I2C and SPI Userspace Access
Talk to I2C and SPI peripherals straight from userspace with i2c-tools, spidev, and two original C programs – part of EmbeddedPathashala’s free Linux kernel development course.
I2C and SPI are the two buses you’ll run into on almost every embedded board – sensors, display controllers, flash chips, and GPIO expanders all tend to hang off one or the other. The good news is you don’t always need a kernel driver to talk to them. The kernel ships generic bus-access drivers – i2c-dev and spidev – that hand raw bus access to userspace, which is perfect for bring-up, scripting, and one-off diagnostics. This lecture in our free linux device drivers course covers both.
What You Will Learn
- How the I2C master-slave protocol and addressing scheme work
- Using i2c-tools (i2cdetect, i2cdump, i2cget, i2cset) for diagnostics
- Writing an original I2C register-read program using combined transactions
- How SPI’s four-wire full-duplex bus differs from I2C
- Writing an original SPI transfer program using spidev
- When to reach for userspace bus access vs writing a kernel driver
Prerequisites
You should be comfortable with C file I/O and ioctl() basics, and it helps to have followed the GPIO and sysfs lectures earlier in this chapter. A board with an I2C sensor or SPI flash chip is useful but not required – every example below is explained independently of specific hardware.
I2C: A Two-Wire Master-Slave Bus
I2C (Inter-Integrated Circuit) is a low-speed, two-wire bus – one clock line (SCL), one data line (SDA) – designed to reach peripherals that sit off the SoC package: display controllers, camera sensors, GPIO expanders, and small sensors. It’s a master-slave protocol; on an embedded SoC, one or more host controllers act as the master, and every peripheral on the bus is a slave identified by a 7-bit address assigned by its manufacturer (10-bit addressing exists but is far less common). Of the 128 possible 7-bit addresses, 16 are reserved for special purposes, leaving 112 usable in practice.
Standard mode runs at 100 KHz and fast mode at 400 KHz, with faster variants (Fast Mode Plus, High Speed) defined in the I2C specification for peripherals that support them. A transaction typically writes one byte to select a register on the peripheral, then reads or writes the following bytes as data for that register – this “register pointer then data” pattern is so common that most sensor datasheets describe their interface in exactly those terms.
Finding I2C Buses and Devices
Each I2C host controller on the SoC shows up as its own device node:
$ ls -l /dev/i2c-*
crw-rw---- 1 root i2c 89, 0 Jan 1 00:18 /dev/i2c-0
crw-rw---- 1 root i2c 89, 1 Jan 1 00:18 /dev/i2c-1
The i2c-tools package gives you four command-line utilities built on top of these nodes:
i2cdetect– lists I2C adapters and probes a bus for responding addressesi2cdump– dumps every register of a given I2C peripherali2cget– reads one value from an I2C slavei2cset– writes one value to an I2C slave
$ sudo i2cdetect -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- 5d --
...
Here a device answers at address 0x5d on bus 1 – we’ll use that address in the demo below. i2cdetect is destructive on some peripherals (it can trigger unintended writes), so always check your peripheral’s datasheet before probing it in production.
Writing an I2C Userspace Program
The classic approach uses SMBus-style helper calls, but those aren’t guaranteed to be available on every adapter, and they don’t support Linux’s repeated-start combined transactions cleanly. The more portable, modern approach uses the I2C_RDWR ioctl with an array of struct i2c_msg, which works on every adapter and lets you chain a register-select write and a data read into one atomic bus transaction with a repeated start – exactly what most sensors expect.
/* ep_i2c_read.c
* Reads a 16-bit register from an I2C peripheral using a combined
* write-then-read transaction (register pointer, then data).
* Usage: ep_i2c_read <bus> <addr-hex> <reg-hex>
* Example: ep_i2c_read 1 0x5d 0x10
*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
int main(int argc, char **argv)
{
if (argc != 4) {
fprintf(stderr, "usage: %s <bus> <addr-hex> <reg-hex>\n", argv[0]);
return 1;
}
char devpath[32];
snprintf(devpath, sizeof(devpath), "/dev/i2c-%s", argv[1]);
unsigned short addr = (unsigned short)strtol(argv[2], NULL, 16);
unsigned char reg = (unsigned char)strtol(argv[3], NULL, 16);
unsigned char rxbuf[2] = {0};
int fd = open(devpath, O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
struct i2c_msg msgs[2] = {
{ .addr = addr, .flags = 0, .len = 1, .buf = ® },
{ .addr = addr, .flags = I2C_M_RD, .len = 2, .buf = rxbuf },
};
struct i2c_rdwr_ioctl_data payload = { .msgs = msgs, .nmsgs = 2 };
if (ioctl(fd, I2C_RDWR, &payload) < 0) {
perror("I2C_RDWR");
close(fd);
return 1;
}
unsigned int value = (rxbuf[1] << 8) | rxbuf[0];
printf("register 0x%02x = 0x%04x\n", reg, value);
close(fd);
return 0;
}
$ gcc -o ep_i2c_read ep_i2c_read.c
$ sudo ./ep_i2c_read 1 0x5d 0x10
register 0x10 = 0x0203
The I2C_M_RD flag on the second message turns that transfer into a read, and because both messages travel in a single ioctl() call, the kernel issues a repeated start between them instead of releasing the bus – critical for peripherals that would otherwise hand control to another master mid-transaction.
SPI: A Faster, Full-Duplex Bus
SPI (Serial Peripheral Interface) trades I2C’s simplicity for speed – clock rates reach into the tens of MHz on typical embedded controllers. Where I2C shares one data line for both directions, SPI uses four wires: clock, MOSI (master-out), MISO (master-in), and a dedicated chip-select line per peripheral, which lets it run full duplex – data flows both directions on every clock edge. It’s commonly used for touchscreens, display controllers, and serial NOR/NAND flash, anywhere raw throughput matters more than wiring simplicity.
Like I2C, SPI is master-slave, with the SoC’s host controller as master and peripherals selected individually via their chip-select line. The kernel’s generic spidev driver, enabled with CONFIG_SPI_SPIDEV, exposes one device node per bus/chip-select pair once the corresponding device tree entry is in place:
$ ls -l /dev/spidev*
crw-rw---- 1 root root 153, 0 Jan 1 00:29 /dev/spidev1.0
spidev1.0 means SPI bus 1, chip select 0.
Writing an SPI Userspace Program
SPI transfers go through the SPI_IOC_MESSAGE ioctl family, built around struct spi_ioc_transfer. Here’s an original demo that reads the JEDEC ID from a SPI NOR flash chip – a near-universal 0x9F command that returns manufacturer and device ID bytes, useful as a quick “is the bus wired correctly” sanity check.
/* ep_spi_jedec_id.c
* Reads the 3-byte JEDEC ID from a SPI NOR flash chip.
* Usage: ep_spi_jedec_id <spidev-path>
* Example: ep_spi_jedec_id /dev/spidev1.0
*/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>
int main(int argc, char **argv)
{
if (argc != 2) {
fprintf(stderr, "usage: %s <spidev-path>\n", argv[0]);
return 1;
}
int fd = open(argv[1], O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
uint8_t mode = SPI_MODE_0;
uint8_t bits = 8;
uint32_t speed = 1000000; /* 1 MHz, conservative for bring-up */
ioctl(fd, SPI_IOC_WR_MODE, &mode);
ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
uint8_t tx[4] = {0x9F, 0x00, 0x00, 0x00}; /* JEDEC ID command + dummy bytes */
uint8_t rx[4] = {0};
struct spi_ioc_transfer xfer = {
.tx_buf = (unsigned long)tx,
.rx_buf = (unsigned long)rx,
.len = sizeof(tx),
.speed_hz = speed,
.bits_per_word = bits,
};
if (ioctl(fd, SPI_IOC_MESSAGE(1), &xfer) < 0) {
perror("SPI_IOC_MESSAGE");
close(fd);
return 1;
}
printf("manufacturer=0x%02x memory_type=0x%02x capacity=0x%02x\n",
rx[1], rx[2], rx[3]);
close(fd);
return 0;
}
$ gcc -o ep_spi_jedec_id ep_spi_jedec_id.c
$ sudo ./ep_spi_jedec_id /dev/spidev1.0
manufacturer=0xef memory_type=0x40 capacity=0x18
The first received byte is discarded because the flash chip hasn’t started responding yet while the command byte is still being clocked out – that’s exactly what full duplex operation looks like: transmit and receive happen simultaneously, byte for byte.
I2C vs SPI at a Glance
| Aspect | I2C | SPI |
|---|---|---|
| Wires | 2 (SCL, SDA) plus ground | 4 (SCLK, MOSI, MISO, CS) plus ground |
| Duplex | Half-duplex, shared data line | Full-duplex, dedicated lines |
| Typical speed | 100 KHz-400 KHz (up to a few MHz for HS variants) | Up to tens of MHz |
| Addressing | 7-bit address in the protocol itself | Dedicated chip-select line per device |
| Userspace node | /dev/i2c-N (one per controller) | /dev/spidevB.C (one per bus/chip-select pair) |
Common Mistakes
Missing device tree entries
Neither i2c-dev nor spidev nodes appear automatically – the peripheral (or the generic dev driver itself) must be described in the device tree and the corresponding kernel config option enabled.
Confusing the address with the read/write bit
I2C addresses in datasheets are sometimes given as 8-bit values that already include the R/W bit. Make sure you’re using the 7-bit form the kernel API expects, or reads silently target the wrong device.
Ignoring SPI mode and bit order
SPI has four clock polarity/phase modes (0-3) and can be MSB-first or LSB-first. Getting either wrong produces garbage data that looks like a wiring fault.
Best Practices
- Prefer the combined I2C_RDWR transaction over separate write-then-read calls whenever the peripheral needs a repeated start.
- Start SPI transfers at a conservative clock speed during bring-up, then increase once communication is verified.
- Double-check chip select polarity and SPI mode against the datasheet before assuming a wiring problem.
- Reach for a proper kernel driver once your userspace tool needs to react to interrupts or integrate with another kernel subsystem (input, IIO, hwmon) rather than growing an ad-hoc polling daemon.
Summary and Key Takeaways
Both i2c-dev and spidev exist precisely so you don’t have to write a kernel driver just to read a register during bring-up or scripting. I2C trades speed for simplicity and address-based addressing over two wires; SPI trades wiring complexity for full-duplex throughput. The I2C_RDWR and SPI_IOC_MESSAGE ioctls are the two building blocks you’ll reuse in almost every userspace bus program you write from here on.
Frequently Asked Questions
Do I need root privileges to access /dev/i2c-* or /dev/spidev*?
By default yes, unless a udev rule grants access to a specific group – the device nodes are typically root-owned.
Why does i2cdetect show UU for some addresses?
UU means a kernel driver has already claimed that address, so i2cdetect cannot safely probe it directly.
Can I use SMBus calls instead of I2C_RDWR?
Yes for simple single-register access, but I2C_RDWR is more portable across adapters and required for combined write-read transactions with a repeated start.
How do I know which SPI mode my peripheral needs?
Check the clock polarity (CPOL) and clock phase (CPHA) values in the datasheet’s timing diagram; together they map directly to SPI_MODE_0 through SPI_MODE_3.
Why is my /dev/spidev node missing even though spidev is enabled?
The kernel config option alone isn’t enough – the device tree must also describe a spidev-compatible node bound to that chip-select line.
What’s the maximum I2C or SPI transfer size?
I2C messages are commonly limited by the controller driver, often to 32-64 bytes per message; SPI transfers are typically only limited by available buffer size.
Should I write a kernel driver instead of using i2c-dev/spidev?
Only once you need interrupt handling, power management integration, or exposure through a standard subsystem like IIO or input – for simple polling and bring-up, userspace access is the right tool.
Keep Building Your Kernel Skills
This lecture is part of EmbeddedPathashala’s free Linux kernel development course, covering device drivers from first principles to real hardware.
Continue to Next Lecture Browse the Full Course
4 Comments