1. 项目概述
如果你正在折腾一块老旧的Freescale(现NXP)MC68HC908AS60微控制器,想要更新它的固件,或者你正在学习老派8位MCU的底层FLASH操作,那你来对地方了。今天咱们不聊那些高大上的ARM Cortex-M系列,就聊聊这颗经典的HC08内核芯片,特别是它内部那块32KB的FLASH 2TS存储器怎么玩。这块FLASH不像现在很多芯片那样有内置的Bootloader或者一键烧录,它的编程和擦除完全需要你手动控制寄存器、精确遵循时序,甚至还要搭外部高压电路,整个过程充满了“古典嵌入式”的硬核乐趣和挑战。
为什么今天还要研究这个?一方面,很多存量设备,特别是工业控制、汽车电子里的老系统,用的就是这些经典芯片,维护和升级离不开对它的深入理解。另一方面,从底层手动操作FLASH是理解存储器原理、时序控制和硬件交互的绝佳实践,能让你对“程序究竟是怎么跑进芯片里的”有更深刻的认知。本文将以官方应用笔记AN1827为蓝本,结合我实际调试中的踩坑经验,为你拆解MC68HC908AS60 FLASH 2TS的编程与擦除全流程,从硬件原理图到汇编/C代码实现,再到那些手册里不会写的注意事项,手把手带你搞定。
2. FLASH 2TS核心原理与硬件设计解析
2.1 FLASH 2TS存储单元与操作机制
MC68HC908AS60内部的FLASH 2TS模块,其核心是浮栅MOSFET存储单元。简单来说,每个存储单元就像一个可以锁住电荷的“小水池”(浮栅)。写入数据(编程)时,我们需要向这个“水池”注入电子;擦除数据时,则是把电子从“水池”里赶出去。这个注入和赶出的过程,需要比芯片正常工作的5V VDD高得多的电压,通常是8V到12V左右,这就是所谓的“高电压(Vhi)”。
芯片内部集成了一个电荷泵(Charge Pump),它的作用就是把VDD的5V“泵”到所需的高电压。这个泵电过程是FLASH操作中最“吃电流”、也最“吵闹”的阶段。正如应用笔记里提到的,编程和擦除模式会产生显著的电磁干扰(EMI)和电源噪声,这是因为电荷泵在短时间内需要很大的瞬态电流。如果你的系统里还有高精度的模数转换器(ADC)在工作,这段时间的转换结果很可能不可靠。因此,在规划系统任务时,最好避免FLASH操作与精密模拟采样同时进行。
2.2 关键硬件电路设计要点
要实现可靠的编程和擦除,外部电路设计至关重要。图9的硬件原理图是这一切的基础,我们来解读几个关键点:
Vhi高压生成与切换:这是整个设计的核心。图中需要一个独立的8V ±1%稳压源作为Vhi。为什么精度要求这么高?因为编程/擦除电压的稳定性直接影响到浮栅上电荷注入的可靠性,电压偏差可能导致编程失败或器件寿命缩短。那个“VOLTAGE SWITCH”是关键,它负责在正常运行时将IRQ引脚连接到VDD(5V),而在需要编程FLASH块保护寄存器(FLBPR)时,切换到Vhi(8V)。特别注意:对于常规的FLASH阵列编程和擦除,IRQ引脚必须始终接VDD!只有操作块保护寄存器时才需要切换到Vhi。这个细节极易被忽略,接错可能导致无法编程或芯片锁死。
电源去耦与滤波:原理图中在VDD和Vhi上都放置了0.1uF的陶瓷电容到地(GND)。这不仅仅是常规操作,对于抑制电荷泵工作引发的电源噪声至关重要。我的经验是,尽可能将这两个电容靠近芯片的电源引脚放置,并且可以考虑并联一个10uF的钽电容来提供更好的低频噪声抑制,确保在电荷泵启动瞬间,电源电压不会出现大幅跌落。
复位与时钟电路:10K的上拉电阻和连接到VDD的复位开关是标准配置。需要注意的是,在FLASH操作期间,必须确保系统时钟稳定。原理图中使用的9.8304MHz晶体和配套的CGMXFC引脚上的电容(图中未显示具体值,通常为10-22pF),需要根据芯片数据手册精确选择,因为总线频率直接关系到后续软件延时子程序的计算。
3. 软件架构与核心寄存器剖析
3.1 FLASH控制寄存器(FLCR1/FLCR2)详解
软件操作FLASH的本质,就是通过配置FLASH控制寄存器(FLCR)来启动内部的状态机和电荷泵。MC68HC908AS60有两个FLCR寄存器,分别对应两个独立的FLASH存储阵列(FLASH-1和FLASH-2)。你需要根据要操作的目标地址,来决定操作哪个寄存器。
- HVEN (Bit 3) - 高电压使能:这是整个操作的“总开关”。置1后,内部电荷泵启动,开始生成编程/擦除所需的高电压。重要原则:高电压施加的时间必须严格遵循数据手册中的时序参数(tERASE, tSTEP等),超时可能损伤存储单元。
- ERASE (Bit 1) - 擦除控制:置1表示进入擦除模式。擦除是以块(Block)为单位进行的,块的大小由BLK位决定。
- PGM (Bit 0) - 编程控制:置1表示进入编程模式。编程是以页(Page,8字节)为单位进行的。
- MARGIN (Bit 2) - 边际读控制:这个位主要用于工厂测试,在正常的用户编程中,它用于“智能编程”算法。在编程脉冲后设置此位,可以以更严格的电压阈值读取FLASH,从而快速验证编程是否成功,避免不必要的重复高压脉冲,提升效率和器件寿命。
- BLK1, BLK0 (在FLCR中):这两位组合起来选择要擦除的块大小。
00对应整个32KB阵列,01对应16KB(半阵列),10对应512字节(8行),11对应64字节(单行)。擦除时,你只需要提供块内的任意一个地址即可。
3.2 块保护寄存器(FLBPR1/FLBPR2)与安全机制
FLASH块保护寄存器用于防止对特定区域的意外擦写,常用于保护Bootloader或关键参数。要编程(即设置或清除保护)这两个寄存器,有一个特殊且关键的硬件要求:必须将IRQ引脚切换到Vhi(8V)高压。这就是原理图中那个电压开关存在的核心原因之一。在软件流程上,无论是擦除还是编程例程,第一步操作之后都会紧接着执行一次对FLBPR寄存器的“虚读”(dummy read)。这个操作并非为了获取数据,而是一个必需的硬件序列,用于解锁内部的高压开关逻辑,是时序中不可或缺的一步。
4. 擦除操作(Erase)的完整实现与拆解
擦除操作的目的是将一整块FLASH的内容全部置为1(0xFF)。官方代码提供了EraseRoutine子程序,其流程严谨地遵循了9步序列。
4.1 擦除主流程(Erase.mrt / Er_mrt.c)解析
主程序Erase.mrt(或C语言的main函数)的任务是搭建舞台,然后调用真正的演员EraseRoutine。
; 汇编示例 (Erase.mrt) Start: mov #$71,config-1 ; 关闭COP(看门狗),但保持LVI(低压复位)开启 lda #$00 sta flcr1 ; 初始化FLCR1和FLCR2中的频率分频控制位 sta flcr2 ; 为电荷泵设置合适的时钟 ldhx #$4001 ; 加载要擦除块内的任意地址 sthx FLASH_addr ; 例如$4001 lda #eraseallrows. ; 设置擦除整个32KB阵列 jsr WriteFLCR ; 调用子程序配置FLCR中的BLK位 jsr EraseRoutine ; 执行擦除 bra * ; 完成后进入死循环关键点与实操心得:
- 关闭COP:FLASH操作耗时较长(毫秒级),必须禁用看门狗定时器(COP),否则操作中途会被复位。
- 电荷泵时钟:
flcr1和flcr2中的FDIV位用于设置电荷泵的时钟分频。示例中设为$00,这需要根据你实际的系统总线频率(Bus Frequency)来调整。计算公式在数据手册中,目的是让电荷泵工作在最佳频率。如果设置不当,可能导致高压生成不稳定,擦写失败。 - 地址选择:
FLASH_addr只需是目标擦除块内的任何一个有效地址。例如,要擦除以$4000开头的64字节块,地址可以是$4000到$403F中的任意值。
4.2 擦除核心子程序(EraseRoutine)步步为营
EraseRoutine是执行擦除时序的精密状态机。我们结合流程图和代码,一步步看:
步骤1-3:启动擦除序列
EraseRoutine: sei ; 步骤0(隐含):关中断,保证时序不被打断 ldhx FLASH_addr ; 加载目标地址 lda #erase. ; 步骤1:设置ERASE位 jsr WriteFLCR lda flbpr1 ; 步骤2:读块保护寄存器(虚读,解锁序列) lda flbpr2 sta ,X ; 步骤3:向目标地址写入任意值(如$FF)注意:步骤3的写入操作是触发擦除状态机所必需的,写入的数据本身无关紧要。
步骤4-6:施加高电压与擦除等待
lda #hven. ; 步骤4:设置HVEN位,启动电荷泵,施加高电压 jsr WriteFLCR lda #terase ; 步骤5:等待擦除时间 tERASE (~102ms) ... ; (这里调用Delay子程序,具体循环见后文) lda #hven. ; 步骤6:清除HVEN位,关闭高电压 jsr WriteFLCR这里有个大坑:tERASE这个延时值(代码中为$F6)是基于特定的总线频率2.4576MHz计算得出的。如果你的系统频率不是这个值,必须重新计算!延时计算公式通常为:延时周期数 = 所需时间(秒) * 总线频率(Hz)。使用错误的延时,要么擦不干净,要么过度应力损伤FLASH。
步骤7-9:收尾与恢复
lda #tkill ; 步骤7:等待高电压消退时间 tKILL (~209µs) jsr Delay lda #erase. ; 步骤8:清除ERASE位 jsr WriteFLCR lda #thvd ; 步骤9:等待返回读模式时间 tHVD (~66µs) jsr Delay cli ; 重新开启中断 rtstKILL和tHVD同样与频率相关,必须校准。
5. 编程操作(Program)的智能算法实现
编程操作比擦除更复杂,因为它采用了一种“智能算法”(Smart Programming Algorithm),目的是用最小的压力和最短的时间完成可靠编程。
5.1 编程主流程与智能算法思想
主程序Program.mrt准备数据并调用Prog8Bytes。智能算法的核心思想是:用尽可能短的高电压脉冲尝试编程,然后立即验证;如果失败,则施加下一个稍长的脉冲,直至成功或达到最大尝试次数。这避免了传统“一刀切”的长脉冲可能带来的过度编程压力。
Prog8Bytes子程序管理一个尝试计数器(attempt),最大尝试次数flsPULSES定义为100次。每次尝试称为一个“脉冲”(Pulse),每个脉冲都包含完整的17个步骤。
5.2 编程核心子程序(Prog8Bytes)流程精讲
步骤1-4:初始化与数据装载
Prog8Bytes: sei ; 关中断 clr attempt ; 步骤1:清空尝试计数器 NextAttempt: ldhx FLASH_addr ; 取编程页起始地址(必须是$xxx0或$xxx8) lda #pgm. ; 步骤2:设置PGM位 jsr WriteFLCR lda flbpr1 ; 步骤3:读块保护寄存器(虚读) lda flbpr2 ... ; 步骤4:将8字节数据从RAM缓冲区复制到FLASH地址 ; (使用循环或如代码所示的8条sta指令)关键细节:编程页的起始地址必须是8字节对齐的(末位十六进制为0或8)。这是由FLASH内存的页缓冲区硬件结构决定的。
步骤5-12:高压脉冲与边际读验证这是最核心的时序循环:
lda #hven. ; 步骤5:设置HVEN,开启高压 jsr WriteFLCR lda #tSTEP ; 步骤6:等待编程步长时间 tSTEP (~1ms) jsr Delay ; 这是主要的编程高压施加时间 lda #hven. ; 步骤7:清除HVEN,关闭高压 jsr WriteFLCR lda #tHVTV ; 步骤8:等待HVEN低到MARGIN高的时间 jsr Delay lda #margin. ; 步骤9:设置MARGIN位,准备边际读验证 jsr WriteFLCR lda #tVTP ; 步骤10:等待MARGIN高到PGM低的时间 jsr Delay lda #pgm. ; 步骤11:清除PGM位 jsr WriteFLCR lda #tHVD ; 步骤12:等待返回读模式时间 jsr Delay步骤13-17:验证与循环判断
... ; 步骤13&14:逐字节比较FLASH内容与原始数据 ; 如果全部匹配,跳转到Complete bne Repeat ; 如果有任何不匹配,跳转到Repeat Complete: ; 编程成功 lda #margin. jsr WriteFLCR ; 清除MARGIN位 clra ; 返回0(成功) bra Return Repeat: ; 编程失败(本次脉冲) lda #margin. jsr WriteFLCR ; 步骤15:清除MARGIN位 inc attempt ; 步骤16:尝试计数器加1 lda attempt cbeqa #flsPULSES, Return ; 步骤17:如果达到最大尝试次数,退出并返回错误 jmp NextAttempt ; 否则,回到NextAttempt开始下一次脉冲 Return: cli rts ; 返回时,A=0表示成功,非0表示失败智能算法的优势:对于已经部分编程或特性较好的存储单元,可能第一个1ms脉冲就成功了,避免了后续不必要的95次高压应力,极大地提高了FLASH的耐久性(Endurance)。
6. 关键支撑子程序与延时计算
6.1 WriteFLCR子程序:地址感知的寄存器写入
这个子程序虽然短小,但至关重要。它根据全局变量FLASH_addr的值,判断目标地址属于FLASH-1(起始地址$8000)还是FLASH-2(起始地址$0450),从而决定操作FLCR1还是FLCR2。
WriteFLCR: cphx #flash-1 ; 与FLASH-1的起始地址比较 bhs Array1 ; 如果大于等于,则跳转到Array1 (FLASH-1) eor flcr2 ; 否则,操作FLCR2:用传入的值与FLCR2异或 sta flcr2 ; (异或操作是为了翻转特定的控制位) bra FLCR990 Array1: eor flcr1 ; 操作FLCR1 sta flcr1 FLCR990: rts这里用到了“异或”(EOR)来设置或清除特定位。例如,要设置HVEN位(0x08),传入0x08,与寄存器异或后,该位会被置1(如果原来是0)或保持1(如果原来是1)。要清除HVEN位,同样传入0x08,异或后该位会被清0。主程序通过传入不同的位掩码(#erase.,#hven.等)来控制流程。
6.2 Delay子程序:精确时序的基石
所有时序参数(tERASE,tSTEP等)都依赖于精确的延时。汇编版本的Delay子程序是一个两层嵌套循环,其延时周期数计算公式在注释中给出:Delay = (2 + (2 + 45 + 3) * A + 2 + 4) / Bus_Freq。
Delay: pshx ; 2周期 Loop: ldx #$0F ; 2周期 dbnzx * ; 3周期,循环15次 -> 45周期 dbnza Loop ; 3周期,外部循环A次 pulx ; 2周期 Delay990: rts ; 4周期计算实例:假设我们需要tERASE = 102ms,总线频率f_bus = 2.4576 MHz,周期T_bus ≈ 0.407 µs。 所需周期数N = 102ms / 0.407µs ≈ 250,614 周期。 代码中tERASE被定义为$F6(十进制246)。代入公式:周期数 = 2 + (2+45+3)*246 + 2 + 4 = 2 + 50*246 + 6 = 12308 周期。 这显然与250K周期相差甚远。这里揭示了应用笔记代码的一个潜在问题:注释中的时间(~102ms)与提供的延时参数可能不匹配,或者该延时子程序被主循环多次调用(如EraseRoutine中ldx #$14循环调用Delay)。在实际项目中,你必须根据你的时钟频率,重新计算并验证所有延时参数!一个常见的做法是编写一个简单的微秒级或毫秒级延时函数,并通过示波器或定时器中断来校准它。
7. 从汇编到C:代码移植与工程实践要点
官方文档提供了C语言版本的实现(Prog_er_srt.c),逻辑与汇编完全一致。使用C语言可以提高代码可读性和可维护性,但在对时序要求极其苛刻的底层操作中,需要特别注意编译器的优化行为。
关键实践建议:
- 使用
volatile关键字:所有对FLASH控制寄存器(FLCR1、FLCR2)和FLASH内存地址的直接操作,其指针必须声明为volatile,以防止编译器优化掉这些“看似无用”的读写操作(如对FLBPR的虚读)。#define FLCR1 (*((volatile unsigned char *)0xFE0B)) - 禁用编译器优化:在包含FLASH操作函数的文件或函数附近,考虑使用编译器指令(如
#pragma)临时关闭优化,或者确保这些函数不被内联(noinline)。 - 精确控制延时:C语言中的
Delay函数使用简单的空循环,其延时受编译器优化和时钟频率影响更大。最好使用硬件定时器来实现精确延时,或者通过反汇编确认空循环产生的机器周期数。 - 中断处理:虽然例程中使用了
SEI/CLI(C中通过asm嵌入)来开关全局中断,但在更复杂的系统中,你可能需要更精细地管理中断优先级,确保FLASH操作期间不会被高优先级中断打断,导致时序错乱。
8. 调试与故障排查实战记录
在实际硬件上调试这些代码时,你几乎一定会遇到问题。以下是我总结的常见故障场景和排查思路:
问题1:编程/擦除完全失败,FLASH内容无变化。
- 检查清单:
- 电源与电压:首先用万用表和示波器确认VDD是否为稳定的5V,Vhi是否为精确的8V?在电荷泵启动瞬间,VDD是否有大幅跌落(应小于5%)?
- 硬件连接:IRQ引脚在正常编程/擦除时是否连接到了VDD(5V)?只有操作FLBPR时才需要切换到Vhi(8V)。这个开关电路工作正常吗?
- 时序参数:你的系统总线频率是多少?代码中的
tERASE、tSTEP等延时常量是否根据你的频率重新计算并校准过?这是最常出错的地方。 - 寄存器配置:
CONFIG1寄存器是否正确配置(关闭COP)?FLCR1/FLCR2中的FDIV位是否根据你的总线频率设置正确? - 软件流程:是否严格遵循了9步(擦除)或17步(编程)序列?特别是对
FLBPR的“虚读”和向目标地址的“虚写”步骤是否执行?
问题2:编程验证偶尔失败,成功率不是100%。
- 排查方向:
- 电源噪声:在电荷泵工作时,用示波器探头(带宽足够)观察VDD和Vhi线路上的噪声。如果噪声过大,增加去耦电容(如并联一个1-10uF的钽电容),并检查电源走线是否过细、过长。
- 电磁干扰:如果系统中有敏感模拟电路,确保在FLASH操作期间暂停ADC采样。可以尝试在代码中,在FLASH操作前后加入短暂的延时,让电源稳定。
- FLASH寿命:如果芯片已经经历了接近标称的擦写次数(通常为10万次),可能会出现编程速度变慢、需要更多脉冲的情况。智能算法中的
flsPULSES(最大100次)就是为此设计的。如果频繁失败,可能是存储单元老化。
问题3:操作后芯片运行异常或复位。
- 检查要点:
- 看门狗:是否在操作前正确关闭了COP?操作完成后是否根据需要重新开启了?
- 中断:是否在关键时序段(
SEI到CLI之间)屏蔽了所有中断?是否有不可屏蔽中断(NMI)? - 代码自修改:确保你编程/擦除的FLASH区域,不是当前正在运行的程序代码所在的区域!否则处理器会取到错误指令导致崩溃。通常需要将FLASH操作代码复制到RAM中执行,或者通过Bootloader模式来更新应用程序区。
调试工具建议:
- 逻辑分析仪:连接芯片的几条关键控制线(如自定义的IO口用于标记状态),可以清晰看到
HVEN、PGM等信号的时序,与数据手册对比。 - 在线调试器:配合支持HC08的仿真器(如P&E Micro的Cyclone MAX),可以单步跟踪代码,查看寄存器状态,是排查软件逻辑问题的利器。
- 示波器:用于测量电源质量和精确的延时时间,不可或缺。
最后,务必反复阅读MC68HC908AS60的数据手册(Data Sheet)和Flash存储器章节的详细规范。应用笔记(AN1827)是一个极佳的范例,但最终权威是数据手册。每一次成功的底层FLASH操作,都是对硬件原理和软件时序掌控的一次深刻验证。