如何编写Linux下Nand Flash驱动

如何编写Linux下Nand Flash驱动,第1张

1. 硬件特性:

【Flash的硬件实现机制】

Flash全名叫做Flash Memory,属于非易失性存储设备(Non-volatile Memory Device),与此相对应的是易失性存储设备(Volatile Memory Device)。关于什么是非易失性/易失性,从名字中就可以看出,非易失性就是不容易丢失,数据存储在这类设备中,即使断电了,也不会丢失,这类设备,除了Flash,还有其他比较常见的入硬盘,ROM等,与此相对的,易失性就是断电了,数据就丢失了,比如大家常用的内存,不论是以前的SDRAM,DDR SDRAM,还是现在的DDR2,DDR3等,都是断电后,数据就没了。

Flash的内部存储是MOSFET,里面有个悬浮门(Floating Gate),是真正存储数据的单元。

在Flash之前,紫外线可擦除(uv-erasable)的EPROM,就已经采用用Floating Gate存储数据这一技术了。

图1.典型的Flash内存单元的物理结构

数据在Flash内存单元中是以电荷(electrical charge) 形式存储的。存储电荷的多少,取决于图中的外部门(external gate)所被施加的电压,其控制了是向存储单元中冲入电荷还是使其释放电荷。而数据的表示,以所存储的电荷的电压是否超过一个特定的阈值Vth来表示。

【SLC和MLC的实现机制】

Nand Flash按照内部存储数据单元的电压的不同层次,也就是单个内存单元中,是存储1位数据,还是多位数据,可以分为SLC和MLC:

1.SLC,Single Level Cell:

单个存储单元,只存储一位数据,表示成1或0.

就是上面介绍的,对于数据的表示,单个存储单元中内部所存储电荷的电压,和某个特定的阈值电压Vth,相比,如果大于此Vth值,就是表示1,反之,小于Vth,就表示0.

对于nand Flash的数据的写入1,就是控制External Gate去充电,使得存储的电荷够多,超过阈值Vth,就表示1了。而对于写入0,就是将其放电,电荷减少到小于Vth,就表示0了。

关于为何Nand Flash不能从0变成1,我的理解是,物理上来说,是可以实现每一位的,从0变成1的,但是实际上,对于实际的物理实现,出于效率的考虑,如果对于,每一个存储单元都能单独控制,即,0变成1就是,对每一个存储单元单独去充电,所需要的硬件实现就很复杂和昂贵,同时,所进行对块擦除的 *** 作,也就无法实现之前的,一闪而过的速度了,也就失去了Flash的众多特性了。

2.MLC,Multi Level Cell:

与SLC相对应,就是单个存储单元,可以存储多个位,比如2位,4位等。其实现机制,说起来比较简单,就是,通过控制内部电荷的多少,分成多个阈值,通过控制里面的电荷多少,而达到我们所需要的存储成不同的数据。比如,假设输入电压是Vin=4V(实际没有这样的电压,此处只是为了举例方便),那么,可以设计出2的2次方=4个阈值, 1/4 的Vin=1V,2/4的Vin=2V,3/4的Vin=3V,Vin=4V,分别表示2位数据00,01,10,11,对于写入数据,就是充电,通过控制内部的电荷的多少,对应表示不同的数据。

对于读取,则是通过对应的内部的电流(与Vth成反比),然后通过一系列解码电路完成读取,解析出所存储的数据。这些具体的物理实现,都是有足够精确的设备和技术,才能实现精确的数据写入和读出的。

单个存储单元可以存储2位数据的,称作2的2次方=4 Level Cell,而不是2 Level Cell,这点,之前差点搞晕了。。。,同理,对于新出的单个存储单元可以存储4位数据的,称作 2的4次方=16 Level Cell。

【关于如何识别SLC还是MLC】

Nand Flash设计中,有个命令叫做Read ID,读取ID,意思是读取芯片的ID,就像大家的身份z一样,这里读取的ID中,是读取好几个字节,一般最少是4个,新的芯片,支持5个甚至更多,从这些字节中,可以解析出很多相关的信息,比如此Nand Flash内部是几个芯片(chip)所组成的,每个chip包含了几片(Plane),每一片中的页大小,块大小,等等。在这些信息中,其中有一个,就是识别此flash是SLC还是MLC。下面这个就是最常见的Nand Flash的datasheet中所规定的,第3个字节,3rd byte,所表示的信息,其中就有SLC/MLC的识别信息:

