Arduino: How to create a second serial communication channel (although it supports only one)

Are you also facing the problem that you want to communicate with your Arduino* from your computer through two serial communication channels, even though there is only one?

This can happen for example if you want to control one device with the Arduino via I²C (Inter-Integrated Circuit) and another device via SPI (Serial Peripheral Interface) at the same time. In theory, you could control the SPI communication with the Python package serial. However, you might have the problem that the required serial communication channel is already being used for the I²C communication.

I encountered the same problem in 2021 in my bachelor thesis, where I wanted to control two DACs (Digital-to-Analog Converters) simultaneously with my Arduino Mega 2560*. The firmware FirmataExpress was installed on the Arduino, which was used to control it via Python from a laptop. In Python I had installed the package pymata4.

As mentioned above, the I²C communication blocked the only serial communication channel of the Arduino. To send additional commands to the Arduino, which it should pass on to another device via SPI, I came up with the following solution…

Digital pins as additional communication channel between computer and Arduino

I simply made use of 21 unused digital pins of the Arduino: These can be switched on and off as desired from the laptop (e.g. via Python) and then trigger the Arduino to read the state of the pins and interpret them as a bit code.

Each pin represents one bit (for example „pin off“ for „0“ and „pin on“ for „1“). In my case, 20 pins represented a bit code, which the Arduino converted to an SPI message which it sent to the DAC. The exact structure of this SPI message had been described in the datasheet of the DAC. Depending on what you want to transmit, you can of course use more or less than 20 pins, as long as the pins are not being used for other purposes. Most importantly, they should not be connected to any devices.

To instruct the Arduino to read out the pins and process the bit code, I used a 21st pin as a trigger: „Pin on“ signified „read out the bit code and set the pins back to ‚off'“, „pin off“ signified „nothing to do“.

Switching the pins on and off with Python

On a computer with Python you can switch the digital pins with board.digital_write(pin, 1) or board.digital_write(pin, 0), where board is initialized by self.board = pymata4.Pymata4() and pin is the number of the pin.

Taking my SPI communication as an example, the complete source code then looked like this:

from pymata4 ifrom pymata4 import pymata4
…
_trigger_pin = 22
_delay_spi = 0.007
…
def on_activate(self):
    …
    self.my_board = pymata4.Pymata4()
    …
    # Initialisation of SPI communication
    for pin in range(self._trigger_pin, self._trigger_pin + 21):
        self.my_board.set_pin_mode_digital_output(pin)  # make all pins output pins (input and pullup don't work)
        self.my_board.digital_write(pin, 0)  # set all pins to zero (just to be on the safe side)
        
    self.set_voltage_spi(self.my_board, 1, 0)
…
def set_voltage_spi(self, board, channel, voltage):
    """ Set voltage of the DAC8734 analog outputs (controlled by Arduino via SPI)
    Set voltage of one of the 4 analog outputs of the DAC8734 by SPI message from Arduino to DAC8734
    create and send a 20-bit SPI message according to DAC8734 Datasheet (first 4 bits: channel bit; 16 data bits)
    uses the 20 digital Arduino pins after the trigger_pin for the SPI message and the trigger_pin as trigger
    don't connect anything to (trigger_pin) until (triggger_pin+20)
    Paramters:
    board:      Arduino-Board (initialized by self.my_board = pymata4.Pymata4())
    channel:    DAC analog output channel (allowed values: 1 - 4)
    voltage:    DAC analog output voltage (allowed values: 0 - 65535)
    """
    if (channel < 1) or (channel > 4):
        self.log.error('SPI channel is not in range 1 - 4.')
    elif (voltage < 0) or (voltage > 65535):
        self.log.error('Bitvalue of SPI voltage is not in range 0 - 65535.')
    else:
        channel = channel + 3  # channel register (channels 3-6 = 4 DAC analog outputs)
        channel = channel << 16  # leave 16 bits after the channel bits
        total_bits = channel + voltage  # add 16 voltage bits to channel bits
        bitcode = format(total_bits, '020b')  # make sure to have a 20 bit number
        for pin in range(self._trigger_pin + 1,
                         self._trigger_pin + 21):  # set 20 pins after the trigger_pin to 1 according to the bitcode
            if bitcode[pin - (self._trigger_pin + 1)] == '1':
                board.digital_write(pin, 1)
            else:
                board.digital_write(pin, 0)
        board.digital_write(self._trigger_pin, 1)  # set trigger_pin to 1, to make Arduino read the digital pins
        board.digital_write(self._trigger_pin, 0)  # set trigger_pin to 0, to stop Arduino from reading digital pins
        time.sleep(self._delay_spi)                # 'reaction time' of the DAC8734 we have to make Python wait,
                                                   # otherwise strange effects happen (e.g. wrong counts at the
                                                   # first pixel of each scan line)

If you are interested in the rest of the source code, you can find it on GitHub.

Required Code lines in the Arduino Firmware

To make the Arduino check the trigger pin regularly and (in case the trigger pin is „on“) read the 20 other pins and set them back to „off“, I had to add a bunch of lines to its firmware (in my case pymata4):

#include <SPI.h>
…
#define CS_PIN                      53
#define triggerpin                  22    // pin that triggers SPI communication
…
void sendSpi(unsigned long bytecode)
{
  unsigned int channel = (bytecode & 0xF0000) >> 16;  // first 4 bits = target register bits
  unsigned int data_1  = (bytecode & 0x0FF00) >>  8;  // next 8 bits = first 8 data bits
  unsigned int data_2  = (bytecode & 0x000FF) >>  0;  // next 8 bits = last 8 data bits
  digitalWrite(CS_PIN, LOW);                          // Start transmission
  SPI.transfer(0b000);                                // Bits without function
  SPI.transfer(channel);                              // Select the target register
  SPI.transfer(data_1);                               // Send the first 8 data bits
  SPI.transfer(data_2);                               // Send the last 8 data bits
  digitalWrite(CS_PIN, HIGH);                         // Stop transmission
}
…
void setup()
{
  SPI.begin();
…
}
…
void loop()
{
  int SPI_trigger = 0;                              // just some auxiliary variables
  int pin_value = 0;                               
  int bitshift = 0;
  SPI_trigger = digitalRead(triggerpin);            // value of the trigger pin
  if (SPI_trigger == 1) {                           // SPI message from Python can be read
    unsigned long serial_SPI_msg = 0x00000;
    for (int i=(triggerpin+1); i < (triggerpin+21); i++){
      pin_value = digitalRead(i);                   // read values of 20 data pins
      if (pin_value == 1) {                         // if value = 1: set corresponding SPI message bit to 1
        bitshift = (triggerpin+20)-i;               // position of the 1 in the SPI bit code
        serial_SPI_msg += ((int32_t)1 << bitshift); // moves 1 to position in the SPI bit code and adds it
      }
    }
    sendSpi(serial_SPI_msg);                        // send the SPI message
  }
…
}

If you are interested in the full source code of the modified FirmataExpress firmware, you can also find it on GitHub.

Did my solution help you or do you have any questions? Then feel free to leave a comment below! 😊

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert