【自学嵌入式:stm32单片机】软件SPI读写W25Q64

软件SPI读写W25Q64

image
我用硬件I2C读取OLED屏幕,采用I2C2接口,对应的OLED屏幕的SCL SDA接入到PB10和PB11,和他的接线不同,要注意

W25Q64接线:CS片选接到PA4,DO从机输出,接到PA6,CLK时钟,接到PA5,DI从机输入,接到PA7,这个引脚实际上也是硬件SPI的引脚,这样软件SPI和硬件SPI都可以任意切换
这里面我把硬件I2C的速度调整到了100KHz,因为OLED屏幕读取有些不正常

代码实现

标准库实现

已开源到:https://gitee.com/qin-ruiqian/jiangkeda-stm32

MYSPI.h

#ifndef __MYSPI_H
#define __MYSPI_H

void MYSPI_Init(void);
void MYSPI_Start(void);
void MYSPI_Stop(void);
uint8_t MYSPI_SwapByte(uint8_t ByteSend);

#endif

MYSPI.c

#include "stm32f10x.h"                  // Device header

//由于SPI速度非常快,操作完引脚后不需要加延时
#define MYSPI_W_SS(x) GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)(x))
#define MYSPI_W_SCK(x) GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)(x))
#define MYSPI_W_MOSI(x) GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)(x))
#define MYSPI_R_MISO() GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6)

//初始化SPI通信
void MYSPI_Init(void)
{
    //PA6 主机输入 上拉输入其他3个为推挽输出
    //打开APB2总线的GPIOA外设并开启时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

    //初始化后引脚默认电平,SS应该为高电平,默认状态,不选中从机
    //模式0,SCK默认低电平
    MYSPI_W_SS(1);
    MYSPI_W_SCK(0);
}

//起始信号
void MYSPI_Start(void)
{
    //置SS低电平
    MYSPI_W_SS(0);
}

//终止信号
void MYSPI_Stop(void)
{
    //置SS高电平 
    MYSPI_W_SS(1);
}

//交换一个字节
//ByteSend是要交换的一个字节
//要通过交换一个字节的时序发送出去
uint8_t MYSPI_SwapByte(uint8_t ByteSend)
{
    uint8_t i = 0;
    // uint8_t ByteReceive = 0; //用于接收的字节
    //一般来说,SS下降沿或SCK下降沿,再数据移除
    //硬件SPI使用了移位寄存器电路,所以SCK下降沿然后MOSI翻转电平几乎是同时发生的,软件SPI,程序一条一条执行的,不可能同时完成两个动作,软件SPI需要看成先后执行的逻辑
    //先SS下降沿,再移出数据(SS下降沿,Start起始信号已经做了)
    //再SCK上升沿,再移入数据
    //再SCK下降沿,再移除数据
    for(i = 0; i < 8 ; i++)
    {
        // //0x80右移i位的作用就是
        // //用来挑出数据的某一位或者某几位
        // //或者描述方式就是用来屏蔽其他的无关位
        // //把这种类型的数据叫做掩码
        // MYSPI_W_MOSI(ByteSend & (0x80 >> i)); //依次取最高位
        //也可以依次移位,没注释的地方就是依次移位的方法,符合之前文章的移位模型
        MYSPI_W_MOSI(ByteSend & 0x80);
        ByteSend <<= 1; //最低为自动补0,最低位空出来了,之后再接收的时候
        //就不需要ByteReceive这个变量了


        MYSPI_W_SCK(1);
        if(MYSPI_R_MISO() == 1)
        {
            // ByteReceive |= (0x80 >> i); //把最高位移入ByteReceive,1的时候就或等于变成1,0的位不变还是0
            ByteSend |= 0x01; //把收到的数据放在ByteSend的最低位
        }
        MYSPI_W_SCK(0);
    }
    return ByteSend;
}

W25Q64.h

#ifndef __W25Q64_H
#define __W25Q64_H

void W25Q64_Init(void);
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID);
void W25Q64_WriteEnable(void);
void W25Q64_WaitBusy(void);
void W25Q64_PageProgram(uint32_t Address, uint8_t* DataArray,  uint16_t DataCount);
void W25Q64_SectorErase(uint32_t Address);
void W25Q64_ReadData(uint32_t Address, uint8_t* DataArray,  uint32_t DataCount);

