
做DSP28335开发的人应该都有过类似的经历板子已经装进设备、外壳固定、甚至已经发到现场这时候甲方说“逻辑要改一下”总不能人背着仿真器出差吧所以串口升级这个功能做设备端的几乎绕不开。这篇文章就专门讲DSP28335的串口升级程序怎么设计、怎么写、怎么调试我会从方案选型、存储规划、通信协议、核心代码、上位机联调一直讲到踩坑排查全程按实际开发顺序走。讲的是28335但里面的思路换到28069、2837xD都一样适用。我默认看这篇文章的同学已经能用CCS编译下载DSP工程也明白Flash和RAM的基本概念。如果你还处在“刚点亮一个LED”的阶段建议先跑几个基础例程再来看不然有些地方会卡住。1. 为什么我最终选了串口做升级通道1.1 现场升级的真实痛点先对号入座先说个最常见的场景。某设备控制器用的是28335装在柜子里平时运行得好好的突然要改一个控制参数或者加一段保护逻辑。如果当初没做在线升级就得开柜门、拆端子、插仿真器运气好半小时搞定运气不好周围全是高压电调试工位都没有根本不敢动。串口升级解决的就是这个问题设备留一个RS232或者RS485口PC用一根USB转串口线连上去通过一段握手协议把新的固件传进去烧到Flash里重启即生效。整个过程不需要打开设备也不需要拆芯片普通售后人员培训一下就能操作。这个需求在电力电子、电机驱动、仪器仪表行业特别普遍。28335虽然老但存量项目非常多很多设备还在稳定跑着不会因为芯片“旧”就随便改平台。给老设备补一个串口升级功能是性价比最高的维护手段。1.2 串口相比其他方式的取舍可能有同学会问28335不是还能用CAN、SPI、并口引导升级吗为什么推荐串口这里我把几个常见方案的优劣放一起对比一下。升级通道硬件成本接线难度传输速率典型场景串口SCI很低芯片自带2根信号线加地线几乎都引出115200bps够用通用型设备维护CAN总线中等需要CAN收发器差分双绞线但需要网关转接较高速率工业总线系统以太网较高需要MAC/PHY网线直连调试方便最高中高端设备并行GPIO几乎为零引脚多定义乱较慢早期应急方案串口胜在“不挑设备”。几乎任何带28335的板子都会把SCI口引出来当调试口或者通信口上位机用USB转串口线就能连驱动人人都有协议自己定不受第三方库限制。速率方面115200bps看起来不高但一份典型的固件也就几十KB到一两百KB算上擦写Flash的时间实际瓶颈根本不在传输而在Flash编程所以速率完全够用。选择串口的另一个理由是可维护性。串口协议是透明的出了问题可以用串口助手抓包分析比黑盒方案好排障得多。我后来做的几个产品升级工具和调试工具直接复用同一个串口只靠帧头区分用途对现场人员来说也容易理解。1.3 这套方案的三个组成部分完整的串口升级系统不是“DSP里写段程序”就完了它由三块组成Bootloader固件烧录在DSP Flash里的一段引导程序上电先跑它负责判断是否升级、接收数据、擦写Flash、跳转APP。上位机工具跑在PC上的小软件负责打开串口、读取固件文件、分包发送、校验应答。通信协议定义Bootloader和上位机之间怎么握手、怎么传数据、怎么判断成功失败。三者缺一不可。协议是纽带Bootloader是核心上位机决定现场操作方不方便。我见过不少人只写了DSP端程序拿串口助手手动发十六进制数据调试验证这当然可以但真正交付时一定要有配套的上位机工具否则现场没人会用。这套方案做下来Bootloader本身就相当于一个简化版“一键刷机工具”跟手机升级系统是同一个逻辑引导区负责接收新系统、写入存储区然后拉起新系统跑。2. 先把地图画清楚存储分区与升级流程设计2.1 Flash空间划分与Bootloader位置选择28335的Flash共256K×16位分8个扇区命名从A到H每个扇区32K×16位。扇区大小、起始地址直接在芯片手册的Memory Map里查即可。升级方案动工前的第一件事不是写代码而是决定“谁住在哪里”。我常用的规划方式是这样区域扇区起始地址存放内容BootloaderH扇区0x3F8000引导程序自身升级专用段H扇区末尾或独立小段0x3F7F00附近APP入口地址、版本号、升级标志APP区A~F扇区0x330000开始用户应用程序预留区G扇区看情况双备份或参数存储为什么Bootloader放在H扇区因为28335上电复位后在Flash引导模式下会跳到0x3F7FF6去取复位向量。0x3F7FF6正好在H扇区边缘Bootloader放在H扇区只要在这个地址放一条跳转指令指向Bootloader入口上电就会自动进入Bootloader不需要额外干预。APP区不要和Bootloader重叠这一点很多人忽略。有人图省事把APP直接放到H扇区升级时把自己坐的椅子给拆了轻则程序跑飞重则变砖返厂。Bootloader和APP的存储范围必须物理隔离中间还要留一小段安全间隙。2.2 Bootloader与APP的交接方式Bootloader和APP是两套独立工程各有各的链接CMD、各有各的入口。Bootloader负责“引导”而不是“包含”APP二者必须保持边界清晰。上电复位后流程是芯片上电Flash引导模式Bootloader入口被执行。Bootloader先初始化时钟、看门狗、SCI。检查升级标志比如串口收到的握手命令、GPIO电平、或者固定延时。如果不需要升级延时一段时间比如500ms等待上位机握手没等到就直接跳转APP。如果需要升级进入接收流程完成后校验并跳转APP。跳转时机很关键。如果每次都傻等握手超时才进APP那设备正常断电重启也会被拖慢几百毫秒对有些实时系统来说不可接受。我的做法是默认快速启动不等待握手只有当检测到特定的GPIO状态组合比如某个拨码开关或者在启动瞬间收到特定握手帧才进入升级模式。这样既安全又快速。APP工程也要配合做一件事在Flash的固定位置放一个入口信息块里面存APP入口地址。Bootloader跳转时从约定的地址读取这个值用函数指针跳过去。如果用固定的“Bootloader编译时写死APP入口地址”的办法也行但版本一多容易乱不如动态读取来得稳。这块信息可以是在APP启动代码里写的一个常量结构体链接时通过#pragma DATA_SECTION放到独立段。2.3 通信协议与握手重传机制协议是整个升级过程中最容易被轻视、后面又最折磨人的部分。我建议在动手前先明确协议帧结构最好再画一个状态图或者写个文档。我的协议帧格式参考如下字段长度说明帧头2字节固定为0xAA55用于同步命令字1字节握手、数据、结束、应答块序号2字节当前块编号从0递增总块数2字节告知总共有多少块数据长度2字节本帧有效数据字节数数据域N字节最大256字节校验2字节CRC16覆盖从命令字到数据域握手流程是上位机发“握手请求帧”Bootloader收到后回“握手应答帧”应答里带Bootloader版本和APP版本号上位机确认版本匹配后开始逐块发送固件数据。每发一块数据Bootloader都回一个ACK或NAK上位机必须收到ACK才能发下一块。如果超时或者收到NAK上位机重发当前块重试达到上限则报错退出。这种“发一帧等一帧”的停止等待协议虽然简单但在115200波特率下面完全够用逻辑也最好排查。为什么不搞连续发送加滑动窗口因为没有必要。处理逻辑越简单踩坑的概率越低现场调试越容易给一位只懂操作的售后同事讲“帧头命令序号长度数据CRC”就能讲明白的事情不要为了技术上的“高端”引入复杂度。2.4 一次完整升级的七步状态流转把整个升级过程串起来就是下面七个状态设备上电Bootloader初始化。检测升级条件进入升级模式。上位机打开串口发送握手帧双方确认协议与版本。上位机读取固件文件分包发送Bootloader边收边写到RAM缓冲区。缓冲区攒够一页对齐Flash扇区大小执行擦写。全部数据写完Bootloader做整体CRC校验反馈结果。上位机发送“启动APP”命令Bootloader跳转。这里有一个细节我反复强调不要每收到一个字节就擦一次FlashFlash的擦写次数是有限的而且擦除很慢。正确做法是先收到RAM缓冲区里凑够一个扇区或者一个页再批量编程。28335的Flash最小可擦单元是扇区编程时字对齐即可但是整片更新时按扇区擦除再逐字写是最稳的。3. 核心代码落地SCI、Flash、跳转三座大山3.1 SCI初始化与波特率计算代码层面第一件事是把SCI配好。我用的是SCIA引脚配置在GPIO28和GPIO29内部上拉使能。28335的SCI有16倍分频和8倍分频两种方式当分频因子BRR不为0时按8倍分频计算波特率。波特率计算公式很简单BRR (LSPCLK / (8 × 波特率)) - 1假设系统时钟SYSCLKOUT为150MHzLSPCLK一般配置为37.5MHz目标是115200bps代入计算BRR (37500000 / (8 × 115200)) - 1 ≈ 39.7BRR取整为40实际波特率约115384bps误差不足0.2%完全在串口容忍范围内。初始化代码示例如下void scia_init(void) { // 使能SCIA时钟 EALLOW; SysCtrlRegs.PCLKCR0.bit.SCIAENCLK 1; EDIS; // GPIO28/29复用为SCI TX/RX EALLOW; GpioCtrlRegs.GPAMUX2.bit.GPIO28 1; // SCITXDA GpioCtrlRegs.GPAMUX2.bit.GPIO29 1; // SCIRXDA GpioCtrlRegs.GPAPUD.bit.GPIO28 0; // 使能上拉 GpioCtrlRegs.GPAPUD.bit.GPIO29 0; EDIS; // 数据格式8位数据、无校验、1个停止位、空闲线模式 SciaRegs.SCICCR.all 0x0007; // 使能发送和接收 SciaRegs.SCICTL1.all 0x0003; // 波特率115200 SciaRegs.SCIHBAUD.all 0x0000; SciaRegs.SCILBAUD.all 0x0028; // 40 SciaRegs.SCICTL1.all 0x0023; // 释放SCI复位 }这段代码看起来不多但有两个坑藏在细节里。第一配置波特率时BRR是16位值不能只给个8位值就完事115200对应的值是40没问题但如果换9600BRR会到243附近只写低8位就会出错。第二初始化最后一步一定要“先把SCI置于复位状态配置完成后释放复位”顺序反了会出现通信数据错乱的现象。3.2 Flash擦写寄存器操作与RAM运行要求Flash擦写是串口升级里最核心也最容易翻车的环节。28335的Flash控制寄存器在EALLOW保护下操作前必须写KEY操作过程中不能随意打断。擦写的硬件约束是Flash控制器执行擦除编程命令期间CPU不能从同一Flash区域取指令。这句话怎么理解如果你的擦写代码本身就是存在Flash里的执行擦除命令时CPU去取下一条指令结果发现自己在擦的位置指令还没取出来就已经被擦成0xFF程序马上跑飞。解决办法就是老生常谈的“把Flash擦写函数搬到RAM里跑”。写代码时把擦写相关函数用预处理指令指定到专门的RAM段链接时把这个段放到RAM区函数调用时会自动从RAM取指。RAM运行代码段的关键CMD写法大致是SECTIONS { .flashapi : load BOOT_FLASH, run RAMCODE, LOAD_START(_flashapi_loadstart), RUN_START(_flashapi_runstart), SIZE(_flashapi_loadsize) }这行配置我建议抄下来贴到工程注释里因为几乎每次新做项目我都会回来翻一下当时是怎么分配的。真正操作Flash的时候我的经验是尽量用芯片原厂提供的Flash API库而不是自己直接撸寄存器序列。原厂库把擦除、编程、状态检查都封装好了虽然自己按手册写也能出来但那些KEY序列、状态位顺序、等待时间手工写错一个比特就会把整个扇区搞坏排查起来非常痛苦。使用API时注意三点调用前关中断、把API库放到RAM运行、不要把API占用的RAM和APP运行所需的RAM重叠。3.3 CMD文件与地址对齐Bootloader和APP两个工程的CMD文件要分开维护而且边界规则要写死。先看Bootloader侧的地址分配我习惯把Bootloader自身放在H扇区也就是0x3F8000到0x3FFFFF这段。这里的“0x3F7FF6”复位向量指向Bootloader入口相当于把引导入口焊死在设备启动链路上。APP侧的起始地址要避开Bootloader的H扇区同时最好从扇区边界开始放也就是落在A扇区或B扇区起始位置。不要从某个扇区的中间地址开始布局倒不是不能用只是后续如果APP内容增长很容易在调整段布局时产生重叠。扇区对齐方面还有个细节擦除命令对的是整个扇区而串口传输的数据包不可能正好等于扇区大小。所以接收缓冲区一定要设计成一个“扇区大小的整数倍”的窗口边收边填填满就编程一页。比如每个扇区32K×16位也就是64KB字节你可以用1KB或者4KB作为一块收到4KB就往Flash编程区写一次这样Flash编程次数可控进度也能做得平滑。3.4 跳转实现与中断处理升级完成后跳转APP代码并不复杂但要求“干净利落”。void jump_to_app(uint32_t entry) { DINT; // 关总中断 IER 0x0000; // 清中断使能 IFR 0x0000; // 清中断标志 // 关闭PIE向量表中的使能位 PieCtrlRegs.PIEIER1.all 0; PieCtrlRegs.PIEIER2.all 0; // 更多PIEIERx可按需关闭 // 复位PIE防止残留中断向量干扰 PieCtrlRegs.PIECTRL.bit.ENPIE 0; // 从APP入口信息块读取跳转地址 // 这里假设APP入口地址存放在固定Flash地址处 uint32_t appEntry read_app_entry(); if (appEntry ! 0xFFFFFFFF) { void (*appMain)(void) (void (*)(void))appEntry; appMain(); } }跳转前最容易被忽略的是“中断残影”。Bootloader运行过程中SCI中断是开着的如果跳转前不把PIE中断表复位、不把所有中断标志清掉进入APP之后一个残留的串口中断马上触发非法向量APP跑起来几秒钟就飞而且很难查。所以在跳转代码里关全局中断、清IER、清IFR、关PIE这些动作一个都不能省。还有一个看起来不起眼但很实用的点跳转之前把看门狗关掉。如果Bootloader阶段把看门狗开着跳到APP之后APP的初始化代码还没来得及重新配置看门狗看门狗就先把系统咬复位了。跳转前禁用看门狗或者干脆在Bootloader初始化的第一行就关掉它直到APP自己的代码去配置。4. 上位机工具设计与联调记录4.1 上位机端该做的几件事DSP端写得再正经上位机工具做得难用现场一样推不下去。我在上位机端讲究四件事读文件、分包、重传、进度反馈。读文件方面不要只支持纯二进制bin还要能解析Intel HEX格式。很多集成开发环境默认导出的是hex文件带地址字段的上位机解析后既要把数据取出来还要核对地址是否落在APP区范围内一旦发现固件分区错位直接弹窗拒绝绝不能拿着错文件把设备刷死。分包发送时块大小要和DSP端协议对齐。比如我定义的一帧数据域上限是256字节那么一个4KB的固件块就被拆成16个数据帧。块序号贯穿整个文件DSP端按块序号组装方便实现续传和校验。重传策略我建议做成“收到NAK就重发当前帧连续超时15次判定失败”。不要一失败就立刻放弃串口链路受线缆质量、电磁干扰影响挺大的偶发一帧错误很正常但也不要无限重试否则现场人员会对着一个永远卡住的进度条干瞪眼。界面上的进度条不是花架子。现场刷固件的人关心的其实是“什么时候能好”一块进度条和清晰的状态文字比任何日志都重要。我做的版本还会在进度条旁边显示“当前块/总块数”这样一旦卡死对方可以口述“卡在123/512块”远程排查就有据可查。4.2 联调时的实际操作流程联调的过程我建议分三步走不要一上来就跑完整流程。第一步测试串口通道是否通畅。用串口助手发一个握手帧看Bootloader能不能正确应答。握手成功说明SCI初始化、波特率、接线都没问题。第二步做一个“只握手不升级”的测试包。DSP端收到握手帧后返回版本号上位机显示握手成功但先不进行擦写。这一步用来验证协议解析和设备识别逻辑。第三步完整升级。先刷一份肯定能跑的APP再反过来用串口升级回退到上一版形成一个闭环验证。第一次完整升级成功之后一定要故意断开一次串口验证上位机的超时重传和Bootloader的等待恢复能力。实测下来联调期间踩得最多的坑反而是环境问题。比如USB转串口线质量差传输稍大文件就丢字节比如调试时用的杜邦线太长超过30cm就可能受周围开关电源影响。有条件的话联调现场尽量用带屏蔽的成品串口线能省掉一半的莫名问题。4.3 调测中比较实用的辅助手段我想额外推荐一个调测手段在Bootloader里预留一个“回环测试”命令。上位机发一帧带特定模式的数据DSP端原样返回。这不是什么高深技术但现场排查“到底是线的问题还是代码的问题”时特别省时间跟在施工现场用万用表先量电压是一个道理。另一个辅助手段是在DSP端定义一个结构体来保存升级状态包括上次升级的块序号、结束状态、失败原因。这些信息放在Flash的独立小扇区里下次上电时Bootloader能读出并显示给上位机。真正出现升级失败变砖的情况时这段状态信息能直接告诉你“上次卡在擦除阶段”还是“卡在校验阶段”比盲猜高到不知道哪里去了。5. 常见问题与排查清单5.1 握手失败连不上Bootloader握手失败是出现频率最高的问题排在第一位的原因永远是“根本没进入Bootloader”。很多同学做升级功能时Bootloader烧下去了但设备上电还是直接跑APP原因是复位向量没有正确指向Bootloader或者Bootloader判断升级条件的代码逻辑写反了。排查顺序我建议从硬件往软件走用示波器或万用表量一下SCI TX/RX引脚上有没有波形。确认波特率匹配PC端设置115200DSP端也必须是115200。确认Bootloader里进入升级模式的GPIO电平状态是对的别把低有效和高有效搞反。最后才怀疑协议帧格式用串口助手手动发一帧看DSP是否返回应答。还有一个经常被忽略的地方Bootloader烧进H扇区后复位向量地址0x3F7FF6位置需要放跳转指令。如果这个位置是0xFFFFFFFF上电后自然就跳到了空白区神仙也连不上。5.2 擦写失败或校验不过擦写失败在仿真器调试阶段不容易暴露通常是在完整升级时才出现。典型现象是传输能完成但整体CRC校验和固件文件不一致。原因集中在几个方面擦写代码没有放到RAM运行擦除时CPU取指失败导致随机数据写入。Flash等待状态没有配置够150MHz主频下RANDWAIT和PAGEWAIT必须按手册设置到位写Flash时访问时序不对就会丢数据。数据帧长度和Flash编程粒度没对齐跨边界编程时地址错位。接收缓冲区覆盖了正在使用的RAM段比如DSP中断向量区被数据覆盖。在代码里我建议在每完成一个扇区擦写后主动读回一次校验发现不符立即停止并报告不要等全部写完才做最终CRC。那样能尽早定位到某个扇区、某个块而不是最后面对一整片错误区域。5.3 升级后跳转不到APP如果升级显示“成功”但设备重启后黑屏无反应问题多半在跳转阶段。先查跳转地址是否正确。APP入口不是Flash段起始地址而是APP的_c_int00入口地址。有人以为把APP放在0x330000跳转地址就填0x330000结果跳过去执行的是APP数据区的垃圾指令。正确做法是在APP工程里定义一个入口指针把它放到Flash的固定位置Bootloader直接读取这个指针的值。再查中断残留。跳转前没关PIE、没清IER、IFR进入APP后一个随机中断就能把程序带飞。这种情况的表现往往不是“完全不跑”而是“跑几下就死了”优先级很高。还有一个隐蔽问题APP工程里也需要配置对应的Flash等待状态如果你只在Bootloader里配了Flash寄存器跳转到APP后APP代码如果重新初始化了相关模块但配置错误照样跑不稳。检查一下APP启动代码里的系统初始化是否完整。5.4 传输中途断连和数据错位传输到一半断连大多数是硬件链路问题。USB转串口线在长线传输时丢帧很常见我会让上位机具备“自动重连”能力检测到串口空闲后重新发送握手帧而不需要人工插拔。数据错位则要怀疑帧同步。串口是字节流没有物理上的帧边界如果Bootloader的接收状态机不健壮一旦起始字节丢失后面的所有帧都会被错误解析。解决方法是帧头用固定的0xAA55并且接收状态机在寻找帧头时是“滑动检测”而不是“固定位置对齐”。也就是说只要当前字节不匹配帧头就继续往后搜而不是傻等着。我再加一个细节如果在接收过程中串口缓冲区溢出要能够恢复。溢出标志一旦置位状态机强制回到“找帧头”阶段丢掉积压数据。这种恢复逻辑看着简单没有它上位机多发了几个字节就会造成后续所有帧全部错位。5.5 长期维护角度看的两点建议最后聊两句长期维护层面的体会。第一Bootloader一旦烧录尽量不要频繁改动。它是设备的“最后防线”改Bootloader本身的风险远大于改APP。我见过有人把所有业务逻辑都放进Bootloader结果升级三次改三次Bootloader最后一次没擦干净直接变砖。Bootloader要小而稳只做引导、升级、跳转三件事。第二APP段规划最好预留双备份区。我后期做项目时会在Flash里规划两个APP区域一个运行区、一个下载区。下载区先接收完新固件并做整体校验确认无误后再一次性复制或切换运行区。虽然这会占用更多Flash空间但换来的好处是“升级失败还能回滚到旧版本”。28335的Flash容量在大多数场景下都够用多留一份空间远比多担一份风险划算。按照我自己的习惯这套串口升级方案从设计到联调通过大概需要三到五天其中一半时间都花在排查一些看起来很蠢的小问题上。真正让我觉得“这套东西值了”的时刻是后来在现场只需要一台笔记本、一根串口线就完成了对它内部的固件更新不用拆机也不用断电。如果你正准备给28335项目加这个功能别急着找代码抄先把存储分区图画出来、把协议表写清楚再动手敲键盘。这两张纸比什么都管用。