spi驱动总线和I2C驱动差不多。只不过对应的API不一样,使用的大致流程都是一样的。
linux下的spi驱动不支持热插拔(i2c同样不支持,usb、hdmi支持热插拔)
SPI总线一般四根线:
SCK:串行时钟,由SPI总线的主机提供,用于同步通信。
CS:一个spi总线上cs可以有多个,主机通过cs引脚控制不同的从机,cs为低电平的时候表示从机被选中,从而主机和被选中的从机通信。同一时刻一般只有从机的cs引脚被置低。
MOSI:主机输出、从机输入。数据由主机向从机发送。
MISO:主机输入、从机输出。数据由从机向主机发送。
时钟极性和时钟相位:
在SPI中,主机可以选择时钟极性和时钟相位。
时钟极性CPOL(Clock Polarity):是用来配置SCLK的电平处于哪种状态时有效。
CPOL=0:表示高电平有效,低电平处于空闲态。
CPOL=1:表示低电平有效,高电平处于空闲态。
时钟相位CPHA(Clock Phase):是用来配置数据采样是在第几个边沿,0表示第一个边沿(前沿Leading edge),1表示第二个边沿(后沿Trailing edge)。
CPHA=0:表示数据采样是在第1个边沿,数据发送在第2个边沿。
CPHA=1:表示数据采样是在第2个边沿,数据发送在第1个边沿。
主机需要根据从机的要求选择时钟极性和时钟相位,也即从机的传输模式决定了主机的传输模式。故先要了解从机的SPI是何种模式,然后再将主机的SPI的模式设置成同样的模式,即可正常通讯。根据CPOL和CPHA位的选择,有四种SPI模式可用。
SPI_SOC驱动
同样的,这部分不同的SOC的SPI控制寄存器不一样,驱动实现的方式也不同,这一般是由原厂去实现。如果只是使用SPI驱动其他外设,只需要知道会用几个API即可,此部分一般和SOC使用无关。
//spi_master申请与释放
struct spi_master *spi_alloc_master(struct device *dev,unsigned size);
void spi_master_put(struct spi_master *master);
//spi_master的注册与注销
int spi_register_master(struct spi_master *master);
void spi_unregister_master(struct spi_master *master);
SPI 设备驱动
该部分才是使用SOC的spi驱动的重点。
/* probe 函数 */
static int xxx_probe(struct spi_device *spi)
{
/* 具体函数内容 */
return 0;
}
/* remove 函数 */
static int xxx_remove(struct spi_device *spi)
{
/* 具体函数内容 */
return 0;
}
/* 传统匹配方式 ID 列表 */
static const struct spi_device_id xxx_id[] = {
{"xxx", 0},
{}
};
/* 设备树匹配列表 */
static const struct of_device_id xxx_of_match[] = {
{ .compatible = "xxx" },
{ /* Sentinel */ }
};
/* SPI 驱动结构体 */
static struct spi_driver xxx_driver = {
.probe = xxx_probe,
.remove = xxx_remove,
.driver = {
.owner = THIS_MODULE,
.name = "xxx",
.of_match_table = xxx_of_match,
},
.id_table = xxx_id,
};
/* 驱动入口函数 */
static int __init xxx_init(void)
{
return spi_register_driver(&xxx_driver);
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
spi_unregister_driver(&xxx_driver);
}
module_init(xxx_init);
module_exit(xxx_exit);
I.MX6U 来讲,SPI 主机的最终数据收发函数为 spi_imx_transfer
再偷个懒,复制一段正点原子的代码。可以如果需要自己写spi读写函数时可以仿照写。
/*
* @description : 从icm20608读取多个寄存器数据
* @param - dev: icm20608设备
* @param - reg: 要读取的寄存器首地址
* @param - val: 读取到的数据
* @param - len: 要读取的数据长度
* @return : 操作结果
*/
static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
{
int ret = -1;
unsigned char txdata[1];
unsigned char * rxdata;
struct spi_message m;
struct spi_transfer *t;
struct spi_device *spi = (struct spi_device *)dev->private_data;
t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL); /* 申请内存 */
if(!t) {
return -ENOMEM;
}
rxdata = kzalloc(sizeof(char) * len, GFP_KERNEL); /* 申请内存 */
if(!rxdata) {
goto out1;
}
/* 一共发送len+1个字节的数据,第一个字节为
寄存器首地址,一共要读取len个字节长度的数据,*/
txdata[0] = reg | 0x80; /* 写数据的时候首寄存器地址bit8要置1 */
t->tx_buf = txdata; /* 要发送的数据 */
t->rx_buf = rxdata; /* 要读取的数据 */
t->len = len+1; /* t->len=发送的长度+读取的长度 */
spi_message_init(&m); /* 初始化spi_message */
spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
ret = spi_sync(spi, &m); /* 同步发送 */
if(ret) {
goto out2;
}
memcpy(buf , rxdata+1, len); /* 只需要读取的数据 */
out2:
kfree(rxdata); /* 释放内存 */
out1:
kfree(t); /* 释放内存 */
return ret;
}
/*
* @description : 向icm20608多个寄存器写入数据
* @param - dev: icm20608设备
* @param - reg: 要写入的寄存器首地址
* @param - val: 要写入的数据缓冲区
* @param - len: 要写入的数据长度
* @return : 操作结果
*/
static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, u8 len)
{
int ret = -1;
unsigned char *txdata;
struct spi_message m;
struct spi_transfer *t;
struct spi_device *spi = (struct spi_device *)dev->private_data;
t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL); /* 申请内存 */
if(!t) {
return -ENOMEM;
}
txdata = kzalloc(sizeof(char)+len, GFP_KERNEL);
if(!txdata) {
goto out1;
}
/* 一共发送len+1个字节的数据,第一个字节为
寄存器首地址,len为要写入的寄存器的集合,*/
*txdata = reg & ~0x80; /* 写数据的时候首寄存器地址bit8要清零 */
memcpy(txdata+1, buf, len); /* 把len个寄存器拷贝到txdata里,等待发送 */
t->tx_buf = txdata; /* 要发送的数据 */
t->len = len+1; /* t->len=发送的长度+读取的长度 */
spi_message_init(&m); /* 初始化spi_message */
spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
ret = spi_sync(spi, &m); /* 同步发送 */
if(ret) {
goto out2;
}
out2:
kfree(txdata); /* 释放内存 */
out1:
kfree(t); /* 释放内存 */
return ret;
}
另外补充一点,在linux/spi/spi.h文件中,存在封装好的读写函数。如下:
/**
* spi_write - SPI synchronous write
* @spi: device to which data will be written
* @buf: data buffer
* @len: data buffer size
* Context: can sleep
*
* This writes the buffer and returns zero or a negative error code.
* Callable only from contexts that can sleep.
*/
static inline int spi_write(struct spi_device *spi, const void *buf, size_t len)
{
struct spi_transfer t = {
.tx_buf = buf,
.len = len,
};
struct spi_message m;
spi_message_init(&m);
spi_message_add_tail(&t, &m);
return spi_sync(spi, &m);
}
/**
* spi_read - SPI synchronous read
* @spi: device from which data will be read
* @buf: data buffer
* @len: data buffer size
* Context: can sleep
*
* This reads the buffer and returns zero or a negative error code.
* Callable only from contexts that can sleep.
*/
static inline int spi_read(struct spi_device *spi, void *buf, size_t len)
{
struct spi_transfer t = {
.rx_buf = buf,
.len = len,
};
struct spi_message m;
spi_message_init(&m);
spi_message_add_tail(&t, &m);
return spi_sync(spi, &m);
}
/* this copies txbuf and rxbuf data; for small transfers only! */
extern int spi_write_then_read(struct spi_device *spi,
const void *txbuf, unsigned n_tx,
void *rxbuf, unsigned n_rx);
另外注意读写还可以使用spi_write_then_read,但是上面的注释写的是用于数据量较少时的传输,这一点我也没有去注意具体多大的数据量会出问题,如果有人知道还往指点一下。
所以最后可以简化读写函数为
目录
/*
* @description : 从icm20608读取多个寄存器数据
* @param - dev: icm20608设备
* @param - reg: 要读取的寄存器首地址
* @param - val: 读取到的数据
* @param - len: 要读取的数据长度
* @return : 操作结果
*/
static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
{
u8 data=0;
struct spi_device *spi = (struct spi_device *)dev->private_data;
data = reg | 0x80;
spi_write_then_read(spi, &data, 1, buf, len); //读寄存器数据,先写入地址再读
return 0;
}
/*
* @description : 向icm20608多个寄存器写入数据
* @param - dev: icm20608设备
* @param - reg: 要写入的寄存器首地址
* @param - val: 要写入的数据缓冲区
* @param - len: 要写入的数据长度
* @return : 操作结果
*/
static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, u8 len)
{
u8 *txdata;
struct spi_device *spi = (struct spi_device *)dev->private_data;
txdata = kzalloc(len+1, GFP_KERNEL); //申请内存
txdata[0] = reg & ~0x80; //起始地址保存要写的寄存器地址
memcpy(&txdata[1], buf, len); //要发送的数据拷贝
spi_write(spi, txdata, len+1);
kfree(txdata);
return 0;
}
另外补充一点。对于imx6ul来说,原厂编写驱动用得是软件片选,也就是将片选引脚配置未gpio,在原厂得驱动里面去控制得。所以要注意设备树里面不能把片选引脚配置未SS0,配置为gpio功能即可。
在读写函数时会拉住片选信号。
[OK] spi_write_then_read(spi, &data, 1, buf, len); //读寄存器数据,先写入地址再读
[ERR] 下面这样实现读函数会有问题,因为在write完了之后,片选会释放,在读得时候会出错。
我们应该要做的是,写之前拉住,直到读完之后才释放才正确
//spi_write(spi, .....);
//spi_read(spi,.....);