SPI子系统驱动框架
1、Linux SPI驱动框架

除开硬件和用户态应用程序,由上到下分成3层:
设备驱动层: spi框架使用者(应用驱动工程师开发)
核心层:spi框架搭建者 (原厂工程师开发)
控制器驱动层:spi框架适配者(原厂工程师开发)
1.1 spi设备驱动层
SPI 设备驱动层的主要作用是编写驱动程序,使 SPI 外设能够正常工作。它创建了对应的设备节点,提供了标准化的接口,使得上层应用程序能够方便地与 SPI 设备进行交互。具体来说,SPI 设备驱动层包含以下几个关键部分:
spi_device 代表一个连接到 SPI 总线上的从设备,包含从设备的地址、所属的 SPI 主设备等信息。
/dev/spiX 设备节点。为上层应用程序提供设备访问的接口;通过打开/读写/控制设备节点,应用程序可以与 SPI 设备进行交互;内核 SPI 子系统负责将应用程序的操作转发到对应的 spi_driver。
spi_driver 实现了具体 SPI 从设备的驱动程序。负责设备的初始化、读写、配置等操作;通过 spi_device 与设备进行交互;向上层提供设备访问的标准化接口。
总结:比如说我有一个LCD或者是传感器要用到spi总线,驱动工程师就可以在spi设备驱动层来编写相应的传感器等外设的driver,这些driver会用到核心层提供的API,借助这些API来完成读写等实际上的对SPI的操作。接下来会介绍设备驱动层的API。
1.2 SPI设备驱动层API(先去看数据结构)
1.2.1 spi_message_init
函数原型:
static inline void spi_message_init(struct spi_message *m)
{
memset(m, 0, sizeof *m);
spi_message_init_no_memset(m);
}
在使用spi_message之前需要对其进行初始化。
1.2.2 spi_message_add_tail
函数原型:
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m);
spi_message初始化完成以后需要将 spi_transfer 添加到 spi_message 队列中
1.2.3 spi_async(异步)
函数原型:
int spi_async(struct spi_device *spi, struct spi_message *message)
{
struct spi_controller *ctlr = spi->controller;
int ret;
unsigned long flags;
ret = spi_maybe_optimize_message(spi, message);
if (ret)
return ret;
spin_lock_irqsave(&ctlr->bus_lock_spinlock, flags);
if (ctlr->bus_lock_flag)
ret = -EBUSY;
else
ret = __spi_async(spi, message);
spin_unlock_irqrestore(&ctlr->bus_lock_spinlock, flags);
return ret;
}
spi_async 和 spi_sync 这两个函数是 Linux SPI 驱动中用来向底层硬件真正下发数据传输命令的两个核心 API
spi_async 发起数据传输。其实就是调用控制器内部的master->transfer。既然是async那就是异步执行的,不会等待传输是否完成,就直接返回。假如master->transfer控制器这边没有去实现。内核会自己填充默认的transfer函数spi_queued_transfer.
1.2.3.1 spi_queued_transfer
spi_queued_transfer(struct spi_device *spi, struct spi_message *msg) {
__spi_queued_transfer(spi, msg, true);
}
static int __spi_queued_transfer(struct spi_device *spi,
struct spi_message *msg,
bool need_pump) {
struct spi_master *master = spi->master;
unsigned long flags;
spin_lock_irqsave(&master->queue_lock, flags);
…
list_add_tail(&msg->queue, &master->queue);
if (!master->busy && need_pump)
kthread_queue_work(&master->kworker, &master->pump_messages);
spin_unlock_irqrestore(&master->queue_lock, flags);
return 0;
}
可以看到默认的spi_queued_transfer就是去把message添加到master(控制器)的发送链表中。接下来就交给工作服务线程__spi_pump_messages去处理message。
1.2.4 spi_sync(同步)
函数原型:
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;
}
同理,spi_sync就是用来同步传输 spi_message, 完成传输后会调用spi_comlete唤醒等待的线程。
那假如master->transfer控制器这边没有去实现。内核也会自己填充默认的transfer函数spi_queued_transfer.可以看到need_pump=false,因此还是一样就是去把message添加到master的发送链表中。
然后调用__spi_pump_messages,也就是工作线程服务,交给工作服务线程去处理message。
最后调用wait_for_completion去等待传输完成
1.2.5 spi_register_driver
向 Linux 内核的 SPI 总线注册你写的驱动程序,和平台总线模型中的注册驱动差不多,只不过这是专门为 SPI 总线(spi_bus_type) 服务的
函数原型:
#define spi_register_driver(driver) \\
__spi_register_driver(THIS_MODULE, driver)
spi_register_driver是__spi_register_driver的第一个参数固定为THIS_MODULE的define实际上还是调用driver_register这个函数。
1.2.6 spi_unregister_driver
向 Linux 内核的 SPI 总线卸载你写的驱动程序
static inline void spi_unregister_driver(struct spi_driver *sdrv)
{
if (sdrv)
driver_unregister(&sdrv->driver);
}
1.2.7 spi_read/spi_write
函数原型:
static inline int spi_read(struct spi_device *spi, void *buf, size_t len) {
struct spi_transfert = {
.rx_buf= buf,
.len= len,
};
return spi_sync_transfer(spi, &t, 1);
}
static inline int spi_write(struct spi_device *spi, const void *buf, size_t len)
{
struct spi_transfert = {
.tx_buf= buf,
.len= len,
};
return spi_sync_transfer(spi, &t, 1);
}
spi_read, spi_write本质还是调用spi_sync传输函数,spi_sync_transfer只不过是做了一层封装把spi_transfrer和spi_message包装好了。
总结:这是我们开发工程师接触到的最多的一层,主要是用来写外设的驱动的,对上提供设备节点,和open,read等接口,使用的是下层核心层提供的接口API来实现这些功能。
举个例子来说明spi设备驱动层是干什么的吧
例如:
标准操作流程(适用于所有复杂场景)
这是最正宗、最严谨的 SPI 数据发送方式。假设你要读取一个传感器的寄存器(先发 1 字节寄存器地址,再读 1 字节数据):
/* 在你的 probe 或自定义函数里 */
struct spi_message msg;
struct spi_transfer xfers[2]; // 准备两个快递盒
u8 tx_cmd = 0x80; // 假设 0x80 是读命令
u8 rx_data = 0;
int ret;
/* 1. 初始化快递车 */
spi_message_init(&msg);
memset(xfers, 0, sizeof(xfers));
/* 2. 打包第一个快递盒:只发命令 */
xfers[0].tx_buf = &tx_cmd;
xfers[0].len = 1;
spi_message_add_tail(&xfers[0], &msg); // 装车
/* 3. 打包第二个快递盒:只收数据 */
xfers[1].rx_buf = &rx_data;
xfers[1].len = 1;
spi_message_add_tail(&xfers[1], &msg); // 装车
/* 4. 发车!(同步阻塞等待) */
/* spi_sync 会一直等到底层硬件把波形发完才返回 */
ret = spi_sync(spi, &msg);
if (ret == 0) {
printk("读取成功!数据是: 0x%x\\n", rx_data);
}
2. 懒人快捷 API(极其常用)
如果每次发个简单的数据都要初始化 message 和 transfer,内核工程师也会疯掉的。所以,内核提供了几个经过深度封装的快捷函数,它们底层其实帮你动态分配了 spi_transfer 和 spi_message:
spi_write(struct spi_device \\*spi, const void \\*buf, size_t len)
- 作用:纯发送数据。
- 场景:给 SPI 屏幕刷一帧图像数据。
spi_read(struct spi_device \\*spi, void \\*buf, size_t len)
- 作用:纯接收数据。
spi_write_then_read(struct spi_device \\*spi, const void \\*txbuf, unsigned n_tx, void \\*rxbuf, unsigned n_rx)
- 作用:极其神仙的 API!先发一段数据,紧接着收一段数据,并且保证中间片选不断开。
- 场景:读传感器寄存器、读 SPI Flash 的 ID。
- 注意:这个函数为了效率,内部用的是一个小小的预分配缓冲区,所以它不适合传输超过 32 字节的大数据(大数据请老老实实用上面的标准操作流程)。
#include <linux/spi/spi.h>
#include <linux/device.h>
/* 假设这是你的传感器读取主逻辑函数 */
static int my_advanced_sensor_work(struct spi_device *spi)
{
int ret;
/* ==========================================================
* 场景一:只写不读 (spi_write)
* 动作:向 0x20 寄存器写入 0x0F (唤醒芯片并配置参数)
* 时序:CS拉低 -> 发送 0x20 -> 发送 0x0F -> CS拉高
* ========================================================== */
u8 config_buf[2] = {0x20, 0x0F};
// 参数:(spi设备, 发送缓冲区指针, 发送字节数)
ret = spi_write(spi, config_buf, 2);
if (ret < 0) {
dev_err(&spi->dev, "第一步:写入配置失败!(%d)\\n", ret);
return ret;
}
dev_info(&spi->dev, "第一步:传感器配置完毕!\\n");
/* ==========================================================
* 场景二:先写后读,中间不断开 (spi_write_then_read)
* 动作:发送读取 ID 的指令 0x90,然后接收 2 个字节的 ID
* 时序:CS拉低 -> 发送 0x90 -> (无缝衔接) 接收 2 字节 -> CS拉高
* ========================================================== */
u8 id_cmd = 0x90;
u8 id_data[2] = {0}; // 准备篮子装数据
// 参数:(spi设备, 发送区指针, 发送长度, 接收区指针, 接收长度)
ret = spi_write_then_read(spi, &id_cmd, 1, id_data, 2);
if (ret < 0) {
dev_err(&spi->dev, "第二步:读取 ID 失败!(%d)\\n", ret);
return ret;
}
dev_info(&spi->dev, "第二步:拿到芯片 ID: 0x%02X%02X\\n", id_data[0], id_data[1]);
/* ==========================================================
* 场景三:只读不写 (spi_read)
* 动作:传感器已经在源源不断输出数据,直接无脑抓取 6 个字节 (X,Y,Z轴)
* 时序:CS拉低 -> (此时 MOSI 线上发送的是无意义的填充波形)
* -> 从 MISO 线上接收 6 字节 -> CS拉高
* 注意:这种操作在 SPI 里相对少见,一般只用于连续流模式的外设
* ========================================================== */
u8 raw_data[6] = {0}; // 准备大一点的篮子
// 参数:(spi设备, 接收缓冲区指针, 接收字节数)
ret = spi_read(spi, raw_data, 6);
if (ret < 0) {
dev_err(&spi->dev, "第三步:盲收数据失败!(%d)\\n", ret);
return ret;
}
dev_info(&spi->dev, "第三步:收到六轴数据!X:0x%02X%02X, Y:0x%02X%02X\\n",
raw_data[0], raw_data[1], raw_data[2], raw_data[3]);
return 0; /* 完美收工 */
}
1.3 spi核心层
SPI核心层代码位于linux_5.10\\drivers\\spi目录
核心层的作用:
1.对上层的使用者,也就是SPI设备驱动:提供标准的spi收发API,以及设备注册函数。也即是说通过 spi_write 和 spi_read函数,核心层将设备驱动层的数据传递给适配器驱动层进行实际的硬件操作,这样不管是什么类型的spi控制器,对设备驱动层来说都是统一的接口,使用起来非常方便。
2.对底下的控制器驱动层:提供注册控制器接口,并提供一些需要控制器驱动实现的回调函数。
1.3.1 核心层初始化 spi_init
static int __init spi_init(void) {
intstatus;
buf = kmalloc(SPI_BUFSIZ, GFP_KERNEL);
if (!buf) {
status = -ENOMEM;
goto err0;
}
status = bus_register(&spi_bus_type);
if (status < 0)
goto err1;
status = class_register(&spi_master_class);
if (status < 0)
goto err2;
if (IS_ENABLED(CONFIG_SPI_SLAVE)) {
status = class_register(&spi_slave_class);
if (status < 0)
goto err3;
}
if (IS_ENABLED(CONFIG_OF_DYNAMIC))
WARN_ON(of_reconfig_notifier_register(&spi_of_notifier));
if (IS_ENABLED(CONFIG_ACPI))
WARN_ON(acpi_reconfig_notifier_register(&spi_acpi_notifier));
return 0;
err3:
class_unregister(&spi_master_class);
err2:
bus_unregister(&spi_bus_type);
err1:
kfree(buf);
buf = NULL;
err0:
return status;
}
spi子系统初始化函数,仅仅是注册了spi bus,以及spi_master class。
成功注册后,在/sys/bus下即可找到spi 文件目录,在/sys/class下可以看到spi_master目录:


1.3.2 spi从设备和驱动的匹配
当spi总线和类注册后,当有驱动和设备匹配上就会调用spi_match_device,也就是spi_bus_type.match函数。
spi_match_device 函数原型:
static int spi_match_device(struct device *dev, struct device_driver *drv) {
const struct spi_device*spi = to_spi_device(dev);
const struct spi_driver*sdrv = to_spi_driver(drv);
/* Check override first, and if set, only use the named driver */
if (spi->driver_override)
return strcmp(spi->driver_override, drv->name) == 0;
/* Attempt an OF style match */
if (of_driver_match_device(dev, drv))
return 1;
/* Then try ACPI */
if (acpi_driver_match_device(dev, drv))
return 1;
if (sdrv->id_table)
return !!spi_match_id(sdrv->id_table, spi);
return strcmp(spi->modalias, drv->name) == 0;
}
通过源码可以看出有四种匹配方式:
1.3.3 核心层API
1.3.3.1 spi_register_master(供适配层调用)
int spi_register_master(struct spi_master *master) {
…
status = of_spi_register_master(master);
if (status)
return status;
…
dev_set_name(&master->dev, "spi%u", master->bus_num);
status = device_add(&master->dev);
if (status < 0)
goto done;
dev_dbg(dev, "registered master %s%s\\n", dev_name(&master->dev),
dynamic ? " (dynamic)" : "");
if (master->transfer)
dev_info(dev, "master is unqueued, this is deprecated\\n");
else {
status = spi_master_initialize_queue(master);
if (status) {
device_del(&master->dev);
goto done;
}
}
…
mutex_lock(&board_lock);
list_add_tail(&master->list, &spi_master_list);
list_for_each_entry(bi, &board_list, list)
spi_match_master_to_boardinfo(master, &bi->board_info);
mutex_unlock(&board_lock);
…
of_register_spi_devices(master);
…
done:
return status;
}
1.4 spi控制器驱动层(Master/Controller Driver)
站在芯片的角度来看,spi控制器也是一个外设,因此,它也需要像普通外设那样通过设备树来和控制器的驱动进行匹配,然后匹配成功之后也会调用probe函数来进行初始化等操作。
控制器驱动就是用来实现spi_master中的成员函数。如transfer, transfer_one_message。基本流程是:申请 spi_master,然后初始化spi_master,最后向 Linux 内核注册 spi_master(注:在老内核里它叫 struct spi_master,现代内核统一升级为了 spi_controller)
1.4 .1 SPI控制器驱动层API
1.4.1.1 spi_alloc_master/spi_alloc_host
向内核申请内存
static inline struct spi_controller *spi_alloc_host(struct device *dev,
unsigned int size)
1.4.1.2 spi_register_master/spi_register_controller
向内核注册一个spi控制器
int spi_register_controller(struct spi_controller *ctlr)
1.4.1.3 spi_unregister_master/spi_unregister_controller
卸载spi驱动器
void spi_unregister_controller(struct spi_controller *ctlr)
1.4.1.4 spi_master_put/spi_controller_put
释放 spi_alloc_host 申请的内存,避免内存泄漏
static inline void spi_controller_put(struct spi_controller *ctlr)
情况 A(注册失败):如果申请了内存但是注册失败了,你必须手动调用 spi_controller_put 否则就是内存泄漏!
情况 B(正常卸载):如果register成功了,将来在 remove 函数里,你只需调用 spi_unregister_controller 即可,绝对不能再调用 put! 因为 unregister 底层自带了引用计数减一的销毁功能,你再 put 一次就会导致“重复释放(Double Free)”,内核当场死机!
下面以一个具体的spi控制器驱动程序来讲解。
rk3568中对spi控制器的设备树代码如下
spi0: spi@fe610000 {
compatible = "rockchip,rk3066-spi";
reg = <0x0 0xfe610000 0x0 0x1000>;
interrupts = <GIC_SPI 103 IRQ_TYPE_LEVEL_HIGH>;
#address-cells = <1>;
#size-cells = <0>;
clocks = <&cru CLK_SPI0>, <&cru PCLK_SPI0>;
clock-names = "spiclk", "apb_pclk";
dmas = <&dmac0 20>, <&dmac0 21>;
dma-names = "tx", "rx";
pinctrl-names = "default", "high_speed";
pinctrl-0 = <&spi0m0_cs0 &spi0m0_cs1 &spi0m0_pins>;
pinctrl-1 = <&spi0m0_cs0 &spi0m0_cs1 &spi0m0_pins_hs>;
status = "disabled";
};
根据compatible = "rockchip,rk3066-spi"可以使用grep去drivers目录下搜索得到对应的spi控制器驱动代码在drivers/spi/spi-rockchip.c路径下

