块设备驱动练习—spi nor flash驱动

首先说明一下,在linux当中已经MTD块设备模型可供使用,自己写spi nor flash驱动仅仅为了练习块设备驱动和理解Linux kernel的设计思想。
块设备驱动和spi设备驱动书上已经写得很多了,但是我想换个角度,去掉kernel提供的api的细节,假如我们想实现一个spi nor flash驱动,我们需要做些什么,这部分搞清楚之后,剩下的细节查阅kernel document就可以了。
《块设备驱动练习—spi nor flash驱动》
简单的从图中看一下块设备驱动在kernel中的位置,而从我们具体的驱动来看,我们的驱动从下往上,首先我们需要通过spi接口来访问nor flash,所以他被挂载在spi bus下面,是一个spi device。nor flash是一个典型的块设备,我们需要通过通用块设备层来实现文件系统的访问。

块设备

从上往下看,首先我们需要实现的是块设备的驱动。

  • 首先需要调用register_blkdev,获得设备的主设备号,并在/proc/device中创建入口。
  • 此外还需要填充gendisk结构体,并使用add_disk将其注册到通用块层。gendisk主要包含了以下的这些信息,需要重点关注的会在下面注释里标注出来。
dev->disk = alloc_disk(MINOR_DEVICE); //注意gen_disk需要动态申请
dev->disk->major = dev->major; //register_blkdev申请的主设备号
dev->disk->first_minor = 0; 
dev->disk->fops = &flash_fops;//块设备实现的一些特殊操作,暂没有实现任何功能,比如nor flash,可以用命令清空整个flash
dev->disk->queue = dev->queue;//块设备的request queue
dev->disk->private_data = dev;
  • 完成了这些之后我们来关注一下request queue,个人认为这是整个块设备中最重要的地方。
    • 上层传递给通用块层的请求对应一个bio,每个bio包含的bio_vec描述了该请求的开始扇区,数据方向和数据的页和offset。每个request又由多个页组成。具体的结构关系我们可以看下面这张图:
      《块设备驱动练习—spi nor flash驱动》
    • 想要进行数据读写,必要的工作就是将request queue中的数据一个个取出(这里只针对有IO调度的设备),也就是获得每个bio_vec中的信息,Kernel为我们提供了以下这些API:
      • blk_fetch_request(blk_peek_request+blk_start_request):从一个queue中取出一个request,并将它从队列中移除(也就是说我们开始处理了这个请求,如果发生处理失败需要再将它放回队列中)
      • blk_end_request_all:报告request是否完成,类似的函数还有很多个版本,大家自己查阅吧。
      • rq_for_each_segment,bio_for_each_segment,__rq_for_each_bio:这几个函数都是用来遍历数据结构的,按照名字可以看出来rq_for_each_segment遍历request中的segment,bio_for_each_segment遍历bio中的segment,__rq_for_each_bio遍历request中的bio。
    • 块设备之所以叫块设备,最大的特点就是,不能随意的在任意地址去读取写入,以nor flash为例,擦除的最小单位就是sector,我使用的这片flash,单个sector的大小为4k,那么我们可以理解成,擦除和写入的最小单位就是4k,在写入小于4k大小的数据前,其实需要先读取该sector的数据,完成擦除后再写入整个sector。那么linux为我们提供了以下接口来控制这些参数:
      • blk_queue_max_hw_sectors:表示单个request可以包含的最大sector数量
      • blk_queue_logical_block_size:设备可以寻址的最小块大小,默认是512字节,事实上我们的flash支持按照字节方式进行寻址,这里我填写了flash单个sector的大小4k
      • blk_queue_physical_block_size:设备在不进行read-modify-write操作(也就是之前说的读取整个sector再进行写入)的情况下,最小的寻址大小,同样是4k。
        好了完成了以上操作以后,我们的块设备已经完成了,但是现在他还不能进行读写数据,我们要在request中实现真正的数据读写。

好了完成了以上操作以后,我们的块设备已经完成了,但是现在他还不能进行读写数据,我们要在request中实现真正的数据读写。

spi设备

linux提供了spi子系统以方便我们的开发。spi子系统将主机驱动和外设的驱动分离,主机驱动用来产生波形,而外设驱动使用linux kernel提供的api来访问就可以了。我们使用spi_driver结构体来描述一个spi外设的驱动。

struct spi_driver {
    const struct spi_device_id *id_table;
    int         (*probe)(struct spi_device *spi);
    int         (*remove)(struct spi_device *spi);
    void            (*shutdown)(struct spi_device *spi);
    struct device_driver    driver;
};

spi_drive包含了probe和remove回调,我们需要这里申请spi的发送和接收缓冲,完成spi flash的初始化,并注册块设备驱动.
spi设备的读写操作,大体上可以分为两种方法:

  • spi_transfer&spi_message:单次的spi数据传输在linux kernel中使用spi_transfer进行描述,而一次完整的传输可能包含着多个spi_transfer,并最终通过spi_message组合在一起。此外发起一次spi_message有同步和异步两种方法,同步的api是spi_sync,异步的api是spi_async。使用spi_async可以自己注册回调函数,在传输完成了之后就会调用回调函数。
  • spi_read&spi_write:如果我们的要求比较简单,也可以这两个快捷的API,他们都是同步接口,免去了自己操作的麻烦。此外还有spi_write_and_read接口,可以完成写入后读取的操作,非常方便。

总结时间

通过以上的介绍,我们在spi设备probe和remove的时候注册和注销相应的块设备,在request当中使用spi来进行数据传输,这样这个spi nor flash驱动大体上就算是完成了。当然需要考虑的细节还很多,加入我们使用了devicetree的话还需要修改对应的dts,针对不同的flash总sector数量也应该能够自己设定等等,这些都有待完善,但是作为一个块设备练习他已经是完整的了。
最后说说自己遇到的一些问题吧,其实都是很基础的问题,写下来希望能给大家一些帮助:

  • 和request queue相关的api需要注意api自身是否有请求锁,一般来说不带’__’的会自己请求锁,但是在调用request回调函数的时候,是已经拥有了锁的状态,那么此时一定要调用带下划线的版本。那么不带下划线的版本何时使用呢,因为种种原因,实际的传输操作往往不在request函数中进行,此时锁资源随着request函数退出已经释放,那么我们就需要使用不带下划线的版本,或者是自己处理上锁和解锁。
  • 几个设定queue中block大小,要注意区分(文章开始有讲过了,这里不再做说明):
    • blk_queue_max_hw_sectors
    • blk_queue_logical_block_size
    • blk_queue_physical_block_size
  • request回调函数进入之前请求了spin lock,一定要注意不要在里面睡眠,所以对于spi设备,如果需要使用同步接口,那么具体的数据传输就必须放在进程上下文的中断下半部中处理,比如我是使用了work queue。或者直接采用异步接口,避免睡眠。
  • 由于spi的写入和读取会产生睡眠,所以我用了一个workqueue来进行数据传输,这样是用本来没有问题。但是需要注意的是,request回调并不会等待你的上一个request完成之后再进行下一个请求,而且根据上层的请求不断的进行调用。这样的话,假如在request中进行fetch的话,就需要一个队列将每个request保存下来再进行处理,这不就自己重建一个queue了么,显然是不正确的方法。那么正确方法,每次触发request回调的时候触发一次workqueue,而fetch的动作放在workqueue中去实现,并实现数据传输。
    最后附上一张思维导图:
    《块设备驱动练习—spi nor flash驱动》
点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注