
简介面向嵌入式开发者与电子爱好者本资源聚焦STM32F1系列驱动AD9834数字波形发生器的完整实现覆盖正弦波、三角波、方波的频率设置及触摸屏交互控制适合学习STM32 SPI通信、DDS芯片应用或波形发生器设计的读者。压缩包以RAR格式打包大小约4.49MB便于快速下载与解压。目前已有1878人浏览学习具备一定参考价值。资源从STM32与AD9834的SPI接口通信入手讲解频率寄存器配置、波形整形电路原理以及触摸屏事件解析流程并涉及电源稳定性、抗干扰设计和信号完整性等实用注意事项开发者可借此理解固件初始化、中断处理与HAL/LL库调用方式为基于DDS的可调频率信号源项目提供直接借鉴。1. 项目整体设计方案与核心思路做信号发生器这个项目最开始我其实纠结过一阵是用纯模拟电路搭还是用DDS芯片解决。后来看了下需求——要同时输出正弦波、三角波、方波而且频率要能连续可调模拟方案光是换挡、调频就要搞一堆电位器和继电器稳定性还一般。最后决定走数字方案用STM32F1作为主控外挂AD9834做波形生成再用触摸屏做交互界面。这套组合物料成本不高但能实现的频率覆盖范围和波形质量已经可以满足大部分电子制作、音频测试、传感器激励方面的应用场景。选择STM32F1的核心原因很简单它太成熟了。网上的参考工程、库函数教程、例程代码一抓一大把碰到问题基本都能搜到答案。我用的是一块STM32F103RCT6的核心板72MHz主频有3个SPI外设还有FSMC接口如果后续想换大屏可以用FSMC方式驱动资源完全够用。触摸屏我选的是常见的3.5寸TFT电阻触摸屏模块SPI接口ILI9481主控芯片——注意不同屏的驱动IC不一样代码里的初始化寄存器序列差别很大后续换屏时一定先确认驱动IC型号。整个系统的信号流向是这样的STM32F1通过SPI接口向AD9834写入频率字、相位字和控制字AD9834内部的DDS核心根据频率字累加相位并查表输出对应的数字波形再经过内置10位DAC转为模拟信号从IOUT引脚输出。方波信号走的是另一条路径——内部比较器把DAC输出的正弦波整形为同频方波从VOUT引脚输出。触摸屏则负责接收用户的点按操作把“调频率”“切波形”这类意图转换成具体寄存器的值。1.1 为什么选AD9834而不是AD9833市面上常用的DDS芯片还有AD9833两者对比下来AD9834多了优势一是支持频率和相位独立可调输出范围更灵活二是内置了可编程比较器方波输出直接就能用不需要外部搭比较电路三是IOUT引脚支持差分输出模式配合外部运放可以很方便地做幅度调节。当然AD9834价格会贵一些但对这个项目来说多花这几块钱换来的是电路更简单、功能更完整很划算。还有一个原因是AD9834的参考时钟范围是5MHz到75MHz我用了25MHz的有源晶振理论上输出频率分辨率可以达到 25MHz / 2^28 ≈ 0.093Hz这意味着我做1kHz信号时误差小于0.1Hz对绝大多数场景绰绰有余。如果换成AD9833虽然也是28位频率字但它没有内置比较器方波输出就得多加一级外部电路。1.2 系统架构与模块划分从软件角度我把这个项目分成了四块相互之间低耦合调试起来非常方便AD9834驱动层封装了SPI读写和寄存器写入函数往上提供 set_frequency(uint32_t freq)、set_phase(uint16_t phase)、set_waveform(enum) 之类的接口。触摸屏UI层负责界面绘制、触摸坐标读取、按钮命中检测、滑动条计算。参数管理层维护当前频率、波形类型、是否启用扫频等状态变量UI层只改参数驱动层只读参数中间通过标志位同步。应用逻辑层处理启动/停止、扫频任务、频率步进等业务流程。这种分层的思路说起来简单但对后续扩展很重要。比如我后来想加一个“扫频模式”就只需要在应用逻辑层加一个循环更新频率的定时器任务UI层加一个开关按钮驱动层完全不用动。如果一开始就把寄存器操作和UI逻辑揉在一起改起来会非常痛苦。2. 硬件连接与关键细节2.1 AD9834最小系统与引脚配置AD9834是28引脚的TSSOP封装焊接有一定难度建议直接买现成的AD9834模块省去手工焊和可能的地环路问题。模块上一般已经做好了电源滤波和输出接口我们需要关心的核心引脚只有几个引脚连接对象说明SCLKSTM32 SPI1_SCK (PA5)SPI时钟输入SDATASTM32 SPI1_MOSI (PA7)串行数据输入FSYNCSTM32 SPI1_NSS (PA4)帧同步低电平有效MCLK25MHz有源晶振参考时钟VOUT方波输出内置比较器输出IOUT模拟波形输出正弦/三角DAC输出需要运放缓冲COMP接100nF电容到AGNDDAC内部偏置补偿FSADJUST接6.8kΩ电阻到AGNDDAC满量程电流调节最容易被忽视的是COMP和FSADJUST这两个引脚。我第一次搭电路时没接FSADJUST电阻结果IOUT输出幅度明显偏小一开始还以为是芯片烧了查了手册才明白——FSADJUST引脚通过外部电阻设定DAC的满量程输出电流公式大约是 I_fullscale 18 / R_fsadjust68kΩ对应约0.26mA6.8kΩ对应约2.65mA。模块上通常会预留这个电阻但如果自己画板子一定别漏。IOUT引脚的输出是电流型信号直接用万用表或示波器看会发现问题幅度和波形都不对。我是在模块外部加了一级运放LM358足够做电流转电压和缓冲转换电阻取200Ω。如果要输出到后级设备建议再加一级阻抗匹配我个人习惯在输出端串一个50Ω电阻方便接同轴电缆。2.2 STM32F1的SPI接口配置AD9834的SPI是16位写入格式即每次FSYNC拉低后连续送入一个16位的控制字或数据字。STM32F1这边我用的是SPI1配置成主模式时钟极性和相位要匹配AD9834的要求——AD9834规定数据在SCLK的下降沿被采样所以SPI配置为 CPOL1, CPHA1空闲时钟为高第二个边沿采样8位数据模式连续发送两次形成一个16位字。我记得这个配置踩过坑用CPOL0, CPHA0也能“偶尔”工作但偶发写入错误信号不稳定。后来用逻辑分析仪抓波形发现是时序边沿采样不确定导致的。建议严格按数据手册来不要偷懒。SPI初始化代码参考如下void AD9834_SPI_Init(void) { GPIO_InitTypeDef gpio; SPI_InitTypeDef spi; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; // SCK, MOSI gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); gpio.GPIO_Mode GPIO_Mode_Out_PP; gpio.GPIO_Pin GPIO_Pin_4; // FSYNC 手动控制 GPIO_Init(GPIOA, gpio); GPIO_SetBits(GPIOA, GPIO_Pin_4); // FSYNC 空闲为高 spi.SPI_Direction SPI_Direction_1Line_Tx; spi.SPI_Mode SPI_Mode_Master; spi.SPI_DataSize SPI_DataSize_8b; spi.SPI_CPOL SPI_CPOL_High; spi.SPI_CPHA SPI_CPHA_2Edge; spi.SPI_NSS SPI_NSS_Soft; spi.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; spi.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, spi); SPI_Cmd(SPI1, ENABLE); }注意这里我把FSYNC当作普通GPIO手动拉高拉低而不是交给SPI硬件NSS自动控制。这样做的原因很简单——AD9834的FSYNC时序要求数据写入前先拉低写完后再拉高而且两次8位数据之间FSYNC必须保持低电平用软件控制最可靠还可以避免硬件NSS的极性和时序配置问题。2.3 触摸屏选型与驱动要点触摸屏部分我直接选了带触控芯片的3.5寸TFT模块常见的触控芯片包括XPT2046电阻屏或GT9147电容屏。这个项目用的是电阻屏因为便宜、触发靠压力感应、适合戴手套或非精准操作正好配合我的DIY场景。电阻屏的SPI读取方式要理解清楚XPT2046是一个12位ADC芯片通过SPI接口向它发送控制字节指定要读X方向还是Y方向通道再读取转换结果。控制字节为8位其中高3位是通道选择S1, A2A1A0101选X通道001选Y通道然后读回12位数据实际发送16个时钟前面8位输出控制字后面12个SCLK时钟读出数据。这里有个容易混淆的点XPT2046的SPI和AD9834的SPI如果共用同一个SPI外设要注意两者的时序和片选信号别交叉。我的做法是两者都挂在SPI1上但用不同的NSS引脚控制PA4给AD9834PB12给XPT2046互不干扰。触摸屏的驱动不是重点重点是校准和抗干扰这块我在第4章专门展开说。3. 软件核心逻辑与实操代码3.1 AD9834寄存器工作原理与写入时序AD9834内部有三个关键寄存器组控制寄存器16位、频率寄存器FREQ0/FREQ1各28位、相位寄存器PHASE0/PHASE1各12位。所有寄存器都是16位宽度的写操作通过地址位A15-A14来区分要写的是哪个寄存器。控制寄存器每个位的作用我需要重点说一下尤其是这几个位名功能B281频率字分两次写入低14位高14位HLB0先写低14位再写高14位RESET1复位DDS核心输出静默MODE1输出三角波MODE0输出正弦波OPBITEN1启用VOUT引脚输出方波DIV20VOUT分频控制影响方波输出比例FSELECT0使用FREQ0寄存器PSELECT0使用PHASE0寄存器SLEEP1/SLEEP12关断DAC/比较器用于省电频率字写入时B28位必须置1然后分两次第一次写低14位到FREQ0寄存器第二次写高14位。这两次必须连续中间不能插其他寄存器写入否则频率值就是错的。写入函数的核心逻辑void AD9834_Write(uint16_t data) { GPIO_ResetBits(GPIOA, GPIO_Pin_4); // FSYNC 0 SPI_I2S_SendData(SPI1, (uint8_t)(data 8)); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, (uint8_t)(data 0xFF)); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); GPIO_SetBits(GPIOA, GPIO_Pin_4); // FSYNC 1 }注意这里的等待条件我用的是TXE标志位表示发送缓冲区已空但不代表数据已经通过移位寄存器全部送出。如果紧接着马上拉高FSYNC最后一个位可能还没完全落定。稳妥做法是等BSY标志清零后再拉高或者用SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) 判断。3.2 三种波形的切换与参数计算AD9834的波形生成原理内部有一个28位的相位累加器每个MCLK周期累加一次频率字累加结果作为正弦查找表的地址输出对应幅度的数字值。输出频率的计算公式为f_out (MCLK × FREQREG) / 2^28反过来给定目标频率求频率字FREQREG f_out × 2^28 / MCLK以25MHz参考时钟、需要输出1kHz正弦波为例FREQREG 1000 × 268435456 / 25000000 10737四舍五入这个值写进寄存器后实际输出频率为 10737 × 25000000 / 268435456 ≈ 1000.0007Hz误差完全可以忽略。设置频率的代码void AD9834_SetFrequency(uint32_t freq) { uint32_t freq_reg (uint32_t)(((uint64_t)freq * 268435456ULL) / 25000000ULL); uint16_t low freq_reg 0x3FFF; uint16_t high (freq_reg 14) 0x3FFF; AD9834_Write(0x2000); // 设置B281FSELECT0写FREQ0 AD9834_Write(0x4000 | low); // 写低14位D150,D141 表示写FREQ0低字节 AD9834_Write(0x4000 | high); // 写高14位 }波形切换是通过控制寄存器的MODE位和OPBITEN位组合实现的void AD9834_SetWaveform(WaveType type) { switch (type) { case WAVE_SINE: AD9834_Write(0x2000 | 0x0000); // MODE0, OPBITEN0 正弦波 break; case WAVE_TRIANGLE: AD9834_Write(0x2000 | 0x0002); // MODE1 三角波 break; case WAVE_SQUARE: AD9834_Write(0x2000 | 0x0020); // OPBITEN1 VOUT输出方波 break; } }方波输出的一个细节DIV2位控制寄存器bit3决定比较器输出是否二分频。如果DIV20比较器直接输出DDS频率的方波如果DIV21输出频率减半占空比更接近50%。实测下来如果你的目标频率超过5MHz建议置DIV21否则方波的边沿比较肉。3.3 触摸屏交互参数调节与界面响应界面布局我做了三块区域顶部是一个波形选择栏三个按钮正弦、三角、方波中间是频率显示和调节区数字显示 “”“-”按钮进行步进调节底部是一个可拖动的频率滑条方便快速大范围扫频。触摸屏的调参逻辑是典型的“点按-命中检测-状态刷新”循环。核心代码如下uint16_t x, y; if (Touch_GetPoint(x, y)) { // 返回1表示检测到有效触摸 // 波形选择按钮区: 高度0-60 if (y 60) { if (x 80) set_current_wave(WAVE_SINE); else if (x 160) set_current_wave(WAVE_TRIANGLE); else set_current_wave(WAVE_SQUARE); } // 频率调节按钮 else if (is_in_rect(x, y, FREQ_PLUS_BTN)) { current_freq frequency_step; current_freq clamp(current_freq, MIN_FREQ, MAX_FREQ); } // 频率调节-按钮逻辑类似 else if (is_in_rect(x, y, FREQ_MINUS_BTN)) { current_freq (current_freq frequency_step) ? current_freq - frequency_step : MIN_FREQ; } // 滑条拖动 else if (is_in_rect(x, y, SLIDER_RECT)) { uint16_t relative_x x - SLIDER_X_START; current_freq MIN_FREQ (uint32_t)((uint64_t)relative_x * (MAX_FREQ - MIN_FREQ) / SLIDER_WIDTH); } AD9834_SetFrequency(current_freq); UI_Refresh(); }这里有个体验优化的细节滑条拖动时如果每次触摸事件都直接重新计算频率并写入AD9834会出现参数跳动过快、用户难以精确控制的问题。我的做法是把滑条分成200个刻度拖动一次至少变化一个刻度同时在拖动过程中只更新UI显示等手指松开触摸检测到pressedfalse时才真正写入寄存器。这样既避免频繁SPI写入造成的主循环卡顿又让操作手感更顺滑。触摸屏的坐标还有一个方向映射的问题如果发现触摸位置和屏幕显示错位很可能只是X/Y方向或者原点位置的问题。在代码里做一个简单的坐标变换x_mapped LCD_WIDTH - 1 - x; // 左右镜像 y_mapped LCD_HEIGHT - 1 - y; // 上下镜像哪个方向反了就对应改哪一行基本一两分钟就能解决。4. 常见问题排查与调试实录4.1 波形输出静态无声/无输出的头号原因我在调试时遇到最多的现象是寄存器写入没有报错SPI时序看起来也正常但示波器上IOUT引脚没有任何信号。排查到最后十有八九是这几个原因RESET位没有正确清零。AD9834上电后处于复位状态需要向控制寄存器写一个RESET0的指令来唤醒DDS核心。我一开始在上电初始化时只配置了B28和FSELECT忘记先写一次带RESET0的指令导致DDS一直停在复位状态。解决方式是初始化序列中加入一条 0x2000 写入然后再配置其他参数。FSYNC时序问题。SPI速率过快时FSYNC拉低后如果SCLK第一个边沿来得太快芯片可能识别不到帧起始。我的做法是在FSYNC拉低后加几个空指令周期延时实测在SPI预分频为418MHz时不需要延时但预分频为236MHz时就要加延时否则偶发不工作。DAC输出没有接负载电阻。AD9834的IOUT引脚是电流输出直接测电压是基本看不到波形的。必须在IOUT到AGND之间接一个负载电阻模块上一般已有或者经过运放转成电压信号。4.2 正弦波变成“台阶波”或带毛刺波形看起来像阶梯状或者高频段有毛刺这通常是两方面的原因一是DAC输出端的重建滤波器缺失。AD9834输出的是DAC采样后的阶梯波开发阶段直接看波形问题不大如果要相对光滑的波形需要在IOUT输出后加一个低通滤波器。截止频率设置为最高输出频率的1.2~1.5倍即可。我用的是最简单的RC一阶低通R100Ω、C100nF实测对10kHz以下信号足够。二是电源噪声。AD9834对模拟电源的纹波很敏感我最初用开发板的3.3V直接供模拟电源输出波形底噪明显变大。后来在AVDD引脚前增加了LC滤波磁珠10uF钽电容100nF波形干净了许多。另外数字地和模拟地要单点连接不要大面积铺铜直接连通。4.3 触摸屏坐标不准、漂移和乱跳电阻触摸屏的校准是一个绕不开的坑。物理屏的电压分压比例、AD转换的偏移都会导致触摸坐标和像素坐标不对应。我的做法是采用经典的三点校准法屏幕三个固定位置显示校准点用户依次点击记录对应的ADC原始值然后计算线性映射关系。校准参数计算的核心是建立两个线性方程X_screen Ax × X_adc Bx × Y_adc Cx Y_screen Ay × X_adc By × Y_adc Cy三个点产生三组方程足够解出这六个系数。实际代码中我用的是简单的联立方程求解不需要矩阵库。校准完成后把系数保存在STM32F1的内部Flash中下次上电直接读取不用每次校准。触摸乱跳还有一个容易被忽视的原因屏幕供电不稳。电阻触摸屏被触碰时会产生微弱电压如果VREF引脚有波动转换结果就会抖动。我实测下来在XPT2046的VREF引脚加一个1uF去耦电容乱跳问题减少了一半以上另外触摸检测的“去抖”逻辑也很重要连续读取三次ADC值若偏差超过阈值则认为是噪声舍弃这次触摸。4.4 SPI偶发通信失败AD9834的SPI通信偶发失败表现是频率偶尔不对或者切换波形时无输出。这种问题多半是电平不匹配或时钟干扰。STM32F1的IO口是3.3V电平AD9834数字IO同样兼容3.3V直接连接没问题。但如果模块上有电平转换电路一定要确认方向和电平是否正确。我踩过的一个坑是有个模块的FSYNC走线特别长而且紧挨着MCLK晶振引脚导致SCLK上升沿时FSYNC被耦合干扰偶发错帧。解决方法是把FSYNC引脚配置为推挽输出并增加上拉同时在PCB走线上尽量远离晶振区。另外SPI通信结束后FSYNC拉高后要留出至少一个SCLK周期的恢复时间再进行下一次传输。我在每次写入函数末尾加了约2us延时使用一个简单自旋遍历实测可靠性提升明显。void AD9834_Write(uint16_t data) { // 拉低FSYNC发送两字节拉高FSYNC // ... for (volatile int i 0; i 20; i); // 约2us延时 }最开始我也嫌加延时是“土办法”但看了数据手册时序图就明白了——FSYNC拉高后芯片内部逻辑需要恢复时间才能处理下一帧加一点延时是符合手册要求的。4.5 电阻触摸屏的“压力”判断电阻触摸屏需要一定按压力才能准确触发但如果压力阈值设置得过于灵敏会出现手指还没碰到就触发、或者手心误触的情况。XPT2046芯片其实支持触摸压力检测通过测量Z1/Z2通道的电阻值可以间接判断按压力度。代码里可以设置一个压力阈值只有检测到足够压力才认为是一次有效的触摸。不过实测下来压力检测的数值稳定性一般我更多是采用“按下-抬起”的边沿检测只在触摸发生“从无到有”的上升沿时触发一次事件而不是按住期间不断触发。配合大约30ms的去抖窗口操作体验很稳定不会出现拖动一个按钮时不断触发多次动作的问题。我这边还遇到过一次奇怪的问题触摸屏单次点按正常但快速拖动滑条时X坐标跳变严重。分析下来是读取触摸状态时XPT2046的采样分辨率不够高且快速移动时ADC采样时刻恰好落在屏幕电压建立过程的中间。解决方法是每次读取时先读取一次X值、一次Y值然后立即丢弃第二次读取的值才是稳定后的值。牺牲了一点采样时间换来了坐标稳定性。关于这个项目我最后想说的话整套系统做下来我对DDS芯片的寄存器级操作有了比看书深刻得多的理解尤其是那些“你以为是玄学、其实是时序”的坑。如果说有什么建议就是调试时一定准备一台靠谱的逻辑分析仪别只靠肉眼观察。很多偶发性问题像FSYNC干扰、SPI时序不满足用示波器不一定能一眼看出来但逻辑分析仪抓一次时序问题基本就水落石出了。另外AD9834模块买回来不要急着接STM32先用树莓派的GPIO或者一个简单的SPI调试器模拟写入几个寄存器在示波器上验证芯片本身是好的再往下做代码。硬件链路和软件链路分开排查能省掉你至少一晚上的痛苦排查时间。如果你按这个思路把项目做得更深入可以考虑下一步加上信号幅度控制用数字电位器或运放VCA或者把频率范围往高频方向扩展换更高参考时钟。反正底层驱动已经跑通往上扩展更多是应用层的事情难度不大有想法就大胆试。本文还有配套的精品资源点击获取