pico-examples icon indicating copy to clipboard operation
pico-examples copied to clipboard

RP2040 DMA I2C?

Open nsk126 opened this issue 4 years ago • 20 comments

Is there any script/functions to use RP2040 DMA to read the I2C bus? Like how this script reads the SPI using DMA?

nsk126 avatar Mar 07 '22 18:03 nsk126

This isn't something I've tried doing myself, but https://github.com/raspberrypi/pico-sdk/releases/tag/1.3.0 says "Added i2c_get_dreq() function to facilitate configuring DMA transfers to/from an I2C instance." (added in https://github.com/raspberrypi/pico-sdk/pull/603 ), so I guess in theory adding DMA to I2C shouldn't be that different from adding DMA to SPI? :shrug: The best place to ask for help might be https://forums.raspberrypi.com/viewforum.php?f=143

lurch avatar Mar 10 '22 00:03 lurch

Trying to figure it out right now. I'm only able to use the SPI-DMA example as a reference to an extent as i need to somehow provide the I2C DMA instance from an address on the I2C bus to read from, which isnt the case for SPI.

nsk126 avatar Mar 12 '22 13:03 nsk126

i need to somehow provide the I2C DMA instance from an address on the I2C bus to read from

With the current I2C SDK functions, it looks like i2c_write_blocking_internal https://github.com/raspberrypi/pico-sdk/blob/master/src/rp2_common/hardware_i2c/i2c.c#L135 and i2c_read_blocking_internal https://github.com/raspberrypi/pico-sdk/blob/master/src/rp2_common/hardware_i2c/i2c.c#L260 are the only functions that set the device-address, and if you're using DMA I guess you probably don't want to use the blocking functions! So I guess for now you might need to study Section 4.3.15 of https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf and bypass the SDK wrapper-functions and write directly to the I2C registers https://github.com/raspberrypi/pico-sdk/blob/master/src/rp2040/hardware_structs/include/hardware/structs/i2c.h ?

@kilograham Is this a "gap" in the hardware_i2c part of the SDK?

Another alternative would be to use the existing SDK functions to read the I2C bus in a blocking manner, but running on the second core?

lurch avatar Mar 13 '22 03:03 lurch

Another alternative would be to use the existing SDK functions to read the I2C bus in a blocking manner, but running on the second core?

That is a possible way. But I'm trying to use both cores to do other tasks which require data from a sensor on the I2C bus. Hence, I thought DMA would solve this problem.

nsk126 avatar Mar 13 '22 05:03 nsk126

i need to somehow provide the I2C DMA instance from an address on the I2C bus to read from

With the current I2C SDK functions, it looks like i2c_write_blocking_internal https://github.com/raspberrypi/pico-sdk/blob/master/src/rp2_common/hardware_i2c/i2c.c#L135 and i2c_read_blocking_internal https://github.com/raspberrypi/pico-sdk/blob/master/src/rp2_common/hardware_i2c/i2c.c#L260 are the only functions that set the device-address, and if you're using DMA I guess you probably don't want to use the blocking functions! So I guess for now you might need to study Section 4.3.15 of https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf and bypass the SDK wrapper-functions and write directly to the I2C registers https://github.com/raspberrypi/pico-sdk/blob/master/src/rp2040/hardware_structs/include/hardware/structs/i2c.h ?

@kilograham Is this a "gap" in the hardware_i2c part of the SDK?

Another alternative would be to use the existing SDK functions to read the I2C bus in a blocking manner, but running on the second core?

There is i2c_set_slave_mode() which absolutely lets you set the address. You need to configure the I2C before you do the DMA which just transfers data.

kilograham avatar Mar 13 '22 19:03 kilograham

I got the impression that @nsk126 was trying to use the RP2040 as the I2C master, reading data from an external I2C sensor? :shrug:

lurch avatar Mar 13 '22 21:03 lurch

@lurch Yes, I am. @kilograham I was using i2c_set_slave_mode(), but still getting it wrong somewhere.

int rx_1 = dma_claim_unused_channel(true);
uint8_t addr = 0x68;
uint8_t reg = 0x75;
uint8_t rx_buff[1];
i2c_set_slave_mode(i2c_default,false,addr);

dma_channel_config c = dma_channel_get_default_config(rx_1);
channel_config_set_transfer_data_size(&c, DMA_SIZE_8);
channel_config_set_read_increment(&c, false);
channel_config_set_write_increment(&c, true);
channel_config_set_dreq(&c, i2c_get_dreq(i2c_default, false));

dma_channel_configure(rx_1, &c,
                      rx_buff, // read address
                      &reg, // write address
                      1, // element count (each element is of size transfer_data_size)
                      true); // don't start yet

