Measured range transmission over sub-1GHz RF

Earlier this year I was connected through a mutual friend with some people looking to build a prototyped proof-of-concept for an idea they had. They had attempted to work with someone through fiverr, but found the quality of work and communication to be frustrating and not up to their standards.

They’ve requested I not share the details of the implementation, so I’ve omitted most details and any pictures, but I still can write about some of the high-level hurdles I ran into and how I worked around them.

Overview

The goal was to use two relatively common microcontrollers, which included built-in RF support, to transmit measured data from one board to the other and utilize that data on the second board. Sounds like the description of close to every hardware project ever right? The first board would use an external sensor to measure time-of-flight and lux to record a short distance, and then build and transmit a packet containing that data over a sub-1GHz RF (868MHz or 915MHz are the most common frequencies depending on your country). The second board would parse the received packet to pull out the necessary data and do any error checking, then use that data to indicate state with a separate output device.

Initial prototype

The client had already designed, printed, and assembled two custom PCBs, but I first built and tested the implementation on two vendor dev boards with the external sensors hooked up to their GPIOs.

The first step was to get a simple transmission working. The nice part about the vendor and chips chosen by the client is that they supported a super simple SDK for their RF transmission. All of the PHY and gnarly details were hidden beneath a typical *_config_init, *_init(&config), *_start driver interface (pseudo function names). The configuration was relatively straightforward, and the SDK-provided example project was enough to cobble together some working code for repeated transmission of a packet containing supplied data.

The next step was to pull in the measured sensor data and bundle that instead of garbage data into the packet. This step proved incredibly frustrating, as the external sensor the client provided me was super inconsistent. I couldn’t tell if it was the solder connection of the jumpers or something within the chip, but it would never follow what its datasheet listed as its startup behavior exactly. Many times it was simply unresponsive and I couldn’t access anything on it, and then it would start working again. I resoldered each jumper wire, tested it with each board and multiple separate GPIOs, and never got consistency. Thankfully it was a simple enough interface to write that I could get it mostly done even with the timesuck, and when it came time to test the code on the actual PCB it worked wonderfully.

After I had the real data properly transmitting and receiving, it was fairly trivial to parse it out on the receiving board and use that to indicate its state externally.

Custom PBC proof-of-concept

With the full dev board prototype working, I switched to the custom PCBs. The dev boards I was using allowed me to switch some jumpers around to use the on-board programmer to program externally through the classic 2x5 JTAG cable. I also had to clean up the code and separate my different board layouts/pinouts for initialization through some #defines. This was a simple enough project spanning only a handful of files, so I chose to keep the branching logic simple: just a single #define in a constants.h file that was used with #if/#else throughout the code to differentiate between dev board- and PBC-specific code. In my opinion, this project didn’t warrant some of the more complicated but robust approaches like different full-board header files included through #ifdef or probing board hardware to determine on boot. The current design leaves those options open to easily migrate to down the road as the project gets more complicated.

I also had the uncertainty of whether or not the boards themselves would work even when programmed correctly. The client had designed the schematics, done the board layout, and even reflowed the SMD components onto the board themselves in the oven (without an EE background, impressive!). Thankfully, minus one or two switched outputs between the schematic and real board, everything ended up working great.

Handling bottlenecks

Once the code and boards were up and running with the desired behavior, the first tradeoff I tackled was how much sensor data to buffer before transmission. The more data sent per-packet, the easier it would be to detect outliers or provide a more complete delta for the sensor reading, but that would also delay the period for how fast each update took. I also didn’t know what the comparison was between the speed I was able to read data from the sensor versus the time it would take to build, transmit, and successfully receive a packet. The sensor rate I could find in the datasheet, but the transmission time was dependent upon the devices, their distance, and real-world factors I couldn’t control.

I did some experimental tests first, starting at a ridiculous amount of data per-packet, and slowly ramped that down. It was a noticeably slower refresh rate for the second board’s output when larger packets were sent, as expected. I then reversed the logic of the code so instead of measuring a specific amount before transmitting, it would continue to intake data only until the board was ready to transmit again. That way, the bottleneck would be the transmission speed rather than the sensor input. The results of this surprised me - when I looked at the data being received, there was only a single distance measurement per-packet. That meant that the transmissions were happening so fast that it was immediately ready for the next one once the next data value was pulled from the sensor. I also verified in the incrementing sequence numbers in each packet that on the receiving board there was never a single packet missed or dropped. I suspect that the underlying SDK implementation just shoved the transmissions into a queue, but even running for a while never saw a slowdown or delay in the time it took for the second board to indicate the current state, so it seemed okay to me.

Overall this was a fairly simple project to work on for a couple of months, but was a nice introduction to RF through the SDK, and a good excuse to learn and test out a different vendor’s hardware and libraries. I wish my client the best of luck going forward!