Słomkowski's technical musings

Playing with software, hardware and touching the sky with a paraglider.

Octoglow VFD - Clock display board

Second module of Octoglow, which contains numerical VFD to display wall time and 433 MHz weather sensor receiver.

Wall time is the most important information which Octoglow should provide, it is reasonable to have separate display for it. Having this in mind, the design principles shifted to more complicated contraption, having multiple boards and bigger enclosure. Octagonal steel chassis was devised at this time. The new board itself features the following:

Update May 2021: Octoglow was rebuilt, some changes where made: the bell was removed, pair of speaker were installed instead, they are driven from Orange Pi’s audio output. BIOWIN weather sensor was replaced by two Fanju radio sensors, software was modified accordingly. Old info stays for reference.

PCB layout was designed in KiCad, all project files are located in pcb/clock-display/. The heart of the board is ATtiny461A microcontroller which acts as I²C slave on the system bus. It interprets the packets from 433.92 MHz receiver, controls the relays and sends the appropriate content to the VFD driver. There are following connectors on the board:

Micro-MaTch are not popular, but they were chosen because of the compactness and overall good look. It was important to me because the board’s front side was meant to be exposed, housing prominent filament resistors and relay connectors.

The VFD itself is salvaged from the pile of scrap electronics I found at the roadside when I was riding my bike. I took the weathered PCB home and examined the display. I was undamaged! I decided to give it a new life and purpose.

The display is quite unusual because it is not multiplexed. Every anode has its own pin and all grids are connected together - multiplexing is not possible in this setup. Overall, there are 30 pins to drive. I again used chip TD62C950RF which successfully works in main display. The grid is driven by TD62C950RF output, but in firmware it is effectively pulled high. The brightness is varied by driving CL ping by ATTiny’s TIMER 1 PWM output. Filament voltage is ~3 V, I used the method described in my other article to find proper values for resistors R8 and R10.

The original 433.92 MHz receiver module was RFM83C-433S (as seen in the schematic), but the performance was rather low, I later replaced it with pin-incompatible RFM210LCF-433D-A. The antenna is made of copper wire as shown on Instructables. Receiver’s output drives the ATTiny’s interrupt line. The protocol and the decoding algorithm is described below.

Relay driving circuit is straightforward. One relay is unused, the other switches the telephone bell. The bell is powered directly from supply transformer’s 24 V AC. In the telephone line, 25 Hz usually is used, but 50 Hz still does its job (for your information, mains power in continental Europe is 230 V 50 Hz). The ringing sound is almost indistinguishable from classic telephone ring. Telephone bell was removed, because updated Octoglow contains speakers with amplifier.

Two-pin header on the front side was meant as 1-Wire interface, but eventually it was left unused. There is also unused pin with PWM output meant for driving analog gauge meter.

Electrical wiring schematic.
Electrical wiring schematic.
↑ click to enlarge ↑


The code follows the same principle as for the main display; its root under firmware/clock-display/. It is quite simple, the only not-so-trivial routine is decoding data from receiver module. In its main loop, the software receives commands from I²C on address 0x10 and acts accordingly. It supports following requests:

Set display content

The display has four digits and separator dots.

'1'|digit 1|digit 2|digit 3|digit 4|dots
Value Size Description
1 1 Magic byte
digit 1 1 ASCII-encoded digit, leftmost.
digit 2 1
digit 3 1
digit 4 1
dots 1 Bitwise OR for display dots: 0-th bit = lower dot, 1-st bit = upper dot.

Set relay state

'2'|relay 1|relay 2
Value Size Description
2 1 Magic byte
relay 1 1 1 - switched on, 0 - switched off.
relay 2 1

Set display brightness

Value Size Description
3 1 Magic byte
brightness 1 From 0 (display shutdown) to 5 (max).

Get weather sensor data

The only read request. To trigger read, send 4 to the device, then read response:

flags|raw data
Value Size Description
flags 1 Valid measurement = 0x2, already read = 0x4.
raw data 5 Raw data (40 bits) from weather sensor. Contains sensor channel number. Needs to be decoded.

Weather sensor protocol - Fanju SP67

Fanju weather sensor.

My new weather sensor is branded Fanju, designated SP67. I found out that someone had already reverse-engineered the protocol used by this sensor. Transmission is OOK-modulated. Receiver puts demodulated signal at its output, which I recorded using sound card. Sample message, shown in the screenshot:

Waveform visualised in Audacity. Low state indicates carrier presence. This message corresponds to 22 deg C and 34%.

Message starts with four pulses of carrier followed by 8 ms of carrier. This is done to adjust the AGC in the receiver. Then 40 bits of data encoded as:

The payload includes checksum, which is calculated with algorithm which is published on aforementioned website. I myself reimplemented it in Kotlin in octoglowd daemon.

Bit Length Function
12 1 1 - transmission forced by TX button, 0 - automatic transmission.
13 1 0 - battery OK, 1 - weak battery.
16-27 12 Temperature value, big endian. Formula to calculate deg C below.
28-35 8 Humidity in %, BCD encoded.
38-39 2 Channel selected by switch: 00 - 1, 01 - 2, 10 - 3.
temperature = (temperatureBits - 1220.0) * 1.0 / 18.0

Old weather sensor protocol - Biowin

This sensor I used previously, labeled BIOWIN BIOTERM, is shown in the photos. I reverse-engineered the protocol by connecting the receiver’s output to PC sound card and analysing the signal with Audacity. The process is described in detail in many articles on the web, for example here.

The protocol description

Weather sensor sends its OOK-modulated data at 433.92 MHz. A couple of packets are sent every minute, less often when battery is almost depleted. Each transmission starts with several impulses to set the Automatic Gain Control in the receiver. Then, following rules apply:

Each single packet consists of:

and has following structure:

Bit Length Function
0-3 4 Always value 9.
4-11 8 Random number generated at power-on.
12 1 0 - battery OK, 1 - weak battery.
13 1 1 - transmission forced by TEST button, 0 - automatic transmission.
14-15 ² Address selected by switch: 00 - 1, 01 - 2, 10 - 3.
16-27 12 Temperature in 0.1 deg C. Signed, two’s complement.
28-35 8 Humidity in %.


The core of the algorithm is implemented in source file src/receiver433.cpp. Since this is legacy code, I tagged it in repository as biowin-weather-sensor.

Receiver is connected to hardware interrupt INT0 and each rising edge triggers the interrupt service routine. TIMER 0 is configured as counter, its purpose is to measure time between the subsequent interrupt calls. Interrupt service routine checks the time elapsed from its previous execution: if it is 4 ms +- 5%, 1 is saved to the packet buffer and the buffer is shifted left. Accordingly, when 2 ms +- 5%, 0 is saved. Other intervals are invalid and the buffer is cleared in this case.

As 36 bits are loaded, the content of the buffer is copied to spare buffer. The idea is to receive the identical packet once again. If their content match, the packet is assumed valid and appropriate fields in structure WeatherSensorState are set among with the flag new measurement ready.

TIMER1, which is used by the display to generate PWM signal, has also interrupt configured to fire in order to implement timeouts. When the timeout fires, the buffer is reset. This is done to reset the state machine in case of partially received packet.