跳到主要内容

Linux SPI 设备驱动开发实践

1. SPI 基础知识

SPI(Serial Peripheral Interface)是一种常用的高速全双工同步串行通信总线,广泛应用于 SoC 与各类外设之间的数据传输。SPI 总线支持主从结构,通常由一个主设备(Master)和一个或多个从设备(Slave)组成。主设备通过片选信号选择目标从设备,并提供时钟信号,所有数据传输均由主设备发起。当前 k1 平台的 SPI 仅支持主模式。SPI 通讯协议如下图。


SPI接口包含四根线:

  • 一根串行时钟线(SCK)

  • 一根主设备输出/从设备输入线(MOSI)

  • 一根主设备输入/从设备输出线(MISO)

  • 一根片选线(CS)

当系统中存在多个从设备时,则需要相应的增加片选线的数量。具体而言,MOSI线负责主设备向从设备发送数据,而从设备则通过MISO线向主设备返回数据。串行时钟线(SCK)由主设备控制,用于同步数据的收发。而片选线(CS)则用于选择特定的从设备进行通信,由主设备控制并以其低电平状态为有效信号。

2. Linux SPI 体系结构


Linux SPI 驱动框架分为三层:SPI Core、SPI 控制器驱动(Master 驱动)和 SPI 设备驱动(Slave 驱动)。

SPI core:

  • spi总线和spi_master类注册

  • spi控制器添加和删除

  • spi设备添加和删除

  • spi设备驱动注册与注销

SPI 控制器驱动:

  • spi master控制器驱动,对spi master控制器进行操作

SPI 设备驱动

  • spi device驱动

3. 关键特性

  • 支持SSP/SPI/MicroWire/PSP协议

  • 最高频率支持53MHz, 最低频率支持6.3kbps

4. Linux SPI 驱动架构概述

Linux SPI 驱动主要分为两大类:

SPI 主机驱动(控制器驱动)

  • 负责 SoC 芯片上的 SPI 控制器(主机)的初始化和管理

  • 实现 SPI 总线的底层读写、时序控制等

  • 常见驱动如 spi-dw(DesignWare SPI)、spi-imx

SPI 设备驱动

  • 负责挂载在 SPI 总线上的具体外设(如 Flash、传感器、显示屏等)

  • 通过 SPI 主机驱动提供的接口与硬件通信,实现设备功能

SPI 主机驱动和设备驱动通过内核 SPI 框架解耦,便于不同硬件平台和多种 SPI 设备的灵活组合和扩展。

5. SPI 主机驱动开发

5.1. 核心数据结构

spi_master 抽象表示一个 SPI 控制器,定义在 path/to/your/linux-6.6/include/linux/spi/spi.h 中:

struct spi_master {
struct device dev; /* 设备结构体 */
struct list_head list; /* 链表节点 */

s16 bus_num; /* 总线编号 */
u16 num_chipselect; /* 片选数量 */
u16 dma_alignment; /* DMA对齐要求 */
u16 mode_bits; /* 支持的模式标志位 */
u32 bits_per_word_mask; /* 支持的数据位宽掩码 */

/* 传输速度限制 */
u32 min_speed_hz; /* 最小传输速度 */
u32 max_speed_hz; /* 最大传输速度 */

u16 flags; /* 其他约束标志 */

/* SPI总线锁定机制 */
spinlock_t bus_lock_spinlock;
struct mutex bus_lock_mutex;
bool bus_lock_flag;

/* 关键回调函数 */
int (*setup)(struct spi_device *spi);
int (*transfer)(struct spi_device *spi, struct spi_message *mesg);
int (*transfer_one_message)(struct spi_master *master, struct spi_message *mesg);
// ...其他字段
};

其中 transfertransfer_one_message 是最核心的函数,实现与 SPI 设备的通信。

5.2. spi_master 申请与释放

SPI 主机驱动的核心就是申请 spi_master,然后初始化 spi_master,最后向 Linux 内核注册 spi_master。

/* 申请spi_master */
struct spi_master *spi_alloc_master(struct device *dev, unsigned size);

/* 释放spi_master */
void spi_master_put(struct spi_master *master);

dev:设备,一般是 platform_device 中的 dev 成员变量。 size:私有数据大小,可以通过 spi_master_get_devdata 函数获取。

5.3. spi_master 注册与注销

/* 注册spi_master */
int spi_register_master(struct spi_master *master);

/* 注销spi_master */
void spi_unregister_master(struct spi_master *master);

5.4. SPI 控制器的设备树表示与驱动

path/to/your/linux-6.6/arch/riscv/boot/dts/spacemit/k1-x.dtsi 中定义了4个 SPI 控制器,分别从 spi0 ~ spi3,以 spi3 为例:

spi3: spi@d401c000 {
compatible = "spacemit,k1x-spi";
reg = <0x0 0xd401c000 0x0 0x34>;
k1x,ssp-id = <3>;
k1x,ssp-clock-rate = <51200000>;
dmas = <&pdma0 DMA_SSP3_RX 1
&pdma0 DMA_SSP3_TX 1>;
dma-names = "rx", "tx";
power-domains = <&power K1X_PMU_BUS_PWR_DOMAIN>;
cpuidle,pm-runtime,sleep;
interrupt-parent = <&intc>;
interrupts = <55>;
clocks = <&ccu CLK_SSP3>;
resets = <&reset RESET_SSP3>;
#address-cells = <1>;
#size-cells = <0>;
interconnects = <&dram_range4>;
interconnect-names = "dma-mem";
status = "disabled";
};

通过 compatible 属性值 spacemit,k1x-spi 可以在Linux 源码中搜索这两个字符串即可找到对应的驱动文件。PI-Pro 的 SPI 控制器驱动文件为 path/to/your/linux-6.6/drivers/spi/spi-k1x.c,在文件中有如下内容:

static const struct of_device_id k1x_spi_dt_ids[] = {
{ .compatible = "spacemit,k1x-spi", .data = (void *) K1X_SSP },
{}
};
MODULE_DEVICE_TABLE(of, k1x_spi_dt_ids);

static struct platform_driver driver = {
.driver = {
.name = "k1x-spi",
.pm = &k1x_spi_pm_ops,
.of_match_table = k1x_spi_dt_ids,
},
.probe = k1x_spi_probe,
.remove = k1x_spi_remove,
.shutdown = k1x_spi_shutdown,
};

static int __init k1x_spi_init(void)
{
return platform_driver_register(&driver);
}
module_init(k1x_spi_init);

static void __exit k1x_spi_exit(void)
{
platform_driver_unregister(&driver);
}
module_exit(k1x_spi_exit);

从上述代码可以看出,MUSE Pi Pro 的 SPI 控制器驱动是个标准的 platform 驱动,也就是说,虽然 SPI 总线为别的设备提供了一种总线驱动框架,但是 SPI 控制器却是 platform 驱动。

在设备和驱动匹配成功以后 k1x_spi_probe 函数就会执行,k1x_spi_probe 函数就会完成 SPI 控制器初始化工作。
k1x_spi_probe 函数内容如下 (有省略):

static int k1x_spi_probe(struct platform_device *pdev)
{
struct spi_master *master;
struct spi_driver_data *drv_data;
struct resource *iores;
int status;

// 1. 申请并初始化 spi_master
master = spi_alloc_master(&pdev->dev, sizeof(struct spi_driver_data));
drv_data = spi_master_get_devdata(master);

// 2. 获取寄存器资源并映射
iores = platform_get_resource(pdev, IORESOURCE_MEM, 0);
drv_data->ioaddr = devm_ioremap_resource(&pdev->dev, iores);

// 3. 获取中断号并注册中断处理函数
drv_data->irq = platform_get_irq(pdev, 0);

status = devm_request_irq(&pdev->dev, drv_data->irq, ssp_int,
IRQF_SHARED, dev_name(&pdev->dev), drv_data);

// 4. 获取并配置时钟
drv_data->clk = devm_clk_get(&pdev->dev, NULL);

// 从设备树获取时钟频率
status = of_property_read_u32(pdev->dev.of_node, "k1x,ssp-clock-rate",
&master->max_speed_hz);

clk_set_rate(drv_data->clk, master->max_speed_hz);
clk_prepare_enable(drv_data->clk);

// 5. 复位控制器
drv_data->reset = devm_reset_control_get_optional(&pdev->dev, NULL);

// 6. 配置 spi_master 参数
master->dev.parent = &pdev->dev;
master->dev.of_node = pdev->dev.of_node;
master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH | SPI_LOOP;
master->bits_per_word_mask = SPI_BPW_RANGE_MASK(4, 32);
master->setup = setup;
master->transfer_one_message = k1x_spi_transfer_one_message;
master->auto_runtime_pm = true;

// 7. 初始化硬件寄存器(加载默认配置)
k1x_spi_write(drv_data, TOP_CTRL, TOP_FRF_Motorola | TOP_DSS(8));
k1x_spi_write(drv_data, FIFO_CTRL, FIFO_RxTresh(RX_THRESH_DFLT) |
FIFO_TxTresh(TX_THRESH_DFLT));

// 8. 初始化工作队列和完成量
INIT_WORK(&drv_data->pump_transfers, pump_transfers);
init_completion(&drv_data->cur_msg_completion);

// 9. 注册 spi_master 到内核
platform_set_drvdata(pdev, drv_data);
status = devm_spi_register_master(&pdev->dev, master);
return 0;
}

5.5. SPI 传输函数分析

在 probe 函数中初始化 spi_master,设置 transfer_one_messagek1x_spi_transfer_one_message,最后向 Linux 内核注册 spi_master。k1x_spi_transfer_one_message 是 SPI 控制器与 SPI 设备的核心通信函数。
重点来看 k1x_spi_transfer_one_message 函数,因为最终就是通过此函数来完成与 SPI 设备通信的。内容如下 (有省略):

static int k1x_spi_transfer_one_message(struct spi_master *master,
struct spi_message *msg)
{
struct spi_driver_data *drv_data = spi_master_get_devdata(master);

drv_data->cur_msg = msg;
/* Initial message state*/
drv_data->cur_msg->state = START_STATE;
drv_data->cur_transfer = list_entry(drv_data->cur_msg->transfers.next,
struct spi_transfer,
transfer_list);

/*
* prepare to setup the SSP, in pump_transfers, using the per
* chip configuration
*/
drv_data->cur_chip = spi_get_ctldata(drv_data->cur_msg->spi);

if (master->max_speed_hz != drv_data->cur_transfer->speed_hz) {
master->max_speed_hz = drv_data->cur_transfer->speed_hz;
clk_set_rate(drv_data->clk, master->max_speed_hz);
}

reinit_completion(&drv_data->cur_msg_completion);
/* Mark as busy and launch transfers */
queue_work(system_wq, &drv_data->pump_transfers);
wait_for_completion(&drv_data->cur_msg_completion);

return 0;
}

6. SPI 设备驱动开发

6.1. 核心数据结构

6.1.1. spi_device 结构体

spi_device 结构体描述 SPI 设备的信息,每检测到一个 SPI 设备就会为其分配一个 spi_device 实例。定义在 path/to/your/linux-6.6/include/linux/spi/spi.h 中:

struct spi_device {
struct device dev; /* 设备结构体 */
struct spi_master *master; /* 所属的SPI主机 */
u32 max_speed_hz; /* 最大传输速度 */
u8 chip_select; /* 片选信号 */
u8 bits_per_word; /* 每字数据位数 */
u16 mode; /* SPI模式 */
int irq; /* 中断号 */
void *controller_state;
void *controller_data;
char modalias[SPI_NAME_SIZE]; /* 模块别名 */
// ...其他字段
};

6.1.2. spi_driver 结构体

spi_driver 结构体描述 SPI 设备驱动的信息,类似于 spi_driver,定义在 path/to/your/linux-6.6/include/linux/spi/spi.h 中:

struct spi_driver {
const struct spi_device_id *id_table; /* 传统匹配ID表 */

/* 标准驱动模型接口 */
int (*probe)(struct spi_device *spi);
int (*remove)(struct spi_device *spi);
void (*shutdown)(struct spi_device *spi);

struct device_driver driver; /* 设备驱动结构体 */
// ...其他字段
};

