在之前的入门篇学习中,都是直接在Ubuntu中进行验证的,对于嵌入式Linux系统来说,也是可以直接移植的,只需要使用嵌入式硬件平台对应的交叉编译工具编译应用程序即可运行。
在嵌入式Linux系统中,编写的应用程序通常需要与硬件设备进行交互、操控硬件,譬如点亮开发板上的一颗LED灯、获取按键输入数据、在LCD屏上显示摄像头采集的图像、应用程序向串口发送数据或采集串口数据、网络编程等,那么本篇开始学习如何编写应用程序控制开发板上的各种硬件外设;Linux系统下,一切皆文件,也包括各种硬件设备,所以在Linux系统下,各种硬件设备是以文件的形式呈现给用户层,应用程序通过对文件的I/O操作来控制硬件设备。
点亮LED
正点原子MP157/Mini开发板(包括核心板和底板)上一共有2颗供用户使用的LED小灯;LED通常是由GPIO所控制的,本章来学习如何编写应用程序控制LED灯的亮灭。
应用层操控硬件的两种方法
设备文件便是各种硬件设备向应用层提供的一个接口,应用层通过对设备文件的I/O操作来操控硬件设备,设备文件通常在/dev/目录下,也把/dev 目录下的文件称为设备节点。当然还可以通过sysfs文件系统对硬件设备进行操控。
sysfs文件系统
sysfs是一个基于内存的文件系统,同devfs、proc文件系统一样,称为虚拟文件系统;它的作用是将内核信息以文件的方式提供给应用层使用。sysfs文件系统的主要功能便是对系统设备进行管理,它可以产生一个包含所有系统硬件层次的视图。
sysfs文件系统把连接在系统上的设备和总线组织成为一个分级的文件、展示设备驱动模型中各组件的层次关系。sysfs提供了一种机制,可以显式的描述内核对象、对象属性及对象间关系,用来导出内核对象(kernel object,譬如一个硬件设备)的数据、属性到用户空间,以文件目录结构的形式为用户空间提供对这些数据、属性的访问支持,如下图:
sysfs与/sys
sysfs文件系统挂载在/sys目录下。包括 block、bus、class、dev、devices、firmware、fs、kernel、module、power等,每个目录下又有许多文件或子目录。
devices就是设备存放的目录;bus是按照总线类型分类的目录;class是按照功能分类的,例如会有leds和input这些子目录;dev是按照设备号放置的目录。
总结
一般简单的设备会使用sysfs操控,例如LED、GPIO等;较复杂设备会使用设备节点操作/dev目录,例如LCD、触摸屏、摄像头等。
标准接口与非标准接口
Linux针对各种常见的设备进行分类,譬如LED类设备、输入类设备、FrameBuffer类设备、video类设备、PWM设备等等,并为每一种类型的设备设计了一套成熟的、标准的、典型的驱动实现的框架,这个就叫做设备驱动框架。
如果不用内核的驱动框架而是自己写,那就是非标准接口。除此之外,还有很多被Linux系统归为杂散设备(misc device)。
LED硬件控制方式
MP157上有两个LED:
对于MP157/Mini开发板出厂系统来说,这两颗LED设备使用的是Linux内核标准LED驱动框架注册而成,在/dev目录下并没有其对应的设备节点,其实现使用sysfs方式控制。
可以进入/sys/class/leds目录进行查看,可以看到例如sys-led都是链接文件,链接到/sys/devices/platform/leds/leds/sys-led;而这个设备文件中,有三个文件,brightness,max_brightness以及trigger这三个关心的文件,分别控制亮度,显示最大亮度以及触发模式。
如果通过cat命令进入trigger触发模式文件,方括号括起来的就是当前的触发方式。
直接控制,可以通过echo命令,示例如下:
echo timer > trigger //将 LED 触发模式设置为 timer
echo none > trigger //将 LED 触发模式设置为 none
echo 1 > brightness //点亮 LED echo 0 > brightness//熄灭 LED
编写LED应用程序
可以先通过宏定义完成文件路径的配置。
宏定义还可以用来直接定义传参错误时的printf信息,非常方便。
通过open打开LED灯的trigger和brightness文件,之后通过strcmp比较传参,借由write写入命令(均需要把trigger设置为none)。
开发板测试
把编译好的可执行文件,复制到开发板根文件系统中,然后可通过如下命令测试:
./testApp on # 点亮 LED ./testApp off # 熄灭 LED ./testApp trigger heartbeat # 将 LED 触发模式设置为 heartbeat |
GPIO应用编程
应用层操控GPIO
进入到/sys/class/gpio目录下,包含了export、unexport以及许多gpiochipX的文件。
gpiochipX,就是当前SoC包含的GPIO控制器,而STM32MP157共有12个控制器,为GPIOA-GPIOK以及GPIOZ,分别对应gpiochip0、gpiochip16、……以此类推。
每一个gpiochipX中,还有base、label、ngpio以及其他一些不太关心的文件,这三个是属性文件,均为只读文件。base也就是最小的编号,label就是标签,ngpio就是引脚的个数。
export,就是指定编号的GPIO引脚引出。导出成功后才能使用该GPIO。这是个只写文件,写入该文件即导出对应的GPIO引脚。导出后就会在/sys/class/gpio生成对应的文件夹。
unexport,就是与export对立,取掉导出的GPIO。同样也是只写文件,使用完GPIO后需要调用来取消导出。
成功导出就会生成gpioX,其中主要关心四个属性文件active_low、direction、edge以及value。
- direction:配置引脚的输入或输出模式。输出out,输入in。
- value:输出模式下,配置0与1来对应低电平和高电平;输入模式下,读取value获取输入电平。
- active_low:控制极性。
- edge:控制中断触发模式。配置前需要先设为输入模式,通过none、rising、falling、both控制触发沿。
GPIO应用编程输出
gpio_config函数来配置GPIO,传入attr和val对应文件以及传入的值,通过sprintf来查看传入参数,通过open打开文件后,write写入命令。
main函数中,先通过access并掺入F_OK判断目录是否存在,如果为1,说明不存在,需要导出。此时open打开export文件,write写入命令来导出gpio。
之后调用gpio_config配置direction、active_low以及value。
GPIO应用编程输入
gpio_config是一样的,这里不再赘述。
main函数也类似,通过access判断是否存在,不存在则需要导出,open打开export后write写入来导出gpio。
然后gpio_config配置,direction就需要配为in,设置active_low然后设置edge为none非中断。
最后通过open打开文件后,read读取value的电平状态。
GPIO应用编程中断
其余的操作与输入是类似的,这里只看中断特有的代码。
gpio_config中,需要配置edge的中断触发方式,这里配置为both。
读取文件,需要用struct pollfd结构体,open打开存入pfd.fd中,然后设置pfd.events为POLLPRI(只关心高优先级数据可读,中断就是高优先级数据),之后先read一次后进入死循环中:调用poll,之后先判断pfd.revents&POLLPRI,为真后进入读取,lseek先把读位置移到头部后再read读取值。
开发板测试
可以选择PE13测试,也就是编号为77的引脚:
GPIO输出
执行如下命令可测试:
./testApp 77 1 #控制 PE13 输出高电平 ./testApp 77 0 #控制 PE13 输出低电平 |
GPIO输入
可通过如下命令读取:
./testApp 77 |
GPIO中断
本实验选用PE4,因为PE13是悬空的,电平状态不确定。PE4的编号就是68,执行如下命令测试:
./testApp 68 # 监测 PE4 引脚中断触发情况 |
输入设备应用程序
对于输入设备的应用编程其主要是获取输入设备上报的数据、输入设备当前状态等,譬如获取触摸屏当前触摸点的X、Y轴位置信息以及触摸屏当前处于按下还是松开状态。
输入类设备编程
input子系统
基于input子系统注册成功的输入设备,都会在/dev/input 目录下生成对应的设备节点,设备节点名称通常为eventX(X表示一个数字编号0、1、2、3等),如/dev/input/event0、/dev/input/event1、
/dev/input/event2等,通过读取这些设备节点可以获取输入设备上报的数据。
读取数据流程
需要先打开设备文件;之后发起读操作(如read),如果无数据会进入休眠(阻塞I/O);有数据则会被唤醒,督导数据返回;应用程序处理读取数据。
应用程序解析数据
read操作获取的就是struct input_event结构体数据。主要关心齐总的type、code以及value成员变量。
type就是用来描述事件的类型;code是具体的事件,每一个事件类型均有不同的对应事件;value就是code对应事件所读取到的值。
读取完成,就是通过同步事件完成的,就是EV_SYN,内核完成了所有数据上报就会上报EV_SYN同步事件。
上报的同步事件通常为SYN_REPORT,value通常为0。
读取struct input_event数据
定义好对应的结构体后,open打开文件,并在死循环中调用read读取并printf打印出来。
开发板验证
MP157有两个按键,就是典型的输入设备:
在/dev/input目录下存在按键对应的设备节点;也可以查看/proc/bus/input/devices文件查询。
可通过如下命令执行:
./testApp /dev/input/event1 |
按键应用编程
对于按键,按下value=1,松开则value=0,长按则value=2。
main函数中,通过open打开文件,之后进入死循环:通过read读取传入struct input_event的结构体指针in_ev中,之后通过in_ev.value成员变量判断,按键的事件为EV_KEY,然后switch判断value的大小并执行printf打印。
开发板上验证可通过如下命令:
./testApp /dev/input/event1 # 测试开发板上的 KEY0 和 KEY1 |
触摸屏应用编程
解析触摸屏设备上报数据
触摸屏分为多点触摸设备和单点触摸设备。单点触摸一次完整数据只有一个触摸点数据:ABS_XXX时间承载上报触摸点信息;多点触摸则可能一次包含多个触摸点信息,大多是以ABS_MT_XXX事件承载上报数据。
MT的协议在触摸屏驱动的时候已经学过了,这里就不再多记录了。大多使用Type B协议,会先生成ABS_MT_SLOT事件并生成ABS_MT_TRACKING_ID之后上报坐标值。
可通过“cat /proc/bus/input/devices”查询LCD的设备名称来找到对应事件,然后同样执行之前的命令,把事件换成触摸屏就可以了。
获取触摸屏信息
可通过ioctl()函数一区触摸屏设备信息。原型如下:
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
第一个参数fd对应文件描述符;第二个参数request与具体要操作的对象有关,没有统一值,表示向文件描述符请求相应的操作,也就是请求指令;此函数是一个可变参函数,第三个参数需要根据request参数来决定,配合request来使用。
可在input.h中查询对应事件的宏定义。
可通过下面的宏,获取触摸屏的slot:
#define EVIOCGABS(abs) _IOR('E', 0x40 + (abs), struct input_absinfo)
例如获取触摸屏的最大触摸点数:定义struct input_absinfo结构体info,然后open打开设备文件后,通过ioctl,第二个参数为EVIOCGABS(ABS_MT_SLOT),读取之后info.maximum-info.minimum+1就是多大点数。
单点触摸应用程序
open打开设备文件,进入死循环:read读取输入事件,通过in_ev.type判断事件类型,如果是EV_ABS就是绝对位移事件,然后可通过in_ev.code判断是x还是y坐标;如果是EV_SYN同步事件,就可以通过in_ev.code判断是否为SYN_REPORT判断是什么事件,printf打印对应信息。
多点触摸应用程序
这里的区别就是定义了几个结构体来存储多个点的数据。
编写ts_read来读取数据,传入fd、max_slots以及自定义的ts_mt结构体指针mt(该结构体存储了每一个触摸点的信息)。其中定义了input_event的in_ev,static的slot以及tp_xy结构体数组xy(存储x和y坐标),之后通过memset先清空mt,然后将mt的id成员变量全部初始化为-2(-1表示触摸点删除,id>=0就是创建)。进入死循环中,read读取事件到in_ev中,通过switch判断in_ev.type,如果是EV_ABS那就通过in_ev.code判断是什么触摸屏事件并存储对应值;如果是EV_SYN那就进行数据上报,在in_ev.code是SYN_REPORT时将xy[]中数据存到mt[]中。
main函数中,open打开设备文件,然后通过ioctl获取最大触摸点数,并通过calloc初始化mt,之后进入死循环中,调用ts_read读取,并把所有数据都printf出来。
使用tslib库
这是Linux系统下,专门为触摸屏开发的应用层函数库。
tslib简介
tslib为触摸屏驱动和应用层之间的适配层,它把应用程序中读取触摸屏struct input_event 类型数据(这是输入设备上报给应用层的原始数据)并进行解析的操作过程进行了封装,向使用者提供了封装好的API接口。tslib从触摸屏中获得原始的坐标数据,并通过一系列的去噪、去抖、坐标变换等操作,来去除噪声并将
原始的触摸屏坐标转换为相应的屏幕坐标。
tslib有一个配置文件ts.conf,该配置文件中提供了一些配置参数,可进行修改。tslib可以作为Qt触摸屏输入插件,为Qt提供输入支持。
tslib移植
这里就直接看教程就好了。
这里感觉驱动教程那边更好,直接在buildroot里面使能编译就可以了。
tslib库函数
使用tslib库函数需要在应用程序中包含tslib的头文件tslib.h,使用tslib编程其实非常简单,基本步骤如下所示:
- 第一步打开触摸屏设备;
- 第二步配置触摸屏设备;
- 第三步读取触摸屏数据。
打开触摸屏设备
使用tslib提供的库函数ts_open打开触摸屏设备,其函数原型如下所示:
#include "tslib.h"
struct tsdev *ts_open(const char *dev_name, int nonblock);
参数dev_name指定了触摸屏的设备节点;参数nonblock表示是否以非阻塞方式打开触摸屏设备,如果nonblock等于0表示阻塞方式,如果为非0值则表示以非阻塞方式打开。调用成功返回一个struct tsdev *指针,指向触摸屏设备句柄;如果打开设备失败,将返回NULL。
还可以使用ts_setup()函数,其函数原型如下所示:
#include "tslib.h"
struct tsdev *ts_setup(const char *dev_name, int nonblock)
参数dev_name可以设置为NULL,ts_setup()函数内部会读取TSLIB_TSDEVICE环境变量,获取该环境变量的内容以得知触摸屏的设备节点。
关闭触摸屏设备使用ts_close()函数:
int ts_close(struct tsdev *);
配置触摸屏设备
调用ts_config()函数进行配置,其函数原型如下所示:
#include "tslib.h"
int ts_config(struct tsdev *ts);
参数ts指向触摸屏句柄。成功返回0,失败返回-1。
读取触摸屏数据
读取触摸屏数据使用ts_read()或ts_read_mt()函数,区别在于ts_read用于读取单点触摸数据,而ts_read_mt则用于读取多点触摸数据,其函数原型如下所示:
#include "tslib.h"
int ts_read(struct tsdev *ts, struct ts_sample *samp, int nr);
int ts_read_mt(struct tsdev *ts, struct ts_sample_mt **samp, int max_slots, int nr);
参数ts指向一个触摸屏设备句柄,参数nr表示对一个触摸点的采样数,设置为1即可!
ts_read_mt()函数有一个max_slots参数,表示触摸屏支持的最大触摸点数,应用程序可以通过调用ioctl()函数来获取触摸屏支持的最大触摸点数以及触摸屏坐标的最大分辨率等信息。
ts_read()函数的samp参数是一个struct ts_sample *类型的指针,指向一个struct ts_sample对象,struct ts_sample数据结构描述了触摸点的信息;调用ts_read()函数获取到的数据会存放在samp指针所指向的内存中。
ts_read_mt()函数的samp参数是一个struct ts_sample_mt **类型的指针,多点触摸应用程序,每一个触摸点的信息使用struct ts_sample_mt数据结构来描述;一个触摸点的数据使用一个struct ts_sample_mt对象
来装载,将它们组织成一个struct ts_sample_mt数组,调用ts_read_mt()时,将数组地址赋值给samp参数。
基于tslib编写触摸屏应用程序
单点触摸
需要定义tsdev结构体指针ts,ts_sample_mt结构体指针mt_ptr,input_absinfo结构体slot。
通过ts_setup打开触摸屏,第一个参数可以直接NULL就打开环境变量中的设备。
然后进入死循环,通过ts_read读取数据,通过samp.pressure判断是否按下,而保存的上一个pressure可辅助判断移动、按下与松开。
多点触摸
这个跟前面差不多,这里只列出区别。
通过ioctl函数获取最大触摸点数,同时mt_ptr需要calloc初始化。
死循环中,通过ts_read_mt读取数据,然后通过mt_ptr[].valid,如果为真就是数据有更新,之后一样通过pressure判断触摸情况。