Aside from my minimalist preference, there is a concrete benefit to bare metal code: certifiability; when I was in the defense industry for a few years after school, the software certification cost was running at about $100~$150 per LOC. I am sure it is more now.
Status (so you don't have to read the whole thing)
- Able to read the dedicated analog input and 15 other auxiliary channels in channel sequencing mode, kicked off from a timer interrupt (to turn off XADC when not in use).
- Have a thermistor circuit that can connect to the Zedboard.
- Reading out the XADC seems to take too long, even though it's just a bunch of memory mapped register read/write operations. I think it may be due to the XADC wizard performing a read over the DRP FIFO internally...
XADC peripheral
To recap, the 2 XADC peripherals on Zynq is (as described in the Zynq TRM) a 12-bit 1M sample/s HW, with up to 17 external analog channels each. There is a not-so-high-quality on-chip reference voltage is good enough for the internal temperature and voltage sensors, but the TRM recommended an external 1.25 V reference IC. The HW has these potentially useful features:- Remembering the maximum/minimum values in dedicated registers--until the next XADC reset.
- Interrupting based on threshold breach--so that the SW filtering can be bypassed. The maskable alarm is latched in the XADCIF_INT_STS register, and is cleared by writing to the same register.
- Averaging in HW: if the system's anti-aliasing filter BW is higher than the application's sampling BW, sampling the ADC fast and then averaging in HW may be the only option.
Q: why does UG480 say: "auxiliary analog inputs do not require any user-specificied constraints or pin locations... All configuration is automatic when the analog inputs are connected to the top level of the design."?
ADC sampling is divided into 3 phases:
- Acquisition: the capacitor in the ADC HW is charged from the input source. Depending on the input impedance, this may take longer than the default 4 ADCCLK.
- Conversion: the charge in the capacitor is converted to digital value, and written out to the appropriate register.
- Readout: SW should read out from the register--unless a streaming arrangement is made ahead of time.
In sequencing mode, the acquisition for the next channel is overlapped with the conversion of the current channel (the pipeline concept). If the acquisition requires more time, the conversion for the next channel can be made to start 10 ADCCLKs later by asserting the ACQ in the control register.
Control interface
The peripheral can be accessed through 2 bus (you have to choose):- PS-XADC: directly from the processor, through the APB bus. Commands are serialized through a 15-word deep FIFO, and against through a read FIFO on the way back. This is a lower speed interface, but more convenient than setting up the AXI channel to the XADC.
- PS-to AXI ADC: through the AXI bus.
It appears that to enable the PS-XADC interface, the XADCIF_CFG[ENABLE] bit has to be asserted, to enable the interface arbiter to choose the PS-XADC (instead of PL-JTAG). The serial interface is clocked by the PCAP_2x clock from the PS clocking subsystem (nominally 200 MHz), but divided by 4 * XADCIF_CFG[TCLK_RATE], to guarantee the serializer clock rate to maximum of 50 MHz. The interface is complicated slightly by its SPI-like behavior: the HW pushes out data to the read FIFO (read through the XADCIF_RDFIFO register) only when the write FIFO is written to, so that a NOOP command (XADCIF_CMD[29:26] = 4'b0000) is necessary to read the value of a single read. In a continuous read case, such NOOP would not be required. I can see that concretely in the XSDK BSP generated examples.
A: BSP thinks I am using sysmon feature to talk to the XADC. xparameters.h:
#define XPS_SYSMON_INT_ID 39
It also thinks my base address is different than what I was assigned! XADC inteferface configuration is apparently not the same as the AXI 4 Lite interface to the XADC wizard IP.
#define XPAR_PS7_XADC_0_BASEADDR 0xF8007100
XSDK BSP generated PS XADC example
Q: why is the INTR_ID in the generated code 39?A: BSP thinks I am using sysmon feature to talk to the XADC. xparameters.h:
#define XPS_SYSMON_INT_ID 39
It also thinks my base address is different than what I was assigned! XADC inteferface configuration is apparently not the same as the AXI 4 Lite interface to the XADC wizard IP.
#define XPAR_PS7_XADC_0_BASEADDR 0xF8007100
This register is shown in the Zynq TRM, XADCIF_CFG, in section B.16, Device Configuration Interface. What is confusing about this at first is that this is NOT the direct interface to the XADC HW itself, but rather the SPI-like FIFO to push the commands to. To control the XADC from the CPU, several writes to the device configuration register blocks were required:
- Write a magic value (0x757BDF0D) to the unlock register (offset 0x34 in the configuration register base address of 0xF807000). This write is a simple memory mapped register write.
- And then the example writes to the XADCIF_CFG register (0xF8007100), turning on the following bits:
- ENABLE
- CFIFOTH = 0xF
- DFIFOTH = 0xF
- Clear the miscellaneous control (MCTL), to release XADC reset
- Reset XADC by bouncing the reset bit in MCTL register just discussed above.
- Write some values into the alarm threshold (like VCCINT_UPPER, which is at 0x51 in the XADC internal register banks) and read it back. This tests the command FIFO to the XADC. Internally, this is what happens during write/read:
- Write
- 32 bit message is formatted into this 32-bit JTAG message
- MSB: write (0x08) or read (0x04)
- register address left shifted by 16 bits (address bits are position [25:16])
- 16-bit data in the 2 LSB
- Write into the command FIFO (@ 0xF8007110).
- Read the Read FIFO (@ 0xF8007114) after any write since for each write, one location of Read FIFO gets updated. Throw the read value away.
- Read
- Create a dummy data (0) to write into the write FIFO: 0x04000000 | (reg offset << 16)
- Write into the CMDFIFO
- Read from the RDFIFO TWICE, and keep only the 2nd read
To configure the XADC for real now,
- Set sequencer mode to the default mode (value 0 to CFG1[15:12]), to prevent alarms from tripping.
- Disable all alarms (value 0 to CFG1[11:0]).
- Restore the on-chip temperature and voltage alarms.
- Register SCU interrupt handler. This interrupt handler reads the XADC int status register @ 0xF8007104 (which only tells you about the FIFO threshold, overtemp, or alarm)--these are not what I am interested in.
- Change the sequencer mode again.
- Read internal registers, like temperature (0x00) and Vccint (0x1)
While it IS possible to control the XADC HW directly through the built-in control path in the PS, it appears that going through the memory mapped XADC wizard register will be simpler (next).
XSDK BSP generate sysmon example
To reset the XADC wizard IP (I guess Xilinx calls it sysmon), example writes 0xA to the XADC wizard base address. Self test consists of writing and reading back the Vccint alarm threshold--over memory mapped 32-bit register access. To configure the sequencer, write safe (default) sequencer mode to the CFR1 (configuration register 1) bits [15:12] @ offset 0x304 from the XADC wizard base address. This is apparently necessary before enabling channels (by writing to the group of 8 registers @ offset 0x320 ~ 0x33C). The format of these channel sequence registers are NOT given in PG091 (XADC wizard IP documentation), but rather in UG480 (XADC HW documentation), Chapter 4, Automatic Channel Sequencer. To add even more confusion, sequence 8 and 9 configuration registers are NOT even continuous with the rest, but in what PG091 calls "test register" block: @ offset 0x318 and 0x31C, and therefore the bits of the register are not even documented.
XADC clock rate is configurable by changing the clock divisor in XADC wizard configuration register 2, @ offset 0x308 (documented in UG480, table 3-6).
Config register @ offset 0x300 MUX bit must be asserted--with an associated input channel--to use the external mux feature.
The BSP generated example only checks for EOS (end of sequence), and reads out all channels. When reading the converted value, one generally reads from blocks at offset 0x200 through 0x2B8. What is NOT obvious from the documentation that is clarified in the example code is that while the XADC input is shared for different channels, the conversion results are stored in the respective readout registers according to the channel. Curiously, the example does NOT right shift the 32 bit readout value, even though the documentation says that the data is MSB justified.
I/O
As you can see in this image, the XADC can become an IO pin hog. If I wanted to sample 16 channels, I would have to sacrifice 32 I/O pins (UG480: "all analog input channels are differential and require two package balls... The analog inputs of the ADC use a differential sampling scheme to reduce the effects of common-mode noise signals")!
Vp/Vn pins should be grounded if not used. Normally, the Vivado XADC wizard will only expose used pins, according to the channel selection mode. But it seems that the XADC wizard always instantiates the Vp/Vn pins.
A way to reduce the I/O pin count is to use an external multiplexer. For example, when in simultaneous mode, two 3-bit multiplexer chip (something I have to solder outside Zynq chip) can select among the 8 pairs of input channels, as you can see below:
XADC wizard configuration does not seem to expose this feature, because I cannot select a pair of inputs into the XADC wizard module...
Zedboard XADC
That the Zedboard is almost the "Cadillac of the Zynq eval boards" can be seen when you see the external reference voltage available for ADC, and that the analog ground is decoupled from the digital ground with a ferrite bead.
Reference voltage
ADCs need reference voltage; sometimes for the external circuit, but often for internal digitization implementation as well. XADC reference voltage requires 1.25 V, which should be placed as close as possible to the reference pins and connected directly to the V REFP input, using the decoupling capacitors recommended in the reference IC data sheet.
Zedboard is generating the 1.25V Vref, as you can see here:
This Vref is available for circuits that need high quality reference voltage (like thermistors) on the Zedboard's XADC header (pin 11):
If I want to use some other reference voltage than the Zedboard generated one above, I can just lift R186 below, and connect pin 11 above to the desired reference voltage.
This Vref is available for circuits that need high quality reference voltage (like thermistors) on the Zedboard's XADC header (pin 11):
If I want to use some other reference voltage than the Zedboard generated one above, I can just lift R186 below, and connect pin 11 above to the desired reference voltage.
For XADC on Zynq, Vref pin should be tied to ground if internal reference is to be used.
The DXP/N (XADC-DXP/N: pins 7/12; according to Zynq TRM Table 2-13 PL Pin Summary, these are temperature sensing diode pins) are bit of an anachronism; according to a Xilinx forum discussion, they should not be used any more.
The DXP/N (XADC-DXP/N: pins 7/12; according to Zynq TRM Table 2-13 PL Pin Summary, these are temperature sensing diode pins) are bit of an anachronism; according to a Xilinx forum discussion, they should not be used any more.
XADC input header on Zedboard
As shown in the XADC header pin assignment above, there are 3 differential pairs of ADC input accessible on the XADC header Zedboard (DXP/N are completely independent of XADC):- XADC-VP/N: dedicated analog input pair on pins 2/1. There is only 1 such dedicated analog input on the XADC. When not used, these should be shorted to ground.
- XADC-AD0_P/N: pins 3/6
- XADC-AD8_P/N: pins 8/7
This is hardly sufficient for low frequency signals like those from an accelerometer, for which 100 Hz BW is more appropriate and can be accomplished by using a 10 kOhm resistor and a 1 uF capacitor combination. This is easily possible with an extra 1 uF capacitor across the P/N pins, and a 1 kOhm resistor on the positive pin.
An FMC connector breakout board allows access to all other differential input pairs, all of them on bank 35 (VADJ).
An FMC connector breakout board allows access to all other differential input pairs, all of them on bank 35 (VADJ).
Analog ground
When I looked at the accelerometer signal on a scope, I connected the analog ground to the Zedboard's digital ground. It is NOT advisable to use the digital ground as an analog ground reference for XADC, because the ground "shakes" in response to the high frequency digital currents. In an effort to improve the ADC performance, a dedicated supply and ground reference is provided on the Zedboard: A ferrite bead filters out the high frequency noise from the command ground is fed to the Zynq's analog ground input pin GNDADC_0, as you can see below.
The jumper J12 lets you bypass this ferrite bead, although I cannot imagine why you would want to do that. It is this ground that I want to use for the analog ground of the accelerometer, so I should short JP12's pin 1 to the common analog ground output of the accelerometer AND the negative pins of the XADC's differential input pins XADC-VN, XADC-AD0_N, XADC-AD8_N in the XADC header.
I cannot directly connect the accelerometer output's 3.3V to the XADC positive pins, because the maximum voltage difference between the XADC P/N pair is 1 V; I have to use a voltage divider instead; something like 2.5 kOhm and 1 kOhm should work.
Since the interrupt goes high for any of the interrupt conditions that can be enabled through the interrupt enable register, the eoc_out, alarm_out, eos_out seem redundant. But because I am only interested in the eoc for now (or eos, if I use the channel sequencer in the future), I connect eoc_out to the only free pin left in the interrupt concatenator (sys_concat_intc[2]). As explained in a previous blog, the Zynq interrupt ID for this interrupt line would be 89.
The existing "Ubuntu on Zedboard" already contains an AXI interconnect for the AXI Lite interfaces. I keep adding more IPs to this multiplexer, as shown in a previous blog. I just add another AXI4Lite port, and connect that to the s_axi_lite interface shown above. As before, I let Vivado assign register address for this new module, and obtain 0x43C10000 as the base address.
Although I will only sample 1 channel in this investigation and therefore do NOT need a MUX, I will bring out the MUX to the top level for posterity's sake. muxaddr_out should be used with an external 5 bit mux, to switch between channels. While this approach saves pins, this presents a problem to a multi-axis device such as an accelerometer, whose 3 axis should ideally be sampled simultaneously. Even with the simultaneous sequencing mode, XADC cannot sample more than 2 channels at the same time. To expose muxaddr_out bus to the top level, right click on the bus in the system diagram view --> make external --> rename to XADC_mux. Similarly, make the ADC input channel Vp_Vn external (and optionally, rename the interface to XADC_in).
Conversion can be started by driving const_in high, but I left it disconnected because I will start the conversion from the SW.
After generating the HDL wrapper (right click on the block design --> Sources --> Hierarchy view --> system.bd --> Generate HDL wrapper), these are the new interface wires to the system:
input XADC_in_v_n;
input XADC_in_v_p;
output [4:0]XADC_mux;
These wires should be brought out to the top level Verilog file verbatim, so the block diagram's pins can be constrained. Therefore, the top level module will just copy these wires verbatim as well. Finally, they have to be constrained. This is what PG091 said about constraining the XADC input pins: "VP/VN and 16 VAUXP/VAUXN pin pairs do not need LOC constraints to be specified in XDC. VP/VN is a dedicated input and VAUXP/VAUXN I/Os are dual mode I/O for 7 series FPGAs.
Vivado tool performs placement of these analog inputs automatically, but VP/VN and 16 VAUXP/VAUXN pin pairs need analog I/O standard constraints for the implementation." These are the Zynq XADC VP/N pins I read off the Zedboard schematic:
set_property -dict {PACKAGE_PIN L11 IOSTANDARD LVCMOS33} [get_ports XADC_in_v_p];
set_property -dict {PACKAGE_PIN M12 IOSTANDARD LVCMOS33} [get_ports XADC_in_v_n];
The mux output can be placed anywhere, so I chose the unused OLED pins.
set_property -dict {PACKAGE_PIN V20 IOSTANDARD LVCMOS33} [get_ports XADC_mux[0]];
set_property -dict {PACKAGE_PIN U20 IOSTANDARD LVCMOS33} [get_ports XADC_mux[1]];
set_property -dict {PACKAGE_PIN V19 IOSTANDARD LVCMOS33} [get_ports XADC_mux[2]];
set_property -dict {PACKAGE_PIN V18 IOSTANDARD LVCMOS33} [get_ports XADC_mux[3]];
set_property -dict {PACKAGE_PIN AB22 IOSTANDARD LVCMOS33} [get_ports XADC_mux[4]];
After a successful bitstream generation and export (of the hardware definition and the bitstream), the memory mapped registers for xadc_wiz_0 appears at 0x43C10000, which the SW can then access.
I cannot directly connect the accelerometer output's 3.3V to the XADC positive pins, because the maximum voltage difference between the XADC P/N pair is 1 V; I have to use a voltage divider instead; something like 2.5 kOhm and 1 kOhm should work.
Including XADC IP in a Zynq HW design
While Zedboard itself is ready for XADC, the XADC is an optional IP in Zynq; the "Ubuntu on Zedboard" Zynq reference HW design I have been using for the bare metal, hard-real-time SW development till now LACKS the XADC block. To include it, open the system.bd (block diagram) in a Vivado project--> click the "Add IP" icon (the one with the + symbol) on the left toolbar --> Enter "xadc" in the search window --> double-click on XADC Wizard --> double-click on the resulting IP block to configure it. To understand the configuration options for the XADC Wizard, read Xilinx document PG091, rather than UG480 which explains the HW level primitive that the wizard encapsulates. The following setup targets human-machine interface application that uses accelerometer and mic, and 1 thermistor for ambient temperature sensing.
- Basic tab
- Leave the interface as AXI4Lite, to control XADC from the SW through memory mapped registers
- Startup channel: sequencer, with MUX, with Vp/n and all Vaux pairs enabled (except for vaxu4 pair, which cannot be selected for some reason, as shown below)
- Single channel mode is inefficient if many channels need to be sampled, because setting up a channel, and then waiting for the conversion takes many clock cycles (26 ADCCLKs for signal acquisition and conversion in a continuous mode, but commanding the XADC over the shared and slow AXI4Lite bus will take many more cycles). The biggest problem with the single channel mode is that a channel is selected (fixed) in the wizard, so there is no chance to monitor multiple channels without using an external mux.
- In all products I worked on, there are MANY ADC channels to monitor, so either a sequencer or simultaneous (sequencing) mode are used.
- In simultaneous mode, 8 pairs channels can be converted simultaneously. The pairs are hard coded in HW to 0/8, 1/9, ..., 7/15. Note that Vp/Vn pair cannot be sampled in this mode. In XADC wizard IP configuration window, selecting simultaneous selection does NOT add another mux input, adding to the confusion.
- Independent ADC is necessary if you need to monitor the Zynq die voltage/temperature constantly, even while monitoring external channels.
- Temp bus if for monitoring the DRAM internal temperature, which I am going to ignore for now.
- Timing mode: event mode vs. continuous; since temperature does not change that fast, there is no need to continuously sample the channels at maximum throughput. Actually, I've used continuous sampling in the past mostly for convenience.
- Event mode trigger: convst_in. Since the IP acts on the OR of the convst_in and the CONVST register (which I will control), I conected convst_in to a Const IP module to pull it up.
- ADC setup
- I enabled external mux even though I only want to sample 1 channel (external thermistor) for now, because a real application will have many channels. I will use the dedicated analog input channel Vp/n.
- Alarms tab: turn off all temperature and voltage alarms, to reduce the pin count of the core.
- Note: a simple control application implements its own alarm in SW, but some safety critical application may want a SW independent alarm trip (that can inhibit the circuit that is driving significant current/voltage).
When using the mux, the SW cannot control which channel to sequence: the XADC picks the next MUX channel, as you can see from this screenshot of UG480:
My understanding is that the core remembers the current channel, and advances to the next one. XADC can (in simultaneous sampling mode) sample 2 simultaneous inputs, with 2 muxes.
The resulting XADC interface, with only the pins I connected (see explanation below) are shown:
The existing "Ubuntu on Zedboard" already contains an AXI interconnect for the AXI Lite interfaces. I keep adding more IPs to this multiplexer, as shown in a previous blog. I just add another AXI4Lite port, and connect that to the s_axi_lite interface shown above. As before, I let Vivado assign register address for this new module, and obtain 0x43C10000 as the base address.
Although I will only sample 1 channel in this investigation and therefore do NOT need a MUX, I will bring out the MUX to the top level for posterity's sake. muxaddr_out should be used with an external 5 bit mux, to switch between channels. While this approach saves pins, this presents a problem to a multi-axis device such as an accelerometer, whose 3 axis should ideally be sampled simultaneously. Even with the simultaneous sequencing mode, XADC cannot sample more than 2 channels at the same time. To expose muxaddr_out bus to the top level, right click on the bus in the system diagram view --> make external --> rename to XADC_mux. Similarly, make the ADC input channel Vp_Vn external (and optionally, rename the interface to XADC_in).
Conversion can be started by driving const_in high, but I left it disconnected because I will start the conversion from the SW.
After generating the HDL wrapper (right click on the block design --> Sources --> Hierarchy view --> system.bd --> Generate HDL wrapper), these are the new interface wires to the system:
input XADC_in_v_n;
input XADC_in_v_p;
output [4:0]XADC_mux;
Vivado tool performs placement of these analog inputs automatically, but VP/VN and 16 VAUXP/VAUXN pin pairs need analog I/O standard constraints for the implementation." These are the Zynq XADC VP/N pins I read off the Zedboard schematic:
- VP: L11
- VN: M12
set_property -dict {PACKAGE_PIN L11 IOSTANDARD LVCMOS33} [get_ports XADC_in_v_p];
set_property -dict {PACKAGE_PIN M12 IOSTANDARD LVCMOS33} [get_ports XADC_in_v_n];
The mux output can be placed anywhere, so I chose the unused OLED pins.
- XADC_mux[0] --> V20
- XADC_mux[1] --> U20
- XADC_mux[2] --> V19
- XADC_mux[3] --> V18
- XADC_mux[4] --> AB22
set_property -dict {PACKAGE_PIN V20 IOSTANDARD LVCMOS33} [get_ports XADC_mux[0]];
set_property -dict {PACKAGE_PIN U20 IOSTANDARD LVCMOS33} [get_ports XADC_mux[1]];
set_property -dict {PACKAGE_PIN V19 IOSTANDARD LVCMOS33} [get_ports XADC_mux[2]];
set_property -dict {PACKAGE_PIN V18 IOSTANDARD LVCMOS33} [get_ports XADC_mux[3]];
set_property -dict {PACKAGE_PIN AB22 IOSTANDARD LVCMOS33} [get_ports XADC_mux[4]];
After a successful bitstream generation and export (of the hardware definition and the bitstream), the memory mapped registers for xadc_wiz_0 appears at 0x43C10000, which the SW can then access.
A thermistor circuit driven by 1.25 Vref
For this experiment, I will use a 10 KOhm thermistor (this is the resistance at 20 C), which has an inverse relationship between the measured temperature and the resistance. The input to the XADC Vp should be the high voltage side of the thermistor and the Vn should be the analog ground, in a voltage divider configuration, like this:
The 5 KOhm value shown above is just an example. To properly size the voltage divider resistor, we have to know the expected operating temperature, and more importantly, the nominal resistance of the thermistor at those 2 extremes. The Vp-Vn into the XADC will be the greatest when the thermistor resistance is high--that is, when the temperature at themistor is low. Let's say we want to measure between 0 C to 50 C. An NTC themistor I have the following resistance values:
- @ 0 C, 32336 Ohm
- @ 50 C, 3635 Ohm
- @ 100 C, 700 Ohm
If I want Vp-Vn to be 1.0 V at 0 C, then I have to solve for the voltage divider equation: 32336/(32336 + R) = 1.0/1.25 => 32336 * 1.25 = 32336 + R => R = 32336 * (0.25) = 8084. Using a 10 kOhm resistor, these are the Vp-Vn values I would see:
- @ 0 C, Vref * 32336 / (32336 + 10k) = 0.954 V
- @ 50 C, Vref * 3633 / (3633 + 10k) = 0.333 V
The ln(Rthermistor) is NON-linear, and the slope flattens out at higher temperature; on the cold side. At the highest temperature, the thermistor resistance is roughly 3 kOhm, so putting a 1 uF capacitor across Vp-Vn would give me a low-pass filter with bandwidth of 1/(3k * 1E-6) ~ 330 Hz, which would reject a lot of high frequency noise. 1 uF capacitor would do an even better job of high frequency noise rejection.
The low-pass resistor 100 kOhm introduces a bias term (on the order of 1/10th) during transient. A larger resistor (and a smaller capacitor) would reduce the error.
Bare metal C++ to read XADC wizard
Most ADC examples you will find on the web uses continuous sequencing, which is easier to program than "sequence only when needed" but wastes power. In this code, I kick off a channel sequencing (sampling multiple channels in sequential order) in a system tick timer. Since the XADC HW should be completely deterministic, the data should be available at a deterministic time WRT when it is needed (extra delay between ADC sampling and control value computation is a seldom discussed but a subtle contributor to a control loop instability).
The XADC wizard HW interface is completely over the AX4Lite shared bus, shown above. Using the base register address assigned by Vivado, a generic macro to access the 32-bit register is:
#define XADC_WIZARD(offset) (*(volatile uint32_t*)(0x43C10000 + offset))
POST (power-on-self-test)
It is a good practice to perform some POST in FW, by explicitly resetting the HW to the default state and performing a basic "wiggle toe" test.
XADC_WIZARD(0) = 0xA;//reset is active for 16 clock cycles; see PG091, SRR
//POST the XADC wizard
XADC_WIZARD(0x340) = 0x55;//This is the test value the BSP example used
Q_ASSERT(0x55 == XADC_WIZARD(0x340));
XADC_WIZARD(0) = 0xA;//reset is active for 16 clock cycles; see PG091, SRR
In case I am writing to the wrong register that does NOT remember the values I wrote, the assert will park the FW in an error state, so that it will be obvious to all.
Initialize the XADC for 16 channels
To configure the sequencer, it must first be put into the default (safe) mode:
#define XADC_SEQUENCE_SAFE (0 << 12 | 0xFFF)
#define XADC_SEQUENCE_OFF (3 << 12 | 0xFFF)
#define XADC_SEQUENCE_CONTINOUS (2 << 12 | 0xFFF)
#define XADC_SEQUENCE_1PASS (1 << 12 | 0xFFF)
//Disable channel sequencer before configuring the sequencer
XADC_WIZARD(0x304) = XADC_SEQUENCE_SAFE;
Select the 16 channels. Given the Zynq's power and flexibility, I hope to read nearly all of the 16 channels.
XADC_WIZARD(0x320) = 1 << 11;//Vp/Vn pair
XADC_WIZARD(0x324) = ~(1 << 4);//Enable all Vaux pairs, except Vaux4
//Leave averaging, input-mode (bi/unipolar), and acquisition time at default
Then enable interrupts:
XADC_WIZARD(0x60) = XADC_WIZARD(0x60);//Clear any pending interrupts
XADC_WIZARD(0x5C) = 1 << 31;//PG091, GIER (global interrupt enable) register
XADC_WIZARD(0x68) = 1 << 4;//PG091, IPIER register, bit EOS
Note that I am only interested in the end of sequencing (vs. end of conversion for each channel).
Enable external mux, which is necessary to save I/O pin counts:
//Enable external mux and connect to Vp/n
XADC_WIZARD(0x300) = 0 << 12 // no averaging
| 1 << 11 //enable mux
| 0 << 9 //event driven sampling: doesn't work?
| 3 << 0; // mux input channel = Vp/n
Finally, put the XADC in low power mode until needed:
//Turn off all alarms and enable calibration; see UG480 Table 3-9
XADC_WIZARD(0x304) = XADC_SEQUENCE_OFF;//Write to the config register 1
XADC_WIZARD(0x308) = 3 << 4; //power down ADC to save power
Start ADC acquisition and conversion from a system tick handler
My system tick timer handles timeout driven processing, so I want that to be jitter-free. Even though starting the sequence itself is deterministic and can potentially be done before I handle the timeout event in the FW, the timeout handling code for all my state machines may take more than the sequencing (of the 16 channels). If so, the timeout handler may get interrupted by the completion of the ADC sequencing. While I write the code to be re-entrant in general, avoiding multi-thread contention is a good practice in general. If sampling ADC and then running a control algorithm is deemed higher priority, I can easily move the following code to BEFORE my timeout handler.
//process the system tick (Q_TIMEOUT_SIG)
//Start the ADC conversion AFTER systick processing is done, to
//keep the Q_TIMEOUT_SIG jitter-free
XADC_WIZARD(0x308) = 0;//Start the XADC clocks
//Start another pass through sequence; PG480
XADC_WIZARD(0x304) = XADC_SEQUENCE_1PASS;
It takes MANY clock cycles to acquire and convert the 16 channels, as you can see in the scope capture below, where the 1st pulse is the timeout interrupt handling (at the end of which I start the acquisition, as explained above):
Eyeballing from the scope capture timeline, it takes about 19 usec for acquisition and conversion to complete, and another 5 usec to read out the ADC values from the registers (code given below).
Handle the EOS (completion of ADC sequencing) interrupt
Please remember from the above discussion that in my HW design that uses an FPGA to CPU interrupt concatenator, the XADC interrupt was mapped to interrupt ID 89, which I enum to INT_ID_XADC. The first thing I should do is acknowledge the interrupt (turn it off)
case INT_ID_XADC: {
uint32_t status = XADC_WIZARD(0x60);//PG091, IPISR register
XADC_WIZARD(0x60) = status;//acknowledge interrupt
XADC_WIZARD(0x304) = XADC_SEQUENCE_OFF;
If the interrupt is EOS, I can read out the data.
if(status & (1 << 4)) {//EOS (end of sequence)
XADC_val[0] = XADC_WIZARD(0x20C);//PG091 table 2-4, Vp/Vn
XADC_val[1] = XADC_WIZARD(0x240);
XADC_val[2] = XADC_WIZARD(0x244);
XADC_val[3] = XADC_WIZARD(0x248);
XADC_val[4] = XADC_WIZARD(0x24C);
XADC_val[5] = XADC_WIZARD(0x254);
XADC_val[6] = XADC_WIZARD(0x258);
XADC_val[7] = XADC_WIZARD(0x25C);
XADC_val[8] = XADC_WIZARD(0x260);
XADC_val[9] = XADC_WIZARD(0x264);
XADC_val[10] = XADC_WIZARD(0x268);
XADC_val[11] = XADC_WIZARD(0x26C);
XADC_val[12] = XADC_WIZARD(0x270);
XADC_val[13] = XADC_WIZARD(0x274);
XADC_val[14] = XADC_WIZARD(0x278);
XADC_val[15] = XADC_WIZARD(0x27C);
XADC_WIZARD(0x304) = XADC_SEQUENCE_OFF;
XADC_WIZARD(0x308) = 3 << 4;//Stop the XADC clocks
}
It was not obvious at first why the sequencer could not be left in a single pass mode, until I found a cryptic passage in the datasheet while debugging: the sequencing starts when the mode CHANGES to single pass mode! Stopping the clock is the PRINCIPLE method of saving power, but I have to wait until the sequencing is complete.
Result without any circuit connected to the Vp/n input (just reading noise)
This is what I see in the JTAG debugger:XADC_val long unsigned int [16] [0x000020b7, 0x000020d2, 0x000020c3, 0x000020d1, 0x000020c7, 0x000020d0, 0x000020d5, 0x000020d1, 0x000020bd, 0x000020ca, 0x000020b5, 0x000020c6, 0x000020d7, 0x000020ca, 0x000020bd, 0x000020c9]
Since the XADC is a 12-bit ADC, the leading '2' is strange, and probably an invalid bit that should be thrown away. The values change with every read, so I think the read values are indeed noise.
Note that the Xilinx documentation is WRONG: it said the data is MSB justified. Clearly, the data is LSB justified!
Removing the XADC Linux device driver from the kernel
The XADC device driver can be removed from the DTS file, but that will leave unused in the kernel. For a thorough excision, I changed the kernel config like this:
# CONFIG_XILINX_XADC is not set