« Back to home

An ATtiny85 Based Safety Light

This project is a simple presence sensing night light for doors, stairs and other areas which could be dangerous or difficult to navigate in low lighting conditions. It is built around an ATtiny85 microcontroller and uses the head from a cheap LED torch as the lighting element. The ambient lighting is detected with a simple LDR and presence with a PIR motion sensor.

Using a microcontroller is probably overkill for this project and the firmware may seem a bit large for what it does (around 1.5K). I wanted to use the project to experiment with some other features as well as making a useful utility for the lab so it has been a little over-engineered as a result.

Some of the more interesting features of the project include:

  • Combining the motion and light sensing inputs on a single pin. * A single pin half duplex serial interface to communicate with MCU. * A very simple communications protocol which allows changing configuration values without needing to re-flash the chip.


    As well as those items it was a good way to teach myself to work with the timers, PWM outputs, analog inputs and EEPROM on the ATtiny directly from C or assembly code. All the files for the project (firmware source, schematic and 3D models for the casing) are available on GitHub under a Creative Commons Attribution-ShareAlike 4.0 International License, feel free to use and abuse it as you see fit.

    The remainder of this article details the various features of the project.

Power Supply

To power the device I'm using a set of 4 AAA batteries and a 3.3V MCP1700 LDO linear regulator to provide power for the digital circuitry. The voltage level of the battery pack is monitoring through a voltage divider consisting of two 10K resistors. The idea is to provide some sort of indication as the batteries go flat so you know when to change them before they fall below a suitable operating voltage.

It would probably be possible to remove the regulator completely, the ATtiny will run with anything from 2.7V to 6.0V without problems. It would make calculating the current limiting resistor for the output LED a little more problematic (it would simply fade as the voltage, and therefore the driving current, dropped) and you would need a simple 3.3V zener diode on the TX pin of the serial interface to keep that voltage level down as well.

Motion and Light Sensing

To detect motion I'm using a fairly standard PIR (passive infrared) module that I picked up from eBay. This particular one does not work at 3.3V unfortunately and the voltage level of the pulse output is the same as the power voltage. I wound up driving it from the battery rather than from the 3.3V regulator and added a diode so the forward voltage drop will bring the voltage closer to 5V for a fully charged battery pack.

For sensing the light intensity I'm using a simple LDR (I got some from Jaycar) which forms one half of a voltage divider. This model has a resistance range of 48K to 148K in light with a 10M resistance in total darkness.

I put the LDR in parallel with a 10K resistor to limit the range of resistance and configured a voltage divider with another 10K resistor such that the voltage will range from 0 to a maximum of half the input voltage. Higher voltages represent darker conditions.

Motion Sensor

Because we are not concerned about the light conditions if there is no warm body present I use the output of the PIR as the input voltage for the divider. This means I can detect both conditions with a single analog input and simply check for a value greater than a certain threshold to determine whether to turn the light on or not.

On problem with this approach is that the measured values will trend downwards as the battery discharges. This can be handled in software though, because we also measure the battery voltage we know what level it is at and can adjust the reading from the motion sensor accordingly. The corresponding code in the firmware that does that is as follows:

    #define BASE_LEVEL 0xE8

     /** Read the analog inputs      *      * This function reads the analog inputs (battery voltage and motion sensor).      * Because the PIR and LDR are being driven directly from the batter we adjust      * that input to the current battery voltage (so readings remain consistent).      */     static void readSensors() {       uint8_t power = adcVoltage();       uint8_t motion = adcMotion();       // Adjust motion value according to current power level       if(power<BASE_LEVEL)         motion = motion + (BASE_LEVEL - power);       else if(power>BASE_LEVEL)         motion = motion - (power - BASE_LEVEL);       // Update sensor values       configWrite(STATE_POWER, power);       configWrite(STATE_MOTION, motion);       }

 We now know if someone is sneaking around in the dark, the next step is to cast some light on them.

# Illumination Control

 For illumination I'm using the head assembly of a cheap LED torch. These have the current limiting resistors built in and give you a concave reflective mirror and protective lens as well - much easier than assembly your own array of super bright LED's. The one I'm using is available in most supermarkets in Australia for around $AU 5 - a price well worth paying for the benefits you get above building up something yourself.

 This module is driven direct from the battery (through the same diode used for the PIR) and controlled through an NPN transistor. I measured the current flowing through the LED assembly while driven directly from it's original 4.5V battery pack at 60mA so it fits neatly under the 100mA limit of a BC547 signal transistor.

# Half Duplex Serial (Single Pin)

 This is one of the more interesting aspects of the design. I came across [this implementation](http://nerdralph.blogspot.ca/2014/01/avr-half-duplex-software-uart.html) of a software UART running at half-duplex that only uses a single IO pin. The implementation is in assembly and only consumes 62 bytes of flash (and no RAM at all). Speeds of up to 115200 baud are supported.

 ![Serial Interface](/content/images/galleries/quickies/one_pin_serial.png)

 There are limitations of course - there is no buffering and you cannot receive data while you are sending (and vice-versa). Some supporting external circuitry is required which uses an NPN transistor to ensure data being sent only goes to the Tx line. Anything that comes in the Rx line will be echoed on the Tx though so client software will have to take this into account.

 In this project I'm running the serial line at 57600 baud and I've exposed the Tx and Rx lines to a 6 pin header that can be used with a 3.3V [FTDI cable](http://www.freetronics.com/products/ftdi-cable#.UzO5rR8ci8w) to communicate with the device.

 I've found that transmits (from the ATtiny to the host) are very reliable but receives (from the host to the ATtiny) can be a little unreliable. Part of this is due to the way the firmware checks for activity on the serial port which can miss part of the initial start bit for a character. A longer term solution might be to use an 'on-change' interrupt on the pin so the device can start processing the incoming byte as soon as the start bit is detected. In the meantime this can easily be worked around in software on the client side of the connection.

## Configuration Settings

 Apart from providing a useful debugging tool during development the ability to communicate over a serial port allows the device to be configured without having to modify and re-flash the firmware. To achieve this I moved all of the values that control the behaviour into an array of bytes and treat it as a virtual bank of registers.

 This includes all the configuration values (trigger levels for low battery and light activation, flash rates for the LED and other  properties) as well as state information (current readings for the voltage and motion sensor for example). The configuration values can be saved to EEPROM and will be loaded when the device powers up. If there are no values in the EEPROM a set of suitable defaults will be used instead.

 The full set of available registers are described in the following table.

 Register          |Index|Description ------------------|-----|---------------------------------------------- CONFIG_FIRMWARE   |0    |Firmware version (read only). CONFIG_TRIGGER    |1    |Trigger value for motion/light sensor. CONFIG_COUNT      |2    |Number of sequential trigger readings required. CONFIG_LOW_POWER  |3    |Low power detection level. CONFIG_LIGHT_ON   |4    |Time to keep the light on (in seconds). CONFIG_LIGHT_START|5    |Starting PWM value for turning on light. CONFIG_LIGHT_STEP |6    |Step value for turning on/off light. CONFIG_LED_ON     |7    |Duration (in 1/10th sec) to keep LED on. CONFIG_LED_OFF    |8    |Duration (in 1/10th sec) to keep LED off. CONFIG_LED_ON_LOW |9    |Duration (in 1/10th sec) to keep LED on (LP). CONFIG_LED_OFF_LOW|10   |Duration (in 1/10th sec) to keep LED off (LP). STATE_POWER       |11   |Current battery power reading. STATE_MOTION      |12   |Current motion/brightness reading. STATE_LIGHT       |13   |Seconds remaining before the light goes off. STATE_COUNT       |14   |Current trigger level count.

 Each of these registers can be read over the serial connection and configuration values can be set. This allows the behaviour of the device to be easily customised for the environment it is in and provides some basic monitoring. All that is needed is a simple communications protocol to support this and a client side tool to provide monitoring and configuration capabilities.

## Communications Protocol

 The protocol used to communicate with the device is deliberately simple, each packet contains a single 16 bit value which is used to describe a command (or response) with parameters. The packet format consists of a start character (!), a sequence of 4 printable hex characters for the value, a single printable hex character as a checksum and is terminated by a newline character. On the wire this comes to a total of 7 ASCII characters and is very easy to verify on the ATtiny without needing a lot of memory or code to do so.

 The protocol is implemented in a *ping/pong* method - for each request sent to the device a single response will be returned. Failure to receive a response indicates a communication error or an invalid request. Here is what the code looks like to send a packet to the device and read a response:
 def __send(self, value):       """ Send a value as a packet       """       if self.serial is None:         raise Exception("Attempting to send on an unopen port")
  # Retry until we get a response       retries = RETRY_COUNT       while retries <> 0:         packet = "%c%04X%1X%c" % (           CHAR_START,           value,           self.__checksum(value),           CHAR_END           )         self.serial.write(packet)
    # Read back what we just sent (side effect of the half-duplex UART)         self.serial.read(PACKET_LENGTH)
    # Read the return value and convert it         response = self.serial.read(PACKET_LENGTH)         if len(response) == PACKET_LENGTH:           value = int(response[1:5], 16)           return value
    # Wait and try again         sleep(0.1)         retries = retries - 1
  # Failed, raise an exception       raise Exception("No response from device.")

As I mentioned earlier anything sent to the device will be echoed back to the client so the code above immediately reads back the command it just sent and discards it before looking for any response from the ATtiny. There is no error checking or validation of the response in the sample above, that needs to be added sometime in the future.

The supported commands that can be sent to the device are:

Command |Hex |Description --------|----|------------------------------------------- CMDGET |1R00|Get the current value of register R. CMDSET |2RNN|Set the value of register R to NN. CMD_SAVE|3000|Save the configuration registers to EEPROM.

The response codes that the device can return are:

Command |Hex |Description ----------|----|-------------------------------------- STATUSOK |0000|The operation was successful. STATUSINF|1RNN|The value of register R is NN. STATUS_ERR|FFFF|An error occurred during the operation.

The STATUSINF response is sent in reply to CMDGET and CMDSET commands, the STATUSOK and STATUSERR commands will be sent in response to CMDSAVE.

Configuration Tool

I wrote a small Python class to handle communication with the device using an FTDI serial cable. This allows me to quickly develop useful tools to help with debugging and configuration. One of the first tools I wrote was a simple monitoring program that dumps the sensor data to a comma-delimited text file for later analysis with a spreadsheet.


I wrote a small GUI in Python (using GTK) to allow you to change the configuration values and keep an eye on the current state. I've found that the LDR's vary in behaviour so it is necessary to adjust the trigger level to match the behaviour of the component you are using.

Board Layout and Casing

The circuit is very simple so a simple small home made PCB will do the trick. I managed to come up with a single sided layout that only uses a single jumper wire. I only need three devices in total so it doesn't seem worth doing a two layer board and having it made up at a PCB fabrication service - etching that many boards by hand is not too onerous a task.


The casing was a little more problematic - I wanted to utilise my 3D printer to create a custom case rather than jury rig a standard project box for the purpose, it's one of the more complex objects I've designed in OpenSCAD. It was a bit fiddly to design but I came up with a working design after only a single prototype print. The design files are in the GitHub repository as well so you can have a look at them for yourself.


This has been an interesting project to work on (and immediately useful as well as being fun to do). The ATtiny chips are very capable devices and make for very simple, small circuit designs. Keeping the IO requirements down by sharing pin functionality or looking for hardware and software tricks to multiplex them is a great learning experience. An additional bonus is that it's very easy to move up to an ATmega chip if you do run out of IO.

The development cycle isn't as quick as using the Arduino environment (without a bootloader you have to manually pull the chip out of the circuit and into the programmer for each code update) but it's not overly complex either. I managed to put everything I need in the Makefile so it was a painless process to compile and flash the code when I made changes.

I have a few other projects in mind that would work well with an ATtiny as the main CPU so you'll definitely see some more articles on the site about them.

I hope you enjoyed this project write up, if you wind up building one for yourself or using the design for other purposes I'd love to hear about it. Once again all the design files for this project are available on GitHub under a Creative Commons Attribution-ShareAlike 4.0 International License so you are welcome to take them and use them as you see fit.