Software Based SPI for Raspberry Pi Kernels > 4.8 API V1

We go over software defined SPI on a Raspberry Pi Kernel > 4.8!

Software Based SPI for Raspberry Pi Kernels > 4.8 API V1

After Kernel 4.8 Raspberry Pi GPIO handling went to ioctl and file based handling. Please note this is for GPIO API Version 1.

You can check your current Linux Kernel with:

uname -a
c@c:~/c_software/20_spi_try_2 $ uname -a
Linux c 6.12.62+rpt-rpi-v6 #1 Raspbian 1:6.12.62-1+rpt1 (2025-12-18) armv6l GNU/Linux

This is a big change because one no longer uses the older gpio_cmd sets.  Grok 4 wrote us a software based SPI using the new model for diagnostic purposes.

If you want the one that is based on the older kernels <=4.8

Software Based SPI for Raspberry Pi Kernel <=4.8
We get Grok 4 to write us a Software based SPI bus protocol for testing purposes!

Complete C Program: Software Bit-Banged SPI Using Modern Linux GPIO Character Device API

The program below implements a pure software (bit-banged) SPI master using the modern Linux GPIO character device interface (/dev/gpiochip0 + ioctl()). This is the recommended replacement for the deprecated sysfs GPIO interface (/sys/class/gpio) in current Linux kernels.

It requests individual line handles for each pin (CE = GPIO 0, CSN = GPIO 1, SCK = GPIO 2, MOSI = GPIO 3, MISO = GPIO 4) with the correct direction. SPI Mode 0 is used (clock idle low, data sampled on rising edge). CSN is asserted (low) only for the duration of each 8-bit transfer.

The interactive main function behaves exactly as requested:

  • 'r' → repeatedly transmits binary 10101010 (0xAA) 20 times and prints the received byte each time.
  • 'b' → prompts for an 8-bit binary string (e.g. 10101010), transmits it, and prints the received byte.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdint.h>
#include <linux/gpio.h>
#include <sys/ioctl.h>

// GPIO pins (BCM numbering)
#define PIN_CE   0
#define PIN_CSN  1
#define PIN_SCK  2
#define PIN_MOSI 3
#define PIN_MISO 4

#define GPIOCHIP "/dev/gpiochip0"

struct gpio_line {
    int fd;
};

// Request a single GPIO line (output or input)
static int gpio_request(struct gpio_line *line, unsigned int offset, int is_output)
{
    int chip_fd = open(GPIOCHIP, O_RDONLY);
    if (chip_fd < 0) {
        perror("Failed to open /dev/gpiochip0");
        return -1;
    }

    struct gpiohandle_request req = {0};
    req.lineoffsets[0] = offset;
    req.lines = 1;
    req.flags = is_output ? GPIOHANDLE_REQUEST_OUTPUT : GPIOHANDLE_REQUEST_INPUT;
    strncpy(req.consumer_label, "soft_spi", sizeof(req.consumer_label) - 1);

    if (ioctl(chip_fd, GPIO_GET_LINEHANDLE_IOCTL, &req) < 0) {
        perror("GPIO_GET_LINEHANDLE_IOCTL failed");
        close(chip_fd);
        return -1;
    }

    line->fd = req.fd;
    close(chip_fd);
    return 0;
}

// Set GPIO value
static void gpio_write(struct gpio_line *line, int value)
{
    struct gpiohandle_data data = {0};
    data.values[0] = value ? 1 : 0;
    ioctl(line->fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data);
}

// Read GPIO value
static int gpio_read(struct gpio_line *line)
{
    struct gpiohandle_data data = {0};
    ioctl(line->fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &data);
    return data.values[0];
}

// Initialize all pins and set idle state (SPI Mode 0)
static int soft_spi_init(struct gpio_line *ce, struct gpio_line *csn,
                         struct gpio_line *sck, struct gpio_line *mosi,
                         struct gpio_line *miso)
{
    if (gpio_request(ce,   PIN_CE,   1) < 0 ||
        gpio_request(csn,  PIN_CSN,  1) < 0 ||
        gpio_request(sck,  PIN_SCK,  1) < 0 ||
        gpio_request(mosi, PIN_MOSI, 1) < 0 ||
        gpio_request(miso, PIN_MISO, 0) < 0) {
        return -1;
    }

