Background

A few weeks ago I showed a project I had created to monitor the temperature in my son's bedroom. Like many toddlers he is fascinated by lights and patterns of lights.

This got me thinking to how I could create a interesting light effect for his bed room based of science and technology.

After a little consideration I decided to implement the Game of Life created by Conway in 1970. The Game of Life is a Cellular Automaton zero player game in which the player watches evolution over a number of generations.

Based on a two dimensional grid each cell within the grid lives, dies or reproduces based upon the following rules.

Any live cell with fewer than two live neighbors dies, replicating under population.

Any live cell with two or three live neighbors lives on to the next generation.

Any live cell with more than three live neighbors dies, replicating overpopulation.

Any dead cell with exactly three live neighbors becomes a live cell, replicating reproduction.

To display the Game of Life, it's evolution I decided to use a number of NeoPixel 8 by 8 panels. This allowed me to create a 256 pixel array on which I can display the game of life in up to 16 million colors.

Initial States

The initial state from which the Game of Life runs can be anything although there are several groupings of initial settings. These include

Oscillators - Return to their original pattern after a number of evolution.

Static or Still Life - These are patterns which do not evolve from one generation to the next.

Space Ships - These move across the grid, there are many classes of space ship from the simple glider to the recently found knightship (The first new elementary spaceship find in 48 Years)

If you read into the Game of Life you will find initial states can be Turing complete. That is they can replicate AND, OR, NOT and even finite state machines. Looking into the Game of Life can become very addictive very quickly.

NeoPixels

NeoPixels are digitally controlled three color LEDs which can display up to 16 million colors thanks to there 24 bit Red Green Blue format.

Each NeoPixels output color is determined by a 24 bit serial word. What is really cool about NeoPixels is they can be daisy changed together. If a NeoPixel begins receiving another serial word within 50 microseconds it will output the previously received word to the next NeoPixel in line.

NeoPixel 24 bit format

As NeoPixels provide only a serial input and output with no clock. They use a unique self-clocking, non-return-to-zero (NRZ) waveform to specify the bit values,

The timing information to drive a NeoPixel is shown below.

NeoPixel Timing Information

Architecture

While the example I am creating will only use a 256 array, I want the design to scale such that it could implement much larger displays if desired.

As such the approach I decided upon is the following

The NeoPixel Array will be driven by a hardware component

The evolution algorithm will be executed in the PS, it is not time critical as we want the evolution to be visible on the display.

Between the PS and PL will use a large BRAM, into this BRAM will be placed the number of pixels in the array and each pixel value This will then be read out by the hardware component.

RTL Design

The first thing we need to do in the design is create and simulate a RTL block that can drive the NeoPixel.

This module is designed to be clocked from a 20 MHz Clock, and is a simple state machine which reads the pixel value from the BRAM and outputs it to the NeoPixel.

LIBRARY IEEE; USE ieee.std_logic_1164.ALL; USE ieee.numeric_std.ALL; ENTITY neo_pixel IS PORT( clk : IN std_logic; dout : OUT std_logic; rstb : OUT STD_LOGIC; enb : OUT STD_LOGIC; web : OUT STD_LOGIC_VECTOR(3 DOWNTO 0); addrb : OUT STD_LOGIC_VECTOR(31 DOWNTO 0); dinb : OUT STD_LOGIC_VECTOR(31 DOWNTO 0); doutb : IN STD_LOGIC_VECTOR(31 DOWNTO 0) ); END ENTITY; ARCHITECTURE rtl OF neo_pixel IS TYPE FSM IS (idle,wait1,led,count,reset,addr_out,wait2,grab,wait_done,done_addr); CONSTANT done : std_logic_vector(25 DOWNTO 0) := "00000000000000000000000001"; --shows when shift reg empty CONSTANT zero : std_logic_vector(24 DOWNTO 0) := "1111111000000000000000000"; --waveform for a zero bit CONSTANT one : std_logic_vector(24 DOWNTO 0) := "1111111111111100000000000"; --waveform for a one bit CONSTANT numb_pixels : integer := 24; --number of bits in a pixel CONSTANT reset_duration : integer := 1000;--number cclocks in the reset period SIGNAL shift_reg : std_logic_vector(24 DOWNTO 0) := (OTHERS=>'0'); -- shift reg containing the output pixel waveform SIGNAL shift_dne : std_logic_vector(25 DOWNTO 0) := (OTHERS=>'0'); -- shift reg for timing the output shift reg for next load SIGNAL current_state : fsm := idle; --fsm to control the pixel beign output SIGNAL prev_state : fsm := idle; --previous state SIGNAL load_shr : std_logic :='0'; --loads the shr with the next pixel SIGNAL pix_cnt : integer RANGE 0 TO 31 := 0; --counts the position in the pixel to op SIGNAL rst_cnt : integer RANGE 0 TO 1023 := 0; --counts number of clocks in the reset period 50 us @ 20 MHz SIGNAL led_numb : integer RANGE 0 TO 1023; --number of LED in the string SIGNAL ram_addr : integer RANGE 0 TO 1023:=0; --address to read from RAM SIGNAL led_cnt : integer RANGE 0 TO 1023;--counts leds it has addressed SIGNAL pixel : std_logic_vector(23 DOWNTO 0); --holds led value to be output BEGIN web <= (OTHERS => '0'); rstb <= '0'; dinb <= (OTHERS =>'0'); pixel_cntrl : PROCESS(clk) BEGIN IF rising_edge(CLK) THEN load_shr <= '0'; enb <= '0'; CASE current_state IS WHEN idle => current_state <= wait1; rst_cnt <= 0; addrb <= std_logic_vector(to_unsigned(ram_addr,32)); enb<='1'; WHEN wait1 => current_state <= led; WHEN led => led_numb <= to_integer(unsigned(doutb)); IF to_integer(unsigned(doutb)) = 0 THEN current_state <= idle; ELSE current_state <= addr_out; ram_addr <= ram_addr +4; END IF; WHEN count => IF pix_cnt = (numb_pixels-1) THEN IF led_cnt = (led_numb-1) THEN current_state <= reset; pix_cnt <= 0; ram_addr <= 0; ELSE ram_addr <= ram_addr+4; current_state <= done_addr; led_cnt <= led_cnt + 1; END IF; ELSE current_state <= wait_done; END IF; WHEN done_addr => IF (shift_dne(shift_dne'high-1) = '1') THEN current_state <= addr_out; END IF; WHEN wait_done => IF (shift_dne(shift_dne'high-1) = '1') THEN load_shr <='1'; pix_cnt <= pix_cnt + 1; current_state <= count; END IF; WHEN addr_out => addrb <= std_logic_vector(to_unsigned(ram_addr,32)); enb<='1'; current_state <= wait2; WHEN wait2 => current_state <= grab; prev_state <= wait2; WHEN grab => pixel <= doutb(doutb'high-8 DOWNTO doutb'low); load_shr <= '1'; current_state <= wait_done; pix_cnt <=0; WHEN reset => pix_cnt <= 0; led_cnt <= 0; IF rst_cnt = (reset_duration-1) THEN current_state <= idle; ram_addr <= 0; ELSE rst_cnt <= rst_cnt + 1; END IF; END CASE; END IF; END PROCESS; shr_op : PROCESS(clk) BEGIN IF rising_edge(clk) THEN IF load_shr ='1' THEN shift_dne <= done; IF pixel((numb_pixels-1)-pix_cnt) = '1' THEN shift_reg <= one; ELSE shift_reg <= zero; END IF; ELSE shift_reg <= shift_reg(shift_reg'high-1 DOWNTO shift_reg'low) & '0'; shift_dne <= shift_dne(shift_dne'high-1 DOWNTO shift_reg'low) & '0'; END IF; END IF; END PROCESS; dout <= shift_reg(shift_reg'high); END ARCHITECTURE;

When the timing was tested on hardware with an oscilloscope, the output timing for a one and zero are as shown below

Outputting a One

Outputting a Zero

Hardware Design

Once the RTL file has been created the next step is to use it in the design, for which we use Vivado. Within Vivado we can add in the following elements

Zynq Processing System - Configures the arm A9

AXI Bram Control - Allows the A9 to read and write the BRAM

Block Memory - Stores the NeoPixel values

AXI Smart Connect - Provides the necessary AXI Connections

Reset Block - Controls resets for the system

Hardware Design

The hardware design will target the MiniZed development board from Avnet. This contains a single core Zynq 7007 device.

To connect the NeoPixel to the MiniZed the design uses Pmod1 and a Pmod CON1 to connect the serial input and ground. As the NeoPixels can have a significant power consumption a external power supply is used to power the pixel array.

Each of the individual LEDs in a NeoPixel requires a maximum current of 20mA. There are three LED’s in each NeoPixel, hence each RGB pixel requires 60mA.

As we have potentially up to 256 NeoPixels powered at one we need to have a considerable power supply. With all LED's showing displaying just one color we would need a power supply solution capable of providing 5.12 Amps while if we want to use multiple LEDs we will need to be able to supply up to 15.36 Amps (which is considerable)

Software Design

The software design is modular and works on a defined grid to which we can add in initial states. e.g. Glider or Exploder.

I have in mind that we can select the pattern over a serial port (eventually wireless and Linux but I need to update the MiniZed BSP which we will do in a project soon)

The architecture is this based on the following functions

next_generation() - This creates the next evolution of the game

update() - Updates the next evolution into the currently displayed generation

load_ram() - This loads the current generation into the NeoPixel display.

To ensure we can observe the updates to each generation I timed the design to generate a new evolution about once every 2.5 seconds

The code to determine the next generation works in two for loops one for each dimension of the array. While inside the inner loop, another two loops are used to determine the number of pixels which are alive near the currently examined pixel.

All of this is performed within the next generation function, alive of dead bits are represented by a single bit within the 2D array.

void next_generation() { for (int l = 1; l < max_x-1; l++) //height was 1 to max-1 { for (int m = 1; m < max_y-1; m++) //width { int alive = 0; for (int i = -1; i <= 1; i++){ for (int j = -1; j <= 1; j++){ alive += present[l + i][m + j]; } } alive -= present[l][m]; if ((present[l][m] == 1) && (alive < 2)) future[l][m] = 0; else if ((present[l][m] == 1) && (alive > 3)) future[l][m] = 0; else if ((present[l][m] == 0) && (alive == 3)) future[l][m] = 1; else future[l][m] = present[l][m]; } } }

It is the load BRAM function which assigns the colors to the pixels in the NeoPixel Array.

void load_ram() { int ram_addr = 0x4; int read_out =0; u32 out; ram_addr = 0x4; read_out = 0; for (int l = 0; l < max_x; l++) //height { for (int m = 0; m < max_y; m++)//width { if((l < 8)&& (m <8)) { if( present[l][m] == 1) { out = 0x00000f; ram_addr = (((l*8)+m)*4)+4; XBram_WriteReg(XPAR_AXI_BRAM_CTRL_0_S_AXI_BASEADDR, ram_addr, out);//present[l][m]); } else{ out = 0x000f00; ram_addr = (((l*8)+m)*4)+4; XBram_WriteReg(XPAR_AXI_BRAM_CTRL_0_S_AXI_BASEADDR, ram_addr, out); } } if((l < 8)&& (m >7)) { if( present[l][m] == 1) { out = 0x00000f; ram_addr = ((256)+((l*8)+(m-8))*4)+4; XBram_WriteReg(XPAR_AXI_BRAM_CTRL_0_S_AXI_BASEADDR, ram_addr, out);//present[l][m]); } else{ out = 0x000f00; ram_addr = ((256)+((l*8)+(m-8))*4)+4; XBram_WriteReg(XPAR_AXI_BRAM_CTRL_0_S_AXI_BASEADDR, ram_addr, out); } } if((l > 7)&& (m >7)) { if( present[l][m] == 1) { out = 0x00000f; ram_addr = ((256)+((l*8)+(m-8))*4)+4; XBram_WriteReg(XPAR_AXI_BRAM_CTRL_0_S_AXI_BASEADDR, ram_addr, out);//present[l][m]); } else{ out = 0x000f00; ram_addr = ((256)+((l*8)+(m-8))*4)+4; XBram_WriteReg(XPAR_AXI_BRAM_CTRL_0_S_AXI_BASEADDR, ram_addr, out); } } if((l > 7)&& (m <8)) { if( present[l][m] == 1) { out = 0x00000f; ram_addr = ((512)+((l*8)+(m))*4)+4; XBram_WriteReg(XPAR_AXI_BRAM_CTRL_0_S_AXI_BASEADDR, ram_addr, out);//present[l][m]); } else out = 0x000f00; ram_addr = ((512)+((l*8)+(m))*4)+4; XBram_WriteReg(XPAR_AXI_BRAM_CTRL_0_S_AXI_BASEADDR, ram_addr, out); } } } //enable the data XBram_WriteReg(XPAR_AXI_BRAM_CTRL_0_S_AXI_BASEADDR, 0, (u32)numb_pixels); }

Building the hardware

The design has neo pixel panels to integrate, giving us a total of 256 pixels to create our game of life. Each NeoPixel array has a Data In and Data Out connections, these enable us to daisy chain the NeoPixel arrays together.

Assembling the Neo Pixel Panels

Testing

Once all four panels were connected together, the next step was to ensure the power architecture is sufficient. For testing I used a bench power supply capable of providing the current required to power the NeoPixel arrays while the MiniZed is powered by the USB connector. To ensure the data signal is correctly received the the ground reference is connected between the Pmod CON1 and the bench PSU.

PSU powering the NeoPixel with the first Glider

Calibration

Once the four panels are constructed I wanted to be able to check that my mapping to each of the NeoPixels in software was correct. To do this in the initial grid I set the outer four corners to be on, along with the middle four LEDS. At the same time each panel had a Blinker running it.

Checking correct LED positioning

When I put this all together I recorded the video below

Glider

Glider moving across the NeoPixel Array

Oscillator

Game of Life Oscillator

Mounting

To mount the four NeoPixel arrays in a grid I will use a 3D printer to create a simple, frame which will hold the four Neo Pixel Arrays. However first I need to buy the printer!

Further Work - Control the MiniZed over Wifi for the initial state

You can find the files associated with this project here:

https://github.com/ATaylorCEngFIET/Hackster

See previous projects here.

More on on Xilinx using FPGA development weekly at MicroZed Chronicles.