Linux SPI驱动学习——注册匹配
@
博客说明
| 撰写日期 | 2019.10.22
---|:--😐---
| 完稿日期 | 2019.10.23
| 最近维护 | 暂无
| 本文作者 | multimicro
| 联系方式 | multimicro@qq.com
| 资料链接 | 本文无附件资料
| GitHub| https://github.com/wifialan/drivers/
| 原文链接| https://blog.csdn.net/multimicro/article/details/102685871
开发环境
环境说明 | 详细信息 | 备注信息 |
---|---|---|
操作系统 | Ubunut 18.04 | |
开发板 | JZ2440-V3 | |
Linux内核 | linux-3.4.2 |
1. Linux SPI概述
鄙人通过查看宋宝华《Linux设备驱动开发详解–基于最新的Linux 4.0内核》 第12章:Linux设备驱动的软件架构思想,初步了解了总线
、设备
和驱动
这三个名词:
总线:
比如4线SPI的总线是四条线,这四条线就构成了SPI总线,但不知道这样解释合不合适,保留疑问。
设备:
对应的是spi_device
——外设设备的抽象
驱动:
对应的是spi_drivce
——外设端驱动
以上解释暂保留疑问。
先知道有这三个名词吧。
下面的内容只是对SPI驱动的初步实现进行感性的认识,先实现,后谈理论分析。
1.1 SPI驱动框架
如下图所示
设备驱动
(外设端驱动)抽象出来一个spi_driver,用外设模块所规定的传输协议收发数据,具体实现就是调用主机端的spi收发函数进行排列组合实现外设协议所规定的波形。
控制器驱动
(主机端驱动)抽象出来一个spi_master,用于产生总线上的波形。比如调用spi_transfer函数发送一个16位的数据,那么在总线上就会生成一个16位的SPI波形,主机端只产生波形不干别的。
2. SPI 注册匹配
2.1 spi_drive注册
再看韦东山SPI视频时,他说参考内核中的其他代码进行编写,如sound/soc/codecs/ad1936.c
文件中第374-388c行:
static struct spi_driver ad1836_spi_driver = {
.driver = {
.name = "ad1836",
.owner = THIS_MODULE,
},
.probe = ad1836_spi_probe,
.remove = __devexit_p(ad1836_spi_remove),
.id_table = ad1836_ids,
};
static int __init ad1836_init(void)
{
return spi_register_driver(&ad1836_spi_driver);
}
module_init(ad1836_init);
Tips:在source inside中采用快捷键ctrl + ?
调出Lookup References
框框,然后输如spi_driver
,在生成的搜索结果里面第一项展开即可直接定位至文件中的spi_driver
所在行。
注册spi_driver的步骤为:
Step 1:
我仿照编写的spi_driver
程序为如下:
路径:drivers/char/w25q16_spi.c
static struct spi_driver w25q16_spi_driver =
{
.driver =
{
.name = "w25q16", /* spi_driver注册成功后,会在/sys/bus/spi/drivers/目录下面显示出该name字段的名字,见下图 */
.owner = THIS_MODULE,
},
.probe = w25q16_bus_spi_probe,
.remove = __devexit_p(w25q16_bus_spi_remove),
};
module_init(w25q16_driver_init);
该程序所在文件的位置为:drives/char/w25q16_spi.c
我把这个flash定为字符驱动进行编写了,所以该文件在char
这个文件夹里面。
按照驱动在内核模块中的加载方式,还需要同步修改Kconfig
和Makefile
Step 2:
在Kconfig
中增添信息
Step 3:
在Makefile
中增添信息
在menuconfig
菜单中勾选此选项即可,另外,为了开启SPI支持,需要在menuconfig
菜单中同步开启如下选项:
配置内核使用主控驱动 spi-s3c24xx.c
-> General setup
[*] Prompt for development and/or incomplete code/drivers
-> Device Drivers
-> SPI support
<*> Samsung S3C24XX series SPI
2.2 spi_device注册
spi_device 的注册可以由系统完成,具体是通过内核中spi_match_master_to_boardinfo
函数(在spi_register_board_info
函数中调用),board_info里含有bus_num, 如果某个spi_master的bus_num跟它一样,则创建一个新的spi_device,代码如下:
路径:drivers/spi/spi.c
static void spi_match_master_to_boardinfo(struct spi_master *master,
struct spi_board_info *bi)
{
struct spi_device *dev;
if (master->bus_num != bi->bus_num)
return;
dev = spi_new_device(master, bi);
if (!dev)
dev_err(master->dev.parent, "can't create new device for %s\n",
bi->modalias);
}
可以看到,如果master->bus_num == bi->bus_num
时,才会执行spi_new_device
函数创建spi_device
。
s3c2440有两个spi控制器,那么bus_num就有两个值:0和1,分别对应SPI0和SPI1。
上述函数中传递的第二个参数是spi_board_info
结构体,那么我们就需要构造一个这样的结构体,这个结构体怎么构造呢?首先就要追溯到这个函数的上层函数spi_register_board_info
中去,在source inside中按照上面讲的方法搜索该函数,则可以找出很多例子,下面是我仿照其他文件中的方式构造的:
只有下面这个程序是本节要单独编写的代码
路径:driver/spi/spi_info_jz2440.c
#include <linux/module.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/spi/spi.h>
#include <linux/gpio.h>
#include <mach/regs-gpio.h>
#include <plat/gpio-cfg.h>
static struct spi_board_info spi_info_jz2440[] = {
{
.modalias = "oled",
.max_speed_hz = 10000000,
.bus_num = 1,
.mode = SPI_MODE_0,
.chip_select = S3C2410_GPF(1),
//.platform_data = (const void *)S3C2410_GPG(4) ,
},
{
.modalias = "w25q16",
.max_speed_hz = 80000000, /* max spi clock (SCK) speed in HZ */
.bus_num = 0,
.mode = SPI_MODE_0,
.chip_select = S3C2410_GPG(2),
}
};
static int spi_info_jz2440_init(void)
{
printk("spi_info_jz2440_init function..\n");
return spi_register_board_info(spi_info_jz2440, ARRAY_SIZE(spi_info_jz2440));
}
module_init(spi_info_jz2440_init);
可以看到spi_board_info
结构体中包含两项(也可以只构造一项),每项都包含名字,最大时钟频率,总线编号,模式和片选等信息。
从名字可以看出,这个结构体主要和外设模块信息有关,它只规定这个外设模块使用多高的SPI时钟频率,接到那个SPI控制器上,片选用那个引脚,采用什么模式等。其实就是把外设模块的信息汇总抽象生成一个结构体,通过调用该结构体,来注册符合实际外设SPI模块的spi_device
。
参考 宋宝华《Linux设备驱动开发详解–基于最新的Linux 4.0内核》 12.4.1节 P322中所述:
4) 板级逻辑。板级逻辑用来描述主机主机和外设是如何互联的,它相当于一个“路由表”。假设板子上由多个SPI控制器和多个SPI外设,那究竟谁接在谁上面?管理互联关系,既不是主机端的责任,也不是外设端的责任,这属于板级逻辑的责任。这部分通常出现在 arch/arm/mach-xxx 下面或者 arch/arm/boot/dts 下面。
下面看一下spi_register_board_info
函数:
路径:drivers/spi/spi.c
int __devinit
spi_register_board_info(struct spi_board_info const *info, unsigned n)
{
struct boardinfo *bi;
int i;
bi = kzalloc(n * sizeof(*bi), GFP_KERNEL);
if (!bi)
return -ENOMEM;
for (i = 0; i < n; i++, bi++, info++) {
struct spi_master *master;
memcpy(&bi->board_info, info, sizeof(*info)); //把info结构体的内容复制到 bi->board_info 里面
mutex_lock(&board_lock);
list_add_tail(&bi->list, &board_list);
list_for_each_entry(master, &spi_master_list, list)
spi_match_master_to_boardinfo(master, &bi->board_info); //这个函数就是上面2.2节的贴出的第一个函数,现在调到这个函数的实体中去,在看一下
mutex_unlock(&board_lock);
}
return 0;
}
分析玩上面函数以及注释后,可以大概得出这样一个流程:
如果spi_board_info
结构体里面的bus_num
和spi_master
里面的bus_num
相等的话,在spi_match_master_to_boardinfo
函数中调用spi_new_device
创建一个spi_device
在/sys/bus/spi/devices/
文件夹里面可以看到spi_device
的注册信息:
这个spi0.194
和spi1.161
的命名在spi_add_device
函数里面:
路径:drivers/spi/spi.c
第357-358行
dev_set_name(&spi->dev, "%s.%u", dev_name(&spi->master->dev),
spi->chip_select);
可以看出,后面的数字是和chip_select
有关,这里的chip_select
是在spi_board_info
里面定义的,通过%u
的格式将其打印输出。回过头看spi_board_info
结构体里面的chip_select
:
static struct spi_board_info spi_info_jz2440[] = {
{
... ...
.bus_num = 1;
.chip_select = S3C2410_GPF(1), //%u 输出是 161
},
{
... ...
.bus_num = 0;
.chip_select = S3C2410_GPG(2), //%u 输出是 194
}
};
就明白后面的数字是怎么回事了。
spi_board_info
结构体里面的chip_select
变量就是获取的该SPI控制器所调用的片选IO引脚
.chip_select = S3C2410_GPF(1)
表示该SPI控制器选用GPF1作为CS引脚
.chip_select = S3C2410_GPG(2)
表示该SPI控制器选用GPG2作为CS引脚
之前认为能作为SPI控制器的CS信号引脚的,一定是芯片级支持的,不是随便找一个IO的,但是实际测试发现,S3C2440这个板子可以使用任意一个引脚作为CS片选引脚
,对于其他板子,不知道可不可以。
下面给出流程:
注:spi_register_board_info
函数不能被编为模块,否则会出现
WARNING: "spi_register_board_info" [drivers/spi/spi_info_jz2440.ko] undefined!
原因就是内核没有将此函数导出来,导致该函数不可被外部程序所调用。
拓展:
为了能让函数在其他模块中使用,内核采用了以下方式修饰函数,这样即可将修饰后的函数供模块外使用。
EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名)
在内核文件driver/spi/spi.c
中使用了大量的EXPORT_SYMBOL_GPL(spi_new_device)
使得修饰后的函数供模块外程序调用。
参考资料:linux模块导出符号 EXPORT_SYMBOL_GPL EXPORT_SYMBOL
2.3 SPI的device和driver匹配
device 和 driver 在内核中分别注册后,若其下的name相同,则会调用 xxx_driver中的probe函数进行配对,使device和driver绑定在同一条总线上面
- 首先看以下spi_driver下的
name
字段
路径:drivers/char/w25q16_spi.c
static struct spi_driver w25q16_spi_driver =
{
.driver =
{
.name = "w25q16",
.owner = THIS_MODULE,
},
.probe = w25q16_bus_spi_probe,
.remove = __devexit_p(w25q16_bus_spi_remove),
};
- 在看以下spi_deivce下的
name
字段(由spi_board_info结构体提供)
路径:driver/spi/spi_info_jz2440.c
static struct spi_board_info spi_info_jz2440[] = {
{
.modalias = "oled",
.max_speed_hz = 10000000,
.bus_num = 1,
.mode = SPI_MODE_0,
.chip_select = S3C2410_GPF(1),
//.platform_data = (const void *)S3C2410_GPG(4) ,
},
{
.modalias = "w25q16",
.max_speed_hz = 80000000, /* max spi clock (SCK) speed in HZ */
.bus_num = 0,
.mode = SPI_MODE_0,
.chip_select = S3C2410_GPG(2),
}
};
两者name
字段都是"w25q16"
。
故在driver和device在内核注册后可自动调用spi_driver
的probe
函数,其实体为w25q16_bus_spi_probe
路径:drivers/char/w25q16_spi.c
struct spi_device *spi_w25q16_pdev;
static int __devinit w25q16_bus_spi_probe(struct spi_device *spi)
{
int ret,err;
dev_t devid;
spi_w25q16_pdev = spi;
s3c2410_gpio_cfgpin(spi->chip_select, S3C2410_GPIO_OUTPUT);
if(major) {
devid = MKDEV(major, 0);
ret = register_chrdev_region(devid, 1, DRV_NAME);
printk(DRV_NAME "\tOrigin Creat node %d\n",major);
} else {
ret = alloc_chrdev_region(&devid, 0, 1, DRV_NAME);
major = MAJOR(devid);
printk(DRV_NAME "\tArrage Creat node %d\n",major);
}
if(ret < 0) {
printk(DRV_NAME "\tnew device failed\n");
//goto fail_malloc;
return ret;
}
w25q16_pdev = kzalloc(sizeof(struct w25q16_dev_t), GFP_KERNEL);
if(!w25q16_pdev) {
ret = -ENOMEM;
goto fail_malloc;
}
cdev_init(&w25q16_pdev->cdev, &w25q16_ops);
err = cdev_add(&w25q16_pdev->cdev, devid, 1);
if(err)
printk(DRV_NAME "\tError %d adding w25q16 %d\n",err, 1);
class = class_create(THIS_MODULE, "w25q16");
device_create(class, NULL, MKDEV(major, minor), NULL, "w25q16");
printk(DRV_NAME "\tcreat device node /dev/w25q16 \n");
return 0;
fail_malloc:
printk("Failed to allocate memory!\n");
return ret;
}
spi_device和spi_driver匹配成功后,在probe
函数内实现字符驱动的注册:
附spi_driver
的注册程序:
static int __init w25q16_driver_init(void)
{
int ret;
printk("\n************ driver init begin ************\n\n");
ret = spi_register_driver(&w25q16_spi_driver);
if(ret)
{
spi_unregister_driver(&w25q16_spi_driver);
printk(DRV_NAME "\tFailed register spi driver. Error: %d\n",ret);
}
printk("\n************* driver init end *************\n\n");
return ret;
}
可以看出,一旦注册完spi_driver
,那么就会自动寻找同名的spi_device
,匹配完成后,则会自动执行probe
函数。
至此,完成了spi_device和spi_driver的匹配注册。总体流程如下图: