☰
STM32F1触摸屏驱动AD9834:DDS波形发生器设计与SPI时序实践
2026/10/15 14:35:28 网站建设 项目流程

简介:一个基于意法半导体STM32F1微控制器与AD9834数字合成器的触摸屏波形发生器项目资源,面向嵌入式系统初学者和需要产生可调频率波形信号的开发者,解决如何通过STM32的SPI接口控制AD9834输出正弦波、三角波与方波,并借助触摸屏实现波形切换和频率设置。资源为RAR压缩包(约4.49MB),页面显示文件总数为0且未提供文件类型明细,解压后请以实际内容为准。目前已有1882人学习下载,适合作为DDS波形发生器设计、STM32外设开发以及SPI通信学习的参考实例。内容涵盖STM32F1初始化SPI接口、配置外部中断响应触摸屏按键事件、解析触摸坐标并换算频率值、生成AD9834控制字与寄存器配置等重要环节,同时涉及HAL/LL库的工程模板使用、波形整形电路原理、电源稳定性与信号完整性处理思路,读者可以据此掌握完整的固件编写流程,并借助示波器对输出波形进行调试与验证。

1. 一块触摸屏让 AD9834 从寄存器手册变成三合一信号源

第一次接触 STM32F1 触摸屏加载 AD9834 正弦波、三角波、方波这套组合时,大多数人的第一反应是“又是个 DDS 波形发生器 Demo”,但真正上手才发现,难点根本不在 DDS 芯片本身,而是屏幕坐标、SPI 时序、寄存器状态机三个环节互相咬合。这个资源解决的是:一块带触摸屏的 STM32F1 板卡,通过 SP I 驱动 AD9834,在屏幕上实时切换波形、调频率和相位,并且每次按键都能看到输出变化。适合做信号源类课程设计、仪器仪表原型、传感器激励源验证的开发者——尤其是那些“想快速跑通、又不想被底层时序卡一周”的人。

2. 为什么选 AD9834 而不是现成 DDS 模块:从相位累加器看一眼寄存器边界

2.1 相位累加器与频率字:1kHz 输出手算一遍再写代码

AD9834 内部是 28 位相位累加器,输出频率由主时钟频率和频率控制字(FTW)共同决定。公式是f_out = (FTW / 2^28) × f_MCLK。很多人直接背公式去写代码,结果换了主频就翻车,因为频率字必须跟着晶振走。

我在调试这个工程时,习惯先用 25MHz 主频手算一个 1kHz 输出验证理解对不对:FTW = 1000 × 2^28 / 25,000,000,算出来约等于 10737。反过来验证,10737 × 25,000,000 / 2^28 ≈ 1000.03 Hz,误差在毫赫兹级别。这个手算结果很关键——它决定了写代码时是先写高 14 位还是低 14 位,也决定了你需要给频率字留多少位宽。

// 25MHz 参考时钟下把频率字算出来 #define AD9834_MCLK_HZ 25000000UL #define AD9834_FULL_SCALE 268435456UL // 2^28 uint32_t AD9834_FrequencyWord(uint32_t freq_hz) { // 用 64 位做乘法,避免 32 位溢出 uint64_t acc = (uint64_t)freq_hz << 28; return (uint32_t)(acc / AD9834_MCLK_HZ); }

这段代码的要点是<<28的时机:如果先除再移位,1kHz 这种小频率会被整数除法直接截成 0。我第一版就吃过这个亏,输出死活是直流。参数上,AD9834_MCLK_HZ必须匹配你板子上实际焊接的晶振,而不是手册里那个“最大支持值”;改晶振时只需要改这一个宏,频率字会自动跟着变。

2.2 AD9834 与 AD9833 的本质区别:多出来的方波路径

这个资源标题里把“正弦波、三角波、方波”并列,不是随便写的。AD9833 的方波是从 DAC 输出端分压兼用的,AD9834 带一个内置比较器,方波从 SIGN 引脚单独走出来。两种方案都能出方波,但 AD9834 的方波沿更干净,输出幅度不受 DAC 满幅影响,这对后面接模拟滤波或比较器阈值很有价值。

选 AD9834 的第二个理由是它有两个频率寄存器和两个相位寄存器,触屏界面切“预置频率”时不需要重算频率字,只要切 FSEL/PSEL 控制位就行。我在这个工程里只用了频率寄存器 0 和相位寄存器 0,但预留了寄存器 1 的接口,后续扩展扫频和跳频不用改驱动框架。

2.3 硬件接线:最少六根线,但别省去耦电容

板上 AD9834 与 STM32F1 的连接按 SPI 从机接法:MCU 的 PA5-PA7 对应 SCLK、SDATA、FSYNC,另外两根是 AD9834 的 FSELECT 和 PSELECT,直接从 GPIO 拉下来复用为寄存器选择。供电用 3.3V 单电源,REF 引脚对地接 0.1μF 和 10μF 并联去耦,这两个电容不能省,否则高频段输出会看到周期性抖动。

STM32F1 引脚AD9834 引脚说明
PA5SCLKSPI 时钟,GPIO 模拟时手动翻转
PA7SDATA串行数据输入
PB0FSYNC帧同步,低电平有效
PB1FSELECT频率寄存器选择
PB2PSEL相位寄存器选择
—COMP比较器输出,方波从这取

FSELECT 和 PSEL 在基础版固件里可以直接接地,只操作寄存器 0。加上它们是为了给后续切“预置信号”留余地。COMP 到 MCU 引脚之间我串了个 100Ω 电阻,主要作用是防止杜邦线过长时反射振铃把比较器输出打成毛刺方波。

2.4 复位时序:上电后第一件事不是写频率字

AD9834 上电后寄存器内容是未定义状态,直接写频率字很可能被内部的随机控制字干扰。正确做法是先执行一次硬件复位,让内部状态机归零,再解除复位并配置控制寄存器。这个“先复位、后配置”的顺序是整个驱动的地基,我见过有人漏了这一步,结果每次上电波形都不一样。

void AD9834_Init(void) { // 片选拉低,进入写状态 AD9834_FSYNC_LOW(); // 写入 RESET 置位,清零内部相位累加器 AD9834_Write16(AD9834_CTRL_RESET); // 解除复位,同时开启 B28 模式:允许两个 16 位字拼成一个 28 位频率字 AD9834_Write16(AD9834_CTRL_B28); AD9834_FSYNC_HIGH(); }

这里AD9834_CTRL_RESET是控制字里第 8 位,AD9834_CTRL_B28是第 13 位。第一次写入把 RESET 置 1,第二次把 RESET 清 0,差分队的间隔不用刻意延时,连续发即可。很多移植代码把这两步写成一条写操作,看起来省事,但内部复位信号没有足够时间完成状态清零,后续频率会偶发错乱。

3. 把波形写进 DDS:初始化、频率计算与 SPI 发送的三段式写法

3.1 初始化流程拆解:控制字、频率字、相位字的分工

AD9834 的寄存器只有三类:控制寄存器、频率寄存器、相位寄存器。控制寄存器决定芯片工作在什么模式;频率寄存器决定输出频率;相位寄存器决定输出相移。这个工程里我把初始化拆成三个独立函数,而不是放在一个大函数里倒流程:AD9834_Init()只负责复位和控制字,AD9834_SetFreq()只负责写频率,AD9834_SetPhase()只负责写相位。这样在调试触摸屏逻辑时,改频率只要调一个函数,不会误碰控制位。

控制寄存器里最重要的三个位是 B28、OPBITEN、MODE。B28=1 让芯片接受 28 位频率字(分两次写入);OPBITEN 和 MODE 一起决定 DAC 输出的是正弦、三角还是方波数据。把这些位用宏定义写清楚,比在代码里堆裸数字可读性强得多。

#define AD9834_CTRL_B28 (1 << 13) // 两个 16 位字拼成 28 位频率字 #define AD9834_CTRL_RESET (1 << 8) // 复位相位累加器 #define AD9834_CTRL_OPBITEN (1 << 5) // 输出数据选择 #define AD9834_CTRL_MODE (1 << 1) // 三角波/正弦波切换 #define AD9834_CTRL_WORD (0xC000) // 控制寄存器标志位,DB15:DB14 = 11

AD9834_CTRL_WORD是控制字的标志位,必须保持高两位为 11 才能让芯片识别这是一条控制寄存器命令。I 一般会在每个控制字写入时做一次| AD9834_CTRL_WORD,防止拼接时丢标志。

3.2 频率字拆分:高 14 位、低 14 位的写入顺序别搞反

频率字是 28 位,AD9834 的串行帧是 16 位,所以必须分两次发送。B28=1 时,芯片期待先收到低 14 位,再收到高 14 位。我见过不少移植代码把顺序写反,输出频率变成乱的,还以为是晶振坏了。

void AD9834_SetFreq(uint32_t freq_hz) { uint32_t ftw = AD9834_FrequencyWord(freq_hz); // 低 14 位:DB15:DB14 = 00 表示这是频率寄存器数据 AD9834_Write16(0x0000 | (ftw & 0x3FFF)); // 高 14 位:同样的标志位,数据右移后取高段 AD9834_Write16(0x0000 | ((ftw >> 14) & 0x3FFF)); }

0x3FFF是 14 位全 1 的掩码,作用是截断多余位。第一帧写入ftw & 0x3FFF,第二帧写入右移后的高 14 位。如果把顺序反过来,芯片会把高段当低段用,频率直接差 16384 倍。调试时你可以在逻辑分析仪上看四个字的顺序,低帧永远紧跟高帧,中间不能插入其他寄存器写入,否则 B28 的拼接逻辑会被打断。

3.3 SPI 发送函数:模拟时序与硬件 SPI 的选择

这个资源里使用的 SPI 发送函数有两种版本,GPIO 模拟版本,适合第一版调试;硬件 SPI 版本,适合稳定运行。我建议先跑 GPIO 模拟版,确认时序逻辑没问题后再切硬件 SPI。原因很简单:GPIO 模拟的时钟边沿是你自己控制的,理论上不会有时序歧义,而硬件 SPI 还要核对 CPOL、CPHA 极性和分频系数。

void AD9834_Write16(uint16_t data) { AD9834_FSYNC_LOW(); // FSYNC 拉低,开始一帧传输 delay_us(1); // FSYNC 建立时间,等待芯片就绪 for (int i = 15; i >= 0; i--) { // 先放数据,再拉时钟,保证上升沿采样时数据已稳定 if (data & (1 << i)) AD9834_SDATA_HIGH(); else AD9834_SDATA_LOW(); AD9834_SCLK_LOW(); delay_us(1); AD9834_SCLK_HIGH(); // 上升沿锁存数据 delay_us(1); } AD9834_FSYNC_HIGH(); // FSYNC 拉高,结束一帧 }

这个函数的核心是“先置数据,再给时钟沿”。如果反过来,SCLK 翻转时机在数据变化前,芯片采到的还是上一位的电平,整帧数据就会左移或右移一位,输出频率直接减半。delay_us(1)在 72MHz 主频下大约产生几十个周期的延时,对 AD9834 来说足够宽松;如果改成硬件 SPI,可以跑 1.8MHz 或更低,FSYNC 同样保持整帧拉低。

3.4 三种波形的切换:不是换 DAC 输出,是换控制位组合

很多刚入门的人以为“波形切换”是修改输出表或者切换不同的查找表,实际上 AD9834 内部只有一张正弦查找表,三角波是正弦数据经过绝对值变形得到的,方波则由比较器产生。三种波形的切换本质是改控制寄存器的 MODE 位和 OPBITEN 位。

typedef enum { WAVE_SINE = 0, WAVE_TRIANGLE, WAVE_SQUARE } wave_type_t; void AD9834_SetWaveform(wave_type_t wave) { uint16_t ctrl = AD9834_CTRL_B28 | AD9834_CTRL_WORD; switch (wave) { case WAVE_SINE: ctrl |= 0x0000; // MODE=0 输出正弦 break; case WAVE_TRIANGLE: ctrl |= AD9834_CTRL_MODE; // MODE=1 输出三角波 break; case WAVE_SQUARE: ctrl |= AD9834_CTRL_OPBITEN; // 打开比较器数据输出 break; } AD9834_FSYNC_LOW(); AD9834_Write16(ctrl); AD9834_FSYNC_HIGH(); }

正弦和三角之间只差一个MODE位,方波则要额外置位 OPBITEN,让 SIGN 引脚输出比较器结果。我在工程里把这三个波形做成枚举,触摸屏菜单里的“波形按键”直接对应到AD9834_SetWaveform()的入参,界面逻辑和寄存器逻辑就解耦了——换屏、换交互方式时不用重写驱动。

4. 触摸屏状态机:从坐标点到波形参数不硬编码

4.1 触摸坐标校准与滑动滤波:把毛刺吃掉再进状态机

触摸屏这部分常见误区是:读到一个坐标就去判定按键区域,结果屏上按一个钮,程序同时触发了频率加减。XPT2046 这类电阻触摸屏的 ADC 结果本身就有抖动,手指按下去瞬间产生的接触电阻变化能造成几十个单位跳动。

#define FILTER_LEN 5 volatile uint16_t touch_raw_x = 0; // 原始坐标缓存,供滤波使用 volatile uint16_t touch_raw_y = 0; uint16_t Touch_Filter(uint16_t new_sample, uint16_t *buf, uint8_t len) { // 移动平均滤波,输出 = 队列内所有采样值的均值 uint32_t sum = 0; for (uint8_t i = 0; i < len - 1; i++) { buf[i] = buf[i + 1]; // 数据前移,实现滑动窗口 sum += buf[i]; } buf[len - 1] = new_sample; // 最后一个位置写入新采样值 sum += buf[len - 1]; return (uint16_t)(sum / len); }

这段代码的代价是每次触摸都多算 4 次累加和 1 次除法,在 72MHz 的 STM32F1 上完全可以忽略。FILTER_LEN取 5 是平衡点:再短滤不掉毛刺,再长会让响应明显变肉,手指已经滑到目标区域,滤波结果还没跟上。

4.2 界面状态机:波形选择、频率调节、相位微调、幅度补偿四态

我不建议在触摸事件里直接写if (x>100 && x<200)这种散装判断,触摸屏交互一旦复杂,这些判断就是泥潭。这个工程的状态机分四态:波形选择、频率粗调、相位微调、幅度补偿。每个状态只关心属于自己的坐标区间,状态切换由“模式按钮”和“返回按钮”控制。

状态触摸动作底层输出
波形选择点三块区域AD9834_SetWaveform()切换
频率粗调滑条上下拖动AD9834_SetFreq()实时刷新
相位微调左右步进按钮AD9834_SetPhase()每次步进 1°
幅度补偿滑条左右拖动控制外部增益,不碰 DDS
uint8_t ui_state = STATE_WAVE_SELECT; void UI_EventLoop(TouchPoint pt) { if (!pt.pressed) return; // 无触摸就跳过整段处理 switch (ui_state) { case STATE_WAVE_SELECT: if (pt.x < 80) AD9834_SetWaveform(WAVE_SINE); else if (pt.x < 160) AD9834_SetWaveform(WAVE_TRIANGLE); else AD9834_SetWaveform(WAVE_SQUARE); break; case STATE_FREQ_ADJUST: // 频率值 = 上次值 + (当前Y - 上次Y) * 步进 freq_hz += (pt.y - last_y) * freq_step; freq_hz = CLAMP(freq_hz, 1, 1000000); // 限制 1Hz ~ 1MHz AD9834_SetFreq(freq_hz); break; } last_y = pt.y; }

状态机的价值是让代码的复杂度跟着状态走,而不是跟着屏幕分辨率走。换一块分辨率不同的屏幕,只需要修改坐标映射函数,状态机逻辑完全不动。CLAMP宏把频率限制在合理范围内,防止滑条拖过头写进非法频率字,底层 DDS 反而输出乱波。

4.3 差异更新:不是每次触摸都重建全部波形参数

触摸屏调节频率时,很多实现是每次触摸都重新初始化 DDS,结果屏幕一卡一卡,频率字低 14 位频频闪烁。真实做法是只更新变化的那部分参数:频率变了就只写频率字,波形变了就只写控制字。DDS 的寄存器本身保留上次状态,你写哪一个就变哪一个,其他不动。

typedef struct { uint32_t freq_hz; uint16_t phase_deg; wave_type_t wave; } signal_param_t; signal_param_t param = { 1000, 0, WAVE_SINE }; void UI_ApplyChange(uint8_t field) { if (field & FIELD_FREQ) AD9834_SetFreq(param.freq_hz); if (field & FIELD_PHASE) AD9834_SetPhase(param.phase_deg); if (field & FIELD_WAVE) AD9834_SetWaveform(param.wave); }

UI_ApplyChange()接收一个位掩码,哪些参数被触摸事件改了,哪些就被重新下发。这个设计在后期添加“按住加速”功能时特别好用——拖动过程中只更新频率字,不重发控制字,DDS 的输出不会出现相位跳变。

5. AD9834 避坑记录:SPI 时序、上电复位与信号完整性的五个翻车现场

5.1 现象:切三角波后输出还是正弦,怎么按都没反应

第一版调试时,我在界面里反复切波形,示波器上始终是正弦。排查发现是 FSYNC 时序问题:AD9834 要求 FSYNC 拉低期间完成一整帧数据发送,拉高的动作必须发生在最后一个 SCLK 上升沿之后。我的代码在写控制字时,FSYNC 拉高过早,最后一两个 bit 被芯片丢弃,控制字里 MODE 位正好落在被丢的区间。

原因:控制字写入被 FSYNC 截断,芯片收到的实际控制字不完整。解决:把 FSYNC 拉高动作放到AD9834_Write16()内部,而不是外面包一层;并加 1μs 延时确保最后一位被可靠锁存。从那以后这个工程的所有写操作都统一走Write16(),不再在调用方手动控制 FSYNC。

5.2 现象:上电后波形乱跳,频率显示和输出完全对不上

有几次上电,屏幕显示 10kHz,示波器却是 20kHz 的乱波,重新焊一块板子又好了。最后对比发现,出问题的板子跳过了复位步骤,在芯片未复位状态下直接写频率字。AD9834 的寄存器上电初值未知,B28 位可能是 0 或者 1,芯片对 28 位频率字的拼接方式就不是预期的。

原因:未执行硬件复位,寄存器初值不可控。解决:初始化第一句必须写RESET=1的控制字,第二句写B28=1解除复位,两条之间不要插入频率字写入。从那以后我写 DDS 驱动,上电复位和解除复位永远是最前面两行,这个顺序写死在文档里,新板卡复制粘贴也不会错。

5.3 现象:触摸屏按键偶发失灵,按一下频率跳几格

触摸层的问题单独排查时,寄存器和 DDS 输出都正常,但只要接上 UI 层就偶发“按键连跳”。逻辑分析仪抓触摸坐标发现,按下瞬间 ADC 采到第一帧毛刺数据,坐标落在了旁边按键区域。

原因:触摸采样未滤波,按下瞬间的接触噪声被当成有效坐标。解决:在触摸回调里先过 5 点滑动平均,再判断区域;同时把“坐标离开屏幕范围”的样本直接丢弃,不作为有效触摸。加上滤波后连跳问题消失,代价是响应延迟增加约 2ms,体感完全无差别。

5.4 现象:1MHz 正弦波顶部被削平,像放大的方波

跑到 1MHz 时,DAC 输出经过放大电路后顶部明显削平。AD9834 的 DAC 满幅输出受参考电压和内部运放摆率限制,直接用同相放大器把幅度拉太高,靠近电源轨的位置就会削波。

原因:放大电路增益设置过高,输出接近电源轨达到饱和。解决:把增益从 3.3 倍降到 1.65 倍,并把参考电压改成 2.5V 基准源;如果必须 3.3V 满幅,换成轨到轨运放。这个坑提醒我,DDS 芯片本身没问题,问题常常在信号链路的尾部。

5.5 现象:SPI 速度拉高后,偶发频率没变化,但逻辑分析仪显示波形正确

GPIO 模拟 SPI 版本稳定运行后,想切到硬件 SPI 省 CPU,结果发现写频率字偶发失败。逻辑分析仪看波形完全正常,SCLK、SDATA、FSYNC 时序都对得上,但 DDS 就是没更新。

原因:硬件 SPI 的时钟边沿和 GPIO 模拟时有细微差别,加上杜邦线过长,信号反射导致 AD9834 判读出错误电平;GPIO 模拟版速度慢、边沿缓,反而不会触发这个问题。解决:把硬件 SPI 时钟分频到 1.8MHz 以下,SCLK 线串联 33Ω 电阻吸收反射,FSYNC 拉低到拉高的间隔加至少 1μs。这类问题最坑的地方在于“看着对了但没对”,必须靠实际输出反查时序参数。

6. 相位校准与波形质量验证:示波器 FFT 和双 DDS 相位对齐

6.1 用示波器 FFT 快速验证 DAC 输出谱纯度

波形“能看”不代表“能用”,尤其是信号源类工具,频谱纯度比时域形状更关键。把 AD9834 输出接到示波器 CH1,打开 FFT 模式并选汉宁窗,看主峰之外有没有明显杂散。AD9834 这类 28 位 DDS 的杂散通常来自两个方面:频率字量化误差,以及输出 DAC 转换毛刺。25MHz 参考时钟下输出 1MHz,理论上杂散应低于 -50dBc;如果杂散高得离谱,多半是参考时钟质量差或者是电源去耦不足。

6.2 双 DDS 相位对齐:写同样的相位字不等于同时对齐

做两路同步输出时,如果两块板各用一个晶振,即使频率字完全一样,相位差也无法固定。这个工程里我做了一个校准流程:先同步拉低两块板的 FSYNC 和复位引脚,让它们在同一时刻启动相位累加器;然后用示波器测两路信号的相位差,再把差值换算成相位字写入。

相位字的换算是相位字 = 相位差(度) × 4096 / 360。比如测出相差 45°,相位字就是 512。把它落到相位寄存器的低 12 位,重新写一次即可。校准完成后两路相位差通常能稳定在 1° 以内,前提是参考时钟频率一致且晶振温度漂移相近。

6.3 从“能出波形”到“输出可靠”的三个习惯

这个工程做完后,我养成了三个习惯:每次改硬件都用逻辑分析仪抓一次 SPI 帧,而不是直接看示波器输出;每次改频率字都先手算一次预期值,验证代码里的位运算是按当前主频推导的;每个控制字都保留十六进制的注释,防止换芯片型号后拿不准位定义。这套流程下来,AD9834 相关的坑基本都能在一小时内定位,而不是靠玄学换电容碰运气。

这套工程里的驱动层和触摸状态机层是分开的,下载后可以把驱动文件直接搬到自己的板子上,触摸层做针对性修改。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询