嵌入式Linux驱动开发
时间:60天
开发板: 100ASK STM32MP157 Pro
- 512MB DDR3L
- 4GB eMMC FLASH
环境搭建 (03/28 1天)
下载BSP,百问网通过 Repo 管理多个 git 仓库中的源码,以组合提供BSP。
- Tfa 版本:V2.2
- Bootloader 版本:uboot 2020.02
- Linux 内核版本:LinuxKerenl 5.4 LTS
sudo ln -s /usr/bin/python3 /usr/bin/python
git clone https://gitee.com/oschina/repo.git
mkdir -p 100ask_stm32mp157_pro-sdk && cd 100ask_stm32mp157_pro-sdk
../repo/repo-py3 init -u https://gitee.com/weidongshan/manifests.git \
-b linux-sdk -m stm32mp1/100ask_stm32mp157_pro_release-v2.0.xml --no-repo-verify
../repo/repo sync -j16
配置交叉编译工具链:
export ARCH=arm
export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
export PATH=/home/amass/Projects/100ask_stm32mp157_pro-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin:$PATH
搭建NFS服务,便于在开发板上直接访问运行编译好的可执行文件或驱动模块。
交叉编译一个 Hello World 程序,确保能够正常编译,在开发板上通过 NFS 挂载的目录下运行。
编译内核
为什么编译驱动程序之前要先编译内核?
- 驱动程序要用到内核文件:比如驱动程序中这样包含头文件:
#include <asm/io.h>
,其中的asm
是一个链接文件,指向asm-arm
或asm-mips
,这需要先配置、编译内核才会生成asm
这个链接文件。 - 编译驱动时用的内核、开发板上运行到内核,要一致。
- 更换板子上的内核后,板子上的其他驱动也要更换。板子使用新编译出来的内核时,板子上原来的其他驱动也要更换为新编译出来的。
cd 100ask_stm32mp157_pro-sdk/Linux-5.4
# 执行之前,确保 ARCH、CROSS_COMPILE 被正确导入环境变量
make 100ask_stm32mp157_pro_defconfig
make uImage LOADADDR=0xC2000040 -j28
# 构建设备树的二进制文件
make dtbs
# 拷贝内核文件和设备树二进制文件
cp arch/arm/boot/uImage ~/Projects/100ask_stm32mp157_pro-sdk/binary
cp arch/arm/boot/dts/stm32mp157c-100ask-512d-v1.dtb ~/Projects/100ask_stm32mp157_pro-sdk/binary
# 构建内核模块
make modules -j28
# 安装内核模块至 binary 目录下备用
make INSTALL_MOD_PATH=~/Projects/100ask_stm32mp157_pro-sdk/binary INSTALL_MOD_STRIP=1 modules_install
将 binary
中的文件拷贝至开发板,并重启:
# 我们将 /home/amass/Projects/100ask_stm32mp157_pro-sdk 挂载到了开发板的 /root/100ask_stm32mp157_pro-sdk 下
cp /root/100ask_stm32mp157_pro-sdk/binary/uImage /boot
cp /root/100ask_stm32mp157_pro-sdk/binary/stm32mp157c-100ask-512d-v1.dtb /boot
cp /root/100ask_stm32mp157_pro-sdk/binary/lib/modules /lib -rfd
sync
reboot
构建系统和烧录系统 (03/28-待定 2天)
启动方式 :STM32MP157支持分别从EMMC、SD、USB加载固件启动。
BOOT | BOOT0 | BOOT1 | BOOT2 |
---|---|---|---|
EMMC | OFF | ON | OFF |
SD | ON | OFF | ON |
USB | OFF | OFF | OFF |
M4(debug) | OFF | OFF | ON |
Yocto 和 Buildroot 都属于嵌入式Linux系统构建工具,用于为嵌入式设备定制和生成轻量化的Linux操作系统。它们帮助开发者自动化地编译内核、工具链、根文件系统及应用程序,适配特定硬件和需求。
Tfa编译
cd ~/Projects/100ask_stm32mp157_pro-sdk/Tfa-v2.2
make -f $PWD/./Makefile.sdk all -j28
编译完成后得到文件 ../build/trusted/tf-a-stm32mp157c-100ask-512d-v1.stm32
。
U-Boot编译
cd ~/Projects/100ask_stm32mp157_pro-sdk/Uboot-2020.02
make stm32mp15_trusted_defconfig
make DEVICE_TREE=stm32mp157c-100ask-512d-v1 all -j28
编译会报错:
/usr/bin/ld: scripts/dtc/dtc-parser.tab.o:(.bss+0x10): multiple definition of `yylloc'; scripts/dtc/dtc-lexer.lex.o:(.bss+0x0): first defined here
修改 scripts/dtc/dtc-lexer.lex.c
,在 YYLTYPE yylloc;
加上 extern
。
编译完成之后最终得到 u-boot.stm32
文件。
编译内核,如之前所述。
使用 Buildroot 构建系统
# 在 Ubuntu 24.04 下编译不过,这里使用容器
docker run --rm -it \
--user amass \
-w /home/amass/Projects/100ask_stm32mp157_pro-sdk \
-v /home/amass/Projects/100ask_stm32mp157_pro-sdk:/home/amass/Projects/100ask_stm32mp157_pro-sdk \
-e ARCH=arm \
-e CROSS_COMPILE=arm-buildroot-linux-gnueabihf- \
-e PATH="/home/amass/Projects/100ask_stm32mp157_pro-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin:$PATH" \
registry.cn-shenzhen.aliyuncs.com/amass_toolset/ubuntu_dev:18.04 \
/bin/bash
cd ~/Projects/100ask_stm32mp157_pro-sdk/Buildroot_2020.02.x
make 100ask_stm32mp157_pro_ddr512m_systemD_qt5_defconfig
unset LD_LIBRARY_PATH
sudo apt install cpio unzip bzip2
make all -j28
驱动入门 (03/29-04/07 8天 )
编写Hello World 驱动(03/29 1天)
在编写字符设备驱动时,要重点关注 struct file_operations
这个结构体:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
// ......
} __randomize_layout;
对于简单的字符设备驱动来说,最重要的一步就是如何对 file_operations
结构体进行填充。
怎么编写驱动程序?
- 确定主设备号,也可以让内核分配。
- 定义自己的
file_operations
结构体。 - 实现对应的
open()
、read()
、write()
、release()
等函数,填入file_operations
结构体。 - 把
file_operations
结构体告诉内核:int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
。 - 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数。
- 有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用
void unregister_chrdev(unsigned int major, const char *name)
。 - 其他完善:提供设备信息,自动创建设备节点:
class_create()
,device_create()
(两个函数位于include/linux/device.h
)。
# 打开内核的打印信息
echo "7 4 1 7" > /proc/sys/kernel/printk
# 安装驱动程序
insmod ./hello_driver.ko
# 确认驱动程序是否生成设备节点
ls /dev/hello -l
# 测试驱动程序
./hello_app -w iwantwork
./hello_app -r
# 卸载驱动
rmmod hello_driver
.ko
文件(Kernel Object)是Linux内核模块的二进制文件,其主要目的是动态扩展内核功能。虽然最常见的用途是加载设备驱动,但内核模块的用途远不止于此。以下是几种典型的非驱动用途:
- 文件系统支持:例如
ext4.ko
、nfs.ko
等,用于在内核中实现文件系统的逻辑。 - 网络协议栈扩展:如
ip_tables.ko
(实现iptables防火墙)、tcp_bbr.ko
(TCP拥塞控制算法)等。 - 内核功能增强:如性能监控工具(
perf
相关模块)、调试工具(如kprobes.ko
用于动态跟踪)等。 - 安全模块:例如SELinux或AppArmor的扩展模块。
- 虚拟化支持:如KVM或容器相关的内核模块。
它与应用开发的 .so
动态库有一定的相似性:
- 例如
.ko
通过insmod
/modprobe
动态加载到内核空间,而无需重新编译整个内核。 - 通过
module_init
和module_exit
定义了模块的入口函数和退出函数,分别对应于执行insmod
、rmmod
时需要执行的函数。 - 可以通过
EXPORT_SYMBOL
导出函数符号 。
总线设备驱动模型(03/29 1天)
总线设备驱动模型的核心目的:为了将驱动代码中,不需要改变的代码和经常需要改变的代码进行隔离。
在 include/linux/device.h
主要定义 bus_type
、device_driver
、device
三个概念。
在 include/linux/platform_device.h
主要定义了 platform_device
、platform_driver
的概念。
在 drivers/base/platform.c
定义了:
struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match,
.uevent = platform_uevent,
.dma_configure = platform_dma_configure,
.pm = &platform_dev_pm_ops,
};
在 drivers/usb/core/driver.c
定义了:
struct bus_type usb_bus_type = {
.name = "usb",
.match = usb_device_match,
.uevent = usb_uevent,
.need_parent_lock = true,
};
所以 linux 的驱动总线并不只有一条。platform_device
是 Linux 内核中用于描述非标准总线设备的一种机制,其核心目的是:
- 表示无总线(或虚拟总线)的设备:许多嵌入式设备(如 SoC 内部集成的 UART、GPIO、时钟控制器等)并不挂载在标准总线(如 PCI、USB、I2C)上,它们无法通过总线枚举机制自动发现。
platform_device
为这类设备提供了统一的抽象,使其能够融入内核的设备驱动模型。 - 解耦硬件描述与驱动逻辑:将设备资源(寄存器地址、中断号等)从驱动代码中剥离,通过
platform_device
静态定义或动态(如设备树)描述,实现驱动代码的复用。 - 兼容旧版内核代码:在设备树(Device Tree)普及前,
platform_device
是描述固定硬件的主要方式(通过硬编码或板级文件)。
在内核的设备模型中,所有设备均以 struct device
为基础,而 platform_device
是其扩展。platform_device
内嵌一个 struct device
体现了面向对象思想的继承,使其可以复用 struct device
的所有功能。同时其还扩展添加了一些其他属性,例如 id
和 resource
。
在调用 platform_driver_register()
和 platform_device_register()
时,都会触发 platform_bus_type
的 platform_match()
函数。如果成功匹配,则会调用platform_driver
的 probe()
函数。
设备树(03/30-04/01 3天)
- GPIO和Pinctrl(04/02-04/04 3天)
- 按键中断驱动(04/05-04/07 3天)
LCD显示子系统 (04/08-04/11 4天)
I2C子系统 (04/12-04/15 4天)
Input输入子系统 (04/16-04/19 4天)
Pinctrl子系统 (04/20-04/23 4天)
GPIO子系统 (04/24-04/27 4天)
Interrupt子系统 (04/28-05/01 4天)
UART子系统 (05/02-05/05 4天)
SPI子系统 (05/06- 05/09 4天)
V4L2子系统 (05/10-05/13 4天)
Uboot 移植 (05/14-05/26 13天)
使用 Buildroot 自定义构建系统
遗留问题
-
在编译 Linux 内核时,一般所说的
zImage
和uImage
到底是什么?-
zImage
是 Linux 内核通过gzip
算法压缩后的二进制镜像文件。其生成过程分为两步:- 将未压缩的
vmlinux
(ELF 格式)转换为纯二进制文件Image
(通过objcopy
工具); - 对
Image
进行gzip
压缩,并在压缩后的数据前添加解压缩代码,最终生成zImage
。压缩后的zImage
体积较小(通常不足 2MB),适合存储空间受限的嵌入式设备。
- 将未压缩的
-
uImage
是专为 U-Boot 引导程序设计的镜像格式,本质上是在zImage
前添加 64 字节的头部信息。该头部包含内核版本、加载地址、入口地址、CRC 校验等元数据,由mkimage
工具生成,生成命令示例:mkimage -A arm -O linux -T kernel -C none -a 0x20007fc0 -e 0x20008000 -n "Linux Kernel" -d zImage uImage
由于头部信息的存在,U-Boot 可通过
bootm
命令直接识别uImage
,并根据头部中的地址信息加载内核到内存。这种设计简化了嵌入式系统的启动流程,尤其适用于需要自定义启动参数的场景
-
-
一般在使用 Buildroot 构建生成的最终可以烧录到 eMMC 的 image 文件是什么?它包含了哪些东西?为什么烧录之后,就可以启动单板了?
在使用 Buildroot 构建嵌入式 Linux 系统时,最终生成的可以烧录到 eMMC 的镜像文件通常是 update.img 或 完整固件包(具体名称可能因 SDK 或厂商工具不同而略有差异)。该镜像文件是一个经过整合的完整系统镜像。
镜像文件包含的内容
- Bootloader(如 U-Boot):负责硬件初始化、加载内核和传递启动参数。例如,在 RK 系列开发板中,uboot.img 和 trust.img(ARM TrustZone 相关)是 Bootloader 的核心部分。
- Linux 内核(Kernel)与设备树(Device Tree):内核镜像(如 zImage 或 Image)是操作系统的核心,负责管理硬件资源和进程调度。设备树文件(.dtb)描述硬件配置信息,确保内核能正确识别单板外设。
- 根文件系统(Rootfs):由 Buildroot 生成的根文件系统(如 rootfs.img),包含 Linux 运行所需的目录结构、库文件、配置文件及用户空间工具(如 BusyBox 命令)。根文件系统可能基于 Buildroot 或 Debian 等框架构建。
- 分区表与固件元数据:例如 parameter.txt 文件定义 eMMC 的分区布局(如 boot、system、user 分区),确保镜像按正确地址烧录。
镜像烧录后单板启动的原理
烧录后的镜像能够启动单板,依赖于 eMMC 的分区结构 和 硬件启动流程 的协同工作:
- 硬件初始化与 Boot ROM:单板上电后,CPU 首先执行固化在芯片内部的 Boot ROM 代码,该代码会初始化 eMMC 接口并加载启动分区(Boot Partition)中的 Bootloader。
- Bootloader 加载内核与根文件系统:Bootloader(如 U-Boot)从 eMMC 的指定分区读取内核镜像和设备树,将其加载到内存中,并传递启动参数(如根文件系统路径)。例如,U-Boot 可能从 boot.img 中解压内核。
- 内核挂载根文件系统:内核启动后,根据 Bootloader 传递的参数挂载根文件系统(如从 rootfs.img 中解压),并执行初始化脚本(如 /etc/inittab),最终进入用户空间。
- 分区与镜像的完整性:烧录工具(如 RKDevTool 或 build.sh 脚本)会将镜像文件按分区表写入 eMMC 的对应区域,确保各组件在硬件预期的地址上。
镜像生成与烧录的关键工具
- Buildroot 的编译流程:Buildroot 通过 make menuconfig 配置目标架构、内核版本、软件包等,最终在 output/images 目录生成根文件系统镜像。结合 SDK 的 build.sh 脚本可将内核、U-Boot 等打包为完整镜像。
- 厂商烧录工具:例如瑞芯微的 RKDevTool 或 Linux_Upgrade_Tool,通过 USB-OTG 接口将 update.img 写入 eMMC 的分区中。
-
一般说的 内核挂载根文件系统,到底是什么意思?