基于I.MX6ULL-MINI开发板
开发板中所有的设备(对象)都会在/sys/devices
体现出来,是 sysfs 文件系统中最重要的目录结构
/sys下的子目录 | 说明 |
---|---|
/sys/devices | 这是系统中所有设备存放的目录,也就是系统中的所有设备在 sysfs 中的呈现、表达,也是 sysfs 管理设备的最重要的目录结构。 |
/sys/block | 块设备的存放目录,这是一个过时的接口,按照 sysfs 的设计理念,系统所有的设备都存放在/sys/devices 目录下,所以/sys/block 目录下的文件通常是链接到/sys/devices 目录下的文件。 |
/sys/bus | 这是系统中的所有设备按照总线类型分类放置的目录结构,/sys/devices 目录下每一种设备都是挂在某种总线下的,譬如 i2c 设备挂在 I2C 总线下。同样,/sys/bus 目录下的文件通常也是链接到了/sys/devices 目录。 |
/sys/class | 这是系统中的所有设备按照其功能分类放置的目录结构,同样该目录下的文件也是链接到了/sys/devices 目录。按照设备的功能划分组织在/sys/class 目录下,譬如/sys/class/leds目录中存放了所有的 LED 设备,/sys/class/input 目录中存放了所有的输入类设备。 |
/sys/dev | 这是按照设备号的方式放置的目录结构,同样该目录下的文件也是链接到了/sys/devices 目录。该目录下有很多以主设备号:次设备号(major:minor)命名的文件,这些文件都是链接文件,链接到/sys/devices 目录下对应的设备。 |
/sys/firmware | 描述了内核中的固件。 |
/sys/fs | 用于描述系统中所有文件系统,包括文件系统本身和按文件系统分类存放的已挂载点。 |
/sys/kernel | 这里是内核中所有可调参数的位置。 |
/sys/module | 这里有系统中所有模块的信息。 |
/sys/power | 这里是系统中电源选项,有一些属性可以用于控制整个系统的电源状态。 |
应用层想要对底层硬件进行操控,通常可以通过两种方式:
- /dev/目录下的设备文件(设备节点);
- /sys/目录下设备的属性文件。
LED学习
led设备路径
bash
/sys/class/leds/sys-led

brightness:LED亮度,可读可写,0亮,非0灭
max_brightness:LED最大亮度,只读
trigger:触发模式,可读可写,模式如下:
none(无触发)、mmc0(当对 mmc0 设备发起读写操作的时候 LED 会闪烁)、timer(LED 会有规律的一亮一灭,被定时器控制住)、heartbeat(心跳呼吸模式,LED 模仿人的心跳呼吸那样亮灭变化)。
代码
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define LED_TRIGGER "/sys/class/leds/sys-led/trigger"
#define LED_BRIGHTNESS "/sys/class/leds/sys-led/brightness"
/*
在 C 语言中,反斜杠 \ 的作用是 行连接符(line continuation character)。
它允许你在多个行中编写一条连续的语句,而不需要写成一行。
具体来说,反斜杠告诉编译器:下一行是当前行的延续,继续合并在一起,而不加上额外的换行符。
*/
#define USAGE() fprintf(stderr,"usage:\n" \
" %s<on|off>\n" \
" %s<trigger><type>\n",argv[0],argv[0]) //argv[0]存放程序的名称或路径
int main(int argc, char *argv[])//argc参数个数,argv[]参数
{
int fd1, fd2;
//校验传参
if(2>argc){
USAGE();
return 1;
}
// 打开sysfs中的trigger和brightness文件
fd1 = open(LED_TRIGGER, O_RDWR);
if (0 > fd1) {
perror("open LED_TRIGGER error");
return 1;
}
fd2 = open(LED_BRIGHTNESS, O_RDWR);
if (0 > fd2) {
perror("open LED_BRIGHTNESS error");
close(fd1);
return 1;
}
//根据传参控制LED
if(!strcmp(argv[1],"on")){//判断传入的第一个参数是否是on
write(fd1,"none",4);//往trigger写入none,共4个字节,trigger设置为无触发
write(fd2,"1",1);//点亮LED
}
else if(!strcmp(argv[1],"off")){//判断传入的第一个参数是否是off
write(fd1,"none",4);
write(fd2,"0",1);//熄灭LED
}
else if(!strcmp(argv[1],"trigger")){//如果传入的第一个参数是trigger但是参数个数不是三个,提示用法
if(3!=argc){
USAGE();
return 1;
}
if(0 > write(fd1,argv[2],strlen(argv[2]))){//判断写入是否正确,将第三个参数即触发模式传给fd1 (none、mmc0、timer、heartbeat)
perror("write error");
return 1;
}
}
else
USAGE();
return 0;
}

GPIO应用编程
GPIO目录
bash
/sys/class/gpio
一共包含了 5 个 GPIO控制器,分别为 GPIO1、GPIO2、GPIO3、GPIO4、GPIO5,在这里分别对应 gpiochip0、gpiochip32、gpiochip64、gpiochip96、gpiochip128 这 5 个文件夹,每一个 gpiochipX 文件夹用来管理一组 GPIO

每一个gpiochipX文件夹里面有以下内容

base:与 gpiochipX 中的 X 相同,表示该控制器所管理的这组 GPIO 引脚中最小的编号,如
gpiochip32
的base就是32label:该组 GPIO 对应的标签,也就是名字
ngpio:该控制器所管理的 GPIO 引脚的数量,引脚编号范围是:base ~ base+ngpio-1
GPIO5_IO10在sysfs中对应的编号:128+10=138
export
export:用于将指定编号的 GPIO 引脚导出。在使用 GPIO 引脚之前,需要将其导出,导出成功之后才能使用它。注意 export 文件是只写文件,不能读取,将一个指定的编号写入到 export 文件中即可将对应的 GPIO 引脚导出
如下图,这个文件夹就是导出来的 GPIO 引脚对应的文件夹,用于管理、控制该 GPIO 引脚

unexport
unexport:将导出的 GPIO 引脚删除。当使用完 GPIO 引脚之后,我们需要将导出的引脚删除,同样该文件也是只写文件、不可读

