目录
1 spi子系统整体架构图
2 SPI控制器驱动和SPI设备驱动软件架构
3 SPI控制器驱动的整理流程
4 SPI发送数据过程
5 SPI设备驱动
6spidev万能驱动
7 费曼学习法:我录制了一个SPI子系统驱动框架讲解视频
参考文献:
1 spi子系统整体架构图
如上图所示是我画的一个spi子系统的整体架构图主要分为用户空间,内核空间 和硬件三大部分,
- 用户空间就是我们的应用程序,
- 内核空间又分为设备驱动层、SPI核心层和SPI控制器驱动层,其中设备驱动层就是具体的SPI设备驱动,这个一般是由普通的驱动工程师负责,然后SPI核心层是内核自带的代码,SPI核心层起到一个承上启下的作用,往下给控制器驱动层提供控制器驱动的注册函数,往上提供标准的SPI收发API以及设备注册函数。然后是SPI控制器驱动层,SPI控制器驱动一般是由芯片原厂编写。
- 硬件空间就是具体的硬件,
当我们在应用程序中调用比如write函数的时候,其实调用的就是spi设备驱动注册进去的file_operations结构体里面的write函数,也就是图中的spidev_write函数,这个spidev_write函数进一步调用的就是spi_write函数,这个spi_write函数就就是在SPI核心层定义的,然后spi_write函数进一步调用的就是SPI控制器驱动程序中的spi_sync函数。
2 SPI控制器驱动和SPI设备驱动软件架构
如上图所示是spi控制器和spi设备的软件架构图,左边是SPI控制器驱动软件结构,右边是SPI设备的软件结构,
- SPI控制器驱动,SPI控制器驱动和SPI控制器设备是挂载到platform_bus_type上的,其中SPI控制器是platform_driver驱动,SPI控制器设备是platform_device类型结构体,内核中的of_platform_default_populate(NULL, NULL, parent);函数会解析设备树中的SPI控制器节点,转换成platform_device结构体,然后会将platform_device增加到内核的设备链表中,platform_driver结构体会被注册到内核的驱动链表中,当增加设备或者驱动的时候,会调用platform_bus_type中的match函数,根据compatible属性进行匹配,当匹配成功后,会调用驱动里面的spi_imx_probe函数,在probe函数里面分配一个spi_master结构体,设置spi_master结构体,调用spi_register_master(spi_master_get(master));注册结构体,然后具体在spi_register_master(spi_master_get(master));函数里面首先会用device_add函数将master->dev注册到内核中,然后还会调用of_register_spi_devices(master);这个函数增加spi_device的,增加spi_device的函数是在register master的时候做的。因为像I2C SPI节点下面的子节点都是由I2C SPI来管理的。然后调用spi_master_initialize_queue(master);,这个函数内部是设置了一些传送函数,后面会重点分析spi_master_initialize_queue(master);。
- SPI设备驱动,首先of_register_spi_devices(struct spi_master *master)函数会解析设备树中的SPI下面的子节点,将子节点转换为spi_device,然后增加到spi_bus_type,另外spi_driver驱动也会被注册到spi_bus_type中,当增加设备或者驱动时,会调用spi_bus_type中的match函数,根据compatible属性进行匹配,匹配成功后probe函数就会被调用,然后再probe函数里面会分配、设置、注册一个file_operation结构体,在这个结构体中就包含了具体的设备的读写函数。
3 SPI控制器驱动的整理流程
如上图所示是SPI控制器的驱动,当增加设备或者驱动时,会调用platform_bus_type中match函数,然后根据compatible属性进行匹配,匹配成功之后probe函数被调用,然后再probe函数中其实主要就是做了分配、设置、注册一个结构体,具体来看在probe函数中
- 首先master = spi_alloc_master(&pdev->dev, sizeof(struct spi_imx_data));分配了一个结构体,然后解析设备书中的控制器的相关信息,比如片选,比如寄存器地址,然后把这些东西赋值给master。
- 然后设置了很多函数,比如spi_imx_setupxfer;函数,这个函数其实就是设置发送接收函数,从上图可以看到这个函数经过一系列的调用,最终调用writel(val, spi_imx->base + MXC_CSPITXDATA); 这就是直接写寄存器的函数了。
- 还有比如spi_imx_transfer;;函数,这个函数其实最终调用的就是上面spi_imx_setupxfer设置的发送接收函数。
- 然后调用rspi_bitbang_start(&spi_imx->bitbang);在这个函数里面首先也是设置了一些函数,比如spi_bitbang_transfer_one;,其实这个函数最终的调用的就是前面spi_imx_setupxfer设置的函数,然后在rspi_bitbang_start(&spi_imx->bitbang);函数里面又调用了spi_register_master(spi_master_get(master));函数,在spi_register_master(spi_master_get(master));函数中主要又调用了device_add函数将设备增加到内核中,然后调用了spi_master_initialize_queue(master);接下来专门用一张图重点看一下spi_master_initialize_queue(master);做了什么。
spi_register_master(spi_master_get(master));函数中主要调用了device_add函数将设备增加到内核中,然后调用了spi_master_initialize_queue(master);接下来重点看一下spi_master_initialize_queue(master)做了什么。
- 在spi_master_initialize_queue(master)里面首先是赋值了master->transfer = spi_queued_transfer这个函数,spi_queued_transfer里面其实是一个传送函数,里面首先会把消息添加到queue的队列,然后检查是否需要启动消息处理:如果master->busy为假且need_pump为真,那么使用kthread_queue_work将master->pump_messages添加到工作队列,以便在稍后的某个时间点处理。
- 然后master->transfer_one_message = spi_transfer_one_message,这里是设置了spi_transfer_one_message函数,这个函数进一步看其实就是master->transfer_one(master, msg->spi, xfer);,那也就是上一张图中的master->transfer_one = spi_bitbang_transfer_one;
- 然后spi_init_queue(master);函数, spi_init_queue函数先初始化kthread_worker,为kthread_worker创建一个内核线程来处理work,随后初始化kthread_work,设置work执行函数,work执行函数为spi_pump_messages。
-
然后是spi_start_queue(master)函数,spi_start_queue就相对简单了,只是唤醒该工作线程而已;自此,队列化的相关工作已经完成,系统等待message请求被发起,然后在工作线程中处理message的传送工作。
4 SPI发送数据过程
我们发送数据的时候,最小单位是spi_transfer,我们将发送数据buf赋值给spi_transfer,然后spi_transfer又会被放到spi_message里面,然后调用__spi_sync函数进行发送,然后在__spi_sync函数里面,
- 设置spi_message完成后回调函数,回调函数中仅仅调用complete函数,唤醒等待唤醒的线程。意思就是,当发送spi_message的工作线程完成发送后,唤醒在等待的spi发送完成的spi_sync函数所在的线程。
- 调用__spi_queued_transfer(spi, message, false)函数,这个函数里面就是将数据添加到发送队列中,检查是否需要启动消息处理:如果master->busy为假且need_pump为真,那么使用kthread_queue_work将master->pump_messages添加到工作队列,以便在稍后的某个时间点处理。
- __spi_pump_messages(master, false);函数,__spi_pump_messages(master, false),false意思是标记不在工作线程中执行该函数。调用该函数,把spi_message发出去,该函数中会判断当前状态,如果可以直接发,则直接在当前线程中发送,如果不能直接发,则唤醒工作线程,稍后发送。
- wait_for_completion(&done);,睡眠等待发送完成函数释放的完成信号量,当接收到信号量时,证明发送完成唤醒并结束spi_sync函数。
5 SPI设备驱动
接下来看一下spi设备驱动,首先在前面spi控制器驱动中已经解析了spi子节点的内容转换成了spi_device了,然后我们这里编写spi设备驱动,然后当增加设备或者驱动的时候,会调用spi_bus_type里面的match函数,根据compatible属性进行匹配,匹配成功之后会得调用probe函数,在probe函数里面会注册字符设备,然后字符设备里面就有相应的write函数。
6spidev万能驱动
前面的方式,我们如果想操作一个spi外设,那么由驱动开发人员编写一个spi设备驱动,然后应用开发人员直接使用open/read/write就可以操作相应的spi设备,应用开发人员不需要知道spi设备的具体配置,就把spi设备当成一个文件进行处理。
但是在spidev.c里面的万能驱动采用的是另一种方式,所谓万能驱动,就是我们不需要编写spi_driver,而是在应用程序里面直接操作硬件,这种情况下要求应用编写人员需要去阅读芯片手册,需要知道怎么操作具体的spi外设。
7 费曼学习法:我录制了一个SPI子系统驱动框架讲解视频
根据费曼学习法,把知识点讲出来能够加深对知识点的理解,于是我录制了一个spi子系统驱动框架的讲解视频。
参考文献:
正点原子驱动开发手册
韦东山老师驱动开发大全学习视频
Linux4.9.88内核源码
Linux SPI驱动框架(1)——核心层_linux spi驱动模型_绍兴小贵宁的博客-CSDN博客