image

表1.Nand Flash 第3个ID的含义

【Nand Flash的物理存储单元的阵列组织结构】

Nand flash的内部组织结构,此处还是用图来解释,比较容易理解:

图2.Nand Flash物理存储单元的阵列组织结构

上图是K9K8G08U0A的datasheet中的描述。

简单解释就是:

1.一个nand flash由很多个块(Block)组成,块的大小一般是128KB,256KB,512KB,此处是128KB。

2.每个块里面又包含了很多页(page)。每个页的大小,对于现在常见的nand flash多数是2KB,更新的nand flash是4KB,这类的,页大小大于2KB的nand flash,被称作big block,对应的发读写命令地址,一共5个周期(cycle),而老的nand flash,页大小是256B,512B,这类的nand flash被称作small block,。地址周期只有4个。

而块,也是Nand Flash的擦除 *** 作的基本/最小单位。

3.每一个页,对应还有一块区域,叫做空闲区域(spare area)/冗余区域(redundant area),而Linux系统中,一般叫做OOB(Out Of Band),这个区域,是最初基于Nand Flash的硬件特性:数据在读写时候相对容易错误,所以为了保证数据的正确性,必须要有对应的检测和纠错机制,此机制被叫做EDC(Error Detection Code)/ECC(Error Code Correction, 或者 Error Checking and Correcting),所以设计了多余的区域,用于放置数据的校验值。

页是Nand Flash的写入 *** 作的基本/最小的单位。

【Nand Flash数据存储单元的整体架构】

简单说就是,常见的nand flash,内部只有一个chip,每个chip只有一个plane。

而有些复杂的,容量更大的nand flash,内部有多个chip,每个chip有多个plane。这类的nand flash,往往也有更加高级的功能,比如下面要介绍的Multi Plane Program和Interleave Page Program等。

比如,型号为K9K8G08U0A这个芯片(chip),内部有两个K9F4G08U0A,每个K9F4G08U0A包含了2个Plane,每个Plane是1Gb,所以K9F4G08U0A的大小是1Gb×2=2Gb=256MB,因此,K9K8G08U0A内部有2个K9F4G08U0A,即4个Plane,总大小是4×256MB=1GB。

而型号是K9WAG08U1A的nand flash,内部包含了2个K9K8G08U0A,所以,总容量是K9K8G08U0A的两倍=1GB×2=2GB,类似地K9NBG08U5A,内部包含了4个K9K8G08U0A,总大小就是4×1GB=4GB。

【Flash名称的由来】

Flash的擦除 *** 作是以block块为单位的,与此相对应的是其他很多存储设备,是以bit位为最小读取/写入的单位,Flash是一次性地擦除整个块:在发送一个擦除命令后,一次性地将一个block,常见的块的大小是128KB/256KB。。,全部擦除为1,也就是里面的内容全部都是0xFF了,由于是一下子就擦除了,相对来说,擦除用的时间很短,可以用一闪而过来形容,所以,叫做Flash Memory。中文有的翻译为 (快速)闪存。

【Flash相对于普通设备的特殊性】

1. 上面提到过的,Flash最小 *** 作单位,有些特殊。

一般设备,比如硬盘/内存,读取和写入都是以bit位为单位,读取一个bit的值,将某个值写入对应的地址的位,都是可以按位 *** 作的。

但是Flash由于物理特性,使得内部存储的数据,只能从1变成0,这点,可以从前面的内部实现机制了解到,只是方便统一充电,不方便单独的存储单元去放电,所以才说,只能从1变成0,也就是释放电荷。

所以,总结一下Flash的特殊性如下:

image

表2.Flash和普通设备相比所具有的特殊性

注:

① 之所以将写 *** 作叫做编程,是因为,flash 和之前的EPROM,EEPROM继承发展而来,而之前的EEPROM(Electrically Erasable Programmable Read-Only Memory),往里面写入数据,就叫做编程Program,之所以这么称呼,是因为其对数据的写入,是需要用电去擦除/写入的,就叫做编程。

② 对于目前常见的页大小是2K/4K的Nand Flash,其块的大小有128KB/256KB/512KB等。而对于Nor Flash,常见的块大小有64K/32K等。

