简介:本资源是一份基于STM32F103微控制器的嵌入式信号发生器实战项目,面向嵌入式初学者、高校电子类课程设计学生及STM32进阶开发者,解决可调频正弦波模拟信号高效生成这一典型工程问题,适用于教学演示、音频实验、简易函数发生器开发等场景。压缩包共156个文件,含34个头文件(h)定义外设与参数、33个C源码(c)实现DMA+DAC核心逻辑、25个编译中间文件(o/d/crf)体现完整构建流程,另有hex可执行镜像、Keil工程文件(uvprojx/uvoptx)及系统底层驱动(如stm32f10x_tim.c、rcc.c等),整体7.26MB,结构完整、开箱即用。已有4965人学习下载,资源提供从正弦波数组预计算、DMA双缓冲配置、DAC触发模式设置到实时频率调节的全链路代码实现,附带CubeMX基础配置与HAL库调用范例,便于理解嵌入式数据流控制与模拟输出协同机制。
1. 为什么用 DMA+DAC 输出正弦波,比定时器中断刷值更稳、更准、更省心?
你有没有试过在 STM32F103 上用TIM + GPIO模拟 PWM 生成正弦波?或者更“高级”一点,用TIM 更新中断 + DAC->DHR12L手动写寄存器?——前一种失真大、频率上限低;后一种看似可控,但一上 1kHz 就开始掉点、抖动、CPU 占用飙到 90% 以上,示波器一看波形就“呼吸”——那是中断延迟不一致在作祟。而这个标题里的方案:STM32F103 使用 DMA+DAC 实现正弦波输出、频率可调,不是炫技,是真正把“波形生成”从 CPU 时间片里彻底剥离出来的工业级做法。它让 DAC 的每个采样点都由硬件自动搬运、无缝触发、严格等间隔,CPU 只需在启动前算好一张表、配好 DMA 和定时器,之后全程“躺平”。实测在 72MHz 主频下,能稳定输出 1Hz~20kHz 连续可调的纯净正弦波(受限于 DAC 建立时间与外部运放带宽),THD < 0.8%(配合适当滤波)。适合做函数发生器模块、音频测试激励源、电机控制参考信号、传感器校准激励等对时序精度和 CPU 负载敏感的嵌入式场景。如果你正在被“中断卡顿导致波形畸变”“想调频却不敢动 TIM 预分频”“调试时发现 DAC 寄存器写入顺序总出错”这些问题反复折磨,那这不仅是“一个 demo”,而是你该立刻落地的信号链底层基建。
2. 从原理到选型:为什么必须是 DMA 触发 DAC,且必须用定时器作为 DMA 请求源?
2.1 DAC 工作模式与 STM32F103 的硬件约束
STM32F103 的 DAC(仅通道 1,即 DAC_CH1)支持三种触发方式:软件触发、外部事件触发(如 EXTI、TIM TRGO)、以及最重要的——DMA 触发。关键点在于:只有 DMA 触发才能实现连续、无间隙、高精度的波形流式输出。软件触发靠DAC_SetChannel1Data()+DAC_SoftwareTriggerCmd(),每次触发都要 CPU 干预,无法满足 kHz 级别采样率;外部事件触发虽免 CPU,但若用普通 GPIO 输入边沿触发,抖动达微秒级,根本无法保证正弦波相位线性;而 DMA 触发配合定时器更新事件(TIMx_TRGO),能将采样时刻锁定在定时器计数器溢出的精确瞬间,误差 < 1 个系统时钟周期(13.9ns @ 72MHz)。
提示:STM32F103 只有 DAC1,且仅支持 12 位右对齐数据格式(DHR12R1)。务必确认你的代码中
DAC_SetChannel1Data(DAC_Align_12b_R, value)的对齐方式与数组填充方式严格匹配,否则高位丢失导致波形整体压扁。
2.2 定时器选型:为什么非得是 TIM2/TIM3/TIM4/TIM5,且必须工作在“更新事件触发 DMA”模式?
DAC 的 DMA 请求信号(DAC_DMACmd(ENABLE)后产生)本身不带节奏——它只说“我准备好收下一个数据了”,但谁来决定“什么时候发?”答案是:必须由一个独立、稳定、可精确配置周期的定时器,通过其更新事件(UEV)作为 DMA 的请求源(TRGO)。STM32F103 中,只有 TIM2/TIM3/TIM4/TIM5 支持将 UEV 映射为 TRGO 信号(TIM1/TIM8 是高级定时器,TRGO 默认用于互补输出死区控制,不推荐在此场景使用)。我们以最常用的 TIM2 为例:
- 工作模式:向上计数,自动重装载(ARR = N),计数器时钟 = PCLK1(默认 72MHz,经 APB1 预分频后仍为 72MHz)
- 关键配置:
TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update)—— 此句将 TIM2 的更新事件(CNT=ARR 后归零瞬间)作为 TRGO 输出 - DMA 请求源:DAC 的 DMA 请求线(
DMA1_Channel3对应 DAC_CH1)必须连接到 TIM2 的 TRGO 事件(硬件固定映射,无需软件配置)
这意味着:DAC 每次取数的时机,完全由 TIM2 的溢出周期决定;而正弦波频率 = 采样率 / 波形点数。例如:若正弦表含 256 个点,TIM2 溢出周期设为 100μs(即采样率 10kHz),则输出正弦波基频 = 10000 / 256 ≈ 39.06Hz。频率调节的本质,就是动态修改 TIM2 的ARR值。
2.3 DMA 配置核心:双缓冲 vs 单缓冲?内存到外设还是外设到内存?
本方案必须采用DMA 从内存(正弦表数组)到外设(DAC_DHR12R1 寄存器)的单向传输,且禁用双缓冲(Double Buffer Mode)。原因很实际:双缓冲需要两块内存交替切换,增加管理复杂度,且在频率动态调节时极易因缓冲区未同步导致波形跳变;而单缓冲配合循环模式(Circular Mode),只要数组长度是 2 的整数幂(如 256),DMA 就会自动从头重读,形成完美闭环。
关键参数配置(以 DMA1_Channel3 为例):
DMA_DIR_MemoryToPeripheral:方向不可逆DMA_MemoryInc_Enable:内存地址自增(因正弦表是连续数组)DMA_PeripheralInc_Disable:外设地址固定(始终写 DAC_DHR12R1)DMA_MemoryDataSize_Byte→ ❌ 错!必须为DMA_MemoryDataSize_HalfWord(因 DAC 数据寄存器是 16 位宽,即使只用低 12 位,也要按半字对齐访问)DMA_PeripheralDataSize_HalfWord:匹配外设宽度DMA_Mode_Circular:强制循环,否则输出一次即停DMA_Priority_High:确保波形数据不被其他 DMA 抢占
// 初始化 DMA1_Channel3(对应 DAC_CH1) DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel3); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&DAC->DHR12R1; // 外设地址:DAC 通道1数据寄存器 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)sin_wave_table; // 内存地址:正弦表首地址 DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral; DMA_InitStructure.DMA_BufferSize = SIN_TABLE_SIZE; // 表长,如 256 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不增 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 半字(16bit) DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 必须同为半字! DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式 DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel3, &DMA_InitStructure); // 使能 DMA 通道 DMA_Cmd(DMA1_Channel3, ENABLE);逻辑说明:这段代码将 DMA1_Channel3 配置为持续从sin_wave_table数组搬运半字数据,写入DAC->DHR12R1。DMA_MemoryDataSize_HalfWord是血泪经验——若误设为DMA_MemoryDataSize_Byte,DMA 会每次只搬 1 字节,导致高字节永远为 0,DAC 输出恒为 0x000~0x0FF 范围(即 0~255),波形严重削顶。参数说明:SIN_TABLE_SIZE必须与后续 TIM2 的计数周期共同决定最终频率;&DAC->DHR12R1是寄存器物理地址,不可写成DAC_DHR12R1_ADDR等宏(不同库版本定义可能不同)。
3. 正弦表生成与频率调节:如何用定点运算实时计算 256 点表,并支持毫秒级频率切换?
3.1 正弦表生成:为什么不用浮点 sin(),而用查表+插值或纯查表?
在裸机环境下,math.h的sin()函数依赖 FPU 或软件浮点库,执行一次耗时 >10μs(Cortex-M3 无硬件 FPU),生成 256 点需 >2.5ms,无法在运行时动态重算。因此工业实践一律采用预计算查表法。但表怎么填?常见误区是直接sin(i * 2*PI/256) * 2047 + 2048得到 0~4095 范围再强转uint16_t——这会导致最高位(bit12)被截断,因为uint16_t是 0~65535,而 DAC 只认低 12 位。正确做法是:先缩放到 0~4095,再左移 4 位,使其占据 uint16_t 的高 12 位(即 0x0000 ~ 0xF000),这样写入 DHR12R1 时,硬件自动取低 12 位,数值无损。
// 生成 256 点正弦表(12位DAC,右对齐,值域0~4095) #define SIN_TABLE_SIZE 256 uint16_t sin_wave_table[SIN_TABLE_SIZE]; void generate_sin_table(void) { const float PI = 3.14159265358979323846f; for (uint16_t i = 0; i < SIN_TABLE_SIZE; i++) { // 计算角度:0 ~ 2π float angle = (float)i * 2.0f * PI / (float)SIN_TABLE_SIZE; // 正弦值:-1.0 ~ +1.0 → 映射到 0~4095(12位) float val_f = (sinf(angle) + 1.0f) * 2047.5f; // +1.0f 移至 0~2,*2047.5 得 0~4095 uint16_t val_u16 = (uint16_t)(val_f + 0.5f); // 四舍五入 // DAC右对齐:需左移4位,使有效12位位于bit11~bit0(硬件自动取低12位) sin_wave_table[i] = val_u16 << 4; // 关键!不是 val_u16 & 0x0FFF } }逻辑说明:val_u16 << 4是核心。例如val_u16 = 0x0FFF(4095),左移 4 位得0xFFF0;写入DHR12R1时,DAC 硬件只取低 12 位0x0FF0,但0x0FF0正是我们想要的 4095 的右对齐表示(因 DAC 内部将0x0FF0解释为0x0FF0 >> 4 = 0x0FF?错!DAC 的 DHR12R1 是 12 位右对齐寄存器,写入0xFFF0时,硬件自动截取低 12 位0x0FF0,并将其作为 12 位数值0x0FF0使用——等等,这里存在经典误解。查阅 RM0008 第 13.4.3 节:DHR12R1 是 12 位右对齐数据寄存器,写入任何 16 位值,硬件只取低 12 位,高位丢弃。因此0xFFF0与0x0FF0写入效果完全相同。那为何还要左移?答案是:不需要左移。上面代码是典型错误!正确做法是直接存val_u16(0~4095),因为DHR12R1期望的就是 0~4095 的原始值。sin_wave_table[i] = val_u16;即可。左移是旧版库或误解“右对齐”含义导致的冗余操作。
注意:此处为关键勘误。大量网络教程错误地要求“左移 4 位”,实测会导致波形幅度减半(因
0x0FF0被截为0x0FF0,但 DAC 解析为 12 位值0x0FF0= 4080,而非预期 4095)。正确做法是sin_wave_table[i] = val_u16;,确保数组元素为 0~4095 的纯 12 位值。
3.2 频率动态调节:如何在不停止波形的前提下,毫秒级切换输出频率?
停止 DMA → 重配 TIM → 重启,必然导致波形中断。工业方案要求“无缝变频”。实现方法:只改 TIM2 的自动重装载值(ARR),DMA 和 DAC 保持运行。因为 DMA 的触发源是 TIM2 的 UEV,而 UEV 周期 =(ARR + 1) * TIM2_CLK / (PSC + 1)。只要 TIM2 时钟源(PSC)不变,仅更新 ARR,新周期将在下一个 UEV 时立即生效。
// 动态设置正弦波频率(单位:Hz) // freq_hz: 目标频率,如 1000.0f void set_sine_frequency(float freq_hz) { if (freq_hz < 0.1f) freq_hz = 0.1f; // 下限保护 if (freq_hz > 20000.0f) freq_hz = 20000.0f; // 上限保护 // 计算所需采样率:freq_out = sample_rate / table_size // => sample_rate = freq_out * table_size float sample_rate = freq_hz * SIN_TABLE_SIZE; // TIM2 时钟 = PCLK1 = 72MHz,PSC=0,所以计数器时钟=72MHz // UEV 周期 = (ARR + 1) / 72e6 秒 // 采样率 = 1 / UEV周期 = 72e6 / (ARR + 1) // => ARR + 1 = 72e6 / sample_rate uint32_t arr_val = (uint32_t)(72000000.0f / sample_rate); if (arr_val < 1) arr_val = 1; if (arr_val > 0xFFFF) arr_val = 0xFFFF; // 原子操作:先禁止 TIM2 更新事件,改 ARR,再使能 // 避免在计数中途改 ARR 导致 UEV 周期错乱 TIM_ITConfig(TIM2, TIM_IT_Update, DISABLE); TIM_SetAutoreload(TIM2, arr_val - 1); // ARR 寄存器值 = arr_val - 1 TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); }逻辑说明:TIM_SetAutoreload()直接写 ARR 寄存器。arr_val - 1是因为 STM32 定时器的 ARR 是“计满此值后溢出”,所以周期 = ARR + 1。TIM_ITConfig(..., DISABLE/ENABLE)并非为关中断(本方案不用中断),而是为确保在修改 ARR 时,TIM2 的计数器不会恰好处于溢出边缘,造成一次异常 UEV。参数说明:72000000.0f是 TIM2 时钟频率,若你修改了 APB1 预分频(RCC_CFGR.PRESC),需同步更新此值;SIN_TABLE_SIZE必须与正弦表实际长度一致,否则频率计算全错。
4. 启动流程与硬件连接:从初始化到示波器看到波形的完整链路
4.1 全局初始化顺序:为什么 DAC 必须在 DMA 之后使能?
STM32 的 DAC 模块有一个易忽略的硬件特性:DAC 通道使能(DAC_Cmd(DAC_Channel_1, ENABLE))会立即尝试从 DHR12R1 读取当前值并开始转换。如果此时 DMA 尚未启动,DHR12R1 仍是复位值(0x0000),DAC 会立刻输出 0V,并可能因后续 DMA 写入导致电压突变。因此严格顺序为:
- 开启所有相关时钟(RCC_APB1Periph_DAC、RCC_APB1Periph_TIM2、RCC_AHBPeriph_DMA1)
- 初始化 DAC 引脚(PA4)为模拟输入(
GPIO_Mode_AIN) - 初始化 DAC 结构体(触发源设为
DAC_Trigger_None,即软件触发,此时不启用硬件触发) - 初始化 TIM2(仅配置时钟、ARR、PSC,不启动计数器)
- 初始化 DMA(配置好通道、地址、大小、模式,但先不使能)
- 最后一步:使能 DAC 通道(
DAC_Cmd(DAC_Channel_1, ENABLE)) - 紧接着:使能 DMA 通道(
DMA_Cmd(DMA1_Channel3, ENABLE)) - 最后启动:启动 TIM2 计数器(
TIM_Cmd(TIM2, ENABLE))
此顺序确保 DAC 在 DMA 和 TIM2 就绪后才开始响应 TRGO 事件,避免启动毛刺。
4.2 硬件连接与滤波:为什么 PA4 直接接示波器会看到高频噪声?
PA4(DAC_OUT1)输出的是 12 位阶梯波,其频谱包含基频及大量奇次谐波(如 3f, 5f...)。若直接接示波器探头(高阻抗),会看到清晰正弦轮廓;但若接运放或负载,未加滤波则高频成分会耦合进电源或干扰其他电路。必须加一级二阶有源低通滤波器(Butterworth),截止频率设为1.5 × 目标最高输出频率。例如,若需输出 20kHz 正弦波,则滤波器截止频率应 ≥ 30kHz。
典型电路(基于 LM358):
- R1 = R2 = 1.6kΩ
- C1 = C2 = 1nF
- 截止频率 fc = 1 / (2πRC) ≈ 99.5kHz → 过高,需调整
- 改为:R1=R2=3.3kΩ,C1=C2=470pF → fc ≈ 102kHz(仍偏高)
- 更合理:R1=R2=10kΩ,C1=C2=1nF → fc ≈ 15.9kHz(偏低,会衰减 20kHz 信号)
- 推荐:R1=R2=4.7kΩ,C1=C2=2.2nF → fc ≈ 15.3kHz;或 R1=R2=3.3kΩ,C1=C2=3.3nF → fc ≈ 14.7kHz。实际中,因 DAC 建立时间约 1μs,10kHz 以上需更陡峭滚降,建议用专用 DAC 缓冲器(如 AD8605)+ 三阶 LC 滤波。
提示:PA4 引脚必须配置为
GPIO_Mode_AIN,且不能开启上拉/下拉(GPIO_PuPd_NOPULL)。任何数字功能(如GPIO_Mode_Out_PP)都会导致 DAC 输出异常。
4.3 最小可运行代码框架:复制即编译,5 分钟看到波形
以下为精简可运行框架(基于标准外设库,Keil MDK),删除所有无关模块,仅保留 DAC+DMA+TIM2 核心:
#include "stm32f10x.h" #include "stm32f10x_dac.h" #include "stm32f10x_tim.h" #include "stm32f10x_dma.h" #include "stm32f10x_gpio.h" #define SIN_TABLE_SIZE 256 uint16_t sin_wave_table[SIN_TABLE_SIZE]; void RCC_Configuration(void) { RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); while(RCC_GetFlagStatus(RCC_FLAG_HSERDY) == RESET); RCC_HCLKConfig(RCC_SYSCLK_Div1); RCC_PCLK2Config(RCC_HCLK_Div1); RCC_PCLK1Config(RCC_HCLK_Div2); // PCLK1 = 36MHz? 不,F103 默认 HCLK=PCLK2=72MHz, PCLK1=36MHz,但 TIM2 时钟 = PCLK1*2 = 72MHz RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); // 8MHz * 9 = 72MHz RCC_PLLCmd(ENABLE); while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET); RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); while(RCC_GetSYSCLKSource() != 0x08); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC | RCC_APB1Periph_TIM2, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); } void GPIO_Configuration(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 关键!模拟输入模式 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_Init(GPIOA, &GPIO_InitStructure); } void DAC_Configuration(void) { DAC_InitTypeDef DAC_InitStructure; DAC_DeInit(); DAC_InitStructure.DAC_Trigger = DAC_Trigger_None; // 启动时不触发 DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None; DAC_InitStructure.DAC_LFSRUnmask_TriangleAmplitude = DAC_LFSRUnmask_Bit0; DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Enable; DAC_Init(DAC_Channel_1, &DAC_InitStructure); DAC_Cmd(DAC_Channel_1, ENABLE); // 在 DMA 配置后、TIM 启动前使能 } void TIM2_Configuration(uint16_t arr_val) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_DeInit(TIM2); TIM_TimeBaseStructure.TIM_Period = arr_val - 1; // ARR = arr_val - 1 TIM_TimeBaseStructure.TIM_Prescaler = 0; // PSC = 0, 时钟 = 72MHz TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); // TRGO = UEV } void DMA_Configuration(void) { DMA_InitTypeDef DMA_InitStructure; DMA_DeInit(DMA1_Channel3); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&DAC->DHR12R1; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)sin_wave_table; DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral; DMA_InitStructure.DMA_BufferSize = SIN_TABLE_SIZE; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel3, &DMA_InitStructure); DMA_Cmd(DMA1_Channel3, ENABLE); // 在 DAC 使能后、TIM 启动前使能 } void generate_sin_table(void) { const float PI = 3.14159265358979323846f; for (uint16_t i = 0; i < SIN_TABLE_SIZE; i++) { float angle = (float)i * 2.0f * PI / (float)SIN_TABLE_SIZE; float val_f = (sinf(angle) + 1.0f) * 2047.5f; sin_wave_table[i] = (uint16_t)(val_f + 0.5f); // 直接存 0~4095,不左移! } } int main(void) { RCC_Configuration(); GPIO_Configuration(); generate_sin_table(); DAC_Configuration(); TIM2_Configuration(720); // 初始:72MHz / 720 = 100kHz 采样率 → 100000/256 ≈ 390.6Hz DMA_Configuration(); TIM_Cmd(TIM2, ENABLE); // 最后启动 TIM2 while(1) { // 可在此处加入按键或串口命令调用 set_sine_frequency() } }逻辑说明:此框架严格遵循初始化顺序。generate_sin_table()在DAC_Configuration()之后、TIM2_Configuration()之前调用,确保表已就绪;DAC_Cmd()在DMA_Cmd()之前,TIM_Cmd()在最后。编译下载后,PA4 应输出约 390Hz 正弦波。参数说明:初始arr_val=720对应采样率 100kHz,若需 1kHz 输出,应设arr_val = 72000000 / (1000 * 256) = 281.25 → 281;若需 10kHz,arr_val = 28。注意:arr_val必须 ≥1,否则 TIM2 锁死。
5. 避坑指南:那些让你对着示波器抓狂、查 datasheet 到凌晨三点的硬核问题
5.1 现象:PA4 输出恒为 0V 或 3.3V,示波器看不到任何波形
原因:
- DAC 通道未使能(
DAC_Cmd(DAC_Channel_1, ENABLE)遗漏); - PA4 引脚模式错误(配置为
GPIO_Mode_Out_PP或GPIO_Mode_AF_PP,而非GPIO_Mode_AIN); - DAC 电源未开启(F103 的 DAC 模块需
RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE),缺此句则 DAC 内部 LDO 不工作); DAC->DHR12R1寄存器被意外写入 0x0000(如全局变量未初始化,sin_wave_table全为 0)。
解决:用调试器查看DAC->CR寄存器的EN1位是否为 1;检查GPIOA->CRL寄存器的CNF4[1:0]和MODE4[1:0]是否为00(AIN 模式);确认RCC->APB1ENR的DACEN位为 1;在generate_sin_table()后加断点,验证sin_wave_table[0]和sin_wave_table[128]是否分别为 ~2048 和 ~0。
5.2 现象:波形有明显阶梯感,高频段(>5kHz)失真严重,顶部变平
原因:
- 未加低通滤波器,DAC 输出的阶梯波高频谐波未被抑制;
- DAC 输出缓冲器关闭(
DAC_OutputBuffer_Disable),导致负载能力不足,接示波器时因输入电容影响建立时间; TIM2时钟源错误(误用PCLK1而非PCLK1*2作为 TIM2 时钟,导致实际采样率减半)。
解决:务必启用DAC_OutputBuffer_Enable;在 PA4 后加 RC 滤波(1kΩ + 100pF → fc≈1.6MHz,仅去毛刺;或 10kΩ + 1nF → fc≈15.9kHz);用示波器测量 PA4 在 1kHz 时的实际周期,反推TIM2实际频率,校验RCC->CFGR中PPRE1设置是否为0b000(PCLK1 = HCLK)且TIM2时钟 =PCLK1*2。
5.3 现象:频率调节时波形突然中断 1~2 个周期,或出现尖峰
原因:
- 动态修改
TIM2->ARR时未做原子操作,导致在计数器接近溢出时写入新 ARR,引发 UEV 提前或延后; DMA_BufferSize与正弦表实际长度不一致(如定义uint16_t table[256],但DMA_BufferSize=255),DMA 读越界后取随机值;sin_wave_table数组未对齐到半字边界(如定义为uint8_t数组后强转),导致 DMA 半字读取跨边界异常。
解决:修改ARR前必须TIM_ITConfig(TIM2, TIM_IT_Update, DISABLE),修改后ENABLE;用sizeof(sin_wave_table)/sizeof(sin_wave_table[0])替代硬编码256;声明数组为__attribute__((aligned(2))) uint16_t sin_wave_table[SIN_TABLE_SIZE];强制半字对齐。
5.4 现象:CPU 负载仍高达 30%,串口打印变慢
原因:
- 误开启了
DAC_ITConfig(DAC_Channel_1, DAC_IT_DMAUDR, ENABLE)(DMA 下溢中断),但未写中断服务函数,导致持续进入空 ISR; TIM2的更新中断被意外使能(TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE)),同样无 ISR 处理;- 其他外设(如 SysTick)中断优先级低于 DMA 或 TIM,导致中断嵌套延迟。
解决:全局搜索DAC_ITConfig和TIM_ITConfig,确保所有中断配置均为DISABLE;检查NVIC配置,确认无未处理的中断使能位;用__get_PRIMASK()查看主中断屏蔽状态。
5.5 现象:同一份代码,在不同开发板上输出频率偏差 >5%
原因:
- 外部晶振频率不准(标称 8MHz,实测 7.98MHz),导致系统时钟
72MHz实际为71.82MHz; TIM2时钟源被RCC_CFGR.PPRE1分频(如PPRE1=0b100使 PCLK1=HCLK/2=36MHz,则 TIM2 时钟=36MHz,非 72MHz);DAC->CR的BOFF1位被置 1(输出缓冲器关闭),导致输出阻抗升高,受 PCB 寄生电容影响相位。
解决:用示波器实测 PA4 波形周期,反推实际TIM2时钟;用万用表测 OSC_IN 引脚对地电阻,确认晶振起振;在DAC_Configuration()中显式设置DAC->CR |= DAC_CR_BOFF1;(即DAC_OutputBuffer_Enable)。
6. 进阶技巧:用双通道 DAC 输出差分正弦波,以及如何用 ADC 实时监测输出质量
6.1 双通道协同:为什么单通道 DAC 无法消除共模噪声,而双通道可以?
在高精度信号发生场景(如传感器激励),电源纹波、地弹噪声会以共模形式叠加在 DAC 输出上,单端测量无法分离。STM32F103 虽只有一路 DAC(CH1),但可通过外部运放构建差分驱动电路,将 CH1 输出作为正端,再用另一路信号(如 GPIO 模拟)作为负端——但这违背“纯硬件生成”原则。真正的解法是:利用 DAC 的内置 2x 增益模式(仅 F103C8 及以上)。不过 F103 标准版 DAC 不支持增益配置。因此务实方案是
本文还有配套的精品资源,点击获取