46、linux开发笔记(主线更新)

14 篇文章 1 订阅
9 篇文章 3 订阅

一、安装Vmware16

1.1、Vmware16的下载

1.2、Vmware16的安装

二、安装Ubuntu20

2.1、Ubuntu20的下载

2.2、Ubuntu20的安装

1、新建一个文件夹并把下载好的Ubuntu20镜像放在里面

2、打开Vmware并新建虚拟机

2.3、首次设置root密码

如果想二次修改密码,使用命令:
sudo passwd root    //修改root用户密码
sudo passwd haut    //修改haut用户密码
sudo passwd 用户名  //.......

2.4、安装vmware tools 

工具能实现界面自动分辨率调整、主机与虚拟机之间拖拉文件等功能,是虚拟机必不可少的软件目前新版本的系统镜像都有自带的open-vm-tools工具,老版本没有,总之如果系统已经自带open-vm-tools且可以自动分辨率调整,就不需要安装vmware tools的,否则,需要安装后者

(Ubuntu16、Ubuntu18不带open-vm-tools,需要用方式一进行安装)

方式一、安装vmware自带的vmware-tools工具(此方式将不再适合Ubuntu18以上的版本)

说明:安装vmware-tools后尽量不要更新(能自动调整屏幕就行、传输文件要习惯用SSH传输)  

方式二、安装open-vm-tools代替vmware-tools(适合Ubuntu20及以上且不带open-vm-tools的版本)

# 一、确保当前系统未安装有vmware-tools和open-vm-tools
sudo vmware-uninstall-tools.pl
sudo apt autoremove open-vm-tools
# 二、安装open-vm-tools
sudo apt install open-vm-tools
sudo apt install open-vm-tools-desktop
sudo reboot

三、配置Ubuntu20

3.1、设置虚拟终端光标

3.2、关闭系统自动更新

如果想手动更新系统,可以在终端中执行如下命令:

sudo apt update   //获取最新版本的软件包列表(仅是列表)
sudo apt list --upgradable //列出哪些软件可以被更新(哪些软件出了新版本)
sudo apt upgrade  //下载需要更新的软件包,并更新所有可以被更新的软件

 3.3、安装文本编辑器vim

3.4、安装程序编译器GCC

3.5、安装visual studio code

我们需要安装的插件有下面几个:
1)、 C/C++,必须。
2)、 C/C++ Snippets,代码重构
3)、 C/C++ Advanced Lint,静态检测 。
4)、 Code Runner,代码运行。
5)、 Include AutoComplete,头文件自动包含。
6)、 Rainbow Brackets,彩虹花括号。
7)、 One Dark Pro, VSCode 的主题。
8)、 GBKtoUTF8,将 GBK 转换为 UTF8。
9)、 ARM,ARM 汇编语法高亮显示。
10)、 Chinese(Simplified),中文环境。
11)、 vscode-icons, VSCode 图标插件,主要是资源管理器下各个文件夹的图标。
12)、 compareit,比较插件,可以用于比较两个文件的差异。
13)、 DeviceTree,设备树语法插件。
14)、 TabNine,一款 AI 自动补全插件,强烈推荐,谁用谁知道!

其它:这里重点说一下两个常用的快捷键(可以设置成如下快捷键)
返回/前进:Alt + <- 和Alt + ->

注意:在界面顶部的中间,也有这两个快捷的按钮

vs code的相关配置繁琐而又复杂,相关配置在另一个文章中有详细介绍,这里不再赘述。

3.6、安装SSH远程服务

SSH服务是远程控制服务,允许远程客户端用网络的方式登录主机,安装好之后,我们就可以在 Windwos下使用终端软件登陆到Ubuntu20。(终端软件推荐使用MobaXterm)
若安装出问题请执行命令行:sudo apt-get update  (获取最新的应用列表)

 MobaXterm是一个超级终端工具之一,其他的终端软件也可以,想获取安装包可以取官网下载社区版,或者加入QQ群649692007,在群文件免费获取**版本。

===========================>常用命令列举如下<===========================
sudo apt-get update                  //系统更新
sudo apt-get install openssh-server  //安装SSH服务端
sudo apt-get install openssh-client  //安装SSH客户端
sudo /etc/init.d/ssh start           //启动SSH
sudo /etc/init.d/ssh stop            //关闭SSH
sudo /etc/init.d/ssh status          //查看status状态
sudo /etc/init.d/ssh restart         //重启SSH
ps -e|grep ssh                       //ps -e:查看进程,grep ssh:搜索ssh

 3.7、设置网络静态IP地址

图中的相关命令如下:

1、sudo vim /etc/netplan/01-network-manager-all.yaml

2、sudo systemctl stop NetworkManager

3、sudo systemctl restart networking

=>网络配置文件01-network-manager-all.yaml添加的内容
  ethernets:
    ens33:
      dhcp4: no
      dhcp6: no
      addresses: [192.168.1.116/24]
      optional: true
      gateway4: 192.168.1.1
      nameservers:
        addresses: [192.168.1.1, 114.114.114.114]

四、开发IMX6ULL

        学习嵌入式linux最好选择一款开发板来学习,这样能更好的接触底层硬件的工作原理,目前市面上教学平台的 开发板的CPU型号有IMX6ULL(单核A7+32bit)STM32MP157(双核A7+单核M4+32bits)RK3399(双A72大核+四A53小核+64bit)等。前两款适合底层驱动学习,后者适合上层应用开发。(=>这里我选择imx6ull系列的开发板作为linux的学习平台<=)

4.01、IMX6ULL处理器简介

        NXP的IMX6ULL系列芯片是一款基于ARM Cortex-A7内核的低功耗高性能且低成本的应用处理器,处理器的内部功能框图CPU丝印命名规则如下:

 4.02、IMX6ULL开发板选择

4.03、IMX6ULL启动方式解析

   (1)imx6ull的几种启动方式:

        IMX6ULL支持多种启动方式以及启动设备,比如可以从SD/EMMC、NAND Flash、QSPI Flash等启动。用户可以根据实际情况,选择合适的启动设备。不同的启动方式其启动方式和启动要求也不一样。
        IMX6ULL有四个BOOT模式,这四个BOOT模式由BOOT_MODE[1:0]来控制,也就是BOOT_MODE1 和 BOOT_MODE0 这两 IO,BOOT 模式配置如表所示:

        BOOT_MODE[1:0]分别是两个IO口,这两个IO口接在了拨码开关1-2上,由拨码开关控制。串行下载的意思就是可以通过 USB 或者 UART 将代码下载到板子上的外置存储设备中,我们可以使用 OTG1这个USB口向开发板上的 SD/EMMC、NAND等存储设备下载代码。内部BOOT模式是指CPU执行内部bootROM代码,这段BootROM代码会进行硬件初始化(一部分外设),然后从 boot 设备(就是存放代码的设备、比如 SD/EMMC、 NAND)中将代码拷贝出来复制到指定的 RAM 中,一般是 DDR。
        对于内部BOOT模式启动,BootROM代码都干了什么?:把时钟打开、读取相关引脚电平确定去哪个存储设备读取用户代码、读取存储设备中的用户代码到内部的RAM中去。读取尺寸如图:        以eMMC为例,BootROM程序先把这4KB的数据(IVT表+BootData+DCD表)读取到内存中去,然后根据这4KB的数据来初始化设备,从这4KB数据中,BootROM程序就知道了把真正的bin程序放置到哪里去。
        mfgtools_for_6ULL工具是官方提供的串口烧写软件,其主要工作方式是:首先把一个定制的linux内核加载到DDR中,并运行此内核,然后通过命令把文件放入到指定的位置(包括分区)。把拨码开关设置成USB串行启动,上电后即可打开软件进行下载。前两个开关用来切换下载与运行,后六个开关用来设置用哪个存储设备启动<一般都是前两个开关频繁使用>,如下图示:

   (2)裸机程序与emmc: 

        由(1)可知,要想让芯片自带的bootROM程序准确的启动我们用户的bin程序,就需要在bin程序添加头部信息,故完整可下载的程序为:IVT表+BootData+DCD表+用户bin,uboot本身也是一个裸机程序,也需要添加头部信息后才能下载到emmc flash中。
        首先,关于EMMC FLASH,需要先大概了解emmc的物理分区:

=>分为四个区:Boot Area Partitions、RPMB Partition、General Purpose Partitions和User Data Area。
  Boot Area Partitions:主要用来存放bootloader(分区1和分区2可以看成两个完全一致的分区)。
  RPMB Partition:未使用。
  General Purpose Partitions:未使用。
  User Data Area:主要用来存放linux内核和rootfs

         其次,关于mfgtools_for_6ULL工具,我们来看一下它是如何对emmc进行分区的:
分区文件shell文件:.mfgtools_for_6ULL\Profiles\Linux\OS Firmware\mksdcard.sh

#!/bin/sh
# partition size in MB
BOOT_ROM_SIZE=10

# call sfdisk to create partition table
# destroy the partition table
node=$1
dd if=/dev/zero of=${node} bs=1024 count=1 # 清除前1k数据

sfdisk --force -uM ${node} << EOF
${BOOT_ROM_SIZE},500,0c	 # 分区1从10M开始,大小为500M,0c为文件系统类型的代码
600,,83                  # 分区2从600M开始,大小为剩余空间,83为文件系统类型的代码
EOF 

        可见,分区是从10M开始的(用户可设定,但要跳过前几个分区,从用户区开始),前10M用于存放裸机(uboot)程序,其实就是让裸机程序位于Boot Aera partition,这部分比较安全,多数型号的EMMC(Boot1=4M,Boot2=4M)。 说明:EMMC的前几个区即使未经过分区,内核驱动也是可以识别的,但用户区若不分区则识别不了。

4.04、IMX6ULL开发目录创建

        在Ubuntu20的虚拟机上配置环境,我创建的用户名为haut,在haut的用户目录下创建一个名为itop_imx6ull的目录,之后所有的开发文件和软件都放在此目录下:

4.05、Ubuntu20上配置tftp服务

        tftp是一个简单的基于udp的文本文件传输协议,我们用它将内核镜像和设备树下载到开发板内存中,并指定地址,只在Ubuntu20虚拟机上配置好tftp服务器即可。
        参考4.4,在 /home/haut/itop_imx6ull 目录下创建 tftp 的根目录:
        1、mkdir tftp_boot                                      # 创建tftp服务的根目录
        2、chmod 777 tftp_boot                             # 修改文件夹权限
        3、sudo apt-get install tftp-hpa tftpd-hpa   # 安装tftp服务
        4、sudo vim /etc/default/tftpd-hpa             # 修改tftp配置文件

         5、sudo service tftpd-hpa restart               # 重启tftp服务

4.06、Ubuntu20上配置nfs服务

        我们将开发板的文件系统放在 PC 端(Ubuntu20),开发板的文件系统类型设置为 nfs, 就可以挂载文件系统了。具体步骤(在 Ubuntu 上操作)。
        参考4.4,在 /home/haut/itop_imx6ull 目录下创建 nfs 的根目录:
        1、mkdir nfs_rootfs                                    # 创建nfs服务的根目录
        2、chmod 777 nfs_rootfs                           # 修改文件夹权限
        3、sudo apt-get install nfs-kernel-server   # 安装nfs服务
        4、sudo vim /etc/exports                            # 修改nfs配置文件、在文件末尾添加如下行

        5、 sudo service nfs-kernel-server restart  # 重启tftp服务
               sudo /etc/init.d/nfs-kernel-server restart

rw:读写访问                               |no_wdelay:如果多个用户要写入NFS目录,则立即写入,当使用async时,无需此设置。     
sync:所有数据在请求时写入共享                |no_hide:共享NFS目录的子目录
async:NFS在写入数据前可以相应请求            |subtree_check:如果共享/usr/bin之类的子目录时,强制NFS检查父目录的权限
secure:NFS通过1024以下的安全TCP/IP端口发送   |no_subtree_check:和上面相对,不检查父目录权限
insecure:NFS通过1024以上的端口发送          |all_squash:共享文件的UID和GID映射匿名用户anonymous,适合公用目录。
wdelay:如果多个用户写入NFS目录,则归组写入(默认)|no_all_squash:保留共享文件的UID和GID
root_squash root 用户的所有请求映射成如 anonymous 用户一样的权限 no_root_squas root 用户具有根目录的完全管理访问权限

说明:下面4.7要用到4.5和4.6的配置。

4.07、IMX6ULL用Uboot启动内核 

   (1)使用uboot配置开发板的网络参数:

setenv ethact FEC1               # 网卡选择
setenv ipaddr 192.168.1.15       # 设置网卡IP地址   (视情况而定)
setenv serverip 192.168.1.16     # 设置服务器IP地址 (视情况而定)
setenv netmask 255.255.255.0     # 网络子网掩码
setenv gatewayip 192.168.1.1     # 局域网网关地址
setenv ethaddr 08:07:03:A0:03:02 # 设置网卡MAC地址
saveenv                          # 保存环境变量到Flash
########################### 以上是注释版、完整的命令如下 ############################
setenv ethact FEC1
setenv ipaddr 192.168.1.15
setenv serverip 192.168.1.16
setenv netmask 255.255.255.0
setenv gatewayip 192.168.1.1
setenv ethaddr 08:07:03:A0:03:02
saveenv

   (2)设置传递给内核的参数bootargs:

# =>不同启动方式的bootargs配置不同,以nfs网络文件系统的为例:
setenv bootargs 'console=ttymxc0,115200 # 表示终端为ttymxc0,串口波特率为115200
                 root=/dev/nfs          # 告诉内核以nfs启动
                 rw                     # 文件系统操作权限
                 nfsroot=${serverip}:/home/haut/itop_imx6ull/nfs_rootfs # nfs的根目录的绝对地址 
                 ip=${ipaddr}:${serverip}:${gatewayip}:${netmask}::eth0:off' # 本地地址:服务器地址:网关:子网掩码::eth0:off
saveenv                                 # 保存设置的环境变量
########################### 以上是注释版、完整的命令如下 ############################
setenv bootargs console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.1.16:/home/haut/itop_imx6ull/nfs_rootfs ip=192.168.1.15:192.168.1.16:192.168.1.1:255.255.255.0::eth0:off
saveenv

   (3)配置Uboot启动时自动执行的命令:

# =>不同内核加载方式的bootcmd配置不同,以tftp命令加载内核为例:
setenv bootcmd 'tftp 80800000 zImage;              # 加载kernel到DRAM
                tftp 83800000 topeet_emmc_4_3.dtb; # 加载设备树到DRAM
                bootz 80800000 - 83800000'         # 启动内核<内核地址>+<设备树地址>
saveenv                                            # 保存设置的环境变量
########################### 以上是注释版、完整的命令如下 ############################
setenv bootcmd 'tftp 80800000 zImage; tftp 83800000 imx6ull_itop_emmc_4_3.dtb; bootz 80800000 - 83800000'
saveenv

    (@)内核启动说明与总结:

        加载内核和设备树到DDR是很简单的操作,也好理解。最重要的是最后一步的bootz命令,传递bootargs参数给内核也是靠它来完成的,相对较复杂,bootz的执行流程图如下:

   启动流程总结:将内核和设备树移到DDR中===>校验内核===>传递参数===>跳转执行内核

4.08、Ubuntu20上安装交叉编译器

1、 arm 表示这是编译 arm 架构代码的编译器。
2、 linux 表示运行在 linux 环境下。
3、 gnueabihf 表示嵌入式二进制接口。
4、 gcc 表示是 gcc 工具。

4.09、Uboot2016-NXP启动流程

   4.09.1、Cortex-A7 MPCore

    以前的ARM处理器有7种运行模型:User、FIQ、IRQ、Supervisor(SVC)、Abort、Undef和System,其中
User是非特权模式,其余6中都是特权模式。但新的Cortex-A架构加入了TrustZone安全扩展,所以就新加了一种
运行模式:Monitor,新的处理器架构还支持虚拟化扩展,因此又加入了另一个运行模式:Hyp,所以Cortex-A7处理
器有9种处理模式:
---------------------------------------------------------------------------------
    模式                   描述
---------------------------------------------------------------------------------
    User(USR)              用户模式,非特权模式,大部分程序运行的时候就处于此模式。
    FIQ                    快速中断模式,进入 FIQ 中断异常
    IRQ                    一般中断模式。
    Supervisor(SVC)        超级管理员模式,特权模式,供操作系统使用。
    Monitor(MON)           监视模式?这个模式用于安全扩展模式。
    Abort(ABT)             数据访问终止模式,用于虚拟存储以及存储保护。
    Hyp(HYP)               超级监视模式?用于虚拟化扩展。
    Undef(UND)             未定义指令终止模式。
    System(SYS)            系统模式,用于运行特权级的操作系统任务
---------------------------------------------------------------------------------
    除了 User(USR)用户模式以外,其它 8 种运行模式都是特权模式。这几个运行模式可以通过软件进行任意
切换,也可以通过中断或者异常来进行切换。大多数的程序都运行在用户模式,用户模式下是不能访问系统所有资
源的,有些资源是受限的,要想访问这些受限的资源就必须进行模式切换。但是用户模式是不能直接进行切换的,
用户模式下需要借助异常来完成模式切换,当要切换模式的时候,应用程序可以产生异常,在异常的处理过程中完
成处理器模式切换。
    当中断或者异常发生以后,处理器就会进入到相应的异常模式种,每一种模式都有一组寄存器供异常处理程
序使用,这样的目的是为了保证在进入异常模式以后,用户模式下的寄存器不会被破坏。
    如果学过 STM32 和 UCOS、 FreeRTOS 就会知道, STM32 只有两种运行模式,特权模式和非特权模式,
但是 Cortex-A 就有 9 种运行模式。

   4.09.2、Cortex-A7寄存器组

        Cortex-A7 有 9 种运行模式,每一种运行模式都有一组与之对应的寄存器组。每一种模式可见的寄存器包括 15 个通用寄存器(R0~R14)、一两个程序状态寄存器和一个程序计数器 PC。在这些寄存器中,有些是所有模式所共用的同一个物理寄存器,有一些是各模式自己所独立拥有的:    

        图中浅色字体的是与 User 模式所共有的寄存器,蓝绿色背景的是各个模式所独有的寄存器。可以看出,在所有的模式中,低寄存器组(R0~R7)是共享同一组物理寄存器的,只是一些高寄存器组在不同的模式有自己独有的寄存器,比如 FIQ 模式下 R8~R14 是独立的物理寄存器。假如某个程序在 FIQ 模式下访问 R13 寄存器,那它实际访问的是寄存器 R13_fiq,如果程序处于 SVC 模式下访问 R13 寄存器,那它实际访问的是寄存器 R13_svc。下面介绍几个重要寄存器:

SP => 堆栈指针  =>  指向堆栈栈顶(堆栈可能向上增长,也可能向下增长)
LR => 链接寄存器 => 如果使用BL或者BLX来调用子函数的话,R14(LR)被设置成该子函数的返回地址
PC => 程序计数器 => (PC)值=当前执行的程序位置+8个字节

CPSR => 当前程序状体寄存器  
SPSR => 备份程序状态寄存器 

   4.09.3、Cortex-A7常用汇编

<1>使用处理器做的最多事情就是在处理器内部来回的传递数据,常见的操作有:
①、将数据从一个寄存器传递到另外一个寄存器。
②、将数据从一个寄存器传递到特殊寄存器,如 CPSR 和 SPSR 寄存器。
③、将立即数传递到寄存器
数据传输常用的指令有三个: MOV、MRS和MSR。
指令   目的   源     描述
MOV    R0,   R1     将R1里面的数据复制到 R0 中,R0=R1。
MOV    R0,   #0x12  将立即数0x12传递给 R0 中,R0=0x12。
MRS    R0,   CPSR   将特殊寄存器 CPSR 里面的数据复制到 R0 中。(涉及特殊寄存器)
MSR    CPSR, R1     将 R1 里面的数据复制到特殊寄存器 CPSR 里中。(涉及特殊寄存器)

<2>ARM 不能直接访问存储器,比如 RAM 中的数据,需要借助存储器访问指令,一般先将要配置的值
写入到 Rx(x=0~12)寄存器中,然后借助存储器访问指令将 Rx 中的数据写入到存储器中,读取时过程相反
指令   目的   源              描述
LDR    Rd,   [Rn, #offset]   从存储器 Rn+offset 的位置读取数据存放到 Rd 中。
STR    Rd,   [Rn, #offset]   将 Rd 中的数据写入到存储器中的 Rn+offset 位置。
示例1:
LDR R0, =0X0209C004 @将寄存器地址 0X0209C004 加载到 R0 中,即 R0=0X0209C004
LDR R1, [R0] @读取地址 0X0209C004 中的数据到 R1 寄存器中
示例2:
LDR R0, =0X0209C004 @(伪指令)将寄存器地址 0X0209C004 加载到 R0 中,即 R0=0X0209C004
LDR R1, =0X20000002 @R1 保存要写入到寄存器的值,即 R1=0X20000002
STR R1, [R0] @将 R1 中的值写入到 R0 中所保存的地址中
LDR 和 STR 都是按照字进行读取和写入的,也就是操作的 32 位数据,如果要按照字节、半字进行操作的话可以在指令“LDR”后面加上 B 或 H,比如按字节操作的指令就是 LDRB 和STRB,按半字操作的指令就是 LDRH 和STRH。

<3>我们通常会在A函数中调用B函数,当B函数执行完以后再回到A函数继续执行.要想再跳回A函数以后
代码能够接着正常运行,那就必须在跳到B函数之前将当前处理器状态保存起来(就是保存 R0~R15这些
寄存器值),当B函数执行完成以后再用前面保存的寄存器值恢复 R0~R15 即可。保存 R0~R15 寄存器
的操作就叫做现场保护,恢复 R0~R15 寄存器的操作就叫做恢复现场。在进行现场保护的时候需要进
行压栈(入栈)操作,恢复现场就要进行出栈操作。压栈的指令为 PUSH,出栈的指令为POP,PUSH 和POP 
是一种多存储和多加载指令,即可以一次操作多个寄存器数据,他们利用当前的栈指针 SP 来生成地址.
PUSH <reg list> @ 将寄存器列表存入栈中。
POP <reg list>  @ 从栈中恢复寄存器列表。
示例1:XX指下一个地址
PUSH {R0~R3, R12} @将 R0~R3 和 R12压栈  =>XX、R12、R3、R2、R1、R0、XX(<-SP)
PUSH {LR}         @将 LR 进行压栈       =>XX、R12、R3、R2、R1、R0、LR、XX(<-SP)
======
POP {LR}          @先恢复 LR            =>XX、R12、R3、R2、R1、R0、XX(<-SP)
POP {R0~R3,R12}   @在恢复 R0~R3,R12     =>XX(<-SP)

<4>算术运算指令,汇编中也可以进行算术运算,比如加减乘除。
指令                 计算公式                  备注
ADD Rd, Rn, Rm       Rd = Rn + Rm             加法运算,指令为 ADD
ADD Rd, Rn, #immed   Rd = Rn + #immed         加法运算,指令为 ADD
------------------------------------------------------------------------
ADC Rd, Rn, Rm       Rd = Rn + Rm + 进位      带进位的加法运算,指令为 ADC
ADC Rd, Rn, #immed   Rd = Rn + #immed + 进位  带进位的加法运算,指令为 ADC
------------------------------------------------------------------------
SUB Rd, Rn, Rm       Rd = Rn - Rm             减法
SUB Rd, #immed       Rd = Rd - #immed         减法
SUB Rd, Rn, #immed   Rd = Rn - #immed         减法
------------------------------------------------------------------------
SBC Rd, Rn, #immed   Rd = Rn - #immed - 借位  带借位的减法
SBC Rd, Rn ,Rm       Rd = Rn - Rm - 借位      带借位的减法
------------------------------------------------------------------------
MUL Rd, Rn, Rm       Rd = Rn * Rm             乘法(32 位)
UDIV Rd, Rn, Rm      Rd = Rn / Rm             无符号除法
SDIV Rd, Rn, Rm      Rd = Rn / Rm             有符号除法
------------------------------------------------------------------------
在嵌入式开发中最常会用的就是加减指令,乘除基本用不到。

<5>逻辑运算指令,C 语言进行 CPU 寄存器配置的时候常常需要用到逻辑运算符号,比如“&”、“|”等
使用汇编语言的时候也可以使用逻辑运算指令。
指令                 计算公式                  备注
AND Rd, Rn           Rd = Rd &Rn
AND Rd, Rn, #immed   Rd = Rn &#immed          按位与
AND Rd, Rn, Rm       Rd = Rn & Rm
------------------------------------------------------------------------
ORR Rd, Rn           Rd = Rd | Rn
ORR Rd, Rn, #immed   Rd = Rn | #immed         按位或
ORR Rd, Rn, Rm       Rd = Rn | Rm
------------------------------------------------------------------------
BIC Rd, Rn           Rd = Rd & (~Rn)
BIC Rd, Rn, #immed   Rd = Rn & (~#immed)      位清除
BIC Rd, Rn , Rm      Rd = Rn & (~Rm)
------------------------------------------------------------------------
ORN Rd, Rn, #immed   Rd = Rn | (#immed)       按位或非
ORN Rd, Rn, Rm       Rd = Rn | (Rm)
------------------------------------------------------------------------
EOR Rd, Rn           Rd = Rd ^ Rn
EOR Rd, Rn, #immed   Rd = Rn ^ #immed         按位异或
EOR Rd, Rn, Rm       Rd = Rn ^ Rm
------------------------------------------------------------------------
要 想 详 细 的 学 习 ARM 的 所 有 指 令 请 参 考 《 ARM ArchitectureReference Manual 
ARMv7-A and ARMv7-R edition.pdf》和《ARM Cortex-A(armV7)编 程手册 V4.0.pdf》这两份文档。

   4.09.4、 位置相关/无关码

<1>位置无关码
   B、BL、MOV、ADR、ADD、SUB、...
<2>位置相关码
   LDR、STR、...

<-1>何为位置无关码
-------------------------------------------------------------------------------------------
start.s文件内容如下:
_start:            # _start是一个链接的地址,在链接时确定,之后就固定了
    .....
    b _start;      # 跳转到链接地址_start(无论程序所处位置如何,此指令效果不变,故为位置无关码)     
-------------------------------------------------------------------------------------------
<-2>这里单独讲解一个指令ADR
示例:
adr r0, _start;    # 伪指令,根据当前指令的链接地址与_start的差,计算_start的运行地址,并存r0中

说明:ADR指令多用于代码重定位,用于代码运行过程中,获取某个标号当前所在的地址(运行地址)。

   4.09.5、汇编代码示例(重点)

@ =>代码段
.text
.global _start                      @ .global表示_start是一个全局符号,会在链接器链接时用到 
_start:                             @ 标签_start,汇编程序的默认入口是_start 
    ldr sp, =(0x80000000+0x100000)  @ 设置堆栈
    b main                          @ 跳转到main函数
    b .                             @ 原地循环
@ =>初始化的数据段
.data
st:
    .long 0x80809090

    .long 0xA0A0D0D0
@ =>未初始化的数据段    
.bss 
    .long 0x0
    .long 0x0

@ =>定义新段(只读数据段)
.section .rodata        @ 自定义一个段,段名为.rodata
    .align 2,0x00       @ 2^2=4字节对齐,空隙用0x00填充
    .long 0x000A000B
    .align 2,0x00       @ 2^2=4字节对齐,空隙用0x00填充
    .byte 0xAB
    .align 4,0x00       @ 2^4=16字节对齐,空隙用0x00填充
    .byte 0xAC
    .align 4,0x00       @ 2^4=16字节对齐,空隙用0x00填充
    .byte 0xDD
.end  @ 汇编代码结束标志,之后的所有代码将被忽略

@ .text、.data、.bss都是汇编伪指令
@ 汇编并没有.rodata的伪指令,需要自己定义
@ 4字节对齐含义:代码放在(0、4、8、C、0、4..)这样地址是4的倍数的位置

   4.09.6、程序链接脚本示例(重点)

<1>代码的几种地址详解
    链接地址:程序在链接时指定的地址,即代码编写者设定的代码的目标地址。
    运行地址:运行地址是代码运行时所处的地址。注意,运行地址需要等于链接地址,不然代码可能出错。

    加载地址:程序代码在bin文件中的地址。
    存储地址:程序代码存储的地址,即bin文件被烧录到存储器的地址。

    注意:虽然链接地址程序员设置的目标运行地址,加载地址程序员设置的目标存储地址,但这并不意味着
就一定链接地址=运行地址,加载地址=存储地址,具体相不相等取决于你是怎么烧录和重定位代码的。

<2>一个基于imx6ull的链接脚本示例
SECTIONS 
{
    . = 0x87800000;          # 设置初始链接地址
    . = ALIGN(4);            # 设置地址4字节对齐
    __text_start = .;        # __text_start = 代码段链接地址首地址
    .text : AT(0)
    {
        start.o (.text)      # 文件名 (.text),意为把文件中的代码段放在此处
        main.o (.text)
        *(.text)
    }
    __text_end = .;          # __text_end = 代码段链接地址末地址
    . = ALIGN(4);
    .rodata : { *(.rodata) }
    . = ALIGN(4);
    .data :
    { 
        *(.data) 
    }
    . = ALIGN(4);
    __bss_start = .;
    .bss : { *(.bss)  *(.COMMON)}
    __bss_end = .;

    __data_linkaddr = ADDR(.data);     # __data_linkaddr = 数据段的链接地址首地址
    __data_sizeof = SIZEOF(.data);     # __data_sizeof = 数据段的长度
    __data_loadaddr = LOADADDR(.data); # __data_loadaddr = 数据段的首加载地址首地址
}

   4.09.7、__attribute__() 使用方式(重点)

        GNU C 的一大特色就是__attribute__ 机制。__attribute__ 可以设置函数属性(Function Attribute )、变量属性(Variable Attribute )和类型属性(Type Attribute )。
        =>这里需要注意几个点<重点>:
         <1>、__attribute__() 是GNU C的扩展语法。
         <2>、C语言中的变量名会变成汇编的标号,变量值会变为汇编中的数据。
         <3>、C语言中也可以定义一个没有变量值的变量名,最后会变为一个汇编的标号

         <4>、C语言中的变量、函数所处于的段、所采用的对齐方式等是由编译器自行决定的。
         <5>、<4>是一般默认的情况,当然用户也可以使用__attribute__()自定义变量等的相关属性

        =>使用示例如下:

#include <stdio.h>
/* 定义:变量名+变量值 */
long i1 __attribute__((section(".__vec"),aligned(4))) = 0x000000AA;  /* 用__attribute__设置变量ℹ1放在.__vec段,并以4字节对齐 */
long i2 __attribute__((section(".__vec"),aligned(16))) = 0x000000BB; /* 用__attribute__设置变量ℹ2放在.__vec段,并以16字节对齐 */
int ax = 6699;                                                       /* 变量ax未用_attribute__进行属性设置,默认放在.data段 */
/*    定义:仅变量名    */
char _st[0] __attribute__((section(".st"))) ;   /* 定义一个标号,它没有占空间,放置在.st段(标号必须被放入到链接脚本中某位置,否则没有意义) */
                                                /* 如果你不把_st加入到链接脚本中,那它在链接过程中会被随机指定,也就失去了存在的意义 */
/* 函数 */
int main(int argc,char* argv[]){

    return 0;
}
/* 执行如下指令: */
    arm-linux-gnueabihf-gcc -nostdlib -c text.c -o text.o        //编译但不链接
    arm-linux-gnueabihf-ld text.o -Ttext 0X87800000 -o text.elf  //链接
    arm-linux-gnueabihf-objdump -D text.elf                      //反汇编
/* 反汇编显示如下: */
ext.elf:     文件格式 elf32-littlearm


Disassembly of section .text:

87800000 <main>:
87800000:       b480            push    {r7}
87800002:       b083            sub     sp, #12
87800004:       af00            add     r7, sp, #0
87800006:       6078            str     r0, [r7, #4]
87800008:       6039            str     r1, [r7, #0]
8780000a:       2300            movs    r3, #0
8780000c:       4618            mov     r0, r3
8780000e:       370c            adds    r7, #12
87800010:       46bd            mov     sp, r7
87800012:       f85d 7b04       ldr.w   r7, [sp], #4
87800016:       4770            bx      lr

Disassembly of section .data:

87810018 <ax>:
87810018:       00001a2b        andeq   r1, r0, fp, lsr #20

Disassembly of section .__vec:

87810020 <i1>:
87810020:       000000aa        andeq   r0, r0, sl, lsr #1
        ...

87810030 <i2>:
87810030:       000000bb        strheq  r0, [r0], -fp
        ...

Disassembly of section .comment:
................................

# 说明1:因为没有链接过程,所以所有的段都是从0开始的。
# 说明2:0长数组不是c语言的里面东西,它是GNC C的扩展。

   4.09.8、Uboot2016-nxp程序起点

        Uboot或者C程序的"入口"是由链接脚本决定的,如果没有编译过 uboot 的话,链接脚本为arch/arm/cpu/u-boot.lds。但是这个不是最终使用的链接脚本,最终的链接脚本是在这个链接脚本的基础上生成的。编译一下 uboot,编译完成以后就会在 uboot 根目录下生成 u-boot.lds 文件,打开此文件可以看到如下内容:

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
 . = 0x00000000;
 . = ALIGN(4);
 .text :
 {
  *(.__image_copy_start)
  *(.vectors)
  arch/arm/cpu/armv7/start.o (.text*)
  *(.text*)
 }
 /* 由于文件太长,这里省略大部分,具体可以自行查看此文件... ... ... */
}

      第三行的 ENTRY(_start) 指定了程序的入口地址,在Uboot源码中搜索标号 _start: 。这里需要注意的是,最好打开 区分大小写 和 全字匹配 。搜索结果如下,_start 这个标号在 arch/arm/lib/vectors.S这个文件里定义,这就是Uboot程序的" 入口 "。(注意 .section ".vectors","ax" 这句,它是指明了此代码所处的段,而不是由编译器默认分配的)

   4.09.9、Uboot2016-nxp编译控制分析

         在linux内核里面,采用menuconfig机制来配置哪些文件将被编译,其原理就利用图形界面产生环境变量,所有的环境变量配置结果都将被写入到主目录的.config文件中。如下图所示:


   4.09.10、Uboot2016-nxp整体启动流程分析

4.10、Uboot2016-NXP代码编译

        这里以NXP官方提供的Uboot源码为例,这是NXP针对IMX6ULL芯片做过适配的。

   (1)创建Uboot编译目录:

   (2)Uboot2016源码文件介绍:
        0、arch/arm/lib/vectors.S             # 存放的向量表,程序入口 _start: 也此文件中
        1、arch/arm/cpu/armv7/start.S    # _start 执行的第1句代码就是跳到此文件的中 reset: 处
        2、arch/arm/include/asm             # 存放arch/cpu文件夹下的源文件所对应的头文件
        3、board/freescale/***/***.c           # 板级文件夹(***.c文件)
        4、include/configs/ ***.h                # 板级头件夹(***.h文件)
   (3)添加开发板

   (4)配置、编译Uboot

4.11、Linux-V4.1.15-NXP代码编译

   (1)配置编译器


   (2)添加开发板

   (3)配置、编译内核

设备树相关:(dtc是设备树的编译器)
    .dts   # 设备树源文件
    .dtsi  # 设备树头文件
    .dtb   # 设备树编译后的二进制文件
设备树编译命令1:make dtbs             //编译所有的设备树
设备树编译命令2:make yourDIR/xxx.dtb  //编译指定的设备树

4.12、Linux-V4.1.15-内核启动分析
 

start_kernel(void);
	//设置架构(根据设备树选择使用哪个机器的初始化代码)
	setup_arch(&command_line); 
		//根据设备树选择机器,并把机器的初始化函数赋值给mdesc
		//每个机器的初始化信息使用DT_MACHINE_START宏声明,放在.arch.info.init段中
		const struct machine_desc *mdesc;
		mdesc = setup_machine_fdt(__atags_pointer);//设置机器的设备树(根据设备树匹配机器)
			of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);//设备树与机器匹配
		machine_desc = mdesc;  //保存为全局变量machine_desc       
		....
		unflatten_device_tree(); //解析二进制设备树,解析为节点类型为device_node的树形结构
			__unflatten_device_tree(initial_boot_params, &of_root, early_init_dt_alloc_memory_arch);
				...
				nflatten_dt_node(blob, mem, &start, NULL, mynodes, 0, false);
				...
		....
	...
	//剩余初始化
	rest_init(); 
		...
		kernel_thread(kernel_init, NULL, CLONE_FS); //创建一个内核线程,线程函数为kernel_init
			kernel_init_freeable();
				do_basic_setup();
					...
					driver_init();
						...
						platform_bus_init(); //(1)=>平台设备总线初始化
							bus_register(&platform_bus_type);
						...
					...
					do_initcalls();
						for(do_initcall_level(level=0->7)){
							//__initcall0_start:__initcall7_start为几个初始化段的起始地址
							//别名为"early"、"core"、"postcore"、"arch"、"subsys"、"fs"、"device"、"late",
							//使用相应的宏,定义的函数,就会被放在相应的段中,然后通过指针依次调用
							//例如:arch_initcall(customize_machine); 把customize_machine函数放在段__initcall3中
							for (fn = initcall_levels[level=0]; fn < initcall_levels[0+1]; fn++)
								do_one_initcall(*fn); //调用__initcall0_start->__initcall0_end里的所有函数
							...
							for (fn = initcall_levels[level=2]; fn < initcall_levels[0+1]; fn++)//遍历所有被宏postcore_initcall()声明的函数
								do_one_initcall(*fn); //(2)=>当*fn = i2c_init()时,为i2c总线(也叫子系统)初始化
									bus_register(&i2c_bus_type);		
							...
							for (fn = initcall_levels[level=3]; fn < initcall_levels[3+1]; fn++){ //遍历所有被宏arch_initcall()声明的函数
								//调用__initcall3_start->__initcall3_end里的所有函数(arch),其中有一个函数customize_machine()
								do_one_initcall(*fn); //当*fn = customize_machine()时,执行如下:								
									//machine_desc在setup_arch()函数中已被赋值
									//以imx6ull为例,搜索找到DT_MACHINE_START(IMX6UL,xxx) 
									//可知,这里调用的是imx6ul_init_machine函数
									machine_desc->init_machine(){ //imx6ul_init_machine
										//(3)=>platform:平台,populate:填充=>将设备树中的设备描述转换为相应的内核设备(platform_device)并进行注册
										of_platform_populate(root=NULL, matches=of_default_bus_match_table, lookup=NULL, parent=NULL);
											root = of_find_node_by_path("/");
											for_each_child_of_node(root, child) //遍历root的第一层子节点child = root->child1-childn
											{ 
												of_platform_bus_create(bus=child, matches, lookup, parent, true) //把当前节点当成总线处理
												{ 
													struct platform_device *dev;
													dev = of_platform_device_create_pdata(bus, bus_id=NULL, platform_data=NULL, parent);
													if (!dev || !of_match_node(matches, bus)) //如果创建失败,或者当前节点不与of_default_bus_match_table匹配
														return 0;  //则停止对其子节点的遍历,递归的终止条件
													for_each_child_of_node(bus, child) { //递归遍历根深的子节点
														rc = of_platform_bus_create(child, matches, lookup, &dev->dev, strict);
													}
												}
											}
											//从root开始遍历,创建platform_device,直到与of_default_bus_match_table不匹配为截止
											//注1:是先创建再判断,第一出现不匹配的那个节点也会被创建platform_device
											//注2:i2c和spi等总线节点(控制器)会被解析platform_device,但其子节点信息不会在此处解析,因为i2c和spi等总线节点的
											//compatible属性与of_default_bus_match_table不匹配,所以解析到总线节点处就不会往下解析了。(注意此时还没有platform_driver)
									}											
								
							}		
							for (fn = initcall_levels[level=4]; fn < initcall_levels[4+1]; fn++){ //遍历所有被宏subsys_initcall()声明的函数
								//调用__initcall4_start->__initcall4_end里的所有函数(subsys),其中有一个函数i2c_adap_imx_init
								do_one_initcall(*fn); //当fn = i2c_adap_imx_init()时,执行如下:(struct platform_driver i2c_imx_driver)
									platform_driver_register(drv=&i2c_imx_driver){ //注册i2c总线(也叫子系统)的platform_driver,(i2c控制器信息已被解析成platform_device)
										__platform_driver_register(drv, THIS_MODULE);
											driver_register(&drv->driver);
												i2c_imx_probe(struct platform_device *pdev); //注册成功则调用驱动的probe函数
													i2c_add_numbered_adapter(&i2c_imx->adapter); //注册带编号的i2c适配器
														i2c_add_adapter(adap);//发现无编号,则调用自动分配编号的函数注册适配器
															__i2c_add_numbered_adapter(adapter);
																id = idr_alloc(&i2c_adapter_idr, adap, adap->nr, adap->nr+1, GFP_KERNEL); //成功分配idr
																i2c_register_adapter(adap); //=>注册适配器
																		adap->dev.bus = &i2c_bus_type;
																		adap->dev.type = &i2c_adapter_type;
																		res = device_register(&adap->dev);
																		of_i2c_register_devices(adap); //(4)=>解析i2c控制器的子节点,并再i2c上注册成设备
																		acpi_i2c_register_devices(adap);
																		acpi_i2c_install_space_handler(adap);
									}
							}
							...			
						}
		...
		kernel_thread(kthreadd, NULL, CLONE_FS|CLONE_FILES);

五、Linux驱动开发

5.01、Linux驱动前言

   (1)、linux下的驱动开发分为三大类
                <1>字符设备驱动   # 使用最多的
                <2>块设备驱动       # 存储设备
                <3>网络设备驱动   # 网络设备
        一个设备并不是说一定只属于某一个类型,比如USB-WIFI、SDIO-WIFI,属于网络设备驱动,因为他又有USB和SDIO,因此也属于字符设备驱动。
   (2)、驱动就是获取外设、或者传感器数据,控制外设。数据会提交给应用程序。
   (3)、Linux操作系统内核和驱动程序运行在内核空间,应用程序运行在用户空间

        其中关于 C 库以及如何通过系统调用“陷入” 到内核空间这个我们不用去管,我们重点关注的是应用程序和具体的驱动,应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中有个叫做file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合,内容如下所示:

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iterate) (struct file *, struct dir_context *);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*mremap)(struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
};
<*>简单介绍一下 file_operation 结构体中比较重要的、常用的函数:
第 2 行, owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
第 3 行, llseek 函数用于修改文件当前的读写位置。
第 4 行, read 函数用于读取设备文件。
第 5 行, write 函数用于向设备文件写入(发送)数据。
第 9 行, poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
第 10 行, unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
第 11 行, compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。
第 12 行, mmap 函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
第 13 行, open 函数用于打开设备文件。
第 15 行, release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
第 16 行, fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
第 17 行, aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据。
<*>在字符设备驱动开发中最常用的就是上面这些函数,关于其他的函数大家可以查阅相关文档。我们在字符设备驱动开发中最主要的工作就是实现上面这些函数,不一定全部都要实现,
但是像 open、 release、 write、 read 等都是需要实现的,当然了,具体需要实现哪些函数还是要
看具体的驱动要求。

   (4)、Linux驱动程序可以编译到内核里面,也就是zImage,也可以编译成模块,即.ko文件。
   (5)、模块有加载和卸载两种操作,编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下:
        module_init(xxx_init);        # 注册模块加载函数
        module_exit(xxx_exit);      # 注册模块卸载函数
        module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用“insmod”命令加载驱动的时候, xxx_init 这个函数就会被调用。 module_exit()函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候xxx_exit 函数就会被调用。
   (6)、模块编译好之后生成xxx.ko文件,把其拷贝到文件系统里面,使用如下命令进行安装操作:
        方式一: insmod xxx.ko    # 加载驱动                 方式二: modprobe xxx.ko    # 加载驱动
                     rmmod xxx.ko    # 卸载驱动                              modprobe -r xxx.ko    # 卸载驱动   
        insmod命令不能解决模块的依赖关系,比如drv.ko依赖first.ko这个模块,就必须先使用insmod命令加first.ko这个模块,然后再加载drv.ko这个模块。但是modprobe就不会存在这个题,modprobe会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,因此modprobe命令相比insmod要智能一些(注意modprobe -r 也会卸载依赖,但rmmod不会卸载依赖)。modprobe 命令主要智能在提供了模块的依赖性分析、错误检查、错误报告等功能,推荐使用 modprobe 命令来加载驱动。modprobe 命令默认会去/lib/modules/<kernel-version>目录中查找模块,比如使用的 Linux kernel 的版本号为 4.1.15,自己构建的文件系统一般没有这个目录,需要手动创建
        对于一个新的模块,使用modprobe加载的时候,需要先调用一下depmod 命令
        <&&>:方式一、二的加载方式都属于手动加载,可以用lsmod查看手动加载了哪些模块(lsmod不能查看编译到内核中的驱动模块)
   (7)、驱动加载后,常用的查看命令
        lsmod                     # 显示当前手动加载的驱动(重启后会消失)
        cat /proc/devices    # 查看注册了哪些设备(主设备号   设备名)
        ls -lah /dev              # 查看设备文件(节点),旧版本驱动需手动创建,新版本可在代码中创建
   (8)、关于设备号
        Linux将设备号分为两部分:主设备号和次设备号,主设备号用高12位,次设备号用低20位。系统中主设备号范围为0~4095。在文件 include/linux/kdev_t.h 中提供了几个关于设备号的操作函数(本质是宏),如下所示:

=>>include/linux/kdev_t.h

6  #define MINORBITS 20
7  #define MINORMASK ((1U << MINORBITS) - 1)
8 
9  #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
10 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
11 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

第 6 行,宏 MINORBITS 表示次设备号位数,一共是 20 位。
第 7 行,宏 MINORMASK 表示次设备号掩码。
第 9 行,宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
第 10 行,宏 MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
第 11 行,宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。

5.02、Linux驱动模板(旧)

   (1)、创建vscode工程、添加内核头文件

   (2)、编写驱动源文件 chrdevbase.c

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/ide.h>

#define CHRDEVBASE_MAJOR  200		   /* 主设备号 */
#define CHRDEVBASE_NAME	  "chrdevbase" /* 设备名   */

static char readbuf[100];	/* 读缓冲 */
static char writebuf[100];	/* 写缓冲 */
static char kerneldata[] = {"kernel data!"};
/*
 * @description	: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 		一般在open的时候将private_data指向设备结构体。
 * @return 	: 0 成功;其他 失败
 */
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
	printk("=>chrdevbase open!\r\n");
	return 0;
}

/*
 * @description	: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 	: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue = 0;	
	/* 向用户空间发送数据 */
	memcpy(readbuf, kerneldata, sizeof(kerneldata));  /* 把kerneldata拷贝到readbuf中 */
	retvalue = copy_to_user(buf, readbuf, cnt);       /* 注意:readbuf是内核空间的内存,用户不能直接访问,需使用copy_to_user()函数*/
	if(retvalue == 0){
		printk("=>kernel senddata ok!\n");
	}else{
		printk("=>kernel senddata failed!\n");
	}
	printk("=>chrdevbase read!\n");
	return 0;
}
/*
 * @description	: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue = 0;
	/* 接收用户空间传递给内核的数据并且打印出来 */
	retvalue = copy_from_user(writebuf, buf, cnt);
	if(retvalue == 0){
		printk("=>kernel recevdata:%s\r\n", writebuf);
	}else{
		printk("=>kernel recevdata failed!\r\n");
	}	
	printk("=>chrdevbase write!\r\n");
	return 0;
}

/*
 * @description	: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 	: 0 成功;其他 失败
 */
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
	//printk("=>chrdevbase release!\n");  //此处的printk和app中的printf冲突
	return 0;
}

/*
 * 设备操作函数结构体
 */
static struct file_operations chrdevbase_fops = {
	.owner = THIS_MODULE,	
	.open = chrdevbase_open,
	.read = chrdevbase_read,
	.write = chrdevbase_write,
	.release = chrdevbase_release,
};

/*
 * @description	: 驱动入口函数 
 * @param 		: 无
 * @return 		: 0 成功;其他 失败
 */
static int __init chrdevbase_init(void)
{
	int retvalue = 0;
	retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);  /* 注册一个字符设备 (要确保CHRDEVBASE_MAJOR没有被占用)*/
	if(retvalue < 0){
		printk("=>chrdevbase driver register failed!\r\n");
	}
	printk("=>chrdevbase init!\n");
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit chrdevbase_exit(void)
{
	unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME); /* 注销一个字符设备 */
	printk("=>chrdevbase exit!\r\n");
}

/* 指定驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
/* 设置驱动的LICENSE和作者信息 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

   (3)、编写编译驱动使用的Makefile文件 Makefile

KERNELDIR := /home/haut/itop_imx6ull/software/Linux_Source_nxpV4.1.15   # 内核路径
CURRENT_PATH := $(shell pwd)  # 当前路径

obj-m := chrdevbase.o

build: kernel_modules

kernel_modules:  # 执行内核路径下的 Makefile 编译此文件
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

        执行make,就可以编译 chrdevbase.c 并生成二进制文件 chrdevbase.ko
   (4)、编写测试App程序 chrdevbaseApp.c

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

static char usrdata[] = {"usr data!"};

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 * ./chrdevbaseApp /dev/chrdevbase 1  # 从驱动读数据 
 * ./chrdevbaseApp /dev/chrdevbase 2  # 向驱动写数据
 */
int main(int argc, char *argv[])
{
	int fd, retvalue;
	char *filename;
	char readbuf[100], writebuf[100];

	if(argc != 3){
		printf("Error Usage!\r\n");
		return -1;
	}
	filename = argv[1];

	/* 打开驱动文件 */
	fd  = open(filename, O_RDWR);
	if(fd < 0){
		printf("Can't open file %s\r\n", filename);
		return -1;
	}
	 /* 从驱动文件读取数据 */
	if(atoi(argv[2]) == 1){
		retvalue = read(fd, readbuf, 50);
		if(retvalue < 0){
			printf("read file %s failed!\r\n", filename);
		}else{
			/*  读取成功,打印出读取成功的数据 */
			printf("read data:%s\r\n",readbuf);
		}
	}
	/* 向设备驱动写数据 */
	if(atoi(argv[2]) == 2){  
		memcpy(writebuf, usrdata, sizeof(usrdata));
		retvalue = write(fd, writebuf, 50);
		if(retvalue < 0){
			printf("write file %s failed!\r\n", filename);
		}
	}
	/* 关闭设备 */
	retvalue = close(fd);
	if(retvalue < 0){
		printf("Can't close file %s\r\n", filename);
		return -1;
	}
	return 0;
}

        执行arm-linux-gnueabihf-gcc chrdevbaseApp.c  -o chrdevbaseApp
        执行 file chrdevbaseApp 可以查看文件的属性,和平台信息。

   (5)、测试
        <1> 加载驱动:
                
depmod
                modprobe chrdevbase.ko
        <2> 创建设备节点:
               
mknod /dev/chrdevbase c 200 0      # c:字符设备,200:主设备号,0:次设备号
        <3> 运行App程序
                ./chrdevbaseApp /dev/chrdevbase 1   # 读
                ./chrdevbaseApp /dev/chrdevbase 2   # 写
   (6)、说明
        使用 register_chrdev() 函数注册设备时,要传入的主设备号是没有被使用的,而且一旦注册成功,主设备号下面的所有次设备号都会被此设备占用,最后还需要自己创建设备节点。后续的新模板会使用其它的函数,不仅可以申请设备号,而且还充分利用了次设备号。

5.03、Linux驱动之LED(旧)

        Linux内核启动的时候会初始化MMU,设置好内存映射,设置好后CPU访问的都是虚拟地址。比如I.MX6ULL 的GPIO1_IO03引脚的复用寄存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03的地址为0X020E0068。如果没有开启 MMU 的话直接向0X020E0068这个寄存器地址写入数据就可以配置 GPIO1_IO03 的复用功能。现在开启了MMU,并且设置了内存映射,因此就不能直接向 0X020E0068 这个地址写入数据了。我们必须得到0X020E0068这个物理地址在Linux系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数: ioremap 和 iounmap。

    (1)、电路

    (2)、驱动程序 led.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define LED_MAJOR    200	/* 主设备号 */
#define LED_NAME	 "led" 	/* 设备名字 */

#define LEDOFF 	0			/* 关灯 */
#define LEDON 	1			/* 开灯 */
 
/* 寄存器物理地址 */
#define CCM_CCGR1_BASE				(0X020C406C)  //时钟
#define SW_MUX_GPIO1_IO03_BASE		(0X020E0068)  //复用
#define SW_PAD_GPIO1_IO03_BASE		(0X020E02F4)  //电气
#define GPIO1_DR_BASE				(0X0209C000)  //方向
#define GPIO1_GDIR_BASE				(0X0209C004)  //输出

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

/*
 * @description		: LED打开/关闭
 * @param - sta 	: LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return 			: 无
 */
void led_switch(u8 sta)
{
	u32 val = 0;
	if(sta == LEDON) {
		val = readl(GPIO1_DR);
		val &= ~(1 << 3);	
		writel(val, GPIO1_DR);
	}else if(sta == LEDOFF) {
		val = readl(GPIO1_DR);
		val|= (1 << 3);	
		writel(val, GPIO1_DR);
	}	
}

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}

	ledstat = databuf[0];		/* 获取状态值 */

	if(ledstat == LEDON) {	
		led_switch(LEDON);		/* 打开LED灯 */
	} else if(ledstat == LEDOFF) {
		led_switch(LEDOFF);	/* 关闭LED灯 */
	}
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 设备操作函数 */
static struct file_operations led_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = 	led_release,
};

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init led_init(void)
{
	int retvalue = 0;
	u32 val = 0;

	/* 初始化LED */
	/* 1、寄存器地址映射 */
  	IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);            //时钟
	SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);  //复用
  	SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);  //电气
	GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);                    //方向
	GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);                //输出

	/* 2、使能GPIO1时钟 */
	val = readl(IMX6U_CCM_CCGR1);
	val &= ~(3 << 26);	/* 清除以前的设置 */
	val |= (3 << 26);	/* 设置新值 */
	writel(val, IMX6U_CCM_CCGR1);

	/* 3、设置GPIO1_IO03的复用功能,将其复用为GPIO1_IO03,最后设置IO属性。 */
	writel(5, SW_MUX_GPIO1_IO03);
	
	/*寄存器SW_PAD_GPIO1_IO03设置IO属性
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
	 */
	writel(0x10B0, SW_PAD_GPIO1_IO03);

	/* 4、设置GPIO1_IO03为输出功能 */
	val = readl(GPIO1_GDIR);
	val &= ~(1 << 3);	/* 清除以前的设置 */
	val |= (1 << 3);	/* 设置为输出 */
	writel(val, GPIO1_GDIR);

	/* 5、默认关闭LED */
	val = readl(GPIO1_DR);
	val |= (1 << 3);	
	writel(val, GPIO1_DR);

	/* 6、注册字符设备驱动 */
	retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
	if(retvalue < 0){
		printk("register chrdev failed!\r\n");
		return -EIO;
	}
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit led_exit(void)
{
	/* 取消映射 */
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);
	/* 注销字符设备驱动 */
	unregister_chrdev(LED_MAJOR, LED_NAME);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

    (3)、app测试程序 ledApp.c

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

#define LEDOFF 	0
#define LEDON 	1
/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, retvalue;
	char *filename;
	unsigned char databuf[1];
	
	if(argc != 3){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];

	/* 打开led驱动 */
	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}

	databuf[0] = atoi(argv[2]);	/* 要执行的操作:打开或关闭 */

	/* 向/dev/led文件写入数据 */
	retvalue = write(fd, databuf, sizeof(databuf));
	if(retvalue < 0){
		printf("LED Control Failed!\r\n");
		close(fd);
		return -1;
	}

	retvalue = close(fd); /* 关闭文件 */
	if(retvalue < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

    (4)、关闭内核心跳灯
        
如果内核驱动已经适配了LED驱动,LED2默认设置成了内核的心跳灯,可以先关闭心跳灯,再测试自己的驱动程序。
        echo none > /sys/class/leds/sys-led/trigger   # 改变LED的触发模式
        echo 1 > /sys/class/leds/sys-led/brightness   # 点亮LED
        echo 0 > /sys/class/leds/sys-led/brightness   # 熄灭LED
        剩下的操作参考5.02章即可

5.04、Linux驱动之LED(新-普通)

        使用 register_chrdev 函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会
带来两个问题:A.需要我们事先确定好哪些主设备号没有使用B.会将一个主设备号下的所有次设备号都使用掉,比如现在设置 LED 这个主设备号为200,那么 0~1048575(2^20-1)这个区间的次设备号就全部都被 LED 一个设备分走了。这样太浪费次设备号了!一个 LED 设备肯定只能有一个主设备号,一个次设备号。
        新的驱动框架:<1>:申请设备号、<2>:注册设备、<3>:创建设备节点。可以看出新的设备驱动,把设备驱动的注册过程分成了几个部分,使驱动程序的编写变得更加灵活。(使用busybox 构建根文件系统的时候,busybox会创建一个udev的简化版本mdev,udev是linux kernel2.6引入的设备管理器,它有自动创建和删除设备节点的功能(热插拔),步骤<3>使用的就是mdev机制)
  (1)、驱动程序 newchrled.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>

#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define NEWCHRLED_CNT	1		  	/* 设备号个数 */
#define NEWCHRLED_NAME	"newchrled"	/* 名字 */
#define LEDOFF 			0			/* 关灯 */
#define LEDON 			1			/* 开灯 */
 
/* 寄存器物理地址 */
#define CCM_CCGR1_BASE				(0X020C406C)	
#define SW_MUX_GPIO1_IO03_BASE		(0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE		(0X020E02F4)
#define GPIO1_DR_BASE				(0X0209C000)
#define GPIO1_GDIR_BASE				(0X0209C004)

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

/* newchrled设备结构体 */
struct newchrled_dev{
	dev_t devid;			/* 设备号 	*/
	struct cdev cdev;		/* cdev     */
	struct class *class;	/* 类 	    */
	struct device *device;	/* 设备 	*/
	int major;				/* 主设备号	 */
	int minor;				/* 次设备号  */
};

struct newchrled_dev newchrled;	/* led设备 */

/*
 * @description		: LED打开/关闭
 * @param - sta 	: LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return 			: 无
 */
void led_switch(u8 sta)
{
	u32 val = 0;
	if(sta == LEDON) {
		val = readl(GPIO1_DR);
		val &= ~(1 << 3);	
		writel(val, GPIO1_DR);
	}else if(sta == LEDOFF) {
		val = readl(GPIO1_DR);
		val|= (1 << 3);	
		writel(val, GPIO1_DR);
	}	
}

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &newchrled; /* 设置私有数据 */
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}

	ledstat = databuf[0];		/* 获取状态值 */

	if(ledstat == LEDON) {	
		led_switch(LEDON);		/* 打开LED灯 */
	} else if(ledstat == LEDOFF) {
		led_switch(LEDOFF);	/* 关闭LED灯 */
	}
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 设备操作函数 */
static struct file_operations newchrled_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = led_release,
};

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init newcharled_init(void)
{
	int ret = 0;
	u32 val = 0;
	/* 初始化LED */
	/* 1、寄存器地址映射 */
  	IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
	SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
  	SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
	GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
	GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
	/* 2、使能GPIO1时钟 */
	val = readl(IMX6U_CCM_CCGR1);
	val &= ~(3 << 26);	/* 清楚以前的设置 */
	val |= (3 << 26);	/* 设置新值 */
	writel(val, IMX6U_CCM_CCGR1);

	/* 3、设置GPIO1_IO03的复用功能,将其复用为
	 *    GPIO1_IO03,最后设置IO属性。
	 */
	writel(5, SW_MUX_GPIO1_IO03);
	
	/*寄存器SW_PAD_GPIO1_IO03设置IO属性
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
	 */
	writel(0x10B0, SW_PAD_GPIO1_IO03);

	/* 4、设置GPIO1_IO03为输出功能 */
	val = readl(GPIO1_GDIR);
	val &= ~(1 << 3);	/* 清除以前的设置 */
	val |= (1 << 3);	/* 设置为输出 */
	writel(val, GPIO1_GDIR);

	/* 5、默认关闭LED */
	val = readl(GPIO1_DR);
	val |= (1 << 3);	
	writel(val, GPIO1_DR);

	/* 注册字符设备驱动 */
	/* 1、申请设备号 */
	newchrled.major = 0; /* 设置为0,即没有定义设备号 */
	if (newchrled.major) { /* 定义了设备号 */
		newchrled.devid = MKDEV(newchrled.major, 0);
		ret = register_chrdev_region(newchrled.devid,   //起始设备号
							   		 NEWCHRLED_CNT,     //设备数量
							   		 NEWCHRLED_NAME);   //设备名称
	} else { /* 没有定义设备号 */
		ret = alloc_chrdev_region(&newchrled.devid,     //储存设备号的变量
								  0,                    //起始次设备号
								  NEWCHRLED_CNT,        //设备数量
								  NEWCHRLED_NAME);      //设备名称
		newchrled.major = MAJOR(newchrled.devid); //获取分配号的主设备号
		newchrled.minor = MINOR(newchrled.devid); //获取分配号的次设备号
	}
	if(ret < 0){
		printk("newchrled chrdev_region err!\r\n"); 
		goto fail_devid;
	}
	printk("newchrled major=%d,minor=%d\r\n",newchrled.major, newchrled.minor);	
	/* 2、注册字符设备 */
	newchrled.cdev.owner = THIS_MODULE;
	cdev_init(&newchrled.cdev, &newchrled_fops);                     //初始化一个cdev
	ret = cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT); //添加一个cdev
	if(ret < 0 ){
		printk("newchrled cdev_add err!\r\n");
		goto fail_cdev;
	}
	/* 3、创建设备节点 */
	newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME); //先创建一个类(创建的类在/sys/class中)
	if (IS_ERR(newchrled.class)) {
		printk("newchrled class_create err!\r\n"); 
		ret = PTR_ERR(newchrled.class);
		goto fail_class;
	}
	newchrled.device = device_create(newchrled.class,  //该设备依附的类
									 NULL,             //父设备
									 newchrled.devid,  //设备号
									 NULL,             //私有数据
									 NEWCHRLED_NAME);  //设备名称
	if (IS_ERR(newchrled.device)) {
		printk("newchrled device_create err!\r\n"); 
		ret = PTR_ERR(newchrled.device);
		goto fail_device;
	}
	return 0;
fail_device:
	class_destroy(newchrled.class); 
fail_class:
	cdev_del(&newchrled.cdev); 
fail_cdev:
	unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);
fail_devid:
	return ret;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit newcharled_exit(void)
{
	/* 取消映射 */
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);
    /* 按照相反的顺序注销 */
	device_destroy(newchrled.class, newchrled.devid);         /* 销毁类*/
	class_destroy(newchrled.class);                           /* 销毁设备节点 */
	cdev_del(&newchrled.cdev);                                /* 删除字符设备 */
	unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT); /* 注销设备号 */
}

module_init(newcharled_init);
module_exit(newcharled_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

//可以看出新的设备驱动,把设备驱动的注册过程分成了几个部分,变得更灵活。

  (2)、app测试程序 ledApp.c

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

#define LEDOFF 	0
#define LEDON 	1
/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, retvalue;
	char *filename;
	unsigned char databuf[1];
	
	if(argc != 3){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];

	/* 打开led驱动 */
	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}

	databuf[0] = atoi(argv[2]);	/* 要执行的操作:打开或关闭 */

	/* 向/dev/led文件写入数据 */
	retvalue = write(fd, databuf, sizeof(databuf));
	if(retvalue < 0){
		printf("LED Control Failed!\r\n");
		close(fd);
		return -1;
	}

	retvalue = close(fd); /* 关闭文件 */
	if(retvalue < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

5.05、Linux驱动之LED(新-设备树)

        Linux 内核通过根节点 compatible 属性找到对应的设备的函数调用过程:

        Linux内核启动的时会解析设备树中各个节点的信息,并且在根文件系统的/proc/devicetree目录下根据节点名字创建不同文件夹。
  <1>、添加设备树节点

/ {
	itop-leds{
	#address-cells = <1>; /* 子节点reg的属性,本节点在其父节点中设置(skeleton.dtsi中) */
	#size-cells = <1>; /* 子节点reg的属性,本节点在其父节点中设置(skeleton.dtsi中) */  
	compatible = "itop-myleds";
	status = "okay";
	reg = <  0x020C406C 0x00000004       /* CCM_CCGR1_BASE */
			 0x020E0068 0x00000004       /* SW_MUX_GPIO1_IO03_BASE */
			 0x020E02F4 0x00000004       /* SW_PAD_GPIO1_IO03_BASE */
			 0x0209C000 0x00000004       /* GPIO1_DR_BASE */
			 0x0209C004 0x00000004 >;    /* GPIO1_GDIR_BASE */
	};
};

重启内核,查看 /proc/devicetree/base 路径下是否有itop-leds节点信息。
  <2>、驱动程序:dtsled.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define NEWCHRLED_CNT	1		  	/* 设备号个数 */
#define NEWCHRLED_NAME	"dtsled"	/* 名字 */
#define LEDOFF 			0			/* 关灯 */
#define LEDON 			1			/* 开灯 */

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

/* dtsled设备结构体 */
struct dtsled_dev{
	dev_t devid;			/* 设备号 	*/
	struct cdev cdev;		/* cdev     */
	struct class *class;	/* 类 	    */
	struct device *device;	/* 设备 	*/
	int major;				/* 主设备号	 */
	int minor;				/* 次设备号  */
	struct device_node *nd; /* 设备节点  */
};
struct dtsled_dev dtsled;	/* led设备 */

/*
 * @description		: LED打开/关闭
 * @param - sta 	: LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return 			: 无
 */
void led_switch(u8 sta)
{
	u32 val = 0;
	if(sta == LEDON) {
		val = readl(GPIO1_DR);
		val &= ~(1 << 3);	
		writel(val, GPIO1_DR);
	}else if(sta == LEDOFF) {
		val = readl(GPIO1_DR);
		val|= (1 << 3);	
		writel(val, GPIO1_DR);
	}	
}

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &dtsled; /* 设置私有数据 */
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}

	ledstat = databuf[0];		/* 获取状态值 */

	if(ledstat == LEDON) {	
		led_switch(LEDON);		/* 打开LED灯 */
	} else if(ledstat == LEDOFF) {
		led_switch(LEDOFF);	/* 关闭LED灯 */
	}
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 设备操作函数 */
static struct file_operations dtsled_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = led_release,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init dtsled_init(void)
{
	int ret = 0;
	u32 val = 0;
	u32 regdata[14];
	const char *str;
	struct property *proper;
	/*一、获取设备树中的属性数据 */
	/* 1.1、获取设备节点:itop-leds */
	dtsled.nd = of_find_node_by_path("/itop-leds");
	if(dtsled.nd == NULL){
		printk("itop-leds node not find!\r\n");
		return -EINVAL;
	} else {
		printk("itop-leds node find!\r\n");
	}
	/* 1.2、获取compatible属性内容 */
	proper = of_find_property(dtsled.nd, "compatible", NULL);
	if(proper == NULL) {
		printk("compatible property find failed\r\n");
	} else {
		printk("compatible = %s\r\n", (char*)proper->value);
	}
	/* 1.3、获取status属性内容 */
	ret = of_property_read_string(dtsled.nd, "status", &str);
	if(ret < 0){
		printk("status read failed!\r\n");
	} else {
		printk("status = %s\r\n",str);
	}
	/* 1.4、获取reg属性内容 */
	ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 10);
	if(ret < 0) {
		printk("reg property read failed!\r\n");
	} else {
		u8 i = 0;
		printk("reg data:\r\n");
		for(i = 0; i < 10; i++)
			printk("%#X ", regdata[i]);
		printk("\r\n");
	}
	/* 二、初始化LED */
	/* 2.1、寄存器地址映射 */
#if 0
	IMX6U_CCM_CCGR1 = ioremap(regdata[0], regdata[1]);
	SW_MUX_GPIO1_IO03 = ioremap(regdata[2], regdata[3]);
  	SW_PAD_GPIO1_IO03 = ioremap(regdata[4], regdata[5]);
	GPIO1_DR = ioremap(regdata[6], regdata[7]);
	GPIO1_GDIR = ioremap(regdata[8], regdata[9]);
#else
	IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0);   //返回reg第0段经过内存映射后的虚拟内存首地
	SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1); //返回reg第1段经过内存映射后的虚拟内存首地
  	SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2); //返回reg第2段经过内存映射后的虚拟内存首地
	GPIO1_DR = of_iomap(dtsled.nd, 3);			//返回reg第3段经过内存映射后的虚拟内存首地
	GPIO1_GDIR = of_iomap(dtsled.nd, 4);		//返回reg第4段经过内存映射后的虚拟内存首地
#endif
	/* 2.2、使能GPIO1时钟 */
	val = readl(IMX6U_CCM_CCGR1);
	val &= ~(3 << 26);	/* 清楚以前的设置 */
	val |= (3 << 26);	/* 设置新值 */
	writel(val, IMX6U_CCM_CCGR1);

	/* 2.3、设置GPIO1_IO03的复用功能,将其复用为
	 *    GPIO1_IO03,最后设置IO属性。
	 */
	writel(5, SW_MUX_GPIO1_IO03);
	
	/*寄存器SW_PAD_GPIO1_IO03设置IO属性
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
	 */
	writel(0x10B0, SW_PAD_GPIO1_IO03);

	/* 2.4、设置GPIO1_IO03为输出功能 */
	val = readl(GPIO1_GDIR);
	val &= ~(1 << 3);	/* 清除以前的设置 */
	val |= (1 << 3);	/* 设置为输出 */
	writel(val, GPIO1_GDIR);

	/* 2.5、默认关闭LED */
	val = readl(GPIO1_DR);
	val |= (1 << 3);	
	writel(val, GPIO1_DR);

	/* 注册字符设备驱动 */
	/* 1、申请设备号 */
	dtsled.major = 0; /* 设置为0,即没有定义设备号 */
	if (dtsled.major) { /* 定义了设备号 */
		dtsled.devid = MKDEV(dtsled.major, 0);
		ret = register_chrdev_region(dtsled.devid,   //起始设备号
							   		 NEWCHRLED_CNT,     //设备数量
							   		 NEWCHRLED_NAME);   //设备名称
	} else { /* 没有定义设备号 */
		ret = alloc_chrdev_region(&dtsled.devid,     //储存设备号的变量
								  0,                    //起始次设备号
								  NEWCHRLED_CNT,        //设备数量
								  NEWCHRLED_NAME);      //设备名称
		dtsled.major = MAJOR(dtsled.devid); //获取分配号的主设备号
		dtsled.minor = MINOR(dtsled.devid); //获取分配号的次设备号
	}
	if(ret < 0){
		printk("dtsled chrdev_region err!\r\n"); 
		goto fail_devid;
	}
	printk("dtsled major=%d,minor=%d\r\n", dtsled.major, dtsled.minor);	
	/* 2、注册字符设备 */
	dtsled.cdev.owner = THIS_MODULE;
	cdev_init(&dtsled.cdev, &dtsled_fops);                     //初始化一个cdev
	ret = cdev_add(&dtsled.cdev, dtsled.devid, NEWCHRLED_CNT); //添加一个cdev
	if(ret < 0 ){
		printk("dtsled cdev_add err!\r\n");
		goto fail_cdev;
	}
	/* 3、创建设备节点 */
	dtsled.class = class_create(THIS_MODULE, NEWCHRLED_NAME); //先创建一个类(创建的类在/sys/class中)
	if (IS_ERR(dtsled.class)) {
		printk("dtsled class_create err!\r\n"); 
		ret = PTR_ERR(dtsled.class);
		goto fail_class;
	}
	dtsled.device = device_create(dtsled.class,  //该设备依附的类
									 NULL,             //父设备
									 dtsled.devid,  //设备号
									 NULL,             //私有数据
									 NEWCHRLED_NAME);  //设备名称
	if (IS_ERR(dtsled.device)) {
		printk("dtsled device_create err!\r\n"); 
		ret = PTR_ERR(dtsled.device);
		goto fail_device;
	}
	return 0;
fail_device:
	class_destroy(dtsled.class); 
fail_class:
	cdev_del(&dtsled.cdev); 
fail_cdev:
	unregister_chrdev_region(dtsled.devid, NEWCHRLED_CNT);
fail_devid:
	return ret;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit dtsled__exit(void)
{
	/* 取消映射 */
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);
    /* 按照相反的顺序注销 */
	device_destroy(dtsled.class, dtsled.devid);         /* 销毁类*/
	class_destroy(dtsled.class);                           /* 销毁设备节点 */
	cdev_del(&dtsled.cdev);                                /* 删除字符设备 */
	unregister_chrdev_region(dtsled.devid, NEWCHRLED_CNT); /* 注销设备号 */
}

module_init(dtsled_init);
module_exit(dtsled__exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

  <3>、应用程序:dtsledApp.c

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

#define LEDOFF 	0
#define LEDON 	1

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, retvalue;
	char *filename;
	unsigned char databuf[1];
	
	if(argc != 3){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];

	/* 打开led驱动 */
	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}

	databuf[0] = atoi(argv[2]);	/* 要执行的操作:打开或关闭 */

	/* 向/dev/led文件写入数据 */
	retvalue = write(fd, databuf, sizeof(databuf));
	if(retvalue < 0){
		printf("LED Control Failed!\r\n");
		close(fd);
		return -1;
	}

	retvalue = close(fd); /* 关闭文件 */
	if(retvalue < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

        测试命令: ./dstledApp /dev/dstled 0       # 关闭LED
                           ./dstledApp /dev/dstled 1       # 打开LED

5.06、Linux驱动之pinctrl和gpio子系统

前言:本章节看似正常,实际有一个大坑,就是pinctrl并未起作用(坑在本章最后进行填埋)

(1)、 pinctrl子系统
      设备树是描述设备信息的一个树形文件。大多数SOC的pin是支持复用的,比如 I.MX6ULL 的GPIO1_IO03 既可以作为普通的 GPIO 使用,也可以作为 I2C1 的 SDA 等等。此外我们还需要置 pin 的电气特性,比如上/下拉、速度、驱动能力等等。传统的配置 pin 的方式就是直接操作相应寄存器,但是这种配置方式比较繁琐、而且容易出问题(比如 pin 功能冲突)。 pinctrl 子系统就是为了解决这个问题而引入的, pinctrl 子系统主要工作内容如下:(你要思考是在何执行的)
        ①、获取设备树中 pin 信息。
        ②、根据获取到的 pin 信息来设置 pin 的复用功能
        ③、根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。
   <&&>重点说一下复用功能,以UART1_RX_DATA这个IO来说,它接受4个pin的复用,如图示:

        这么问题就来了,UART1_RX_DATA 该接受哪一个pin的复用呢,这就需要设置,于是需要配置 IOMUXC_UART1_RX_DATA_SELECT_INPUT 寄存器来选择哪一个作为输入。所以完整的复用设置包括两个部分:<1>设置复用目标,<2>让目标选择自己。如果复用目标只接受一个pin的复用,那么它步骤2就不需要了,因为它只能选择自己。所以说如果你复用的目标接受多pin复用时,你还要设置它的 SELECT_INPUT 寄存器让它选择自己才行。对于我们使用者来讲,只需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始化工作均由 pinctrl 子系统来完成, pinctrl 系统源码目录为 drivers/pinctrl。
        IOMUXC控制器有三类寄存器SW_MUX_CTL_XXX、SW_PAD_CTL_XXXXXX_SELECT_INPUT,pinctrl子系统就是操作这三类寄存器实现的。通过观察参考手册可知,IMX6ULL把自己的PAD(PAD也叫PIN)分为了两组,一组由IOMUXC控制,另一组由IOMUXC_SNVS控制器控制(后面会讲到)。在Linux内核的arch/arm/boot/dts/imx6ull-14x14-evk.dts文件中有如下定义:

        可以看到,每行有一个宏和一个十六进制数组成,宏在arch/arm/boot/dts/imx6ull-pinfunc.h和arch/arm/boot/dts/imx6ul-pinfunc.h文件中定义。如下图所示:

设IMX6ULL_PAD_UART1_RTS_B称为复用源,GPIO1_GPIO1_IO9称为复用目标,如下:
        mux_reg : 复用源的io复用配置寄存器的偏移地址(对应 SW_MUX_CTL_XXX寄存器)
        conf_reg : 复用源的io电气配置寄存器的偏移地址(对应 SW_PAD_CTL_XXX寄存器)
        input_reg : 复用目标的io输入配置寄存器的偏移地址(对应 XXX_SELECT_INPUT寄存器)
        mux_mode : mux_reg的值
        input_val : input_reg的值(如果复用源只有一个,则没有此寄存器,配置成0即可)
        0x17059 : conf_reg的值
注意:io输入配置寄存器,指的是当有多个复用源时,配置选择哪一个复用源。
(2)、 gpio子系统

        gpio子系统顾名思义,就是用于初始化GPIO并且提供相应的API函数,比如设置GPIO为输入输出,读取GPIO的值等。gpio子系统的主要目的就是方便驱动开发者使用gpio,驱动开发者在设备树中添加gpio相关信息,然后就可以在驱动程序中使用gpio子系统提供的API函数来操作GPIO,Linux内核向驱动开发者屏蔽掉了GPIO的设置过程,极大的方便了驱动开发者使用 GPIO。
        gpio子系统都是写好的,不需要我们添加节点或者更改,在设备节点中调用即可。gpio子系统的使用方式如下图所示,即<&gpio1 3 GPIO_ACTIVE_LOW>,&gpio1 3 这两个部分在驱动获取时,获取到的是io的id,每个io在linux内核中都有一个唯一标识的id。至于GPIO_ACTIVE_LOW,它是个标志,可以在驱动中获取此属性,获取到的其实就是GPIO_ACTIVE_LOW这个宏的数值。        注意命名规则,设备节点名为leds,pinctrl子系统节点名为ledsgrp、label名为pinctrl_leds。后面的章节尽量采用这样的命名规则,尽量让设备树结构更清晰。(下面为驱动程序,测试程序和上一章节的一样)
(3)、驱动程序

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define NEWCHRLED_CNT	1		  	/* 设备号个数 */
#define NEWCHRLED_NAME	"gpioled"	/* 名字 */
#define LEDON 			1			/* 开灯 */
#define LEDOFF 			0			/* 关灯 */

/* gpioled设备结构体 */
struct gpioled_dev{
	dev_t devid;			/* 设备号 	*/
	struct cdev cdev;		/* cdev     */
	struct class *class;	/* 类 	    */
	struct device *device;	/* 设备 	*/
	int major;				/* 主设备号	 */
	int minor;				/* 次设备号  */
	struct device_node *nd; /* 设备节点  */
	int gpio_id;            /* gpio编号  */
	bool active_low;        /* gpio标志 */
};
struct gpioled_dev gpioled;	/* led设备 */

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &gpioled; /* 设置私有数据 */
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;
	struct gpioled_dev *dev = filp->private_data;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}
	ledstat = databuf[0]; /* 获取状态值 */
	if(ledstat == LEDON) {	
		gpio_set_value(dev->gpio_id, !dev->active_low); /* 打开LED灯,设置为有效电平 */
	} else if(ledstat == LEDOFF) {
		gpio_set_value(dev->gpio_id, dev->active_low);  /* 关闭LED灯,设置为无效电平 */
	}
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 设备操作函数 */
static struct file_operations gpioled_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = led_release,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init led_init(void)
{
	int ret = 0;
	struct property *proper;
	enum of_gpio_flags flags;
	/*一、获取设备树中的属性数据 */
	/* 1.1、获取设备节点:leds */
	gpioled.nd = of_find_node_by_path("/leds");
	if(gpioled.nd == NULL){
		printk("pinctrl_gpio-led node not find!\r\n");
		return -EINVAL;
	} else {
		printk("pinctrl_gpio-led node find!\r\n");
	}
	/* 1.2、获取compatible属性内容 */
	proper = of_find_property(gpioled.nd, "compatible", NULL);
	if(proper == NULL) {
		printk("compatible property find failed\r\n");
	} else {
		printk("compatible = %s\r\n", (char*)proper->value);
	}
	/* 1.3、获取gpio的属性,gpio的编号和flags */
	if (of_gpio_named_count(gpioled.nd, "led-gpio") > 0) {
		gpioled.gpio_id = of_get_named_gpio_flags(gpioled.nd, "led-gpio", 0, &flags);
		if (gpioled.gpio_id < 0){
			printk("can't get led-gpio");
			return -EINVAL;
		} /* 注意:低电平有效的宏为1,高电平有效的宏为0 */
		gpioled.active_low = (flags == OF_GPIO_ACTIVE_LOW); 
		printk("gpio_id = %d\r\n", gpioled.gpio_id);
		printk("gpio_flags = %d\r\n", flags);
	} else {
		printk("not have led-gpio");
		return -EINVAL;
	}
	/* 1.4、设置GPIO1_IO03为无效电平 */
	if(gpioled.active_low){
		ret = gpio_direction_output(gpioled.gpio_id, 1);  /* 设置为无效电平 */
	} else {
		ret = gpio_direction_output(gpioled.gpio_id, 0);  /* 设置为无效电平 */
	}	
	if(ret < 0) printk("can't set gpio!\r\n");

	/* 注册字符设备驱动 */
	/* 1、申请设备号 */
	gpioled.major = 0; /* 设置为0,即没有定义设备号 */
	if (gpioled.major) { /* 定义了设备号 */
		gpioled.devid = MKDEV(gpioled.major, 0);
		ret = register_chrdev_region(gpioled.devid,   //起始设备号
							   		 NEWCHRLED_CNT,   //设备数量
							   		 NEWCHRLED_NAME); //设备名称
	} else { /* 没有定义设备号 */
		ret = alloc_chrdev_region(&gpioled.devid,  //储存设备号的变量
								  0,               //起始次设备号
								  NEWCHRLED_CNT,   //设备数量
								  NEWCHRLED_NAME); //设备名称
		gpioled.major = MAJOR(gpioled.devid);      //获取分配号的主设备号
		gpioled.minor = MINOR(gpioled.devid);      //获取分配号的次设备号
	}
	if(ret < 0){
		printk("gpioled chrdev_region err!\r\n"); 
		goto fail_devid;
	}
	printk("gpioled major=%d,minor=%d\r\n", gpioled.major, gpioled.minor);	
	/* 2、注册字符设备 */
	gpioled.cdev.owner = THIS_MODULE;
	cdev_init(&gpioled.cdev, &gpioled_fops); //初始化一个cdev
	ret = cdev_add(&gpioled.cdev, gpioled.devid, NEWCHRLED_CNT); //添加一个cdev
	if(ret < 0 ){
		printk("gpioled cdev_add err!\r\n");
		goto fail_cdev;
	}
	/* 3、创建设备节点 */
	gpioled.class = class_create(THIS_MODULE, NEWCHRLED_NAME); //先创建一个类(创建的类在/sys/class中)
	if (IS_ERR(gpioled.class)) {
		printk("gpioled class_create err!\r\n"); 
		ret = PTR_ERR(gpioled.class);
		goto fail_class;
	}
	gpioled.device = device_create(gpioled.class,  //该设备依附的类
									 NULL,             //父设备
									 gpioled.devid,  //设备号
									 NULL,             //私有数据
									 NEWCHRLED_NAME);  //设备名称
	if (IS_ERR(gpioled.device)) {
		printk("gpioled device_create err!\r\n"); 
		ret = PTR_ERR(gpioled.device);
		goto fail_device;
	}
	return 0;
fail_device:
	class_destroy(gpioled.class); 
fail_class:
	cdev_del(&gpioled.cdev); 
fail_cdev:
	unregister_chrdev_region(gpioled.devid, NEWCHRLED_CNT);
fail_devid:
	return ret;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit led_exit(void)
{
    /* 按照相反的顺序注销 */
	device_destroy(gpioled.class, gpioled.devid);  /* 销毁类*/
	class_destroy(gpioled.class);                     /* 销毁设备节点 */
	cdev_del(&gpioled.cdev);                             /* 删除字符设备 */
	unregister_chrdev_region(gpioled.devid, NEWCHRLED_CNT); /* 注销设备号 */
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

填坑:

1、看了本章的实验,你能向面试官说出什么pinctrl子系统吗?希望如下总结能帮助到你。
    pictrl子系统是对引脚电气属性进行配置的,这毋庸置疑。pinctrl子系统为啥又有自己的驱动呢?
学习完驱动,你要理解向内核注册驱动的实质:在内核中创建变量。pinctrl子系统的驱动就是把设备树
部分解析后放在特定的结构体变量中,其它驱动程序通过接口可以访问到这个变量。如此而已。。。

2、pinctrl子系统驱动只解析,不配置。那是在哪里进行配置的呢?如下所示:
 -->platform_driver_register()
    -->__platform_driver_register()
       -->driver_register()
          -->bus_add_driver()
             -->driver_attach()
                -->bus_for_each_dev()
                   -->__driver_attach()
                      -->driver_probe_device()
                         -->really_probe()
                            -->pinctrl_bind_pins();  # 此函数会获取pinctrl,设置电气属性
                            -->dev->bus->probe(dev) or drv-> probe() # 驱动probe函数执行

3、关于坑?
    看了2,不知你有没有发现坑,是的,pinctrl在普通设备驱动框架下是无法自动激活的,需要手动激活。
    而在 platform 驱动框架下,会在驱动与设备匹配的时候,执行一次来自pinctrl的电器属性配置。

4、关于动态更改电气属性?
    程序执行的过程中(即驱动与设备匹配),用户也是可以更改pinctrl属性的,具体用法参考pinctrl_bind_pins()函数。

5.07、Linux驱动之蜂鸣器

        蜂鸣器实验和LED实验一样,也是通过控制一个IO口输出高低电平来操纵器件,这里不再赘述。迅为itop-imx6ull开发板的蜂鸣器电路如下,imx6ull这款CPU有两组IO,一组由IOMUXC控制器控制(LED使用的这组里面的IO),一组由IOMUXC_SVNS控制器控制(BEEP使用的这组里面的IO),和LED实验不同的是,这次的pinctrl_beep节点要添加在iomuxc_svns下,其它的几乎相同:

  <1>、添加设备树节点 

   <2>、驱动程序:beep.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define NEWCHRLED_CNT	1		 /* 设备号个数 */
#define NEWCHRLED_NAME	"beep"	 /* 设备节点名 */
#define BEEPOFF 		0		 /* 关闭 */
#define BEEPON 			1	     /* 打开 */

/* beep设备结构体 */
struct beep_dev{
	dev_t devid;			/* 设备号 	*/
	struct cdev cdev;		/* cdev     */
	struct class *class;	/* 类 	    */
	struct device *device;	/* 设备 	*/
	int major;				/* 主设备号	 */
	int minor;				/* 次设备号  */
	struct device_node *nd; /* 设备节点  */
	int gpio_id;            /* gpio编号  */
	bool active_low;        /* gpio标志  */
};
struct beep_dev beep;	    /* beep设备 */

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int beep_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &beep; /* 设置私有数据 */
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t beep_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t beep_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char beepstat;
	struct beep_dev *dev = filp->private_data;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}
	beepstat = databuf[0]; /* 获取状态值 */

	if(beepstat == BEEPON) {	/* 打开 */
		gpio_set_value(dev->gpio_id, !dev->active_low); /* 打开BEEP,设置为有效电平 */
	} else if(beepstat == BEEPOFF) { /* 关闭*/
		gpio_set_value(dev->gpio_id, dev->active_low);  /* 关闭BEEP,设置为有效电平 */
	} 
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int beep_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 设备操作函数 */
static struct file_operations beep_fops = {
	.owner = THIS_MODULE,
	.open = beep_open,
	.read = beep_read,
	.write = beep_write,
	.release = beep_release,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init beep_init(void)
{
	int ret = 0;
	struct property *proper;
	enum of_gpio_flags flags;
	/*一、获取设备树中的属性数据 */
	/* 1.1、获取设备节点:beep */
	beep.nd = of_find_node_by_path("/beep");
	if(beep.nd == NULL){
		printk("beep node not find!\r\n");
		return -EINVAL;
	} else {
		printk("beep node find!\r\n");
	}
	/* 1.2、获取compatible属性内容 */
	proper = of_find_property(beep.nd, "compatible", NULL);
	if(proper == NULL) {
		printk("compatible property find failed\r\n");
	} else {
		printk("compatible = %s\r\n", (char*)proper->value);
	}
	/* 1.3、获取gpio的属性,gpio的编号和flags */
	if (of_gpio_named_count(beep.nd, "beep-gpio") > 0) {
		beep.gpio_id = of_get_named_gpio_flags(beep.nd, "beep-gpio", 0, &flags);
		if (beep.gpio_id < 0){
			printk("can't get beep-gpio");
			return -EINVAL;
		} /* 注意:低电平有效的宏为1,高电平有效的宏为0 */
		beep.active_low = (flags == OF_GPIO_ACTIVE_LOW); 
		printk("gpio_id = %d\r\n", beep.gpio_id);
		printk("gpio_flags = %d\r\n", flags);
	} else {
		printk("not have beep-gpio");
		return -EINVAL;
	}
	/* 1.4、设置beep-gpio为无效电平 */
	if(beep.active_low){
		ret = gpio_direction_output(beep.gpio_id, 1);  /* 设置为无效电平 */
	} else {
		ret = gpio_direction_output(beep.gpio_id, 0);  /* 设置为无效电平 */
	}	
	if(ret < 0) printk("can't set gpio!\r\n");

	/* 注册字符设备驱动 */
	/* 1、申请设备号 */
	beep.major = 0; /* 设置为0,即没有定义设备号 */
	if (beep.major) { /* 定义了设备号 */
		beep.devid = MKDEV(beep.major, 0);
		ret = register_chrdev_region(beep.devid,   //起始设备号
							   		 NEWCHRLED_CNT,     //设备数量
							   		 NEWCHRLED_NAME);   //设备名称
	} else { /* 没有定义设备号 */
		ret = alloc_chrdev_region(&beep.devid,     //储存设备号的变量
								  0,                    //起始次设备号
								  NEWCHRLED_CNT,        //设备数量
								  NEWCHRLED_NAME);      //设备名称
		beep.major = MAJOR(beep.devid); //获取分配号的主设备号
		beep.minor = MINOR(beep.devid); //获取分配号的次设备号
	}
	if(ret < 0){
		printk("beep chrdev_region err!\r\n"); 
		goto fail_devid;
	}
	printk("beep major=%d,minor=%d\r\n", beep.major, beep.minor);	
	/* 2、注册字符设备 */
	beep.cdev.owner = THIS_MODULE;
	cdev_init(&beep.cdev, &beep_fops);                     //初始化一个cdev
	ret = cdev_add(&beep.cdev, beep.devid, NEWCHRLED_CNT); //添加一个cdev
	if(ret < 0 ){
		printk("beep cdev_add err!\r\n");
		goto fail_cdev;
	}
	/* 3、创建设备节点 */
	beep.class = class_create(THIS_MODULE, NEWCHRLED_NAME); //先创建一个类(创建的类在/sys/class中)
	if (IS_ERR(beep.class)) {
		printk("beep class_create err!\r\n"); 
		ret = PTR_ERR(beep.class);
		goto fail_class;
	}
	beep.device = device_create(beep.class,  //该设备依附的类
									 NULL,             //父设备
									 beep.devid,  //设备号
									 NULL,             //私有数据
									 NEWCHRLED_NAME);  //设备名称
	if (IS_ERR(beep.device)) {
		printk("beep device_create err!\r\n"); 
		ret = PTR_ERR(beep.device);
		goto fail_device;
	}
	return 0;
fail_device:
	class_destroy(beep.class); 
fail_class:
	cdev_del(&beep.cdev); 
fail_cdev:
	unregister_chrdev_region(beep.devid, NEWCHRLED_CNT);
fail_devid:
	return ret;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit beep_exit(void)
{
    /* 按照相反的顺序注销 */
	device_destroy(beep.class, beep.devid);              /* 销毁类*/
	class_destroy(beep.class);                           /* 销毁设备节点 */
	cdev_del(&beep.cdev);                                /* 删除字符设备 */
	unregister_chrdev_region(beep.devid, NEWCHRLED_CNT); /* 注销设备号 */
}

module_init(beep_init);
module_exit(beep_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

   <3>、应用程序:beepApp.c

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

#define BEEPOFF 0
#define BEEPON 	1

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, retvalue;
	char *filename;
	unsigned char databuf[1];
	
	if(argc != 3){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];

	/* 打开/dev/xxx驱动 */
	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}

	databuf[0] = atoi(argv[2]);	/* 要执行的操作:打开或关闭 */

	/* 向/dev/xxx文件写入数据 */
	retvalue = write(fd, databuf, sizeof(databuf));
	if(retvalue < 0){
		printf("LED Control Failed!\r\n");
		close(fd);
		return -1;
	}

	retvalue = close(fd); /* 关闭文件 */
	if(retvalue < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

   <4>、编译命令:Makefile

KERNELDIR := /home/haut/itop_imx6ull/software/Linux_Source_nxpV4.1.15   # 内核路径
CURRENT_PATH := $(shell pwd)  # 当前路径

obj-m := beep.o

build: kernel_modules

kernel_modules:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

app:
    arm-linux-gnueabihf-gcc ./beepApp.c -o beepApp
cp:
    cp ./beep.ko ./beepApp /home/haut/itop_imx6ull/nfs_rootfs/lib/modules/4.1.15

5.08、Linux驱动之并发&竞争

        Linux 系统是个多任务操作系统,会存在多个任务同时访问同一片内存区域,这些任务可能会相互覆盖这段内存中的数据,造成内存数据混乱。针对这个问题必须要做处理,严重的话可能会导致系统崩溃。现在的 Linux 系统并发产生的原因很复杂,总结一下有下面几个主要原因:
        ①、多线程并发访问, Linux 是多任务(线程)的系统,所以多线程访问是最基本的原因。
        ②、抢占式并发访问,从 2.6 版本内核开始, Linux 内核支持抢占,也就是说调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。
        ③、中断程序并发访问,这个无需多说,学过 STM32 的同学应该知道,硬件中断的权利可是很大的。
        ④、 SMP(多核)核间并发访问,现在 ARM 架构的多核 SOC 很常见,多核 CPU 存在核间并发访问
        当我们发现驱动程序中存在并发和竞争的时候一定做相应的处理,不然可能会产生非常严重问题,接下来我们依次来学习一下Linux 内核提供的几种并发和竞争的处理方法。        

5.8.1、原子操作实验

        本实验是在5.05章的LED实验上进行更改,使用原子变量实现驱动设备在同一时刻只允许一个App使用,设备树和5.05章的一样,不需要做修改。
   <1>、驱动程序:atomic.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define NEWCHRLED_CNT	1		  	/* 设备号个数 */
#define NEWCHRLED_NAME	"gpioled"	/* 名字 */
#define LEDON 			1			/* 开灯 */
#define LEDOFF 			0			/* 关灯 */


/* gpioled设备结构体 */
struct gpioled_dev{
	dev_t devid;			/* 设备号 	*/
	struct cdev cdev;		/* cdev     */
	struct class *class;	/* 类 	    */
	struct device *device;	/* 设备 	*/
	int major;				/* 主设备号	 */
	int minor;				/* 次设备号  */
	struct device_node *nd; /* 设备节点  */
	int gpio_id;            /* gpio编号 */
	bool active_low;        /* gpio标志 */
	atomic_t lock;          /* 原子变量  */
};
struct gpioled_dev gpioled;	/* led设备 */

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
	/* 通过判断原子变量的值来检查LED有没有被别的应用使用 */
	/* 先将gpioled.lock的值减1,然后判断其值是否为0(为0则返回真,否则返回假) */
	if(!atomic_dec_and_test(&gpioled.lock)){ /*减1后为0,不执行if,获取设备成功*/
		atomic_inc(&gpioled.lock); /* 小于0的话就加1,使其原子变量等于0 */
		return -EBUSY; /* LED被使用,返回忙 */
	}
	filp->private_data = &gpioled; /* 设置私有数据 */
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;
	struct gpioled_dev *dev = filp->private_data;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}
	ledstat = databuf[0]; /* 获取状态值 */
	if(ledstat == LEDON) {	
		gpio_set_value(dev->gpio_id, !gpioled.active_low); /* 打开LED灯,设置为有效电平 */
	} else if(ledstat == LEDOFF) {
		gpio_set_value(dev->gpio_id, gpioled.active_low);  /* 关闭LED灯,设置为无效电平 */
	}
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
	struct gpioled_dev *dev = filp->private_data;
	/* 关闭驱动文件的时候释放原子变量,即将原子变量恢复为初始值1 */
	atomic_inc(&dev->lock);  
	return 0;
}

/* 设备操作函数 */
static struct file_operations gpioled_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = led_release,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init led_init(void)
{
	int ret = 0;
	struct property *proper;
	enum of_gpio_flags flags;
	atomic_set(&gpioled.lock, 1); /* 初始化原子变量为1 */ 
	/*一、获取设备树中的属性数据 */
	/* 1.1、获取设备节点:leds */
	gpioled.nd = of_find_node_by_path("/leds");
	if(gpioled.nd == NULL){
		printk("pinctrl_gpio-led node not find!\r\n");
		return -EINVAL;
	} else {
		printk("pinctrl_gpio-led node find!\r\n");
	}
	/* 1.2、获取compatible属性内容 */
	proper = of_find_property(gpioled.nd, "compatible", NULL);
	if(proper == NULL) {
		printk("compatible property find failed\r\n");
	} else {
		printk("compatible = %s\r\n", (char*)proper->value);
	}
	/* 1.3、获取gpio的属性,gpio的编号和flags */
	if (of_gpio_named_count(gpioled.nd, "led-gpio") > 0) {
		gpioled.gpio_id = of_get_named_gpio_flags(gpioled.nd, "led-gpio", 0, &flags);
		if (gpioled.gpio_id < 0){
			printk("can't get led-gpio");
			return -EINVAL;
		} /* 注意:低电平有效的宏为1,高电平有效的宏为0 */
		gpioled.active_low = (flags == OF_GPIO_ACTIVE_LOW); 
		printk("gpio_id = %d\r\n", gpioled.gpio_id);
		printk("gpio_flags = %d\r\n", flags);
	} else {
		printk("not have led-gpio");
		return -EINVAL;
	}
	/* 1.4、设置GPIO1_IO03为无效电平 */
	if(gpioled.active_low){
		ret = gpio_direction_output(gpioled.gpio_id, 1);  /* 设置为无效电平 */
	} else {
		ret = gpio_direction_output(gpioled.gpio_id, 0);  /* 设置为无效电平 */
	}	
	if(ret < 0) printk("can't set gpio!\r\n");

	/* 注册字符设备驱动 */
	/* 1、申请设备号 */
	gpioled.major = 0; /* 设置为0,即没有定义设备号 */
	if (gpioled.major) { /* 定义了设备号 */
		gpioled.devid = MKDEV(gpioled.major, 0);
		ret = register_chrdev_region(gpioled.devid,   //起始设备号
							   		 NEWCHRLED_CNT,   //设备数量
							   		 NEWCHRLED_NAME); //设备名称
	} else { /* 没有定义设备号 */
		ret = alloc_chrdev_region(&gpioled.devid,  //储存设备号的变量
								  0,               //起始次设备号
								  NEWCHRLED_CNT,   //设备数量
								  NEWCHRLED_NAME); //设备名称
		gpioled.major = MAJOR(gpioled.devid);      //获取分配号的主设备号
		gpioled.minor = MINOR(gpioled.devid);      //获取分配号的次设备号
	}
	if(ret < 0){
		printk("gpioled chrdev_region err!\r\n"); 
		goto fail_devid;
	}
	printk("gpioled major=%d,minor=%d\r\n", gpioled.major, gpioled.minor);	
	/* 2、注册字符设备 */
	gpioled.cdev.owner = THIS_MODULE;
	cdev_init(&gpioled.cdev, &gpioled_fops); //初始化一个cdev
	ret = cdev_add(&gpioled.cdev, gpioled.devid, NEWCHRLED_CNT); //添加一个cdev
	if(ret < 0 ){
		printk("gpioled cdev_add err!\r\n");
		goto fail_cdev;
	}
	/* 3、创建设备节点 */
	gpioled.class = class_create(THIS_MODULE, NEWCHRLED_NAME); //先创建一个类(创建的类在/sys/class中)
	if (IS_ERR(gpioled.class)) {
		printk("gpioled class_create err!\r\n"); 
		ret = PTR_ERR(gpioled.class);
		goto fail_class;
	}
	gpioled.device = device_create(gpioled.class,  //该设备依附的类
									 NULL,             //父设备
									 gpioled.devid,  //设备号
									 NULL,             //私有数据
									 NEWCHRLED_NAME);  //设备名称
	if (IS_ERR(gpioled.device)) {
		printk("gpioled device_create err!\r\n"); 
		ret = PTR_ERR(gpioled.device);
		goto fail_device;
	}
	return 0;
fail_device:
	class_destroy(gpioled.class); 
fail_class:
	cdev_del(&gpioled.cdev); 
fail_cdev:
	unregister_chrdev_region(gpioled.devid, NEWCHRLED_CNT);
fail_devid:
	return ret;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit led_exit(void)
{
    /* 按照相反的顺序注销 */
	device_destroy(gpioled.class, gpioled.devid);         /* 销毁类*/
	class_destroy(gpioled.class);                           /* 销毁设备节点 */
	cdev_del(&gpioled.cdev);                                /* 删除字符设备 */
	unregister_chrdev_region(gpioled.devid, NEWCHRLED_CNT); /* 注销设备号 */
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

   <2>、应用程序:atomicApp.c 

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

#define LEDON 	1
#define LEDOFF 	0
/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, retvalue;
	char *filename;
	unsigned char cnt = 0;
	unsigned char databuf[1];
	
	if(argc != 3){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];

	/* 打开led驱动 */
	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}

	databuf[0] = atoi(argv[2]);	/* 要执行的操作:打开或关闭 */

	/* 向/dev/led文件写入数据 */
	retvalue = write(fd, databuf, sizeof(databuf));
	if(retvalue < 0){
		printf("LED Control Failed!\r\n");
		close(fd);
		return -1;
	}
	/* 模拟占用驱动25s */
	while(1){
		sleep(5);
		cnt++;
		printf("App running times:%d\r\n", cnt);
		if(cnt >= 5) break;
	}
	printf("App running finished!");
	/* 关闭文件 */
	retvalue = close(fd);
	if(retvalue < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

    <3>、编译命令:Makefile

KERNELDIR := /home/haut/itop_imx6ull/software/Linux_Source_nxpV4.1.15   # 内核路径
CURRENT_PATH := $(shell pwd)  # 当前路径

obj-m := atomic.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

app:
	arm-linux-gnueabihf-gcc ./atomicApp.c -o atomicApp
cp:
	cp ./atomic.ko ./atomicApp /home/haut/itop_imx6ull/nfs_rootfs/lib/modules/4.1.15

   <4>、运行测试

# 一、加载驱动
  cd lib/modules/4.1.15  //进入modprobe的工作目录
  depmod                 //第一次加载驱动的时候需要运行此命令
  modprobe atomic.ko     //加载驱动

# 二、运行程序
  ./atomicApp /dev/gpioled 1&  //打开LED灯,“&”表示在后台运行 atomicApp 这个软件
  ./atomicApp /dev/gpioled 0   //再次启动一个应用程序

5.8.2、自旋锁实验

        上一节我们使用原子变量实现了一次只能有一个应用程序访问 LED 灯,本节我们使用自旋锁来实现此功能。在使用自旋锁之前,先回顾一下自旋锁的使用注意事项:
        ①、自旋锁保护的临界区要尽可能的短因此在 open 函数中申请自旋锁然后在 release 函数中释放自旋锁的方法就不可取。我们可以使用一个变量来表示设备的使用情况,如果设备被使用了那么变量就加一,设备被释放以后变量就减 1,我们只需要使用自旋锁保护这个变量即可。
        ②、考虑驱动的兼容性,合理的选择 API 函数。综上所述,在本节例程中,我们通过定义一个变量dev_stats表示设备的使用情况,dev_stats为 0 的时候表示设备没有被使用,dev_stats大于 0 的时候表示设备被使用驱动 open 函数中先判断 dev_stats 是否为 0,也就是判断设备是否可用,如果为 0 的话就使用设备,并且将dev_stats加 1,表示设备被使用了。使用完以后在 release 函数中将dev_stats 减1,表示设备没有被使用了。因此真正实现设备互斥访问的是变量 dev_stats,但是我们要使用自旋锁对 dev_stats 来做保护。

   <1>、驱动程序:spinlock.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define NEWCHRLED_CNT	1		  	/* 设备号个数 */
#define NEWCHRLED_NAME	"gpioled"	/* 名字 */
#define LEDON 			1			/* 开灯 */
#define LEDOFF 			0			/* 关灯 */
/* gpioled设备结构体 */
struct gpioled_dev{
	dev_t devid;			/* 设备号 	*/
	struct cdev cdev;		/* cdev     */
	struct class *class;	/* 类 	    */
	struct device *device;	/* 设备 	*/
	int major;				/* 主设备号	 */
	int minor;				/* 次设备号  */
	struct device_node *nd; /* 设备节点  */
	int gpio_id;            /* gpio编号 */
	bool active_low;        /* gpio标志 */
	int dev_status;         /* 设备状态  */
	spinlock_t lock;        /* 自旋锁变量 */
	/* dev_status=0:未使用, dev_status=1:正在使用 */
};
struct gpioled_dev gpioled;	/* led设备 */

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
	unsigned long flags;
	filp->private_data = &gpioled; /* 设置私有数据 */
	spin_lock_irqsave(&gpioled.lock, flags); /* 保存中断状态,禁止本地中断,并获取自旋锁*/

	/* 注意:中断一旦关闭,临界区就产生了,直至中断打开前,都属于临界区 */
	if (gpioled.dev_status) {/* 如果设备被使用了 */
		spin_unlock_irqrestore(&gpioled.lock, flags);/* 解锁 */
		return -EBUSY;
	}
	gpioled.dev_status++;	/* 如果设备没有打开,那么就标记已经打开了 */
	
	spin_unlock_irqrestore(&gpioled.lock, flags);/* 将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁 */
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;
	struct gpioled_dev *dev = filp->private_data;
	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}
	ledstat = databuf[0]; /* 获取状态值 */
	if(ledstat == LEDON) {	
		gpio_set_value(dev->gpio_id, !gpioled.active_low); /* 打开LED灯,设置为有效电平 */
	} else if(ledstat == LEDOFF) {
		gpio_set_value(dev->gpio_id, gpioled.active_low);  /* 关闭LED灯,设置为无效电平 */
	}
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
	unsigned long flags;
	struct gpioled_dev *dev = filp->private_data;

	/* 关闭驱动文件的时候将dev_stats减1 */
	spin_lock_irqsave(&dev->lock, flags); /* 上锁 */
	if (dev->dev_status) {
		dev->dev_status--;
	}
	spin_unlock_irqrestore(&dev->lock, flags); /* 解锁 */

	return 0;
}

/* 设备操作函数 */
static struct file_operations gpioled_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = led_release,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init led_init(void)
{
	int ret = 0;
	struct property *proper;
	enum of_gpio_flags flags;
	gpioled.dev_status = 0;        /* 初始化设备状态*/
	spin_lock_init(&gpioled.lock); /* 初始化自旋锁 */ 
	/*一、获取设备树中的属性数据 */
	/* 1.1、获取设备节点:leds */
	gpioled.nd = of_find_node_by_path("/leds");
	if(gpioled.nd == NULL){
		printk("pinctrl_gpio-led node not find!\r\n");
		return -EINVAL;
	} else {
		printk("pinctrl_gpio-led node find!\r\n");
	}
	/* 1.2、获取compatible属性内容 */
	proper = of_find_property(gpioled.nd, "compatible", NULL);
	if(proper == NULL) {
		printk("compatible property find failed\r\n");
	} else {
		printk("compatible = %s\r\n", (char*)proper->value);
	}
	/* 1.3、获取gpio的属性,gpio的编号和flags */
	if (of_gpio_named_count(gpioled.nd, "led-gpio") > 0) {
		gpioled.gpio_id = of_get_named_gpio_flags(gpioled.nd, "led-gpio", 0, &flags);
		if (gpioled.gpio_id < 0){
			printk("can't get led-gpio");
			return -EINVAL;
		} /* 注意:低电平有效的宏为1,高电平有效的宏为0 */
		gpioled.active_low = (flags == OF_GPIO_ACTIVE_LOW); 
		printk("gpio_id = %d\r\n", gpioled.gpio_id);
		printk("gpio_flags = %d\r\n", flags);
	} else {
		printk("not have led-gpio");
		return -EINVAL;
	}
	/* 1.4、设置GPIO1_IO03为无效电平 */
	if(gpioled.active_low){
		ret = gpio_direction_output(gpioled.gpio_id, 1);  /* 设置为无效电平 */
	} else {
		ret = gpio_direction_output(gpioled.gpio_id, 0);  /* 设置为无效电平 */
	}	
	if(ret < 0) printk("can't set gpio!\r\n");

	/* 注册字符设备驱动 */
	/* 1、申请设备号 */
	gpioled.major = 0; /* 设置为0,即没有定义设备号 */
	if (gpioled.major) { /* 定义了设备号 */
		gpioled.devid = MKDEV(gpioled.major, 0);
		ret = register_chrdev_region(gpioled.devid,   //起始设备号
							   		 NEWCHRLED_CNT,   //设备数量
							   		 NEWCHRLED_NAME); //设备名称
	} else { /* 没有定义设备号 */
		ret = alloc_chrdev_region(&gpioled.devid,  //储存设备号的变量
								  0,               //起始次设备号
								  NEWCHRLED_CNT,   //设备数量
								  NEWCHRLED_NAME); //设备名称
		gpioled.major = MAJOR(gpioled.devid);      //获取分配号的主设备号
		gpioled.minor = MINOR(gpioled.devid);      //获取分配号的次设备号
	}
	if(ret < 0){
		printk("gpioled chrdev_region err!\r\n"); 
		goto fail_devid;
	}
	printk("gpioled major=%d,minor=%d\r\n", gpioled.major, gpioled.minor);	
	/* 2、注册字符设备 */
	gpioled.cdev.owner = THIS_MODULE;
	cdev_init(&gpioled.cdev, &gpioled_fops); //初始化一个cdev
	ret = cdev_add(&gpioled.cdev, gpioled.devid, NEWCHRLED_CNT); //添加一个cdev
	if(ret < 0 ){
		printk("gpioled cdev_add err!\r\n");
		goto fail_cdev;
	}
	/* 3、创建设备节点 */
	gpioled.class = class_create(THIS_MODULE, NEWCHRLED_NAME); //先创建一个类(创建的类在/sys/class中)
	if (IS_ERR(gpioled.class)) {
		printk("gpioled class_create err!\r\n"); 
		ret = PTR_ERR(gpioled.class);
		goto fail_class;
	}
	gpioled.device = device_create(gpioled.class,  //该设备依附的类
									 NULL,             //父设备
									 gpioled.devid,  //设备号
									 NULL,             //私有数据
									 NEWCHRLED_NAME);  //设备名称
	if (IS_ERR(gpioled.device)) {
		printk("gpioled device_create err!\r\n"); 
		ret = PTR_ERR(gpioled.device);
		goto fail_device;
	}
	return 0;
fail_device:
	class_destroy(gpioled.class); 
fail_class:
	cdev_del(&gpioled.cdev); 
fail_cdev:
	unregister_chrdev_region(gpioled.devid, NEWCHRLED_CNT);
fail_devid:
	return ret;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit led_exit(void)
{
    /* 按照相反的顺序注销 */
	device_destroy(gpioled.class, gpioled.devid);         /* 销毁类*/
	class_destroy(gpioled.class);                           /* 销毁设备节点 */
	cdev_del(&gpioled.cdev);                                /* 删除字符设备 */
	unregister_chrdev_region(gpioled.devid, NEWCHRLED_CNT); /* 注销设备号 */
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

   <2>、应用程序:spinlockApp.c 

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

#define LEDON 	1
#define LEDOFF 	0
/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, retvalue;
	char *filename;
	unsigned char cnt = 0;
	unsigned char databuf[1];
	
	if(argc != 3){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];

	/* 打开led驱动 */
	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}

