7.1 KiB


TIMDAC is a public-domain, cheap, precision DAC, comprising a hardware reference design and a firmware module.

Why you should use it

It boasts pretty good specs for precision control applications:

  • 15 usable bits
  • DNL ≤ ±0.7 LSB
  • INL ≤ ±2.75 LSB
  • Optional bipolar output
  • Channel counts up to 254, practically limited by scan frequency
  • Can natively output any voltage reachable in your supply rails

It uses an integrating sample-and-hold topology, where a microcontroller timer is used to create a precise pulse, an integrator converts it into a level, and a sample-and-hold amplifier captures and buffers the level. The scan rate is about 770 Hz, and the typical update latency is from 1.3 to 2.6 ms for a single channel, or worst-case, 1.3 ms times the number of channels if all channels are updating frequently.

At a build quantity of 10, BOM cost is around $3.00 + $0.67 n, where n is the number of output channels up to 8. This gets you a full 15 bits! Also, most parts are quite generic and can be easily substituted if hit by part shortages.

Here, have some plots:

Plot of DNL and INL vs DAC code

Why you shouldn't use it

The design has some drawbacks, as does any DAC. Here are some reasons TIMDAC might not be right for you:

  • Requires a microcontroller actively driving it (note that CPU utilization is typically about 0.9%)
  • This microcontroller needs to be a 32-bit part that can run its timer at around 72 MHz
  • Update rate is limited by the scan frequency, so if you need to update very fast or update several outputs simultaneously, you may find it limiting

Supported platforms

The following microcontrollers are presently supported. TIMDAC is portable and has relatively basic requirements for its microcontroller as long as the timer is fast enough, so adding more ports is straightforward.

  • WCH CH32V103

Implementation guide


To implement TIMDAC, you will need a supported microcontroller with an available timer peripheral. It's also easy to port; if you plan to port it, the timer peripheral must run at around 72 MHz, count to 65535, support emitting a single pulse of specified width and then stopping, and be able to fire an interrupt when it stops.

First, look at the reference hardware implementation above. You will need to build this circuit on your board, and connect it to the microcontroller as shown.

Firmware (using Meson)

TIMDAC provides a Meson build file, so if you use Meson you can include it directly as a subproject. The following options should be defined:

  • timdac_hw — set to the name of the hardware port. Options are ch32v103 and none. If you use none, TIMDAC can link against port functions you provide externally.

  • bsp — set to the name of a subproject containing your board support pack. This is where the config header, timdac_config.h, should live. This is a nice, clean way to pull hardware definitions from the root project into subprojects in Meson. If you're not using this approach already, you can configure a very small board support pack like this:

$ ls subprojects/bsp-foo

$ cat subprojects/bsp-foo/meson.build
project('bsp-timdac', 'c')

bsp_dep = declare_dependency(include_directories: include_directories('.'))

If you want to call the dependency something other than bsp_dep, you can set the same-named option (bsp_dep) to its actual name.

Firmware (not using Meson)

  • Add the source files under src to your build (only add one of the timdac_hw files, matching your target platform).
  • Add the include directory inc to your build.
  • When building TIMDAC itself, ensure that wherever timdac_config.h lives is in TIMDAC's include path.

Firmware configuration

TIMDAC needs to be given a build configuration with hardware and runtime options as timdac_config.h. A template configuration file can be found in timdac_config_template_ch32v103.h. All of the configuration options for TIMDAC itself are documented in timdac.h, and the configuration options for each hardware driver are documented in that driver's source file (e.g. timdac_hw_ch32v103.c).

Some of these options are labeled [REFHW] in the documentation. These options pertain to the reference hardware implementation, and as long as you are following the hardware suggestions, you should not change them. They can have surprising and difficult-to-measure effects on the DAC performance. To get the correct options for a reference design, you should #include that reference design's sub-config in your config:

#include "timdac_config_ref1p0.h"


Once integrated into your codebase, it can be initialized and started very simply, and then you can set output values:

// First, ensure all necessary GPIO and timer clocks are running, any pin
// remapping has been done, and interrupts are enabled:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
GPIO_PinRemapConfig(GPIO_FullRemap_TIM3, ENABLE);

timdac_set(channel, value);

When you set a channel, it is jumped to the start of the scan queue so that it will receive an output as fast as possible. If another channel is already in the pending queue-jump slot when you write a channel, this will not be done (there is only one queue-jump slot). You may wait for an available slot:

while (timdac_pending());

Future versions roadmap

  • 1.0.1 or 1.1: Improve the scan algorithm to support many queue-jumpers.

  • 1.1: Minor tweaks to the reference design BOM for cheaper cost and slightly improved performance. A comparator is replaced by a cheaper one, a diode is replaced by a capacitor, and several capacitors are replaced with lower dielectric absorption units.

  • 2.0: Add 16-bit operation! This will require an additional $1.21 part — a digital potentiometer. The 15-bit version of TIMDAC uses a "hidden" 16th bit to perform software scaling of output values to tune itself as timing parameters vary; by moving this to an external potentiometer the full 16-bit range of the timer can be realized. This will be a substantial upgrade that is going to require careful redesign and testing on my end to maintain usably low DNL and INL.


Want to contribute something? Bugs? Ports? Improvements? Sure! Hopefully gitea/forgejo gets federation someday, but until then, I can be emailed: (this username) @ (this domain).