简介:这份资源是面向嵌入式开发者的4路AD9833驱动工程,重点解决单片机通过SPI接口同时控制四片AD9833、生成四路超声波信号的问题,适用于超声波测距、工业检测、医疗换能器等需要多通道同步波形的场景,适合具备STM32或AVR基础、想深入掌握DDS与SPI通信的中级开发者。压缩包共231个文件,约4.37MB,以c源码、h头文件、o目标文件、crf编译中间文件为主,另含uvprojx、uvoptx等Keil工程文件、sct分散加载脚本、hex烧录文件、map映射表及bat批处理脚本,构成一套可直接编译调试的完整工程。目前已有317人学习下载。工程已通过实际调试,涵盖SPI驱动、AD9833寄存器配置、四通道波形生成逻辑与硬件初始化代码,读者可据此理解频率与占空比调节、信号同步、振荡器稳定性及噪声抑制等排错思路,快速复用到自己的多路信号发生方案中。
1. 四路超声波驱动遇上四路 AD9833:这套组合到底解决什么问题
如果你手上有四路超声波换能器,需要同时输出四路可独立调频、调相的正弦激励信号,用单片机 PWM 加模拟滤波的方案大概率会让你在调试台上耗掉一整周。PWM 方案的问题很直接:占空比和频率耦合、谐波滤不干净、四路之间的相位差靠定时器硬凑,一旦要求频率在 20kHz 到 200kHz 之间连续可调,滤波器的截止频率就得跟着换。四路 AD9833 驱动工程配合四路超声波驱动的思路,本质上是把信号生成这件事从 MCU 的定时器里彻底剥离出来,交给四片专用 DDS 芯片各自独立完成,MCU 只负责写寄存器和协调时序。
这套方案适合谁?做超声波相控阵激励、多通道超声清洗、超声雾化或者超声测距阵列的硬件工程师和嵌入式开发者。你不需要 MCU 有多强的算力,一颗普通的 STM32F103 甚至 8 位单片机就能带动四片 AD9833。代价是你需要理解 DDS 的寄存器模型、SPI 时序约束,以及四路超声波换能器负载对输出级的要求。AD9833 本身输出的是 0.6Vpp 左右的正弦波,直接驱动换能器是不够的,中间必须加功率放大和阻抗匹配,这部分才是四路超声波驱动真正的工程难点。
2. AD9833 的寄存器模型与四路独立调频的实现逻辑
2.1 为什么选 AD9833 而不是 MCU 内部 DDS 或 PWM
AD9833 是一颗 10 位 DDS 芯片,内部有 28 位频率寄存器、12 位相位寄存器和 10 位 DAC。输出频率公式是 f_out = f_MCLK × FREQREG / 2^28。假设外部时钟用 25MHz 晶振,频率分辨率是 25MHz / 268435456 ≈ 0.093Hz。这个分辨率意味着你在 40kHz 附近调频时,步进可以做到 0.1Hz 以下,对于需要精确匹配换能器谐振点的场景非常关键。
用 MCU 内部 PWM 做等效的事情,你需要至少 12 位以上的定时器分辨率,还要面对占空比固定 50% 才能保证对称波形的问题。更麻烦的是四路独立调频:STM32 的高级定时器最多给你几路互补输出,但频率是共享的。要四路不同频率,就得占用四个定时器,还要处理它们之间的同步和中断冲突。AD9833 的方案里,四片芯片共享一个 SPI 总线,用独立的 FSYNC 引脚片选,MCU 依次写入频率寄存器和相位寄存器,每片芯片自己维持输出,互不干扰。
选型上还有一个容易被忽略的点:AD9833 的输出是正弦波,谐波分量在 25MHz 时钟下,第一镜像频率在 f_MCLK - f_out 附近,对于 40kHz 输出,镜像在 24.96MHz,离得很远,一个简单的 LC 低通就能滤掉。PWM 方案里,载波频率如果只有几百 kHz,谐波会落在几十 kHz 到几百 kHz 范围内,滤波器的过渡带设计非常紧张。
2.2 四路 AD9833 的 SPI 总线拓扑与片选策略
四片 AD9833 共用 SCK、SDATA 两根线,每片一个独立的 FSYNC。AD9833 的 SPI 模式是 CPOL=0、CPHA=1,数据在时钟下降沿被采样,16 位一帧。写入时序里,FSYNC 拉低后,16 个时钟周期内把数据移入,然后 FSYNC 拉高。注意 FSYNC 拉高之后需要至少一个 SCK 周期的延时才能开始下一帧,这个在数据手册里叫 t_7,典型值 10ns 左右,但实际用 STM32 的硬件 SPI 时,因为 CS 由软件 GPIO 控制,两次写之间自然会有足够延时。
四路片选的 GPIO 分配建议放在同一个 GPIO 端口上,比如 PA4 到 PA7,这样可以用 BSRR 寄存器一次性操作,减少片选切换的抖动。如果分散在不同端口,每次切换要读改写,在高速连续写寄存器时会引入额外的时间开销。
// AD9833 寄存器地址定义 #define AD9833_REG_CTRL 0x2000 // 控制寄存器,B28 位决定写频率的方式 #define AD9833_REG_FREQ0 0x4000 // 频率寄存器 0 #define AD9833_REG_FREQ1 0x8000 // 频率寄存器 1 #define AD9833_REG_PHASE0 0xC000 // 相位寄存器 0 #define AD9833_REG_PHASE1 0xE000 // 相位寄存器 1 // 四路片选引脚,假设接在 PA4~PA7 #define FSYNC0_PIN GPIO_Pin_4 #define FSYNC1_PIN GPIO_Pin_5 #define FSYNC2_PIN GPIO_Pin_6 #define FSYNC3_PIN GPIO_Pin_7 #define FSYNC_PORT GPIOA // 向指定通道的 AD9833 写入 16 位数据 void AD9833_Write(uint8_t ch, uint16_t data) { uint16_t pin; switch(ch) { case 0: pin = FSYNC0_PIN; break; case 1: pin = FSYNC1_PIN; break; case 2: pin = FSYNC2_PIN; break; case 3: pin = FSYNC3_PIN; break; default: return; } GPIO_ResetBits(FSYNC_PORT, pin); // 拉低片选 SPI_I2S_SendData(SPI1, data); // 发送 16 位数据 while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY)); // 等待发送完成 GPIO_SetBits(FSYNC_PORT, pin); // 拉高片选,锁存数据 }这段代码里,AD9833_Write是底层写函数,每次调用只操作一路。片选拉低后通过硬件 SPI 发送 16 位数据,等待 BSY 标志清除后拉高片选。参数ch是通道号 0 到 3,data是已经拼好寄存器地址的 16 位值。注意SPI_I2S_SendData之后必须等 BSY 清零,否则在连续写多个寄存器时,后一帧数据可能覆盖前一帧。
2.3 频率寄存器与相位寄存器的写入顺序
AD9833 上电后的默认状态是 RESET 位为 1,此时 DAC 输出中间电平,不产生波形。初始化流程是:先写控制寄存器,把 B28 位置 1,选择 28 位频率写入模式;然后分两次写频率寄存器的低 14 位和高 14 位;最后写相位寄存器;再写一次控制寄存器,清除 RESET 位,同时选择频率寄存器和相位寄存器。
// 初始化一路 AD9833,设置频率和相位 void AD9833_Init(uint8_t ch, float freq_hz, float phase_deg) { // 计算 28 位频率字,MCLK = 25MHz uint32_t freq_word = (uint32_t)((freq_hz * 268435456.0) / 25000000.0); uint16_t freq_lsb = (uint16_t)(freq_word & 0x3FFF); // 低 14 位 uint16_t freq_msb = (uint16_t)((freq_word >> 14) & 0x3FFF); // 高 14 位 // 计算 12 位相位字,360 度对应 4096 uint16_t phase_word = (uint16_t)((phase_deg / 360.0) * 4096.0) & 0x0FFF; // 1. 复位,B28=1,选择 28 位写入模式 AD9833_Write(ch, AD9833_REG_CTRL | 0x2100); // 2. 写频率寄存器 0 的低 14 位 AD9833_Write(ch, AD9833_REG_FREQ0 | freq_lsb); // 3. 写频率寄存器 0 的高 14 位 AD9833_Write(ch, AD9833_REG_FREQ0 | freq_msb); // 4. 写相位寄存器 0 AD9833_Write(ch, AD9833_REG_PHASE0 | phase_word); // 5. 清除 RESET,选择 FREQ0 和 PHASE0,输出正弦波 AD9833_Write(ch, AD9833_REG_CTRL | 0x2000); }freq_word的计算用浮点乘法再转整型,在 STM32F103 上没有 FPU,这个转换会消耗几十个周期,但只在初始化时调用一次,不影响实时性。freq_lsb和freq_msb各取 14 位,因为 AD9833 的频率寄存器是分两次写入的,每次 14 位有效数据,寄存器地址占高两位。phase_word取 12 位,对应 0 到 4095 的相位值。最后一步写控制寄存器时,0x2000表示 B28=1、RESET=0,同时默认选择 FREQ0 和 PHASE0。
四路独立调频的实现就是循环调用AD9833_Init,每路传入不同的freq_hz和phase_deg。如果你需要动态调频,比如做频率扫描,可以只重写频率寄存器,不需要重新走完整的复位流程。但要注意,在 B28=1 模式下,写频率需要两次 16 位操作,中间不能被其他通道的片选打断,否则频率字会错位。
3. 四路超声波驱动的功率级设计与阻抗匹配
3.1 AD9833 输出为什么不能直接接换能器
AD9833 的 DAC 输出阻抗大约 200 欧姆,输出幅度在 0.6Vpp 左右,负载能力很弱。超声波换能器在谐振频率附近的等效阻抗可能低到几欧姆到几十欧姆,直接连接的话,AD9833 输出会被拉垮,波形严重失真,甚至触发芯片内部的过流保护。我见过有人直接把换能器焊在 AD9833 输出引脚上,结果波形变成方波,换能器发烫,AD9833 也烫,这就是典型的阻抗不匹配。
正确的做法是在 AD9833 输出和换能器之间加两级:第一级是缓冲放大,用运放做电压跟随或者同相放大,把输出阻抗降下来;第二级是功率放大,用推挽或者 H 桥结构,提供足够的电流驱动能力。对于 40kHz 左右的超声换能器,驱动电压通常需要 10Vpp 到 30Vpp,具体看换能器的规格书。
3.2 推挽功率级与变压器匹配的实操参数
一个常见的四路超声波驱动功率级方案是:AD9833 输出经过 RC 低通滤波后,送入运放做 5 倍同相放大,然后驱动一对互补三极管组成的推挽级,推挽输出经过变压器升压和匹配,接到换能器。变压器在这里有两个作用:一是升压,把 12V 供电升到换能器需要的驱动电压;二是阻抗变换,把换能器的低阻抗变换到推挽级能接受的负载阻抗。
变压器的选型参数包括匝数比、初级电感量、漏感和饱和电流。对于 40kHz 的应用,初级电感量通常在几百微亨到几毫亨之间,匝数比根据供电电压和换能器驱动电压来定。假设供电 12V,推挽输出摆幅约 20Vpp,换能器需要 40Vpp,匝数比就是 1:2。初级电感量要保证在 40kHz 时感抗远大于推挽级的输出阻抗,一般取感抗为输出阻抗的 5 到 10 倍。
# 用 Python 快速计算变压器初级电感量和匝数比 # 假设推挽输出阻抗 50 欧姆,工作频率 40kHz python3 -c " import math f = 40000 Z_out = 50 L_primary = (Z_out * 5) / (2 * math.pi * f) print(f'初级电感量至少: {L_primary*1e6:.1f} uH') # 供电 12V,推挽摆幅约 20Vpp,换能器需要 40Vpp turns_ratio = 40 / 20 print(f'匝数比: 1:{turns_ratio:.1f}') "这个计算给出的是下限值,实际绕制时初级电感量可以取计算值的 1.5 到 2 倍,留出余量。匝数比 1:2 意味着次级匝数是初级的 2 倍。注意变压器的漏感会和换能器的容性负载形成谐振,这个谐振点要避开工作频率,否则会在某个频率上出现电压尖峰,烧毁推挽管。调试时用示波器看推挽输出和换能器两端的波形,如果看到明显的振铃,就是漏感和换能器电容在谐振,需要调整变压器绕法或者加 RC 吸收。
3.3 四路通道之间的串扰抑制
四路超声波驱动放在同一块板子上,通道之间的串扰是绕不开的问题。串扰的来源有三个:电源耦合、空间电磁耦合和地线回流耦合。电源耦合最容易被忽视,四路推挽级如果共用一条电源走线,某一路的电流突变会在走线电感上产生压降,影响其他路的供电电压。解决办法是每路推挽级单独走电源线,从电源入口处星型连接到每路,并且在每路的电源引脚附近放 100uF 电解电容和 100nF 陶瓷电容。
空间耦合主要来自变压器和换能器连线。四路变压器如果靠得太近,磁场会互相感应。我一般会把四路变压器排成一排,间距至少 10mm,并且变压器轴线互相垂直。换能器连线用双绞线或者屏蔽线,屏蔽层单端接地。
地线回流耦合是最隐蔽的。四路推挽级的电流都通过地线回流,如果地线走线有共享段,共享段的阻抗会把一路的电流波动耦合到另一路。建议用四路独立的地线走线,在电源入口处单点汇聚。如果板子空间允许,用四层板,单独一层做地平面,这个问题会小很多。
4. 四路 AD9833 驱动工程的避坑与排查
4.1 现象:某一路输出频率是设定值的两倍或一半
原因:频率寄存器写入时高低 14 位的顺序搞反了,或者 B28 位没有正确设置。AD9833 在 B28=1 时,频率寄存器分两次写入,先写低 14 位,再写高 14 位。如果顺序反了,频率字会变成原来的 2^14 倍或者 1/2^14,表现出来就是频率严重偏离。另一个可能是控制寄存器的 B28 位没有置 1,芯片工作在两次 14 位写入模式,但你的代码只写了一次 28 位。
解决:用逻辑分析仪抓 SPI 波形,确认每次 FSYNC 拉低后发送的 16 位数据里,高两位是寄存器地址,低 14 位是数据。对照 AD9833 数据手册的时序图,确认写入顺序。初始化时先写控制寄存器0x2100,这个值里 B28=1、RESET=1,然后再写频率寄存器。
4.2 现象:四路同时工作时,某一路波形幅度明显偏低
原因:SPI 总线冲突。四片 AD9833 共用 SCK 和 SDATA,如果某一片的 FSYNC 没有正确拉高,它会一直监听总线,把发给其他片的数据也移入自己的移位寄存器。更常见的情况是 FSYNC 拉高的时机不对,在 SCK 还在翻转时就拉高,导致最后一位数据没有正确锁存。
解决:检查每片 AD9833 的 FSYNC 引脚在空闲时是否确实为高电平。用示波器同时看 SCK 和 FSYNC,确认 FSYNC 拉低和拉高的边沿都在 SCK 空闲期间。如果用的是硬件 SPI,确认 NSS 引脚配置为软件管理,不要用硬件 NSS 输出。
4.3 现象:换能器发出声音但幅度不够,或者发热严重
原因:阻抗匹配没做好。换能器在谐振频率附近的阻抗是实数,偏离谐振点后变成复数,如果变压器匝数比是按谐振点阻抗设计的,偏离后功率传输效率会急剧下降。发热严重通常是推挽管工作在非线性区,或者变压器饱和。
解决:用阻抗分析仪测换能器在 20kHz 到 100kHz 范围内的阻抗曲线,找到谐振频率和反谐振频率。变压器匝数比按谐振点的阻抗来设计,并且在推挽输出串联一个小的限流电阻,防止换能器阻抗骤降时电流过大。如果变压器发热,检查初级电感量是否足够,或者供电电压是否超过了变压器的伏秒积。
4.4 现象:AD9833 输出有高频毛刺,换能器有啸叫
原因:AD9833 的 MCLK 时钟泄漏到了输出。AD9833 内部没有专门的时钟抑制电路,如果 MCLK 走线离输出引脚太近,或者电源去耦不充分,25MHz 的时钟会耦合到正弦输出上。换能器对高频分量虽然响应弱,但高频分量会在驱动级产生交调失真,听起来就是啸叫。
解决:在 AD9833 的 VDD 引脚旁边放 100nF 和 10uF 电容,越近越好。MCLK 走线用地线包围,远离输出引脚。在 AD9833 输出和运放输入之间加一个截止频率为 1MHz 左右的 RC 低通,把 25MHz 的泄漏衰减掉。如果毛刺仍然存在,检查晶振的负载电容是否匹配,晶振输出幅度是否过大。
4.5 现象:动态调频时输出频率跳变不连续
原因:在 B28=1 模式下,写频率寄存器需要两次 16 位操作,如果在这两次操作之间被其他中断打断,或者被其他通道的 SPI 操作插入,频率字会错位。表现出来就是频率跳变到错误的值,然后下一次写入又恢复。
解决:在写频率寄存器时关中断,或者用 DMA 把两次 16 位写入打包成一个传输序列。如果四路需要同时调频,建议用两个频率寄存器交替:先把新频率写到 FREQ1,然后切换控制寄存器的 FSELECT 位,从 FREQ0 切到 FREQ1。这样频率切换是原子操作,不会出现中间状态。
5. 用相位控制做四路超声波的波束合成与验证
四路超声波驱动如果只做到独立调频,那只是四个独立的信号源。真正体现这套方案价值的地方是相位控制。AD9833 的相位寄存器是 12 位的,分辨率是 360/4096 ≈ 0.088 度。四路之间的相位差可以精确设定,这就为波束合成提供了基础。
假设四个换能器排成一条直线,间距为 d,工作频率为 f,声速为 c。要让波束偏转角度 θ,相邻换能器之间的相位差应该是 Δφ = 2π × f × d × sin(θ) / c。用 AD9833 实现时,把 Δφ 换算成 12 位相位字,依次写入四路的相位寄存器。
import math def calc_phase_words(freq, spacing, angle_deg, sound_speed=343): """计算四路超声波波束合成所需的相位字""" angle_rad = math.radians(angle_deg) delta_phi = 2 * math.pi * freq * spacing * math.sin(angle_rad) / sound_speed phase_words = [] for i in range(4): phase_rad = i * delta_phi # 归一化到 0~2pi phase_rad = phase_rad % (2 * math.pi) # 转换为 12 位相位字 word = int(phase_rad / (2 * math.pi) * 4096) & 0x0FFF phase_words.append(word) return phase_words # 示例:40kHz,间距 4.3mm(半波长),偏转 30 度 words = calc_phase_words(40000, 0.0043, 30) print("四路相位字:", [hex(w) for w in words])这段代码计算的是均匀直线阵的相位分布。spacing取半波长是为了避免栅瓣,40kHz 在空气中的波长约 8.6mm,半波长就是 4.3mm。angle_deg是期望的波束偏转角度。计算出的phase_words直接写入四路 AD9833 的相位寄存器。
验证波束是否按预期偏转,最直接的方法是用麦克风在远场测量声压分布。把麦克风固定在距离阵列 1 米左右的圆弧上,每转 5 度记录一次声压幅值,画出方向图。如果方向图的主瓣指向设定角度,说明相位控制正确。如果没有条件做声学测量,可以用示波器看四路换能器两端的电压波形,确认相位差和计算值一致。
我自己的习惯是先在 Excel 里把相位字算好,生成一个查找表,MCU 里直接查表写入,避免在嵌入式端做浮点三角函数。这个表按角度每 5 度一个点,覆盖 -60 度到 +60 度,一共 25 组,每组 4 个 16 位值,占不了多少 Flash。调试的时候先用示波器确认四路相位关系,再上声学测量,这样能把电气问题和声学问题分开定位。希望帮到你。
本文还有配套的精品资源,点击获取