1 Linux platform子系统
在Linux 2.6内核中,提出了总线、设备、驱动的架构,目的是让我们写出来的驱动通用性更强。
arm核内部总线结构:
1.1 核心思想
-
将设备的信息从驱动中分离出来,我们需要在操作系统中,添加设备和驱动两部分。
-
设备中包含是设备的信息(资源),驱动中包含的是操作设备函数接口。
-
为了能让驱动最终能操作我们的硬件设备,我们在驱动中必须获取设备的信息(资源)。驱动是如何获取具体设备信息呢?
-
设备和驱动都会注册到总线上,当注册设备的时候,会去寻找同名的驱动,当注册驱动的时候,也会去找同名的设备。相互查找。一旦匹配成功,操作系统就会自动调用驱动提供的probe函数。我们只需要在probe函数中,使用操作系统提供的通用API获取硬件的资源即可。
1.2 Linux总线的理解
- 总线在操作系统中本质就是两个链表: 挂载设备的链表和挂载驱动的链表。
- 在操作系统中总线种类可以分成两大类:
- 平台总线:平台总线挂载都是控制器设备,用于CPU核与硬件控制器之间的通信。在Linux系统中用"platform bus"表示。
- 边缘设备之间通信的总线:边缘设备之间通信的总线挂载是符合总线时序的外围设备如:i2c , spi ,usb , uart 等。不同的边缘设备之间通信的总线,总线时序是不一样的,
- 对于这些总线,Linux 内核是单独实现的
1.3 基于总线写驱动的思路
- 根据自己的设备,确定总线的类型
- 根据总线的类型, 确定设备在总线上如何描述
- 根据总线的类型, 确定驱动在总线上如何描述
- 根据总线的类型,确定在总线上如何注册设备
- 根据总线的类型,确定在总线上如何注册驱动
- 根据总线的类型, 确定设备和驱动匹配原则
设备和驱动匹配后,操作系统就会调用驱动提供的probe
函数。在这个函数中,一般需要做两件事情:
- (1)获取匹配的硬件资源
- (2)向上层提供硬件设备的操作函数接口(如:注册字符设备)
案例代码:
- 驱动程序:
#include "linux/export.h"
#include "linux/platform_device.h"
#include <linux/init.h>
#include <linux/module.h>
#include <linux/mod_devicetable.h>
#define PLATFORM_LED_NAME "imx_led"
static int imx_led_probe(struct platform_device *pdev)
{
int err = 0;
struct resource *led_ccm_reg;
struct resource *led_mux_reg;
struct resource *led_gpio_reg;
printk("imx led probe\n");
/* probe函数
* 1、获取平台设备资源
2、注册设备驱动,向上层提供接口
*/
/* 1、获取平台设备资源 */
led_ccm_reg = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!led_ccm_reg) {
err = -ENODEV;
printk("platform_get_resource ccm\n");
goto err_platform_get_resource;
}
printk("ccm addr: %#x\n", (unsigned int)led_ccm_reg->start);
led_mux_reg = platform_get_resource(pdev, IORESOURCE_MEM, 1);
if (!led_mux_reg) {
err = -ENODEV;
printk("platform_get_resource mux\n");
goto err_platform_get_resource;
}
printk("mux addr: %#x\n", (unsigned int)led_mux_reg->start);
led_gpio_reg = platform_get_resource(pdev, IORESOURCE_MEM, 2);
if (!led_gpio_reg) {
err = -ENODEV;
printk("platform_get_resource gpio\n");
goto err_platform_get_resource;
}
printk("gpio addr: %#x\n", (unsigned int)led_gpio_reg->start);
/* 2、注册字符设备驱动:向应用层提供接口 todo */
return 0;
err_platform_get_resource:
return err;
}
static int imx_led_remove(struct platform_device *pdev)
{
printk("imx led remove\n");
return 0;
}
/* 3、关联平台设备 */
static const struct of_device_id imx_lex_dt_ids[] = {
{ .compatible = PLATFORM_LED_NAME },
{ }
};
MODULE_DEVICE_TABLE(of, imx_lex_dt_ids);
/* 1、描述平台驱动 */
static struct platform_driver drv = {
.probe = imx_led_probe,
.remove = imx_led_remove,
.driver = {
.name = PLATFORM_LED_NAME,
.owner = THIS_MODULE,
.of_match_table = imx_lex_dt_ids,
}
};
static int __init led_init(void)
{
int err = 0;
/* 2、注册平台驱动 */
err = platform_driver_register(&drv);
if (err < 0) {
printk("platform_driver_register failed\n");
goto err_platform_driver_register;
}
printk("driver init success\n");
return 0;
err_platform_driver_register:
return err;
}
static void __exit led_exit(void)
{
platform_driver_unregister(&drv);
printk("driver exit\n");
}
MODULE_AUTHOR("jk luo");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("This is platform driver");
module_init(led_init);
module_exit(led_exit);
- 设备程序:
#include "linux/kernel.h"
#include "linux/platform_device.h"
#include <linux/init.h>
#include <linux/module.h>
#define PLATFORM_LED_NAME "imx_led"
#define LED_CCM_REG 0x20C406C // 26、27管脚高电平
#define LED_CCM_REG_SIZE 0x4
#define LED_MUXCTL_REG 0x20E00B0 // 低四位设置为0101
#define LED_MUXCTL_REG_SIZE 0x4
#define LED_GPIO_BASE_REG 0x209C000
#define LED_GPIO_REG_SIZE 0x8
#define LED_GPIO_DAT_REG_OFFSET 0x0 // gpio1的27号管脚
#define LED_GPIO_DIR_REG_OFFSET 0x4 // 高电平输出,低电平输入
void led_device_release(struct device *dev)
{
printk("led_device_release\n");
}
static struct resource led_device_resource[] = {
[0] = {
.start = LED_CCM_REG,
.end = LED_CCM_REG + LED_CCM_REG_SIZE - 1,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = LED_MUXCTL_REG,
.end = LED_MUXCTL_REG + LED_MUXCTL_REG_SIZE - 1,
.flags = IORESOURCE_MEM,
},
[2] = {
.start = LED_GPIO_BASE_REG,
.end = LED_GPIO_BASE_REG + LED_GPIO_REG_SIZE - 1,
.flags = IORESOURCE_MEM,
}
};
static struct platform_device pdev = {
.name = PLATFORM_LED_NAME,
.resource = led_device_resource,
.num_resources = ARRAY_SIZE(led_device_resource),
.dev = {
.release = led_device_release,
}
};
static int __init led_init(void)
{
int err = 0;
err = platform_device_register(&pdev);
if (err < 0) {
printk("platform_device_register failed\n");
goto err_platform_device_register;
}
printk("device init success\n");
return 0;
err_platform_device_register:
return err;
}
static void __exit led_exit(void)
{
platform_device_unregister(&pdev);
printk("unregister device\n");
}
MODULE_AUTHOR("jk luo");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("This is platform device");
module_init(led_init);
module_exit(led_exit);
2 Linux设备树
-
Linux内核在启动的时候,要求把设备树文件传递给它。它拿到设备树之后,会解析设备树文件,从而识别设备信息
-
因为设备的信息是针对于特定平台的,如果我们在Linux内核中包含太多设备信息,则Linux内核移植性就会变差。引入设备树之后,设备的信息的描述不再在是以代码的形式存在于Linux内核源代码中
-
dtc,device tree compiler,是将.dts 编译为.dtb需要用到的**编译工具,**是编译设备树的小工具
-
dts,device tree source,是设备树**源码文件,**是描述硬件信息的asiII文本文件
-
dtsi,device tree source include,是设备树源码文件要用到的头文件,类似头文件,描述平台共性
-
dtb,device tree binary,是将DTS 编译以后得到的二进制文件是编译后的二进制文件,可被bootloader/kernel识别并解析
一般开发的时候,把这些设备树文件拷贝出来,方便查找:
imx6ull-14x14-smartcar.dts
:外围设备相关设备树文件imx6ull.dtsi
:芯片相关设备树文件
将dtb文件反编译成dts文件:C.将test.dtb文件反编译成test.dts文件:dtc -O dts -I dtb -o test.dts test.dtb
2.1 设备树语法规则
一个节点就是用来描述一个设备的信息,每个节点必须有一个<名称>[@<设备地址>]
形式的名字。
- 1、名称就是一个ascii字符串,节点的命名应该根据设备的功能来命名。例如一个3com以太网适配器的节点就应该命名为ethernet,而不应该是3com509
- 2、如果存在
reg
属性:则设备地址
是reg属性的第一个数字。 - 3、每个设备的节点都应该有一个compatible属性
2.2 节点属性
属性有两种格式:
- 格式1(没有值) : property-name
- 格式2(键值对) : property-name = value
2.3 添加设备树节点
在设备树文件的首节点的最后一个大括号之前添加新节点: 每个设备树节点必须要有compatible属性。compatible用于与驱动匹配,reg表示寄存器地址和大小。
};
// 在首节点的最后一个大括号之前添加新节点。节点名称格式为:名称@reg中第一个数字
smartcar-led@20c406c {
compatible = "imx-led"; // 这个名称与驱动匹配
reg = <0x20c406c 0x4>,<0x20e00b0 0x4>,<0x209c000 0x8>; // 设备的寄存器地址,多个寄存器,用逗号隔开
};
};
在开发板的/sys/firemware/devicetree/base
目录下可以查看设备树是否添加成功。
2.4 在驱动中匹配设备树节点
通过struct device_driver
结构体中的of_match_table
成员,来匹配设备树节点。
static struct platform_driver imx_led_driver = {
.driver = {
.of_match_table = led_dt_ids, // 匹配设备树节点
},
};
3 pinctrl和GPIO子系统
所谓的子系统,实质上就是设备树中,对SOC芯片不同寄存器进行了定义和配置。驱动开发时,仅需了解了SOC芯片厂商是如何在设备树中定义这些寄存器的,然后将这些子系统配置,以节点属性的方式添加到我们新增的设备树节点中;最后编译设备树,并在驱动程序中调用各子系统的系统API,操作寄存器。
比如:使用IMX6ULL,通过GPIO管脚控制LED流水灯亮、灭。
- 控制流水灯,需要根据硬件电路图,找到对应的控制管脚,设置管脚IO复用模式IOMUX为GPIO的工作模式(IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA)
- 配置好IOMUX后,还需要配置管脚PAD属性,这些属性配置比较复杂,需要根据SOC厂商的芯片说明(IOMUXC_SW_PAD_CTL_PAD_UART1_TX_DATA),进行配置,通常参考设备树中其他pinctrl的配置值即可。
- 最后要配置管脚的输入输出工作模式。如果为输出工作模式,还需要配置高低电平。
基于以上流程:
- SOC芯片厂商,在设备树中,对IOMUX和输入工作模式配置,通过一个宏定义直接做好了,我们只需找到对应的宏即可
- 管脚PAD属性:参考设备树中,同一IOMUX工作模式下,厂商的配置。
- 最后,将这些配置,在pinctrl子系统中,新增子节点。
pinctrl_rgb_led: rgb_led {
fsl,pins = <
MX6UL_PAD_GPIO1_IO04__GPIO1_IO04 0x1b0b1
MX6UL_PAD_CSI_VSYNC__GPIO4_IO19 0x1b0b1
MX6UL_PAD_CSI_HSYNC__GPIO4_IO20 0x1b0b1
>;
};
GPIO子系统:
- 在设备树文件
imx6ull.dtsi
中,定义了各gpioX组的设备信息,只需在我们新增的设备树节点中,以属性的方式引用对应的gpioX组;最后在驱动程序中,调用系统提供的API操作GPIO管脚。
};
// 在首节点的最后一个大括号之前添加新节点
smartcar-led@20c406c {
compatible = "imx-led"; // 这个名称与驱动匹配
reg = <0x20c406c 0x4>,<0x20e00b0 0x4>,<0x209c000 0x8>; // 设备的寄存器地址
status = okay;
pinctrl-0 = <&pinctrl_rgb_led>;
rgb_led_red = <gpio1 4 GPIO_ACTIVE_LOW>;
rgb_led_green = <gpio4 20 GPIO_ACTIVE_LOW>;
rgb_led_red = <gpio4 19 GPIO_ACTIVE_LOW>;
};
};