1、新建一个文件夹并把下载好的Ubuntu20镜像放在里面
如果想二次修改密码,使用命令:
sudo passwd root //修改root用户密码
sudo passwd haut //修改haut用户密码
sudo passwd 用户名 //.......
工具能实现界面自动分辨率调整、主机与虚拟机之间拖拉文件等功能,是虚拟机必不可少的软件目前新版本的系统镜像都有自带的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
如果想手动更新系统,可以在终端中执行如下命令:
sudo apt update //获取最新版本的软件包列表(仅是列表)
sudo apt list --upgradable //列出哪些软件可以被更新(哪些软件出了新版本)
sudo apt upgrade //下载需要更新的软件包,并更新所有可以被更新的软件
我们需要安装的插件有下面几个:
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 自动补全插件,强烈推荐,谁用谁知道!
vs code的相关配置繁琐而又复杂,相关配置在另一个文章中有详细介绍,这里不再赘述。
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
图中的相关命令如下:
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]
学习嵌入式linux最好选择一款开发板来学习,这样能更好的接触底层硬件的工作原理,目前市面上教学平台的 开发板的CPU型号有IMX6ULL(单核A7+32bit)、STM32MP157(双核A7+单核M4+32bits)、RK3399(双A72大核+四A53小核+64bit)等。前两款适合底层驱动学习,后者适合上层应用开发。(=>这里我选择imx6ull系列的开发板作为linux的学习平台<=)
NXP的IMX6ULL系列芯片是一款基于ARM Cortex-A7内核的低功耗高性能且低成本的应用处理器,处理器的内部功能框图和CPU丝印命名规则如下:
(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的前几个区即使未经过分区,内核驱动也是可以识别的,但用户区若不分区则识别不了。
在Ubuntu20的虚拟机上配置环境,我创建的用户名为haut,在haut的用户目录下创建一个名为itop_imx6ull的目录,之后所有的开发文件和软件都放在此目录下:
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服务
我们将开发板的文件系统放在 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的配置。
(1)使用uboot配置开发板的网络参数:
setenv ipaddr 192.168.1.115 # 开发板网卡IP地址
setenv ethaddr 08:07:03:A0:03:02 # 开发板网卡MAC地址
setenv gatewayip 192.168.1.1 # 局域网网关地址
setenv netmask 255.255.255.0 # 网络子网掩码
setenv serverip 192.168.1.116 # 服务器IP地址
saveenv # 保存环境变量
########################### 以上是注释版、完整的命令如下 ############################
setenv ipaddr 192.168.1.115
setenv ethaddr 08:07:03:A0:03:02
setenv gatewayip 192.168.1.1
setenv netmask 255.255.255.0
setenv serverip 192.168.1.116
saveenv
(2)设置传递给内核的参数bootargs:
# =>不同启动方式的bootargs配置不同,以nfs网络文件系统的为例:
setenv bootargs 'console=ttymxc0,115200 # 表示终端为ttymxc0,串口波特率为115200
root=/dev/nfs # 告诉内核以nfs启动
rw # 文件系统操作权限
nfsroot=192.168.1.116:/home/haut/itop_imx6ull/nfs_rootfs # nfs的根目录的绝对地址
ip=192.168.1.115:192.168.1.116:192.168.1.1:255.255.255.0::eth0:off' # 本地地址:服务器地址:网关:子网掩码::eth0:off
saveenv # 保存设置的环境变量
########################### 以上是注释版、完整的命令如下 ############################
setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.1.116:/home/haut/itop_imx6ull/nfs_rootfs ip=192.168.1.115:192.168.1.116: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中===>校验内核===>传递参数===>跳转执行内核
1、 arm 表示这是编译 arm 架构代码的编译器。
2、 linux 表示运行在 linux 环境下。
3、 gnueabihf 表示嵌入式二进制接口。
4、 gcc 表示是 gcc 工具。
(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 种运行模式。
(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 => 备份程序状态寄存器
(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 @ 将寄存器列表存入栈中。
POP @ 从栈中恢复寄存器列表。
示例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) 位置相关/无关码
<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指令多用于代码重定位,用于代码运行过程中,获取某个标号当前所在的地址(运行地址)。
(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的倍数的位置
(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 = 数据段的首加载地址首地址
}
(6) __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
/* 定义:变量名+变量值 */
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 :
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 :
87810018: 00001a2b andeq r1, r0, fp, lsr #20
Disassembly of section .__vec:
87810020 :
87810020: 000000aa andeq r0, r0, sl, lsr #1
...
87810030 :
87810030: 000000bb strheq r0, [r0], -fp
...
Disassembly of section .comment:
................................
# 说明1:因为没有链接过程,所以所有的段都是从0开始的。
# 说明2:0长数组不是c语言的里面东西,它是GNC C的扩展。
(7) 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" 这句,它是指明了此代码所处的段,而不是由编译器默认分配的)
(8) Uboot2016-nxp编译控制分析
在linux内核里面,采用menuconfig机制来配置哪些文件将被编译,其原理就利用图形界面产生环境变量,所有的环境变量配置结果都将被写入到主目录的.config文件中。如下图所示:
这里以NXP官方提供的Uboot源码为例,这是NXP针对IMX6ULL芯片做过适配的。
(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
(1)配置编译器
(2)添加开发板
(3)配置、编译内核
设备树相关:(dtc是设备树的编译器)
.dts # 设备树源文件
.dtsi # 设备树头文件
.dtb # 设备树编译后的二进制文件
设备树编译:make dtbs
(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/
对于一个新的模块,使用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 类型的设备号。
(2)、编写驱动源文件 chrdevbase.c
#include
#include
#include
#include
#include
#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() 函数注册设备时,要传入的主设备号是没有被使用的,而且一旦注册成功,主设备号下面的所有次设备号都会被此设备占用,最后还需要自己创建设备节点。后续的新模板会使用其它的函数,不仅可以申请设备号,而且还充分利用了次设备号。
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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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章即可
使用 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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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;
}
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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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
前言:本章节看似正常,实际有一个大坑,就是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_XXX、XXX_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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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()函数。
蜂鸣器实验和LED实验一样,也是通过控制一个IO口输出高低电平来操纵器件,这里不再赘述。迅为itop-imx6ull开发板的蜂鸣器电路如下,imx6ull这款CPU有两组IO,一组由IOMUXC控制器控制(LED使用的这组里面的IO),一组由IOMUXC_SVNS控制器控制(BEEP使用的这组里面的IO),和LED实验不同的是,这次的pinctrl_beep节点要添加在iomuxc_svns下,其它的几乎相同:
<1>、添加设备树节点
<2>、驱动程序:beep.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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
Linux 系统是个多任务操作系统,会存在多个任务同时访问同一片内存区域,这些任务可能会相互覆盖这段内存中的数据,造成内存数据混乱。针对这个问题必须要做处理,严重的话可能会导致系统崩溃。现在的 Linux 系统并发产生的原因很复杂,总结一下有下面几个主要原因:
①、多线程并发访问, Linux 是多任务(线程)的系统,所以多线程访问是最基本的原因。
②、抢占式并发访问,从 2.6 版本内核开始, Linux 内核支持抢占,也就是说调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。
③、中断程序并发访问,这个无需多说,学过 STM32 的同学应该知道,硬件中断的权利可是很大的。
④、 SMP(多核)核间并发访问,现在 ARM 架构的多核 SOC 很常见,多核 CPU 存在核间并发访问
当我们发现驱动程序中存在并发和竞争的时候一定做相应的处理,不然可能会产生非常严重问题,接下来我们依次来学习一下Linux 内核提供的几种并发和竞争的处理方法。
本实验是在5.05章的LED实验上进行更改,使用原子变量实现驱动设备在同一时刻只允许一个App使用,设备树和5.05章的一样,不需要做修改。
<1>、驱动程序:atomic.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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 //再次启动一个应用程序
上一节我们使用原子变量实现了一次只能有一个应用程序访问 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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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 //再次启动一个应用程序
自旋锁是一直傻乎乎的在那里“自旋”等待,所以自旋锁拿到之后不能拥有太长时间,不然会导致别的程序获取此锁时一直循环等待,故使用时要保证自旋锁保护的临界区要尽可能的短。相比于自旋锁,信号量可以使线程进入休眠状态,而不是让它循环等待,故使用信号量会提高处理器的使用效率,但是,信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态以后会切换线程,切换线程就会有开销。总结一下信号量的特点:
①、因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。
②、因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
③、如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。
信号量有一个信号量值,相当于一个房子有 10 把钥匙,这 10 把钥匙就相当于信号量值为10。因此,可以通过信号量来控制访问共享资源的访问数量,如果要想进房间,那就要先获取一把钥匙,信号量值减 1,直到 10 把钥匙都被拿走,信号量值为 0,这个时候就不允许任何人进入房间了,因为没钥匙了。如果有人从房间出来,那他要归还他所持有的那把钥匙,信号量值加 1,此时有 1 把钥匙了,那么可以允许进去一个人。相当于通过信号量控制访问资源的线程数,在初始化的时候将信号量值设置的大于 1,那么这个信号量就是计数型信号量,计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源。如果要互斥的访问共享资源那么信号量的值就不能大于 1,此时的信号量就是一个二值信号量。
<1>、驱动程序:semaphore.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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 //再次启动一个应用程序
将信号量的值设置为 1 就可以使用信号量进行互斥访问了,虽然可以通过信号量实现互斥,但是 Linux 提供了一个比信号量更专业的机制来进行互斥,它就是互斥体—mutex。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体。在我们编写 Linux 驱动的时候遇到需要互斥访问的地方建议使用 mutex。在使用 mutex 之前要先定义一个 mutex 变量。在使用 mutex 的时候要注意如下几点:
①、mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
②、和信号量一样, mutex 保护的临界区可以调用引起阻塞的 API 函数。
③、因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁。
<1>、驱动程序:mutex.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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 //再次启动一个应用程序
在前面我们都是使用的 GPIO 输出功能,还没有用过 GPIO 输入功能,本章我们就来学习一下如果在 Linux 下编写 GPIO 输入驱动程序,同时利用原子操作来对按键值进行保护(注意此次实验对驱动的并发访问没有处理,可参考上一章进行添加)。其次,前几章的实验也没有对GPIO的使用做并发处理,其实gpio子系统是支持IO互斥的,就是使用IO是要先申请再使用,当某个IO被申请成功且没有执行释放的条件下,其它程序是无法成功申请此IO的(注意:这种申请=>使用=>释放的顺序原则仅在所有程序都以此方式书写时有互斥作用,如果别的程序不遵守这个顺序,上来就直接使用IO也是可以的,这种情况下,互斥也就失去了作用。只能说gpio子系统希望我们按这个顺序来,至于尊不遵守,由驱动开发人员决定)。KEY按键的电路图如下:
<2>、驱动程序:key.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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
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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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
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>、添加设备树节点
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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; iirqkey[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; iirqkey[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; iirqkey[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; ikeytimer);
}
return 0;
fail_irq:
for(i=0; iirqkey[i].irqnum, dev);
fail_gpio:
for(i=0; iirqkey[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; iirqkey[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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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; iirqkey[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; iirqkey[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; iirqkey[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; iirqkey[i].irqnum, dev);
fail_gpio:
for(i=0; iirqkey[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; iirqkey[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");
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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; iirqkey[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; iirqkey[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; iirqkey[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; iirqkey[i].irqnum, dev);
fail_gpio:
for(i=0; iirqkey[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; iirqkey[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");
阻塞和非阻塞 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 函数。
等待队列是用链表实现的,链表的组成包括表头和表项,所以等待队列包括等待队列头和等待队列项,等待队列项对应的是某个进程。所以使用方法是:创建等待队列头=>初始化等待队列头=>创建等待队列项=>把等待队列项加入到等待队列中。最后在适当的条件下通过唤醒函数唤醒队列中的项就可以了。等待队列头定义后初始化就行了,等待队列项可以自己创建(创建好后还需要自己手动添加项到表中),也可以使用等待事件函数(创建项并添加到表中)。
<1>、使用5.11章的设备树
<2>、驱动程序:blockio.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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使用率很高
用户程序调用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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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 //运行程序
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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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 //运行程序
驱动的分离:如下图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描述设备信了
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
input 就是输入的意思,因此 input 子系统就是管理输入的子系统,和 pinctrl、 gpio 子系统一样,都是 Linux 内核针对某一类设备而创建的框架。比如按键输入、键盘、鼠标、触摸屏等等这些都属于输入设备,不同的输入设备所代表的含义不同,按键和键盘就是代表按键信息,鼠标和触摸屏代表坐标信息,因此在应用层的处理就不同,对于驱动编写者而言不需要去关心应用层的事情,我们只需要按照要求上报这些输入事件即可。为此 input 子系统分为 input 驱动层、 input 核心层、 input 事件处理层,最终给用户空间提供可访问的设备节点, input 子系统框架如图所示:
程序启动时,可以指定其使用哪个标准输入输出设备(即终端),也可以不指定(不涉及输入输出时就不用指定)(子进程默认使用父进程的终端)。系统使用的的标准输入输出设备叫做控制台,其它程序一般使用终端(也可以使用控制台),无论是控制台还是终端,他们都是标准输入输出设备。在文件系统下,体现为/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
#include
#include
#include
#include
#include
#include
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;
}
}
}
date -s "2022-09-15 13:06:06" //设置当前系统时间
hwclock -w //将当前系统时间写入到 RTC 里面
// ===>文件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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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 = ® /* 读取的首地址 */
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
#include
#include
#include
#include
/*
* @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;
}
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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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
#include
#include
#include
#include
/*
* @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;
}
测试:echo -e "hello world\r" > /dev/ttymxc2
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 大容量存储设备
......