Luckfox开发板移植0.96寸oled屏幕—驱动篇

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方式,虽然操控简单,但内核的调用流程对于目前的我来说有点黑盒的感觉。需要进一步学习,有哪位知道相关资料的同学看到以后希望可以多多交流。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