26 Ứng dụng SPI-FLASH 
Giới thiệu W25Q128 
W25Q128 là một thiết bị bộ nhớ flash nối tiếp phổ biến, sử dụng giao thức SPI (Serial Peripheral Interface), có khả năng đọc, ghi và xóa tốc độ cao, được sử dụng để lưu trữ và đọc dữ liệu. Chip W25Q128 có dung lượng 128 Mbit (16 MB), trong đó số sau tên đại diện cho các tùy chọn dung lượng khác nhau. Các mẫu và tùy chọn dung lượng khác nhau có thể đáp ứng nhu cầu của các ứng dụng khác nhau, thường được sử dụng trong các thiết bị nhúng, thiết bị lưu trữ, router và các thiết bị điện tử hiệu suất cao khác.
Việc phân bổ bộ nhớ của chip flash W25Q128 được thực hiện theo sector (Sector) và block (Block), mỗi sector có kích thước 4KB, mỗi block chứa 16 sector, tức là một block có kích thước 64KB.

Giao diện phần cứng 
Trong phiên bản cao cấp của board phát triển có tích hợp sẵn một chip lưu trữ W25Q128, mô tả các chân của nó được thể hiện trong bảng dưới đây.
| CLK | Nhận xung clock từ bên ngoài, cung cấp xung clock cho chức năng đầu vào đầu ra | 
|---|---|
| DI | SPI tiêu chuẩn sử dụng DI một chiều để ghi nối tiếp lệnh, địa chỉ hoặc dữ liệu vào FLASH. | 
| DO | SPI tiêu chuẩn sử dụng DO một chiều để đọc dữ liệu hoặc trạng thái. | 
| WP | Ngăn chặn việc ghi vào thanh ghi trạng thái | 
| HOLD | Khi có hiệu lực, cho phép thiết bị tạm dừng, mức thấp: chân DO ở trạng thái trở kháng cao, tín hiệu chân DI CLK bị bỏ qua. Mức cao: thiết bị khởi động lại, khi nhiều thiết bị chia sẻ cùng một tín hiệu SPI, chức năng này có thể được sử dụng | 
| CS | Khi CS ở mức cao, các chân khác ở trạng thái trở kháng cao, khi ở mức thấp, có thể đọc ghi dữ liệu | 
Kết nối giữa nó và STM32F407 như sau:
| STM32F407(Master) | W25Q1282(Slave) | Mô tả | 
|---|---|---|
| PA4(SPI1_NSS) | CS(NSS) | Đường chọn chip | 
| PA5(SPI1_SCK) | CLK | Đường xung clock | 
| PA6(SPI_MISO) | DO(IO1)(MISO) | Đường master input slave output | 
| PA7(SPI_MOSI) | DI(IO0)(MOSI) | Đường master output slave input | 

Cần lưu ý rằng, chúng ta sử dụng phương thức SPI phần cứng để điều khiển W25Q128, do đó chúng ta cần xác định các chân mà chúng ta thiết lập có giao diện ngoại vi SPI phần cứng hay không. Trong datasheet của STM32F407, PA4~7 có thể được sử dụng như 4 đường truyền thông của SPI1.
Cấu hình chân 
Trước tiên chúng ta định nghĩa macro.
#define BSP_GPIO_RCU           RCC_AHB1Periph_GPIOA // Xung clock GPIO
#define BSP_SPI_RCU            RCC_APB2Periph_SPI1  // Xung clock SPI
#define BSP_SPI_NSS_RCU        RCC_AHB1Periph_GPIOA // Xung clock chân CS
#define BSP_GPIO_PORT          GPIOA
#define BSP_GPIO_AF            GPIO_AF_SPI1
#define BSP_SPI                SPI1
#define BSP_SPI_NSS            GPIO_Pin_4        // CS phần mềm
#define BSP_SPI_SCK            GPIO_Pin_5
#define BSP_SPI_SCK_PINSOURCE  GPIO_PinSource5
#define BSP_SPI_MISO           GPIO_Pin_6
#define BSP_SPI_MISO_PINSOURCE GPIO_PinSource6
#define BSP_SPI_MOSI           GPIO_Pin_7
#define BSP_SPI_MOSI_PINSOURCE GPIO_PinSource7
#define W25QXX_CS_ON(x)  GPIO_WriteBit(BSP_GPIO_PORT, BSP_SPI_NSS, x ? Bit_SET : Bit_RESET)Sử dụng ngoại vi SPI1 phần cứng, chúng ta cần bật xung clock SPI1 phần cứng tương ứng, và bật chức năng chân phụ, liên kết đường phụ.
GPIO_InitTypeDef GPIO_InitStructure;
/* Bật xung clock GPIO */
RCC_AHB1PeriphClockCmd (BSP_GPIO_RCU, ENABLE);
/* Bật xung clock SPI */
RCC_APB2PeriphClockCmd(BSP_SPI_RCU, ENABLE);
/* Thiết lập chân phụ */
GPIO_PinAFConfig(BSP_GPIO_PORT, BSP_SPI_SCK_PINSOURCE, BSP_GPIO_AF);
GPIO_PinAFConfig(BSP_GPIO_PORT, BSP_SPI_MISO_PINSOURCE, BSP_GPIO_AF);
GPIO_PinAFConfig(BSP_GPIO_PORT, BSP_SPI_MOSI_PINSOURCE, BSP_GPIO_AF);
/* Cấu hình chân SPI SCK */
GPIO_InitStructure.GPIO_Pin = BSP_SPI_SCK;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(BSP_GPIO_PORT, &GPIO_InitStructure);
/* Cấu hình chân SPI MISO */
GPIO_InitStructure.GPIO_Pin = BSP_SPI_MISO;
GPIO_Init(BSP_GPIO_PORT, &GPIO_InitStructure);
/* Cấu hình chân SPI MOSI */
GPIO_InitStructure.GPIO_Pin = BSP_SPI_MOSI;
GPIO_Init(BSP_GPIO_PORT, &GPIO_InitStructure);
/* Cấu hình chân SPI CS */
GPIO_InitStructure.GPIO_Pin = BSP_SPI_NSS;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
GPIO_Init(BSP_GPIO_PORT, &GPIO_InitStructure);
/* Chân CS mức cao */
W25QXX_CS_ON(1);Cấu hình SPI 
Theo datasheet của W25Q128, chúng ta có cấu hình sau:
- SPI được cấu hình ở chế độ full duplex, có thể đồng thời gửi và nhận dữ liệu;
- STM32F407 được cấu hình ở chế độ master, STM32F407 tạo xung clock để giao tiếp với slave W25Q128;
- Truyền dữ liệu theo 8 bit.
- Phương thức chọn chip sử dụng điều khiển phần mềm. Trong SPI phần cứng, một SPI chỉ có một đường chọn chip, điều này dẫn đến việc nếu SPI phần cứng chọn điều khiển tín hiệu chọn chip bằng phần cứng, chỉ có thể điều khiển một slave. Chúng ta muốn một SPI có thể điều khiển nhiều slave, do đó chọn phương thức chọn chip bằng phần mềm, đường chọn chip có thể được thiết lập tùy ý.
- Chia xung clock chọn chia 4, theo datasheet của W25Q128, xung clock SPI của W25Q128 có thể đạt 30MHz, và nguồn xung clock SPI0 của chúng ta là PCLK2=84MHz, cấu hình SPI phải yêu cầu chia tần, tần số sau khi chia là 21MHz, thấp hơn 30MHz, W25Q1282 hoàn toàn có thể tương thích.
- Thứ tự byte chọn bit cao trước.
SPI_InitTypeDef  SPI_InitStructure;
/* Cấu hình chế độ FLASH_SPI */
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // Chế độ truyền full duplex
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;                // Cấu hình làm master
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;        // Dữ liệu 8 bit
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;                // Cực tính pha
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;                        // CS phần mềm
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; // Hệ số chia trước xung clock SPI là 2
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;        // Bit cao trước
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(BSP_SPI, &SPI_InitStructure);
/* Bật FLASH_SPI  */
SPI_Cmd(BSP_SPI, ENABLE);Việc khởi tạo SPI đã hoàn thành, chúng ta cần chuẩn bị các bước đọc ghi SPI. SPI chúng ta cấu hình ở chế độ full duplex, tức có thể đọc và ghi. Để đảm bảo gửi và nhận dữ liệu thành công, khi gửi, cần đảm bảo dữ liệu trong buffer gửi đã được gửi hoàn thành, tức buffer gửi trống, mới có thể tiến hành gửi dữ liệu tiếp theo; khi nhận, cần đảm bảo buffer nhận có dữ liệu mới có thể tiến hành nhận.
uint8_t spi_read_write_byte(uint8_t dat)
{
    //Chờ buffer gửi trống
    while(RESET == SPI_I2S_GetFlagStatus(BSP_SPI,  SPI_I2S_FLAG_TXE) );
    //Gửi một byte dữ liệu thông qua SPI4
    SPI_I2S_SendData(BSP_SPI, dat);
    //Chờ cờ buffer nhận không trống
    while(RESET == SPI_I2S_GetFlagStatus(BSP_SPI,  SPI_I2S_FLAG_RXNE) );
    //Đọc và trả về dữ liệu một byte đọc được từ SPI
    return SPI_I2S_ReceiveData(BSP_SPI);
}Ví dụ đọc ID thiết bị 

