1. DAC数模转换原理与STM32架构定位
DAC(Digital-to-Analog Converter)是嵌入式系统中实现数字域与模拟域双向交互的关键外设。其核心功能是将处理器内部以二进制形式存储和运算的数字量,线性映射为连续可变的电压或电流信号。这一过程与ADC(Analog-to-Digital Converter)构成完整的信号链闭环:传感器采集的物理量经ADC量化为数字码流,由CPU或DSP完成滤波、控制、逻辑判断等处理后,再通过DAC还原为驱动执行器(如电机驱动电路、音频功放、精密电源调节模块)所需的模拟激励信号。
在STM32F103系列微控制器中,DAC并非可选外设,而是集成于芯片硅片之上的固定功能模块。其设计目标明确指向工业控制、音频信号生成、波形发生器等对模拟输出精度与实时性有基本要求的应用场景。理解DAC在STM32整体架构中的位置,是进行可靠配置的前提。
从总线拓扑看,DAC模块挂载于APB1总线(Advanced Peripheral Bus 1),与USART2、I2C1、SPI2等低速外设共享同一时钟域。这意味着DAC的寄存器读写操作受APB1总线频率约束,其配置时序必须满足APB1总线协议。值得注意的是,DAC不依赖于AHB总线,因此其数据寄存器无法被DMA直接寻址——这是初学者常陷入的误区。实际工程中,DAC的数据寄存器(DORx)是只写的,且其更新机制与DMA通道存在特定的耦合关系,这一点将在后续DMA配置环节详述。
DAC的参考电压源(VREF+)引脚与ADC共用,这并非设计冗余,而是硬件层面强制统一模拟前端基准的体现。在普中科技玄武/朱雀开发板上,VREF+已通过0Ω电阻或跳线帽硬连接至VDDA(3.3V)。这一物理连接决定了整个模拟子系统的量化基准:当DAC工作在12位模式时,理论满量程输出电压为3.3V,最小可分辨电压增量(LSB)为3.3V / 4096 ≈ 0.806mV。若应用对输出精度要求严苛,必须确保VDDA电源纹波低于10mV,并远离数字开关噪声源;若仅需粗略控制,此默认连接已足够。
2. STM32F103 DAC硬件特性深度解析
STM32F103xC/D/E系列配备两个完全独立的DAC通道:DAC1与DAC2。这种双通道设计并非简单复制,而是从底层硬件资源到软件抽象均实现物理隔离。每个通道拥有专属的:
- 数字数据寄存器(DHR12R1/DHR12R2用于12位右对齐;DHR12L1/DHR12L2用于12位左对齐;DHR8R1/DHR8R2用于8位模式)
- 输出数据寄存器(DOR1/DOR2),该寄存器由硬件自动更新,软件不可直接写入
- 通道使能与触发控制寄存器(CR)
这种独立性带来三个关键工程优势:
1.通道间无干扰:DAC1与DAC2可同时输出不同幅值、不同波形的模拟信号,互不影响。例如,在电机FOC控制中,DAC1可输出U相参考电压,DAC2输出V相参考电压。
2.触发源可异构:DAC1可配置为软件触发,DAC2则可配置为TIM2更新事件触发,实现精确的相位差控制。
3.功耗可精细管理:当仅需单路输出时,可单独关闭另一通道的电源,降低系统待机功耗。
DAC的分辨率支持8位与12位两种模式,此选择直接决定系统动态范围与控制粒度。12位模式提供4096级离散电平,是绝大多数应用的首选;8位模式仅256级,适用于LED亮度PWM调光等对精度要求不高的场合。分辨率选择不仅影响数据寄存器宽度,更关键的是决定了数据对齐方式:
| 模式 | 数据寄存器 | 对齐方式 | 写入值范围 | 实际有效位 |
|---|---|---|---|---|
| 12位右对齐 | DHR12R1 | 右对齐 | 0x000–0xFFF | bit[11:0] |
| 12位左对齐 | DHR12L1 | 左对齐 | 0x000–0xFFF | bit[15:4](高12位) |
| 8位 | DHR8R1 | 仅右对齐 | 0x00–0xFF | bit[7:0] |
为何存在左对齐?这是为兼容某些需要高位字节操作的旧式总线协议或简化软件移位而设计。在现代基于HAL库的开发中,右对齐因其直观性(写入值即为期望的12位数值)而成为绝对主流。左对齐需将12位数据左移4位再写入,易引入移位错误,除非有明确的遗留系统兼容需求,否则应规避。
DAC输出缓冲器(Buffer)是一个常被低估的关键特性。当启用缓冲器时(CR寄存器中BOFFx=0),DAC输出阻抗极低(典型值<150Ω),可直接驱动负载电流≤1mA的电路;当禁用缓冲器时(BOFFx=1),输出阻抗显著升高(>100kΩ),此时必须外接运放进行阻抗匹配与电流放大。普中开发板原理图显示,DAC_OUT1(PA4)与DAC_OUT2(PA5)引脚后均未集成运放,故在驱动任何非高阻抗负载(如示波器探头输入阻抗1MΩ)前,必须启用内部缓冲器,否则输出电压将因负载效应严重跌落。
3. DAC波形发生器能力与工程取舍
STM32F103的DAC硬件原生支持两种特殊波形生成模式:噪声波(Noise Wave)与三角波(Triangle Wave)。此能力由DAC控制寄存器(CR)中的WAVE1/WAVE2位与MAMP1/MAMP2位协同控制。
- 噪声波模式:硬件内部集成一个8位线性反馈移位寄存器(LFSR),其输出序列具有伪随机性。通过设置MAMPx位(0b000–0b111),可配置LFSR的周期长度(从32到8192个时钟周期)。该模式常用于EMI测试中的宽带噪声注入,或简易的音频白噪声发生器。
- 三角波模式:硬件内置一个计数器,按设定的幅度(MAMPx)在0至设定值之间线性递增/递减。当计数器达到上限时自动反转方向,形成对称三角波。此模式无需CPU干预,可稳定输出频率达数MHz的三角波,适用于开关电源斜坡补偿、函数发生器校准信号等场景。
然而,在绝大多数实际项目中,这两种硬件波形发生器极少被采用,原因在于其灵活性与可控性远逊于软件方案:
-频率精度受限:波形频率由APB1时钟分频决定,无法实现亚赫兹级精细调节;
-幅度不可编程:MAMPx仅提供8个离散档位,无法实现任意幅度设定;
-相位不可控:无法实现波形起始相位的精确同步;
-波形不可定制:仅限噪声与三角波,无法生成正弦、方波、锯齿波等常用波形。
因此,工程实践中的主流做法是:禁用WAVE1/WAVE2位,将DAC回归为纯粹的数字量→模拟量转换器。所有复杂波形均由主程序或DMA预填充的波形表(Waveform Table)驱动。这种方式虽增加少量RAM开销,但赋予了开发者对波形形状、频率、相位、幅度的完全控制权,是构建高精度信号源的基础。
4. DAC触发机制与实时性保障
DAC的转换启动方式分为两类:软件触发与硬件触发。理解其差异与适用场景,是保障模拟输出实时性的核心。
4.1 软件触发(SWTRIG)
当CR寄存器中TENx=0时,DAC进入软件触发模式。此时,向DHRx寄存器写入新数据后,DAC立即启动一次转换,将该数据加载至DORx并更新模拟输出。此模式最简单,适用于:
- 手动调节类应用(如电位器替代,用户按键改变输出电压);
- 非实时性要求场景(如传感器校准偏置电压的缓慢调整);
- 调试阶段快速验证DAC硬件连通性。
其局限性在于:每次输出更新均需CPU执行一次寄存器写操作,若需高频更新(如>1kHz),CPU将被持续占用,无法处理其他任务。
4.2 硬件触发(Hardware Trigger)
当TENx=1时,DAC转为硬件触发模式。此时DHRx寄存器写入的数据不会立即生效,而是等待外部事件“拍下快门”。STM32F103支持的触发源包括:
-定时器更新事件(TSELx=0b100):如TIM2/TRGO、TIM3/TRGO、TIM4/TRGO、TIM5/TRGO、TIM6/TRGO、TIM7/TRGO。其中TIM6/TIM7为专用DAC触发定时器,无输出通道,仅提供精准周期中断。
-外部中断线(TSELx=0b001):EXTI Line9(对应GPIOA_Pin9)。
-软件触发(TSELx=0b000):此为特殊硬件触发,等效于写入SWTRIG位,但走硬件触发路径。
为何推荐使用TIM6/TIM7?因为其设计初衷即为DAC服务:
- 无输出比较通道,避免与其他外设冲突;
- 更新事件(UEV)频率由ARR寄存器精确设定,误差仅源于APB1时钟抖动;
- 支持自动重装载,可生成严格周期性波形;
- 在低功耗模式下,若TIM6/TIM7时钟源为LSI(32kHz),仍可维持波形输出。
以生成1kHz正弦波为例:若DAC工作在12位模式,一个周期采样256点,则TIM6需配置为256kHz更新频率(256点 × 1kHz = 256kHz)。此频率远高于人耳听觉上限,可保证波形平滑。TIM6的ARR寄存器值计算公式为:ARR = (TIM6CLK / TargetFreq) - 1。若APB1时钟为36MHz,则ARR = (36,000,000 / 256,000) - 1 = 140 - 1 = 139。
5. DMA协同机制与零CPU干预输出
DAC与DMA的协同是实现高吞吐量、低CPU占用模拟输出的核心技术。但其工作机制与ADC-DMA有本质区别,必须精准把握。
5.1 DAC-DMA数据流拓扑
ADC-DMA是“外设→内存”(Peripheral to Memory)模式,DMA控制器从ADC数据寄存器搬运数据至RAM缓冲区。而DAC-DMA是“内存→外设”(Memory to Peripheral)模式,DMA控制器将RAM中预存的波形数据搬运至DAC的数据保持寄存器(DHRx)。
关键约束在于:DAC的数据寄存器(DHRx)不是普通内存地址,而是APB1总线上的外设寄存器。因此,DMA通道必须配置为:
-PeriphInc = DMA_PINC_DISABLE(外设地址不自增,始终写入同一DHRx地址);
-MemInc = DMA_MINC_ENABLE(内存地址自增,依次读取波形表各点);
-PeriphDataSize = DMA_PDATAWIDTH_HALFWORD(DHRx为16位宽寄存器,即使8位模式也需写入半字);
-MemDataSize = DMA_MDATAWIDTH_HALFWORD(内存中波形表元素为16位);
-Mode = DMA_CIRCULAR(循环模式,实现波形连续播放)。
5.2 DMA请求映射与使能
DAC1与DAC2各自关联一个专用DMA请求信号:
- DAC1 Channel1 → DMA1 Channel3
- DAC2 Channel1 → DMA1 Channel4
此映射关系由硬件固化,不可更改。在初始化时,必须:
1. 启用DMA1时钟(RCC->AHBENR |= RCC_AHBENR_DMA1EN);
2. 配置DMA通道参数(如上所述);
3. 在DAC控制寄存器(DAC->CR)中,设置DMAEN1/DMAEN2位为1,使能对应通道的DMA请求;
4. 设置DMAUDRIE1/DMAUDRIE2位为1(可选),使能DMA更新中断,用于波形切换或数据重装。
5.3 实际工程配置示例
假设使用DAC1输出1kHz正弦波,波形表sine_table[256]已预先计算并存放于SRAM中:
// 1. 初始化DAC1(软件触发,缓冲器使能,不启用DMA) DAC->CR |= DAC_CR_EN1 | DAC_CR_BOFF1; // 2. 初始化DMA1 Channel3 DMA1_Channel3->CPAR = (uint32_t)&DAC->DHR12R1; // 外设地址:DAC1 DHR12R1 DMA1_Channel3->CMAR = (uint32_t)sine_table; // 内存地址:波形表首地址 DMA1_Channel3->CNDTR = 256; // 传输数量:256点 DMA1_Channel3->CCR = DMA_CCR_EN | DMA_CCR_DIR | DMA_CCR_CIRC | DMA_CCR_MINC | DMA_CCR_PSIZE_1 | DMA_CCR_MSIZE_1; // 半字传输,循环模式 // 3. 关联DAC1与DMA:使能DAC1的DMA请求 DAC->CR |= DAC_CR_DMAEN1; // 4. 启动TIM6作为触发源(ARR=139, PSC=0 → 256kHz更新) TIM6->PSC = 0; TIM6->ARR = 139; TIM6->EGR = TIM_EGR_UG; // 产生一次更新事件 TIM6->CR1 = TIM_CR1_CEN; // 启动计数自此,TIM6每256kHz产生一次更新事件,DAC1自动从DHR12R1加载新数据,DMA1 Channel3自动将sine_table下一点数据写入DHR12R1。整个过程CPU全程无参与,可专注于其他任务。
6. 开发板硬件资源与接口约束
普中科技玄武/朱雀F103开发板的DAC硬件接口具有明确的物理限制,忽视这些约束将导致实验失败。
6.1 引脚定义与电气特性
- DAC_OUT1:映射至GPIOA_Pin4(PA4),即DAC1通道输出;
- DAC_OUT2:映射至GPIOA_Pin5(PA5),即DAC2通道输出。
这两路输出在PCB上均未配置任何缓冲或驱动电路,直接引出至排针。这意味着:
-最大负载电流:受限于DAC内部缓冲器能力,典型值为±1mA(数据手册Section 5.3.12);
-输出电压范围:0V 至 VREF+(3.3V),无法输出负电压或超过3.3V的电压;
-直流精度:12位模式下,积分非线性(INL)典型值为±1 LSB,微分非线性(DNL)为±0.5 LSB。若应用要求更高精度,必须进行两点校准(零点与满量程)。
6.2 与ADC的VREF+共享风险
VREF+引脚同时为ADC与DAC提供基准,这一设计在简化硬件的同时引入潜在风险。当ADC进行高速采样(如使用DMA连续扫描多通道)时,其内部采样电容的充放电会在VREF+线上产生瞬态电流尖峰。若此尖峰未被充分滤除,将直接耦合至DAC输出,表现为叠加在期望波形上的高频毛刺。
工程缓解措施:
- 在VREF+引脚就近(<5mm)放置一个100nF X7R陶瓷电容与一个10μF钽电容并联,形成低阻抗储能;
- 若系统对DAC输出纯净度要求极高,可在VREF+与DAC的VREF+输入引脚之间串联一个10Ω磁珠,并在其后再次并联100nF电容,构成π型滤波;
- 在软件层面,避免ADC与DAC在同一APB1时钟周期内密集操作,可利用NVIC抢占优先级将DAC相关中断(如DMA传输完成)设为最高优先级。
6.3 测试与验证方法
在首次验证DAC功能时,推荐按以下步骤进行,以快速定位问题层级:
1.静态电压测试:向DHR12R1写入0x000、0x800、0xFFF,用万用表DC电压档测量PA4对地电压,应分别接近0V、1.65V、3.3V。若偏差过大,检查VREF+是否确为3.3V,及PA4是否被其他外设(如JTAG/SWD)复用;
2.方波测试:配置TIM6为10kHz更新,DMA搬运一个两元素数组{0x000, 0xFFF},用示波器观察PA4,应得到清晰方波。若波形上升/下降沿缓慢,确认DAC缓冲器已启用(BOFF1=0);
3.正弦波测试:使用前述256点正弦表,观察波形平滑度。若出现阶梯状失真,检查DMA是否正确配置为循环模式且内存地址自增。
7. HAL库配置流程与关键参数详解
虽然标准库(StdPeriph Library)曾是STM32F1的主流,但HAL库(Hardware Abstraction Layer)凭借其标准化接口与完善的文档,已成为当前工程首选。以下为基于HAL库的DAC1初始化完整流程,每一步均阐明其底层寄存器操作与工程意义。
7.1 时钟使能与GPIO配置
// 1. 使能DAC与GPIOA时钟 __HAL_RCC_DAC_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); // 2. 配置PA4为模拟输入模式(关键!) GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_4; GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; // 必须为ANALOG,非AF_PP! GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);为何必须配置为ANALOG模式?PA4在复位后默认为浮空输入。若错误配置为复用推挽(AF_PP),PA4将被GPIO外设驱动,与DAC1输出形成电气冲突,轻则输出异常,重则损坏IO口。GPIO_MODE_ANALOG将PA4的施密特触发器与上/下拉电阻全部断开,仅保留模拟通路,是DAC输出的唯一安全模式。
7.2 DAC句柄初始化与通道配置
DAC_HandleTypeDef hdac; hdac.Instance = DAC; hdac.Init.DAC_Trigger = DAC_TRIGGER_NONE; // 初始禁用触发,先测试软件触发 hdac.Init.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE; // 必须启用缓冲器 hdac.Init.DAC_LFSRUnmask_TriangleAmplitude = DAC_LFSR_UNMASK_BITS11_0; // 未使用,占位 hdac.Init.DAC_ThirdPartyTrigger = DAC_EP_TD1_TRG1; // 未使用,占位 if (HAL_DAC_Init(&hdac) != HAL_OK) { Error_Handler(); // 初始化失败,通常因时钟未使能 }HAL_DAC_Init()执行的核心操作是:
- 将DAC->CR寄存器清零(复位状态);
- 根据Init结构体配置DAC->CR:设置EN1位使能DAC1,BOFF1位为0启用缓冲器,TEN1位为0禁用触发;
- 此时不启动任何转换,仅为DAC硬件上电并进入就绪状态。
7.3 软件触发输出与验证
// 启用DAC1通道 if (HAL_DAC_Start(&hdac, DAC_CHANNEL_1) != HAL_OK) { Error_Handler(); } // 输出1.65V(12位右对齐,0x800 = 2048 = 3.3V / 2) HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 0x800); // 触发一次转换(软件触发) HAL_DAC_Start(&hdac, DAC_CHANNEL_1); // 此调用在HAL库中实际执行SWTRIGHAL_DAC_SetValue()仅向DHR12R1写入数据;HAL_DAC_Start()在软件触发模式下,会向DAC->SWTRIGR寄存器写入SWTRIG1位,从而启动转换。此两步分离的设计,允许用户先准备数据,再在精确时刻触发,是实现确定性时序的基础。
7.4 迁移至硬件触发与DMA
// 1. 配置TIM6为256kHz更新源 TIM_HandleTypeDef htim6; htim6.Instance = TIM6; htim6.Init.Prescaler = 0; htim6.Init.Period = 139; // ARR = 139 if (HAL_TIM_Base_Init(&htim6) != HAL_OK) { Error_Handler(); } HAL_TIM_Base_Start(&htim6); // 2. 配置DAC1为TIM6触发 hdac.Init.DAC_Trigger = DAC_TRIGGER_T6_TRGO; HAL_DAC_Init(&hdac); // 重新初始化以更新触发源 HAL_DAC_Start(&hdac, DAC_CHANNEL_1); // 3. 配置DMA(以HAL库封装) hdma_dac1.Instance = DMA1_Channel3; hdma_dac1.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_dac1.Init.PeriphInc = DMA_PINC_DISABLE; hdma_dac1.Init.MemInc = DMA_MINC_ENABLE; hdma_dac1.Init.PeriphDataSize = DMA_PDATAWIDTH_HALFWORD; hdma_dac1.Init.MemDataSize = DMA_MDATAWIDTH_HALFWORD; hdma_dac1.Init.Mode = DMA_CIRCULAR; if (HAL_DMA_Init(&hdma_dac1) != HAL_OK) { Error_Handler(); } // 4. 关联DAC与DMA __HAL_LINKDMA(&hdac, DMA_Handle1, hdma_dac1); HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)sine_table, 256, DAC_ALIGN_12B_R, DAC_DMA_NORMAL);HAL_DAC_Start_DMA()是关键函数,其内部执行:
- 启用DAC1的DMA请求(DAC->CR |= DAC_CR_DMAEN1);
- 启动DMA通道;
- 启动DAC1通道(DAC->CR |= DAC_CR_EN1);
- 此时,TIM6的每一次更新事件,都将触发DAC1转换,并由DMA自动更新DHR12R1。
我在实际项目中曾遇到一个典型问题:DMA搬运波形表时,输出波形出现周期性跳变。排查发现,是由于波形表sine_table被定义在.data段(初始值由Flash拷贝至RAM),而编译器优化将其放置在RAM中靠近堆栈的区域。当任务创建较多时,堆栈增长覆盖了波形表内存,导致数据被破坏。解决方案是将波形表显式置于.ccmram段(若MCU有CCM RAM)或使用__attribute__((section(".my_wave")))指定独立段,并在链接脚本中为其分配安全地址空间。