前言

前五天我主要在补 C 语言和嵌入式常用数据处理能力:位运算、字符串和内存函数、链表、数组查找、环形缓冲区、UART 帧解析。今天不是继续盲目往后学新知识,而是先做一次集中验收,再把这些训练内容放回自己的真实项目里理解。

这一天的重点有两个:

  1. 把前几天留下的基础漏洞补掉,保证练习脚本真正通过。
  2. 对照 smart_env_monitorzhijing_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);
}

这里有两个点要注意:

  1. 比较时应该按 unsigned char 处理,避免字符最高位导致符号扩展问题。
  2. 循环结束后,不管是遇到不同字符,还是其中一个字符串结束,都可以用当前字符差值作为结果。

比如:

"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;
}

核心顺序不能乱:

  1. 先保存 next,否则改掉 current->next 后会丢失后续链表。
  2. 再让 current->next 指向 prev
  3. 最后整体向前移动。

链表中点和环检测也都用快慢指针:

  • 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 项目”更具体,因为它包含了任务划分、数据流和通信链路。

九、今天的收获

今天最重要的不是多学了一个新知识点,而是把前五天的内容合并成一条工程链路:

  1. 基础函数必须能重新写出来,不能只靠看懂。
  2. 练习是否完成,要以脚本输出为准。
  3. 字符串、链表、数组、缓冲区、帧解析都可以在真实项目里找到对应场景。
  4. STM32 + FreeRTOS 项目表达要讲清楚任务职责,而不是只列外设名字。
  5. 串口通信的关键不是“能发能收”,而是能稳定定义消息边界、处理异常数据、避免 ISR 里做复杂逻辑。

总结

Day6 完成了一次集中验收:Day1 的 C 基础重写练习,以及 Day2 到 Day5 的数组、环形缓冲区、UART 帧解析练习都已经重新通过。今天还把这些训练内容接到了自己的 STM32 环境监测项目和 PC 端边缘网关项目里。

接下来不再围绕训练目录名展开,而是固定两条主线:一条是继续补 C 语言硬功,重点放在指针、内存、结构体、回调函数和模块化接口;另一条是继续吃透自己的项目,把 STM32 端采集控制、UART/DTU 通信、PC 网关解析、Web/MQTT 输出这些链路讲清楚、改明白、跑出证据。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