跳到主要内容

嵌入式Linux驱动开发

阅读量: 101阅读人次: 102

时间: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-armasm-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加载固件启动。

BOOTBOOT0BOOT1BOOT2
EMMCOFFONOFF
SDONOFFON
USBOFFOFFOFF
M4(debug)OFFOFFON

YoctoBuildroot 都属于嵌入式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 这个结构体:

include/linux/fs.h
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 结构体进行填充。

怎么编写驱动程序?

  1. 确定主设备号,也可以让内核分配。
  2. 定义自己的 file_operations 结构体。
  3. 实现对应的 open()read()write()release() 等函数,填入 file_operations 结构体。
  4. file_operations 结构体告诉内核: int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
  5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数。
  6. 有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用 void unregister_chrdev(unsigned int major, const char *name)
  7. 其他完善:提供设备信息,自动创建设备节点: 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.konfs.ko等,用于在内核中实现文件系统的逻辑。
  • 网络协议栈扩展:如ip_tables.ko(实现iptables防火墙)、tcp_bbr.ko(TCP拥塞控制算法)等。
  • 内核功能增强:如性能监控工具(perf相关模块)、调试工具(如kprobes.ko用于动态跟踪)等。
  • 安全模块:例如SELinux或AppArmor的扩展模块。
  • 虚拟化支持:如KVM或容器相关的内核模块。

它与应用开发的 .so 动态库有一定的相似性:

  • 例如.ko通过insmod/modprobe 动态加载到内核空间,而无需重新编译整个内核。
  • 通过 module_initmodule_exit 定义了模块的入口函数和退出函数,分别对应于执行insmodrmmod时需要执行的函数。
  • 可以通过 EXPORT_SYMBOL 导出函数符号。

总线设备驱动模型(03/29 1天)

总线设备驱动模型的核心目的:为了将驱动代码中,不需要改变的代码和经常需要改变的代码进行隔离。

include/linux/device.h 主要定义 bus_typedevice_driverdevice 三个概念。

include/linux/platform_device.h 主要定义了 platform_deviceplatform_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 的所有功能。同时其还扩展添加了一些其他属性,例如 idresource

在调用 platform_driver_register()platform_device_register() 时,都会触发 platform_bus_typeplatform_match() 函数。如果成功匹配,则会调用platform_driverprobe() 函数。

设备树(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 自定义构建系统

遗留问题

  1. 在编译 Linux 内核时,一般所说的 zImageuImage 到底是什么?

    • zImage 是 Linux 内核通过 gzip 算法压缩后的二进制镜像文件。其生成过程分为两步:

      1. 将未压缩的 vmlinux(ELF 格式)转换为纯二进制文件 Image(通过 objcopy 工具);
      2. 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,并根据头部中的地址信息加载内核到内存。这种设计简化了嵌入式系统的启动流程,尤其适用于需要自定义启动参数的场景

  2. 一般在使用 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 的分区结构 和 硬件启动流程 的协同工作:

    1. 硬件初始化与 Boot ROM:单板上电后,CPU 首先执行固化在芯片内部的 Boot ROM 代码,该代码会初始化 eMMC 接口并加载启动分区(Boot Partition)中的 Bootloader。
    2. Bootloader 加载内核与根文件系统:Bootloader(如 U-Boot)从 eMMC 的指定分区读取内核镜像和设备树,将其加载到内存中,并传递启动参数(如根文件系统路径)。例如,U-Boot 可能从 boot.img 中解压内核。
    3. 内核挂载根文件系统:内核启动后,根据 Bootloader 传递的参数挂载根文件系统(如从 rootfs.img 中解压),并执行初始化脚本(如 /etc/inittab),最终进入用户空间。
    4. 分区与镜像的完整性:烧录工具(如 RKDevTool 或 build.sh 脚本)会将镜像文件按分区表写入 eMMC 的对应区域,确保各组件在硬件预期的地址上。

    镜像生成与烧录的关键工具

    1. Buildroot 的编译流程:Buildroot 通过 make menuconfig 配置目标架构、内核版本、软件包等,最终在 output/images 目录生成根文件系统镜像。结合 SDK 的 build.sh 脚本可将内核、U-Boot 等打包为完整镜像。
    2. 厂商烧录工具:例如瑞芯微的 RKDevTool 或 Linux_Upgrade_Tool,通过 USB-OTG 接口将 update.img 写入 eMMC 的分区中。
  3. 一般说的 内核挂载根文件系统,到底是什么意思?