当前位置: 首页 > news >正文

从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC)

从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC)

文章目录

  • 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC)
    • @[TOC]
    • 1. 什么是 SPI?硬件信号与连接![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/bb66c86932dd4437ab05ddf4c8a8eedb.png)
    • 2. SPI 四种模式(CPOL / CPHA)
    • 3. Linux SPI 子系统框架
    • 4. 设备树中如何描述 SPI 设备
    • 5. 最简单的 SPI 字符设备驱动框架
    • 6. 实战:TLC5615 DAC 驱动完整编写![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/55b946b759e74c2baeb038874d3efc9b.png)
      • 6.1 TLC5615 芯片及数据格式
      • 6.2 完整驱动代码(含注释)
      • 6.3 驱动代码关键点解析
    • 7. 编译、装载与测试
      • 7.1 编译驱动
      • 7.2 更新设备树
      • 7.3 装载驱动与测试
    • 8. 常见问题与内核错误分析
      • 8.1 错误:`cannot set clock freq: 2 (base freq: 60000000)`
      • 8.2 内核 `paging request` 崩溃
    • 9 完整流程框架(以写入数值 200 为例)
      • 9.1 用户空间程序 `dac_test` 工作
      • 9.2 C 库到系统调用
      • 9.3 VFS 层找到对应的字符设备驱动
      • 9.4 驱动 `spi_drv_write` 内部执行细节
      • 9.5内核 SPI 核心层(`spi_sync_transfer`)
      • 9.6SPI 控制器驱动(硬件操作)
      • 9.7 回到驱动层及用户空间
      • 9.8 硬件端 TLC5615 响应
    • 10. 总结与面试自测题
      • 面试自测题(附答案)

1. 什么是 SPI?硬件信号与连接

SPI(Serial Peripheral Interface)是一种全双工、同步串行总线,由摩托罗拉提出。基本信号有 4 根线:

信号名方向(相对于主控)作用
SCK主 → 从时钟信号,由主机产生
MOSI主 → 从主机发送,从机接收(Master Out Slave In)
MISO主 ← 从从机发送,主机接收(Master In Slave Out)
CS/SS主 → 从片选信号,低有效,选中某个从设备

你的板子上已经引出了这些信号

  • J2 上标注了SPI1 MOSISPI1 MISOSPI1 SCLKSPI1 CS0
  • J7 的 19、20、21、22 脚也是SPI1 SCLKSPI1 CS0SPI1 MOSISPI1 MISO

这意味着你的 IMX6ULL 开发板通过排针把 SPI1 控制器的引脚都引出来了,你可以直接拿杜邦线外接 SPI 设备(比如这次要驱动的 TLC5615 DAC 小板子)。

通信过程概括
主机拉低 CS 选中从机,然后产生时钟。在每个时钟边沿,主机从 MOSI 线移出一位数据,同时从 MISO 线移入一位数据。传输完一个或多个字节后,主机拉高 CS 结束会话。


2. SPI 四种模式(CPOL / CPHA)

SPI 没有官方标准,不同从设备对时钟极性和相位的要求不一样,于是有了 4 种模式,由两个参数决定:

  • CPOL(时钟极性)
    • CPOL=0:空闲时 SCK 为低电平
    • CPOL=1:空闲时 SCK 为高电平
  • CPHA(时钟相位)
    • CPHA=0:在第一个时钟边沿采样数据
    • CPHA=1:在第二个时钟边沿采样数据

组合起来:

模式CPOLCPHA空闲 SCK采样边沿
000上升沿
101下降沿
210下降沿
311上升沿

在 Linux 设备树或spi_board_info中,通过spi-cphaspi-cpol属性来指定。例如:

dts

spi-cpha; spi-cpol;

不加时默认模式 0。TLC5615 数据手册要求 CPOL=0, CPHA=1(即模式 1)吗?其实很多 DAC 只是要求在 SCK 上升沿移入数据,需要查手册。实验中如果不稳定,很可能就是模式没配对。我们的例子里暂未加这两个属性,内核会按模式 0 工作,但为了严谨,应该根据芯片手册填写。


