基于STM32与AD9959的AM信号模拟系统设计与实现
发布时间:2026/9/29 18:33:50 锦皓数字建站

1. 方案整体架构与设计思路拆解1.1 为什么选STM32加AD9959这套组合做无线AM信号模拟这个题目核心诉求其实就三条载波频率要稳、调制深度要能调、基带信号要干净。我前后试过几种方案最早用分立元件搭LC振荡器加变容二极管调幅频率漂移能飘到你怀疑人生手一靠近线圈频率就变根本没法做定量实验。后来考虑用FPGA直接做DDS性能确实好但开发周期长而且对于国赛这种时间紧、任务重的场景FPGA的调试成本太高了。最后锁定STM32加AD9959这套组合理由很实在。AD9959是ADI出的四通道DDS芯片每个通道最高能输出200MHz的模拟正弦波内部有14位DAC、32位频率调谐字、14位相位偏移字还有10位幅度控制字。关键是它支持多通道独立控制这意味着我可以一个通道出载波另一个通道出调制信号甚至还能留两个通道做后续扩展。STM32这边我选的是F103系列原因很简单资料多、生态好、价格便宜而且它的SPI接口速率足够驱动AD9959的寄存器写入。从系统架构上看整个方案分成三块STM32主控负责参数计算和SPI通信AD9959负责产生高频载波和调制信号模拟乘法器或者AD9959内部的幅度调制功能完成AM调制。这里有个细节值得说AD9959本身支持通过改变幅度控制字来实现AM调制但这种方式有个问题——幅度控制字的更新速率受SPI写入速度限制调制频率做不高。所以我最终采用的是外部模拟乘法器方案AD9959只负责产生纯净的载波和调制信号乘法器完成混频。这样调制频率可以轻松做到几十kHz完全满足AM广播的模拟需求。1.2 系统框图和信号链路设计整个信号链路是这样的STM32通过SPI配置AD9959的通道0输出高频载波比如1MHz通道1输出低频调制信号比如1kHz正弦波。两路信号分别经过低通滤波器和缓冲放大器后送入模拟乘法器AD835。AD835的输出就是标准的AM调幅信号表达式为u(t) Uc × [1 m × cos(2πfm t)] × cos(2πfc t)其中Uc是载波幅度m是调制深度fm是调制频率fc是载波频率。这个表达式就是AM调制的数学本质理解了它后面的参数计算就顺理成章了。调制深度m的控制是通过调节调制信号的幅度来实现的。AD9959的幅度控制字是10位范围0到1023对应输出幅度从0到满量程。假设载波通道幅度设为满量程调制通道幅度设为A那么调制深度m就等于A除以满量程值。比如调制通道幅度字设为512那m就是0.5也就是50%调制。这个计算关系很直接但在实际调试中要注意AD9959各通道之间的幅度一致性不同通道在同一幅度字下的实际输出幅度可能有几个百分点的差异需要实测校准。滤波器的设计也不能马虎。AD9959输出的是阶梯正弦波内部DAC采样率是500MSPS输出1MHz信号时镜像频率出现在499MHz、501MHz这些位置虽然离得很远但为了信号纯净度还是建议加一个截止频率在10MHz左右的LC低通滤波器。调制通道输出1kHz信号镜像频率在499kHz和501kHz用一个截止频率50kHz的有源低通滤波器就能滤得很干净。1.3 关键参数的计算与选型依据载波频率的选择要考虑两个因素一是要落在AM广播频段内中波广播是526.5kHz到1606.5kHz我选1MHz作为中心频率二是要方便后续的功率放大和天线匹配。1MHz这个频率波长300米四分之一波长天线要75米显然不现实所以实际测试时用环形天线或者直接电缆连接示波器观察波形。调制频率选1kHz这是音频范围内最典型的测试频率人耳最敏感也方便用示波器观察包络。调制深度从0%到100%连续可调这是AM信号模拟的基本要求。0%就是纯载波100%就是满调制超过100%就是过调制会产生包络失真。在实际操作中我建议把调制深度限制在90%以内留一点余量避免因器件温漂导致意外过调制。AD9959的频率调谐字计算公式是FTW (f_out × 2^32) / f_sysclk其中f_sysclk是系统时钟我用的25MHz晶振经过内部PLL倍频到500MHz。以1MHz输出为例FTW (1,000,000 × 4,294,967,296) / 500,000,000 8,589,934.592取整就是8,589,935。这个值写入通道0的频率调谐寄存器实际输出频率就是8,589,935 × 500,000,000 / 4,294,967,296 1,000,000.0006Hz误差在亚赫兹级别完全够用。2. 硬件电路设计与核心细节解析2.1 STM32最小系统与外围接口STM32F103C8T6最小系统板是这次的主力核心电路包括8MHz晶振、复位电路、SWD调试接口、3.3V稳压。这里有个坑要提醒AD9959的IO口是3.3V电平和STM32是匹配的但如果用5V的STM32板子必须加电平转换否则会烧AD9959的输入引脚。我第一版就是没注意这个直接烧了一片AD9959心疼了好几天。SPI接口我用的是SPI1引脚分配是PA5作为SCKPA7作为MOSIPA4作为CS片选PA3作为IO_UPDATE更新引脚PA2作为复位引脚。AD9959的SPI是四线制支持最高25MHz的SCK频率我实际跑在10MHz稳定可靠。这里要注意AD9959的SPI模式是CPOL0CPHA0也就是时钟空闲低电平数据在上升沿采样。STM32的SPI配置要对应设置否则数据写不进去。还有一个细节是IO_UPDATE引脚。AD9959的寄存器写入是双缓冲的数据先写到缓冲寄存器然后给IO_UPDATE一个上升沿数据才会真正生效。这个机制的好处是可以同时更新多个通道的参数保证相位一致性。我在初始化时先把所有通道的参数都写好最后统一给一个IO_UPDATE脉冲这样两个通道的输出是严格同步的。2.2 AD9959外围电路与滤波设计AD9959的外围电路有几个关键点。首先是参考时钟我用的是25MHz的无源晶振通过内部的PLL倍频到500MHz。晶振的负载电容要按规格书选一般是12pF到22pF我用的18pF实测频率稳定度在±10ppm以内。如果对频率精度要求更高可以换用温补晶振或者恒温晶振但成本会上去不少。DAC输出端需要接一个50Ω的电阻到地这是电流输出转电压输出的标准配置。AD9959的DAC满量程输出电流是可编程的通过寄存器设置范围是0到15mA。我设的是10mA配合50Ω电阻输出幅度就是500mV峰峰值。这个幅度直接送乘法器有点小所以后面加了一级宽带运放做缓冲放大用的是AD8009带宽足够噪声也低。低通滤波器的设计我用了五阶巴特沃斯LC滤波器截止频率10MHz。电感用0805封装的贴片电感电容用NPO材质的贴片电容保证温度稳定性。实测1MHz载波的二次谐波抑制比达到45dB三次谐波抑制比超过60dB波形非常干净。调制通道的滤波器用的是二阶有源低通截止频率50kHz运放用的是NE5532噪声低失真小。2.3 模拟乘法器AD835的调制实现AD835是一片四象限模拟乘法器带宽250MHz输出表达式是W XY/U Z其中U是缩放系数典型值1V。我把载波信号接到X输入调制信号接到Y输入Z输入接地输出就是两路信号的乘积。但这里有个问题标准的AM调制是(1m×cos)形式的而乘法器直接输出的是cos×cos缺少直流分量。解决办法是在调制信号上叠加一个直流偏置。具体做法是在AD835的Y输入端加一个加法电路把调制信号和一个可调的直流电压相加。直流电压的大小决定了调制深度当直流电压等于调制信号幅度时就是100%调制。这个直流电压我用STM32的DAC输出控制通过一个运放缓冲后送到加法电路这样就能用软件精确控制调制深度了。AD835的输入幅度范围是±1V超过会失真。所以载波和调制信号在送入乘法器之前都要做幅度调整。我用了两级运放第一级做缓冲第二级做可调增益放大增益通过数字电位器控制这样就能在软件里调节输出幅度了。实测下来这套电路在调制深度10%到90%范围内包络失真度小于3%完全满足实验要求。3. 软件实现与核心环节操作3.1 STM32的SPI驱动与AD9959寄存器配置STM32的SPI驱动我用的是标准库配置成主机模式8位数据宽度时钟极性低时钟相位第一边沿。SPI速率分频到10MHz实际用示波器抓过波形SCK的上升沿和MOSI的数据建立时间都满足AD9959的时序要求。这里有个小技巧在SPI初始化之后先发几个空字节让时钟线稳定下来再开始正式的寄存器写入可以避免第一次写入失败的问题。AD9959的寄存器写入格式是先发一个字节的地址地址的最高位表示是读还是写0表示写1表示读。然后是数据数据长度取决于寄存器有1字节、2字节、4字节几种。比如频率调谐寄存器是4字节幅度控制寄存器是2字节。我封装了一个函数AD9959_WriteReg(uint8_t addr, uint32_t data, uint8_t len)根据len参数决定发几个字节的数据。初始化流程是这样的先给复位引脚一个高脉冲复位AD9959然后配置系统时钟设置PLL倍频系数为20使系统时钟达到500MHz接着配置通道0和通道1的频率调谐字、相位偏移字、幅度控制字最后给IO_UPDATE一个脉冲让所有配置生效。整个过程大概需要几十毫秒上电后延时100ms再开始初始化确保电源稳定。3.2 频率调谐字与幅度控制字的计算实现频率调谐字的计算前面已经说了公式在代码里我用的是浮点运算然后强制转换为uint32_t。这里要注意精度问题STM32F103没有硬件浮点单元浮点运算是软件模拟的速度慢但精度够。计算一次大概几十微秒对于参数更新来说完全够用。幅度控制字的计算稍微复杂一点。AD9959的幅度控制是10位的但实际输出幅度和幅度字之间不是严格的线性关系在低幅度区域有非线性。我实测了一组数据幅度字从0到1023每隔64测一个点用示波器记录输出幅度然后做了一个查表加线性插值的方案。这样在需要精确控制调制深度的时候可以查表得到更准确的幅度字。调制深度的控制逻辑是这样的用户设定一个目标调制深度m范围0到1。我先计算出调制通道需要的输出幅度A m × A_full其中A_full是调制通道满量程输出幅度。然后根据A查表得到对应的幅度控制字写入AD9959。同时直流偏置电压也要相应调整保证在调制信号为零时乘法器输出为纯载波。这个联动关系在代码里用一个函数SetModulationDepth(float m)封装调用一次就能完成所有参数的更新。3.3 上位机通信与参数实时调节为了方便调试我加了一个串口通信功能。STM32的USART1配置成115200波特率接收上位机发来的命令。命令格式很简单一个字节的命令码后面跟参数。比如0x01表示设置载波频率后面跟4字节的频率值0x02表示设置调制频率后面跟4字节0x03表示设置调制深度后面跟1字节的百分比值。上位机我用Python写了一个简单的GUI用tkinter做界面串口通信用pyserial库。界面上有载波频率、调制频率、调制深度三个滑动条拖动滑动条就实时发送命令给STM32。实测下来从拖动滑动条到AD9959输出变化延迟在50ms以内操作手感很流畅。这个上位机虽然简单但在调试阶段帮了大忙不用每次都重新编译下载程序。串口接收我用的是中断方式每收到一个字节就进一次中断存到缓冲区里。主循环里检查缓冲区如果收到完整的一帧命令就解析执行。这里要注意帧同步问题如果上位机发送过程中被打断可能会造成帧错位。我的解决办法是在命令码前面加一个帧头0xAA收到0xAA才开始记录收到完整帧后校验和验证校验失败就丢弃重来。这个机制在实际使用中很可靠没出现过误动作。4. 调试过程与常见问题排查4.1 AD9959不出波形或波形异常的排查第一次上电AD9959没有输出示波器上只有一条直线。排查过程是这样的先用万用表量AD9959的电源引脚3.3V正常再量参考时钟引脚用示波器看25MHz晶振有没有起振结果发现晶振没起振。检查电路发现晶振的负载电容焊反了换过来之后晶振正常起振但AD9959还是没有输出。接着检查SPI通信用逻辑分析仪抓SCK和MOSI的波形发现STM32确实在发数据但AD9959没有响应。量了一下CS片选信号发现CS一直处于高电平没有拉低。查代码发现是GPIO初始化的时候CS引脚配置成了输入模式改成推挽输出之后CS正常拉低AD9959开始有波形输出了。但波形幅度很小只有几十毫伏。检查DAC输出端的50Ω电阻发现焊成了1kΩ换回50Ω之后幅度正常。这个问题的教训是AD9959的DAC是电流输出必须接低阻值的电阻做I/V转换阻值太大会导致输出幅度严重下降。4.2 调制包络失真与载波泄漏问题调制功能调通之后发现包络波形有失真正半周和负半周不对称。用示波器看乘法器的输入信号发现调制信号上叠加的直流偏置有纹波纹波频率是50Hz明显是电源干扰。在直流偏置电路上加了一个100μF的电解电容和一个0.1μF的陶瓷电容做退耦纹波从50mV降到了5mV以下包络失真明显改善。另一个问题是载波泄漏。当调制深度设为0时理论上输出应该是纯载波但实测发现包络有轻微的起伏说明有调制信号泄漏到了输出。检查电路发现是乘法器的Y输入端有直流偏移导致即使调制信号为零Y端也不是严格的零电压。在Y输入端加了一个隔直电容同时用运放做一个伺服环路把Y端的直流电平自动调零载波泄漏降到了-60dB以下基本看不出来了。4.3 常见问题速查表问题现象可能原因排查方法解决办法无输出波形晶振未起振示波器测晶振引脚检查负载电容更换晶振无输出波形CS片选未拉低逻辑分析仪抓CS信号检查GPIO配置改为推挽输出输出幅度小DAC负载电阻过大万用表测电阻值更换为50Ω电阻包络失真电源纹波干扰示波器看直流偏置加退耦电容载波泄漏乘法器输入偏移测Y输入端直流电平加隔直电容和伺服环路频率偏差大晶振精度不够频率计测输出频率更换高精度晶振SPI写入失败时序不匹配逻辑分析仪抓SPI波形调整CPOL和CPHA设置4.4 实操心得与避坑经验第一个心得是关于PCB布局的。AD9959是高速器件系统时钟500MHzPCB布局不好会严重影响性能。我的经验是AD9959的电源引脚旁边一定要放0.1μF和10μF的电容而且越近越好晶振要尽量靠近AD9959的时钟输入引脚走线要短且直SPI信号线要远离模拟输出线避免数字噪声耦合到模拟信号上。我第一版PCB没注意这些输出信号上叠加了明显的数字噪声第二版重新布局之后噪声降低了20dB以上。第二个心得是关于调试顺序的。不要一上来就调调制功能先把载波调通确认频率准确、幅度正常、波形干净然后再调调制通道确认调制信号的频率和幅度都对最后再调乘法器观察包络波形。这个顺序可以快速定位问题如果载波都没调好就去调调制出了问题根本不知道是哪一级的问题。第三个心得是关于参数保存的。调试过程中会频繁修改频率、幅度这些参数如果每次都要重新编译下载效率太低。我加了一个Flash保存功能把当前参数存到STM32的内部Flash里上电自动加载。这样调试的时候只需要通过串口改参数改好了存一下下次上电就是上次的状态省了很多时间。5. 系统测试与性能评估5.1 载波频率准确度与稳定度测试载波频率的测试我用的是频率计型号是安捷伦53131A闸门时间设1秒。设定输出1MHz实测频率是999,999.8Hz误差0.2Hz相对误差2×10^-7这个精度对于AM广播模拟来说绰绰有余。频率稳定度方面连续观察1小时频率漂移在±0.5Hz以内主要来自晶振的温度漂移。如果对稳定度要求更高可以把晶振换成温补晶振漂移可以降到±0.1Hz以内。频率切换速度也测了一下。从STM32发出频率更新命令到AD9959输出稳定在新频率用示波器测切换时间大概在200μs左右。这个速度对于大多数应用来说够用了但如果要做跳频通信模拟可能还需要优化。优化的方向是减少SPI写入的数据量只更新频率调谐寄存器不更新其他寄存器切换时间可以缩短到100μs以内。5.2 调制深度线性度与失真度测试调制深度的线性度测试是这样做的设定调制深度从10%到90%每隔10%测一个点用示波器测量包络的最大值和最小值计算出实际调制深度。实测数据如下表设定调制深度实测调制深度误差10%10.3%0.3%20%20.5%0.5%30%30.2%0.2%40%40.8%0.8%50%50.4%0.4%60%60.6%0.6%70%70.3%0.3%80%80.9%0.9%90%90.5%0.5%从数据看线性度还是不错的最大误差0.9%主要来自AD9959幅度控制字的非线性。如果用查表法校准误差可以降到0.3%以内。失真度测试用的是音频分析仪测包络信号的THD。在调制深度50%、调制频率1kHz的条件下THD实测为1.2%主要失真是二次谐波。这个失真度对于AM广播模拟来说是可以接受的如果要求更高可以在乘法器输出端加一个二阶低通滤波器把二次谐波滤掉THD可以降到0.5%以下。5.3 系统功耗与散热评估整个系统的功耗测了一下STM32最小系统板大概50mAAD9959在500MHz系统时钟下大概350mA乘法器和运放大概100mA总共500mA左右用USB供电就够了。发热方面AD9959是主要热源连续工作1小时后芯片表面温度大概45度用手摸是温的不烫手。如果环境温度高建议加一个小散热片或者降低系统时钟频率到400MHz功耗可以降20%左右。6. 方案扩展与后续优化方向6.1 增加数字调制功能现在做的是模拟AM调制如果要做数字调制比如ASK、FSK这套硬件基本不用改只需要改软件。ASK就是把调制信号换成数字基带信号控制乘法器的Y输入在0和1之间切换FSK就是控制AD9959的频率调谐字在两个频率之间切换。AD9959支持频率扫描功能可以自动在两个频率之间来回切换不需要STM32干预效率更高。6.2 提高调制频率上限现在调制频率上限受限于乘法器的带宽和滤波器的截止频率大概在50kHz左右。如果要做到几百kHz甚至上MHz的调制频率需要换更高速的乘法器比如AD834带宽500MHz滤波器也要相应调整截止频率提高到1MHz以上。但这样会带来新的问题高频调制时载波和边带之间的间隔很小滤波器的过渡带要做得非常陡可能需要用高阶椭圆滤波器。6.3 加入自动增益控制现在输出幅度是手动调节的如果要做自动增益控制可以在输出端加一个检波电路检测输出幅度反馈到STM32的ADCSTM32根据ADC值调整AD9959的幅度控制字形成闭环。这样即使负载变化或者温度漂移输出幅度也能保持稳定。这个功能在需要精确控制输出功率的场景下很有用。6.4 用STM32的DMA提高SPI效率现在SPI写入是阻塞式的每次写寄存器都要等SPI传输完成CPU利用率不高。如果要用STM32做更复杂的实时控制比如根据外部信号实时改变调制参数可以用DMA来搬运SPI数据CPU只需要准备好数据缓冲区DMA自动完成传输CPU可以腾出来做其他事情。这个优化对于提高系统响应速度很有帮助。7. 个人实操体会与建议这个项目从开始到调通前后花了大概三周时间其中大部分时间花在硬件调试上。软件部分反而比较顺利因为AD9959的寄存器配置逻辑很清晰只要时序对了基本一次就能通。硬件部分坑比较多主要是电源和布局的问题这两个方面如果一开始就注意可以省很多时间。对于准备做类似项目的朋友我的建议是第一先把AD9959的数据手册仔细看一遍特别是时序图和寄存器说明这两部分看懂了后面就顺了第二PCB布局一定要认真对待高速器件的布局不好后面调试会非常痛苦第三调试要分模块进行先调通载波再调调制最后调乘法器不要跳步第四善用逻辑分析仪和示波器这两个工具在调试SPI和模拟信号时不可或缺。最后分享一个小技巧AD9959的IO_UPDATE引脚在更新多个通道时非常关键如果发现某个通道的参数没生效先检查IO_UPDATE有没有给脉冲。我遇到过好几次参数写进去了但输出没变查了半天发现是IO_UPDATE忘了给。后来我在代码里把IO_UPDATE操作封装在写寄存器的函数里每次写完自动给脉冲就再也没出过这个问题。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。