Potentiometers and Colors
So so pretty
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.
Do not take the MCP3008 ADC and potentiomters out of lab. We need to share them across all students. When you are not working with them, put them back at the front of lab.
You now have a functioning SPI controller that can both send out and read in bits. We want to now use this to communicate with something. We have just the candidate for that: The MCP3008 eight-channel Analog-to-Digital-Converter (ADC). This device is great. It can measure, on command, eight separate analog voltages in the 0 to 3.3V range. It digitizes those measurements, quantizing them into 10 bit values, which is great since we are, after all, working in an digital system. ADC's are one of the great interface devices in existence. The MCP3008, as it turns out, is controllable over SPI so we need to figure out how to do that on this page. We know the SPI language, but now we need to figure out how to have a particular type of conversation in that language.
Download the MCP3008 Datasheet. Have a copy open on your computer since it will be useful throughout this lab!
For motivation, we're going to use this ADC to read the voltages produced by three different potentiometrs hooked up as variable voltage dividers built from potentiometrs. The overall diagram for this will be the following:
If you're unfamiliar with what a potentiometer is, firstly, you should go and take 6.200. As a quick explanation, though, read through the content in the show-hide below.
The canonical voltage divider takes on a format shown below. There is a fixed voltage source (which we'll say is 3.3V for our lab) and two resistors R1
and R2
.
It can be shown using Ohm's Law and Kirchoff's two Laws (Voltage and Current laws) that the voltage v_out
, with respect to the ground node, will have a value of:
The resistors in effect divide the voltage according to the fraction shown above.
A potentiometer is a resistor of a fixed value between two outer legs, but with a "wiper" arm that can tap into somewhere along that resistor throughout its span. The symbol for a potentiometer is the following:
A "10 KiloOhm Potentiometer" refers to the fact that the potentiometer has a resistance of 10 KiloOhms between its outer two legs. The wiper touches somewhere along that continuous stretch of resistance and in the process forms a sequence of two resistors like shown. Here the variable a
ranges from 0 to 1 is a sort of normalized position variable. Because of this, the sum of those two resistors is actually RT*a + RT*(1-a) = RT
.
If we drop the potentiomer into the original voltage divider:
The equation from earlier turns into the following, where we replace a
with an angle \alpha corresponding to the angular turn position in a rotary potentiometer:
or
or
So boom, we have an angle turned into a variable voltage.
We'll wire things up later on in the page. Let's first focus on setting things up in the FPGA side to talk to our MCP3008 over SPI.
Configuration
First thing we need to do is enable and rename a few pins in our xdc
file to allow us to connect up to our ADC. The ADC we're using comes on a little carrier board, and we'll be plugging it into the "PMOD" connector found on the left side of our FPGA board. PMOD stands for "Peripheral Module", and allows for I/O interfacing to modules external to the FPGA. This saves you some wiring (but not all wiring...that's coming later).
It connects to six pins (on the top row starting from the PMODA side). Two are 3.3V and Ground (from which it is powered). Those are always on. The other four pins will be used for our SPI communication bus. We need to activate those pins and bring them into our top_level.sv
module.
The pins of an FPGA are all located underneath the chip in what is called a ball grid array. The pins are organized and named based on where in this 2D grid of connections they are, with columns being denoted by letters and rows being denoted by numbers. The four pins on our FPGA connected to those four pins on that PMOD connector we're plugging the ADC into are: F14
, F15
, H13
and H14
.
In the default .xdc
file we give you, those pins are commented out, and they're also given names that are too generic for our purposes. Go into the .xdc
file. Find the four lines (they're right next to one another) that refer to the four pins above, and change them so that they have meaningful names for our use case. Specifically we'll use:
F14
forCS
F15
forCOPI
H13
forCIPO
H14
forDCLK
A properly edited chunk of the XDC will look like the following:
set_property -dict {PACKAGE_PIN F14 IOSTANDARD LVCMOS33} [ get_ports "cs" ]
set_property -dict {PACKAGE_PIN F15 IOSTANDARD LVCMOS33} [ get_ports "copi" ]
set_property -dict {PACKAGE_PIN H13 IOSTANDARD LVCMOS33} [ get_ports "cipo" ]
set_property -dict {PACKAGE_PIN H14 IOSTANDARD LVCMOS33} [ get_ports "dclk" ]
Then in your top_level.sv
file, add in four new one bit connections corresponding to these values.
dclk
: should be anoutput logic
and is the data clock output of your SPI controllercopi
: should be anoutput logic
and is the data output of your SPI controllercipo
: should be aninput wire
and is the data input of your SPI controllercs
: should be anoutput logic
and is the chip select line for the SPI bus.
Talking to the MCP3008
Again, if you didn't already, download the MCP3008 Datasheet. Open it up and let's get to studying it.
First, some simple questions about the MCP3008:
We're going to hook up three of the MCP3008 channels up to the three pots. We'll use MCP3008 channels 0, 1, and 2. We'll use one channel for red, one for green, and one for blue. Since we only need 8 bits for each RGB brightness amount, we'll need to use only the top eight bits of the 10 bit measurement.
Now how do we actually get the measurement?
Figure 5-1 of the datasheet is going to be key here. It shows a standard SPI transaction for this chip. Keep in mind this datasheet is written from the perspective of the MCP3008 so:
DIN
refers to our universalCOPI
lineDOUT
refers to our universalCIPO
lineSCLK
is the same as ourDCLK
(a rose by any other name...)
This diagram shows what the MCP3008 is expecting as far as a conversation goes. If you speak to it correctly (and give it enough time to respond back), you'll get what you want out of it.
You'll notice that during a "conversation", the controller and peripheral have certain regions of time/data where they are responsible for setting specific bits and other regions where it doesn't really matter. This is important to know because when you read data back as the controller module, it is your job to know which bits actually are relevant and which are just noise/to be ignored.
Now even though the controller and peripheral only really need to set certain ranges of bits, something still needs to go on the line during those "don't care" regions. So when driving the SPI controller with a command message to send, you'll need to send a message of the appropriate size even if you are just padding "junk" in the don't care regions in order to give the peripheral enough clock cycles to send its data back.
So as the controller, even though you only need to send a few bits to get a reading, in the context of how we wrote our SPI controller, you'll have to send a message equivalent in size to the full conversation. What you put in the "don't care' regions is totally up to you. Similarly, when you get the message back from the MCP3008, you should remember that only parts of that message are actual legit data...the rest are random bits (could be zero, one, or random).
The Final Push
Do not take the MCP3008 ADC and potentiomters out of lab. We need to share them across all students. When you are not working with them, put them back at the front of lab.
So digesting all of this, we now have some idea of what we need to send to the MCP3008 to make measurements from it.
Our strategy for interacting with the MCP3008 will be as follows:
- Starting from rest, we will read from a channel on the ADC. We will send the appropriately sized SPI command to read from that channel, and receive the response. The appropriate bits from the response will be extracted, and those bits will be used to set one of the color channels driving our RGB controller.
- A millisecond later we will read from the next channel on the ADC (if we previously did 0, now we do 1, previously 1? do 2, previously 2? do 0), get the response, and assign it to the next color
- A millisecond later we will read from the next next channel on the ADC (if we previously did 0, now we do 1, previously 1? do 2, previously 2? do 0), get the response, and assign it to the next color
- And repeat forever and ever.
On average, therefore a change to a potentiometer's output voltage will be measured by our system within 3 milliseconds of it happening. Even though we're cycling across all three, it'll happen so fast (hopefully we're noticing a pattern here in digital systems)...that the human eye won't notice.
The MCP3008 can run at a SPI clock rate of about 3 MHz or so when powered from 3.3V. So for an SPI clock period, let's target 2 MHz to be safe. That's plenty, plenty fast to do a measurement within a 1 millisecond measure period.
We want our ADC readings to control the RGB LED (similar to the lab01, but with potentiometer controls instead of switches), with a channel for each color. We also want to display what we read from the ADC on our seven-segment display. Take the most significant 8 bits from each channel, concatenate it to each other with 4'h0
between consecutive channels, then display the result on the seven-segment display. For example, if the current values of red, green, and blue are 255, 100, and 13, respectively, we'd want to see FF06400d
on yoru display. If it was 255, 255, 255, it should be FF0FF0FF
, etc...
Thus, as you rotate each potentiometer, you should notice its corresponding color brightening / dimming, and its corresponding 8 bits on the seven-segment display showing the approximate voltage reading.
Eventually, you should hopefully get to this sort of behavior:
To help you get started in this, we wrote some starter code that should be dumped into the end of your current top_level
module. This code will not run right away and will require studying on your part to get integrated (there are incomplete lines which you need to figure out, for example). Nevertheless, your answers from above, this code, and your powerful, powerful brain need to get this all working so we have that nice RGB control with seven-segment readout.
Because I am a just and benevolent mentor on digital design journey, I've annoted spots in the code where you will need to be making changes with //MUST CHANGE
comments. There may be a couple others in your currently existing top_level
that also need to get modified/commented out to avoid collissions.
//NEW CODE TO ADD IN
//rgb controller instance (from last week!)
logic [7:0] red, green, blue; //8 bit channels to hold color values
assign val_to_display = 32'b0; //MUST CHANGE
rgb_controller mrgbc (.clk_in(clk_100mhz),
.rst_in(btn[0]),
.r_in(red),
.g_in(green),
.b_in(blue),
.r_out(rgb0[0]),
.g_out(rgb0[1]),
.b_out(rgb0[2]));
parameter ADC_DATA_WIDTH = 2; //MUST CHANGE
parameter ADC_DATA_CLK_PERIOD = 2; //MUST CHANGE
parameter ADC_READ_PERIOD = 100_000; //read one channel of ADC every millisec
//SPI interface controls
logic [ADC_DATA_WIDTH-1:0] spi_write_data;
logic [ADC_DATA_WIDTH-1:0] spi_read_data;
logic spi_trigger;
logic spi_read_data_valid;
//built in previous section:
spi_con
#( .DATA_WIDTH(ADC_DATA_WIDTH),
.DATA_CLK_PERIOD(ADC_DATA_CLK_PERIOD)
)my_spi_con
( .clk_in(clk_100mhz),
.rst_in(sys_rst),
.data_in(spi_write_data),
.trigger_in(spi_trigger),
.data_out(spi_read_data),
.data_valid_out(spi_read_data_valid), //high when output data is present.
.chip_data_out(copi), //(serial dout preferably)
.chip_data_in(cipo), //(serial din preferably)
.chip_clk_out(dclk),
.chip_sel_out(cs)
);
//counter from week 1. that will count up and we'll use for triggering
logic [31:0] select_count;
counter //include your regular counter from week 1 in source!
select_counter
( .clk_in(clk_100mhz),
.rst_in(sys_rst),
.period_in(ADC_READ_PERIOD),
.count_out(select_count)
);
//adc_count used for cycling through measuring ADC channel 0, 1, and 2
logic [1:0] adc_count;
always_ff @(posedge clk_100mhz)begin
if (spi_read_data_valid)begin//if the SPI has received message back:
case(adc_count) //update appropriate values based on adc_count
0: //MUST CHANGE
1: //MUST CHANGE
2: //MUST CHANGE
default: //MUST CHANGE
endcase
end
if (select_count=='d1)begin //once a millisecond select_count==1
case(adc_count) //look ahead:
0: begin
spi_write_data <= 0; //MUST CHANGE
end
1: begin
spi_write_data <= 0; //MUST CHANGE
end
2: begin
spi_write_data <= 0; //MUST CHANGE
end
default:begin
spi_write_data <= 0;
end
endcase
spi_trigger <= 1; //trigger spi transaction (channel read based on case above)
adc_count <= adc_count==2?0:adc_count + 1; //keep cycle to 0, 1, 2, 0..
end else begin
spi_trigger <= 0;
end
end
Assembly
At some point you're going to want to actually integrate your system together. As already stated, the ADC can be plugged nice and pretty into the PMOD connector like shown:
From there you need to wire it up. Here's the schematic again for your convenience.
You should grab some potentiometers, some wire, a breadboard and build this and integrate it with your ADC.
For reference, a breadboard is just an ordered set of metal connectors with grabby things. It'll let you assemble your board without having to hold all the parts together.
The potentiometers should be able to fit right into the board, but remember that not all pins on the pot are interchangeable.
Do not take the MCP3008 ADC and potentiomters out of lab. We need to share them across all students. When you are not working with them, put them back at the front of lab.
Logic Analyzer Debugging
Let's say your systme doesn't work right away (happens to the best of us). This is not a system you want to debug with an LED or by just looking at your stuff...the clock speed is 2 MHz. Hopefully, your SPI module is working fine (why you/we tried to test it on the previous page), but you never know..we can only test so much. You're going to need to read actual signals to make sure things are working correctly. For debugging our system connect the Logic Analyzer that came in your kit. If you haven't already set this device or software (Pulseview) up, go to the documentation page for it.
If you are on a M1/M2/M3 silicon Mac, there is a good chance that this software will not work on your computer, so you should log onto a lab machine and use it there (your kerberos and MIT ID are your login credentials). When logged into the machine, you can launch PulseView from a terminal using pulseview
.
Once you have things installed/verified, connect your logic analyzer probes to the COPI
, CIPO
, DCLK
, and CS
signals (as well as GND). Set things up to grab 500 samples at 16 MHz on the falling edge trigger of the CS signal. Do some general captures until hopefully you're seeing some signals showing up. Click on the channel that CS
is on and set a falling-edge trigger on it. That way when you press run it will only capture the relevant portion of time. For example, with my falling trigger set you should get something similar to below:
Coloring may be different depending on what probes you used to hook up to your device.
In the capture below, I am triggering on the falling edge of D3
which corresponds to the CS
pin. We can see, very clearly:
- A clock signal on
D0
corresponding toDCLK
- Some content at the beginning of the transaction on
D2
corresponding to theCOPI
line (the initial read instruction from the FPGA to the MCP3008) - Some content towards the end of the transaction on
D1
corresponding to theCIPO
line (the MCP3008's ADC 10 bit channel measurement response to the FPGA). - The
CS
signal returning high after the completion of the transaction.
for your checkoff make sure you have some logic analyzer captures to show your staff member.
Show us your Potentiometer to RGB Controller working. You should also show us your signals on your logic analyzer. No logic analyzer, no checkoff.