Chinaunix首页 | 论坛 | 博客
  • 博客访问: 199362
  • 博文数量: 70
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 865
  • 用 户 组: 普通用户
  • 注册时间: 2013-02-01 20:58
文章分类

全部博文(70)

文章存档

2015年(10)

2014年(15)

2013年(45)

我的朋友

分类: 嵌入式

2014-04-02 11:48:34

1I/O 端口和 I/O 内存

每个外设都是通过读写其寄存器来控制的。通常一个设备有几个寄存器,它们位于内存地址空间或者I/O地址空间,并且地址是连续的。

在硬件层上,内存区和I/O区域没有概念上的区别它们都是通过在地址总线和控制总线上发出电信号来存取(即,读写信号),并且通过数据总线来读写数据。

在一些CPU制造商在其芯片上实现了一个单地址空间(统一编址)的同时,其它的CPU制造商认为外设不同于内存,应该有一个独立的地址空间给外设(单独编址),其生产处理器(特别是x86家族)I/O端口有自己的读写信号线和特殊的CPU指令来存取端口。因为外设要与外设总线相匹配,并且大部分流行的I/O总线都是以个人计算机(主要是x86家族)作为模型,所以即便那些没有单独地址空间给I/O端口的处理器,也必须在访问外设时模拟成读写端口。这通常通过外部芯片组(PC中的南北桥)或者在CPU核中附加额外电路来实现(基于嵌入式应用的处理器)。关于编址方式、I/O 端口和 I/O 内存更详细内容见http://blog.chinaunix.net/u3/96613/showart_1926286.html

由于同样的理由,Linux在所有计算机平台上都实现了I/O端口,甚至在那些单地址空间的CPU平台上(模拟I/O端口)。并不是所有的设备都会将其寄存器映射到I/O端口。虽然ISA设备普遍使用I/O端口,但大部分PCI设备将寄存器映射到某个内存地址区。这种I/O内存方法通常是首选的,因为它无需使用特殊的处理器指令,CPU存取内存也更有效率,并且编译器在存取内存时在寄存器分配和寻址模式的选择上有更多自由。

1.1I/O 寄存器和常规内存

I/O寄存器和RAM的主要不同是I/O操作有边际效应side effect而内存操作没有:访问内存只是在内存某一位置存储数值。因为内存存取速度严重影响CPU的性能,编译器可能会对源码进行优化,主要是:使用高速缓存和重排读/写指令的顺序。对于传统内存(至少在单处理器系统)这些优化是透明有益的,但是对于I/O 寄存器,这可能是致命错误,因为它们干扰了那些"边际效应"(驱动程序存取I/O 寄存器就是为了获取边际效应)。因此,驱动程序必须确保在存取寄存器时,不能使用高速缓存并且不能重新编排读写指令的顺序。

side effect 是指:访问I/O寄存器时,不仅仅会像访问普通内存一样影响存储单元的值,更重要的是它可能改变CPU的I/O端口电平、输出时序或CPU对I/O端口电平的反应等等,从而实现CPU的控制功能。CPU在电路中的意义就是实现其side effect 。举个例子,有些设备的中断状态寄存器只要一读取,便自动清零。


硬件缓冲的问题是最易解决的:只要将底层硬件配置(或者自动地或者通过Linux 初始化代码)为当存取I/O区时,禁止任何硬件缓冲(不管是I/O 内存还是I/O 端口)。

编译器优化和硬件重编排读写指令顺序的解决方法是:在硬件或处理器必须以一个特定顺序执行的操作之间安放一个内存屏障(memory barrierLinux 提供以下几个宏来实现这个功能:

#include <linux/kernel.h>
/* barrier(软件内存屏障)告知编译器插入一个内存屏障但是对硬件没有影响。编译后的代码将当前CPU寄存器所有修改过的值保存到内存,并且在需要时重新读取它们。barrier可阻止在屏障前后的编译器优化,但硬件能完成自己的重新排序 */
void barrier(void)

#include <asm/system.h>
/* rmb(read memory barrier)保证任何出现于屏障前的读在执行任何后续读之前完成 */
void rmb(void);
/* read_barrier_depends是一种特殊的、弱些的读屏障形式。rmb 阻止屏障前后的所有读指令的重新排序,read_barrier_depends 只阻止依赖于其他读指令返回的数据的读指令的重新排序。区别微小, 并且不是所有体系都支持。除非你确切地理解它们的差别, 并确信完整的读屏障会增加系统开销,否则应当始终使用 rmb。*/
void read_barrier_depends(void);
/* wmb(write memory barrier)保证任何出现于屏障前的写在执行任何后续的写之前完成 */
void wmb(void);
/* rmb(memory barrier)保证任何出现于屏障前的读写操作在执行任何后续的读写操作之前完成 */
void mb(void);
/* 以上这些宏都是是barrier的超集,它们插入硬件内存屏障在编译的指令流中,并且它们的实际实现是平台相关的 */


/* 以下这些屏障宏仅当SMP系统时插入硬件屏障;否则它们只是简单被替换成barrier宏 */
void smp_rmb(void);
void smp_read_barrier_depends(void);
void smp_wmb(void);
void smp_mb(void);



典型例子:
 

writel(dev->registers.addr, io_destination_address);
writel(dev->registers.size, io_size);
writel(dev->registers.operation, DEV_READ);

wmb( ); /* 在开始执行操作(最后一个写操作)之前,先将各相关控制寄存器设置好(前三个写操作)。其实wmb就是一条分界线,它保证其之前的三个写操作执行完了之后,才执行其后的写操作,但是前面三个写操作顺序则无法保证 */

writel(dev->registers.control, DEV_GO);

 

内存屏障影响系统性能,所以只能在确实需要它们的地方使用。不同类型的屏障也有不同的性能特性,因此,应当尽可能使用最合适的屏障类型

值得注意的是大部分处理同步的内核原语,例如自旋锁和atomic_t操作,也具有内存屏障的功能。还有就是有些外设总线(PCI 总线)有它们自己的缓冲问题我们在后面章节遇到时讨论这些问题

某些体系允许将赋值和内存屏障组合在一起,以提高效率。们如下定义:


#define set_mb(varvalue) do {var = value; mb();} while 0
#define set_wmb(var,,value) do {var = value; wmb();} while 0
#define set_rmb(var,,value) do {var = value; rmb();} while 0


在合适的地方,为了更快的完成任务,<asm/system.h> 使用体系特定的(architecture-specific)指令来定义这些宏。注意,很少体系支持set_rmb使用do...while 结构来定义宏是标准C的惯用方法(在内核源码中非常常见),它使被扩展的宏可在所有上下文环境中作为一个正常的C语句被执行

 

2、使用I/O 端口

I/O 端口是驱动用来和很多设备通讯的方法。

2.1、分配I/O 端口

在驱动还没独占设备之前,不应对端口进行操作。内核提供了一个注册接口,以允许驱动声明其需要的端口:

#include <linux/ioport.h>

/* request_region告诉内核:要使用first开始的n个端口。参数name为设备名。如果分配成功返回值是非NULL;否则无法使用需要的端口(/proc/ioports包含了系统当前所有端口的分配信息,若request_region分配失败时,可以查看该文件,看谁先用了你要的端口) */
struct resource *request_region(unsigned long first, unsigned long n, const char *name);

/* 用完I/O端口后(可能在模块卸载时),应当调用release_region将I/O端口返还给系统。参数start和n应与之前传递给request_region一致 */
void release_region(unsigned long start, unsigned long n); 

/* check_region用于检查一个给定的I/O端口集是否可用。如果给定的端口不可用,check_region返回一个错误码。不推荐使用该函数,因为即便它返回0(端口可用),它也不能保证后面的端口分配操作会成功,因为检查和后面的端口分配并不是一个原子操作。而request_region通过加锁来保证操作的原子性,因此是安全的 */
int check_region(unsigned long first, unsigned long n);


2.2、操作I/O端口


在驱动成功请求到I/O 端口后,就可以读写这些端口了。部分硬件会将81632位端口区分开,无法像访问内存那样混淆使用。驱动程序必须调用不同的函数来访问不同大小的端口。

如同前面所讲的,仅支持单地址空间的计算机体系通过将I/O端口地址重新映射到内存地址来伪装端口I/O 。为了提高移植性,内核对驱动隐藏了这些细节。Linux 内核头文件(系依赖的头文件定义了下列内联函数来存取I/O端口:

/* inb/outb:读/写字节端口(8位宽)。有些体系将port参数定义为unsigned long;而有些平台则将它定义为unsigned short。inb的返回类型也是依赖体系的 */
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);

/* inw/outw:读/写字端口(16位宽) */
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);

