1. C16x/ST10系统内FLASH编程技术解析
在嵌入式系统开发中,FLASH编程是一项基础但至关重要的技术。它允许我们在设备运行时更新固件,而无需拆卸芯片或使用专用编程器。对于汽车电子、工业控制等需要长期稳定运行的场景,这种在线编程能力直接决定了系统的可维护性和生命周期成本。
传统FLASH编程面临一个核心矛盾:当我们在编程FLASH时,CPU不能从正在被修改的FLASH区域取指令。对于使用单一FLASH存储器的系统(大多数低成本嵌入式设备的典型配置),这就意味着编程代码必须被复制到RAM中执行。过去开发者需要手动管理这个过程——编写独立的编程程序、计算代码长度、处理地址重定位,这些工作既繁琐又容易出错。
Keil C166开发工具从4.02版本开始,通过L166链接器的SROM扩展功能,提供了一套优雅的解决方案。它允许代码存储在FLASH中,但指定不同的执行地址(通常在RAM),并自动生成代码段的位置和长度信息。这种"存储地址与执行地址分离"的技术,将开发者从底层细节中解放出来,使系统内FLASH编程变得简单可靠。
2. 技术实现原理与工具链支持
2.1 存储与执行地址分离机制
L166链接器的核心创新在于扩展了SECTIONS指令语法,新增了执行地址(exec_address)和存储地址(store_address)的分离定义能力。其完整语法格式为:
SECTIONS (sectionname%classname (exec_address) [store_address])其中方括号[]标志着这是一个SROM段,具有以下特性:
exec_address定义代码实际运行时的物理地址(RAM区域)store_address定义代码在FLASH中的存储位置(可选)- 当store_address以"!"前缀时,链接器不会在exec_address保留内存空间
- 未指定store_address时,代码将被放置在SROM内存类定义的区域
这种设计带来了三个关键优势:
- 位置无关代码:编程代码可以在任何可用RAM区域运行,不受原始存储位置影响
- 内存利用率优化:通过"!"前缀可临时借用系统栈等区域,减少专用RAM占用
- 链接时地址确定:所有地址解析在链接阶段完成,避免运行时计算开销
2.2 编译器与头文件支持
配套的srom.h头文件提供了一组精妙的宏定义,用于简化代码搬运过程:
#define SROM_PS(n) \ extern unsigned char huge _PR_##n##_s_; /* 存储起始地址 */ \ extern unsigned char huge _PR_##n##_l_; /* 段长度 */ \ extern unsigned char huge _PR_##n##_t_; /* 目标执行地址 */ #define SROM_PS_SRC(n) ((void *)&_PR_##n##_s_) #define SROM_PS_TRG(n) ((void *)&_PR_##n##_t_) #define SROM_PS_LEN(n) ((unsigned int)&_PR_##n##_l_)这些宏的工作原理值得深入理解:
- 链接器会根据SECTIONS指令自动生成
_PR_[section]_[s/l/t]符号 - 取这些符号的地址实际上获取的是其对应的值(链接器的巧妙设计)
huge指针类型确保可以访问整个16MB C16x地址空间- 宏展开后的类型转换保证了C语言类型安全
3. 完整实现流程与实操示例
3.1 工程配置与链接器设置
在μVision开发环境中,需要特别关注L166链接器的配置。假设我们有一个FLASH编程代码段?PR?PFLASH,需要将其配置为:
SECTIONS (?PR?PFLASH%FLASH_CODE (0xFA00) [])这表示:
- 代码段存储在FLASH中(由SROM类决定具体位置)
- 运行时需要被复制到RAM的0xFA00地址执行
- FLASH_CODE类通常定义为可执行代码段
对应的内存布局配置可能如下:
ROM: 0x0000-0x7FFF // 主FLASH存储区 RAM: 0xF600-0xFDFF // 系统RAM区 SROM: 0x8000-0xFBFF // 代码存储区(可选)3.2 编程代码实现
一个完整的FLASH编程流程通常包含以下步骤,下面是经过优化的实现代码:
#include <string.h> #include <intrins.h> #include <srom.h> #include <reg167.h> // 声明SROM段信息(对应PFLASH.A66中的代码) SROM_PS(PFLASH); // FLASH操作函数声明 extern int far PFlash_Write(void huge *target_adr, void huge *buffer); extern int far PFlash_Erase(void huge *sector_adr); extern void far PFlash_Reset(void); // 主编程流程 void flash_programming(void) { // 1. 复制编程代码到RAM hmemcpy(SROM_PS_TRG(PFLASH), SROM_PS_SRC(PFLASH), SROM_PS_LEN(PFLASH)); // 2. 准备编程环境 _bfld_(PSW, 0xF000, 0xF000); // 禁用中断 _nop_(); _nop_(); // 等待流水线清空 // 3. 执行FLASH擦除 if(PFlash_Erase(0x4000) != 0) { // 错误处理 PFlash_Reset(); return; } // 4. 准备编程数据 unsigned char buf[64] = { /* 数据内容 */ }; // 5. 执行FLASH写入 if(PFlash_Write(0x4000, buf) != 0) { PFlash_Reset(); return; } // 6. 恢复系统状态 PFlash_Reset(); _bfld_(PSW, 0xF000, 0x0000); // 启用中断 }3.3 关键操作细节解析
代码复制过程:
hmemcpy函数用于大内存块拷贝(超过64KB)- 源地址来自
SROM_PS_SRC宏,指向FLASH中的代码存储位置 - 目标地址来自
SROM_PS_TRG宏,即链接时指定的RAM执行地址 - 拷贝长度由
SROM_PS_LEN自动计算,确保完整复制
中断处理:
- 编程前必须禁用中断,防止打断关键时序
- 使用
_bfld_内联函数直接操作PSW寄存器效率最高 _nop_()指令提供必要的延迟,确保状态稳定
错误处理:
- 每次FLASH操作后应检查返回值
- 出现错误时立即调用
PFlash_Reset恢复FLASH状态 - 在实际产品代码中,还应记录错误日志或触发恢复机制
4. 高级应用技巧与优化建议
4.1 内存布局优化策略
对于资源受限的系统,可以采用以下优化方案:
临时内存借用:
SECTIONS (?PR?PFLASH%FLASH_CODE (0xFA00) [!0x1000])"!"前缀允许编程代码临时覆盖系统栈区域,节省专用RAM空间。但需确保:
- 编程期间不发生中断或函数调用
- 编程完成后立即恢复栈指针
- 总操作时间控制在毫秒级以内
分段编程技术: 将大型FLASH分区处理,每次只复制必要的编程代码:
SECTIONS (?PR?PFLASH_ERASE%FLASH_CODE (0xFA00) []) SECTIONS (?PR?PFLASH_WRITE%FLASH_CODE (0xFC00) [])这样可以将RAM需求从2KB降低到1KB以下。
4.2 安全编程实践
校验机制:
void verify_flash(void *addr, void *buf, int len) { unsigned char *p = addr; unsigned char *q = buf; while(len--) { if(*p++ != *q++) { // 校验失败处理 } } }编程后必须进行数据校验,特别是关键固件区域。
看门狗管理:
// 编程前 WDT_CON = 0x1F; // 设置最大超时时间 // 编程循环中 _wdtl_(); // 喂狗长时间编程操作需要妥善处理看门狗,防止意外复位。
电源监测:
if(POWER_STAT & 0x80) { // 电压不足,中止编程 }FLASH编程对电源稳定性要求高,需实时监测电压状态。
5. 典型问题排查与解决方案
5.1 常见错误代码分析
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| 编程后系统崩溃 | 中断过早恢复 | 确保所有编程操作完成后再启用中断 |
| 校验失败 | FLASH未正确擦除 | 检查擦除操作返回值,必要时重复擦除 |
| 写入卡死 | 时序不符合要求 | 检查CPU时钟配置,增加关键操作后的延迟 |
| 随机数据错误 | 电源干扰 | 增加电源去耦电容,编程期间关闭外围设备 |
5.2 调试技巧
存储地址验证:
printf("Store: %p, Exec: %p, Len: %d\n", SROM_PS_SRC(PFLASH), SROM_PS_TRG(PFLASH), SROM_PS_LEN(PFLASH));在开发阶段打印关键地址信息,确保链接器配置正确。
反汇编检查:
oh166 -f flash.hex > flash.lst使用OH166反汇编工具验证代码是否被正确放置在指定地址。
RAM内容dump:
void dump_ram(void *addr, int len) { unsigned char *p = addr; while(len--) { printf("%02x ", *p++); } }编程前dump RAM目标区域,确认内存拷贝是否成功。
6. 工程实践中的经验总结
在实际项目中应用这套技术时,有几个关键点需要特别注意:
代码大小优化: FLASH编程代码应该尽量精简,减少需要复制到RAM的代码量。建议:
- 使用-O2或-O3优化级别编译
- 避免在编程函数中使用大型库函数
- 将非关键代码分离到其他段
多芯片兼容性: 不同型号的C16x/ST10芯片可能有特殊的FLASH编程要求:
#if defined(__C167CR__) #define FLASH_TIMEOUT 1000 #elif defined(__ST10F269) #define FLASH_TIMEOUT 500 #endif使用条件编译处理芯片差异。
批量编程优化: 当需要编程多个区域时,应该:
void program_multiple(void) { copy_to_ram(); // 只需复制一次 for(int i=0; i<COUNT; i++) { program_block(addr[i], data[i]); } }避免重复复制编程代码,提高效率。
这套SROM技术在Keil工具链中的实现,代表了嵌入式FLASH编程方案的一个典范。它通过工具链的深度整合,将原本复杂的技术细节封装成简单的开发接口,让开发者可以专注于业务逻辑而非底层机制。对于需要可靠在线更新的嵌入式产品,这种方案能显著降低开发难度,提高系统可靠性。