一、系统移植完整流程
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 驱动三大分类
- 字符设备驱动:字节流访问,led、beep、传感器、串口;
- 块设备驱动:按块读写,SD 卡、EMMC、NAND Flash;
- 网络设备驱动:网卡,收发数据包。
字符设备特点: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 读取传感器数据、解析数据