嵌入式Linux系统移植与驱动开发完整指南

一、系统移植完整流程

1、上电启动完整步骤

(1)芯片上电,BootROM(芯片内部固化 ROM 程序)执行

  • 将 u-boot.imx 搬移到 OCRAM(片上 RAM);
  • 初始化 DDR 内存;
  • 加载完整 U-Boot 程序。

(2)U-Boot 初始化硬件

  • 初始化时钟、初始化内存;
  • 设置 CPU 为 SVC 管理模式;
  • 关闭看门狗、关闭中断、关闭 D-Cache、关闭 MMU。

(3)板级硬件初始化:初始化串口、网口。

(4)软件初始化:初始化网络(TFTP 功能依赖)。

(5)网络加载镜像到 DDR 内存

复制代码
tftp 0x80800000 zImage
tftp 0x83000000 imx6ull-altientek-emmc.dtb
  • 0x80800000:内核 zImage 加载地址;
  • 0x83000000:设备树 dtb 加载地址。

(6)设置 bootargs 内核启动参数

复制代码
setenv bootargs = root=/dev/nfs nfsroot=/home/linux/nfs/rootfs serverip 192.168.1.100 ip 192.168.1.50 console=ttymxc0 init=/linuxrc

(7)U-Boot 引导 Linux 内核启动

  • 跳转到内核入口点;
  • 执行内核启动的第一个 C 语言函数 start_kernel()(位于 /init/main.c)。

bootargs 参数每一项含义:

参数 说明
root=/dev/nfs 根文件系统挂载方式:NFS 网络文件系统
nfsroot=xxx Ubuntu 主机上根文件系统的路径
serverip Ubuntu 主机 IP 地址
ip 开发板的 IP 地址(U-Boot 阶段)
console=ttymxc0 控制台串口设备,打印内核 log
init=/linuxrc 用户空间启动的第一个进程

start_kernel() 完成的内核初始化:

  • 初始化内核调度;
  • 初始化 MMU;
  • 开启中断;
  • 初始化控制台;
  • 初始化 init 进程。

2、内核镜像三种镜像区别

  • Image:原生 Linux 内核镜像,没有压缩,体积大,不便于网络 / 烧写移植。
  • zImage :Image + 自解压代码,压缩后的内核镜像,体积小,i.MX6ULL 常用。
  • uImage :zImage + 64字节头部信息;头部包含 CPU 架构、镜像下载地址、镜像大小;U-Boot 传统镜像格式。查看 uImage 头部信息命令:mkimage -l uImage

3、Linux 内核与根文件系统作用

(1)Linux 内核 zImage

  • 文件系统管理;
  • 多任务调度(时间片轮询调度);
  • 网络协议栈;
  • 内存管理;
  • 硬件设备管理。

(2)rootfs 根文件系统 :Linux 内核启动完成之后第一个挂载的文件系统,提供用户态运行环境。

  • 常用构建工具:busybox / buildroot;
  • 执行 init 进程:用户空间第一个进程。

二、Linux 驱动三大分类

  1. 字符设备驱动:字节流访问,led、beep、传感器、串口;
  2. 块设备驱动:按块读写,SD 卡、EMMC、NAND Flash;
  3. 网络设备驱动:网卡,收发数据包。

字符设备特点:insmod 加载 ko 之后,默认不会自动生成 /dev 设备节点 。两种方案:① 手动:mknod /dev/xxx c 主设备号 次设备号;② 代码内部使用 class_create + device_create 自动创建设备节点。

设备号组成

dev_t 32 位数据:

  • 高 12bit:主设备号 major;
  • 低 20bit:次设备号 minor。

三、普通字符设备驱动完整开发框架(cdev 框架)

1、驱动开发框架总览

  • 构建 cdev 结构体 :描述一个具体的字符型设备信息(设备号、设备名称、file_operations 结构体);
  • 构建 file_operations 结构体 :描述驱动层操作函数的集合(.open、.read、.write、.release);
  • 实现驱动入口函数 init (执行时机:insmod xxx.ko 自动调用);
  • 实现驱动注销函数 exit (执行时机:rmmod xxx 自动调用);
  • 通过 MODULE_INIT 和 MODULE_EXIT 宏绑定驱动的入口和注销函数;
  • 实现 file_operations 结构体中的 open、read、write、close 等驱动操作函数。

2、驱动入口函数 __init(insmod xxx.ko 自动调用)

(1)申请设备号

  • 动态申请:alloc_chrdev_region(&dev_no, 0, count, name)
  • 静态指定:register_chrdev_region(dev_no, count, name)

(2)cdev 初始化 + cdev 注册

复制代码
cdev_init(&cdev, &fops);    //绑定file_operations
cdev_add(&cdev, dev_no, 1); //向内核VFS注册字符设备

(3)【可选自动创建设备节点】

复制代码
class_create();
device_create();

(4)申请、初始化硬件资源

  • gpio_request() 申请 GPIO;
  • request_irq() 申请中断;
  • spin_lock_init() 自旋锁初始化。

3、驱动注销函数 __exit(rmmod xxx 自动调用)

注销顺序和 init 严格逆序

(1)注销设备号:unregister_chrdev_region(dev_no, count)

(3)删除 cdev:cdev_del(&cdev)

(3)【如果创建过 class/device】

复制代码
device_destroy();
class_destroy();

(4)释放硬件资源

  • gpio_free() 释放 GPIO;
  • free_irq() 释放中断;
  • spin_lock_destroy() 销毁自旋锁。

4、关键结构体

(1)struct cdev:描述一个字符设备,保存设备号、fops 指针。

(2)struct file_operations:驱动操作函数集合

  • .open 用户态 open 调用;
  • .read 用户态 read 调用;
  • .write 用户态 write 调用;
  • .release 用户态 close 调用。

5、模块宏

复制代码
module_init(xxx_init);   //绑定入口函数
module_exit(xxx_exit);   //绑定卸载函数
MODULE_LICENSE("GPL");

6、内核时间函数

  • time();
  • do_gettimeofday(); // 获取高精度时间,常用于超声波计算脉冲时间

四、Platform 平台驱动 + misc 混杂设备驱动框架

misc 混杂设备:属于字符设备,次设备号固定 10,简化字符设备开发。

1、platform 总线匹配机制

platform 总线是一种虚拟总线,用于连接设备(设备树节点)和驱动(platform_driver)。匹配过程:

  • 设备侧:设备树节点提供 compatible 属性;
  • 驱动侧:of_device_id 提供 .compatible 字符串;
  • 匹配函数:platform_match() 内部调用 strcmp() 进行字符串比较;
  • 匹配成功:自动调用驱动的 probe 函数。

2、platform_driver 结构体

  • .probe:设备树 compatible 匹配成功之后自动执行;做硬件资源获取、初始化。
  • .remove:设备和驱动解绑时自动执行;释放资源。
  • .of_match_table:数组,存放 of_device_id,用于和设备树 compatible 字符串匹配。

3、of_device_id 结构体

复制代码
static const struct of_device_id xxx_of_match[] = {
    { .compatible = "厂商,设备名" },
    { /*哨兵 */ }
};

compatible 字符串必须和设备树节点 compatible 完全一致,匹配才会调用 probe。

4、probe 函数内部典型执行步骤

  • misc_register() 注册混杂设备;
  • of_get_named_gpio():从设备树节点读取 GPIO 引脚编号;
  • devm_gpio_request():devm 托管申请 GPIO 资源(不需要手动 free,卸载自动释放);
  • 硬件外设初始化。

5、remove 函数典型步骤

  • misc_deregister() 注销混杂设备。

6、misc_device 结构体

描述混杂设备:次设备号、设备名字、file_operations。

7、模块注册宏

复制代码
module_platform_driver(xxx_driver);
//等价于
platform_driver_register(&xxx_driver);
platform_driver_unregister(&xxx_driver);

第五部分:设备树 DTS 关键知识点

1、设备树基本概念

设备树(Device Tree Source,DTS)用于描述硬件资源和设备信息,从 Linux 3.0 开始引入,替代了传统的平台设备硬编码方式。

2、pinctrl 引脚配置节点示例

复制代码
pinctrl_ds18b20: ds18b20grp {
    fsl,pins = <MX6ULL_PAD_GPIO1_IO03_GPIO1_IO03 0x10B0>;
};
  • MX6ULL_PAD_GPIO1_IO03_GPIO1_IO03:引脚复用配置;
  • 0x10B0:PAD 电气属性配置。

3、设备树头部属性

  • #address-cells = <1>;:子节点中寄存器地址占几个 cell(1cell=4 字节);
  • #size-cells = <1>;:子节点中寄存器长度占几个 cell。

作用:解析 reg 属性,获取物理寄存器地址。

4、设备树节点完整示例

复制代码
/ {
    ds18b20 {
        #address-cells = <1>;          // 寄存器地址长度
        #size-cells = <1>;             // 寄存器长度
        compatible = "dallas,ds18b20";  // 用于和驱动匹配
        pinctrl-name = "default";       // 引脚默认状态
        pinctrl-0 = <&pinctrl_ds18b20>; // 引脚复用 + 电气属性
        gpio-ds18b20 = <&gpio1 3 GPIO_ACTIVE_HIGH>; // 数据引脚
        status = "okay";                // 设备使能
    };
};
  • compatible:用于和驱动匹配的字符串;
  • pinctrl-0:引用 pinctrl 配置节点,设置引脚复用和电气属性;
  • gpio-ds18b20:自定义属性,描述数据引脚对应的 GPIO;
  • status = "okay":使能该设备节点。

5、DS18B20 温度传感器驱动开发要点

DS18B20 是一款常用的单总线数字温度传感器,在嵌入式 Linux 驱动开发中经常作为字符设备驱动的实战案例。下面梳理其驱动开发的关键知识点。

(1)传感器特性

  • 单总线协议:仅需一根数据线(DQ)即可完成通信;
  • 测量范围:-55℃ 到 +125℃;
  • 分辨率可配置:9~12 位,默认 12 位;
  • 供电方式:外部供电或寄生供电。

(2)驱动框架选择

  • 推荐使用 platform 平台驱动 + misc 混杂设备框架;
  • 设备树节点提供 GPIO 引脚信息,驱动通过 of_get_named_gpio() 获取;
  • 注册为 misc 设备,次设备号固定为 10,简化 /dev 节点创建。

(3)单总线时序操作

  • 初始化时序:主机发送复位脉冲,等待传感器应答脉冲;
  • 写时序:写 0 和写 1 的时序宽度不同;
  • 读时序:主机拉低总线后释放,在指定时间窗口内采样电平;
  • 时序操作对延时精度要求高,需使用 do_gettimeofday() 或 ndelay() 精确控制。

(4)温度读取流程

复制代码
// 1. 复位总线
ds18b20_reset();
// 2. 跳过 ROM 匹配(单设备场景)
ds18b20_write_byte(0xCC);
// 3. 启动温度转换
ds18b20_write_byte(0x44);
// 4. 等待转换完成(12 位分辨率约 750ms)
msleep(750);
// 5. 复位总线
ds18b20_reset();
// 6. 跳过 ROM 匹配
ds18b20_write_byte(0xCC);
// 7. 读取暂存器
ds18b20_write_byte(0xBE);
// 8. 读取低字节和高字节
low = ds18b20_read_byte();
high = ds18b20_read_byte();
// 9. 计算温度值
temp = (high << 8) | low;
if (temp & 0x8000) {
    temp = ~temp + 1;  // 负数处理
    temperature = -temp * 0.0625;
} else {
    temperature = temp * 0.0625;
}

(5)file_operations 实现

  • .open:打开设备节点,可做初始化检查;
  • .read:读取温度值,将计算结果拷贝到用户空间;
  • .release:关闭设备节点,释放资源。

与设备树配合

  • 设备树节点中通过 compatible = "dallas,ds18b20" 与驱动匹配;
  • 通过 pinctrl-0 配置 GPIO 引脚复用和电气属性;
  • 通过自定义属性 gpio-ds18b20 指定数据引脚对应的 GPIO 编号。

注意事项 :单总线时序对延时非常敏感,建议在驱动中使用 ndelay() 或 udelay() 精确控制时序;同时注意 GPIO 方向切换(输入/输出)的时机,避免时序错乱导致通信失败。

第六部分:各类传感器信息

1、DHT11 温湿度传感器(GPIO 单总线)

  • 量程:温度 0~50℃;湿度 20~90% RH
  • 精度:温度 ±2℃,湿度 ±5% RH;分辨率 1
  • 供电:3.3~5.5V
  • 一帧 5 字节:湿度整数、湿度小数、温度整数、温度小数、校验和(前 4 字节相加)

2、HC-SR04 超声波模块(GPIO)

  • VCC/GND、Trig 触发引脚、Echo 回声输出引脚
  • 测距:2cm ~ 450cm;精度 0.3cm;分辨率 1
  • 时序:Trig 发送≥10us 高电平触发,读取 Echo 高电平持续时间;距离公式:距离 = 高电平时间 × 340 / 2(除以 2,往返路程)

3、I2C 传感器列表

  • LM75:I2C 温度传感器
  • BH1750:I2C 光照传感器
  • MAX30100 / MAX30102:心率血氧传感器
  • MPU6050:六轴姿态传感器(加速度 + 角速度)
  • SHT30:温湿度传感器
BH1750 工作模式
  • 高分辨率连续测量模式:连续采样,分辨率 1lx,测量时间 120ms~180ms
  • 低分辨率连续测量模式:分辨率 4lx
  • 高分辨率单次测量模式:采集一次后自动掉电
  • 低分辨率单次测量模式

BH1750 读取流程:

  • 发送 Power On 上电指令
  • 发送测量模式指令(高分辨率连续)
  • 等待测量完成(180ms)
  • 读取 2 字节原始数据(先发高字节,后低字节)
  • 照度计算公式:照度(lx) = raw数据 / 1.2

**硬件接线:**IMX6ULL 3.3V 给 BH1750 供电;SDA、SCL 分别接到 I2C 总线 SDA/SCL 引脚。

4、UART 串口子系统

  • 串口通信参数:波特率、数据位、校验位、停止位
  • 硬件:ATGM336H GPS 模块(UART)
  • i.MX6ULL 串口:UART1、UART3;设备节点 /dev/ttymxc0 /dev/ttymxc2
  • UART 电平:TTL;RS485 差分;三相电表使用 Modbus
  • 应用层操作流程:① open 打开串口 /dev/ttymxcX ② read 读取数据

Modbus 协议流程:

  • 设置从站地址
  • 发送 Modbus 帧:地址码 + 功能码 + 数据 + CRC 校验
  • 电表回复报文
  • 解析 Modbus 协议数据帧

**GPS 模块输出:**NMEA 文本协议

NMEA 字段:$GNGGA,包含时间、经度、纬度、海拔高度、卫星数量等信息

示例:$GNGGA,084852.00,2236.9453,N,11408.4700,E,1,05,1.9,48.0,M,,M,,*48

字段含义:时间、纬度、北纬 / 南纬、经度、东经 / 西经、定位状态、卫星数量、HDOP、海拔高度等。

5、ADC 子系统(模拟量采集)

硬件:MQ-2 烟雾浓度传感器(模拟电压输出)

  • 输出电压范围:0~3.3V;模拟量范围 0~4095(12bit ADC)
  • i.MX6ULL 内置 ADC

开发步骤:

  • 设备树配置 ADC 硬件信息
  • 内核打开 ADC 模块使能
  • 使用 make menuconfig 配置内核,使能 IIO 子系统
  • 用户态访问路径 /sys/bus/iio/devices/iio:device0/in_voltageX_raw
  • 读取原始 ADC 值,换算电压:电压 = raw值 * 3.3 / 4095
  • 电压再代入公式,计算烟雾浓度 ppm

6、时间相关内核函数

  • time():获取时间,1970 年为时间起点
  • do_gettimeofday() 获取高精度时间,用于超声波计算脉冲时长

7、开发完整工程流程(统一流程)

(1)阅读芯片手册:功能、接口、数据格式、时序

(2)看传感器原理图:引脚、供电

(3)编写传感器驱动(platform 设备 + 驱动分离)

  • 设备:DTS 设备树节点,pinctrl 引脚配置,compatible 属性,reg 地址,gpio 引脚
  • 驱动:platform_driver,of_match_table

(4)编译驱动模块:Makefile obj-m += xxx.o

(5)部署 ko、dtb、zImage 到 NFS/TFTP

(6)加载驱动:insmod xxx.ko;卸载 rmmod xxx

(7)编写应用程序 APP:open 打开设备节点、read 读取传感器数据、解析数据

相关推荐
坤坤子吖1 小时前
Linux 线程同步与互斥——互斥锁
linux·服务器·c++·笔记·学习
TDTC_XQ1 小时前
Linux网络编程accept的讲解
linux·服务器·c++
Python自动化直播1 小时前
自动化笔记:素材管理、命名、轮换的几条土办法
linux·运维·数据库
FlightYe2 小时前
音视频修炼之容器与协议(一):MP4和MOV的box剖析
android·linux·c++·ffmpeg·音视频
暴雨课堂2 小时前
tp5 tp6被攻击篡改后恢复
linux·服务器·php
djjjx.2 小时前
Linux进程篇(四):内核托底与生死交错 ,深度硬核剖析“僵尸”与“孤儿”进程
linux·运维·服务器
我叫洋洋3 小时前
Cadence 16.6 从 OrCAD Capture 原理图到 Allegro PCB 全流程(含 5 个常见坑)
c语言·eda·嘉立创
为思念酝酿的痛3 小时前
Reactor反应堆模式
linux·服务器·网络
沫璃染墨3 小时前
从零入门计算机网络系列(六):TCP套接字编程实战——从listen监听到客户端与服务器通信
linux·服务器·c++·tcp/ip·计算机网络·tcp