简介这份资源面向嵌入式开发与物联网方向的学习者提供一套基于LoRaWAN技术的环境监测系统完整工程源码可用于温湿度、光照等环境数据的采集与远程监控场景。系统支持触摸屏参数配置、数据可视化展示并具备掉电存储与上电恢复能力适合作为课程设计、毕业设计或项目练手的参考方案。压缩包共299个文件约23.07MB以C语言源文件与头文件为核心包含STM32L4系列HAL库驱动、LCD应用层代码、工程配置文件及说明文档覆盖从底层驱动到应用逻辑的完整结构。目前已有64人学习下载。读者可从中获取LoRaWAN节点与网关通信的工程实现思路、触摸屏交互与数据可视化模块的组织方式以及掉电存储恢复机制的具体代码参考便于快速理解系统架构并移植到自己的项目中。1. 从一块 STM32L4 和利尔达模组说起这套环境监测系统到底能跑出什么如果你手头正好有一块 STM32L4 系列的板子、一块利尔达 LoRaWAN 模组、一个 SPI 或并口屏再加上几个温湿度光照传感器那这套源码包基本就是为你准备的。它做的事情很具体用 STM32L4 的 HAL 库把 I2C 传感器读起来把数据通过 UART 喂给 LoRaWAN 模组发出去同时在本地屏幕上做参数配置和数据可视化掉电时把关键数据写进 Flash上电后恢复现场。整套逻辑不依赖云平台也能本地跑通适合做环境监测类课程设计、毕业设计或者公司里要快速搭一个低功耗广域网的采集节点原型。我见过太多人拿到这类压缩包后卡在“编译能过但跑不起来”的阶段——HAL 库版本对不上、RTC 备份域没配置、Flash 擦写地址和链接脚本打架。这篇笔记就按我实际拆包复现的顺序把每个模块的配置逻辑、参数含义和容易翻车的地方讲清楚。你不需要先精通 LoRaWAN 协议栈但得知道 HAL 库的基本用法和 Keil 或 CubeIDE 的工程结构。2. 工程结构与 HAL 驱动层先搞清哪些文件在干活2.1 从文件清单反推系统架构打开压缩包源码目录里能看到这些关键文件stm32l4xx_hal_tim.c、stm32l4xx_hal_i2c.c、stm32l4xx_hal_uart.c、stm32l4xx_hal_rcc.c、stm32l4xx_hal_rcc_ex.c、stm32l4xx_hal_rtc.c、stm32l4xx_hal_rtc_ex.c、stm32l4xx_hal_flash_ex.c以及应用层的Lcd_App.c。这不是一个完整的 LoRaWAN 协议栈实现而是把 STM32L4 的外设驱动和应用逻辑做了裁剪LoRaWAN 的入网和收发大概率由利尔达模组内部的固件处理MCU 只负责通过 UART 发 AT 指令或透传数据。这种架构的好处是开发门槛低你不用啃 LoRaWAN 的 MAC 层代码代价是模组行为对你来说是个黑匣子出问题时只能看串口日志。我一般会先把 UART 的波特率和模组手册对齐再用 USB-TTL 抓一次模组上电后的输出确认它到底跑的是 AT 固件还是透传固件。2.2 时钟树与 RCC 配置别让 RTC 和 UART 打架stm32l4xx_hal_rcc.c和stm32l4xx_hal_rcc_ex.c负责系统时钟和外设时钟使能。STM32L4 的时钟源比较多HSI16、MSI、PLL 都能用但 LoRaWAN 模组的 UART 波特率通常要求比较准的时钟。如果你用 MSI 默认的 4MHz 去配 115200 波特率误差可能超过 2%模组会丢包。常见做法是把 PLL 开到 80MHzUART 时钟源选 PCLK这样波特率误差能压到 0.1% 以内。RTC 这块要特别注意stm32l4xx_hal_rtc_ex.c里涉及备份域写保护。如果你要用 RTC 做时间戳或者周期性唤醒必须先使能 PWR 时钟解除备份域写保护再配置 RTC。顺序错了RTC 要么不走要么一上电就复位。/* 使能 PWR 时钟并解除备份域写保护 */ __HAL_RCC_PWR_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess(); /* 选择 LSE 作为 RTC 时钟源并等待就绪 */ RCC_OscInitTypeDef osc {0}; osc.OscillatorType RCC_OSCILLATORTYPE_LSE; osc.LSEState RCC_LSE_ON; HAL_RCC_OscConfig(osc); /* 配置 RTC 时钟源为 LSE */ RCC_PeriphCLKInitTypeDef periph {0}; periph.PeriphClockSelection RCC_PERIPHCLK_RTC; periph.RtcClockSelection RCC_RTCCLKSOURCE_LSE; HAL_RCCEx_PeriphCLKConfig(periph);这段代码的逻辑是先打开电源管理时钟允许访问备份域寄存器然后启动外部低速晶振 LSE等它稳定最后把 RTC 的时钟源切到 LSE。参数上LSE 的频率是 32.768kHz分频后能得到 1Hz 的秒信号。如果你板子上没焊 LSE 晶振就得改用 LSI但 LSI 精度差长时间跑时间戳会漂。2.3 I2C 与传感器读取时序和上拉电阻是玄学重灾区stm32l4xx_hal_i2c.c是温湿度光照传感器的通信基础。常见传感器比如 SHT30、BH1750 都是 I2C 接口。HAL 库的 I2C 配置里ClockSpeed一般设 100kHz 或 400kHz但真正决定能不能读通的是上拉电阻和总线电容。我翻车过好几次模块单独测没问题一接长排线就 NACK。后来用示波器看波形上升沿太缓传感器把数据线拉低的时间不够。解决办法是把上拉电阻从 10k 换成 4.7k同时把 I2C 的DutyCycle设为I2C_DUTYCYCLE_2让高低电平时间更均衡。另外HAL 库的HAL_I2C_Mem_Read超时参数别设太小环境监测节点可能被 LoRa 发送阻塞I2C 操作要留够重试时间。/* I2C1 初始化100kHz7 位地址模式 */ hi2c1.Instance I2C1; hi2c1.Init.Timing 0x10909CEC; /* 对应 100kHz具体值用 CubeMX 生成 */ hi2c1.Init.OwnAddress1 0; hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE; HAL_I2C_Init(hi2c1); /* 读取 SHT30 温湿度命令 0x2C06 是周期测量 */ uint8_t cmd[2] {0x2C, 0x06}; uint8_t data[6] {0}; HAL_I2C_Master_Transmit(hi2c1, 0x44 1, cmd, 2, 100); HAL_Delay(20); HAL_I2C_Master_Receive(hi2c1, 0x44 1, data, 6, 100);Timing寄存器的值不是随便填的它跟你的 I2C 时钟源频率有关。用 CubeMX 生成最稳手算容易错。NoStretchMode建议关掉因为有些传感器需要拉低 SCL 来争取处理时间你禁止了时钟拉伸反而读不到数据。3. 触摸屏配置与数据可视化Lcd_App.c 里的交互逻辑3.1 屏幕驱动与触摸校准Lcd_App.c是应用层里最厚的文件它把屏幕驱动、触摸检测、界面刷新和参数配置揉在一起。常见的屏是 ILI9341 或 ST7789 配 XPT2046 触摸芯片走 SPI 接口。代码里一般会有一个LCD_Init()和一个Touch_Scan()前者初始化屏幕控制器后者读触摸坐标。触摸校准是第一个坑。电阻屏的原始坐标和屏幕像素坐标不是线性对应的你得先采四个角或者两个点算出一个校准矩阵。我一般会在Lcd_App.c里找Touch_Adjust()函数如果没有就自己加一个屏幕上画四个十字依次点击把原始 ADC 值记下来用最小二乘法算x a*raw_x b*raw_y c的系数存到 Flash 里。/* 触摸校准系数结构体存到 Flash 固定地址 */ typedef struct { float a, b, c; /* x 轴系数 */ float d, e, f; /* y 轴系数 */ } TouchCalib_t; /* 用采集到的原始点和屏幕坐标做最小二乘拟合 */ void Touch_Calibrate(TouchCalib_t *cal, int *raw_x, int *raw_y, int *scr_x, int *scr_y, int n) { /* 这里省略矩阵运算细节核心是解超定方程 */ /* 实际项目中我直接用现成的校准算法避免手写矩阵出错 */ }校准系数存 Flash 的时候要注意地址对齐STM32L4 的 Flash 双字编程要求 8 字节对齐写之前先擦除整页。3.2 参数配置界面与掉电存储的联动触摸屏上一般会有几个配置项采集周期、LoRaWAN 发送间隔、报警阈值、设备地址。用户改完点保存系统要把这些参数写进 Flash同时更新 RAM 里的运行变量。这里有个容易忽略的点Flash 写入期间如果断电可能写一半上电后读出来是乱码。常见做法是加一个“参数有效标志”写参数前先把标志清零写完再置位。上电时检查标志无效就加载默认参数。#define PARAM_FLASH_ADDR 0x0807F800 /* 最后一页根据芯片型号调整 */ #define PARAM_MAGIC 0x5A5A typedef struct { uint16_t magic; uint32_t sample_period_ms; uint32_t send_interval_s; float temp_high; float humi_low; uint16_t dev_addr; } SysParam_t; void Param_Save(SysParam_t *p) { HAL_FLASH_Unlock(); /* 擦除整页 */ FLASH_EraseInitTypeDef erase {0}; erase.TypeErase FLASH_TYPEERASE_PAGES; erase.Page (PARAM_FLASH_ADDR - FLASH_BASE) / FLASH_PAGE_SIZE; erase.NbPages 1; uint32_t page_err 0; HAL_FLASHEx_Erase(erase, page_err); /* 先写 magic 为 0表示正在写 */ p-magic 0; uint64_t *src (uint64_t *)p; for (int i 0; i sizeof(SysParam_t) / 8; i) { HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, PARAM_FLASH_ADDR i * 8, src[i]); } /* 最后写回 magic表示写入完成 */ p-magic PARAM_MAGIC; HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, PARAM_FLASH_ADDR, *(uint64_t *)p); HAL_FLASH_Lock(); }这段代码的关键在于写入顺序先擦页再把结构体当成一串双字写进去最后单独把 magic 写回。如果写到一半断电magic 还是 0上电检测到就认为参数无效。PARAM_FLASH_ADDR要选在程序空间之外的页具体地址查芯片参考手册的 Flash 分区表。3.3 数据可视化曲线刷新与内存占用屏幕上画温湿度曲线最直接的做法是维护一个环形缓冲区存最近 N 个采样点每次刷新重绘整个曲线区域。但 STM32L4 的 RAM 有限如果你开 320x240 的屏幕一个全屏缓冲区就要 150KBL4 系列很多型号只有 128KB RAM根本不够。所以Lcd_App.c里大概率是分区刷新或者直接画点不存全屏 buffer。我一般会限制曲线点数为 60 到 100 个每个点用uint16_t存占用不到 200 字节。刷新时只清除曲线区域重画坐标轴和折线。如果屏幕支持硬件滚动那就更省事新数据从右边进旧数据左移。注意SPI 刷屏速度受限于 SPI 时钟和屏幕控制器。如果你发现界面卡顿先把 SPI 时钟降到 10MHz 以下试试有些 ILI9341 模块走 20MHz 以上会花屏。4. 避坑与排查从编译通过到稳定运行的五个坎4.1 现象程序下载后屏幕白屏串口无输出原因时钟配置错误导致主频没跑起来或者 Flash 等待周期没设对。STM32L4 在 80MHz 下需要 4 个等待周期如果你从别的工程抄了HAL_RCC_ClockConfig但没改FLASH_LATENCYCPU 取指会出错。解决检查SystemClock_Config()里的FLASH_LATENCY是否和主频匹配。80MHz 对应FLASH_LATENCY_448MHz 对应FLASH_LATENCY_2。另外确认HAL_Init()里有没有调用HAL_NVIC_SetPriorityGrouping优先级分组不对会导致中断嵌套异常。4.2 现象LoRaWAN 模组入网失败串口一直返回错误码原因UART 波特率偏差太大或者模组的频段和网关不匹配。利尔达模组常见的有 CN470、EU868 等频段如果你买的模组是 EU868网关却配的 CN470永远入不了网。解决先用 USB-TTL 直接连模组发ATVER确认固件版本和频段。然后检查 MCU 的 UART 时钟源用示波器测 TX 引脚波形算实际波特率。偏差超过 2% 就换时钟源或调整 PLL。4.3 现象掉电后参数丢失上电恢复默认值原因Flash 擦写地址和程序空间重叠或者 magic 标志写入顺序不对。有些开发者把参数存在0x08000000附近结果一烧程序就把参数页擦了。解决查链接脚本确认程序占用的 Flash 范围把参数页放在程序末尾之后的独立页。STM32L4 的页大小有的是 2KB有的是 4KB按型号查手册。写入时严格按“擦页 → 写数据 → 写 magic”的顺序。4.4 现象I2C 读传感器偶尔返回 HAL_BUSY原因I2C 总线被意外拉低或者上一次传输没正确产生 STOP 条件。HAL 库的 I2C 状态机在超时后可能没复位导致后续操作一直 BUSY。解决在 I2C 错误处理里加总线复位逻辑把 SCL 和 SDA 配成 GPIO 开漏输出手动发 9 个时钟脉冲再重新初始化 I2C。另外HAL_I2C_Mem_Read的超时别设 0设 100ms 以上。4.5 现象触摸屏坐标漂移点 A 区域触发 B 区域原因校准系数没存住或者屏幕旋转方向变了但校准数据没跟着变。有些屏幕驱动支持 0/90/180/270 度旋转旋转后坐标映射关系要重新算。解决把校准系数和屏幕旋转方向绑定存储每次切换方向时重新加载对应的校准参数。如果屏幕是电容屏一般不需要校准但要注意触摸芯片的 I2C 地址和中断引脚配置。5. 进阶技巧用 RTC 唤醒做低功耗采集以及一个验证数据完整性的土办法这套系统如果只跑在 USB 供电下低功耗优势体现不出来。真正让它像环境监测节点的地方是把 STM32L4 的 STOP 模式用起来让 RTC 周期性唤醒醒来读传感器、发 LoRa、写 Flash然后继续睡。stm32l4xx_hal_rtc_ex.c里的HAL_RTCEx_SetWakeUpTimer_IT就是干这个的。配置 RTC 唤醒定时器的步骤先按第 2 章的方法把 RTC 时钟源配好然后设置唤醒周期。唤醒周期由WakeUpClock和WakeUpCounter决定。比如选RTC_WAKEUPCLOCK_RTCCLK_DIV16LSE 是 32.768kHz分频后是 2048Hz计数器设 2048 就是 1 秒唤醒一次。/* 配置 RTC 唤醒定时器周期 10 秒 */ HAL_RTCEx_SetWakeUpTimer_IT(hrtc, 20480, RTC_WAKEUPCLOCK_RTCCLK_DIV16); /* 在唤醒中断里做采集和发送 */ void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc) { float temp, humi; SHT30_Read(temp, humi); LoRa_Send(temp, humi); /* 写 Flash 保存最新数据用于掉电恢复 */ DataLog_Write(temp, humi); }20480这个计数值对应 10 秒因为 2048Hz 下 20480 个周期就是 10 秒。RTC_WAKEUPCLOCK_RTCCLK_DIV16是分频系数你可以根据需要的唤醒精度选 DIV2 到 DIV16。唤醒中断里不要做耗时操作LoRa 发送如果阻塞太久会错过下一次唤醒。验证数据完整性我有个土办法在 Flash 里开两个区域交替写入每个区域带一个递增的序号和 CRC 校验。上电时读两个区域选序号大且 CRC 正确的那个。这样即使写入过程中断电至少有一个区域的数据是完整的。代码不复杂但能省掉很多“数据怎么少了一条”的后悔药。typedef struct { uint32_t seq; float temp; float humi; uint32_t crc; } LogEntry_t; /* 双区域交替写区域 A 和区域 B 各占一页 */ void DataLog_Write(float temp, float humi) { static uint32_t seq 0; LogEntry_t entry; entry.seq seq; entry.temp temp; entry.humi humi; entry.crc CRC32((uint8_t *)entry, sizeof(entry) - 4); uint32_t addr (seq % 2) ? LOG_A_ADDR : LOG_B_ADDR; Flash_WritePage(addr, (uint8_t *)entry, sizeof(entry)); }CRC32你可以用现成的软件实现也可以开 STM32L4 的硬件 CRC 外设。LOG_A_ADDR和LOG_B_ADDR要选两个独立的 Flash 页别挨着防止擦一页影响另一页。从那以后我每次做掉电存储都强制走一遍“写一半断电”的测试在Flash_WritePage里插一个while(1)下载后手动复位看能不能恢复出上一条完整数据。这个习惯帮我拦住了至少三次现场丢数据的投诉。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?