STM32 + DAC 正弦波生成方案详解:从入门到高频优化


前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站
人工智能学习网站


基于 STM32G4 + DAC81408 的实际项目经验,全面梳理正弦波生成的各种方案、原理及工程实践。


目录

  1. 背景:一个"跑不动"的正弦波
  2. 核心概念:查表法 vs 实时计算法
  3. 理论基础:奈奎斯特采样定理
  4. DDS 直接数字频率合成
  5. FreeRTOS 下的五种实现方案
  6. 方案对比总览
  7. 代码实现
  8. 常见问题与注意事项
  9. 总结

1. 背景:一个"跑不动"的正弦波

1.1 初始代码

以下是一个典型的 STM32 HAL 库风格的正弦波输出函数:

HAL_StatusTypeDef dac81408_generate_sine_wave(uint8_t ch, float amplitude, float frequency)
{
    float time_elapsed = 0.0;
    float sine_value;

    // ... 设置 DAC 输出范围 ...

    do {
        // 实时计算正弦值
        sine_value = amplitude * sinf(2.0f * PI * frequency * time_elapsed / 1000);

        // SPI 发送给 DAC
        dac81408_set_value(ch, sine_value);
        dac81408_set_value(ch + 1, sine_value);

        time_elapsed++;

        // 延时 1ms → 采样率 = 1000 Hz
        HAL_Delay(1);
    } while (1);
}

1.2 这段代码有什么问题?

问题影响
HAL_Delay(1) 阻塞采样率被锁死在 1000 Hz,且 100% 占用 CPU
sinf() 实时计算每次循环都要算浮点三角函数,在 MCU 上极慢
FreeRTOS 不友好死循环 + 阻塞延时,其他任务永远得不到执行
频率上限极低根据奈奎斯特定理,最高只能输出约 50 Hz 的平滑波形

1.3 波形效果实测

输出频率每周期采样点数波形效果
10 Hz100 点非常平滑
50 Hz20 点平滑
100 Hz10 点略有锯齿
200 Hz5 点明显阶梯
500 Hz2 点奈奎斯特极限,不可用
1000 Hz1 点全为 0,输出平直线

结论:当前方案只适合 50 Hz 以下的低频正弦波,且无法在 FreeRTOS 中使用。


2. 核心概念:查表法 vs 实时计算法

这是生成正弦波数据的两种基本思路,核心区别在于"正弦值从哪里来"。

2.1 实时计算法(Generate Points On-the-Fly)

每次输出时,当场调用 sinf() 计算一个值。

// 每次循环都要算一次 sinf()
sine_value = amplitude * sinf(2.0f * PI * frequency * current_time);

流程图:

每次输出 → sinf() 浮点计算 → 得到正弦值 → SPI 发送给 DAC

特点分析:

维度评价
灵活性✅ 极高:可随时改频率、幅度、相位,完全自由
内存占用✅ 0 字节:不需要存储任何表格
计算速度❌ 极慢:sinf() 在 Cortex-M4 上约 100~200 个 CPU 周期
高频适用性❌ 差:算一次 sinf 的时间可能比 SPI 发送还久
适用场景低频、不频繁输出、需要动态计算

形象比喻: 就像每次要吃饭才去菜市场买菜、洗菜、切菜、炒菜——新鲜,但慢。


2.2 查表法(Lookup Table)

提前算好 N 个正弦值存数组里,输出时直接"查字典"取出来。

// ===== 初始化:只执行一次 =====
#define SINE_TABLE_SIZE 256
static float sine_table[SINE_TABLE_SIZE];

void sine_table_init(void) {
    for (int i = 0; i < SINE_TABLE_SIZE; i++) {
        sine_table[i] = sinf(2.0f * PI * i / SINE_TABLE_SIZE);
    }
}

// ===== 输出时:直接查表,O(1) 操作 =====
uint16_t index = (phase_acc >> 16) & 0xFF;  // 取高 8 位作为索引
sine_value = amplitude * sine_table[index];

流程图:

初始化(只一次)→ 预计算 256 个正弦值 → 存入数组

每次输出 → 查表 index → 取出值 → 乘以幅度 → SPI 发送给 DAC

特点分析:

维度评价
灵活性⚠️ 中等:通过 DDS 相位累加也能灵活控制频率
内存占用⚠️ 256 × 4 = 1 KB(float),可忽略
计算速度✅ 极快:数组下标访问,仅 2~3 个 CPU 周期
高频适用性✅ 极好:采样率可达 200~300 kHz
适用场景高频、连续输出、嵌入式实时系统

形象比喻: 就像提前做好的"预制菜",热一下就能吃,极快。


2.3 查表精度与表大小的关系

查表法本质上是用离散点逼近连续正弦波,表越大精度越高:

表大小内存占用相位分辨率实际效果
64 点256 B5.625°低频还行,高频锯齿明显
128 点512 B2.8125°中等场景够用
256 点1 KB1.40625°推荐:工程最佳平衡点
512 点2 KB0.703°更精细,但收益递减
1024 点4 KB0.352°高精度音频场景
4096 点16 KB0.088°极高精度,一般不需要

为什么 256 点是"最佳平衡点"?

  • 1 KB 内存对 STM32 来说微不足道
  • 1.4° 的相位分辨率足以产生 10 kHz 以内极其平滑的波形
  • 再增大表尺寸,人眼/示波器已经看不出区别(收益递减)

2.4 查表法的进阶:对称性压缩

如果内存极度紧张,可以利用正弦波的对称性:

正弦波一个周期 = 4 个象限,彼此对称

象限 0 (0° ~ 90°):    存 64 个点
象限 1 (90° ~ 180°):   镜像象限 0 的值
象限 2 (180° ~ 270°):  取反象限 0 的值
象限 3 (270° ~ 360°):  取反并镜像象限 0 的值

64 点存储 → 等效 256 点精度,内存减少 75%
// 对称性压缩查表
float sine_lookup_compressed(uint8_t index_8bit) {
    uint8_t quadrant = index_8bit >> 6;       // 高 2 位 = 象限
    uint8_t local_index = index_8bit & 0x3F;  // 低 6 位 = 象限内索引

    switch (quadrant) {
        case 0: return  sine_table_64[local_index];           // 0°~90°
        case 1: return  sine_table_64[63 - local_index];      // 90°~180°
        case 2: return -sine_table_64[local_index];           // 180°~270°
        case 3: return -sine_table_64[63 - local_index];      // 270°~360°
    }
}

3. 理论基础:奈奎斯特采样定理

3.1 定理内容

奈奎斯特-香农采样定理: 要从采样信号中完美重建原始信号,采样频率必须大于信号最高频率的 2 倍

F s a m p l e > 2 × F s i g n a l F_{sample} > 2 \times F_{signal} Fsample>2×Fsignal

3.2 工程中的实际要求

理论极限是 2 倍,但工程中要达到平滑波形,至少需要 10~20 个点/周期

F o u t _ m a x ≈ F s a m p l e 20 F_{out\_max} \approx \frac{F_{sample}}{20} Fout_max20Fsample

示例计算:

采样率 Fsample理论极限 (2 点/周期)工程上限 (10 点/周期)平滑上限 (20 点/周期)
1,000 Hz(当前)500 Hz ❌100 Hz ⚠️50 Hz ✅
50,000 Hz25 kHz ❌5 kHz ⚠️2.5 kHz ✅
200,000 Hz100 kHz ❌20 kHz ⚠️10 kHz ✅
500,000 Hz250 kHz ❌50 kHz ⚠️25 kHz ✅

4. DDS 直接数字频率合成

4.1 什么是 DDS?

DDS(Direct Digital Synthesis) 是一种用数字方式精确控制输出频率的技术。核心思想是用一个相位累加器按固定步长递增,每次溢出时完成一个周期。

4.2 DDS 工作原理

┌─────────────────┐      ┌──────────────────┐      ┌──────────────┐
│  相位累加器      │      │  正弦查找表        │      │  DAC 输出     │
│  32 位寄存器     │─────▶│  256 点 (-1 ~ 1)  │─────▶│  SPI → 电压   │
│  += phase_step  │      │  index = 高 8 位  │      │              │
└─────────────────┘      └──────────────────┘      └──────────────┘

步骤拆解:

  1. 相位累加器:一个 32 位无符号整数,每次循环加 phase_step
  2. 取高 8 位index = (phase_acc >> 16) & 0xFF,作为查表索引
  3. 查表输出value = sine_table[index],得到对应的正弦值
  4. DAC 转换:SPI 发送给 DAC,输出模拟电压

4.3 相位步进公式

p h a s e _ s t e p = F o u t × T A B L E _ S I Z E × 2 16 F s a m p l e phase\_step = \frac{F_{out} \times TABLE\_SIZE \times 2^{16}}{F_{sample}} phase_step=FsampleFout×TABLE_SIZE×216

其中:

  • F o u t F_{out} Fout:目标输出频率(Hz)
  • T A B L E _ S I Z E TABLE\_SIZE TABLE_SIZE:查找表大小(通常 256)
  • 2 16 = 65536 2^{16} = 65536 216=65536:固定点小数精度(16 位小数部分)
  • F s a m p l e F_{sample} Fsample:实际采样率(Hz)

4.4 计算示例

假设 F s a m p l e = 250 , 000 F_{sample} = 250,000 Fsample=250,000 Hz,目标输出 F o u t = 1 , 000 F_{out} = 1,000 Fout=1,000 Hz:

phase_step = 1000 × 256 × 65536 / 250000
           = 16,777,216,000 / 250,000
           ≈ 67,109

每次循环累加 67,109,当 32 位累加器溢出时,正好完成一个正弦波周期。

4.5 为什么用 32 位累加器?

  • 高 16 位:整数部分,表示相位(0~255 对应 0°~360°)
  • 低 16 位:小数部分,提供亚像素级精度

这意味着频率分辨率极高:

Δ F = F s a m p l e T A B L E _ S I Z E × 2 16 = 250 , 000 256 × 65536 ≈ 0.015  Hz \Delta F = \frac{F_{sample}}{TABLE\_SIZE \times 2^{16}} = \frac{250,000}{256 \times 65536} \approx 0.015 \text{ Hz} ΔF=TABLE_SIZE×216Fsample=256×65536250,0000.015 Hz


5. FreeRTOS 下的五种实现方案

原始代码用 while(1) + HAL_Delay(1) 死循环,在 FreeRTOS 中等于自杀——所有其他任务永远得不到 CPU。

以下是五种可行的 FreeRTOS 方案:


方案①:定时器中断 + DDS 查表(推荐 ⭐⭐⭐⭐⭐)

原理: 用硬件定时器以固定采样率触发中断,在 ISR 中执行 DDS 相位累加 + 查表 + SPI 输出。

定时器中断 (如 200 kHz)
  └─→ phase_acc += phase_step
  └─→ index = (phase_acc >> 16) & 0xFF
  └─→ value = amplitude * sine_table[index]
  └─→ SPI 发送
  └─→ 返回,FreeRTOS 任务继续运行
// 定时器中断服务函数
void TIMx_IRQHandler(void) {
    if (__HAL_TIM_GET_FLAG(&htimx, TIM_FLAG_UPDATE)) {
        __HAL_TIM_CLEAR_FLAG(&htimx, TIM_FLAG_UPDATE);

        // DDS 查表
        uint16_t index = (phase_acc >> 16) & 0xFF;
        float value = amplitude * sine_table[index];

        // 输出
        dac81408_set_value(ch, value);
        dac81408_set_value(ch + 1, value);

        phase_acc += phase_step;
    }
}

优点:

  • ✅ CPU 大部分时间空闲,FreeRTOS 任务正常运行
  • ✅ 频率精度极高(DDS 32 位累加器)
  • ✅ 代码相对简单,逻辑清晰
  • ✅ 采样率稳定(硬件定时器保证)

缺点:

  • ⚠️ 中断频率过高(>100 kHz)时 ISR 开销可能影响系统
  • ⚠️ SPI 发送在 ISR 中执行,需要注意 ISR 执行时间

适用场景: 通用推荐方案,适合绝大多数场景。


方案②:定时器 + DMA + 预计算波形(CPU 零占用 ⭐⭐⭐⭐)

原理: 预先计算好一整周期正弦波数据,用定时器触发 DMA 自动搬运到 SPI 数据寄存器,完全不需要 CPU 参与

┌──────────┐   触发    ┌─────┐   ┌──────┐   ┌─────────┐
│ 定时器    │────────▶│ DMA │──▶│ SPI  │──▶│ DAC81408│
│ (采样率)  │         │     │   │ TX   │   │         │
└──────────┘         └─────┘   └──────┘   └─────────┘
                         ▲
                         │ 读取
                  ┌──────┴──────┐
                  │ sine_buffer │  (预计算)
                  │ [N个采样点]  │
                  └─────────────┘
// 预计算一整周期波形
#define WAVE_POINTS 256
static uint16_t wave_buffer[WAVE_POINTS];

void prepare_wave_buffer(float amplitude, float offset) {
    for (int i = 0; i < WAVE_POINTS; i++) {
        float val = offset + amplitude * sinf(2 * PI * i / WAVE_POINTS);
        wave_buffer[i] = voltage_to_dac_code(val);
    }
}

// 配置 DMA 循环模式
HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)wave_buffer, WAVE_POINTS * 2);
// 定时器触发 DMA,自动循环输出,CPU 完全空闲

优点:

  • ✅ CPU 完全空闲,最理想的 FreeRTOS 方案
  • ✅ 采样率可以非常高(受限于 SPI 时钟频率)
  • ✅ 波形稳定,无抖动

缺点:

  • ❌ 只能输出固定频率(改变频率需要重新准备缓冲区)
  • ❌ 频率切换时可能有相位不连续
  • ⚠️ 内存占用较大(N × 2 字节)

适用场景: 固定频率输出、系统负载重、需要省 CPU 的场景。


方案③:FreeRTOS 任务 + vTaskDelayUntil(简单低频 ⭐⭐⭐)

原理: 创建专用高优先级任务,用 vTaskDelayUntil() 精确控制输出节奏。

void sine_wave_task(void *pvParameters) {
    TickType_t xLastWakeTime = xTaskGetTickCount();
    const TickType_t xPeriod = pdMS_TO_TICKS(1);  // 1ms → 1000 Hz 采样率

    while (1) {
        vTaskDelayUntil(&xLastWakeTime, xPeriod);

        // DDS 相位累加
        phase_acc += phase_step;
        uint16_t index = (phase_acc >> 16) & 0xFF;
        float value = amplitude * sine_table[index];

        dac81408_set_value(ch, value);
        dac81408_set_value(ch + 1, value);
    }
}

优点:

  • ✅ 最简单的 FreeRTOS 实现
  • ✅ 任务调度正常,其他任务能运行
  • ✅ 代码直观,易于理解和维护

缺点:

  • ❌ 采样率受限于 FreeRTOS tick(通常 1 kHz),最高输出频率 ~100 Hz
  • ❌ 精度受 tick 精度影响(一般 ±1 tick 抖动)
  • ⚠️ 高优先级任务可能影响其他任务

适用场景: 低频输出(< 100 Hz)、快速原型验证。


方案④:双缓冲 + DMA + 定时器(高频可变频率 ⭐⭐⭐⭐)

原理: 结合方案①和方案②,用两个缓冲区交替工作。DMA 发送缓冲区 A 时,CPU 在后台用 DDS 计算填充缓冲区 B。

时刻 T0:  缓冲区 A ── DMA ──▶ SPI ──▶ DAC    (正在输出)
          缓冲区 B ── CPU 计算填充             (准备下一轮)

时刻 T1:  缓冲区 B ── DMA ──▶ SPI ──▶ DAC    (正在输出)
          缓冲区 A ── CPU 计算填充             (准备下一轮)
#define BUF_SIZE 256
static uint16_t buf_a[BUF_SIZE], buf_b[BUF_SIZE];
static uint8_t active_buf = 0;  // 0=A, 1=B

// DMA 半传输完成中断
void HAL_SPI_TxHalfCpltCallback(SPI_HandleTypeDef *hspi) {
    // 前一半已发送完,填充前一半
    fill_buffer_with_dds(active_buf ? buf_a : buf_b, 0, BUF_SIZE / 2);
}

// DMA 传输完成中断
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
    // 后一半已发送完,填充后一半,切换缓冲区
    fill_buffer_with_dds(active_buf ? buf_a : buf_b, BUF_SIZE / 2, BUF_SIZE / 2);
    active_buf = !active_buf;
}

优点:

  • ✅ 频率连续可变(DDS 实时计算)
  • ✅ CPU 占用极低(只在中断中填充缓冲区)
  • ✅ 输出连续无间断

缺点:

  • ❌ 实现最复杂,需要深入理解 DMA 中断机制
  • ❌ 代码量大,调试困难

适用场景: 高频且需要动态改变频率的高端应用。


方案⑤:实时计算 + 定时器中断(灵活但慢 ⭐⭐)

原理: 在定时器中断中实时调用 sinf() 计算,最大化灵活性。

void TIMx_IRQHandler(void) {
    if (__HAL_TIM_GET_FLAG(&htimx, TIM_FLAG_UPDATE)) {
        __HAL_TIM_CLEAR_FLAG(&htimx, TIM_FLAG_UPDATE);

        current_time += dt;  // dt = 1 / Fsample
        float value = amplitude * sinf(2.0f * PI * frequency * current_time);

        dac81408_set_value(ch, value);
        dac81408_set_value(ch + 1, value);
    }
}

优点:

  • ✅ 最灵活:可随时改变频率、幅度、相位、波形
  • ✅ 不需要查表内存

缺点:

  • sinf() 在 ISR 中执行太慢,限制采样率上限
  • ❌ 浮点运算在 ISR 中会触发 FPU 上下文保存,增加延迟

适用场景: 低频、需要动态改变波形参数的场景。


6. 方案对比总览

方案CPU 占用最高频率频率精度实现难度适合场景
① 定时器中断 + DDS 查表低 (~5%)~50 kHz极高⭐⭐通用推荐
② 定时器 + DMA + 预计算0%~100 kHz⭐⭐⭐固定频率输出
③ FreeRTOS 任务 + vTaskDelayUntil~100 Hz低频简单场景
④ 双缓冲 DMA + DDS极低 (~1%)~100 kHz极高⭐⭐⭐⭐高频可变频率
⑤ 实时计算 + 定时器中断中 (~20%)~10 kHz⭐⭐动态波形参数

决策树

需要输出正弦波
  ├─ 频率 < 100 Hz,简单就行?
  │    └─ 方案③:FreeRTOS 任务 + vTaskDelayUntil
  │
  ├─ 频率固定,追求 CPU 零占用?
  │    └─ 方案②:定时器 + DMA + 预计算
  │
  ├─ 频率需要动态改变,高频输出?
  │    ├─ CPU 负载轻 → 方案①:定时器中断 + DDS 查表
  │    └─ CPU 负载重 → 方案④:双缓冲 DMA + DDS
  │
  └─ 需要动态改变波形(不止正弦波)?
       └─ 方案⑤:实时计算 + 定时器中断

7. 代码实现

7.1 方案① 完整代码(推荐)

以下是在 dac81408.c 中的完整实现:

/* ================================================================
 * 正弦查找表(只初始化一次)
 * ================================================================ */
#define SINE_TABLE_SIZE 256
static float sine_table[SINE_TABLE_SIZE];
static uint8_t sine_table_ready = 0;

static void dac81408_sine_table_init(void)
{
    for (int i = 0; i < SINE_TABLE_SIZE; i++) {
        sine_table[i] = sinf(2.0f * 3.14159265359f * i / SINE_TABLE_SIZE);
    }
    sine_table_ready = 1;
}

/* ================================================================
 * DDS 正弦波参数结构体
 * ================================================================ */
struct dac81408_dds {
    uint8_t  channel;        // DAC 通道
    float    amplitude;      // 幅度
    float    frequency;      // 目标频率
    uint32_t phase_acc;      // 相位累加器(32 位)
    uint32_t phase_step;     // 相位步进
    uint8_t  running;        // 运行标志
};

static struct dac81408_dds dds_ctx = {0};

/* ================================================================
 * 初始化 DDS 正弦波
 * ================================================================ */
HAL_StatusTypeDef dac81408_dds_sine_start(uint8_t ch, float amplitude, float frequency)
{
    HAL_StatusTypeDef ret;
    float max_voltage, min_voltage;
    float offset = 0.0f;

    // 1. 参数校验
    if (ch >= DAC81408_CHANNEL) {
        return HAL_ERROR;
    }

    // 2. 初始化查表(只一次)
    if (!sine_table_ready) {
        dac81408_sine_table_init();
    }

    // 3. 幅度限制
    if (amplitude > (dac81408_priv[ch].vmax - dac81408_priv[ch].vmin) / 2.0f) {
        amplitude = (dac81408_priv[ch].vmax - dac81408_priv[ch].vmin) / 2.0f;
    }

    // 4. 自动选择 DAC 输出范围
    max_voltage = offset + amplitude;
    min_voltage = offset - amplitude;

    if (max_voltage > 20) {
        ret  = dac81408_set_range(ch,     dac81408_range_40v);
        ret |= dac81408_set_range(ch + 1, dac81408_range_40v);
    } else if (max_voltage > 12) {
        ret  = dac81408_set_range(ch,     dac81408_range_20v);
        ret |= dac81408_set_range(ch + 1, dac81408_range_20v);
    } else if (max_voltage > 10.0f || min_voltage < -10.0f) {
        ret  = dac81408_set_range(ch,     dac81408_range_d20v);
        ret |= dac81408_set_range(ch + 1, dac81408_range_d20v);
    } else if (max_voltage > 5.0f || min_voltage < -5.0f) {
        ret  = dac81408_set_range(ch,     dac81408_range_d10v);
        ret |= dac81408_set_range(ch + 1, dac81408_range_d10v);
    } else if (max_voltage > 2.5f || min_voltage < -2.5f) {
        ret  = dac81408_set_range(ch,     dac81408_range_d5v);
        ret |= dac81408_set_range(ch + 1, dac81408_range_d5v);
    } else {
        ret  = dac81408_set_range(ch,     dac81408_range_d2v5);
        ret |= dac81408_set_range(ch + 1, dac81408_range_d2v5);
    }

    if (ret != HAL_OK) {
        return ret;
    }

    // 5. 计算 DDS 相位步进
    //    假设采样率 = 200 kHz(由定时器中断频率决定)
    const float Fsample = 200000.0f;  // 定时器中断频率
    float step = frequency * (float)SINE_TABLE_SIZE * 65536.0f / Fsample;

    // 6. 保存 DDS 上下文
    dds_ctx.channel    = ch;
    dds_ctx.amplitude  = amplitude;
    dds_ctx.frequency  = frequency;
    dds_ctx.phase_acc  = 0;
    dds_ctx.phase_step = (uint32_t)step;
    dds_ctx.running    = 1;

    return HAL_OK;
}

/* ================================================================
 * 定时器中断服务函数中调用此函数
 * 每次中断输出一个采样点
 * ================================================================ */
void dac81408_dds_isr_handler(void)
{
    if (!dds_ctx.running) {
        return;
    }

    // DDS 查表
    uint16_t index = (dds_ctx.phase_acc >> 16) & 0xFF;
    float sine_value = dds_ctx.amplitude * sine_table[index];

    // 输出到两个通道
    dac81408_set_value(dds_ctx.channel,     sine_value);
    dac81408_set_value(dds_ctx.channel + 1, sine_value);

    // 相位累加
    dds_ctx.phase_acc += dds_ctx.phase_step;
}

/* ================================================================
 * 停止正弦波输出
 * ================================================================ */
void dac81408_dds_sine_stop(void)
{
    dds_ctx.running = 0;

    // 输出 0V
    dac81408_set_value(dds_ctx.channel,     0.0f);
    dac81408_set_value(dds_ctx.channel + 1, 0.0f);
}

/* ================================================================
 * 动态改变频率
 * ================================================================ */
void dac81408_dds_set_frequency(float frequency)
{
    const float Fsample = 200000.0f;
    dds_ctx.frequency  = frequency;
    dds_ctx.phase_step = (uint32_t)(frequency * (float)SINE_TABLE_SIZE * 65536.0f / Fsample);
}

7.2 定时器配置示例

/* 配置 TIM2 产生 200 kHz 中断 */
void dac81408_timer_init(void)
{
    TIM_HandleTypeDef htim2;
    __HAL_RCC_TIM2_CLK_ENABLE();

    htim2.Instance = TIM2;
    htim2.Init.Prescaler         = 0;                    // 不分频
    htim2.Init.CounterMode       = TIM_COUNTERMODE_UP;
    htim2.Init.Period            = SystemCoreClock / 200000 - 1;  // 200 kHz
    htim2.Init.ClockDivision     = TIM_CLOCKDIVISION_DIV1;
    htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;

    HAL_TIM_Base_Init(&htim2);

    HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);  // 高优先级
    HAL_NVIC_EnableIRQ(TIM2_IRQn);

    HAL_TIM_Base_Start_IT(&htim2);
}

/* TIM2 中断服务函数 */
void TIM2_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&htim2);
}

/* 定时器中断回调 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2) {
        dac81408_dds_isr_handler();  // 调用 DDS 输出
    }
}

7.3 FreeRTOS 任务中的调用方式

/* 用户任务:启动正弦波输出 */
void user_sine_wave_task(void *pvParameters)
{
    // 启动正弦波:通道 0,幅度 5V,频率 1000 Hz
    dac81408_dds_sine_start(0, 5.0f, 1000.0f);

    // 启动定时器(开始输出)
    dac81408_timer_init();

    // 任务可以继续做其他事情,或者挂起等待
    while (1) {
        // 例如:每 5 秒改变一次频率
        vTaskDelay(pdMS_TO_TICKS(5000));
        dac81408_dds_set_frequency(2000.0f);  // 改为 2 kHz

        vTaskDelay(pdMS_TO_TICKS(5000));
        dac81408_dds_set_frequency(500.0f);   // 改为 500 Hz
    }
}

8. 常见问题与注意事项

8.1 中断优先级问题

DDS 输出在定时器中断中执行,如果 FreeRTOS 使用的中断优先级配置不当,可能导致 HardFault。

规则: FreeRTOS 中,ISR 优先级必须 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY(数值更小 = 优先级更高)。

// 正确配置
#define DDS_TIMER_IRQ_PRIORITY  0   // 最高优先级,不调用 FreeRTOS API
HAL_NVIC_SetPriority(TIM2_IRQn, DDS_TIMER_IRQ_PRIORITY, 0);

注意: DDS ISR 中 不要调用任何 FreeRTOS API(如 xQueueSendxSemaphoreGive 等),因为这个优先级高于 FreeRTOS 管理的范围。

8.2 SPI 在 ISR 中的使用

HAL_SPI_Transmit() 在 ISR 中是轮询模式,如果 SPI 传输失败会一直等待。建议:

// 在 ISR 中使用带超时的 SPI 发送
void dac81408_set_value_isr(uint8_t ch, float value)
{
    uint16_t code = (value - dac81408_priv[ch].vmin) * 65535 /
                    (dac81408_priv[ch].vmax - dac81408_priv[ch].vmin);

    uint8_t buff[3];
    buff[0] = DAC81408_REG_DAC0 + ch;
    buff[1] = code >> 8;
    buff[2] = code & 0xff;

    dac81408CsCtrl(0);
    // 使用短超时,避免 ISR 卡死
    HAL_SPI_Transmit(dacSpi, buff, 3, 1);  // 1ms 超时
    dac81408CsCtrl(1);
}

8.3 频率上限检查

建议在启动前检查频率是否合理:

#define MIN_POINTS_PER_CYCLE  20   // 每周期最少 20 个点

if (frequency > Fsample / MIN_POINTS_PER_CYCLE) {
    // 频率过高,波形会明显失真
    frequency = Fsample / MIN_POINTS_PER_CYCLE;
    // 或者返回错误
}

8.4 采样率自校准

不同硬件、不同 SPI 时钟下,实际采样率不同。可以用 DWT(数据观察点)硬件周期计数器测量:

// 启用 DWT
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

// 测量 1000 次 SPI 输出的时间
uint32_t t0 = DWT->CYCCNT;
for (int i = 0; i < 1000; i++) {
    dac81408_set_value(ch, 0.0f);
    dac81408_set_value(ch + 1, 0.0f);
}
uint32_t t1 = DWT->CYCCNT;

// 计算实际采样率
float Fsample = 1000.0f * SystemCoreClock / (t1 - t0);

8.5 多通道同步输出

DAC81408 支持 8 通道,如需多通道输出不同频率/相位的正弦波:

struct dac81408_dds dds_ctx[DAC81408_CHANNEL / 2];  // 每对通道一个 DDS

// 在 ISR 中遍历所有活跃的 DDS 通道
for (int i = 0; i < DAC81408_CHANNEL / 2; i++) {
    if (dds_ctx[i].running) {
        uint16_t index = (dds_ctx[i].phase_acc >> 16) & 0xFF;
        float value = dds_ctx[i].amplitude * sine_table[index];
        dac81408_set_value(dds_ctx[i].channel,     value);
        dac81408_set_value(dds_ctx[i].channel + 1, value);
        dds_ctx[i].phase_acc += dds_ctx[i].phase_step;
    }
}

9. 总结

核心要点

  1. 查表法 vs 实时计算: 查表法用空间换时间,是嵌入式高频正弦波的首选方案。256 点查表是工程最佳平衡点。

  2. DDS 相位累加: 用 32 位累加器实现亚像素级频率精度,比简单的 sin(2πft) 循环精确得多。

  3. FreeRTOS 兼容: 死循环 + 延时 = 阻塞所有任务。必须用定时器中断DMA来驱动输出。

  4. 方案选择:

    • 通用场景 → 方案①:定时器中断 + DDS 查表
    • 固定频率 → 方案②:定时器 + DMA + 预计算
    • 简单低频 → 方案③:FreeRTOS 任务 + vTaskDelayUntil
    • 高频可变 → 方案④:双缓冲 DMA + DDS

性能提升

指标原始方案优化后(方案①)提升
采样率1 kHz200 kHz200×
最高平滑频率50 Hz10 kHz200×
CPU 占用100%~5%20× 降低
FreeRTOS 兼容❌ 阻塞✅ 正常

本文基于 STM32G4 + DAC81408 实际项目编写,代码经过验证。如有疑问欢迎交流讨论。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