7.1 ADDA 综合作业实现

ADDA 综合作业实现笔记

这份笔记解决什么问题

这份笔记用于复现一套完整的 ADDA 综合作业系统:

 DAC 输出正弦波 / 方波 / 三角波
 按键控制波形周期 / 频率
 旋钮控制峰峰值
 串口打印至少两个周期波形
 串口查询 AMP / FREQ / TYPE
 串口控制 TYPE / FREQ / AMP
 FREQ 和 AMP 来自 ADC 回采数据
 TYPE 基础版来自 ADC 回采 + FFT / 谐波分析

当前工程:

 D:\GD32F4_demo\FFT_test

参考工程:

 D:\Embedded_Softwave_trellis\西门子嵌入式\scr\GD32\ADDA_FFT\GD32_Xifeng_ADDA

核心思路是两条闭环:

 输出闭环:
 按键 / 串口 / 旋钮
     -> 修改波形、频率、幅值
     -> 重新生成 DAC 波形表
     -> TIM6 触发 DAC DMA
     -> PA4 输出波形
 ​
 回采闭环:
 PA4 -> PA5
     -> TIM3 触发 ADC DMA
     -> 拆出 PA5 回采数据 dac_val_buffer
     -> 计算峰峰值、FFT 频率、波形类型
     -> 串口 GET 命令返回

0. 硬件和串口准备

硬件连接:

 PA4 / DAC_OUT1 -> PA5 / ADC_CHANNEL_5
 PC0 / ADC_CHANNEL_10 -> 旋钮电压
 USART1 -> USB 转串口

串口建议:

 BaudRate = 460800
 WordLength = 8B
 StopBits = 1
 Parity = None
 Mode = TX_RX

波特率最好在 CubeMX 的 USART1 里改成 460800,不要只手改 usart.c,否则重新生成代码会被覆盖。


1. 先确认 ADDA 底座能跑

加 FFT 前必须先确认底座成立:

 TIM6 -> DAC DMA -> PA4 输出
 PA4 -> PA5 短接
 TIM3 -> ADC DMA -> PA5 回采
 串口能打印 {dac}xxxx

验收:

 PA4 有波形
 {dac} 数据有周期性
 改变输出后,PA5 回采跟着变

如果这一步不通,后面 FFT 怎么改都没有意义。


2. 把串口打印改成可控

不要让 ADC DMA 每采满一包就打印,否则串口会拖慢系统。

adc_app.c 中保留:

 #define PRINT_POINTS 200
 uint8_t uart_send_flag = 0;

只在打开开关时打印一包:

 if (uart_send_flag)
 {
     uart_send_flag = 0;
 ​
     for (uint16_t i = 0; i < PRINT_POINTS; i++)
     {
         my_printf(&huart1, "{dac}%lu\r\n", dac_val_buffer[i]);
     }
 }

按键事件里只需要:

 uart_send_flag = 1;

注意:

 FFT 用 1024 点
 串口画图只打印 200 点
 不要把 PRINT_POINTS 改成 1024

3. 模块分工

最终文件职责:

 Core/Src/main.c
     初始化入口。
     调用 ringbuffer、按键、DAC、FFT、ADC、scheduler 初始化。
 ​
 APP/dac_app.c / .h
     管 DAC 输出。
     生成三种波形,设置频率,设置幅值,重启 DAC DMA。
 ​
 APP/adc_app.c / .h
     管 ADC 回采。
     启动 TIM3 + ADC DMA,拆 PC0 和 PA5 数据,保留 dac_val_buffer。
 ​
 APP/usart_app.c / .h
     管串口命令。
     解析 GET / SET,调用 dac_app 和 waveform_analyzer_app。
 ​
 APP/waveform_analyzer_app.c / .h
     管回采分析。
     计算 Vpp、FFT 频率、波形类型。
 ​
 APP/ebtn_app.c / .h
     管按键事件。
     触发打印、切换波形、调节频率。
 ​
 APP/mydefine.h
     公共 include 和 extern 声明。
 ​
 Middlewares/ST/ARM/DSP/
     CMSIS DSP 头文件和库。

4. 拆出 dac_app

dac_app.h 对外提供:

 typedef enum
 {
     WAVEFORM_SINE = 0,
     WAVEFORM_SQUARE,
     WAVEFORM_TRIANGLE
 } dac_waveform_t;
 ​
 void dac_app_init(void);
 void dac_app_set_waveform(dac_waveform_t waveform);
 void dac_app_set_frequency(uint32_t freq_hz);
 void dac_app_set_amplitude(uint16_t amplitude);
 uint32_t dac_app_get_frequency(void);
 dac_waveform_t dac_app_get_waveform(void);
 float dac_app_get_adc_sampling_interval_us(void);

dac_app.c 负责:

 SineWave 波形表
 三种波形生成
 current_waveform
 current_frequency_hz
 current_amplitude
 TIM6 频率设置
 DAC DMA 启停

SineWave 只真正定义一次:

 // dac_app.c
 uint16_t SineWave[SINE_SAMPLES];

其他文件只声明:

 // mydefine.h
 extern uint16_t SineWave[SINE_SAMPLES];

5. 生成三种波形

虽然数组还叫 SineWave,但现在理解成“当前 DAC 波形表”。

统一生成入口:

 static void dac_app_generate_waveform(void)
 {
     switch (current_waveform)
     {
     case WAVEFORM_SINE:
         dac_app_generate_sine_wave(SineWave, SINE_SAMPLES,
                                    current_amplitude, 0.0f);
         break;
 ​
     case WAVEFORM_SQUARE:
         dac_app_generate_square_wave(SineWave, SINE_SAMPLES,
                                      current_amplitude);
         break;
 ​
     case WAVEFORM_TRIANGLE:
         dac_app_generate_triangle_wave(SineWave, SINE_SAMPLES,
                                        current_amplitude);
         break;
 ​
     default:
         dac_app_generate_sine_wave(SineWave, SINE_SAMPLES,
                                    current_amplitude, 0.0f);
         break;
     }
 }

设置波形时:

 保存 current_waveform
 停 DAC DMA
 停 TIM6
 重新生成波形表
 开 TIM6
 开 DAC DMA

验收:

 SET:TYPE:0 -> 正弦波
 SET:TYPE:1 -> 方波
 SET:TYPE:2 -> 三角波

6. 频率 / 周期控制

波形频率由 TIM6 触发 DAC 的速度决定。

公式:

 DAC 更新频率 = 波形频率 * 每周期点数

例如:

 SINE_SAMPLES = 100
 目标波形频率 = 50Hz
 TIM6 触发频率 = 50 * 100 = 5000Hz

代码核心:

 update_freq = freq_hz * SINE_SAMPLES;
 arr = (DAC_TIMER_CLOCK_HZ / update_freq) - 1;

验收:

 SET:FREQ:50
 SET:FREQ:100
 SET:FREQ:200

波形速度应该跟着变。

注意:

 SET:FREQ 是控制输出目标
 GET:FREQ 必须从 ADC 回采 + FFT 算出来
 不能直接返回 current_frequency_hz

7. 幅值控制

要分清三个量:

 DAC 原始值:0 ~ 4095
 Vp:峰值幅度,围绕中点上下摆动多少
 Vpp:峰峰值,最高点和最低点差值

关系:

 Vpp = 2 * Vp

串口设置峰峰值:

 SET:AMP:1000
 SET:AMP:2500

转换到 DAC 原始峰值幅度:

 peak_raw = ((vpp_mv / 2) * 4095UL) / 3300UL;
 dac_app_set_amplitude((uint16_t)peak_raw);

旋钮控制幅值:

 PC0 ADC 值 -> 求平均 -> 映射到安全幅值 -> dac_app_set_amplitude()

旋钮和串口固定幅值用开关区分:

 uint8_t knob_amp_enable = 1;

规则:

 默认:旋钮控制幅值
 SET:AMP:x:串口固定幅值,关闭旋钮覆盖
 SET:AMP:AUTO:恢复旋钮控制

8. ADC 双通道数据拆分

ADC Rank:

 Rank1 = PC0 / 旋钮
 Rank2 = PA5 / DA 回采

DMA 数据交错存储:

 adc_val_buffer[0] -> PC0
 adc_val_buffer[1] -> PA5
 adc_val_buffer[2] -> PC0
 adc_val_buffer[3] -> PA5

拆数据:

 for (uint16_t i = 0; i < BUFFER_SIZE / 2; i++)
 {
     knob_val_buffer[i] = adc_val_buffer[i * 2];
     dac_val_buffer[i] = adc_val_buffer[i * 2 + 1];
 }

一定不要把 i * 2i * 2 + 1 写反。

进入 FFT 后,dac_val_buffer 是最近一包 PA5 回采数据,不能清零:

 // Keep dac_val_buffer as latest PA5 capture for FFT query.
 // memset(dac_val_buffer, 0, sizeof(dac_val_buffer));

如果清零,GET:FREQ 会拿到全 0,返回 0Hz。


9. 串口命令

最终命令:

 GET:AMP
 GET:FREQ
 GET:TYPE
 GET:ALL
 ​
 SET:TYPE:0
 SET:TYPE:1
 SET:TYPE:2
 ​
 SET:FREQ:50
 SET:FREQ:100
 SET:FREQ:200
 ​
 SET:AMP:1000
 SET:AMP:2500
 SET:AMP:AUTO

最终语义:

 GET:AMP  -> 从 dac_val_buffer 算 PA5 回采峰峰值
 GET:FREQ -> 从 dac_val_buffer 做 FFT 测频
 GET:TYPE -> 从 dac_val_buffer 做 FFT / 谐波识别
 GET:ALL  -> 一次返回 AMP / FREQ / TYPE

GET:ALL 核心:

 float freq;
 float mean;
 float rms;
 float vpp;
 ADC_WaveformType waveform;
 ​
 freq = Get_Waveform_Frequency(dac_val_buffer);
 waveform = Get_Waveform_Type(dac_val_buffer);
 vpp = Get_Waveform_Vpp(dac_val_buffer, &mean, &rms);
 ​
 my_printf(&huart1, "AMP=%.0fmV\r\n", vpp * 1000.0f);
 my_printf(&huart1, "FREQ=%.2fHz\r\n", freq);

不要在 GET:FREQ 里直接调用完整 Get_Waveform_Info()
它会额外算相位和谐波,开销大,曾导致查询后系统卡死。

最终拆法:

 GET:AMP  -> Get_Waveform_Vpp()
 GET:FREQ -> Get_Waveform_Frequency()
 GET:TYPE -> Get_Waveform_Type()

10. 接入 FFT

先改 ADC 总缓冲区:

 #define BUFFER_SIZE 2048

原因:

 ADC 是双通道交错采样
 总缓冲区 2048
 拆出 PA5 后是 1024 点
 CMSIS FFT 使用 1024 点

保持:

 #define PRINT_POINTS 200

复制文件:

 参考工程 APP/waveform_analyzer_app.c -> 当前工程 APP/
 参考工程 APP/waveform_analyzer_app.h -> 当前工程 APP/
 参考工程 Middlewares/ST/ARM/DSP -> 当前工程 Middlewares/ST/ARM/DSP

Keil 设置:

 1. APP 组加入 waveform_analyzer_app.c
 2. Include path 加入 ..\Middlewares\ST\ARM\DSP\Inc
 3. 工程加入 ..\Middlewares\ST\ARM\DSP\Lib\arm_cortexM4l_math.lib

正常 Build Output 应该出现:

 compiling waveform_analyzer_app.c...

waveform_analyzer_app.c 顶部需要:

 #include "waveform_analyzer_app.h"
 ​
 #ifndef ARM_MATH_CM4
 #define ARM_MATH_CM4
 #endif
 #include "arm_math.h"
 ​
 #include <float.h>
 ​
 #ifndef PI
 #define PI 3.14159265358979323846f
 #endif

main.c 初始化:

 dac_app_init();
 My_FFT_Init();
 adc_tim_dma_init();
 scheduler_init();

11. 给 FFT 提供采样间隔

FFT 必须知道采样率。

dac_app.h

 float dac_app_get_adc_sampling_interval_us(void);

dac_app.c

 #define ADC_TIMER_CLOCK_HZ 90000000UL
 ​
 float dac_app_get_adc_sampling_interval_us(void)
 {
     float sample_freq_hz;
 ​
     sample_freq_hz = (float)ADC_TIMER_CLOCK_HZ /
                      ((float)(htim3.Init.Prescaler + 1U) *
                       (float)(htim3.Init.Period + 1U));
 ​
     if (sample_freq_hz <= 0.0f)
     {
         return 0.0f;
     }
 ​
     return 1000000.0f / sample_freq_hz;
 }

如果 TIM3 是:

 Prescaler = 180 - 1
 Period    = 100 - 1

采样率约:

 90MHz / 180 / 100 = 5kHz

采样间隔约:

 200us

12. FFT 输入必须去直流

ADC 回采值围绕 2048 上下摆动,也就是带约 1.65V 直流偏置。

如果不去直流,0 号频点会很大,交流波形可能被误判成 DC。

FFT 前先减均值:

 float mean = 0.0f;
 ​
 for (int i = 0; i < FFT_LENGTH; i++)
 {
     mean += (float)adc_val_buffer_f[i];
 }
 mean /= (float)FFT_LENGTH;
 ​
 for (int i = 0; i < FFT_LENGTH; i++)
 {
     FFT_InputBuf[2 * i] =
         ((float)adc_val_buffer_f[i] - mean) / 4096.0f * 3.3f;
     FFT_InputBuf[2 * i + 1] = 0;
 }

至少用在:

 Perform_FFT()
 Analyze_Frequency_And_Type()

现象:

 未去直流:AMP 明明接近 2000mV,FREQ=0Hz,TYPE=DC
 去直流后:50Hz 设定下约 48.83Hz,可识别三种波形

13. 最终验收

先固定幅值:

 SET:AMP:2500

