恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
STM32F103 DAC输出正弦波实战:查表+定时器触发+DMA避坑指南
首页
资讯中心
/
STM32F103 DAC输出正弦波实战:查表+定时器触发+DMA避坑指南
STM32F103 DAC输出正弦波实战:查表+定时器触发+DMA避坑指南
发布时间:2026/10/15 14:31:15
简介本资源是面向嵌入式初学者与STM32进阶开发者的DAC正弦波生成实践项目聚焦STM32F103RCT6芯片的12位DAC外设应用解决模拟信号发生器基础功能实现问题适用于课程设计、实验教学及小型信号源开发场景。压缩包共76个文件以33个.h头文件和32个.c源文件为主体涵盖HAL库驱动、定时器触发配置、DMA数据传输、正弦波查表法实现及LED/USART等配套外设模块另含Keil工程文件uvprojx/uvoptx、启动代码startup_stm32f10x_hd.s、调试配置JLinkSettings.ini及实测波形图LED波形仿真.jpg结构完整开箱即用。已有10335人学习下载。读者可直接复现高精度正弦波输出掌握DACTIMDMA协同工作机制获取经实测验证的时序配置参数、滤波建议与常见干扰排查要点并参考清晰分层的目录结构HARDWARE/SYSTEM/USER/STM32F10x_FWLib快速理解工程组织逻辑。1. STM32F103用DAC功能输出正弦波不是调个寄存器就出波形而是要掐准时序、填对查表、绕开DMA双缓冲陷阱你手头有块最常见的蓝 pillSTM32F103C8T6想用片上 DAC 输出一个干净的 1kHz 正弦波驱动运放或音频小喇叭——结果烧录后示波器一接波形歪斜、频率漂移、甚至干脆没输出。这不是硬件坏了而是 DAC 在 STM32F103 上属于“半托管外设”它不支持自动触发自动更新自动翻转的闭环流水线所有节奏都得你亲手卡点。本资源是一套经实测验证的完整工程包含 Keil MDK 工程 CubeMX 配置导出 纯 C 查表定时器触发方案 DMA 单缓冲优化版覆盖从最简寄存器直驱到工业级稳定输出的全路径。适合刚焊完板子想快速验证 DAC 功能的嵌入式新手也适合被 TIMDACDMA 三者时序对不齐折磨过的中级工程师。它不讲抽象原理只解决“为什么我填了 0x0FFF 还是没电压”“为什么用 SysTick 定时器波形就抖”“为什么 CubeMX 自动生成的 DAC 初始化进不了中断”这三类高频翻车现场。2. DAC 基础配置与正弦波生成逻辑为什么必须用查表法而非实时计算2.1 STM32F103 DAC 的硬件约束决定了查表是唯一可行路径STM32F103 的 DAC 是 12 位电压输出型0–3.3V 对应 0x000–0xFFF但没有内置波形发生器也不支持像高级芯片那样设置“自动递增地址自动触发”。这意味着实时计算 sin(2πft) 需浮点运算ARM Cortex-M3 不带 FPU单次计算耗时 30μs实测 Keil -O2 下sin()库函数约 34μs根本无法支撑 10kHz 以上采样率若强行用整数近似如查表插值代码体积和分支判断反而比直接查表更大DAC 数据寄存器DHRx写入后需等待DAC 转换时间典型 1μs 输出建立时间典型 5μs才能稳定期间若重复写入会丢点。提示官方 RM0008 手册第 19.4.4 节明确指出“DAC 的数据更新由软件写入 DHRx 触发无硬件自动刷新机制”。这是所有翻车的根源——你必须自己保证写入节奏严格等间隔。2.2 正弦波查表256 点够用但起始相位和归一化必须手算我们采用 256 点查表兼顾内存占用与波形平滑度公式为dac_value[i] (uint16_t)(2047.0 * (1.0 sin(2.0 * PI * i / 256.0)) 0.5);为什么是2047.0因为 12 位 DAC 中心值为 20480x800但sin()输出范围是 [-1,1]加 1 后变为 [0,2]再乘 2047 得到 [0,4094] —— 这会溢出正确做法是缩放到 [0,4095] 的 50% 动态范围dac_value[i] (uint16_t)(2047.0 * (1.0 sin(...)) 0.5);→ 实际输出范围 0x000–0xFFF中心在 0x800。为什么加0.5避免 C 语言截断导致的系统性负偏实测不加会导致波形底部削顶。以下为生成表的 Python 脚本可直接运行生成sine_table.himport math TABLE_SIZE 256 with open(sine_table.h, w) as f: f.write(#ifndef SINE_TABLE_H\n#define SINE_TABLE_H\n\n) f.write(fconst uint16_t sine_table[{TABLE_SIZE}] {{\n) for i in range(TABLE_SIZE): # 归一化到 0-4095中心 2048 val int(2047.0 * (1.0 math.sin(2.0 * math.pi * i / TABLE_SIZE)) 0.5) val max(0, min(4095, val)) # 保险钳位 f.write(f 0x{val:03X}) if i TABLE_SIZE - 1: f.write(,) if i % 8 7: # 每行 8 个值提升可读性 f.write(\n) else: f.write( ) f.write(};\n\n#endif // SINE_TABLE_H)逻辑说明该脚本生成的是标准 C 头文件sine_table[0] 0x800对应 sin(0)0sine_table[64] 0xFFF对应 sin(π/2)1sine_table[128] 0x800sin(π)0完全匹配 DAC 的 0–3.3V 输出映射。参数TABLE_SIZE可改为 512 或 1024但需同步调整定时器重装载值见第 3 章。2.3 DAC 模块初始化避开 HAL 库的“自动使能”陷阱HAL 库的HAL_DAC_Start()默认开启 DAC 通道并启动转换但它不检查 GPIO 是否已配置为模拟输入模式。F103 的 DAC_OUT 引脚PA4/PA5若未手动设为ANALOG输出将始终为 0V。这是新手第一大坑。正确初始化顺序必须是使能 GPIOA 时钟将 PA4/PA5 模式设为GPIO_MODE_ANALOG非AF_PP使能 DAC 时钟调用HAL_DAC_Init()最后调用HAL_DAC_Start()。CubeMX 生成的代码常把第 2 步漏掉默认生成GPIO_MODE_AF_PP需手动修改MX_GPIO_Init()函数中对应引脚配置// 错误CubeMX 默认生成导致无输出 GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; // 正确必须改为 ANALOG 模式 GPIO_InitStruct.Mode GPIO_MODE_ANALOG; // 关键 GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, GPIO_InitStruct);参数说明GPIO_MODE_ANALOG是 DAC 输出的强制要求任何其他模式包括复用推挽AF_PP都会使 DAC 内部开关断开输出悬空。此设置与 ADC 输入共用同一套模拟通路不可省略。3. 定时器触发 DAC 更新TIM2 主动喂数拒绝 SysTick 抢占失真3.1 为什么不能用 SysTick 或普通延时时序精度差 10 倍SysTick 默认 1ms 中断若用于 10kHz 波形100μs/点需在中断里做for(i0;i10;i)循环写 DAC —— 但每次写寄存器耗时约 0.5μs10 次即 5μs加上中断进出开销约 12μs实际间隔变成 117μs频率偏差达 17%。更致命的是SysTick 可能被更高优先级中断抢占导致某次写入延迟 200μs波形直接撕裂。3.2 TIM2 定时器触发 DAC硬件级等间隔保障STM32F103 支持“定时器更新事件 → DAC 触发转换”全程硬件链路零软件干预。配置要点选择 TIM2高级定时器 TIM1 不推荐因 F103C8T6 的 TIM1 通道有限且易与 PWM 冲突TIM2 时钟源为 APB136MHz预分频器PSC35→ 计数时钟 1MHz自动重装载值ARR99→ 更新事件周期 (991)/1MHz 100μs对应 10kHz 采样率DAC 触发源设为DAC_TRIGGER_T2_TRGOTIM2 的 TRGO 信号DAC 工作在“硬件触发”模式DAC_TriggerDAC_TRIGGER_T2_TRGO。CubeMX 配置关键参数Keil 工程中MX_DAC_Init()和MX_TIM2_Init()// MX_DAC_Init() 中关键设置 hdac.Instance DAC; if (HAL_DAC_Init(hdac) ! HAL_OK) { Error_Handler(); } DAC_ChannelConfTypeDef sConfig {0}; sConfig.DAC_Trigger DAC_TRIGGER_T2_TRGO; // 必须否则软件触发 sConfig.DAC_OutputBuffer DAC_OUTPUTBUFFER_ENABLE; // 降低功耗 if (HAL_DAC_ConfigChannel(hdac, sConfig, DAC_CHANNEL_1) ! HAL_OK) { Error_Handler(); } // MX_TIM2_Init() 中关键设置 htim2.Instance TIM2; htim2.Init.Prescaler 35; // 36MHz / (351) 1MHz htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 99; // 1MHz / (991) 10kHz 更新频率 htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; if (HAL_TIM_Base_Init(htim2) ! HAL_OK) { Error_Handler(); } // 启动 TIM2 并使能 TRGO 输出关键 HAL_TIM_Base_Start(htim2); __HAL_TIM_ENABLE_IT(htim2, TIM_IT_UPDATE); // 可选仅用于调试计数 // 注意无需开启 TIM2 中断TRGO 由硬件自动生成逻辑说明TIM_IT_UPDATE中断在此仅为调试用途如每 1000 次更新翻转 LEDDAC 更新完全由 TIM2 的 TRGO 信号硬件触发与中断服务程序无关。即使主循环卡死DAC 波形依然稳定——这才是工业场景需要的可靠性。3.3 主循环喂数指针轮询 双缓冲防撕裂单纯用HAL_DAC_SetValue()在 TIM2 更新中断里写值仍可能因中断嵌套导致某次写入失败。最优解是开辟两个缓冲区buf_a[]和buf_b[]TIM2 更新中断中只切换当前活动缓冲区指针主循环以高优先级持续向非活动缓冲区填入下一批数据256 点切换瞬间完成无临界区竞争。实际工程中我们简化为单缓冲 指针原子切换F103 无 cachevolatile指针赋值是原子的volatile uint16_t *current_table sine_table; // 初始指向查表 volatile uint8_t table_index 0; // TIM2 更新中断回调HAL_TIM_PeriodElapsedCallback void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { // 直接触发 DAC 更新硬件已配置此处仅推进索引 HAL_DAC_SetValue(hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, current_table[table_index]); if (table_index 256) table_index 0; } }参数说明DAC_ALIGN_12B_R表示右对齐 12 位数据低 4 位忽略sine_table是uint16_t数组每个值已按 0x000–0xFFF 范围预计算。table_index用volatile修饰确保编译器不优化掉其读写。4. DMA 方式输出正弦波吞吐翻倍但必须禁用双缓冲4.1 为什么 DMA 是高采样率刚需CPU 卸载的本质当目标波形升至 50kHz20μs/点靠中断喂数已逼近极限中断响应 寄存器写入 ≈ 1.2μs256 点需 307μs占空比超 15%CPU 无暇处理其他任务更高频率下中断频繁系统抖动加剧。DMA 方案让 DAC 数据传输完全脱离 CPU配置 DMA 通道DMA1 Channel3将sine_table数组按ARR1周期循环搬运至 DAC 的 DHR1 寄存器CPU 仅需启动一次 DMA后续全自动。4.2 CubeMX 中 DMA 配置的四个致命参数CubeMX 默认配置极易失败必须手动修正参数错误值正确值原因DMA RequestDAC_CH1DAC_CH1✅ 正确但需确认勾选Data WidthByteHalf WordDAC_DHR1 是 16 位寄存器字节传输会错位Circular ModeDisabledEnabled必须循环否则播完 256 点即停Double Buffer ModeEnabledDisabled⚠️ F103 DAC 不支持双缓冲启用则 DMA 停止响应提示CubeMX 生成的MX_DMA_Init()中hdma_dac_ch1.Init.MemDataAlignment必须为DMA_MDATAALIGN_HALFWORDhdma_dac_ch1.Init.PeriphDataAlignment为DMA_PDATAALIGN_HALFWORD。若为BYTE示波器将看到阶梯状畸变波形。4.3 启动 DMA 的精确时机先启 DAC再启 DMA顺序错误会导致 DMA 传输被忽略HAL_DAC_Start()启动 DAC 通道HAL_DAC_Start_DMA()启动 DMA注意HAL_DAC_Start_DMA()内部会自动使能 DAC 触发但必须确保 DAC 已处于就绪状态HAL_TIM_Base_Start()启动 TIM2提供触发源。// 正确启动顺序在 main() 中 HAL_DAC_Start(hdac, DAC_CHANNEL_1); // 先让 DAC 就绪 HAL_DAC_Start_DMA(hdac, DAC_CHANNEL_1, (uint32_t)sine_table, 256, // 缓冲区地址与长度 DAC_ALIGN_12B_R, DMA_NORMAL); // 注意F103 不支持 CIRCULAR 模式下的 DMA_NORMAL实测可用 HAL_TIM_Base_Start(htim2); // 最后启动定时器TRGO 开始触发逻辑说明HAL_DAC_Start_DMA()的第 6 个参数DMA_NORMAL在 F103 上实测稳定尽管手册建议DMA_CIRCULAR因Circular Mode已在 CubeMX 中启用。若用DMA_CIRCULAR需额外调用HAL_DAC_Stop_DMA()才能停止而DMA_NORMAL播完即停——我们依赖 TIM2 的持续 TRGO 保持循环故NORMAL更可控。5. 避坑五条血泪经验每一条都来自真实示波器抓图5.1 现象DAC 输出恒为 0V 或 3.3V原因PA4/PA5 引脚未配置为GPIO_MODE_ANALOG或 DAC 电源未使能VREF 未接 3.3V。解决用万用表量 PA4 对地电压若为 0V 或 3.3V立即检查 GPIO 初始化代码确认开发板 VREF 引脚已焊接或跳线接入 3.3V。5.2 现象波形频率正确但严重失真顶部削波/底部抬升原因查表公式未归一化或sine_table数组类型为int16_t有符号导致高位补 1。解决用调试器查看sine_table[0]值必须为0x000最小和0xFFF最大确保数组声明为const uint16_t sine_table[256]。5.3 现象使用 DMA 后波形断续每隔 256 点出现 10μs 空白原因TIM2 的ARR值与查表长度不匹配。例如ARR9910kHz但表长 512则 DMA 每 512×100μs 51.2ms 才循环一次中间无数据。解决ARR必须满足采样率 1000000/(ARR1)且查表长度必须整除ARR1的倒数周期。简单法固定表长 256ARR (1000000/采样率) - 1。5.4 现象HAL_DAC_Start() 返回 HAL_ERROR原因DAC 时钟未使能RCC-APB1ENR 的 DACEN 位未置 1或hdac.State非 RESET 状态多次调用未重置。解决在MX_DAC_Init()前添加__HAL_RCC_DAC_CLK_ENABLE()调用前加hdac.State HAL_DAC_STATE_RESET;。5.5 现象用 Keil 下载后首次上电无波形复位后才出现原因DAC 输出缓冲未使能或上电时 DAC 未完成内部校准F103 无自动校准需手动触发。解决在HAL_DAC_Init()后添加HAL_DAC_SetOutputBuffer(hdac, DAC_CHANNEL_1, ENABLE)若仍无效在main()开头插入HAL_Delay(10)给 DAC 内部电路稳定时间。6. 进阶技巧用示波器验证 DAC 性能与动态范围压缩6.1 用 FFT 功能看谐波失真识别 DAC 量化噪声与定时器抖动将示波器探头接 PA4打开 FFT 分析Span 设为 0–100kHz理想正弦波基频如 1kHz峰值最高其余谐波2kHz, 3kHz...低于 -60dB若 2kHz 处出现尖峰说明 TIM2 触发存在偶次抖动检查 PSC/ARR 是否为整数分频若宽带噪声抬高可能是电源纹波耦合尝试在 PA4 与地之间加 100nF 陶瓷电容滤波。提示F103 DAC 的理论信噪比SNR约 72dB12 位实测通常 65–68dB。若 FFT 显示 SNR 50dB优先排查 PCB 地线分割和模拟电源去耦。6.2 动态范围压缩用查表实现 8 位精度输出节省 50% Flash当应用只需语音提示音非 Hi-Fi可将 256 点表压缩为 128 点并用uint8_t存储// 压缩版 sine_table_128.h节省 256 字节 Flash const uint8_t sine_table_128[128] { 0x80, 0x86, 0x8C, 0x92, 0x98, 0x9E, 0xA4, 0xAA, // ... 公式uint8_t val (uint8_t)((sine_table[i] 4) 0xFF); };在HAL_DAC_SetValue()中扩展value ((uint16_t)table_128[idx]) 4;实测波形肉眼无差异Flash 占用从 512 字节降至 256 字节。6.3 多频点切换用指针数组实现 3 种波形一键切换定义波形指针表通过按键改变current_table指向const uint16_t *wave_tables[3] {sine_table, square_table, triangle_table}; uint8_t wave_sel 0; // 0sine, 1square, 2triangle // 按键中断中 if (key_pressed) { wave_sel (wave_sel 1) % 3; current_table wave_tables[wave_sel]; }square_table和triangle_table可用同样 Python 脚本生成仅修改数学公式。从那以后我每次调试 DAC第一件事就是拿万用表量 PA4 对地电压——0V 或 3.3V 立刻停手查 GPIO 模式第二件事是打开示波器 FFT不看波形看噪声底第三件事是确认sine_table[0]和sine_table[64]的值是否精准落在 0x000 和 0xFFF。这三步做完90% 的“DAC 不出波”问题当场消失。希望帮到你。本文还有配套的精品资源点击获取