For my Path to Programmable project, I wanted to build something that would use both the PS & PL of the Zynq-7000.
The original plan was to build a logic analyzer that used BRAM & DRAM as a sample buffer, but I realized that I might not get it to work before the deadline.
In search of something simple, I decided to build a WS2812 controller.
The WS2812 RGB LED uses a single wire serial protocol that infers '1's and '0's by varying the width of a pulse. WS2812s can be driven from a microcontroller, but most implementations that do not use DMA take up a lot of CPU time because even if a single LED in the string needs to be updated, the entire strip needs to be refreshed - so unless the data is stored in a buffer, data for the entire strip needs to be generated.
My project moves the serial output generation & refresh to the PL. The idea is that software in the PL simply writes 'data' to an 'address' in the BRAM (which corresponds to the RGB data of a LED of a given index), and the logic in the PL independently iterates through all the data in the BRAM using the other port, refreshing the LEDs.
As shown, the main parts are the application that runs in the PS, the AXI4-Lite interface, a custom AXI to BRAM interface, dual port BRAM, a custom BRAM to RGB interface and a RGB to WS2812 generator. I could have used memory mapped BRAM using the Xilinx AXI-BRAM IP, but decided to build a custom interface that uses registers.
I used Markus Koch's WS2812b VHDL controller - which only needs to be supplied with RGB data and handles the pulse generation for the serial output.
I also wired up the PL switch on the MiniZed to an AXI4-Lite register, which allows software in the PS to read the state of the switch.
The AXI to BRAM interface is currently very simple: it simply uses the address and data from the AXI4 registers to write to the BRAM. I plan on modifying this so that I can set bits in the control register to control all these operations much better.
axi_proc: process (clk) begin if rst = '1' then bram_data_rgb <= (others=> '0'); bram_addr_rgb <= (others=> '0'); elsif rising_edge(clk) then bram_data_rgb <= i_data_rgb (23 downto 0); -- write data to the BRAM bram_addr_rgb <= i_addr_rgb (4 downto 0); -- at this address bram_write_en <= '1'; end if; end process;
The code for BRAM to RGB interface is more complex and uses FSM to control everything. It goes through a couple of states which involve fetching data from the BRAM, sending it to the WS2812 PHY, waiting for the PHY to signal that it's ready for the next set of data and then incrementing the address or sending the longer 'latch' pulse depending on whether the all the LEDs in the string have been refreshed.
rgb_phy: process (clk) begin if reset = '1' then bram_rd_addr <= (others=> '0'); --bram_rd_data <= (others=> '0'); elsif rising_edge(clk) then CASE State IS when S_GET_BRAM => bram_rd_addr <= LED_Addr_Count; -- send an address to the BRAM State <= S_SEND_TO_PHY1; when S_SEND_TO_PHY1 => pixData_red <= bram_rd_data (23 downto 16); --map the data from the BRAM to the WS2812 Controller inputs pixData_green <= bram_rd_data (15 downto 8); pixData_blue <= bram_rd_data (7 downto 0); State <= S_SEND_TO_PHY2; when S_SEND_TO_PHY2 => pixData_valid <= '1'; -- latch the data (this is required by the WS2812 controller State <= S_WAIT_PHY_ACK; WHEN S_WAIT_PHY_ACK => if (pixData_next = '1') then --wait for the WS2812 PHY to assert its ready signal before sending more data State <= S_NEXT_DAT; end if; WHEN S_NEXT_DAT=> if (LED_Addr_Count = "11111") then --if all the LEDs have been refreshed (32 total) pixData_valid <= '0'; --de-assert the 'valid' signal (as required by the WS2812 PHY) LED_Addr_Count <= "00000"; --reset the counter State <= S_LATCH_LED; --move to the state that sends a long pulse to latch the LEDs else LED_Addr_Count <= LED_Addr_Count + '1'; --else, increment the LED address count State <= S_GET_BRAM; end if; WHEN S_LATCH_LED => pixData_valid <= '0'; --to latch the LEDs, de-assert the valid control signal if (pixData_next = '1') then --wait for the PHY to respond State <= S_GET_BRAM; end if; WHEN others => sFSM_State <= "00110"; State <= S_GET_BRAM; END CASE; end if; end process;
{gallery} WS2812 Controller IP |
---|
BRAM configuration |
IP Schematic |
WS2812 Controller |
The WS2812 controller (PHY, BRAM and AXI controllers) were packaged as IP, and imported into the main block design.
Coming to the software in Xilinx SDK:
These pointers map to the AXI4-Lite registers that are connected to the design in the PL.
//Register 0 is the LED Address Xuint32 *LED_ADDRESS_REGISTER = ((Xuint32 *)XPAR_WS2812_IP_0_S00_AXI_BASEADDR+0); //Register 1 is for the LED RGB Data Xuint32 *LED_RGB_DATA_REGISTER = ((Xuint32 *)XPAR_WS2812_IP_0_S00_AXI_BASEADDR+1); //Register 2 is a Control Register - currently unused Xuint32 *LED_CONTROL_REGISTER = ((Xuint32 *)XPAR_WS2812_IP_0_S00_AXI_BASEADDR+2); /* Register 3 is a 'response' register i.e. read so that PL cand send data to the PS. Currently, the 0th bit is mapped to the PL switch. */ Xuint32 *LED_RESPONSE_REGISTER = ((Xuint32 *)XPAR_WS2812_IP_0_S00_AXI_BASEADDR+3);
To write to the BRAM, write an address, followed by the data.
/* *Give this function the address & RGB data, and it will write it to the BRAM *Note that only the 24 lower bits are mapped i.e. 0xXXRRGGBB, X = don't care. */ void writeLED (Xuint32 address, Xuint32 data) { if (address<0) { return; } *LED_ADDRESS_REGISTER = address; *LED_RGB_DATA_REGISTER = data; }
For example:
writeLED(0x00000001,0x00000000); //LED1 is OFF writeLED(0x00000002,0x00ffffff); //LED2 is white writeLED(0x00000003,0x00ff0000); //LED3 is red writeLED(0x00000004,0x0000ff00); //LED4 is green writeLED(0x00000005,0x000000ff); //LED5 is blue
To generate a pattern, write code that defines the colors of each LED.
Note that since the refresh is performed by hardware in the PL and uses data from the BRAM buffer, application code only needs to update the data in the BRAM for the positions that need to be modified.
This example also reads the position of the PL switch, which is used to pause the animation.
void CylonEyes() { Xuint32 RED = 0x009f0000; Xuint32 RED_HALF = 0x001f0000; Xuint32 OFF = 0x00000000; Xuint32 PLSW; uint i = 1; for (i=0; i<13;i++) { //Read and mask the PL switch PLSW = ((*LED_RESPONSE_REGISTER) & 0x00000001); //If the PL switch is off, the animation pauses. if(PLSW != 1) { i--; //undo the increment to stop the animation continue; } //RED gradient for the LEDs writeLED(i-2,OFF); writeLED(i-1,RED_HALF); writeLED(i,RED); writeLED(i+1,RED_HALF); writeLED(i+2,OFF); //75msec delay usleep_A9(75000); } for (i=13; i>0;i--) { //Read and mask the PL switch PLSW = ((*LED_RESPONSE_REGISTER) & 0x00000001); //If the PL switch is off, the animation pauses. if(PLSW != 1) { i++; continue; } writeLED(i-2,OFF); writeLED(i-1,RED_HALF); writeLED(i,RED); writeLED(i+1,RED_HALF); writeLED(i+2,OFF); usleep_A9(75000); } }
Here's what it looks like (I made a mistake with the video, which is why it stutters).
In the future, this will be merged with a HDMI receiver to build something like an Ambilight. Most DIY implementations require installation of software on a computer, but the FPGA implementation allows it to work with any HDMI source. The plan is to to implement HDMI RX and a WS2812 controller in hardware, and to use the PS for configuration (LED spacing, display dimensions etc.)
Top Comments