简介:面向TMS320F28335 DSP学习与开发者的CAN通讯源码包,聚焦ECan模块驱动实现,适用于工业控制、电力电子等领域中强调实时性与可靠性的分布式通信场景。包内提供ECan模块的完整驱动源码:一个C源文件包含模块初始化、消息发送与接收函数,一个H头文件定义相关数据结构和函数原型,并导出清晰的调用接口,方便在CCS工程中直接引用。通过这份源码,可系统学习CAN控制器工作模式(正常/测试)的配置方法、波特率预分频与位定时参数的计算、多个邮箱的接收滤波与发送优先级设置,以及错误计数器和状态标志的查询;同时,源码示范了如何在中断服务程序中调用接收函数处理数据,避免通信冲突与数据丢失。资源压缩包仅10KB,共2个文件,结构精简,从控制器模式选择到消息收发再到错误处理,形成完整闭环,可作为二次开发的基础模板。目前已有176人学习下载,适合正在调试TMS320F28335 CAN接口的嵌入式工程师,也适合刚接触C2000系列的新手理解CAN通信机制。
1. 源码在手的 TMS320F28335:从哪拆、怎么用、坑在哪
TMS320F28335 作为 TI C2000 系列里生命力极长的一颗浮点 DSP,不少工程师手里都有过一套或半套“流传”下来的源码工程。源码在手,第一反应往往不是兴奋,而是想快速跑起来看现象。但实测下来,最常见的局面是:编译报错一堆、下载没反应、PWM 输出完全不对,最后不得不把 TI 官方例程翻出来逐行对比。这篇博文就以“TMS320F28335 源码”为线索,把拿到一套源码之后从读懂工程结构、配置最小系统到调通 PWM 与 ADC 的完整路径讲清楚,顺带把链接器脚本、PIE 中断向量表、JTAG 调试这几个容易卡住人的点逐个拆开。适合刚切换到 C2000 平台的嵌入式工程师,也适合手上正攥着一份源码但迟迟不敢下手的开发老手。
2. 先拆源码工程:目录结构与“隐藏”的配置项
2.1 源码工程里的文件,到底各自干什么
一套结构合理的 TMS320F28335 源码工程,通常包含四类文件:硬件驱动源码、应用逻辑代码、链接器命令文件(.cmd)和系统初始化文件。硬件驱动源码一般以模块命名,比如DSP2833x_Adc.c、DSP2833x_PieCtrl.c、DSP2833x_GlobalVariableDefs.c,它们对应芯片内部外设的寄存器级操作,属于“硬件无关接口”之下的最底层。应用逻辑代码则是你真正要改的部分,比如电机控制里的pid_controller.c、pwm_interface.c,这些文件里通常直接调用驱动层提供的 API。链接器命令文件决定代码段和数据段在内存里的物理布局。系统初始化文件则包括DSP2833x_GlobalVariableDefs.c和DSP2833x_CpuTimers.c,前者为全局寄存器变量分配地址并映射到对应外设寄存器区域,后者负责 CPU 定时器的配置。
还有一个文件容易被忽略:DSP2833x_Headers_nonBIOS.cmd。它不是给链接器做最终分配用的,而是为了把外设寄存器结构体映射到物理地址。这个文件通常配合DSP2833x_Device.h里的结构体定义工作。只要换了芯片型号,这些头文件和 cmd 文件必须同步替换,否则整个工程会访问到错误的寄存器地址。
2.2 编译源码前,必须检查的 3 个编译器选项
源码工程拿到手后,第一件事不是改代码,而是确认编译选项是否匹配当前环境。以 Code Composer Studio 为例,有三个选项直接决定能否编译通过。
第一个是 “--silicon_version” 系列选项。F28335 属于 C28x 浮点系列,必须设置为--silicon_version=28并勾选 FPU 硬件支持。如果工程是从 F2812 这种定点 DSP 迁移过来的,这一项经常是错的,导致浮点运算被模拟成软件浮点,编译不会报错但运行速度会骤降。
第二个是 “--define” 宏定义。代码里经常出现#ifdef CPU1或#ifdef FLASH这样的条件编译块。在 Debug 模式下通常需要定义_LAUNCHXL_F28379D(如果使用的是官方板),或者定义FLASH来告诉编译器代码最终会烧写到 Flash。若源码里使用了 RAM 加载功能,但宏定义没打开,启动时会跳过memcpy数据段拷贝,全局变量初始值全部错乱。
第三个是包含路径。#include "DSP2833x_Device.h"这类头文件如果不在工程目录下,就必须在编译器的 Include Options 里添加路径。建议统一用相对路径,比如${PROJECT_ROOT}/include,避免换机器后路径失效导致源码目录结构被改乱。
2.3 链接器脚本:F28335 的“内存地图”由谁决定
CMD 文件是源码里的“隐藏 Boss”。一个典型的非 BIOS 工程至少需要两个 CMD 文件:一个是头文件 CMD,做寄存器地址映射;另一个是内存布局 CMD,决定.text、.data、.bss等段的位置。内存布局 CMD 里,MEMORY指令划分物理存储区域,SECTIONS指令决定哪个段放哪个区。
F28335 片内有 256K 字的 Flash 和 34K 字的 SARAM。常见做法是:BEGIN段指向 Flash 入口地址0x3F7FF6,这是复位后 CPU 跳转的地址;.text段一般放在 Flash 的RUNTIME区;codestart段放在 Flash 起始处。Debug 调试时,为了下载速度更快,也会把部分段安排在 RAM 里,比如.text放进RAML0,但正式释放固件时必须切回 Flash 布局。CMD 文件里的每一条origin和length都不能随便改,改错一个字,程序就会跑飞但编译不会报错。
3. 让源码跑起来:从导入工程到点亮一颗 LED
3.1 用 CCS 导入源码工程的两种操作路径
拿到源码后,不推荐直接在 CCS 里“File → New → Project”重新建工程,因为那样会丢掉原本的编译选项和依赖关系。更稳妥的做法是:首先进入 “Project → Import CCS Projects”,在 “Select search-directory” 里选中源码根目录,CCS 会扫描目录下的.project和.ccsproject文件。若能扫到,直接勾选导入;若扫不到,说明工程文件缺失,需要手动新建一个空项目,再通过链接方式把源码引进来。
手动引入的关键是 “Link the files to the project”,而不是复制代码。这样保持了源码结构不变,后续 Git diff 也会干净很多。导入后立即检查 Properties → Build → C2000 Compiler → Processor Options,确认设备型号是 TMS320F28335,并且浮点支持已勾选。还要在 “Runtime Model Options” 里,将 C 标准从默认的--c89保持住,因为 C2000 编译器对 C99 的stdint.h支持并不完整,改用 C99 反而会让某些结构体对齐方式产生意外。
3.2 最小系统源码:初始化系统时钟与看门狗
从头写一个最小启动文件,不如直接把 TI 官方的DSP2833x_SysCtrl.c拿过来用,但必须看懂它在做什么。以下是一段典型的时钟初始化调用:
// 初始化系统控制:PLL、看门狗、外设时钟使能 InitSysCtrl(); // 先将EALLOW置1,允许修改受保护的系统寄存器 EALLOW; // 禁用写保护 SysCtrlRegs.WDCR = 0xE8; // 禁止看门狗,置位WDCHK // 配置PLL倍频系数 // 外部晶振通常为 30MHz,PLLCR = 10 时 SYSCLKOUT = 150MHz SysCtrlRegs.PLLCR = 0x000A; // 等待PLL锁定 while (!(SysCtrlRegs.PLLSTS & 0x0001)) ; // 切换CPU时钟源到PLL输出 SysCtrlRegs.PLLSTS |= 0x0080; EDIS;这段代码的逻辑并不复杂:先打开写保护,关闭看门狗,然后设置 PLL 倍频系数。需要特别说明的是,PLLCR等于 10 并不会直接产生时钟输出,写在注释里的 150MHz 是通过锁相环内部经过两步计算得到的。真实情况是:F28335 外部晶振为 30MHz,PLL 模块先做倍频再分频,最后在PLLSTS的 DIVSEL 位为 0 时,直接输出 150MHz。若PLLSTS.DIVSEL设为 1,则输出 75MHz。这个参数在地下室候诊时容易被忽略,但它直接影响所有外设的时基。
3.3 GPIO 模块驱动:控制一个 LED 引脚的电平翻转
时钟跑起来之后,GPIO 操作就变得非常直接。以下代码把 GPIO0 配置为输出口:
// 使能 GPIO 时钟 EALLOW; SysCtrlRegs.PCLKCR0.bit.GPIOENCLK = 1; // GPIO 外设时钟 SysCtrlRegs.PCLKCR3.bit.GPIOINENCLK = 1; // GPIO 输入滤波时钟 EDIS; // 复用引脚为 GPIO 功能(而非外设功能) GpioCtrlRegs.GPAMUX1.bit.GPIO0 = 0; // 0 = 普通GPIO,1 = EPWM1A等外设 GpioCtrlRegs.GPADIR.bit.GPIO0 = 1; // 1 = 输出,0 = 输入 // 拉低电平,点亮LED GpioDataRegs.GPADAT.bit.GPIO0 = 0;这里有一个新手几乎必踩的坑:F28335 的引脚复用是软件控制的,每个引脚默认值可能是外设功能而不是 GPIO 功能。如果不手动把GPAMUX1对应的位清零,GPIO0 可能一直是 EPWM1A 的输出引脚,写数据寄存器不会报错,但引脚上的波形完全不对。另外,GPADIR决定方向,这个寄存器上电默认全部是 0,即输入模式,如果不把引脚设为输出,会读到外部电平而无法控制 LED 亮灭。
3.4 代码运行在 RAM 还是在 Flash?看codestart段
调试中最容易出现“下载成功但程序没跑起来”的现象,常见原因不是代码逻辑,而是入口地址错误。F28335 的 Boot ROM 会在复位后检查引导模式引脚,然后跳转到0x3F7FF6执行codestart分支。你的源码里必须有一个专门的分支段,它通常由汇编文件DSP2833x_CodeStartBranch.asm提供:
.sect "codestart" ; 指定段名为 codestart LB _c_int00 ; 跳转到C运行环境的入口如果链接脚本里没有把这个段放在0x3F7FF6,复位后 CPU 跳到 Flash 公共区域,那里可能是 0xFFFF 或随机数据,程序直接跑飞。这个错误编译器不会报任何信息,只能通过查看 map 文件来确认入口地址。用 CCS 的 Memory Browser 直接查看0x3F7FF6地址的机器码是否对应LB _c_int00的跳转指令,是最高效的判断手段。
4. 让源码干“正事”:PWM 与 ADC 的组合实战
4.1 配置 EPWM 模块:输出一组带死区的互补波形
F28335 的 EPWM 模块比很多工程师预想得更复杂,但只要是电机控制相关的源码,必然用到它。下面这段代码配置 EPWM1A 和 EPWM1B 输出频率为 20kHz、死区 1 微秒的互补 PWM 波:
// 关闭全局中断 DINT; // 初始化 EPWM1 的模块寄存器 EPwm1Regs.TBSTS.all = 0; // 清零时基状态 EPwm1Regs.TBPHS.half.TBPHS = 0; // 相位寄存器清零 EPwm1Regs.TBCTR = 0x0000; // 时基计数器清零 // 时基时钟 = SYSCLKOUT / (HSPCLKDIV × CLKDIV) // 150MHz / (1 × 1) = 150MHz,时基频率过高,实际应用中应再分频 EPwm1Regs.TBCTL.bit.CLKDIV = 0; // 分频因子 1 EPwm1Regs.TBCTL.bit.HSPCLKDIV = 0; // 高速时钟分频 1 EPwm1Regs.TBCTL.bit.CTRMODE = 0; // 向上递增计数模式 // 设周期寄存器得到 20kHz // TBPRD = 150MHz / 20kHz - 1 = 7499 EPwm1Regs.TBPRD = 7499; // 比较值设为周期一半,即占空比 50% EPwm1Regs.CMPA.half.CMPA = 3750; // 动作限定器:CNT=0 时置高,CNT=CMPA 时置低 EPwm1Regs.AQCTLA.bit.ZRO = 2; // 0x02 = 强制高 EPwm1Regs.AQCTLA.bit.CAU = 1; // 0x01 = 强制低 // 死区设置:上升沿延迟 150 个时钟周期 = 1 微秒 EPwm1Regs.DBCTL.bit.OUT_MODE = 3; // 双通道都有死区逻辑 EPwm1Regs.DBCTL.bit.POLSEL = 2; // A通道不反转,B通道反转 EPwm1Regs.DBRED = 150; // B通道上升沿延时 EPwm1Regs.DBFED = 150; // A通道下降沿延时 // 使能输出 EPwm1Regs.ETSEL.bit.SOCAEN = 1; // 使能 ADC 启动触发这段配置的关键在于死区逻辑,POLSEL = 2表示 EPWM1B 输出极性反转,配合延迟时间,就能在互补波形之间插入死区。这里的DBRED和DBFED的单位是 TBCLK 周期,TBCLK 是经过分频后的时钟。若想算成微秒,就要把分频系数考虑进去。把CLKDIV设为 1 时,TBCLK 直接等于 SYSCLKOUT,因此 150 个周期正好对应 1 微秒。
4.2 ADC 模块:用 EPWM 触发中断采集电压
F28335 的 ADC 是 12 位逐次逼近型,最大采样率为 12.5MSPS。源码中常见的做法是让 EPWM 触发 ADC 启动,然后在 ADC 中断里执行控制算法。下面的代码片段实现了顺序采样两个通道:
// 配置 ADC 的控制寄存器 AdcRegs.ADCTRL1.bit.ACQ_PS = 0x07; // 采样窗口为 8 个时钟周期 AdcRegs.ADCTRL1.bit.CPS = 0; // 预分频 /1 AdcRegs.ADCTRL2.bit.EPWM_SOCA_SEQ1 = 1; // 由 EPWM1A 的 SOCA 触发序列 AdcRegs.ADCTRL2.bit.INT_ENA_SEQ1 = 1; // 使能序列1中断 AdcRegs.ADCMAXCONV.bit.MAX_CONV1 = 1; // 转换两个通道(0和1) // 对通道 ADCINA0 和 ADCINA1 不用 AdcRegs.ADCCHSELSEQ 指定 // 因为F28335的默认状态会从SEQ1开始按顺序采样 // ADC 中断服务函数 interrupt void adc_isr(void) { // 读取转换结果,F28335为12位结果左对齐 Uint16 value0 = AdcResult.ADCRESULT0 >> 4; Uint16 value1 = AdcResult.ADCRESULT1 >> 4; // 将结果存储到全局变量,供主循环使用 adc_result[0] = value0; adc_result[1] = value1; // 清除中断标志,重新使能中断 AdcRegs.ADCTRL2.bit.INT_SEQ1_CLR = 1; // 清除 INT SEQ1 标志 PieCtrlRegs.PIEACK.all = PIEACK_GROUP1; // 向 PIE 控制器发送确认 }ADC 中断里有两处高频踩坑点。第一处是ADCRESULT0的位宽,F28335 的 ADC 结果是 12 位,存储在 16 位寄存器里按左对齐,所以在读取时要右移 4 位或者直接使用低 12 位。第二处是 PIE 模块的PIEACK,如果不在中断退出前清掉它,后续同组中断会被全部屏蔽,整个系统的外设中断都会“卡死”,但看代码又看不出问题。
4.3 把控制周期压进 5 微秒的源码优化思路
源码里控制周期长了,第一反应是优化 CPU 时间,但在 F28335 上更有效的做法是减少中断延迟。F28335 的中断响应默认要经历 PIE 仲裁、保护现场、进 C 环境这几步,若是裸机无实时操作系统的场景,可以把不必要的中断关掉,改用查询方式读取 ADC。另一种常见做法是把控制算法拆成两个部分,电压/电流内环用高优先级 ADC 中断驱动,速度环放在主循环里用定时器触发,这样既不丢数据,主循环压力也小。若中断服务程序里还调用了sin()或浮点除法,要在编译选项里打开 FPU 硬件支持否则会被转为库函数调用,计算耗时会从几百纳秒变成几微秒,直接挤爆控制周期预算。
5. 排错与验证:3 个能省半天调试时间的源码分析技巧
5.1 用 CCS 的 Expression 窗口核对寄存器实际值
源码“看起来对”但实际运行不符合预期时,不要猜,直接在 CCS 的 Debug 视图里添加寄存器表达式。比如把GpioDataRegs.GPADAT和EPwm1Regs.TBCTR添加进 Expressions 窗口,单步执行若干周期后观察TBCTR是否按预期的方向递增计数。如果TBCTR一直为零,可将EPwm1Regs.TBCTL拆开来逐位查看,重点确认CTRMODE和CLKDIV的二进制值。一个常见现象是:CCS 显示的值和你写入的完全不同。这种问题多数是EALLOW/EDIS没有闭合造成的。在EALLOW和EDIS之间写的寄存器才允许被修改,若缺少EALLOW,寄存器写入操作会被静默忽略。
5.2 查看 MAP 文件确认代码段是否漂移
编译生成的.map文件是源码工程的“体检报告”。在 CCS 的 Console 窗口或工程的 Debug 目录下打开.map文件,重点查看codestart节和.text节的起始地址。正常情况codestart应为0x3F7FF6,.text若运行在 Flash 布局中,地址范围应在 Flash 区域。若看到codestart地址变成了0x000000,说明链接器脚本中的BEGIN段没有被正确匹配,需要回到 CMD 文件检查BEGIN段的origin设置。还可通过.map文件里的GLOBAL SYMBOLS看到_c_int00的入口地址,确认它是否落在.text段内。这个方法比反复烧写 Flash 验证入口要快得多,也不依赖调试器连接状态。
5.3 借助示波器验证 PWM 波形与死区的物理真实性
软件里算出的频率和死区数值,最终要回到示波器上验证才算闭环。用示波器探头夹在EPWM1A和EPWM1B引脚上,两通道同时显示,分别读出频率与占空比。死区时间要在“脉宽放大”模式下观察,把时基调到 500ns/格。如果实测死区时间与设置值差异较大,比如设置了 1 微秒得到的是 1.5 微秒,需要回头检查CLKDIV分频系数是否真的为 1,因为TBCTL一旦被代码在运行时修改,初始化的寄存器配置会被覆盖。实际开发中常见遇到以下配置错误:EPWM 的时基时钟被前段的测试代码意外改成了 2 分频,但初始化函数没有重新赋值,导致死区偏大。示波器是检验源码行为最可靠的物理依据,比任何仿真器都直接。
本文还有配套的精品资源,点击获取