作为一名嵌入式开发者,我最近完成了一个基于STM32的简易信号发生器项目。这个项目实现了三种基础波形(正弦波、方波、三角波)的生成,支持通过按键切换波形类型、调节输出频率,以及通过旋钮调节输出幅度。整个系统采用STM32CubeMX进行硬件配置,使用HAL库开发,实现了高效、稳定的波形输出。
这个项目特别适合已经掌握STM32基础开发的工程师进阶学习,它涵盖了多个嵌入式开发的核心知识点:DAC输出、ADC采样、定时器触发、DMA传输以及任务调度等。通过这个项目,你可以深入理解数模转换的原理,掌握实时信号生成的实现方法,以及学习如何构建一个高效的非阻塞式任务调度系统。
信号发生器的核心是STM32F4系列单片机,我选择了STM32F407VET6作为主控芯片。这款芯片具有以下关键特性:
DAC(数模转换器)是项目的核心外设,它将数字信号转换为模拟电压输出。STM32的DAC具有以下特点:
在CubeMX中,我们需要完成以下关键配置:
时钟树配置
DAC配置
定时器配置
ADC配置
GPIO配置
UART配置
提示:在配置DAC时,务必启用输出缓冲器,这可以提高驱动能力,但会略微增加功耗。如果对输出精度要求极高,可以考虑禁用缓冲器。
项目采用非阻塞式任务调度架构,这是嵌入式系统中常用的设计模式。调度器的主要特点包括:
调度器的核心数据结构是一个任务数组,每个任务包含三个要素:
c复制typedef struct {
void (*task_func)(void); // 任务函数指针
uint32_t rate_ms; // 执行周期(ms)
uint32_t last_run; // 上次执行时间
} task_t;
调度器的主循环通过比较当前时间与任务的上次执行时间来决定是否执行任务:
c复制void scheduler_run(void) {
for (uint8_t i = 0; i < task_num; i++) {
uint32_t now_time = HAL_GetTick();
if (now_time >= scheduler_task[i].rate_ms + scheduler_task[i].last_run) {
scheduler_task[i].last_run = now_time;
scheduler_task[i].task_func();
}
}
}
这种设计有以下几个优点:
正弦波的生成使用标准数学库中的sinf函数,计算过程如下:
c复制void Generate_Sine(uint16_t* buffer, uint16_t samples, uint16_t amplitude) {
float step = 2.0f * 3.1415926f / (float)samples;
for(uint32_t i = 0; i < samples; i++) {
float sine_value = sinf(i * step);
buffer[i] = (uint16_t)((sine_value * amplitude) + 2047);
}
}
这里有几个关键点需要注意:
方波的生成相对简单,只需在缓冲区中交替填充高电平和低电平:
c复制void Generate_Square(uint16_t* buffer, uint16_t samples, uint16_t amplitude) {
uint16_t high = 2047 + amplitude;
uint16_t low = 2047 - amplitude;
for(uint32_t i = 0; i < samples/2; i++) {
buffer[i] = high;
}
for(uint32_t i = samples/2; i < samples; i++) {
buffer[i] = low;
}
}
三角波的生成需要分段线性计算:
c复制void Generate_Triangle(uint16_t* buffer, uint16_t samples, uint16_t amplitude) {
uint16_t half_samples = samples / 2;
float step = (float)amplitude / half_samples;
// 上升沿
for(uint32_t i = 0; i < half_samples; i++) {
buffer[i] = 2047 - amplitude + (uint16_t)(i * step);
}
// 下降沿
for(uint32_t i = half_samples; i < samples; i++) {
buffer[i] = 2047 + amplitude - (uint16_t)((i - half_samples) * step);
}
}
DAC的输出通过DMA实现,这是确保波形连续稳定的关键。配置步骤如下:
c复制void Update_Waveform(void) {
HAL_DAC_Stop_DMA(&hdac, DAC_CHANNEL_1);
switch (current_wave) {
case WAVE_SINE: Generate_Sine(wave_buffer, SINE_SAMPLES, wave_amplitude); break;
case WAVE_SQUARE: Generate_Square(wave_buffer, SINE_SAMPLES, wave_amplitude); break;
case WAVE_TRIANGLE: Generate_Triangle(wave_buffer, SINE_SAMPLES, wave_amplitude); break;
}
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t *)wave_buffer,
SINE_SAMPLES, DAC_ALIGN_12B_R);
}
DMA配置要点:
项目使用三个按键实现功能控制:
按键处理采用事件驱动方式,通过回调函数实现:
c复制void prv_btn_event(struct ebtn_btn *btn, ebtn_evt_t evt) {
if (evt == EBTN_EVT_ONCLICK) {
switch (btn->key_id) {
case USER_BUTTON_0: // 切换波形
current_wave = (WaveType_t)((current_wave + 1) % 3);
Update_Waveform();
break;
case USER_BUTTON_1: // 减慢频率
if (htim6.Instance->ARR < 65000) {
htim6.Instance->ARR += 100;
}
break;
case USER_BUTTON_2: // 加快频率
if (htim6.Instance->ARR > 150) {
htim6.Instance->ARR -= 100;
}
break;
}
}
}
频率调节原理:
旋钮通过ADC采样实现幅度调节,处理流程如下:
c复制void adc_task(void) {
uint16_t knob_raw = (uint16_t)adc_knob_data[0];
uint16_t target_amp = knob_raw / 2;
// 防抖处理
if (abs(target_amp - wave_amplitude) > 40) {
wave_amplitude = target_amp;
Update_Waveform();
}
}
防抖设计要点:
主程序负责初始化各模块并启动调度器:
c复制int main(void) {
HAL_Init();
SystemClock_Config();
// 外设初始化
MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();
MX_DAC_Init();
MX_TIM6_Init();
// 应用层初始化
app_ebtn_init();
adc_dma_init();
scheduler_init();
// 主循环
while (1) {
scheduler_run();
}
}
初始化顺序很重要:
在开发过程中,我总结了以下调试经验:
使用示波器观察输出波形
串口打印调试信息
常见问题排查
性能优化建议
这个基础项目还有很大的扩展空间:
增加波形类型
增强功能
性能提升
界面改进
在实际开发中,我发现通过DMA传输结合定时器触发的方式可以极大减轻CPU负担,使系统能够同时处理其他任务。这种设计思路可以应用到很多需要实时数据输出的场景中,如音频处理、电机控制等。