Intro: LED Music Visualizer With Zybo Board
This tutorial will describe how to make an LED Music Visualizer using the Zybo Zynq 7000 Development board from Xilinx. The Zybo is pretty awesome because it has both an FPGA and an ARM Processor on the board. Because of this, custom hardware accelerators can be designed on the FPGA to aid the processor.
The basic idea for this project is to take in audio data from the speakers, convert it to digital data, process it, and change the LEDs on an LED Matrix depending on the data in the audio signal.
The digital audio data will be processed using a Fast Fourier Transform in order to parse out the frequencies from the audio signal. The frequencies in the signal will be what determines what is shown on the LED Matrix.
The necessary components for this project are:
- A 3 pin male to male header for the LED matrix
- A 1000uF capacitor
- Male to male wires
This tutorial also assumes that you have the Vivado IDE and the Zybo drivers already installed. There's plenty of good information out there on how to do this if you don't!
Also, please make sure to download the attached files, as they will be necessary to complete the project!
Step 1: Basic Setup and Adding the Zynq IP Block
Go ahead and create a new Vivado project. Choose what to name the project and where to place it. Click next.
Click the parts button and then type xc7z010clg400-1 in the search box. It should be the only result. Click it and press next. Then click finish.
Vivado should open the new project now. Click on “Create Block Design” under the IP Integrator section on the leftmost side of the screen. A window should pop up. Type whatever you want to name the project in the box then click “OK”.
A green bar should now be visible in vivado. Click the hyperlinked “Add IP” text and type in “Zynq”. Double click “ZYNQ7 Processing System”. A new block will be added to the system. Double click it because we need to add some things.
Step 2: Configuring the Zynq Block
Now click on the “MIO Configuration” tab on the left side of the screen. Expand “I/O Peripherals”. Check both “SD 0”, “ENET 0”, and “UART 1”. Then expand “GPIO” within that. Check the “GPIO MIO” box. Some other boxes will checked as well which is ok. Then expand “Application Processor Unit” and check “Timer 0” and “Watchdog”.
Click on the "Clock Configuration" tab on the left side of the screen. Expand "PL Fabric Clocks". Make sure "FCLK_CLK0" is selected and that its value is 100 MHz.
Now click on the “Interrupts” tab on the left side of the screen. Click the “Fabric Interrupts” box. Then expand “PL-PS” and check the “IRQ_F2P[15:0]” box. This will be used later in the project to generate interrupts for the processor from the FPGA. Go ahead and click “OK” at the bottom right of the window.
Step 3: Adding GPIO Blocks
Now we’re going to add some “GPIO” blocks to the system. GPIO is nice because it allows us to easily send small amounts of information from FPGA peripherals to the processor or vice versa. For larger amounts of data AXI is a better option. Go ahead and right click some white space on the Block Design canvas and click “Add IP”. Type in GPIO and double click “AXI GPIO”. We’re gonna need five of these blocks total, so go ahead and add four more. The result should look like the screenshot below.
Four of the GPIO blocks will need to be what is called “dual channel”. This just means that each GPIO block can accept/send two different pieces of data. Double click the GPIO 0 block. A window should pop up with a bunch of options. Check the “Enable Dual Channel” box. Then check “All Inputs” for both channels. Make the width of the first GPIO 1 and the width of the second GPIO 24. Also, check the “Enable Interrupt” box at the bottom of the screen. Then click OK.
Now hover over the little block attached to the “GPIO” label on the GPIO 0 block. A little pencil should appear as your cursor. Then right click and select “Make External”. Do this for the “GPIO2” label as well. Double click the GPIO port that was created and rename it to “LDATA_RDY”. Then double click the GPIO2 port that was created and name it “LDATA”. This GPIO will be used to read data from the audio codec as well as to see when data is ready.
Now Click on the little black line coming out of “ip2intc_irpt” and drag it to “IRQ_F2P[0:0]” on the ZYNQ block.
Repeat the previous process with the GPIO 1 block except do NOT click “Dual Channel”, click “All Outputs” instead of “All Inputs”, make the channel width 4, and do NOT click “Enable Interrupt”. Create an external port again and name it “SWITCHES”. This GPIO will be used to determine what is displayed on the LED matrix.
For GPIO 2, enable dual channel and make both channels “All Outputs”. The first channel should be 7 bits wide and the second channel should be 9 bits wide. Do NOT enable interrupts. Make both ports external. Name the first one “Addr” and the second one “data”. This GPIO will be used to configure the audio codec on the zybo board.
For GPIO 3, enable dual channel, make the first channel “All Inputs”, and make the second channel “All Outputs”. Both channels should have a bit width of 1. Do NOT enable interrupts. Create external ports for both channels. Name the first one “ready” and the second one “reset”. This GPIO is involved with configuring the audio codec.
Finally, for GPIO 4, enable dual channel and make the first and second channel “All Outputs”. Make the channel width 1 for both. Do NOT check enable interrupts. Make the ports external and name the first one “timer_GPIO” and the second “gpio_rtl”. This GPIO is used merely to facilitate testing.
Step 4: Running Connection Automation for GPIO
Now click “Run Automation” at the top and click OK. Then click “Run Connection Automation”, select all available boxes, then click OK. That might take a while.
A bunch of wires and two new blocks should have appeared on the screen. You may find it helpful to right click on some white space and select “Regenerate Layout”. That cleans things up quite nicely!
Step 5: Adding the AXI Fifo Block
OK, now we need to add something called an AXI fifo block. This is a queue that will be used when the software needs to write LED information to the LED hardware block. Right click on some white space and click “Add IP” again. This time type in “FIFO” and select the “AXI Stream FIFO”. Double click on the newly added block and a configuration window will pop up. Go ahead and deselect “Enable Transmit Control” and “Enable Receive Data”. Click OK and then click “Run Connection Automation” again. Right click the AXI_STR_TXD port and select “Make External”. You can just leave the name as is.
Step 6: Add New Clock Source Block
We need to create a 12.288 MHz clock source for the audio codec. Right click and some white space, click “Add IP”, and type in “clock”. Select “Clocking Wizard”. Double click the newly created block. Click the “Output Clocks” tab and change the requested frequency to 12.288 MHz then click ok. Hover over the black line coming out of the FCLK_CLK0 port on the ZYNQ block and drag it to the clk_in1 port of the clocking wizard block. Right click on the clk_out1 port and select “Make External”. Name the new port “clk_audio_master”. We’re going to need to create another clock port as well that is just the main clock. Right click on the white space again, and select “create port”. Name it “clk”, change the direction to output, and click ok. Move your mouse over the port and drag to “FCLK_CLK0” on the ZYNQ block.
Step 7: Run Connection Automation for the Clock Source
Finally, click run connection automation at the top. You're block diagram should look like the picture above.
Step 8: Generate the HDL Wrapper
Now click the Sources tab in the Sources pane. Right click on the “LEDVisualizer.bd” file under “Design Sources” and select “Create HDL Wrapper”. Go ahead and leave “Let Vivado Manage Wrapper and Auto-Update” checked. If you have problems you can try checking the other one. This can potentially take a while. The board portion of the project is now complete. Onto some custom Verilog!
Step 9: Adding Custom Verilog Modules
We are going to be adding three custom verilog modules. I will give a brief overview of each them.
The first module we’ll need is an I2S driver. I2S is a communication protocol for sending and receiving audio data. The Audio Codec we’ll be using to convert analog audio signals to digital ones uses this protocol. We’re going to be using a 48 KHz sample rate, which will require the “mclk” signal on the audio chip to run at 12.288 MHz. The “bclk” signal will be running at mclk/4. The “lrclk” signal needs to be running at 48 KHz since it is the sample clock. Once we have these signals running and the audio codec configured (which we will get to shortly), data will come out of the recdata pin on the chip. I’m not going to go into great detail about I2S here since there’s a lot of good stuff on the internet, but the picture above demonstrates what the clocks should look like. Also, for more information on the codec go to the link below and look at the data sheet.
The next module we’ll need to add is the configuration module. The audio codec is configured over I2C. I2C is typically used to attach ICs (such as the audio codec) to processors. See the picture above for an example of an I2C write with the main address as ‘00011010’, the register address of ‘0000100’, and the data value of ‘000001010’. I will again refer you to the audio codec data sheet if you want more information on I2C. There is a pretty thorough explanation in there.
The final custom verilog module we’ll need to add is a module to write data to the LED matrix. The LED matrix is a one wire interface. Writing data to it consists of altering the duty cycle with some very tight timing constraints. You write data for the first LED, then the second, etc. Again, I will refer you to the datasheet:
Anyway, let’s go ahead and add the files. Make sure you’ve downloaded the provided files. Go to file and select “Add Sources”. Select “Add or create design sources” and click next. Click add files and navigate to where you downloaded the provided files. Select “AudioCodec.v”, “AudioCodecSetup.v”, and “fsm.v”. Then click finish.
Step 10: Editing the HDL Wrapper
Now double click on “LEDVisualizer_wrapper” (the first part will change depending on what you named your design). We need to make some changes here in order to include our custom verilog in the design. And as a side note, you would typically want to make custom IP to do this. I haven’t here because when I tried doing that it was somewhat of a pain.
First, comment out the four signals starting with “AXI” at the top of the “module” section. Scroll to the bottom of the module section and comment out everything after “FIXED_IO_ps_srstb”. Then we need to add some signals in the section with input/output at the start. Look at the pictures above to see which ones. Now comment out the four “AXI” input/output signals. Now comment out all input/output signals after “FIXED_IO_ps_srstb”. Then add the signals shown in the picture above. Now add the wires shown in the picture above to the wires section. DO NOT comment out any wire signals.
We now need to let the wrapper know that we want to incorporate our custom Verilog into the system. We do this by defining instances of our Verilog modules in the wrapper. Go ahead and copy the instances of fsm, AudioSetup, and AudioCodecSetup from the provided wrapper file and paste them into your own (or just copy the whole wrapper file from the one that has been provided).
Finally, just keep in mind that if you named something differently then I did you may get errors when trying to generate the bitstream. Keep an eye out for that!
After you save the file you should notice that all of the module are now under the wrapper files in the Sources pane.
Step 11: Adding and Editing the Constraints File
Now we need to add what is called a constraints file. This file maps verilog signals to actual pins on the board. Make sure you’ve downloaded the provided files folder then go to file and select “Add Sources” as we did before. This time, however, select “Add or create constraints”, not design sources. Click add files then navigate to the folder where you downloaded the provided files and select “ZYBO_Master.xdc”. Then click finish. There should be a new file under the constraints->constrs_1 folder in the sources pane. Double click on “ZYBO_Master.xdc”. Find the section that starts with “##switches” and uncomment all of the lines that start with “set_property” in this section. You should notice that the names after the “get_ports” statements do not match up with what the name of the switches signals that are in the wrapper file. Therefore, change the names from “sw” to “switches_tri_o”. The indexes can stay the same.
Next, find the section that is called “##I2S Audio Codec”. Uncomment all of the set property lines in this section. You should not need to change the names.
Step 12: Editing the Constraints File, Continued
Next, find the section right below the audio codec section called “##Audio Codec/external EEPROM IIC bus”. Uncomment all of the set_property lines in this section.
Finally, find the section labeled “Pmod Header JE”. Uncomment all the “set_property” lines in this section as well. You do not need to change the names.
Step 13: Generating the Bitstream and Exporting the Hardware
Now we need to generate the bitstream for the project. Go to Flow and select “Generate Bitstream”. This can take upwards of ten years on an old single core laptop like mine, so be patient.
After that is complete and you see “write_bitstream Complete” at the top right of the screen, go to file->Export->Export Hardware. A window should pop up. Check the “Include Bitstream” box and hit ok. Then go to file and select “Launch SDK”. The default values should be fine so hit ok.
You’re now done with the Vivado portion of the project!
Step 14: Setting Up the SDK and Adding Files
Go to File and select “Import” in the SDK. A window should pop up. Select General->Existing Projects Into Workspace. Navigate to wherever you extracted the copy of FreeRTOS that was in the provided files. Go into FreeRTOSV8.2.1->FreeRTOS->Demo->CORTEX_A9_Zynq_ZC702. Click OK. Then uncheck RTOSDemo_bsp and ZC702_hw_platform. Then Click Finish.
Now we need to make a board support package based on the hardware from Vivado. Go to File->New->Board Support Package. Name the board support package “RTOSDemo_bsp”. Click Finish. Another window should come up but just hit OK.
Also, for whatever reason there seems to currently be an issue with the file “FreeRTOS_tick_config.c”. Navigate to this file by going to RTOSDemo->src->FreeRTOS_tick_config.c. Navigate to the line ‘#include “Task.h”’. Change the uppercase T in “Task.h” to a lowercase t “task.h”. Then press “ctrl-s” to save.
If you’re having any compilation issues, try right clicking on the RTOSDemo_bsp folder and clicking “Re-generate BSP Sources”. Then right click on the RTOSDemo folder and select “Change Referenced BSP” and reselect “RTOSDemo_bsp”.
Step 15: Adding New Code
Now what we’re going to do is just run our application under the RTOSDemo framework except replace the “main blinky” program code with our code. So Expand RTOSDemo->src->Blinky_demo. Then open “main_blinky.c”. Select everything in the file and delete it. Copy and paste the provided main_blinky code into the file instead.
Now this won’t compile as is. We need to make some build modifications first. Right click on RTOSDemo and scroll to the very bottom and select “Properties”. Expand the “C/C++ Build” tab on the left of the screen and select settings. Then select “Libraries” under “ARM gcc linker”. Click the little green + to add a library and type in “m”. Hit OK then OK again. The project will rebuild and should compile without errors (although there will be some warnings which you need not worry about).
The default size of the stack and heap on this system are pretty small so we’ll need to change that. Expand RTOSDemo->src and double click on “lscript.ld” Change both the stack and heap sizes to 0xF42400. This should provide plenty of space to work with. Hit “ctrl-s”.
Step 16: Setting Up the Debugger
We now need to setup the debug configuration for the system so we can actually run the code. Right click on RTOSDemo, expand “Debug As”, then select “Debug Configurations”.
Select the “Xilinx C/C++ application (System Debugger)” tab on the left side of the screen. Under “Target Setup”, check “Rest entire system” and make sure that “Program FPGA” is checked. Then select the application tab and check “Stop at program entry”. Then click apply. Don’t click debug yet, just close the window.
Step 17: Hooking Up the Hardware
At this point we’ll need to connect the LED matrix to the board. Connect the data line to PMOD JE7. You’ll likely have to solder some header pins to the board.
You’ll need to connect the ground and VCC pins to an appropriate 5V 10A power supply. A 2A supply likely won’t do the trick, unfortunately. You’ll also want to connect a 1000 uF cap across the VCC to ground terminal. This helps eliminate any dangerous power fluctuations that may harm the LEDs. Also, get an externally powered audio splitter and run one output to your speakers and one into the line in of the Zybo.
Step 18: Running the Code
If you set up your system as described in the previous system and connected the Zybo to your computer, we can go ahead and run the system!
Click the arrow to the right of the green bug at the top of vivado then select the debug configuration we made earlier. Click “Debug”. Press yes to all the windows that come up. Hit the green “play” button at the top of the screen until the code is running. The switches on the zybo will change the mode of operation. If switch one is up, the LEDs will behave like a spectrogram. If the second switch is the only switch up, the blue component will be from lower FFT bins, the red will be from middle bins, and the green will be from higher bins. If the third switch is the only up, the blue will be from lower bins, there will be no red, and the green will be from higher bins. Play around and see what happens!