gpioX文件夹里的文件
direction :配置 GPIO 引脚为输入或输出模式。该文件可读、可写,读表示查看 GPIO 当前是输入还是输出模式,写表示将 GPIO 配置为输入或输出模式;读取或写入操作可取的值为"out"(输出模式)和"in"(输入模式)
value :在 GPIO 配置为输出模式下,向 value 文件写入"0"控制 GPIO 引脚输出低电平,写入"1"则控制 GPIO 引脚输出高电平。在输入模式下,读取 value 文件获取 GPIO 引脚当前的输入电平状态。
active_low :这个属性文件用于控制极性,可读可写,默认情况下为 0,如果设置为1,则逻辑1为低电平,逻辑0为高电平
edge :控制中断的触发模式,该文件可读可写。在配置 GPIO 引脚的中断触发模式之前,需将其设置为输入模式
非中断引脚 :echo "none" > edge
上升沿触发 :echo "rising" > edge
下降沿触发 :echo "falling" > edge
边沿触发 :echo "both" > edge
输出代码
c
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
static char gpio_path[100];//存放gpio设备文件路径
//修改GPIO的状态
static int gpio_config(const char *attr, const char *value)//attr:修改的选项(direction、active_value、value)value:传入的值
{
char file_path[100];
int fd;
int len;
sprintf(file_path, "%s/%s", gpio_path, attr);//将/sys/class/gpio/gpiox/(direction或active_value或value)传给file_path
if(0 > (fd = open(file_path,O_WRONLY)))
{
perror("open error");
return fd;
}
len = strlen(value);
if(len!= write(fd,value,len))//向file_path写入value,即修改gpio的状态
{
perror("write error");
close(fd);
return 1;
}
close(fd);
return 0;
}
int main(int argc, char *argv[])
{
//判断传参是否正确
if(3!=argc)
{
fprintf(stderr,"usage: %s<gpio><value>\n", argv[0]);
return 1;
}
//判断gpiox文件是否导出
sprintf(gpio_path,"/sys/class/gpio/gpio%s",argv[1]);
if(access(gpio_path,F_OK))//判断路径是否存在,不存在则创建,F_OK参数用于判断文件是否存在
{
int fd;
int len;
if(0 > (fd = open("/sys/class/gpio/export",O_WRONLY)))//打开export文件并将路径存放在fd,准备写入
{
perror("open error");
return 1;
}
len = strlen(argv[1]);
if(len != write(fd,argv[1],len))//向export写入gpio号,即导出gpio文件
{
perror("write error");
close(fd);
return 1;
}
close(fd);//关闭文件
}
//修改gpio的状态
if(gpio_config("direction","out"))//配置为输出模式
{
return 1;
}
if(gpio_config("active_low","0"))//默认极性
{
return 1;
}
if(gpio_config("value",argv[2]))//控制GPIO高低电平
{
return 1;
}
//退出程序
return 0;
}
中断输入代码
c
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <poll.h>
static char gpio_path[100];//存放gpio设备文件路径
//修改GPIO的状态
static int gpio_config(const char *attr, const char *value)//attr:修改的选项(direction、active_low、value)value:传入的值
{
char file_path[100];
int fd;
int len;
sprintf(file_path, "%s/%s", gpio_path, attr);//将/sys/class/gpio/gpiox/(direction或active_low或value)传给file_path
if(0 > (fd = open(file_path,O_WRONLY)))
{
perror("open error");
return fd;
}
len = strlen(value);
if(len!= write(fd,value,len))//向file_path写入value,即修改gpio的状态
{
perror("write error");
close(fd);
return 1;
}
close(fd);
return 0;
}
int main(int argc, char *argv[])
{
struct pollfd pfd;//用于poll调用来监视文件描述符的事件,记得包含头文件poll.h
char file_path[100];//存储/sys/class/gpio/gpiox/value的路径
int ret; //poll函数的返回值
char value; //从GPIO引脚读取的值
//判断传参是否正确
if(2!=argc)
{
fprintf(stderr,"usage: %s<gpio> <value>\n", argv[0]);
return 1;
}
//判断gpiox文件是否导出
sprintf(gpio_path,"/sys/class/gpio/gpio%s",argv[1]);
if(access(gpio_path,F_OK))//判断路径是否存在,不存在则创建,F_OK参数用于判断文件是否存在
{
int fd;
int len;
if(0 > (fd = open("/sys/class/gpio/export",O_WRONLY)))//打开export文件并将路径存放在fd,准备写入
{
perror("open error");
return 1;
}
len = strlen(argv[1]);
if(len != write(fd,argv[1],len))//向export写入gpio号,即导出gpio文件
{
perror("write error");
return 1;
}
close(fd);//关闭文件
}
//修改gpio的状态
if(gpio_config("direction","in"))//配置为输入模式
{
return 1;
}
if(gpio_config("active_low","0"))//默认极性
{
return 1;
}
if(gpio_config("edge","both"))//设置中断触发方式,both是边沿触发
{
return 1;
}
//打开value属性文件
sprintf(file_path,"%s/%s",gpio_path,"value");
if(0 > (pfd.fd = open(file_path,O_RDONLY)))//用pfd.fd存储vlaue的文件路径
{
perror("open error");
return 1;
}
/*
调用poll
struct pollfd{
int fd; //文件描述符
short events; //等待的事件
short revents; //返回的事件
}
*/
pfd.events = POLLPRI;//只关心高优先级数据可读(中断)
read(pfd.fd,&value, 1);//在进入循环之前,先读取一次GPIO状态,以清除先前的状态
for( ; ; )
{
ret = poll(&pfd,1,-1);//调用poll,阻塞直到事件发生
if(0 > ret)
{
perror("poll error");
return 1;
}
else if(0 == ret)
{
fprintf(stderr,"poll timeout.\n");
continue;
}
//校验高优先级数据是否可读
if(pfd.revents & POLLPRI)
{
if(0 > lseek(pfd.fd,0,SEEK_SET))//使用lseek重置文件指针,确保每次读取最新的GPIO状态
{
perror("lseek error");
return 1;
}
if(0 > read(pfd.fd,&value,1))
{
perror("read error");
return 1;
}
printf("gpio中断触发<value=%c\n",value);
}
}
//退出程序
return 0;
}
poll函数复习
c
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
/*
fds:指向一个 struct pollfd 类型的数组,储存关心的文件描述符
nfds:fds数组中元素的个数
timeout:决定poll函数的阻塞行为
-1:一直阻塞,直到有信号来
0:不阻塞
>0:阻塞时间上限
*/
//struct pollfd 结构体
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 返回的事件 */
};

