嵌入式Linux嵌入式Linux驱动开发:板级DTS实操与完整实战演练——从修改设备树到点亮LED的完整闭环

嵌入式Linux嵌入式Linux驱动开发:板级DTS实操与完整实战演练------从修改设备树到点亮LED的完整闭环

仓库已经开源!所有教程,主线内核移植,跑新版本imx-linux/uboot都在这里,或者一起来尝试跑7.0的Linux!欢迎各位大佬观摩!喜欢的话点个⭐!

仓库地址:https://github.com/Awesome-Embedded-Learning-Studio/imx-forge
静态网页:https://awesome-embedded-learning-studio.github.io/imx-forge/

前言:从理论到实践的跨越

跟着教程一路走过来,你现在应该对设备树有了相当全面的了解:知道它是什么、语法怎么写、OF API 怎么用。上一章我们也完成了从硬编码驱动到设备树驱动的改造。但说实话,这些知识如果不动手,永远只是"纸上谈兵"。

很多朋友在这个阶段会遇到一个尴尬的问题:教程里的示例都看懂了,但面对自己手里的开发板,却不知道从哪里下手。厂商给的设备树文件一大堆,动辄几千行,看着就头皮发麻。这一章,我们要把之前学的所有知识串起来,从空白开始走完整个流程:确认板级 DTS 位置、编写 DTS 文件、编译 DTB、编写驱动代码、编译驱动模块、部署到板子、加载驱动测试。这六步走完,你就真正掌握了设备树驱动开发的完整闭环。

我们的实验目标是点亮 LED。这个实验足够简单,不会让你在硬件调试上浪费太多时间,同时又涵盖了设备树驱动的所有核心要素------描述硬件,而不是编码硬件,这就是现代驱动开发的分水岭。


环境准备:先搞清楚我们在改什么

在动手之前,最重要的一步是搞清楚"我们要改什么"。

确认开发板型号

首先确认你手里的开发板型号。最可靠的方法是看串口启动信息,你会看到类似这样的输出:

复制代码
Model: Freescale i.MX6 UltraLite 14x14 EVK Board

或者:

复制代码
Machine: ALIENTEK ATK-IMX6ULL

找到板级 DTS 文件

确认板子型号之后,找到对应的 DTS 文件。在我们的 imx-forge 项目中,设备树文件存放在 driver/device_tree/alpha-board/ 目录下。如果使用 NXP 官方 BSP,通常在内核源码的 arch/arm/boot/dts/ 目录下。

典型的设备树目录结构:

复制代码
arch/arm/boot/dts/
├── imx6ull.dtsi              # SOC 级通用定义(类似 C 语言头文件)
├── imx6ull-14x14-evk.dts     # 官方 EVK 板级文件
├── imx6ull-14x14-evk.dtb     # 编译后的二进制文件
└── ...

三种文件的区别:

  • .dtsi:SOC 级或模块级的通用定义,可被多个 .dts 引用
  • .dts:具体的板级定义,包含这块板子特有的硬件配置
  • .dtb:编译后的二进制设备树,内核真正读取的文件

重要原则:永远不要直接修改 .dtsi 文件! 这些文件是公用的,修改会影响所有引用它的板子。正确做法是在你的 .dts 文件里通过引用标签来修改或追加内容。

备份原有文件

修改之前养成备份的习惯:

bash 复制代码
# 备份原始文件
cp imx6ull-aes-led.dts imx6ull-aes-led.dts.bak

# 或者用 git
git stash save "修改前的备份"
git checkout -b experiment/device-tree-modification

步骤1:编写 DTS 设备树文件

确定寄存器地址

根据 IMX6ULL 芯片手册,控制 LED 需要操作以下寄存器:

寄存器 地址 用途
CCM_CCGR1 0x020C406C 时钟使能
SW_MUX_GPIO1_IO03 0x020E0068 GPIO 复用
SW_PAD_GPIO1_IO03 0x020E02F4 GPIO 电气属性
GPIO1_DR 0x0209C000 GPIO 数据
GPIO1_GDIR 0x0209C004 GPIO 方向

编写设备节点

打开设备树文件(在我们的项目中位于 driver/device_tree/alpha-board/device_tree_try_03/imx6ull-aes-led.dts),在根节点 / 下添加 LED 节点:

dts 复制代码
/dts-v1/;
#include "imx6ull.dtsi"
#include "imx6ull-aes.dtsi"

