Skip to content

This is a minimal, single-threaded SPI communication library for Linux (e.g., Raspberry Pi). It provides a thin wrapper around the Linux spidev interface with global 32-byte TX/RX buffers, builds to a static library, and returns error codes instead of exiting (V2.0+).

License

Notifications You must be signed in to change notification settings

chmelat/lib_spi

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SPI Communication Library

A lightweight and efficient C library for SPI (Serial Peripheral Interface) communication in Linux environments, primarily designed for single-board computers like Raspberry Pi.

License Platform

Table of Contents

Features

  • Simple API - Minimal learning curve with intuitive function names
  • Configurable SPI modes - Support for modes 0, 1, 2, and 3
  • Adjustable transfer speed - Set custom clock frequencies
  • Flexible bits-per-word - Configure data width (typically 8 bits)
  • Safety checks - Automatic validation of transfer sizes to prevent buffer overflows
  • Error handling - Return codes instead of aborts (V2.0+)
  • Debug support - Multiple verbosity levels for troubleshooting
  • Zero dependencies - Only requires Linux kernel with spidev support

Requirements

System Requirements

  • Linux kernel with spidev driver support
  • Read/write permissions for /dev/spidevX.Y devices
  • C compiler (Clang, GCC, or compatible)

Development Tools

# Debian/Ubuntu
sudo apt-get install build-essential

# Fedora/RHEL
sudo dnf install gcc make

# Arch Linux
sudo pacman -S base-devel

Hardware Setup

Enable SPI interface on Raspberry Pi:

sudo raspi-config
# Navigate to: Interface Options -> SPI -> Enable

Verify SPI devices are available:

ls -l /dev/spidev*
# Expected output: /dev/spidev0.0, /dev/spidev0.1, etc.

Installation

Method 1: Using Makefile (Recommended)

# Build the static library
make

# Install to user directory ($HOME/lib and $HOME/include)
make install

# Or install system-wide (requires sudo)
sudo make PREFIX=/usr/local install

# Uninstall
make uninstall  # or: sudo make PREFIX=/usr/local uninstall

# Clean build artifacts
make clean

Installation paths:

  • Default: $HOME/lib/libspi.a and $HOME/include/spi_base.h
  • System-wide: /usr/local/lib/libspi.a and /usr/local/include/spi_base.h

Method 2: Use GCC Instead of Clang

make CC=gcc
make CC=gcc install

Method 3: Direct Integration

Copy spi_base.c and spi_base.h into your project and compile directly:

gcc -o my_program my_program.c spi_base.c -Wall -O2

Method 4: Link Against Installed Library

After running make install:

gcc -o my_program my_program.c -lspi -L$HOME/lib -I$HOME/include

Quick Start

Basic Example

#include "spi_base.h"
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(void) {
    int fd;
    int ret;

    // Open SPI device
    fd = open("/dev/spidev0.0", O_RDWR);
    if (fd < 0) {
        perror("Failed to open SPI device");
        return 1;
    }

    // Configure SPI parameters
    ret = set_spi_mode(fd, 3);        // SPI Mode 3
    if (ret < 0) {
        fprintf(stderr, "Failed to set SPI mode\n");
        close(fd);
        return 1;
    }

    ret = set_spi_bpw(fd, 8);          // 8 bits per word
    if (ret < 0) {
        fprintf(stderr, "Failed to set bits per word\n");
        close(fd);
        return 1;
    }

    ret = set_spi_speed(fd, 500000);   // 500 kHz
    if (ret < 0) {
        fprintf(stderr, "Failed to set speed\n");
        close(fd);
        return 1;
    }

    // Prepare data to transmit
    tx[0] = 0x01;
    tx[1] = 0x80;
    tx[2] = 0x00;

    // Perform SPI transfer
    ret = spi_trans(fd, 3);
    if (ret < 0) {
        fprintf(stderr, "SPI transfer failed\n");
        close(fd);
        return 1;
    }

    // Read received data
    printf("Received: 0x%02X 0x%02X 0x%02X\n", rx[0], rx[1], rx[2]);

    close(fd);
    return 0;
}

Compile and run:

gcc -o quick_start quick_start.c spi_base.c
sudo ./quick_start  # May require sudo for /dev/spidev access

API Reference

Configuration Functions

set_spi_mode()

int set_spi_mode(int fd, uint32_t mode);

Set SPI communication mode.

Parameters:

  • fd - File descriptor of the SPI device
  • mode - SPI mode (0-3)

Returns: 0 on success, negative error code on failure

SPI Modes:

Mode CPOL CPHA Description
0 0 0 Clock idle low, data captured on rising edge
1 0 1 Clock idle low, data captured on falling edge
2 1 0 Clock idle high, data captured on falling edge
3 1 1 Clock idle high, data captured on rising edge

set_spi_bpw()

int set_spi_bpw(int fd, uint8_t bpw);

Set bits per word (data width).

Parameters:

  • fd - File descriptor of the SPI device
  • bpw - Bits per word (typically 8)

Returns: 0 on success, negative error code on failure


set_spi_speed()

int set_spi_speed(int fd, uint32_t speed);

Set SPI clock frequency in Hz.

Parameters:

  • fd - File descriptor of the SPI device
  • speed - Speed in Hz (e.g., 500000 for 500 kHz)

Returns: 0 on success, negative error code on failure

Common speeds:

  • 125000 (125 kHz) - Very slow, for long cables
  • 500000 (500 kHz) - Default, reliable
  • 1000000 (1 MHz) - Standard speed
  • 10000000 (10 MHz) - High speed (check device specs)

set_spi_delay()

void set_spi_delay(uint16_t usecs);

Set delay between transfers in microseconds.

Parameters:

  • usecs - Delay in microseconds

Returns: Nothing (cannot fail)


Data Transfer Functions

spi_trans()

int spi_trans(int fd, uint32_t len);

Perform full-duplex SPI transfer using global tx[] and rx[] buffers.

Parameters:

  • fd - File descriptor of the SPI device
  • len - Number of bytes to transfer (1-32)

Returns: 0 on success, -EINVAL if length invalid, -EIO on transfer failure

Usage:

tx[0] = 0xAA;
tx[1] = 0xBB;
spi_trans(fd, 2);
printf("Received: 0x%02X 0x%02X\n", rx[0], rx[1]);

spi_write()

int spi_write(int fd, const void *buf, size_t n);

Write data to SPI device (transmit only).

Parameters:

  • fd - File descriptor of the SPI device
  • buf - Pointer to data buffer
  • n - Number of bytes to write (1-32)

Returns: 0 on success, negative error code on failure

Usage:

uint8_t data[] = {0x10, 0x20, 0x30};
spi_write(fd, data, 3);

spi_read()

int spi_read(int fd, void *buf, size_t n);

Read data from SPI device (receive only).

Parameters:

  • fd - File descriptor of the SPI device
  • buf - Pointer to receive buffer
  • n - Number of bytes to read (1-32)

Returns: 0 on success, negative error code on failure

Usage:

uint8_t data[4];
spi_read(fd, data, 4);

Utility Functions

pabort()

void pabort(const char *s);

Print error message and exit the program. Use for critical errors only.

Parameters:

  • s - Error message string

Usage:

if (fd < 0)
    pabort("Cannot open SPI device");

print_binary()

void print_binary(unsigned int n, int bits);

Print integer value in binary format (LSB first).

Parameters:

  • n - Value to print
  • bits - Number of bits to display

Usage:

print_binary(0xA5, 8);  // Prints: 10100101

spi_msg()

void spi_msg(const char *s, int len, uint8_t *buf);

Print buffer contents in binary format with label.

Parameters:

  • s - Label string
  • len - Buffer length
  • buf - Pointer to data buffer

Global Variables

extern uint8_t tx[TR_BUF_LEN];              // Transmit buffer (32 bytes)
extern uint8_t rx[TR_BUF_LEN];              // Receive buffer (32 bytes)
extern struct spi_ioc_transfer tr;          // Transfer configuration
extern int verbose_spi;                     // Debug verbosity level (0-2)

Constants:

  • TR_BUF_LEN - Maximum buffer size (32 bytes)
  • SPEED - Default SPI speed (500000 Hz)

Error Handling

V2.0 Error Handling (Current)

All functions return error codes instead of calling exit():

int ret = set_spi_mode(fd, 3);
if (ret < 0) {
    fprintf(stderr, "Error: %d\n", ret);
    // Handle error gracefully
}

Common error codes:

  • 0 - Success
  • -EINVAL - Invalid parameter (e.g., length > 32 bytes)
  • -EIO - I/O error during transfer
  • -errno - Other system errors (see errno.h)

Retry Pattern

int retry_transfer(int fd, int len, int max_retries) {
    int ret;
    for (int i = 0; i < max_retries; i++) {
        ret = spi_trans(fd, len);
        if (ret == 0)
            return 0;  // Success

        fprintf(stderr, "Attempt %d failed, retrying...\n", i + 1);
        usleep(10000);  // Wait 10ms
    }
    return ret;  // All retries failed
}

Using pabort() for Critical Errors

fd = open("/dev/spidev0.0", O_RDWR);
if (fd < 0)
    pabort("Cannot open SPI device - is SPI enabled?");

Advanced Usage

Device Fallback Strategy

int open_any_spi_device(void) {
    const char *devices[] = {
        "/dev/spidev0.0",
        "/dev/spidev0.1",
        "/dev/spidev1.0"
    };

    for (int i = 0; i < 3; i++) {
        int fd = open(devices[i], O_RDWR);
        if (fd >= 0) {
            printf("Opened: %s\n", devices[i]);

            // Test configuration
            if (set_spi_mode(fd, 3) == 0 &&
                set_spi_bpw(fd, 8) == 0 &&
                set_spi_speed(fd, 500000) == 0) {
                return fd;  // Success
            }
            close(fd);
        }
    }
    return -1;  // No device available
}

Mode Fallback

// Try mode 3, fallback to mode 1
int ret = set_spi_mode(fd, 3);
if (ret < 0) {
    fprintf(stderr, "Mode 3 failed, trying mode 1...\n");
    ret = set_spi_mode(fd, 1);
    if (ret < 0)
        pabort("Both modes failed");
}

Communicating with ADC (MCP3008 example)

uint16_t read_adc_channel(int fd, uint8_t channel) {
    if (channel > 7)
        return 0;

    // MCP3008 command format
    tx[0] = 0x01;                           // Start bit
    tx[1] = (0x80 | (channel << 4));        // Single-ended mode + channel
    tx[2] = 0x00;

    if (spi_trans(fd, 3) < 0)
        return 0;

    // Extract 10-bit result
    uint16_t result = ((rx[1] & 0x03) << 8) | rx[2];
    return result;
}

Handling Large Data Sets

For data larger than 32 bytes, split into chunks:

int transfer_large_data(int fd, uint8_t *data, size_t total_len) {
    size_t offset = 0;

    while (offset < total_len) {
        size_t chunk_size = (total_len - offset > 32) ? 32 : (total_len - offset);

        // Copy chunk to tx buffer
        memcpy(tx, data + offset, chunk_size);

        // Transfer chunk
        int ret = spi_trans(fd, chunk_size);
        if (ret < 0)
            return ret;

        // Copy received data if needed
        memcpy(data + offset, rx, chunk_size);

        offset += chunk_size;
    }
    return 0;
}

Hardware Considerations

Raspberry Pi Specifics

SPI Buses:

  • SPI0 (/dev/spidev0.0, /dev/spidev0.1) - Recommended, all modes work
  • SPI1 (/dev/spidev1.0, /dev/spidev1.1) - Modes 1 and 3 may not work reliably

Pin Mapping (SPI0):

Signal GPIO Physical Pin
MOSI 10 19
MISO 9 21
SCLK 11 23
CE0 8 24
CE1 7 26

Common Devices and Modes

Device Type Recommended Mode Typical Speed
ADC (MCP3008, MCP3204) Mode 0 or 3 1-2 MHz
DAC (MCP4921) Mode 0 10-20 MHz
SD Card Mode 0 400 kHz - 25 MHz
Flash Memory Mode 0 or 3 1-50 MHz
RFID (RC522) Mode 0 1-10 MHz

Wiring Best Practices

  1. Keep wires short - Reduces noise and allows higher speeds
  2. Use ground plane - Connect all grounds together
  3. Add pull-up resistors - 10kΩ on MISO if device doesn't drive it
  4. Level shifting - Use for 5V devices with 3.3V Raspberry Pi
  5. Decoupling capacitors - 100nF close to device power pins

Debugging

Enable Debug Output

verbose_spi = 2;  // Maximum verbosity

Verbosity levels:

  • 0 - Silent (default, production use)
  • 1 - Configuration info and errors
  • 2 - Detailed transfer data in binary format

Example Debug Output (verbose_spi = 2)

SPI mode: 3
SPI bits per word: 8
SPI speed [Hz]: 500000

Send data:
0. 10000000
1. 11000000
2. 00000000

