Using micro:bits to send data to and from an Arduino wirelessly

Summary

We can use microbits to act as a wireless bridge between an Arduino Uno and a PC. One micro:bit connects to the Arduino. The second connects to the PC. Data can be passed to and from the Arduino serial port through the micro:bits.

The problem

A friend has an Arduino based instrument that he wants to communicate with from his PC wirelessly. Why? I don't ask why. He's bigger than I am.

A solution - outline

A micro:bit is connected to the serial pins (TX and RX) on an Arduino Uno through a logic level converter. This micro:bit sends data to and receives data from the serial port on the Arduino. The data received from the Arduino is transmitted to a second micro:bit connected to the PC through a USB port. Data received from the micro:bit connected to the PC is sent to the micro:bit connected to the Arduino.

Some folk call the serial port the UART. This is probably a more accurate name. What ports aren't serial nowadays? The Arduino uses the 'Serial' library to communication with this port, so I'll carry on using the term 'serial' for the port.

The micro:bit connected to the Arduino acts as a wireless bridge to the micro:bit connected to the laptop.

The micro:bit connected to the laptop acts as a wired bridge to the laptop. This micro:bit sends data to a virtual serial port on the laptop and receives information from the laptop on the same serial port. The received data is transmitted to the micro:bit connected to the instrument.

The two micro:bits create a wireless bridge between the Arduino and the laptop, allowing two-way serial communication.

Data flow

Arduino Uno RX, TX pins <-wires-> logic level converter <-wires-> micro:bit <- radio link -> micro:bit connected to PC <- USB cable-> PC

Fritzing diagram showing the Arduino Uno, logic level converter and micro:bit connections

The diagram below shows the connections on the Arduino side. The micro:bit on the PC side is connected to the PC by a USB cable.

Connections between the Arduino Uno, logic level converter and the micro:bit.

Arduino to micro:bit connection

The Arduino Uno used for testing works at 5V, the micro:bit works at 3V. So I use a logic level converter in-between the two boards to allow communication.

I drew a Fritzing diagram showing all the connections. Like many Fritzing diagrams, it looks pretty but is hard to follow. I paid for the software, so I'm using it.

Please see my blog post on the level converter to see how to connect the Arduino to the micro:bit via a logic level converter. The setup may well work without the logic level converter. However, the input pins on the micro:bit are not rated for more than 3.9V. So, applying the 5V signal levels from the Arduino is out of spec, meaning that the micro:bit could fail at any time. Or fail a little bit, just enough to corrupt your data occassionally, leaving you chasing Heisenbugs.

micropython code for the micro:bits

The script for the two micro:bits is almost the same. The UART is set up to use the edge connector pads as RX and TX for the micro:bit connected to the logic level converter. The UART for the micro:bit connected to the PC is left as the default which uses the USB connection to send and receive data to and from the UART.

micropython code for the micro:bit connected to the logic level converter

'''
Forward serial data from pins 0&1 to radio.
Matthew Oppenheim May 2022
'''

import radio
from microbit import display, Image, pin0, pin1, sleep, uart

radio.on()
radio.config(channel=40, group=40, data_rate=radio.RATE_1MBIT)

uart.init(baudrate=115200)
uart.write('Receive data from radio and send to uart.\n')
uart.write('Receive data from uart and send to radio.\n')

# connections to edge connector pads from RX, TX on instrument
uart.init(baudrate=115200, tx=pin1, rx=pin0)

def flash_display():
    ''' Flash a chessboard onto the display. '''
    display.show(Image.CHESSBOARD)
    sleep(200)
    display.clear()

def send_uart(msg_str):
    ''' Send msg_str to serial port. '''
    uart.write(msg_str)

def send_radio(msg_str):
    ''' Send msg_str over radio. '''
    radio.send(msg_str)

# script starts here
flash_display()

while True:
    display.show(Image.DIAMOND)
    # radio.receive() returns a string
    radio_in = radio.receive()
    if radio_in:
        send_uart(radio_in)
# flash display only on instrument side, not laptop
        flash_display()
    if uart.any():
        msg_bytes = uart.read()
        msg_str = str(msg_bytes, 'UTF-8')
        send_radio(msg_str)

micropython code for the micro:bit connected to the PC

'''
Receive data from uart and send to radio.
Matthew Oppenheim May 2022
''''''

import radio
from microbit import display, Image, pin0, pin1, sleep, uart

uart.init(baudrate=115200)
radio.on()
radio.config(channel=40, group=40, data_rate=radio.RATE_1MBIT)
uart.write('Receive data from radio and send to uart.\n')
uart.write('Receive data from uart and send to radio.\n')

def flash_display():
    ''' Flash a chessboard onto the display. '''
    display.show(Image.CHESSBOARD)
    sleep(200)
    display.clear()

def send_uart(msg_str):
    ''' Send msg_str to serial port. '''
    uart.write(msg_str)

def send_radio(msg_str):
    ''' Send msg_str over radio. '''
    radio.send(msg_str)

# script starts here
flash_display()

while True:
    display.show(Image.DIAMOND)
    # radio.receive() returns a string
    radio_in = radio.receive()
    if radio_in:
        send_uart(radio_in)
        #flash_display()
    if uart.any():
        msg_bytes = uart.read()
        msg_str = str(msg_bytes, 'UTF-8')
        send_radio(msg_str)

Testing

I created a test string which mimicks the length of string that my friend wants to send from his instrument to the PC. I created a function which flashes the Arduino LED when the string 'flash' is received. This allows me to test two way communication. The string is streamed at around 100Hz to the PC. I send the string 'flash' from the Arduino serial plotter and see that the LED on the Arduino flashes.

By making the test string a number, I can use the serial plotter tool in the Arduino IDE. So long as the incoming string is not corrupted, I see a straight line. This gives a quick visual check that the data is intact.

I found that so long as the string is 16 characters or less, transmission is robust. Once the string exceeds about 16 characters, there is a marked decrease in reliability. The string is often corrupted. The limiting factor seems to be string length rather than the frequency that the string is sent at. I tested at up to about 100Hz - that is the string is sent about every 10ms. I say 'about' as I didn't use timers and interrupt service routines to try and make this exact. I just put a delay function in the main loop, which is never going to give an exact frequency rate. But it is good enough for the testing presented here.

If you want to get serious about precise timing, have a look at using FreeRTOS and timer callbacks here.

Arduino Uno test code

/*
Send a test string through the serial port.
Check for incoming serial data. 
Flash LED if the incoming serial data starts with "flash".
Matthew Oppenheim May 2022
*/

// String to hold data for the serial port
String g_inputString = "";
bool g_stringComplete = false;
int g_count = 0;
// strings used for testing data transmission
//const String g_test_string = "1,30.2;2,30.4;3,30.64;4,30.64;5,30.64;6,30.64;7,30.64;8,30.64;9,30.64;10,30.64;11,30.64;12,30.64;13,30.64;14,30.64;15,30.64;16,30.64;|A";
//const String g_test_string_1_4 = "1,11.1;2,22.2;3,33.3;4,44.4";
//const String g_test_string_1_2 = "1,11.1;2,22.2|A";
const String g_test_string_1_2 = "123456789012345";

void setup() {
   Serial.begin(115200);
   pinMode(LED_BUILTIN, OUTPUT);
   Serial.println("counter test");
}

void loop() {
  // Check if the serial data string has a newline character at end.
  if (g_stringComplete) {
    // Check if the serial data has a command in it.
    processSerialData(g_inputString);
    // Reset the serial data string and completion flag.
    g_inputString = "";
    g_stringComplete = false;
  }
    g_count = updateCount(g_count);
    //Serial.println(g_count);
    Serial.println(g_test_string_1_2);
    delay(10);
}

void flashLed() {
  digitalWrite(LED_BUILTIN, HIGH);
  delay(200);
  digitalWrite(LED_BUILTIN, LOW);
  delay(200);
}

// increment count up to 255, then reset to 0
int updateCount(int count) {
  if (count > 254) {
    return 0;
  }
  ++count;
  return count;
}

// Check for command in serial_string.
void processSerialData(const String &serialString){
  const String flash_command = "flash";
  // Use startsWith to accommodate different EOL symbols in data.
  if (serialString.startsWith(flash_command)){
    flashLed();
  }
}

// Handle incoming data on serial hardware port
void serialEvent() {
  while (Serial.available()) {
    // Read in character from serial port and add to serial data string.
    char inChar = (char)Serial.read();
    // use += as this modifies left operator without creating new instances
    g_inputString += inChar;
    // If a newline character is received, set the serial data complete flag true.
    if (inChar = '\n') {
      g_stringComplete = true;
    }
  }
}

Other Solutions

My friend tried interacting with his instrument using wifi with a Feather Huzzah connected to the instrument. This works well at home, but failed using the University wifi. We may have a go using our own wifi switch.

A wired solution is always more reliable than wireless. Using a USB cable to stream data from the UART port to the PC will always be more robust than a wireless solution. If you need to access two or more UARTs on your Arduino - for instance the Arduino Mega has four UARTs - we can use a USB to serial converter cable such as this one to connect with each of the UARTs and stream data to the PC. Each of the cables creates a virtual COM port on the PC.

Logic level converter between an Arduino Uno and micro:bit using a FET transistor

Summary

We can use a single FET transistor with two resistors to make a two way logic level converter to interface e.g. Arduino Uno and a micro:bit.

The problem

I need to interface a micro:bit to an Arduino Uno, connecting the RX and TX pins of the Arduino to tabs on the edge connector of the micro:bit. The Arduino Uno has 5V logic levels. The micro:bit has 3V logic levels. The 5V signals from the Arduino Uno may damage the micro:bit.

The setup may well work without the logic level converter. However, the input pins on the micro:bit are not rated for more than 3.9V. So, applying the 5V signal levels from the Arduino is out of spec, meaning that the micro:bit could fail at any time. Or fail a little bit, just enough to corrupt your data occassionally, leaving you chasing Heisenbugs.

A solution

I need a logic level converter. This sits inbetween the Arduino Uno and the micro:bit boards and converts the signal levels so that each board receives the logic levels it works at.

How a logic level converter works

I learned about how logic level converters work from an excellent article in Circuit Cellar by Robert Lacoste. You can read this article for free by opening a free account on the website here. The simplest bi-directional logic level converter in the Lacoste article uses a single field-effect transistor with two 2.2K Ohm resistors. This circuit is simulated below.

Simulation of the FET based logic level converter using Falstad

I use the free online circuit simulation tool Falstad to simulate the device. Go to the site, then click on File, Import from Text. Copy and paste this text in to the text box and click OK.

$ 1 2e-8 21.593987231061412 50 5 50 5e-11
r 256 272 256 352 0 2200
r 464 272 464 352 0 2200
f 368 320 368 368 32 1.5 0.02
w 256 352 256 368 0
w 256 368 352 368 0
w 464 352 464 368 0
w 464 368 384 368 0
w 256 272 256 256 0
w 256 256 368 256 0
w 368 256 368 320 0
R 160 368 96 368 0 2 10000 2.5 2.5 0 0.5
w 160 368 256 368 0
R 256 256 176 256 0 0 40 3 0 0 0.5
R 464 256 560 256 0 0 40 5 0 0 0.5
w 464 256 464 272 0
403 640 352 768 416 0 6_64_0_4099_5_0.003125_-1_2_6_3
w 464 624 464 640 0
R 464 624 560 624 0 0 40 5 0 0 0.5
R 256 624 176 624 0 0 40 3 0 0 0.5
R 496 736 544 736 0 2 10000 2.5 2.5 0 0.5
w 368 624 368 688 0
w 256 624 368 624 0
w 256 640 256 624 0
w 464 736 384 736 0
w 464 720 464 736 0
w 256 736 352 736 0
w 256 720 256 736 0
f 368 688 368 736 32 1.5 0.02
r 464 640 464 720 0 2200
r 256 640 256 720 0 2200
w 496 736 464 736 0
403 32 672 160 736 0 26_64_0_4099_5_0.0015625_-1_2_26_3
207 464 368 528 368 4 High\slevel\slogic
207 256 736 176 736 4 Low\slevel\slogic
403 32 288 160 352 0 11_64_0_4099_5_0.00625_-1_2_11_3
403 512 656 640 720 0 30_64_0_4099_5_0.00625_-1_2_30_3
x 157 222 687 225 4 24 Low-level\slogic\sinput,\shigh-level\slogic\soutput
x 131 587 658 590 4 24 High-level\slogic\sinput,\slow-level\slogic\soutput

You should see this circuit:

One note about Falstad - the app can suck up a lot of the CPU and ramp up the CPU temperature.

The 'scopes shows voltage in green and current in brown at the same time. We are interested in the green voltage levels.

There are two circuits shown. These show the same logic level converter circuit but with the inputs and outputs reversed.

The top circuit shows a 3V signal, simulated by a 10kHz square wave, from the simulated low-level logic device converted to a 5V signal.

The bottom circuit shows a 5V signal input to the right side of the circuit and a 3V signal output from the left.

So the logic level converter simulates correctly for an input signal to either side.

I use 2.2K resistors in the simulated circuit as this is what the boards I received use. Lacoste uses 2.2K resistors in his example.

Explanation of the simulated FET circuit

The FET is called an n-type FET as it uses an n-type semiconductor channel to transmit charge through the component. This channel is incomplete unless the gate has a positive bias compared with the source. Then current can flow along the completed channel. This current flow is shown as animated dots in Falstad. If you hover your cursor over the FET symbol, the gate, drain and source terminals show up as G, D and S. This gate-source bias needs to be above a threshold level for the semiconductor channel to complete and for current to flow. This is a pretty sketchy summary. There are lots of good YouTube videos explaining how a FET works with great animations I wish I'd had access to in the Chibanian when I studied this.

The FET gate is permanently held high by the low level side of the logic converter, in this case 3V. The source and drain of the FET are both weakly held high by 2.2K resistors.

If we slow down the simulation, we see that current flows when the source is held low by the low-level logic driver in the top simulation. Similarly, in the bottom simulation, current flows when the drain is held low by the high-level logic simulation. In both cases, a path to ground is created when the FET starts to conduct. This means that the output level also sees ground.

Have a play with the simulation to get your head around how the device works.

Testing

I bought a logic level converter board from eBay. The board has four logic level converter channels. A photo of the board with four converter channels can be seen below. The top pins are labelled HV1..HV4 - these are the connections for the high logic values. In my use case, these connect to the RX and TX pins on the Arduino Uno. So I only need two of the four channles. The pin labelled HV is connected to the high logic level voltage rail (5V in the case of the Arduino Uno). There is a ground pin connection on both the top and bottom rows, labelled GND. Similarly, the bottom row of connections has LV1..LV4 for each of the four low voltage signals. In my use case, the low level logic channels connect with pads 0 and 1 of the micro:bit's edge connector. I only need two channels. The unused channels are left unconnected.

Logic level converter.

I set the logic level converter up on some breadboard. I hooked a micro:bit to one side and an Arduino Uno to the other. The TX pin from the Arduino goes into the high-level logic side on HV1. Pad 0 of a micro:bit is connected to the low-level logic side on LV1. I connect to pad 0 on the micro:bit using a pin on a Kitronix edge connector that the micro:bit slots into.

A photo showing this arrangement can be seen below.

Testing a logic level converter.

I set up the Arduino Uno to send a signal from the TX pin and scoped the input to the micro:bit on my pocket oscilloscope, a DS213. This may not be a high-spec oscilloscope, but it is good enough for this application. One advantage of the DSO213 'scope is that it is battery powered, so there is no danger of creating ground loops. A close up of the oscilloscope display showing input and output serial data can be seen below:

High level logic input, low level logic output DSO213 display.

The green trace is the low level logic, the yellow the high level trace. I can see that the signal going to the micro:bit is shifted to the expected 3V logic level. The time axis is set to 10us per square. The square waves can be seen to occupy about one 'scope square for each high and low, which gives a baud rate of approximately 1/(10*10^-6) = 100kHz. I set the baud to be 115200, so this all looks about right.

The screen shot shows that the output signals are not the crisp square waves shown on the simulation. This is due to the capacitance in the circuit which was not part of the simulation There is also some ripple and noise on the signals. This is from other real-world artefacts of building circuitry, such as inductance. As the input frequency increases, the capacitance will have an increasing effect on the output signal, causing the edges to become more rounded. We can add capacitors and inductors to our simulated circuits to get more accurate outputs. That is a topic to deal with in another blog one day.

Once I was happy that the setup worked to convert from a high level voltage to a low level voltage, I tested the opposite signal direction. I connected pin 1 from the micro:bit edge connector to a second channel on the low logic level side of the converter board (LV4) and the RX pin of the Arduino Uno to the same channel on the high voltage side (HV4). I generated 3V signals with the micro:bit as the input to the logic level converter. The output was at the 5V logic level of the Arduino Uno. The output signals showed a similar rounding of the signal edges due to capacitance as the high level to low level conversion did.

Low level logic input, high level logic output DSO213 display.

Difference between the converter board and the simulated circuit

The transistors on my boards have 'J1Y' marked on the backs. The only transistor I can find with this marking on the back is the KSA1298Y, which is a bipolar transistor, not a FET. I don't see how the circuit will work with a single bipolar transistor per channel, but I'm open to suggestions. My conclusion is that the transistor is an unidentified FET. The converter board I bought uses 10K Ohm resistors, but this is a minor difference with the 2.2K Ohm resistors used in the simulation. A wide range of resistance values can be used for the resistors in the circuit.

Conclusions

The logic level converter works as expected. The real world implementation has effects from the inherent capacitance present in the circuitry. I'm not sure what model of transistor is used in the circuit boards that I purchased. Using a printed circuit board may reduce the rounding of the signal corners. I'm not sure how well the converter board would work in the MHz range - the rounding may become so severe that the output signals don't reach the necessary logic voltage levels. However, for passing data between serial ports at 115200 baud, the board is adequate.