在嵌入式系统开发中,OTA(Over-The-Air)技术已经成为现代物联网设备必备的核心功能之一。作为一名长期从事STM32开发的工程师,我想分享一个完整的OTA和BootLoader开发案例,重点讲解外设功能开发的关键实现细节。
这个项目基于STM32F103C8T6开发板,通过构建完整的BootLoader系统,实现了固件的远程更新功能。相比简单的串口烧录,这套方案具有以下技术优势:
项目使用的主控芯片是STM32F103C8T6,这是一款性价比极高的Cortex-M3内核MCU,主要外设资源包括:
存储器件选型考虑:
W25Q64(8MB SPI Flash):
AT24C02(2KB I2C EEPROM):
开发板与各模块的连接采用最小系统设计:
code复制[STM32F103C8T6]――――[USB转串口模块]
|――――[W25Q64 Flash模块]
|――――[AT24C02 EEPROM模块]
|――――[OLED显示屏](调试用)
具体引脚分配如下表所示:
| 外设模块 | STM32引脚 | 备注 |
|---|---|---|
| USB转串口-TXD | PA9 | 需接1K上拉电阻 |
| USB转串口-RXD | PA10 | |
| W25Q64-CS | PA4 | 片选信号 |
| W25Q64-CLK | PA5 | SPI时钟 |
| W25Q64-DO | PA6 | 主出从入 |
| W25Q64-DI | PA7 | 主入从出 |
| AT24C02-SDA | PB11 | I2C数据线 |
| AT24C02-SCL | PB10 | I2C时钟线 |
硬件设计注意事项:
- SPI Flash的VCC引脚建议增加0.1uF去耦电容
- I2C总线必须接上拉电阻(通常4.7K)
- 串口线路在长距离传输时应考虑增加TVS二极管保护
采用模块化设计思想,工程目录组织如下:
code复制OTA_BootLoader/
├── CMSIS/ // 内核支持文件
├── Hardware/
│ ├── AT24C02/ // EEPROM驱动
│ ├── MyFLASH/ // 内部Flash操作
│ ├── MyI2C/ // 软件I2C实现
│ ├── MySPI/ // 软件SPI实现
│ └── W25Q64/ // SPI Flash驱动
├── Libraries/ // 标准外设库
├── User/
│ ├── main.c // 主程序
│ ├── serial.c // 串口+DMA实现
│ └── stm32f10x_it.c // 中断服务程序
└── MDK-ARM/ // Keil工程文件
针对STM32F103C8T6的有限资源,采用以下内存优化方案:
Flash空间划分:
RAM使用规划:
采用三重缓冲机制确保数据完整性:
c复制typedef struct {
uint8_t *start; // 数据起始地址
uint8_t *end; // 数据结束地址
} UCB_URxBuffptr;
typedef struct {
uint16_t URxCounter; // 缓冲区使用计数
UCB_URxBuffptr URxDataPtr[10]; // 数据块指针数组
UCB_URxBuffptr *URxDataIN; // 写入指针
UCB_URxBuffptr *URxDataOUT; // 读取指针
UCB_URxBuffptr *URxDataEND; // 数组末尾标记
} UCB_CB;
c复制void DMA_Config(void) {
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)U0_RxBuff;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = U0_RX_MAX + 1;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel5, &DMA_InitStructure);
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
DMA_Cmd(DMA1_Channel5, ENABLE);
}
当检测到总线空闲时,执行以下操作:
c复制void USART1_IRQHandler(void) {
if(USART_GetITStatus(USART1, USART_IT_IDLE) == SET) {
USART1->SR; // 清除IDLE标志
USART_ReceiveData(USART1);
// 计算接收数据长度
uint16_t len = (U0_RX_MAX + 1) - DMA_GetCurrDataCounter(DMA1_Channel5);
U0CB.URxCounter += len;
// 更新当前数据块信息
U0CB.URxDataIN->end = &U0_RxBuff[U0CB.URxCounter - 1];
// 移动IN指针
if(++U0CB.URxDataIN > U0CB.URxDataEND) {
U0CB.URxDataIN = &U0CB.URxDataPtr[0];
}
// 检查缓冲区空间
if(U0_RX_SIZE - U0CB.URxCounter < U0_RX_MAX) {
U0CB.URxDataIN->start = U0_RxBuff;
U0CB.URxCounter = 0;
} else {
U0CB.URxDataIN->start = &U0_RxBuff[U0CB.URxCounter];
}
// 重置DMA
DMA_Cmd(DMA1_Channel5, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel5, U0_RX_MAX + 1);
DMA1_Channel5->CMAR = (uint32_t)U0CB.URxDataIN->start;
DMA_Cmd(DMA1_Channel5, ENABLE);
}
}
关键函数包括:
c复制void AT24C02_WritePage(uint8_t WordAddress, uint8_t* Data_Array) {
MyI2C_Start();
MyI2C_SendByte(0xA0);
MyI2C_ReceiveAck();
MyI2C_SendByte(WordAddress);
MyI2C_ReceiveAck();
for(uint8_t i=0; i<8; i++) {
MyI2C_SendByte(Data_Array[i]);
MyI2C_ReceiveAck();
}
MyI2C_Stop();
Delay_ms(5); // 必须的写入延时
}
版本信息存储:
状态标志存储:
参数存储:
注意事项:
- EEPROM每个字节有10万次写入寿命,应避免频繁写入同一地址
- 页写入时不能跨页,否则会回卷到页首覆盖数据
- 每次写入后需要5ms左右的编程时间
在原有驱动基础上增加:
c复制void W25Q64_Erase64K(uint8_t BlockNumber) {
W25Q64_WriteEnable();
MySPI_Start();
MySPI_SwapByte(0xD8); // 64KB块擦除指令
MySPI_SwapByte((BlockNumber*64*1024) >> 16);
MySPI_SwapByte((BlockNumber*64*1024) >> 8);
MySPI_SwapByte(BlockNumber*64*1024);
MySPI_Stop();
W25Q64_WaitBusy(); // 等待擦除完成
}
块分配:
镜像头结构:
关键功能实现:
c复制void MyFLASH_WriteFlash(uint32_t StartAddress, uint32_t *wData, uint32_t wnum) {
FLASH_Unlock();
FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
while(wnum >= 4) {
FLASH_ProgramWord(StartAddress, *wData);
StartAddress += 4;
wData++;
wnum -= 4;
}
FLASH_Lock();
}
在不同波特率下的数据传输稳定性:
| 波特率 | 最大持续速率 | CPU占用率 |
|---|---|---|
| 115200 | 90KB/s | <5% |
| 460800 | 350KB/s | 8% |
| 921600 | 700KB/s | 15% |
| 1.5M | 1.1MB/s | 25% |
实际项目中选择921600波特率作为最佳平衡点。
通过实测得到的各存储器件操作耗时:
| 操作类型 | 耗时(ms) |
|---|---|
| W25Q64扇区擦除(4KB) | 45 |
| W25Q64块擦除(64KB) | 180 |
| W25Q64页编程(256B) | 1.2 |
| 内部Flash页擦除(1KB) | 20 |
| 内部Flash字编程 | 0.05 |
优化措施:
DMA数据丢失:
EEPROM写入失败:
SPI Flash识别错误:
通过这个项目的开发,我总结了以下几点重要经验:
缓冲区设计是DMA应用的关键,合理的大小和结构能显著提升系统稳定性。在本项目中,2048字节的缓冲区配合10个数据块描述符的结构,在测试中即使连续传输10MB数据也未出现丢失。
Flash操作必须考虑意外断电的情况。我们的解决方案是在写入前先保存状态到EEPROM,并在重启时检查状态标志,确保能恢复中断的更新过程。
对于资源受限的MCU,内存管理需要特别关注。我们通过精确计算各模块的内存需求,并采用静态分配方式,避免了动态内存分配带来的不确定性。
这个BootLoader系统目前已经稳定运行在多个产品中,支持通过串口、蓝牙和Wi-Fi等多种方式进行固件更新。后续计划增加差分升级功能,以进一步减少传输数据量。