【动手学STM32G4】(1)STM32G431之导入和创建项目
【动手学STM32G4】(2)STM32G431之外部中断
【动手学STM32G4】(3)STM32G431之PWM
【动手学STM32G4】(4)STM32G431之ADC/DAC
【动手学STM32G4】(5)STM32G431之UART 串口通信
【动手学STM32G4】(6)STM32G431之USB 虚拟串口通信
【动手学STM32G4】(7)STM32G431之上位机波形显示
【动手学STM32G4】(8)STM32G431之 DAC进阶
【动手学STM32G4】(8)STM32G431之 DAC进阶
-
- [1. 项目简介](#1. 项目简介)
-
- [1.1 实验内容](#1.1 实验内容)
- [1.2 DAC:模拟电压输出](#1.2 DAC:模拟电压输出)
- [1.3 软硬件准备](#1.3 软硬件准备)
- [2. 实验 1:输出1.65V电压](#2. 实验 1:输出1.65V电压)
-
- [2.1 CubeMX 工程配置](#2.1 CubeMX 工程配置)
- [2.2 STM32CubeIDE 编程与和调试](#2.2 STM32CubeIDE 编程与和调试)
- [3. 实验 2:带占空比控制的方波发生器](#3. 实验 2:带占空比控制的方波发生器)
- [4. 实验 3:滴答定时器 + DAC 生成正弦波](#4. 实验 3:滴答定时器 + DAC 生成正弦波)
-
- [4.1 实时计算输出正弦波](#4.1 实时计算输出正弦波)
- [4.2 查表法输出正弦波](#4.2 查表法输出正弦波)
- [5. 实验4:TIM+DAC 生成正弦波](#5. 实验4:TIM+DAC 生成正弦波)
-
- [5.1 定时器中断+DAC 生成正弦波](#5.1 定时器中断+DAC 生成正弦波)
- [5.2 DMA+TIM+DAC方案](#5.2 DMA+TIM+DAC方案)
- [5. 总结:](#5. 总结:)
1. 项目简介
1.1 实验内容
本系列实验基于STM32G431RB微控制器平台,系统探究了数模转换器(DAC)在嵌入式系统中的三种典型应用模式。实验从基础到高级,逐步深入,旨在帮助学习者掌握DAC模块的核心原理、配置方法及性能优化技巧。
实验按照技术复杂度和性能层级设计为三个阶段:
-
基础层:滴答定时器+DAC方案
采用系统滴答定时器(SysTick)作为时间基准,主循环中轮询检查时间并更新DAC输出,实时计算正弦波数值,软件触发转换。
特点:实现简单,适合初学者理解DAC基本原理
-
进阶层:定时器中断+DAC方案
使用硬件定时器(TIM6)产生精确中断,中断服务程序中计算并更新DAC值,结合正弦波对称性优化,采用半周期查表法。
特点:平衡性能与灵活性,适合大多数应用场景
-
高级层:DMA+TIM+DAC全硬件方案
构建"TIM6触发→DAC请求→DMA传输"硬件链路,预计算完整波形表,DMA自动循环传输,无需 CPU干预生成波形。
特点:专业级性能,适合高要求信号生成应用
1.2 DAC:模拟电压输出
DAC(Digital-to-Analog Converter,数模转换器)用于将 MCU 内部处理得到的数字量重新转换为模拟电压,是实现信号重构、波形输出、控制量生成等场景的重要外设。理解 DAC 的输出范围、分辨率、触发模式与数据更新方式,有助于在实验中实现稳定的模拟信号输出。
DAC 的工作流程可以分为三个阶段:
- 装载(Load):将数字数值写入 DAC 数据寄存器
- 转换(Convert):根据输入数字量生成对应的模拟电压
- 输出(Output):通过 DAC 通道输出到引脚,供外部电路使用
STM32G431 片上集成 12 位 DAC,可提供稳定的模拟信号输出,支持缓冲器模式、触发模式以及多种更新方式。
STM32G4 DAC 支持多种触发源:
- 软件更新(Software Trigger)
- 定时器触发(TIMx TRGO)→ 实现固定频率的波形输出
- 外部触发输入(EXTI)
STM32G431 DAC 提供若干输出通道(如 DAC1_CH1、DAC1_CH2),用户可根据需要选择:
- 单通道输出(Single Output)
- 多通道独立控制(Dual Output)
数模转换器(DAC)是将数字量转换为模拟量的关键接口。STM32G431RB内置12位DAC,其主要特性如下:
- 分辨率:12位,输出有4096( 2 12 2^{12} 212)个离散电平等级
- 参考电压:使用 VDDA(3.3V)作为参考基准
- 输出范围:0V 到 VDDA(0-3.3V)
- 转换公式: V o u t = D i n 4095 ∗ V D D A V_{out} = \frac{D_{in}}{4095}*V_{DDA} Vout=4095Din∗VDDA,其中: D i n D_{in} Din 为12位数字输入值(0-4095), V D D A V_{DDA} VDDA 为参考电压(3.3V)。
1.3 软硬件准备
硬件资源:
-
NUCLEO-G431RB 开发板
内置 STM32G431 具有高速 12 位 ADC、12 位 DAC 以及丰富的模拟外设资源,是本实验的核心平台。
-
USB 数据线(Micro USB → USB-A)
用于开发板供电、程序下载和串口通信。
-
示波器
用于观察 DAC 输出的实际模拟波形,便于与采样数据进行对比。
软件资源:
STM32CubeMX, STM32CubeIDE,VOFA+ 串口调试工具
硬件连接:
- Nucleo-G431RB 开发板通过其USB端口(Micro-USB)连接到PC。
安装 ST-LINK 驱动后,在设备管理器里能看到一个 "ST-LINK Virtual COM Port (COMx)"。
2. 实验 1:输出1.65V电压
实验目的:通过 PA4 引脚(DAC1)输出固定 1.65V 电压。
2.1 CubeMX 工程配置
- 新建工程
启动 STM32CubeMX,点击 "Start New Project" (或Ctrl-N快捷键)新建工程,进入 New Project 界面。
- 选择MCU为 STM32G431RBT6(参考开发板的 MCU 型号选择)。
- 选择开发板为 NUCLEO-G431RB 开发板。
- 点击右上角 "Start Project" 创建项目。

- 基础设置:
选择"引脚配置(Pinout & Configuration)",
(1)选择 "System Core -- SYS" 设置调试器类型,将 Debug 模式设为 "Serial Wire"。
(2)选择 "System Core -- SYS" 设置基础时钟源(Timebase Source),可以选择默认设置 "SysTick"。

(3)选择 "System Core -- RCC" 配置时钟模式,设置高速、低速时钟为外部晶振,将 High Speed Clock (HSE) 、Low Speed Clock (LSE) 都设为 "Crystal/Ceramic Resonator"。

- DAC 配置:使用 OUT1(PA4) 作为模拟输入通道,单通道模拟输出。
(1)从左侧下拉列表中选择 "Analog -- DAC1" ,将 OUT1 设置为 "Connected to external pin only"。工作模式和参数选择默认值即可。

(2)DMA 配置。虽然本实验可以使用 查询(Polling)方式读写 DAC,但考虑到后续连续采样或实时波形输出,可以为 DAC 配置 DMA。

- GPIO 配置
(1)完成 DAC 配置后,Pinout view 视图中的 PA4 引脚就自动配置为 "DAC1_OUT1"。如未自动完成,可以手动设置。
(2)如有需要,可额外配置一些 GPIO 用于 LED 指示或按键触发。例如,将 PA5 配置为 GPIO_OUT(LD2),将 PC13 配置为 GPIO_EXTI13(UserBTN)。

- 时钟与工程配置:在 Clock Configuration 视图进行时钟配置,如下图所示。

-
点击工具栏 "Project Manager" 进入工程配置界面,如下图所示。
(1)在 Project Name 输入项目名称 "STM32G431_ADC01"。
(2)在 Toolchain/IDE 选择 IDE 工具为 "STM32CubeIDE"(也可以根据需要选择其它 IDE 工具 )。
(3)在 "Project Manager" 继续向下拉,"在 MCU and Firmware Package" 栏中,取消选中 "Use latest available version",根据所安装的 G4 固件版本,选择 "STM32Cube FW_G4 V1.6.0";
(4)如果固件包不是安装在默认路径,则要取消选中 "Use Default Firmware Location",通过 Browse 选择固件包的安装路径。 -
生成工程代码
点击右上角 "GENERATE CODE" ,将自动生成带 .ioc 的工程文件 "STM32G431_DAC01.ioc"
加载完毕后,弹出代码生成提示窗口,点击" OPEN PROJECT",进入 STM32CubeIDE。

2.2 STM32CubeIDE 编程与和调试
-
在 STM32CubeIDE 打开代码文件 main.c。
代码生成后,已经自动进入 STM32CubeIDE,并打开创建的 STM32G431_DAC01 项目。在 "Core\Src" 目录中,已经生成了 dac.c 和 main.c 等基础程序。
-
从 "Core\Src" 目录打开程序文件 main.c,如下图所示。

-
修改 main.c 代码。
在Initialize all configured peripherals之后、Infinite loop轮询之前,添加代码如下,使能指定的DAC通道,开始进行数模转换并输出模拟电压。
c
/* USER CODE BEGIN 2 */
HAL_DAC_SetValue(&hdac1,DAC_CHANNEL_1,DAC_ALIGN_12B_R,2048); // DAC configuration with Vout=1/2*Vpsr
HAL_DAC_Start(&hdac1,DAC_CHANNEL_1); // DAC Start
/* USER CODE END 2 */
代码说明:
这段代码先命令 DAC 通道1 准备输出一个中间量程的电压(1/2 满量程),然后将其启动,使其开始持续输出该电压。
(1)&hdac1:指向 DAC 硬件实例(hdac1)的指针,指定操作的是第一个DAC模块(DAC1)。
(2)DAC_CHANNEL_1 :指定使用 DAC1 的通道1。
(3)DAC_ALIGN_12B_R:指定数据在寄存器中的对齐方式为"12位右对齐",因此数字输入范围是 0 到 4095 (2^12 - 1)。
(4)2048:用户设定值,确定最终的输出电压。计算公式为:输出电压 = (设定值 / 4095) * 参考电压。
- 程序编辑、编译与调试
- 点击工具栏中 "Build Debug" 按键对程序代码进行编译。
- 点击工具栏中 "Debug" 按键,将程序下载烧录到目标板 NUCLEO-G431RB 。
- 点击工具栏中 "Resume" 按键 或 F8 快捷键,运行程序。
用万用表或示波器测量 PA4 引脚对地电压,输出稳定在 1.65V左右,符合预期结果。
3. 实验 2:带占空比控制的方波发生器
实现了一个带占空比控制、周期可调的软件定时方波发生器。
- 修改 main.c 代码。
(1)在 main.c 顶部的 "用户变量区" 添加用户变量。
c
/* USER CODE BEGIN PV */
// 方波参数
#define SQUARE_PERIOD 100 // 方波周期 100ms (0.1秒)
#define SQUARE_AMP_VOLT 2.00f // 方波幅值 2.0V
#define SQUARE_DUTY_CYCLE 0.33f // 方波占空比 33%
#define UPDATE_INTERVAL 2 // DAC更新间隔 2ms (保证精度)
// 全局变量
uint32_t time_counter = 0; // 时间计数器(ms)
uint32_t last_tick = 0; // 上次记录的系统滴答值
uint16_t high_duration = 0; // 高电平持续时间(ms)
uint16_t dac_high_value = 0; // 计算出的DAC高电平值
(3)轮询程序 while(1) 的代码如下,在 PA4 引脚 输出方波。
c
/* USER CODE BEGIN 2 */
// 计算DAC高电平对应的数字值 (12位DAC, VDDA=3.3V)
// 公式: DAC值 = (期望电压 / 参考电压) * 4095
dac_high_value = (uint16_t)((SQUARE_AMP_VOLT / 3.3f) * 4095);
// 计算高电平持续时间 (100ms * 0.33 = 33ms)
high_duration = (uint16_t)(SQUARE_PERIOD * SQUARE_DUTY_CYCLE);
// 启动DAC通道1
HAL_DAC_Start(&hdac1, DAC_CHANNEL_1);
// 记录初始时间
last_tick = HAL_GetTick();
/* Infinite loop */
while (1)
{
/* USER CODE BEGIN 3 */
uint32_t current_tick = HAL_GetTick();
// 每2ms执行一次DAC更新(100ms周期需要足够的分辨率)
if (current_tick - last_tick >= UPDATE_INTERVAL)
{
last_tick = current_tick;
time_counter += UPDATE_INTERVAL; // 增加2ms
// 计算当前相位 (0 ~ SQUARE_PERIOD-1),100ms周期
uint32_t phase = time_counter % SQUARE_PERIOD;
// 根据相位决定输出电平
// 相位 < 33ms 时输出高电平(2.0V),否则输出低电平(0V)
uint16_t dac_output = (phase < high_duration) ? dac_high_value : 0;
// 设置DAC输出值 (使用12位右对齐)
HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, dac_output);
// 防止计数器溢出(youcans@qq.com)
// 100ms周期下,time_counter到最大值需要很长时间,此检查可选
if (time_counter > 60000) // 60秒复位一次(可选)
{
time_counter = 0;
}
}
}
程序说明:
(1)5ms更新间隔:每5ms检查并更新一次DAC输出;
(2)相位计算:通过 time_counter % SQUARE_PERIOD 确定在周期内的位置;
(3)电平判断:相位<330ms输出高电平,否则输出低电平;
(4)非阻塞设计:主循环快速执行,可添加其他任务。
- 程序编辑、编译与调试
用示波器测量 PA4 引脚波形,如图所示,生成所需的100ms周期、2.0V幅值、33%占空比方波,结果符合预期。

4. 实验 3:滴答定时器 + DAC 生成正弦波
4.1 实时计算输出正弦波
本实验旨在通过STM32G431RB微控制器的数模转换器(DAC)模块,实现正弦波波形生成功能。
本实验采用实时计算法生成正弦波,数学原理如下:
bash
相位更新:θ[n] = θ[n-1] + 2π × f × Δt
电压计算:V[n] = A × sin(θ[n]) + V_{offset}
数字转换:D[n] = (V[n] / V_{DDA}) × 4095
为确保精确的50Hz频率,系统采用系统滴答定时器实现1ms精度的时序控制:
- 系统滴答中断配置为1ms周期;
- 每1ms触发一次DAC更新;
- 50Hz波形每周期包含20个采样点(20ms/1ms=20)。
程序结构如下:
bash
main.c
├── 头文件包含(main.h, dac.h, gpio.h, math.h)
├── 正弦波参数定义
├── 全局变量声明
├── main()函数
│ ├── 系统初始化(HAL_Init, SystemClock_Config)
│ ├── 外设初始化(GPIO, DAC)
│ ├── DAC启动(HAL_DAC_Start)
│ └── 主循环
│ ├── 非阻塞时间检查(每1ms)
│ ├── 相位更新与归一化
│ ├── 正弦电压计算
│ ├── 电压限幅保护
│ ├── DAC数值转换
│ └── DAC输出设置
└── 系统时钟配置函数
主要程序如下:
c
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "dac.h"
#include "gpio.h"
#include <math.h> // 添加数学库用于sin函数
/* Private variables youcans@qq.com ------------------------------------------*/
/* USER CODE BEGIN PV */
// 正弦波参数
#define PI_F 3.1415926f
#define SINE_FREQ_HZ 50.0f // 正弦波频率 50Hz(原例程是5Hz)
#define SINE_AMPLITUDE_V 1.65f // 正弦波幅值 1.65V
#define SINE_OFFSET_V 1.65f // 偏移量 = 幅值(确保最小值=0V)
#define SAMPLE_INTERVAL 1 // 采样间隔 1ms(对应1000Hz采样率)
// 控制变量
float phase_angle = 0.0f; // 正弦波相位角(弧度)
float dac_set_voltage = 0.0f; // DAC设定电压(V)
uint16_t dac_output = 0; // DAC输出值(0-4095)
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
SystemClock_Config();
/* Initialize all configured peripherals youcans@qq.com */
MX_GPIO_Init();
MX_DAC1_Init();
/* USER CODE BEGIN 2 */
// 启动 DAC通道1
HAL_DAC_Start(&hdac1, DAC_CHANNEL_1);
// 记录初始时间(用于非阻塞延时)
uint32_t last_tick = HAL_GetTick();
/* Infinite loop */
while (1)
{
uint32_t current_tick = HAL_GetTick();
// 非阻塞延时:每1ms执行一次
if (current_tick - last_tick >= SAMPLE_INTERVAL)
{
last_tick = current_tick;
// 1. 更新相位: 相位增量 = 2π * 频率 * 时间间隔(秒)
phase_angle += 2.0f * PI_F * SINE_FREQ_HZ * 0.001f;
// 相位归一化到0-2π范围
if (phase_angle > 2.0f * PI_F)
{
phase_angle -= 2.0f * PI_F;
HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin); // 过零点翻转LED
}
// 2. 计算正弦波电压(关键:修正电压计算公式)
dac_set_voltage = SINE_OFFSET_V + SINE_AMPLITUDE_V * sinf(phase_angle);
// 电压限幅(安全保护)
if (dac_set_voltage > 3.3f) dac_set_voltage = 3.3f;
if (dac_set_voltage < 0.0f) dac_set_voltage = 0.0f;
// 3. 转换为DAC值(使用12位DAC)
dac_output = (uint16_t)(dac_set_voltage * 4095.0f / 3.3f);
// 4. 设置DAC输出(使用12位对齐)
HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, dac_output);
}
}
}
通过示波器观测STM32G431RB的PA4引脚(DAC_OUT1),可观察到频率 50Hz、峰值 3.3V、偏置电压 1.65V 的正弦波波形如下,符合预期要求。

4.2 查表法输出正弦波
本实验学习使用预先计算的正弦表生成模拟波形的方法。
由于实时计算正弦函数需要大量计算资源,本设计采用预计算查表法:预先计算一个完整周期的正弦波采样点,运行时直接查表输出,减少实时计算负担。
利用正弦波的对称特性,只需存储0-180°的采样值,对于 180-360°的值可通过对称性计算得出,节省存储空间。
算法流程如下:
bash
开始
↓
初始化系统时钟和外设
↓
启动DAC,输出初始值1.65V
↓
进入主循环
↓
检查是否到达1ms时间间隔
↓
是 → 更新相位索引phase_index
↓
根据phase_index计算DAC值:
- 如果phase_index < 100: 直接查表
- 如果phase_index ≥ 100: 使用对称性计算
↓
设置DAC输出新值
↓
返回主循环
程序代码如下:
c
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "dac.h"
#include "gpio.h"
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
// 正弦波参数
#define SINE_FREQ_HZ 5.0f // 频率 5Hz
#define HALF_TABLE_SIZE 100 // 半周期表大小(0-180°)
const uint16_t SINE_HALF_TABLE[HALF_TABLE_SIZE] = {
// 第一象限:0°-90° (50点)
2048, 2112, 2176, 2240, 2304, 2368, 2431, 2494, 2557, 2619,
2680, 2741, 2801, 2861, 2919, 2977, 3034, 3090, 3145, 3198,
3251, 3302, 3353, 3402, 3449, 3495, 3540, 3583, 3625, 3665,
3704, 3741, 3776, 3810, 3842, 3872, 3900, 3927, 3951, 3974,
3995, 4014, 4031, 4046, 4059, 4070, 4079, 4086, 4091, 4094,
// 第二象限:90°-180° (50点)
4095, 4094, 4091, 4086, 4079, 4070, 4059, 4046, 4031, 4014,
3995, 3974, 3951, 3927, 3900, 3872, 3842, 3810, 3776, 3741,
3704, 3665, 3625, 3583, 3540, 3495, 3449, 3402, 3353, 3302,
3251, 3198, 3145, 3090, 3034, 2977, 2919, 2861, 2801, 2741,
2680, 2619, 2557, 2494, 2431, 2368, 2304, 2240, 2176, 2112
};
// 控制变量
uint32_t last_tick = 0; // 上次更新时间
uint16_t phase_index = 0; // 相位索引(0-199)
uint16_t dac_output = 2048; // DAC输出值
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
SystemClock_Config();
/* Initialize all configured peripherals youcans@qq.com*/
MX_GPIO_Init();
MX_DAC1_Init();
/* USER CODE BEGIN 2 */
// 启动 DAC通道1
HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, dac_output);
HAL_DAC_Start(&hdac1, DAC_CHANNEL_1);
// 记录初始时间
last_tick = HAL_GetTick();
/* Infinite loop */
while (1)
{
/* USER CODE BEGIN 3 */
uint32_t current_tick = HAL_GetTick();
// 每1ms执行一次DAC更新(5Hz = 200ms周期,200点)
if (current_tick - last_tick >= 1)
{
last_tick = current_tick;
// 更新相位索引(0-199循环)
phase_index += 1;
if (phase_index >= 200) // 200点一个完整周期
{
phase_index = 0;
}
// 根据对称性直接计算DAC值
if (phase_index < 100) // 第一、二象限
{
dac_output = SINE_HALF_TABLE[phase_index];
}
else // 第三、四象限
{
uint16_t symmetric_index = 199 - phase_index;
dac_output = 4095 - SINE_HALF_TABLE[symmetric_index];
}
// 设置DAC输出
HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, dac_output);
}
}
}
通过示波器观测STM32G431RB的PA4引脚(DAC_OUT1),可观察到频率 5Hz、峰值 3.3V、偏置电压 1.65V 的正弦波波形如下,符合预期要求。
这是最基础的软件定时方案,完全依赖CPU轮询系统滴答定时器来控制DAC输出节奏。虽然实现简单直观,但定时精度受限于1ms的系统滴答分辨率,CPU需要持续参与计算和触发,导致资源占用率高且波形频率受限,仅适用于低频、低要求的演示场景,展现了嵌入式系统中软件定时的基本原理与局限。

5. 实验4:TIM+DAC 生成正弦波
5.1 定时器中断+DAC 生成正弦波
实验目的:使用定时器产生定时中断触发 DAC,生成频率可调的正弦波。
本实验基于STM32G431RB微控制器,利用其内置的12位数模转换器(DAC)模块和通用定时器(TIM6),实现了一个高精度、高稳定性的50Hz正弦波发生器。
实验的原理:
bash
定时器配置:
时钟源 → PSC分频 → 计数器 → ARR重载 → 更新事件
↓
触发中断 & 触发DAC转换
↓
DAC启动转换 → 模拟输出
定时器参数的计算:
bash
目标频率 = 50Hz
每周期点数 = 200点
所需中断频率 = 50Hz × 200 = 10kHz
定时器计算:
系统时钟 = 160MHz
PSC = 1599 → 分频后 = 160MHz/1600 = 100kHz
ARR = 9 → 中断频率 = 100kHz/10 = 10kHz
中断周期 = 1/10kHz = 0.1ms
程序架构:
c
main.c
├── 系统初始化
│ ├── 系统时钟配置(160MHz)
│ ├── GPIO初始化(PA4自动配置为DAC)
│ ├── TIM6初始化(PSC=1599, ARR=9)
│ └── DAC初始化(TIM6触发)
├── 主程序
│ ├── 设置DAC初始值(1.65V)
│ ├── 启动DAC通道
│ └── 启动TIM6中断
└── 中断服务
└── HAL_TIM_PeriodElapsedCallback()
├── 计算正弦值(sinf())
├── 转换为电压值
├── 电压限幅保护
├── 转换为DAC值
├── 写入DHR12R1寄存器
└── 更新相位角
CubeMX配置:
- TIM6 配置:
计算预分频器(PSC)
TIM6挂载在APB1总线,时钟频率 = 160MHz。
所需中断频率 = 10kHz。考虑ARR通常设为较小值以获得高分辨率,选择 PSC = 1599,分频后频率 = 160MHz / (1599+1) = 160MHz / 1600 = 100kHz。
计算自动重载值(ARR):
中断频率 = 分频后频率 / (ARR+1)
ARR = (分频后频率 / 中断频率) - 1 = (100kHz / 10kHz) - 1 = 10 - 1 = 9
| 参数 | 设置值 | 说明 |
|---|---|---|
| Clock Source | Internal Cloc k | 使用内部时钟源 |
| Prescaler (PSC) | 1599 | 预分频值 |
| Counter Mode | Up | 向上计数模式 |
| Counter Period (ARR) | 9 | 自动重载值 |
| auto-reload preload | Enable | 启用自动重载预装载 |
| Trigger Event Selection | Update Event | 选择更新事件作为触发输出 |
| NVIC Settings | TIM6 global interrupt:Enable | 选择更新事件作为触发输出 |
注意 NVIC配置 必须启用 TIM6 global interrupt,否则无法进入中断服务程序。

- DAC1 配置:
| 参数 | 设置值 | 说明 |
|---|---|---|
| OUT1 Configuration | 启用 | 使能DAC通道1输出 |
| Mode | External Trigger | 外部触发模式 |
| Trigger | Timer 6 Trigger Out event | 触发源选择TIM6 |
| Wave generation mode | Disable | 禁用内置波形生成 |
| Output Buffer | Disable | 禁用输出缓冲器 |

关键代码如下:
c
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "dac.h"
#include "dma.h"
#include "gpio.h"
#include <math.h>
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
// 正弦波参数 - 固定为50Hz
#define SINE_FREQUENCY 50.0f // 正弦波频率 50Hz(固定)
#define SINE_AMPLITUDE 1.65f // 幅值 1.65V
#define SINE_OFFSET 1.65f // 直流偏置 1.65V
#define SAMPLE_POINTS 200 // 每周期200个点
// 计算得到的参数
#define TIMER_INTERRUPT_FREQ 10000.0f // 定时器中断频率 10kHz
#define PHASE_INCREMENT (2.0f * 3.1415926535f * SINE_FREQUENCY / TIMER_INTERRUPT_FREQ)
// 控制变量
volatile float phase_angle = 0.0f; // 当前相位角
volatile uint16_t dac_value = 2048; // DAC输出值(初始为1.65V)
/* Private function prototypes youcans@qq.com --------------------------------*/
void SystemClock_Config(void);
int main(void)
{
HAL_Init();
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DAC1_Init();
MX_TIM6_Init();
/* USER CODE BEGIN 2 */
dac_value = (uint16_t)(SINE_OFFSET * 4095.0f / 3.3f);
HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, dac_value);
HAL_DAC_Start(&hdac1, DAC_CHANNEL_1); // 启动DAC(使用定时器触发模式)
HAL_TIM_Base_Start_IT(&htim6); // 启动定时器(触发中断和DAC转换)
/* USER CODE END 2 */
/* Infinite loop */
while (1)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 使用PA5作为心跳指示
HAL_Delay(200);
}
}
c
/* USER CODE BEGIN 4 */
/** youcans@qq.com
* @brief 定时器更新中断回调函数
* @note 每0.1ms(10kHz)执行一次,计算并设置下一个DAC值
* 注意:DAC实际转换由定时器触发事件自动启动
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM6)
{
float voltage;
float sine_val;
sine_val = sinf(phase_angle); // 1. 计算当前相位的正弦值
voltage = SINE_OFFSET + SINE_AMPLITUDE * sine_val; // 2. 计算输出电压
if (voltage > 3.3f) voltage = 3.3f; // 3. 电压限幅(安全保护)
if (voltage < 0.0f) voltage = 0.0f;
dac_value = (uint16_t)(voltage * 1240.9090909f); // 4. 转换为12位DAC值
DAC->DHR12R1 = dac_value; // 5. 写入DAC数据保持寄存器
phase_angle += PHASE_INCREMENT; // 6. 更新相位角
if (phase_angle >= 6.28318530718f) // 7. 相位归一化(防止长时间运行溢出)
{
phase_angle -= 6.28318530718f;
}
}
}
/* USER CODE END 4 */
运行结果如下:

定时器硬件自动触发中断,CPU仅在中断服务程序中计算并更新DAC值,平衡了性能与灵活性,支持kHz级别的波形生成,是大多数实际应用的优选方案,体现了硬件外设分担CPU负载的嵌入式设计思想。
- 本实验方法的优点在于:
利用TIM6硬件定时器直接触发DAC转换,实现了真正的硬件级同步,相比软件触发方式具有更高的时序精度和稳定性,完全消除了软件延迟带来的定时误差,能够生成频率精确、波形连续的高质量50Hz正弦波; - 本实验方法的缺点在于:
中断服务程序仍存在执行时间的不确定性,如中断延迟可能影响波形相位的一致性,中断响应被更高优先级中断抢占时可能导致波形输出间隔的微小抖动。
5.2 DMA+TIM+DAC方案
实验目的:通过DMA将数据从指定内存传送给DAC数据寄存器,并由定时器触发DAC转换。
DMA (Direct Memory Access,直接内存访问)是一种允许外设直接与内存进行数据交换,而无需CPU干预的技术。这样可以大大提高数据传输的效率,释放 CPU 资源以执行其他任务。其工作流程如下图所示:
bash
传统中断模式:
CPU → 计算数据 → 写入DAC寄存器 → DAC输出
↑中断触发 ↑耗时操作 ↑软件控制
DMA模式:
CPU → 预计算波形表 → 存入内存 → DMA配置
↓
DMA控制器自动搬运 → DAC输出
↑
定时器硬件触发
在STM32G431RB中,DMA自动传输的正确配置需要建立完整的硬件触发链:
bash
触发链配置:
系统时钟 → TIM6定时器 → TRGO触发信号 → DAC转换请求 → DMA传输请求
↓ ↓ ↓ ↓ ↓
160MHz PSC=1599,ARR=9 更新事件 自动启动转换 自动读取内存数据
↓定时10kHz ↓硬件信号 ↓硬件触发 ↓无需CPU干预
CubeMX配置:
- DMA 配置:
bash
路径:DAC1 → DMA Settings → Add → DMA1 Channel 3
关键参数:
- Direction: Memory To Peripheral(内存到外设)
- Priority: High(高优先级)
- Mode: Circular(循环模式)
- Increment Address: Memory(内存地址递增)
- Data Width: Half Word(16位,匹配DAC数据宽度)

- DAC配置:
bash
OUT1 mode: Connected to external pin only
DAC Out1 Settings:
Mode selected: Normal Mode
Output Buffer: Disable
Trigger: Timer 6 Trigger Out Event
Wave generation mode: Disable
DMA Settings: DAC1_CH1
NVIC Settings: TIM6 global interrupt - Enabled
关键代码如下:
c
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "dac.h"
#include "dma.h"
#include "tim.h"
#include "gpio.h"
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
// 正弦波参数
#define SINE_FREQUENCY 50.0f // 正弦波频率 50Hz
#define UPDATE_RATE 10000.0f // 更新率 10kHz
#define WAVE_POINTS 200 // 完整周期点数
#define HALF_TABLE_SIZE 100 // 半周期表大小
// 预计算的半周期正弦表(0-180°,100点)
const uint16_t SINE_HALF_TABLE[HALF_TABLE_SIZE] = {
// 第一象限:0°-90° (50点)
2048, 2112, 2176, 2240, 2304, 2368, 2431, 2494, 2557, 2619,
2680, 2741, 2801, 2861, 2919, 2977, 3034, 3090, 3145, 3198,
3251, 3302, 3353, 3402, 3449, 3495, 3540, 3583, 3625, 3665,
3704, 3741, 3776, 3810, 3842, 3872, 3900, 3927, 3951, 3974,
3995, 4014, 4031, 4046, 4059, 4070, 4079, 4086, 4091, 4094,
// 第二象限:90°-180° (50点)
4095, 4094, 4091, 4086, 4079, 4070, 4059, 4046, 4031, 4014,
3995, 3974, 3951, 3927, 3900, 3872, 3842, 3810, 3776, 3741,
3704, 3665, 3625, 3583, 3540, 3495, 3449, 3402, 3353, 3302,
3251, 3198, 3145, 3090, 3034, 2977, 2919, 2861, 2801, 2741,
2680, 2619, 2557, 2494, 2431, 2368, 2304, 2240, 2176, 2112
};
// 完整周期波形缓冲区(200点)
uint16_t full_wave_buffer[WAVE_POINTS];
// DMA传输状态标志
volatile uint8_t dma_transfer_complete = 0;
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
void Generate_Full_Wave_Table(void);
/** youcans@qq
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_DAC1_Init();
MX_TIM6_Init();
/* USER CODE BEGIN 2 */
// 建立硬件传输链:TIM6(10kHz) → 触发DAC → DAC请求DMA → DMA传输数据 → DAC输出
// 1. 生成完整周期波形表(利用对称性)
Generate_Full_Wave_Table();
// 2. 设置DAC初始输出值(中间值1.65V)
uint16_t init_value = 2048; // 1.65V对应的DAC值
HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, init_value);
// 3. 启动DAC(必须在DMA传输前启动)
HAL_DAC_Start(&hdac1, DAC_CHANNEL_1);
// 4. 启动DMA传输(循环模式,自动重复)
HAL_DAC_Start_DMA(&hdac1, DAC_CHANNEL_1,
(uint32_t*)full_wave_buffer,
WAVE_POINTS,
DAC_ALIGN_12B_R);
// 5. 启动定时器(触发DAC转换)
HAL_TIM_Base_Start(&htim6); // PSC=1599, ARR=9 → 10kHz
// 6. 设置状态指示(可选)
// HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // LED亮表示运行
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 使用PA5作为心跳指示
HAL_Delay(200);
}
/* USER CODE END 3 */
}
正弦波波形表
c
/* USER CODE BEGIN 4 */
/** youcans@qq
* @brief 生成完整周期波形表(0-360°,200点)
*/
void Generate_Full_Wave_Table(void)
{
uint16_t i;
// 0-180°(直接使用半周期表)
for (i = 0; i < HALF_TABLE_SIZE; i++)
{
full_wave_buffer[i] = SINE_HALF_TABLE[i];
}
// 180-360°(利用对称性生成)
// 对称公式:DAC(θ) = 4095 - DAC(360°-θ)
// 对于索引i (0-99),对应角度180+θ
// 对称点在半周期表中的索引为99-i
for (i = 0; i < HALF_TABLE_SIZE; i++)
{
// full_wave_buffer[100 + i] = 4095 - SINE_HALF_TABLE[99 - i];
uint16_t symmetric_angle_index = 99 - i; // 360°-θ对应的索引
full_wave_buffer[100 + i] = 4095 - SINE_HALF_TABLE[symmetric_angle_index];
}
// 可选:微调周期连续性,确保波形首尾平滑连接
if (full_wave_buffer[WAVE_POINTS - 1] != full_wave_buffer[0])
{
full_wave_buffer[WAVE_POINTS - 1] = full_wave_buffer[0];
}
}
/* USER CODE END 4 */
使用 DAC(数模转换器)配合 DMA(直接存储器访问)输出正弦波,构建了"定时器触发→DAC请求→DMA传输"的完整硬件链路。CPU仅在初始化时预计算波形表和配置硬件,运行时完全解放,实现了零抖动、零CPU占用的专业级波形输出,支持高达100kHz的高质量信号生成。
DMA+TIM+DAC方案 具有以下优势:
- 提高系统性能:降低CPU负载,提高数据传输效率;
- 增强数据传输效率:减少中断次数,提高数据传输的准确性;
- 提高系统灵活性:易于实现动态调整,支持多种数据格式。
5. 总结:
| 性能指标 | 滴答定时器+DAC | 定时器中断+DAC | DMA+TIM+DAC | 改进倍数 |
|---|---|---|---|---|
| 定时精度 | ±1ms | ±0.5μs | ±0.01μs | |
| 频率范围 | 1-100Hz | 1-5kHz | 1-100kHz | |
| CPU占用率 | 80-100% | 3-10% | <0.1% | |
| 波形THD | 3-5% | 0.5-1.5% | <0.3% | |
| 定时抖动 | 高(ms级) | 中(μs级) | 极低(ns级) | |
| 配置复杂度 | 简单 | 中等 | 复杂 | |
| 灵活性 | 高 | 中 | 低 | |
| 实时性 | 差 | 好 | 极好 |
版权声明:
【动手学电机驱动】是 youcans@qq 原创作品,转载必须标注原文链接:(https://blog.csdn.net/youcans/article/details/155774119)
Copyright@youcans 2025
Crated:2026-01