As a sort of test and measurement engineer at my job, we have to take temperature measurements on live electrical circuits. A thermocouple is great for this – you can simply embed the tip of a thermocouple wire inside a block of copper and you’ll get its temperature even if there’s 500 Amps going through it.
The hardware for this is expensive. The National Instruments 9212 is an 8-channel electrically isolated thermocouple input module for the NI family of DAQ units. This module costs $1600, and that doesn’t even consider the ‘base station’ each of these modules needs. If you want to read electrically isolated thermocouple inputs, expect to spend at least $250 per channel.
I can do this for about $15 per channel.
Thermocouples are just two bits of wire made of dissimilar metals that produce a voltage proportional to temperature. At full scale – the minimum and maximum temperatures published in thermocouple lookup tables – the voltages are fairly large and on the order of -10mV to about +50mV. That’s 1/10000th to 1/50000ths of a Volt. To get sub 1°C resolution I will need to measure microvolts, or millionths of a volt. With a range of +/- 50mV for the DAC, that means I need an effective resolution of at least 16 bits.
These inputs also need to be isolated. There is a chip that does this, the Texas Instruments AMC3336 and related chips like the AMC3306M05 (+/- 50mV), and AMC3305M25 (+/- 250mV). These chips are isolated delta-sigma (ΔΣ) DACs. They all operate with a single power supply of 3.3V, and they will read the microvolt voltages needed for thermocouple measurement.
The specs for these chips is great, especially on the AMC3306M05. single 3.3V supply will allow me to read a single thermocouple input, +/- 50mV, at about 16 bits of resolution. It has 1200V of isolation. The only problem is that it’s a ΔΣ DAC, so I need to figure out how to read that.
A ΔΣ DAC is simple to describe – feed it a square wave, and it will give you another square wave at half the frequency, the duty cycle of which is proportional to the voltage on the input. Here’s what it looks like on an oscilloscope, clock input on top, signal on bottom:
The ideal tool for this is the RP2040, also known as the Raspberry Pi microcontroller. The RP2040 has a PIO module that can toggle and read GPIOs very fast. Think of it as the microcontroller version of the BeagleBone PRU.
Each of the two PIOs on the RP2040 has four State Machines (SM), a “sub-processor”, with a handful of registers (four, actually, x, y, and an input and output shift register) and runs a handful of instructions (read, write, shift in, jump, and decrement). The input and and output shift registers are 32 bits wide, meaning if you want to read a ΔΣ DAC, you can only read 32 points before shifting that data out to the main processor via DMA.
To use these PIOs with the ACM3306, one SM needs to generate a clock. Another SM needs to wait for an IRQ from the clock generator PIO, then read a pin and shove it into the Input Shift Register. The code for each is listed below
.program clock_gen
set pindirs, 1 ; Set pin direction to output
.wrap_target
set pins, 1 ; Set pin high
nop ; Delay for low state
set pins, 0 ; Set pin low
irq nowait 0 ; Set IRQ 0 for the other SM.
; This is a non-blocking IRQ, it
; only sets a flag to be read by
; other state machines
set pins, 1 ; Set pin high
nop ; Only want one IRQ
set pins, 0 ; Set pin low
nop ; Delay for low state
.wrap
.program sample_counter
set pindirs, 0 ; Set pin direction to input
set x, 31 ; Initialize x for 32 bits
.wrap_target
do_capture:
wait 1 irq 0 ; Wait for IRQ 0 (triggered by clock_gen)
in pins, 1 ; Read 1 bit and shift into ISR
jmp x-- do_capture ; Loop until x is 0
push block ; Push 32 bits from ISR to RX FIFO
set x, 31 ; Reload x for the next 32-bit capture
.wrap
A graphic representation of what’s happening in this code is shown below. This code relies on setting an IRQ on the clock_gen PIO code, and reading that IRQ in the sample_counter PIO code.
This code collects 32 bits into the Input Shift Register of the PIO, and shoves that over to the main CPU when it’s full. using _builtin_popcount() I can count the number of high bits and eventually get the proportion of high bits to total bits in the ΔΣ signal.
This works for one channel, but because the RP2040 only has eight total State Machines, I cannot read eight channels while generating the clock signal. The RP2350 with twelve total SMs is a much better fit. I’ll use that for the production version of this.
The PIO code gives me a bitstream for each output of an ACM3306 chip, but I still need to turn that into a voltage which will eventually be converted into a temperature.
To process this data, I’m using a Sync3 filter that works like this:
The filter also includes a running average to further smooth the output. The result is remarkably clean voltage readings. I’m getting microvolt resolution and sample rates around 10Hz.
Because the microcontroller I’m using has a fantastic DMA system, I’m using the RP2040 to automatically transfer blocks of data from the PIO’s RX FIFO to memory without CPU intervention. This greatly reduces the processing burden on the main CPU, enabling it to focus on tasks such as additional signal processing.
As a quick aside, reading one channel of ACM DAC data is processing about 15 Megabits per second. Eight channels is 120Mbps. That’s processing data faster than late 90s Ethernet on a chip that costs $4. That’s absurd.
For this to be a useful industrial DAQ, it needs to have inputs and outputs. For this, I added a W5500 Ethernet controller. USB-C is the future, but I don’t need USB3. I wired up a USB-C port as a USB 2.0 interface, which is sufficient. Power is provided either through the USB-C port or a barrel jack while four transistors form a power OR circuit, allowing this device to be powered by either the barrel jack or USB port.
One thing I’ve noticed on industrial hardware is the complete lack of a user interface. I’ve used Ethernet DAQs where the only way to tell what IP address a device is set to is to use nmap after plugging it in. This device has a small OLED display that shows you its own IP address. I expect this to save at least a man-week of work every year. There are also side-mounted LEDs below this display and light pipes through the case. Even I question the utility of these LEDs but they look great and were cheap to implement.
Connectivity is mostly through Modbus over Ethernet, although streaming over serial is also supported. This is in line with most of the other data acquisition tooling at my job.
A thermocouple reader also needs cold-junction compensation, or a temperature measurement of the interior of the device. This is accomplished by an LMT01 temperature sensor placed underneath the header for the thermocouples. This is read by another bit of PIO code on the microcontroller, freeing up a little bit more processing power.