Receive data:
0. 00000000
1. 00000011
2. 11111111

Common Issues and Solutions

Problem: open() returns "Permission denied"

# Solution: Add user to spi group
sudo usermod -a -G spi $USER
# Or run with sudo (not recommended for development)

Problem: set_spi_mode() returns -1

# Solution: Check if SPI is enabled
ls /dev/spidev*
# If no devices, enable SPI in raspi-config

Problem: No data received (rx[] all zeros)

  • Check wiring (MISO connection)
  • Verify device is powered
  • Ensure correct SPI mode
  • Try lower speed

Problem: Garbage data received

  • Check clock polarity/phase (try different modes)
  • Reduce transfer speed
  • Check for noise/interference

Examples

Build and Run Examples

# Build example program
make example

# Run (may need sudo for device access)
sudo ./spi_example

# Clean
make clean-example

Example 1: Basic Communication

See Quick Start section above.

Example 2: Reading Temperature Sensor (MAX6675)

float read_max6675_temperature(int fd) {
    // Read 16 bits from MAX6675
    if (spi_trans(fd, 2) < 0)
        return -1.0;

    // Combine bytes (MSB first)
    uint16_t raw = (rx[0] << 8) | rx[1];

    // Check for errors
    if (raw & 0x04)
        return -1.0;  // Thermocouple not connected

    // Extract temperature (bits 15-3, resolution 0.25°C)
    raw >>= 3;
    return raw * 0.25;
}

Example 3: Robust Initialization

int init_spi_safe(const char *device, uint32_t speed) {
    int fd = open(device, O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }

    // Try mode 3 first (common for ADCs)
    if (set_spi_mode(fd, 3) < 0) {
        fprintf(stderr, "Mode 3 failed, trying mode 0...\n");
        if (set_spi_mode(fd, 0) < 0) {
            close(fd);
            return -1;
        }
    }

    if (set_spi_bpw(fd, 8) < 0 || set_spi_speed(fd, speed) < 0) {
        close(fd);
        return -1;
    }

    return fd;
}

Limitations

  1. Buffer size - Fixed 32-byte limit per transfer (defined by TR_BUF_LEN)
  2. Thread safety - Global tx[]/rx[] buffers are not thread-safe
  3. Single-threaded design - Not suitable for concurrent SPI operations
  4. Platform - Linux-only (requires spidev kernel driver)
  5. Hardware quirks - Some mode combinations may not work on certain SPI buses (Raspberry Pi SPI1)

Workarounds

For larger transfers:

// Split into 32-byte chunks (see Advanced Usage section)

For multi-threading:

// Use mutex to protect global buffers
pthread_mutex_t spi_mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock(&spi_mutex);
tx[0] = 0xAA;
spi_trans(fd, 1);
result = rx[0];
pthread_mutex_unlock(&spi_mutex);

For thread-safe design:

// Future enhancement: Add reentrant functions
// int spi_trans_r(int fd, uint8_t *tx_buf, uint8_t *rx_buf, uint32_t len);

Version History

V2.0 - 2025-10-01

  • Breaking change: Functions now return error codes instead of calling exit()
  • Improved error handling and validation
  • Better support for production code

V1.2 - 2023-04-22

  • Enhanced security with buffer overflow checks
  • Improved error diagnostics

V1.1 - 2021-09-12

  • Added spi_write() and spi_read() functions
  • Debug output improvements

V1.0 - 2021-08-20

  • Initial release
  • Basic SPI communication functionality

Contributing

Contributions are welcome! Areas for improvement:

  1. Thread-safe API - Add reentrant functions with explicit buffers
  2. Dynamic buffers - Support for transfers > 32 bytes
  3. Context-based design - Eliminate global state
  4. More examples - Common sensors and devices
  5. Unit tests - Automated testing framework

Please submit issues and pull requests via the project repository.

License

This library is provided as-is for educational and commercial use. Check the LICENSE file for details.

Support

For issues, questions, or contributions:

  • Create an issue in the project repository
  • Check existing documentation and examples
  • Review the example.c file for usage patterns

Author: See git history Last Updated: 2025-10-01 Version: 2.0

About

This is a minimal, single-threaded SPI communication library for Linux (e.g., Raspberry Pi). It provides a thin wrapper around the Linux spidev interface with global 32-byte TX/RX buffers, builds to a static library, and returns error codes instead of exiting (V2.0+).

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published