STM32串口发送数据和接收数据方式总结
2021-04-07

串口发送数据

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

void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);

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

void Send_data(u8 *s) {
 while(*s!='\0')
 { 
  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!='\0')
 { 
  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\r\n",Uart2_Buffer);        //这里我做打印数据处理     Uart2_Rx=0;                                   
   } 
   else    {
    Uart2_Rx=0;                                   //不是我们需要的数据或者达到最大接收数则开始重新接收    }
  }
 }
}

数据的头标识为“\n”既换行符,尾标识为“+”。该函数将串口接收的数据存放在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中断可查看:

最后在讲下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\r\n",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

声明: 本文转载自其它媒体或授权刊载,目的在于信息传递,并不代表本站赞同其观点和对其真实性负责,如有新闻稿件和图片作品的内容、版权以及其它问题的,请联系我们及时删除。(联系我们,邮箱:evan.li@aspencore.com )
0
评论
热门推荐
  • 相关技术文库
  • 单片机
  • 嵌入式
  • MCU
  • STM
  • 智能化交通信号机解决方案

    面向交通信号灯行业,ZLG推出智能化交通信号机解决方案,该方案可大幅提升设备的智能化和道路使用率,改善道路拥堵,打造高效畅通的“智慧交通”。   行业背景 据公安部统计,2020年,全国机动车保有量达3.72亿辆,其中汽车2.81亿辆。百度地图发布的《2020年度

    昨天
  • 单片机如何实现Bootloader?

    去某新能源大厂出了一次差,这次出差是为了升级程序解决Bug,需要给单片机重新烧录.hex文件,用户已经将产品封装起来,无法开盖,只能使用CAN总线来更新程序,用Bootloader实现。其实就是通过上位机把.bin/hex文件以CAN通讯的方式发送给单片机并存储在规定的F

    04-13
  • 单片机应用系统的开发流程

    我们学习单片机的目的就是为了进行嵌入式系统的开发,学好单片机首先要有一个整体认识,下面将简要介绍一下单片机应用系统的开发流程,如图1所示。 图1 单片机系统开发流程 (1)明确任务 分析和了解项目的总体要求,并综合考虑系统使用环境、可靠性要求、可维

    04-12
  • 太经典了!用最少的IO口,扫最多的键

    在做项目(工程)的时候,我们经常要用到比较多的按键,而且IO资源紧张,于是我们就想方设法地在别的模块中节省IO口,好不容易挤出一两个IO口,却发现仍然不够用,实在没办法了就添加一个IC来扫键。一个IC虽然价格不高,但对于大批量生产而且产品利润低的厂家

    03-30
  • 单片机的学习方法和步骤

    注 | 文末留言有福利 作为一名电子技术从业人员,你学过单片机吗?你会运用单片机吗? 我想你一定学过,但不一定会运用。 因为学习单片机比学习其他学科需要付出更多的努力和代价,不仅要学习理论知识还要练习实际操作,而且主要是在实际操作中才能真正学到单

    03-25
  • 单片机内存的分配

    单片机执行指令过程详解 单片机执行程序的过程,实际上就是执行我们所编制程序的过程。即逐条指令的过程。计算机每执行一条指令都可分为三个阶段进行。即取指令-----分析指令-----执行指令。 取指令的任务是:根据程序计数器PC中的值从程序存储器读出现行指令

    03-22
  • 几种常用单片机之间的通信方式

    越来越多的功能各异的单片机为我们的设计提供了许多新的方法与思路。对于莫一些场合,比如:复杂的后台运算及通信与高实时性前台控制系统、软件资源消耗大的系统、功能强大的低消耗系统、加密系统等等。如果合理使用多种不同类型的单片机组合设计,可以得到极

    03-17
  • STM32F4的总线架构和STM8的中断控制

    STM32F4的总线架构 总线架构    DMA: Direct Memory Access,直接内存存取。    八条主控总线: Cortex-M4 内核I总线,D总线和S总线; DMA1存储器总线,DMA2存储器总线; DMA2外设总线; 以太网DMA总线; USB OTG HS DMA总线。 七条被控总线: 内部FLASH ICo

    03-17
  • 单片机P0口必须加上上拉电阻?

    在我们刚一开始接触到51单片机的时候对P0口必须加上上拉电阻,否则P0就是高阻态。 对这个问题可能感到疑惑,为什么是高阻态?加上拉电阻?今天针对这一概念进行简单讲解。 高阻态 高阻态这是一个数字电路里常见的术语,指的是电路的一种输出状态,既不是高电

    03-15
  • 单片机51系列定时器实用方法

    第一部分:51系列定时器 定时/计数器 0 和定时/计数器1都有4种定时模式。 16位定时器对内部机器周期进行技术,机器周期加1,定时器值加1,1MHZ模式下,一个机器周期为1us 。 定时器工作模式寄存器TMOD,不可位寻址,需整体赋值,高4位用于定时器1,第四位用于

    03-12
  • 如何成为一名超强的51单片机高手

    很多初学者最初接触单片机时较为苦恼的就是如何入门,也就是从哪一部份,按照怎样的步骤进行学习。在摸索学习步骤的过程浪费时间的同时也会造成学习兴趣的降低。为了帮助大家解决这种情况。 第一步:基础理论知识学习 基础理论知识包括模拟电路、数字电路和C

    03-10
  • 纯干货,PSI 原理解析与应用

    一、什么是 PSI Pressure Stall Information 提供了一种评估系统资源压力的方法。系统有三个基础资源:CPU、Memory 和 IO,无论这些资源配置如何增加,似乎永远无法满足软件的需求。一旦产生资源竞争,就有可能带来延迟增大,使用户体验到卡顿。 如果没有一种

    03-09
下载排行榜
更多
广告
X
广告