【DMA】FreeRTOS下STM32串口接收不定长数据并结合DMA实现环形缓冲区:1 相关函数理论部分与环形缓冲区设计
目录
使用HAL_UARTEx_ReceiveToIdle_DMA打开空闲中断
中断一:half size of receive (半满) (DMA 的中断回调:DMA2_Stream2_IROHandler(void))
中断二:cplt size of receive (全满) (DMA的中断回调:DMA2_Stream2_IRQHandler(void))
中断三:Idle中断(当串口的起始位在持续一个字节的时间都没有到来的时候,触发空闲中断)
HAL_UART_RxCpltCallback 回调函数和 HAL_UARTEx_RxEventCallback 回调函数的区别?
源码解析全满中断、半满中断的中断回调函数的注册与最终调用的回调函数
串口框图与空闲标志(来源《STM32F4xx技术参考手册》)
HAL库一个有趣的设计:当接收长度刚好触发DMA全满中断时,串口空闲中断函数将不会重复进入 HAL_UARTEx_RxEventCallback 回调函数
使head往后移动自定义的距离(与串口&DMA中断配合使用)
1 相关中断与回调函数
使用HAL_UARTEx_ReceiveToIdle_DMA打开空闲中断
如果 UART 和 DMA 一起使用的时候,他们各自是两个外设,他们彼此的中断是互相独立的,不要把他们混淆在一起。
在 HAL库中,一旦打开空闲中断,则默认打开半满(DMA)中断和全满(DMA)中断。
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(UART HandleTypeDef *huart, uint8 t *pData, uint16 tSize);(胶水接口 DMA + UART)
-
参数一:串口的对象(UART)
-
参数二:指向的地址空间(自增地址)(DMA)
-
参数三:最大的搬运数据(DMA)
中断一:half size of receive (半满) (DMA 的中断回调:DMA2_Stream2_IROHandler(void))
DMA 在搬运完一半 Size 大小后的数据,产生中断事件。
此中断产生后,不会去重新配置串口外设的寄存器,仅仅是发生中断,可以在中断回调函数中处理一些事情(比如发送信号量给串口数据处理线程)

中断二:cplt size of receive (全满) (DMA的中断回调:DMA2_Stream2_IRQHandler(void))
DMA 在搬运完 Size大小后的数据,产生中断事件。
此中断产生后,会重新配置DMA的待搬运计数寄存器,从整个buffer的头再去装数,可能开始产生覆盖之前数据的情况。

中断三:Idle中断(当串口的起始位在持续一个字节的时间都没有到来的时候,触发空闲中断)
(UART的中断回调 HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size))
中断触发原理:串口外设在一个字节长的时间中没有接收到串口起始位,则发生IDLE中断。IDLEIE(空闲中断使能寄存器)

三个中断的配合例子:
中断一(DMA半满中断),中断二(DMA全满中断),中断三(串口空闲中断)之间的关系:
-
当使用 HAL_UARTEx_ReceiveToIdle_DMA 函数时,假设参数三Size是32字节(半满16字节)。
-
当发送17个字节时,会先进入中断一(DMA半满中断),然后再进入中断三(串口空闲中断)。
-
当发送15个字节时,会直接进入中断三(串口空闲中断)。
-
当发送33个字节时,会先进入中断一(DMA半满中断),然后进入中断二(DMA全满中断),然后进入中断三(串口空闲中断)。并且,由于时33个字节,如果这个时候DMA的配置为DMA_NORMAL模式,则第33个字节会被丢掉。如果,DMA被配置为DMA_CIRCULAR,则第33个字节会覆盖最开始的第一个字节。
HAL_UART_RxCpltCallback 回调函数和 HAL_UARTEx_RxEventCallback 回调函数的区别?
-
HAL_UART_RxCpltCalback,主要用于固定长度的 UART 接收,当接收到设定长度的数据后触发。用HAL_UART_Receive_IT 或 HAL_UART_Receive_DMA 函数(实验内容中的 1~4 步均是这个回调),接收到指定长度的时候会调用,不适用于接收可变长的串口数据。
-
HAL_UARTEx_RxEventCallback,主要用于可变长度的数据接收,特别是结合空闲行检测,可以在接收到不确定长度的数据时触发。通常与 DMA 接收模式一起用,主要用于 HAL_UARTEx_ReceiveToIdle_DMA 函数(实验内容中的第 5 步是这个回调),当检测到空闲,或者到达 DMA 最大缓冲时,使用这个回调。
源码解析全满中断、半满中断的中断回调函数的注册与最终调用的回调函数
当我们调用串口DMA空闲中断接收后:

UART_Start_Receive_DMA函数:

全满中断:根据配置的DMA接收类型在 UART_DMAReceiveCplt() 函数中选择最终调用的回调函数:(图中高亮划出的是开启串口DMA空闲接收后会调用的回调函数)
同时,在下图中的/* DMA Normal mode */里的内容可以看到,当DMA选择normal模式、同时开启了串口空闲中断时,会在这个全满中断服务函数中关闭串口空闲中断(串口空闲中断也会调用 HAL_UARTEx_RxEventCallback() ,这个函数在 UART_DMAReceiveCplt() 中会调用一次,不需要重复调用)。
需要注意,IDLE 串口空闲标志由硬件置位,关闭串口空闲中断只是失能中断,串口空闲标志寄存器依然会被硬件置位子。