3. Linux SPI 子系统框架

Linux 把 SPI 架构分成三层,像搭积木一样:

  1. SPI 控制器驱动spi_master或新版本叫spi_controller
    直接与 SoC 的硬件 SPI 外设打交道。像spi_imx就是 IMX6ULL 的 SPI 控制器驱动,已经在内核里写好了,我们不用管。
  2. SPI 设备struct spi_device
    描述一个挂载在 SPI 总线上的从设备。它保存着该设备的片选索引、最大频率、模式等。这些信息主要来自设备树。
  3. SPI 设备驱动struct spi_driver
    我们写的驱动,负责与具体的从设备交互。内核通过compatible属性把它和设备树中的节点绑定起来。

一次 SPI 数据传输的核心数据结构

  • struct spi_transfer:描述一次传输的细节(发送缓冲区、接收缓冲区、长度、速度等)。
  • struct spi_message:将多个spi_transfer链接成一个原子操作,在全部传输完成后才释放 CS。
  • spi_sync_transfer(spi, xfers, num):同步接口,提交传输并阻塞等待完成。这是我们驱动中最常用的函数。

辅助函数:

c

static inline int spi_read(struct spi_device *spi, void *buf, size_t len) { struct spi_transfer t = { .rx_buf = buf, .len = len, }; return spi_sync_transfer(spi, &t, 1); }

就是一个只读的同步封装,简单明了。


4. 设备树中如何描述 SPI 设备

看你的设备树片段(arch/arm/boot/dts/100ask_imx6ull-14x14.dts):

dts

&ecspi1 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_ecspi1>; fsl,spi-num-chipselects = <2>; cs-gpios = <&gpio4 26 GPIO_ACTIVE_LOW>, <&gpio4 24 GPIO_ACTIVE_LOW>; status = "okay"; dac: dac { compatible = "100ask,spidev"; reg = <0>; spi-max-frequency = <1000000>; }; };

逐行解释

  • &ecspi1:引用 SPI1 控制器节点。
  • pinctrl-0 = <&pinctrl_ecspi1>;:指定引脚复用为 SPI 功能(已在别处定义)。
  • cs-gpios:指定两个片选引脚,分别是 GPIO4_26 和 GPIO4_24,低有效。控制器会根据reg编号自动选择对应 GPIO。
  • status = "okay";:启用该控制器。
  • 子节点dac:代表一个挂在 ecspi1 上的 SPI 设备。reg = <0>表示使用第 0 个片选(即 GPIO4_26)。spi-max-frequency = <1000000>限制最大时钟为 1 MHz。
  • compatible = "100ask,spidev";这是最关键的匹配字符串。内核会用它与所有spi_driverof_match_table比较,相等时就会调用驱动的probe函数,并把该节点生成的spi_device传进去。

你终端里执行ls /sys/bus/spi/devices/spi0.0能看到drivermodaliasof_node等,说明设备spi0.0已正确创建并与驱动绑定。


5. 最简单的 SPI 字符设备驱动框架

我们不只是让内核能识别设备,还要让用户空间程序(比如dac_test)能打开、写入、读取这个 SPI 设备。套路是:probe中注册一个字符设备,生成/dev/myspi节点

结构体骨架:

c

static struct spi_driver my_spi_driver = { .driver = { .name = "100ask_spi_drv", .owner = THIS_MODULE, .of_match_table = myspi_dt_match, // 与设备树 compatible 匹配 }, .probe = spi_drv_probe, .remove = spi_drv_remove, };

probe函数完成三件事:

  1. 保存spi_device *指针(通常放到全局变量或spi_set_drvdata)。
  2. 申请字符设备号,绑定file_operations
  3. 创建设备类并在/dev下生成节点。

你没贴出来的file_operations里面,read / write就是最终与硬件通信的地方。


6. 实战:TLC5615 DAC 驱动完整编写

6.1 TLC5615 芯片及数据格式

TLC5615 是一个 10 位 DAC,它接受 12 位或 16 位输入序列。图片给出了格式:

  • 12 位模式:高 10 位是数据,最低 2 位是无用位(sub-LSB)。
  • 16 位模式:高 4 位无意义,接着 10 位数据,最后 2 位 sub-LSB 无用位。

我们要驱动它输出一个模拟电压,只需要给它发送合适的数字值即可。为了方便,我们可以固定使用16 位模式,即发送两个字节,高 4 位随便填(但一般会考虑到对齐),后面跟着左移后的 10 位数据。

在老师提供的驱动代码write函数里,数据转换如下:

c

err = copy_from_user(&val, buf, size); // 得到用户空间的 16 位值(实际只用到低 10 位) val <<= 2; // 数据左移 2 位,把 10 位数据挪到 D11..D2 位置,低 2 位为 sub-LSB val &= 0x0fff; // 屏蔽高 4 位,确保发送 12 位有效数据

为何val <<= 2
因为 10 位数据在芯片的 16 位帧里位于 “4 dummy bits + 10 data bits + 2 extra bits” 结构。如果我们把 10 位数据放在一个 16 位字的高 10 位,val << 2就是把它移到 D15…D6 吗? 不,实际上代码里:

  • valunsigned shortcopy_from_user(&val, buf, 2)得到的是原始的用户值。
  • 左移 2 位再与0x0fff相与,结果是一个 12 位的值,其高 10 位是数据,低 2 位为 0。
  • 再把它拆成两个字节发送:ker_buf[0] = val >> 8; ker_buf[1] = val;
    那么对于 16 位帧来说,我们发送了两个字节共 16 位:高字节是val的高 8 位,低字节是val低 8 位。结果就是前 4 位为 0(因为val & 0x0fff清除了高 4 位),接着 10 位数据,最后 2 位为 0。这完全符合 16 位输入序列格式。

6.2 完整驱动代码(含注释)

下面是整合了老师源码的完整驱动程序,我将write方法配上详尽注释,并实现一个简单的read(DAC 通常不需要读,这里返回错误)。你可以直接使用。

c

#include <linux/spi/spi.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/errno.h> #include <linux/kernel.h> #include <linux/major.h> #include <linux/init.h> #include <linux/device.h> #include <linux/slab.h> #include <linux/uaccess.h> static int major = 0; static struct class *my_spi_class; static struct spi_device *g_spi; static ssize_t spi_drv_write(struct file *file, const char __user *buf, size_t size, loff_t *offset) { int err; unsigned short val; // 用户写来的值, 2 字节 unsigned char ker_buf[2]; // 内核中将要发送的 2 字节 struct spi_transfer t; // 我们约定一次必须写入 2 字节 (一个 DAC 数值) if (size != 2) return -EINVAL; // 从用户空间读取 2 字节到 val err = copy_from_user(&val, buf, size); if (err) return -EFAULT; /* * TLC5615 数据格式(16 位模式): * 高 4 位: 无关位 * 接着 10 位: 有效数据 (D9~D0) * 最低 2 位: sub-LSB 位 (通常填 0) * * 我们先把用户给的 val (假设其低 10 位有效) 左移 2 位, * 使 10 位数据位于 12 位字段的高 10 位,然后清除高 4 位。 * 最终 val 是一个 12 位的值,再分为两个字节发送,结果: * 字节0: 0000xxxx (前4位为0) 字节1: xxxxxx00 (后2位为0) * 满足芯片要求。 */ val <<= 2; // 10-bit data → D11..D2 val &= 0x0fff; // 屏蔽高4位,确保前4位为0 ker_buf[0] = (val >> 8) & 0xff; // 高字节 ker_buf[1] = val & 0xff; // 低字节 memset(&t, 0, sizeof(t)); t.tx_buf = ker_buf; t.len = 2; err = spi_sync_transfer(g_spi, &t, 1); if (err) { pr_err("spi_sync_transfer failed: %d\n", err); return err; } return size; } static ssize_t spi_drv_read(struct file *file, char __user *buf, size_t size, loff_t *offset) { // TLC5615 是纯写入设备,不支持读取,直接返回错误 return -ENOTSUP; } static int spi_drv_open(struct inode *inode, struct file *file) { return 0; } static int spi_drv_release(struct inode *inode, struct file *file) { return 0; } static const struct file_operations spi_drv_fops = { .owner = THIS_MODULE, .open = spi_drv_open, .release = spi_drv_release, .write = spi_drv_write, .read = spi_drv_read, }; static int spi_drv_probe(struct spi_device *spi) { g_spi = spi; // 保存 spi_device,供读写使用 major = register_chrdev(0, "100ask_spi", &spi_drv_fops); if (major < 0) { pr_err("Failed to register chrdev\n"); return major; } my_spi_class = class_create(THIS_MODULE, "100ask_spi_class"); if (IS_ERR(my_spi_class)) { unregister_chrdev(major, "100ask_spi"); return PTR_ERR(my_spi_class); } device_create(my_spi_class, NULL, MKDEV(major, 0), NULL, "myspi"); pr_info("myspi device created, major=%d\n", major); return 0; } static int spi_drv_remove(struct spi_device *spi) { device_destroy(my_spi_class, MKDEV(major, 0)); class_destroy(my_spi_class); unregister_chrdev(major, "100ask_spi"); return 0; } static const struct of_device_id myspi_dt_match[] = { { .compatible = "100ask,spidev" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, myspi_dt_match); static struct spi_driver my_spi_driver = { .driver = { .name = "100ask_spi_drv", .owner = THIS_MODULE, .of_match_table = myspi_dt_match, }, .probe = spi_drv_probe, .remove = spi_drv_remove, }; module_spi_driver(my_spi_driver); MODULE_LICENSE("GPL"); MODULE_AUTHOR("YourName"); MODULE_DESCRIPTION("SPI DAC driver for TLC5615");

6.3 驱动代码关键点解析

  • 模块入口module_spi_driver(my_spi_driver);是宏,自动生成module_initmodule_exit,里面调用spi_register_driverspi_unregister_driver。比手写更简洁。
  • compatible 匹配:设备树里有compatible = "100ask,spidev";,驱动of_match_table包含同样的字符串,所以它们会绑定。
  • 字符设备注册:老用法register_chrdev一次占用 256 个次设备号,简单够用。主设备号动态分配(传入 0)。
  • 数据传输spi_sync_transfer第一个参数是g_spi,它指向该设备对应的spi_device。内核自动使用设备树中指定的spi-max-frequency、片选等。我们不需要手动控制 CS,核心层会帮我们开关cs-gpios

7. 编译、装载与测试

7.1 编译驱动

把上述代码保存为spi_drv.c,在同一目录编写Makefile

makefile

KERN_DIR = /path/to/your/kernel/source # 例如 ~/100ask_imx6ull-sdk/Linux-4.9.88 obj-m += spi_drv.o all: make -C $(KERN_DIR) M=$(PWD) modules clean: make -C $(KERN_DIR) M=$(PWD) clean

然后执行make,生成spi_drv.ko

7.2 更新设备树

根据操作截图:

  1. 编辑arch/arm/boot/dts/100ask_imx6ull-14x14.dts,在&ecspi1内加入dac节点(你已添加)。
  2. 在内核源码目录执行make dtbs
  3. 将新生成的100ask_imx6ull-14x14.dtb复制到/boot目录或开发板的启动分区。
  4. 重启开发板。

7.3 装载驱动与测试

在开发板终端:

bash

insmod spi_drv.ko

查看是否成功生成/dev/myspi

bash

ls -l /dev/myspi

你的截图里还能看到/sys/bus/spi/devices/spi0.0,以及dac_test应用程序。dac_test应该是接收两个参数:/dev/myspi和 一个数值,例如:

bash

./dac_test /dev/myspi 100

这会让 DAC 输出与数字 100 对应的模拟电压。你那几条执行记录:

bash

./dac_test /dev/myspi 1000


bash

./dac_test /dev/myspi 500


bash

./dac_test /dev/myspi 200

说明驱动工作正常,能多次写入不同值控制电压(后面 LED 亮度会变化)。


8. 常见问题与内核错误分析

8.1 错误:cannot set clock freq: 2 (base freq: 60000000)

你截图里出现过:

text

spi_imx 2008000.ecspi: cannot set clock freq: 2 (base freq: 60000000)

这是因为你传给驱动的最大频率可能太小(比如 2 Hz),或者某个spi_transferspeed_hz设得不成比例。但你的设备树里spi-max-frequency = <1000000>(1 MHz),不应出现这个错误。可能原因:

  • 之前测试时修改过频率但没有更新 dtb。
  • 或者驱动程序里额外设置了t.speed_hz = 2;之类。
    解决办法:检查设备树频率,确保不要极端值;spi-max-frequency取 100000 ~ 按芯片最大能力。

8.2 内核paging request崩溃

你图中:

text

Unable to handle kernel paging request at virtual address 8010e710

常见原因:访问了非法指针,比如g_spi还是 NULL 时就调用了spi_sync_transfer,或者copy_from_user的缓冲区不合法。确保驱动在probe之后才被读写,并且g_spi被正确赋值。


9 完整流程框架(以写入数值 200 为例)

9.1 用户空间程序dac_test工作

  • 解析命令行参数:argv[1] = "/dev/myspi"argv[2] = "200"
  • 调用open("/dev/myspi", O_WRONLY)打开设备文件。
  • 将字符串"200"转换为整数200atoistrtol)。
  • 准备 2 字节数据:因为 TLC5615 是 10 位 DAC,用户程序可能直接将200当成 16 位无符号数写入,也就是准备unsigned short val = 200;,然后调用write(fd, &val, 2)向设备写入 2 个字节。

注:你的截图中./dac_test /dev/myspi 100等用法与上述逻辑一致。

9.2 C 库到系统调用

  • write(fd, &val, 2)触发SYS_write系统调用,陷入内核(ARM 上通过swi/svc指令)。
  • 内核根据系统调用号进入sys_write(),然后调用vfs_write()

9.3 VFS 层找到对应的字符设备驱动

  • vfs_write()fd对应的struct file中取出file->f_op,即spi_drv_fops
  • 调用file->f_op->write(file, buf, count, &pos),也就是我们的spi_drv_write
  • buf指向用户空间栈上的val地址(需要copy_from_user),count为 2。

9.4 驱动spi_drv_write内部执行细节

c

unsigned short val; unsigned char ker_buf[2]; struct spi_transfer t;
  • 长度检查size != 2→ 返回-EINVAL(这里为 2,通过)。

  • 拷贝用户数据copy_from_user(&val, buf, 2)将用户空间的两个字节复制到内核变量val。此时val = 200(无论主机字节序,内核内部视为 16 位无符号数)。

  • 数据格式转换(符合 TLC5615 的 16 位帧要求):

