Classroom Glossary Public page

Week 12: Driver-Writing Track

1,416 words

Everything you have built so far lives in software. This week the software talks to hardware it did not design.


Reading

Required. Petzold, CODE, Ch 16 ("An Assemblage of Memory") and Ch 18 ("Let's Build a Clock!"). Ch 16 covers memory-mapped I/O (the mechanism underlying all three drivers); Ch 18 traces serial communication from Morse code through telegraph through UART through modern serial buses. I2C and SPI are direct descendants of the serial ideas Petzold introduces in Ch 18. Read both chapters before the first lab session.

Required. SSD1306 datasheet, sections 8.1 (I2C interface), 8.5 (command table), and 10.1.3 (initialization sequence). Available from Adafruit and Solomon Systech; search "SSD1306 datasheet". The datasheet is the authoritative reference for the driver you write.

Required. ENC28J60 datasheet, sections 1 (overview), 3 (SPI interface), 4 (register map), and 6 (transmit and receive procedures). Available from Microchip; search "ENC28J60 datasheet".


Lecture: Writing Drivers From Datasheets

What a driver is and is not

A device driver is the OS code that translates the OS's abstract interface to a device's physical interface. The OS calls screen_write_pixel(x, y, color); the driver translates that into the specific byte sequence the SSD1306 command protocol requires. The driver does not define what the device can do; the datasheet does. The driver's job is to implement the datasheet's protocol correctly.

A driver is not:

  • A library. Libraries are user-mode. Drivers run at OS privilege.
  • A hardware description. The driver runs on the CPU; the device is a separate chip.
  • Portable. A driver written for the SSD1306 over I2C is not the same driver as one for the SSD1306 over SPI, even though the command set is the same.

I2C protocol (SSD1306 interface)

I2C (Inter-Integrated Circuit) is a two-wire serial bus: SCL (clock) and SDA (data). The controller (CPU) generates the clock and initiates all transfers. The device (SSD1306) responds.

I2C frame structure for SSD1306:

START | slave_address | W | ACK | control_byte | ACK | data_byte | ACK | ... | STOP

The SSD1306's I2C slave address is 0x3C or 0x3D (set by a pin on the module). The control byte distinguishes commands from data:

  • 0x00: following bytes are command bytes (control registers)
  • 0x40: following bytes are GDDRAM data (pixel data)
  • 0x80: single command byte (legacy mode)

SSD1306 initialization sequence (from datasheet section 10.1.3; condensed):

// send as command bytes:
0xAE           // display off
0xD5, 0x80     // set display clock divide ratio
0xA8, 0x3F     // set multiplex ratio (64 rows - 1)
0xD3, 0x00     // set display offset = 0
0x40           // set display start line = 0
0x8D, 0x14     // charge pump: enable
0x20, 0x00     // memory addressing mode: horizontal
0xA1           // segment remap (col 127 -> SEG0)
0xC8           // scan direction: COM[N-1] to COM[0]
0xDA, 0x12     // set COM pins hardware config
0x81, 0xCF     // set contrast
0xD9, 0xF1     // set pre-charge period
0xDB, 0x40     // set VCOMH deselect level
0xA4           // output RAM to display (not all-on)
0xA6           // normal display (not inverted)
0xAF           // display on

Software-bit-bang I2C on DE10-Nano. The DE10-Nano does not expose hardware I2C directly on the easy-to-wire GPIO header. Use bit-banging: toggle GPIO pins for SCL and SDA manually in the driver, with delays that satisfy the I2C timing spec (tLow >= 4.7 us, tHigh >= 4.0 us for standard-mode 100 kHz I2C). At a 50 MHz CPU clock (20 ns/cycle), one 4.7 us period = 235 cycles. Implement as a delay_i2c_half_bit() function that loops 117 times.

SPI protocol (SD card and ENC28J60 interfaces)

SPI (Serial Peripheral Interface) is a four-wire bus: MOSI (master out/slave in), MISO (master in/slave out), SCK (clock), and CS (chip select, one per device). SPI is full-duplex: the controller sends and receives a byte simultaneously in a single clock sequence.

SPI frame: pull CS low; clock out 8 bits on MOSI while clocking in 8 bits on MISO; release CS high.

SPI has four modes defined by clock polarity (CPOL) and phase (CPHA). The SD card uses SPI mode 0 (CPOL=0, CPHA=0: clock idles low; data sampled on rising edge). The ENC28J60 also uses SPI mode 0.

SD card SPI mode initialization:

  1. Hold CS high; send >= 74 clock cycles (power-up delay).
  2. Pull CS low; send CMD0 (0x40, 0x00, 0x00, 0x00, 0x00, 0x95) to reset the card.
  3. Wait for R1 response = 0x01 (idle state).
  4. Send CMD8 to detect SDHC/SDXC; for SDSC, CMD8 is unsupported and returns 0x05.
  5. Send ACMD41 (= CMD55 followed by CMD41) to initialize the card.
  6. Wait for R1 = 0x00 (card ready).
  7. Set block length to 512 bytes (CMD16).

ENC28J60 SPI register access: The ENC28J60 uses SPI commands (5 bits opcode + 5 bits register address in the first byte):

  • 000AAAAA: read control register at address AAAAA
  • 010AAAAA: write control register
  • 011AAAAA: write buffer memory command
  • 111XXXXX: soft reset

Reading the MACON3 (MAC control register 3) to verify full-duplex mode:

uint8_t read_register(uint8_t bank, uint8_t addr) {
    select_bank(bank);
    spi_cs_low();
    spi_tx(0x00 | addr);   // READ_CTRL_REG opcode
    uint8_t val = spi_rx();
    spi_cs_high();
    return val;
}

Driver architecture in Virtus OS v2

Each driver exposes a uniform interface to the kernel:

// SSD1306 driver interface
void ssd1306_init(void);
void ssd1306_clear(void);
void ssd1306_draw_char(uint8_t col, uint8_t row, char c);
void ssd1306_flush(void);  // commit framebuffer to display

// SD card driver interface
int  sd_init(void);        // returns 0 on success
int  sd_read_block(uint32_t block_addr, uint8_t* buf);   // 512-byte block
int  sd_write_block(uint32_t block_addr, const uint8_t* buf);

// ENC28J60 driver interface
void enc28j60_init(const uint8_t* mac_addr);
int  enc28j60_send(const uint8_t* packet, uint16_t len);
int  enc28j60_recv(uint8_t* buf, uint16_t max_len);

The kernel registers these drivers in a device table. The filesystem layer calls sd_read_block / sd_write_block; the screen service calls ssd1306_*; the network stack calls enc28j60_*.

Architecture Comparison Sidebar: I2C vs SPI vs UART

Property I2C SPI UART
Wires 2 (SCL + SDA) 4 per device (MOSI, MISO, SCK, CS) 2 (TX + RX)
Multi-device Bus (multiple devices share wires) Separate CS per device Point-to-point
Speed (typical) 100 kHz - 400 kHz - 3.4 MHz 1-100 MHz 9600 - 921600 baud
Full-duplex No (half-duplex) Yes Yes
Protocol overhead Address + ACK per byte CS toggle per transaction Start + stop bits per byte
Use cases Low-speed sensors, display controllers Flash, SD cards, Ethernet Debug console, GPS, serial comms
Petzold reference Descendant of early serial telegraphy (Ch 18) Same lineage, wider data path Most direct serial descendant

The ENC28J60 over SPI can handle packets at 10 Mbps Ethernet line rate because SPI can clock at up to 20 MHz. An I2C Ethernet controller would be impractically slow.


Lab exercises

See labs/lab-12-driver-writing.md for the full specification.

Lab 12.1: SSD1306 OLED driver written from datasheet; output verified. You will implement the SSD1306 driver in C (targeting the DE10-Nano) by following the datasheet initialization sequence. The lab verifies the driver by displaying a string on the OLED.

Part A: Implement the bit-bang I2C layer (SDA/SCL pin toggle + timing). Test by toggling SCL and measuring the frequency on the logic analyzer (target: 100 kHz).

Part B: Implement the SSD1306 command and data send functions. Send the initialization sequence and verify the display powers on.

Part C: Implement ssd1306_draw_char using a 5x7 font table. Display the string "Virtus OS v2" on the OLED. Verify visually.

Bonus: implement the SD card SPI initialization sequence (Parts D-F in the lab spec). Verify by reading the card's CID register (Card Identification Data) which returns the manufacturer ID and card name.


Independent practice

  1. The SSD1306 uses a GDDRAM (Graphic Display Data RAM) organized as 8 pages of 128 bytes. Each byte is a vertical 8-pixel column. Sketch the mapping: for a 128x64 display (8 pages of 8 rows = 64 rows total), what is the byte address and bit position of pixel (col=5, row=3)? Write the formula.

  2. I2C requires the ACK bit from the slave after each byte. What should the driver do if the slave sends NACK (non-acknowledge)? Write the error-handling logic.

  3. Toolchain Diary entry: logic analyzer (Saleae Logic 8 or SignalTap). Record how to probe an I2C bus: which channels map to SCL and SDA, how to set the trigger, and how to decode I2C frames from the captured waveform.

  4. The ENC28J60's SPI interface requires bit-banging on the DE10-Nano's GPIO header. What is the maximum safe clock frequency for bit-bang SPI at a 50 MHz CPU clock (considering the GPIO toggle overhead at 1 instruction per toggle = 20 ns per half-period)? How does this compare to the ENC28J60's maximum 20 MHz SPI clock?


Reflection prompts

  1. The SSD1306 framebuffer (1024 bytes for 128x64 monochrome) is written a page at a time in horizontal addressing mode. A ssd1306_flush() that sends the entire framebuffer every time a pixel changes takes ~1024 I2C byte-writes at 400 kHz = ~20 ms. At 50 Hz refresh, this is 100% of the timer budget. What optimization would reduce the flush cost? (Hint: dirty tracking.)

  2. The SD card SPI protocol uses CRC bytes for data integrity. The CSA-201 driver initializes the card with CRC disabled (CMD0's CRC byte 0x95 is valid for CMD0 specifically; subsequent commands use CRC=0x00 with the CRC bypass enabled). What attack does the CRC protect against? In a lab environment, why is CRC bypass acceptable?

  3. The ENC28J60 receives packets into a circular receive buffer on the chip. If the driver doesn't poll fast enough, the buffer fills and new packets are dropped. What interrupt mechanism does the ENC28J60 provide to avoid polling? (Check the datasheet section 12 -- INT pin + EIE register.)


What's next

Module 13 connects the filesystem. The SD card driver you just wrote is the block-level interface. Module 13 adds the FAT16 filesystem walker above it: reading the FAT table, locating directory entries, and reading file data blocks. Module 13 also covers the DE10-Nano's external DRAM as a storage expansion: the Tang silicon BRAM ceiling (1,008 Kbit on Primer 25K) is too small for large programs; Module 13 moves the heap to DDR3.