Projects

Mega Hertz

Using underrated 433MHz modules

Dan Koch

Issue 74, September 2023

We take a look at some underrated 433MHz radio modules and learn how to use them with digital projects.

When we think of wireless projects, many makers think of IoT or at the very least, WiFi. Two-way, encoded communication makes all sorts of projects possible, like our rocket launcher project which used the two-way nature of the WiFi point-to-point (non-networked) connection to send verifications back and forth to confirm instructions had been carried out.

However, sometimes the situation is simpler than that and WiFi is overkill. LoRa is an interesting technology which uses the TCP/IP protocol like WiFi does, but over a 433MHz connection. LoRa can work across multiple kilometres with basic antennae, but cannot carry a streamed video signal, for example. At least, it cannot carry very much resolution if you do manage to get it to carry video! As a general rule, the faster a frequency, the greater the amount of data can be carried, but the shorter the range, which is why LoRa has greater range but less data capacity than WiFi. Additionally, the lower the frequency, the greater its capability to penetrate objects in the way, like walls.

LoRa modules have come down in price but using them with Arduino or Raspberry Pi still requires knowledge of network protocols and operation. It also uses more memory and resources than simpler options so if there is no need to do this, there are alternatives.

If all you need is a simple on-off remote control or an alarm triggered report, then small 433MHz UHF modules may be the answer. They're quite cheap and very simple to use. One of the examples we are going to explore involves multiple soil moisture sensors with transmitters, and a simple receiver with multiple LEDs. When the soil sensor is too dry, it sends a one-way alert, and the receiver lights the corresponding LED. A reciprocal scenario is multiple radio-controlled relays, with one transmitter. If there is no need for a network, 433MHz basic modules are fine. You can turn on any number of relays with one controller with a minimum of fuss.

Many people have seen remote controlled relay modules on the market, such as Jaycar's LR8857 or Altronics' A1011. These are more expensive than is justifiable for many projects because they are rolling code high security items with different functions inbuilt in such a way that the user can select them with jumper links and no real electronics knowledge.

The cheaper and simpler alternative comes in the form of small 433MHz radio modules which work on a principle called 'Amplitude Shift Keying', or 'ASK'. These modules are quite small, with the transmitter being in the neighbourhood of 20 x 15mm for most brands, and the receiver around 45 x 12mm. They usually have a pad or pin for attaching an antenna, so the antenna size is not included in these figures. They have a minimum of pins, too, with power, ground, data and antenna being the set on the transmitter, and ground, power, data out, and antenna being the duplicated pins on each side of the receiver.

In very simplified theory, if the data pin on the transmitter is high, it transmits a 433MHz carrier wave. If the pin is low, there is no transmission. On the receiver, the data pin is high whenever there is a received signal, and low when there is not. So, can you just connect a switch to the data pin of the transmitter and a transistor drive for a relay coil to the data pin on the receiver? In short, no. There are three colossal problems with that approach. The first is that the 433MHz band is well-used. Simply emitting a continuous signal is going to affect anything else in range by swamping or at least interfering with other receivers. These devices are classed as 'Low Interference Potential Devices' (LIPD) but that refers to the overall scheme of radio and electronics, not the local situation. They are very capable of interfering with things in their zone, they just won't interfere with something in the next city.

The second problem is related to the first: With just an 'on/off' approach, any other 433MHz transmitter could trigger your receiver. There are a range of channels within the 433MHz band but all of these little modules are designed to use the same channel, to make them compatible because there is no means of changing or setting channels: The channel is built into the circuit. Additionally, they utilise a channel in the 433MHz band that is dedicated to these devices and others like them, unlike, say, UHF CB radio which has many channels allocated and a user can change to an vacant one.

The third major problem is Automatic Gain Control, or AGC. The circuit on the receiver is designed so that when no signal is received by the antenna, the amplifier steps up the gain in increments until a signal is found, enabling it to work with different signals at different ranges. It is sensitive enough that eventually, it just pulls in background radio noise.

There is enough happening on the band that unless you're in the middle of a desert, there will be some noise on the band. Even then, the middle of a desert probably has enough signals from travellers and surrounding agricultural users that the AGC would still find something. Even in the most radio-free places, atmospheric and cosmic radio noise would cause a trigger. The results looked like this on our oscilloscope:

There is another issue, and that is that the units are designed not to pass data if the rate is less than, for most brands, 300Hz. Any pulse width longer than around 0.002 seconds (the high time of a 300Hz square wave at a 60% duty cycle) results in no transmission. Just to make sure, we hooked up a pair and tried them with the data pin of the transmitter hooked to the supply rail.

Sure enough, the receiver output showed a flat zero, so it was receiving a signal (remember, if there is no signal, the AGC means we see noise on the oscilloscope), but outputting nothing. This is another reason we use Amplitude Shift Keying. Before we get into how ASK works, we should go over the modules themselves and the parts that make them up.

It should be noted that online we have found several references to these modules working with a DC-tied data pin and outputting continuous radio signal. Our versions do not, but there are many brands and suppliers of the same FS1000A-style units around. Not all are created equal but because of the interference issue, having one that can continuously transmit is not desirable anyway.

THE MODULES - TRANSMITTER

The transmitter is the smaller of the two modules. The first one we used was the Jaycar ZW3100 which measures at 19.5 x 14.5mm, excluding pins, and is 3.9mm at its thickest point. It has four pins: +V, GND, Data In, and Antenna. Some models use a pad elsewhere on the PCB for the antenna. Hidden among those tiny SMD components is a Surface Acoustic Wave (SAW) resonator, which is a crystal-based oscillator giving a very stable 433.92MHz waveform output. The rest of the components are a signal amplifier to take that resonant signal and give it enough power to be transmitted from the antenna, and a modulator transistor to control when the signal gets to the amplifier.

Different models approach this in different ways. While the documentation rarely confirms, many use the switching transistor to interrupt the output of the SAW to the amplifier. Some use it to control power to the amplifier, some the resonator, and some control power to the whole unit. However, while these approaches may be better on power consumption on paper, they are less reliable as neither the resonator nor the amplifier are stable in the microseconds after power is applied. Generally the amplifier design has a low standby current and the resonator even lower, so unless the device is active with a high on the data line, standby current is low enough to leave both resonator and amplifier powered up.

During bench testing with ours, active current while the data line was just tied high for testing was 7.32mA, while standby current was so low our multimeter couldn't even measure it when set to microamperes (µA). The datasheet also does not help us, either, but t did state a 20ms turn-on time between when power was applied and data could be transmitted, so the standby-state must keep parts of the unit active seeing as 20ms is too long to transmit 10,000 bps if the data pin did simply interrupt power. Whatever method this one uses, the end result is the same: When the data pin is high, radio waves come out of the antenna.

One important note with these transmitters, and the receivers too, is the operating voltage. Both supply and data voltages vary between brands. Our transmitter was a 3V unit for supply, while the receiver from the same supplier was a 5V unit. However, the transmitter on ours had the data pin connected to what our testing efforts identified as an NPN transistor buffer. This means that, although the circuit operates from 3V, using 5V to drive the data pin straight out of an Arduino is ok. This may or may not be true for all brands.

Another brand of receiver we bought can be powered from 4V to 12V. Always check the data for your specific unit because aside from voltages, these units are practically interchangeable and this can mean plugging a 3V unit into a circuit you have seen designed for 5V without realising that there is a potential failure point.

THE MODULES - RECEIVER

The receiver is bigger but not much more complex. Ours measures 43.5 x 11.5mm, and is 7.5mm thick thanks to a through-hole mounted capacitor. It has a local oscillator based on a crystal, some amplifier stages, and a phase-locked loop (PLL). The phase-locked loop, in overly simple terms, allows the local oscillator signal to be synchronised or locked onto the incoming radio wave so that both are in phase. This helps time the decoding of the signal and also helps prevent interference. The end result is that when valid pulses of radio signal are received, they are transferred to the data output pin as digital highs.

The receiver has more pins but most are duplicates. On ours, there are three GND pins, two Vcc pins, two Data pins, and one antenna pin. We checked operating current, and found that while receiving valid data, the unit consumed 2.18mA, while it took 2.19mA with no valid signal. That tiny 0.01mA difference could be just measurement rounding, or the effect of the AGC circuit doing its thing looking for weak signals. Either way, that's a pretty good indicator of its constant current requirement: There is no 'standby state' as such with the receivers.

ANTENNAE AND RANGE

These modules work fine across a metre or so of workbench space but for any more range than that, an antenna is needed. For this project, the antenna is just a specific length of wire. However, there are many more options available and a science to figuring out even the length of straight wire. As a result, we have a Fundamentals this month on understanding and making antennas. The focus is on the 433MHz band, but much of the information is viable for other bands, too.

AMPLITUDE SHIFT KEYING

To get around the problems highlighted above, these 433MHz RF modules are used with a technique called 'Amplitude Shift Keying'. There are many definitions and some are better than others, but it can be simplified like this: Turning it off, then on again, repeatedly. Amplitude shift is just the change (shift) in level (amplitude) of the radio signal, and keying is the control of that. ASK is just the term used for sending groups of pulses of radio signal. There are no rules to how this is done, except that the data should be between 300Hz and 10kHz based on a 40% to 60% duty cycle of 300 to 10,000 bits per second. Anything between those limits will work well. The only other rule is a courtesy one: Keep transmissions as short as practical to avoid interfering with other peoples' stuff. This is what happened on the oscilloscope when we fed a 500Hz square wave (yellow trace) to the data input of a transmitter module, placed next to a receiver module with the data output connected to the blue trace. Note we did not use antennas in this case.

The carrier wave emitted by the transmitter is constantly generated in the resonator in many designs and switched to the amplifier. Whether or not it is connected to the amplifier and then the antenna depends on what is going on at the data input. In other designs the resonator stabilises fast enough to turn the entire lot on and off as a modulation method. ASK can include systems where different amplitudes are used to carry data. For example, a radio signal with four amplitude levels can transmit more data than just an off/on signal. However, it is more complex.

There needs to be a way to define what the levels are and with the inverse square law meaning that the signal is weaker with distance, this takes a lot of doing The system needs to 'learn' all levels at the beginning of each transmission, so that signal strength differences due to distance can be factored in. In our simple use cases, it is unnecessary. That said, if you are reading about ASK, you will probably encounter this. You may also see the term 'On/Off Keying', (OOK). This is the one-level version, used here, where the signal is turned off and on as an amplitude shift method: It shifts between off and on, not between, say, 30%, 60%, and 100%. So, all OOK is ASK, but not all ASK is OOK.

ASK is very easy to use with a microcontroller (thanks to libraries), requires relatively little in the way of system resources, and consumes little current. The disadvantages are the propensity to interference, the low amount of data that can be transmitted, and the lack of security. However, in many scenarios, the lack of security and bandwidth are irrelevant and all but extreme interference can be overcome with coding and manipulation of the data sent.

It is possible to use these modules with analog systems but ASK is still required, some decoding of different data is needed, and the complexity of doing this with analog circuitry is therefore way beyond what is practical for our purposes.

WHY MAY YOU ASK, BUT NOT RECEIVE?

The challenge with ASK, and any data system like it, is that it is a blind, one-way system. Think of it as yelling out in a building. If someone is in range, they can hear you. However, you have no way of knowing that they heard you unless they answer back. Even if they do answer back, unless they repeat your entire message, you don't know if they heard you correctly. What if a word was muffled or a local noise meant they did not hear it? In the ASK system, there is no answer back, unless you fit both a transmitter and receiver to each side and code so that there is two-way data.

For this reason, you can also get transceiver modules with both transmitter and receiver in one. Some of these are half duplex, which means data can be transmitted or received, but not at once. That is like most common two-way radios used for voice communication, where you push a button to talk, and nothing is received while the button is down. If both parties press their buttons at the same time, neither hears the other. Full duplex is more resource-heavy and requires more advanced circuitry, but it can send and receive data at the same time. This is more like a phone, where if you and the person you are calling speak at the same time, you can still hear each other.

In addition, it is hard to make sure that each side knows what the other was meant to say. Sometimes, when someone is calling out to you, what you hear is not what they said, but if what you heard or think you heard makes sense, then you never realise there is a problem. So, when using ASK systems, there is a limit to the complexity and the criticality of what can be done. If you are sending temperature data, for example, it is generally ok if a packet does not arrive.

At worst, the temperature for that instant is not recorded, and the transmitter side does not need to know if the data arrived and made sense. On the other hand, a secure connection needs two-way communication for hand-shakes and confirmations. An EFTPOS machine, for example, would never be built with a one-way ASK connection.

As an aside, in the 'Reading and Resources' section, we have linked to a Tom Scott video on the Two Generals' Problem, a classic of computer science studies. This details the issues of two-way confirmations in data transmission, and is worth considering when designing any system that needs to communicate.

ASK WITH CODE

We know that we need to give high and low signals to the transmitter (and pull them from the receiver at the other end) and that this needs to happen between 300 and 10,000 bps (bits per second). However, that's only part of the story. It is the patterns of these highs and lows that really carries our data, just like any other binary transmission of ones and zeros.

There is no inbuilt error detection in these modules because they do not have data processing onboard. So, we need to do this with code. There is a lot more in one string of data sent between these modules, too. While there are different ways of doing things, they follow the same principles. Therefore, we will describe the specifics of the RadioHead library that we chose, but be mindful that exact numbers may change between libraries and systems.

The message is divided into 'packets' of bits, a bit being a one or a zero. At the beginning of a packet is a group of thirty-six bits called the 'training preamble'. Its job is to set up the conditions for the link. They help the receiver set up its gain and lock onto the frequency before the main data occurs. This is where the phase-locked loop comes in. Next in line is twelve bits called the 'Start Symbol', which is used by the code to determine that the training preamble is over and the real data is about to begin. Following this is the unique data that makes up what you want to transmit from point A to point B, and this is eight bits long.

After this, a Cyclic Redundancy Check (CRC) of sixteen bits is tacked on, which is a string of bits that a predetermined mathematical operation is applied to. The bits are determined by the unique data according to more maths, and if the CRC returns the correct answer, then the message has arrived without loss or interference. If the CRC returns an incorrect result, the code knows the message has been corrupted or damaged in some way, and can reject it. Generally, the message will be sent several times so that if the CRC says that a message is corrupt, the next one can be used. You may need quite a few packets to get all your data across the connection.

Thankfully, this is all handled for us in several libraries that are available online. Of the ones we tried, we liked 'RadioHead' by AirSpayce. We are not supplying this library directly, nor are we building it into a commercial product we are selling. Therefore, rather than figure out which licence applies, we ask that you please visit the link in the 'Reading and Resources' section to visit AirSpayce's page and read it thoroughly. They have put a lot of effort into this software and there is valuable information on the page and you will find the download link there.

LIBRARIES

The 'RadioHead library needs to be installed before any testing or exploring is done. The SPI library is needed too but it should be already loaded in the Arduino IDE anyway: It's one of the basics. Many Arduino users have skipped this section because this is old news but for those who are new to the whole field or are approaching Arduino as a new thing to further their existing electronics knowledge, read on.

After opening the Arduino IDE, go to the 'Sketch' menu tab at the top, then hover over 'Include Library', and select 'Add .ZIP Library'. Once you do, a navigation box will open where you can find the .zip file. Unless you chose otherwise, it's probably in your 'Downloads' file. Clock on it, then click the 'Add' button. The library is now in your inventory and can be added during any sketch.

SHOPPING

We found several versions of these items on the retail market within Australia. Most are similar and even generally have the same pinouts. Voltage is the main variation, but bear in mind that some are sold as a pair and some individually. We have tabulated the information to help. Some are different designs as well, like the superheterodyne receiver from Phipps. Also note that these are all 433.92MHz modules. That is the standard frequency for low-power wireless communication of this class, but always check the frequency before you buy.

SUPPLIER

PART NUMBER

TYPE

VOLTAGE

CURRENT

ANTENNA CONNECTION

RATED POWER ON

PRICE (AUD)

Jaycar

ZW3100

Transmitter

3V

10mA Max

Pin

3dBm

$7.95

Jaycar

ZW3102

Receiver

5V

10mA Max

Pin

N/A

$11.95

Altronics

Z6900

Transmitter

3V to 12V

45mA Peak (const. NK)

Pin

10mW

$9.95

Altronics

Z6905A

Receiver

5V

5mA

Pin

108dB

$10.95

Phipps Electronics

PHI1001311

Transmitter *

3.5V to 12V

NK

Pad

10mW

$10.95

Phipps Electronics

PHI1001312

Receiver

5V

4mA

Pad

105dB

$9.95

Phipps Electronics

PHI1002024

Receiver

3V to 5.5V

5.7mA to 7.3mA

Pin

115dB

$12.95

Phipps Electronics

PHI1002291

Pair *

3.5-12V Tx, 5V Rx

NK Tx, 4mA Rx

Pad Tx, Pad Rx

10mW Tx, 105dB Rx

$12.95

* While looking the same at first glance, the standalone transmitter has a few extra components over the one in the pair, including an extra coil, and so is a different item.

Some are on other 433MHz UHF channels (generally cheap unregulated ones from online marketplaces) while others operate in a legitimate 315MHz band. There are others besides these, but try to stay with Australian suppliers (or local if you live in another country) for all the usual reasons like regulation compliance and warranty.

A NOTE ON INPUT VOLTAGES

In several places in this article, we have stated that these ASK RF modules are not all the same even when they use the same footprint and overall design. Those that look the same may not be the same unless they all came from one supplier in one batch. Some manufacturers and suppliers publish datasheets for their products, and some of these are more detailed than others. On the receiver, data is outputted at whatever the supply voltage is, generally speaking. On the transmitters, however, things are not always so clear.

There are some transmitters which have NPN transistor buffered inputs. On these, the data pin input goes to the base of an NPN transistor, which switches the circuitry inside as described elsewhere in this article. If the operating voltage is 3V for the transmitter, running the data input on 5V would not be a huge problem: The voltage goes through the base to ground anyway.

However, other designs use other systems or no buffering at all, with the data pin connected to the inner workings of the circuit. In these situations, problems may arise feeding the data pin with a higher voltage than the unit is rated for.

Arduinos generally operate at 5V for the I/O and many smaller ICs like the ATtiny85 have 3.3V I/O. Development boards like the digispark ATtiny85 boards we use later may change the situation, because some use drivers between the I/O pin on the IC and the pad or pin on the board.

This can make matching data voltages hard. The datasheet for the ZW3100 transmitter from Jaycar states very clearly that both the maximum supply and data voltages are 3V. That is a challenge if it really is, because even the 3.3V output might conceivably do damage.

We explored ours with a multimeter for some time and concluded that they were in fact NPN buffered inputs and so not so limited. It is worth noting carefully that ours are quite old stock: We bought these years ago for a project and never used them, and it is possible that the design differs from what Jaycar is selling now under the same part number. However, we have run ours using the 3.3V supply from an Arduino Uno's onboard regulator but 5V inputs from the I/O of the Arduino, and have not yet had a failure.

That does not mean we will not experience one later, but so far all is working well.

However, this will not be everyone's experience with these or other brands. If you are concerned, or know for certain that the input voltage from your I/O is going to damage the data input on a transmitter module, you have several options.

You can build your own transistor buffer circuit, which uses an NPN transistor to switch a PNP transistor. The NPN transistor conducts when the I/O line is high, which causes the PNP transistor to conduct. The PNP transistor is connected as a high-side switch, to the same power supply used for the transmitter. That way, a safe voltage is used for the data input.

Alternatively, you could use a zener diode on the data input. This clamps the voltage to whatever the zener voltage is. They're cheap and simple, and we used this method later in the article when we built the remote controlled lights using a transmitter bought more recently which had different-looking (and harder to probe) circuitry on it.

Be aware that 3.3V is the lowest zener value on the general retail market. To get 3V or less, you'll need a trade supplier like Element14. However, the tolerance on most ranges is 20% so with a Zener diode tester, you may well get a 3V one out of a batch of 3.3V, and could use it therefore to limit 3.3V logic to 3V.

Finally, and perhaps most easily, you can use a logic level converter module. These take both 3.3V and 5V supply connections, and have high-to-low and low-to-high data transmission paths across them. This example is available from both Altronics and Jaycar, as the Z6390 and XC4486 respectively. 3.3V will be good enough for many units but may still be too high for others.

EXPLORING ON BREADBOARDS

Transmitter
Receiver
Parts Required:JaycarAltronics
2 x Arduino Uno or equivalentXC4410Z6240
2 x Solderless BreadboardsPB8820P1002
1 pk wire linksPB8850P1014A
10 x Plug-to-plug jumper wiresWC6024P1022
1m Solid-core Hookup WireWH3032W2430
1 x 433MHz RF TransmitterZW3100Z6900
1 x 433MHz RF ReceiverZW3102Z6905A
2 x Tactile PushbuttonsSP0601S1120
2 x 150Ω ResistorsRR0552R7538
2 x 10kΩ ResistorsRR0596R7582
2 x 100pF CapacitorsRC5324R2822
2 x LEDsZD1694Z0003
1 x 4AA BAttery PackPH9200S5031 + P0455
4 x AA BatteriesSB2425S4955B

Parts Required:

For testing and exploration purposes, we need two microcontrollers and the easiest in context are Arduino Unos (or Nanos, for that matter). Later, when we develop projects, we can use smaller packages such as the ATtiny85. However, having the headers, power supply, USB connection and other features of the Unos, as well as the ease of USB uploading and the Arduino IDE, are definite advantages at this prototyping point.

Start by plugging a transmitter and receiver into a solderless breadboard each. Note that the pinouts of your modules could be different. Ours are the Jaycar ones, and the transmitter takes 3V. We used the 3.3V rail from the Arduino, which is slightly but not damagingly over the voltage rating. We also used the ground connection on the Arduino, and wired the data pin to Arduino pin 12.

This is because it is the default for the library we want to use, and there is no sense changing it for now. We could have used plug-to-socket jumpers and skipped the breadboard but the module pins are too fine. We need

the spring contacts of the breadboard to make up the difference. On that note, some breadboards use one long flat spring contact rather than a segmented one, and in that case, you'll need the jumpers at one end of the row and the pins of the modules at the other.

Next, cut two lengths of solid-core hookup wire at 175mm long. Strip 3mm from the end so that the active section is 172mm long. Press it into the breadboard right in front of the antenna pin so that the insulation is as close to the spring contacts as possible. This is the antenna wire. Do this for both transmitter and receiver.

From the GND pin of the transmitter module, place a 10kΩ resistor so it goes out to one side. Plug a pushbutton tactile switch into the board so that one side lines up with the 10kΩ resistor. Use another wire link to tie the other side of the switch to the Vcc pin of the transmitter, and place the ceramic capacitor across it for debouncing. Finally, take one more jumper wire and plug it from the resistor side of the switch to Arduino pin 2. For the transmitter, that's all you need besides power. The receiver side will stay plugged into the USB port on the computer used but the transmitter will stand alone. So, add a 4 x AA battery pack with the red wire in the VIN header on the Arduino, and the black wire in the GND header.

