2.1 SPI协议介绍
参考资料:
- 《SPI Block Guide V04.01.pdf》
- 《S3C2440A_UserManual_Rev13.pdf》
引脚含义如下:
引脚 | 含义 |
---|---|
DO(MOSI) | Master Output, Slave Input, SPI主控用来发出数据,SPI从设备用来接收数据 |
DI(MISO) | Master Input, Slave Output, SPI主控用来发出数据,SPI从设备用来接收数据 |
SCK | Serial Clock,时钟 |
CS | Chip Select,芯片选择引脚 |
SPI控制器内部结构
这个图等我们看完后面的SPI协议,再回过头来讲解:
传输示例
假设现在主控芯片要传输一个0x56数据给SPI Flash,时序如下:
首先CS0先拉低选中SPI Flash,0x56的二进制就是0b0101 0110,因此在每个SCK时钟周期,DO输出对应的电平。
SPI Flash会在每个时钟周期的上升沿读取D0上的电平。
SPI模式
在SPI协议中,有两个值来确定SPI的模式。
CPOL:表示SPICLK的初始电平,0为电平,1为高电平
CPHA:表示相位,即第一个还是第二个时钟沿采样数据,0为第一个时钟沿,1为第二个时钟沿
CPOL | CPHA | 模式 | 含义 |
---|---|---|---|
0 | 0 | 0 | SPICLK初始电平为低电平,在第一个时钟沿采样数据 |
0 | 1 | 1 | SPICLK初始电平为低电平,在第二个时钟沿采样数据 |
1 | 0 | 2 | SPICLK初始电平为高电平,在第一个时钟沿采样数据 |
1 | 1 | 3 | SPICLK初始电平为高电平,在第二个时钟沿采样数据 |
我们常用的是模式0和模式3,因为它们都是在上升沿采样数据,不用去在乎时钟的初始电平是什么,只要在上升沿采集数据就行。 |
极性选什么?格式选什么?通常去参考外接的模块的芯片手册。比如对于OLED,查看它的芯片手册时序部分:
SCLK的初始电平我们并不需要关心,只要保证在上升沿采样数据就行。
2.2 SPI驱动框架
2.2.1 SPI体系结构
2.2.2 重要的数据结构
struct spi_master {
// 设备模型结构,用于表示SPI主控制器设备
struct device dev;
// 用于将SPI主控制器加入链表管理
struct list_head list;
// SPI总线编号,负值表示动态分配
s16 bus_num;
// 支持的片选(Chip Select)数量
u16 num_chipselect;
// DMA缓冲区对齐要求
u16 dma_alignment;
// 支持的SPI设备模式标志
u16 mode_bits;
// 支持的数据位宽掩码
u32 bits_per_word_mask;
// 最小传输速度(单位:Hz)
u32 min_speed_hz;
// 最大传输速度(单位:Hz)
u32 max_speed_hz;
// 其他限制标志,如半双工、无接收/发送等
u16 flags;
// 获取最大传输大小的回调函数
size_t (*max_transfer_size)(struct spi_device *spi);
// 获取最大消息大小的回调函数
size_t (*max_message_size)(struct spi_device *spi);
// I/O互斥锁,用于保护SPI主控制器的I/O操作
struct mutex io_mutex;
// SPI总线锁定的自旋锁和互斥锁
spinlock_t bus_lock_spinlock;
struct mutex bus_lock_mutex;
// 标志位,表示SPI总线是否被锁定独占使用
bool bus_lock_flag;
// 设置SPI设备模式、时钟等的回调函数
int (*setup)(struct spi_device *spi);
// 执行SPI传输的回调函数
int (*transfer)(struct spi_device *spi,
struct spi_message *mesg);
// 释放SPI设备资源的回调函数
void (*cleanup)(struct spi_device *spi);
// 判断是否支持DMA传输的回调函数
bool (*can_dma)(struct spi_master *master,
struct spi_device *spi,
struct spi_transfer *xfer);
// 用于支持队列化传输的标志和工作线程相关字段
bool queued;
struct kthread_worker kworker;
struct task_struct *kworker_task;
struct kthread_work pump_messages;
spinlock_t queue_lock;
struct list_head queue;
struct spi_message *cur_msg;
bool idling;
bool busy;
bool running;
bool rt;
bool auto_runtime_pm;
bool cur_msg_prepared;
bool cur_msg_mapped;
struct completion xfer_completion;
size_t max_dma_len;
// 准备/取消硬件传输的回调函数
int (*prepare_transfer_hardware)(struct spi_master *master);
int (*transfer_one_message)(struct spi_master *master,
struct spi_message *mesg);
int (*unprepare_transfer_hardware)(struct spi_master *master);
// 准备/取消消息的回调函数
int (*prepare_message)(struct spi_master *master,
struct spi_message *message);
int (*unprepare_message)(struct spi_master *master,
struct spi_message *message);
// SPI Flash读取相关回调函数
int (*spi_flash_read)(struct spi_device *spi,
struct spi_flash_read_message *msg);
bool (*flash_read_supported)(struct spi_device *spi);
// 设置片选信号的回调函数
void (*set_cs)(struct spi_device *spi, bool enable);
// 执行单次传输的回调函数
int (*transfer_one)(struct spi_master *master, struct spi_device *spi,
struct spi_transfer *transfer);
// 处理错误的回调函数
void (*handle_err)(struct spi_master *master,
struct spi_message *message);
// GPIO片选引脚数组
int *cs_gpios;
// SPI传输统计信息
struct spi_statistics statistics;
// DMA通道
struct dma_chan *dma_tx;
struct dma_chan *dma_rx;
// 全双工设备的虚拟数据缓冲区
void *dummy_rx;
void *dummy_tx;
// 固件翻译片选编号的回调函数
int (*fw_translate_cs)(struct spi_master *master, unsigned cs);
};
struct spi_driver {
const struct spi_device_id *id_table;
int (*probe)(struct spi_device *spi);
int (*remove)(struct spi_device *spi);
void (*shutdown)(struct spi_device *spi);
struct device_driver driver;
};
2.3 SPI核心
SPI核心**(drivers/spi/spi.c)
**中提供了一组不依赖于硬件平台的接口函数,这个文件一般不需要被工程师修改,但是理解其中的主要函数非常关键,因为SPI总线驱动和设备驱动之间以SPI核心作为纽带。SPI核心中的主要函数如下。
(1)增加/删除spi_master
int spi_register_master(struct spi_master *master);
void spi_unregister_master(struct spi_master *master);
(2)增加/删除spi_driver
int spi_register_driver(struct spi_driver *driver);
void spi_unregister_driver(struct spi_driver *driver);
(3)SPI传输
int spi_sync(struct spi_device *spi, struct spi_message *message);
int spi_async(struct spi_device *spi, struct spi_message *message);
spi_sync()
函数用于同步方式发送SPI消息,它会等待消息发送完成。spi_async()
函数用于异步方式发送SPI消息,它会立即返回,消息会在后台线程中处理。这两个函数内部会调用spi_master
的transfer
或transfer_one_message
函数来完成消息的传输。
(4)SPI消息构建
void spi_message_init(struct spi_message *m);
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m);
spi_message_init()
函数用于初始化一个SPI消息。spi_message_add_tail()
函数用于将一个SPI传输添加到消息的末尾。通过这两个函数,可以构建包含多个SPI传输的消息,然后通过spi_sync()
或spi_async()
函数发送。
(5)SPI设备操作
int spi_write(struct spi_device *spi, const u8 *buf, size_t len);
int spi_read(struct spi_device *spi, u8 *buf, size_t len);
int spi_write_then_read(struct spi_device *spi, const u8 *txbuf, size_t txlen, u8 *rxbuf, size_t rxlen);
spi_write()
函数用于向SPI设备发送数据。spi_read()
函数用于从SPI设备读取数据。spi_write_then_read()
函数用于先向SPI设备发送数据,然后从设备读取数据。这些函数内部会构建SPI消息并调用spi_sync()
函数来完成传输。
2.4 SPI控制器驱动程序
SPI控制器的驱动程序可以基于"平台总线设备驱动"模型来实现:
- 在设备树里描述SPI控制器的硬件信息,在设备树子节点里描述挂在下面的SPI设备的信息
- 在platform_driver中提供一个probe函数
- 它会注册一个spi_master
- 还会解析设备树子节点,创建spi_device结构体
(1)SPI Master的注册与注销
SPI 主机驱动的核心就是申请 spi_master
,然后初始化 spi_master
,最后向 Linux 内核注册spi_master
。
① spi_master
申请与释放:
spi_alloc_master
函数用于申请 spi_master
,函数原型如下:
struct spi_master *spi_alloc_master(struct device *dev, unsigned size)
{
struct spi_master *master;
if (!dev)
return NULL;
master = kzalloc(size + sizeof(*master), GFP_KERNEL);
if (!master)
return NULL;
device_initialize(&master->dev);
master->bus_num = -1;
master->num_chipselect = 1;
master->dev.class = &spi_master_class;
master->dev.parent = dev;
pm_suspend_ignore_children(&master->dev, true);
spi_master_set_devdata(master, &master[1]);
return master;
}
EXPORT_SYMBOL_GPL(spi_alloc_master);
dev:设备,一般是 platform_device 中的 dev 成员变量。
size: 私有数据大小,可以通过 spi_master_get_devdata 函数获取到这些私有数据。
返回值: 申请到的 spi_master。
spi_master
的释放通过 spi_master_put
函数来完成,当我们删除一个 SPI 主机驱动的时候就需要释放掉前面申请的 spi_master
, spi_master_put
函数原型如下:
void spi_master_put(struct spi_master *master)
②spi_master
注册
当 spi_master
初始化完成以后就需要将其注册到 Linux 内核, spi_master
注册函数为spi_register_master
,函数原型如下:
int spi_register_master(struct spi_master *master)
{
static atomic_t dyn_bus_id = ATOMIC_INIT((1<<15) - 1); // 静态变量,用于动态分配总线编号
struct device *dev = master->dev.parent; // 获取主控制器的父设备
struct boardinfo *bi; // 用于遍历板级支持信息的指针
int status = -ENODEV; // 初始化返回状态
int dynamic = 0; // 标志位,用于指示是否动态分配总线编号
// 检查主控制器是否有父设备
if (!dev)
return -ENODEV;
// 尝试通过设备树注册主控制器
status = of_spi_register_master(master);
if (status)
return status;
// 检查是否有至少一个片选信号
if (master->num_chipselect == 0)
return -EINVAL;
// 如果总线编号未指定且使用设备树,则尝试从设备树获取总线编号
if ((master->bus_num < 0) && master->dev.of_node)
master->bus_num = of_alias_get_id(master->dev.of_node, "spi");
// 如果总线编号未指定,则动态分配一个总线编号
if (master->bus_num < 0) {
master->bus_num = atomic_dec_return(&dyn_bus_id); // 动态分配总线编号
dynamic = 1;
}
// 初始化主控制器的队列和锁
INIT_LIST_HEAD(&master->queue);
spin_lock_init(&master->queue_lock);
spin_lock_init(&master->bus_lock_spinlock);
mutex_init(&master->bus_lock_mutex);
mutex_init(&master->io_mutex);
master->bus_lock_flag = 0;
init_completion(&master->xfer_completion);
if (!master->max_dma_len)
master->max_dma_len = INT_MAX;
// 注册主控制器设备
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;
}
}
// 初始化统计信息锁
spin_lock_init(&master->statistics.lock);
// 将主控制器加入全局列表并匹配板级支持信息
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);
// 注册设备树和ACPI中的SPI设备
of_register_spi_devices(master);
acpi_register_spi_devices(master);
done:
return status; // 返回最终状态
}
如果要注销 spi_master 的话可以使用 spi_unregister_master 函数,此函数原型为:
void spi_unregister_master(struct spi_master *master)
I.MX6U 的 SPI 主机驱动会采用
spi_bitbang_start
这个 API 函数来完成spi_master
的注册,spi_bitbang_start
函数内部其实也是通过调用spi_register_master
函数来完成spi_master
的注册。
(2)实现SPI Master通信方式
SPI Master注册的时候需要实现传输函数,比如 transfer
函数和**transfer_one_message
函数**。
transfer
函数
- 功能:这是 SPI 主机控制器的 核心数据传输函数,用于处理 SPI 设备之间的通信。它通常由 SPI 主机驱动实现,是与硬件直接交互的函数。
- 工作方式:
- 它接收一个
struct spi_device
和一个struct spi_message
,负责将消息中的数据通过 SPI 总线发送到目标设备。 - 它可能直接操作硬件寄存器,控制 SPI 总线的时钟、数据线等,完成数据的发送和接收。
- 它通常是一个 阻塞式 的函数,调用后会等待传输完成。
- 它接收一个
- 适用场景:
- 适用于简单的 SPI 传输场景,或者在不需要复杂队列管理的情况下直接进行数据传输。
- 通常用于 非队列化 的 SPI 驱动实现。
transfer_one_message
函数
- 功能:这是用于 队列化传输 的核心函数,主要用于处理一个完整的
spi_message
。 - 工作方式:
- 它也是接收一个
struct spi_master
和一个struct spi_message
,但它的主要任务是将消息加入到一个传输队列中。 - 它通常会启动一个工作线程(
kworker
),在后台逐步处理队列中的消息。 - 它允许多个消息排队等待处理,适合多任务、高并发的场景。
- 它也是接收一个
- 适用场景:
- 适用于需要支持 队列化传输 的 SPI 驱动,例如在多设备、多任务的复杂场景中。
- 它通常用于 队列化 的 SPI 驱动实现。
两者的区别与关系
- 区别:
transfer
函数 是一个直接的、同步的传输函数,通常用于简单的、即时的传输任务。transfer_one_message
函数 是一个异步的、队列化的传输函数,用于将消息加入队列,由工作线程在后台处理。
- 关系:
- 如果驱动程序实现了 队列化传输机制(即
queued
标志为true
),那么transfer
函数通常会被禁用(即不能同时定义transfer
和transfer_one_message
)。 - 如果驱动程序没有实现队列化机制,则需要实现
transfer
函数来直接处理数据传输。 - 从功能上来说,
transfer_one_message
是对transfer
的一种扩展,它通过队列化机制提高了系统的并发能力和灵活性。
- 如果驱动程序实现了 队列化传输机制(即
(3)SPI Master的注册示例
和 I2C 的适配器驱动一样, SPI 主机驱动一般都由 SOC 厂商编写好了,打开 imx6ull.dtsi文件,找到如下所示内容:
ecspi3: ecspi@02010000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
reg = <0x02010000 0x4000>;
interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_ECSPI3>,
<&clks IMX6UL_CLK_ECSPI3>;
clock-names = "ipg", "per";
dmas = <&sdma 7 7 1>, <&sdma 8 7 2>;
dma-names = "rx", "tx";
status = "disabled";
};
I.MX6U 的 ECSPI 主机驱动文件为 drivers/spi/spi-imx.c
,在此文件中找到如下内容:
static struct platform_device_id spi_imx_devtype[] = {
{
.name = "imx1-cspi",
.driver_data = (kernel_ulong_t) &imx1_cspi_devtype_data,
}, {
.name = "imx21-cspi",
.driver_data = (kernel_ulong_t) &imx21_cspi_devtype_data,
......
}, {
.name = "imx6ul-ecspi",
.driver_data = (kernel_ulong_t) &imx6ul_ecspi_devtype_data,
}, {
/* sentinel */
}
};
static const struct of_device_id spi_imx_dt_ids[] = {
{ .compatible = "fsl,imx1-cspi",
.data =&imx1_cspi_devtype_data, },
......
{ .compatible = "fsl,imx6ul-ecspi",
.data =&imx6ul_ecspi_devtype_data, },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, spi_imx_dt_ids);
......
......
static struct platform_driver spi_imx_driver = {
.driver = {
.name = DRIVER_NAME,
.of_match_table = spi_imx_dt_ids,
.pm = IMX_SPI_PM,
},
.id_table = spi_imx_devtype,
.probe = spi_imx_probe,
.remove = spi_imx_remove,
};
module_platform_driver(spi_imx_driver);
spi_imx_probe
函数会从设备树中读取相应的节点属性值,申请并初始化 spi_master
,最后调用 spi_bitbang_start
函数(spi_bitbang_start
会调用 spi_register_master
函数)向 Linux 内核注册spi_master
。
static int spi_imx_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node; // 获取设备树节点
const struct of_device_id *of_id =
of_match_device(spi_imx_dt_ids, &pdev->dev); // 匹配设备树 ID
struct spi_imx_master *mxc_platform_info =
dev_get_platdata(&pdev->dev); // 获取平台数据
struct spi_master *master; // SPI 主控制器结构
struct spi_imx_data *spi_imx; // 私有数据结构
struct resource *res; // 资源结构
int i, ret, irq; // 临时变量
// 检查是否提供了设备树节点或平台数据
if (!np && !mxc_platform_info) {
dev_err(&pdev->dev, "can't get the platform data\n");
return -EINVAL;
}
// 分配 SPI 主控制器结构
master = spi_alloc_master(&pdev->dev, sizeof(struct spi_imx_data));
if (!master)
return -ENOMEM;
// 设置平台设备的私有数据
platform_set_drvdata(pdev, master);
// 初始化 SPI 主控制器的基本字段
master->bits_per_word_mask = SPI_BPW_RANGE_MASK(1, 32); // 支持的数据位宽范围
master->bus_num = np ? -1 : pdev->id; // 总线编号,设备树模式下为 -1
// 获取 SPI 主控制器的私有数据
spi_imx = spi_master_get_devdata(master);
spi_imx->bitbang.master = master; // 初始化 bitbang 结构
spi_imx->dev = &pdev->dev; // 设置设备指针
// 获取设备类型数据
spi_imx->devtype_data = of_id ? of_id->data :
(struct spi_imx_devtype_data *)pdev->id_entry->driver_data;
// 如果提供了平台数据,初始化片选信号
if (mxc_platform_info) {
master->num_chipselect = mxc_platform_info->num_chipselect; // 片选数量
master->cs_gpios = devm_kzalloc(&master->dev,
sizeof(int) * master->num_chipselect, GFP_KERNEL); // 分配片选 GPIO 数组
if (!master->cs_gpios)
return -ENOMEM;
for (i = 0; i < master->num_chipselect; i++)
master->cs_gpios[i] = mxc_platform_info->chipselect[i]; // 初始化片选 GPIO
}
// 初始化 bitbang 回调函数
spi_imx->bitbang.chipselect = spi_imx_chipselect;
spi_imx->bitbang.setup_transfer = spi_imx_setupxfer;
spi_imx->bitbang.txrx_bufs = spi_imx_transfer;
spi_imx->bitbang.master->setup = spi_imx_setup;
spi_imx->bitbang.master->cleanup = spi_imx_cleanup;
spi_imx->bitbang.master->prepare_message = spi_imx_prepare_message;
spi_imx->bitbang.master->unprepare_message = spi_imx_unprepare_message;
spi_imx->bitbang.master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH; // 支持的 SPI 模式
if (is_imx35_cspi(spi_imx) || is_imx51_ecspi(spi_imx))
spi_imx->bitbang.master->mode_bits |= SPI_LOOP; // 支持回环模式
// 初始化传输完成的完成变量
init_completion(&spi_imx->xfer_done);
// 获取并映射内存资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
spi_imx->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(spi_imx->base)) {
ret = PTR_ERR(spi_imx->base);
goto out_master_put;
}
spi_imx->base_phys = res->start; // 物理地址
// 获取中断资源
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
ret = irq;
goto out_master_put;
}
// 请求中断
ret = devm_request_irq(&pdev->dev, irq, spi_imx_isr, 0,
dev_name(&pdev->dev), spi_imx);
if (ret) {
dev_err(&pdev->dev, "can't get irq%d: %d\n", irq, ret);
goto out_master_put;
}
// 获取时钟资源
spi_imx->clk_ipg = devm_clk_get(&pdev->dev, "ipg");
if (IS_ERR(spi_imx->clk_ipg)) {
ret = PTR_ERR(spi_imx->clk_ipg);
goto out_master_put;
}
spi_imx->clk_per = devm_clk_get(&pdev->dev, "per");
if (IS_ERR(spi_imx->clk_per)) {
ret = PTR_ERR(spi_imx->clk_per);
goto out_master_put;
}
// 启用时钟
ret = clk_prepare_enable(spi_imx->clk_per);
if (ret)
goto out_master_put;
ret = clk_prepare_enable(spi_imx->clk_ipg);
if (ret)
goto out_put_per;
// 获取时钟频率
spi_imx->spi_clk = clk_get_rate(spi_imx->clk_per);
// 初始化 DMA(如果支持)
if (is_imx51_ecspi(spi_imx)) {
ret = spi_imx_sdma_init(&pdev->dev, spi_imx, master);
if (ret == -EPROBE_DEFER)
goto out_clk_put;
if (ret < 0)
dev_err(&pdev->dev, "dma setup error %d, use pio\n",
ret);
}
// 调用设备类型特定的复位函数
spi_imx->devtype_data->reset(spi_imx);
// 禁用中断
spi_imx->devtype_data->intctrl(spi_imx, 0);
// 设置设备树节点
master->dev.of_node = pdev->dev.of_node;
// 启动 bitbang
ret = spi_bitbang_start(&spi_imx->bitbang);
if (ret) {
dev_err(&pdev->dev, "bitbang start failed with %d\n", ret);
goto out_clk_put;
}
// 检查片选 GPIO 是否有效
if (!master->cs_gpios) {
dev_err(&pdev->dev, "No CS GPIOs available\n");
ret = -EINVAL;
goto out_clk_put;
}
// 请求片选 GPIO
for (i = 0; i < master->num_chipselect; i++) {
if (!gpio_is_valid(master->cs_gpios[i]))
continue;
ret = devm_gpio_request(&pdev->dev, master->cs_gpios[i],
DRIVER_NAME);
if (ret) {
dev_err(&pdev->dev, "Can't get CS GPIO %i\n",
master->cs_gpios[i]);
goto out_clk_put;
}
}
// 打印成功信息
dev_info(&pdev->dev, "probed\n");
// 禁用时钟
clk_disable_unprepare(spi_imx->clk_ipg);
clk_disable_unprepare(spi_imx->clk_per);
return ret;
out_clk_put:
clk_disable_unprepare(spi_imx->clk_ipg);
out_put_per:
clk_disable_unprepare(spi_imx->clk_per);
out_master_put:
spi_master_put(master); // 释放 SPI 主控制器
return ret;
}
对于 I.MX6U 来讲, SPI 主机的最终数据收发函数为 spi_imx_transfer
,此函数通过如下层层调用最终实现 SPI 数据发送:
spi_imx_transfer
-> spi_imx_pio_transfer
-> spi_imx_push
-> spi_imx->tx
spi_imx
是个 spi_imx_data
类型的机构指针变量,其中 tx
和 rx
这两个成员变量分别为 SPI数据发送和接收函数。 I.MX6U SPI 主机驱动会维护一个 spi_imx_data
类型的变量 spi_imx
,并且使用 spi_imx_setupxfer
函数来设置 spi_imx
的 tx
和 rx
函数。根据要发送的数据数据位宽的不同,分别有 8 位、 16 位和 32 位的发送函数,如下所示:
struct spi_imx_data {
struct spi_bitbang bitbang;
struct device *dev;
struct completion xfer_done;
void __iomem *base;
unsigned long base_phys;
struct clk *clk_per;
struct clk *clk_ipg;
unsigned long spi_clk;
unsigned int spi_bus_clk;
unsigned int bytes_per_word;
unsigned int count;
void (*tx)(struct spi_imx_data *);
void (*rx)(struct spi_imx_data *);
void *rx_buf;
const void *tx_buf;
unsigned int txfifo; /* number of words pushed in tx FIFO */
/* DMA */
bool usedma;
u32 wml;
struct completion dma_rx_completion;
struct completion dma_tx_completion;
const struct spi_imx_devtype_data *devtype_data;
};
spi_imx_buf_tx_u8
spi_imx_buf_tx_u16
spi_imx_buf_tx_u32
spi_imx_buf_rx_u8
spi_imx_buf_rx_u16
spi_imx_buf_rx_u32
我们就以 spi_imx_buf_tx_u8
这个函数为例,看看,一个自己的数据发送是怎么完成的,在spi-imx.c
文件中找到如下所示内容:
#define MXC_SPI_BUF_RX(type) \
static void spi_imx_buf_rx_##type(struct spi_imx_data *spi_imx) \
{ \
unsigned int val = readl(spi_imx->base + MXC_CSPIRXDATA); \
\
if (spi_imx->rx_buf) { \
*(type *)spi_imx->rx_buf = val; \
spi_imx->rx_buf += sizeof(type); \
} \
}
#define MXC_SPI_BUF_TX(type) \
static void spi_imx_buf_tx_##type(struct spi_imx_data *spi_imx) \
{ \
type val = 0; \
\
if (spi_imx->tx_buf) { \
val = *(type *)spi_imx->tx_buf; \
spi_imx->tx_buf += sizeof(type); \
} \
\
spi_imx->count -= sizeof(type); \
\
writel(val, spi_imx->base + MXC_CSPITXDATA); \
}
MXC_SPI_BUF_RX(u8)
MXC_SPI_BUF_TX(u8)
MXC_SPI_BUF_RX(u16)
MXC_SPI_BUF_TX(u16)
MXC_SPI_BUF_RX(u32)
MXC_SPI_BUF_TX(u32)
spi_imx_buf_tx_u8
函数是通过 MXC_SPI_BUF_TX
宏来实现的。就是将要发送的数据值写入到 ECSPI
的 TXDATA
寄存器里面去,这和我们 SPI 裸机实验的方法一样。将 MXC_SPI_BUF_TX(u8)
展开就是 spi_imx_buf_tx_u8
函数。其他的 tx
和 rx
函数都是这样实现的,这里就不做介绍了。关于 I.MX6U 的主机驱动程序就讲解到这里,基本套路和 I2C 的适配器驱动程序类似。
2.5 SPI设备驱动程序
跟"平台总线设备驱动模型"类似,Linux中也有一个"SPI总线设备驱动模型":
- 左边是spi_driver,使用C文件实现,里面有id_table表示能支持哪些SPI设备,有probe函数
- 右边是spi_device,用来描述SPI设备,比如它的片选引脚、频率
- 可以来自设备树:比如由SPI控制器驱动程序解析设备树后创建、注册spi_device
- 可以来自C文件:比如使用
spi_register_board_info
创建、注册spi_device
(1)SPI driver的注册
struct spi_driver {
const struct spi_device_id *id_table;
int (*probe)(struct spi_device *spi);
int (*remove)(struct spi_device *spi);
void (*shutdown)(struct spi_device *spi);
struct device_driver driver;
};
同样的, spi_driver
初始化上面结构体的成员变量,然后需要向 Linux 内核注册, spi_driver
注册函数为spi_register_driver
,函数原型如下:
/* use a define to avoid include chaining to get THIS_MODULE */
#define spi_register_driver(driver) \
__spi_register_driver(THIS_MODULE, driver)
/*************************************************************/
int __spi_register_driver(struct module *owner, struct spi_driver *sdrv)
{
sdrv->driver.owner = owner; // 设置驱动程序的所有者模块
sdrv->driver.bus = &spi_bus_type; // 设置驱动程序所属的总线类型为 SPI 总线
// 如果用户定义了 probe 函数,则设置 driver 的 probe 函数
if (sdrv->probe)
sdrv->driver.probe = spi_drv_probe;
// 如果用户定义了 remove 函数,则设置 driver 的 remove 函数
if (sdrv->remove)
sdrv->driver.remove = spi_drv_remove;
// 如果用户定义了 shutdown 函数,则设置 driver 的 shutdown 函数
if (sdrv->shutdown)
sdrv->driver.shutdown = spi_drv_shutdown;
// 调用 driver_register 注册驱动程序
return driver_register(&sdrv->driver);
}
EXPORT_SYMBOL_GPL(__spi_register_driver);
/*************************************************************/
int driver_register(struct device_driver *drv)
{
int ret;
struct device_driver *other;
// 检查驱动程序的总线是否已初始化
BUG_ON(!drv->bus->p);
// 检查驱动程序是否使用了过时的回调函数
if ((drv->bus->probe && drv->probe) ||
(drv->bus->remove && drv->remove) ||
(drv->bus->shutdown && drv->shutdown))
printk(KERN_WARNING "Driver '%s' needs updating - please use "
"bus_type methods\n", drv->name);
// 检查是否已经注册了同名的驱动程序
other = driver_find(drv->name, drv->bus);
if (other) {
// 如果已注册,打印错误信息并返回 -EBUSY
printk(KERN_ERR "Error: Driver '%s' is already registered, "
"aborting...\n", drv->name);
return -EBUSY;
}
// 将驱动程序添加到总线中
ret = bus_add_driver(drv);
if (ret)
return ret;
// 添加驱动程序的属性组
ret = driver_add_groups(drv, drv->groups);
if (ret) {
// 如果添加属性组失败,从总线中移除驱动程序
bus_remove_driver(drv);
return ret;
}
// 发送 KOBJ_ADD 事件,通知用户空间驱动程序已注册
kobject_uevent(&drv->p->kobj, KOBJ_ADD);
return ret; // 返回最终结果
}
EXPORT_SYMBOL_GPL(driver_register);
注销 SPI 设备驱动以后也需要注销掉前面注册的 spi_driver
,使用 spi_unregister_driver
函数完成 spi_driver
的注销,函数原型如下:
void spi_unregister_driver(struct spi_driver *sdrv)
(2)SPI driver的注册示例
/* probe 函数 */
static int xxx_probe(struct spi_device *spi)
{
/* 具体函数内容 */
return 0;
}
8
/* remove 函数 */
static int xxx_remove(struct spi_device *spi)
{
/* 具体函数内容 */
return 0;
}
/* 传统匹配方式 ID 列表 */
static const struct spi_device_id xxx_id[] = {
{"xxx", 0},
{}
};
/* 设备树匹配列表 */
static const struct of_device_id xxx_of_match[] = {
{ .compatible = "xxx" },
{ /* Sentinel */ }
};
/* SPI 驱动结构体 */
static struct spi_driver xxx_driver = {
.probe = xxx_probe,
.remove = xxx_remove,
.driver = {
.owner = THIS_MODULE,
.name = "xxx",
.of_match_table = xxx_of_match,
},
.id_table = xxx_id,
};
/* 驱动入口函数 */
static int __init xxx_init(void)
{
return spi_register_driver(&xxx_driver);
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
spi_unregister_driver(&xxx_driver);
}
module_init(xxx_init);
module_exit(xxx_exit);
(3)SPI 设备和驱动匹配过程
SPI总线为 spi_bus_type
,定义在 drivers/spi/spi.c
文件中,内容如下:
struct bus_type spi_bus_type = {
.name = "spi",
.dev_groups = spi_dev_groups,
.match = spi_match_device,
.uevent = spi_uevent,
};
EXPORT_SYMBOL_GPL(spi_bus_type);
SPI 设备和驱动的匹配函数为 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);
/* 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;
/* 传统的、无设备树的 SPI 设备和驱动匹配 */
if (sdrv->id_table)
return !!spi_match_id(sdrv->id_table, spi);
return strcmp(spi->modalias, drv->name) == 0;
}
(4)SPI 设备数据收发处理流程
SPI 设备驱动的核心是 spi_driver
。当我们向 Linux 内核注册成功 spi_driver
以后就可以使用 SPI 核心层提供的 API 函数来对设备进行读写操作了。首先是 spi_transfer
结构体,此结构体用于描述 SPI 传输信息,结构体内容如下:
struct spi_transfer {
/* it's ok if tx_buf == rx_buf (right?)
* for MicroWire, one buffer must be null
* buffers must work with dma_*map_single() calls, unless
* spi_message.is_dma_mapped reports a pre-existing mapping
*/
const void *tx_buf; // 发送缓冲区,存储要发送的数据
void *rx_buf; // 接收缓冲区,存储接收到的数据
unsigned len;
dma_addr_t tx_dma; // DMA 地址,用于发送数据
dma_addr_t rx_dma; // DMA 地址,用于接收数据
struct sg_table tx_sg; // 散列缓冲区,用于发送数据(支持 DMA)
struct sg_table rx_sg; // 散列缓冲区,用于接收数据(支持 DMA)
unsigned cs_change:1; // 是否改变片选信号,1 表示在传输后改变片选信号
unsigned tx_nbits:3; // 每个字的发送位数(1、2 或 4 位)
unsigned rx_nbits:3;
#define SPI_NBITS_SINGLE 0x01 /* 1bit transfer */
#define SPI_NBITS_DUAL 0x02 /* 2bits transfer */
#define SPI_NBITS_QUAD 0x04 /* 4bits transfer */
u8 bits_per_word;
u16 delay_usecs;
u32 speed_hz;
struct list_head transfer_list;// 链表节点,用于将多个传输添加到 SPI 消息队列
};
len
是要进行传输的数据长度, SPI 是全双工通信,因此在一次通信中发送和接收的字节数都是一样的,所以 spi_transfer 中也就没有发送长度和接收长度之分。
spi_transfer
需要组织成 spi_message
, spi_message
也是一个结构体,内容如下:
struct spi_message {
struct list_head transfers; // 存储该消息中的所有 SPI 传输
struct spi_device *spi; // 关联的 SPI 设备
unsigned is_dma_mapped:1; // 是否进行了 DMA 映射
void (*complete)(void *context); // 完成回调函数
void *context; // 回调函数的上下文参数
unsigned frame_length; // 消息的总帧长度(位)
unsigned actual_length; // 实际传输的数据长度(字节)
int status; // 消息的处理状态
struct list_head queue; // 用于将消息添加到队列
void *state; // 供当前拥有该消息的驱动使用
struct list_head resources; // 存储与消息关联的资源
};
在使用spi_message
之前需要对其进行初始化, spi_message
初始化函数为spi_message_init
,函数原型如下:
void spi_message_init(struct spi_message *m)
spi_message
初始化完成以后需要将 spi_transfer
添加到 spi_message
队列中,这里我们要用到 spi_message_add_tail
函数,此函数原型如下:
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
spi_message
准备好以后就可以进行数据传输了,数据传输分为同步传输和异步传输,同步传输会阻塞的等待 SPI 数据传输完成,同步传输函数为 spi_sync
,函数原型如下:
int spi_sync(struct spi_device *spi, struct spi_message *message)
异步传输不会阻塞的等到 SPI 数据传输完成,异步传输需要设置 spi_message
中的 complete
成员变量, complete
是一个回调函数,当 SPI 异步传输完成以后此函数就会被调用。 SPI 异步传输函数为 spi_async
,函数原型如下:
int spi_async(struct spi_device *spi, struct spi_message *message)
为什么 spi_transfer
需要组织成 spi_message
?
- 按顺序执行多个传输:
- 在 SPI 通信中,一个设备可能需要连续执行多个操作(如发送命令、读取数据等)。每个操作可以表示为一个
spi_transfer
,而spi_message
则将这些传输组织成一个逻辑上的“消息”,确保它们按顺序执行。 - 例如,读取一个寄存器的值可能需要先发送寄存器地址(一个
spi_transfer
),然后读取数据(另一个spi_transfer
)。通过将这两个传输组织成一个spi_message
,可以确保它们按顺序执行。
- 在 SPI 通信中,一个设备可能需要连续执行多个操作(如发送命令、读取数据等)。每个操作可以表示为一个
- 整体管理:
spi_message
提供了一个整体的管理机制,可以设置消息的属性(如 DMA 映射、完成回调等),并跟踪整个消息的执行状态。- 例如,
spi_message
的complete
回调函数可以在消息处理完成后通知应用程序。
队列在 SPI 通信中的作用
- 按顺序处理消息:
- SPI 主控制器(
spi_master
)通常会有一个队列来管理多个spi_message
。队列的作用是按顺序处理这些消息,确保消息的执行顺序。 - 例如,
spi_controller
结构体中的queue
成员用于存储等待传输的消息队列。
- SPI 主控制器(
- 异步处理:
- 队列支持异步操作,允许应用程序提交多个消息,而无需等待每个消息完成。
- 例如,
spi_async
函数可以将消息添加到队列中,然后由内核线程(如kworker
)异步处理
2.6 示例
(1)I.MX6U ECSPI 简介
I.MX6U 自带的 SPI 外设叫做 ECSPI,全称是 Enhanced Configurable Serial Peripheral Interface,别看前面加了个“EC”就以为和标准 SPI 有啥不同的, 其实就是 SPI。 ECSPI 有 6432 个接收FIFO(RXFIFO)和 6432 个发送 FIFO(TXFIFO) 。
①、全双工同步串行接口。
②、可配置的主/从模式。
③、四个片选信号,支持多从机。
④、发送和接收都有一个 32x64 的 FIFO。
⑤、片选信号 SS/CS,时钟信号 SCLK 极性可配置。
⑥、支持 DMA。
I.MX6U 的 ECSPI 可以工作在主模式或从模式,本章我们使用主模式, I.MX6U 有 4 个ECSPI,每个 ECSPI 支持四个片选信号,也就说,如果你要使用 ECSPI 的硬件片选信号的话,一个 ECSPI 可以支持 4 个外设。如果不使用硬件的片选信号就可以支持无数个外设,本章实验我们不使用硬件片选信号,因为硬件片选信号只能使用指定的片选 IO,软件片选的话可以使用任意的 IO。
均抄录自 《正点原子 imx6ull 驱动开发指南》
(2)ICM-20608 简介
ICM-20608 是 InvenSense 出品的一款 6 轴 MEMS 传感器,包括 3 轴加速度和 3 轴陀螺仪。ICM-20608 尺寸非常小,只有 3x3x0.75mm,采用 16P 的 LGA 封装。 ICM-20608 内部有一个 512字节的 FIFO。陀螺仪的量程范围可以编程设置,可选择± 250,±500,±1000 和±2000° /s,加速度的量程范围也可以编程设置,可选择± 2g,±4g, ±8g 和±16g。陀螺仪和加速度计都是 16 位的 ADC,并且支持 I2C 和 SPI 两种协议,使用 I2C 接口的话通信速度最高可以达到400KHz,使用 SPI 接口的话通信速度最高可达到 8MHz。
①、陀螺仪支持 X,Y 和 Z 三轴输出,内部集成 16 位 ADC,测量范围可设置:±250,± 500,±1000 和±2000° /s
②、加速度计支持 X,Y 和 Z 轴输出,内部集成 16 位 ADC,测量范围可设置:±2g,±4g, ±4g,±8g 和±16g
③、用户可编程中断
④、内部包含 512 字节的 FIFO
⑤、内部包含一个数字温度传感器。
⑥、耐 10000g 的冲击。
⑦、支持快速 I2C,速度可达 400KHz
⑧、支持 SPI,速度可达 8MHz。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
如果使用 IIC 接口的话 ICM-20608 的 AD0 引脚决定 I2C 设备从地址的最后一位,如果 AD0为 0 的话 ICM-20608 从设备地址是 0X68,如果 AD0 为 1 的话 ICM-20608 从设备地址为 0X69。
本章我们使用 SPI 接口,跟上一章使用 AP3216C 一样, ICM-20608 也是通过读写寄存器来配置和读取传感器数据,使用 SPI 接口读写寄存器需要 16 个时钟或者更多(如果读写操作包括多个字节的话)
第一个字节包含要读写的寄存器地址,寄存器地址最高位是读写标志位,如果是读的话寄存器地址最高位要为 1,如果是写的话寄存器地址最高位要为 0,剩下的 7 位才是实际的寄存器地址,寄存器地址后面跟着的就是读写的数据。
表 27.1.3.1 列出了本章实验用到的一些寄存器和位,关于 ICM-20608 的详细寄存器和位的介绍请参考 ICM-20608 的寄存器手册:
寄存器地址 | 位 | 寄存器功能 | 描述 |
---|---|---|---|
0X19 | SMLPRT_DIV[7:0] | 输出速率设置 | 设置输出速率,输出速率计算公式如下:SAMPLE_RATE=INTERNAL_SAMPLE_RATE/(1 + SMPLRT_DIV) |
0X1A | DLPF_CFG[2:0] | 芯片配置 | 设置陀螺仪低通滤波。可设置 0~7。 |
0X1B | FS_SEL[1:0] | 陀螺仪量程设置 | 0:±250dps;1:±500dps;2:±1000dps;3:±2000dps |
0X1C | ACC_FS_SEL[1:0] | 加速度计量程设置 | 0:±2g;1:±4g;2:±8g;3:±16g |
0X1D | A_DLPF_CFG[2:0] | 加速度计低通滤波设置 | 设置加速度计的低通滤波,可设置 0~7。 |
0X1E | GYRO_CYCLE[7] | 陀螺仪低功耗使能 | 0:关闭陀螺仪的低功耗功能。 1:使能陀螺仪的低功耗功能。 |
0X23 | YG_FIFO_EN[5] | FIFO 使能控制 | 1:使能陀螺仪 Y 轴 FIFO。 0:关闭陀螺仪 Y 轴 FIFO。 |
0X23 | ZG_FIFO_EN[4] | FIFO 使能控制 | 1:使能陀螺仪 Z 轴 FIFO。 0:关闭陀螺仪 Z 轴 FIFO。 |
0X3B | ACCEL_FIFO_EN[3] | FIFO 使能控制 | 1:使能加速度计 FIFO。 0:关闭加速度计 FIFO。 |
0X3B | ACCEL_XOUT_H[7:0] | 加速度 X 轴数据高 8 位 | 加速度 X 轴数据高 8 位 |
0X3C | ACCEL_XOUT_L[7:0] | 加速度 X 轴数据低 8 位 | 加速度 X 轴数据低 8 位 |
0X3D | ACCEL_YOUT_H[7:0] | 加速度 Y 轴数据高 8 位 | 加速度 Y 轴数据高 8 位 |
0X3E | ACCEL_YOUT_L[7:0] | 加速度 Y 轴数据低 8 位 | 加速度 Y 轴数据低 8 位 |
0X3F | ACCEL_ZOUT_H[7:0] | 加速度 Z 轴数据高 8 位 | 加速度 Z 轴数据高 8 位 |
0X40 | ACCEL_ZOUT_L[7:0] | 加速度 Z 轴数据低 8 位 | 加速度 Z 轴数据低 8 位 |
0X41 | TEMP_OUT_H[7:0] | 温度数据高 8 位 | 温度数据高 8 位 |
0X42 | TEMP_OUT_L[7:0] | 温度数据低 8 位 | 温度数据低 8 位 |
0X43 | GYRO_XOUT_H[7:0] | 陀螺仪 X 轴数据高 8 位 | 陀螺仪 X 轴数据高 8 位 |
0X44 | GYRO_XOUT_L[7:0] | 陀螺仪 X 轴数据低 8 位 | 陀螺仪 X 轴数据低 8 位 |
0X45 | GYRO_YOUT_H[7:0] | 陀螺仪 Y 轴数据高 8 位 | 陀螺仪 Y 轴数据高 8 位 |
0X46 | GYRO_YOUT_L[7:0] | 陀螺仪 Y 轴数据低 8 位 | 陀螺仪 Y 轴数据低 8 位 |
0X47 | GYRO_ZOUT_H[7:0] | 陀螺仪 Z 轴数据高 8 位 | 陀螺仪 Z 轴数据高 8 位 |
0X48 | GYRO_ZOUT_L[7:0] | 陀螺仪 Z 轴数据低 8 位 | 陀螺仪 Z 轴数据低 8 位 |
0X6B | DEVICE_RESET[7] | 电源管理寄存器 1 | 1:复位 ICM-20608。 |
0X6B | SLEEP[6] | 电源管理寄存器 1 | 0:退出休眠模式;1,进入休眠模式 |
0X6C | STBY_XA[5] | 电源管理寄存器 2 | 0:使能加速度计 X 轴。 1:关闭加速度计 X 轴。 |
0X6C | STBY_YA[4] | 电源管理寄存器 2 | 0:使能加速度计 Y 轴。 1:关闭加速度计 Y 轴。 |
0X6C | STBY_ZA[3] | 电源管理寄存器 2 | 0:使能加速度计 Z 轴。 1:关闭加速度计 Z 轴。 |
0X6C | STBY_XG[2] | 电源管理寄存器 2 | 0:使能陀螺仪 X 轴。 1:关闭陀螺仪 X 轴。 |
0X6C | STBY_YG[1] | 电源管理寄存器 2 | 0:使能陀螺仪 Y 轴。 1:关闭陀螺仪 Y 轴。 |
0X6C | STBY_ZG[0] | 电源管理寄存器 2 | 0:使能陀螺仪 Z 轴。 1:关闭陀螺仪 Z 轴。 |
0X75 | WHOAMI[7:0] | ID 寄存器 | ICM-20608G 的 ID 为 0XAF,ICM-20608D 的 ID 为 0XAE。 |