    gpio_write(ce,   0);   // CE low (default)
    gpio_write(csn,  1);   // CSN inactive high
    gpio_write(sck,  0);   // Clock idle low
    gpio_write(mosi, 0);
    printf("Software SPI initialized (GPIO character device API)\n");
    return 0;
}

// Perform one 8-bit SPI transfer (MSB first, Mode 0)
static uint8_t spi_transfer_byte(struct gpio_line *mosi, struct gpio_line *miso,
                                 struct gpio_line *sck, uint8_t tx_byte)
{
    uint8_t rx_byte = 0;
    for (int i = 7; i >= 0; i--) {
        gpio_write(mosi, (tx_byte >> i) & 1);

        gpio_write(sck, 1);                // Rising edge
        usleep(2);
        if (gpio_read(miso))
            rx_byte |= (1 << i);

        gpio_write(sck, 0);                // Falling edge
        usleep(2);
    }
    return rx_byte;
}

// Full transfer with CSN framing
static void do_transfer(struct gpio_line *csn, struct gpio_line *mosi,
                        struct gpio_line *miso, struct gpio_line *sck,
                        uint8_t tx)
{
    gpio_write(csn, 0);                    // Assert CSN
    usleep(10);

    uint8_t rx = spi_transfer_byte(mosi, miso, sck, tx);

    gpio_write(csn, 1);                    // De-assert CSN
    usleep(10);

    // Print result
    printf("Sent: 0x%02X (binary ", tx);
    for (int i = 7; i >= 0; i--) printf("%d", (tx >> i) & 1);
    printf(")  ->  Received: 0x%02X (binary ", rx);
    for (int i = 7; i >= 0; i--) printf("%d", (rx >> i) & 1);
    printf(")\n");
}

int main(void)
{
    struct gpio_line ce, csn, sck, mosi, miso;

    if (soft_spi_init(&ce, &csn, &sck, &mosi, &miso) != 0) {
        fprintf(stderr, "GPIO initialization failed. Run as root or add user to gpio group.\n");
        return 1;
    }

    printf("\nSoftware SPI Simulator (GPIO 0-4)\n");
    printf("Commands:\n");
    printf("  r - repeatedly send binary 10101010 (0xAA)\n");
    printf("  b - send custom 8-bit binary value\n");
    printf("  q - quit\n\n");

    while (1) {
        printf("Enter command: ");
        char cmd;
        if (scanf(" %c", &cmd) != 1) break;

        if (cmd == 'q' || cmd == 'Q') {
            break;
        }
        else if (cmd == 'r' || cmd == 'R') {
            printf("Sending 0xAA repeatedly (20 transfers)...\n");
            for (int i = 0; i < 20; i++) {
                do_transfer(&csn, &mosi, &miso, &sck, 0xAA);
                usleep(150000);   // 150 ms pause between transfers
            }
        }
        else if (cmd == 'b' || cmd == 'B') {
            printf("Enter 8 bits (e.g. 10101010): ");
            char bits[9] = {0};
            scanf("%8s", bits);

            uint8_t val = 0;
            for (int i = 0; i < 8 && bits[i]; i++) {
                if (bits[i] == '1')
                    val |= (1u << (7 - i));
            }
            do_transfer(&csn, &mosi, &miso, &sck, val);
        }
        else {
            printf("Unknown command. Use r, b or q.\n");
        }
    }

    // Cleanup (optional – OS closes on exit)
    close(ce.fd);
    close(csn.fd);
    close(sck.fd);
    close(mosi.fd);
    close(miso.fd);

    printf("Program terminated.\n");
    return 0;
}

Compilation and Execution

gcc -o soft_spi soft_spi.c
sudo ./soft_spi

(Or add your user to the gpio group on Raspberry Pi / embedded boards so sudo is not required.)

This code is fully self-contained, uses only standard POSIX and Linux GPIO ioctl calls, and matches the exact pin mapping and interactive behaviour you specified. Let me know if you need any modifications (e.g., different SPI mode, CE toggling, or migration to GPIO v2 API).

Linux Rocks Every Day