Theo datasheet của W25Q128 có thể biết, lệnh đọc ID sử dụng 90H. Trong datasheet đã đưa ra sơ đồ thời gian đọc của nó.

Các bước đọc:
- Kéo chân CS xuống mức thấp;
- Gửi lệnh 90H(1001_0000);
- Gửi địa chỉ 000000H(0000_0000_0000_0000_0000_0000);
- Đọc ID nhà sản xuất, theo datasheet có thể biết ID nhà sản xuất là EFh;
- Đọc ID thiết bị;
- Khôi phục chân CS về mức cao;
Mã thực hiện:
//Đọc ID chip
//Đọc ID thiết bị
uint16_t W25Q128_readID(void)
{
    uint16_t  temp = 0;
    //Kéo chân CS xuống mức thấp
    W25QXX_CS_ON(0);
    //Gửi lệnh 90h
    spi_read_write_byte(0x90);//Gửi lệnh đọc ID
    //Gửi địa chỉ  000000H
    spi_read_write_byte(0x00);
    spi_read_write_byte(0x00);
    spi_read_write_byte(0x00);
    //Nhận dữ liệu
    //Nhận ID nhà sản xuất
    temp |= spi_read_write_byte(0xFF)<<8;
    //Nhận ID thiết bị
    temp |= spi_read_write_byte(0xFF);
    //Khôi phục chân CS về mức cao
    W25QXX_CS_ON(1);
    //Trả về ID
    return temp;
}Ví dụ quy trình ghi dữ liệu 
Các bước ghi dữ liệu như hình bên phải. Trong bộ nhớ FLASH, mỗi lần ghi dữ liệu đều phải đảm bảo dữ liệu trong đó là 0xFF, là do thao tác ghi của bộ nhớ FLASH là một thao tác xóa-ghi. Thao tác xóa là đưa tất cả dữ liệu trong ô lưu trữ về 1, tức là 0xFF. Sau đó, chỉ có vị trí bit dữ liệu cần ghi là 0 mới có thể tiến hành thao tác ghi, thay đổi nó thành 0. Quá trình này là không thể đảo ngược, vì vậy trước khi ghi dữ liệu, cần đảm bảo vị trí cần ghi là 0xFF, sau đó mới ghi dữ liệu. Thao tác xóa-ghi này được quyết định bởi cấu trúc đặc biệt của bộ nhớ FLASH. Ô lưu trữ trong bộ nhớ FLASH được điều khiển thông qua trạng thái của cổng điện tử, mỗi cổng có thể lưu trữ một bit nhị phân. Thao tác xóa cần khôi phục trạng thái cổng về trạng thái ban đầu, tức tất cả là 1. Sau đó thông qua thay đổi trạng thái cổng, thay đổi bit dữ liệu cần lưu trữ thành 0. Vì vậy trước khi ghi dữ liệu, cần đảm bảo trạng thái ô lưu trữ là 1, để tiến hành thao tác ghi chính xác. Ngoài ra, thao tác xóa của bộ nhớ FLASH được thực hiện theo đơn vị block, chứ không phải ô lưu trữ đơn lẻ. Vì vậy nếu vị trí cần ghi dữ liệu đã có dữ liệu tồn tại, cần tiến hành thao tác xóa, đưa tất cả dữ liệu của block về 1, sau đó mới ghi dữ liệu mới. Đây cũng là lý do tại sao trước khi ghi dữ liệu vào FLASH cần đảm bảo dữ liệu trong đó là 0xFF.
flowchart TD
    A([▶️ Bắt đầu]) --> B([🧹 Xóa dữ liệu tại địa chỉ ghi])
    B --> C([🔓 Bật chức năng ghi])
    C --> D{🔎 Kiểm tra bận?}
    D --> E([📝 Gửi lệnh ghi trang])
    E --> F([📍 Gửi địa chỉ ghi 24 bit])
    F --> G([📦 Gửi dữ liệu cần ghi])
    G --> H([⏳ Chờ ghi hoàn tất])
    H --> I([🏁 Kết thúc])Cho phép ghi 
Trước khi tiến hành thao tác ghi, cần sử dụng lệnh cho phép ghi (Write Enable). Tác dụng của cho phép ghi là kích hoạt thao tác ghi cho chip flash. Trong trạng thái mặc định, chip flash ở trạng thái bảo vệ, cấm tiến hành thao tác ghi đối với nó, chủ yếu là để ngăn chặn thao tác nhầm làm hỏng dữ liệu. Lệnh cho phép ghi có thể bỏ trạng thái bảo vệ này, thiết lập chip flash có thể tiến hành thao tác ghi.
Thông qua gửi lệnh cho phép ghi, chip flash sẽ vào một trạng thái cụ thể, làm cho các lệnh ghi tiếp theo có thể được chấp nhận và thực thi. Trước khi ghi dữ liệu, cần gửi lệnh cho phép ghi để đảm bảo chip flash ở trạng thái có thể ghi. Sau đó, mới có thể gửi lệnh ghi để ghi dữ liệu vào vị trí lưu trữ được chỉ định. Sử dụng lệnh cho phép ghi có thể bảo vệ hiệu quả tính toàn vẹn và an toàn của dữ liệu, ngăn chặn thao tác nhầm ghi hoặc sửa đổi dữ liệu. Đồng thời, cũng có thể đảm bảo
tính nhất quán của dữ liệu, tránh lỗi hoặc nhiễu trong quá trình ghi. Do đó, khi sử dụng W25Q128 để tiến hành thao tác ghi, cần gửi lệnh cho phép ghi trước, để đảm bảo chip flash ở trạng thái có thể ghi, rồi mới tiến hành thao tác ghi dữ liệu. Trong datasheet của W25Q128, về thời gian cho phép ghi như sau:

