Category Archives: Arduino

Arduino programming, tips and research.

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

Wokwi online Arduino/ESP32 simulator

Oh, Wokwi! Where have you been all my life? Or at least where were you a few years ago when I was working on Arduino projects?

I initially started using this Sub-Etha Software blog for Arduino projects. I did crazy things like porting a Color BASIC program to Arduino C, and fun things like figuring out how to write Pac-Man using the Arduio TVOut library. Eventually, I merged my interests in the old Radio Shack Color Computer (CoCo) with Arduino (and ESP8266/ESP32) and experimented with a serial port sound player and WiFi modem.

Prototype “Sir Sound” sound module for the CoCo (or anything with a serial port, actually).

So … many … wires.

At the time, I was hoping to find some kind of Arduino emulator so I could write and test code without hooking up hardware. I found nothing.

But that seems to have changed. I just learned about Wokwi which allows one to “simulate IoT projects in your browser.” In a nutshell, it’s a website that has a code editor (which appears to be Microsoft Visual Studio Code), compiler, and virtual target hardware like Arduino and ESP32 devices. It even supports some add-on hardware, like buttons, LCD displays, LEDs and more.

Here’s a project someone made that simulates an Arduino hooked to a numeric keypad and LCD display:

And you can build and run it right there!

There is a library of devices that are supported, and you can add them to your project and wire them up to the computer’s I/O pins. For example, as I write this blog post, I opened up a starter project that is an Arduino and a two-line LCD display. I then added a pushbutton to it.

I could then move the button to where I wanted it, then click on the connectors and draw wire lines between it and I/O pins on the Arduino. By hooking one side to an I/O pin, and the other to ground, I could then modify the program to read that button and, for this example, increment a counter while the button is being held done.

It’s just that easy! I had no idea!

The files can be downloaded and used on real hardware, or you can make an account and log back in to continue working on them. (It has an unusual way to log in — it sends you an e-mail and you click a link to log in, rather than having a username and password. This seems to mean I cannot log in from any system that I don’t have my e-mail account configured on, but I do see options for using a Google or Github login.)

I hope you find this as useful as I already have.

Happy coding!

Arduino, ZigBee and motion sensor help needed.

For a future project, I need to make use of remote triggers. These could be motion sensors, beam sensors, pressure mats, etc.

The ZigBee standard seems to be the way to go since I can find cheap consumer motion sensors that run on batteries. There also seems to be ZigBee repeaters, which allow giving the distance I need simply by plugging them in from place to place to create a mesh network.

XBee might be another option, if cheap motion sensors and repeaters are also available.

The goal is to have a central location be able to read the motion sensor status for many sensors, that could be spread out beyond walls hundreds of feet away.

Any pointers to where I might get started would be appreciated. Ideally I’d drive this all by a low-cost Arduino since the device will be used in an area where power might not be stable (and I wouldn’t want to corrupt the Linux file system on a Raspberry Pi).

Thanks…

16-bits don’t always add up.

Consider this simple program:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

int main(int argc, char **argv)
{
    uint16_t    val1;
    uint16_t    val2;
    uint32_t    result;

    val1 = 40000;
    val2 = 50000;

    result = val1 + val2;

    printf ("%u + %u = %u\n", val1, val2, result);

    return EXIT_SUCCESS;
}

What will it print?

On my Windows PC, I see the following:

40000 + 50000 = 90000

…but if I convert the printf() and run the same code on an Arduino:

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

    uint16_t    val1;
    uint16_t    val2;
    uint32_t    result;

    val1 = 40000;
    val2 = 50000;

    result = val1 + val2;

    //printf ("%u + %u = %u\n", val1, val2, result);
    Serial.print(val1);
    Serial.print(" + ");
    Serial.print(val2);
    Serial.print(" = ");
    Serial.println(result);
}

void loop() {
  // put your main code here, to run repeatedly:

}

This gives me:

40000 + 50000 = 24464

…and this was the source of a bug I introduced and fixed at my day job recently.

Tha’s wrong, int’it?

I tend to write alot of code using the GCC compiler since I can work out and test the logic much quicker than repeatedly building and uploading to our target hardware. Because of that, I had “fully working” code that was incorrect for our 16-bit PIC24 processor.

In this case, the addition of “val1 + val2” is being done using native integer types. On the PC, those are 32-bit values. On the PIC24 (and Arduino, shown above), they are 16-bit values.

A 16-bit value can represent 65536 values in the range of 0-65535. If you were to have a value of 65535 and add 1 to it, on a 16-bit variable it would roll over and the result would be 0. In my example, 40000 + 50000 was rolling over 65535 and producing 24464 (which is 90000 – 65536).

You can see this happen using the Windows calculator. By default, it uses DWORD (double word – 32-bit) values. You can do the addition just fine:

You see that 40,000 + 50,000 results in 90,000, which is 0x15F90 in hex. That 0x1xxxx at the start is the rollover. If you switch the calculator in to WORD mode you see it gets truncated and the 0x1xxxx at the start goes away, leaving the 16-bit result:

Can we fix it?

The solution is very simple. In C, any time there is addition which might result in a value larger than the native int type (if you know it), you simply cast the two values being added to a larger data type, such as a 32-bit uint32_t:

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

    uint16_t    val1;
    uint16_t    val2;
    uint32_t    result;

    val1 = 40000;
    val2 = 50000;

    // Without casting (native int types):
    result = val1 + val2;

    //printf ("%u + %u = %u\n", val1, val2, result);
    Serial.print(val1);
    Serial.print(" + ");
    Serial.print(val2);
    Serial.print(" = ");
    Serial.println(result);

    // Wish casting:
    result = (uint32_t)val1 + (uint32_t)val2;

    Serial.print(val1);
    Serial.print(" + ");
    Serial.print(val2);
    Serial.print(" = ");
    Serial.println(result);
}

void loop() {
  // put your main code here, to run repeatedly:

}

Above, I added a second block of code that does the same add, but casting each of the val1 and val2 variables to 32-bit values. This ensures they will not roll over since even the max values of 65535 + 65535 will fit in a 32-bit variable.

The result:

40000 + 50000 = 24464
40000 + 50000 = 90000

Since I know adding any two 16-bit values can be larger than what a 16-bit value can hold (i.e., “1 + 1” is fine, as is “65000 + 535”, but larger values present a rollover problem), it is good practice to just always cast upwards. That way, the code works as intended, whether the native int of the compiler is 16-bits or 32-bits.

As my introduction of this bug “yet again” shows, it is a hard habit to get in to.

Until next time…

Arduino Serial output C macros

Here is a quickie.

In Arduino, instead of being able to use things like printf() and puchar(), console output is done by using the Serial library routines. It provides functions such as:

Serial.print();
Serial.println();
Serial.write();

These do not handle any character formatting like printf() does, but they can print strings, characters or numeric values in different formats. Where you might do something like:

int answer = 42;
printf("The answer is %d\r\n", answer);

…the Arduino version would need to be:

int answer = 42;
Serial.print("This answer is ");
Serial.print(answer);
Serial.println();

To handle printf-style formatting, you can us sprintf() to write the formatted string to a buffer, then use Serial.print() to output that. I found this blog post describing it.

I recently began porting my Arduino Telnet routine over to standard C to use on some PIC24 hardware I have at work. I decided I should revisit my Telnet code and try to make it portable, so the code could be built for Arduino or standard C. This would mean abstracting all console output, since printf() is not used on the Arduino.

I quickly came up with these Arduino-named macros I could use on C:

#include <stdio.h>
#include <stdlib.h>

#define SERIAL_PRINT(s)     printf(s)
#define SERIAL_PRINTLN(s)   printf(s"\r\n")
#define SERIAL_WRITE(c)     putchar(c)

int main()
{
    SERIAL_PRINT("1. This is a line");
    SERIAL_PRINTLN();
    SERIAL_PRINTLN();

    SERIAL_PRINTLN("2. This is a second line.");

    SERIAL_PRINT("3. This is a character:");
    SERIAL_WRITE('x');
    SERIAL_PRINTLN();

    SERIAL_PRINTLN("done.");

    return EXIT_SUCCESS;
}

Ignoring the Serial.begin() setup code that Arduino requires, this would let me replace console output in the program with these macros. For C, it would use the macros as defined above. For Arduino, it would be something like…

#define SERIAL_PRINT(s)     Serial.print(s)
#define SERIAL_PRINTLN(s)   Serial.println(s)
#define SERIAL_WRITE(c)     Serial.write(c)

By using output macros like that, my code would still look familiar to Arduino folks, but build on a standard C environment (for the most part).

This isn’t the most efficient way to do it, since Arduino code like this…

  Serial.print("[");
  Serial.print(val);
  Serial.println("]");

…would be one printf() in C:

printf ("[%d]\n", val);

But, if I wanted to keep code portable, C can certainly do three separate printf()s to do the same output as Arduino, so we code for the lowest level output.

One thing I don’t do, yet, is handle porting things like:

Serial.print(val, HEX);

On Arduino, that outputs the val variable in HEX. I’m not quite sure how I’d make a portable macro for that, unless I did something like:

#define SERIAL_PRINT_HEX(v) Serial.print(v, HEX)

#define SERIAL_PRINT_HEX(v) printf("%x, v)

That would let me do:

SERIAL_PRINT("[");
SERIAL_PRINT_HEX(val);
SERIAL_PRINTLN("]");

I expect to add more macros as-needed when I port code over. This may be less efficient, but it’s easier to make Arduino-style console output code work on C than the other way around.

Cheers…

Arduino compatible bit Macros

Years ago, I cloned the Arduino IDE “bit” macros for use in a GCC program (for testing generic Arduino code on a different system). I dug them out recently for a work project, and decided to share them here. Maybe they will be useful to someone else. (And apologies for WordPress clobbering the ampersand and turning it to HTML. When I edit and view it, it looks good, then WordPress “helps” — even though I am using a Syntax Highlighter plugin specifically for code examples.)

#ifndef BITMACROS_H
#define BITMACROS_H
// Bit macros, based on Arduino standard.
// https://www.arduino.cc/reference/en/language/functions/bits-and-bytes/bit/
//
// x: the numeric variable to which to write.
// n: which bit of the number to write, starting at 0 for the least-significant (rightmost) bit.
// b: the value to write to the bit (0 or 1).
#define bit(n)              (1 << (n))
#define bitSet(x, n)        ((x) |= bit(n))
#define bitClear(x, n)      ((x) &= ~bit(n))
#define bitRead(x, n)       (((x) & bit(n)) !=0 )
#define bitWrite(x, n, b)   ((b) ? bitSet((x), (n)) : bitClear((x), (n)))
/* Verification tests:
bool showBits (uint32_t value, unsigned int bits)
{
    bool status;
    //printf ("showBits (%u, %u)\n", value, bits);
    if ((bits == 0) || (bits > sizeof(value)*8))
    {
        status = false;
    }
    else
    {
        // Must use signed to check against 0.
        for (int bit = (bits-1); bit >= 0; bit--)
        {
            printf ("%u", bitRead(value, bit));
        }
        printf ("\n");
    }
    return status;
}
int main()
{
    unsigned int value;
    // Test bit()
    for (unsigned int bit = 0; bit < 8; bit++)
    {
        printf ("bit(%u) = %u\n", bit, bit(bit));
    }
    printf ("\n");
    // Test bitSet()
    for (unsigned int bit = 0; bit < 8; bit++)
    {
        value = 0;
        printf ("bitSet(%u, %u) = ", value, bit);
        bitSet(value, bit);
        showBits (value, 8);
    }
    printf ("\n");
    // Test bitClear()
    for (unsigned int bit = 0; bit < 8; bit++)
    {
        value = 0xff;
        printf ("bitClear(%u, %u) = ", value, bit);
        bitClear (value, bit);
        showBits (value, 8);
    }
    printf ("\n");
    // Test bitRead()
    value = 0b10101111;
    showBits (value, 8);
    for (unsigned int bit = 0; bit < 8; bit++)
    {
        printf ("bitRead(%u, %u) = %u\n", value, bit, bitRead (value, bit));
    }
    printf ("\n");
    // Test bitWrite - 1
    value = 0x00;
    showBits (value, 8);
    for (unsigned int bit = 0; bit < 8; bit++)
    {
        printf ("bitWrite(%u, %u, 1) = ", value, bit);
        bitWrite (value, bit, 1);
        showBits (value, 8);
    }
    // Test bitWrite - 0
    showBits (value, 8);
    for (unsigned int bit = 0; bit < 8; bit++)
    {
        printf ("bitWrite(%u, %u, 0) = ", value, bit);
        bitWrite (value, bit, 0);
        showBits (value, 8);
    }
    printf ("\n");
    return EXIT_SUCCESS;
}
*/
#endif // BITMACROS_H
// End of BitMacros.h

CoCoWiFi and SirSound project updates

Things have been real busy lately at Sub-Etha Galactic Headquarters… Here are some updates:

CoCoWiFi

The CoCoWiFi has been tested on the bitbanger serial port and things seem to work just fine.

On the RS-232 Pak, a modification was needed to make the pak actually receive data (forcing the carrier detect signal). This has been done by an easy soldering mod to the DB9 connector. The downside is that it does not provide true CD, and there is no support for hardware flow control or DTR to drop calls. For just a bit more, there are RS232-to-TTL adapters that provide hardware flow control, which might help for doing high speed transfers. However, the lack of carrier detect and DTR means they won’t work with a BBS like they should

Thanks to David Chesek, new RS232-to-TTL adapters were located that include all the signals. These would be the best choice for running with an RS-232 Pak. I have two different types of adapters, but have only done some initial testing. I was unsuccessful getting the first adapter to work. I plan to test the second version this week.

I hope to have a hardware announcement to make before the CoCoFEST!

SirSound

I learned much while working on CoCoWiFi, so I returned to work on the previously “announced” SirSound project. I got everything wired up properly and was able to write a BASIC program to make it play tones. I also worked with John Strong and migrated the prototype over to an Arduino Nano (matching the original Arduino sound player board he sent me last year).

The next phases is to figure out the various modes that sound module will run in. I have proposed:

  1. Direct. This mode just passes bytes to the sound chip, the same way you might do with a POKE command if it was a memory-mapped chip. BASIC is very slow at ANDing and ORing bits to make the messages, which is how my test program works, but this could be heavily optimized. This mode is mostly here to allow someone to port over code that was written for one of the other platforms that use an on-board SN76489 sound chip, though some of the bit-blasting players would probably not work as well over slow serial.
  2. PLAY. This is the BASIC mode, that will simulate the PLAY command. You will be able to send a string of notes to SirSound and play them just as easy as in EXTENDED COLOR BASIC. There are a few things that have to be adapted, like support for the multiple channels of audio, and sub-strings. Last year, it was suggested to look at the MSX computer’s PLAY command for examples of how Microsoft did this very thing. I may follow that syntax. MSX also ads a PLAY() function that can tell is background audio is in progress, and we will be able to achieve the same results using the Printer Read/CD signal on the bitbanger port. I plan to also add some sequencing extensions so repeating music loops don’t have to be sent over and over again.
  3. Optimized. This mode would be for assembly programs, and would pack data into 8-bit values rather than longer ASCII strings.
  4. Interactive. I am planning on having a shell/command-line interface (CLI) available which could be accessed. It would be used for testing the device without needing to write a BASIC program.

More to come…

Sir Sound prototype, version 2.

Now working without the crystal. This is using a Teensy and one of the pins to generate a 4mhz signal. I will post a video in coming days, hopefully, as soon as I have some CoCo BASIC code talking to it.

The green and black wires running off of it wold go to a headphone jack that the cassette cable could plug in to so you could feed Sound back to the CoCo without needing to use an external speaker. AUDIO ON!

Introducing the Sir Sound CoCo Sound Card

NEW “PRODUCT” ANNOUNCEMENT

The team that brought you* the CoCoPilot DriveWire Server is proud to announce their latest innovation:

“Sir Sound”

Sir Sound is a solid-state multi-voice audio synthesizer that operates over a serial transport mechanism**. It provides arcade-quality*** sound with up to three independent tonal voices plus one white noise channel all in an external module that doesn’t require voiding your warranty to install. In fact, you won’t even need tools!

Pricing is to be announced but hopefully it will be around $50. Or maybe $30. Or cheaper. Or less if you build it yourself. Heck, we’ll probably just make kit versions available since we don’t really like to solder.

Sir Sound Configurations

  • Turnkey – This is a “plug and go” version where you just plug it in and go. No special drivers are needed, as they are already built in to both BASIC and OS-9.****
  • BYOE – The bring-your-own-everything edition is shipped as a set of simple instructions containing a parts list and how to run wires between the parts.
  • Custom – Also planned to be available are various custom configurations, like what color of case it comes in.

Pricing

We estimate the thing is gonna cost us, like, ten or so bucks to make using off-the-shelf parts ordered in small quantities from China. But, to make it a product, we really should have an integrated circuit board and a case made, which will run the costs up dramatically. Rest assured, we’ll pass those unsavings along to you!

Availability

The first prototype is in the process of being tested. Quit rushing us. We’ll let you know when it’s done.

Specs

Basically it’s a Texas Instruments SN76489 sound chip hooked to a tiny Arduino micro-controller with a TTL-to-RS232 adapter. Here’s the prototype John Strong of StrongWare sent me:

SN76849 sound chip hooked to an Arduino Nano on a neat 3-D printed platform from StrongWare.

You kinda have to use some micro-controller since the sound chip turns on and starts making sound. Something has to issue the “shut up” instruction to it. If you just had hardware to translate a serial byte in to the command, and made the CoCo do all the work, the CoCo would have to load and run a program to shut the thing up every time you powered up. Fortunately, a custom-built Arduino that handles this can be done for like $5. There are cheaper PIC chips that could do it for less.

Then, you add a MAX232 type chip that goes from the TTL signal levels of the Arduino to RS232 signal levels, or using one of these $3 (or less) boards that’s already wired:

TTL-to-RS232 adapter.

Lastly, add a CoCo serial cable (4-pin DIN to DB9), and you are set.

Prototype “Sir Sound” sound module for the CoCo (or anything with a serial port, actually).

A small program on the Arduino will monitor the serial port for bytes and then relay them to the sound chip.

By doing some POKEs in BASIC to set the baud rate, you could make music by doing things like this:

REM PLAY MIDDLE C
PRINT #-2,CHR$(&H8E);CHR$(&H1D);CHR$(&H90);

FOR A=1 TO 1000:NEXT A

REM VOLUME OFF
PRINT #-2,CHR$(&H9F);

The notes always play, so you shut them off by setting volume off. There are different channel values for each of the four channels.

I envision having a “raw” mode where the device just translates the bytes from serial to the sound chip, and a “smart” mode where you could use an API and just send note values (like 1-88 of a piano keyboard, or MIDI note values).

“Smart” mode could simplify the control so it might look like this:

REM ALL DATA: PLAY CHANNEL 0, NOTE 10, AT VOLUME 15
PRINT #-2,CHR$(&H00);CHR$(&HA);CHR$(&HF);

REM NOTE ONLY: PLAY CHANNEL 0, NOTE 10
PRINT #-2,CHR$(&H01);CHR$(&HA);

REM NOTE ONLY: PLAY CHANNEL 1, NOTE 10
PRINT #-2,CHR$(&H11);CHR$(&HA);

REM VOLUME ONLY: CHANNEL 0, VOLUME 5
PRINT #-2,CHR$(&H20);CHR$(&H5);

And, I could also add a “super smart” mode where it could parse PLAY command-style strings, then spool them in the background while you do other things:

REM PLAY COMMAND, CHANNEL 0
PRINT #-2,CHR$(&H30);"CDEFGAB";

And, a “super super smart” mode could let it store string sequences, and play them by triggering with a simple byte:

REM STORE NOTE SEQUENCE 0
PRINT #-2,CHR$(&H40);"CCDCECFECCDCECFE";CHR$(0);

REM PLAY NOTE SEQUENCE 0
PRINT #-2,CHR$(&H50);

REM PLAY NOTE SEQUENCE 0 FIVE TIMES
PRINT #-2,CHR$(&H55);

…or whatever. You could sequence them together, like MIDI sequencers do, and have complex patterns that could play in the background while the program does other things.

There are lots of possibilities. We could even see about using the Carrier Detect line as a way to tell if the sound card was still playing something (rather than needing routines to read data back from the device, which would be doable but not from BASIC without assembly language code).

If this “sounds” fun to you, leave a comment…

Until then…


Notes:

* If you call making a blog post “bringing it” to you.

** It plugs in to the Serial I/O port. “Sir” sounds like “Ser”, get it? Marketing thought SerSound wasn’t friendly enough.

*** This part is true. The same sound hardware is used in the arcade mega-hits Congo Bongo and Mr. Do, among others.

**** PRINT#-2, yo.

Arduino and the Texas Instruments SN76489

You may have never heard of the Texas Instruments SN76489, but if you are reading this article, there’s a good chance you have heard it.

The SN76489 is a sound chip which, according to the Wikipedia entry, was used in systems such as:

…and in arcade games such as:

…and many more. I am just naming the machines and games I have heard of or seen/played.

Side Note: The Wikipedia entry also claims the Sega Genesis used one, but it had far fancier sound. A quick search shows the Genesis did not use this chip, so other systems may also be incorrect. Ah, Wikipedia…)

This chip is able to produce three tones and one white noise at a time, which sounds an awful lot like the audio capabilities of my first computer, the VIC-20.

The chip has none of the fancy synthesizer features found in other chips, such as the famous Commodore 64 SID chip. The only thing you can do is adjust the volume level of each channel of sound. Clever software uses this to produce bell sounds and other effects. (If Congo Bongo is really using this chip, it’s doing some fancy things to make those bongo sounds!)

Thanks to StrongWare‘s John Strong, I now have one of these chips to experiment with. It is wired up to an Arduino Nano clone. (NOTE: I had issues getting this clone recognized on my Mac, due to it using a different serial chip. I found the solution, and wrote about it earlier this week.)

SN76849 sound chip hooked to an Arduino Nano on a neat 3-D printed platform from StrongWare.

John pointed me to this short tutorial on how to use the chip:

Using sample code there, I was able to get the device making tones, and then expanded it to play a sequence of tones to make a tune.

The next day I added more code so it could do a multitrack sequence.

I thought it might be fun to share what I have learned the first two days of playing with the device, and share the code I have come up with.

I will do a full article on the chip and how it works (summarizing some overly complex explanations I have been reading), but until then, here is my sample project:

https://github.com/allenhuffman/MusicSequencerTest

It contains routines to poke bytes to the SN76489, plus a work-in-progress multitrack music sequence that currently plays the 2-voice intro music to Pac-Man :)

I’ve been fixing up the comments and squashing some bugs, so check back for the latest. I still have to add real/better support for the “noise” channel, but it works right now for playing simple tunes.

More to come…