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 StatesThe 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.
NeoPixelsNeoPixels 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.
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.
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.
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
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
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 DesignThe 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.
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.
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.
When I put this all together I recorded the video below
Glider
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.
Comments