一、实验目的

  1. 掌握嵌入式 Linux 系统下 GPIO(通用输入输出)的控制方法。
  2. 理解通过 /dev/mem + mmap 直写物理寄存器的工作原理,绕过 sysfs 限制。
  3. 学习按键消抖、下拉沿检测等输入信号处理方法。
  4. 实现外部按键与主板 LED 的联动控制:面包板按键按下 → 主板 LED 明暗翻转。
  5. 培养嵌入式系统软硬件协同调试能力。

二、实验设备

设备 型号/说明
嵌入式开发主板 迅为 iTOP-4412(精英版),Exynos 4412 ARM Cortex-A9 四核
外部按键模块 面包板 + 微动开关 ×2 + 杜邦线
SD 卡 8GB TF 卡,存放编译好的可执行文件
调试工具 Xshell 8(串口终端,波特率 115200)
开发主机 Windows 11,安装 arm-none-linux-gnueabihf 交叉编译工具链

三、实验原理

3.1 GPIO 控制方式对比

本实验板内核未编译 CONFIG_GPIO_SYSFS/sys/class/gpio/ 目录不存在),无法使用传统的 sysfs 应用层控制方式。因此采用 /dev/mem + mmap 寄存器直写方案:

1
2
3
4
5
6
7
用户态程序

└─ open("/dev/mem") → mmap(物理地址 0x11000000)

└─ 直接读写 GPIO 寄存器 (GPX1CON, GPK1DAT, GPL2DAT 等)

└─ Exynos4412 SoC 引脚 → 物理引脚电平变化

3.2 寄存器地址映射

Exynos4412 的 GPIO 寄存器物理基址为 0x11000000(4KB 页面内),本实验涉及的寄存器:

寄存器 物理地址 功能
GPX1CON 0x1100_0C20 GPX1 配置寄存器(输入/输出模式选择)
GPX1DAT 0x1100_0C24 GPX1 数据寄存器(读取引脚电平)
GPX1PUD 0x1100_0C28 GPX1 上下拉电阻配置寄存器
GPL2CON 0x1100_0260 GPL2 配置寄存器
GPL2DAT 0x1100_0264 GPL2 数据寄存器(设置 LED2 输出电平)
GPK1CON 0x1100_0060 GPK1 配置寄存器
GPK1DAT 0x1100_0064 GPK1 数据寄存器(设置 LED3 输出电平)

3.3 引脚分配

功能 丝印名称 GPIO 编号 对应寄存器位
外部按键1(面包板) GPX1_4 gpio-148 GPX1DAT[4]
外部按键2(面包板) GPX1_3 gpio-147 GPX1DAT[3]
主板 LED2(底板左侧) GPL2_0 gpio-126 GPL2DAT[0]
主板 LED3(底板右侧) GPK1_1 gpio-90 GPK1DAT[1]

3.4 按键检测与消抖

1
2
3
4
5
6
7
8
GPX1DAT 读取 ──→ 高电平(1) = 未按下
──→ 低电平(0) = 按下(下降沿)

├── 延时 15ms ──→ 再读一次确认(消抖)

├── 确认按下 → 翻转 LED 状态

└── 等待释放(防连发)

3.5 面包板硬件接线

1
2
3
4
5
6
7
8
9
10
iTOP-4412 底板 20pin 扩展排针              面包板
┌────────────────────────┐ ┌──────────────┐
│ GPX1_4 (按键1) │─── 杜邦线 ──→ │ 按键1 引脚A │
│ GPX1_3 (按键2) │─── 杜邦线 ──→ │ 按键2 引脚A │
│ GND │──┬ 杜邦线 ──→ │ 按键1 引脚C │
│ │ └ 杜邦线 ──→ │ 按键2 引脚C │
└────────────────────────┘ └──────────────┘

主板 LED 在底板自带,无需面包板外接。
按键按下时 GPX1 引脚接地 → 电平拉低 → 检测到下降沿。

四、实验步骤