半满中断:根据配置的DMA接收类型在 UART_DMARxHalfCplt() 函数中选择最终调用的回调函数:(图中高亮划出的是开启串口DMA空闲接收后会调用的回调函数)

也就是说,只要开启了串口DMA空闲中断,不管DMA是circular模式还是normal模式,DMA半满/全满中断都会调用 HAL_UARTEx_RxEventCallback() 回调函数。
串口框图与空闲标志(来源《STM32F4xx技术参考手册》)


由上图可知,IDLE 串口空闲标志由硬件置位。
需要注意的是,在串口中断服务函数中,如果串口接收被配置为“等待空闲事件”类型,并且是空闲中断调用的串口中断服务函数(即空闲标志IDLE被置位),则会在中断服务函数中清除IDLE标志:(也就是说,在串口空闲中断回调函数 HAL_UARTEx_RxEventCallback() 中 IDLE 标志是已经被清除了的)

而在串口空闲中断接收、DMA接收)函数中,都会再次检查 IDLE 标志并清除标志:
串口空闲DMA接收:

串口空闲中断接收:

HAL库一个有趣的设计:当接收长度刚好触发DMA全满中断时,串口空闲中断函数将不会重复进入 HAL_UARTEx_RxEventCallback 回调函数
我们知道,开启串口空闲中断后会自动开启DMA半满中断和DMA全满中断,而这三个中断最终都会调用 HAL_UARTEx_RxEventCallback 回调函数,而如果有一次接收的长度刚好够DMA全满中断,在DMA全满中断进入 HAL_UARTEx_RxEventCallback 回调函数后,串口空闲中断将不会进入 HAL_UARTEx_RxEventCallback 回调函数,因为数据已经在上一次进入回调函数时处理完了,hal 库很聪明地设计了这个不会重复进入的机制。

