目的
前面文章 《嵌入式Linux驱动开发 03:平台(platform)总线驱动模型》 引入了资源和驱动分离的概念,这篇文章将在前面基础上更进一步,引入设备树的概念。
基础说明
在平台总线驱动模型中资源和驱动已经从逻辑上和代码组织上进行了分离,但每次调整资源还是会涉及到内核,所以现在更加流行的是设备树方式。设备树的好处是通过独立于内核存在,这样如果设备上外设功能启用与否以及位置变动的话很多时候不用修改与编译内核,只要重新处理设备树文件即可。
设备树代码(或者都算不上代码)只是一些树状的数据,有点像JSON。通常每个系列的芯片厂家都会编写好后缀为 .dtsi
的设备树文件,里面把芯片基本上的功能资源都定义了。而对于某个具体的电路板来说,只要编写后缀为 .dts
的文件,在其中引入前述的 .dtsi
文件,然后在 .dts
文件中选择性启用 .dtsi
中已经定义好的并且电路中需要用到的功能。当然在 .dts
文件中也可以自定义新的功能。
.dts
文件最终可以编译为 .dtb
文件,系统在启动的时候会通过Bootloader将该文件传递给内核,内核就会解析取用其中的资源并与驱动进行匹配。如果资源需要调整,通常只需要调整 .dts
文件生成新的 .dtb
文件即可。
开发准备
本文中演示中涉及目录与文件结构组织如下:
基本上和前文相同,只需稍作修改。
进入源码目录:
cd ~/nuc980-sdk/NUC980-linux-5.10.y/
调整 drivers/user/char_dev
目录下的 Makefile
文件,其内容改为如下:
obj-$(CONFIG_USER_CHAR_DEV) += char_drv.o
设备树调整
在这篇文章中将在设备树中创建一个自己的节点供下面的驱动程序使用:
# cd ~/nuc980-sdk/NUC980-linux-5.10.y/
gedit arch/arm/boot/dts/nuc980-dev-v1.0.dts
自定义节点内容如下:
nx_node@0 {
compatible = "nx_dts_node";
str = "Naisu 233!";
num = <0x00000000 0x00000020>;
};
nx_node@1 {
compatible = "nx_dts_node";
str = "Hello Naisu!";
num = <0x000000 0x00000040>;
};
完整的设备树文件内容见文章结尾。
上面节点中 compatible
字段的内容用于驱动程序查找匹配; str
字段后面的形式是字符串; num
字段后面是数值,可以用来表示u32或u64(需要用两个u32,中间用空格隔开)。
修改完成后编译然后拷贝到开发板上进行测试:
# 设置编译工具链
# export ARCH=arm; export CROSS_COMPILE=arm-buildroot-linux-gnueabi-
# export PATH=$PATH:/home/nx/nuc980-sdk/buildroot-2023.02/output/host/bin
# 编译生成设备树文件
make dtbs
# 编译完成后拷贝到电脑上再拷贝到SD卡中
# sudo cp arch/arm/boot/dts/nuc980-dev-v1.0.dtb /media/sf_common/
# 我这里开发环境和开发板在同一局域网中,所以可以直接通过网络将dtb文件拷贝到开发板上
# 在开发板中挂载boot分区
# mount /dev/mmcblk0p1 /mnt/
# 在ubuntu中使用scp命令拷贝dtb文件到开发板上
# scp arch/arm/boot/dts/nuc980-dev-v1.0.dtb root@192.168.31.142:/mnt/
# 拷贝完成后重启开发板即可测试
# reboot
可以在 /sys/firmware/devicetree/base/
或者 /proc/device-tree/
目录下看到被内核解析后设设备树节点信息:
驱动程序与测试
驱动文件 char_drv.c 内容如下:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/of_device.h>
static int major = 0;
static const char *char_drv_name = "char_drv";
static struct class *char_drv_class;
static struct device *char_drv_device;
/* 探测到资源操作 */
static int char_probe(struct platform_device *pdev)
{
const char *tmp_str;
u64 tmp_num;
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
of_property_read_string(pdev->dev.of_node, "str", &tmp_str); // 读取字符串数据
of_property_read_u64(pdev->dev.of_node, "num", &tmp_num); // 读取u64内容
printk("NX modlog: %s %llu\n", tmp_str, tmp_num);
return 0;
}
/* 移除资源操作 */
static int char_remove(struct platform_device *pdev)
{
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
static const struct of_device_id dts_device_ids[] = {
{ .compatible = "nx_dts_node", } // 通过设备树 compatible 字段进行匹配
};
/* 定义platform_driver,用于探测和获取资源等 */
static struct platform_driver char_driver = {
.probe = char_probe,
.remove = char_remove,
.driver = {
.name = "naisu_char_dev", // 没有该字段启动时会崩溃
.of_match_table = dts_device_ids,
},
};
/* 驱动文件操作接口集合 */
static const struct file_operations char_drv_fops = {
.owner = THIS_MODULE,
};
/* 模块加载操作 */
static int __init char_drv_init(void)
{
int err;
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(0, char_drv_name, &char_drv_fops); // 注册字符设备,第一个参数0表示让内核自动分配主设备号
char_drv_class = class_create(THIS_MODULE, "char_drv_class");
if (IS_ERR(char_drv_class))
{
unregister_chrdev(major, char_drv_name);
return -1;
}
char_drv_device = device_create(char_drv_class, NULL, MKDEV(major, 0), NULL, char_drv_name); // 创建设备节点创建设备节点,成功后就会出现/dev/char_drv_name的设备文件
if (IS_ERR(char_drv_device))
{
device_destroy(char_drv_class, MKDEV(major, 0));
unregister_chrdev(major, char_drv_name);
return -1;
}
err = platform_driver_register(&char_driver); // 注册platform_driver
return err;
}
/* 模块退出操作 */
static void __exit char_drv_exit(void)
{
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
platform_driver_unregister(&char_driver); // 释放platform_driver
device_destroy(char_drv_class, MKDEV(major, 0)); // 销毁设备节点,销毁后/dev/下设备节点文件就会删除
class_destroy(char_drv_class);
unregister_chrdev(major, char_drv_name); // 注销字符设备
}
module_init(char_drv_init); // 模块入口
module_exit(char_drv_exit); // 模块出口
MODULE_LICENSE("GPL"); // 模块许可
修改完成后编译然后拷贝到开发板上进行测试:
# 设置编译工具链
# export ARCH=arm; export CROSS_COMPILE=arm-buildroot-linux-gnueabi-
# export PATH=$PATH:/home/nx/nuc980-sdk/buildroot-2023.02/output/host/bin
# 编译生成内核镜像
make uImage
# 可以根据电脑配置使用make -jx等加快编译速度
# 编译完成后拷贝到电脑上再拷贝到SD卡中
# sudo cp arch/arm/boot/uImage /media/sf_common/
# 我这里开发环境和开发板在同一局域网中,所以可以直接通过网络将uImage文件拷贝到开发板上
# 在开发板中挂载boot分区
# mount /dev/mmcblk0p1 /mnt/
# 在ubuntu中使用scp命令拷贝dtb文件到开发板上
# scp arch/arm/boot/uImage root@192.168.31.142:/mnt/
# 拷贝完成后重启开发板即可测试
# reboot
总结
这篇文章简单试了下通过设备树创建资源节点,然后在驱动程序中获取这些节点的数据。实时上关于设备树以及设备树下驱动程序资源类型和资源获取还有很多细节的内容和操作函数,这些内容更多的可以通过使用过程中参考各种已有的驱动代码来了解,这里就不进行展开了。
设备树文件内容
/*
* Device Tree Source for NUC980 DEV board
*
* Copyright (C) 2018 Nuvoton Technology Corp.
*
* The code contained herein is licensed under the GNU General Public
* License. You may obtain a copy of the GNU General Public License
* Version 2 or later at the following locations:
*
* http://www.opensource.org/licenses/gpl-license.html
* http://www.gnu.org/copyleft/gpl.html
*/
/dts-v1/;
#include "nuc980.dtsi"
/ {
model = "Nuvoton NUC980 DEV V1.0";
compatible = "nuvoton,nuc980-dev-v1.0", "nuvoton,nuc980";
chosen {
bootargs = "console=ttyS0,115200n8 noinitrd rootfstype=ext4 root=/dev/mmcblk0p2 rw rootwait mem=64M";
};
apb {
uart1: serial@b0071000 {
status = "disabled";
};
uart2: serial@b0072000 {
status = "disabled";
};
uart3: serial@b0073000 {
status = "disabled";
};
uart4: serial@b0074000 {
status = "disabled";
};
uart5: serial@b0075000 {
status = "disabled";
};
uart6: serial@b0076000 {
status = "disabled";
};
uart7: serial@b0077000 {
status = "disabled";
};
uart8: serial@b0078000 {
status = "disabled";
};
uart9: serial@b0079000 {
status = "disabled";
};
can0: can@b00a0000 {
status = "disabled";
};
can1: can@b00a1000 {
status = "disabled";
};
rtc: rtc@b0041000 {
status = "disabled";
};
gpio: gpio@b0004000 {
pinctrl-0 = <>;
eint2-config = <0 0 0>;
eint3-config = <0 0 0>;
};
nadc: nadc@b0043000 {
status = "disabled";
};
pwm0: pwm@b0058000 {
status = "disabled";
};
pwm1: pwm@b0059000 {
status = "disabled";
};
etimer0: etimer0@b0050000 {
status = "disabled";
};
etimer1: etimer1@b0050100 {
status = "disabled";
};
etimer2: etimer2@b0051000 {
status = "disabled";
};
etimer3: etimer3@b0051100 {
status = "disabled";
};
i2c0: i2c0@b0080000 {
status = "disabled";
};
i2c1: i2c1@b0081000 {
status = "disabled";
pinctrl-0 = <&pinctrl_i2c1_PB>;
};
i2c2: i2c2@b0082000 {
status = "disabled";
pinctrl-0 = <&pinctrl_i2c2_PB>;
};
};
ahb {
usbh_ehci@b0015000 {
pinctrl-0 = <>; /*disable PWREN and OVC*/
ov_active = <1>;/*disable PWREN and OVC*/
status = "okay";
};
usbh_ohci@b0017000{
status = "okay";
};
usbdev@b0016000 {
status = "okay";
};
fmi@b0019000 {
status = "disabled";
};
sdh@b0018000 {
status = "okay";
};
emac0@b0012000 {
status = "okay";
};
emac1@b0022000 {
status = "disabled";
};
ccap0@b0024000 {
status = "disabled";
};
i2c_gpio0: i2c-gpio-0 {
status = "disabled";
};
ccap1@b0014000 {
status = "disabled";
};
i2c_gpio1: i2c-gpio-1 {
status = "disabled";
};
dma@b0008000 {
status = "okay";
};
i2s: i2s@b0020000 {
status = "disabled";
};
i2s_pcm: i2s_pcm {
status = "disabled";
};
sound {
compatible = "nuvoton,nuc980-audio";
i2s-controller = <&i2s>;
i2s-platform = <&i2s_pcm>;
status = "disabled";
};
ebi: ebi@b0010000 {
status = "disabled";
};
};
nx_node@0 {
compatible = "nx_dts_node";
str = "Naisu 233!";
num = <0x00000000 0x00000020>;
};
nx_node@1 {
compatible = "nx_dts_node";
str = "Hello Naisu!";
num = <0x000000 0x00000040>;
};
};