③在写数据之前,要先擦除,内部就都变成0xFF了,然后才能写入数据,也就是将对应位由1变成0。

【Nand Flash引脚(Pin)的说明】

图3.Nand Flash引脚功能说明

上图是常见的Nand Flash所拥有的引脚(Pin)所对应的功能,简单翻译如下:

1. I/O0 ~ I/O7:用于输入地址/数据/命令,输出数据

2. CLE:Command Latch Enable,命令锁存使能,在输入命令之前,要先在模式寄存器中,设置CLE使能

3. ALE:Address Latch Enable,地址锁存使能,在输入地址之前,要先在模式寄存器中,设置ALE使能

4. CE#:Chip Enable,芯片使能,在 *** 作Nand Flash之前,要先选中此芯片,才能 *** 作

5. RE#:Read Enable,读使能,在读取数据之前,要先使CE#有效。

6. WE#:Write Enable,写使能, 在写取数据之前,要先使WE#有效。

7. WP#:Write Protect,写保护

8. R/B#:Ready/Busy Output,就绪/忙,主要用于在发送完编程/擦除命令后,检测这些 *** 作是否完成,忙,表示编程/擦除 *** 作仍在进行中,就绪表示 *** 作完成.

9. Vcc:Power,电源

10. Vss:Ground,接地

11. N.C:Non-Connection,未定义,未连接。

1. 硬件特性:

【Flash的硬件实现机制】

Flash全名叫做Flash Memory,属于非易失性存储设备(Non-volatile Memory Device),与此相对应的是易失性存储设备(Volatile Memory Device)。关于什么是非易失性/易失性,从名字中就可以看出,非易失性就是不容易丢失,数据存储在这类设备中,即使断电了,也不会丢失,这类设备,除了Flash,还有其他比较常见的入硬盘,ROM等,与此相对的,易失性就是断电了,数据就丢失了,比如大家常用的内存,不论是以前的SDRAM,DDR SDRAM,还是现在的DDR2,DDR3等,都是断电后,数据就没了。

Flash的内部存储是MOSFET,里面有个悬浮门(Floating Gate),是真正存储数据的单元。

在Flash之前,紫外线可擦除(uv-erasable)的EPROM,就已经采用用Floating Gate存储数据这一技术了。

图1.典型的Flash内存单元的物理结构

数据在Flash内存单元中是以电荷(electrical charge) 形式存储的。存储电荷的多少,取决于图中的外部门(external gate)所被施加的电压,其控制了是向存储单元中冲入电荷还是使其释放电荷。而数据的表示,以所存储的电荷的电压是否超过一个特定的阈值Vth来表示。

【SLC和MLC的实现机制】

Nand Flash按照内部存储数据单元的电压的不同层次,也就是单个内存单元中,是存储1位数据,还是多位数据,可以分为SLC和MLC:

1.SLC,Single Level Cell:

单个存储单元,只存储一位数据,表示成1或0.

就是上面介绍的,对于数据的表示,单个存储单元中内部所存储电荷的电压,和某个特定的阈值电压Vth,相比,如果大于此Vth值,就是表示1,反之,小于Vth,就表示0.

对于nand Flash的数据的写入1,就是控制External Gate去充电,使得存储的电荷够多,超过阈值Vth,就表示1了。而对于写入0,就是将其放电,电荷减少到小于Vth,就表示0了。

关于为何Nand Flash不能从0变成1,我的理解是,物理上来说,是可以实现每一位的,从0变成1的,但是实际上,对于实际的物理实现,出于效率的考虑,如果对于,每一个存储单元都能单独控制,即,0变成1就是,对每一个存储单元单独去充电,所需要的硬件实现就很复杂和昂贵,同时,所进行对块擦除的 *** 作,也就无法实现之前的,一闪而过的速度了,也就失去了Flash的众多特性了。

2.MLC,Multi Level Cell:

与SLC相对应,就是单个存储单元,可以存储多个位,比如2位,4位等。其实现机制,说起来比较简单,就是,通过控制内部电荷的多少,分成多个阈值,通过控制里面的电荷多少,而达到我们所需要的存储成不同的数据。比如,假设输入电压是Vin=4V(实际没有这样的电压,此处只是为了举例方便),那么,可以设计出2的2次方=4个阈值, 1/4 的Vin=1V,2/4的Vin=2V,3/4的Vin=3V,Vin=4V,分别表示2位数据00,01,10,11,对于写入数据,就是充电,通过控制内部的电荷的多少,对应表示不同的数据。

