SPI子系统简述
-
SPI为同步全双工串口协议
为了能够为上层提供统一的接口,Linux内核实现SPI子系统,该子系统将底层的实现过程留给驱动工程师来实现,而仅仅提供一个标准的接口函数
-
*1实际上控制器驱动相当于st7789v控制器驱动(提供了寄存器),厂商提供。
-
*2 SPI总线是soc厂商提供的总线驱动。
-
*3我们要做的驱动程序就是使其二者联系起来,并且按照一定的逻辑来编写底层接口,
-
当然在开发过程中linux提供了SPI子系统接口,帮助我们更方便、更规范的去实现。
-
实际上大多数复杂通用的设备都是用对应的框架
-
*SPI子系统驱动中最底层为SPI控制器(实现最基本的读写操作)
-
*实际上驱动开发分为两个部分,一部分由BSP开发工程师来完成SPI控制器驱动的实现(与SOC硬件非常精密)。另一部分那些SPI提供的接口则是给驱动开发工程师来完成具体驱动,例如开发一个网卡驱动。
SPI设备结构体:
struct spi_device{
struct device dev;//该成员为设备文件,一般将其设置为 platform 的dev即可。
u32 max_speed_hz;//最大传输速率,驱动中可用spi_transfer.speed_hz修改
u8 chip_select;//片选线,默认低电平。可以用 SPI_CS_HIGH 来设置。
u32 mode;//数据传输模式。可以通过指定 SPI_LSB_FIRST 来设置。
int irq;//传输时的中断号,该值内核中通过解析设备树来获取
};
- 实际上设备结构体的初始化并不需要驱动开发工程师来完成,因为已经引入设备树。
spi0: spi@1c68000 { compatible = "allwinner,sun8i-h3-spi"; //用来匹配SPI控制器驱动程序 reg = <0x01c68000 0x1000>;//指定spi控制器的寄存器物理地址 interrupts = <GIC_SPI 65 IRQ_TYPE_LEVEL_HIGH>;//指定SPI控制器的中断线为spi第65个中断号, clocks = <&ccu CLK_BUS_SPI0>, <&ccu CLK_SPI0>; clock-names = "ahb", "mod"; //CLK_BUS_SPI0 是 ahb 时钟,CLK_SPI0 是 mod 时钟 dmas = <&dma 23>, <&dma 23>; dma-names = "rx", "tx"; pinctrl-names = "default"; pinctrl-0 = <&spi0_pins>; resets = <&ccu RST_BUS_SPI0>; //resets 属性指定了其复位信号线 status = "disabled"; #address-cells = <1>; #size-cells = <0>; //#address-cells 和#size-cells 指定了其子节点的 reg 列表格式 };
- 此时我们将两个SPI设备挂载在SPI0总线上,ili9341控制器的TFT液晶显示屏和MAX6675热电偶探测器
&spi0 { cs-gpios=<&pio 6 0 GPIO_ACTIVE_LOW>,<&pio 6 1 GPIO_ACTIVE_LOW>;//SPI子系统内部属性,指定了SPI主设备的片选线 status = "okay"; ili9341@0 {//ili9341控制器的TFT液晶显示屏 compatible = "ilitek,ili9341"; reg = <0>;//指定该节点的地址,即第一个设备 spi-max-frequency = <15000000>; spi-cpol; spi-cpha;//是 SPI 子系统内部的特有属性,用来指定 SPI 的工作模式: rotate = <270>; bgr; fps = <10>; buswidth = <8>; reset-gpios = <&pio 1 7 GPIO_ACTIVE_LOW>; dc-gpios = <&pio 1 5 GPIO_ACTIVE_LOW>; debug = <0>; }; max6675@1 {//MAX6675热电偶探测器 compatible = "max,max6675"; reg = <1>;//reg = <1>表示挂载的第二个设备; spi-max-frequency = <1000000>; } };
实例化一个 spi 驱动
spi结构体与platform_driver结构体类似
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;//该成员为设备驱动结构体,保存设备的一些信息
};
static struct spi_driver w25qxx_cdev = {
.driver = {
.name = "w25qxx",
.owner = THIS_MODULE,
},
.probe = w25qxx_probe,
.remove = w25qxx_remove,
.id_table = w25qxx_ids,
};
SPI驱动注册与注销:
ret = spi_register_driver(&w25qxx_cdev);//上述定义的w25qxx_cdev的驱动,对其进行注册
spi_unregister_driver(&w25qxx_cdev);//对注册的驱动进行注销
上面等价于module_spi_driver(w25qxx_cdev);
SPI 数据传输
- Linux将SPI子系统的数据传输抽象为SPI IO模型,该模型类似于客户端通信,我们发送数据被抽象为向客户端发送一条或多条消息的过程。而一条消息又由多个传输包组成。
- 为了将数据在总线传输提供了两种数据结构。
分别是 spi_message 和 spi_transfer(该结构体是 SPI 数据传输的最小单元)struct spi_transfer { const void *tx_buf;//发送数据缓存区 void *rx_buf;//接收数据缓存区 unsigned len;//发送和接收缓存区的大小,发送和缓存区的大小必须相同 u8 bits_per_word;//单次传输数据时的长度,这里的单位为字 struct spi_delay cs_change_delay;//指定传输数据之后延时多久再改变 cs 片选线的状态 };
- 消息事务包含了一个或者多个传输事务,最终数据传输给 SPI 控制器驱动程序的是以消息的方式,因此我们需要知道如何去操作。
struct spi_message { struct list_head transfers;//传输事物 unsigned is_dma_mapped:1;//是否使用dma void (*complete)(void *context);//传输完毕的回调函数 void *context;//回调函数的形参 unsigned frame_length;//传输长度 unsigned actual_length;//实际传输长度 int status;//传输状态报告标志,0为成功 };
- 实现一个完整的数据传输需要将 spi_transfer 添加到 spi_message 中,首先我们需要先初始化消息事务,
//消息初始化: static inline void spi_message_init(struct spi_message *m) //现将 spi_transfer 添加到 spi_message 中 static inline void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m) //通知 SPI 控制器驱动程序来进行数据传输 extern int spi_sync(struct spi_device *spi, struct spi_message *message);//同步传输 extern int spi_async(struct spi_device *spi, struct spi_message *message); 同步传输函数,即此时 CPU 将会在此等待传输结束,因此该函数不能用于中断中 异步传输函数,此时 CPU 发送数据之后会执行其他的,当数据传输完毕后,此时会去调用回调函数