/* inl/outl:读/写32位端口。longword也是依赖体系的,有的体系为unsigned long;而有的为unsigned int */
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);


从现在开始,当我们使用unsigned 没有进一步指定类型时,表示是一个依赖体系的定义

注意,没有64位的I/O端口操作函数。即便在64位体系中,端口地址空间使用一个32(最大)的数据通路。

2.3、从用户空间访问I/O端口

2.2节介绍的函数主要是提供给驱动使用,但它们也可在用户空间使用,至少在PC机上可以GNU C 库在  中定义它们。如果在用户空间使用这些函数,必须满足下列条件:

1)、程序必须使用-O选项编译来强制扩展内联函数

2)、必须使用iopermiopl系统调用(#include ) 来获得进行操作I/O端口的权限ioperm 为获取单个端口的操作许可iopl 为获取整个I/O空间许可。这2个函数都是x86特有的

3)、程序必须以root来调用ioperm或者iopl,或者其父进程(祖先)必须以root获得的端口操作权限

如果平台不支持iopermiopl系统调用,通过使用/dev/prot设备文件,用户空间仍然可以存取I/O 端口。但是要注意的是,这个文件的定义也是依赖平台的

2.4、字串操作

除了一次传递一个数据的I/O操作,某些处理器实现了一次传递一序列数据(单位可以是字节、字和双字)的特殊指令。这些所谓的字串指令,它们完成任务比一个C语言循环更快。下列宏定义实现字串操作,在某些体系上,它们通过使用单个机器指令实现;但如果目标处理器没有进行字串I/O指令,则通过执行一个紧凑的循环实现

字串函数的原型是:


/* insb:从I/O端口port读取count个数据(单位字节)到以内存地址addr为开始的内存空间 */
void insb(unsigned port, void *addr, unsigned long count);

/* outsb:将内存地址addr开始的count个数据(单位字节)写到I/O端口port */
void outsb(unsigned port, void *addr, unsigned long count);

/* insw:从I/O端口port读取count个数据(单位字)到以内存地址addr为开始的内存空间 */
void insw(unsigned port, void *addr, unsigned long count);

/* outsw:将内存地址addr开始的count个数据(单位字)写到I/O端口port */
void outsw(unsigned port, void *addr, unsigned long count);

/* insl:从I/O端口port读取count个数据(单位双字)到以内存地址addr为开始的内存空间 */
void insl(unsigned port, void *addr, unsigned long count);
/* outsl:将内存地址addr开始的count个数据(单位双字)写到I/O端口port */
void outsl(unsigned port, void *addr, unsigned long count);


注意:使用字串函数时它们直接将字节流从端口中读取或写入。当端口和主机系统有不同的字节序时,会导致不可预期的结果。 使用 inw 读取端口应在必要时自行转换字节序,以匹配主机字节序。

2.5、暂停式I/O操作函数

由于处理器的速率可能与外设(尤其是低速设备)的并不匹配,当处理器过快地传送数据到或自总线时,这时可能就会引起问题。解决方法是:I/O 指令后面紧跟着另一个相似的I/O 指令,就必须插入一个小的延时。为此,Linux提供了暂停式I/O操作函数,这些函数的名子只是在非暂停式I/O操作函数(前面提到的那些I/O操作函数都是非暂停式的)名后加上_p inb_p、outb_p等。大部分体系都支持这些函数,尽管它们常常被扩展为与非暂停 I/O 同样的代码,因为如果体系使用一个合理的现代外设总线,没有必要额外暂停。

以下是ARM体系暂停式I/O宏的定义:

#define outb_p(val,port)    outb((val),(port))
#define outw_p(val,port)    outw((val),(port))
#define outl_p(val,port)    outl((val),(port))
#define inb_p(port)        inb((port))
#define inw_p(port)        inw((port))
#define inl_p(port)        inl((port))

#define outsb_p(port,from,len)    outsb(port,from,len)
#define outsw_p(port,from,len)    outsw(port,from,len)
#define outsl_p(port,from,len)    outsl(port,from,len)
#define insb_p(port,to,len)    insb(port,to,len)
#define insw_p(port,to,len)    insw(port,to,len)
#define insl_p(port,to,len)    insl(port,to,len)


因为ARM使用内部总线,就没有必要额外暂停,所以暂停式的I/O函数被扩展为与非暂停式I/O 同样的代码。

2.6、平台依赖性

于自身的特性,I/O指令高度依赖于处理器,非常难以隐藏各体系间的不同。因此,大部分的关于端口 I/O 的源码是平台依赖的。以下是x86和ARM所使用函数的总结:

IA-32 (x86)

x86_64

这个体系支持本章介绍的所有函数;port参数的类型为unsigned short。

ARM

端口映射到内存,并且支持本章介绍的所有函数;port参数的类型为unsigned int;字串函数用C语言实现。

 

3、使用 I/O 内存

尽管 I/O 端口在x86世界中非常流行,但是用来和设备通讯的主要机制是通过内存映射的寄存器和设备内存,两者都称为I/O 内存,因为寄存器和内存之间的区别对软件是透明的。

I/O 内存仅仅是一个类似于RAM 的区域,处理器通过总线访问该区域,以实现对设备的访问。同样,读写这个区域是有边际效应。

根据计算机体系和总线不同,I/O 内存可分为可以或者不可以通过页表来存取。若通过页表存取,内核必须先重新编排物理地址,使其对驱动程序可见,这就意味着在进行任何I/O操作之前,你必须调用ioremap;如果不需要页表,I/O内存区域就类似于I/O端口,你可以直接使用适当的I/O函数读写它们。

由于边际效应的缘故,不管是否需要 ioremap,都不鼓励直接使用I/O内存指针,而应使用专门的I/O内存操作函数。这些I/O内存操作函数不仅在所有平台上是安全,而且对直接使用指针操作 I/O 内存的情况进行了优化。

3.1I/O 内存分配和映射

I/O 内存区在使用前必须先分配。分配内存区的函数接口定义中:


/* request_mem_region分配一个开始于start,len字节的I/O内存区。分配成功,返回一个非NULL指针;否则返回NULL。系统当前所有I/O内存分配信息都在/proc/iomem文件中列出,你分配失败时,可以看看该文件,看谁先占用了该内存区 */
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);

/* release_mem_region用于释放不再需要的I/O内存区 */
void release_mem_region(unsigned long start, unsigned long len); 

/* check_mem_region用于检查I/O内存区的可用性。同样,该函数不安全,不推荐使用 */
int check_mem_region(unsigned long start, unsigned long len);


在访问I/O内存之前,分配I/O内存并不是唯一要求的步骤,你还必须保证内核可存取该I/O内存。访问I/O内存并不只是简单解引用指针,在许多体系中,I/O 内存无法以这种方式直接存取。因此,还必须通过ioremap 函数(第8章第4节介绍过)设置一个映射。

#include <asm/io.h>
/* ioremap用于将I/O内存区映射到虚拟地址。参数phys_addr为要映射的I/O内存起始地址,参数size为要映射的I/O内存的大小,返回值为被映射到的虚拟地址 */
void *ioremap(unsigned long phys_addr, unsigned long size);

/* ioremap_nocache为ioremap的无缓存版本。实际上,在大部分体系中,ioremap与ioremap_nocache的实现一样的,因为所有 I/O 内存都是在无缓存的内存地址空间中 */
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);

/* iounmap用于释放不再需要的映射 */
void iounmap(void * addr);


经过 ioremap (iounmap)之后,设备驱动就可以存取任何I/O内存地址。注意,ioremap返回的地址不可以直接解引用相反,应当使用内核提供的访问函数。

3.2、访问I/O内存

访问I/O内存的正确方式是通过一系列专门用于实现此目的的函数:

#include <asm/io.h>
/* I/O内存读函数。参数addr应当是从ioremap获得的地址(可能包含一个整型偏移); 返回值是从给定I/O内存读取到的值 */
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);

/* I/O内存写函数。参数addr同I/O内存读函数,参数value为要写的值 */
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);

/* 以下这些函数读和写一系列值到一个给定的 I/O 内存地址,从给定的buf读或写count个值到给定的addr。参数count表示要读写的数据个数,而不是字节大小 */
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 *bufunsigned long count);
void iowrite16_rep(void *addr, const void *buf, unsigned long count);
void iowrite32_rep(void *addr,,onst void *buf,,nsigned long count);

/* 需要操作一块I/O 地址时,使用下列函数(这些函数的行为类似于它们的C库类似函数): */
void memset_io(void *addr, u8 value, unsigned int count);
void memcpy_fromio(void *dest, void *source, unsigned int count);
void memcpy_toio(void *dest, void *source, unsigned int count);

/* 旧的I/O内存读写函数,不推荐使用 */
unsigned readb(address);
unsigned readw(address);
unsigned readl(address); 
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address); 


3.3、像I/O 内存一样使用端口

一些硬件有一个有趣的特性有些版本使用 I/O 端口;而有些版本则使用 I/O 内存。不管是I/O 端口还是I/O 内存,处理器见到的设备寄存器都是相同的,只是访问方法不同。为了统一编程接口,使驱动程序易于编写,2.6 内核提供了一个ioport_map函数:

/* ioport_map重新映射count个I/O端口,使它们看起来I/O内存。此后,驱动程序可以在ioport_map返回的地址上使用ioread8和同类函数。这样,就可以在编程时,消除了I/O 端口和I/O 内存的区别 */
void *ioport_map(unsigned long port, unsigned int count); 

/* ioport_unmap用于释放不再需要的映射 */
void ioport_unmap(void *addr);


意,I/O 端口在重新映射前必须使用request_region分配分配所需的I/O 端口。
4、ARM体系的I/O操作接口

s3c24x0处理器使用的是I/O内存,也就是说:s3c24x0处理器使用统一编址方式,I/O寄存器和内存使用的是单一地址空间,并且读写I/O寄存器和读写内存的指令是相同的。所以推荐使用I/O内存的相关指令和函数但这并不表示I/O端口的指令在s3c24x0中不可用。如果你注意过s3c24x0关于I/O方面的内核源码,你就会发现:其实I/O端口的指令只是一个外壳,内部还是使用和I/O内存一样的代码

下面是ARM体系原始的I/O操作函数。其实后面I/O端口和I/O内存操作函数,只是对这些函数进行再封装。从这里也可以看出为什么我们不推荐直接使用I/O端口和I/O内存地址指针,而是要求使用专门的I/O操作函数——专门的I/O操作函数会检查地址指针是否有效是否为IO地址(通过__iomem或__chk_io_ptr


#include <asm-arm/io.h>

/*
 * Generic IO read/write. These perform native-endian accesses. Note
 * that some architectures will want to re-define __raw_{read,write}w.
 */

extern void __raw_writesb(void __iomem *addr, const void *data, int bytelen);
extern void __raw_writesw(void __iomem *addr, const void *data, int wordlen);
extern void __raw_writesl(void __iomem *addr, const void *data, int longlen);

extern void __raw_readsb(const void __iomem *addr, void *data, int bytelen);
extern void __raw_readsw(const void __iomem *addr, void *data, int wordlen);
extern void __raw_readsl(const void __iomem *addr, void *data, int longlen);

#define __raw_writeb(v,a)    (__chk_io_ptr(a), *(volatile unsigned char __force *)(a) =(v))
#define __raw_writew(v,a)    (__chk_io_ptr(a), *(volatile unsigned short __force *)(a) =(v))
#define __raw_writel(v,a)    (__chk_io_ptr(a), *(volatile unsigned int __force *)(a) =(v))

#define __raw_readb(a)        (__chk_io_ptr(a), *(volatile unsigned char __force *)(a))
#define __raw_readw(a)        (__chk_io_ptr(a), *(volatile unsigned short __force *)(a))
#define __raw_readl(a)        (__chk_io_ptr(a), *(volatile unsigned int __force *)(a))


关于__force和__iomem


#include <linux/compiler.h>

/* __force表示所定义的变量类型是可以做强制类型转换的 */
#define __force __attribute__((force)) 

/* __iomem是用来修饰一个变量的,这个变量必须是非解引用(no dereference)的,即这个变量地址必须是有效的,而且变量所在的地址空间必须是2,即设备地址映射空间。0表示normal space,即普通地址空间,对内核代码来说,当然就是内核空间地址了。1表示用户地址空间,2表示是设备地址映射空间 */
#define __iomem __attribute__((noderef, address_space(2)))


I/O端口


#include <asm-arm/io.h>

#define outb(v,p)        __raw_writeb(v,__io(p))
#define outw(v,p)        __raw_writew((__force __u16) \
                    cpu_to_le16(v),__io(p))
#define outl(v,p)        __raw_writel((__force __u32) \
                    cpu_to_le32(v),__io(p))

#define inb(p)    ({ __u8 __v = __raw_readb(__io(p)); __v; })
#define inw(p)    ({ __u16 __v = le16_to_cpu((__force __le16) \
            __raw_readw(__io(p))); __v; })
#define inl(p)    ({ __u32 __v = le32_to_cpu((__force __le32) \
            __raw_readl(__io(p))); __v; })

#define outsb(p,d,l)        __raw_writesb(__io(p),d,l)
#define outsw(p,d,l)        __raw_writesw(__io(p),d,l)
#define outsl(p,d,l)        __raw_writesl(__io(p),d,l)

#define insb(p,d,l)        __raw_readsb(__io(p),d,l)
#define insw(p,d,l)        __raw_readsw(__io(p),d,l)
#define insl(p,d,l)        __raw_readsl(__io(p),d,l)


I/O内存


#include <asm-arm/io.h>

#define ioread8(p)    ({ unsigned int __v = __raw_readb(p); __v; })
#define ioread16(p)    ({ unsigned int __v = le16_to_cpu((__force __le16)__raw_readw(p));__v; })
#define ioread32(p)    ({ unsigned int __v = le32_to_cpu((__force __le32)__raw_readl(p));__v; })

#define iowrite8(v,p)    __raw_writeb(v, p)
#define iowrite16(v,p)    __raw_writew((__force __u16)cpu_to_le16(v), p)
#define iowrite32(v,p)    __raw_writel((__force __u32)cpu_to_le32(v), p)

#define ioread8_rep(p,d,c)    __raw_readsb(p,d,c)
#define ioread16_rep(p,d,c)    __raw_readsw(p,d,c)
#define ioread32_rep(p,d,c)    __raw_readsl(p,d,c)

#define iowrite8_rep(p,s,c)    __raw_writesb(p,s,c)
#define iowrite16_rep(p,s,c)    __raw_writesw(p,s,c)
#define iowrite32_rep(p,s,c)    __raw_writesl(p,s,c)


注意:

1)、所有的读写指令(I/O操作函数)所赋的地址必须都是虚拟地址,你有两种选择:使用内核已经定义好的地址,如在include/asm-arm/arch-s3c2410/regs-xxx.h中定义了s3c2410处理器各外设寄存器地址(其他处理器芯片也可在类似路径找到内核定义好的外设寄存器的虚拟地址;另一种方法就是使用自己用ioremap映射的虚拟地址。绝对不能使用实际的物理地址,否则会因为内核无法处理地址而出现oops。

2)、在使用I/O指令时,可以不使用request_region和request_mem_region,而直接使用outb、ioread等指令。因为request的功能只是告诉内核端口被谁占用了,如再次request,内核会制止(资源busy)。但是不推荐这么做,这样的代码也不规范,可能会引起并发问题(很多时候我们都需要独占设备)。

3)、在使用I/O指令时,所赋的地址数据有时必须通过强制类型转换为 unsigned long ,不然会有警告

4)、在include\asm-arm\arch-s3c2410\hardware.h中定义了很多io口的操作函数,有需要可以在驱动中直接使用,很方便。

阅读(1127) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~