IIC和EEPROM(24c02):12:HAL----I2C_hal i2c-CSDN博客
FLASH(W25Q64):11:STM32---spl通信_根据右边的 spl flash 的连接原理图和下面的读时序图,用你所熟悉的单片机语言,写-CSDN博kuzhan
目标:
项目概述
在实际工作中,终端模块类设备需要远程升级、长期无人值守。为保障安全可靠、灵活升级、防止fash坏块程序采用A&B冗余程序系统设计,采用多种接口二级引导系
并可远程升级。
项目实现
● MCU任意RTOS可选freeTos/ucosi/RTI、PC软件可选QT/Vinform,通讯端口UART+ETH(iwip+tcp)+wifi模组(spi+OTA),三种通讯方式皆要有。
● 串口通讯可采用自定义协议(参考modbus),协议格式包含文字节前音、2字节总长度、N字节数据区、4字节保留、2字节ccc16检验。
● boot程序+app程序、FLASH存储分部及分区、boot区/参数区/运行区/A存储区/B存储区。
● boot程序等待若干秒,等待过程中如果收到上位机指令,跳转到永久等待模式,等待接收上位机下发的app.bin,或其它指令。如果等待超时,根据标志区标志,搬
到运行区,并跳转到运行区执行app。
● boot可为裸机程序。app需要做出tros程序,可做2个打印任务作为最后成功现象观察。
bootloader:
cpp
static uint8_t BootLoader_Enter(uint8_t timeout);
static void BootLoader_Info(void);
static void LOAD_A(uint32_t app_addr);
static void BootLoader_Clear(void);
typedef void (*load_a)(void); //函数指针类型的定义
load_a load_A;
void BootLoader_Brance(void)
{
if(BootLoader_Enter(30)==0){
if(OTA_Info.OTA_flag == OTA_SET_FLAG){ //判断OTA_flag是不是OTA_SET_FLAG定义的值,是的话进入if
u1_printf("OTA更新\r\n"); //串口0输出信息
// BootStaFlag |= UPDATA_A_FLAG; //置位标志位,表明需要更新A区
//UpDataA.W25Q64_BlockNB = 0; //W25Q64_BlockNB等于0,表明是OTA要更新A区
}else{ //判断OTA_flag是不是OTA_SET_FLAG定义的值,不是的话进入else
u1_printf("跳转A分区\r\n"); //串口0输出信息
LOAD_A(0x08020000); //跳转到运行区
}
}
u1_printf("进入BootLoader命令行\r\n");
BootLoader_Info();
}
static uint8_t BootLoader_Enter(uint8_t timeout){
u1_printf("%ds内,输入小写字母 w ,进入BootLoader命令行\r\n",(timeout*100)/1000);
while(timeout--){
delay_ms(100);
if(USART1_RXBuf[0] == 'w'){
return 1; //进入命令行
}
}
return 0; //不进入命令行
}
static void BootLoader_Info(void){
u1_printf("\r\n");
u1_printf("[1]擦除A区\r\n");
u1_printf("[2]串口IAP下载A区程序\r\n");
u1_printf("[3]设置OTA版本号\r\n");
u1_printf("[4]查询OTA版本号\r\n");
u1_printf("[5]向外部Flash下载程序\r\n");
u1_printf("[6]使用外部Flash内程序\r\n");
u1_printf("[7]重启\r\n");
u1_printf("[8]设置服务器连接信息\r\n");
}
__asm void MSR_SP(uint32_t addr){
MSR MSP, r0
BX r14
}
//跳出boot的时候需要把32复位到初始状态()
// 关键修改:直接操作寄存器复位外设(无库函数依赖)
static void BootLoader_Clear(void)
{
// 1. 关闭全局中断
__disable_irq();
// 2. 复位GPIOA~GPIOD(AHB1总线外设)
// 步骤:置位复位位 → 延时 → 清除复位位(完成复位)
RCC->AHB1RSTR |= (RCC_AHB1RSTR_GPIOARST | // GPIOA复位
RCC_AHB1RSTR_GPIOBRST | // GPIOB复位
RCC_AHB1RSTR_GPIOCRST | // GPIOC复位
RCC_AHB1RSTR_GPIODRST); // GPIOD复位
delay_ms(1); // 用自定义延时,避免依赖HAL_Delay
RCC->AHB1RSTR &= ~(RCC_AHB1RSTR_GPIOARST | // 解除GPIOA复位
RCC_AHB1RSTR_GPIOBRST | // 解除GPIOB复位
RCC_AHB1RSTR_GPIOCRST | // 解除GPIOC复位
RCC_AHB1RSTR_GPIODRST); // 解除GPIOD复位
// 3. 复位USART1(APB2总线外设)
RCC->APB2RSTR |= RCC_APB2RSTR_USART1RST; // USART1复位
delay_ms(1);
RCC->APB2RSTR &= ~RCC_APB2RSTR_USART1RST; // 解除USART1复位
// 4. 复位SPI1(APB2总线外设)
RCC->APB2RSTR |= RCC_APB2RSTR_SPI1RST; // SPI1复位
delay_ms(1);
RCC->APB2RSTR &= ~RCC_APB2RSTR_SPI1RST; // 解除SPI1复位
// 5. 恢复时钟默认状态(HSI 16MHz)
RCC->CR |= RCC_CR_HSION; // 使能HSI
while((RCC->CR & RCC_CR_HSIRDY) == 0); // 等待HSI就绪
RCC->CFGR = 0x00000000; // 时钟配置复位(系统时钟=HSI)
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_HSI); // 等待HSI生效
// 6. 清除所有中断(禁用+清除挂起)
for(uint8_t i = 0; i < 8; i++){
NVIC->ICER[i] = 0xFFFFFFFF; // 禁用所有中断
NVIC->ICPR[i] = 0xFFFFFFFF; // 清除所有挂起中断
}
// 7. 使能全局中断
__enable_irq();
}
static void LOAD_A(uint32_t app_addr){ // app_addr:APP在Flash的起始地址(如0x08008000)
uint32_t app_stack_top; // APP栈顶地址(向量表第1个元素,必须在IRAM1或IRAM2)
uint32_t app_entry; // APP入口地址(向量表第2个元素,必须在Flash)
// 1. 校验APP起始地址合法性(Flash范围,如STM32F4的Flash通常是0x08000000~0x080FFFFF)
if(app_addr < 0x08000000 || app_addr > 0x080FFFFF){
u1_printf("跳转失败:APP地址非法(不在Flash范围)\r\n");
return;
}
// 2. 读取APP向量表的前2个核心元素
app_stack_top = *(volatile uint32_t*)app_addr; // 向量表第1位:栈顶地址(SRAM) SP
app_entry = *(volatile uint32_t*)(app_addr+4);// 向量表第2位:复位入口地址(Flash) PC
// 3. 校验栈顶地址合法性(必须在IRAM1或IRAM2范围内,且8字节对齐)
// 3.1 检查是否在IRAM1或IRAM2中
if( !((app_stack_top >= IRAM1_START && app_stack_top <= IRAM1_END) ||
(app_stack_top >= IRAM2_START && app_stack_top <= IRAM2_END)) ){
u1_printf("跳转失败:栈顶地址不在SRAM分区内\r\n");
u1_printf("合法范围:IRAM1(0x%X~0x%X) 或 IRAM2(0x%X~0x%X)\r\n",
IRAM1_START, IRAM1_END, IRAM2_START, IRAM2_END);
return;
}
// 3.2 检查8字节对齐(Cortex-M架构强制要求,否则硬件异常)
if((app_stack_top & 0x07) != 0){
u1_printf("跳转失败:栈顶地址未8字节对齐(地址:0x%X)\r\n", app_stack_top);
return;
}
// 4. 校验APP入口地址合法性(必须在Flash范围内,且是可执行代码地址)
if(app_entry < 0x08000000 || app_entry > 0x080FFFFF){
u1_printf("跳转失败:APP入口地址非法(不在Flash范围)\r\n");
return;
}
load_A = (load_a)app_entry;
BootLoader_Clear();
MSR_SP(app_stack_top);
load_A();
}

因为他的boot对于其他来说都是单向的所以只需要PC,SP指针
PC 是 CPU 中最关键的寄存器之一,它的值始终指向 **"当前正在执行的指令的下一条指令的存储地址"**。 R15
栈(Stack)是内存中一块连续的区域,用于临时存储数据(如函数参数、返回地址、局部变量、上下文信息等),SP 寄存器的值始终指向栈的 "当前顶部"(栈顶)。R13
因为栈在SARM中在LOAD_A函数中需要判断栈顶指针有没有在合法sarm中
FLASH规划
我们使用的是STM32F407ZGT6一共有1M的FALSH非常大,所以不需要我们外挂FLASH。
STM32F407 系列的 Flash 总容量为 1MB(1024KB),硬件上固定分为2 个 Bank ,每个 Bank 的容量为512KB,划分规则由地址范围严格界定:
- Bank 1 :地址范围
0x08000000 ~ 0x0807FFFF(共 512KB) - Bank 2 :地址范围
0x08080000 ~ 0x080FFFFF(共 512KB)
| 扇区编号 | 起始地址 | 结束地址 | 扇区大小 | 所属 Bank | 备注 |
|---|---|---|---|---|---|
| 0 | 0x08000000 | 0x08003FFF | 16KB | Bank 1 | 小扇区 |
| 1 | 0x08004000 | 0x08007FFF | 16KB | Bank 1 | 小扇区 |
| 2 | 0x08008000 | 0x0800BFFF | 16KB | Bank 1 | 小扇区 |
| 3 | 0x0800C000 | 0x0800FFFF | 16KB | Bank 1 | 小扇区 |
| 4 | 0x08010000 | 0x0801FFFF | 64KB | Bank 1 | 中扇区 |
| 5 | 0x08020000 | 0x0803FFFF | 128KB | Bank 1 | 大扇区 |
| 6 | 0x08040000 | 0x0805FFFF | 128KB | Bank 1 | 大扇区 |
| 7 | 0x08060000 | 0x0807FFFF | 128KB | Bank 1 | 大扇区(Bank 1 结束) |
| 8 | 0x08080000 | 0x0809FFFF | 128KB | Bank 2 | 大扇区(Bank 2 开始) |
| 9 | 0x080A0000 | 0x080BFFFF | 128KB | Bank 2 | 大扇区 |
| 10 | 0x080C0000 | 0x080DFFFF | 128KB | Bank 2 | 大扇区 |
| 11 | 0x080E0000 | 0x080FFFFF | 128KB | Bank 2 | 大扇区(总结束地址) |
我们的划分的区域:
- 擦除必须以扇区为单位(或者全部擦除) :任何扇区中的数据,若要修改其中一部分,必须先完整擦除整个扇区 (擦除后扇区所有地址值为
0xFFFFFFFF),不能单独擦除扇区中的某一段。- 编程(写入)需基于擦除后的区域 :只能向已擦除(值为
0xFFFFFFFF)的地址写入数据,无法直接覆盖非0xFFFFFFFF的区域(即不能 "局部改写" 未擦除的区域)。
| 区域名称 | 包含扇区 | 扇区数量 | 单扇区大小 | 区域总大小 | 起始地址 | 结束地址 | 所属 Bank | 说明 |
|---|---|---|---|---|---|---|---|---|
| Boot 区 | 扇区 0~3 | 4 个 | 16KB | 64KB | 0x08000000 | 0x0800FFFF | Bank1 | 启动引导程序 |
| 参数区 | 扇区 4 | 1 个 | 64KB | 64KB | 0x08010000 | 0x0801FFFF | Bank1 | OTA 标志、设备参数等 |
| 运行区 | 扇区 5~7 | 3 个 | 128KB | 384KB | 0x08020000 | 0x0807FFFF | Bank1 | 主应用程序区 |
| APP1 区 | 扇区 8~9 | 2 个 | 128KB | 256KB | 0x08080000 | 0x080BFFFF | Bank2 | 备用应用程序区(OTA 升级用) |
| APP2 区 | 扇区 10~11 | 2 个 | 128KB | 256KB | 0x080C0000 | 0x080FFFFF | Bank2 | 备用应用程序区(OTA 升级用) |
代码:
cpp
// STM32F407ZGT6 Flash扇区编号与地址对应表(关键!)
//FLAHS标志区---#define FLASH_SECTOR_4 4U // 0x08010000-0x0801FFFF, 64KB
/**
* @brief STM32F407擦除Flash(按扇区操作)
* @param start_sector: 起始扇区编号(0-11,对应上述宏定义)
* @param sector_num: 要擦除的扇区数量(1-12,不超过总扇区数)
* @note 擦除前需确保扇区未存储运行中的代码
*/
void STM32F4_EraseFlash(uint16_t start_sector, uint16_t sector_num)
{
FLASH_EraseInitTypeDef flash_erase_init = {0};
uint32_t error_addr = 0; // 存储擦除错误地址
HAL_StatusTypeDef status = HAL_OK;
// 1. 检查参数有效性(扇区编号不越界)
// 修正后:
if (start_sector > 11 || (start_sector + sector_num) > 12) return;
// 说明:扇区编号最大11,总扇区数12个(0~11),确保不越界
// 2. Flash解锁
HAL_FLASH_Unlock();
// 3. 配置擦除参数
flash_erase_init.TypeErase = FLASH_TYPEERASE_SECTORS; // 扇区擦除模式
flash_erase_init.VoltageRange = FLASH_VOLTAGE_RANGE_3; // 供电电压3.3V(根据实际调整)
flash_erase_init.Sector = start_sector; // 起始扇区
flash_erase_init.NbSectors = sector_num; // 扇区数量
// 4. 执行擦除(带错误检查)
status = HAL_FLASHEx_Erase(&flash_erase_init, &error_addr);
if (status != HAL_OK)
{
// 擦除失败处理(可添加日志或指示灯提示)
uint32_t err_code = HAL_FLASH_GetError();
(void)err_code; // 避免未使用变量警告,实际可扩展错误处理
}
// 5. Flash锁定
HAL_FLASH_Lock();
}
/**
* @brief STM32F407写入Flash(32位字编程) 1字(Word) =4 字节
* @param address: 写入起始地址(必须4字节对齐,且在0x08000000-0x080FFFFF范围内)
* @param data: 待写入数据指针(uint32_t类型数组)
* @param wnum: 待写入总字节数(必须是4的整数倍,对应wnum/4个32位字)
*/
void STM32F4_WriteFlash(uint32_t address, uint32_t *data, uint32_t wnum)
{
HAL_StatusTypeDef status = HAL_OK;
// 1. 参数有效性检查
if (data == NULL || wnum == 0) return;
if (address < 0x08000000 || address > 0x080FFFFF) return;
if ((address % 4) != 0) return; // 地址必须4字节对齐
// 2. Flash解锁
HAL_FLASH_Unlock();
// 3. 循环写入(按32位字编程)
while (wnum > 0 && address <= 0x080FFFFF)
{
status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address, *data);
if (status == HAL_OK)
{
wnum -= 4; // 减少4字节(1个32位字)
address += 4; // 地址偏移4字节
data++; // 数据指针偏移
}
else
{
// 写入失败处理
uint32_t err_code = HAL_FLASH_GetError();
(void)err_code;
break;
}
}
// 4. Flash锁定
HAL_FLASH_Lock();
}
//
// 读取Flash数据(配套使用)
void STM32F4_ReadFlash(uint32_t address, uint32_t *data, uint32_t rnum)
{
if (data == NULL || rnum == 0) return;
if (address < 0x08000000 || address > 0x080FFFFF) return;
while (rnum > 0 && address <= 0x080FFFFF)
{
*data = *(volatile uint32_t *)address;
rnum -= 4;
address += 4;
data++;
}
}
void ReadOTAInfo()
{
memset(&OTA_Info,0,OTA_INFOCB_SIZE);
STM32F4_ReadFlash(0x08010000, (uint32_t *)&OTA_Info, OTA_INFOCB_SIZE);
}
uint32_t=4个字节 一个字=4个字节
为什么需要按32写入?
一、Flash 硬件控制器的编程单位限制
STM32 的 Flash 存储器控制器(FPEC,Flash Program/Erase Controller)对编程操作的最小单位有明确规定:
- 支持的编程模式包括:字节(8 位)、半字(16 位)、字(32 位) ,但实际硬件层面,32 位字编程是最基础、最稳定的模式。
- 底层硬件设计中,Flash 的存储阵列通常按 32 位宽度组织(与 Cortex-M4 内核的 32 位架构匹配),一次编程操作可以直接覆盖一个 32 位存储单元,而字节 / 半字编程需要控制器内部做额外的 "拼接" 处理(先读出整个 32 位单元,修改对应字节 / 半字,再重新写入),反而增加了复杂度和出错风险。
二、地址对齐要求
Flash 编程对地址的对齐性有严格要求:
- 32 位字编程(
FLASH_TYPEPROGRAM_WORD)要求地址必须是4 字节对齐 (即地址的最后 2 位必须为 0,如0x08020000、0x08020004);- 16 位半字编程(
FLASH_TYPEPROGRAM_HALFWORD)要求地址2 字节对齐;- 8 位字节编程(部分型号支持)要求地址1 字节对齐。
但在实际开发中,32 位字编程是最优选择:
- 代码中
address % 4 != 0的检查,正是为了确保地址符合 32 位编程的对齐要求,否则会触发FLASH_ERROR_PG(编程错误);- 若强行使用字节 / 半字编程,不仅需要额外处理对齐问题,还可能因硬件时序不匹配导致数据写入错误(尤其是高频时钟下)
USART接收数据设
boot:使用串口+DMA空闲中断


开辟一块栈(USART使用DMA接收,DMA把UASRT的数据转运的地方)----USART_RXBuf[USART1_RXDataLen]
开启一块数组里面放入start、end指针对,用于入栈和出栈的操作。和每当数据来的时候URxDataPtr[X]->END放入标记,然后移动到一下数据标记(URxDataPtr++),每次标记的时候需要和end对比防止越界。对比剩下的内存能不能放下单次接收的最大数据,不行回卷。
出栈:如果入栈的数组URxDataOUT的URxDataIN和不一样表示有数据
空闲中断:USART起始位-->标志一个数据帧的开始,固定为低电平
停止位:用于数据帧间隔,固定为高电平
USART 的空闲中断(IDLE)触发机制是:当串口接收数据过程中,总线上连续出现一个完整的字节传输时间(由波特率决定,如 115200 波特率下约 8.68μs)的高电平(空闲状态)时,USART 的状态寄存器(SR)会置位 IDLE 标志,进而触发中断。这一特性恰好用于判断 "一帧数据结束"------ 无论 DMA 是否还在传输(只要总线空闲达到阈值),即可中断即可会被唤醒处理当前帧。
cpp
#define U1_RX_MAX 256 //单次接收最大数据
#define NUM 10
#define USART1_TXDataLen 2048
#define USART1_RXDataLen (U1_RX_MAX*NUM)
extern uint8_t USART1_TXBuf[USART1_TXDataLen];
extern uint8_t USART1_RXBuf[USART1_RXDataLen];
typedef struct{
uint8_t *start; // 缓冲区起始地址指针(指向数据块的第一个字节)
uint8_t *end; // 缓冲区结束地址指针(指向数据块的最后一个字节的下一位,用于判断边界)
}UCB_URxBuffptr;
typedef struct{
uint16_t URxCounter; // 接收计数器:可能用于记录当前有效缓冲区数量,或累计接收字节数
UCB_URxBuffptr URxDataPtr[NUM]; // 接收缓冲区数组:存储NUM个缓冲区的起止指针(NUM为宏定义,代表缓冲区总数)
// 作用:管理多个独立的接收缓冲区,实现"双缓冲"或"多缓冲"机制
UCB_URxBuffptr *URxDataIN; // 入队指针:指向当前可写入数据的缓冲区(新接收的数据写入该缓冲区)
UCB_URxBuffptr *URxDataOUT; // 出队指针:指向当前可读取数据的缓冲区(应用程序从该缓冲区读取数据)
UCB_URxBuffptr *URxDataEND; // 结束指针:指向缓冲区数组的末尾(用于判断缓冲区循环边界,避免越界)
}UCB_CB;
核心实现代码:
cpp
static void U1RX_PtrInit(void)
{
U1CB.URxDataIN=&U1CB.URxDataPtr[0];
U1CB.URxDataOUT=&U1CB.URxDataPtr[0];
U1CB.URxDataEND=&U1CB.URxDataPtr[NUM-1];
U1CB.URxDataIN->start=USART1_RXBuf;
U1CB.URxCounter=0;
}
void MX_USART1_UART_Init(void)
{
.....正常配置就ok
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); //使能IDLE中断
/*
DMA数据转运:每收到一个字节,自动转运到USART1_RXBuf地址中去
当转运的数据=U1_RX_MAX+1的时候触发DMA完成中断
*/
HAL_UART_Receive_DMA(&huart1,USART1_RXBuf,U1_RX_MAX+1);
U1RX_PtrInit();
}
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET) {
// 1. 清除空闲中断标志(必须先读SR寄存器,再读DR寄存器,HAL库内部已实现)
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
// 2. 中止当前DMA接收(停止当前帧的传输,以便计算接收长度)
HAL_UART_DMAStop(&huart1);
// 3. 计算当前接收长度:总配置长度 - DMA剩余未传输的长度
// U1_RX_MAX+1是DMA初始配置的接收长度,__HAL_DMA_GET_COUNTER获取剩余未传字节数
U1CB.URxCounter += (U1_RX_MAX + 1) - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx); // 单独存储单帧长度
// 4. 记录当前帧的结束位置(环形缓冲区管理)
U1CB.URxDataIN->end = &USART1_RXBuf[U1CB.URxCounter - 1];
U1CB.URxDataIN++; // 移动到下一个帧指针
if (U1CB.URxDataIN == U1CB.URxDataEND) {
U1CB.URxDataIN = &U1CB.URxDataPtr[0]; // 环形回卷
}
// 5. 配置下一次DMA接收的起始地址(避免缓冲区溢出)
if (USART1_RXDataLen - U1CB.URxCounter >= U1_RX_MAX) {
// 剩余空间足够,从当前位置继续接收
U1CB.URxDataIN->start = &USART1_RXBuf[U1CB.URxCounter];
} else {
// 剩余空间不足,回卷到缓冲区起始位置
U1CB.URxDataIN->start = USART1_RXBuf;
U1CB.URxCounter = 0;
}
HAL_UART_Receive_DMA(&huart1, U1CB.URxDataIN->start, U1_RX_MAX + 1);
}
}
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(U1CB.URxDataOUT != U1CB.URxDataIN){
u1_printf("本次接收了%d字节数据\r\n",U1CB.URxDataOUT->end - U1CB.URxDataOUT->start + 1);
for(i=0;i<U1CB.URxDataOUT->end - U1CB.URxDataOUT->start + 1;i++) u1_printf("%c",U1CB.URxDataOUT->start[i]);
u1_printf("\r\n\r\n");
U1CB.URxDataOUT++;
if(U1CB.URxDataOUT==U1CB.URxDataEND){
U1CB.URxDataOUT = &U1CB.URxDataPtr[0];
}
}
}
串口iap下载:
使用串口的Xmode协议实现,使用bin文件下载到FLASH中:
XMODE:
cpp
int main(void)
{
。。。。。
ReadOTAInfo();
BootLoader_Brance();
while (1)
{
Boot_Usart_Reception();
CopyToRunArea() ;
}
void BootLoader_Brance(void)
{
if(BootLoader_Enter(30)==0){
if(OTA_Info.OTA_flag == OTA_SET_FLAG){ //判断OTA_flag是不是OTA_SET_FLAG定义的值,是的话进入if
u1_printf("OTA更新\r\n"); //串口0输出信息
//BootStaFlag |= UPDATA_A_FLAG; //置位标志位,表明需要更新A区
UpDataA.W25Q64_BlockNB = 0; //W25Q64_BlockNB等于0,表明是OTA要更新A区
}else{ //判断OTA_flag是不是OTA_SET_FLAG定义的值,不是的话进入else
u1_printf("跳转运行区\r\n"); //串口0输出信息
CopyToRunArea();
LOAD_A(STM32_RUN_SADDR); //跳转到运行区
}
}
u1_printf("进入BootLoader命令行\r\n");
BootLoader_Info();
}
static uint8_t BootLoader_Enter(uint8_t timeout){
u1_printf("%ds内,输入小写字母 w ,进入BootLoader命令行\r\n",(timeout/10));
while(timeout--){
delay_ms(8000);
if(USART1_RXBuf[0] == 'w'){
return 1; //进入命令行
}
}
return 0; //不进入命令行
}
static void BootLoader_Info(void){
u1_printf("\r\n");
u1_printf("[0]擦除运行区\r\n");
u1_printf("[1]擦除A区\r\n");
u1_printf("[2]擦除B区\r\n");
u1_printf("[3]串口IAP下载A区程序\r\n");
u1_printf("[4]串口IAP下载B区程序\r\n");
u1_printf("[5]运行A区程序\r\n");
u1_printf("[6]运行B区程序\r\n");
u1_printf("[7]重启\r\n");
}
/*-------------------------------------------------*/
/*函数名:BootLoader处理串口数据 */
/*参 数:data:数据指针 datalen:数据长度 */
/*返回值:无 */
/*-------------------------------------------------*/
void BootLoader_Event(uint8_t *data, uint16_t datalen)
{
int temp,i; //temp用于版本号sscanf判断格式 i用于for循环
/*--------------------------------------------------*/
/* 没有任何事件,判断顶层命令 */
/*--------------------------------------------------*/
if(BootStaFlag == 0){ //如果BootStaFlag等于0,没有任何事件,进入if,判断是哪个命令
if((datalen==1)&&(data[0]=='0')){ //如果数据长度1字节 且 是字符 1
u1_printf("擦除运行区\r\n"); //串口0输出信息
STM32F4_EraseFlash(STM32_RUN_START_SECTOR,STM32_RUN_SECTOR_NUM); //擦除A分区占用的扇区
}
else if((datalen==1)&&(data[0]=='1')){ //如果数据长度1字节 且 是字符 1
u1_printf("擦除A区\r\n"); //串口0输出信息
STM32F4_EraseFlash(8,2); //擦除A分区占用的扇区
}
else if((datalen==1)&&(data[0]=='2')){ //如果数据长度1字节 且 是字符 1
u1_printf("擦除B区\r\n"); //串口0输出信息
STM32F4_EraseFlash(10,2); //擦除B分区占用的扇区
}
else if((datalen==1)&&(data[0]=='3')){ //如果数据长度1字节 且 是字符 2
u1_printf("通过Xmodem协议,串口IAP下载A区程序,请使用bin格式文件\r\n"); //串口0输出信息
STM32F4_EraseFlash(STM32_APP1_START_SECTOR,STM32_APP1_SECTOR_NUM);
BootStaFlag |= (AIAP_XMODEMC_FLAG|AIAP_XMODEMD_FLAG); //置位 IAP_XMODEMC_FLAG 和 IAP_XMODEMD_FLAG 标志位
UpDataA.XmodemTimer = 0; //Xmodem发送大写C 间隔时间变量清零
UpDataA.XmodemNB= 0;
//保持接收Xmodem协议数据包个数的变量清零
}
else if((datalen==1)&&(data[0]=='4')){ //如果数据长度1字节 且 是字符 2
u1_printf("通过Xmodem协议,串口IAP下载B区程序,请使用bin格式文件\r\n"); //串口0输出信息
STM32F4_EraseFlash(10,2);
BootStaFlag |= (BIAP_XMODEMC_FLAG|BIAP_XMODEMD_FLAG); //置位 IAP_XMODEMC_FLAG 和 IAP_XMODEMD_FLAG 标志位
UpDataB.XmodemTimer = 0; //Xmodem发送大写C 间隔时间变量清零
UpDataB.XmodemNB= 0; //保持接收Xmodem协议数据包个数的变量清零
}
else if((datalen==1)&&(data[0]=='5')){ //如果数据长度1字节 且 是字符 3
u1_printf("准备运行A程序\r\n"); //串口0输出信息
OTA_Info.UpData_Flag=UPDATA_FROM_A_FLAG; //置位 SET_VERSION_FLAG
}
else if((datalen==1)&&(data[0]=='6')){ //如果数据长度1字节 且 是字符 4
OTA_Info.UpData_Flag=UPDATA_FROM_B_FLAG;
u1_printf("准备运行B程序\r\n"); //串口0输出信息
WriteOTAInfo(); //串口输出命令行信息
}
// else if((datalen==1)&&(data[0]=='5')){ //如果数据长度1字节 且 是字符 5
// u0_printf("向外部Flash下载程序,输入需要使用的块编号(1~9)\r\n"); //串口0输出信息
// BootStaFlag |= CMD_5_FLAG; //置位 CMD_5_FLAG
// }
// else if((datalen==1)&&(data[0]=='6')){ //如果数据长度1字节 且 是字符 6
// u0_printf("使用外部Flash内的程序,输入需要使用的块编号(1~9)\r\n"); //串口0输出信息
// BootStaFlag |= CMD_6_FLAG; //置位 CMD_6_FLAG
// }
else if((datalen==1)&&(data[0]=='7')){ //如果数据长度1字节 且 是字符 7
u1_printf("重启\r\n"); //串口0输出信息
delay_ms(100); //延时
NVIC_SystemReset(); //重启
}
}
/*--------------------------------------------------*/
/* 发生Xmodem A事件,处理该事件 */
/*--------------------------------------------------*/
else if (BootStaFlag & AIAP_XMODEMD_FLAG) // Xmodem协议接收数据标志
{
// 1. 处理标准数据包(133字节:帧头0x01+包号+反包号+128字节数据+2字节CRC)
if ((datalen == 133) && (data[0] == 0x01))
{
BootStaFlag &= ~AIAP_XMODEMC_FLAG; // 已收到数据包,停止发送'C'(Xmodem握手信号)
// 计算128字节数据的CRC
UpDataA.XmodemCRC = Xmodem_CRC16(&data[3], 128);
// 校验CRC(接收的CRC:高8位data[131],低8位data[132])
if (UpDataA.XmodemCRC == (data[131] << 8 | data[132])) // 用<<8替代*256,更高效
{
UpDataA.XmodemNB++; // 接收包计数+1
// 暂存数据到缓冲区:按"包号%8"计算偏移(8包=1页)
memcpy(
&UpDataA.Updatabuff[((UpDataA.XmodemNB - 1) % (STM32_PAGE_SIZE / 128)) * 128],
&data[3], // 有效数据从第3字节开始(跳过帧头、包号、反包号)
128 // 每次接收128字节
);
// 当接收满8包(1024字节=1页),写入STM32内部Flash
if ((UpDataA.XmodemNB % (STM32_PAGE_SIZE / 128)) == 0)
{
// 计算写入地址:A区起始地址 + 已完成的页数×页大小
uint32_t write_addr = STM32_APP1_SADDR +
((UpDataA.XmodemNB / (STM32_PAGE_SIZE / 128)) - 1) * STM32_PAGE_SIZE;
// 写入1页数据(1024字节,4的整数倍,符合STM32要求)
STM32F4_WriteFlash(write_addr, (uint32_t *)UpDataA.Updatabuff, STM32_PAGE_SIZE);
}
u1_printf("\x06"); // CRC正确,返回ACK
}
else
{
u1_printf("\x15"); // CRC错误,返回NAK(要求重发)
}
}
// 2. 处理结束包(EOT:1字节0x04,表示数据传输完成)
if ((datalen == 1) && (data[0] == 0x04))
{
u1_printf("\x06"); // 收到EOT,返回ACK
// 处理不足1页的剩余数据(若有)
if ((UpDataA.XmodemNB % (STM32_PAGE_SIZE / 128)) != 0)
{
// 计算剩余数据长度(包数×128字节)
uint32_t remain_len = (UpDataA.XmodemNB % (STM32_PAGE_SIZE / 128)) * 128;
// 补全长度为4的整数倍(STM32 Flash写入要求)
if (remain_len % 4 != 0)
{
remain_len += (4 - remain_len % 4);
}
// 计算写入地址:A区起始地址 + 已完成的页数×页大小
uint32_t write_addr = STM32_APP1_SADDR +
(UpDataA.XmodemNB / (STM32_PAGE_SIZE / 128)) * STM32_PAGE_SIZE;
// 写入剩余数据
STM32F4_WriteFlash(write_addr, (uint32_t *)UpDataA.Updatabuff, remain_len);
}
// 清除Xmodem接收标志
BootStaFlag &= ~AIAP_XMODEMD_FLAG;
// // 处理命令5启动的Xmodem(保存OTA信息到内部Flash)
// if (BootStaFlag & CMD5_XMODEM_FLAG)
// {
// BootStaFlag &= ~CMD5_XMODEM_FLAG; // 清除命令5标志
// 计算总长度(包数×128字节)并保存到OTA信息
OTA_Info.UpData_Flag=UPDATA_FROM_A_FLAG;
UpDataA.W25Q64_BlockNB=1;
OTA_Info.Firelen[UpDataA.W25Q64_BlockNB] = UpDataA.XmodemNB * 128;
WriteOTAInfo(); // 写入STM32内部Flash(替代24C02)
delay_ms(100);
BootLoader_Info(); // 输出命令行信息
delay_ms(100);
NVIC_SystemReset(); // 重启生效
}
}
/*--------------------------------------------------*/
/* 发生Xmodem B事件,处理该事件 */
/*--------------------------------------------------*/
// B区:Xmodem协议接收数据(对应命令4:串口IAP下载B区程序)
else if (BootStaFlag & BIAP_XMODEMD_FLAG) // 替换:B区Xmodem接收标志
{
// 1. 处理标准数据包(133字节:帧头0x01+包号+反包号+128字节数据+2字节CRC)
if ((datalen == 133) && (data[0] == 0x01))
{
BootStaFlag &= ~BIAP_XMODEMC_FLAG; // 替换:停止发送'C'(B区握手标志)
// 计算128字节数据的CRC(替换:使用B区结构体变量UpDataB)
UpDataB.XmodemCRC = Xmodem_CRC16(&data[3], 128);
// 校验CRC(接收的CRC:高8位data[131],低8位data[132])
if (UpDataB.XmodemCRC == (data[131] << 8 | data[132]))
{
UpDataB.XmodemNB++; // 替换:B区接收包计数+1
// 暂存数据到B区缓冲区(替换:使用UpDataB.Updatabuff)
memcpy(
&UpDataB.Updatabuff[((UpDataB.XmodemNB - 1) % (STM32_PAGE_SIZE / 128)) * 128],
&data[3], // 有效数据起始位置不变
128 // 每次接收128字节不变
);
// 接收满8包(1页=1024字节),写入B区Flash
if ((UpDataB.XmodemNB % (STM32_PAGE_SIZE / 128)) == 0)
{
// 替换:写入地址=B区起始地址 + 已完成页数×页大小
uint32_t write_addr = STM32_APP2_SADDR +
((UpDataB.XmodemNB / (STM32_PAGE_SIZE / 128)) - 1) * STM32_PAGE_SIZE;
// 写入1页数据(替换:使用B区缓冲区UpDataB.Updatabuff)
STM32F4_WriteFlash(write_addr, (uint32_t *)UpDataB.Updatabuff, STM32_PAGE_SIZE);
}
u1_printf("\x06"); // CRC正确,返回ACK(不变)
}
else
{
u1_printf("\x15"); // CRC错误,返回NAK(不变)
}
}
// 2. 处理结束包(EOT:1字节0x04,表示数据传输完成)
if ((datalen == 1) && (data[0] == 0x04))
{
u1_printf("\x06"); // 收到EOT,返回ACK(不变)
// 处理不足1页的剩余数据(替换:使用UpDataB变量)
if ((UpDataB.XmodemNB % (STM32_PAGE_SIZE / 128)) != 0)
{
// 计算剩余数据长度(B区接收包数×128字节)
uint32_t remain_len = (UpDataB.XmodemNB % (STM32_PAGE_SIZE / 128)) * 128;
// 补全为4字节整数倍(STM32 Flash写入要求,不变)
if (remain_len % 4 != 0)
{
remain_len += (4 - remain_len % 4);
}
// 替换:写入地址=B区起始地址 + 已完成页数×页大小
uint32_t write_addr = STM32_APP2_SADDR +
(UpDataB.XmodemNB / (STM32_PAGE_SIZE / 128)) * STM32_PAGE_SIZE;
// 写入剩余数据(替换:使用B区缓冲区)
STM32F4_WriteFlash(write_addr, (uint32_t *)UpDataB.Updatabuff, remain_len);
}
// 清除B区Xmodem接收标志(替换:清除BIAP_XMODEMD_FLAG)
BootStaFlag &= ~BIAP_XMODEMD_FLAG;
// 保存B区OTA更新信息(替换:B区专属标志和索引)
OTA_Info.UpData_Flag = UPDATA_FROM_B_FLAG; // 替换:B区更新标志
UpDataB.W25Q64_BlockNB = 2; // 替换:B区对应Firelen索引=2
OTA_Info.Firelen[UpDataB.W25Q64_BlockNB] = UpDataB.XmodemNB * 128; // B区固件长度
WriteOTAInfo(); // 写入参数区(不变)
delay_ms(100);
BootLoader_Info(); // 输出命令行(不变)
delay_ms(100);
NVIC_SystemReset(); // 重启生效(不变)
}
}
// /*
LWIP以太网实现:
https://blog.csdn.net/m0_74739916/article/details/154571736
https://blog.csdn.net/m0_74739916/article/details/154571736LWIP传输需要双方指定协议,我们实验Xmode协议,现在实验python调试,全部搞完了实验QT写出上位机
cpp
......................
...../**
* @brief lwIP tcp_recv()函数的回调函数
* @param arg : 回调函数传入的参数
* @param tpcb : TCP控制块
* @param p : 网络数据包
* @param err : 错误码
* @retval 返回错误码
*/
err_t lwip_tcp_client_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) {
uint32_t data_len = 0;
struct pbuf *q;
struct tcp_client_struct *es = (struct tcp_client_struct *)arg;
err_t ret_err = ERR_OK;
// XMODEM协议常量定义
#define XMODEM_SOH 0x01 // 128字节数据块帧头
#define XMODEM_ACK 0x06 // 确认响应
#define XMODEM_NAK 0x15 // 否定响应(请求重发)
#define XMODEM_CAN 0x18 // 取消传输
#define XMODEM_EOT 0x04 // 传输结束标志
#define XMODEM_BLOCK_SIZE 128 // 标准数据块大小(128字节)
#define XMODEM_FRAME_LEN 133 // 帧长度:1(SOH) + 1(块号) + 1(反块号) + 128(数据) + 2(CRC)
LWIP_ASSERT("arg != NULL", arg != NULL);
if (p == NULL) { // 连接关闭
es->state = ES_TCPCLIENT_CLOSING;
es->p = p;
g_lwip_send_flag &= ~(1 << 5);
u1_printf("TCP连接关闭\r\n");
}
else if (err != ERR_OK) { // 接收错误
if (p) pbuf_free(p);
ret_err = err;
// u1_printf("TCP接收错误: %d\r\n", err);
}
else if (es->state == ES_TCPCLIENT_CONNECTED) { // 连接状态
// 拷贝pbuf数据到全局接收缓冲区
memset(g_lwip_demo_recvbuf, 0, LWIP_DEMO_RX_BUFSIZE);
for (q = p; q != NULL; q = q->next) {
uint32_t copy_len = (q->len > (LWIP_DEMO_RX_BUFSIZE - data_len)) ?
(LWIP_DEMO_RX_BUFSIZE - data_len) : q->len;
memcpy(g_lwip_demo_recvbuf + data_len, q->payload, copy_len);
data_len += copy_len;
if (data_len >= LWIP_DEMO_RX_BUFSIZE) break;
}
tcp_recved(tpcb, p->tot_len);
pbuf_free(p);
g_lwip_send_flag |= (1 << 6);
// u1_printf(" %d\r\n", data_len);
// -------------------------- A区XMODEM处理(与参考逻辑完全对齐) --------------------------
if (BootStaFlag & LWIPA_YMODEMD_FLAG) {
BootStaFlag &= ~LWIPA_YMODEMC_FLAG; // 停止发送握手信号'C'
// 1. 处理标准数据块(SOH帧:133字节=0x01+块号+反块号+128数据+2CRC)
if ((data_len == XMODEM_FRAME_LEN) && (g_lwip_demo_recvbuf[0] == XMODEM_SOH)) {
uint8_t block_num = g_lwip_demo_recvbuf[1];
uint8_t block_num_inv = g_lwip_demo_recvbuf[2];
// CRC校验(128字节数据)
UpDataA.XmodemCRC = Xmodem_CRC16(&g_lwip_demo_recvbuf[3], XMODEM_BLOCK_SIZE);
uint16_t crc_recv = (g_lwip_demo_recvbuf[131] << 8) | g_lwip_demo_recvbuf[132];
if (UpDataA.XmodemCRC == crc_recv) {
UpDataA.XmodemNB++; // 接收包计数+1
// 暂存数据到缓冲区:按"包号%8"计算偏移(8包=1页)
uint32_t buf_idx = ((UpDataA.XmodemNB - 1) % (STM32_PAGE_SIZE / XMODEM_BLOCK_SIZE)) * XMODEM_BLOCK_SIZE;
memcpy(&UpDataA.Updatabuff[buf_idx], &g_lwip_demo_recvbuf[3], XMODEM_BLOCK_SIZE);
// 满1页(8包=1024字节)时写入Flash
if ((UpDataA.XmodemNB % (STM32_PAGE_SIZE / XMODEM_BLOCK_SIZE)) == 0) {
uint32_t write_addr = STM32_APP1_SADDR +
((UpDataA.XmodemNB / (STM32_PAGE_SIZE / XMODEM_BLOCK_SIZE)) - 1) * STM32_PAGE_SIZE;
STM32F4_WriteFlash(write_addr, (uint32_t *)UpDataA.Updatabuff, STM32_PAGE_SIZE);
}
TCP_SendSingleByte(tpcb, es, XMODEM_ACK); // CRC正确,发送ACK
} else {
u1_printf("A区CRC校验失败:计算0x%04X,接收0x%04X\r\n", UpDataA.XmodemCRC, crc_recv);
TCP_SendSingleByte(tpcb, es, XMODEM_NAK); // CRC错误,发送NAK请求重发
return ret_err;
}
}
// 2. 处理结束包(EOT:1字节0x04,传输完成)
else if ((data_len == 1) && (g_lwip_demo_recvbuf[0] == XMODEM_EOT)) {
TCP_SendSingleByte(tpcb, es, XMODEM_ACK); // 收到EOT,发送ACK
// 处理不足1页的剩余数据
uint32_t remain_blocks = UpDataA.XmodemNB % (STM32_PAGE_SIZE / XMODEM_BLOCK_SIZE);
if (remain_blocks != 0) {
uint32_t remain_len = remain_blocks * XMODEM_BLOCK_SIZE;
// 补全长度为4的整数倍
if (remain_len % 4 != 0) {
remain_len += (4 - remain_len % 4);
}
uint32_t write_addr = STM32_APP1_SADDR +
(UpDataA.XmodemNB / (STM32_PAGE_SIZE / XMODEM_BLOCK_SIZE)) * STM32_PAGE_SIZE;
STM32F4_WriteFlash(write_addr, (uint32_t *)UpDataA.Updatabuff, remain_len);
}
// 清除Xmodem标志,保存OTA信息,重启生效
BootStaFlag &= ~LWIPA_YMODEMD_FLAG;
OTA_Info.UpData_Flag = UPDATA_FROM_A_FLAG;
OTA_Info.Firelen[1] = UpDataA.XmodemNB * XMODEM_BLOCK_SIZE;
WriteOTAInfo(); // 写入内部Flash(替代24C02)
delay_ms(100);
u1_printf("APP1 XMODEM接收完毕!固件大小:%lu字节,准备重启\r\n", OTA_Info.Firelen[1]);
NVIC_SystemReset(); // 系统重启
}
else {
u1_printf("未知XMODEM帧类型,发送NAK\r\n");
TCP_SendSingleByte(tpcb, es, XMODEM_NAK);
}
}
// -------------------------- B区XMODEM处理(与参考逻辑完全对齐) --------------------------
if (BootStaFlag & LWIPB_YMODEMD_FLAG) {
BootStaFlag &= ~LWIPB_YMODEMC_FLAG; // 停止发送握手信号'C'
// 1. 处理标准数据块(SOH帧:133字节=0x01+块号+反块号+128数据+2CRC)
if ((data_len == XMODEM_FRAME_LEN) && (g_lwip_demo_recvbuf[0] == XMODEM_SOH)) {
uint8_t block_num = g_lwip_demo_recvbuf[1];
uint8_t block_num_inv = g_lwip_demo_recvbuf[2];
// CRC校验(128字节数据)
UpDataB.XmodemCRC = Xmodem_CRC16(&g_lwip_demo_recvbuf[3], XMODEM_BLOCK_SIZE);
uint16_t crc_recv = (g_lwip_demo_recvbuf[131] << 8) | g_lwip_demo_recvbuf[132];
if (UpDataB.XmodemCRC == crc_recv) {
UpDataB.XmodemNB++; // 接收包计数+1
// 暂存数据到缓冲区:按"包号%8"计算偏移(8包=1页)
uint32_t buf_idx = ((UpDataB.XmodemNB - 1) % (STM32_PAGE_SIZE / XMODEM_BLOCK_SIZE)) * XMODEM_BLOCK_SIZE;
memcpy(&UpDataB.Updatabuff[buf_idx], &g_lwip_demo_recvbuf[3], XMODEM_BLOCK_SIZE);
// 满1页(8包=1024字节)时写入Flash
if ((UpDataB.XmodemNB % (STM32_PAGE_SIZE / XMODEM_BLOCK_SIZE)) == 0) {
uint32_t write_addr = STM32_APP2_SADDR +
((UpDataB.XmodemNB / (STM32_PAGE_SIZE / XMODEM_BLOCK_SIZE)) - 1) * STM32_PAGE_SIZE;
STM32F4_WriteFlash(write_addr, (uint32_t *)UpDataB.Updatabuff, STM32_PAGE_SIZE);
}
TCP_SendSingleByte(tpcb, es, XMODEM_ACK); // CRC正确,发送ACK
} else {
u1_printf("A区CRC校验失败:计算0x%04X,接收0x%04X\r\n", UpDataB.XmodemCRC, crc_recv);
TCP_SendSingleByte(tpcb, es, XMODEM_NAK); // CRC错误,发送NAK请求重发
return ret_err;
}
}
// 2. 处理结束包(EOT:1字节0x04,传输完成)
else if ((data_len == 1) && (g_lwip_demo_recvbuf[0] == XMODEM_EOT)) {
TCP_SendSingleByte(tpcb, es, XMODEM_ACK); // 收到EOT,发送ACK
// 处理不足1页的剩余数据
uint32_t remain_blocks = UpDataB.XmodemNB % (STM32_PAGE_SIZE / XMODEM_BLOCK_SIZE);
if (remain_blocks != 0) {
uint32_t remain_len = remain_blocks * XMODEM_BLOCK_SIZE;
// 补全长度为4的整数倍
if (remain_len % 4 != 0) {
remain_len += (4 - remain_len % 4);
}
uint32_t write_addr = STM32_APP2_SADDR +
(UpDataB.XmodemNB / (STM32_PAGE_SIZE / XMODEM_BLOCK_SIZE)) * STM32_PAGE_SIZE;
STM32F4_WriteFlash(write_addr, (uint32_t *)UpDataB.Updatabuff, remain_len);
}
// 清除Xmodem标志,保存OTA信息,重启生效
BootStaFlag &= ~LWIPB_YMODEMD_FLAG;
OTA_Info.UpData_Flag = UPDATA_FROM_B_FLAG;
OTA_Info.Firelen[2] = UpDataB.XmodemNB * XMODEM_BLOCK_SIZE;
WriteOTAInfo(); // 写入内部Flash(替代24C02)
delay_ms(100);
u1_printf("APP1 XMODEM接收完毕!固件大小:%lu字节,准备重启\r\n", OTA_Info.Firelen[1]);
NVIC_SystemReset(); // 系统重启
}
else {
u1_printf("未知XMODEM帧类型,发送NAK\r\n");
TCP_SendSingleByte(tpcb, es, XMODEM_NAK);
}
}
}
else { // 连接已关闭但收到数据
tcp_recved(tpcb, p->tot_len);
es->p = NULL;
pbuf_free(p);
}
return ret_err;
}
。。。。。。。。。。。。。。。。。。。
QT上位机:
扩展:
FLASH:

一、FLASH 硬件核心特性(必须掌握)
存储结构
- STM32F407ZGT6 的 FLASH 为1MB(1024KB) ,起始地址
0x08000000,按扇区划分(0~11 共 12 个扇区),扇区大小不等(16KB/64KB/128KB),且分为 2 个 Bank(各 512KB)。- 扇区是擦除操作的最小单位(无法擦除单个字节 / 字,必须整扇区擦除),擦除后所有位为
0xFFFFFFFF(空白状态)。操作限制
- 编程(写入) :只能向空白区域(
0xFFFFFFFF)写入,且地址必须4 字节对齐(32 位字编程),无法直接覆盖非空白数据(需先擦除)。- 擦除 :需指定扇区,且擦除前必须解锁 FLASH(默认锁定,防止误操作);擦除 / 编程需满足电压范围(通常 3.3V,对应
FLASH_VOLTAGE_RANGE_3)。- 速度:擦除单扇区约 150ms,整片擦除约 1.8s;编程单字约 30us,受系统时钟影响(需配置 FLASH 等待周期)。
保护机制
读保护(RDP):通过选项字节配置,等级 1 禁止外部读取,等级 2 不可逆(禁止调试,谨慎使用)。
写保护 :可按扇区独立配置,保护后无法擦除 / 写入(需通过选项字节解除)。
编程(写入)时只能从 1 变为 0,擦除时才能从 0 变回 1,这是 FLASH 的硬件特性决定的。
核心逻辑拆解
FLASH 的初始 / 擦除状态 擦除后的 FLASH,所有存储位都会变成
1(对应十六进制0xFFFFFFFF),这是 FLASH 的 "空白状态"。编程(写入)的位变化 写入数据时,只能将需要的位从
1置为0(比如要写0x12345678,就是把对应位的1改成0,未涉及的位保持1)。无法直接将已为0的位改回1,这也是 "不能覆盖写入" 的核心原因。擦除的位变化 只有执行扇区擦除操作,才能把扇区内所有位从
0恢复为1,回到空白状态后才能重新写入新数据。实际操作影响
想修改某段数据时,必须先擦除整个扇区(让所有位变回
1),再重新写入包含修改后内容的完整数据(不能只写修改的部分)。比如参数区某字节从
0x11(00010001)改成0x10(00010000),可直接写(仅把最后一位1变0);但要从0x10改回0x11,必须先擦除整个扇区再写。
FLASH和ROM
这是 Keil MDK 开发环境中的存储器配置界面,用于指定程序和数据在芯片存储器中的存储区域,不同区域的用途如下:
一、
IROM1(内置 Flash 程序存储区)
- 地址 :
0x08020000~0x08020000 + 0x60000 - 1(即0x08080000),总容量384KB。- 用途 :存放应用程序代码(如主程序、函数、常量数据等)。结合之前的分区规划,这里对应 "运行区 / APP1 区",是程序实际执行的代码存储区域(Bootloader 跳转后,程序从该区域启动)。
二、
IRAM1(内置 SRAM 数据存储区)
- 地址 :
0x20000000~0x20000000 + 0x1C000 - 1(即0x2001C000),总容量112KB。- 用途 :存放程序运行时的临时数据 ,包括:
- 全局变量、局部变量;
- 函数调用的栈空间(Stack);
- 动态内存分配(如
malloc申请的空间)。三、
IRAM2(内置 SRAM 扩展数据区,部分 STM32 型号支持)
- 地址 :
0x2001C000~0x2001C000 + 0x4000 - 1(即0x20020000),总容量16KB。- 用途 :
- 可作为额外的临时数据存储区(如存放高频访问的缓存、大数组);
- 部分芯片中,IRAM2 可配置为 "CCM RAM"(内核紧密耦合内存),用于存储对速度要求极高的代码或数据(执行效率比普通 SRAM 更高)。
总结
IROM1(FALSH):存程序代码,掉电不丢;IRAM1/IRAM2:存运行时的临时数据,掉电丢失。这种配置是为了让程序在 "非默认 Flash 起始地址"(0x08020000)运行,同时合理分配 RAM 资源,确保程序稳定执行。
中断向量表:
默认放在FLASH的起始位置0x8000000但是,我们将FLASH划分为多个区域,需要将中断向量表偏移(除了boot程序其他的全部都要偏移)
SCB->VTOR=FLASH_BASE|偏移量 ;

keil配置:
keil的falsh配置:
Erase Sectors--》擦除扇区

bootloader:

APP程序的FLASH,需要加入中断向量表的偏移:
EGG:运行区域


生成Bin文件:
方法一:
1:将KEIL下面的bin文件加入环境变量

2:fromelf --bin --output ./Run/Run.bin ./Run/Run.axf

./和../是表示相对路径的符号,./----->当前目录
../----->上级目录
方法二:
在keil里面加入: D:\keil5\ARM\ARMCC\bin\fromelf.exe --bin -o .\Run\Run.bin .\Run\Run.axf
D:\keil5\ARM\ARMCC\bin\fromelf.exe 你的keil安装的目录也是
.\Run\Run.bin 生成bin文件的目录
\Run\Run.axf axf文件的目录


需要注意:当前打开keil的目录是Run.uvprojx , 所以需要注意.\ 和..\

过程发现的问题:
使用DMA+串口中断使用HAL_DMA_Abort停止DMA传输,使用这个发现问题第一次输入stm32可以正常接收,第二次不能接收,第三次正常接收,第四从不能接收,以此类推 (您描述的"交替成功")
一:串口DMA空闲中断
HAL_DMA_Abort
总之这个函数就是判断DMA的句柄状态不忙的话,清除中断相关的寄存器,关闭DMA数据流, 一次性清除指定 DMA 流的所有中断标志。不对其他外设(USART)操作。
perl
HAL_StatusTypeDef HAL_DMA_Abort(DMA_HandleTypeDef *hdma)
{
/* calculate DMA base and stream number */
DMA_Base_Registers *regs = (DMA_Base_Registers *)hdma->StreamBaseAddress;
uint32_t tickstart = HAL_GetTick();
if(hdma->State != HAL_DMA_STATE_BUSY)
{
hdma->ErrorCode = HAL_DMA_ERROR_NO_XFER;
/* Process Unlocked */
__HAL_UNLOCK(hdma);
return HAL_ERROR;
}
else
{
/* 清除 传输完成中断使能 传输错误中断使能 直接模式错误中断使能*/
hdma->Instance->CR &= ~(DMA_IT_TC | DMA_IT_TE | DMA_IT_DME);
/* DMA 流量xFIFO 控制寄存器
清除 :FIFO 错误中断使能 (FIFO error interrupt enable)*/
hdma->Instance->FCR &= ~(DMA_IT_FE);
/*检查普通DMA模式的半传输完成回调函数是否被设置*/
if((hdma->XferHalfCpltCallback != NULL) || (hdma->XferM1HalfCpltCallback != NULL))
{
/*清除 半传输中断使能 (Half transfer interrupt enable) */
hdma->Instance->CR &= ~(DMA_IT_HT);
}
/* 关闭 数据流使能/读作低电平时数据流就绪标志*/
__HAL_DMA_DISABLE(hdma);
/* EN:数据流使能/读作低电平时数据流就绪标志 */
while((hdma->Instance->CR & DMA_SxCR_EN) != RESET)
{
/* Check for the Timeout */
if((HAL_GetTick() - tickstart ) > HAL_TIMEOUT_DMA_ABORT)
{
/* Update error code */
hdma->ErrorCode = HAL_DMA_ERROR_TIMEOUT;
/* Change the DMA state */
hdma->State = HAL_DMA_STATE_TIMEOUT;
/* 结锁 */
__HAL_UNLOCK(hdma);
return HAL_TIMEOUT;
}
}
/*
regs 指向 DMA 寄存器组
IFCR :DMA中断清除寄存器
hdma->StreamIndex dma的那个数据流
次性清除指定 DMA 流的所有中断标志,
*/
regs->IFCR = 0x3FU << hdma->StreamIndex;
/* Change the DMA state*/
hdma->State = HAL_DMA_STATE_READY;
/* Process Unlocked */
__HAL_UNLOCK(hdma);
}
return HAL_OK;
}




HAL_UART_DMAStop
我们只使用了接收的,所以只看接收的(其实接收和发送写的一样)
使能串口的DMA接收器,判断串口的状态如果为忙,失能DMA接收器。判断串口有没有配置dma接收通道,配置了调HAL_DMA_Abort。再把串口的接收中断清理。
perl
HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart)
{
uint32_t dmarequest = 0x00U;
/* The Lock is not implemented on this API to allow the user application
to call the HAL UART API under callbacks HAL_UART_TxCpltCallback() / HAL_UART_RxCpltCallback():
when calling HAL_DMA_Abort() API the DMA TX/RX Transfer complete interrupt is generated
and the correspond call back is executed HAL_UART_TxCpltCallback() / HAL_UART_RxCpltCallback()
*/
/* Stop UART DMA Tx request if ongoing */
dmarequest = HAL_IS_BIT_SET(huart->Instance->CR3, USART_CR3_DMAT);
if ((huart->gState == HAL_UART_STATE_BUSY_TX) && dmarequest)
{
ATOMIC_CLEAR_BIT(huart->Instance->CR3, USART_CR3_DMAT);
/* Abort the UART DMA Tx stream */
if (huart->hdmatx != NULL)
{
HAL_DMA_Abort(huart->hdmatx);
}
UART_EndTxTransfer(huart);
}
/*
#define USART_CR3_DMAR_Pos (6U)
#define USART_CR3_DMAR_Msk (0x1UL << USART_CR3_DMAR_Pos)
#define USART_CR3_DMAR USART_CR3_DMAR_Msk
USART_CR3_DMAR 实际上为 0100 0000
使能 DMA 使能接收器 (DMA enable receiver)
*/
dmarequest = HAL_IS_BIT_SET(huart->Instance->CR3, USART_CR3_DMAR);
/*
HAL_UART_STATE_BUSY_RX 串口接收数据状态忙
*/
if ((huart->RxState == HAL_UART_STATE_BUSY_RX) && dmarequest)
{
//失能 DMA 使能接收器 (DMA enable receiver)
ATOMIC_CLEAR_BIT(huart->Instance->CR3, USART_CR3_DMAR);
/*判断UART是否配置了DMA接收通道 */
if (huart->hdmarx != NULL)
{
HAL_DMA_Abort(huart->hdmarx);
}
/*
这个是对于串口操作
禁用接收中断 - 关闭RXNE、奇偶错误和各类错误中断
清理空闲检测中断 - 如果是"接收至空闲"模式,额外关闭空闲中断
重置状态标志 - 将接收状态恢复为就绪状态,接收类型重置为标准模式
核心目的:安全地停止UART接收并清理相关配置。
*/
UART_EndRxTransfer(huart);
}
return HAL_OK;
}
为什么再使用DMA+空闲中断的时候只能使用HAL_UART_DMAStop?
主要区别
HAL_DMA_Abort(&hdma_usart1_rx)- 仅停止DMA
❌ 只中止DMA传输
❌ 不处理UART侧的DMA配置
❌ USART_CR3_DMAR 位仍然保持使能状态
❌ UART可能继续产生DMA请求,但DMA已停止
HAL_UART_DMAStop(&huart1)- 完整停止UART+DMA
✅ 中止DMA传输
✅ 清除UART的DMA接收使能位 (USART_CR3_DMAR)
✅ 更新UART状态机 (RxState)
✅ 清理相关中断标志
✅ 确保UART和DMA完全同步停止
为什么直接使用
HAL_DMA_Abort会导致错误?问题所在:
UART仍在请求DMA :虽然DMA停止了,但UART的
USART_CR3_DMAR位仍然为1状态不一致 :UART认为DMA仍在工作 (
HAL_UART_STATE_BUSY_RX),但DMA实际已停止后续操作混乱:重新启动DMA时可能出现状态冲突
可能丢失数据:UART可能继续接收数据并尝试DMA传输,但DMA已不可用
总结
必须使用
HAL_UART_DMAStop的原因:同步操作:确保UART和DMA同时停止
状态一致性:维护正确的状态机状态
简单说:
HAL_UART_DMAStop是一个"完整的套餐",而HAL_DMA_Abort只是其中的"一道菜",单独使用会导致系统状态不一致。安全性:避免硬件状态不一致导致的异常
完整性:清理所有相关的配置和中断
debug:
描述的"交替成功:使用
HAL_DMA_Abort会导致hdma->State 状态异常,第一次为HAL_DMA_STATE_BUSY可以进入else,第二次为HAL_DMA_STATE_READY不能进入依次循环
perlHAL_StatusTypeDef HAL_DMA_Abort(DMA_HandleTypeDef *hdma) { /* calculate DMA base and stream number */ DMA_Base_Registers *regs = (DMA_Base_Registers *)hdma->StreamBaseAddress; uint32_t tickstart = HAL_GetTick(); if(hdma->State != HAL_DMA_STATE_BUSY) { } else { /* Disable all the transfer interrupts */ 这个里面是清除dma相关的 } return HAL_OK; }
DMA这个我自己写的:
这个是别的up的写的不错参考一下: