The Bluebell, a Bluetooth Notification Device

Wednesday, June 04 2014 @ 09:35 PM

The Bluebell

After more than three weeks, numerous interruptions and delays the project I though would take a few days is finally finished. Finished enough to publish at least.

This post describes the Bluebell - a small Bluetooth controlled ambient device built around an ATtiny85 CPU. This is one of the first complete projects based around my ATtiny85 template library which is a bit of a milestone for that project.

The Hardware

The project pulls together a number of things I've spoken about in recent posts such as the software based UART, a HC-05 Bluetooth module and RGB LEDs driven by PWM so I won't discuss them in detail here.

I've put those components together in a simple circuit, added support for battery powered operation and wrote some firmware to provide a very simple serial protocol for remote control. The whole thing is then packaged in a 3D printed case. As usual, the entire project is available on GitHub under a Creative Commons License if you would like to build your own.

You can see the schematic of the logic board in the image to the left. To make things fit more neatly in the final case I split the LEDs out on to a separate board and use male and female pin headers to join the two together. Although the schematic shows NPN BJT transistors driving the LEDs I am actually using 2N7000 N-channel FETs for this task - I couldn't find a suitable Fritzing footprint for them but the generic NPN transistor was the closest matching component with a compatible PCB layout.

The only part of the circuit that I haven't discussed in previous posts is the power supply. Previously I have used AA battery packs (4 batteries for a total of 6V output) for my projects but I wanted to try and minimise the size of this one so I opted for a 9V battery as the main source of power instead.

There are two regulators in the circuit - a 5V switching regulator and a 3.3V LDO regulator. I'm using a RECOM DC/DC converter for the 5V supply, it is a drop in replacement for the classic 7805 regulator but far more efficient and more suitable for a battery powered device. The output from this regulator is used to drive the LEDs and provide the input to the next regulator. The 3.3V regulator is a MCP1702 LDO regulator which can provide up to 200mA - more than enough for what we need.

To monitor the battery voltage I pass it through a voltage divider and into an analog input. We want to report this back over the Bluetooth connection so it's easy to know when the battery is going to need replacing.

The Firmware

The firmware that is currently in the repository is very simple and doesn't use all the features available on the device - there is no read of the battery level for example even though it is available on an analog input.

