<Booterloader学习第二期>串口和Flash
串口通信
基础的串口交互
代码用于while循环中,代码需要挂起等待,有缓冲区,缓冲区有限,容易丢字节
//定义一个缓冲区
uint_8t recive_buff[16]={0};
while(1)
{
// 1: 串口编号 句柄 2: 数据 3: 数据长度 4.超时时间
//接收
HAL_UART_Receive(&huart1, receive_buff, 16, HAL_MAX_DELAY);
//判断是否有数据
if (strlen((char *)receive_buff) > 0)
{
HAL_UART_Transmit(&huart1, receive_buff, 16, 1000);
//清空为0
memset(receive_buff, 0, 16);
}
基础的串口交互优化
利用空闲帧,支持任意串口数据接收回显,特殊函数->挂起等待->idle空闲帧模式->一个地址来存储实际字节长度
缓冲区大小不足,会造成单次接收数据丢失,常用的单次接收缓冲区是256字节
// 接收缓冲区(16字节,初始化为0)
uint8_t receive_buff[16] = {0};
// 实际接收的数据长度
uint16_t receive_len = 0;
// 串口空闲接收:接收数据直到总线空闲,最大16字节
HAL_UARTEx_ReceiveToIdle(&huart1, receive_buff, 16, &receive_len, HAL_MAX_DELAY);
// 判断:成功接收到数据
if(receive_len > 0)
{
// 直接用实际接收长度发送,杜绝strlen越界风险
HAL_UART_Transmit(&huart1, receive_buff, receive_len, 1000);
// 清空缓冲区 + 重置长度,为下一次接收做准备
memset(receive_buff, 0, sizeof(receive_buff));
receive_len = 0;
}
串口交互中断
此函数是以空闲中断作为接收完成的标志,而不是接收长度,因此可以接收任意长度的数据
__HAL_UART_CLEAR_IDLEFLAG(&huart1)
清除空闲中断标志:防止初始化时误触发空闲中断,保证程序稳定
__HAL_UART_CLEAR_OREFLAG(&huart1) 清除串口溢出标志:如果接收速度过快,串口会产生溢出,不清除会导致接收卡死,必须加
/**
* @brief 串口空闲中断回调函数
* @param huart 串口句柄
* @param Size 实际接收的数据长度
*/
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
// 判断是否为串口1
if(huart->Instance == USART1)
{
// 保存实际接收长度
usart_rec_len = Size;
// 将数据原样回发
HAL_UART_Transmit_IT(&huart1, usart_rec_buff, usart_rec_len);
// ============= 清除标志 + 重新开启下一次接收 =============
__HAL_UART_CLEAR_OREFLAG(&huart1); // 清除溢出标志
__HAL_UART_CLEAR_IDLEFLAG(&huart1); // 清除空闲标志
HAL_UARTEx_ReceiveToIdle_IT(&huart1, usart_rec_buff, LEN_R);
}
}
串口交互DMA
由于是不定长数据的接收,因此缓冲区大小可以根据实际需求调整,只能大不能小,否则可能会丢失数据
// 不定长数据接收完成回调函数
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART1)
{
// 使用DMA将接收到的数据发送回去
HAL_UART_Transmit_DMA(&huart1, rx_data, Size);
// 重新启动接收,使用Ex函数,接收不定长数据
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_data, sizeof(rx_data));
// 关闭DMA传输过半中断(HAL库默认开启,但我们只需要接收完成中断)
__HAL_DMA_DISABLE_IT(huart1.hdmarx, DMA_IT_HT);
}
}
Flash的交互

