蓝桥杯物联网省赛:矩阵按键、LoRa与ADC非阻塞联动 简介面向备战2024年蓝桥杯第十五届物联网省赛的选手与指导教师的完整代码包围绕矩阵按键扫描、LoRa双向通信和电位器模拟量采集三类赛题任务组织可作为赛前联调、方案对照与薄弱环节补强的实践参考。压缩包共371个文件、约11.27MB以h头文件与c源文件为主体配有o、d、crf等编译中间文件以及uvprojx、uvoptx工程配置、hex烧录文件和map、lst等链接与列表输出另含ioc与mxproject等STM32CubeMX工程资料便于直接打开工程复现实验。资源包含LORA_A、LORA_B两套嵌入式工程体现LoRa模块收发配置、按键值处理、ADC取值与数据封装传输的完整链路。已有189人学习适合具备STM32与HAL库基础、希望从能跑通到理解排错思路的参赛者参考。1. 蓝桥杯物联网省赛三样单独跑都正常一合并就全乱省赛现场最反直觉的一幕不是写不出功能而是矩阵按键、LoRa 通信、电位器取值三样单跑都正常一合并就全乱按键按下去要等两秒才有反应LoRa 一发数据串口就丢字节电位器的值在屏幕上跳得像心电图。蓝桥杯物联网省赛考的从来不是“会不会写某个外设驱动”而是这几个模块能不能在同一个 10ms 节拍里互不打架。拿分的逻辑很实在赛题基本按“看得见的现象”给分显示对不对、按键响应快不快、双机数据能不能对上都算分点。这场考试适合已经把主控板玩过一轮、能点亮屏幕能读串口的人不适合从零起步。下面按考点分布、三个高频模块、易错点、答题写法一路拆开能直接拿去练。2. 考点分布省赛编程题的分值落在哪几个功能点上2.1 卷面结构一道题被拆成多少个可观测的功能点物联网省赛的编程题通常给一套带扩展板的硬件平台赛题描述是一段场景文字下面跟着若干条功能要求。这些要求看起来是“实现某某功能”落到评分上其实是被拆成一个个可以现场演示的现象屏幕有没有正确显示、按键按下后数值有没有按预期变化、两块板之间的数据能不能对上、电位器转动时输出有没有跟着线性变化。所以读题的第一件事不是打开工程写代码而是拿笔把赛题拆成“基础分”和“联动分”两层。基础分是单模块能独立跑通的部分比如上电初始化、屏幕显示固定界面、按键能读到键值、ADC 能读到原始值联动分是把两个以上模块串起来的部分比如按键切换页面后屏幕内容跟着换、电位器数值变化后通过 LoRa 发到另一块板并在对方屏幕显示。拆完你会发现一个规律基础分占的分值不小但难度低属于必须全拿的部分联动分的分值集中在少数几条要求上是拉开差距的地方。血泪经验是很多人一上来就冲着联动分写结果基础分的显示还没调通最后两头空。合理的顺序永远是先把基础分全部跑成“上电即可见”再动联动。还有一层容易被忽略的赛题里对时间参数的要求。像“按键响应时间小于 100ms”“数据每 200ms 上报一次”“长按 2 秒进入设置”这些数字不是装饰评分时会拿秒表卡。凡是赛题里出现的毫秒数、秒数、次数都要在代码里找到对应的常量并写注释这是最容易被漏掉的送分点。2.2 三个必拿分模块的判分口径矩阵按键、LoRa 通信、电位器取值这三块几乎每年都以不同形式出现判分口径也很固定。下面这张表是准备时该盯住的东西模块评分看什么常见给分点典型丢分点矩阵按键按下到现象变化的延迟、键值是否唯一单键识别正确、短按长按区分、连按不丢键用阻塞延时消抖、多键同时按下串键、松手才响应LoRa 通信双机数据能否对上、连续发是否丢包帧头帧尾正确、校验通过、超时能重发发送前不判忙、接收不用缓冲、粘包后解析错位电位器 ADC数值是否稳定、换算是否线性原始值范围正确、电压/百分比换算对、抖动小分辨率当 1024、参考电压搞错、单次采样直接用这张表里最值得琢磨的是“典型丢分点”那一列。它们有一个共同特征代码能跑现象也有但经不起连续操作和长时间观察。评分老师不会只看你按一下他会连着按、快速转、让两块板连续通信几分钟。所以练的时候也要按这个标准自测别停在“能亮就行”。2.3 赛前四周的准备顺序时间紧的时候顺序比努力重要。四周的节奏可以这么排第一周只做一件事把矩阵按键的扫描、消抖、短按长按全部写成不阻塞的定时任务按键事件通过一个队列抛给应用层应用层拿到事件再决定干什么。这一周的目标是“按多快都不卡”。第二周啃 LoRa。先做到单向发一帧能被对方正确解析再加上校验、超时重发、接收环形缓冲。目标是在连续发几千帧的情况下不丢、不乱序。第三周做 ADC。原始值、电压值、百分比三层数据同时能读出来加上中值滤波和滑动平均观察静止时数值波动是否在一个很小的范围内。第四周才做联动把三个模块塞进同一个节拍重点观察互相干扰按键时通信会不会断、通信时按键响应会不会变慢、ADC 采样时屏幕刷新会不会闪。这个顺序的道理很直白——单模块不稳联动一定崩单模块稳了联动只是排时序的问题。翻车最多的不是不会写是不肯在单模块上多待两天。3. 矩阵按键行扫描、消抖与短按长按的状态机3.1 4×4 矩阵按键的接法与扫描真值矩阵按键能用 8 根 IO 读出 16 个键靠的是行列交叉。常规接法是四根行线做输出、四根列线做输入并上拉扫描时逐行拉低其余行保持高电平然后读四根列线哪一列读到低电平就说明那一行那一列的键被按下。这套逻辑的真值很干净行 r 拉低、列 c 读到 0键号就是 r×4c。没有按键时四列全是高电平读到 0xFF。实践里最容易出问题的是“幽灵键”——同时按下三个键时某一行拉低会让另外两个键形成通路读出一个根本不存在的键。彻底解决要靠二极管隔离赛场上做不到那就退一步扫描时只认第一路读到的低电平并且在消抖窗口内要求读到的是同一个键才作数这叫“一致性校验”能挡掉大部分串键。还有一个接线细节要确认行线是推挽输出还是开漏输出。推挽输出拉低时电流会灌进按键如果硬件上列线没有串电阻长期这么干不太友好开漏输出配外部上拉更稳。赛场上没法改板子但至少要知道自己用的是哪种读不到值的时候先怀疑这里。3.2 扫描代码10ms 节拍 三态状态机下面这段是矩阵按键的核心思路是“扫描只管读消抖交给状态机事件交给队列”。整个函数每 10ms 被定时器调用一次绝不阻塞。/* 4x4 矩阵按键行输出、列输入上拉10ms 定时节拍调用 */ #include stdint.h #define ROW_NUM 4 #define COL_NUM 4 #define DEBOUNCE_TICKS 2 /* 2 x 10ms 20ms 消抖窗口 */ #define LONG_TICKS 80 /* 80 x 10ms 800ms 判长按 */ typedef enum { KEY_IDLE 0, KEY_DEBOUNCE, KEY_PRESSED, KEY_LONG } key_state_t; static key_state_t s_state KEY_IDLE; static uint8_t s_key 0xFF; /* 0xFF 表示无键 */ static uint16_t s_ticks 0; /* 板级层只把第 r 行拉低其余行拉高 */ extern void key_set_row(uint8_t r); /* 板级层读 4 位列线返回列号 0~3无键返回 0xFF */ extern uint8_t key_read_col(void); /* 应用层投递按键事件KEY_EVT_SHORT / KEY_EVT_LONG */ extern void key_event_post(uint8_t key, uint8_t evt); /* 一次完整扫描返回 0~15 键号无键返回 0xFF */ static uint8_t key_scan_once(void) { for (uint8_t r 0; r ROW_NUM; r) { key_set_row(r); /* 只拉低第 r 行避免多行同时导通 */ uint8_t col key_read_col(); if (col ! 0xFF) { return (uint8_t)(r * COL_NUM col); } } return 0xFF; } /* 每 10ms 调用一次扫描 消抖 短按/长按判定 */ void key_task_10ms(void) { uint8_t now key_scan_once(); switch (s_state) { case KEY_IDLE: if (now ! 0xFF) { s_key now; s_ticks 0; s_state KEY_DEBOUNCE; } break; case KEY_DEBOUNCE: if (now s_key) { /* 20ms 内一直是同一个键才认 */ if (s_ticks DEBOUNCE_TICKS) { s_state KEY_PRESSED; s_ticks 0; } } else { s_state KEY_IDLE; /* 抖动或换键本次作废 */ } break; case KEY_PRESSED: if (now 0xFF) { /* 松手上报短按 */ key_event_post(s_key, 0); s_state KEY_IDLE; } else if (s_ticks LONG_TICKS) { key_event_post(s_key, 1); /* 到达长按阈值上报一次不重复 */ s_state KEY_LONG; } break; case KEY_LONG: if (now 0xFF) { s_state KEY_IDLE; } /* 长按只认一次松手复位 */ break; } }这段代码有三个值得说明的地方。第一key_scan_once里一旦读到低电平立刻返回不做全行扫描这样一次扫描的耗时被压到最短10ms 的节拍留出大量余量给别的任务。第二KEY_DEBOUNCE状态里判断的是“读到的键和候选键相同”而不是简单的计数这挡掉了按下 A 键瞬间被读成 B 键的误判。第三长按事件只在跨过阈值的那一刻上报一次之后进入KEY_LONG状态等松手不会每隔 10ms 就重复上报把队列刷爆。参数怎么调DEBOUNCE_TICKS设 2 对应 20ms手感偏硬但抗抖强设 1 会把 10ms 的机械抖动放进来设 5 响应就明显变慢。LONG_TICKS要看赛题要求题目写“长按 2 秒”就得改成 200。这两个值都写成宏而不是魔数改的时候不用满工程找。3.3 真题演练按键值上报与界面切换怎么答这类题的常见形态是上电后屏幕显示主界面按下不同键进入不同子页面其中某个键长按进入参数设置设置项用另外两个键加减再按一次确认返回。面试官最想听的不是“我写了个 switch”而是你有没有把按键和界面解耦。答题思路按这个顺序说先讲事件队列——扫描层只负责产生“键号 短按/长按”的事件投进一个长度为 8 的环形队列队列满就丢弃最旧的保证不会阻塞扫描再讲应用层怎么消费——主循环里每轮取一个事件交给一个状态机处理状态机维护“当前页面”和“当前选中项”两个变量最后讲边界——在设置页面里如果 30 秒没有按键就自动返回主界面这个超时用同一个 10ms 节拍累加不额外开定时器。具体的代码结构大致是/* 应用层按键事件驱动页面状态机主循环内非阻塞调用 */ typedef enum { PAGE_MAIN 0, PAGE_DATA, PAGE_SET, PAGE_SET_EDIT } page_t; static page_t s_page PAGE_MAIN; static uint8_t s_sel 0; /* 设置页当前选中项 */ static uint32_t s_idle_ms 0; /* 无操作计时用于自动返回 */ void app_key_handler(uint8_t key, uint8_t evt) { s_idle_ms 0; /* 任何按键都重置超时 */ switch (s_page) { case PAGE_MAIN: if (evt 0 key 0) s_page PAGE_DATA; else if (evt 1 key 1) s_page PAGE_SET; /* 长按进设置 */ break; case PAGE_SET: if (evt 0 key 4) s_page PAGE_SET_EDIT; /* 进入编辑 */ else if (evt 1 key 5) s_page PAGE_MAIN; break; default: break; } } /* 每 10ms 调用30 秒无操作自动回主界面 */ void app_tick_10ms(void) { if (s_page ! PAGE_MAIN s_idle_ms 3000u) { s_page PAGE_MAIN; s_idle_ms 0; } }这里有个关键取舍页面状态机里不出现任何“画屏”的代码只改状态屏幕刷新是另一个任务10ms 或 50ms 一次根据当前状态决定画什么。这样按键响应和画面刷新互不牵连按得再快也不会卡屏。评审看的是“按下去屏幕立刻变”至于底层怎么组织说清楚解耦这件事就够拿分了。对了还要主动提一句如果赛题要求按键有提示音或者指示灯反馈反馈必须在事件产生的那一刻做不能等到界面刷新时再做否则会出现“按键响应 200ms”的观感问题。4. LoRa 通信组帧、收发判忙与丢包重发4.1 点对点链路的最小可用流程LoRa 模块在赛题平台上的用法一般是串口透传主控把字节流丢给模块模块负责空口收发对面的模块再把字节流吐给自己的主控。看上去简单坑全在“串口是字节流”这件事上——你发的是帧收到的可能是一半帧、两帧粘在一起、或者中间夹着上电时的杂乱字节。所以最小可用流程必须包含四步。第一步是组帧给数据加帧头、长度、命令字、校验让接收端知道一帧从哪开始、到哪结束、有没有出错。第二步是发送前判忙模块在发送期间会拉高忙标志或者返回忙状态这时候硬塞数据进去会丢包。第三步是接收侧用中断 环形缓冲中断里只把字节塞进缓冲区解析放到主循环里做绝不在中断里做状态机跳转之外的事。第四步是超时重发发出去之后启动一个计时器规定时间内没收到对方的应答就重发重发三次还失败就上报通信故障。这四步里最容易被跳过的是第四步。大家觉得“发出去就行了”可实际空口环境里丢一帧太正常了。赛题如果要求“数据可靠传输”评分时很可能会故意拉开两块板的距离或者用手挡一下没有重发机制的方案直接掉分。4.2 组帧与解析代码帧格式建议固定成帧头两字节、长度一字节、命令字一字节、数据若干、校验和一字节。帧头用 0xAA 0x55 这种交替的位模式比单个 0xAA 更容易从杂乱数据里对齐。/* 上行帧格式AA 55 | LEN | CMD | DATA[0..LEN-1] | SUM */ #define LORA_HEAD0 0xAA #define LORA_HEAD1 0x55 #define LORA_MAX_PAYLOAD 16 typedef struct { uint8_t cmd; uint8_t len; uint8_t data[LORA_MAX_PAYLOAD]; } lora_msg_t; /* 组帧返回总字节数校验从 LEN 开始累加帧头不参与 */ static uint8_t frame_build(const lora_msg_t *m, uint8_t *out) { uint8_t n 0, sum 0; out[n] LORA_HEAD0; out[n] LORA_HEAD1; out[n] m-len; out[n] m-cmd; for (uint8_t i 0; i m-len; i) out[n] m-data[i]; for (uint8_t i 2; i n; i) sum out[i]; out[n] sum; return n; }解析侧用状态机逐字节推进任何时候校验失败就整体回到找帧头的状态这是防粘包和防错位最省事的做法typedef enum { RX_H0, RX_H1, RX_LEN, RX_CMD, RX_DATA, RX_SUM } rx_state_t; /* 串口中断里收到的每个字节都丢进来内部只做状态推进 */ void lora_rx_byte(uint8_t b) { static rx_state_t st RX_H0; static lora_msg_t m; static uint8_t idx 0, sum 0; switch (st) { case RX_H0: if (b LORA_HEAD0) st RX_H1; break; case RX_H1: st (b LORA_HEAD1) ? RX_LEN : RX_H0; break; case RX_LEN: if (b LORA_MAX_PAYLOAD) { st RX_H0; break; } m.len b; idx 0; sum b; st RX_CMD; break; case RX_CMD: m.cmd b; sum b; st (m.len 0) ? RX_SUM : RX_DATA; break; case RX_DATA: m.data[idx] b; sum b; if (idx m.len) st RX_SUM; break; case RX_SUM: if (b sum) lora_msg_post(m); /* 校验通过才投递到应用层 */ st RX_H0; break; } }发送侧必须加判忙和超时别直接把字节写进串口寄存器就完事/* 发送一帧先等模块就绪再整帧写出超时返回 -1 */ int lora_send(const lora_msg_t *m, uint32_t timeout_ms) { uint8_t buf[32]; uint8_t n frame_build(m, buf); if (!lora_wait_ready(timeout_ms)) return -1; /* 模块忙/未就绪先等 */ return (uart_write(buf, n) n) ? 0 : -1; }lora_wait_ready的实现看模块有的模块有 BUSY 引脚轮询电平即可没有的话就查模块的状态寄存器或者等它返回的应答字节。超时值取 100ms 到 500ms 之间比较合理太小会误判忙太大一卡就是半秒。4.3 真题演练双机数据同步题的答法这类题通常是A 板采集电位器和按键状态通过 LoRa 发到 B 板B 板在屏幕上显示同时 B 板可以回发一条确认A 板收到确认后点亮一个指示灯。答题时先讲拓扑一台做主机定时上报一台做从机收到即回执角色固定不做双向对等避免两边同时发导致空口碰撞。再讲时序主机每 200ms 发一帧帧里带序号从机回执时把序号原样带回主机收到匹配的序号才算这一轮成功否则在下一轮重发上一帧。这个“序号回执”的设计比单纯的超时重发更靠谱因为它能区分“帧丢了”和“回执丢了”。然后讲异常处理这是加分的部分从机连续 5 秒收不到主机的帧就认为链路断开屏幕显示一个断连标志等重新收到帧再恢复主机连续 3 次重发失败也进入断连状态并停止快节奏重发改成 1 秒一次慢速探测避免把空口占满。面试官听到这里基本会认可你的工程意识。再补一句“帧里的数据按小端排布浮点数用定点数乘 100 后按整型传避免两边结构体对齐不一致”这个细节能说明你不是只会照着例程抄。5. 电位器取值ADC 换算、滤波与阈值判定5.1 从原始码值到可用物理量的换算电位器接到 ADC 通道上读出来的是 0 到满量程的整数码值。常见的 12 位 ADC 满量程是 4095参考电压一般是 3.3V于是电压换算就是码值乘 3300 再除以 4095单位毫伏。这一步看着简单出错的概率却高得离谱主要是三个地方把分辨率当成 1024、把参考电压当成 5V、除法用整型直接砍掉精度。正确的做法是统一用毫伏做中间量先算raw * 3300 / 4095注意乘法要在除法之前做而且中间结果要用 32 位否则raw * 3300在 16 位下会溢出。百分比则是raw * 100 / 4095同样先乘后除。还有一件常被忽略的事ADC 的采样时间。电位器输出阻抗不低采样时间设得太短采到的值会偏小而且不稳定。赛题平台的初始化代码如果给了默认值一般够用如果自己配采样周期至少给到几十个 ADC 时钟周期。5.2 中值 滑动平均的滤波代码单次采样的值一定在跳直接显示会让人以为板子坏了。滤波的做法是两层先在一小段采样里取中值杀掉毛刺再对中值序列做滑动平均压平随机噪声。/* 电位器采样12 位 ADC参考 3.3V5 次取中值 8 点滑动平均 */ #define ADC_FULL_SCALE 4095u #define ADC_VREF_MV 3300u #define WIN_SIZE 8 static uint16_t s_win[WIN_SIZE]; static uint8_t s_pos 0; extern uint16_t adc_read_raw(uint8_t ch); /* 板级层单次读取原始码值 */ uint16_t adc_read_filtered(void) { uint16_t buf[5]; for (uint8_t i 0; i 5; i) buf[i] adc_read_raw(0); /* 冒泡排序取中值先把偶发的大幅毛刺杀掉 */ for (uint8_t i 0; i 4; i) for (uint8_t j 0; j 4 - i; j) if (buf[j] buf[j 1]) { uint16_t t buf[j]; buf[j] buf[j 1]; buf[j 1] t; } s_win[s_pos] buf[2]; s_pos (uint8_t)((s_pos 1) % WIN_SIZE); uint32_t acc 0; for (uint8_t i 0; i WIN_SIZE; i) acc s_win[i]; return (uint16_t)(acc / WIN_SIZE); } uint16_t adc_to_mv(uint16_t raw) { return (uint16_t)((uint32_t)raw * ADC_VREF_MV / ADC_FULL_SCALE); } uint8_t adc_to_pct(uint16_t raw) { return (uint8_t)((uint32_t)raw * 100u / ADC_FULL_SCALE); }adc_read_filtered里连续采 5 次是在同一个任务周期内完成的耗时取决于单次转换时间一般几十微秒到几百微秒放在 10ms 任务里完全够。中值取buf[2]就是 5 个数里的中间那个。滑动平均的窗口取 8是因为 8 是 2 的幂除法可以被编译器优化成移位而且窗口太长会让转动电位器时数值明显滞后。参数调整的边界窗口拉到 16 以上静止时数值很稳但快速转动时屏幕上的值会比手慢半拍赛题如果要求“实时跟随”就会扣分窗口缩到 4 以下抖动又回来了。8 是个比较中庸的选择具体看赛题的实时性要求。5.3 真题演练电位器控制阈值/占空比怎么答常见题面是电位器模拟一个传感器输入转到某个位置以上点亮 LED转到另一个位置以上让蜂鸣器间歇响同时屏幕显示当前百分比和设定的阈值。答题思路分三层说。第一层讲采样与显示ADC 值经滤波后换算成百分比屏幕每 100ms 刷新一次刷新频率低于采样频率避免屏幕闪烁也避免占用太多时间。第二层讲阈值判定不要拿连续值直接和阈值比要给一个迟滞区间比如上限 70%、下限 65%避免电位器停在阈值附近时输出反复跳变。第三层讲输出LED 和蜂鸣器的动作由判定结果驱动蜂鸣器的间歇响用同一个 10ms 节拍计数分频实现不用额外的软件延时。迟滞这一条是评分里隐形但真实存在的点。没有迟滞的方案在阈值附近会出现指示灯疯狂闪烁评委一眼就能看出来。代码写起来就两行/* 带迟滞的阈值判定上行 70%、下行 65%避免边界抖动 */ static uint8_t s_out 0; uint8_t threshold_update(uint8_t pct) { if (s_out 0 pct 70) s_out 1; else if (s_out 1 pct 65) s_out 0; return s_out; }如果赛题要求用电位器调占空比比如“电位器控制 LED 亮度”那核心是把百分比映射到 PWM 的比较值上映射时注意占空比的范围和定时器周期要对齐别出现 100% 时还有一点点暗、0% 时还微微亮的边界问题。这类题在答的时候主动说一句“我会在两端各留一个死区保证全暗和全亮是真正的全暗全亮”会显得你踩过坑。6. 易错点五个一写就扣分的坑6.1 消抖写成阻塞延时错在哪if (key_down()) { delay_ms(20); if (key_down()) { ... } }这是最经典的写法也是最容易翻车的地方。20ms 内主循环完全停住通信接收、屏幕刷新、ADC 采样全部延后。为什么错赛题是并发场景不是单任务演示。按键按下期间如果正好有 LoRa 帧进来串口会溢出丢字节如果正好该刷新屏幕画面会卡一下。连续快速按键时阻塞会让响应时间累积到几百毫秒。怎么答对消抖交给定时器节拍扫描函数只读电平不等待用状态机记录“连续几次读到同一个键”20ms 之后才认定按下。整个流程零阻塞主循环永远在跑。6.2 LoRa 发送不判忙、接收不校验错在哪uart_send(buf, len)直接发收到字节就往应用层丢既不查模块状态也不验校验和。为什么错模块在发射期间不接受新数据硬塞会丢帧接收侧没有校验一旦空口误码或者串口上电杂波进来应用层就会拿到一堆垃圾数据可能出现“屏幕显示 9999%”这种离谱现象。怎么答对发送前等就绪标志超时就放弃并计一次失败接收侧用状态机对齐帧头校验和不对就整帧丢弃重新找头。这两件事加起来不到三十行代码但直接把可靠性拉上一个档次。6.3 ADC 分辨率与参考电压搞混错在哪代码里写voltage raw * 3.3 / 1024看着挺合理。为什么错12 位 ADC 的满量程是 4095不是 1023用 1024 去除会让满量程时算出 4 倍于实际的电压转到百分比也跟着全错。参考电压如果不是 3.3V 而是板上的另一个基准误差会整体系数级偏移。怎么答对先查清楚 ADC 的位数和板上基准电压把ADC_FULL_SCALE和ADC_VREF_MV写成宏所有换算只引用这两个宏。再用一个已知状态验证电位器拧到最小和最大看算出来的电压是不是接近 0 和基准电压对不上就是参数错了。6.4 中断里做长耗时操作错在哪串口接收中断里直接调用解析函数解析完了直接调用显示函数显示函数里又有软件延时。为什么错中断服务程序应该短进短出。在中断里做几十毫秒的事会阻塞同级和低优先级中断表现为按键丢键、串口溢出、定时器节拍不准。怎么答对中断里只做两件事——把字节塞进环形缓冲、设置一个标志。真正的解析放到主循环或者一个专门的任务里按节拍取数据。判断标准很简单中断函数体应该能在几十微秒内执行完超过这个量级就说明放错地方了。6.5 显示刷新与数据更新绑死错在哪每次 ADC 采样完立刻整屏重绘每次收到 LoRa 帧立刻整屏重绘。为什么错采样 10ms 一次、通信可能几毫秒来一帧屏幕被高频重绘一是闪二是刷屏占用的时间挤掉了其他任务三是如果刷屏里有延时整个系统的节拍都会被拖垮。怎么答对数据更新只改变量屏幕刷新单独一个节奏比如 100ms 一次刷新时读取当前变量的值来画。变量和画面之间加一层“脏标志”数据变了置位刷新时检查标志决定要不要重画某一块。这样采样频率、通信频率、刷新频率三者互不干扰。7. 答题技巧把代码写成卷面能看见的分数写到最后讲一个我自己吃过亏才养成的方法把每一个功能点都做成“可独立演示的最小闭环”再往上叠。具体做法是准备一份自测清单每完成一个模块就在清单上打勾并写一句话说明怎么演示。比如“矩阵按键上电后按 0 号键屏幕第一行从 MAIN 变成 DATA延迟小于 100ms”“LoRa两块板上电A 板每 200ms 发一帧B 板屏幕第二行计数递增拔掉 B 板电源 5 秒后 A 板亮故障灯”。这份清单的价值在于赛场上时间不够时你能立刻知道哪些分已经稳了、哪些还没动。还有一个更实用的习惯给每个关键常量写注释注明它对应赛题的哪一句话。常量取值对应赛题要求DEBOUNCE_TICKS220ms按键响应时间小于 100msLONG_TICKS2002s长按 2 秒进入设置LORA_TX_PERIOD200ms数据每 200ms 上报一次ADC_WIN_SIZE8显示值稳定波动不超过 ±1%PAGE_IDLE_MS300030s无操作 30 秒返回主界面这张表看着朴素作用却不小调参数的时候不用回头翻题面评分的时候也能一眼看出哪个要求还没落地。我带过的一个同学就是靠这张表发现自己漏了“30 秒自动返回”补上之后多拿了一条功能分。最后一个提醒关于时间分配。省赛的编程时间通常够用但不算宽裕比较稳的节奏是读题拆点占 10%三个基础模块各占 20%联动和调试占 30%。如果某个模块卡住超过预期时间的一半先跳过把别的分拿稳再回来。翻车的往往不是难题是在一个消抖上死磕了四十分钟。从那以后我每次准备这类比赛都强制走一遍“清单—常量表—最小闭环”这三步先让每个功能单独能演再谈合并。这个习惯比多背几段例程管用得多希望帮到你。本文还有配套的精品资源点击获取