STM32串口发送数据和接收数据方式总结

大鱼机器人 2020-12-24 00:00

关注、星标公众号 直达精彩内容

串口发送数据

1、串口发送数据最直接的方式就是标准调用库函数 。

void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);

第一个参数是发送的串口号,第二个参数是要发送的数据了。但是用过的朋友应该觉得不好用,一次只能发送单个字符,所以我们有必要根据这个函数加以扩展:

void Send_data(u8 *s)
{
 while(*s!='')
 { 
  while(USART_GetFlagStatus(USART1,USART_FLAG_TC )==RESET); 
  USART_SendData(USART1,*s);
  s++;
 }
}

以上程序的形参就是我们调用该函数时要发送的字符串,这里通过循环调用USART_SendData来一 一发送我们的字符串。

while(USART_GetFlagStatus(USART1,USART_FLAG_TC )==RESET);

这句话有必要加,他是用于检查串口是否发送完成的标志,如果不加这句话会发生数据丢失的情况。这个函数只能用于串口1发送。有些时候根据需要,要用到多个串口发送那么就还需要改进这个程序。如下:

void Send_data(USART_TypeDef * USARTx,u8 *s)
{
 while(*s!='')
 { 
  while(USART_GetFlagStatus(USARTx,USART_FLAG_TC )==RESET); 
  USART_SendData(USARTx,*s);
  s++;
 }
}

这样就可实现任意的串口发送。但有一点,我在使用实时操作系统的时候(如UCOS,Freertos等),需考虑函数重入的问题。

当然也可以简单的实现把该函数复制一下,然后修改串口号也可以避免该问题。然而这个函数不能像printf那样传递多个参数,所以还可以在改进,最终程序如下:

void USART_printf ( USART_TypeDef * USARTx, char * Data, ... )
{
 const char *s;
 int d;   
 char buf[16];
 
 va_list ap;
 va_start(ap, Data);
 
 while ( * Data != 0 )     // 判断是否到达字符串结束符
 {                              
  if ( * Data == 0x5c )  //''
  {           
   switch ( *++Data )
   {
    case 'r':                 //回车符
    USART_SendData(USARTx, 0x0d);
    Data ++;
    break;
 
    case 'n':                 //换行符
    USART_SendData(USARTx, 0x0a); 
    Data ++;
    break;
 
    default:
    Data ++;
    break;
   }    
  }
  
  else if ( * Data == '%')
  {           //
   switch ( *++Data )
   {    
    case 's':            //字符串
    s = va_arg(ap, const char *);
    
    for ( ; *s; s++) 
    {
     USART_SendData(USARTx,*s);
     while( USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET );
    }
    
    Data++;
    
    break;
 
    case 'd':   
     //十进制
    d = va_arg(ap, int);
    
    itoa(d, buf, 10);
    
    for (s = buf; *s; s++) 
    {
     USART_SendData(USARTx,*s);
     while( USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET );
    }
    
    Data++;
    
    break;
    
    default:
    Data++;
    
    break;
    
   }   
  }
  
  else USART_SendData(USARTx, *Data++);
  
  while ( USART_GetFlagStatus ( USARTx, USART_FLAG_TXE ) == RESET );
  
 }
}

该函数就可以像printf使用可变参数,方便很多。通过观察函数但这个函数只支持了%d,%s的参数,想要支持更多,可以仿照printf的函数写法加以补充。

2、 直接使用printf函数。

很多朋友都知道想要STM32要直接使用printf不行的。需要加上以下的重映射函数:

如果不想添加以上代码,也可以勾选以下的Use MicroLI选项来支持printf函数使用:

相关笔记:串口打印知多少?

串口接收数据

串口接收最后应有一定的协议,如发送一帧数据应该有头标志或尾标志,也可两个标志都有。

这样在处理数据时既能能保证数据的正确接收,也有利于接收完后我们处理数据。串口的配置在这里就不在赘述,这里我以串口2接收中断服务程序函数且接收的数据包含头尾标识为例。

