STM32H7 DAC Driver
The STM32H7 DAC is a 12-bit, voltage-output, dual-channel digital-to-analog converter embedded in the STM32H7 MCU. The driver supports two operating modes:
Basic (direct write) mode — the application writes individual samples via the standard POSIX
write()call.DMA mode — the DAC outputs samples from an internal DMA buffer at a rate determined by a timer. DMA mode itself has two sub-modes: circular (fixed buffer replayed periodically) and stream (double-buffered with half-transfer interrupts for real-time refill).
Hardware Features
12-bit resolution
2 independent channels (DAC1_CH1 on PA4, DAC1_CH2 on PA5)
Built-in output buffer for each channel
DMA with timer-triggered transfers
Configurable DMA buffer size per channel (
CONFIG_STM32_DACxCHy_DMA_BUFFER_SIZE)Stream mode with half-transfer interrupts for double-buffering
Timer range on H7: 1–15, with configurable output frequency
Driver Interface
The STM32H7 DAC driver follows the NuttX upper-half/lower-half DAC
architecture. Board-level initialization calls stm32_dacinitialize()
and registers the device with dac_register():
#include <nuttx/config.h>
#include <nuttx/analog/dac.h>
#include "stm32_dac.h"
int board_dac_initialize(void)
{
struct dac_dev_s *dac;
#ifdef CONFIG_STM32_DAC1CH1
dac = stm32_dacinitialize(0);
if (dac == NULL)
return -ENODEV;
ret = dac_register("/dev/dac0", dac);
if (ret < 0)
return ret;
#endif
return OK;
}
The DAC is accessed through a character device in /dev (e.g. /dev/dac0).
Standard file operations and driver-specific ioctl commands are used for
control and data transfer.
Operating Modes
Basic Mode (direct write)
In basic mode the application calls write() with a dac_msg_s
structure for each sample. The upper-half driver queues the message
and the lower-half interrupt handler writes it to the DAC data register.
#include <nuttx/analog/dac.h>
#include <fcntl.h>
int fd = open("/dev/dac0", O_WRONLY);
if (fd < 0)
return ERROR;
struct dac_msg_s msg;
msg.am_channel = 0; /* channel 1 */
msg.am_data = 2048; /* mid-scale (0–4095) */
write(fd, &msg, sizeof(msg));
close(fd);
Each write() call delivers one sample. For continuous waveforms a
timer or loop is needed; for higher update rates use DMA mode.
DMA Mode
In DMA mode a timer periodically triggers the DAC to read the next sample
from the DMA buffer. The buffer is split into two halves for stream mode
(double-buffering). The ANIOC_DAC_INFO command returns the buffer
configuration:
#include <nuttx/analog/dac.h>
#include <sys/ioctl.h>
struct dac_info_s info;
ioctl(fd, ANIOC_DAC_INFO, (unsigned long)&info);
/* info.dma_buffer_size — total DMA buffer in samples
* info.dma_timer_frequency — timer frequency in Hz
* The half length is dma_buffer_size / 2.
* The per-sample rate is dma_timer_frequency / dma_buffer_size.
*/
Circular sub-mode (halfint = 0)
The DMA engine cycles through the entire buffer endlessly. Only transfer-complete interrupts are enabled; no events are delivered to the application. The application must pre-fill the entire buffer before starting.
Example: fill the full DMA buffer with a sawtooth and start circular output:
#include <nuttx/analog/dac.h>
#include <sys/ioctl.h>
struct dac_info_s info;
ioctl(fd, ANIOC_DAC_INFO, (unsigned long)&info);
uint16_t *buf = malloc(info.dma_buffer_size * sizeof(uint16_t));
for (int i = 0; i < info.dma_buffer_size; i++)
buf[i] = (uint16_t)((uint32_t)i * 4095 / info.dma_buffer_size);
/* Copy the whole buffer into the internal DMA buffer */
ioctl(fd, ANIOC_DAC_DMABUFF_INIT, (unsigned long)buf);
/* Start circular output */
struct dac_dma_start_s start;
start.halfint = 0;
ioctl(fd, ANIOC_DAC_DMA_START, (unsigned long)&start);
/* Let it run ... */
sleep(5);
ioctl(fd, ANIOC_DAC_DMA_STOP, 0);
close(fd);
free(buf);
The total DMA buffer length is configured by
CONFIG_STM32_DACxCHy_DMA_BUFFER_SIZE (default 256). The
DMAHBUF_WRITE command can also be used to write each half
separately instead of DMABUFF_INIT:
/* Equivalent to one DMABUFF_INIT using two half writes */
uint32_t half = info.dma_buffer_size / 2;
struct dac_dma_event_s ev;
ev.buffer = first_half_data; /* half 0 */
ev.half = 0;
ioctl(fd, ANIOC_DAC_DMAHBUF_WRITE, (unsigned long)&ev);
ev.buffer = second_half_data; /* half 1 */
ev.half = 1;
ioctl(fd, ANIOC_DAC_DMAHBUF_WRITE, (unsigned long)&ev);
Stream sub-mode (halfint = 1)
Both half-transfer (HTIF) and transfer-complete (TCIF) interrupts are
enabled. Each time a half-transfer finishes the driver posts an event
to a semaphore. The application calls GET_EVENT (blocking) to
obtain the index of the completed half, writes new data into that half
via DMAHBUF_WRITE, and the DMA engine continues filling the other
half — enabling continuous real-time waveform synthesis.
Initial fill: both halves must be written before starting the DMA.
Refill: after each GET_EVENT call, write only one half
(the half whose index was returned).
Example: DDS sine wave generation using stream mode:
#include <nuttx/analog/dac.h>
#include <sys/ioctl.h>
struct dac_info_s info;
ioctl(fd, ANIOC_DAC_INFO, (unsigned long)&info);
uint32_t half_len = info.dma_buffer_size / 2;
uint16_t *workbuf = malloc(half_len * sizeof(uint16_t));
int phase_acc = 0;
/* Fill both halves with the initial sine data */
for (int j = 0; j < 2; j++)
{
for (int i = 0; i < half_len; i++)
{
workbuf[i] = sine_table[phase_acc >> 12];
phase_acc = (phase_acc + phase_step) & (1024 * 4096 - 1);
}
struct dac_dma_event_s ev;
ev.buffer = workbuf;
ev.half = j;
ioctl(fd, ANIOC_DAC_DMAHBUF_WRITE, (unsigned long)&ev);
}
/* Start stream mode */
struct dac_dma_start_s start;
start.halfint = 1;
ioctl(fd, ANIOC_DAC_DMA_START, (unsigned long)&start);
/* Refill loop — write one half per event */
volatile int stop = 0;
while (!stop)
{
struct dac_dma_event_s ev;
int ret = ioctl(fd, ANIOC_DAC_DMA_GET_EVENT,
(unsigned long)&ev);
if (ret < 0)
break; /* e.g. -EINTR from signal */
/* Fill the completed half with new sine samples */
for (int i = 0; i < half_len; i++)
{
workbuf[i] = sine_table[phase_acc >> 12];
phase_acc = (phase_acc + phase_step) & (1024 * 4096 - 1);
}
ev.buffer = workbuf; /* write to the half returned by GET_EVENT */
ioctl(fd, ANIOC_DAC_DMAHBUF_WRITE, (unsigned long)&ev);
}
ioctl(fd, ANIOC_DAC_DMA_STOP, 0);
close(fd);
free(workbuf);
DMA Structures
These structures are defined in include/nuttx/analog/dac.h.
-
struct dac_dma_start_s
Describes the DMA start configuration:
struct dac_dma_start_s
{
uint8_t halfint; /* 0 = circular (TC only),
* 1 = stream (HT + TC) */
};
-
struct dac_dma_event_s
Carries a half-buffer pointer and half index for stream mode:
struct dac_dma_event_s
{
FAR uint16_t *buffer; /* data to write (DMAHBUF_WRITE) */
int half; /* 0 = first half, 1 = second half
* (returned by GET_EVENT,
* set by caller for DMAHBUF_WRITE) */
};
-
struct dac_info_s
Contains DAC capabilities and runtime state:
struct dac_info_s
{
uint8_t sample_bits; /* DAC resolution (12 bits) */
uint8_t dma_enabled; /* 1 if DMA is currently running */
uint8_t halfint_enabled; /* 1 if stream (HT) mode is active */
uint32_t dma_buffer_size; /* total DMA buffer length in samples */
uint32_t dma_timer_frequency; /* timer output frequency in Hz */
};
DMA ioctl Commands
These commands are defined in include/nuttx/analog/ioctl.h.
The following commands are available when DMA is enabled
(CONFIG_STM32_DAC1CH1_DMA or CONFIG_STM32_DAC1CH2_DMA).
For the standard DAC ioctl commands see
DAC Drivers.
-
ANIOC_DAC_DMABUFF_INIT
Copy the entire user buffer into the internal DMA buffer
(dma_buffer_size samples). The caller must provide a buffer
of exactly dma_buffer_size * sizeof(uint16_t) bytes.
IN:
uint16_t *— source buffer OUT: None
-
ANIOC_DAC_DMA_START
Start the DMA transfer. The argument is a struct dac_dma_start_s *.
IN:
struct dac_dma_start_s *OUT: None
-
ANIOC_DAC_DMA_STOP
Stop the DMA transfer and the associated timer.
IN: None OUT: None
-
ANIOC_DAC_DMA_GET_EVENT
Wait (blocking) for a half-transfer DMA event. Returns the index of
the completed half (0 or 1) in dac_dma_event_s::half.
Returns -EINTR if interrupted by a signal (e.g. Ctrl+C).
IN: None OUT:
struct dac_dma_event_s *—halfset to the completed half
-
ANIOC_DAC_DMAHBUF_WRITE
Write data into one half of the internal DMA buffer. The
buffer field points to the user data; half selects which half
(0 = first, 1 = second) to overwrite. The data length is always
(dma_buffer_size / 2) * sizeof(uint16_t) bytes.
IN:
struct dac_dma_event_s *OUT: None
-
ANIOC_DAC_INFO
Query DAC capabilities and current runtime state. Returns the values
in a struct dac_info_s *. This command does not require DMA to be
enabled; it always reports the fixed resolution (12 bits) and,
depending on configuration, may return zero for DMA-related fields.
IN: None OUT:
struct dac_info_s *
Configuration
Each channel has its own Kconfig options:
Name |
Description |
|
Enable DAC1 channel 1 / channel 2 |
|
Enable DMA for the channel |
|
DMA buffer size in samples (default 256) |
|
DMA buffer size in samples (default 256) |
|
DMA stream priority (default Medium) |
|
DMA stream priority (default Medium) |
|
Timer number for DMA trigger (range 1–15 on H7) |
|
Timer number for DMA trigger (range 1–15 on H7) |
|
Timer output frequency in Hz |
|
Timer output frequency in Hz |
The corresponding timer peripheral must also be enabled:
CONFIG_STM32_TIM<n>=y
where <n> matches the timer number set in STM32_DAC1CH1_TIMER
(or STM32_DAC1CH2_TIMER).