Microphone UART Upload
testing, testing, 1,2,1,2
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
For this checkoff, you'll begin to use the UART protocol to communicate between your computer and your FGPA board! So far, we've written a UART transmitter, so we'll specifically transmit data from the FPGA to a python script running on your computer. To have some meaningful data to play with, we'll read the audio signal out from a MAX9814 microphone, using the same analog to digital converter we used last week, so that we can capture audio and record it into a .wav file to play on our computers!
Diagram
Files
Make a new lab directory and get started by creating the directory structure you're familiar with from the last couple labs: hdl
, xdc
, sim
, and obj
. Collect some files to get yourself started here:
hdl/
top_level.sv
: base top_level file to work off of for this labpwm.sv
from week 01: we'll repurpose it to reconstruct an audio signal. Make sure you are using a good version ofpwm
as we talked about in lecture with no division or mod. If you aren't doing that, this lab will 100% not work well.spi_con.sv
: from week 02: we'll keep using the MCP3008 over SPI.uart_transmit.sv
: from the exercise you just completed.
xdc/
top_level.xdc
: from the end of week 02, with the changes we made to label our SPI wires.
You'll need to also go in and uncomment the ports forspkl
,spkr
,uart_rxd
, anduart_txd
in the xdc file!
ctrl/
: a new directory for the computer-side scripts we'll use to talk to the board! Keep this out of the lab directory (do it one level above) so that way it doesn't get tangled with your FPGA builds.test_ports.py
: Run this script to find the proper port for communication with your FPGA board. See section below: Finding your serial port.save_wav.py
: Script to run to receive audio sent from the FPGA and write it to a .wav file.
An analog device: the MAX9814 microphone
Last week, we used our MCP3008 analog-to-digital converter to measure the analog voltage of some potentiometer knobs we were turning; but there's a whole world more of analog phenomena we could capture with an ADC! One thing that jumps to mind is audio. Audio is really just analog pressure waves in the air. We can use a MICROPHONE to convert the pressure waves into voltage waves of similar frequency and relative amplitude. The board we'll use for this is the MAX9814:
This is actually a pretty nice little setup. There's the microphone, which is the part that handles the actual pressure-to-voltage conversion. There's then a on-board amplifier that actually has what is known as "automatic gain control" (AGC) that will adjust its level so that the relative amplitude of the sound signals coming out stay about the same. This is very helpful when working with human speech since we change the amplitude of our voice quite rapidly and this can make clipping or low signals a problem. This amplifier will try (using feedback) to keep things somewhat even and therefore relatively easy to hear.
Human ears can hear up to about 20 kHz. In practice, unless you're listening to Mariah Carey or weird high frequency things, most of what you hear in human speech exists in the spectrum up to about 3kHz1. The rules and laws of signal processing (specifically The Nyquist-Shannon Sampling Theorem), require that in order to capture signals at frequencies f_s and below, we need to sample the signals at above 2\cdot f_s. That means to capture a decent chunk of human speech, we'd need to sample above 6000 samples per second (6 ksps). For good measure, we'll do 8 ksps since that is an industry standard.
Setting up the SPI Controller: for Audio
In order to capture 8ksps audio quality from our microphone, we'll need to capture a new message over SPI from our ADC once every 1/8000th of a second. We'll need to set up a single-cycle high trigger that fires at an 8kHz frequency.
counter
module from week 01 to count clock cycles in between; configure the counter module in your sample top_level
module to create the trigger you need.
Since we're connecting up the output voltage of our microphone to CH7
and we never need to read from any other channels, the messages we need to send to the MCP3008 are simpler than last week; it'll always be the exact same message! Based on how you requested channels last week, assign your spi_write_data
to be a constant value that will let you access the data of CH7
.
Finally, create an audio_sample
register to read the appropriate bits from your SPI result and capture 8-bit audio!2 You should only ever update the value in audio_sample
when your SPI controller indicated read data as valid.
Audio Out PWM
Now that we have an audio sample that's updating at 8ksps, we want to have a way to listen to the samples we're capturing, and make sure they're accurate! For that, we'll use the 3.5mm line out audio port, on the top-left of the boards. And just like when we were powering our on-board LEDs the last week, we're looking to take our digital signal and turn it into an analog output; last time, it was the brightness of the LED, and this time, it's the amplitude of the audio wave at a given moment in time. So once again, PWM is the tool of choice to create our analog signal!
When we used PWM for light, we were giving our eyes the "illusion" of different brightnesses via duty-cycling the LED on and off at a high frequency. High is relative. Eyes have a hard time seeing above 60 Hz (and are basically immune to frequencies above 100 Hz). This meant that as long as our PWM was flashing on and off at a frequency much faster than 100 Hz, we'd be able to keep the illusion going. That was easy to do.
Ears are tougher. They are sensitive to audio frequencies up to about 20 kHz. That means in order to give our ears the "illusion" of audio using PWM, we need to make sure the period of our "flashing" is fast enough that the frequency is still far beyond what our ears can hear.
As we can see, the frequency of our PWM wave is at least an order of magnitude faster than what human ears are sensitive to, which is good enough to maintain the illusion.3
So let's set up a pwm
module in our top-level to drive a PWM output, and listen to a reconstruction of the audio samples we just captured in our microphone.
WARNING: Protect Your Ears!
Before passing the ADC samples into your audio output, you'll want to reduce the scale of your values significantly! Otherwise, the volume of the microphone input will play incredibly loudly out of your headphones. Before passing the signal values into your PWM, divide them by about 8 (or shift right by 3 bits!)4 to get your audio output to not play so loudly. If it's too loud and your headphones are near your microphone, you will get some terrible speaker-microphone feedback. For good measure, when you first listen to your output, maybe don't put the headphones all the way into your ears...
With the stored audio samples from the audio above connected up to your PWM, you should be able to listen to audio output from your board's headphone jack! Build your design in Vivado and, if you need a pair of wired headphones to listen with, grab a pair from the front of lab5.
UART transmitter
Now that you know you've successfully captured some audio data, we can use the UART transmitter we just wrote to make that audio data escape FPGA-land and be accessible in a file on our computer!
The FTDI2232
As we've learned with the UART protocol, we only need two wires to communicate over UART. But it's not exactly like we can send two wires out of the pins on the side of our FPGA and plug plain old wires into our laptops: the only wires we have available to us are inside the micro-USB cable you've been using to write bitstreams to your board. That's where the FTDI2232 chip comes in: it's part of the Urbana dev board, and it handles all the translations necessary to communicate over USB, a much more complex protocol that we couldn't ask you to implement in lab6. It's already been handling all the translations necessary to write bitstreams to the FPGA fabric, and now we'll use the other half of its capability, to let us send UART messages to the chip and trust that all the necessary translations will happen so that the messages show up in Python shells in a moment!
Hold That Thought: Keeping Samples when UART is busy
Before communicating over UART, we'll need some logic to ensure safe passage of our data from the SPI output to the UART transmitter. As we saw when writing our transmitter, there are many cycles when the UART transmitter is busy and we won't be able to pass along the next byte of data to be sent. Additionally, we want to make sure we don't accidentally send the same byte of data multiple times. Make a signal called audio_sample_waiting
, that goes high when the SPI controller spits out a new sample, and gets pulled back low once the sample is transferred on to the UART module (which should only happen when the busy
signal is no longer high). You should also gate your valid
signal so that it'll only give valid data to uart_transmit
when sw[0]
is turned on! This way, we can turn on the switch when our computer is ready to listen for data.
Choose a BAUD bitrate that is higher than the minimum we just determined. Baud rates can theoretically can be any value, but it's nice to choose from among a commonly understood standard set of values: 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600. The faster you run your UART, the more you might run into errors in your design, so perhaps don't choose an excessively fast BAUD rate! Instantiate your uart_transmit
module with this BAUD rate as a parameter, and the inputs you just configured above; connect it to the uart_txd
top-level port, which will connect the output to the FT2232 and in turn send the data to your computer.
Listening on the computer's side
Since we're aiming to talk to our computers from the FPGA board, we need to talk about how we'll go about actually making our computers listen! What we'll want here is a Python script that can listen to the UART_TX
wire output and interpret those bytes as numbers; Python has a library called pyserial
which provides the functionality to talk or listen on a UART port, and that's what we'll use here. For this lab, we've written the Python script for you, but take a look at the script! It's not too complicated, and following the pattern of the script provided can let you work in Python with any data you send up from your FPGA.
In order to use pyserial
, you'll need to install it: you already have a Python environment running from setting up cocotb, so with that environment activated, run pip install pyserial
or a similar command in order to install it!
Configuration: Finding your serial port
In the python script we've provided for you, you need to specify the SERIAL_PORT_NAME
for pyserial to use when looking for UART data. Depending on your operating system, the string that is needed to select the proper serial port for your board will look different -- luckily, pyserial has a tool built into it to see your available ports. This test script will find all the available ports for usage, and test out sending data over each to find the correct port to use.
With your board plugged in and turned on, run the above script to see all the serial ports available to you. There may be a handful of different serial ports that appear, but the one's we're interested in are manufactured by Xilinx and are described as "JTAG+Serial". On Ubuntu (or Ubuntu in WSL with the board attached the same way as for flashing), the results we're looking for will look something like:
Ports found:
/dev/ttyUSB1: JTAG+Serial - JTAG+Serial [manufacturer: Xilinx]
/dev/ttyUSB0: JTAG+Serial - JTAG+Serial [manufacturer: Xilinx]
and the serial port names in this case are the names on the left: /dev/ttyUSB1
and /dev/ttyUSB0
. On Windows, the port names will instead look something like COM7
(or some other number). Note for the Window folks: WSL will probably make your (and our) life easier in this case. It is possible to get the script to work with Windows Python, but you must have the WinUSB driver (from Zadig) installed for the flashing interface and the original FTDIBUS driver (replaced with Zadig) installed for the serial interface. This process can be a pain...
Following the prompts, test out sending data over UART to the Xilinx ports: the test will send some messages over UART, and you can tell the board is actually receiving them if the TX
LED (near the USB port) starts flashing green. Typically, two ports will be labeled as Xilinx, and only one of them is good for UART communication!
Once you find the port that successfully sends data to the board, replace <YOUR SERIAL PORT HERE>
in this checkoff's Python script with the portname you found. Keep a note of it for any future usage of UART!
Now that you know the name of your serial port, set the variable in save_wav.py
appropriately, and set the BAUD rate appropriately. Build your design, and flash it to the board! Once it's running, start up your listener script with python3 save_wav.py
, and then switch sw[0]
on to allow your UART module to start running. Leave sw[0]
on for a few seconds, and say something into the microphone, then turn sw[0]
off again. After your computer sees silence for a couple seconds, it'll save the the audio samples you sent in a .wav
file, where you can play it with any audio file player on your computer.
Show a staff member your system capturing an audio recording from the MAX microphone, and the audio playing out of your line out port. Be ready to talk about the BAUD rate you chose, how you set up your trigger, and the buffer logic you wrote to preserve your audio samples.
Debugging with the Logic Analyzer
Let's say your system is not working for whatever reason? That sucks. No testbench is ever perfect so our checkers may not have covered every possible bad way to write the module. It would be good to read the actual UART signals and make sure they look like the right size and everything. We can do this with our logic analyzer from Week 2. Unfortunately the UART connections are on tiny traces on the FPGA board.
So in order to fix this, we'll "break out" the UART signal to two pins on the PMOD connector allowing us to probe them with our logic analyzer. Go into your xdc file and uncomment two wires on PMOD B:
#set_property -dict {PACKAGE_PIN H18 IOSTANDARD LVCMOS33} [ get_ports "pmodb[0]" ]
#set_property -dict {PACKAGE_PIN G18 IOSTANDARD LVCMOS33} [ get_ports "pmodb[1]" ]
Bring them in in your top_level.sv
and then connect them up to your UART signals. This can be as simple as:
assign pmodb[0] = uart_rxd;
assign pmodb[1] = uart_txd;
Then jam wires into those two spots on the PMOD and measure away. Make sure your BAUD and everything looks good!
Footnotes
1yes, there is legit information in higher frequencies, but the bare-minimum for intelligibility is usually found at or below 3 kHz
2why store 8 bits and not 10? Since we're going to be writing data to a .wav
file in a little bit, it makes life easier to store data in a width that works well with the bytes stored in a computer's file!
3in reality this is actually still going to cause some artifacts to come through...that's ok for now, but if you need better audio, you may want to look into Pulse Density Modulation (P_D_M) which does some neat tricks with noise. Depending on time we might go over this in class later in the semester.
4This does degrade your audio quality a fair amount, but to be fair, everything about this lab is degrading your audio quality :) If you work with audio in your final project, look for the design choices to change in these designs to preserve audio quality!
5If you don't trust your design yet, maybe don't plug in your fancy expensive headphones and blow out their speakers...
6lest this class end up with an hour rating like power electronics... i hope our friends making audio square waves on the other side of lab get some sleep :)