Software Based SPI for Raspberry Pi Kernels > 5.1 API V2

We go over Raspberry Pi GPIO Software SPI using V2 API using > 4.8 Kernel!

Software Based SPI for Raspberry Pi Kernels > 5.1 API V2

Please note - in the C language if you are driving GPIO pins from a Raspberry pi :

  • Your kernel will determine which library to call (< 4.8 or < 5.1 (API v1) or > 5.1 (API V2))
  • Grok 4 wrote this specifically for hopefully the latest Kernel (5.10+) and the Latest API (v2)

If you you would like older references, here are some guides for that too!

Raspberry Pi: Understanding GPIO Pins in the System Context
Understanding how pins are driven high and low in a Raspberry Pi.
Software Based SPI for Raspberry Pi Kernels > 4.8 API V1
We go over software defined SPI on a Raspberry Pi Kernel > 4.8!

This can be important to know as you can pull all kinds of guides off the internet - they will compile - but they won't work. A true frustration!

Here is the complete, rewritten program using the modern Linux GPIO v2 character device API. This is the current recommended interface (introduced in kernel 5.10 and the long-term standard).

Check your current kernel with:

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

Key improvements in v2:

  • Supports mixed directions (outputs and inputs) in a single line request.
  • Uses struct gpio_v2_line_request and struct gpio_v2_line_config.
  • Individual line attributes allow precise configuration (output vs. input).
  • Values are set/read using 64-bit masks and bits for efficiency.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdint.h>
#include <signal.h>
#include <linux/gpio.h>
#include <sys/ioctl.h>

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

#define GPIOCHIP "/dev/gpiochip0"
#define NUM_LINES 5

static int line_fd = -1;   // Single file descriptor for all requested lines

// Bit masks for each pin (corresponding to their position in the request)
#define MASK_CE   (1ULL << 0)
#define MASK_CSN  (1ULL << 1)
#define MASK_SCK  (1ULL << 2)
#define MASK_MOSI (1ULL << 3)
#define MASK_MISO (1ULL << 4)

static void sigint_handler(int sig)
{
    (void)sig;
    printf("\nSIGINT received. Cleaning up GPIO lines...\n");
    if (line_fd >= 0) close(line_fd);
    exit(EXIT_SUCCESS);
}

// Set a GPIO line high (1) or low (0) using its mask
static void gpio_write(uint64_t mask, int value)
{
    struct gpio_v2_line_values vals = {0};
    vals.mask = mask;
    vals.bits = value ? mask : 0;   // Set bits only where mask is 1

    if (ioctl(line_fd, GPIO_V2_LINE_SET_VALUES_IOCTL, &vals) < 0) {
        perror("GPIO_V2_LINE_SET_VALUES_IOCTL");
    }
}

// Read the value of MISO
static int gpio_read_miso(void)
{
    struct gpio_v2_line_values vals = {0};
    vals.mask = MASK_MISO;

    if (ioctl(line_fd, GPIO_V2_LINE_GET_VALUES_IOCTL, &vals) < 0) {
        perror("GPIO_V2_LINE_GET_VALUES_IOCTL");
        return 0;
    }
    return (vals.bits & MASK_MISO) ? 1 : 0;
}

