Saturday, December 26, 2020

Setting up a ground station for radiosonde tracking

Only a few weeks ago,  I was introduced to the world of radiosondes through an amazingly informative video from Andreas Spiess.  By lunchtime the next day, I had one in my hands.

These devices pack so many things I'm interested into one little box; weather forecasting, radio telemetry, high-altitude ballooning and long-distance radio transmission and reception.  Honestly, I'm amazed I hadn't looked into it sooner, but making up for lost time I've already snagged three radiosondes and even made a new friend in a fellow hunter.

The Vaisala Radiosonde RS41 a.k.a. free STM32 devkit

This post intends to pick up where Andreas left off, covering the following:

There's already a couple of sites out there that you can track launches for nearly their entire flight such as and (aka  These sites rely on crowd-sourced data and while there's coverage in my area,  I'm interested in direct telemetry reception, and there's definitely no harm in feeding additional data to these services since I'm putting together a ground station to receive it.

Antenna performance testing

Three main antenna setups were tested:

Despite the radiosonde's low transmit power (60mW), you're almost guaranteed line-of-sight for most of the flight, making them super easy to receive.  This means even basic kit will do an acceptable job of receiving radiosondes, though as I'll show later you will definitely benefit from a decent antenna.

Challenger 1: RTL-SDR Blog dipole antenna

I picked up this kit from the RTL-SDR Blog's store, and it's great as a fixed-frequency testing rig.  

Mounted horizontally on a west-facing window and adjusted to 1/2 wavelength (74.6cm per side) I honestly wasn't expecting much, especially once the balloon moved east of my house, but was surprised to track a couple of radiosondes to over 120km south-east.  

As expected, visibility was lost at low to mid altitudes, and reception was quite unpredictable, but absolutely fine for experimentation, portable use or just proof-of-concept.

Pros:    Cheap, quick, indoor/outdoor use, temporary
Cons:    Somewhat directional reception if wall mounted, average performance, temporary

RTL-SDR Blog dipole: Mount anywhere, tune anything

Challenger 2: Discone antenna

I've had this antenna on my roof for years and it's served well as my general purpose SDR antenna.  Unfortunately, being a wideband antenna, it's actually fairly shit at everything.

It performed marginally better than the dipole, however had a tendency to lose the radiosonde for extended periods depending on the vertical angle of the sonde relative to my station; these designs are known for having greatest sensitivity toward the horizon, making them largely unsuitable for our purpose.

Pros:    Can be used for other frequencies
Cons:    Relatively deaf, needs proper mounting hardware

Discone antenna: Generally mounted upright

Challenger 3: DIY quarter-wave ground plane antenna

I've been wanting to build a proper antenna for a while, and this was my chance.  Following M0UKD's 1/4 Wave Ground Plane Antenna Calculator and with some pointers from my newly-made hunting friend, I put this together with some brazing rods from Bunnings and an SO259 panel mount connector from Jaycar.  The top element is made from some twisted household copper wiring, which is much easier to trim for making fine tuning adjustments.

The quarter-wave ground plane in all its glory

Once assembled, I borrowed a mate's NanoVNA and used this to fine-tune the antenna, which showed that the upright element needed to be trimmed about 10mm shorter than the maths had me believe.  I suspect more experimenting with the tuning of the ground radials would give a better result, however due to their permanent soldering in-place I didn't want to shorten them, just in case.  It's important to note that encasing it in PVC will affect the velocity factor, meaning each adjustment must be checked with the casing in place. I found this shifted the response down about 5-6 MHz.

VNA showing a clear dip around 401MHz

I designed, 3D printed and painted a basic weather cap for it and cut some PVC pipe to cover the top element.  Pipe cement seems to do a fine job of adhering to PLA, and a bit of silicone around the gaps above the ground radials doesn't hurt.  You can download the STL from Thingiverse here.

Assembled, tuned, painted and weather-proofed

Not surprisingly, this thing wipes the floor with the other antennas!  Side by side with the discone, signal-to-noise was on average between 10 and 20dB better, with consistent tracking through the entire flight.  Performance was further proven when a balloon launched from Mt. Gambier was heard for close to 30 minutes, as far as 355km away.

