Developing timdac

timdac is a timer-based precision integrating DAC (using the timers — TIMs — in the CH32V103 microcontroller). This is a project I'm developing to publish as a public-domain firmware module and hardware reference design. Some target specs:

The code and circuits are in a git repository.

TODO: add schematic sketches for all of these, including the timer in the schem.

Step 1: breadboarding a basic circuit


The first attempt was a simple breadboard containing some analog switches, an integrator and a buffer. It performed well enough to suggest that there was cause to build a nicer version.

Close-up of two chips and some passives on a breadboard

Oscilloscope screenshot of it outputting a negative-going stairstep

This version used the microcontroller timer output directly as the signal to integrate. This meant the ramp could only go negative (not a problem — I kept this limitation into the final version and worked around it in a different way) and also introduced some error due to the tiny, but still present, ground difference between the microcontroller and the opamp. It could also allow power supply noise to be injected into the DAC as an error term, especially with a microcontroller also performing other tasks. This design also meant the system gain was unpredictable, as it depended on the supply voltage and the resistance and capacitance on the integrator.

Step 2: close a loop


To solve the unpredictable gain, a comparator was added. This comparator detects when the ramp crosses a stable reference voltage, and its output feeds into the timer's input capture channel. Periodically, the firmware would let out a ramp long enough to cross vref, and capture the duration required. It would save the captured duration as a calibration scale factor, then immediately discharge the integrator without transferring the pulse to the holding capacitor. This proved quite reliable at locking the output to a stable reference.

The same breadboard with a couple modifications, including a comparator chip added. A scope probe is sticking out towards the camera.

Step 3: keep the grounds local!


To solve the drift due to ground offset, a new version was built. In this build, instead of driving the integrator directly from the timer, the timer output would control an analog switch, closing and opening a connection to a local, stable voltage reference. This version also added an inverter, allowing a positive voltage reference to produce a positive ramp. It was built up on a piece of solderable protoboard, which gave substantially better performance than the breadboard, but was still somewhat lacking.

A piece of protoboard styled to look like a breadboard, with a similar circuit on it, this time with four chips.

Among the problems in this version was some pretty substantial noise (a few LSB worth), which some experimentation showed to be a result of charge injection through the final multiplexer as well as capacitive coupling from control signals. A fourth hardware version was required...

Step 4: fixing all the (hardware) problems


To get better control of layout, this one was done as a PCB:

PCB version of the above circuit.

The PCB version substantially reduced signal coupling, both with a cleaner layout, and with series resistors on many slower signals to cut their slew rates. This version upgraded to a significantly more stable voltage reference, ensuring that the reference would stay constant during a full all-codes sweep. Several more output channels were populated to show off multi-channel operation, and a spare analog switch was allocated to select between the positive-going and negative-going ramps in order to give bipolar outputs. Finally, the output multiplexer was upgraded from a CD4051 to a DG4051, Vishay's special low charge injection version.

This version solved all the known problems of the previous versions. Output noise was totally imperceptible within the capabilities of my equipment; I will set up a more capable test soon in order to get that characterized. Drift was also essentially zero; I was able to call up 1.0000V and then summon the exact same voltage the next morning with the same DAC code setting. The zero-code output was about 4.5mV, which I'm pretty sure is just from opamp offset voltages, and it immediately starts out with good linearity going from 0 to 1 to 2.

For this version, I was finally able to set up an all-codes sweep test. Over about 5 hours, I stepped the DAC through all 32768 points and logged the result on a 5.5-digit multimeter. Sadly, this is not where the story ends — it produced the following INL/DNL plot:

INL/DNL plot, showing a DNL that starts around +/- 0.7 but increases to nearly +/- 2

It's a turkey. The INL is acceptable, but the DNL is atrocious. Non-monotonic codes (where it goes down instead of up) abound, with a few even hitting a -2 LSB DNL. At this point, my best guess for the cause of this is the tuning loop, where the DAC calibrates itself against the voltage reference using an input capture channel. I'd already known this to produce a bit of "hunting" noise, and zooming in on the voltage-vs-code plot, this looks very much like what I was seeing on the multimeter in earlier testing. I just failed to realize that this would scale with DAC code, and while it wasn't as visible to the eye on my multimeter at high codes, it'd blow up DNL.

Still, while this is poor performance for a 15-bit DAC, it's otherwise quite good. A DNL of -2 here is an error of 76µV. The fact that I got this far and managed to get in under a hundred microvolts with cheap parts is encouraging. In the next step, I am going to tweak the tuning routine to kill this scale factor wandering with some more clever filtering and higher precision. I am also going to build the system into a thermally and electrically shielded box this time, use a coaxial cable to deliver the signal to the multimeter, and replace the GW-Instek meter with my venerable old HP 3468A, whose linearity I trust much more.

