RPi-Pico mit 5V-I2C-Modulen             

von Ralph Maas                       

Elektronik-Labor  Projekte  Mikrocontroller  Raspberry            




Motivation

Ich wollte ein Projekt, das ich mit dem Arduino begonnen hatte, mit dem RPi-Pico fortführen. Das über I2C angebundene Display läuft aber mit 5V, der RPi-Pico mit 3,3V. Ich benötige also einen „Level-Shifter“.  Zudem hat der RPi-Pico mehr als einen I2C-Anschluss, aber alle Beispiele die ich gefunden habe bezogen sich auf Micro-Python. Ich will das ganze aber in der Arduino-IDE machen. Das Ergebnis sind 3 Varianten von Scan-Programmen, um angeschlossene I2C-Module zu erkennen und viele Erfahrungen, wie I2C auf dem RPi-Pico funktioniert.
Angeblich kann man auf den Logic-Level-Converter verzichten, wenn das I2C-Modul keine Pull-Up-Widerstände nach 5V hat.
Bei mir ist das nicht der Fall und ich habe das daher nicht ausprobiert.

Schaltplan

Der I2C-Converter ist für jeden der 4 Kanäle mit einem MOSFET und zwei Widerständen aufgebaut, nicht isolierend wie im Schaltplan:



Code

Was mich auf den ersten Blick irritiert hat, ist dass die GPIO-PIN fest einer von zwei Hardware-Einheiten zugeordnet sind. Wenn man das falsch zuordnet, dann kann das zum Programmabsturz führen. Die Zuordnung ist im Pinot erklärt. GP4/5 sind die Default-PIN für die i2c0-Hardware. GP0/1 ist auch an i2c0, GP2/3 an der Hardware i2c1



Es folgen 3 Code-Beispiele, einmal für den Default-I2C, dann für einen beliebigen GPIO-Pin und zuletzt für einen dynamischen Scan über alle PIN.

Default I2C-PINs:

---

// Scan des I2C-Bus auf den DEFAULT-PIN 4 + 5
//
// Dieses Beispiel verwendet die Pico-Arduino Boarderweiterung von Earle Philhower (Version 2.6.3)
// nach Anregung des Buches "RPi Pico Schaltungen und Projekte" von Burkhard Kainka, Seite 5
// Einbindung über die Boardverwalter-URL https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
// unter "Datei | Voreinstellungen" in der Arduino-IDE
//
// Anregung von https://hutscape.com/tutorials/pico-i2c
// Teile der Dokumenation von https://raspberrypi.github.io/pico-sdk-doxygen/group__hardware__i2c.html
// und https://github.com/raspberrypi/pico-examples/blob/master/i2c/bus_scan/bus_scan.c
// Umgestellt auf setup/loop und Seriellen Monitor statt UART Schnittstelle
//

#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/i2c.h"

char Textausgabe[4]; // da sprintf einen Null-Wert anhängt, muss dieses Feld länger sein als die benötigte Textausgabe

bool reserved_addr(uint8_t addr) {
return (addr & 0x78) == 0 || (addr & 0x78) == 0x78; // Adressen der Form "000 0xxx" oder "111 1xxx" sind im I2C-Protokoll für Spezielle Zwecke vorgesehen
}

void setup() {
#if !defined(i2c_default) || !defined(PICO_DEFAULT_I2C_SDA_PIN) || !defined(PICO_DEFAULT_I2C_SCL_PIN)
#warning Keine DEFAULT_PIN für I2C definiert
#else
// Hier wird I2C0 mit den DEFAULT SDA und SCL pins (4, 5) verwendet
i2c_init(i2c_default, 100000);
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);

//stdio_init_all(); // Das wäre für UART
Serial.begin(115200);
}

void loop() {
Serial.println("5 Sekunden warten bis Anwender den Seriellen Monitor geöffnet hat.");
sleep_ms(5000);
//printf("\nI2C Bus Scan mit Default-PIN 4 und 5:\n");
//printf(" 0 1 2 3 4 5 6 7 8 9 A B C D E F\n");
Serial.println("I2C Bus Scan mit Default-PIN 4 und 5:");
Serial.println(" 0 1 2 3 4 5 6 7 8 9 A B C D E F");

for (int addr = 0; addr < (1 << 7); ++addr) {
if (addr % 16 == 0) {
// printf("%02x ", addr); // printf waren aus der Beispieldatei für UART
sprintf(Textausgabe, "%02x ", addr); // sprintf hat dieselben Parameter wie printf, plus das Textfeld in das reinkopiert wird
Serial.print(Textausgabe);
}

// Es wird versucht, 1 Byte zu lesen,
// wenn die Adresse exisitiert, wird die Anzahl von übertragenen Bytes zurückgegeben.
// wenn die Adresse nicht existiert, wird -1 zurückgegeben
int ret;
uint8_t rxdata;
if (reserved_addr(addr))
Serial.print("r ");
else {
ret = i2c_read_blocking(i2c_default, addr, &rxdata, 1, false);
Serial.print(ret < 0 ? ". " : "X ");
}
if (addr % 16 == 15) Serial.println("|");
}
}
#endif

/* So sieht die Ausgabe im Seriellen Monitor aus

I2C Bus Scan mit Default-PIN 4 und 5:
0 1 2 3 4 5 6 7 8 9 A B C D E F
00 r r r r r r r r . . . . . . . . |
10 . . . . . . . . . . . . . . . . |
20 . . . . . . . X . . . . . . . . |
30 . . . . . . . . . . . . . . . . |
40 . . . . . . . . . . . . . . . . |
50 . . . . . . . . . . . . . . . . |
60 . . . . . . . . . . . . . . . . |
70 . . . . . . . . r r r r r r r r |
5 Sekunden warten bis Anwender den Seriellen Monitor geöffnet hat.

Einzelne Adressen findet man auf: https://i2cdevices.org/addresses

*/

---------------

Beliebige I2C-PINs:

---

// Scan des I2C-Bus auf anderen als den DEFAULT-PIN 4 + 5
//
// Dieses Beispiel verwendet die Pico-Arduino Boarderweiterung von Earle Philhower (Version 2.6.3)
// nach Anregung des Buches "RPi Pico Schaltungen und Projekte" von Burkhard Kainka, Seite 5
// Einbindung über die Boardverwalter-URL https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
// unter "Datei | Voreinstellungen" in der Arduino-IDE
//
// Anregung von https://hutscape.com/tutorials/pico-i2c
// Teile der Dokumenation von https://raspberrypi.github.io/pico-sdk-doxygen/group__hardware__i2c.html
// und https://github.com/raspberrypi/pico-examples/blob/master/i2c/bus_scan/bus_scan.c
// Umgestellt auf setup/loop und Seriellen Monitor statt UART Schnittstelle
//
// Viele Erklärungen zu I2C in
// /home/username/.arduino15/packages/rp2040/hardware/rp2040/2.6.3/pico-sdk/src/rp2_common/hardware_i2c/include/hardware/i2c.h
// ==> hier sind i2c0 und i2c_default definiert
// dort wird auch eingebunden
// /home/username/.arduino15/packages/rp2040/hardware/rp2040/2.6.3/pico-sdk/src/boards/include/boards/pico.h
// ==> Da ist die Definition von
//#define PICO_DEFAULT_I2C 0
//#define PICO_DEFAULT_I2C_SDA_PIN 4
//#define PICO_DEFAULT_I2C_SCL_PIN 5
//
// In der Datei
// /home/username/.arduino15/packages/rp2040/hardware/rp2040/2.6.3/pico-sdk/src/common/pico_stdlib/include/pico/stdlib.h
// wird eingebunden
// /home/username/.arduino15/packages/rp2040/hardware/rp2040/2.6.3/pico-sdk/src/rp2_common/hardware_gpio/include/hardware/gpio.h
// ==> da sind die Pinout in Tabellenform, die erklären welche PIN für i2c0 und welche für i2c1 verwendet werden können
//

#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/i2c.h"

// Damit das zusammen passt, hier hintereinander die Definitionen
#define I2C_HW i2c1 // Alternativ i2c0 oder i2c1
#define PIN_SDA 2 // mit i2c0 geht: 0, 4, 8, 12, 16, 20, 24, 28
// i2c1 geht: 2, 6, 10, 14, 18, 22, 26
#define PIN_SCL 3 // SCL-pin immer eine Nummer höher als SDA, also 1, 3, 5,...