/ {
    model = "Awesome Embedded Studio IMX6ULL Example Driver";
    compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";

    imx_aes_led {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "atkalpha-led";
        status = "okay";
        reg = < 0X020C406C 0X04    /* CCM_CCGR1_BASE */
                0X020E0068 0X04    /* SW_MUX_GPIO1_IO03_BASE */
                0X020E02F4 0X04    /* SW_PAD_GPIO1_IO03_BASE */
                0X0209C000 0X04    /* GPIO1_DR_BASE */
                0X0209C004 0X04 >; /* GPIO1_GDIR_BASE */
    };
};

逐行解析关键部分:

  • 节点名 imx_aes_led :直接挂在根节点下,完整路径是 /imx_aes_led,驱动代码通过这个路径查找节点
  • #address-cells = <1>:子节点 reg 属性中,地址占 1 个单元格(32 位)
  • #size-cells = <1>:子节点 reg 属性中,长度占 1 个单元格(32 位)
  • compatible = "atkalpha-led":驱动匹配标识,平台设备驱动中会用它来自动匹配
  • status = "okay" :设备启用;如果设为 "disabled",内核会跳过这个设备
  • reg 属性:寄存器地址列表,格式为"地址1 长度1 地址2 长度2...",5 个寄存器共 10 个 u32 值

新手易踩的坑 :reg 里的数字必须严格遵循 #address-cells#size-cells 定义的格式。我们定义了都是 1,所以格式就是地址、长度、地址、长度......以此类推。

节点位置选择

你可能会问:为什么把这个节点直接放在根节点下面?答案取决于设备连接方式:

  • 设备挂在 SOC 内部总线上(比如直接操作 GPIO 寄存器)→ 放在根节点下
  • 设备挂在外部总线上(I2C、SPI)→ 放在对应的总线节点下

如果需要修改现有节点而不是添加新节点,可以通过 &label 引用:

dts 复制代码
&i2c1 {
    clock-frequency = <100000>;
    status = "okay";  /* 覆盖原来的 "disabled" */

    mag3110@0e {
        compatible = "fsl,mag3110";
        reg = <0x0e>;
    };
};

语法检查

编译之前先检查语法:

bash 复制代码
dtc -I dts -O dtb -o /dev/null imx6ull-aes-led.dts

没有报错就说明语法基本正确。


步骤2:编译 DTB

内核只能读取二进制的 DTB 文件,不能直接读取 DTS 文本文件。

使用构建脚本

在 imx-forge 项目中最简单的方式:

bash 复制代码
cd /home/charliechen/imx-forge
./scripts/driver_helper/build_driver.sh device_tree_try_03 alpha-board

脚本会自动完成:查找源码和设备树文件 → 编译驱动生成 .ko → 编译设备树生成 .dtb → 产物放到 out/driver_artifacts/device_tree_try_03/alpha-board/

手动编译

bash 复制代码
# 基本编译
dtc -I dts -O dtb -o imx6ull-aes-led.dtb imx6ull-aes-led.dts

# 带 include 路径(DTS 引用了其他文件时)
dtc -I dts -O dtb -i driver/device_tree/alpha-board/ \
    -o imx6ull-aes-led.dtb \
    driver/device_tree/alpha-board/device_tree_try_03/imx6ull-aes-led.dts

反编译验证

把编译出来的 DTB 反编译回 DTS 格式,对比确认正确性:

bash 复制代码
dtc -I dtb -O dts -o test_from_dtb.dts imx6ull-aes-led.dtb
grep -A 10 "imx_aes_led" test_from_dtb.dts

反编译的 DTS 在格式上可能和原始 DTS 有差异,但节点结构和属性值应该一致。


步骤3:编写设备树驱动代码

设备树准备好了,现在编写驱动。核心逻辑:去设备树里把刚才填的值"抠"出来,然后操作寄存器控制 LED

我们的驱动代码分为两个文件:led_hw.c 负责硬件操作,device_tree_try_03_driver_main.c 负责字符设备框架。这种分离让代码结构更清晰,也便于以后复用。

头文件与结构体

c 复制代码
#include <linux/of.h>
#include <linux/of_address.h>

struct led_handle {
    void __iomem* ccm_ccgr1;
    void __iomem* sw_mux_gpio;
    void __iomem* sw_pad_gpio;
    void __iomem* gpio_dr;
    void __iomem* gpio_gdir;
    struct device_node* device_tree_node;
};

