☰
2023电赛综合测评B题国一方案:STM32测频测幅与标定
2026/10/26 19:44:22 网站建设 项目流程

简介:2023年全国大学生电子设计竞赛综合测评B题的国一方案配套工程,面向准备电赛综合测评、希望复现同轴电缆长度与终端负载检测装置的高校学生及电子爱好者。方案基于梁山派开发板与STM32,采用CubeMX+CLion+OpenOCD开发环境,核心实现包括TDR时域反射法测长度、分压法测电阻负载、NE555振荡电路测电容负载,并给出与赛题基本要求、发挥部分相对应的误差指标和调试思路。压缩包共228个文件,约22.64MB,以h/c源码、obj编译产物、png截图、txt说明、pdf文档及xlsx数据表为主,兼顾工程复现、参数记录与结果整理。该方案已有3442人学习,可作为电赛综合测评备赛的完整参考,帮助读者快速理解测量原理、搭建硬件电路、对照误差要求排查问题。

1. 从"现场四小时"倒推:2023 年电赛综合测评 B 题国一方案要解决什么

2023 年电赛综合测评 B 题这类封闭题,真正难的不是算法,是四个小时里把一块半成品板子调到指标内。这份国一方案把 STM32 主控、模拟前端调理、测频测幅、直流输出、标定流程拆成可复现的模块,每一步都对应现场能拿万用表和示波器验的判据。它适合两类人:正在准备综合测评、需要一个能直接照抄的硬件骨架和代码骨架的参赛队;以及做课程设计、想找一份带完整设计推导的 STM32 方案模板的读者。关键词里那些"范文、模板、素材",说到底要的是结构,不是一段能跑的代码。

2. 总体方案与技术选型:三条支路的预算怎么切

综合测评的题面通常长这样:给定一块已经焊好运放和接口的板子,要求对输入信号完成测量,再按测量结果输出一个可控的直流量,过程中用显示和按键完成人机交互。这类题看着是"测量题",实际考的是通道预算——你的增益怎么分、带宽怎么留、基准怎么稳。三条支路(采集、运算、输出)任何一条预算给错,后面代码写得再漂亮也救不回来。所以方案的第一步不是选芯片,是把指标拆成每一级电路的输入输出范围。

2.1 主控与资源分配:为什么主频不是第一优先级

现场选主控,第一优先级是"外设够不够、能不能一次配对",主频排在后面。综合测评里常见的运算是 256 点到 1024 点的定点平方和、一次开方、几次浮点除法,72 MHz 的 Cortex-M3 完全跑得动,真正卡住人的是定时器数量和 ADC 与 DMA 的联动能力。

我的取舍是:优先选带独立 DAC、双 ADC、至少 6 个通用定时器的型号,例如 STM32F407 这一档;如果现场只给最小系统板(比如常见的 F103C8 类板卡),就把直流输出改成 PWM + 二阶 RC 滤波,代价是建立时间从微秒级掉到几十毫秒,题面若要求输出响应快,这一条会直接扣分。这个退让必须在方案阶段想清楚,不能等焊完了才发现没有 DAC。

外设用途配置要点冲突风险
ADC1 + DMA2主采样通道定时器触发,规则组单通道,12 位与 DAC 共用 DMA 时注意流冲突
TIM3采样触发源更新事件触发 ADC,200 kHz不要用 TIM2,留给捕获
TIM2 CH1输入捕获测频预分频 1,双边沿捕获高频时溢出计数要原子读
DAC1直流输出12 位右对齐,缓冲输出输出阻抗高,必须加跟随
I2C1OLED 显示400 kHz 快速模式与按键扫描共用主循环,需节流
GPIO4 按键 + 量程切换上拉输入 + 外部 100 nF模拟开关控制脚别和 JTAG 复用

这张表的作用是提前把"谁跟谁抢资源"写在纸上。综合测评最典型的翻车是 TIM2 同时被拿去触发 ADC 又拿去捕获,结果测频读数永远差一截,查半小时才发现是同一个定时器。

2.2 模拟通道的增益与带宽预算

前端要解决三件事:把信号抬到 ADC 量程中点、把幅度放大到用满量程、把高频挡在 ADC 前面。三件事对应三级电路,顺序不能乱。

第一级是输入缓冲,用运放做电压跟随,输入阻抗做到兆欧级,避免前级信号源内阻分压。第二级是程控增益,用模拟开关切反馈电阻,做 1×、10×、100× 三挡,挡位由主控 GPIO 控制。第三级是抗混叠低通,用二阶 Sallen-Key,截止频率设在采样率的 1/2.5 左右。若采样率是 200 kHz,截止就放在 80 kHz 附近,这样 100 kHz 以上衰减足够,同时不让有用信号被削。

增益预算要倒着算:ADC 参考 3.3 V,单端输入摆幅按 0~3.3 V 算,去掉上下各 0.2 V 的线性区,可用约 2.9 V 峰峰。若最小待测信号是 10 mV 有效值,峰值约 14 mV,需要约 200 倍总增益才勉强用满,这时候三挡不够,得加一挡 1000×。反过来,最大输入若是 3 V 有效值,峰值 4.2 V 已经超过电源轨,必须在第一级前加分压或钳位。很多队伍在这一步偷懒,结果小信号挡位信噪比极差,大信号挡位直接削顶,两头都拿不到分。

提示:增益挡位的切换判据不要写成"小于某值就换挡",要留 20% 回差,否则信号在阈值附近抖动时继电器或模拟开关会来回跳,读数一直不稳。

2.3 供电与基准:把漂移关进笼子

模拟通道的精度上限由基准决定,不由 ADC 位数决定。12 位 ADC 在 3.3 V 下的 1 LSB 是 0.8 mV,如果基准本身随温度漂 5 mV,后面所有标定都白做。稳妥做法是模拟部分单独用一颗低噪声 LDO 供电,基准单独用一颗 3.0 V 串联基准,数字部分和屏幕走另一路,两路只在电源入口单点汇合。

地线处理是最容易被忽略的一环。模拟地和数字地不要大面积混铺,用 0 欧电阻或磁珠在 ADC 下方单点连接;ADC 的模拟输入脚旁边放 100 nF 加 10 μF,去耦电容回流路径越短越好。综合测评现场通常有开关电源和继电器,干扰比实验室大,这一条不做,测出来的有效值会带着几十毫伏的随机跳动,怎么算都进不了 1%。

3. 关键模块实现:五个模块的代码骨架与配置要点

方案落到板上,就是五个模块的初始化和回调。这里给出的都是可以对照 HAL 库直接改的骨架,重点在参数为什么这么取、回调里为什么这么写,而不是把整份工程贴出来。每个模块后面都写清了失败时先看哪个量,现场排故按这个顺序走,比盲目改代码快得多。

3.1 采样链:定时器触发加 DMA 双缓冲

采样链的写法决定了后面所有计算的稳定性。用定时器更新事件触发 ADC,可以让采样间隔严格等距,RMS 计算才成立;用 DMA 双缓冲,是为了让"正在采的半块"和"正在算的半块"互不干扰,避免算到一半数据被覆盖,读出来的波形看着像被啃掉一块。

/* TIM3 以 200 kHz 触发 ADC1,DMA 循环搬运两个 256 点缓冲区 */ #define ADC_BLOCK 256 static uint16_t adc_buf[2][ADC_BLOCK]; volatile uint8_t adc_half_ready = 0; /* 0=前半块 1=后半块 */ void Sample_Init(void) { /* DMA 配置为循环模式、半满与全满中断都开 */ HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, 2 * ADC_BLOCK); HAL_TIM_Base_Start(&htim3); /* 此时才开始产生触发 */ } /* 半满回调:前半块采完,可以拿去算 */ void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { adc_half_ready = 0; } /* 全满回调:后半块采完 */ void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { adc_half_ready = 1; }

三个参数值得展开。采样率 200 kHz 是带宽和运算量的折中:按每周期采 20 点算,可测上限约 10 kHz 的波形细节,高于这个频率就只能靠测频支路,不靠采样还原。块长 256 点对应 1.28 ms 一帧,50 Hz 信号能覆盖 1/16 个周期,若题面要求测 10 Hz,块长要加到 1024 点,否则 RMS 会随相位跳动。双缓冲的标志位用 volatile 修饰,是因为它在中断里改、在主循环里读,不加编译器可能把它优化进寄存器,出现"标志永远不更新"的假死。

失败时先看两个量:一是用示波器量 TIM3 的触发脚有没有 200 kHz 方波,没有就是定时器没起来;二是把 adc_buf 里前 10 个点打到屏幕上看是不是全 0 或全 4095,全 0 多半是 DMA 通道映射错,全 4095 是输入悬空顶到轨。

3.2 测频:输入捕获与测周法的切换点

测频有两条路:高频用固定闸门计脉冲数(测频法),低频用测两个上升沿之间的时间(测周法)。综合测评的输入范围往往跨三个数量级,只用一种方法必然有一头精度崩掉,所以要在中间设一个切换点,我一般取 10 kHz。

/* TIM2 CH1 输入捕获:一次捕获记录边沿时刻,配合溢出计数还原周期 */ static uint32_t last_cap = 0, overflow_cnt = 0; static float freq_hz = 0.0f; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { uint32_t now = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); uint32_t period; if (now >= last_cap) period = now - last_cap; else /* 计数器回绕,补一个满周期 */ period = (0xFFFF - last_cap) + now + 1; period += overflow_cnt * 65536U; /* 加上期间溢出的次数 */ overflow_cnt = 0; last_cap = now; if (period > 0) freq_hz = (float)TIM_CLK_HZ / (float)period; } /* 更新中断里累加溢出,超过 10 kHz 时改用固定闸门计数 */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2 && freq_hz < 10000.0f) overflow_cnt++; }

这里的关键是溢出补偿。72 MHz 定时器在 16 位计数下约 0.9 ms 就回绕一次,测 100 Hz 信号时两个边沿之间要溢出十几次,不补溢出计数,读出来的频率会在几十赫兹和几万赫兹之间乱跳,这种"玄学跳变"十有八九是溢出没处理。切换点定在 10 kHz 是因为:低于这个值测周法的相对误差随周期线性变好,高于这个值闸门计数的分辨率更划算。

失败时先量输入脚有没有信号,再确认捕获极性设的是上升沿还是双边沿——设成双边沿会让周期算出来正好一半,这个错很像"信号源频率不对",实际是配置问题。

3.3 幅值:RMS 与峰值检波的取舍

有效值有两条实现路线。模拟峰值检波加二极管补偿,速度快、代码少,但对非正弦波误差大;数字 RMS 对任意波形都准,代价是要占用运算时间并要求采样等距。综合测评的题面若明确写"正弦波",两种都行;若写了方波或含谐波,只能走数字路线。

/* 去直流后做平方累加,定点累加防溢出,最后开方还原 */ float Calc_Rms(const uint16_t *buf, uint16_t n, int32_t offset) { uint64_t sum = 0; for (uint16_t i = 0; i < n; i++) { int32_t v = (int32_t)buf[i] - offset; /* 减掉偏置,得到真实摆幅 */ sum += (uint64_t)(v * v); /* 累加值最大约 256*4096^2,64 位够用 */ } /* adc_lsb 为每 LSB 对应的电压,含前端增益与基准系数 */ return sqrtf((float)sum / (float)n) * adc_lsb; }

偏置 offset 不能直接取 2048,要用实测值:输入端短接到地时采一帧求平均,得到的数才是这一块板子这一路的真实零点,它包含了运放失调和基准偏差。这一步做完,小信号挡位的零点误差能从几十毫伏压到几毫伏,是性价比最高的一处修正。

失败时先看 offset 是不是每次上电都变,如果变化超过 ±30 LSB,多半是模拟地和数字地混铺导致 ADC 参考脚带进了开关噪声,这时候改代码没用,得回去处理地去耦。

3.4 直流输出:DAC 加跟随器的线性区

输出支路最容易犯的错是把 DAC 引脚直接当电源用。DAC 输出阻抗在几十千欧量级,接任何负载都会掉压,必须后接一级运放跟随。同时要注意运放的输出摆幅到不了电源轨,3.3 V 供电的轨到轨运放实际可用上限约 3.2 V,下限约 0.1 V,标定时要避开这两段。

/* 按标定直线反解码值:v = k * code + b */ void Out_SetVoltage(float v) { float code = (v - cal_b) / cal_k; if (code < 0.0f) code = 0.0f; if (code > 4095.0f) code = 4095.0f; HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, (uint32_t)(code + 0.5f)); }

这里的 cal_k 和 cal_b 来自标定流程,不是理论值。理论斜率是 3.3/4096,实际因为基准偏差和运放增益误差,斜率常常差 1%~2%,把理论值写死,输出精度就卡在 2% 上不去。加 0.5 再取整是四舍五入,不加的话码值永远偏小,多挡位叠加后误差会累积成可见的偏差。

3.5 人机交互:按键状态机与显示节流

现场调试时,按键和屏幕的稳定性直接影响你判断故障的速度。用延时消抖会把主循环卡住,采样帧一断,RMS 就跳变;正确写法是状态机加时间戳,主循环里非阻塞地走。

typedef enum { KEY_IDLE, KEY_DEBOUNCE, KEY_DOWN, KEY_REPEAT } key_state_t; /* 每 10 ms 调一次,返回值:0 无动作 1 短按 2 长按 */ uint8_t Key_Scan(uint8_t raw, uint32_t now_ms) { static key_state_t st = KEY_IDLE; static uint32_t t_press = 0; switch (st) { case KEY_IDLE: if (raw) { st = KEY_DEBOUNCE; t_press = now_ms; } break; case KEY_DEBOUNCE: if (now_ms - t_press < 20) break; /* 20 ms 消抖窗口 */ st = raw ? KEY_DOWN : KEY_IDLE; if (raw) return 1; /* 短按只上报一次 */ break; case KEY_DOWN: if (!raw) st = KEY_IDLE; else if (now_ms - t_press > 800) { st = KEY_REPEAT; return 2; } break; case KEY_REPEAT: if (!raw) st = KEY_IDLE; break; } return 0; }

显示刷新同理,OLED 整屏刷新一次约 10 ms,每帧采样都刷会把主循环拖慢。我的做法是 100 ms 刷一次,且只在数值变化超过显示分辨率时才写屏,这样按键响应和采样节拍都不受影响。综合测评现场最怕的就是"屏幕在闪、按键不灵",八成是刷新没节流。

4. 标定与误差补偿:把"能跑"推到"指标内"

能跑通和能拿分之间隔着一整套标定。综合测评的评分表上,功能分和精度分是分开的,功能做出来只保证不难看,精度要靠标定把系统误差一项项抠掉。标定的顺序不能颠倒:先修零点,再修增益,最后修非线性,因为后一步的残差会污染前一步的判据。

4.1 两点标定与分段修正

最省事的做法是两点标定,用一条直线拟合整个量程。做法是输入一个接近满量程下限的标准值,读一次码值;再输入一个接近上限的标准值,再读一次,解出斜率和截距。这两点要选在量程的 10% 和 90% 附近,不要选在两端,因为两端正好是运放和 ADC 线性最差的地方。

若题面要求在整个量程内都进 1%,两点直线往往不够,因为运放的失调和 ADC 的积分非线性会让中间段鼓出来。这时候用分段线性:把量程分四段,每段存一组 k、b,查表时按输入范围选段。分段数不是越多越好,四段之后残差已经接近噪声水平,再加段只是把噪声拟合进去。

标定参数要掉电保存,写进 Flash 的最后一页。

typedef struct { uint32_t magic; /* 0x5A5A0001,用来判断是否已写过 */ float k[4]; float b[4]; int32_t offset_ch1; /* 第一路零点,单位 LSB */ uint16_t crc; } cal_t; /* 上电读取:magic 不对或 CRC 不过就回退到默认值 */ void Cal_Load(cal_t *c) { memcpy(c, (void *)CAL_ADDR, sizeof(cal_t)); if (c->magic != 0x5A5A0001u || Cal_Crc(c) != c->crc) { for (int i = 0; i < 4; i++) { c->k[i] = 3.3f / 4096.0f; c->b[i] = 0.0f; } c->offset_ch1 = 2048; } }

加 magic 和 CRC 两个字段,是为了防止标定值写了一半掉电,下次上电读到半截数据,整台设备的读数全部错位——这种故障在现场极难查,因为程序逻辑完全正常。这个习惯我后来在所有需要掉电保存参数的项目里都保留着。

4.2 零漂与温漂的现场处置

综合测评从进场到测评结束通常有几个小时,板子会从室温升到明显发烫。运放的失调电压随温度变化,几十微伏每度的漂移在 1000 倍增益下就是几十毫伏的输出偏移,小信号挡位直接崩掉。

处置办法有两个层次。硬件上是选低失调低温漂的运放,并在反馈网络里让正负端对地阻抗尽量匹配,抵消偏置电流引起的漂移。软件上是在每一挡增益切换后、正式测量前,做一次"短接校零":把输入通过模拟开关短接到地,采一帧求平均,把这个数作为该挡位的零点存起来,测量时减掉。这一步只需要 2 ms,但能把温漂带来的偏移压掉大半。

注意:校零动作会占用采样通道,必须在测量间隙做,不能和正式采样并行,否则会把短接期间的数据混进 RMS。

4.3 指标对照表:每一项误差对应哪一段电路

调试时最耗时间的不是改代码,是不知道误差来自哪里。把指标拆成表,一项项对,比反复重烧程序快得多。

指标项目标主要误差来源补偿手段
幅值测量误差≤1%前端增益误差、基准偏差两点/分段标定
频率测量误差≤0.1%晶振偏差、捕获溢出未补溢出计数 + 闸门时间校正
直流输出误差≤1%DAC 增益、运放失调反解标定直线 + 输出端实测校正
零点漂移≤5 mV/小时运放温漂、地线干扰分挡校零 + 单点接地
挡位切换误差≤2%模拟开关导通电阻每挡独立标定,不共用系数

这张表的价值在于建立"现象—电路"的映射。比如测出幅值误差随输入增大而变大,问题多半在前端增益而非基准;误差基本恒定不随输入变,那就是零点没校好。有了这个映射,现场改起来是有的放矢。