#define Max_BUFF_Len 18
unsigned char Uart2_Buffer[Max_BUFF_Len];
unsigned int Uart2_Rx=0;
void USART2_IRQHandler() 
{
 if(USART_GetITStatus(USART2,USART_IT_RXNE) != RESET) //中断产生 
 {
  USART_ClearITPendingBit(USART2,USART_IT_RXNE); //清除中断标志
    
  Uart2_Buffer[Uart2_Rx] = USART_ReceiveData(USART2);     //接收串口1数据到buff缓冲区
  Uart2_Rx++; 
        
  if(Uart2_Buffer[Uart2_Rx-1] == 0x0a || Uart2_Rx == Max_BUFF_Len)    //如果接收到尾标识是换行符(或者等于最大接受数就清空重新接收)
  {
   if(Uart2_Buffer[0] == '+')                      //检测到头标识是我们需要的 
   {
    printf("%s
"
,Uart2_Buffer);        //这里我做打印数据处理
    Uart2_Rx=0;                                   
   } 
   else
   {
    Uart2_Rx=0;                                   //不是我们需要的数据或者达到最大接收数则开始重新接收
   }
  }
 }
}

数据的头标识为“ ”既换行符,尾标识为“+”。该函数将串口接收的数据存放在USART_Buffer数组中,然后先判断当前字符是不是尾标识,如果是说明接收完毕,然后再来判断头标识是不是“+”号,如果还是那么就是我们想要的数据,接下来就可以进行相应数据的处理了。但如果不是那么就让Usart2_Rx=0重新接收数据。

这样做的有以下好处:

  • 可以接受不定长度的数据,最大接收长度可以通过Max_BUFF_Len来更改

  • 可以接受指定的数据

  • 防止接收的数据使数组越界

这里我的把接受正确数据直接打印出来,也可以通过设置标识位,然后在主函数里面轮询再操作。

以上的接收形式,是中断一次就接收一个字符,这在UCOS等实时内核系统中频繁的中断,非常消耗CPU资源,在有些时候我们需要接收大量数据时且波特率很高的情况下,长时间中断会带来一些额外的问题。

所以以DMA形式配合串口的IDLE(空闲中断)来接受数据将会大大的提高CPU的利用率,减少系统资源的消耗。首先还是先看代码。

#define DMA_USART1_RECEIVE_LEN 18
void USART1_IRQHandler(void)                                 
{     
    u32 temp = 0;  
    uint16_t i = 0;  
      
    if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)  
    {  
        USART1->SR;  
        USART1->DR; //这里我们通过先读SR(状态寄存器)和DR(数据寄存器)来清USART_IT_IDLE标志    
        DMA_Cmd(DMA1_Channel5,DISABLE);  
        temp = DMA_USART1_RECEIVE_LEN - DMA_GetCurrDataCounter(DMA1_Channel5); //接收的字符串长度=设置的接收长度-剩余DMA缓存大小 
        for (i = 0;i < temp;i++)  
        {  
            Uart2_Buffer[i] = USART1_RECEIVE_DMABuffer[i];  
                
        }  
        //设置传输数据长度  
        DMA_SetCurrDataCounter(DMA1_Channel5,DMA_USART1_RECEIVE_LEN);  
        //打开DMA  
        DMA_Cmd(DMA1_Channel5,ENABLE);  
    }        

之前的串口中断是一个一个字符的接收,现在改为串口空闲中断,就是一帧数据过来才中断进入一次。而且接收的数据时候是DMA来搬运到我们指定的缓冲区(也就是程序中的USART1_RECEIVE_DMABuffer数组),是不占用CPU时间资源的。

关于IDLE中断可查看:STM32串口空闲中断接收不定长数据(DMA方式)

最后在讲下DMA的发送:

#define DMA_USART1_SEND_LEN 64
void DMA_SEND_EN(void)
{
 DMA_Cmd(DMA1_Channel4, DISABLE);      
 DMA_SetCurrDataCounter(DMA1_Channel4,DMA_USART1_SEND_LEN);   
 DMA_Cmd(DMA1_Channel4, ENABLE);
}

这里需要注意下DMA_Cmd(DMA1_Channel4,DISABLE)函数需要在设置传输大小之前调用一下,否则不会重新启动DMA发送。

有了以上的接收方式,对一般的串口数据处理是没有问题的了。下面再讲一下,在ucosiii中我使用信号量+消息队列+储存管理的形式来处理我们的串口数据。先来说一下这种方式对比其他方式的一些优缺点。

一般对串口的处理形式是"生产者"和"消费者"的模式,即本次接收的数据要马上处理,否则当数据大量涌进的时候,就来不及"消费"掉生产者(串口接收中断)的数据,那么就会丢失本次的数据处理。所以使用队列就能够很方便的解决这个问题。