Flash的擦除
**Flash 的写入规则是 “先擦除,再写入”,核心原因是:Flash 存储单元只能从 1 变为 0,无法直接从 0 变回 1。
擦除单位:通常按 页(Page) 擦除(也可执行整片擦除),擦除会清空整个页的所有数据。
检查页内字节是否为0xFF,如果是的话就擦除,计算该字节的起始地址->地址 - (地址 % 页大小)
/**
* 智能擦除Flash:检查写入区域是否已有数据,有则擦除整个页
*/
static void Int_flash_erase(void)
{
uint8_t need_erase = 0; // 是否需要擦除:1要,0不要
uint32_t PAGE_addr = 0; // 需要擦除的页起始地址
// 检查从写入起始地址开始的 usart_len 个字节,看是否有非0xFF的数据
for (uint8_t i = 0; i < usart_len; i++)
{
// 读取Flash中对应位置的数据
uint8_t data = *(volatile uint8_t*)(APP_START_ADDR + flash_write_offset + i);
if (data != 0xFF) // 只要有一个字节不是0xFF(说明已被写过)
{
need_erase = 1; // 标记需要擦除
// 计算出该字节所在的页起始地址(页对齐)
PAGE_addr = (APP_START_ADDR + flash_write_offset + i) -
((APP_START_ADDR + flash_write_offset + i) % FLASH_PAGE_SIZE);
break; // 找到一个就够了,不用继续查
}
}
// 如果需要擦除,就擦除整个页
if (need_erase)
{
FLASH_EraseInitTypeDef erase_init; // 擦除配置结构体
erase_init.TypeErase = FLASH_TYPEERASE_PAGES; // 按页擦除
erase_init.PageAddress = APP_START_ADDR; // 要擦除的页地址(这里固定从APP_START_ADDR开始,有问题)
erase_init.NbPages = 1; // 擦除1页
uint32_t page_error = 0; // 记录出错页(未使用)
HAL_FLASHEx_Erase(&erase_init, &page_error); // 执行擦除
}
}
Flash的写入
写入前:必须优先执行解锁操作,解除写保护,否则擦写指令会被硬件拒绝。
写入完成后:重新执行上锁操作,恢复 Flash 保护状态,避免程序运行中被意外修改。
支持的单次写入位宽:仅支持 16 位(半字)、32 位(字)、64 位(双字) 写入,不支持直接按单字节写入。
字节序:默认采用 小端序(Little-Endian)存储,即数据的低字节存放在低地址,高字节存放在高地址
如果没有字节遗留的情况下直接按16位写入
// 处理没有遗留字节的情况,直接按16位写入
static void Int_flash_write_no_last(void)
{
// 正好能够写入 => 不再有遗留的字节 0 6
for (uint16_t i = 0; i < usart_len; i += 2)
{
uint32_t flash_addr = APP_START_ADDR + i + flash_write_offset;
uint16_t data16;
if (i + 1 < usart_len)
{
data16 = usart_rec_buff[i] | (usart_rec_buff[i + 1] << 8);
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, flash_addr, data16);
}
}
}
处理有遗留字节的情况,使用上次遗留的字节和当前数据的第一个字节组成16位数据写入Flash
// 处理有遗留字节的情况,使用上次遗留的字节和当前数据的第一个字节组成16位数据写入Flash
static void Int_flash_write_with_last(void)
{
for(uint16_t i=0;i<usart_len;i+=2)
{
uint32_t flash_addr = APP_START_ADDR + flash_write_offset+i;
uint16_t data16;
if(i==0) // 第一个16位数据,使用上次遗留的字节和当前数据的第一个字节组成
{
//拼接上一次字节
data16 = last_byte |(usart_rec_buff[i] << 8) ; // 上次遗留字节在高位,当前第一个字节在低位
}
else
{
data16 = usart_rec_buff[i-1] | (usart_rec_buff[i] << 8); // 低字节在前,高字节在后
}
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, flash_addr, data16);
}
}
奇偶长度+尾字节控制,总长度 = 当前接收长度 + 尾字节(0/1),
🔹 总长度 = 偶数
有遗留 (flag=1) → 拼接写入 → offset+=(len+1) → flag=0
无遗留 (flag=0) → 直接写入 → offset+=len → flag=0
🔹 总长度 = 奇数
有遗留 (flag=1) → 拼接写入 → 缓存最后 1 字节 → offset+=len → flag=1
无遗留 (flag=0) → 直接写入 → 缓存最后 1 字节 → offset+=(len-1) → flag=1
static void Int_flash_write_halfword(void)
{
// 本次之后没剩余:总字节数为偶数(无遗留)
if ((uart_rec_len + last_byte_flag) % 2 == 0)
{
if (last_byte_flag)
{
// 有剩余 数据长度是奇数 5 => 这次需要作为第一个字节写入
Int_flash_write_with_last();
// 2.4 记录偏移量
flash_write_offset += uart_rec_len + 1;
}
else
{
// 无剩余 数据长度是偶数 => 不再有遗留的字节 0 6
Int_flash_write_no_last();
// 2.4 记录偏移量
flash_write_offset += uart_rec_len;
}
last_byte_flag = 0; // 清除遗留标志
}
else // 本次之后有剩余:总字节数为奇数(遗留1字节)
{
if (last_byte_flag)
{
// 有剩余 数据长度是偶数
Int_flash_write_with_last();
// 修改最后剩下的字节
last_byte = uart_rec_buff[uart_rec_len - 1];
// 2.4 记录偏移量
flash_write_offset += uart_rec_len;
}
else
{
// 上次没有遗留字节 这次会留下一个
Int_flash_write_no_last();
last_byte = uart_rec_buff[uart_rec_len - 1];
// 2.4 记录偏移量
flash_write_offset += uart_rec_len - 1;
}
last_byte_flag = 1; // 设置遗留标志
}
}
回调函数:HAL_UARTEx_RxEventCallback (),接收数据 → 记录长度 → 解锁 Flash → 擦除 → 写入 → 锁 Flash → 清缓存 → 重启接收
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if(huart->Instance==USART1)
{
usart_len=Size;
sum+=usart_len;
HAL_FLASH_Unlock();
Int_flash_erase();
Int_flash_write_halfword();
HAL_FLASH_Lock();
memset(usart_rec_buff,0,BOOTLOADER_UART_BUFF);
__HAL_UART_CLEAR_OREFLAG(&huart1);
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
HAL_UARTEx_ReceiveToIdle_IT(&huart1,usart_rec_buff,BOOTLOADER_UART_BUFF);
}
}
Flash的初始化
在初始化内擦除十页,减少运行时间,防止丢失串口数据
void int_bootloader(void)
{
HAL_FLASH_Unlock();
FLASH_EraseInitTypeDef erase_init; // Flash擦除配置结构体
erase_init.TypeErase = FLASH_TYPEERASE_PAGES; // 擦除模式:按页擦除
erase_init.PageAddress = APP_START_ADDR; // 要擦除的页起始地址
erase_init.NbPages = 10; // 只擦除10页
uint32_t page_error = 0; // 擦除错误码
HAL_FLASHEx_Erase(&erase_init, &page_error); // 执行擦除
HAL_FLASH_Lock();
__HAL_UART_CLEAR_OREFLAG(&huart1);
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
HAL_UARTEx_ReceiveToIdle_IT(&huart1,usart_rec_buff,BOOTLOADER_UART_BUFF);
}
Flash的完整代码
#include "int_bootloader.h"
uint8_t usart_rec_buff[BOOTLOADER_UART_BUFF]={0};
uint16_t usart_len=0;
volatile int sum = 0;
uint32_t flash_write_offset=0;
//末尾可能出现的单字节
uint8_t last_byte_flag = 0;
uint8_t last_byte = 0;
static void Int_flash_write_no_last(void);
static void Int_flash_write_with_last(void);
static void Int_flash_write_halfword(void);
static void Int_flash_erase(void)
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if(huart->Instance==USART1)
{
usart_len=Size;
sum+=usart_len;
HAL_FLASH_Unlock();
Int_flash_erase();
Int_flash_write_halfword();
HAL_FLASH_Lock();
memset(usart_rec_buff,0,BOOTLOADER_UART_BUFF);
__HAL_UART_CLEAR_OREFLAG(&huart1);
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
HAL_UARTEx_ReceiveToIdle_IT(&huart1,usart_rec_buff,BOOTLOADER_UART_BUFF);
}
}
void int_bootloader(void)
{
HAL_FLASH_Unlock();
FLASH_EraseInitTypeDef erase_init; // Flash擦除配置结构体
erase_init.TypeErase = FLASH_TYPEERASE_PAGES; // 擦除模式:按页擦除
erase_init.PageAddress = APP_START_ADDR; // 要擦除的页起始地址
erase_init.NbPages = 10; // 只擦除10页
uint32_t page_error = 0; // 擦除错误码
HAL_FLASHEx_Erase(&erase_init, &page_error); // 执行擦除
HAL_FLASH_Lock();
__HAL_UART_CLEAR_OREFLAG(&huart1);
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
HAL_UARTEx_ReceiveToIdle_IT(&huart1,usart_rec_buff,BOOTLOADER_UART_BUFF);
}
int fputc(int ch, FILE *f)
{
// 将字符发送到USART1
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
static void Int_flash_erase(void)
{
uint8_t need_erase = 0; // 擦除标志:1=需要擦除页,0=不需要
uint32_t PAGE_addr = 0; // 待擦除的Flash页地址
// ===================== 2. 检测当前Flash页是否需要擦除 =====================
for(uint8_t i=0;i<usart_len;i++)
{
uint8_t data=*(volatile uint8_t*)(APP_START_ADDR+flash_write_offset+i);
if(data!=0xFF) // 只要有一个字节非0xFF,就需要擦除,直接退出循环
{
need_erase=1;
PAGE_addr=(APP_START_ADDR+flash_write_offset+i)-((APP_START_ADDR+flash_write_offset+i)%FLASH_PAGE_SIZE);
break;
}
}
// ===================== 3. 执行Flash页擦除操作 =====================
if(need_erase)
{
FLASH_EraseInitTypeDef erase_init; // Flash擦除配置结构体
erase_init.TypeErase = FLASH_TYPEERASE_PAGES; // 擦除模式:按页擦除
erase_init.PageAddress = APP_START_ADDR; // 要擦除的页起始地址
erase_init.NbPages = 1; // 只擦除1页
uint32_t page_error = 0;
HAL_FLASHEx_Erase(&erase_init, &page_error); // 执行擦除
}
}
// 处理没有遗留字节的情况,直接按16位写入
static void Int_flash_write_no_last(void)
{
// 正好能够写入 => 不再有遗留的字节 0 6
for (uint16_t i = 0; i < usart_len; i += 2)
{
uint32_t flash_addr = APP_START_ADDR + i + flash_write_offset;
uint16_t data16;
if (i + 1 < usart_len)
{
data16 = usart_rec_buff[i] | (usart_rec_buff[i + 1] << 8);
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, flash_addr, data16);
}
}
}
// 处理有遗留字节的情况,使用上次遗留的字节和当前数据的第一个字节组成16位数据写入Flash
static void Int_flash_write_with_last(void)
{
for(uint16_t i=0;i<usart_len;i+=2)
{
uint32_t flash_addr = APP_START_ADDR + flash_write_offset+i;
uint16_t data16;
if(i==0) // 第一个16位数据,使用上次遗留的字节和当前数据的第一个字节组成
{
//拼接上一次字节
data16 = last_byte |(usart_rec_buff[i] << 8) ; // 上次遗留字节在高位,当前第一个字节在低位
}
else
{
data16 = usart_rec_buff[i-1] | (usart_rec_buff[i] << 8); // 低字节在前,高字节在后
}
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, flash_addr, data16);
}
}
static void Int_flash_write_halfword(void)
{
// 本次之后没剩余:总字节数为偶数(无遗留)
if ((usart_len + last_byte_flag) % 2 == 0)
{
if (last_byte_flag)
{
// 有剩余 数据长度是奇数 5 => 这次需要作为第一个字节写入
Int_flash_write_with_last();
// 2.4 记录偏移量
flash_write_offset += usart_len + 1;
}
else
{
// 无剩余 数据长度是偶数 => 不再有遗留的字节 0 6
Int_flash_write_no_last();
// 2.4 记录偏移量
flash_write_offset += usart_len;
}
last_byte_flag = 0; // 清除遗留标志
}
else // 本次之后有剩余:总字节数为奇数(遗留1字节)
{
if (last_byte_flag)
{
// 有剩余 数据长度是偶数
Int_flash_write_with_last();
// 修改最后剩下的字节
last_byte = usart_rec_buff[usart_len - 1];
// 2.4 记录偏移量
flash_write_offset += usart_len;
}
else
{
// 上次没有遗留字节 这次会留下一个
Int_flash_write_no_last();
last_byte = usart_rec_buff[usart_len - 1];
// 2.4 记录偏移量
flash_write_offset += usart_len - 1;
}
last_byte_flag = 1; // 设置遗留标志
}
}
#ifndef __INT_BOOTLOADER_H__
#define __INT_BOOTLOADER_H__
#include "usart.h"
#include "string.h"
#include <stdio.h>
#define BOOTLOADER_UART_BUFF 512
#define APP_START_ADDR 0x08004000 //起始地址
#define APP_ERASE_PAGE_COUNT 10
void int_bootloader(void);
void int_bootloader_finish(void);
int fputc(int ch, FILE *f);
#endif
总结
串口通信
轮询:简单但易丢数据。
空闲帧:HAL_UARTEx_ReceiveToIdle 支持不定长接收,需记录长度。
中断:回调中必须清除空闲标志和溢出标志,防止卡死。
DMA:配合空闲中断实现高效接收,回调中重启DMA并关闭半传输中断
Flash交互
规则:Flash只能1→0,擦除(页/整片)才能0→1;写前解锁,写后上锁。
擦除:动态检查非0xFF则擦页;或启动时预擦除10页,减少运行耗时。
半字写入:不支持单字节,用last_byte_flag和last_byte处理跨包拼接,根据总长度奇偶更新偏移。
流程:解锁→擦除(如需)→半字写入→上锁→清缓冲→重启接收
更多推荐

所有评论(0)