驱动开发日记

系统性的学习文章已经很多了,此处仅简单记录我的学习过程中的部分感悟。(想到哪里算哪,顺序有点乱)
开悟!
一、MCU开发与Linux开发的差异
从MCU开发转入到Linux开发,首先需要理解我们为什么需要使用Linux。

FreeRTOS 是为 “资源极有限的单片机” 设计的实时操作系统(RTOS),Linux 是为 “通用场景” 设计的通用操作系统(GPOS),两者都是操作系统,前者更加重视实时性和简单的多任务切换,目的是实现在低性能轻量化的MCU的工业场景应用。而linux目标是 “支持复杂硬件和多样化应用”,核心诉求是通用性、兼容性、功能丰富度(比如跑 Python、音频驱动、多进程并发,实时性不是首要目标)。Linux可以实现多线程的并发,切换开销大(毫秒级),支持复杂逻辑(如 “线程 1 运行 ASR 模型,线程 2 处理音频播放,线程 3 响应用户输入”),且有内存隔离(一个线程崩溃不影响整个系统)。(可以简单理解为Linux是RTOS的上位替代吧,当然是在对实时性要求不严格的情况下)
内核态和用户态的简单理解,内核态就是可以直接访问寄存器和CPU等硬件设备,用户态就是只能调用linux给出的标准系统接口。
裸机开发的话是没有操作系统的,不可以简单理解为内核态(两者就不该放一起讨论,就当我闲得慌吧),就是我们常用的MCU开发
内核态和用户态的划分,是操作系统为了保护系统稳定性而设计的:
内核态:拥有最高特权,可直接访问物理内存、硬件寄存器、CPU 特权指令(如中断控制),内核、驱动都运行在这个状态。
用户态:特权受限,只能访问 OS 分配的虚拟内存,无法直接操作硬件,应用程序运行在这个状态。
裸机编程的核心是 “无 OS 直接操作硬件”:
整个程序是系统中唯一的执行代码,没有 OS 进行特权管控,自然不存在 “态” 的划分。但它能直接读写硬件物理地址(如寄存器地址)、控制中断、操作 GPIO/I2C 等,这种 “不受限制的硬件访问权限”,和 OS 中内核态的特权完全一致。
linux的软硬是真的分离啊,驱动干驱动,程序开发干程序开发,分层隔离、统一接口,不跟MCU开发一样全栈随便干
linux开发不能直接操作寄存器,而是操作映射过来的虚拟内存,对于带MMU的处理器,我们必须使用虚拟内存进行外设的开发,此前我们开发mcu时都是直接对IIC,SPI的硬件寄存器地址进行直接操作,但是在linux中cpu只能访问虚拟地址,并通过给出的标准读写寄存器函数对寄存器内容进行读写(因为内核虚拟地址空间有严格的分区(如内核代码段、用户空间、IO 映射区),直接使用物理地址可能冲突(如物理地址0x1000可能对应内核代码区的虚拟地址)。简单来说就是防止内存地址冲突)

二、对驱动的整体理解

首先需要明确驱动是位于内核之中的,驱动程序通过提供标准的函数接口给上层的应用程序开发使用,下面两个图就能完全说明驱动的调用全过程,大佬讲的很清楚
image
image

既然驱动位于内核之中,那么我们是不是每次运行都需要重新编译内核?答案肯定是不需要,编译一次内核少说十几分钟多则几个小时,所以我们可以将驱动单独编译成.ko模块,通过linux内置的指令与内核进行连接,完成产品开发后再将驱动编译进内核即可

三、设备树到底是什么

之前一直觉得设备树是个挺难的东西,每次都是一知半解的,其实仔细梳理一下就会发现,就是个描述了接口配置的文件,类似cubemx的引脚接口配置,dtsi类似于设备头文件,dts是引脚描述(会覆盖dtsi配置),编译之后就是dtb,通过bootloader加载到内核即可
但是需要学习一下设备树的语法和操作函数,实际去看两个设备树文件的内容就知道到底是啥了,让豆包生成两个例子看看也能明白差不多(小白初学,描述有误,还望指正)

点击查看代码
// 1. 声明设备树版本(必须放在第一行)
/dts-v1/;

// 2. 引用RK3506芯片级通用设备树(存放CPU、GPIO控制器、I2C/UART总线等基础硬件信息)
#include "rk3506.dtsi"

// 3. 根节点(所有设备的父节点)
/ {
    // 开发板型号(用于内核识别板级信息)
    model = "RK3506 My Custom Board";
    // 兼容属性(内核匹配板级支持的标识)
    compatible = "rockchip,rk3506", "rockchip,rk3500";

    // 4. 电源节点(描述板载3.3V固定电源,供外设使用)
    vcc_3v3: regulator-3v3 {
        // 兼容属性(匹配内核中的固定电源驱动)
        compatible = "regulator-fixed";
        // 电源名称(用于其他节点引用)
        regulator-name = "vcc_3v3";
        // 最小供电电压(3.3V = 3300000微伏)
        regulator-min-microvolt = <3300000>;
        // 最大供电电压(固定3.3V)
        regulator-max-microvolt = <3300000>;
        // 始终使能(上电后持续供电)
        regulator-always-on;
    };

    // 5. GPIO LED节点(红色用户LED,挂在根节点下,直接受GPIO控制器控制)
    led_red: led@0 {
        // 兼容属性(匹配内核中的GPIO LED驱动)
        compatible = "gpio-leds";
        // LED标签(用于用户识别,如/sys/class/leds/red:user)
        label = "red:user";
        // GPIO引脚配置:GPIO2控制器的第5个引脚,高电平点亮
        gpios = <&gpio2 5 GPIO_ACTIVE_HIGH>;
        // 默认状态(上电关闭)
        default-state = "off";
        // LED触发方式(可选,这里设为“心跳”模式,用于指示系统运行)
        linux,default-trigger = "heartbeat";
    };
};

// 6. 扩展I2C2总线节点(引用芯片级设备树中已定义的i2c2节点,补充板级配置)
&i2c2 {
    // 使能I2C2总线(芯片级默认可能禁用)
    status = "okay";
    // 引脚配置名称(对应pinctrl节点中的配置)
    pinctrl-names = "default";
    // 绑定I2C2的引脚复用配置(&i2c2m1_xfer在rk3506.dtsi中定义,指定SDA/SCL引脚)
    pinctrl-0 = <&i2c2m1_xfer>;
    // I2C总线时钟频率(400kHz,高速模式)
    clock-frequency = <400000>;

    // 7. I2C总线上的AHT20温湿度传感器节点(子节点)
    aht20@48 {
        // 驱动匹配标识(必须与AHT20驱动的of_match_table一致)
        compatible = "alientek,aht20";
        // I2C从地址(AHT20默认0x48,硬件可修改)
        reg = <0x48>;
        // 中断父控制器(传感器中断引脚连接到GPIO3控制器)
        interrupt-parent = <&gpio3>;
        // 中断信息:GPIO3的第2个引脚,上升沿触发(数据就绪时触发中断)
        interrupts = <2 IRQ_TYPE_EDGE_RISING>;
        // 传感器供电(引用前面定义的3.3V电源节点)
        vcc-supply = <&vcc_3v3>;
        // 复位引脚(GPIO0的第10个引脚,低电平复位)
        reset-gpios = <&gpio0 10 GPIO_ACTIVE_LOW>;
    };
};

需要记住一些核心属性,以及部分语法,但是现在的话直接扔给AI也不是不行

四、驱动开发启程

绝大部分的外设基本厂家都做了配套的驱动,我们只需要简单include进来就可以使用了(比如我手上的这个TI的TIL320AIC3254芯片)。

参考学习资料:
https://blog.csdn.net/qq_44814825/article/details/129107911?ops_request_misc=elastic_search_misc&request_id=19bd21b1e99006af663cecd3f6f4571f&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_positive~default-1-129107911-null-null.142v102pc_search_result_base9&utm_term=linux%E9%A9%B1%E5%8A%A8%E5%BC%80%E5%8F%91&spm=1018.2226.3001.4187#t7