OV-Watch(一)(IAP_F411学习)
Ymodem文件夹中的flash_if.c
一、文件整体作用
类比:把 Flash 想象成一本可以反复擦写的笔记本,flash_if.c
就是 “笔记本操作手册”,规定了如何:
- 擦除整页或指定页内容(擦除函数)
- 往指定位置写入数据(写入函数)
- 检查或解除写保护(避免误操作)
二、核心函数与流程
1. 初始化 Flash(解锁并清状态)
void FLASH_If_Init(void)
{HAL_FLASH_Unlock(); // 解锁 Flash,允许写入/擦除操作// 清除之前操作的错误标志(比如上次擦除失败的标记)__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | ...);
}
- 为什么要解锁?
Flash 为防止误操作,默认处于 “锁定” 状态,任何写入 / 擦除前必须先解锁(类似银行卡输入密码才能操作)。 - 清状态的作用
清除之前操作的完成标志或错误标志,确保本次操作从 “干净” 的状态开始。
2. 擦除用户 Flash 区域(核心功能)
2.1 擦除整个用户区域(FLASH_If_Erase
)
uint32_t FLASH_If_Erase(uint32_t StartSector)
{uint32_t UserStartSector = GetSector(APPLICATION_ADDRESS); // 获取用户区域起始扇区FLASH_EraseInitTypeDef pEraseInit;// 配置擦除参数:按扇区擦除,指定起始扇区和扇区数量pEraseInit.TypeErase = TYPEERASE_SECTORS;pEraseInit.Sector = UserStartSector;// 计算需要擦除的扇区总数:从用户起始扇区到结束扇区pEraseInit.NbSectors = GetSector(USER_FLASH_END_ADDRESS) - UserStartSector + 1;pEraseInit.VoltageRange = VOLTAGE_RANGE_3; // 适配 3.3V 电压// 调用 HAL 库擦除函数,传入参数和错误记录变量if (HAL_FLASHEx_Erase(&pEraseInit, &SectorError) != HAL_OK) {return 1; // 擦除失败}return 0; // 擦除成功
}
- 关键逻辑
- 通过
GetSector
函数确定用户区域对应的扇区(比如用户程序从0x0800C000
开始,属于扇区 3)。 - 擦除必须按 “扇区” 操作(不能擦除半个扇区),所以需要计算起始扇区到结束扇区的总数量。
HAL_FLASHEx_Erase
会自动逐个擦除每个扇区,擦除后数据全为0xFF
(类似把笔记本整页涂黑)。
- 通过
2.2 擦除单个扇区(FLASH_If_Erase_One_Sector
)
uint32_t FLASH_If_Erase_One_Sector(uint32_t StartSector)
{FLASH_EraseInitTypeDef pEraseInit;pEraseInit.NbSectors = 1; // 只擦除 1 个扇区,其他参数类似全擦除// 调用 HAL 库函数,流程同上
}
3. 往 Flash 写入数据(逐字写入并验证)
uint32_t FLASH_If_Write(__IO uint32_t* FlashAddress, uint32_t* Data, uint32_t DataLength)
{for (uint32_t i = 0; i < DataLength; i++) {// 写入一个 32 位字(4 字节)到 Flash 地址FLASH_ProgramWord(*FlashAddress, *(Data + i));// 验证写入是否成功:读取 Flash 内容,与写入数据对比if (*(uint32_t*)*FlashAddress != *(Data + i)) {return 2; // 验证失败}FlashAddress += 4; // 地址后移 4 字节,准备写下一个字}return 0; // 写入并验证成功
}
- 关键逻辑
- Flash 写入必须以 32 位字(4 字节) 为单位,且只能从
0xFF
写成非0xFF
(不能直接覆盖,必须先擦除)。 - 每写入一个字后立即验证,确保数据正确(类似写完作业检查一遍)。
- 地址必须按 4 字节对齐,否则会导致错误。
- Flash 写入必须以 32 位字(4 字节) 为单位,且只能从
4. 获取写保护状态 & 禁用写保护
4.1 检查用户区域是否被写保护(FLASH_If_GetWriteProtectionStatus
)
uint16_t FLASH_If_GetWriteProtectionStatus(void)
{uint32_t UserStartSector = GetSector(APPLICATION_ADDRESS);// 读取芯片的选项字节(存储保护配置)if ((*(uint16_t*)(OPTCR_BYTE2_ADDRESS) >> (UserStartSector/8)) == (0xFFF >> (UserStartSector/8))) {return 1; // 无写保护(可以擦除/写入)} else {return 0; // 有写保护(需要先解除)}
}
- 为什么需要写保护?
防止误操作破坏重要数据(比如 Bootloader 区域被写入乱码导致无法启动)。
4.2 禁用写保护(FLASH_If_DisableWriteProtection
)
uint32_t FLASH_If_DisableWriteProtection(void)
{HAL_FLASH_Unlock(); // 解锁 Flash// 配置要解除保护的扇区(用户区域对应的扇区)FLASH_OB_DisableWRP(UserWrpSectors, FLASH_BANK_1); // 启动解除保护流程if (HAL_FLASH_OB_Launch() != HAL_OK) {return 2; // 解除失败}return 1; // 解除成功
}
- 操作步骤
先解锁,再通过芯片的选项字节配置解除保护,最后启动生效。
5. 辅助函数:根据地址获取扇区(GetSector
)
static uint32_t GetSector(uint32_t Address)
{// 根据地址范围判断属于哪个扇区(STM32 F411 的扇区划分)if (Address >= ADDR_FLASH_SECTOR_0 && Address < ADDR_FLASH_SECTOR_1) {return FLASH_SECTOR_0;} else if (...) { /* 其他扇区判断 */ }else {return FLASH_SECTOR_7; // 默认最后一个扇区}
}
- 作用
Flash 操作必须基于扇区,这个函数相当于 “地址翻译器”,把具体地址转换为对应的扇区编号(比如0x0800C000
属于扇区 3)。
三、完整操作流程(以固件升级为例)
- 初始化:调用
FLASH_If_Init
解锁 Flash 并清状态。 - 检查写保护:如果用户区域被保护,先调用
FLASH_If_DisableWriteProtection
解除。 - 擦除旧固件:调用
FLASH_If_Erase
擦除整个用户区域(为写入新固件腾空间)。 - 写入新固件:通过 Ymodem 协议接收数据,每收到一包数据,调用
FLASH_If_Write
写入 Flash(按字写入并验证)。 - 错误处理:任何步骤失败(如擦除超时、写入验证不通过),返回错误码,终止升级。
四、关键注意事项
-
Flash 特性限制
- 写入前必须擦除(擦除后全为
0xFF
,写入是将0xFF
改为具体数据)。 - 擦除以扇区为单位(如扇区 0 是 16KB,扇区 4 是 64KB,具体看芯片手册)。
- 写入必须 32 位对齐,且不能超过用户区域地址(
USER_FLASH_END_ADDRESS
)。
- 写入前必须擦除(擦除后全为
-
与 Ymodem 协议的配合
- 在
Ymodem_Receive
函数中,收到文件后先擦除用户区域(FLASH_If_Erase
),再逐包写入(调用FLASH_If_Write
)。 - 写入地址由
APPLICATION_ADDRESS
开始(如0x0800C000
),确保不覆盖 Bootloader 区域(前 48KB 通常留给 Bootloader)。
- 在
-
错误处理的重要性
- 每次 Flash 操作后必须检查状态(如擦除是否成功,写入是否验证通过),否则可能导致固件损坏,设备无法启动。
总结
flash_if.c
是 Flash 操作的 “底层工具箱”,为 Ymodem 传输提供了可靠的硬件操作支持。核心逻辑围绕 “擦除 - 写入 - 验证” 三大步骤,确保固件升级过程中数据的正确性和安全性。理解这些函数的作用,就能明白 IAP 如何通过串口等方式安全地更新设备固件,就像给手机 “无线升级系统” 一样,每一步都需要严格的擦写和验证。
第一段代码:禁用 Flash 写保护(FLASH_OB_DisableWRP
)
功能
关闭指定 Flash 存储区的写保护,允许后续对这些区域进行擦除或编程操作。
(写保护是一种安全机制,防止误操作修改 Flash 数据,类似 U 盘的写保护开关)
核心逻辑拆解
-
参数检查
assert_param(IS_OB_WRP_SECTOR(WRPSector)); // 检查要禁用保护的扇区是否合法 assert_param(IS_FLASH_BANK(Banks)); // 检查目标存储区(Bank1/Bank2)是否合法
- 确保输入的扇区编号和存储区(如 Bank1)是芯片支持的有效值,避免无效操作。
-
等待上次操作完成
status = FLASH_WaitForLastOperation(50000); // 等待最长50ms
- Flash 操作(如擦除、编程)需要时间,必须等上一次操作完成才能开始新操作,否则会出错。
50000
是超时时间(单位:微秒),防止代码卡死在等待中。
-
修改选项字节(Option Bytes)
*(__IO uint16_t*)OPTCR_BYTE2_ADDRESS |= (uint16_t)WRPSector;
- 硬件原理:STM32 的 Flash 写保护通过 “选项字节” 配置,这是一组特殊的寄存器,存储在 Flash 的特定区域。
- 代码操作:
OPTCR_BYTE2_ADDRESS
是选项字节中控制写保护的寄存器地址。(__IO uint16_t*)
将地址转换为 16 位指针(因为该寄存器是 16 位),__IO
(即volatile
)确保直接操作硬件,禁止编译器优化。|=
按位或操作,将WRPSector
对应的写保护位设为允许写入(具体逻辑取决于芯片手册,通常写保护位为 0 时允许写入,这里通过置位操作禁用保护)。
通俗比喻
就像给保险箱(Flash)解锁:先确认要解锁的抽屉(扇区)和保险箱编号(Bank)正确,等上一次操作(比如关保险箱)完成,然后转动密码锁(修改选项字节),让抽屉可以打开(允许写入)。
第二段代码:Flash 字编程(FLASH_ProgramWord
)
功能
向 Flash 的指定地址写入一个 32 位数据(字),是 Flash 编程的核心操作。
(类似往 U 盘的某个位置写入 4 字节数据,但 Flash 必须先擦除到全 1 才能写入 0)
核心逻辑拆解
-
参数检查
assert_param(IS_FLASH_ADDRESS(Address)); // 检查地址是否在Flash有效范围内
- 确保写入地址是芯片 Flash 区域的合法地址,避免操作到 RAM 或外设区域。
-
配置 Flash 控制器
CLEAR_BIT(FLASH->CR, FLASH_CR_PSIZE); // 清除编程大小标志 FLASH->CR |= FLASH_PSIZE_WORD; // 设置编程单位为32位(字) FLASH->CR |= FLASH_CR_PG; // 使能编程操作
FLASH->CR
是 Flash 控制寄存器:PSIZE
位设置编程单位(字节、半字、字),这里设置为 32 位(字),确保每次写入 4 字节。PG
位是编程使能位,置 1 后允许执行编程操作。
-
直接写入数据到硬件地址
*(__IO uint32_t*)Address = Data;
- 硬件原理:Flash 存储区被映射到 CPU 的地址空间,直接向地址写入数据会触发硬件编程操作。
- 代码操作:
(__IO uint32_t*)Address
将 32 位地址值转换为 32 位指针,*
解引用指针,相当于 “找到这个地址对应的存储单元”。__IO
确保每次操作都真实作用于硬件,避免编译器优化(比如缓存数据不写入硬件)。
通俗比喻
好比在书架(Flash)的指定格子(地址)放一本书(数据):先告诉管理员(Flash 控制器)要放的书大小(32 位),允许放书(使能编程),然后直接把书塞进格子(写入地址)。
两段代码的共同关键点
-
直接操作硬件地址
通过指针强制类型转换(如(__IO uint32_t*)Address
)操作内存映射的硬件寄存器 / Flash 存储区,这是嵌入式开发的常见操作,必须严格按芯片手册的地址和数据格式操作。 -
volatile 的作用
__IO
(即volatile
)告诉编译器:“这个变量的值可能随时被硬件改变,不要优化我的操作,每次都要真实读写硬件!”,避免因编译器优化导致操作失效。 -
严格的时序和状态检查
Flash 操作对时序要求极高,必须等上一次操作完成(通过状态寄存器或等待函数)才能进行下一步,否则会导致操作失败或硬件错误。
总结
- 写保护函数:通过修改选项字节,解除特定扇区的写保护,为后续擦除 / 编程做准备。
- 字编程函数:配置 Flash 控制器,按 32 位单位向指定地址写入数据,是 Flash 编程的基础操作。
两者都是 STM32 Flash 底层驱动的核心代码,操作时必须严格遵循芯片手册的流程和限制(如地址对齐、电压范围等)。
以下是对两段代码的通俗讲解,尽量用直白语言解释功能、逻辑和关键细节:
一、语法本质:指针类型转换与解引用
1. 核心操作拆解
(__IO uint32_t*)Address // 步骤1:将地址值`Address`强制转换为`__IO uint32_t*`类型的指针
*(__IO uint32_t*)Address // 步骤2:解引用该指针,得到指针指向的32位内存单元
*(__IO uint32_t*)Address = Data; // 步骤3:向该内存单元写入32位数据`Data`
__IO
宏:通常定义为 volatile
(如 #define __IO volatile
),用于告知编译器禁止优化该地址的访问,确保每次操作都直接读写硬件内存。
- 类型转换
(uint32_t*)
:将通用的uint32_t
地址值转换为指向 32 位无符号整数的指针,使编译器按 32 位单位操作该地址。 - 解引用
*
:通过指针操作实际访问物理地址,实现对 Flash 存储单元的写入。
2. 为什么不直接使用 Address = Data
?
Address
是uint32_t
类型的地址数值(如0x08000000
),直接赋值Address = Data
是对变量赋值,而非操作内存。- 通过
(uint32_t*)
转换为指针后,*指针
表示该地址处的内存单元,才能实现对硬件地址的写入操作。
Ymodem 文件夹中 common.c
文件的详细解读
一、文件定位:Ymodem 协议的 “基础设施”
common.c
提供了 串口通信底层接口 和 数据处理辅助函数,是 Ymodem 协议(文件传输)和菜单系统(用户交互)的基础支撑。核心功能包括:
- 串口字符 / 字符串发送:实现向电脑端(串口助手)输出数据
- 用户输入获取:读取串口接收的数据(用户按键或命令)
- 数据格式转换:字符串与整数的相互转换(用于解析文件大小等信息)
- 底层串口操作:封装 STM32 的 USART 寄存器操作,简化上层调用
二、串口通信核心函数:与电脑端 “对话” 的桥梁
1. SerialPutChar
:发送单个字符
- 功能:通过 USART1 发送一个字节数据,并等待发送缓冲区为空(确保发送完成)
- 关键代码:
void SerialPutChar(uint8_t c) {USART_SendData(USART1, c); // 写入 USART 数据寄存器while (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE) == RESET) {} // 等待发送缓冲区空 }
- 应用场景:发送 Ymodem 协议控制字符(如
ACK
、NAK
、CA
),或向用户显示单个字符(如菜单提示)。
2. Serial_PutString
:发送字符串
- 功能:逐字符发送字符串,直到遇到
\0
结束符 - 关键代码:
void Serial_PutString(uint8_t *s) {while (*s != '\0') { // 遍历字符串SerialPutChar(*s); // 调用单个字符发送函数s++;} }
- 应用场景:显示菜单界面(如
Main_Menu
函数中的提示信息)、错误提示(如 “Write Protection disabled...”)。
3. USART_SendData
:底层串口数据发送
- 功能:直接操作 USART 寄存器,发送数据(被
SerialPutChar
调用) - 关键代码:
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data) {assert_param(IS_USART_ALL_PERIPH(USARTx)); // 检查串口参数有效性assert_param(IS_USART_DATA(Data)); // 检查数据有效性(0~0x1FF)USARTx->DR = (Data & 0x01FF); // 写入数据寄存器 }
- 底层支撑:确保数据按 STM32 USART 硬件要求发送,避免非法操作。
三、用户输入处理:获取串口接收的数据
1. SerialKeyPressed
:检测是否有按键输入
- 功能:检查 USART1 的接收缓冲区是否有数据(非阻塞检测)
- 关键代码:
uint32_t SerialKeyPressed(uint8_t *key) {if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET) { // 接收缓冲区非空标志*key = huart1.Instance->DR & 0x01FF; // 读取数据寄存器return 1; // 有数据}return 0; // 无数据 }
- 作用:避免
GetKey
函数阻塞,先检测是否有数据再读取。
2. GetKey
:等待用户输入并返回按键值
- 功能:循环调用
SerialKeyPressed
,直到检测到有效输入(阻塞式获取按键) - 关键代码:
uint8_t GetKey(void) {uint8_t key = 0;while (1) {if (SerialKeyPressed(&key)) break; // 等待按键按下}return key; }
- 应用场景:菜单系统中获取用户选择(如按下数字键 1、2、3),或 Ymodem 传输中处理用户取消命令。
3. GetInputString
:获取用户输入的字符串(支持退格)
- 功能:接收用户输入的字符串,处理退格(
\b
)、换行(\r
),支持最长CMD_STRING_SIZE
(128 字节) - 关键逻辑:
- 遇到
\r
结束输入,添加\0
结束符 - 退格时删除最后一个字符,并回显
\b \b
(删除并空格覆盖) - 过滤非法字符(仅允许 ASCII 0x20~0x7E,即可打印字符)
- 遇到
- 应用场景:输入命令参数(如文件路径),但在当前代码中主要用于菜单交互的辅助(实际 Ymodem 传输由协议自动处理)。
四、数据格式转换:字符串与整数的 “翻译官”
1. Str2Int
:字符串转整数(支持十进制、十六进制、单位后缀)
- 功能:将用户输入的字符串(如
1234
、0x1A
、10k
)转换为整数 - 处理逻辑:
- 十六进制:以
0x
开头,解析每位十六进制字符(0-9
、A-F
、a-f
) - 十进制:支持
k
(1024 倍)、M
(1024*1024 倍)后缀,如2M
转换为2097152
- 错误处理:超过 10 位十进制 / 8 位十六进制、非法字符时返回错误(
res=0
)
- 十六进制:以
- 关键代码:
uint32_t Str2Int(uint8_t *inputstr, int32_t *intnum) {if (inputstr[0] == '0' && (inputstr[1] == 'x' || inputstr[1] == 'X')) {// 处理十六进制(从第3个字符开始)val = (val << 4) + CONVERTHEX(inputstr[i]); // 每4位转换为一个十六进制数} else {// 处理十进制,支持 k/M 后缀val = val * 10 + CONVERTDEC(inputstr[i]); // 逐位累加} }
- 应用场景:Ymodem 接收文件时解析文件名中的文件大小(如
main.bin 12345
中的12345
)。
2. Int2Str
:整数转字符串(未完整实现,当前代码有缺陷)
- 功能:将整数转换为字符串(当前代码仅处理十进制,且逻辑不完整,可能无法正确处理零和负数)
- 现有代码问题:
- 循环固定 10 次,未处理不同位数的整数
Status
变量逻辑错误,导致前导零无法正确过滤(如0
会被转为空字符串)
- 实际用途:在 Ymodem 发送文件时,将文件大小转换为字符串,嵌入文件名数据包中(需结合
Ymodem_PrepareIntialPacket
使用)。
五、整体流程:common.c 如何支撑系统运行?
1. 串口输出流程(以菜单显示为例)
plaintext
Main_Menu函数调用 Serial_PutString("菜单文本")
└─ 逐字符调用 SerialPutChar└─ 调用 USART_SendData 写入 USART1 数据寄存器└─ 等待 TXE 标志,确保字符发送完成
2. 用户输入流程(以选择菜单选项为例)
plaintext
用户在串口助手输入数字键(如 '1')
└─ USART1 接收数据,RXNE 标志置位
└─ GetKey 函数循环调用 SerialKeyPressed,检测到数据后返回按键值(0x31)
└─ 菜单逻辑根据按键值执行对应操作(如调用 SerialDownload 进行文件下载)
3. 数据转换流程(以解析文件大小为例)
plaintext
Ymodem 接收文件名包中的文件大小字符串(如 "1024")
└─ 调用 Str2Int 转换为整数 1024
└─ 验证是否超过 Flash 容量(USER_FLASH_SIZE),决定是否擦除 Flash
六、关键函数关系图
plaintext
common.c 函数关系
├─ 串口发送层:USART_SendData → SerialPutChar → Serial_PutString
├─ 输入检测层:SerialKeyPressed → GetKey → GetInputString
└─ 数据转换层:Str2Int ↔ Int2Str
七、总结:common.c 的 “默默无闻” 与重要性
虽然 common.c
没有 Ymodem 协议的核心逻辑(如数据包解析、Flash 写入),但它是整个系统的 “神经系统”:
- 人机交互桥梁:通过串口发送和接收数据,让用户能通过菜单操作、查看提示
- 数据处理基石:字符串与整数的转换确保文件大小、地址等关键信息正确解析
- 底层兼容性:封装 STM32 USART 操作,屏蔽硬件细节,让上层代码(ymodem.c、menu.c)专注于业务逻辑
它就像一个 “万能工具包”,为 Ymodem 文件传输和菜单系统提供了最基础但不可或缺的支持,确保整个 IAP(在应用编程)系统稳定运行。
Ymodem文件夹中的Ymodem .c
一、文件定位:Ymodem 协议的 “引擎”
ymodem.c
实现了 Ymodem 文件传输协议,用于单片机(如 STM32)通过串口与电脑进行文件(如固件程序)的双向传输(发送 / 接收)。核心功能包括:
- 接收文件:从电脑接收程序,写入单片机 Flash(对应菜单选项 1)
- 发送文件:将 Flash 中的程序发送到电脑(菜单中被禁用,但代码中保留实现)
- 数据校验:通过 CRC16 或 Checksum 确保数据传输正确
- 错误处理:处理超时、校验失败、用户取消等异常
二、核心变量与协议常量
先理解代码中定义的关键 协议常量(位于 ymodem.h
),这些是 Ymodem 协议的 “语言规则”:
#define SOH 0x01 // 128字节数据包起始符
#define STX 0x02 // 1024字节数据包起始符(优化大文件传输)
#define EOT 0x04 // 传输结束符
#define ACK 0x06 // 确认接收
#define NAK 0x15 // 未正确接收,请求重发
#define CA 0x18 // 连续两个 CA 表示取消传输
#define CRC16 0x43 // 请求使用 CRC16 校验(替代 Checksum)
#define PACKET_SIZE 128 // 小数据包大小
#define PACKET_1K_SIZE 1024 // 大数据包大小(提升传输效率)
三、文件接收流程:Ymodem_Receive
函数(核心逻辑)
1. 初始化与准备
- 目标地址:确定文件写入 Flash 的起始地址(
APPLICATION_ADDRESS
,用户程序区) - 缓冲区:使用
packet_data
存储接收的数据包,file_size
解析文件大小
uint32_t flashdestination = APPLICATION_ADDRESS; // Flash 目标地址
uint8_t packet_data[PACKET_1K_SIZE + PACKET_OVERHEAD]; // 数据包缓冲区
2. 会话循环:等待文件传输开始
通过 Receive_Packet
函数接收第一个数据包,可能是 文件名包 或 结束符(EOT):
switch (Receive_Packet(packet_data, &packet_length, NAK_TIMEOUT)) {case 0: // 成功接收数据包if (packet_length == 0) { // 收到 EOT,传输结束Send_Byte(ACK);break;}// 处理文件名和文件大小(首次接收时)if (packets_received == 0 && packet_data[PACKET_HEADER] != 0) {// 解析文件名(如 "main.bin")和文件大小(如 "12345")for (i=0; *file_ptr!='\0'; i++) FileName[i] = *file_ptr++;Str2Int(file_size, &size); // 字符串转整数,获取文件大小// 验证文件是否超过 Flash 容量if (size > USER_FLASH_SIZE) { Send_Byte(CA); Send_Byte(CA); return -1; }FLASH_If_Erase(APPLICATION_ADDRESS); // 擦除目标 Flash 区域}
}
3. 数据块接收与写入 Flash
- 数据包解析:检查序列号(防止乱序),提取数据部分(从
PACKET_HEADER
开始) - Flash 写入:通过
FLASH_If_Write
函数将数据写入 Flash(4 字节对齐)
memcpy(buf_ptr, packet_data + PACKET_HEADER, packet_length); // 数据存入缓冲区
// 写入 Flash(每次写入 4字节,因为 STM32 Flash 按字编程)
if (FLASH_If_Write(&flashdestination, (uint32_t*)buf, packet_length/4) != 0) {Send_Byte(CA); Send_Byte(CA); // 写入失败,取消传输return -2;
}
Send_Byte(ACK); // 确认接收,继续下一包
4. 错误处理与重试
- 超时处理:若
Receive_Byte
超时(NAK_TIMEOUT
),发送CRC16
请求重发 - 最大错误次数:超过
MAX_ERRORS
(5 次)则取消传输(发送两个CA
)
if (errors > MAX_ERRORS) {Send_Byte(CA); Send_Byte(CA); // 连续两个 CA,通知对方取消return 0;
}
四、文件发送流程:Ymodem_Transmit
函数(辅助功能)
虽然菜单中上传功能被禁用,但代码保留了发送逻辑,流程如下:
1. 准备初始数据包(文件名和大小)
- 构造首包:包含
SOH
(起始符)、序列号0x00
、反码0xFF
、文件名、文件大小
Ymodem_PrepareIntialPacket(data, sendFileName, &sizeFile);
data[0] = SOH; data[1] = 0x00; data[2] = 0xFF; // 首包固定格式
2. 分块发送数据
- 数据包类型:根据数据大小选择
SOH
(128 字节)或STX
(1024 字节) - 校验方式:使用 CRC16(更可靠)或 Checksum(简单累加)
if (CRC16_F) { // 使用 CRC16 校验(默认启用)tempCRC = Cal_CRC16(data+3, packet_length); // 计算 CRC16Send_Byte(tempCRC >> 8); Send_Byte(tempCRC & 0xFF); // 发送两个字节校验值
} else {tempCheckSum = CalChecksum(data+3, packet_length); // 简单累加校验Send_Byte(tempCheckSum);
}
3. 结束传输
- 发送
EOT
(结束符),等待对方ACK
确认后,发送一个空包(序列号0x00
)作为收尾
Send_Byte(EOT); // 通知传输结束
if (收到 ACK) {Send_Byte(ACK); // 确认结束
}
五、关键函数细节
1. Receive_Packet
:数据包解析器
- 识别包类型:根据第一个字节(
SOH
/STX
/EOT
/CA
)判断数据包类型 - 序列号校验:确保当前包序列号与预期一致(
packet_data[1] == packets_received
)
if (data[PACKET_SEQNO_INDEX] != ((data[PACKET_SEQNO_COMP_INDEX] ^ 0xFF) & 0xFF)) {return -1; // 序列号错误,丢弃包
}
2. Cal_CRC16
:循环冗余校验
- 算法原理:通过多项式
0x1021
计算数据的 CRC16 值,比 Checksum 更可靠 - 应用场景:在大数据传输(如固件)时使用,减少误码导致的程序损坏
uint16_t crc = 0;
while (data < dataEnd) crc = UpdateCRC16(crc, *data++); // 逐个字节更新 CRC
return crc & 0xFFFF; // 返回 16位校验值
3. FLASH_If_Write
配合
- Flash 写入特性:必须按 4 字节(字)编程,且目标地址必须已擦除(全
0xFFFFFFFF
) - 数据对齐:
ymodem.c
接收的数据会被转换为 32 位字,确保写入 Flash 时格式正确
六、整体流程图(接收方向)
plaintext
等待接收第一个数据包(文件名/大小包)
├─ 解析文件名和文件大小,验证 Flash 容量
├─ 擦除目标 Flash 区域
├─ 循环接收数据块:
│ ├─ 接收数据包(SOH/STX 开头)
│ ├─ 校验序列号和 CRC/Checksum
│ ├─ 数据写入 Flash
│ └─ 发送 ACK(成功)或 NAK(失败,请求重发)
├─ 接收 EOT 结束符,发送 ACK 确认
└─ 返回文件大小(成功)或错误码(失败)
七、总结:ymodem.c 如何 “保障可靠传输”?
- 分包策略:大文件拆分为 1024 字节(STX)或 128 字节(SOH)的数据包,平衡传输效率和错误处理成本
- 校验机制:通过 CRC16(默认)或 Checksum 检测数据错误,确保写入 Flash 的程序完整无误
- 错误重试:对 NAK(重发请求)和超时错误,最多重试 10 次,避免偶然干扰导致传输失败
- Flash 适配:与
flash_if.c
配合,实现擦除、按字写入等操作,符合单片机 Flash 硬件特性
它就像一个 “可靠的快递员”,确保文件在串口传输中不丢失、不损坏,是实现单片机固件升级(IAP)的核心模块。
Ymodem文件夹中的menu.c
一、文件定位:串口交互的 “主控中心”
menu.c
是 STM32 单片机通过串口与电脑交互的菜单程序,主要实现以下功能:
- 显示操作菜单(下载程序、上传程序、运行程序、解除 Flash 写保护)
- 接收用户输入(通过串口工具发送数字 1-4)
- 调用底层函数(Flash 操作、Ymodem 协议)完成具体任务
- 处理错误和异常情况
二、核心变量与全局状态
先看代码中定义的关键全局变量,理解程序 “记忆” 的信息:
pFunction Jump_To_Application; // 函数指针,用于跳转运行新程序
uint32_t JumpAddress; // 新程序的入口地址
__IO uint32_t FlashProtection = 0; // Flash 写保护状态(0:未保护,1:已保护)
uint8_t tab_1024[1024] = {0}; // 临时缓冲区,用于接收 Ymodem 传输的数据
uint8_t FileName[FILE_NAME_LENGTH];// 存储接收文件的文件名
三、主函数流程:Main_Menu
函数(核心循环)
1. 初始化与欢迎界面
程序启动后,先打印 版权信息和版本号(类似软件启动时的 “开场白”):
SerialPutString("======================================================================");
// 中间是固定的版权文字,略
SerialPutString("STM32F4xx In-Application Programming Application (Version 1.0.0)");
2. 检测 Flash 写保护状态
if (FLASH_If_GetWriteProtectionStatus() == 0) {FlashProtection = 1; // 如果检测到写保护,标记为“已保护”
} else {FlashProtection = 0; // 否则标记为“未保护”
}
- 目的:决定菜单是否显示 “解除写保护” 选项(选项 4)。
- 类比:就像检测保险箱是否上锁,决定是否提供 “解锁” 按钮。
3. 主菜单循环(while(1)
)
菜单会反复显示,直到用户操作或断电,流程如下:
3.1 显示菜单选项
根据 FlashProtection
状态动态显示选项:
SerialPutString("================== Main Menu ============================");
SerialPutString(" Download Image To the STM32F4xx Internal Flash ------- 1"); // 选项1:下载程序
SerialPutString(" Upload Image From the STM32F4xx Internal Flash ------- 2"); // 选项2:上传程序(代码中被禁用)
SerialPutString(" Execute The New Program ------------------------------ 3"); // 选项3:运行程序
if (FlashProtection != 0) {SerialPutString(" Disable the write protection ------------------------- 4"); // 选项4:解除写保护(仅保护时显示)
}
- 选项 2 被禁用:代码中
SerialUpload
函数被注释,提示用户 “功能禁用”。
3.2 接收用户输入(GetKey()
)
通过串口获取用户输入的数字(对应 ASCII 码,如数字 1 对应 0x31
):
key = GetKey(); // 阻塞等待用户输入,直到收到有效字符
3.3 根据输入执行功能(if-else
分支)
根据用户输入的数字(1-4),调用对应的函数:
❶ 选项 1:下载程序到 Flash(SerialDownload
函数)
- 核心目的:通过串口接收电脑发送的程序文件,写入单片机的 Flash。
- 流程拆解:
-
擦除标志扇区:
uint32_t flashdestination = ADDR_FLASH_SECTOR_2; FLASH_If_Erase_One_Sector(2U); // 擦除扇区2(用于存储“程序已准备好”的标志)
- Flash 写入前必须擦除(擦除后内容为全 1),否则无法写入 0。
-
等待文件传输:
SerialPutString("Waiting for the file to be sent ... (press 'a' to abort)\n\r"); Size = Ymodem_Receive(&tab_1024[0]); // 调用 Ymodem 协议接收文件
Ymodem_Receive
负责处理串口数据的分包接收、校验、写入 Flash。
-
处理接收结果:
- 成功(Size > 0):显示文件名、文件大小,并在 Flash 中写入 “APP FLAG” 标志(标记程序可运行)。
- 失败:根据错误码显示原因(如文件太大、验证失败、用户取消等)。
-
❷ 选项 2:上传程序(代码中禁用)
SerialPutString("This function is disabled! Please Use 1\r"); // 直接提示用户不可用
- 原因:可能出于安全考虑(防止程序被非法读取)或功能未完善。
❸ 选项 3:运行新程序
- 核心操作:让单片机从当前的 IAP 程序(引导程序)跳转到新下载的用户程序。
- 关键步骤:
- 关闭系统定时器:
SysTick->CTRL = 0X00; // 禁用 SysTick 定时器,避免干扰跳转过程
- 获取新程序入口地址:
JumpAddress = *(__IO uint32_t*)(APPLICATION_ADDRESS + 4);
- 用户程序的入口地址存储在 Flash 的固定位置(
APPLICATION_ADDRESS
是程序起始地址,+4 是向量表偏移)。
- 用户程序的入口地址存储在 Flash 的固定位置(
- 设置栈指针并跳转:
__set_MSP(*(__IO uint32_t*)APPLICATION_ADDRESS); // 初始化用户程序的栈指针 Jump_To_Application = (pFunction)JumpAddress; // 将地址转换为函数指针 Jump_To_Application(); // 跳转执行用户程序
- 这一步类似 “换跑道”,让单片机从 IAP 程序切换到用户程序运行。
- 关闭系统定时器:
❹ 选项 4:解除 Flash 写保护(仅保护时可用)
- 前提:
FlashProtection == 1
(即检测到写保护)。 - 流程:
switch (FLASH_If_DisableWriteProtection()) { // 调用底层函数解除保护case 1: SerialPutString("Write Protection disabled...\r\n"); FlashProtection = 0; break; // 成功case 2: SerialPutString("Error: Flash write unprotection failed...\r\n"); break; // 失败 }
- 写保护是 Flash 的安全机制,解除后才能写入新程序(类似去掉 U 盘的写保护开关)。
3.4 无效输入处理
如果用户输入非 1-4 的数字,根据 FlashProtection
状态提示合法选项:
if (FlashProtection == 0) {SerialPutString("Invalid Number ! ==> The number should be either 1, 2 or 3\r");
} else {SerialPutString("Invalid Number ! ==> The number should be either 1, 2, 3 or 4\r");
}
四、关键函数细节
1. SerialDownload
函数:文件下载的 “幕后工作者”
- 核心任务:协调 Ymodem 协议和 Flash 操作,完成程序写入。
- 特殊操作:
- 擦除标志扇区:在 Flash 的扇区 2 写入 “APP FLAG”,用于标记程序可运行(类似在文件上贴 “已准备好” 标签)。
- 错误码处理:根据
Ymodem_Receive
的返回值(如 -1 表示文件过大,-2 表示写入失败),精准提示用户问题所在。
2. 程序跳转的 “底层魔法”
- 为什么需要设置栈指针(MSP)?
单片机运行程序时,需要一个 “内存规划图” 来管理函数调用和局部变量,栈指针就是这个规划图的起点。用户程序的栈指针存储在 Flash 起始地址,必须正确设置才能避免程序 “跑飞”。 __disable_irq()
的作用:
关闭中断,避免跳转过程中被外部事件打断,确保操作原子性(类似 “请勿打扰” 模式)。
五、整体流程图
plaintext
程序启动
├─ 打印欢迎信息
├─ 检测 Flash 写保护状态(决定是否显示选项4)
└─ 进入主循环:├─ 显示菜单(根据保护状态动态显示选项)├─ 等待用户输入(数字1-4)├─ 根据输入执行功能:│ ├─ 1:下载程序(调用 Ymodem 接收,写入 Flash,设置标志)│ ├─ 2:上传程序(禁用,提示用户)│ ├─ 3:运行程序(设置栈指针,跳转至用户程序入口)│ └─ 4:解除写保护(仅保护时可用,调用底层函数)└─ 处理无效输入(提示合法选项)
六、总结:menu.c 如何 “串联” 整个系统?
- 用户交互层:通过串口菜单降低操作门槛,用户无需了解底层协议和硬件细节,只需输入数字即可完成操作。
- 功能调度层:调用
ymodem.c
(文件传输)和flash_if.c
(Flash 操作),将复杂的硬件操作封装成简单的选项。 - 安全保障层:检测 Flash 写保护状态,防止误操作;验证文件大小和 Flash 容量,避免硬件损坏。
它就像一个 “嵌入式系统的前台接待”,让用户通过简单的菜单交互,完成单片机程序的更新和运行,是 IAP(在应用编程)的核心控制模块。
细读代码menu.c
FLASH_If_Erase_One_Sector(2U);
这段代码是 STM32 内部 Flash 擦除操作的关键步骤,位于 SerialDownload
函数中(文件下载功能),用于在接收新文件前清除目标存储区域。以下是详细解析:
1. 函数功能与参数
FLASH_If_Erase_One_Sector(2U);
- 函数名:
FLASH_If_Erase_One_Sector
属于底层 Flash 操作接口(定义在flash_if.c
中),用于擦除指定编号的 Flash 扇区。 - 参数:
2U
表示擦除 第 2 个扇区(STM32 的 Flash 扇区编号从 0 开始)。
2. STM32 Flash 扇区布局(以 F411 为例)
STM32F411 的 Flash 扇区划分通常如下(具体地址需结合芯片型号和项目定义):
扇区编号 | 起始地址 | 大小 | 用途(本例) |
---|---|---|---|
0 | 0x08000000 | 16 KB | 引导程序(IAP)存储区 |
1 | 0x08004000 | 16 KB | 保留或临时数据区 |
2 | 0x08008000 | 16 KB | 用户应用程序存储区(目标区域) |
... | ... | ... | 后续扇区(根据芯片容量扩展) |
- 本例关键:代码中擦除的扇区 2(
0x08008000
起始)是 用户应用程序的目标存储区域,下载新文件前需先擦除,确保写入数据的正确性(Flash 写入前必须擦除至全 1,即 0xFFFFFFFF)。
3. 上下文逻辑:为什么在文件下载前擦除?
该代码位于 SerialDownload
函数开头(文件下载入口),执行流程如下:
void SerialDownload(void)
{// **擦除扇区 2**uint32_t flashdestination = ADDR_FLASH_SECTOR_2; // 0x08008000(扇区 2 起始地址)FLASH_If_Erase_One_Sector(2U); // 擦除扇区 2// 等待接收 Ymodem 文件Size = Ymodem_Receive(&tab_1024[0]);// 接收成功后,将文件数据写入 Flash(目标地址为扇区 2 起始地址)if (Size > 0) {FLASH_If_Write(&flashdestination, ...); // 写入擦除后的扇区 2}
}
- 目的:
- 清除旧数据:确保目标扇区(用户应用程序存储区)在写入新文件前是空白的(全 0xFF)。
- 准备写入空间:Flash 只能将 1 改写为 0,无法直接覆盖写,必须先擦除以重置为全 1。
4. 底层实现:FLASH_If_Erase_One_Sector
做了什么?
该函数是对 STM32 Flash 硬件操作的封装,核心步骤(伪代码):
uint32_t FLASH_If_Erase_One_Sector(uint32_t SectorNum)
{// 1. 解锁 Flash 控制寄存器(擦除/写入前必须解锁)HAL_FLASH_Unlock();// 2. 设置擦除命令:指定扇区编号FLASH_EraseInitTypeDef EraseInit;EraseInit.Sector = SectorNum; // 扇区编号(如 2U)EraseInit.NbSectors = 1; // 擦除 1 个连续扇区EraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;// 3. 执行擦除操作uint32_t PageError = 0;HAL_FLASHEx_Erase(&EraseInit, &PageError);// 4. 重新锁定 Flash 控制寄存器HAL_FLASH_Lock();return PageError; // 0 表示成功,非 0 表示擦除失败(地址错误等)
}
- 注意:
本例中未检查擦除返回值(直接调用后继续执行),实际项目中应增加错误处理(如擦除失败时终止下载)。
5. 与后续代码的联动
擦除扇区 2 后,代码中定义的 flashdestination
变量指向该扇区的起始地址(ADDR_FLASH_SECTOR_2 = 0x08008000
),后续通过 FLASH_If_Write
将接收的文件数据写入该地址:
FLASH_If_Write(&flashdestination, (uint32_t*) APP_FLAG, 2); // 写入扇区 2 起始地址
- 关键点:
擦除操作确保写入地址的 Flash 区域可被正确编程(否则写入会失败或数据错误)。
总结:代码的核心作用
在文件下载前擦除目标存储扇区(扇区 2),为后续写入新的用户程序或数据做准备,是 Flash 操作的标准流程(擦除 → 写入 → 验证)的第一步。该操作保证了存储区域的 “干净”,避免旧数据干扰新程序的运行。
Size = Ymodem_Receive(&tab_1024[0]);
这段代码位于 SerialDownload
函数中,是 通过 Ymodem 协议接收文件数据的核心操作,具体作用是 启动文件接收过程并存储数据到缓冲区。以下是详细解析:
1. 函数功能与参数
Size = Ymodem_Receive(&tab_1024[0]);
- 函数名:
Ymodem_Receive
实现 Ymodem 协议的文件接收逻辑,用于通过串口接收外部发送的二进制文件。 - 参数:
&tab_1024[0]
指向接收缓冲区的首地址。tab_1024
是一个大小为 1024 字节的数组(定义在文件开头),用于暂存接收的文件数据(Ymodem 协议通常以 1024 字节为一个数据块传输)。
2. Ymodem 协议基础
Ymodem 是一种基于串口的文件传输协议,常用于嵌入式系统与主机(如 PC)之间的文件交换,特点包括:
- 分块传输:文件数据被分割为 1024 字节的块(最后一块可能更小)。
- 错误校验:每个数据块附带 CRC-16 校验和,确保数据完整性。
- 协议交互:接收方通过发送
ACK
/NAK
等控制字符与发送方通信。
此处 Ymodem_Receive
函数负责处理协议交互(如握手、数据块接收、校验、重传请求等),并将有效数据写入缓冲区 tab_1024
。
3. 返回值 Size
的含义
- 成功接收:
Size > 0
表示接收的文件总字节数(如Size = 0x1234
表示文件大小为 4660 字节)。 - 错误码:
Size = -1
:文件大小超过目标 Flash 存储区域的容量限制(后续代码检查存储空间)。Size = -2
:数据校验失败(如 CRC 校验不通过)。Size = -3
:用户主动取消接收(代码中通过检测按键 'a' 触发)。Size <= -4
:其他错误(如串口通信故障)。
后续代码根据 Size
的值执行不同逻辑(如写入 Flash、提示错误等)。
4. 上下文逻辑:接收流程的位置
代码位于 SerialDownload
函数的关键路径:
void SerialDownload(void)
{// 1. 擦除目标扇区(扇区 2)FLASH_If_Erase_One_Sector(2U);// 2. 提示等待文件发送SerialPutString("Waiting for the file to be sent ... (press 'a' to abort)\n\r");// **3. 调用 Ymodem 接收文件(当前代码行)**Size = Ymodem_Receive(&tab_1024[0]);// 4. 根据接收结果处理(写入 Flash 或提示错误)if (Size > 0) {// 写入 Flash 并设置标志} else {// 处理不同错误情况}
}
- 前置条件:目标扇区已擦除(确保 Flash 可写入)。
- 后续操作:若接收成功,通过
FLASH_If_Write
将tab_1024
中的数据写入 Flash 存储区(起始地址为ADDR_FLASH_SECTOR_2
,即扇区 2 起始地址)。
5. 缓冲区 tab_1024
的作用
- 临时存储:作为 Ymodem 协议的数据缓冲区,每次接收一个数据块(1024 字节),直到文件传输完成。
- 适配协议:Ymodem 协议的标准数据块大小为 1024 字节(扩展 Ymodem 支持 2048 字节,但此处使用 1024 字节块),缓冲区大小与块大小匹配,避免多次内存拷贝。
6. 潜在注意事项
- 缓冲区溢出:若接收的文件块大小超过 1024 字节,可能导致缓冲区溢出。但 Ymodem 协议规定块大小固定为 1024 字节(除非使用扩展协议),此处代码假设块大小匹配缓冲区。
- 线程安全:由于
SerialDownload
是单线程函数(无多任务环境),无需考虑缓冲区并发访问问题。 - 错误处理:代码对
Size
的负值做了分类处理(如用户取消、校验失败等),增强了鲁棒性。
总结:代码的核心作用
通过 Ymodem 协议接收串口传输的文件数据,存储到 1024 字节的缓冲区 tab_1024
,并返回接收状态或文件大小。这是整个文件下载流程的关键步骤,连接了外部文件传输与内部 Flash 存储,确保数据正确接收后再写入硬件存储设备。
if (FLASH_If_Write(&flashdestination, (uint32_t*) APP_FLAG, 2) == 0)
这段代码位于 SerialDownload
函数中,是 向 STM32 内部 Flash 写入标志数据的关键操作,用于标记用户程序存储区域的有效性。以下是结合上下文的详细解析:
1. 代码功能与核心参数
if (FLASH_If_Write(&flashdestination, (uint32_t*) APP_FLAG, 2) == 0)
- 函数名:
FLASH_If_Write
底层 Flash 写入接口(定义在flash_if.c
中),用于将数据写入指定的 Flash 地址。 - 参数解析:
&flashdestination
:目标写入地址(指针)flashdestination
在函数开头定义为ADDR_FLASH_SECTOR_2
(即扇区 2 起始地址0x08008000
),指向用户程序存储区的起始位置。
(uint32_t*) APP_FLAG
:待写入的数据(强制转换为 32 位无符号整数指针)APP_FLAG
是用户定义的字符串const char *str_flag = "APP FLAG";
,但此处通过(uint32_t*)
转换为 32 位整数指针,实际写入的是该字符串的起始地址(或其指向的内容,需结合具体实现)。
2
:写入的 32 位字数量(每个字 4 字节,总写入 8 字节)。
- 返回值判断:
== 0
表示写入成功,非 0 表示失败。
2. 上下文逻辑:为什么写入标志数据?
代码位于文件接收成功(Size > 0
)后的处理分支中,执行流程如下:
if (Size > 0) {// 打印接收成功信息// ...// **写入标志数据(当前代码行)**const char *str_flag = "APP FLAG";uint32_t * APP_FLAG = (uint32_t *)str_flag; // 将字符串地址转换为 uint32_t 指针if (FLASH_If_Write(&flashdestination, APP_FLAG, 2) == 0) {// 标志设置成功} else {// 标志设置失败}
}
- 核心目的:
在用户程序存储区(扇区 2)的起始位置写入特定标志(如 "APP FLAG" 的前 8 字节),用于后续系统启动时校验程序的有效性(例如判断是否存在合法的用户程序)。 - 数据转换注意:
str_flag
是字符串常量(如 ASCII 码为41 50 50 20 46 4C 41 47
),通过(uint32_t*)
转换后,按 32 位字写入 Flash,即前 4 字节("APP "
)和后 4 字节("FLAG"
)分别写入两个 32 位单元。
3. FLASH_If_Write
函数的底层逻辑(伪代码)
该函数封装了 STM32 Flash 的编程操作,核心步骤:
uint32_t FLASH_If_Write(uint32_t *pDestination, uint32_t *pBuffer, uint16_t NumOfWordToWrite)
{// 1. 解锁 Flash 控制寄存器(写入前必须解锁)HAL_FLASH_Unlock();// 2. 逐字写入:每次写入一个 32 位字for (uint16_t i = 0; i < NumOfWordToWrite; i++) {if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, (uint32_t)pDestination + i*4, pBuffer[i]) != HAL_OK) {// 写入错误,返回非 0 错误码HAL_FLASH_Lock();return 1;}}// 3. 重新锁定 Flash 控制寄存器HAL_FLASH_Lock();return 0; // 写入成功
}
- 关键特性:
- 按 32 位字对齐写入(STM32 Flash 编程的最小单位为字)。
- 每次写入前无需再次擦除(因为扇区已在
SerialDownload
开头擦除完毕)。
4. 与前后代码的联动
- 前置条件:
- 目标扇区 2 已通过
FLASH_If_Erase_One_Sector(2U)
擦除,确保写入区域为全 0xFFFFFFFF(可被编程为 0)。 - 通过 Ymodem 协议接收的文件数据已存储在缓冲区
tab_1024
中(后续可能通过循环写入整个文件,此处先写入标志)。
- 目标扇区 2 已通过
- 后续影响:
写入的标志数据(如 "APP FLAG")可在系统重启时被引导程序检测,用于判断是否跳转执行用户程序(见Main_Menu
中的Execute The New Program
选项)。
5. 潜在问题与注意事项
- 数据对齐:写入地址
flashdestination
必须是 4 字节对齐(STM32 Flash 编程要求),由于ADDR_FLASH_SECTOR_2
通常为扇区起始地址(如0x08008000
,4 字节对齐),此处无需担心。 - 标志内容设计:写入的
APP_FLAG
是用户自定义的标识,需确保其值不会被误判(例如避免全 0 或全 1)。 - 错误处理:若写入标志失败,代码提示
APP Flag Set Error
,但不会回滚已写入的文件数据(实际项目中可能需要增加数据一致性处理)。
总结:代码的核心作用
在用户程序存储区(扇区 2 起始地址)写入自定义标志数据(如 "APP FLAG" 的前 8 字节),用于标记该区域存在有效程序或数据。这是嵌入式系统中常见的 “状态标记” 机制,配合后续的程序跳转逻辑(如 Execute The New Program
选项),实现对用户应用程序的合法性校验和启动引导。
1. 代码功能与核心变量
Jump_To_Application = (pFunction) JumpAddress;
-
类型定义:
pFunction
是一个函数指针类型,通常定义为:typedef void (*pFunction)(void); // 无参数、无返回值的函数指针
用于指向用户应用程序的入口函数(复位后的第一条执行指令)。
-
JumpAddress
的来源:
代码前一行:JumpAddress = *(__IO uint32_t*) (APPLICATION_ADDRESS + 4);
APPLICATION_ADDRESS
是用户应用程序在 Flash 中的起始地址(如0x08008000
,即扇区 2 起始地址)。- 根据 ARM Cortex-M 架构规范,用户程序的 向量表首地址(
APPLICATION_ADDRESS
)存储的是 栈顶指针(MSP),第二个字(+4
偏移)存储的是 复位向量地址(即程序入口函数地址)。 - 因此,
JumpAddress
获取的是用户程序的 入口函数地址(如main
函数或编译器生成的启动代码入口)。
2. 上下文逻辑:跳转前的准备
在执行 Jump_To_Application = (pFunction) JumpAddress;
之前,代码完成了以下关键操作:
SysTick->CTRL = 0X00; // 禁止 SysTick 定时器(避免跳转时产生中断)
SysTick->LOAD = 0; // 清空定时器加载值
SysTick->VAL = 0; // 清空定时器当前值
__disable_irq(); // 关闭所有中断(确保跳转过程不受干扰)JumpAddress = *(__IO uint32_t*) (APPLICATION_ADDRESS + 4); // 获取程序入口地址
__set_MSP(*(__IO uint32_t*) APPLICATION_ADDRESS); // 设置用户程序的栈顶指针(MSP)
- 栈顶指针(MSP)的重要性:
用户程序运行需要独立的栈空间,APPLICATION_ADDRESS
处的第一个字即为用户程序的栈顶地址,必须通过__set_MSP
正确初始化,否则跳转后会因栈指针错误导致硬件异常。
3. 强制类型转换的作用
(pFunction)
将 JumpAddress
(uint32_t
类型的地址值)转换为函数指针,允许直接通过函数调用的方式跳转到目标地址:
Jump_To_Application(); // 调用函数指针,执行用户程序
- ARM 架构的兼容性:
Cortex-M 处理器支持直接通过函数指针跳转,前提是目标地址是 字对齐的(最低两位为0b00
)。由于用户程序的入口地址由编译器生成,必然满足字对齐要求,因此转换是安全的。
4. 跳转后的执行流程
调用 Jump_To_Application()
后,程序会:
- 跳转到用户程序的入口地址(如
Reset_Handler
)。 - 执行用户程序的启动代码(初始化堆栈、全局变量等)。
- 进入用户主函数(如
main
)开始正常运行。
- 与 IAP 引导程序的关系:
跳转后,IAP 引导程序的代码不再执行,用户程序完全接管系统。若用户程序需要重新进入 IAP(如后续升级),通常需要通过特定机制(如按键触发、通信协议)重新跳转回引导程序地址。
5. 潜在风险与注意事项
-
地址有效性校验:
代码未检查JumpAddress
是否为合法地址(如是否在用户程序存储区范围内),若用户程序未正确下载或 Flash 数据损坏,跳转可能导致硬件错误(如取指异常)。 -
中断与定时器状态:
跳转前关闭了所有中断和 SysTick 定时器,用户程序需在启动代码中重新初始化中断系统和定时器,否则可能导致功能异常。 -
向量表重定位:
用户程序的向量表默认位于APPLICATION_ADDRESS
,若用户程序在代码中调用SCB->VTOR
重定位向量表,需确保跳转前已正确配置。
总结:代码的核心作用
将用户应用程序的入口地址转换为函数指针,并通过调用该指针实现从 IAP 引导程序到用户程序的跳转。这是嵌入式系统中典型的 “程序执行切换” 机制,依赖 ARM Cortex-M 架构的向量表特性,确保用户程序以正确的栈环境和入口地址启动,是实现 In-Application Programming(应用内编程)的关键步骤。
int32_t Ymodem_Receive (uint8_t *buf)
关键代码逻辑解析
1. 初始化与主循环
flashdestination = APPLICATION_ADDRESS; // 固件写入起始地址(如0x08010000)
for (session_done = 0, errors = 0, session_begin = 0; ;) {// 会话循环(支持多文件传输)for (packets_received = 0, file_done = 0, buf_ptr = buf; ;) {// 接收单个数据包switch (Receive_Packet(...)) { ... }}
}
- APPLICATION_ADDRESS:新固件的写入地址(参考网页4的QSPI Flash设计)
- 双循环结构实现多文件传输支持(外层循环管理会话,内层循环处理单个文件)
2. 数据包处理
switch (packet_length) {case -1: // 发送方终止(如用户取消)Send_Byte(ACK);return 0;case 0: // 传输结束标志(EOT)Send_Byte(ACK);file_done = 1;break;default: // 正常数据包// 校验包序号(防丢包)if (packet_data[PACKET_SEQNO_INDEX] != (packets_received & 0xff)) {Send_Byte(NAK); // 请求重传(参考网页6的错误处理)}
}
- 序号校验:通过包序号(packet_data[PACKET_SEQNO_INDEX])和反码机制实现数据完整性检查
3. 文件信息解析
if (packets_received == 0) { // 第一个包为文件头// 解析文件名(如"firmware_v1.2.bin")for (i=0, file_ptr=packet_data+PACKET_HEADER; (*file_ptr!=0) && (i<FILE_NAME_LENGTH);) {FileName[i++] = *file_ptr++;}// 解析文件大小(如"65536"字节)Str2Int(file_size, &size);// 校验Flash空间(防止溢出)if (size > USER_FLASH_SIZE) {Send_Byte(CA); // 发送取消指令(参考网页3的错误处理)return -1;}FLASH_If_Erase(APPLICATION_ADDRESS); // 擦除Flash目标区域
}
- 文件头结构:符合Ymodem协议规范(文件名 + 文件大小 + 填充0x00)
- Flash擦除:预先清空目标区域(参考网页4的QSPI Flash操作)
4. 数据写入Flash
memcpy(buf_ptr, packet_data + PACKET_HEADER, packet_length);
ramsource = (uint32_t)buf;
// 写入Flash(4字节对齐处理)
if (FLASH_If_Write(&flashdestination, (uint32_t*)ramsource, packet_length/4) == 0) {Send_Byte(ACK); // 确认写入成功
}
- Flash写入函数:
FLASH_If_Write
需实现4字节对齐写入(STM32的Flash特性) - 内存对齐:
packet_length/4
确保按32位字写入(参考网页4的二进制文件处理)
5. 错误处理
if (errors > MAX_ERRORS) { // 超过最大错误次数Send_Byte(CA); // 发送取消信号return 0;
}
- 错误重试机制:连续错误超过阈值则终止会话(典型设计为5次重试)
- 取消指令:发送
CAN
字符(0x18)终止传输(参考网页3的协议规范)
关键协议特性实现
- ACK/NAK机制:
Send_Byte(ACK)
:确认数据包正确接收Send_Byte(NAK)
:请求重传错误包(如CRC校验失败)
- 多文件支持:
- 外层循环通过
session_done
控制是否继续接收新文件
- 外层循环通过
- 传输完整性:
- 序号校验 + CRC校验双重保障(参考网页3的CRC计算代码)
- Flash保护:
- 文件大小预校验(
USER_FLASH_SIZE
) - 擦除-写入分离操作(符合嵌入式Flash操作规范)
- 文件大小预校验(
典型执行流程
- 接收方发送字符
C
启动传输(参考网页8的握手流程) - 接收文件名和大小信息包(包序号0)
- 分块接收数据包(包序号1~N),写入Flash
- 接收结束包(包序号0),发送最终ACK
- 跳转到新固件地址执行(代码未展示,需在外部实现)
应用场景扩展建议
- 断电保护:记录最后成功写入的包号,支持断点续传(参考网页3的流式传输)
- 数字签名:在文件头添加固件签名,升级前验证合法性
- 双备份机制:使用A/B分区设计,防止升级失败变砖(参考网页4的进阶设计)
函数定义与功能概述
int32_t Ymodem_Receive (uint8_t *buf)
函数功能:通过 Ymodem 协议接收文件数据,并将其写入目标 Flash 区域(用于固件升级)。
参数:
buf
:接收数据的缓冲区(通常为 RAM 地址,用于临时存储接收到的数据包)。
返回值:- 成功时返回文件大小(字节数);
- 失败时返回负数错误码(如
-1
表示文件过大,-2
表示 Flash 写入失败等)。
核心逻辑解析(按代码执行流程)
1. 变量初始化与准备
uint8_t packet_data[PACKET_1K_SIZE + PACKET_OVERHEAD], file_size[FILE_SIZE_LENGTH], *file_ptr, *buf_ptr;
int32_t i, packet_length, session_done, file_done, packets_received, errors, session_begin, size = 0;
uint32_t flashdestination, ramsource;
flashdestination = APPLICATION_ADDRESS; // Flash 目标地址(固件存储起始位置)
- 关键变量:
packet_data
:存储接收的数据包(包含协议头部)。flashdestination
:目标 Flash 地址,通常为用户应用程序区起始地址(如0x08008000
)。size
:最终返回的文件大小,初始化为 0。
2. 会话级循环(外层循环)
for (session_done = 0, errors = 0, session_begin = 0; ;)
- 作用:处理整个文件传输会话,支持续传或多次文件传输(Ymodem 支持发送多个文件,以空文件名包结束)。
- 状态标志:
session_done
:会话结束标志(收到空文件名包时置 1)。session_begin
:会话开始标志(收到第一个有效数据包后置 1)。
3. 数据包接收循环(内层循环)
for (packets_received = 0, file_done = 0, buf_ptr = buf; ;)
- 作用:逐个接收数据包,直到文件传输完成(
file_done
置 1)。 - 核心逻辑:通过
Receive_Packet
函数获取数据包,根据包类型(文件名包、数据包、结束包)分支处理。
4. 处理 Receive_Packet
返回值
switch (Receive_Packet(packet_data, &packet_length, NAK_TIMEOUT))
Receive_Packet
功能:接收一个数据包,解析包类型(SOH/STX/EOT/CA/ABORT),并返回包长度或状态码。- 关键分支:
case 0
(正常数据包):处理 SOH(128 字节包)、STX(1024 字节包)、EOT(传输结束)、CA(发送方终止)等。case 1
(用户终止):返回错误码-3
。default
(超时或错误):重试或终止会话。
5. 文件名包处理(首次接收)
if (packets_received == 0) {// 解析文件名和文件大小for (i = 0, file_ptr = packet_data + PACKET_HEADER; (*file_ptr != 0) && (i < FILE_NAME_LENGTH);)FileName[i++] = *file_ptr++;// 解析文件大小字符串到整数Str2Int(file_size, &size);// 检查文件大小是否超过 Flash 容量if (size > (USER_FLASH_SIZE + 1)) {Send_Byte(CA); Send_Byte(CA); // 发送终止信号return -1;}// 擦除用户应用区 FlashFLASH_If_Erase(APPLICATION_ADDRESS);Send_Byte(ACK); Send_Byte(CRC16); // 确认接收文件名和大小
}
- 核心步骤:
- 从数据包中提取文件名(存储到全局变量
FileName
)和文件大小。 - 校验文件大小是否超过目标 Flash 容量(
USER_FLASH_SIZE
定义 Flash 总大小)。 - 擦除目标 Flash 区域(为写入新固件做准备)。
- 从数据包中提取文件名(存储到全局变量
6. 数据包包处理(非首次接收)
else {// 将数据从 RAM 缓冲区写入 Flashmemcpy(buf_ptr, packet_data + PACKET_HEADER, packet_length);ramsource = (uint32_t)buf;if (FLASH_If_Write(&flashdestination, (uint32_t*) ramsource, (uint16_t) packet_length/4) == 0) {Send_Byte(ACK); // 写入成功,确认接收} else {Send_Byte(CA); Send_Byte(CA); // 写入失败,终止会话return -2;}packets_received++; // 数据包序号递增
}
- 关键操作:
- 通过
memcpy
将数据包有效载荷(跳过协议头部)复制到 RAM 缓冲区buf
。 - 调用
FLASH_If_Write
将数据写入 Flash(按 4 字节字对齐,因此长度需除以 4)。 - 写入成功后发送 ACK,失败则发送 CA 终止传输。
- 通过
7. 序列号校验
if ((packet_data[PACKET_SEQNO_INDEX] & 0xff) != (packets_received & 0xff)) {Send_Byte(NAK); // 序列号不匹配,请求重发
}
- 作用:确保数据包按顺序接收(Ymodem 协议使用 8 位序列号,循环 0~255)。
- 机制:若当前数据包序列号与预期不符(
packets_received
),发送 NAK 让发送方重传。
8. 传输结束处理
- 收到 EOT(结束符):
case EOT:return 0; // 结束传输,返回文件大小(后续由上层处理)
- 收到空文件名包:
else {Send_Byte(ACK);file_done = 1;session_done = 1; // 会话结束(Ymodem 支持多文件,空包表示所有文件传输完成) }
错误处理与可靠性设计
- 超时重试:
Receive_Packet
使用NAK_TIMEOUT
控制等待时间,超时后发送 CRC16(请求重传)。- 错误计数
errors
超过MAX_ERRORS
(通常为 10 次)时终止会话。
- Flash 操作保护:
- 写入前擦除目标区域,确保 Flash 可写。
- 写入失败时立即终止,避免部分写入导致固件损坏。
- 协议控制字符处理:
- 支持 CA(发送方终止)、ABORT(用户终止)等异常情况,通过发送双 CA 确认终止。
与 IAP(在应用编程)的关系
- 核心作用:作为 BootLoader 的一部分,通过串口接收新固件数据,写入用户应用区(
APPLICATION_ADDRESS
),实现 OTA(无线升级)。 - 数据流向:
- 串口接收数据 →
packet_data
缓冲区。 - 解析后暂存到
buf
(RAM) → 写入 Flash。
- 串口接收数据 →
- 兼容性:支持 128 字节(SOH)和 1024 字节(STX)数据包,适应不同传输速率和稳定性需求。
总结
Ymodem_Receive
是 STM32 固件升级的核心函数,通过 Ymodem 协议可靠接收文件数据,完成文件名解析、Flash 擦除、数据写入及错误处理。其设计严格遵循嵌入式系统的可靠性要求,确保在不稳定的串口传输中完成固件更新,是 BootLoader 实现 OTA 功能的关键组件。
static int32_t Receive_Packet (uint8_t *data, int32_t *length, uint32_t timeout)
代码解析:Receive_Packet
函数定义与功能概述
static int32_t Receive_Packet (uint8_t *data, int32_t *length, uint32_t timeout)
1. 函数声明与作用
- 函数功能:从串口接收一个完整的 Ymodem 数据包,解析包类型(如数据帧、结束帧、终止帧),并校验序列号和数据完整性。
- 访问权限:
static
表示仅在当前文件(ymodem.c
)内可见,用于内部逻辑封装。 - 参数:
data
:指向接收缓冲区的指针,存储完整的数据包(包括协议头部、有效载荷、校验字段)。length
:指向整数的指针,用于返回有效载荷长度(如 128 或 1024 字节,不包含协议开销)。timeout
:接收超时时间(单位通常为毫秒或滴答数),用于防止无限阻塞。
- 返回值:
0
:正常接收数据包,*length
为有效载荷长度。-1
:超时或数据包错误(如起始符无效、序列号校验失败)。1
:接收到用户终止信号(ABORT1
或ABORT2
)。
2. 核心逻辑解析(按代码执行流程)
步骤 1:接收起始符并判断包类型
uint8_t c;
*length = 0; // 初始化长度为 0
if (Receive_Byte(&c, timeout) != 0) {return -1; // 接收起始符超时,返回错误
}
switch (c) {case SOH: // 128 字节数据包(协议定义 SOH=0x01)packet_size = PACKET_SIZE; // PACKET_SIZE=128break;case STX: // 1024 字节数据包(STX=0x02)packet_size = PACKET_1K_SIZE; // PACKET_1K_SIZE=1024break;case EOT: // 传输结束符(EOT=0x04)return 0; // 无需接收后续数据,直接返回(*length 保持 0)case CA: // 发送方终止信号(CA=0x18,需连续接收两个 CA)if ((Receive_Byte(&c, timeout) == 0) && (c == CA)) {*length = -1; // 标记为发送方终止return 0;} else {return -1; // 仅收到单个 CA,视为错误}case ABORT1: case ABORT2: // 用户终止信号(ABORT1=0x1A,ABORT2=0x4)return 1; // 直接返回用户终止状态default: // 无效起始符(非 SOH/STX/EOT/CA/ABORT)return -1;
}
- 关键判断:
- 通过起始符区分数据包类型(SOH/STX)、控制信号(EOT/CA/ABORT)。
- CA 需连续接收两个以确认终止,避免误判。
步骤 2:接收完整数据包
*data = c; // 存储起始符到缓冲区头部
for (i = 1; i < (packet_size + PACKET_OVERHEAD); i++) {if (Receive_Byte(data + i, timeout) != 0) { // 接收后续字节return -1; // 接收过程中超时,返回错误}
}
- 数据包结构:
- 总长度 =
PACKET_OVERHEAD(5 字节) + packet_size(128/1024 字节)
。 PACKET_OVERHEAD
包含:- 起始符(1 字节,已接收)。
- 序列号(1 字节,
data[1]
)和反码(1 字节,data[2]
)。 - 校验字段(2 字节,尾部)。
- 总长度 =
- 循环作用:填充缓冲区
data
,依次接收序列号、反码、有效载荷、CRC-16 校验和。
步骤 3:序列号校验
if (data[PACKET_SEQNO_INDEX] != ((data[PACKET_SEQNO_COMP_INDEX] ^ 0xff) & 0xff)) {return -1; // 序列号与反码不匹配,数据包错误
}
*length = packet_size; // 有效载荷长度赋值
return 0; // 接收成功
- 校验逻辑:
PACKET_SEQNO_INDEX=1
(序列号位置),PACKET_SEQNO_COMP_INDEX=2
(反码位置)。- 反码应为序列号的按位取反(
~seqno
),通过^ 0xff
实现取反(如0x01 ^ 0xff = 0xfe
)。 - 校验失败说明数据包可能乱序或损坏,拒绝接收。
3. 错误处理与协议控制
- 超时处理:
- 任何阶段调用
Receive_Byte
超时(返回 -1),直接终止接收并返回 -1。 timeout
参数由调用方(如Ymodem_Receive
)指定,通常为NAK_TIMEOUT
(如 1000ms)。
- 任何阶段调用
- 异常信号处理:
- 接收到
EOT
(传输结束)时,直接返回 0,通知上层结束传输。 - 接收到
ABORT1/ABORT2
(用户终止)时,返回 1,由上层处理中断逻辑(如发送CA
确认终止)。
- 接收到
4. 与上层函数的交互
- 在
Ymodem_Receive
中的调用:switch (Receive_Packet(packet_data, &packet_length, NAK_TIMEOUT)) {case 0: // 正常数据包,处理有效载荷case 1: // 用户终止,返回错误码 -3default: // 超时,重试或终止会话 }
- 数据流向:
Receive_Packet
填充packet_data
缓冲区(含协议字段)。- 上层函数解析
packet_data[1]
(序列号)、packet_data+3
(有效载荷起始)、packet_data+3+packet_length
(CRC 校验字段)。
5. 设计考量与可靠性
- 字节级接收循环:逐个字节接收,确保每个数据包完整,避免缓冲区溢出(通过
packet_size + PACKET_OVERHEAD
限制接收长度)。 - 双校验机制:
- 序列号校验(防乱序):确保数据包按顺序接收。
- CRC-16 校验(在
Ymodem_Receive
中处理):后续通过校验字段验证数据完整性。
- 状态机适配:返回不同状态码(0/-1/1),配合上层循环实现重试逻辑(如超时后发送
CRC16
请求重传)。
总结
Receive_Packet
是 Ymodem 协议的底层核心函数,负责 可靠接收单个数据包,通过起始符解析包类型、校验序列号,并处理异常控制信号。其设计严格遵循协议规范,为上层文件接收逻辑(如 Ymodem_Receive
)提供了稳定的数据包读取接口,是实现串口固件升级(IAP)的关键组件。
1. 用户触发升级:等待命令输入
核心函数:GetInputString
、GetKey
-
流程:
设备启动后,通过串口等待用户输入命令(比如输入A
开始升级)。GetInputString
会循环读取用户输入的字符(支持退格删除),直到用户按下回车(\r
)。- 输入的命令会显示在终端(比如输入
update
或对应的触发字符),程序解析后进入升级流程。
-
类比:
就像你在手机上打开 “系统更新” APP,点击 “检查更新” 按钮,设备开始等待你的操作。
2. 串口数据接收:检测与读取字符
核心函数:SerialKeyPressed
-
流程:
当用户通过串口(比如 USB 转串口工具)发送升级文件时,SerialKeyPressed
会实时检测串口是否收到数据:- 通过
__HAL_UART_GET_FLAG
检查串口接收缓冲区是否有数据(UART_FLAG_RXNE
标志位)。 - 如果有数据,从串口数据寄存器
DR
中读取字符(用0x01FF
掩码确保只取有效字节),并返回1
表示收到数据。
- 通过
-
类比:
相当于快递员不断查看邮箱,一旦有新包裹(数据)就马上取出来。
3. 阻塞等待数据:确保接收完整
核心函数:GetKey
-
流程:
GetKey
会调用SerialKeyPressed
,并在一个while(1)
循环中阻塞,直到真正收到字符才返回。- 这一步确保程序不会漏掉用户输入的任何一个字符(比如升级开始的触发键)。
-
类比:
就像你在 ATM 机上插卡后,必须等输入密码才能继续,程序必须等到用户 “按下确认键” 才会继续执行升级。
4. 数据校验:确保传输正确
依赖函数:Cal_CRC16
、UpdateCRC16
(在 ymodem.c
中)
-
流程:
升级文件通过 Ymodem 协议分块传输,每块数据都会计算 CRC16 校验值:UpdateCRC16
对每个字节计算校验值,Cal_CRC16
对整块数据生成最终校验码。- 接收方收到数据后重新计算校验码,与发送方的校验码对比,确保数据没传错。
-
类比:
就像你网购时拆包裹前检查快递单上的条形码,确认货物没有破损或发错。
5. 写入闪存:更新程序存储区
依赖函数:flash_if.c
中的闪存操作函数(如擦除、写入)
-
流程:
当确认数据正确后,通过flash_if
库函数将数据写入 STM32 的闪存(程序存储区):- 先擦除目标闪存扇区(类似格式化 U 盘),再按页写入数据(每页 256 字节)。
- 写入过程中通过
SerialPutString
输出进度(比如Erasing sector...
),让用户知道升级进度。
-
类比:
相当于在电脑上安装软件,把下载好的安装包数据写入硬盘指定位置。
6. 升级完成:重启设备生效
隐藏逻辑:在 menu.c
或主函数中
-
流程:
所有数据写入完成后,程序通过复位函数让设备重启,新的程序会从闪存新地址开始运行。- 升级过程中如果出现错误(如校验失败、闪存写入失败),会通过
SerialPutString
提示错误,并保持旧程序运行,避免设备变砖。
- 升级过程中如果出现错误(如校验失败、闪存写入失败),会通过
-
类比:
就像手机更新系统后自动重启,开机后直接运行新版本系统。
整个流程总结(一句话版):
- 用户通过串口输入升级命令 → 2. 程序检测到命令后,通过串口接收升级文件 → 3. 每收到一块数据就校验是否正确 → 4. 正确的数据写入闪存 → 5. 所有数据写完后重启设备,新程序生效。
关键函数的 “分工”:
SerialKeyPressed
/GetKey
:负责 “听” 用户的操作(接收字符)。GetInputString
:负责 “理解” 用户的命令(解析输入的字符串)。Cal_CRC16
:负责 “检查” 数据是否传错(校验数据完整性)。flash_if
函数:负责 “写入” 新程序到芯片(操作闪存)。SerialPutString
:负责 “反馈” 进度给用户(打印提示信息)。
这样一来,即使设备没有屏幕,也能通过串口终端完成升级,就像给芯片 “远程安装软件” 一样!
细节
1. USART数据寄存器的位宽设计
STM32的USART数据寄存器(USART_DR)是一个9位寄存器,其有效数据位为低9位(Bit 0~Bit 8)。
- 当数据字长为8位时:仅Bit 0~Bit 7有效,Bit 8未使用。
- 当数据字长为9位时:Bit 0~Bit 8均有效(例如奇偶校验模式或协议中定义的特殊控制位)。
掩码0x01FF
的二进制形式为 0000 0001 1111 1111
,其作用是:
- 保留全部9位数据:确保无论是8位还是9位模式,都能正确读取完整的有效数据位。
- 屏蔽高7位无效位:USART_DR寄存器的高23位(Bit 9~Bit 31)在STM32中未使用,需要屏蔽以避免干扰。
2. Ymodem协议的兼容性需求
Ymodem协议虽然默认使用8位数据,但在某些实现中会通过第9位(Bit 8)传递协议控制信息(如包序号、校验状态等)。
- 示例场景:Ymodem的包序号字段可能占用多个位,若仅用
0xFF
(仅保留8位),会导致第9位被截断,破坏协议逻辑。 - STM32F411的硬件支持:该芯片的USART模块支持9位字长(通过USART_CR1寄存器的M位配置),因此需保留第9位以适配协议扩展需求。
3. 代码中的具体实现
在代码段*key = (uint16_t)((&huart1)->Instance->DR & (uint16_t)0x01FF)
中:
- 操作目的:从USART_DR寄存器读取接收到的数据,并确保仅保留有效位。
- 使用
0x01FF
而非0xFF
:0xFF
(二进制0000 0000 1111 1111
)仅保留低8位,会丢失第9位数据。0x01FF
(二进制0000 0001 1111 1111
)保留全部9位数据,兼容8位和9位模式。
总结
掩码值 | 适用场景 | 数据完整性保障 |
---|---|---|
0xFF | 仅需8位数据的场景 | 可能丢失第9位 |
0x01FF | 需要兼容9位数据的场景 | 完整保留9位 |
在Ymodem协议实现中,使用0x01FF
可确保协议控制字段和校验信息的完整性,同时适配STM32硬件特性。这一设计是STM32 USART模块与通信协议深度匹配的典型体现。
参考:
No-Chicken/OV-Watch: A powerful Smart Watch based on STM32, FreeRTOS, LVGL.