Linux驱动设计——内存与IO访问

名词解释

内存空间与IO空间

内存空间是计算机系统里面非系统内存区域的地址空间,现在的通用X86体系提供32位地址,寻址4G字节的内存空间,但一般的计算机只安装256M字节或者更少的内存,剩下的高位内存就被用于PCI或者AGP及系统桥设备的使用上面,主机可以像访问系统内存一样访问这些高端内存,这样对于扩展的设备有更大的空间。

Linux用户空间与内核空间

IO空间是X86系统上面的专用空间,现在的IO空间大小是64K字节,从0x0000到0xffff,可以供设备使用,比如南桥很多的设备就是挂在IO空间上,很多的PCI设备也使用IO空间,IO空间寻址使用专门的IO命令来完成。

配置空间是即插即用设备的广义描述,一般的配置空间指的是PCI设备或者PCI桥设备的配置空间,在配置空间里,一般的PCI设备的配置空间是256字节,但很多桥设备都是用扩展的配置空间,比如系统桥空间可以达到1k字节。配置空间为设备提供其配置信息,比如设备的IO基地址,内存基地址和中断号等信息。这些信息由BIOS或者操作系统写入,一般只有驱动程序才会访问配置空间。

在X86 处理器中存在着I/O 空间的概念,I/O 空间是相对于内存空间而言的,它通过特定的指令in、out 来访问。端口号标识了外设的寄存器地址。

目前,大多数嵌入式微控制器如ARM、PowerPC 等中并不提供I/O 空间,而仅存在内存空间。内存空间可以直接通过地址、指针来访问,程序和程序运行中使用的变量和其他数据都存在于内存空间中。内存地址可以直接由C 语言指针操作。

例如在186 处理器中执行如下代码:

unsigned char *p = (unsigned char *)0xF000FF00;
*p=11;  //程序的意义为在绝对地址0xF0000+0xFF00(186 处理器使用16 位段地址和16 位偏移地址)写入11。

ARM、PowerPC 等未采用段地址,p 指向的内存空间就是0xF000FF00,而*p = 11 就是在该地址写入11。

Question:对于ARM来说,内存空间0xF000FF00是物理地址还是虚拟地址? Answer:虚拟地址,物理地址必须映射为虚拟地址才可以访问

即便是在X86 处理器中,虽然提供了I/O 空间,如果由我们自己设计电路板,外设仍然可以只挂接在内存空间。此时,CPU 可以像访问一个内存单元那样访问外设I/O 端口,而不需要设立专门的I/O 指令。因此,内存空间是必须的,而I/O 空间是可选的。



内存管理单元(MMU)

功能:内存管理(虚拟地址和物理地址的映射、内存访问权限保护和Cache 缓存控制等硬件支持)

操作原理:



内存的存取



设备IO 端口和IO 内存的访问

设备通常会提供一组寄存器来用于控制设备、读写设备和获取设备状态,即控制寄存器、数据寄存器和状态寄存器。这些寄存器可能位于I/O 空间,也可能位于内存空间。当位于I/O 空间时,通常被称为I/O 端口,位于内存空间时,对应的内存空间被称为I/O 内存

当然对于ARM来说,这些端口寄存器是位于内存空间(IO内存)。

Linux对IO端口的访问

把IO端口映射到内存空间

以后用到再来添加

Linux对IO内存的访问

在内核中访问I/O 内存之前,需首先使用ioremap()函数将设备所处的物理地址映射到虚拟地址。

//ioremap()的原型如下:void *ioremap(unsigned long offset, unsigned long size);//通过ioremap()获得的虚拟地址应该被iounmap()函数释放void iounmap(void * addr);

在设备的物理地址被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是可以使用Linux 内核的如下一组函数来完成设备内存映射的虚拟地址的读写

//(1)读I/O 内存。
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
//与上述函数对应的较早版本的函数为(这些函数在Linux 2.6 中仍然被支持):
unsigned readb(address);
unsigned readw(address);
unsigned readl(address);
//(2)写I/O 内存。
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);
//与上述函数对应的较早版本的函数为(这些函数在Linux 2.6 中仍然被支持):
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);
//(3)读一串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);
//(4)写一串I/O 内存。
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 内存。
void memcpy_fromio(void *dest, void *source, unsigned int count);
void memcpy_toio(void *dest, void *source, unsigned int count);
//(6)设置I/O 内存。
void memset_io(void *addr, u8 value, unsigned int count);

beep_ioremap实例

