欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 教育 > 锐评 > Linux驱动开发--SPI子系统

Linux驱动开发--SPI子系统

2025/10/21 12:08:15 来源:https://blog.csdn.net/qq_62494717/article/details/146610457  浏览:    关键词:Linux驱动开发--SPI子系统

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从设备用来接收数据
SCKSerial Clock,时钟
CSChip 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为第二个时钟沿

CPOLCPHA模式含义
000SPICLK初始电平为低电平,在第一个时钟沿采样数据
011SPICLK初始电平为低电平,在第二个时钟沿采样数据
102SPICLK初始电平为高电平,在第一个时钟沿采样数据
113SPICLK初始电平为高电平,在第二个时钟沿采样数据
我们常用的是模式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_mastertransfertransfer_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_masterspi_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 函数通常会被禁用(即不能同时定义 transfertransfer_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); // 匹配设备树 IDstruct 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;// 启动 bitbangret = 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;}// 请求片选 GPIOfor (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 类型的机构指针变量,其中 txrx 这两个成员变量分别为 SPI数据发送和接收函数。 I.MX6U SPI 主机驱动会维护一个 spi_imx_data 类型的变量 spi_imx,并且使用 spi_imx_setupxfer 函数来设置 spi_imxtxrx 函数。根据要发送的数据数据位宽的不同,分别有 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_u32spi_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 宏来实现的。就是将要发送的数据值写入到 ECSPITXDATA 寄存器里面去,这和我们 SPI 裸机实验的方法一样。将 MXC_SPI_BUF_TX(u8)展开就是 spi_imx_buf_tx_u8 函数。其他的 txrx 函数都是这样实现的,这里就不做介绍了。关于 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) {// 如果已注册,打印错误信息并返回 -EBUSYprintk(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_messagespi_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

  1. 按顺序执行多个传输
    • 在 SPI 通信中,一个设备可能需要连续执行多个操作(如发送命令、读取数据等)。每个操作可以表示为一个 spi_transfer,而 spi_message 则将这些传输组织成一个逻辑上的“消息”,确保它们按顺序执行。
    • 例如,读取一个寄存器的值可能需要先发送寄存器地址(一个 spi_transfer),然后读取数据(另一个 spi_transfer)。通过将这两个传输组织成一个 spi_message,可以确保它们按顺序执行。
  2. 整体管理
    • spi_message 提供了一个整体的管理机制,可以设置消息的属性(如 DMA 映射、完成回调等),并跟踪整个消息的执行状态。
    • 例如,spi_messagecomplete 回调函数可以在消息处理完成后通知应用程序。

队列在 SPI 通信中的作用

  1. 按顺序处理消息
    • SPI 主控制器(spi_master)通常会有一个队列来管理多个 spi_message。队列的作用是按顺序处理这些消息,确保消息的执行顺序。
    • 例如,spi_controller 结构体中的 queue 成员用于存储等待传输的消息队列。
  2. 异步处理
    • 队列支持异步操作,允许应用程序提交多个消息,而无需等待每个消息完成。
    • 例如,spi_async 函数可以将消息添加到队列中,然后由内核线程(如 kworker)异步处理

linux设备驱动 spi详解4-spi的数据传输流程 - Action_er - 博客园

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 的寄存器手册:

