历史上的今天
返回首页

历史上的今天

今天是:2024年09月01日(星期日)

正在发生

2018年09月01日 | STM32串口发送数据和接收数据方式总结

2018-09-01 来源:eefocus

之前写了篇关于ESP8266使用AT指令进行互相通讯的实验,在写STM32串口接发数据的程序中,觉得有必要将之前学的有关于串口方面的使用经历加以总结。



串口发送数据:

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不行的。需要加上以下的说明

最后记得还要修改一下选中Code Generation——选中Use MicroLI


串口接收数据:       


串口接收最后应有一定的协议,如发送一帧数据应该有头标志或尾标志,也可两个标志都有。这样在处理数据时既能能保证数据的正确接收,也有利于接收完后我们处理数据。串口的配置在这里就不在赘述,这里我以串口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重新接收数据。这样做的有以下好处:

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

        2.可以接受指定的数据

        3.防止接收的数据使数组越界


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

        

以上的接收形式,是中断一次就接收一个字符,这在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需要朋友们先行了解,下方有参考了解链接。        最后在讲下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发送。

    参考链接:

    https://blog.csdn.net/jdh99/article/details/8444474

    https://blog.csdn.net/phker/article/details/51925668   


有以上的接收方式,对一般的串口数据处理是没有问题的了。下面我讲一下,在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);  

}

}


推荐阅读

史海拾趣

Ava Electronics Corp公司的发展小趣事

AVA电子的创始人凭借对市场的敏锐洞察和对技术的深刻理解,于2004年决定成立这家以IT产品为主的新兴高科技企业。当时,中国的电子行业正迎来一轮发展高潮,而流媒体技术、网络控制技术和多媒体音视频切换及传输技术则被认为是未来行业发展的关键。然而,创业初期,公司面临着资金短缺、人才匮乏以及市场竞争激烈等多重挑战。创始人带领团队,通过不断研发创新产品,积极拓展市场,逐渐在行业中站稳脚跟。

ASC Capacitors公司的发展小趣事

ASC Capacitors的创始人,凭借对电子行业的深厚情感和对电容技术的独到见解,于XXXX年创立了这家公司。他们深知电容在电子行业中的重要性,因此立志要打造一家专业制造高质量电容器的企业。从最初的几间厂房和几名员工,ASC Capacitors凭借坚韧不拔的精神和对技术的执着追求,逐渐在电子行业中崭露头角。

台湾稳态公司的发展小趣事

随着企业实力的增强和产品质量的提升,台湾稳态公司开始积极拓展市场。公司不仅在国内市场取得了良好的销售业绩,还积极开拓海外市场,将产品销往全球多个国家和地区。同时,稳态公司还制定了全球化战略,通过与国际知名企业的合作和交流,不断提升自身的国际竞争力。

电连(ECT)公司的发展小趣事

随着汽车智能化的发展,ECT看到了车载连接器市场的巨大潜力。从2013年开始,公司开始布局车载连接器领域,并成功开发出多款适用于不同车型和场景的车载连接器产品。这一拓展不仅为ECT带来了新的增长点,也进一步巩固了公司在电子连接器行业的领先地位。

Analogic Corporation公司的发展小趣事

ECT在射频连接器领域取得了显著的技术突破。从2006年到2008年,公司开始研发射频同轴连接器,并在2009年实现精密射频同轴连接器的量产,并成功获得专利。这一技术突破为ECT在射频连接器市场赢得了重要地位,也为公司后续的发展奠定了坚实的基础。

Decawave公司的发展小趣事

随着UWB技术的不断发展和应用领域的不断扩展,Decawave不断丰富和完善其产品和解决方案。除了UWB芯片外,公司还推出了与UWB芯片兼容的模块和开发工具,以及针对特定应用场景的解决方案。这些产品和解决方案不仅满足了客户的不同需求,也进一步巩固了Decawave在UWB技术领域的领先地位。

问答坊 | AI 解惑

FM调频发射制作实验

这是一个比较简单的实用型制作,本文打算从简到繁一步步深入,你若是愿意同步动手实验,不久你将能够制作适合正式场合使用的调频发射机。当然,实验还是从最简单的做起,下图是一个最简单的振荡器,它是调频发射的基础。   图中的线圈用1.0 ...…

查看全部问答>

路由器安装

路由器安装技术…

查看全部问答>

帮忙看一个电路

我做的是直流电机控制,H桥这部分的电路不知道对了没有?帮忙看一下!谢谢了…

查看全部问答>

EVC的treectrl控件的成员函数setbkcolor不能用吗(分数不多了 多谢大家帮忙看看)

不知道大家在EVC下编界面程序的时候遇到这个问题没有 m_tree.SetBkColor();编译的时候说不是tree的成员函数 很奇怪? 要设置tree控件的背景颜色 HBRUSH CTreeListDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { HBRUSH hbr = ...…

查看全部问答>

关于Wi-Fi开发

   开发板是君正的开发板,Samsung2440处理器, Wi-Fi模块是Atheros的6002, Wince 5.0的系统。把Wi-Fi模块的驱动移植好后,在不加密的情况下,是很良好的与AP 连接。 但在加密的情况下,输入密钥连接时, 会一直提示,验证,连接不上的情 ...…

查看全部问答>

请问lpc2148的usb virtualcom 功能会占用uart资源吗?

我下了一个lpc2148 usb virtualcom的例程,跑了一下。 里面有两种方式:一种是 VirtuaCom 和 PhysicalCom之间的通讯,另一种是直接VirtualCom的下行通讯点灯和上行通讯,在pc机的串口调试助手可以看到上发的信息提示。 第一种方式,当然是肯定用了 ...…

查看全部问答>

工业上的PLC程控与java有什么区别

请问一下工业上的PLC程控与java有什么区别和联系,两者可以互相替代吗?…

查看全部问答>

哪位高人开启了官方的那个USB_MASS..那个例程的USB双缓冲?

                                 小弟最近测试了下官方的那个USB读写SD卡的例程,发现读写较慢,查到网上可以修改双缓冲,但是和例程对应不上,自己改动 ...…

查看全部问答>

TMS320DM642中文手册

TMS320DM642中文手册 …

查看全部问答>

怎样提高采样频率

我要做一AD采样,平率希望可达到40khz,我选用的是ADC12SC触发,但是频率总只能到达5khz 希望大家帮我想想办法。。。谢谢 /*采样的相关初始化*/ void init_clk() { uint i; BCSCTL1&=~XT2OFF;// do { IFG1&=~OFIFG; ...…

查看全部问答>