Raspberry Pi 5 IoT In C - I2C with Gpio5
Written by Harry Fairhead   
Wednesday, 16 July 2025
Article Index
Raspberry Pi 5 IoT In C - I2C with Gpio5
I2C Functions -Read/Write
End User Functions

The first function we need is something to initialize the I2C channel we are about to use:

void i2c_enable(I2C i2c, bool enable)
{
   i2c->enable = enable?  1:  0;
}
uint32_t i2c_init(I2C i2c, uint32_t baudrate)
{  
    i2c_enable(i2c, false);
    // Configure as a fast-mode master with 
    // RepStart support, 7-bit addresses
    i2c->con = (0x2ul << 1) | 0x01 | 0x040 |
0x20 | 0x100; // Set FIFO watermarks to 1 i2c->tx_tl = 0; i2c->rx_tl = 0; return i2c_set_baudrate(i2c, baudrate); }

This is based on the Pico SDK function and it uses other functions to set the baud rate and format.

The set_baudrate function is complicated by the intricacies of the I2C clock format:

int32_t i2c_set_baudrate(I2C i2c, int32_t baudrate)
{
  i2c_enable(i2c, false);
   // use "fast" mode
  i2c->con = (i2c->con & ~0x06ul) | (0x02 << 1 & 0x06);
  // set frequency and duty
  uint32_t period = (I2CClock + baudrate / 2) / baudrate;
  i2c->fs_scl_lcnt = period * 3 / 5; // 40% duty cycle
  i2c->fs_scl_hcnt = period - period * 3 / 5;
  // set spike suppression
  i2c->fs_spklen = i2c->fs_scl_lcnt < 16 ? 1 :
                                 i2c->fs_scl_lcnt / 16;
  // set hold time
  uint32_t sda_tx_hold_count=(baudrate < 1000000)?
((I2CClock * 3) / 10000000) + 1:
((I2CClock * 3) / 25000000) + 1; i2c->sda_hold = (i2c->sda_hold & ~0x0000ffff) | (sda_tx_hold_count & 0x0000ffff); i2c_enable(i2c, true); return I2CClock / period; }

For simplicity, fast mode is always used as it works at the lower speed 400kHz. The clock frequency is set by a count in hcnt which gives the high time and lcnt which gives the low time in terms of the main clock. For fast mode the duty cycle is around 40% and 2/5ths is a good approximation. Next we set some of the more subtle aspects of the clock. The spike suppression sets a threshold for noise pulses which are ignored. Finally the hold time for the data line is set – this is the time that the data line is held after the clock.

Once initialized, all we need is a write function and a read function. Following the Pico SDK, it is worth creating functions with timeouts:

int i2c_write_blocking_internal(I2C i2c, uint8_t addr,
         const uint8_t *src, size_t len, bool nostop,
uint32_t timeout_per_char_us) { i2c_enable(i2c, false); i2c->tar = addr; i2c_enable(i2c, true); bool abort = false; bool timeout = false; uint32_t abort_reason = 0; int byte_ctr; int ilen = (int)len; for (byte_ctr = 0; byte_ctr < ilen; ++byte_ctr) { bool first = byte_ctr == 0; bool last = byte_ctr == ilen - 1; uint32_t startbitnext = ((uint32_t)!!(first && restart_on_next)) << 10; uint32_t stopbit = ((uint32_t)!!
(last && !nostop)) << 9; uint64_t tm = micros() + timeout_per_char_us; i2c->data_cmd = startbitnext | stopbit | *src++; do { if (micros() > tm) timeout = true; } while (!timeout &&
!(i2c->raw_intr_stat & 0x10)); if (timeout) break; // check for non-timeout abort abort_reason = i2c->tx_abrt_source; if (abort_reason) { int32_t temp = i2c->clr_tx_abrt; abort = true; } } restart_on_next = nostop; if (abort || timeout) return i2c_handleAbort(i2c, timeout,
abort_reason); return byte_ctr; }

 

First we set the address of the slave that the data is to be sent to – this does not start transmission and is not sent until the data is sent. The first and last byte of the data are special in that we need to send a start bit and perhaps a stop bit. The stop bit can be sent or suppressed. The instruction:

 i2c->data_cmd = startbitnext | stopbit | *src++;

sends the next item of data and a start bit and stop bit as appropriate. This is also where we start measuring the time for the transaction so as to implement a timeout. Next we loop until the Tx FIFO is empty or a timeout occurs. If a timeout occurs the send loop is exited and the error reported. If the Tx FIFO empties we still have to check for any errors the hardware reported and pass the error code back to the calling program:

int32_t i2c_handleAbort(I2C i2c, bool timeout,
int32_t abortreason) { if (timeout) return 1 << 31 | 1 << 30; // bit 30 set for timout return abortreason | 1 << 31; }

In this case if there wasn’t a timeout we return a negative value, by setting the high bit, and set the remaining bits of the abort register. If there was a timeout, we return a negative value with bit 30 set.

A read with timeout follows very similar lines:

int i2c_read_blocking_internal(I2C i2c, uint8_t addr,
uint8_t *dst,size_t len, bool nostop,
uint32_t timeout_per_char_us) { i2c_enable(i2c, false); i2c->tar = addr; i2c_enable(i2c, true); bool abort = false; bool timeout = false; uint32_t abort_reason; int byte_ctr; int ilen = (int)len; for (byte_ctr = 0; byte_ctr < ilen; ++byte_ctr) { bool first = byte_ctr == 0; bool last = byte_ctr == ilen - 1; while (!i2c_get_write_available(i2c)) { }; uint32_t startbitnext = (uint32_t)!!(first && restart_on_next) << 10; uint32_t stopbit =
(uint32_t)!!(last && !nostop) << 9; uint64_t tm = micros() + timeout_per_char_us; i2c->data_cmd = startbitnext | stopbit | 0x100; do { if (micros() > tm) { timeout = true; abort = true; } abort_reason = i2c->tx_abrt_source; // check tx abort bits if (i2c->raw_intr_stat & 0x40) { abort = true; i2c->clr_tx_abrt; } } while (!abort && !i2c_get_read_available(i2c)); if (abort) break; *dst++ = (uint8_t)i2c->data_cmd; } restart_on_next = nostop; if (abort) return i2c_handleAbort(i2c, timeout,
abort_reason); return byte_ctr; }

The first thing we have to do is make sure that there is space in the Tx FIFO buffer. Next we set the command register to the appropriate start and stop bits and attempt a read. To do this we have to loop until there is data in the FIFO – tested for by i2c_get_read_available. If this is successful then the data is in the data_cmd register and can be transferred to the buffer. However we still need to check for errors and timeout and report any status codes to the calling program.

The functions used are:

size_t i2c_get_write_available(I2C i2c)
{
    return I2C_TX_BUFFER_DEPTH - (i2c->txflr);
}
size_t i2c_get_read_available(I2C i2c)
{
    return i2c->rxflr;
}
uint64_t micros()
{
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
    uint64_t us = ts.tv_sec * 1000000 +
ts.tv_nsec / 1000; return us; }

We also need a global variable to keep track of when a stop bit needs to be sent:

bool restart_on_next = false;

This should be a per-controller variable, but for simplicity we assume that only one controller is active at any given time. If you need to break this rule then you need to store the state in an array with one location per I2C controller.



Last Updated ( Saturday, 19 July 2025 )