1 前言
硬件连接完成后,就要搞定驱动层了,这也是Linux的魅力之一,通过驱动的匹配实现硬件高度的兼容性。
因为oled屏幕与官方的不一样,所以设备树要做对应的修改。要改设备树就要先大体明白整个程序的逻辑和配置情况,才能做到有的放矢的改动代码配置设备树。
读完整个程序后我有两个问题:
1.sys操控gpio引脚的方式内部实现是什么样的。跟正常的驱动匹配方式有什么关系?
2.设备树的regulater属性与应用层的交互方式是什么样的?
带着这两个问题我分析了从设备树到驱动层再到应用层的整个代码。
2 sysfs虚拟文件系统分析
之前没使用过sysfs虚拟文件系统操控gpio引脚,因此想要往下继续走,就要拓展新的知识框架学习相关的知识。
我们知道内核和应用层之间交互方式有三种:dev设备文件系统、proc虚拟文件系统、sys虚拟文件系统。这三种方式通过系统调用(open、read、write、close)、ioctl、内存映射等方式进行数据传输。
2.1 文件系统简介
2.1.1 dev设备文件系统
/dev设备文件系统:用于访问设备。在 Linux 中,一切皆文件,设备也不例外。/dev 目录包含了系统中所有的设备文件,通过这些文件可以访问硬件设备,比如磁盘驱动器、串口、打印机等。当你连接一个新设备到开发板上时,通常会在 /dev 目录下看到相应的设备文件。
2.1.2 proc虚拟文件系统
/proc虚拟文件系统:提供了对系统内核和运行中进程的访问。在 /proc 目录下,你可以找到许多以数字命名的目录和文件,每个对应一个进程或系统信息。例如,/proc/cpuinfo 文件包含了 CPU 相关的信息,/proc/meminfo 包含了内存使用情况等。
/proc 是正在运行的内核信息映射。它主要输出:进程信息、内存资源信息、磁盘分区信息等。
/proc下文件基本都是只读的,除了/proc/sys目录,它是可写的(查看和修改内核的运行参数)
2.1.2.1 目录含义
/proc下数字命令的目录就是对于PID的进程目录:
/proc/cmdline 启动时传递给kernel的参数信息(就是bootargs信息)
/proc/cpuinfo cpu的信息
/proc/crypto 内核使用的所有已安装的加密密码及细节
/proc/devices 已经加载的设备并分类
/proc/dma 已注册使用的ISA DMA频道列表
/proc/execdomains Linux 内核当前支持的execution domains
/proc/fb 帧缓冲设备列表,包括数量和控制它的驱动
/proc/filesystems 内核当前支持的文件系统类型
/proc/interrupts x86架构中的每个IRQ中断数
/proc/iomem 每个物理设备当前在系统内存中的映射
/proc/ioports 一个设备的输入输出所使用的注册端口范围
/proc/kcore 代表系统的物理内存,存储为核心文件格式,里边显示的是字节数,等于RAM大小加上4kb
/proc/kmsg 记录内核生成的信息,可以通过/sbin/klogd或/bin/dmesg来处理
/proc/loadavg 根据过去一段时间内CPU和IO的状态得出的负载状态,与uptime命令有关
/proc/locks 内核锁住的文件列表
/proc/mdstat 多硬盘,RAID配置信息(md=multiple disks)
/proc/meminfo RAM使用的相关信息
/proc/misc 其他的主要设备(设备号为10)上注册的驱动
/proc/modules 所有加载到内核的模块列表
/proc/mounts 系统中使用的所有挂载
/proc/partitions 分区中的块分配信息
/proc/pci 系统中的PCI设备列表
/proc/slabinfo 系统中所有活动的 slab 缓存信息
/proc/stat 所有的CPU活动信息
/proc/uptime 系统已经运行了多久
/proc/swaps 交换空间的使用情况
/proc/version Linux内核版本和gcc版本
/proc/bus 系统总线(Bus)信息,例如pci/usb等
/proc/driver 驱动信息
/proc/fs 文件系统信息
/proc/ide ide设备信息
/proc/irq 中断请求设备信息
/proc/net 网卡设备信息
/proc/scsi scsi设备信息
/proc/tty tty设备信息
/proc/net/dev 显示网络适配器及统计信息
/proc/vmstat 虚拟内存统计信息
/proc/vmcore 内核panic时的内存映像
/proc/diskstats 取得磁盘信息
/proc/schedstat kernel调度器的统计信息
/proc/zoneinfo 显示内存空间的统计信息,对分析虚拟内存行为很有用
2.1.2.2 进程N的信息
以下是/proc目录中进程N的信息:
/proc/N/cmdline 进程启动命令
/proc/N/cwd 链接到进程当前工作目录
/proc/N/environ 进程环境变量列表
/proc/N/exe 链接到进程的执行命令文件
/proc/N/fd 包含进程相关的所有的文件描述符 (ls /proc//fd | wc -l 查看某个进程打开多少FD)
/proc/N/maps 与进程相关的内存映射信息
/proc/N/mem 指代进程持有的内存,不可读
/proc/N/root 链接到进程的根目录
/proc/N/stat 进程的状态
/proc/N/statm 进程使用的内存的状态
/proc/N/status 进程状态信息,比stat/statm更具可读性
/proc/self 链接到当前正在运行的进程
2.1.3 sysfs虚拟文件系统
/sys虚拟文件系统:sys 是一个用于内核参数调整和查询的虚拟文件系统。它提供了一种机制,允许用户空间程序和内核之间进行通信和交换数据。在 /sys 目录下,你可以找到很多以 sysfs 格式呈现的系统信息和控制接口,比如 /sys/class 包含了设备类信息,/sys/kernel 包含了内核参数和信息等。综合的说它提供了比/dev简单的操控gpio的方法,又具有/proc操控内核的接口。推出的目的应该是要给用户层更简单的接口吧。
2.1.3.1 sys硬件设备的驱动程序信息:
block bus class dev devices firmware fs kernel module power
2.1.3.2 目录含义
① /sys/devices (/sys文件系统最重要的目录结构)
该目录下是全局设备结构体系,包含所有被发现的注册在各种总线上的各种物理设备。一般来说,所有的物理设备都按其在总线上的拓扑结构来显示,但有两个例外,即platform devices和system devices。platform devices一般是挂在芯片内部的高速或者低速总线上的各种控制器和外设,它们能被CPU直接寻址;system devices不是外设,而是芯片内部的核心结构,比如CPU,timer等,它们一般没有相关的驱动,但是会有一些体系结构相关的代码来配置它们。
② /sys/dev
该目录下有字符设备(block)和块设备(char)两个子目录,里面全是以主次设备号(major\:minor)命名的链接文件,链接到/sys/devices。
③ /sys/class (按功能分类设备)
该目录下包含所有注册在kernel里面的设备类型,每个设备类型表达具有一种功能的设备。每个设备类型子目录下是具体设备的符号链接,这些链接指向/sys/devices/…下的具体设备。设备类型和设备并没有一一对应的关系,一个物理设备可能具备多种设备类型;一个设备类型只表达具有一种功能的设备,比如:系统所有输入设备都会出现在/sys/class/input之下,而不论它们是以何种总线连接到系统的。(/sys/class也是构成linux统一设备模型的一部分)
④ /sys/block (从linux2.6.26版本开始已经移到了/sys/class/block)
代表着系统中当前被发现的所有块设备。按照功能来说防止在/sys/class下会更合适,但由于历史遗留因素而一直存在于/sys/block,但从linux2.6.22内核开始这部分就已经标记为过去时,只有打开了CONFIG_SYSFS_DEPRECATED配置编译才会有这个目录存在,并且其中的内容在从linux2.6.26版本开始已经正式移到了/sys/class/block,旧的接口/sys/block为了向后兼容而保留存在,但其中的内容已经变为了指向它们在/sys/devices/中真实设备的符号链接文件。
⑤ /sys/bus (按总线类型分类设备)
一般来说每个子目录(总线类型)下包含两个子目录,一个是devices,另一个是drivers;其中devices下是这个总线类型下的所有设备,这些设备都是符号链接,它们分别指向真正的设备(/sys/devices/…下);而drivers下是所有注册在这个总线上的驱动,每个driver子目录下 是一些可以观察和修改的driver参数。 (它也是构成linux统一设备模型的一部分)
⑥ /sys/module
该目录包含所有被载入Kernel的模块,无论这些模块是以内联(inlined)方式编译到内核映像文件中还是编译为外模块(.ko文件)
⑦ /sys/fs
该目录用来描述系统中所有的文件系统,包括文件系统本身和按照文件系统分类存放的已挂载点。
⑧ /sys/kernel
该目录下存放的是内核中所有可调整的参数
⑨ /sys/firmware
该目录下包含对固件对象(firmware object)和属性进行操作和观察的接口,即这里是系统加载固件机制的对用户空间的接口.(关于固件有专用于固件加载的一套API)
⑩/sys/power
该目录下有几个属性文件可以用于控制整个机器的电源状态,如向其中写入控制命令让机器关机/重启等等。
2.2 分析源码
了解与内核传输数据的三种方式后,就可以接着往下分析源码了。
2.2.1 sysfs操控gpio引脚的流程
整个程序的头从oled屏幕初始化开始,可以看出rst引脚在这里被调用,使用的是"OLED_RST_1"宏。
static void OLED_Reset(void)
{
OLED_RST_1;
DEV_Delay_ms(100);
OLED_RST_0;
DEV_Delay_ms(100);
OLED_RST_1;
DEV_Delay_ms(100);
}
2.2.2 定义宏函数
往下追宏定义可以看到在.h文件中被定义为宏函数。
//OLED Define
#define OLED_DC_PIN 34
#define OLED_RST_PIN 51
#define OLED_RST_0 DEV_Digital_Write(OLED_RST_PIN,0)
#define OLED_RST_1 DEV_Digital_Write(OLED_RST_PIN,1)
#define OLED_DC_0 DEV_Digital_Write(OLED_DC_PIN,0)
#define OLED_DC_1 DEV_Digital_Write(OLED_DC_PIN,1)
2.2.3 中间跳转层
追踪宏函数,看到使用的是SYSFS_GPIO_Write函数进行引脚的操作。
void DEV_Digital_Write(UWORD Pin, UBYTE Value)
{
#ifdef USE_DEV_LIB
SYSFS_GPIO_Write(Pin, Value);
#endif
}
2.2.4 sys操控gpio引脚
继续追踪"SYSFS_GPIO_Write"函数,发现操作方式是sysfs的文件操作方式操控引脚。
int SYSFS_GPIO_Write(int Pin, int value)
{
const char s_values_str[] = "01";
char path[DIR_MAXSIZ];
int fd;
snprintf(path, DIR_MAXSIZ, "/sys/class/gpio/gpio%d/value", Pin);
fd = open(path, O_WRONLY);
if (fd < 0) {
SYSFS_GPIO_Debug( "Write failed : Pin%d,value = %d\n", Pin, value);
return -1;
}
if (write(fd, &s_values_str[value == SYSFS_GPIO_LOW ? 0 : 1], 1) < 0) {
SYSFS_GPIO_Debug( "failed to write value!\n");
return -1;
}
close(fd);
return 0;
}
2.2.5 对sysfs操控gpio引脚的分析
如下所示是最后sysfs操作gpio引脚的核心代码。使用snprintf向数组内写入字符串"/sys/class/gpio/gpio%d/value",其中%d是后面pin的值,pin值就是上面计算的编号(如:GPIO1_C7_d 的引脚编号为55);然后使用open函数打开对应的文件,权限是可读可写;向文件内写入0/1。
snprintf(path, DIR_MAXSIZ, "/sys/class/gpio/gpio%d/value", Pin);
fd = open(path, O_WRONLY);
write(fd, &s_values_str[value == SYSFS_GPIO_LOW ? 0 : 1], 1)
下面是完整的使用sysfs操控引脚的流程,实现方式如下:
echo 55 >/sys/class/gpio/export //激活对应的sys下硬件引脚
echo out >/sys/class/gpio/gpio55/direction //将gpio引脚设置为输出
echo in >/sys/class/gpio/gpio55/direction //设置为输入
cat /sys/class/gpio/gpio3/direction //查看设置情况
in/out //输入或输出
注:一般sysfs操控方式系统默认都会开启,若未开可以设置menuconfig开启。
Device Drivers
--> GPIO Support
--> /sys/class/gpio/...(sysfs interface)
到此sysfs在应用层的代码已经走完了,应该分析驱动层的代码了。
2.2.6 在/sys/class创建硬件信息的方法
正常的驱动在/sys/class创建对应的目录和设备信息是使用class_create和device_create两个函数;创建时机是使用insmod安装驱动后,函数向上提交目录名和设备信息;这时内核发起hotplug event。然后由应用层hotplug唤醒udevd,由udev/mudev创建设备节点。这时/dev下的设备节点就创建完成
udev的工作原理 当系统内核发现安装或者卸载了某一个硬件设备时,内核会执行hotplug,以便让hotplug去安装或卸载该硬件的驱动程序;hotplug在处理完硬件的驱动程序后,就会去呼叫执行udevd,以便让udevd可以产生或者删除硬件的设备文件。 接着udevd会通过libsysfs读取sys文件系统,以便取得该硬件设备的信息;然后再向namedev查询该外部设备的设备文件信息,例如文件的名称、权限等。最后,udevd就依据上述的结果,在/dev/目录中自动建立该外部设备的设备文件,同时在/etc/udev/rules.d下检查有无针对该设备的使用权限。
这时问题产生了,/sys/class/gpio/gpio%d的文件是哪里来的?是上面所说的由驱动创建的吗?按照思路继续往下分析,要判断使用的哪个驱动就要先看设备树,根据设备树信息查找驱动。因此继续往下分析设备树。
2.3 分析设备树
设备树的的信息都大同小异,这里节选rst引脚模块的设备树进行分析。
/*RST*/
gpio1pc3:gpio1pc3 {
compatible = "regulator-fixed";
pinctrl-names = "default";
pinctrl-0 = <&gpio1_pc3>;
regulator-name = "gpio1_pc3";
regulator-always-on;
};
/*RST*/
gpio1-pc3 {
gpio1_pc3:gpio1-pc3 {
rockchip,pins = <1 RK_PC3 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
2.3.1 regulator属性
2.3.1.1 regulator-fixed字段
compatible = "regulator-fixed";
在compatible属性中regulator-fixed是特殊字段,它在系统解析设备树的时候会默认创建电压固定的regulator,或由某个GPIO开关控制的regulator。特点是不能控制电压,只有enable和disabled两种状态,没有设备使用时自动关电(disabled)。
2.3.1.2 同类属性
compatible = "regulator-gpio";
作用:创建一个由 GPIO 控制的 regulator。一般是 GPIO 控制输出多个不同的电压或电流(支持多个 GPIO),因此除了开(enable) \ 关(disabled)两种操作外,往往还支持电压或电流的控制。
2.3.1.3 保持电平
regulator-always-on;
一直上电,防止因其他原因被关电,否则需要在其他驱动中获取此 regulator 来手动控制:regulator_enable() regulator_disable()。但我在后面不改动设备树的情况下驱动使用gpio_set_value函数也正常控制引脚的电平转换,因此这个值是作为保持值,不控制它,它的初始状态一直是高电平。
2.3.1.4 regulator的名字
regulator-name = "gpio1_pc3";
作用是设置/sys/class/regulator/目录/name中name的名称。
注:我没在项目中使用regulator属性,有错误的话请指正。
2.3.1.5 帮助文件
查阅sysdrv/source/kernel/Documentation/devicetree/bindings/regulator下的帮助手册不知道咋回事全变成yaml文件了,打开以后虽然也是帮助文档,但结构没有下面老的看着舒服。而且我查了一下资料没有关于yaml文件的使用例子,我在实际工作中除了编译modules软件包时会用到yaml文件,其他地方也不会用到;而且我的用法和帮助文件完全不搭边啊!所以很纳闷这个帮助文件应该怎么用,正常来说是不是应该有对应的软件读取这个yaml文件呢?有知道咋用的可以交流一下。
这个是在网上搜到的老版本的帮助文件。
基于内核版本 4.16.1。页面生成于 2018-04-09 11:52 EST。
1 Fixed Voltage regulators
2
3 Required properties:
4 - compatible: Must be "regulator-fixed";
5
6 Optional properties:
7 - gpio: gpio to use for enable control
8 - startup-delay-us: startup time in microseconds
9 - enable-active-high: Polarity of GPIO is Active high
10 If this property is missing, the default assumed is Active low.
11 - gpio-open-drain: GPIO is open drain type.
12 If this property is missing then default assumption is false.
13 -vin-supply: Input supply name.
14
15 Any property defined as part of the core regulator
16 binding, defined in regulator.txt, can also be used.
17 However a fixed voltage regulator is expected to have the
18 regulator-min-microvolt and regulator-max-microvolt
19 to be the same.
20
21 Example:
22
23 abc: fixedregulator@0 {
24 compatible = "regulator-fixed";
25 regulator-name = "fixed-supply";
26 regulator-min-microvolt = <1800000>;
27 regulator-max-microvolt = <1800000>;
28 gpio = <&gpio1 16 0>;
29 startup-delay-us = <70000>;
30 enable-active-high;
31 regulator-boot-on;
32 gpio-open-drain;
33 vin-supply = <&parent_reg>;
34 };
2.3.1.6 其他属性
有一些没用的属性,这里也介绍一下:
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
regulator 最小及最大电压限制。对于 regulator-fixed 无实际意义。
regulator-boot-on;
开机时自动上电。注意:若一段时间内无设备在使用此 regulator,则会自动关电(猜测应该和系统低功耗有关),因此必须加上 regulator-always-on。
cat /sys/class/regulator/regulator.*/num_users # 查看有多少个设备在使用此 regulator
cat /sys/class/regulator/regulator.*/state # 查看此 regulator 的状态:enabled or disabled
查看系统中regulator的使用情况。
2.3.2 pinctrl属性
pinctrl子系统,主要作用是引脚复用,如下是对复用引脚含义的解释。
rockchip,pins = <1 RK_PC3 RK_FUNC_GPIO &pcfg_pull_none>;
RK_PC3:引脚在芯片内部的名称
RK_FUNC_GPIO:将引脚配置为gpio功能
&pcfg_pull_none:引脚不连接上下拉电阻
2.4 查找驱动
2.4.1 通过设备树查找驱动
到这步设备树所有属性也都分析完成了。根据设备树信息查找对应的驱动,所以在luckfox-pico/sysdrv/source/kernel/drivers/筛查驱动文件。驱动匹配设备树的方式有多种:路径、节点名、compatible属性、device_type 属性等,都筛一遍发现能匹配上的就是compatible属性。
再进一步看compatible属性的字符串是"regulator-fixed"。最终筛出来两个比较符合实际的驱动文件:regulator/fixed.c和gpio/gpiolib-of.c。
sysdrv/source/kernel/drivers$ grep -rn "regulator-fixed"
Binary file regulator/fixed.o matches
regulator/fixed.c:265: .compatible = "regulator-fixed",
regulator/fixed.c:269: .compatible = "regulator-fixed-clock",
gpio/gpiolib-of.c:143: (of_device_is_compatible(np, "regulator-fixed") ||
Binary file gpio/gpiolib-of.o matches
media/i2c/imx219.c:463: * case of DT for regulator-fixed one should define the startup-delay-us
staging/mt7621-dts/mt7621.dtsi:53: compatible = "regulator-fixed";
staging/mt7621-dts/mt7621.dtsi:62: compatible = "regulator-fixed";
首先分析regulator/fixed.c文件。发现虽然是驱动文件,但读下来发现不会在/sys/class/创建gpio目录。这就和应用层的路径产生冲突了,所以最终判断不是设备树匹配的驱动文件。
然后分析gpio/gpiolib-of.c文件。读完整个代码发现压根就不是驱动文件,连驱动框架都没有,就是一个函数库。深入分析是不是间接调用了这个函数文件然后间接匹配的呢?查询调用gpio/gpiolib-of.c中相关函数的.c文件,发现调用函数的驱动也不符合。
到这整个分析就陷入死路了!
没有驱动作为中间层的话应用层如何获取设备树的信息呢?在我了解的驱动模型中想要在/sys/class/下注册目录和设备信息,必然会调用创建的函数;所以内核必然也不会脱离这个规则,所以必然有这个驱动。但现在设备树到驱动这个分析方向走不通,那就换个思路从应用层到驱动。
2.4.1 通过应用层查找驱动
如下是应用层控制gpio引脚的初始程序。
fd = open("/sys/class/gpio/export", O_WRONLY);
从上面的代码可以看出某个驱动在gpio类下注册了export这个设备信息。那就使用grep查找export这个关键字:
grep -r "export"
.gpiolib-legacy.o.cmd: include/linux/export.h \
gpio-max730x.c: * The driver exports a standard gpiochip interface
Binary file gpiolib-devres.o matches
gpio-sta2x11.c: * FIXME : this functionality shall be managed (and exported to other drivers)
.gpiolib-of.o.cmd: include/linux/export.h \
gpiolib.h: * @owner: helps prevent removal of modules exporting active GPIOs
gpiolib.h:#define FLAG_SYSFS 3 /* exported via /sys/class/gpio/control */
gpio-it87.c: * newly-exported GPIO interfaces are set to input.
Binary file gpiolib-sysfs.o matches
.gpio-rockchip.o.cmd: include/linux/export.h \
.gpiolib.o.cmd: include/linux/export.h \
gpiolib-sysfs.c: * Lock to serialise gpiod export and unexport, and prevent re-export of
gpiolib-sysfs.c: * /sys/class/gpio/gpioN... only for GPIOs that are exported
gpiolib-sysfs.c: * /sys/class/gpio/export ... write-only
gpiolib-sysfs.c: * integer N ... number of GPIO to export (full access)
gpiolib-sysfs.c: * /sys/class/gpio/unexport ... write-only
gpiolib-sysfs.c: * integer N ... number of GPIO to unexport
gpiolib-sysfs.c:static ssize_t export_store(struct class *class,
gpiolib-sysfs.c: * request and export were done by on behalf of userspace, so
gpiolib-sysfs.c: status = gpiod_export(desc, true);
gpiolib-sysfs.c:static CLASS_ATTR_WO(export);
gpiolib-sysfs.c:static ssize_t unexport_store(struct class *class,
gpiolib-sysfs.c: /* reject bogus commands (gpio_unexport ignores them) */
gpiolib-sysfs.c: * request and export were done by on behalf of userspace, so
gpiolib-sysfs.c:static CLASS_ATTR_WO(unexport);
gpiolib-sysfs.c: &class_attr_export.attr,
gpiolib-sysfs.c: &class_attr_unexport.attr,
gpiolib-sysfs.c: * gpiod_export - export a GPIO through sysfs
gpiolib-sysfs.c:int gpiod_export(struct gpio_desc *desc, bool direction_may_change)
gpiolib-sysfs.c: /* can't export until sysfs is available ... */
gpiolib-sysfs.c: gpiod_dbg(desc," %s: unavailable (requested=%d, exported=%d)\n",
gpiolib-sysfs.c:EXPORT_SYMBOL_GPL(gpiod_export);
gpiolib-sysfs.c:static int match_export(struct device *dev, const void *desc)
gpiolib-sysfs.c: * gpiod_export_link - create a sysfs link to an exported GPIO node
gpiolib-sysfs.c: * @desc: GPIO to create symlink to, already exported
gpiolib-sysfs.c:int gpiod_export_link(struct device *dev, const char *name,
gpiolib-sysfs.c: cdev = class_find_device(&gpio_class, NULL, desc, match_export);
gpiolib-sysfs.c:EXPORT_SYMBOL_GPL(gpiod_export_link);
gpiolib-sysfs.c: * gpiod_unexport - reverse effect of gpiod_export()
gpiolib-sysfs.c:void gpiod_unexport(struct gpio_desc *desc)
gpiolib-sysfs.c: dev = class_find_device(&gpio_class, NULL, desc, match_export);
gpiolib-sysfs.c:EXPORT_SYMBOL_GPL(gpiod_unexport);
gpiolib-sysfs.c: /* prevent further gpiod exports */
gpiolib-sysfs.c: * registered, and so arch_initcall() can always gpio_export().
gpiolib.c: gpiod_unexport(desc);
gpiolib-acpi.c:#include
.gpiolib-cdev.o.cmd: include/linux/export.h \
Binary file gpiolib.o matches
gpio-amd8111.c: * This data only exists for exporting the supported
.gpiolib-sysfs.o.cmd: include/linux/export.h \
gpio-cs5535.c: * by the gpio_chip API, so these are exported. For a full list of the
Binary file gpiolib-cdev.o matches
gpiolib-legacy.c: err = gpiod_export(desc, flags & GPIOF_EXPORT_CHANGEABLE);
.gpiolib-devres.o.cmd: include/linux/export.h \
Binary file gpiolib-legacy.o matches
Binary file gpiolib-of.o matches
Binary file gpio-rockchip.o matches
从上面一路分析下来,我判断使用的驱动文件应该是gpio/gpiolib-sysfs.c文件。打开文件分析发现大部分函数就是控制/sys/class/gpio引脚的几个逻辑函数。如下就是获取到硬件信息后注册"gpio+编号"的函数:
dev = device_create_with_groups(&gpio_class, &gdev->dev,
MKDEV(0, 0), data, gpio_groups,
ioname ? ioname : "gpio%u",
desc_to_gpio(desc));
但还有个疑问,是怎么获取到设备树信息的呢?
这个驱动文件是采用老方法写的,那初始化一定是从init函数开始走的,那注册函数肯定在init中,接着分析:
static int __init gpiolib_sysfs_init(void)
{
int status;
unsigned long flags;
struct gpio_device *gdev;
status = class_register(&gpio_class);
if (status < 0)
return status;
/* Scan and register the gpio_chips which registered very
* early (e.g. before the class_register above was called).
*
* We run before arch_initcall() so chip->dev nodes can have
* registered, and so arch_initcall() can always gpio_export().
*/
spin_lock_irqsave(&gpio_lock, flags);
list_for_each_entry(gdev, &gpio_devices, list) {
if (gdev->mockdev)
continue;
/*
* TODO we yield gpio_lock here because
* gpiochip_sysfs_register() acquires a mutex. This is unsafe
* and needs to be fixed.
*
* Also it would be nice to use gpiochip_find() here so we
* can keep gpio_chips local to gpiolib.c, but the yield of
* gpio_lock prevents us from doing this.
*/
spin_unlock_irqrestore(&gpio_lock, flags);
status = gpiochip_sysfs_register(gdev);
spin_lock_irqsave(&gpio_lock, flags);
}
spin_unlock_irqrestore(&gpio_lock, flags);
return status;
}
从class_register函数可以看出是注册gpio目录的函数。继续往下读发现核心代码是下面两个函数,当应用层传递引脚信息到内核时,内核调用list_for_each_entry函数遍历gpio_devices列表,若引脚没有注册则会在/sys/class/gpio/下生成对应的文件,若已生成则会跳出。
list_for_each_entry(gdev, &gpio_devices, list)
status = gpiochip_sysfs_register(gdev);
到这是打通了应用层到驱动的路,那驱动到设备树呢?通读整个驱动文件,发现设备树和这个驱动也没匹配啊?那设备树是谁在什么时候解析的呢?
正常来说系统一上电后会解析对应的设备树文件获取硬件信息,所以我判断是不是从系统中获取到的设备信息呢?
在整个编译环境中搜索设备树文件"rv1103g-luckfox-pico-plus.dts",可以发现其已经被定义为RK_KERNEL_DTS,其文件名称为BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Plus-IPC.mk。
grep -rn "rv1103g-luckfox-pico-plus.dts"
project/cfg/BoardConfig_IPC/BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Plus-IPC.mk:25:export RK_KERNEL_DTS=rv1103g-luckfox-pico-plus.dts
在执行build.sh编译选择分支时可以看到对应的mk文件。至此可知在编译系统阶段已将对应设备树编译进内核了。
├── build.sh -> project/build.sh ---- SDK编译脚本
├── media --------------------------- 多媒体编解码、ISP等算法相关(可独立SDK编译)
├── sysdrv -------------------------- U-Boot、kernel、rootfs目录(可独立SDK编译)
├── project ------------------------- 参考应用、编译配置以及脚本目录
│ ├── cfg
│ ├── BoardConfig_IPC
│ ├── BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk
│ ├── BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico_Mini_A-IPC.mk
│ ├── BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Mini_B-IPC.mk
│ ├── BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Plus-IPC.mk
│ ├── BoardConfig-SPI_NAND-NONE-RV1106_Luckfox_Pico_Pro_Max-IPC.mk
│ └── ...
├── output -------------------------- SDK编译后镜像文件存放目录
├── docs ---------------------------- SDK文档目录
└── tools --------------------------- 烧录镜像打包工具以及烧录工具
2.5 小结
换而言之对应的设备树在系统上电阶段就已经被解析了,引脚信息被放进了gpio_devices链表中,然后被驱动调用。
注:这段分析的很艰难,网上资料仅限于应用层对sysfs虚拟文件系统下引脚控制的控制讲解,一点不涉及其内部实现机理。在查阅资料又和别人讨论后,最后给出了这个起码逻辑自洽的解释!有大佬看到以后希望可以给一些建议。
至此官方源码操控gpio的方式已经全流程打通了!
3 编写自己的驱动
3.1 修改设备树
打开设备树主文件/luckfox-pico/sysdrv/source/kernel/arch/arm/boot/dts/rv1103g-luckfox-pico-plus.dts编写自己的设备树。
因为使用的是platform总线协议,匹配方式是设备树。所以改动设备树。首先匹配属性改为自己定义,因此pinctrl属性舍弃,直接配置。设备树看着简单,但主要难在开头属性不知道咋配置上,配置成功后每条属性很简单易懂。
/ {
model = "Luckfox Pico Plus";
compatible = "rockchip,rv1103g-38x38-ipc-v10", "rockchip,rv1103";
/*DC*/
gpio1pa2:gpio1pa2 {
compatible ="myoled-dc,spi"; //匹配驱动的名称
gpionode-dc=<&gpio1 2 GPIO_ACTIVE_HIGH>;//获取引脚的信息
&gpio1:gpio1组引脚
2:引脚编号为2
GPIO_ACTIVE_HIGH:输出电压值为1。设备树只是描述硬件设备,生效是在驱动中,因此即使设置为高电平,在驱动中设置为低电平后,原有的高点平是不生效的
};
/*RST*/
gpio1pc3:gpio1pc3 {
compatible ="myoled-rst,spi";
gpionode-rst=<&gpio1 19 GPIO_ACTIVE_HIGH>;
};
};
/**********SPI**********/
/*注意:131行~159行 使用SPI打开 使用I2C注释*/
&spi0 {
status = "okay"; //SPI控制器已启用
pinctrl-names = "default";//表示设备的状态是默认
pinctrl-0 = <&spi0m0_pins>;//使用名为spi0m0_pins的引脚控制器配置SPI控制器的引脚
cs-gpios = <&gpio1 RK_PC0 1>;//片选引脚,配置为高电平
// cs-gpios = <&gpio1 26 1>;
#address-cells = <1>;//reg属性有1个地址
#size-cells = <0>;//reg属性无长度
spidev@0 {
compatible = "rockchip,spidev";//匹配驱动的属性
spi-max-frequency = <1000000000>;//最大时钟频率
reg = <0>;//设备的寄存器地址为0
};
};
&pinctrl {
spi0 {
/omit-if-no-ref/
spi0m0_pins: spi0m0-pins {//对应的上面pinctrl的名字
rockchip,pins =
/* spi0_clk_m0 */
<1 RK_PC1 4 &pcfg_pull_none>,//时钟引脚。pcfg_pull_none:不配置特殊功能
/* spie_miso_m0 */
/* <1 RK_PC3 6 &pcfg_pull_none>, *//主入从出引脚
/* spi_mosi_m0 */
<1 RK_PC2 6 &pcfg_pull_none>;//主出从入引脚
};
};
};
spi驱动是代码的核心,目前我的水平改起来比较花时间,在后续驱动电阻式lcd触摸屏的项目中我会重新写一个带dma的spi驱动,优化数据传输速率。
3.2 编译设备树
编译内核,将更改后的设备树更新到内核中
生成默认配置文件
make ARCH=arm CROSS_COMPILE=arm-rockchip830-linux-uclibcgnueabihf- rockchip_defconfig
编译设备树
make ARCH=arm -j4 CROSS_COMPILE=arm-rockchip830-linux-uclibcgnueabihf- dtbs
还有一种最简单的方式,上面的命令不知道咋填,直接重新编译内核
make -j4 all
3.3 编写dc驱动
下面是我编写的dc驱动
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define CNAME1 "gpiodevdc"
struct device_node *node;
int gpiono_dc;
int major;
struct class *cls;
struct device *dev;
int condition = 0;
char kbuf[1] ={'0'};
int ret;
struct device_node *node;
int gpio_open(struct inode *inode, struct file *file)
{
printk("%s:%d\n", __func__, __LINE__);
return 0;
}
ssize_t gpio_read(struct file *file,
char __user *ubuf, size_t size, loff_t *offs)
{
int i, j;
// 将status拷贝到用户空间
if (size > sizeof(kbuf))
size = sizeof(kbuf);
i = gpio_get_value(gpiono_dc);
j = gpio_get_value(gpiono_rst);
kbuf[0] = '0' + i;
kbuf[1] = '0' + j;
ret = copy_to_user(ubuf, kbuf, size); // 内核发数据到用户
if (ret)
{
printk("copy data to user error\n");
return -EIO;
}
return size;
}
ssize_t gpio_write(struct file *file,
const char __user *ubuf, size_t size, loff_t *offs)
{
memset(kbuf, 0, sizeof(kbuf));
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
ret = copy_from_user(kbuf, ubuf, sizeof(kbuf)); // 用户发数据到内核
if (ret)
{
printk("copy from user error\n");
return -EINVAL;
}
ret =kbuf[0] - '0';
printk("ret值[%d]\n",ret);
gpio_set_value(gpiono_dc, ret);
return size;
}
int gpio_close(struct inode *inode, struct file *file)
{
printk("%s:%d\n", __func__, __LINE__);
return 0;
}
struct file_operations fops = {
.open = gpio_open,
.read = gpio_read,
.write = gpio_write,
.release = gpio_close,
};
// 初始化
int platform_gpio_probe(struct platform_device *pdev)
{
printk("进入platform总线开始初始化\n");
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
// 1.获取设备树的节点
node = of_find_compatible_node(NULL, NULL, "myoled-dc,spi");
if (node == NULL)
{
printk("get dc-node error\n");
return -ENODATA;
}
printk("成功匹配上获取节点号\n");
// 获取gpio编号
gpiono_dc = of_get_named_gpio(node, "gpionode-dc", 0); // 0是第0个属性,起始是0,gpionode-dc
if (gpiono_dc < 0)
{
printk("parse gpiono error\n");
ret = gpiono_dc;
goto ERR1;
}
// 2.通过gpio申请去使用
if ((ret = gpio_request(gpiono_dc, NULL)) != 0)
{
printk("gpio request error\n");
goto ERR1;
}
printk("成功匹配上获取DC-gpio编号\n");
// 设置gpio初始值,初始化led为熄灭状态
if( (ret = gpio_direction_output(gpiono_dc, 0))<0)
{
printk("设置DC引脚为输出失败\n");
goto ERR2;
}
printk("设置DC引脚为输出成功\n");
printk("申请gpio成功\n");
// 4.注册字符设备驱动
major = register_chrdev(0, CNAME1, &fops);
if (major < 0)
{
printk("register char dev error\n");
ret = -EAGAIN;
goto ERR2;
}
// 5.自动创建设备节点
cls = class_create(THIS_MODULE, CNAME1);
if (IS_ERR(cls))
{
printk("class create error\n");
ret = PTR_ERR(cls);
goto ERR4;
}
dev = device_create(cls, NULL, MKDEV(major, 0), NULL, CNAME1);
if (IS_ERR(dev))
{
printk("device create error\n");
ret = PTR_ERR(dev);
goto ERR5;
}
printk("注册成功\n");
return 0;
ERR5:
class_destroy(cls);
ERR4:
unregister_chrdev(major, CNAME1);
ERR2:
gpio_free(gpiono_dc);
ERR1:
return ret;
}
int platform_gpio_remove(struct platform_device *pdev)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
device_destroy(cls, MKDEV(major, 0));
class_destroy(cls);
unregister_chrdev(major, CNAME1);
gpio_free(gpiono_dc);
return 0;
}
const struct of_device_id oftable[] = {
{
.compatible = "myoled-dc,spi",
},
{/*end*/}};
MODULE_DEVICE_TABLE(of, oftable);
struct platform_driver platform_gpio = {
.probe = platform_gpio_probe,
.remove = platform_gpio_remove,
.driver = {
.name = "myoled-dc,spi",
.of_match_table = oftable,
},
};
module_platform_driver(platform_gpio);
MODULE_LICENSE("GPL");
从上面dc的驱动代码读下来,可以看出其实整体十分简单,核心就是一个gpio_set_value(gpiono_dc, ret)的函数。基础就是使用paltform总线协议匹配设备树,获取设备树的gpio编号。然后在系统上注册字符设备驱动就行了。后面看到野火的教程发现也是这么实现的,唯一的区别就是写到了spi驱动里而已。
3.4 编写驱动的makefile
编写驱动的makefile,编译驱动文件:
KERNELDIR:= /home/luckfox/Luckfox-Pico/luckfox-pico/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin/arm-rockchip830-linux-uclibcgnueabihf- #开发板可安装的驱动
ARCH:=arm
CC:=$(CROSS_COMPILE)gcc
LD:=$(CROSS_COMPILE)ld
obj-m := gpiodevrst.o
module-objs = gpiodevrst.o
PWD := $(shell pwd)
#内核的路径
KERN_DIR=/home/luckfox/Luckfox-Pico/luckfox-pico/sysdrv/source/kernel
#PWD:=$(shell pwd)
#$(shell )在执行makefile的时候起一个终端,在终端上执行pwd命令
#将结果赋值给PWD变量
all:
make -C $(KERN_DIR) M=$(PWD) modules ARCH=arm
#make CROSS_COMPILE=${KERNELDIR} ARCH=arm -C $(KERN_DIR) M=$(pwd) modules
# @#make -C $(KERNELDIR) 进入内核的路径下
# @#进入到上述的路径下之后执行 make M=$(PWD) modules
# @# M=$(PWD)表示只编译当前目录下的模块
clean:
rm -rf *.ko *.mod* *.o *.symvers *.order
# @#make -C $(KERNELDIR) 进入内核的路径下
# @#进入到上述的路径下之后执行 make M=$(PWD) clean
# @# M=$(PWD)表示只清除当前目录下的模块
3.5 安装测试驱动
在测试阶段将编译出的.ko文件直接放置到开发板上,使用insmod安装即可。
insmod xxx.ko
验证驱动和设备树是否成功匹配。最简单的方式是在驱动中填充打印信息,使用dmesg查看。
dmesg //回显打印信息
sudo dmesg -C //清除打印信息
安装驱动后,可以将驱动配置到内核中实现开机自启。
4 总结
至此驱动层的代码也分析完成了,官方使用的sysfs方式操控gpio引脚总感觉很难受,所以我自己写了驱动重新匹配了一下,看着就舒服多了。而且我这个方式逻辑清晰明了,很容易分析明白;不像是sysfs方式,虽然操控简单,但内核的调用流程对于目前的我来说有点黑盒的感觉。需要进一步学习,有哪位知道相关资料的同学看到以后希望可以多多交流。