当前位置: 首页 > news >正文

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 字节对齐,否则会导致错误。
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)。

三、完整操作流程(以固件升级为例)

  1. 初始化:调用 FLASH_If_Init 解锁 Flash 并清状态。
  2. 检查写保护:如果用户区域被保护,先调用 FLASH_If_DisableWriteProtection 解除。
  3. 擦除旧固件:调用 FLASH_If_Erase 擦除整个用户区域(为写入新固件腾空间)。
  4. 写入新固件:通过 Ymodem 协议接收数据,每收到一包数据,调用 FLASH_If_Write 写入 Flash(按字写入并验证)。
  5. 错误处理:任何步骤失败(如擦除超时、写入验证不通过),返回错误码,终止升级。

四、关键注意事项

  1. Flash 特性限制

    • 写入前必须擦除(擦除后全为 0xFF,写入是将 0xFF 改为具体数据)。
    • 擦除以扇区为单位(如扇区 0 是 16KB,扇区 4 是 64KB,具体看芯片手册)。
    • 写入必须 32 位对齐,且不能超过用户区域地址(USER_FLASH_END_ADDRESS)。
  2. 与 Ymodem 协议的配合

    • 在 Ymodem_Receive 函数中,收到文件后先擦除用户区域(FLASH_If_Erase),再逐包写入(调用 FLASH_If_Write)。
    • 写入地址由 APPLICATION_ADDRESS 开始(如 0x0800C000),确保不覆盖 Bootloader 区域(前 48KB 通常留给 Bootloader)。
  3. 错误处理的重要性

    • 每次 Flash 操作后必须检查状态(如擦除是否成功,写入是否验证通过),否则可能导致固件损坏,设备无法启动。

总结

flash_if.c 是 Flash 操作的 “底层工具箱”,为 Ymodem 传输提供了可靠的硬件操作支持。核心逻辑围绕 “擦除 - 写入 - 验证” 三大步骤,确保固件升级过程中数据的正确性和安全性。理解这些函数的作用,就能明白 IAP 如何通过串口等方式安全地更新设备固件,就像给手机 “无线升级系统” 一样,每一步都需要严格的擦写和验证。

第一段代码:禁用 Flash 写保护(FLASH_OB_DisableWRP

功能

关闭指定 Flash 存储区的写保护,允许后续对这些区域进行擦除或编程操作。
(写保护是一种安全机制,防止误操作修改 Flash 数据,类似 U 盘的写保护开关)

核心逻辑拆解
  1. 参数检查

    assert_param(IS_OB_WRP_SECTOR(WRPSector)); // 检查要禁用保护的扇区是否合法
    assert_param(IS_FLASH_BANK(Banks)); // 检查目标存储区(Bank1/Bank2)是否合法
    
     
    • 确保输入的扇区编号和存储区(如 Bank1)是芯片支持的有效值,避免无效操作。
  2. 等待上次操作完成

    status = FLASH_WaitForLastOperation(50000); // 等待最长50ms
    
     
    • Flash 操作(如擦除、编程)需要时间,必须等上一次操作完成才能开始新操作,否则会出错。
    • 50000是超时时间(单位:微秒),防止代码卡死在等待中。
  3. 修改选项字节(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)

核心逻辑拆解
  1. 参数检查

    assert_param(IS_FLASH_ADDRESS(Address)); // 检查地址是否在Flash有效范围内
    
     
    • 确保写入地址是芯片 Flash 区域的合法地址,避免操作到 RAM 或外设区域。
  2. 配置 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 后允许执行编程操作。
  3. 直接写入数据到硬件地址

    *(__IO uint32_t*)Address = Data;
    
     
    • 硬件原理:Flash 存储区被映射到 CPU 的地址空间,直接向地址写入数据会触发硬件编程操作。
    • 代码操作
      • (__IO uint32_t*)Address将 32 位地址值转换为 32 位指针,*解引用指针,相当于 “找到这个地址对应的存储单元”。
      • __IO确保每次操作都真实作用于硬件,避免编译器优化(比如缓存数据不写入硬件)。
通俗比喻

好比在书架(Flash)的指定格子(地址)放一本书(数据):先告诉管理员(Flash 控制器)要放的书大小(32 位),允许放书(使能编程),然后直接把书塞进格子(写入地址)。

两段代码的共同关键点

  1. 直接操作硬件地址
    通过指针强制类型转换(如(__IO uint32_t*)Address)操作内存映射的硬件寄存器 / Flash 存储区,这是嵌入式开发的常见操作,必须严格按芯片手册的地址和数据格式操作。

  2. volatile 的作用
    __IO(即volatile)告诉编译器:“这个变量的值可能随时被硬件改变,不要优化我的操作,每次都要真实读写硬件!”,避免因编译器优化导致操作失效。

  3. 严格的时序和状态检查
    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 协议控制字符(如 ACKNAKCA),或向用户显示单个字符(如菜单提示)。
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:字符串转整数(支持十进制、十六进制、单位后缀)
  • 功能:将用户输入的字符串(如 12340x1A10k)转换为整数
  • 处理逻辑
    • 十六进制:以 0x 开头,解析每位十六进制字符(0-9A-Fa-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 写入),但它是整个系统的 “神经系统”:

  1. 人机交互桥梁:通过串口发送和接收数据,让用户能通过菜单操作、查看提示
  2. 数据处理基石:字符串与整数的转换确保文件大小、地址等关键信息正确解析
  3. 底层兼容性:封装 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 如何 “保障可靠传输”?

  1. 分包策略:大文件拆分为 1024 字节(STX)或 128 字节(SOH)的数据包,平衡传输效率和错误处理成本
  2. 校验机制:通过 CRC16(默认)或 Checksum 检测数据错误,确保写入 Flash 的程序完整无误
  3. 错误重试:对 NAK(重发请求)和超时错误,最多重试 10 次,避免偶然干扰导致传输失败
  4. 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。
  • 流程拆解
    1. 擦除标志扇区

      uint32_t flashdestination = ADDR_FLASH_SECTOR_2;
      FLASH_If_Erase_One_Sector(2U); // 擦除扇区2(用于存储“程序已准备好”的标志)
      
       
      • Flash 写入前必须擦除(擦除后内容为全 1),否则无法写入 0。
    2. 等待文件传输

      SerialPutString("Waiting for the file to be sent ... (press 'a' to abort)\n\r");
      Size = Ymodem_Receive(&tab_1024[0]); // 调用 Ymodem 协议接收文件
      
       
      • Ymodem_Receive 负责处理串口数据的分包接收、校验、写入 Flash。
    3. 处理接收结果

      • 成功(Size > 0):显示文件名、文件大小,并在 Flash 中写入 “APP FLAG” 标志(标记程序可运行)。
      • 失败:根据错误码显示原因(如文件太大、验证失败、用户取消等)。
❷ 选项 2:上传程序(代码中禁用)
SerialPutString("This function is disabled! Please Use 1\r"); // 直接提示用户不可用

  • 原因:可能出于安全考虑(防止程序被非法读取)或功能未完善。
❸ 选项 3:运行新程序
  • 核心操作:让单片机从当前的 IAP 程序(引导程序)跳转到新下载的用户程序。
  • 关键步骤
    1. 关闭系统定时器
      SysTick->CTRL = 0X00; // 禁用 SysTick 定时器,避免干扰跳转过程
      
    2. 获取新程序入口地址
      JumpAddress = *(__IO uint32_t*)(APPLICATION_ADDRESS + 4); 
      
       
      • 用户程序的入口地址存储在 Flash 的固定位置(APPLICATION_ADDRESS 是程序起始地址,+4 是向量表偏移)。
    3. 设置栈指针并跳转
      __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 如何 “串联” 整个系统?

  1. 用户交互层:通过串口菜单降低操作门槛,用户无需了解底层协议和硬件细节,只需输入数字即可完成操作。
  2. 功能调度层:调用 ymodem.c(文件传输)和 flash_if.c(Flash 操作),将复杂的硬件操作封装成简单的选项。
  3. 安全保障层:检测 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 扇区划分通常如下(具体地址需结合芯片型号和项目定义):

扇区编号起始地址大小用途(本例)
00x0800000016 KB引导程序(IAP)存储区
10x0800400016 KB保留或临时数据区
20x0800800016 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}
}
  • 目的
    1. 清除旧数据:确保目标扇区(用户应用程序存储区)在写入新文件前是空白的(全 0xFF)。
    2. 准备写入空间: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 地址。
  • 参数解析
    1. &flashdestination:目标写入地址(指针)
      • flashdestination 在函数开头定义为 ADDR_FLASH_SECTOR_2(即扇区 2 起始地址 0x08008000),指向用户程序存储区的起始位置。
    2. (uint32_t*) APP_FLAG:待写入的数据(强制转换为 32 位无符号整数指针)
      • APP_FLAG 是用户定义的字符串 const char *str_flag = "APP FLAG";,但此处通过 (uint32_t*) 转换为 32 位整数指针,实际写入的是该字符串的起始地址(或其指向的内容,需结合具体实现)。
    3. 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. 与前后代码的联动

  • 前置条件
    1. 目标扇区 2 已通过 FLASH_If_Erase_One_Sector(2U) 擦除,确保写入区域为全 0xFFFFFFFF(可被编程为 0)。
    2. 通过 Ymodem 协议接收的文件数据已存储在缓冲区 tab_1024 中(后续可能通过循环写入整个文件,此处先写入标志)。
  • 后续影响
    写入的标志数据(如 "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) 将 JumpAddressuint32_t 类型的地址值)转换为函数指针,允许直接通过函数调用的方式跳转到目标地址:

Jump_To_Application();  // 调用函数指针,执行用户程序

  • ARM 架构的兼容性
    Cortex-M 处理器支持直接通过函数指针跳转,前提是目标地址是 字对齐的(最低两位为 0b00)。由于用户程序的入口地址由编译器生成,必然满足字对齐要求,因此转换是安全的。

4. 跳转后的执行流程

调用 Jump_To_Application() 后,程序会:

  1. 跳转到用户程序的入口地址(如 Reset_Handler)。
  2. 执行用户程序的启动代码(初始化堆栈、全局变量等)。
  3. 进入用户主函数(如 main)开始正常运行。

  • 与 IAP 引导程序的关系
    跳转后,IAP 引导程序的代码不再执行,用户程序完全接管系统。若用户程序需要重新进入 IAP(如后续升级),通常需要通过特定机制(如按键触发、通信协议)重新跳转回引导程序地址。

5. 潜在风险与注意事项

  1. 地址有效性校验
    代码未检查 JumpAddress 是否为合法地址(如是否在用户程序存储区范围内),若用户程序未正确下载或 Flash 数据损坏,跳转可能导致硬件错误(如取指异常)。

  2. 中断与定时器状态
    跳转前关闭了所有中断和 SysTick 定时器,用户程序需在启动代码中重新初始化中断系统和定时器,否则可能导致功能异常。

  3. 向量表重定位
    用户程序的向量表默认位于 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的协议规范)

​关键协议特性实现​

  1. ​ACK/NAK机制​​:
    • Send_Byte(ACK):确认数据包正确接收
    • Send_Byte(NAK):请求重传错误包(如CRC校验失败)
  2. ​多文件支持​​:
    • 外层循环通过session_done控制是否继续接收新文件
  3. ​传输完整性​​:
    • 序号校验 + CRC校验双重保障(参考网页3的CRC计算代码)
  4. ​Flash保护​​:
    • 文件大小预校验(USER_FLASH_SIZE
    • 擦除-写入分离操作(符合嵌入式Flash操作规范)

​典型执行流程​

  1. 接收方发送字符C启动传输(参考网页8的握手流程)
  2. 接收文件名和大小信息包(包序号0)
  3. 分块接收数据包(包序号1~N),写入Flash
  4. 接收结束包(包序号0),发送最终ACK
  5. 跳转到新固件地址执行(代码未展示,需在外部实现)

​应用场景扩展建议​

  1. ​断电保护​​:记录最后成功写入的包号,支持断点续传(参考网页3的流式传输)
  2. ​数字签名​​:在文件头添加固件签名,升级前验证合法性
  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); // 确认接收文件名和大小
}

  • 核心步骤
    1. 从数据包中提取文件名(存储到全局变量 FileName)和文件大小。
    2. 校验文件大小是否超过目标 Flash 容量(USER_FLASH_SIZE 定义 Flash 总大小)。
    3. 擦除目标 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++; // 数据包序号递增
}

  • 关键操作
    1. 通过 memcpy 将数据包有效载荷(跳过协议头部)复制到 RAM 缓冲区 buf
    2. 调用 FLASH_If_Write 将数据写入 Flash(按 4 字节字对齐,因此长度需除以 4)。
    3. 写入成功后发送 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 支持多文件,空包表示所有文件传输完成)
    }
    

错误处理与可靠性设计

  1. 超时重试
    • Receive_Packet 使用 NAK_TIMEOUT 控制等待时间,超时后发送 CRC16(请求重传)。
    • 错误计数 errors 超过 MAX_ERRORS(通常为 10 次)时终止会话。
  2. Flash 操作保护
    • 写入前擦除目标区域,确保 Flash 可写。
    • 写入失败时立即终止,避免部分写入导致固件损坏。
  3. 协议控制字符处理
    • 支持 CA(发送方终止)、ABORT(用户终止)等异常情况,通过发送双 CA 确认终止。

与 IAP(在应用编程)的关系

  • 核心作用:作为 BootLoader 的一部分,通过串口接收新固件数据,写入用户应用区(APPLICATION_ADDRESS),实现 OTA(无线升级)。
  • 数据流向
    1. 串口接收数据 → packet_data 缓冲区。
    2. 解析后暂存到 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 字节,已接收)。
      2. 序列号(1 字节,data[1])和反码(1 字节,data[2])。
      3. 校验字段(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: // 超时,重试或终止会话
    }
    
  • 数据流向
    1. Receive_Packet 填充 packet_data 缓冲区(含协议字段)。
    2. 上层函数解析 packet_data[1](序列号)、packet_data+3(有效载荷起始)、packet_data+3+packet_length(CRC 校验字段)。
