Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Deep Dive into SPI Communication and W25Q64 Flash Interfacing with STM32

Tech May 16 1

SPI Protocol Fundamentals

The Serial Peripheral Interface (SPI) is a synchronous, full-duplex communication protocol widely used for high-speed data exchange between a microcontroller (Master) and various peripherals (Slaves). Unlike I2C, SPI does not use device addresses in the data stream; instead, it utilizes a dedicated hardware line for device selection.

The protocol relies on four primary signal lines:

  • SCK (Serial Clock): Generated by the master to synchronize data transmission.
  • MOSI (Master Output Slave Input): Data line for sending bits from the master to the slave.
  • MISO (Master Input Slave Output): Data line for receiving bits from the slave to the master.
  • SS/CS (Slave Select / Chip Select): A low-active signal used by the master to initiate communication with a specific slave.

Hardware Connection and Logic

In a multi-slave configuration, the SCK, MOSI, and MISO lines are shared across all devices. However, the Master must provide a unique SS line for each slave. Only the slave with its SS line pulled low will respond to the clock and drive the MISO line. To prevent bus contention, the MISO pin of an unselected slave must enter a high-impedance state.

The core mechanism of SPI is the "Exchange" principle. Both the master and slave contain an 8-bit shift register. On every clock pulse, the master shifts one bit out via MOSI and simultaneously shifts one bit in via MISO. After eight clock cycles, the master and slave have effectively swapped one byte of data.

SPI Transmission Modes

SPI flexibility comes from two parameters: Clock Polarity (CPOL) and Clock Phase (CPHA). These define the idle state of the clock and which edge is used to sample data.

W25Q64 Flash Memory Architecture

The W25Q64 is an 8MB (64Mbit) Nor Flash memory module. Its organized hierarchically to balance storage capacity and erase efficiency:

  • Block: 128 blocks of 64KB each.
  • Sector: Each block contains 16 sectors of 4KB each. The sector is the minimum unit for erasure.
  • Page: Each sector contains 16 pages of 256 bytes each. The page is the unit for programming (writing).

Flash Constraints:

  1. Write Enable: A "Write Enable" commend must be sent before any modify operation.
  2. Erasure: Memory must be erased (bits set to 1) before it can be programmed. Programming can only change a bit from 1 to 0.
  3. Page Boundary: A single write operation cannot exceed 256 bytes or cross a page boundary; otherwise, the pointer wraps around to the start of the current page.
  4. Busy State: During internal write or erase cycles, the device sets a BUSY bit in its status register and ignores most commands.

Software-Based SPI Implementation (Bit-Banging)

Software SPI provides maximum flexibility by manually toggling GPIO pins. The following example demonstrates a Mode 0 implementation.


/* Physical Layer Abstraction */
void SPI_Set_CS(uint8_t state) {
    GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)state);
}

void SPI_Set_CLK(uint8_t state) {
    GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)state);
}

void SPI_Set_MOSI(uint8_t state) {
    GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)state);
}

uint8_t SPI_Get_MISO(void) {
    return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}

/* Bit-Banging Byte Transfer (Mode 0) */
uint8_t SPI_Transfer(uint8_t dataOut) {
    uint8_t dataIn = 0x00;
    for (int i = 0; i < 8; i++) {
        // Setup MOSI (MSB first)
        SPI_Set_MOSI(dataOut & (0x80 >> i));
        // Rising edge for sampling
        SPI_Set_CLK(1);
        if (SPI_Get_MISO()) {
            dataIn |= (0x80 >> i);
        }
        // Falling edge for next bit setup
        SPI_Set_CLK(0);
    }
    return dataIn;
}

Interfacing with W25Q64 using SPI

Once the transfer primitive is established, higher-level functions are built by sending specific instruction codes defined in the W25Q64 datasheet.


#define CMD_READ_ID         0x9F
#define CMD_WRITE_ENABLE    0x06
#define CMD_SECTOR_ERASE    0x20
#define CMD_PAGE_PROGRAM    0x02
#define CMD_READ_DATA       0x03
#define STATUS_REG_BUSY     0x01

void W25_WaitForReady(void) {
    SPI_Set_CS(0);
    SPI_Transfer(0x05); // Read Status Register 1
    while (SPI_Transfer(0xFF) & STATUS_REG_BUSY);
    SPI_Set_CS(1);
}

void W25_EraseSector(uint32_t addr) {
    SPI_Set_CS(0);
    SPI_Transfer(CMD_WRITE_ENABLE);
    SPI_Set_CS(1);

    SPI_Set_CS(0);
    SPI_Transfer(CMD_SECTOR_ERASE);
    SPI_Transfer((addr >> 16) & 0xFF);
    SPI_Transfer((addr >> 8) & 0xFF);
    SPI_Transfer(addr & 0xFF);
    SPI_Set_CS(1);
    
    W25_WaitForReady();
}

STM32 Hardware SPI Configuration

Using the STM32's internal SPI peripheral reduces CPU overhead. The peripheral handles timing and shift register logic automatically.


void Hardware_SPI_Init(void) {
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);

    GPIO_InitTypeDef GPIO_InitStruct;
    // SCK and MOSI as Alternate Function Push-Pull
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    // MISO as Input Pull-up
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    // Software CS on PA4
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    GPIO_SetBits(GPIOA, GPIO_Pin_4);

    SPI_InitTypeDef SPI_InitStruct;
    SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
    SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
    SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low;
    SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge;
    SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;
    SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
    SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
    SPI_InitStruct.SPI_CRCPolynomial = 7;
    SPI_Init(SPI1, &SPI_InitStruct);

    SPI_Cmd(SPI1, ENABLE);
}

uint8_t Hardware_SPI_Transfer(uint8_t data) {
    // Wait for transmit buffer empty
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
    SPI_I2S_SendData(SPI1, data);
    // Wait for receive buffer not empty
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
    return SPI_I2S_ReceiveData(SPI1);
}

The hardware implementation follows a strict sequence: ensure TXE (Transmit Empty) is set before writing to the Data Register (DR), and ensure RXNE (Receive Not Empty) is set before reading from it. This ensures the 8-bit exchange cycle is fully completed.

Related Articles

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

SBUS Signal Analysis and Communication Implementation Using STM32 with Fus Remote Controller

Overview In a recent project, I utilized the SBUS protocol with the Fus remote controller to control a vehicle's basic operations, including movement, lights, and mode switching. This article is aimed...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.