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");
