Connect a second module without losing track of the conversation

One sensor works on its own. Add a display, and suddenly the project is harder to understand. Is the wrong pin selected, is an address duplicated, or is the program reading a message before it has finished arriving?

For a team combining modules, understanding the conversation is more useful than memorizing every communication block. This note walks through the UART and I2C blocks of the BIPES editor for MicroPython on a Raspberry Pi Pico. It is a reference for the mechanisms, not a recorded multi-device test. The same mechanisms come up in real integration questions, such as an Arduino forum project combining sensors, a display and a buzzer.

UART carries a stream of bytes

The UART template selects port 0, 9600 baud, TX on GPIO0 and RX on GPIO1. Both endpoints need matching communication settings and compatible electrical levels. In an ordinary two-device link, each side's TX goes to the other side's RX, with a shared ground reference.

Sending "Hello" puts bytes on that link. It does not create an application protocol. If the receiver waits for a newline, the sender must include one. If the receiver expects a fixed-size message, both sides must agree on the size and format.

Reading is where most mistakes appear. Read a byte count, read one line and read whatever is available answer different questions:

Keep a partial message until its boundary is known, then validate it before acting on it. Checking that data exists is not the same as checking that it is valid.

I2C selects a device before exchanging data

I2C uses a shared data line and clock line, and devices are selected by address. That makes it convenient for displays, light, distance and motion modules. It also adds constraints: the pins must support the chosen bus, the pull-ups must suit the circuit, and every device on one bus needs a distinct address and compatible electrical conditions.

A scan reports which addresses respond:

from machine import Pin, I2C

i2c = I2C(0, sda=Pin(4), scl=Pin(5))
print([hex(a) for a in i2c.scan()])

A response does not identify the sensor model or verify its measurements. An addressed read accesses a device; a register read additionally selects a location within that device. The device address and the register address are different numbers with different meanings, and mixing them up is a common reason a working module returns nonsense.

The block palette also offers low-level primitives such as start, stop, readinto and write. MicroPython's I2C documentation describes these as operations available on software I2C, separate from the normal addressed transactions, and lists port restrictions for deinit. The presence of a block does not make every method available in the firmware on your board.

Add complexity in a way you can explain

When a second module goes in, keep the working single-module configuration and change one thing at a time. For each module, record:

Then add the next module and run the scan again. If both appear, read each one separately before running them together. If one disappears or both misbehave only when combined, the record tells you whether to look at address conflicts, pull-ups, cable length or initialization order.

When modules work separately and fail together

This is one of the most common points where a prototype stalls. The usual suspects are a duplicated address, a bus with too many or too few pull-ups, a UART message read before it completed, or two drivers that assume they own the same pins. Each is easier to find with the connection map, the driver versions and the exact observed failure written down than with another round of rewiring.