简介:面向嵌入式系统与物联网方向学习者,这是一份围绕GEC6818开发板实现温湿度数据采集、显示与无线传输的参考资源。内容涉及开发板GPIO、ADC、UART等外设的调用方式,DHT系列/AM2302温湿度传感器的数据读取与换算,LCD显示或串口输出的编程方法,以及ZigBee协议栈基础、模块信道/网络ID配置和数据收发流程。通过阅读可掌握从传感器数据采集、本地处理到ZigBee远程上报的完整链路,也能了解串口调试工具与参数配置技巧,对课程设计、毕业设计或实际项目开发均有直接帮助。资源包为RAR压缩格式,大小约29.85MB,目前未提供内部文件清单,下载后可查看具体内容。已有3300余人学习,适合有一定单片机基础、正在入门嵌入式与物联网开发的读者。
1. GEC6818 温湿度显示项目为什么要选 ZigBee 搭建无线链路
朋友实验室的四间机房,原来每个房间一台 GEC6818 开发板挂 DHT11 测温湿度,显示屏就放门口,数据靠人走过去看。后来改成一块 GEC6818 做显示终端,两块 ZigBee 透传模块组星型网络,一块接传感器当采集节点,一块接 GEC6818 当协调器接收端,DHT11 数据从 UART 出去,绕一圈无线链路再回到 LCD。
GEC6818 是三星 S5P6818 八核 Cortex-A53 平台的板子,跑 ARM Linux,GPIO、UART、LCD 接口都现成,串口外设和 Qt 界面都能直接上手。ZigBee 模块选透传型,不碰 IEEE 802.15.4 协议栈细节,把模块当无线串口用,配置好信道、PAN ID、波特率就能用。这套链路对做嵌入式课设、小规模物联网网关、机房环境监测都有参考价值——单总线采集时序、ZigBee 组网参数、串口帧定义三个问题,挨个拆。
2. GEC6818 的 GPIO 驱动方式与 DHT11 单总线时序采集
2.1 引脚分配与传感器选型
GEC6818 的 GPIO 按 bank 分组,常见引出有 PE、PH 组和 EXP 扩展口。以 PE 组一个空闲引脚为例,先通过 sysfs 导出引脚、设置方向、写电平:
echo 128 > /sys/class/gpio/export echo out > /sys/class/gpio/gpio128/direction echo 0 > /sys/class/gpio/gpio128/value这里的 128 是内核 GPIO 编号,由 bank 起始号和引脚偏移计算而来。GEC6818 的内核版本不同,bank 起始编号可能不一样,最稳的方法是查内核 debug 接口:
cat /sys/kernel/debug/gpio确认这个编号没有被 LCD、SD 卡、调试串口等功能占用,再选一个空闲引脚。这一步不做好,会出现引脚能 export 但电平被其他外设拉死的情况,DHT11 数据读出来就全是 0xFF。
温湿度传感器这部分,课程设计里最常见的是 DHT11 和 AM2302(DHT22),参数对比如下:
| 参数 | DHT11 | AM2302(DHT22) |
|---|---|---|
| 湿度范围 | 20%-90%RH | 0%-99.9%RH |
| 湿度精度 | ±5%RH | ±2%RH |
| 温度范围 | 0-50℃ | -40-80℃ |
| 温度精度 | ±2℃ | ±0.5℃ |
| 通信方式 | 单总线 | 单总线 |
| 最小采样间隔 | 1s | 2s |
DHT11 的两个明显短板是采样间隔 1 秒、精度偏低,长期记录温漂数据时曲线不太好看。但它便宜、单线通信、接线简单,验证 GEC6818 到 ZigBee 的整条链路是够用的。如果项目要做趋势分析或温控策略,建议换 AM2302 或 I2C 接口的 SHT30,程序侧改动范围很小。
2.2 用户态申请 GPIO 的常见做法
GEC6818 没有树莓派那样的 wiringPi 库可用,用户态操作 GPIO 最直接的方式就是文件系统接口。把 open/read/write/close 封装成基础函数,后面读 DHT11 时序时直接调用:
#include <stdio.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #define GPIO_BASE "/sys/class/gpio/gpio128" static void gpio_direction(char *dir) { char path[64]; int fd; snprintf(path, sizeof(path), "%s/direction", GPIO_BASE); fd = open(path, O_WRONLY); write(fd, dir, strlen(dir)); close(fd); } static void gpio_set(int val) { char path[64]; int fd; snprintf(path, sizeof(path), "%s/value", GPIO_BASE); fd = open(path, O_WRONLY); write(fd, val ? "1" : "0", 1); close(fd); } static int gpio_get(void) { char path[64], buf[2]; int fd, ret; snprintf(path, sizeof(path), "%s/value", GPIO_BASE); fd = open(path, O_RDONLY); ret = read(fd, buf, 1); close(fd); return ret > 0 ? buf[0] - '0' : -1; }这段封装把一次 GPIO 操作映射为一次文件读写,逻辑简单,控制继电器、读取按键、检测电平都没问题。但代价是每次 open/read/close 都要陷入内核,叠加起来有延迟,所以用户态 GPIO 做不了微秒级时序操作。DHT11 的读取恰好属于后者,下面这套代码作为理解时序的演示可以跑起来,真正长期稳定运行建议换成 I2C 传感器或把驱动写进内核模块。
2.3 DHT11 时序读取与校验
DHT11 单总线协议的关键点在时序:主机先把总线拉低 18ms 以上触发传感器,然后释放总线等待响应;传感器回一个 80us 低电平和 80us 高电平,之后每个数据位以 50us 低电平开始,高电平持续 26-28us 表示 0,持续 70us 表示 1。完整数据是 40bit,即 5 个字节。
基于上面的 GPIO 封装,读取函数如下:
static int wait_level(int level, int timeout_us) { int us = 0; while (gpio_get() != level) { if (++us > timeout_us) return -1; usleep(1); } return 0; } int dht11_read(unsigned char buf[5]) { int i, j, data = 0, high_us; gpio_direction("out"); gpio_set(0); usleep(20000); // 拉低 20ms,触发 DHT11 gpio_set(1); usleep(30); // 高电平 30us 后释放总线 gpio_direction("in"); if (wait_level(0, 200)) return -1; // 传感器应答低电平 if (wait_level(1, 200)) return -1; // 应答高电平 if (wait_level(0, 200)) return -1; // 数据位前低电平 for (j = 0; j < 5; j++) { for (i = 0; i < 8; i++) { high_us = 0; wait_level(1, 60); // 跳过 50us 低电平 while (gpio_get() == 1) { // 高电平持续时长判断 0/1 high_us++; usleep(1); if (high_us > 100) return -1; } data = (data << 1) | (high_us > 30 ? 1 : 0); } buf[j] = data; data = 0; } if ((unsigned char)(buf[0] + buf[1] + buf[2] + buf[3]) != buf[4]) return -2; // 校验失败 return 0; }wait_level 超时设 200us,是给 DHT11 的 80us 应答低电平留足余量;数据位判断阈值 high_us > 30 是我在 GEC6818 上实际调出来的,不同板子环境温度不同可以微调,但不要低于 25 也不要高于 40,否则容易误判 0 和 1。
数据格式是 buf[0] 湿度整数、buf[1] 湿度小数、buf[2] 温度整数、buf[3] 温度小数、buf[4] 校验和。DHT11 的小数位通常返回 0,AM2302 会返回真实小数。温度零下时 buf[2] 最高位置 1,计算前要先做符号扩展,否则显示出来是 200 多的乱值。
这里也要说清楚边界:usleep 的精度依赖内核时钟粒度,高负载下可能睡过头,导致 wait_level 或 high_us 计数不准。实际部署如果每隔几分钟出现一次读失败,我一般直接把传感器换成 SHT30,用 I2C 替代单总线,采集稳定性会好很多。链路后续部分默认数据已经通过上述方式拿到,接下来重点是 ZigBee 模块配置和串口帧设计。
3. ZigBee 透传模块配置:信道、PAN ID、短地址一次搞定
3.1 模块角色与透传原理
ZigBee 网络节点分三类:协调器(Coordinator)负责建网和地址管理,路由器(Router)负责数据转发,终端节点(End Device)只能收发不能转发。实际项目中常见的透传模块(如 DL-22、DRF1605H 这类单串口模块)出厂时做了协议封装,开发者不用写 ZigBee 协议栈,只要把模块当无线串口用——串口发什么,对端模块就吐什么。
这里有个容易踩的点:终端节点为了省电默认会休眠。在透传模式下,如果 GEC6818 的 UART 输出一直保持高电平,模块会误以为有数据要发而无法入睡;反过来,对端模块还没入网就发数据,数据会被直接丢弃。所以配参数之前先把拓扑想清楚:本例中采集侧是终端节点,GEC6818 侧是协调器,只有两个模块,直接组星型网络。
3.2 三个必配参数与推荐值
同一个 ZigBee 网络里,模块能否互相通信由三个核心参数决定:信道(CH)、网络标识(PAN ID)、短地址(SHORT)。
| 参数 | 作用 | 协调器 | 终端节点 |
|---|---|---|---|
| CH | 射频信道,范围 11-26 | 15 | 15 |
| PAN ID | 网络标识,同一网络必须一致 | 0x1234 | 0x1234 |
| SHORT | 节点在网络内的短地址 | 0x0000 | 0x0001 |
| UART | 与主控通信的串口参数 | 9600,8,1,0 | 9600,8,1,0 |
信道选 15 只是推荐值,关键是所有模块必须一致。PAN ID 的作用类似 WiFi 的 SSID,PID 不同即使信道相同也组不了网。短地址在同一网络内不能冲突,协调器固定为 0x0000,其他节点从 0x0001 开始分配。波特率这行最容易出问题:模块出厂默认 9600 8N1,如果 GEC6818 串口程序配成 115200,会出现一端能发、一端收不全的现象,查半天发现是两边速率不一致。
3.3 AT 指令配置流程
下面这套指令是常见透传模块的配置格式,新大陆课程设计板载模块的指令集略有出入,但配置思路一样。先用 USB-TTL 把模块接到电脑,打开串口调试助手发送:
# 协调器 AT+ROLE=0 AT+CH=15 AT+PID=0x1234 AT+SHORT=0x0000 AT+UART=9600,8,1,0 AT+RESET # 终端节点 AT+ROLE=2 AT+CH=15 AT+PID=0x1234 AT+SHORT=0x0001 AT+UART=9600,8,1,0 AT+RESET每条指令以回车换行结尾,发完等模块回 OK 再发下一条。AT+RESET 是保存配置并重启模块,这步不做,参数断电后全部丢失。RESET 后模块进入透传模式,此时再发 AT 指令会被当成普通数据发给对端,想改参数得发特定前缀重新进入 AT 模式。
配置完成后做链路测试:两个模块都接电脑,A 串口发 "hello",B 串口能收到说明入网成功。收不到则按顺序排查:先看协调器建网指示灯、终端入网指示灯是否常亮;指示灯都正常但数据不通,回到波特率,用逻辑分析仪量模块实际输出的位宽。透传模式下模块对串口数据原样转发,GEC6818 侧不需要做协议转换,但裸数据在无线链路上传输时,对端很难判断一帧从哪里开始、到哪里结束,这正好引出下一章的帧设计。
4. 串口数据帧定义与 GEC6818 LCD 实时刷新
4.1 为什么需要自定义帧格式
直接往 ZigBee 模块串口丢两个温度值字节,接收端没法判断字节是否连续、是否完整。无线链路偶发丢包、粘包,接收端可能把上一帧的尾巴当成下一帧开头,CRC 校验也无从谈起。常见做法是定义一套最小帧:帧头、命令字、长度、有效载荷、CRC、帧尾。
| 字段 | 字节数 | 说明 |
|---|---|---|
| 0xAA 0x55 | 2 | 帧头,标识一帧开始 |
| CMD | 1 | 0x01 表示温湿度上报 |
| LEN | 1 | 有效载荷长度,本例为 0x04 |
| HUM_INT / HUM_DEC | 2 | 湿度整数、小数 |
| TEMP_INT / TEMP_DEC | 2 | 温度整数、小数 |
| CRC16 | 2 | 从帧头到数据区的校验值,低字节在前 |
| 0x0D 0x0A | 2 | 帧尾,便于调试观察 |
帧头用两个字节而不是一个,连续两字节匹配才能确认帧边界,误判概率低。业务数据里如果出现 0xAA 0x55,接收端靠 LEN 字段跳过即可。CRC 用 CRC16-CCITT,多项式 0x1021,初始化 0xFFFF,实现简单、查错能力足够。
4.2 发送端:GEC6818 串口封装与 CRC 计算
GEC6818 有多个 UART,调试口一般占 UART0,业务串口常用 UART2,设备节点是 /dev/ttySAC2。打开串口并设为 raw 模式:
int uart_open(char *path) { int fd = open(path, O_RDWR | O_NOCTTY); struct termios opt; tcgetattr(fd, &opt); cfsetispeed(&opt, B9600); cfsetospeed(&opt, B9600); opt.c_cflag &= ~CSIZE; // 8 数据位 opt.c_cflag |= CS8; opt.c_cflag &= ~PARENB; // 无校验 opt.c_cflag &= ~CSTOPB; // 1 停止位 opt.c_cflag &= ~CRTSCTS; // 禁用硬件流控 opt.c_iflag &= ~(IXON | IXOFF | IXANY); // 关闭软件流控 opt.c_lflag &= ~(ICANON | ECHO); // 非规范模式 opt.c_cc[VTIME] = 0; // 不等待超时 opt.c_cc[VMIN] = 1; // 有数据立即返回 tcsetattr(fd, TCSANOW, &opt); return fd; }这组配置要和 ZigBee 模块的 UART 参数对齐:模块配 9600 8N1,这里就是 B9600、CS8、无校验、1 停止位。VMIN 设为 1 意味着 read 至少要读到一个字节才返回,配合 VTIME 超时设置可以避免阻塞死等。
接下来是 CRC16 与组帧发送:
unsigned short crc16_ccitt(unsigned char *data, int len) { unsigned short crc = 0xFFFF; while (len--) { crc ^= *data++; for (int i = 0; i < 8; i++) crc = (crc & 0x0001) ? (crc >> 1) ^ 0x1021 : crc >> 1; } return crc; } int send_th_frame(int fd, unsigned char h_int, unsigned char h_dec, unsigned char t_int, unsigned char t_dec) { unsigned char frame[12]; unsigned short crc; frame[0] = 0xAA; frame[1] = 0x55; frame[2] = 0x01; // CMD:温湿度上报 frame[3] = 0x04; // LEN:4 字节有效载荷 frame[4] = h_int; frame[5] = h_dec; frame[6] = t_int; frame[7] = t_dec; crc = crc16_ccitt(frame, 8); frame[8] = crc & 0xFF; // CRC 低字节在前 frame[9] = (crc >> 8) & 0xFF; frame[10] = 0x0D; // 帧尾 frame[11] = 0x0A; return write(fd, frame, sizeof(frame)); }CRC 计算范围是 frame[0] 到 frame[7],也就是帧头、命令、长度、数据共 8 字节,frame[8] 和 frame[9] 是计算结果。低字节在前是嵌入式常见的字节序习惯,和小端环境下强转 short 的存储顺序一致。帧尾加 0x0D 0x0A 纯粹方便调试时在串口工具里肉眼观察,嫌带宽浪费可以去掉。
4.3 接收端:状态机解析与 CRC 校验
接收端按字节读,每收到一个字节就送入状态机:
#define MAX_PAYLOAD 8 int parse_byte(unsigned char c) { static unsigned char rx[2 + 1 + 1 + MAX_PAYLOAD + 2]; static int state = 0, len = 0, idx = 0; switch (state) { case 0: if (c == 0xAA) state = 1; break; case 1: state = (c == 0x55) ? 2 : 0; break; case 2: rx[0] = c; // CMD state = 3; break; case 3: len = c; // LEN idx = 0; if (len > 0 && len <= MAX_PAYLOAD) state = 4; else state = 0; // 长度超限,丢弃整帧 break; case 4: rx[1 + idx++] = c; if (idx == len) state = 5; break; case 5: rx[1 + len] = c; // CRC 低字节 state = 6; break; case 6: rx[1 + len + 1] = c; // CRC 高字节 // 用 crc16_ccitt(rx, 1 + len) 与组合出的校验值比较 state = 0; break; } return 0; }状态机的好处是接收缓冲区可以很小,不依赖 read 是否一次读满一帧。缺点是状态必须保存在静态变量里,多路串口同时解析时要改成结构体传参。实际工程中我会再补两个细节:LEN 上限做硬保护,防止异常长度把缓冲撑爆;两个帧头之间间隔超过 1 秒强制复位状态,防止半截帧卡住后续数据。CRC 比较逻辑放在 state 6,注意计算范围是 rx[0] 到数据区末尾,不包括 CRC 自身。
4.4 LCD 显示刷新
GEC6818 板载 LCD 常见是 7 寸 LVDS 屏,分辨率 1024x600,Linux 下跑 Qt 最方便。串口读线程解析出温湿度后,不要直接在线程里操作 QLabel,正确做法是写入全局变量,UI 线程用 QTimer 定期刷新:
QTimer *timer = new QTimer(this); connect(timer, &QTimer::timeout, []() { tempLabel->setText(QString("%1.%2°C").arg(g_temp_int).arg(g_temp_dec)); humLabel->setText(QString("%1.%2%RH").arg(g_hum_int).arg(g_hum_dec)); }); timer->start(1000);刷新间隔 1000ms 足够,温湿度是慢变量,没必要 100ms 刷一次。如果板子不带 Qt 环境,也可以直接操作 framebuffer 写数字,但那样要自己做字库点阵,开发量明显增加。Qt 方案在一线项目里更常见,后续扩展曲线绘制、多屏显示也方便。
5. 遇到采集数据乱码与掉线时,先查这三处
5.1 用逻辑分析仪确认串口位宽
两个模块一个改成 115200、另一个还在 9600,接收端数据变成乱码,先别急着改代码。把逻辑分析仪夹在接收端 UART 的 RX 引脚上,让对端发送字节 0x55,数一下一个 bit 的持续宽度。9600bps 下位宽约 104us,115200 下约 8.68us,量出来的值和预期偏差超过 5%,说明有模块的串口参数没生效,或者 RESET 后恢复成出厂值。这类问题用波形定位只需要两分钟,比反复改代码靠谱得多。
5.2 掉线自恢复:检测 LINK 引脚而不是应用层超时
ZigBee 透传模块通常会引出一根 LINK 状态引脚,入网成功后由低变高。把 LINK 接到 GEC6818 的 GPIO132,用查询方式读取,连续低电平超过 10 秒就让模块重新入网:
// LINK 引脚已导出为 gpio132 while (1) { if (gpio_get_by_num(132) == 0) { // 发 +++ 退出透传模式,再发 AT+RESET send_at_cmd(fd, "+++"); sleep(1); send_at_cmd(fd, "AT+RESET"); sleep(10); // 等模块重新入网 } sleep(1); }注意重新入网后不要立刻发业务数据,终端节点还没完成地址分配,数据会被网络层丢弃。这里用 LINK 引脚判断比应用层超时更可靠,因为模块掉线但串口链路还在时,应用层收不到数据会误判为“对端没发”,而 LINK 引脚直接反映射频链路状态。
5.3 终端节点低功耗的睡眠与唤醒节奏
ZigBee 终端节点本身支持休眠,配置为 End Device 后,串口数据发完模块会自动入睡,功耗从持续收发的 20-30mA 降到微安级。最容易翻车的细节在主控侧:GEC6818 的 UART 输出空闲时为高电平,这个高电平一直挂在模块 RX 上,模块会误认为有数据到达而保持唤醒。处理方法是采集节点发完一帧后,把 UART 的 TX 引脚配成输入模式(三态),或者用 MOS 管在休眠期间切断模块供电。睡眠周期和上报周期要错开,比如每 30 秒唤醒一次发完立刻睡,用功耗仪观察 24 小时曲线,确认没有异常尖峰后再固化流程。
本文还有配套的精品资源,点击获取