static struct led_handle led;

linux/of.h 提供设备树基础 API(of_find_node_by_pathof_property_read_string 等),linux/of_address.h 提供地址映射 API(of_iomap)。结构体最后一个成员 device_tree_node 保存设备树节点指针,后续所有操作都靠它。

硬件初始化:从设备树获取信息

真正的重头戏在 led_hw_init 里------数据从 DTS 流向内核内存,再流向驱动变量:

c 复制代码
int led_hw_init(void) {
    u32 regdata[10];
    int ret;
    const char* str;
    struct property* proper;
    u32 val;

    /* 1. 获取设备节点 */
    led.device_tree_node = of_find_node_by_path("/imx_aes_led");
    if (led.device_tree_node == NULL) {
        pr_err("dtsled node can not found!\n");
        return -EINVAL;
    }
    pr_info("dtsled node has been found!\n");

第一步:找人。 of_find_node_by_path 就像在电话簿里按名字找人。路径必须和 DTS 里写的一样(包括根节点 /)。如果返回 NULL,说明设备树里没这个节点或者路径写错了。

注意:of_find_node_by_path 成功时会增加节点引用计数,用完后必须调用 of_node_put 释放,否则会内存泄漏。

c 复制代码
/* 2. 获取 compatible 属性(验证性读取) */
    proper = of_find_property(led.device_tree_node, "compatible", NULL);
    if (proper == NULL) {
        pr_err("compatible property find failed\n");
    } else {
        pr_info("compatible = %s\n", (char*)proper->value);
    }

第二步:查户口。 of_find_property 找具体的属性,proper->value 指向属性值的原始数据。对于字符串属性,可以直接转成 char* 打印。这里主要是验证性读取------我们通过路径查找节点而非通过 compatible 匹配,所以失败也不中断。

c 复制代码
/* 3. 获取 status 属性 */
    ret = of_property_read_string(led.device_tree_node, "status", &str);
    if (ret < 0) {
        pr_err("status read failed!\n");
    } else {
        pr_info("status = %s\n", str);
    }

第三步:看状态。 of_property_read_string 专读字符串属性。标准值是 "okay"(可用)和 "disabled"(禁用)。第三个参数是 const char**,函数会把字符串地址存到这个指针指向的位置,不需要调用者手动释放内存。

c 复制代码
/* 4. 获取 reg 属性(关键) */
    ret = of_property_read_u32_array(led.device_tree_node, "reg", regdata, 10);
    if (ret < 0) {
        pr_err("reg property read failed!\n");
        of_node_put(led.device_tree_node);
        return -EINVAL;
    }

    pr_info("reg data:\n");
    for (int i = 0; i < 10; i++) {
        pr_cont("%#X ", regdata[i]);
    }
    pr_cont("\n");

第四步:拿地址。 DTS 里 5 组寄存器,每组"地址+长度"两个值,共 10 个 u32。执行完这一句,regdata 数组里就存满了我们在 DTS 里写的那串十六进制数字。

注意:这里读取失败时我们手动调用了 of_node_put 释放节点引用,因为直接 return 了,不会走到后面的统一清理逻辑。这个细节很容易遗忘,但忘记释放会导致内存泄漏。

内存映射:使用 of_iomap

拿到了物理地址,下一步是映射。of_iomap 可以直接从 reg 属性中取第 N 组地址进行映射:

c 复制代码
/* 5. 使用 of_iomap 进行寄存器地址映射 */
    led.ccm_ccgr1 = of_iomap(led.device_tree_node, 0);
    led.sw_mux_gpio = of_iomap(led.device_tree_node, 1);
    led.sw_pad_gpio = of_iomap(led.device_tree_node, 2);
    led.gpio_dr = of_iomap(led.device_tree_node, 3);
    led.gpio_gdir = of_iomap(led.device_tree_node, 4);

    if (!led.ccm_ccgr1 || !led.sw_mux_gpio || !led.sw_pad_gpio ||
        !led.gpio_dr || !led.gpio_gdir) {
        pr_err("ioremap failed!\n");
        of_node_put(led.device_tree_node);
        return -ENOMEM;
    }

of_iomap(node, index) 的设计非常巧妙:驱动代码不需要知道具体的地址值,只需要知道"我需要第几个寄存器"。索引 0 对应 reg 属性第一组 0X020C406C 0X04,索引 1 对应第二组,以此类推。具体的地址是什么,那是设备树的事情。 换一块板子,只需要改设备树文件,驱动代码完全不用动。

of_iomap 内部会调用 ioremap,失败时返回 NULL。它还会自动处理 reg 属性中的地址转换(某些架构上设备树地址可能不是直接的物理地址)。

配置寄存器

映射完成后,操作寄存器和硬编码版本完全一样:

c 复制代码
/* 6. 使能 GPIO1 时钟 */
    val = readl(led.ccm_ccgr1);
    val &= ~(3 << 26);
    val |= (3 << 26);
    writel(val, led.ccm_ccgr1);

    /* 7. 设置 GPIO1_IO03 复用为 GPIO */
    writel(5, led.sw_mux_gpio);

    /* 8. 设置 GPIO1_IO03 电气属性 */
    writel(0x10B0, led.sw_pad_gpio);

    /* 9. 设置 GPIO1_IO03 为输出 */
    val = readl(led.gpio_gdir);
    val |= (1 << 3);
    writel(val, led.gpio_gdir);

    /* 10. 默认关闭 LED(高电平) */
    val = readl(led.gpio_dr);
    val |= (1 << 3);
    writel(val, led.gpio_dr);

    pr_info("LED Init OK!\n");
    return 0;
}