在下面的程序中,对数据的处理是先接受,在处理,如果在处理的过程中,有串口中断接受数据,那么就把它依次放在队列中,队列的特征是先进先出,在串口中就是先处理先接受的数据,所以根据生产和消费的速度,定义不同大小的消息队列缓冲区就可以了。缺点就是太占用系统资源,一般51单片机是没可能了。下面是从我做的项目中截取过来的程序:

OS_MSG_SIZE  Usart1_Rx_cnt;          //字节大小计数值
unsigned char Usart1_data;           //每次中断接收的数据
unsigned char* Usart1_Rx_Ptr;        //储存管理分配内存的首地址的指针
unsigned char* Usart1_Rx_Ptr1;       //储存首地址的指针

void USART1_IRQHandler() 
{
 OS_ERR err;
 OSIntEnter();
 
  if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE) != RESET) //中断产生 
  {   
    USART_ClearFlag(USART1, USART_FLAG_RXNE);     //清除中断标志
  
    Usart1_data = USART_ReceiveData(USART1);     //接收串口1数据到buff缓冲区
  
  if(Usart1_data =='+')                     //接收到数据头标识
  {
//   OSSemPend((OS_SEM*  )&SEM_IAR_UART,  //这里请求信号量是为了保证分配的存储区,但一般来说不允许
//   (OS_TICK  )0,                   //在终端服务函数中调用信号量请求但因为
//   (OS_OPT   )OS_OPT_PEND_NON_BLOCKING,//我OPT参数设置为非阻塞,所以可以这么写
//   (CPU_TS*  )0,
//   (OS_ERR*  )&err); 
//   if(err==OS_ERR_PEND_WOULD_BLOCK)     //检测到当前信号量不可用
//   {
//     printf("error");
//   }    
   Usart1_Rx_Ptr=(unsigned char*) OSMemGet((OS_MEM*)&UART1_MemPool,&err);//分配存储区
   Usart1_Rx_Ptr1=Usart1_Rx_Ptr;          //储存存储区的首地址
  }
  if(Usart1_data == 0x0a )       //接收到尾标志
  {                    
   *Usart1_Rx_Ptr++=Usart1_data;
   Usart1_Rx_cnt++;                         //字节大小增加
   OSTaskQPost((OS_TCB    *  )&Task1_TaskTCB,
                                   (void      *  )Usart1_Rx_Ptr1,    //发送存储区首地址到消息队列
                                   (OS_MSG_SIZE  )Usart1_Rx_cnt,
                                   (OS_OPT       )OS_OPT_POST_FIFO,  //先进先出,也可设置为后进先出,再有地方很有用
                                   (OS_ERR    *  )&err);
         
   Usart1_Rx_Ptr=NULL;          //将指针指向为空,防止修改
   Usart1_Rx_cnt=0;      //字节大小计数清零
  }
  else
  {
   *Usart1_Rx_Ptr=Usart1_data; //储存接收到的数据
   Usart1_Rx_Ptr++;
   Usart1_Rx_cnt++;
  } 
 }    
 OSIntExit();
}

上面被注释掉的代码为我是为了防止当分区中没有空闲的存储块时加入信号量,打印出报警信息。当然我们也可以将存储块直接设置大一点,但是还是无法避免当没有可有存储块时会程序会崩溃现象。希望懂的朋友能告知下~。

下面是串口数据处理任务,这里删去了其他代码,只把他打印出来了而已。

void task1_task(void *p_arg)
{
 OS_ERR err;
 OS_MSG_SIZE Usart1_Data_size;
 u8 *p;
 
 while(1)
 {
  p=(u8*)OSTaskQPend((OS_TICK  )0//请求消息队列,获得储存区首地址
   (OS_OPT    )OS_OPT_PEND_BLOCKING,
   (OS_MSG_SIZE* )&Usart1_Data_size,
   (CPU_TS*   )0,
   (OS_ERR*   )&err);
 
  printf("%s
"
,p);        //打印数据
 
  delay_ms(100);
  OSMemPut((OS_MEM* )&UART1_MemPool,    //释放储存区
  (void*   )p,
  (OS_ERR*  )&err);
       
  OSSemPost((OS_SEM* )&SEM_IAR_UART,    //释放信号量
  (OS_OPT  )OS_OPT_POST_NO_SCHED,
  (OS_ERR* )&err);
       
  OSTimeDlyHMSM(0,0,1,500,OS_OPT_TIME_PERIODIC,&err);     
 }
}


