目录

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 回调函数

2 环形缓冲区中间件代码设计

环形缓冲区结构体的设计:

.h文件中相关枚举类型和函数总览

创建一个环形缓冲区

环形缓冲区判空

环形缓冲区判满

尾插(同时head++)

拿数据(同时tail++)

获取head的值

使head往后移动自定义的距离(与串口&DMA中断配合使用)

3 串口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不定长接收代码实践

【DMA】FreeRTOS下STM32串口接收不定长数据并结合DMA实现环形缓冲区:2 串口DMA不定长接收代码实践

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