5. 避坑与现场排查:五个最容易翻车的点

综合测评的四个小时里,真正让人崩盘的不是难题,是几个反复出现的小坑。下面五条都是我在不同场次里亲眼见过或者自己踩过的,每条按现象、原因、解决写,照着对号入座能省下大量时间。

第一,采样值满量程乱跳,屏幕上数字一直顶到 4095。原因是输入悬空或者前端运放自激,ADC 采到的是运放输出的高频振荡,不是信号。解决是先断开前级,把 ADC 输入直接接地,看读数是否稳定在 0 附近;稳定说明问题在前端,用示波器看运放输出有没有几百千赫的等幅振荡,有的话在反馈电阻上并 10~22 pF 电容做相位补偿,同时检查运放电源脚去耦是否漏焊。

第二,测频读数在高频段成倍跳,10 kHz 信号有时显示 10 kHz,有时显示 5 kHz。原因是捕获极性设成了双边沿,或者定时器预分频被两处代码重复设置。解决是把捕获极性统一为上升沿,并在初始化函数里用注释标死每个定时器由谁配置,避免两个模块各写一遍。这类"改了 A 处 B 处又覆盖回去"的问题在多文件工程里特别常见。

第三,标定完了精度还是上不去,输出和输入的线性度看着还行,但整体偏 1%~2%。原因是标定时用的标准表和设备实际读数是同一个表,等于自己跟自己比。解决办法是引入一个独立的标准源,或者在标定前先用高精度万用表把设备的实际输出量一遍,确认标定链路本身没问题。血泪经验是:标定工具本身也需要被标定。

第四,OLED 一刷新,ADC 读数就跟着跳几十个 LSB。原因是屏幕的 I2C 走线和模拟输入走线并排,或者共用同一路电源且没有隔离。解决的物理手段是走线分开、屏幕电源单独加 100 μF,软件手段是在屏幕刷新期间暂停采样触发,刷完再恢复。综合测评现场没法改板,软件规避是最现实的做法。

第五,整机上电后功能全对,一接上被测量的信号源就死机或者复位。原因是输入端的共模电压超出运放的输入范围,或者信号源的地和板子的地之间存在电位差,电流从输入脚灌进芯片。解决是在输入端串 100 Ω 电阻加双二极管钳位到电源和地,并把信号源和板子接到同一个插排上。这个坑不做保护基本躲不掉,而加保护只要两分钟。

6. 验收与答辩:把指标讲成一条能被追问的链

综合测评的验收通常分两步:先测功能,再测指标,最后有几分钟的问答。功能部分照题面逐条过,不要抢时间,做一条勾一条;指标部分要主动出示你的测量方法,而不是等评委问。我的习惯是提前准备一张单页,正面写指标表,反面写每一路的测试点和测试方法,评委扫一眼就知道你在测什么、怎么测的。

答辩环节的问题高度集中在几个方向,提前把回答骨架想好,现场就不会卡壳:

常见追问回答骨架
你的误差怎么保证的先讲误差分解,再讲标定方法和标定用的基准源,最后给实测残差
换一块板子还能不能达到指标哪些靠器件保证、哪些靠软件补偿,软件补偿的部分可以复用
为什么选这个采样率和块长带宽要求、单周期点数、运算耗时三者怎么折中
高频段为什么不用测频法分辨率随频率下降的定量关系,给切换点的计算依据
温度变了怎么办校零机制 + 器件选型的温漂指标 + 实测的漂移数据

这里有个技巧:所有回答都落到数字上。"精度大概 1%"这种话在答辩里等于没说,"满量程 5 V 下最大偏差 38 mV,折合 0.76%,主要来自前端增益的 0.5%"才是有效回答。评委追问的往往不是你的方案有多先进,而是你有没有真正测过。

还有一点是时间管理。进场后的前 30 分钟只做一件事:确认电源正常、主控能下载、屏幕能点亮,这三件事不通,后面所有调试都无从谈起。中间两小时做功能和初步标定,最后 90 分钟留给精度和稳定性测试,包括至少半小时的连续通电观察漂移。我见过太多队伍把时间全花在功能上,最后半小时才发现小信号挡位误差 5%,已经来不及改。

强调一下记录习惯。从那以后我每次做这类封闭测评,都强制在草稿纸第一页写三行:当前版本改了什么、上一次实测的指标是多少、下一步准备改什么。没有这三行,两个小时后你一定会忘了上一版为什么被推翻,然后重新踩一遍同样的坑。希望帮到你。

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

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

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

立即咨询