nimphea/per/i2c

Search:
Group by:
Source   Edit  

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  
I2CHandle {.importcpp: "daisy::I2CHandle".} = object
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  
I2CResult {.importcpp: "daisy::I2CHandle::Result", size: 4.} = enum
  I2C_OK = 0, I2C_ERR
Source   Edit  
I2CSpeed {.importcpp: "daisy::I2CHandle::Config::Speed", size: 4.} = enum
  I2C_100KHZ = 0, I2C_400KHZ, I2C_1MHZ
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 GetConfig(this: I2CHandle): I2CConfig {.importcpp: "#.GetConfig()",
    header: "daisy_seed.h", importcpp, ...raises: [], tags: [], forbids: [].}
Source   Edit  
proc Init(this: var I2CHandle; config: I2CConfig): I2CResult {.
    importcpp: "#.Init(@)", header: "daisy_seed.h", importcpp, ...raises: [],
    tags: [], forbids: [].}
Source   Edit  
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 read(i2c: var I2CHandle; deviceAddr: uint16; buffer: var openArray[uint8];
          timeout: uint32 = 100): I2CResult {.inline, ...raises: [], tags: [],
    forbids: [].}
Read bytes from an I2C device into provided buffer 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 write(i2c: var I2CHandle; deviceAddr: uint16; data: openArray[uint8];
           timeout: uint32 = 100): I2CResult {.inline, ...raises: [], tags: [],
    forbids: [].}
Write bytes to an I2C device 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