寄存器地址寄存器功能描述
0X19SMLPRT_DIV[7:0]输出速率设置设置输出速率,输出速率计算公式如下:SAMPLE_RATE=INTERNAL_SAMPLE_RATE/(1 + SMPLRT_DIV)
0X1ADLPF_CFG[2:0]芯片配置设置陀螺仪低通滤波。可设置 0~7。
0X1BFS_SEL[1:0]陀螺仪量程设置0:±250dps;1:±500dps;2:±1000dps;3:±2000dps
0X1CACC_FS_SEL[1:0]加速度计量程设置0:±2g;1:±4g;2:±8g;3:±16g
0X1DA_DLPF_CFG[2:0]加速度计低通滤波设置设置加速度计的低通滤波,可设置 0~7。
0X1EGYRO_CYCLE[7]陀螺仪低功耗使能0:关闭陀螺仪的低功耗功能。
1:使能陀螺仪的低功耗功能。
0X23YG_FIFO_EN[5]FIFO 使能控制1:使能陀螺仪 Y 轴 FIFO。
0:关闭陀螺仪 Y 轴 FIFO。
0X23ZG_FIFO_EN[4]FIFO 使能控制1:使能陀螺仪 Z 轴 FIFO。
0:关闭陀螺仪 Z 轴 FIFO。
0X3BACCEL_FIFO_EN[3]FIFO 使能控制1:使能加速度计 FIFO。
0:关闭加速度计 FIFO。
0X3BACCEL_XOUT_H[7:0]加速度 X 轴数据高 8 位加速度 X 轴数据高 8 位
0X3CACCEL_XOUT_L[7:0]加速度 X 轴数据低 8 位加速度 X 轴数据低 8 位
0X3DACCEL_YOUT_H[7:0]加速度 Y 轴数据高 8 位加速度 Y 轴数据高 8 位
0X3EACCEL_YOUT_L[7:0]加速度 Y 轴数据低 8 位加速度 Y 轴数据低 8 位
0X3FACCEL_ZOUT_H[7:0]加速度 Z 轴数据高 8 位加速度 Z 轴数据高 8 位
0X40ACCEL_ZOUT_L[7:0]加速度 Z 轴数据低 8 位加速度 Z 轴数据低 8 位
0X41TEMP_OUT_H[7:0]温度数据高 8 位温度数据高 8 位
0X42TEMP_OUT_L[7:0]温度数据低 8 位温度数据低 8 位
0X43GYRO_XOUT_H[7:0]陀螺仪 X 轴数据高 8 位陀螺仪 X 轴数据高 8 位
0X44GYRO_XOUT_L[7:0]陀螺仪 X 轴数据低 8 位陀螺仪 X 轴数据低 8 位
0X45GYRO_YOUT_H[7:0]陀螺仪 Y 轴数据高 8 位陀螺仪 Y 轴数据高 8 位
0X46GYRO_YOUT_L[7:0]陀螺仪 Y 轴数据低 8 位陀螺仪 Y 轴数据低 8 位
0X47GYRO_ZOUT_H[7:0]陀螺仪 Z 轴数据高 8 位陀螺仪 Z 轴数据高 8 位
0X48GYRO_ZOUT_L[7:0]陀螺仪 Z 轴数据低 8 位陀螺仪 Z 轴数据低 8 位
0X6BDEVICE_RESET[7]电源管理寄存器 11:复位 ICM-20608。
0X6BSLEEP[6]电源管理寄存器 10:退出休眠模式;1,进入休眠模式
0X6CSTBY_XA[5]电源管理寄存器 20:使能加速度计 X 轴。
1:关闭加速度计 X 轴。
0X6CSTBY_YA[4]电源管理寄存器 20:使能加速度计 Y 轴。
1:关闭加速度计 Y 轴。
0X6CSTBY_ZA[3]电源管理寄存器 20:使能加速度计 Z 轴。
1:关闭加速度计 Z 轴。
0X6CSTBY_XG[2]电源管理寄存器 20:使能陀螺仪 X 轴。
1:关闭陀螺仪 X 轴。
0X6CSTBY_YG[1]电源管理寄存器 20:使能陀螺仪 Y 轴。
1:关闭陀螺仪 Y 轴。
0X6CSTBY_ZG[0]电源管理寄存器 20:使能陀螺仪 Z 轴。
1:关闭陀螺仪 Z 轴。
0X75WHOAMI[7:0]ID 寄存器ICM-20608G 的 ID 为 0XAF,ICM-20608D 的 ID 为 0XAE。

(3)添加设备树

(4)设备驱动

(5)测试程序

2.7 IIC、SPI、USB驱动架构类比

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词