Essentially all it does is listen on the serial port for incoming commands from the controlling PC. Each command consists of 6 bytes - a start byte (I'm using the '!' character for this), the red, green and blue values for the LEDs, a time period (in seconds) and a termination byte (the newline character). The use of start and termination characters makes it easier to identify a valid command and to re-sync with incoming commands if there is any noise on the line.

Rather than simply set the requested colour the LEDs will cross fade from their current colour to the requested one, hold it for the specified number of seconds and then fade back to off. This serves two purposes - it means the device is not always glowing at you so you are more likely to pay attention when it does start glowing and it improves the battery life.

There are two RGB LEDs in the device, both of which always display the same colour. In the worst case (the LED's are full white) that means a current draw of 120mA (20mA per colour for two LEDs).

To establish the serial connection you simply connect to the Bluetooth device as a virtual serial port. I have pre-configured the HC-05 module as described here so no additional work is required on the firmware side.

Controller Software

To drive the device I wrote a simple Python wrapper that uses pySerial to communicate with the serial port. I separated that out into a separate class that you can use in your own applications. There is a sample command line tool that simply takes the serial port name, the colour values and (optionally) the time and sends that to the device.

Next Steps

Although I have something up and working it is still very much the seed of an idea rather than the complete fulfilment of one. The whole idea of an ambient device is great and I would really like to spread them through my house like confetti. Currently available implementations seem too overpriced for my liking and lack an easy interface.

The current iteration is very clunky, to say the least, and the position of the two LEDs isn't the most appealing when they are lit. I would really like to shrink it down to a much smaller size. Improving the power consumption or moving to a different power source would help - it's the battery and supporting power management circuit that takes up a lot of room.

The boxy form factor needs to go as well. I recently came across some small LED lights at a dollar store that are fairly appealing - you can see one of them in the image to the right. If I replaced the clear plastic top cover with something that provide a bit more diffuse light it would be a perfect size and easy to mount unobtrusively around the house and the garage.

The Bluetooth based connection works well (and the HC-05 module is very easy to use) but requires a controller that is always on so the connection remains established. It also means you need a separate virtual serial port for each device you have connected - not bad if there is only one or two of them but I would like to have the ability to use a larger number than that.

The biggest improvement would not be related to hardware at all but a simple communications protocol that could be used to control devices such as this as well as reading data from remote sensors that makes it easy to integrate everything together. That however, is a much larger project.


PWM Output on the ATtiny85

Monday, May 19 2014 @ 07:10 PM

The next step in the Bluebell project is a way to provide notifications. We can control the device remotely using Bluetooth and now we need to make it display the data sent to it in an easily noticeable format.

I'm going to use an RGB LED for this - it can be driven through standard digital pins to give 7 different colour values but it would be nice to expand that range. Because the ATtiny doesn't have any way to control the current or voltage on an output pin we have to simulate that using PWM.

What is PWM?

PWM (Pulse Width Modulation) is a way of simulating different voltage levels with a single digital (on/off) output pin. This is achieved by modifying the duty cycle (the portion of time that the output is high) of the signal to simulate a lower voltage (or current) over time.

PWM Generation

The graph to the left gives a brief example of how this works. In this case we want the equivalent of a 3V output from a 5V digital output pin. In the graph the blue line represents our target voltage and the green line represents the actual pin output. The output pin is pulled high for 3/5ths of each cycle and low for 2/5ths - over time this is the equivalent of a 3V output.

You can use this technique to control the brightness of an LED (or the speed of a motor) by changing the effective current passing through the device over time. If the LED is connected to the output pin with a current limiting resistor to allow 20mA of current the duty cycle will change the effective amount of current flowing through the device. Using a duty cycle of 50% (the output is high for 50% of the time and low for the other 50%) means that, over time, the effective current through the LED will be 10mA - ff the output pin was always on the LED would consume 20mAh after one hour, with a 50% duty cycle that is reduced to 10mAh.

The frequency of the PWM signal is important as well. When you are driving a LED a frequency above 30Hz will be high enough to control the brightness without any visible flicker. When controlling a motor or simulating an output voltage you will need a much higher frequency. For the purpose of this post I'm just going to concentrate on driving LEDs, motor control and other applications will be left to another post.

PWM Hardware on the ATtiny

The ATtiny has direct hardware support for generating PWM output using the two timer/counter modules on the chip. Both of them can be used to drive the output of a digital pin to generate a PWM signal through the use of a comparison register (OCR0A and OCR0B for timer 0). The counter associated with the timer will cycle from 0 to 255, reset to 0 and then repeat the sequence continuously.

When the output compare function is enabled the corresponding output pin will be set high while the current counter value is less than the comparison register and switch to low when it exceeds the value.

Each timer on the ATtiny is capable of driving two PWM output pins. One of these pins overlaps (it can be driven by either TIMER0 or TIMER1 but not by both simultaneously) which gives a total of 3 possible PWM outputs.

My template library provides control over the two PWM outputs associated with TIMER0 leaving TIMER1 available for other purposes. In this implementation the PWM runs at around 32KHz and has a full 8 bit range giving 256 discrete steps from fully off to fully on. The trace to the left shows the PWM output for a 25% and 50% duty cycle on the outputs.

Implementing PWM in Software

To use the maximum 3 channels of PWM output requires using both timer modules. It is possible, however, to simulate PWM output in software on as many pins as needed using a single timer.

Both timers can generate an overflow interrupt which is triggered when the counter transitions from 255 back to 0. Using this interrupt to generate the time base, an array of our own comparison registers and by manually changing the state of the output pins we can generate a PWM pulse ourselves (although it will be at a much lower frequency than the hardware generated pulse).

The main purpose I have in mind for the software driven PWM is to drive an RGB LED - in this case the PWM frequency needs to be above 30 Hz, enough to have some control over the brightness of the LED without any visible flicker. For this application we don't need the full 8 bit range of values either (realistically the human eye is not going to notice much of a difference when you change the output by 1 bit in either direction).

I settled on a 60Hz PWM frequency with 6 bits (64 steps) of resolution. There are a number of advantages to this:

  1. The overflow interrupt will be triggered at 3840 Hz (60 x 64) which is once every 2083 clock cycles when using an 8MHz clock. Using an average of 2 clock cycles per instruction means that the interrupt is triggered once every 1000 instructions or so. The interrupt handler is less than 100 instructions in length so we are using less than 10% of the processors capacity to provide the PWM.
  2. The 60Hz frequency is a reasonable base for longer time measurements. Using the same interrupt I maintain a tick counter which measures the number of ticks that have elapsed - at this frequency there are 60 ticks per second.

Setting up the timer for this is fairly straight forward:

// Set up the prescaler and enable overflow interrupt
TCCR1 = (1 << CS12); // Divide by 8
TIMSK |= (1 << TOIE1);

Most of the timer settings are left at power up defaults. I use the system clock (8MHz) as the timer clock source and use the prescaler to divide by 8 which will increment the counter at 1MHz. The overflow interrupt will be triggered every 256th count (when the counter wraps around) or 3906 times a second - about as close as to the 3840Hz target frequency as we can get.

Every time the interrupt is triggered we update a ticklet count. To map the 6 bit range to a full 8 bit range we increment it by 4 each time prior to using it - this means that 0 is still 'off' and 255 is 'on' just as they are for the hardware PWM even though there are only 64 steps available. Finally we compare the current counter value against our target to determine if we should turn the output on or off. The code looks like this:

// Update the 'ticklet' count
g_ticklet += (256 / TICKLETS);
// Update PWM outputs
if((g_pwmout[0]>0)&&(g_ticklet<=g_pwmout[0]))
  pinHigh(SPWM_PIN0);
else
  pinLow(SPWM_PIN0);

The output is a nice stable PWM signal at close to 60Hz as shown in the trace to the right. The library uses #define statements in hardware.h to specify the number of PWM channels to enable and to assign each one to an output pin.

Although you could theoretically use all 6 IO pins as PWM outputs using this method I've imposed an upper limit of 4 pins in the implementation. This can easily be extended if needed.

A typical set up (in this case for 3 PWM outputs on B0, B1 and B2) looks like this:

/** Enable the SOFTPWM functions */
#define SOFTPWM_ENABLED

/** Use 3 PWM channels */
#define SPWM_COUNT 3

/** Pin associated with SPWM0 */
#define SPWM_PIN0 PINB0

/** Pin associated with SPWM1 */
#define SPWM_PIN1 PINB1

/** Pin associated with SPWM2 */
#define SPWM_PIN2 PINB2

You can use both the hardware PWM and software PWM side by side. The software implementation is useful for controlling LED's while the hardware PWM can be used to control motors or servos.

Controlling an RGB LED

Using the software PWM output to control an RGB LED is straight forward. You will need three channels as shown above (one each for the red, green and blue elements of the LED). I run my ATtiny from a 3.3V source which is not enough to drive a blue LED (they typically have a forward voltage drop of over 3V) so I drive the LED's from a separate 5V supply through a 2N7000 FET. This also has the advantage of minimising the current being pulled through the output pins of the ATtiny (20mA per LED which means 60mA if they are all on simultaneously).

The sample code below transitions the LED from red to green to blue and back again:

static uint8_t g_red = 0;
static uint8_t g_grn = 0;
static uint8_t g_blu = 0;

/** Set the target values for the LED's
 *
 * @param red target for red LED
 * @param grn target for green LED
 * @param blu target for blue LED
 */
static void ledSet(uint8_t red, uint8_t grn, uint8_t blu) {
  g_red = red;
  g_grn = grn;
  g_blu = blu;
  }

/** Update the current PWM values to bring them closer to the target
 */
static void ledUpdate() {
  // Update RED value
  uint8_t val = spwmValue(SPWM0);
  if(val<g_red)
    val++;
  else if(val>g_red)
    val--;
  spwmOut(SPWM0, val);
  // Update GREEN value
  val = spwmValue(SPWM1);
  if(val<g_grn)
    val++;
  else if(val>g_grn)
    val--;
  spwmOut(SPWM1, val);
  // Update BLUE value
  val = spwmValue(SPWM2);
  if(val<g_blu)
    val++;
  else if(val>g_blu)
    val--;
  spwmOut(SPWM2, val);
  }

static bool ledMatch() {
  return ((spwmValue(SPWM0)==g_red)&&(spwmValue(SPWM1)==g_grn)&&(spwmValue(SPWM2)==g_blu));
  }

//---------------------------------------------------------------------------
// Main program
//---------------------------------------------------------------------------

/** Program entry point
 */
void main() {
  spwmInit();
  sei();
  // Set up output values
  uint32_t output = 0x00FF0000L;
  ledSet((output>>16)&0xFF, (output>>8)&0xFF, output&0xFF);
  while(true) {
    if(ledMatch()) {
      output = output >> 8;
      if(output==0)
        output = 0x00FF0000L;
      ledSet((output>>16)&0xFF, (output>>8)&0xFF, output&0xFF);
      }
    ledUpdate();
    wait(10);
    }
  }

Rather than simply force the requested colour output the code performs a smooth transition by incrementing (or decrementing) the actual PWM output over time until the target is reached.

As the video above shows this results in a set of intermediate colours before the target is reached and is a good visual test of how smooth the PWM output is. In this case I'm very happy with the result - it's exactly the sort of look I'm after for my notification device.

Next Steps

The core parts of the notification system are now in place - it can be controlled remotely over a Bluetooth link and I have a way of providing some visual feedback using an RGB LED. Now I just need to put those components together in a single circuit, add a battery powered supply and put it in a case. That is going to be the topic of the next post.


Using the HC-05 Bluetooth Breakout

Tuesday, May 13 2014 @ 06:38 PM

In my previous post I described the implementation of a two pin, interrupt driven serial interface for the ATtiny - in this post I'll describe how to configure and use a HC-05 Bluetooth breakout board with that interface so you can remotely control any ATtiny based circuit.

The HC-05 Bluetooth Module

HC-05 Breakout

These modules are widely available and usually relatively cheap (about $AU 6 on eBay). There are two common configurations available - one is a module with a pair of IO pins down each side and the other format is a breakout board with some supporting circuitry and a set of 6 pins at one end. Both have the same functionality but using the module gives you easier access to the GPIO pins on the module as well as some additional state - they are a little harder to design into your circuit though.

For this post I'm using the breakout board, the techniques I describe can still be used with the module format although the pin connections will be in a different location.

Module Configuration

The modules I am using (and, from what I can tell, most other variants) default to using the device name HC-05 and communicate at 9600,N,8,1. We certainly want to change the speed and it would be nice to change the name as well. Thankfully - the chipset has an AT command mode you can enter which allows you to reconfigure the device settings. The changes you make using this method are persistent and will be preserved across power cycles.

HC-05 Configuration

To talk to the module for configuration I am using a 3.3V FTDI Friend cable to provide power as well as communications lines. The diagram to the left shows how to connect the devices together. When you wire it up on the breadboard do not connect the 3.3V lines before you plug the FTDI cable into the USB port.

According to the datasheet there are two ways to enter AT command mode:

  1. If the STATE pin is held high when power is applied to the module it will boot into command mode and set the serial port to 38400N81 regardless of the current serial port settings on the device.
  2. While the device is running pulling the STATE pin high will switch to command mode at the current serial port settings.

For this example we are going to use the first method. First, connect all wires except the 3.3V ones. Plug the FTDI adaptor into your USB port and connect to it with your favourite terminal program at 38400N81. Make sure your serial program is configured to send CR/LF (\r\n) line endings. Connect a wire from the FTDI 3.3V output to the STATE pin and then connect the wire from the FTDI 3.3V output to the 3.3V pin on the breakout board. The red LED on the board should start flashing slowly (about once every two seconds).

To test the connection, try the following command (in these examples the < and > characters indicate the data transfer direction - don't type them).

> AT
< OK

That example simply sends the basic AT (attention) command and the device should respond with a simple OK to indicate that it got it. If you don't get the OK response double check your wiring, the voltage level and your serial port settings.

NOTE: These are relatively cheap devices and are not always of the best quality - it is well worth buying more than one just in case you get a unit that doesn't work. I bought a batch of 5 from the same supplier and one of them simply refuses to enter command mode for some reason even though I can communicate with it over Bluetooth with the default settings.

Once you have access to command mode there are a wide range of options you can change (see the datasheet for a full description of what is available). In this post I'm just going to concentrate on some of the most common options.

The first thing we will want to change is the serial configuration - 9600 is way to slow for most purposes (and my software UART implementation won't work at such low speeds anyway). The following commands will do the trick ...

> AT+UART?
< OK
< +UART:9600,0,0
> AT+UART=57600,0,0
< OK
> AT+UART?
< OK
< +UART:57600,0,0

In this case we are querying the current settings ('AT+UART?') and the device responds with the currently active settings (9600 N81 in this case). Next, we tell the device to use 57600 baud, no parity and 1 stop bit ('AT+UART=57600,0,0' - the command format and parameters are described in the datasheet). Just to check that everything is saved away safely we query the current settings again to make sure they match what we just set.

The next thing to change is the device name - this is what appears when you browse for available Bluetooth devices. The default name for the module is HC-05 so something that better reflects what you are connecting to would be nice. Use this sequence of commands ...

> AT+NAME?
< +NAME:HC-05
> AT+NAME=BLUEBELL
< OK
> AT+NAME?
< OK
< +NAME:BLUEBELL

Once again we query the device for the current settings, change it to the value we want (in this case 'BLUEBELL' which is what I've decided to call the device I'm working on) and then query it again to make sure that our changes have gone through.

Finally, we want to change the pass-code used to pair with the device from the default value (1234) to something that is (presumably) secret.

> AT+PSWD?
< OK
< +PSWD:1234
> AT+PSWD=0000
< OK
> AT+PSWD?
< OK
< +PSWD:0000

Testing

Testing is very easy - from the ATtiny side we are simply writing to a serial port, no other setup is required and the only pins you need to connect (apart from 3.3V power and ground) are the Tx and Rx pins. As long as the baud-rate and serial settings match we just read and write data as normal. From the PC side all we need to do is pair with the Bluetooth module and enable it as a serial port.

Serial Connection

I use Xubuntu on my desktop and pairing is done through the Bluetooth Manager application in the Settings Manager. The pairing process is straight forward and the device will automatically be detected as being a serial port.

On my system the pairing process results in a new serial port called /dev/rfcomm0 which I can connect to with my normal serial terminal (in my case I use CuteCom which is perfect for development tasks). Connecting on this port at 57600 now lets me talk to the code running on the ATtiny.

The sample program I'm using simply echoes back all the data it receives and converts lower case letters to upper case just to prove it is not going to some internal loop-back. The image to left shows the results in CuteCom.

The sample program is very simple - this is what the main function looks like:

void main() {
  uartInit();
  sei();
  while(true) {
    if(uartAvail()) {
      uint8_t ch = uartRecv();
      if((ch>='a')&&(ch<='z'))
        ch = (ch - 'a') + 'A';
      else if((ch>='A')&&(ch<='Z'))
        ch = (ch - 'A') + 'a';
      uartSend(ch);
      }
    }
  }

Next Steps

Now we have a way to communicate with device without wires we can easily send and retrieve state from it. I'm planning on using an RGB LED for the notification indicator and I'd like to use PWM to get a full range of colours out of it. That is going to be the subject of the next post.


A Software UART for the ATtiny85

Saturday, May 10 2014 @ 08:18 PM

I've recently started work on a small project to provide visual notifications from a PC over a Bluetooth connection. I mentioned it on G+ a little while ago, it's essentially a minimalist version of this product.

I started working on a post describing the work I was doing but it quickly expanded into something much larger than I expected as I started to describe aspects of my ATtiny85 template library which is being used as the basis of the project.

I decided to split the post into a number of smaller ones so I can cover the various topics in detail without swamping you with information. This post, the first in the series, provides an overview of the software UART implementation in the library - how it's implemented and how it's used. The upcoming posts will cover:

  1. Using a HC-05 breakout module with the library. This will include using AT commands to configure the module and basic connections to the ATtiny.
  2. PWM output on the ATtiny and the template libraries equivalent to the analogOut() function in the Arduino library.
  3. Finally the full project including schematics and code. This uses RGB LEDs to provide colour coded notifications under control of a remote PC through Bluetooth. Use it to provide information in a glance - you can control the colour based on whatever data you choose, stock information, website analytics or Bitcoin exchange rate.

For now, a brief introduction to the library and how to use the software UART.

The ATtiny85 Template Library

I've mentioned my ATtiny Breakout Board and the associated template library in previous posts but only on a very superficial level. Although this post will concentrate on the software UART implementation it's worth running over it again.

Breakout Board

The library is available on GitHub at the link above and the documentation for it can be found here. I've done my best to provide enough information in the project documentation to make it easy to get started. I'd appreciate any feedback you might have on how to improve it.

To use the library as a basis for your own projects simply fork the GitHub project (or clone your own local copy) and start modifying the code in the firmware directory. To get started you only need to start modifying the file hardware.h (which contains the pin assignments and is used to enable or disable various parts of the library) and main.c.

To keep up to date with newer version of the library simply merge the master branch of the original GitHub library into your project every now and then. The majority of updates will be in the firmware/shared and firmware/include directories which contain the implementations and definitions of the library components respectively.

Serial IO Basics

Before I get into the implementation details of the UART functions it's probably best to run through how serial communication appears on the wire. Because we are generating these signals in software it's a good idea to know what is expected and what sort of issues to look out for.

The diagram above shows the voltage levels on the Tx pin when the ASCII character sequence "AVR" is being sent. Obviously this is what will be seen on the Rx pin as well on the receiver end of the connection. In this case the connection is running at 3.3V levels (not RS232 levels) and the connection is configured for 57.6 KBaud, 8 data bits, 1 stop bit and no parity (57600 8N1). I've marked each character on the diagram along with the values for the individual bits.

The diagram above shows the detail for a single character (in this case the ASCII character 'A'). Even though we are only sending 8 bits of data per character additional bits are required for framing (to allow the receiver to detect the start of a character and to determine it has reached the end). In every case there is a single start bit - this causes the communications line to transition from it's idle state (high) to low for at least the duration of a single bit. The data bits come next starting from the least significant bit (bit 0) to the most (bit 7) and finally, the stop bit. The stop bit ensures the line comes back up to it's idle state for at least one bit.

The width, or duration, of each bit is a function of the baud rate - for the speed we are running at that is 1/57600 seconds per bit (about 17 microseconds) - and each bit must be exactly the same width. For our configuration it takes 10 bits to transmit a single byte (1 start bit, 8 data bits and a stop bit) so it takes 170 microseconds per byte of data giving a peak transfer rate of 5.6Kb per second assuming there are no gaps between the bytes. In reality it will be lower).

From the first diagram you can see that there are no restrictions on the gaps between the characters (the inter-character delay). The line must remain at an idle state for this period (logic high) but the duration doesn't have to be a multiple of whole bits and can be as long as is needed.

Writing Serial Data

Writing data out the serial port is fairly straight forward - we know the width of each bit so we simply toggle an output pin as needed and ensure it stays in the right state for the full duration of a bit. All we need to do is pull the output pin low for one bit width for the start bit, send out the individual bits from LSB to MSB and then bring the output high for one bit width as the stop bit. The output is then left high to represent the idle state.

The current implementation does just that in a small loop of assembly code. Because the timing is critical interrupts are disabled while this is happening - you will need to keep this in mind if you are depending on interrupts in your code.

NOTE: The serial send and receive code in the library makes heavy use of the assembly routines developed by Ralph Doncaster and described here. I said that generating the output is straight forward and, as a concept, it is. The actual implementation to get the timing right based purely on the number of instruction cycles needed to execute each op-code is another matter. The code developed by Ralph is a really nice piece of work - I highly recommend visiting his site for more examples.

Reading Serial Data

Reading the serial data is the reverse operation. We simply watch the input pin and wait for it to transition from high (idle state) to low which will be our start bit. We then wait for 1.5 bit periods and then start sampling for the input pin once every bit period to get the value for the appropriate bit. We need 9 samples in all - 8 for the data and 1 for the stop bit.

The reason for the 1.5 bit delay at the start is to ensure that we are sampling the bit value in the middle of it's transmission period. If we are slightly off in our timing we will still get the right value for the bit.

The full implementation of the receiver can be found here. Initially this only provided a blocking implementation - if you called the uartRecv() function it would wait indefinitely until a start bit was detected before it would read and return a character. In cases where nothing happens until it is requested over the serial port (like a bootloader for example) it doesn't matter that the processor is locked in an endless loop with interrupts turned off.

If you want to be able to do work and only respond to serial input when it was available it was not the perfect solution. I've now added an option to have the start bit trigger a pin change interrupt which will cause the incoming byte to be read and buffered for later processing. There is also a uartAvail() function that tells you how many characters are currently buffered.

Pin Change Interrupts

The AVR allows for an interrupt to be triggered when the value of an input pin changes state (in either direction). On the ATtiny85 all of the inputs can be used to trigger this interrupt (which uses the vector PCINT0) but, if you enable multiple pins as triggers, you can't tell which one actually caused the interrupt. You can figure out what the transition was though - if the current reading on the input is 0 you can assume a high to low transition for example.

When the interrupt handler is invoked interrupts will be disabled until they are explicitly enabled by the handler code or the interrupt handler returns. The interrupts don't queue so if another triggering event occurs while the current interrupt is being handled it won't be immediately triggered again when it is done.

Handling the Interrupt

Supporting the interrupt is actually straight forward - what was the original code for the uartRecv() function has been moved to an ISR handler. The code to wait for the start bit is no longer necessary as it has already been detected to trigger the interrupt.

ISR(PCINT0_vect) {
  uint8_t ch;
  // Make sure it is our pin and it is 0
  if(!(PINB&(1<<UART_RX))) {
    // Start the read (assuming we have the start bit)
    asm volatile(
      "  ldi r18, %[rxdelay2]              \n\t" // 1.5 bit delay
      "  ldi %0, 0x80                      \n\t" // bit shift counter
      "RxBit:                              \n\t"
      // 6 cycle loop + delay - total = 5 + 3*r22
      // delay (3 cycle * r18) -1 and clear carry with subi
      "  subi r18, 1                       \n\t"
      "  brne RxBit                        \n\t"
      "  ldi r18, %[rxdelay]               \n\t"
      "  sbic %[uart_port]-2, %[uart_pin]  \n\t" // check UART PIN
      "  sec                               \n\t"
      "  ror %0                            \n\t"
      "  brcc RxBit                        \n\t"
      "StopBit:                            \n\t"
      "  dec r18                           \n\t"
      "  brne StopBit                      \n\t"
      : "=r" (ch)
      : [uart_port] "I" (_SFR_IO_ADDR(PORTB)),
        [uart_pin] "I" (UART_RX),
        [rxdelay] "I" (RXDELAY),
        [rxdelay2] "I" (RXDELAY2)
      : "r0","r18","r19");
    // Now put it in the buffer (if we have room)
    if(g_index<UART_BUFFER)
      g_buffer[g_index++] = ch;
    }
  }

For our purposes we only want to respond to the start bit which is a transition from 1 to 0 on the input. We sample the Rx pin and if it is not 0 we simply ignore the interrupt and move on. If it is zero we take it to be the start bit and start reading the character.

Because the compiler generates some start up code for the ISR we take that into account in the delay time before we start reading bits.

Input Buffering

Because the receive code is not called directly and can't return a value we need to store the input character somewhere so the main application code can access it later. For this purpose we maintain a small FIFO buffer - newly read characters are added to the end of the buffer and calling the uartRecv() function will take any pending characters from the front of the buffer.

The size of the buffer is set in hardware.h for the project, it generally doesn't need to be too large - just enough to store characters until the main program gets a chance to read the cached characters and do something about them.

Using the Serial Port

The serial port, like other modules in the library, is enabled and configured in the firmware/hardware.h file. The relevant sections look like this:

/* Software UART
 *
 * This feature provides a software implementation of a UART allowing you to
 * perform serial communications. It supports speeds of up to 250Kbps, can be
 * configured to use a single IO pin and has an option to be interrupt driven.
 */
#define UART_ENABLED

/* Baud rate to use
 *
 * The implementation is optimised for higher baudrates - please don't use
 * anything below 57600 on an 8MHz clock. It does work at up to 250000 baud
 * but you may experience a small amount of dropped packets at that speed.
 */
#define BAUD_RATE 57600

/* Define the pin to use for transmission
 */
#define UART_TX   PINB1

/* Define the pin to use for receiving
 *
 * If this pin is the same as the TX pin the code for the single pin UART
 * implementation is compiled. This means no buffering and no interrupts.
 */
#define UART_RX   PINB0

/* Enable interrupt driven mode
 *
 * Only valid in two pin configuration. If this is defined and you are using
 * two pins for the UART then received data will be read and buffered as it
 * arrives.
 */
#define UART_INTERRUPT

/* Size of input buffer
 *
 * Only available in interrupt driven mode. This sets the size of the receive
 * buffer (max 256 bytes).
 */
#define UART_BUFFER 4

The UART_ENABLED macro must be defined to enable any UART functions, UART_TX and UART_RX define the pins to use for Tx and Rx respectively (because the ATtiny only has a single IO port you can use the definitions from - PINB2, PINB3, etc). If UART_INTERRUPT is defined then interrupt driven IO with an input buffer is enabled. Finally UART_BUFFER defines the size of the input buffer to use - this must be at least 1, I've found that 4 is a reasonable value for most cases.

NOTE: Because this is implemented in software rather than hardware it is not full duplex. The code uses instruction cycles to determine the timing of each bit, to ensure that this is accurate interrupts are disabled during both send and receive. What this means is that if a character is incoming while you are sending when the send is complete the receiving code could detect a low data bit as the start bit. This will result in garbage data being detected as a valid character. The solution is to implement a command/response type protocol so both sides of the connection have an idea of when they should be expecting data and when they should be sending it.

With the new changes there are now 3 modes of operation for the UART that you can choose from. These are ...

Single Pin Mode

To use this mode simply define UART_TX and UART_RX to be the same pin, the code will recognise this and change it to be an input or output as needed for communications.

One Pin Serial

This mode is best to use when you want to minimise the number of pins in use on your project but it has a few drawbacks. First, it requires some additional discrete components to work (a transistor, a resistor and a diode - shown in the schematic to the right) and secondly it will echo any data it receives back to the sender. Any code you write to communicate with a device using this configuration will have to take that into account.

This is the configuration used in the sample schematics included in the template and used by the Microboot bootloader. In that situation we use PINB5 (and disable the RESET functionality normally attached to the pin) for code transfer. It's a good place to start with your project and enables you to easily add debugging output using the same connection you load code through.

This mode does not support interrupts so the UART_INTERRUPT macro will be ignored and calling uartRecv() will block until data is available. The uartAvail() function is still present but it will always return 0.

Two Pin Mode (Blocking)

In this mode Tx and Rx are on different pins so no external hardware is needed. It also eliminates the transmission echo that is present on the previous mode.

In the blocking version of the mode (UART_INTERRUPT is not defined) the functions behave as they do for the single pin mode - calling uartRecv() will block until data is available. The uartAvail() function is still present but it will always return 0.

Two Pin Mode with Buffering

If UART_INTERRUPT is set and UART_TX and UART_RX are defined as different pins you will get the interrupt driven implementation with a receive buffer. The value of UART_BUFFER will be used for the buffer size and the uartAvail() function will return the number of characters currently in the buffer - if this value is greater than zero than a call to uartRecv() will not block. If uartAvail() returns 0 then a call to uartRecv() will block until a character is received.

When you are using the interrupt driven implementation you will need to enable interrupts on the chip in your application initialisation as follows:

void main() {
  uartInit();
  sei();
  while(true) {
    // Do stuff here
    }
  }

Helper Functions

I've added a number of helper functions to the UART library that work with any of the operation modes. You can see a full description of them in the documentation.

Essentially they provide a simple way to print 16 bit values to the serial port (in decimal or hexadecimal) as well as zero terminated strings from either RAM or program memory. There is also a minimalist 'printf()' style function called uartFormat() that lets you use a format string to display a combination of strings and integers.

Next Steps

The next post will describe how to configure and use a HC-05 Bluetooth breakout board with the library so we can control a device remotely. Once that is done we can start adding visual notifications with an RGB LED and finally put it all together with a battery pack in a nice 3D printed case.


Nokia 5110 LCD Displays on the ATtiny

Wednesday, April 30 2014 @ 07:08 PM

To test the software SPI implementation in my ATtiny85 template project I wanted to use a component that I have had experience with before, was fairly simple to use and would be useful. A small Nokia LCD display fits the bill nicely.

If you are not familiar with these they are a 1.5" monochrome LCD with an 84 x 48 pixel resolution that were used in older Nokia phones such as the 3110 and 5110. These screens are widely available and very cheap (around $AU 3 on eBay). You can get them from SparkFun and AdaFruit as well.

There are a number of interface libraries available for the Arduino already but in this post I'm going to present a more low level interface. Most of the Arduino libraries I've seen use an off screen frame buffer to prepare the image before sending it to the display itself. This method consumes 504 bytes of RAM which is almost all of the available 512 bytes on an ATtiny so it's just not acceptable in our restricted environment. This library consumes no RAM and sends the data directly to the display - this imposes some limitations on what you can display and prevents any meaningful image composition (drawing text over images for example) but with a bit of forethought you can work around this to achieve nice looking and functional displays for your project.

NOTE: These modules run at 3.3V so if you are using a 5V supply on your CPU you will need to run the inputs through a level converter first as well as provide a 3.3V power supply for module itself.

This code has already been integrated into the library, you can grab the repository from here and here is the documentation for the new functions. Some sample code to drive the routines can be found in this gist.

Connecting the Display

The display provides an SPI interface with most of the expected pins present (MOSI, SCK and CE - MISO is missing, you can't read data back from the device which is why many libraries use the off screen frame-buffer). On top of that though there is a pin to select between command and data modes as well as a RESET pin which you will need to be able to manipulate during device initialisation. The next image shows how I wired everything up for this example.

LCD Connections

As this is the only SPI device in use I've wired the chip select line directly to ground so it is always selected. The backlight is pulled to ground as well, you can tie this to Vcc through a current limiting resistor if you like or drive it from a digital output or push button if you want to be able to turn it on or off to match the lighting conditions. The remaining four pins are connected to the ATtiny.

Double check the pin assignments on the module before connecting it up, I have a few from different suppliers and the pin order is slightly different in two of them.

Communications

Before you do any communication with the device it needs to be reset. The datasheet is very specific on how this occurs; the RESET line must be LOW when power stabilises and must be pulled high within 100ms of power on - this means that the initialisation process must occur very early on in the program. Once the device has been reset you can start sending commands and data to it over the SPI bus.

The C/D pin is used to determine if the data being sent is a command (C/D is low) or data (C/D is high). The pin state must be sent prior to the first bit being sent. Bytes are sent to the device with the MSB coming first.

Sending Commands

The LCD supports 8 separate commands which are broken into a 'normal' and 'extended' command set. The majority of the 'extended' commands are to do with initialisation and device configuration, they are generally only sent immediately after the device is turned on in order to put it in a working state.

To send a single command all you need to do is set the C/D pin to LOW and then send out the command byte on MOSI clocked by the SCK pin. The MOSI value is read by the LCD on the rising edge of the SCK signal. The following simple function achieves this:

/** Send a command byte to the LCD
 *
 * @param cmd the command byte to send.
 */
static void lcdCommand(uint8_t cmd) {
  // Bring CD low
  PORTB &= ~(1 << LCD_CD);
  // Send the data
  sspiOutMSB(LCD_SCK, LCD_MOSI, cmd, 8);
  }

The LCD_CD, LPC_SCK and LCD_MOSI constants define the pins that are being used for communication. To actually send data I'm using the sspiOutMSB() which is part of the template library I developed. This particular function implements the SPI protocol in software allowing you to transfer data via SPI on arbitrary pins.

Sending data is similar, first the C/D pin must be set HIGH and then the data byte is sent MSB first. This variation looks like:

/** Send a data byte to the LCD
 *
 * @param data the data byte to send.
 */
static void lcdData(uint8_t data) {
  // Bring CD high
  PORTB |= (1 << LCD_CD);
  // Send the data
  sspiOutMSB(LCD_SCK, LCD_MOSI, data, 8);
  }

These two functions are all that is needed to start developing the library.

Initialising the Device

The initialisation sequence was a little difficult to get working, it seems that some implementations of the chipset are a little picky about the order in which the commands are sent. Once that is resolved it easy enough to do - the code here has worked on a range of screens from multiple vendors without fail.

/** Initialise the LCD
 *
 * Sets up the pins required to communicate with the LCD screen and then does
 * the actual chipset initialisation. The pin numbers to use are defined in
 * @ref hardware.h.
 */
void lcdInit() {
  // Set up the output pins, ensuring they are all 'low' to start
  uint8_t val = (1 << LCD_SCK) | (1 << LCD_MOSI) | (1 << LCD_RESET) | (1 << LCD_CD);
  PORTB &= ~val;
  DDRB |= val;
  // Do a hard reset on the LCD
  wait(10);
  PORTB |= (1 << LCD_RESET);
  // Initialise the LCD
  lcdCommand(0x21);  // LCD Extended Commands.
  lcdCommand(0xA1);  // Set LCD Vop (Contrast).
  lcdCommand(0x04);  // Set Temp coefficent.
  lcdCommand(0x14);  // LCD bias mode 1:48.
  lcdCommand(0x20);  // LCD Normal commands
  lcdCommand(0x0C);  // Normal display, horizontal addressing
  }

The first thing we do is initialise the IO pins that are going to used and ensure they are all set low. We then wait for 10ms (to make sure the voltage levels have well and truly settled) and raise the RESET pin high to reset the LCD.

Immediately after that we configure the LCD driver operation. The temperature coefficient and bias mode are the recommended values from the datasheet and can be left as they are. The LCD Vop value is essentially a contrast control (it controls the bias voltage to the liquid crystal). The command format is 01nnnnnn in binary where nnnnnn is a value between 0 and 64 inclusive. If this value is too high the entire display will be too dark and you won't be able to make out the image from the background, conversely if it is too low the display will appear washed out. The value used in the initialisation sequence above seems to provide good readability with sharp contrast - you can raise or lower it to suit your display if you need to - just make minor modifications (+/- 8 to start with) until you get a value that works for you.

The final command sets the addressing mode - this determines the order in which pixel data is sent to the device. Before we move on to that we need to look at how the pixels are stored in the displays memory.

Memory Layout

The pixels are stored in memory in 6 rows of 84 columns each. Each row is 8 pixels high (giving the vertical resolution of 48 pixels) and each column is a single byte setting the value of each of the 8 pixels.

Memory Layout

To change the displayed pixels you send a byte to a specific row and column address - the bits in the byte set the value of each pixel in the row from the top (bit 0) to the bottom (bit 7). If you are used to the normal frame-buffer layout that goes from left to right, top to bottom this can be a little more difficult to get used to, calculate the correct offset to write a pixel using standard cartesian co-ordinates is certainly more complicated.

Writing Pixels

To write pixels to the display you need to set the row and column addresses as a command and then write data values to store in the LCD memory. The address will automatically increment as each data byte is received so you don't need to set the address for each byte. The following code shows a simple example of this:

/** Clear the screen
 *
 * Clear the entire display.
 *
 * @param invert if true the colors are inverted and the screen will be filled
 *               with black.
 */
void lcdClear(bool invert) {
  uint8_t fill = invert?0xFF:0x00;
  // Set the position
  lcdCommand(0x80);
  lcdCommand(0x40);
  // Fill in the whole display
  for(uint16_t index = 0; index < (LCD_COL * LCD_ROW); index++)
    lcdData(fill);
  }

In this case we are clearing the screen by setting all pixels off (or, if inverted, setting them on). The first two commands set the column address to 0 and then the Y address to 0. We then send 504 bytes to fill the entire memory. When the internal address counter reaches the end of a row it will automatically wrap to the next row so we don't have to reset the addresses every time we change rows.

Simple Display Routines

I've kept the set of routines available very simple on purpose. As well as the screen clearing function (shown above) there is a function to clear a single row. For general output I've provided functions to display text (both plain and inverted) and small images. These functions are described below.

Drawing Text

The controller chip in the LCD does not have direct support for displaying text, you have to send the characters as bitmaps. I have included static data for a simple 5 x 7 pixel font in the library that is laid out in vertical strips so it is a quick process to render individual characters.

Single Character

The font draws in a 5 x 7 pixel area but fills a 6 x 8 pixel square to provide spacing between characters both horizontally and vertically. At this resolution you can display up to 14 characters on each of the 6 lines of the display. I've provided functions for displaying single characters as well as NUL terminated strings from RAM or PROGMEM.

Characters cannot span rows but they can be positioned horizontally at any pixel location. Be aware that all pixels in the character cell will be modified when it is drawn, you cannot overlay graphics on an image without part of the image being erased.

Drawing Images

To draw images efficiently it helps if they are stored in the same layout as the graphics memory in the module - that is, a sequence of 8 pixel vertical strips that can be sent to the display. I've added a function that will display images directly from PROGMEM. It expects a descriptor byte as the first data value which contains the width (in pixels) and height (in rows) of the image data that follows. This is packed into a single byte as HHWWWWWW where HH is one less than the height of the image (in 8 pixel rows) and WWWWWW is one less the width of the image in pixels. The bytes following the descriptor are vertical strips of 8 pixels each, WWWWWW + 1 for each row.

As with the character drawing functions the top of the image must be at the start of a row but it can be placed at any pixel position horizontally. If the image extends past the borders of the display it will simply be clipped. Once again, the routine will modify all pixels in the rectangle covered by the image erasing what was previously on the screen in that area.

Image Conversion

Because manually generating the byte array for an image would be a time consuming (not to mention annoying) process I wrote a simple Python script to convert arbitrary images into array definitions that can be embedded in your code. The script is called lcdimage.py and I've added it to the tools directory in the GitHub repository - simply run the script with a list of filenames and it will generate C code that you can paste into your project.

It uses the Python Imaging Library to read a wide range of image formats, for best results use a monochrome image or a PNG with transparency.

What Next?

These displays are cheap and easy to use - a good option to add a simple UI to your project. The downside is the number of pins they use - 4 out of the 6 available on the ATtiny. It might be possible to control the RESET pin on the module with external circuitry rather than though an IO pin which would save another pin. The RESET line is not used after initialisation so you could reuse it as an analog input as long as the voltage being sampled doesn't fall below 0.7V - in one circuit I'm designing I'm using it to monitor a 6V battery pack through a voltage divider, in this case the device will power off before the voltage drops below 1.6V so it will not trigger a reset of the display.

You could use the single pin keypad I described earlier to provide simple menu navigation - the one pin serial interface could then be used to send menu selection data to a host machine (such as a Raspberry Pi without a display) to trigger some action.

Another idea is a simple Bluetooth enabled temperature sensor with display - the ATtiny has a built in temperature sensor so no external components are required for that, the battery could be monitored through the RESET line and the remaining two pins could be used to implement a serial interface to a HC05 Bluetooth module.

Overall it's another useful component that can be added to your toolbox. The code is now available in the template library, I'm looking forward to seeing what people come up with.


Follow me on Google+ to see build images, construction notes and previews of upcoming projects.

Recommended Sites

  • EEWeb

    Electrical Engineering News, Resources, and Community.

  • Sprites Mods

    A collection of projects from a very talented engineer.

  • Blondihacks

    A great collection of hacks and projects by Quinn Dunki.

  • Embedded Projects

    Embedded Projects from Around the Web.

Donate Doge

Like the site? Be kind and send a few Dogecoin my way.
DAqGZFkVj9meRgHMve9W5KQshdvZ3UCtGF