I thought that I would pass on some of the research that I’ve done on setting up two Pico MCUs to communicate with each other via I2C.
My use case is that I want to create a Pico based robot sensor that can communicate its findings with another Pico that will be controlling the behavior of the robot.
First off I found that the Pico C/ C++ SDK only has example code to set up a Pico as a master node and that the team that maintains the SDK would get around to adding a I2C slave node implementation as time permitted. Ouch. With a little digging I found the generous contribution of Valentin Milea on GitHub. His example code runs both the master and the slave nodes on a single Pico. After I got that running and reviewed all his code, I wired up two Picos, split his code into the bit that runs that master node and the bit that runs the slave node and I’m off to the races… I can post the programs if anyone wants to see it.
Here are some useful links for anyone interested.
https://github.com/vmilea/pico_i2c_slave
https://www.analog.com/en/technical-articles/i2c-primer-what-is-i2c-part-1.html
Tom
To err is human.
To really foul up, use a computer.
@thrandell Great information Tom, thanks.
First computer 1959. Retired from my own computer company 2004.
Hardware - Expert in 1401, 360, fairly knowledge in PC plus numerous MPU's & MCU's
Major Languages - Machine language, 360 Macro Assembler, Intel Assembler, PL/I and PL1, Pascal, Basic, C plus numerous job control and scripting languages.
My personal scorecard is now 1 PC hardware fix (circa 1982), 1 open source fix (at age 82), and 2 zero day bugs in a major OS.
If its just 2 pico communicating with each other and alternative that may be worth considering is just to use simple serial UART comms. Search for 'Hello from Northumberland' on this forum for an example, but it was a python example so may not be of interest to many.
As a follow up to setting up i2c between two Pico MCUs I’ve upload some basic functionality code that I got to work.
In the hopes of shortening the wires used for the i2c connection between the two Picos I thought that I’d stack the two MCUs and use the same GPIOs on both the master and the slave. Well, after countless attempts I couldn’t get that to work. As I learn more about the software I may find a way to use the same GPIO pins on both micro-controllers, but I’m moving forward with what I’ve got working. The wires are still pretty short and haven’t presented any problems. Here is a pix of the proto-boards on my project. I’m using GPIO pins 10 and 11 on the main board and GPIO 12 and 13 on the sensor board. I also wired a connection between two GND pins.
I noticed that the SDK documentation states that only a baud rate of 100kHz is supported, but the i2c hardware vendor’s documentation says that it will support Fast mode Plus speeds. I also noticed some comments in the SDK code that say things like ‘always use Fast mode’ so I tried it and it worked! The software supports 400kHz!
I added the i2c code to the sensor program I’ve been working on and the way that the Pico can handle all the interrupts is pretty impressive. There is an interrupt to control the patterns emitted by the IEDs and eight GPIO interrupts for the IR receivers and now this i2c interrupt. Wild.
Here is the code that runs on the slave Pico
/* * Copyright (c) 2021 Valentin Milea <valentin.milea@gmail.com> * * SPDX-License-Identifier: MIT */ #include <i2c_fifo.h> #include <i2c_slave.h> #include <pico/stdlib.h> #include <stdio.h> #include <string.h> static const uint I2C_SLAVE_ADDRESS = 0x17; static const uint I2C_BAUDRATE = 400000; // 100 kHz static const uint I2C_SLAVE_SDA_PIN = 12; static const uint I2C_SLAVE_SCL_PIN = 13; // The slave implements a 256 byte memory. To write a series of bytes, the master first // writes the memory address, followed by the data. The address is automatically incremented // for each byte transferred, looping back to 0 upon reaching the end. Reading is done // sequentially from the current memory address. static struct { uint8_t mem[256]; uint8_t mem_address; bool mem_address_written; } context; // Our handler is called from the I2C ISR, so it must complete quickly. Blocking calls / // printing to stdio may interfere with interrupt handling. static void i2c_slave_handler(i2c_inst_t *i2c, i2c_slave_event_t event) { switch (event) { case I2C_SLAVE_RECEIVE: // master has written some data if (!context.mem_address_written) { // writes always start with the memory address context.mem_address = i2c_read_byte(i2c); context.mem_address_written = true; } else { // save into memory context.mem[context.mem_address] = i2c_read_byte(i2c); context.mem_address++; } break; case I2C_SLAVE_REQUEST: // master is requesting data // load from memory i2c_write_byte(i2c, context.mem[context.mem_address]); context.mem_address++; break; case I2C_SLAVE_FINISH: // master has signalled Stop / Restart context.mem_address_written = false; break; default: break; } } static void setup_slave() { gpio_init(I2C_SLAVE_SDA_PIN); gpio_set_function(I2C_SLAVE_SDA_PIN, GPIO_FUNC_I2C); gpio_pull_up(I2C_SLAVE_SDA_PIN); gpio_init(I2C_SLAVE_SCL_PIN); gpio_set_function(I2C_SLAVE_SCL_PIN, GPIO_FUNC_I2C); gpio_pull_up(I2C_SLAVE_SCL_PIN); i2c_init(i2c0, I2C_BAUDRATE); // configure I2C0 for slave mode i2c_slave_init(i2c0, I2C_SLAVE_ADDRESS, &i2c_slave_handler); } static void run_slave() { uint64_t waitTime; while(true) { for(uint8_t x = 0;x < 256;x++) { waitTime = time_us_64() + 250000; // 4 times per second context.mem[0] = x; //printf("slave is waiting.\n"); while (time_us_64() < waitTime) {} } } } int main() { stdio_init_all(); puts("\nI2C slave example"); setup_slave(); run_slave(); }
This runs on the master Pico
/* * Big thank you to Valentin Milea <valentin.milea@gmail.com> * */ #include <i2c_fifo.h> #include <i2c_slave.h> #include <pico/stdlib.h> #include <stdio.h> #include <string.h> static const uint I2C_SLAVE_ADDRESS = 0x17; static const uint I2C_BAUDRATE = 400000; // 100 kHz static const uint I2C_MASTER_SDA_PIN = 10; // slave sda pin 12; static const uint I2C_MASTER_SCL_PIN = 11; // slave scl pin 13; static void run_master() { gpio_init(I2C_MASTER_SDA_PIN); gpio_set_function(I2C_MASTER_SDA_PIN, GPIO_FUNC_I2C); gpio_pull_up(I2C_MASTER_SDA_PIN); gpio_init(I2C_MASTER_SCL_PIN); gpio_set_function(I2C_MASTER_SCL_PIN, GPIO_FUNC_I2C); gpio_pull_up(I2C_MASTER_SCL_PIN); uint realRate = i2c_init(i2c1, I2C_BAUDRATE); printf("Requested baud rate was %d actual rate is %d\n", I2C_BAUDRATE, realRate); // actual = 399361! while (true) { uint8_t mem_address = 0; uint8_t buf; //seek to mem_address int count = i2c_write_blocking(i2c1, I2C_SLAVE_ADDRESS, &mem_address, 1, true); if (count < 0) { puts("Couldn't write to slave, please check your wiring!"); return; } if (count != 1) { printf("Count returned from write is %d, exiting.\n", count); return; } count = i2c_read_blocking(i2c1, I2C_SLAVE_ADDRESS, &buf, 1, false); if (count != 1) { printf("Count returned from read is %d, exiting.\n", count); return; } printf("Read at memory address 0 returned %d\n", buf); puts(""); sleep_ms(1000); } } int main() { stdio_init_all(); for (int x = 0; x < 5; x++) { sleep_ms(2000); printf("I2C master waiting to start, cnt = %d\n", x); } run_master(); }
Tom
To err is human.
To really foul up, use a computer.