	databuf[0] = atoi(argv[2]);	/* 要执行的操作:打开或关闭 */

	/* 向/dev/led文件写入数据 */
	retvalue = write(fd, databuf, sizeof(databuf));
	if(retvalue < 0){
		printf("LED Control Failed!\r\n");
		close(fd);
		return -1;
	}
	/* 模拟占用驱动25s */
	while(1){
		sleep(5);
		cnt++;
		printf("App running times:%d\r\n", cnt);
		if(cnt >= 5) break;
	}
	printf("App running finished!");
	/* 关闭文件 */
	retvalue = close(fd);
	if(retvalue < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

   <3>、编译命令:Makefile

KERNELDIR := /home/haut/itop_imx6ull/software/Linux_Source_nxpV4.1.15   # 内核路径
CURRENT_PATH := $(shell pwd)  # 当前路径

obj-m := spinlock.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

app:
	arm-linux-gnueabihf-gcc ./spinlockApp.c -o spinlockApp
cp:
	cp ./spinlock.ko ./spinlockApp /home/haut/itop_imx6ull/nfs_rootfs/lib/modules/4.1.15

   <4>、运行测试 

# 一、加载驱动
  cd lib/modules/4.1.15  //进入modprobe的工作目录
  depmod                 //第一次加载驱动的时候需要运行此命令
  modprobe spinlock.ko   //加载驱动

# 二、运行程序
  ./spinlockApp /dev/gpioled 1&  //打开LED灯,“&”表示在后台运行 spinlockApp 这个软件
  ./spinlockApp /dev/gpioled 0   //再次启动一个应用程序

5.8.3、信号量实验

        自旋锁是一直傻乎乎的在那里“自旋”等待,所以自旋锁拿到之后不能拥有太长时间,不然会导致别的程序获取此锁时一直循环等待,故使用时要保证自旋锁保护的临界区要尽可能的短。相比于自旋锁,信号量可以使线程进入休眠状态,而不是让它循环等待,故使用信号量会提高处理器的使用效率,但是,信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态以后会切换线程,切换线程就会有开销。总结一下信号量的特点:
        ①、因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。
        ②、因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
        ③、如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。
        信号量有一个信号量值,相当于一个房子有 10 把钥匙,这 10 把钥匙就相当于信号量值为10。因此,可以通过信号量来控制访问共享资源的访问数量,如果要想进房间,那就要先获取一把钥匙,信号量值减 1,直到 10 把钥匙都被拿走,信号量值为 0,这个时候就不允许任何人进入房间了,因为没钥匙了。如果有人从房间出来,那他要归还他所持有的那把钥匙,信号量值加 1,此时有 1 把钥匙了,那么可以允许进去一个人。相当于通过信号量控制访问资源的线程数,在初始化的时候将信号量值设置的大于 1,那么这个信号量就是计数型信号量,计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源如果要互斥的访问共享资源那么信号量的值就不能大于 1,此时的信号量就是一个二值信号量
   <1>、驱动程序:semaphore.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define NEWCHRLED_CNT	1		  	/* 设备号个数 */
#define NEWCHRLED_NAME	"gpioled"	/* 名字 */
#define LEDON 			1			/* 开灯 */
#define LEDOFF 			0			/* 关灯 */
/* gpioled设备结构体 */
struct gpioled_dev{
	dev_t devid;			/* 设备号 	*/
	struct cdev cdev;		/* cdev     */
	struct class *class;	/* 类 	    */
	struct device *device;	/* 设备 	*/
	int major;				/* 主设备号	 */
	int minor;				/* 次设备号  */
	struct device_node *nd; /* 设备节点  */
	int gpio_id;            /* gpio编号 */
	bool active_low;        /* gpio标志 */
	struct semaphore sem;   /* 信号量 */
};
struct gpioled_dev gpioled;	/* led设备 */

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &gpioled; /* 设置私有数据 */

	/* 获取信号量,获取成功则信号量的值减1,获取失败则进程进入休眠 */
	if (down_interruptible(&gpioled.sem)) {/* 获取信号量,进入休眠状态的进程可以被信号打断 */	
		return -ERESTARTSYS;
	}
#if 0 
	down(&gpioled.sem); /* 获取信号量,因为会导致休眠,因此不能在中断中使用 */
	own_interruptible(&gpioled.sem);/* 获取信号量,和down类似,只是使用down进入休眠状态的线程不能被信号打断。*/
									 /* 而使用此函数进入休眠以后是可以被信号打断的 */
	int down_trylock(struct semaphore *sem); /* 尝试获取信号量,如果能获取到信号量就获取,并且返回 0。*/
											 /* 如果不能就返回非 0,并且不会进入休眠。*/
#endif

	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;
	struct gpioled_dev *dev = filp->private_data;
	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}
	ledstat = databuf[0]; /* 获取状态值 */
	if(ledstat == LEDON) {	
		gpio_set_value(dev->gpio_id, !gpioled.active_low); /* 打开LED灯,设置为有效电平 */
	} else if(ledstat == LEDOFF) {
		gpio_set_value(dev->gpio_id, gpioled.active_low);  /* 关闭LED灯,设置为无效电平 */
	}
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
	struct gpioled_dev *dev = filp->private_data;
	up(&dev->sem); /* 释放信号量,信号量的值加1 */
	return 0;
}

/* 设备操作函数 */
static struct file_operations gpioled_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = led_release,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init led_init(void)
{
	int ret = 0;
	struct property *proper;
	enum of_gpio_flags flags;
	/* 初始化信号量 */
	sema_init(&gpioled.sem, 1);
	/*一、获取设备树中的属性数据 */
	/* 1.1、获取设备节点:leds */
	gpioled.nd = of_find_node_by_path("/leds");
	if(gpioled.nd == NULL){
		printk("pinctrl_gpio-led node not find!\r\n");
		return -EINVAL;
	} else {
		printk("pinctrl_gpio-led node find!\r\n");
	}
	/* 1.2、获取compatible属性内容 */
	proper = of_find_property(gpioled.nd, "compatible", NULL);
	if(proper == NULL) {
		printk("compatible property find failed\r\n");
	} else {
		printk("compatible = %s\r\n", (char*)proper->value);
	}
	/* 1.3、获取gpio的属性,gpio的编号和flags */
	if (of_gpio_named_count(gpioled.nd, "led-gpio") > 0) {
		gpioled.gpio_id = of_get_named_gpio_flags(gpioled.nd, "led-gpio", 0, &flags);
		if (gpioled.gpio_id < 0){
			printk("can't get led-gpio");
			return -EINVAL;
		} /* 注意:低电平有效的宏为1,高电平有效的宏为0 */
		gpioled.active_low = (flags == OF_GPIO_ACTIVE_LOW); 
		printk("gpio_id = %d\r\n", gpioled.gpio_id);
		printk("gpio_flags = %d\r\n", flags);
	} else {
		printk("not have led-gpio");
		return -EINVAL;
	}
	/* 1.4、设置GPIO1_IO03为无效电平 */
	if(gpioled.active_low){
		ret = gpio_direction_output(gpioled.gpio_id, 1);  /* 设置为无效电平 */
	} else {
		ret = gpio_direction_output(gpioled.gpio_id, 0);  /* 设置为无效电平 */
	}	
	if(ret < 0) printk("can't set gpio!\r\n");

	/* 注册字符设备驱动 */
	/* 1、申请设备号 */
	gpioled.major = 0; /* 设置为0,即没有定义设备号 */
	if (gpioled.major) { /* 定义了设备号 */
		gpioled.devid = MKDEV(gpioled.major, 0);
		ret = register_chrdev_region(gpioled.devid,   //起始设备号
							   		 NEWCHRLED_CNT,   //设备数量
							   		 NEWCHRLED_NAME); //设备名称
	} else { /* 没有定义设备号 */
		ret = alloc_chrdev_region(&gpioled.devid,  //储存设备号的变量
								  0,               //起始次设备号
								  NEWCHRLED_CNT,   //设备数量
								  NEWCHRLED_NAME); //设备名称
		gpioled.major = MAJOR(gpioled.devid);      //获取分配号的主设备号
		gpioled.minor = MINOR(gpioled.devid);      //获取分配号的次设备号
	}
	if(ret < 0){
		printk("gpioled chrdev_region err!\r\n"); 
		goto fail_devid;
	}
	printk("gpioled major=%d,minor=%d\r\n", gpioled.major, gpioled.minor);	
	/* 2、注册字符设备 */
	gpioled.cdev.owner = THIS_MODULE;
	cdev_init(&gpioled.cdev, &gpioled_fops); //初始化一个cdev
	ret = cdev_add(&gpioled.cdev, gpioled.devid, NEWCHRLED_CNT); //添加一个cdev
	if(ret < 0 ){
		printk("gpioled cdev_add err!\r\n");
		goto fail_cdev;
	}
	/* 3、创建设备节点 */
	gpioled.class = class_create(THIS_MODULE, NEWCHRLED_NAME); //先创建一个类(创建的类在/sys/class中)
	if (IS_ERR(gpioled.class)) {
		printk("gpioled class_create err!\r\n"); 
		ret = PTR_ERR(gpioled.class);
		goto fail_class;
	}
	gpioled.device = device_create(gpioled.class,  //该设备依附的类
									 NULL,             //父设备
									 gpioled.devid,  //设备号
									 NULL,             //私有数据
									 NEWCHRLED_NAME);  //设备名称
	if (IS_ERR(gpioled.device)) {
		printk("gpioled device_create err!\r\n"); 
		ret = PTR_ERR(gpioled.device);
		goto fail_device;
	}
	return 0;
fail_device:
	class_destroy(gpioled.class); 
fail_class:
	cdev_del(&gpioled.cdev); 
fail_cdev:
	unregister_chrdev_region(gpioled.devid, NEWCHRLED_CNT);
fail_devid:
	return ret;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit led_exit(void)
{
    /* 按照相反的顺序注销 */
	device_destroy(gpioled.class, gpioled.devid);         /* 销毁类*/
	class_destroy(gpioled.class);                           /* 销毁设备节点 */
	cdev_del(&gpioled.cdev);                                /* 删除字符设备 */
	unregister_chrdev_region(gpioled.devid, NEWCHRLED_CNT); /* 注销设备号 */
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

   <2>、应用程序:semaphoreApp.c

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

#define LEDON 	1
#define LEDOFF 	0
/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, retvalue;
	char *filename;
	unsigned char cnt = 0;
	unsigned char databuf[1];
	
	if(argc != 3){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];

	/* 打开xxx驱动 */
	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}

	databuf[0] = atoi(argv[2]);	/* 要执行的操作:打开或关闭 */

	/* 向/dev/xxx文件写入数据 */
	retvalue = write(fd, databuf, sizeof(databuf));
	if(retvalue < 0){
		printf("LED Control Failed!\r\n");
		close(fd);
		return -1;
	}
	/* 模拟占用驱动25s */
	while(1){
		sleep(5);
		cnt++;
		printf("App running times:%d\r\n", cnt);
		if(cnt >= 5) break;
	}
	printf("App running finished!");
	/* 关闭文件 */
	retvalue = close(fd);
	if(retvalue < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

   <3>、编译命令:Makefile

KERNELDIR := /home/haut/itop_imx6ull/software/Linux_Source_nxpV4.1.15   # 内核路径
CURRENT_PATH := $(shell pwd)  # 当前路径

obj-m := semaphore.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

app:
	arm-linux-gnueabihf-gcc ./semaphoreApp.c -o semaphoreApp
cp:
	cp ./semaphore.ko ./semaphoreApp /home/haut/itop_imx6ull/nfs_rootfs/lib/modules/4.1.15

   <4>、运行测试

# 一、加载驱动
  cd lib/modules/4.1.15  //进入modprobe的工作目录
  depmod                 //第一次加载驱动的时候需要运行此命令
  modprobe semaphore.ko   //加载驱动

# 二、运行程序
  ./semaphoreApp /dev/gpioled 1&  //打开LED灯,“&”表示在后台运行 spinlockApp 这个软件
  ./semaphoreApp /dev/gpioled 0   //再次启动一个应用程序

5.8.4、互斥量实验

        将信号量的值设置为 1 就可以使用信号量进行互斥访问了,虽然可以通过信号量实现互斥,但是 Linux 提供了一个比信号量更专业的机制来进行互斥,它就是互斥体—mutex。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体。在我们编写 Linux 驱动的时候遇到需要互斥访问的地方建议使用 mutex。在使用 mutex 之前要先定义一个 mutex 变量。在使用 mutex 的时候要注意如下几点:
        ①、mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
        ②、和信号量一样, mutex 保护的临界区可以调用引起阻塞的 API 函数。
        ③、因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁。
   <1>、驱动程序:mutex.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define NEWCHRLED_CNT	1		  	/* 设备号个数 */
#define NEWCHRLED_NAME	"gpioled"	/* 名字 */
#define LEDON 			1			/* 开灯 */
#define LEDOFF 			0			/* 关灯 */
/* gpioled设备结构体 */
struct gpioled_dev{
	dev_t devid;			/* 设备号 	*/
	struct cdev cdev;		/* cdev     */
	struct class *class;	/* 类 	    */
	struct device *device;	/* 设备 	*/
	int major;				/* 主设备号	 */
	int minor;				/* 次设备号  */
	struct device_node *nd; /* 设备节点  */
	int gpio_id;            /* gpio编号 */
	bool active_low;        /* gpio标志 */
	struct mutex lock;      /* 互斥体 */
};
struct gpioled_dev gpioled;	/* led设备 */

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &gpioled; /* 设置私有数据 */
	/* 获取互斥体,可以被信号打断 */
	if (mutex_lock_interruptible(&gpioled.lock)) {
		return -ERESTARTSYS;
	}
	
#if 0
	mutex_lock(&gpioled.lock);	/* 不能被信号打断 */
#endif
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;
	struct gpioled_dev *dev = filp->private_data;
	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}
	ledstat = databuf[0]; /* 获取状态值 */
	if(ledstat == LEDON) {	
		gpio_set_value(dev->gpio_id, !gpioled.active_low); /* 打开LED灯,设置为有效电平 */
	} else if(ledstat == LEDOFF) {
		gpio_set_value(dev->gpio_id, gpioled.active_low);  /* 关闭LED灯,设置为无效电平 */
	}
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
	struct gpioled_dev *dev = filp->private_data;
	mutex_unlock(&dev->lock);/* 释放互斥锁 */
	return 0;
}

/* 设备操作函数 */
static struct file_operations gpioled_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = led_release,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init led_init(void)
{
	int ret = 0;
	struct property *proper;
	enum of_gpio_flags flags;
	/* 初始化互斥体 */
	mutex_init(&gpioled.lock);
	/*一、获取设备树中的属性数据 */
	/* 1.1、获取设备节点:leds */
	gpioled.nd = of_find_node_by_path("/leds");
	if(gpioled.nd == NULL){
		printk("pinctrl_gpio-led node not find!\r\n");
		return -EINVAL;
	} else {
		printk("pinctrl_gpio-led node find!\r\n");
	}
	/* 1.2、获取compatible属性内容 */
	proper = of_find_property(gpioled.nd, "compatible", NULL);
	if(proper == NULL) {
		printk("compatible property find failed\r\n");
	} else {
		printk("compatible = %s\r\n", (char*)proper->value);
	}
	/* 1.3、获取gpio的属性,gpio的编号和flags */
	if (of_gpio_named_count(gpioled.nd, "led-gpio") > 0) {
		gpioled.gpio_id = of_get_named_gpio_flags(gpioled.nd, "led-gpio", 0, &flags);
		if (gpioled.gpio_id < 0){
			printk("can't get led-gpio");
			return -EINVAL;
		} /* 注意:低电平有效的宏为1,高电平有效的宏为0 */
		gpioled.active_low = (flags == OF_GPIO_ACTIVE_LOW); 
		printk("gpio_id = %d\r\n", gpioled.gpio_id);
		printk("gpio_flags = %d\r\n", flags);
	} else {
		printk("not have led-gpio");
		return -EINVAL;
	}
	/* 1.4、设置GPIO1_IO03为无效电平 */
	if(gpioled.active_low){
		ret = gpio_direction_output(gpioled.gpio_id, 1);  /* 设置为无效电平 */
	} else {
		ret = gpio_direction_output(gpioled.gpio_id, 0);  /* 设置为无效电平 */
	}	
	if(ret < 0) printk("can't set gpio!\r\n");

	/* 注册字符设备驱动 */
	/* 1、申请设备号 */
	gpioled.major = 0; /* 设置为0,即没有定义设备号 */
	if (gpioled.major) { /* 定义了设备号 */
		gpioled.devid = MKDEV(gpioled.major, 0);
		ret = register_chrdev_region(gpioled.devid,   //起始设备号
							   		 NEWCHRLED_CNT,   //设备数量
							   		 NEWCHRLED_NAME); //设备名称
	} else { /* 没有定义设备号 */
		ret = alloc_chrdev_region(&gpioled.devid,  //储存设备号的变量
								  0,               //起始次设备号
								  NEWCHRLED_CNT,   //设备数量
								  NEWCHRLED_NAME); //设备名称
		gpioled.major = MAJOR(gpioled.devid);      //获取分配号的主设备号
		gpioled.minor = MINOR(gpioled.devid);      //获取分配号的次设备号
	}
	if(ret < 0){
		printk("gpioled chrdev_region err!\r\n"); 
		goto fail_devid;
	}
	printk("gpioled major=%d,minor=%d\r\n", gpioled.major, gpioled.minor);	
	/* 2、注册字符设备 */
	gpioled.cdev.owner = THIS_MODULE;
	cdev_init(&gpioled.cdev, &gpioled_fops); //初始化一个cdev
	ret = cdev_add(&gpioled.cdev, gpioled.devid, NEWCHRLED_CNT); //添加一个cdev
	if(ret < 0 ){
		printk("gpioled cdev_add err!\r\n");
		goto fail_cdev;
	}
	/* 3、创建设备节点 */
	gpioled.class = class_create(THIS_MODULE, NEWCHRLED_NAME); //先创建一个类(创建的类在/sys/class中)
	if (IS_ERR(gpioled.class)) {
		printk("gpioled class_create err!\r\n"); 
		ret = PTR_ERR(gpioled.class);
		goto fail_class;
	}
	gpioled.device = device_create(gpioled.class,  //该设备依附的类
									 NULL,             //父设备
									 gpioled.devid,  //设备号
									 NULL,             //私有数据
									 NEWCHRLED_NAME);  //设备名称
	if (IS_ERR(gpioled.device)) {
		printk("gpioled device_create err!\r\n"); 
		ret = PTR_ERR(gpioled.device);
		goto fail_device;
	}
	return 0;
fail_device:
	class_destroy(gpioled.class); 
fail_class:
	cdev_del(&gpioled.cdev); 
fail_cdev:
	unregister_chrdev_region(gpioled.devid, NEWCHRLED_CNT);
fail_devid:
	return ret;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit led_exit(void)
{
    /* 按照相反的顺序注销 */
	device_destroy(gpioled.class, gpioled.devid);         /* 销毁类*/
	class_destroy(gpioled.class);                           /* 销毁设备节点 */
	cdev_del(&gpioled.cdev);                                /* 删除字符设备 */
	unregister_chrdev_region(gpioled.devid, NEWCHRLED_CNT); /* 注销设备号 */
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

    <2>、应用程序:mutexApp.c

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

#define LEDON 	1
#define LEDOFF 	0
/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, retvalue;
	char *filename;
	unsigned char cnt = 0;
	unsigned char databuf[1];
	
	if(argc != 3){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];

	/* 打开xxx驱动 */
	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}

	databuf[0] = atoi(argv[2]);	/* 要执行的操作:打开或关闭 */

	/* 向/dev/xxx文件写入数据 */
	retvalue = write(fd, databuf, sizeof(databuf));
	if(retvalue < 0){
		printf("LED Control Failed!\r\n");
		close(fd);
		return -1;
	}
	/* 模拟占用驱动25s */
	while(1){
		sleep(5);
		cnt++;
		printf("App running times:%d\r\n", cnt);
		if(cnt >= 5) break;
	}
	printf("App running finished!");
	/* 关闭文件 */
	retvalue = close(fd);
	if(retvalue < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

   <3>、编译命令:Makefile

KERNELDIR := /home/haut/itop_imx6ull/software/Linux_Source_nxpV4.1.15   # 内核路径
CURRENT_PATH := $(shell pwd)  # 当前路径

obj-m := mutex.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

app:
	arm-linux-gnueabihf-gcc ./mutexApp.c -o mutexApp
cp:
	cp ./mutex.ko ./mutexApp /home/haut/itop_imx6ull/nfs_rootfs/lib/modules/4.1.15

   <4>、运行测试

# 一、加载驱动
  cd lib/modules/4.1.15 //进入modprobe的工作目录
  depmod                //第一次加载驱动的时候需要运行此命令
  modprobe mutex.ko     //加载驱动

# 二、运行程序
  ./mutexApp /dev/gpioled 1&  //打开LED灯,“&”表示在后台运行 spinlockApp 这个软件
  ./mutexApp /dev/gpioled 0   //再次启动一个应用程序

5.09、Linux驱动之按键的输入

        在前面我们都是使用的 GPIO 输出功能,还没有用过 GPIO 输入功能,本章我们就来学习一下如果在 Linux 下编写 GPIO 输入驱动程序,同时利用原子操作来对按键值进行保护(注意此次实验对驱动的并发访问没有处理,可参考上一章进行添加)。其次,前几章的实验也没有对GPIO的使用做并发处理,其实gpio子系统是支持IO互斥的,就是使用IO是要先申请再使用,当某个IO被申请成功且没有执行释放的条件下,其它程序是无法成功申请此IO的(注意:这种申请=>使用=>释放的顺序原则仅在所有程序都以此方式书写时有互斥作用,如果别的程序不遵守这个顺序,上来就直接使用IO也是可以的,这种情况下,互斥也就失去了作用。只能说gpio子系统希望我们按这个顺序来,至于尊不遵守,由驱动开发人员决定)。KEY按键的电路图如下:

  <1>、添加设备树节点 

  <2>、驱动程序:key.c 

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define NEWCHRLED_CNT	1		/* 设备号个数 */
#define NEWCHRLED_NAME	"key"	/* 名字 */
/* 定义按键值 */
#define KEY0VALUE		0XF0	/* 按键值 */
#define INVAKEY			0X00	/* 无效的按键值 */

/* key设备结构体 */
struct key_dev{
	dev_t devid;			/* 设备号 	*/
	struct cdev cdev;		/* cdev    */
	struct class *class;	/* 类 	   */
	struct device *device;	/* 设备 	*/
	int major;				/* 主设备号	 */
	int minor;				/* 次设备号  */
	struct device_node *nd; /* 设备节点  */
	int gpio_id;            /* gpio编号 */
	bool active_low;        /* gpio标志 */
	atomic_t keyvalue;      /* 原子变量  */
};
struct key_dev keydev;	/* led设备 */
/*
 * @description	: 初始化按键IO,open函数打开驱动的时候
 * 				  初始化按键所使用的GPIO引脚。
 * @param 		: 无
 * @return 		: 无
 */
static int keyio_init(void)
{
	int ret = 0;
	/* 设置IO为输入 */ 
	/* 使用gpio前一定要先申请,否则一个驱动使用IO,另一个驱动又来使用就会出现严重错误 */
	ret = gpio_request(keydev.gpio_id, "key0");	/* 申请IO,返回0:成功,其它:失败 */
	if(ret){
		printk("gpio request failed!\r\n");
		return ret;
	}
	ret = gpio_direction_input(keydev.gpio_id);
	if(ret < 0) {
		printk("gpio set failed!\r\n");
		return ret;
	}
	return 0;
}
/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int key_open(struct inode *inode, struct file *filp)
{
	int ret = 0;
	filp->private_data = &keydev; /* 设置私有数据 */
	ret = keyio_init();
	if(ret < 0) return ret;
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int ret = 0;
	int value;
	struct key_dev *dev = filp->private_data;
	if(gpio_get_value(dev->gpio_id) == 0){ /* key0按下 */
		while(!gpio_get_value(dev->gpio_id));		/* 等待按键释放 */
		atomic_set(&dev->keyvalue, KEY0VALUE);	
	} else {
		atomic_set(&dev->keyvalue, INVAKEY);		/* 无效的按键值 */
	}
	value = atomic_read(&dev->keyvalue);
	ret = copy_to_user(buf, &value, sizeof(value));
	return ret;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int key_release(struct inode *inode, struct file *filp)
{
	struct key_dev *dev = filp->private_data;
	gpio_free(dev->gpio_id);
	return 0;
}

/* 设备操作函数 */
static struct file_operations keydev_fops = {
	.owner = THIS_MODULE,
	.open = key_open,
	.read = key_read,
	.write = key_write,
	.release = key_release,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
/* 内核中的也有一个名为key_init()的函数,这里加个x,是为了避免冲突 */
static int __init keyx_init(void) /* 避免与内核中的 */
{
	int ret = 0;
	struct property *proper;
	enum of_gpio_flags flags;
	/* 初始化原子变量 */
	atomic_set(&keydev.keyvalue, INVAKEY); /* INVAKEY:0x00 */
	/*一、获取设备树中的属性数据 */
	/* 1.1、获取设备节点:key */
	keydev.nd = of_find_node_by_path("/key");
	if(keydev.nd == NULL){
		printk("pinctrl key-led node not find!\r\n");
		return -EINVAL;
	} else {
		printk("pinctrl key-led node find!\r\n");
	}
	/* 1.2、获取compatible属性内容 */
	proper = of_find_property(keydev.nd, "compatible", NULL);
	if(proper == NULL) {
		printk("compatible property find failed\r\n");
	} else {
		printk("compatible = %s\r\n", (char*)proper->value);
	}
	/* 1.3、获取gpio的属性,gpio的编号和flags */
	if (of_gpio_named_count(keydev.nd, "key-gpio") > 0) {
		keydev.gpio_id = of_get_named_gpio_flags(keydev.nd, "key-gpio", 0, &flags);
		if (keydev.gpio_id < 0){
			printk("can't get key-gpio");
			return -EINVAL;
		} /* 注意:低电平有效的宏为1,高电平有效的宏为0 */
		keydev.active_low = (flags == OF_GPIO_ACTIVE_LOW); 
		printk("gpio_id = %d\r\n", keydev.gpio_id);
		printk("gpio_flags = %d\r\n", flags);
	} else {
		printk("not have key-gpio");
		return -EINVAL;
	}
	/* 注册字符设备驱动 */
	/* 1、申请设备号 */
	keydev.major = 0; /* 设置为0,即没有定义设备号 */
	if (keydev.major) { /* 定义了设备号 */
		keydev.devid = MKDEV(keydev.major, 0);
		ret = register_chrdev_region(keydev.devid,   //起始设备号
							   		 NEWCHRLED_CNT,   //设备数量
							   		 NEWCHRLED_NAME); //设备名称
	} else { /* 没有定义设备号 */
		ret = alloc_chrdev_region(&keydev.devid,  //储存设备号的变量
								  0,               //起始次设备号
								  NEWCHRLED_CNT,   //设备数量
								  NEWCHRLED_NAME); //设备名称
		keydev.major = MAJOR(keydev.devid);      //获取分配号的主设备号
		keydev.minor = MINOR(keydev.devid);      //获取分配号的次设备号
	}
	if(ret < 0){
		printk("keydev chrdev_region err!\r\n"); 
		goto fail_devid;
	}
	printk("keydev major=%d,minor=%d\r\n", keydev.major, keydev.minor);	
	/* 2、注册字符设备 */
	keydev.cdev.owner = THIS_MODULE;
	cdev_init(&keydev.cdev, &keydev_fops); //初始化一个cdev
	ret = cdev_add(&keydev.cdev, keydev.devid, NEWCHRLED_CNT); //添加一个cdev
	if(ret < 0 ){
		printk("keydev cdev_add err!\r\n");
		goto fail_cdev;
	}
	/* 3、创建设备节点 */
	keydev.class = class_create(THIS_MODULE, NEWCHRLED_NAME); //先创建一个类(创建的类在/sys/class中)
	if (IS_ERR(keydev.class)) {
		printk("keydev class_create err!\r\n"); 
		ret = PTR_ERR(keydev.class);
		goto fail_class;
	}
	keydev.device = device_create(keydev.class,  //该设备依附的类
									 NULL,             //父设备
									 keydev.devid,  //设备号
									 NULL,             //私有数据
									 NEWCHRLED_NAME);  //设备名称
	if (IS_ERR(keydev.device)) {
		printk("keydev device_create err!\r\n"); 
		ret = PTR_ERR(keydev.device);
		goto fail_device;
	}
	return 0;
fail_device:
	class_destroy(keydev.class); 
fail_class:
	cdev_del(&keydev.cdev); 
fail_cdev:
	unregister_chrdev_region(keydev.devid, NEWCHRLED_CNT);
fail_devid:
	return ret;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit keyx_exit(void)
{
    /* 按照相反的顺序注销 */
	device_destroy(keydev.class, keydev.devid);            /* 销毁类*/
	class_destroy(keydev.class);                           /* 销毁设备节点 */
	cdev_del(&keydev.cdev);                                /* 删除字符设备 */
	unregister_chrdev_region(keydev.devid, NEWCHRLED_CNT); /* 注销设备号 */
	gpio_free(keydev.gpio_id);                             /* 释放gpio */
}

module_init(keyx_init);
module_exit(keyx_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

  <3>、应用程序:keyApp.c

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

/* 定义按键值 */
#define KEY0VALUE	0XF0
#define INVAKEY		0X00
/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, ret;
	char *filename;
	int keyvalue;
	
	if(argc != 2){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];

	/* 打开xxx驱动 */
	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}

	/* 循环读取按键值数据! */
	while(1) {
		usleep(1000); /* 睡眠1ms,避免程序cpu占用过高 */ 
		read(fd, &keyvalue, sizeof(keyvalue));
		if (keyvalue == KEY0VALUE) {	/* KEY0 */
			printf("KEY0 Press, value = %#X\r\n", keyvalue);	/* 按下 */
		}
	}

	ret= close(fd); /* 关闭文件 */
	if(ret < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

  <4>、编译命令:Makefile 

KERNELDIR := /home/haut/itop_imx6ull/software/Linux_Source_nxpV4.1.15   # 内核路径
CURRENT_PATH := $(shell pwd)  # 当前路径

obj-m := key.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

app:
	arm-linux-gnueabihf-gcc ./keyApp.c -o keyApp
cp:
	cp ./key.ko ./keyApp /home/haut/itop_imx6ull/nfs_rootfs/lib/modules/4.1.15

  <5>、运行测试

# 一、加载驱动
  cd lib/modules/4.1.15 //进入modprobe的工作目录
  depmod                //第一次加载驱动的时候需要运行此命令
  modprobe key.ko       //加载驱动

# 二、运行程序
  ./keyApp /dev/key

5.10、Linux驱动之内核定时器

        Linux 内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序、对于我们驱动编写者来说最常用的定时器。硬件定时器提供时钟源,时钟源的频率可以设置, 设置好以后就周期性的产生定时中断,系统使用定时中断来计时。中断周期性产生的频率就是系统频率,也叫做节拍率(tick rate)(有的资料也叫系统频率),比如 1000Hz, 100Hz 等等说的就是系统节拍率。系统节拍率是可以设置的,单位是 Hz,我们在编译 Linux 内核的时候可以通过图形化界面设置系统节拍率,按照如下路径打开配置界面:

         从图可以看出,可选的系统节拍率为 100Hz、 200Hz、 250Hz、 300Hz、 500Hz和1000Hz,默认情况下选择 100Hz。设置好以后打开 Linux 内核源码根目录下的.config 文件,在
此文件中有如下图所示定义:
        图中的 CONFIG_HZ 为 100, Linux 内核会使用 CONFIG_HZ 来设置自己的系统时钟。打开文件 include/asm-generic/param.h,有如下内容:
        第 7 行定义了一个宏 HZ,宏 HZ 就是 CONFIG_HZ,因此 HZ=100,我们后面编写 Linux
驱动的时候会常常用到 HZ,因为 HZ 表示一秒的节拍数,也就是频率。
        Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会将 jiffies 初始化为 0, jiffies 定义在文件 include/linux/jiffies.h 中,定义如下:

        第 76 行,定义了一个 64 位的 jiffies_64。
        第 77 行,定义了一个 unsigned long 类型的 32 位的 jiffies。
        jiffies_64 和 jiffies 其实是同一个东西, jiffies_64用于64位系统,而jiffies用于32位系统。为了兼容不同的硬件,jiffies其实就是jiffies_64的低32位, jiffies_64 和 jiffies 的结构如图所示:
        当我们访问 jiffies 的时候其实访问的是 jiffies_64 的低 32 位,使用 get_jiffies_64 这个函数可以获取 jiffies_64 的值。在 32 位的系统上读取 jiffies 的值,在 64 位的系统上 jiffes 和 jiffies_64表示同一个变量,因此也可以直接读取 jiffies 的值。所以不管是 32 位的系统还是 64 位系统,都可以使用 jiffies。
        前面说了 HZ 表示每秒的节拍数, jiffies 表示系统运行的 jiffies 节拍数,所以 jiffies/HZ 就是系统运行时间,单位为秒。不管是 32 位还是 64 位的 jiffies,都有溢出的风险,溢出以后会重新从 0 开始计数,相当于绕回来了,因此有些资料也将这个现象也叫做绕回。假如 HZ 为最大值 1000 的时候, 32 位的 jiffies 只需要 49.7 天就发生了绕回,对于 64 位的 jiffies 来说大概需要5.8 亿年才能绕回,因此 jiffies_64 的绕回忽略不计。处理 32 位 jiffies 的绕回显得尤为重要,Linux 内核提供了如下表所示的几个 API 函数来处理绕回
        如果 unkown 超过 known 的话,time_after 函数返回真,否则返回假。如果 unkown 没有超过 known 的话 time_before 函数返回真,否则返回假。 time_after_eq 函数和 time_after 函数似,只是多了判断等于这个条件。同理, time_before_eq 函数和 time_before 函数也类似。比如我们
要判断某段代码执行时间有没有超时,此时就可以使用如下所示代码:

unsigned long timeout;
timeout = jiffies + (2 * HZ); /* 超时的时间点,设置为2秒后 */

/* 具体的代码 */
if(time_before(jiffies, timeout)) {/* 判断有没有超时 */
    /* 超时未发生 */
} else {
    /* 超时发生 */
}

        为了方便开发,Linux内核提供了几个jiffies和ms、us、ns之间的转换函数,如表所示:

        Linux 内核定时器采用系统时钟来实现,并不是我们在裸机篇中讲解的 PIT 等硬件定时器。Linux 内核定时器使用只需要提供超时时间(相当于定时值)和定时处理函数即可,因为其是基于系统时钟实现的软件模拟定时器,故使用内核定时器不需要做一大堆的寄存器初始化工作。需要注意的是内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器Linux 内核使用 timer_list 结构体表示内核定时器,timer_list 定义在文件include/linux/timer.h 中,示例程序如下:(使用leds设备树节点)。
   <1>、驱动程序:timer.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define TIMER_CNT	  1		  	        /* 设备号个数 */
#define TIMER_NAME	  "timer-led"	    /* 设备名称   */
#define OPEN_CMD      (_IO(0XEF, 0x1))  /* 打开定时器 */
#define CLOSE_CMD     (_IO(0XEF, 0x2))  /* 关闭定时器 */
#define SETPERIOD_CMD (_IO(0XEF, 0x3))  /* 设置定时器周期 */
#define LEDON 		  1	                /* 开灯 */  
#define LEDOFF 		  0		            /* 关灯 */
/* timer设备结构体 */
struct timer_dev{
	dev_t devid;			 /* 设备号 	*/
	struct cdev cdev;		 /* cdev    */
	struct class *class;	 /* 类 	    */
	struct device *device;	 /* 设备 	 */
	int major;				 /* 主设备号  */
	int minor;				 /* 次设备号  */
	struct device_node *nd;  /* 设备节点  */
	int gpio_id;             /* gpio编号 */
	bool active_low;         /* gpio标志 */
	int dev_status;          /* 设备状态  */
	int timeperiod;          /* 定义周期(ms) */
	struct timer_list timer; /* 定时器 */
	spinlock_t lock;         /* 自旋锁变量 */
};
struct timer_dev timerdev;	 /* 设备 */
/*
 * @description	: 初始化LED灯IO,open函数打开驱动的时候
 * 				  初始化LED灯所使用的GPIO引脚。
 * @param 		: 无
 * @return 		: 无
 */
static int ledio_init(void)
{
	int ret = 0;
	/* 使用gpio前一定要先申请,否则一个驱动使用IO,另一个驱动又来使用就会出现严重错误 */
	ret = gpio_request(timerdev.gpio_id, "led2");	/* 申请IO,返回0:成功,其它:失败 */
	if(ret){
		printk("gpio request failed!\r\n");
		return ret;
	}
	return 0;
}
/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int timer_open(struct inode *inode, struct file *filp)
{
	int ret = 0;
	filp->private_data = &timerdev; /* 设置私有数据 */
	timerdev.timeperiod = 1000; /* 默认周期为1000ms */
	ret = ledio_init();
	if(ret < 0) return ret;
	return 0;
}
/*
 * @description		: ioctl函数,
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - cmd 	: 应用程序发送过来的命令
 * @param - arg 	: 参数
 * @return 			: 0 成功;其他 失败
 */
static long timer_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	struct timer_dev *dev =  (struct timer_dev *)filp->private_data;
	int timerperiod;
	unsigned long flags;
	switch (cmd) {
		case OPEN_CMD: /* 打开定时器 */
			spin_lock_irqsave(&dev->lock, flags); /* 上锁 */
			timerperiod = dev->timeperiod;
			spin_unlock_irqrestore(&dev->lock, flags); /* 开锁 */
			mod_timer(&dev->timer, jiffies + msecs_to_jiffies(timerperiod)); /* 修改定时器值,并激活定时器 */
			break;
		case CLOSE_CMD: /* 关闭定时器 */
			del_timer_sync(&dev->timer);
			break;
		case SETPERIOD_CMD: /* 设置定时器周期 */
			spin_lock_irqsave(&dev->lock, flags);
			dev->timeperiod = arg;
			spin_unlock_irqrestore(&dev->lock, flags);
			mod_timer(&dev->timer, jiffies + msecs_to_jiffies(arg));
			break;
		default:
			break;
	}
	return 0;
}

/* 定时器回调函数 */
void timer_function(unsigned long arg)
{
	struct timer_dev *dev = (struct timer_dev *)arg;
	static int sta = 1;
	int timerperiod;
	unsigned long flags;

	sta = !sta;		/* 每次都取反,实现LED灯反转 */
	gpio_set_value(dev->gpio_id, sta);
	
	/* 重启定时器 */
	spin_lock_irqsave(&dev->lock, flags);
	timerperiod = dev->timeperiod;
	spin_unlock_irqrestore(&dev->lock, flags);
	mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timeperiod)); 
 }
/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int timer_release(struct inode *inode, struct file *filp)
{
	struct timer_dev *dev = (struct timer_dev *)filp->private_data;
 	gpio_free(dev->gpio_id);/* 释放gpio */ 
	return 0;
}
/* 设备操作函数 */
static struct file_operations timerdev_fops = {
	.owner = THIS_MODULE,
	.open = timer_open,
	.release = timer_release,
	.unlocked_ioctl = timer_unlocked_ioctl,
};
/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init timer_init(void)
{
	int ret = 0;
	struct property *proper;
	enum of_gpio_flags flags;
	/* 初始化自旋锁 */ 
	spin_lock_init(&timerdev.lock); 
	/*一、获取设备树中的属性数据 */
	/* 1.1、获取设备节点:leds */
	timerdev.nd = of_find_node_by_path("/leds");
	if(timerdev.nd == NULL){
		printk("pinctrl_gpio-led node not find!\r\n");
		return -EINVAL;
	} else {
		printk("pinctrl_gpio-led node find!\r\n");
	}
	/* 1.2、获取compatible属性内容 */
	proper = of_find_property(timerdev.nd, "compatible", NULL);
	if(proper == NULL) {
		printk("compatible property find failed\r\n");
	} else {
		printk("compatible = %s\r\n", (char*)proper->value);
	}
	/* 1.3、获取gpio的属性,gpio的编号和flags */
	if (of_gpio_named_count(timerdev.nd, "led-gpio") > 0) {
		timerdev.gpio_id = of_get_named_gpio_flags(timerdev.nd, "led-gpio", 0, &flags);
		if (timerdev.gpio_id < 0){
			printk("can't get led-gpio");
			return -EINVAL;
		} /* 注意:低电平有效的宏为1,高电平有效的宏为0 */
		timerdev.active_low = (flags == OF_GPIO_ACTIVE_LOW); 
		printk("gpio_id = %d\r\n", timerdev.gpio_id);
		printk("gpio_flags = %d\r\n", flags);
	} else {
		printk("not have led-gpio");
		return -EINVAL;
	}
	/* 1.4、设置GPIO为无效电平 */
	ret = gpio_request(timerdev.gpio_id, "led2");	/* 申请IO,返回0:成功,其它:失败 */
	if(ret){
		printk("gpio request failed!\r\n");
		return ret;
	}
	/* 设置led-gpio为无效电平 */
	if(timerdev.active_low){
		ret = gpio_direction_output(timerdev.gpio_id, 1);  /* 设置为无效电平 */
	} else {
		ret = gpio_direction_output(timerdev.gpio_id, 0);  /* 设置为无效电平 */
	}	
	if(ret < 0) {
		printk("can't set gpio!\r\n");
		return ret;
	}
	gpio_free(timerdev.gpio_id); /* 释放gpio */ 
	/* 注册字符设备驱动 */
	/* 1、申请设备号 */
	timerdev.major = 0; /* 设置为0,即没有定义设备号 */
	if (timerdev.major) { /* 定义了设备号 */
		timerdev.devid = MKDEV(timerdev.major, 0);
		ret = register_chrdev_region(timerdev.devid, //起始设备号
							   		 TIMER_CNT,      //设备数量
							   		 TIMER_NAME);    //设备名称
	} else { /* 没有定义设备号 */
		ret = alloc_chrdev_region(&timerdev.devid,   //储存设备号的变量
								  0,                 //起始次设备号
								  TIMER_CNT,         //设备数量
								  TIMER_NAME);       //设备名称
		timerdev.major = MAJOR(timerdev.devid);      //获取分配号的主设备号
		timerdev.minor = MINOR(timerdev.devid);      //获取分配号的次设备号
	}
	if(ret < 0){
		printk("timerdev chrdev_region err!\r\n"); 
		goto fail_devid;
	}
	printk("timerdev major=%d,minor=%d\r\n", timerdev.major, timerdev.minor);	
	/* 2、注册字符设备 */
	timerdev.cdev.owner = THIS_MODULE;
	cdev_init(&timerdev.cdev, &timerdev_fops); //初始化一个cdev
	ret = cdev_add(&timerdev.cdev, timerdev.devid, TIMER_CNT); //添加一个cdev
	if(ret < 0 ){
		printk("timerdev cdev_add err!\r\n");
		goto fail_cdev;
	}
	/* 3、创建设备节点 */
	timerdev.class = class_create(THIS_MODULE, TIMER_NAME); //先创建一个类(创建的类在/sys/class中)
	if (IS_ERR(timerdev.class)) {
		printk("timerdev class_create err!\r\n"); 
		ret = PTR_ERR(timerdev.class);
		goto fail_class;
	}
	timerdev.device = device_create(timerdev.class,  //该设备依附的类
									 NULL,           //父设备
									 timerdev.devid, //设备号
									 NULL,           //私有数据
									 TIMER_NAME);    //设备名称
	if (IS_ERR(timerdev.device)) {
		printk("timerdev device_create err!\r\n"); 
		ret = PTR_ERR(timerdev.device);
		goto fail_device;
	}
	/* 4、初始化timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */
	init_timer(&timerdev.timer);
	timerdev.timer.function = timer_function;
	timerdev.timer.data = (unsigned long)&timerdev;

	return 0;
fail_device:
	class_destroy(timerdev.class); 
fail_class:
	cdev_del(&timerdev.cdev); 
fail_cdev:
	unregister_chrdev_region(timerdev.devid, TIMER_CNT);
fail_devid:
	return ret;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit timer_exit(void)
{
    /* 按照相反的顺序注销 */
	device_destroy(timerdev.class, timerdev.devid); /* 销毁类*/
	class_destroy(timerdev.class);                    /* 销毁设备节点 */
	cdev_del(&timerdev.cdev);                            /* 删除字符设备 */
	unregister_chrdev_region(timerdev.devid, TIMER_CNT);   /* 注销设备号 */
	/*******************************************************************/
	del_timer_sync(&timerdev.timer); /* 关闭定时器,定时器没激活返回0,*/
	if(timerdev.active_low) gpio_direction_output(timerdev.gpio_id, 1);  /* 设置为无效电平 */
	else gpio_direction_output(timerdev.gpio_id, 0);  /* 设置为无效电平 */
	gpio_free(timerdev.gpio_id); /* 释放gpio */ 
}

module_init(timer_init);
module_exit(timer_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

   <2>、应用程序:timerApp.c

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include "linux/ioctl.h"
/* 命令值 */
#define CLOSE_CMD 		(_IO(0XEF, 0x1))	/* 关闭定时器 */
#define OPEN_CMD		(_IO(0XEF, 0x2))	/* 打开定时器 */
#define SETPERIOD_CMD	(_IO(0XEF, 0x3))	/* 设置定时器周期命令 */

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, ret;
	char *filename;
	unsigned int cmd;
	unsigned int arg;
	unsigned char str[100];

	if (argc != 2) {
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];

	fd = open(filename, O_RDWR);
	if (fd < 0) {
		printf("Can't open file %s\r\n", filename);
		return -1;
	}

	while (1) {
		printf("Input CMD:");
		ret = scanf("%d", &cmd);
		if (ret != 1) {				/* 参数输入错误 */
			gets(str);				/* 防止卡死 */
		}

		if(cmd == 1)				/* 关闭LED灯 */
			cmd = CLOSE_CMD;
		else if(cmd == 2)			/* 打开LED灯 */
			cmd = OPEN_CMD;
		else if(cmd == 3) {
			cmd = SETPERIOD_CMD;	/* 设置周期值 */
			printf("Input Timer Period:");
			ret = scanf("%d", &arg);
			if (ret != 1) {			/* 参数输入错误 */
				gets(str);			/* 防止卡死 */
			}
		}
		ioctl(fd, cmd, arg);		/* 控制定时器的打开和关闭 */	
	}

	close(fd);
}

   <3>、编译命令:Makefile

KERNELDIR := /home/haut/itop_imx6ull/software/Linux_Source_nxpV4.1.15   # 内核路径
CURRENT_PATH := $(shell pwd)  # 当前路径

obj-m := timer.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

app:
	arm-linux-gnueabihf-gcc ./timerApp.c -o timerApp
cp:
	cp ./timer.ko ./timerApp /home/haut/itop_imx6ull/nfs_rootfs/lib/modules/4.1.15

   <4>、运行测试

# 一、加载驱动
  cd lib/modules/4.1.15 //进入modprobe的工作目录
  depmod                //第一次加载驱动的时候需要运行此命令
  modprobe timer.ko       //加载驱动

# 二、运行程序
  ./timerApp /dev/timer-led

5.11、Linux驱动之中断实验

        GIC 是 ARM 公司给 Cortex-A/R 内核提供的一个中断控制器,类似 Cortex-M 内核中的NVIC。目前 GIC 有 4 个版本:V1~V4, V1 是最老的版本,已经被废弃了。 V2~V4 目前正在大量的使用。 GIC V2 是给 ARMv7-A 架构使用的,比如 Cortex-A7、 Cortex-A9、 Cortex-A15 等,V3 和 V4 是给 ARMv8-A/R 架构使用的,也就是 64 位芯片使用的。I.MX6U 是 Cortex-A 内核的,因此我们主要讲解 GIC V2。 GIC V2 最多支持 8 个核。 ARM 会根据 GIC 版本的不同研发出不同的 IP 核,那些半导体厂商直接购买对应的 IP 核即可,比如 ARM 针对 GIC V2 就开发出了 GIC400 这个中断控制器 IP 核。当 GIC 接收到外部中断信号以后就会报给 ARM 内核,但是ARM 内核只提供了四个信号给 GIC 来汇报中断情况: VFIQ、 VIRQ、 FIQ 和 IRQ,他们之间的关系如图所示:
        在上图中, GIC 接收众多的外部中断,然后对其进行处理,最终就只通过四个信号报给 ARM 内核,这四个信号的含义如下:
        <1>、VFIQ:虚拟快速FIQ。<2>、VIRQ:虚拟外部IRQ。
        <3>、FIQ:快速中断IRQ。<4>、IRQ:外部中断IRQ。
VFIQ 和 VIRQ 是针对虚拟化的,不多讨论虚拟化,剩下的就是 FIQ 和 IRQ 了。那么 GIC 是如何完成这个工作的呢?GICV2 的逻辑图如图所示:
        图中左侧部分就是中断源,中间部分就是 GIC 控制器,最右侧就是中断控制器向处理器内核发送中断信息。我们重点要看的肯定是中间的 GIC 部分, GIC 将众多的中断源分为分为三类:
        ①、 SPI(Shared Peripheral Interrupt),共享中断(注意),顾名思义,所有 Core 共享的中断,这个是最常见的,那些外部中断都属于 SPI 中断(注意!不是 SPI 总线那个中断) 。比如按键中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core
        ②、 PPI(Private Peripheral Interrupt),私有中断,我们说了 GIC 是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。
        ③、 SGI(Software-generated Interrupt),软件中断,由软件触发引起的中断,通过向寄存器GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信。
        中断号的分配:ID0~ID15:这 16 个 ID 分配给 SGI。ID16~ID31:这 16 个 ID 分配给 PPI。ID32~ID1019:这 988 个 ID 分配给 SPI,像 GPIO 中断、串口中断等这些外部中断。至于具体到某个 ID 对应哪个中断那就由半导体厂商根据实际情况去定义了。比如 I.MX6U 的总共使用了 128 个中断 ID,加上前面属于 PPI 和 SGI 的 32 个 ID, I.MX6U 的中断源共有 128+32=160个,这 128 个中断 ID 对应的中断在《I.MX6ULL 参考手册》的“3.2 Cortex A7 interrupts”小节。
        在linux驱动中,常用irq_of_parse_and_map(dev->nd, dev->name)函数获取中断号,需要注意的是这个获取的中断号不是芯片参考手册中的中断号,它和硬件的中断号(中断源)是有映射关系的。通常把这个中断号叫做linux中断号,所以我们平时编程会见到两种中断号:硬件中断号、linux中断号。< linux中断号也叫软件中断号,它是唯一的 >< 可以用 cat /proc/interrupts 查看当前系统注册了哪些中断 >

# x1、单个中断的使能和禁止
    void enable_irq(unsigned int irq)  # 打开中断
    void disable_irq(unsigned int irq) # 关闭中断(先阻塞,等待中断处理函数执行结束再关闭)
    void disable_irq_nosync(unsigned int irq) # 关闭中断(不阻塞,不管中断处理函数是否结束,都立即关闭)
# x2、全局中断的使能和禁止
    local_irq_disable() # 关闭全局中断
    local_irq_enable()  # 打开全局中断
    # 一般都使用下面这两个:
    local_irq_save(flags)     # 将中断状态(开或关)保存在flags, 然后关闭全局中断
    local_irq_restore(flags)  # 将中断状态(开或关)恢复到flags状态
原因:local_irq_enable 用于使能当前处理器中断系统, local_irq_disable 用于禁止当前处理器中断统。
假如 A 任务调用 local_irq_disable 关闭全局中断 10S,当关闭了 2S 的时候 B 任务开始运行,B任务也调用 
local_irq_disable 关闭全局中断 3S, 3 秒以后 B 任务调用 local_irq_enable 函数将全局中断打开了。
此时才过去 2+3=5 秒的时间,然后全局中断就被打开了,此时 A 任务要关闭 10S 全局中断的愿望就破灭了,
然后 A 任务就“生气了”,结果很严重,可能系统都要被A 任务整崩溃。为了解决这个问题, B 任务不能直接
简单粗暴的通过 local_irq_enable 函数来打开全局中断,而是将中断状态恢复到以前的状态,要考虑到别的任
务的感受,
# x3、中断下半部
    tasklet是利用软中断来实现的另外一种下半部机制,在软中断和tasklet之间,建议大家使tasklet。
# x4、工作队列
    工作队列是另外一种下半部执行机制,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线
程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡
眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet
# xx:下半部的实现机制不止一种,还有其他的方式,比如中断线程化:百度搜索 request_threaded_irq蜗窝科技 具体查看。

   <1>、添加设备树节点


   <2>、驱动程序:imx6uirq.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <linux/errno.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/irq.h>
#include <linux/of_irq.h>
#include <linux/cdev.h>
#include <linux/timer.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/of_address.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <asm/mach/map.h>

#define DEV_CNT     0x01	   /* 设备号个数 */
#define DEV_NAME    "imx6uirq" /* 名字 */
#define KEY_NUM     0x01       /* 按键数 */
#define INVAKEY		0XFF	   /* 无效的按键值 */
#define KEY0_VALUE	0x01	   /* K0按键值 */

struct irq_keydesc{
	int gpio_id;        /* gpio编号 */
	int gpio_flag;      /* gpio标志 */
	int irqnum;	        /* 中断号 */
	unsigned char value;/* 按键值 */
	char name[10];      /* 注册时使用的label */
	irqreturn_t (*handler)(int, void*);/* 中断处理函数 */
};
/* imx6uirq设备结构体 */
struct imx6uirq_dev{
	dev_t devid;			 /* 设备号 	*/
	int major;				 /* 主设备号 */
	int minor;				 /* 次设备号 */
	struct cdev cdev;		 /* cdev   */
	struct class *class;	 /* 类 	    */
	struct device *device;	 /* 设备 	*/
	struct device_node *nd;  /* 设备节点 */
	struct irq_keydesc irqkey[KEY_NUM];	
	/* 按键值处理 */
	atomic_t keyvalue;    /* key0有效按键值 */
	atomic_t keyreleaseflag; /* key0释放标志 */
	struct timer_list keytimer; /* key0消抖定时器 */
};
struct imx6uirq_dev imx6uirq; /* irq设备 */

static irqreturn_t key0_handler(int irq, void *dev_id)
{
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;
	/* 启动消抖动定时器 */
	dev->keytimer.data = (unsigned long)dev_id; /* 传入定时器中断的参数 */
	mod_timer(&dev->keytimer, jiffies + msecs_to_jiffies(10)); /* 10ms */
	return IRQ_RETVAL(IRQ_HANDLED);
}
/* @description	: 定时器服务函数,用于按键消抖,定时器到了以后
 *				  再次读取按键值,如果按键还是处于按下状态就表示按键有效。
 * @param - arg	: 设备结构变量
 * @return 		: 无
 */
void key0_timer_handler(unsigned long arg)
{
	int value = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;
	/* 读取key0按键值 */
	value = gpio_get_value(dev->irqkey[0].gpio_id);
	if(value == 0){ /*按下*/
		atomic_set(&dev->keyvalue, dev->irqkey[0].value);
	}
	else{           /*释放*/
		atomic_set(&dev->keyvalue, (0x80)|(dev->irqkey[0].value)); /*最高位为释放标志*/
		atomic_set(&dev->keyreleaseflag, 1);
	}
}

static int keyio_init(struct imx6uirq_dev *dev){
	int ret = 0,i;
	enum of_gpio_flags flags;
	/*一、获取设备树中的属性数据 */
	/* 1.1、获取设备节点:key */
	dev->nd = of_find_node_by_path("/key");
	if(dev->nd == NULL){
		printk("pinctrl key-led node not find!\r\n");
		return -EINVAL;
	} else {
		printk("pinctrl key-led node find!\r\n");
	}
	/* 1.2、获取gpio的属性,gpio的编号和flags */
	if (of_gpio_named_count(dev->nd, "key-gpio") > 0) {
		for(i=0; i<KEY_NUM; i++){
			dev->irqkey[i].gpio_id = of_get_named_gpio_flags(dev->nd, "key-gpio", i, &flags);
			dev->irqkey[i].gpio_flag = (int)flags;
		}
	} else {
		printk("not have key-gpio");
		return -EINVAL;
	}
	printk("gpio:id=%d,flag=%d\r\n",dev->irqkey[0].gpio_id,dev->irqkey[0].gpio_flag);
	/* 1.3、申请io、设置为输入、获取中断号、设定中断入口函数 */
	for(i=0; i<KEY_NUM; i++){
		memset(dev->irqkey[i].name, i, sizeof(dev->irqkey[i].name));
		sprintf(dev->irqkey[i].name,"KEY%d",i);
		ret = gpio_request(dev->irqkey[i].gpio_id, dev->irqkey[i].name); /* 注册IO */
		if(ret < 0){
			printk("%s gpio request fail!\r\n",dev->irqkey[i].name);
			goto fail_gpio;
		}
		ret = gpio_direction_input(dev->irqkey[i].gpio_id); /* 设置方向 */
		if(ret < 0){
			printk("%s gpio set input fail!\r\n",dev->irqkey[i].name);
			goto fail_gpio;
		}
		dev->irqkey[i].irqnum = irq_of_parse_and_map(dev->nd, i); /* 获取中断号(注意返回的是linux中断号,不是硬件中断号) */
		/*dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio_id);*/
		if(!dev->irqkey[i].irqnum){
			printk("%s gpio irqnum get fail!\r\n",dev->irqkey[i].name);
			goto fail_gpio;
		}	
	}
	printk("irq:irqnum=%d\r\n",dev->irqkey[0].irqnum);
	/* 1.4、中断初始化(注册中断) */
	dev->irqkey[0].handler = key0_handler;
	dev->irqkey[0].value = KEY0_VALUE;
	for(i=0; i<KEY_NUM; i++){
		ret = request_irq(dev->irqkey[i].irqnum,  /*中断号*/
						  dev->irqkey[i].handler, /*中断函数*/
						  IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,/*触发方式*/
						  dev->irqkey[i].name,    /*注册label,就是取个名*/
						  dev                     /*中断函数的传入参数*/
						);
		if(ret){ /* 返回非零,则为失败 */
			ret = -EBUSY;
			printk("irq %d request fail!",dev->irqkey[i].irqnum);
			goto fail_irq;
		}
	}
	/* 1.5、初始化消抖定时器 */
	dev->keytimer.function = key0_timer_handler;/* 注意这里值设定了一个 */
	for(i=0; i<KEY_NUM; i++){
		init_timer(&dev->keytimer);
	}
	return 0;
fail_irq:
	for(i=0; i<KEY_NUM; i++) free_irq(dev->irqkey[i].irqnum, dev);
fail_gpio:
	for(i=0; i<KEY_NUM; i++) gpio_free(dev->irqkey[i].gpio_id);
	return ret;
}
/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int imx6uirq_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &imx6uirq; /* 设置私有数据 */	
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int ret = 0;
	unsigned char keyvalue = 0;
	unsigned char keyreleaseflag = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;

	keyvalue = atomic_read(&dev->keyvalue);
	keyreleaseflag = atomic_read(&dev->keyreleaseflag);
	if(keyreleaseflag){
		if(keyvalue & 0x80){ /*最高位表示按键值有效*/
			keyvalue &= ~0x80;
			ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
		}
		else{
			return -EINVAL;
		}
		atomic_set(&dev->keyreleaseflag, 0); /* 按下标志清零 */
	}else{
		return -EINVAL;
	}
	return ret;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t imx6uirq_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int imx6uirq_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 设备操作函数 */
static struct file_operations imx6uirq_fops = {
	.owner = THIS_MODULE,
	.open = imx6uirq_open,
	.read = imx6uirq_read,
	.write = imx6uirq_write,
	.release = imx6uirq_release,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
/* 内核中的也有一个名为key_init()的函数,这里加个x,是为了避免冲突 */
static int __init imx6uirq_init(void) /* 避免与内核中的 */
{
	int ret = 0;
	struct imx6uirq_dev *dev = &imx6uirq;
	/* 设备初始化 */
	ret = keyio_init(dev);
	if(ret < 0){
		printk("keyio_init fail!\r\n");
		goto fail_keyio_init;
	}
	/* 注册字符设备驱动 */
	/* 1、申请设备号 */
	dev->major = 0; /* 设置为0,即没有定义设备号 */
	if (dev->major) { /* 定义了设备号 */
		dev->devid = MKDEV(dev->major, 0);
		ret = register_chrdev_region(dev->devid,//起始设备号
							   		 DEV_CNT,   //设备数量
							   		 DEV_NAME); //设备名称
	} else { /* 没有定义设备号 */
		ret = alloc_chrdev_region(&dev->devid,  //储存设备号的变量
								  0,            //起始次设备号
								  DEV_CNT,      //设备数量
								  DEV_NAME);    //设备名称
		dev->major = MAJOR(dev->devid);     //获取分配号的主设备号
		dev->minor = MINOR(dev->devid);     //获取分配号的次设备号
	}
	if(ret < 0){
		printk("imx6uirq chrdev_region err!\r\n"); 
		goto fail_devid;
	}
	printk("imx6uirq major=%d,minor=%d\r\n", dev->major, dev->minor);	
	/* 2、注册字符设备 */
	dev->cdev.owner = THIS_MODULE;
	cdev_init(&dev->cdev, &imx6uirq_fops); //初始化一个cdev
	ret = cdev_add(&dev->cdev, dev->devid, DEV_CNT); //添加一个cdev
	if(ret < 0 ){
		printk("keydev cdev_add err!\r\n");
		goto fail_cdev;
	}
	/* 3、创建设备节点 */
	dev->class = class_create(THIS_MODULE, DEV_NAME); //先创建一个类(创建的类在/sys/class中)
	if (IS_ERR(dev->class)) {
		printk("keydev class_create err!\r\n"); 
		ret = PTR_ERR(dev->class);
		goto fail_class;
	}
	dev->device = device_create(dev->class,  //该设备依附的类
								NULL,        //父设备
								dev->devid,  //设备号
								NULL,        //私有数据
								DEV_NAME);   //设备名称
	if (IS_ERR(dev->device)) {
		printk("imx6uirq device_create err!\r\n"); 
		ret = PTR_ERR(dev->device);
		goto fail_device;
	}
	return 0;
fail_device:
	class_destroy(dev->class); 
fail_class:
	cdev_del(&dev->cdev); 
fail_cdev:
	unregister_chrdev_region(dev->devid, DEV_CNT);
fail_devid:
fail_keyio_init:
	return ret;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit imx6uirq_exit(void)
{
	int i;
	struct imx6uirq_dev *dev = &imx6uirq;
    /* 按照相反的顺序注销 */
	device_destroy(dev->class, dev->devid);        /*销毁类*/
	class_destroy(dev->class);                     /*销毁设备节点*/
	cdev_del(&dev->cdev);                          /*删除字符设备*/
	unregister_chrdev_region(dev->devid, DEV_CNT); /*注销设备号*/
	/* 注销irq、注销gpio */
	for(i=0; i<KEY_NUM; i++){
		free_irq(dev->irqkey[i].irqnum, dev);
		gpio_free(dev->irqkey[i].gpio_id);
	}
	/* 注销定时器 */
	del_timer_sync(&dev->keytimer);
}

module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

说明:这个驱动程序是不完全的,多按键的支持只写了一半。 
   <3>、应用程序:imx6uirqApp.c

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

/* 定义按键值 */
#define KEY0VALUE	0X01
#define INVAKEY		0X00
/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, ret;
	char *filename;
	int keyvalue = 0;
	if(argc != 2){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];
	
	/* 打开xxx驱动 */
	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}
	/* 循环读取按键值数据! */
	while(1) {
		usleep(1000); /* 睡眠1ms,避免程序cpu占用过高 */ 
		ret = read(fd, &keyvalue, sizeof(keyvalue));
		if(ret < 0){
			/* ...... */
		}else{
			if (keyvalue == KEY0VALUE) {	/* KEY0 */
				printf("KEY0 Press, value = %#X\r\n", keyvalue);	/* 按下 */
			}
		}
	}
	ret= close(fd); /* 关闭文件 */
	if(ret < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

   <4>、编译命令:Makefile 

KERNELDIR := /home/haut/itop_imx6ull/software/Linux_Source_nxpV4.1.15   # 内核路径
CURRENT_PATH := $(shell pwd)  # 当前路径

obj-m := imx6uirq.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

app:
	arm-linux-gnueabihf-gcc ./imx6uirqApp.c -o imx6uirqApp
cp:
	cp ./imx6uirq.ko ./imx6uirqApp /home/haut/itop_imx6ull/nfs_rootfs/lib/modules/4.1.15

   <5>、运行测试

# 一、加载驱动
  cd lib/modules/4.1.15 //进入modprobe的工作目录
  depmod                //第一次加载驱动的时候需要运行此命令
  modprobe imx6uirq.ko  //加载驱动

# 二、运行程序
  ./imx6uirqApp /dev/imx6uirq

5.12、Linux驱动之中断下半部

5.12.1、tasklet方式

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <linux/errno.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/irq.h>
#include <linux/of_irq.h>
#include <linux/cdev.h>
#include <linux/timer.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/of_address.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <asm/mach/map.h>

#define DEV_CNT     0x01	   /* 设备号个数 */
#define DEV_NAME    "imx6uirq" /* 名字 */
#define KEY_NUM     0x01       /* 按键数 */
#define INVAKEY		0XFF	   /* 无效的按键值 */
#define KEY0_VALUE	0x01	   /* K0按键值 */

struct irq_keydesc{
	int gpio_id;        /* gpio编号 */
	int gpio_flag;      /* gpio标志 */
	int irqnum;	        /* 中断号 */
	unsigned char value;/* 按键值 */
	char name[10];      /* 注册时使用的label */
	irqreturn_t (*handler)(int, void*);/* 中断处理函数 */
};
/* imx6uirq设备结构体 */
struct imx6uirq_dev{
	dev_t devid;			 /* 设备号 	*/
	int major;				 /* 主设备号 */
	int minor;				 /* 次设备号 */
	struct cdev cdev;		 /* cdev   */
	struct class *class;	 /* 类 	    */
	struct device *device;	 /* 设备 	*/
	struct device_node *nd;  /* 设备节点 */
	struct irq_keydesc irqkey[KEY_NUM];	
	/* 按键值处理 */
	atomic_t keyvalue;    /* key0有效按键值 */
	atomic_t keyreleaseflag; /* key0释放标志 */
	struct timer_list keytimer; /* key0消抖定时器 */
    struct tasklet_struct tasklet; /* tasklet:中断下半部 */
};
struct imx6uirq_dev imx6uirq; /* irq设备 */

/* key0的按键中断 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;
    tasklet_schedule(&dev->tasklet); 
	return IRQ_RETVAL(IRQ_HANDLED);
}
/* tasklet (中断下半部) */
static void key0_tasklet_func(unsigned long data)
{
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)data;
    printk("tasklet run!\r\n");
    /* 启动消抖动定时器 */
	mod_timer(&dev->keytimer, jiffies + msecs_to_jiffies(10)); /* 10ms */
}
/* 内核timer (消去抖动) */
void key0_timer_func(unsigned long arg)
{
	int value = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;
	/* 读取key0按键值 */
	value = gpio_get_value(dev->irqkey[0].gpio_id);
	if(value == 0){ /*按下*/
		atomic_set(&dev->keyvalue, dev->irqkey[0].value);
	}
	else{           /*释放*/
		atomic_set(&dev->keyvalue, (0x80)|(dev->irqkey[0].value)); /*最高位为释放标志*/
		atomic_set(&dev->keyreleaseflag, 1);
	}
}

static int keyio_init(struct imx6uirq_dev *dev){
	int ret = 0,i;
	enum of_gpio_flags flags;
	/*一、获取设备树中的属性数据 */
	/* 1.1、获取设备节点:key */
	dev->nd = of_find_node_by_path("/key");
	if(dev->nd == NULL){
		printk("pinctrl key-led node not find!\r\n");
		return -EINVAL;
	} else {
		printk("pinctrl key-led node find!\r\n");
	}
	/* 1.2、获取gpio的属性,gpio的编号和flags */
	if (of_gpio_named_count(dev->nd, "key-gpio") > 0) {
		for(i=0; i<KEY_NUM; i++){
			dev->irqkey[i].gpio_id = of_get_named_gpio_flags(dev->nd, "key-gpio", i, &flags);
			dev->irqkey[i].gpio_flag = (int)flags;
		}
	} else {
		printk("not have key-gpio");
		return -EINVAL;
	}
	printk("gpio:id=%d,flag=%d\r\n",dev->irqkey[0].gpio_id,dev->irqkey[0].gpio_flag);
	/* 1.3、申请io、设置为输入、获取中断号、设定中断入口函数 */
	for(i=0; i<KEY_NUM; i++){
		memset(dev->irqkey[i].name, i, sizeof(dev->irqkey[i].name));
		sprintf(dev->irqkey[i].name,"KEY%d",i);
		ret = gpio_request(dev->irqkey[i].gpio_id, dev->irqkey[i].name); /* 注册IO */
		if(ret < 0){
			printk("%s gpio request fail!\r\n",dev->irqkey[i].name);
			goto fail_gpio;
		}
		ret = gpio_direction_input(dev->irqkey[i].gpio_id); /* 设置方向 */
		if(ret < 0){
			printk("%s gpio set input fail!\r\n",dev->irqkey[i].name);
			goto fail_gpio;
		}
		dev->irqkey[i].irqnum = irq_of_parse_and_map(dev->nd, i); /* 获取中断号(注意返回的是linux中断号,不是硬件中断号) */
		/*dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio_id);*/
		if(!dev->irqkey[i].irqnum){
			printk("%s gpio irqnum get fail!\r\n",dev->irqkey[i].name);
			goto fail_gpio;
		}	
	}
	printk("irq:irqnum=%d\r\n",dev->irqkey[0].irqnum);
	/* 1.4、中断初始化(注册中断) */
    dev->irqkey[0].value = KEY0_VALUE;
	dev->irqkey[0].handler = key0_handler;
	for(i=0; i<KEY_NUM; i++){
		ret = request_irq(dev->irqkey[i].irqnum,  /*中断号*/
						  dev->irqkey[i].handler, /*中断函数*/
						  IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,/*触发方式*/
						  dev->irqkey[i].name,    /*注册label,就是取个名*/
						  dev                     /*中断函数的传入参数*/
						);
		if(ret){ /* 返回非零,则为失败 */
			ret = -EBUSY;
			printk("irq %d request fail!",dev->irqkey[i].irqnum);
			goto fail_irq;
		}
	}
    /* 1.5、初始化tasklet */
    tasklet_init(&dev->tasklet, key0_tasklet_func, (unsigned long)dev);

	/* 1.6、初始化消抖定时器 */
    dev->keytimer.data = (unsigned long)dev;
	dev->keytimer.function = key0_timer_func;
	init_timer(&dev->keytimer);

	return 0;
fail_irq:
	for(i=0; i<KEY_NUM; i++) free_irq(dev->irqkey[i].irqnum, dev);
fail_gpio:
	for(i=0; i<KEY_NUM; i++) gpio_free(dev->irqkey[i].gpio_id);
	return ret;
}
/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int imx6uirq_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &imx6uirq; /* 设置私有数据 */	
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int ret = 0;
	unsigned char keyvalue = 0;
	unsigned char keyreleaseflag = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;

	keyvalue = atomic_read(&dev->keyvalue);
	keyreleaseflag = atomic_read(&dev->keyreleaseflag);
	if(keyreleaseflag){
		if(keyvalue & 0x80){ /*最高位表示按键值有效*/
			keyvalue &= ~0x80;
			ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
		}
		else{
			return -EINVAL;
		}
		atomic_set(&dev->keyreleaseflag, 0); /* 按下标志清零 */
	}else{
		return -EINVAL;
	}
	return ret;
}
/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t imx6uirq_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int imx6uirq_release(struct inode *inode, struct file *filp)
{
	return 0;
}
/* 设备操作函数 */
static struct file_operations imx6uirq_fops = {
	.owner = THIS_MODULE,
	.open = imx6uirq_open,
	.read = imx6uirq_read,
	.write = imx6uirq_write,
	.release = imx6uirq_release,
};
/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
/* 内核中的也有一个名为key_init()的函数,这里加个x,是为了避免冲突 */
static int __init imx6uirq_init(void) /* 避免与内核中的 */
{
	int ret = 0;
	struct imx6uirq_dev *dev = &imx6uirq;
	/* 设备初始化 */
	ret = keyio_init(dev);
	if(ret < 0){
		printk("keyio_init fail!\r\n");
		goto fail_keyio_init;
	}
	/* 注册字符设备驱动 */
	/* 1、申请设备号 */
	dev->major = 0; /* 设置为0,即没有定义设备号 */
	if (dev->major) { /* 定义了设备号 */
		dev->devid = MKDEV(dev->major, 0);
		ret = register_chrdev_region(dev->devid,//起始设备号
							   		 DEV_CNT,   //设备数量
							   		 DEV_NAME); //设备名称
	} else { /* 没有定义设备号 */
		ret = alloc_chrdev_region(&dev->devid,  //储存设备号的变量
								  0,            //起始次设备号
								  DEV_CNT,      //设备数量
								  DEV_NAME);    //设备名称
		dev->major = MAJOR(dev->devid);     //获取分配号的主设备号
		dev->minor = MINOR(dev->devid);     //获取分配号的次设备号
	}
	if(ret < 0){
		printk("imx6uirq chrdev_region err!\r\n"); 
		goto fail_devid;
	}
	printk("imx6uirq major=%d,minor=%d\r\n", dev->major, dev->minor);	
	/* 2、注册字符设备 */
	dev->cdev.owner = THIS_MODULE;
	cdev_init(&dev->cdev, &imx6uirq_fops); //初始化一个cdev
	ret = cdev_add(&dev->cdev, dev->devid, DEV_CNT); //添加一个cdev
	if(ret < 0 ){
		printk("keydev cdev_add err!\r\n");
		goto fail_cdev;
	}
	/* 3、创建设备节点 */
	dev->class = class_create(THIS_MODULE, DEV_NAME); //先创建一个类(创建的类在/sys/class中)
	if (IS_ERR(dev->class)) {
		printk("keydev class_create err!\r\n"); 
		ret = PTR_ERR(dev->class);
		goto fail_class;
	}
	dev->device = device_create(dev->class,  //该设备依附的类
								NULL,        //父设备
								dev->devid,  //设备号
								NULL,        //私有数据
								DEV_NAME);   //设备名称
	if (IS_ERR(dev->device)) {
		printk("imx6uirq device_create err!\r\n"); 
		ret = PTR_ERR(dev->device);
		goto fail_device;
	}
	return 0;
fail_device:
	class_destroy(dev->class); 
fail_class:
	cdev_del(&dev->cdev); 
fail_cdev:
	unregister_chrdev_region(dev->devid, DEV_CNT);
fail_devid:
fail_keyio_init:
	return ret;
}
/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit imx6uirq_exit(void)
{
	int i;
	struct imx6uirq_dev *dev = &imx6uirq;
    /* 按照相反的顺序注销 */
	device_destroy(dev->class, dev->devid);        /*销毁类*/
	class_destroy(dev->class);                     /*销毁设备节点*/
	cdev_del(&dev->cdev);                          /*删除字符设备*/
	unregister_chrdev_region(dev->devid, DEV_CNT); /*注销设备号*/
	/* 注销irq、注销gpio */
	for(i=0; i<KEY_NUM; i++){
		free_irq(dev->irqkey[i].irqnum, dev);
		gpio_free(dev->irqkey[i].gpio_id);
	}
	/* 注销定时器 */
	del_timer_sync(&dev->keytimer);
}
module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

​​​​​5.12.2、work方式

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <linux/errno.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/irq.h>
#include <linux/of_irq.h>
#include <linux/cdev.h>
#include <linux/timer.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/of_address.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <asm/mach/map.h>

#define DEV_CNT     0x01	   /* 设备号个数 */
#define DEV_NAME    "imx6uirq" /* 名字 */
#define KEY_NUM     0x01       /* 按键数 */
#define INVAKEY		0XFF	   /* 无效的按键值 */
#define KEY0_VALUE	0x01	   /* K0按键值 */

struct irq_keydesc{
	int gpio_id;        /* gpio编号 */
	int gpio_flag;      /* gpio标志 */
	int irqnum;	        /* 中断号 */
	unsigned char value;/* 按键值 */
	char name[10];      /* 注册时使用的label */
	irqreturn_t (*handler)(int, void*);/* 中断处理函数 */
};
/* imx6uirq设备结构体 */
struct imx6uirq_dev{
	dev_t devid;			 /* 设备号 	*/
	int major;				 /* 主设备号 */
	int minor;				 /* 次设备号 */
	struct cdev cdev;		 /* cdev   */
	struct class *class;	 /* 类 	    */
	struct device *device;	 /* 设备 	*/
	struct device_node *nd;  /* 设备节点 */
	struct irq_keydesc irqkey[KEY_NUM];	
	/* 按键值处理 */
	atomic_t keyvalue;    /* key0有效按键值 */
	atomic_t keyreleaseflag; /* key0释放标志 */
	struct timer_list keytimer; /* key0消抖定时器 */
    struct work_struct worker; /* worker:中断下半部 */
};
struct imx6uirq_dev imx6uirq; /* irq设备 */


/* key0的按键中断 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;
    schedule_work(&dev->worker); 
	return IRQ_RETVAL(IRQ_HANDLED);
}
/* work(中断下半部) */
static void key0_work_func(struct work_struct *work)
{
    struct imx6uirq_dev *dev = container_of(work, struct imx6uirq_dev, worker);
    printk("work run!\r\n");
    /* 启动消抖动定时器 */
	mod_timer(&dev->keytimer, jiffies + msecs_to_jiffies(10)); /* 10ms */
}
/* 内核timer(消去抖动) */
void key0_timer_func(unsigned long arg)
{
	int value = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;
	/* 读取key0按键值 */
	value = gpio_get_value(dev->irqkey[0].gpio_id);
	if(value == 0){ /*按下*/
		atomic_set(&dev->keyvalue, dev->irqkey[0].value);
	}
	else{           /*释放*/
		atomic_set(&dev->keyvalue, (0x80)|(dev->irqkey[0].value)); /*最高位为释放标志*/
		atomic_set(&dev->keyreleaseflag, 1);
	}
}


static int keyio_init(struct imx6uirq_dev *dev){
	int ret = 0,i;
	enum of_gpio_flags flags;
	/*一、获取设备树中的属性数据 */
	/* 1.1、获取设备节点:key */
	dev->nd = of_find_node_by_path("/key");
	if(dev->nd == NULL){
		printk("pinctrl key-led node not find!\r\n");
		return -EINVAL;
	} else {
		printk("pinctrl key-led node find!\r\n");
	}
	/* 1.2、获取gpio的属性,gpio的编号和flags */
	if (of_gpio_named_count(dev->nd, "key-gpio") > 0) {
		for(i=0; i<KEY_NUM; i++){
			dev->irqkey[i].gpio_id = of_get_named_gpio_flags(dev->nd, "key-gpio", i, &flags);
			dev->irqkey[i].gpio_flag = (int)flags;
		}
	} else {
		printk("not have key-gpio");
		return -EINVAL;
	}
	printk("gpio:id=%d,flag=%d\r\n",dev->irqkey[0].gpio_id,dev->irqkey[0].gpio_flag);
	/* 1.3、申请io、设置为输入、获取中断号、设定中断入口函数 */
	for(i=0; i<KEY_NUM; i++){
		memset(dev->irqkey[i].name, i, sizeof(dev->irqkey[i].name));
		sprintf(dev->irqkey[i].name,"KEY%d",i);
		ret = gpio_request(dev->irqkey[i].gpio_id, dev->irqkey[i].name); /* 注册IO */
		if(ret < 0){
			printk("%s gpio request fail!\r\n",dev->irqkey[i].name);
			goto fail_gpio;
		}
		ret = gpio_direction_input(dev->irqkey[i].gpio_id); /* 设置方向 */
		if(ret < 0){
			printk("%s gpio set input fail!\r\n",dev->irqkey[i].name);
			goto fail_gpio;
		}
		dev->irqkey[i].irqnum = irq_of_parse_and_map(dev->nd, i); /* 获取中断号(注意返回的是linux中断号,不是硬件中断号) */
		/*dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio_id);*/
		if(!dev->irqkey[i].irqnum){
			printk("%s gpio irqnum get fail!\r\n",dev->irqkey[i].name);
			goto fail_gpio;
		}	
	}
	printk("irq:irqnum=%d\r\n",dev->irqkey[0].irqnum);
	/* 1.4、中断初始化(注册中断) */
    dev->irqkey[0].value = KEY0_VALUE;
	dev->irqkey[0].handler = key0_handler;
	for(i=0; i<KEY_NUM; i++){
		ret = request_irq(dev->irqkey[i].irqnum,  /*中断号*/
						  dev->irqkey[i].handler, /*中断函数*/
						  IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,/*触发方式*/
						  dev->irqkey[i].name,    /*注册label,就是取个名*/
						  dev                     /*中断函数的传入参数*/
						);
		if(ret){ /* 返回非零,则为失败 */
			ret = -EBUSY;
			printk("irq %d request fail!",dev->irqkey[i].irqnum);
			goto fail_irq;
		}
	}
    /* 1.5、初始化work */
    INIT_WORK(&dev->worker,key0_work_func);

	/* 1.6、初始化消抖定时器 */
    dev->keytimer.data = (unsigned long)dev;
	dev->keytimer.function = key0_timer_func;
	init_timer(&dev->keytimer);

	return 0;
fail_irq:
	for(i=0; i<KEY_NUM; i++) free_irq(dev->irqkey[i].irqnum, dev);
fail_gpio:
	for(i=0; i<KEY_NUM; i++) gpio_free(dev->irqkey[i].gpio_id);
	return ret;
}
/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int imx6uirq_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &imx6uirq; /* 设置私有数据 */	
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int ret = 0;
	unsigned char keyvalue = 0;
	unsigned char keyreleaseflag = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;

	keyvalue = atomic_read(&dev->keyvalue);
	keyreleaseflag = atomic_read(&dev->keyreleaseflag);
	if(keyreleaseflag){
		if(keyvalue & 0x80){ /*最高位表示按键值有效*/
			keyvalue &= ~0x80;
			ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
		}
		else{
			return -EINVAL;
		}
		atomic_set(&dev->keyreleaseflag, 0); /* 按下标志清零 */
	}else{
		return -EINVAL;
	}
	return ret;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t imx6uirq_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int imx6uirq_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 设备操作函数 */
static struct file_operations imx6uirq_fops = {
	.owner = THIS_MODULE,
	.open = imx6uirq_open,
	.read = imx6uirq_read,
	.write = imx6uirq_write,
	.release = imx6uirq_release,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
/* 内核中的也有一个名为key_init()的函数,这里加个x,是为了避免冲突 */
static int __init imx6uirq_init(void) /* 避免与内核中的 */
{
	int ret = 0;
	struct imx6uirq_dev *dev = &imx6uirq;
	/* 设备初始化 */
	ret = keyio_init(dev);
	if(ret < 0){
		printk("keyio_init fail!\r\n");
		goto fail_keyio_init;
	}
	/* 注册字符设备驱动 */
	/* 1、申请设备号 */
	dev->major = 0; /* 设置为0,即没有定义设备号 */
	if (dev->major) { /* 定义了设备号 */
		dev->devid = MKDEV(dev->major, 0);
		ret = register_chrdev_region(dev->devid,//起始设备号
							   		 DEV_CNT,   //设备数量
							   		 DEV_NAME); //设备名称
	} else { /* 没有定义设备号 */
		ret = alloc_chrdev_region(&dev->devid,  //储存设备号的变量
								  0,            //起始次设备号
								  DEV_CNT,      //设备数量
								  DEV_NAME);    //设备名称
		dev->major = MAJOR(dev->devid);     //获取分配号的主设备号
		dev->minor = MINOR(dev->devid);     //获取分配号的次设备号
	}
	if(ret < 0){
		printk("imx6uirq chrdev_region err!\r\n"); 
		goto fail_devid;
	}
	printk("imx6uirq major=%d,minor=%d\r\n", dev->major, dev->minor);	
	/* 2、注册字符设备 */
	dev->cdev.owner = THIS_MODULE;
	cdev_init(&dev->cdev, &imx6uirq_fops); //初始化一个cdev
	ret = cdev_add(&dev->cdev, dev->devid, DEV_CNT); //添加一个cdev
	if(ret < 0 ){
		printk("keydev cdev_add err!\r\n");
		goto fail_cdev;
	}
	/* 3、创建设备节点 */
	dev->class = class_create(THIS_MODULE, DEV_NAME); //先创建一个类(创建的类在/sys/class中)
	if (IS_ERR(dev->class)) {
		printk("keydev class_create err!\r\n"); 
		ret = PTR_ERR(dev->class);
		goto fail_class;
	}
	dev->device = device_create(dev->class,  //该设备依附的类
								NULL,        //父设备
								dev->devid,  //设备号
								NULL,        //私有数据
								DEV_NAME);   //设备名称
	if (IS_ERR(dev->device)) {
		printk("imx6uirq device_create err!\r\n"); 
		ret = PTR_ERR(dev->device);
		goto fail_device;
	}
	return 0;
fail_device:
	class_destroy(dev->class); 
fail_class:
	cdev_del(&dev->cdev); 
fail_cdev:
	unregister_chrdev_region(dev->devid, DEV_CNT);
fail_devid:
fail_keyio_init:
	return ret;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit imx6uirq_exit(void)
{
	int i;
	struct imx6uirq_dev *dev = &imx6uirq;
    /* 按照相反的顺序注销 */
	device_destroy(dev->class, dev->devid);        /*销毁类*/
	class_destroy(dev->class);                     /*销毁设备节点*/
	cdev_del(&dev->cdev);                          /*删除字符设备*/
	unregister_chrdev_region(dev->devid, DEV_CNT); /*注销设备号*/
	/* 注销irq、注销gpio */
	for(i=0; i<KEY_NUM; i++){
		free_irq(dev->irqkey[i].irqnum, dev);
		gpio_free(dev->irqkey[i].gpio_id);
	}
	/* 注销定时器 */
	del_timer_sync(&dev->keytimer);
}

module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

5.13、Linux驱动之阻塞与非阻塞IO

        阻塞和非阻塞 IO 是 Linux 驱动开发里面很常见的两种设备访问模式,在编写驱动的时候一定要考虑到阻塞和非阻塞。这里的“IO” 不是 “GPIO”(也就是引脚)。这里的 IO 指的Input/Output,也就是输入/输出,是应用程序对驱动设备的输入/输出操作。
        当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。如下图所示:

# 示例1、应用程序阻塞读取数据
int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR);   /* 阻塞方式打开 */
ret = read(fd, &data, sizeof(data)); /* 读取数据 */

# 示例2、应用程序非阻塞读取数据
int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */
ret = read(fd, &data, sizeof(data)); /* 读取数据 */

# 说明:无参情况下,设备驱动文件的默认读取方式就是阻塞式的。加上参数O_NONBLOCK后,就是以非阻塞方式读取。(注意阻不阻塞体现在read函数上)

等待队列:用户程序使用阻塞访问方式时,在设备文件当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU 资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。Linux内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作。
轮询:用户程序使用非阻塞访问方式时,设备驱动程序就要提供非阻塞的处理方式,也就是轮询。poll、 epoll 和 select 可以用于处理轮询,应用程序通过 select、 epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用 select、 epoll 或 poll 函数的时候设备驱动程序中的 poll 函数就会执行,因此需要在设备驱动程序中编写 poll 函数。

5.13.1、阻塞式IO

        等待队列是用链表实现的,链表的组成包括表头和表项,所以等待队列包括等待队列头和等待队列项,等待队列项对应的是某个进程。所以使用方法是:创建等待队列头=>初始化等待队列头=>创建等待队列项=>把等待队列项加入到等待队列中。最后在适当的条件下通过唤醒函数唤醒队列中的项就可以了。等待队列头定义后初始化就行了,等待队列项可以自己创建(创建好后还需要自己手动添加项到表中),也可以使用等待事件函数(创建项并添加到表中)。

   <1>、使用5.11章的设备树
   <2>、驱动程序:blockio.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <linux/errno.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/irq.h>
#include <linux/of_irq.h>
#include <linux/cdev.h>
#include <linux/timer.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/of_address.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <asm/mach/map.h>

#define DEV_CNT     0x01	   /* 设备号个数 */
#define DEV_NAME    "imx6uirq" /* 名字 */
#define KEY_NUM     0x01       /* 按键数 */
#define INVAKEY		0XFF	   /* 无效的按键值 */
#define KEY0_VALUE	0x01	   /* K0按键值 */

/* imx6uirq设备结构体 */
struct imx6uirq_dev{
	/* 设备与节点 */
	dev_t devid;			 /* 设备号 	*/
	int major;				 /* 主设备号 */
	int minor;				 /* 次设备号 */
	struct cdev cdev;		 /* cdev   */
	struct class *class;	 /* 类 	    */
	struct device *device;	 /* 设备 	*/
	struct device_node *nd;  /* 设备节点 */
	/* key0按键相关 */
	int gpio_id;                /* gpio编号 */
	int gpio_flag;              /* gpio标志 */
	int irqnum;	                /* 中断号 */
	unsigned char value;        /* 按键值 */
	char name[10];              /* 注册时使用的label */
	irqreturn_t (*handler)(int, void*);/* 中断处理函数 */
	atomic_t keyvalue;          /* key0有效按键值 */
	atomic_t keyreleaseflag;    /* key0释放标志 */
	struct timer_list keytimer; /* key0消抖定时器 */
	/* 阻塞IO相关 */
	wait_queue_head_t r_wait;	/* 定义一个等待队列头 */
}imx6uirq;

static irqreturn_t key0_handler(int irq, void *dev_id)
{
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;
	/* 启动消抖动定时器 */
	dev->keytimer.data = (unsigned long)dev_id; /* 传入定时器中断的参数 */
	mod_timer(&dev->keytimer, jiffies + msecs_to_jiffies(10)); /* 10ms */
	return IRQ_RETVAL(IRQ_HANDLED);
}
/* @description	: 定时器服务函数,用于按键消抖,定时器到了以后
 *				  再次读取按键值,如果按键还是处于按下状态就表示按键有效。
 * @param - arg	: 设备结构变量
 * @return 		: 无
 */
void key0_timer_handler(unsigned long arg)
{
	int value = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;
	/* 读取key0按键值 */
	value = gpio_get_value(dev->gpio_id);
	if(value == 0){ /*按下*/
		atomic_set(&dev->keyvalue, dev->value);
	}
	else{           /*释放*/
		atomic_set(&dev->keyvalue, (0x80)|(dev->value)); /*最高位为释放标志*/
		atomic_set(&dev->keyreleaseflag, 1);
	}
	/* 唤醒进程 */
	if(atomic_read(&dev->keyreleaseflag)){
		//wake_up(&dev->r_wait); /* 将等待队列中的所有的睡眠进程唤醒 */
		wake_up_interruptible(&dev->r_wait);/* 将等待队列中所有处于 TASK_INTERRUPTIBLE 状态的进程 */
	}
	/* 注意:唤醒是指的是唤醒睡眠状态的进程,有两种状态是睡眠状态:
		1、TASK_INTERRUPTIBLE    可中断的
		2、TASK_UNINTERRUPTIBLE  不可中断的
	*/
}

static int keyio_init(struct imx6uirq_dev *dev){
	int ret = 0;
	enum of_gpio_flags flags;
	/*一、获取设备树中的属性数据 */
	/* 1.1、获取设备节点:key */
	dev->nd = of_find_node_by_path("/key");
	if(dev->nd == NULL){
		printk("pinctrl key-led node not find!\r\n");
		return -EINVAL;
	} else {
		printk("pinctrl key-led node find!\r\n");
	}
	/* 1.2、获取gpio的属性,gpio的编号和flags */
	if (of_gpio_named_count(dev->nd, "key-gpio") > 0) {
		dev->gpio_id = of_get_named_gpio_flags(dev->nd, "key-gpio", 0, &flags); /* 第三个参数为key-gpio的元素索引 */
		dev->gpio_flag = (int)flags;
	} else {
		printk("not have key-gpio");
		return -EINVAL;
	}
	printk("gpio:id=%d,flag=%d\r\n",dev->gpio_id,dev->gpio_flag);
	/* 1.3、申请io、设置为输入、获取中断号、设定中断入口函数 */
	memset(dev->name, 0, sizeof(dev->name));
	sprintf(dev->name,"KEY%d",0);
	ret = gpio_request(dev->gpio_id, dev->name); /* 注册IO */
	if(ret < 0){
		printk("%s gpio request fail!\r\n",dev->name);
		goto fail_gpio;
	}
	ret = gpio_direction_input(dev->gpio_id); /* 设置方向 */
	if(ret < 0){
		printk("%s gpio set input fail!\r\n",dev->name);
		goto fail_gpio;
	}
	dev->irqnum = irq_of_parse_and_map(dev->nd, 0); /* 的第二个参数是index,是设备树interrupts元素索引 */
	/*dev->irqnum = gpio_to_irq(dev->gpio_id);*/ /* 获取中断号(注意返回的是linux中断号,不是硬件中断号) */
	if(!dev->irqnum){
		printk("%s gpio irqnum get fail!\r\n",dev->name);
		goto fail_gpio;
	}	
	printk("key0-irq:irqnum=%d\r\n",dev->irqnum);
	/* 1.4、中断初始化(注册中断) */
	dev->handler = key0_handler;
	dev->value = KEY0_VALUE;
	ret = request_irq(dev->irqnum,  /*中断号*/
					  dev->handler, /*中断函数*/
					  IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,/*触发方式*/
					  dev->name,    /*注册label,就是取个名*/
					  dev);         /*中断函数的传入参数*/				 
	if(ret){ /* 返回非零,则为失败 */
		ret = -EBUSY;
		printk("irq %d request fail!",dev->irqnum);
		goto fail_irq;
	}	
	/* 1.5、初始化消抖定时器、原子变量 */
	dev->keytimer.function = key0_timer_handler;/* 注意这里值设定了一个 */
	init_timer(&dev->keytimer);
	atomic_set(&dev->keyvalue, INVAKEY);
	atomic_set(&dev->keyreleaseflag, 0);
	/* 1.6、初始化等待队列头 */
	init_waitqueue_head(&dev->r_wait);
	return 0;
fail_irq:
	free_irq(dev->irqnum, dev);
fail_gpio:
	gpio_free(dev->gpio_id);
	return ret;
}
/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int imx6uirq_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &imx6uirq; /* 设置私有数据 */	
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int ret = 0;
	unsigned char keyvalue = 0;
	unsigned char keyreleaseflag = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
#if 0
	wait_event_interruptible(dev->r_wait, atomic_read(&dev->keyreleaseflag));
#else
	DECLARE_WAITQUEUE(wait, current); /* 创建一个等待队列项,对象名为wait */
	while(atomic_read(&dev->keyreleaseflag)== 0){ /* 判断条件 */
		add_wait_queue(&dev->r_wait,&wait);       /* 将项添加到等待队列中 */
		__set_current_state(TASK_INTERRUPTIBLE);  /* 设置当前进程状态 */
		schedule();                               /* 调度一次:此句后进程睡眠-停在这*/
		/* 至此:被唤醒 */
		__set_current_state(TASK_RUNNING);        /* 将当前进程设置为TASK_RUNNING状态 */
		remove_wait_queue(&dev->r_wait, &wait);   /* 将对应的队列项从等待队列中移除 */
		if(signal_pending(current)){              /* 有信号需要处理 */
			return -ERESTARTSYS;
		} 
	}
#endif
	/* 读取按键值 */
	keyvalue = atomic_read(&dev->keyvalue);
	keyreleaseflag = atomic_read(&dev->keyreleaseflag);
	if(keyreleaseflag){
		if(keyvalue & 0x80){ /*最高位表示按键值有效*/
			keyvalue &= ~0x80;
			ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
		}
		else{
			return -EINVAL; /* 数据错误 */
		}
		atomic_set(&dev->keyreleaseflag, 0); /* 按下标志清零 */
	}else{
		return -EINVAL; /* 数据错误 */
	}
	return ret;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t imx6uirq_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int imx6uirq_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 设备操作函数 */
static struct file_operations imx6uirq_fops = {
	.owner = THIS_MODULE,
	.open = imx6uirq_open,
	.read = imx6uirq_read,
	.write = imx6uirq_write,
	.release = imx6uirq_release,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
/* 内核中的也有一个名为key_init()的函数,这里加个x,是为了避免冲突 */
static int __init imx6uirq_init(void) /* 避免与内核中的 */
{
	int ret = 0;
	struct imx6uirq_dev *dev = &imx6uirq;
	/* 设备初始化 */
	ret = keyio_init(dev);
	if(ret < 0){
		printk("keyio_init fail!\r\n");
		goto fail_keyio_init;
	}
	/* 注册字符设备驱动 */
	/* 1、申请设备号 */
	dev->major = 0; /* 设置为0,即没有定义设备号 */
	if (dev->major) { /* 定义了设备号 */
		dev->devid = MKDEV(dev->major, 0);
		ret = register_chrdev_region(dev->devid,//起始设备号
							   		 DEV_CNT,   //设备数量
							   		 DEV_NAME); //设备名称
	} else { /* 没有定义设备号 */
		ret = alloc_chrdev_region(&dev->devid,  //储存设备号的变量
								  0,            //起始次设备号
								  DEV_CNT,      //设备数量
								  DEV_NAME);    //设备名称
		dev->major = MAJOR(dev->devid);     //获取分配号的主设备号
		dev->minor = MINOR(dev->devid);     //获取分配号的次设备号
	}
	if(ret < 0){
		printk("imx6uirq chrdev_region err!\r\n"); 
		goto fail_devid;
	}
	printk("imx6uirq major=%d,minor=%d\r\n", dev->major, dev->minor);	
	/* 2、注册字符设备 */
	dev->cdev.owner = THIS_MODULE;
	cdev_init(&dev->cdev, &imx6uirq_fops); //初始化一个cdev
	ret = cdev_add(&dev->cdev, dev->devid, DEV_CNT); //添加一个cdev
	if(ret < 0 ){
		printk("keydev cdev_add err!\r\n");
		goto fail_cdev;
	}
	/* 3、创建设备节点 */
	dev->class = class_create(THIS_MODULE, DEV_NAME); //先创建一个类(创建的类在/sys/class中)
	if (IS_ERR(dev->class)) {
		printk("keydev class_create err!\r\n"); 
		ret = PTR_ERR(dev->class);
		goto fail_class;
	}
	dev->device = device_create(dev->class,  //该设备依附的类
								NULL,        //父设备
								dev->devid,  //设备号
								NULL,        //私有数据
								DEV_NAME);   //设备名称
	if (IS_ERR(dev->device)) {
		printk("imx6uirq device_create err!\r\n"); 
		ret = PTR_ERR(dev->device);
		goto fail_device;
	}
	return 0;
fail_device:
	class_destroy(dev->class); 
fail_class:
	cdev_del(&dev->cdev); 
fail_cdev:
	unregister_chrdev_region(dev->devid, DEV_CNT);
fail_devid:
fail_keyio_init:
	return ret;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit imx6uirq_exit(void)
{
	struct imx6uirq_dev *dev = &imx6uirq;
    /* 按照相反的顺序注销 */
	device_destroy(dev->class, dev->devid);        /*销毁类*/
	class_destroy(dev->class);                     /*销毁设备节点*/
	cdev_del(&dev->cdev);                          /*删除字符设备*/
	unregister_chrdev_region(dev->devid, DEV_CNT); /*注销设备号*/
	/* 注销irq、注销gpio */
	free_irq(dev->irqnum, dev);
	gpio_free(dev->gpio_id);
	/* 注销定时器 */
	del_timer_sync(&dev->keytimer);
}

module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

   <3>、应用程序:blockioApp.c

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

/* 定义按键值 */
#define KEY0VALUE	0X01
#define INVAKEY		0X00
/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, ret;
	char *filename;
	int keyvalue = 0;
	if(argc != 2){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];
	
	/* 打开xxx驱动 */
	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}
	/* 循环读取按键值数据! */
	while(1) {
		ret = read(fd, &keyvalue, sizeof(keyvalue)); /* 如果数据没好,阻塞 */
		if(ret < 0){
			/* ...... */
		}else{
			if (keyvalue == KEY0VALUE) {	/* KEY0 */
				printf("KEY0 Press, value = %#X\r\n", keyvalue);	/* 按下 */
			}
		}
	}
	ret= close(fd); /* 关闭文件 */
	if(ret < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

   <4>、编译命令:Makefile

KERNELDIR := /home/haut/itop_imx6ull/software/Linux_Source_nxpV4.1.15   # 内核路径
CURRENT_PATH := $(shell pwd)  # 当前路径

obj-m := blockio.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

app:
	arm-linux-gnueabihf-gcc ./blockioApp.c -o blockioApp
cp:
	cp ./blockio.ko ./blockioApp /home/haut/itop_imx6ull/nfs_rootfs/lib/modules/4.1.15

   <5>、运行测试

# 一、加载驱动
  cd lib/modules/4.1.15 //进入modprobe的工作目录
  depmod                //第一次加载驱动的时候需要运行此命令
  modprobe blockio.ko   //加载驱动

# 二、运行程序
  ./blockioApp /dev/imx6uirq &  //后台运行程序
  top  //查看CPU占用率(cpu占用率很低),前几章的读取,没使用阻塞IO,程序一直在空转,CPU使用率很高

5.13.2、非阻塞式IO

        用户程序调用poll()和select()函数,最终do_poll()函数执行,do_poll()里是一个循环,整个poll的机制的核心就是这个循环,循环的伪代码如下(省略信号):
for(;;){
        if(驱动poll()) break;
        if(timeout) break;
        current = TASK_INTERRUPTIBL # 进入超时睡眠(超时则自动唤醒或被在别处被wake_up)
}

注意:驱动poll里的poll_wait会做两件事:1、把等待队列头加入到poll table中,2、把当前线程加入到等待队列中(加入到等待队列,但并没有设置当前线程状态)
   <1>、使用5.11章的设备树
   <2>、驱动程序:noblockio.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <linux/errno.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/irq.h>
#include <linux/of_irq.h>
#include <linux/poll.h>
#include <linux/cdev.h>
#include <linux/timer.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/of_address.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <asm/mach/map.h>

#define DEV_CNT     0x01	   /* 设备号个数 */
#define DEV_NAME    "imx6uirq" /* 名字 */
#define KEY_NUM     0x01       /* 按键数 */
#define INVAKEY		0XFF	   /* 无效的按键值 */
#define KEY0_VALUE	0x01	   /* K0按键值 */

/* imx6uirq设备结构体 */
struct imx6uirq_dev{
	/* 设备与节点 */
	dev_t devid;			 /* 设备号 	*/
	int major;				 /* 主设备号 */
	int minor;				 /* 次设备号 */
	struct cdev cdev;		 /* cdev   */
	struct class *class;	 /* 类 	    */
	struct device *device;	 /* 设备 	*/
	struct device_node *nd;  /* 设备节点 */
	/* key0按键相关 */
	int gpio_id;                /* gpio编号 */
	int gpio_flag;              /* gpio标志 */
	int irqnum;	                /* 中断号 */
	unsigned char value;        /* 按键值 */
	char name[10];              /* 注册时使用的label */
	irqreturn_t (*handler)(int, void*);/* 中断处理函数 */
	atomic_t keyvalue;          /* key0有效按键值 */
	atomic_t keyreleaseflag;    /* key0释放标志 */
	struct timer_list keytimer; /* key0消抖定时器 */
	/* 阻塞IO相关 */
	wait_queue_head_t r_wait;	/* 定义一个等待队列头 */
}imx6uirq;

static irqreturn_t key0_handler(int irq, void *dev_id)
{
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;
	/* 启动消抖动定时器 */
	dev->keytimer.data = (unsigned long)dev_id; /* 传入定时器中断的参数 */
	mod_timer(&dev->keytimer, jiffies + msecs_to_jiffies(10)); /* 10ms */
	return IRQ_RETVAL(IRQ_HANDLED);
}
/* @description	: 定时器服务函数,用于按键消抖,定时器到了以后
 *				  再次读取按键值,如果按键还是处于按下状态就表示按键有效。
 * @param - arg	: 设备结构变量
 * @return 		: 无
 */
void key0_timer_handler(unsigned long arg)
{
	int value = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;
	/* 读取key0按键值 */
	value = gpio_get_value(dev->gpio_id);
	if(value == 0){ /*按下*/
		atomic_set(&dev->keyvalue, dev->value);
	}
	else{           /*释放*/
		atomic_set(&dev->keyvalue, (0x80)|(dev->value)); /*最高位为释放标志*/
		atomic_set(&dev->keyreleaseflag, 1);
	}
	/* 唤醒进程 */
	if(atomic_read(&dev->keyreleaseflag)){
		//wake_up(&dev->r_wait); /* 将等待队列中的所有的睡眠进程唤醒 */
		wake_up_interruptible(&dev->r_wait);/* 将等待队列中所有处于 TASK_INTERRUPTIBLE 状态的进程 */
	}
	/* 注意:唤醒是指的是唤醒睡眠状态的进程,有两种状态是睡眠状态:
		1、TASK_INTERRUPTIBLE    可中断的
		2、TASK_UNINTERRUPTIBLE  不可中断的
	*/
}

static int keyio_init(struct imx6uirq_dev *dev){
	int ret = 0;
	enum of_gpio_flags flags;
	/*一、获取设备树中的属性数据 */
	/* 1.1、获取设备节点:key */
	dev->nd = of_find_node_by_path("/key");
	if(dev->nd == NULL){
		printk("pinctrl key-led node not find!\r\n");
		return -EINVAL;
	} else {
		printk("pinctrl key-led node find!\r\n");
	}
	/* 1.2、获取gpio的属性,gpio的编号和flags */
	if (of_gpio_named_count(dev->nd, "key-gpio") > 0) {
		dev->gpio_id = of_get_named_gpio_flags(dev->nd, "key-gpio", 0, &flags); /* 第三个参数为key-gpio的元素索引 */
		dev->gpio_flag = (int)flags;
	} else {
		printk("not have key-gpio");
		return -EINVAL;
	}
	printk("gpio:id=%d,flag=%d\r\n",dev->gpio_id,dev->gpio_flag);
	/* 1.3、申请io、设置为输入、获取中断号、设定中断入口函数 */
	memset(dev->name, 0, sizeof(dev->name));
	sprintf(dev->name,"KEY%d",0);
	ret = gpio_request(dev->gpio_id, dev->name); /* 注册IO */
	if(ret < 0){
		printk("%s gpio request fail!\r\n",dev->name);
		goto fail_gpio;
	}
	ret = gpio_direction_input(dev->gpio_id); /* 设置方向 */
	if(ret < 0){
		printk("%s gpio set input fail!\r\n",dev->name);
		goto fail_gpio;
	}
	dev->irqnum = irq_of_parse_and_map(dev->nd, 0); /* 的第二个参数是index,是设备树interrupts元素索引 */
	/*dev->irqnum = gpio_to_irq(dev->gpio_id);*/ /* 获取中断号(注意返回的是linux中断号,不是硬件中断号) */
	if(!dev->irqnum){
		printk("%s gpio irqnum get fail!\r\n",dev->name);
		goto fail_gpio;
	}	
	printk("key0-irq:irqnum=%d\r\n",dev->irqnum);
	/* 1.4、中断初始化(注册中断) */
	dev->handler = key0_handler;
	dev->value = KEY0_VALUE;
	ret = request_irq(dev->irqnum,  /*中断号*/
					  dev->handler, /*中断函数*/
					  IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,/*触发方式*/
					  dev->name,    /*注册label,就是取个名*/
					  dev);         /*中断函数的传入参数*/				 
	if(ret){ /* 返回非零,则为失败 */
		ret = -EBUSY;
		printk("irq %d request fail!",dev->irqnum);
		goto fail_irq;
	}	
	/* 1.5、初始化消抖定时器、原子变量 */
	dev->keytimer.function = key0_timer_handler;/* 注意这里值设定了一个 */
	init_timer(&dev->keytimer);
	atomic_set(&dev->keyvalue, INVAKEY);
	atomic_set(&dev->keyreleaseflag, 0);
	/* 1.6、初始化等待队列头 */
	init_waitqueue_head(&dev->r_wait);
	return 0;
fail_irq:
	free_irq(dev->irqnum, dev);
fail_gpio:
	gpio_free(dev->gpio_id);
	return ret;
}
/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int imx6uirq_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &imx6uirq; /* 设置私有数据 */	
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int ret = 0;
	unsigned char keyvalue = 0;
	unsigned char keyreleaseflag = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
	/* 阻塞与非阻塞处理 */
	if(filp->f_flags & O_NONBLOCK) { /* 非阻塞 */
		if(atomic_read(&dev->keyreleaseflag) == 0){
			return -EAGAIN;
		}
	} 
	else                             /* 阻塞 */
	{
		#if 0
			wait_event_interruptible(dev->r_wait, atomic_read(&dev->keyreleaseflag));
		#else
			DECLARE_WAITQUEUE(wait, current); /* 创建一个等待队列项,对象名为wait */
			while(atomic_read(&dev->keyreleaseflag)== 0){ /* 判断条件 */
				add_wait_queue(&dev->r_wait,&wait);       /* 将项添加到等待队列中 */
				__set_current_state(TASK_INTERRUPTIBLE);  /* 设置当前进程状态 */
				schedule();                               /* 调度一次:此句后进程睡眠-停在这*/
				/* 至此:被唤醒 */
				__set_current_state(TASK_RUNNING);        /* 将当前进程设置为TASK_RUNNING状态 */
				remove_wait_queue(&dev->r_wait, &wait);   /* 将对应的队列项从等待队列中移除 */
					if(signal_pending(current)){              /* 有信号需要处理 */
					return -ERESTARTSYS;
			    } 
	  		 }
		#endif

	}
	/* 读取按键值 */
	keyvalue = atomic_read(&dev->keyvalue);
	keyreleaseflag = atomic_read(&dev->keyreleaseflag);
	if(keyreleaseflag){
		if(keyvalue & 0x80){ /*最高位表示按键值有效*/
			keyvalue &= ~0x80;
			ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
		}
		else{
			return -EINVAL; /* 数据错误 */
		}
		atomic_set(&dev->keyreleaseflag, 0); /* 按下标志清零 */
	}else{
		return -EINVAL; /* 数据错误 */
	}
	return ret;
}
 /*
  * @description     : poll函数,用于处理非阻塞访问
  * @param - filp    : 要打开的设备文件(文件描述符)
  * @param - wait    : 等待列表(poll_table)
  * @return          : 设备或者资源状态,
  */
unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait)
{
	unsigned int mask = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
	poll_wait(filp, &dev->r_wait, wait);	/* 将等待队列头添加到poll_table中(此句阻塞) */
	
	/*是否可读*/
	if(atomic_read(&dev->keyreleaseflag)) { /* 按键按下,可读 */
		mask = POLLIN | POLLRDNORM;	 /* POLLIN | POLLRDNORM:表示可读 */
	}
	return mask;
}
/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t imx6uirq_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int imx6uirq_release(struct inode *inode, struct file *filp)
{
	return 0;
}
/* 设备操作函数 */
static struct file_operations imx6uirq_fops = {
	.owner = THIS_MODULE,
	.open = imx6uirq_open,
	.read = imx6uirq_read,
	.poll = imx6uirq_poll,
	.write = imx6uirq_write,
	.release = imx6uirq_release,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
/* 内核中的也有一个名为key_init()的函数,这里加个x,是为了避免冲突 */
static int __init imx6uirq_init(void) /* 避免与内核中的 */
{
	int ret = 0;
	struct imx6uirq_dev *dev = &imx6uirq;
	/* 设备初始化 */
	ret = keyio_init(dev);
	if(ret < 0){
		printk("keyio_init fail!\r\n");
		goto fail_keyio_init;
	}
	/* 注册字符设备驱动 */
	/* 1、申请设备号 */
	dev->major = 0; /* 设置为0,即没有定义设备号 */
	if (dev->major) { /* 定义了设备号 */
		dev->devid = MKDEV(dev->major, 0);
		ret = register_chrdev_region(dev->devid,//起始设备号
							   		 DEV_CNT,   //设备数量
							   		 DEV_NAME); //设备名称
	} else { /* 没有定义设备号 */
		ret = alloc_chrdev_region(&dev->devid,  //储存设备号的变量
								  0,            //起始次设备号
								  DEV_CNT,      //设备数量
								  DEV_NAME);    //设备名称
		dev->major = MAJOR(dev->devid);         //获取分配号的主设备号
		dev->minor = MINOR(dev->devid);         //获取分配号的次设备号
	}
	if(ret < 0){
		printk("imx6uirq chrdev_region err!\r\n"); 
		goto fail_devid;
	}
	printk("imx6uirq major=%d,minor=%d\r\n", dev->major, dev->minor);	
	/* 2、注册字符设备 */
	dev->cdev.owner = THIS_MODULE;
	cdev_init(&dev->cdev, &imx6uirq_fops); //初始化一个cdev
	ret = cdev_add(&dev->cdev, dev->devid, DEV_CNT); //添加一个cdev
	if(ret < 0 ){
		printk("keydev cdev_add err!\r\n");
		goto fail_cdev;
	}
	/* 3、创建设备节点 */
	dev->class = class_create(THIS_MODULE, DEV_NAME); //先创建一个类(创建的类在/sys/class中)
	if (IS_ERR(dev->class)) {
		printk("keydev class_create err!\r\n"); 
		ret = PTR_ERR(dev->class);
		goto fail_class;
	}
	dev->device = device_create(dev->class,  //该设备依附的类
								NULL,        //父设备
								dev->devid,  //设备号
								NULL,        //私有数据
								DEV_NAME);   //设备名称
	if (IS_ERR(dev->device)) {
		printk("imx6uirq device_create err!\r\n"); 
		ret = PTR_ERR(dev->device);
		goto fail_device;
	}
	return 0;
fail_device:
	class_destroy(dev->class); 
fail_class:
	cdev_del(&dev->cdev); 
fail_cdev:
	unregister_chrdev_region(dev->devid, DEV_CNT);
fail_devid:
fail_keyio_init:
	return ret;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit imx6uirq_exit(void)
{
	struct imx6uirq_dev *dev = &imx6uirq;
    /* 按照相反的顺序注销 */
	device_destroy(dev->class, dev->devid);        /*销毁类*/
	class_destroy(dev->class);                     /*销毁设备节点*/
	cdev_del(&dev->cdev);                          /*删除字符设备*/
	unregister_chrdev_region(dev->devid, DEV_CNT); /*注销设备号*/
	/* 注销irq、注销gpio */
	free_irq(dev->irqnum, dev);
	gpio_free(dev->gpio_id);
	/* 注销定时器 */
	del_timer_sync(&dev->keytimer);
}
module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

   <3>、应用程序:noblockioApp.c

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "poll.h"
#include "stdlib.h"
#include "string.h"

/* 定义按键值 */
#define KEY0VALUE	0X01
#define INVAKEY		0X00
/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, ret;
	char *filename;
	struct pollfd fds;     /* 结构体1:poll使用 */
	fd_set readfds;        /* 结构体2:select使用 */
	struct timeval timeout;/* 结构体3:select使用 */

	int keyvalue = 0;
	if(argc != 2){
		printf("Error Usage!\r\n");
		return -1;
	}
	filename = argv[1];
	/* 打开xxx驱动 */
	fd = open(filename, O_RDWR | O_NONBLOCK);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}

#if 1
	fds.fd = fd;
	fds.events = POLLIN;
	while(1){
		ret = poll(&fds, 1, 1000); /* 500ms超时唤醒一次 */
		if(ret){ /* 数据有效 */
			ret = read(fd, &keyvalue, sizeof(keyvalue));
			if(ret < 0){
				 /* 数据读取错误 */
			} else {
				if(keyvalue == KEY0VALUE) 
					printf("KEY0 Press, value = %#X\r\n", keyvalue);
			}
		} else if(ret == 0) { /* 超时 */
			printf("poll timeout!\r\n");
		} else if(ret < 0){ /* 错误 */

		}
		
	}
#else
	while(1){
		FD_ZERO(&readfds);
		FD_SET(fd, &readfds);
		timeout.tv_sec = 0;
		timeout.tv_usec = 1000000; /* 1000ms */
		ret = select(fd+1, &readfds, NULL, NULL ,&timeout);
		switch(ret){
			case 0:     /* 超时 */
				printf("select timeout!\r\n");
				break;
			case -1:    /* 错误 */

				break;
			default:    /* 可以读取数据 */
				if(FD_ISSET(fd, &readfds)){ /* 判断是不是readfs,后面还有两个,没使用设置成NULL了 */
					ret = read(fd, &keyvalue, sizeof(keyvalue)); /* 如果数据没好,阻塞 */
					if(ret < 0){
					/* ...... */
					}else{
						if (keyvalue == KEY0VALUE) {	/* KEY0 */
						printf("KEY0 Press, value = %#X\r\n", keyvalue);	/* 按下 */
						}
					}
				}
				break;
			}
	}
#endif
	ret= close(fd); /* 关闭文件 */
	if(ret < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

   <4>、编译命令:Makefile

KERNELDIR := /home/haut/itop_imx6ull/software/Linux_Source_nxpV4.1.15   # 内核路径
CURRENT_PATH := $(shell pwd)  # 当前路径

obj-m := noblockio.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

app:
	arm-linux-gnueabihf-gcc ./noblockioApp.c -o noblockioApp
cp:
	cp ./noblockio.ko ./noblockioApp /home/haut/itop_imx6ull/nfs_rootfs/lib/modules/4.1.15

   <5>、运行测试

# 一、加载驱动
  cd lib/modules/4.1.15   //进入modprobe的工作目录
  depmod                  //第一次加载驱动的时候需要运行此命令
  modprobe noblockio.ko   //加载驱动

# 二、运行程序
  ./noblockioApp /dev/imx6uirq  //运行程序

5.14、Linux驱动之异步通知实验

        Linux中有三种常用的中断:硬中断、软中断、信号
                硬中断是外部设备对CPU的中断;
                软中断通常是硬中断服务程序对内核的中断;
                信号是内核(或其他进程)对某个进程的中断;
        异步通知的核心就是信号,应用程序获取到信号以后,对这个信号做出响应,所谓响应就是执行信号所对应的函数,这和" 中断 "很像,其实信号就是对中断的软件模拟,只不过软件的模拟没有硬件的响应快,可能会有点延迟,但是他们的工作原理是相似的。阻塞、非阻塞、异步通知,这三种是针对不同的场合提出来的不同的解决方法,没有优劣之分(阻塞IO是等待设备可访问后再访问)、(非阻塞IO是查询设备是狗可以访问)、(异步通知是设备通知自身可以访问)
        在 arch/xtensa/include/uapi/asm/signal.h 文件中定义了 Linux 所支持的所有信号,这些信号如下所示:

#define SIGHUP 1     /* 终端挂起或控制进程终止 */
#define SIGINT 2     /* 终端中断(Ctrl+C 组合键) */
#define SIGQUIT 3    /* 终端退出(Ctrl+\组合键) */
#define SIGILL 4     /* 非法指令 */
#define SIGTRAP 5    /* debug 使用,有断点指令产生 */
#define SIGABRT 6    /* 由 abort(3)发出的退出指令 */
#define SIGIOT 6     /* IOT 指令 */
#define SIGBUS 7     /* 总线错误 */
#define SIGFPE 8     /* 浮点运算错误 */
#define SIGKILL 9    /* 杀死、终止进程 */
#define SIGUSR1 10   /* 用户自定义信号 1 */
#define SIGSEGV 11   /* 段违例(无效的内存段) */
#define SIGUSR2 12   /* 用户自定义信号 2 */
#define SIGPIPE 13   /* 向非读管道写入数据 */
#define SIGALRM 14   /* 闹钟 */
#define SIGTERM 15   /* 软件终止 */
#define SIGSTKFLT 16 /* 栈异常 */
#define SIGCHLD 17   /* 子进程结束 */
#define SIGCONT 18   /* 进程继续 */
#define SIGSTOP 19   /* 停止进程的执行,只是暂停 */
#define SIGTSTP 20   /* 停止进程的运行(Ctrl+Z 组合键) */
#define SIGTTIN 21   /* 后台进程需要从终端读取数据 */
#define SIGTTOU 22   /* 后台进程需要向终端写数据 */
#define SIGURG 23    /* 有"紧急"数据 */
#define SIGXCPU 24   /* 超过 CPU 资源限制 */
#define SIGXFSZ 25   /* 文件大小超额 */
#define SIGVTALRM 26 /* 虚拟时钟信号 */
#define SIGPROF 27   /* 时钟信号描述 */
#define SIGWINCH 28  /* 窗口大小改变 */
#define SIGIO 29     /* 可以进行输入/输出操作 */
#define SIGPOLL SIGIO
/* #define SIGLOS 29 */
#define SIGPWR 30    /* 断点重启 */
#define SIGSYS 31    /* 非法的系统调用 */
#define SIGUNUSED 31 /* 未使用信号 */

        在这些信号中,除了 SIGKILL(9)和 SIGSTOP(19)这两个信号不能被忽略外,其他的信号都可以忽略。这些信号就相当于中断号,不同的中断号代表了不同的中断,不同的中断所做的处理同,因此,驱动程序可以通过向应用程序发送不同的信号来实现不同的功能,如果要在应用程序中使信号,那么就必须设置信号所使用的信号处理函数,在应用程序中使用 signal 函数来设置指定信号的处理函数。使用“ kill -9 PID”杀死指定进程的方法就是向指定的进程(PID)发送SIGKILL 这个信号。当按下键盘上的CTRL+C组合键以后会向当前正在占用终端的应用程序发出SIGINT信号,SIGINT信号默认的动作是关闭当前应用程序。我们修改一下SIGINT信号的默认处理函数,让其先打印一串文字再退出,如下所示:
#include "stdlib.h"
#include "stdio.h"
#include "signal.h"
void sigint_handler(int num) {
        printf("\r\nSIGINT signal!\r\n");
        exit(0);

}
int main(void) {
        signal(SIGINT, sigint_handler);   //设置SIGINT信号的处理函数为sigint_handler
        while(1);
        return 0;
}

   <1>、驱动程序:asyncnoti.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <linux/errno.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/irq.h>
#include <linux/of_irq.h>
#include <linux/poll.h>
#include <linux/cdev.h>
#include <linux/timer.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/of_address.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <asm/mach/map.h>

#define DEV_CNT     0x01	   /* 设备号个数 */
#define DEV_NAME    "imx6uirq" /* 名字 */
#define KEY_NUM     0x01       /* 按键数 */
#define INVAKEY		0XFF	   /* 无效的按键值 */
#define KEY0_VALUE	0x01	   /* K0按键值 */

/* imx6uirq设备结构体 */
struct imx6uirq_dev{
	/* 设备与节点 */
	dev_t devid;			 /* 设备号 	*/
	int major;				 /* 主设备号 */
	int minor;				 /* 次设备号 */
	struct cdev cdev;		 /* cdev   */
	struct class *class;	 /* 类 	    */
	struct device *device;	 /* 设备 	*/
	struct device_node *nd;  /* 设备节点 */
	/* key0按键相关 */
	int gpio_id;                /* gpio编号 */
	int gpio_flag;              /* gpio标志 */
	int irqnum;	                /* 中断号 */
	unsigned char value;        /* 按键值 */
	char name[10];              /* 注册时使用的label */
	irqreturn_t (*handler)(int, void*);/* 中断处理函数 */
	atomic_t keyvalue;          /* key0有效按键值 */
	atomic_t keyreleaseflag;    /* key0释放标志 */
	struct timer_list keytimer; /* key0消抖定时器 */
	/* 阻塞与非阻塞相关 */
	wait_queue_head_t r_wait;	/* 定义一个等待队列头 */
	/* 异步同志相关 */
	struct fasync_struct *fasync_queue;	/* 异步相关结构体(不需要init()) */	
}imx6uirq;

static irqreturn_t key0_handler(int irq, void *dev_id)
{
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;
	/* 启动消抖动定时器 */
	dev->keytimer.data = (unsigned long)dev_id; /* 传入定时器中断的参数 */
	mod_timer(&dev->keytimer, jiffies + msecs_to_jiffies(10)); /* 10ms */
	return IRQ_RETVAL(IRQ_HANDLED);
}
/* @description	: 定时器服务函数,用于按键消抖,定时器到了以后
 *				  再次读取按键值,如果按键还是处于按下状态就表示按键有效。
 * @param - arg	: 设备结构变量
 * @return 		: 无
 */
void key0_timer_handler(unsigned long arg)
{
	int value = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;
	/* 读取key0按键值 */
	value = gpio_get_value(dev->gpio_id);
	if(value == 0){ /*按下*/
		atomic_set(&dev->keyvalue, dev->value);
	}
	else{           /*释放*/
		atomic_set(&dev->keyvalue, (0x80)|(dev->value)); /*最高位为释放标志*/
		atomic_set(&dev->keyreleaseflag, 1);
	}
	
	/* 判断数据是否有效 */
	if(atomic_read(&dev->keyreleaseflag)){
		if(dev->fasync_queue) kill_fasync(&dev->fasync_queue, SIGIO, POLL_IN);
	}

#if 0
	/* 唤醒进程 */
	if(atomic_read(&dev->keyreleaseflag)) {	/* 完成一次按键过程 */
		wake_up_interruptible(&dev->r_wait);
	}
#endif
}

static int keyio_init(struct imx6uirq_dev *dev){
	int ret = 0;
	enum of_gpio_flags flags;
	/*一、获取设备树中的属性数据 */
	/* 1.1、获取设备节点:key */
	dev->nd = of_find_node_by_path("/key");
	if(dev->nd == NULL){
		printk("pinctrl key-led node not find!\r\n");
		return -EINVAL;
	} else {
		printk("pinctrl key-led node find!\r\n");
	}
	/* 1.2、获取gpio的属性,gpio的编号和flags */
	if (of_gpio_named_count(dev->nd, "key-gpio") > 0) {
		dev->gpio_id = of_get_named_gpio_flags(dev->nd, "key-gpio", 0, &flags); /* 第三个参数为key-gpio的元素索引 */
		dev->gpio_flag = (int)flags;
	} else {
		printk("not have key-gpio");
		return -EINVAL;
	}
	printk("gpio:id=%d,flag=%d\r\n",dev->gpio_id,dev->gpio_flag);
	/* 1.3、申请io、设置为输入、获取中断号、设定中断入口函数 */
	memset(dev->name, 0, sizeof(dev->name));
	sprintf(dev->name,"KEY%d",0);
	ret = gpio_request(dev->gpio_id, dev->name); /* 注册IO */
	if(ret < 0){
		printk("%s gpio request fail!\r\n",dev->name);
		goto fail_gpio;
	}
	ret = gpio_direction_input(dev->gpio_id); /* 设置方向 */
	if(ret < 0){
		printk("%s gpio set input fail!\r\n",dev->name);
		goto fail_gpio;
	}
	dev->irqnum = irq_of_parse_and_map(dev->nd, 0); /* 的第二个参数是index,是设备树interrupts元素索引 */
	/*dev->irqnum = gpio_to_irq(dev->gpio_id);*/ /* 获取中断号(注意返回的是linux中断号,不是硬件中断号) */
	if(!dev->irqnum){
		printk("%s gpio irqnum get fail!\r\n",dev->name);
		goto fail_gpio;
	}	
	printk("key0-irq:irqnum=%d\r\n",dev->irqnum);
	/* 1.4、中断初始化(注册中断) */
	dev->handler = key0_handler;
	dev->value = KEY0_VALUE;
	ret = request_irq(dev->irqnum,  /*中断号*/
					  dev->handler, /*中断函数*/
					  IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,/*触发方式*/
					  dev->name,    /*注册label,就是取个名*/
					  dev);         /*中断函数的传入参数*/				 
	if(ret){ /* 返回非零,则为失败 */
		ret = -EBUSY;
		printk("irq %d request fail!",dev->irqnum);
		goto fail_irq;
	}	
	/* 1.5、初始化消抖定时器、原子变量 */
	dev->keytimer.function = key0_timer_handler;/* 注意这里值设定了一个 */
	init_timer(&dev->keytimer);
	atomic_set(&dev->keyvalue, INVAKEY);
	atomic_set(&dev->keyreleaseflag, 0);
	/* 1.6、初始化等待队列头 */
	init_waitqueue_head(&dev->r_wait);
	return 0;
fail_irq:
	free_irq(dev->irqnum, dev);
fail_gpio:
	gpio_free(dev->gpio_id);
	return ret;
}
/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int imx6uirq_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &imx6uirq; /* 设置私有数据 */	
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int ret = 0;
	unsigned char keyvalue = 0;
	unsigned char keyreleaseflag = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
	/* 阻塞与非阻塞处理 */
	if(filp->f_flags & O_NONBLOCK) { /* 非阻塞 */
		if(atomic_read(&dev->keyreleaseflag) == 0){
			return -EAGAIN;
		}
	} 
	else /* 阻塞 */
	{
		ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->keyreleaseflag));
		if(ret) return ret;
	}
	/* 读取按键值 */
	keyvalue = atomic_read(&dev->keyvalue);
	keyreleaseflag = atomic_read(&dev->keyreleaseflag);
	if(keyreleaseflag){
		if(keyvalue & 0x80){ /*最高位表示按键值有效*/
			keyvalue &= ~0x80;
			ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
		}
		else{
			return -EINVAL; /* 数据错误 */
		}
		atomic_set(&dev->keyreleaseflag, 0); /* 按下标志清零 */
	}else{
		return -EINVAL; /* 数据错误 */
	}
	return ret;
}
 /*
  * @description     : poll函数,用于处理非阻塞访问
  * @param - filp    : 要打开的设备文件(文件描述符)
  * @param - wait    : 等待列表(poll_table)
  * @return          : 设备或者资源状态,
  */
unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait)
{
	unsigned int mask = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
	poll_wait(filp, &dev->r_wait, wait);	/* 将等待队列头添加到poll_table中(此句阻塞) */
	
	/* 是否可读 */
	if(atomic_read(&dev->keyreleaseflag)) { /* 按键按下,可读 */
		mask = POLLIN | POLLRDNORM;	 /* POLLIN | POLLRDNORM:表示可读 */
	}
	return mask;
}
/*
 * @description     : fasync函数,用于处理异步通知
 * @param - fd		: 文件描述符
 * @param - filp    : 要打开的设备文件(文件描述符)
 * @param - on      : 模式
 * @return          : 负数表示函数执行失败
 */
static int imx6uirq_fasync(int fd, struct file *filp, int on)
{
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
	printk("fa run,on = %d\r\n",on);
	return fasync_helper(fd, filp, on, &dev->fasync_queue);
}
/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int imx6uirq_release(struct inode *inode, struct file *filp)
{
	return imx6uirq_fasync(-1, filp, 0);
}
/* 设备操作函数 */
static struct file_operations imx6uirq_fops = {
	.owner = THIS_MODULE,
	.open = imx6uirq_open,
	.read = imx6uirq_read,
	.poll = imx6uirq_poll,
	.fasync = imx6uirq_fasync,
	.release = imx6uirq_release,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
/* 内核中的也有一个名为key_init()的函数,这里加个x,是为了避免冲突 */
static int __init imx6uirq_init(void) /* 避免与内核中的 */
{
	int ret = 0;
	struct imx6uirq_dev *dev = &imx6uirq;
	/* 设备初始化 */
	ret = keyio_init(dev);
	if(ret < 0){
		printk("keyio_init fail!\r\n");
		goto fail_keyio_init;
	}
	/* 注册字符设备驱动 */
	/* 1、申请设备号 */
	dev->major = 0; /* 设置为0,即没有定义设备号 */
	if (dev->major) { /* 定义了设备号 */
		dev->devid = MKDEV(dev->major, 0);
		ret = register_chrdev_region(dev->devid,//起始设备号
							   		 DEV_CNT,   //设备数量
							   		 DEV_NAME); //设备名称
	} else { /* 没有定义设备号 */
		ret = alloc_chrdev_region(&dev->devid,  //储存设备号的变量
								  0,            //起始次设备号
								  DEV_CNT,      //设备数量
								  DEV_NAME);    //设备名称
		dev->major = MAJOR(dev->devid);         //获取分配号的主设备号
		dev->minor = MINOR(dev->devid);         //获取分配号的次设备号
	}
	if(ret < 0){
		printk("imx6uirq chrdev_region err!\r\n"); 
		goto fail_devid;
	}
	printk("imx6uirq major=%d,minor=%d\r\n", dev->major, dev->minor);	
	/* 2、注册字符设备 */
	dev->cdev.owner = THIS_MODULE;
	cdev_init(&dev->cdev, &imx6uirq_fops); //初始化一个cdev
	ret = cdev_add(&dev->cdev, dev->devid, DEV_CNT); //添加一个cdev
	if(ret < 0 ){
		printk("keydev cdev_add err!\r\n");
		goto fail_cdev;
	}
	/* 3、创建设备节点 */
	dev->class = class_create(THIS_MODULE, DEV_NAME); //先创建一个类(创建的类在/sys/class中)
	if (IS_ERR(dev->class)) {
		printk("keydev class_create err!\r\n"); 
		ret = PTR_ERR(dev->class);
		goto fail_class;
	}
	dev->device = device_create(dev->class,  //该设备依附的类
								NULL,        //父设备
								dev->devid,  //设备号
								NULL,        //私有数据
								DEV_NAME);   //设备名称
	if (IS_ERR(dev->device)) {
		printk("imx6uirq device_create err!\r\n"); 
		ret = PTR_ERR(dev->device);
		goto fail_device;
	}
	return 0;
fail_device:
	class_destroy(dev->class); 
fail_class:
	cdev_del(&dev->cdev); 
fail_cdev:
	unregister_chrdev_region(dev->devid, DEV_CNT);
fail_devid:
fail_keyio_init:
	return ret;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit imx6uirq_exit(void)
{
	struct imx6uirq_dev *dev = &imx6uirq;
    /* 按照相反的顺序注销 */
	device_destroy(dev->class, dev->devid);        /*销毁类*/
	class_destroy(dev->class);                     /*销毁设备节点*/
	cdev_del(&dev->cdev);                          /*删除字符设备*/
	unregister_chrdev_region(dev->devid, DEV_CNT); /*注销设备号*/
	/* 注销irq、注销gpio */
	free_irq(dev->irqnum, dev);
	gpio_free(dev->gpio_id);
	/* 注销定时器 */
	del_timer_sync(&dev->keytimer);
}

module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

   <2>、应用程序:asyncnotiApp.c

#include "stdio.h"
#include "poll.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include "signal.h"
#include "unistd.h"
#include "sys/time.h"
#include "sys/stat.h"
#include "sys/types.h"
#include "sys/select.h"
#include "linux/ioctl.h"

/* 定义按键值 */
#define KEY0VALUE	0X01
#define INVAKEY		0X00

/*
 * SIGIO信号处理函数
 * @param - signum 	: 信号值
 * @return 			: 无
 */
static int fd = 0;	/* 文件描述符 */
static void sigio_signal_func(int signum)
{
	int err = 0;
	unsigned int keyvalue = 0;

	err = read(fd, &keyvalue, sizeof(keyvalue));
	if(err < 0) {
		/* 读取错误 */
	} else {
		printf("sigio signal! key value=%d\r\n", keyvalue);
	}
}

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int ret, flags;
	char *filename;
	int keyvalue = 0;
	if(argc != 2){
		printf("Error Usage!\r\n");
		return -1;
	}
	filename = argv[1];
	/* 打开xxx驱动 */
	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}
	/* 设置信号SIDIO的处理函数 */
	signal(SIGIO, sigio_signal_func);   /*设置信号的处理函数*/
	fcntl(fd, F_SETOWN, getpid());      /*设置当前进程接收SIGIO信号*/
	flags = fcntl(fd, F_GETFL);         /*获取当前进程的状态*/
	fcntl(fd, F_SETFL, flags | FASYNC); /*设置进程启用异步通知功能(驱动的.fasync会执行)*/
	while(1) {
		sleep(2);
	}

	ret= close(fd); /* 关闭文件 */
	if(ret < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

   <3>、编译命令:Makefile

KERNELDIR := /home/haut/itop_imx6ull/software/Linux_Source_nxpV4.1.15   # 内核路径
CURRENT_PATH := $(shell pwd)  # 当前路径

obj-m := asyncnoti.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

app:
	arm-linux-gnueabihf-gcc ./asyncnotiApp.c -o asyncnotiApp
cp:
	cp ./asyncnoti.ko ./asyncnotiApp /home/haut/itop_imx6ull/nfs_rootfs/lib/modules/4.1.15

   <4>、运行测试

# 一、加载驱动
  cd lib/modules/4.1.15   //进入modprobe的工作目录
  depmod                  //第一次加载驱动的时候需要运行此命令
  modprobe asyncnoti.ko   //加载驱动

# 二、运行程序
  ./asyncnotiApp /dev/imx6uirq  //运行程序

5.15、Linux驱动之platform设备驱动实验

        驱动的分离:如下图1所示,把主机驱动和设备驱动分开,通过核心层设定的API进行管理。但在实际大多数情况下,主机驱动半导体厂商都已经写好了,所以Linux内核的驱动模型如图2所示,这个就是 Linux 中的总线(bus)、驱动(driver)和设备(device)模型。其中总线有I2C、SPI、USB等总线,但是在 SOC 中有些外设是没有总线这个概念的,但是又要使用总线、驱动和设备模型该怎么办呢?为了解决此问题,Linux提出了platform这个虚拟总线,相应的就有 platform_driver 和 platform_device。
 

        驱动的分层:网络有7层模型,不同的层负责不同的内容。同样的,Linux下的驱动往往也是分层的,分层的目的也是为了在不同的层处理不同的内容。以其他书籍或者资料常常使用到的input(输入子系统,后面有专门的章节详细的讲解)为例,简单介绍一下驱动的分层。

1、Linux系统内核使用bus_type结构体表示总线,此结构体定义在文件include/linux/device.h。bus_type 结构体内容如下:

struct bus_type {
	const char *name; /* 总线名字 */
	const char *dev_name;
	struct device *dev_root;
	struct device_attribute *dev_attrs;
	const struct attribute_group **bus_groups; /* 总线属性 */
	const struct attribute_group **dev_groups; /* 设备属性 */
	const struct attribute_group **drv_groups; /* 驱动属性 */
	
	int (*match)(struct device *dev, struct device_driver *drv);
	int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
	int (*probe)(struct device *dev);
	int (*remove)(struct device *dev);
	void (*shutdown)(struct device *dev);
	
	int (*online)(struct device *dev);
	int (*offline)(struct device *dev);
	int (*suspend)(struct device *dev, pm_message_t state);
	int (*resume)(struct device *dev);
	const struct dev_pm_ops *pm;
	const struct iommu_ops *iommu_ops;
	struct subsys_private *p;
	struct lock_class_key lock_key;
};

2、platform_driver结构体表示platform驱动,体定义在include/linux/platform_device.h中,内容如下:

struct platform_driver {
	int (*probe)(struct platform_device *);
	int (*remove)(struct platform_device *);
	void (*shutdown)(struct platform_device *);
	int (*suspend)(struct platform_device *, pm_message_t state);
	int (*resume)(struct platform_device *);
	struct device_driver driver;
	const struct platform_device_id *id_table;
	bool prevent_deferred_probe;
};

3、platform_device 这个结构体表示 platform 设备,这里我们要注意,如果内核支持设备树
的话就不要再使用 platform_device 来描述设备了,因为改用设备树去描述了。当然了,你如果
一定要用 platform_device 来描述设备信息的话也是可以的。 platform_device 结构体定义在文件
include/linux/platform_device.h 中,结构体内容如下:

struct platform_device {
	const char *name;
	int id;
	bool id_auto;
	struct device dev;
	u32 num_resources;
	struct resource *resource;
	
	const struct platform_device_id *id_entry;
	char *driver_override; /* Driver name to force a match */
	
	/* MFD cell pointer */
	struct mfd_cell *mfd_cell;
	
	/* arch specific additions */
	struct pdev_archdata archdata;
};

X. 总结:用platform_device注册设备资源,即描述设备信息,用platform_driver注册设备驱动
        有了设备树,就可以用设备树来描述设备信息,就不需要用platform_device描述设备信了

5.16、Linux驱动之Linux自带的LED驱动实验

5.17、Linux驱动之MISC驱动实验

        misc 的意思是混合、杂项的,因此 MISC 驱动也叫做杂项驱动,也就是当我们板子上的某些外设无法进行分类的时候就可以使用 MISC 驱动。MISC驱动其实就是最简单的字符设备驱动,通常嵌套在 platform 总线驱动中,实现复杂的驱动。
        所有的 MISC 设备驱动的主设备号都为 10,不同的设备使用不同的从设备号。随着Linux字符设备驱动的不断增加,设备号变得越来越紧张,尤其是主设备号,MISC设备驱动就用于解决此问题。MISC 设备会自动创建 cdev,不需要像我们以前那样手动创建,因此采用 MISC 设备驱动可以简化字符设备驱动的编写我们需要向 Linux 注册一个 miscdevice 设备,miscdevice是一个结构体,定义在文件 include/linux/miscdevice.h 中,内容如下:

struct miscdevice {
	int minor;        /* 子设备号(如果设置为255,则自动分配子设备号,而不是设置成255) */
	const char *name; /* 设备名字 */
	const struct file_operations *fops; /* 设备操作集 */
	struct list_head list;
	struct device *parent;
	struct device *this_device;
	const struct attribute_group **groups;
	const char *nodename;
	umode_t mode;
};

        定义一个 MISC 设备(miscdevice 类型)以后我们需要设置 minor、 name 和 fops 这三个成员变量。 minor 表示子设备号, MISC 设备的主设备号为 10,这个是固定的,需要用户指定子设备号, Linux 系统已经预定义了一些 MISC 设备的子设备号,这些预定义的子设备号定义在include/linux/miscdevice.h 文件中,如下所示:

/* 我们在使用的时候可以从这些预定义的子设备号中挑选一个,当然也可以自己定义,只要这个子设备号没有被其他设备使用接口。*/
#define PSMOUSE_MINOR		 1
#define MS_BUSMOUSE_MINOR	 2	/* unused */
#define ATIXL_BUSMOUSE_MINOR 3	/* unused */
/*#define AMIGAMOUSE_MINOR	 4	FIXME OBSOLETE */
#define ATARIMOUSE_MINOR	 5	/* unused */
#define SUN_MOUSE_MINOR		 6	/* unused */
#define APOLLO_MOUSE_MINOR	 7	/* unused */
#define PC110PAD_MINOR		 9	/* unused */
/*#define ADB_MOUSE_MINOR	 10	FIXME OBSOLETE */
#define WATCHDOG_MINOR		 130	/* Watchdog timer     */
#define TEMP_MINOR		     131	/* Temperature Sensor */
#define RTC_MINOR		     135
#define EFI_RTC_MINOR		 136	/* EFI Time services */
#define VHCI_MINOR		     137
#define SUN_OPENPROM_MINOR	 139
#define DMAPI_MINOR		     140	/* unused */
#define NVRAM_MINOR		     144
#define SGI_MMTIMER		     153
#define STORE_QUEUE_MINOR	 155	/* unused */
#define I2O_MINOR		     166
#define MICROCODE_MINOR		 184
#define VFIO_MINOR		     196
#define TUN_MINOR		     200
#define CUSE_MINOR		     203
#define MWAVE_MINOR		     219	/* ACP/Mwave Modem */
#define MPT_MINOR		     220
#define MPT2SAS_MINOR		 221
#define MPT3SAS_MINOR		 222
#define UINPUT_MINOR		 223
#define MISC_MCELOG_MINOR	 227
#define HPET_MINOR		     228
#define FUSE_MINOR		     229
#define KVM_MINOR		     232
#define BTRFS_MINOR		     234
#define AUTOFS_MINOR		 235
#define MAPPER_CTRL_MINOR	 236
#define LOOP_CTRL_MINOR		 237
#define VHOST_NET_MINOR		 238
#define UHID_MINOR		     239
#define MISC_DYNAMIC_MINOR	 255

    <1>、设备树

    <2>、驱动程序:Makefile 

5.18、Linux驱动之INPUT子系统实验

        input 就是输入的意思,因此 input 子系统就是管理输入的子系统,和 pinctrl、 gpio 子系统一样,都是 Linux 内核针对某一类设备而创建的框架。比如按键输入、键盘、鼠标、触摸屏等等这些都属于输入设备,不同的输入设备所代表的含义不同,按键和键盘就是代表按键信息,鼠标和触摸屏代表坐标信息,因此在应用层的处理就不同,对于驱动编写者而言不需要去关心应用层的事情,我们只需要按照要求上报这些输入事件即可。为此 input 子系统分为 input 驱动层、 input 核心层、 input 事件处理层,最终给用户空间提供可访问的设备节点, input 子系统框架如图所示:


5.19、Linux驱动之LCD驱动实验

        程序启动时,可以指定其使用哪个标准输入输出设备(即终端),也可以不指定(不涉及输入输出时就不用指定)(子进程默认使用父进程的终端)。系统使用的的标准输入输出设备叫做控制台,其它程序一般使用终端(也可以使用控制台),无论是控制台还是终端,他们都是标准输入输出设备。在文件系统下,体现为/dev/console  /dev/ttyx,学过驱动的都知道这些都是驱动的设备节点文件,大多数情况下一般不手动创建它们,都是驱动程序注册的,一个驱动程序可能注册有多个设备节点文件,程序只能操作设备节点文件,至于怎么操作,操作的哪个具体的硬件设备(有时候并不是硬件),由驱动程序决定
        /dev/console 是控制台设备,(取名为console,它是系统默认的标准输入输出设备)
        /dev/ttyx        是终端设备,(取名为ttyx,一般是作为普通程序的标准输入输出设备)
重点1:它们是标准的输入输出,但是他们并不是真实的硬件设备(是虚拟设备),他们仅仅是对普通的输入输出做标准化而已,普通的输入输出一般都是一个具体的硬件设备(比如串口、显示器等),所以你要想使用终端,比如把终端与普通输入输出设备联系起来,如下图所示:

重点2:思考一下,终端/dev/tty1是怎么和/dev/fb0联系起来的?
重点3:终端和终端也是可以联系起来的。如下展示几种组合
        /dev/ttymxc0   <=>    /dev/console   <=     内核
        /dev/ttymxc0   <=>    /dev/console   <=>   /bin/bash
        /dev/fb0           <=>    /dev/tty1         <=>   /dev/console <=> /bin/bash
        硬件(普通输入输出)    <=>    终端(标准输入输出)    <= | => | <=>    应用程序

重点4:/dev/fb0 一般搭配UI库使用,也可以自己直接操作(可以用如下代码体验一下):

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <linux/fb.h>
#include <fcntl.h>
#include <sys/mman.h>
static struct fb_var_screeninfo var; 
static int screen_size;
static unsigned char *fb_base;
static unsigned int line_size; /*每行像素所占字节数*/
static unsigned int pixel_size; /*每个像素所占字节数*/
void lcd_put_pixel(int x, int y, unsigned int color); /*打点函数*/
int main(int argc, char *argv[])
{
	int fd, i;
	int ret = 0;
	char *filename;
	if (argc != 2) {
		printf("Error Usage!\r\n");
		return -1;
	}
	/* 打开设备 */
	filename = argv[1];
	fd = open(filename, O_RDWR);
	if(fd < 0) {
		printf("can't open file %s\r\n", filename);
		return -1;
	}
	/* 获取设备信息 */
 	if (ioctl(fd, FBIOGET_VSCREENINFO, &var))
	{
    	printf("can't get var\n");
    	return -1;
	}
	line_size = var.xres * var.bits_per_pixel / 8;                /*每行像素所占字节数*/
	pixel_size = var.bits_per_pixel / 8;                          /*每个像素所占字节数*/
	screen_size = var.xres * var.yres * var.bits_per_pixel / 8;   /*屏幕所占总的字节数*/
	printf("xres=%d,yres=%d\n",var.xres,var.yres);
	printf("bits_per_pixel = %d\n",var.bits_per_pixel);
	/* 为/dev/fb0映射一个buff */
	fb_base = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	if (fb_base == (unsigned char *)-1)
	{
		printf("can't mmap\n");
		return -1;
	}
	/* 清屏: 全部设为白色 */
	memset(fb_base, 0xff, screen_size);

	/* 随便设置出 100 个为红色 */
	for (i = 0; i < 100; i++)
	lcd_put_pixel(var.xres/2+i, var.yres/2, 0xFF0000);

	// 释放映射
	if (munmap(fb_base, screen_size) == -1)
	{
		printf("munmap failed\n");
		close(fd);
		return -1;
	}
	close(fd);
	return 0;
}

//传入的 color 表示颜色,它的格式永远是 0x00RRGGBB,即 RGB888。
//当 LCD 是 16bpp 时,要把 color 变量中的 R、 G、 B 抽出来再合并成 RGB565 格式。
void lcd_put_pixel(int x, int y, unsigned int color)
{
    unsigned char *pen_8 = fb_base+y*line_size+x*pixel_size;
    //计算(x,y)坐标上像素对应的 Framebuffer 地址。
    unsigned short *pen_16;
    unsigned int *pen_32;
    pen_16 = (unsigned short *)pen_8;
    pen_32 = (unsigned int *)pen_8;

    unsigned int red, green, blue;
    switch (var.bits_per_pixel)
    {
      //对于 8bpp, color 就不再表示 RBG 三原色了,这涉及调色板的概念, color 是调色板的值。
        case 8:
            {
                *pen_8 = color;
                break;
            }
        case 16:
            {
                /* 565 */
                //先从 color 变量中把 R、 G、 B 抽出来。
                red = (color >> 16) & 0xff;
                green = (color >> 8) & 0xff;
                blue = (color >> 0) & 0xff;
                //把 red、 green、 blue 这三种 8 位颜色值,根据 RGB565 的格式,
                //只保留 red 中的高 5 位、 green 中的高 6 位、 blue 中的高 5 位,
                //组合成一个新的 16 位颜色值。
                color = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
                //把新的 16 位颜色值写入 Framebuffer
                *pen_16 = color;
                break;
            }
        case 32:
            {
                //对于 32bpp,颜色格式跟 color 参数一致,可以直接写入Framebuffer
                *pen_32 = color;
                break;
            }
        default:
            {
                printf("can't surport %dbpp\n",var.bits_per_pixel);
                break;
            }
     }
}

5.20、Linux驱动之RTC驱动实验

 date -s "2022-09-15 13:06:06"  //设置当前系统时间
 hwclock -w                                 //将当前系统时间写入到 RTC 里面

5.21、Linux驱动之IIC驱动实验

如下是一个IIC子系统的框架图 

如下是IIC子系统程序的实现方式

// ===>文件1:ap3216creg.h

#ifndef AP3216C_H
#define AP3216C_H
#define AP3216C_ADDR    	0X1E	/* AP3216C器件地址  */
/* AP3316C寄存器 */
#define AP3216C_SYSTEMCONG	0x00	/* 配置寄存器       */
#define AP3216C_INTSTATUS	0X01	/* 中断状态寄存器   */
#define AP3216C_INTCLEAR	0X02	/* 中断清除寄存器   */
#define AP3216C_IRDATALOW	0x0A	/* IR数据低字节     */
#define AP3216C_IRDATAHIGH	0x0B	/* IR数据高字节     */
#define AP3216C_ALSDATALOW	0x0C	/* ALS数据低字节    */
#define AP3216C_ALSDATAHIGH	0X0D	/* ALS数据高字节    */
#define AP3216C_PSDATALOW	0X0E	/* PS数据低字节     */
#define AP3216C_PSDATAHIGH	0X0F	/* PS数据高字节     */
#endif

// ===>文件2:ap3216cApp.c
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include "ap3216creg.h"
#define AP3216C_CNT	  1
#define AP3216C_NAME  "ap3216c"
struct ap3216c_dev {
	dev_t devid;			    /* 设备号 	*/
	struct cdev cdev;		    /* cdev    */
	struct class *class;	    /* 类 	   */
	struct device *device;	    /* 设备     */
	struct device_node	*nd;    /* 设备节点 */
	int major;			        /* 主设备号 */
	void *private_data;	        /* 私有数据 */
	unsigned short ir, als, ps;	/* 三个光传感器数据 */
}ap3216cdev;

static int ap3216c_read_regs(struct ap3216c_dev *dev, u8 reg, void *val, int len)
{
	int ret;
	struct i2c_msg msg[2];
	struct i2c_client *client = (struct i2c_client *)dev->private_data;

	/* msg[0]为:发送=>要读取的首地址 */
	msg[0].addr = client->addr;			/* ap3216c地址 */
	msg[0].flags = 0;					/* 标记为发送数据 */
	msg[0].buf = &reg;					/* 读取的首地址 */
	msg[0].len = 1;						/* reg长度*/

	/* msg[1]为:读取=>数据 */
	msg[1].addr = client->addr;			/* ap3216c地址 */
	msg[1].flags = I2C_M_RD;			/* 标记为读取数据*/
	msg[1].buf = val;					/* 读取数据缓冲区 */
	msg[1].len = len;					/* 要读取的数据长度*/

	ret = i2c_transfer(client->adapter, msg, 2); /*进行两个msg*/
	if(ret == 2) {
		ret = 0;
	} else {
		printk("i2c rd failed=%d reg=%06x len=%d\n",ret, reg, len);
		ret = -EREMOTEIO;
	}
	return ret;
}
static s32 ap3216c_write_regs(struct ap3216c_dev *dev, u8 reg, u8 *buf, u8 len)
{
	/*因为两个msg都是写,所以这里合并在一起(也可以分两个msg写)*/
	u8 b[64];
	struct i2c_msg msg;
	struct i2c_client *client = (struct i2c_client *)dev->private_data;
	
	b[0] = reg;					/* 寄存器首地址 */
	memcpy(&b[1],buf,len);		/* 将要写入的数据拷贝到数组b里面 */
		
	msg.addr = client->addr;	/* ap3216c地址 */
	msg.flags = 0;				/* 标记为写数据 */

	msg.buf = b;				/* 要写入的数据缓冲区 */
	msg.len = len + 1;			/* 要写入的数据长度 */

	return i2c_transfer(client->adapter, &msg, 1);
}
static unsigned char ap3216c_read_reg(struct ap3216c_dev *dev, u8 reg)
{
	u8 data = 0;
	ap3216c_read_regs(dev, reg, &data, 1);
	return data;

#if 0
	struct i2c_client *client = (struct i2c_client *)dev->private_data;
	return i2c_smbus_read_byte_data(client, reg);
#endif
}
static void ap3216c_write_reg(struct ap3216c_dev *dev, u8 reg, u8 data)
{
	u8 buf = 0;
	buf = data;
	ap3216c_write_regs(dev, reg, &buf, 1);
}
/*
 * @description	: 读取AP3216C的数据,读取原始数据,包括ALS,PS和IR, 注意!
 *				: 如果同时打开ALS,IR+PS的话两次数据读取的时间间隔要大于112.5ms
 * @param - ir	: ir数据
 * @param - ps 	: ps数据
 * @param - ps 	: als数据 
 * @return 		: 无。
 */
void ap3216c_readdata(struct ap3216c_dev *dev)
{
	unsigned char i =0;
    unsigned char buf[6];
	
	/* 循环读取所有传感器数据 */
    for(i = 0; i < 6; i++)	
    {
        buf[i] = ap3216c_read_reg(dev, AP3216C_IRDATALOW + i);	
    }

    if(buf[0] & 0X80) 	/* IR_OF位为1,则数据无效 */
		dev->ir = 0;					
	else 				/* 读取IR传感器的数据   		*/
		dev->ir = ((unsigned short)buf[1] << 2) | (buf[0] & 0X03); 			
	
	dev->als = ((unsigned short)buf[3] << 8) | buf[2];	/* 读取ALS传感器的数据 			 */  
	
    if(buf[4] & 0x40)	/* IR_OF位为1,则数据无效 			*/
		dev->ps = 0;    													
	else 				/* 读取PS传感器的数据    */
		dev->ps = ((unsigned short)(buf[5] & 0X3F) << 4) | (buf[4] & 0X0F); 
}


static int ap3216c_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &ap3216cdev;
	/* 初始化AP3216C */
	ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0x04);		/* 复位AP3216C 			*/
	mdelay(50);														/* AP3216C复位最少10ms 	*/
	ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0X03);		/* 开启ALS、PS+IR 		*/
	printk("ap3216c_open():Init ap3216c ok!!!\n");
	return 0;
}
static ssize_t ap3216c_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
    short data[3];
	long err = 0;
	struct ap3216c_dev *dev = (struct ap3216c_dev *)filp->private_data;
	ap3216c_readdata(dev);
	data[0] = dev->ir;
	data[1] = dev->als;
	data[2] = dev->ps;
	err = copy_to_user(buf, data, sizeof(data));
	return 0;
}
static int ap3216c_release(struct inode *inode, struct file *filp)
{
	return 0;
}
static const struct file_operations ap3216c_ops = {/* AP3216C操作函数 */
	.owner = THIS_MODULE,
	.open = ap3216c_open,
	.read = ap3216c_read,
	.release = ap3216c_release,
};
static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id){
/*
*   client为此ap3216c设备信息的指针,也就是设备树中的信息,设备在匹配的时候,
*   总线的匹配函数会创建一个struct i2c_client结构体变量,并把设备信息填写进去
*   client就是指向这个结构体变量,变量包括设备的地址,设备父节点是谁(设备对应的那个I2C)
*/
    /* 1、构建设备号 */
	if (ap3216cdev.major) {
		ap3216cdev.devid = MKDEV(ap3216cdev.major, 0);
		register_chrdev_region(ap3216cdev.devid, AP3216C_CNT, AP3216C_NAME);
	} else {
		alloc_chrdev_region(&ap3216cdev.devid, 0, AP3216C_CNT, AP3216C_NAME);
		ap3216cdev.major = MAJOR(ap3216cdev.devid);
	}
	/* 2、注册设备 */
	cdev_init(&ap3216cdev.cdev, &ap3216c_ops);
	cdev_add(&ap3216cdev.cdev, ap3216cdev.devid, AP3216C_CNT);
	/* 3、创建类 */
	ap3216cdev.class = class_create(THIS_MODULE, AP3216C_NAME);
	if (IS_ERR(ap3216cdev.class)) {
		return PTR_ERR(ap3216cdev.class);
	}
	/* 4、创建设备 */
	ap3216cdev.device = device_create(ap3216cdev.class, NULL, ap3216cdev.devid, NULL, AP3216C_NAME);
	if (IS_ERR(ap3216cdev.device)) {
		return PTR_ERR(ap3216cdev.device);
	}
	ap3216cdev.private_data = client; /**/
	printk("ap3216c_probe():add i2c dev to kernel ok!!\n");
    return 0;
}
static int ap3216c_remove(struct i2c_client *client)
{
    /* 删除设备 */
	cdev_del(&ap3216cdev.cdev);
	unregister_chrdev_region(ap3216cdev.devid, AP3216C_CNT);
	/* 注销掉类和设备 */
	device_destroy(ap3216cdev.class, ap3216cdev.devid);
	class_destroy(ap3216cdev.class);
	return 0;
}
static const struct i2c_device_id ap3216c_id[] = { /* 传统匹配方式ID列表 */
	{"itop-evk,ap3216c", 0},  
	{}
};
static const struct of_device_id ap3216c_of_match[] = { /* 设备树匹配列表 */
	{ .compatible = "itop-evk,ap3216c" },
	{ /* Sentinel */ }
};
/* i2c驱动结构体 */	
static struct i2c_driver ap3216c_driver = {
    .probe = ap3216c_probe,
    .remove = ap3216c_remove,
    .driver = {
        .name = "itop-evk,dev-ap3216c", /*驱动名称(解析在/sys/bus/i2c/drivers)*/
        .owner = THIS_MODULE,
        .of_match_table = ap3216c_of_match,
    },
    .id_table = ap3216c_id /*未使用dts的匹配*/
};
/* @description	: 驱动入口函数 */
static int __init ap3216c_init(void)
{
	return i2c_add_driver(&ap3216c_driver);
}
/* @description	: 驱动出口函数 */
static void __exit ap3216c_exit(void)
{
    i2c_del_driver(&ap3216c_driver);
}
module_init(ap3216c_init);
module_exit(ap3216c_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

// ===>文件3:ap3216c.c
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>
/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd;
	char *filename;
	unsigned short databuf[3];
	unsigned short ir, als, ps;
	int ret = 0;
	if (argc != 2) {
		printf("Error Usage!\r\n");
		return -1;
	}
	filename = argv[1];
	fd = open(filename, O_RDWR);
	if(fd < 0) {
		printf("can't open file %s\r\n", filename);
		return -1;
	}
	while (1) {
		ret = read(fd, databuf, sizeof(databuf));
		if(ret == 0) { 			/* 数据读取成功 */
			ir =  databuf[0]; 	/* ir传感器数据 :红外线强度*/
			als = databuf[1]; 	/* als传感器数据:环境光强度*/
			ps =  databuf[2]; 	/* ps传感器数据 :接近距离*/
			printf("ir = %d, als = %d, ps = %d\r\n", ir, als, ps);
		}
		usleep(200000); /*100ms */
	}
	close(fd);	/* 关闭文件 */	
	return 0;
}

5.22、Linux驱动之SPI驱动实验

        SPI 全称是 Serial Perripheral Interface,也就是串行外围设备接口。SPI 通信都是由主机发起的,主机需要提供通信的时钟信号。SPI 是 Motorola 公司推出的一种同步串行接口技术,是一种高速、全双工的同步通信总线, SPI 时钟频率相比 I2C 要高很多,最高可以工作在上百 MHz。 SPI 以主从方式工作,通常是有一个主设备和一个或多个从设备,一般 SPI 需要 4 根线,但是也可以使用三根线(单向传输)。
        SPI 通信都是由主机发起的,主机需要提供通信的时钟信号。主机通过 SPI 线连接多个从
设备的结构如图所示:

    =>SPI 有四种工作模式,通过串行时钟极性(CPOL)和相位(CPHA)的搭配来得到四种工作模式:
        ①、 CPOL=0,串行时钟空闲状态为低电平。
        ②、 CPOL=1,串行时钟空闲状态为高电平,此时可以通过配置时钟相位(CPHA)来选择具
        体的传输协议。
        ③、 CPHA=0,串行时钟的第一个跳变沿(上升沿或下降沿)采集数据。
        ④、 CPHA=1,串行时钟的第二个跳变沿(上升沿或下降沿)采集数据。
    =>这四种工作模式如图所示:

        跟 I2C 一样,SPI也是有时序图的,以CPOL=0,CPHA=0这个工作模式为例,SPI进行全双工通信的时序如图所示:

         从图可以看出, SPI 的时序图很简单,不像 I2C 那样还要分为读时序和写时序,因为 SPI 是全双工的,所以读写时序可以一起完成。图中, CS 片选信号先拉低,选中要通信的从设备,然后通过 MOSI 和 MISO 这两根数据线进行收发数据, MOSI 数据线发出了0XD2 这个数据给从设备,同时从设备也通过 MISO 线给主设备返回了 0X66 这个数据。这个就是 SPI 时序图。

I.MX6U ECSPI 简介:I.MX6U 自带的 SPI 外设叫做 ECSPI,全称是 Enhanced Configurable Serial Peripheral Interface,别看前面加了个“EC”就以为和标准 SPI 有啥不同的, 其实就是 SPI。 ECSPI 有 64*32 个接收FIFO(RXFIFO)和 64*32 个发送 FIFO(TXFIFO), ECSPI 特性如下:
①、全双工同步串行接口。②、可配置的主/从模式。③、四个片选信号,支持多从机。
④、发送和接收都有一个 32x64 的 FIFO。⑤、片选信号 SS/CS,时钟信号 SCLK 极性可配置。⑥、支持 DMA。
        I.MX6U 的 ECSPI 可以工作在主模式或从模式,本章我们使用主模式, I.MX6U 有 4 个ECSPI,每个 ECSPI 支持四个片选信号,也就说,如果你要使用 ECSPI 的硬件片选信号的话,一个 ECSPI 可以支持 4 个外设。如果使用软件片选的话可以使用任意的 IO。(由于迅为开发板没有接SPI的外设,这里使用正点原子开发板的电路<此历程不用6D_INT且使用软件片选>)(使用软件片选的含义是:不把引脚复用为SPI->SS0(由SPI控制器操控),而是复用为普通引脚(由驱动程序操控))

// ===>icm20608reg.h
#ifndef ICM20608_H
#define ICM20608_H

#define ICM20608G_ID			0XAF	/* ID值 */
#define ICM20608D_ID			0XAE	/* ID值 */
/* ICM20608寄存器 
 *复位后所有寄存器地址都为0,除了
 *Register 107(0X6B) Power Management 1 	= 0x40
 *Register 117(0X75) WHO_AM_I 				= 0xAF或0xAE
 */
/* 陀螺仪和加速度自测(出产时设置,用于与用户的自检输出值比较) */
#define	ICM20_SELF_TEST_X_GYRO		0x00
#define	ICM20_SELF_TEST_Y_GYRO		0x01
#define	ICM20_SELF_TEST_Z_GYRO		0x02
#define	ICM20_SELF_TEST_X_ACCEL		0x0D
#define	ICM20_SELF_TEST_Y_ACCEL		0x0E
#define	ICM20_SELF_TEST_Z_ACCEL		0x0F
/* 陀螺仪静态偏移 */
#define	ICM20_XG_OFFS_USRH			0x13
#define	ICM20_XG_OFFS_USRL			0x14
#define	ICM20_YG_OFFS_USRH			0x15
#define	ICM20_YG_OFFS_USRL			0x16
#define	ICM20_ZG_OFFS_USRH			0x17
#define	ICM20_ZG_OFFS_USRL			0x18
#define	ICM20_SMPLRT_DIV			0x19
#define	ICM20_CONFIG				0x1A
#define	ICM20_GYRO_CONFIG			0x1B
#define	ICM20_ACCEL_CONFIG			0x1C
#define	ICM20_ACCEL_CONFIG2			0x1D
#define	ICM20_LP_MODE_CFG			0x1E
#define	ICM20_ACCEL_WOM_THR			0x1F
#define	ICM20_FIFO_EN				0x23
#define	ICM20_FSYNC_INT				0x36
#define	ICM20_INT_PIN_CFG			0x37
#define	ICM20_INT_ENABLE			0x38
#define	ICM20_INT_STATUS			0x3A
/* 加速度输出 */
#define	ICM20_ACCEL_XOUT_H			0x3B
#define	ICM20_ACCEL_XOUT_L			0x3C
#define	ICM20_ACCEL_YOUT_H			0x3D
#define	ICM20_ACCEL_YOUT_L			0x3E
#define	ICM20_ACCEL_ZOUT_H			0x3F
#define	ICM20_ACCEL_ZOUT_L			0x40
/* 温度输出 */
#define	ICM20_TEMP_OUT_H			0x41
#define	ICM20_TEMP_OUT_L			0x42
/* 陀螺仪输出 */
#define	ICM20_GYRO_XOUT_H			0x43
#define	ICM20_GYRO_XOUT_L			0x44
#define	ICM20_GYRO_YOUT_H			0x45
#define	ICM20_GYRO_YOUT_L			0x46
#define	ICM20_GYRO_ZOUT_H			0x47
#define	ICM20_GYRO_ZOUT_L			0x48
#define	ICM20_SIGNAL_PATH_RESET		0x68
#define	ICM20_ACCEL_INTEL_CTRL 		0x69
#define	ICM20_USER_CTRL				0x6A
#define	ICM20_PWR_MGMT_1			0x6B
#define	ICM20_PWR_MGMT_2			0x6C
#define	ICM20_FIFO_COUNTH			0x72
#define	ICM20_FIFO_COUNTL			0x73
#define	ICM20_FIFO_R_W				0x74
#define	ICM20_WHO_AM_I 				0x75
/* 加速度静态偏移 */
#define	ICM20_XA_OFFSET_H			0x77
#define	ICM20_XA_OFFSET_L			0x78
#define	ICM20_YA_OFFSET_H			0x7A
#define	ICM20_YA_OFFSET_L			0x7B
#define	ICM20_ZA_OFFSET_H			0x7D
#define	ICM20_ZA_OFFSET_L 			0x7E
#endif

//===>icm20608.c
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <linux/spi/spi.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include "icm20608reg.h"

#define ICM20608_CNT	1
#define ICM20608_NAME	"icm20608"

struct icm20608_dev {
	dev_t devid;				/* 设备号 	 */
	struct cdev cdev;			/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;		/* 设备 	 */
	struct device_node	*nd; 	/* 设备节点 */
	int major;					/* 主设备号 */
	void *private_data;			/* 私有数据 		*/
	signed int gyro_x_adc;		/* 陀螺仪X轴原始值 	 */
	signed int gyro_y_adc;		/* 陀螺仪Y轴原始值		*/
	signed int gyro_z_adc;		/* 陀螺仪Z轴原始值 		*/
	signed int accel_x_adc;		/* 加速度计X轴原始值 	*/
	signed int accel_y_adc;		/* 加速度计Y轴原始值	*/
	signed int accel_z_adc;		/* 加速度计Z轴原始值 	*/
	signed int temp_adc;		/* 温度原始值 			*/
};

static struct icm20608_dev icm20608dev;

/*
 * @description	: 从icm20608读取多个寄存器数据
 * @param - dev:  icm20608设备
 * @param - reg:  要读取的寄存器首地址
 * @param - val:  读取到的数据
 * @param - len:  要读取的数据长度
 * @return 		: 操作结果
 */
static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
{

	int ret = -1;
	unsigned char txdata[1];
	unsigned char * rxdata;
	struct spi_message m;
	struct spi_transfer *t;
	struct spi_device *spi = (struct spi_device *)dev->private_data;
    
	t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);	/* 申请内存 */
	if(!t) {
		return -ENOMEM;
	}

	rxdata = kzalloc(sizeof(char) * len, GFP_KERNEL);	/* 申请内存 */
	if(!rxdata) {
		goto out1;
	}

	/* 一共发送len+1个字节的数据,第一个字节为
	寄存器首地址,一共要读取len个字节长度的数据,*/
	txdata[0] = reg | 0x80;		/* 写数据的时候首寄存器地址bit8要置1 */			
	t->tx_buf = txdata;			/* 要发送的数据 */
    t->rx_buf = rxdata;			/* 要读取的数据 */
	t->len = len+1;				/* t->len=发送的长度+读取的长度 */
	spi_message_init(&m);		/* 初始化spi_message */
	spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
	ret = spi_sync(spi, &m);	/* 同步发送 */
	if(ret) {
		goto out2;
	}
	
    memcpy(buf , rxdata+1, len);  /* 只需要读取的数据 */

out2:
	kfree(rxdata);					/* 释放内存 */
out1:	
	kfree(t);						/* 释放内存 */
	
	return ret;
}

/*
 * @description	: 向icm20608多个寄存器写入数据
 * @param - dev:  icm20608设备
 * @param - reg:  要写入的寄存器首地址
 * @param - val:  要写入的数据缓冲区
 * @param - len:  要写入的数据长度
 * @return 	  :   操作结果
 */
static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, u8 len)
{
	int ret = -1;
	unsigned char *txdata;
	struct spi_message m;
	struct spi_transfer *t;
	struct spi_device *spi = (struct spi_device *)dev->private_data;
	
	t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);	/* 申请内存 */
	if(!t) {
		return -ENOMEM;
	}
	
	txdata = kzalloc(sizeof(char)+len, GFP_KERNEL);
	if(!txdata) {
		goto out1;
	}
	
	/* 一共发送len+1个字节的数据,第一个字节为
	寄存器首地址,len为要写入的寄存器的集合,*/
	*txdata = reg & ~0x80;	/* 写数据的时候首寄存器地址bit8要清零 */
    memcpy(txdata+1, buf, len);	/* 把len个寄存器拷贝到txdata里,等待发送 */
	t->tx_buf = txdata;			/* 要发送的数据 */
	t->len = len+1;				/* t->len=发送的长度+读取的长度 */
	spi_message_init(&m);		/* 初始化spi_message */
	spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
	ret = spi_sync(spi, &m);	/* 同步发送 */
    if(ret) {
        goto out2;
    }
	
out2:
	kfree(txdata);				/* 释放内存 */
out1:
	kfree(t);					/* 释放内存 */
	return ret;
}

/*
 * @description	: 读取icm20608指定寄存器值,读取一个寄存器
 * @param - dev:  icm20608设备
 * @param - reg:  要读取的寄存器
 * @return 	  :   读取到的寄存器值
 */
static unsigned char icm20608_read_onereg(struct icm20608_dev *dev, u8 reg)
{
	u8 data = 0;
	icm20608_read_regs(dev, reg, &data, 1);
	return data;
}

/*
 * @description	: 向icm20608指定寄存器写入指定的值,写一个寄存器
 * @param - dev:  icm20608设备
 * @param - reg:  要写的寄存器
 * @param - data: 要写入的值
 * @return   :    无
 */	

static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value)
{
	u8 buf = value;
	icm20608_write_regs(dev, reg, &buf, 1);
}

/*
 * @description	: 读取ICM20608的数据,读取原始数据,包括三轴陀螺仪、
 * 				: 三轴加速度计和内部温度。
 * @param - dev	: ICM20608设备
 * @return 		: 无。
 */
void icm20608_readdata(struct icm20608_dev *dev)
{
	unsigned char data[14] = { 0 };
	icm20608_read_regs(dev, ICM20_ACCEL_XOUT_H, data, 14);

	dev->accel_x_adc = (signed short)((data[0] << 8) | data[1]); 
	dev->accel_y_adc = (signed short)((data[2] << 8) | data[3]); 
	dev->accel_z_adc = (signed short)((data[4] << 8) | data[5]); 
	dev->temp_adc    = (signed short)((data[6] << 8) | data[7]); 
	dev->gyro_x_adc  = (signed short)((data[8] << 8) | data[9]); 
	dev->gyro_y_adc  = (signed short)((data[10] << 8) | data[11]);
	dev->gyro_z_adc  = (signed short)((data[12] << 8) | data[13]);
}

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做pr似有ate_data的成员变量
 * 					  一般在open的时候将private_data似有向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int icm20608_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &icm20608dev; /* 设置私有数据 */
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t icm20608_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
	signed int data[7];
	long err = 0;
	struct icm20608_dev *dev = (struct icm20608_dev *)filp->private_data;

	icm20608_readdata(dev);
	data[0] = dev->gyro_x_adc;
	data[1] = dev->gyro_y_adc;
	data[2] = dev->gyro_z_adc;
	data[3] = dev->accel_x_adc;
	data[4] = dev->accel_y_adc;
	data[5] = dev->accel_z_adc;
	data[6] = dev->temp_adc;
	err = copy_to_user(buf, data, sizeof(data));
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int icm20608_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* icm20608操作函数 */
static const struct file_operations icm20608_ops = {
	.owner = THIS_MODULE,
	.open = icm20608_open,
	.read = icm20608_read,
	.release = icm20608_release,
};

/*
 * ICM20608内部寄存器初始化函数 
 * @param  	: 无
 * @return 	: 无
 */
void icm20608_reginit(void)
{
	u8 value = 0;
	
	icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x80);
	mdelay(50);
	icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x01);
	mdelay(50);

	value = icm20608_read_onereg(&icm20608dev, ICM20_WHO_AM_I);
	printk("ICM20608 ID = %#X\r\n", value);	

	icm20608_write_onereg(&icm20608dev, ICM20_SMPLRT_DIV, 0x00); 	/* 输出速率是内部采样率					*/
	icm20608_write_onereg(&icm20608dev, ICM20_GYRO_CONFIG, 0x18); 	/* 陀螺仪±2000dps量程 				*/
	icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG, 0x18); 	/* 加速度计±16G量程 					*/
	icm20608_write_onereg(&icm20608dev, ICM20_CONFIG, 0x04); 		/* 陀螺仪低通滤波BW=20Hz 				*/
	icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG2, 0x04); /* 加速度计低通滤波BW=21.2Hz 			*/
	icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_2, 0x00); 	/* 打开加速度计和陀螺仪所有轴 				*/
	icm20608_write_onereg(&icm20608dev, ICM20_LP_MODE_CFG, 0x00); 	/* 关闭低功耗 						*/
	icm20608_write_onereg(&icm20608dev, ICM20_FIFO_EN, 0x00);		/* 关闭FIFO						*/
}

 /*
  * @description     : spi驱动的probe函数,当驱动与
  *                    设备匹配以后此函数就会执行
  * @param - client  : i2c设备
  * @param - id      : i2c设备ID
  * 
  */	
static int icm20608_probe(struct spi_device *spi)
{
	/* 1、构建设备号 */
	if (icm20608dev.major) {
		icm20608dev.devid = MKDEV(icm20608dev.major, 0);
		register_chrdev_region(icm20608dev.devid, ICM20608_CNT, ICM20608_NAME);
	} else {
		alloc_chrdev_region(&icm20608dev.devid, 0, ICM20608_CNT, ICM20608_NAME);
		icm20608dev.major = MAJOR(icm20608dev.devid);
	}

	/* 2、注册设备 */
	cdev_init(&icm20608dev.cdev, &icm20608_ops);
	cdev_add(&icm20608dev.cdev, icm20608dev.devid, ICM20608_CNT);

	/* 3、创建类 */
	icm20608dev.class = class_create(THIS_MODULE, ICM20608_NAME);
	if (IS_ERR(icm20608dev.class)) {
		return PTR_ERR(icm20608dev.class);
	}

	/* 4、创建设备 */
	icm20608dev.device = device_create(icm20608dev.class, NULL, icm20608dev.devid, NULL, ICM20608_NAME);
	if (IS_ERR(icm20608dev.device)) {
		return PTR_ERR(icm20608dev.device);
	}

	/*初始化spi_device */
	spi->mode = SPI_MODE_0;	/*MODE0,CPOL=0,CPHA=0*/
	spi_setup(spi);
	icm20608dev.private_data = spi; /* 设置私有数据 */

	/* 初始化ICM20608内部寄存器 */
	icm20608_reginit();		
	return 0;
}

/*
 * @description     : i2c驱动的remove函数,移除i2c驱动的时候此函数会执行
 * @param - client 	: i2c设备
 * @return          : 0,成功;其他负值,失败
 */
static int icm20608_remove(struct spi_device *spi)
{
	/* 删除设备 */
	cdev_del(&icm20608dev.cdev);
	unregister_chrdev_region(icm20608dev.devid, ICM20608_CNT);

	/* 注销掉类和设备 */
	device_destroy(icm20608dev.class, icm20608dev.devid);
	class_destroy(icm20608dev.class);
	return 0;
}

/* 传统匹配方式ID列表 */
static const struct spi_device_id icm20608_id[] = {
	{"alientek-evk,icm20608", 0},  
	{}
};

/* 设备树匹配列表 */
static const struct of_device_id icm20608_of_match[] = {
	{ .compatible = "alientek-evk,icm20608" },
	{ /* Sentinel */ }
};

/* SPI驱动结构体 */	
static struct spi_driver icm20608_driver = {
	.probe = icm20608_probe,
	.remove = icm20608_remove,
	.driver = {
			.owner = THIS_MODULE,
		   	.name = "icm20608",
		   	.of_match_table = icm20608_of_match, 
		   },
	.id_table = icm20608_id,
};
		   
/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init icm20608_init(void)
{
	return spi_register_driver(&icm20608_driver);
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit icm20608_exit(void)
{
	spi_unregister_driver(&icm20608_driver);
}
module_init(icm20608_init);
module_exit(icm20608_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");

//===>icm20608App.c
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd;
	char *filename;
	signed int databuf[7];
	unsigned char data[14];
	signed int gyro_x_adc, gyro_y_adc, gyro_z_adc;
	signed int accel_x_adc, accel_y_adc, accel_z_adc;
	signed int temp_adc;

	float gyro_x_act, gyro_y_act, gyro_z_act;
	float accel_x_act, accel_y_act, accel_z_act;
	float temp_act;

	int ret = 0;

	if (argc != 2) {
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];
	fd = open(filename, O_RDWR);
	if(fd < 0) {
		printf("can't open file %s\r\n", filename);
		return -1;
	}

	while (1) {
		ret = read(fd, databuf, sizeof(databuf));
		if(ret == 0) { 			/* 数据读取成功 */
			gyro_x_adc = databuf[0];
			gyro_y_adc = databuf[1];
			gyro_z_adc = databuf[2];
			accel_x_adc = databuf[3];
			accel_y_adc = databuf[4];
			accel_z_adc = databuf[5];
			temp_adc = databuf[6];

			/* 计算实际值 */
			gyro_x_act = (float)(gyro_x_adc)  / 16.4;
			gyro_y_act = (float)(gyro_y_adc)  / 16.4;
			gyro_z_act = (float)(gyro_z_adc)  / 16.4;
			accel_x_act = (float)(accel_x_adc) / 2048;
			accel_y_act = (float)(accel_y_adc) / 2048;
			accel_z_act = (float)(accel_z_adc) / 2048;
			temp_act = ((float)(temp_adc) - 25 ) / 326.8 + 25;


			printf("\r\n原始值:\r\n");
			printf("gx = %d, gy = %d, gz = %d\r\n", gyro_x_adc, gyro_y_adc, gyro_z_adc);
			printf("ax = %d, ay = %d, az = %d\r\n", accel_x_adc, accel_y_adc, accel_z_adc);
			printf("temp = %d\r\n", temp_adc);
			printf("实际值:");
			printf("act gx = %.2f°/S, act gy = %.2f°/S, act gz = %.2f°/S\r\n", gyro_x_act, gyro_y_act, gyro_z_act);
			printf("act ax = %.2fg, act ay = %.2fg, act az = %.2fg\r\n", accel_x_act, accel_y_act, accel_z_act);
			printf("act temp = %.2f°C\r\n", temp_act);
		}
		usleep(100000); /*100ms */
	}
	close(fd);	/* 关闭文件 */	
	return 0;
}

5.23、Linux驱动之串口驱动实验

测试:echo -e "hello world\r" > /dev/ttymxc2

5.24、Linux驱动之多点电容触摸屏实验

5.25、Linux驱动之音频驱动实验

5.26、Linux驱动之USB驱动实验

5.26.1、Linux 内核自带HOST实验

        USB HOST 试验也就是开发板做 USB 主机,然后外接 USB设备,比如 USB 鼠标键盘、 USB 转 TTL 串口线、U 盘等设备。 Linux 内核已经集成了大量的USB 设备驱动,尤其是我们常见的 USB 鼠标键盘、U 盘等,本节我们就来学习一下如何使能Linux 内核常见的 USB 设备驱动。

        HID人体学接口设备(Human Interface Device),是一类设备的定义(也叫协议)。HID设备有基于USB,IIC的等等,也被叫做USB-HID、IIC-HID。USB 鼠标键盘就属于 HID 设备,内核已经集成了相应的驱动, NXP 官方提供的 linux 内核默认已经使能了 USB 鼠标键盘驱。(手动配置参考=>1)

        SCSI小型计算机系统接口(Small Computer System Interface),是一类设备的定义(也叫协议),是一种用于计算机及其周边设备之间(硬盘、软驱、光驱、打印机、扫描仪等)系统级接口的独立处理器标准。U盘就是属于SCSI设备,

=>1、使能USB-HID驱动

/*配置1:*/
-> Device Drivers
    -> HID support
        -> <*> Generic HID driver //使能通用 HID 驱动

/*配置2:*/
-> Device Drivers
    -> HID support
        -> USB HID support
            -> <*> USB HID transport layer

=>2、使能U盘驱动

/*配置1:*/
-> Device Drivers
    -> SCSI device support
        -> <*> SCSI disk support //SCSI接口硬盘支持
/*配置2:*/
-> Device Drivers
    -> [*] USB support (USB_SUPPORT [=y])
        -> <*> Support for Host-side USB
        -> <*> USB Mass Storage support //USB 大容量存储设备

5.26.2、Linux 内核自带USB-OTG实验

......

5.27、Linux驱动之块设备驱动

......

5.28、Linux驱动之网络驱动实验

......

5.29、Linux驱动之WIFI驱动实验

......

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值