基于STM32与NRF24L01的无线信号模拟系统设计:从FFT分析到DDS合成实战
2026/9/23 0:12:42 网站建设 项目流程

简介:本资源是2024年全国大学生电子设计竞赛C题‘无线传输信号模拟系统’的完整实现源码,面向计算机、电子信息类专业学生及电赛备赛群体,聚焦高频信号生成、调制解调仿真与无线信道建模等核心难点,可直接用于课程设计、毕业设计或竞赛复盘。压缩包含2000个文件,主体为1096个C源文件与832个头文件(支撑嵌入式底层驱动、DSP算法模块及LVGL人机界面),辅以41个配置说明文本、18个Markdown文档(含环境搭建指南与模块接口说明)、少量Python脚本(用于信号预处理与数据验证)及JSON/Shell配置文件,整体84.72MB,结构清晰、模块解耦度高。已有860人学习下载,代码经导师审核并获省赛第一名、评审分99分,包含完整工程框架、可一键编译运行的Makefile、典型测试用例及多级调试日志支持,小白亦能快速上手理解信号链全流程。

1. 项目概述:从省一到背后的无线信号模拟实战

去年带队参加电赛,我们组拿下了C题“无线传输信号模拟系统”的省第一名。这个题目听起来挺唬人,什么“无线传输”、“信号模拟”,乍一看像是要搞通信协议或者复杂调制。但实际做下来,你会发现,它的核心根本不是让你去发明一种新的无线通信方式,而是考验你在一个限定条件下的系统集成、信号处理算法实现以及稳定性的综合把控能力。简单来说,就是给你一个“黑盒”任务:你需要设计一个系统,能够模拟生成特定的基带信号,通过一个指定的无线模块(比如常见的NRF24L01)发送出去,然后在接收端,不仅要能正确接收,还要能对接收到的信号进行一系列的分析与识别,比如判断信号类型、计算频率、解调出数字信息等。整个过程,从信号生成算法、到驱动编写、再到上位机软件设计,环环相扣,任何一个环节的微小失误都可能导致整个系统“跑飞”。今天,我就把这套拿了省一的源码和背后的设计思路、踩过的坑,毫无保留地分享出来。无论你是正在备战电赛的学生,还是对嵌入式系统、信号处理感兴趣的朋友,相信这篇近万字的实战复盘,都能给你带来直接的参考价值。

这个系统的价值在于,它剥离了复杂的射频电路设计,让你能专注于数字信号处理的核心算法和系统稳定性设计。我们采用的平台是STM32F4系列单片机作为主控,搭配NRF24L01+ 2.4GHz无线模块,上位机使用Python的PyQt5框架开发。整个项目涉及嵌入式C编程、SPI通信、定时器/PWM/DAC应用、简单的调制解调算法(如FSK、ASK)、以及PC端的数据可视化与协议解析。接下来,我将从系统顶层设计思路开始,一步步拆解每个模块的实现细节、参数选择背后的考量,以及我们是如何在有限的比赛时间内,通过一系列“骚操作”和严谨的调试,让系统稳定跑满所有测试点的。

2. 系统顶层设计与核心思路拆解

2.1 题目要求与我们的解题策略

当年的C题要求可以抽象为几个核心功能点:1. 发射端能根据指令产生不同频率、不同调制方式(如正弦波、方波、FSK信号)的模拟信号;2. 通过无线模块将该信号的“特征信息”或“采样数据”发送至接收端;3. 接收端能根据接收到的信息,在本地复现(模拟)出发射端的信号,并完成参数测量与识别。这里有一个关键陷阱:题目要求的“无线传输”并非直接传输模拟信号波形(那需要高速ADC和极大的带宽),而是传输信号的描述参数低速采样数据。因此,我们的策略是:在发射端进行实时信号分析与特征提取,将提取出的少量关键参数打包成数据帧,通过无线模块发送;在接收端,根据这些参数重新合成(模拟)出信号。这样做的好处是数据量极小,对无线传输的实时性要求低,非常稳定。

我们选择了“参数化描述+关键点采样”的混合模式。对于标准波形(如固定频率正弦波、方波),只传输波形类型、频率、幅度三个参数。对于调制信号(如FSK),则传输调制方式、载波频率、符号率、以及符号序列本身。同时,为了应对可能出现的任意波形题目要求,我们增加了一个“采样传输模式”,即发射端用ADC对信号进行低速采样,将采样值数组发过去,接收端用DAC还原。这个模式是保底方案,数据量较大,仅在必要时启用。整个系统的核心思路,就是用复杂度换稳定性和精度,把计算压力放在资源相对丰富的发射端(可以进行FFT等较复杂运算),接收端只做简单的参数解析和信号合成,确保实时性。

2.2 硬件平台选型与考量

硬件是系统的骨架,选型决定了性能天花板和开发难度。

  1. 主控MCU:STM32F407VET6

    • 为什么是F4?电赛常用F1和F4系列。F103虽然便宜,但其主频和计算能力(特别是浮点运算)在面对实时FFT分析时可能捉襟见肘。F407拥有168MHz主频,带硬件浮点单元(FPU),做1024点的FFT速度极快,这对于我们在发射端快速分析信号频率至关重要。而且其丰富的定时器、DAC、DMA资源,为生成高质量模拟信号提供了硬件保障。
    • 外设使用规划
      • DAC + DMA + 定时器:用于接收端高质量地重建模拟信号。这是输出模拟波形的黄金组合,能实现极高精度的时序控制。
      • ADC + DMA + 定时器:用于发射端的信号采样。同样利用DMA减轻CPU负担,实现固定频率采样。
      • SPI:用于驱动NRF24L01无线模块。必须使用硬件SPI,软件模拟SPI速度跟不上。
      • 多个通用定时器:用于产生PWM(模拟某些波形)、精确延时、以及作为其他外设的触发时钟。
  2. 无线模块:NRF24L01+ 增强版(带PA+LNA)

    • 为什么是它?成本极低、资料极多、接口简单(SPI)。虽然它的实际有效数据传输速率并不高(在稳定纠错模式下,可能只有几十kbps),但对于传输我们这种小数据包(一个数据包通常不到32字节)的参数信息来说,完全够用。选择带功率放大(PA)和低噪声放大(LNA)的增强版,是为了增加传输距离和抗干扰能力,在比赛场馆这种无线环境复杂的地方非常有必要。
    • 关键配置:我们将其设置为250kbps的空中速率、自动重发5次、自动应答(ACK)开启。较低的空中速率能换来更好的接收灵敏度,也就是更远的距离和更强的抗干扰性。自动重发和应答机制保证了数据传输的可靠性,这是我们系统稳定的基石。
  3. 信号调理电路(自制板)

    • 这是容易被忽略但至关重要的一环。题目给的输入信号幅度范围未知,可能超过MCU的ADC输入范围(0-3.3V)。因此,发射端前端必须设计一个电压抬升+衰减的可调调理电路,通常是一个运放构成的加法器与分压电路组合,确保任何合理的输入信号都能被安全地抬升到0-3.3V的ADC最佳测量区间。
    • 同样,接收端DAC输出一般是0-3.3V,如果题目要求驱动特定负载或输出特定幅度,可能也需要一个运放构成的放大或缓冲电路。

注意:硬件平台的确定,意味着软件架构的基调也定了下来。F4的HAL库虽然方便,但效率不如标准外设库。为了极致性能和可控性,我们选择了基于标准外设库(StdPeriph)进行开发,关键时序部分直接操作寄存器。这带来了更好的性能,但也增加了初期的工作量。

3. 发射端核心:信号分析与特征提取

发射端的任务是“识别人”。它需要准确判断输入信号的类型和参数,并将这些信息压缩成短短的几个字节。

3.1 信号采集与预处理

信号通过调理电路进入MCU的ADC引脚。我们使用ADC1,配合定时器TIM2触发,以固定的采样率(例如20kHz)进行采样。采样数据通过DMA直接搬运到一个双缓冲循环数组中,这样ADC可以持续工作,不会因为CPU处理数据而丢失点。

预处理的第一步是去除直流分量。即使调理电路做了抬升,信号中仍可能包含直流偏置。我们采用减去滑动平均的方法来消除直流。对于一个长度为N的采样数组,计算其平均值作为直流估计值,然后每个采样点减去该值。这能防止后续的FFT分析出现巨大的零频分量,干扰对基波频率的判断。

3.2 关键特征提取算法实现

这是发射端软件最核心的部分,我们实现了一个多级判断流水线:

  1. 过零检测与初步频率估算

    • 首先对预处理后的数据做一次简单的过零检测,粗略估算信号频率。这能快速判断信号是低频(如几Hz到几百Hz)还是高频(几kHz以上)。对于低频信号,时域方法足够准确;对于高频或复杂信号,则需要频域分析。
    • 技巧:为了防止噪声引起的误触发,我们设置了迟滞比较。只有当信号连续跨越一个正阈值和一个负阈值时,才计为一次有效过零。
  2. 基于FFT的精确频谱分析

    • 对于非明显方波或复杂信号,我们启用FFT。使用STM32F4的DSP库中的arm_cfft_f32函数,对1024个采样点进行FFT运算。得益于硬件FPU,整个过程在几个ms内即可完成。
    • 关键操作:找到FFT结果数组中幅值最大的谱线(忽略直流分量),其对应的频率就是信号的主频。这里有一个经典坑点:频率分辨率 = 采样率 / FFT点数。例如20kHz采样率、1024点FFT,分辨率约为19.5Hz。这意味着你测出来的频率只能是19.5Hz的整数倍。为了获得更高精度,我们采用了质心法(幅度加权平均),在最大谱线附近进行插值估算,可以将频率精度提高到1Hz以内。
    • 代码片段(核心思路)
      // 假设 fftOutput 为FFT后的复数数组,采样率fs,点数N uint32_t maxIndex = 0; float maxMagnitude = 0; for (int i = 1; i < N/2; i++) { // 忽略直流和负频率 float mag = sqrtf(fftOutput[i].r * fftOutput[i].r + fftOutput[i].i * fftOutput[i].i); if (mag > maxMagnitude) { maxMagnitude = mag; maxIndex = i; } } float rawFreq = (float)maxIndex * fs / N; // 使用质心法在 maxIndex 附近进行插值,得到精确频率 preciseFreq // ...
  3. 波形识别(正弦、方波、三角波等)

    • 单纯靠频率无法区分波形。我们结合了时域和频域特征。
    • 正弦波:FFT频谱非常干净,基本上只有主频分量,谐波分量幅值远小于主频(比如小于主频的1/20)。
    • 方波:FFT频谱有丰富的奇次谐波(3次、5次、7次...),且谐波幅度衰减规律符合1/n。
    • 三角波:谐波衰减规律符合1/n²。
    • 我们编写了一个简单的“特征匹配”函数,计算谐波能量与主频能量的比值,与预设阈值比较,来判断波形类别。
  4. 调制信号识别(FSK/ASK)

    • 对于数字调制信号,我们首先用前面方法估算出载波频率。然后,对信号进行包络检波(ASK)或鉴频(FSK)的数字化处理。
    • 以FSK为例:我们使用了一个简单的差分鉴频算法。计算相邻采样点的相位差(可以通过希尔伯特变换得到解析信号,也可以近似处理),相位差的变化规律就反映了频率的变化。再通过一个过零检测或平均比较,就可以解调出0和1的符号序列,并计算出符号率。

3.3 数据帧封装与无线发送

特征提取完成后,我们需要将这些信息打包。我们定义了一个精简的协议帧结构:

typedef struct { uint8_t frameHead; // 帧头,固定为0xAA uint8_t waveType; // 波形类型:0-正弦,1-方波,2-三角波,3-FSK,4-ASK... uint16_t frequency; // 频率/载频,单位Hz uint16_t amplitude; // 幅度,单位mV(DAC参考电压下的值) uint8_t symbolRate; // 符号率(仅调制信号有效),单位kbps uint8_t dataLength; // 跟随的数据长度 uint8_t payload[16]; // 负载,如符号序列、采样数据等 uint8_t checksum; // 校验和(前面所有字节的累加和取反) } WirelessFrame_t;

这个结构体总共不到30字节,正好可以放入NRF24L01的一个数据包(32字节上限)。封装好帧后,通过SPI接口发送给NRF24L01模块。这里有一个重要优化:我们不是每计算一帧就发送,而是设置了一个小的发送缓冲区,并启用NRF24L01的自动重发和应答机制。发送函数会阻塞等待发送成功或超时。如果超时,则记录错误,并可能根据策略重试或切换为更稳健的“采样传输模式”。

实操心得:无线发送的成功率是系统成败的关键。我们实测发现,在比赛现场复杂的2.4GHz环境下(无数手机、Wi-Fi),单纯的发送很容易丢包。因此,除了模块本身的ACK机制,我们在应用层也做了一个简单的序列号+确认重传。接收端每收到一帧,会回传一个ACK帧(包含收到的序列号)。发射端如果没收到ACK,会在下一个周期重发上一帧数据。虽然增加了少量延迟,但保证了在极端情况下参数也能准确送达,这个设计让我们在最终测试的干扰环节表现非常稳健。

4. 接收端核心:信号重建与系统同步

接收端的任务是“扮演人”。它需要根据收到的参数,尽可能真实地还原出原始信号。

4.1 无线数据接收与解析

接收端的NRF24L01配置为与发射端相同的通信频率、地址和速率。我们将其设置为接收模式(PRX),并开启中断。当收到数据时,硬件中断触发,在中断服务例程(ISR)中,将数据从模块的FIFO快速读取到一个接收缓冲区,然后置位一个软件标志位。在主循环中,检测到这个标志位后,再进行协议解析、校验和验证。

解析过程是封装的逆过程。校验和通过后,根据waveType字段,将其他参数赋值给对应的信号合成变量。这里有一个状态机的设计:接收端可能处于“空闲”、“等待参数”、“合成信号”等不同状态。只有当收到一帧完整且校验正确的参数帧后,才会切换到“合成信号”状态。

4.2 基于DDS原理的高质量信号合成

信号合成的核心是直接数字频率合成(DDS)思想。我们利用STM32的DAC配合定时器和DMA来实现。

  1. 正弦波/三角波合成

    • 我们预先在内存中存储了一个周期的正弦波表(例如256个点)。对于任意要求的频率f_out,我们需要计算出一个“相位增量”(Phase Increment)。
    • 公式相位增量 = (f_out * 波表长度) / 定时器触发频率
    • 定时器以固定频率(例如100kHz)触发DMA,DMA每次触发就从波表中读取一个点送给DAC输出。DMA的读取地址由一个累加器控制:每次触发后,累加器加上“相位增量”,累加器的高位部分(例如高8位)作为波表的索引。这样,通过改变“相位增量”,就实现了输出频率的精确、连续变化。
    • 优势:这种方法产生的信号频率非常精确,波形失真小,且CPU占用率极低(只有DMA和定时器在工作)。
  2. 方波合成

    • 方波合成更简单。我们直接使用一个通用定时器的PWM输出功能。根据收到的频率参数,动态计算定时器的分频值(PSC)和重载值(ARR),即可产生占空比为50%的方波。如果需要非50%占空比,则还需要解析额外的参数。
  3. 调制信号(FSK/ASK)合成

    • 这需要将DDS与符号序列结合起来。我们维护一个符号缓冲区(来自接收到的payload)。
    • 对于FSK:定义两个频率f0f1。根据当前要发送的符号(0或1),动态切换DDS的“相位增量”值。符号之间的切换时机由另一个定时器控制,以实现精确的符号率。
    • 对于ASK:则是在发送1时开启DDS输出(或PWM输出),发送0时关闭输出(输出零电平)。
    • 这里的关键是时序同步。接收端必须知道符号的起始边界。我们在协议中约定,每一帧数据的开始,都代表一个新符号周期的开始。接收端在解析完一帧数据后,会复位其合成时序,确保从帧边界开始生成信号。

4.3 系统同步与抗干扰设计

无线传输必然有延迟和抖动,如何让接收端合成的信号与发射端原始信号在“概念上”同步?

  1. 时间戳与缓冲:我们在发射端发送的每一帧数据中,可以嵌入一个简易的“时间戳”(比如一个每毫秒递增的计数器)。接收端收到后,可以根据这个时间戳的连续性,判断是否有数据包丢失,并进行一定的缓冲和插值(对于连续变化的参数),使输出更加平滑。
  2. 心跳包机制:除了数据帧,发射端定期发送一种特殊的“心跳帧”(只包含帧头和序列号)。接收端通过监测心跳帧的间隔,可以判断无线链路是否中断。一旦心跳超时,接收端可以进入安全状态(例如输出静默信号或特定提示波形),而不是一直输出错误或陈旧的数据。
  3. 参数平滑过渡:当接收到的频率参数发生跳变时,我们不直接让DDS的相位增量突变,而是用一个简单的线性插值,在几十毫秒内渐变到新值。这可以避免DAC输出产生可闻的“咔嗒”声或频谱上的毛刺,使信号切换更“优雅”,在观感上更接近真实信号源切换的效果。

5. 上位机软件:监控、调试与可视化

一套好的上位机软件,是调试和展示的利器。我们使用Python + PyQt5开发,主要功能是双向通信和数据可视化。

5.1 通信协议与数据接口

上位机通过USB转串口模块与接收端的STM32连接(为什么不连发射端?因为发射端可能离电脑较远)。我们定义了一套简单的串口指令协议,用于:

  • 下行指令(PC -> 接收端):切换接收端的工作模式、请求重发参数、设置本地合成参数(用于手动测试)。
  • 上行数据(接收端 -> PC):接收端将当前正在合成的信号参数、接收到的无线数据包统计信息(如丢包率)、以及自身ADC采集到的最终输出信号波形(用于闭环验证)实时上传给PC。

5.2 核心功能模块实现

  1. 信号参数显示面板:实时显示当前信号的类型、频率、幅度、符号率等所有解析出的参数,数值清晰直观。
  2. 波形实时绘图:使用PyQtGraph库(性能远优于Matplotlib),开辟两个绘图区域。
    • 图一:接收参数趋势图。横轴为时间,纵轴为频率或幅度,以曲线形式展示接收到的信号参数如何随时间变化。这对于观察FSK信号的频率跳变过程非常有用。
    • 图二:信号波形图。显示接收端通过自身ADC回采的、最终输出的模拟信号波形。这是对整个系统性能的最终验证——我们看到的,就是实际从接收端DAC输出的电压波形。
  3. 数据统计与日志:显示无线链路的信号强度(NRF24L01的RPD引脚)、数据包收发数量、丢包率、最近一次通信的误码率(针对调制信号)等。所有数据可以保存为CSV文件,供赛后分析。
  4. 手动控制面板:可以手动输入一组参数,通过串口下发给接收端,让接收端直接合成指定信号。这个功能在独立调试接收端DAC输出电路时非常方便,无需依赖发射端。

避坑技巧:PyQt5的UI线程不能进行阻塞性的操作(如长时间的数据处理或等待串口)。我们必须使用多线程,将串口数据的读取、解析放在一个独立的子线程中,通过信号(Signal)和槽(Slot)机制将解析好的数据发送给主UI线程进行更新显示。否则,界面会卡死。另外,实时绘图的数据量很大,要使用setData()方法而不是清空重绘,并合理设置绘图缓冲区大小,防止内存暴涨。

6. 系统联调与性能优化实录

将所有模块组合在一起后,真正的挑战才开始。我们遇到了无数问题,以下是几个最典型的及其解决方案。

6.1 无线通信不稳定,时好时坏

  • 现象:距离稍远或用手遮挡,数据就开始大量丢失。
  • 排查
    1. 首先检查电源。NRF24L01+对电源纹波非常敏感,必须使用LDO稳压供电,并在模块的VCC和GND引脚就近放置一个10uF和一个0.1uF的电容。我们最初使用开发板的3.3V,发现不行,后来改为独立的AMS1117-3.3供电,稳定性大增。
    2. 检查天线。增强版模块的鞭状天线要确保完全展开,且周围不要有金属物体遮挡或大面积地平面覆盖。
    3. 检查SPI时序。用逻辑分析仪抓取STM32与NRF24L01之间的SPI通信波形,发现时钟极性(CPOL)和相位(CPHA)设置错误。必须严格按照模块数据手册设置(通常是Mode 0)。
    4. 优化软件配置。将无线信道设置在相对干净的2.4GHz频段(如100频道),避开常见的Wi-Fi信道(1, 6, 11)。将自动重发延迟从250us调整为1500us,给模块更充分的处理时间。
  • 解决:综合以上措施后,在比赛场馆内,无遮挡传输距离稳定在15米以上,完全满足要求。

6.2 接收端合成信号频率有偏差

  • 现象:发射端输入1kHz正弦波,接收端合成出来的信号用示波器测量是1.05kHz。
  • 排查
    1. 检查发射端分析出的频率值是否正确。通过串口打印发现,发射端FFT分析结果就是1000Hz,准确。
    2. 检查接收端定时器触发DAC的频率。我们使用的是TIM6触发DMA。计算公式为:DAC输出频率 = (定时器时钟 / (PSC+1) / (ARR+1))。问题出在定时器时钟上。STM32F4的定时器时钟源是APB1或APB2总线时钟,而APB总线时钟又由系统时钟分频而来。我们在系统初始化时,为了达到168MHz主频,对时钟树进行了复杂配置,但忽略了给定时器时钟的倍频器(APB prescaler)设置。当APB分频系数不为1时,定时器时钟会有一个x2的补偿。最终发现是ARR寄存器的计算值有误。
    3. 检查DDS相位累加器的计算。使用了浮点数计算相位增量,可能存在精度损失。
  • 解决
    1. 仔细核对时钟树配置,明确TIM6的最终输入时钟频率(通过读取RCC->CFGR寄存器确认)。
    2. 将频率相关的计算全部改为整数运算。例如,将“相位增量”公式变形为:PhaseIncrement = (f_out * 波表长度 * (ARR+1) * (PSC+1)) / TimerClock。其中f_out波表长度ARRPSCTimerClock都是整数,最后结果取整。这完全避免了浮点误差。
    3. 在程序初始化时,用示波器测量一个已知频率(如1kHz)的输出,微调计算参数进行校准,并将校准系数存入代码。

6.3 调制信号解调误码率高

  • 现象:FSK信号在传输一段随机序列后,接收端解调出的序列有误码。
  • 排查
    1. 首先确认无线传输的误码。让发射端发送固定的已知序列,接收端将收到的原始字节打印出来对比,发现无线传输本身无误码。问题出在发射端的解调算法或接收端的合成/解调算法上。
    2. 在发射端,将ADC采样后的原始数据通过串口发送到电脑,用Python脚本离线运行解调算法,发现误码率依然存在。锁定问题在发射端的数字化鉴频算法。
    3. 分析发现,简单的差分鉴频算法对噪声敏感。当信号幅度有微小波动时,计算的相位差抖动很大,导致符号判决错误。
  • 解决
    1. 增加数字滤波:在鉴频之前,先对采样信号进行一个移动平均滤波,平滑掉高频噪声。
    2. 优化判决门限:不再使用固定的零门限进行判决,而是采用动态门限。例如,计算一段时间内相位差的正负平均值,以其中心值作为判决门限,可以抵抗信号的直流漂移。
    3. 引入同步头:在发送有效数据前,先发送一段固定的前导码(如01010101)。接收端在解调时,先通过这段前导码来调整和锁定最佳的判决门限和符号时钟相位。这个改进让误码率下降了90%以上。

6.4 系统整体延迟与实时性

  • 现象:发射端信号变化后,接收端输出变化有明显的延迟感。
  • 分析:延迟来自多个环节:发射端ADC采样一帧时间(1024点/20kHz = 51.2ms)+ FFT计算时间(约5ms)+ 无线传输与重传等待时间(平均10-50ms)+ 接收端解析与合成启动时间(<1ms)。总延迟可能在70-120ms之间。对于低频信号(几Hz),这个延迟是明显的。
  • 优化
    1. 降低FFT点数:对于频率高于200Hz的信号,将FFT点数从1024降为256,采样率不变,这样单帧采样时间降到12.8ms,频率分辨率虽然降低(约78Hz),但对于较高频率的信号,质心插值法仍能保证精度。
    2. 流水线操作:将ADC采样、FFT计算、无线发送三个过程重叠。当DMA搬运完一半缓冲区时,就触发中断开始计算上一半的数据;同时,上一帧的计算结果正在被发送。这样,有效减少了等待时间。
    3. 区分传输模式:对于已知的、参数变化不快的信号(如固定正弦波),采用“变更才发送”的策略。发射端持续分析信号,只有当识别出的参数与上次发送的参数差值超过一定阈值时,才组包发送,大大减少了无线信道的数据量。

7. 源码结构与关键代码导读

我们的工程代码结构清晰,遵循模块化设计,方便调试和复用。

Wireless_Signal_Simulator/ ├── README.md ├── Firmware_Transmitter/ # 发射端固件 │ ├── Core/ # 启动文件、系统时钟配置 │ ├── Drivers/ # STM32标准外设库 │ ├── Inc/ # 头文件 │ │ ├── adc_signal_acq.h │ │ ├── fft_analyzer.h # FFT分析模块 │ │ ├── nrf24l01.h # 无线驱动 │ │ ├── signal_identifier.h # 信号识别主逻辑 │ │ └── ... │ └── Src/ # 源文件 │ ├── main.c # 主循环,状态机调度 │ ├── adc_signal_acq.c │ ├── fft_analyzer.c │ ├── nrf24l01.c │ └── ... ├── Firmware_Receiver/ # 接收端固件(结构类似) │ ├── Inc/ │ │ ├── dds_signal_gen.h # DDS信号生成器 │ │ ├── nrf24l01.h │ │ ├── uart_protocol.h # 与上位机通信协议 │ │ └── ... │ └── Src/ │ ├── main.c │ ├── dds_signal_gen.c │ └── ... └── Host_Software/ # 上位机软件 ├── main.py # 主程序入口 ├── serial_manager.py # 串口通信线程 ├── data_parser.py # 数据解析 ├── plot_widgets.py # 绘图组件 └── ui_mainwindow.py # PyQt5主界面

关键代码片段1:发射端信号识别主状态机(signal_identifier.c)

SignalIdentifyState_t IdentifySignal(float* sampledData, uint16_t length, WirelessFrame_t* outputFrame) { static SignalIdentifyState_t state = STATE_INIT; static float prevFreq = 0.0f; float currentFreq, currentAmp; WaveType_t currentType; switch(state) { case STATE_INIT: // 初始化滤波器、缓冲区等 state = STATE_COLLECTING; break; case STATE_COLLECTING: // 等待DMA收集够足够数据(如1024点) if(isDataReady()) { state = STATE_PROCESSING; } break; case STATE_PROCESSING: // 1. 预处理(去直流、滤波) PreprocessSignal(sampledData, length); // 2. 过零检测粗估频率 float roughFreq = ZeroCrossingDetect(sampledData, length, SAMPLE_RATE); // 3. 根据粗估频率选择策略 if(roughFreq < 500.0f) { // 低频,用时域法 currentFreq = RefineFreqTimeDomain(sampledData, length, SAMPLE_RATE); currentType = IdentifyWaveformTimeDomain(sampledData, length); } else { // 高频或复杂,用FFT currentFreq = RefineFreqFFT(sampledData, length, SAMPLE_RATE); currentType = IdentifyWaveformFFT(fftOutput); // 如果是调制信号,进一步分析 if(currentType == WAVE_FSK || currentType == WAVE_ASK) { DemodulateAndGetSymbols(sampledData, length, outputFrame->payload, &(outputFrame->dataLength)); } } // 4. 计算幅度 currentAmp = CalculateAmplitude(sampledData, length); // 5. 判断参数是否变化显著,决定是否发送 if(fabsf(currentFreq - prevFreq) > FREQ_CHANGE_THRESHOLD || ...) { outputFrame->frequency = (uint16_t)currentFreq; outputFrame->waveType = (uint8_t)currentType; outputFrame->amplitude = (uint16_t)(currentAmp * 1000); // 转mV prevFreq = currentFreq; state = STATE_SENDING; } else { state = STATE_COLLECTING; } break; case STATE_SENDING: if(SendWirelessFrame(outputFrame) == STATUS_OK) { state = STATE_COLLECTING; } else { // 发送失败,记录错误,可能进入降级模式 state = STATE_ERROR_HANDLING; } break; case STATE_ERROR_HANDLING: // 处理错误,如切换为低速采样模式 HandleTransmissionError(); state = STATE_COLLECTING; break; } return state; }

关键代码片段2:接收端DDS信号合成(dds_signal_gen.c)

void DDS_UpdateFrequency(uint32_t targetFreq) { // 整数运算计算相位增量,避免浮点误差 // SystemCoreClock 是系统主频,TIM6挂在APB1上,需根据时钟树计算实际时钟 uint32_t timerClock = Get_TIM6_Clock(); // 自定义函数,获取定时器实际输入时钟 uint32_t arr = TIM6->ARR; uint32_t psc = TIM6->PSC; uint32_t phaseIncrement; // PhaseIncrement = (f_out * LUT_SIZE * (arr+1) * (psc+1)) / timerClock // 为防止溢出,分步计算并使用64位中间变量 uint64_t temp = (uint64_t)targetFreq * LUT_SIZE; temp *= (arr + 1); temp *= (psc + 1); phaseIncrement = (uint32_t)(temp / timerClock); __disable_irq(); // 关键操作,关闭中断防止冲突 g_dds_phase_accumulator += phaseIncrement; // 更新全局相位累加器增量 __enable_irq(); } // TIM6触发DMA的中断服务函数(或DMA完成中断) void TIM6_DAC_IRQHandler(void) { if(TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM6, TIM_IT_Update); // 更新DMA内存地址:从波形表中读取下一个点 static uint32_t phaseAcc = 0; phaseAcc += g_dds_phase_accumulator; // 累加相位 uint32_t index = (phaseAcc >> 24) & 0xFF; // 取高8位作为256点波表索引 DAC_SetChannel1Data(DAC_Align_12b_R, sineTable[index]); // 输出到DAC } }

8. 备赛与现场调试经验

电赛不仅考技术,更考策略、心态和现场应变能力。

  1. 模块化开发与单元测试:从一开始就将系统划分为独立的模块(ADC采集、FFT分析、无线收发、DDS合成、上位机)。每个模块都编写了简单的测试程序(如让DAC输出一个固定频率正弦波,用示波器看;让无线模块自发自收,测试误码率)。确保每个模块单独工作正常后,再进行集成。这能极大降低后期联调的复杂度。
  2. 预留调试接口与“后门”:在代码中大量使用printf通过串口打印状态信息(使用ITM_SendChar或半主机模式,避免占用通信串口)。定义一些简单的串口命令,可以随时查询或修改系统内部状态(如当前识别出的频率、无线模块配置寄存器值等)。在现场无法连接仿真器时,这些“后门”是救命稻草。
  3. 制定详细的调试清单:准备一份表格,列出所有需要测试的功能点和性能指标(如频率测量范围与精度、波形识别准确率、无线最远距离、信号重建失真度等)。每完成一项测试就打勾,做到心中有数,避免遗漏。
  4. 电源!电源!电源!:准备一个干净、功率充足的实验室电源。电机、舵机等感性负载必须与数字电路部分分开供电,并使用隔离模块或磁珠。我们在前期测试中,就因为舵机动作导致电源纹波增大,使得无线模块和ADC工作异常,排查了很久。
  5. 准备备用方案与降级模式:我们的“采样传输模式”就是备用方案。当参数化传输因干扰过大持续失败时,可以通过上位机一键切换到该模式,虽然波形质量下降,但能保证系统有输出,不至于得零分。在现场,稳定输出一个“有瑕疵的结果”远好过“没有结果”。
  6. 团队分工与时间管理:三个人明确分工:一人负责底层驱动和信号合成(接收端),一人负责信号分析和无线协议(发射端),一人负责上位机软件和文档。每天定点汇总进度,解决阻塞问题。最后一天一定要留出完整时间进行全系统联调和性能测试,而不是还在写代码。

这套“无线传输信号模拟系统”的源码和设计思路,其价值不仅仅在于完成了一道赛题。它完整地展示了一个嵌入式系统项目的开发全貌:从需求分析、方案选型、模块设计、算法实现、调试优化到最终集成。其中关于信号处理、实时系统、无线通信和软硬件协同的思考,对于从事物联网、工业控制、音频处理等相关领域的朋友,都有很强的借鉴意义。代码本身可能随着芯片型号和库的更新而过时,但解决问题的思路和方法是永恒的。如果你正在准备电赛,希望这份复盘能帮你少走弯路;如果你只是对技术感兴趣,也希望这个项目能成为你动手实践的一个有趣起点。所有的源码已整理开源,你可以在我的项目仓库中找到它,结合这篇文章,应该能完全重现这个系统。

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

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

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

立即咨询