尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32串口IAP实现字库存储:Bootloader设计与Flash分区实战

STM32串口IAP实现字库存储:Bootloader设计与Flash分区实战 1. 项目概述与核心价值最近在折腾一个基于STM32F103的显示项目需要用到中文字库。一开始图省事直接把字库数组编译进代码里结果一个16x16的点阵字库就占了几百KB直接把Flash空间给撑爆了程序都烧不进去。相信不少用STM32做过显示的朋友都遇到过类似的问题。后来琢磨了一下决定换个思路把庞大的字库文件单独存放到芯片的Flash里需要显示的时候再去读取。但问题来了怎么把这个动辄几百KB甚至上MB的二进制文件弄进芯片里呢总不能每次修改字库都重新编译、下载整个程序吧太折腾了。最直接、成本最低的方案就是利用STM32F103自带的串口UART。通过串口我们可以将外部的字库文件数据像“灌水”一样一点一点地写入到芯片内部Flash的指定区域。这个方案有几个明显的优点首先它不依赖昂贵的调试器如J-Link、ST-Link只需要一个最常见的USB转串口模块比如CH340、CP2102就能搞定其次它实现了程序代码和字库数据的物理分离字库更新独立于应用程序后期维护和升级非常灵活最后整个过程完全由我们自己的Bootloader程序控制安全性和可靠性有保障。这个“通过串口将字库文件下载到Flash中”的项目本质上是一个自定义的、轻量级的IAP在应用编程过程。它不仅仅是解决字库存储的单一问题更是一种通用的、将大块外部数据如图片、音频、配置文件固化到MCU内部的经典方法。对于资源紧张的STM32F103C8T664KB Flash这类芯片来说掌握这项技能能让你在项目开发中更加游刃有余。2. 整体方案设计与核心思路拆解2.1 为什么选择串口和内部Flash在嵌入式开发中存储和加载外部数据有多种方案。比如使用外部SPI Flash或SD卡它们容量大但需要额外的硬件成本和PCB面积也增加了驱动程序的复杂性。对于字库这种“只读”且相对固定的数据如果芯片内部Flash有剩余空间那么优先使用内部Flash是最经济、最稳定的选择。STM32F103的内部Flash读写速度远快于外部存储器且无需初始化任何外设上电即可用。而通信接口的选择串口UART几乎是所有MCU的标配硬件简单协议透明在PC端有无数成熟的调试助手如SSCOM、XCOM可以发送文件。虽然速度比不上USB或CAN但对于下载字库这种“一次性”或“偶尔性”的操作其速度是完全可接受的。更重要的是利用串口可以实现一个非常简单的通信协议让MCU主动控制数据的接收和写入过程避免因PC端软件的不当操作导致数据错误。2.2 方案架构与工作流程整个方案可以清晰地分为两个部分运行在PC端的上位机发送端和运行在STM32中的Bootloader接收与编程端。上位机PC端职责打开指定的字库文件通常是.bin或.dat格式的二进制文件。通过串口与STM32建立连接。按照自定义的通信协议将文件分块、打包并附加校验信息如CRC16发送出去。接收STM32的反馈根据反馈决定是重发当前数据包还是发送下一个。BootloaderSTM32端职责上电后首先检查是否有“下载字库”的触发信号如检测某个按键是否按下或收到特定的串口指令。如果没有触发则直接跳转到主应用程序App运行。如果触发则进入“字库下载模式”。初始化串口等待接收上位机的指令和数据包。对收到的数据包进行校验CRC校验校验通过后将数据写入Flash的预定地址。每写完一包向上位机回复“确认ACK”信号请求下一包。所有数据包接收并写入完毕后进行整体校验如校验和成功后回复“完成”信号然后复位或跳转到App。关键设计点Flash地址规划这是整个方案的基础必须在设计之初就确定好并在Bootloader和App中保持一致。我们需要在芯片的Flash内存地图中划出一块专属区域给字库。 以STM32F103C8T664KB Flash地址0x0800 0000 - 0x0800 FFFF为例一种常见的划分方式如下Bootloader区(0x0800 0000 - 0x0800 1FFF)占用8KB存放负责下载的引导程序。应用程序(App)区(0x0800 2000 - 0x0800 BFFF)占用40KB存放用户的主程序。字库存储区(0x0800 C000 - 0x0800 FFFF)占用16KB专门用于存放字库数据。注意Flash的写入操作必须以“页”为单位进行擦除。STM32F103的Flash页大小一般为1KB或2KB具体需查数据手册。在规划地址时字库区的起始地址必须对齐到页的起始地址。例如如果页大小是1KB那么字库起始地址最好是0x0800 C000这样的整数倍地址。2.3 通信协议设计简单可靠是关键为了保证数据传输的可靠性我们需要设计一个简单的应用层协议。这个协议不需要像Modbus那样复杂但必须包含帧头、数据长度、数据内容、校验和以及帧尾。一个典型的自定义协议帧格式可以如下所示[帧头1] [帧头2] [包序号] [数据长度L] [数据N字节] [CRC16高字节] [CRC16低字节] [帧尾]帧头2字节如0xAA, 0x55用于标识一个数据包的开始帮助接收方在数据流中同步。包序号1字节从0开始递增用于标识当前是第几个包。当发生丢包或错包时上位机可以根据STM32返回的期望包序号进行重发。数据长度L1字节指示本帧中“数据”字段的实际字节数。为了适应串口缓冲区和Flash编程速度这个值不宜过大通常设置为128或256字节。数据N字节实际要写入Flash的字库数据片段。CRC162字节对整个数据包从帧头到数据结束计算得到的循环冗余校验码用于验证数据在传输过程中是否出错。帧尾1字节如0x0D, 0x0A回车换行标识帧结束。上位机按此格式组包发送STM32端按此格式解析、校验。只有CRC校验通过的包才会被写入Flash并回复ACK包含下一个期望的包序号。如果校验失败则回复NAK包含当前期望的包序号请求上位机重发。3. Bootloader程序的关键实现细节3.1 启动流程与模式判断Bootloader的main函数入口处首先要完成最基本的系统初始化时钟、GPIO、串口。之后核心逻辑就是判断启动模式。int main(void) { // 1. 系统初始化 SystemInit(); GPIO_Init(); USART_Init(115200); // 初始化串口波特率115200 // 2. 模式判断 if (Check_Update_Trigger()) { // 进入字库下载模式 Run_Font_Update_Mode(); } else { // 跳转到主应用程序 Jump_To_Application(); } while (1); }Check_Update_Trigger()函数是实现模式切换的关键。常见的设计有以下几种你可以根据硬件条件选择或组合使用按键检测上电时检测某个GPIO连接按键的电平。如果按键按下则进入下载模式。#define UPDATE_KEY_PIN GPIO_Pin_0 #define UPDATE_KEY_PORT GPIOA #define UPDATE_KEY_PRESSED (GPIO_ReadInputDataBit(UPDATE_KEY_PORT, UPDATE_KEY_PIN) 0) uint8_t Check_Update_Trigger(void) { // 延时消抖 Delay_ms(20); if (UPDATE_KEY_PRESSED) { // 按键仍处于按下状态确认为触发 return 1; } return 0; }串口指令触发上电后Bootloader在短时间内如3秒等待串口发送特定的指令如字符串“UPDATE”。如果收到则进入下载模式超时则跳转App。Flash标志位在Flash的某个固定地址如App区的末尾写入一个特殊的标志如0x5A5A5A5A。Bootloader检查该标志如果存在则进入下载模式并在完成后清除该标志。这种方式可以由App软件触发更新。3.2 Flash驱动解锁、擦除与写入操作内部Flash是Bootloader的核心功能。STM32的Flash在写入前必须先解锁然后以“页”为单位擦除最后才能按“半字”16位或“字”32位写入。1. 解锁FlashSTM32的Flash默认是锁定的以防止误操作。解锁需要向特定的寄存器序列写入密钥。void FLASH_Unlock(void) { FLASH-KEYR FLASH_KEY1; // 写入第一个密钥 FLASH-KEYR FLASH_KEY2; // 写入第二个密钥 } // 密钥值在标准外设库或HAL库的头文件中已定义2. 擦除Flash页在写入新数据前必须确保目标页是已擦除状态全为0xFF。擦除操作会将整页Flash恢复为0xFF。FLASH_Status FLASH_ErasePage(uint32_t Page_Address) { FLASH_Status status FLASH_COMPLETE; status FLASH_ErasePage(Page_Address); // 调用库函数 while (FLASH_GetStatus() ! FLASH_COMPLETE); // 等待擦除完成 return status; }重要提示擦除操作是不可逆的且耗时较长约几十ms。在执行擦除和写入操作时必须禁止所有中断因为Flash编程期间CPU会暂停执行指令。可以使用__disable_irq()和__enable_irq()函数。3. 写入数据到FlashSTM32F103的Flash编程宽度是16位半字。即使你想写入一个8位字节也需要以16位为单位操作。FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data) { FLASH_Status status FLASH_COMPLETE; status FLASH_ProgramHalfWord(Address, Data); // 调用库函数 while (FLASH_GetStatus() ! FLASH_COMPLETE); // 等待写入完成 return status; }在实际写入字库数据时我们通常以字节流的形式接收但需要将其转换为uint16_t数组然后每次写入一个半字。void Write_Data_To_Flash(uint32_t start_addr, uint8_t *data, uint32_t len) { uint32_t i; uint32_t flash_addr start_addr; uint16_t half_word; __disable_irq(); // 关闭中断 FLASH_Unlock(); // 解锁Flash // 确保起始地址是半字对齐的 if(start_addr % 2 ! 0) { // 处理非对齐情况这里简单返回错误或进行填充 return; } for(i 0; i len; i 2) { // 组合两个字节为一个半字 half_word (uint16_t)(data[i1] 8) | data[i]; FLASH_ProgramHalfWord(flash_addr, half_word); flash_addr 2; // 地址增加2个字节 // 可选验证写入的数据 // if(*(volatile uint16_t*)(flash_addr-2) ! half_word) { /* 错误处理 */ } } FLASH_Lock(); // 重新锁定Flash __enable_irq(); // 打开中断 }3.3 串口通信与协议解析Bootloader需要可靠地接收数据包。这里强烈建议使用串口空闲中断IDLE结合DMA的方式或者至少使用串口接收中断环形缓冲区。绝对避免在main函数中用while循环查询接收那会严重阻塞程序导致无法及时处理Flash写入和应答。使用接收中断环形缓冲区的示例#define RX_BUF_SIZE 512 uint8_t uart_rx_buf[RX_BUF_SIZE]; uint16_t uart_rx_index 0; void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { // 读取一个字节到缓冲区 uart_rx_buf[uart_rx_index] USART_ReceiveData(USART1); uart_rx_index (uart_rx_index 1) % RX_BUF_SIZE; // 环形索引 // 可以在这里简单判断是否收到帧尾触发协议解析 } }在主循环中程序不断检查缓冲区调用协议解析函数Parse_UART_Frame()。该函数负责寻找帧头、提取包序号、数据长度、计算CRC并进行校验。校验成功后将数据部分拷贝出来准备写入Flash。3.4 应用程序跳转当不进入下载模式或者下载完成后Bootloader需要跳转到主应用程序。跳转的本质是将PC指针程序计数器设置为应用程序的起始地址。typedef void (*pFunction)(void); // 定义函数指针类型 void Jump_To_Application(void) { uint32_t JumpAddress; pFunction Jump_To_App; // 1. 检查应用程序起始地址栈顶是否有效 // 应用程序的栈顶指针存放在其向量表的第一个位置 if (((*(__IO uint32_t*)APPLICATION_ADDRESS) 0x2FFE0000) 0x20000000) { // 2. 关闭所有外设中断避免在跳转后产生意外中断 // ... 关闭SysTick、所有使能的外设中断等 // 3. 设置主堆栈指针MSP为应用程序的栈顶 __set_MSP(*(__IO uint32_t*)APPLICATION_ADDRESS); // 4. 获取应用程序的复位向量地址向量表第二个位置 JumpAddress *(__IO uint32_t*)(APPLICATION_ADDRESS 4); Jump_To_App (pFunction)JumpAddress; // 5. 跳转 Jump_To_App(); } else { // 应用程序地址无效可以在此处点亮一个错误LED或循环 Error_Handler(); } }注意APPLICATION_ADDRESS必须与你在链接脚本.ld文件或分散加载文件中为App设置的起始地址完全一致。在跳转前务必清理现场关闭已开启的外设尤其是定时器、DMA、禁用全局中断。App程序的中断向量表需要做相应的偏移设置通过修改VTOR寄存器或IDE中的配置。4. 上位机软件的实现要点上位机软件可以使用任何你熟悉的语言开发如C#、Python、Qt等。其核心逻辑是文件读取、协议打包和串口通信。基本流程如下打开文件以二进制模式打开字库文件。打开串口配置正确的端口号、波特率需与Bootloader一致如115200、数据位、停止位、校验位。发送启动指令可选。发送一个特定指令如字符‘U’通知Bootloader开始传输。循环读取并发送数据包从文件中读取固定长度如256字节的数据。按照协议格式组包添加帧头、包序号、长度、计算CRC、添加帧尾。通过串口发送整个数据包。等待Bootloader的应答ACK。如果收到ACK且包序号正确则继续发送下一包。如果收到NAK或超时未收到应答则重发当前包可设置最大重试次数如3次。发送结束包与校验文件发送完毕后可以发送一个特殊的“结束包”通知Bootloader传输完成。Bootloader可以对整个字库区域进行校验和计算并与上位机计算的校验和比对确保写入无误。Python示例代码片段使用pyserial和binasciiimport serial import struct import time from crcmod import crcmod # 配置串口 ser serial.Serial(COM3, 115200, timeout1) # 打开字库文件 with open(font_lib.bin, rb) as f: font_data f.read() packet_size 256 total_packets len(font_data) // packet_size (1 if len(font_data) % packet_size else 0) crc16_func crcmod.mkCrcFun(0x18005, revTrue, initCrc0xFFFF, xorOut0x0000) for seq in range(total_packets): start seq * packet_size end start packet_size data_chunk font_data[start:end] actual_len len(data_chunk) # 组包帧头(0xAA55) 包序号 数据长度 数据 CRC16 header b\xAA\x55 seq_byte struct.pack(B, seq 0xFF) len_byte struct.pack(B, actual_len) # 计算CRC (对 headerseqlendata 计算) crc_data header seq_byte len_byte data_chunk crc16 crc16_func(crc_data) crc_bytes struct.pack(H, crc16) # 大端字节序 packet header seq_byte len_byte data_chunk crc_bytes retry 0 while retry 3: ser.write(packet) # 等待ACK ack ser.read(1) if ack b\x06: # ACK字符 expected_seq ser.read(1) if expected_seq[0] ((seq 1) 0xFF): print(fPacket {seq} sent successfully.) break retry 1 print(fPacket {seq} retry {retry}...) time.sleep(0.1) else: print(fFailed to send packet {seq} after 3 retries.) break ser.close() print(Download finished.)5. 主应用程序如何读取字库字库成功写入Flash后主应用程序需要能够读取它。由于字库数据是只读的我们可以直接通过指针访问。首先你需要在App的代码中明确知道字库存放的起始地址FONT_START_ADDR这个地址必须与Bootloader中使用的地址完全一致。对于点阵字库如16x16读取一个汉字字模的示例一个16x16的汉字点阵需要32字节16行 * 16列 / 8位每字节。字库文件通常是按汉字机内码如GB2312顺序排列的。// 假设字库起始地址 #define FONT_START_ADDR 0x0800C000 // 每个字模占用的字节数 #define FONT_16x16_SIZE 32 // 根据汉字GB2312码计算其在字库中的偏移量并读取 // 区码qh, 位码wh (GB2312编码) uint32_t GetFontOffset_GB2312(uint8_t qh, uint8_t wh) { // GB2312 94个区每区94位 // 实际字库可能从第16区开始存放汉字 uint16_t qu_wei (qh - 0xA1) * 94 (wh - 0xA1); return (uint32_t)(qu_wei * FONT_16x16_SIZE); } const uint8_t* GetFontData_16x16(uint8_t qh, uint8_t wh) { uint32_t offset GetFontOffset_GB2312(qh, wh); // 直接返回Flash中对应地址的指针 return (const uint8_t*)(FONT_START_ADDR offset); } // 在显示函数中使用 void Display_Chinese_Char(uint16_t x, uint16_t y, uint8_t qh, uint8_t wh) { const uint8_t *font_ptr GetFontData_16x16(qh, wh); for(int i0; i32; i) { uint8_t font_byte font_ptr[i]; // 直接从Flash读取 // ... 将font_byte的每一位绘制到屏幕的对应像素上 ... } }关键点font_ptr是一个指向常量数据的指针。编译器会将其视为指向Flash的指针从而生成正确的读取指令LDR。你不需要调用任何特殊的读函数。6. 开发、调试与烧录全流程实操6.1 环境准备与工程设置硬件准备STM32F103开发板如最小系统板。USB转串口模块CH340/CP2102/FT232等将其TX连接到MCU的RXPA10RX连接到MCU的TXPA9GND共地。用于触发下载模式的按键可选。软件准备IDEKeil MDK-ARM或STM32CubeIDE。串口调试助手SSCOM、XCOM或Putty。字库文件制作工具可由汉字点阵提取工具生成保存为二进制.bin格式。工程设置Keil为例Bootloader工程设置Flash起始地址IROM1为0x08000000大小根据规划设定如8KB。在代码中定义应用程序跳转地址APPLICATION_ADDRESS如0x08002000。应用程序App工程设置Flash起始地址为0x08002000。需要设置中断向量表偏移。在system_stm32f1xx.c中修改VECT_TAB_OFFSET宏定义为0x2000。或者在main函数最开始调用SCB-VTOR FLASH_BASE | 0x2000;。6.2 分步烧录与联合调试这是一个容易出错的环节务必按顺序操作第一步烧录Bootloader使用ST-Link或J-Link通过SWD接口将编译好的Bootloader程序烧录到0x08000000起始的地址。烧录后复位此时如果触发条件不满足如按键未按下程序会立刻跳转但由于App区域是空的全0xFF或随机值可能会导致硬件错误而复位。这是正常现象。第二步烧录应用程序烧录你的主应用程序到0x08002000地址。现在上电后如果不触发下载Bootloader就能成功跳转到App运行了。第三步测试字库下载功能将Bootloader的触发条件设置为“上电后一段时间内检测串口指令”。给开发板上电立即通过串口助手发送启动指令如“UPDATE”。运行上位机软件选择正确的串口和字库文件开始下载。观察上位机日志和开发板LED如果有状态指示的反馈。6.3 调试技巧与问题定位Bootloader无法跳转App检查栈顶值在Bootloader跳转前打印或通过调试器查看APPLICATION_ADDRESS处的值。一个有效的栈顶地址应该在RAM范围内如0x2000xxxx。检查VTOR确保App程序中正确设置了中断向量表偏移。可以在App开头打印SCB-VTOR的值确认。单步调试在跳转语句Jump_To_App();前设置断点单步执行观察是否成功跳转。跳转后观察PC指针是否指向App的Reset_Handler。串口通信乱码或丢包确认波特率Bootloader和上位机软件的波特率必须绝对一致包括数据位、停止位、校验位。检查电平确保USB转串口模块是3.3V电平与STM32匹配。优化接收缓冲如果丢包严重尝试减小数据包长度如从256改为128或降低波特率如从115200改为57600。添加流量控制在协议中实现“停止等待”ARQ确保每包确认后再发下一包。字库写入后读取错误地址对齐确保写入的起始地址是半字2字节对齐的。Write_Data_To_Flash函数中已做处理。校验机制Bootloader在写入每页后可以立即读回校验。上位机在发送完所有数据后可以发送一个“校验命令”让Bootloader计算整个字库区的CRC并返回与上位机本地计算的CRC比对。指针类型在App中读取时确保使用const uint8_t*或const uint16_t*类型的指针并正确计算偏移量。7. 常见问题与深度避坑指南在实际操作中我踩过不少坑这里总结几个最典型的1. 中断向量表偏移的坑这是导致跳转后程序跑飞的最常见原因。Bootloader占用了前一部分FlashApp的中断向量表必须相应后移。在Keil中除了修改VECT_TAB_OFFSET还需要在项目的“Options for Target” - “Linker”中勾选“Use Memory Layout from Target Dialog”并确保“IROM1”的起始地址设置正确。在CubeIDE中需要修改链接脚本.ld文件。找到MEMORY部分将FLASH区域的起始地址改为0x08002000长度相应减少。同时在代码中main函数开头调用SCB-VTOR 0x08002000;。验证方法在App中如果中断如SysTick定时器中断能正常工作基本说明向量表偏移设置正确。2. Flash操作期间的死锁在擦除或写入Flash时如果发生中断可能导致操作失败甚至芯片死锁。务必关闭总中断在FLASH_Unlock()之前调用__disable_irq()在FLASH_Lock()之后调用__enable_irq()。注意SysTickSysTick中断可能由HAL_Delay()等函数触发。在Flash操作期间避免使用这类延时函数改用简单的循环延时。3. 字库文件与地址的匹配文件大小确保字库二进制文件的大小不超过你规划的Flash字库区大小。最好在Bootloader开头进行大小检查。起始地址Bootloader、App和上位机软件中使用的字库起始地址FONT_START_ADDR必须是同一个值。建议在公共的头文件中统一定义。4. 电源稳定性Flash编程对电源电压非常敏感。在使用USB供电特别是从电脑USB口取电进行下载时如果线材不好或电脑USB口供电不足可能导致Flash写入错误进而使整个芯片无法启动。对策使用外部稳定的3.3V电源为开发板供电。确保在下载过程中没有大的电流波动。5. Bootloader本身的更新一旦Bootloader固化如何更新它自己这需要更高级的设计例如“Bootloader跳转到Bootloader”的两阶段更新或者通过App来更新Bootloader风险较高。对于大多数项目Bootloader功能稳定后无需更新所以初次设计时务必考虑周全预留测试接口。这个项目虽然不复杂但涵盖了嵌入式开发中几个非常核心的概念内存映射、固件分区、IAP、通信协议、Flash操作。把它吃透不仅能解决字库存储的问题更能为你以后实现OTA升级、参数存储等高级功能打下坚实的基础。在实际操作时耐心调试每一步尤其是通信协议和地址映射这两点通了整个流程就顺了。
返回列表