FPGA XADC

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:

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.

Xilinx XADC

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.

Xilinx XADC Timing

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.

XADC Project

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:

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;

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:

    //
    //  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)
    );

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:

    //
    //  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

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:

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
    );

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:

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];

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):

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));

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:

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

This code is instantiated 4 times inside display4 like this:

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) );

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.

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 

The entire code can be found here. It is instantiated inside top.v like this:

    //
    //  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)
        );    

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.

XADC Instantiation Using IP Wizard

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:

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:

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:

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)
    );

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):

 
        .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    NameComments
40'h0016config 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'h31AFconfig reg 1 disable temperature alarms, enable ADC gain corrections, disable offset corrections, set single channel mode
42'h0200config reg 2 ADCCLK = dclk_in divided by x2
As detailed above, the way to use the XADC is to wait for the signal adc_data_ready to be asserted, and when it is, latch the data from the .do_out output adc_data into what we can call r_adc_data. So we can use the following code.
    //
    // 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;

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:

    //
    //  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;

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.

    //
    //  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]};

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:

## 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]}]

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).

XADC Timing

In the above code we have brought out some signals to look at with a logic analyzer:

    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.

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:

  1. Latch on the posedge of adc_data_ready as in the diagram above
  2. Latch when adc_data_ready is high
  3. Latch on the negedge of isbusy
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.

So I recommend that in the above code where we form the signal r_adc_data:

    //
    // 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;

that it be changed to

    //
    // 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.

The entire code for top.v is 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

The above project archive can be found here.


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