char Textausgabe[4]; // da sprintf einen Null-Wert anhängt, muss dieses Feld länger sein als die benötigte Textausgabe

bool reserved_addr(uint8_t addr) {
return (addr & 0x78) == 0 || (addr & 0x78) == 0x78; // Adressen der Form "000 0xxx" oder "111 1xxx" sind im I2C-Protokoll für Spezielle Zwecke vorgesehen
}

void setup() {
i2c_init(I2C_HW , 100000);
gpio_set_function(PIN_SDA, GPIO_FUNC_I2C);
gpio_set_function(PIN_SCL, GPIO_FUNC_I2C);
gpio_pull_up(PIN_SDA);
gpio_pull_up(PIN_SCL);

//stdio_init_all(); // Das wäre für UART
Serial.begin(115200);
}

void loop() {
Serial.println("5 Sekunden warten bis Anwender den Seriellen Monitor geöffnet hat.");
sleep_ms(5000);

Serial.print("I2C Bus Scan mit PIN SDA=");
Serial.print(PIN_SDA);
Serial.print(" und SCL=");
Serial.print(PIN_SCL);
Serial.print(" auf I2C-Hardware ");
Serial.println(i2c_hw_index(I2C_HW)); // Verwandelt i2c0 in "0" bzw. i2c1 in "1"
Serial.println(" 0 1 2 3 4 5 6 7 8 9 A B C D E F");

for (int addr = 0; addr < (1 << 7); ++addr) {
if (addr % 16 == 0) {
// printf("%02x ", addr); // printf waren aus der Beispieldatei für UART
sprintf(Textausgabe, "%02x ", addr); // sprintf hat dieselben Parameter wie printf, plus das Textfeld in das reinkopiert wird
Serial.print(Textausgabe);
}
int ret;
uint8_t rxdata;
if (reserved_addr(addr))
Serial.print("r ");
else { // Es wird versucht, 1 Byte zu lesen,
ret = i2c_read_blocking(I2C_HW, addr, &rxdata, 1, false); // wenn die Adresse exisitiert, wird die Anzahl von übertragenen Bytes zurückgegeben.
Serial.print(ret < 0 ? ". " : "X "); // wenn die Adresse nicht existiert, wird -1 zurückgegeben
}
if (addr % 16 == 15) Serial.println("|");
}
}

/* So sieht die Ausgabe im Seriellen Monitor aus

I2C Bus Scan mit PIN SDA=2 und SCL=3 auf I2C-Hardware 1
0 1 2 3 4 5 6 7 8 9 A B C D E F
00 r r r r r r r r . . . . . . . . |
10 . . . . . . . . . . . . . . . . |
20 . . . . . . . X . . . . . . . . |
30 . . . . . . . . . . . . . . . . |
40 . . . . . . . . . . . . . . . . |
50 . . . . . . . . . . . . . . . . |
60 . . . . . . . . . . . . . . . . |
70 . . . . . . . . r r r r r r r r |
5 Sekunden warten bis Anwender den Seriellen Monitor geöffnet hat.


Einzelne Adressen findet man auf: https://i2cdevices.org/addresses

Falsche Kombi von PIN und i2c0 bzw. i2c1 führen zu

I2C Bus Scan mit PIN SDA=2 und SCL=3 auf I2C-Hardware 0
0 1 2 3 4 5 6 7 8 9 A B C D E F
00 r r r r r r r r

==> Schon der erste Versuch eine Adresse zu lesen führt zum Absturz

*/

---------------

Alle I2C-PINs:

---

// Scan des I2C-Bus auf dynamisch wechselnden GPIO --> über alle möglichen GPIO
//
// Dieses Beispiel verwendet die Pico-Arduino Boarderweiterung von Earle Philhower (Version 2.6.3)
// nach Anregung des Buches "RPi Pico Schaltungen und Projekte" von Burkhard Kainka, Seite 5
// Einbindung über die Boardverwalter-URL https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
// unter "Datei | Voreinstellungen" in der Arduino-IDE
//

#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/i2c.h"

char Textausgabe[4]; // da sprintf einen Null-Wert anhängt, muss dieses Feld länger sein als die benötigte Textausgabe

bool reserved_addr(uint8_t addr) {
return (addr & 0x78) == 0 || (addr & 0x78) == 0x78; // Adressen der Form "000 0xxx" oder "111 1xxx" sind im I2C-Protokoll für Spezielle Zwecke vorgesehen
}

