In this lab we will learn how to use the FPGA's built-in ADC (called XADC). The Artix7 FPGA version on the BASYS3 board (XC7A35T) contains analog-to-digital (ADC) circuitry that allows it to monitor various temperatures, voltages, and other things needed to know how the chip is working. You can access this circuitry to input an analog voltage, either directly through dedicated analog input pints, or through IO pins that can be used for either analog or digital.
The ADC that runs up to 1 MSps (1 MHz). The project will tie the ACD into some firmware, and display the result on the 4-digit LED display. The ADC circuitry is complex, but for simple slowly changing analog signals, is extremely useful. The technical details are patented, and Xilinx is not keen on disclosing, however it is described in some detail in a Google Patent here (but good luck in digging out too much details, it is a patent so difficult to read). The circuitry is available in many of the Xilinx chips other than Artix7, and is called an "XADC" block.
The Artix7 we are using contains 1 XADC block, which has 2 12-bit 1 MSPS (mega samples per second) ADCs, and an on-chip analog multiplexer so that you can route 17 different inputs into the ADC. The amplifiers support unipolar, bipolar, and differential inputs. For more technical information on how to use it, see the Xilinx app note ug480_7Series_XADC.pdf. These ADCs can be used for various things like temperature monitoring, or even DAQ for externally driven circuits. In fact, if you don't use the ADC in your design, it then automatically digitizes all on-chip sensors for readout over the serial JTAG interface (this is described elsewhere).
The internal XADC can convert signals from:
as detailed in the following table:
CPG236 Name | Artix7 pin | BASYS3 JXADC Pin |
---|---|---|
IO_L7P_T1_AD6P_35 | J3 | 1 |
IO_L7N_T1_AD6N_35 | K3 | 7 |
IO_L8P_T1_AD14P_35 | L3 | 2 |
IO_L8N_T1_AD14N_35 | M3 | 8 |
IO_L9P_T1_DQS_AD7P_35 | M2 | 3 |
IO_L9N_T1_DQS_AD7N_35 | M1 | 9 |
IO_L10P_T1_AD15P_35 | N2 | 4 |
IO_L10N_T1_AD15N_35 | N1 | 10 |
The JXADC header is the one next to the 4 digit display (and is labeled on the BASYS3 board), and they are paired (positive/negative) such that the positive pin is on the top row and the negative pin is on the bottom row in the same column. For our projects using the XADC, we will be using pints J3 for the positive input and K3 for the negative input, and the ADC will digitize the voltage across those pins.
The XADC can measure unipolar (0 to 1 volt) or bipolar (-0.5 to +0.5 volt) signals.
The figure below comes from page 30 of the
XADC manual):
We will be using unipolar mode, and the BASYS3 board only wires up the
dual analog/digital inputs to the FPGA, so we will only be using the
"VAUX" inputs. That means that the input impedance of our XADC will
be around 10kΩ, and the sampling capacitor will be around 3pF,
giving an RC chargeup time of around 30ns. Note that in unipolar mode, there are 2
switches that connect the positive and negative sides of the sampling capacitor to the
inputs. They are labeled Vp and Vn, which are the dedicated analog inputs,
but it is the same for the "VAUX" inputs that we will use via "JXADC" header.
The way the XADC works is typical of analog-to-digital conversion circuits.
A "sample and hold" capacitor is charged up by virtue of an incoming voltage
signal, and this is usually gated so that you can control when it charges.
Charging is the "sample" phase.
Once it is charged, it is disconnected from the inputs, and the voltage will
hold while the signal is being converted from a voltage into a digital
number. This is the "hold" phase. Ideally you want the capacitor to be
small enough so that it charges up fast, but not too small such that any
stray capacitance can compete. And you want the input impedance to be such
that the RC time for charging (τ = RC) is small. Often you will see
ADCs that first charge, and then hold and convert, doing them serially,
which puts a big burden on the front-end analog circuitry to charge up
quickly (so that the overall data rate can be large).
What the Xilinx XADC
does, instead, is to have two sample and hold capacitors like in the figure.
So the XADC can sample and convert simultaneously.
The ADC itself is 12 bits. This means there are 2x1012=4096 possible
values, and since the maximum voltage is 1.0 volts, that means that
the LSB is 1/212=0.244mV, which means the precision is around
half that, or δV = 0.122mV. The rise time of the voltage on the sampling
capacitor is given by τ = RC = 30ns, which means that the voltage
on the capacitor Vc increases with time according to:
Vc = Vin(1-e-t/τ)
If Vin is the maximum 1.0 volts, then we can calculate the
time tδ (or the number of RC times N\τ) that
it will take for the signal to get to within δV of Vin, so
that the charging does not dominate the precision:
Vδ=Vin(1-δV)=Vin(1-e-N)
which means δV=e-tδ/τ, and solving for tδ
gives
Nτ = -τlnδV = 9.01τ
For τ=30ns, that means we would need around 270ns of charging
so that our precision is not dominated by the charging time. The XADC
will run at 1M samples per second (1 MSPS), or 1μs, with parallel
sampling and conversion, so charging will not be a problem, but you should
keep this precision in mind in case you use it at a slower sampling rate.
Using the XADC with VAUX inputs to measure voltages that change on a time
scale longer than the 1μs operation time will work great, even if we
are not controlling the conversion with a "trigger", something that synchronizes
conversion with the incoming signal. However, if you want to use the XADC
to measure voltages that are changing fast with respect to 1μs, you have
to build a preamp that will integrate the signal using a differential
amplifier with a capacitor as feedback. This is the subject of another
course.
The figure below details the timing of the XADC internals:
A full cycle of conversion takes 26 ticks of the internal clock called ADCCLK, which is derived
from the input clock .dclk_in. Setting configuration register 2 allows you
to determine the ADCCLK frequency.
The documentation is a bit fuzzy, but there was a
technical note
that says the ADCCLK has to be between 1 and 26 MHz if you want to run the ADC at the maximum
1 MSps conversion. For our purposes, we will use the on board 100MHz clock, divide
it in half to get 50MHz, and use that as
input .dclk_in, so our divider will be 2 (see table above), and we will have a 25MHz
ADCCLK which will set the conversion rate to R=25/26=961.5kSps.
By using a 50MHz .dclk_in, some of the timing pulses (described below) will be
~20ns wide, which means we can use the 100MHz clock to run state machines and branch
on pulses without encountering race conditions (this is probably overly cautious!).
The conversion period includes all of the time it
takes to assemble and latch the output bits so that they become available
to be latched inside the FPGA by your code.
A good reference for different ways to use the XADC is available
here.
The ADC allows 4 ticks for the capacitor to fully charge and settle,
and can be increased to 10 (see documentation). There are 2
sample-and-hold capacitors, so that one can be charging up
while the other is being digitized.
As you can see in the diagram, .eoc_out is asserted on every conversion,
so the .eoc_out for channel "N-1" in the diagram is the second one asserted
in the diagram, which happens when channel "N" is being converted. Below we
show the logic analyzer output for the XADC.
The ADC on the Artix7 can be configured so that you can trigger it from an
event, or you can enable it by controlling the input enable (.den_in,
short for "data enable in"),
or you can just let it run free and keep digitizing the analog signal you
are sending it by tying the .eoc_out signal, which is a 1 clock tick signal
meaning "end of conversion", to the .den_in signal. This is how we will use
it to build the voltmeter.
A caveat is that if the signal is rising during the time you are digitizing,
you might not get the full value of the voltage, but if your voltage is DC
(or changing slow compared to 1μs) then you won't notice this.
If your signal has a significant AC component however, this can be handled by using the
XADC in event mode instead of the continuous mode that we will be using.
All of this is detailed starting on page 73 of the
XADC manual.
The input voltage to be converted has to be between 0 and 1.0 volts (for
unipolar mode), and the ADC produces a 12 bit number in the upper 12 bits
of the .do-out bus. The resolution of a single bit (LSB) is therefore
δ = 1V/212=244μV. Bipolar mode is more complex and won't be
used here.
Go ahead and create a new project, call it anything (like "xadc"), and of course
use the XC7A35TCPG236-1 part. Then Vivado should be ready to build the project.
First create a top level verilog file (call it anything, I'll call mine top.v).
We will need some standard inputs and outputs:
Note that the parameter "VERSION" will be our firmware version (see below).
The way the XADC works is that you give it an input clock, and specify a clock
divider amount, and it will form what it calls an "ADC Clock" which is the ratio.
To convert, it needs 26 ticks of this clock, so the conversion rate will be given
by the ADC clock rate divided by 26. The fastest it can convert is 1M sample per second
(1MSps), which means that your input clock will have to be 1MHz x divider x 26.
For a divider of 4, that means an input clock of 104MHz. If we used the 100MHz clock
we would get a conversion rate of 25MHz/26 = 961.54kSPS, however for this project
let's stick with the nice roun 1MSps, which means we need to generate a 104MHz clock.
To generate this clock we can use the Xilinx IP Catalog, search for "clock", and click
on "Clocking Wizard". You leave the "Component Name" as is or change it to anything
you like but let's use clock_104 (defaul is clk_wiz_0).
Leave everything as is in the "Clocking Options" tab,
and click on the "Output Clocks" tab. Make sure "clk_out1" is checked and
set the "Output Freq (MHz) Requested" column to 104.000 for "clk_out1".
Every other tab can be left as is.
Hit the "OK" in the bottom right corner, then "Generate" in the next window and you
should see the "clock_104" source appear in the "Sources" window.
Instantiation inside top.v is like this:
Next we should set up some debouncers so that the latch push button works
the way we want (we don't need to debounce the version and reset, bouncing is not
important on those lines as bouncing won't change anything).
The debouncer will be the same as in the
FPGA<->FPGA-RPI Serial Communication
project, so you can copy the code from that. Make a new source file called
debouncer, open it and copy the code from the previous debouncer.v file,
or just take it from here.
The code to debounce the latch will be instantiated like this:
The BASYS3 has a 4 digit LED display, towards the lower left corner (item 4
in the picture below)
and is described starting on page 15 of the
BASYS3 manual, which states that each of the 4 digits is composed of 7
LEDs in segments arranged in a figure-8 pattern, with an additional LED for the
period in the lower right corner. So you have to have 8 outputs from your top level
verilog module, and driving the right ones can give you any particular pattern
where the most useful are the 10 decimal digits. There are also 4 signals
(see figure 16) that point to each of the 4 segments.
So the way you display a 4-digit number (or any 4-digit pattern) is to set
the patter, select the first segment, then set the pattern and select the
second, and so on, faster than the human eye can follow, which means you would
make a clock that runs at say 60Hz, and drive each one in turn. So if your
eye could see fast enough, you would not see a 4 digit pattern, but you
would instead see the 4 digits go on and off in a row.
To make a new verilog circuit to take care of all this, it would have as
inputs the system clock, and the 4-digit number you want to display.
The outputs would be connected to the outputs of top.v, which would be
segment (the 7 segments of one of the displays),
dp (the period), and digit (which digit to display). It
would look something like this:
We will use the system clock (100MHz) so that we can make a slower 60Hz clock
internal to this module. The input number will be in hex, and will be 16 bits
(4 bits per digit, 0-A). So the next thing the module will do is to separate
the 4 digits into 4 4-bit variables:
To make a clock that runs at around 60Hz (16ms period), starting with a 10ns
period, you would need a counter that has 18 bits (remember, a 1 bit counter
is a clock divider, so the period of the new clock is larger by a factor of
2N+1 for an N bit counter). So for 18 bits, the new period will
be 219×10ns = 5.2ms. That translates into 190Hz, but that's
what we want because we want to use that clock to drive each of the 4 digits,
one after the other, which means each of them turns on and off at around
47Hz, which is fine for our purposes. The code looks like this (and we
will use a BUFG to put the new clock into a clock line inside the FPGA):
Note that the register we will use as the new 47Hz clock is
called clock.
Next we need a counter that tells us which digit to drive:
This code is instantiated 4 times inside display4 like this:
Now all we have to do is use the clock to change which
digit is being driven, one at a time (and below, we will
turn the period off by driving it to 1.
The entire code can be found here.
It is instantiated inside top.v like this:
Note that the display_this variable is driven below using an assign
statement (see below).
Next, the ADC code. After we instantiate that, we will build in the
state machine logic to handle the UART send and receive.
The XADC is an in-house module that Xilinx provides.
To be able to use the XADC, we click on "IP Catalog" under "PROJECT MANAGER" in the
left panel. It will bring up a new window in one of your panels,
with a tab labeled "IP Catalog". It will look something like this:
Type XADC into the search window, and it should find the "XACD Wizard".
Double click and it will run the wizard, you should see something
like this to begin:
You will see a text field called "Component Name" and you will see
"xadc_wiz_0" in that field. Change it to "myxadc", and
it will show up with this name in your verilog sources panel.
Underneath "Component Name" you will see 5 tabs labeled "Basic",
"ADC Setup", "Alarms", "Single Channel", and "Summary", and these
are used to set up the instantiation. Here's what
is recommended for each of these tabs:
Click "OK", and you should see a popup window that asks if it's ok to
create a new directory to house all of the new files. It should be
in your project directory. Click "OK". It will then pop up a window
labeled "Generate Output Product". Click "Generate", it will initiate
some activity, and at the end will inform you that it did what it was
supposed to do. Click OK.
Now you should see a new source for the XADC called myxadc
appear in the same panel with the other
sources.
If you open up what's below "myxadc" you should see a file called
"myxadc (myxadc.v)". That's your source file, it contains
the instantiation of the XADC. You can double click on it, and you will
a huge number of lines. Don't worry, all we have to do now is
instantiate myxacd and that module will do all the heavy lifting.
Here's what you do with each of these ports:
The XADC instantiation will produce a series of configuration registers that
control how the XADC works, and you can see them in the myxadc.v
The configuration registers can be written to
and read from using the DRP (Dynamic Reconfiguration Port), which we will not
use. But it's good to see how these registers are configured, as depicted
in the list below (which comes from the verilog instantiation):
Note that this is where we assign display_this.
To latch the ADC into the LEDs, we can use the debounced latch signal, which is
called latch_pulse:
Finally, add these lines to send some debug data out the JA and JB PMOD ports so that we
can see what the hardware is actually doing, using a logic analyzer.
Note that in the code in top.v, we will have 4 different 16-bit values for
the ADC:
Now you have to enter the pin assignments into the constraints file, which you
will first need to create. The assignments are here:
Note that the ADC value that comes out of the XADC block
(adc_data from the .do_out port) has 16 bits.
The upper 12 bits are the ADC value, the lower 4 bits are
the status. These are detailed
here.
Basically the 4 lower bits of the ADC output are telling
you something about what the ADC is doing (digitizing,
waiting, etc), which is why the lower of the 4 LED digits
is always cycling. The next 4 bits (2nd LED digit) is
also flickering, because remember the ADC is a 12 bit
number with a bit resolution of 0.244mV. So
if you put some signal into the JXADC inputs (pin 1
is positive, 7 is negative) then if it's not steady then
the lower bits of the 12-bit ADC value will flicker.
Now you should be ready to build and download the project.
What
you should see is the ADC value displayed as a 16 bit (4-digit hex)
number continuously. Push the latch button (BTNL) and the
ADC value will be displayed on the LEDs until you push it again
to get another one. If you push the version button (BTNU) then
you should get the version number displayed instead of the ADC,
which will be whatever you set it to ('h0001' here).
In the above code we have brought out some signals to look at with a
logic analyzer:
Here's what you would see:
The logic analyzer is configured to show bit 14 of the XADC data
("d14") right above bit 14 of the latched data using the posedge of
adc_data_ready ("latched 14"), and the same for bit 15.
Since we are latching on the
posedge of adc_data_ready, then the value of "latched d8" should
be the same value as in adc_data in the previous XADC
conversion (that is, 1μs earlier), so "latched 15" will follow
"d15". And in the diagram, it does appear that the purple waveform
("latched 15") is ahead of the orange waveform ("d15") by 1μs.
But if you look at the 2 waveforms for bit 14, right in the middle
you see that "latched 14" does not quite follow "d14", but it does
elsewhere. This is likely a "race condition" caused by the fact
that the posedge of of "adc_data_ready" comes slightly early, when
"d14" is still high and yet to transition to it's negative value
(red waveform).
In future projects, we will want to latch the
ADC value (this is our
signal adc_data which comes out of the "do_out" port)
so that we can store the data into FIFOs and read it out later.
There are several ways to do this:
So I recommend that in the above code where we form the signal r_adc_data:
that it be changed to
The entire code for top.v is here:
The above project archive can be found
here.
Xilinx XADC
Xilinx XADC Timing
XADC Project
Our verilog input will look like this:
module top(
input clock, // system clock
input reset, // BTNR
input version, // BTNU
input latch, // BTNL
input adc_n, adc_p, // VCAUX 6, P and N
output [15:0] led, // 16 onboard LEDs above the switches
output [6:0] segment, // 7-segment digit LED display
output dp, // "." next to each LED digit
output [3:0] digit, // which of the 4 digits to drive
output [7:0] JB, // debugging
output [7:0] JA //
);
parameter VERSION = 'h0001;
//
// the XADC input clock and an internal clock divider determine the conversion
// rate. we will shoot for 1MSps. XADC takes 26 clock ticks for a conversion, so
// to get the input clock with a divider of 4 we multiply 4x26x1MHz = 104MHz.
//
// to get such a frequency, use the Xilinx clock wizard. let's make a 104 clock
// for the XADC
//
wire locked, clock104;
clock_104 MYCLOCKS(
.reset(reset),
.clk_in1(clock),
.locked(locked),
.clk_out1(clock104)
);
//
// debounce the latch button
//
wire latch_level, latch_pulse;
debouncer DEBOUNCE_LATCH(
.clock(clock),
.button(latch),
.level(latch_level),
.pulse(latch_pulse)
);
4-Digit LED Display
module display4(
input clk100,
output reg [3:0] digit = 0, //digit 3 is leftmost (MSD), digit 1 is rightmost (LSD)
output reg [6:0] segments = 'b111111, //7 segments: top,mid,bot and top_left/bot_left and same for right
output reg period,
input [15:0] number //4 hex digits
);
wire [3:0] digit3 = number[15:12];
wire [3:0] digit2 = number[11:8];
wire [3:0] digit1 = number[7:4];
wire [3:0] digit0 = number[3:0];
reg [17:0] counter = 0;
//
// use negedge so we don't have race conditions later
//
always @ (negedge clk100) counter <= counter + 1;
wire digit_clock = counter[17];
wire clock;
BUFG clkdbuf (.I(digit_clock),.O(clock));
reg [1:0] which_digit = 0;
always @ (posedge clock) which_digit <= which_digit + 1;
Now we need some way to go from the 4-bit number in
digit1 and the other 3 digits as above, to which of
the 7 LEDs in each segment to drive. This is a tricky thing to
write, it requires you to map the 7 segments as detailed on page
16 of the manual to each digit. The code for doing this is
shown next, where the input is the 4-bit hex nmber and
the output are the 7 segments to drive:
module segnum (
input clk,
input [3:0] number,
output reg [6:0] seg = 0
);
parameter [6:0] p0 = 'b1000000;
parameter [6:0] p1 = 'b1111001;
parameter [6:0] p2 = 'b0100100;
parameter [6:0] p3 = 'b0110000;
parameter [6:0] p4 = 'b0011001;
parameter [6:0] p5 = 'b0010010;
parameter [6:0] p6 = 'b0000010;
parameter [6:0] p7 = 'b1111000;
parameter [6:0] p8 = 'b0000000;
parameter [6:0] p9 = 'b0010000;
parameter [6:0] pa = 'b0001000;
parameter [6:0] pb = 'b0000011;
parameter [6:0] pc = 'b1000110;
parameter [6:0] pd = 'b0100001;
parameter [6:0] pe = 'b0000110;
parameter [6:0] pf = 'b0001110;
parameter [6:0] pp = 'b1111101;
always @ (posedge clk)
case (number)
'h0: seg <= p0;
'h1: seg <= p1;
'h2: seg <= p2;
'h3: seg <= p3;
'h4: seg <= p4;
'h5: seg <= p5;
'h6: seg <= p6;
'h7: seg <= p7;
'h8: seg <= p8;
'h9: seg <= p9;
'hA: seg <= pa;
'hB: seg <= pb;
'hC: seg <= pc;
'hD: seg <= pd;
'hE: seg <= pe;
'hF: seg <= pf;
endcase
endmodule
wire [6:0] wseg0, wseg1, wseg2, wseg3;
segnum S0 ( .clk(clk100), .number(digit0), .seg(wseg0) );
segnum S1 ( .clk(clk100), .number(digit1), .seg(wseg1) );
segnum S2 ( .clk(clk100), .number(digit2), .seg(wseg2) );
segnum S3 ( .clk(clk100), .number(digit3), .seg(wseg3) );
always @ (posedge clock) begin
period <= 1; // turn it off for now
case (which_digit)
'h0: begin
digit <= 'b1110;
segments <= wseg0;
end
'h1: begin
digit <= 'b1101;
segments <= wseg1;
end
'h2: begin
digit <= 'b1011;
segments <= wseg2;
end
'h3: begin
digit <= 'b0111;
segments <= wseg3;
end
endcase
end
//
// next drive the 4 7-segment displays
//
wire [15:0] display_this;
display4 DISPLAY (
.clk100(clock),
.number(display_this),
.digit(digit),
.segments(segment),
.period(dp)
);
XADC Instantiation Using IP Wizard
Now you are ready to generate the instantiation. You will see in the
left panel what pins will be driven, it should look like this:
For more info click on
this.
To instantiate you should place the following in your code:
//
// here is the XADC block
//
wire [6:0] daddr_in = 7'h16;
wire adc_ready, isbusy, adc_data_ready, eos_out, alarm;
wire [15:0] adc_data;
wire [4:0] channel_out;
myxadc XADC_INST (
.daddr_in(7'h16), // specifies vcaux6 pints to digitize
.dclk_in(clock104), // 50MHz clock
.den_in(adc_ready), // tied to adc_ready, tells adc to convert, tieing causes continuous conversions
.di_in(16'h0), // to set the data to something, not used here
.dwe_in(1'b0), // set to enable writing to di_in, which we don't want to do
.vauxp6(adc_p), // positive input to digitize
.vauxn6(adc_n), // negative input to digitize
.busy_out(isbusy), // tells you the adc is busy converting
.channel_out(channel_out[4:0]), // for using more than 1 channel, tells you which one. not used here
.do_out(adc_data), // adc value from conversion
.drdy_out(adc_data_ready), //tells you valid data is ready to be latched
.eoc_out(adc_ready), // specifies that the ADC is ready (conversion complete)
.eos_out(eos_out), // specifies that conversion sequence is complete
.alarm_out(alarm), // OR's output of all internal alarms, not used here
.vp_in(1'b0), // dedicated analog input pair for differential, tied to 0 if not used
.vn_in(1'b0)
);
.INIT_40(16'h0016), // config reg 0
.INIT_41(16'h31AF), // config reg 1
.INIT_42(16'h0200), // config reg 2
.INIT_48(16'h0100), // Sequencer channel selection
.INIT_49(16'h0000), // Sequencer channel selection
.INIT_4A(16'h0000), // Sequencer Average selection
.INIT_4B(16'h0000), // Sequencer Average selection
.INIT_4C(16'h0000), // Sequencer Bipolar selection
.INIT_4D(16'h0000), // Sequencer Bipolar selection
.INIT_4E(16'h0000), // Sequencer Acq time selection
.INIT_4F(16'h0000), // Sequencer Acq time selection
.INIT_50(16'hB5ED), // Temp alarm trigger
.INIT_51(16'h57E4), // Vccint upper alarm limit
.INIT_52(16'hA147), // Vccaux upper alarm limit
.INIT_53(16'hCA33), // Temp alarm OT upper
.INIT_54(16'hA93A), // Temp alarm reset
.INIT_55(16'h52C6), // Vccint lower alarm limit
.INIT_56(16'h9555), // Vccaux lower alarm limit
.INIT_57(16'hAE4E), // Temp alarm OT reset
.INIT_58(16'h5999), // VCCBRAM upper alarm limit
.INIT_5C(16'h5111), // VCCBRAM lower alarm limit
The following table summarizes the configuration registers. For our purposes, since we are running
in continuous single channel mode and no alarms, only the configuration registers are important.
Register (hex) Value
Name Comments
40 'h0016 config reg 0
4:0 selects ADC input channels, 16 means VAUX 6 only.
Settling time is 4 ticks, continuous mode,
unipolar, no external multiplexer mode,
and use averaging to calculate calibration coefficients.
41 'h31AF config reg 1
disable temperature alarms, enable ADC gain corrections, disable offset
corrections, set single channel mode
42 'h0200 config reg 2
ADCCLK = dclk_in divided by x2
//
// wait for XADC to tell you something is ready to latch
//
reg [15:0] r_adc_data;
always @ (posedge adc_data_ready)
if (reset) r_adc_data <= 16'h0;
else r_adc_data <= adc_data;
We want to use the push button to latch the adc value into the LEDs, but also
have the ADC value continuously displayed on the 4 7-segment LEDs. Let's make
a 1Hz clock for the 4 display characters to latch the values
so that the numbers don't change too fast, making it easier to see. We can use
the clock (100MHz) line and a 27-bit counter (which is approximately 108
counts) for this:
//
// make a ~1Hz clock so we can run the LED display slower
//
reg [26:0] counter;
reg [15:0] s_adc_data;
always @ (posedge clock) begin
if (reset) counter <= 0;
else counter <= counter + 1;
end
wire clock_1hz = counter[26];
always @ (posedge clock_1hz) s_adc_data <= r_adc_data;
assign display_this = version ? VERSION : s_adc_data;
//
// latching the adc value for LED output from latch pushbutton
//
reg [15:0] latched_adc; // latched adc data
always @ (posedge latch_pulse)
if (reset) latched_adc <= 0;
else latched_adc <= r_adc_data;
assign led = latched_adc;
//
// some output for debugging if necessary
//
assign JA = {3'b000,eos_out,adc_data_ready,adc_ready,isbusy,clock104};
assign JB = {adc_data[11:8],r_adc_data[11:8]};
## clock
set_property PACKAGE_PIN W5 [get_ports clock]
set_property IOSTANDARD LVCMOS33 [get_ports clock]
# LEDs
set_property PACKAGE_PIN U16 [get_ports {led[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led[0]}]
set_property PACKAGE_PIN E19 [get_ports {led[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led[1]}]
set_property PACKAGE_PIN U19 [get_ports {led[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led[2]}]
set_property PACKAGE_PIN V19 [get_ports {led[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led[3]}]
set_property PACKAGE_PIN W18 [get_ports {led[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led[4]}]
set_property PACKAGE_PIN U15 [get_ports {led[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led[5]}]
set_property PACKAGE_PIN U14 [get_ports {led[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led[6]}]
set_property PACKAGE_PIN V14 [get_ports {led[7]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led[7]}]
set_property PACKAGE_PIN V13 [get_ports {led[8]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led[8]}]
set_property PACKAGE_PIN V3 [get_ports {led[9]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led[9]}]
set_property PACKAGE_PIN W3 [get_ports {led[10]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led[10]}]
set_property PACKAGE_PIN U3 [get_ports {led[11]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led[11]}]
set_property PACKAGE_PIN P3 [get_ports {led[12]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led[12]}]
set_property PACKAGE_PIN N3 [get_ports {led[13]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led[13]}]
set_property PACKAGE_PIN P1 [get_ports {led[14]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led[14]}]
set_property PACKAGE_PIN L1 [get_ports {led[15]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led[15]}]
##
## 7 segment display
set_property PACKAGE_PIN W7 [get_ports {segment[0]} ]
set_property IOSTANDARD LVCMOS33 [get_ports {segment[0]} ]
set_property PACKAGE_PIN W6 [get_ports {segment[1]} ]
set_property IOSTANDARD LVCMOS33 [get_ports {segment[1]} ]
set_property PACKAGE_PIN U8 [get_ports {segment[2]} ]
set_property IOSTANDARD LVCMOS33 [get_ports {segment[2]} ]
set_property PACKAGE_PIN V8 [get_ports {segment[3]} ]
set_property IOSTANDARD LVCMOS33 [get_ports {segment[3]} ]
set_property PACKAGE_PIN U5 [get_ports {segment[4]} ]
set_property IOSTANDARD LVCMOS33 [get_ports {segment[4]} ]
set_property PACKAGE_PIN V5 [get_ports {segment[5]} ]
set_property IOSTANDARD LVCMOS33 [get_ports {segment[5]} ]
set_property PACKAGE_PIN U7 [get_ports {segment[6]} ]
set_property IOSTANDARD LVCMOS33 [get_ports {segment[6]} ]
##
## LED period (dot)
set_property PACKAGE_PIN V7 [get_ports {dp}]
set_property IOSTANDARD LVCMOS33 [get_ports {dp}]
##
## digit select
set_property PACKAGE_PIN U2 [get_ports {digit[0]} ]
set_property IOSTANDARD LVCMOS33 [get_ports {digit[0]} ]
set_property PACKAGE_PIN U4 [get_ports {digit[1]} ]
set_property IOSTANDARD LVCMOS33 [get_ports {digit[1]} ]
set_property PACKAGE_PIN V4 [get_ports {digit[2]} ]
set_property IOSTANDARD LVCMOS33 [get_ports {digit[2]} ]
set_property PACKAGE_PIN W4 [get_ports {digit[3]} ]
set_property IOSTANDARD LVCMOS33 [get_ports {digit[3]} ]
## Buttons
set_property PACKAGE_PIN T18 [get_ports version]
set_property IOSTANDARD LVCMOS33 [get_ports version]
set_property PACKAGE_PIN W19 [get_ports latch]
set_property IOSTANDARD LVCMOS33 [get_ports latch]
set_property PACKAGE_PIN T17 [get_ports reset]
set_property IOSTANDARD LVCMOS33 [get_ports reset]
## Pmod Header JXADC
## Schematic name = XA1_P
set_property PACKAGE_PIN J3 [get_ports adc_p ]
set_property IOSTANDARD LVCMOS33 [get_ports adc_p ]
## Schematic name = XA1_N
set_property PACKAGE_PIN K3 [get_ports adc_n ]
set_property IOSTANDARD LVCMOS33 [get_ports adc_n ]
##Pmod Header JB
##Sch name = JB1
set_property PACKAGE_PIN A14 [get_ports {JB[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {JB[0]}]
##Sch name = JB2
set_property PACKAGE_PIN A16 [get_ports {JB[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {JB[1]}]
##Sch name = JB3
set_property PACKAGE_PIN B15 [get_ports {JB[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {JB[2]}]
##Sch name = JB4
set_property PACKAGE_PIN B16 [get_ports {JB[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {JB[3]}]
##Sch name = JB7
set_property PACKAGE_PIN A15 [get_ports {JB[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {JB[4]}]
##Sch name = JB8
set_property PACKAGE_PIN A17 [get_ports {JB[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {JB[5]}]
##Sch name = JB9
set_property PACKAGE_PIN C15 [get_ports {JB[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {JB[6]}]
##Sch name = JB10
set_property PACKAGE_PIN C16 [get_ports {JB[7]}]
set_property IOSTANDARD LVCMOS33 [get_ports {JB[7]}]
#Pmod Header JA
#Sch name = JA1
set_property PACKAGE_PIN J1 [get_ports {JA[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {JA[0]}]
#Sch name = JA2
set_property PACKAGE_PIN L2 [get_ports {JA[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {JA[1]}]
#Sch name = JA3
set_property PACKAGE_PIN J2 [get_ports {JA[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {JA[2]}]
#Sch name = JA4
set_property PACKAGE_PIN G2 [get_ports {JA[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {JA[3]}]
#Sch name = JA7
set_property PACKAGE_PIN H1 [get_ports {JA[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {JA[4]}]
#Sch name = JA8
set_property PACKAGE_PIN K2 [get_ports {JA[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {JA[5]}]
#Sch name = JA9
set_property PACKAGE_PIN H2 [get_ports {JA[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {JA[6]}]
#Sch name = JA10
set_property PACKAGE_PIN G3 [get_ports {JA[7]}]
set_property IOSTANDARD LVCMOS33 [get_ports {JA[7]}]
XADC Timing
assign JA = {3'b000,eos_out,adc_data_ready,adc_ready,isbusy,clock104};
assign JB = {adc_data[11:8],r_adc_data[11:8]};
This is so we can look at the actual transitions of the XADC data output
(signal adc_data) and make sure we know how to latch the data into the signal
r_adc_data.
Hook up the logic analyser
to the 8 JB outputs and 8 JA outputs.
To make sure we are exercising the XADC, we will put a voltage into
the VAUX6 inputs from a sine wave generator that has a DC offset
of 0.5V and an amplitude just under 0.5V, so that the voltage
swings the full range of the XADC, which is from 0 to 1 volt,
which should be exercising all of the 12 bits of ADC.
Then let's capture some data with the logic analyzer and
see what the actual timing looks like.
Item 1 is not recommended, as the posedge of adc_data_ready is too close to
the transition edge of the data. Item 2 is also not recommendced as adc_data_ready
is very narrow compared to the clock, so this will only work when the ADC is running with
an input clock that is slow compared to the system clock used for latching. Item 3 seems
to be the safest, as it is far away from the transition edges, and we can use the edge
itself as a latch.
//
// wait for XADC to tell you something is ready to latch
//
reg [15:0] r_adc_data;
always @ (posedge adc_data_ready)
if (reset) r_adc_data <= 16'h0;
else r_adc_data <= adc_data;
//
// wait for XADC to tell you something is ready to latch
//
reg [15:0] r_adc_data;
always @ (negedge isbusy)
if (reset) r_adc_data <= 16'h0;
else r_adc_data <= adc_data;
On doing so and checking with the logic analyzer, we see no discrepancy. However
the real test will come when we analyze audio signals in the RPi in some of the last
labs here.
`timescale 1ns / 1ps
module top(
input clock, // system clock
input reset, // BTNR
input version, // BTNU
input latch, // BTNL
input adc_n, adc_p, // VCAUX 6, P and N
output [15:0] led, // 16 onboard LEDs above the switches
output [6:0] segment, // 7-segment digit LED display
output dp, // "." next to each LED digit
output [3:0] digit, // which of the 4 digits to drive
output [7:0] JB, // debugging
output [7:0] JA //
);
parameter VERSION = 'h0001;
//
// the XADC input clock and an internal clock divider determine the conversion
// rate. we will shoot for 1MSps. XADC takes 26 clock ticks for a conversion, so
// to get the input clock with a divider of 4 we multiply 4x26x1MHz = 104MHz.
//
// to get such a frequency, use the Xilinx clock wizard. let's make a 104 clock
// for the XADC
//
wire locked, clock104;
clock_104 MYCLOCKS(
.reset(reset),
.clk_in1(clock),
.locked(locked),
.clk_out1(clock104)
);
//
// debounce the latch button
//
wire latch_level, latch_pulse;
debouncer DEBOUNCE_LATCH(
.clock(clock), .button(latch), .level(latch_level), .pulse(latch_pulse) );
//
// next drive the 4 7-segment displays
//
wire [15:0] display_this;
display4 DISPLAY (
.clk100(clock),
.number(display_this),
.digit(digit),
.segments(segment),
.period(dp)
);
//
// here is the XADC block
//
wire [6:0] daddr_in = 7'h16;
wire adc_ready, isbusy, adc_data_ready, eos_out, alarm;
wire [15:0] adc_data;
wire [4:0] channel_out;
myxadc XADC_INST (
.daddr_in(7'h16), // specifies vcaux6 pints to digitize
.dclk_in(clock104), // 50MHz clock
.den_in(adc_ready), // tied to adc_ready, tells adc to convert, tieing causes continuous conversions
.di_in(16'h0), // to set the data to something, not used here
.dwe_in(1'b0), // set to enable writing to di_in, which we don't want to do
.vauxp6(adc_p), // positive input to digitize
.vauxn6(adc_n), // negative input to digitize
.busy_out(isbusy), // tells you the adc is busy converting
.channel_out(channel_out[4:0]), // for using more than 1 channel, tells you which one. not used here
.do_out(adc_data), // adc value from conversion
.drdy_out(adc_data_ready), //tells you valid data is ready to be latched
.eoc_out(adc_ready), // specifies that the ADC is ready (conversion complete)
.eos_out(eos_out), // specifies that conversion sequence is complete
.alarm_out(alarm), // OR's output of all internal alarms, not used here
.vp_in(1'b0), // dedicated analog input pair for differential, tied to 0 if not used
.vn_in(1'b0)
);
//
// wait for XADC to tell you something is ready to latch
//
reg [15:0] r_adc_data;
always @ (negedge isbusy)
if (reset) r_adc_data <= 16'h0;
else r_adc_data <= adc_data;
//
// make a ~1Hz clock so we can run the LED display slower
//
reg [26:0] counter;
reg [15:0] s_adc_data;
always @ (posedge clock) begin
if (reset) counter <= 0;
else counter <= counter + 1;
end
wire clock_1hz = counter[26];
always @ (posedge clock_1hz) s_adc_data <= r_adc_data;
assign display_this = version ? VERSION : s_adc_data;
//
// latching the adc value for LED output from latch pushbutton
//
reg [15:0] latched_adc; // latched adc data
always @ (posedge latch_pulse)
if (reset) latched_adc <= 0;
else latched_adc <= r_adc_data;
assign led = latched_adc;
//
// some output for debugging if necessary
//
assign JA = {3'b000,eos_out,adc_data_ready,adc_ready,isbusy,clock104};
assign JB = {r_adc_data[15:12],adc_data[15:12]};
endmodule
All rights reserved. No part of this publication may be reproduced, distributed, or transmitted in any
form or by any means, including photocopying, recording, or other electronic or mechanical methods, without
prior written permission, except in the case of brief quotations embodied in critical
reviews and certain other noncommercial uses permitted by copyright law.
Unless indicated otherwise, any lecture handouts, exams, homework and exam solutions,
and the lectures themselves (including audio and video recordings) are copyrighted by
me and may not be distributed or reproduced for anything other than your personal use
without my written permission.
Last updated January, 2024 Drew Baden