SPI Flash 操作 (Read/Write/Erase)

前言

此篇將使用STM32透過UART下指令來對SPI Flash做 Erase/Write/Read 等動作

UART下指令的部分要借助 Command Line Interface - CLI via UART 這一篇內容;但MCU和I/O腳位不同



Serial NOR Flash

Nor Flash支援SPI/Dual SPI/Quad SPI三種spi傳輸規格,上電後預設是走SPI mode

從spec看來SPI的傳輸速度可以到達104MHz,Dual mode有208MHz,Quad mode有416MHz

三種mode在腳位定義上,從pin 1開始

  • Standard SPI:/CS,DO,/WP,GND,DI,CLK,/HOLD,VCC
  • Dual SPI        :/CS,IO1,/WP,GND,IO0,CLK,/HOLD,VCC
  • Quad SPI      :/CS,IO1,IO2,GND,IO0,CLK,IO3,VCC

PIN DESCRIPTION (SPI mode)

PIN NO. PIN NAME I/O FUNCTION
1 /CS I Chip Select Input
2 DO O Data Output
3 /WP I Write Protect Input (特別要注意,/WP是用來保護Status Register狀態暫存器的;對data無保護作用)
4 GND Ground
5 DI I Data Input
6 CLK I Serial Clock Input
7 /HOLD I Hold Input ( /HOLD為LOW時 無法接收任何command或data)
8 VCC Power Supply

Instruction Set 指令表


Instruction Set 指令表解析

0x9F JEDEC ID

讀取flash的manufacture id和device id

我個人習慣程式一開始先讀取0x9F確認SPI介面和flash能正常溝通,flash是否能作動

9F 00 00 00
00 EF 40 13//read back

0x03 Read Data

讀取資料,0x03指令後接3個byte的記憶體讀取起始點,然後就可以依序一直讀取data

03 12 34 56 00 00 00//讀取位址0x123456開始的3個byte
00 00 00 00 XX XX XX//"XX" means data

0x06 Write Enable

開啟寫入功能

無論是 Erase或Write(Program)在執行前都須先執行0x06開啟寫入

0xC7/0x60 Chip Erase

06//write enable
C7//erase chip

0x20 Erase Sector (4KB)

06//write enable
20 00 00 00//erase sector from 0x000000

0x02 Program (Write)

06//write enable
02 00 00 00 11 22 33//write in 0x11 0x22 0x33 start from 0x000000

🚨 要注意的事,NOR flash在寫入data前一定要記得先erase memory,

erase後記憶體就會變成0xFF,寫作的動作就是把bit從1變0 或不變




操作環境

  • Board:NUCLEO-F401RE
  • IDE:STM32CubeIDE
  • Code:STM32CubeMX
  • Flash:W25Q40CLSNIG

MCU I/O配置

  • UART:TX(PA9),RX(PA10)
  • SPI:MOSI(PA7),MISO(PA6),SCK(PA5),CS(PB6),WP(PA8),HOLD(PB10)

1. STM32CubeMX

勾選USART2,選 Asynchronous,bardrate:115200bps

勾選USART2的中斷功能

勾選SPI1,

Flash支持SPI (mode 0及3),設定上就使用預設的mode 0 (CPOL:LOW,CPHA:1 Edge第一個邊緣變化)

Prescaler選64,SPI1的clock source使用APB2,目前APB2為84MHz,所以SPI1的速度為84/64=1.3MHz

勾選SPI1中斷功能

新增GPIO WP/HOLD/CS 為output push pull



2. STM32CubeIDE

首先先是完成UART CLI的功能

main.c

/* USER CODE BEGIN Includes */
#include <stdio.h>
#include <string.h>
/* USER CODE END Includes */

/* USER CODE BEGIN PV */
#ifdef __GNUC__
/* With GCC/RAISONANCE, small printf (option LD Linker->Libraries->Small printf
   set to 'Yes') calls __io_putchar() */
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */

PUTCHAR_PROTOTYPE
{
  /* Place your implementation of fputc here */
  /* e.g. write a character to the USART1 and Loop until the end of transmission */
  HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
  return ch;
}
/* USER CODE END PV */
/* USER CODE BEGIN 0 */
uint8_t uart_rx[2];
char	cli_data[80];
char 	*cli_pointer = cli_data;
uint8_t	cli_key_enter;