For the receiver, connect the Vcc pin to the 5V header on the arduino, and the GND pin to the GND header. Connect the Data Out pin to Arduino pin 11, because this is the default in the library used. Add two LEDs of your colour choice to an empty area of the breadboard, with a 150Ω resistor on the cathode (short leg) of each one, connected to the ground pin on the receiver module, which in turn connects to GND on the Arduino. Use jumpers to connect the anodes (long leg) of the LEDs to the Arduino: One to pin 3, and one to pin 4.v

We will need a pushbutton on the receiver for our door alert circuit, so the alter can be acknowledged by the operator and the system rest for another button press. Plug in a tactile switch to an empty area of the board and connect one side of the switch to ground using a 10kΩ resistor. Place a 100pF capacitor across the terminals for debouncing, and connect the other side of the switch to the +5V rail. You can probably use a wire link between the switch and the 5V pin on the receiver module. Finally, use a plug-to-plug jumper wire to connect the low side (at the junction of the pushbutton and the 10kΩ resistor) to pin 8 of the Arduino. Finally, you can add the antenna in the same way as on the transmitter module.

TEST ONE: REPEATED SERIAL

The first test does not use the pushbuttons or LEDs. Its sole purpose is to verify that the RF link works and that data is being transmitted. It is still useful in this state, however. This is the way we might send sensor data once an hour, or the identity of an alarm station if triggered. We are not going to have a trigger from an external source, instead relying on the 'delay' function to pass two seconds between every transmission. In a real application we would not use delay, instead opting for 'millis' or something like it. Delay would normally interrupt any other operation the Arduino has to do, like monitor and act on other inputs. However, in a sketch without any other function happening, using delay means we don't have to establish a timer and with nothing for delay to interrupt, that's desirable.

Either download and open the supplied sketches, or open them and copy/paste the code into fresh sketches. There are two files you will need: Serial_Tester_Transmitter, and Serial_Tester_Receiver. Load the Transmitter sketch onto the battery-powered Arduino with the transmitter connected, then disconnect the USB cable. Put it to one side of your workbench. Load the Receiver sketch onto the other

Arduino, with the receiver connected, and leave this one connected to your computer. Not only is it powered by the USB connection, but we need the serial monitor, which you should open. To do so, navigate to the 'Tools' menu at the top, click on it to open, then click 'Serial Monitor' from the list. It opens in a separate dialogue box.

With the serial monitor open, add four AA batteries to the transmitter Arduino's battery pack. You should see 'DIYODE SEPTEMBER 2023' appear every three seconds on the serial monitor. Notice the asterisks and the digit '8' at the end of our message? That's because we maxed the buffer size to 27 and the library fills the space. If you know the exact length of your message, make the buffer the same size and this will not happen. If you have messages of different lengths, you will need to either write code that ignores, or simply utilises, these characters.

At the very top of the code, you will see where the two libraries are included. The RadioHead one has been covered but SPI is included, too. It isn't used directly by the code. Rather, the RadioHead library makes use of it when compiling the serial data. There is no use reinventing the wheel when the end result is still going to be the same wheel. Immediately under that is RH_ASK rf_driver, This line creates the object for the RadioHead library to use.

#include <RH_ASK.h> 
#include <SPI.h>
 
RH_ASK rf_driver;

The setup loop has very little going on, besides initialising the rf driver object. The loop is where the main action is. The first line establishes a character constant to contain the message data that we want to send. The message is anything after the = and between the " ". There can be a maximum of 27 characters herem and spaces count as characters. Therefore, DIYODE September 2023 is twenty-one characters.

