江协科技STM32学习- P38 软件SPI读写W25Q64
🚀write in front🚀
🔎大家好,我是黄桃罐头,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝💬本系列哔哩哔哩江科大STM32的视频为主以及自己的总结梳理📚
🚀Projeet source code🚀
💾工程代码放在了本人的Gitee仓库:iPickCan (iPickCan) - Gitee.com
引用:
STM32入门教程-2023版 细致讲解 中文字幕_哔哩哔哩_bilibili
Keil5 MDK版 下载与安装教程(STM32单片机编程软件)_mdk528-CSDN博客
STM32之Keil5 MDK的安装与下载_keil5下载程序到单片机stm32-CSDN博客
0. 江协科技/江科大-STM32入门教程-各章节详细笔记-查阅传送门-STM32标准库开发_江协科技stm32笔记-CSDN博客
【STM32】江科大STM32学习笔记汇总(已完结)_stm32江科大笔记-CSDN博客
江科大STM32学习笔记(上)_stm32博客-CSDN博客
STM32学习笔记一(基于标准库学习)_电平输出推免-CSDN博客
STM32 MCU学习资源-CSDN博客
stm32学习笔记-作者: Vera工程师养成记
stem32江科大自学笔记-CSDN博客
术语:
英文缩写 | 描述 |
GPIO:General Purpose Input Onuput | 通用输入输出 |
AFIO:Alternate Function Input Output | 复用输入输出 |
AO:Analog Output | 模拟输出 |
DO:Digital Output | 数字输出 |
内部时钟源 CK_INT:Clock Internal | 内部时钟源 |
外部时钟源 ETR:External Trigger | 时钟源 External 触发 |
外部时钟源 ETR:External Trigger mode 1 | 外部时钟源 External 触发 时钟模式1 |
外部时钟源 ETR:External Trigger mode 2 | 外部时钟源 External 触发 时钟模式2 |
外部时钟源 ITRx:Internal Trigger inputs | 外部时钟源,ITRx (Internal trigger inputs)内部触发输入 |
外部时钟源 TIx:exTernal Input pin | 外部时钟源 TIx (external input pin)外部输入引脚 |
CCR:Capture/Comapre Register | 捕获/比较寄存器 |
OC:Output Compare | 输出比较 |
IC:Input Capture | 输入捕获 |
TI1FP1:TI1 Filter Polarity 1 | Extern Input 1 Filter Polarity 1,外部输入1滤波极性1 |
TI1FP2:TI1 Filter Polarity 2 | Extern Input 1 Filter Polarity 2,外部输入1滤波极性2 |
DMA:Direct Memory Access | 直接存储器存取 |
正文:
0. 概述
从 2024/06/12 定下计划开始学习下江协科技STM32课程,接下来将会按照哔站上江协科技STM32的教学视频来学习入门STM32 开发,本文是视频教程 P2 STM32简介一讲的笔记。
感兴趣的一件事:
STM32HAL-最简单的长、短、多击按键框架
STM32HAL-最简单的长、短、多击按键框架(多按键)-CSDN博客
这就是一个简易的操作系统代码(瞎说的),基于时间片调度,没有抢占,但是有任务优先级。也就100行。
https://www.zhihu.com/question/658017543
1.🚚 软件SPI读写W25Q64
接线图
CS(片选)接到PA4 ,DO(从机输出)接到PA6 ,CLK时钟接到PA5 ,DI(从机输入)接到PA7,当然我这里引脚其实并不是任意选的,实际上是接到了硬件SPI的引脚上,这样的话软件SPI和硬件SPI都可以任意切换。
PA6为上拉输出入,PA4,PA5 ,PA7为推挽输出。
交换字节:W25Q64支持模式0和模式3,这里一般选择模式0。
程序的整体框架:
指令集:读取ID号时序:起始,先交换发送指令9F,随后连续交换接收3个字节,停止
第一个字节是厂商ID,后两个是设备ID,其中设备ID高8位,表示存储器类型,低8位表示容量。
- 💎第一个是写使能指令,发送一个指令码06
- 💎第二个是读状态寄存器1,作用是判断芯片是不是忙状态,要读取BUSY位,是否置1,1表示芯片在忙,0表示芯片不忙了。还要实现一个等待BUSY为0的函数,调用这个函数,BUSY为1,进入等待;BUSY为0,函数执行完毕。
- 💎第三个是页编程函数,先发送一个指令码02,再发3个字节的地址,最后发数据。
- 💎第四个扇区擦除,执行扇区擦除,先发送指令20,再发送3个字节的地址,这样指定地址,所在的扇区就会擦除。
最后一个读取数据,流程是:交换发送指令03,再发送3个字节的地址,随后转入接收,就可以依次接收数据
MySPI.c
#include "stm32f10x.h" // Device header
#include "MySPI.h"
void MySPI_W_SS(uint8_t bitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)bitValue);
}
void MySPI_W_SCK(uint8_t bitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)bitValue);
}
void MySPI_W_MOSI(uint8_t bitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)bitValue);
}
uint8_t MySPI_R_MISO(void)
{
return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}
void MySPI_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef gpioInitStructure;
gpioInitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
gpioInitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
gpioInitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpioInitStructure);
gpioInitStructure.GPIO_Mode = GPIO_Mode_IPU;
gpioInitStructure.GPIO_Pin = GPIO_Pin_6;
gpioInitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpioInitStructure);
MySPI_W_SS(1);
MySPI_W_SCK(0); //use SPI mode0
}
void MySPI_Start(void)
{
MySPI_W_SS(0);
}
void MySPI_Stop(void)
{
MySPI_W_SS(1);
}
uint8_t MySPI_SwapByte(uint8_t byteSend)
{
uint8_t byteRecv = 0;
for(int i=0; i<8; i++)
{
MySPI_W_MOSI(byteSend & (0x80 >> i)); //SPI MOSI发送数据
MySPI_W_SCK(1); //SPI CLK上升沿
if(MySPI_R_MISO()) byteRecv |= (0x80 >> i); //SPI MISO读取数据
MySPI_W_SCK(0); //SPI下降沿
}
return byteRecv;
}
W25Q64.c
#include "stm32f10x.h" // Device header
#include "MySPI.h"
#include "W25Q64.h"
void W25Q64_Init(void)
{
MySPI_Init();
}
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
uint8_t Data = 0;
MySPI_Start(); //SPI Start
MySPI_SwapByte(0x9F);
*MID = MySPI_SwapByte(0xFF);
Data = MySPI_SwapByte(0xFF);
Data = Data<< 8 | MySPI_SwapByte(0xFF);
*DID = Data;
MySPI_Stop(); //SPI Stop
}
Main.c
#include "stm32f10x.h" // Device header
#include "oled.h"
#include "Countersensor.h"
#include "Encoder.h"
#include "Timer.h"
#include "AD.h"
#include "Delay.h"
#include "MyDMA.h"
#include "UART.h"
#include <stdio.h>
#include "Key.h"
#include "String.h"
#include "LED.h"
#include "MyI2C.h"
#include "MP6050.h"
#include "MP6050_Reg.h"
#include "W25Q64.h"
uint8_t MID;
uint16_t DID;
int main(int argc, char *argv[])
{
OLED_Init();
W25Q64_Init();
W25Q64_ReadID(&MID, &DID);
OLED_ShowHexNum(1, 1, MID, 2);
OLED_ShowHexNum(1, 4, DID, 4);
while(1)
{
}
return 1;
}
💎现象
注意:写入数据前必须擦除,如不执行擦除,读出的数据=原始数据&写入的数据。
1.1 实验1-读取芯片ChipID
1.2实验2-写入读取W25Q64
写入读取W25Q64,写入第一个扇区起始地址 0x000000 写入数组 [0x01,0x02,0x03,0x04]后再独处数组显示在OLED屏幕上。
源码:
W25Q64.C
#include "stm32f10x.h" // Device header
#include "MySPI.h"
#include "W25Q64.h"
#include "W25Q64_Ins.h"
void W25Q64_Init(void)
{
MySPI_Init();
}
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
uint8_t Data = 0;
MySPI_Start(); //SPI Start
MySPI_SwapByte(W25Q64_JEDEC_ID);
*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
Data = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
Data = Data<< 8 | MySPI_SwapByte(W25Q64_DUMMY_BYTE);
*DID = Data;
MySPI_Stop(); //SPI Stop
}
void W25Q64_WriteEnable(void)
{
MySPI_Start();
MySPI_SwapByte(W25Q64_WRITE_ENABLE);
MySPI_Stop();
}
uint8_t W25Q64_Read_StatusReg1(void)
{
uint8_t Data = 0;
MySPI_Start();
MySPI_SwapByte(W25Q64_WRITE_STATUS_REGISTER);
Data = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
MySPI_Stop();
return Data;
}
void W25Q64_WaitBusy(void)
{
MySPI_Start();
MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
while((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & (0x01)) == 0x01);
MySPI_Stop();
}
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArry, uint16_t Count)
{
W25Q64_WriteEnable(); //写使能
MySPI_Start();
MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
MySPI_SwapByte(Address >> 16);
MySPI_SwapByte(Address >> 8);
MySPI_SwapByte(Address);
for(uint16_t i=0; i<Count; i++)
{
MySPI_SwapByte(DataArry[i]);
}
MySPI_Stop();
//事后等待
//事前等待,读取操作也需要等待
W25Q64_WaitBusy();
}
void W25Q64_SectorErase(uint32_t Address)
{
W25Q64_WriteEnable(); //写使能
MySPI_Start();
MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
MySPI_SwapByte(Address >> 16);
MySPI_SwapByte(Address >> 8);
MySPI_SwapByte(Address);
MySPI_Stop();
//事后等待
//事前等待,读取操作也需要等待
W25Q64_WaitBusy();
}
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArry, uint32_t Count)
{
MySPI_Start();
MySPI_SwapByte(W25Q64_READ_DATA);
MySPI_SwapByte(Address >> 16);
MySPI_SwapByte(Address >> 8);
MySPI_SwapByte(Address);
for(uint16_t i=0; i<Count; i++)
{
DataArry[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
}
MySPI_Stop();
}
W25Q64_Ins.h
#ifndef __W25Q64_INS_H
#define __W25Q64_INS_H
#define W25Q64_WRITE_ENABLE 0x06
#define W25Q64_WRITE_DISABLE 0x04
#define W25Q64_READ_STATUS_REGISTER_1 0x05
#define W25Q64_READ_STATUS_REGISTER_2 0x35
#define W25Q64_WRITE_STATUS_REGISTER 0x01
#define W25Q64_PAGE_PROGRAM 0x02
#define W25Q64_QUAD_PAGE_PROGRAM 0x32
#define W25Q64_BLOCK_ERASE_64KB 0xD8
#define W25Q64_BLOCK_ERASE_32KB 0x52
#define W25Q64_SECTOR_ERASE_4KB 0x20
#define W25Q64_CHIP_ERASE 0xC7
#define W25Q64_ERASE_SUSPEND 0x75
#define W25Q64_ERASE_RESUME 0x7A
#define W25Q64_POWER_DOWN 0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE 0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET 0xFF
#define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB
#define W25Q64_MANUFACTURER_DEVICE_ID 0x90
#define W25Q64_READ_UNIQUE_ID 0x4B
#define W25Q64_JEDEC_ID 0x9F
#define W25Q64_READ_DATA 0x03
#define W25Q64_FAST_READ 0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B
#define W25Q64_FAST_READ_DUAL_IO 0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT 0x6B
#define W25Q64_FAST_READ_QUAD_IO 0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3
#define W25Q64_DUMMY_BYTE 0xFF
#endif
Main.c
#include "stm32f10x.h" // Device header
#include "oled.h"
#include "Countersensor.h"
#include "Encoder.h"
#include "Timer.h"
#include "AD.h"
#include "Delay.h"
#include "MyDMA.h"
#include "UART.h"
#include <stdio.h>
#include "Key.h"
#include "String.h"
#include "LED.h"
#include "MyI2C.h"
#include "MP6050.h"
#include "MP6050_Reg.h"
#include "W25Q64.h"
uint8_t MID;
uint16_t DID;
uint8_t ArryWrite[4] = {0x01, 0x02, 0x03, 0x04};
uint8_t ArryRead[4];
int main(int argc, char *argv[])
{
OLED_Init();
W25Q64_Init();
W25Q64_ReadID(&MID, &DID);
OLED_ShowHexNum(1, 1, MID, 2);
OLED_ShowHexNum(1, 4, DID, 4);
OLED_ShowString(2,1,"W");
OLED_ShowString(3,1,"R");
W25Q64_SectorErase(0x000000);
W25Q64_PageProgram(0x000000, ArryWrite, 4);
W25Q64_ReadData(0x000000, ArryRead, 4);
OLED_ShowHexNum(2, 3, ArryWrite[0], 2);
OLED_ShowHexNum(2, 6, ArryWrite[1], 2);
OLED_ShowHexNum(2, 9, ArryWrite[2], 2);
OLED_ShowHexNum(2, 12, ArryWrite[3], 2);
OLED_ShowHexNum(3, 3, ArryRead[0], 2);
OLED_ShowHexNum(3, 6, ArryRead[1], 2);
OLED_ShowHexNum(3, 9, ArryRead[2], 2);
OLED_ShowHexNum(3, 12, ArryRead[3], 2);
while(1)
{
}
return 1;
}
1.3实验3-擦除W25Q64扇区
在Mian.c中擦除扇区之后,注释掉PageProgram的函数,擦除扇区后直接读删除0x000000起始的4个字节,读取到的值都是 0xFF,表示扇区擦除后Flash里都是0.
W25Q64_SectorErase(0x000000);
//W25Q64_PageProgram(0x000000, ArryWrite, 4);
W25Q64_ReadData(0x000000, ArryRead, 4);
1.4实验4-验证Flash中的数据只能从1写0,不能从0写1
验证Flash中的数据只能从1写0,不能从0写1
写入数据之前必须擦除,否则写入数据就是原始数据和新写入数据的按位与的结果。
依次在0x000000地址处,先也擦除载依次协议0xAA, 0xBB, 0xCC , 0xDD。读取出来的结果是
uint8_t ArryWrite[4] = {0xAA, 0xBB, 0xCC, 0xDD};
uint8_t ArryRead[4];
//页擦除
W25Q64_SectorErase(0x000000);
//依次写入0xAA 0xBB 0xCC 0xDD
W25Q64_PageProgram(0x000000, ArryWrite, 4);
W25Q64_ReadData(0x000000, ArryRead, 4);
注释掉页擦除,然后网0x000000地址处,尝试写入 0x55 0x66 0x77 0x88,再读取出来Flash地址处的值显示在OLED屏幕上。读取出来的值不是写入的值,和预期的结果不一样,愿意是因为Flash只能支持从1写0不能从0写1。
uint8_t ArryWrite[4] = {0x55, 0x66, 0x77, 0x88};
uint8_t ArryRead[4];
//页擦除
//W25Q64_SectorErase(0x000000);
//依次写入0xAA 0xBB 0xCC 0xDD
W25Q64_PageProgram(0x000000, ArryWrite, 4);
W25Q64_ReadData(0x000000, ArryRead, 4);
Flahs的特性就是:“只能从1写0不能从0写1”,所以写入数据之前必须擦除,否则写入数据就是原始数据和新写入数据的按位与的结果。
1.5 实验5-验证写数据不能跨页
从第一页的0xFF地址开始写,因为一页大小是256字节,所以从0xff开始写4个字节,后面3个字节会发生跨页。然后从地址0xFF读取4个字节,看从0xFF开始的4个字节地址里Flash中的数据。
uint8_t ArryWrite[4] = {0x66, 0x77, 0x88, 0x99};
uint8_t ArryRead[4];
//页擦除
W25Q64_SectorErase(0x000000);
//从第一页的0xFF地址开始写,因为一页大小是256字节,所以从0xff开始写4个字节,后面3个字节会发生跨页
W25Q64_PageProgram(0x0000FF, ArryWrite, 4);
//从地址0xFF读取4个字节
W25Q64_ReadData(0x0000FF, ArryRead, 4);
看到0xFF地址后面的3个字节的内容是第二页的起始3个字节的内容,其中数据时0xFF也就是扇区擦除后的内容全为1(0xFF)。
然后把读取地址修改为 0x00,看写入后第一个扇区的的前4个字节的内容
W25Q64_SectorErase(0x000000);
W25Q64_PageProgram(0x0000FF, ArryWrite, 4);
W25Q64_ReadData(0x000000, ArryRead, 4);
看到0x00第一页起始三个字节的内容是写入数据跨页之后重新回卷到页的起始地址写入的数据。
结论是:Flash不能跨页写,如果写入数据长度超过页的结尾将回卷到页的起始地址开始写。
Flash不能跨页写,但是可以跨页读取。