对于读取,则是通过对应的内部的电流(与Vth成反比),然后通过一系列解码电路完成读取,解析出所存储的数据。这些具体的物理实现,都是有足够精确的设备和技术,才能实现精确的数据写入和读出的。

单个存储单元可以存储2位数据的,称作2的2次方=4 Level Cell,而不是2 Level Cell,这点,之前差点搞晕了。。。,同理,对于新出的单个存储单元可以存储4位数据的,称作 2的4次方=16 Level Cell。

【关于如何识别SLC还是MLC】

Nand Flash设计中,有个命令叫做Read ID,读取ID,意思是读取芯片的ID,就像大家的身份z一样,这里读取的ID中,是读取好几个字节,一般最少是4个,新的芯片,支持5个甚至更多,从这些字节中,可以解析出很多相关的信息,比如此Nand Flash内部是几个芯片(chip)所组成的,每个chip包含了几片(Plane),每一片中的页大小,块大小,等等。在这些信息中,其中有一个,就是识别此flash是SLC还是MLC。下面这个就是最常见的Nand Flash的datasheet中所规定的,第3个字节,3rd byte,所表示的信息,其中就有SLC/MLC的识别信息:

表1.Nand Flash 第3个ID的含义

【Nand Flash的物理存储单元的阵列组织结构】

Nand flash的内部组织结构,此处还是用图来解释,比较容易理解:

图2.Nand Flash物理存储单元的阵列组织结构

上图是K9K8G08U0A的datasheet中的描述。

简单解释就是:

1.一个nand flash由很多个块(Block)组成,块的大小一般是128KB,256KB,512KB,此处是128KB。

2.每个块里面又包含了很多页(page)。每个页的大小,对于现在常见的nand flash多数是2KB,更新的nand flash是4KB,这类的,页大小大于2KB的nand flash,被称作big block,对应的发读写命令地址,一共5个周期(cycle),而老的nand flash,页大小是256B,512B,这类的nand flash被称作small block,。地址周期只有4个。

而块,也是Nand Flash的擦除 *** 作的基本/最小单位。

3.每一个页,对应还有一块区域,叫做空闲区域(spare area)/冗余区域(redundant area),而Linux系统中,一般叫做OOB(Out Of Band),这个区域,是最初基于Nand Flash的硬件特性:数据在读写时候相对容易错误,所以为了保证数据的正确性,必须要有对应的检测和纠错机制,此机制被叫做EDC(Error Detection Code)/ECC(Error Code Correction, 或者 Error Checking and Correcting),所以设计了多余的区域,用于放置数据的校验值。

页是Nand Flash的写入 *** 作的基本/最小的单位。

【Nand Flash数据存储单元的整体架构】

简单说就是,常见的nand flash,内部只有一个chip,每个chip只有一个plane。

而有些复杂的,容量更大的nand flash,内部有多个chip,每个chip有多个plane。这类的nand flash,往往也有更加高级的功能,比如下面要介绍的Multi Plane Program和Interleave Page Program等。

比如,型号为K9K8G08U0A这个芯片(chip),内部有两个K9F4G08U0A,每个K9F4G08U0A包含了2个Plane,每个Plane是1Gb,所以K9F4G08U0A的大小是1Gb×2=2Gb=256MB,因此,K9K8G08U0A内部有2个K9F4G08U0A,即4个Plane,总大小是4×256MB=1GB。

而型号是K9WAG08U1A的nand flash,内部包含了2个K9K8G08U0A,所以,总容量是K9K8G08U0A的两倍=1GB×2=2GB,类似地K9NBG08U5A,内部包含了4个K9K8G08U0A,总大小就是4×1GB=4GB。

【Flash名称的由来】

Flash的擦除 *** 作是以block块为单位的,与此相对应的是其他很多存储设备,是以bit位为最小读取/写入的单位,Flash是一次性地擦除整个块:在发送一个擦除命令后,一次性地将一个block,常见的块的大小是128KB/256KB。。,全部擦除为1,也就是里面的内容全部都是0xFF了,由于是一下子就擦除了,相对来说,擦除用的时间很短,可以用一闪而过来形容,所以,叫做Flash Memory。中文有的翻译为 (快速)闪存。

【Flash相对于普通设备的特殊性】

1. 上面提到过的,Flash最小 *** 作单位,有些特殊。

一般设备,比如硬盘/内存,读取和写入都是以bit位为单位,读取一个bit的值,将某个值写入对应的地址的位,都是可以按位 *** 作的。

但是Flash由于物理特性,使得内部存储的数据,只能从1变成0,这点,可以从前面的内部实现机制了解到,只是方便统一充电,不方便单独的存储单元去放电,所以才说,只能从1变成0,也就是释放电荷。

所以,总结一下Flash的特殊性如下:

表2.Flash和普通设备相比所具有的特殊性

注:

① 之所以将写 *** 作叫做编程,是因为,flash 和之前的EPROM,EEPROM继承发展而来,而之前的EEPROM(Electrically Erasable Programmable Read-Only Memory),往里面写入数据,就叫做编程Program,之所以这么称呼,是因为其对数据的写入,是需要用电去擦除/写入的,就叫做编程。

② 对于目前常见的页大小是2K/4K的Nand Flash,其块的大小有128KB/256KB/512KB等。而对于Nor Flash,常见的块大小有64K/32K等。

③在写数据之前,要先擦除,内部就都变成0xFF了,然后才能写入数据,也就是将对应位由1变成0。

【Nand Flash引脚(Pin)的说明】

图3.Nand Flash引脚功能说明

上图是常见的Nand Flash所拥有的引脚(Pin)所对应的功能,简单翻译如下:

1. I/O0 ~ I/O7:用于输入地址/数据/命令,输出数据

2. CLE:Command Latch Enable,命令锁存使能,在输入命令之前,要先在模式寄存器中,设置CLE使能

3. ALE:Address Latch Enable,地址锁存使能,在输入地址之前,要先在模式寄存器中,设置ALE使能

4. CE#:Chip Enable,芯片使能,在 *** 作Nand Flash之前,要先选中此芯片,才能 *** 作

5. RE#:Read Enable,读使能,在读取数据之前,要先使CE#有效。

6. WE#:Write Enable,写使能, 在写取数据之前,要先使WE#有效。

7. WP#:Write Protect,写保护

8. R/B#:Ready/Busy Output,就绪/忙,主要用于在发送完编程/擦除命令后,检测这些 *** 作是否完成,忙,表示编程/擦除 *** 作仍在进行中,就绪表示 *** 作完成.

9. Vcc:Power,电源

10. Vss:Ground,接地

11. N.C:Non-Connection,未定义,未连接。

[小常识]

 

在数据手册中,你常会看到,对于一个引脚定义,有些字母上面带一横杠的,那是说明此引脚/信号是低电平有效,比如你上面看到的RE头上有个横线,就是说明,此RE是低电平有效,此外,为了书写方便,在字母后面加“#”,也是表示低电平有效,比如我上面写的CE#;如果字母头上啥都没有,就是默认的高电平有效,比如上面的CLE,就是高电平有效。

【为何需要ALE和CLE】

突然想明白了,Nand Flash中, 为何设计这么多的命令,把整个系统搞这么复杂的原因了:

比如命令锁存使能(Command Latch Enable,CLE) 和 地址锁存使能(Address Latch Enable,ALE),那是因为,Nand Flash就8个I/O,而且是复用的,也就是,可以传数据,也可以传地址,也可以传命令,为了区分你当前传入的到底是啥,所以,先要用发一个CLE(或ALE)命令,告诉nand Flash的控制器一声,我下面要传的是命令(或地址),这样,里面才能根据传入的内容,进行对应的动作。否则,nand flash内部,怎么知道你传入的是数据,还是地址,还是命令啊,也就无法实现正确的 *** 作了.

【Nand Flash只有8个I/O引脚的好处】

1.减少外围引脚:相对于并口(Parellel)的Nor Flash的48或52个引脚来说,的确是大大减小了引脚数目,这样封装后的芯片体积,就小很多。现在芯片在向体积更小,功能更强,功耗更低发展,减小芯片体积,就是很大的优势。同时,减少芯片接口,也意味着使用此芯片的相关的外围电路会更简化,避免了繁琐的硬件连线。

2.提高系统的可扩展性,因为没有像其他设备一样用物理大小对应的完全数目的addr引脚,在芯片内部换了芯片的大小等的改动,对于用全部的地址addr的引脚,那么就会引起这些引脚数目的增加,比如容量扩大一倍,地址空间/寻址空间扩大一倍,所以,地址线数目/addr引脚数目,就要多加一个,而对于统一用8个I/O的引脚的Nand Flash,由于对外提供的都是统一的8个引脚,内部的芯片大小的变化或者其他的变化,对于外部使用者(比如编写nand flash驱动的人)来说,不需要关心,只是保证新的芯片,还是遵循同样的接口,同样的时序,同样的命令,就可以了。这样就提高了系统的扩展性。

一、Linux device driver 的概念

系统调用是 *** 作系统内核和应用程序之间的接口,设备驱动程序是 *** 作系统内核和机器硬件之间的接口。设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,应用程序可以象 *** 作普通文件一样对硬件设备进行 *** 作。设备驱动程序是内核的一部分,它完成以下的功能:

1、对设备初始化和释放;

2、把数据从内核传送到硬件和从硬件读取数据;

3、读取应用程序传送给设备文件的数据和回送应用程序请求的数据;

4、检测和处理设备出现的错误。

在Linux *** 作系统下有三类主要的设备文件类型,一是字符设备,二是块设备,三是网络设备。字符设备和块设备的主要区别是:在对字符设备发出读/写请求时,实际的硬件I/O一般就紧接着发生了,块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O *** 作。块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的CPU时间来等待。

已经提到,用户进程是通过设备文件来与实际的硬件打交道。每个设备文件都都有其文件属性(c/b),表示是字符设备还是块设备?另外每个文件都有两个设备号,第一个是主设备号,标识驱动程序,第二个是从设备号,标识使用同一个设备驱动程序的不同的硬件设备,比如有两个软盘,就可以用从设备号来区分他们。设备文件的的主设备号必须与设备驱动程序在登记时申请的主设备号一致,否则用户进程将无法访问到驱动程序。

最后必须提到的是,在用户进程调用驱动程序时,系统进入核心态,这时不再是抢先式调度。也就是说,系统必须在你的驱动程序的子函数返回后才能进行其他的工作。如果你的驱动程序陷入死循环,不幸的是你只有重新启动机器了,然后就是漫长的fsck。

二、实例剖析

我们来写一个最简单的字符设备驱动程序。虽然它什么也不做,但是通过它可以了解Linux的设备驱动程序的工作原理。把下面的C代码输入机器,你就会获得一个真正的设备驱动程序。

由于用户进程是通过设备文件同硬件打交道,对设备文件的 *** 作方式不外乎就是一些系统调用,如 open,read,write,close…, 注意,不是fopen, fread,但是如何把系统调用和驱动程序关联起来呢?这需要了解一个非常关键的数据结构:

STruct file_operatiONs {

int (*seek) (struct inode * ,struct file *, off_t ,int)

int (*read) (struct inode * ,struct file *, char ,int)

int (*write) (struct inode * ,struct file *, off_t ,int)

int (*readdir) (struct inode * ,struct file *, struct dirent * ,int)

int (*select) (struct inode * ,struct file *, int ,select_table *)

int (*ioctl) (struct inode * ,struct file *, unsined int ,unsigned long)

int (*mmap) (struct inode * ,struct file *, struct vm_area_struct *)

int (*open) (struct inode * ,struct file *)

int (*release) (struct inode * ,struct file *)

int (*fsync) (struct inode * ,struct file *)

int (*fasync) (struct inode * ,struct file *,int)

int (*check_media_change) (struct inode * ,struct file *)

int (*revalidate) (dev_t dev)

}

这个结构的每一个成员的名字都对应着一个系统调用。用户进程利用系统调用在对设备文件进行诸如read/write *** 作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。这是linux的设备驱动程序工作的基本原理。既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填充file_operations的各个域。

下面就开始写子程序。

#include <linux/types.h>基本的类型定义

#include <linux/fs.h>文件系统使用相关的头文件

#include <linux/mm.h>

#include <linux/errno.h>

#include <asm/segment.h>

unsigned int test_major = 0

static int read_test(struct inode *inode,struct file *file,char *buf,int count)

{

int left用户空间和内核空间

if (verify_area(VERIFY_WRITE,buf,count) == -EFAULT )

return -EFAULT

for(left = count left >0 left--)

{

__put_user(1,buf,1)

buf++

}

return count

}

这个函数是为read调用准备的。当调用read时,read_test()被调用,它把用户的缓冲区全部写1。buf 是read调用的一个参数。它是用户进程空间的一个地址。但是在read_test被调用时,系统进入核心态。所以不能使用buf这个地址,必须用__put_user(),这是kernel提供的一个函数,用于向用户传送数据。另外还有很多类似功能的函数。请参考,在向用户空间拷贝数据之前,必须验证buf是否可用。这就用到函数verify_area。为了验证BUF是否可以用。

static int write_test(struct inode *inode,struct file *file,const char *buf,int count)

{

return count

}

static int open_test(struct inode *inode,struct file *file )

{

MOD_INC_USE_COUNT模块计数加以,表示当前内核有个设备加载内核当中去

return 0

}

static void release_test(struct inode *inode,struct file *file )

{

MOD_DEC_USE_COUNT

}

这几个函数都是空 *** 作。实际调用发生时什么也不做,他们仅仅为下面的结构提供函数指针。

struct file_operations test_fops = {?

read_test,

write_test,

open_test,

release_test,

}

设备驱动程序的主体可以说是写好了。现在要把驱动程序嵌入内核。驱动程序可以按照两种方式编译。一种是编译进kernel,另一种是编译成模块(modules),如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态的卸载,不利于调试,所以推荐使用模块方式。

int init_module(void)

{

int result

result = register_chrdev(0, "test", &test_fops)对设备 *** 作的整个接口

if (result <0) {

printk(KERN_INFO "test: can't get major number\n")

return result

}

if (test_major == 0) test_major = result/* dynamic */

return 0

}

在用insmod命令将编译好的模块调入内存时,init_module 函数被调用。在这里,init_module只做了一件事,就是向系统的字符设备表登记了一个字符设备。register_chrdev需要三个参数,参数一是希望获得的设备号,如果是零的话,系统将选择一个没有被占用的设备号返回。参数二是设备文件名,参数三用来登记驱动程序实际执行 *** 作的函数的指针。

如果登记成功,返回设备的主设备号,不成功,返回一个负值。

void cleanup_module(void)

{

unregister_chrdev(test_major,"test")

}

在用rmmod卸载模块时,cleanup_module函数被调用,它释放字符设备test在系统字符设备表中占有的表项。

一个极其简单的字符设备可以说写好了,文件名就叫test.c吧。

下面编译 :

$ gcc -O2 -DMODULE -D__KERNEL__ -c test.c –c表示输出制定名,自动生成.o文件

得到文件test.o就是一个设备驱动程序。

如果设备驱动程序有多个文件,把每个文件按上面的命令行编译,然后

ld ?-r ?file1.o ?file2.o ?-o ?modulename。

驱动程序已经编译好了,现在把它安装到系统中去。

$ insmod ?–f ?test.o

如果安装成功,在/proc/devices文件中就可以看到设备test,并可以看到它的主设备号。要卸载的话,运行 :

$ rmmod test

下一步要创建设备文件。

mknod /dev/test c major minor

c 是指字符设备,major是主设备号,就是在/proc/devices里看到的。

用shell命令

$ cat /proc/devices

就可以获得主设备号,可以把上面的命令行加入你的shell script中去。

minor是从设备号,设置成0就可以了。

我们现在可以通过设备文件来访问我们的驱动程序。写一个小小的测试程序。

#include <stdio.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

main()

{

int testdev

int i

char buf[10]

testdev = open("/dev/test",O_RDWR)

if ( testdev == -1 )

{

printf("Cann't open file \n")

exit(0)

}

read(testdev,buf,10)

for (i = 0i <10i++)

printf("%d\n",buf[i])

close(testdev)

}

编译运行,看看是不是打印出全1

以上只是一个简单的演示。真正实用的驱动程序要复杂的多,要处理如中断,DMA,I/O port等问题。这些才是真正的难点。上述给出了一个简单的字符设备驱动编写的框架和原理,更为复杂的编写需要去认真研究LINUX内核的运行机制和具体的设备运行的机制等等。希望大家好好掌握LINUX设备驱动程序编写的方法。


欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/yw/7626654.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-04-07
下一篇2023-04-07

发表评论

登录后才能评论

评论列表(0条)

    保存