电赛H题代码包拆解:评分项到嵌入式工程实践
简介:面向参加电子设计竞赛、准备嵌入式赛题的高校学生与开发者,这套24电赛H题代码包提供了一份可研读的赛题工程参考,可用于理解32位微控制器在竞赛系统中的作用,并对照复现硬件初始化、外设驱动与程序结构。压缩包共254个文件、约8.49MB,以C源文件和H头文件为主体,配有O、D、CRF等编译中间产物,以及Keil工程、MAP/LST映射和调试配置,完整呈现从源码组织到构建输出的工程面貌。已有787人学习,说明它在电赛备赛群体中具有一定参考价值。工程内还保留中断处理、数据采集与通信接口等竞赛常见环节的代码线索,便于对照H题需求梳理架构、验证模块,也能为自建工程提供目录组织与构建配置参照。读者可观察STM32标准外设库的调用方式,学习定时器、ADC、I2C、CAN等外设配置思路,并借鉴编译输出检查与问题排查方法。
1. 一份赛后定版的代码包,为什么值得当成真题来读
电赛(全国大学生电子设计竞赛)从来不公布标准答案,赛场上跑通的工程也很少完整留到第二年,所以一份按日期定版的 H 题代码包,价值不在照着抄,而在它把评分表上的每一栏都翻译成了具体的文件和函数:哪一段对应基本要求、哪一段对应发挥部分、哪个参数是现场现调的。四天三夜里真正拉开差距的,从来不是谁用了更高级的算法,而是谁的基本要求一项没丢、谁的发挥指标能稳定复现、谁站在测评台前能把参数讲清楚。这份命名里带 20241020 的包适合三类人:第一次参赛想搞清工程怎么搭的、赛前想复盘自己哪一步慢了的、答辩时被问住过的。下面按考什么、高频考点、真题怎么答、哪里容易翻车的顺序拆开。
2. 考点分布:测评表上真正按项打分的四栏
电赛的分数不像考试那样按题给分,它是一栏一栏打勾的。搞不清这个结构,代码写得再漂亮也可能在某一栏上直接归零。这一章先把评分结构讲透,再说代码工程该怎么对着这张表长出来。
2.1 基本要求拿满只是及格线,不是优势
多数赛区把评分拆成基本要求、发挥部分、设计报告三块,基本要求通常占一半上下,各赛区略有差异。关键在于基本要求的判分方式:它更接近“做到给分、做不到整项归零”,而不是“做了一半给一半分”。一个指标要求测量范围覆盖某个下限到上限,你只做到中间一段,这一小项很可能直接判零,不给过程分。
这就决定了时间分配的原则——基本要求必须做到 100% 稳定,不能停在“差不多能跑”。赛场上最常见的翻车是:前三天全押在发挥部分,第四天上午回头补基本要求的第三小项,发现传感器在小量程段根本没法稳定读数,剩下的时间只够改到“能出数但不可信”,测评时评委一测边界点就露了。
代码层面能看出一个队伍有没有想明白这件事:基本功能那条主路径应该是干净的、外设依赖最少的、能在最差的那块板子上跑通的。发挥部分才是加花活的地方,所有花活都应该能被一个开关关掉,关掉之后主路径照常工作。这份 H 题代码包里主函数的分层方式就是这个思路——基础流程一条线走到底,增强功能挂在旁边。
提示:赛前把基本要求的每一项单独列出来,逐项标上“稳定 / 偶尔成功 / 没做”,只允许有一项是“偶尔成功”,其余必须是稳定。
2.2 发挥部分的每一项都要能单独演示
发挥部分是阶梯式加分,评委按项打勾,不会因为你做了三项就给第四项的辛苦分。所以每一个发挥项在代码里都要有独立的触发入口,能在一秒之内切过去演示,而不是“要重新烧一次固件才能看到”。
现场测评的时间非常紧,一支队伍常常只有几分钟。评委说“展示第二项”,你如果还要低头改宏定义、重新编译、等下载,这几分钟就没了。合理的做法是做一个功能索引表,把评分项和代码入口一一对上,测评前一晚把这张表打印出来贴在桌上。
| 评分项类型 | 代码里的入口形式 | 现场演示动作 | 建议耗时 |
|---|---|---|---|
| 基本功能 | 上电自启动的主流程 | 直接上电,等自检完成 | 30 秒内 |
| 指标提升类 | 带参数的函数调用 | 按键切换到对应档位 | 20 秒内 |
| 附加功能类 | 独立的任务函数 | 串口发一条切换指令 | 20 秒内 |
| 异常保护类 | 保护状态机分支 | 人为制造超限,看是否自动退出 | 40 秒内 |
最后一行经常被忽略,但“保护动作”本身就是不少赛题的发挥项之一,而且它能证明你的系统不是靠运气跑起来的。演示保护时不要怕出问题,评委更愿意看到系统自己刹住,而不是你手忙脚乱拔电源。
2.3 代码工程结构怎么对着评分表长出来
一份能在测评现场救命的工程,目录分层要按“改动频率”来切,而不是按“文件类型”来切。推荐四层:板级支撑层放引脚定义、外设初始化、器件驱动;算法层放滤波、闭环、解算;应用层放业务流程和状态机;配置层放所有可调参数和标定值。
这样切的好处很直接:换一块板子只动板级层,调指标只动配置层,答辩时评委问“这个参数在哪”,你能当场翻到那一行。反面的写法是把所有东西堆进一个主文件,改一个限幅值要顺着两千行往下滚,滚错一行就多出一个玄学 bug,最后连自己都不敢动。
顺带说一个容易被低估的点:配置层单独成文件之后,参数就可以做成“上电从存储区读、串口可改、改完可保存”。这既是发挥项,也是现场校准的后悔药。
2.4 现场答辩与综合测评的分值拿法
答辩时间通常只有几分钟,评委会问的问题高度集中,基本跑不出三类:方案为什么这么选、指标是怎么测出来的、边界条件在哪里。这三问对应三种准备:一页方案对比、一张测量方法说明、一份边界测试记录。
很多队伍栽在第二问上。他们能说出“误差小于 2%”,但说不出这个 2% 是用什么仪器、在什么量程、测了几个点、取的是最大值还是平均值。评委一追问就露底,这一项的分就悬了。正确的答法是:说清测量设备、测量点位、重复次数、取值方式,再补一句“最差点出现在量程的哪一端”。
第三问更狠,评委常故意问“如果环境温度变了还准吗”“换一个同型号器件还能跑吗”。这时候不要硬撑说“没问题”,说清已知边界反而加分:在什么条件下会退化、退化的原因是什么、代码里有没有对应的补偿或保护。
3. 高频考点:采样、滤波、闭环、通信四个必写模块
不管哪一道题,代码里反复出现的就这几块。它们不算难,但写错一个细节就会在现场反复翻车。这一章按上电到运行的顺序讲,每块都给出可以直接对照的写法。
3.1 主控初始化的固定顺序,错一步后面全是玄学
初始化顺序不是风格问题,是因果问题。时钟没定下来,后面所有分频都是错的;引脚没先拉到安全电平,上电瞬间执行机构可能抽一下;DMA 没在 ADC 启动前挂好,第一帧数据必丢。这份 H 题代码包里的初始化就是按依赖关系排的,顺序值得直接背下来。
/* 模拟项目:某闭环控制Demo 的初始化骨架,HAL 风格 */ void Board_Init(void) { HAL_Init(); /* 1. 系统节拍与中断向量表打底 */ SystemClock_Config(); /* 2. 时钟最先定,后续分频全部依赖它 */ MX_GPIO_Init(); /* 3. 引脚先给安全电平,避免上电抽动 */ MX_TIM2_Init(); /* 4. 定时器提供控制与采样的统一时基 */ MX_DMA_Init(); /* 5. DMA 必须在 ADC 启动前挂好 */ MX_ADC1_Init(); /* 6. ADC 由定时器触发,采样率才可控 */ MX_USART1_UART_Init(); /* 7. 调试口最后开,避免先吐乱码 */ HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0); /* 控制中断优先级最高 */ HAL_NVIC_SetPriority(ADC1_IRQn, 2, 0); HAL_NVIC_SetPriority(USART1_IRQn, 3, 0); /* 通信可以让一让 */ HAL_TIM_Base_Start_IT(&htim2); HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN); }这段代码的关键不在函数名,而在三处因果关系。第一处是时钟配置必须早于所有外设,因为串口波特率、定时器计数频率、ADC 采样时间全是从系统时钟分频出来的。第二处是 DMA 初始化和 ADC 启动的先后,ADC 一旦开始转换就会往缓冲区写,DMA 通道没配好就会出现首帧数据错位,表现为“上电第一次读数永远偏大”。第三处是中断优先级:控制和采样必须高于串口,否则一次长报文接收就能把控制周期拖乱。
参数上要盯的是ADC_BUF_LEN。它决定 DMA 缓冲区能存多少个采样点,取太小会在高频采样时溢出,取太大会让半满中断的响应变慢。经验做法是让缓冲区长度等于“一个控制周期内的采样点数”乘以 2,这样半满中断正好落在控制周期边界上,读到的是一整段完整数据。
注意:初始化顺序改动的代价很高,赛场上不要为了“看起来整洁”去重排。改之前先想清楚谁依赖谁。
3.2 采样与滤波:把抖动压到指标以内容
采样精度不够,后面闭环调得再漂亮也没用。常见的问题是原始数据抖得厉害,队伍第一反应是把闭环增益调小,结果响应慢得像蜗牛,发挥项的响应时间直接超。正确顺序是先治数据,再调环。
最省算力的两种滤波是滑动平均和一阶低通。滑动平均对周期性干扰有效,但会引入固定延时;一阶低通延时可算,参数好调,适合放在控制周期里。高频采样中断里绝对不要跑浮点,定点实现足够用。
/* 模拟项目:一阶低通,定点实现,可直接放进 10kHz 中断 */ #define ALPHA_NUM 1 /* 新值权重分子 */ #define ALPHA_DEN 8 /* 分母,等效时间常数约 8 个采样周期 */ static int32_t filt; int32_t LowPass_Update(int32_t raw) { /* filt += (raw - filt) * 1/8,全程整数运算 */ filt += ((raw - filt) * ALPHA_NUM) / ALPHA_DEN; return filt; }这段的逻辑是把“新值和旧值之差”按比例补进去,比例越小越平滑、越滞后。等效截止频率约为采样率除以 2π 再除以等效周期数:采样率 10kHz、分母取 8,截止点大概在 200Hz 附近。分母每加一倍,平滑度上一个台阶,相位滞后也跟着上一个台阶。
调用位置很关键。它必须放在 ADC 采样完成之后、闭环计算之前,而且调用频率必须固定。有的人把它放进主循环里,主循环一忙就掉帧,滤波出来的曲线跟着抖,现场根本没法解释。参数怎么定:先用示波器看原始波形的噪声幅度,再定分母,边加边看响应时间,直到响应时间刚好卡在指标以内就停手,不要为了“更平滑”继续加。
3.3 闭环参数整定:增量式 PID 的落地写法
控制类题目绕不开闭环,而闭环最容易出的问题是积分饱和——输出顶到上限还在累加,等到该往回走的时候要很久才退出来,表现为超调巨大、来回振荡。增量式 PID 天然规避这个问题,因为它算的是“本次相对上次的修正量”,不保存历史积分项。
/* 模拟项目:增量式 PID,系数放大 1000 倍做定点 */ typedef struct { int32_t kp, ki, kd; /* 放大 1000 倍后的系数 */ int32_t err, err1, err2; /* 本次、上次、上上次误差 */ int32_t out, out_max; } PID_Inc_t; int32_t PID_Inc_Calc(PID_Inc_t *p, int32_t set, int32_t fdb) { int32_t d; p->err = set - fdb; /* 增量 = Kp*Δe + Ki*e + Kd*(e - 2e1 + e2) */ d = p->kp * (p->err - p->err1) + p->ki * p->err + p->kd * (p->err - 2 * p->err1 + p->err2); d /= 1000; /* 还原放大倍数 */ p->out += d; /* 在上次输出基础上叠加 */ if (p->out > p->out_max) p->out = p->out_max; /* 输出限幅 */ if (p->out < -p->out_max) p->out = -p->out_max; p->err2 = p->err1; p->err1 = p->err; return p->out; }三个参数的含义在这段里很直白。kp管“差多少补多少”,决定响应快慢;ki管“差着的时间越久补得越多”,负责消静差;kd管“变化趋势”,压制冲过头。系数放大一千倍是为了避开浮点,改参数时只要改整数就行。
整定顺序建议照这个走:先把ki、kd清零,只加kp到系统开始等幅振荡,记下这个临界值,取它的六成作为起点;再加ki,从很小开始加到静差消失为止,注意观察超调;最后加kd压制超调,加过头会让系统对噪声敏感,表现为输出抖动。这套方法不新鲜,但比“凭感觉随便填”靠谱得多。
调用周期必须固定,这一点面试官和评委都爱问。PID 的两个相邻误差之间默认隔了一个固定时间,周期一变,微分项的含义就变了,整定好的参数全部失效。定时器中断里做闭环、主循环只负责显示和通信,就是为这个。
3.4 通信帧与状态机:现场演示不要卡死
测评现场最尴尬的画面是程序卡在一个while里等数据,评委站在旁边看你重启。通信解析必须写成非阻塞状态机,收到什么处理什么,收不全就退回去等下一帧。
/* 模拟项目:串口帧解析,帧格式 AA 55 | LEN | CMD | DATA | CRC8 */ typedef enum { WAIT_H1, WAIT_H2, WAIT_LEN, WAIT_CMD, WAIT_DATA, WAIT_CRC } ParseState_t; static ParseState_t st = WAIT_H1; static uint8_t len, idx, cmd, data[32]; void UART_ParseByte(uint8_t b) { switch (st) { case WAIT_H1: if (b == 0xAA) st = WAIT_H2; /* 等第一个帧头 */ break; case WAIT_H2: /* 第二个字节不对就退回,而不是死等 */ st = (b == 0x55) ? WAIT_LEN : WAIT_H1; break; case WAIT_LEN: len = b; idx = 0; if (len > sizeof(data)) { st = WAIT_H1; break; } /* 长度越界直接丢 */ st = WAIT_CMD; break; case WAIT_CMD: cmd = b; st = WAIT_DATA; break; case WAIT_DATA: if (idx < len) data[idx++] = b; else { /* 最后一字节是校验,验不过就整帧丢弃,不做部分采纳 */ if (CRC8(data, len) == b) Dispatch(cmd, data, len); st = WAIT_H1; } break; default: st = WAIT_H1; break; } }这段的关键设计有三处。双字节帧头用来对抗误同步,单字节帧头在噪声大的场合很容易被数据里的同值字节骗到。长度越界检查是非做不可的,否则一个错误长度就能把缓冲区写穿,表现为“偶尔莫名其妙重启”,这是赛场上的经典黑匣子问题。校验不过整帧丢弃而不是只丢一个字节,是为了避免错帧被当成正确帧的一部分。
Dispatch函数里只做状态切换和参数赋值,不要在这里面做耗时计算。串口中断里跑重活会拖慢响应,还可能和控制中断打架。参数上,data缓冲区给 32 字节够大多数指令用;真正要传大块数据时不要加大缓冲区,改成带偏移量的分包,这样内存占用可控。
4. 真题演练:赛题指标怎么反推成代码参数
赛题发下来那一刻,大多数队伍直接从“用什么芯片”开始想,其实正确的入口是“指标要求什么”。指标反推方案,方案反推参数,参数反推代码结构,这条链子顺下来,后面改起来才不会推倒重来。这一章用三类常见指标要求做演练,每类给出评委最想听的答法。
4.1 三道指标题,各自怎么落到参数上
第一类是范围与分辨率指标,比如要求输出频率覆盖很宽的范围、步进很小。这类题的第一个分岔是方案选择:用定时器直接翻转引脚,频率越低越准、频率越高分辨率越差;用查表加数模转换,分辨率好但高频段受器件限制。答题思路是先算分界点——把两种方案的适用范围各算一段,写清哪一段用哪种,切换点在哪,切换时怎么避免跳变。评委听到“分段设计加无扰切换”基本就认可你想清楚了,听到“就用一种方案硬扛”就知道你没算过。
第二类是时间指标,比如响应时间不超过某个上限、超调不超过某个百分比。这两个指标互相拉扯,先把控制周期定下来:控制周期至少要快于响应时间要求的一个数量级,才谈得上“跟得上”。然后再定执行机构的动作范围,最后才整定参数。答辩时的正确顺序是:先说控制周期是多少、为什么取这个值,再说执行机构的死区有多大、怎么补偿,最后说参数在什么工况下标定的。
第三类是精度指标,比如测量误差不超过某个百分比。误差这事儿要拆成三块讲:传感器本身的精度、采样链路引入的误差、算法引入的误差。第一块靠选型,第二块靠基准源和采样位数,第三块靠滤波和标定。面试官最想听的是你知不知道“总误差不是简单相加”,以及你有没有实测过最差点出现在量程的哪一端。
4.2 测评现场的三个高频提问,答到点子上
第一个问题通常是“为什么选这个方案”。不要背优点清单,要讲取舍。标准答法是:先说不选的那个方案卡在哪——成本、精度、调试时间、器件是否好买,再说明选中的方案在这个约束下最优。四天三夜的比赛,时间本身就是硬约束,你把“调试时间不够”作为理由讲出来,反而显得真实。
第二个问题是“这个指标怎么测的”。答的时候带上器具、点位、次数。比如:用什么仪器测的、测了量程的几个点、每个点重复几次、取的是最大值还是均值、最差点在哪。这句话说出来,评委就不会往深里追。
第三个问题是“断电再上电还能不能跑”。这是在问参数保存和自恢复。如果你做了参数存到非易失存储、上电自检、自检不过退到安全状态,就把这三步说出来。如果没有,也不要编,说清现在的做法和已知的风险点。评委不反感没做完,反感的是不知道边界在哪。
4.3 设计报告里代码部分该放什么
报告里最忌讳贴整段代码。评审看报告的时间很短,贴得越多,重点越糊。该放的是:系统框图、关键模块的流程图、参数配置表、误差测试记录表。
参数配置表最值钱,把采样率、控制周期、滤波器系数、闭环参数、通信波特率列成一张表,标注取值范围和现场实测值。这张表在答辩时也能直接用,一举两得。误差测试记录表按点位列,每个点位写上实测值、参考值、偏差,最后一行写最大值出现在哪。
流程图不要画成软件工程课上的那种大而全的图,只画数据流:数据从哪进来、经过哪几级处理、从哪里出去。评委看的是你能不能把链路说清楚,不是你画了多少个方框。
5. 失分点:五类在测评台上被判低分的常见错误
赛场上的失分大多不是不会,而是会了没做对。下面五条是每年都在重复的,每一条都按错在哪、为什么错、怎么答对来讲。
5.1 基本要求没跑通就去堆发挥部分
错在哪:时间全花在提高指标上,基本要求的边界点没测过,测评时一测就出问题。为什么错:队伍潜意识里觉得基本要求简单、随时能补,但基本要求的难点往往在边界条件,而边界条件恰好需要最长的调试时间。怎么答对:赛题发下来当天就列一张基本要求清单,逐项标注“稳定、偶尔成功、没做”,并且规定只有全部稳定之后才允许动发挥部分。清单贴在墙上,每天收工前更新一次。
5.2 中断里放延时、浮点和打印
错在哪:采样中断里写了一个毫秒级延时等器件响应,或者在里面调用了打印函数。为什么错:中断里延时会把整个时基拖乱,控制周期变得不固定,之前整定好的参数全部失效;打印函数内部往往带缓冲和忙等,在中断里调用轻则丢字符,重则死锁。怎么答对:中断里只做三件事——取数据、置标志、算增量。所有耗时动作挪到主循环,用标志位驱动。需要调试输出时改成往环形缓冲区里写,主循环里再发出去。
5.3 参数写死在代码里,换一台设备就崩
错在哪:滤波系数、闭环参数、零点偏移全部写成常量。为什么错:同一批器件之间也有离散性,赛场提供的设备和你调试用的未必是同一台,硬编码的参数在别人手上就是错的。怎么答对:把所有可调参数集中到一个配置结构体,上电从非易失存储读取,读不到就用默认值;串口提供读写指令,现场发现不对可以当场改、当场存。这一条本身就是不少赛题的发挥项。
5.4 没有一键复位和现场校准
错在哪:程序一旦卡住只能重新上电,上电之后又要手动走一遍流程。为什么错:测评现场没有时间让你走流程,评委看的是系统能不能自己回到可用状态。怎么答对:留一个按键做软复位,复位之后进自检,自检通过直接进主流程;再留一个校准入口,一键采集零点,采完自动存。这两个动作各占二十秒,但能把一整项的分捞回来。
5.5 最后一晚版本失控
错在哪:最后一晚还在大改结构,改完编译不过,回头发现旧版本已经被覆盖。为什么错:压力下人的判断力会下降,越急越想推倒重来,而重来的代价在最后几个小时里根本付不起。怎么答对:从第一天起就定规矩——每完成一个可演示的状态就打一次标签、拷一份完整工程,命名带日期和一句说明。最后一晚只允许改参数,不允许改结构。这条规矩听着笨,但它是唯一能让你在凌晨三点还有后悔药的办法。
6. 答题技巧:四天三夜的代码排期与现场话术
时间排期的核心原则是“先做减法再做加法”。第一天不写业务代码,只做三件事:定方案、搭工程骨架、把最小可运行链路跑通——上电能亮、串口能发、定时器能进中断。这三件事做完,后面所有工作才有地方挂。
第二天攻基本要求,一项一项过,每过一项就在清单上打勾并保存一份工程副本。第三天上午开始做发挥部分,中午之前必须有一项能演示,下午补第二项。第四天上午只做联调和边界测试,中午之后封版,只改参数不改结构,下午准备答辩材料。
| 时间节点 | 代码任务 | 验收标准 |
|---|---|---|
| 第一天 | 工程骨架、时基、通信链路 | 上电自检通过,串口能收发 |
| 第二天 | 基本要求逐项实现 | 清单全部标“稳定” |
| 第三天上午 | 发挥部分第一项 | 能独立触发演示 |
| 第三天下午 | 发挥部分第二项、保护逻辑 | 人为超限能自动退出 |
| 第四天上午 | 边界测试、参数标定 | 量程两端都能稳定读数 |
| 第四天下午 | 封版、答辩材料 | 工程可复现,参数表打印 |
现场话术就一条原则:说取舍,不说优点。评委问方案,你回答“在可买到的器件里,这个方案的调试时间最短,代价是高频段精度差一点,我们用分段补偿压下去了”;评委问指标,你回答“测了五个点,每个点十次,取最大值,最差点在量程上限”。这种答法不带一点夸张,但每一句都在证明你想过。
我自己的习惯是从那以后每次比赛,第一天收工前都强制走一遍“拔电重插”流程:拔掉电源、等三秒、重新上电,看系统能不能自己回到可用状态。第一次这么做的时候,好几支队伍当场发现问题——参数没保存、自检卡住、初始化顺序有依赖。这个动作花不到一分钟,但它是整场比赛里性价比最高的一分钟。希望帮到你。
这份 H 题代码包的价值也在这里:它把一个完整工程的初始化顺序、参数分层、状态机写法、保护逻辑都留了下来,你可以对着它把自己的工程结构改一遍,也可以直接拿它当基线,把参数换成自己板子上的实测值。想要这份定版工程的,按页面上的方式取走就行。
本文还有配套的精品资源,点击获取