news 2026/10/15 15:35:26

DSP28377D CAN发送与中断接收实战:从寄存器配置到避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DSP28377D CAN发送与中断接收实战:从寄存器配置到避坑指南

简介:这份资源面向嵌入式开发初学者与DSP工程师,聚焦德州仪器DSP28377D微控制器上CAN总线的发送与中断接收实现,帮助读者掌握硬件模块配置、消息帧构建、中断服务例程编写及错误处理等关键环节。压缩包共217个文件,约818KB,以79个h头文件、30个c源文件、38个obj目标文件及36个d依赖文件为主,另含asm汇编、cmd链接命令、mk与makefile构建脚本、ccsproject与project工程配置等,完整呈现CCS工程结构。内容涵盖CAN模块选择、波特率设置、GPIO复用配置、发送邮箱填充与CRC校验、接收中断使能及位错误、格式错误等异常处理,并配有详细代码注释,便于理解程序流程。目前已有3258人学习下载,适合作为工业自动化、汽车电子等实时通信项目的参考范例,也可结合引导加载程序示例进一步理解启动阶段的CAN初始化过程。

1. DSP28377D 的 CAN 发送与中断接收:从寄存器到可复现的最小闭环

如果你手上有块 28377D 的控制板,CAN 收发却一直停在“能编译、不通、不知道卡在哪”的状态,这篇就是写给你的。DSP28377D 的 CAN 模块本质上是 eCAN 的升级版,叫 DCAN,寄存器布局和 28335 那套有相似也有差异,很多人第一次配的时候会栽在“消息对象(Message Object)到底怎么分配”上。我见过太多人把发送配通了、接收用轮询也能跑,但一上中断就翻车——要么进不去,要么进去出不来,要么收一帧丢一帧。这篇笔记就围绕“发送 + 中断接收”这条最小闭环,把邮箱分配、位定时、中断使能、ISR 写法、以及几个血泪级的坑讲清楚。适合已经能点灯、能跑通 GPIO,但对 CAN 还停留在“抄例程”阶段的嵌入式工程师。

2. 先把 DCAN 的邮箱机制吃透:为什么发送和接收不能随便抢邮箱

2.1 消息对象不是 FIFO,是带仲裁的独立槽位

DSP28377D 的 DCAN 模块有 32 个消息对象(Message Object),编号 1~32。每个对象都能独立配置成发送或接收,带自己的标识符、数据长度、掩码。它不像 STM32 的 bxCAN 那样有明确的“发送邮箱 3 个、FIFO 2 个”的固定分区,而是你把哪个对象配成发送,它就是发送;配成接收,它就是接收。这个灵活性是优点,也是坑的来源——因为一旦你把对象 1 配成发送、对象 2 配成接收,中断线怎么挂、状态怎么读,全看你自己的规划。

常见做法是:把对象 1~8 留给发送,对象 9~32 留给接收,中间留出边界,方便后面加过滤规则。我一般会先画一张表,把每个对象的用途、ID、方向写清楚,再动寄存器。否则调到最后,自己都忘了哪个对象在干嘛。

对象编号方向用途标识符中断使能
1发送周期上报0x100否
2发送事件告警0x101否
9接收控制指令0x200是
10接收参数下发0x201是

这张表不是形式主义,后面写 ISR 的时候,你要靠它判断“是谁触发的中断”。

2.2 位定时参数:别照抄别人的 500k 配置

位定时是 CAN 最容易“看起来对、实际错”的地方。DSP28377D 的 DCAN 时钟来自 CAN 模块时钟,通常由系统时钟分频得到。假设你的 CAN 时钟是 100MHz,想跑 500kbps,那么一个位时间就是 200 个时钟周期。这 200 个周期要拆成同步段、传播段、相位缓冲段 1、相位缓冲段 2。

我一般用这个公式先算:

位时间 = SYNC_SEG + (TSEG1 + 1) + (TSEG2 + 1)

其中 SYNC_SEG 固定为 1。采样点建议放在 75%~80% 位置。对于 200 个周期,可以取 TSEG1 = 149,TSEG2 = 49,采样点 = (1 + 150) / 200 = 75.5%。对应寄存器里 BRP 设成 0(分频 1),TSEG1 寄存器写 149,TSEG2 写 49。