Các bước thao tác:
- Kéo chân CS xuống mức thấp;
- Gửi lệnh 06H(0000_0110);
- Khôi phục chân CS về mức cao;
Mã thực hiện cụ thể như sau:
//Gửi cho phép ghi
void W25Q128_write_enable(void)
{
    //Kéo chân CS xuống mức thấp
    W25QXX_CS_ON(0);
    //Gửi lệnh 06h
    spi_read_write_byte(0x06);
    //Kéo chân CS lên mức cao
    W25QXX_CS_ON(1);
}Kiểm tra thiết bị bận 
Trong datasheet của W25Q1282, có 3 thanh ghi trạng thái, có thể kiểm tra W25Q128 hiện tại có đang truyền, ghi, đọc dữ liệu hay không, mỗi lần chúng ta muốn thao tác với W25Q128, cần kiểm tra trước W25Q128 có bận hay không. Nếu ở trạng thái bận, chúng ta thao tác với W25Q128, rất có thể sẽ dẫn đến mất dữ liệu và thao tác thất bại. Việc kiểm tra có bận hay không, được thực hiện thông qua bit S0 của thanh ghi trạng thái 1, địa chỉ thanh ghi trạng thái 1 là 0X05.
Sơ đồ thời gian đọc thanh ghi trạng thái như sau:
- Kéo chân CS xuống mức thấp;
- Gửi lệnh 05h(0000_0101);
- Nhận giá trị thanh ghi trạng thái;
- Khôi phục chân CS về mức cao;

Mã thực hiện cụ thể như sau:
/**********************************************************
 * Tên hàm         :W25Q128_wait_busy
 * Chức năng hàm   :Kiểm tra đường truyền có bận hay không
 * Tham số truyền vào:Không
 * Giá trị trả về  :Không
 * Tác giả         :LC
 * Ghi chú         :Không
**********************************************************/
void W25Q128_wait_busy(void)
{
    unsigned char byte = 0;
    do
     {
        //Kéo chân CS xuống mức thấp
        W25QXX_CS_ON(0);
        //Gửi lệnh 05h
        spi_read_write_byte(0x05);
        //Nhận giá trị thanh ghi trạng thái
        byte = spi_read_write_byte(0Xff);
        //Khôi phục chân CS về mức cao
        W25QXX_CS_ON(1);
     //Kiểm tra bit BUSY có phải là 1 hay không, nếu là 1 nghĩa là đang bận, đọc ghi lại bit BUSY cho đến khi là 0
     }while( ( byte & 0x01 ) == 1 );
}Xóa sector 
Việc phân bổ bộ nhớ của chip flash W25Q128 được thực hiện theo sector (Sector) và block (Block), mỗi sector có kích thước 4KB, mỗi block chứa 16 sector, tức là một block có kích thước 64KB.
Xóa sector của chip flash W25Q128 là thao tác xóa toàn bộ dữ liệu trong một sector cụ thể. Thao tác xóa sẽ đưa tất cả dữ liệu trong sector về 1 (tức 0xFF), khôi phục về trạng thái ban đầu. Dưới đây là quy trình chung của việc xóa sector W25Q128:
- Cho phép ghi (Write Enable): Trước tiên, phải đảm bảo chip flash ở trạng thái có thể ghi. Gửi lệnh cho phép ghi, thiết lập chip flash ở chế độ có thể ghi, bỏ bảo vệ ghi.
- Thiết lập xóa sector (Sector Erase Setup): Gửi lệnh thiết lập xóa sector đến W25Q128, và chỉ định địa chỉ sector cần xóa. W25Q128 hỗ trợ nhiều lệnh xóa sector khác nhau, có thể chọn xóa một hoặc nhiều sector theo nhu cầu.
- Xác nhận xóa sector (Sector Erase Confirm): Chờ xác nhận xóa sector. Chip W25Q1282 tiến hành thao tác xóa cần một khoảng thời gian nhất định, thời gian cụ thể có thể tham khảo trong datasheet của chip đó. Trong thời gian tiến hành thao tác xóa, thường sẽ đọc bit bận của thanh ghi trạng thái để xác định việc xóa đã hoàn thành chưa. Đọc dữ liệu quá sớm trong thao tác xóa có thể dẫn đến kết quả không chính xác.
- Hoàn thành xóa sector: Khi xóa sector thành công, thanh ghi trạng thái sẽ chỉ ra thao tác xóa đã hoàn thành. Lúc này, dữ liệu trong sector đó đã được xóa toàn bộ thành 1.
Thao tác xóa sector là một thao tác cao cấp, cần sử dụng cẩn thận. Trong ứng dụng thực tế, thường sẽ kết hợp logic lập trình và bộ điều khiển tương ứng để quản lý thao tác xóa và ghi của chip flash, để đảm bảo tính an toàn và toàn vẹn của dữ liệu.
Khi sử dụng thao tác xóa sector, có một số điều cần lưu ý đặc biệt:
- Phạm vi xóa: Phải đảm bảo phạm vi xóa là chính xác, chỉ xóa sector đích, tránh xóa nhầm dữ liệu trong các sector khác. Trước khi thực thi thao tác xóa, hãy kiểm tra cẩn thận địa chỉ sector cần xóa, và đảm bảo không có lỗi.
- Sao lưu dữ liệu: Do thao tác xóa sector sẽ xóa toàn bộ dữ liệu thành 1 (0xFF), trước khi thực thi xóa, nên đảm bảo dữ liệu quan trọng đã được sao lưu. Sau khi xóa, dữ liệu sẽ không thể khôi phục, vì vậy trước khi thực thi thao tác xóa sector dữ liệu quan trọng, hãy làm tốt công việc sao lưu dữ liệu.
Sơ đồ thời gian xóa sector như sau:
- Kéo chân CS xuống mức thấp;
- Gửi lệnh 20h(0010_0000);
- Gửi địa chỉ đầu sector 24 bit;
- Khôi phục chân CS về mức cao;

Mã thực hiện cụ thể như sau:
Mã sau có một số khác biệt với sơ đồ thời gian xóa sector, thêm kiểm tra bận và cho phép ghi.
/**********************************************************
 * Tên hàm         :W25Q128_erase_sector
 * Chức năng hàm   :Xóa một sector
 * Tham số truyền vào:addr=số sector cần xóa
 * Giá trị trả về  :Không
 * Tác giả         :LC
 * Ghi chú         :addr=số sector cần xóa, phạm vi=0~4096。
W25Q128 chia dung lượng 16M thành 256 block, mỗi block có kích thước 64K (64000) byte, mỗi block lại chia thành 16 sector, mỗi sector 4K byte.
Đơn vị xóa nhỏ nhất của W25Q128 là một sector, tức mỗi lần phải xóa 4K byte.
**********************************************************/
void W25Q128_erase_sector(uint32_t addr)
{
        //Tính số sector, một sector 4KB=4096
        addr *= 4096;
        W25Q128_write_enable();  //Cho phép ghi
        W25Q128_wait_busy();     //Kiểm tra bận, nếu bận thì chờ
        //Kéo chân CS xuống mức thấp
        W25QXX_CS_ON(0);
        //Gửi lệnh 20h
        spi_read_write_byte(0x20);
        //Gửi 8 bit cao của địa chỉ sector 24 bit
        spi_read_write_byte((uint8_t)((addr)>>16));
        //Gửi 8 bit giữa của địa chỉ sector 24 bit
        spi_read_write_byte((uint8_t)((addr)>>8));
        //Gửi 8 bit thấp của địa chỉ sector 24 bit
        spi_read_write_byte((uint8_t)addr);
        //Khôi phục chân CS về mức cao
        W25QXX_CS_ON(1);
        //Chờ xóa hoàn thành
        W25Q128_wait_busy();
}Ghi dữ liệu 
Bây giờ các bước tiền đề để ghi dữ liệu: xóa dữ liệu-> cho phép ghi-> kiểm tra bận chúng ta đã hoàn thành, chỉ còn lại việc ghi dữ liệu vào địa chỉ tương ứng để lưu trữ.

Mã ghi dữ liệu cụ thể như sau:
/**********************************************************
 * Tên hàm         :W25Q128_write
 * Chức năng hàm   :Ghi dữ liệu vào W25Q128 để lưu trữ
 * Tham số truyền vào:buffer=nội dung dữ liệu ghi  addr=địa chỉ ghi  numbyte=độ dài dữ liệu ghi
 * Giá trị trả về  :Không
 * Tác giả         :LC
 * Ghi chú         :Không
**********************************************************/
void W25Q128_write(uint8_t* buffer, uint32_t addr, uint16_t numbyte)
{
    unsigned int i = 0;
    //Xóa dữ liệu sector
    W25Q128_erase_sector(addr/4096);
    //Cho phép ghi
    W25Q128_write_enable();
    //Kiểm tra bận
    W25Q128_wait_busy();
    //Ghi dữ liệu
    //Kéo chân CS xuống mức thấp
    W25QXX_CS_ON(0);
    //Gửi lệnh 02h
    spi_read_write_byte(0x02);
    //Gửi 8 bit cao của địa chỉ ghi 24 bit
    spi_read_write_byte((uint8_t)((addr)>>16));
    //Gửi 8 bit giữa của địa chỉ ghi 24 bit
    spi_read_write_byte((uint8_t)((addr)>>8));
    //Gửi 8 bit thấp của địa chỉ ghi 24 bit
    spi_read_write_byte((uint8_t)addr);
    //Ghi liên tục dữ liệu buffer theo độ dài byte cần ghi
    for(i=0;i<numbyte;i++)
    {
        spi_read_write_byte(buffer[i]);
    }
    //Khôi phục chân CS về mức cao
    W25QXX_CS_ON(1);
    //Kiểm tra bận
    W25Q128_wait_busy();
}Đọc dữ liệu 
Sơ đồ thời gian đọc dữ liệu như sau:
- Kéo chân CS xuống mức thấp;
- Gửi lệnh 03h(0000_0011);
- Gửi địa chỉ đọc dữ liệu 24 bit;
- Nhận dữ liệu đọc được;
- Khôi phục chân CS về mức cao;

Mã thực hiện cụ thể như sau:
/**********************************************************
 * Tên hàm         :W25Q128_read
 * Chức năng hàm   :Đọc dữ liệu của W25Q128
 * Tham số truyền vào:buffer=địa chỉ lưu dữ liệu đọc ra  read_addr=địa chỉ đọc   read_length=độ dài đọc
 * Giá trị trả về  :Không
 * Tác giả         :LC
 * Ghi chú         :Không
**********************************************************/
void W25Q128_read(uint8_t* buffer,uint32_t read_addr,uint16_t read_length)
{
        uint16_t i;
        //Kéo chân CS xuống mức thấp
        W25QXX_CS_ON(0);
        //Gửi lệnh 03h
        spi_read_write_byte(0x03);
        //Gửi 8 bit cao của địa chỉ đọc dữ liệu 24 bit
        spi_read_write_byte((uint8_t)((read_addr)>>16));
        //Gửi 8 bit giữa của địa chỉ đọc dữ liệu 24 bit
        spi_read_write_byte((uint8_t)((read_addr)>>8));
        //Gửi 8 bit thấp của địa chỉ đọc dữ liệu 24 bit
        spi_read_write_byte((uint8_t)read_addr);
        //Đọc ra địa chỉ theo độ dài đọc và lưu vào buffer
        for(i=0;i<read_length;i++)
        {
            buffer[i]= spi_read_write_byte(0XFF);
        }
        //Khôi phục chân CS về mức cao
        W25QXX_CS_ON(1);
}Kiểm tra SPI FLASH phần cứng 
Tạo hai file, đặt tên lần lượt là spi_flash.c và spi_flash.h. Viết mã hoàn chỉnh vào trong:
spi_flash.c
/*
 * Tài liệu phần cứng và phần mềm của board phát triển và các board mở rộng có liên quan đều được mở nguồn hoàn toàn trên website
 * Website board phát triển:www.lckfb.com
 * Hỗ trợ kỹ thuật thường trú trên diễn đàn, mọi vấn đề kỹ thuật đều được chào đón để trao đổi học hỏi
 * Diễn đàn Lập Chuàng:https://oshwhub.com/forum
 * Theo dõi tài khoản bilibili:【Lập Chuàng Board Phát Triển】,nắm bắt động thái mới nhất của chúng tôi!
 * Không dựa vào việc bán board để kiếm tiền, lấy việc đào tạo kỹ sư Trung Quốc làm sứ mệnh
 * Change Logs:
 * Date           Author       Notes
 * 2024-08-02     LCKFB-LP    first version
 */
#include "spi_flash.h"
#include "board.h"
/**********************************************************
 * Tên hàm         :bsp_spi_init
 * Chức năng hàm   :Khởi tạo SPI
 * Tham số truyền vào:Không
 * Giá trị trả về  :Không
 * Tác giả         :LC
 * Ghi chú         :Không
**********************************************************/
void bsp_spi_init(void)
{
    SPI_InitTypeDef  SPI_InitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;
    /* Bật xung clock GPIO */
    RCC_AHB1PeriphClockCmd (BSP_GPIO_RCU, ENABLE);
    /* Bật xung clock SPI */
    RCC_APB2PeriphClockCmd(BSP_SPI_RCU, ENABLE);
    /* Thiết lập chân phụ */
    GPIO_PinAFConfig(BSP_GPIO_PORT, BSP_SPI_SCK_PINSOURCE, BSP_GPIO_AF);
    GPIO_PinAFConfig(BSP_GPIO_PORT, BSP_SPI_MISO_PINSOURCE, BSP_GPIO_AF);
    GPIO_PinAFConfig(BSP_GPIO_PORT, BSP_SPI_MOSI_PINSOURCE, BSP_GPIO_AF);
    /* Cấu hình chân SPI SCK */
    GPIO_InitStructure.GPIO_Pin = BSP_SPI_SCK;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_Init(BSP_GPIO_PORT, &GPIO_InitStructure);
    /* Cấu hình chân SPI MISO */
    GPIO_InitStructure.GPIO_Pin = BSP_SPI_MISO;
    GPIO_Init(BSP_GPIO_PORT, &GPIO_InitStructure);
    /* Cấu hình chân SPI MOSI */
    GPIO_InitStructure.GPIO_Pin = BSP_SPI_MOSI;
    GPIO_Init(BSP_GPIO_PORT, &GPIO_InitStructure);
    /* Cấu hình chân SPI CS */
    GPIO_InitStructure.GPIO_Pin = BSP_SPI_NSS;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
    GPIO_Init(BSP_GPIO_PORT, &GPIO_InitStructure);
    /* Chân CS mức cao */
    W25QXX_CS_ON(1);
    /* Cấu hình chế độ FLASH_SPI */
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // Chế độ truyền full duplex
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;                // Cấu hình làm master
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;            // Dữ liệu 8 bit
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;                // Cực tính pha
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;                   // CS phần mềm
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; // Hệ số chia trước xung clock SPI là 2
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;        // Bit cao trước
    SPI_InitStructure.SPI_CRCPolynomial = 7;
    SPI_Init(BSP_SPI, &SPI_InitStructure);
    /* Bật FLASH_SPI  */
    SPI_Cmd(BSP_SPI, ENABLE);
    W25QXX_CS_ON(1);         // Kéo chọn chip lên cao
}
uint8_t spi_read_write_byte(uint8_t dat)
{
    //Chờ buffer gửi trống
    while(RESET == SPI_I2S_GetFlagStatus(BSP_SPI,  SPI_I2S_FLAG_TXE) );
    //Gửi một byte dữ liệu thông qua SPI4
    SPI_I2S_SendData(BSP_SPI, dat);
    //Chờ cờ buffer nhận không trống
    while(RESET == SPI_I2S_GetFlagStatus(BSP_SPI,  SPI_I2S_FLAG_RXNE) );
    //Đọc và trả về dữ liệu một byte đọc được từ SPI
    return SPI_I2S_ReceiveData(BSP_SPI);
}
//Đọc ID chip
//Đọc ID thiết bị
uint16_t W25Q128_readID(void)
{
    uint16_t  temp = 0;
    //Kéo chân CS xuống mức thấp
    W25QXX_CS_ON(0);
    //Gửi lệnh 90h
    spi_read_write_byte(0x90);//Gửi lệnh đọc ID
    //Gửi địa chỉ  000000H
    spi_read_write_byte(0x00);
    spi_read_write_byte(0x00);
    spi_read_write_byte(0x00);
    //Nhận dữ liệu
    //Nhận ID nhà sản xuất
    temp |= spi_read_write_byte(0xFF)<<8;
    //Nhận ID thiết bị
    temp |= spi_read_write_byte(0xFF);
    //Khôi phục chân CS về mức cao
    W25QXX_CS_ON(1);
    //Trả về ID
    return temp;
}
//Gửi cho phép ghi
void W25Q128_write_enable(void)
{
    //Kéo chân CS xuống mức thấp
    W25QXX_CS_ON(0);
    //Gửi lệnh 06h
    spi_read_write_byte(0x06);
    //Kéo chân CS lên mức cao
    W25QXX_CS_ON(1);
}
/**********************************************************
 * Tên hàm         :W25Q128_wait_busy
 * Chức năng hàm   :Kiểm tra bus SPI có đang bận hay không
 * Tham số vào     :Không
 * Giá trị trả về  :Không
 * Tác giả         :LC
 * Ghi chú         :Không
**********************************************************/
void W25Q128_wait_busy(void)
{
    unsigned char byte = 0;
    do
     {
        // Kéo chân CS xuống mức thấp
        W25QXX_CS_ON(0);
        // Gửi lệnh 05h (Read Status Register-1)
        spi_read_write_byte(0x05);
        // Nhận giá trị thanh ghi trạng thái
        byte = spi_read_write_byte(0Xff);
        // Khôi phục chân CS lên mức cao
        W25QXX_CS_ON(1);
     // Kiểm tra bit BUSY, nếu =1 tức là đang bận, tiếp tục polling đến khi =0
     }while( ( byte & 0x01 ) == 1 );
}
/**********************************************************
 * Tên hàm         :W25Q128_erase_sector
 * Chức năng hàm   :Xóa một sector
 * Tham số vào     :addr = số thứ tự sector cần xóa
 * Giá trị trả về  :Không
 * Tác giả         :LC
 * Ghi chú         :addr = số sector, phạm vi 0~4096
 *
 * W25Q128 có dung lượng 16MB, chia thành 256 block (mỗi block 64KB).
 * Mỗi block chia thành 16 sector, mỗi sector 4KB.
 * Đơn vị xóa nhỏ nhất là sector (4KB).
**********************************************************/
void W25Q128_erase_sector(uint32_t addr)
{
        // Tính địa chỉ sector, 1 sector = 4096B
        addr *= 4096;
        W25Q128_write_enable();  // Ghi enable
        W25Q128_wait_busy();     // Chờ nếu đang bận
        // Kéo CS xuống mức thấp
        W25QXX_CS_ON(0);
        // Gửi lệnh 20h (Sector Erase 4KB)
        spi_read_write_byte(0x20);
        // Gửi địa chỉ 24-bit: cao, giữa, thấp
        spi_read_write_byte((uint8_t)((addr)>>16));
        spi_read_write_byte((uint8_t)((addr)>>8));
        spi_read_write_byte((uint8_t)addr);
        // Khôi phục CS lên mức cao
        W25QXX_CS_ON(1);
        // Chờ xóa xong
        W25Q128_wait_busy();
}
/**********************************************************
 * Tên hàm         :W25Q128_write
 * Chức năng hàm   :Ghi dữ liệu vào W25Q128
 * Tham số vào     :buffer = dữ liệu cần ghi,
 *                   addr = địa chỉ ghi,
 *                   numbyte = số byte cần ghi
 * Giá trị trả về  :Không
 * Tác giả         :LC
 * Ghi chú         :Không
**********************************************************/
void W25Q128_write(uint8_t* buffer, uint32_t addr, uint16_t numbyte)
{
    unsigned int i = 0;
    // Xóa sector chứa địa chỉ này
    W25Q128_erase_sector(addr/4096);
    // Enable ghi
    W25Q128_write_enable();
    // Chờ sẵn sàng
    W25Q128_wait_busy();
    // Ghi dữ liệu
    W25QXX_CS_ON(0);                       // Kéo CS xuống
    spi_read_write_byte(0x02);             // Lệnh 02h (Page Program)
    spi_read_write_byte((uint8_t)((addr)>>16)); // Địa chỉ cao
    spi_read_write_byte((uint8_t)((addr)>>8));  // Địa chỉ giữa
    spi_read_write_byte((uint8_t)addr);         // Địa chỉ thấp
    for(i=0;i<numbyte;i++)                 // Ghi liên tục numbyte byte
    {
        spi_read_write_byte(buffer[i]);
    }
    W25QXX_CS_ON(1);                       // Thả CS
    W25Q128_wait_busy();                   // Chờ hoàn tất
}
/**********************************************************
 * Tên hàm         :W25Q128_read
 * Chức năng hàm   :Đọc dữ liệu từ W25Q128
 * Tham số vào     :buffer = nơi lưu dữ liệu đọc,
 *                   read_addr = địa chỉ đọc,
 *                   read_length = số byte cần đọc
 * Giá trị trả về  :Không
 * Tác giả         :LC
 * Ghi chú         :Không
**********************************************************/
void W25Q128_read(uint8_t* buffer,uint32_t read_addr,uint16_t read_length)
{
        uint16_t i;
        W25QXX_CS_ON(0);                       // Kéo CS xuống
        spi_read_write_byte(0x03);             // Lệnh 03h (Read Data)
        spi_read_write_byte((uint8_t)((read_addr)>>16)); // Địa chỉ cao
        spi_read_write_byte((uint8_t)((read_addr)>>8));  // Địa chỉ giữa
        spi_read_write_byte((uint8_t)read_addr);         // Địa chỉ thấp
        for(i=0;i<read_length;i++)             // Đọc liên tục
        {
            buffer[i]= spi_read_write_byte(0XFF);
        }
        W25QXX_CS_ON(1);                       // Thả CS
}spi_flash.h
/*
 * Tài liệu phần cứng & phần mềm của bo mạch phát triển LCSC (立创开发板) 
 * cùng với các board mở rộng liên quan đều được **mã nguồn mở** trên trang chính thức.
 *
 * Trang web chính thức: www.lckfb.com
 * Diễn đàn hỗ trợ kỹ thuật luôn có người trực, hoan nghênh mọi thảo luận & trao đổi.
 * Diễn đàn LCSC: https://oshwhub.com/forum
 *
 * Theo dõi kênh Bilibili: 【立创开发板】 để cập nhật tin tức mới nhất!
 *
 * Không dựa vào việc bán board để kiếm lợi nhuận, 
 * mà lấy sứ mệnh **bồi dưỡng kỹ sư Trung Quốc** làm mục tiêu.
 *
 * Change Logs:
 * Date           Author       Notes
 * 2024-08-02     LCKFB-LP     phiên bản đầu tiên
 */