I assumed setting the slave arg in i2c_set_slave_mode() false should make it master mode.

nsk126 avatar Mar 14 '22 00:03 nsk126

i2c_set_slave_mode() https://github.com/raspberrypi/pico-sdk/blob/master/src/rp2_common/hardware_i2c/i2c.c#L114 only seems to set the RP2040 slave-address register (i2c->hw->sar) when slave-mode is true, it doesn't set the external-device-address register (i2c->hw->tar) when slave-mode is false. Which agrees with what it says at https://raspberrypi.github.io/pico-sdk-doxygen/group__hardware__i2c.html#ga42571b2db5d1ed0dc083385c230dc9e8

lurch avatar Mar 14 '22 00:03 lurch

I tried to set the i2c->hw->tar register manually. And instead tried to read from the IC_DATA_CMD register. I set the data size to DMA_SIZE_16 since the data_cmd register is also 16 bits(hope this is the correct way). Not sure what's wrong but I'm getting a 0x18 reading, instead of a 0x68.

For context, I'm trying to read the 0x75 register of the MPU-6050(WHOAMI register) to test the DMA transfers. It should have an 8bit value 0x68.

int rx_1 = dma_claim_unused_channel(true);
uint8_t addr = 0x68;
uint8_t reg = 0x75;
uint16_t rx_buff[1];
i2c_set_slave_mode(i2c_default,false,addr);

dma_channel_config c = dma_channel_get_default_config(rx_1);
channel_config_set_transfer_data_size(&c, DMA_SIZE_16);
channel_config_set_read_increment(&c, false);
channel_config_set_write_increment(&c, true);
channel_config_set_dreq(&c, i2c_get_dreq(i2c_default, false));

i2c0->hw->enable = 0;
i2c0->hw->tar = reg;
i2c0->hw->enable = 1;

dma_channel_configure(rx_1, &c,
                      rx_buff, // read address
                      &i2c_default->hw->data_cmd, // write address
                      1, // element count (each element is of size transfer_data_size)
                      true); // don't start yet
   
while (1)
{
    uint8_t reg8 = rx_buff[0] >> 8;
    printf("0x%x\r\n",reg8);

    sleep_ms(500);
}

dma_channel_unclaim(rx_1);

nsk126 avatar Mar 14 '22 01:03 nsk126

From my limited understanding, I think i2c0->hw->tar = reg; should be i2c0->hw->tar = addr; ?

lurch avatar Mar 14 '22 03:03 lurch

Shouldn't i2c0->hw->sar be the slave address and i2c0->hw->tar be the target register address to read/write from? That is what I understand from tar and sar. Nonetheless, I've tried changing i2c0->hw->tar = reg and even swapping addr and reg. Nothing has changed, reg8 still contains a value 0x18.

nsk126 avatar Mar 14 '22 04:03 nsk126

My understanding is largely from looking at the hardware_i2c/i2c.c file that I mentioned earlier. You can see that i2c0->hw->sar is only used inside i2c_set_slave_mode when slave is true, and i2c0->hw->tar is used inside i2c_write_blocking_internal and i2c_read_blocking_internal (which are used when the RP2040 is being used as the !2C master).

https://github.com/raspberrypi/pico-examples/blob/master/i2c/mpu6050_i2c/mpu6050_i2c.c shows how to talk to the MPU-6050 using the "normal" I2C SDK functions, so my assumption (which could be wrong) is that you need to replace i2c_write_blocking (which is what sends the reg value?) with DMA writes, and replace the i2c_read_blocking with DMA reads? However this is about as far as my I2C knowledge goes, so you may need to ask on the forums for further help.

lurch avatar Mar 14 '22 11:03 lurch

Thanks for the tip. I didn't see the source code for i2c_set_slave_mode.

you need to replace i2c_write_blocking (which is what sends the reg value?) with DMA writes, and replace the i2c_read_blocking with DMA reads?

I tried working on this today. Used 2 channels, one to read and one to write to the target register.

int rx_1 = dma_claim_unused_channel(true);
int rx_2 = dma_claim_unused_channel(true);

uint8_t addr = 0x68;
uint8_t reg = 0x75;
uint16_t rx_buff[1];
// i2c_set_slave_mode(i2c_default,false,reg);

dma_channel_config c = dma_channel_get_default_config(rx_1);
channel_config_set_transfer_data_size(&c, DMA_SIZE_8);
channel_config_set_read_increment(&c, true);
channel_config_set_write_increment(&c, false);
channel_config_set_dreq(&c, i2c_get_dreq(i2c_default, true));

