文章目录
- 10. ecspi基本知识
- 10.1 spi物理总线
- 10.2 spi时序
- 10.3 spi通信模式
- 10.4 ecspi驱动框架
- 10.5 关键数据结构
- 10.5 ecspi设备驱动
- 10.5.1 ecspi核心函数
- 10.5.2 ecspi同步与互斥
- 10.5.3 spi数据收发流程
10. ecspi基本知识
ecSPI是 Enhanced Configurable SPI 直译为增强可配置SPI,可以理解为是功能更强的SPI接口
ecspi驱动和i2c驱动非常像,它们都是按照总线、设备驱动模型编写。 ecspi总线驱动由NXP官方编写,ecspi设备驱动需要我们根据实际连接的设备编写对应的驱动,例如spi接口的oled屏,spi接口的外置AD转换新芯片等等。
这里主要介绍的是在 linux 下,实现 spi 传输的原理以及涉及到的 api 函数
10.1 spi物理总线
i2c总线和spi总线都可以挂载多个设备,spi支持一主多从,全双工通信,最大速率可达上百MHz。其中四根控制线包括:
- SCK:时钟线,数据收发同步
- MOSI:数据线,主设备数据发送、从设备数据接收
- MISO:数据线,从设备数据发送,主设备数据接收
- NSS、CS:片选信号线
i2c通过i2c设备地址选择通信设备,而spi通过片选引脚选中要通信的设备。
10.2 spi时序
- 起始信号:NSS(输出) 信号线由高变低
- 停止信号:NSS(输出)信号由低变高
- 数据传输:在 SCK的每个时钟周期 MOSI和 MISO同时传输一位数据,高/低位传输没有硬性规定
- 传输单位:8位或16位 , 单位数量:允许无限长的数据传输
10.3 spi通信模式
总线空闲时 SCK 的时钟状态以及数据采样时刻
- 时钟极性 CPOL:指 SPI 通讯设备处于空闲状态时,SCK信号线的电平信号:
- CPOL=0时,SCK在空闲状态时为低电平; CPOL=1时,SCK在空闲状态时为高电平
- 时钟相位 CPHA:数据的采样的时刻:
- CPHA=0时,数据在SCK时钟线的“奇数边沿”被采样; CPHA=1时,数据在SCK时钟线的“偶数边沿”被采样
10.4 ecspi驱动框架
ecspi设备驱动和i2c设备驱动非常相似,可对比学习。
如框架图所示,ecspi可分为spi总线驱动和spi设备驱动。spi总线驱动已经由芯片厂商提供,我们适当了解其实现机制。 而spi设备驱动由我们自己编写,则需要明白其中的原理。spi设备驱动涉及到字符设备驱动、SPI核心层、SPI主机驱动,具体功能如下。
- SPI核心层:提供SPI控制器驱动和设备驱动的注册方法、注销方法、SPI通信硬件无关接口函数。
- SPI主机驱动:主要包含SPI硬件体系结构中适配器(spi控制器)的控制,用于产生SPI 读写时序。
- SPI设备驱动:通过SPI主机驱动与CPU交换数据。
10.5 关键数据结构
这里对整个ecspi驱动框架所涉及的关键数据结构进行整理,可先跳过,后续代码中遇到这些数据结构时再回来看详细定义。
- spi_transfer
在spi设备驱动程序中,spi_transfer结构体用于指定要发送的数据,后面称为传输结构体 :
struct spi_transfer {
/* it's ok if tx_buf == rx_buf (right?)
* for MicroWire, one buffer must be null
* buffers must work with dma_*map_single() calls, unless
* spi_message.is_dma_mapped reports a pre-existing mapping
*/
const void *tx_buf;
void *rx_buf;
unsigned len;
dma_addr_t tx_dma;
dma_addr_t rx_dma;
struct sg_table tx_sg;
struct sg_table rx_sg;
unsigned cs_change:1;
unsigned tx_nbits:3;
unsigned rx_nbits:3;
#define SPI_NBITS_SINGLE 0x01 /* 1bit transfer */
#define SPI_NBITS_DUAL 0x02 /* 2bits transfer */
#define SPI_NBITS_QUAD 0x04 /* 4bits transfer */
u8 bits_per_word;
u16 delay_usecs;
u32 speed_hz;
struct list_head transfer_list;
};
传输结构体的成员较多,需要我们自己设置的很少,这里只介绍我们常用的配置项。
- tx_buf: 发送缓冲区,用于指定要发送的数据地址。
- rx_buf: 接收缓冲区,用于保存接收得到的数据,如果不接收不用设置或设置为NULL.
- len: 要发送和接收的长度,根据SPI特性发送、接收长度相等。
- tx_dma、rx_dma: 如果使用了DAM,用于指定tx或rx DMA地址。
- bits_per_word: speed_hz,分别用于设置每个字节多少位、发送频率。如果我们不设置这些参数那么会使用默认的配置,也就是我初始化spi是设置的参数。
- spi_master
spi_master会在SPI主机驱动中使用到。 spi_controller实际是一个宏,指向spi_controller结构体。 - spi_message
总的来说spi_transfer结构体保存了要发送(或接收)的数据,而在SPI设备驱动中数据是以“消息”的形式发送。 spi_message是消息结构体,我们把它称为消息结构体,发送一个消息分四步, 依次为定义消息结构体、初始化消息结构体、“绑定”要发送的数据(也就是初始化好的spi_transfer结构)、执行发送。
spi_message结构体定义如下所示:
struct spi_message {
struct list_head transfers;
struct spi_device *spi;
unsigned is_dma_mapped:1;
/* completion is reported through a callback */
void (*complete)(void *context);
void *context;
unsigned frame_length;
unsigned actual_length;
int status;
/* for optional use by whatever driver currently owns the
* spi_message ... between calls to spi_async and then later
* complete(), that's the spi_master controller driver.
*/
struct list_head queue;
void *state;
};
spi_message结构体成员我们比较陌生,如果我们不考虑具体的发送细节我们可以不用了解这些成员的含义,因为spi_message的初始化以及“绑定”spi_transfer传输结构体都是由内核函数实现。 唯一要说明的是第二个成员“spi”,它是一个spi_device类型的指针,我们讲解spi_device结构体时说过,一个spi设备对应一个spi_device结构体,这个成员就是用于指定消息来自哪个设备。
10.5 ecspi设备驱动
10.5.1 ecspi核心函数
- ecspi设备的注册和注销函数ecspi设备的注册和注销函数分别在驱动入口和出口函数调用,这与平台设备驱动、I2C设备驱动相同。
ecspi设备注册和注销函数如下:
int spi_register_driver(struct spi_driver *sdrv)
static inline void spi_unregister_driver(struct spi_driver *sdrv)
/*
* 参数:spi spi_driver类型的结构体(spi设备驱动结构体),一个spi_driver结构体就代表了一个ecspi设备驱动
* 成功: 0 失败: 其他任何值都为错误码
*/
对比i2c设备的注册和注销函数,不难发现把“spi”换成“i2c”就是i2c设备的注册和注销函数了,并且用法相同。
- spi_setup()函数
函数设置spi设备的片选信号、传输单位、最大传输速率等,函数中调用spi控制器的成员controller->setup(), 也就是spi_imx->bitbang.master->setup(),在函数spi_imx_probe()中我们将spi_imx_setup赋予该结构体。
int spi_setup(struct spi_device *spi)
/**
* 参数: spi spi_device spi设备结构体
* 成功: 0 失败: 其他任何值都为错误码
**/
- spi_message_init()函数,初始化spi_message
static inline void spi_message_init(struct spi_message *m)
{
memset(m, 0, sizeof *m);
spi_message_init_no_memset(m);
}
/**
* m spi_message 结构体指针, spi_message是消息结构体,我们把它称为消息结构体,发送一个消息分四步,
* 依次为定义消息结构体、初始化消息结构体、“绑定”要发送的数据(也就是初始化好的spi_transfer结构)、执行发送。
**/
- spi_message_add_tail()函数
static inline void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
{
list_add_tail(&t->transfer_list, &m->transfers);
}
/**
* 这个函数很简单就是将将spi_transfer结构体添加到spi_message队列的末尾。
**/
10.5.2 ecspi同步与互斥
- SPI同步传输数据
阻塞当前线程进行数据传输,spi_sync()内部调用__spi_sync()函数,mutex_lock()和mutex_unlock()为互斥锁的加锁和解锁。
int spi_sync(struct spi_device *spi, struct spi_message *message)
{
int ret;
mutex_lock(&spi->controller->bus_lock_mutex);
ret = __spi_sync(spi, message);
mutex_unlock(&spi->controller->bus_lock_mutex);
return ret;
}
//阻塞当前线程,当message发送完成时结束阻塞。
- SPI异步传输数据
int spi_async(struct spi_device *spi, struct spi_message *message)
{
...
ret = __spi_async(spi, message);
...
}
/*在驱动程序中调用async时不会阻塞当前进程,只是把当前message结构体添加到当前spi控制器成员queue的末尾。
然后在内核中新增加一个工作,这个工作的内容就是去处理这个message结构体。*/
10.5.3 spi数据收发流程
该过程涉及到两个重要的结构体两个重要结构体 spi_message 和 spi_transfer
- 申请并初始化 spi_transfer,设置 spi_transfer 的 tx_buf 成员变量, tx_buf 为要发送的数据。然后设置 rx_buf 成员变量, rx_buf 保存着接收到的数据,最后设置 len 成员变量,也就是要进行数据通信的长度。
- 使用 spi_message_init 函数初始化 spi_message。
- 使用spi_message_add_tail函数将前面设置好的 spi_transfer 添加到 spi_message 队列中。
- 使用 spi_sync 函数完成 spi 数据同步传输。
void spi_message_init(struct spi_message *m)
// 初始化 spi_message
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
// 将 spi_transfer 添加到 spi_message 队列中
int spi_sync(struct spi_device *spi, struct spi_message *message)
// 同步传输函数
int spi_async(struct spi_device *spi, struct spi_message *message)
// 异步传输函数