STM32的UART串口通信_串口应用拓展_DMA/串口重定向/解析字符串/错误解决
DMA
DMA原理
-
数据传输方式
使用中断方式进行串口通信时每接收或发送一个字节都需要进行一次中断,当通信速率较高、数据量较大时,建议使用DMA方式避免频繁中断阻碍其他工作
在中断一章开头中简单介绍过DMA,简而言之就是既不需要像轮询方式一样CPU控制也不需要像中断那样保护与恢复现场,而是通过硬件(DMA控制器)在外设和储存器之间开辟一条道路的数据传输方式,在接受或发送完指定数量的数据后才产生一次DMA中断,通知CPU进行处理
可以发现,空闲中断接收的原理与DMA的设计不谋而合,都是不再每接收一个数据就进一次中断,而是不停接收数据,在之后统一进中断,我们可以使用DMA方式来配合空闲中断实现不定长数据接收
-
DMA的要素
实现DMA有四个要素:
- 传输源与传输目标:将连接两者的数据通路称为通道或数据流,设置相应的通道即决定了数据传输的方向:外设到储存器、储存器到外设、以及储存器到储存器
- 传输数量:要发送的字节数
- 触发信号:启动一次DMA数据传输的动作
-
DMA的初始化配置
使用DMA方式进行UART通信,除了按照一般流程配置UART(参数同使用轮询方式的UART)外,对于接下来使用DMA进行不定长数据的接收,还需要在DMA Settings标签页对DMA进行如下配置:
-
点击ADD按钮,在出现的Select菜单中选择USARTx_RX,添加串口发送的DMA数据流(具体使用哪一个由DMA控制器和CubeMX自动分配),数据传输方向和DMA优先级使用默认配置
-
设置DMA参数:
-
Mode:DMA工作模式,分为Normal(单次传输,传输完成后需要再次启动DMA才能再次传输数据)和Circular(连续传输,传输完成后会将传输数据的数量重置为初值,数据缓冲区指向首地址,只要有数据就会继续传输)
因为我们要接收的数据不定长,需要在接收完成后对数据进行处理,获取字节数,所以选用Normal模式
-
Increment Address:设置地址递增:所谓地址用于标识DMA传输中的待传输数据的地址:外设是单一设备,地址固定,无需递增(因此peripheral的方框是无法勾选的);而存储器中包含多个存储单元,地址自动递增可以自动指向下一个待传输数据的地址
-
Data Width:设置数据宽度,有byte/half word(2byte)/word(4byte)三种选择,串口一次只能收发一字节,选byte
-
Use FIFO:是否使能FIFO:FIFO可以看作DMA传输过程的缓冲区,一般不使能
-
-
使能UART全局中断以能触发IDLE空闲中断,DMA数据流中断使能由CubeMX软件自动勾选
-
DMA方式UART的使用
-
串口DMA方式发送函数HAL_UART_Transmit_DMA
在发送完成后,同样调用串口发送中断回调函数
HAL_UART_TxCpltCallbackHAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) /* UART_HandleTypeDef *huart:uart的句柄 // uint8_t *pData:待发送数据所存放的地址 // uint16_t Size:需要发送的字节数 */ -
串口DMA方式接收函数HAL_UART_Receive_DMA
在接收完成后,同样调用串口接受中断回调函数
HAL_UART_RxCpltCallbackHAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) /* UART_HandleTypeDef *huart:uart的句柄 // uint8_t *pData:接收到数据所存放的地址 // uint16_t Size:需接收的字节数 */ -
串口DMA传输停止函数HAL_UART_DMAStop
同时停止DMA方式下串口接收数据流与串口发送数据流
HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart) -
DMA数据项数读取函数
用于获取DMA数据流中未传输数据的项数,也即读取寄存器
DMA_SxNDTR的内容,每传输一个数据,该寄存器的值减1,例如我们设置DMA接收100个数据项,如果现在已经接收了20个,那么该寄存器值为80__HAL_DMA_GET_COUNTER(__HANDLE__) -
DMA方式收发的代码实现
使用DMA方式来配合空闲中断实现不定长数据接收
//因为使用的是DMA方式,所以不用前文介绍的串口空闲中断接收函数来开中断 extern volatile uint8_t RxFlag;//由于没有中断,需要一个标志位判断接收是否完成 void USART1_IQRHandler(void) { HAL_UART_IRQHandler(&huart1); /*USER CODE BEGIN USART1_IRQn1*/ //在中断处理函数中添加对空闲中断的处理,并对标志位进行处理 if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE)!=RESET) { __HAL_UART_CLEAR_IDLEFLAG(&huart2); RxFlag=1; } } MX_DMA_Init(); __HAL_UART_ENBLE_IT(&huart1,UART_IT_IDLE);//开启空闲中断 HAL_UART_Receive_DMA(&huart2,(uint8_t *)RxBuffer,LENGTH);//开启DMA接收 //LENGTH是接收缓冲区长度,使用宏定义避免出现魔法数字 while(1) { if(RxFlag) { RxFlag=0; HAL_UART_DMAStop(&huart1); RxCount=LENGTH - __HAL_DMA_GET_COUNTER(__HANDLE__); //停止DMA接收,已经接收的数据项数=数据总量-DMA数据流中剩余未接受数据项数 //此处可以对RxBuffer的数据进行处理 for(uint8_t i = 0; i<RxCount; i++) { RxBuffer[i]=0;//清空数据缓冲区 } RxCount=0; HAL_UART_Receive_DMA(&huart2,(uint8_t *)RxBuffer,LENGTH);//重启传输 } }
UART的应用
串口重定向
-
输入与输出重定向到串口
对于C语言开发,有标准输入输出:
printf将信息发送到显示屏上,也有scanf读取键盘输入的信息而在嵌入式系统开发中,很可能没有显示器和键盘,这就需要把
printf的输出和scanf的输入重新定向到串口,让串口作为嵌入式系统的输入输出 -
串口重定向代码修改
修改之前的例子,依旧是接收到
LED0时,点亮LED1并通过UART发送LED1 Open!,接收到LEDC时,熄灭LED1并发送LED1 Close!,但将接收与发送字符从使用串口助手直接进行串口通信改为使用C语言的控制台首先写出程序的工作逻辑
#include "main.h" #include "string.h" //主函数 char rx_buf[20]; if(scanf("%s",rx_buf)==1)//将输入保存到rx_buf中,当接收到有效输入时 { if(strcmp(rx_buf,"LEDO")==0) { ucled=0x01;//开灯 printf("LED1 Open!\r\n"); } else if(strcmp(rx_buf,"LEDC")==0) { ucled=0x00;//关灯 printf("LED1 Close!\r\n"); } } -
现在将上文程序中用到
printf和scanf重定向到串口,我们重写fputc和fgetc函数来实现这一点//串口的重定向函数 int fputc(int ch, FILE *f) //文件指针用不到,只是为了保证自己定义的fputc函数和库函数原本的fputc一致 { //原本的fputc是将一个字符显示到屏幕,改为串口发送1字节数据 //int类型的ch需要强制类型转换,等待时间无限 HAL_UART_Transmit(&huart1,(uint8_t *)&ch,1,HAL_MAX_DELAY); return ch; } int fgetc(FILE *f) { //原本的fgetc是从键盘接收一个字符,现在改为从串口接收1字节数据 uint8_t ch; HAL_UART_Receive(&huart1,(uint8_t *)&ch,1,HAL_MAX_DELAY); return ch; }
通信协议
-
数据帧与通信协议
前文提到,UART是一帧一帧地发生数据,每一帧中包含多个比特,不同格式的数据帧不同位置的每个比特有不同作用(起始、数据、校验、结束)
而这样的过程仅仅是完成了数据的收发,如果想要进行信息的传递,就要用到通信协议
通信协议也以帧为单位,每一帧由多个字符(字节)组成,不同字段的字符代表不同的含义(起始符、设备地址、功能代码……),在使用串口接收这一帧的数据后,按通信协议,进行解析,执行对应操作
通信协议就像是机器之间沟通的语言,与人之间交流对比:其他人说完一句话(发送完一帧数据),按照一句话的主谓宾结构分析各个位置的词语是什么意思(按照通信协议解析数据帧),最后根据话的意思做相应的事情
以工控领域常用的通信协议Modbus为例:
起始符 设备地址 功能代码 数据 检验 结束符 1字符 2字符 1字符 n字符 2字符 1字符 -
自定义通信协议
依葫芦画瓢,我们也可以设计自己的通信协议:
起始符 设备地址 功能代码 帧尾 0xaa 1字符 1字符 0x55 当接收到0xaa时,数据帧开始,接下来的一个字符指代设备(设定0x01表示LED1),再接下来的一个字符是对设备的操作(设定0x00为关灯,0x01为开灯),如果接收到了0x55表示这一帧结束了
-
通信协议的解析
按照我们设计的通信协议,编写解析一帧数据的代码
如果要增加新的设备、新的操作,只要在相应的地方增加
if就行了HAL_UART_Receive_IT(&huart1,&rx_data,1); void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { rx_buf[rx_count++]=rx_data; HAL_UART_Receive_IT(&huart1,&rx_data,1); } void Usart_Proc(void) { if(rx_count==4)//当接收到4个字符时开始判断 { if(rx_buf[0]==0xaa && rx_buf[3]==0x55)//判断帧头帧尾 { if(rx_buf[1]==0x01)//判断设备码 { if(rx_buf[2]==0x00) {/*关灯*/} else if(rx_buf[2]==0x01) {/*开灯*/} else {/*error,可以设置一个表示出错的标志位,其他的else同理,省略*/} } } } }
解析接收的字符串
-
sscanf函数
sscanf()和scanf()之间的区别就好比之前介绍的sprintf()和printf()sprintf()是把提供的参数经过格式化后输出到给定的字符串中,sscanf()则是反向操作,把输入的字符串去掉格式字符,取出其中的参数存放到给定的变量中int sscanf(const char* str, const char* format, ……); /* str:要解析的输入字符串。 format:格式字符串,指定要匹配的格式规则 后面还有可变参数列表,用于接收解析后的数据 */ -
通过串口发送解析后的字符串
输入指定格式的字符串,解析出其中的参数
案例:一个停车场管理系统,输入停车类型:车辆编号:进入时间(YYMMDDHHmmSS),将这三种数据解析出来,比如输入CNBR:A392:200202120000,解析后得到停车类型为CNBR,停车编号为A392,进入停车场时间为2020年2月2日12点整
void Usart_Proc(void) { if(rx_count==22)//记录接收字符数的变量 { sscanf(rx_buf,"%4s:%4s:%12s",car_type,car_num,car_time); //从UART输入到rx_buf,再解析到到car_type,car_num,car_time中 sprintf(tx_buf,"car_type:%4s\r\n",car_type); HAL_UART_Transmit(&huart1,(uint8_t*)tx_buf,strlen(tx_buf),50); sprintf(tx_buf,"car_num:%4s\r\n",car_num); HAL_UART_Transmit(&huart1,(uint8_t*)tx_buf,strlen(tx_buf),50); sprintf(tx_buf,"car_time:%4s\r\n",car_time); HAL_UART_Transmit(&huart1,(uint8_t*)tx_buf,strlen(tx_buf),50); rx_count=0;//清零接收字符串,重新开始下一轮的接收 memset(rx_buf,0,20);//string.h中的函数,用以快速给数组清零 //将rx_buf所指的字符串的前20个字符全部写为0 } }
UART使用排障
-
接收指令不完整
调用
Uart_Proc处理接收到指令时,如果接收到的数据不完整,可以尝试增加延时,等待接收完成//在User Code 3内(死循环)不直接使用Usart_Proc(),而是增加等待的部分 if(rx_count!=0)//如果已经有接收到的字符,证明接收是正常工作的 { uint8_t temp=rx_count; HAL_Delay(1);//等待 //如果1ms后rx_count并未改变,说明没有发生接收,接收未完成 if(temp==rx_count) Usart_Proc(); } -
接收错误指令的处理
以前面指令控制开灯为例,如果用户发送
L,再发送LEDO,这时rx_buf读到的数据将是LLED解决方法是:设置一个时间变量,在进入中断后计时,每隔一段时间清空一次
rx_buf和rx_count,因为串口传输速度较快,50ms内能接收完成,所以这样清空不会对正常的接收造成影响void RxClear_Proc(void) { if(usrxClear<50) return;//没到指定时间不清空接收区 usrxClear=0;//开始计时 //将usrxClear++放到systick中 rx_count=0; memset(rx_buf,0,20);//清空缓冲区 } -
修改后的代码
while (1) { /* USER CODE BEGIN 3 */ rxClear_Proc(); if(rx_count!=0) { uint8_t temp=rx_count; HAL_Delay(1); if(temp==rx_count) Uart_Proc(); } } /* USER CODE END 3 */
本文来自博客园,作者:无术师,转载请注明原文链接:https://www.cnblogs.com/artlessist/p/18926165
本文使用知识共享4.0协议许可 CC BY-NC-SA 4.0
特别说明版权归属的文章以及不归属于本人的转载内容(如引用的文章与图片)除外

浙公网安备 33010602011771号