GEC6818+GY-39环境监测:嵌入式Linux显示触摸与传感器实战
2026/9/11 17:55:17 网站建设 项目流程

简介:这是一份基于GEC6818开发板的嵌入式Linux综合项目资源,适合需要完成课程设计或入门嵌入式开发的读者。项目以C语言实现,融合了GPIO控制、GY-39传感器采集、LCD显示、触摸屏交互与音乐播放等功能,可触屏控制小灯开关以及音乐的播放、暂停、上一首、下一首,同时实时显示温湿度、光照强度和烟雾值,覆盖了驱动加载、交叉编译与板端部署的完整流程。资源包共25个文件、5.73MB,主要包括7个C源文件、6个头文件、5幅BMP图片、3首MP3、2个可加载的ko驱动、1个main可执行程序和1个json配置,结构清晰,便于对照源码理解模块划分。目前已有3041人学习,入手后可将main、图片与歌曲导入开发板直接运行,也能基于源码修改功能或复用驱动与界面逻辑,是快速跑通传感器加多媒体交互项目的实用参考。

1. GEC6818+GY-39 环境监测项目的硬件与软件分层

接触过 GEC6818 的人都知道,这颗 S5P6818 跑起嵌入式 Linux 之后,最麻烦的不是功能本身,而是把 LCD、触摸屏、I2C 传感器、PWM 这些内核设备在用户态拼成一条完整逻辑。这个项目正是把这几块全部串了起来:GY-39 实时返回温湿度、光照和烟雾数据,主界面在 LCD 上显示数值与状态图;触摸屏负责六个交互区域,分别控制音乐播放、暂停、上一首、下一首以及小灯开关。工程清一色 C 语言,编译环境是 Linux,用交叉编译器生成 main 后,连图片带歌曲一起灌进开发板就能直接运行。

这套代码里最值得琢磨的不是某个算法,而是嵌入式应用层和内核驱动打交道的标准姿势:framebuffer 映射、input 事件读取、i2c-dev 访问、sysfs 操作 PWM。它把很多教程里零散的点连成了一条可复现的主线,适合正在做课程设计或者刚上手嵌入式 Linux 应用开发的人。下面按显示、触摸、传感器、音频四条线把工程拆开讲,最后给出交叉编译和烧录验证的完整命令。

2. LCD framebuffer 显示与 BMP/字模坐标计算

2.1 /dev/fb0 初始化与内存映射

GEC6818 的 LCD 在 Linux 下对应/dev/fb0,操作显示本质上就是往这块内存里写像素。第一步用ioctl拿屏幕参数,再用mmap做共享映射,这样用户态程序可以直接用指针修改屏幕内容,不需要每次write都经过文件系统。

#include <stdio.h> #include <fcntl.h> #include <sys/mman.h> #include <sys/ioctl.h> #include <linux/fb.h> static int fb_fd; static unsigned int *fb_base; static struct fb_var_screeninfo vinfo; int lcd_init(void) { fb_fd = open("/dev/fb0", O_RDWR); if (fb_fd < 0) return -1; if (ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo) < 0) return -1; fb_base = mmap(NULL, vinfo.xres * vinfo.yres * 4, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0); if (fb_base == MAP_FAILED) return -1; return 0; } void lcd_draw_point(int x, int y, unsigned int color) { if (x < 0 || x >= vinfo.xres || y < 0 || y >= vinfo.yres) return; fb_base[y * vinfo.xres + x] = color; } void lcd_clear(unsigned int color) { for (int i = 0; i < vinfo.xres * vinfo.yres; i++) fb_base[i] = color; }

这里mmap的 length 是xres * yres * 4,因为 GEC6818 出厂 LCD 一般是 32 位色 ARGB8888,每个像素占 4 字节。vinfo.xresvinfo.yres一定要从驱动读,不要写死成 800x480。我碰到过把屏幕从 7 寸换成 10.1 寸的板子,分辨率变成 1024x600,写死坐标的代码直接花屏。lcd_draw_point里的越界判断也不能省,BMP 边缘像素或者字模边界稍微算错,就会污染屏幕外的内存地址。

2.2 BMP 图片的解析与显示位置计算

工程里大量使用 24 位 BMP 做界面底图和按钮图标。BMP 文件结构不复杂:前 54 字节是文件头和信息头,offset 18处是宽度,offset 22处是高度,offset 28处是位深。关键是 BGR 存储顺序以及行序自下而上。