4.1 硬件接线

  1. 将两个微动开关插入面包板。
  2. 用杜邦线将面包板按键1的引脚A连接到开发板 20pin 扩展口的 GPX1_4 排针。
  3. 用杜邦线将面包板按键2的引脚A连接到开发板 20pin 扩展口的 GPX1_3 排针。
  4. 将两个按键的引脚C共地,接到扩展口的 GND 排针。

4.2 软件开发

核心程序 led_button.c 工作流程:

  1. open("/dev/mem") — 打开物理内存设备文件
  2. mmap() — 将 GPIO 物理地址 0x11000000 映射到用户态虚拟地址空间
  3. 配置 GPX1 — 设置按键引脚为输入模式 + 开启内部上拉电阻
  4. 配置 GPL2/GPK1 — 设置 LED 引脚为输出模式
  5. 主循环轮询 — 检测按键按下 → 消抖确认 → 翻转 LED → 打印日志
  6. SIGINT 信号处理 — Ctrl+C 时自动熄灭 LED、解除内存映射

4.3 交叉编译

在 Windows 开发机上:

1
arm-none-linux-gnueabihf-gcc -static -o led_button.exe led_button.c

使用 -static 静态链接确保在精简 Linux 上也能运行。

4.4 部署运行

1
2
3
4
# 开发板串口终端
cp /mnt/usb/led_button.exe /tmp/led_button.exe
chmod +x /tmp/led_button.exe
/tmp/led_button.exe

