一、与硬件进行通信的原理
每个外设都是通过读写它芯片上的寄存器来控制。大部分时间一个设备有几个寄存器, 并且是在连续地址空间上存取它们,,这个空间在内存地址空间或者在 I/O 地址空间。在硬件级别上, 内存地址空间区域和 I/O地址空间区域没有概念上的区别:它们都是通过在地址总线和控制总线上发出电信号来存取(即, 读写信号)并且读自或者写到数据总线。
二、使用 I/O 地址空间与硬件进行通信的内核API介绍
1、I/O 端口分配
#include <linux/ioport.h>
struct resource *request_region(unsigned long first, unsigned long n, const char *name);
这个函数告诉内核, 你要使用 n 个端口, 从 first 开始,name 参数是你的设备的名字,它会出现在/proc/ioports 中。可以通过/proc/ioports接口查看系统中I/O端口的分配情况。
还有一个函数可以允许你的驱动来检查一个给定的 I/O 端口组是否可用:
int check_region(unsigned long first, unsigned long n);
2、I/O端口释放
当你用完一组 I/O 端口(在模块卸载时, 也许), 应当返回它们给系统, 使用:
void release_region(unsigned long start, unsigned long n);
3、单次操作I/O端口
在硬件请求了在它的活动中需要使用的 I/O 端口范围之后, 驱动必须读且/或写到这些端口。为此,大部分硬件区别8-位, 16-位, 和 32-位端口。
- unsigned inb(unsigned port);
- void outb(unsigned char byte, unsigned port);
读或写字节端口( 8 位宽 )。 port 参数在某些平台定义为 unsigned long 以及在其他上的定义为unsigned short.
- unsigned inw(unsigned port);
- void outw(unsigned short word, unsigned port);
这些函数存取 16-位 端口( 一个字宽 );
- unsigned inl(unsigned port);
- void outl(unsigned longword, unsigned port);
这些函数存取 32-位 端口. longword 声明为或者 unsigned long 或者 unsigned int, 根据平台
4、重复操作I/O端口
- void insb(unsigned port, void *addr, unsigned long count);
- void outb(unsigned port, void *addr, unsigned long count);
读或写从内存地址 addr 开始的 count 字节. 数据读自或者写入单个 port 端口.
- void insw(unsigned port, void *addr, unsigned long count);
- void outw(unsigned port, void *addr, unsigned long count);
读或写 16-位 值到一个16-位 端口
- •void insl(unsigned port, void *addr, unsigned long count);
- void outsl(unsigned port, void *addr, unsigned long count);
读或写 32-位 值到一个32-位 端口
三、使用 I/O 内存地址空间与硬件进行通信的内核API介绍
I/O 内存实际位置是外设控制器芯片上的物理寄存器,其地址空间与普通内存进行统一编址,位于0--4G的某个位置(如果是32位机的话)。程序对I/O内存进行读写操作就等于是对外设控制器芯片上的物理寄存器进行读写操作,从而就完成了对外设的驱动控制。ARM体系结构下,只有I/O内存,没有二所述的I/O端口,所以对外设的访问均使用I/O内存,而不是I/O端口。而在X86体系结构下存在I/O端口,所以在X86下可以访问I/O端口,但由于I/O端口并不是与内存进行统一编址的(它独立编址),所以X86下会提供一套有别于访存指令的I/O端口访问指令。
1、I/O 内存分配和释放
- I/O 内存区必须在使用前分配。分配内存区的接口是( 在 <linux/ioport.h> 定义):
- struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);
这个函数分配一个 len 字节的内存区, 从 start 开始。如果一切顺利, 一个非NULL 指针返回;否则返回值是 NULL。name是申请到的区域名称,会在/proc/iomem 中列出。可以通过/proc/iomem接口查看系统中I/O内存的分配情况。
- 当内存区不再需要时,应当释放:
- void release_mem_region(unsigned long start, unsigned long len);
2、I/O内存映射
I/O内存分配和释放函数使用的是物理地址,而在linux操作系统下都必须使用虚拟地址才能对内存进行合法访问,因此在使用申请到的I/O内存区之前,必须对该区域进行实地址到虚地址的映射成功后,才能使用虚地址对该区域进行访问。
- #include <asm/io.h>
- void *ioremap(unsigned long phys_addr, unsigned long size);
将长度为size,起始地址为phys_addr的物理内存地址映射到虚拟地址,虚拟地址的首地址作为返回值返回。其本质是在MMU的页表中新建条目
- void *ioremap_nocache(unsigned long phys_addr, unsigned long size);
同ioremap,区别在于不允许映射的内存区域的内容可在CPU的cache中缓存。其本质是在新建MMU的页表条目时,在该条目的相应域指定不可缓存。但由于对外设寄存器的映射都不应该允许缓存,所以内核对这2个API的实现是一样的。
- void iounmap(void * addr);
取消ioremap建立的虚实地址映射。其本质是在MMU的页表中删除条目
3、简单存取I/O内存
从I/O内存地址addr处读取字节、半字、字
- unsigned int ioread8(void *addr);
- unsigned int ioread16(void *addr);
- unsigned int ioread32(void *addr);
向I/O内存地址addr处写入字节、半字、字
- void iowrite8(u8 value, void *addr);
- void iowrite16(u16 value, void *addr);
- void iowrite32(u32 value, void *addr);
4、重复存取I/O内存
- void ioread8_rep(void *addr, void *buf, unsigned long count);
- void ioread16_rep(void *addr, void *buf, unsigned long count);
- void ioread32_rep(void *addr, void *buf, unsigned long count);
- void iowrite8_rep(void *addr, const void *buf, unsigned long count);
- void iowrite16_rep(void *addr, const void *buf, unsigned long count);
- void iowrite32_rep(void *addr, const void *buf, unsigned long count);
5、存取I/O内存的老接口
我们在编写驱动时,应该使用3、4介绍的新API,但由于内核中有很多以前编写的驱动,它们使用的是老API。我们需要能读懂老驱动,因此在这里也介绍一下老的API。能猜出它们各自与哪些新API对应吧!
作业:请写出老的API与新的API的对应关系
- unsigned readb(address);
- unsigned readw(address);
- unsigned readl(address);
- void writeb(unsigned value, address);
- void writew(unsigned value, address);
- void writel(unsigned value, address);
- void __raw_writel(unsigned value, address);
四、通过I/O内存驱动硬件的实战——LED灯驱动
在ARM体系结构与编程课程中,我们已经学习了如何在裸机(实地址)下,驱动(点亮、熄灭)LED灯,现在就来学习如何在linux操作系统下驱动LED灯。先单击下载led灯的驱动源码以及测试程序,然后将驱动和测试程序进行编译。
1、体验LED驱动
# mount -t nfs -o nolock 192.168.2.11:/work /mnt
# cd /mnt/studydriver/ledsdriver/
# insmod leds.ko
leds initialized
此时会看到4个led灯全部被点亮
# mknod /dev/leds c 400 0
# ./leds_test 3 off
leds conditon = 0x8
此时会看到第4个灯熄灭,其它3个灯仍然点亮。如果灯亮为0,灯灭为1的话,此时的状况为0b1000=0x8
# ./leds_test 2 off
leds conditon = 0xc
# ./leds_test 3 on
leds conditon = 0x4
# rmmod leds
leds driver unloaded
此时会看到4个led灯全部熄灭
2、led灯驱动程序分析
1)、设备初始化
133-142申请设备号;146-152为led灯的自定义设备结构体分配空间并清0;153注册字符设备;155申请I/O内存 0x56000010-0x5600001C,这个区域正是控制led等的GPIO控制器的寄存器物理地址;159映射该I/O内存,得到的虚拟地址virtaddr就可被驱动程序用来读写该I/O内存,从而实现驱动led灯;165行可被分解为166-171行,其目的是初始化GPBCON,将GPB5-8设置为输出管脚(详情参见ARM体系结构与编程的相关文章);174行可被分解为175-178行,其目的是设置GPBDAT的5-8bit为0,以点亮4个led灯(详情参见ARM体系结构与编程的相关文章);184-189则是在进行错误的善后处理工作,以使驱动更加健壮。顺便提一句,看到goto语句在驱动中是用来干什么的了吧!
作业:请说出你对goto在驱动中的用法这个问题的看法。
特别说明:有的人认为由于物理内存0x56000010-0x5600001C实际已经存在,在ARM裸机编程中可以直接使用,因此现在只要进行虚实地址映射后就可使用,所以155行申请I/O内存的API没有任何意义,可以不要。这种看法是完全不对的,哈哈,个人认为持这种看法的人是典型的自由主义者。我来打个比喻。假定物理存在1座房子(相当于物理内存0x56000010-0x5600001C),你(相当于驱动)在不向房管局(相当于操作系统)申请(相当于调用155行的API)并获得批准(相当于该API返回非NULL)的情况下,可以住进去吗?当然可以!但是你不要忘了,此时房管局并不知道你住了该房子,在房管局的信息系统中显示的是该房子没有住人,所以当我(相当于另一个驱动)向房管局提出申请要住你现在住的房子的时候,会得到批准,于是我就会与你分享同一间房子(相当于2个驱动同时操控同一个硬件),这是不允许出现的情况。难道你愿意与他人分享房屋吗?小子,出现这样的情况,归根结底是你太自由主义化了,不遵守规则,总有一天你会倒霉的!所以,编程的时候,一定要调用155行的API向操作系统申请该I/O内存,并在操作系统同意的情况下(该API返回非NULL),才能进行后续操作,映射和使用该I/O内存区。
随便说一下,155行的API本质上执行的操作是:遍历操作系统的一个内部链表(称它为I/O内存链表吧),查看申请的内存是否已经分配。如果是,直接返回NULL。反之,生成1个代表该内存的节点(含有内存地址范围、这个范围的名字等信息)并将它链入I/O内存链表,最后返回非NULL(应该就是节点的地址)。我们cat /proc/iomem其实就是在遍历I/O内存链表,并显示每个节点的相关信息。
37 #define LEDS_MAJOR 400
40 #define GPBCON 0x56000010
42 struct leds_dev {
43
44
45 };
47 static struct leds_dev *leds_devp;
48 static int leds_major = 400;
49 static void *virtaddr;
130 static int leds_init(void)
131 {
132
133
137
138
139
140
141
142
146
147
148
149
150
152
153
155
157
158
159
161
162
165
166
174
175
182
184 fail_ioremap:
185
186 fail_request_mem_region:
187
188 fail_malloc:
189
190
191 }
206 module_init(leds_init);
2)、驱动(点亮或熄灭)LED灯
当应用(测试)程序(leds_test.c)要点亮第4个led灯时,将会执行:
32
其实就是执行ioctl(fd, 0, 3),表示要驱动执行的命令是0号命令(即:点亮LED灯),传给该命令的参数是3(即:第4个led灯)。注:某个编号的命令代表什么命令,以及传给命令的参数代表什么含义,一般而言是由驱动编写者在编写驱动时予以确定,并编写1个头文件,在其中定义将会在ioctl的第2个及后续参数使用的各种宏以及对这些宏的说明。最后将该头文件提供给应用程序编写者使用。
ioctl的执行最终将导致操作系统调用驱动的leds_fops.ioctl函数(即:leds_ioctl函数。参见leds.c的第114行),并将0,3这两个参数原封不动地传给该函数。leds_ioctl函数将会检查传入的2个参数是否合法,检查不通过将向操作系统返回对应的错误代码(94、105行),操作系统将向应用程序返回-1,并将errno设置为错误代码;检查通过则将以得到的参数去执行指定的命令(98、102行)。
90 static int leds_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)
91 {
93
94
95
96
98
99
100
102
103
104
105
106
107
108 }
98
这样一来,执行的就是ledon(3);
ledon函数的76行点亮第4个灯;78行修改led灯的自定义设备结构体中的value字段,以反映4个led灯最新的亮灭状况,供将来应用程序读取。
74 static void ledon(unsigned long arg)
75 {
76
78
80
81 }
熄灭led灯的程序与点亮led灯类似。你不会懒到还要我再给你分析一遍吧!
至于82和89行是什么意思,就留待后文讲解吧。如果你能自行钻研出来的话,我会很高兴地奖励你口香糖。
82 EXPORT_SYMBOL(ledon);
89 EXPORT_SYMBOL(ledoff);
3)、获得led灯的亮灭情况
这是由驱动的leds_read函数完成的。如果你还坚持要我为你讲解leds_read函数,而不是自己解决的话,我就准备把你踢出教室。
作业:请仿照2. 1)那样,简单分析一下leds_read函数的工作。指出在并发控制方面这个驱动存在什么不足,并设法展示出这个不足(可修改源代码),最后修正这个不足。
3、完善该驱动
当我们将leds驱动从内核中卸载后,再加载的时候必须手工建立设备文件/dev/leds。一次次地重复低级劳动令我到了崩溃的边缘,你的感觉如何呢?如果你也有同感的话,试试ledsv2.ko吧。哈哈,喜从天降。想知道怎么实现的吗?
其实,so easy。参见“详解制作根文件系统”一文可知,只要我们在/sys目录下导出了有关设备的基本信息(主要就是设备号),udev就可以为我们自动在/dev目录下创建正确的设备文件。在/sys目录下导出了有关设备的基本信息是谁的责任呢?当然是驱动。
# ls /sys/class
graphics
i2c-adapter
input
mem
# insmod ledsv2.ko
leds initialized
# ls /sys/class
graphics
i2c-adapter
input
leds_class
# ls /sys/class/leds_class/
leds
# ls /sys/class/leds_class/leds
dev
# cat /sys/class/leds_class/leds/dev
400:0
下面展示了在ledsv2.c中,与leds.c主要的不同:
177行将在/sys目录下导出一个类leds_class;180行将在/sys目录下导出属于leds_class类的设备leds的基本信息,其中就包含设备名称“leds”,设备号devno。
这之后,udev就会自动为我们在/dev目录下创建设备文件leds了,并将它与正确设备号相关联。
232行在leds_class类下删除设备号为devno的设备;233行删除设备类leds_class。
28 #include <linux/device.h>
56 static struct class *leds_class;
152 static int leds_init(void)
153 {
177
178
179
180
220 }
222 void leds_cleanup(void)
223 {
232
233
235 }