void show_bmp(const char *path, int x0, int y0) { FILE *fp = fopen(path, "r"); if (fp == NULL) return; unsigned char header[54]; fread(header, 1, 54, fp); int w = header[18] | header[19] << 8 | header[20] << 16 | header[21] << 24; int h = header[22] | header[23] << 8 | header[24] << 16 | header[25] << 24; int bpp = header[28] | header[29] << 8; int line_bytes = ((w * bpp / 8) + 3) & ~3; unsigned char *buf = malloc(line_bytes); if (buf == NULL) { fclose(fp); return; } for (int y = h - 1; y >= 0; y--) { fread(buf, 1, line_bytes, fp); for (int x = 0; x < w; x++) { unsigned char b = buf[x * 3]; unsigned char g = buf[x * 3 + 1]; unsigned char r = buf[x * 3 + 2]; lcd_draw_point(x0 + x, y0 + y, (r << 16) | (g << 8) | b); } } free(buf); fclose(fp); }

注意line_bytes的计算。BMP 每行会按 4 字节对齐,比如宽 50 像素、24 位色的行数据是 150 字节,但实际存储会补齐到 152 字节。不处理这 2 字节,后续每一行都会错位,图片显示出来是斜的。另外读取循环从h - 1倒着走,因为 BMP 底部先存储,正着读会把图片上下颠倒。函数参数x0y0是图片在屏幕上的起始坐标,缩放和裁剪不在这个函数里做,底图图片我一般直接用板子同分辨率的 BMP,省去缩放算法。

偏移长度含义典型值
02文件标识"BM"
184图像宽度800
224图像高度480
282像素位深24
54-像素数据起点54

2.3 字模库与中文字符显示

界面标题和数值单位用中文字符更直观,但 Linux 默认没有内置点阵字库给应用层用。工程里的zimo.c用的是 16x16 HZK16 字库方案:每个汉字固定 32 字节,每字节的每个 bit 对应 16x16 点阵上的一个像素。

void show_hanzi(int x, int y, const unsigned char *str, unsigned int color) { FILE *fp = fopen("HZK16", "rb"); if (fp == NULL) return; unsigned char qm = str[0] - 0xB0; unsigned char wm = str[1] - 0xA1; long offset = (long)(qm * 94 + wm) * 32; fseek(fp, offset, SEEK_SET); unsigned char row[32]; fread(row, 1, 32, fp); for (int i = 0; i < 16; i++) { for (int j = 0; j < 2; j++) { for (int k = 0; k < 8; k++) { if (row[i * 2 + j] & (0x80 >> k)) { lcd_draw_point(x + j * 8 + k, y + i, color); } } } } fclose(fp); }

GB2312 编码里汉字区码从 0xB0 开始,位码从 0xA1 开始,所以计算索引前要先把这两个值减掉。字模文件 HZK16 必须和 main 放在同一目录下,否则fopen失败后界面汉字会全部消失,只剩数字和英文。字体颜色直接传入前景色,背景不用单独画,底图本身就是深色的。

3. 触摸屏事件解析与五按键控制逻辑

3.1 input_event 结构与原始坐标读取

触摸屏在 Linux 里是标准的 input 设备,节点一般在/dev/input/event0event1。用户态程序read这个设备节点时,每次读到的是一个struct input_event,里面通过typecodevalue三个字段描述一类输入事件。

#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <linux/input.h> #define TOUCH_DEV "/dev/input/event0" static int ts_fd; int ts_open(void) { ts_fd = open(TOUCH_DEV, O_RDWR); return ts_fd; } int ts_read(int *px, int *py) { struct input_event ev; int x = 0, y = 0; while (read(ts_fd, &ev, sizeof(ev)) > 0) { if (ev.type == EV_ABS) { if (ev.code == ABS_X) x = ev.value; else if (ev.code == ABS_Y) y = ev.value; } else if (ev.type == EV_KEY && ev.code == BTN_TOUCH) { if (ev.value == 0) { *px = x; *py = y; return 0; } } } return -1; }

read是阻塞的,没有触摸时进程会一直睡在内核态。EV_ABS表示绝对坐标事件,ABS_XABS_Y对应的value是触摸屏控制器上报的原始坐标,EV_KEY里的BTN_TOUCH表示手指按下或抬起,value = 0是抬起,value = 1是按下。我选择在抬起时返回坐标而不是按下时返回,因为按下过程的坐标会有明显漂移,抬起时手指位置最接近用户意图。

字段含义常见值
type事件大类EV_ABS = 3, EV_KEY = 1
code具体事件代码ABS_X = 0, ABS_Y = 1, BTN_TOUCH = 330
value事件数据按下/抬起为 1/0,坐标为具体数值

3.2 坐标映射与触摸校准

触摸屏的原始坐标范围和 LCD 分辨率不一定一致。GEC6818 早期出厂镜像用的 7 寸电阻屏,触摸控制器上报范围常见是 1024x600,而屏幕输出是 800x480,所以要按比例缩放。也有些触摸驱动会在内核里直接校准好,上报的就是屏幕坐标,此时再做一次缩放反而会错位。