static int soft_spi_init(void)
{
    int chip_fd = open(GPIOCHIP, O_RDONLY);
    if (chip_fd < 0) {
        perror("Failed to open /dev/gpiochip0");
        return -1;
    }

    struct gpio_v2_line_request req = {0};
    struct gpio_v2_line_config  config = {0};

    // Specify the lines we want (order defines bit positions 0..4)
    req.offsets[0] = PIN_CE;
    req.offsets[1] = PIN_CSN;
    req.offsets[2] = PIN_SCK;
    req.offsets[3] = PIN_MOSI;
    req.offsets[4] = PIN_MISO;
    req.num_lines = NUM_LINES;

    strncpy(req.consumer, "soft_spi", sizeof(req.consumer) - 1);

    // Configure each line individually via attributes
    config.num_attrs = 5;

    // CE - Output, initial low
    config.attrs[0].mask = MASK_CE;
    config.attrs[0].attr.flags = GPIO_V2_LINE_FLAG_OUTPUT;

    // CSN - Output, initial high (SPI idle)
    config.attrs[1].mask = MASK_CSN;
    config.attrs[1].attr.flags = GPIO_V2_LINE_FLAG_OUTPUT;

    // SCK - Output, initial low (Mode 0)
    config.attrs[2].mask = MASK_SCK;
    config.attrs[2].attr.flags = GPIO_V2_LINE_FLAG_OUTPUT;

    // MOSI - Output, initial low
    config.attrs[3].mask = MASK_MOSI;
    config.attrs[3].attr.flags = GPIO_V2_LINE_FLAG_OUTPUT;

    // MISO - Input
    config.attrs[4].mask = MASK_MISO;
    config.attrs[4].attr.flags = GPIO_V2_LINE_FLAG_INPUT;

    req.config = config;

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

    line_fd = req.fd;   // This is the file descriptor for all operations
    close(chip_fd);

    // Set initial idle state (SPI Mode 0)
    gpio_write(MASK_CE,   0);
    gpio_write(MASK_CSN,  1);
    gpio_write(MASK_SCK,  0);
    gpio_write(MASK_MOSI, 0);

    printf("Software SPI initialized using GPIO v2 API (pins CE=0, CSN=1, SCK=2, MOSI=3, MISO=4)\n");
    return 0;
}

// Bit-banged 8-bit SPI transfer (MSB first, Mode 0)
static uint8_t spi_transfer_byte(uint8_t tx_byte)
{
    uint8_t rx_byte = 0;

    for (int i = 7; i >= 0; i--) {
        gpio_write(MASK_MOSI, (tx_byte >> i) & 1);

        gpio_write(MASK_SCK, 1);           // Rising edge
        usleep(2);

        if (gpio_read_miso())
            rx_byte |= (1u << i);

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

static void do_transfer(uint8_t tx)
{
    gpio_write(MASK_CSN, 0);               // Assert CSN (active low)
    usleep(10);

    uint8_t rx = spi_transfer_byte(tx);

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

    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)
{
    signal(SIGINT, sigint_handler);

    if (soft_spi_init() != 0) {
        fprintf(stderr, "GPIO v2 initialization failed. Run with sudo or ensure your user has access to /dev/gpiochip0.\n");
        return EXIT_FAILURE;
    }

    printf("\nSoftware SPI Simulator (GPIO v2 API)\n");
    printf("Commands:\n");
    printf("  r - repeatedly send binary 10101010 (0xAA)\n");
    printf("  b - send a specific 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 (10101010) 20 times...\n");
            for (int i = 0; i < 20; i++) {
                do_transfer(0xAA);
                usleep(150000);
            }
        }
        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(val);
        }
        else {
            printf("Unknown command. Use r, b or q.\n");
        }
    }

    if (line_fd >= 0) close(line_fd);
    printf("Program terminated.\n");
    return EXIT_SUCCESS;
}

Compilation and Execution

gcc -o soft_spi_v2 soft_spi_v2.c -Wall -Wextra
sudo ./soft_spi_v2

This version uses a single line request with per-line configuration attributes, ensuring all output pins can drive high/low correctly while MISO remains an input. The v2 API is more robust and future-proof.

If you encounter any errors (e.g., ioctl failure), please share the exact message. On Raspberry Pi systems, ensure the kernel supports GPIO v2 (most recent Raspberry Pi OS versions do). Let me know if further adjustments are needed.

Debugging If it Doesn't Work
The GPIO v2 program compiled and ran without errors, yet the pins do not change state. This is a common situation on Raspberry Pi (especially the Pi Zero you referenced earlier) when using the character device API.

Primary Cause

The Linux kernel's GPIO subsystem grants exclusive ownership of the requested lines to your program. However, on Raspberry Pi hardware, certain low-numbered GPIO lines (including GPIO 0–4) are frequently claimed at boot by other kernel drivers or device tree overlays. These include:

  • I²C (GPIO 0 + 1)
  • SPI (GPIO 7–11, but sometimes lower pins via overlays)
  • UART or other peripherals

When a line is already in use by another driver, the GPIO_V2_GET_LINE_IOCTL call succeeds silently (no error), but the kernel does not actually drive the pin as requested. The pin remains under the control of the other driver or stays in its default input/high-impedance state.

Diagnostic Steps (Run These First)

Please execute the following commands on your Raspberry Pi and share the output:

Check which GPIO chip and lines are available:

gpioinfo

(If gpioinfo is not installed: sudo apt install gpiod)

Check current state of your specific pins:

gpioget gpiochip0 0 1 2 3 4

List active device tree overlays and enabled interfaces:

dtoverlay -l
cat /boot/config.txt | grep -E 'dtparam|dtoverlay'

Check kernel messages for GPIO claims:

dmesg | grep -i gpio

GPIO 0–4 are rarely suitable for direct user control on a standard Raspberry Pi OS image. Change to a clean set of 5 consecutive free pins, for example GPIO 17, 18, 19, 20, 21 (these are almost always available on Pi Zero).

Here is the updated program with:

  • New pin mapping (easily changeable)
  • Added debug output showing success of each ioctl
  • Clear initial state messages
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdint.h>
#include <signal.h>
#include <linux/gpio.h>
#include <sys/ioctl.h>

// === CHANGE THESE PINS IF NEEDED (use free GPIO numbers) ===
#define PIN_CE   17
#define PIN_CSN  18
#define PIN_SCK  19
#define PIN_MOSI 20
#define PIN_MISO 21
// ============================================================

#define GPIOCHIP "/dev/gpiochip0"
#define NUM_LINES 5

static int line_fd = -1;

#define MASK_CE   (1ULL << 0)
#define MASK_CSN  (1ULL << 1)
#define MASK_SCK  (1ULL << 2)
#define MASK_MOSI (1ULL << 3)
#define MASK_MISO (1ULL << 4)

static void sigint_handler(int sig)
{
    (void)sig;
    printf("\nCleaning up GPIO lines...\n");
    if (line_fd >= 0) close(line_fd);
    exit(EXIT_SUCCESS);
}

static void gpio_write(uint64_t mask, int value)
{
    struct gpio_v2_line_values vals = {0};
    vals.mask = mask;
    vals.bits = value ? mask : 0;

    if (ioctl(line_fd, GPIO_V2_LINE_SET_VALUES_IOCTL, &vals) < 0) {
        perror("GPIO_V2_LINE_SET_VALUES_IOCTL failed");
    }
}

static int gpio_read_miso(void)
{
    struct gpio_v2_line_values vals = {0};
    vals.mask = MASK_MISO;

    if (ioctl(line_fd, GPIO_V2_LINE_GET_VALUES_IOCTL, &vals) < 0) {
        perror("GPIO_V2_LINE_GET_VALUES_IOCTL failed");
        return 0;
    }
    return (vals.bits & MASK_MISO) ? 1 : 0;
}

static int soft_spi_init(void)
{
    int chip_fd = open(GPIOCHIP, O_RDONLY);
    if (chip_fd < 0) {
        perror("Failed to open /dev/gpiochip0");
        return -1;
    }

    struct gpio_v2_line_request req = {0};
    struct gpio_v2_line_config config = {0};

    req.offsets[0] = PIN_CE;
    req.offsets[1] = PIN_CSN;
    req.offsets[2] = PIN_SCK;
    req.offsets[3] = PIN_MOSI;
    req.offsets[4] = PIN_MISO;
    req.num_lines = NUM_LINES;
    strncpy(req.consumer, "soft_spi", sizeof(req.consumer) - 1);

    config.num_attrs = 5;

    // CE - Output
    config.attrs[0].mask = MASK_CE;
    config.attrs[0].attr.flags = GPIO_V2_LINE_FLAG_OUTPUT;

    // CSN - Output
    config.attrs[1].mask = MASK_CSN;
    config.attrs[1].attr.flags = GPIO_V2_LINE_FLAG_OUTPUT;

    // SCK - Output
    config.attrs[2].mask = MASK_SCK;
    config.attrs[2].attr.flags = GPIO_V2_LINE_FLAG_OUTPUT;

    // MOSI - Output
    config.attrs[3].mask = MASK_MOSI;
    config.attrs[3].attr.flags = GPIO_V2_LINE_FLAG_OUTPUT;

    // MISO - Input
    config.attrs[4].mask = MASK_MISO;
    config.attrs[4].attr.flags = GPIO_V2_LINE_FLAG_INPUT;

    req.config = config;

    printf("Requesting lines %d,%d,%d,%d,%d ...\n", PIN_CE, PIN_CSN, PIN_SCK, PIN_MOSI, PIN_MISO);
    if (ioctl(chip_fd, GPIO_V2_GET_LINE_IOCTL, &req) < 0) {
        perror("GPIO_V2_GET_LINE_IOCTL failed");
        close(chip_fd);
        return -1;
    }

    line_fd = req.fd;
    close(chip_fd);

    printf("Lines successfully requested. Setting initial idle state (SPI Mode 0)...\n");

    gpio_write(MASK_CE,   0);
    gpio_write(MASK_CSN,  1);
    gpio_write(MASK_SCK,  0);
    gpio_write(MASK_MOSI, 0);

    printf("Initialization complete. Output pins should now be controllable.\n");
    return 0;
}

// ... (the rest of the functions remain identical: spi_transfer_byte, do_transfer, main)

static uint8_t spi_transfer_byte(uint8_t tx_byte)
{
    uint8_t rx_byte = 0;
    for (int i = 7; i >= 0; i--) {
        gpio_write(MASK_MOSI, (tx_byte >> i) & 1);
        gpio_write(MASK_SCK, 1);
        usleep(5);                     // Slightly longer delay for visibility
        if (gpio_read_miso())
            rx_byte |= (1u << i);
        gpio_write(MASK_SCK, 0);
        usleep(5);
    }
    return rx_byte;
}

static void do_transfer(uint8_t tx)
{
    gpio_write(MASK_CSN, 0);
    usleep(20);

    uint8_t rx = spi_transfer_byte(tx);

    gpio_write(MASK_CSN, 1);
    usleep(20);

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

int main(void)
{
    signal(SIGINT, sigint_handler);

    if (soft_spi_init() != 0) {
        fprintf(stderr, "Failed to initialize GPIO. Check gpioinfo output and try different pins.\n");
        return EXIT_FAILURE;
    }

    printf("\nSoftware SPI Simulator (GPIO v2) - Pins: CE=%d, CSN=%d, SCK=%d, MOSI=%d, MISO=%d\n",
           PIN_CE, PIN_CSN, PIN_SCK, PIN_MOSI, PIN_MISO);
    printf("Commands: r (repeat 0xAA), b (custom 8 bits), 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 20 times...\n");
            for (int i = 0; i < 20; i++) {
                do_transfer(0xAA);
                usleep(200000);
            }
        }
        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(val);
        }
        else {
            printf("Unknown command.\n");
        }
    }

    if (line_fd >= 0) close(line_fd);
    printf("Program terminated.\n");
    return EXIT_SUCCESS;
}

