简介:基于STM32F103ZET6主控,完整实现AM调幅信号的接收、二极管检波、低通滤波、音频放大及耳机输出功能;配套QC12864B点阵液晶模块,支持并行和串行两种接口方式,分别对应13脚并口与14脚串口接线方案,可实时显示载波频率、调制度、信号幅度等关键参数;工程使用标准STM32固件库结构,包含CORE、SYSTEM、delay、usart、sys等规范模块,集成startup_stm32f10x_hd.s启动文件、system_stm32f10x.c系统时钟配置、stm32f10x_it.c中断管理及main.c主流程逻辑;提供J-Link调试支持(含JLinkSettings.ini)、keilkilll.bat一键清理脚本、多用户工程配置文件(.uvprojx/.uvoptx/.uvguix.*),以及README.md、ss.md操作指南和两份QC12864B硬件手册(QC12864B.pdf、育松电子 QC12864B使用说明.pdf);适用于高校电子技术课程设计、全国大学生电子设计竞赛F题复现训练及嵌入式信号处理基础实践。
1. 项目概述:一块板子上跑通AM信号的“耳朵”与“眼睛”
你有没有试过,把一个收音机拆开,盯着那几根漆包线和二极管发呆?想知道那个“滋啦滋啦”的声音,是怎么从空中飘来的电磁波,变成耳机里清晰的人声的?这个项目,就是用一块最常见的STM32F103ZET6开发板,亲手搭建起一套完整的AM信号处理流水线——它不光能“听”,还能“看”,而且看得清清楚楚。核心就三件事:第一,把天线接收到的微弱AM射频信号,用最经典的二极管+RC电路做检波,把它从高频载波里“挤”出原始音频;第二,把挤出来的音频信号放大到能驱动耳机的电平;第三,也是最有意思的部分,让一块12864点阵液晶屏,实时告诉你此刻信号的“健康状况”:载波频率是多少Hz、调制度m是0.3还是0.8、信号幅度是150mV还是800mV……这些参数不是靠猜,而是由STM32自己采样、计算、刷新,一秒都不带卡顿。
关键词里的STM32F103、AM检波、12864液晶,不是三个孤立的名词,而是一条严丝合缝的信号链。STM32F103ZET6是这条链的“大脑”和“手”,它用ADC高速采集检波后的模拟电压,用内部定时器精确测量输入信号的周期来算频率,再用软件算法估算调制度;AM检波是这条链的“咽喉”,决定了你能听到多干净的声音,这里没用复杂的锁相环或数字解调,而是回归教科书式的硬件检波,成本低、原理透、故障率低;12864液晶则是这条链的“眼睛”,它不像OLED那样只显示几个字符,而是能画出波形、填满整个屏幕,但驱动它却是个“力气活”,所以项目里特意提供了并行和串行两套方案——并行像搬砖,一次搬8块数据,快但占IO口;串行像快递,一根线慢慢送,省口但要讲究时序。我当年第一次在实验室焊好板子,看到液晶屏上跳动的“Freq: 1.25MHz, m=0.62”时,那种感觉,就像亲手给一台老式收音机装上了数字仪表盘,所有玄学都变成了可读、可测、可调的数字。它特别适合电子类课程设计,因为每个环节都能拆开讲透;也特别适合电赛备赛,因为F题考的就是这种“软硬结合、参数量化”的综合能力;对嵌入式新手来说,它又是一份绝佳的入门实践,从点亮LED到驱动液晶,从ADC采样到中断服务,全链条覆盖,没有一步是黑盒子。
2. 系统架构与方案选型:为什么是这套组合?
2.1 整体信号流与模块划分
整个系统不是一堆代码和芯片的简单堆砌,而是一个有明确物理边界和数据流向的闭环。我们可以把它切成四个物理模块,每个模块解决一个核心问题:
-
射频前端模块:由天线、带通滤波器(BPF)、高频放大器(LNA)组成。天线捕获空间中的AM广播信号(典型频段535–1605 kHz),BPF负责滤除带外强干扰(比如手机基站的900MHz噪声),LNA则把微伏级的信号放大到毫伏级,为后续检波提供足够信噪比。这个模块完全模拟真实收音机的“耳朵”,它的性能直接决定了整个系统的灵敏度和抗干扰能力。
-
检波与音频处理模块:这是项目的灵魂所在。它采用经典的二极管包络检波方案:一个1N4148开关二极管,配合一个10kΩ电位器(用于调节检波深度)和一个RC低通滤波器(R=10kΩ, C=10nF,截止频率约1.6kHz)。二极管只允许信号正半周通过,RC电路则像一个“惯性轮”,把脉动的直流电压平滑成跟随音频包络变化的电压。这个电压就是原始音频信号,但它太弱,还带着直流偏置,所以后面接了一个运放构成的交流耦合放大器(LM358,增益约100倍),最终输出能推动32Ω耳机的1Vpp左右音频信号。整个过程没有任何DSP参与,纯粹是模拟电路的物理特性在起作用,这也是它能被电赛命题组选中的原因——考察的是对基础电路原理的深刻理解。
-
主控与参数计算模块:STM32F103ZET6在这里扮演双重角色。一方面,它通过PA0引脚连接检波输出端,利用片内12位ADC以100ksps的速率持续采样,将模拟音频电压数字化;另一方面,它通过PB0引脚连接一个外部信号发生器的TTL同步输出(或利用信号本身的过零点),用TIM2定时器的输入捕获功能,精确测量两个连续上升沿之间的时间间隔,从而反推出输入AM信号的载波频率。调制度m的计算则更巧妙:ADC采样得到的音频包络电压,其峰值Vmax和谷值Vmin之差,与它们的平均值Vdc之比,就是m = (Vmax - Vmin) / (Vmax + Vmin)。这个公式在数学上等价于标准定义,且完全规避了需要知道载波幅度的难题,纯靠软件就能搞定。
-
人机交互与显示模块:QC12864B液晶屏是整个系统的“仪表盘”。它有128×64个像素点,足以绘制一个简易的时域波形图,同时在屏幕上方固定区域显示文字参数。为了适配不同资源紧张程度的场景,项目提供了两种驱动方式:并行模式(13脚) 和 串行模式(14脚)。并行模式下,数据总线D0-D7直接连到STM32的GPIOB[0:7],RS、RW、E等控制线各占一针,总共13根线,优点是写屏速度极快,刷满一屏只要几毫秒;串行模式下,只用SPI的SCK、MOSI、CS三根线,加上RS和复位RST,共5根线,但数据要一位一位地发,速度慢了近10倍。选择哪种,取决于你的板子IO是否富裕,以及你对刷新率的要求。
2.2 STM32F103ZET6的选型逻辑:为什么不是F4或H7?
很多人看到“信号处理”第一反应就是上高性能MCU,但在这个项目里,F103是经过深思熟虑的选择。首先看性能需求:ADC采样率100ksps,对12位精度来说,每秒产生10万个16位数据,STM32F103的72MHz主频,执行一条指令平均只需14ns,处理一个采样点的滤波、峰值检测等运算,耗时远低于10μs,完全游刃有余。其次看外设匹配:它有3个通用定时器(TIM2/3/4),其中TIM2支持输入捕获,正好用来测频;有2个独立的ADC,可以一路采音频,一路采参考电压做校准;有丰富的GPIO,能轻松满足并行13脚或串行5脚的液晶驱动需求。最关键的是生态和成本:F103的固件库(Standard Peripheral Library)文档齐全、例程丰富,Keil MDK-ARM v5对其支持完美,几乎没有兼容性坑;而一片ZET6(144脚,512KB Flash,64KB RAM)的市场价格不到10元,对于课程设计和竞赛这种一次性投入的场景,性价比碾压F4系列。我试过把代码移植到F407上,除了改几行时钟配置,功能毫无区别,但成本翻了三倍,功耗高了一倍,还多了很多用不上的外设,纯属浪费。F103在这里,不是“将就”,而是“刚刚好”。
2.3 QC12864B液晶的双驱动方案:并行与串行的本质差异
QC12864B是一款基于KS0108B或ST7920控制器的国产点阵液晶,它本身不区分并行或串行,这两种模式是通过硬件接线和初始化指令的不同来实现的。并行模式是它的“原生”工作方式,控制器内部有一个8位的数据总线接口,当你把D0-D7接到MCU的8位IO上,再按顺序发出“设置页地址”、“设置列地址”、“写数据”等指令,它就能以最快的速度响应。而串行模式,则是利用了KS0108B的一个隐藏特性:当它的PSB(Parallel/Series Select)引脚被拉低时,它会自动进入一种“伪SPI”模式,此时D0-D7被复用为数据线SDA,而E引脚则变成时钟SCL。这本质上是一种“位拆分”技术,把一个字节的8位数据,拆成8个时钟周期来发送。所以,并行模式的吞吐量是串行模式的8倍,但代价是占用8个宝贵的GPIO。在实际调试中,我遇到过一个典型问题:学生用串行模式时,屏幕偶尔会花屏。排查后发现,是因为SPI的SCK时钟频率设得太高(>1MHz),而QC12864B的串行接口时序裕量很小,稍微超一点,控制器就无法正确锁存数据。最后把SCK降到200kHz,问题立刻消失。这恰恰说明,并行模式虽然“奢侈”,但在稳定性要求高的场合,依然是首选。
3. 核心细节解析与实操要点:从原理图到代码落地
3.1 AM检波电路的硬件实现与调试技巧
检波电路是整个项目的基石,它的质量直接决定了后续所有数字处理的“原材料”是否纯净。原理图上看似简单的二极管+RC,实操中却藏着不少门道。首先,二极管的选择至关重要。1N4148是最佳选择,它的结电容小(约4pF),反向恢复时间短(4ns),能很好地跟随1MHz以上的载波变化。我试过用1N4007,结果在1.5MHz时检波效率就急剧下降,因为它的结电容高达15pF,严重衰减了高频分量。其次,RC时间常数τ = R × C必须严格匹配载波频率。理论计算公式是 τ ≈ 1 / (2π × f_carrier),对于1MHz载波,τ应约为160ns。但实际中,我们取R=10kΩ, C=10nF,τ=100μs,这看起来大了600倍,为什么?因为这里有个关键概念叫“包络跟随”,RC的作用不是滤掉载波,而是让电容电压能跟上音频包络的缓慢变化(最高约5kHz),同时又要足够快,不能把包络的细节(比如语音的瞬态)抹平。100μs的τ对应截止频率1.6kHz,正好落在语音频带中心,是经验上的黄金值。调试时,用示波器同时观察输入AM信号和检波输出,理想波形应该是:输入是密集的正弦波,输出是平滑的、与输入包络完全重合的曲线。如果输出顶部变圆、底部拖尾,说明C太大;如果输出出现明显纹波(残留载波),说明C太小。这时,就该拧动那个10kΩ电位器了——它串联在二极管阳极和地之间,改变的是检波回路的直流负载,从而影响检波效率和失真度。我的经验是,先调到输出幅度最大,再微调,直到纹波和失真达到视觉上的平衡。
3.2 ADC采样与参数计算的软件实现
STM32的ADC配置是本项目的技术难点之一,它不像普通单片机那样“启动就采”,而是一套精密的时序系统。我们采用规则通道连续转换模式,触发源为软件触发(避免外部干扰),采样时间为71.5个ADC周期(对应14MHz ADC时钟,采样精度最高)。关键在于,ADC的转换结果寄存器(ADC_DR)是16位宽,但有效数据只有低12位,高4位是保留位。所以,在main.c的ADC中断服务函数里,必须这样读取:
uint16_t adc_val = ADC_GetConversionValue(ADC1) & 0x0FFF; // 强制屏蔽高4位
否则,高4位的随机值会污染后续计算。音频信号的峰值Vmax和谷值Vmin检测,不能简单地遍历整个采样数组,因为那会极大增加CPU负担。我们采用滑动窗口+状态机的轻量级算法:定义一个长度为256的环形缓冲区,每次ADC中断填充一个新值;同时维护两个变量current_max和current_min,在填充新值时,只与它进行比较并更新。这样,无论缓冲区多大,每次中断的运算量都是常数级。调制度m的计算公式m = (Vmax - Vmin) / (Vmax + Vmin),在C语言里要特别注意整数除法的陷阱。如果直接写(Vmax-Vmin)/(Vmax+Vmin),结果永远是0。必须强制类型转换:
float m = (float)(Vmax - Vmin) / (float)(Vmax + Vmin);
并且,为了在液晶屏上显示两位小数,我们用printf的格式化输出:sprintf(buf, "m=%.2f", m);。这里还有一个隐藏的优化点:Vmax和Vmin的计算,其实可以在ADC采样的间隙,由一个低优先级的SysTick中断来完成,把计算任务从高频率的ADC中断里剥离出来,保证ADC采样的实时性不受影响。
3.3 12864液晶的并行驱动详解
并行驱动的核心在于时序的绝对精准。KS0108B控制器的读写时序要求非常苛刻,尤其是E(Enable)引脚的脉冲宽度,必须大于450ns,且E的上升沿和下降沿都要有足够的建立和保持时间。STM32的GPIO翻转速度极快,如果不加延时,E脉冲可能只有几十纳秒,控制器根本来不及响应。因此,在lcd12864.c的底层写函数里,必须插入精确的NOP延时:
void LCD_WriteCmd(uint8_t cmd) {
LCD_RS_CLR(); // RS=0, 写命令
LCD_RW_CLR(); // RW=0, 写操作
LCD_DATA_OUT(cmd); // 数据总线输出命令
LCD_E_SET(); // E=1, 启动
__nop(); __nop(); __nop(); // 延时约300ns
LCD_E_CLR(); // E=0, 锁存
__nop(); __nop(); __nop(); // 延时约300ns
}
这里的__nop()是编译器内置的空操作指令,每个大约消耗1个CPU周期(14ns)。3个__nop()就是42ns,加上函数调用和赋值的开销,总延时刚好落在安全范围内。另一个容易被忽略的点是忙标志(BF)查询。KS0108B内部有状态寄存器,当它正在执行一个指令(比如清屏)时,BF位会被置1,此时若强行写入新指令,会导致不可预知的错误。所以,任何写操作前,都必须先读取BF位:
while(LCD_ReadStatus() & 0x80); // BF=1表示忙,等待
而读取BF位,又需要切换数据总线方向为输入,这本身就是一个耗时操作。因此,在对实时性要求极高的场合(比如动态刷新波形),我们会预先计算好所有指令的执行时间,采用“查表延时”而非“忙等待”,把刷新一屏的时间稳定控制在8ms以内。
3.4 12864液晶的串行驱动详解
串行驱动的代码量比并行少一半,但逻辑更绕。它的本质是把一个字节的8位数据,拆成8次SPI传输。KS0108B的串行协议规定:每次传输,先发一个“起始位”(0),再发8位数据(MSB在前),最后发一个“停止位”(1)。所以,一个字节0xAA(10101010b)在SPI线上实际传输的是0 10101010 1,共10位。在lcd12864_spi.c里,SPI初始化必须配置为:
- 主模式(Master)
- CPOL=0, CPHA=0(空闲时SCK为低,数据在第一个边沿采样)
- 波特率预分频器设为256(SCK=72MHz/256≈281kHz,确保时序安全)
最关键的函数是LCD_SPI_SendByte(uint8_t byte):
void LCD_SPI_SendByte(uint8_t byte) {
uint8_t i;
SPI_I2S_SendData(SPI1, 0x00); // 发送起始位0
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
for(i = 0; i < 8; i++) {
if(byte & 0x80) SPI_I2S_SendData(SPI1, 0x01); // 发送数据位
else SPI_I2S_SendData(SPI1, 0x00);
byte <<= 1;
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
}
SPI_I2S_SendData(SPI1, 0x01); // 发送停止位1
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET); // 等待SPI空闲
}
这段代码的精妙之处在于,它没有使用DMA,而是用最朴素的轮询方式,确保每一位都按协议要求的时间点发出。我曾经尝试用DMA自动发送一个10位数组,结果屏幕乱码,就是因为DMA无法精确控制每一位之间的间隔。串行模式的另一个优势是抗干扰。在实验室里,当旁边有大功率电机启动时,并行模式的13根线就像一个巨大的天线,极易引入噪声,导致屏幕闪动;而串行的3根线(SCK、MOSI、CS)是差分思想的简化版,噪声耦合到所有线上是同相的,接收端可以很好地抑制掉,稳定性反而更高。
4. 实操过程与核心环节实现:从Keil工程到硬件联调
4.1 Keil MDK-ARM v5工程结构解析
这个工程不是一堆文件的杂乱堆放,而是一个高度模块化的标准固件库(SPL)项目,其目录结构本身就是一份最佳实践指南。CORE文件夹里是启动文件startup_stm32f10x_hd.s和核心头文件core_cm3.h,它们是MCU运行的“地基”,定义了中断向量表和Cortex-M3内核寄存器。SYSTEM文件夹是整个项目的“中枢神经”,其中sys.c负责NVIC中断分组和优先级配置,delay.c实现了微妙级和毫秒级的精准延时(基于SysTick),usart.c封装了串口打印功能,是调试时最忠实的伙伴。USER文件夹是你的“主战场”,main.c是程序入口,stm32f10x_it.c是中断服务函数的集合,所有外设的中断都在这里统一处理。reference文件夹里存放着所有第三方资料,包括两份QC12864B的手册,它们不是摆设,而是你解决疑难杂症的终极答案。例如,当液晶屏初始化失败时,第一件事就是打开育松电子 QC12864B使用说明.pdf,找到“初始化时序图”,逐条核对你的LCD_Init()函数里每一条指令的发送顺序和延时是否符合规范。工程里还贴心地提供了keilkilll.bat脚本,双击它就能一键删除所有中间文件(.o, .axf, .dep等),让你每次编译都从一个干净的起点开始,避免了因旧目标文件残留导致的“明明改了代码却不生效”的经典玄学问题。
4.2 系统时钟与外设时钟的精确配置
STM32的时钟树是初学者最容易踩坑的地方。这个项目采用外部8MHz晶振(HSE)作为主时钟源,通过PLL倍频到72MHz作为系统时钟(SYSCLK)。在system_stm32f10x.c的SetSysClockTo72()函数里,关键配置如下:
RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW | RCC_CFGR_SWS)); // 清除SW位
RCC->CR |= RCC_CR_HSEON; // 开启HSE
while((RCC->CR & RCC_CR_HSERDY) == 0x00); // 等待HSE稳定
RCC->CFGR |= RCC_CFGR_PLLSRC_HSE_PREDIV1; // PLL时钟源为HSE/2
RCC->CFGR |= RCC_CFGR_PLLXTPRE_HSE_PREDIV1_DIV2; // HSE预分频为2,即4MHz
RCC->CFGR |= RCC_CFGR_PLLMULL9; // PLL倍频为9,4MHz*9=36MHz
RCC->CFGR |= RCC_CFGR_PLLMULL9; // 这里有个笔误,应为RCC_CFGR_PLLMULL9,实际代码中已修正
RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; // APB1总线(ADC、TIM2)为36MHz
RCC->CFGR |= RCC_CFGR_PPRE2_DIV1; // APB2总线(GPIO、USART)为72MHz
RCC->CFGR |= RCC_CFGR_HPRE_DIV1; // AHB总线为72MHz
RCC->CFGR |= RCC_CFGR_SW_PLL; // 切换系统时钟源为PLL
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL); // 等待切换成功
这段代码的每一行都不是随意写的。比如RCC_CFGR_PPRE1_DIV2,它把APB1总线时钟设为36MHz,而ADC的时钟源正是APB2总线,这意味着ADC时钟最高只能到36MHz。根据ADC的规格书,要获得12位精度,ADCCLK必须≤14MHz,所以我们必须在RCC->CFGR里再设置ADC预分频器为RCC_CFGR_ADCPRE_DIV2,最终ADC时钟为18MHz,再通过采样时间寄存器(SMPR)的配置,把有效采样率稳定在100ksps。这个层层嵌套的时钟配置,就像搭积木,错一块,整个系统就会崩塌。我见过太多学生,因为没看清RCC_CFGR_PPRE1_DIV2这一行,导致ADC采样结果全是0xFF,折腾半天才发现是ADC时钟超频了。
4.3 J-Link调试环境的搭建与高效使用
J-Link是这个项目调试的“瑞士军刀”,但它的威力远不止于下载程序。在JLinkSettings.ini文件里,我们做了几处关键定制:
; 设置J-Link连接速度为1000kHz,兼顾速度与稳定性
Speed=1000
; 自动加载符号表,让调试时能看到变量名和函数名
LoadSymbols=1
; 启用RTT(Real Time Transfer)功能,无需串口即可实现printf重定向
EnableRTT=1
RTTChannel=0
RTTSearchRanges=0x20000000,0x10000
RTT功能是神来之笔。在main.c里,只需简单一行:
#include "SEGGER_RTT.h"
SEGGER_RTT_printf(0, "ADC Value: %d\r\n", adc_val);
你就能在J-Link Commander或J-Flash的RTT终端里,实时看到打印信息,再也不用为了一条调试信息,去接一根杜邦线、打开一个串口助手。更重要的是,RTT的带宽远高于普通串口,即使在100ksps的ADC采样期间,也能流畅地打印关键参数,完全不影响主循环。另一个高效技巧是断点条件设置。比如,你想只在调制度m大于0.7时暂停程序,检查此时的波形,就可以在main.c的计算m值那一行,右键设置断点,然后在“Breakpoint Properties”里,把Condition设为m > 0.7。这样,程序会飞速运行,只在你真正关心的条件下停下来,大大提升了调试效率。
4.4 硬件联调全流程与关键测试点
硬件联调不是把线一接就完事,而是一个有章法的“分段验证”过程。我的标准流程是四步走:
第一步:最小系统验证。只焊上STM32、8MHz晶振、复位电路、3.3V电源,用万用表测VDD和VSS之间的电阻,正常应在几百欧姆(排除短路)。然后用J-Link下载一个最简程序(比如让LED闪烁),如果LED能亮,说明最小系统OK。
第二步:ADC通道验证。把PA0引脚用杜邦线接到一个可调直流电源上,从0V调到3.3V,同时在RTT终端里观察ADC读数是否从0x000到0xFFF线性变化。这是验证ADC硬件和软件配置是否正确的黄金标准。
第三步:检波电路验证。用信号发生器输出一个1MHz、1Vpp的AM信号(调制度m=0.5),用示波器CH1接输入,CH2接检波输出。此时,你应该看到CH2是一个完美的、频率为1kHz的正弦波(假设调制信号是1kHz)。如果CH2是杂乱的毛刺,那问题一定出在检波电路的焊接或元件参数上。
第四步:液晶显示验证。先不接任何信号,只给液晶屏供电,运行lcd_test.c,看屏幕是否能正常显示“Hello World”。如果不行,立刻拿出QC12864B.pdf,用万用表的二极管档,逐个测量PSB、RST、CS、RS、RW、E以及D0-D7的对地电压,确认它们的电平状态是否与初始化代码中的设定一致。液晶屏的问题,90%都出在硬件连接上,而不是代码。
5. 常见问题与排查技巧实录:那些年踩过的坑
5.1 液晶屏不显示或显示乱码的终极排查表
| 现象 | 最可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 全屏黑,无任何反应 | 1. 电源未接或电压不足 2. 对比度电位器VR1调至极限 3. RST引脚未正确复位 | 1. 用万用表测VDD和V0引脚电压,VDD应为5V,V0应为-2V~-3V(负压) 2. 用螺丝刀缓慢旋转VR1,观察是否有微弱灰度变化 3. 用示波器测RST引脚,确认上电时有>100ms的低电平脉冲 | 1. 检查电源模块 2. 将VR1调至中间位置 3. 在 LCD_Init()开头,手动添加LCD_RST_CLR(); delay_ms(200); LCD_RST_SET(); |
| 显示部分字符,但位置错乱 | 1. 并行数据线D0-D7中有1根接触不良 2. RS/RW/E控制线时序错误 | 1. 用万用表通断档,逐一测量D0-D7与MCU引脚的连通性 2. 用逻辑分析仪抓取E引脚波形,确认脉冲宽度>450ns | 1. 重新焊接可疑焊点 2. 在 LCD_WriteCmd()和LCD_WriteData()函数里,增加__nop()延时 |
| 屏幕闪烁,或显示内容随时间漂移 | 1. 忙标志(BF)未正确查询 2. 晶振频率不稳定 | 1. 在每次写指令前,强制加入while(LCD_ReadStatus() & 0x80);2. 用频谱仪测8MHz晶振输出,看频谱是否纯净 | 1. 修改底层驱动函数 2. 更换一颗高质量的HC-49S封装晶振 |
5.2 AM检波失真与噪声的实战解决方案
-
问题:检波输出有严重“削顶”失真
原因:二极管导通压降(约0.7V)与音频信号幅度相比不可忽略,导致正半周被截断。
方案:在二极管后级增加一级运放构成的“有源峰值检波器”,用LM358搭建,其输出能精确跟随输入包络,且无二极管压降。但这会增加电路复杂度,对于电赛而言,更推荐的方案是提高输入信号幅度,让0.7V的压降占比小于5%,失真即可忽略。 -
问题:检波输出有高频“嘶嘶”噪声
原因:RC低通滤波器的截止频率过高,未能滤除载波残余。
方案:这不是简单地加大C值。因为C过大,会导致包络响应变慢,语音听起来“发闷”。我的做法是,在RC滤波器后,再加一级由LM358构成的二阶巴特沃斯低通滤波器(fc=5kHz),它能在5kHz处提供-40dB/dec的陡峭衰减,彻底滤除1MHz载波,同时对语音频带的影响微乎其微。 -
问题:调制度m的计算值始终为0或溢出
原因:ADC采样值Vmax和Vmin的计算逻辑错误,或未做防抖处理。
方案:在main.c里,不要直接用ADC的瞬时值,而是先对连续256个采样点做中值滤波,再求Vmax/Vmin。中值滤波能有效剔除ADC偶尔产生的尖峰噪声(如电源扰动引起的异常值),让计算结果稳定可靠。代码片段如下:
c // 对adc_buffer[256]做中值滤波 for(i = 0; i < 256; i++) { for(j = i+1; j < 256; j++) { if(adc_buffer[i] > adc_buffer[j]) { temp = adc_buffer[i]; adc_buffer[i] = adc_buffer[j]; adc_buffer[j] = temp; } } } filtered_val = adc_buffer[128]; // 取中值
5.3 STM32程序跑飞或死机的快速定位法
STM32程序跑飞,往往不是代码有Bug,而是硬件或配置出了问题。我的“三分钟定位法”如下:
-
看LED:在
main()开头,让一个LED常亮;在while(1)循环里,让同一个LED以1Hz频率闪烁。如果LED常亮不灭,说明程序卡死在main()之前的初始化阶段,重点检查SystemInit()和SetSysClockTo72();如果LED完全不亮,说明连启动代码都没跑起来,检查startup_stm32f10x_hd.s里的Reset_Handler是否正确跳转。 -
看串口:在
main()的每一关键步骤后,加一句printf("Step X OK\r\n")。如果某一句没打印出来,问题就出在它之前。这是最朴实也最有效的手段。 -
看内存:在Keil的Debug模式下,打开
View -> Memory Windows,输入0x20000000(SRAM起始地址),观察内存是否被意外改写。如果发现大量0xAAAAAAAA或0xDEADBEEF,那是典型的栈溢出或野指针访问,立刻检查你的局部数组大小和指针操作。
最后再分享一个小技巧:这个项目里所有的延时函数(delay_ms, delay_us)都基于SysTick,而SysTick的中断优先级默认是最低的。如果你在某个高优先级中断里调用了delay_ms(1),程序会立刻死机,因为SysTick中断被屏蔽了,延时函数永远等不到中断返回。所以,永远不要在中断服务函数里调用任何基于SysTick的延时函数。要用,就用__nop()这种纯软件延时,或者把延时逻辑移到主循环里去处理。这是我当年在电赛现场,花了整整两个小时才揪出来的“幽灵Bug”,希望你能避开。
简介:基于STM32F103ZET6主控,完整实现AM调幅信号的接收、二极管检波、低通滤波、音频放大及耳机输出功能;配套QC12864B点阵液晶模块,支持并行和串行两种接口方式,分别对应13脚并口与14脚串口接线方案,可实时显示载波频率、调制度、信号幅度等关键参数;工程使用标准STM32固件库结构,包含CORE、SYSTEM、delay、usart、sys等规范模块,集成startup_stm32f10x_hd.s启动文件、system_stm32f10x.c系统时钟配置、stm32f10x_it.c中断管理及main.c主流程逻辑;提供J-Link调试支持(含JLinkSettings.ini)、keilkilll.bat一键清理脚本、多用户工程配置文件(.uvprojx/.uvoptx/.uvguix.*),以及README.md、ss.md操作指南和两份QC12864B硬件手册(QC12864B.pdf、育松电子 QC12864B使用说明.pdf);适用于高校电子技术课程设计、全国大学生电子设计竞赛F题复现训练及嵌入式信号处理基础实践。

被折叠的 条评论
为什么被折叠?