6.2. SPI 设备驱动注册与注销

对于 SPI 设备驱动编写,重点工作就是构建 spi_driver,构建完之后需要向内核注册这个 spi_driver:

/* 注册SPI驱动 */
int __spi_register_driver(struct module *owner, struct spi_driver *sdrv);

/* 注销SPI驱动 */
void spi_unregister_driver(struct spi_driver *sdrv);

6.3. SPI 数据传输函数

在 SPI 设备驱动的 probe 函数里面需要与 SPI 设备进行通信,这里就要用到 spi_sync 等传输函数:

/* 初始化spi_message */
void spi_message_init(struct spi_message *m);

/* 添加spi_transfer到spi_message的transfer列表 */
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m);

/* 同步传输函数 */
int spi_sync(struct spi_device *spi, struct spi_message *message);

/* 异步传输函数 */
int spi_async(struct spi_device *spi, struct spi_message *message);

/* 简化的读写函数 */
int spi_write(struct spi_device *spi, const void *buf, size_t len);
int spi_read(struct spi_device *spi, void *buf, size_t len);

6.3. SPI 消息结构体

SPI 传输是以消息(message)为单位进行的,一个消息可以包含多个传输(transfer):

struct spi_message {
struct list_head transfers; /* 传输列表 */
struct spi_device *spi; /* 目标SPI设备 */
unsigned is_dma_mapped:1; /* 是否已进行DMA映射 */
void (*complete)(void *context); /* 完成回调函数 */
void *context; /* 回调函数参数 */
unsigned actual_length; /* 实际传输长度 */
int status; /* 传输状态 */
// ...其他字段
};

struct spi_transfer {
const void *tx_buf; /* 发送缓冲区 */
void *rx_buf; /* 接收缓冲区 */
unsigned len; /* 传输长度 */
dma_addr_t tx_dma; /* 发送DMA地址 */
dma_addr_t rx_dma; /* 接收DMA地址 */
unsigned cs_change:1; /* 传输后是否改变片选 */
unsigned tx_nbits:4; /* 发送时的数据线数 */
unsigned rx_nbits:4; /* 接收时的数据线数 */
u8 bits_per_word; /* 每字数据位数 */
u32 speed_hz; /* 传输速度 */
struct list_head transfer_list; /* 传输链表节点 */
// ...其他字段
};

6.4. SPI 设备驱动开发流程

此处结合 SPI OLED 模块驱动程序进行说明。

6.4.1. 定义匹配表

/* 设备树匹配列表 */
static const struct of_device_id spi_oled_of_match[] = {
{ .compatible = "ssd1106,oled" },
{ }
};
MODULE_DEVICE_TABLE(of, spi_oled_of_match);

6.4.2. 定义设备结构体

struct spi_oled_dev {
struct spi_device *spi; // SPI设备指针
struct cdev cdev; // 字符设备
struct device *device; // 设备节点
struct class *class; // 设备类
dev_t devt; // 设备号
int dc_gpio; // DC控制GPIO
struct mutex lock; // 互斥锁
};

6.4.3. 实现文件操作接口

static const struct file_operations spi_oled_fops = {
.owner = THIS_MODULE,
.open = spi_oled_open,
.release = spi_oled_release,
.write = spi_oled_write,
.unlocked_ioctl = spi_oled_ioctl,
};

static long spi_oled_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct spi_oled_dev *dev = file->private_data;
int ret = 0;
u8 data;

switch (cmd) {
case SPI_OLED_SET_DC_HIGH:
gpio_set_value(dev->dc_gpio, 1);
break;

case SPI_OLED_SET_DC_LOW:
gpio_set_value(dev->dc_gpio, 0);
break;

case SPI_OLED_WRITE_CMD:
if (copy_from_user(&data, (void __user *)arg, 1))
return -EFAULT;

mutex_lock(&dev->lock);
gpio_set_value(dev->dc_gpio, 0); // DC=0 for command
udelay(10);
ret = spi_write(dev->spi, &data, 1);
mutex_unlock(&dev->lock);
break;

case SPI_OLED_WRITE_DATA:
if (copy_from_user(&data, (void __user *)arg, 1))
return -EFAULT;

mutex_lock(&dev->lock);
gpio_set_value(dev->dc_gpio, 1); // DC=1 for data
udelay(10);
ret = spi_write(dev->spi, &data, 1);
mutex_unlock(&dev->lock);
break;

default:
return -ENOTTY;
}

