Software Based SPI for Raspberry Pi Kernels > 4.8 API V1
We go over software defined SPI on a Raspberry Pi Kernel > 4.8!
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 -ac@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

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 binary10101010(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).
