C语言基础回炉第六天:补漏洞、跑验收、串起 STM32 数据链路
前言
前五天我主要在补 C 语言和嵌入式常用数据处理能力:位运算、字符串和内存函数、链表、数组查找、环形缓冲区、UART 帧解析。今天不是继续盲目往后学新知识,而是先做一次集中验收,再把这些训练内容放回自己的真实项目里理解。
这一天的重点有两个:
- 把前几天留下的基础漏洞补掉,保证练习脚本真正通过。
- 对照
smart_env_monitor和zhijing_edge_gateway两个项目,理解从 STM32 采集数据到 PC 端解析、Web/API/MQTT 输出的完整链路。
这比单独刷题更接近真实嵌入式开发。因为项目里不会只问一个函数能不能写出来,而是要看数据从哪里来、经过哪些任务、通过什么接口传出去、异常时怎么定位。
一、今天先做集中验收
今天重新运行了前五天的核心练习。第一次验收时,数组、环形缓冲区和 UART 帧解析相关练习已经通过,但 Day1 的重写练习暴露出两个回归问题:
FAIL ...02_string_memory_retype.c:74 expression: MyStrcmp("abc", "abd") < 0
FAIL ...03_singly_linked_list_retype.c:56 expression: head == &nodes[4]
这两个失败说明:
MyStrcmp找到第一个不同字符后没有返回差值,而是错误地返回了 0。- 单链表反转函数仍然是占位实现,直接返回了原头节点。
这也提醒我,前几天写过的内容不能只靠印象判断“会了”。必须重新运行脚本,让测试结果说话。
修复后再次运行 Day1 脚本,结果为:
Building 01_bit_ops_retype.c
Running 01_bit_ops_retype.exe
01_bit_ops_retype passed
Building 02_string_memory_retype.c
Running 02_string_memory_retype.exe
02_string_memory_retype passed
Building 03_singly_linked_list_retype.c
Running 03_singly_linked_list_retype.exe
03_singly_linked_list_retype passed
All day01 retype exercises passed
Day2 到 Day5 的综合练习也重新运行通过,数组处理、环形缓冲区和 UART 帧解析三部分都没有再失败:
Building 01_array_tools.c
Running 01_array_tools.exe
01_array_tools passed
Building 02_ring_buffer.c
Running 02_ring_buffer.exe
02_ring_buffer passed
Building 03_uart_frame_parser.c
Running 03_uart_frame_parser.exe
03_uart_frame_parser passed
All embedded data structure exercises passed
到这里,Day1 到 Day5 的核心练习才算有了完整的代码证据。
二、MyStrcmp 这次错在哪里
strcmp 的语义不是“相同返回 0,不同也随便返回一个值”,而是要根据第一个不同字符的大小返回正数或负数。正确逻辑是:
static int MyStrcmp(const char *a, const char *b)
{
const unsigned char *pa = (const unsigned char *)a;
const unsigned char *pb = (const unsigned char *)b;
while (*pa != '\0' && *pa == *pb) {
pa++;
pb++;
}
return (int)(*pa) - (int)(*pb);
}
这里有两个点要注意:
- 比较时应该按
unsigned char处理,避免字符最高位导致符号扩展问题。 - 循环结束后,不管是遇到不同字符,还是其中一个字符串结束,都可以用当前字符差值作为结果。
比如:
"abc" 和 "abd" 比较到 c 和 d,c - d < 0
"abd" 和 "abc" 比较到 d 和 c,d - c > 0
"abc" 和 "abcd" 比较到 \0 和 d,\0 - d < 0
这个函数看起来很小,但它训练的是边界意识:字符串结束符本身也参与比较。
三、链表反转和快慢指针
单链表反转这次失败,是因为函数还没有真正改指针。正确反转需要三个指针:
static Node *ReverseList(Node *head)
{
Node *prev = NULL;
Node *current = head;
while (current != NULL) {
Node *next = current->next;
current->next = prev;
prev = current;
current = next;
}
return prev;
}
核心顺序不能乱:
- 先保存
next,否则改掉current->next后会丢失后续链表。 - 再让
current->next指向prev。 - 最后整体向前移动。
链表中点和环检测也都用快慢指针:
FindMiddle:快指针每次走两步,慢指针每次走一步;偶数长度返回第二个中点。HasCycle:如果链表有环,快慢指针最终会相遇;如果没有环,快指针会先走到空。
这三个函数都不是为了背模板,而是为了训练指针移动顺序。嵌入式里很多队列、链表、内存块管理,本质上也离不开这种指针状态维护。
四、前五天内容如何接到真实项目
今天看的真实项目主要有两个:
D:\develop\programfirst\smart_env_monitor
D:\develop\programfirst\zhijing_edge_gateway
第一个是 STM32F1 + FreeRTOS 的环境监测项目,第二个是 Windows PC 端的边缘网关程序。
整体数据链路可以概括为:
ADC 采样
-> SensorTask 滤波和换算
-> 全局传感器值 g_temp_value / g_light_value
-> WifiTask / DTU_SendTelemetry 组 JSON
-> UART / DTU 输出
-> PC 网关读取串口行
-> 解析 [SENSOR] JSON
-> Web API / MQTT 发布
这条链路正好把前五天内容串了起来:
- 位运算和内存操作:底层寄存器、缓冲区、协议字段都会用到。
- 数组和有效长度:传感器历史、payload、接收缓存都不能只看数组容量。
- 环形缓冲区:适合 UART 连续接收,解决生产者和消费者速度不一致的问题。
- UART 帧解析:解决字节流里如何识别完整业务消息的问题。
- 复杂度和固定内存:STM32F103C8T6 只有 20KB SRAM,不能随意动态分配和无界缓存。
五、STM32 项目里的任务划分
在 smart_env_monitor 里,FreeRTOS 任务大致可以这样理解:
SensorTask :读取 ADC,做滑动均值滤波,更新温度和光照值
DisplayTask :刷新 OLED 显示
ControlTask :根据模式、阈值、传感器值控制蜂鸣器、LED、舵机
WifiTask :处理 DTU 连接、定时上报和下行命令
KeyTask :处理按键、模式切换、阈值调整
这就是项目表达里很重要的一点:任务不是随便拆的,而是按职责拆。采集、显示、控制、通信、按键输入各自有清晰边界。
其中 SensorTask 里用到了滑动均值滤波:
新 ADC 值 -> 替换环形位置里的旧值 -> 更新 sum -> 求平均值
这和 Day4 的环形缓冲区思想很接近,只不过这里不是存 UART 字节,而是存最近若干次 ADC 采样值。
六、UART/DTU 接收和 Day4、Day5 的关系
项目里 HAL_UART_RxCpltCallback 每次接收 1 个字节,并在遇到 \r 或 \n 时认为一行命令结束。它做的事情比较轻:收字节、拼缓冲、设置命令就绪标志,然后重新开启下一次中断接收。
这和 Day4 里总结的原则一致:ISR 里不要做复杂业务逻辑。
更合理的分层应该是:
UART 中断层:快速收字节,放入缓存或设置标志
协议解析层:判断一行或一帧是否完整
业务处理层:执行 SERVO、sget、阈值调整等命令
Day5 的 UART 帧解析训练虽然用的是二进制帧:
0xAA 0x55 LEN CMD PAYLOAD CHECKSUM
而当前项目里 PC 网关解析的是文本行:
[SENSOR] {"light":74,"temp":27,"mode":0,"servo":1500}
但它们的本质是一样的:底层拿到的是连续字节流,应用层必须定义消息边界。二进制协议用帧头和长度,文本协议用换行符和标记字符串 [SENSOR]。
七、PC 网关项目的验证
今天也运行了 zhijing_edge_gateway 的本地自测:
.\build\Release\zhijing_edge_gateway.exe --self-test
输出为:
self-test passed.
它覆盖了几个关键点:
[SENSOR]JSON 解析。- 最新数据 JSON 输出。
- MQTT Remaining Length 编码。
- MQTT CONNECT 报文编码。
- MQTT PUBLISH 报文编码。
再运行 demo:
.\build\Release\zhijing_edge_gateway.exe --demo
输出为:
Demo input: [SENSOR] {"light":74,"temp":27,"mode":0,"servo":1500}
[16:01:46] temp=27 C, light=74 %, mode=0, servo=1500 us
这说明 PC 端程序能从一行串口文本里提取业务字段,并转换成可展示、可上报的数据结构。
八、今天必须能口述的项目表达
今天整理后,我需要能把项目讲成下面这段话:
我的项目是一个 STM32F103 + FreeRTOS 的环境监测系统。STM32 端分成传感器采集、显示、控制、通信和按键任务。传感器任务周期读取 ADC,并用滑动均值降低抖动;控制任务根据温度、光照和模式控制 LED、蜂鸣器、舵机;通信任务把实时数据组成 JSON,通过 UART/DTU 上报。PC 端边缘网关从串口读取
[SENSOR]数据行,解析出温度、光照、模式和舵机值,再提供 Web API 或通过 MQTT 发布。这个项目里我重点关注了任务职责划分、UART 数据边界、缓冲区有效长度、以及异常数据不应该阻塞主流程。
这段话比单纯说“我做过 STM32 项目”更具体,因为它包含了任务划分、数据流和通信链路。
九、今天的收获
今天最重要的不是多学了一个新知识点,而是把前五天的内容合并成一条工程链路:
- 基础函数必须能重新写出来,不能只靠看懂。
- 练习是否完成,要以脚本输出为准。
- 字符串、链表、数组、缓冲区、帧解析都可以在真实项目里找到对应场景。
- STM32 + FreeRTOS 项目表达要讲清楚任务职责,而不是只列外设名字。
- 串口通信的关键不是“能发能收”,而是能稳定定义消息边界、处理异常数据、避免 ISR 里做复杂逻辑。
总结
Day6 完成了一次集中验收:Day1 的 C 基础重写练习,以及 Day2 到 Day5 的数组、环形缓冲区、UART 帧解析练习都已经重新通过。今天还把这些训练内容接到了自己的 STM32 环境监测项目和 PC 端边缘网关项目里。
接下来不再围绕训练目录名展开,而是固定两条主线:一条是继续补 C 语言硬功,重点放在指针、内存、结构体、回调函数和模块化接口;另一条是继续吃透自己的项目,把 STM32 端采集控制、UART/DTU 通信、PC 网关解析、Web/MQTT 输出这些链路讲清楚、改明白、跑出证据。
更多推荐

所有评论(0)