static int rockchip_spi_probe(struct platform_device *pdev)
{
int ret;
struct rockchip_spi *rs; //rk自定义的spi资源
struct spi_controller *ctlr;
struct resource *mem;
struct device_node *np = pdev->dev.of_node;
u32 rsd_nsecs;
bool slave_mode;
struct pinctrl *pinctrl = NULL;
slave_mode = of_property_read_bool(np, "spi-slave"); //查看自己是不是从机
if (slave_mode)
ctlr = spi_alloc_slave(&pdev->dev,
sizeof(struct rockchip_spi));
else
ctlr = spi_alloc_master(&pdev->dev, //申请内存空间
sizeof(struct rockchip_spi));
if (!ctlr)
return -ENOMEM;
platform_set_drvdata(pdev, ctlr);
rs = spi_controller_get_devdata(ctlr);
ctlr->slave = slave_mode;
/* Get basic io resource and map it */
mem = platform_get_resource(pdev, IORESOURCE_MEM, 0); //获取中断资源
rs->regs = devm_ioremap_resource(&pdev->dev, mem);
if (IS_ERR(rs->regs)) {
ret = PTR_ERR(rs->regs);
goto err_put_ctlr;
}
rs->apb_pclk = devm_clk_get(&pdev->dev, "apb_pclk");
if (IS_ERR(rs->apb_pclk)) {
dev_err(&pdev->dev, "Failed to get apb_pclk\\n");
ret = PTR_ERR(rs->apb_pclk);
goto err_put_ctlr;
}
rs->spiclk = devm_clk_get(&pdev->dev, "spiclk");
if (IS_ERR(rs->spiclk)) {
dev_err(&pdev->dev, "Failed to get spi_pclk\\n");
ret = PTR_ERR(rs->spiclk);
goto err_put_ctlr;
}
ret = clk_prepare_enable(rs->apb_pclk);
if (ret < 0) {
dev_err(&pdev->dev, "Failed to enable apb_pclk\\n");
goto err_put_ctlr;
}
ret = clk_prepare_enable(rs->spiclk);
if (ret < 0) {
dev_err(&pdev->dev, "Failed to enable spi_clk\\n");
goto err_disable_apbclk;
}
spi_enable_chip(rs, false);
ret = platform_get_irq(pdev, 0);
if (ret < 0)
goto err_disable_spiclk;
ret = devm_request_threaded_irq(&pdev->dev, ret, rockchip_spi_isr, NULL,
IRQF_ONESHOT, dev_name(&pdev->dev), ctlr);
if (ret)
goto err_disable_spiclk;
rs->dev = &pdev->dev;
rs->freq = clk_get_rate(rs->spiclk);
rs->gpio_requested = false;
if (!of_property_read_u32(pdev->dev.of_node, "rx-sample-delay-ns",
&rsd_nsecs)) {
/* rx sample delay is expressed in parent clock cycles (max 3) */
u32 rsd = DIV_ROUND_CLOSEST(rsd_nsecs * (rs->freq >> 8),
1000000000 >> 8);
if (!rsd) {
dev_warn(rs->dev, "%u Hz are too slow to express %u ns delay\\n",
rs->freq, rsd_nsecs);
} else if (rsd > CR0_RSD_MAX) {
rsd = CR0_RSD_MAX;
dev_warn(rs->dev, "%u Hz are too fast to express %u ns delay, clamping at %u ns\\n",
rs->freq, rsd_nsecs,
CR0_RSD_MAX * 1000000000U / rs->freq);
}
rs->rsd = rsd;
}
rs->fifo_len = get_fifo_len(rs);
if (!rs->fifo_len) {
dev_err(&pdev->dev, "Failed to get fifo length\\n");
ret = -EINVAL;
goto err_disable_spiclk;
}
pm_runtime_set_active(&pdev->dev);
pm_runtime_enable(&pdev->dev);
ctlr->auto_runtime_pm = true;
ctlr->bus_num = pdev->id;
ctlr->mode_bits = SPI_CPOL | SPI_CPHA | SPI_LOOP | SPI_LSB_FIRST | SPI_CS_HIGH;
if (slave_mode) {
ctlr->mode_bits |= SPI_NO_CS;
ctlr->slave_abort = rockchip_spi_slave_abort;
} else {
ctlr->flags = SPI_MASTER_GPIO_SS;
}
ctlr->num_chipselect = ROCKCHIP_SPI_MAX_CS_NUM;
ctlr->dev.of_node = pdev->dev.of_node;
ctlr->bits_per_word_mask = SPI_BPW_MASK(16) | SPI_BPW_MASK(8) | SPI_BPW_MASK(4);
ctlr->min_speed_hz = rs->freq / BAUDR_SCKDV_MAX;
ctlr->max_speed_hz = min(rs->freq / BAUDR_SCKDV_MIN, MAX_SCLK_OUT);
ctlr->set_cs = rockchip_spi_set_cs; //怎么设置cs引脚的高低电平
ctlr->setup = rockchip_spi_setup; //怎么配置时钟极性
ctlr->cleanup = rockchip_spi_cleanup;
ctlr->transfer_one = rockchip_spi_transfer_one; //怎么发数据
ctlr->max_transfer_size = rockchip_spi_max_transfer_size;
ctlr->handle_err = rockchip_spi_handle_err;
ctlr->dma_tx = dma_request_chan(rs->dev, "tx");
if (IS_ERR(ctlr->dma_tx)) {
/* Check tx to see if we need defer probing driver */
if (PTR_ERR(ctlr->dma_tx) == -EPROBE_DEFER) {
ret = -EPROBE_DEFER;
goto err_disable_pm_runtime;
}
dev_warn(rs->dev, "Failed to request TX DMA channel\\n");
ctlr->dma_tx = NULL;
}
ctlr->dma_rx = dma_request_chan(rs->dev, "rx");
if (IS_ERR(ctlr->dma_rx)) {
if (PTR_ERR(ctlr->dma_rx) == -EPROBE_DEFER) {
ret = -EPROBE_DEFER;
goto err_free_dma_tx;
}
dev_warn(rs->dev, "Failed to request RX DMA channel\\n");
ctlr->dma_rx = NULL;
}
if (ctlr->dma_tx && ctlr->dma_rx) {
rs->dma_addr_tx = mem->start + ROCKCHIP_SPI_TXDR;
rs->dma_addr_rx = mem->start + ROCKCHIP_SPI_RXDR;
ctlr->can_dma = rockchip_spi_can_dma;
}
switch (readl_relaxed(rs->regs + ROCKCHIP_SPI_VERSION)) {
case ROCKCHIP_SPI_VER2_TYPE1:
case ROCKCHIP_SPI_VER2_TYPE2:
if (ctlr->can_dma && slave_mode)
rs->cs_inactive = true;
else
rs->cs_inactive = false;
break;
default:
rs->cs_inactive = false;
}
pinctrl = devm_pinctrl_get(&pdev->dev);
if (!IS_ERR(pinctrl)) {
rs->high_speed_state = pinctrl_lookup_state(pinctrl, "high_speed");
if (IS_ERR_OR_NULL(rs->high_speed_state)) {
dev_warn(&pdev->dev, "no high_speed pinctrl state\\n");
rs->high_speed_state = NULL;
}
}
ret = devm_spi_register_controller(&pdev->dev, ctlr);
if (ret < 0) {
dev_err(&pdev->dev, "Failed to register controller\\n");
goto err_free_dma_rx;
}
return 0;
//错误处理部分
err_free_dma_rx:
if (ctlr->dma_rx)
dma_release_channel(ctlr->dma_rx);
err_free_dma_tx:
if (ctlr->dma_tx)
dma_release_channel(ctlr->dma_tx);
err_disable_pm_runtime:
pm_runtime_disable(&pdev->dev);
err_disable_spiclk:
clk_disable_unprepare(rs->spiclk);
err_disable_apbclk:
clk_disable_unprepare(rs->apb_pclk);
err_put_ctlr:
spi_controller_put(ctlr); //释放内存,防止内存泄漏
return ret;
}
2、SPI相关的数据结构
2.1 spi_master/spi_controller(现代内核)
struct spi_master {
struct devicedev;/* SPI设备的device数据结构 */
s16bus_num;/* SPI总线序号 */
u16num_chipselect;/* 片选信号数量 */
u16dma_alignment; /* SPI控制器DMA缓冲区对齐定义 */
u16mode_bits;/* 工作模式位,由驱动定义 */
u32min_speed_hz;/* 最小速度 */
u32max_speed_hz;/* 最小速度 */
u16flags;/* 限制条件标志 */
int(*setup)(struct spi_device *spi);/* 设置SPI设备的工作参数 */
int(*transfer)(struct spi_device *spi,/* SPI发送函数1 */
struct spi_message *mesg);
void (*cleanup)(struct spi_device *spi); /* SPI清除函数,当spi_master被释放时调用 */
int (*transfer_one_message)(struct spi_master *master,/* SPI发送函数2 */
int (*transfer_one)(struct spi_master *master, struct spi_device *spi,/* SPI发送函数3 */
struct spi_transfer *transfer);
…
};
2.2 spi_driver
struct spi_driver {
const struct spi_device_id *id_table;
int(*probe)(struct spi_device *spi);
void(*remove)(struct spi_device *spi);
void(*shutdown)(struct spi_device *spi);
struct device_driverdriver;
};
2.3 spi_device

2.4 spi_message/spi_transfer
如果把 SPI 核心层比作“顺丰快递的中转站”,那么 spi_message 就是“一整张快递运单”,而 spi_transfer 就是这张运单里面打包的具体“纸箱子”。
它们之间是包含关系:一个 spi_message 里面,可以挂载一个或多个 spi_transfer。
我们来硬核拆解一下这两个家伙到底装了什么,以及为什么 Linux 费尽心机要把它们拆分成两层:
2.4.1 spi_transfer(具体的纸箱子)
这是 SPI 数据传输的最小物理单位。每次芯片底层的 SPI 控制器(比如 RK3568)踩油门发数据,都是对着一个 spi_transfer 发的。
它里面装的都是最实在的干货(截取自内核源码):
struct spi_transfer {
const void *tx_buf; // 【发件箱】你要发给外设的数据,存在这儿
void *rx_buf; // 【收件箱】外设发给你的数据,存在这儿
unsigned len; // 【数据长度】这个箱子有多大(发多少字节)
unsigned cs_change:1; // 【特殊指令】发完这个箱子后,要不要临时松开 CS 片选线?
u32 speed_hz; // 【限速】如果你想让这个箱子传得慢一点,可以在这里单独设置波特率
// … 其他微调参数
};
极其重要的硬件常识(全双工机制):
你看它里面同时有 tx_buf 和 rx_buf。因为 SPI 是“全双工”的(发送和接收同时发生)。
当你把 tx_buf 里的 1 个字节通过 MOSI 线挤出去的同时,必定会从 MISO 线挤进来 1 个字节存到 rx_buf 里。发多少,必定收多少! 它们俩的长度永远共用那个 len。
2.4.2 spi_message(完整的快递运单)
这是一个逻辑上的管理单位。Linux 规定,向外设发送数据,必须是以 spi_message 为单位进行排队的。
struct spi_message {
struct list_head transfers; // 【清单】一个链表头,把所有的 spi_transfer (箱子) 串起来
struct spi_device *spi; // 【收件人】指向你的外设(比如触摸屏)
void (*complete)(void *context); // 【回执单】发完之后,内核会自动调用这个回调函数通知你
int status; // 【快递状态】成功还是失败了?
// … 其他排队用的参数
};
2.4.3 灵魂拷问:为什么非要搞“运单+箱子”这么复杂?
很多初学者会骂娘:“我就是想给屏幕发个亮屏指令,直接塞个数组给底层的 spi_write 函数不就行了吗?为什么要建箱子、填运单,搞出两个结构体?”
这是因为 SPI 硬件通信中有一个极其严苛的要求:原子操作(Atomic Transaction)。
在 SPI 总线中,只要 CS(片选线)被拉低,外设就竖起耳朵听。一旦 CS 被拉高,外设就认为“这一次对话结束了”,并立刻去执行刚刚收到的命令,或者重置自己的状态机。
实战场景:读取传感器的寄存器
假设你要读一个温度传感器的 0x55 寄存器。你需要做两步:
如果你不使用 spi_message,而是直接调用两次独立的底层发送函数,会发生什么悲剧?
- 内核拉低 CS -> 发送 0x55 -> 内核拉高 CS。(传感器以为你发完了,刚准备准备回传数据,结果发现 CS 被拔了,强制复位!)
- 内核拉低 CS -> 傻乎乎地去读 2 个字节 -> 内核拉高 CS。(传感器一脸懵逼:你都没发指令,我凭什么给你数据?于是给你回传了一堆乱码 0xFF 0xFF)。
spi_message 完美解决这个痛点:
Linux 规定:只要是在同一个 spi_message 里的所有 spi_transfer,在传输过程中,CS 线会被死死地按在低电平,绝不松手! 直到运单里所有的箱子都传完了,CS 线才会被拉高。
所以正确的做法是:
准备箱子 A(spi_transfer):tx_buf 装指令 0x55,len = 1。
准备箱子 B(spi_transfer):rx_buf 准备接收,len = 2。
把箱子 A 和箱子 B 都串到一张 spi_message 运单上。
把运单交给内核。
内核就会一气呵成地执行:拉低 CS -> 发 A -> 收 B -> 拉高 CS。传感器舒服了,你也拿到正确数据了!
总结
linux中总线-设备-驱动三模型架构,以spi来举例讲解,当设备刚开机的时候SPI 虚拟总线连影子都还没有的时候,Linux 依靠的是系统自带的“原生大总线”——平台总线(Platform Bus)将spi控制器和驱动匹配起来然后调用probe函数,在这个函数中实现注册一个虚拟的SPI总线。现在有了SPI 虚拟总线,我们自己的外设(屏幕,mpu6050之类的板子的外设)就可以通过SPI 虚拟总线来进行设备和驱动的匹配,然后再进入到probe函数中执行对自己的外设的一些初始化操作。 linux中总线-设备-驱动三模型架构就是用来完成设备和驱动的解耦。假设主板上接了 3 块完全一样的 OLED 屏幕(分别挂在 SPI0, SPI1, SPI2)在设备树里写了 3 个节点,但 C 语言驱动只需要写一份SPI 总线会拿着这一份驱动,去和 3 个设备节点分别匹配,连续调用 3 次你的 probe 函数,生成 3 个独立的设备实例。 还有就是可以跨平台移植:写好的的 OLED 驱动,里面没有任何 开发板的寄存器地址。如果要把这块屏幕换到其他开发板上,驱动 C 代码一行都不用改只需要修改设备树,往里面加一个节点就可以了
3、 实战编写一个spi的驱动MCP2515

3.1修改设备树
找到开发板场上的设备树文件
kernel/arch/arm64/boot/dts/rockchip/rk3568-evb1-ddr4-v10.dtsi
直接对spi0进行引用修改
&spi0 {
status = "okay";
pinctrl-0 = <&spi0m1_cs0 &spi0m1_pins>;
pinctrl-1 = <&spi0m1_cs0 &spi0m1_pins_hs>; //high-speed
mcp2515:mcp2515@0 {
compatible = "my-mcp2515";
reg = <0>; //表示片选,由原理图可以知道使用的是片选0
spi-max-frequency = <10000000>;
// spi-cpha;//表示相位属性是1,如果不写就默认为0
// spi-cpol; //表示极性属性是1,如果不写就默认为0
// spi-lsb-first; //表示先从低位开始传输,不写就是默认从高位开始传输
// spi-cs-high; //表示这个spi外设是cs脚高电平选中,不写默认是低电平
status = "okay";
};
//mcp2515这个设备节点是在spi控制器的probe函数中被解析的,如果设备树mcp2515的节点中没有reg = <0>;
//spi-max-frequency = <10000000>;这两个属性,则会报错。
};
./build.sh kernel 编译内核生成boot.img 烧到开发板中

cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins
使用命令查看引脚复用情况

在路径下可以正确看到mcp2515的设备节点,表示添加成功

接下来先写一个spi驱动的壳子
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
// MCP2515 设备初始化函数
static int mcp2515_probe(struct spi_device *spi)
{
printk("This is mcp2515 probe\\n");
return 0;
}
// MCP2515 设备移除函数
static int mcp2515_remove(struct spi_device *spi)
{
return 0;
}
// MCP2515 设备匹配表,用于设备树匹配
static const struct of_device_id mcp2515_of_match_table[] = {
{ .compatible = "my-mcp2515" },
{}
};
// MCP2515 SPI 驱动结构体
static struct spi_driver spi_mcp2515 = {
.probe = mcp2515_probe, // 探测函数
.remove = mcp2515_remove, // 移除函数
.driver = {
.name = "mcp2515", // 驱动名称
.owner = THIS_MODULE, // 所属模块
.of_match_table = mcp2515_of_match_table, // 设备树匹配表
},
};
// 驱动初始化函数
static int __init mcp2515_init(void)
{
int ret;
// 注册 SPI 驱动
ret = spi_register_driver(&spi_mcp2515);
if (ret < 0) {
// 注册失败,打印错误信息
printk("spi_register_driver error\\n");
return ret;
}
return ret;
}
// 驱动退出函数
static void __exit mcp2515_exit(void)
{
// 注销 SPI 驱动
spi_unregister_driver(&spi_mcp2515);
}
module_init(mcp2515_init);
module_exit(mcp2515_exit);
MODULE_LICENSE("GPL");
写好之后编译,插入内核,可以看到如下信息,表示壳子写的没问题。

根据mcp2515的设备类型可知,可以选择字符设备驱动框架来编写驱动,下面将根据字符设备驱动来进行壳子的填充。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
dev_t dev_num; // 设备号
struct cdev mcp2515_cdev; // 字符设备结构体
struct class *mcp2515_class; // 设备类
struct device *mcp2515_device; // 设备
struct spi_device * spi_dev ;
// 打开设备文件的回调函数
int mcp2515_open(struct inode *inode, struct file *file)
{
return 0; // 返回成功
}
// 读取设备文件的回调函数
ssize_t mcp2515_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
return 0; // 返回成功
}
// 写入设备文件的回调函数
ssize_t mcp2515_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
return 0; // 返回成功
}
// 关闭设备文件的回调函数
int mcp2515_release(struct inode *inode, struct file *file)
{
return 0; // 返回成功
}
// 设备文件操作集合
struct file_operations mcp2515_fops =
{
.open = mcp2515_open,
.read = mcp2515_read,
.write = mcp2515_write,
.release = mcp2515_release,
};
// MCP2515 设备初始化函数
int mcp2515_probe(struct spi_device *spi)
{
int ret;
printk("This is mcp2515_probe\\n");
spi_dev = spi; // 保存 SPI 设备指针
// 分配字符设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, "mcp2515");
if (ret < 0)
{
printk("alloc_chrdev_region error\\n");
}
// 初始化字符设备
cdev_init(&mcp2515_cdev, &mcp2515_fops);
mcp2515_cdev.owner = THIS_MODULE;
// 添加字符设备
ret = cdev_add(&mcp2515_cdev, dev_num, 1);
if (ret < 0)
{
printk("cdev_add error\\n");
return -1;
}
// 创建设备类
mcp2515_class = class_create(THIS_MODULE, "spi_to_can");
if (IS_ERR(mcp2515_class))
{
printk("mcp2515_class error\\n");
return PTR_ERR(mcp2515_class);
}
// 创建设备
mcp2515_device = device_create(mcp2515_class, NULL, dev_num, NULL, "mcp2515");
if (IS_ERR(mcp2515_device))
{
printk("mcp2515_device error\\n");
return PTR_ERR(mcp2515_device);
}
return 0; // 返回成功
}
// MCP2515 SPI 设备的移除函数
static int mcp2515_remove(struct spi_device *spi)
{
device_destroy(mcp2515_class, dev_num);
class_destroy(mcp2515_class);
cdev_del(&mcp2515_cdev);
unregister_chrdev_region(dev_num, 1);
return 0;
}
// MCP2515 设备匹配表,用于设备树匹配
static const struct of_device_id mcp2515_of_match_table[] =
{
{ .compatible = "my-mcp2515" },
{}
};
// MCP2515 SPI 驱动结构体
static struct spi_driver spi_mcp2515 =
{
.probe = mcp2515_probe, // 探测函数
.remove = mcp2515_remove, // 移除函数
.driver =
{
.name = "mcp2515", // 驱动名称
.owner = THIS_MODULE, // 所属模块
.of_match_table = mcp2515_of_match_table, // 设备树匹配表
},
};
// 驱动初始化函数
static int __init mcp2515_init(void)
{
int ret;
// 注册 SPI 驱动
ret = spi_register_driver(&spi_mcp2515);
if (ret < 0)
{
// 注册失败,打印错误信息
printk("spi_register_driver error\\n");
return ret;
}
return ret;
}
// 驱动退出函数
static void __exit mcp2515_exit(void)
{
// 注销 SPI 驱动
spi_unregister_driver(&spi_mcp2515);
}
module_init(mcp2515_init);
module_exit(mcp2515_exit);
MODULE_LICENSE("GPL");
写好之后编译,插入内核,可以看到如下信息,表示写的正确。进入/dev目录下可以看到设备节点