return ret;
}

6.4.4. 实现 probe/remove 等回调函数

static int spi_oled_probe(struct spi_device *spi)
{
struct spi_oled_dev *dev;
int ret;

/* 1、分配设备结构体内存 */
dev = devm_kzalloc(&spi->dev, sizeof(*dev), GFP_KERNEL);
dev->spi = spi;
spi_set_drvdata(spi, dev);

/* 2、获取并配置GPIO资源 */
dev->dc_gpio = of_get_named_gpio(spi->dev.of_node, "dc-gpios", 0);

ret = devm_gpio_request_one(&spi->dev, dev->dc_gpio,
GPIOF_OUT_INIT_LOW, "spi-oled-dc");

/* 3、配置SPI参数 */
spi->mode = SPI_MODE_0;
spi->bits_per_word = 8;
ret = spi_setup(spi);

/* 4、初始化互斥锁 */
mutex_init(&dev->lock);

/* 5、构建设备号 */
ret = alloc_chrdev_region(&dev->devt, 0, 1, DEVICE_NAME);

/* 6、注册字符设备 */
cdev_init(&dev->cdev, &spi_oled_fops);
dev->cdev.owner = THIS_MODULE;

ret = cdev_add(&dev->cdev, dev->devt, 1);

/* 7、创建设备类 */
dev->class = class_create(CLASS_NAME);

/* 8、创建设备节点 */
dev->device = device_create(dev->class, &spi->dev, dev->devt,
NULL, DEVICE_NAME);
return 0;
}

static void spi_oled_remove(struct spi_device *spi)
{
struct spi_oled_dev *dev = spi_get_drvdata(spi);

/* 销毁设备和类 */
device_destroy(dev->class, dev->devt);
class_destroy(dev->class);

/* 注销字符设备 */
cdev_del(&dev->cdev);
unregister_chrdev_region(dev->devt, 1);

dev_info(&spi->dev, "SPI OLED driver removed\n");
}

6.4.5. 构建 spi_driver 结构体

static struct spi_driver spi_oled_driver = {
.driver = {
.name = "spi-oled",
.of_match_table = spi_oled_of_match,
},
.probe = spi_oled_probe,
.remove = spi_oled_remove,
};

7. SPI 设备驱动开发实例

说明:本次实验采用 GPIO75 (SPI3_SCLK) 、GPIO76 (SPI3_CS) 、GPIO77 (SPI3_MOSI) 、GPIO49 (SPI3_DC) 与 7 针 OLED 进行 SPI 通信。

7.1. 设备树配置

本次实验我们使用GPIO75、76、77、78 (spi3) 与 OLED 模块进行通信。 打开设备树 path/to/your/linux-6.6/arch/riscv/boot/dts/spacemit/k1-x_pinctrl.dtsi 查看引脚复用功能配置,默认已复用 SPI 功能,内容如下:

pinctrl_ssp3_0: ssp3_0_grp {
pinctrl-single,pins = <
K1X_PADCONF(GPIO_75, MUX_MODE2, (EDGE_NONE | PULL_DIS | PAD_3V_DS4)) /* ssp3_sclk */
K1X_PADCONF(GPIO_76, MUX_MODE2, (EDGE_NONE | PULL_UP | PAD_3V_DS4)) /* ssp3_frm */
K1X_PADCONF(GPIO_77, MUX_MODE2, (EDGE_NONE | PULL_DIS | PAD_3V_DS4)) /* ssp3_txd */
K1X_PADCONF(GPIO_78, MUX_MODE2, (EDGE_NONE | PULL_DIS | PAD_3V_DS4)) /* ssp3_rxd */
>;
};

