Networking and communications
Theory / lecture notes
Networking purposes:
- location
- parallelism
- modularity
- interference
A visual representation of how the different protocols interact in their network.
UART
no clock, but fixed speed
NB. TX connects to RX (and vice versa).
I2C
I2C is aka TWI
7-bit addresses (mostly)
Uses these pins:
- SCL = clock
- SDA = data
Open-Drain Logic: I²C uses open-drain drivers, meaning devices can only pull the lines (SDA and SCL) down to ground (low state); they cannot drive them high directly.
Instead:
High State: Pull-up resistors connected to the supply voltage ensure the lines return to a high state when no device is pulling them low.
This design allows multiple devices to share the same bus without conflicts, but it relies on pull-up resistors for proper operation.
Most boards that speak a certain protocol have built in pullup/down resistors.
SPI
- Has clock and data lines
- data out / data in (sdo/sdi)(MOSI/MISO)(PICO/POCI)
- More pins than I2C
Advantage: more pins = higher data rate
- SCLK - clock
- SS/CS - Chip select / enable
- MOSI (Master Out secondary In): This pin carries data from the master device to the secondary device. It is used for sending commands or data.
- MISO (Master In secondary Out): This pin carries data from the secondary device back to the master device, typically used for receiving responses.
CPOL (Clock Polarity) parameter determines the idle state of the clock signal when no data is being transferred.
Network arrangement types (topologies)
Serial bus
Daisy chain
Star - One main with many secondaries - UART / SPI
Ring topology - neopixels / SPI
Bus topology - One main with one big data line and main/secondaries connected to it - needs a network address (MAC address, for eg.)
TCP/IP
protocol used on the internet
OSI layers (7 layers) - standardizes how networks interact / communicate
- physical
- ethernet - Handles communication between devices on the same local network using MAC addresses.
- network layer - IPV4/6 - routing data packets between different networks, ensuring that data reaches its intended destination.
- transport layer TCP (data) / UDP (streaming - faster - throws data like a potato) - with TCP/IP you always get back an acknowledge
- session layer - how we're talking - HTTP - peer to peer
- presentation - encryption
- application - Defines how users interact with the network through applications.
Every device on a network has a unique IP address, which identifies it for communication.
Networks are divided into subnets, each with a default gateway (the router that connects the subnet to other networks).
DHCP is a protocol that automatically assigns IP addresses and other settings to devices on a network.
Wireshark is a program that lets us see which packages out computer is sending and receiving, that would otherwise go unnoticed.
Group project
- Connect two devices
- Show logic analyzer between them
- Do so using I2C and SPI
I2C communication
The secondary must write it's own I2C address (this is done in the code).
AND
Set callback functions:
- to read received messages
- to handle requests
Irja and Sam uploaded the code to their ESP32s and used cables to connect them to one another.
NB. Power the master ON first.
To get a better understanding of how the MCUs are communicating, we hooked certain pins (always Ground first) up to the Logic analyzer and ran the program, Logic, to view the signals.
NB. When opening Logic, specify which protocol is being used (I2C).
Remember the screenshot of Erwin's slides about the transmissions sequence of an I2C signal?
This is that! (W0x55-0x48-0x65-0x6C•0x6C-0x6F-0x20-0x57-0x6F•0x72-0x)
Zooming in: 0x48 = "H"
Copying the data from the Terminal and converting it from Hex, the message that was sent becomes clear.
There is a setting in Logic that does the ASCII conversion. That's easier than pasting it into a website.
SPI communication (failure)
Initially we tried using this SPI tutorial, but it's for Arduinos.
ESPs aren't able to communicate over SPI.
Our attempts at making SPI connections were all unsuccessful.
ESP-Now
ESP-NOW is a “protocol developed by Espressif, which enables multiple devices to communicate with one another without using Wi-Fi. The protocol is similar to the low-power 2.4GHz wireless connectivity.
A very useful part about the EXSPRESSIF MCUs is that they can communicate over ESP-Now.
This guide is apparently the best resource for setting up ESP-Now.
Start by getting mac address of the secondary (with Arduino code) (NB. make sure code has WiFi.begin).
Sending success - serial monitor (Master):
Receiving success - serial monitor (Secondary):
Individual project
design, build, and connect wired or wireless node(s) with network or bus addresses and local input &/or output device(s)
UART
Because ESP-Now isn't an option between the XIAO RP2040 and ESP32c3, and the ESP32c3 doesn't work with the TOF sensor, I decided to try UART. I wasn't able to get the ESP32 and RP2040 to communicate properly though. However, I did get some random messages on the RP2040 from the ESP32.
My initial hope was to try and get TOF input from the RP2040 to the ESP32.
RP2040 code:
void setup() {
Serial1.begin(115200); // Initialize UART on Serial1 (pins D0 and D1)
Serial.begin(115200); // For debugging via USB
}
void loop() {
// Send data to ESP32-C3
Serial1.println("Hello from RP2040!");
// Check if data is available from ESP32-C3
if (Serial1.available()) {
String received = Serial1.readStringUntil('\n'); // Read incoming data
Serial.print("Received from ESP32-C3: ");
Serial.println(received);
}
delay(1000); // Send data every second
}
ESP32c3 code:
void setup() {
Serial1.begin(115200, SERIAL_8N1, 1, 0); // Initialize UART on pins D1 (RX) and D0 (TX)
Serial.begin(115200); // For debugging via USB
}
void loop() {
// Check if data is available from RP2040
if (Serial1.available()) {
String received = Serial1.readStringUntil('\n'); // Read incoming data
Serial.print("Received from RP2040: ");
Serial.println(received);
// Respond to RP2040
Serial1.println("Hello from ESP32-C3!");
}
delay(100); // Small delay to avoid flooding the serial buffer
}
Using the code above, I got this result when clicking the Reset button of the ESP32. So, it seems like there is some kind of communication.
I thought maybe the issue was that a wrong pin was being pulled down. So I tried coding the pinMode of a few of the GPIOs to make sure that they read HIGH.
I got a bit more communication back:
This was the last of the code for the ESP32 before I started following another route:
And for the ESP32c3:
Finishing previous boards
Both of my boards, from Inputs and Outputs weeks, were unfinished. I spent some time getting those working.
INPUTS WEEK (Time of Flight sensor):
OUTPUTS WEEK (DRV8825 step stick w/ NEMA 17):
I2C between XIAOs
ESP32c3 (sender):
#include <Wire.h>
void setup() {
Wire.begin(); // Initialize I2C as master
}
void loop() {
Wire.beginTransmission(8); // Address of the slave device
// Convert the string to a byte array
const char message[] = "Hello ESP32C3";
Wire.write((uint8_t*)message, sizeof(message) - 1); // Send the byte array
Wire.endTransmission();
delay(1000);
}
The original bit of code that I tried had:
char c = Wire.read(); // Read data sent by master
Serial.print(c);
But to communicate over I2C I learned that we must convert the message into bytes.
const char message[] = "Hello ESP32C3"; // Define the message
Wire.write((uint8_t*)message, sizeof(message) - 1); // Send the byte array
sizeof(message) - 1
ensures the null terminator (\0) is excluded from transmission.
RP2040 (receiver):
#include <Wire.h>
void setup() {
Wire.begin(8); // Initialize I2C as secondary with address 8
Wire.onReceive(receiveEvent); // Register receive event handler
}
void loop() {
// Empty loop
}
void receiveEvent(int bytes) {
while (Wire.available()) {
char c = Wire.read(); // Read incoming data
Serial.print(c); // Print received data to Serial Monitor
}
}
The outcome:
To get the message showing on a new line, include a Serial.println();
after the while{}
in void receiveEvent(int bytes)
¨Ω
ESP32 board w/ I2C
I'd like to be able to control a stepper motor with an ESP32 and for the ESP32 to be able to receive information. The plan is that the received information will affect how the stepper motor moves.
I read up about the different controls from the AccelStepper.h Library.
- I started by just getting the code to work.
- I increased the speed and it worked.
- I tried to change
AccelStepper stepper;
toAccelStepper stepper(AccelStepper::DRIVER, 1, 0);
and it didn't work. - I tried to combine the I2C code and the Stepper code. The I2C worked but the stepper didn't move.
There was a delay(1000);
at the end of the I2C transmission that was causing errors. I got rid of that and it was able to send messages and to move the stepper motor!
The I2C code for the RP2040 is the same as earlier. The code for the ESP32c3 is as follows:
#include <AccelStepper.h>
#include <Wire.h>
AccelStepper stepper;
// Defaults to AccelStepper::FULL4WIRE (4 pins) on 2, 3, 4, 5
void setup() {
Wire.begin(); // Initialize I2C as master
stepper.setMaxSpeed(1000);
stepper.setSpeed(200);
}
void loop()
{
stepper.runSpeed();
Wire.beginTransmission(8);
// Convert the string to a byte array
const char message[] = "Hello ESP32C3";
Wire.write((uint8_t*)message, sizeof(message) - 1);
Wire.endTransmission();
}
Trying to figure out I2C
In this section I did a lot of learning about the Wire.h
and [VL53L1X.h](https://github.com/pololu/vl53l1x-arduino)
libraries.
I couldn't figure out why swapping the (working) code of the ESP32 and RP2040 wouldn't work.
I felt that I needed a better understanding of I2C. Here's an I2C description from Arduino.
That answered a big question I had:
Because the I2C protocol allows for each enabled device to have it's own unique address, and as both controller and peripheral devices to take turns communicating over a single line, it is possible for your Arduino board to communicate (in turn) with many devices, or other boards, while using just two pins of your microcontroller.
- The controller sends out instructions through the I2C bus on the data pin (SDA), and the instructions are prefaced with the address, so that only the correct device listens.
- Then there is a bit signifying whether the controller wants to read or write.
- Every message needs to be acknowledged, to combat unexpected results, once the receiver has acknowledged the previous information it lets the controller know, so it can move on to the next set of bits.
- 8 bits of data
- Another acknowledgement bit
- 8 bits of data
- Another acknowledgement bit
But how does the controller and peripherals know where the address, messages, and so on starts and ends? That's what the SCL wire is for. It synchronises the clock of the controller with the devices, ensuring that they all move to the next instruction at the same time.
However, you are nearly never going to actually need to consider any of this, in the Arduino ecosystem we have the Wire library that handles everything for you.
That lead me to believe that the reason the TOF sensor wasn't working on the ESP32 was because the code wasn't setup correctly.
I2C scanner -- I used this to get the addresses of the secondaries in the chain.
The first address is for the TOF sensor. The second address is for the ESP32c3.
Reviewed the notes on the Wire.h
library to help me figure out how to communicate messages across I2C to the ESP32 (from the RP2040).
I got the ESP32 to receive a written message. The next step will be communicate the data being read from the TOF sensor.
Then I managed to get the data to send, but something was funky and it came through in three lines at once, and not in numbers that made sense relating to the data I had previously received via the TOF sensor.
After much fiddling, I was able to get slightly more sensical numbers, but they still weren't correct. This was my prevailing question.
"I have a sensor that gives uint16_t data and I want to send that over i2c to another MCU, but when I do, the data is received / printed all jumbled"
"The issue of jumbled data when transferring uint16_t values over I2C typically arises because I2C is inherently an 8-bit protocol, meaning it transfers data byte by byte. To send and receive uint16_t values correctly, you need to split the 16-bit data into two bytes (high and low) before transmission and reassemble them on the receiving end."
Useful links:
- This helped me set up the sensor to be read correctly.
- Wire (I2C) guide.
- VL53L1X library.
- Here, I found code that works on ESP32. In doing so, I learned more about how the sensor writes data.
#include "VL53L1X.h"
VL53L1X sensor; //create an instance of the VL53L1X class, which allows us to interact with the sensor
void sensor_init(VL53L1X::DistanceMode range_mode, bool high_speed) {
Wire.begin();
sensor.setTimeout(500);
sensor.init();
sensor.setDistanceMode(range_mode);
int budget = high_speed ? 33000 : 140000;
sensor.setMeasurementTimingBudget(budget);
}
void setup() {
Serial.begin(9600);
// range_mode: VL53L1X::Short, VL53L1X::Medium, or VL53L1X::Long
sensor_init(VL53L1X::Medium, false);
}
void loop() {
int dist = sensor.readRangeSingleMillimeters();
Serial.println(dist);
delay(1000);
}