Update: it's the fucking voltage reference!


Ok, so. Fixed all the shit. I put it in an electrically shielded box. Fixed the tuning loop. Swapped the other switch IC for low charge injection too. Brought in a better voltmeter.

It turns out these were all good changes to make, but the immediate result is that noise is still there 🤬. So now I went on a noise hunt. Started moving the probe point from the output to various points in the circuit looking for anything that seemed to move in a similar way. And yup — there it is, directly on the voltage reference!

Remember that "significantly more stable voltage reference", from above? Yeah, well, it's stable over temperature, but the more keen-eyed (than me) would have noticed the absolutely dreadful noise spec: 240µVpp! I hardly even looked at the noise spec because this system doesn't need anything too clean, but I never considered it might be that...bad...

OK, let's do a quick test. Keep the LM4132 in the tuning loop, with the new much slower tuning constant to kill the noise. But cut the trace going to the integrator, and for the integrator input, use a series pair of AA alkaline cells. These are practically noiseless. They do have some other pretty bad specs that actually led to at least one circuit improvement (poor load regulation creates poor INL because timdac's load impedance shown to the voltage reference is a function of DAC code — I can fix that and make it constant!) but got me far enough to do a new DNL measurement and confirm we can get good performance. And yes, we can! The new setup with all of these changes now shows DNL figures of at worst about 0.75LSB, around 0.5LSB in most areas. I can work with that! So now I have a better still, but somehow also cheaper reference on order. This one's noise is 4.8µVpp, much lower than a LSB.

In the meantime I'll work on tidying up the code.

Yup, that fixes it.


Yeah, the MAX6070 cleans it up a ton.

Improved INL/DNL plot; DNL is mostly below 0.5. There is still one non-monotonic code.

There are still a few issues that I'd like to resolve before wrapping this up:

Adding a SAR tuning loop


Up until this point, the tuning system was timing-based. It would fling the integrator into a maximum-duration ramp, and an external comparator combined with the timer's input capture channel would record the duration required for that ramp to cross the voltage reference. This value was saved, low-pass filtered, and used to scale the output voltage.

To avoid instability in timing causing variation here, I moved to a SAR (Successive Approximation Register). This algorithm essentially implements a binary search:

  1. Start with a range of possible tune values guaranteed to contain the reference voltage.
  2. Split it; emit a pulse with a duration equal to the center of that range.
  3. Allow the comparator to stabilize.
  4. Check whether it indicates High or Low. If Low, the reference must be in the upper half of the range; if High, it's in the lower half.
  5. Replace the original range with the half-range now known to contain the reference.
  6. Repeat until the range only contains one value.

Initially, I kept the timing mechanism too, in order to speed up the search: the reference was measured once with input capture, then a range of 1024 samples around it was used for the SAR search.

This improved the performance quite a bit.

Further improved plots; DNL is again mostly below 0.5, and all non-monitoic codes are gone.

It's not visible in this plot, but the temperature variation is pretty much gone at this point. There is no "warmup" time after powering on now. For the most part, the tuning is pretty stable as well, which eliminates the previous non-monotonic codes — though a few still get close. Let's improve this by adjusting filter and threshold parameters a bit:

Improved again. Now all DNLs are between -0.6 and +0.5, with most between -0.5 and +0.2.

The aforementioned refactoring continues. Once I get the code cleaned up, this should be pretty close to a release-ready state.

One more rev bump to the reference design

Alright, this performs really well. I think I'd like to make a few more adjustments to the hardware before finally publishing, so it's time for a new PCB build (I may just hack up an existing board though):

Plus, a more distant change

As the multimodal DNL distribution suggests, most of the currently remaining DNL is the result of quantization in the tuning algorithm. I tried a test with that disabled entirely, and it puts DNL below 0.2 LSB. This means that if we can find a new way to implement tuning, we could have sixteen bits! So I am going to make a bougie 16-bit version, which will use an external digipot for tuning instead of simple scaling, leaving the entire 16-bit timer register available for use. This increases the BOM cost by another about $1.50.

OK, let's publish this motherfucker


Well, I think that kinda wraps up for now. I'm so encouraged by the performance that I could keep improving it forever, but y'know, perfect is the enemy of good and all that. Let's get something out. In the past couple week, I have:

So that's that. The files are up in Forgejo, including reference hardware and the firmware library. Go forth and emit voltages!

Last modified: Tue Jan 3 20:43:31 MST 2023. Converted from blog/
RSS feedBlogHomeUpIndex