#define LCD_X 800 #define LCD_Y 480 #define TS_X_MAX 1024 #define TS_Y_MAX 600 int ts_get_pos(int *lcd_x, int *lcd_y) { int raw_x, raw_y; if (ts_read(&raw_x, &raw_y) < 0) return -1; *lcd_x = raw_x * LCD_X / TS_X_MAX; *lcd_y = raw_y * LCD_Y / TS_Y_MAX; return 0; }

这个线性映射只解决缩放,不解决触摸屏反向的问题。如果左右相反,把TS_X_MAX改成TS_X_MAX - raw_x再做映射;如果上下颠倒,对 y 做同样处理。判断方向有个笨办法:在屏幕上画两个对角点,分别点一下,看触摸点和显示点是否落在同一位置。项目里的ts.c并没有做热插拔处理,触摸屏文件节点固定为/dev/input/event0,如果你用的是 USB 触摸屏,节点可能变成event2,打开失败时用ls /dev/input/确认。

3.3 按钮区域划分与防抖策略

界面底部放了六个触控区域:播放/暂停、上一首、下一首、开灯、关灯,外加一个预留区域。每个区域本质是一个矩形范围,坐标映射后直接判断落在哪个矩形里。

typedef enum { BTN_PLAY_PAUSE = 1, BTN_PREV, BTN_NEXT, BTN_LED_ON, BTN_LED_OFF, BTN_NONE } btn_id_t; btn_id_t map_button(int x, int y) { if (y < 350 || y > 450) return BTN_NONE; if (x >= 30 && x < 140) return BTN_PLAY_PAUSE; if (x >= 160 && x < 270) return BTN_PREV; if (x >= 290 && x < 400) return BTN_NEXT; if (x >= 420 && x < 530) return BTN_LED_ON; if (x >= 550 && x < 660) return BTN_LED_OFF; return BTN_NONE; }

按键区域之间留了 20 像素的间隔,目的是避免相邻按钮误触。实际使用中还要加时间戳防抖,同一坐标区域 200 毫秒内的第二次触摸不处理,否则一次按压可能会触发两次事件。处理逻辑里先读gettimeofday比较差值,小于 200ms 直接丢弃。这个防抖参数不能太小,电阻屏按下瞬间经常会同时上报两三个坐标点。

4. GY-39 数据帧解析与 PWM/GPIO 驱动点灯

4.1 GY-39 传感器的通讯方式与设备地址

GY-39 是一块多合一环境传感器,支持 I2C 和串口两种模式。工程里按键切换、界面刷新都在主循环里跑,用 I2C 读取最稳定,不占串口调试口。在嵌入式 Linux 下访问 I2C 设备,标准做法是打开/dev/i2c-N,用ioctl设置从机地址,然后write寄存器地址、read读回数据。

#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <linux/i2c-dev.h> #define GY39_ADDR 0x76 int read_gy39(float *temp, float *humi, float *light, int *smoke) { int fd = open("/dev/i2c-1", O_RDWR); if (fd < 0) return -1; if (ioctl(fd, I2C_SLAVE, GY39_ADDR) < 0) { close(fd); return -1; } unsigned char reg = 0x00; unsigned char buf[8]; if (write(fd, &reg, 1) != 1) { close(fd); return -1; } if (read(fd, buf, 8) != 8) { close(fd); return -1; } *temp = (short)((buf[0] << 8) | buf[1]) / 100.0f; *humi = (short)((buf[2] << 8) | buf[3]) / 100.0f; *light = (buf[4] << 8) | buf[5]; *smoke = (buf[6] << 8) | buf[7]; close(fd); return 0; }

GY-39 的 I2C 从机地址受硬件引脚电平影响,常见是0x76,也有部分模块是0x69。上电后可以用 busybox 的i2cdetect扫描确认,执行i2cdetect -y 0i2cdetect -y 1,看哪条总线上出现了设备地址。数据帧里温度、湿度都按 100 倍放大存放,所以读出来要转成 short 再除以 100;光照和烟雾是 ADC 直接转换值,是否需要线性换算看具体传感器固件。我实际调试时被这个模块坑过一次:从寄存器读回来的数据全是0xFF,最后发现是模块供电接了 5V,GY-39 的 I2C 电平是 3.3V,换电源后数据正常。

字节偏移含义原始数据示例处理后结果
0-1温度0x0A 0x1C25.88°C
2-3湿度0x1F 0x4080.00%
4-5光照0x00 0x2E46
6-7烟雾/空气状态0x00 0x1A26

4.2 加载 pwm.ko 与 led_drv.ko 驱动模块

点灯功能在工程里并不只是 GPIO 拉高拉低,还需要用 PWM 控制亮度变化。项目提供的pwm.koled_drv.ko分别是 PWM 控制器驱动和 LED 字符设备驱动。驱动不是设备树自动加载的,必须手动insmod

insmod /mnt/udisk/pwm.ko insmod /mnt/udisk/led_drv.ko ls /dev/led /sys/class/pwm/pwmchip0

如果ls能看到/dev/led/sys/class/pwm/pwmchip0,说明模块加载成功。看不到时先执行dmesg | tail -20,最典型的问题是insmod: can't insert 'pwm.ko': Exec format error,这是内核版本与模块编译环境不匹配。GEC6818 出厂系统是 3.4.x 内核,必须用配套的 BSP 内核源码编译驱动,不能用通用交叉编译器随便编一个 .ko 塞进去。

4.3 通过 sysfs 调整 PWM 周期和占空比

PWM 驱动注册成功后,Linux 会在/sys/class/pwm/pwmchip0下导出控制节点。先 export 出通道,再分别写周期、占空比和使能。周期单位是纳秒,占空比也是纳秒,表示一个周期内高电平持续的时间。

文件节点写入示例作用
export0导出 PWM 通道 0
period1000000设置周期为 1ms,即 1kHz
duty_cycle500000设置占空比 50%
enable1使能 PWM 输出
echo 0 > /sys/class/pwm/pwmchip0/export echo 1000000 > /sys/class/pwm/pwmchip0/pwm0/period echo 500000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle echo 1 > /sys/class/pwm/pwmchip0/pwm0/enable

占空比设为period的一半,LED 亮度就是 50%。如果想要呼吸灯效果,程序里循环递增duty_cycle,每步加上一个延时,就能看到渐亮渐灭。如果 LED 接在普通 GPIO 而不是 PWM 输出脚,走/dev/led更直接:

void led_set(int on) { system(on ? "echo 1 > /dev/led 2>/dev/null" : "echo 0 > /dev/led 2>/dev/null"); }

2>/dev/null不能省。板子上串口终端会把write失败的错误信息打印出来,污染 LCD 界面的同时还会让系统调用卡顿。用system调 shell 虽然看起来不够优雅,但在这种小工程里比直接open("/dev/led")更省事,也方便在调试时手动切换。

5. madplay 音频控制与交叉编译部署实操

5.1 播放器的选择与运行依赖

GEC6818 上播放 MP3,最常见方案是用madplay。它依赖libmad解码库,有些出厂根文件系统自带,有些没有。先执行madplay test.mp3确认能不能跑,如果提示madplay: not found,要用交叉编译出的madplaylibmad.so.0放到板子同一目录,并设置动态库路径:

export LD_LIBRARY_PATH=/mnt/udisk:$LD_LIBRARY_PATH

5.2 进程级播放控制:暂停与切歌

工程要支持播放、暂停、上一首、下一首,做法是让madplay在后台跑,通过信号和进程名控制它。

void music_play(int index) { char cmd[128]; system("killall madplay 2>/dev/null"); snprintf(cmd, sizeof(cmd), "madplay /mnt/udisk/%d.mp3 &", index); system(cmd); } void music_pause(void) { system("kill -STOP $(pidof madplay) 2>/dev/null"); } void music_resume(void) { system("kill -CONT $(pidof madplay) 2>/dev/null"); }

切歌前先killall madplay是为了避免两个解码进程同时占用声卡。暂停用SIGSTOP信号冻结进程,恢复用SIGCONT,这个方案比杀掉重启快得多,也不会有爆音。pidof madplay拿进程号而不是自己保存,是因为system启动的 madplay 可能被 shell 包装一层,自己记忆的 pid 容易失效。

5.3 交叉编译命令与常见故障

工程所有源码都在一个目录里,编译命令比较直接:

arm-linux-gnueabihf-gcc -o main main.c lcd.c ts.c drv.c bmp.c zimo.c cgq.c -lm

如果触摸或者传感器代码里用了多线程刷新,再加-lpthread。生成的 main 是 ARM 架构可执行文件,可以在 Ubuntu 主机上用file main确认,会输出ARM字样。部署时用 NFS 最方便:

mount -t nfs -o nolock 192.168.1.100:/home/share /mnt/nfs cd /mnt/nfs ./main

触摸没反应先ls /dev/input/看 event 节点编号;LCD 花屏检查dmesg里 framebuffer 的分辨率打印;播不了歌直接命令行跑madplay /mnt/udisk/1.mp3看解码器报错。如果 mp3 是 44.1kHz 双声道以外的高规格音频,用ffmpeg -i in.mp3 -ar 44100 -ac 2 -b:a 128k out.mp3转一下再放进板子。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询