#ifndef __SPI_FLASH_H__
#define __SPI_FLASH_H__
#include "stm32f4xx.h"
#define BSP_GPIO_RCU                  RCC_AHB1Periph_GPIOA // Clock GPIO
#define BSP_SPI_RCU                   RCC_APB2Periph_SPI1  // Clock SPI
#define BSP_SPI_NSS_RCU               RCC_AHB1Periph_GPIOA // Clock CS pin
#define BSP_GPIO_PORT                 GPIOA
#define BSP_GPIO_AF                   GPIO_AF_SPI1
#define BSP_SPI                       SPI1
#define BSP_SPI_NSS                   GPIO_Pin_4        // CS phần mềm
#define BSP_SPI_SCK                   GPIO_Pin_5
#define BSP_SPI_SCK_PINSOURCE         GPIO_PinSource5
#define BSP_SPI_MISO                  GPIO_Pin_6
#define BSP_SPI_MISO_PINSOURCE        GPIO_PinSource6
#define BSP_SPI_MOSI                  GPIO_Pin_7
#define BSP_SPI_MOSI_PINSOURCE        GPIO_PinSource7
#define W25QXX_CS_ON(x)               GPIO_WriteBit(BSP_GPIO_PORT, BSP_SPI_NSS, x ? Bit_SET : Bit_RESET)
void bsp_spi_init(void);
uint8_t spi_read_write_byte(uint8_t dat);
uint16_t W25Q128_readID(void);
void W25Q128_write_enable(void);
void W25Q128_wait_busy(void);
void W25Q128_erase_sector(uint32_t addr);
void W25Q128_write(uint8_t* buffer, uint32_t addr, uint16_t numbyte);
void W25Q128_read(uint8_t* buffer,uint32_t read_addr,uint16_t read_length);
#endifTrong file main.c viết đoạn code như sau:
/*
 * Tài liệu phần cứng & phần mềm của bo mạch phát triển LCSC (立创开发板) 
 * cùng với các board mở rộng liên quan đều được **mã nguồn mở** trên trang chính thức.
 *
 * Trang web chính thức: www.lckfb.com
 * Diễn đàn hỗ trợ kỹ thuật luôn có người trực, hoan nghênh mọi thảo luận & trao đổi.
 * Diễn đàn LCSC: https://oshwhub.com/forum
 *
 * Theo dõi kênh Bilibili: 【立创开发板】 để cập nhật tin tức mới nhất!
 *
 * Không dựa vào việc bán board để kiếm lợi nhuận, 
 * mà lấy sứ mệnh **bồi dưỡng kỹ sư Trung Quốc** làm mục tiêu.
 *
 * Change Logs:
 * Date           Author       Notes
 * 2024-08-02     LCKFB-LP     phiên bản đầu tiên
 */
#include "board.h"
#include "bsp_uart.h"
#include <stdio.h>
#include "spi_flash.h"
#include <string.h>
int main(void)
{
        board_init();
        uart1_init(115200U);
        /* Khởi tạo SPI */
        bsp_spi_init();
        // Khai báo buffer
        unsigned char buff[20] = {0};
        printf("\r\n=========【Bắt đầu】========\r\n");
        // Xóa sector 0 của Flash
        printf("\r\n【1】Xóa sector 0 của Flash......\r\n");
        W25Q128_erase_sector(0);
        printf("Xóa sector 0 hoàn tất!!\r\n");
        delay_ms(200);
        // Đọc ID thiết bị W25Q128
        printf("\r\n【2】Đọc ID thiết bị......\r\n");
        printf("Thiết bị ID = %X\r\n",W25Q128_readID());
        // Đọc 10 byte dữ liệu từ địa chỉ 0 vào buff
        printf("\r\n【3】Đọc 10 byte dữ liệu từ địa chỉ 0 vào buff......\r\n");
        W25Q128_read(buff, 0, 10);
        // Xuất dữ liệu vừa đọc
        printf("Dữ liệu đọc được = %s\r\n",buff);
        delay_ms(200);
        // Ghi 10 byte dữ liệu "立创开发板" vào địa chỉ 0
        printf("\r\n【4】Ghi 10 byte dữ liệu \"立创开发板\" vào địa chỉ 0......\r\n");
        W25Q128_write((uint8_t *)"立创开发板", 0, 10);
        // Chờ ghi hoàn tất
        delay_ms(200);
        printf("Ghi dữ liệu thành công!\r\n");
        // Đọc lại 10 byte từ địa chỉ 0 vào buff
        printf("\r\n【5】Đọc 10 byte dữ liệu từ địa chỉ 0 vào buff......\r\n");
        W25Q128_read(buff, 0, 10);
        // Xuất dữ liệu vừa đọc
        printf("Dữ liệu đọc được = %s\r\n",buff);
        delay_ms(1000);
        // Vì đã ghi từ địa chỉ 0, nên phải xóa lại sector 0
        printf("\r\n【6】Kết thúc test, xóa dữ liệu đã ghi......\r\n");
        W25Q128_erase_sector(0);
        delay_ms(200);
        // Clear buffer
        memset(buff,0,sizeof(buff));
        printf("\r\n=========【Kết thúc】========\r\n");
        while(1)
        {
                delay_ms(100);
        }
}Xác nhận hiện tượng: kiểm tra chức năng đọc/ghi 
- Đọc được ID = EF17
- Sau khi ghi dữ liệu, đọc lại hiển thị 立创开发板

Code trong chương này 
Có trong thư mục Baidu Netdisk đi kèm tài liệu giới thiệu bo mạch:立创·梁山派·天空星 STM32F407VET6 开发板资料 / 第03章软件资料 / 代码例程 / 013硬件SPI(flash)
Kiểm tra SPI FLASH phần mềm 
Tạo hai file, đặt tên lần lượt là spi_flash.c và spi_flash.h. Viết mã hoàn chỉnh vào trong:
spi_flash.c
/*
 * Tài liệu phần cứng & phần mềm của bo mạch phát triển LCKFB
 * Trang chính thức: www.lckfb.com
 * Diễn đàn kỹ thuật: https://oshwhub.com/forum
 * Bilibili: 【立创开发板】
 * Mục tiêu: không kiếm tiền từ việc bán bo mạch, mà đào tạo kỹ sư Trung Quốc
 *
 * Change Logs:
 * Date           Author       Notes
 * 2024-08-02     LCKFB-LP     first version
 */
#include "spi_flash.h"
#include "board.h"
/**********************************************************
 * Tên hàm   :bsp_spi_init
 * Chức năng:Khởi tạo SPI
 * Tham số vào:Không
 * Trả về    :Không
 * Tác giả   :LC
 * Ghi chú   :Không
**********************************************************/
void bsp_spi_init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure1;
    GPIO_InitTypeDef GPIO_InitStructure2;
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
    GPIO_InitStructure1.GPIO_Pin   = BSP_SPI_NSS|BSP_SPI_SCK|BSP_SPI_MOSI;
    GPIO_InitStructure1.GPIO_Mode  = GPIO_Mode_OUT;   // Xuất push-pull
    GPIO_InitStructure1.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(BSP_GPIO_PORT, &GPIO_InitStructure1);
    GPIO_InitStructure2.GPIO_Pin  = BSP_SPI_MISO;
    GPIO_InitStructure2.GPIO_Mode = GPIO_Mode_IN;     // Chế độ nhập
    GPIO_Init(BSP_GPIO_PORT, &GPIO_InitStructure2);
    W25QXX_CS_ON(1);   // Kéo CS lên mức cao
    W25QXX_SCK_ON(1);  // Kéo SCK lên mức cao
}
/**
 * Gửi/nhận 1 byte qua SPI (bit-bang)
 */
uint8_t spi_read_write_byte(uint8_t dat)
{
    uint8_t i;
    uint8_t rxData = 0;
    for(i = 0; i < 8; i++)
    {
        W25QXX_SCK_ON(0);
        delay_us(1);
        // Gửi dữ liệu
        if(dat & 0x80)
            W25QXX_MOSI_ON(1);
        else
            W25QXX_MOSI_ON(0);
        dat <<= 1;
        delay_us(1);
        W25QXX_SCK_ON(1);
        delay_us(1);
        // Nhận dữ liệu
        rxData <<= 1;
        if(W25QXX_MISO_ON())
            rxData |= 0x01;
        delay_us(1);
    }
    W25QXX_SCK_ON(0);
    return rxData;
}
/**
 * Đọc ID của chip W25Q128
 */
uint16_t W25Q128_readID(void)
{
    uint16_t temp = 0;
    W25QXX_CS_ON(0);
    spi_read_write_byte(0x90); // Lệnh đọc ID
    spi_read_write_byte(0x00); // Địa chỉ 000000H
    spi_read_write_byte(0x00);
    spi_read_write_byte(0x00);
    temp |= spi_read_write_byte(0xFF) << 8; // ID hãng sản xuất
    temp |= spi_read_write_byte(0xFF);      // ID thiết bị
    W25QXX_CS_ON(1);
    return temp;
}
/**
 * Gửi lệnh cho phép ghi
 */
void W25Q128_write_enable(void)
{
    W25QXX_CS_ON(0);
    spi_read_write_byte(0x06); // Lệnh Write Enable
    W25QXX_CS_ON(1);
}
/**********************************************************
 * Tên hàm   :W25Q128_wait_busy
 * Chức năng:Kiểm tra xem chip có đang bận không
 * Tham số vào:Không
 * Trả về    :Không
**********************************************************/
void W25Q128_wait_busy(void)
{
    unsigned char byte = 0;
    do {
        W25QXX_CS_ON(0);
        spi_read_write_byte(0x05);          // Lệnh đọc thanh ghi trạng thái
        byte = spi_read_write_byte(0xFF);  // Nhận trạng thái
        W25QXX_CS_ON(1);
    } while((byte & 0x01) == 1); // Nếu BUSY=1 thì tiếp tục chờ
}
/**********************************************************
 * Tên hàm   :W25Q128_erase_sector
 * Chức năng:Xóa một sector
 * Tham số vào:addr = số sector cần xóa (0 ~ 4096)
 * Trả về    :Không
 * 
 * Ghi chú:
 *  - Dung lượng W25Q128 = 16 MB
 *  - Chia thành 256 Block, mỗi Block = 64 KB
 *  - Mỗi Block chia thành 16 Sector, mỗi Sector = 4 KB
 *  - Đơn vị xóa nhỏ nhất là 1 Sector (4 KB)
**********************************************************/
void W25Q128_erase_sector(uint32_t addr)
{
    addr *= 4096;                // Tính địa chỉ thực
    W25Q128_write_enable();      // Cho phép ghi
    W25Q128_wait_busy();         // Chờ sẵn sàng
    W25QXX_CS_ON(0);
    spi_read_write_byte(0x20);   // Lệnh xóa sector
    spi_read_write_byte((uint8_t)(addr >> 16));
    spi_read_write_byte((uint8_t)(addr >> 8));
    spi_read_write_byte((uint8_t)addr);
    W25QXX_CS_ON(1);
    W25Q128_wait_busy();         // Chờ xóa xong
}
/**********************************************************
 * Tên hàm   :W25Q128_write
 * Chức năng:Ghi dữ liệu vào W25Q128
 * Tham số vào:buffer = dữ liệu, addr = địa chỉ, numbyte = số byte
 * Trả về    :Không
**********************************************************/
void W25Q128_write(uint8_t* buffer, uint32_t addr, uint16_t numbyte)
{
    unsigned int i = 0;
    W25Q128_erase_sector(addr/4096); // Xóa sector trước khi ghi
    W25Q128_write_enable();          // Cho phép ghi
    W25Q128_wait_busy();             // Chờ sẵn sàng
    W25QXX_CS_ON(0);
    spi_read_write_byte(0x02);       // Lệnh Page Program
    spi_read_write_byte((uint8_t)(addr >> 16));
    spi_read_write_byte((uint8_t)(addr >> 8));
    spi_read_write_byte((uint8_t)addr);
    for(i = 0; i < numbyte; i++)
        spi_read_write_byte(buffer[i]);
    W25QXX_CS_ON(1);
    W25Q128_wait_busy();             // Chờ ghi xong
}
/**********************************************************
 * Tên hàm   :W25Q128_read
 * Chức năng:Đọc dữ liệu từ W25Q128
 * Tham số vào:buffer = nơi lưu dữ liệu
 *              read_addr = địa chỉ bắt đầu
 *              read_length = số byte cần đọc
 * Trả về    :Không
**********************************************************/
void W25Q128_read(uint8_t* buffer, uint32_t read_addr, uint16_t read_length)
{
    uint16_t i;
    W25QXX_CS_ON(0);
    spi_read_write_byte(0x03); // Lệnh Read Data
    spi_read_write_byte((uint8_t)(read_addr >> 16));
    spi_read_write_byte((uint8_t)(read_addr >> 8));
    spi_read_write_byte((uint8_t)read_addr);
    for(i = 0; i < read_length; i++)
        buffer[i] = spi_read_write_byte(0xFF);
    W25QXX_CS_ON(1);
}spi_flash.h
/*
 * Tài liệu phần cứng & phần mềm của bo mạch phát triển LCKFB
 * Trang chính thức: www.lckfb.com
 * Diễn đàn kỹ thuật: https://oshwhub.com/forum
 * Bilibili: 【立创开发板】
 * Mục tiêu: không kiếm tiền từ việc bán bo mạch, mà đào tạo kỹ sư Trung Quốc
 *
 * Change Logs:
 * Date           Author       Notes
 * 2024-08-02     LCKFB-LP     first version
 */
#ifndef __SPI_FLASH_H__
#define __SPI_FLASH_H__
#include "stm32f4xx.h"
/* Cấu hình GPIO dùng cho SPI (bit-bang) */
#define BSP_GPIO_RCU      RCC_AHB1Periph_GPIOA   // Clock GPIO
#define BSP_GPIO_PORT     GPIOA                  // Port GPIO
/* Chân SPI Flash */
#define BSP_SPI_NSS       GPIO_Pin_4   // CS (Chip Select)
#define BSP_SPI_SCK       GPIO_Pin_5   // SCK (Clock)
#define BSP_SPI_MISO      GPIO_Pin_6   // MISO (Master In Slave Out)
#define BSP_SPI_MOSI      GPIO_Pin_7   // MOSI (Master Out Slave In)
/* Macro điều khiển chân */
#define W25QXX_SCK_ON(x)   GPIO_WriteBit(BSP_GPIO_PORT, BSP_SPI_SCK,  x ? Bit_SET : Bit_RESET)
#define W25QXX_MOSI_ON(x)  GPIO_WriteBit(BSP_GPIO_PORT, BSP_SPI_MOSI, x ? Bit_SET : Bit_RESET)
#define W25QXX_CS_ON(x)    GPIO_WriteBit(BSP_GPIO_PORT, BSP_SPI_NSS,  x ? Bit_SET : Bit_RESET)
#define W25QXX_MISO_ON()   GPIO_ReadInputDataBit(BSP_GPIO_PORT, BSP_SPI_MISO)
/* Prototype các hàm SPI Flash */
void bsp_spi_init(void);
uint8_t  spi_read_write_byte(uint8_t dat);
uint16_t W25Q128_readID(void);
void     W25Q128_write_enable(void);
void     W25Q128_wait_busy(void);
void     W25Q128_erase_sector(uint32_t addr);
void     W25Q128_write(uint8_t* buffer, uint32_t addr, uint16_t numbyte);
void     W25Q128_read(uint8_t* buffer, uint32_t read_addr, uint16_t read_length);
#endifTrong file main.c viết như sau: 
/*
 * Tài liệu phần cứng & phần mềm của bo mạch phát triển LCKFB
 * Trang chính thức: www.lckfb.com
 * Diễn đàn kỹ thuật: https://oshwhub.com/forum
 * Bilibili: 【立创开发板】
 * Mục tiêu: không kiếm tiền từ việc bán bo mạch, mà đào tạo kỹ sư Trung Quốc
 *
 * Change Logs:
 * Date           Author       Notes
 * 2024-08-02     LCKFB-LP    first version
 */
#include "board.h"
#include "bsp_uart.h"
#include <stdio.h>
#include "spi_flash.h"
#include <string.h>
int main(void)
{
    board_init();
    uart1_init(115200U);
    /* Khởi tạo SPI */
    bsp_spi_init();
    // Định nghĩa bộ đệm
    unsigned char buff[20] = {0};
    printf("\r\n=========【Bắt đầu】========\r\n");
    // 1. Xóa sector 0 của Flash
    printf("\r\n【1】Đang xóa sector 0 của Flash...\r\n");
    W25Q128_erase_sector(0);
    printf("Xóa sector 0 của Flash thành công!!\r\n");
    delay_ms(200);
    // 2. Đọc ID của W25Q128
    printf("\r\n【2】Đọc ID thiết bị...\r\n");
    printf("Thiết bị ID = %X\r\n", W25Q128_readID());
    // 3. Đọc 10 byte từ địa chỉ 0 vào buff
    printf("\r\n【3】Đọc 10 byte dữ liệu từ địa chỉ 0 vào buff...\r\n");
    W25Q128_read(buff, 0, 10);
    printf("Dữ liệu đọc được = %s\r\n", buff);
    delay_ms(200);
    // 4. Ghi 10 byte dữ liệu "LCKFB板" vào địa chỉ 0
    printf("\r\n【4】Ghi 10 byte dữ liệu “立创开发板” vào địa chỉ 0...\r\n");
    W25Q128_write((uint8_t *)"立创开发板", 0, 10);
    delay_ms(200);
    printf("Ghi dữ liệu thành công!\r\n");
    // 5. Đọc lại 10 byte từ địa chỉ 0 vào buff
    printf("\r\n【5】Đọc 10 byte dữ liệu từ địa chỉ 0 vào buff...\r\n");
    W25Q128_read(buff, 0, 10);
    printf("Dữ liệu đọc được = %s\r\n", buff);
    delay_ms(1000);
    // 6. Vì đã ghi từ địa chỉ 0 nên xóa lại sector 0
    printf("\r\n【6】Kết thúc test – Xóa dữ liệu vừa ghi...\r\n");
    W25Q128_erase_sector(0);
    delay_ms(200);
    // Xóa sạch bộ đệm
    memset(buff, 0, sizeof(buff));
    printf("\r\n=========【Kết thúc】========\r\n");
    while(1)
    {
        delay_ms(100);
    }
}Hiện tượng kiểm chứng 
- Kiểm tra chức năng đọc/ghi của SPI Flash.
- Đọc được ID = EF17.
- Sau khi ghi dữ liệu, đọc lại nhận được chuỗi: 立创开发板 (Bo mạch phát triển LCKFB).

📂 Mã nguồn của chương này 
Nằm trong link Baidu Netdisk giới thiệu bo mạch:
LCKFB · Liangshanpai · SkyStar STM32F407VET6 └── Tài liệu chương 03 – Phần mềm └── Ví dụ code └── 014 SPI bằng phần mềm (Flash)