My4TH can now drive NeoPixel LEDs

First of all:

The original My4TH cannot control NeoPixels directly. Instead, you need to install the special firmware ROM "my4th-neo", which is included in the "my4th-roms" ZIP file available in the Downloads section. The firmware requires My4TH to be clocked at 10 MHz.

What are NeoPixel?

Nowadays, NeoPixel is a generic term for digitally controllable and daisy-chainable RGB LEDs. But originally, the brand name NeoPixel was created by Adafruit for WS2812-compatible RGB LEDs. A WS2812 LED has four pins: VCC, GND, data input and data output. There is no limit to the number of LEDs that can be daisy-chained together. Each individual LED in the chain is controlled by a 24-bit wide serial data word, which contains the 8-bit brightness levels for the colors red, green and blue. The data rate of the bitstream is typically around 800 kHz. At this speed, it’s possible to drive a 32 x 32 LED matrix with a refresh rate of 30 Hz.

Generating the Serial Data

Modern MCUs are fast enough to generate the serial data stream in software. There are many libraries available on the internet for nearly every hardware platform. But the My4TH platform is much too slow. It should be clear that a driver for WS2812 LEDs can not be written in Forth. Also controlling the LEDs through an lower-level assembly program is not possible. It is also impossible to generate the bitstream within the My4TH microcode while adhering to the timing specifications defined in the WS2812 datasheet. Fortunately, though, it’s not as difficult as it might seem at first glance from the WS2812 spec sheet. As Josh Levine discovered, it isn’t actually that difficult to ensure a working bitstream timing if you take the maximum permissible tolerances into account. He also found that the required reset time (referred to as RES or TLL in the datasheet) is just 6 µs. Armed with this knowledge, I managed to implement a working bitstream transmitter in the My4TH microcode!

Work Around Platform Limitations: Memory and Speed

To speed up the transmission and limit the memory required for the color data, the transmission routine uses only an 8-bit color value (1 byte) per LED instead of 24 bits. This reduces the number of available colors to 255 plus black (all LEDs off), but it also reduces memory requirements. Due to the tight timing constraints, the transmission routine must be informed in advance of how many LEDs are to be controlled. Therefore, the user must prepare a buffer containing the color values for all LEDs in the chain. Unfortunately, the counter for the number of LEDs to be controlled is limited to 8 bits, as the tight timing does not allow for the use of a 16-bit counter. For this reason, My4TH can control a maximum of 256 WS2812 LEDs.

My4TH-neo Color Table

This table shows all 256 colors that My4TH-neo can display on WS2812 LEDs. Click on the table to open it in high resolution in a new window:

The Assembly Instruction that controls the LEDs

The My4TH-neo ROM contains very complex microcode for controlling the LEDs. To make room for the microcode, I had to remove support for the Forth Deck from the ROM. Don’t confuse it with the My4TH-nfd ROM. My4TH-neo is just as slow as the original My4TH ROM, but it does not support the LCD and the keyboard. As a little bonus, it also includes the improved text editing features.

The new instruction has the opcode 0x6F, and its mnemonic is "NEO". The instruction expects two input values: The PTR register must point to the LED color data in RAM (one byte per LED), and the accumulator must be set to the number of LEDs in the chain. If the chain contains 256 LEDs, the accumulator is set to zero. Note that the NEO instruction itself is only one byte long.

The Forth Way

Of course it is also possible to control the LED string with a program written in Forth. The corresponding Forth word is "neo":

neo ( ptr cnt -- )

The word takes two inputs: A pointer to the serial data stream and the count of bytes (colors) to send, which must not exceed 256. Note that each LED is controlled by a single byte (8 bit color code). The ROM contains a table that translates between the color byte and the 24 bit RGB value needed by the NeoPixel LEDs.

Connecting the LEDs to My4TH

The serial data stream is output at pin 18 of J3 (Output 7). If My4TH is only controlling a few LEDs, you can take the power supply for this LEDs from pin 2 (+5V) and pin 20 (GND) of J3. Note that a larger number of LEDs (like the 8x8 LED matrix in my examples) quickly consumes a lot of power, especially if the LEDs are set to a very bright level. In this case, the LEDs cannot be powered via USB.

Important to note: The remaining digital outputs 3 to 6 of J3 remain usable, but their state is reset to zero every time the LEDs are updated. For example, you can still connect a serial shift register such as the 74HC595 to expand the output port. But faster would be an I/O port expander connected via I2C.




Example Projects

This ring clock consists of a NeoPixel ring with 12 LEDs and a DS1307 RTC board:

----[001]-------------------------------------------------------
\ Display time on a NeoPixel ring with 12 LEDs (by D. Kuschel)
\ Uses a RTC chip clock driver that exports the word GETTIME
decimal   ( You can use my DS1307 or PCF8523 RTC driver )
5  constant brightness  ( range 0 - 15 )
3  constant red     6  constant yellow     8  constant green
10 constant cyan    14 constant purple     12 constant blue
\ the hour is blue, minutes blink colorful, see order below:
create mcol   red c, yellow c, green c, cyan c, purple c,
create leds 12 allot
: col! ( pos col -- )  brightness 4 lshift or swap leds + c! ;
: .clock ( sec min hour -- )  \ display time on the ring
  leds 12 0 fill  12 mod blue col! \ all LEDs off, set hour led
  dup 5 / swap 5 mod mcol + c@ \ calculate minute pos and color
  rot 1 and 0<> if col! else 2drop then leds 12 neo ; \ blink
: clock begin GETTIME .clock rinp 1 and 0= if quit then again ;
clock \ Run the clock. Note: First, set the time using SETTIME
----[EOF]-------------------------------------------------------

Note: You can use these RTC drivers for this project.


This matrix clock consists of three 8x8 LED matrix modules and a DS1307 RTC board. The first LED is at the bottom left, and the last LED in the string is at the top right edge:

----[001]-------------------------------------------------------
\ Display time on a 24 x 8 NeoPixel LED matrix (by D. Kuschel)
\ Uses a RTC chip clock driver that exports the word GETTIME
hex  ( You can use my DS1307 or PCF8523 RTC driver )
43 constant charcol   46 constant dotcol
4B constant hseccol   2B constant mseccol
0B constant lseccol   ( 4 x 7 pixel charset below: )
: n0 s" .#####.#.....##.....#.#####." ;
: n1 s" ....#.......#.#######......." ;
: n2 s" ##...#.#.#...##..#..##...##." ;
: n3 s" .#....##..#..##..##.#.##..##" ;
: n4 s" ...##.....#.#.#######...#..." ;
: n5 s" #..#####..#..##..#..#.##...#" ;
: n6 s" .#####.#..#..##..#..#.##...." ;
: n7 s" ......###....#..##..#....###" ;
: n8 s" .##.##.#..#..##..#..#.##.##." ;
: n9 s" ....##.#..#..##..#..#.#####." ;
----[002]-------------------------------------------------------
create cset ' n0 , ' n1 , ' n2 , ' n3 , ' n4 ,
            ' n5 , ' n6 , ' n7 , ' n8 , ' n9 ,
create tcol lseccol c, mseccol c, hseccol c,
create lcol mseccol c, lseccol c, 0 c,
create leds c0 allot  \ frame buffer

: 2+ 1+ 1+ ;  ( n -- n+2 )

: chptr ( nbr -- ptr )  \ convert numbers 0-9 to charset ptr
  cells cset + @ execute drop ;

: prep-cset \ prepare charset, set color to "charcol"
  a 0 do i chptr 1c 0 do
    dup c@ [char] # = if charcol else 0 then over c! 1+
  loop drop loop ;
prep-cset forget prep-cset
----[003]-------------------------------------------------------
: cout ( char column -- )  \ copy number-bitmap into framebuffer
  swap chptr swap 3 lshift 1+ leds +   ( csetptr pixmemptr )
  4 0 do  2dup 7 move 8 + swap 7 + swap  loop 2drop ;

: .sec ( sec -- ) \ update only the seconds-display (moving dot)
  2+ 3 /mod  dup 14 = if drop 0 then
  dup 0= if 13 else dup 1- then
  rot swap over
  lcol + c@  swap 2+ 3 lshift leds + c!
  tcol + c@  swap 2+ 3 lshift leds + c!
  leds c0 neo ;  \ output the frame buffer to the LED matrix

: .clock ( sec min hour -- )  \ print out time with sec/hour/min
  leds c0 0 fill  dotcol leds 5b + 2dup c! 2+ c! 
  a /mod dup 0<> if 1 cout else drop then 6 cout
  a /mod d cout 13 cout .sec ;
----[004]-------------------------------------------------------
: run
    \ initialise, print time and remember current second
    gettime rot dup >r rot rot .clock
    begin
        \ get time, check if second has changed
        gettime rot dup r> dup >r =
        if 2drop drop else
           \ only if second has changed, remember new second
           r> drop dup >r
           \ if second is 0, update hole display (this is slow)
           dup 0= if rot rot .clock
           \ otherwise update only the seconds
           else .sec 2drop then
        then
    again ;
run \ start the clock program
----[EOF]-------------------------------------------------------

Note: You can use these RTC drivers for this project.


My4TH can now play Game Of Life as well. All you need is the my4th-neo ROM, four 8x8 LED matrix modules, and a small assembler program:

The four LED matrix modules are connected as follows:

Software Installation:
Download the Game Of Life ZIP archive. The archive contains the source code for the assembler program and the program’s binary file. Upload the binary file to your My4TH board, for example to blocks 60 and 61:

$ my4th write /dev/ttyUSB0 binary 60 gameoflife.bin

On your My4TH, start the program with

60 bload

Now enjoy the evolution of these fascinating patterns!