// 假设 CAN 模块时钟 100MHz,目标 500kbps // 位时间 = 200 个 CAN 时钟周期 // 采样点约 75.5% CanbRegs.CAN_BRPE.bit.BRPE = 0; // 预分频扩展位,0 表示不分频 CanbRegs.CAN_BTR.bit.BRP = 0; // 波特率预分频,实际分频 = BRP + 1 = 1 CanbRegs.CAN_BTR.bit.TSEG1 = 149; // 传播段 + 相位缓冲段1 CanbRegs.CAN_BTR.bit.TSEG2 = 49; // 相位缓冲段2 CanbRegs.CAN_BTR.bit.SJW = 3; // 同步跳转宽度,一般取 3~4

这里 BRP 和 BRPE 拼起来才是真正的分频系数。很多人只改 BRP 忘了 BRPE,结果波特率差一倍。另外 SJW 别设太大,否则在干扰环境下反而容易误同步。

2.3 发送配置:从写入数据到请求发送的完整流程

发送一个 CAN 帧,在 DCAN 里不是“填个寄存器就发”,而是要先把数据写进消息对象的 DATA 区,再写控制位,最后置发送请求位。顺序错了,要么发不出去,要么发出去的是旧数据。

// 以对象 1 发送标准帧为例 void CAN_SendFrame(uint32_t id, uint8_t *data, uint8_t len) { // 1. 等待对象 1 空闲,避免覆盖正在发送的数据 while (CanbRegs.CAN_IF1CMD.bit.BUSY); // 2. 把消息对象 1 的内容读到 IF1 接口寄存器 CanbRegs.CAN_IF1CMD.bit.CONTROL = 1; // 访问控制位 CanbRegs.CAN_IF1CMD.bit.DATA_A = 1; // 访问数据 A CanbRegs.CAN_IF1CMD.bit.DATA_B = 1; // 访问数据 B CanbRegs.CAN_IF1CMD.bit.ARB = 1; // 访问仲裁位 CanbRegs.CAN_IF1CMD.bit.MASK = 1; // 访问掩码位 CanbRegs.CAN_IF1CMD.bit.MSG_NUM = 1; // 目标对象编号 CanbRegs.CAN_IF1CMD.bit.DIR = 1; // 方向:读 CanbRegs.CAN_IF1CMD.bit.BUSY = 1; // 启动传输 while (CanbRegs.CAN_IF1CMD.bit.BUSY); // 等待完成 // 3. 修改仲裁寄存器:标准帧、数据帧、ID CanbRegs.CAN_IF1ARB.bit.ID = id; CanbRegs.CAN_IF1ARB.bit.XTD = 0; // 标准帧 CanbRegs.CAN_IF1ARB.bit.DIR = 1; // 发送方向 CanbRegs.CAN_IF1ARB.bit.MSG_VAL = 1; // 消息有效 // 4. 写数据长度和数据 CanbRegs.CAN_IF1MCTL.bit.DLC = len; CanbRegs.CAN_IF1MCTL.bit.EOB = 1; // 单对象传输 CanbRegs.CAN_IF1MCTL.bit.TX_RQ = 1; // 请求发送 CanbRegs.CAN_IF1DATA.bit.DATA0 = data[0] | (data[1] << 8); CanbRegs.CAN_IF1DATA.bit.DATA1 = data[2] | (data[3] << 8); // 数据长度超过 4 时继续写 DATA2、DATA3 // 5. 把修改后的内容写回消息对象 1 CanbRegs.CAN_IF1CMD.bit.MSG_NUM = 1; CanbRegs.CAN_IF1CMD.bit.DIR = 0; // 方向:写 CanbRegs.CAN_IF1CMD.bit.BUSY = 1; while (CanbRegs.CAN_IF1CMD.bit.BUSY); }

这段代码里最关键的是 IF1CMD 的 DIR 位:读对象时 DIR=1,写对象时 DIR=0。很多人第一次写的时候忘了切换,结果数据写到了接口寄存器,根本没进消息对象。另外 TX_RQ 是在 MCTL 寄存器里,不是 ARB 里,别放错位置。

3. 中断接收怎么配才不丢帧:从邮箱到 ISR 的完整链路

3.1 接收邮箱的过滤规则:掩码不设对,什么都能进来

接收对象要配两样东西:标识符和掩码。掩码决定哪些位参与比较。如果你把掩码全设成 0,那就是“全接收”,任何 ID 都能触发这个对象。调试阶段可以这么干,但正式用的时候一定要收紧。

