Build · Test · Model · Evaluate · Connect 12 essays · 6 projects
Building & modeling systems

Wireless Race Car

Getting Involved

One of the things I regret about my university days is spending too much time playing video games and not getting involved in enough student societies. So when I started my masters program last year, I did things a bit differently. Given my interest in racing, the Bristol Racing society was my top choice. There, I joined the electronics team to help primarily with the brain of the car -- a battle-worn but functional Arduino.

The team participated in the electric Greenpower Formula 24 class. These are single-motored, car battery-powered cars with custom chassis. It's a bit different when the cars aren't screaming with engine and turbo noise but racing is still racing. There's something special in the air that makes my heart race.

How It Works

The vehicle was powered by an Arduino Mega which controlled the motor and tracked sensor data. As such, we used an Arduino UNO as a receiver and used a LoRa RFM95 module. It was a bit finnicky to get everything working but the pressure of an upcoming race was enough motivation. Later, we found that this was much easier to implement on a ESP32 when preparing for the next iteration of the race car.

LoRa wiring diagram

Image courtesy of https://randomnerdtutorials.com/esp32-lora-rfm95-transceiver-arduino-ide/

LoRa Receiver Code

// Arduino9x_RX
// -*- mode: C++ -*-
// Example sketch showing how to create a simple messaging client (receiver)
// with the RH_RF95 class. RH_RF95 class does not provide for addressing or
// reliability, so you should only use RH_RF95 if you do not need the higher
// level messaging abilities.
// It is designed to work with the other example Arduino9x_TX

#include <SPI.h>
#include <RH_RF95.h>

#define RFM95_CS 10
#define RFM95_RST 9
#define RFM95_INT 2

// Change to 434.0 or other frequency, must match RX's freq!
#define RF95_FREQ 434.0

// Singleton instance of the radio driver
RH_RF95 rf95(RFM95_CS, RFM95_INT);

// Blinky on receipt
#define LED 13

void setup() 
{
  pinMode(LED, OUTPUT);     
  pinMode(RFM95_RST, OUTPUT);
  digitalWrite(RFM95_RST, HIGH);

  while (!Serial);
  Serial.begin(9600);
  delay(100);

  Serial.println("Arduino LoRa RX Test!");
  
  // manual reset
  digitalWrite(RFM95_RST, LOW);
  delay(10);
  digitalWrite(RFM95_RST, HIGH);
  delay(10);

  while (!rf95.init()) {
    Serial.println("LoRa radio init failed");
    while (1);
  }
  Serial.println("LoRa radio init OK!");

  // Defaults after init are 434.0MHz, modulation GFSK_Rb250Fd250, +13dbM
  if (!rf95.setFrequency(RF95_FREQ)) {
    Serial.println("setFrequency failed");
    while (1);
  }
  Serial.print("Set Freq to: "); Serial.println(RF95_FREQ);

  // Defaults after init are 434.0MHz, 13dBm, Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on

  // The default transmitter power is 13dBm, using PA_BOOST.
  // If you are using RFM95/96/97/98 modules which uses the PA_BOOST transmitter pin, then 
  // you can set transmitter powers from 5 to 23 dBm:
  rf95.setTxPower(23, false);
}

void loop()
{
  if (rf95.available())
  {
    // Should be a message for us now   
    uint8_t buf[RH_RF95_MAX_MESSAGE_LEN];
    uint8_t len = sizeof(buf);
    
    if (rf95.recv(buf, &len))
    {
      digitalWrite(LED, HIGH);
      RH_RF95::printBuffer("Received: ", buf, len);
      Serial.print("Got: ");
      Serial.println((char*)buf);
       Serial.print("RSSI: ");
      Serial.println(rf95.lastRssi(), DEC);
      
      // Send a reply
      uint8_t data[] = "And hello back to you";
      rf95.send(data, sizeof(data));
      rf95.waitPacketSent();
      Serial.println("Sent a reply");
      digitalWrite(LED, LOW);
    }
    else
    {
      Serial.println("Receive failed");
    }
  }
}

LoRa Sender Code

The sender code is integrated with our main codebase. It combines the various sensor outputs into a comma-delimited string and broadcasts the string via LoRa. It tracked metrics like temperature, voltage, and current. The GitHub commit can be found here.

Field Testing

After trialing the kit at the lab, we eagerly brought out the prototype to the famed Goodwood Motor Circuit. As cars rolled up to the start/finish line, messages from our race car started trickling in. Once the cars got going, we quickly found that the signal dropped once the car was not in direct line of sight. However, we still got a few pings as the car passed pit lane. This was enough for peace of mind.

At some point, we started getting incredibly good signal from the race car and were wondering what happened. Turned out the chain fell off and the driver had to pull over at the end of pit exit. Oops. We ended the day with a DNF but at least the electronics held up the whole time!

What Stuck

So what did a fussy little radio actually buy us? For the first time, we could watch the car think -- temperature, voltage, and current trickling onto a laptop while it ran, instead of guessing after the fact from a driver's shrug and a worryingly warm battery pack. The rig was never going to win us the race. But it turned the car from a black box into something we could actually reason about.

The lessons were the cheap kind that only show up in the field. LoRa is wonderful right up until a grandstand or the car's own body slips between you and the antenna -- line of sight matters far more than the datasheet's optimistic range numbers let on. And the jump from the Arduino UNO and RFM95 to the ESP32 was a reminder that half of hardware is just picking the part that fights you the least.

If there's one thing I took from the whole build, it has nothing to do with radios. Left to my own schedule, a project like this drifts for months -- the rig only came together because the race was coming, and a race doesn't move for anyone. So if there's something you've been meaning to make, find yourself a start line, and let the deadline drag the rest out of you.

← Back to Writing & Projects