文章目录
设备树参考
自定义设备树
设备树有什么用?
驱动是什么?驱动可以理解为一个中间软件,将底层硬件资源抽象封装并提供API给上层应用使用。
在学习STM32开发时,没有驱动这一说法,因为一般都是在一个程序中就实现了:对硬件资源的操作,并实现相应的业务逻辑。以uart的使用为例:
- 先在头文件中定义好uart的硬件资源信息,如引脚、波特率、寄存器等设置,
FLAG-dts
; - 编写相应的驱动函数,如uart的初始化、发送一个字节、接收一个字节等,
FLAG-driver
; - 利用驱动函数编写业务逻辑,如uart双方先定义好通信协议,然后对uart收到的数据进行解析,以执行相应的动作,
FLAG-app
; - 上面的功能实现可以认为是写在一个程序中的;
而对于嵌入式Linux驱动开发,则是通过设备树单独描述硬件资源。
- 在设备树中通过节点对外设进行描述,描述的属性不定,但常见的有
compatible, reg, pinctrl, gpio
等,作用同FLAG-dts
; compatible
的值与内核中某个驱动程序相匹配,则该驱动程序就会执行(在驱动程序中就会使用节点所描述的硬件资源,并给上层应用提供API),作用同FLAG-driver
;- 利用驱动程序提供的API编写应用程序,作用同
FLAG-app
; - 上面的功能实现在不同的文件中:设备树、驱动程序;
设备树在哪里?
Linux源码下的arch/arm/boot/dts/
如何编译设备树
在Linux顶层文件夹下make dtbs
相关makefile
基于 ARM 架构的 SOC 有很多种,一种 SOC 又可以制作出很多款板子,每个板子都有一个对应的 DTS 文件,那么如何确定编译哪一个 DTS 文件呢?我们就以 I.MX6ULL 这款芯片对应的板子为例来看一下,打开 arch/arm/boot/dts/Makefile
,有如下内容:
dtb-$(CONFIG_SOC_IMX6UL) += \
imx6ul-14x14-ddr3-arm2.dtb \
imx6ul-14x14-ddr3-arm2-emmc.dtb \
......
dtb-$(CONFIG_SOC_IMX6ULL) += \
imx6ull-14x14-ddr3-arm2.dtb \
......
imx6ull-14x14-evk.dtb \
imx6ull-14x14-evk-btwifi.dtb \
imx6ull-14x14-evk-emmc.dtb \
imx6ull-14x14-evk-gpmi-weim.dtb \
imx6ull-14x14-evk-usb-certi.dtb \
# imx6ull-alientek-emmc.dts对应的编译结果文件
imx6ull-alientek-emmc.dtb \
imx6ull-14x14-emmc-4.3-800x480-c.dtb \
如果我们使用 I.MX6ULL 新做了一个板子,只需要新建一个此板子对应的.dts 文件,然后将对应的.dtb 文件名添加到 dtb-$(CONFIG_SOC_IMX6ULL)
下,这样在编译设备树的时候就会将对应的.dts 编译为二进制的.dtb 文件。
如何创建自己的设备树文件
-
首先可以根据相似的、已有的设备树文件进行复制,如使用命令
cp imx6ull-14x14-evk.dts imx6ull-alientek-emmc.dts
; -
然后再根据自己的实际板卡对
imx6ull-alientek-emmc.dts
进行修改; -
此时只是有了设备树文件,如需要编译,则需要将其加入到Makefile中去。在同级目录中找到Makefile,并找到
dtb-$(CONFIG_SOC_IMX6ULL)
,添加自己的设备树文件;dtb-$(CONFIG_SOC_IMX6ULL) += \ ...... // 将自己的设备树添加到makefile中去 imx6ull-alientek-emmc.dtb \
-
通过命令
make dtbs
即可生成imx6ull-alientek-emmc.dtb
;
gpio和pinctrl子系统
pinctrl
注意:pin controller 节点的格式,没有统一的标准!!!!每家芯片都不一样。甚至上面的 group、function 关键字也不一定有,但是概念是有的
//client device
gpioled {
compatible = "atk,gpioled";
pinctrl-names = "default"; //gpioled这个设备有1个状态,
pinctrl-0 = <&pinctrl_led>; //这是对应的状态,即在pinctrl_led那里声明了。
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
status = "okay";
};
//pin ctroller(imx6ull的格式)
pinctrl_led: ledgrp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x17059 //将GPIO1_IO03复用为GPIO1_IO03
>;
};
gpio
在几乎所有 ARM 芯片中,GPIO 都分为几组,每组中有若干个引脚。所以在使用 GPIO 子系统之前,就要先确定:它(我们要用的管脚)是哪组的?组里的哪一个?
在设备树中,“GPIO 组”就是一个 GPIO Controller,这通常都由芯片厂家设置好。我们要做的是找到它名字,比如“gpio1”,然后指定要用它里面的哪个引脚,比如<&gpio1 0>。
定义 GPIO Controller 是芯片厂家的事,我们怎么引用某个引脚呢?在自己的设备节点中使用属性"[<name>-]gpios"
,示例如下:
gpioled {
compatible = "atk,gpioled";
pinctrl-names = "default"; //gpioled这个设备有1个状态,
pinctrl-0 = <&pinctrl_led>; //这是对应的状态,即在pinctrl_led这里声明了。
//gpioled这个设备使用gpio1的第3个引脚,并设为低有效
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
status = "okay";
};
总结
用上面gpioled这个设备结点来说明gpio和pinctrol子系统。
pinctrl-names是说明这个结点有几个状态的,对于一个“client device”来说,比如对于一个 UART 设备,它有多个“状态”:default、sleep 等,那对应的引脚也有这些状态。怎么理解? 比如默认状态下,UART 设备是工作的,那么所用的引脚就要复用为 UART 功能。在休眠状态下,为了省电,可以把这些引脚复用为 GPIO 功能;或者直接把它们配置输出高电平。
deviceNode {
pinctrl-names = "uart", "led";
pinctrl-0 = <&pinctrl_uart>;
pinctrl-1 = <&pinctrl_led>;
}
//解释如下:
这个结点有两种状态(两个名字),是父亲时,就要履行父亲的责任,所以要变成父亲的状态(pinctrl_uart)。是学生时,要做学生该做的事,要变成学生的状态(pinctrl_led)。
下图中让某个结点在某种状态下的将gpio1-3复用为gpio1-3
led-gpio设置某个设备用那个管脚,并默认为什么状态。
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
#解释
gpiol这个GPIO组已经被厂商写好,我们只管用就行。名字格式为[<name>-]gpio。
其实pinctrl和gpio中都出现了具体的管脚。但led-gpio只是说用那个,而pinctrl还要设置为对应的模式。
# 此设备结点没有用到gpio子系统,只是用了pinctrol子系统来配置管脚的模式
&usdhc2 {
pinctrl-names = "default", "state_100mhz", "state_200mhz";
pinctrl-0 = <&pinctrl_usdhc2_8bit>;
pinctrl-1 = <&pinctrl_usdhc2_8bit_100mhz>;
pinctrl-2 = <&pinctrl_usdhc2_8bit_200mhz>;
bus-width = <8>;
non-removable;
status = "okay";
};
# 状态0(默认状态)对应的配置情况
pinctrl_usdhc2_8bit: usdhc2grp_8bit {
fsl,pins = <
MX6UL_PAD_NAND_RE_B__USDHC2_CLK 0x10069
MX6UL_PAD_NAND_WE_B__USDHC2_CMD 0x17059
MX6UL_PAD_NAND_DATA00__USDHC2_DATA0 0x17059
MX6UL_PAD_NAND_DATA01__USDHC2_DATA1 0x17059
MX6UL_PAD_NAND_DATA02__USDHC2_DATA2 0x17059
MX6UL_PAD_NAND_DATA03__USDHC2_DATA3 0x17059
MX6UL_PAD_NAND_DATA04__USDHC2_DATA4 0x17059
MX6UL_PAD_NAND_DATA05__USDHC2_DATA5 0x17059
MX6UL_PAD_NAND_DATA06__USDHC2_DATA6 0x17059
MX6UL_PAD_NAND_DATA07__USDHC2_DATA7 0x17059
>;
};
我发现pinctrl都在结点iomuxc或iomuxc_snvs中。
再看一个lcd结点
&lcdif {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_lcdif_dat
&pinctrl_lcdif_ctrl>;
display = <&display0>;
}
# 解释
lcdif这个设备结点有1种默认状态,这种状态它的配置情况是pinctrl-0,包含两个具体的配置情况,一个是数据接口,另一个是控制接口。
其实我觉得可以写在一起,比如:
pinctrl-0=<&pinctrl_lcdif_dat_ctrl>
# 数据管脚组的配置情况
pinctrl_lcdif_dat: lcdifdatgrp {
fsl,pins = <
MX6UL_PAD_LCD_DATA00__LCDIF_DATA00 0x18
MX6UL_PAD_LCD_DATA01__LCDIF_DATA01 0x18
......
>;
}
# 控制管脚组的配置情况
pinctrl_lcdif_ctrl: lcdifctrlgrp {
fsl,pins = <
MX6UL_PAD_LCD_CLK__LCDIF_CLK 0x18
MX6UL_PAD_LCD_ENABLE__LCDIF_ENABLE 0x18
MX6UL_PAD_LCD_HSYNC__LCDIF_HSYNC 0x18
MX6UL_PAD_LCD_VSYNC__LCDIF_VSYNC 0x18
>;
};
# 合在一起
pinctrl_lcdif_dat_ctrl: lcdifdatctrlgrp {
fsl,pins = <
......
>
}
gpioled驱动编写
设备树编写
alpha正点原子上的led灯在gpio1的第3个管脚上,所以在根结点下创建一个gpioled结点,如下:
/{
gpioled {
compatible = "cdj,led";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl-led>;
led-gpios = <gpio1 3 GPIO_ACTIVE_LOW>;
status = "oaky";
};
......
}
&iomuxc {
pinctrl_led: ledgrp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x17059
>;
};
......
}
驱动程序编写
1. 驱动入口-注册平台驱动-__init中
/* 1. 定义platform_driver */
static struct platform_driver chip_demo_gpio_driver = {
.probe = chip_demo_gpio_probe,
.remove = chip_demo_gpio_remove,
.driver = {
.name = "100ask_led",c
.of_match_table = ask100_leds,
},
};
//在led_init中注册
err = platform_driver_register(&chip_demo_gpio_driver);
2. 当平台驱动和平台设备匹配成功后会调用probe函数
在probe函数中获取led的管脚描述符-struct gpio_desc
static int chip_demo_gpio_probe(struct platform_device *pdev)
{
/* 4.1 设备树中定义有: led-gpios=<...>; */
led_gpio = gpiod_get(&pdev->dev, "led", 0);
/* 4.2 注册file_operations */
major = register_chrdev(0, "100ask_led", &led_drv);
led_class = class_create(THIS_MODULE, "100ask_led_class");
if (IS_ERR(led_class)) {
unregister_chrdev(major, "led");
gpiod_put(led_gpio);
return PTR_ERR(led_class);
}
device_create(led_class, NULL, MKDEV(major, 0), NULL, "100ask_led%d", 0); /* /dev/100ask_lecd0 */
return 0;
}
3. 在open中初始化led
gpiod_direction_output(led_gpio, 0);
4. 在write中设置led
err = copy_from_user(&status, buf, 1);
/* 根据次设备号和status控制LED */
gpiod_set_value(led_gpio, status);
5. 驱动卸载时-remove函数
device_destroy(led_class, MKDEV(major, 0));
class_destroy(led_class);
unregister_chrdev(major, "100ask_led");
gpiod_put(led_gpio);
6.模块退出(卸载)时-__exit函数中
我觉得吧,remove函数被调用后就会自动调用__exit函数。
platform_driver_unregister(&chip_demo_gpio_driver);