ARTICLE DETAIL

资讯详情

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

GD32F303串口USART三种收发方式:中断、空闲中断+DMA、发送DMA详解

GD32F303串口USART三种收发方式:中断、空闲中断+DMA、发送DMA详解 调串口这件事可以说是嵌入式开发里最绕不开的环节了。做控制板要跟传感器通信做采集模块要给上位机回数据做设备联网要先过AT指令这一关哪哪都离不开USART。我前阵子用GD32F303重写一套采集模块的通信协议从最基础的接收中断开始到接收空闲中断DMA实现不定长帧解析再把发送侧也切到DMA整个过程踩了不少坑也把USART的几个收发姿势挨个摸了一遍。这篇就把三种方式的原理、代码、调试要点全部记录下来给正在折腾GD32F303的朋友做个参考。文章适合刚上手GD32的开发者也适合从STM32F103迁过来、对GD32固件库API不太熟的人内容偏实操可以直接对着抄作业。1. 三种USART收发方案怎么选1.1 先看清GD32F303的家底GD32F303是兆易创新基于Arm Cortex-M4内核做的通用MCU主频能跑到120MHzFlash和SRAM的配置也比较丰富。串口资源方面以常见的GD32F303系列为例一般会有USART0、USART1、USART2以及UART3、UART4等多个串口外设其中USART0/1/2功能更全支持同步模式、LIN、智能卡这些高级功能UART3/4则精简一些。对调试来说几个关键点要心里有数USART0默认映射在PA9(TX)/PA10(RX)使用AF7复用功能串口时钟使能走RCUReset and Clock UnitUSART0挂在APB2总线上别把时钟树搞错GD32F303有两个DMA控制器分别是DMA0和DMA1每个控制器下面有多个通道。USART的收发DMA请求已经映射到了特定通道具体要查GD32F30x参考手册里的DMA请求映射表不能拍脑袋选通道。GD32的寄存器布局和STM32F103有很高的相似度所以很多人从F103迁过来会觉得亲切。但注意“相似”不等于“相同”GD32的固件库API、部分外设寄存器命名、DMA请求映射、中断标志位命名都不一样。比如STM32习惯叫RXNE接收寄存器非空GD32固件库里这个标志经常写成RBNEReceive Buffer Not Empty函数名也有差异。直接照搬STM32的HAL库代码过来是编译不过的。1.2 接收中断、空闲中断DMA、发送DMA的定位我在实际项目里是这么定义三种方案的方案核心机制适用场景CPU负载代码复杂度接收中断每收到1字节触发RBNE中断CPU逐字节读取低速、短帧、交互不频繁的控制命令高每字节都要进中断最低接收空闲中断DMADMA自动把数据搬到内存总线空闲时触发IDLE中断CPU一次性处理整帧不定长帧通信如Modbus、AT指令、GPS NMEA、私有协议低一帧只进一次中断中发送DMACPU配置好DMA后数据自动从内存搬到USART发送寄存器批量日志、固件升级、周期上报大量数据低发送过程不占CPU中有些朋友问CAN总线一般用中断接收还是DMA接收这里顺带说一句CAN外设本身通常没有独立的DMA接口所以CAN收发一般还是走中断FIFO的软件处理方式和USART的DMA接收不是一回事不能套用。1.3 为什么“空闲中断DMA”是串口协议解析的主流串口通信的帧格式千奇百怪有固定长度的也有不定长的。固定长度好办DMA配置好长度传输完成中断一到就是完整一帧。但实际产品里更多是不定长帧比如Modbus RTU以3.5个字符时间的静默作为帧结束AT指令以\r\n结尾这些都没有固定长度没法直接用DMA传输完成中断来判断“一帧结束”。这时候就要用到USART的空闲中断IDLE。所谓空闲是指RX线上接收到一个字节后持续一个完整字节周期的高电平状态。通俗点说主机发完一帧数据后总会有个停顿这个停顿就是“空闲帧”。把DMA和IDLE中断配合起来DMA负责把数据从接收寄存器搬到内存缓冲CPU不用管每字节的到达等到IDLE中断触发说明一帧发完了CPU再从DMA缓冲区里取出整帧数据来处理。这样一帧数据只进一次中断CPU负载大幅下降。这个方案在115200波特率以下非常稳。我实测过115200波特率下每字节间隔约87us用接收中断的话每87us就要打断一次CPU而用DMAIDLE如果一帧是100字节CPU只被打断一次效率差距是数量级的。2. 先跑通最基础的接收中断2.1 接收中断的执行流程接收中断是整个串口调试的起点也是排查问题最小集。先把这条路跑通后面DMA方案出了问题也能回退到这个最简单状态来排查。接收中断的流程是这样的USART在RX引脚上检测到起始位逐个位移位接收一个字节完成后硬件会把数据放进接收数据寄存器同时把RBNE接收缓冲非空标志置1。如果此时RBNE中断已经使能且NVIC里USART0中断也开着CPU就会跳进USART0_IRQHandler。在中断服务函数里读USART_DATA寄存器RBNE标志会自动清除。这是“每字节进一次中断”的典型模式优缺点非常鲜明优点实现简单任意长度任意数据都能收不需要事先知道帧长度缺点高波特率下中断频率高CPU负载大而且中断服务函数里如果处理太久下一个字节来了会触发溢出错误ORE数据直接丢。所以接收中断一般只用于小数据量场景或者作为其他方案调试时的第一步验证。2.2 初始化代码与中断处理GD32F303的接收中断初始化我习惯按“时钟-引脚-串口参数-中断”四步来写void usart0_rx_irq_init(uint32_t baud) { /* 1. 使能时钟 */ rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_USART0); /* 2. 配置PA9/PA10为复用功能注意AF7对应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); /* 3. 初始化USART0参数 */ usart_deinit(USART0); usart_baudrate_set(USART0, baud); usart_word_length_set(USART0, USART_WL_8BIT); usart_stop_bit_set(USART0, USART_STB_1BIT); usart_parity_config(USART0, USART_PM_NONE); usart_receive_config(USART0, USART_RECEIVE_ENABLE); usart_transmit_config(USART0, USART_TRANSMIT_ENABLE); usart_enable(USART0); /* 4. 使能接收中断并配置NVIC */ usart_interrupt_enable(USART0, USART_INT_RBNE); nvic_irq_enable(USART0_IRQn, 0, 0); }中断服务函数里最简单的收据缓存可以这样写#define RX_BUFF_SIZE 256 uint8_t rx_buff[RX_BUFF_SIZE]; volatile uint16_t rx_len 0; void USART0_IRQHandler(void) { if (usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE) ! RESET) { rx_buff[rx_len] usart_data_receive(USART0); if (rx_len RX_BUFF_SIZE) { rx_len; } } }注意中断服务函数里尽量只做“搬数据”这件事协议解析放到主循环或者更低优先级的地方去处理。我见过有人直接把Modbus解析写进中断里的一帧几十个寄存器的读写操作耗掉几百微秒直接把系统拖垮。2.3 接收中断里容易踩的几个坑第一个坑RBNE标志没有手动清除的“焦虑”。读usart_data_receive()会自动清标志你不要想着去调一个usart_flag_clear来清RBNE反而会把事情搞乱。GD32固件库里确实提供了一些标志清除函数但RBNE这个标志走的是“读数据即清除”的硬件逻辑。第二个坑溢出错误。如果中断响应不及时前一个字节还在接收数据寄存器里没读走后面一个字节又到了USART硬件就会置ORE溢出错误标志这时候新数据是丢的。所以用接收中断时中断优先级别设太低服务函数里也别干重活代码里最好处理一下错误标志if (usart_interrupt_flag_get(USART0, USART_INT_FLAG_ERR) ! RESET) { usart_flag_clear(USART0, USART_FLAG_ORERR); }这里不同固件库版本的函数名有差异有的叫usart_interrupt_flag_clear有的叫usart_flag_clear以你手上的版本为准关键是理解你要处理的是“溢出错误”这件事。第三个坑缓冲区固定长度越界。上面示例里如果数据超过RX_BUFF_SIZE我做了个简单保护但更好的做法是用环形缓冲区。串口接收做成环形队列是基本功接收中断里只管把字节塞进队列主循环里再从队列取走解析这样即使主循环忙于其他事情数据也不容易丢。3. 用空闲中断DMA实现不定长帧接收3.1 工作原理DMA搬数据空闲帧当帧尾这是一个我强烈推荐的串口接收方案也是目前项目中用得最多的那种。原理拆开来看其实就两句话DMA自动把USART接收数据寄存器里的字节搬到内存缓冲区每收到一个字节DMA通道的CNT计数器减1当RX线空闲停止一段时间时USART产生IDLE中断CPU在中断里读一下DMA还剩多少个字节没搬用“配置长度减去剩余长度”就能算出本次接收了多少字节。这个方案的精髓在于“一帧只进一次中断”。不管这一帧是10字节还是500字节CPU都只在总线空闲那一刻被中断一次。而且因为DMA搬数据足够快接收数据寄存器基本不会堆积ORE溢出的概率也比纯中断方式低很多。适用场景非常广Modbus RTU、AT指令、GPS NMEA、自定义的变长协议都可以直接用。3.2 完整配置流程DMAIDLE配置步骤分为四块GPIO和USART初始化、DMA通道初始化、使能USART的DMA接收请求、使能IDLE中断。#define RX_DMA_BUF_SIZE 512 uint8_t dma_rx_buf[RX_DMA_BUF_SIZE]; volatile uint32_t dma_rx_len 0; void usart0_dma_rx_idle_init(uint32_t baud) { /* 1. GPIO和USART初始化同接收中断那步略去重复代码 */ rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_USART0); rcu_periph_clock_enable(RCU_DMA0); 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); usart_deinit(USART0); usart_baudrate_set(USART0, baud); usart_word_length_set(USART0, USART_WL_8BIT); usart_stop_bit_set(USART0, USART_STB_1BIT); usart_parity_config(USART0, USART_PM_NONE); usart_receive_config(USART0, USART_RECEIVE_ENABLE); usart_transmit_config(USART0, USART_TRANSMIT_ENABLE); usart_enable(USART0); /* 2. 配置DMA0通道x外设地址是USART0数据寄存器 */ dma_parameter_struct dma_init_struct; dma_deinit(DMA0, DMA_CH3); dma_init_struct.periph_addr (uint32_t)(USART_DATA(USART0)); dma_init_struct.periph_inc DMA_PERIPH_INCREASE_DISABLE; dma_init_struct.memory_addr (uint32_t)dma_rx_buf; dma_init_struct.memory_inc DMA_MEMORY_INCREASE_ENABLE; dma_init_struct.periph_width DMA_PERIPHERAL_WIDTH_8BIT; dma_init_struct.memory_width DMA_MEMORY_WIDTH_8BIT; dma_init_struct.direction DMA_PERIPHERAL_TO_MEMORY; dma_init_struct.number RX_DMA_BUF_SIZE; dma_init_struct.priority DMA_PRIORITY_HIGH; dma_init(DMA0, DMA_CH3, dma_init_struct); /* 3. 先关闭DMA通道配置好数量后再使能 */ dma_channel_disable(DMA0, DMA_CH3); dma_transfer_number_config(DMA0, DMA_CH3, RX_DMA_BUF_SIZE); dma_channel_enable(DMA0, DMA_CH3); /* 4. 使能USART的DMA接收请求和IDLE中断 */ usart_dma_receive_config(USART0, ENABLE); usart_interrupt_enable(USART0, USART_INT_IDLE); nvic_irq_enable(USART0_IRQn, 1, 0); }需要注意一点DMA通道不是随便选的。USART0_RX的DMA请求映射到了DMA0的某个通道USART0_TX又映射到另一个通道具体是哪个必须以GD32F30x参考手册的DMA请求映射表为准。上面代码里写DMA_CH3只是举例实际用的时候不要照抄查一下手册或者用厂家提供的库函数去映射。3.3 数据长度怎么算这是整个方案里最容易理解错的点。DMA传输数量寄存器CNT存的是“还剩多少没搬”不是“已经搬了多少”。举个例子我把DMA配置长度设为512主机发来一帧40字节的数据DMA把40字节全部搬到缓冲区后CNT寄存器里的值就是 512 - 40 472。IDLE中断触发时用配置长度减去CNT当前值就得到实际接收长度void USART0_IRQHandler(void) { if (usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE) ! RESET) { /* 清IDLE标志GD32库有现成函数最好没有就按读STAT再读DATA的方式清 */ usart_interrupt_flag_clear(USART0, USART_INT_FLAG_IDLE); /* 算长度 */ dma_rx_len RX_DMA_BUF_SIZE - dma_transfer_number_get(DMA0, DMA_CH3); if (dma_rx_len 0) { /* 这里就是一帧完整数据 */ handle_rx_frame(dma_rx_buf, dma_rx_len); } /* 准备接收下一帧重新配置DMA传输数量 */ dma_channel_disable(DMA0, DMA_CH3); dma_transfer_number_config(DMA0, DMA_CH3, RX_DMA_BUF_SIZE); dma_channel_enable(DMA0, DMA_CH3); } }关于清IDLE标志GD32不同固件库版本提供的接口不太一样。有的版本直接支持usart_interrupt_flag_clear(USART0, USART_INT_FLAG_IDLE)如果你用的版本没有这个函数就按老办法清先读一下USART_STAT状态寄存器再读一下USART_DATA数据寄存器IDLE标志就清了。这个操作顺序是硬件规定的别反了。3.4 为什么DMA方案比纯中断更不容易溢出USART接收数据寄存器只有一个字节深CPU读慢了就会堆积。纯中断方案里如果主循环里有耗时操作中断优先级低或者被关中断了数据就可能会丢即便优先级设得再高每字节一次中断的开销也是实打实的CPU负载。DMA方案把“从寄存器搬数据”这件事从CPU手里接过去了。DMA没有中断延迟收到一个字节搬一个字节只要总线带宽够接收寄存器根本不会堆积。DMA搬运一个字节基本就是一两个时钟周期的事比软件进中断、压栈、读寄存器、弹栈要快得多。不过DMA方案也不是没有风险。如果一帧数据超过DMA缓冲区大小普通模式下DMA会搬满缓冲区后停止后面的数据就丢了而且IDLE中断可能不会按预期触发。所以缓冲区大小要根据最大帧长来定一般留两倍余量比较稳。3.5 循环模式还是普通模式GD32的DMA支持循环模式circulation mode。区别在于普通模式DMA搬完number个数据后通道自动关闭CNT变成0。需要软件重新配置传输数量并重新使能通道才能再次接收。好处是数据不会覆盖坏处是IDLE中断里得多做几步重置操作。循环模式DMA搬完number个数据后硬件自动把地址和计数器恢复到初值继续搬运新数据软件不用干预。好处是接收可以连续进行坏处是如果缓冲区满了而主机还在发新数据会覆盖旧数据。我做不定长接收时一般先用普通模式。每帧处理完在IDLE中断里重新配置一次DMA。这样逻辑清晰数据不会互相覆盖。如果确认帧长小于缓冲区一半而且帧间隔比较明显用循环模式也可以能少写几行重置代码。4. 发送DMA给CPU减负的正确姿势4.1 发送DMA的基本原理接收方向有DMA发送方向自然也有。发送DMA做的事情是把内存缓冲区里的数据自动搬运到USART发送数据寄存器CPU只管配置好DMA然后该干嘛干嘛数据发完了DMA会置一个传输完成标志CPU再查一下美国硬件的TC标志确认所有字节都从移位寄存器出去了就可以回收这个DMA通道。发送DMA的收益在大量数据发送时特别明显。比如固件升级要往串口写几十KB数据用轮询方式发CPU全程卡在while里等TXE用中断方式发每发一字节进一次中断也挺烦。用DMA发CPU把DMA通道一开回头去干别的等到发送完成标志置位再来收尾就行。4.2 发送DMA配置与代码发送DMA的代码套路我一般写成这样void usart0_dma_send(uint8_t *buf, uint32_t len) { if (buf NULL || len 0) { return; } /* 1. 配置DMA方向为内存到外设 */ dma_parameter_struct dma_init_struct; dma_deinit(DMA0, DMA_CH2); dma_init_struct.periph_addr (uint32_t)(USART_DATA(USART0)); dma_init_struct.periph_inc DMA_PERIPH_INCREASE_DISABLE; dma_init_struct.memory_addr (uint32_t)buf; dma_init_struct.memory_inc DMA_MEMORY_INCREASE_ENABLE; dma_init_struct.periph_width DMA_PERIPHERAL_WIDTH_8BIT; dma_init_struct.memory_width DMA_MEMORY_WIDTH_8BIT; dma_init_struct.direction DMA_MEMORY_TO_PERIPHERAL; dma_init_struct.number len; dma_init_struct.priority DMA_PRIORITY_HIGH; dma_init(DMA0, DMA_CH2, dma_init_struct); /* 2. 使能USART发送方向的DMA请求 */ usart_dma_transmit_config(USART0, ENABLE); /* 3. 启动DMA传输 */ dma_channel_enable(DMA0, DMA_CH2); /* 4. 等待DMA搬完再等USART最后一字节发出 */ while (dma_flag_get(DMA0, DMA_CH2, DMA_FLAG_FTF) RESET) { } while (usart_flag_get(USART0, USART_FLAG_TC) RESET) { } /* 5. 关闭DMA通道USART的DMA请求可以留着 */ dma_channel_disable(DMA0, DMA_CH2); }最后这个dma_flag_get的参数在不同固件库版本里写法不太一样有的叫DMA_INT_FLAG_FTFfull transfer finish有的是DMA_FLAG_FTF还有老版本是DMA_FLAG_TC。只要你理解这个标志的含义是“DMA把配置的number个数据传输完毕了”就行函数名以当前固件库为准。4.3 最后一字节丢数据的坑发送DMA最经典的坑就是DMA把数据搬完了但USART的移位寄存器可能还有最后一个字节没发出去。如果这时候你直接把DMA通道关了甚至把串口发送禁用了最后一个字节就丢了。这也是为什么上面代码里在等待DMA传输完成标志之后还要再等一个USART_FLAG_TC。TCtransmission complete标志的含义是“移位寄存器里最后一个字节已经发送出去TX线上回到了空闲状态”。只有TC置位才能保证这一帧数据真的全部发完了。我最早写发送DMA时偷懒只等了DMA的标志没等TC结果用示波器一抓发现每次发送完最后一个字节总是缺了半个位的时间后面排查了半天才发现是这个问题。这个坑值得单独记一笔。另一个要注意的是DMA通道的复用。如果同一个DMA通道被多个外设共用必须先做完一个传输再配置下一个不能这边还发着数据呢那边又重新初始化DMA通道会把正在进行的传输打断。4.4 什么时候适合用发送DMA发送DMA并不是所有场景都适用。如果你的数据量很小比如每次只发几个字节的状态帧用查询或者中断方式发就挺够用的DMA的配置开销反而显得多余。适合用发送DMA的场景是数据长度有一定规模比如几十字节到几千字节发送频率稳定比如每秒都要上报一次CPU在发送期间有更重要的事要做比如传感器采集、PID运算、显示刷新。配置DMA通道本身也要花几个us发送1个字节也走这套流程就有点“杀鸡用牛刀”了。5. 调试心得与排错实录5.1 裸板调串口我为什么建议先回环测试拿到一块新板子不管三七二十一先把代码写满再调试出了问题定位很痛苦。我现在的习惯是先把硬件回环测试做了把USART0的TX和RX引脚用杜邦线短接然后写个最简单的程序串口初始化好后直接usart_data_transmit发一个字节再用接收中断收回来。如果自己发自己收能通说明GPIO复用、串口时钟、波特率配置基本都没问题。回环测试通过之后再通过USB转TTL工具接到电脑上用串口助手测试跟PC的通信。如果接上PC后收发异常优先检查地线是否共地、电平是否匹配、波特率是否一致。这一步很基础但能筛掉一大半的问题。5.2 常见现象与排查思路速查表调试过程中我整理了一个速查表遇到问题先对着表排查效率高很多现象可能原因排查方法完全收不到数据IO复用配置错误、串口未使能、DMA通道映射错先做回环测试再用逻辑分析仪看RX引脚有没有波形数据显示乱码波特率不匹配、单片机实际时钟频率不对、数据位/校验位配置不一致用示波器抓波形量单bit时间是否对应波特率接收中断一直不触发RBNE中断没使能、NVIC没配好、串口没有实际收到数据在初始化后加一个测试点手动发一个字节看标志位空闲中断不触发IDLE中断没使能、RX线上一直是高电平、RBNE未及时读导致状态异常确认主机确实发了数据且有帧间隔用示波器看总线空闲时间DMA收到的长度总是0IDLE触发后未正确读取CNT、缓冲区指针为空、DMA通道没使能断点查看DMA通道寄存器CNT值先确认DMA本身在搬数据DMA收到长度正确但数据错位缓冲区地址没对齐、DMA数据宽度和外设宽度不匹配统一用8bit宽度检查memory_addr是否指向了正确的数组发送DMA最后少一个字节没有等待USART TC标志、DMA被提前关闭在关DMA前加TC等待用示波器确认最后一bit完整5.3 从STM32F103迁移到GD32重点检查这几处GD32F303和STM32F103的很多外设逻辑是相似的迁移起来不算难但常有几个地方会踩坑第一外设地址不同。GD32F303和STM32F103的外设基地址不完全一样直接复制STM32工程的寄存器地址或者老宏定义很可能指向错误的寄存器。建议统一使用GD32固件库提供的宏和外设访问方法不要自己硬编码地址。第二固件库API风格差异很大。GD32的库不是STM32标准外设库的直接翻版函数命名和参数结构都有不同。比如GPIO初始化STM32标准库习惯GPIO_InitStructure GPIO_InitGD32可能是gpio_mode_set gpio_output_options_set gpio_af_set。迁移代码前先花半小时过一遍固件库里的usart、gpio、dma、rcu这几个模块的API后面能省很多事。第三中断标志命名差异。前面的代码里提到过USART接收非空标志在STM32里叫RXNEGD32固件库里经常叫RBNE。还有DMA传输完成标志有的库叫FTF有的叫TC写代码前先确认你用的版本里怎么定义的。第四时钟树要查手册。STM32F103的外部时钟默认8MHz、主频72MHzGD32F303常见主频是120MHz系统时钟配置不一样会直接影响波特率。GD32的USART波特率寄存器值是基于其APB时钟算的如果时钟没配对数据发出来全是乱码。5.4 固件库函数不够用直接操作寄存器兜底固件库函数确实方便但版本差异坑人。有时候找一个清标志的接口翻半天文档不如直接操作寄存器来得痛快。调试USART的时候我偶尔会直接用寄存器来确认硬件状态。比如USART0的数据寄存器用固件库宏可以写成USART_DATA(USART0)这实际上就是一个地址映射的宏。读数据就是从这个地址取值写数据就是往这个地址写值。状态寄存器也是一样的思路读一下USART_STAT(USART0)就能看到RBNE、TC、IDLE这些位的实时状态比在调试器里翻结构体要直观得多。DMA通道的CNT寄存器也是可以直读的用dma_transfer_number_get(DMA0, DMA_CH3)或者直接读DMA_CHCNT(DMA0, DMA_CH3)都行。不过这些宏的具体形式要根据固件库版本稍作变化查一下头文件定义就好不用死记。5.5 关于LIN模式和CAN接收的热搜问题简单说两句最近看到一些同学在问LIN模式下串口发送出去的数据会触发接收中断吗还有CAN总线一般用中断接收还是DMA接收。这两个问题跟本文的USART调试有一定关联但不完全是一回事。LIN模式和普通USART模式在GD32上是一个外设的不同工作模式。如果在调试中开了LIN或者回环模式自己发出去的数据确实可能被自己接收这在总线调试时要先排除掉别让它干扰你的正常接收中断判断。建议调试阶段先把工作模式固定为最普通的异步收发模式把回环关掉。CAN的情况前面提过CAN外设通常没有DMA接口实际项目以中断接收FIFO为主。由于CAN有报文ID过滤和硬件FIFO接收机制和USART差异很大不要照着USART DMA的思路硬套。最后分享一点我的调试习惯代码写到这里其实已经把GD32F303的USART三种收发方式都串了一遍。我个人最大的体会是串口调试最怕的不是技术难而是没有章法。如果你也卡在某一步调不通建议按照“回环测试-单字节收发-断点看标志位-再上协议解析”这个顺序重新走一遍大概率能快速定位问题。另外一个可以留作扩展的点是如果你的协议帧有固定帧头帧尾可以在IDLE中断的基础上再加一层软件状态机过滤掉不完整帧或校验失败的帧如果数据量特别大缓冲区可以设计成双缓冲DMA在后台往一个缓冲区搬数据主循环或另一个中断在处理上一个缓冲区这样可以做到收发和处理完全流水线化。这些我后面整理好了再单独写一篇。希望这篇能帮到正在GD32F303上折腾USART的朋友少踩几个我踩过的坑。
返回列表