Category Archives: Electronics

Arduino and LCD2004 and PCF8574 and I2C – part 1

See also: part 1.

NOTE: This article was originally written two years ago, and meant to be part of a series. I never got around to writing Part 2, so I am just publishing this initial part by itself. If there is interest, I will continue the series. My Github actually shows the rest of the work I did for my “full” and “small” version of the drive code for this LCD.

Recently, my day job presented me an opportunity to play with a small 20×4 LCD display that hooked up via I2C. The module was an LCD2004. The 20 is the number of columns and the 04 is the number of rows. The LCD1602 would be a 16×2 display.

While I have found many “tutorials” about these displays, virtually all of them just teach you how to download a premade library and use library functions. Since I was going to be implementing code for an in-house project, and did not have room for a full library of functions I would not be using, I really needed to know how the device worked. Hopefully this article may help others who need (or just want) to do what I did.

LCD2004 / LCD1602 / etc.

These LCD modules use a parallel interface and require eleven I/O pins. The pinout on the LCD looks like this:

1 - VSS (Ground)
2 - VDD (5V)
3 - V0 (LCD Contrast)
4 - RS (Register Select: LOW=Instruction, HIGH=Data)
5 - RW (Read/Write: LOW=Write, HIGH=Read)
6 - E (Enable)
7 - D0 (Data lines, 8-bit)
8 - D1
9 - D2
10 - D3
11 - D4
12 - D5
13 - D6
14 - D7
15 - A/LED+ (Backlight Power)
16 - K/LED- (Backlight Ground)

A few of the pins are listed by different names based on whoever created the data sheet or hardware. On my LCD2004 module, pins 15 and 16 are listed as A and K, but I now know they are just power lines for the backlight.

If you have something like an Arduino with enough available I/O pins, you can wire the display up directly to pins. You should be able to hook up power (5V to VDD, Ground to VSS, and probably some power to the backlight and maybe something to control contrast), and then connect the eight data lines (D0-D7) to eight available digital I/O pins on the Arduino.

The LCD module has a simple set of instruction bytes. You set the I/O pins (HIGH and LOW, each to represent a bit in a byte), along with the RS (register select) and RW (read/write) pins, then you toggle the E (Enable) pin HIGH to tell the LCD it can read the I/O pins. After a moment, you toggle E back to LOW.

The data sheets give timing requirements for various instructions. If I read it correctly, it looks like the E pin needs to be active for a minimum of 150 nanoseconds for the LCD to read the pins.

Here is a very cool YouTube video by Ian Ward that shows how the LCD works without using a CPU. He uses just buttons and dip switches. I found it quite helpful in understanding how to read and write to the LCD.

If you don’t have 11 I/O pins, you need a different solution.

Ian Ward’s excellent LCD2004 video.

A few pins short of a strike…

If you do not have eleven I/O pins available, the LCD can operate in a 4-bit mode, needing only four pins for data. You send the upper four bits of a byte using the E toggle, followed by the lower 4-bits of the byte. This is obviously twice as slow, but allows the part to be used when I/O pins are limited.

If you don’t have 7 I/O pins, you need a different solution.

PCF8574: I2C to I/O

If you do not have seven I/O pins available, you can use the PCF8574 chip. This chip acts as an I2C to I/O pin interface. You write a byte to the chip and it will toggle the eight I/O pins based on the bits in the byte. Send a zero, and all pins are set LOW. Send a 255 (0xff) and all pins are set HIGH.

Using a chip like this, you can now use the 2-wire I2C interface to communicate with the LCD module–provided it is wired up and configured to operate in 4-bit mode (four pins for data, three pins for RS, RW and E, and the spare pin can be used to toggle the backlight on and off).

Low-cost LCD controller boards are made that contain this chip and have pins for hooking up to I2C, and other pins for plugging directly to the LCD module. For just a few dollars you can buy an LCD module already soldered on to the PCF8574 board and just hook it up to 5V, Ground, I2C Data and I2C Clock and start talking to it.

If you know how.

I did not know how, so I thought I’d document what I have learned so far.

What I have learned so far.

The PCF8574 modules I have all seem to be wired the same. There is a row of 16-pins that aligns with the 16 pins of the LCD module.

PCF8574 module.

One LCD I have just had the board soldered directly on to the LCD.

LCD2004 with the PCD8574 module soldered on.

Another kit came with separate boards and modules, requiring me to do the soldering since the LCD did not have a header attached.

PCF8574 module and LCD1602, soldering required.

If you are going to experiment with these, just get one that’s already soldered together or make sure the LCD has a header that the board can plug in to. At least if you are like me. My soldering skills are … not optimal.

The eight I/O pins of the PCF modules I have are connected to the LCD pins as follows:

1 - to RS
2 - to RW
3 - to E
4 - to Backlight On/Off
5 - D4
6 - D5
7 - D6
8 - D7

If I were to send an I2C byte to this module with a value of 8 (that would be bit 3 set, with bits numbers 0 to 7), that would toggle the LCD backlight on. Sending a 0 would turn it off.

That was the first thing I was able to do. Here is an Arduino sketch that will toggle that pin on and off, making the backlight blink:

// PCF8574 connected to LCD2004/LCD1602/etc.
#include <Wire.h>

void setup() {
  // put your setup code here, to run once:
  Wire.begin ();
}

void loop() {
  // put your main code here, to run repeatedly:
  Wire.beginTransmission (39); // I2C address
  Wire.write (8); // Backlight on
  Wire.endTransmission ();

  delay (500);

  Wire.beginTransmission (39); // I2C address
  Wire.write (0); // Backlight off
  Wire.endTransmission ();

  delay (500);
}

Once I understood which bit went to which LCD pin, I could then start figuring out how to talk to the LCD.

One of the first things I did was create some #defines representing each bit:

#define BIT(b)      (1<<(b))

#define DB7_BIT     BIT(7) // High nibble for 4-bit mode. 
#define DB6_BIT     BIT(6) //
#define DB5_BIT     BIT(5) //
#define DB4_BIT     BIT(4) //
#define BL_BIT      BIT(3) // Backlight (0=Off, 1=On)
#define E_BIT       BIT(2) // Enable (0=Disable, 1=Enable)
#define RW_BIT      BIT(1) // Read/Write (0=Write, 1=Read)
#define RS_BIT      BIT(0) // Register Select (0=Instruction, 1=Data)

We’ll use this later when building our own bytes to send out.

Here is a datasheet for the LCD2004 module. Communicating with an LCD1602 is identical except for how many lines you have and where they exist in screen memory:

https://cdn-shop.adafruit.com/datasheets/TC2004A-01.pdf

I actually started with an LCD1602 datasheet and had it all working before I understood what “1602” meant a different sized display than whatI had ;-)

Sending a byte

As you can see from the above sample code, to send an I2C byte on the Arduino, you have to include the Wire library (for I2C) and initialize it in Setup:

#include <Wire.h>

void setup() {
  // put your setup code here, to run once:
  Wire.begin ();
}

Then you use a few lines of code to write the byte out to the I2C address of the PCF8574 module. The address is 39 by default, but there are solder pads on these boards that let you change it to a few other addresses.

Wire.beginTransmission (39); // I2C address
Wire.write (8); // Backlight on
Wire.endTransmission ();

Communicating with the LCD module requires a few more steps. First, you have to figure out which pins you want set on the LCD, then you write out a byte that represents them. The “E” pin must be set (1) to tell the LCD to look at the data pins.

After a tiny pause, you write out the value again but with the E pin bit unset (0).

That’s all there is to it! The rest is just understanding what pins you need to set for what command.

Instructions versus Data

The LCD module uses a Register Select pin (RS) to tell it if the 8-bits of I/O represents an Instruction, or Data.

  • Instruction – If you set the 8 I/O pins and have RS off (0) then toggle the Enable pin on and off, the LCD receives those 8 I/O pins as an Instruction.
  • Data – If you set the 8 I/O pins and have RS on (1) then toggle the Enable pin on and off, the LCD received those 8 I/O pins as a Data byte.

Reading and Writing

In addition to sending Instructions or Data to the LCD, you can also read Data back. This tutorial will not cover that, but it’s basically the same process except you set the Read/Write pin to 1 and then pulse the E pin high/low and then you can read the pins that will be set by the LCD.

Initialize the LCD to 4-bit mode

Since only 4 of the PCF8574 I/O pins are used for data, the first thing that must be done is to initialize the LCD module to 4-bit mode. This is done by using the Function Set instruction.

Function set is described as the following:

RS  RW  DB7 DB6 DB5 DB4 DB3 DB2 DB1 DB0
--- --- --- --- --- --- --- --- --- ---
0 0 0 0 1 DL N F x x

Above, RS is the Register Select pin, RW is the Read/Write pin, and DB7-DB0 are the eight I/O pins. For Function Set, pins DB7-DB5 are “001” representing the Function Select instruction. After that, the pins are used for settings of Function Select:

  • DB4 is Data Length select bit. (DL)
  • DB3 is Number of Lines select bit
  • DB2 is Font select bit

When we are using the PCF8574 module, it ONLY gives us access to DB7-DB4, so it is very smart that they chose to make the DL setting one of those four bits. We have no way to access the pins for N or F until we toggle the LCD in to 4-bit data length mode.

If we were using all 8 I/O pins, we’d set them like this to go in to 4-bit mode:

RS  RW  DB7 DB6 DB5 DB4 DB3 DB2 DB1 DB0 E
--- --- --- --- --- --- --- --- --- --- ---
0   0   0   0   1   1   0   0   0   0   1   <- Enable pin ON
(pause)
0   0   0   0   1   1   0   0   0   0   0   <- Enable pin OFF

…EXCEPT, the LCD won’t listen to us until we initialize it by sending a series of Instructions first. Here is what the LCD expects to wake it up:

RS  RW  DB7 DB6 DB5 DB4 DB3 DB2 DB1 DB0 E
--- --- --- --- --- --- --- --- --- --- ---
0   0   0   0   1   1   0   0   0   0   1   <- Enable pin ON
(pause)
0   0   0   0   1   1   0   0   0   0   0   <- Enable pin OFF

(wait at least 4.1 ms)

0   0   0   0   1   1   0   0   0   0   1   <- Enable pin ON
(pause)
0   0   0   0   1   1   0   0   0   0   0   <- Enable pin OFF

(wait at least 100 us)

0   0   0   0   1   1   0   0   0   0   1   <- Enable pin ON
(pause)
0   0   0   0   1   1   0   0   0   0   0   <- Enable pin OFF

That sequence will initialize the LCD so we can send it commands. After that, we can use Function Set to change it to 4-bit mode (DB4 as 0 for 4-bit mode):

RS  RW  DB7 DB6 DB5 DB4 DB3 DB2 DB1 DB0 E
--- --- --- --- --- --- --- --- --- --- ---
0   0   0   0   1   0   0   0   0   0   1   <- Enable pin ON
(pause)
0   0   0   0   1   0   0   0   0   0   0   <- Enable pin OFF

If we used all 8 I/O pins directly, we could also set Font and Number of lines at the same time after the three initializing writes. BUT, since we are using the PCD8547 and only have access to the top four bits (DB7-DB4), we must put the LCD in to 4-bit mode first. More details on how we use that in a moment.

If I wanted to initialize the LCD, I would just need to translate the I/O pins into the bits of a PCF8574 byte. For the first three initialization writes, it would look like this:

LCD pins:                                    PCD8574 pins:

RS  RW  DB7 DB6 DB5 DB4 DB3 DB2 DB1 DB0 E      DB7 DB6 DB5 DB4 BL E  RW RS
--- --- --- --- --- --- --- --- --- --- --- =  --- --- --- --- -- -- -- --
0   0   0   0   1   1   0   0   0   0   1      0   0   1   1   0  1  0  0
(pause)
0   0   0   0   1   1   0   0   0   0   0      0   0   1   1   0  0  0  0

And that would turn in these two I2C writes:

Wire.beginTransmission(PCF8574_ADDRESS);

Wire.write(0b00110100); // DB7 DB6 DB5 DB4 BL E RW RS
delayMicroseconds(1);
Wire.write(0b00110000); // DB7 DB6 DB5 DB4 BL E RW RS
delayMicroseconds(37);
Wire.endTransimssion();

You could manually do the intiializtion writes like that, with the required sleeps between each one.

But, instead, I wrote a function that will send a 4-bit value out to the upper four I/O pins (DB7-DB4):

// [7  6  5  4  3  2  1  0 ]
// [D7 D6 D5 D4 BL -E RW RS]
void LCDWriteInstructionNibble(uint8_t nibble)
{
    uint8_t dataByte = BL_BIT | (nibble << 4);

    Wire.beginTransmission(PCF8574_ADDRESS);

    Wire.write(E_BIT | dataByte);
    delayMicroseconds(1);

    Wire.write(dataByte);
    delayMicroseconds(37);

    Wire.endTransmission();
}

ABove, you see only need to pass in the bit pattern for DB7 DB6 DB5 DB4. This routine will set the Backlight Bit (it doesn’t have to, but I didn’t want the screen to blank out when sending these instructions), and then write the byte out with the E pin set, pause, then write it out again with E off.

Thus, my initialization can now look like this:

// Initialize all pins off and give it time to settle.
Wire.beginTransmission(PCF8574_ADDRESS);
Wire.write(0x0);
Wire.endTransmission();

delayMicroseconds(50000);

// [7  6  5  4  3  2  1  0 ]
// [D7 D6 D5 D4 BL -E RW RS]
LCDWriteInstructionNibble(0b0011);
delay(5); // min 4.1 ms

LCDWriteInstructionNibble(0b0011);
delayMicroseconds(110); // min 100 us

LCDWriteInstructionNibble(0b0011);
delayMicroseconds(110); // min 100 us

// Set interface to 4-bit mode.
LCDWriteInstructionNibble(0b0010);

That looks much more obvious, and reduces the amount of lines we need to look at since the function will do the two writes (E on, E off) for us.

Sending 8-bits in a 4-bit world

Now that the LCD is in 4-bit mode, it will expect those four I/O pins set twice — the first time for the upper 4-bits of a byte, and then the second time for the lower 4-bits. We could, of course, do this manually as well by figuring all this out and building the raw bytes outselves.

But that makes my head hurt and is too much work.

Instead, I created a second function that will send an 8-bit value 4-bits at a time:

void LCDWriteByte(uint8_t rsBit, uint8_t dataByte)
{
    uint8_t newByte;

    Wire.beginTransmission(PCF8574_ADDRESS);

    newByte = BL_BIT | rsBit | (dataByte & 0xf0);
    Wire.write(E_BIT | newByte);
    delayMicroseconds(1);
    Wire.write(newByte);
    delayMicroseconds(37);

    newByte = BL_BIT | rsBit | (dataByte << 4);
    Wire.write(E_BIT | newByte);
    delayMicroseconds(1);
    Wire.write(newByte);
    delayMicroseconds(37);

    Wire.endTransmission();
}

You’ll notice I pass in the Register Select bit, which can either be 0 (for an Instruction) or 1 (for data). That’s jumping ahead a bit, but it makes sense later.

I can then pass in a full instruction, like sending Function set to include the bits I couldn’t set during initialization when the LCD was in 8-bit mode and I didn’t have access to DB3-DB0. My LCDInit() routine set the LCD to 4-bit mode, and then uses this to send out the rest of the initialization:

// Function Set
// [0  0  1  DL N  F  0  0 ]
// DL: 1=8-Bit, 0=4-Bit
//  N: 1=2 Line, 0=1 Line
//  F: 1=5x10, 0=5x8
//             [--001DNF00]
LCDWriteByte(0, 0b00101000); // RS=0, Function Set

// Display On
// [0  0  0  0  1  D  C  B ]
// D: Display
// C: Cursor
// B: Blink
//             [--00001DCB]
LCDWriteByte(0, 0b00001100); // RS=0, Display On

// Display Clear
// [0  0  0  0  0  0  0  1 ]
LCDWriteByte(0, 0b00000001);
delayMicroseconds(3);  // 1.18ms - 2.16ms

// Entry Mode Set
// [0  0  0  0  0  1  ID S ]
// ID: 1=Increment, 0=Decrement
//  S: 1=Shift based on ID (1=Left, 0=Right)
//             [--000001IS]
LCDWriteByte(0, 0b00000110);

To make things even more clear, I then created a wrapper function for writing an Instruction that has RS at 0, and another for writing Data that has RS at 1:

void LCDWriteInstructionByte(uint8_t instruction)
{
    LCDWriteByte(0, instruction);
}


/*--------------------------------------------------------------------------*/
// Write with RS bit 1.
void LCDWriteDataByte(uint8_t data)
{
    LCDWriteByte(RS_BIT, data);
}

That lets the code look a bit cleaner, and means the user doesn’t have to know about using RS_BIT or 0 … just write Instruction or Data like this:

// Function Set
// [0 0 1 DL N F 0 0 ]
// DL: 1=8-Bit, 0=4-Bit
// N: 1=2 Line, 0=1 Line
// F: 1=5x10, 0=5x8
// [--001DNF00]
LCDWriteInstructionByte(0b00101000);

// Display On
// [0 0 0 0 1 D C B ]
// D: Display
// C: Cursor
// B: Blink
// [--00001DCB]
LCDWriteInstructionByte(0b00001100);

// Display Clear
// [--00000001]
LCDWriteInstructionByte(0b00000001);
delayMicroseconds(3); // 1.18ms - 2.16ms

// Entry Mode Set
// [0 0 0 0 0 1 ID S ]
// ID: 1=Increment, 0=Decrement
// S: 1=Shift based on ID (1=Left, 0=Right)
// [--000001IS]
LCDWriteInstructionByte(0b00000110);

The Display Clear instruction is 00000001. There are no other bits that need to be set, so I can clear the screen by doing “LCDWriteInstructionByte (0b00000001)” or simply “LCDWriteInstructionByte(1)”;

Ultimately, I’d probably create #defines for the different instructions, and the settable bits inside of them, allowing me to build a byte like this:

uint8_t functionSetByte = FUNCTION_SET | DL_BIT | N_BIT | F_BIT;

FUNCTION_SET would represent the bit pattern 0b00100000, and the DL_BIT would be BIT(4), N_BIT would be BIT(3) and F_BIT would be BIT(2). Fleshing out all of those defines and then making wrapper functions would be trivial.

But in my case, I only needed a few, so if you wanted to make something that did that, you could:

void LCDFunctionSet (uint8_t dlBit, uint8_t nBit, uint8_t fBit)
{
   uint8_t instruction = FUNCTION_SET;
   if (dlBit)
   {
      instruction |= DL_BIT;
   }

   if (nBit)
   {
      instruction |= N_BIT;
   }

   if (fBit)
   {
      instruction |= F_BIT;
   }

   LCDWriteInstructionByte (instruction);
}

This type of thing can allow your code to spiral out of control as you create functions to set bits in things like “Display On/Off Control” and then write wrapper functions like “LCDDisplayON()”, “LCDBlinkOn()” and so on.

But we won’t be going there. I’m just showing you the basic framework.

Now what?

With the basic steps to Initialize to 4-Bit Mode, then send out commands, the rest is pretty simple. If you want to write out bytes to be displayed on the screen, you just write out a byte with the Register Select bit set (for Data, instead of Instruction). The byte appears at whatever location the LCD has for the cursor position. Simple!

At the very least, you need a Clear Screen function:

void LCDClear(void)
{
    // Display Clear
    // [0  0  0  0  0  0  0  1 ]
    LCDWriteInstructionByte(0b00000001);
    delay(3); // 1.18ms - 2.16ms
}

…and a function to write out data bytes, such as raw data (point to the data, give it the size):

void LCDWriteData(uint8_t *dataPtr, uint8_t dataSize)
{
    for (int idx = 0; idx < dataSize; idx++)
    {
        LCDWriteDataByte(dataPtr[idx]);
    }
}

…and maybe a nicer function that deals with C strings:

void LCDWriteDataString(char *message)
{
    LCDWriteData((uint8_t *)message, strlen(message));
}

Wrap, wrap, wrap we go…

The last thing I implemented was a thing that sets the X/Y position of where text will go. This is tricky because the display doesn’t match the memory inside the screen. Internally my LCD2004 just has a buffer of screen memory that maps to the LCD somehow.

The LCD data is not organized as multiple lines of 20 characters (or 16). Instead, it is just a buffer of screen memory that is mapped to the display. In the case of the LCD2004, the screen is basically 128 bytes of memory, with the FIRST line being bytes 0-19, the SECOND line being bytes 64-83, the THIRD line being bytes 20-39, and the FOURTH line being bytes 84-103.

Consider this example:

+--------------------+
|ABCDEFGHIJKLMNOPQRST| (bytes 0-19)
|abcdefghijklmnopqrst| (bytes 64-83)
|UVWXYZ              | (bytes 20-39)
|uvwxyz              | (bytes 84-103)
+--------------------+

If you were to start at memory offset 0 (top left of the display) and write 80 bytes of data (thinking you’d get 20, 20, 20 and 20 bytes on the display), that wouldn’t happen ;-) You’d see some of your data did not show up since it was writing out in the memory that is not mapped in to the display. (You can also use that memory for data storage, but I did not implement any READ routines in this code — yet.)

If you actually did start at offset 0 (the first byte of screen memory) and wrote a series of characters from 32 (space) to 127 (whatever that is), it would look like this:

Above, you can see the first line continues on line #3, and then after the end of line 3 (…EFG” we don’t see any characters until we get to the apostrophe which displays on line 2. Behind the scenes, memory looks like this:

0  [ !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_] 63
64 [`abcdefghijklmnopqrstuvwxyz{|}--                                ] 127

That is the “2 line” mode of the LCD. But, the physical display treats that data like this:

0  [ !"#$%&'()*+,-./0123] 19
64 [`abcdefghijklmnopqrs] 83
20 [456789:;<=>?@ABCDEFG] 39 
84 [tuvwxyz{|}--        ] 103

not visible:

40 [HIJKLMNOPQRSTUVWXYZ[\]^_] 63
104[                        ] 127

Clear as mud? Good.

All you need to know is that the visible screen doesn’t match LCD memory, so when creating a “set cursor position” that translates X and Y to an offset of memory, it has to have a lookup table, like this one:

void LCDSetOffset(uint8_t offset)
{
    // DDRAM AD SET
    LCDWriteInstructionByte(0b10000000 | offset);
    delayMicroseconds(40);
}

void LCDSetXY(uint8_t x, uint8_t y)
{
    uint8_t offset = 0;

    if (y == 1)
    {
        offset = 64; // 0x40
    }
    else if (y == 2)
    {
        offset = 20; // 0x14
    }
    else if (y == 3)
    {
        offset = 84; // 0x54
    }
    else
    {
        // Offset will be 0.
    }

    offset = offset + x;

    LCDSetOffset(offset);
}

You will see I created a function that sends the “Set Offset” instruction (memory location 0 to 127, I think) and then a “Set X/Y” function that translates columns and rows to an offset.

With all that said, here are the routines I cam up with. Check my GitHub for the latest versions:

https://github.com/allenhuffman/PCF8547_LCD2004

The LCDTest.ino program also demonstrates how you can easily send an Instruction to load character data, and then send that data using the LCDWriteData functions.

I plan to revisit this with more details on how all that works, but wanted to share what I had so far.

Until next time…


LCD2004_PCF8574.h

#ifndef LCD2004_PCF8547_H
#define LCD2004_PCF8547_H

#include <stdint.h>

#define PCF8574_ADDRESS   0x27 // 39 (7-bit address)
//#define PCF8574_ADDRESS   (0x27*2) // 39 (8-bit address)

#if !defined(BIT)
#define BIT(b)      (1<<(b))
#endif

// PCF8574 8-bit pins map to the following LCD2004 pins:
//
// [7  6  5  4  3  2  1  0 ]
// |D7 D6 D5 D4 BL -E RW RS
//
// NOTE: This is hard-coded to assume the upper four bits are
// the 4 data lines to the LCD, in that order.

typedef enum
{
    DB7_BIT = BIT(7), // High nibble for 4-bit mode. 
    DB6_BIT = BIT(6), //
    DB5_BIT = BIT(5), //
    DB4_BIT = BIT(4), //
    BL_BIT  = BIT(3), // Backlight (0=Off, 1=On)
    E_BIT   = BIT(2), // Enable (0=Disable, 1=Enable)
    RW_BIT  = BIT(1), // Read/Write (0=Write, 1=Read)
    RS_BIT  = BIT(0)  // Register Select (0=Instruction, 1=Data)
} LCDBitEnum;

// #define DB7_BIT     BIT(7) // High nibble for 4-bit mode. 
// #define DB6_BIT     BIT(6) //
// #define DB5_BIT     BIT(5) //
// #define DB4_BIT     BIT(4) //
// #define BL_BIT      BIT(3) // Backlight (0=Off, 1=On)
// #define E_BIT       BIT(2) // Enable (0=Disable, 1=Enable)
// #define RW_BIT      BIT(1) // Read/Write (0=Write, 1=Read)
// #define RS_BIT      BIT(0) // Register Select (0=Instruction, 1=Data)

// Function Prototypes
bool IsLCDEnabled (void);

bool LCDInit (void);
void LCDTerm (void);

void LCDWriteInstructionNibble (uint8_t nibble);

void LCDWriteByte (uint8_t rsBit, uint8_t value);
void LCDWriteInstructionByte (uint8_t instruction);
void LCDWriteDataByte (uint8_t data);

void LCDWriteData (uint8_t *dataPtr, uint8_t dataSize);
void LCDWriteDataString (uint8_t x, uint8_t y, char *message);

void LCDSetOffset (uint8_t offset);
void LCDSetXY (uint8_t x, uint8_t y);

void LCDClear (void);

void LCDWaitForBusyFlag (void);

#endif   /* LCD2004_PCF8547_H */

// End of LCD2004_PCF8547.h

LCD2004_PCF8547.ino

#ifndef LCD2004_PCF8547_C
#define LCD2004_PCF8547_C

/*--------------------------------------------------------------------------*/
// Include Files
/*--------------------------------------------------------------------------*/
#include <stdint.h>

#include <Wire.h>

#include "LCD2004_PCF8547.h"


/*--------------------------------------------------------------------------*/
// Variables
/*--------------------------------------------------------------------------*/
static bool S_IsLCDEnabled = false;


/*--------------------------------------------------------------------------*/
// Functions
/*--------------------------------------------------------------------------*/
bool IsLCDEnabled(void)
{
    return S_IsLCDEnabled;
}


/*--------------------------------------------------------------------------*/
// Initialize the LCD2004.
/*--------------------------------------------------------------------------*/
//    4-Bit data mode
//    2 Line display
//    Display On
//    Display Clear
//    Left-to-Right display mode.
bool LCDInit(void)
{
    int ack = 0;

    Wire.begin();

    // Set all PCF8547 I/O pins LOW.
    Wire.beginTransmission(PCF8574_ADDRESS);
    Wire.write(0x0);
    ack = Wire.endTransmission();

    if (ack != 0)
    {
        return false;
    }
    delayMicroseconds(50000);

    // [7  6  5  4  3  2  1  0 ]
    // [D7 D6 D5 D4 BL -E RW RS]
    LCDWriteInstructionNibble(0b0011);
    delay(5); // min 4.1 ms

    LCDWriteInstructionNibble(0b0011);
    delayMicroseconds(110); // min 100 us

    LCDWriteInstructionNibble(0b0011);
    delayMicroseconds(110); // min 100 us

    // Set interface to 4-bit mode.
    LCDWriteInstructionNibble(0b0010);

    // Function Set
    // [0  0  1  DL N  F  0  0 ]
    // DL: 1=8-Bit, 0=4-Bit
    //  N: 1=2 Line, 0=1 Line
    //  F: 1=5x10, 0=5x8
    //                      [--001DNF00]
    LCDWriteInstructionByte(0b00101000);

    // Display On
    // [0  0  0  0  1  D  C  B ]
    // D: Display
    // C: Cursor
    // B: Blink
    //                      [--00001DCB]
    LCDWriteInstructionByte(0b00001100);

    // Display Clear
    // [0  0  0  0  0  0  0  1 ]
    LCDWriteInstructionByte(0b00000001);
    delayMicroseconds(3);  // 1.18ms - 2.16ms

    // Entry Mode Set
    // [0  0  0  0  0  1  ID S ]
    // ID: 1=Increment, 0=Decrement
    //  S: 1=Shift based on ID (1=Left, 0=Right)
    //                      [--000001IS]
    LCDWriteInstructionByte(0b00000110);

    S_IsLCDEnabled = true;

    return true;
}


/*--------------------------------------------------------------------------*/
// Disable LCD screen.
/*--------------------------------------------------------------------------*/
void LCDTerm(void)
{
    S_IsLCDEnabled = false;
}


/*--------------------------------------------------------------------------*/
// Write out a 4-bit value.
/*--------------------------------------------------------------------------*/
// [7  6  5  4  3  2  1  0 ]
// [D7 D6 D5 D4 BL -E RW RS]
void LCDWriteInstructionNibble(uint8_t nibble)
{
    uint8_t dataByte = BL_BIT | (nibble << 4);

    Wire.beginTransmission(PCF8574_ADDRESS);

    Wire.write(E_BIT | dataByte);
    delayMicroseconds(1);

    Wire.write(dataByte);
    delayMicroseconds(37);

    Wire.endTransmission();
}


/*--------------------------------------------------------------------------*/
// Write a byte out, 4-bits at a time. The rsBit will determine
// if it is an Instruction (rsBit=0) or Data byte (rsBit=1).
/*--------------------------------------------------------------------------*/
void LCDWriteByte(uint8_t rsBit, uint8_t dataByte)
{
    uint8_t newByte;

    Wire.beginTransmission(PCF8574_ADDRESS);

    newByte = BL_BIT | rsBit | (dataByte & 0xf0);
    Wire.write(E_BIT | newByte);
    delayMicroseconds(1);
    Wire.write(newByte);
    delayMicroseconds(37);

    newByte = BL_BIT | rsBit | (dataByte << 4);
    Wire.write(E_BIT | newByte);
    delayMicroseconds(1);
    Wire.write(newByte);
    delayMicroseconds(37);

    Wire.endTransmission();
}


/*--------------------------------------------------------------------------*/
// Write with RS bit 0.
/*--------------------------------------------------------------------------*/
void LCDWriteInstructionByte(uint8_t instruction)
{
    LCDWriteByte(0, instruction);
}


/*--------------------------------------------------------------------------*/
// Write with RS bit 1.
/*--------------------------------------------------------------------------*/
void LCDWriteDataByte(uint8_t data)
{
    LCDWriteByte(RS_BIT, data);
}


/*--------------------------------------------------------------------------*/
// Write out one or more data bytes.
/*--------------------------------------------------------------------------*/
void LCDWriteData(uint8_t *dataPtr, uint8_t dataSize)
{
    for (int idx = 0; idx < dataSize; idx++)
    {
        LCDWriteDataByte(dataPtr[idx]);
    }
}


/*--------------------------------------------------------------------------*/
// Write out a NIL terminated C string.
/*--------------------------------------------------------------------------*/
void LCDWriteDataString(uint8_t x, uint8_t y, char *message)
{
    if (S_IsLCDEnabled == true)
    {
        LCDSetXY(x, y);
        LCDWriteData((uint8_t *)message, strlen(message));
    }
}


/*--------------------------------------------------------------------------*/
// Set LCD offset in screen memory.
/*--------------------------------------------------------------------------*/
void LCDSetOffset(uint8_t offset)
{
    // DDRAM AD SET
    LCDWriteInstructionByte(0b10000000 | offset);
    delayMicroseconds(40);
}


/*--------------------------------------------------------------------------*/
// Set LCD cursor position.
/*--------------------------------------------------------------------------*/
// LCD2004 is internally treated like a two line display of 64 characters
// each. The first internal line is bytes 0-63 and the second internal
// line is bytes 64-127.
//
// For the physical LCD2004 20x4 four-line display, the first 20 bytes of
// internal line 1 (0-19) is the display line 1. The second 20 bytes of
// internal line 1 (20-39) is the display line 3. The first 20 bytes
// of internal line 2 (64-83) is display line 2. The second 20 bytes
// of internal line 2 (84-103) is display line 4.
//
// Super easy and not confusing at all.
//
//                          +--------------------+
// Internal Line 1 (0-19)   |aaaaaaaaaaaaaaaaaaaa| Display line 1
// Internal Line 2 (64-83)  |bbbbbbbbbbbbbbbbbbbb| Display line 2
// Internal Line 1 (20-39)  |aaaaaaaaaaaaaaaaaaaa| Display line 3
// Internal Line 2 (84-103) |bbbbbbbbbbbbbbbbbbbb| Display line 4
//                          +--------------------+
//
// Because of this, we will use a simple translation to get between
// column (x) and row (y) to the actual offset of these two internal
// 64-byte lines.
//
void LCDSetXY(uint8_t x, uint8_t y)
{
    uint8_t offset = 0;

    if (y == 1)
    {
        offset = 64; // 0x40
    }
    else if (y == 2)
    {
        offset = 20; // 0x14
    }
    else if (y == 3)
    {
        offset = 84; // 0x54
    }
    else
    {
        // Offset will be 0.
    }

    offset = offset + x;

    LCDSetOffset(offset);
}


/*--------------------------------------------------------------------------*/
// Clear the LCD (and home the cursor position).
/*--------------------------------------------------------------------------*/
void LCDClear(void)
{
    if (S_IsLCDEnabled == true)
    {
        // Display Clear
        // [0  0  0  0  0  0  0  1 ]
        LCDWriteInstructionByte(0b00000001);
        delay(3); // 1.18ms - 2.16ms
    }
}


// NOT WORKING YET! I was experimenting with doing a READ operation, and
// seeing if I could poll the BF (busy flag) bit.
#if 0
void LCDWaitForBusyFlag (void)
{
  while (1)
  {
    // Toggle on READ mode.
    Wire.beginTransmission (PCF8574_ADDRESS);
    Wire.write (E_BIT | RW_BIT | DB7_BIT);
    Wire.endTransmission ();

    // Read I/O pins.
    Wire.requestFrom (PCF8574_ADDRESS, 1);
    if ((Wire.read() & DB7_BIT) == 0)
    {
      break;
    }

    // Else toggle off E, to start another ready.
    Wire.beginTransmission (PCF8574_ADDRESS);
    Wire.write (RW_BIT | DB7_BIT);
    Wire.endTransmission ();
  }
}
#endif // 0

#endif /* LCD2004_PCF8547_C */

// End of LCD2004_PCF8547.c

LCDTest.ino

// LCDTest.ino

// TI PCF8574 (or compatible)

#include <stdint.h>

#include <Wire.h>

#include "LCD2004_PCF8547.h"

#define LCD_WIDTH   20
#define LCD_HEIGHT  4

uint8_t data[] =
{
  // Pac Close
  0b01110,
  0b11111,
  0b11111,
  0b11111,
  0b11111,
  0b11111,
  0b11111,
  0b01110,

  // Pack Right
  0b01110,
  0b11111,
  0b11110,
  0b11100,
  0b11100,
  0b11110,
  0b11111,
  0b01110,

  // Ghost 1
  0b01110,
  0b11111,
  0b11111,
  0b10101,
  0b11111,
  0b11111,
  0b11111,
  0b10101,

  // Ghost 2
  0b01110,
  0b11111,
  0b11111,
  0b10101,
  0b11111,
  0b11111,
  0b11111,
  0b01010,

  // Dot
  0b00000,
  0b00000,
  0b00000,
  0b01100,
  0b01100,
  0b00000,
  0b00000,
  0b00000,

  // Maze Left
  0b00000,
  0b01111,
  0b10000,
  0b10000,
  0b10000,
  0b10000,
  0b01111,
  0b00000,

  // Maze Center
  0b00000,
  0b11111,
  0b00000,
  0b00000,
  0b00000,
  0b00000,
  0b11111,
  0b00000,

  // Maze Right
  0b00000,
  0b11110,
  0b00001,
  0b00001,
  0b00001,
  0b00001,
  0b11110,
  0b00000,
};

void setup()
{
  Serial.begin(9600);
  Wire.begin(); // I2C Master

  Serial.print ("sizeof(data) = ");
  Serial.println (sizeof(data));

  LCDInit ();

  delay(500);

  // Set CGRAM Address
  uint8_t offset = 0;
  LCDWriteInstructionByte (0b01000000 | offset);
  delayMicroseconds(40);
  LCDWriteData (&data[0], sizeof(data));

  LCDClear ();
}


void loop()
{
  delay (1000);

  // LCD memory locations 0-127.
  // for (int idx=0; idx<128; idx++)
  // {
  //   LCDSetOffset (idx);
  //   LCDWriteDataByte (idx+32);
  // }

  LCDWriteDataString (0, 0, "8 Programmable Chars");

  LCDWriteDataString (1, 2, "0:");
  LCDWriteDataByte (0);
  LCDWriteDataString (6, 2, "1:\1  2:\2  3:\3");

  LCDWriteDataString (1, 3, "4:\4  5:\5  6:\6  7:\7");

  delay (4000);

  LCDWriteDataString (0, 1, "\5\6\6\6\6\6\6\6\6\6\6\6\6\6\6\6\6\6\6\7");
  LCDWriteDataString (0, 2, "\4\4\4\4\4\4\4\4\4\4\4\4\4\4\4\4\4\4\4\4");
  LCDWriteDataString (0, 3, "\5\6\6\6\6\6\6\6\6\6\6\6\6\6\6\6\6\6\6\7");

  delay (2000);

  for (int x=0; x<24; x++)
  {
    if (x < 20)
    {
      LCDSetXY (x , 2);
      LCDWriteDataByte (x & 1);
    }

    if (x > 4)
    {
      LCDSetXY (x-4, 2);
      LCDWriteDataByte (2 + (x & 1));
    }

    delay(250);
    
    if (x < 20)
    {
      LCDSetXY (x, 2);
      LCDWriteDataByte (32);
    }

    if (x > 4)
    {
      LCDSetXY (x-4, 2);
      LCDWriteDataByte (32);
    }
  }

  delay(3000);

  LCDClear ();

  int x = 0;
  int y = 0;
  int xm = 1;
  int ym = 1;

  for (int idx=0; idx<40; idx++)
  {
    LCDWriteDataString (x, y, "Sub-Etha!");
    delay(250);
    LCDWriteDataString (x, y, "         ");

    x = x + xm;
    if ((x < 1) || (x >= LCD_WIDTH-8-1))
    {
      xm = -xm;
    }

    y = y + ym;
    if ((y < 1) || (y >= LCD_HEIGHT-1))
    {
      ym = -ym;
    }
  }

  delay(3000);
}

// End of LCDTest.ino

RadioShack is back!

Have you checked the RadioShack store locator page lately? I did, last week, and instead of just finding three dealer stores in my state (none within an hour), I was surprised to see on in nearby Ankeny, Iowa. It was in some place called HobbyTown.

Somehow I missed the announcement that the Shack posted to their Facebook page back in July. Indeed, they now have a store-within-a-store at some HobbyTown locations.

I went to visit this location, and thought I’d share a trip report.

HobbyTown… Remote controlled cars, model rockets, and … RadoShack Express!
Yes, Virginia. There IS a RadioShack inside.
They only have the electronics parts, all displayed on the wall. No answering machines, video cables, or cell phones.
Even perfboards! This IS your father’s RadioShack! Except it’s still missing the space between the words :(

There was also another small glass cabinet nearby with a few other parts, and at the front checkout counter they had RadioShack brand batteries and such.

I have been working on some projects for Halloween, and found several items that I could put to use. I picked up a 12V buzzer, some 12V lights, some button switches, and some wire nuts. (Yeah, I know I could buy a whole bag of industrial wire nuts at the hardware store for just a bit more, but I only needed a few and “it was just a few bucks.”)

My bounty from my first trip to a RadioShack Express. They even use the old RS catalog numbers (after the RSH).

Leave a comment if you have a RadioShack near you.

RS232 to TTL adapters with all signals, including DCD

I have been experimenting with hooking up low-cost ESP8266 WiFi modules to the Radio Shack Color Computer (or anything with an RS-232 port) using cheap RS232-to-TTL adapters. Most of these adapters only support 3-wire RS-232 (transmit, receive and ground). This is fine for most uses of the CoCo’s built-in Serial I/O port (bitbanger) since it is only a 4-pin DIN connector with TX, RX, GND and CD, and most things do not rely on carrier detect.

But, for the Deluxe RS-232 Program Pak, a hardware 6551 UART chip is used and that chip requires to see carrier detect before it will receive any data. A workaround is to simply tie some of the TTL converter pins together to make it always present DCD to the RS-232 Pak:

This works fine if you are just connecting to remote systems, but if you want to run a BBS, those often rely on carrier detect to know if a caller is online. If a caller disconnects without properly logging off, the system may sit there forever (worst case) or until some software timeout is reached and the system resets. During that time, it would be possible for the next person connecting to resume that session.

This was a real and common issue with BBSes back in the 1980s, for this very reason – lack of carrier detection.

Another issue is the lack of hardware flow control. There could be times when the ESP8266 could try to send data faster than the computer could process, and there is no way (other than XON/OFF, which can’t be used for binary transfers) to tell it to pause sending.

I have found RS232-to-TTL adapters that provided 5-wire signals (adding RTS and CTS), which is needed for this hardware flow control. Those are a bit more costly (I found one for $9 on Amazon), and still do not address the carrier detect issue.

Once again, David Chesek comes to the rescue. He located an adapter that supports all the RS-232 signals, including RI (ring indicator). It is available for $11.50 plus shipping from a company in Las Vegas called Pololu (and might even be manufactured there):

Pololu RS-232 to TTL adapter with all signals. (Photo taken from Pololu.com)
https://www.pololu.com/product/126

I took a gamble (pun intended) and ordered a few. I will report back as soon as I receive them and have had a chance to try them out.

Meanwhile, knowing such a thing exists, I did more searching and found a similar one on e-Bay from a seller called MDFLY. They have them for $3.25 plus shipping, and are shipped from California:

eBay seller MDFLY RS-232 to TTL adapter with all signals. (Photo taken from their eBay listing.)
https://rover.ebay.com/rover/0/0/0?mpre=https%3A%2F%2Fwww.ebay.com%2Fulk%2Fitm%2F382224238410

The board layouts are quite different, but the functionality should be similar. I will be comparing both of these (and noting shipping time) when I have them.

With either one of these boards, properly wired to additional pins on a full ESP8266 development module, it should be able to provide hardware flow control and carrier detect. (Sorry, the ESP-01 module only has two I/O pins, so we’d have to pick and choose what to support on that one — like DTR and DCD for BBS use, or CTS/RTS flow control.)

More to come…

YQ8008 bicycle LED light for $74.96 on e-Bay

  • 2015/8/25 – Added not about $36 YQ8007.
YQ8008 bicycle LED light (pic from e-Bay store), currently $75 on e-Bay.
YQ8008 bicycle LED light (pic from e-Bay store), currently $75 on e-Bay.

A relatively new e-Bay store, Newell Development, has a listing for the YQ8008 three-arm bicycle LED light for $74.96 with free shipping from China. This model typically sells for around $130, but many e-Bay stores have it for around $80 with a $20 shipping fee. This $74.96 price is the lowest I have found so far.

They list the item as “generic” but I wrote them to ask if it was a YQ8008 (they use all the same official photos) and they responded:

“…it is Original with 100 Modes Programmable DIY Bike Bicycle Wheel Spoke Light. And it is in stock.”

Although the XuanWheel has four arms (so it can display images at lower speeds), the YQ8008 has a higher LED count per arm and thus produces a higher resolution image. You can check my comparison chart to see more details.

I have also found the YQ8007 (two arms) for $40 with free shipping from GearBest.com. I have received one to review. It shipped on 8/11 and was received in Iowa on 8/20, so just over a week — not bad. (As of this update, it is currently $36.)

See Also: XuanWheel for $79.

XuanWheel bicycle LED light for $79 on Amazon

  • 2015/8/14: Added note about e-Bay seller.
  • 2015/8/24: Added note that it now is shipped by Amazon, and qualifies for Amazon Prime shipping.
  • 2015/12/8: $72 on Amazon currently, and there are some reviews now (and notes from the seller explaining why the iOS app is “untrusted”. Buyer beware!)
XuanWheel (pic from Amazon store).
XuanWheel (pic from Amazon store).

The XuanWheel (or is it Xuan Wheel?) just saw a $10 price drop. It is currently $79 at Amazon with shipping  from Amazon, so it qualifies for Amazon Prime. This model has four arms, and thus produces an image (or moving video) at lower speeds than the cheaper two arm models.

One of the two e-Bay sellers has them for $69 with free shipping (from China), currently.

See Also: There is also the YQ8008 (now found for $75 on e-Bay with free shipping) three arm unit which has a higher density of LEDs no each arm for higher resolution photos. XuanWheel is probably better at slower speeds, and YQ8008 probably has better images at higher speeds.

More on bike spoke light LED signs (POV)

  • 2004/8/09 – Adding link to Hokey Spokes.
  • 2014/8/10 – Adding link to manufacturer of YQ800X series products.

Last year, I posted an article discussing a cheap bike wheel LED display I picked up for $6 on e-Bay. Recently, I discovered many other ones seem much better. The cheap one I have has 32 blue LEDs, and is single sided, so you can only view it from on side of the bike. Since then, I have discovered full color versions with more LEDs and, most importantly, double-sided so they can be viewed on either side of the bike. Here is a rundown of my researc so far, mostly posted here so it can be indexed in Google, BING, etc. and maybe help others.

I will post links to the items available from Amazon (but NONE are actually sold BY Amazon, and most ship from China and take weeks to arrive). I have found hundreds of e-Bay stores selling them, too, often at far lower prices.

There is a company called ExcelVan that makes several, ranging from $20 to over $100.

The ones I have found so far include:

  1. YQ8003 – $45, double-sided, two arm, 128 LED, programmed via USB cable.
    http://www.amazon.com/Excelvan-Colorful-Waterproof-Programmable-customize/dp/B00WS2I8K2
  2. YQ8005 – $26, double-sided, two-arm, 96 LED, maybe not programmable (25 included pictures).
    http://www.amazon.com/Excelvan-Colorful-Pictures-Waterproof-Mountain/dp/B00W8QC1JC/ref=sr_1_cc_2?s=aps&ie=UTF8&qid=1439079623&sr=1-2-catcorr&keywords=YQ8005
  3. YQ8007 – $90 (but I found it for $40), double-sided, two-arm, 144 LED, programmable by SD memory card. This Amazon link is for a different brand name, so it is either a clone/bootleg or just another company selling the item under their name.
    http://www.amazon.com/Yongchengg-Programmable-Programming-Double-side-Waterproof/dp/B011U02790/ref=sr_1_1?s=sporting-goods&ie=UTF8&qid=1439079839&sr=1-1&keywords=YQ8007
  4. YQ8008 – $150 (Amazon Prime), double-sided, three-arm, 216 LEDs, programmable by SD memory card. By having THREE arms, it can display the color picture at a slower speed.
    http://www.amazon.com/gp/product//B00RE6KGNY/ref=twister_dp_update?ie=UTF8&psc=1

Update: Since the original posting, I think I have located the manufacturer of these devices. They produce YQ8001 to YQ8009. Some use preset patterns, some are programmable (they call them “DIY”), and some can even do video. I will try to put a chart together as I learn more.

Here is the YQ8003 installation video:

By searching for the “YQxxxx” numbers, you can find them being sold all over e-Bay and other online places — most shipping from China. The prices vary greatly. GearBest has the YQ8007 (they claim) for $40.99 with free shipping, for example.

XuanWheel (pic from Amazon store).
XuanWheel (pic from Amazon store).

There is a difference in how they work, too. Some just display static photos, and some can display animation. But, the best one (maybe), is the XuanWheel.

http://www.ixuanlun.com/en/indexEnMobile.html

I believe it started out as an IndieGogo campaign called HaloWheel, but since Halo Wheels is a name of a bike wheel brand, maybe that’s why they changed it to XuanWheel? It is a double-sided, four-armed one that is programmed via Bluetooth over an Android or iOS device. This HaloWheel (per IndiGogo name) or XuanWheel (per website) runs $89 on Amazon (there is a $5 discount code right now) with free shipping (from China, so it takes a month to reach the USA). I found similar devices on e-Bay for as low as $73 (they may be knockoffs or clones).

This one looks like it can synchronize both wheel displays (if you have two). I could not find ANY information on what size hub it would fit, so I asked on YouTube and they replied:

The diameter of the hub should not be larger than 3.8 centimeter

WARNING: Their iOS app is not in the App Store. Instead, you just go and download it direct from their website. Assuming you like to just download random apps from sites in China… Yes, just like Android, you can directly install iOS apps without going through the App Store. BUT, they are not supposed to do that. That is, I think, how developers allow beta testers to get access to their apps before they are done and submitted to Apple. They only get a limited number of installs this way, I believe, and they are not meant to be distributing software like this. At least the iOS device will warn you:

Currently not in the App Store, you have to take changes with a non-inspected app from a website in China. Scary!
Currently not in the App Store, you have to take changes with a non-inspected app from a website in China. Scary!

And lastly, there is even the Monkey Light Pro  by Monkeylectric that sells for $1000. It looks good, but not $1000 good!

More to come… I am hoping to have a review unit of one of these in a few weeks.

UPDATE: Commenter wb8nbs pointed me to Hokey Spokes, which at $20 16-LED spoke lights that can display preset patterns or simple one line text. The unique thing about them is you can use just one, or multiple. They sync to each other using infrared, and from the demo videos, it appears they all just do the same thing so all patterns look symmetrical (thus, any text would show the same in three places of the wheel when using three of these). Not color, but you can get them in different colors and create interesting rainbow effects. Not the most cost effective solution, but if you just want cool lights, one would be pretty cheap, and they ship from Indiana!

Cheap bicycle wheel LED sign (POV)

Due to finances, about the only form of recreation I have these days is riding my bike. I like to take casual rides on the many trails here in Des Moines (Iowa is famous for it’s massive bike trail network).

A few years ago, I came across this neat LED display that attaches to a bike wheel and created images using persistence of image (i.e. lights flashing real fast as the wheel rotates, which the eye sees as a complete, though flickering, image):

http://www.ladyada.net/make/spokepov/

Go there and look at the pictures and watch the videos. It is able to display full color and even animate images (I love the Pac-Man and Ghost images they show). It works by having a row of LEDs that flicker on and off as the wheel spins. The wheel has to spin very fast to show an image, so they sell kits with multiple circuit boards of LEDs. The more on the wheel, the slower it has to turn to show an image.

It looked really cool, but it came in a kit, and I am not really that capable of an electronics person. Plus, the kit with three circuit boards was $113.

A found similar (much cheaper) devices on Amazon, though most of them just did preset patterns and didn’t let you load your own. You can find some simple LED wheel lights at Amazon for around $13 that can display short text messages:

Do some searching and you will find all kinds of cheap POV displays for bike wheels. I even found one for around $12 that was programmable via USB:

None of these are anywhere near as cool as the Adafruit SPOKEPOV kit, but they might be pretty neat for the money.

Then, I came across this one on from an e-Bay store:

http://www.ebay.com/itm/PC-Programmable-Wireless-LED-Custom-Message-Bike-Wheel-Lights-/400354534043?pt=LH_DefaultDomain_0&hash=item5d36fd629b

It had two rows of LEDs and could attach to the hub of the bike and display messages or graphics. At the time I found it, there was a seller auctioning them off, and I picked one up for $6 (shipped from the US, even). Unfortunately, the device was not like the pictures show — it was not in color, just blue (the description says this, but all the listings, including Amazon, use pictures showing one with color).

Also, the device would not fit the hub of my bike (the bolts were too short), nor the sensor on the frame (the bracket was too small). I am not sure what tiny little bikes these were made for, but my old 1998 Trek wasn’t one of them.

However, with a bit of rigging, it was easy to attach the device to the spokes of my bike (rather than clamping it around the hub) using some tape, and then I could do some quick experiments.

If I get time, I would like to experiment with a very low-cost version of this, using an Arduino-style device and a strip of the high speed LPN8806 addressable LEDs. An Arduino in a plastic enclosure with batteries could easily power two segments of 32 LEDs (wider spacing than the commercial units, though) and do full color. The only other hardware would be a magnet and a sensor so the device can tell when the led strips circle around.

Sound fun? More to come… (I already have created JavaScript code that lets me load in an image in a web browser and convert it to the format that this device would need to display it.)

Simple scrolling LED Sign for NeoPixel (WS2811) or LPN8806

  • 2014/03/16 Update: The source code to this is now on GitHub. Check the Arduino link at the top of each page of this site.

Yesterday evening, I coded up a simple scrolling message sign that uses addressable LED strips like the Adafruit NeoPixels (WS2811) or LPN8806. The code I created is built for NeoPixels, since those were the ones I had access to, but it would be trivial to make it work with the Adafruit LPN8806 library. Future versions will make this simpler.

First, let’s talk about LED signs.

The BetaBrite is a commercially available scrolling message sign that’s been around for ages. I bought one at SAM’S CLUB back in the late 1990s. The BetaBrite that I have uses an 80×7 array of LEDs. This is what I will be trying to replicate.

If you shop around (ahem, e-Bay), you can find 1 meter long WS2811 LED strips with 60 RGB LEDs for around $8-$9. If you had seven of those, you could make a 60×7 LED sign. It wouldn’t be able to show as many characters at the same time as a BetaBrite does, but it would be good enough to experiment with. (There are strips with 144 pixels per meter, but they are very expensive. And, when you get past 500 or so LEDs, you start running out of memory on the Arduino. I plan to fix this with some updates to the LED library, eventually.)

Consider this wonderful drawing as I discuss a few possible ways to present a sign made out of LED strips:

LED Sign ideas

A. At the top is an example of one of these LED strips with LED number 0 to “n”. One end hooks to the Arduino and power, and the other end can be used to daisy chain multiple strips together. The first LED will be 0, and they count up to the end of the last strip. If you have three 60 LED strips, you have LEDs 0 to 179. The green arrow shows the direction of the data (the LEDs count up in that direction).

B. Next is an example of how you might arrange multiple strips so they could make up an LED sign. Each strip is shown running left-to-right, so at the end of the first strip the cables go all the way back to the left to connect to the start of the next strip. Wiring them like this makes it real easy to do things with. Notice that the green arrow runs left-to-right on each row.

C. However, it would be much much easier to just connect them like this, without all the extra wires running around. But, this causes every other row to run in the opposite direction (again, see the green arrows). This means the software has to be smart enough to know how to reverse drawing the pixels for every other row.

ALSO, based on where you decide to make LED 0, that changes everything. In these drawings, we are hooking the Arduino up at the top left. But, if it was easier to hook up at the bottom right, the entire numbering system would be backwards.

I decided to write a simple LED message program that could handle all of this. It’s not pretty, but it (maybe) works. I configure it with the number of LEDs in use, and how many are in each row, then I set where the start pixel is (TOPLEFT, TOPRIGHT, BOTTOMLEFT or BOTTOMRIGHT). I support running the rows STRAIGHT (A) or ZIGZAG (B). It can even do something fun…

D. This is the only thing I have actually done. I had two 1 meter strips, so I decided to spiral them with 20 LEDs in each spiral before the next row starts. These 120 LEDs can be split up in my program as six rows of 20 LEDs each, and then (with a small enough font), a message can rotate around it.

If you’d like to try out my code, I have posted it to GitHub:

https://github.com/allenhuffman/LEDSign

I have only tested it in the D configuration, but I have done some debug prints that make me think it should be handling all the other variations. Until I have access to more LED strips, I won’t know for sure.

Anyone want to try it out and let me know how it works for you?

Poor documentation, and the code could be cleaned up and optimized quite a bit. Perhaps I will have that done when I reach version 1.0.

Here’s a video of my first working version:

Arduino WS2811 scrolling message sign

I have two 1m 60 LED WS2811 strips, and expect to receive several more in coming weeks from Chinese suppliers. Once I have seven of them, I plan to use them as a scrolling message sign.

However, with two 1m strips (120 LEDs total), you can spiral them loosely (not tight enough to break the strip) and end up with six rows… Sorta. And, with a quick Arduino sketch (making use of some TVout fonts)… You can create a mini-circular scrolling message sign.

Here is “HELLO WORLD”:

Just for fun…