05、Linux中通用SPI设备驱动分析
参考资料:
- 内核驱动:
drivers\spi\spidev.c
- 内核提供的测试程序:
tools\spi\spidev_fdx.c
- 内核文档:
Documentation\spi\spidev
一、内核和设备树配置
通用 SPI 设备驱动在迅为提供的 Linux 内核中默认已经勾选了, 具体路径如下所示:
C
> Device Drivers
> SPI support
1
2
2
除了内核支持之外, 还需要修改设备树, 由于之前已经使能了 SPI0, 所以这直接修改之前编写的 mcp2515 设备树节点, 具体设备树为 :
二、设备节点
开发板启动后,如果能看到类似 /dev/spidev3.0
的设备节点,说明系统对SPI硬件的配置是正确的。
这个设备名称的含义很简单:
- 前面的数字
3
表示这是第4号SPI总线(总线编号从0开始算) - 后面的数字
0
表示这条总线上第一个设备(设备编号同样从0开始)
举个例子:
/dev/spidev0.0
就是第1号总线上的第1个设备/dev/spidev3.2
就是第4号总线上的第3个设备
系统通过总线号区分不同的SPI控制器,而设备号则通过硬件的片选信号(CS)来指定具体连接的芯片。每个SPI总线最多可以连接多个设备,只要硬件提供了足够的片选引脚。
三、spidev驱动分析
使能CONFIG_SPI_SPIDEV
,这样spidev
驱动将会被编译,我们今天来看一下:
驱动流程图:
3.1、init
目录:spi/spidev.c
第一步操作集注册:
然后会注册:
对应的device信息在设备树里面:
3.2、probe
当SPI设备被识别后,spidev_probe函数会执行以下步骤:
- 记录设备信息 创建一个数据容器(spidev_data结构体),专门用来保存当前SPI设备的相关信息。
- 保存到列表中 把这个数据容器存入一个列表里,方便后续查找。
- 分配唯一编号 为设备分配一个独立的编号。以后可以通过这个编号在列表里快速找到对应的数据容器。
- 生成设备文件 根据总线号(B)和设备序号(D),在
/dev
目录下创建一个设备文件,文件名格式为spidevB.D
。 (例如:总线0上的第1个设备,文件名就是spidev0.1
)
整个过程相当于: 给新发现的SPI设备创建一个"身份档案",分配一个"身份证号",再生成一个用户可访问的设备文件。
C
static int spidev_probe(struct spi_device *spi)
{
struct spidev_data *spidev;
int status;
unsigned long minor;
/*
* spidev should never be referenced in DT without a specific
* compatible string, it is a Linux implementation thing
* rather than a description of the hardware.
*/
WARN(spi->dev.of_node &&
of_device_is_compatible(spi->dev.of_node, "spidev"),
"%pOF: buggy DT: spidev listed directly in DT\n", spi->dev.of_node);
spidev_probe_acpi(spi);
/* Allocate driver data */
spidev = kzalloc(sizeof(*spidev), GFP_KERNEL);
if (!spidev)
return -ENOMEM;
/* Initialize the driver data */
spidev->spi = spi;
spin_lock_init(&spidev->spi_lock);
mutex_init(&spidev->buf_lock);
INIT_LIST_HEAD(&spidev->device_entry);
/* If we can allocate a minor number, hook up this device.
* Reusing minors is fine so long as udev or mdev is working.
*/
mutex_lock(&device_list_lock);
minor = find_first_zero_bit(minors, N_SPI_MINORS);
if (minor < N_SPI_MINORS) {
struct device *dev;
//创建设备号
spidev->devt = MKDEV(SPIDEV_MAJOR, minor);
//创建设备
dev = device_create(spidev_class, &spi->dev, spidev->devt,
spidev, "spidev%d.%d",
spi->master->bus_num, spi->chip_select);
status = PTR_ERR_OR_ZERO(dev);
} else {
dev_dbg(&spi->dev, "no minor number available!\n");
status = -ENODEV;
}
if (status == 0) {
//设置位
set_bit(minor, minors);
//把spidev添加到device_list
list_add(&spidev->device_entry, &device_list);
}
mutex_unlock(&device_list_lock);
spidev->speed_hz = spi->max_speed_hz;
if (status == 0)
spi_set_drvdata(spi, spidev);
else
kfree(spidev);
return status;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
3.3、spidev_fops
① spidev_write
② spidev_read
③ spidev_open
④ spidev_ioctl
可以分为三类:
- spi通信参数获取
- spi通信参数设置
- 数据的传输
常用SPI ioctl参数及功能:
**SPI_IOC_RD_MODE**
读取当前SPI模式。 会把当前的通信时序模式(极性和相位设置)读到一个整数中,方便查看。**SPI_IOC_WR_MODE**
设置SPI模式。 需要传入一个数字,用两位二进制表示时钟极性(CPOL)和相位(CPHA)。**SPI_IOC_RD_BITS_PER_WORD**
读取数据位宽。 会把每个数据包包含的位数(比如8位、16位)读到一个整数里。**SPI_IOC_WR_BITS_PER_WORD**
设置数据位宽。 传入一个数字,指定每个数据包发送/接收的位数(如8位)。**SPI_IOC_RD_MAX_SPEED_HZ**
读取当前最大速率。 会把SPI总线的当前最大传输速度(单位:赫兹)读到一个整数里。**SPI_IOC_WR_MAX_SPEED_HZ**
设置最大速率。 传入一个数字,指定SPI总线允许的最高速率(如1MHz)。**SPI_IOC_MESSAGE(N)**
执行多步读写操作。 需要传入一个包含_N_
个传输任务的数组,每个任务描述一次发送/接收操作(比如先发数据再读结果)。**SPI_IOC_RD_LSB_FIRST**
读取数据顺序设置。 会返回是否启用“先发送最低有效位(LSB)”的模式。**SPI_IOC_WR_LSB_FIRST**
设置数据顺序。 传入0或1,决定是否启用“先发送LSB”的模式(默认通常是高位在前)。
:::
简单总结:
RD_ 开头的参数:读取当前设置(如模式、速率)。
WR_ 开头的参数:设置新值(需传入数值)。
SPI_IOC_MESSAGE
是核心操作,用于实际的数据收发,支持一次执行多个步骤。 :::
C
static long
spidev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
int retval = 0;
struct spidev_data *spidev;
struct spi_device *spi;
u32 tmp;
unsigned n_ioc;
struct spi_ioc_transfer *ioc;
/* Check type and command number */
if (_IOC_TYPE(cmd) != SPI_IOC_MAGIC)
return -ENOTTY;
/* guard against device removal before, or while,
* we issue this ioctl.
*/
spidev = filp->private_data;
spin_lock_irq(&spidev->spi_lock);
spi = spi_dev_get(spidev->spi);
spin_unlock_irq(&spidev->spi_lock);
if (spi == NULL)
return -ESHUTDOWN;
/* use the buffer lock here for triple duty:
* - prevent I/O (from us) so calling spi_setup() is safe;
* - prevent concurrent SPI_IOC_WR_* from morphing
* data fields while SPI_IOC_RD_* reads them;
* - SPI_IOC_MESSAGE needs the buffer locked "normally".
*/
mutex_lock(&spidev->buf_lock);
switch (cmd) {
/* read requests */
case SPI_IOC_RD_MODE:
retval = put_user(spi->mode & SPI_MODE_MASK,
(__u8 __user *)arg);
break;
case SPI_IOC_RD_MODE32:
retval = put_user(spi->mode & SPI_MODE_MASK,
(__u32 __user *)arg);
break;
case SPI_IOC_RD_LSB_FIRST:
retval = put_user((spi->mode & SPI_LSB_FIRST) ? 1 : 0,
(__u8 __user *)arg);
break;
case SPI_IOC_RD_BITS_PER_WORD:
retval = put_user(spi->bits_per_word, (__u8 __user *)arg);
break;
case SPI_IOC_RD_MAX_SPEED_HZ:
retval = put_user(spidev->speed_hz, (__u32 __user *)arg);
break;
/* write requests */
case SPI_IOC_WR_MODE:
case SPI_IOC_WR_MODE32:
if (cmd == SPI_IOC_WR_MODE)
retval = get_user(tmp, (u8 __user *)arg);
else
retval = get_user(tmp, (u32 __user *)arg);
if (retval == 0) {
u32 save = spi->mode;
if (tmp & ~SPI_MODE_MASK) {
retval = -EINVAL;
break;
}
tmp |= spi->mode & ~SPI_MODE_MASK;
spi->mode = (u16)tmp;
retval = spi_setup(spi);
if (retval < 0)
spi->mode = save;
else
dev_dbg(&spi->dev, "spi mode %x\n", tmp);
}
break;
case SPI_IOC_WR_LSB_FIRST:
retval = get_user(tmp, (__u8 __user *)arg);
if (retval == 0) {
u32 save = spi->mode;
if (tmp)
spi->mode |= SPI_LSB_FIRST;
else
spi->mode &= ~SPI_LSB_FIRST;
retval = spi_setup(spi);
if (retval < 0)
spi->mode = save;
else
dev_dbg(&spi->dev, "%csb first\n",
tmp ? 'l' : 'm');
}
break;
case SPI_IOC_WR_BITS_PER_WORD:
retval = get_user(tmp, (__u8 __user *)arg);
if (retval == 0) {
u8 save = spi->bits_per_word;
spi->bits_per_word = tmp;
retval = spi_setup(spi);
if (retval < 0)
spi->bits_per_word = save;
else
dev_dbg(&spi->dev, "%d bits per word\n", tmp);
}
break;
case SPI_IOC_WR_MAX_SPEED_HZ:
retval = get_user(tmp, (__u32 __user *)arg);
if (retval == 0) {
u32 save = spi->max_speed_hz;
spi->max_speed_hz = tmp;
retval = spi_setup(spi);
if (retval >= 0)
spidev->speed_hz = tmp;
else
dev_dbg(&spi->dev, "%d Hz (max)\n", tmp);
spi->max_speed_hz = save;
}
break;
default:
/* segmented and/or full-duplex I/O request */
/* Check message and copy into scratch area */
ioc = spidev_get_ioc_message(cmd,
(struct spi_ioc_transfer __user *)arg, &n_ioc);
if (IS_ERR(ioc)) {
retval = PTR_ERR(ioc);
break;
}
if (!ioc)
break; /* n_ioc is also 0 */
/* translate to spi_message, execute */
retval = spidev_message(spidev, ioc, n_ioc);
kfree(ioc);
break;
}
mutex_unlock(&spidev->buf_lock);
spi_dev_put(spi);
return retval;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
spi_ioc_transfer
- spidev_get_ioc_message
- spidev_message
四、spi_register_driver分析
spi/spi.c
五、spi_dev缺点
使用SPI通信时:
- 读写函数(read/write)只能单次完成一个方向操作(要么读,要么写),这叫半双工模式。
- 通过ioctl命令可以设置参数,让设备同时进行读写操作(全双工模式)。
但使用spidev接口有两点限制:
- 没有中断功能:设备状态变化时无法自动通知CPU
- 只能同步操作:执行读/写命令时,程序必须等到操作完全结束才能继续往下执行,不能同时处理其他任务