Skip to content

Serial Bus Interfaces: I2C, SPI, I2S

This reference covers the three serial buses used throughout the course — their electrical characteristics, protocol details, Linux kernel subsystems, Device Tree configuration, and common debugging techniques.


1. At a Glance

I2C SPI I2S
Full name Inter-Integrated Circuit Serial Peripheral Interface Inter-IC Sound
Signals SDA, SCL (2 wires) MOSI, MISO, SCLK, CS (4+ wires) BCLK, LRCLK, DOUT (3+ wires)
Direction Half-duplex Full-duplex Simplex (per data line)
Speed 100 kHz – 3.4 MHz 1 MHz – 100+ MHz Determined by sample rate
Addressing 7-bit address on bus Chip select per device Left/Right channel clock
Multi-device Yes (shared bus, addresses) Yes (one CS per device) Yes (TDM slots)
Typical use Sensors, EEPROMs, RTCs Displays, flash, ADCs, IMUs Microphones, DACs, codecs
Pull-ups needed Yes (SDA + SCL) No No
Course tutorials MCP9808 Driver BMI160 SPI, SPI Display I2S Audio Viz

When to Choose Which

  • I2C when you have multiple slow sensors and want minimal wiring (2 wires for all devices)
  • SPI when you need speed (displays, IMUs at high sample rates, flash memory)
  • I2S when you need audio (microphones, DACs, audio codecs)

2. I2C — Inter-Integrated Circuit

2.1 Electrical Layer

I2C uses two open-drain lines with external pull-up resistors:

VCC (3.3V)
 │        │
 ├──┤4.7kΩ├──┬──── SDA (data)
 │        │  │
 ├──┤4.7kΩ├──┼──── SCL (clock)
 │           │
 │    ┌──────┴──────┐    ┌──────────────┐
 │    │ Master (Pi) │    │ Slave (sensor)│
 │    │ SDA  SCL    │    │ SDA  SCL     │
 │    └─────────────┘    └──────────────┘
GND ─────────────────────────────────────

Open-drain means devices can only pull the line LOW. The pull-up resistor returns it to HIGH. This allows multi-master arbitration — if two devices drive simultaneously, both see the combined result.

Pull-up resistor sizing: Too high → slow rise time (fails at high speed). Too low → excessive current when driving low. Rule of thumb:

Speed Capacitance Resistor
100 kHz (standard) < 400 pF 4.7 kΩ
400 kHz (fast) < 400 pF 2.2 kΩ
1 MHz (fast-plus) < 550 pF 1 kΩ

The Raspberry Pi has built-in 1.8 kΩ pull-ups on the I2C pins (GPIO 2/3). For most sensors, no external pull-ups are needed.

2.2 Protocol

Every I2C transaction follows this pattern:

START  7-bit addr  R/W  ACK  Data byte  ACK  Data byte  ACK  STOP
  │         │       │    │       │       │       │       │     │
  ▼         ▼       ▼    ▼       ▼       ▼       ▼       ▼     ▼
SDA: ──╲  [A6..A0] [0]  [0]  [D7..D0]  [0]  [D7..D0]  [0]  ╱──
SCL: ───╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╲───
       S                                                      P
  • START (S): SDA falls while SCL is high
  • Address: 7 bits identifying the slave (e.g., MCP9808 = 0x18)
  • R/W bit: 0 = write, 1 = read
  • ACK: Slave pulls SDA low to acknowledge (NACK = no response = device not present)
  • Data: 8 bits per byte, MSB first
  • STOP (P): SDA rises while SCL is high

Register read (common pattern for sensors):

1. Write: START → addr+W → register_addr → STOP
2. Read:  START → addr+R → data_byte(s)  → NACK → STOP

This is called a "repeated start" — the master doesn't release the bus between write and read.

2.3 SMBus vs I2C

SMBus (System Management Bus) is a stricter subset of I2C used by most Linux sensor drivers:

I2C SMBus
Timeout None (can hang forever) 35 ms (bus released on timeout)
Max speed 3.4 MHz 100 kHz
Address range 0x08–0x77 0x08–0x77 (same)
Defined transactions Arbitrary Block read/write, word read/write, byte read/write
Linux API i2c_transfer() (raw) i2c_smbus_read_word_data() (structured)

