This article describes using an RFM01 or RFM12b FSK RF transceiver with a Raspberry Pi to receive sensor data from a Fine Offset WH1080 or WH1081 (specifically a Maplin N96GY) weather station’s RF transmitter.
I originally used the RFM12b, simply because I had one to hand, but later found that the RFM01 appears to work far better – the noise immunity and the range of the RFM01 in OOK mode is noticeably better. They’re pin compatible, but the SPI registers differ between the modules, in terms of both register-address and function.
The weather station comprises an LCD display and a mast of sensors. The LCD display also incorporates temperature and humidity sensors for indoor readings, as well as a barometric pressure sensor. The mast contains sensors for temperature, humidity, wind-speed, wind-direction, and rainfall, and transmits data-packets using a 433MHz transmitter. It’s the data from the mast that this project is focused on, along with the addition of a Bosch BMP085 I2C module to get barometric pressure.
There are variants of the Fine Offset WH1080 with transmitters of differing types (e.g. different frequency, modulation, or both), so while this project will apply in principle to similar devices with different branding, in practice some tweaking may be required.
For anyone who already has a rough idea of what’s required, and just wants a quick overview for some pointers, I’ve presented a summary first below.
Wiring the Module
Note: the revision 2 boards affect this project in two ways. First, if you’re using the BMP085 pressure sensor, then you’ll need to address I2C-1 rather than I2c-0. Changing the base address of the I2C peripheral in the code should be all that’s required. Second, the GPIO pin on header P1-13 is now GPIO27 rather than GPIO21. Again, the code will need simple modifications. I’ll incorporate these into the code in future, but until then you’ll have to figure out where the changes go if you want to build this on Rev-2. Email me if you need help.
Pin connections from the RFM01 (and RFM12B) to the Raspberry Pi are as follows. You may choose to add current limiting resistors. Pull ups may be required for SDO and SDI (I’ve never found a definitive specification for SPI – it works with them, and it probably works without them too): -
SDO -> MISO (PU resistor?) SDI -> MOSI (PU resistor?) SCK -> SCLK SEL -> CE0_N FSK/DATA/nFFS -> GPIO21 (P1-13) ANT -> 17.3cm wire VDD -> 3V3 from Pi GND -> GND on Pi Optionally, you may want to drive nRES from a GPIO output (I used 'nRES -> GPIO22' in the code, though this will probably be removed after I finish experimenting).
I used a breadboard to wire this up, and unsurprisingly found this to be far from ideal with the RFM12b module. At close range, the receiver works very reliably, but as the signal weakens, noise often drowns it out (even just leaning close to the circuit, or moving a component).
A breadboard is fine to test the SPI stuff, and also works well when the transmitter is in close range, but to test the receiver properly, etching a PCB (or even just wiring the bits together) would be a better idea.
I decided to wire the RFM12b to a 26-pin female header to avoid breadboard noise, but discovered that the Pi was generating enough of noise of its own, and having the module (more accurately, the antenna) so close to the Pi was affecting reception. A long ribbon cable improved things, as did a length of coax to move the antenna away from the rest of the electronics.
When I switched in the RFM01 (after carelessly frying my RFM12b), the situation seemed much better, and the device worked more reliably than the RFM12b did in exactly the same environment.
The RFM01 seems far more immune to noise -it’s wired to a 26-pin header and plugged directly onto the Pi. The antenna is simply a 1/4 wavelength (17.5cm) wire soldered to the module. It receives perfectly even in this harsh environment, possibly in part because it works on the lowest LNA (Low Noise Amplifier) settings.
Any wires near the Pi are capable of generating RF noise, even just a breadboard jumper wire sitting underneath the Pi, or an unshielded Ethernet cable. Be aware of this and keep noise sources out of the way while testing (or wire the antenna on a length of 50Ω coax).
The transmitter sends 11 bytes of data as follows. Some of these differ from any of the documentation I’ve found on Fine Offset weather stations, so I’ve described them here (letters to the nibbles for the description below): -
Byte 0 1 2 3 4 5 6 7 8 9 Nibble ab cd ef gh ij kl mn op qr st Example a1 82 0e 5d 02 04 00 4e 06 86
I’ve interpreted the nibbles as follows: -
- abc: device identifier
- def: temperature
- gh: humidity
- ij: average wind speed low byte
- kl: gust wind speed low byte
- m: unknown
- n: rainfall counter high nibble
- op: rainfall counter
- q: battery-low indicator
- r: wind direction
- st: checksum
The nibble ‘q’ is likely to be the low-battery indicator, thanks to Ken McCullagh reporting that his transmitter is now sending a ’1′ rather than a ’0′, and his LCD display is also now showing a low-battery symbol.
Nibble ‘m’ is unknown. It may be that the rainfall counter is two whole bytes (time will tell – I may take a watering can to the rain collector), or perhaps it’s the high nibble of either the wind-speed or gust-speed. Between them, nibbles ‘m’ and ‘q’ could provide this extra wind data.
To get temperature, subtract 400 (0×190) and divide the decimal by 10. For example, reading 0x2oe – 0×190 = 0x7e, which is 126. This represents 12.6 degrees C.
To get a wind speed values (in m/s), multiply the value by 0.34 and round the result to one decimal place. So, value 0×04 is round(4 * 0.34) = 1.4 m/s.
For rainfall, as documented elsewhere, multiply by 0.3 to get the recorded rainfall in millimetres. This value is a counter that increments to zero after 0xff (or 0xfff if it turns out that nibble ‘q’ is part of the counter).
Humidity is simply displayed as a decimal, and wind direction ranges from 0 to 15 for N, NNE, NE, ENE, E, etc., as per the LCD display. The checksum is CRC8, and a nicely written function, adapted from the OneWire Arduino Library by Luc Small, is included in the source code to calculate this checksum.
You can download the source code from here. Unpack the tarball on the Pi, cd into the created directory, and run ‘make’. This should generate the ‘wh1080_rf’ executable.
If you’re using a cross compiler, the you may need to edit the CC variable in ‘Makefile’ to point to your compiler. If you’re using an RFM12b, then edit file ‘rfm01.h’ and change ‘#define RFM01′ to ‘#define RFM12B’.
The program configures the RFM01 using SPI, and then continuously looks for interesting data from the module on the Pi’s GPIO pin, and dumping valid packets to stdout as hex strings. It also decodes the hex-data into more meaningful values, and sends them to stdout as printf() formatted strings.
Note that the calculations on the Raspberry Pi are done using floating-point arithmetic, whereas the WH1080 uses fixed-point. This means that the output may differ slightly between the Pi and the LCD display, because the Pi’s calculations are more accurate. For example, raw wind speed of 0×01 yields 0.34m/s on the Pi, but only 0.3m/s on the WH1080. Converting this to mph yields 0.8 mph on the Pi, and 0.7 mph on the WH1080. Therefore the Raspberry Pi reading is more accurate.
You may notice that the output below also displays barometric pressure and an indoor temperature value. These have come from a Bosch BST-BMP085 Barometric Pressure Sensor via I2C. Wiring the module to the Pi was trivial, since it has the pull-up resistors and bypass capacitors already on-board. Therefore, simply wiring SDA and SCL, along with 3V3 and GND, to the Pi is enough to get this sensor working. If you don’t plan to use one of these, remove the line ‘#define USE_BMP085′ in wh1080_rf.h before compiling.
The output from the software includes a scan of RSSI duty on all amplification levels, RSSI thresholds, and baseband bandwidths. This lets you see where the noise is in your environment so that you can spot likely configuration values that will yield reliable results. I’ve omitted this from the example below for clarity.
root@pi:~/wh1080_rf# make gcc -c -Wall wh1080_rf.c gcc -c -Wall bcm2835.c gcc wh1080_rf.o bcm2835.o -o wh1080_rf root@pi:~/wh1080_rf# ./wh1080_rf Initialising RFM01 SPI: mode 0, 8-bit, 1000 KHz RSSI Duty 0.00 Listening for transmission Data bits = 88 (offset 8) (0 short) Frequency deviation 0.0KHz (0) a1 82 0d 5a 03 06 00 4e 08 d3 crc ok (gap 48s) Pulse stats: Hi: 478 - 658 Lo: 1441 - 1687 (88 point) Threshold now 1049 Temperature: 23.3C Pressure p0 (sea level): 1006.6 hPa Station Id: 0A18 Temperature: 12.5C, Humidity: 90% Wind speed: 1.0 m/s, Gust Speed 2.0 m/s, S Wind speed: 2.3 mph, Gust Speed 4.6 mph, S Total rain: 23.4 mm Listening for transmission Data bits = 88 (offset 8) (0 short) Frequency deviation -1.0KHz (-1) a1 82 0d 59 02 05 00 4e 0a 07 crc ok (gap 48s) Pulse stats: Hi: 491 - 721 Lo: 1531 - 1720 (88 point) Threshold now 1126 Temperature: 23.2C Pressure p0 (sea level): 1006.8 hPa Station Id: 0A18 Temperature: 12.5C, Humidity: 89% Wind speed: 0.7 m/s, Gust Speed 1.7 m/s, SW Wind speed: 1.5 mph, Gust Speed 3.8 mph, SW Total rain: 23.4 mm
The data-bits are the number of bits seen in this packet, including preamble, and the offset is the position of the device-id byte (or zero if it wasn’t found). The short packet count gives an indication of how much noise is being seen as data.
The ‘gap’ value tells us how long it’s been since we’ve seen a valid packet, and the stats show the pulse widths (in microseconds) that the receiver’s demodulator sent to us. The frequency deviation shows the values of the AFC offset registers. The AFC is manually strobed on each successful packet, in order to slew the frequency offsets with frequency drift.
Prior to a reading, the code switches the process to the Kernel’s built-in realtime (SCHED_RR) scheduler policy so as not to miss any bit transitions on the GPIO pin, and this is enough to make the process immune even to heavy workloads. I’ve tried running multiple CPU and IO intensive processes while reading, and didn’t see any packet loss. This may not hold true over time but, if not, there are sysctl parameters for the scheduler that can be altered to improve latency, for example: -
root@pi:~# sysctl kernel.sched_wakeup_granularity_ns=50000
In any case, it should be possible to run weather software and perhaps a web server on the Pi at the same time, while reliably taking RF sensor readings. I haven’t got that far yet.
Note that the transmitter actually sends two packets in succession on every other transmit cycle. It sends a packet, and repeats it 100ms later. I presume this is to increase receive reliability on the LCD display, or perhaps their receiving circuit sometimes fails to set AGC appropriately with a single packet. The code will ignore any subsequent data after receiving a valid packet until the next 48 second gap elapses.
Tuning the RFM01 Settings
The RFM01 configuration will depend on your environment. The following lines of code are most relevant: -
uint16_t cmd_config = CMD_CONFIG|BAND_433|LOAD_CAP_12C0|BW_67; int16_t cmd_rcon = (CMD_RCON|RX_EN|VDI_DRSSI|LNA_MEDIUM|RSSI_97);
The CMD_CONFIG (the module settings) command includes the receiver bandwidth setting. This can be one of BW_67, BW_134, and BW_200, BW_270, BW_340, and BW_400, and the value determines how sensitive the receiver is, but wider bandwidths mean more susceptibility to noise.
The CMD_RCON (receiver settings) command sets the Low Noise Amplifier setting (LNA_LOW, LNA_MEDIUM, LNA_HIGH, and LNA_MAX), and the RSSI threshold (dBm) at which we consider a signal to be valid. The VDI setting follows RSSI in the above example, because I’m not sure how relevant the other settings are to an OOK signal.
If you launch the program with any parameter, it will enter RSSI mode, which continuously samples DRSSI with the current settings, and reports the duty (percent) of the DRSSI signal from the SPI status register. You can use this to figure out the best combination of settings for your setup.
I find that the lowest total-gain/bandwidth combination that reports a small amount of noise (e.g. < 5%), or that reports zero-noise close to that boundary, gives the best results. If you’re settings are too sensitive, you’ll lose signal strength in the noise. If they’re not sensitive enough, you won’t maximise your potential reception.
For weaker signals/longer ranges, you may have to allow for variations in the environment – heavy rain can affect reception, outside temperature variations will affect the transmitter to some degree.
If the transmitter is close enough to the receiver, most settings that don’t saturate the receiver with noise will work fine, particularly with an RFM01 module.
Oscilloscope and Logic Traces
The output from the transmitter looks like this in my logic analyser: -
This is the signal that comes from the the transmitter’s microcontroller to the RF part of the circuit, and is the modulation of the 433MHz carrier. I think this modulation is called RZI PWM (Return to Zero Inverted, Pulse Width Modulation), RZI because the fixed clock-pulse always returns to ‘not-zero’, and PWM because pulse width defines the data bits.
The narrow pulses are binary-one, the wide pulses are binary-zero, and these are interspersed with 1ms clock pulses. The first eight narrow pulses are the 0xff preamble, and the next eight pulses represent the first data byte (always 0xa1, the device id).
When it gets to the DATA pin on the RFM01 receiver, the signal looks something like this:-
Note that it can now be seen as RTZ PWM (Return To Zero, Pulse Width Modulation) because the clock-pulse is now inverted (i.e. at zero) and the data pulses are at 3V3. Again, it’s the pulse-width that determines the data (and hence why this isn’t actually an OOK signal, and why the RF modules are being used out of specification).
This oscilloscope output is what our software is sampling via the GPIO pin. It samples hi-low transitions, recorded as pulse-width in microseconds, into a buffer that gets filled until a gap of 5ms is seen (or 500 points maximum, in which case we’re very unlikely to have valid data).
The software could be far more efficient and elegant – there’s really no need for a buffer to scan into, as it can all be done in realtime as it arrives. However capture-and-process is easier to experiment with and study, particularly when you have to wait 48 seconds for the next wave of bits.
You can play with the internal pull-up and pull-down resistors on GPIO pins using Gordon Henderson’s GPIO Utility. I have a compiled binary of this here (link) – make sure it’s executable (‘chmod a+x gpio’), and copy it to ‘/usr/local/bin/’.
However, by most accounts no pull-ups should be needed in this project, and any that are can be provided by the internal weak resistors already on the GPIO lines. If I used them in my wiring, it’s only because it was easier to pop one in the breadboard than it would have been to write code to configure them (before I found Gordon’s utility).
The RFM01 is accessed using the SPI kernel driver. Therefore the modules spi_bcm2708 and spidev should be loaded. If not, then you’ll get the error “can’t open device”. To load them, run ‘modprobe spi_bcm2708′ and then ‘modprobe spidev’ – use sudo if you’re not root.
The RSSI will vary by environment – even the location of the Pi, and the antenna’s orientation to the Pi can make a noticeable difference with a weak signal and high-gain/low RSSI threshold parameters.
In the output of the software line with ‘Short’ in it means “short packets seen” – there should be zero in a quiet environment, but one or two are quite possible. If you’re getting lots of short packets reported, then you’re probably picking up noise, because your receiver configuration is too sensitive.
The code has a few lines specifically for the configuration of the receiver – the LNA and RSSI threshold values, and also the bandwidth values are the most important ones.
The table of data that shows on startup shows the noise sampled for each combination of these values. The columns, though they’re not labelled, are noise for the different bandwidth settings of the RFM01 (e.g. how far above & below the centre frequency the receiver should listen for a signal).
You need to look at the table and pick some settings that have zero noise, but are close to settings that do have noise (e.g. we’re looking for a setting that’s sensitive enough to hear the signal, but not so sensitive that any background RF is picked up as data.
There are a number of things I want to do in this project: -
- Integrate the decoded values into some open source weather software so that it’s usable from the Internet (probably wview).
- Rewrite the code to improve the structure and make it more readable (the current version was only a tool for experimentation).
- Add some heuristics to the software for auto-tuning of receiver parameters.
RFM01 Datasheet – from the HopeRF site. The title is ‘RFM01 Universal ISM Band FSK Receiver’ should you need to search for it because of a broken link.
Raspberry Pi Low Level Peripherals – wiki page describing the pin-headers and attached peripherals.
JeeLabs – Receiving OOK/ASK with a modified RFM12B – a detailed article on getting OOK signals out of an RFM12B.
JeeLabs RFM12B Command Calculator – a useful client-side browser application to calculate register settings using form inputs and selections. Only a few apply to the RFM01 (e.g. frequency, AFC, and data-rate).
Strobotics RFM12 Tutorials – a good, though micro-controller centric, description of the RFM12B. Much of the information also relates, broadly at least, to the RFM01.
Luc Small – Hacking the WH2 Wireless Weather Station Outdoor Sensor – this station seems to use a similar RF modulation and packet format (albeit a subset) of the Maplin WH1081 variant.
WH1080 EEPROM Data Definition – document detailing the memory organisation of the LCD display unit. This differs to some extent from that of the Maplin device, but there are enough clues in it to make it useful.