ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

GD32F303 USART实战:接收中断、空闲中断+DMA、发送DMA全解析

GD32F303 USART实战:接收中断、空闲中断+DMA、发送DMA全解析 说实话我一开始拿到GD32F303这块芯片心里是有点不以为然的。毕竟在STM32上摸爬滚打了这么多年串口这种最基础的外设还不是分分钟搞定结果真上手调USART才发现GD32的固件库风格、DMA控制器的行为、中断标志位的清除方式和STM32的老库差别真的不小。网上能找到的例程大多写一半能看不能用踩坑全靠自己。这篇小记我把GD32F303的USART三种收发方式——接收中断、接收空闲中断配合DMA、发送DMA——从原理到代码、从踩坑到稳定运行的过程完整记录下来给准备从STM32转GD32或者第一次用GD32的兄弟做个参考也方便自己后面回看。1. 先捋清楚三种USART收发方式分别解决什么问题很多新手上来就问“到底用哪种接收方式”我的建议是先把应用场景想清楚再决定用什么方案而不是哪个方案听起来高级就用哪个。我把三种方式放在一起对比一下你大概就知道怎么选了。1.1 接收中断最基础但别指望它扛大流量接收中断是USART最基础的接收方式。每收到一个字节硬件把数据放进接收数据寄存器同时置RBNE标志并触发中断你在中断服务函数里把数据读出来保存。它的优点是实时性高、代码简单、几乎不需要额外配置调试初期用来验证串口通路非常方便。但缺点也很明显中断次数和收到的字节数成正比。算一笔账115200bps波特率下一个字节要传10个bit8位数据加起始位停止位大约87us收一个字节意味着不到100us就要进一次中断。如果主循环里还有别的任务或者在中断里做了协议解析稍微一耽误就导致数据溢出。1.2 空闲中断DMA处理不定长帧数据的关键说它“关键”是因为实际项目里百分之八十的串口报文都是不定长的。AT指令、modbus从机响应、GPS的NMEA语句、私有帧协议你根本没法提前知道主机会发多少字节。用传统接收中断得自己拼帧还要处理帧与帧之间的超时判断非常麻烦。改用DMA把数据先搬到内存缓冲区同时用USART的IDLE空闲中断作为“一帧结束”的标志思路一下就清晰了。DMA全程不需要CPU参与只有整帧传输结束后产生一次IDLE中断CPU只在那一瞬间处理数据。中断次数从“每个字节一次”降为“每帧一次”高波特率下也能轻松扛住。1.3 发送DMA把CPU从发送等待里解放出来发送DMA的价值经常被低估。很多人觉得“发送不就是往数据寄存器写值嘛多等一会能耽误多少事”。其实在批量回包或者OTA升级这种场景一包数据几百上千字节用while轮询等待发送寄存器空CPU基本就卡在发送函数里出不来了这段时间什么别的事都干不了。用了发送DMA把缓冲区地址和长度交给DMA外设CPU可以立即返回继续做其他任务等发送完成再通过中断通知你。接收方向的优化也能照搬这个思路USART的RX和TX各占一条DMA通道互不干扰这一点和SPI全双工需要独立收发两条DMA通道是同一个道理。三种方式对比如下方案中断频率CPU占用适合场景实现复杂度普通接收中断每字节一次高高速下容易丢数据低速、少量数据、实时响应要求高低空闲中断DMA接收每帧一次极低不定长帧协议、高波特率批量接收中发送DMA每帧一次极低大数据量发送、回包、OTA中2. GD32F303 USART初始化时钟、引脚与参数配置不管用哪种收发方式USART本身的初始化流程是一致的这一步搞错了后面全白搭。我先以USART0、引脚PA9TX和PA10RX为例把初始化讲透。2.1 开时钟、配置引脚的关键步骤GD32F303的GPIO复用配置方式和STM32F103老库完全不同我见过很多人把STM32的GPIO配置代码直接搬过来结果引脚死活不出波形原因就是复用功能的设置方式根本不对。GD32F303需要先开GPIOA和USART0的时钟然后用gpio_af_set选择复用功能号再调用gpio_mode_set把引脚模式设置成复用模式。void uart_gpio_init(void) { rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_USART0); gpio_af_set(GPIOA, GPIO_AF_7, GPIO_PIN_9 | GPIO_PIN_10); gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_9 | GPIO_PIN_10); gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9); }这里的GPIO_AF_7是USART0的复用功能号具体以芯片手册的AF映射表为准。PA9作为TX推挽输出PA10作为RX输入实际使用中建议加上拉防止悬空时串口误触发。如果改用USART1或者USART2注意复用功能号和DMA通道映射都要跟着变不能照抄。2.2 串口参数初始化的几个注意点GPIO配置好之后再初始化串口参数。GD32固件库的操作方式和STM32老的standard peripheral库有点类似但API名字不能混用。void uart_param_init(uint32_t baudrate) { usart_deinit(USART0); usart_baudrate_set(USART0, baudrate); usart_word_length_set(USART0, USART_WL_8BIT); usart_stop_bit_set(USART0, USART_STB_1BIT); usart_parity_config(USART0, USART_PM_NONE); usart_hardware_flow_rts_config(USART0, USART_RTS_DISABLE); usart_hardware_flow_cts_config(USART0, USART_CTS_DISABLE); usart_receive_config(USART0, ENABLE); usart_transmit_config(USART0, ENABLE); usart_enable(USART0); }注意无校验、8位数据位、1位停止位是串口调试最常用的组合。如果使能了奇偶校验数据位一定要设置成9位因为校验位要占用一位很多人在这里搞混导致通信失败。波特率的计算不需要你手动去算usart_baudrate_set内部会读取USART挂载的总线时钟根据分频系数自动配置波特率寄存器。前提是你的系统时钟初始化没有问题。我之前遇到过USB转串口工具发出来的数据永远是乱码排查到最后发现不是代码问题而是上位机软件里波特率设错了。这类低级错误其实比芯片本身的坑更常见遇到乱码先检查参数别一上来就怀疑代码。2.3 中断优先级与NVIC配置如果方案里用到中断必须调用nvic_irq_enable使能对应的中断通道。我习惯把串口接收中断的抢占优先级设成1或者2不要设成0因为系统里总得有更高优先级的东西比如系统滴答定时器。nvic_irq_enable(USART0_IRQn, 1, 0);在空闲中断DMA方案里IDLE中断的优先级可以比其他外设中断高一点。原因很简单IDLE中断意味着数据帧已经完整到达如果优先级太低被其他中断卡太久DMA缓冲区可能已经被下一帧数据覆盖导致上一帧还没被处理就丢了。3. 接收中断实操RBNE处理与ORE溢出排查接收中断虽然简单但实际跑起来会有一堆问题。这一章我重点讲两个内容中断服务函数的正确写法以及ORE溢出这个隐形杀手。3.1 接收中断代码示例与处理顺序先看一个最简单的实现volatile uint8_t s_rx_byte; void USART0_IRQHandler(void) { if (RESET ! usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE)) { s_rx_byte (uint8_t)usart_data_receive(USART0); } }在中断里把数据寄存器读出来RBNE标志会自动清除。重点在于读数据这个动作一定要在中断里完成千万别只清标志不读数据。如果RBNE中断触发了但不读数据新来的数据会覆盖接收数据寄存器立刻触发溢出错误之后再读到的数据全是错位的。如果项目需要做帧处理我建议中断里只负责把数据搬到环形缓冲区协议解析丢到主循环去做。这样中断处理时间可以压到几个微秒基本不会丢字节。3.2 ORE溢出标志的清除顺序ORE是USART最容易踩的坑尤其在高波特率或者中断处理里干了重活的时候。现象很典型串口助手发一串数据过来板子只回第一个字节或者中间出现乱码。本质原因是上一字节还在数据寄存器里没被读走新字节已经完整接收硬件就把旧数据覆盖了同时置ORE标志。GD32需要按照“读状态寄存器、再读数据寄存器”的顺序才能把ORE清掉如果只读数据或者只清标志下一次溢出照样会出现。我的建议是在RBNE中断处理里顺带查一下ORE标志void USART0_IRQHandler(void) { if (RESET ! usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE)) { s_rx_byte (uint8_t)usart_data_receive(USART0); } if (RESET ! usart_flag_get(USART0, USART_FLAG_ORERR)) { (void)usart_data_receive(USART0); } }再强调一点如果开启了DMA接收RBNE中断一定要关掉。DMA搬运和中断读数据会互相竞争接收数据寄存器造成数据错乱。DMA模式下数据由DMA直接读走RBNE标志也会被DMA的读操作清掉你没有必要再开一个中断来“确认”这件事。4. 空闲中断DMA接收最实用的一套方案这套方案我在好几个项目中稳定跑了很久下面把配置流程按顺序拆开讲你照着配基本一次成功。4.1 DMA接收通道配置全流程DMA初始化是这套方案的核心先把代码放出来#define UART0_RX_BUFFER_SIZE 256 uint8_t uart0_rx_buf[UART0_RX_BUFFER_SIZE]; void uart0_dma_rx_init(void) { dma_parameter_struct dma_para; rcu_periph_clock_enable(RCU_DMA0); dma_deinit(DMA0, DMA_CH3); dma_struct_para_init(dma_para); dma_para.direction DMA_PERIPHERAL_TO_MEMORY; dma_para.memory_addr (uint32_t)uart0_rx_buf; dma_para.memory_inc DMA_MEMORY_INCREASE_ENABLE; dma_para.memory_width DMA_MEMORY_WIDTH_8BIT; dma_para.number UART0_RX_BUFFER_SIZE; dma_para.periph_addr (uint32_t)USART0-DATA; dma_para.periph_inc DMA_PERIPH_INCREASE_DISABLE; dma_para.periph_width DMA_PERIPHERAL_WIDTH_8BIT; dma_para.priority DMA_PRIORITY_HIGH; dma_init(DMA0, DMA_CH3, dma_para); dma_circulation_enable(DMA0, DMA_CH3); dma_channel_enable(DMA0, DMA_CH3); }关键点有三个。第一个DMA通道必须和USART的接收请求对应USART0的RX对应DMA0的通道3具体要查芯片手册的DMA请求映射表不是随便用一个通道就能收到数据。第二个外设地址要写成USART0-DATA不是写USART0或者某个别的寄存器这个地址写错的话DMA搬运出来的数据就是垃圾。第三个循环模式要按需开启后面我会专门讲循环模式和普通模式的区别。DMA配好之后还要在USART侧使能接收方向的DMA请求同时开启IDLE中断usart_dma_receive_config(USART0, ENABLE); usart_interrupt_enable(USART0, USART_INT_IDLE); nvic_irq_enable(USART0_IRQn, 2, 0);注意顺序先使能DMA接收请求再开IDLE中断。如果反过来可能在DMA通道还没就绪的时候USART已经开始接收数据一帧结束后IDLE标志置位但DMA通道是空闲的这一帧就会丢掉。4.2 空闲中断处理与数据长度计算中断服务函数是这样写的volatile uint16_t uart0_rx_len 0; volatile uint8_t uart0_rx_frame_ok 0; void USART0_IRQHandler(void) { if (RESET ! usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE)) { usart_interrupt_flag_clear(USART0, USART_INT_FLAG_IDLE); dma_channel_disable(DMA0, DMA_CH3); uart0_rx_len UART0_RX_BUFFER_SIZE - dma_transfer_number_get(DMA0, DMA_CH3); uart0_rx_frame_ok 1; dma_channel_enable(DMA0, DMA_CH3); } }计算长度的原理很简单DMA的传输计数器存放的是“还剩多少个字节没传”初始值是256。IDLE中断触发时当前帧已经接收完毕此时计数器已经减去了实际收到的字节数所以“初始化长度减去剩余计数”就是本帧的字节数。这里有一个我踩过坑的细节关DMA、读长度、重新使能DMA这三步必须严格按顺序来。如果先读长度再关DMA在你读取的瞬间DMA可能又搬运了几个字节读到的剩余计数并不是本帧结束时的准确值长度就会偏大或偏小。同样重新使能DMA前要确保帧完成标志已经被应用层取走否则下一帧到达时会把同一块数据又算一遍。IDLE标志的清除必须按照手册的序列来做也就是读状态寄存器、再读数据寄存器。GD32固件库的usart_interrupt_flag_clear已经封装好了这个动作直接调用即可。如果漏了这个操作IDLE中断会反复进入表现就是主循环里的帧完成标志被疯狂置位看起来像芯片死机了一样。4.3 循环模式/普通模式的选择与现场经验DMA的循环模式和普通模式很多人选不明白。我在项目中是这样权衡的循环模式下DMA搬运完256字节后自动回到起始地址继续搬运无需软件干预。这种模式和空闲中断天然搭配你不关心缓冲区什么时候满只关心“当前这帧结束后缓冲区里到底多了哪些数据”。坏处是如果应用层处理不及时新数据会覆盖旧数据。所以缓冲区大小一定要结合项目里最大的报文长度和协议处理速度来定我一般留至少2倍余量。普通模式下DMA搬运完设定长度后自动停止必须软件重新设置number并再次使能才能开始下次接收。这和空闲中断就不太好配合了因为空闲中断触发时DMA未必搬运完而一旦搬运完DMA就停了后续数据就进不来了。普通模式更适合定长接收或者你明确知道协议帧长度上限的场景。我之前做过一个modbus从机项目报文最长256字节我用循环模式加256字节缓冲区跑起来后发现一个问题主站连续发两条报文时第二条报文只有几十字节但因为缓冲区是循环的DMA计数没法区分“这一圈”和“上一圈”的数据。后来我改成缓冲区翻倍应用层处理完后立刻推进读指针同时在空闲中断里加了一个帧序号判断逻辑才彻底解决。所以遇到数据错位的兄弟优先检查缓冲区溢出和帧序号逻辑不要一上来就怪DMA配置有问题。5. 发送DMA配置与缓冲区管理经验接收方向搞定了接下来就是发送方向。发送DMA的核心价值在于“非阻塞”但非阻塞也带来一个比较棘手的问题缓冲区生命周期管理。5.1 发送DMA初始化与触发流程USART0的TX对应DMA0的通道4。发送方向的DMA配置和接收方向几乎对称只是方向反了void uart0_dma_send(uint8_t *buf, uint16_t len) { dma_parameter_struct dma_para; dma_deinit(DMA0, DMA_CH4); dma_struct_para_init(dma_para); dma_para.direction DMA_MEMORY_TO_PERIPHERAL; dma_para.memory_addr (uint32_t)buf; dma_para.memory_inc DMA_MEMORY_INCREASE_ENABLE; dma_para.memory_width DMA_MEMORY_WIDTH_8BIT; dma_para.number len; dma_para.periph_addr (uint32_t)USART0-DATA; dma_para.periph_inc DMA_PERIPH_INCREASE_DISABLE; dma_para.periph_width DMA_PERIPHERAL_WIDTH_8BIT; dma_para.priority DMA_PRIORITY_HIGH; dma_init(DMA0, DMA_CH4, dma_para); dma_circulation_disable(DMA0, DMA_CH4); dma_channel_enable(DMA0, DMA_CH4); }注意发送方向我用的是普通模式关闭循环。因为发送是一锤子买卖发完就结束。如果开了循环DMA会反复把同一块缓冲区内容发出去除非你是有意做周期广播否则这就是个隐藏bug。配置好DMA后还需要在USART初始化时使能发送方向的DMA请求usart_dma_transmit_config(USART0, ENABLE);如果这个请求没使能即使DMA通道配好了也发了使能数据也不会从USART口出去因为USART根本没把“发送数据寄存器空”这个事件挂到DMA请求线上。这个问题很隐蔽我一度以为是自己DMA配置有问题折腾了半天才发现是漏了这一步。5.2 缓冲区生命周期、完成中断与低功耗注意点发送DMA最大的一个坑前面提过再强调一次千万不要把局部变量数组传给发送函数。下面这个写法看起来没问题实际跑起来数据就是乱的void send_test(void) { uint8_t buf[64]; memset(buf, A, sizeof(buf)); uart0_dma_send(buf, sizeof(buf)); // 错 }函数执行完buf所在栈空间就被回收了DMA并不知道还在按照之前记录的地址搬运搬出来的内容完全不可控。正确做法是定义成全局数组、静态数组或者用动态内存并保证在发送完成前不释放。发送完成的通知通常有三种做法。第一种是查询DMA通道的完成标志这个标志置位说明DMA已经把数据全部搬运到USART的数据寄存器。第二种是开DMA完成中断void DMA0_Channel4_IRQHandler(void) { if (RESET ! dma_interrupt_flag_get(DMA0, DMA_CH4, DMA_INT_FLAG_FTF)) { dma_interrupt_flag_clear(DMA0, DMA_CH4, DMA_INT_FLAG_FTF); dma_channel_disable(DMA0, DMA_CH4); uart0_tx_done 1; } }第三种是干脆不关心中断只在下次发送前判断上一次是不是已经发完。对于大多数协议类项目第一种或第三种已经够用。如果你要把串口降到低功耗模式或者发送完立刻关闭串口光等DMA完成中断还不够。DMA完成只代表数据从内存搬到了USART的发送数据寄存器不代表移位寄存器已经把最后一个位发到了TX引脚。这时候必须额外等待USART的发送完成标志置位while (RESET usart_flag_get(USART0, USART_FLAG_TC)) { }这个等待时间很短但对低功耗或关串口的场景是必须的否则最后几个字节会丢。我遇到过一次发送完数据立刻进入停止模式结果对端总是少收最后两个字节加了这个操作就好了。6. 常见问题排查与避坑速查调试USART的过程里我攒了不少“看上去跟串口无关”但实际就是串口引起的诡异问题这一章整理给你做参考。6.1 典型问题速查表现象最可能的原因排查方法接收到的数据全部乱码波特率不匹配或时钟初始化错误用示波器或逻辑分析仪测TX引脚波形核对波特率只收到第一个字节后面全丢RBNE中断里处理时间太长或没及时读取数据导致溢出中断只做数据搬运解析移到主循环检查ORE标志串口发数据对端没反应USART发送DMA请求没使能确认usart_dma_transmit_config已调用DMA搬运的数据是垃圾外设地址写错或缓冲区地址不是全局地址打印剩余计数结果核对USART0-DATAIDLE中断疯狂进入IDLE标志没清除确认调用usart_interrupt_flag_clear总线空闲时TX引脚为低电平引脚复用或模式配置不对用示波器看空闲电平正常应为高发送完成立即关串口导致丢字节没有等待TC标志关闭前先等待TC置位6.2 几个容易被忽略的故障点第一个是GPIO复用冲突。GD32F303的PA9/PA10复用功能是USART0但不是所有工程配置都能自动处理好这个映射。如果有两个外设同时占用PA9后初始化的那个会悄悄改掉前一个的复用设置表现就是串口忽然不通了。遇到这种奇怪现象先把其他外设的GPIO初始化注释掉逐个排除。第二个是DMA通道被其他外设占用。GD32F303的DMA通道是共享的比如ADC采集用DMA、SPI收发用DMA如果配置顺序不当后配置的会覆盖先配置的通道设置。我建议在工程里把DMA通道分配列成一张表每个外设独占一条通道避免交叉使用。网上有人问“GD32E230的ADC用DMA采集数据会紊乱”排到最后也是通道复用问题思路是一样的。第三个是关于“LIN模式下串口发送出去的数据会触发接收中断吗”这类问题。正常全双工模式下TX发送的数据不会进入RX路径自然也不会触发接收中断。但如果你把板子的TX和RX用杜邦线短接做回环测试或者误开了单线半双工模式发送的数据就会被自己的接收端收到这时候触发接收中断是正常的千万别以为是芯片bug。很多看起来像灵异现象的问题其实都是硬件回环造成的。第四个是DMA的连续请求参数。有些外设的DMA请求支持连续模式开启之后DMA会按外设的突发长度连续搬运而不是每个事件搬一次。USART接收场景默认不开连续模式保持普通事件请求就行。如果你从其他项目的例程里复制了DMA配置记得检查这个参数否则USART接收可能出现一帧数据被拆成好几段进入缓冲区的情况。最后分享一个我自己的习惯串口调试阶段不管最终方案是中断还是DMA我都会先把最简单的查询式收发跑通确认硬件通路没问题再逐步加DMA和中断。这样做的好处是一旦后面的高级方案出问题至少能确定问题出在哪一层不用把硬件、驱动、协议三者的锅混在一起查。USART这套东西说到底是所有嵌入式外设里最基础也最考验细心程度的一环配置顺序、标志位清除顺序、缓冲区生命周期每一个细节都藏着坑踩过一遍之后你就会形成自己的避坑反射。这篇先写到这里后续我还会把GD32F303其他外设的调试记录陆续补上有同样在搞GD32的兄弟欢迎一起交流。
返回列表