3. 底层驱动开发:SPI控制器初始化、读写单字节/多字节、扇区擦除、整片擦除、状态寄存器操作
好,咱们直接进入正题。这一章讲的是底层驱动,说白了就是让MCU和NOR Flash能“对上话”。很多新手移植文件系统失败,问题都出在这一层——SPI时序不对、状态寄存器没查、擦除操作没等完成就往下走了。嗯,这些坑我都踩过。
3.1 SPI控制器初始化——先让硬件“醒过来”
我个人习惯,写任何外设驱动之前,先看数据手册里的时钟树。SPI模块的时钟源是哪个?有没有使能位?我记得有一次在STM32F103上移植,折腾了半天SPI不工作,最后发现是RCC里忘了开SPI1的时钟。
初始化SPI控制器,通常需要做这几件事:
- 使能时钟:RCC相关寄存器,打开SPI外设时钟和GPIO时钟
- 配置GPIO:SCK、MOSI、MISO、CS(片选)引脚,设置为复用功能推挽输出/输入
- 设置SPI模式:主模式、极性(CPOL)、相位(CPHA)、数据位宽(8位)、波特率
- 使能SPI:设置SPI_CR1的SPE位
这里有个关键点——波特率不能太高。NOR Flash的SPI时钟频率通常有上限,比如W25Q64最大支持80MHz,但很多便宜的Flash只能跑到50MHz。我在项目中遇到过,为了追求速度把SPI时钟设到72MHz,结果读出来的数据全是0xFF。后来降频到36MHz,一切正常。
核心原则:SPI时钟频率 = 系统时钟 / 分频系数。分频系数选2、4、8、16等,确保最终频率 ≤ Flash支持的最大频率。
// 以STM32 HAL库为例,初始化SPI1
void SPI_Flash_Init(void)
{
// 1. 使能时钟
__HAL_RCC_SPI1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// 2. 配置GPIO
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; // SCK, MISO, MOSI
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 配置SPI
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0
hspi1.Init.NSS = SPI_NSS_SOFT; // 软件控制CS
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; // 分频4
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
HAL_SPI_Init(&hspi1);
}
小技巧:CPOL和CPHA的组合,NOR Flash通常支持模式0(CPOL=0, CPHA=0)和模式3(CPOL=1, CPHA=1)。我习惯用模式0,兼容性最好。
3.2 读写单字节/多字节——最基础的数据搬运
SPI通信本质上是“主发从收”同时进行。你发一个字节,同时收到一个字节。读Flash的时候,主机要发送“假字节”来产生时钟,从机才会把数据送出来。
单字节读写很简单,但实际项目中很少用——效率太低。你想想看,读1KB数据要调用1024次SPI收发函数,每次都有函数调用开销。所以,多字节读写才是主力。
// 单字节读写(底层函数)
uint8_t SPI_Flash_ReadWriteByte(uint8_t data)
{
uint8_t rxdata;
HAL_SPI_TransmitReceive(&hspi1, &data, &rxdata, 1, 100);
return rxdata;
}
// 多字节读(连续读取)
void SPI_Flash_Read_Buffer(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead)
{
// 1. 拉低CS
CS_LOW();
// 2. 发送读命令 0x03
SPI_Flash_ReadWriteByte(0x03);
// 3. 发送24位地址(高字节在前)
SPI_Flash_ReadWriteByte((ReadAddr >> 16) & 0xFF);
SPI_Flash_ReadWriteByte((ReadAddr >> 8) & 0xFF);
SPI_Flash_ReadWriteByte(ReadAddr & 0xFF);
// 4. 连续读取数据
for(uint16_t i = 0; i < NumByteToRead; i++)
{
pBuffer[i] = SPI_Flash_ReadWriteByte(0xFF); // 发送0xFF产生时钟
}
// 5. 拉高CS
CS_HIGH();
}
注意:多字节读的时候,地址会自动递增。但地址递增是循环的——读到芯片末尾会回卷到0地址。所以跨页读取时要小心,最好按页(256字节)为单位操作。
3.3 扇区擦除与整片擦除——擦干净才能写
NOR Flash的特性是:写之前必须先擦除。而且擦除的最小单位是扇区(通常4KB),不能只擦一个字节。整片擦除就是把整个芯片清成0xFF。
擦除操作需要发送命令序列,并且要等待内部操作完成。怎么知道擦完了?查状态寄存器。
// 扇区擦除(4KB)
void SPI_Flash_Erase_Sector(uint32_t SectorAddr)
{
// 1. 写使能
SPI_Flash_Write_Enable();
// 2. 拉低CS
CS_LOW();
// 3. 发送扇区擦除命令 0x20
SPI_Flash_ReadWriteByte(0x20);
// 4. 发送24位地址
SPI_Flash_ReadWriteByte((SectorAddr >> 16) & 0xFF);
SPI_Flash_ReadWriteByte((SectorAddr >> 8) & 0xFF);
SPI_Flash_ReadWriteByte(SectorAddr & 0xFF);
// 5. 拉高CS
CS_HIGH();
// 6. 等待擦除完成
SPI_Flash_Wait_Busy();
}
// 整片擦除
void SPI_Flash_Erase_Chip(void)
{
SPI_Flash_Write_Enable();
CS_LOW();
SPI_Flash_ReadWriteByte(0xC7); // 整片擦除命令
CS_HIGH();
SPI_Flash_Wait_Busy(); // 整片擦除耗时较长,可能几秒
}
避坑指南:我曾经在整片擦除时没有等待完成就断电了,结果芯片里数据全乱套。后来加了个超时机制——如果等待超过5秒还没完成,就报错并重试。工业产品里,超时处理是必须的。
3.4 状态寄存器操作——Flash的“健康检查”
状态寄存器(Status Register)是Flash的“仪表盘”。最常用的是忙标志位(BUSY)和写使能锁存位(WEL)。
| 位 | 名称 | 说明 |
|---|---|---|
| BIT0 | BUSY | 1=忙(正在擦除/写入),0=空闲 |
| BIT1 | WEL | 1=写使能已锁存,0=未使能 |
| BIT2 | BP0~BP2 | 块保护位,用于写保护 |
// 读取状态寄存器
uint8_t SPI_Flash_ReadSR(void)
{
uint8_t byte;
CS_LOW();
SPI_Flash_ReadWriteByte(0x05); // 读状态寄存器命令
byte = SPI_Flash_ReadWriteByte(0xFF);
CS_HIGH();
return byte;
}
// 写使能
void SPI_Flash_Write_Enable(void)
{
CS_LOW();
SPI_Flash_ReadWriteByte(0x06); // 写使能命令
CS_HIGH();
}
// 等待忙状态结束
void SPI_Flash_Wait_Busy(void)
{
uint32_t timeout = 0;
while((SPI_Flash_ReadSR() & 0x01) == 0x01) // BUSY位为1
{
timeout++;
if(timeout > 0xFFFFFF) // 超时处理
{
printf("Flash busy timeout!\r\n");
break;
}
}
}
经验之谈:每次写操作(页编程、擦除)之前,一定要先发写使能命令(0x06)。否则Flash会忽略后续操作。我见过有人忘了这一步,调试了一整天。
3.5 本章知识体系
下面这张图总结了底层驱动的核心流程,你可以把它当作“操作手册”来用:
这张图把整个流程串起来了。你从初始化开始,根据需求选择读、写或擦除,最后都要检查状态寄存器。记住:写和擦除必须等忙状态结束,读操作不需要等。
好了,底层驱动这块就讲到这里。代码虽然不多,但每个细节都关系到系统稳定性。下一章我们会把这些驱动封装成文件系统需要的接口函数。
公众号:蓝海资料掘金营,微信deep3321