-
如上图日志所示,此时设置的DMA接收长度为10,则刚好接收10个数据时不会在串口空闲中断中再次进入 HAL_UARTEx_RxEventCallback 回调函数(如上图红框中标记的日志)
-
而如果接收11个数据,则串口空闲中断会照常进入 HAL_UARTEx_RxEventCallback 回调函数(如上图绿框中标记的日志)
2 环形缓冲区中间件代码设计
环形缓冲区结构体的设计:
#define CIRCULAR_BUFFER_SIZE 10 // .h文件
typedef uint8_t data_type_t; // .h文件
typedef struct circular_buffer
{
data_type_t data[CIRCULAR_BUFFER_SIZE]; // 数据存储的buffer
uint32_t head; // 指向下一个存储数据的位置
uint32_t tail; // 指向要拿的数据(APP解包)
}circular_buffer_t;
上面的data是malloc出的一块内存空间,可以是malloc(单片的堆上),也可以是pvmalloc(RTOS的堆上)。
该环形缓冲区的容量为 CIRCULAR_BUFFER_SIZE - 1,具体原因看下面的判满逻辑。
.h文件中相关枚举类型和函数总览
typedef enum CIR_BUF_RETURN_VALUE
{
CIR_BUF_OK,
CIR_BUF_ERROR,
CIR_BUF_IS_FULL,
CIR_BUF_IS_EMPTY,
}CIR_BUF_RETURN_VALUE;
typedef enum BUFFER_STATES
{
BUFFER_IS_EMPTY,
BUFFER_IS_FULL,
BUFFER_IS_NOT_EMPTY,
BUFFER_IS_NOT_FULL,
PBUFFER_IS_NULL,
}BUFFER_STATES;
/* creatEmptyCircularBuffer */
/* return: pointer of empty CircularBuffer */
circular_buffer_t* creatEmptyCircularBuffer(void);
/* judge buffer_is_empty */
/* return value:
BUFFER_IS_EMPTY
BUFFER_IS_NOT_EMPTY
PBUFFER_IS_NULL */
enum BUFFER_STATES buffer_is_empty(circular_buffer_t* p_buffer);
/* judge buffer_is_full */
/* return value:
BUFFER_IS_FULL
BUFFER_IS_NOT_FULL
PBUFFER_IS_NULL */
enum BUFFER_STATES buffer_is_full(circular_buffer_t* p_buffer);
/* insert data to target circular buffer */
/* return value:
CIR_BUF_OK
CIR_BUF_ERROR
CIR_BUF_IS_FULL */
enum CIR_BUF_RETURN_VALUE insert_data(circular_buffer_t* p_buffer,
data_type_t data);
/* get data and delete it from target circular buffer */
/* return value:
CIR_BUF_OK
CIR_BUF_ERROR
CIR_BUF_IS_EMPTY */
enum CIR_BUF_RETURN_VALUE get_data(circular_buffer_t* p_buffer,
data_type_t* data);
/* get target circular buffer's head pos */
/* return value:
CIR_BUF_OK
CIR_BUF_ERROR */
CIR_BUF_RETURN_VALUE get_head_pos(circular_buffer_t* p_buffer,
uint32_t* head_pos);
/* increase the target circular buffer's head */
/* return value:
CIR_BUF_OK
CIR_BUF_ERROR */
CIR_BUF_RETURN_VALUE head_pos_increment(circular_buffer_t* p_buffer,
uint32_t increment_num);
创建一个环形缓冲区
/* creatEmptyCircularBuffer */
/* return: pointer of empty CircularBuffer */
circular_buffer_t* creatEmptyCircularBuffer(void)
{
circular_buffer_t* p_buffer_tmp = NULL;
// 1.alloc the memory space
p_buffer_tmp = (circular_buffer_t*)malloc(sizeof(circular_buffer_t));
if (NULL == p_buffer_tmp)
{
log_e("error: creatEmptyCircularBuffer malloc error!");
return NULL;
}
// 2.memory init
memset(p_buffer_tmp, 0, sizeof(circular_buffer_t));
return p_buffer_tmp;
}
环形缓冲区判空
判空思路:当head指向的位置 == tail指向的位置时,认为此时环形缓冲区为空。
/* judge buffer_is_empty */
/* return value:
BUFFER_IS_EMPTY
BUFFER_IS_NOT_EMPTY
PBUFFER_IS_NULL */
enum BUFFER_STATES buffer_is_empty(circular_buffer_t* p_buffer)
{
if (NULL == p_buffer) {
return PBUFFER_IS_NULL;
}
if (p_buffer->head == p_buffer->tail){
return BUFFER_IS_EMPTY;
}
else {
return BUFFER_IS_NOT_EMPTY;
}
}
环形缓冲区判满
head指向位置的下一个位置为tail时,认为环形缓冲区为满。(这种环形缓冲区的设计就是会浪费一个位置)
/* judge buffer_is_full */
/* return value:
BUFFER_IS_FULL
BUFFER_IS_NOT_FULL
PBUFFER_IS_NULL */
enum BUFFER_STATES buffer_is_full(circular_buffer_t* p_buffer)
{
if (NULL == p_buffer) {
return PBUFFER_IS_NULL;
}
// if the next of head is tail, the cir_buffer is full
if ((p_buffer->head + 1) % CIRCULAR_BUFFER_SIZE
== (p_buffer->tail) % CIRCULAR_BUFFER_SIZE)
{
return BUFFER_IS_FULL;
}
else {
return BUFFER_IS_NOT_FULL;
}
}
尾插(同时head++)
/* insert data to target circular buffer */
/* return value:
CIR_BUF_OK
CIR_BUF_ERROR
CIR_BUF_IS_FULL */
enum CIR_BUF_RETURN_VALUE insert_data(circular_buffer_t* p_buffer,
data_type_t data)
{
if (NULL == p_buffer){
return CIR_BUF_ERROR;
}
if (BUFFER_IS_FULL == buffer_is_full(p_buffer)){
return CIR_BUF_IS_FULL;
}
// write data
p_buffer->data[(p_buffer->head) % CIRCULAR_BUFFER_SIZE] = data;
++(p_buffer->head);
return CIR_BUF_OK;
}
拿数据(同时tail++)
/* get data and delete it from target circular buffer */
/* return value:
CIR_BUF_OK
CIR_BUF_ERROR
CIR_BUF_IS_EMPTY */
enum CIR_BUF_RETURN_VALUE get_data(circular_buffer_t* p_buffer,
data_type_t* data)
{
if (NULL == p_buffer) {
return CIR_BUF_ERROR;
}
if (BUFFER_IS_EMPTY == buffer_is_empty(p_buffer)) {
return CIR_BUF_IS_EMPTY;
}
// get data and delete it from target cir_buffer
*data = p_buffer->data[(p_buffer->tail) % CIRCULAR_BUFFER_SIZE];
++(p_buffer->tail);
return CIR_BUF_OK;
}
获取head的值
/* get target circular buffer's head pos */
/* return value:
CIR_BUF_OK
CIR_BUF_ERROR */
CIR_BUF_RETURN_VALUE get_head_pos(circular_buffer_t* p_buffer,
uint32_t* head_pos)
{
if (NULL == p_buffer)
{
return CIR_BUF_ERROR;
}
*head_pos = p_buffer->head;
return CIR_BUF_OK;
}
使head往后移动自定义的距离(与串口&DMA中断配合使用)
/* increase the target circular buffer's head */
/* return value:
CIR_BUF_OK
CIR_BUF_ERROR */
CIR_BUF_RETURN_VALUE head_pos_increment(circular_buffer_t* p_buffer,
uint32_t increment_num)
{
if (NULL == p_buffer)
{
return CIR_BUF_ERROR;
}
p_buffer->head += increment_num;
return CIR_BUF_OK;
}
3 串口DMA不定长接收代码实践
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)