简介:面向DSP28335开发者的一套CAN通信工程程序包,基于TI官方库实现增强型CAN(eCAN)模块的完整收发流程,涵盖波特率配置、滤波器设置、中断服务程序以及错误处理机制,适合正在学习28335外设编程或需要快速搭建CAN通信功能模块的嵌入式工程师。资源以rar压缩包发布,共74个文件、480KB,包括C源码、头文件、汇编启动文件、TI工程链接与配置文件,以及编译生成的.out、.map等调试文件,目录结构一目了然,便于直接对照参考。目前已有4025人学习下载,说明该工程具备较高的实践参考价值。通过阅读代码可以掌握eCAN初始化流程、消息邮箱与缓冲区操作、发送接收中断的触发逻辑,以及基于查询方式的备用处理方法;同时工程内置DSP2833x标准外设库,可扩展用于其他外设协同开发。
1. DSP28335 CAN 通信的难点不在协议,而在邮箱机制
在 DSP28335 上做 CAN 通信,最容易把人绕晕的不是 CAN 2.0B 的帧格式,而是它那套“先注册邮箱再收发”的外设模型。DSP28335 的 CAN 模块叫 eCAN,一共有 32 个邮箱,每个邮箱用之前都必须声明方向、写 ID、配屏蔽,这颗芯片不像串口那样打开就能直接收发。如果你带着其他单片机的 CAN 使用习惯直接改,第一版代码大概率会卡在三个问题上:发不出去、收不到、标志位清不掉。这篇笔记按一条可落地的路径,把 28335 的 eCAN 从初始化、邮箱配置、查询/中断收发到总线验证依次拆开讲,适合刚拿到 28335 控制板、正在把 CAN 通讯加进自己项目里的工程师,也适合被例程里一堆 Shadow 寄存器和标志位绕晕的人。读完你应该能独立完成两个 28335 节点之间的 CAN 互通,并且知道现象不对时去哪里查。
2. eCAN 初始化:先从时钟、位定时和引脚复用这三件套下手
2.1 先看清楚 CAN 模块从哪儿拿时钟:别一上来就配波特率
28335 的 eCAN 模块没有独立的 PLL 分频链,它直接挂在系统时钟 SYSCLKOUT 上。也就是说,你烧进去的程序如果跑的 SYSCLKOUT 不是 150MHz,那后面所有波特率计算都得跟着变。很多人第一步就翻车,是因为只改了 CAN 寄存器,没确认 PLL 有没有被初始化到目标主频。
常见的工程入口是 main 函数里先调用系统初始化,把 PLL 锁定到 150MHz,之后 CAN 的波特率计算才以这个频率为基准。如果你在开发环境的寄存器窗口里看到 PCLKCR0 的 ECANAENCLK 位还是 0,那 eCAN 模块相当于没上电,所有寄存器访问都可能读到无效值或写不进去。这一位就是 CAN 外设的时钟门控,在配置任何 CAN 寄存器之前必须先把它置 1。
EALLOW; SysCtrlRegs.PCLKCR0.bit.ECANAENCLK = 1; // 打开 eCAN-A 模块时钟 EDIS;这里 EALLOW/EDIS 是 28335 上写系统控制寄存器必须的保护开关,没有它,寄存器写操作会被 CPU 拦截。实际项目里我一般会把这段放在最前面,紧接着读一遍 PCLKCR0 确认写入成功。如果读回来还是 0,往往是 EALLOW 没配对,或者代码跑在非 RAM 的受保护区域,这个要排查启动文件。
2.2 CANBTC 位定时:把波特率和采样点一次说清
28335 的波特率配置不在“波特率寄存器”里,而在 CANBTC 这个位定时寄存器里。它由四部分组成:分频系数 BRPREG、同步跳转宽度 SJWREG、时间段 1 的 TSEG1REG、时间段 2 的 TSEG2REG。工程上最常用的推导办法是先定采样点,再反推分频。
CAN 的一个位时间由三块组成:同步段固定占 1 个时间量子,TSEG1 表示传播段加相位缓冲段 1,TSEG2 表示相位缓冲段 2。时间量子 tq 等于(BRPREG + 1) / SYSCLKOUT,整个位时间就是(1 + TSEG1REG + TSEG2REG)个 tq。
以 150MHz 主频跑 500kbps 为例,位周期是 2us。我一般会把一个位拆成 20 个时间量子,这样采样点分辨率高,抗干扰也好。每个 tq 就是 100ns,所以 BRPREG + 1 = 15,BRPREG 取 14。采样点放在 80%,也就是同步段加 TSEG1 占 16 个 tq,于是 TSEG1REG 取 15,TSEG2REG 取 4。SJWREG 取 1,兼顾时钟偏差。
| 参数 | 寄存器字段 | 示例取值 |
|---|---|---|
| 位速率 | — | 500 kbps |
| 分频系数 | BRPREG | 14 |
| 时间段1 | TSEG1REG | 15 |
| 时间段2 | TSEG2REG | 4 |
| 同步跳转宽度 | SJWREG | 1 |
| 采样点 | — | 80% |
如果总线很长或者节点数多,采样点可以往 75% 方向调,也就是把 TSEG1 减小、TSEG2 增大。如果板上晶振偏差大,SJW 尽量不小于 2 个 tq,否则节点之间时钟漂移超过 SJW 时会同步失败,总线会隔一会儿就冒一个错误帧。
2.3 引脚复用与板级接线:GPIO30/32 不是拿来直接跑信号的
28335 的 eCAN-A 发送和接收引脚分别复用 GPIO30 和 GPIO32,默认状态是 GPIO 功能,必须切换到 CAN 外设功能。这个漏掉,后面配置再对也不会有波形。切换方式是把对应复用控制寄存器里的两位改成外部功能选项。
EALLOW; GpioCtrlRegs.GPAMUX2.bit.GPIO30 = 1; // GPIO30 复用为 CANRXA GpioCtrlRegs.GPBMUX1.bit.GPIO32 = 1; // GPIO32 复用为 CANTXA EDIS;注意 GPIO30 属于 A 组复用寄存器,GPIO32 属于 B 组,别写到一个寄存器里。换芯片型号时这个表完全不同,建议查对应数据手册的 Pin Mux 表再改。
板级接线上,CANTXA 和 CANRXA 是 3.3V 电平的控制器侧信号,不能直接怼到总线上,中间必须有 CAN 收发器。收发器选 3.3V 供电的常见型号即可,例如 SN65HVD230 这类,RXD 接 CANRXA,TXD 接 CANTXA。CANH 和 CANL 之间要放 120 欧终端电阻,位置放总线两端。还有一个高频坑:两个节点之间除了 CANH/CANL,必须共地。差分信号虽然理论上不依赖地,但收发器共模范围有限,地电位差大了总线会表现为时通时不通,这是现场调试最容易忽略的一条。
2.4 一个能用的 eCAN 初始化函数
把前三节合起来,就是一份最小可用的 eCAN-A 初始化代码。这里只做模块级配置,不涉及具体邮箱,邮箱方向在收发章节单独讲。
void EcanInit(void) { EALLOW; // 1. eCAN-A 时钟门控 SysCtrlRegs.PCLKCR0.bit.ECANAENCLK = 1; // 2. 引脚复用 GpioCtrlRegs.GPAMUX2.bit.GPIO30 = 1; // CANRXA GpioCtrlRegs.GPBMUX1.bit.GPIO32 = 1; // CANTXA EDIS; // 3. 位定时,500kbps @ 150MHz,采样点 80% ECanaRegs.CANBTC.bit.BRPREG = 14; ECanaRegs.CANBTC.bit.TSEG1REG = 15; ECanaRegs.CANBTC.bit.TSEG2REG = 4; ECanaRegs.CANBTC.bit.SJWREG = 1; // 4. 默认禁止所有邮箱,后续按需使能 ECanaRegs.CANME.all = 0; // 5. 清一遍事件标志,写1清除 ECanaRegs.CANTA.all = 0xFFFFFFFF; ECanaRegs.CANRMP.all = 0xFFFFFFFF; }这段代码里第 4 步很重要:28335 的 32 个邮箱上电后默认状态不一定干净,先全部禁掉,再在收发初始化里逐个打开,可以避免某些邮箱处于半配置状态导致总线错误。第 5 步清除的 CANTA 和 CANRMP 分别是发送应答标志和接收挂起标志,它们都是写 1 清除的机制,初始化时统一清一遍,防止上电时残留标志被误认为一次收发事件。如果你的主频不是 150MHz,按 2.2 节的公式重新算 BRPREG 和 TSEG1/2REG,不要照抄这组数。
3. 邮箱收发:把 CANTA、CANTRS、CANRMP 这几个标志玩明白
3.1 邮箱方向与 ID 过滤:32 个邮箱想清楚再分配
eCAN 的邮箱设计逻辑和普通 CAN 控制器不太一样:每个邮箱本身没有固定身份,你要先决定它是发送邮箱还是接收邮箱。方向由 CANMD 寄存器控制,对应位写 1 表示该邮箱作为发送方,写 0 表示接收方。这个方向在运行中也可以改,但改之前必须先把对应邮箱的使能位清掉,否则配置写入过程中控制器可能正在使用该邮箱,后果很难查。
另一个重点是接收邮箱的 ID 过滤。28335 的每个接收邮箱都要在 MSGID 里写一个期望收到的 ID,同时置 AME 位允许过滤。配合局部接收屏蔽寄存器 LAM,可以让某个邮箱只收特定 ID,也可以让一批 ID 共用同一个接收邮箱。实际项目里,我习惯把固定周期上报的状态帧分给专用接收邮箱,把命令帧分给另一个接收邮箱,两边互不干扰,协议解析会简单很多。
3.2 查询式发送:请求、应答、清标志三步循环
发送邮箱的完整动作是:先把邮箱配置成发送方向,写入 ID 和数据长度,再往数据域填数据,最后置位发送请求。发送完成后控制器会自动把发送请求位清掉,同时置发送应答位。这套动作在代码里要严格按顺序走,顺序反了会出现数据还是旧的但请求已经置位的问题。
void CanSendStandard(Uint16 mbNo, Uint16 stdId, Uint16 dlc, Uint16 *data) { Uint32 mask = (Uint32)1 << mbNo; Uint16 i; // 等待上一次发送真正结束,避免覆盖还在发送的内容 while (ECanaRegs.CANTRS.all & mask) {} EALLOW; // 修改邮箱配置前先禁止该邮箱 ECanaRegs.CANME.all &= ~mask; ECanaRegs.CANMD.all |= mask; // 方向:发送 ECanaMailbox.MBOX[mbNo].MSGID.bit.STDID = stdId; ECanaMailbox.MBOX[mbNo].MSGID.bit.IDE = 0; // 标准帧 ECanaMailbox.MBOX[mbNo].MSGID.bit.AAM = 0; // 不使能自动应答模式 ECanaMailbox.MBOX[mbNo].MSGCTRL.bit.DLC = dlc; // 数据长度 0-8 // 16位总线宽度下,8字节数据拆成4个16位字写入 ECanaMailbox.MBOX[mbNo].MDL.word[0] = data[0]; ECanaMailbox.MBOX[mbNo].MDL.word[1] = data[1]; ECanaMailbox.MBOX[mbNo].MDH.word[0] = data[2]; ECanaMailbox.MBOX[mbNo].MDH.word[1] = data[3]; ECanaRegs.CANME.all |= mask; // 重新使能邮箱 EDIS; ECanaRegs.CANTRS.all = mask; // 置位发送请求 while (!(ECanaRegs.CANTA.all & mask)) {} // 等待发送应答 EALLOW; ECanaRegs.CANTA.all = mask; // 写1清除应答标志 EDIS; }这段代码有几处细节值得展开。第一,开头的while (CANTRS.all & mask)是防止上次请求还没发完就再次置位,有些工程里省掉这一步,结果发送频率一高就丢帧。第二,配置邮箱时先失能、写完后使能,这是 28335 邮箱操作的标准姿势。第三,CANTA 标志清除必须写 1,写 0 是无效操作,这个机制在 C2000 系列很多外设里都存在。
参数上,mbNo 是邮箱号,0 到 31;stdId 是 11 位标准 ID,传入前先做& 0x7FF截断比较保险;data 数组固定传 4 个 Uint16 元素,即使 DLC 只有 2 字节,也建议把数组补满再调用,避免读到自己不确定的残留值。
3.3 查询式接收与局部接收屏蔽:别让无关帧打断你的节奏
接收邮箱的初始化比发送稍多两步:要配期望 ID,还要设置 LAM 屏蔽字和 AME 使能位。LAM 的每一位对应 ID 的一位,写 0 表示该位必须严格匹配,写 1 表示不关心这一位。如果 LAM 全写 1,等于邮箱不过滤 ID,总线上的帧都会进这个邮箱,用得好是广播接收,用得不好就是中断风暴。
void CanRecvConfig(Uint16 mbNo, Uint16 stdId, Uint32 lam) { Uint32 mask = (Uint32)1 << mbNo; EALLOW; ECanaRegs.CANME.all &= ~mask; // 先失能 ECanaRegs.CANMD.all &= ~mask; // 方向:接收 ECanaMailbox.MBOX[mbNo].MSGID.bit.STDID = stdId; ECanaMailbox.MBOX[mbNo].MSGID.bit.IDE = 0; // 标准帧 ECanaMailbox.MBOX[mbNo].MSGID.bit.AME = 1; // 使能ID过滤 ECanaMailbox.MBOX[mbNo].MSGCTRL.bit.DLC = 8; // 按最大长度准备 ECanaLAMRegs.LAM[mbNo].all = lam; // 0=严格匹配,1=忽略 ECanaRegs.CANME.all |= mask; // 使能邮箱 EDIS; }接收数据的读取函数相对简单:查询 CANRMP 的对应位,置位说明邮箱收到了一帧新数据。读取顺序很关键,先把数据从邮箱搬到自己的缓冲区,再清 CANRMP,否则下一帧同 ID 报文到达时会直接覆盖旧数据。
int CanReceiveStandard(Uint16 mbNo, Uint16 *rxWord) { Uint32 mask = (Uint32)1 << mbNo; if ((ECanaRegs.CANRMP.all & mask) == 0) return 0; // 没有新数据 rxWord[0] = ECanaMailbox.MBOX[mbNo].MDL.word[0]; rxWord[1] = ECanaMailbox.MBOX[mbNo].MDL.word[1]; rxWord[2] = ECanaMailbox.MBOX[mbNo].MDH.word[0]; rxWord[3] = ECanaMailbox.MBOX[mbNo].MDH.word[1]; EALLOW; ECanaRegs.CANRMP.all = mask; // 写1清除挂起位 EDIS; return 1; }这里 rxWord 同样是 4 个 Uint16,如果你的上层协议按字节处理,记得把每个 16 位字拆成高 8 位和低 8 位。28335 的 eCAN 在字节序上有一个 DBO 位会改变数据域字节的存放顺序,所以不同工程里看到的数据高低字节不一样,不要怀疑协议,先确认 DBO 配置一致。这一点在第 5 章还会再展开。
4. 中断式接收:把 PIE 与 eCAN 的中断链打通
4.1 eCAN 中断源和 PIE 映射:邮箱 0 到 15 与 16 到 31 分两路
28335 的 eCAN 中断分两路:邮箱 0 到 15 的事件走一路,邮箱 16 到 31 走另一路。两路各自进入 PIE 的第 9 组。也就是说,你选的邮箱号决定了它进哪个 CPU 中断通道。如果只用一个接收邮箱,建议优先用 0 到 15 里的邮箱,配置最简单,例程也多。
中断使能要同时做三件事:打开对应邮箱的中断屏蔽位、打开 eCAN 模块级的中断放行位、在 PIE 里把对应通道使能。少任何一环,中断都进不来,而且由于没有现象,排查起来特别费时间。
4.2 中断服务程序里标志清理顺序:写 1 清除不是玩笑
接收中断的服务程序有一个强制顺序:读数据、清挂起、清 PIEACK。清挂起要写 1,写 0 的话标志永远不消失,中断会持续触发,看起来就像系统卡死在中断里。PIEACK 的清法是把第 9 组对应的位写 1,这一步如果漏了,这一组后续中断全部进不来。
interrupt void ECAN0INTA_ISR(void) { Uint32 pend = ECanaRegs.CANRMP.all; if (pend & 0x0001) // 邮箱0收到新帧 { appRxBuf[0] = ECanaMailbox.MBOX0.MDL.word[0]; appRxBuf[1] = ECanaMailbox.MBOX0.MDL.word[1]; appRxBuf[2] = ECanaMailbox.MBOX0.MDH.word[0]; appRxBuf[3] = ECanaMailbox.MBOX0.MDH.word[1]; EALLOW; ECanaRegs.CANRMP.all = 0x0001; // 清邮箱0挂起位 EDIS; } PieCtrlRegs.PIEACK.all = 0x0100; // 清PIE第9组应答位 }中断服务程序里我一般只做数据搬移,不做协议解析。解析放在主循环或任务里做,否则中断里多几条乘除法,实时性就说不清了。清 PIEACK 放在函数最后是 28335 的老规矩,先清会导致中断嵌套重入,后清则保证当前帧处理完再开放下一笔。
中断初始化代码如下,放在 EcanInit 之后调用:
EALLOW; PieCtrlRegs.PIEIER9.bit.INTx1 = 1; // 邮箱0-15中断通道使能 ECanaRegs.CANMIM.all |= 0x0001; // 邮箱0允许产生中断 ECanaRegs.CANGIM.bit.I0EN = 1; // 放行中断到PIE EDIS; IER |= M_INT9; // CPU中断第9组使能 EINT; // 开全局中断请注意INTx1对应邮箱 0 到 15 的中断通路,如果接收邮箱选在 16 到 31,这里要改成INTx2,并且 CANGIM 里使能的是另一路放行位。选邮箱之前先定中断通路,比配置到一半再换邮箱省事得多。
4.3 混合方案:发送轮询,接收中断
实际项目里,发送频率通常可控,而且发送失败需要重试逻辑,用查询方式写起来更直接。接收则往往不可控,节点随时可能发命令过来,所以我一般会做成“发送查询、接收中断”的混合方案。
中断方式下,应用层喂数据给 CanSendStandard 发送,不会有冲突。DSP28335 的邮箱机制天然支持这种混合设计,发送邮箱和接收邮箱各自独立,只要初始化时把邮箱号分开,中断接收和查询发送完全互不干扰。这样做还有个好处:主循环只需要在收到新帧标志时去解析缓冲,不需要每次循环都轮询 CANRMP。
5. DSP28335 CAN 通信会踩的 5 个坑:现象、原因、处理
5.1 明明使能了时钟,发送引脚就是没有波形
现象:程序跑起来,TXD 引脚接示波器完全没电平变化,CAN 模块好像不存在一样。检查 PCLKCR0 的 ECANAENCLK 位也确实是 1。
原因:最常见的是 GPIO 复用没配。GPIO30/32 出厂默认是普通 GPIO,不切到 CAN 功能,外设信号根本出不来。另一个原因是收发器供电没给,控制器侧有数据,但收发器不工作。
解决:回头检查 GPAMUX2 的 GPIO30 位和 GPBMUX1 的 GPIO32 位,确认都切到了外部功能。然后用万用表量收发器供电脚和 VCC 之间的电压。这两步能解决掉九成“没有波形”的问题。
5.2 发不出去帧,TA 标志一直不置位
现象:发送请求置位后,CANTRS 一直为 1,CANTA 不置位,程序卡在等待应答的死循环里。示波器看 CANTXA 有波形,但对方节点收不到。
原因:CAN 协议要求总线上至少有一个节点对发送者的帧给出 ACK 应答,一个节点连在总线上自说自话时,发送帧永远不会被应答。常见场景是收发器坏了、对方节点没上电、终端电阻把信号拉没了,或者总线只接了一个节点。
解决:先用另一个已知正常的节点或 CAN 分析仪挂在总线上,确认发送帧有没有出来。如果分析仪能看到帧但 28335 的 TA 不置位,极大可能是收发器 RS 引脚被拉高了,让发送器处于静默状态。用分析仪确认总线活动,比反复查代码快得多。
5.3 想清中断标志,写 0 等于白写
现象:ISR 里把 CANRMP 对应位写 0,退出中断后中断立刻再次进入,程序像卡死在中断里,主循环根本跑不动。
原因:28335 的 eCAN 标志位是写 1 清除机制,写 0 不会改变标志状态。这个机制和大多数单片机的“写 0 清除”习惯相反,几乎每个从其他平台转过来的人都会踩一次。
解决:所有清标志操作统一改成写 1。发送应答 CANTA、接收挂起 CANRMP、还有 PIE 的 PIEACK,都是这个套路。写代码时留意注释里的“W1C”含义,看到这三个字母就知道该写 1。
5.4 接收到的数据高低字节错乱
现象:两个节点互发报文,发送侧按字节顺序填的数据,接收侧读出来高低字节对不上,但 ID 和 DLC 都是对的。
原因:28335 eCAN 数据域有 DBO 位控制字节存放顺序。发送和接收两侧 DBO 配置不一致时,同一个报文的字节在对方眼里就是乱的。此外,有些开发板例程为了兼容性会把 DBO 设成 1,自己写代码时没注意到这个位,就出现了逻辑上“不可能出错”的错。
解决:通信双方统一 DBO。在初始化里显式配置,而不是依赖默认值。建议固定为 0,所有数据按正常字节序处理。调试时用固定两个字节的测试帧,例如 0xAA55,收到后能立刻看出是整体翻转还是按 16 位翻转了。
5.5 两边看着都是 500k,就是握不上
现象:两个 28335 节点都用同一个 CANBTC 配置,波特率都按 500k 算的,但双方一通信就出错,错误计数器一直涨。
原因:波特率公式里差一个参数,或者采样点设得太偏。最常见的是把 BRPREG 当成了直接分频值,实际有效分频是 BRPREG + 1,算出来的波特率差一档。也有的是 TSEG1/TSEG2 组合让采样点落在 90% 以上,总线稍微有点干扰就采样失败。两个节点如果晶振偏差再大一点,SJW 又设得小,同步会失败。
解决:用示波器量 CANTXA 上最窄的显性位宽度。500kbps 时一个显性位正好 2us,120k 时约 8.33us,量出来就知道实际波特率是多少。确认波特率正确后,把采样点调到 75% 到 80%,SJW 调大到 2 个 tq 以上。这两个调整可以解决绝大多数“看着都是 500k 就是不通”的玄学问题。
6. 双节点验证与采样点微调技巧
确认基础配置没问题后,我一般按三步做整机验证。第一步,两个 28335 节点都完成初始化,用一根短杜邦线或 PCB 走线连接 CANH/CANL,两端各放一个 120 欧终端电阻。第二步,节点 A 循环发送递增计数值,节点 B 用中断接收并在主循环里把收到的值写回一个 GPIO 控制的测试数组,通过调试器观察。第三步,反过来测 A 收 B,确认双向通路都通。
如果手头有 CAN 分析仪,挂在总线上同时看两边的帧,能直接区分是发送侧问题还是接收侧问题。没有分析仪时,把节点 B 的接收邮箱改成广播模式,也就是 LAM 全写 1,看能否收到节点 A 发来的任意帧,这一步能快速判断总线物理层正不正常。
采样点微调有一个原则:总线越短、节点越少,采样点可以越靠后;总线越长、节点越多,采样点要往前提。短总线上 80% 的采样点很稳,超过 10 个节点或者线长超过 10 米时,我习惯把 TSEG1 减小 1 到 2,让采样点落到 75% 左右,同时 SJW 保持 2。这样牺牲了一点抗噪声余量,换来的是总线上的容错。
调试期间还要养成看错误计数器的习惯。28335 的 CANES 寄存器里保留了错误状态和错误计数,通信偶发失败时,先看这个寄存器,区分是总线错误还是发送超时。如果错误计数值只增不减,优先怀疑终端电阻和共地问题,而不是代码逻辑。我第一次移植 28335 的 CAN 通信时,在 TA 标志上卡了一个下午,最后发现是总线没接终端电阻,发送帧一直得不到有效应答,从此所有 CAN 调试我都先确认物理层。
希望这些配置方法和踩坑记录能帮到你,让你在 DSP28335 的 CAN 通信上少走几步弯路。
本文还有配套的精品资源,点击获取