测三种波形:

 SET:TYPE:0
 SET:FREQ:50
 GET:ALL
 ​
 SET:TYPE:1
 GET:ALL
 ​
 SET:TYPE:2
 GET:ALL

预期:

 TYPE 分别为 SINE / SQUARE / TRIANGLE
 FREQ 接近当前设定频率
 AMP 是 ADC 回采峰峰值

测频率变化:

 SET:FREQ:50
 GET:ALL
 ​
 SET:FREQ:100
 GET:ALL
 ​
 SET:FREQ:200
 GET:ALL

测幅值变化:

 SET:AMP:1000
 GET:ALL
 ​
 SET:AMP:2500
 GET:ALL

测串口画图:

 按一次打印键

应该能看到至少两个周期左右的 {dac} 波形。

已实测:

 GET:FREQ 可返回 ADC/FFT 频率,例如 50Hz 设定下约 48.83Hz
 GET:TYPE 可识别三种波形
 GET:AMP 可从 ADC 回采算峰峰值
 GET:ALL 可一次返回 AMP / FREQ / TYPE
 查询后系统不卡死,仍能继续打印波形

14. 最短复现清单

 硬件:
 1. PA4 短接 PA5。
 2. PC0 接旋钮。
 3. 串口助手设置 460800。
 ​
 CubeMX / Keil:
 4. USART1 Baud Rate = 460800。
 5. ADC1 Rank1=PC0,Rank2=PA5。
 6. ADC DMA Normal,TIM3 TRGO。
 7. DAC_OUT1 用 TIM6 TRGO,DAC DMA Circular。
 8. Keil 加 waveform_analyzer_app.c。
 9. Keil 加 DSP include path。
 10. Keil 加 arm_cortexM4l_math.lib。
 ​
 代码:
 11. BUFFER_SIZE = 2048。
 12. PRINT_POINTS = 200。
 13. 拆 ADC:PC0=i*2,PA5=i*2+1。
 14. 不清空 dac_val_buffer。
 15. 实现 dac_app 三种波形、频率、幅值。
 16. 实现 dac_app_get_adc_sampling_interval_us()。
 17. main.c 调 My_FFT_Init()。
 18. FFT 输入去直流。
 19. GET:FREQ 用 Get_Waveform_Frequency()。
 20. GET:TYPE 用 Get_Waveform_Type()。
 21. GET:AMP 用 Get_Waveform_Vpp()。
 22. GET:ALL 返回 AMP / FREQ / TYPE。
 ​
 验收:
 23. 编译 0 error。
 24. 按键打印能画波形。
 25. SET:TYPE:0/1/2 后 GET:ALL 能识别。
 26. SET:FREQ 后 FREQ 跟着变。
 27. SET:AMP 后 AMP 跟着变。

15. 常见坑

 1. PA4 没短接 PA5,没有真实回采数据。
 2. ADC Rank 拆反,旋钮和 PA5 数据混了。
 3. dac_val_buffer 被清零,GET:FREQ 返回 0。
 4. Keil 工程树有文件,但 Build Output 没有 compiling waveform_analyzer_app.c。
 5. waveform_analyzer_app.c / .h 是空文件,My_FFT_Init undefined。
 6. 忘记加 arm_math.h include path。
 7. 忘记加 arm_cortexM4l_math.lib。
 8. 忘记定义 ARM_MATH_CM4。
 9. PI 未定义。
 10. FFT 输入不去直流,误判 DC。
 11. GET:FREQ 调 Get_Waveform_Info(),可能卡死。
 12. 串口助手和工程波特率不一致。
 13. CubeMX 重新生成后,把手改的波特率覆盖回 115200。

16. 复盘结论

本次最终完成:

 DA 输出正弦波 / 方波 / 三角波
 按键控制频率 / 周期
 旋钮控制峰峰值
 串口打印至少两个周期波形
 串口查询 AMP / FREQ / TYPE / ALL
 串口控制 TYPE / FREQ / AMP
 FREQ 从 ADC 回采 + FFT 计算
 AMP 从 ADC 回采计算
 TYPE 从 ADC 回采 + FFT / 谐波识别

保留不足:

 1. GET:TYPE 不是绝对即时,刚切换波形后可能读到上一包 ADC DMA 数据。
 2. waveform_analyzer_app.c 有两个未使用变量 warning,不影响运行。
 3. 波形切换瞬间尖峰暂不优化。
 4. 旋钮自动幅值的实时性取决于 ADC DMA 刷新节奏。

后续优化方向:

 1. GET:TYPE 前触发一次新采样,减少上一包数据影响。
 2. 给 ADC 数据加版本号或时间戳。
 3. 清理 waveform_analyzer_app.c 的 warning。
 4. 优化波形切换衔接,减少尖峰。
 5. 后续如果 OLED 加入,可以把 AMP / FREQ / TYPE 显示到屏幕。