Most sensor drivers use the SMBus API because it's simpler and more portable:

/* Read 16-bit temperature from MCP9808 register 0x05 */
int raw = i2c_smbus_read_word_swapped(client, 0x05);
float temp = (raw & 0x0FFF) / 16.0f;
if (raw & 0x1000) temp -= 256.0f;

2.4 Linux I2C Subsystem

User space:
  /dev/i2c-N          ← raw I2C access (i2c-tools, Python smbus)

Kernel:
  ┌─────────────────────────────────────────────┐
  │ I2C core (drivers/i2c/i2c-core.c)          │
  │  ├── i2c_adapter  (bus controller)           │
  │  ├── i2c_client   (device on the bus)        │
  │  └── i2c_driver   (device driver)            │
  ├─────────────────────────────────────────────┤
  │ Adapter drivers (per SoC):                   │
  │  bcm2835-i2c (Pi), i2c-designware (Intel)   │
  ├─────────────────────────────────────────────┤
  │ Device drivers (per chip):                   │
  │  mcp9808, bmp280, ssd1306, ...              │
  └─────────────────────────────────────────────┘

Hardware:
  BCM2835 BSC (Broadcom Serial Controller)
  Registers at 0x7E804000 (I2C1), 0x7E805000 (I2C0)

2.5 Device Tree for I2C

Enable I2C bus:

/* In config.txt (Pi-specific shorthand): */
dtparam=i2c_arm=on

/* Equivalent DT overlay: enables i2c1 on GPIO 2 (SDA) + GPIO 3 (SCL) */

Add a device:

/dts-v1/;
/plugin/;

/ {
    compatible = "brcm,bcm2835";

    fragment@0 {
        target = <&i2c1>;
        __overlay__ {
            #address-cells = <1>;
            #size-cells = <0>;
            status = "okay";

            mcp9808@18 {
                compatible = "microchip,mcp9808";
                reg = <0x18>;           /* 7-bit I2C address */
                /* Optional: interrupt pin */
                /* interrupt-parent = <&gpio>; */
                /* interrupts = <4 2>;  GPIO4, falling edge */
            };
        };
    };
};

Key properties: - compatible — matches the kernel driver's of_match_table - reg — the 7-bit I2C slave address (find with i2cdetect -y 1) - status = "okay" — enables this node (overrides "disabled" from base DT)

Compile and load:

dtc -@ -I dts -O dtb -o mcp9808.dtbo mcp9808-overlay.dts
sudo cp mcp9808.dtbo /boot/overlays/
# Add to config.txt: dtoverlay=mcp9808

2.6 Debugging I2C

# Scan bus for devices
i2cdetect -y 1
#      0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
# 00:          -- -- -- -- -- -- -- -- -- -- -- -- --
# 10: -- -- -- -- -- -- -- -- 18 -- -- -- -- -- -- --
#                               ↑ MCP9808 at 0x18

# Read register 0x05 from device 0x18
i2cget -y 1 0x18 0x05 w

# Write 0x00 to register 0x01
i2cset -y 1 0x18 0x01 0x00

# Dump all registers
i2cdump -y 1 0x18

Common problems:

Symptom Cause Fix
i2cdetect shows no devices Bus not enabled, wrong bus number Check dtparam=i2c_arm=on, use -y 1 (not 0)
Address shows UU Kernel driver already bound Device is working — access via driver, not i2c-tools
Address changes randomly Floating address pins Tie A0/A1/A2 to VCC or GND
Read returns 0xFF Pull-ups missing or too high Add 4.7kΩ pull-ups (or rely on Pi's built-in)
Intermittent failures Clock stretching + Pi bug Pi 1-3 have a hardware I2C clock stretching bug; use dtparam=i2c_arm_baudrate=50000 to slow down

3. SPI — Serial Peripheral Interface

3.1 Electrical Layer

SPI uses 4 signals (full-duplex, one CS per slave):

         ┌──────────┐          ┌───────────────┐
         │  Master   │          │  Slave 0      │
         │  (Pi)     │          │  (BMI160 IMU) │
         │          MOSI ──────▶ DIN            │
         │          MISO ◀────── DOUT           │
         │          SCLK ──────▶ CLK            │
         │          CE0  ──────▶ CS (active low)│
         └──────────┘          └───────────────┘
                    │          ┌───────────────┐
                    │          │  Slave 1      │
                    │          │  (SPI display) │
                    ├── MOSI ─▶ DIN            │
                    ├── MISO ◀─ (not connected)│
                    ├── SCLK ─▶ CLK            │
                    └── CE1  ─▶ CS (active low)│
                               └───────────────┘
  • MOSI (Master Out Slave In): data from master to slave
  • MISO (Master In Slave Out): data from slave to master
  • SCLK: clock generated by master
  • CS/CE (Chip Select/Enable): active LOW, one per slave

No pull-ups needed — SPI uses push-pull drivers (not open-drain).

3.2 Clock Modes (CPOL/CPHA)

SPI has 4 clock modes defined by polarity (CPOL) and phase (CPHA):

Mode 0 (CPOL=0, CPHA=0) — most common:
SCLK: ──┐ ┌─┐ ┌─┐ ┌─┐ ┌──
         └─┘ └─┘ └─┘ └─┘
DATA: ──X───X───X───X───X──
        ▲   ▲   ▲   ▲
        Sample on rising edge

Mode 1 (CPOL=0, CPHA=1):
SCLK: ──┐ ┌─┐ ┌─┐ ┌─┐ ┌──
         └─┘ └─┘ └─┘ └─┘
DATA: ────X───X───X───X────
          ▲   ▲   ▲   ▲
          Sample on falling edge

Mode 2 (CPOL=1, CPHA=0):
SCLK: ┌─┐ ┌─┐ ┌─┐ ┌─┐
       │ └─┘ └─┘ └─┘ └─┘
DATA: ──X───X───X───X───X──
        ▲   ▲   ▲   ▲
        Sample on falling edge

Mode 3 (CPOL=1, CPHA=1):
SCLK: ┌─┐ ┌─┐ ┌─┐ ┌─┐
       │ └─┘ └─┘ └─┘ └─┘
DATA: ────X───X───X───X────
          ▲   ▲   ▲   ▲
          Sample on rising edge

The device datasheet tells you which mode to use. BMI160 uses Mode 0 or 3. Most SPI flash and displays use Mode 0.

Warning

Wrong clock mode = garbage data. If reads return 0xFF or random values, check CPOL/CPHA first. The Pi defaults to Mode 0.

3.3 SPI Register Access Pattern

Most SPI sensors use this convention:

Read register 0x40:
  Master sends: [0x80 | 0x40] [0x00]     ← bit 7 = read flag
  Slave returns: [----]       [data]     ← first byte is dummy

Write register 0x40 = 0x05:
  Master sends: [0x00 | 0x40] [0x05]     ← bit 7 = 0 (write)
  Slave returns: [----]       [----]     ← ignored

In Linux:

/* Read register */
uint8_t tx[2] = { 0x80 | reg, 0x00 };
uint8_t rx[2] = { 0 };
struct spi_transfer xfer = {
    .tx_buf = tx, .rx_buf = rx, .len = 2
};
spi_sync_transfer(spi, &xfer, 1);
return rx[1];  /* data is in second byte */

3.4 Linux SPI Subsystem

User space:
  /dev/spidevN.M        ← raw SPI access (spidev driver)

Kernel:
  ┌─────────────────────────────────────────────┐
  │ SPI core (drivers/spi/spi.c)                │
  │  ├── spi_controller  (bus controller)        │
  │  ├── spi_device      (device on the bus)     │
  │  └── spi_driver      (device driver)         │
  ├─────────────────────────────────────────────┤
  │ Controller drivers (per SoC):                │
  │  spi-bcm2835 (Pi), spi-sun6i (Allwinner)    │
  ├─────────────────────────────────────────────┤
  │ Device drivers (per chip):                   │
  │  bmi160_spi, ili9341 (display), spidev      │
  └─────────────────────────────────────────────┘

3.5 Device Tree for SPI

/dts-v1/;
/plugin/;

/ {
    compatible = "brcm,bcm2835";

    fragment@0 {
        target = <&spi0>;
        __overlay__ {
            #address-cells = <1>;
            #size-cells = <0>;
            status = "okay";

            bmi160@0 {
                compatible = "bosch,bmi160";
                reg = <0>;                  /* CS0 (CE0) */
                spi-max-frequency = <10000000>;  /* 10 MHz */
                spi-cpol;                   /* CPOL=1 (Mode 2 or 3) */
                spi-cpha;                   /* CPHA=1 (Mode 3) */
                /* interrupt-parent = <&gpio>; */
                /* interrupts = <25 1>;  GPIO25, rising edge */
            };
        };
    };
};

Key properties: - reg — CS line number (0 = CE0, 1 = CE1 on Pi) - spi-max-frequency — maximum clock in Hz (device-specific) - spi-cpol / spi-cpha — clock polarity/phase (omit both for Mode 0)

3.6 SPI Speed and Throughput

The Pi's SPI controller can run up to ~125 MHz, but practical limits:

Device Typical max Notes
BMI160 IMU 10 MHz Datasheet limit
SSD1351 OLED 20 MHz Display refresh ~30 fps
ILI9341 TFT 32 MHz 320×240 @ 30 fps needs ~36 Mbps
SPI flash (W25Q) 80 MHz Bulk transfers, DMA helps

Throughput calculation:

Raw: 10 MHz clock × 1 bit/clock = 10 Mbps = 1.25 MB/s
Overhead: ~20% (CS setup, inter-byte gaps, kernel overhead)
Effective: ~1 MB/s at 10 MHz

For display refresh at 320×240×16bpp×30fps = 36.9 Mbps → need ≥ 40 MHz SPI clock. See SPI DMA Optimization for achieving this.


4. I2S — Inter-IC Sound

4.1 Electrical Layer

I2S is designed specifically for digital audio between chips:

         ┌──────────┐          ┌───────────────┐
         │  Master   │          │  INMP441 Mic  │
         │  (Pi)     │          │               │
         │          BCLK ──────▶ SCK (bit clk)  │
         │          LRCLK ─────▶ WS (word sel)  │
         │          DIN  ◀────── SD (data out)  │
         └──────────┘          │  L/R  ← GND=L │
                               └───────────────┘
  • BCLK (Bit Clock): clocks each data bit, generated by master
  • LRCLK (Left/Right Clock, also called WS — Word Select): toggles between left and right channel. Frequency = sample rate
  • DOUT/SD (Serial Data): audio samples, MSB first

4.2 Frame Format

The standard I2S frame for 24-bit audio at 48 kHz:

LRCLK: ────────┐                              ┌──────────
               │         LEFT CHANNEL          │         RIGHT CHANNEL
               └──────────────────────────────┘
BCLK:  ─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─ ... ─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─
         └─┘ └─┘ └─┘ └─┘ └─┘       └─┘ └─┘ └─┘ └─┘ └─┘

DOUT:  ──[B23][B22][B21]...[B0][0][0][0][0][B23][B22]...
          ▲                              ▲
          MSB first                      Right channel starts
          1 BCLK delay after             on LRCLK rising edge
          LRCLK transition

Standard I2S has a 1-bit delay: data starts one BCLK after the LRCLK transition. This distinguishes it from left-justified format (no delay) and right-justified format (data aligned to end of slot).

4.3 Clock Relationships

The three clocks are mathematically related:

LRCLK = sample_rate                      (e.g., 48000 Hz)
BCLK  = sample_rate × bits_per_slot × 2  (e.g., 48000 × 32 × 2 = 3.072 MHz)
MCLK  = sample_rate × 256                (e.g., 48000 × 256 = 12.288 MHz)
        (optional master clock, some codecs need it)
Sample rate Bits/slot BCLK MCLK (×256)
8 kHz 32 512 kHz 2.048 MHz
16 kHz 32 1.024 MHz 4.096 MHz
44.1 kHz 32 2.822 MHz 11.289 MHz
48 kHz 32 3.072 MHz 12.288 MHz
96 kHz 32 6.144 MHz 24.576 MHz
Why 32-Bit Slots for 24-Bit Audio?

The INMP441 microphone outputs 24-bit samples in a 32-bit slot (the lower 8 bits are zero). This is standard — I2S slots are always a power of 2 (16 or 32 bits). The 24-bit data is MSB-aligned within the 32-bit word.

In ALSA, this appears as S32_LE (signed 32-bit little-endian) format. The driver reads 32 bits per sample, and the application divides by 2^31 to normalize to float:

float sample = (float)raw_s32 / 2147483648.0f;

4.4 Raspberry Pi I2S Configuration

The Pi's BCM2835/2711 has one I2S interface (active PCM pins):

Signal Pi GPIO Pin # Function
BCLK GPIO 18 12 PCM_CLK
LRCLK GPIO 19 35 PCM_FS
DOUT (mic → Pi) GPIO 20 38 PCM_DIN
DIN (Pi → DAC) GPIO 21 40 PCM_DOUT

Device Tree overlay for INMP441:

The simplest approach uses the generic simple-audio-card framework:

# In /boot/config.txt:
dtoverlay=i2s-mems-mic

This overlay (provided by the course repo or Raspberry Pi OS) does:

  1. Enables the bcm2835-i2s controller
  2. Creates an ALSA sound card with the snd-simple-card driver
  3. Configures the codec as a "dummy" (the mic is too simple for a real codec driver)

If the standard overlay isn't available, create a custom one:

/dts-v1/;
/plugin/;

/ {
    compatible = "brcm,bcm2835";

    fragment@0 {
        target = <&i2s_clk_producer>;
        __overlay__ {
            status = "okay";
        };
    };

    fragment@1 {
        target-path = "/";
        __overlay__ {
            mic_codec: simple-codec {
                compatible = "invensense,inmp441", "linux,spdif-dit";
                #sound-dai-cells = <0>;
                status = "okay";
            };

            sound {
                compatible = "simple-audio-card";
                simple-audio-card,name = "I2S-Mic";
                simple-audio-card,format = "i2s";

                simple-audio-card,cpu {
                    sound-dai = <&i2s_clk_producer>;
                };

                simple-audio-card,codec {
                    sound-dai = <&mic_codec>;
                };
            };
        };
    };
};

Key DT properties: - simple-audio-card,format = "i2s" — standard I2S format (1-bit delay) - The Pi generates BCLK and LRCLK as master; the mic is slave - The "codec" is a dummy — INMP441 is a digital mic with no configuration registers

4.5 Linux ALSA Subsystem for I2S

User space:
  arecord, aplay          ← command-line tools
  snd_pcm_open()          ← ALSA library (libasound)
  /dev/snd/pcmC*D*c       ← capture device nodes
  /dev/snd/pcmC*D*p       ← playback device nodes

Kernel:
  ┌─────────────────────────────────────────────┐
  │ ALSA core (sound/core/)                      │
  │  ├── snd_card        (sound card)            │
  │  ├── snd_pcm         (PCM stream)            │
  │  └── snd_pcm_hw      (hardware params)       │
  ├─────────────────────────────────────────────┤
  │ ASoC (ALSA System-on-Chip):                  │
  │  ├── Platform driver   (bcm2835-i2s)         │
  │  ├── Codec driver      (dummy or real)        │
  │  └── Machine driver    (simple-audio-card)    │
  └─────────────────────────────────────────────┘

Hardware:
  BCM2835 PCM/I2S controller
  Registers at 0x7E203000
  DMA channels for capture and playback

ASoC (ALSA System-on-Chip) splits the audio path into three components: - Platform (CPU-side DMA + I2S controller): bcm2835-i2s - Codec (the audio chip): snd-soc-spdif-dit for dummy codecs, or real codec drivers (e.g., wm8960, pcm5102a) - Machine (connects platform + codec): simple-audio-card or a board-specific driver

4.6 ALSA Configuration for Low Latency

The ALSA parameters that affect latency:

/* Period: smallest unit of transfer. ALSA wakes the app every period. */
snd_pcm_hw_params_set_period_size_near(pcm, hw, &period_frames, NULL);

/* Buffer: total ALSA buffer = N × period_size. Larger = more latency but fewer underruns. */
snd_pcm_hw_params_set_periods_near(pcm, hw, &num_periods, NULL);

/* Format: S32_LE for INMP441 (24-bit data in 32-bit slot) */
snd_pcm_hw_params_set_format(pcm, hw, SND_PCM_FORMAT_S32_LE);
Period Buffer (3 periods) Latency Underrun risk
1024 frames 3072 frames 64 ms Low
512 frames 1536 frames 32 ms Low
256 frames 768 frames 16 ms Medium
128 frames 384 frames 8 ms High (needs RT scheduling)

See Audio Pipeline Latency for hands-on measurement.

4.7 Debugging I2S

# Check if the overlay loaded
dmesg | grep -i i2s
# → bcm2835-i2s 3f203000.i2s: ...

# List ALSA devices
arecord -l
# → card 1: sndrpisimplecar [snd_rpi_simple_card], device 0: ...

# Test capture (5 seconds, 48 kHz, stereo, S32_LE)
arecord -D hw:1,0 -f S32_LE -r 48000 -c 2 -d 5 test.wav

# Check with audio_viz_full
./audio_viz_full -D    # list all ALSA devices
./audio_viz_full -d hw:1,0 -c 2 -f

Common problems:

Symptom Cause Fix
arecord -l shows no card Overlay not loaded Add dtoverlay=i2s-mems-mic to config.txt, reboot
Captures silence L/R pin wrong INMP441: L/R to GND = left channel, to VCC = right. Try both -c 1 and -c 2
Captures noise only BCLK/LRCLK swapped Check wiring: BCLK=GPIO18, LRCLK=GPIO19, DOUT=GPIO20
Very quiet signal 24-bit in 32-bit slot Normal — apply software gain (4x–32x). See Audio Visualizer
Clicks/pops Buffer underrun Increase period size or enable SCHED_FIFO

5. Bus Comparison for Design Decisions

When designing an embedded system, the bus choice depends on the device requirements:

Criterion I2C SPI I2S
Wiring complexity Lowest (2 wires for all devices) Medium (4 + 1 CS per device) Low (3 wires)
Speed Slow (≤ 3.4 MHz) Fast (≤ 125 MHz) Fixed (by sample rate)
CPU overhead Low (small transfers) Low-medium (DMA for bulk) Low (DMA-driven)
Multi-device Easy (addresses) Harder (CS lines) TDM (complex)
Hot-pluggable Yes (with address scan) No (CS must be wired) No
Power Low (can clock-gate) Medium (always clocking) Low (idle = no clock)
Debugging Easy (i2cdetect) Harder (need scope) Medium (arecord)
Typical devices Temperature, humidity, RTC, EEPROM IMU, display, flash, ADC Mic, DAC, audio codec

Mixed-Bus Design Example (This Course's Pi Setup)

                     Raspberry Pi 4
                    ┌──────────────────────────────┐
I2C1 (100 kHz) ────┤ GPIO 2 (SDA), GPIO 3 (SCL)  │
  ├── MCP9808 temp  │                              │
  └── SSD1306 OLED  │                              │
                    │                              │
SPI0 (10 MHz) ─────┤ GPIO 10/9/11 (MOSI/MISO/CLK)│
  ├── BMI160 IMU    │ GPIO 8 (CE0)                 │
  └── ILI9341 TFT   │ GPIO 7 (CE1)                 │
                    │                              │
I2S (3.072 MHz) ───┤ GPIO 18/19/20 (BCLK/LR/DIN) │
  ├── INMP441 mic L │                              │
  └── INMP441 mic R │                              │
                    │                              │
HDMI ──────────────┤ (display output)              │
USB ───────────────┤ (keyboard, mouse)             │
Ethernet ──────────┤ (SSH, network)                │
                    └──────────────────────────────┘

I2C for slow sensors (temperature reads 2 bytes every second), SPI for fast data (IMU at 200 Hz, display at 30 fps), I2S for continuous audio (48 kHz stereo).


Further Reading

Specifications: - I2C-bus specification (NXP) — the official spec - SPI overview (Motorola/NXP) — original Motorola spec - I2S bus specification (Philips) — the original 1986 spec

Datasheets (course devices): - MCP9808 — I2C temperature sensor - BMI160 — SPI/I2C 6-axis IMU - INMP441 — I2S MEMS microphone

Linux kernel docs: - I2C subsystem — writing I2C drivers - SPI subsystem — writing SPI drivers - ASoC (ALSA SoC) — audio codec/platform drivers

Course tutorials: - Enable I2C — setup and verification - MCP9808 I2C Driver — write a kernel driver from scratch - BMI160 SPI Driver — SPI kernel driver with IIO - I2S Audio Visualizer — capture and process I2S audio - SPI Display — drive a TFT display over SPI - SPI DMA Optimization — bulk transfers with DMA - Audio Pipeline Latency — measure I2S capture-to-playback latency