I2C support for libDaisy Nim wrapper
This module provides I2C (Inter-Integrated Circuit) communication support for the Daisy Audio Platform. It supports both master and slave modes, blocking and DMA transfers.
⚠️ IMPORTANT - Blocking vs DMA Functions:
- Blocking functions (write, read, writeRegister, readRegister) will stall the CPU while waiting for the I2C transaction to complete. This can cause audio glitches if called from the audio callback or main loop during audio processing.
- DMA functions (transmitDma, receiveDma) use Direct Memory Access to transfer data in the background without blocking the CPU. These are safe to use during audio processing if your buffers are in the correct memory region.
DMA Buffer Requirements:
- Buffers must be in D2 memory domain (not stack variables!)
- Use {.section: ".sram1_bss".} or allocate on heap
- Or use dsy_dma_clear_cache_for_buffer() before transfer (advanced)
DMA Availability:
- I2C1, I2C2, I2C3: Share a single DMA channel (only one can use DMA at a time)
- I2C4: No DMA support (use blocking functions only)
Example - Simple I2C master (blocking):
import nimphea, per/i2c, per/uart var daisy = initDaisy() var i2c = initI2C(I2C_1, D11(), D12(), I2C_400KHZ) startLog() # Scan for devices (BLOCKS - don't use in audio callback!) var foundDevices: array[112, uint8] let count = i2c.scan(foundDevices) print("Found ") print(count) printLine(" devices") # Write to device (BLOCKS) var txData = [0x01'u8, 0xFF] if i2c.write(0x48, txData) == I2C_OK: printLine("Write OK") # Read from device (BLOCKS) var rxData: array[4, uint8] if i2c.read(0x48, rxData) == I2C_OK: printLine("Read OK")
Example - Register access (blocking):
# Write to register (BLOCKS) discard i2c.writeRegister(0x3C, 0x00, 0xAF) # Read from register (BLOCKS) let (result, value) = i2c.readRegister(0x3C, 0x01)
Example - Non-blocking DMA transfer:
import nimphea, per/i2c # DMA buffers MUST be in D2 memory, not on stack! var txBuffer {.section: ".sram1_bss".}: array[64, uint8] var transferComplete = false proc onTransferComplete(context: pointer, result: I2CResult) {.cdecl.} = # Called from interrupt - keep this FAST! transferComplete = true var daisy = initDaisy() var i2c = initI2C(I2C_1, D11(), D12(), I2C_400KHZ) # Start DMA transfer (non-blocking) discard i2c.transmitDma(0x48, txBuffer, onTransferComplete, nil) # CPU is free to do other work while transfer happens in background while not transferComplete: # Do audio processing or other tasks discard
Types
I2CCallbackFunctionPtr = proc (context: pointer; result: I2CResult) {.cdecl.}
- Source Edit
I2CConfig {.importcpp: "daisy::I2CHandle::Config", bycopy.} = object periph* {.importc: "periph".}: I2CPeripheral pin_config* {.importc: "pin_config".}: I2CPinConfig speed* {.importc: "speed".}: I2CSpeed mode* {.importc: "mode".}: I2CMode address* {.importc: "address".}: uint8
- Source Edit
I2CDirection {.importcpp: "daisy::I2CHandle::Direction", size: 4.} = enum I2C_TRANSMIT = 0, I2C_RECEIVE
- Source Edit
I2CHandleImpl {.importcpp: "daisy::I2CHandle::Impl".} = object
- Source Edit
I2CMode {.importcpp: "daisy::I2CHandle::Config::Mode", size: 4.} = enum I2C_MASTER = 0, I2C_SLAVE
- Source Edit
I2CPeripheral {.importcpp: "daisy::I2CHandle::Config::Peripheral", size: 4.} = enum I2C_1 = 0, I2C_2, I2C_3, I2C_4
- Source Edit
I2CPinConfig {.importcpp: "daisy::I2CHandle::Config::pin_config", bycopy.} = object scl* {.importc: "scl".}: Pin sda* {.importc: "sda".}: Pin
- Source Edit
Consts
I2C_ADDR_ADS1115 = 0x48'u8
- ADS1115 ADC Source Edit
I2C_ADDR_AT24C32 = 0x50'u8
- AT24C32 EEPROM Source Edit
I2C_ADDR_BMP280 = 0x76'u8
- BMP280 pressure sensor Source Edit
I2C_ADDR_BMP280_ALT = 0x77'u8
- BMP280 alternate address Source Edit
I2C_ADDR_DS3231 = 0x68'u8
- DS3231 RTC Source Edit
I2C_ADDR_MCP23017 = 0x20'u8
- MCP23017 I/O expander Source Edit
I2C_ADDR_MPU6050 = 0x68'u8
- MPU6050 IMU Source Edit
I2C_ADDR_PCF8574 = 0x20'u8
- PCF8574 I/O expander Source Edit
I2C_ADDR_SSD1306 = 0x3C'u8
- SSD1306 OLED display Source Edit
I2C_ADDR_SSD1306_ALT = 0x3D'u8
- SSD1306 alternate address Source Edit
Procs
proc initI2C(peripheral: I2CPeripheral; sclPin, sdaPin: Pin; speed: I2CSpeed = I2C_400KHZ; mode: I2CMode = I2C_MASTER; slaveAddress: uint8 = 0x00000010): I2CHandle {....raises: [], tags: [], forbids: [].}
-
Initialize I2C interface
Parameters: peripheral: I2C_1, I2C_2, I2C_3, or I2C_4 sclPin: Clock pin (e.g., D11()) sdaPin: Data pin (e.g., D12()) speed: I2C_100KHZ, I2C_400KHZ, or I2C_1MHZ mode: I2C_MASTER or I2C_SLAVE slaveAddress: Device address when in slave mode
Example:
var i2c = initI2C(I2C_1, D11(), D12(), I2C_400KHZ)
Source Edit proc readRegister(i2c: var I2CHandle; deviceAddr: uint16; regAddr: uint8; timeout: uint32 = 100): tuple[result: I2CResult, value: uint8] {. inline, ...raises: [], tags: [], forbids: [].}
- Read a single byte from a device register Source Edit
proc readRegisters(i2c: var I2CHandle; deviceAddr: uint16; regAddr: uint8; buffer: var openArray[uint8]; timeout: uint32 = 100): I2CResult {. inline, ...raises: [], tags: [], forbids: [].}
- Read multiple bytes from consecutive device registers into provided buffer Source Edit
proc receiveDma(i2c: var I2CHandle; deviceAddr: uint16; buffer: var openArray[uint8]; callback: I2CCallbackFunctionPtr = nil; context: pointer = nil): I2CResult {. ...raises: [], tags: [], forbids: [].}
-
Non-blocking DMA receive from I2C device
⚠️ CRITICAL: Buffer MUST be in D2 memory domain:
- Use {.section: ".sram1_bss".} pragma on buffer declaration
- Or allocate on heap with alloc/create
- DO NOT use stack variables (will cause DMA errors)
⚠️ DMA Sharing: I2C1/I2C2/I2C3 share one DMA channel. Only one can use DMA at a time. I2C4 has NO DMA support - use blocking functions only.
Parameters: deviceAddr: 7-bit I2C device address (e.g., 0x48) buffer: Buffer to receive data into (must be in D2 memory!) callback: Called when transfer completes (from interrupt, keep fast!) context: User data pointer passed to callback
Returns: I2C_OK if transfer queued successfully, I2C_ERR on error
Example:
var rxBuf {.section: ".sram1_bss".}: array[64, uint8] proc onComplete(ctx: pointer, res: I2CResult) {.cdecl.} = # Process received data discard discard i2c.receiveDma(0x48, rxBuf, onComplete, nil)
Source Edit proc scan(i2c: var I2CHandle; found: var openArray[uint8]; timeout: uint32 = 10): int {. ...raises: [], tags: [], forbids: [].}
- Scan the I2C bus for devices, storing responding addresses in provided buffer Returns number of devices found. Only works in master mode. Buffer should be at least 112 bytes to hold all possible addresses (0x08-0x77) Source Edit
proc TransmitBlocking(this: var I2CHandle; address: uint16; data: ptr uint8; size: uint16; timeout: uint32): I2CResult {. importcpp: "#.TransmitBlocking(@)", header: "daisy_seed.h", importcpp, ...raises: [], tags: [], forbids: [].}
- Source Edit
proc TransmitDma(this: var I2CHandle; address: uint16; data: ptr uint8; size: uint16; callback: I2CCallbackFunctionPtr; callback_context: pointer): I2CResult {. importcpp: "#.TransmitDma(@)", header: "daisy_seed.h", importcpp, ...raises: [], tags: [], forbids: [].}
- Source Edit
proc transmitDma(i2c: var I2CHandle; deviceAddr: uint16; buffer: var openArray[uint8]; callback: I2CCallbackFunctionPtr = nil; context: pointer = nil): I2CResult {. ...raises: [], tags: [], forbids: [].}
-
Non-blocking DMA transmit to I2C device
⚠️ CRITICAL: Buffer MUST be in D2 memory domain:
- Use {.section: ".sram1_bss".} pragma on buffer declaration
- Or allocate on heap with alloc/create
- DO NOT use stack variables (will cause DMA errors)
⚠️ DMA Sharing: I2C1/I2C2/I2C3 share one DMA channel. Only one can use DMA at a time. I2C4 has NO DMA support - use blocking functions only.
Parameters: deviceAddr: 7-bit I2C device address (e.g., 0x48) buffer: Data to transmit (must be in D2 memory!) callback: Called when transfer completes (from interrupt, keep fast!) context: User data pointer passed to callback
Returns: I2C_OK if transfer queued successfully, I2C_ERR on error
Example:
var txBuf {.section: ".sram1_bss".}: array[64, uint8] proc onComplete(ctx: pointer, res: I2CResult) {.cdecl.} = echo "Transfer done!" discard i2c.transmitDma(0x48, txBuf, onComplete, nil)
Source Edit proc writeRegister(i2c: var I2CHandle; deviceAddr: uint16; regAddr: uint8; value: uint8; timeout: uint32 = 100): I2CResult {.inline, ...raises: [], tags: [], forbids: [].}
- Write a single byte to a device register Source Edit
proc writeRegisters(i2c: var I2CHandle; deviceAddr: uint16; regAddr: uint8; values: openArray[uint8]; timeout: uint32 = 100): I2CResult {. inline, ...raises: [], tags: [], forbids: [].}
- Write multiple bytes to consecutive device registers Source Edit