资讯动态

STM32G474+FreeRTOS信号发生器设计:DAC/DMA/CORDIC实战

发布时间:2026/9/16 21:02:34 来源:尧图企业网站定制
简介这是一份基于STM32G474微控制器与FreeRTOS实时操作系统的信号发生器工程源码面向有单片机基础、希望深入学习嵌入式实时系统的开发者和学生重点解决多任务下波形生成、参数配置和实时输出之间的调度问题。压缩包内含139个文件大多数为C源码与H头文件另涵盖CubeMX工程配置.ioc、链接脚本、启动文件与说明文档整体大小约1.05MB结构清晰便于按模块查阅。目前已有150人学习下载。从内容预览可见资源覆盖定时器触发、DAC模拟输出、FreeRTOS任务创建与消息队列传输等多个关键模块这些代码能帮助读者理解STM32G4外设驱动方式以及如何用FreeRTOS将信号生成任务与控制任务解耦从而构建可扩展的信号发生器系统。对于正在设计信号发生器或同类数据采集输出系统的开发者这份源码还能提供完整的外设初始化与RTOS任务划分参考。1. 为什么用 STM32G474 和 FreeRTOS 做信号发生器会让项目变简单手头不止一个项目被信号发生器卡在“波形出得来但频率一上去就抖、界面一刷新波形就断”的怪圈里。MCU 裸机跑时按键扫描、屏幕刷新、波形计算挤在一个中断上下文里稍微加个功能就手忙脚乱换成 STM32G474 加上 FreeRTOS 之后波形生成、人机交互、参数控制各归各的任务调度难点从“怎么把所有事塞进一个 while 循环”变成了“怎么把硬件外设用对”。这个组合能解决的问题很直接G474 自带高精度定时器、双 DAC、CORDIC 协处理器再配合 FreeRTOS 的优先级抢占可以在输出正弦波、方波、三角波的同时保持 UI 响应。比较反直觉的一点是用实时操作系统反而比裸机更容易控制波形输出的确定性因为关键波形生成逻辑由 DMA 和定时器硬件完成CPU 只负责配置和更新参数而 FreeRTOS 负责保证“更新参数这件事不会和其他任务打架”。适合的读者是做过嵌入式开发、但第一次在信号发生器这种对时序敏感的设备上引入 RTOS 的工程师。2. 硬件架构DAC、HRTIM、CORDIC 和 FMAC 如何为波形生成服务2.1 选择 STM32G474 而非通用 MCU 的理由信号发生器的核心是“按固定时间间隔输出一个电压序列”。听起来简单但对 MCU 的要求很具体DAC 位数和速度、定时器分辨率、DMA 通道数、以及能否在产生波形的同时处理其他任务。STM32G474 属于 STM32G4 系列主频 170MHz内含两个 12 位 DAC最快转换时间约 1.5us单看指标不算极致但它的杀手锏是内部集成了 CORDIC 和 FMAC 两个硬件加速器。CORDIC 能直接计算正弦、余弦、正切FMAC 能做滤波和乘加运算这意味着生成高次谐波或做 PWM 调制的实时运算不必占用 Cortex-M4 的 FPU 持续计算CPU 可以去处理 FreeRTOS 调度和 UI。另一个关键是 G474 的 HRTIM高分辨率定时器有 10 皮秒级别的分辨率虽然做信号发生器不常用到这么极端的时间精度但用它作为 DAC 的触发源可以把采样率抖动控制在极低水平。普通电机控制类 MCU 的定时器也能触发 DAC但 HRTIM 的事件对齐和 PWM 互补输出能力在以后想扩展 DDS直接数字频率合成时会少走不少弯路。2.2 从 DAC 到 DMA 的波形链路最常见的波形生成链路是CPU 或 DMA 将一个周期的波形采样点写入 DAC 数据寄存器DAC 在定时器触发下周期性转换输出。G474 的 DAC 支持 DMA 请求因此可以配置一个 DMA 通道把 FLASH 或 SRAM 中的波形查找表按循环模式搬运到 DAC。CPU 只在波形频率需要改变时重新计算查找表或修改定时器分频系数。这条链路的参数配置在 CubeMX 里很直观DAC 选择 Output Buffer 开启外接驱动能力触发源选定时器DMA 模式选 Circular数据宽度选 Half Word因为 DAC 数据寄存器是 12 位。要注意 DMA 搬运方向是 Memory-to-Peripheral优先级可以设成 High避免频率较高时和 FreeRTOS 的 SysTick 产生总线争用。实际项目里如果发现输出波形有周期性毛刺优先检查 DMA 仲裁优先级是否低于其他高频外设。2.3 生成方波和 PWM 的替代路径方波和 PWM 并不一定要走 DACG474 的定时器输出比较通道是更干净的方案。定时器可以直接输出 50% 占空比方波或通过比较寄存器输出可调占空比的 PWM。对信号发生器来说方波通常要求陡峭的边沿和极小的抖动使用定时器通道直接驱动 GPIO 输出比 DAC 再到比较器整形要更稳定。想到经常有人在 FreeRTOS 任务里用 GPIO 翻转模拟方波然后发现频率越高抖动越大就是这个原因。把这几种输出方式分开后FreeRTOS 的压力就小很多正弦波、三角波、任意波由 DMA 和 DAC 负责方波和 PWM 由定时器硬件负责任务层只需修改参数寄存器不需要在中断里做实时波形点计算。3. 在 STM32G474 上把 FreeRTOS 任务分层3.1 用 CubeMX 初始化 FreeRTOS 和时钟树STM32CubeMX 生成的 FreeRTOS 工程可以作为起步模板手动移植也能做但需要自行处理 SysTick 优先级和 PendSV时间成本不划算。首先要确认时钟树G474 最高主频 170MHzCubeMX 中选择外部晶振 HSE锁相环 PLL 倍频到 170MHz。接着在 Middleware 里选择 FreeRTOSCMSIS_V1 和 CMSIS_V2 接口都可以G474 用 V2 能拿到更完整的 API。需要注意一个易错点FreeRTOS 的 SysTick 会占用系统滴答定时器但 G474 的 HAL 库依赖 HAL_IncTick 函数来维护毫秒计数。如果直接把 SysTick 交给 FreeRTOS 接管HAL 的 HAL_Delay 就会失去作用。解决办法是让 FreeRTOS 的 xTaskGetTickCount 和 HAL 共用同一个时基或者在 CubeMX 里把 Timebase Source 改成 TIM6。常见做法是把 Timebase Source 设为 TIM7避免 FreeRTOS 和 HAL 抢同一个中断。3.2 UI 任务与波形生成任务划分信号发生器的业务逻辑可以拆成四个任务UI 任务负责按键扫描、LCD 或 OLED 刷新控制任务负责解析用户设置并更新频率、波形类型波形管理任务负责计算查找表并将新的采样点交给 DMA可选的数据采集任务负责回采输出信号用于校准。这四个任务优先级从高到低分别是波形管理 控制 UI 数据采集。波形管理必须最高因为它直接影响输出连续性控制任务偶尔延迟几十毫秒用户察觉不到。创建任务的示例代码如下osThreadAttr_t waveTask_attr { .name wave_task, .stack_size 128 * 4, .priority osPriorityHigh, }; osThreadAttr_t uiTask_attr { .name ui_task, .stack_size 256 * 4, .priority osPriorityLow, }; void app_main(void) { osThreadNew(wave_task, NULL, waveTask_attr); osThreadNew(ui_task, NULL, uiTask_attr); osKernelStart(); }任务栈大小用128 * 4而不是直接写 512是因为在 Cortex-M4 上栈单位是字128 字等于 512 字节。波形管理任务里通常没有太多局部变量128 字足够UI 任务如果用了 sprintf 或字体渲染库256 字是起步值。优先级用 osPriorityHigh 和 osPriorityLow 拉开差距FreeRTOS 在同等优先级下才会轮询调度这里刻意错开。3.3 用队列和信号量做任务间同步UI 任务修改频率后如果直接调用全局变量让波形管理任务读取会出现读取一半、修改一半的数据不一致问题。更可靠的方式是使用 FreeRTOS 队列。控制任务将“波形类型 频率 幅度 偏移”打包成一个结构体通过 osMessageQueuePut 发送给波形管理任务。因为队列本身是线程安全的双方都不需要加临界区。typedef struct { uint8_t wave_type; uint32_t freq_hz; uint16_t amplitude; int16_t offset; } wave_config_t; void ui_task(void *arg) { wave_config_t cfg { .wave_type WAVE_SINE, .freq_hz 1000, .amplitude 2048, .offset 0 }; while (1) { // 按键处理函数 read_key(); // 仅在设置变化时发送 if (settings_changed) { osMessageQueuePut(wave_queue, cfg, 0, 10); } osDelay(50); } }osMessageQueuePut的最后一个参数是超时时间单位是 tick。这里给 10 tick意味着如果队列满线程最多等 10 个系统节拍。更好的策略是纹理监控告机制如果 UI 任务连续发送队列满了直接丢弃因为只关心最新配置。FreeRTOS 的队列在满时可以选择丢弃旧数据这里为了简单保留了超时阻塞实际项目中建议多配置一文档。3.4 内存预算与堆栈分配G474 的 SRAM 最多 32KB看起来不多但波形查找表、DMA 缓冲区、FreeRTOS 任务栈都要从里面出。一个 1024 点的 16 位正弦表占 2KB两个 DMA 缓冲再占 4KB剩下给任务栈的空间并不富裕。可以用uxTaskGetStackHighWaterMark检查每个任务的实际栈消耗调试阶段打印出来。另一种做法是把波形表放到 FLASHDMA 直接从 FLASH 搬运到 DAC 数据寄存器。注意 FLASH 的访问速度比 SRAM 慢但 G474 的 FLASH 有等待周期如果 DMA 读取 FLASH 导致总线等待波形输出可能出现周期性的缺口。比较稳妥的方式是启动时由 DMA 将 FLASH 中的初始波形表搬进 SRAM然后后续更新只修改 SRAM 里的数据。这样既节省 SRAM 预算又保证了运行期的高速访问。4. 信号发生器的实现查表、DMA、CORDIC4.1 正弦波查找表与采样点数选择正弦波生成最直接的方式是预先计算一个周期的采样点。假设输出频率 $f_{out}$采样率 $f_s$查找表点数 $N$三者关系是 $f_s N \times f_{out}$。比如要输出 1kHz 正弦波采样率 100kHz就需要 100 个点。表越大谐波越低但占用的内存和 DMA 循环周期也会变长。工程上常见做法是固定 1024 点甚至 4096 点这样不同输出频率只需要改变定时器触发频率不用重算整张表。生成查找表的函数可以放在启动阶段void sine_table_init(uint16_t *table, uint32_t points) { for (uint32_t i 0; i points; i) { double angle 2.0 * 3.141592653589793 * (double)i / points; table[i] (uint16_t)((2047.0 * sin(angle)) 2047.5); } }这里把 12 位 DAC 的满量程映射到 0~4095正弦波峰值为 2047中点处于 2048也就是 VREF 的一半。(2047.0 * sin(angle)) 2047.5的最后加上 0.5 是为了做接近最近整数的舍入避免固定偏移导致的直流分量。如果使用 STM32G474 的 CORDIC 加速器这里的sin()调用可以替换为硬件计算但启动阶段一次性生成使用 FPU 和数学库足够。4.2 用定时器触发 DAC 和 DMADAC 输出需要精确的采样时钟使用 TIM6 或 TIM7 作为触发源是常规做法。TIM6 是基本定时器没有外部引脚适合作为内部触发。假设 APB1 定时器时钟为 170MHz要得到 100kHz 采样率自动重装载值 ARR 为 1699170000000 / 100000 - 1 1699。上一小节提到的 1kHz 正弦波、1024 点表正好对应这个 100kHz 采样率。定时器配置代码hdac.Instance DAC1; hdac.Init.Trigger DAC_TRIGGER_T6_TRGO; HAL_DAC_Init(hdac); htim6.Instance TIM6; htim6.Init.Period 1699; htim6.Init.Prescaler 0; htim6.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_DISABLE; HAL_TIM_Base_Init(htim6); HAL_TIM_Base_Start(htim6); HAL_DAC_Start_DMA(hdac, DAC_CHANNEL_1, (uint32_t *)sine_table, 1024, DAC_ALIGN_12B_R);HAL_DAC_Start_DMA启动后DMA 会以循环模式持续向 DAC 灌数据。中断里不需要做任何事DAC 和 DMA 硬件完成全部工作。参数sine_table要确保是 16 位传入因为DAC_ALIGN_12B_R表示右对齐在低 12 位DAC 寄存器是 32 位的但 DMA 搬运的是半字。这里必须注意表类型是uint16_t如果用uint32_tDMA 的宽度和数据指针都要跟着改。4.3 频率控制与 CORDIC 实时计算前面固定 1024 点表只能输出固定的频率组合。要连续调频有两种方案要么改变定时器触发频率要么在每次调频时重算整个查找表。改变定时器ARR的响应最快但会出现每个输出周期点数不是整数的问题造成相位不连续。一个更好理解的方案是固定定时器采样率让软件每隔一段时间更新查找表这样输出频率变化是平滑的。另一个更进阶的方法是 DDS 原理用一个相位累加器代替固定寻址每次触发中断或由 DMA 搬运时按照频率控制字跳地址。但这样每次采样都要做相位到幅度的计算除非用 CORDIC否则 CPU 处理不过来。STM32G474 的 CORDIC 可以直接计算正弦值硬件完成一个正弦大约几十个周期比软件库函数快得多。使用 CORDIC 的函数可以参考相关库配置CORDIC_ConfigTypeDef选择运算功能为CORDIC_FUNCTION_SINE数据大小设为CORDIC_DATA_SIZE_16B。调用前必须先把角度归一化到 [-pi, pi] 范围因为硬件 CORDIC 的输入范围有限这个归一化在 CPU 里做一次除法即可。在查找表方案里CORDIC 适合作为频率更新后重算全表的加速器而不是每次采样都调用。频率设置的实际公式目标频率为 1000Hz采样率 100000Hz每个周期 100 个点。更新查找表时只需将相位步长2 * PI * 1000 / 100000累加 100 次并把结果变成 DAC 码。这个过程由控制任务发起波形管理任务在内存中生成新表然后修改 DMA 目标地址。注意修改 DMA 地址时先停止 DAC 的 DMA 请求再换地址最后重新启动避免从旧表跳到新表的瞬间出现半周期毛刺。4.4 任意波形的扩展信号发生器如果能输出任意波形才真正达到实用标准。在 FreeRTOS 的管理下可以专门分出一个“任意波导入”任务它接收上位机通过串口发送来的采样点数组并将数组合法写入 SRAM 中的任意波缓冲区。缓冲区大小根据内存限制设定在 2048 点左右DMA 循环读取该缓冲区即可。void arbitrary_wave_task(void *arg) { while (1) { uint16_t *buffer get_free_buffer(); int len receive_samples_from_uart(buffer, MAX_SAMPLES); if (len 0) { HAL_DAC_Stop_DMA(hdac, DAC_CHANNEL_1); memcpy(current_buffer, buffer, len * sizeof(uint16_t)); HAL_DAC_Start_DMA(hdac, DAC_CHANNEL_1, (uint32_t *)current_buffer, len, DAC_ALIGN_12B_R); } } }get_free_buffer和current_buffer需要做成双缓冲结构。正在被 DMA 读的缓冲区不能改写另一个缓冲区用来接收新数据交换时机比直接在原表上 memcpy 更安全。双缓冲的切换由 FreeRTOS 队列通知让 UI 任务在显示器上弹出“任意波更新完成”的提示。5. 调试 FreeRTOS 任务与信号质量的验证技巧5.1 用逻辑分析仪检查 DAC 输出抖动DMA 定时器 DAC 的链路在硬件上已经固定了采样节奏但总线竞争和优先级反转会间接影响 DMA 搬运。调试时先用逻辑分析仪测量 DAC 输出电压的相邻上升沿间隔正常情况任意两个采样间隔应严格等于定时器周期。如果发现少数间隔拉长或缩短说明 DMA 请求没有得到及时响应或者有其他外设在总线上占用了太多时钟周期。一个常见坑是启用了 DAC 内部输出缓冲后DAC 驱动大电容负载会导致建立时间不足输出波形出现台阶。排查方法是从 DAC 引脚断开负载直接接示波器高阻输入。另一个坑是 FreeRTOS 的configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY设置过低导致定时器中断无法抢占系统调用但 DAC 触发用的是硬件事件不经过中断基本不受影响。5.2 堆栈高水位与任务运行时间分析FreeRTOS 任务栈溢出是信号发生器项目里最容易出的问题。尤其 UI 任务里如果调用了稍大一点的格式化函数栈消耗会突然超过预估值。调试阶段在每个任务里周期调用UBaseType_t highWaterMark uxTaskGetStackHighWaterMark(NULL);调用该函数时传 NULL 表示查询当前任务返回剩余栈空间的最小值单位不是字节而是字。这个值如果接近 0说明任务曾经非常逼近栈顶需要加大stack_size。发布前可以保留一个除错任务每 5 秒向串口打印所有任务的高水位。除错任务自身栈也要留足够余量否则它先溢出就看不到任何信息了。运行时间分析可以借助 FreeRTOS 的vTaskGetRunTimeStats函数配合一个定时器统计各任务 CPU 占用率。信号发生器中波形管理任务通常只占用 5% 以下 CPU负载大头在 UI 刷新如果波形管理任务占用过高多半是查找表更新逻辑陷入忙等应该检查是否有信号量或队列超时设置过长。5.3 优先级反转对波形连续性的影响优先级反转发生在低优先级任务持有某个资源而高优先级任务等待该资源时。波形管理任务和 UI 任务如果共享同一个串口打印调试信息而串口被低优先级的数据采集任务占着就会发生波形管理任务阻塞。现象是输出波形出现一段固定时间的停止随后又恢复用示波器对比时间点会发现问题正好对应串口收发节奏。解决办法是使用互斥量代替二进制信号量保护串口因为 FreeRTOS 互斥量自带优先级继承机制低优先级任务在等待期间会临时提升到高优先级任务的优先级缩短反转窗口。更彻底的办法是让调试信息通过独立 DMA 环形缓冲发送任何任务只写环形缓冲由 UART 中断空闲时自动发出彻底消除共享资源竞争。5.4 输出校准和增益调整DAC 的零点、满度、增益误差会直接影响信号发生器的精度。出厂前可以做两件最实用的校准一是先用万用表测出 DAC 输出为 0 和 4095 时的实际电压算出修正斜率和偏移二是通过试凑写入补偿值把输出 1kHz 正弦波的幅值误差调到一个可接受范围。在线校准通常放在启动后的独立任务里因为要操作到 DAC 寄存器所以这个任务的优先级要低于波形管理任务避免在波形输出过程中频繁打断硬件操作。校准参数保存到 G4 芯片内部 FLASH 的最后一个扇区写入前先擦除擦除期间总线可能暂停这个操作要放在系统空闲时执行例如用户长时间未按键时。最后的实用技巧是如果希望得到双极性输出可以在 DAC 之后接一个运放减法电路将 0~3.3V 的单极性信号变换为 -1.65V ~ 1.65V然后通过数字电位器调节幅度这种方式比在 MCU 内部计算偏移再输出负向电压要简单得多也省去了负电源轨的额外设计。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价