STM32的UART串口通信_轮询/中断方式串口收发_空闲中断接收不定长字符串
UART简介
UART通信原理
-
UART简述
关于串口通信和UART的种种概念与具体原理已有单独文章解释,在此仅作简述:
UART全称Universal Asynchronous Receiver Transmitter,通用异步通信,提供一种异步、串行、全双工的通信方式
异步串行通信让UART只需要两条通讯线(对应单片机两个引脚):RX用作接收,TX用作发送,即可实现全双工(同时接收与发送)的通信
-
UART的数据帧
使用TTL电平:高电平(>2.4V)表示1,低电平(<0.8V)表示0,由此通过电平的高低表示一个位(1或0)
而将多个位按一定的格式连续发送(1011001……),就形成了数据帧
数据帧的格式是指每一帧的长度固定,而且一帧中不同位置上的位作用不同:比如默认数据帧格式为起始位1bit/数据位8bit/校验位1bit/终止位1bit,也就是每发送一帧,就有8位长的数据被发送出去,起始位和结束位用于让接收的设备知道从哪里“读”到哪,校验位用于检测数据是否发送错误
当然,数据帧的格式是可自定义的UART在通信过程中通过传输多个这样的数据帧来进行通信,从微观来说也就是通过电平的高低变化来传递信息;
STM32的串口收发单元
-
STM32串口的核心部分是串口收发单元,其由发送引脚TX,接收引脚RX与以下单元构成
-
数据寄存器DR
该寄存器在物理上包含两个寄存器:
- 发送数据寄存器TDR
- 接收数据寄存器RDR
两个寄存器通过数据的写/读操作来区分,数据的发送和接受单元均采用双缓冲结构:
- 由数据寄存器与移位寄存器两部分组成
- 接收时:接收移位寄存器从接收引脚RX上把数据逐位移入寄存器内,在8位数据接收完成后再整体转移到接收数据寄存器RDR中,CPU通过数据总线读取RDR
- 发送时:CPU通过数据总线把写入TDR,再转移到发送移位寄存器,发送移位寄存器把并行数据转换为串行数据,低位在前高位在后逐个从发送引脚TX输出
- 使用双缓冲结构的好处是在数据收发过程中可以同时写入新的数据或读取已经接收的数据,提高了收发效率
-
状态标志位寄存器SR
保存通信状态标志位,有以下三个位:
-
TXE
发送数据寄存器空标志:当发送数据寄存器TDR的内容已经转移到发送移位寄存器时,该位置1(若TXEIE
位置1此时将会触发发送数据寄存器空中断)TXE=1
时TDR为空,可以写入新的数据(但数据可能还在发送) -
TC
发送完成标志:当发送移位寄存器内容已经发送完成,同时发送数据寄存器TDR也为空时,此位置1(若TCIE位置1此时将会触发发送完成中断)TC=1
时数据已经完成发送,用于判断发送完成 -
RXNE
接收数据寄存器不为空标志:当移位寄存器的内容转移到接收数据寄存器RDR时,该位由硬件置1(若RXNEIE位置1,将会触发接收数据寄存器不为空中断)RXNE=1
时接收到了数据,表示CPU可以读取新的数据
-
UART的初始化配置
-
UART的引脚配置
UART需要RX接收与TX发送两条通讯线,而对蓝桥杯开发板:USART1(通用同步收发器)默认配置的引脚是PC4、PC5,如果要同时使用LCD(占用PC组端口),就需要手动改PA9、PA10的引脚功能为USART1_TX与USART1_RX
-
UART的参数配置
- 在CubeMX左侧栏中打开Connectivity一栏,勾选USART1以启用
- 在右侧窗口上方的Mode选项中选择Asynchronous异步通信
- 在右侧窗口下方的Basic Parameters可以修改波特率(一般是9600,按需求修改),数据帧长度,是否启用校验,停止位长度
- 在更下方的Advanced Parameters中有更多参数可供选择,如数据方向等
- 如果使用中断方式:切换到NVIC Setting标签页,勾选Interrupt Table一栏下USART1 global interrupt的Enabled选项,这样最重要的是使能接收中断(也即前文提到的RXNEIE位置1),在接收到UART发送而来的数据时进入中断回调函数以便对数据处理
- 在配置完成后,CubeMX生成的代码将自动包含
HAL_UART_Init()
,下文不再介绍
UART的HAL库函数
轮询方式的UART接口函数
-
串口轮询方式发送函数HAL_UART_Transmit
用于以轮询方式发送指定数量的数据(字符串),可以与LCD实验章节介绍的
sprintf()
函数配合,将要发送的字符通过sprintf()
输出到发送用的字符数组中去,再通过UART发送HAL_UART_Transmit(UART_HandleTypeDef *huart,uint8_t *pData,uint16_t Size, uint16_t time) /* UART_HandleTypeDef *huart:uart的句柄,使用的是UART1端口时,此处写&huart1 // uint8_t *pData:待发送数据所存放的地址 // uint16_t Size:将发送的字节数 小技巧:可以使用strlen来计算发送长度(使用此函数计算将不会将结尾的\0计入字数) // uint16_t time:用以确定函数等待发送完成时间(以毫秒为单位) 如果UART在指定的时间内未能完成数据发送,函数将停止尝试并返回 如果这个参数设置为0,则函数会立即返回,不会等待传输完成 */
-
串口轮询方式接收函数HAL_UART_Receive
用于以轮询方式接收指定数量的数据,如果需要连续接收,则通过判断
RXNE
标志位来接收新的数据HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint16_t time) /* UART_HandleTypeDef *huart:uart的句柄 // uint8_t *pData:接收到的数据所存放的地址 // uint16_t Size:接收的字节数 // uint16_t time:接收超时时间 */
-
接口函数的返回值
各个发送、接收函数有
HAL_StatusTypeDef
类型的返回值,在此统一介绍:HAL_OK
:发送/接收成功HAL_ERROR
:表示参数错误HAL_BUSY
:串口被占用HAL_TIMEOUT
:发送/接收超时
-
轮询方式收发的代码实现
使用UART1发送Hello World!为例
char tx_buf[20];//设置一个缓冲区来存放待发送的数据 char rx_buf[20];//设置一个缓冲区来存放待接收的数据 sprintf(tx_buf,"Hello World!\r\n"); //为了更灵活,可以先把要发送的字符串打印到预先定义的UART发送缓冲区中 //然后再用strlen来确定数据的长度 HAL_UART_Transmit(&huart1,(uint8_t*)tx_buf,strlen(tx_buf),50); //再把这个缓冲区中的数据发送出去 //设定接收到字符串的长度为20,接收100ms超时 if(HAL_UART_Receive(&huart1,&rx_buf,20,100)==HAL_OK) { //HAL_UART_Receive返回HAL_OK表示接收成功 //接收到数据保存在rx_buf中,此处编写接收完成后的操作 }
中断方式的UART接口函数
-
串口中断方式发送函数HAL_UART_Transmit_IT
这个函数将使能发送中断,置位
TXEIE
和TCIE
,在指定量的数据发送完成后再清零关闭中断,并且调用发送中断回调函数HAL_UART_TxCpltCallback
进行后续处理HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) /* UART_HandleTypeDef *huart:uart的句柄 // uint8_t *pData:待发送数据所存放的地址 // uint16_t Size:发送的字节数,比如当其为1时表示每次中断只发送一个字符 // 中断方式接口函数与轮询方式参数上最大的区别便是无需规定超时时间 */
-
串口发送中断回调函数HAL_UART_TxCpltCallback
这个函数将在数据发送完成后执行,用作发送完成后的任务
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { //这是一个空函数,串口中断发送完之后,会进入该函数 if(huart == &huart1) { //各个串口的发送中断都会调用此回调函数,因此需要判断是哪一个串口 } }
-
串口中断方式接收函数HAL_UART_Receive_IT
这个函数将打开接收中断:置位
RXNEIE
,使能RXNE中断,在接收完成后清零关闭中断HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) /* UART_HandleTypeDef *huart:uart的句柄,使用的是UART1端口时,此处写&huart1 // uint8_t *pData:接收到的数据所存放的地址 // uint16_t Size:需要接收的字节数,比如当其为1时表示每次中断只能接收一个字符 */
-
串口接收中断回调函数HAL_UART_RxCpltCallback
这个函数将在接收到数据产生中断时执行,用作接收到数据的处理
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { //这是一个空函数,串口中断接收完之后,会进入该函数 if(huart == &huart1) { //各个串口的接收中断都会调用此回调函数,因此需要判断是哪一个串口 } HAL_UART_Receive_IT(&huart1,&rx_data,1); //因为指定数量数据接收完成后就会关中断,所以需要重新使能接收以备下一次中断 }
-
中断相关的宏函数
-
串口中断使能
__HAL_UART_ENABLE_IT(__HANDLE__,__INTERRUPT__) /* __HANDLE__:串口句柄指针 // __INTERRUPT__:串口的中断类型,常用的几个取值如下 UART_IT_TXE:发送数据寄存器空 UART_IT_TC:发送完成 UART_IT_RXNE:接收寄存器非空 UART_IT_IDLE:线路空闲中断 */
-
串口中断标志查询,将返回
SET
或者RESET
__HAL_UART_GET_FLAG(__HANDLE__,__FLAG__) /* __FLAG__:串口的中断类型,常用的几个取值如下 UART_FLAG_TXE:发送数据寄存器空 UART_FLAG_TC:发送完成 UART_FLAG_RXNE:接收寄存器非空 UART_FALG_IDLE:线路空闲中断 */
-
空闲中断标志清除
__HAL_UART_CLEAR_IDLEFLAG(__HANDLE__)
-
-
中断方式收发的代码实现
使用例:接收到
0
时,点亮LED1并通过UART发送LED1 Open!
,接收到C
时,熄灭LED1并发送LED1 Close!
char tx_buf[20];//存放发送内容的字符串 uint8_t rx_data;//存放接收内容的变量,因为只接收单个字符所以用uint8_t HAL_UART_Receive_IT(&huart1,&rx_data,1); //打开接收中断,把数据存放在rx_data中 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {//接收到数据后进入此函数 if(rx_data=='O') { ucled=0x01; sprintf(tx_buf,"LED1 Open!\r\n"); HAL_UART_Transmit(&huart1,(uint8_t*)tx_buf,strlen(tx_buf),50); } else if(rx_data=='C') { ucled=0x00; sprintf(tx_buf,"LED1 Close!\r\n"); HAL_UART_Transmit(&huart1,(uint8_t*)tx_buf,strlen(tx_buf),50); } else { sprintf(tx_buf,"Error!\r\n"); HAL_UART_Transmit(&huart1,(uint8_t*)tx_buf,strlen(tx_buf),50); } HAL_UART_Receive_IT(&huart1,&rx_data,1); //处理完接收到的数据后,重新使能中断接收以备下一次接收 }
-
基于上一个例子进一步编写代码,这一次将开关灯的指令从一个字改为一串确定长度的字符串:接收到
LEDO
开灯,接收到LEDC
关灯实现这一点利用的是C语言的字符串函数
strcmp
,比较接收到的字符串和设定的指令是否一致,注意使用这个函数需要头文件string.h
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);//再打开中断接收下一个字符 } /* 通过记录接收次数来判断是否接收到了设定长度的字符串 与直接设定接收数据长度相比,这样的好处是更加灵活 如果需要接收其他长度的数据,在函数中增加新的if(rx_count==???)即可 */ void Usart_Proc(void)//在主函数中循环执行此函数来判断 { if(rx_count==4)//当接收到4个字符时开始判断 { if(strcmp(rx_buf,"LEDO")==0)//如果接收到的是LEDO { ucled=0x01;//开灯 sprintf(tx_buf,"LED Open!\r\n"); HAL_UART_Transmit(&huart1,(uint8_t*)tx_buf,strlen(tx_buf),50); } if(strcmp(rx_buf,"LEDOC")==0)//如果接收到的是LEDC { ucled=0x00;//关灯 sprintf(tx_buf,"LED Open!\r\n"); HAL_UART_Transmit(&huart1,(uint8_t*)tx_buf,strlen(tx_buf),50); } } }
空闲中断的UART接口函数
-
空闲中断
顾名思义,空闲中断就是一段时间内空闲(称为发生空闲事件)就产生中断,对于串口来说,接收到一段数据后在一定的时间检测到没有数据到来,串口总线空闲,便产生一个空闲中断
在实际使用中,我们往往需要接受的数据长度是不定的,如果用上文的中断方式,需要为不同长度的数据单独设定判断代码,非常繁琐,为此引入空闲中断,让串口从接收到一定长度的数据就中断,变成串口空闲(数据接收完成)后中断(当然,接收的数据达到设定接收长度也还会产生中断,可以通过把长度设置得和缓冲区一样长)
注意,空闲中断和上文UART的中断方式是不同级别的概念,空闲中断串口接收也分轮询方式、中断方式与DMA方式三种,空闲中断是指:(无论哪种接收方式)在串口进入空闲时产生一个中断(下文只介绍中断方式,其它两种类似)
-
串口空闲中断接收函数HAL_UARTEx_ReceiveToIdle_IT
用于启用空闲中断接收,用法和
HAL_UART_Receive_IT
一致HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) //参数和平常的中断接收函数一致
-
空闲接收中断回调函数HAL_UARTEx_RxEventCallback
当接收完成,UART总线进入空闲状态时,就会产生空闲中断,进入此中断回调函数
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { //用法和HAL_UART_RxCpltCallback一致 }
-
空闲接收中断方式的代码实现
由于空闲中断接收到的数据本质上是整个缓冲区的内容,因此我们需要在其中查找我们需要判断的字符串
借助函数
strstr
,这个函数用于在一个字符串中查找子字符串的第一次出现位置,如果没有找到就会返回NULL
,由此可以用于判断是否接收到了对应的字符串例如:使用串口发送命令控制外设,
F1ON
开启外设F1,F1OFF
关闭外设1,接收到的数据没有相应命令时返回NULL
HAL_UARTEx_ReceiveToIdle_IT(&huart1, RX_Buf, 20);//启用空闲接收中断 void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if(huart == &huart1) { if(mode == USART)//宏定义USART为1,其他模式为0,在其他通信模式下关掉UART //mode是一个标志位,用于标记当前使用的通信模式,这个技巧用于实现UART的开关 //因为若是直接开关中断会有一定延迟,不如保持工作进行而屏蔽掉工作的结果 //同样的思路也可用于其他需要开关的外设,比如前一节的定时器测量信号频率 //如果直接关闭定时器的捕获中断,再开启时测量数据会在中断启动完成期间因延迟而异常 { if(strstr(RX_Buf,"F1ON"))F1_falg = 1;//同上,使用标志位实现外设启停 else if(strstr(RX_Buf,"F1OFF"))F1_falg = 0; else HAL_UART_Transmit(&huart1,"NULL",4,100); //如果收到错误字符串返回NULL } else { HAL_UART_Transmit(&huart1,"NULL",4,100); } HAL_UARTEx_ReceiveToIdle_IT(&huart1, RX_Buf, 20);//再开启接收 } }
本文来自博客园,作者:无术师,转载请注明原文链接:https://www.cnblogs.com/artlessist/p/18926038
本文使用知识共享4.0协议许可 CC BY-NC-SA 4.0
特别说明版权归属的文章以及不归属于本人的转载内容(如引用的文章与图片)除外