简介:基于 NXP IMX6ULL 的环境监测自主调控系统完整工程代码,面向嵌入式 Linux 学习者、物联网项目开发者以及需要课程设计或综合实训方案的高年级学生,可用于实现温湿度、光照、空气质量等参数的采集、显示与自动调节。压缩包共 312 个文件、约 11.35MB,核心为 53 个 h、38 个 cpp、32 个 c 源码文件,配合 12 个 Makefile 与 Qt 的 ui/pro/qrc 工程配置,另含 141 张 png 图片便于查看界面与硬件连线;rar 包内还附带 libev、jsonrpc-c 等依赖源码,能降低环境搭建成本,目录结构清晰。已有 752 人学习下载。代码涉及 IMX6ULL 的 GPIO、ADC、PWM、UART、I2C 等外设驱动,覆盖 DHT11、AP3216、SR04 等传感器,并集成 JSON-C/libev 网络通信与可视化界面;整个工程把嵌入式系统、传感器技术、网络通信和界面显示整合为一体,适合理解设备树、驱动注册、中断与定时采集等关键点,可直接对照原理图阅读驱动编写、数据处理和自主调控逻辑,便于二次开发或完成项目报告。
1. 先从 IMX6ULL 聊起:这套环境监测自主调控代码到底能帮你省多少事?
做嵌入式 Linux 的人,手里十有八九有一块 IMX6ULL 板子。这颗 Cortex-A7 芯片在项目里最常见的死法不是性能不够,而是从裸机点灯到 Linux 驱动这一跳没人带。这套环境监测自主调控系统的完整代码,就是把跳台阶铺平的一个实例:它把温湿度、光照、空气质量的采集,继电器控制风扇、加热器、补光灯的自主调控,以及数据记录和本地显示,全部串在一起。也就是说,你拿到的不是一段 demo,而是一个能直接跑、能改逻辑、能换传感器的可运行工程。适合正在做课程设计、准备电子设计竞赛,或者想要一套可魔改的物联网原型代码的读者。如果你拿到手先不急着烧录,花半天把设备树和驱动过一遍,后面改起来会顺手很多。
2. 系统架构与硬件选型:为什么 IMX6ULL 适合做自主调控
2.1 硬件平台:IMX6ULL 的接口资源够不够
IMX6ULL 是 NXP 的工业级 Cortex-A7 单核处理器,主频 528MHz,片上集成了两路 I2C、两路 SPI、八路 UART、两个 12 位 ADC 控制器(共 8 通道)以及大量可复用的 GPIO。对一套环境监测自主调控系统来说,这些接口刚好卡在“够用且不浪费”的位置上:传感器大多是单总线、I2C 或模拟输出,执行器只需要数字输出控制继电器,数据量又不足以压垮 CPU,所以 IMX6ULL 跑 Linux 后的实时性和带宽都留有余量。
很多人会问,为什么不用 STM32?STM32 做裸机采集、继电器开关完全没问题,但一旦想把采到的数据存下来、开个网页看看曲线、或者用 Python 跑个控制脚本,就发现内存和网络栈都不好伺候。IMX6ULL 的优势是能跑完整 Linux,却比树莓派这类消费级板卡更强调工业稳定性。环境监测通常要求设备 7×24 小时开机,核心板加底板的散热和供电设计比树莓派那种紧凑板子更容易做稳。
从项目代码的角度看,IMX6ULL 平台的选型还有一个隐性好处:学习资料多。正点原子、野火、NXP 官方都有成熟的 BSP 和文档。这套环境监测代码是基于常见 IMX6ULL 核心板写的,引脚定义集中在设备树里,换板子时只需要改 dts,不需要动应用层逻辑。这也就是为什么我建议你把重点放在理解设备和应用之间的调用关系上,而不是死记某个引脚号。
2.2 传感器与执行器选型:DHT11/BH1750/继电器怎么接
这套系统里传感器选型遵循“稳定性优先,精度够用”的原则。温湿度用 DHT11 或者 AM2301,两者都是单总线协议,只是一根数据线,接任意 GPIO 即可;光照用 BH1750,走 I2C 总线,属于数字传感器,抗干扰比光敏电阻强;空气质量用 MQ-135 这种模拟输出传感器,需要接在 IMX6ULL 的 ADC 通道上,读取电压换算成 ppm 浓度级别。执行器部分,风扇、加热器、补光灯这类 220V 或大电流设备,统一用继电器控制,继电器模块带光耦隔离,由 GPIO 输出电平驱动。
下面是我在项目中常用的接线表,具体引脚号需要根据你自己的底板原理图调整:
| 功能模块 | 型号/接口 | IMX6ULL 接入位置 | 关键参数 |
|---|---|---|---|
| 温湿度 | DHT11 单总线 | GPIO1_IO04 | 数据线上拉 4.7kΩ,采集间隔 ≥1s |
| 光照 | BH1750 I2C | I2C2,地址 0x23 | 分辨率 1~65535 lx,测量周期约 120ms |
| 空气质量 | MQ-135 模拟量 | ADC1_CH1 | 预热 3 分钟,输出电压随浓度升高 |
| 继电器 | 光耦隔离继电器模块 | GPIO1_IO05/06 高电平触发 | 控制风扇/加热器/补光灯,电源独立 |
注意 DHT11 的采集周期是硬约束,它内部是慢速温湿度芯片,读得太快会返回错误。BH1750 的 I2C 地址有 0x23 和 0x5C 两种,取决于 ADDR 引脚电平,接线前先查原理图。MQ-135 上电后前几分钟输出漂移很大,代码里我会做一个预热期的逻辑,启动后跳过最初的 180 秒采样,或者对采样值做一个滑动平均。
2.3 软件总体流程:从设备树到应用层的调用链
软件层面,这套代码是按 Linux 驱动与应用分离的思路组织的。设备树文件负责描述引脚复用、I2C 设备地址、GPIO 编号;内核驱动把传感器变成 /dev/i2c-x 或 /dev/mq135 这样的节点;应用层程序则通过/dev/gpiochipX、/dev/i2c-x和 sysfs 接口读写硬件。项目根目录结构如下:
imx6ull-env-monitor/ ├── dts/ │ └── imx6ull-env-board.dts ├── driver/ │ ├── dht11_gpio.c # 单总线GPIO驱动 │ ├── mq135_adc.c # 模拟量ADC驱动 │ └── relay_gpio.c # 继电器GPIO驱动 ├── app/ │ ├── main.c # 主程序:线程调度与控制逻辑 │ ├── sensor.c # 传感器读取封装 │ ├── control.c # 自主调控阈值与PID逻辑 │ ├── storage.c # SQLite存储与HTTP服务 │ └── Makefile ├── scripts/ │ ├── env-monitor.service # systemd服务文件 │ └── load_module.sh # 加载驱动并创建设备节点 └── README.md这个目录设计的核心是“驱动负责拿数据,应用负责做决策”。驱动层不写业务逻辑,业务逻辑全部集中在 app 下的 control.c 和 main.c 里,方便你换控制策略。驱动层只暴露最简单的 read 接口,比如 DHT11 驱动被打开后,每次 read 返回一个 4 字节的结构体,包含温度、湿度、校验状态;MQ135 驱动返回原始 ADC 值和换算后的浓度。
应用层主程序开了三个线程:采集线程每 1 秒读取一次所有传感器;控制线程每 2 秒根据最新数据运算并刷新继电器输出;存储线程每 10 秒把一次汇总记录写入 SQLite,同时运行一个监听 8080 端口的 HTTP 服务,让局域网内的电脑可以看实时数据和历史曲线。这三个线程之间用全局结构体加互斥锁传递数据,没有用复杂的消息队列,原因很简单:这个系统的数据量极小,锁的开销可以忽略,代码却好读很多。
3. 把设备树和驱动跑通:GPIO、I2C 与 ADC 的配置细节
3.1 设备树修改:pinctrl 与设备节点的写法
环境监测系统在设备树里要干三件事:把引脚复用成正确的功能(比如 I2C 引脚下拉到内部偏置或复用为 I2C 模式);声明传感器设备挂在哪个总线上;给 GPIO 起个容易识别的名字。IMX6ULL 的设备树继承自 imx6ull.dtsi,你需要在自己板子的 dts 里追加节点。下面是一段我在项目中实际使用的设备树片段,去掉了无关板级配置:
&i2c2 { clock-frequency = <100000>; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_i2c2>; status = "okay"; bh1750: bh1750@23 { compatible = "rohm,bh1750"; reg = <0x23>; }; }; &adc1 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_adc1>; vref-supply = <®_3p3v>; status = "okay"; }; pinctrl_dht11: dht11grp { fsl,pins = < MX6UL_PAD_GPIO1_IO04__GPIO1_IO04 0x8000 >; }; relay-gpios { compatible = "gpio-leds"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_relay>; status = "okay"; fan { gpios = <&gpio1 5 GPIO_ACTIVE_HIGH>; default-state = "off"; }; heater { gpios = <&gpio1 6 GPIO_ACTIVE_HIGH>; default-state = "off"; }; };这段设备树里,我最想强调的是 pinctrl 属性里的那个0x8000。很多刚上手的人修改 dts 时只改了fsl,pins里的宏定义,忽略了后面的配置值,导致引脚内部上拉电阻没打开、I2C 上不拉、GPIO 高低电平不受控。0x8000是典型的使能内部上拉并设置高转换率的配置,对于 DHT11 这种开漏单总线设备尤其重要。另外&adc1下的vref-supply必须指向一个管脚供电节点,否则 ADC 转换结果会一直读成 0。
编译设备树时,我一般直接在你内核源码目录下跑make dtbs,生成imx6ull-env-board.dtb后拷贝到 boot 分区。注意不要用make dts这个目标,那是无关的旧目标,很多人因为拼写错误找半天。烧写后先用cat /proc/device-tree/检查节点是否被加载,再用i2cdetect -y 2看看 I2C2 上能不能扫到 0x23 地址。
注意:DHT11 的 pinctrl 配置不要随便加
SION位,单总线 GPIO 模式只要标准输入输出即可,开 SION 反而会让数据线被其他外设旁路拉死后读不到起始信号。
3.2 传感器驱动:I2C 设备驱动框架
BH1750 的驱动适合用 Linux 标准 I2C 驱动框架写,核心是i2c_driver的 probe 函数里初始化设备,然后通过i2c_smbus_read_word_data这类接口读数据。我提供了rohm,bh1750compatible 的名字,这样驱动会通过设备树自动匹配,不需要手动 insmod 带参数。下面是一个最简 BH1750 驱动的读取部分:
static int bh1750_read_light(struct i2c_client *client, int *value) { struct i2c_msg msg[1] = { { .addr = client->addr, .flags = 0, .len = 1, .buf = (u8[]){0x00}, }, }; if (i2c_transfer(client->adapter, msg, 1) != 1) return -EIO; msleep(120); // 等待测量完成,BH1750约120ms u8 buf[2]; struct i2c_msg read[1] = { { .addr = client->addr, .flags = I2C_M_RD, .len = 2, .buf = buf, }, }; if (i2c_transfer(client->adapter, read, 1) != 1) return -EIO; *value = (buf[0] << 8) | buf[1]; *value = *value * 10 / 12; // 按手册将原始值转换为 lx,需配合采样精度 return 0; }这个读取过程里有两个细节容易翻车。第一,msleep(120)不是随便拍的。BH1750 的测量时间和分辨率模式有关,手册上写高分辨率模式最大 180ms,我在代码里留足 120ms 只是最小值,实际项目里我把读取间隔统一放到主循环,每 200ms 只调用一次,避免重复测量。第二,原始值换算系数不是一成不变的,BH1750 有个测量精度寄存器可以配置为 0.5、1、4 lx 每步,代码里的*10/12对应默认精度,如果你改了测量模式就要一起改。
驱动写完不要急着insmod,先查busybox devmem或/sys/kernel/debug/pinctrl确认引脚复用。I2C 驱动最常见的问题不是代码逻辑,而是设备树里reg = <0x23>与从机地址不一致,或者 I2C 总线上有别的设备抢了地址。
3.3 应用层直接操作 GPIO:sysfs 与 libgpiod 的选择
如果你的系统内核版本比较旧,习惯用 sysfs 操作 GPIO,但新内核已经推荐 libgpiod。IMX6ULL 常见 BSP 内核是 4.9 或 5.4,两者都能用,我建议新项目直接用 libgpiod,因为 sysfs 的 export 机制在调用频繁时会出现权限和竞态问题。
#include <gpiod.h> #define RELAY_FAN 5 #define RELAY_HEATER 6 static void relay_init(struct gpiod_chip *chip) { struct gpiod_line *line; for (int offset : {RELAY_FAN, RELAY_HEATER}) { line = gpiod_chip_get_line(chip, offset); gpiod_line_request_output(line, "relay", 0); gpiod_line_set_value(line, 0); } } static void relay_set(int offset, int value) { struct gpiod_line *line; line = gpiod_chip_get_line(chip, offset); gpiod_line_set_value(line, value); }gpiod_chip_get_line的第一个参数是struct gpiod_chip,通过gpiod_chip_open_by_name("gpiochip0")获取。注意 IMX6ULL 的 GPIO 控制器被枚举成多个 gpiochip,你的继电器接在哪一组 GPIO 上,chip 编号就是哪个,不能想当然默认 gpiochip0。判断方法很简单,跑一遍gpiodetect,根据控制器基址对应到芯片手册里的 GPIO1/GPIO2/GPIO3/GPIO4/GPIO5。
libgpiod 的优点是偏移量可以直接用设备树下发,不用自己去/sys/class/gpio/export踢一脚。但要注意gpiod_line_request_output函数的第二个参数是用户自定义的 consumer 标签,调试时会出现在/sys/kernel/debug/gpio里,方便确认谁占用了引脚。我还习惯在 set 之前先判断当前状态,只在变化时写一次,输出引脚的电平才不会抖动。
4. 自主调控逻辑与数据流:阈值控制与 PID 的区别
4.1 控制策略:多传感器多执行器的优先级
自主调控系统最核心的问题不是读数据,而是怎么让多个执行器协同。比如温度高于 30℃ 就开风扇,湿度低于 40% 就开加热器,但风扇和加热器同时开着就互相打架。项目里我采用的策略是一个带优先级的规则表:先判断温度是否超限,再判断湿度,最后是光照,每个规则之间加滞回区。这样避免设备频繁启停。
规则用结构体数组描述,方便扩展:
typedef struct { enum sensor_type type; /* TEMP / HUMI / LIGHT / AIR */ int high_threshold; /* 上限触发值 */ int low_threshold; /* 下限解除值 */ int high_deadzone; /* 上限回差 */ int low_deadzone; /* 下限回差 */ int target_relay; /* 控制哪个继电器 */ int active_level; /* 触发时输出电平 */ int priority; /* 数值越大优先级越高 */ } env_rule_t; static const env_rule_t rules[] = { {TEMP, 30, 26, 2, 1, RELAY_FAN, 1, 100}, {HUMI, 0, 0, 0, 40, RELAY_HEATER, 1, 90}, {LIGHT, 3000, 1500, 500, 200, RELAY_LIGHT, 1, 80}, };这里每一行就是一个典型的滞回控制:温度到 30℃ 开风扇,降到 26℃ 才关;湿度低于 40% 开加热器(假设是除湿场景),升到 45% 关;光照超过 3000 lx 关补光灯,低于 1500 lx 开。滞回区间high_deadzone存在的意义是避免临界点震荡,比如温度在 29.9℃ 和 30.1℃ 之间反复横跳,继电器每几秒咔嚓一次。写控制逻辑时不要在采集线程里直接操作继电器,应该由独立控制线程读取全局数据后统一决定输出,保证每个周期输出的结果是一致的。
4.2 核心控制循环:poll 多路复用还是线程
应用层主循环我会用线程,而不是 poll。理由是传感器采集间隔不一,DHT11 需要 1 秒以上间隔,BH1750 需要 120ms,MQ-135 需要连续采样做平滑,如果用poll统一阻塞,时间片反而难安排。我更倾向于开三个线程,用条件变量或直接互斥锁同步。
static pthread_mutex_t g_data_lock; static env_data_t g_sensor_data; static void *collect_thread(void *arg) { while (1) { struct env_data_t prev = g_sensor_data; read_dht11(&g_sensor_data.temp, &g_sensor_data.humi); read_bh1750(&g_sensor_data.light); read_mq135(&g_sensor_data.air_ppm); pthread_mutex_lock(&g_data_lock); g_sensor_data = prev; // 实际代码中是赋值新数据,这里仅为示意 pthread_mutex_unlock(&g_data_lock); usleep(200000); // 200ms周期 } return NULL; }实际上,更好的做法是让每个传感器有自己的读取函数,采集线程每隔 200ms 把读取结果写入全局缓存,控制线程每隔 500ms 读取一次,执行控制规则。锁的范围越小越好,我上面的代码为了示意把锁放在整个数据复制外面,真正项目里应该让传感器驱动内部自带上一次结果缓存,采集线程只负责更新,避免两个线程同时访问同一个 I2C 设备。I2C 总线本身不允许并发写,两个线程同时i2c_transfer会返回 EBUSY,这是常见问题。
控制线程里我一般还会加一个“强制手动模式”标志。当调试时通过串口把manual_mode置 1,控制线程就只按手动命令输出,不执行规则表。这样测试继电器是否接错线时,不用反复改温度。
4.3 数据存储与可视化:SQLite + HTTP 服务的轻量方案
数据记录我选择 SQLite,而不是直接写 CSV。原因很简单:查询历史数据方便,比如“昨天 14:00 到 15:00 的温湿度曲线”用一条 SQL 就出来了,CSV 还得自己解析时间戳。IMX6ULL 的 528MHz 主频跑 SQLite 完全没有压力,数据库文件放在 SD 卡或 eMMC 上。
CREATE TABLE env_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER NOT NULL, temp REAL, humi REAL, light INTEGER, air INTEGER ); CREATE INDEX idx_ts ON env_log(ts);插入用 prepared statement,批量每 10 秒一条,一周的数据量也就是 6 万条左右。SQLite 写这个量非常轻松,但要注意不要每次插入后都做一次VACUUM,那是典型的性能自杀操作。应用层调用 SQLite 的 C API 时,我会把数据库操作封装在 storage.c 里,对外只暴露storage_init和storage_insert两个接口,主程序不用关心 SQL 长什么样。
HTTP 服务我用的是轻量级库,监听 8080 端口,输出一个简单的 JSON 接口。浏览器端可以拉/api/latest看到当前数据,拉/api/history?hours=12拿到最近 12 小时的数据并画图。这部分的坑主要是 SQLite 在多线程环境下的打开模式,必须用SQLITE_OPEN_FULLMUTEX,否则存储线程偶尔会报database is locked。
5. 避坑指南:IMX6ULL 环境监测项目的常见问题与排查
5.1 设备树改了不生效:引脚复用冲突
现象:修改 dts 后重新编译烧写,发现 GPIO 输出电平没有变化,cat /sys/kernel/debug/gpio里看到请求该引脚前被其他模块占用,或者引脚状态一直是 pending。
原因:设备树里的fsl,pins宏定义与板级 dts 里已有配置冲突,最常见的是同一引脚被 I2C 和 GPIO 都申请了。IMX6ULL 的引脚复用是由 IOMUXC 控制器决定的,同一个 ALT 模式只能选一个功能,dts 里后加载的节点虽然可能不报错,但 IOMUXC 的寄存器优先级并不是按节点顺序,而是按 pin 的启动时握手顺序。
解决:先用cat /sys/kernel/debug/pinctrl/...查看当前 pin 的 mux 状态,确认你改了配置后实际生效值。如果是复用冲突,把冲突的引脚功能注释掉,或者把传感器换到另一组复用空闲的引脚上。我在测试 DHT11 时遇到过一次,它的 GPIO1_IO04 在另一个 dtsi 中被定义成 UART1_RTS 信号,结果 dts 里 pinctrl 不冲突,但 U-Boot 启动时早期初始化把它拉高,导致 DHT11 一直读不到起始信号。
5.2 I2C 传感器读不到数据:时序和上拉电阻
现象:i2cdetect -y 2能扫到 0x23,但应用层读 BH1750 返回 -EIO,或者读出来的光照值全为 0。
原因:BH1750 这类 I2C 通讯时单片机的时钟速率太高,或者 SCL/SDA 上拉电阻缺失。IMX6ULL 的 I2C 控制器默认时钟是 66MHz 经过分频,设备树里 clock-frequency = <100000> 是给软件一个目标值,但实际高低电平时间受引脚驱动能力影响。如果上拉电阻阻值太大(比如 10kΩ),上升沿太缓,设备在高速模式下采样不到正确电平。
解决:把 I2C 频率先降到 100kHz,确认能读再慢慢加。硬件上在 I2C2 的 SCL/SDA 上加上 4.7kΩ 到 3.3V 的上拉,有开发板已经引出了内部上拉,但仍要用万用表确认电平。另外要注意的是,BH1750 的供电必须稳定,如果与继电器共用 5V 电源,继电器吸合瞬间会影响 I2C 通讯,严重时会复位传感器。我踩过一次这个坑,后来把传感器供电单独用 3.3V LDO 隔离就稳定了。
5.3 继电器动作导致系统重启:电源隔离与浪涌
现象:继电器吸合时 IMX6ULL 偶尔死机或者重启,尤其是控制 220V 交流负载时,SD 卡里的程序报错。
原因:继电器线圈是感性负载,吸合和释放瞬间会产生反向电动势,如果驱动电路没有续流二极管,这个尖峰顺着电源线打进核心板,造成复位。更严重的是 220V 交流负载断开瞬间产生的高频干扰,通过空间耦合到传感器模拟线上,导致 ADC 读数乱跳。
解决:继电器模块用光耦隔离型,驱动输入端和输出负载侧分开供电。我习惯用 5V 给继电器模块供电,3.3V 只负责 IMX6ULL 的 GPIO 控制信号,两者电源不共地或者通过磁珠单点连接。如果板子上没有隔离电源,至少也要在继电器线圈两端并联一个 1N4148 二极管,反向接法吸收电动势。纯逻辑控制场合,用固态继电器比电磁继电器好得多,只是价格贵一点。
5.4 自主调控阈值附近震荡:加滞回回差
现象:温度在 30℃ 附近波动,风扇每隔几秒开关一次,继电器线圈发烫甚至触点粘死。
原因:规则表只有单阈值,温度过上限就开,低于上限就关,没有滞回区。系统热惯性大,温度在临界值附近细微变化都会导致输出翻转。
解决:给每个阈值加回差,也就是我前面规则表里的 high_deadzone 和 low_deadzone。风扇开是 30℃,关是 26℃;补光灯开是 1500 lx,关是 3000 lx。回差必须大于传感器噪声和温度波动范围,DHT11 的波动约 ±0.5℃,所以回差设 2℃ 比较合理。另外控制周期也要对,控制线程至少 500ms 运行一次,别在采集线程里频繁判断。
6. 把系统跑稳的进阶技巧:校准、看门狗与日志定位
6.1 传感器校准与滤波
MQ-135 这类模拟传感器出厂一致性差,不校准的话数据只有相对意义。我会在系统首次上电后进入校准模式:让设备在洁净空气中运行 3 分钟,记录 ADC 稳定值作为基准,再换算成 PPM 时用这个基准而不是厂商典型值。校准结果存到 /etc/env_monitor.conf,每次启动读取。
static float mq135_calibrate(int adc_base_raw) { static float coef = 10.0; // 经验公式:ppm = (V/V0 - 1) * coef,V0是洁净空气基准电压 return (1.0f * adc_current / adc_base_raw - 1.0f) * coef; }这个公式只是个线性模型,精度不高,但胜在稳定。实际污染气体浓度和电压的关系更接近对数曲线,如果你有标准气源,可以把 coef 换成分段查表。我一般还会对 ADC 原始值做滑动平均滤波,连续采 5 次取中间值和平均值,能滤掉继电器开关造成的尖峰。滤波窗口长度别设太大,否则控制反应变慢,反而在真实污染时耽误事。
6.2 看门狗与进程守护
环境监测是长时间运行设备,应用进程一旦崩了,传感器就失效但继电器可能停在错误状态。IMX6ULL 上我会用系统看门狗守护应用进程,内核配置了 CONFIG_WATCHDOG,Linux 下可以喂狗。
#!/bin/sh while true; do if ! pgrep -f "env-monitor" > /dev/null; then echo "env-monitor crashed at $(date)" >> /var/log/env_monitor.log /usr/bin/env-monitor --daemon & fi sleep 5 done这个 shell 脚本由 systemd 以Restart=always的方式拉起来,即使主进程崩溃也能自动拉起。注意喂狗的是应用进程本身,应用里每 5 秒写一次 /dev/watchdog,如果应用卡死不再喂狗,看门狗硬件会在 30 秒后强制重启系统,但这不是最优解。我一般把看门狗超时设置成 60 秒,避免主流程偶尔忙时拉不开。
6.3 用 systemd 实现开机自启与重启管理
项目里我给你准备了 env-monitor.service 文件,但你拿到手要改路径和用户。标准配置如下:
[Unit] Description=IMX6ULL Environmental Monitor After=network-online.target [Service] ExecStart=/usr/local/bin/env-monitor -c /etc/env_monitor.conf Restart=always RestartSec=3 User=root WorkingDirectory=/usr/local/env-monitor [Install] WantedBy=multi-user.target这里Restart=always是必须的,环境监测进程不像一次性服务跑完就退,需要常驻。RestartSec=3是智能的:进程崩溃后停 3 秒再拉起来,避免快速循环重启吃满 CPU。如果进程是被 systemd stop 命令停掉的,Restart=always不会生效,所以不会出现“系统关机时服务反复被拉起”的尴尬。
我从那次继电器尖峰把数据库文件搞坏之后,每回写这种长期运行的项目都会强制做三件事:给继电器加续流保护、给进程配 systemd 重启、给数据表开 WAL 模式。这三个习惯救过我好几次,也让我逐渐把注意点从“代码能跑”转移到“设备能稳定跑一个月不碰它”。这套 IMX6ULL 环境监测项目代码从一开始就是按这个标准整理的,你在复现时如果遇到启动不了、数据异常,先按第 5 章的排查顺序走一遍,大部分问题都能收敛到接线或设备树配置上。希望帮到你。
本文还有配套的精品资源,点击获取