Home Swift UNIX C Assembly Go Web MCU Research Non-Tech

Choose I2C controller, SDA and SCL pins using C/C++ on Raspberry Pi Pico

2023-08-27 | MCU | #Words: 1374 | 中文原版

This blog begins with a description of the solution, and record my discovery process and the structure of the Pico I2C code ZhongUncle explored.

Prerequisite knowledge

First let me explain: Pico has two I2C, namely two sets of SDA and SCL. You can see this from the names in the pin diagram. For example, Pin 4 and Pin 5 in below are for I2C1, while the default Pin 6 and Pin 7 are for I2C0.

I2C0 and I2C1

By default, only the first I2C (I2C0) is enabled. It means, even if you modify the pin, but not in I2C0, it still doesn’t work.

How to choose which I2C controller, SDA and SCL pins

Declare three variables or macros to facilitate development. It is recommended to use macros, which is more fit the style of Raspberry Pi:

#define I2C				i2c0
#define I2C_SDA_PIN 	4
#define I2C_SCL_PIN 	5

If macro goes wrong, use variables.

Initializing I2C is setting which I2C controller, SDA and SCL pins to use. According to the above setting, the first I2C controller I2C0 is used here, SDA uses GP4, SCL uses GP5, and the frequency is 1000000:

i2c_init(I2C, 1000000);
gpio_set_function(I2C_SDA_PIN, GPIO_FUNC_I2C);
gpio_set_function(I2C_SCL_PIN, GPIO_FUNC_I2C);
gpio_pull_up(I2C_SDA_PIN);
gpio_pull_up(I2C_SCL_PIN);

Since there are two I2C controllers, you can use two sets of SDA and SCL pins at the same time, but they must be the pins of I2C0 and I2C1, not the same controller.

Discovery process (optional reading)

This part is not necessary to read. Here is a record of how I knew it was handled this way. I also learned about the structure and data transmission process, so that I don’t have to spend time looking through it if I need it in the future.

First try

First, I analyze the problem: To define a pin, I need to know how the value of the pin is used. And I will know how to transfer and process this value.

Generally, during initialization, you set which I2C controller to use and the SDA and SCL pins. The code is generally as follows:

i2c_init(i2c_default, CLK);
gpio_set_function(PICO_DEFAULT_I2C_SDA_PIN, GPIO_FUNC_I2C);
gpio_set_function(PICO_DEFAULT_I2C_SCL_PIN, GPIO_FUNC_I2C);
gpio_pull_up(PICO_DEFAULT_I2C_SDA_PIN);
gpio_pull_up(PICO_DEFAULT_I2C_SCL_PIN);

When I wrote Modify the default SDA and SCL pins of I2C using C/C++, I knew default pin set in pico.h. There are 3 variables about it, PICO_DEFAULT_I2C,PICO_DEFAULT_I2C_SDA_PIN and PICO_DEFAULT_I2C_SCL_PIN, so just tracing them can find the answer.

But it’s hard to find, there are too many references. So I tried to start from the other side: I2C is initialized through the i2c_init() function, as follows:

i2c_init(i2c_default, SSD1306_I2C_CLK);

I just need first parameter i2c_default, because this parameter passes some information. The second parameter uint baudrate is to pass the rate and has nothing about how pin work.

So what is the content of i2c_init()? Only knowing this, we can know what type returned from i2c_default and what processing is done internally.

i2c_init() is declared in pico-sdk/src/rp2_common/hardware_i2c/i2c.c. The declaration of function is:

uint i2c_init(i2c_inst_t *i2c, uint baudrate) {
    i2c_reset(i2c);
    i2c_unreset(i2c);
    i2c->restart_on_next = false;

    i2c->hw->enable = 0;

    ...
    
    // Re-sets i2c->hw->enable upon returning:
    return i2c_set_baudrate(i2c, baudrate);
}

So what type is i2c_inst_t? I keep looking for it.

In line 52 of pico-sdk/src/rp2_common/hardware_i2c/include/hardware/i2c.h, you can see i2c_inst_t is typedef of i2c_inst structure:

