STM32串口发送数据和接收数据方式总结_stm32串口fifo收发程序,2024年最新源码解析

279 阅读7分钟
			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不行的。需要加上以下的重映射函数


![](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f983d39c3e2c423b86f74ab2a9530b9e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo5a2m5Lmg5LmL5b-DQUk=:q75.awebp?rk3s=f64ab15b&x-expires=1771856419&x-signature=EajSdwHakKxtfyc%2F%2BZsAisSObN4%3D)


       如果不想添加以上代码,也可以勾选以下的Use MicroLI选项来支持printf函数使用。![](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/40cf40e28e0d4492b911ca8c4c6e825e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo5a2m5Lmg5LmL5b-DQUk=:q75.awebp?rk3s=f64ab15b&x-expires=1771856419&x-signature=Edruti7QUr3L%2BI1hdWW2vRWKp5U%3D)


 


串口接收数据:       


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


    参考链接:


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


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


   最后在讲下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);				 
}

}


 


 



![img](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7bd804de6c19454b9bf7d7672eacbce0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo5a2m5Lmg5LmL5b-DQUk=:q75.awebp?rk3s=f64ab15b&x-expires=1771856419&x-signature=KhlcRJUK0iOlOs9DdglkpvuReC0%3D)
![img](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/69795764cf854ddd878c7b2f4bdc59f9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo5a2m5Lmg5LmL5b-DQUk=:q75.awebp?rk3s=f64ab15b&x-expires=1771856419&x-signature=0R90peKqCW%2Br9dlcRcQ40c4ak9g%3D)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://gitee.com/vip204888)**