5. 设计考量与可靠性
  • 字节级接收循环:逐个字节接收,确保每个数据包完整,避免缓冲区溢出(通过 packet_size + PACKET_OVERHEAD 限制接收长度)。
  • 双校验机制
    1. 序列号校验(防乱序):确保数据包按顺序接收。
    2. CRC-16 校验(在 Ymodem_Receive 中处理):后续通过校验字段验证数据完整性。
  • 状态机适配:返回不同状态码(0/-1/1),配合上层循环实现重试逻辑(如超时后发送 CRC16 请求重传)。

总结

Receive_Packet 是 Ymodem 协议的底层核心函数,负责 可靠接收单个数据包,通过起始符解析包类型、校验序列号,并处理异常控制信号。其设计严格遵循协议规范,为上层文件接收逻辑(如 Ymodem_Receive)提供了稳定的数据包读取接口,是实现串口固件升级(IAP)的关键组件。

1. 用户触发升级:等待命令输入

核心函数:GetInputStringGetKey
  • 流程
    设备启动后,通过串口等待用户输入命令(比如输入 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_CRC16UpdateCRC16(在 ymodem.c 中)
  • 流程
    升级文件通过 Ymodem 协议分块传输,每块数据都会计算 CRC16 校验值:

    • UpdateCRC16 对每个字节计算校验值,Cal_CRC16 对整块数据生成最终校验码。
    • 接收方收到数据后重新计算校验码,与发送方的校验码对比,确保数据没传错。
  • 类比
    就像你网购时拆包裹前检查快递单上的条形码,确认货物没有破损或发错。

5. 写入闪存:更新程序存储区

依赖函数:flash_if.c 中的闪存操作函数(如擦除、写入)
  • 流程
    当确认数据正确后,通过 flash_if 库函数将数据写入 STM32 的闪存(程序存储区):

    • 先擦除目标闪存扇区(类似格式化 U 盘),再按页写入数据(每页 256 字节)。
    • 写入过程中通过 SerialPutString 输出进度(比如 Erasing sector...),让用户知道升级进度。
  • 类比
    相当于在电脑上安装软件,把下载好的安装包数据写入硬盘指定位置。

6. 升级完成:重启设备生效

隐藏逻辑:在 menu.c 或主函数中
  • 流程
    所有数据写入完成后,程序通过复位函数让设备重启,新的程序会从闪存新地址开始运行。

    • 升级过程中如果出现错误(如校验失败、闪存写入失败),会通过 SerialPutString 提示错误,并保持旧程序运行,避免设备变砖。
  • 类比
    就像手机更新系统后自动重启,开机后直接运行新版本系统。

整个流程总结(一句话版)

  1. 用户通过串口输入升级命令 → 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.

相关文章:

  • 【Part 2安卓原生360°VR播放器开发实战】第一节|通过传感器实现VR的3DOF效果
  • Milvus(1):什么是 Milvus
  • 21. git apply
  • 大模型技术解析与应用 | 大语言模型:从理论到实践(第2版)| 复旦大学 | 533页
  • 深度学习方向急出成果,是先广泛调研还是边做实验边优化?
  • springboot自动装配的原理
  • 修改PointLIO项目
  • RHCSA知识点
  • 2025-4-19 情绪周期视角复盘(mini)
  • Linux命令--将控制台的输入写入文件
  • C语言之高校学生信息快速查询系统的实现
  • RocketMQ实现基于可靠消息的最终一致性
  • electron打包是没有正确生成electron.exe,x ENOENT: no such file or directory, rename:
  • 位运算---总结
  • 微信小程序上传腾讯云
  • Dubbo QoS操作手册
  • 【网工第6版】第4章 无线通信网
  • 肖特基二极管详解:原理、作用、应用与选型要点
  • 分布式入门
  • Tailwindcss 入门 v4.1
  • 印巴局势紧张或爆发军事冲突,印度空军能“一雪前耻”吗?
  • 体坛联播|欧冠巴萨3比3战平国米,柯洁未进入国家集训队
  • 八成盈利,2024年沪市主板公司实现净利润4.35万亿元
  • 中国海警位中国黄岩岛领海及周边区域执法巡查
  • 融创服务全面退出彰泰服务集团:约8.26亿元出售广西彰泰融创智慧80%股权
  • 坚持科技创新引领,赢得未来发展新优势