typedef struct i2c_inst i2c_inst_t;

Keeping looking for i2c_inst, this structure in line 135 at the same file:

struct i2c_inst {
    i2c_hw_t *hw;
    bool restart_on_next;
};

The end is variable i2c_hw_t *hw looked for at beginning, because only it may pass the value of the pin, so keep looking for what data returned from i2c_hw_t.

The declaration of this type is in pico-sdk/src/rp2040/hardware_structs/include/hardware/structs/i2c.h. In other words, this file is for i2c_hw_t structure:

the i2c_hw_t structure

This structure stores a lot of I2C information, but I still couldn’t find the pin information, so I went back to the beginning to search.

Second try

At first, I looked for the type i2c_inst_t of the first parameter of i2c_init(), but I didn’t find enough. But I haven’t found its value yet, so this time I start from the parameter value i2c_default. Where is this value defined?

The required things were found in pico-sdk/src/rp2_common/hardware_i2c/include/hardware/i2c.h where i2c_inst_t declaration and definition are in:

#ifdef PICO_DEFAULT_I2C_INSTANCE
#define i2c_default PICO_DEFAULT_I2C_INSTANCE
#endif

What is PICO_DEFAULT_I2C_INSTANCE? Look upward:

#if !defined(PICO_DEFAULT_I2C_INSTANCE) && defined(PICO_DEFAULT_I2C)
#define PICO_DEFAULT_I2C_INSTANCE (__CONCAT(i2c,PICO_DEFAULT_I2C))
#endif

Finally, I see a required value: PICO_DEFAULT_I2C. As before we know, this defaults to 0.

(__CONCAT(i2c,PICO_DEFAULT_I2C)) conjuncts the values ​​of i2c and PICO_DEFAULT_I2C, which is i2c0 by default. In other words, the parameter i2c_default is i2c0.

This skill is very good, but some compilers cannot use it. For example, when I use Clang x86_64-apple-darwin21.6.0, I cannot extend PICO_DEFAULT_I2C.

Go deeper

But what is i2c0 here? What type is it?

I2C HW Block 0 and 1

This is a part of code in the pico-sdk/src/rp2_common/hardware_i2c/include/hardware/i2c.h (as shown above):

#define i2c0 (&i2c0_inst) ///< Identifier for I2C HW Block 0
#define i2c1 (&i2c1_inst) ///< Identifier for I2C HW Block 1

You can see i2c0 is the address of i2c0_inst, and the comment says this is identifier for I2C HW Block 0.

extern i2c_inst_t i2c0_inst;
extern i2c_inst_t i2c1_inst;

From before, we can see i2c0_inst and i2c1_inst are external variables, and their type is i2c_inst_t. I have seen the structure defined before:

struct i2c_inst {
    i2c_hw_t *hw;
    bool restart_on_next;
};

Where is i2c0_inst declared?

It is in pico-sdk/src/rp2_common/hardware_i2c/i2c.c:

i2c_inst_t i2c0_inst = {i2c0_hw, false};
i2c_inst_t i2c1_inst = {i2c1_hw, false};

What is i2c0_hw? Where is it declare?

It is in pico-sdk/src/rp2040/hardware_structs/include/hardware/structs/i2c.h:

#define i2c0_hw ((i2c_hw_t *)I2C0_BASE)
#define i2c1_hw ((i2c_hw_t *)I2C1_BASE)

i2c0_hw represents ((i2c_hw_t *)I2C0_BASE), means I2C0_BASE is a pointer to i2c_hw_t, declared in pico-sdk/src/rp2040/hardware_regs/include/hardware/regs/addressmap.h:

#define I2C0_BASE _u(0x40044000)
#define I2C1_BASE _u(0x40048000)

In other words, I2C0_BASE is 0x40044000, and the address of i2c0_hw is 0x40044000.

In addition, _() here means unsigned integer, which is defined in pico-sdk/src/rp2040/hardware_regs/include/hardware/platform_defs:

#ifndef _u
#ifdef __ASSEMBLER__
#define _u(x) x
#else
#define _u(x) x ## u
#endif
#endif

I hope these will help someone in need~