使用设备树的时候 SPI 设备信息通过创建相应的节点就行了,所以我们在 spi3 节点下创建子节点,然后在这个子节点内描述 OLED 这个模块的相关信息。打开 path/to/your/linux-6.6/arch/riscv/boot/dts/spacemit/k1-x_MUSE-Pi-Pro.dts 这个设备树文件,然后找到如下内容:

&spi3 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ssp3_0>;
k1x,ssp-disable-dma;
status = "okay";
k1x,ssp-clock-rate = <25600000>;
};

在 spi3 节点下进行配置,如下:

&spi3 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ssp3_0>;
k1x,ssp-disable-dma;
status = "okay";
k1x,ssp-clock-rate = <25600000>;

oled@0 {
compatible = "ssd1315,oled";
reg = <0>;
spi-max-frequency = <25600000>;
dc-gpios = <&gpio 49 0>; // 标准属性名
status = "okay";
};
};

启动 Docker。

docker start cross-build-user
docker exec -it cross-build-user bash

进入 linux-6.6 目录重新编译 dtb 文件。

cd cross-builder/linux-6.6
make dtbs

在 MUSE Pi Pro 上执行下面命令查看 ip:

ifconfig

如下图所示,ip 为 10.0.91.67


执行下面命令将编译好的 k1-x_MUSE-Pi-Pro.dtb 文件传输到 MUSE Pi Pro 板子上。

:下面的 bianbu@10.0.91.67 根据上面结果替换成对应的 ip 。

sudo scp arch/riscv/boot/dts/spacemit/k1-x_MUSE-Pi-Pro.dtb bianbu@10.0.91.67:/home/bianbu

首次通过 ssh 连接 MUSE Pi Pro 会弹出连接确认,键盘输入 yes ,接着再输入密码回车即可。


在 MUSE Pi Pro 上执行以下命令进行替换:

sudo cp ~/k1-x_MUSE-Pi-Pro.dtb /boot/spacemit/6.6.63

将 MUSE Pi Pro 进行重启:

sudo reboot

使用新的设备树启动 Linux 内核之后,可以通过查看设备树在内核中的实际挂载情况来验证 oled@0 子节点是否已经被正确添加。执行以下命令:

ls /proc/device-tree/soc/spi@d401c000

如果设备树节点被内核正确解析正确的话,能看到 oled@0 目录,如图所示:


7.2. 运行和测试

7.2.1. 编译驱动程序

下载源码压缩包

spi_test.zip

补充:源码文件具体作用请看压缩包里的 README.md 文件。

解压

unzip 08_spi_test.zip -d ~/

编译

驱动文件需要在Ubuntu上进行编译,因为SDK在Ubuntu上。修改Makefile里的内核路径为实际路径。

cd ~/08_spi_test
vim Makefile

修改KERN_DIR 的路径,如图所示:


执行以下命令,编译驱动

make

编译成功以后就会生成一个名为 spi_drv.ko 的驱动模块文件。


7.2.2. 加载驱动

将上一小节编译好的 08_spi_test 文件夹拷贝到 MUSE Pi Pro 开发板上,执行下面命令。

:下面的 bianbu@10.0.91.67 根据上面结果替换成对应的 ip 。

scp -r ~/08_spi_test/ bianbu@10.0.91.67:/home/bianbu

在开发板上进入该目录并输入如下命令加载 spi_drv.ko 这个驱动模块。

cd 08_spi_test
sudo insmod spi_drv.ko

7.2.3. 硬件连接

将 MUSE Pi Pro 与 OLED 模块进行硬件连接。


具体说明:

GND  ————  GND
DO     ————  SPI3_SCLK_3V3
DI       ————  SPI3_MOSI_3V3
RES    ————  浮空 (不接)
DC      ————  GPIO_49_3V3
CS       ————  SPI3_CS_3V3

7.2.4. 运行测试

当驱动模块加载成功以后使用 spi_test 来测试,输入如下命令:

sudo ./spi_test /dev/spi_oled

应用程序会发送 ABC 到 OLED 模块,然后显示出来,如图所示: