Analog Devices AD7606-8 on Raspberry pi zero W


Abstract :
This article documents the development and testing of the Linux driver module and the interface between the Raspberry Pi GPIOs and the AD7606-F4 development board. The AD7606-F4 is based on the AD7606-8, The 8 channel version of the AD7606, true bipolar, ±5V / ±10V input range, 16 bit, with 1 to 64 hardware oversampling, simultaneous sampling ADC. The driver development effort is focused on the 16 bit parallel interface mode instead of SPI. It leverages the Linux industrial IO layer. The goal is mainly to characterize maximum achievable sample rate / triggering rate, as well as sample loss and jitter induced by kernel burden and scheduling, depending on triggering rate, while using “worst case” interfacing. No backend IC is used to perform hardware protocol management and hardware buffering in our tests.
Using a small SOC with limited RAM and CPU capabilities also conforms with worst case performance testing, and will allow us to identify the limiting factor of such a setup.
Initial driver development was performed by Analog Devices and the driver maintainer and is available in the linux kernel source. It lacked however several features, but offered a good starting base for a fully functional driver. The available driver is split into two source files, one for the parallel interface and one for the SPI interface.
We will explore the various methods of interacting with GPIOs inside the kernel module, as to improve the conversion/read cycle time for all channels.
The maximum sampling speed is mainly dictated by the kernel load, as a direct interfacing of the AD7606 to the Raspberry Pi requires handling interrupts at the sampling speed. The main limiting factor however seems to be the gpiod_set_raw_value() execution time, and thread synchronization between the trigger handler and the IRQ handling callback, limiting sampling rate short of 8 ksps.
IIO provides a mechanism, called backend, to interface signaling (such as conversion start, handling of IRQ and first channel read detection) by an intermediary device such as an IP core. This provides also hardware buffering so that IIO gets data from the hardware buffer in chunks, so that IRQ loading to the kernel is lowered.
Note that besides kernel performance limitations, high sampling rates also require application of common hardware interfacing best practices, such as :
- Minimising trace or cable length to minimize loop inductance
- Keeping same trace length, but it is secondary since the baud rate is still conservative even at the max rate allowed by the ad7606, dicated by the minimum timings specified in the datasheet.
- Adding small values resistors such as 51 ohm on the lines to minimize ringing
- Adding guard traces grounded on one side to reduce crosstalk and inductive coupling between data lines, and between signalling lines.
- The GPIO pin layout vs the line pin layout on the AD7606-F4 dev board we are using cannot allow the use of a 40 pin ribbon cable such as those use for ATA UDMA transfer, as it would require crossing over lines, so a mating PCB or hat design is required.
- Low sampling rates can be achieved using standard dupont cables, and assessing the sampling rate limits for such a solution is one of the goals of this article.
Note that using 20cm dupont pin cables, without current/ringing limiting resistors as inductive dampers, it is highly improbable to reach sampling rates such as 48 ksps as found on audio interfaces using backend-less interfacing. Pushing the sampling rate above 8 ksps would mainly require, in that order :
- MMIO for signalling (CONVST, CS/RD strobes) instead of reliance on gpiod_set_raw_value()
- Using try_wait_for_completion() in a preempt_disable() / preempt_enable() context
- A more robust and faster SOC, with dual core at least to enable IRQ pinning on a core
- Using a RT linux kernel
- A hat or PCB mating interface to improve signal integrity
However, determining the practical limit of such a solution is useful as it is the most cost effective and fast way to interface a Raspberry Pi or other SOC to perform ADC operations using the industrial IO layer.
The Github link to the kernel module driver and its associated helpers is available at the very end of this article.
General interfacing using the parallel interface layer.
The AD-7606-F4 development board we used and available on Aliexpress or other chinese marketplaces has a small footprint. The development board we got does not exposes the standby pin, and was factory set for parallel interface transfer through a SMD resistor on the 8080 pad identified by silkscreen marking. Swapping the resistor into the SPI pad would obviously enable SPI mode. Note that the AD7606, to achieve full sampling rate performance for SPI requires dual SPI mode.
In parallel mode, the signalling requires at least the following lines connected between the AD7606 and the Raspberry Pi :
We will use the Raspberry Pi (host interface, as the reference for IN/OUT specification)
- CONVSTA/CONVSTB : these two lines can be tied together into a single GPIO on the Raspberry Pi. These are OUT pins. They are strobed low to signal that we want to initiate a conversion. if the pins are strobed low at the same time, all 8 channels are simultaneously sampled, if strobed independently, they allow sampling of the first 4 and last 4 channels independently, the sampling delay allows compensation of group delays induced by filters or CT vs VT in power grid measurements.
- BUSY : IN pin, the falling edge indicates end of conversion and signals the SOC (through GPIO IRQ management) that it is ready for data reading of all channels.
- FRSTDATA : IN pin, this signals the readout of the first channel, so that there are no channel alignment issues : if FRSTDATA level is not high when the SOC reads the first channel as the driver internal state indicates, or if FRSTDATA level is high when the SOC expects to read subsequent channels, it should be treated as an IO error and all channels samples should be discarded from the read cycle. The AD7606 should be reset through the RESET pin. Monitoring RESET events from this condition is recommended in the debugging phase, as it indicates potential hardware interfacing problems and instability. If too much FRSTDATA mismatches occur, try increasing timing delays for all pin strobes.
- RESET : OUT pin, commands the AD7606 to reset. Used if an inconsistent state is detected, see above. A reset event will lower sampling rate, as the interface needs some time after the reset to be fully operational. Reset is also called in the probe function at device initialization as the ad7606 should be reset after power-up or resume from standby.
- CS/RD : OUT pins. tells the AD7606 to shift next channel into the parallel interface registers, and select the AD7606 if they are stacked on the same parallel line. if exactly one AD7606 is present on all lines, then CS/RD can be strobed simultaneously (linked mode). Signaling is easier this way, as independent CS/RD management gives a slightly different timing protocol. Refer to the datasheet for independent management of CS/RD. Our driver uses linked CS/RD mode, but our tests were performed with two separate lines driven concomitantly. (GPIO levels set on two pins synchronously)
The remaining pins, RANGE, 0S0, 0S1, 0S2 are configuration pins used to select the input voltage range specification (+-5V or +-10V) and OSx pins are used to set the bits that select the oversampling mode.
The AD7606-F4 dev board does not expose the STBY pin on the 40 pin header.
The “VO” pin on the AD7606-F4 corresponds to the AD7606 Datasheet pin mnemonic “VDRIVE“. This pin should be connected to the 3.3V line of the Raspberry Pi. This line supplies the logic power and sets the levels used on the data pins, FRSTDATA, and BUSY.
In our setup, leftover pins are GPIO0, GPIO1 and GPIO27. GPIO0 and GPIO1 are reserved and used by external EEPROM hats, they can be reclaimed using the force_eeprom_read=0. This prevents EEPROM boot time operations on these pins. We did not test these pins in our use case. GPIO27 is leftover.
If CS/RD were tied, another pin would be returned to the pool of free pins, making 4 leftover pins.
Oversampling can be configured statically to reclaim 3 more pins
In order to avoid interface conflicts, raspi-config or edition of /boot/firmware/config.txt should be performed to disable pin functions such as UART, SPI, I2C, 2wire, EEPROM.
Parallel interface Data Pins
The AD7606 provides byte mode parallel transfers besides word parallel mode, in byte mode, each channel data is supplied to the host through two successive data reads. (MSB/LSB or LSB/MSB). This mode halves the maximum achievable data rate, but allow recovery of 8 pins for other uses.
The 16 bit (word) data transfer rate, require a single CS/RD or RD strobe per channel read, at the expense of using a full 16 pin block. This is the mode we used in our demo.
Note that data line pin mapping between the host and AD7606 should be contiguous and ordered to prevent unnecessary bit reordering and masking. GPIO order is determined by the BCM GPIO pin nomenclature, not pin ID number.
Bit shifting is required however if the data lines start at an offset, that is, if they don’t start at GPIO0.
In our use case, we used the GPIO8 to GPIO23 range as the 16 parallel GPIO lines. ARM architecture is optimized to read 32 bits, with correct alignment when using MMIO (memory mapped IO), which means, in our case, reading 4 bytes at GPIOLEV0 offset, and extracting the 2 center bytes, which requires a simple bit shift, bit mask and cast to u16 operation. for MMIO, we used the industry tested readl() MMIO function, although recent kernel practices seem to shift towards the use of ioread32(), Which seems to work well too in our tests.
MMIO in kernel space on the Raspberry Pi Zero W.
Using gpiod_get_* functions for parallel data transfers is grossly inefficient. That is why a more direct path is required for transfers, which is achievable using memory mapped IO. This will be tested in future revisions of this article.
Basically, performing MMIO requires knowing the Physical memory mapping base address of the BCM8235 bus address. On Raspberry pi Zero W, it is 0x20200000. (not 0x7…, as indicated as base on the BCM8235/6 datasheet : this is the BUS address, and not 0x32…. : this is the physical memory mapping on newer Raspberry pi Boards such as Pi2/3/4)
Note that the pinctrl tool can be used to ascertain the physical memory mapping base address. Here is the trap however : 0x32000000 base address seems to work in our case to get GPIO levels using a mmap wrapper with Python, since it reflected pin changes, but it did not work in kernel space, so this address base is misleading.
However, The correct 0x202000000 physical memory address base cannot be accessed directly in kernel or user space though. It needs another remapping operation into kernel virtual memory address space.
This is where the request_memory_region() and ioremap() functions come into play and shall be used in the kernel driver module.
request_memory_region() reserves the physical memory region for ioremap(), and prevents any outside access from kernel or user space, which could lead to instabilities and conflicts. Once the requested memory region is allocated to the kernel module drive through the former call, any other request, read or remap operation will result in an error, such as EBUSY.
This is also why disabling conflicting functions such as I2C, SPI, UART, 2wire, EEPROM and consorts that would conflict with the module is required
In our case, we disabled all of the above mentioned functions.
Device Tree Overlays
The driver needs to know which pins to use and which MMIO address space to use. The current best practice is to use only device tree overlays that provide platform specific and implementation specific GPIO pin mapping and MMIO base address information. Using .c or header files as driver board configuration files is a deprecated and discouraged practice, especially in loadable/unloadable (not kernel integrated) modules.
This is why writing a device tree (dts file) overlay is mandatory.
The device tree provides signalling and configuration pin information, as well as data lines pins declaration. The signalling and configuration pin information is used by GPIOd_* functions.
Although data line pins are used by MMIO, which does not use GPIOd_* functions for data transfer, the pin declarations as an array of pins in the device tree overlay is required for configuration of these data line pins as INPUTS, which is done once in the module .probe() callback function, and this step uses GPIOd_set functions for mode configuration.
Configuration of the physical base address used to access GPIOLEV0 registers and data transfer, so that request_mem_region() and ioremap() get the required base address information and number of memory pages to remap is done through the “reg” device tree property.
reg = <0x20200000 0x00004096>;
the first hexadecimal number is the physical memory base address of the GPIO peripheral, and the second number is the number of bytes to be used in request_mem_region() and ioremap() functions.
Note that ARM paging requires at least 4 bytes as size, in our case we mapped a full SZ_4K region, at it is usually done in other similar drivers. The start address needs to be aligned at 4 bytes, which is the case as it is divisible by 4. The total GPIO address space is x bytes.
the “adi,” prefix encountered in properties can be seen as a namespace information (Analog Devices manufacturer), and is used to avoid ambiguous / property name conflicts with other existing device tree properties at the time of dtoverlay loading. This is preferable since some common tokens such as ‘reset’ may already be used.
Pull up / Pull down configuration
Standard pin definitions <&gpio pin_number flags> as pin desc definitions used by the ad7606 are not sufficient to specify the configuration of the internal pulls. These need to be specified as the brcm2835 device level, and referenced by the ad7606 device tree fragment. Pull ups / Pull down not only allow the logic state to settle if the driving side is in high-z mode such as the tri-state FRSTDATA pin, preventing undefined behaviour, they also allow quicker rise and fall edge timings, which is a requirement for high speed transfers.
/dts-v1/;
/plugin/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
/ {
compatible = "brcm,bcm2835";
fragment@0{
target = <&gpio>;
__overlay__{
frstdatapin: frstdatapin {
brcm,pins = <7>;
brcm,function = <0>;
brcm,pull = <1>;
};
busypin: busypin {
brcm,pins = <4>;
brcm,function = <0>;
brcm,pull = <1>;
};
parallel_datapins: parallel_datapins {
brcm,pins = <8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23>;
brcm,function = <0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0>;
brcm,pull = <1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1>;
};
};
};
};
/ {
compatible = "brcm,bcm2835";
fragment@1{
target-path = "/";
__overlay__{
#address-cells = <1>;
#size-cells = <1>;
ad7606-8@0 {
compatible = "adi,ad7606-8";
reg = <0x20200000 0x00004096>;
avcc-supply = <&vdd_5v0_reg>;
interrupts = <4 IRQ_TYPE_EDGE_FALLING>; /* linked to AD7606 busy line ? */
interrupt-parent = <&gpio>;
adi,conversion-start-gpios = <&gpio 5 GPIO_ACTIVE_HIGH>;
reset-gpios = <&gpio 6 GPIO_ACTIVE_HIGH>;
adi,first-data-gpios = <&gpio 1 GPIO_ACTIVE_HIGH>;
cs-gpios = <&gpio 2 GPIO_ACTIVE_HIGH>;
rd-gpios = <&gpio 3 GPIO_ACTIVE_HIGH>;
cs-rd-gpios = <
&gpio 2 GPIO_ACTIVE_HIGH
&gpio 3 GPIO_ACTIVE_HIGH
>;
adi,oversampling-ratio-gpios = <&gpio 24 GPIO_ACTIVE_HIGH
&gpio 25 GPIO_ACTIVE_HIGH
&gpio 26 GPIO_ACTIVE_HIGH>;
standby-gpios = <&gpio 27 GPIO_ACTIVE_LOW>;
adi,parallel-data-gpios = <
&gpio 8 GPIO_ACTIVE_HIGH
&gpio 9 GPIO_ACTIVE_HIGH
&gpio 10 GPIO_ACTIVE_HIGH
&gpio 11 GPIO_ACTIVE_HIGH
&gpio 12 GPIO_ACTIVE_HIGH
&gpio 13 GPIO_ACTIVE_HIGH
&gpio 14 GPIO_ACTIVE_HIGH
&gpio 15 GPIO_ACTIVE_HIGH
&gpio 16 GPIO_ACTIVE_HIGH
&gpio 17 GPIO_ACTIVE_HIGH
&gpio 18 GPIO_ACTIVE_HIGH
&gpio 19 GPIO_ACTIVE_HIGH
&gpio 20 GPIO_ACTIVE_HIGH
&gpio 21 GPIO_ACTIVE_HIGH
&gpio 22 GPIO_ACTIVE_HIGH
&gpio 23 GPIO_ACTIVE_HIGH
>;
adi,sw-mode;
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&frstdatapin &busypin ¶llel_datapins>;
};
};
};
};
Makefile information
The Makefile is used to compile two units :
- The device tree, with a preprocessing step. the dts files get pre-processed into a dts.preprocessed file, which itself gets compiled into a binary device tree object (dtbo) that can be used by device tree overlay loading functions such as dtoverlay or dtoverlay statements in the Raspberry Pi /boot/firmware/config.txt
- The driver itself, made from one c file and one header files (ad7606_par.c and ad7606.h)
obj-m +=ad7606_par.o
CFLAGS_ad7606_par.o := -UDEBUG
EXTRA_CFLAGS += -fno-inline
all: module dt
echo Built Device Tree Overlay and kernel module
module:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) V=1 modules
dt: ad7606.dts
cpp -nostdinc -I /usr/src/linux-headers-$(shell uname -r | sed 's/-rpi-v[0-9]*//')-common-rpi/include/ -I arm64 -undef -x assembler-with-cpp ad7606.dts ad7606.dts.preprocessed
dtc -@ -Hepapr -I dts -O dtb -o ad7606.dtbo ad7606.dts.preprocessed
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) V=1 clean
rm -rf ad7606.dtbo ad7606.dts.preprocessed
Module Loading helper bash script
The file load.sh is used as helper to load the device tree and kernel driver, as well as its dependencies. In a final driver and its installation procedure, the dependencies are usually stored as lines in a dependency file, .dep.
At the time of writing this article, distribution effort was not yet done, and so it is outside the scope of the article. Which means that modprobe calls in the correct order have to be done to load the required dependencies.
Those are :
- industrialio
- iio-trig-hrtimer
- industrialio_triggered_buffer
Then, the hrtimer0 instance is created as it will be subject to validation by our driver module validate trigger function.
following is the DT overlay load and the kernel module load. The remaining calls are used to configure the sample buffer, triggered buffer sampling frequency (managed by iio hrtimer), and buffer enable at the end, with a real time read of the iio device by using dd.
Commented calls are used for ftrace profiling, to minimize dev_dbg overhead.
#!/bin/bash
sudo modprobe industrialio # load industrial io kernel module
sudo modprobe iio-trig-hrtimer # load hrtimer industrial io kernel module
sudo modprobe industrialio_triggered_buffer # load industrial triggered buffer kernel module
sudo mkdir /sys/kernel/config/iio/triggers/hrtimer/hrtimer0 # configure one hrtimer instance
sudo dtoverlay -v ad7606.dtbo #load compiled device tree snippet with ad7606 pin to rpi config
sudo insmod ad7606_par.ko #load ad7606 driver module
sudo sh -c "echo 10.0 > /sys/bus/iio/devices/trigger0/sampling_frequency" #sets triggered sampling frequency to 10 sps
sudo sh -c "cat /sys/bus/iio/devices/trigger0/name > /sys/bus/iio/devices/iio\:device0/trigger/current_trigger" # configure the ad7606 iio device to use the hrtimer0 instance
sudo sh -c "echo 1 > /sys/bus/iio/devices/iio\:device0/scan_elements/in_voltage1_en" # configure the triggered buffer to output a single channel, the second channel. (channels are 0 indexed)
cat /sys/bus/iio/devices/iio\:device0/in_voltage1_raw #output raw value for testing. The first sample is usually invalid, this needs debugging.
cat /sys/bus/iio/devices/iio\:device0/in_voltage1_raw #gets another sample, this value should be ok.
#sudo vclog -m
#Make sure tracing is disabled during tracing reconfiguration
#echo "disabling tracing and current_tracer"
#sudo sh -c "echo 0 > /sys/kernel/debug/tracing/tracing_on"
#sudo sh -c "echo nop > /sys/kernel/debug/tracing/current_tracer"
#sudo sh -c "echo ad7606_* > /sys/kernel/debug/tracing/set_ftrace_filter"
#echo "ad7606_* written to set_ftrace_filter"
#sudo sh -c "echo function_graph > /sys/kernel/debug/tracing/current_tracer"
#sudo sh -c "echo 2 > /sys/kernel/debug/tracing/max_graph_depth"
#echo "set max_graph_depth to 2"
#sudo sh -c "echo 1 > /sys/kernel/debug/tracing/tracing_on"
#echo "function_graph enabled in current_tracer, and enabling tracing "
echo "now enabling device buffer"
sudo sh -c 'echo 1 > /sys/bus/iio/devices/iio\:device0/buffer0/enable'
echo "buffer enabled"
#echo "reading trace_pipe in 2 secs"
#sleep 2
#sudo sh -c "cat /sys/kernel/debug/tracing/trace_pipe > /root/trace.log"
echo "will now output raw, aligned sample bytes from buffer device"
sudo sh -c "dd if=/dev/iio\:device0 bs=20 iflag=fullblock | hexdump"
Performance testing
The following picture shows the testing conditions. Such a crude interface is useful for getting “worst case performance” data, which are useful in setups where the AD7606 must be up and running in the least amount of time and least amount of money (from placing order to sampling, using off the shelf components)

When using MMIO for data transfer, and gpiod_* functions for signalling CONVSTx, CS/RD, and reading FRSTDATA, the gpio_d* function calls will be bottleneck. A full convst/read cycle takes around 60us. In that case, the maximum theoretical sampling rate would be 15 ksps. We expect that 8 ksps could be a conservative max practical limit accounting for kernel IRQ burden. Using full MMIO would probably help push the limit higher, if care is given to disable as much system noise as possible (such as HDMI, videocore drivers, bluetooth, framebuffers, etc). A more capable raspberry pi such as 3/4 which have more than 1 core, could use IRQ pinning to help such a fully MMIO optimized driver to work at maximum speed, provided a mating board or a full fledged ad7606 hat is used for hardware performance.
The FRSTDATA channel synchronization mechanism.
The tristate (High-Z, HIGH/LOW) FRSTDATA input is used to signal that the first channel is ready to be read. This hardens the transfer protocol as the sample frames remains aligned, that is, the channels do not get misaligned in the output buffer. Algorithmically, a boolean “first-channel” is set to true at the beginning of the samples read process. it is compared to the level of the FRSTDATA pin, and the comparison should branch to true. The boolean is then set to false and compared to FRSTDATA level for the remaining channels and should branch to true. If there is a mismatch, sample readout is aborted, and a RESET is sent to the ad7606, through the ad7606_reset functions, where the internal driver pin state is also reset so that the whole conversion/readout cycle can be resumed.
The FRSTDATA pin is a good indicator of hardware stability and hardware interfacing quality. Lowering strobe cycle periods and overall timing delays will typically increase the number of spurious FRSTDATA states and subsequent resets, especially when reaching the limits of hardware interface stability, which can happen well before the timing limits of ad7606 datasheet when using an ad-hoc long Dupont header pins cable interface. In our case, the IRQ load on the kernel as well as the gpiod_set_* command timing overhead are the bottleneck.
For reference, This is the average spurious FRSTDATA error rate over 1 hour sampling at various sampling rates. Test conditions : Raspberry Pi Zero W, 20 cm Dupont header cables, internal pulls used on all input pins, no current limiting resistors.
1000 sps = 2.44 %
2000 sps = 2.74 %
4000 sps = 3.08 %
In our case the total execution time of the trigger handler was on average 126us, using 100ns timing for pin strobe delays. Note however, that most of the time spent is not accounted from these timing delays, but by the slowness of the gpiod_set_* functions, and IRQ thread synchronization to trigger handler using completions. The CONVSTA/B to BUSY falling edge published in the datasheet is specified as 5us, which is a minor contribution to the overall cycle. Note that wait_for_completion_timeout() puts the thread on the wait queue, and the IRQ handles completion using complete(), which resumes the trigger handler thread, this has probably quite some overhead, but is safe in IRQ contexts.
wait_for_completion_timeout() also returns a non zero integer if timeout is not reached, representing the remaining jiffies in the supplied timeout. It can thus be used to check the time waited by a simple subtraction. Note that with a kernel “HZ” value of 100, one jiffy is 10 msec, which resolution is too low to provide any sensible performance metric for tuning. Any value returned different of the supplied timeout would only inform of a severe contention of IRQ mishandling.
At 126 us conversion/read cycle, the theoretical maximum sampling rate is short of 8 ksps.
Thus using MMIO instead of gpiod_set_* and maybe experimenting with try_wait_for_completion() could improve theoretical sampling rate.
The following oscilloscope capture shows the FRSTDATA channel read synchronization signal on CH1 and the linked mode CS/RD pulse strobes used to latch all channels (8) data into the parallel interface lines

Github repository of kernel module driver code and helpers
https://github.com/rodv92/ad7606_par_rpi
Moving forward
While this approach can get you sampling 8 channels fast, it mainly limits to slow sampling requirements and cannot guarantee equispaced sampling. On the other hand, no hardware buffering means the latency is minimal, and it also builds on top of IIO seamlessly.
For serious speeds a fully integrated acquisition system featuring a MCU and the AD7606, and a convenient interface such as USB is preferable.
An AD7606 development board is 7 to 11 USD pu (Aug 2025).
A full-fledged USB acquisition system based on AD7606 is expected to cost 50 to 60 USD pu (Aug 2025).
Such a system will probably be the focus of our interest in our next article about the AD7606. Stay tuned.