简介这份资源是面向嵌入式开发者与物联网方向学习者的完整项目资料围绕基于NB-IoT的无人水质监测船展开适合希望掌握STM32L4系列MPU、FreeRTOS实时操作系统与物联网云平台数据交互的中高级实践者。项目以无人船为载体集成多种传感器与NB-IoT传输模块实现自主航行至指定区域、实时采集水温、PH值、溶解氧、TDS、浑浊度等水质参数并回传至云平台同时支持返航与太阳能锂电池供电可应用于水域环保监管与养殖环境监测场景。压缩包为zip格式整体约348.61MB文件总数与类型明细上游暂未提供但内容应涵盖工程源码、硬件驱动与通信配置等核心模块。目前已有117人学习下载读者可从中获取完整的系统方案、运动控制与传感器采集逻辑、NB-IoT联网思路及低功耗供电设计参考便于快速复现与二次开发。1. 从一条“船”说起NB-IoT 水质监测到底能落地成什么样去年帮一个做水产养殖的朋友看现场他承包的鱼塘有八十多亩每天天不亮就得划着小船到塘中央测溶氧和 pH测完回来一身泥。他问我能不能搞个东西自己跑过去测、测完把数据发手机上。这个需求其实不新鲜但真正能跑起来的方案不多——要么是 Wi-Fi 覆盖不到塘中央要么是 4G 模块功耗太高撑不过一个白天。直到我拆开这份「基于 NB-IOT 的无人水质监测船」的源码包才觉得这个方向有得聊。这份资源是一套完整的嵌入式工程核心思路很清晰用 STM32L4 系列低功耗 MPU 跑 FreeRTOS 实时系统通过 NB-IoT 模组把温度、pH、溶解氧、TDS、浑浊度这些水质参数传到云平台船体本身支持前进、后退、左转、右转和停止供电走太阳能加锂电池。它适合两类人一类是做嵌入式毕设或课程设计的学生需要一套能跑通“采集—传输—上云”全链路的参考实现另一类是做智慧农业、水产养殖监控的从业者想评估 NB-IoT 在这个场景下到底靠不靠谱。下面我按“先搞懂架构、再动手跑通、最后避开坑”的顺序把这份资源拆开讲。2. 系统架构拆解STM32L4 FreeRTOS NB-IoT 怎么串起来2.1 为什么选 STM32L4 而不是 F1 或 F4拿到一份嵌入式资源第一件事是看主控选型。这份工程用的是 STM32L4 系列不是大家更熟悉的 F103 或 F407。原因很直接水质监测船靠太阳能加锂电池供电功耗是硬约束。L4 系列在 Stop 模式下电流可以压到微安级而 F1 通常在几十微安F4 更高。船在待机等指令的时候主控如果能进 Stop 模式电池续航能多出好几个小时。另一个原因是 L4 的外设够用且灵活。它有多路 ADC 用于采样 pH、溶解氧、TDS 这些模拟传感器有多个 UART 可以同时接 NB-IoT 模组和调试串口还有定时器用于 PWM 控制电机。F1 虽然便宜但 ADC 精度和通道数在同时接五路传感器时会比较紧张。注意如果你手头只有 F103 的板子工程不能直接烧。L4 的 HAL 库和 F1 差异较大时钟树配置、低功耗模式寄存器都不一样移植工作量不小。2.2 FreeRTOS 任务划分与优先级安排这份工程跑的是 FreeRTOS不是裸机大循环。任务划分大致是这样的任务名优先级职责栈大小字SensorTask3轮询五路传感器做滤波和标定换算256CommTask2驱动 NB-IoT 模组组包发送、接收指令512MotorTask2解析运动指令输出 PWM 控制电机256MonitorTask1看门狗喂狗、电池电压监测、状态灯128优先级安排有个讲究SensorTask 最高因为传感器采样有时序要求pH 和溶解氧的模拟输出需要稳定采集窗口被打断会导致数据跳变。CommTask 和 MotorTask 同级因为发送数据和电机控制没有严格先后依赖。MonitorTask 最低它只是兜底。任务间通信主要靠消息队列。SensorTask 采完一轮数据打包成一个结构体丢进队列CommTask 从队列取出来通过 NB-IoT 发走。MotorTask 则监听另一个队列等待来自云端的运动指令。2.3 NB-IoT 模组的接入方式与 AT 指令流程工程里 NB-IoT 模组走的是 UART 接口用 AT 指令控制。常见做法是模组上电后先做一串初始化查卡、注网、配 APN、建 socket。代码里封装了一个nb_send_cmd函数带超时和重试。关键流程如下// NB-IoT 初始化与数据发送核心逻辑基于工程源码整理 #define NB_UART_TIMEOUT_MS 2000 #define NB_RETRY_MAX 3 static int nb_wait_response(const char *expect, uint32_t timeout_ms) { uint32_t start HAL_GetTick(); while ((HAL_GetTick() - start) timeout_ms) { if (strstr((char *)rx_buffer, expect) ! NULL) { memset(rx_buffer, 0, sizeof(rx_buffer)); return 0; // 命中预期响应 } } return -1; // 超时 } int nb_init(void) { // 1. 检查模组是否在线 if (nb_send_cmd(AT\r\n, OK, NB_RETRY_MAX) ! 0) return -1; // 2. 查 SIM 卡状态 if (nb_send_cmd(ATCFUN1\r\n, OK, NB_RETRY_MAX) ! 0) return -1; // 3. 查询网络注册状态等待注网成功 for (int i 0; i 30; i) { if (nb_send_cmd(ATCEREG?\r\n, CEREG:0,1, NB_RETRY_MAX) 0) break; vTaskDelay(pdMS_TO_TICKS(1000)); } // 4. 配置 APN不同运营商 APN 不同需按实际卡修改 nb_send_cmd(ATCGDCONT1,\IP\,\ctnb\\r\n, OK, NB_RETRY_MAX); // 5. 建立 UDP socket 指向云平台 nb_send_cmd(ATNSOCRDGRAM,17,0\r\n, OK, NB_RETRY_MAX); return 0; } int nb_send_data(const uint8_t *payload, uint16_t len) { char cmd[128]; // socket 0发送到云平台指定 IP 和端口 snprintf(cmd, sizeof(cmd), ATNSOST0,\xxx.xxx.xxx.xxx\,port,%d,\%s\\r\n, len, payload); return nb_send_cmd(cmd, OK, NB_RETRY_MAX); }这段代码的逻辑说明nb_wait_response是阻塞式等待在超时窗口内不断检查接收缓冲区里有没有出现预期字符串。nb_init按顺序做五件事——确认模组在线、开射频、等注网、配 APN、建 socket。其中注网那一步用了循环等待因为 NB-IoT 注网时间可能到十几秒不能只等一次。nb_send_data把传感器数据通过 UDP 发出去IP 和端口需要替换成你实际用的云平台地址。参数方面NB_UART_TIMEOUT_MS设 2000 毫秒是经验值NB-IoT 的 AT 响应通常在这个范围内但注网阶段要单独放宽。NB_RETRY_MAX设 3 次再多会拖慢任务周期。APN 字符串“ctnb”是某运营商的 NB-IoT 专用 APN你用别的卡要换成对应的。2.4 传感器采样与数据标定五路传感器里温度一般走 DS18B20 单总线pH、溶解氧、TDS、浑浊度都是模拟量走 ADC。工程里对模拟量的处理是每路连续采 16 次去掉最大最小各 2 个剩下 12 个取平均。这个做法能压掉水面波动带来的尖峰噪声。标定是绕不过去的。pH 传感器出厂给的是一条理想曲线但实际电极有偏移。工程里留了一个标定接口用两点法把电极放进 pH 4.00 和 6.86 的标准液里记录 ADC 值反算出斜率和截距写进 Flash。溶解氧和 TDS 同理只是标准液不同。// 两点法标定 pH 的简化实现 typedef struct { float slope; float offset; } calib_t; calib_t ph_calib; void ph_calibrate(uint16_t adc_low, uint16_t adc_high) { // adc_low 对应 pH 4.00adc_high 对应 pH 6.86 float ph_low 4.00f, ph_high 6.86f; ph_calib.slope (ph_high - ph_low) / (float)(adc_high - adc_low); ph_calib.offset ph_low - ph_calib.slope * (float)adc_low; } float ph_read(uint16_t adc_raw) { return ph_calib.slope * (float)adc_raw ph_calib.offset; }逻辑很直白用两个已知 pH 值对应的 ADC 读数算出线性关系的斜率和截距之后每次采样套这个公式。参数adc_low和adc_high要在标定时实测填入不能照抄别人的值因为每个电极和电路的偏置都不一样。3. 从零跑通编译、烧录与云平台对接的完整步骤3.1 开发环境搭建与工程编译这份工程用的是 Keil MDK 加 STM32CubeMX 生成的 HAL 库工程。如果你习惯用 IAR 或 GCC需要自己移植启动文件和链接脚本。我一般建议先用 Keil 跑通确认硬件没问题再考虑换工具链。步骤是这样的先装 Keil MDK 和 STM32L4 的 Device Family Pack然后装 ST-Link 驱动。打开工程后在 Options for Target 里确认芯片型号选的是你板子上的具体型号比如 STM32L431RCT6。编译前检查两个地方一是FreeRTOSConfig.h里的configTOTAL_HEAP_SIZE工程默认给了 10240 字节如果你加任务或加队列要相应调大二是stm32l4xx_hal_conf.h里使能的外设模块确保 ADC、UART、TIM 都开了。编译如果报错undefined reference to xQueueCreate多半是 FreeRTOS 的源文件没加进工程组。检查 Middlewares/FreeRTOS 目录下的.c文件是否都在。3.2 烧录与串口调试烧录用 ST-Link 直接下载。第一次烧录后先别急着接传感器用 USB 转串口接调试串口看有没有打印启动日志。工程里MonitorTask会周期性输出电池电压和任务栈使用情况这是判断系统有没有跑飞的第一手信息。如果串口没输出先查波特率。工程默认 115200但有些板子的外部晶振频率和代码里SystemClock_Config的配置不一致会导致实际波特率偏移。用示波器量一下 TX 引脚波形最直接没有示波器就换几个常用波特率试。3.3 云平台对接与数据上报验证云平台这块工程里用的是 UDP 直连方式没有走 MQTT。好处是协议简单、开销小坏处是数据可靠性不如 TCP。对于水质监测这种周期性上报的场景丢一两个包可以接受所以 UDP 是合理选择。对接步骤先在云平台侧建一个设备拿到 IP 和端口填进nb_send_data里的地址。然后确认云平台的数据解析格式和工程里组包的格式一致。工程里组包用的是简单的 TLV 格式一个字节的类型、一个字节的长度、后面跟数据。温度是 float 四个字节pH 也是 float溶解氧和 TDS 同理浑浊度用 uint16。验证方法烧录后让船在岸上跑传感器可以先用信号发生器模拟看云平台能不能收到数据。如果收不到先在nb_send_data返回处打断点看 AT 指令有没有发出去、模组有没有回 OK。常见问题是 APN 配错或者 socket 没建成功。# 用串口助手手动发 AT 指令排查 NB-IoT 模组状态 AT # 应返回 OK ATCFUN? # 查射频状态1 为开启 ATCEREG? # 查注网状态CEREG:0,1 表示已注册 ATCGATT? # 查附着状态CGATT:1 表示已附着 ATNSOCRDGRAM,17,0 # 建 UDP socket返回 socket 编号这几条指令按顺序发哪一步卡住就查哪一步。注网那一步如果一直返回CEREG:0,2说明正在搜网等就行如果返回0,0说明没注册上查卡和天线。3.4 电机控制与运动指令解析运动控制这块工程用 TIM 输出四路 PWM 驱动两个电机正反转。前进是左右电机都正转后退都反转左转是左电机停或慢、右电机快右转反过来。指令格式是单字节0x01 前进、0x02 后退、0x03 左转、0x04 右转、0x00 停止。MotorTask 从队列拿到指令后先做限幅保护——如果电池电压低于阈值拒绝执行前进指令只允许停止或返航。这个逻辑在源码里是一个if (battery_mv 3300) return MOTOR_STOP;的判断别小看这一行能防止船在低电量时跑远回不来。4. 避坑与排查NB-IoT 水质监测船最容易翻车的五个地方4.1 注网慢或注不上先查天线和 APN现象模组上电后串口一直打印CEREG:0,2等了一分钟还没变成0,1。原因最常见的是 NB-IoT 天线没接好或者频段不匹配。NB-IoT 常用频段是 B5 和 B8不同运营商不一样。另一个原因是 APN 配错有些卡需要特定 APN 才能注网。解决先确认天线接的是 NB-IoT 专用天线不是 GPS 或 4G 天线。然后用ATCGDCONT?查当前 APN和运营商确认正确的 APN 字符串。如果还不行用ATCFUN0再ATCFUN1重启射频。4.2 传感器读数跳变滤波和接地没做好现象pH 值在 6.5 到 8.2 之间来回跳溶解氧读数也不稳。原因水面本身有波动电极响应会抖。更关键的是模拟地和数字地没分开电机 PWM 的噪声串进了 ADC 参考电压。解决软件上把采样次数从 16 次加到 32 次滤波窗口拉长。硬件上在 ADC 输入端加 RC 低通截止频率设在 10Hz 左右。电机驱动和主控的电源入口加磁珠隔离。4.3 数据发不出去但 AT 返回 OKsocket 没建或地址错现象nb_send_data返回 0说明 AT 指令收到了 OK但云平台就是没数据。原因ATNSOST返回 OK 只表示模组接受了指令不表示数据真的发出去了。如果 socket 没建成功或者目标 IP 端口写错数据会静默丢弃。解决发数据前先ATNSOCR建 socket确认返回了 socket 编号。然后检查 IP 和端口是不是云平台给的。可以用ATNSOST发一条测试数据然后在云平台侧看有没有收到。如果用的是域名NB-IoT 模组不一定支持 DNS要直接填 IP。4.4 电池续航远低于预期低功耗模式没进对现象标称 10Ah 的锂电池跑不到一天就没电了。原因STM32L4 虽然支持低功耗但工程里如果没正确进入 Stop 模式或者 NB-IoT 模组一直保持在线功耗会很高。NB-IoT 模组在空闲时如果不发 PSM 或 eDRX待机电流也有几毫安。解决在 FreeRTOS 的空闲任务钩子里调用__WFI()让内核休眠。NB-IoT 侧配置 PSM 模式用ATCPSMS1开启让模组在不上报时进入深度休眠。上报周期从 10 秒改成 60 秒功耗能降一个数量级。4.5 船跑偏或原地打转电机死区和 PWM 频率不匹配现象发前进指令船往一边偏发左转船原地打转。原因两个电机的死区不一样同样的 PWM 占空比一个转一个不转。或者 PWM 频率太低电机有啸叫且低速扭矩不足。解决在代码里给每个电机单独设死区补偿值左电机加 50、右电机加 80 这种。PWM 频率从 1kHz 提到 8kHz 或 16kHz避开人耳可听范围同时低速线性度更好。调试时先用串口手动发指令看两个电机在相同占空比下的转速差再补。5. 进阶玩法把数据用起来和验证系统可靠性的几个技巧跑通基本功能之后这份资源还有不少可以深挖的地方。我一般会做两件事一是把上报的数据在云平台侧做可视化二是加一套离线缓存机制防止网络断的时候数据丢了。可视化这块云平台一般都有数据流转功能把 NB-IoT 上报的 TLV 格式解析成 JSON再推到仪表盘。温度、pH、溶解氧、TDS、浑浊度五个指标做成实时曲线加上阈值告警。比如溶解氧低于 4mg/L 就触发提醒这对养殖户来说是最实用的功能。离线缓存的做法是在 CommTask 里加一个环形缓冲区。每次nb_send_data之前先把数据写进缓冲区发送成功后再标记为已发送。如果发送失败数据留在缓冲区里等网络恢复后按时间顺序补发。缓冲区大小根据你的上报频率和断网时长来定60 秒上报一次、断网一小时需要存 60 条记录每条 20 字节左右1.2KB 的 RAM 就够。// 环形缓冲区实现离线缓存简化版 #define CACHE_SIZE 64 #define RECORD_LEN 20 typedef struct { uint8_t data[CACHE_SIZE][RECORD_LEN]; uint16_t head; uint16_t tail; uint16_t count; } ring_cache_t; ring_cache_t g_cache; int cache_push(const uint8_t *record) { if (g_cache.count CACHE_SIZE) return -1; // 缓冲区满丢弃最旧或报警 memcpy(g_cache.data[g_cache.head], record, RECORD_LEN); g_cache.head (g_cache.head 1) % CACHE_SIZE; g_cache.count; return 0; } int cache_pop(uint8_t *record) { if (g_cache.count 0) return -1; memcpy(record, g_cache.data[g_cache.tail], RECORD_LEN); g_cache.tail (g_cache.tail 1) % CACHE_SIZE; g_cache.count--; return 0; }这段代码的逻辑cache_push往环形缓冲区写一条记录满了就返回 -1调用方可以选择丢弃最旧的或者报警。cache_pop从尾部取一条记录用于补发。参数CACHE_SIZE设 64 是折中值再大 RAM 吃紧再小断网半小时就满了。验证系统可靠性我习惯做三个测试。第一个是断网恢复测试把 NB-IoT 天线拔掉让船跑十分钟再插回去看缓存的数据能不能补发到云平台。第二个是低电量返航测试把电池电压慢慢调低看船在阈值附近是不是正确触发返航。第三个是长时间运行测试让船在塘里跑一整天看 FreeRTOS 的任务栈有没有溢出、看门狗有没有复位。从那以后我每次拿到类似的嵌入式物联网资源都强制走一遍“先看功耗预算、再查通信链路、最后跑断网恢复”的流程。这份水质监测船的工程在架构上是完整的传感器驱动、RTOS 任务划分、NB-IoT 通信、电机控制都有可复用的代码坑也集中在硬件调试和网络配置上软件逻辑本身没有太玄学的地方。希望帮到你。本文还有配套的精品资源点击获取