串口
前言
不定长数据接收的原理及其解决的方法
在 STM32 中。USART 发送接收有三种基本方式,轮询、中断和 DMA。
轮询方式为堵塞模式,使用超时管理机制。它每次接收一个字节,在规定时间内接收固定长度的数据。在对于某些数据不固定长度接收的数据,轮询的方式有时候不够灵活。
中断的方式为每一个字节都中断一次,当时比较消耗系统资源。特别是HAL库中,从中断到回调函数运行了不少的程序,频繁的中断很可能造成数据溢出。为了避免这个问题,我们使用指定接收一定长度的数据,再调用回调函数,这会让我们可以接收大数据,但是这种情况则造成了,要求每次的包是固定长度。
为了解决以上一些问题,网上最常用的办法是使用空闲中断,即在串口空闲的时候,触发一次中断,通知内核,本次运输完成了。数据传输过程为了尽量不占用CPU的处理数据时间,所以就使用DMA接收串口的数据。
什么是串口空闲
一般我们串口接收的时候都是使用的RXNE,接收到一个字节数据就进入一次中断,然后把它放入缓存,但是数据量很大的时候会频繁进入中断影响单片机的时效性。这时就可以使用到IDLE空闲中断,即在接收到一段数据后在一定的时间检测到没有数据到来,就认为此时串口总线空闲便产生一个空闲中断。
空闲中断的标志也就是停止位,一个上升沿
轮询和中断方式的简单实现 介绍
轮询
生成
STM32CubeMX 配置 STM32F407时钟树和烧录方式
API代码
串口阻塞式发送数据,使用超时管理机制 HAL_UART_Transmit()
串口阻塞式接收数据,使用超时管理机制 HAL_UART_Receive()
中断
生成
在上面的基础上,在NVIC中使能串口中断,并配置优先级。
串口中断模式发送 HAL_UART_Transmit_IT(),发送完会回调 HAL_UART_TxCpltCalback()
串口中断模式接收 HAL_UART_Receive_IT(),接收完会回调 HAL_UART_RxCpltCallback()
每个字节进一次中断,在 HAL_UART_IRQHandler() 中由HAL库自行处理。
对接收到的数据进行判断和处理 判断是发送中断还是接收中断,然后进行数据的发送和接收,在中断服务函数中使用
总结:使用中断方式收发串口的流程
1、调用函数开启一次中断收发的流程
2、每发送或接收到一个字节会进入中断,由HAL库进行处理,用户程序无需参与
3、HAL调用发送或接收完成回调函数
拓展:其他常用串口中断
- UART_IT_TXE 发送寄存器空中断,开启后当一个字节传送完毕就会进入中断,HAL中断方式发送依赖该中断。
- UART_IT_RXE 接收寄存器非空中断,开启后当接收到一个字节会进入中断,HAL库中断方式接收依赖该中断。
DMA
DMA(Direct memory access),即直接存储器访问。用于在外设与存储器之间以及存储器与存储器之间提供一种高速数据传输的方式。它在开始发送和接收完成数据时会给CPU相应的信号或者中断,在数据传输过程中无需CPU参与,通过硬件方式为RAM与I/O设备提供一条直接传送数据的通道。
DMA发送函数
HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
函数主要功能是以DAM模式发送pData指针指向的数据中固定长度的数据,并同时设置和使能DMA中断,具体怎么设置和使能中断的,打开此函数源码会发现下面这个函数。
HAL_DMA_Start_IT(huart->hdmatx, *(uint32_t *)tmp, (uint32_t)&huart->Instance->DR, Size);
此函数是启动DMA传输并启用中断。从函数源码可以看出此函数首先判断DMA传输状态是否是Ready:
- 如果是Ready则使能了DMA三个中断:DMA 半传输,DMA传输完成和DMA传输出错。如果发送数据正常,进入2次进入DMA中断(DMA 半传输和DMA传输完成);错误的话进入DMA传输出错中断;从这里也能看出HAL库比标准库严谨但效率低,DMA 半传输中断如果觉得效率低可以在程序中屏蔽掉,这样数据正常发送完成就只会进入一次DMA完成中断,三种DMA中断其实是同一个函数
HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma)
此函数功能是处理DMA中断请求,主要工作是清除中断标志位,改写DMA的状态,只有把状态改成HAL_DMA_STATE_READY,下一次才能正常使用DMA功能,否则会进入 HAL_BUSY状态 - 如果不是Ready状态,则进入HAL_BUSY状态,这也是为什么连续使用
HAL_UART_Transmit_DMA()
函数发送数据,第二次会发不出来数据,而且第二次函数会进入HAL_BUSY状态,所以要想使用HAL_UART_Transmit_DMA()
函数连续发送数据,相邻两次之间要有延时间隔或者检测DMA数据是否完成。
DMA 接收函数
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
函数说明:此函数的功能在DMA模式下接收大量数据,同时设置DMA线和哪个串口外设连接,以及将DMA线接收到的数据搬 *pData对应地内存中,和上面DMA发送函数一样,此函数同时具有设置和使能DMA中断的功能。可以看出此函数不仅是一个接收函数同时也是一个初始化函数,在main()之前调用,初始化串口和DMA连接和DMA接收BUF,以及设置和使能中断。如果DMA模式设置成循环模式时,只需设置这一次,如果DMA模式设置成正常模式时,每次读取完数据后需要再从新设置一次(就是再调用一次此函数),分析函数源码会发现此函数内部同样会调用以下两个函数,使用方法和分析和发送类似。
-
HAL_DMA_Start_IT(huart->hdmatx, *(uint32_t *)tmp, (uint32_t)&huart->Instance->DR, Size);
-
HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma);
主要说说HAL_UART_Receive_DMA()
,怎么配合IDLE串口空闲中断使用,main()函数之前一般调用此函数,一个主要目的是指明DMA传输串口数据存到指定的地方。一般情况我们会开辟一个全局变量的缓存
extern uint8_t receive_buff[BUFFER_SIZE]
。比如函数初始化为HAL_UART_Receive_DMA(&huart2, (uint8_t*)receive_buff, BUFFER_SIZE)
; 就是设置串口2接收到数据通过DMA线直接到receive_buff
中了,配合串口空闲中断,当进入串口空闲中断,说明一帧数据已接收完成。我们读取receive_buff相应长度的数据就是此次接收一帧的数据,这里还需要再介绍一个函数; -
__HAL_DMA_GET_COUNTER(__HANDLE__)
此函数的功能:获取当前DMA通道传输中receive_buff[BUFFER_SIZE]
缓存还剩余多少个数据单元。这样就能算出这一帧数据到底接收了多少单元的数据(数据长度=缓存总长度-缓存剩余的长度),
length = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);
空闲中断
空闲中断介绍
空闲中断是接受数据后出现一个byte的高电平(空闲)状态,就会触发空闲中断.并不是空闲就会一直中断,准确的说应该是上升沿(停止位)后一个byte,如果一直是低电平是不会触发空闲中断的(会触发break中断)。所以为了减少误进入串口空闲中断,串口RX的IO管脚建议设置成Pull-up<上拉模式>,串口空闲中断只是接收的数据时触发,发送时不触发。
# STM32系列-串口-uart-软件引脚内部上拉 或者 外部电阻上拉-原因问题的搜寻
但是我本人通过软件,把RX配置成PULL UP之后,串口就无法正常工作了
空闲中断使用
串口空闲中断的判定是:当串口开始接收数据后,检测到1字节数据的时间内没有数据发生,则认为串口空闲了,进入相应的串口中断。在中断内清除空闲中断标志位和调用串口回调函数,在回调函数内处理读取,判断,处理接收的数据。处理完数据以后我再把串口中断打开重复上面的流程,就可以完整的接收一帧一帧的数据。同时利用空闲中断也可以省去很多的的判断。
实现DMA+IDLE
生成
在前面的基础上,因为我使用了USART1的DMA发送和接收数据,首先是将UART1_RX和UART1_TX映射到DMA对应的通道上,对应的配置就是在CubeMX配置就是添加相应的DMA通道。再设置DMA几个参数。
- DMA模式,DMA有循环模式和正常模式2种模式。
(1) DMA接收如果用 循环模式,只需调用一次接收函数即可,重复调用也只有第一次调用有效 若是 正常模式,需要在每次接收完成后重复调用
(2) DMA发送要用 正常模式,需在每次发送时重复调用 还要打开UART中断,否则即使重复调用DMA发送函数也只有第一次发送有效,其余不会执行(UART发送状态一直处于 BUSY) 若是 循环模式,则会连续不断地发送,不会停止。 - 地址增加
(1)在传输数据时,可以配置指向寄存器数据的指针是否自动向后递增。
(2)通过单个寄存器访问外设源或目标数据时,禁止递增模式十分有用。
我这里将RX和