XMC1400四路UART并行通信:USIC中断与环形缓冲区实战解析
简介这是一份面向英飞凌XMC系列开发者的四路串口并行通信基本例程压缩包内含完整Keil工程解决多路UART同时收发时的引脚映射、中断配置与数据流组织问题适合刚接触XMC USIC模块或需要快速移植多串口应用的工程师。资源共69个文件源码以c/h文件为主另有uvprojx工程文件、o/crf编译中间文件、map/lst链接与列表文件以及Readme说明包体约1.42MB结构清晰。目前已有530人学习下载。例程给出了UART0_CH0、UART0_CH1、UART1_CH0、UART1_CH1四路通道的完整宏定义包括TX/RX引脚选择、复用功能和接收中断号配合retarget_io、ring_buffer、serial、shell等模块可帮助读者理解XMC多路串口的初始化流程、环形缓冲收发机制以及串口调试命令扩展方法。下载后对照工程即可直接验证四路并行通信效果也能作为后续项目的基础模板反复复用。1. 场景很常见一台设备要同时挂多个串口外设瓶颈往往在中断搬运场景很常见一台设备要同时挂GPS、姿态传感器、调试串口和上位机主控用的是英飞凌XMC14系列。很多人以为四路串口并行通信必须上RTOS或DMA实际上XMC的USIC模块天然支持单模块双通道两个USIC就能拼出四路硬件UART配合中断和环形缓冲区就能稳定跑。这套UART_Demmo例程正是干这件事的UART0和UART1两个模块共四个通道全部接入shell任意一路收数据都不阻塞其他路。适合刚拿到XMC1400开发板、要同时处理多个TTL串口外设的嵌入式工程师。理解了这套结构后面想扩展到TC2xx多路串口或自定义帧协议不用推翻重来。2. USIC通道映射先把四路UART的引脚、中断源和复用功能对齐2.1 USIC是UART的物理基础一个USIC模块就是两个独立串口XMC的UART不叫USART而是挂在可编程串行通信单元USIC上的协议引擎。每个USIC模块拥有两个独立通道编号是CH0和CH1通道共享输入输出节点但控制寄存器彼此独立天然支持并行收发。四路串口的本质是USIC0_CH0、USIC0_CH1、USIC1_CH0、USIC1_CH1。每个通道的波特率发生器BRG独立可分别设置成4800、115200和921600而互不干扰。这和STM32不一样STM32里一个USART是独立外设XMC则把UART、SPI、I2C统一抽象成USIC协议处理换协议时不用换引脚只需修改通道的协议配置。许多从STM32转过来的开发者第一次看XMC初始化代码会疑惑为什么配置UART还要设输入节点DX0原因在USIC的输入十字开关任意DX0~DX3输入可以路由到任意通道宏定义里的USIC0_C0_DX0_P2_0就是告诉硬件把P2_0信号送到USIC0通道0的数据输入端。如果这里写错中断能触发但读到的数据永远是错位或空的。顺带对比一下接口类型UART是异步收发收发双方各自用自己的时钟所以波特率误差必须控制在正负2%以内SPI和I2C是同步通信由主机提供时钟。XMC的USIC也可以运行在I2C或SPI模式但本例程只按UART使用。四路串口并行通信时尤其要注意各路外设可能参考时钟不同不能用同一个波特率配置去适配所有设备。2.2 四路串口的引脚和中断源宏定义背后的映射关系例程关键信息都集中在头文件宏定义里抽取其中一路展开看#define UART0_CH0 XMC_UART0_CH0 #define UART0_CH0_TX_PIN P2_1 #define UART0_CH0_TX_PIN_AF (XMC_GPIO_MODE_t)((int32_t)XMC_GPIO_MODE_OUTPUT_PUSH_PULL | (int32_t)P2_1_AF_U0C0_DOUT0) #define UART0_CH0_RX_PIN P2_0 #define UART0_CH0_RX_INPUT USIC0_C0_DX0_P2_0 #define UART0_CH0_RX_IRQN IRQ15_IRQn第一行把外设句柄重命名成更短符号后续所有初始化代码都能用UART0_CH0引用。TX_PIN_AF是“输出推挽”与“复用功能P2_1_AF_U0C0_DOUT0”的按位或组合而不是简单的一个AF编号。RX_INPUT则绑定输入节点DX0和引脚P2_0IRQ15是这一路串口的中断请求号。把四个通道的宏全部平铺后对应关系如下逻辑名外设通道TX引脚RX引脚RX输入节点RX中断UART0_CH0XMC_UART0_CH0P2_1P2_0USIC0_C0_DX0_P2_0IRQ15UART0_CH1XMC_UART0_CH1P0_7P0_6USIC0_C1_DX0_P0_6IRQ9UART1_CH0XMC_UART1_CH0P3_3P3_4USIC1_C0_DX0_P3_4IRQ13UART1_CH1XMC_UART1_CH1P2_12P2_13USIC1_C1_DX0_P2_13IRQ14注意UART1_CH0的RX引脚是P3_4部分XMC14系列封装上这个引脚可能被其他外设占用。如果板子上的LED正好也在P3_4接收就会异常先查原理图再改代码。IRQ号也不是任意分配的XMC1400中断控制器里USIC0_CH0、USIC0_CH1、USIC1_CH0、USIC1_CH1分别占用不同IRQn错用IRQ会导致数据进错回调函数这类问题很难通过软排错发现。2.3 波特率误差计算BRG分频不是拍脑袋填XMC的UART波特率由外设时钟和USIC的BRG共同决定例程默认115200只是典型值。实践中经常有人把STM32的波特率配置方式直接搬过来但XMC库函数会根据传入的baudrate自动计算BRG分频系数这就依赖外设时钟频率。如果SYSCLK配置成32MHz而某个通道的输入时钟来自另一条分频链实际波特率就会偏离预期。初始化后可以把计算值读回来验证uint32_t actual_baud XMC_UART_CH_GetBaudrate(UART0_CH0); printf(calculated baudrate: %lu\n, (unsigned long)actual_baud);读回来的值要和终端串口工具设置一致。偏差超过正负2%时接收端会出现偶发丢字节或乱码尤其四路并行时某一路的时钟树配置错误会影响整条链路判断。我的经验是先把外设时钟树定死再统一设置四路波特率不要依赖DAVE默认初始化和手动配置之间的差值。如果四路波特率不同BRG分频参数各自独立互不影响。3. 初始化四路UART并接入Shell从XMC_UART_CH_Init到ring_buffer3.1 串口初始化配置结构体参数逐个对齐main.c的初始化流程大致是GPIO复用、USIC通道初始化、设置输入节点、配置NVIC、使能接收。这里给出一段在XMC1400上可用的模板void uart0_ch0_init(void) { XMC_UART_CH_CONFIG_t uart_config { .baudrate 115200, .data_bits 8, .stop_bits 1, .parity_mode XMC_USIC_CH_PARITY_MODE_NONE, .oversampling 16 }; XMC_UART_CH_Init(UART0_CH0, uart_config); XMC_UART_CH_SetInputSource(UART0_CH0, (XMC_USIC_CH_INPUT_t)UART0_CH0_RX_INPUT, XMC_USIC_CH_INPUT_DX0); XMC_UART_CH_SetStartFrameTransmitMode(UART0_CH0, XMC_USIC_CH_START_FRAME_MODE_NONE); XMC_UART_CH_EnableEvent(UART0_CH0, XMC_UART_CH_EVENT_RECEIVE_STANDARD); NVIC_SetPriority(UART0_CH0_RX_IRQN, 3); NVIC_EnableIRQ(UART0_CH0_RX_IRQN); XMC_UART_CH_Start(UART0_CH0); }XMC_UART_CH_Init会一次性配置帧格式和波特率oversampling16意味着每个数据位采样16次抗干扰能力强但最高波特率受外设时钟限制追求高速时可以把采样次数降到3或4但要保证信号质量。SetInputSource把RX输入绑定到DX0节点这里漏掉会导致中断永不触发。EnableEvent开启标准接收事件即每收到一个字节触发一次中断。NVIC优先级数值越小优先级越高四路并行时业务量大的那一路可以设为0或1调试Shell那路设低一些。写四路初始化时不要复制四份代码更推荐封装成数组驱动把通道句柄、引脚宏、IRQ号作为参数传入。这样新增第五路只需增加一组宏定义和数组条目。3.2 GPIO复用一个容易被看漏的细节XMC_GPIO_SetMode(UART0_CH0_TX_PIN, (XMC_GPIO_MODE_t)((int32_t)XMC_GPIO_MODE_OUTPUT_PUSH_PULL | (int32_t)P2_1_AF_U0C0_DOUT0)); XMC_GPIO_SetMode(UART0_CH0_RX_PIN, XMC_GPIO_MODE_INPUT_TRISTATE);TX引脚需要同时设置推挽输出和复用功能选择两种属性通过按位或组合。如果只设置XMC_GPIO_MODE_OUTPUT_PUSH_PULL而不加AF编号引脚仍然作为普通GPIO工作串口发不出数据。RX引脚用输入三态并确认外部电路有上拉。对于3.3V外设可以直接对接如果外设是1.8V逻辑必须在TX和RX之间增加UART电平转换电路否则长期运行可能损坏引脚。例程默认全用3.3V TTL电平这是绝大多数开发板的USB转串口芯片已经处理好的状态。3.3 环形缓冲区无锁实现的边界要清楚ring_buffer.h/c是例程的核心公共部分。四路串口并行时每路一个独立环形缓冲区后级轮询读取。核心实现如下typedef struct { volatile uint8_t buffer[256]; volatile uint16_t head; volatile uint16_t tail; } ring_buffer_t; bool ring_buffer_write(ring_buffer_t *rb, uint8_t byte) { uint16_t next (rb-head 1) % sizeof(rb-buffer); if (next rb-tail) return false; // 缓冲区满 rb-buffer[rb-head] byte; rb-head next; return true; } bool ring_buffer_read(ring_buffer_t *rb, uint8_t *byte) { if (rb-head rb-tail) return false; // 缓冲区空 *byte rb-buffer[rb-tail]; rb-tail (rb-tail 1) % sizeof(rb-buffer); return true; }写操作只改head读操作只改tail单生产者单消费者场景下不需要关中断也没有加锁开销。缓冲区满时write返回false例程的处理是直接丢弃新数据。实际产品里可以在满时设置溢出计数由主循环统计溢出次数判断是否突发数据超过设计阈值。256字节对115200波特率意味着约22毫秒缓冲能力如果外设一次突发达几十KB就需要调大缓冲区或交给DMA处理。3.4 中断服务函数动作越短越稳void UART0_CH0_IRQHandler(void) { if (XMC_UART_CH_GetStatusFlag(UART0_CH0) XMC_UART_CH_STATUS_FLAG_RECEIVE_STANDARD) { uint8_t data (uint8_t)XMC_UART_CH_GetReceivedData(UART0_CH0); ring_buffer_write(g_uart0_ch0_rb, data); XMC_UART_CH_ClearStatusFlag(UART0_CH0, XMC_UART_CH_STATUS_FLAG_RECEIVE_STANDARD); } }先读状态寄存器确认事件类型再读数据寄存器最后清标志顺序不能颠倒。如果先把标志清掉再读数据读的过程中新字节到达硬件状态位再次置起可能导致中断标志被误清除而丢字节。这里没有开FIFO所以每收一个字节就中断一次服务函数越短四路并行时不互相阻塞的概率越高。3.5 Shell与retarget_io串口输出重定向的取舍例程里shell.c实现一个小型命令解释器retarget_io.c负责把printf重定向到指定串口。常见做法是让UART0_CH0作为唯一调试口其余三路保留给业务数据。调试口的printf输出可以用宏集中控制#define DBG_PORT UART0_CH0 #define DBG_PRINT(fmt, ...) printf(fmt, ##__VA_ARGS__)所有调试信息都走这一路业务数据不进printf避免在主循环被日志抢占。主循环读取四个环形缓冲区并喂给shell解析器while (1) { uint8_t c; if (ring_buffer_read(g_uart0_ch0_rb, c)) shell_handle_char(g_shell, c); if (ring_buffer_read(g_uart0_ch1_rb, c)) shell_handle_char(g_shell, c); if (ring_buffer_read(g_uart1_ch0_rb, c)) shell_handle_char(g_shell, c); if (ring_buffer_read(g_uart1_ch1_rb, c)) shell_handle_char(g_shell, c); }这个循环没有阻塞点四路数据按照到达顺序进入同一个Shell解析器。shell_handle_char内部维护状态机遇到回车符才执行命令粘包问题基本可控。但要注意如果某路串口持续输入二进制数据且恰好包含0x0D会被误判成命令回车。生产环境中业务串口不要接Shell只在调试口上启用命令解释其他三路用独立帧解析函数处理。4. 中断优先级、DMA与丢字节排错四路并行通信真正要处理的坑4.1 四路中断互相阻塞时先看服务函数执行时间四路UART同时收数据时如果UART0_CH0的中断正在执行此时UART0_CH1也有数据到达NVIC会挂起新中断。如果UART0_CH0服务函数几微秒内退出UART0_CH1的数据还在硬件接收缓冲中不丢。但如果在中断里调用printf或做协议解析执行时间拉到毫秒级其他三路持续接收时必定溢出。四路并行通信中断设计铁律是服务函数只做数据搬运解析全部放到主循环。如果通道自带FIFO可以进一步降低中断频率XMC_USIC_CH_FIFO_EnableEvent(UART1_CH0, XMC_USIC_CH_FIFO_EVENT_RECEIVE_STANDARD); XMC_USIC_CH_FIFO_SetReceiveTriggerLimit(UART1_CH0, 4);把FIFO触发阈值设为4表示收满4个字节才产生一次中断中断次数降为原来的四分之一。代价是实时性从单字节粒度变成4字节粒度对常见帧协议没问题。若外设帧长度不固定需要配合超时判断例如每收到一字节后启动定时器超过3个字符时间认为一帧结束。这种方式比每字节中断高效但占用一个定时器资源。4.2 收到FF或00乱码时的排查顺序最典型的故障是串口助手收到一堆0xFF刚上电时尤其明显。0xFF表示RX线一直处于高电平说明数据可能根本没到达USIC。排查顺序遵循从外到内先量RX引脚电平变化再查GPIO模式然后查SetInputSource的DX编号再查中断是否使能最后看终端工具是否误开了RTS/CTS流控。对应到代码就是检查RX引脚有没有被错误地配置成输出或者错误地加上拉。周边环境噪声大时把RX引脚模式改为XMC_GPIO_MODE_INPUT_PULL_UP从内部加弱上拉而不是完全依赖外部电阻。另一个隐藏点是用USB转串口模块时如果TX和RX交叉接反也会看到0xFF这属于硬件问题排查时先把TX和RX对调试一下。4.3 JLinkLog与EventRecorder不占串口的日志通道工程里的JLinkLog.txt是J-Link调试器输出记录说明开发时已经用调试器抓过日志。多路串口并行时不建议占用某一路UART打印调试信息因为那会改变并行通信的时序。EventRecorder配合J-Link RTT通过调试接口读写数据不影响UART收发。在主循环里可以周期输出四路接收计数SEGGER_RTT_printf(0, ch0:%lu ch1:%lu ch2:%lu ch3:%lu\r\n, (unsigned long)ch0_cnt, (unsigned long)ch1_cnt, (unsigned long)ch2_cnt, (unsigned long)ch3_cnt);每秒打印一次观察四路计数是否都随时间增长。某一路计数不变说明该路中断没触发计数增长但数据错问题大概率在波特率或帧格式计数增长快且溢出标志频繁置位说明消费速度跟不上需要优化主循环或开启FIFO。这种调试手段不需要逻辑分析仪现场定位效率很高。4.4 DMA、FIFO和中断三种方式的边界对于典型的三路9600bps加一路115200bps中断加环形缓冲区完全够用CPU负载很低。如果多路都在921600bps甚至更高每个字节产生一次中断的开销会线性增长。此时DMA的优势才显现出来数据从USIC接收寄存器直接搬到内存DMA控制器每传输一批完成后只触发一次中断。XMC的DMA配置比中断复杂需要用XMC_DMA_CH_CreateDescriptor描述数据源、目的地址和传输长度通常四路各绑定一个DMA通道由硬件自动搬移。例程没有展开这部分属于从基本例程升级到高吞吐应用的必经之路。低于1Mbps用中断高于1Mbps或需要批量收发时再上DMA这个分界线基本能应对大多数项目。5. 让四路串口的Shell命令互相隔离给每个UART通道绑定独立输出上下文5.1 为什么需要命令路由如果四路串口都接在同一个shell上业务串口可能输入二进制帧里面偶尔包含0x0D会被shell误判为命令回车。更合理的做法是每个串口拥有独立shell上下文输入输出各自接管。调试串口执行配置命令业务串口只做帧解析。例程里的shell.h就是为此设计的上下文结构体。5.2 实现方式定义四路的上下文数组并为每路注册不同的输出函数typedef struct { XMC_UART_CH_t *port; ring_buffer_t *rxbuf; shell_t shell; } uart_shell_ctx_t; static uart_shell_ctx_t g_ctx[4]; static void shell_output_0(char ch) { XMC_UART_CH_Transmit(UART0_CH0, (uint8_t)ch); } static void shell_output_1(char ch) { XMC_UART_CH_Transmit(UART0_CH1, (uint8_t)ch); }初始化时把g_ctx[0].port指向UART0_CH0g_ctx[1].port指向UART0_CH1每个ctx的shell.output指向各自的shell_output函数。主循环仍按四路轮询但调用对应的shell_handle_char。这样命令从哪个口进结果就从哪个口出四路之间不会串话。这个方式适合产品联调阶段UART0_CH0作为控制口UART0_CH1作为数据口UART1_CH0和UART1_CH1分别接两个传感器每路都有独立调试接口。5.3 验证四路并行效果的最小压测准备四根USB转TTL线接到四路UART上主机端用Python脚本开四个线程同时向四路发送数据检验每路能否独立回包import serial, threading ports [/dev/ttyUSB0, /dev/ttyUSB1, /dev/ttyUSB2, /dev/ttyUSB3] def send_on(port): with serial.Serial(port, 115200, timeout1) as ser: ser.write(bping\r) reply ser.read(32) print(port, reply) threads [threading.Thread(targetsend_on, args(p,)) for p in ports] for t in threads: t.start() for t in threads: t.join()运行时四个线程同时发送同时等待回复。如果每路都能收到对应回包说明并行收发链路正常。想要更严格把四路波特率分别设为9600、115200、38400、57600重复发送并观察是否有误码或漏包。遇到问题回到第四章的排查顺序重点检查各路的IRQn和波特率。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →