- 作者:老汪软件技巧
- 发表时间:2024-12-25 11:05
- 浏览量:
· 靠自媒体连续年入百万,靠自己买房买车。
我本科及硕士都是学机械,通过自学成功进入世界500强外企。我已经将自己的学习经验写成了一本电子书,超千人通过此书学习并转行成功。现在将这本电子书免费分享给大家,希望对你们有帮助:
电子书链接:
2. W25Q128介绍2.1 W25Q128型号介绍
W25Q128是华邦公司推出的一款容量为 128M-bit(相当于 16M-byte)的 SPI 接口的 NOR Flash 芯片。
给大家解释一下新单词:
它还有很多不同容量的好兄弟:
型号容量
W25Q256
256M bits = 32M bytes
W25Q128
128M bits = 16M bytes
W25Q64
64M bits = 8M bytes
W25Q32
32M bits = 4M bytes
W25Q16
16M bits = 2M bytes
W25Q80
8M bits = 1M bytes
2.2 W25Q128模块参数及引脚介绍
W25Q128 的模块各个厂家做的各有不同,只是长得不一样而已,使用方式、引脚都是一样的。下面我介绍的是我们自绘的 W25Q128 模块。
W25Q128参数:
参考接线如下:
W25Q128STM32备注
VCC
3.3
电源正极
CS
A4/B12
片选信号
DO
A6/B14
输出
GND
电源负极
CLK
A5/B13
时钟信号
DI
A7/B15
输入
如果你对引脚介绍有点懵,没关系,看看下面的 SPI 介绍你就明白了。
2.3 W25Q128存储架构
W25Q128 将 16M 的容量分为 256 个块(block),每块 64K 字节;每块分为 16 个扇区(sector),一扇区 4K 字节;每扇区分为 16 个页(page),一页 256 字节。
W25Q128 的最小擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。这样我们需要给 W25Q128 开辟一个至少 4K 的缓存区。
2.4 W25Q128常用指令
W25Q128 有非常多的指令,这里我们只介绍几个指令。
指令(HEX)名称作用
0x06
写使能
写入数据/擦除之前,必须先发送该指令
0x05
读 SR1
判定 FLASH 是否处于空闲状态,擦除用
0x03
读数据
读取数据
0x02
页写
写入数据,最多写256字节
0x20
扇区擦除
扇区擦除指令,最小擦除单位
具体工作时序如下:
写使能 (06H)
执行页写,扇区擦除,块擦除,片擦除,写状态寄存器等指令前,需要写使能。
拉低 CS 片选 → 发送 06H → 拉高 CS 片选
读SR1(05H)
拉低 CS 片选 → 发送 05H → 返回SR1的值 → 拉高 CS 片选
读数据(03H)
拉低 CS 片选 → 发送 03H → 发送24位地址 → 读取数据(1~n)→ 拉高 CS 片选
页写 (02H)
页写命令最多可以向FLASH传输256个字节的数据。
拉低 CS 片选 → 发送 02H → 发送24位地址 → 发送数据(1~n)→ 拉高 CS 片选
扇区擦除(20H)
写入数据前,检查内存空间是否全部都是 0xFF ,不满足需擦除。
拉低 CS 片选 → 发送 20H→ 发送24位地址 → 拉高 CS 片选
2.5 W25Q128状态寄存器
W25Q128 一共有 3 个状态寄存器,它们的作用是跟踪芯片的状态。
这里我们只介绍常用的状态寄存器 1:
我不过多介绍了,感兴趣的小伙伴可以去看芯片手册。
我们需要记住的是在状态寄存器 1 中:
BUSY:指示当前的状态,0 表示空闲;1 表示忙碌。
WEL:写使能锁定,为 1 时,可以操作页/扇区/块;为 0 时,写禁止。
3. SPI介绍
SPI(Serial Peripheral Interface)串行外设接口,是一种高速、全双工、同步的通信总线,仅使用四根线来连接芯片的管脚,节省了管脚和PCB布局空间。由于其简单易用的特性,越来越多的芯片集成了SPI通信协议。
3.1 SPI物理架构
SPI 工作模式:
SPI 通信分为主设备(Master)和从设备(Slave)。一个完整的 SPI 通信系统需要包含一个主设备和一个或多个从设备。主设备提供时钟信号,从设备接收时钟信号。所有的读写操作都由主设备发起。当存在多个从设备时,通过各自的片选信号进行管理。
SPI 是全双工,并且没有定义速度限制,一般的实现通常能达到甚至超过 10Mbps。
SPI 信号线:
SPI 一般使用四条信号线通信:
示意图如下:
3.2 SPI工作原理
SPI 通信中,主机和从机都有一个串行移位寄存器。主机通过向自己的 SPI 串行寄存器写入一个字节来发起传输。
首先,拉低相应的 SS 信号线,表示与特定的从机进行通信。主机通过发送 SCLK 时钟信号告诉从机进行数据的读写操作。注意,SCLK 时钟信号可以是低电平有效或高电平有效,因为SPI有不同的模式(下文将介绍)。主机将要发送的数据写入发送数据缓冲区,然后通过移位寄存器逐位地将数据传输给从机的串行移位寄存器,使用 MOSI 信号线进行传输。同时,从机的 MISO 接口接收到的数据也经过移位寄存器一位一位地移到接收缓冲区。从机也通过 MISO 信号线将自己串行移位寄存器中的内容返回给主机。同时,从机通过 MOSI 信号线接收主机发送的数据。这样,两个移位寄存器中的内容就被交换。
SPI通信只有主模式和从模式,没有明确的读和写操作之分。实际上,外设的写操作和读操作是同步完成的。在SPI通信中,发送一个数据必然会收到一个数据;如果要接收一个数据,就必须先发送一个数据。
如果只进行写操作,主机可以忽略从设备传输过来的字节,因为主机不需要接收数据。
如果主机要读取从设备的一个字节,那么主机必须发送一个空字节来引发从设备的传输。
3.3 SPI工作模式
SPI 有4种不同的工作模式。
从设备的 SPI 模式是厂家设定的,不可变。但主从设备必须在同一工作模式下才能正常工作。所以我们可以设置主设备的 SPI 模式。
那怎么设置呢?通过 CPOL(时钟极性)和 CPHA(时钟相位)来控制,具体如下:
CPOL(时钟极性)定义了时钟空闲状态电平:
CPHA(时钟相位)定义数据的采集时间:
总结如下表:
SPI 模式CPOLCPHA空闲时 SCK 时钟采样边沿采样时刻
低电平
上升沿
奇数边沿
低电平
下降沿
偶数边沿
高电平
下降沿
奇数边沿
高电平
上升沿
偶数边沿
四个模式的时序图如下,方便大家理解。绿线表示开始与结束,黄线表示数据采样,蓝线表示数据发送。
1.模式0(常用)CPOL = 0,CPHA = 0。
空闲时 SCLK 为低电平,采样时刻为第一个边沿,即上升沿。
2.模式1CPOL = 0,CPHA = 1。
空闲时 SCLK 为低电平,采样时刻为第二个边沿,即下降沿。
3.模式2,CPOL = 1,CPHA = 0。
空闲时 SCLK 为高电平,采样时刻为第一个边沿,即上升沿。
4.模式3(常用),CPOL = 1,CPHA = 1。
空闲时 SCLK 为高电平,采样时刻为第二个边沿,即上升沿。
4. 编程实战
实战目标:使用 SPI 通讯读写 W25Q128 模块。
4.1 硬件接线
本教程使用的硬件如下:
W25Q128STM32USB 转 TTL
VCC
3.3
CS
A4
CLK
A5
DO
A6
DI
A7
A10
TX
A9
RX
GND
烧录的时候接线如下表,如果不会烧录的话可以看我之前的文章【STM32下载程序的五种方法】。
ST-Link V2STM32
SWCLK
SWCLK
SWDIO
SWDIO
GND
GND
3.3V
3V3
接好如下图。开发板使用的是我们自绘的板子。大家也可以用自己的板子,只要是 STM32F103C8T6 主控芯片就行。
4.2 SPI初始化
SPI 的工作模式我们配置为 0,即 CPOL = 0,CPHA = 0。
STM32F1系列的 SPI 接口有两个,SPI1 和 SPI2,这里我们选择 SPI1,引脚对应关系如下:
void SPI1_Init(void)
{
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;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi1.Init.CRCPolynomial = 10;
HAL_SPI_Init(&hspi1);
}
void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle)
{
GPIO_InitTypeDef GPIO_InitStruct;
if(spiHandle->Instance==SPI1)
{
__HAL_RCC_SPI1_CLK_ENABLE(); /* SPI1时钟使能 */
__HAL_RCC_GPIOA_CLK_ENABLE();
/*
PA4 ------> SPI1_CS
PA5 ------> SPI1_SCK
PA6 ------> SPI1_MISO
PA7 ------> SPI1_MOSI
*/
GPIO_InitStruct.Pin = W25Q128_CS_GPIO_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(W25Q128_CS_GPIO_PORT, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_6;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
}
4.3 SPI读写一个字节
我们利用 HAL 库的 SPI 数据发送和接收函数 HAL_SPI_TransmitReceive 来读写一个字节。
函数原型:HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout)。
参数说明:
根据 SPI 的工作原理,我们发送一个字节的 data,得到一个字节的 rec_data。后续如果我们只需要读取一个字节,就发送一个无意义的 0xFF。
uint8_t read_write_one_byte(uint8_t data)
{
uint8_t rec_data = 0;
HAL_SPI_TransmitReceive(&hspi1, &data, &rec_data, 1, 1000);
return rec_data;
}
4.4 W25Q128初始化
初始化我们做个小检测,确保这个芯片是 W25Q128,而不是 W25Q64 或者 W25Q32。W25Q128 的芯片号是 0XEF17,从哪来的呢,当然是芯片手册啦。
void w25q128_init(void)
{
uint16_t flash_type;
read_write_one_byte(0xFF); /* 清除DR的作用 */
W25Q128_CS(1); /* 拉高片选 */
flash_type = w25q128_read_id(); /* 读取FLASH ID. */
if (flash_type == 0XEF17) /* FLASH芯片号0XEF17 */
printf("检测到W25Q128芯片\r\n");
}
uint16_t w25q128_read_id(void)
{
uint16_t deviceid;
W25Q128_CS(0); /* 拉低片选 */
read_write_one_byte(FLASH_ManufactDeviceID); /* 发送读 ID 命令 0x90 */
read_write_one_byte(0); /* 写入三个0 */
read_write_one_byte(0);
read_write_one_byte(0);
deviceid = read_write_one_byte(0xFF) << 8; /* 读取高8位字节 */
deviceid |= read_write_one_byte(0xFF); /* 读取低8位字节 */
W25Q128_CS(1); /* 拉高片选 */
return deviceid;
}
4.5 W25Q128等待空闲
前面我们提到状态寄存器 1 中 BUSY 是指示当前的状态,0 表示空闲;1 表示忙碌。
所以我们读取 W25Q128 的状态寄存器 1 的值,
static void w25q128_wait_busy(void)
{
while ((w25q128_rd_sr1() & 0x01) == 0x01); /* 等待BUSY位为0 */
}
uint8_t w25q128_rd_sr1(void)
{
uint8_t rec_data = 0;
W25Q128_CS(0); /* 拉低片选 */
read_write_one_byte(FLASH_ReadStatusReg1); /* 读状态寄存器1 0x05 */
rec_data = read_write_one_byte(0xFF);
W25Q128_CS(1); /* 拉高片选 */
return rec_data;
}
4.6 W25Q128写使能
写入数据/擦除之前必须写使能。
按照 W25Q128 写使能的工作时序:拉低 CS 片选 → 发送 06H → 拉高 CS 片选,编写代码。
void w25q128_write_enable(void)
{
W25Q128_CS(0); /* 拉低片选 */
read_write_one_byte(FLASH_WriteEnable); /* 发送写使能 0x06 */
W25Q128_CS(1); /* 拉高片选 */
}
4.7 W25Q128发送地址
read_write_one_byte 一次发送一字节数据,而 W25Q128 的地址有三字节,所以我们分三次发送。
static void w25q128_send_address(uint32_t address)
{
read_write_one_byte((uint8_t)((address)>>16)); /* 发送 bit23 ~ bit16 地址 */
read_write_one_byte((uint8_t)((address)>>8)); /* 发送 bit15 ~ bit8 地址 */
read_write_one_byte((uint8_t)address); /* 发送 bit7 ~ bit0 地址 */
}
4.8 W25Q128擦除一个扇区
传参 saddr 表示要擦除第几扇区,注意我们计算机是从0开始数数哦。剩下就是按工作时序写理论,注释写的很清楚啦,不多讲。
void w25q128_erase_sector(uint32_t saddr)
{
saddr *= 4096; /* 一扇区4096字节 */
w25q128_write_enable(); /* 写使能 */
w25q128_wait_busy(); /* 等待空闲 */
W25Q128_CS(0); /* 拉低片选 */
read_write_one_byte(FLASH_SectorErase); /* 发送扇区擦除命令 0x20 */
w25q128_send_address(saddr); /* 发送地址 */
W25Q128_CS(1); /* 拉高片选 */
w25q128_wait_busy(); /* 等待扇区擦除完成 */
}
4.9 W25Q128页写和读数据
传参 pbuf :要写入/读取的数据,addr:开始写入的地址,datalen:字节数。剩下就是按工作时序写理论,注释写的很清楚啦,不多讲。
void w25q128_write_page(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
uint16_t i;
w25q128_write_enable(); /* 写使能 */
W25Q128_CS(0); /* 拉低片选 */
read_write_one_byte(FLASH_PageProgram); /* 发送页写命令 0x02*/
w25q128_send_address(addr); /* 发送地址 */
for(i=0;i/* 循环写入 */
}
W25Q128_CS(1); /* 拉高片选 */
w25q128_wait_busy(); /* 等待写入结束 */
}
void w25q128_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
uint16_t i;
W25Q128_CS(0); /* 拉低片选 */
read_write_one_byte(FLASH_ReadData); /* 发送读取命令 0x03 */
w25q128_send_address(addr); /* 发送地址 */
for(i=0;i0XFF); /* 循环读取 */
}
W25Q128_CS(1); /* 拉高片选 */
}
4.10 主函数
我们向 W25Q128 写入一句“良许 嵌入式”,然后读出。
int main(void)
{
uint8_t datatemp[TEXT_SIZE];
HAL_Init(); /* 初始化HAL库 */
stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
uart1_init(115200); /* 串口初始化,波特率115200 */
printf("SPI通讯读写W25Q128模块...\r\n");
SPI1_Init();
w25q128_init();
/* 写入数据 */
sprintf((char *)datatemp, "良许 嵌入式");
w25q128_erase_sector(0); /* 擦除第一个扇区 */
w25q128_write_page(datatemp, 0x00000, TEXT_SIZE); /* 从第0位开始写 */
printf("数据写入完成!\r\n");
/* 读出数据 */
memset(datatemp, 0, TEXT_SIZE);
w25q128_read(datatemp, 0x00000, TEXT_SIZE); /* 从第0位开始读 */
printf("读出数据:%s\r\n", datatemp);
while(1)
{
}
}
4.11 最终效果
串口输出如下:
5. 小结
细心的小伙伴会发现我只是简单的写页、读数据、擦扇区。一页有256字节,那如果我第一页只写了50字节,又去第二页写100字节,这不是很浪费存储空间吗。不是我不会更完善的代码,源码我都藏着呢,只是作为入门教程这样的程度刚刚好,剩下的进阶优化就留作课后作业吧。
感谢各位看官,peace and love!
另外,想进大厂的同学,一定要好好学算法,这是面试必备的。这里准备了一份 BAT 大佬总结的 LeetCode 刷题宝典,很多人靠它们进了大厂。
刷题 | LeetCode算法刷题神器,看完 BAT 随你挑!
有收获?希望老铁们来个三连击,给更多的人看到这篇文章