输入设备
读取输入设备时,应用程序打开输入设备对应的设备文件,向其发起读操作,每一次 read 操作获取的都是一个 structinput_event 结构体类型数据,该结构体定义在<linux/input.h>头文件中
c
struct input_event {
struct timeval time;
__u16 type; //描述发生了哪一种类型的时间
__u16 code; //该类事件具体是哪个事件,如键盘上不同的按键1、2、3...
__s32 value;
/*
内核每次上报事件都会向应用层发送一个数据 value,对 value 值的解释随着 code 的变化而变化。譬如对于按键事件(type=1)来说,如果 code=2(键盘上的数字键 1,也就是 KEY_1),那么如果 value 等于 1,则表示 KEY_1 键按下;value 等于 0 表示 KEY_1 键松开,如果 value 等于 2则表示 KEY_1 键长按。再比如,在绝对位移事件中(type=3),如果 code=0(触摸点 X 坐标 ABS_X),那么 value 值就等于触摸点的 X 轴坐标值;同理,如果 code=1(触摸点 Y 坐标 ABS_Y),此时value 值便等于触摸点的 Y 轴坐标值;所以对 value 值的解释需要根据不同的 code 值而定
*/
};
type
code
Key

相对位移

绝对位移

通过数据同步使得应用程序得知本轮已经读取到完整的数据同步类事件有如下,所有的输入设备都需要上报同步事件,上报的同步事件通常是SYN_REPORT,而 value 值通常为 0
查看输入设备是哪个设备节点
c
cat /proc/bus/input/devices

输入设备代码
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>
int main(int argc, char *argv[])
{
struct input_event in_ev = {0};
int fd = -1;
int value = -1;
//校验传参
if(2 != argc)
{
fprintf(stderr, "Usage: %s <input_dev>\n", argv[0]);
return 1;
}
// 打开输入事件文件
if(0 > (fd = open(argv[1], O_RDONLY)))
{
perror("open error");
return 1;
}
for( ; ; )
{
// 读取输入事件
if(sizeof(struct input_event) != (read(fd, &in_ev, sizeof(struct input_event))))
{
perror("read error");
break;
}
// 按键输入的类型
if(EV_KEY == in_ev.type)
{
switch (in_ev.value)
{
case 0:
printf("code<%d>:松开\n", in_ev.code);
break;
case 1:
printf("code<%d>:按下\n", in_ev.code);
break;
case 2:
printf("code<%d>:长按\n", in_ev.code);
break;
}
}
}
}
开发板KEY0按键测试
键盘测试
接入键盘,我使用的是2.4G连接方式
查看设备时发现有两个

event3是键盘上的按键

event4是键盘上的旋钮
由此可知这两个东西不是同一个设备

鼠标测试
只能读取鼠标的左、右和滚轮的按下和松开
