An OCaml library for interfacing with Raspberry Pi hardware using Linux APIs. Includes GPIO, SPI, I2C, and drivers for various display and sensor modules.
- GPIO Character Device API - Uses
/dev/gpiochipN(not deprecated sysfs) - SPI Communication - Hardware SPI support via
/dev/spidev - I2C Communication - I2C bus support for sensors and peripherals
- Hardware Drivers:
- Waveshare 2.13" ePaper Display V4 (with partial update support)
- MAX7219 LED matrix display (chainable)
- LCD2004 20x4 character display with I2C backpack
- DS3231 Real-Time Clock
- AT24C32 I2C EEPROM
- Graphics Libraries:
- 1-bit framebuffer with rotation support (0°, 90°, 180°, 270°)
- 5×7 ASCII bitmap font
- Drawing primitives (lines, rectangles, circles)
- Pure OCaml with ctypes - No C code, uses OCaml FFI
- 32-bit and 64-bit compatible
opam install ctypes ctypes-foreign duneopam exec -- dune buildDemo of graphics capabilities on ePaper display:
sudo opam exec -- dune exec test/epaper_graphics.exeShows text, lines, rectangles, circles, and complex scenes.
Drive multiple chained 8×8 LED matrices:
sudo opam exec -- dune exec test/test_max7219.exeDS3231 I2C RTC with temperature sensor:
sudo opam exec -- dune exec test/test_rtc.exeAT24C32 EEPROM read/write:
sudo opam exec -- dune exec test/test_eeprom.exeMAX7219 display showing time from DS3231 RTC:
sudo opam exec -- dune exec test/clock.exeLED blinking, button reading, PWM:
sudo opam exec -- dune exec test/test_led.exe
sudo opam exec -- dune exec test/test_button.exe
sudo opam exec -- dune exec test/test_pwm.exeScan for I2C devices on the bus:
sudo opam exec -- dune exec test/i2c_scan.exe20x4 character LCD with I2C backpack:
sudo opam exec -- dune exec test/test_lcd2004.exe
sudo opam exec -- dune exec test/seven_seg_clock.exeModern character device API for GPIO control:
let chip = Gpio.open_chip () in (* Auto-detects GPIO chip *)
let led = Gpio.setup_output chip 17 in
Gpio.write led true;
Gpio.release_line led;
Gpio.close_chip chipHardware SPI communication:
let spi = Spi.open_device ~speed_hz:1000000 "/dev/spidev0.0" in
Spi.write_bytes spi [0x42; 0xFF];
Spi.close_device spiI2C bus communication:
let fd = I2c.open_device 1 in (* Open /dev/i2c-1 *)
I2c.set_slave_address fd 0x68;
I2c.write_bytes fd [0x00; 0x45];
let data = I2c.read_bytes fd 7 in
I2c.close_device fdWaveshare 2.13" ePaper display with partial update support:
let display = Epaper.init () in
Epd2in13_v4.init display;
(* Full refresh *)
Epd2in13_v4.display display buffer;
(* Set base image for partial updates *)
Epd2in13_v4.display_partial_base display buffer;
(* Partial updates (only changed pixels) *)
Epd2in13_v4.display_partial display buffer;1-bit graphics with rotation support (0°, 90°, 180°, 270°):
(* Create 250×122 framebuffer with 90° rotation *)
let fb = Framebuffer.create ~rotation:90 250 122 0xFF in
(* Drawing primitives *)
Framebuffer.draw_line fb 0 0 100 100 true;
Framebuffer.draw_circle fb 50 50 20 true;
Framebuffer.draw_filled_rect fb 10 10 30 40 true;
(* Get rotated buffer for display *)
let buffer = Framebuffer.to_list_rotated fb in5×7 ASCII bitmap font:
let width = Font.draw_string fb 10 10 "Hello, World!" true in
let char_width = Font.draw_char fb x y 'A' true inLED matrix display driver with chaining:
let display = Max7219.init "/dev/spidev0.0" 4 in (* 4 daisy-chained displays *)
Max7219.set_intensity display 5;
Max7219.set_row display 0 3 0x7F; (* device 0, row 3 *)
Max7219.clear_all display;
Max7219.close display;Real-time clock with temperature sensor:
let rtc = Ds3231.open_device 1 in (* Open on I2C bus 1 *)
(* Read date/time *)
let dt = Ds3231.read_datetime rtc in
Printf.printf "%02d:%02d:%02d\n" dt.hours dt.minutes dt.seconds;
(* Write date/time *)
let dt = { year = 2025; month = 1; date = 15; day = 3;
hours = 14; minutes = 30; seconds = 0 } in
Ds3231.write_datetime rtc dt;
(* Read temperature *)
let temp = Ds3231.read_temperature rtc in
Printf.printf "Temperature: %.2f°C\n" temp;
Ds3231.close_device rtc;I2C EEPROM with page writing:
let eeprom = At24c32.open_device 1 0x57 in (* Bus 1, address 0x57 *)
(* Single byte operations *)
At24c32.write_byte eeprom 0x0000 0x42;
let value = At24c32.read_byte eeprom 0x0000 in
(* Multi-byte operations *)
At24c32.write_bytes eeprom 0x0010 [0x01; 0x02; 0x03];
let data = At24c32.read_bytes eeprom 0x0010 3 in
(* String operations *)
At24c32.write_string eeprom 0x0100 "Hello, EEPROM!";
let str = At24c32.read_string eeprom 0x0100 64 in
At24c32.close_device eeprom;- VCC → 3.3V
- GND → Ground
- DIN → SPI0 MOSI (GPIO 10)
- CLK → SPI0 SCLK (GPIO 11)
- CS → SPI0 CE0 (GPIO 8) - or any GPIO if using software CS
- DC → GPIO 25
- RST → GPIO 17
- BUSY → GPIO 24
- PWR → GPIO 18 (optional)
Note: When using hardware SPI CS, the CS pin is automatically managed by the SPI driver.
- VCC → 5V
- GND → Ground
- DIN → SPI0 MOSI (GPIO 10)
- CLK → SPI0 SCLK (GPIO 11)
- CS → SPI0 CE0 (GPIO 8)
- VCC → 3.3V or 5V
- GND → Ground
- SDA → I2C1 SDA (GPIO 2)
- SCL → I2C1 SCL (GPIO 3)
- VCC → 3.3V or 5V
- GND → Ground
- SDA → I2C1 SDA (GPIO 2)
- SCL → I2C1 SCL (GPIO 3)
- WP → Ground (write enable)
- A0, A1, A2 → Ground (address pins)
# Enable interfaces using raspi-config
sudo raspi-config
# Interface Options → SPI → Enable
# Interface Options → I2C → Enable
# Reboot
sudo reboot
# Verify devices exist
ls /dev/spidev* /dev/i2c*GPIO, SPI, and I2C require appropriate permissions:
# Add user to gpio, spi, i2c groups
sudo usermod -a -G gpio,spi,i2c $USER
# Log out and back in for changes to take effect
# Or run with sudo
sudo opam exec -- dune exec test/test_max7219.exeUses modern /dev/gpiochipN interface with ioctl calls:
GPIO_V2_GET_LINE_IOCTL- Request GPIO linesGPIO_V2_LINE_SET_VALUES_IOCTL- Set output valuesGPIO_V2_LINE_GET_VALUES_IOCTL- Read input values
All ioctl constants use int32 for 32-bit compatibility.
The display has two RAM buffers (0x24 and 0x26) for hardware pixel comparison:
- Set base image → writes to both buffers
- Partial update → controller compares and updates only changed pixels
- Results in flicker-free updates (ideal for clocks, status displays)
Supports 0°, 90°, 180°, 270° rotation:
- Rotation applied when converting buffer to display format
- Allows landscape/portrait modes without redrawing logic
- Useful for physical mounting orientations
All ioctl constants are defined as int32 to work on both 32-bit and 64-bit systems. On 32-bit OCaml, the int type is only 31 bits, so large ioctl values like 0xC250B407 require int32.
gpio/
├── lib/
│ ├── ioctl.ml - Low-level ioctl wrapper
│ ├── gpio.ml - GPIO character device API
│ ├── spi.ml - SPI communication
│ ├── i2c.ml - I2C communication
│ ├── epaper.ml - Generic ePaper driver
│ ├── epd2in13_v4.ml - Waveshare 2.13" V4 specific
│ ├── max7219.ml - MAX7219 LED matrix
│ ├── ds3231.ml - DS3231 RTC
│ ├── at24c32.ml - AT24C32 EEPROM
│ ├── framebuffer.ml - Graphics primitives
│ ├── font.ml - Bitmap font rendering
│ └── dune - Library definitions
├── test/
│ ├── test_max7219.ml - LED matrix demo
│ ├── clock.ml - RTC + LED matrix clock
│ ├── test_rtc.ml - RTC test
│ ├── test_eeprom.ml - EEPROM test
│ ├── epaper_graphics.ml - Graphics demo
│ ├── test_epaper.ml - ePaper test
│ ├── test_led.ml - LED blink
│ ├── test_button.ml - Button input
│ ├── test_pwm.ml - Software PWM
│ ├── test_lcd2004.ml - LCD2004 display test
│ ├── seven_seg_clock.ml - 7-segment clock on LCD
│ ├── i2c_scan.ml - I2C bus scanner
│ └── dune - Test executables
└── dune-project - Project configuration