作者:可以吃的鱼

原文:https://blog.csdn.net/qq_35281599

免责声明:本文来源网络,免费传达知识,版权归原作者所有。如涉及作品版权问题,请联系我进行删除。

-END-

整理文章为传播相关技术,版权归原作者所有 |
| 如有侵权,请联系删除 |

往期好文合集


干货 | 怎么能学好嵌入式Linux ?

状态机思路在嵌入式开发中的应用详解

知乎热议:嵌入式开发中C++好用吗?

  最 后   
 

若觉得文章不错,转发分享,也是我们继续更新的动力。
5T资源大放送!包括但不限于:C/C++,Linux,Python,Java,PHP,人工智能,PCB、FPGA、DSP、labview、单片机、等等
在公众号内回复「更多资源」,即可免费获取,期待你的关注~

大鱼机器人 一个专注于机器人技术,单片机,嵌入式系统,智能家居,智能设备,PCB设计,IT最新动态的自媒体。此外,还有海量学习资源等你来领取。作者:张巧龙,个人微信号:well_xiaolong。欢迎关注公众号,名称:大鱼机器人,公众号ID:All_best_xiaolong
评论
  • Ubuntu20.04默认情况下为root账号自动登录,本文介绍如何取消root账号自动登录,改为通过输入账号密码登录,使用触觉智能EVB3568鸿蒙开发板演示,搭载瑞芯微RK3568,四核A55处理器,主频2.0Ghz,1T算力NPU;支持OpenHarmony5.0及Linux、Android等操作系统,接口丰富,开发评估快人一步!添加新账号1、使用adduser命令来添加新用户,用户名以industio为例,系统会提示设置密码以及其他信息,您可以根据需要填写或跳过,命令如下:root@id
    Industio_触觉智能 2025-01-17 14:14 128浏览
  • 高速先生成员--黄刚这不马上就要过年了嘛,高速先生就不打算给大家上难度了,整一篇简单但很实用的文章给大伙瞧瞧好了。相信这个标题一出来,尤其对于PCB设计工程师来说,心就立马凉了半截。他们辛辛苦苦进行PCB的过孔设计,高速先生居然说设计多大的过孔他们不关心!另外估计这时候就跳出很多“挑刺”的粉丝了哈,因为翻看很多以往的文章,高速先生都表达了过孔孔径对高速性能的影响是很大的哦!咋滴,今天居然说孔径不关心了?别,别急哈,听高速先生在这篇文章中娓娓道来。首先还是要对各位设计工程师的设计表示肯定,毕竟像我
    一博科技 2025-01-21 16:17 115浏览
  •  万万没想到!科幻电影中的人形机器人,正在一步步走进我们人类的日常生活中来了。1月17日,乐聚将第100台全尺寸人形机器人交付北汽越野车,再次吹响了人形机器人疯狂进厂打工的号角。无独有尔,银河通用机器人作为一家成立不到两年时间的创业公司,在短短一年多时间内推出革命性的第一代产品Galbot G1,这是一款轮式、双臂、身体可折叠的人形机器人,得到了美团战投、经纬创投、IDG资本等众多投资方的认可。作为一家成立仅仅只有两年多时间的企业,智元机器人也把机器人从梦想带进了现实。2024年8月1
    刘旷 2025-01-21 11:15 560浏览
  • 本文介绍瑞芯微开发板/主板Android配置APK默认开启性能模式方法,开启性能模式后,APK的CPU使用优先级会有所提高。触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。源码修改修改源码根目录下文件device/rockchip/rk3562/package_performance.xml并添加以下内容,注意"+"号为添加内容,"com.tencent.mm"为AP
    Industio_触觉智能 2025-01-17 14:09 173浏览
  • 数字隔离芯片是一种实现电气隔离功能的集成电路,在工业自动化、汽车电子、光伏储能与电力通信等领域的电气系统中发挥着至关重要的作用。其不仅可令高、低压系统之间相互独立,提高低压系统的抗干扰能力,同时还可确保高、低压系统之间的安全交互,使系统稳定工作,并避免操作者遭受来自高压系统的电击伤害。典型数字隔离芯片的简化原理图值得一提的是,数字隔离芯片历经多年发展,其应用范围已十分广泛,凡涉及到在高、低压系统之间进行信号传输的场景中基本都需要应用到此种芯片。那么,电气工程师在进行电路设计时到底该如何评估选择一
    华普微HOPERF 2025-01-20 16:50 81浏览
  •     IPC-2581是基于ODB++标准、结合PCB行业特点而指定的PCB加工文件规范。    IPC-2581旨在替代CAM350格式,成为PCB加工行业的新的工业规范。    有一些免费软件,可以查看(不可修改)IPC-2581数据文件。这些软件典型用途是工艺校核。    1. Vu2581        出品:Downstream     
    电子知识打边炉 2025-01-22 11:12 90浏览
  •  光伏及击穿,都可视之为 复合的逆过程,但是,复合、光伏与击穿,不单是进程的方向相反,偏置状态也不一样,复合的工况,是正偏,光伏是零偏,击穿与漂移则是反偏,光伏的能源是外来的,而击穿消耗的是结区自身和电源的能量,漂移的载流子是 客席载流子,须借外延层才能引入,客席载流子 不受反偏PN结的空乏区阻碍,能漂不能漂,只取决于反偏PN结是否处于外延层的「射程」范围,而穿通的成因,则是因耗尽层的过度扩张,致使跟 端子、外延层或其他空乏区 碰触,当耗尽层融通,耐压 (反向阻断能力) 即告彻底丧失,
    MrCU204 2025-01-17 11:30 191浏览
  • 嘿,咱来聊聊RISC-V MCU技术哈。 这RISC-V MCU技术呢,简单来说就是基于一个叫RISC-V的指令集架构做出的微控制器技术。RISC-V这个啊,2010年的时候,是加州大学伯克利分校的研究团队弄出来的,目的就是想搞个新的、开放的指令集架构,能跟上现代计算的需要。到了2015年,专门成立了个RISC-V基金会,让这个架构更标准,也更好地推广开了。这几年啊,这个RISC-V的生态系统发展得可快了,好多公司和机构都加入了RISC-V International,还推出了不少RISC-V
    丙丁先生 2025-01-21 12:10 198浏览
  • 现在为止,我们已经完成了Purple Pi OH主板的串口调试和部分配件的连接,接下来,让我们趁热打铁,完成剩余配件的连接!注:配件连接前请断开主板所有供电,避免敏感电路损坏!1.1 耳机接口主板有一路OTMP 标准四节耳机座J6,具备进行音频输出及录音功能,接入耳机后声音将优先从耳机输出,如下图所示:1.21.2 相机接口MIPI CSI 接口如上图所示,支持OV5648 和OV8858 摄像头模组。接入摄像头模组后,使用系统相机软件打开相机拍照和录像,如下图所示:1.3 以太网接口主板有一路
    Industio_触觉智能 2025-01-20 11:04 169浏览
  • 2024年是很平淡的一年,能保住饭碗就是万幸了,公司业绩不好,跳槽又不敢跳,还有一个原因就是老板对我们这些员工还是很好的,碍于人情也不能在公司困难时去雪上加霜。在工作其间遇到的大问题没有,小问题还是有不少,这里就举一两个来说一下。第一个就是,先看下下面的这个封装,你能猜出它的引脚间距是多少吗?这种排线座比较常规的是0.6mm间距(即排线是0.3mm间距)的,而这个规格也是我们用得最多的,所以我们按惯性思维来看的话,就会认为这个座子就是0.6mm间距的,这样往往就不会去细看规格书了,所以这次的运气
    wuliangu 2025-01-21 00:15 204浏览
  • 临近春节,各方社交及应酬也变得多起来了,甚至一月份就排满了各式约见。有的是关系好的专业朋友的周末“恳谈会”,基本是关于2025年经济预判的话题,以及如何稳定工作等话题;但更多的预约是来自几个客户老板及副总裁们的见面,他们为今年的经济预判与企业发展焦虑而来。在聊天过程中,我发现今年的聊天有个很有意思的“点”,挺多人尤其关心我到底是怎么成长成现在的多领域风格的,还能掌握一些经济趋势的分析能力,到底学过哪些专业、在企业管过哪些具体事情?单单就这个一个月内,我就重复了数次“为什么”,再辅以我上次写的:《
    牛言喵语 2025-01-22 17:10 85浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