// 配置对象 9 接收标准帧,只接收 ID = 0x200 void CAN_ConfigRxMailbox(void) { while (CanbRegs.CAN_IF2CMD.bit.BUSY); // 读对象 9 当前内容 CanbRegs.CAN_IF2CMD.bit.CONTROL = 1; CanbRegs.CAN_IF2CMD.bit.ARB = 1; CanbRegs.CAN_IF2CMD.bit.MASK = 1; CanbRegs.CAN_IF2CMD.bit.MSG_NUM = 9; CanbRegs.CAN_IF2CMD.bit.DIR = 1; CanbRegs.CAN_IF2CMD.bit.BUSY = 1; while (CanbRegs.CAN_IF2CMD.bit.BUSY); // 设置接收标识符 CanbRegs.CAN_IF2ARB.bit.ID = 0x200; CanbRegs.CAN_IF2ARB.bit.XTD = 0; // 标准帧 CanbRegs.CAN_IF2ARB.bit.DIR = 0; // 接收方向 CanbRegs.CAN_IF2ARB.bit.MSG_VAL = 1; // 设置掩码:每一位都参与比较 CanbRegs.CAN_IF2MSK.bit.MSK = 0x7FF; // 标准帧 11 位全比较 // 使能接收中断 CanbRegs.CAN_IF2MCTL.bit.DLC = 8; CanbRegs.CAN_IF2MCTL.bit.EOB = 1; CanbRegs.CAN_IF2MCTL.bit.RXIE = 1; // 接收中断使能 CanbRegs.CAN_IF2MCTL.bit.UMASK = 1; // 使用掩码 // 写回对象 9 CanbRegs.CAN_IF2CMD.bit.MSG_NUM = 9; CanbRegs.CAN_IF2CMD.bit.DIR = 0; CanbRegs.CAN_IF2CMD.bit.BUSY = 1; while (CanbRegs.CAN_IF2CMD.bit.BUSY); }

掩码寄存器 MSK 的每一位对应 ID 的每一位,写 1 表示该位必须匹配,写 0 表示忽略。如果你只想匹配高 8 位,低 3 位随便,那就写 0x7F8。这个在接收多个相似 ID 的时候很有用。

3.2 中断使能的三层结构:模块级、对象级、PIE 级

DSP28377D 的中断链路比普通单片机多一层。CAN 的中断要经过:消息对象的中断使能位 → CAN 模块的全局中断使能 → PIE 中断使能 → CPU 中断使能。少一层,中断就进不来。

// 1. 消息对象级:在 MCTL 里已经设了 RXIE = 1 // 2. CAN 模块级:使能接收中断线 CanbRegs.CAN_CTL.bit.IE0 = 1; // 使能中断线 0 CanbRegs.CAN_CTL.bit.IE1 = 1; // 使能中断线 1 CanbRegs.CAN_CTL.bit.SIE = 1; // 状态改变中断使能 CanbRegs.CAN_CTL.bit.EIE = 1; // 错误中断使能 // 3. PIE 级:把 CAN 中断挂到 PIE 组 PieCtrlRegs.PIECTRL.bit.ENPIE = 1; // 使能 PIE PieCtrlRegs.PIEIER9.bit.INTx5 = 1; // CANB 中断在 PIE 组 9 的第 5 通道 PieCtrlRegs.PIEIER9.bit.INTx6 = 1; // CANB 中断线 1 // 4. CPU 级:使能全局中断 IER |= M_INT9; // 使能 INT9 EINT; // 使能全局中断 ERTM; // 使能实时调试中断

这里最容易漏的是 PIEIER9 的通道号。不同封装、不同参考例程里 CANB 挂的通道可能不一样,一定要对着数据手册的中断向量表确认。我见过有人把 INTx5 写成 INTx6,结果中断死活不进,查了一下午。

3.3 ISR 里到底该做什么:读状态、清标志、取数据

中断服务函数不是越短越好,而是“该做的做完,不该做的别碰”。接收中断进来后,你要先判断是哪个对象触发的,然后读数据,最后清中断标志。顺序反了,要么丢帧,要么重复进中断。

__interrupt void CANB_RxISR(void) { uint32_t status; uint32_t id; uint8_t data[8]; // 1. 读中断状态,判断是哪个对象 status = CanbRegs.CAN_IF2CMD.bit.MSG_NUM; // 这里只是示意,实际要读 CAN_ES 或对象状态 // 2. 把接收到的数据从对象 9 读到 IF2 接口 while (CanbRegs.CAN_IF2CMD.bit.BUSY); CanbRegs.CAN_IF2CMD.bit.CONTROL = 1; CanbRegs.CAN_IF2CMD.bit.DATA_A = 1; CanbRegs.CAN_IF2CMD.bit.DATA_B = 1; CanbRegs.CAN_IF2CMD.bit.ARB = 1; CanbRegs.CAN_IF2CMD.bit.MSG_NUM = 9; CanbRegs.CAN_IF2CMD.bit.DIR = 1; CanbRegs.CAN_IF2CMD.bit.BUSY = 1; while (CanbRegs.CAN_IF2CMD.bit.BUSY); // 3. 取 ID 和数据 id = CanbRegs.CAN_IF2ARB.bit.ID; data[0] = CanbRegs.CAN_IF2DATA.bit.DATA0 & 0xFF; data[1] = (CanbRegs.CAN_IF2DATA.bit.DATA0 >> 8) & 0xFF; data[2] = CanbRegs.CAN_IF2DATA.bit.DATA1 & 0xFF; data[3] = (CanbRegs.CAN_IF2DATA.bit.DATA1 >> 8) & 0xFF; // 4. 清中断标志:写 1 清除 CanbRegs.CAN_IF2CMD.bit.MSG_NUM = 9; CanbRegs.CAN_IF2CMD.bit.DIR = 0; CanbRegs.CAN_IF2CMD.bit.BUSY = 1; while (CanbRegs.CAN_IF2CMD.bit.BUSY); // 5. 应答 PIE PieCtrlRegs.PIEACK.all = PIEACK_GROUP9; }

注意第 4 步:清标志不是写 0,是写 1。这是很多外设的通用规则,但 DCAN 里具体清哪个位要看手册。另外 PIEACK 必须写,否则同组中断只能进一次。

4. 避坑与排查:CAN 调不通时先看这五个地方

4.1 现象:发送正常,接收中断死活不进

原因:PIE 通道号配错,或者 CAN_CTL 里的 IE0/IE1 没使能。还有一种可能是消息对象的 RXIE 没置 1,只配了全局中断。

解决:按“对象级 → 模块级 → PIE 级 → CPU 级”的顺序逐层检查。用调试器看 CanbRegs.CAN_CTL 的 IE0/IE1 位,看 PIEIER9 对应通道,看 IER 的 M_INT9。哪一层是 0,就补哪一层。

4.2 现象:能进中断,但数据全是 0 或者旧数据

原因:读数据之前没有把消息对象的内容搬到接口寄存器,或者 DIR 位设错。还有一种情况是中断标志没清,ISR 反复进,读到的永远是第一帧。

解决:确认 IF2CMD 的 DIR=1(读方向),BUSY 位等待完成后再读 DATA。清标志要在读完之后做,顺序不能反。

4.3 现象:波特率对不上,总线报错

原因:BRP 和 BRPE 没配合好,或者采样点算错。CAN 对波特率误差很敏感,差 1% 就可能通信失败。

解决:用示波器量一位的时间,反推实际波特率。如果差一倍,先查 BRPE。如果误差在 5% 左右,重新算 TSEG1/TSEG2,把采样点调到 75%~80%。

4.4 现象:多个接收对象时,中断只进一个

原因:多个对象的 RXIE 都使能了,但 ISR 里只处理了一个对象,其他对象的中断标志没清,导致中断线一直被拉低。

解决:在 ISR 里遍历所有使能了接收中断的对象,逐个读状态、取数据、清标志。或者用中断线分组,把不同对象挂到不同中断线上。

4.5 现象:发送请求置了,但数据没发出去

原因:TX_RQ 写在了错误的寄存器,或者消息对象的 MSG_VAL 没置 1。还有一种可能是 CAN 总线没有其他节点应答,发送一直重试。

解决:确认 TX_RQ 在 MCTL 寄存器里,MSG_VAL 在 ARB 寄存器里。用示波器看 CAN_H/CAN_L 有没有波形。如果没有波形,检查收发器使能引脚和终端电阻。

5. 进阶技巧:用中断线分组和双缓冲提升接收吞吐

5.1 把接收对象挂到两条中断线上

DSP28377D 的 DCAN 有两条中断线(INT0 和 INT1)。默认情况下,所有对象的中断都往 INT0 挤。如果你有多个高频接收对象,可以把它分成两组,一组挂 INT0,一组挂 INT1,这样两个中断可以并行响应,减少排队。

具体做法是在消息对象的 MCTL 寄存器里,除了 RXIE,还有一个位叫“中断线选择”。不同手册里叫法可能不同,有的叫 INTSEL,有的叫 MIL。把它设成 0 或 1,就能把对象分配到不同中断线。然后在 PIE 里分别使能对应的通道。

5.2 双缓冲接收:一个对象收,另一个对象备

CAN 接收最怕的是“上一帧还没取走,下一帧就来了”。DCAN 的消息对象本身没有 FIFO 深度,一个对象只能存一帧。如果数据来得快,就会覆盖。

一个实用的技巧是:用两个对象接收同一个 ID,一个作为“当前缓冲区”,一个作为“备用缓冲区”。在 ISR 里,如果当前缓冲区满了,就切换到备用缓冲区,然后尽快把当前缓冲区的数据取走。这样能扛住短时间的突发流量。

// 伪代码示意:双缓冲切换 volatile uint8_t active_buf = 0; void CAN_RxISR(void) { if (active_buf == 0) { // 从对象 9 取数据 read_data_from_mailbox(9); // 切换备用对象 10 为接收状态 enable_rx_mailbox(10); disable_rx_mailbox(9); active_buf = 1; } else { read_data_from_mailbox(10); enable_rx_mailbox(9); disable_rx_mailbox(10); active_buf = 0; } PieCtrlRegs.PIEACK.all = PIEACK_GROUP9; }

这个思路的核心是“永远有一个对象处于可接收状态”,避免数据覆盖。代价是占用两个邮箱,以及切换时的短暂窗口。如果总线负载不高,单对象就够了;如果负载超过 50%,建议上双缓冲。

5.3 验证方法:用回环模式先跑通,再上真实总线

DCAN 支持外部回环和内部回环。内部回环是发送的数据直接进接收,不经过引脚。调试阶段先用内部回环验证发送和接收的配置对不对,再切到正常模式接真实总线。

// 使能内部回环 CanbRegs.CAN_CTL.bit.TEST = 1; // 进入测试模式 CanbRegs.CAN_CTL.bit.LBACK = 1; // 内部回环 // 发送一帧,看接收中断能不能进,数据对不对 // 验证通过后,关掉回环 CanbRegs.CAN_CTL.bit.LBACK = 0; CanbRegs.CAN_CTL.bit.TEST = 0;

这个习惯帮我省了很多时间。以前我总是一上来就接总线,结果分不清是配置问题还是硬件问题。后来改成“先回环、再总线”,排查范围小了一半。

我自己的教训是:CAN 这东西,配置对了就是通了,配置错一个位就是不通,没有中间状态。所以每次改完寄存器,我都会用回环模式跑一遍,确认发送和接收的链路是通的,再上真实总线。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 15:35:24

C#上位机实战:EmguCV 4.2二维码检测、定位、标记与识别全解析

简介&#xff1a;本资源面向具备一定C#基础的计算机视觉开发者与学习者&#xff0c;聚焦EmguCV 4.2环境下二维码的检测、定位、标记与识别全流程实现。内容围绕QRCodeDetector的DetectAndDecode方法展开&#xff0c;涵盖边界框绘制、QrCode.Data数据解析&#xff0c;以及任意倾…

作者头像 李华
网站建设 2026/10/15 15:35:15

SHP矢量文件完整指南:含坐标系、QGIS加载与常见问题排查

简介&#xff1a;一套中亚五国矢量图数据集&#xff0c;以Shapefile格式封装&#xff0c;面向地理信息系统学习者、科研人员及规划分析从业者。数据覆盖哈萨克斯坦、乌兹别克斯坦、吉尔吉斯斯坦、塔吉克斯坦和土库曼斯坦五国边界范围&#xff0c;可直接用于地图制作、空间查询、…

作者头像 李华
网站建设 2026/10/15 15:34:36

快速排序深度解析:从分区优化到C++高性能实现

1. 为什么快速排序值得你花时间吃透如果你写过C&#xff0c;大概率在某个时刻被要求手写一个排序算法。很多人第一反应是冒泡或者插入排序&#xff0c;代码短、逻辑直白&#xff0c;考试也能过。但真正到了处理几万、几十万条数据的场景&#xff0c;O(n)的算法直接让你等到怀疑…

作者头像 李华
网站建设 2026/10/15 15:34:32

VRGIS与经纬网格驱动的AI标绘及动态播报系统构建

几乎每一个做应急调度、数字孪生大屏或态势感知系统的GIS开发者&#xff0c;都经历过这样的场景&#xff1a;地图上的点线面由操作员盯着监控手工标绘&#xff0c;画完之后还要打电话通知现场人员&#xff0c;现场处理完再回到地图上更新状态。整个过程快则几分钟&#xff0c;慢…

作者头像 李华
网站建设 2026/10/15 15:32:53

架构设计中的质量与风险:从源头到闭环管理

做架构设计这些年&#xff0c;我发现大多数团队在聊“软件开发的质量与风险”时&#xff0c;其实都在聊一个已经晚了一步的问题。质量缺陷被暴露出来的时候&#xff0c;通常早就不在它被创造的环节了&#xff1b;风险事件爆发的时候&#xff0c;也往往不在风险被引入的那个决策…

作者头像 李华