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!
Our Raspberry Pi Zero was not working the SCLK of the SPI0.1 So we looked into seeing if we could bypass the system SPI and just use a software based one for diagnostic purposes. Here is what Grok produced.
Please NOTE: This does not use the new ioctl / file system type GPIO since Linux Kernel 4.8. I discovered this the hard way. Check your 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
If you want the one that works with the later kernals

Here is a clean, professional implementation of software (bit-banged) SPI in C for the Raspberry Pi, using the specified GPIO pins in BCM numbering:
- CE (Chip Enable for nRF24L01+) → GPIO 0
- CSN (Chip Select, active low) → GPIO 1
- SCK (Serial Clock) → GPIO 2
- MOSI (Master Out Slave In) → GPIO 3
- MISO (Master In Slave Out) → GPIO 4
The code provides exactly two core functions as requested:
spi_write_byte(uint8_t byte)— writes one byte (used for commands and data)spi_read_byte(void)— reads one byte (returns the value on MISO while sending a dummy byte, usually 0xFF)
These low-level functions can be used directly by higher-level nRF24L01+ functions (e.g., nrf_read_reg, nrf_write_reg, etc.).
Complete Software SPI Implementation
Save this as soft_spi.c (or integrate into your existing file).
#include <stdint.h> // Grok 4 will always forget this line??
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
// GPIO pins (BCM numbering) as specified
#define PIN_CE 0
#define PIN_CSN 1
#define PIN_SCK 2
#define PIN_MOSI 3
#define PIN_MISO 4
// GPIO sysfs paths
static char gpio_export_path[] = "/sys/class/gpio/export";
static char gpio_direction_path[64];
static char gpio_value_path[64];
// ====================== GPIO Helpers ======================
static int gpio_export(int pin) {
int fd = open(gpio_export_path, O_WRONLY);
if (fd < 0) return -1;
char buf[8];
snprintf(buf, sizeof(buf), "%d", pin);
write(fd, buf, strlen(buf));
close(fd);
usleep(100000); // Allow time for export
return 0;
}
static int gpio_set_direction(int pin, int output) {
snprintf(gpio_direction_path, sizeof(gpio_direction_path),
"/sys/class/gpio/gpio%d/direction", pin);
int fd = open(gpio_direction_path, O_WRONLY);
if (fd < 0) return -1;
write(fd, output ? "out" : "in", output ? 3 : 2);
close(fd);
return 0;
}
static void gpio_write(int pin, int value) {
snprintf(gpio_value_path, sizeof(gpio_value_path),
"/sys/class/gpio/gpio%d/value", pin);
int fd = open(gpio_value_path, O_WRONLY);
if (fd >= 0) {
write(fd, value ? "1" : "0", 1);
close(fd);
}
}
static int gpio_read(int pin) {
snprintf(gpio_value_path, sizeof(gpio_value_path),
"/sys/class/gpio/gpio%d/value", pin);
int fd = open(gpio_value_path, O_RDONLY);
if (fd < 0) return 0;
char buf[2];
read(fd, buf, 1);
close(fd);
return (buf[0] == '1') ? 1 : 0;
}
// ====================== Software SPI Core ======================
static void soft_spi_init(void) {
// Export and configure all pins
gpio_export(PIN_CE);
gpio_export(PIN_CSN);
gpio_export(PIN_SCK);
gpio_export(PIN_MOSI);
gpio_export(PIN_MISO);
gpio_set_direction(PIN_CE, 1); // Output
gpio_set_direction(PIN_CSN, 1); // Output
gpio_set_direction(PIN_SCK, 1); // Output
gpio_set_direction(PIN_MOSI, 1); // Output
gpio_set_direction(PIN_MISO, 0); // Input
// Initial idle state (SPI Mode 0: CPOL=0, CPHA=0)
gpio_write(PIN_CSN, 1); // CSN inactive (high)
gpio_write(PIN_SCK, 0); // Clock idle low
gpio_write(PIN_CE, 0); // CE low by default
gpio_write(PIN_MOSI,0);
}
// Write one byte (MSB first)
void spi_write_byte(uint8_t byte) {
for (int i = 7; i >= 0; i--) {
gpio_write(PIN_MOSI, (byte >> i) & 1); // Set data bit
gpio_write(PIN_SCK, 1); // Rising edge
usleep(1); // Small delay for stability
gpio_write(PIN_SCK, 0); // Falling edge
usleep(1);
}
}
// Read one byte while sending dummy byte (usually 0xFF)
uint8_t spi_read_byte(void) {
uint8_t byte = 0;
for (int i = 7; i >= 0; i--) {
gpio_write(PIN_SCK, 1); // Rising edge
usleep(1);
if (gpio_read(PIN_MISO)) {
byte |= (1 << i);
}
gpio_write(PIN_SCK, 0); // Falling edge
usleep(1);
}
return byte;
}
// Optional convenience: transfer one byte (write + read simultaneously)
uint8_t spi_transfer_byte(uint8_t tx_byte) {
uint8_t rx_byte = 0;
for (int i = 7; i >= 0; i--) {
gpio_write(PIN_MOSI, (tx_byte >> i) & 1);
gpio_write(PIN_SCK, 1);
usleep(1);
if (gpio_read(PIN_MISO)) {
rx_byte |= (1 << i);
}
gpio_write(PIN_SCK, 0);
usleep(1);
}
return rx_byte;
}
Usage Example (Integration with nRF24L01+)
Add the following to your main program or nRF24L01+ functions:
// In main() or initialization
soft_spi_init();
// Example: Read STATUS register (0x07)
void example_read_status(void) {
gpio_write(PIN_CSN, 0); // Select device
spi_write_byte(0x07 | 0x00); // R_REGISTER | STATUS
uint8_t status = spi_read_byte(); // or spi_transfer_byte(0xFF)
gpio_write(PIN_CSN, 1); // Deselect
printf("STATUS register = 0x%02X\n", status);
}
Notes for Reliable Operation
- The small
usleep(1)provides basic timing. For higher speed you can remove or reduce the delays, but test stability with your logic analyzer. - This implementation uses SPI Mode 0 (clock idle low, data sampled on rising edge), which is correct for the nRF24L01+.
- CE remains a separate output (GPIO 0) and is not managed by the SPI functions — control it manually as before.
- For better performance in production, consider switching to the
libgpiodlibrary instead of sysfs (sysfs is slower and being deprecated).
Would you like me to provide the full updated nRF24L01+ register monitor program using these two new software SPI functions? Just confirm, and I will deliver the complete integrated code.
This software SPI layer gives you full control and bypasses any hardware SPI pin limitations you encountered earlier.
