一、硬件设计
信号源的硬件设计理念很简单:通过STM32的两个DAC,搭配TLV2374构成的差分放大器,就可以实现±5V范围内,任意偏置、任意波形的生成。

我们简要看一下硬件设计,标签DAC是从STM32输出的波形,比如说正弦波、三角波等等;DAC_OFFSET则是一个直流电压,用来控制波形的偏置。
STM32的DAC,如果要轨到轨输出,就不能开启内部buffer(否则输出只能到3.05V左右),因此DAC和DAC_OFFSET会先经过U55.1和U55.2构成的电压跟随器,以此提升带载能力,避免干扰。
U55.4的功能很简单,就是一个差分放大器,它的作用就是 用同向端电压 – 反向端电压,将这个差值放大3倍再输出。理论上,最大可以实现(3.3 – 0)*3 = 9.9V的输出,但受限于运放的±5V的电源,实际的输出范围被局限在了±5V。我们可以很简单的得到最终输出与DAC、DAC_OFFSET电压的关系:
二、软件设计
软件层面基于STM32 HAL库开发,核心思路是“定时器触发DAC DMA输出”:通过定时器TRGO信号触发DAC以固定频率输出预计算的波形数据(存储在DMA缓冲区),同时利用双DAC分工实现“波形输出(DAC1)+ 偏置调节(DAC2)”,最终通过硬件差分放大实现±5V波形输出。整体架构采用“通用初始化 + 个性化波形生成”的设计,兼顾代码复用性和扩展性。
2.1 核心参数定义(wave_generator.h)
首先在头文件中定义了信号发生器的关键配置宏,这些参数是软件与硬件匹配的核心,也是波形计算的基础:
#define MAX_SAMPLE 256 // 最大采样点数 (2^7),RAM占用 256*2=512Bytes
#define DAC_MAX 4095 // 12位DAC最大值 (2^12 - 1)
#define VREF 3.3f // DAC参考电压 (V)
#define MAX_AMPLITUDE 4.95f // 最大输出振幅 (V),匹配运放±5V供电
#define MIN_AMPLITUDE 0.0f // 最小输出振幅 (V)
#define MAX_BIAS 4.95f // 最大输出偏置 (V)
#define MIN_BIAS (-4.95f)// 最小输出偏置 (V)
#define TRIG_MAX_FREQ 1000000 // DAC最大触发频率 (Hz)
#define TIM_TRGO_HANDLE htim7 // 触发DAC的定时器句柄(TIM7 TRGO)
#define TIM_FREQ 72000000.0f // 定时器时钟频率 (Hz)
MAX_SAMPLE:限定DMA缓冲区最大长度为256点(2的幂次),兼顾波形精度和RAM占用;DAC_MAX:12位DAC的满量程码值(0~4095),对应硬件3.3V参考电压;TRIG_MAX_FREQ:限制DAC触发频率不超过1MHz,避免超出STM32 DAC的硬件性能;- 振幅/偏置范围:匹配硬件差分放大后的±5V输出范围,软件层面提前做边界限制。
2.2 通用初始化模块(WaveGen_CommonInit)
所有波形生成前都需要执行通用初始化,该函数封装了“参数校验→采样点数计算→定时器配置→偏置DAC配置”的核心逻辑,避免重复代码:
2.2.1 入参合法性检查
首先校验频率(1~50000Hz)、振幅、偏置是否在合法范围,非法则直接返回HAL_ERROR:
if (freq <= 0 || freq > 50000 ||
amplitude < MIN_AMPLITUDE || amplitude > MAX_AMPLITUDE ||
bias < MIN_BIAS || bias > MAX_BIAS) {
return HAL_ERROR;
}
2.2.2 采样点数(N)计算
采样点数N决定波形的分辨率,需满足 N × freq ≤ TRIG_MAX_FREQ(避免触发频率超限),且N为2的幂次(从256向下查找):
for (int k = (int)log2(MAX_SAMPLE); k >= 4; k--) { // k从7(256)到4(16)
uint16_t candidate = 1 << k; // 2^k
if (candidate * freq <= TRIG_MAX_FREQ) {
*N = candidate;
break;
}
}
2.2.3 定时器参数配置(PSC/ARR)
定时器用于生成DAC的触发信号,核心是计算分频系数(PSC)和重装载值(ARR),满足:触发频率 = TIM_FREQ / [(PSC+1) × (ARR+1)] = N × freq
为适配16位定时器的寄存器范围(0~65535),通过find_best_divisor函数将总分频值K分解为两个≤65535的因子:
uint32_t K = (uint32_t)roundf(TIM_FREQ / f_trig);
if (K <= 65536) {
PSC = 0;
ARR = (uint16_t)(K - 1);
} else {
uint32_t a = find_best_divisor(K); // 找最接近平方根的因子
PSC = (uint16_t)(a - 1);
ARR = (uint16_t)((K / a) - 1);
}
// 配置并复位定时器
HAL_TIM_Base_Stop(&TIM_TRGO_HANDLE);
TIM_TRGO_HANDLE.Instance->PSC = PSC;
TIM_TRGO_HANDLE.Instance->ARR = ARR;
TIM_TRGO_HANDLE.Instance->CNT = 0;
2.2.4 偏置DAC2配置
DAC2用于输出偏置电压Voffset,结合硬件放大倍数(DDS_Factor=3)和校准值,计算并设置DAC2的码值:
// 偏置电压转DAC码值(带偏移校准)
float dac2_val = 2047.5f - (bias * DAC_MAX) / (UserParam.DDS_Factor * VREF) - (float)UserParam.DDS_Original;
dac2_val = (dac2_val < 0) ? 0 : (dac2_val > DAC_MAX) ? DAC_MAX : dac2_val;
HAL_DAC_SetValue(&hdac, DAC_CHANNEL_2, DAC_ALIGN_12B_R, (uint16_t)roundf(dac2_val));
HAL_DAC_Start(&hdac, DAC_CHANNEL_2);
2.3 波形生成核心逻辑
所有波形生成函数遵循统一接口 HAL_StatusTypeDef WaveGen_XXX(float freq, float amplitude, float bias, float duty_cycle),仅需在通用初始化后填充DMA缓冲区,再启动DAC DMA和定时器即可。
2.3.1 基础波形实现
(1)正弦波(WaveGen_Sin)
通过三角函数计算每个采样点的角度,将正弦值映射到DAC码值范围(中心值2047.5,对应1.65V):
for (uint16_t i = 0; i < N; i++) {
float theta = 2 * (float)M_PI * i / N; // 0~2π角度
float val = 2047.5f + amp_code * sinf(theta); // 映射到DAC码值
dac1_buffer[i] = (uint16_t)roundf(val);
// 边界限制
dac1_buffer[i] = (dac1_buffer[i] > DAC_MAX) ? DAC_MAX : (dac1_buffer[i] < 0) ? 0 : dac1_buffer[i];
}
(2)方波(WaveGen_Square)
通过占空比划分高低电平采样点,分别设置固定DAC码值:
uint16_t high_samples = (uint16_t)roundf(N * duty_cycle); // 高电平点数
uint16_t high_level = (uint16_t)roundf(2047.5f + amp_code); // 高电平码值
uint16_t low_level = (uint16_t)roundf(2047.5f - amp_code); // 低电平码值
// 填充缓冲区
for (uint16_t i = 0; i < N; i++) {
dac1_buffer[i] = (i < high_samples) ? high_level : low_level;
}
(3)锯齿波/三角波(WaveGen_Sawtooth)
通过占空比设置升降沿分界点,分别计算线性上升/下降的DAC码值(占空比0.5时为三角波):
uint16_t breakpoint = (uint16_t)roundf(N * duty_cycle);
float total_amp_span = 2 * amp_code; // 总电压跨度
for (uint16_t i = 0; i < N; i++) {
float val;
if (i < breakpoint) {
// 上升沿:从2047.5-amp_code线性到2047.5+amp_code
val = 2047.5f - amp_code + ((float)i / (breakpoint - 1)) * total_amp_span;
} else {
// 下降沿:从2047.5+amp_code线性到2047.5-amp_code
val = 2047.5f + amp_code - (((float)(i - breakpoint)) / (N - breakpoint - 1)) * total_amp_span;
}
dac1_buffer[i] = (uint16_t)roundf(val);
}
2.3.2 特殊波形实现
(1)噪声(WaveGen_Noise)
基于线性同余发生器(LCG)生成伪随机数,映射到指定振幅范围:
// LCG伪随机数生成
#define LCG_A 1664525
#define LCG_C 1013904223
#define LCG_M 0xFFFFFFFFU
static float rand_float(void) {
lcg_seed = (LCG_A * lcg_seed + LCG_C) % LCG_M;
return (float)lcg_seed / (float)LCG_M;
}
// 填充随机码值
float noise_min = 2047.5f - amp_code;
float noise_max = 2047.5f + amp_code;
for (uint16_t i = 0; i < N; i++) {
float rand_val = rand_float();
float dac_val = noise_min + rand_val * (noise_max - noise_min);
dac1_buffer[i] = (uint16_t)roundf(dac_val);
}
(2)直流信号(WaveGen_DC)
忽略频率/振幅参数,将DMA缓冲区填充为固定中点码值(2048,对应1.65V):
uint16_t dc_code = (uint16_t)roundf(2047.5f);
for (uint16_t i = 0; i < N; i++) {
dac1_buffer[i] = dc_code;
}
(3)指数波形/整流波形/Sinc/Lorenz
这类波形基于特定数学公式实现(如指数上升exp(t/tau)、全波整流fabsf(sinf(theta))、Sinc函数sin(πx)/(πx)),核心逻辑仍是“数学公式计算→DAC码值映射→缓冲区填充”,与基础波形一致。
2.4 DMA与定时器联动
所有波形最终通过DAC DMA输出,避免CPU持续干预:
// 启动DAC1 DMA输出(缓冲区、点数、12位右对齐)
if (HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)dac1_buffer, N, DAC_ALIGN_12B_R) != HAL_OK)
return HAL_ERROR;
// 启动定时器,触发DAC输出
if (HAL_TIM_Base_Start(&TIM_TRGO_HANDLE) != HAL_OK) {
HAL_DAC_Stop_DMA(&hdac, DAC_CHANNEL_1); // 启动失败则停止DMA
return HAL_ERROR;
}
- DMA:将预计算的波形数据批量传输到DAC数据寄存器,无需CPU逐点写入;
- 定时器TRGO:TIM7的更新事件作为触发源,控制DAC以固定频率输出每个采样点,最终实现连续波形。
2.5 停止与资源释放
提供统一的停止函数,释放定时器、DAC、DMA资源:
void WaveGen_Stop(void) {
HAL_TIM_Base_Stop(&TIM_TRGO_HANDLE); // 停止定时器
HAL_DAC_Stop_DMA(&hdac, DAC_CHANNEL_1); // 停止DAC1 DMA
HAL_DAC_Stop(&hdac, DAC_CHANNEL_1); // 停止DAC1
HAL_DAC_Stop(&hdac, DAC_CHANNEL_2); // 停止DAC2(偏置)
}
2.6 接口设计亮点
所有波形函数采用统一参数接口:(freq, amplitude, bias, duty_cycle),其中:
freq:波形频率(Hz),仅噪声/直流信号忽略;amplitude:振幅(V),对应硬件放大后的输出幅度;bias:直流偏置(V),通过DAC2实现;duty_cycle:占空比(0~1),仅方波/锯齿波生效,其他波形保留(便于函数指针统一调用)。
这种设计让上层代码可通过函数指针数组快速切换波形,例如:
// 波形函数指针类型
typedef HAL_StatusTypeDef (*WaveGen_Func)(float, float, float, float);
// 波形函数数组
WaveGen_Func wave_funcs[] = {
WaveGen_Sin, WaveGen_Square, WaveGen_Sawtooth, WaveGen_Noise
};
// 调用示例:生成1kHz正弦波,2V振幅,0V偏置
wave_funcs[0](1000.0f, 2.0f, 0.0f, 0.5f);
总结
软件设计的核心是“硬件参数抽象化 + 通用逻辑复用 + 个性化波形生成”:通过通用初始化封装定时器、DAC的配置逻辑,通过统一接口降低波形切换成本,最终结合DMA和定时器实现“无CPU干预”的连续波形输出,兼顾性能和扩展性。目前已支持正弦波、方波、锯齿波、噪声、指数波等13种波形,且可通过简单扩展公式实现更多自定义波形。
偷懒用一下AI总结软件部分,效果还不错,(●’◡’●)