void setup() {
Serial.begin(115200);
}

void loop() {
Serial.println("5 Sekunden warten bis Anwender den Seriellen Monitor geöffnet hat.");
sleep_ms(5000);
scan_hw(i2c0); // Alternativ i2c0 oder i2c1
Serial.println("||");
scan_hw(i2c1); // Alternativ i2c0 oder i2c1
Serial.println("||");
}

void scan_hw(i2c_inst_t* hardware) {
int PIN_SDA = i2c_hw_index(hardware) *2; // Bei i2c0 wird daraus PIN=0, bei i2c1 wird daraus PIN=2
// mit i2c0 geht: 0, 4, 8, 12, 16, 20, 24, 28
// i2c1 geht: 2, 6, 10, 14, 18, 22, 26
Serial.print("i2c");
Serial.print(i2c_hw_index(hardware)); // Verwandelt i2c0 in "0" bzw. i2c1 in "1"
Serial.print(":");
while (PIN_SDA < 29) { // i2c0: 0 ... 28; i2c1: 2 .. 26
Serial.print(" PIN ");
Serial.print(PIN_SDA);
Serial.print("/");
Serial.print(PIN_SDA +1 ); // SCL ist immer ein Pin höher als SDA
scan(hardware, PIN_SDA);
PIN_SDA += 4; // i2c0: 0, 4, 8, ... 28; i2c1: 2, 6, 10, ... 26;
}
}

void scan(i2c_inst_t* hardware, int sda) {
i2c_init(hardware , 100000);
gpio_set_function(sda, GPIO_FUNC_I2C);
gpio_set_function(sda +1, GPIO_FUNC_I2C);

gpio_pull_up(sda);
gpio_pull_up(sda +1);
Serial.print(": "); // Wenn das ausgegeben wird ist init erfolgreich

for (int addr = 0; addr < (1 << 7); ++addr) {
int ret;
uint8_t rxdata;
if (!reserved_addr(addr)) { // Reservierte Adressen werden nicht gelesen
ret = i2c_read_blocking(hardware, addr, &rxdata, 1, false); // Es wird versucht, 1 Byte zu lesen,
if (ret >= 0) { // wenn die Adresse exisitiert, wird die Anzahl von übertragenen Bytes zurückgegeben.
// wenn die Adresse nicht existiert, wird -1 zurückgegeben
sprintf(Textausgabe, "%02x ", addr); // Ausgabe Hexadezimal
Serial.print(Textausgabe); //
}
}
if (addr % 32 == 15) Serial.print("."); // Als "progress-bar"
}

Serial.print("|"); // wird vor deinit ausgegeben
i2c_deinit(hardware);
gpio_set_function(sda, GPIO_FUNC_NULL);
gpio_set_function(sda +1, GPIO_FUNC_NULL);
}

/* So sieht die Ausgabe im Seriellen Monitor aus, wenn erst 20/21 und dann 0/1 angeschlossen wird

i2c0: PIN 0/1: ....| PIN 4/5: ....| PIN 8/9: ....| PIN 12/13: ....| PIN 16/17: ....| PIN 20/21: .27 ...| PIN 24/25: ....| PIN 28/29: ....|||
i2c1: PIN 2/3: ....| PIN 6/7: ....| PIN 10/11: ....| PIN 14/15: ....| PIN 18/19: ....| PIN 22/23: ....| PIN 26/27: ....|||
5 Sekunden warten bis Anwender den Seriellen Monitor geöffnet hat.
i2c0: PIN 0/1: .27 ...| PIN 4/5: ....| PIN 8/9: ....| PIN 12/13: ....| PIN 16/17: ....| PIN 20/21: ....| PIN 24/25: ....| PIN 28/29: ....|||
i2c1: PIN 2/3: ....| PIN 6/7: ....| PIN 10/11: ....| PIN 14/15: ....| PIN 18/19: ....| PIN 22/23: ....| PIN 26/27: ....|||
5 Sekunden warten bis Anwender den Seriellen Monitor geöffnet hat.


Einzelne Adressen findet man auf: https://i2cdevices.org/addresses

*/

---------------




Elektronik-Labor  Projekte  Mikrocontroller  Raspberry