mem 设备控制 GPIO - C程序通过sysfs文件系统使用GPIO中断
提示:通过 mem 设备控制 GPIO 知识点;使用C程序通过sysfs文件系统使用GPIO中断
文章目录
- 前言
- 参考资料
- 一、通过 mem 设备控制 GPIO
- /dev/mem 设备 知识点
- 控制源码程序
- map_base 函数分析
- 参数详解
- addr - 映射起始地址
- length - 映射区域长度
- prot - 内存保护标志
- flags - 映射类型和选项
- fd - 文件描述符
- offset - 文件偏移量
- mem 控制业务分析和步骤
- 控制GPIO方案 mmap-sysfs-字符设备 比较
- 二、使用C程序通过sysfs文件系统使用GPIO中断
- 控制源码程序
- 基础知识
- memset 结构体
- pollfd 结构体
- 结构体定义
- 事件标志说明
- 基本使用
- 监听中断流程
- gpio_interrupt 监听中断分析
- 为什么需要调用read()
- 配置硬件测试环境
- 总结
前言
通过io 操作实现点亮LED灯操作。 但是IO操作的都是寄存器地址,所以先搞清楚寄存器知识点。
参考资料
GPIO 控制和操作-使用命令通过sysfs文件系统控制GPIO
RK3568驱动指南|第十二篇 GPIO子系统-第129章 GPIO控制和操作实验
使用C程序通过sysfs文件系统控制gpio
rk3588写入寄存器
linux IO指令 读写GPIO口电平实例
操作寄存器来控制GPIO-点亮LED灯
- 这里目标是LED灯点亮实验,在寄存器控制GPIO基础上进一步底层抽象,用mem设备来控制GPIO
- 在C程序控制sysfs文件系统控制GPIO基础上,进一步了解、掌握 C程序通过sysfs文件系统,使用GPIO中断
这本身是两个知识点,但都很重要,有必要进一步理解学习。
一、通过 mem 设备控制 GPIO
/dev/mem 设备 知识点
/dev/mem 是一个特殊的字符设备文件,它提供了对整个物理内存的直接访问。通过这个设备,用户空间程序可以读写任意的物理内存地址。
-
在某些情况下, 我们可能无法直接使用 IO 命令来访问 GPIO 寄存器, 或者希望使用更高级的抽象来控制硬件。 这时, 可以使用/dev/mem 设备来操作物理内存, 以实现对 GPIO
寄存器的访问。 -
通过打开/dev/mem 设备文件, 并将其映射到用户空间的内存中, 我们可以直接读写物理内存地址, 从而实现对 GPIO 寄存器的控制。 这种方法相对于 IO 命令更加灵活, 可以使用更高级的编程语言(如 C/C++) 来编写控制逻辑。
控制源码程序
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>#define GPIO_REG_BASE 0xFDD60000 // GPIO base address
#define GPIO_SWPORT_DDR_L_OFFSET 0x0008 // DDR direction mode offset
#define GPIO_SWPORT_DR_L_OFFSET 0x0000 // DR direction mode offset
#define SIZE_MAP 0x1000// 打开LED灯
void LED_ON(unsigned char *base)
{// 设置LED灯的方向为输出*(volatile unsigned int *)(base + GPIO_SWPORT_DDR_L_OFFSET) = 0x80008044;// 将LED灯打开*(volatile unsigned int *)(base + GPIO_SWPORT_DR_L_OFFSET) = 0x80008044;
}// 关闭LED灯
void LED_OFF(unsigned char *base)
{// 设置LED灯的方向为输出*(volatile unsigned int *)(base + GPIO_SWPORT_DDR_L_OFFSET) = 0x80008044;// 将LED灯关闭*(volatile unsigned int *)(base + GPIO_SWPORT_DR_L_OFFSET) = 0x80000044;
}int main(int argc, char *argv[])
{int fd;unsigned char *map_base;// 打开/dev/mem设备fd = open("/dev/mem", O_RDWR);if (fd < 0){printf("open /dev/mem error \n");return -1;}// 将物理地址映射到用户空间map_base = (unsigned char *)mmap(NULL, SIZE_MAP, PROT_READ | PROT_WRITE, MAP_SHARED, fd, GPIO_REG_BASE);if (map_base == MAP_FAILED){printf("map_base error \n");return -2;}while (1){// 打开LED灯LED_ON(map_base);// 等待1秒usleep(1000000);// 关闭LED灯LED_OFF(map_base);// 等待1秒usleep(1000000);}// 解除映射munmap(map_base, SIZE_MAP);// 关闭文件描述符close(fd);return 0; // 返回0表示程序正常退出
}
将 控制程序编译成可执行文件,然后拷贝到开发板上,执行即可实现LED的亮灭闪烁:
./gpioctrl_by_mem 15
map_base 函数分析
mmap(memory map)函数用于将设备内存或文件映射到进程的地址空间,从而允许应用程序直接通过内存访问来操作硬件寄存器。
参数详解
addr - 映射起始地址
-
建议设为 NULL,由内核自动选择合适地址
-
如果指定地址,通常需要页对齐(sysconf(_SC_PAGE_SIZE))
-
非 NULL 时,内核会将其作为提示,但不保证使用该地址
length - 映射区域长度
-
映射的字节数
-
实际会向上取整到系统的页大小整数倍
-
可以通过 sysconf(_SC_PAGE_SIZE) 获取页大小
prot - 内存保护标志
控制对映射内存的访问权限,可以组合使用:
标志 | 说明 | 值 |
---|---|---|
PROT_READ | 页面可读 | 0x1 |
PROT_WRITE | 页面可写 | 0x2 |
PROT_EXEC | 页面可执行 | 0x4 |
PROT_NONE | 页面不可访问 | 0x0 |
常用组合:
PROT_READ | PROT_WRITE // 可读写
PROT_READ // 只读
PROT_READ | PROT_EXEC // 可读可执行
flags - 映射类型和选项
控制映射的行为和特性:
映射类型(必选其一):
-
MAP_SHARED:共享映射,修改对其他进程可见,会写回文件
-
MAP_PRIVATE:私有映射,修改不会写回文件,采用写时复制
其他选项(可选):
-
MAP_FIXED:必须使用指定的 addr,如果不可用则失败
-
MAP_ANONYMOUS:创建匿名映射,不关联文件(此时 fd 应为 -1)
-
MAP_LOCKED:锁定页面在内存中,防止被换出
-
MAP_NORESERVE:不为交换空间预留空间
fd - 文件描述符
- 要映射的文件描述符
- 对于匿名映射,设为 -1
offset - 文件偏移量
- 从文件开头算起的偏移量
- 必须是系统页大小的整数倍
mem 控制业务分析和步骤
如上对于mem源码控制代码中,核心流程其实就是通过寄存器直接控制的程序代码化
- 假使已经实现类 IO口的GPIO复用,那么第一步 mmap 函数实现 将物理地址映射到用户空间
- 提前通过操作手册,找到 GPIO方向GPIO_SWPORT_DDR_L_OFFSET 和 GPIO 高低电平的 GPIO_SWPORT_DR_L_OFFSET 偏移量
- 最后通过 映射mmap函数拿到基地址到用户空间,然后用户空间拼接方向模式、高低电平具体的值,赋值实际控制的值即可 立刻同步到内核空间,进而通过mem 机制实现了控制GPIO功能
控制GPIO方案 mmap-sysfs-字符设备 比较
替代方案比较
方法 | 性能 | 复杂度 | 灵活性 | 适用场景 |
---|---|---|---|---|
mmap | 高 | 中 | 高 | 高性能、实时控制 |
sysfs | 低 | 低 | 低 | 简单应用、调试 |
字符设备 | 中 | 中 | 中 | 生产环境 |
通过 mmap 直接映射 GPIO 寄存器内存,可以实现最高性能的 GPIO 控制,特别适合需要快速响应和精确时序控制的嵌入式应用。
二、使用C程序通过sysfs文件系统使用GPIO中断
通过 GPIO 的输入中断程序, 将中断触发方式设置为边沿触发, 每当触发中断会打印 value的值。
在使用C程序通过sysfs文件系统控制gpio 篇中其实已经了解过 通过C程序控制 GPIO。
这里其实就是通过物理硬件环境模拟中断,然后程序能够检测到。
如何检测:就是通过poll机制,检测。
控制源码程序
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <poll.h>int fd; // 文件描述符
int ret; // 返回值
char gpio_path[100]; // GPIO路径
int len; // 字符串长度
char file_path[100]; // 文件路径
char buf[2]; // 缓冲区struct pollfd fds[1]; // poll结构体数组// 导出GPIO引脚
int gpio_export(char *argv)
{fd = open("/sys/class/gpio/export", O_WRONLY); // 打开export文件if (fd < 0){printf("open /sys/class/gpio/export error \n"); // 打开文件失败return -1;}len = strlen(argv); // 获取字符串长度ret = write(fd, argv, len); // 写入引脚号到export文件if (ret < 0){printf("write /sys/class/gpio/export error \n"); // 写入失败return -2;}close(fd); // 关闭文件
}// 取消导出GPIO引脚
int gpio_unexport(char *argv)
{fd = open("/sys/class/gpio/unexport", O_WRONLY); // 打开unexport文件if (fd < 0){printf("open /sys/class/gpio/unexport error \n"); // 打开文件失败return -1;}len = strlen(argv); // 获取字符串长度ret = write(fd, argv, len); // 写入引脚号到unexport文件if (ret < 0){printf("write /sys/class/gpio/unexport error \n"); // 写入失败return -2;}close(fd); // 关闭文件
}// 控制GPIO引脚的属性
int gpio_ctrl(char *arg, char *val)
{sprintf(file_path, "%s/%s", gpio_path, arg); // 构建属性文件的路径fd = open(file_path, O_WRONLY); // 打开属性文件if (fd < 0){printf("open file_path error \n"); // 打开文件失败return -1;}len = strlen(val); // 获取字符串长度ret = write(fd, val, len); // 写入属性值到属性文件if (ret < 0){printf("write file_path error\n"); // 写入失败return -2;}close(fd); // 关闭文件
}// 监听GPIO引脚的中断事件
int gpio_interrupt(char *arg)
{sprintf(file_path, "%s/%s", gpio_path, arg); // 构建文件路径fd = open(file_path, O_RDONLY); // 打开文件if (fd < 0){printf("open file_path error \n"); // 打开文件失败return -1;}memset((void *)fds, 0, sizeof(fds)); // 清空poll结构体数组fds[0].fd = fd; // 设置poll结构体的文件描述符fds[0].events = POLLPRI; // 设置poll结构体的事件类型为POLLPRI,表示有紧急数据可读read(fd, buf, 2); // 读取文件内容,清除中断事件ret = poll(fds, 1, -1); // 调用poll函数等待中断事件发生,阻塞直到事件发生if (ret <= 0){printf("poll error \n"); // 调用poll失败或超时return -1;}if(fds[0].revents & POLLPRI){lseek(fd, 0, SEEK_SET); // 重新定位文件指针到文件开头read(fd, buf, 2); // 读取文件内容,获取中断事件的值buf[1] = '\0';printf("value is %s\n", buf); // 输出中断事件的值}
}// 读取GPIO引脚的值
int gpio_read_value(char *arg)
{sprintf(file_path, "%s/%s", gpio_path, arg); // 构建文件路径fd = open(file_path, O_WRONLY); // 打开文件,以只写模式打开是一个错误,应该使用只读模式if (fd < 0){printf("open file_path error\n"); // 打开文件失败return -1;}ret = read(fd, buf, 1); // 读取文件内容,获取引脚的值if (!strcmp(buf, "1")){printf("The value is high\n"); // 引脚值为高电平return 1;}else if (!strcmp(buf, "0")){printf("The value is low\n"); // 引脚值为低电平return 0;}return -1; // 这里应该返回读取到的引脚值(0或1),而不是返回固定的-1close(fd); // 关闭文件(这行代码无法执行到,应该放在read之前)
}int main(int argc, char *argv[]) // 主函数
{int value;sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]); // 构建GPIO路径if (access(gpio_path, F_OK)) // 检查GPIO路径是否存在{gpio_export(argv[1]); // 不存在则导出GPIO引脚}else{gpio_unexport(argv[1]); // 存在则取消导出GPIO引脚}gpio_ctrl("direction", "in"); // 设置GPIO引脚为输入模式gpio_ctrl("edge", "both"); // 设置GPIO引脚的中断触发方式为上升沿和下降沿gpio_interrupt("value"); // 监听GPIO引脚的中断事件gpio_unexport(argv[1]); // 最后取消导出GPIO引脚return 0; // 返回0表示程序正常退出
}
基础知识
memset 结构体
在 C/C++ 中,使用 memset 初始化结构体是一种常见的做法。以下是详细的使用方法和注意事项:
struct Person p1;// 将结构体所有字节设置为0memset(&p1, 0, sizeof(struct Person));
pollfd 结构体
结构体定义
#include <poll.h>struct pollfd {int fd; /* 文件描述符 */short events; /* 等待的事件 */short revents; /* 实际发生的事件 */
};
事件标志说明
事件标志 | 说明 | 方向 |
---|---|---|
POLLIN | 有数据可读 | 输入 |
POLLPRI | 有紧急数据可读 | 输入 |
POLLOUT | 可写,不会阻塞 | 输出 |
POLLRDHUP | 对端关闭连接 | 输入 |
POLLERR | 发生错误 | 输出 |
POLLHUP | 挂起 | 输出 |
POLLNVAL | 无效请求 | 输出 |
基本使用
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
#include <sys/socket.h>#define TIMEOUT_MS 5000 // 5秒超时int main() {struct pollfd fds[2];// 初始化 pollfd 结构体memset(fds, 0, sizeof(fds));// 监视标准输入 (fd = 0)fds[0].fd = STDIN_FILENO;fds[0].events = POLLIN;// 假设有另一个文件描述符fds[1].fd = -1; // 无效的文件描述符fds[1].events = POLLIN;printf("等待输入(5秒超时)...\n");int ret = poll(fds, 2, TIMEOUT_MS);if (ret == -1) {perror("poll");return 1;} else if (ret == 0) {printf("超时!\n");} else {// 检查哪些文件描述符就绪if (fds[0].revents & POLLIN) {printf("标准输入有数据可读\n");}if (fds[1].revents & POLLIN) {printf("文件描述符 %d 有数据可读\n", fds[1].fd);}// 检查错误条件if (fds[0].revents & POLLERR) {printf("标准输入发生错误\n");}if (fds[1].revents & POLLNVAL) {printf("文件描述符 %d 无效\n", fds[1].fd);}}return 0;
}
监听中断流程
有了前面的知识点,其实代码业务流程很清晰的,如下:
- 导出GPIO gpio_export
- gpio_ctrl(“direction”, “in”); // 设置GPIO引脚为输入模式
- gpio_ctrl(“edge”, “both”); // 设置GPIO引脚的中断触发方式为上升沿和下降沿
- gpio_interrupt(“value”); // 监听GPIO引脚的中断事件
在上面分析 pollfd 后,我们再仔细看看我们程序里面的监听中断到底如何试下的,如下:
gpio_interrupt 监听中断分析
int gpio_interrupt(char *arg)
{sprintf(file_path, "%s/%s", gpio_path, arg); // 构建文件路径fd = open(file_path, O_RDONLY); // 打开文件if (fd < 0){printf("open file_path error \n"); // 打开文件失败return -1;}memset((void *)fds, 0, sizeof(fds)); // 清空poll结构体数组fds[0].fd = fd; // 设置poll结构体的文件描述符fds[0].events = POLLPRI; // 设置poll结构体的事件类型为POLLPRI,表示有紧急数据可读read(fd, buf, 2); // 读取文件内容,清除中断事件ret = poll(fds, 1, -1); // 调用poll函数等待中断事件发生,阻塞直到事件发生if (ret <= 0){printf("poll error \n"); // 调用poll失败或超时return -1;}if(fds[0].revents & POLLPRI){lseek(fd, 0, SEEK_SET); // 重新定位文件指针到文件开头read(fd, buf, 2); // 读取文件内容,获取中断事件的值buf[1] = '\0';printf("value is %s\n", buf); // 输出中断事件的值}
}
- 打开节点路径 open,获取到文件描述符 fd
- 通过memset 方法,每次运行时候清空pollfd 结构体
- read 消费一次中断
- poll(fds, 1, -1); // 调用poll函数等待中断事件发生,阻塞直到事件发生
- 阻塞,等待返回结果,直到有中断产生。
为什么需要调用read()
// 如果不调用read(),会发生什么?
// poll()会立即返回,因为中断事件没有被消费
// 导致程序进入忙等待状态
配置硬件测试环境
当可执行文件执行后 ./gpioctrl 42& ,模拟实际测试环境:
由于中断并没有被触发, 所以程序会阻塞, 等待中断的进行, 然后使用杜邦线的另一端将GPIO 底座的 3.3V 接到 GPIO1_PB2 pin 脚, 进行中断的测试
总结
- 了解 mem 控制GPIO的 这种机制
- C程序如何监听GPIO中断