UART RX and Audio Download
MIT Fall 2024
Please Log In for full access to the web site.
Note that this link will take you to an external site (https://shimmer.mit.edu) to authenticate, and then you will be redirected back to this page.
Overview
Now we've built a receiver for UART as well, which means that, using the micro-USB cable and the our UART RX, we can send data we have on our computers to the FPGA! Let's keep working with audio data and build a system that can send
Since we're sending a decently large quantity of data, and we don't know exactly how quickly our Python script will deliver messages, we'll also need to use a new memory component on our FGPAs: Block RAM memory.
Diagram
Files
To get going with your audio download system, keep using the same lab setup you had for the first checkoff, but make sure you have a couple more files included in your project directories.
ctrl/
test_ports.py
: If you still need it, the port finding script used in the first checkoff.send_wav.py
: Script to run to send the audio samples from a .wav file to the FPGA over UART.secret_revealed.wav
: A sample .wav file to try playing out of your FPGA audio jack!sine.wav
: A sample .wav file of a 440Hz sine wave to try playing out of your FPGA audio jack, for testing purposes
hdl/
xilinx_true_dual_port_read_first_2_clock_ram.v
: Module to instantiate a BRAM block: no need to touch it.uart_receive.sv
: from the exercise you just completed!
Uncomment the section of top_level.sv
for checkoff 2 to get started!
UART Receiver
In order to start receiving data from our computers, we'll need to setup a receiver module in our FPGA fabric. Instantiate the uart_receive
module you just wrote and tested in the last exercise, connected to the uart_rxd
wire coming from our computer (via translation from the FT2232, like we talked about in checkoff 1!) Be sure to choose a BAUD rate again, and take note of it so you ensure you're using the same one when you later run your Python script to send data!
Avoiding metastability
In order to reliably read data over UART, pass your uart_rxd
wire through a couple of buffer registers, and only use the output of those buffers when running your UART receiver module. We talked about this at the start of lecture 5. This is very important to avoid random transmission of false bytes. Do not skip this.
BRAM Memory
Our UART receiver is going to be receiving a long sequence of 8-bit audio samples, and our goal in this system is to store those samples in some kind of memory, and then play back all the audio samples on loop on the line out output!
Now you may want to just make a gigundous logic array, but it turns out that such an entity is likely not going to be synthesizable in a way that you want. If there's one thing that you take away from 6.205 is that when it comes to memory, there is large, cheap, and fast, and you can only really ever have two of these. We need a lot of memory to store our audio samples. Even just one second of audio at 8ksps is going to need 64,000 bits. You can go ahead and try to synthesize that using a construct like logic [7:0] thing [7999:0]
, but it'll end up using a lot of resources on the FPGA and likely make it very hard, if not impossible, to meet timing. You may indeed be able to make a simulation work with them, but real life will be less kind. The reason for this is you're using tons and tons of flip flops to store bits for one large data structure, when they're really meant to be storing lots of little things all over the place. Synchronizing tons of them for the purpose of making a monolithic memory will make designs very hard to fit in the FPGA.
All is not hopeless, though. Instead, the FPGA has special memory modules that exist specifically for storing relatively large chunks of data. They are called Block RAMs (BRAMs). We'll talk more about them as the class goes on. As a brief overview, though:
The BRAM units on our FPGA are able to run with 2 ports: each port has a set of inputs and outputs to request a memory address, write data in, and read data out.1 For our uses, we'll use the two ports to serve the two places where we need to access our audio memory:
- port A will be used request memory addresses and read out data to be played by our PWM audio player; so we'll need to set our
addra
port to define what memory we want, and ourdouta
port will provide us with data.2wea
will be held low, since we're not writing any data and we don't want to write enable port a, andena
will be high because we do want to enable the port overall. - port B, will be used to write audio samples to memory as they come in from the UART receiver. We'll need to set our
addrb
port to dictate where our data will be written, and we'll specify the data we need to write withdinb
. We'll write enable port b by pullingweb
high, and otherwise configure the control wires the same as port a.
BRAM is very powerful and has more complexity than we're going to really give it credit for in this lab, and we'll be exploring it in much greater depth in the coming weeks, but for now we'll just provide you with the instantiation of it, and it'll be your job to set the data and address wires appropriately to access the memory.
Memory addressing with an event counter
For both of the memory ports, we're always going to want to access memory in order, before perhaps looping back around to address 0. That sure sounds like some counter modules we've already written! Last week, you created an evt_counter
to count single-cycle high events as they happen; back then it was for counting the rising edges of button clicks, but we have other single-cycle high signals we can use in our design this week to address into memory!
- for port A (memory read -> audio output): once every 1/8000th of a second, you want to transition to reading the next memory address, and your
spi_trigger
already creates the 8kHz trigger you'd need to make yourevt_counter
generate the address changes you want to see! - for port B (uart receiver -> memory write): each time you receive a new byte on UART, you want to increment the address you write to by 1, so that a sequence of bytes all get written to adjacent memory addresses in your BRAM. Your
uart_receive
module provides a single-cycle highnew_data_out
signal each time it receives a byte, which you can use as the event for this secondevt_counter
There is one extra parameter we need to add to our evt_counter
in order to use it the way we want to this week: we need a MAX_COUNT
parameter for our counter. We need our evt_counter
to check for a MAX_COUNT
and loop back around to 0. Modify your evt_counter
to use this parameter, and then when you instantiate your evt_counter
s in the top level for controlling an address, set the MAX_COUNT
to be equal to our BRAM_DEPTH
parameter, which is the number of different memory addresses we have in the BRAM we just initialized.
Configure your evt_counter
s to provide you with helpful addresses to pass into each port of the BRAM, and connect them to the addra
and addrb
ports provided. Then, connect the the douta
and dinb
data ports to the appropriate places to pipe data into and out of your BRAM. The output data from your memory should be played by the speaker via PWM encoding, replacing the old connection to the microphone input from checkoff 1.
Writing from the computer
Just like from the first checkoff, we'll use a Python script utilizing pyserial
to communicate over UART! The script you want to use this time will be sending data from the computer, so that it can be received by our uart_receiver
module and make its way to the memory and audio playback. Download the send_wav.py
script for your ctrl/
directory, and be sure to set your BAUD rate to match that of your uart_receiver
If your port configuration isn't done, reference checkoff 1 to get it up and running! Set your serial port name to be the value you found from the tester script when getting your ports running. Run the script as python3 send_wav.py <audio_filename>.wav
.
What .wav
files will work?
A .wav
file stores a set of raw audio samples with no compression, which is incredibly convenient for the way we're trying to play out our audio data on the FPGA! But there are some settings that need to match the way we're playing things out; our audio samples need to be 8-bit values, sampled at 8kHz, with only one audio channel (no left and right) in order to get things working. Most modern audio files are not going to use such low-quality settings, but any audio file can be resampled/re-encoded to match these settings using a tool like ffmpeg
(or probably some sketchy online website will do it for you too)! Importantly, we also only have 5 seconds of audio memory available in our BRAM, so anything too long may start overwriting your audio memory.
An example ffmpeg command might look like:
ffmpeg -i <input_file>.wav -ar 8000 -ac 1 -acodec pcm_u8 -t 00:00:05 <output_file.wav>
All of the files provided above match the necessary encoding to play back properly, but if you want to run your own audio snippets, you can use tools that let you make your own!
Build your design and flash it to the board, and then run the Python script! If all goes well, you should be able to hear the audio snippet you've sent over UART playing out of your speakers!
Crackly Audio?
If you're getting audio in that sounds kinda correct, but is crackly, it would probably be a good idea to recheck how your UART Receive module is working. One possible cause of crackling could be you're receiving false bytes in. The checking of of the start and stop bits really should involve robust verification of both. If that's the case we'd recommend:
- Checking that the line stays low from the falling edge of your start bit up until 0.5 \times BAUD (not just at 0.5 \times BAUD).
- Checking that the line stays high from the midpoint (0.5 \times BAUD) of your stop bit up until about 0.75\times BAUD.
This can really help to throw out any spurious pieces of garbage data that don't pass the start/stop bit requirement. (it is quite easy to randomly get lucky and have a single low and single high measurement 9 BAUD apart, but much harder to randomly have those two signals be of any legit size).
Audio Not Playing At All?
If you're getting no audio, it might be a good idea to use the onboard LEDs to help debug. Some ideas could be to put your addra
or addrb
values onto the display. You'll never see addra
show up as anything other than a blur of all LEDs since it is counting up 8000 times per second, but you could grab and hold single values of it with a button or switch and use that to make sure it is changing during playback. Also during a download, watching addrb
grow, could be a useful thing to verify.
Show a staff member your system sending an audio file down to the FPGA and playing back out of your headphone jack!
Footnotes
1They also can operate on different clocks! That's not important now, but it'll be pretty cool in the future.
2In the future, we'll care about the fact that BRAM doesn't return a read data value on the cycle you request it; the data response for a given address request comes two cycles later. For our purposes right now, we're reading every value for more than a thousand cycles, so we don't have to worry too much about that, but it's good to remember!