...
//Port GPBx  Register address declaration
#define GPBCON (unsigned long)ioremap(0x56000010,4)
#define GPBDAT (unsigned long)ioremap(0x56000014,4)
#define GPBUP  (unsigned long)ioremap(0x56000018,4)
...
void beep_start( void )
{
	/*config GPBCON, set GPB0 as output port*/
	port_status = readl(GPBCON);
	port_status &= ~0x03;
	port_status |= 0x01;
	writel(port_status,GPBCON);

	port_status = readl(GPBDAT);
	port_status |= 0x01; // set 1 to GPB0
	writel(port_status,GPBDAT);
}

void beep_stop( void )
{
	/*config GPBCON, set GPB0 as output port*/
	port_status = readl(GPBCON);
	port_status &= ~0x03;
	port_status |= 0x01;
	writel(port_status,GPBCON);

	port_status = readl(GPBDAT);
	port_status &= ~0x01;// set 0 to GPB0
	writel(port_status,GPBDAT);
}
...

  

 申请和释放IO端口和IO内存

(此处只看IO内存)

//一组函数用于申请和释放I/O 内存的范围,成对存在
//这个函数向内核申请n 个内存地址,这些地址从first 开始,name 参数为设备的名称。如果分配成功返回值是非 NULL,如果返回NULL,则意味着申请I/O 内存失败。
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);
//
void release_mem_region(unsigned long start, unsigned long len);

上述request_region()和release_mem_region()都不是必须的,但建议使用。其任务是检查申请的资源是否可用,如果可用则申请成功,并标志为已经使用,其他驱动想再次申请该资源时就会失败。
有许多设备驱动程序在没有申请I/O 端口和I/O 内存之前就直接访问了,这不够安全。

设备 I/O 端口和I/O 内存访问流程

I/O 内存的访问步骤:

1. 调用request_mem_region()申请资源

2. 将寄存器地址通过ioremap()映射到内核空间虚拟地址

3. 通过Linux 设备访问编程接口访问这些设备的寄存器

4. 访问完成后,应对ioremap()申请的虚拟地址进行释放,并释放release_mem_region()申请的I/O 内存资源

将设备地址映射到用户空间

1.内存映射与VMA

一般情况下,用户空间是不可能也不应该直接访问设备的,但是,设备驱动程序中可实现mmap()函数,这个函数可使得用户空间直能接访问设备的物理地址。实际上,mmap()实现了这样的一个映射过程:它将用户空间的一段内存与设备内存关联,当用户访问用户空间的这段地址范围时,实际上会转化为对设备的访问。

(这种能力对于显示适配器一类的设备非常有意义,如果用户空间可直接通过内存映射访问显存的话,屏幕帧的各点的像素将不再需要一个从用户空间到内核空间的复制的过程。)

mmap()必须以PAGE_SIZE 为单位进行映射,实际上,内存只能以页为单位进行映射,若要映射非PAGE_SIZE 整数倍的地址范围,要先进行页对齐,强行以PAGE_SIZE 的倍数大小进行映射。

//驱动中mmap()函数的原型
int(*mmap)(struct file *, struct vm_area_struct*);
//应用层的系统调用
caddr_t mmap (caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset);
/*
参数fd 为文件描述符,一般由open()返回,fd 也可以指定为!1,此时需指定flags 参数MAP_ANON,表明进行的是匿名映射。
len 是映射到调用用户空间的字节数,它从被映射文件开头offset 个字节开始算起,offset 参数一般设为0,表示从文件头开始映射。
prot 参数指定访问权限,可取如下几个值的“或”:PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行)和PROT_NONE(不可访问)。
参数addr 指定文件应被映射到用户空间的起始地址,一般被指定为NULL,这样,选择起始地址的任务将由内核完成,而函数的返回值就是映射到用户空间的地址。其类型caddr_t 实际上就是void *。
*/

当用户调用mmap()的时候,内核会进行如下处理。
① 在进程的虚拟空间查找一块VMA。
② 将这块VMA 进行映射。
③ 如果设备驱动程序或者文件系统的file_operations 定义了mmap()操作,则调用它。
④ 将这个VMA 插入进程的VMA 链表中。



IO内存静态映射



DMA

时间: 2024-08-10 08:19:09

Linux驱动设计——内存与IO访问的相关文章

linux cpu、内存、IO、网络的测试工具(转)

一.linux cpu.内存.IO.网络的测试工具cpu测试工具1.Super Pi for linuxSuper PI是利用CPU的浮点运算能力来计算出π(圆周率),所以目前普遍被用户用做测试系统稳定性和测试CPU计算完后特定位数圆周率所需的时间. ./super_pi 2020为位数.表示要算2的多少次方位,如通常要算小数点后1M位.2.sysbenchsysbench是一个开源的.模块化的.跨平台的多线程性能测试工具,可以用来进行CPU.内存.磁盘I/O.线程.数据库的性能测试. 内存测试

linux驱动---用I/O命令访问PCI总线设备配置空间

PCI总线推出以来,以其独有的特性受到众多厂商的青睐,已经成为计算机扩展总线的主流.目前,国内的许多技术人员已经具备开发PCI总线接口设备的能 力.但是PCI总线的编程技术,也就是对PCI总线设备的操作技术,一直是一件让技术人员感到头疼的事情.PCI总线编程的核心技术是对相应板卡配置空间 的理解和访问.一般软件编程人员基于对硬件设备原理的生疏,很难理解并操作配置空间,希望硬件开发人员直接告诉他们怎样操作:而PCI总线硬件开发人员虽 深刻地理解了其意义,在没有太多编程经验地前提下,也难于轻易地操作

Linux驱动设计—— 驱动调试技术

参考博客与书籍: <Linux设备驱动开发详解> <Linux设备驱动程序> http://blog.chinaunix.net/uid-24219701-id-2884942.html 对于驱动程序设计来说,核心问题之一就是如何完成调试.当前常用的驱动调试技术可分为: 1. 打印调试 printk 重定向控制台消息 消息记录 开启和关闭消息速度限制 打印设备编号 2. 调试器调试 gdb kdb内核调试器 kgdb补丁 3. 查询调试 使用/proc文件系统 ioctl方法 4.

Linux驱动设计——并发控制

四种并发控制机制:原子操作.自旋锁.信号量和完成量. 原子变量操作 原子变量操作绝对不会再执行完毕前被任何其他任务或事件打断.原子操作需要硬件的支持,因此是架构相关的,其API和原子类型的定义都定义在内核源码树中的include/asm/atomic.h文件中,它们都是使用汇编语言实现的. 常用于多个应用程序对同一个共享的值进行操作的情况. 自旋锁 自旋锁是实现信号量和完成量的基础.对资源有很好的保护作用. Linux系统中提供了一些锁机制来避免竞争条件,最简单的一种就是自旋锁.引入锁的机制是因

Linux驱动设计—— 中断与时钟

中断和时钟技术可以提升驱动程序的效率 中断 中断在Linux中的实现 通常情况下,一个驱动程序只需要申请中断,并添加中断处理函数就可以了,中断的到达和中断函数的调用都是内核实现框架完成的.所以程序员只要保证申请了正确的中断号及编写了正确的中断处理函数即可. 中断的宏观分类 1.硬中断 由系统硬件产生的中断.系统硬件通常引起外部事件.外部事件事件具有随机性和突发性,因此硬件中断也具有随机性和突发性. 2.软中断 软中断是执行中断指令时产生的.软中断不用外设施加中断请求信号,因此软中断的发生不是随机

Linux驱动设计编译错误信息集锦

1.warning: passing argument 2 of 'request_irq' from incompatible pointer type http://blog.sina.com.cn/s/blog_7321be1101012gek.html 今天在些key的driver的时候...写完了编译出现一个warmming如下:warning: passing argument 2 of 'request_irq' from incompatible pointer type 我的r

Linux驱动设计——阻塞和同步

阻塞和非阻塞是设备访问的两种基本方式,阻塞和非阻塞驱动程序使用时,经常会用到等待队列. 阻塞和非阻塞 阻塞调用是指调用结果返回之前,当前线程会被挂起.函数只有得到结果之后才会返回.而对于同步调用来说,许多时候当前线程还是激活的,只是逻辑上当前函数没有返回而已. 非阻塞指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回. 完整实例程序分析 等待队列 等待队列机制使等待的进程暂时睡眠,当等待的信号到来时,便唤醒等待队列中进程继续执行. 等待队列的基本数据结构是一个双向链表,这个链表可以存

Linux驱动虚拟地址和物理地址的映射(转)

原文地址:http://blog.chinaunix.net/uid-20792373-id-2979673.html 参考链接: Linux 虚拟地址与物理地址的映射关系分析  https://blog.csdn.net/ordeder/article/details/41630945 虚拟地址映射到物理地址的学习(linux篇) https://blog.csdn.net/chablin/article/details/79827226 linux--物理地址到虚拟地址映射,ioremap(

linux驱动之i2c子系统device注册driver注册简单分析

Linux 驱动设计主要是根据分层分离思想,i2c子系统分为i2cocre.adapter.及device_driver层,其实adapter也是个device,只不过是我们主控芯片的I2C控制接口而已,我们的主控芯片有几个I2C接口就有几个adapter; i2ccore这一层linux已经帮我们实现,主要的工做是类似platform总线的作用,负责drvier及设备的注册,相比platform多了个adapter的注册管理工作,以及i2c的数据发送接收等等算法,说算法有点夸大,其实就是按照i