近段时间在TI的DM6467平台上搞起了linux系统下的ARM开发,前几日弄了个SPI的驱动程序,里面对SPI寄存器操作时直接定义了一个寄存器组的结构体:
//定义SPI寄存器结构体
struct davinci_spi_reg {
volatile __u32 __bitwise
SPIGCR0;
volatile __u32 __bitwise
SPIGCR1;
volatile __u32 __bitwise
SPIINT;
volatile __u32 __bitwise
SPILVL;
volatile __u32 __bitwise
SPIFLG;
volatile __u32 __bitwise
SPIPC0;
volatile __u32 __bitwise
SPIPC1;
volatile __u32 __bitwise
SPIPC2;
volatile __u32 __bitwise
SPIPC3;
volatile __u32 __bitwise
SPIPC4;
volatile __u32 __bitwise
SPIPC5;
volatile __u32 __bitwise
SPIPC6;
volatile __u32 __bitwise
SPIPC7;
volatile __u32 __bitwise
SPIPC8;
volatile __u32 __bitwise
SPIDAT0;
volatile __u32 __bitwise
SPIDAT1;
volatile __u32 __bitwise
SPIBUF;
volatile __u32 __bitwise
SPIEMU;
volatile __u32 __bitwise
SPIDELAY;
volatile __u32 __bitwise
SPIDEF;
volatile __u32 __bitwise
SPIFMT[4];
volatile __u32 __bitwise
TGINTVEC[2];
volatile __u8 __bitwise
RSVD0[8];
volatile __u32 __bitwise
MIBSPIE;
};
之后对寄存器进行操作时就直接调用结构体成员进行赋值:
spi_reg_davinci->SPIDELAY =
0
| (
8 << 24
) // C2TDELAY
| (
8 << 16 ); //
T2CDELAY
起初对这种寄存器的操作方式没有想太多,感觉这些寄存器名应该有一个宏定义对其地址预先进行了定义。由于我之前是从C8051F120单片机转入的ARM开发,所以感觉应该也有一个像c8051f120.h这样的头文件来定义寄存器,另一方面,在网上查找了一些资料,在一些说到三星的S3C2410的文章里面也发现了类似单片机的寄存器定义的内容,于是更加坚定了我的想法。然而在网上搜索了一大圈也没发现davinci平台中的寄存器定义的头文件,很是郁闷。
终于在查找get_clk_rate()函数时偶然的找到了突破点。为了寻找platform_device结构体的内容,又把驱动的初始化和注册部分看了下,突然发现一条语句:
其中已经在之前定义了#define
DAVINCI_SPI_BASE (0x01C66800),也就是SPI相关寄存器的基地址。ioremap()函数是将物理地址映射为内核可以操作的虚拟地址,查了一下该函数的定义:
static inline void __iomem * ioremap(unsigned long
offset, unsigned long size)
{
return __ioremap(offset, size, 0);
}
可以看出ioremap()函数的返回值是一个地址,上面这条语句就是将SPI寄存器物理地址的首地址通过ioremap()函数映射后获得的虚拟地址作为SPI寄存器组结构体的起始地址,那么该结构体中对应的各成员的地址也就确定了,就可以对各成员进行操作了,对成员的操作也就实现了对寄存器的操作。
注意到(struct davinci_spi_reg *)ioremap(DAVINCI_SPI_BASE,200)其中的(struct davinci_spi_reg *),这是对ioremap()函数的返回值进行类型强制转换。因为ioremap()函数的返回值是(void __iomem *)类型,而spi_reg_davinci结构体是(struct davinci_spi_reg *)类型的,虽然两者对应的都是地址指针,但是编译时可能会有警告,而严谨的程序员是不容许出现警告的,这样的类型转换正体现了程序的严谨。
那么这里为什么要这样来操作寄存器,而不是像
#define
I2C_BASE 0x1c21000
#define
I2C_OAR *( volatile Uint32* )( I2C_BASE + 0x00 )
这样来定义寄存器呢?在网上查找到了以下内容:
--------为了使软件访问I/O内存,必须为设备分配虚拟地址.这就是ioremap的工作.这个函数专门用来为I/O内存区域分配虚拟地址(空间).对于直接映射的I/O地址ioremap不做任何事情(uClinux中是这么实现的??)有了ioremap(和iounmap),设备就可以访问任何I/O内存空间,不论它是否直接映射到虚拟地址空间.但是,这些地址永远不能直接使用(指物理地址),而要用readb这种函数.根据计算机平台和所使用总线的不同,I/O 内存可能是,也可能不是通过页表访问的,通过页表访问的是统一编址(PowerPC),否则是独立编址(Intel)。如果访问是经由页表进行的,内核必须首先安排物理地址使其对设备驱动
程序可见(这通常意味着在进行任何 I/O 之前必须先调用ioremap)。如果访问无需页表,那么
I/O 内存区域就很象 I/O 端口,可以使 用适当形式的函数读写它们。不管访问
I/O 内存时是否需要调用ioremap,都不鼓励直接使用指向
I/O 内存的指针。尽管(在“I/O 端口和 I/O 内存” 介绍过)I/O 内存在硬件一级是象普通 RAM 一样寻址的,但在“I/O 寄存器和常规内存”中描述过的那些需要额外小心的情况中已经建议不要使用普
通指针。相反,使用“包装的”函数访问
I/O 内存,一方面在所有平台上都是安全的,另一方面,在可以直接对指针指向的内存区域执行操作的时候,该函数是经过优化的。
看了上面的文字后,我认为使用ioremap()是为了满足程序的通用性和更好的安全性,同时也找到了关于__iomem的叙述:
__iomem是2.6.9中加入的特性。是用来个表示指会指向一个I/O的内存空间。主要是为了driver的通用性考虑。由于不同的CPU体系结构对I/O空间的表示可能不同。当使用__iomem时,compiler会忽略对变量的检查(因为用的是void __iomem)。但sparse会对它进行检查,当__iomem的指针和正常的指针混用时,就会发出一些warnings。
到此为止,基本上是理解了linux系统中这种对寄存器的定义和操作的方法及原因,虽然走了一些弯路,但最终还是小有所得,困惑我几天的问题终于得以解决了。
若文中有错误或者不足之处还请指出和讨论。