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 * 2 和 i * 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 显示到屏幕。