MC68HC908JB16 USB在系统编程(ICP)实战:固件升级与向量重定向详解
1. 项目概述与核心价值
在嵌入式产品开发中,固件更新一直是个绕不开的痛点。想象一下,一个已经焊接在无线键盘接收器板子上的微控制器,如果发现程序有BUG或者需要增加新功能,传统做法是什么?要么得把芯片吹下来,放到专用编程器上烧录,再焊回去;要么就得在板子上预留昂贵的专用编程接口(比如JTAG),不仅占用宝贵的PCB空间,还增加BOM成本。这对于产品开发阶段的快速迭代、生产线上的最后时刻修改,乃至产品售出后的功能升级,都构成了巨大的障碍。
我当年做消费类HID(人机接口设备)项目时,就深受其苦。直到遇到了在系统编程(In-Circuit Programming, ICP)技术,尤其是利用设备本身已有的USB接口来实现,才算真正找到了优雅的解决方案。飞思卡尔(现为NXP)的MC68HC908JB16这款微控制器,凭借其内置的USB模块和监控ROM,为ICP提供了绝佳的硬件基础。它不需要外部高压编程电压,靠内部电荷泵就能搞定FLASH的擦写,这让通过USB数据线进行“免拆机”固件升级从理论变成了触手可及的工程实践。
简单来说,这项技术的核心价值就是**“连接即升级”。用户无需任何特殊工具,只需一条USB线,配合PC端的上位机软件,就能完成固件的更新。这对于我们这些嵌入式工程师而言,意味着开发调试效率的极大提升,对生产部门而言,简化了流程,对最终用户而言,获得了产品持续改进的可能。今天,我就结合官方应用笔记AN2399和我的实际踩坑经验,把MC68HC908JB16通过USB实现FLASH存储器在系统编程**这套方案的里里外外、关键细节和实操要点,给大家掰开揉碎了讲清楚。
2. 硬件基础与编程模式解析
2.1 MC68HC908JB16的FLASH与USB特性
MC68HC908JB16(后文简称JB16)是HC08家族的一员,定位很明确:低成本USB设备。它集成了16KB的用户FLASH和32字节的用户向量区。最妙的是,它的FLASH编程和擦除不需要外部提供12V高压,全靠片内电荷泵自举升压完成。这就为通过USB这种只有5V和3.3V电平的接口进行编程扫清了最大的硬件障碍。
它的USB模块是一个全速(12Mbps)的功能控制器,符合USB 1.1规范。对于ICP来说,我们并不需要实现一个完整的、功能复杂的USB设备驱动,而是利用芯片监控ROM(Monitor ROM)中已经固化的USB通信处理程序。这片ROM的地址范围是$FA00–$FDFF和$FE10–$FFCF。这意味着,即使你的用户FLASH区域是空白的,芯片上电后,只要满足特定条件,就能自动运行这片ROM里的代码,建立起与PC主机的USB通信,从而为后续的编程操作铺平道路。这是JB16相比前代JB8等型号在ICP方面的巨大优势,我们不需要自己从头实现底层的USB协议栈。
2.2 两种关键的编程模式:空白器件与非空白器件
根据目标板上的JB16芯片其FLASH是否已被编程过,ICP的进入方式和流程有显著不同,这是理解整个方案的第一步。
2.2.1 对空白JB16的编程
所谓“空白”,指的是芯片出厂后或经过整片擦除(Mass Erase)后,FLASH内容全为$FF的状态。对于一块全新的、焊接到目标板上的JB16,如何让它第一次“认识”USB并进入编程模式呢?
答案藏在硬件连接上。应用笔记给出了一个经典的电路:在USB的D+和D-数据线上,分别通过一个470kΩ的电阻(R1和R2)上拉到VDD。这个电路通常可以做成一个小的“编程适配板”,串接在USB线和目标板之间。当JB16复位时,其USB模块会检测D+和D-线的状态。如果检测到这两个上拉电阻,芯片就会判定当前需要进入“USB ICP模式”,而不是去执行可能不存在的用户程序。
实操心得:这里有个细节,R2电阻是可选的,前提是你的目标板USB接口上已经在D-线有一个标准的1.5kΩ上拉电阻(用于标识全速设备)。但在实际工程中,我强烈建议保留R2。原因有二:第一,保证适配板的通用性,无论目标板是否有上拉,都能工作;第二,有些HID设备在正常工作时,可能会在软件中动态控制内部上拉电阻的连接,在ICP模式下,这个内部上拉可能未使能,导致检测失败。加上R2能确保硬件状态绝对可靠。
一旦通过这个硬件“暗号”成功进入USB ICP模式,PC端(需要预先安装好特定的USBICP驱动)就会识别到一个新的USB设备,而不是一个HID键盘或鼠标。此时,就可以通过上位机软件(如USBICP.EXE)向芯片发送固件数据了。这个过程是对空白芯片的“初次写入”,会写入包括用户程序、ICP判定代码以及用户向量在内的所有内容。
2.2.2 对非空白JB16的编程
这才是ICP技术的精髓所在:对已经运行着用户程序的设备进行升级。此时,芯片已经是一个功能正常的USB HID设备(比如键盘)。你不能再指望它上电后自动进入监控ROM模式,因为它会直接跳转到你的用户程序入口。
那么,如何让一个正在正常工作的设备“切换”到编程模式呢?这就需要我们在用户程序中预先埋入一个“后门”。这个后门的触发机制是整套方案设计的巧妙之处。JB16的ICP判定代码(通常被烧录在FLASH的$F800-$F9FF区域)在每次复位后都会执行一段检查逻辑,决定是跳转到用户程序(正常模式),还是留在监控ROM中执行ICP程序(编程模式)。
3. ICP判定机制与向量重定向详解
3.1 ICP判定代码的工作流程
ICP判定代码存储在$F800地址开始的地方,而JB16的复位向量($FFFE:$FFFF)默认就指向$F800。所以,每次芯片复位,第一个跑的就是这段代码。它的决策逻辑可以用一个流程图来清晰展示,但核心就是检查两个条件:
- 用户程序复位向量是否有效:它会去读一个叫做“伪复位向量”的位置($FF7C,这个地址存放的是用户程序实际入口地址的高字节)。如果这个值不在用户FLASH地址范围($BA 到 $F7)内,说明用户程序可能不完整或损坏,直接进入ICP模式。
- ICP_FLAG校验和是否正确:这是主要的软件触发机制。ICP_FLAG是一个两字节的标志字,位于用户FLASH区域的最后两个字节($F7FE:$F7FF)。它的值应该是从$F600到$F7FD这片FLASH区域所有字节和的补码(1‘s complement)。判定代码会实时计算这个校验和,并与存储在ICP_FLAG中的值比对。如果匹配,说明用户程序完好且未请求升级,跳转到用户程序;如果不匹配(尤其是被特意写为$0000),则进入ICP模式。
因此,要让一个运行中的设备进入升级模式,我们只需要在用户程序中,通过某种方式(例如,响应一个特殊的USB HID报告)将ICP_FLAG这两个字节擦写为$0000,然后让设备复位(通常就是模拟USB的重新插拔)。复位后,判定代码计算出的校验和必然与$0000不匹配,于是顺利进入ICP模式。
3.2 向量重定向:块擦除模式下的必由之路
ICP模式又分为整片擦除(Mass Erase)和块擦除(Block Erase)。整片擦除简单粗暴,把整个FLASH(包括用户程序、ICP判定代码、用户向量)全部擦掉再重写。但这种方式会擦掉$F800开始的ICP判定代码本身,导致下次复位后失去进入ICP的能力,除非你再次通过硬件上拉电阻的方式从空白芯片开始编程。这显然不是我们想要的“可持续”升级方案。
因此,实用的升级方案是块擦除模式。在这种模式下,我们只擦除和编程用户程序区($BA00-$F7FF),而保留$F800-$F9FF的ICP判定代码和$FFE0-$FFFF的用户向量区。这里就引出了一个关键问题:用户向量区是受保护的,只有整片擦除操作才能擦除它。既然我们不整片擦,那么这些向量地址里指向的中断服务程序入口地址就是固定的、旧的。
但我们的新程序的中断入口地址很可能已经变了。怎么办?答案就是向量重定向(Vector Redirecting)。
其原理是在用户程序区($F600-$F7FD之间)开辟一块区域,存放一系列的“伪向量”。每个伪向量由3字节组成:一个JMP指令码($CC),加上一个16位的绝对地址,该地址指向新程序中实际的中断服务子程序(ISR)。然后,我们把FLASH中固定的用户向量(如键盘中断向量在$FFE0:$FFE1)的内容,修改为指向对应的伪向量地址。
例如,新程序的键盘中断服务程序(KBI_ISR)实际在地址$AABB。那么我们在用户程序区找一个空闲位置,比如$F7DB,写入三个字节:$CC, $BB, $AA(注意HC08是小端格式,低字节在前)。然后将FLASH中$FFE0和$FFE1的内容分别改为$DB和$F7。这样,当发生键盘中断时,CPU会跳到$F7DB,执行那里的JMP $AABB指令,最终跳转到真正的中断服务程序。通过这种方式,我们实现了中断向量的“动态”指向,尽管向量表本身的物理位置是固定的。
注意事项:编写链接器脚本(.prm文件)或汇编头文件时,必须精确定义这些伪向量的地址,并确保它们所在的FLASH块不会被你的应用程序代码覆盖。通常把这部分区域放在用户程序区的末尾、ICP_FLAG之前,是一个稳妥的做法。
4. 安全与可靠性设计考量
4.1 安全访问保护:防止未经授权的代码读取
JB16的监控模式(Monitor Mode)功能强大,可以读取FLASH内容。为了防止他人通过此方式轻易dump你的固件代码,芯片设计了一个8字节的密码保护机制,密码存放在$FFF6到$FFFD这八个字节中。进入监控模式需要提供这8个字节的正确值。
如果我们的伪向量像上面举例那样,整齐地排列在$F7CF-$F7FD,那么攻击者很容易猜到密码就是这八个地址($FFF6-$FFFD对应向量)所指向的伪向量的目标地址(即JMP后面的地址)。为了增加破解难度,应用笔记建议采用更灵活的策略:
- 打乱顺序:不按中断向量表的顺序存放伪向量。
- 地址偏移:将整个伪向量数组在内存中整体平移一两个字节。
- 插入空位:在数组中故意留出空档。
- 随机嵌入:最高安全级别,将这关键的8个伪向量(对应
$FFF6-$FFFD)的目标地址字节,随机地分散隐藏在用户程序代码的各个地方。
在代码附录中,我们看到$FFD6-$FFDD这8个字节被预定义为了$01到$08。这实际上是用于另一种安全验证:在用户模式下,通过USB HID的Set_Feature报告发送一个8字节数据,只有这8个字节与芯片中存储的(即$FFD6-$FFDD的内容)完全匹配,才会执行将ICP_FLAG清零的操作。这为通过用户程序触发升级增加了一道软件密码锁。
4.2 应对编程过程中的电源故障
ICP过程中如果突然断电,可能导致FLASH数据写入不完整,使设备“变砖”。JB16的ICP方案通过ICP_FLAG的校验和机制,巧妙地实现了恢复能力。
核心思想是:只有校验和正确的程序才是可执行的完整程序。具体流程如下:
- 升级开始前,上位机软件首先发送命令将
ICP_FLAG写为$0000。 - 设备复位后,因校验和不匹配而进入ICP模式。
- 上位机开始擦除和编程新的用户代码。注意,此时先不写
ICP_FLAG。 - 如果在编程中途断电,那么新程序不完整,
ICP_FLAG也还是$0000。下次上电,校验和依然不匹配,设备会再次进入ICP模式,等待重新编程。 - 只有所有用户代码都成功写入并验证后,上位机才最后计算这片新代码的校验和,并将其写入
ICP_FLAG。 - 设备再次复位,此时校验和匹配,跳转到新的用户程序,升级完成。
这个机制确保了即使在最糟糕的断电情况下,设备也永远会停留在要么可运行旧程序(如果ICP_FLAG是旧的校验和),要么可进入ICP模式(如果ICP_FLAG是$0000或错误值)的状态,而不会陷入一个既不能运行也不能编程的“死循环”。
5. USB通信协议与上位机操作实录
5.1 基于USB标准请求与厂商自定义请求的协议
ICP模式下,JB16枚举成一个自定义的USB设备。通信主要依赖两类请求:
- 标准USB请求:用于基本的设备枚举,如
Get Descriptor(获取设备描述符)、Set Address(设置地址)、Set Configuration(设置配置)等。这部分由监控ROM中的代码处理,我们无需关心。 - 厂商自定义请求(Vendor-Specific Request):用于实际的FLASH操作。这是我们需要在上位机软件中实现的。关键命令如下表所示:
| 命令功能 | bmRequestType | bRequest | wValue (地址高字节) | wIndex (地址低字节) | wLength | 数据阶段 |
|---|---|---|---|---|---|---|
| Program_Row(编程一行) | 0x40(OUT) | 0x81 | 起始地址高字节 | 起始地址低字节 | 数据长度 | 要写入的数据 |
| Erase_Block(擦除块) | 0x40(OUT) | 0x82 | 起始地址高字节 | 起始地址低字节 | 0x0000 | 无 |
| Mass_Erase(整片擦除) | 0x40(OUT) | 0x83 | 0x0000 | 0x0000 | 0x0000 | 无 |
| Verify_Row(验证行) | 0x40(OUT) | 0x87 | 起始地址高字节 | 起始地址低字节 | 数据长度 | 期望的数据 |
| Get_Result(获取结果) | 0xC0(IN) | 0x8F | 0x0000 | 0x0000 | 0x0001 | 返回结果字节 |
其中,Get_Result命令用于查询上一个Program_Row、Erase_Block或Verify_Row命令的执行结果。返回0x01表示成功,0x04表示失败。
5.2 上位机软件操作步骤与避坑指南
官方提供了USBICP.EXE和MotorolaHID.exe(内含SetICP功能)等工具。下面结合我的经验,详解操作流程和关键点。
5.2.1 驱动安装(Demo 1)这是第一步,也是最容易出错的一步。你需要为处于ICP模式的JB16安装特定的USBICP.SYS驱动。当首次插入一个空白或已触发ICP模式的设备时,Windows会弹出“找到新硬件”向导。
- 关键点:必须选择“从列表或指定位置安装”,并手动指向包含
USBICP.INF文件的目录。如果让Windows自动搜索,很可能会失败。 - 避坑:建议在开发PC上预先安装好这个驱动。对于生产或升级环境,可以将驱动打包进你自己的上位机安装程序,或者使用
libusb等免驱方案重新实现上位机(当然,这需要更深入的开发)。
5.2.2 使用USBICP.EXE进行编程(Demo 2)
- 选择参数文件:启动
USBICP.EXE,首先会要求选择一个.imp参数文件。jb16icp_me.imp对应整片擦除模式,会编程所有区域(包括ICP代码)。jb16icp_be.imp对应块擦除模式,只编程用户区,保留ICP代码和向量。这是第一个关键选择,选错会导致后续无法再次USB升级。 - 擦除与空白检查:对于非空白芯片,先点击“Erase”擦除用户FLASH,然后点“Blank Check”确认是否擦除干净。对于首次编程的空白芯片,可以跳过此步。
- 加载S19文件:选择你的目标固件文件,格式是Motorola S-record(.s19)。这个文件由编译器(如CodeWarrior)生成,包含了地址和代码数据。
- 编程与校验:点击“Program”开始编程,完成后务必点击“Verify”进行校验。编程和校验两步缺一不可。校验是通过
Verify_Row命令逐字节比对FLASH内容和S19文件,是确保数据完整性的最后关卡。
5.2.3 使用MotorolaHID.exe触发ICP模式(Demo 3)当设备运行在正常模式(作为键盘)时,你需要用这个工具来“说服”它进入编程模式。
- 运行
MotorolaHID.exe,选择“SetICP (kbd, mse)”选项卡。 - 工具会自动枚举当前的HID设备。如果JB16产品的VID/PID是自定义的,可能需要手动修改。
- 安全码:这里需要输入8字节安全码,必须与芯片
$FFD6-$FFDD处存储的值一致(默认是$01, $02, ..., $08)。如果您的固件修改了这些值,这里必须同步修改。 - 点击“OK”,如果安全码正确,工具会通过HID报告命令将
ICP_FLAG写为零。 - 最后一步,也是必不可少的一步:将USB设备拔下再重新插入。只有硬件复位,才能让JB16重新执行
$F800处的判定代码,检测到ICP_FLAG无效,从而进入ICP模式。此时,设备管理器里会看到一个新的“Freescale JB16 ICP Device”,而不是原来的HID设备。
6. 工程实现要点与代码剖析
6.1 用户程序工程配置要点
要在你的项目中启用USB ICP功能,需要对开发环境(以CodeWarrior为例)进行特定配置:
链接文件(.prm):这是重中之重。你必须精确划分内存区域。
- 保留
$F800-$F9FF给ICP判定代码。这段代码通常直接包含官方提供的汇编文件(如JB16_ICP.asm)即可,它会固定链接到这个区域。 - 在用户区末尾(例如
$F600-$F7FD)划分出一块空间用于存放伪向量表。需要在.prm文件中用STACKTOP或SEGMENT命令保留这些地址,防止编译器分配变量或代码到此。 - 定义
ICP_FLAG符号,固定指向$F7FE。例如:ICP_FLAG: = 0xF7FE;。 - 将中断向量表(
VECTOR)的地址重定向到你的伪向量表起始地址。
- 保留
中断服务程序(ISR)的编写:你的每个ISR(如
KBI_ISR,Timer_ISR)都需要被正确定义,并且编译器要知道它们的绝对地址。在C语言中,通常使用#pragma TRAP_PROC或__interrupt关键字来声明中断函数。编译器会在链接时将这些函数的地址填入你指定的伪向量中。生成S19文件:确保编译器输出设置中,包含了从
$BA00开始到$F7FF的所有内容,并且格式为Motorola S19。
6.2 ICP判定代码关键逻辑解读
附录中的汇编代码是核心。我们看最关键的校验和判断部分(已添加注释):
clr V_ChkSumH ; 清零校验和高字节临时变量 clra ; 清零累加器A,用于计算校验和低字节 ldhx #$F600 ; H:X 寄存器对指向校验和起始地址 $F600 ChkSum_Loop: add ,x ; A = A + (X指向的字节) bcc Not_Overflow ; 如果加法没有进位,跳转 inc V_ChkSumH ; 如果有进位,校验和高字节加1 Not_Overflow: aix #1 ; X寄存器加1,指向下一个地址 cphx #(ICP_FLAG) ; 比较是否到达ICP_FLAG地址 ($F7FE) bne ChkSum_Loop ; 如果没到,继续循环 ; 循环结束,此时A中是$F600-$F7FD字节和的低字节,V_ChkSumH是高字节 add ICP_FLAG+1 ; A = A + ICP_FLAG低字节的内容 bcc Not_Overflow1 ; 处理进位 inc V_ChkSumH Not_Overflow1: tsta ; 测试A(低字节和)是否为零 bne USB_ICP ; 不为零,跳转到ICP模式 lda ICP_FLAG ; 加载ICP_FLAG高字节内容 add V_ChkSumH ; 加上校验和高字节 bne USB_ICP ; 结果不为零,跳转到ICP模式 ; 如果以上两个结果都为零,说明校验和匹配 jmp JMP_Reset_Init ; 跳转到用户程序(伪复位向量指向的地址)这段代码清晰地实现了我们之前说的校验和验证:计算$F600-$F7FD的和,加上ICP_FLAG本身的值,结果应为零。任何不匹配都会导致进入USB_ICP模式。
7. 常见问题排查与实战经验
在实际项目中应用这套方案,我踩过不少坑,这里总结几个典型问题和解决方法:
问题1:设备无法进入ICP模式,始终被识别为HID键盘。
- 排查步骤:
- 检查触发机制:确认上位机
SetICP工具发送的安全码是否正确,并提示发送成功。 - 确认复位:发送
Set_ICP_Flag命令后,必须物理上重新插拔USB。软件复位(如看门狗)有时可能不够彻底。 - 检查硬件连接:确保USB的D+和D-线没有对地短路或与其他信号线短路。测量D+和D-之间的差分电压是否正常。
- 检查ICP_FLAG:在用户程序中,添加一个调试接口(如通过串口打印),输出
ICP_FLAG地址的值,确认是否被成功写为$0000。 - 检查判定代码:确认烧录的固件中,
$F800开始的ICP判定代码确实存在且未被意外破坏。
- 检查触发机制:确认上位机
问题2:编程过程中失败,提示“Verify Error”。
- 排查步骤:
- 电源稳定性:这是最常见的原因。FLASH编程和擦除对电源电压和纹波非常敏感。务必确保目标板供电充足、稳定。尝试在目标板靠近MCU的电源引脚处并联一个100uF的电解电容和一个0.1uF的陶瓷电容。
- 时钟稳定性:JB16的FLASH操作依赖于内部时钟。检查CONFIG寄存器的配置,确保芯片使用的是稳定的时钟源(如外部晶振)。
- 数据缓冲区:上位机发送编程数据的速度可能快于MCU写入FLASH的速度。虽然监控ROM的代码应该处理了流控,但可以尝试在
Program_Row命令后增加适当的延迟,再发送Get_Result。 - FLASH寿命:FLASH有擦写次数限制(通常10万次)。如果同一块区域被反复测试编程,可能导致损坏。尝试对另一个FLASH块进行编程测试。
问题3:升级后程序运行不正常,但单独烧录正常。
- 排查步骤:
- 向量重定向错误:这是最大嫌疑。检查你的.prm文件,确认伪向量表的地址分配正确,且没有与其他代码/数据段重叠。使用仿真器或调试器,在中断发生时查看PC指针是否跳转到了正确的伪向量地址,以及伪向量中的
JMP指令是否指向正确的ISR地址。 - ICP_FLAG未更新:升级完成后,上位机软件必须正确计算并写入新的
ICP_FLAG校验和。如果这一步失败,设备下次上电会因为校验和不匹配而直接回到ICP模式,无法运行新程序。检查上位机日志,确认最后一步“Program ICP Flag”成功。 - 块擦除边界:FLASH擦除以块(Block)为单位。确认你的擦除操作覆盖了所有需要更新的区域,且没有误擦到不应擦除的区域(如伪向量表本身)。查看编译器生成的map文件,确认代码和数据分布。
- 向量重定向错误:这是最大嫌疑。检查你的.prm文件,确认伪向量表的地址分配正确,且没有与其他代码/数据段重叠。使用仿真器或调试器,在中断发生时查看PC指针是否跳转到了正确的伪向量地址,以及伪向量中的
问题4:如何在自己的上位机软件中集成ICP功能?官方工具是很好的参考,但往往需要集成到自己的生产测试工具或升级工具中。思路如下:
- 驱动层面:可以继续使用
USBICP.SYS驱动,通过Windows API(CreateFile,DeviceIoControl)与设备通信。你需要分析.inf文件,了解设备的GUID和通信方式。 - 免驱方案:更通用的方法是使用
libusb或WinUSB。当设备处于ICP模式时,其USB设备标识符(VID/PID)会改变。你可以为这个特定的VID/PID安装libusb的驱动。然后,使用libusb的库函数直接发送厂商自定义请求(libusb_control_transfer)来实现Program_Row、Erase_Block等命令。这需要你仔细实现协议逻辑,包括命令构造、数据发送、结果检查等。 - 协议实现:核心就是按照前面表格中的格式,构造USB控制传输(Control Transfer)的Setup包和数据包。注意USB的端序是小端(Little-Endian),与HC08 MCU一致。务必处理好每个命令后的
Get_Result确认。
通过这套详细的方案,我们成功地在多个基于MC68HC908JB16的键盘、演示器产品中实现了可靠的USB固件升级功能。它不仅仅是一项技术,更是一种提升产品全生命周期维护能力的设计理念。将升级的便利性交给用户,将调试的灵活性留给自己,这正是嵌入式工程师追求的价值所在。