Neil's assignment for this week:
     individual assignment:
      design, build, and connect wired or wireless node(s)
      with network or bus addresses
   group assignment:
      send a message between two projects

Group Work

I was unable to write up this week's group assignment with my group, so I redid it on my own and am writing up my results here. The group assignment is to "send a message between two projects". The simplest way to do this is using software serial communications between two commercial Arduino boards.

I wrote a program called softserial_repeater.ino, which sets up both a hardware and a software serial link, and copies any bytes it reads from software serial to hardware serial, and vice versa.

#include <SoftwareSerial.h>

#define RXPIN 2
#define TXPIN 3

SoftwareSerial MySerial(RXPIN,TXPIN);

void setup() {
  Serial.begin(9600);
  MySerial.begin(9600);
}

void loop() {
  int incomingByte = 0;
  if (Serial.available() > 0) {
    incomingByte = Serial.read();
    MySerial.write(incomingByte);    
  }
  if (MySerial.available() > 0) {
    incomingByte = MySerial.read();
    Serial.write(incomingByte);    
  }
  delay(5);
}      
I uploaded this to two Arduino Uno clones, one from Sparkfun and one from Elegoo, and connected both to serial monitors on my PC. I connected the Unos' grounds together, and tied Pin 2 (Software serial receive) of one to Pin 3 (Software serial transmit) of the other, and vice versa. Software serial repeater circuit When I type in a message into one serial monitor window, it appears on the other, as shown below:
Sending from Elegoo to Sparkfun
Serial monitor for Elegoo is on the left, Sparkfun is on the right. Typing a message to send from Elegoo to Sparkfun.
Sending from Sparkfun to Elegoo
Sparkfun has received our message. Typing a message to send from Sparkfun back to Elegoo.
Elegoo has received msg
Elegoo has received the return message.
I did this project in the simplest way possible, but I did learn two things from it. First, I found that sending individual bytes was much more reliable and less tricky than sending whole strings. Second, when I did this at first I was only getting one-way communication. The blinky lights on the Uno are very helpful in telling you when hardware serial data is being sent: this helped me narrow the problem down to a loose wire on the software serial link. Another helpful tool might be to add blinky LEDs to the software serial connection, so I can see when data is flowing through them too.

Goal: Mastering I2C communications

My final project is going to involve a lot of I2C communications between a microcontroller and a bunch of magnetic field sensors. The sensors have configurable I2C addresses, so I'll be able to have many of them on the same bus ... so long as I can figure out how to assign them different addresses. I may also need more than the two I2C interfaces supported by the RP2040's hardware. So my task for this week is to get good at I2C communications. Here are my goals:
  1. Demonstrate I2C communications between two microcontrollers, with custom I2C addresses
  2. Demonstrate software I2C on the Xiao RP2040
  3. Demonstrate I2C address configuration with multiple TMAG5273 sensors on the same I2C bus
There won't be any showy results for this week, it's all just grunt work, but it's important.

Unfortunately, I ran into a bunch of technical problems accomplishing the first and second tasks, and wasn't able to get to the third. But I did do some extra work with I2C communications using CircuitPython instead.

I2C between microcontrollers

This should be easy, but the devil's in the details. My goal is to set up two microcontrollers and set one as an I2C "master" / primary and one as an I2c "slave" secondary and assign a custom I2C address to the secondary. The primary will send ASCII text to the secondary, which will reverse the order of the characters and send it back.

Here are the key code snippets required for the primary:

  #include <Wire.h>
  Wire.begin();
  // Send data to address
  Wire.beginTransmission(address);
  Wire.write(data);
  Wire.endTransmission();
  // Request data from address
  Wire.requestFrom(address, numbytes);
  while(Wire.available()) {
    data = Wire.read(); // Read a byte at a time
  }
The secondary is more tricky, because it must only send and receive data when requested. This requires the use of event handler functions, which are outside the main program loop, and are only called when the I2C interface receives a request from the primary. They are called as interrupts, which means you can't do any other interrupt activities inside them. Printing to the serial port is an interrupt activity! This caused me a ton of problems, because serial port code I inserted to debug these functions caused them to not work.

The key code snippets required for the secondary are:

    #include <Wire.h>
    Wire.begin();
    Wire.begin(4);   // Start I2C device with address 4
    Wire.onReceive(receiveEvent); // When data is received, call receiveEvent
    Wire.onRequest(requestEvent); // When data is requested, call requestEvent

    void receiveEvent(int howMany) { // Receive event handler
      while (Wire.available()) {
        data = Wire.read();
      }
    }

    void requestEvent() { // Request event handler
        Wire.write(data);
    }

I first started working on this using my Beansprout RP2040 microcontroller and an Adafruit Feather M0 I had lying around. (Can't use an Uno with the RP2040 because the RP2040 is a 3.3V device.) I could not get it to work at all. When acting as primary, the RP2040 crashed immediately. When acting as secondary, it crashed as soon as it received a message from the primary. I tried some of the pre-made examples provided with Arduino (master_reader.ino, slave_sender.ino, master_writer.ino, slave_receiver.ino without success.)

To debug, I went back to trying to get two Arduino Unos talking to each other. THE SAME CODE WORKED PERFECTLY ON THE UNOS! This led to a ton of debugging work to figure out what was wrong with the RP2040 / M0 system. It took hours. A few of the things I tried:

In the end, I found two problems.
  1. When setting up a microcontroller as primary, the Uno really doesn't mind if you give it an I2C address using Wire.begin(address). The primary never uses this address, but the Arduino documentation says it's "optional". The RP2040 does care: a RP2040 as primary MUST be initialized with Wire.begin() with no arguments.
  2. For some reason the RP2040 refuses to act as secondary. Still not sure if I've written the code wrong, or if there's a bug, or if it's physically incapable. The good news is, I don't need it to do this for my final project, and I can satisfy this week's assignment with it only acting as primary.
So, partial success. Anyway, here's a diagram of the wiring I used (using Unos as examples). Note the use of 5K "pull-up" resistors on the I2C lines.
Here's a video of the two Unos talking to each other. The primary is on the right window, the secondary is on the left. Here's the Beansprout RP2040 connected to the Feather M0. I was only able to get it working with the RP2040 acting as primary, not secondary.
A photo of the circuit hookup. The pullup resistors are on the breadboard, and the oscilloscope leads are attached to the SDA and SCL lines.
Oscilloscope traces for the "reversing repeater" code, with the RP2040 as primary. SCL is on the top, SDA is on the bottom.
Here's a video showing the reversing-repeater code in action.
A screenshot of the reverser output. The secondary is on the left, the primary is on the right.
Here's a video showing the same setup with the RP2040 as secondary. At the start of the video, the I2C cable is disconnected, the primary is sending and correctly reporting no connection. At 0:12, I plug in the cable. The primary connects, sends a burst of data, the RP2040 secondary crashes pulling SDA low, and the primary then hangs, waiting for a response. Still not sure what the problem is.

Software I2C

The next thing I tried was to get software I2C working. In theory, it should be possible to use software "bit-banging" rather than a hardware interface to send I2C signals, which would allow me to have as many I2C interfaces as I'd like.

I used the Arduino_Software_I2C library provided by Seeed Studios. I chose this library because it's from the same people that created the Xiao board I'm using, and because its programming interface is most similar to the Wire library used for hardware I2C. The drawback is that it's really poorly documented, and it can only act as a master (primary). But since I couldn't get the RP2040 working as a secondary with hardware I2C, this isn't a big deal.

Another drawback is that I couldn't get it to work. I was able to send data back and forth to the "reverser" board, but the text was corrupted both in sending and receiving. Looking at the oscilloscope traces, the problem is apparent: the software I2C library doesn't generate a reliable clock signal (it pauses occasionally), and it looks like it "talks over" the receiving board's response. You can see some half-height digital pulses where one microcontroller is trying to assert HIGH and the other is sending LOW. I think the problem is that the software I2C library doesn't properly implement clock stretching, in which the secondary is supposed to be able to force the primary to wait by holding SCL low.

Oscilloscope trace of Software I2C library failing. You can see that it's skipping clock pulses on the left, and trying to assert clock pulses while the secondary is holding SCL LOW on the right, creating half-height pulses.

I don't have the time or expertise to fix this.

I2C with CircuitPython

As a final effort, I tried to get I2C working within the CircuitPython platform. This turned out to be surprisingly easy! Here are the key code elements:
    import board
    import busio

    msg = bytearray("What's up?") # Message to send
    i2c = busio.I2C(board.SCL,board.SDA) # Start I2C
    
    if i2c.try_lock():          # Lock the I2C device so we an use it
      i2c.writeto(4,msg)        # Write the message to the secondary
      i2c.readfrom_into(4,msg)  # Get a response
    i2c.unlock()                # Unlock the I2C device so others can use it
Screenshot of a CircuitPython I2C primary on the Beansprout RP2040 (right) interacting with the "reverser" Arduino secondary on a Feather M0 (left). This particular code flips the same text reverse and forward, repeatedly.
Working with CircuitPython was pleasant enough that I might try using it for other things in the future. I really like being able to use the REPL command line to test out code and debug. Maybe I'll get ambitious and write a TMAG5273 module...

TMAG5273 code for CircuitPython

Okay, one last thing. I did go ahead and write some CircuitPython code to interface with the TMAG5273 magnetometer chip that was the focus of Week 11. It's nowhere near a full library, just some bare-bones code to see if I can do it. I stole some I2C register-mangling functions from this site. Here's the output:
Screenshot of a CircuitPython code to read data from the TMAG5273 magnetometer. From left to right: temperature, Bx, By, Bz. I am moving a magnet in a circle around the sensor, starting from the -Z axis.

Design Files