其实写到这里还是通用的spi设备驱动,但是接下来就要进行mcp2515这个外设独有的spi驱动的开发了。
MCP2515 具有五种模式,分别为配置模式、正常模式、休眠模式、仅监听模式和环回模式,只有在配置模式下才能对关键寄存器进行初始化和配置,当 MCP2515 上电或者复位时,器件会自动进入配置模式,因此我们需要先编写一个复位函数让器件复位,然后再操作他的关键寄存器进行配置,可以参考mcp2515的数据手册,查看这个外设提供的SPI 指令。

通过向 MCP2515 发送上述 SPI 指令就能实现复位、读、写等操作,复位操作对应的指令格式为 11000000,在 Linux 驱动中可以使用 spi_write 函数来实现向 SPI 从设备发送数据。
接下来实现复位函数
void mcp2515_reset(void)
{
int ret; //1100 0000
char write_buf[] = {0xc0}; //一个指令正好是1个字节,所以使用char类型的正好
ret = spi_write(spi_dev,write_buf,sizeof(write_buf));
if(ret<0)
{
printk("spi_write is error\\n");
}
}
接下来实现读寄存器函数

由芯片手册可知,状态寄存器的地址为0x0e, h代表16进制,由于上一步我们写了reset函数,现在我们需要验证到底有没有reset成功,如果成功那么,状态寄存器的高三位是100。接下来我们编写寄存器读取函数来验证。
根据指令操作可以知道我们要先发一个0000 0011(0x03)读指令,然后再读取寄存器发过来的寄存器的值,接下来开始编程。
char mcp2515_read_reg(char reg)
{
char write_buf[] = {0x03, reg}; // SPI 写缓冲区写入 SPI 读指令 0x03
char read_buf; // SPI 读缓冲区
int ret;
ret = spi_write_then_read(spi_dev, write_buf, sizeof(write_buf), &read_buf, sizeof(read_buf)); // 调用 SPI写读函数
if (ret < 0)
{
printk("spi_write_then_read error\\n");
return ret;
}
return read_buf;
}
void mcp2515_write_reg(char reg, char value)
{
int ret;
char write_buf[] = {0x02, reg, value}; // SPI 写缓冲区,用于发送写寄存器命令
ret = spi_write(spi_dev, write_buf, sizeof(write_buf)); // 发送 SPI 写命令
if (ret < 0)
{
printk("mcp2515_write_reg error\\n");
}
}
// MCP2515 修改寄存器位函数
void mcp2515_change_regbit(char reg, char mask, char value)
{
int ret;
char write_buf[] = { 0x05, reg, mask, value }; // SPI 写缓冲区,用于发送修改寄存器位命令
ret = spi_write(spi_dev, write_buf, sizeof(write_buf)); // 发送 SPI 写命令
if (ret < 0)
{
printk("mcp2515_change_regbit error\\n");
}
}
#define CNF1 0x2a // 寄存器定义
#define CNF2 0x29
#define CNF3 0x28
#define RXB0CTRL 0x2b
#define CANINTE 0x60
mcp2515_write_reg(CNF1, 0x01); // 写入寄存器配置值
mcp2515_write_reg(CNF2, 0xb1);
mcp2515_write_reg(CNF3, 0x05);
mcp2515_write_reg(RXB0CTRL , 0x60);
mcp2515_write_reg(CANINTE , 0x05);
注意这里的mcp2515的寄存器的值不能写在设备树里,因为这个寄存器本身属于spi外设,根本就不是cpu管的,是由spi控制器管理的,mcp2515的寄存器地址假如有一个是 0x0F这个 0x0F 根本不是物理内存地址 它是 MCP2515 内部自己的一个“编号”。接下来一些对mcp2515的操作我就不展开讲了,涉及到寄存器的配置,以及收发的时序规则,可以自己上网看mcp2515的使用方法,主要还是聚焦在spi驱动框架的使用。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/uaccess.h>
#define CNF1 0x2a // 寄存器定义
#define CNF2 0x29
#define CNF3 0x28
#define RXB0CTRL 0x60
#define CANINTE 0x2b
#define CANCTRL 0xf // CAN 控制寄存器
#define TXB0CTRL 0x30 //发送缓冲器控制寄存器地址
#define CANINTF 0x2c
dev_t dev_num; // 设备号
struct cdev mcp2515_cdev; // 字符设备结构体
struct class *mcp2515_class; // 设备类
struct device *mcp2515_device; // 设备
struct spi_device *spi_dev; // SPI 设备指针
// MCP2515 芯片复位函数
void mcp2515_reset(void)
{
int ret;
char write_buf[] = {0xc0}; // 复位指令 0x11000000 即 0xc0
ret = spi_write(spi_dev, write_buf, sizeof(write_buf)); // 发送复位命令
if(ret < 0)
{
printk("spi_write is error\\n"); // 打印错误信息
}
}
// MCP2515 读寄存器函数
char mcp2515_read_reg(char reg) {
char write_buf[] = {0x03, reg}; // SPI 写缓冲区写入 SPI 读指令 0x03
char read_buf; // SPI 读缓冲区
int ret;
ret = spi_write_then_read(spi_dev, write_buf, sizeof(write_buf), &read_buf, sizeof(read_buf));
// 调用SPI 写读函数
if (ret < 0)
{
printk("spi_write_then_read error\\n");
return ret;
}
return read_buf;
}
// MCP2515 写寄存器函数
void mcp2515_write_reg(char reg, char value)
{
int ret;
char write_buf[] = {0x02, reg, value}; // SPI 写缓冲区,用于发送写寄存器命令
ret = spi_write(spi_dev, write_buf, sizeof(write_buf)); // 发送 SPI 写命令
if (ret < 0)
{
printk("mcp2515_write_reg error\\n");
}
}
// MCP2515 修改寄存器位函数
void mcp2515_change_regbit(char reg, char mask, char value)
{
int ret;
char write_buf[] = { 0x05, reg, mask, value }; // SPI 写缓冲区,用于发送修改寄存器位命令
ret = spi_write(spi_dev, write_buf, sizeof(write_buf)); // 发送 SPI 写命令
if (ret < 0)
{
printk("mcp2515_change_regbit error\\n");
}
}
// 打开设备文件的回调函数
int mcp2515_open(struct inode *inode, struct file *file)
{
return 0; // 返回成功
}
// 读设备操作函数,从设备读取数据到用户缓冲区
ssize_t mcp2515_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
char r_kbuf[13] = {0}; // 内核缓冲区,用于存储从设备读取的数据
int i;
int ret;
// 等待接收缓冲区满标志位被设置
while (!(mcp2515_read_reg(CANINTF) & (1 << 0)));
// 从接收缓冲区读取数据到内核缓冲区
for (i = 0; i < sizeof(r_kbuf); i++)
{
r_kbuf[i] = mcp2515_read_reg(0x61 + i);
}
// 清除接收缓冲区满标志位
mcp2515_change_regbit(CANINTF, 0x01, 0x00);
// 将内核缓冲区的数据复制到用户缓冲区
ret = copy_to_user(buf, r_kbuf, size);
if (ret)
{
printk("copy_to_user r_kbuf is error\\n");
return -1; // 返回-1 表示复制数据失败
}
return 0; // 返回 0 表示成功读取数据
}
// 写设备操作函数
ssize_t mcp2515_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
char w_kbuf[13] = {0};
int ret;
int i;
// 设置 TXB0CTRL 寄存器的部分位
mcp2515_change_regbit(TXB0CTRL, 0x03, 0x03);
// 从用户空间复制数据到内核缓冲区
ret = copy_from_user(w_kbuf, buf, size);
if (ret)
{
printk("copy_from_user w_kbuf is error\\n");
return -1;
}
// 将数据写入 MCP2515 寄存器
for (i = 0; i < sizeof(w_kbuf); i++) {
mcp2515_write_reg(0x31 + i, w_kbuf[i]);
}
// 设置 TXB0CTRL 寄存器的部分位,启动发送
mcp2515_change_regbit(TXB0CTRL, 0x08, 0x08);
// 等待发送完成
while (!(mcp2515_read_reg(CANINTF) & (1 << 2)));
// 清除发送完成标志
mcp2515_change_regbit(CANINTF, 0x04, 0x00);
return size;
}
// 关闭设备文件的回调函数
int mcp2515_release(struct inode *inode, struct file *file)
{
return 0; // 返回成功
}
// 设备文件操作集合
struct file_operations mcp2515_fops =
{
.open = mcp2515_open,
.read = mcp2515_read,
.write = mcp2515_write,
.release = mcp2515_release,
};
// MCP2515 设备初始化函数
int mcp2515_probe(struct spi_device *spi)
{
int ret, value;
printk("This is mcp2515_probe\\n");
spi_dev = spi; // 保存 SPI 设备指针
// 分配字符设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, "mcp2515");
if (ret < 0)
{
printk("alloc_chrdev_region error\\n");
}
// 初始化字符设备
cdev_init(&mcp2515_cdev, &mcp2515_fops);
mcp2515_cdev.owner = THIS_MODULE;
// 添加字符设备
ret = cdev_add(&mcp2515_cdev, dev_num, 1);
if (ret < 0)
{
printk("cdev_add error\\n");
return -1;
}
// 创建设备类
mcp2515_class = class_create(THIS_MODULE, "spi_to_can");
if (IS_ERR(mcp2515_class))
{
printk("mcp2515_class error\\n");
return PTR_ERR(mcp2515_class);
}
// 创建设备
mcp2515_device = device_create(mcp2515_class, NULL, dev_num, NULL, "mcp2515");
if (IS_ERR(mcp2515_device))
{
printk("mcp2515_device error\\n");
return PTR_ERR(mcp2515_device);
}
mcp2515_reset(); // 复位 MCP2515 设备
value = mcp2515_read_reg(0x0e); // 读取寄存器值
printk("value is %x\\n", value); // 打印读取的值
mcp2515_write_reg(CNF1, 0x01); // 写入寄存器配置值
mcp2515_write_reg(CNF2, 0xb1);
mcp2515_write_reg(CNF3, 0x05);
mcp2515_write_reg(RXB0CTRL, 0x60);
mcp2515_write_reg(CANINTE, 0x05);
mcp2515_change_regbit(CANCTRL, 0xe0, 0x40);
value = mcp2515_read_reg(0x0e); // 读取寄存器值
printk("value is %x\\n", value); // 打印读取的值
return 0; // 返回成功
}
// MCP2515 SPI 设备的移除函数
static int mcp2515_remove(struct spi_device *spi)
{
device_destroy(mcp2515_class, dev_num);
class_destroy(mcp2515_class);
cdev_del(&mcp2515_cdev);
unregister_chrdev_region(dev_num, 1);
return 0;
}
// MCP2515 设备匹配表,用于设备树匹配
static const struct of_device_id mcp2515_of_match_table[] =
{
{ .compatible = "my-mcp2515" },
{}
};
// MCP2515 SPI 驱动结构体
static struct spi_driver spi_mcp2515 =
{
.probe = mcp2515_probe, // 探测函数
.remove = mcp2515_remove, // 移除函数
.driver =
{
.name = "mcp2515", // 驱动名称
.owner = THIS_MODULE, // 所属模块
.of_match_table = mcp2515_of_match_table, // 设备树匹配表
},
};
// 驱动初始化函数
static int __init mcp2515_init(void)
{
int ret;
// 注册 SPI 驱动
ret = spi_register_driver(&spi_mcp2515);
if (ret < 0)
{
// 注册失败,打印错误信息
printk("spi_register_driver error\\n");
return ret;
}
return ret;
}
// 驱动退出函数
static void __exit mcp2515_exit(void)
{
// 注销 SPI 驱动
spi_unregister_driver(&spi_mcp2515);
}
module_init(mcp2515_init);
module_exit(mcp2515_exit);
MODULE_LICENSE("GPL");
至此mcp2515的驱动编写完毕。
4、spi通用驱动
万能SPI从设备驱动对应spidev, 驱动代码位于/drivers/spi/spidev.c。不用为每个spi从设备去写一个驱动,linux内核有一个万能通用的从设备驱动。为什么说spidev.c是一个通用的从设备驱动。spidev不是专门针对某一SPI硬件设备做的驱动,只是简单的注册字符设备,用户空间通过read、write、ioctl,直接对spi进行操作,相当于是用户空间和spi设备的桥梁。用户空间操作时,打开设备节点,然后通过IOCTL设置模式、速度等参数,然后就可以调用read write进行操作了。
调用device_create创建了/dev/下的spidev节点,主设备号SPIDEV_MAJOR= 153,如spi总线0上cs1设备,则设备名为/dev/spidev0.1,其他以此类推
5、mcp2515驱动移植
实际上 mcp2515 的驱动程序在内核中已经提供了,我们只需要打开然后进行设备树的配置即可。
Linux 内 核 源 码 中 默 认 已 经 有 了 MCP2515 的 驱 动 程 序,路径为
“linux_sdk/kernel/drivers/net/can/spi/mcp251x.c
但是默认是关闭的,所以需要我们去menuconfig中打开。
//在内核目录下设置环境
export ARCH=arm64
//然后打开
make menuconfig
> Networking support
> CAN bus subsystem support
> CAN Device Drivers
> CAN SPI interfaces
<*> Microchip MCP251x and MCP25625 SPI CAN controllers
配置好之后会在kernel下有一个.config文件,因为rk提供的脚本默认的配置文件时是rockchip_linux_defconfig,所以此时需要我们去用这个新生成的.config文件去覆盖原来未修改的rockchip_linux_defconfig文件。
kernel/arch/arm64/configs路径下的rockchip_linux_defconfig文件

//执行以下命令进行覆盖
cp /linux_sdk/kernel/.config /linux_sdk/kernel/arch/arm64/configs/rockchip_linux_defconfig
覆盖完了再进行make,生成内核即可。
然后修改设备树文件
&spi0 {
status = "okay";
pinctrl-0 = <&spi0m1_cs0 &spi0m1_pins>;
pinctrl-1 = <&spi0m1_cs0 &spi0m1_pins_hs>;
mcp2515: mcp2515@0
{
compatible = "microchip,mcp2515";
reg = <0>;
spi-max-frequency = <10000000>;
interrupt-parent = <&gpio0>;
interrupts = <RK_PB0 IRQ_TYPE_EDGE_FALLING>;
pinctrl-names = "default";
pinctrl-0 = <&mcp2515_int>;
clocks = <&clk8m>;
status = "okay";
};
clk8m: clk8m
{
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <8000000>;
};
};
//一定要有中断属性和时钟晶振属性
然后在 pinctrl 节点下添加以下内容
mcp2515-gpio
{
mcp2515_int:mcp2515-int
{
rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
然后将设备树和修改好的config一起make,生成boot,烧到开发板中就移植好了。