2

I'm using the C/C++ SDK of the Pi Pico and trying to use the DMA to read I2C data in the background. However, there is no example script in Pico-Examples that shows how to use the DMA to read from I2C. There is one for SPI, called spi-dma. But It doesn't directly correlate to I2C because I have to give the device address too along with the register address for I2C.

Can anyone help me understand what to change in the following lines for it to work with an I2C device?

const uint dma_rx = dma_claim_unused_channel(true);

static uint8_t rxbuf[1024];

dma_channel_config c = dma_channel_get_default_config(dma_rx);

channel_config_set_transfer_data_size(&c, DMA_SIZE_8);
channel_config_set_dreq(&c, spi_get_dreq(spi_default, false));
channel_config_set_read_increment(&c, false);
channel_config_set_write_increment(&c, true);
dma_channel_configure(dma_rx, &c,
                      rxbuf, // write address
                      &spi_get_hw(spi_default)->dr, // read address
                      TEST_SIZE, // element count (each element is of size transfer_data_size)
                      false); // don't start yet

dma_start_channel_mask(1u << dma_rx);
dma_channel_wait_for_finish_blocking(dma_rx);
dma_channel_unclaim(dma_rx);

I Know a few changes to be made like

channel_config_set_dreq(&c, i2c_get_dreq(i2c_default, false));
dma_channel_configure(dma_rx, &c,
                     rxbuf, // write address
                     i2c_get_hw(i2c_default), // read address
                     TEST_SIZE, // element count (each element is of size transfer_data_size)
                     true); // don't start yet

But what more after this?

SKrish
  • 43
  • 6
  • 2
    DMA for I²C almost always creates a **worse** performance since charging DMA transfer takes a lot of CPU cycles anyway and I²C messages usually are too short (a few to several bytes). – 0andriy Mar 14 '22 at 13:30
  • But isn't that just for the initial few clock cycles? DMA should reduce the entire sequence of peripheral to CPU clock cycles and just have 1 cycle from memory to CPU, right? – SKrish Mar 16 '22 at 03:09
  • 1
    @0andriy "usually" - sure. But there are many cases in which larger writes are necessary and i2c an be very useful: like audio codecs, displays – s-ol May 25 '22 at 08:22
  • 2
    No, even for those case the I2C is an auxiliary interface with low trafic. What you can point out is HID protocol, but even there it’s compensated by FIFO in the controller. Note, the lesser FIFO is the uselessness of DMA is higher (DMA also uses device, I.e. host controller, FIFO and needs much more bursts in case of little FIFO or none, also each burst better to be aligned according to the architecture constraints, unaligned bursts are too slow. – 0andriy May 25 '22 at 08:33

1 Answers1

2

Don't like the SPI protocol, when you read from an I2C device, you also need to send some register address to the device.

There are some helper functions to illustrate the process.

long long rx_ind = 0;
uint16_t rx_buf[300];
static int gi2c_read_blocking_internal(i2c_inst_t *i2c, uint8_t addr, uint8_t *dst, size_t len, bool nostop,
                               check_timeout_fn timeout_check, timeout_state_t *ts) {
    invalid_params_if(I2C, addr >= 0x80); // 7-bit addresses
    invalid_params_if(I2C, i2c_reserved_addr(addr));
    invalid_params_if(I2C, len == 0);
    invalid_params_if(I2C, ((int)len) < 0);
    int kk = rx_ind;
    i2c->hw->enable = 0;
    i2c->hw->tar = addr;
    i2c->hw->enable = 1;

    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))
            tight_loop_contents();
        rx_buf[rx_ind++] =
                bool_to_bit(first && i2c->restart_on_next) << I2C_IC_DATA_CMD_RESTART_LSB |
                bool_to_bit(last && !nostop) << I2C_IC_DATA_CMD_STOP_LSB |
                I2C_IC_DATA_CMD_CMD_BITS;
        i2c->hw->data_cmd =
                bool_to_bit(first && i2c->restart_on_next) << I2C_IC_DATA_CMD_RESTART_LSB |
                bool_to_bit(last && !nostop) << I2C_IC_DATA_CMD_STOP_LSB |
                I2C_IC_DATA_CMD_CMD_BITS; // -> 1 for read

        do {
            abort_reason = i2c->hw->tx_abrt_source;
            abort = (bool) i2c->hw->clr_tx_abrt;
            if (timeout_check) {
                timeout = timeout_check(ts);
                abort |= timeout;
            }
        } while (!abort && !i2c_get_read_available(i2c));

        if (abort)
            break;

        *dst++ = (uint8_t) i2c->hw->data_cmd;
        rx_buf[rx_ind++] = *(dst-1);
    }

    int rval;

    if (abort) {
        if (timeout)
            rval = PICO_ERROR_TIMEOUT;
        else if (!abort_reason || abort_reason & I2C_IC_TX_ABRT_SOURCE_ABRT_7B_ADDR_NOACK_BITS) {
            // No reported errors - seems to happen if there is nothing connected to the bus.
            // Address byte not acknowledged
            rval = PICO_ERROR_GENERIC;
        } else {
//            panic("Unknown abort from I2C instance @%08x: %08x\n", (uint32_t) i2c->hw, abort_reason);
            rval = PICO_ERROR_GENERIC;
        }
    } else {
        rval = byte_ctr;
    }

    i2c->restart_on_next = nostop;
    do{
        printf("rx %#08x\n", rx_buf[kk++]);
    } while (rx_ind > kk);
    return rval;
}

long long tx_ind = 0;
uint16_t tx_buf[300];
static int gi2c_write_blocking_internal(i2c_inst_t *i2c, uint8_t addr, const uint8_t *src, size_t len, bool nostop,
                                       check_timeout_fn timeout_check, struct timeout_state *ts) {
    invalid_params_if(I2C, addr >= 0x80); // 7-bit addresses
    invalid_params_if(I2C, i2c_reserved_addr(addr));
    // Synopsys hw accepts start/stop flags alongside data items in the same
    // FIFO word, so no 0 byte transfers.
    invalid_params_if(I2C, len == 0);
    invalid_params_if(I2C, ((int)len) < 0);
    int kk = tx_ind;
    i2c->hw->enable = 0;
    i2c->hw->tar = addr;
    i2c->hw->enable = 1;

    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;
        tx_buf[tx_ind++] =
                bool_to_bit(first && i2c->restart_on_next) << I2C_IC_DATA_CMD_RESTART_LSB |
                bool_to_bit(last && !nostop) << I2C_IC_DATA_CMD_STOP_LSB |
                *src;
        i2c->hw->data_cmd =
                bool_to_bit(first && i2c->restart_on_next) << I2C_IC_DATA_CMD_RESTART_LSB |
                bool_to_bit(last && !nostop) << I2C_IC_DATA_CMD_STOP_LSB |
                *src++;

        // Wait until the transmission of the address/data from the internal
        // shift register has completed. For this to function correctly, the
        // TX_EMPTY_CTRL flag in IC_CON must be set. The TX_EMPTY_CTRL flag
        // was set in i2c_init.
        do {
            if (timeout_check) {
                timeout = timeout_check(ts);
                abort |= timeout;
            }
            tight_loop_contents();
        } while (!timeout && !(i2c->hw->raw_intr_stat & I2C_IC_RAW_INTR_STAT_TX_EMPTY_BITS));

        // If there was a timeout, don't attempt to do anything else.
        if (!timeout) {
            abort_reason = i2c->hw->tx_abrt_source;
            if (abort_reason) {
                // Note clearing the abort flag also clears the reason, and
                // this instance of flag is clear-on-read! Note also the
                // IC_CLR_TX_ABRT register always reads as 0.
                i2c->hw->clr_tx_abrt;
                abort = true;
            }

            if (abort || (last && !nostop)) {
                // If the transaction was aborted or if it completed
                // successfully wait until the STOP condition has occured.

                // TODO Could there be an abort while waiting for the STOP
                // condition here? If so, additional code would be needed here
                // to take care of the abort.
                do {
                    if (timeout_check) {
                        timeout = timeout_check(ts);
                        abort |= timeout;
                    }
                    tight_loop_contents();
                } while (!timeout && !(i2c->hw->raw_intr_stat & I2C_IC_RAW_INTR_STAT_STOP_DET_BITS));

                // If there was a timeout, don't attempt to do anything else.
                if (!timeout) {
                    i2c->hw->clr_stop_det;
                }
            }
        }

        // Note the hardware issues a STOP automatically on an abort condition.
        // Note also the hardware clears RX FIFO as well as TX on abort,
        // because we set hwparam IC_AVOID_RX_FIFO_FLUSH_ON_TX_ABRT to 0.
        if (abort)
            break;
    }

    int rval;

    // A lot of things could have just happened due to the ingenious and
    // creative design of I2C. Try to figure things out.
    if (abort) {
        if (timeout)
            rval = PICO_ERROR_TIMEOUT;
        else if (!abort_reason || abort_reason & I2C_IC_TX_ABRT_SOURCE_ABRT_7B_ADDR_NOACK_BITS) {
            // No reported errors - seems to happen if there is nothing connected to the bus.
            // Address byte not acknowledged
            rval = PICO_ERROR_GENERIC;
        } else if (abort_reason & I2C_IC_TX_ABRT_SOURCE_ABRT_TXDATA_NOACK_BITS) {
            // Address acknowledged, some data not acknowledged
            rval = byte_ctr;
        } else {
            //panic("Unknown abort from I2C instance @%08x: %08x\n", (uint32_t) i2c->hw, abort_reason);
            rval = PICO_ERROR_GENERIC;
        }
    } else {
        rval = byte_ctr;
    }

    // nostop means we are now at the end of a *message* but not the end of a *transfer*
    i2c->restart_on_next = nostop;

    do{
        printf("tx %#08x\n", tx_buf[kk++]);
    } while (tx_ind > kk);
    return rval;
}
int gi2c_write_blocking(i2c_inst_t *i2c, uint8_t addr, const uint8_t *src, size_t len, bool nostop) {
    return gi2c_write_blocking_internal(i2c, addr, src, len, nostop, NULL, NULL);
}
int gi2c_read_blocking(i2c_inst_t *i2c, uint8_t addr, uint8_t *dst, size_t len, bool nostop) {
    return gi2c_read_blocking_internal(i2c, addr, dst, len, nostop, NULL, NULL);
}

// Write 1 byte to the specified register
int reg_write(  i2c_inst_t *i2c,
                const uint addr,
                const uint8_t reg,
                uint8_t *buf,
                const uint8_t nbytes) {

    int num_bytes_read = 0;
    uint8_t msg[nbytes + 1];

    // Check to make sure caller is sending 1 or more bytes
    if (nbytes < 1) {
        return 0;
    }

    // Append register address to front of data packet
    msg[0] = reg;
    for (int i = 0; i < nbytes; i++) {
        msg[i + 1] = buf[i];
    }

    printf("write func.\n");
    // Write data to register(s) over I2C
    gi2c_write_blocking(i2c, addr, msg, (nbytes + 1), false);

    return num_bytes_read;
}

// Read byte(s) from specified register. If nbytes > 1, read from consecutive
// registers.
int reg_read(  i2c_inst_t *i2c,
                const uint addr,
                const uint8_t reg,
                uint8_t *buf,
                const uint8_t nbytes) {

    int num_bytes_read = 0;

    // Check to make sure caller is asking for 1 or more bytes
    if (nbytes < 1) {
        return 0;
    }
    printf("write func.\n");
    // Read data from register(s) over I2C
    gi2c_write_blocking(i2c, addr, &reg, 1, true);
    printf("read func.\n");
    num_bytes_read = gi2c_read_blocking(i2c, addr, buf, nbytes, false);

    return num_bytes_read;
}

In gi2c_read_blocking_internal function, you can see first you need to write the i2c->hw->data_cmd to control the stop, restart... states in I2C protocol, and then you can read from i2c->hw->data_cmd to get information from I2C device.

In reg_read function, first send the register address by call gi2c_write_blocking, and then you can read the register by call gi2c_read_blocking

After look up the whole process, you need two dma channel trigger simultaneously to simulate the reading from I2C device process(one for write i2c->hw->data_cmd and one for read i2c->hw->data_cmd).

apoptosis
  • 61
  • 5