1. 项目概述与硬件准备
在嵌入式开发中,将固件存储在外部SPI Flash是一种常见的解决方案。STM32F407 Discovery开发板搭配W25Q16 SPI Flash芯片,可以构建一个灵活可靠的固件存储系统。这个方案特别适合需要远程更新固件或存储大量配置数据的应用场景。
硬件准备清单:
- STM32F407 Discovery开发板(主控为STM32F407VG)
- W25Q16 SPI Flash芯片(2MB容量)
- 杜邦线若干
- USB Type-A转Micro-B数据线
硬件连接示意图:
| W25Q16引脚 | STM32F407引脚 | 功能说明 |
|---|---|---|
| CS | PB12 | 片选信号 |
| DO | PB14 | 数据输出 |
| CLK | PB13 | 时钟信号 |
| DI | PB15 | 数据输入 |
| VCC | 3.3V | 电源正极 |
| GND | GND | 电源地 |
注意:实际连接时建议使用示波器检查信号质量,特别是当SPI时钟频率较高时。信号完整性不佳可能导致通信失败。
2. 开发环境配置与工程创建
2.1 STM32CubeMX配置
首先使用STM32CubeMX创建基础工程:
- 选择MCU型号为STM32F407VG
- 开发板选择STM32F407 Discovery
- 系统时钟配置为168MHz(HSE晶振输入)
SPI2外设配置要点:
- 模式:Full-Duplex Master
- 硬件NSS:Disable(使用软件控制片选)
- 预分频:8(得到21MHz时钟)
- 数据大小:8位
- 时钟极性:Low
- 时钟相位:1 Edge
c复制// 生成的SPI初始化代码片段
hspi2.Instance = SPI2;
hspi2.Init.Mode = SPI_MODE_MASTER;
hspi2.Init.Direction = SPI_DIRECTION_2LINES;
hspi2.Init.DataSize = SPI_DATASIZE_8BIT;
hspi2.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi2.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi2.Init.NSS = SPI_NSS_SOFT;
hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8;
hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi2.Init.TIMode = SPI_TIMODE_DISABLE;
hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi2.Init.CRCPolynomial = 10;
2.2 USB CDC配置
启用USB OTG FS功能:
- 模式:Device Only
- 速度:Full Speed
- 启用VBUS sensing
在Middleware中:
- 启用USB Device
- 选择Communication Device Class (CDC)
- 配置CDC参数:
- 接收缓冲区大小:512字节
- 发送缓冲区大小:512字节
- 最大包大小:64字节
c复制// USB CDC描述符配置示例
USBD_CDC_ItfTypeDef USBD_Interface_fops_FS = {
CDC_Init_FS,
CDC_DeInit_FS,
CDC_Control_FS,
CDC_Receive_FS
};
3. W25Q16驱动实现详解
3.1 基本操作函数
W25Q16的操作基于一系列标准SPI命令,以下是核心驱动函数的实现:
c复制// 片选控制宏
#define W25Q_CS_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET)
#define W25Q_CS_HIGH() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET)
// SPI全双工单字节传输
uint8_t SPI_RW(uint8_t byte) {
uint8_t d_read = 0;
HAL_SPI_TransmitReceive(&hspi2, &byte, &d_read, 1, 100);
return d_read;
}
// 写使能命令(必须在对Flash进行写操作前执行)
void W25Q_WriteEnable(void) {
W25Q_CS_LOW();
SPI_RW(0x06); // WREN指令
W25Q_CS_HIGH();
}
// 等待Flash操作完成
void W25Q_WaitBusy(void) {
uint8_t status = 0;
do {
W25Q_CS_LOW();
SPI_RW(0x05); // 读状态寄存器1
status = SPI_RW(0xFF);
W25Q_CS_HIGH();
} while (status & 0x01); // 检查BUSY位
}
3.2 页编程与读取
W25Q16的编程以页为单位(256字节),以下是关键操作函数:
c复制// 页编程函数
void W25Q_WritePage(uint32_t Addr, uint8_t* pBuf, uint16_t Len) {
W25Q_WriteEnable();
W25Q_CS_LOW();
SPI_RW(0x02); // 页编程指令
// 发送24位地址
SPI_RW((uint8_t)(Addr >> 16));
SPI_RW((uint8_t)(Addr >> 8));
SPI_RW((uint8_t)Addr);
// 发送数据
HAL_SPI_Transmit(&hspi2, pBuf, Len, 100);
W25Q_CS_HIGH();
W25Q_WaitBusy(); // 等待编程完成
}
// 读取数据函数
void W25Q_ReadBytes(uint32_t Addr, uint8_t *pBuf, uint32_t Len) {
W25Q_CS_LOW();
(void)SPI_RW(0x03U); // 读数据指令
// 发送24位地址
(void)SPI_RW((uint8_t)(Addr >> 16));
(void)SPI_RW((uint8_t)(Addr >> 8));
(void)SPI_RW((uint8_t)Addr);
// 读取数据
for (uint32_t i = 0; i < Len; i++) {
pBuf[i] = SPI_RW(0xFFU);
}
W25Q_CS_HIGH();
}
重要提示:W25Q16的页编程操作只能在已擦除的页上进行。尝试对未擦除的页进行编程会导致数据错误。在执行编程操作前,必须确保目标区域已被擦除。
4. 固件烧录系统设计
4.1 数据流架构
整个固件烧录系统采用以下数据流:
- PC端通过USB虚拟串口发送固件数据
- STM32接收数据并缓存到RAM
- 当缓存达到256字节时,写入Flash
- 最后不足256字节的数据通过按键触发写入
mermaid复制graph TD
A[PC端固件文件] -->|USB CDC| B(STM32 USB接收缓冲区)
B --> C{缓冲区满256字节?}
C -->|是| D[写入Flash]
C -->|否| E[继续接收]
F[用户按键] --> G[写入剩余数据]
4.2 主循环处理逻辑
主程序中实现了以下核心功能:
c复制#define W25Q_HEX_DUMP_LEN (65536u)
uint8_t Page_Buffer[512]; // 双缓冲设计
volatile uint32_t Buf_Idx = 0;
volatile uint32_t Data_Ready = 0;
volatile uint8_t HexDump_Requested = 0;
uint32_t Current_Addr = 0;
void main(void) {
// 初始化代码...
while (1) {
if (Data_Ready) {
// 写入256字节到Flash
W25Q_WritePage(Current_Addr, Page_Buffer, 256);
Current_Addr += 256;
// 处理缓冲区剩余数据
if(Buf_Idx > 256) {
memcpy(Page_Buffer, &Page_Buffer[256], Buf_Idx - 256);
}
Buf_Idx -= 256;
Data_Ready = 0;
// 通知PC可以发送下一包数据
CDC_Transmit_FS((uint8_t*)"OK\r\n", 4);
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
}
// 处理十六进制转储请求
if (HexDump_Requested) {
HexDump_Requested = 0;
W25Q_DumpHexToCdc(0, W25Q_HEX_DUMP_LEN);
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
}
// 处理用户按键(写入剩余数据)
if (HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin) == GPIO_PIN_SET) {
HAL_Delay(40); // 消抖
if (HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin) == GPIO_PIN_SET) {
while (HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin) == GPIO_PIN_SET) {
HAL_Delay(2);
}
if (Data_Ready == 0U && Buf_Idx > 0U) {
FlushRemainderToFlash();
}
}
}
}
}
5. USB CDC通信实现
5.1 数据接收处理
USB CDC接收回调函数负责处理来自PC的数据:
c复制static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len) {
// 处理特殊命令
if (*Len == 1U) {
if (Buf[0] == 'H') { // 十六进制转储请求
HexDump_Requested = 1U;
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
return USBD_OK;
}
if (Buf[0] == 'E') { // 全片擦除请求
W25Q_EraseChip();
CDC_Transmit_FS((uint8_t*)"Erased\r\n", 8);
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
return USBD_OK;
}
}
// 存储接收到的数据
memcpy(&Page_Buffer[Buf_Idx], Buf, *Len);
Buf_Idx += *Len;
// 检查是否达到写入条件
if (Buf_Idx >= 256) {
Data_Ready = 1;
} else {
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
}
return USBD_OK;
}
5.2 数据发送处理
为确保可靠的数据传输,实现了阻塞式发送函数:
c复制static void CDC_TransmitBlocking(const uint8_t *data, uint16_t len) {
uint32_t t0 = HAL_GetTick();
while (CDC_Transmit_FS((uint8_t *)data, len) == USBD_BUSY) {
if ((HAL_GetTick() - t0) > 5000U) {
break; // 超时处理
}
}
}
6. 上位机操作指南
6.1 固件烧录步骤
- 连接开发板到PC,等待识别为虚拟串口
- 打开串口终端(如Tera Term),选择正确的COM端口
- 配置串口参数:
- 波特率:无意义(USB虚拟串口)
- 数据位:8
- 停止位:1
- 校验位:无
- 在终端菜单中选择"文件 → 发送文件"
- 选择要烧录的二进制文件
- 等待传输完成(终端会显示传输进度)
- 按下开发板上的用户按键B1,确保所有数据写入Flash
6.2 烧录验证
启用十六进制转储功能验证烧录结果:
- 在串口终端发送单个字符'H'
- 等待接收Flash内容转储
- 检查转储数据是否与原始文件一致
c复制void W25Q_DumpHexToCdc(uint32_t addr, uint32_t len) {
uint8_t row[16];
char line[96];
for (uint32_t off = 0; off < len; off += 16U) {
uint32_t n = (off + 16 > len) ? len - off : 16;
W25Q_ReadBytes(addr + off, row, n);
// 格式化十六进制输出
int p = snprintf(line, sizeof(line), "%08lX:", (unsigned long)(addr + off));
for (uint32_t i = 0; i < n; i++) {
p += snprintf(line + p, sizeof(line) - p, " %02X", row[i]);
}
line[p++] = '\r';
line[p++] = '\n';
line[p] = '\0';
CDC_TransmitBlocking((const uint8_t *)line, p);
}
}
7. 常见问题与解决方案
7.1 SPI通信失败
症状:
- Flash无响应
- 读取的数据全为0xFF或随机值
排查步骤:
- 检查硬件连接是否正确,特别是片选信号
- 用示波器检查SPI时钟和数据信号
- 降低SPI时钟频率测试(如改为预分频32)
- 检查Flash供电是否稳定(3.3V±10%)
7.2 USB CDC无法识别
症状:
- PC无法识别虚拟串口
- 枚举失败
解决方案:
- 检查USB连接线是否正常
- 确保正确配置了USB时钟(48MHz)
- 检查USB DP引脚是否连接了1.5k上拉电阻
- 更新STM32 USB驱动
7.3 Flash写入失败
症状:
- 写入后读取数据不一致
- 写入操作返回错误
可能原因及解决:
- 目标区域未擦除 → 先执行擦除操作
- 写入地址超出范围 → 检查地址计算
- 电源不稳定导致写入错误 → 加强电源滤波
- 连续写入过快 → 增加写入间隔
8. 性能优化建议
- 双缓冲设计:使用两个256字节缓冲区交替工作,提高吞吐量
- DMA传输:对SPI和USB使用DMA传输,减轻CPU负担
- 压缩传输:在上位机实现压缩算法,减少传输数据量
- 校验机制:添加CRC校验确保数据传输完整性
- 断点续传:记录已传输位置,支持传输中断后继续
c复制// 双缓冲实现示例
uint8_t Page_Buffer[2][256];
volatile uint8_t active_buf = 0;
void CDC_Receive_FS(uint8_t* Buf, uint32_t *Len) {
memcpy(&Page_Buffer[active_buf][buf_idx], Buf, *Len);
buf_idx += *Len;
if (buf_idx >= 256) {
Data_Ready = 1;
active_buf ^= 1; // 切换缓冲区
buf_idx = 0;
}
}
通过这个完整的实现方案,开发者可以构建一个可靠的固件烧录系统,适用于各种需要远程更新或大容量存储的STM32应用场景。实际项目中,还可以根据需要扩展更多功能,如加密传输、多固件版本管理等。