In all, clearly a very effective antenna for around $20.

Pros:    Cheap, effective
Cons:    Some effort to build, VNA required for precise tuning

Station setup and data contribution

The next step is to set up radiosonde_auto_rx.  Developed by Mark Jessop (who has written extensively about radiosondes) and Michael Wheeler, this will run happily on a Raspberry Pi 3 or newer (it currently runs on my overclocked Pi 2, but at stock speeds it'll max out a core and miss packets.)

I won't go into detail on the install - it's documented perfectly over on their wiki.  Be sure to pay special attention to the  ExecStart and  WorkingDirectory  fields in Section 3.5.1 if you clone the repository to a different location.

Make sure your location is accurate; this not only enables auto_rx to calculate the distance of the radiosonde, but can also be optionally shown on the map when uploading to or (so maybe fudge the numbers slightly!)

station_lat = -37.987
station_lon = 145.123
station_alt = 10.0

Additionally, you might want to keep the last few days' worth of radiosondes loaded; by default they're no longer shown after 2 hours.  Just change the following setting:

archive_age = 120

Be aware that setting this to higher values may result in slower loading times or memory consumption.

Data upload to

Next, we configure the device for habhub - open the station.cfg file in your radiosonde_auto_rx/auto_rx directory, and edit the following items:

habitat_enabled = True
uploader_callsign = YOUR_CS_HERE

If you want your station to be shown on the map, also update these:

upload_listener_position = True
uploader_antenna = Antenna make/model or description

That's it!  I've found that habhub tends to be quite busy and you will often get error messages relating to timeouts or server load; if you see this it doesn't mean that you've set it up wrong!  If you've opted to share your location, you can verify this on the map at

Integrating with habhub has the added benefit of giving the radiosonde_auto_rx web interface a clickable link for each sonde to view the live data from all stations and predicted flight path and landing site.

Data upload to

In the same station.cfg file as before, edit the following item:

aprs_enabled = False

Next, set your callsign and APRS password.  The APRS password is hashed from your callsign, which you can generate here.  Make sure you have the correct corresponding values, and enter them here:

aprs_user = YOUR_CS_HERE
aprs_pass = 00000

Finally, if you want your location to be visible to logged-in users of, enable the following:

station_beacon_enabled = True

That's it!  Make sure you start the server as per the instructions in the radiosonde_auto_rx wiki and you're good to go.  Happy hunting!

Monday, July 20, 2020

Adapting the Arduino LiquidCrystal library for I2C (and VFD)

Recently, I got my hands on a Samsung 16T202DA1 Vacuum Fluorescent Display module that I salvaged out of an old HTPC case.  As luck would have it, these are fully compatible with the Hitachi HD44780 protocol, making it easy to use with the Arduino LiquidCrystal library.

The project I had in mind for this - a standalone, ESP8266-powered data display, didn't have enough pins to support both the display and the other components I wanted to hang off it.  To work around this, I picked up a couple of I2C LCD backpack adaptors, which can be found for well under $1 each at your favourite Chinese electronics marketplace.

These modules employ the PCF8574 chip, which is a "remote 8-bit I/O expander for I2C bus", which is basically a fancy name for a serial to parallel convertor; that is - it takes one byte of serial data in (via the I2C, or two-wire bus), and expands that out into 8 individual digital pins as bits.

Modules, How Do They Work?

I wasn't able to find much else in the way of documentation on how the chip was implemented on the module, so after using a multimeter to figure out which of the PCF8574's pins actually connected to the LCD, I built a small rig on a breadboard to analyse what pins are affected by what input on the I2C bus:

From this, I was able to determine the following byte-to-pin mappings:

      Bit          7       6       5       4       3       2       1       0   
Pin D7 D6 D5 D4 K EN RW RS

A nifty little feature of the backpack is the ability to control the backlight of the LCD.  This isn't part of the HD44780 protocol, but an independent input that's normally driven from a separate GPIO.  The I2C backpack implements this by switching the LCD’s cathode (pin 16) between open and ground through an NPN transistor (note this is opposite behaviour to all other pins, which are set high or low).

So all up, we've got 8 pins of output from only 2 GPIOs... but how do we use this with the ubiquitous LiquidCrystal library?

Stick to the Protocol

First of all - just a very small bit of background as to how the HD44780 works.  These have a total of 16 pins - two for power, two for backlighting, one for contrast, 8 for data and 3 for control.  The 8 data pins accept 8 individual data bits in parallel, however we also have the option of operating in 4-bit mode, meaning we only need four of the data pins (but everything takes twice as long to send, as we have to send each byte in two halves.)

To differentiate between a display data and command data, we use the RS, or "register select" pin.  Commands include functions such as display initialisation, showing or hiding the cursor, or scrolling the display.

To send data, we hold each of the corresponding data pins high or low, representing the bits that we want to send.  Once we've got all the data pins set to their appropriate values, we send a quick pulse to the "enable" pin, which signals to the device to read the pins as they're currently set.  Each byte of data sent in this mode will be displayed on the LCD according to the current parameters.

If you want a bit more of a deep-dive on how this all works, the 8-Bit Guy does a great video on the subject which you can find here.

Adapt, Improvise, Overcome

I’m aware that several libraries exist already – however I found most implementations to be a little slow; the VFD doesn’t suffer from the same poor response rate as the LCD, meaning animated effects look a lot nicer – but this calls for a faster-performing library.  In addition, I couldn’t find any libraries that implemented the VFD’s brightness control functions, and I wanted something that was as close to a drop-in replacement for the existing LiquidCrystal library as possible.

First off, we can tidy up the existing code a little by eliminating 8-bit mode from the library.  We’ve only got 8 bits of output from the PCF8574 module – of this, we need to be able to not only send data, but also toggle the register select pin and the enable pin as well as control the backlight., so we’re forced to use the HD44780’s 4-bit mode for data entry.

Most of the magic happens in in the write4bits() function.  In the original library, this simply splits the input into four bits, and writes this to their corresponding pins using  digitalWrite(), then sends a quick pulse to the "enable" or "E" pin through the pulseEnable() function:

void LiquidCrystal::write4bits(uint8_t value) {
  for (int i = 0; i < 4; i++) {
    digitalWrite(_data_pins[i], (value >> i) & 0x01);


This is where we need to do 99% of the work.

Instead of writing to four GPIOs, we need to redirect these writes to I2C.  We do this by taking the 4 bytes of data, shifting these left four places (you’ll remember earlier that that bits 4 - 7 are our data bits), and appending our other pin settings (RS, E and backlight) for bits 0, 2 and 3.  As we never read data from the LCD, "RW" is always held low.

Because the LCD backlight isn’t part of the HD44780’s control functions, we also send the backlight status with every command; if we don't, it will switch off every time we send fresh data!

In order to speed things up, we can take a shortcut around the pulseEnable() function.  It turns out, we don't need to pulse the "E" pin; it's only once the "E" pin is returned to low that the data is read by the LCD.  Instead of setting the data pins and then sending a quick pulse to "E", we can set the "E" pin at the same time as our data pins, and then set "E" to low, saving us a a third of our I2C bandwidth:

void LiquidCrystal::write4bits(uint8_t value, uint8_t mode) {
  uint8_t bits = ((value & 0x0F) << 4);    // mask 4 data bits and shift left
  writeToI2C(bits | _backlight | (mode & RS_PIN));

void LiquidCrystal::writeToI2C(uint8_t bits) {
  Wire.write(bits | EN_PIN);               // write data bits + enable bit
  delayMicroseconds(1);                    // enable pulse must be >450ns
  delayMicroseconds(50);                   // commands need > 37us to settle

A slight modification is required to enable the write4bits() to be able to set the "RS" pin; in the stock Arduino library, this is set by a previous function, where they had the luxury of being able to set pins independently.  This is provided by including the uint8_t mode parameter.

All that's left is to incorporate the LCD backlight control, which is handled by the backlight() and noBacklight() commands:

void LiquidCrystal::noBacklight() {
  _backlight = LCD_BACKLIGHTOFF;

void LiquidCrystal::backlight() {
  _backlight = LCD_BACKLIGHTON;

And finally, incorporate VFD backlight control (after finding the relevant command from the datasheet) with the setBrightness() command:

void LiquidCrystal::setBrightness(uint8_t value) {
  _displayfunction &= ~0x03;
  _displayfunction |= (value & 0x03);
  command(LCD_FUNCTIONSET | _displayfunction);

Seriously - that's it!  I talk a big game, but this did take me a good week or more to reverse-engineer the module, build the test rig and adapt the library (though I'm sure a competent engineer could knock it over in half a day!)

Here's a short video of it in action with the VFD:

I wrote a quick bit of test code to see just how quickly the library performed versus other comparable implementations, and found it to be 68% faster (largely due to the pulseEnable() shortcut, as best as I can tell.)  Take that, other implementations!

Visit Your Local Library

You can download the library from my GitHub, which includes full detail on how to use this or adapt existing implementations. 

Tuesday, May 19, 2020

Selection between RTL-SDR device indexes (as a command-line parameter)

If you have multiple RTL-SDRs in attached to system for monitoring various services, you'll know that in most cases, you need to specify a device index to make sure you've got the correct device selected.

In my case, I'm watching POCSAG on 148 MHz, ADS-B on 1090 MHz and a weather station on 433 MHz.  Different frequencies need different antennas, so it's important I select the right one; I've found that the device index follows no pattern (it's been reported it's based on the order it's plugged in, but I've got devices that will take on index 0 even if another device already has it).

To achieve this, we need to be able to do three key things:
  • Uniquely identify each device in software
  • List all RTL-SDRs in a system by index and unique identifier
  • Retrieve the index that can be supplied in-line to existing tools

To uniquely identify the device, we can leverage the device serial number.  Fortunately, this turns out to be a user-editable value using the rtl_eeprom tool from the rtl-sdr library:

rtl_eeprom -d 0 -s 00000666

Just plug one device in at a time to ensure you've got the right one!  Everything going to plan, you'll see the message Configuration successfully written once the serial is updated.

Next, we need to enumerate all devices in the system - but we have to cheat a little bit here.  Like many of the tools in the rtl-sdr library, most will list all devices as part of their output.  We use the rtl_sdr tool for this, but the cheat is that we need to specify an invalid device index to get it to enumerate all devices in a way we can scrape the output:

pi@pitwo:~ $ rtl_sdr -d 9999
Found 3 device(s):
  0:  Realtek, RTL2838UHIDIR, SN: 00000001
  1:  Realtek, RTL2838UHIDIR, SN: 00000002
  2:  Realtek, RTL2838UHIDIR, SN: 00000666

No matching devices found.
rtl_sdr, an I/Q recorder for RTL2832 based DVB-T receivers

Usage:   -f frequency_to_tune_to [Hz]
        [-s samplerate (default: 2048000 Hz)]
        [-d device_index (default: 0)]
        [-g gain (default: 0 for auto)]
        [-p ppm_error (default: 0)]
        [-b output_block_size (default: 16 * 16384)]
        [-n number of samples to read (default: 0, infinite)]
        [-S force sync output (default: async)]
        filename (a '-' dumps samples to stdout)

Now all we need to do is grep the output for the serial number of the device we want, and return the corresponding device index:

rtl_sdr -d 9999 |& grep -P -o "\d(?=:\s*.+SN: 00000666)"

Which returns only the device index (2 in this instance).  If you're playing along at home, the above regex comprises the following:
  • \d: Any single digit
  • (? ): Lookahead query
  • .+ : Match any number of any characters
  • : and SN: 00000666: Strings to match

From here, you can slip it inline with an existing command using $():

$(rtl_sdr -d 9999 |& grep -P -o "\d(?=:\s*.+SN: 00000666)")

For example - here's rtl_fm (piping into multimon_ng for the sake of completeness):

rtl_fm -A lut -s 22050 -f 148.9125M -d $(rtl_sdr -d 9999 |& grep -P -o "\d(?=:\s*.+SN: 00000666)") -g 17.9 | multimon-ng -t raw -a POCSAG512 -a POCSAG1200 -a POCSAG2400 -f alpha /dev/stdin

It's worth noting that certain tools, such as dump1090, allow you to specify a serial number instead of device index - always use the device serial as the identifier wherever available!

Monday, November 26, 2018

Reverse-engineering a 433MHz remote-controlled power socket for use with Arduino


The 433 MHz ISM band is a RF band often used for low-power, short range wireless communication in devices such as garage door openers, wireless weather stations, and remote-controlled power sockets, like this device:

The device is an Olsent RCS-5A which I picked up from a local clearance shop for the bargain price of $9, and three additional slave units at $6 each.

The end goal of this project is to reverse-engineer the protocol this device uses to communicate, and interface this with a dirt-cheap 433 MHz transmitter module attached to an Aduino Nano (and fully portable to ESP8266) to enable custom control of attached devices.

There's a couple of approaches that can be taken to decoding the signal.  I've previously used an Arduino Mega as a logic analyser with an IR receiver module for decoding infra-red; the same could be done here with the 433 MHz receiver module.  Instead, I've opted to use an RTL-SDR together with Universal Radio Hacker, which is designed to exact and decode this exact kind of thing.

Everything required:

Not a lot to this one at all - it almost takes some of the fun out of it!

  • Software-Defined Radio dongle
  • Arduino Nano (any Atmel-based model will work)
  • 433 MHz RX / TX modules (only the TX is needed)
  • A copy of Universal Radio Hacker (

Assuming you've already got the SDR, you're up for less than $5 in parts.

Capturing and decoding the signal

If you're playing along at home, fire up URH and click on "Record Signal", select your model of SDR, enter the frequency (usually written on the device) and click the start button.  Trigger your device (try a few different durations to be safe), click stop and save your captured signal, and if you're end up with something like this:

This modulation is a form of Amplitude Shift Keying, or ASK, known as On-Off Keying (OOK), the same type as the 433 MHz module implements.  Zooming in a bit, you should be able to understand how it works:

Pulses lengths are precisely timed to within a few microseconds.  If the signal is present for a pre-defined duration (in the above sample, around 350 microseconds) a 1 is recorded, and a 0 if absent.  In the above example, a sequence of 16 bits is highlighted.

URH did a pretty good job of auto detecting the parameters, and with some mild fine-tuning of the bit length, I was able to obtain consistent results, such as the following sequence:


That's exactly 100 bits... but there's a pattern here.  You'll notice that each 4-bit sequence is either 1000 or 1110, which means that the 25th bit is actually the start of a 1000 sequence - but because the transmitter's last three pulses were "off" , we don't know they're there.

Assuming 1000 is a 1 and 1110 is a 0, we can compress this down to the following sequence:


Which represented as hexadecimal, becomes:


Which looks like complete crap in ASCII... let's stick with hex.

Repeating the above for each of the 8 buttons, we get the following codes:

Button On Sequence Off Sequence
1 1FD7755 1FD7757
2 1FD5F55 1FD5F57
3 1FDD755 1FDD757
4 1FF5755 1FF5757

Hex is a great way to display data that's aligned on 4 or 8-bit boundaries - this probably isn't.  My best guess is that at the bit level, the data is split into an initial 7 bits (all high), an 8-bit device address, an 8-bit constant and a 2-bit command, like so:


To make the data a bit easier to work when using C/C++, we can break this up a bit differently.

By discarding the first bit and automatically sending it at the start of every sequence, we can split the remaining 24 bits into 16-bit address and 8-bit command.  For any given button, we simply need to send a initial "1" bit (which you'll remember was one high pulse and three low pulses) , followed by the device address (e.g., FDD7) and finally the command (55 for on or 57 for off).

Building the circuit

This is about as easy as an Arduino circuit gets:

To drive the 433 MHz module, you simply need to use the digitalWrite() function; as long as the pin is held high, a constant signal will be sent from the device.  If we modulate the signal by timing our digitalWrite() calls accordingly, we've got ourselves a pretty good approximation of the original OOK-modulated signal sent by the transmitter unit.

For the antenna, I'm using a 1/4-wave monopole, which is a 172mm wire attached to the antenna pin.  Ideally, a 1/2 wave would perform much better, but at 344mm is somewhat impractical.

Writing the code

I could use one of the many libraries that are available for the 433 MHz units, but there's no fun to be had that way!

Rolling-your-own code for these kind of implementations really makes you think about how they work at a logical level, and often saves precious flash space by only implementing the functions your code uses.
Implementing this in code can be broken down into two main tasks:
  1. Extracting the bits of the 'compressed' version of the sequence
  2. Sending the bits as their relevant sequence of 4-bit pulses
Here's how I've gone about it:

#include <Arduino.h>

#define HIGH_PULSE 0x8  // 4-bit pulse sequence representing a logical 1
#define LOW_PULSE 0xE   // 4-bit pulse sequence representing a logical 0
#define BIT_LEN 360     // length of individual bit pulses

#define SEQ_LEN 24      // total number of bits in sequence
#define SEQ_RPT 4       // number of times to repeat full sequence (requires a minimum of 3 to register)
#define SEQ_PAUSE 11500 // time in microseconds to pause between repeated sequences

#define CMD_LEN 8       // total number of bits in command
#define CMD_ON 0x55     // on command sequence
#define CMD_OFF 0x57    // sequence for off command

#define DEV_1 0xFD77   // device 1 address sequence
#define DEV_2 0xFD5F   // device 2 address sequence
#define DEV_3 0xFDD7   // device 3 address sequence
#define DEV_4 0xFF57   // device 4 address sequence

#define TX_PIN 2        // physical pin to which 433MHz module is connected

/* send 4-bit pulse sequence representing logical 1 or 0 */
void send_pulse(bool state) {
  uint8_t pulse = state ? HIGH_PULSE : LOW_PULSE;  // determine pulse sequence based on state

  for (uint8_t j = 4; j > 0; j--) {
    bool bit = pulse >> (j - 1) & 1;               // extract individual bits from pulse from most to least significant bit
    digitalWrite(TX_PIN, bit);                     // set pin state based on pulse bit
    delayMicroseconds(BIT_LEN);                    // hold pin state for the bit length time
    digitalWrite(TX_PIN, LOW);                     // pull pin low once bit sent

/* send command to specified device address */
void send_command(uint8_t chan, bool state) {
  uint16_t addrs[] = {DEV_1, DEV_2, DEV_3, DEV_4}; // load device addresses into array
  uint8_t command = state ? CMD_ON : CMD_OFF;      // determine command sequence based on state
  uint32_t addr = addrs[chan - 1];                 // determine address of device by index
  uint32_t seq = addr << CMD_LEN | command;        // concatenate address and command sequences

  for (uint8_t i = 0; i < SEQ_RPT; i++) {          // repeat command for specified count; several sequences required for command to register on reciever
    send_pulse(1);                                 // send initial bit pulse sequence

    for (uint8_t j = SEQ_LEN; j > 0; j--) {
      bool bit = seq >> (j - 1) & 1;               // extract individual bits from sequence from MSB to LSB
      send_pulse(bit);                             // send pulse sequence of extracted bit

    delayMicroseconds(SEQ_PAUSE);                  // delay between repeated sequences

void setup() {
  pinMode(TX_PIN, OUTPUT);

void loop() {
  send_command(1, 1);                              // turn device 1 on
  delay(1000);                                     // wait one second
  send_command(1, 0);                              // turn device 1 off
  delay(1000);                                     // wait one second

I've commented the code pretty heavily - most of it should be pretty self explanatory.  For the sake of demonstration, this just flicks receiver #1 on and off with a delay in between.  By changing the first parameter of the  send_command() function for another address, any of the four devices can be controlled - no libraries needed!

Testing it all 

For whatever reason, the device won't respond to a single pulse - I fired up URH again and sure enough, if I press the button on the original remote quickly enough to trigger only a single pulse, the device doesn't respond.  Further testing showed that at least four pulses were needed for the device to trigger consistently.  This is the purpose of the #define SEQ_RPT 4 macro.

So after a quick change to the code, everything works as expected; I can control any of the four devices from almost anywhere in my house.

Here's the output from my device, captured using Universal Radio Hacker:

So, what now?

I've got a few ideas in mind for this; this is most ideal for switching circuits that are mains powered for home automation purposes- such as lights or some appliances.  This isn't necessary for most devices that use step down or switching power supplies - we could use a simple low-voltage relay to interrupt the low-voltage side of the supply for these.

It's worth noting that it's only a small jump from here to interface with Blynk to enable you to even build your own mobile apps - that's one for another post in itself!

Thursday, July 19, 2018

The perfect multi-button input resistor ladder

A resistor ladder is a brilliant but super simple way to attach multiple buttons to a single analog to digital converter (ADC) pin on an Arduino or other microcontroller.  By expanding on the concept of the voltage divider, the resistor ladder offers a way to create various reference voltages by tapping into each 'rung' of the ladder.  Consider the following schematic:

By attaching our analog pin between the first and subsequent series resistors and a button to earth at each 'rung', we're effectively creating a variable voltage divider.  If we measure or calculate the values read by our analog pin, we can interpret each reading as a different button being pressed with a few lines of code:

#define INPUT_PIN A0

void setup() {
  pinMode(INPUT_PIN, INPUT);

void loop() {
  int result = readAnalogButton();

int readAnalogButton() {
  int button = analogRead(INPUT_PIN);
  if (button > 921) return 0;
  if (button < 256) return 1;
  if (button < 598) return 2;
  if (button < 726) return 3;
  if (button < 794) return 4;
  if (button < 921) return 5;

Rather than telling our code to look for the exact values we expect the analog pin to read, we set a range of values that can be interpreted as belonging to a specific button.  We do this by calculating the points halfway between each expected ADC reading and set these as as the boundaries of our ranges.  Just like that, we've got 5 buttons on the one pin!  We're not done just yet though, because this is just about the worst possible way we could implement this.

Assuming perfect resistors, the voltage readings for each button should be around 0.0V, 2.5V, 3.33V, 3.75V and 4.0V, with corresponding ADC readings of around 0, 512, 683, 768 and 819.  You might have already noticed a trend by this point...

If we add another 5 switches each with a 1kΩ resistor, the subsequent voltages will be 4.17V, 4.29V, 4.38V, 4.44V  and 4.5V, and will continue to grow logarithmically with each 1kΩ resistor we add:

This might work for a project with a small number of buttons if you measure each value, and calculate and hard-code your ADC boundaries for each button - but magic numbers are bad, and you should feel bad.

If we change the voltage scaling from logarithmic to linear, each adjacent ADC value will become roughly equidistant.  This means that we can dynamically calculate the corresponding button for any given reading, rather than hard-coding fixed values.  This allows us to use the same piece of code for any number of buttons and as a bonus, uses the full range of our ADC - in the above example, we jump straight from 0 to 512 between the first two buttons - that's half our resolution!

So, how do we know what resistors we should use?

For the following example, I've used a 47k Ω resistor for R1.  I've chosen this value as it allows us to operate the circuit at a super low current (by Ohm's law, 5V / 47kΩ = 0.11 mA) and still use a wide-range of commonly available resistor values.  We'll use 5 buttons total, and calculate the value for button 1 (which is actually the second button, like a zero-indexed array).

Since we have a fixed value of R1 we can work out the value for R2 using the following formula:

Rbutton = R1 / (1 - (Bindex / Btotal)) - Rtotal

Where Bindex is the index of the button in question, Btotal is the total number of buttons and Rtotal is the combined value of all resistors in the circuit prior to the current (in this case, it's just the 47kΩ for button 0).  So from the above, we have 47000 / (1 - (1 / 5)) - 47000 = 11750Ω.  We can use a lookup table to find our closest equivalent value, which in this instance is 12kΩ.

If we do this for the remaining buttons, we get values of 20k, 29k and 120k, which should give us corresponding ADC readings of around 0, 208, 415, 616 and 822.

That's still a bit of a process to work out - especially if you've got more than a handful of buttons, which is why I made this Google Sheets spreadsheet to do all the heavy lifting!  Just enter the number of buttons you want to use and the tolerance of your resistors (which is only a consideration with a very large number of buttons - otherwise you can leave this at the default 2%.)

The column you're most interested in is "Closest R" - this performs a lookup of the closest common resistor value to the calculated required value.  The "Req'd" columns indicate the exact values we're looking for, while the "Actual" columns indicate the voltages for the calculated closest available resistor.  "Variance" simply tells us the difference between the current and previous ADC reading - if the result here is greater than the specified resistor tolerance, this will be indicated by the "In Tolerance" column.

Great - so we've got our resistors all worked out - all that's left now is the code!

#define BUTTONS 5
#define RESOLUTION 1023

int readAnalogButton() {
  float avg = RESOLUTION / float(BUTTONS);
  int val = analogRead(ANALOG_PIN);

  if (val > (BUTTONS - 0.5) * avg) { 
    return 0; 

  for (int i = 0; i < BUTTONS; i++) {
    if (val < round((i + 0.5) * avg)) { 
      return i + 1; 

  return 0;

We start by determining the average difference between adjacent ADC readings, enabling us to dynamically calculate each button's expected ADC reading.  To test for an open circuit - the most likely result - we check if the measured value is less than halfway between 0 and our dynamically-calculated reading for the first button.  If this evaluates to false, we begin by testing for each button in turn, comparing our ADC reading against the range boundaries that lie between adjacent readings, until we find a match.

Note that because "no button pressed" is a valid result in addition to each individual button press, we treat a zero value as an open circuit, and commence indexing our buttons from 1, as opposed to the zero index used during calculation.

With the above code, we can scale up and down by simply changing the value defined by the macro #define BUTTONS 5, with the number of buttons only limited by the tolerance of your resistors and resolution of your microcontroller's ADC. We'll see just how far we can push this in a future post!

Thursday, May 31, 2018

Postcards from the ISS

A few years ago, I learned of slow-scan TV transmissions from the International Space Station.  SSTV is essentially a form of narrowband television - a single frame, taking anywhere from a few seconds to a few minutes to transmit, used mainly in amateur radio applications.

The SSTV images from the ISS are transmitted by ARISS ( to commemorate various events and anniversaries.  Usually, a series of 12 images will be transmitted over the period of a few days, allowing anyone with the right equipment to have a chance at capturing and decoding the images as the ISS passes overhead.

Most recently, a series of images celebrating the 40th anniversary of the Interkosmos Soviet space program were transmitted between the 11th and 14th of April.  These were transmitted in PD120 format, which take 120 seconds for a full frame at 640x496.

I recently got my hands on an RTL-SDR dongle ( and a 25-1300 MHz discone antenna and was keen to see what I could receive.

The first step was to determine when the ISS would be passing overhead using Heavens Above (  Make sure you select all passes (not just visible only) - this will show all passes of the ISS that are 10 degrees or higher above the horizon:

Next, I set up VB-Cable ( to route audio output back to input, and configured MMSSTV ( to decode the images.  Both programs require only a minimum of configuration and work largely out-of-the-box.

A few minutes before the ISS was above the horizon, I used SDR Sharp ( to tune the SDR to 145.800 MHz with a gain of around 33dB, and soon started to receive the transmission:

You'll see above that I'm tuned slightly off the centre frequency.  This is due to Doppler shift as the ISS approaches, causing the signal to appear slightly higher than transmitted - as it crosses overhead and moves away from the antenna, the frequency slightly and gradually decreases.

With everything in place, I had several attempts over the course of a few days with some moderate success:

...and quite a few failures:
Even though I'm reasonably happy with the result, I was able to learn plenty that should help me achieve even better results next time.

Receiver gain can be both your friend and enemy!  Depending on if the ISS is directly overhead or approaching the horizon, it can be anything from just over 400km to a few thousand kilometres away, requiring the receiver gain to be set to a reasonably high level to receive a clear signal.  ISS transmissions sit in the 2-meter band which in Australia, and particularly major cities, is adjacent to the band heavily used for POCSAG pager traffic.  POCSAG transmissions are very high power, which results in interference in adjacent bands when the receiver gain is increased. This results in artefacts in the received images (visible in some of the above images as horizontal lines of interference.)  A notch filter may be effective in filtering these to some extent.

Before the next series of transmissions occurs, I'll build myself a tape measure Yagi antenna and maybe move somewhere a bit more radio-silent.  The directivity of the antenna will increase the gain of the received signal and the narrow bandwidth of the Yagi decrease the gain of interfering neighboring frequencies (at least on paper!)

You can listen to one of the better transmissions below.  If you're setting this up for yourself, you can use this to test that VB-Cable and MMSSTV are configured correctly.

With any luck, we'll see another series of transmissions before the end of the year.