无论地址来自硬编码还是设备树,硬件寄存器的操作方式不变。这正是"配置与代码分离"的好处。

LED 控制与资源清理

c 复制代码
void led_set_status(bool status) {
    u32 val = readl(led.gpio_dr);
    if (status) {
        val &= ~(1 << 3); /* 低电平点亮 */
    } else {
        val |= (1 << 3);  /* 高电平熄灭 */
    }
    writel(val, led.gpio_dr);
}

bool led_get_status(void) {
    u32 val = readl(led.gpio_dr);
    return (val & (1 << 3)) == 0;
}

void led_hw_deinit(void) {
    if (led.ccm_ccgr1) { iounmap(led.ccm_ccgr1); led.ccm_ccgr1 = NULL; }
    if (led.sw_mux_gpio) { iounmap(led.sw_mux_gpio); led.sw_mux_gpio = NULL; }
    if (led.sw_pad_gpio) { iounmap(led.sw_pad_gpio); led.sw_pad_gpio = NULL; }
    if (led.gpio_dr) { iounmap(led.gpio_dr); led.gpio_dr = NULL; }
    if (led.gpio_gdir) { iounmap(led.gpio_gdir); led.gpio_gdir = NULL; }

    if (led.device_tree_node) {
        of_node_put(led.device_tree_node);
        led.device_tree_node = NULL;
    }
}

资源清理的几个要点:

  1. 检查 NULL 再释放 --- 防止 led_hw_deinit 被调用两次时的双重释放
  2. 释放后置 NULL --- 防止悬空指针,错误使用时至少触发空指针异常而非内存破坏
  3. 最后调用 of_node_put --- 释放设备树节点引用,这是设备树 API 的要求

字符设备框架代码(file_operations、cdev、class、device 等)和传统驱动完全一样,此处不再赘述,完整代码请参考项目中的 device_tree_try_03_driver_main.c


步骤4:编译驱动模块

驱动代码写好了,编译生成 .ko 文件。

使用构建脚本

bash 复制代码
cd /home/charliechen/imx-forge
./scripts/driver_helper/build_driver.sh device_tree_try_03 alpha-board

脚本会自动处理:设置交叉编译工具链 → 指定内核源码路径 → 调用 make 编译 → 产物放到输出目录。

手动编译

bash 复制代码
cd driver/device_tree_try_03/alpha-board
make

Makefile 大致如下:

makefile 复制代码
obj-m := device_tree_try_03_driver.o
device_tree_try_03_driver-y := device_tree_try_03_driver_main.o led_hw.o

ARCH := arm
CROSS_COMPILE := arm-none-linux-gnueabihf-
KDIR := $(PROJECT_ROOT)/third_party/linux-${KERNEL_TYPE}

modules:
	$(MAKE) -C $(KDIR) M=$(CURDIR) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules

注意驱动由两个 .o 文件组成,链接成一个 .ko 模块。

检查编译结果

bash 复制代码
ls -lh out/driver_artifacts/device_tree_try_03/alpha-board/
# 预期产物:
# device_tree_try_03_driver.ko    (约14K)
# imx6ull-aes-led.dtb             (约35K)

modinfo device_tree_try_03_driver.ko
# 查看 vermagic 确认内核版本匹配

如果 vermagic 显示的内核版本和板子运行的内核不一致,模块加载会因版本不匹配而失败。


步骤5:部署到板子

编译好了 .ko.dtb,部署到开发板上。

部署 DTB 文件(TFTP)

bash 复制代码
# 使用部署脚本
./scripts/driver_helper/deploy_driver.sh device_tree_try_03 alpha-board --target=tftp

# 或手动拷贝
sudo cp out/driver_artifacts/device_tree_try_03/alpha-board/imx6ull-aes-led.dtb \
    /srv/tftp/imx6ull-aes.dtb

注意目标文件名是 imx6ull-aes.dtb,不是 imx6ull-aes-led.dtb。U-Boot 启动时加载的 DTB 文件名由环境变量 fdt_file 决定:

复制代码
=> printenv fdt_file
fdt_file=imx6ull-aes.dtb

如果不确定自己的板子用哪个文件名,在 U-Boot 命令行输入 printenv 查看。

部署 KO 文件

bash 复制代码
# 通过 NFS
./scripts/driver_helper/deploy_driver.sh device_tree_try_03 alpha-board --target=nfs

# 或通过 scp
scp device_tree_try_03_driver.ko root@192.168.1.100:/lib/modules/

重启并验证设备树

部署 DTB 后重启板子,验证设备树是否正确加载:

bash 复制代码
# 查看节点是否存在
ls /proc/device-tree/imx_aes_led/

# 查看属性
cat /proc/device-tree/imx_aes_led/compatible
# 输出:atkalpha-led

cat /proc/device-tree/imx_aes_led/status
# 输出:okay

hexdump -C /proc/device-tree/imx_aes_led/reg
# 输出寄存器地址列表

如果节点不存在,说明 DTB 没有正确加载或者节点路径写错了。如果属性值不对,说明 DTS 里的属性定义有问题。


步骤6:加载驱动测试

终于到了见证结果的时刻。

加载驱动

bash 复制代码
insmod device_tree_try_03_driver.ko

# 查看内核日志
dmesg | tail -30

预期日志:

复制代码
[   12.345678] dtsled node has been found!
[   12.345679] compatible = atkalpha-led
[   12.345680] status = okay
[   12.345681] reg data:
[   12.345682] 0X20C406C 0X4 0X20E0068 0X4 0X20E02F4 0X4 0X209C000 0X4 0X209C004 0X4
[   12.345683] LED Init OK!
[   12.345684] LED handle get the device number: major: 245, minor: 0

这串日志意味着:内核找到了 imx_aes_led 节点、成功读取了属性、reg data 和 DTS 文件中写的完全一致、寄存器映射成功、字符设备创建成功。

测试 LED 控制

bash 复制代码
# 检查设备文件
ls -l /dev/AES_LED

# 点亮 LED(低电平有效)
echo 1 > /dev/AES_LED

# 熄灭 LED
echo 0 > /dev/AES_LED

# 读取 LED 状态
cat /dev/AES_LED

看板子上的 LED,亮了吗?如果亮了,恭喜------你完成了完整的设备树驱动开发!

卸载驱动

bash 复制代码
rmmod device_tree_try_03_driver
dmesg | tail
# [  234.567890] Deinit LED Hardware

调试技巧与常见问题

即使严格按教程操作,也可能会遇到各种问题。这里总结最常见的坑点。

dmesg 日志分析

dmesg 是你最好的调试朋友:

bash 复制代码
dmesg | tail -20           # 最近的消息
dmesg | grep -i "led"      # 过滤关键词
dmesg -w                   # 实时监控

常见错误排查

dtsled node can not found! --- 节点找不到

  • 检查节点路径(注意大小写和前导 /
  • 检查 DTB 是否正确部署
  • /proc/device-tree 中查看节点是否存在

reg property read failed! --- reg 属性读取失败

  • 检查 DTS 中 reg 属性是否存在
  • 确认格式正确(地址和长度成对出现)
  • hexdump -C /proc/device-tree/imx_aes_led/reg 查看实际值

ioremap failed! --- 内存映射失败

  • reg 属性中的地址可能无效
  • of_iomap 的索引可能超出 reg 属性中定义的组数
  • 检查是否已有其他驱动占用了这些地址

编译通过但内核报 "Duplicate node" --- 节点重复

  • 你在 .dts 里定义了一个 .dtsi 里已存在的节点
  • 改用 &label 引用现有节点,或用 /delete-node/ 删除

DTB 部署后不生效 --- 加载的不是你的 DTB

  • U-Boot 中执行 printenv fdt_file 确认加载的文件名
  • 检查 TFTP 目录下是否有旧文件
  • 手动在 U-Boot 里加载:tftp 0x83000000 imx6ull-aes.dtb; bootm 0x80800000 - 0x83000000

DTC 编译报错 "FDT_ERR_BADSTRUCTURE" --- 编译失败但信息模糊

  • 检查所有 #include 指令,确保文件存在
  • -i 选项指定 include 路径
  • 单独编译被 include 的 .dtsi 文件排查

系统排查清单

按以下顺序逐一排查:

  1. 设备树是否加载? --- /proc/device-tree 中有无对应节点
  2. 驱动是否加载? --- lsmod 中有无模块,dmesg 有无报错
  3. 设备文件是否创建? --- /dev 目录下有无设备文件,权限是否正确
  4. 硬件是否正常? --- LED 连接的 GPIO 是否正确,用万用表测量电平
  5. 驱动逻辑是否正确? --- 添加更多调试打印,或用 strace 跟踪用户态调用

总结

这一章我们把所有知识串起来,走完了从修改设备树到点亮 LED 的完整闭环。回顾一下:

  • 环境准备:确认板子型号、找到 DTS 文件、备份
  • 编写 DTS:在设备树中"描述"硬件,而不是在代码中"编码"硬件
  • 编译 DTB:用构建脚本或手动 DTC 编译
  • 编写驱动 :通过 OF API 从设备树获取硬件信息,用 of_iomap 映射寄存器
  • 编译部署 :交叉编译 .ko 模块,通过 TFTP/NFS 部署到板子
  • 测试验证:加载驱动、测试功能、分析日志

核心思想只有一个:把"硬件描述"和"驱动逻辑"彻底解耦 。驱动只负责"怎么操作一个 GPIO",设备树负责"这个 GPIO 在哪里"。以前移植驱动需要改代码重新编译 .ko,现在只需改 .dts 重新编译 DTB(几秒钟的事),驱动代码连动都不用动

这就是 Linux 内核引入设备树模型的初衷,也是构建可移植嵌入式系统的基石。

当然,我们展示的还是最基础的使用方式。在实际工程中,你还会遇到中断、DMA、时钟、电源管理......这些都可以通过设备树描述,但万变不离其宗。在未来的学习中,你会接触到 Platform 设备驱动框架,届时你会发现今天折腾的 device_node 和 OF 函数,在 Platform 驱动里以更标准、更优雅的方式被封装起来。

现在,找一块开发板,把这一章的流程完整走一遍。只有在实践中,理论才能变成你自己的技能。


相关阅读

  1. 04. OF API 基础与验证------从 DTS 到代码的桥梁 - 相似度 100%
  2. 通用GUI编程技术------图形渲染实战(四十三)------D3D12设计哲学:显式控制与性能解锁 - 相似度 82%
相关推荐
MAVER1CK7 小时前
Docker容器创建好后修改容器配置
运维·docker·容器
匆匆那年9679 小时前
VSCode 远程 Linux 使用Codex
linux·ide·vscode
NightReader10 小时前
CPU 高使用率,怎么降下来
运维·服务器
SWAGGY..10 小时前
Linux系统编程:(七)Makefile入门:轻松掌握编译自动化
linux·运维·自动化
开开心心就好11 小时前
免费流畅的远程控制实用工具
linux·运维·服务器·网络·智能手机·excel
黑猫学长呀12 小时前
存储宝典第2篇:盲封TT wafer是什么意思?
linux·嵌入式硬件·项目·芯片·ufs·晶圆·产测
Strugglingler13 小时前
【Linux 用户态操作 UART】
linux·uart
代码熬夜敲Q13 小时前
ENSP 网络工程实验
linux·运维·服务器
銳昊城13 小时前
项目七: 配置与管理Web服务器(2) C2
运维·服务器