Topics

► Games

► Sound & Music

► Watches & Clocks

► GPS

► Power Supplies

► Computers

► Graphics

► Thermometers

► Wearables

► Test Equipment

► Tutorials

► Libraries

► PCB-Based Projects

By processor

AVR ATtiny

► ATtiny10

► ATtiny2313

► ATtiny84

► ATtiny841

► ATtiny85

► ATtiny861

► ATtiny88

AVR ATmega

► ATmega328

► ATmega1284

AVR 0-series and 1-series

► ATmega4809

► ATtiny1604

► ATtiny1614

► ATtiny3216

► ATtiny3227

► ATtiny402

► ATtiny404

► ATtiny414

► ATtiny814

AVR DA/DB-series

► AVR128DA28

► AVR128DA32

► AVR128DA48

► AVR128DB28

ARM

► ATSAMD21

► RP2040

► RA4M1

About me

  • About me
  • Twitter
  • Mastodon

Feeds

RSS feed

Scrolling Text Display

11th September 2024

This is a scrolling text display based on four 8x8 LED dot-matrix displays, controlled by an ATtiny85:

ScrollingTextDisplay.jpg

The Scrolling Text Display, based on an ATtiny85.

It includes a USB socket, so you can plug in a PS/2-compatible USB keyboard to allow you to enter and edit the message, and a potentiometer that allows you to adjust the scroll speed.

It could be used as an advertising sign, such as in a shop window, or as a message board to allow you to leave messages for your partner or housemates.

Updates

11th October 2024: Released v2 of the program that fixes a bug that could cause junk characters to appear on the end of a message.

24th September 2024: Added infomation about a second compatible keyboard.

12th September 2024: Added a note about using jumper wires in construction.

Introduction

I was inspired to build this for a friend's son who is keen on trains, to allow him to make a scrolling train sign like the ones used on many trains and platforms. It uses an external keyboard to allow you to enter a message of up to 256 characters. While you're entering the message the last few characters are shown on the display, and you can press Backspace to delete back to correct mistakes.

When you've entered the message you can press Enter, and the whole message will then start scrolling repeatedly across the display. A five-space gap is inserted between the end of one presentation and the start of the next. While the message is scrolling you can adjust the speed using the potentiometer.

Pressing Esc at any time returns to editing mode, and you can then edit starting at the end of the existing message, or press Tab to clear the message and enter a new message.

When you press Enter the message is stored in EEPROM, so if you disconnect the power and reconnect it, the last message will continue scrolling on the display. This allows you to detach the keyboard if you don't want to change the message.

The circuit

Here's the circuit of the Scrolling Text Display:

ScrollingTextDisplay.gif

Circuit of the Scrolling Text Display.

Processor

I decided to base the circuit on an old favourite: the 8-pin ATtiny85. One reason is that it's available in a PDIP package and so it's easy to mount on the breadboard. It's also a perfect fit for this application. It has 512 bytes of RAM and 512 bytes of EEPROM, ample for the message buffer and for saving the message. It also has just the right number of spare pins: two for the I2C interface to the displays, two for the clock and data lines from the keyboard, and one for the ADC input from the potentiometer.

I considered using one of the newer 0-series or 1-series 8-pin devices, such as the ATtiny402 or ATtiny412, but these are only available in a surface-mount package and so would have to be mounted on a breakout board. In addition, they only have 256 bytes of RAM and 128 bytes of EEPROM, which would restrict the Scrolling Text Display to a much shorter message.

Displays

For the displays I used four low-cost Keyestudio I2C displays, which incorporate a HT16K33 driver chip [1] to handle the display multiplexing and I2C interface, allowing you to control them with just two I/O lines [2]. Adafruit make a similar display [3]. Three solder links allow you to choose one of eight I2C addresses for each display, allowing you to drive up to eight displays simultaneously. The Keyestation modules have got the labels for A1 and A2 swapped around on the PCB, so the addresses you get with the eight possible PCB link settings are as follows:

A2 A1 A0 Address
      0x70 (112)
    X 0x71 (113)
  X   0x74 (116)
  X X 0x75 (117)
X     0x72 (114)
X   X 0x73 (115)
X X   0x76 (118)
X X X 0x77 (119)

You can use any addresses for the four displays, provided they're different. The array Addresses[] specifies the sequence, from left to right; for example:

int Addresses[] = {112, 113, 116, 117};

Potentiometer

The potentiometer allows you to adjust the scrolling speed. It can be any value from 10kΩ to 100kΩ, or you can omit it and make a small change to the program if you don't want to make the scroll speed adjustable. A suitable 50kΩ breadboard-compatible trimmer is available from The Pi Hut [4].

Keyboard

Although it would be possible to interface a USB keyboard using a USB Host keyboard interface, this would require a more powerful processor capable of running TinyUSB [5]; current processors supported are the nRF52, ATSAMD21/51, RP2040, and ESP32. Fortunately many USB keyboards include support for the older PS/2 interface, and this uses a much simpler protocol that can be decoded by a processor such as the ATtiny85. Before buying a keyboard, check that it supports PS/2.

The one I used is the 220 x 118mm MC Saite MC-8017 Miniature PS/2 and USB keyboard, available from Adafruit [6], which is fitted with a USB-A plug:

Keyboard.jpg

MC Saite MC-8017 Miniature PS/2 and USB keyboard.

I've tested the project with a slightly larger low-cost 284 x 122mm Slim Chiclet Keyboard [7], which also also supports PS/2 codes, but note that it doesn't work below 3.5V.

Power

I powered the Scrolling Text Display using a switched battery box that holds two 1.5V AA batteries [8], but you can use any power source that provides between 3V and 5V.

Construction

To make building the Scrolling Text Display as easy as possible I designed it to fit on a full-sized breadboard [9], using through-hole components. To fit the displays onto the breadboard I cut off the right-angle headers supplied on the displays, and soldered straight headers in their place.

I find that the easiest way to wire up breadboards like this is using a set of precut jumper wires of different lengths [10]; the shorter lengths are coded by length using the resistor colour code. The displays are interconnected using links on the breadboard under the displays.

For the USB-A socket I used a breakout board from Sparkfun [11], because the through-hole sockets aren't breadboard friendly.

The program

Keyboard interface

The keyboard interface is based on my Simple PS/2 Keyboard Interface. PS/2 keyboards use a two-wire serial protocol, using a clock pin and a data pin. 

My original routine used the PS/2 clock input to generate an INT0 interrupt on each falling edge. On the ATtiny85 the INT0 input is already in use for the I2C interface SCL pin, so I used a pin-change interrupt on the PB4 input instead. To ensure that we only respond to falling edges I added the following line to the start of the interrupt-service routine:

  if (PINB & 1<<ClockPin) return;

Here's the whole interrupt-service routine:

ISR(PCINT0_vect) {
  static uint8_t Break = 0, Modifier = 0, Shift = 0;
  static int ScanCode = 0, ScanBit = 1;
  if (PINB & 1<<ClockPin) return;               // Only respond to a falling edge
  if (PINB & 1<<DataPin) ScanCode = ScanCode | ScanBit;
  ScanBit = ScanBit << 1;
  if (ScanBit != 0x800) return;
  // Process scan code
  if ((ScanCode & 0x401) != 0x400) return;      // Invalid start/stop bit
  int s = (ScanCode & 0x1FE) >> 1;
  ScanCode = 0, ScanBit = 1;
  if (s == 0xAA) return;                        // BAT completion code
  //
  if (s == 0xF0) { Break = 1; return; }
  if (s == 0xE0) { Modifier = 1; return; }
  if (Break) {
    if ((s == 0x12) || (s == 0x59)) Shift = 0;
    Break = 0; Modifier = 0; return;
  }
  if ((s == 0x12) || (s == 0x59)) Shift = 1;
  if (Modifier) return;
  char c = pgm_read_byte(&Keymap[s + KeymapSize*Shift]);
  if (c == 32 && s != 0x29) return;
  ProcessKey(c);
  return;
}

The PS/2 data output is connected to PB1, and the data consists of a '0' start bit, eight data bits, a parity bit, and a '1' stop bit. This then needs to be converted to an ASCII character using a lookup table, Keymap[] to cater for the rather haphazard arrangement of codes on PS/2 keyboards:

const uint8_t Keymap[] PROGMEM = 
// Without shift
"             \011`      q1   zsaw2  cxde43   vftr5  nbhgy6   mju78  ,kio09"
"  ./l;p-   \' [=    \015] \\        \010  1 47   0.2568\033  +3-*9      "
// With shift
"             \011~      Q!   ZSAW@  CXDE$#   VFTR%  NBHGY^   MJU&*  <KIO)("
"  >?L:P_   \" {+    \015} |        \010  1 47   0.2568\033  +3-*9       ";

For details of the PS/2 keyboard Scan Code Set 2 encoding used by most keyboards see PS2 Keyboard Scan Codes.

Processing keys

When a keypress has been decoded the interrupt-service routine calls ProcessKey() to handle it:

void ProcessKey (char c) {
  if (c == 27) {                                // Escape key goes to edit mode
    DisplayMode = false;
    Scroll = 6 * max(WritePtr-5, 0);
    MessageChars = MessageSize;
    return;
  }
  if (DisplayMode) return;                      // Ignore all other keys
  // Edit buffer
  if (c == '\r') {                              // Return key goes to display mode
    DisplayMode = true;
    MessageChars = WritePtr + EndMessageGap;
    EEPROM.write(0, WritePtr);
    EEPROM.write(1, MessageChars);
    EEPROM.put(2, KybdBuf);                     // Save our message
    return;
  }
  if (c == 9) {                                 // Tab key
    for (int i=0; i<WritePtr+EndMessageGap; i++) KybdBuf[i] = ' ';
    WritePtr = 0;
  } else if (c == 8) {                          // Backspace key
    if (WritePtr > 0) {
      if (WritePtr <= 5) Scroll = Scroll - 6;
      WritePtr--;
      KybdBuf[WritePtr] = ' ';
    }
  } else if (WritePtr < MessageSize) {
    KybdBuf[WritePtr++] = c;
  }
  Scroll = 6 * max(WritePtr-5, 0);
  return;
}

In display mode, when DisplayMode is true, the scolling message is displayed and the only key that the program responds to is Esc (code 27) This switches back to edit mode, with the cursor positioned at the end of the previous message.

In edit mode characters are entered into an edit buffer, KybdBuf[], large enough to hold a 256-character message plus a gap of 5 spaces between messages. Pressing Enter at any time saves the edit buffer in EEPROM, and switches back to display mode. In edit mode the Tab key (code 9) clears the whole message, or the Backspace key (code 8) deletes the last character in the edit buffer.

Setup

The setup() routine initialises the keyboard interface, and sets up the four displays. The display brightness is controlled by the variable Brightness which can be set to between 1 and 15, but I found that a setting of 2 was amply bright. It then initialises the message from EEPROM. When you first build the project this will be random characters; press Enter and Tab to clear the message.

The main loop

The main loop continuously refreshes the displays from the keyboard buffer:

void loop() {
  for (int disp=0; disp<4; disp++) {
    for (int c=0; c < 8; c++) {
      int dcol = disp*8 + c;
      int column = dcol + Scroll;
      int nchar = (column/6)%MessageChars;
      int cchar = column%6;
      int addr = Addresses[disp];
      Wire.beginTransmission(addr);
      Wire.write(c<<1);
      int segs = ReverseByte(pgm_read_byte(&CharMap[KybdBuf[nchar]-32][cchar]));
      Wire.write(segs>>1 | segs<<7);
      Wire.endTransmission();
    }
  }
  delay(analogRead(ADCPin)>>3);
  if (DisplayMode) Scroll++;
}

In edit mode it displays the last five characters in the keyboard buffer.

In display mode it increments the variable Scroll, which scrolls the display by one pixel each time round the loop. The delay between scrolls is determined by a value read from the analogue input connected to the potentiometer, allowing you to adjust the scroll speed.

Compiling and uploading

Compile the program using Spence Konde's ATTiny Core [12]. Choose the ATtiny25/45/85 (No bootloader) option under the ATTinyCore heading on the Board menu. Then check that the subsequent options are set as follows (ignore any other options):

Chip: "ATtiny85"
Clock Source (Only set on bootload): "8 MHz (internal)"

Connect to the appropriate ATtiny85 pins on the breadboard using an ISP programmer; I used Sparkfun's Tiny AVR Programmer Board; see ATtiny-Based Beginner's Kit. The connections are as follows:

Programmer ATtiny85
Reset RST (pin 1)
MOSI PB0 (pin 5)
MISO PB1 (pin 6)
5V VCC (pin 8)
SCK PB2 (pin 7)
GND GND (pin 4)

Set Programmer to USBtinyISP (ATTinyCore) SLOW (or whatever's appropriate for your programmer). Choose Burn Bootloader on the Tools menu to set the fuses appropriately. Then choose Upload on the Sketch menu to upload the program.

To test the program you need to disconnect the programmer from the MOSI pin, as it interferes with the I2C interface.

Resources

Here's the whole program: Scrolling Text Display program.

I'm working on a printed circuit board for the project, and hope to publish details in a future article.


  1. ^ HT16K33 Datasheet on Adafruit.
  2. ^ Keyestudio I2C 8x8 LED Matrix on AliExpress.
  3. ^ Mini 8x8 LED Matrix w/I2C Backpack on Adafruit.
  4. ^ Breadboard-friendly Trim Potentiometer on The Pi Hut.
  5. ^ Arduino library for TinyUSB on GitHub
  6. ^ Miniature PS/2 and USB keyboard on Adafruit.
  7. ^ Slim Chiclet Keyboard on Pimoroni.
  8. ^ Switched Battery Box 2xAA (3V) on The Pi Hut.
  9. ^ Full Sized Premium Breadboard - 830 Tie Points on Adafruit.
  10. ^ Jumper Wires, 140 Pack Assorted on CPC.
  11. ^ USB Type A Socket Breakout on Sparkfun.
  12. ^ ATTinyCore on GitHub.

blog comments powered by Disqus