Next Actions

  1. Replace the pin definitions at the top with free pins (e.g., 17–21 or any unused ones reported by gpioinfo).
  2. Recompile and run with sudo.
  3. Connect an LED + resistor (or multimeter) to one of the output pins (e.g., CE or SCK) and observe during the 'r' command.

If the pins still do not toggle after changing to higher numbers, please provide the output of the diagnostic commands above (gpioinfo, gpioget, etc.). This will allow precise further diagnosis.

More Diagnosis Options If you Need It

The gpioinfo output reveals the root cause of the problem.

Your Raspberry Pi (likely a Pi Zero) has many GPIO lines already claimed or configured by the kernel at boot time. Lines 17 through 21 — the ones your program requests — are shown as "input" with no consumer, but more importantly, several low-numbered lines (especially GPIO 0–3) are marked as "output" with names like "ID_SDA", "ID_SCL", "GPIO2", etc. This indicates that the kernel or device tree has reserved or partially configured these pins for other functions (I²C, HAT ID, etc.).

When you request lines via the GPIO v2 API and ask the kernel to configure them as outputs, the kernel grants the request (no error on GPIO_V2_GET_LINE_IOCTL), but the underlying hardware configuration or pinmux prevents the lines from actually driving high/low. The subsequent GPIO_V2_LINE_SET_VALUES_IOCTL then fails with "Operation not permitted" because the kernel detects that the lines are not in a state where user-space can reliably control them as pure outputs.

Why Pins 17–21 Do Not Work

Even though gpioinfo shows them as unused inputs, the Pi's pinmux (controlled by the device tree) often keeps them in a safe/default state. The character device API respects these restrictions more strictly than the old sysfs interface.

Disable conflicting interfaces in /boot/config.txt and reboot. This frees the pins for user control.

Edit the config file:

sudo nano /boot/config.txt

Add or modify these lines near the bottom (uncomment or add them):

# Disable I2C (frees GPIO 0,1,2,3)
dtparam=i2c_arm=off

# Disable SPI (if enabled)
dtparam=spi=off

# Disable 1-Wire (if enabled)
dtoverlay=w1-gpio,gpio_pin=4   # comment out or remove if present
dtparam=w1_gpio=off

Save and reboot:

sudo reboot

After reboot, run gpioinfo again and check that lines 17–21 now appear as "unused" input with no other consumers.

Recompile and run your current program (with pins 17–21). The "Operation not permitted" errors should disappear, and the pins should now toggle.

Alternative: Try Different Free Pins

If the above does not fully resolve it, switch to a different set of pins that are less likely to be reserved. Change the defines at the top of your program to:

#define PIN_CE   22
#define PIN_CSN  23
#define PIN_SCK  24
#define PIN_MOSI 25
#define PIN_MISO 27

Recompile and test again after the reboot.

Updated Code Snippet (Minimal Fix for Mask Issue)

While the main issue is the pin configuration shown in gpioinfo, here is the corrected gpio_write function that avoids the mixed-direction mask error (use this in your existing v2 program):

// Write to ONE output pin only (never include MISO in the mask)
static void gpio_write(uint64_t mask, int value)
{
    struct gpio_v2_line_values vals = {0};
    vals.mask = mask;
    vals.bits = value ? mask : 0ULL;

    if (ioctl(line_fd, GPIO_V2_LINE_SET_VALUES_IOCTL, &vals) < 0) {
        perror("GPIO_V2_LINE_SET_VALUES_IOCTL");
    }
}

Please:

  • Make the changes to /boot/config.txt and reboot.
  • Run gpioinfo again after reboot and share the new output for lines 17–27.
  • Then run the program and tell me whether the "Operation not permitted" messages are gone and whether the pins now toggle (test with a multimeter or LED on e.g. pin 17/CE).

If you prefer, I can also provide a version that falls back to the older GPIO v1 API with separate handles, or even a simple memory-mapped version (faster but requires more care). Let me know the results after the reboot.

Linux Rocks Every Day