void setup()
{
    rf_driver.init();
}
void loop()
{
    const char *msg = "DIYODE SEPTEMBER 2023";

Under this is a line which actually sends the data as a string of ones and zeros on the chosen Arduino pin. It addresses the RF object first so the compiler knows what to do with the message, then sends the contents of the bracket. There are two arguments inside the send() command's brackets. The first is uint8_t, which is an 8-bit unsigned variable (these 433MHz modules always use an 8-bit message packet), which is tied to the *msg from above. Then, strlen(msg) tells the library how long the message is. In our case it's 21 characters but doing it this way means the message can be changed and everything else automatically changes, and also means we do not have to know how many packets the library needs to divide our text into.

Note that neither here nor in the headers did we define the pin that the data line of the RF module is connected to: That's because in the RadioHead library, pin 12 is used for the transmitter and pin 11 is used for the receiver. Changing it means dealing with the library and there is just no advantage for us in doing so.

rf_driver.send((uint8_t *)msg, strlen(msg));
// parameters inside send() are the data to be 
// sent (uint8_t *)msg, and strlen(msg),
 rf_driver.waitPacketSent();
delay(3000);

Finally, the rf-driver.waitPacketSent() line ensures the code waits until the specific message is sent, not a predetermined time because we do not know how long the message will take to send, given that we don't know how many packets it is. Then, there is a delay of three seconds so the serial monitor is actually readable.

On the receiving end, largely the same start applies, with the addition of the startup code for the serial monitor at its 9600 baud rate. The RadioHead and SPI libraries are at the top, followed by the definition of the RF object, then the setup with the initialisations in it.

#include <RH_ASK.h>
#include <SPI.h>
 
RH_ASK rf_driver;
 
void setup()
{
    rf_driver.init();
    Serial.begin(9600);
}

The first section of the loop is used to set up a buffer to store the length of the intended message, and one to store the length of the received message. The recv() command does the main work, receiving the incoming message and comparing it to the intended message size. If it is valid, then the next two lines inside the if statement transfer the message to the serial monitor.

void loop()
{
    uint8_t buf[27];
    uint8_t buflen = sizeof(buf);
if (rf_driver.recv(buf, &buflen))
    {  
      //Serial.print("Message Received: ");
      Serial.println((char*)buf);   
    }
}

Of course there is a lot more that you can do with the appropriate coding skills, including creating code that can handle varying lengths of message. For many coders, however, a simpler approach is to just define the buffer by the longest message you will send, and use spaces and punctuation to make up the difference for shorter messages. Remember, spaces and punctuation are characters, too. For example, if the receiver is to accept messages from several transmitter stations which need to tell a base station that their input (say, a PIR) has been triggered, then having messages the same length such as 'StationA, Station B' would do the job. There is a line that is commented out: It is optional, and if used, is tacked onto the front of the message in the serial monitor.

TEST TWO: BUTTON PUSH ALERT

This is representative of the code used when a transmitter needs to trigger an action on the receiver. While the previous code was about sending messages, this one is just about action, and applies to single or multiple transmitters. First, you'll have to change sketches. Unplug the batteries from the transmitter board and load Pushbutton_Tester_Transmitter, then reinstall the batteries. Plug the receiver back into USB and load Pushbutton_Tester_Receiver, and leave it plugged in. We do not need a serial monitor this time, but we'll stay with USB power for the receiver.

When you press the pushbutton on the transmitter, a message is sent from there to the receiver. Only when a valid message is received will action be carried out by the receiver. In this case, that is lighting an LED for. We named the two transmitters in the code, and if you really want to test this code, you can build another transmitter according to the instructions above. However, we did not. We just changed the message in the code and used the same transmitter again to test the second LED function.

We originally had the transmitters named just 'a' and 'b'. The original code we wrote was for a single-point on/off switch, and it worked on message length. If multiple transmitters are used in that case, any single character message will light the LED. That's good for some cases, like when you want multiple stations to trigger the same alarm, for example. However, if there is differentiation needed, then the current code would be more appropriate. Different messages are handled easily by the RadioHead library, which calculates the error detection algorithm and outputs the results as plain text. So, we are free to use simple labels.

The situation we are going for here is an alert for a small commercial premises which might have a front door and a rear loading dock. A transmitter can be placed at each, with a different message established in the code of each

one, and a single receiver can then light a different LED depending on which transmitter it is receiving from. We are using an 'acknowledge' button to clear the LEDs and reset the receiver. This is partly to make sure the system does not reset before someone has seen it, and also to make sure there is some responsibility: No one presses the 'acknowledge' button until they are on their way to the door. The way this system is set up, the receiver will still show the additional alert if a transmitter is triggered after another has already been pressed. It would be fairly easy to code and wire in a buzzer for this circuit but it was not necessary to bench-test the communication and code.

#include <RH_ASK.h>
#include <SPI.h>
RH_ASK driver;
const int pushbutton = 2; 

This is the now-familiar library inclusion and object definition section. We have added a constant to store a name for pin 2, the pushbutton.

void setup() {
  Serial.begin(9600);
  if (!driver.init())
    Serial.println("RadioHead initialization 
failed");
  pinMode(pushbutton, INPUT);
}

The setup section has two jobs: creating a serial monitor message for debugging if the initialization doesn't work, and establishing pin 2 as an input. You can use any pin, and different pins could send different messages if multiple buttons or other triggers are connected if used in a different situation besides a doorbell.

void loop() {
  if (digitalRead(pushbutton) == HIGH) {
    char msg[] = "frontDoor";
    driver.send((uint8_t *)msg, strlen(msg));
    driver.waitPacketSent();
    Serial.println("frontDoor button pressed"); 
    delay(500);
  }
}

The loop checks for a button press and when one is detected, it sends the message, in this case the transmitter location 'frontDoor'', in the same way as for the serial monitor version above. The differences are that is button-triggered rather than recurring, and there is a delay of 500ms.

This purely to allow the button to be released and not send hundreds of messages while the button is still down. You can extend that to cope with longer pushes or even write better debouncing code, like one that must see the switch open for more than normal bounce time before it will send the message again.

That's all there is to the transmitter code in this case! The receiver code is also similar to the serial monitor version above, with the addition of some output code for the LEDs and the use of the acknowledge button..

#include <RH_ASK.h> 
#include <SPI.h>
RH_ASK driver;
const int frontDoorLed = 3;
const int loadingDockLed = 4; 
const int acknowledge = 8;

Besides the familiar headers adding the libraries and defining the object, we have two constant integers to name the objects that relate to the LEDs, and assign them to output pins. We also establish the acknowledge pushbutton name on pin 8.

void setup()
{
  Serial.begin(9600);
  if (!driver.init())
    Serial.println("RadioHead initialization failed");
  pinMode(frontDoorLed, OUTPUT);
  pinMode(loadingDockLed, OUTPUT); 
  pinMode(acknowledge, INPUT);
}

The first section sets up a serial monitor output for debugging. If the driver fails to initiate, plugging the Arduino into the USB connection and opening the serial monitor will tell us so. The next lines set up the inputs and outputs. Arduino pin modes are generally outputs by default but it is good practice to make sure. We are using the input in the pull-down configuration, so the word 'INPUT' is used alone. If we were utilising the internal pullup resistors the text would read 'INPUT_PULLUP'.

void loop()
{
  uint8_t buf[RH_ASK_MAX_MESSAGE_LEN];
  uint8_t buflen = sizeof(buf);
if (driver.recv(buf, &buflen)) { 
    buf[buflen] = '�';
      Serial.print("Received: ");
      Serial.println((char *)buf); 
    
    if (strcmp((char *)buf, "frontDoor") == 0) {
          digitalWrite(frontDoorLed, HIGH);
    }
    if (strcmp((char *)buf, "loadingDock") == 0) {
          digitalWrite(loadingDockLed, HIGH);
    }
  }

At the top of the loop, an 8-bit unsigned variable is set up as a buffer for the incoming message, followed by another to store the message length. Then, an 'if' statement checks the buffer for a valid message. If one is received, the next few lines deal with it. These are a set of nested 'if' statements used within the one above which checked the message. The first determines if the message matches one of the transmitter labels, and if it does, sends the corresponding LED high. The other nested 'if' does the same for the other label.

  if (digitalRead(acknowledge) == HIGH) { 
       digitalWrite(frontDoorLed, LOW); 
       digitalWrite(loadingDockLed, LOW);

After the message handling texts, the last part of the loop is to check the input pin for activation of the acknowledge button. If it is detected, the LEDs are reset low.

That's it, really. The code would need to be more sophisticated for more advanced roles but it shows the basics of how the RF modules are used. Further development concerns what messages are sent and what they are used to do. There are different ways to do the same thing, too, and some suit different data. Before committing to a project, always look at how other people have done the same or similar things and why. However, be aware that they may not have a lot of knowledge themselves!

USE CASES

There are a great many situations where the data needing to be sent does not justify the use of more sophisticated RF units like LoRa or WiFi. Of the situations we thought of where one receiver works with multiple transmitters, a set of soil moisture sensors, temperature sensors, and wireless alarm system stood out.

The first two require individual transmitter identification as well as the data sent, but the hardware remains the same as we have shown, and the beginning of the code is here, too. The alarm situation may or may not need ID: You could just want several doors protected and an alarm to sound if any one of them is opened.

There are use-cases where one transmitter and multiple receivers is needed. The turning on and off of several lights with one remote is an example. If these are required to be controlled individually, then addressed receivers and a transmitter with multiple pushbuttons is needed. However, there are situations where you want the entire group to turn on and off together, but it is just not feasible to connect the loads together for whatever reason.

The same could be said of the triggering of blind or curtain motors - you may want them individually controllable or simply all to activate together. The really obvious case for this arrangement is a wireless doorbell: If there is only one entry point to a premises but multiple locations where someone might be, receivers can be located in all of them with the transmitter at the entry point. If all receivers are set to respond to the one message, the transmitter at the door triggers them all.

Of course there are other cases, too. It is possible to have single-point use, where one transmitter and one receiver are used. A single-point doorbell or a fridge temperature monitor come to mind. We have even seen some great projects where these devices are used to make simple radio controlled boats and cars!

Unfortunately these situations need encoder/decoder ICs, a form of parallel to serial converter, of a sort that we cannot locate a good Australian source of. All of the ones commonly used online in projects, and specifically the most common HT12D and HT12E, appear as 'Available until stock is exhausted' or equivalent on most suppliers' websites. We have not got around to researching alternatives yet because they are outside the scope of this article anyway.

THE HARDWARE QUESTION

The RadioHead library works great but it is memory-intensive. That is because it is very, very capable, far more so than any ASK module could ever use. Arduino platforms handle it well, as we have shown. However, Arduino is not always the best platform.

Consider the situation where you have multiple battery-powered garden soil moisture sensors. At roughly AUD 30 each for a generic Uno clone, that will add up fast. A better alternative is the ATtiny85: It retails for closer to AUD 5, is much smaller and takes less power, but has much less memory and fewer I/O pins. The solution is to use a less intensive library, and once again, the community has done the work. Thijs, a member of an online maker community who we found in several forums, has taken RadioHead and stripped out many of the features not needed by this situation. The result is TinyHead, a library that can comfortably run on ATtiny85.

We have linked to it below in the 'Reading and Resources' section, but we did not actually try it. Instead, we have also linked to 'VirtualWire', another library from Airspayce. This was the precursor to RadioHead and has a lot less features. However, it still works very well and while no longer supported, is still available for download from Airspayce. This is the one we went with to use the ATtiny85.

We did discover, however, some challenges of our own with the ATtiny85 development boards from Digispark. This board uses software serial to achieve USB connectivity, rather than a separate programmer as would normally be used with a discrete, bare-bones ATtiny 85 IC. The Attiny85 has a tiny amount of memory as-is, but the bootloader on the Digispark boards (and many others like it) takes up some of that. Therefore, while we can use the development boards for something like the remote on/off switch later, there was not enough room for the VirtualWire Library and the DHT sensor suite library as well when constructing the temperature sensor project.

It is entirely possible to use the ATtiny boards as temperature sensor hosts, however you would need to either write code that did not use the DHT library, or use other, simpler sensors. For example, a humidity sensor based on a capacitive sensor and an oscillator, and a temperature sensor based on a sensor IC with a linear voltage output like the LM335, would work well. The humidity sensor would connect to a digital pin and the frequency would be measured, and the temperature sensor to an analog pin.

REAL-WORLD BUILDS - SCENARIO 1

ONE RECEIVER, MULTIPLE TRANSMITTERS - TEMPERATURE SENSORS

Receiver Fritzing
Transmitter Schematic
Transmitter Fritzing
Parts Required:JaycarAltronics
1 x Arduino Uno or EquivalentXC4410Z6240
3 x Leonardo Tiny or EquivalentXC4431-
3 x Thermistor Temperature Sensor ModulesXC4494-
3 x 2AA Battery PacksPH9202S5057 + P0455
6 x AA BatteriesSB2425S4955B
3 x 5V DC-DC ConvertersXC4512Z6366
1m Hookup Wire for Antennas and LinksWH3032W2430
1 x 433MHz RF TransmitterZW3100Z6900
1 x 433MHz RF ReceiverZW3102Z6905A
3 x 3.3V Zener DiodesZR1398Z0603

Parts Required:

To demonstrate a real-world use for these RF modules, we are going to make a temperature sensor system which has one receiver and two or more transmitters. While inside/outside thermometers are easy enough to buy, including ones with more than two temperature sensors or the capability for add-ons, many of them are either very expensive or just not that great. In our experience, they can have both problems! Accuracy is often an issue. However, the main point here is as a simple but practical demonstration, so we're going with it anyway.

The temperature transmitters are based on a thermistor voltage divider module. It is far from the most accurate sensor around but it is cheap and available. We were going to use the DHT11 sensor, but there were two problems. The first is that the library is quite large. It is large enough that it and the RadioHead library will not fit together on the Digispark ATtiny85 development boards.

In fact, we couldn't even avoid 'text overrun' errors when trying to use the VirtualWire library! The other challenge is, because ours were on-hand ones that had been used and abused, two of them were heating up far too much and were causing worry. They had been damaged at that point and our local Jaycar was waiting on new stock. We were on a timeline, so we went with the thermistor.

It is paired with an RF transmitter module and an Arduino Leonardo Tiny microcontroller. This is smaller and cheaper than a Uno, but still not as small and cheap as an ATtiny85. It will fit the DHT library and the VirtualWire or even RadioHead libraries, and we had bought them with that in mind. In the end, they are overkill for the thermistor but like most makers, sometimes we use what we have, especially when stock on hand at the shops is a problem! The ensemble is finished off with a power supply consisting of a 2xAA battery pack, and DC-DC 5V output converter.

Start by installing two libraries. The VirtualWire library can be downloaded from the link below, and installed as a .zip library as described earlier in the article. The math.h library should be part of the Arduino IDE but may or may not be enabled by default. Check your Library Manager to be sure. If it is not already installed, it will definitely be in the list to install with a single click.

As far as microcontrollers are concerned, we have opted for a middle-ground option. Because this is a development platform, and for the reasons outlines elsewhere, we are using an Arduino Leonardo Tiny. They make life a bit easier when it comes to the serial monitor, as the ATtiny development boards have software serial that uses the I/O pins and program memory However, the tradeoffs are being more expensive and marginally bigger. However, we felt it was better to use these for a proof of concept than to deal with programming and transferring discrete Attiny85 ICs. Even better for a real-use project but much harder to do would be to use an ATtiny85 and write code for a sensor that does not use a library at all.

We soldered directly onto the Leonardo Tiny board, because it was the simplest and neatest way to do things. The RF transmitter and the Thermistor module are soldered on, along with the same wire antenna used in the previous builds soldered directly to the pin on the transmitter. Power is from two AA batteries and a DC-DC converter. In a more serious build, a bare-bones ATtiny85 would be used with a small regulator/charger and a 3.7V battery, or AA batteries and a regulator to give a consistent 3.3V. You can build as many of these as you like but we made three.

We have not put these in an enclosure as yet, because they are test platforms. To make them permanent, we would mount the temperature sensor on short wires and put it in a Stevenson Screen instrument shelter, with the electronics in a waterproof box below and the antenna bent around the inside of the enclosure.

The receiver is based on an Arduino Uno. We want the serial monitor to display the data, but in a permanent application this would likely be made with its own screen for display (and all the extra coding and hardware). It has the RF receiver attached, and nothing else because the computer will provide both power, and the display. Because of that, the build is about as simple as it gets. We connected power and added the antenna.

CODE

The code for the transmitter is built for the Leonardo, but is by and large the same as Uno code. At the very top, the required libraries are included, with VirtualWire being the chosen protocol to hopefully enable ATtiny use down the track. The '#define' lines set up the handles for later use, and there are a bunch of constants for the temperature maths in between. In particular, Station ID needs to change between transmitters so you know which is which.

#include <VirtualWire.h>
#include <math.h>
const double A = 0.001129148;
const double B = 0.000234125;
const double C = 0.0000000876741;
const int THERMISTOR_PIN = A0;
#define TRANSMIT_PIN 9
#define STATION_ID 1
void setup() {
  Serial.begin(9600);
  vw_set_tx_pin(TRANSMIT_PIN);
  vw_setup(2000);
}

The setup starts the serial monitor for debugging and sets it at a baud rate, then assigns the transmitter data pin to pin 9 of the Leonardo as defined earlier. The last line sets the radio transmission rate in bits per second.

double readTemperature(int rawADC) {
  double Temp = log((10240000.0 / rawADC) - 10000.0);
  Temp = 1.0 / (A + (B + C * Temp * Temp) * Temp);
  Temp = Temp - 273.15;
  return Temp;
}

This block is the maths for the temperature sensor, or at least part of it. The overall goal is to convert the voltage to a temperature but because NTC thermistors are not linear, there is a logarithmic algorithm plus some other bits. We took this mostly from the datasheet of our sensor and yours may be different. The approach will be the same but the constants may not be.

void loop() {
  float temperature = dht.readTemperature();
  float humidity = dht.readHumidity();
  
  char msg[50];
  sprintf(msg, "%d:%.2fC %.2f%%", transmitterId, temperature, humidity);
  
  vw_send((uint8_t *)msg, strlen(msg));
  vw_wait_tx();  // Wait for transmission to complete
  
  delay(60000);  // Send data once per minute

In the loop, we start with the lines to read the analog to digital converter value from analog pin 0, and store it as an integer. The 'double temperature' line is part of the mathematical operation and may not be the same for all sensors. After that, a character buffer is established and populated with the message information, before the packets are sent. The usual wait while transmitting is included, followed by a sixty-second delay.

The receiving code is similarly simple. There is only one library, VirtualWire, and then the setup contains the beginning of the serial monitor transmission and its baud rate, plus the receiver pin assignment, data rate, and initiation.

#include <VirtualWire.h>
#define RECEIVER_PIN 5
void setup() {
  Serial.begin(9600);
  vw_set_rx_pin(RECEIVER_PIN);
  vw_setup(2000);
  vw_rx_start();
}

The loop contains the buffer unsigned long setups, and then an if statement checking for messages. An integer is used for the station ID of the message, while a string is used for the data from the sensor. Then, the serial print commands for each.

void loop() {
  uint8_t buf[VW_MAX_MESSAGE_LEN];
  uint8_t buflen = VW_MAX_MESSAGE_LEN;
  if (vw_get_message(buf, &buflen)) {
    buf[buflen] = '�'; // Null-terminate the received data
    Serial.println((char *)buf);
  }
}

We powered on our sensors, giving a ten-second-ish gap between each, then moved them around the office. The serial monitor started showing who has their air conditioner set to hot or cold, or who doesn't use one, depending on where in the building we put these.

REAL-WORLD BUILDS - SCENARIO 2

ONE TRANSMITTER, MULTIPLE RECEIVERS - DECORATIVE BATTERY POWERED LIGHTS

Transmitter Schematic
Receiver Schematic
Parts Required:JaycarAltronics
4 x Digispark ATtiny85 Development BoardsXC3940-
3 x 3.7V 2000mAh Lithium Ion Batteries--
1 x 3.7V 400mAh Lithium Ion Batteries--
4 x Lithium Ion Battery ChargersXC4502Z6388
4 x 5V DC to DC ConvertersXC4512Z6366
2 x Tactile PushbuttonsSP0601S1120
3 x 1kΩ ResistorsRR0572R7558
2 x 10kΩ ResistorsRR0596R7582
2 x 100pF CapacitorsRC5324R2822
3 x BC337 NPN TransistorsZT2115Z1035
1 x 433MHz RF TransmitterZW3100Z6900
3 x 433MHz RF ReceiverZW3102Z6905A
3 x LED String Lights, See Text--

Parts Required:

This project is for situations where one transmitter needs to trigger multiple receivers without differentiating, although building in differentiation would not be hard. While we have just an on and an off button, it would be plausible to add on and off buttons for each receiver, and a master 'all on/off' pair as well. With an Arduino UNO, multiplexing would not be needed to achieve this, as four receivers is ten buttons (four specific on/off pairs plus the master all on/all off pair) so that is under the I/O ceiling as-is. We went with a single pair of buttons, because our intended use does not require individual control. If it did, we would simply add numbers or labels to the 'on' and 'off' messages you see in the code, like "oneOff" or something like that.

There are systems that do what we want already on the market, but they are mains-powered. This is a set of three from Jaycar, MS6147. The button arrangement is exactly what we would make for individual and group control, but these cannot be made to switch non-mains voltages.

Instead, we need to build our own. Because size is an issue, we have again opted for the ATtiny85, and again using the development boards. They're small and practical, and save the issues of loading code onto the bare-bones IC which does not have a bootloader.

To physically demonstrate this, we are modifying some battery powered string lights to be remote controlled. When in use, these are often stashed with the battery boxes in discreet, and therefore hard to access, locations. That is because they are used decoratively rather than as area lighting or some other utility. If they are not strung out around an ornament or piece of furniture, they are often in a jar or vase with the battery box under the lid or out of sight under the vase.

THE TRANSMITTER

The transmitter will have two pushbuttons: One for 'on', and one for 'off'. The transmitter being a 3V device, but the ATtiny85 being a 5V device when used on the development board, we used a 5V DC to DC converter to run the board, and a Zener diode to run the transmitter. The data pin is connected directly, as described in the introduction. The I/O on these boards is 5V on some pins and 3.3V on others (see below) but the logic on the transmitter modules seems to handle 5V, especially briefly. It is transistor buffered, after all. We have found no reference to anyone online having damaged these specific units by running the logic at 5V because of the buffer. However, keep in mind that not all are made the same and another brand may be damaged. If in doubt, check the datasheets and explore with a multimeter. Some data sheets show a full schematic, which will allow you to verify if the input is buffered or not.

The transmitter circuit is simple, with two pushbuttons and their associated pulldown resistors and debouncing capacitors. We chose two buttons rather than one button. Although it would be simpler to have one button, with the first press turning the receivers on and the second turning them off, there is a problem. If one receiver does not turn on (or off, as the case may be) because of interference, blocking by a metal object, or the like, then the cycle has to be repeated. If range is the issue, the transmitter may need to be moved a bit, but in either case there is the issue of the receivers that were already on, now being turned off while the receiver that did not turn on the first time now does. That could make for a never-ending problem! To make sure any situation is catered for, we have two buttons. One sends the 'on' signal, and can be pressed any number of times as necessary. Receivers that are already on will not be turned off. The reciprocal is also true.

Assembly makes use of a small solder breadboard to mount the batteries, modules, and components. The buttons are soldered in with their resistors and capacitors, and the Digispark and DC-DC converter modules glued on and connected. Small lengths of hookup wire are used to make the connections between boards.

THE RECEIVER

The receiver is just as simple, and consists of the Digispark ATtiny85 board, RF receiver module, and the power supply components, plus an output current limiting resistor and a

BC337 NPN transistor for load switching of up to 500mA. The switched loads are three metre LED string lights, the sort available from hardware, homewares, and variety stores which are powered by three AA batteries.

The receiver wiring is 'air wiring', with the components glued into a 3D printed case and wired with hookup wire. We were tempted to use the existing battery bases that the lights came with as enclosures.

However, the batteries would not quite fit. We chose a 3.7V 2000mAh battery, and the charger and DC-DC converter modules we used with the transmitter build. In a previous use of these lights, for our firefly jar project in issue 39, we used 18650 and 14500 batteries. These did fit with the electronics into the 3AA battery case. You might like to check that out too if you want other power options.

However, we intend this as a demonstrator and not too many people will be modifying LED light strings, we think. In either case, the LED string with its included resistor was desoldered from the supplied battery terminals and soldered onto our new electronics.

On that note, if you happen to be using these light strings, substituting the existing resistor for a higher value 1W rated variety might help, because the nominal 3AA battery voltage is 4.5V and the DC-DC converter outputs 5V.

However, fully charged AA batteries are a little over 1.5V each. Ours measured 4.78V fully loaded, and the DC-DC converter outputted 4.95V. Because of that, we left ours as-is, like we did back in issue 39, and those ones are still working.

THE TRANSMITTER CODE

#include <VirtualWire.h>
#define ON_BUTTON 1
#define OFF_BUTTON 2
#define TX_PIN 

At the very top, we have the usual library inclusion, as well as one of several methods for assigning labels to pins.

The Arduino guides show several ways to do this but most code that makes use of the ATtiny85, that we found, uses #define. We labelled the output pin to the transmitter data input, and the two pushbuttons.

void setup() {
  pinMode(ON_BUTTON, INPUT);
  pinMode(OFF_BUTTON, INPUT);
  vw_set_tx_pin(TX_PIN);
  vw_setup(500);
}

The setup includes two lines to establish how the library needs to deal with data: The pin it is to be sent to, using the definition from above, and the data rate on the next line down. The data rate needs to be between 300 and 10,000 to work but some documentation suggested this library has a narrower range.

We could not confirm that. WE chose a lower data rate to help with noise immunity, because we could. The first two lines just establish pins 1 and 2 as inputs, which are high-going and hence the lack of '_PULLUP' to activate the pullup resistor and tie the pin high that some code uses.

void loop() {
  if (digitalRead(ON_BUTTON) == HIGH) {
    const char *msg = "ON";
    vw_send((uint8_t *)msg, strlen(msg) + 1);
    vw_wait_tx();
    delay(350);
  }

The loop has two nearly identical 'if' statements. They differ only in which button they read at the beginning, and using labels at the beginning rather than pin numbers through the code really helps not get these two mixed up! If the pin is low, the loop progresses to the next 'if' statement, and around and around until one button is pressed.

When it is, the relevant 'if' statement reads its pin as 'high' and executes whatever is within the brackets. The first line of that is the establishment of a character variable and the assignment of the message 'On' to it. The second line within the if statement sends the message according to the VirtualWire library, using an unsigned 8-bit variable, and includes both the message and message length.

The third line halts the loop until the message has been sent, an amount of time defined by the message length rather than an arbitrary number. This means that the message length can be changed and you do not have to remember to find and change the wait time to suit. Finally, there is a delay of 350ms, which denounces the button to a comfortable push time.

There is one very important detail in the second 'if' statement. We had to change the word 'off' to 'of' to make the messages the same length. Having different message lengths is challenging with the VirtualWire library and this cost us hours before we realised what was wrong, because everything compiles properly.

 if (digitalRead(OFF_BUTTON) == HIGH) {
    const char *msg = "OF";
    vw_send((uint8_t *)msg, strlen(msg) + 1);
    vw_wait_tx();
    delay(350);
  }

Something else to consider is using an oscilloscope. This helped us verify that data was indeed being sent and received, as we could see it on both transmit and receive data pins. That told us the problem was in the receiver code, and we eventually tracked down that the message length was the issue.v

THE RECEIVER CODE

#include <VirtualWire.h>
#define LED 1
#define RX_PIN 0
void setup() {
  pinMode(LED, OUTPUT);
  vw_set_rx_pin(RX_PIN);
  vw_setup(500);
  vw_rx_start();
  Serial.begin(9600); 

The receiver code is very similar to the start of the transmitter code, with one glaring exception: the addition of the second-last line. In the transmitter, the transmit loop is triggered when a button is pressed.

In the receiver, this vw_rx_start(); line accesses a loop within the library which constantly checks the data pin for incoming information and sorts out whether it is noise, or valid data. If it is a valid message, then it compiles the information for the loop to use. We also added the serial monitor for debugging.

void loop() {
  uint8_t buf[VW_MAX_MESSAGE_LEN];
  uint8_t buflen = VW_MAX_MESSAGE_LEN;
  if (vw_get_message(buf, &buflen)) {
    if (buflen == 3) { // Check the message length
      buf[buflen] = '�'; 
// Null-terminate the received data
      Serial.println((char *)buf); 
// Print received command for debugging
      if (strcmp((char *)buf, "ON") == 0) {
        digitalWrite(LED, HIGH);
      } else if (strcmp((char *)buf, "OF") == 0) {
        digitalWrite(LED, LOW);
      }
    }
  }
}

The loop is triggered when valid data is received. The first two lines establish the buffers to store the message, and separately, the message length. The two are used in a mathematical calculation as part of the Cyclic Redundancy Check.

The first if statement is used if the CRC returns a valid result: in other words, if the message is real and not just stray data from elsewhere or a message with bits missing. If the message is valid, it is stored as a string of character variables and the loop continues. If it is not valid, the loop returns to the beginning.

The second 'if' statement is an 'if-else' nested set. The first line of it determines if the message is "ON" and if it is, the indented next line, digitalWrite, is executed to send the transistor pin high. If the message is not "ON", the loop proceeds to the 'else' line, which checks if the message is "OFF". If it is, then the next line, the indented digitalWrite, sends the transistor pin low. If the message is anything else, the loop does nothing and proceeds back to the beginning.

The only occasion that this would occur, however, is if another valid message was received. Remember, these transmissions are not secure, so any 8-bit data string could be valid data even if it does not come from the intended source transmitter. A nearby doorbell, for example, may also use the same band and the same ASK 8-bit data with the same training bits, preamble, and CRC.

USING CHATGTP TO WRITE CODE

We have mentioned the phrase 'reinventing the wheel' several times in this article. The idea is that if someone has made something already, there is not an automatic value in making it again from scratch. Sometimes, you want to re-engineer it to be more rugged, more sophisticated, have extra features, or be simpler and cheaper.

Sometimes, the original is not up to scratch, possibly poorly engineered or designed on partial knowledge. However, if the thing does exactly what you need it to, there is little point putting the effort into designing another one yourself from the beginning.

Writing code is a similar story. Quite often, many makers search forums or guides for how to do a certain thing, then try to integrate it with what they already know or have written. This was our experience for this article, learning how best to use the RadioHead and VirtualWire libraries to do what we did with them. IN the case of the doorbell system especially, this turned out to be very different to what we had expected.

However, using bits of someone else's code or a block of code from a tutorial, and then making it work with the rest of yours, can be problematic. In particular, the way something was done elsewhere may have been dependent on parts of the code you do not need and did not look at, or something like that.

We explored something new to us with this article, because of the challenges described above. We have used ChatGTP to write the code, and then adjusted the results for our use. We have found it a huge time-saver. As long as we think carefully about the prompts and instructions we give it, the outputted code is either usable as-is, with only a few tweaks and personal preference changes, or needs little modification to get it working.

The best part is, there are no missing brackets or semicolons! It helps if there is one little thing you haven't thought of or bit of knowledge you are missing, and saves hours of research and forum-reading for what is often the same end result. It is also a great tool if coding is far from your personal strong point.

There are caveats: There is a definite complexity limit to what it can deliver. Your instructions to the AI have to be well-thought out and -worded, too. In addition, the AI does not always find the libraries you want to use, if there have not been enough use-cases for it to study.

After all, AI does not think, it analyses and averages existing information and looks for patterns, before building a result based on what it sees. However, for tasks like ours, it was a great tool with which to start. We then modified the results to suit our needs and wants, and the results are what you see.

THE ATTINY85 DEVELOPMENT BOARDS - SOME QUIRKS AND INSTRUCTIONS

The DigiSpark ATtiny development boards that we have been using are a little quirky and not straightforward compared to an Arduino. Some of the issues are related to the ATtiny85 and apply to the bare-bones IC as well. Those are the limited memory space and I/O pins, which mean some libraries have to be modified and some code cannot run on them. The other quirks come from the fact that the USB connectivity is entirely software-based on these boards, via a small bootloader. There is no hardware USB whatsoever. As a consequence, sometimes Windows PCs in particular will not recognise the device.

We found that one solution, which worked well for us, was to use a USB hub. Not just any hub, but the more sophisticated variety that have some logic in them which handles sharing. These appear as a USB device on their own, satisfying Windows. Often, using a powered USB3.0 hub is a good way to increase your chances of the hub having its own logic, but it's not a guarantee.

In addition to that, ensure that you have the drivers installed. In our case, the manual from the retailer's product page had the link to the github repository with the driver .zip file. This is necessary before Windows will talk to the device at all. They are separate from the Arduino IDE board driver installation process.

If you want to use the stand-alone ATtiny85, check out issue 14, where we built a development shield for Arduino which allows plug in/plug out programming of the ATtiny85 discrete ICs.

The stand-alone ATtiny85 IC is a 3.3V device but the Digispark development board is a 5V device. We needed to know if the logic was 5V, so we wrote a terribly basic code that sent all pins high. We got an array of voltages, so we took the unit off the USB cable and powered it with 5V directly. Pin 0 showed 4.99V, but pin 1 showed 1.8V; Pin 2 was 4.99V while pin 3 was 3.003V. Pin 4 was at 2.76V and pin 5 was at 4.75V.

However, when we changed the code to send all pins low, pin 5 was sitting at 4.45V, on a supply voltage measured at 4.75V. Pins 4 and 3 were both at 2.76V, while pins 2, 1, and 0 were down to a few millivolts. The challenge here is that the USB communication is software-based, and these pins are used. This must be considered when designing a project. When we took the Digispark board off the USB connection and powered it with 5V directly to the board again, pin 4 had dropped to 0V, pin 5 was 4.76V, and pin 3 was 2.9V.

There is an internal pullup resistor mentioned in the documentation on pin 4, which explains the supply voltage there, but we have not come up with an answer for the 2.9V on pin 3. We have not compared it to a discrete ATtiny85 IC on its own yet, but we suspect some of the quirks come from the development board and not the ATtiny85 itself.

ANTENNA OPTIONS

We have run out of space to bring you a Fundamentals on Antenna design. We're going to bring that to you next month. It will cover the basics of antenna design and a range of options to suit 433MHz Rf use. Some will be easily modified for 2.4GHz or other fequencies.

CONCLUSION

These small, (relatively) cheap and simple RF modules are ideal for the purposes for which they are intended, and we have found them both versatile and easy to use. They are never going to transmit 4K video wirelessly, or be used for secure EFTPOS transactions, but for simple on/off or low-data tasks, they are perfect. As long as you understand the limitations, and use the correct libraries, there should be many more things you can do with them besides what we covered here.