准备知识
建议先读下博文 https://blog.csdn.net/wenhao_ir/article/details/146467895
关于设备驱动spidev.c的来历和介绍
关于设备驱动spidev.c的来历和介绍,在前面的博文中我已经介绍了,链接 https://blog.csdn.net/wenhao_ir/article/details/146467895
也可以参考视频(百度网盘搜索“1-5_05_spidev的使用(SPI用户态API)”),然后前面的1分30秒就对其来历进行了介绍。
万能SPI设备驱动spidev.c的代码分析
spidev.c的源码路径
文件spidev.c的路径:\Linux-4.9.88\drivers\spi\spidev.c
spidev.c使用的SPI控制器的接口函数来自于哪里?是哪些?
SPI控制器的接口函数来自于SPI子系统,关于SPI子系统的详解及有哪些函数,详情见博文 https://blog.csdn.net/wenhao_ir/article/details/146551375
spidev.c的spi_drver的匹配列表
打开文件spidev.c,文件spidev.c的路径:\Linux-4.9.88\drivers\spi\spidev.c
可见,spi_drver中的driver成员的name属性值为spidev
,所以我把注册的设备名写为spidev
,那么根据匹配规则,是肯定可以匹配到的。SPI总线的匹配规则和Platform总线是一样的,详情见
spidev.c的主设备号和次设备号的来历及相关代码分析
打开文件spidev.c,文件spidev.c的路径:\Linux-4.9.88\drivers\spi\spidev.c,然后定位到probe函数内部,具体位置是第782行代码:
关于宏MKDEV的讲解见我的另一篇博文 https://blog.csdn.net/wenhao_ir/article/details/144888989
这里,宏MKDEV将主设备号SPIDEV_MAJOR和次设备号minor合成为一个完整的设备号。
SPIDEV_MAJOR是一个宏定义,在spidev.c的第53行,如下:
这说明,spidev.c将自己创建的驱动程序编号为153号。
次设备号来自于 下面这句代码:
minor = find_first_zero_bit(minors, N_SPI_MINORS);
函数**find_first_zero_bit()
**的作用:查找 minors
位图中 第一个未被占用(值为0) 的位置,并返回其索引,即一个可用的次设备号。
这个函数在 Linux 内核中定义如下:
unsigned long find_first_zero_bit(const unsigned long *addr, unsigned long size);
addr
:指向位图(bitmap)的指针,存储已分配的次设备号。size
:位图的大小(最大支持的 minor 数)。
它会 从低位到高位 遍历 addr
指向的位图,找到第一个为 0
的 bit,返回其索引。
示例:
假设 minors
位图为:
Index: 0 1 2 3 4 5
Value: 1 1 0 1 0 0
在这个示例这里, find_first_zero_bit(minors, 6)
返回 2
,因为 index=2
的 bit 为 0
。
第1个参数位图指针minors
来自于spidev.c的第56行,如下:
宏DECLARE_BITMAP
的定义如下:
可见,实际上是定义了一个名字为minors
的unsigned long
数组,数组的元素个数为N_SPI_MINORS
,N_SPI_MINORS
的宏定义在spidev.c的第54行,如下:
所以可见spidev.c将自己创建的驱动程序的支持设备数限定为了32个。
一旦一个次设备号被占用,那么就会被标记该次设备号为被占用状态,如下图所示:
spidev.c创建的设备文件的文件名分析
打开文件spidev.c,文件spidev.c的路径:\Linux-4.9.88\drivers\spi\spidev.c,然后定位到probe函数内部,具体位置是第783行代码:
dev = device_create(spidev_class, &spi->dev, spidev->devt, spidev, "spidev%d.%d", spi->master->bus_num, spi->chip_select);
要读懂这行代码,需要了解device_create
函数,详情请参考我的另一篇博文 https://blog.csdn.net/wenhao_ir/article/details/144888989
从中我们可以看出,spidev.c创建的设备文件的文件名的格式为spidev%d.%d
,第一个%d
是spi->master->bus_num
,代表SPI控制器的设备编号,第2个%d
来自于对片选信号的编号。
spidev.c如何管理多个SPI设备?
一个次设备号代表一个SPI设备,那么spidev.c如何管理这些设备的呢?答案是通过链表,下面这句代码就可以体现其通过链表管理SPI设备:
关于DAC模块的原理与结构描述
DAC芯片的型号是德州仪器的TLC5615,相关的DAC模块的实物图、原理图和Datasheet的百度网盘下载地址如下:
https://pan.baidu.com/s/1ueh3IZLrdBWcA78RQR-1vQ?pwd=yqzd
如果要了解详细的原理分析,请百度网盘搜索视频“1-6_06_使用spidev操作SPI_DAC模块”,然后从头开始看。
这里摘录重点如下:
DAC模块的结构如下:
从图中我们可以总结出数据传输时的格式要求如下:
①SPI发送数据时,是先发送数据的最高位,再发送数据的最低位。
②发送的16位数据的中间10位才被转换为模拟量。
③发送的16位数据的最低2位必须是0;
④发送的16位数据的高4位是填充位(没用实际作用),是用来进行数据格式对齐的,通常我们以0来进行填充,当然也可能需要以其它值填充,具体需要以什么值填充,请查阅相关的DAC芯片的手册。
关于接收到的数据重点要注意下面两点:
①SPI发送数据时,是先发送数据的最高位,再发送数据的最低位。
②则本次数据传送完后通过DAC的DOUT输出的16位数据是上次传送的16位数据,而不是本次传送的16位数据,原因:请看上面的DAC的结构图,DAC芯片中的16-Bit Shift Register
是进一位,然后出一位到DOUT中。
用户空间测试程序
Demo示例代码的分析和重要说明
spidev.c提供的用户空间的Demo示例代码的路径如下:
“\Linux-4.9.88\tools\spi\spidev_fdx.c”
其完整代码如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>
static int verbose;
static void do_read(int fd, int len)
{
unsigned char buf[32], *bp;
int status;
/* read at least 2 bytes, no more than 32 */
if (len < 2)
len = 2;
else if (len > sizeof(buf))
len = sizeof(buf);
memset(buf, 0, sizeof buf);
status = read(fd, buf, len);
if (status < 0) {
perror("read");
return;
}
if (status != len) {
fprintf(stderr, "short read\n");
return;
}
printf("read(%2d, %2d): %02x %02x,", len, status,
buf[0], buf[1]);
status -= 2;
bp = buf + 2;
while (status-- > 0)
printf(" %02x", *bp++);
printf("\n");
}
static void do_msg(int fd, int len)
{
struct spi_ioc_transfer xfer[2];
unsigned char buf[32], *bp;
int status;
memset(xfer, 0, sizeof xfer);
memset(buf, 0, sizeof buf);
if (len > sizeof buf)
len = sizeof buf;
buf[0] = 0xaa;
xfer[0].tx_buf = (unsigned long)buf;
xfer[0].len = 1;
xfer[1].rx_buf = (unsigned long) buf;
xfer[1].len = len;
status = ioctl(fd, SPI_IOC_MESSAGE(2), xfer);
if (status < 0) {
perror("SPI_IOC_MESSAGE");
return;
}
printf("response(%2d, %2d): ", len, status);
for (bp = buf; len; len--)
printf(" %02x", *bp++);
printf("\n");
}
static void dumpstat(const char *name, int fd)
{
__u8 lsb, bits;
__u32 mode, speed;
if (ioctl(fd, SPI_IOC_RD_MODE32, &mode) < 0) {
perror("SPI rd_mode");
return;
}
if (ioctl(fd, SPI_IOC_RD_LSB_FIRST, &lsb) < 0) {
perror("SPI rd_lsb_fist");
return;
}
if (ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bits) < 0) {
perror("SPI bits_per_word");
return;
}
if (ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed) < 0) {
perror("SPI max_speed_hz");
return;
}
printf("%s: spi mode 0x%x, %d bits %sper word, %d Hz max\n",
name, mode, bits, lsb ? "(lsb first) " : "", speed);
}
int main(int argc, char **argv)
{
int c;
int readcount = 0;
int msglen = 0;
int fd;
const char *name;
while ((c = getopt(argc, argv, "hm:r:v")) != EOF) {
switch (c) {
case 'm':
msglen = atoi(optarg);
if (msglen < 0)
goto usage;
continue;
case 'r':
readcount = atoi(optarg);
if (readcount < 0)
goto usage;
continue;
case 'v':
verbose++;
continue;
case 'h':
case '?':
usage:
fprintf(stderr,
"usage: %s [-h] [-m N] [-r N] /dev/spidevB.D\n",
argv[0]);
return 1;
}
}
if ((optind + 1) != argc)
goto usage;
name = argv[optind];
fd = open(name, O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
dumpstat(name, fd);
if (msglen)
do_msg(fd, msglen);
if (readcount)
do_read(fd, readcount);
close(fd);
return 0;
}
使用方法如下:
如下面的命令行语句:
spidev_fdx -r 100 /dev/spidevB.D
具体的对spidev_fdx.c代码分析见:
百度网盘搜索“1-5_05_spidev的使用(SPI用户态API)”,然后从14分40秒开始看。
文档如下:
https://pan.baidu.com/s/1K7jHN8L9Y9RXpXqgWeirqw?pwd=jqkr
从分析中可以知道,spidev.c提供的spidev_read和spidev_write在同一时刻都只能读或写,如果要同时读和写,是不能用这两个函数的,即不能用用户空间的read和write函数,而要通过用户空间的ioctl函数来调用驱动程序中的unlocked_ioctl
成员函数,unlocked_ioctl
这个成员函数可以实现同时读写操作,即双工传输。
完整源码(spidev_dac_test.c)
/* 参考: tools\spi\spidev_fdx.c */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>
/* dac_test /dev/spidevB.D <val> */
int main(int argc, char **argv)
{
int fd;
unsigned int val;
struct spi_ioc_transfer xfer[1];
int status;
unsigned char tx_buf[2];
unsigned char rx_buf[2];
if (argc != 3)
{
printf("Usage: %s /dev/spidevB.D <val>\n", argv[0]);
return 0;
}
fd = open(argv[1], O_RDWR);
if (fd < 0) {
printf("can not open %s\n", argv[1]);
return 1;
}
val = strtoul(argv[2], NULL, 0);
val <<= 2; /* 设置 bit0,bit1 = 0b00 */
val &= 0xFFC; /* DAC芯片的有效位数为10位,所以只保留10bit */
tx_buf[1] = val & 0xff;
tx_buf[0] = (val>>8) & 0xff;
memset(xfer, 0, sizeof xfer);
xfer[0].tx_buf = (unsigned long)tx_buf;
xfer[0].rx_buf = (unsigned long)rx_buf;
xfer[0].len = 2; //传输长度为2字节
status = ioctl(fd, SPI_IOC_MESSAGE(1), xfer);
if (status < 0) {
printf("SPI_IOC_MESSAGE\n");
return -1;
}
/* 打印接收到的数据 */
val = (rx_buf[0] << 8) | (rx_buf[1]);
val >>= 2;
printf("Last value is: %d\n", val);
return 0;
}
源码分析说明
C标准库函数strtoul
的作用
val = strtoul(argv[2], NULL, 0);
用于将字符串转换为无符号长整型(unsigned long)。
原型如下:
#include <stdlib.h>
unsigned long strtoul(const char *nptr, char **endptr, int base);
参数说明
nptr
:指向要转换的字符串。endptr
:用于存储转换过程中遇到的第一个无效字符的地址(可为NULL
,如果不需要获取无效字符的位置),后面我会对这个参数举例说明。base
:指定转换时使用的进制(范围:2~36,或 0)。0
:自动检测进制(默认规则如下)。2~36
:显式指定进制,例如 2(二进制)、8(八进制)、10(十进制)、16(十六进制)等。
进制自动检测(base = 0)时的规则
- 以
0x
或0X
开头的字符串按 16 进制 解析。 - 以
0
开头的字符串按 8 进制 解析。 - 其他情况默认按 10 进制 解析。
返回值
- 成功时,返回转换后的
unsigned long
值。 - 若
nptr
为空字符串或无可转换的数字,则返回0
。 - 若转换过程中超出
unsigned long
能表示的范围,则返回ULONG_MAX
,并设置errno = ERANGE
。
使用 endptr
判断转换结果
endptr
指向第一个无效字符,可以用来检查整个字符串是否被成功转换。例如:
char *end;
unsigned long val = strtoul("123abc", &end, 10);
if (*end != '\0') {
printf("Conversion stopped at: %s\n", end); // 输出 "abc"
}
由于"123abc"a是第一个无效字符,所以上面的运行结果是输出 “abc”。
C标准库函数memset
的作用
memset(xfer, 0, sizeof xfer);
关于memset的详细介绍见 https://blog.csdn.net/wenhao_ir/article/details/125337117
代码ioctl(fd, SPI_IOC_MESSAGE(1), xfer);
status = ioctl(fd, SPI_IOC_MESSAGE(1), xfer);
SPI_IOC_MESSAGE(1)
代表执行 1 次 SPI 传输,即处理一个struct spi_ioc_transfer
结构体。struct spi_ioc_transfer
结构体的定义如下:
struct spi_ioc_transfer {
__u64 tx_buf; // 发送数据缓冲区的地址
__u64 rx_buf; // 接收数据缓冲区的地址
__u32 len; // 传输的字节数
__u32 speed_hz; // 传输速率(Hz)
__u16 delay_usecs; // 传输间的延迟(微秒)
__u8 bits_per_word; // 每个字传输的位数
__u8 cs_change; // 是否更改片选
__u32 pad; // 保留位
};
编译用户空间测试程序
将文件spidev_dac_test.c
复制到Ubuntu中:
然后执行下面的语句进行交叉编译:
arm-buildroot-linux-gnueabihf-gcc -o spidev_dac_test spidev_dac_test.c
这样就得到了用户空间的ELF可执行程序spidev_dac_test
了,先放在那里备用。
设备树节点的书写、分析和设备树文件的编译
找到相应的设备树节点
根据博文对SPI总线的介绍,https://blog.csdn.net/wenhao_ir/article/details/146467895,我们需要通过设备树节点去生成DAC的spi_device
结构体。
先把dts设备树文件复制到VScode工程目录中,然后把dts和dtb设备树文件都进行备份:
VScode中打开文件100ask_imx6ull-14x14.dts
,开始修改。
我们同样先查看文件imx6ul.dtsi
,看下aliases关系:
aliases等号左边的名字中末尾的数字用于Linux的相关子系统提取相关设备的编号,这个我们之前在确定串口设备在子系统中的设备文件名时就已经领教过了。详情见博文 https://blog.csdn.net/wenhao_ir/article/details/146268977 【搜索“我们UART的串口1在TTY子系统中对应于serial0”】
由上面的aliases关系,我们在100ask_imx6ull-14x14.dts
中搜索&ecspi1
:
&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";
/*
spidev0: spi@0 {
compatible = "rohm,dh2228fv";
reg = <0>;
spi-max-frequency = <5000000>;
};
spidev1: spi@1 {
compatible = "rohm,dh2228fv";
reg = <1>;
spi-max-frequency = <5000000>;
};
*/
};
SPI的设备树描述语句分析
关于上面这段关于SSPI控制器及SPI设备的设备树语句,Linux内核有官方文档进行描述,官方文档路径如下:
\Linux-4.9.88\Documentation\devicetree\bindings\spi\spi-bus.txt
里面介绍了设备树语句中各字段的含义。这个文档我单独提取了出来,百度网盘下载地址:
https://pan.baidu.com/s/18XcbNpbsqpFr0oVztBZaXQ?pwd=x74k
我这里说下里面重点字段的含义:
pinctrl:这个之前在博文 https://blog.csdn.net/wenhao_ir/article/details/145060032 中已经说过了,其实就是与SPI相关的引脚的复用控制。
fsl,spi-num-chipselects
:这个的值是一个数值,代表有几个片选信号,即可以选择几个SPI设备。
cs-gpios
:这个代表具体的片选信号由哪些引脚产生,fsl,spi-num-chipselects
的值是多少,这里就该有有几个引脚,比如在上面的设备树代码中,由于fsl,spi-num-chipselects
的值为2,所以这里对应于两个引脚用于产生片选信号,具体的两个引脚为<&gpio4 26 GPIO_ACTIVE_LOW>和<&gpio4 24 GPIO_ACTIVE_LOW>,即GPIO4的第26和第24个引脚,低电平代表选中,GPIO4_26和GPIO4_24分别代表下面子节点中reg值为0和1的SPI设备。具体的对应关系如下图所示:
SPI控制器的子设备节点的spi-max-frequency
代表该SPI设备能支持的最大SPI时钟信号。
增加SPI设备子节点
明白以上字段值的含义后,我们就可以像下面这样在引用结构&ecspi1
下增加以下这个标签名为spidac
、名字为spi@0
的代表DAC模块的子节点。
spidac: spidac@0{
compatible = "ti,tlc5615";
reg = <0>;
spi-max-frequency = <20000000>;
};
注意:增加的子节点的名字不能为spi@0
,否则会有冲突
我实际测试,发现如果取名为spi@0
,那么在系统启动时,会报下面的错:
Booting from mmc ...
0 bytes read in 42 ms (0 Bytes/s)
Kernel image @ 0x80800000 [ 0x000000 - 0x86fbf0 ]
## Flattened Device Tree blob at 83000000
Booting using the fdt blob at 0x83000000
reserving fdt memory region: addr=83000000 size=a000
Using Device Tree in place at 83000000, end 8300cfff
ft_system_setup for mx6
Unable to update property /soc/aips-bus@02100000/ethernet@02188000:mac-address, err=FDT_ERR_BADSTRUCTURE
Unable to update property /soc/aips-bus@02100000/ethernet@02188000:local-mac-address, err=FDT_ERR_BADSTRUCTURE
Unable to update property /soc/aips-bus@02000000/ethernet@020b4000:mac-address, err=FDT_ERR_BADSTRUCTURE
Unable to update property /soc/aips-bus@02000000/ethernet@020b4000:local-mac-address, err=FDT_ERR_BADSTRUCTURE
Starting kernel ...
然后内核启动不起来。当我把这个子节点的名字改为spi@0
后就没有问题了。
compatible
字段值的解释
为什么compatible
字段值填为ti,tlc5615"
?因为DAC芯片的生产商是TI、型号为TLC5615,所以填为ti,tlc5615
。
当然,根据之前我写的博文https://blog.csdn.net/wenhao_ir/article/details/146467895【搜索“SPI总线的match函数分析”】中对SPI总线的匹配机制的分析,我等会儿还要去修改spidev.c中的spidev_dt_ids
,使它增加匹配项ti,tlc5615
。
reg字段值的解释(片选信号引脚硬件实物图和原理图的分析)
我们把reg字段值填为0:
也就是说,我们这个DAC_SPI设备所使用的片选信号是GPIO4_26引脚,我们来验证下是不是。
DAC扩展模块的实物图和原理图见博文 https://blog.csdn.net/wenhao_ir/article/details/144409013
我们将把DAC扩展模块插到扩展板的SPI_A,即J2上,如下图所示:
从DAC模块的原理图:
我们可以知道,DAC模块从上往下数第8个脚是片选信号,所以扩展板上对应的片选引脚如下图所示:
所以我们要去查看一下扩展板J2的CS0引脚连接到处理器的哪个引脚上的。
J2原理图如下:
所以我们需要到J7中去查看网络号SPI1_CS0
对应的引脚:
可见在J7中,网络号SPI1_CS0
对应的引脚为20号引脚。
然后我们再去底层
需要去开发板的Base_board(底板)原理图中搜索“J5”,查看下与上面J7中20号引脚对应的引脚:
J7和J5的原理图一对比,发现其实J5的20号引脚就是J7的20号引脚,所以我们去Core_board(核心板)原理图中搜索网络标号CSI_DATA5
,
可见对应的引脚名应该就是CSI_DATA5
。
我们去开发板制造商提供的网络标号对应的引脚复用情况表中去查看一下复用情况(当然也可以用NXP官方提供的工具i.MX Pins Tool v6
来查看):
可见,网络编号CSI_DATA5可复用为gpio4.IO[26]
,所以我们这里把reg字段的值设置为0是对的,即我们这里的DAC模块使用的片选信号就是GPIO4_26这个信号。
spi-max-frequency字段值的解释
DAC芯片的型号是德州仪器的TLC5615,相关的DAC模块的实物图、原理图和Datasheet的百度网盘下载地址如下:
https://pan.baidu.com/s/1ueh3IZLrdBWcA78RQR-1vQ?pwd=yqzd
在DAC芯片TLC5615原理图的第4页,有下面这张表:
最后两项给出了其SCLK时钟信号的低电平和高电平高少持续时间,那么一个完整的时钟周期的最短持续时间应该为25ns+25ns = 50 ns,这个最短持续时间对应着它的最大频率,即:
1/50*10^9 Hz = 20000000 Hz = 20MHz
所以我们把 spi-max-frequency字段的值设置为20000000
。
确认下扩展板的J2是否对应于设备树中的SPI控制器
工程开发中无小事,任何一个细微的地方写错都有可能导致结果的失败,所以本着严谨的态度,我们应该再去确认下这里标签名为ecspi1
的SPI控制器是否对应于扩展板的J2。
我们首先去查看ecspi1
的pinctrl
属性,在100ask_imx6ull-14x14.dts
中搜索pinctrl_ecspi1:
pinctrl_ecspi1: ecspi1 {
fsl,pins = <
MX6UL_PAD_CSI_DATA04__ECSPI1_SCLK 0x000010B0
MX6UL_PAD_CSI_DATA06__ECSPI1_MOSI 0x000010B0
MX6UL_PAD_CSI_DATA07__ECSPI1_MISO 0x000010B0
MX6UL_PAD_CSI_DATA05__GPIO4_IO26 0x000010B0
MX6UL_PAD_CSI_DATA03__GPIO4_IO24 0x000010B0
>;
};
与片选信号相关的CSI_DATA05__GPIO4_IO26
、CSI_DATA03__GPIO4_IO24
这里不用考虑,我们要看的是CSI_DATA07__ECSPI1_MISO
、CSI_DATA06__ECSPI1_MOSI
、DATA04__ECSPI1_SCLK
,其实这三个我们任看一个就行了,我们就看CSI_DATA07__ECSPI1_MISO
吧,不妨在开发板制造商提供的网络标号对应的引脚复用情况表中搜索“CSI_DATA7”:
所以我们就去看下Base_board(底板)的原理图中的J5中有没有引脚使用了网络标号CSI_DATA7
,结果是有的:
22号引脚,对应于扩展展的J7的22号引脚:
扩展板的22号引脚的网络标号为:SPI1_MISO
,而扩展板的J2的第6个引脚的网络标号正好是SPI1_MISO
。
我们的DAC模块就是接在扩展板的J2上,所以没问题,J2的相关引脚确实是连接SPI1,而我们的设备树中的节点ecspi1
也是使用的SPI1。
编译设备树生成dtb文件
先把原来的设备树文件复制到VScode中,然后把原来的设备树文件进行备份。
接着在VScode修改设备树文件,按上面的分析进行修改:
spidac: spidac@0{
compatible = "ti,tlc5615";
reg = <0>;
spi-max-frequency = <20000000>;
};
修改完成后再复制回Ubuntu中的Linux内核中:
然后进入内核目录:
cd /home/book/100ask_imx6ull-sdk/Linux-4.9.88
然后执行下面这条命令编译出设备树的dtb文件:
make dtbs
把生成的dtb文件复制到网络文件系统目录中备用。
更新开发板上的设备树文件
打开串口终端→打开开发板→挂载网络文件系统:
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
然后执行下面的命令复制设备树dtb文件并覆盖之前的dtb文件:
cp /mnt/100ask_imx6ull-14x14.dtb /boot/
然后重启开发板…查看下内核中有没有相关的设备树节点了
看下内核中有没有相关的设备树节点了
如果内核中含有某个节点,那么在目录/proc/device-tree
下的某个N级子目录中会有以节点名命令名的目录生成,所以我们用下面这条命令进行搜索,看下节点spidac@0
是否存在于内核中了:
find /proc/device-tree/ -name "*spidac@0*"
运行结果如下:
[root@imx6ull:~]# find /proc/device-tree/ -name "*spidac@0*"
/proc/device-tree/soc/aips-bus@02000000/spba-bus@02000000/ecspi@02008000/spidac@0
这说明找到了名字为spidac@0
的目录,我们进入这个名为spidac@0
的目录:
cd /proc/device-tree/soc/aips-bus@02000000/spba-bus@02000000/ecspi@02008000/spidac@0
然后看一下它下面的文件和目录信息:
可见,有四个文件,分别记录了这个节点的4个属性。
我们看下文件compatible
的内容:
cat compatible
可见正是我们在节点的compatible
字段写的值“ti,tlc5615”
注意:因为reg和 spi-max-frequency是数值,所以不能用cat命令,cat命令通常会将内容作为普通文本输出,而我们需要查看的是原始数据,此时可以用下面的命令输出值:
xxd reg
xxd spi-max-frequency
注意:上面这个值0x004c4b40
转化为十进制5 000000HZ,为什么不是20 000000呢?因为我后来又修改了设备树…
修改spidev.c中的spidev_dt_ids
结构体
根据博文 https://blog.csdn.net/wenhao_ir/article/details/146467895 介绍的SPI总线匹配机制,由于我们在设备树中填写的DAC子节点的compatible
字段值填为ti,tlc5615"
,所以我们需要修改spidev.c中的spidev_dt_ids
结构体,增加项:ti,tlc5615"
。
文件spidev.c
的路径为:Linux-4.9.88/drivers/spi/spidev.c
打开它后搜索spidev_dt_ids
然后增加项:
{ .compatible = "ti,tlc5615" },
修改完成后保存。
配置并编译内核模块(将spidev.c以模块的形式编译出)
配置内核
打开终端,进入内核目录:
cd /home/book/100ask_imx6ull-sdk/Linux-4.9.88
然后进行配置界面:
make menuconfig
按下/
键进行搜索界面,搜索下“CONFIG_SPI_SPIDEV”,看下它在哪个位置:
所以位置在:Device Drivers-> SPI support,并且其在配置界面的中名字为“User mode SPI device driver support”
然后根据这个位置找到CONFIG_SPI_SPIDEV
的配置位置:
把上面截图中的配置项按下M键设置为M
,即把它编译成模块:
然后一层一层的Exit
,最后保存:
编译内核模块
同样要先进入Linux的源码目录:
cd /home/book/100ask_imx6ull-sdk/Linux-4.9.88
用下面这条命令编译内核模块:
make modules
用下面的命令把生成的spidev.ko
复制到网络文件目录中备用:
cp drivers/spi/spidev.ko ~/nfs_rootfs/
不带DAC模块的上板测试
短接MOSI和MISO两个引脚
带DAC模块的测试很简单,就是把MOSI和MISO两个引脚用杜邦线短接就行了,这样MOSI的数据就直接传给MISO了,如下图所示:
加载驱动模块spidev.ko
注意:前面我已经更新了设备树了,接下来我们加载spidev.ko
,并运行测试程序试一下。
打开串口终端→打开开发板→挂载网络文件系统
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
先加载驱动模块:
insmod /mnt/spidev.ko
检查是否有相应的驱动和设备树文件生成
然后我们看下是否有相应的驱动和设备树文件生成:
cat /proc/devices
果然如我们之前分析的,对应的major编译为153.
ls /dev/spidev*
可见有相关的设备树文件了,并且也符号之前我们对设备文件名的分析,所以如果后面应用程序要使用设备文件,就用路径“/dev/spidev0.0”
运行测试程序
之前已经编译了用户空间的测试程序,只是还没有复制到网络文件系统目录中,现在我们把它复制到网络文件系统目录中:
然后运行下面的命令执行程序
/mnt/spidev_dac_test /dev/spidev0.0 200
再运行下面这条命令:
/mnt/spidev_dac_test /dev/spidev0.0 300
也没问题。
运行结果分析
上面的结果说明我们利用spidev.c间接操作SPI控制器,至少在操作SPI控制器的MOSI和MISO两个引脚是没有问题的。
但是片选信号的操作就有问题了,详情请在本博文中搜索“韦老师远程给我操作了两个小时也没有找到问题”。
带DAC模块的上板测试
将DAC模块插到扩展板的J2位置
根据上面的分析(搜索“我们将把DAC扩展模块插到扩展板的SPI_A”),我们应该将DAC模块插到扩展板的J2的位置上:
加载驱动模块spidev.ko
注意:前面我已经更新了设备树了,接下来我们加载spidev.ko
,并运行测试程序试一下。
打开串口终端→打开开发板→挂载网络文件系统
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
先加载驱动模块:
insmod /mnt/spidev.ko
检查是否有相应的驱动和设备树文件生成
然后我们看下是否有相应的驱动和设备树文件生成:
cat /proc/devices
果然如我们之前分析的,对应的major编译为153.
ls /dev/spidev*
可见有相关的设备树文件了,并且也符号之前我们对设备文件名的分析,所以如果后面应用程序要使用设备文件,就用路径“/dev/spidev0.0”
运行测试程序
之前已经编译了用户空间的测试程序,只是还没有复制到网络文件系统目录中,现在我们把它复制到网络文件系统目录中:
然后运行下面的命令执行程序
/mnt/spidev_dac_test /dev/spidev0.0 200
/mnt/spidev_dac_test /dev/spidev0.0 300
后面的300代表我给DAC模块输入的数字值。
这个结果表明DAC模块没有正常工作,联系了韦老师协助,用命令cat /sys/kernel/debug/gpio
查看了被使用的GPIO口的状态,发现GPIO4_26(编号是122)一直是输入状态。
韦老师远程给我操作了两个小时也没有找到问题,最后归结起来就是SPI控制器的驱动程序是没有问题的,用打印语句可以看出SPI控制器文件
drivers/spi/spi.c
中的函数spi_imx_setup
是把GPIO4_26设置为输出引脚了,而且到GPIO子系统中,修改include/asm-generic/gpio.h,打印出对GPIO子系统函数的调用,也可以看出确实是调用了GPIO子系统的GPIO口方向设置函数。
但是就是不知道在哪个地方GPIO4_26被设置为了输入引脚。韦老师用命令行单独操作GPIO4_26也能将其操作为输出引脚,说明引脚本身是没有问题的。
所以这个问题我也不去搞了,这不是我目前的重点,以后如果需要使用这一块,那么我在SPI设备的驱动的open函数中把GPIO4_26设置为输出引脚就行了,比如可以修改spidev.c
的open函数使其调用GPIO子系统去再设置GPIO4_26的方向嘛。
这里要注意:GPIO4_26在GPIO子系统中的编号为122。
附相关文件
VScode完整工程目录
百度网盘下载链接:
https://pan.baidu.com/s/1Co6qtuhHi5TotvsLKA5wtA?pwd=i6gb
修改后的spidev.c
https://pan.baidu.com/s/1rteWsVe858PAV2hvriO-7w?pwd=kqdy
在Ubuntu中编译出的用户空间测试程序
百度网盘下载链接:
https://pan.baidu.com/s/1fd6Sc_8dmPhpUm9a6Mbn3w?pwd=fm8w