12-Linux驱动开发- SPI子系统

SPI 驱动基本知识
html 复制代码
1.SPI 物理总线
SPI (Serial Peripheral Interface) 是一种高速、全双工、同步的通信总线。标准的 SPI 通常使用 4 根线:
	SCLK (Serial Clock): 时钟线,由主设备(SoC/MCU)产生。
	MOSI (Master Output Slave Input): 主发从收数据线。
	MISO (Master Input Slave Output): 主收从发数据线(对于OLED这种只显示的设备,这条线通常不接)。
	CS/SS (Chip Select / Slave Select): 片选线,低电平有效,用于选择通信的目标设备。
2.时序与模式 (CPOL & CPHA)
SPI 的核心在于时钟极性 (CPOL) 和 时钟相位 (CPHA) 的组合,这决定了数据是在时钟的上升沿还是下降沿被采样。
Mode 0 | 0 (低电平) | 0 (第一个边沿) | 最常用。上升沿采样,下降沿切换。
Mode 1 | 0 (低电平) | 1 (第二个边沿) | 下降沿采样,上升沿切换。
Mode 2 | 1 (高电平) | 0 (第一个边沿) | 下降沿采样,上升沿切换。
Mode 3 | 1 (高电平) | 1 (第二个边沿) | 上升沿采样,下降沿切换。
SPI 驱动框架与核心数据结构

核心数据结构

html 复制代码
struct spi_master (控制器):
	代表 SPI 硬件控制器(如 SoC 内部的 SPI 模块)。
	包含传输算法、总线号、片选数量等信息
struct spi_device (从设备):
	代表挂在 SPI 总线上的具体硬件(如 OLED 屏)。
	包含:max_speed_hz (最大速率), chip_select (片选号), mode (SPI模式)。
	来源: 通常由设备树 (Device Tree) 解析而来。
struct spi_driver (设备驱动):
	这是我们需要编写的驱动结构体。
	包含:probe (匹配成功后的初始化), remove (卸载), id_table (匹配列表)。

数据传输结构

html 复制代码
SPI 的数据传输是通过消息队列机制完成的:
struct spi_transfer: 代表一次读写操作(包含发送buf、接收buf、长度)。
struct spi_message: 一个消息包含一串 spi_transfer,原子性地执行。
SPI 总线、核心层与控制器

层次划分

html 复制代码
SPI 核心层 (drivers/spi/spi.c):
	负责注册 SPI 总线。
	提供 API(如 spi_register_driver, spi_sync)。
	实现设备与驱动的匹配逻辑(Match)。
SPI 控制器驱动 (Master Driver):
	操作硬件寄存器,产生时钟和数据信号。
SPI 协议驱动 (Device Driver):
	针对特定外设(OLED)编写,不关心底层寄存器,只调用核心层 API 发送数据。

匹配流程

html 复制代码
系统启动,解析设备树中的 OLED 节点,生成 struct spi_device 并注册到 SPI 总线。
加载我们在第五部分编写的模块,注册 struct spi_driver 到 SPI 总线。
SPI 总线通过 .compatible 属性(如 "solomon,ssd1306")进行匹配。
匹配成功,调用驱动的 probe() 函数。
驱动编写常用 API

简易 API (常用)

c 复制代码
spi_write(struct spi_device *spi, const void *buf, size_t len);//同步写数据。
spi_read(struct spi_device *spi, void *buf, size_t len);//同步读数据。
spi_w8r8(struct spi_device *spi, u8 cmd);//写 1 字节,读 1 字节。

高级 API (构建消息)

如果需要在一个 CS 有效周期内发送复杂的命令+数据组合,需要手动构建消息:

c 复制代码
spi_message_init(struct spi_message *m);//初始化消息头
spi_message_add_tail(struct spi_transfer *t, struct spi_message *m);//将传输段添加到消息。
spi_sync(struct spi_device *spi, struct spi_message *m);//同步传输(阻塞等待完成)。
spi_async(struct spi_device *spi, struct spi_message *m);//异步传输(立即返回,完成时调用回调函数,用于高性能场景)。
SPI 驱动 OLED 液晶屏示例

为 SSD1306 OLED 编写一个字符设备驱动。
硬件连接与设备树 (DTS)

除了 SPI 线(SCK, MOSI, CS),OLED 通常还需要两个 GPIO:

D/C (Data/Command): 高电平发数据,低电平发命令。

RES (Reset): 复位引脚。

c 复制代码
#include <dt-bindings/pinctrl/stm32-pinfunc.h>
#include <dt-bindings/input/input.h>
#include <dt-bindings/mfd/st,stpmic1.h>
#include <dt-bindings/gpio/gpio.h>

/{
	fragment@0{
		target=<&spi5>;
		__overlay__{
			#address-cells = <1>;
			#size-cells = <0>;
            		pinctrl-names = "default", "sleep";
            		pinctrl-0 = <&spi5_pins_a>;
            		pinctrl-1 = <&spi5_sleep_pins_a>;
			cs-gpios = <&gpiob 6 0>;
			status = "okay";

			spi_oled@0 {
				compatible = "fire,spi_oled";
				spi-max-frequency = <10000000>;
				d_c_control_pin = <&gpiof 6 0>;
				reg = <0>;
			};
		};
	};
};

spi_oled.c驱动文件

c 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/spi/spi.h>
#include <linux/gpio/consumer.h> // 新版 GPIO 接口
#include <linux/delay.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>

#define DRIVER_NAME "oled_spi"

/* 自定义设备结构体,用于管理上下文 */
struct oled_device {
    struct spi_device *spi;
    struct gpio_desc *dc_gpio;   // D/C 引脚描述符
    struct gpio_desc *rst_gpio;  // 复位 引脚描述符
    dev_t dev_num;               // 设备号
    struct cdev cdev;            // 字符设备
    struct class *class;         // 类
    struct device *device;       // 设备
    void *buffer;                // 显存 Buffer (可选,用于优化)
};

/* OLED 初始化命令序列 (SSD1306) */
static const u8 oled_init_cmd[] = {
    0xAE, 0x00, 0x10, 0x40, 0x81, 0xCF, 0xA1, 0xC8,
    0xA6, 0xA8, 0x3F, 0xD3, 0x00, 0xD5, 0x80, 0xD9,
    0xF1, 0xDA, 0x12, 0xDB, 0x40, 0x20, 0x02, 0x8D,
    0x14, 0xA4, 0xA6, 0xAF
};

/* * 优化后的写数据/命令函数 
 * len: 数据长度
 * is_cmd: 1为命令,0为数据
 */
static int oled_write_bytes(struct oled_device *oled, const u8 *data, size_t len, int is_cmd)
{
    struct spi_message m;
    struct spi_transfer t = {0}; // 在栈上定义,无需 malloc

    /* 1. 设置 D/C 引脚电平 (使用新版 gpiod 接口) */
    /* 命令=Low(0), 数据=High(1) */
    gpiod_set_value(oled->dc_gpio, is_cmd ? 0 : 1);

    /* 2. 构建 SPI 消息 */
    t.tx_buf = data;
    t.len = len;
    spi_message_init(&m);
    spi_message_add_tail(&t, &m);

    /* 3. 同步发送 */
    return spi_sync(oled->spi, &m);
}

static void oled_hardware_init(struct oled_device *oled)
{
    /* 1. 硬件复位 */
    if (oled->rst_gpio) {
        gpiod_set_value(oled->rst_gpio, 1); // 如果是低电平复位,这里设为高(无效)
        mdelay(10);
        gpiod_set_value(oled->rst_gpio, 0); // 复位
        mdelay(10);
        gpiod_set_value(oled->rst_gpio, 1); // 释放复位
    }

    /* 2. 发送初始化序列 */
    // 这种写法比逐个发送字节效率高得多
    oled_write_bytes(oled, oled_init_cmd, sizeof(oled_init_cmd), 1);
}

/* * 字符设备 write 函数 
 * 应用层直接传入要显示的数据(假设应用层已经做好了点阵转换)
 */
static ssize_t oled_cdev_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
    struct oled_device *oled = filp->private_data;
    u8 *tmp_buf;
    int ret;

    /* 限制最大传输大小,防止内核内存爆掉 */
    if (count > 4096) count = 4096;

    tmp_buf = kzalloc(count, GFP_KERNEL);
    if (!tmp_buf) return -ENOMEM;

    if (copy_from_user(tmp_buf, buf, count)) {
        kfree(tmp_buf);
        return -EFAULT;
    }

    /* 发送数据到 OLED */
    ret = oled_write_bytes(oled, tmp_buf, count, 0); // 0 表示数据

    kfree(tmp_buf);
    return ret ? ret : count;
}

static int oled_cdev_open(struct inode *inode, struct file *filp)
{
    /* 获取我们在 probe 中保存的结构体指针 */
    struct oled_device *oled = container_of(inode->i_cdev, struct oled_device, cdev);
    filp->private_data = oled;
    return 0;
}

static const struct file_operations oled_fops = {
    .owner = THIS_MODULE,
    .open = oled_cdev_open,
    .write = oled_cdev_write,
};

/* Probe 函数:驱动匹配成功时调用 */
static int oled_probe(struct spi_device *spi)
{
    struct oled_device *oled;
    int ret;

    /* 1. 申请私有结构体内存 (自动管理,卸载时自动释放) */
    oled = devm_kzalloc(&spi->dev, sizeof(*oled), GFP_KERNEL);
    if (!oled) return -ENOMEM;

    oled->spi = spi;
    spi_set_drvdata(spi, oled); // 将数据绑定到 spi 设备上

    /* 2. 获取 GPIO (匹配设备树中的 dc-gpios 和 reset-gpios) */
    /* GPIOD_OUT_LOW_INIT 表示获取后设置为输出低电平 */
    oled->dc_gpio = devm_gpiod_get(&spi->dev, "dc", GPIOD_OUT_LOW);
    if (IS_ERR(oled->dc_gpio)) {
        dev_err(&spi->dev, "Failed to get DC GPIO\n");
        return PTR_ERR(oled->dc_gpio);
    }

    /* reset 是可选的 */
    oled->rst_gpio = devm_gpiod_get_optional(&spi->dev, "reset", GPIOD_OUT_HIGH);

    /* 3. SPI 设置 (通常相信设备树的配置,这里仅作防御性设置) */
    spi->mode = SPI_MODE_0;
    spi->bits_per_word = 8;
    ret = spi_setup(spi);
    if (ret < 0) return ret;

    /* 4. 硬件初始化 OLED */
    oled_hardware_init(oled);

    /* 5. 注册字符设备 */
    ret = alloc_chrdev_region(&oled->dev_num, 0, 1, DRIVER_NAME);
    if (ret < 0) return ret;

    cdev_init(&oled->cdev, &oled_fops);
    oled->cdev.owner = THIS_MODULE;
    ret = cdev_add(&oled->cdev, oled->dev_num, 1);
    if (ret < 0) goto error_cdev;

    oled->class = class_create(THIS_MODULE, DRIVER_NAME);
    oled->device = device_create(oled->class, NULL, oled->dev_num, NULL, DRIVER_NAME);

    dev_info(&spi->dev, "OLED Driver Probed!\n");
    return 0;

error_cdev:
    unregister_chrdev_region(oled->dev_num, 1);
    return ret;
}

static void oled_remove(struct spi_device *spi)
{
    struct oled_device *oled = spi_get_drvdata(spi);

    device_destroy(oled->class, oled->dev_num);
    class_destroy(oled->class);
    cdev_del(&oled->cdev);
    unregister_chrdev_region(oled->dev_num, 1);
    
    dev_info(&spi->dev, "OLED Driver Removed\n");
    // devm_kzalloc 申请的内存会自动释放
}

/* 匹配列表 */
static const struct of_device_id oled_dt_ids[] = {
    { .compatible = "alientek,oled" }, // 必须与设备树一致
    { }
};
MODULE_DEVICE_TABLE(of, oled_dt_ids);

static struct spi_driver oled_driver = {
    .driver = {
        .name = "oled_spi",
        .of_match_table = oled_dt_ids,
    },
    .probe = oled_probe,
    .remove = oled_remove,
};

module_spi_driver(oled_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("SPI OLED Driver");
相关推荐
沸速存储5 分钟前
CPU 和 GPU 核心差别在哪?为什么 AI 训练离不开 GPU
服务器·人工智能·科技·嵌入式硬件·电脑
呱呱巨基1 小时前
Linux 查找命令
linux·笔记·学习·查找命令
basketball6161 小时前
软考中级网络工程师 第4章 无线通信网 备考总结
linux·服务器·网络·网络工程师·软考
我不会JAVA!1 小时前
【无标题】
嵌入式硬件·h.264
芯岭技术郦1 小时前
OM6625A芯片 Timer 定时器计数模式使用说明
stm32·单片机·嵌入式硬件
好评1242 小时前
【Linux】进程信号
linux·运维·服务器
zhangrelay2 小时前
ROS 2 Lyrical 第4章 URDF机器人建模与Gazebo Garden仿真
linux·笔记·学习·ubuntu·机器人·ros2
wuminyu2 小时前
JVM利用io_uring优化堆外内存性能原理剖析
java·linux·c语言·jvm·c++
xianyuCcCcCCCcc2 小时前
Nginx 深度解析:从基础架构到反向代理与负载均衡实战全解
linux·运维·nginx·bash·负载均衡
爱折腾的小码农3 小时前
解决Navicat 17 Premium Lite在Linux上运行报错“LIBSYSTEMD_251‘ not found”问题
linux·运维