i2c_default->hw->enable = 0;
i2c_default->hw->tar = addr;
i2c_default->hw->enable = 1;


dma_channel_configure(rx_1, &c,
                      &i2c_default->hw->data_cmd, 
                      &reg, 
                      1, 
                      false);


c = dma_channel_get_default_config(rx_2);
channel_config_set_transfer_data_size(&c, DMA_SIZE_16);
channel_config_set_read_increment(&c, false);
channel_config_set_write_increment(&c, true);
channel_config_set_dreq(&c, i2c_get_dreq(i2c_default, false));

i2c_default->hw->enable = 0;
i2c_default->hw->tar = addr;
i2c_default->hw->enable = 1;


dma_channel_configure(rx_2, &c,
                      rx_buff, 
                      &i2c_default->hw->data_cmd, 
                      1, 
                      false);
   
dma_start_channel_mask((1u << rx_1) | (1u << rx_2));

dma_channel_unclaim(rx_1);
dma_channel_unclaim(rx_2);


while (1)
{
    uint8_t reg8 = rx_buff[0] >> 8;
    printf("0x%x\r\n",reg8);

    sleep_ms(500);
}

But I'm not sure if I'm setting the dma_channel_configure(rx_1, &c,&i2c_default->hw->data_cmd,&reg,1,false); correctly. Not sure where to move from here. I don't think starting the channels at once would be an issue either. I tried asking at https://forums.raspberrypi.com/ but no one responded.

nsk126 avatar Mar 15 '22 03:03 nsk126

I've not written any DMA code myself, so can't really offer any guidance, but one thing that immediately jumps out at me is that you're unclaiming the DMA channels before your while loop, which doesn't seem right. https://raspberrypi.github.io/pico-sdk-doxygen/group__hardware__dma.html#gac50200739b88a2fd52316f4150533035

EDIT: Also, there's probably no need to set i2c_default->hw->tar twice?

lurch avatar Mar 15 '22 11:03 lurch

I stumbled into this today as well: how to read from i2c with dma.

Using the pico as a master and have it write to another i2c device with DMA is pretty straight forward: inflate the byte buffer you want to send to uint16 and set the appropriate control bits (like STOP or RESTART) on the first/last byte and then just DMA-write them to the i2c-command register. Works.

Having the Pico read from another device seems much harder. If you read i2c_read_blocking_internal() carefully and compare with the documentation, you'll see that for every byte you want to read you will have to write a control-command as well. So you'd have to ping-pong bounce between two 1-byte DMA transfers all the time: one writes the control command, the other reads the data, rince repeat. No straight forward bulk read afaict.

bruelltuete avatar Apr 06 '22 15:04 bruelltuete

[...] for every byte you want to read you will have to write a control-command as well. So you'd have to ping-pong bounce between two 1-byte DMA transfers all the time: one writes the control command, the other reads the data, rince repeat. No straight forward bulk read afaict.

https://github.com/raspberrypi/pico-examples/blob/master/dma/control_blocks/control_blocks.c should be a good example for how to wire this up. Haven't tried it yet myself...

bruelltuete avatar Apr 06 '22 16:04 bruelltuete

You just need to use two DMA channels in parallel (you don't need to ping pong). see https://github.com/kilograham/rp2040-doom/blob/c77136a24837a41fe1561778d4563d22679850fb/src/pico/piconet.c#L535

kilograham avatar Apr 06 '22 18:04 kilograham

You just need to use two DMA channels in parallel (you don't need to ping pong). see https://github.com/kilograham/rp2040-doom/blob/c77136a24837a41fe1561778d4563d22679850fb/src/pico/piconet.c#L535

hm... for some reason i thought we'd have to coordinate access to i2c's data_cmd register. Synchronise via the DMA controller. But I guess there already is inherent sync: the write channel puts read-more-data control words into data_cmd, paced by DREQ_I2C0_TX; and the read channel tries to read as fast as DREQ_I2C0_RX allows; and concurrent read/write is coordinated by the bus and allows only one at a time.

bruelltuete avatar Apr 07 '22 15:04 bruelltuete

It's likely the wrong place to ask here, but if at all possible could this be updated in the datasheet of the rp2040? The datasheet says nothing about any of this as far as I can see. The way the i2c and the DMA interact is fundamentally a hardware thing and having at least some guidance on how to read write with DMA sounds quite useful

Phylex avatar Mar 11 '24 17:03 Phylex

Sounds like a reasonable idea. Can you open an issue on raspberrypi/pico-feedback and link it back to this issue?

aallan avatar Mar 11 '24 17:03 aallan