void CLI(void)
{
	static uint8_t values[20];
	uint8_t length = 0;
	char *token;
	uint8_t command = 0;
//	extern USBD_HandleTypeDef hUsbDeviceFS;

	if(cli_key_enter != 1) return;
	cli_key_enter = 0;

	token = strtok(cli_data, " ");
//    while (token != NULL) {
//        //printf("%s\n", token);
//        token = strtok(NULL, " ");
//    }
	while(token != NULL)
	{
		if((strcmp(token, "WP") == 0) ||(strcmp(token, "wp") == 0))
		{
			command = 0x01;
			length++;
		}else if((strcmp(token, "CS") == 0) ||(strcmp(token, "cs") == 0))
		{
			command = 0x02;
			length++;
		}else if((strcmp(token, "HOLD") == 0) ||(strcmp(token, "hold") == 0))
		{
			command = 0x03;
			length++;
		}else{
			values[length++] = strtol(token, NULL, 16);
		}
		//values[length++] = atoi( token );
		token = strtok(NULL, " ");
	}

	if(length>0)
	{
		switch(command)
		{
		case 0x01://WP
			if(length == 1){
				printf("WP:%d\r\n", HAL_GPIO_ReadPin(WP_GPIO_Port, WP_Pin));
			}else if(length >= 2){
				if(values[1] == 0)
				{
					WP(0);
				}else if(values[1] == 1)
				{
					WP(1);
				}
			}
			break;
		case 0x02://CS
			if(length == 1){
				printf("CS:%d\r\n", HAL_GPIO_ReadPin(CS_GPIO_Port, CS_Pin));
			}else if(length >= 2){
				if(values[1] == 0)
				{
					CS(0);
				}else if(values[1] == 1)
				{
					CS(1);
				}
			}
			break;
		case 0x03://HOLD
			if(length == 1){
				printf("HOLD:%d\r\n", HAL_GPIO_ReadPin(HOLD_GPIO_Port, HOLD_Pin));
			}else if(length >= 2){
				if(values[1] == 0)
				{
					HOLD(0);
				}else if(values[1] == 1)
				{
					HOLD(1);
				}
			}
			break;
		default://SPI
			printf("%d\r\n", length);
			CS(0);
			spi_count = length;
			HAL_SPI_TransmitReceive_IT(&hspi1, values, spi_rx, spi_count);
			break;
		}
	}else{
		printf("--- CLI console ---\r\n");
		printf("Key in multi hex values A0 00 10 and press ENTER to execute\r\n");
		printf("-------------------\r\n");
	}
	cli_pointer = cli_data;
	memset(cli_data, 0, sizeof(cli_data));
}
/* USER CODE END 0 */
int main(void)
{
    /* USER CODE BEGIN 2 */
    HAL_UART_Receive_IT(&huart2, uart_rx, 1);
    printf("Halo Flash\r\n");
    /* USER CODE END 2 */
    
    while(1)
    {
        /* USER CODE BEGIN 3 */
        CLI();   
    }
}

UART中斷程序,用來判斷接收字元

/* USER CODE BEGIN 4 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	HAL_UART_Receive_IT(&huart2, uart_rx, 1);
	HAL_UART_Transmit(&huart2, uart_rx, 1, HAL_MAX_DELAY);

	switch(uart_rx[0])
	{
	case '\r'://"Enter 0x13"
		printf("\n");
		cli_key_enter = 1;
		break;
	case 0x7F://"backspace"
		if(cli_pointer > cli_data)
		{
			*(cli_pointer) = 0;
			cli_pointer--;
		}
		break;
	case 0x1B://"ESC"
		cli_pointer = cli_data;
		memset(cli_data, 0, 3);
		printf("\r");
		break;
	default:
		if((cli_pointer - cli_data) >=80) cli_pointer = cli_data;
		*(cli_pointer++) = (char)uart_rx[0];
		break;
	}
//	int size = (cli_pointer) - (cli_data);
}
/* USER CODE END 4 */

完成後,上電可以看到 Halo Flash,按 Enter則出現訊息


再來完成SPI的程式

main.c

SPI的發送/接收buffer大小設定20個byte,使用者以自己的需求做調整

/* USER CODE BEGIN PD */
#define CS(x)	HAL_GPIO_WritePin(HOLD_GPIO_Port, CS_Pin, x)
#define WP(x)	HAL_GPIO_WritePin(WP_GPIO_Port, WP_Pin, x)
#define HOLD(x)	HAL_GPIO_WritePin(CS_GPIO_Port, HOLD_Pin, x)
/* USER CODE END PD */

/* USER CODE BEGIN 0 */
uint8_t spi_tx[20];
uint8_t spi_rx[20];
uint8_t spi_count;
/* USER CODE END 0 */
int main(void)
{
  /* USER CODE BEGIN 2 */
  CS(1);
  /* USER CODE END 2 */

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

  }
}

SPI完成傳輸中斷程序

/* USER CODE BEGIN 4 */
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
	int i = 0;

	CS(1);
	while((spi_count >0)&&(spi_count >i)){
		printf("%02X ", spi_rx[i]);
		i++;
	}
	printf("\r\n");
	spi_count = 0;
}
/* USER CODE END 4 */


3. Result

驗証SPI及Flash功能

鍵入holdwp可以確認目前腳位的電位高低,下圖回覆都為0 低電位

輸入0x9F查詢JEDEC ID,結果得到00 00 00,這是因為HOLD為0,flash無法接收任何指令/資料

輸入hold 1把HOLD拉為高電位,然後輸入0x9F就可以得到JEDEC ID EF 40 13

輸入0x03讀取位址00 00 00開始的3個byte,回傳FF FF FF (回傳值依狀況不固定值)