串行外设接口(SPI)是四线总线:MOSI、MISO、串行时钟SCK和片选CS。它常用于连接闪存、AD/DA转换器。主设备生成时钟和管理片选CS,速度可达80MB,远超I2C总线。
SPI设备在内核中表示为struct spi_device{},管理他们的驱动程序的实例是struct spi_driver{}。spi的拓扑结构如下图:
struct spi_device {
struct device dev;
struct spi_master *master; // 表示设备所连接的SPI控制器
u32 max_speed_hz; // 设备的最大时钟频率,可以在传输时用spi_transfer().speed_hz参数修改频率
u8 chip_select; //
u8 bits_per_word;
u16 mode; // 指定是LSB还是MSB,默认是MSB
int irq; //这代表中断号,应该将它传递给request_irq来接收此设备的中断。
[...];
int cs_gpio;
};
spi_driver{}结构体
struct spi_driver {
const struct spi_device_id *id_table;
struct device_driver driver;
int (*probe)(struct spi_device *spi);
int (*remove)(struct spi_device *spi);
int (*shutdown)(struct spi_device *spi);
};
// SPI可以修改CS状态、每字的位数、时钟,其probe函数如下
static int my_probe(struct spi_device *spi) {
int ret;
[...]
spi->mode = SPI_MODE_0;
spi->max_speed_hz = 20000000; // 设备的最大时钟数
spi->bits_per_word = 16; // 每个字的位数
ret = spi_setup(spi);
if (ret < 0) return ret;
[...] // 其他省略的内容
return ret;
}
可以获取对应的struct spi_device_id{}的指针,另外也支持void spi_set_drvdata(struct spi_device *, void *)和void *spi_get_drvdata(spi_device *spi)获取driverdata,使用示例如下:
struct mc33880 {
struct mutext lock;
u8 bar;
struct foo chip;
struct spi_device spi;
};
static int mc33880_probe(struct spi_device *spi) {
struct mc33880 *mc;
[...] // 设备配置
mc = devm_kzalloc(&spi_dev, sizeof(*mc), GFP_KERNEL);
mutex_init(&mc->lock);
spi_set_drvdata(mc);
mc->spi = spi;
[...] // 其他配置
return 0;
}
static int mc33880_remove(struct spi_device *spi) {
struct mc33880 *mc = spi_get_drvdata(spi);
mutex_destroy(&mc->lock);
[...] // 其他需要注销的内容
}
static struct spi_driver mc33880_driver {
.driver = {.name = "", .of_match_table = NULL,},
.probe = mc33880_probe,
.remove = mc33880_remove,
/*...其他省略字段*/
};
// 代替了spi_register_driver(drv), spi_unregister_driver(drv)流程,使用如下:
module_spi_driver(&mc33880_driver);
对于SPI设备,必须使用spi_device_id{}数组以供device_id进行匹配。
struct spi_device_id {
char name[SPI_NAME_SIZE];
kernel_ulong_t driver_data;
};
static struct spi_device_id foo_id_table[] = {
{"foo", 0},{"bar", 1}, {}
};
MODULE_DEVICE_TABLE(spi, foo_id_table);
static struct spi_driver foo_driver = {
.driver = {.name="KBUILD_MODULE"},
.id_table = foo_id_table, .probe = foo_probe, .remove = foo_remove,
};
module_spi_driver(foo_driver);
struct my_platform_data {
int foo; bool bar;
};
static struct my_platform_data mpfd = {
.foo = 15, .bar = true,
};
static struct spi_board_info my_board_spi_board_info[] __initdata = {
{
//modalias 必须与spi设备驱动程序的名称相同
.modalias = "ad7887",
.max_speed_hz = 1000000,
.bus_num = 0,
.irq = GPIO_IRQ(40),
.chip_select = 3,
.platform_data = &mpfd,
.mode = SPI_MOD_3,
},
{
//modalias 必须与spi设备驱动程序的名称相同
.modalias = "spidev",
.max_speed_hz = 1000000,
.bus_num = 1,
.chip_select = 0,
.mode = SPI_MOD_3,
},
};
static int __init board_init(void)
{
[...]
spi_register_board_info(my_board_spi_board_info, 2);
[...]
return 0;
}
spi设备属于DT设备中的非存储器映射设备系列,可以寻址。这里的寻址是分配个控制器的CS片选信号的顺序编号。下面是SPI设备示例:
&ecspi1 {
fsl,spi-num-chipselects = <3>;
cs-gpios = <&gpio5 17 0>,<&gpio5 17 0>,<&gpio5 17 0>;
pinctrl-0 = <&pinctrl_ecspi1 &pinctrl_ecspi_cs>;
#address-cell=<1>;
#size-cell=<0>;
compatible=""fsl,imx6q-ecspi", "fsl,imx51-ecspi";
reg = <0x02008000 0x4000>;
status = "okay";
ad7606r8_0: ad7606r8@0 {
compatible = "ad7606-8";
reg = <0>;
spi-frequency = <10000000>;
interrupt-parent = <&gpio4>;
interrupts = <30 0x0>;
};
label : fake_device@1 {
compatible = "packt,foobar-device";
reg = <1>;
spi-cs-high;
};
mcp2115can: can@0 {
compatible = "microchip,mcp2151";
reg = <2>;
spi-frequency = <10000000>;
interrupt-parent = <&gpio4>;
interrupts = <29 IRQ_TYPE_LEVEL_LOW>;
clocks = <&clk8m>;
};
};
对应的使用上述DT的对应驱动实现方式如下:
static struct of_device_id foobar_of_match[] = {
{.compatible = "packt,foobar-device"},
{.compatible = "packt,foobar-device"},
{},
};
MODULE_DEVICE_TABLE(of, foobar_of_match);
static struct spi_driver foo_driver = {
.driver = { .name="foo",
.of_match_table = of_match_ptr(foobar_of_match)/*此处为DT的match数组*/,},
.probe = foo_probe, .id_table = foo_id_table,
};
static int foo_probe(struct spi_device *spi) {
const struct of_device_id *match;
match = of_match_device(of_match_ptr(foobar_of_match), &spi->dev);
if (match) {
/*of_match相关代码*/
} else {
/*spi_device_id{}配置相关代码*/
}
}
module_spi_driver(foo_driver);
SPI IO模型有一组消息队列组成。在提交若干个spi_message时,这些结构以同步或异步的方式处理完成。单个消息由一个或多个struct spi_transfer{}对象组成,每个对象代表全双工SPI传输。
结构体如下:
struct spi_transfer {
const void *tx_buf; // 缓冲区要写入的数据
void *rx_buf; // 要读取的数据
unsigned len;
dma_addr_t tx_dma; // 当spi_message.is_dma_mapp被设置为1时,使用tx_buf;
dma_addr_t rx_dma;
unsigned cs_change:1;
unsigned tx_nbits:3;
unsigned rx_nbits:3;
u8 bits_per_word;
u16 delay_usecs;
u32 speed_hz;
};
struct spi_message {
struct list_head transfers; // 消息中的transfer按照复工顺序处理
struct spi_device *spi;
struct is_dma_mapped:1;
/*通过回调报告完成情况*/
void (*complete)(void *context);
unsigned frame_length;
unsigned actual_length;
int status;
};
SPI消息传输的主要函数:
// 在消息提交到总线之前,必须先初始化
void spi_message_init(struct spi_message *message);
// 对于要添加到spi_message中的每个spi_transfer,使用如下函数添加
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m);
// 同步传输函数
it spi_sync(struct spi_device *spi, struct spi_message *msg);
// 异步spi传输,异步传输时,所提供的回调函数在传输完成时执行
it spi_sync(struct spi_device *spi, struct spi_message *msg);
// 另外,SPI核心提供了对spi_sync()的包装函数,简化一些少量数据传输的场景
int spi_read(struct spi_device *spi, void *buf, size_t len);
int spi_write(struct spi_device *spi, const void *buf, size_t len);
int spi_write(struct spi_device *spi, const void *txbuf, size_t n_tx,
void *rxbuf, size_t n_rx);
SPI消息传输示例:
struct data{
char buffer[10];
char cmd[2]
int foo
};
struct data my_data[3];
initialized_data(my_data, 3);
struct spi_transfer multi_xfer[3];
struct spi_message msg;
multi_xfer[0].rx_buf = data[0].buffer;
multi_xfer[0].len = 5;
multi_xfer[0].cs_change = 1;
multi_xfer[1].tx_buf = data[1].cmd;
multi_xfer[1]len = 2;
multi_xfer[1].cs_change = 1;
multi_xfer[0].rx_buf = data[0].buffer;
multi_xfer[0].len = 10;
spi_message_init(&msg);
spi_message_add_tail(&multi_xfer[0], &msg);
spi_message_add_tail(&multi_xfer[1], &msg);
spi_message_add_tail(&multi_xfer[2], &msg);
ret = spi_sync(spi_device, &msg);
使用用户模式SPI设备驱动程序,相当于是给设备绑定了标准SPI总线slave驱动程序——驱动程序spidev。在DT中绑定驱动的方式如下:
spidev@0x00 {
compatilble = "spidev";
spi_max_frequency = <800000>;
reg = <0>; // 绑定后会在出现设备文件/dev/spidev0.0供用户空间访问
};
可以调用read/write函数或ioctl()访问spi设备文件,调用read/write时,一次只能读或者写。如果要全双工读和写,则必须使用ioctl。
通过总线ioctl发送数据时,可以使用SPI_IOC_MESSAGE(N)发送请求,提供全双工访问和复合操作。可以参考内核源码中的示例documentation/spi/spidev_test.c。