stm32-hal库串口dma空闲中断的正确使用方式+解析sbus信号
一. 问题描述
能够点进这篇文章的小伙伴肯定是对stm32串口dma空闲中断接收数据感兴趣的啦,今天用这一功能实现串口解析航模遥控器sbus信号时,查阅了很多网友发布的文章(勤劳的搬运工~),包括自己之前写过一篇博客 stm32_hal库_cubemx串口dma通信(dma发送+dma空闲接收不定长数据)。本文进一步梳理一下hal库串口空闲中断三种不同的使用方式,其中前两种使用dma方式,最后一种使用hal库自带的空闲中断机制。
二. 方法一——使用hal_uart_receive_dma
最常见的方法就是使用hal_statustypedef hal_uart_receive_dma(uart_handletypedef *huart, uint8_t *pdata, uint16_t size)
这个库函数,其使用方法类似于hal_statustypedef hal_uart_receive_it(uart_handletypedef *huart, uint8_t *pdata, uint16_t size)
,初始化时需要调用一次,然后每次在中断服务函数里面处理完数据后重新调用一次。
使用hal_uart_receive_it
只需要打开串口接收中断,即hal_nvic_enableirq(uart5_irqn)
;
使用hal_uart_receive_dma
空闲中断需要在初始化时打开串口空闲中断使能,调用方式为:__hal_uart_enable_it(&huart2, uart_it_idle)
。
此时dma中断可开可不开,开了也不用管,因为数据处理是在串口空闲中断中进行的。
1. 串口初始化代码:
void mx_usart2_uart_init(void)
{
huart2.instance = usart2;
huart2.init.baudrate = 100000;
huart2.init.wordlength = uart_wordlength_9b;
huart2.init.stopbits = uart_stopbits_1;
huart2.init.parity = uart_parity_even;
huart2.init.mode = uart_mode_tx_rx;
huart2.init.hwflowctl = uart_hwcontrol_none;
huart2.init.oversampling = uart_oversampling_16;
if (hal_uart_init(&huart2) != hal_ok)
{
error_handler();
}
__hal_uart_enable_it(&huart2, uart_it_idle); // 开启串口空闲中断,必须调用
hal_uart_receive_dma(&huart2,usart2_rx_buf,usart_rec_len); // 启动dma接收
}
void hal_uart_mspinit(uart_handletypedef* uarthandle)
{
if(uarthandle->instance==usart2)
{
__hal_rcc_usart2_clk_enable();
__hal_rcc_gpioa_clk_enable();
gpio_initstruct.pin = gpio_pin_2|gpio_pin_3;
gpio_initstruct.mode = gpio_mode_af_pp;
gpio_initstruct.pull = gpio_nopull;
gpio_initstruct.speed = gpio_speed_freq_very_high;
gpio_initstruct.alternate = gpio_af7_usart2;
hal_gpio_init(gpioa, &gpio_initstruct);
hdma_usart2_rx.instance = dma1_stream5;
hdma_usart2_rx.init.channel = dma_channel_4;
hdma_usart2_rx.init.direction = dma_periph_to_memory;
hdma_usart2_rx.init.periphinc = dma_pinc_disable;
hdma_usart2_rx.init.meminc = dma_minc_enable;
hdma_usart2_rx.init.periphdataalignment = dma_pdataalign_byte;
hdma_usart2_rx.init.memdataalignment = dma_mdataalign_byte;
hdma_usart2_rx.init.mode = dma_normal;
hdma_usart2_rx.init.priority = dma_priority_medium;
hdma_usart2_rx.init.fifomode = dma_fifomode_disable;
if (hal_dma_init(&hdma_usart2_rx) != hal_ok)
{
error_handler();
}
__hal_linkdma(uarthandle,hdmarx,hdma_usart2_rx);
/* usart2 interrupt init */
hal_nvic_setpriority(usart2_irqn, 0, 0);
hal_nvic_enableirq(usart2_irqn);
}
}
初始化中调用__hal_uart_enable_it(&huart2, uart_it_idle)
开启空闲中断,hal_uart_receive_dma(&huart2,usart2_rx_buf,usart_rec_len)
启动dma串口接收,usart2_rx_buf
是接收缓冲区,usart_rec_len
是定义的dma接收缓冲区长度,接收的数据不能超过这个长度。
2. 中断处理:
中断处理有两种方式,第一种是直接定义在void usart1_irqhandler(void)
中,第二种是自定义一个函数,然后在void usart1_irqhandler(void)
中调用。注意网上很多资源自定义了中断回调函数但代码又没贴全,读者可能以为是hal库自带的回调函数,结果因为没有在void usart1_irqhandler(void)
中调用导致失败。
第一种形式——直接定义:
void usart2_irqhandler(void)
{
uint32_t tmp_flag ;
u8 len;
u8 data[25];
tmp_flag = __hal_uart_get_flag(&huart2, uart_flag_idle);
if( tmp_flag != reset)
{
__hal_uart_clear_idleflag(&huart2);//清除标志位
hal_uart_dmastop(&huart2); //停止dma接收,防止数据出错
len = usart_rec_len - __hal_dma_get_counter(&hdma_usart2_rx);// 获取dma中传输的数据个数
// 以下为用户数据处理,将数据拷贝出去
if(len == 25)
{
memcpy(data,usart2_rx_buf,len);
update_sbus(data);
}
hal_uart_receive_dma(&huart2,usart2_rx_buf,usart_rec_len); //打开dma接收,数据存入rx_buffer数组中。
}
hal_uart_irqhandler(&huart2); //调用hal库中断处理公用函数
}
第二种形式——自定义回调函数:
void hal_uart_idlecpltcallback(uart_handletypedef *huart)
{
uint32_t tmp_flag ;
u8 len;
u8 data[25];
if (huart->instance == usart2)
{
tmp_flag = __hal_uart_get_flag(huart, uart_flag_idle);
if( tmp_flag != reset)
{
__hal_uart_clear_idleflag(huart);//清除标志位
hal_uart_dmastop(huart); //停止dma接收
len = usart_rec_len - __hal_dma_get_counter(&hdma_usart2_rx);// 获取dma中传输的数据个数
// 以下为用户数据处理,将数据拷贝出去
if(len == 25)
{
memcpy(data,usart2_rx_buf,len);
update_sbus(data);
}
hal_uart_receive_dma(huart,usart2_rx_buf,usart_rec_len); //打开dma接收,数据存入rx_buffer数组中。
}
}
}
void usart2_irqhandler(void)
{
hal_uart_idlecpltcallback(&huart2)
hal_uart_irqhandler(&huart2); //调用hal库中断处理公用函数
}
再次提醒一下,hal库中并没有类似void hal_uart_idlecpltcallback(uart_handletypedef *huart)
这样的回调函数,大家不要看到了以为是自带的回调函数而不在void usart2_irqhandler(void)中调用而导致失败。
中断处理中的函数说明:
__hal_uart_get_flag(huart, uart_flag_idle)
:返回中断标志位,产生空闲中断后会返回1;
__hal_uart_clear_idleflag(&huart2)
:清除空闲中断标志位,必须调用;
hal_uart_dmastop(&huart2)
:停止dma接收,防止数据处理出错,数据处理完成后重新打开;
__hal_dma_get_counter(&hdma_usart2_rx)
:返回dma中未传输的数据量,用缓冲区总长减去未传输数量就是已接收的数据长度;
hal_uart_receive_dma(huart,usart2_rx_buf,usart_rec_len)
:重新打开dma接收。
以上就能成功实现不定长数据的接受了,其实只要步骤对了还是很简单的。
三. 方法二——使用hal_uartex_receivetoidle_dma
第二种方式是使用hal_statustypedef hal_uartex_receivetoidle_dma(uart_handletypedef *huart, uint8_t *pdata, uint16_t size)
这个库函数,并重定义void hal_uartex_rxeventcallback(uart_handletypedef *huart, uint16_t size)
这个中断回调函数。
attention:__weak void hal_uartex_rxeventcallback(uart_handletypedef *huart, uint16_t size)
是正儿八经的库函数(弱函数),不是自定义的,我们可以重定义它。
使用这个库函数,串口初始化代码中就不需要调用__hal_uart_enable_it(&huart2, uart_it_idle)
了,因为hal_uartex_receivetoidle_dma
函数内部已经打开了空闲中断,当然你加上也没事。
1. 串口初始化代码:
void mx_usart2_uart_init(void)
{
huart2.instance = usart2;
huart2.init.baudrate = 100000;
huart2.init.wordlength = uart_wordlength_9b;
huart2.init.stopbits = uart_stopbits_1;
huart2.init.parity = uart_parity_none;
huart2.init.mode = uart_mode_tx_rx;
huart2.init.hwflowctl = uart_hwcontrol_none;
huart2.init.oversampling = uart_oversampling_16;
if (hal_uart_init(&huart2) != hal_ok)
{
error_handler();
}
//__hal_uart_enable_it(&huart2, uart_it_idle);// 开启串口空闲中断,可省略
hal_uartex_receivetoidle_dma(&huart2,usart2_rx_buf,usart_rec_len); // 使用串口dma空闲中断,会自动开启空空闲中断
}
void hal_uart_mspinit(uart_handletypedef* uarthandle)
{
if(uarthandle->instance==usart2)
{
__hal_rcc_usart2_clk_enable();
__hal_rcc_gpioa_clk_enable();
gpio_initstruct.pin = gpio_pin_2|gpio_pin_3;
gpio_initstruct.mode = gpio_mode_af_pp;
gpio_initstruct.pull = gpio_nopull;
gpio_initstruct.speed = gpio_speed_freq_very_high;
gpio_initstruct.alternate = gpio_af7_usart2;
hal_gpio_init(gpioa, &gpio_initstruct);
hdma_usart2_rx.instance = dma1_stream5;
hdma_usart2_rx.init.channel = dma_channel_4;
hdma_usart2_rx.init.direction = dma_periph_to_memory;
hdma_usart2_rx.init.periphinc = dma_pinc_disable;
hdma_usart2_rx.init.meminc = dma_minc_enable;
hdma_usart2_rx.init.periphdataalignment = dma_pdataalign_byte;
hdma_usart2_rx.init.memdataalignment = dma_mdataalign_byte;
hdma_usart2_rx.init.mode = dma_normal;
hdma_usart2_rx.init.priority = dma_priority_medium;
hdma_usart2_rx.init.fifomode = dma_fifomode_disable;
if (hal_dma_init(&hdma_usart2_rx) != hal_ok)
{
error_handler();
}
__hal_linkdma(uarthandle,hdmarx,hdma_usart2_rx);
/* usart2 interrupt init */
hal_nvic_setpriority(usart2_irqn, 0, 0);
hal_nvic_enableirq(usart2_irqn);
}
}
这里__hal_uart_enable_it(&huart2, uart_it_idle)
是可省略的,hal_uartex_receivetoidle_dma(&huart2,usart2_rx_buf,usart_rec_len)
启动dma串口空闲中断接收,usart2_rx_buf
是接收缓冲区,usart_rec_len
是定义的dma接收缓冲区长度,接收的数据不能超过这个长度。
2. 中断处理:
中断处理定义在void hal_uartex_rxeventcallback(uart_handletypedef *huart, uint16_t size)
这个中断回调函数中。
void hal_uartex_rxeventcallback(uart_handletypedef *huart, uint16_t size)
{
u8 len;
u8 data[25];
if(huart->instance == usart2)
{
hal_uart_dmastop(huart);
len = usart_rec_len - __hal_dma_get_counter(&hdma_usart2_rx);// 获取dma中传输的数据个数
if (usart2_rx_buf[0] == 0x0f && len == 25) //接受完一帧数据
{
memcpy(data,usart2_rx_buf,len);
update_sbus(data);
}
hal_uartex_receivetoidle_dma(&huart2,usart2_rx_buf,usart_rec_len); // 再次开启dma空闲中断
}
}
void usart2_irqhandler(void)
{
hal_uart_irqhandler(&huart2); //调用hal库中断处理公用函数
}
当然也可以像方法一种一样,中断逻辑处理直接定义在void usart2_irqhandler(void)
中。
四. 方法三——使用hal_uartex_receivetoidle_it(不使用dma)
这种方法参考这篇文章: stm32 hal库串口空闲中断最新用法
使用方法:
1、在主函数中调用hal_uartex_receivetoidle_it()
2、在回调函数hal_uartex_rxeventcallback()
中做中断逻辑处理。
这里我换用串口4,实现上位机发送,单片机原封不动返回数据。
1. 串口初始化代码:
void mx_uart4_init(void)
{
huart4.instance = uart4;
huart4.init.baudrate = 115200;
huart4.init.wordlength = uart_wordlength_8b;
huart4.init.stopbits = uart_stopbits_1;
huart4.init.parity = uart_parity_none;
huart4.init.mode = uart_mode_tx_rx;
huart4.init.hwflowctl = uart_hwcontrol_none;
huart4.init.oversampling = uart_oversampling_16;
if (hal_uart_init(&huart4) != hal_ok)
{
error_handler();
}
// hal_uartex_receivetoidle_it同时开启空闲中断,开启接收
hal_uartex_receivetoidle_it(&huart4,usart4_rx_buf,usart_rec_len);
}
void hal_uart_mspinit(uart_handletypedef* uarthandle)
{
if(uarthandle->instance==uart4)
{
__hal_rcc_uart4_clk_enable();
__hal_rcc_gpioc_clk_enable();
gpio_initstruct.pin = gpio_pin_10|gpio_pin_11;
gpio_initstruct.mode = gpio_mode_af_pp;
gpio_initstruct.pull = gpio_pullup;
gpio_initstruct.speed = gpio_speed_freq_very_high;
gpio_initstruct.alternate = gpio_af8_uart4;
hal_gpio_init(gpioc, &gpio_initstruct);
hal_nvic_setpriority(uart4_irqn, 0, 0);
hal_nvic_enableirq(uart4_irqn);
}
}
可以看到这里是没有使用dma的,初始化中调用了hal_uartex_receivetoidle_it(&huart4,usart4_rx_buf,usart_rec_len)
来开启串口4的空闲中断,usart4_rx_buf是接收缓冲区,usart_rec_len是缓冲区的长度。这里可以看下库中hal_uartex_receivetoidle_it
的定义和提示:
框1的地方说,这个函数用于在中断模式下接收一定量数据,直到指定的数据长度被接收到或者空闲中断产生。也就是说,如果usart_rec_len定义很小,还没到产生空闲中断,接收就会完成。
框2的地方,即是打开串口空闲中断。所以不需要额外再去开启空闲中断了。
2. 中断处理:
中断处理定义在void hal_uartex_rxeventcallback(uart_handletypedef *huart, uint16_t size)
这个中断回调函数中。注意到,和使用dma传输时是同一个回调函数。
void hal_uartex_rxeventcallback(uart_handletypedef *huart, uint16_t size)
{
u8 len=0;
u8 data[25];
u8 uart4_len=0;
u8 uart4_data[50];
if(huart->instance == usart2)
{
hal_uart_dmastop(huart);
len = usart_rec_len - __hal_dma_get_counter(&hdma_usart2_rx);// 获取dma中传输的数据个数
if (usart2_rx_buf[0] == 0x0f && len == 25) //接受完一帧数据
{
memcpy(data,usart2_rx_buf,len);
update_sbus(data);
}
hal_uartex_receivetoidle_dma(&huart2,usart2_rx_buf,usart_rec_len); // 再次开启dma空闲中断
}
if(huart->instance == uart4)
{
while (usart4_rx_buf[uart4_len] != '\0') uart4_len++;
memcpy(uart4_data,usart4_rx_buf,uart4_len);
hal_uart_transmit(&huart4,uart4_data,uart4_len,0xff);
// 调用printf是使用串口1
printf("data: %s\r\n", uart4_data);
printf("data length: %d\r\n", uart4_len);
memset(usart4_rx_buf, 0, usart_rec_len);
memset(uart4_data, 0, sizeof(uart4_data));
hal_uartex_receivetoidle_it(&huart4,usart4_rx_buf,usart_rec_len);
}
}
void usart2_irqhandler(void)
{
hal_uart_irqhandler(&huart2); //调用hal库中断处理公用函数
}
void uart4_irqhandler(void)
{
hal_uart_irqhandler(&huart4); //调用hal库中断处理公用函数
}
回调函数中就是读取usart4_rx_buf
缓冲区的数据和数据长度,然后做相应处理,这里只是简单的原数据发送回去,如果是sbsu信号或modbus或其他带协议帧的数据,则可根据帧头帧尾、数据长度、校验位等做进一步处理。
最终实现的效果就是上位机发送什么,单片机返回什么,如图:
五. 总结
以上三种方式都能实现串口空闲中断接收不定长数据,适合用于处理不定长数据接收、减少cpu负担,相比较而言,方法三不使用dma,使用上更简洁。方法一、二使用dma,好处是可以减少cpu负担。
发表评论