TMS320F28335双CAN总线通信实战:从初始化到数据收发的完整指南

TMS320F28335双CAN总线通信实战:从初始化到数据收发的完整指南 1. 项目概述基于DSP 28335的双CAN总线通信实战在汽车电子、工业控制这些对实时性和可靠性要求极高的领域控制器局域网CAN总线几乎是嵌入式工程师的必修课。最近我在一个分布式电机控制项目中需要用到德州仪器TI的TMS320F28335这颗经典的浮点DSP同时驱动其内部集成的两个增强型CANeCAN模块实现板间高速、可靠的数据交换。网上关于28335 CAN的代码片段不少但往往只给个初始化函数参数怎么算、邮箱怎么配、中断怎么处理、实际通信流程如何这些关键细节要么语焉不详要么藏着掖着。今天我就把自己从零搭建、调试通过的双CAN通信项目代码和盘托出并附上每一步背后的设计逻辑、参数计算方法和踩过的坑。这份代码可以直接移植到你的28335项目里用于实现点对点或简单的网络通信。无论你是刚接触DSP的CAN还是想深入了解eCAN模块的配置细节相信这篇近万字的实战总结都能给你带来实实在在的帮助。2. 核心硬件与模块架构解析在深入代码之前我们必须先搞清楚TMS320F28335这颗芯片的CAN家底以及我们项目要达成的目标。盲目地复制粘贴寄存器配置是嵌入式开发的大忌理解硬件是写出稳健代码的第一步。2.1 TMS320F28335的eCAN模块特性F28335内部集成了两个完全独立的eCAN模块命名为eCAN-A和eCAN-B。这里的“e”代表“enhanced”意味着它完全兼容CAN 2.0B标准同时增加了一些增强特性。每个模块都拥有32个邮箱Mailbox这是一个非常充裕的资源。每个邮箱都可以独立配置为发送或接收并且可以配置为使用11位标准标识符Standard ID或29位扩展标识符Extended ID。对于我们的双CAN通信测试物理上需要将eCAN-A和eCAN-B的TX、RX引脚通过一个CAN收发器如SN65HVD230连接到同一个CAN总线上。这里有个关键点虽然两个模块在芯片内部是独立的但它们共享同一个系统时钟并且可以配置成相同的波特率因此可以挂在同一总线上进行自发自收测试这是验证驱动代码是否正确的最直接方法。在实际多节点应用中它们当然也可以连接到不同的物理总线上实现网关或桥接功能。2.2 项目目标与通信设计本次实战的目标很明确让DSP的eCAN-A和eCAN-B模块在同一个CAN网络上实现双向数据收发。我们设计一个简单的通信场景邮箱配置为每个CAN模块配置两个邮箱。邮箱1配置为发送邮箱使用标准ID 0x1C7十进制455。邮箱0配置为接收邮箱监听相同的ID 0x1C7。这样每个模块既能发送数据给自己环回测试也能接收到另一个模块发送的数据。数据内容发送固定的测试数据例如0x2555, 0x2AAA等并通过串口打印出接收到的数据内容便于观察。工作模式采用正常模式Normal Mode而非自测试模式Self-Test Mode。自测试模式内部环回不经过总线适合快速验证驱动但无法测试物理层收发器和总线冲突。我们直接上正常模式更贴近真实应用。这个设计虽然简单但涵盖了CAN通信最核心的环节初始化、邮箱配置、数据发送与接收。搞通了它更复杂的多邮箱、中断接收、远程帧处理都是在此基础上进行扩展。3. 关键参数计算与初始化流程详解初始化函数init_eCAN_A/B是CAN通信的基石。它完成了从引脚复用、模块使能到波特率设置的所有准备工作。我们逐段分析并重点攻克最让人头疼的波特率参数计算。3.1 引脚复用与IO配置任何外设使用前首先要搞定引脚。28335的GPIO功能强大但配置也稍显繁琐。EALLOW; GpioCtrlRegs.GPAPUD.bit.GPIO18 0; // Enable pull-up for GPIO18 (CANRXA) GpioCtrlRegs.GPAMUX2.bit.GPIO18 3; // Configure GPIO18 for CANRXA operation GpioCtrlRegs.GPAPUD.bit.GPIO19 0; // Enable pull-up for GPIO19 (CANTXA) GpioCtrlRegs.GPAMUX2.bit.GPIO19 3; // Configure GPIO19 for CANTXA operation EDIS;EALLOW/EDIS这是28335特有的保护机制。对某些关键寄存器如GPIO配置、PLL控制进行写操作前必须执行EALLOW指令“打开保护锁”操作完成后用EDIS“锁上”。忘记它们会导致配置不生效。GPAPUD上拉/下拉使能寄存器。设为0表示使能内部上拉电阻。对于CAN这类开放式总线使能上拉是个好习惯可以帮助总线在空闲时稳定在高电平隐性电平。GPAMUX2GPIO A组功能复用选择寄存器2。GPIO18和GPIO19的默认功能是普通IO我们需要将其复用为CAN外设功能。查数据手册可知bit.GPIO18 3和bit.GPIO19 3分别将其配置为CANRXA和CANTXA。注意不同封装的28335CAN引脚可能不同。务必根据你的具体芯片型号和封装查阅对应的数据手册《TMS320F28335 Data Manual》中的“Pin Multiplexing”章节确认正确的GPIO编号和复用值。盲目照抄引脚代码是硬件调试中最常见的“坑”。3.2 模块全局控制与配置使能接下来是配置CAN模块本身的工作模式。/* Disable all Mailboxes */ ECanaRegs.CANME.all 0; EALLOW; ECanaRegs.CANMIM.all 0xFFFFFFFF; // 禁用所有邮箱中断掩码先全部屏蔽 ECanaShadow.CANMC.all ECanaRegs.CANMC.all; ECanaShadow.CANMC.bit.CCR 1; // 设置配置改变请求位 ECanaRegs.CANMC.all ECanaShadow.CANMC.all; EDIS; do { ECanaShadow.CANES.all ECanaRegs.CANES.all; } while (ECanaShadow.CANES.bit.CCE ! 1 ); // 等待配置使能位被置位禁用所有邮箱CANME 0在配置邮箱如设置ID、方向前必须将其禁用这是硬性规定。进入配置模式CAN模块的波特率等核心参数只能在“配置模式”下修改。通过将CANMC寄存器的CCR位设为1来请求进入该模式。等待CCE位置位模块不会立刻进入配置模式需要轮询CANES寄存器的CCE位直到它变为1才表明模块已准备好接受配置。这里必须使用循环等待否则后续的波特率设置可能失败。3.3 波特率参数计算BRP, TSEG1, TSEG2, SJW这是CAN初始化的核心也是难点。代码中调用init_eCAN_A(10, 6, 3, 1, 2, 1);这些参数如何得来CAN总线波特率由以下公式决定波特率 SYSCLKOUT / (BRP * (TSEG1 TSEG2 1))其中SYSCLKOUT是输入到CAN模块的系统时钟。在28335中默认是CPU时钟150MHz经过低速外设预定标器LOSPCP后的时钟。通常我们设置LOSPCP使CAN时钟为SYSCLKOUT/4。假设CPU主频为150MHz则CAN模块时钟 150MHz / 4 37.5MHz。这是计算的前提你必须根据你的系统时钟配置来确认这个值。BRP (Bit Rate Prescaler)波特率预分频器取值范围1-1024。代码中传入10实际写入寄存器的是BRPREG bitRatePrescaler-1 9。TSEG1 (Time Segment 1)时间段1包含传播时间段Prop_Seg和相位缓冲段1Phase_Seg1。代码中传入6写入TSEG1REG timeSeg1-1 5。TSEG2 (Time Segment 2)相位缓冲段2Phase_Seg2。代码中传入3写入TSEG2REG timeSeg2-1 2。一个位时间Bit Time由同步段Sync_Seg固定1个时间份额、TSEG1和TSEG2组成即1 (TSEG11) (TSEG21) 1 6 3 10个时间份额Time Quanta, Tq。代入计算时间份额周期Tq BRP / CAN_CLK 10 / 37.5MHz ≈ 266.7ns位时间Tbit Tq * 10 2.667us波特率 1 / Tbit ≈ 375,000 bps (即375kbps)所以这段初始化代码将CAN总线波特率配置为375kbps这是工业CAN和汽车CAN中非常常用的一个速率。参数选择经验TSEG1 和 TSEG2的比例会影响采样点的位置。通常采样点应位于位时间的50%-80%之间。TSEG1 : TSEG2的比例为 6:3采样点位于(1TSEG1) / (1TSEG1TSEG2) 7/10 70%这是一个比较理想的位置。SJW (Synchronization Jump Width)同步跳转宽度用于在节点间时钟偏差时进行微调通常设置为1-4。代码中设置为2是TSEG2值为3的2/3符合规范建议SJW ≤ min(Phase_Seg1, Phase_Seg2)。SAM (Sampling)采样次数。1表示仅采样一次0表示采样三次取多数。在噪声较低的环境中采样一次可以提高速度。3.4 退出配置模式与工作模式设置波特率设置好后需要退出配置模式并设置其他工作参数。ECanaShadow.CANMC.all ECanaRegs.CANMC.all; ECanaShadow.CANMC.bit.CCR 0 ; // 清除CCR请求退出配置模式 ECanaRegs.CANMC.all ECanaShadow.CANMC.all; EDIS; // 等待CCE位清零确认已退出配置模式 do { ECanaShadow.CANES.all ECanaRegs.CANES.all; } while (ECanaShadow.CANES.bit.CCE ! 0 ); EALLOW; ECanaShadow.CANMC.all ECanaRegs.CANMC.all; ECanaShadow.CANMC.bit.STM 0; // 正常模式非自测试 ECanaShadow.CANMC.bit.SCB 1; // 选择eCAN模式支持32邮箱29位ID ECanaShadow.CANMC.bit.DBO 1; // 数据字节顺序小端优先低字节在前 ECanaRegs.CANMC.all ECanaShadow.CANMC.all; EDIS;退出配置模式将CCR位清零并等待CCE位跟着清零。必须等待否则模块可能无法正常工作。STM位自测试模式。0为正常模式数据通过TX引脚真正发送到总线上。SCB位eCAN模式选择。必须设为1以使用增强型CAN的32邮箱和所有功能。DBO位数据字节顺序。这是28335 eCAN模块一个非常重要的特性。当DBO1时数据存储在邮箱中的顺序与CAN总线上发送的顺序一致即低字节在低地址。这通常更符合我们的编程习惯。如果DBO0顺序则是相反的处理多字节数据如float, int32时需要特别小心。4. 邮箱配置数据收发的核心枢纽初始化完成后CAN模块还只是一个空壳我们需要为其配置“邮箱”Mailbox来定义数据的收发规则。config_eCAN_A_mbx函数就负责这项工作。4.1 邮箱类型与标识符配置void config_eCAN_A_mbx (uint16_T mbxType, uint16_T mbxNo, uint32_T msgID, uint16_T msgType) { uint32_T maskRx 0x1; uint32_T maskTx; struct ECAN_REGS ECanaShadow; volatile struct MBOX *mbx ECanaMboxes.MBOX0 mbxNo; // 获取指定邮箱的指针 maskRx maskRx mbxNo; // 生成对应邮箱的位掩码 maskTx ~maskRx; mbx-MSGCTRL.bit.RTR 0; // 设置为数据帧非远程请求帧 // 临时禁用所有邮箱安全配置 ECanaShadow.CANME.all ECanaRegs.CANME.all; ECanaRegs.CANME.all 0x00000000; // 配置消息标识符 if (msgType1) mbx-MSGID.all msgID; // 扩展ID使用bits 0:28 else mbx-MSGID.bit.STDMSGID msgID; // 标准ID使用bits 18:28 mbx-MSGID.bit.AME 0; // 禁用接收掩码即精确匹配ID mbx-MSGID.bit.AAM 0; // 非自动应答邮箱针对远程帧 mbx-MSGID.bit.IDE msgType; // 标识符扩展位 // 配置邮箱方向发送或接收 ECanaShadow.CANMD.all ECanaRegs.CANMD.all; if (mbxType0) { // 接收邮箱 ECanaShadow.CANMD.all | maskRx; // CANMD对应位写1表示接收 } else { // 发送邮箱 ECanaShadow.CANMD.all maskTx; // CANMD对应位写0表示发送 } ECanaRegs.CANMD.all ECanaShadow.CANMD.all; // 使能该邮箱 ECanaShadow.CANME.all | maskRx; ECanaRegs.CANME.all ECanaShadow.CANME.all; }关键点解析邮箱指针ECanaMboxes是一个映射到所有32个邮箱的结构体数组。通过ECanaMboxes.MBOX0 mbxNo可以方便地访问任意一个邮箱的寄存器组。安全配置在修改邮箱的MSGID标识符或MSGCTRL控制位之前最佳实践是先通过CANME寄存器禁用该邮箱代码中是禁用了所有邮箱。虽然28335的数据手册没有强制要求但在某些情况下如果邮箱处于使能状态对其标识符的写操作可能被忽略导致配置失败。这是一个重要的防错技巧。标识符配置标准帧 vs 扩展帧由IDE位决定。标准帧使用11位ID存放在MSGID.bit.STDMSGID位18-28扩展帧使用29位ID存放在MSGID.all的低29位。代码中的判断逻辑非常清晰。AME位接收掩码使能。当AME1时该邮箱使用接收掩码寄存器CANGAM/LAM进行过滤可以接收一组ID。我们这里设为0表示只接收与msgID完全匹配的帧。AAM位自动应答。仅在邮箱配置为接收且收到远程帧时若AAM1则自动用本邮箱的数据回复一个数据帧。我们这里简单场景设为0。邮箱方向CANMD寄存器决定邮箱方向。某一位为1表示对应邮箱是接收邮箱为0则是发送邮箱。注意这里与CANME邮箱使能是独立的。4.2 项目中的邮箱配置实例在CAN_NET_initialize函数中我们进行了如下配置config_eCAN_B_mbx (1U, 1, 455, 0); // eCAN-B邮箱1发送标准ID 455 config_eCAN_A_mbx (1U, 1, 455, 0); // eCAN-A邮箱1发送标准ID 455 config_eCAN_A_mbx (0U, 0, 455, 0); // eCAN-A邮箱0接收标准ID 455 config_eCAN_B_mbx (0U, 0, 455, 0); // eCAN-B邮箱0接收标准ID 455这就构建了一个对称的双向通信结构每个CAN模块都有一个发送邮箱1号和一个接收邮箱0号它们都使用相同的ID 455。因此任何一个模块发送的数据两个模块的接收邮箱都能收到包括自己。这非常适合进行回环测试和总线监听。5. 数据收发实战与代码逐行分析配置妥当后就到了最激动人心的环节发送和接收数据。我们提供的CAN_A_TX,CAN_B_TX,CAN_A_RX,CAN_B_RX函数展示了最基础的查询式收发流程。5.1 发送数据流程void CAN_A_TX(void) { { // 1. 填充数据到发送邮箱 ECanaMboxes.MBOX1.MDL.word.LOW_WORD 0x2555; ECanaMboxes.MBOX1.MDL.word.HI_WORD 0x2aaa; ECanaMboxes.MBOX1.MDH.word.LOW_WORD 0x2bbb; ECanaMboxes.MBOX1.MDH.word.HI_WORD 0x2ccc; // 2. 设置数据长度码 (DLC) ECanaMboxes.MBOX1.MSGCTRL.bit.DLC 8; // 8字节数据 // 3. 置位发送请求位启动发送 ECanaRegs.CANTRS.all (((uint32_T) 0x00000001) 1); } }数据填充eCAN的邮箱数据区有8个字节分为MDL低4字节和MDH高4字节。每个部分又分为LOW_WORD和HI_WORD。这里我们填充了4个16位的测试数据。注意数据的存放顺序受前面设置的DBO位影响。我们设置了DBO1小端低字节在前所以0x2555在总线上会先发送0x55再发送0x25。设置DLC数据长度码范围0-8。必须与实际填充的数据字节数一致。设为8表示发送全部8个字节。启动发送CANTRS发送请求设置寄存器的每一位对应一个邮箱。将第1位置1因为邮箱编号是1就向该邮箱发出了发送请求。模块的发送调度器会自动处理发送流程。重要提示CANTRS是“设置”寄存器写1置位。对应的CANTRR是“复位”寄存器。还有一个CANTA发送应答寄存器当某个邮箱成功发送后硬件会自动将CANTA的对应位置1软件需要写1来清除该标志位。在查询式发送中可以通过检查CANTA位来判断发送是否完成。本例中主循环简单延时未做检查在实际高可靠应用中需要添加状态检查或使用中断。5.2 接收数据与标志位处理接收函数CAN_A_RX的逻辑是查询式的典范也揭示了eCAN接收机制的关键。void CAN_A_RX(void) { ... { struct ECAN_REGS ECanaShadow; // 检查邮箱0的接收消息挂起位 (RMP0) if (ECanaRegs.CANRMP.bit.RMP0) { // 1. 清除接收标志位必须操作 EALLOW; ECanaShadow.CANRMP.all 0x0; ECanaShadow.CANRMP.bit.RMP0 1; // 写1清除RMP0位 ECanaRegs.CANRMP.all ECanaShadow.CANRMP.all; EDIS; // 2. 从邮箱数据区读取数据 CAN_NET_B.CAN_A_RX_o2[0] ECanaMboxes.MBOX0.MDL.word.LOW_WORD; CAN_NET_B.CAN_A_RX_o2[1] ECanaMboxes.MDL.word.HI_WORD; ... // 读取其他数据 } // 3. 数据变化检测与输出通过串口 if (CAN_A_TEMP1 ! CAN_NET_B.CAN_A_RX_o2[0]){ string_shift_hex(txString0, CAN_NET_B.CAN_A_RX_o2[0]); scib_xmit((char*)txString0, 31); ... } // 4. 更新临时变量用于下次变化检测 CAN_A_TEMP1 CAN_NET_B.CAN_A_RX_o2[0]; ... } }接收流程详解轮询RMP位CANRMP接收消息挂起寄存器的每一位对应一个邮箱。当该邮箱成功接收到一帧符合ID过滤条件的CAN消息后硬件会自动将对应的RMP位置1。这是驱动知道“有数据来了”的唯一标志。清除RMP标志这是至关重要的一步。读取数据后必须通过向CANRMP的对应位写1来清除该标志。如果不清除该邮箱将无法接收下一帧消息因为硬件认为“上次的数据还没取走”。代码中通过影子寄存器操作先清零再置位是一种安全的写法。读取数据从对应的邮箱数据寄存器MBOXx.MDL/MDH中读取数据。读取操作本身不会清除RMP标志。数据处理本例中将数据存入一个全局结构体数组并与旧值比较仅在数据变化时通过串口打印。这避免了串口被重复数据刷屏是调试时的实用技巧。关于代码中的重复if判断原始代码中在同一个函数里对if (ECanaRegs.CANRMP.bit.RMP0)判断了两次且逻辑完全一样。这可能是笔误或未清理的冗余代码。在实际应用中只需要判断和处理一次即可。第二个完全相同的if块应该删除。6. 主程序逻辑与系统集成理解了各个子函数后我们再看主函数如何将它们串联起来形成一个完整的、可工作的测试程序。void main(void) { volatile boolean_T noErr; init_board(); // 开发板基础初始化时钟、GPIO、串口等 CAN_NET_initialize(1); // 初始化双CAN模块及邮箱配置 while(1) { CAN_A_TX(); // eCAN-A 发送数据 delay(10); // 短暂延时避免总线持续拥堵 CAN_B_TX(); // eCAN-B 发送数据 delay(10); CAN_A_RX(); // 查询并处理eCAN-A接收到的数据 CAN_B_RX(); // 查询并处理eCAN-B接收到的数据 delay(1000); // 主循环延时1秒 } }主循环设计解析顺序发送先让CAN-A发送再让CAN-B发送。中间插入10ms的短延时。这个延时不是必须的但可以避免两个发送请求在时间上完全重叠有助于在逻辑分析仪或示波器上更清晰地观察两帧数据。查询式接收发送完成后立即调用两个接收函数。由于CAN总线速度很快375kbps一帧数据不到1ms在发送完成后立即查询肯定能收到对方以及自己刚发出的数据。1秒循环主循环延时1秒意味着这个测试程序每秒会发送两帧数据A和B各一帧并打印出接收到的数据。这个速率对于观察和调试来说非常合适。init_board()函数的重要性这是一个假设存在的函数它必须完成28335上电后最关键的系统初始化通常包括初始化系统时钟PLL将内核时钟提高到150MHz。配置低速外设时钟预定标器LOSPCP确保CAN模块得到正确的37.5MHz输入时钟。初始化串口SCI用于打印调试信息。可能还包括看门狗禁用、外设时钟使能等。如果这些基础初始化没做CAN模块根本无法工作。很多初学者调不通CAN问题往往出在这个最开始的系统初始化上。7. 进阶话题与避坑指南基于这个基础框架在实际项目开发中你肯定会遇到更多需求和挑战。这里分享几个关键的进阶配置和常见“坑点”。7.1 中断驱动接收查询式接收Polling会占用CPU时间在复杂系统中不可取。中断方式才是王道。配置中断主要涉及以下几个寄存器邮箱中断掩码CANMIM决定哪个邮箱在成功发送或接收后能产生中断。例如ECanaRegs.CANMIM.bit.MIM0 1;使能邮箱0的中断。邮箱中断级别CANMIL决定邮箱中断映射到CPU的哪个中断线ECAN0INT或ECAN1INT。通常所有邮箱中断可以映射到同一根线。全局中断屏蔽CANGIM这是总开关。需要使能中断线I0EN或I1EN并可能配置错误中断等。PIE向量表配置在DSP的PIE控制器中为ECAN0INT/ECAN1INT分配中断服务函数ISR。中断服务函数ISR在ISR中需要读取CANRMP或CANTA寄存器确定是哪个邮箱触发的中断接收完成还是发送完成。执行数据读取或发送完成处理。清除中断标志对于接收写1清除CANRMP对应位对于发送写1清除CANTA对应位。清除PIE和CPU的中断应答位。避坑提示中断里清除标志位的操作和查询式一样必须使用“写1清除”的方式。忘记清除中断标志会导致中断持续触发程序卡死在ISR中。7.2 接收过滤与掩码配置我们的例子中使用了精确ID匹配AME0。在实际网络中一个节点可能需要接收一组ID的消息。这时就需要使用接收掩码。全局接收掩码CANGAM适用于标准帧为所有邮箱提供一个全局的过滤掩码。局部接收掩码LAM每个邮箱对于标准帧或每对邮箱对于扩展帧可以有自己的局部接收掩码寄存器LAM0-LAM3。工作原理当AME1时邮箱使用掩码进行过滤。掩码寄存器中的某一位为1表示接收到的消息ID中对应的位需要与邮箱MSGID中对应的位进行比较为0则表示“不关心”dont care。例如设置MSGID0x180LAM0x7F0那么可以接收ID从0x180到0x18F的所有消息低4位不关心。7.3 常见问题排查清单当你按照代码搭建环境却发现收不到数据时可以按照以下清单逐项排查硬件连接CANH和CANL是否接反是否接了120欧姆的终端电阻总线两端各一个CAN收发器如SN65HVD230的电源和地是否稳定VCC通常是3.3V。用示波器测量CANH和CANL之间的差分信号。空闲时应有约2.5V的差分电压。发送时应有明显的位电平变化。软件配置系统时钟和CAN模块时钟确认了吗这是波特率计算错误的根源。用示波器测量一个GPIO翻转的周期来反推系统时钟频率。波特率参数算对了吗使用CAN总线分析仪如PCAN-USB, ZLG USBCAN监听看是否能检测到总线波特率和你配置的一致。邮箱使能了吗检查CANME寄存器对应位是否为1。邮箱方向设对了吗检查CANMD寄存器发送邮箱对应位应为0接收邮箱为1。发送请求发出了吗检查CANTRS寄存器对应位是否被置1。发送完成后该位会被硬件清零同时CANTA对应位置1。接收标志位清除了吗如果CANRMP位一直是1则无法接收新数据。确保在读取数据后写1清除它。标识符匹配吗确保发送和接收邮箱配置的ID标准/扩展和数值完全一致。使用分析仪查看总线上实际帧的ID。调试技巧启用自测试模式STM1在初始化时将CANMC.bit.STM设为1。在此模式下TX引脚内部环回到RX引脚不经过外部收发器。如果自测试能成功收发说明软件配置和CPU内部CAN模块是好的问题出在外部硬件收发器、总线。使用逻辑分析仪抓取TX、RX引脚MCU侧的波形看MCU是否确实发出了数据。这能区分是软件没发还是发了但硬件没传出去。简化测试先只用一个CAN模块配置一个发送邮箱和一个接收邮箱在自测试模式下自发自收。成功了再切换到正常模式接上收发器。最后再测试双模块通信。