Long Range Wireless CAN Bus Bridge
During my time on the WMU Formula SAE racing team, I started to learn the value of dumping nearly all data we could ever need onto the vehicle's CAN bus. This made firmware development super easy becuase any module could control or query data from any other module. Additionally, we could plug in a laptop with a USB to CAN tool and graph the data live with a tool I made that leverages Grafana.
I wanted to use this exact same tool while not directly connected to the car, so that we could monitor software states, coolant temps, exhaust gas temperatures, and tons of other data live. This way, we don't use two different tools when connected to the car over a CAN wire or over the wireless telemetry system.
To accomplish this, I designed a transciever board, transmitter firmware, receiver firmware, and a small helper library.
Links:
The hardware is based on a low frequency, high range 900Mhz XBEE radio transceiver. This module is stupid simple to use, and basically acts like a wireless UART cable. We managed to get over half a mile of range through trees and over the elevation changes of the WMU engineering campus with small and cheap 2.1dBi gain antennas on either end. The whole thing has 4 pins that connect to the car: CAN high, CAN low, +12V, and gnd.
This system proved to be more than reliable enough for our testing, as most of the lots we tested in were wide open, and had us posted near the center of the track to pull telemetry while the driver ran laps.
Firmware
For this project, there were a few challenges with the firmware. First, due to the limited bandwidth of the XBEEs, I wanted to only send a subset of the car's available CAN bus frames. Next, I needed to buffer CAN data in order to pack it all into one large binary string and send it in a single packet. This is due to the inter frame wait time that the XBEE requires, along with the relatively small max packet size of 256 bytes of the XBEE.
For the above test, I set up a micro to continously send data to the transmitter, but we see on the receiver that the data is chunked into 256 byte packets with a 6ms inter frame delay. This means we can't just stream CAN data without any pre planning.
To deal with this, the transmitter firmware builds a map containing only the CAN IDs that I define as relevant. For code readability I chose to use a C++ map which requires dynamic memory allocation (I did not want to use a custom allocator here). This is frowned upon, but I was aware of the application's boundaries and knew this wouldn't lead to any issues, especially with the huge RAM size of the STM32F303RE we are using here.
// Takes an MBed style CAN message and updates the map if we care about the ID
void updateCanMap(CANMessage *msg) {
CANFrame tempFrame = {0};
// Don't add any new values that weren't in the map when we initialized it
if (canData.find(uint32_t(msg->id)) != canData.end()) {
// Copy data to serializable frame format
if (msg->format == CANExtended) {
tempFrame.extended = true;
} else {
tempFrame.extended = false;
}
tempFrame.id = msg->id;
tempFrame.dlc = msg->len;
memcpy(tempFrame.data, msg->data, msg->len);
fillChecksum(&tempFrame);
canData[tempFrame.id] = tempFrame;
}
}
This way we can read from the map at any moment and know we have the latest data frame of each ID. This is especially important because each of the frames comes in at a random time, and at any frequency from 10hz to 100hz on this race car.
while (1) {
// if a message is available, log it.
if (can.read(inmsg)) {
updateCanMap(&inmsg);
}
// time to transmit data
if (timer.read_ms() >= TRANSMIT_INTERVAL_MS) {
timer.reset();
// light LED while transmitting a packet
led.write(1);
// iterate through entire map
uint8_t i = 0;
for (const auto &[key, value] : canData) {
// copy each CANFrame struct to the output buffer
memcpy(packetBuf + i, &value, sizeof(value));
// offset output buffer destination by the size of the frame we just
// added
i += sizeof(CANFrame);
}
xbee.write(packetBuf, sizeof(packetBuf));
led.write(0);
}
}
}
When it's time to transmit, we copy a bunch of CANFrame structs to the output buffer, then shoot it off with the XBEE.
To keep this code simple, I made a portable can serializer module used here. This contains a platform agnostic CANFrame struct and means to checksum and validate the data. The cpp file is small, and works well:
When receiving data, there is one tricky thing to deal with. When signal drops, or if the transmitter is power cycled, the tx and rx devices will no longer be synchronized to the start of a packet. This means that the receiver needs to iterate over every incoming byte until it finds a valid start of packet. Once it finds a start of packet, it will build a trial CANFrame, then calculate the checksum of this trial frame. If the checksum is good, we continue. If not, the loop will iterate one single byte forwards and try again. This way it never takes more than 1 dropped frame to recover from signal loss.
To keep things simple, the receiver chunks incoming bytes onto a queue as they are received, then operates on the data in the superloop. The queue is dynamically allocated, but I have a very conservative boundary (for this micro) of 512 bytes set which causes the whole queue to be wiped out.
Conclusion
Starting this project was a little daunting for me as a college student with very little wireless experience. These XBEE modules made it super simple, and provided a nice GUI to set up the modules themselves. After a little bit of code, this system proved to be very useful at testing days.