#endif

W25Q64.c

#include "stm32f10x.h"                  // Device header
#include "MYSPI.h"
#include "W25Q64_Ins.h"

//初始化W25Q64
void W25Q64_Init(void)
{
    MYSPI_Init();
}

//获取设备ID
//MID是厂商ID
//DID是16位设备ID
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
    MYSPI_Start();
    MYSPI_SwapByte(W25Q64_JEDEC_ID); //返回值不要了,没有意义,0x9F读ID号指令
    //下一次交换就会返回ID号
    //为了交换,发送0xFF
    *MID = MYSPI_SwapByte(W25Q64_DUMMY_BYTE);
    //接下来两个字节分别是设备ID高8位和低8位
    *DID = MYSPI_SwapByte(W25Q64_DUMMY_BYTE);
    *DID <<= 8;
    *DID |= MYSPI_SwapByte(W25Q64_DUMMY_BYTE);
    MYSPI_Stop();
}

//写使能
void W25Q64_WriteEnable(void)
{
    MYSPI_Start();
    MYSPI_SwapByte(W25Q64_WRITE_ENABLE);
    MYSPI_Stop();
}

//等待W25Q64的忙状态
void W25Q64_WaitBusy(void)
{
    uint32_t Timeout = 100000; //防止死循环卡死的超时处理
    MYSPI_Start();
    MYSPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
    //用掩码取最低位读BUSY
    while((MYSPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)
    {
        Timeout--;
        if(Timeout == 0)
        {
            //可加错误处理函数
            break;
        }
    }
    MYSPI_Stop();
}

//页编程
//24位地址,没有24位的变量,就用32位的无符号整数
//Address写数据的地址
//待写入的数据数组DataArray
//要传输多少字节DataCount
//由于页编程最大一次性传256个字节
//DataCount指定义为8位的,只能存0-255
//这样写入256个数据时就会出问题
void W25Q64_PageProgram(uint32_t Address, uint8_t* DataArray,  uint16_t DataCount)
{
    uint16_t i = 0;
    //写入操作前,必须先进行写使能
    W25Q64_WriteEnable();
    MYSPI_Start();
    MYSPI_SwapByte(W25Q64_PAGE_PROGRAM);
    //接下来发送3个字节的地址
    //依次从高位到低位,只能接收8位数据
    //所以获取到对应的高8位,次高8位,低8位
    //其余位置的二进制都舍弃了
    MYSPI_SwapByte(Address >> 16);
    MYSPI_SwapByte(Address >> 8);
    MYSPI_SwapByte(Address);
    //地址发完,开始写入数据
    for(i = 0; i < DataCount; i++)
    {
        MYSPI_SwapByte(DataArray[i]);
    }
    MYSPI_Stop();
    //写入后事后等待BUSY位
    //也可以事前等待,写入之前等待BUSY位
    //事后等待最保险,在函数之外的地方,芯片肯定是不忙的状态
    //事后等待只需要在写入之后调用,而事前等待,在写入操作和读取操作之前,都得调用
    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();
}

//读数据
//通过数组输出DataArray
//读取数据可以非常大,16位无符号整数不够用
//所以改成32位整数DataCount类型
void W25Q64_ReadData(uint32_t Address, uint8_t* DataArray,  uint32_t DataCount)
{
    uint32_t i = 0;
    MYSPI_Start();
    MYSPI_SwapByte(W25Q64_READ_DATA);
    //接下来发送3个字节的地址
    //依次从高位到低位,只能接收8位数据
    //所以获取到对应的高8位,次高8位,低8位
    //其余位置的二进制都舍弃了
    MYSPI_SwapByte(Address >> 16);
    MYSPI_SwapByte(Address >> 8);
    MYSPI_SwapByte(Address);
    for(i = 0; i < DataCount; i++)
    {
        DataArray[i] = MYSPI_SwapByte(W25Q64_DUMMY_BYTE);
    }
    MYSPI_Stop();
}

main.c

#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "MYOLED.h"
#include "W25Q64.h"
//#include "Serial.h"

uint8_t MID; //厂商ID
uint16_t DID; //设备ID

uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04};
uint8_t ArrayRead[4];

int main(void)
{
	MYOLED_Init();
	//Serial_Init();
    W25Q64_Init();
	//Serial_Printf("W25Q64初始化完成\r\n");
	//Serial_Printf("MYOLED初始化完成\r\n");
    MYOLED_ShowString(0,0,"MID:  ;DID:   ");
    MYOLED_ShowString(0,1,"W:");
    MYOLED_ShowString(0,2,"R:");
    W25Q64_ReadID(&MID, &DID);
	MYOLED_ShowHexNum(4,0,MID,2);
    MYOLED_ShowHexNum(11,0,DID,4);
    //向0x00 00 00地址写数据,写之前擦扇区
    //最好对齐扇区的起始地址,也就是末尾3位都是0
    W25Q64_SectorErase(0X000000);
    W25Q64_PageProgram(0x000000, ArrayWrite, 4);
    W25Q64_ReadData(0x000000, ArrayRead, 4);
    MYOLED_ShowHexNum(2,1,ArrayWrite[0],2);
    MYOLED_ShowHexNum(5,1,ArrayWrite[1],2);
    MYOLED_ShowHexNum(8,1,ArrayWrite[2],2);
    MYOLED_ShowHexNum(11,1,ArrayWrite[3],2);
    MYOLED_ShowHexNum(2,2,ArrayRead[0],2);
    MYOLED_ShowHexNum(5,2,ArrayRead[1],2);
    MYOLED_ShowHexNum(8,2,ArrayRead[2],2);
    MYOLED_ShowHexNum(11,2,ArrayRead[3],2);
    while (1) 
    {
        
    }
}

HAL库实现

已开源到:https://gitee.com/qin-ruiqian/jiangkeda-stm32-hal
HAL库移植非常方便,仅修改MYSPI.c的包含的头文件以及如下代码即可,其余内容可见开源仓库:

//由于SPI速度非常快,操作完引脚后不需要加延时
#define MYSPI_W_SS(x) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, x)
#define MYSPI_W_SCK(x) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, x)
#define MYSPI_W_MOSI(x) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, x)
#define MYSPI_R_MISO() HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_6)

main.c

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2025 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "i2c.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "MYOLED.h"
#include "W25Q64.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */
uint8_t MID; //厂商ID
uint16_t DID; //设备ID

uint8_t ArrayWrite[] = {0x0A, 0x0B, 0x0C, 0x0D};
uint8_t ArrayRead[4];
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_I2C2_Init();
  /* USER CODE BEGIN 2 */
	MYOLED_SetI2CHandleBeforeInit(&hi2c2);
	MYOLED_Init();
	W25Q64_Init();
	MYOLED_ShowString(0,0,"MID:  ;DID:   ");
	MYOLED_ShowString(0,1,"W:");
	MYOLED_ShowString(0,2,"R:");
	W25Q64_ReadID(&MID, &DID);
	MYOLED_ShowHexNum(4,0,MID,2);
	MYOLED_ShowHexNum(11,0,DID,4);
	//向0x00 00 00地址写数据,写之前擦扇区
	//最好对齐扇区的起始地址,也就是末尾3位都是0
	W25Q64_SectorErase(0X000000);
	W25Q64_PageProgram(0x000000, ArrayWrite, 4);
	W25Q64_ReadData(0x000000, ArrayRead, 4);
	MYOLED_ShowHexNum(2,1,ArrayWrite[0],2);
	MYOLED_ShowHexNum(5,1,ArrayWrite[1],2);
	MYOLED_ShowHexNum(8,1,ArrayWrite[2],2);
	MYOLED_ShowHexNum(11,1,ArrayWrite[3],2);
	MYOLED_ShowHexNum(2,2,ArrayRead[0],2);
	MYOLED_ShowHexNum(5,2,ArrayRead[1],2);
	MYOLED_ShowHexNum(8,2,ArrayRead[2],2);
	MYOLED_ShowHexNum(11,2,ArrayRead[3],2);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

实现效果

image

posted @ 2025-08-23 21:19  秦瑞迁  阅读(49)  评论(0)    收藏  举报