ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

DSP28335移植UCOSII并集成CANopen的完整开发记录

DSP28335移植UCOSII并集成CANopen的完整开发记录 简介一套面向嵌入式开发者的UCOSII移植与CANOpen应用工程基于TMS320F28335 DSP解决实时操作系统运行与Copley驱动器控制问题。压缩包共264个文件、约1.64MB包含以.c和.h为主的工程源码以及大量.obj、.asm、.cmd、.lib等编译链接与启动文件还有.pjt、.ccsproject等CCS工程配置可直接在CCS中打开编译。工程覆盖UCOSII在C28x内核上的移植细节如任务调度、内存管理、中断服务适配也完整展示CANOpen协议栈的集成方法包括对象字典、SDO/PDO配置与Copley驱动器的通信控制。已有965人学习代码结构清晰可作为工业伺服控制、运动控制项目的参考蓝本也能帮助初学者快速建立RTOS现场总线的整体认知。1. 项目背景与方案选型这两年做DSP28335相关的控制项目遇到的需求越来越像一个问题公式运算要够快、控制要够稳、通信还得够标准。这回的活儿就是一个典型样本——要在DSP28335上把UCOSII实时操作系统跑起来再叠加一层CANopen现场总线协议设备靠着CAN总线和上位机、其他节点做标准化的数据交换。先说结论这个组合单独看都不算新东西但真正把它们捏合在一起涉及中断体系、任务调度、总线时序、协议栈适配的交叉问题调试过程踩坑无数网上又找不到一篇能直接照做的完整记录所以我把整个流程和心得写下来给后面做类似移植的兄弟一个参考。1.1 为什么不直接裸机跑先说DSP28335本身。这颗芯片算是TI C2000系列的常青树150MHz主频32位浮点单元片内带256KB Flash和34KB SARAM做电机控制、并网逆变、电源类算法绰绰有余。裸机时代我写过很多主循环加中断的工程刚开始没什么问题但任务一多就露馅了ADC采样要实时、PWM占空比要刷新、通信协议要解析、状态指示灯要闪全揉在一个大循环里每增加一个功能都要重新梳理执行顺序而且一旦某个分支耗时过长整个系统的时序就跟着飘。UCOSII的价值恰恰是把这类问题结构化。它是一个抢占式实时内核任务按优先级调度高优先级任务就绪时能立刻抢到CPU。像我这次的应用控制回路任务必须保证严格周期通信任务可以容忍几毫秒延迟显示和调试任务压根不着急这种优先级天然分层的场景UCOSII的调度模型几乎就是照着需求画的。我选择UCOSII而不是FreeRTOS原因也很实际手头这块DSP28335的现有工程和参考资料里UCOSII移植案例更多内核代码量小调度逻辑直观出问题好排查。FreeRTOS对C2000的支持这几年也好起来了但在这个项目周期里选更熟悉、更稳妥的方案才是第一原则。1.2 CANopen在这里解决什么问题CANopen跑在CAN物理层之上遵循CiA 301标准核心是把通信行为标准化。以前多设备互联每家一个私有协议握手方式、报文格式、错误处理全都不一样联调时最痛苦的就是两边对着报文用手册猜。有了CANopen一切都围绕对象字典OD展开通信方式就四种PDO过程数据实时性高、SDO服务数据用来读写参数、NMT网络管理控制节点状态、心跳Heartbeat监测节点在线状态。放到这个项目里主站通过NMT命令让从站进入Operational状态从站周期性通过TPDO上报运行数据和采样值主站通过RPDO下发设定值参数调试走SDO。这套模型一旦跑通后续加节点、加通道都不需要改协议逻辑改对象字典和映射表就行。所以我毫不犹豫地把CANopen定成了这辆通信总线上的“普通话”而不是继续沿用关起门来自定义的协议。2. 开发环境与工程骨架搭建动手之前先把工具链和工程结构理清楚。这类移植项目最怕的就是代码写了一半发现编译器的指令集支持有问题或者工程目录乱成一团后面找文件全靠猜所以第一步必须稳。2.1 工具链组合与版本坑我的环境选择如下项目选择集成开发环境CCS 7.4.0编译器TI C2000 Compiler v18.1.5调试器XDS100v2目标芯片TMS320F28335为什么不推荐用太新的CCS版本原因是老工程依赖的库文件和头文件版本可能与新编译器不兼容编译报错一堆还没法快速定位。CCS 7.4搭配v18.x编译器对28335的支持已经很成熟网上参考案例也多遇到问题能搜到答案。另外TI现在主推CCS Theia新老工程混用容易出环境问题做移植这类工作稳定压倒一切。装完环境后建议先做一件事新建一个空的CCS工程烧一个GPIO翻转的裸机程序进去确认仿真器连接、Flash下载都正常。别小看这一步它能帮你把环境问题、仿真器驱动问题、目标板供电问题提前排除干净否则后面UCOSII跑不起来你根本分不清是代码问题还是环境问题。2.2 工程目录怎么摆后面找代码不抓狂移植项目文件多尤其UCOSII源码本身就有一堆.c和.h如果不做分层最后工程里会乱成一锅粥。我用的是分层结构把内核、移植层、驱动、应用完全拆开Project/ ├── UCOSTR/ │ ├── source/ /* ucos_ii.c、os_core.c、os_task.c 等内核源码 */ │ ├── port/ /* os_cpu.h、os_cpu_a.asm、os_cpu_c.c */ │ └── config/ /* ucos_ii.h 裁剪配置、includes.h */ ├── APP/ │ ├── main.c /* 板级初始化、系统初始化、任务创建 */ │ ├── tasks.c /* 应用任务定义 */ │ └── canopen_app.c /* CANopen 应用层逻辑 */ ├── BSP/ │ ├── ecandrv.c /* ECAN 底层驱动 */ │ ├── timer.c /* CPU Timer0 时钟节拍 */ │ └── led.c └── LPLD/ /* 第三方或现成的库文件 */这样拆分有个明显好处出问题能快速定位层次。比如任务调度异常先查port层和内核配置CANopen收发卡住直接去BSP和canopen_app里看不用在整个工程里翻来翻去。另外UCOSII源码文件我建议全量加入工程然后通过头文件宏裁剪而不是手动删除文件因为有些宏之间有关联少一个文件可能导致莫名其妙的编译错误。3. UCOSII移植的核心动作UCOSII能在PC上跑、能在STM32上跑不代表到了DSP28335上也能直接跑。移植的本质是把CPU相关的寄存器操作、栈操作、中断响应和系统节拍对齐到当前处理器架构上。DSP28335的C28x内核和ARM Cortex-M差异很大这几个关键动作必须要做到位。3.1 源码准备与数据类型对齐UCOSII源码我用的V2.91版本网上很容易找到。下下来之后不要急着加工程先打开os_cpu.h把数据类型定义确认一遍。这里有一个DSP28335特有的大坑C28x编译器里char类型默认是16位不是标准C的8位如果不处理后面INT8U、INT16U这些类型全都会错位更严重的是CANopen协议栈里大量使用uint8_t字节就错了。解决办法有两个方向一是在编译选项中加--char_bits8让char变为真正的8位二是在数据定义时小心处理。我个人建议直接用编译选项强制8位char这样可以保证UCOSII和CANopen协议栈的数据类型对齐到标准C模型后续砍掉的坑更多。还有一个细节是C28x栈的增长方向栈是从高地址向低地址增长的也就是递减栈OS_STK_GROWTH要设为1这个方向一旦搞反任务切换时现场恢复必然出错。3.2 任务栈初始化与上下文切换UCOSII的移植核心是四个东西OSTaskStkInit负责初始化任务栈OSStartHighRdy启动第一个任务OSCtxSw做任务级切换OSIntCtxSw做中断级切换。C28x的寄存器组比ARM要多现场保护需要把ACC、PREG、XT、ST0、ST1、DP、IER、IFR、XAR0到XAR7这些寄存器全部保存到任务栈中。我用TI的C28x汇编指令实现现场保护关键片段如下; os_cpu_a.asm 关键片段示意 _OSCtxSw: SETC INTM ; 关中断保护临界区 PUSH ST1 PUSH ST0 PUSH DP PUSH XAR7 PUSH XAR6 PUSH XAR5 PUSH XAR4 PUSH XAR3 PUSH XAR2 PUSH XAR1 PUSH XAR0 PUSH RPC pushf ; 保存 SP 到当前任务的 TCB MOVW DP, #_OSTCBCur MOVL XAR0, _OSTCBCur MOV *XAR0[0], SP ; 加载新任务 MOVW DP, #_OSTCBHighRdy MOVL ACC, _OSTCBHighRdy MOVW DP, #_OSTCBCur MOVL _OSTCBCur, ACC ...我特意说明这是示意代码实际使用时要对照TI的编译器手册确认每条指令的操作数寻址方式尤其要注意C28x的PUSH指令能一次压入32位寄存器对合理安排压栈顺序能减少指令数量。写完汇编后必须用CCS的Disassembly窗口单步跟一遍看每个寄存器的保存位置和恢复顺序是否严格对称这是整个移植过程中最需要耐心的一步。3.3 系统节拍与中断衔接UCOSII的时基我用的CPU Timer0配置成1ms中断一次。DSP28335的中断系统是两级外设中断经过PIE模块映射到CPU内核中断所以中断服务函数里要先清PIE应答位再调用内核相关函数。时钟中断服务函数这样写interrupt void Timer0ISR(void) { OSIntEnter(); // 进入中断 OSTimeTick(); // 通知内核一个时钟节拍 PieCtrlRegs.PIEACK.all PIEACK_GROUP1; // 清PIE应答 OSIntExit(); // 退出中断可能触发调度 }C2000编译器自带的interrupt关键字会在进入ISR时自动保存一部分现场退出时执行IRET。但UCOSII的中断切换有自己的现场管理逻辑两者配合时容易重复保存或者少保存务必要在编译后查看生成的汇编代码确认现场保护没有遗漏。我在这个环节花了整整一个下午最后是通过逐行对比纯裸机ISR和带OS的ISR汇编才定位到问题。3.4 最小多任务验证移植完成后不要急着上CANopen先跑一个最小验证程序。我建了三个任务两个LED任务以不同频率闪烁一个调试任务周期打印运行计数。代码如下void TaskLED1(void *pArg) { for (;;) { GpioDataRegs.GPBSET.bit.GPIO52 1; OSTimeDly(500); GpioDataRegs.GPBCLEAR.bit.GPIO52 1; OSTimeDly(500); } } void TaskDebug(void *pArg) { for (;;) { printf(UCOSII on DSP28335 OK, cnt%d\n, g_taskCnt); OSTimeDly(1000); } }两个LED闪烁频率明显不同调试任务正常打印说明任务创建、调度、延时机制已经打通。如果到这里都跑不顺就不要继续往上层加东西返回去查栈初始化、时钟节拍和中断这三块。4. CANopen协议栈集成UCOSII跑稳之后真正的大头才刚开始把CANopen协议栈接到系统里。CANopen在设计之初就做了平台无关性处理理论上任何带CAN控制器的芯片都能跑但实际落地时底层的CAN驱动适配、任务模型整合、对象字典配置每一步都有讲究。4.1 协议栈选型对比市面上的开源CANopen协议栈不少我评估了三种方案方案优点缺点CANopenNode代码结构清晰仍在维护SDO/PDO/NMT/心跳全支持驱动层需要自己适配文档偏少CANfestival历史久使用人数多EDS工具链完整工程结构较老栈大小消耗偏大自研精简版代码量最小完全可控只适合固定场景协议扩展性差最后我选了CANopenNode。原因很直接它的架构分层做得干净协议核心和底层驱动完全分离我只需要把ECAN的收发函数接到它的驱动接口上就行。如果你的项目只是固定的几个节点做周期数据交换自研精简版其实是更快的路子但考虑到后续可能要接不同厂家的主站设备、可能要扩展SDO配置完整协议栈的兼容性更让我放心。4.2 底层CAN驱动适配DSP28335自带ECAN模块支持CAN2.0B协议最多32个邮箱。初始化分几步使能ECAN外设时钟配置GPIO复用为CAN收发引脚设置波特率配置邮箱组最后使能CAN控制器。波特率配置是最容易出错的环节ECAN的位时间寄存器基于系统时钟150MHz计算频率越高BRP分频值的选择范围越窄。我用的250kbps波特率位时序参数通过TI的SYS/BIOS工具附件表验证过这里贴一个初始化片段void ECAN_Init(void) { EALLOW; SysCtrlRegs.PCLKCR0.bit.ECANAENCLK 1; // GPIO18: CANTXA, GPIO19: CANRXA GpioCtrlRegs.GPAMUX2.bit.GPIO18 1; GpioCtrlRegs.GPAMUX2.bit.GPIO19 1; EDIS; // 复位ECAN模块 ECanaRegs.CANMC.bit.DBO 1; // 数据字节顺序小端 ECanaRegs.CANMC.bit.SCB 1; // 32位邮箱模式 ECanaShadows.CANBTC.all 0x00E3; // 波特率计算值250kbps // 配置邮箱1为发送、邮箱2为接收 ECanaRegs.CANME.all 0; ECanaRegs.CANMD.all 0; ECanaRegs.CANMD.bit.MD2 0; // 接收方向 ECanaRegs.CANMD.bit.MD1 1; // 发送方向 ECanaRegs.CANME.all 0x03; ECanaRegs.CANMC.bit.CCR 0; // 退出配置模式 }底层驱动对外提供四个函数就行初始化、发送、接收、清除标志。发送函数要考虑邮箱是否忙接收函数要处理好数据从邮箱寄存器搬运到缓冲区的过程。CANopenNode底层在调用发送接口时会传入COB-ID、数据长度和数据指针这些都要能正确对应到ECAN邮箱的数据字段。提示ECAN模块在配置寄存器时必须先将CANMC的CCR位置1进入CAN配置模式改完配置再清零退出否则寄存器写不进去。4.3 对象字典与心跳映射配置CANopen的通信逻辑全部围绕对象字典展开。CANopenNode里OD通过一个大的结构体数组定义每个条目有关键的索引、子索引、读写权限和回调函数。我配置的关键对象如下索引名称配置值说明1000hDevice Type0x00000000设备类型1005hSync COB-ID0x00000080同步报文ID1017hHeartbeat Time0x00000064心跳周期100ms1800hTPDO1 COB-ID0x00000183节点ID3TPDO1 ID为0x1832000hApp Input0x00000000自定义应用数据心跳周期这里我特意选了100ms太短会增加总线负载太长又会让主站离线检测变得迟钝。如果你对实时性要求高可以把心跳周期缩到50ms但一定要评估总线在满负载情况下是否还能保证正常通信带宽。节点状态转换是CANopen里一个容易忽略但特别重要的点。设备上电后自动发送一个Boot-up报文COB-ID是0x700节点ID主站收到这个报文才知道节点上线了。随后主站发NMT命令让节点进入Pre-operational再切到Operational。如果节点在线它必须周期性发送心跳报文主站侧配置了心跳消费者后一旦超过规定时间没收到心跳就判定节点离线。这就是“超线进线离开”现象的本质——心跳超时即离线不需要总线级错误检测机制。4.4 让CANopen在UCOSII里安稳跑起来协议栈是代码也得有人调度它跑。我的做法是给CANopen单独开一个任务优先级放在控制任务之下、调试任务之上void CanopenTask(void *pArg) { for (;;) { CO_process(canopen_obj); // 处理SDO、NMT、PDO等事件 ECAN_CheckReceived(canopen_obj); // 将接收到的报文喂给协议栈 OSTimeDly(1); // 让出CPU保证其他任务运行 } }CANopen任务周期定为1ms实际执行时间远小于这个值所以不会挤压低优先级任务太多CPU。但这里要特别注意CANopen协议栈内部很多数据结构是全局的如果接收中断直接操作这些结构和CANopen任务会产生竞争。我最终把所有报文都放在CANopen任务里统一轮询处理没有使用接收中断虽然牺牲了一点实时性但换来了稳定性和调试便利。PDO发送则利用ECAN的周期发送机制我在CANopen任务里调用协议栈的PDO发送函数把传感器采样值、运行状态、报警标志等塞进TPDO映射缓冲区按同步帧触发或者周期发送。上位机主站再通过SDO读写2000h这类自定义对象实现参数在线整定。5. 调试实录与避坑经验这个项目做到联调阶段问题一个接一个蹦出来。我把印象最深的问题整理成速查表后面再遇到类似现象可以直接对照排查。5.1 UCOSII移植期的三个经典问题第一个是任务栈溢出现象是跑几分钟后系统死机复位后又能跑一会儿。排查思路很简单在任务里故意写循环检测栈最大使用深度或者直接用CCS的RTOS分析工具查看栈使用率。我的经验是DSP28335片内RAM充足任务栈直接给到2K字CANopen任务甚至给了4K字代价是RAM占用大一点但换来了稳定。第二个是时钟节拍不对现象是OSTimeDly(500)实际延时超过或者不足500ms。原因出在Timer0的周期寄存器配置上。C28x的Timer0周期计算是系统时钟/分频/目标频率如果PLL配置和Timer分频的对应关系没算清楚节拍就会偏。我用一个GPIO翻转示波器实测确认实际延时和设计值一致才继续往下走。第三个是中断切换现场错乱现象是任务切换后偶尔死机而且毫无规律。最后定位到OSIntCtxSw在中断返回时SP指针没有对准到任务栈的正确位置。UCOSII对OSIntCtxSw的入口条件是有要求的它假设SP已经指向中断帧的某一固定位置任何细微偏差都会导致恢复出来的寄存器不对。我最后在汇编里加了注释和调试断点逐字节对照SP的变化才解决。5.2 CANopen联调阶段的常见坑现象可能原因处理方法节点一直发心跳但主站不显示在线心跳消费者参数没配置或者Boot-up报文被过滤用CAN卡抓包确认Boot-up报文ID是否等于0x700节点ID主站下发NMT后节点无响应节点处于Stopped状态不响应任何非NMT报文先发启动节点命令(0x01)再发进入Operational命令(0x01或0x05)PDO数据在总线上能看到但主站解析乱码字节序或长度映射不对核对PDO映射条目确认DSP小端字节序和主站一致SDO读写超时对象字典地址或子索引填错对照EDS文件逐一检查索引和子索引最让我头疼的是一个SDO超时问题。现象是主站能收到心跳和PDO但SDO读2000h时一直超时。查了两天最后发现是CANopenNode的对象字典条目没有注册读写回调函数导致协议栈不知道如何处理这个对象的访问请求。补上回调后立马正常。这种问题靠看协议文档很难一眼定位最好的办法就是抓总线报文看请求发出后节点有没有回错误响应以及错误码是什么。5.3 性能和稳定性优化心得整个系统跑起来之后我做了几轮优化。UCOSII的系统节拍从1ms改成了500us一次控制任务的响应更平滑了代价是CPU占用率上升了约8%仍在可接受范围内。CANopen任务优先级我放在了控制任务之下两档保证PDO报文传输不会被控制计算卡在后面。还有一点是关于任务栈和中断。DSP28335的中断响应本身很快但ECAN模块的报文接收如果在中断里处理和CANopen任务轮询会打架这个我前面已经说了。如果你坚持用中断收CAN报文建议用信号量同步接收ISR只负责把数据搬进环形缓冲区并释放信号量CANopen任务阻塞等待信号量收到后再交给协议栈。这种方式实时性更好但代码复杂度会高一截我这次为了稳妥选择了轮询后续如果要提升通信实时性会优先改造这个环节。再分享一个调试小技巧CANopen联调阶段务必准备一个USB CAN分析仪价格不贵但能省下大量抓瞎时间。每一步操作都在总线上看报文ID和方向协议栈有没有问题一目了然。测心跳超时、NMT切换、PDO映射没有总线工具几乎等于蒙眼开车。本文还有配套的精品资源点击获取
返回列表