    • val <<= 2;200 << 2 = 800(二进制:0000 0011 0010 0000,十六进制0x320
    • val &= 0x0fff;0x320 & 0xfff = 0x320(确保只保留低 12 位,防止溢出)
    • 此时 12 位值0x320的组成:高 4 位0x3是 dummy 位(无关位),中间 10 位0x320 >> 2 = 200是有效数据,低 2 位为 0(sub-LSB)
  • 拆分为两个字节

    • ker_buf[0] = (val >> 8) & 0xff;0x03
    • ker_buf[1] = val & 0xff;0x20
  • 构造 SPI 传输

    c

    memset(&t, 0, sizeof(t)); t.tx_buf = ker_buf; // 发送缓冲区 t.len = 2; // 发送 2 字节
  • 发起同步 SPI 传输

    c

    err = spi_sync_transfer(g_spi, &t, 1);
    • g_spi是在probe中保存的spi_device指针,它包含了从设备树继承的片选号、最大频率等信息。
    • 如果发送成功,返回 0;发生的字节数size(2)返回给用户空间。

9.5内核 SPI 核心层(spi_sync_transfer

  • spi_sync_transfer内部创建一个spi_message,将spi_transfer添加进去,然后调用spi_sync(spi, &message)
  • spi_sync为此次传输设置一个等待队列,然后将spi_message提交给 SPI 控制器驱动,并阻塞等待传输完成。
  • 实际的传输由struct spi_controllertransfer_one_message方法完成,这里对应spi_imx驱动(IMX6ULL 的 SPI 控制器驱动)。

9.6SPI 控制器驱动(硬件操作)

  • 片选控制:控制器驱动根据spi_device的片选信息(来自设备树cs-gpios = <&gpio4 26 ...>;)将GPIO4_26 拉低,选中 TLC5615。
  • 配置时钟:根据设备树中的spi-max-frequency = <1000000>配置 SCK 为 1 MHz,并根据模式 0(未添加spi-cpha/cpol)设置空闲低电平、上升沿采样等。
  • 启动传输
    • ker_buf的两个字节(0x03,0x20)通过 MOSI 引脚顺序移出。
    • 控制器自动产生 16 个时钟脉冲,每当时钟边沿到来时,发送一位数据。
    • 由于本次只写不收,MISO 线上的数据被忽略(控制器不会将接收到的数据存入任何缓冲区,或者即使接收也不处理)。
  • 等待完成:硬件发送完所有位后触发中断,中断服务程序通知 SPI 核心传输结束。
  • 释放片选:控制器将 GPIO4_26 拉高,结束本次 SPI 会话。
  • spi_sync_transfer被唤醒,返回成功状态。

9.7 回到驱动层及用户空间

  • spi_drv_write获得err == 0,返回size(2)。
  • vfs_write将返回值 2 一路返回给用户空间的write()调用。
  • 用户程序dac_test收到write返回 2,之后可能调用close(fd)关闭设备。

9.8 硬件端 TLC5615 响应

  • TLC5615 在 16 个时钟周期内收到了完整的一帧 16 位数据:0x030x20
  • 根据芯片数据手册,它解析出 10 位有效数据200,同时忽略高 4 位和低 2 位。
  • 内部 DAC 寄存器更新,模拟输出电压变为Vout = Vref × (200 / 1024)
  • 你实验板上的 LED 亮度随之变化,因为 LED 接在 DAC 输出端(你的截图备注“DAC效果展示LED灯”)。

整个链条总结为
用户敲命令 → 用户程序写设备节点 → 系统调用陷入内核 → VFS 调用驱动 write → 驱动转换数据并调用spi_sync_transfer→ SPI 核心阻塞等待 → IMX SPI 控制器拉低 CS、产生时钟、发送两字节 → 完成后拉高 CS、唤醒等待 → write 返回 → 用户程序退出 → DAC 芯片更新电压输出。

任何一环出问题,都可以对照这个框架进行精准排查。

10. 总结与面试自测题

这篇教程带你走完了从 SPI 物理总线到底层驱动的全过程:

  • 认识了四根 SPI 信号线和硬件连接。
  • 理解了 CPOL/CPHA 决定的四种模式。
  • 掌握了 Linux SPI 子系统的分层模型,关键结构体spi_devicespi_driverspi_transfer
  • 学会了在设备树中添加 SPI 从设备节点,并通过compatible与驱动绑定。
  • 完成了字符设备驱动的编写,使用spi_sync_transfer发送数据。
  • 分析了 TLC5615 的数据格式,并在write函数中实现了位操作转换。
  • 最后在开发板上实际装载并成功控制 DAC 输出。

面试自测题(附答案)

1. SPI 总线有几根线?分别是什么?
答:4 根,SCK(时钟)、MOSI(主发从收)、MISO(主收从发)、CS(片选,通常低有效)。

2. 什么是 CPOL 和 CPHA?它们在设备树中如何指定?
答:CPOL 定义空闲时钟电平(0 低 1 高),CPHA 定义数据采样边沿(0 第一边沿,1 第二边沿)。设备树中通过spi-cpolspi-cpha布尔属性设置。

3. 在 Linux SPI 子系统中,spi_transferspi_message的关系是什么?
答:spi_transfer描述单次传输(TX/RX 缓冲区、长度)。spi_message是多个spi_transfer的集合,保证它们被原子地执行(CS 在整条消息期间保持有效)。

4. 设备树中compatible属性有什么作用?
答:它告诉内核设备的型号,内核用它来匹配对应的驱动程序。驱动程序通过of_match_table声明自己支持的 compatible 列表,匹配成功后调用 probe 函数。

5. 驱动中的probe函数主要做哪些事情?
答:①获取并保存spi_device;②初始化硬件(如配置时钟、复位);③注册字符设备/创建 sysfs 接口;④创建设备节点,让用户空间可以操作。

6.copy_from_usercopy_to_user为什么是必需的?
答:因为用户空间和内核空间的内存是隔离的,不能直接解引用用户指针。这两个函数会处理地址合法性检查和缺页异常,安全地拷贝数据。

7. 如果板子上有两个同样的 SPI 设备,分别挂在 CS0 和 CS1,驱动应该怎么做才能同时支持?
答:不能在驱动中用单一全局变量g_spi指向设备。应当将spi_device填到fileprivate_data或者用spi_set_drvdatacontainer_ofspi_device获取私有结构。每次 open 时根据次设备号或 inode 定位到对应的spi_device,避免覆盖。

8. TLC5615 的 12 位帧与 16 位帧在使用上有何区别?代码中val <<= 2; val &= 0x0fff;换成val <<= 4再发两个字节会怎样?
答:本驱动直接按 16 位帧发送,代码中的移位和掩码实现了高 4 位为 0、接着 10 位数据、低 2 位为 0 的正确帧格式。如果改成val <<= 4,会导致数据偏移到更高位,超出 12 位范围,发送的时序不符合芯片要求,输出电压会出错。

http://www.jsqmd.com/news/707457/

相关文章:

  • 【项目实训】——管理员前端页面开发
  • Canvas Quest与3D建模工作流结合:生成贴图与概念设计
  • 世界及中国地震相关数据(2012-2024年)
  • Python单变量函数优化算法与应用实践
  • 虚拟级联技术:运营商网络的带宽优化方案
  • 终极抖音下载指南:免费开源工具让你的视频获取效率飙升300%
  • 关于Navicat Premium 17破解方法
  • cv_unet_image-matting WebUI二次开发指南:从改颜色到加功能的完整教程
  • 机器学习核心原理与实践指南:从数据到智能应用
  • 智能体“自我纠错”循环的设计模式:何时重试、何时求助、何时报错?
  • Clink 在 VS 2022 Developer Command Prompt 中的配置与路径精简调校
  • 【CLAUDE】CLAUDE.md 完全实战指南:用好Claude Code的核心记忆体系
  • Rust的#[non_exhaustive]:防止模式匹配穷尽的可扩展枚举
  • 《B4447 [GESP202512 二级] 环保能量球》
  • Flux2-Klein-9B-True-V2效果集:Proteus电路仿真与AI概念艺术设计的碰撞
  • 原创文档:智慧地下管廊知识图谱设计与实现
  • 2026年最新实测:5个降AI工具助我把知网AIGC率从79%降至6.2%(附免费反向优化法) - 降AI实验室
  • 别再用namespace硬隔离了!MCP 2026正式启用硬件辅助隔离(Intel AMX+AMD SVM-V),性能损耗<0.7%?
  • 2026插座选哪个牌子性价比高?实用推荐指南 - 品牌排行榜
  • 登山包/电脑包/军用背包用TPU牛津布厂家推荐:轻便+防水+耐刮
  • 立知多模态重排序模型体验:图片搜索排序新利器
  • Day56基本包装类型
  • SCH16T-K01和K10提供高精度6DoF惯性传感器
  • 2026年毕业论文提交前终审降AI攻略:最后一遍处理完整方案
  • 关于java 调用阿里千问大模型,流式返回,并返回给前端
  • MCP 2026推理加速实战:5步完成KV Cache压缩、量化感知重编译与动态批处理调优,延迟直降63%
  • nli-MiniLM2-L6-H768快速部署:Kubernetes Helm Chart一键部署到生产集群
  • Windows 11锁屏壁纸别浪费!教你一键导出Spotlight精选图库到本地
  • 2026API服务商实测:3款稳定AI大模型接口方案,商用成本参考解析
  • 市场比较好的国标pvdf管厂家(2026年) - 品牌排行榜