五、核心代码及注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/*
* iTOP-4412 外部按键控制主板LED - /dev/mem 直写寄存器方案
* 绕过 sysfs(该内核未编译 CONFIG_GPIO_SYSFS)
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <signal.h>
#include <stdint.h>

// Exynos4412 GPIO 物理基址(三个 GPIO 组都在同一 4KB 页面内)
#define GPIO_BASE 0x11000000
#define GPIO_SIZE 0x1000

// 寄存器偏移(除以4是因为用 uint32_t 数组下标访问)
#define GPK1CON (0x0060 / 4) // GPK1 配置寄存器
#define GPK1DAT (0x0064 / 4) // GPK1 数据寄存器
#define GPL2CON (0x0260 / 4) // GPL2 配置寄存器
#define GPL2DAT (0x0264 / 4) // GPL2 数据寄存器
#define GPX1CON (0x0C20 / 4) // GPX1 配置寄存器
#define GPX1DAT (0x0C24 / 4) // GPX1 数据寄存器
#define GPX1PUD (0x0C28 / 4) // GPX1 上下拉寄存器

// 引脚位掩码
#define BTN1_MASK (1 << 4) // GPX1_4 = 按键1
#define BTN2_MASK (1 << 3) // GPX1_3 = 按键2
#define LED2_MASK (1 << 0) // GPL2_0 = LED2
#define LED3_MASK (1 << 1) // GPK1_1 = LED3

static volatile uint32_t *gpio; // mmap 映射后的虚拟地址
static int led2_state = 0, led3_state = 0;

// 按键检测(带硬件消抖)
int button_pressed(uint32_t mask) {
// 读取数据寄存器,检查对应位是否拉低(按下 = 接地 = 0)
if (gpio[GPX1DAT] & mask) return 0;
usleep(15000); // 15ms 消抖等待
if (gpio[GPX1DAT] & mask) return 0; // 不是稳定低电平 = 抖动
while (!(gpio[GPX1DAT] & mask)) // 等待按键释放
usleep(10000);
return 1;
}

int main() {
int mem_fd = open("/dev/mem", O_RDWR | O_SYNC);
gpio = (volatile uint32_t *)mmap(NULL, GPIO_SIZE,
PROT_READ | PROT_WRITE, MAP_SHARED, mem_fd, GPIO_BASE);
close(mem_fd);

// 配置按键引脚:输入模式 + 上拉
uint32_t val = gpio[GPX1CON];
val &= ~((0xF << 16) | (0xF << 12)); // 清除 GPX1_4 和 GPX1_3 的配置位
gpio[GPX1CON] = val; // 所有位为 0 = 输入模式
val = gpio[GPX1PUD];
val |= (0x3 << 8) | (0x3 << 6); // 开启内部上拉
gpio[GPX1PUD] = val;

// 配置 LED 引脚:输出模式,初始熄灭
gpio[GPL2CON] = (gpio[GPL2CON] & ~0xF) | 0x1;
gpio[GPK1CON] = (gpio[GPK1CON] & ~(0xF << 4)) | (0x1 << 4);
gpio[GPL2DAT] &= ~LED2_MASK;
gpio[GPK1DAT] &= ~LED3_MASK;

signal(SIGINT, cleanup); // Ctrl+C 自动清理

// 主循环:5ms 轮询
while (1) {
if (button_pressed(BTN1_MASK)) {
led2_state = !led2_state;
if (led2_state) gpio[GPL2DAT] |= LED2_MASK;
else gpio[GPL2DAT] &= ~LED2_MASK;
}
if (button_pressed(BTN2_MASK)) {
led3_state = !led3_state;
if (led3_state) gpio[GPK1DAT] |= LED3_MASK;
else gpio[GPK1DAT] &= ~LED3_MASK;
}
usleep(5000);
}
}

六、实验结果

测试项 预期结果 实际结果
上电后 LED 初始状态 LED2、LED3 均熄灭 ✅ 正确
按下按键1 LED2 翻转(灭→亮) ✅ 正确
再次按下按键1 LED2 翻转(亮→灭) ✅ 正确
按下按键2 LED3 翻转(灭→亮) ✅ 正确
再次按下按键2 LED3 翻转(亮→灭) ✅ 正确
Ctrl+C 退出 两个 LED 自动熄灭,程序退出 ✅ 正确
按键消抖 按一次只触发一次翻转,不连发 ✅ 正确

七、踩坑记录与经验总结

7.1 sysfs 不可用

内核未编译 CONFIG_GPIO_SYSFS,尝试 echo 148 > /sys/class/gpio/export 失败,报 nonexistent directory解决:改用 /dev/mem + mmap 直写物理寄存器。

7.2 SD 卡反复掉线

SD 卡插入后频繁出现 Unable to re-detect card (-123),但文件系统仍能短暂访问。解决:在卡稳定时快速执行 cp 操作,或将常用文件放到 /tmp

7.3 CP 目标路径错误

cp /mnt/usb/led_button.exe /root/Is a directory教训:BusyBox 的 cp 必须写完整目标文件名 cp src/xxx /dst/xxx

7.4 寄存器地址计算

Exynos4412 数据手册中 GPIO 寄存器地址需要精确计算。CON 寄存器每个引脚占 4 bits,DAT 寄存器每个引脚占 1 bit,PUD 寄存器每个引脚占 2 bits。配置时需要先读取当前值,用掩码清除目标位后再写入新值,避免误改其他引脚配置。

7.5 按键消抖

机械按键按下时会有 5~20ms 的抖动期。本实验采用 15ms 延时 + 二次确认 的软件消抖策略,有效防止了误触发。同时增加了”等待释放”逻辑,避免按一次触发多次翻转。

八、实验拓展

基于本实验的寄存器直写方法,可以拓展实现以下功能:

  1. PWM 控制 LED 亮度:通过定时器 / 软件 PWM 实现 LED 呼吸灯效果
  2. LCD 屏显联动:按键状态和 LED 状态实时显示在 LCD 屏幕上
  3. 中断驱动替代轮询:使用 request_irq 注册中断处理函数,降低 CPU 占用
  4. 内核驱动模块:将逻辑封装为标准的字符设备驱动(/dev/led_btn),供多个应用调用

实验日期:2026年6月17日

开发板:iTOP-4412(Exynos 4412)

工具链:arm-none-linux-gnueabihf-gcc 14.2