ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

嵌入式Linux 21天速成:从驱动开发到NFS根文件系统挂载实战

嵌入式Linux 21天速成:从驱动开发到NFS根文件系统挂载实战 1. 这本书到底解决了谁的痛点嵌入式Linux这个方向坑多、链条长、入门曲线陡几乎是所有从单片机转过来的开发者共同的感受。我见过太多人抱着《Linux设备驱动开发详解》啃了三个月结果连一块开发板都没跑起来也见过培训班出来的学员面试时能把字符设备驱动的框架背得滚瓜烂熟真给他一个I2C传感器却不知道从设备树哪个节点开始改。飞凌嵌入式这次联合北京大学出版社推出《嵌入式Linux系统开发21天速成》从书名就能看出来它瞄准的不是“学院派”的完整知识体系而是“上手干活”这条最短路径。这本书的核心价值在于把嵌入式Linux开发拆成了一条有时间刻度的学习路线。21天不是噱头它对应的是三个完整的工程阶段环境搭建与系统认知、驱动开发与外设调试、系统集成与项目实战。每个阶段7天每天一个明确的产出目标。这种结构对于自学者来说非常友好因为嵌入式Linux最大的问题不是知识点难而是知识点之间的依赖关系太乱——你先学内核还是先学驱动先搞根文件系统还是先搞uboot没有一条清晰的路径很容易在某个环节卡死然后放弃。适合读这本书的人我梳理了一下大概有三类。第一类是有C语言和单片机基础、想转嵌入式Linux的开发者这类人最需要的就是把“裸机思维”切换成“操作系统思维”书里前7天的内容基本就是干这个的。第二类是在校学生或者刚入行的新人需要一套能跑通的完整流程来建立信心飞凌的板子加这本书的组合相当于把“实验环境”和“实验指导”打包了。第三类是有一定经验但知识碎片化的工程师比如会写应用但不懂驱动或者会调驱动但不懂系统启动流程这本书的章节编排可以当手册来查漏补缺。需要提前说明的是这本书大概率是以飞凌自家的开发板比如OK系列为硬件平台来写的这意味着书中的例程、设备树配置、交叉编译工具链都是针对特定板子验证过的。好处是“抄作业”成功率高坏处是换一块板子可能需要做移植。但嵌入式Linux的学习本来就是这样先在一块确定的板上把全流程跑通比在十块不同的板上各跑一半要有价值得多。2. 21天速成的底层逻辑拆解嵌入式Linux的知识依赖链2.1 为什么是21天而不是30天或100天市面上嵌入式Linux的教材动辄五六百页从操作系统原理讲到内核源码分析学完一遍半年过去了回头一看开发板还在吃灰。这种“先学完再动手”的模式对于已经工作的开发者来说几乎不可行。21天的设定本质上是把“最小可运行知识集”抽出来砍掉了大量在入门阶段用不上的理论细节。我对比过几本主流教材的目录结构大多数是按照“计算机组成原理→操作系统→Linux命令→Shell编程→内核→驱动→应用”这种自底向上的顺序组织的。这种顺序在逻辑上没问题但在学习体验上很糟糕因为前两周你都在跟命令行和理论打交道看不到任何“嵌入式”的东西。飞凌这本书的编排逻辑更像是“自顶向下、按需下钻”先让你把板子跑起来看到串口输出登录进系统然后再告诉你这个系统是怎么来的最后才深入到驱动层。21天的时间分配我推测大概是这样的第1-7天搞定开发环境虚拟机、交叉编译工具链、串口终端、网络配置理解启动流程BootROM→SPL→uboot→kernel→rootfs能烧录系统并登录第8-14天从最简单的字符设备驱动开始逐步过渡到GPIO、中断、I2C、SPI等常用外设同时讲解设备树的基本语法和修改方法第15-21天做一个综合项目比如通过I2C读取传感器数据并通过网络上报把驱动、应用、系统配置串起来。这个节奏对于每天能投入3-4小时的人来说是合理的如果每天只有1小时可能需要拉长到6-8周但路径本身是清晰的。2.2 根文件系统挂载NFS v3为什么是调试利器热搜词里出现了“嵌入式linux 根文件系统挂载 使用nfs v3”这恰恰是嵌入式开发中一个极其关键但又容易被教材一笔带过的环节。我单独把它拎出来讲因为在实际开发中NFS挂载根文件系统的能力直接决定了你的调试效率。先解释一下为什么需要NFS挂载根文件系统。在嵌入式开发中根文件系统rootfs包含了系统启动后需要的所有目录、库、配置文件和应用程序。传统的做法是把rootfs打包成镜像烧写到板子的Flash里但这样每次修改一个配置文件或者重新编译一个应用程序都要重新打包、烧录、重启一轮下来至少五分钟。而通过NFSNetwork File System把主机上的一个目录挂载为板子的根文件系统修改主机上的文件后板子重启就能生效甚至有些文件可以热更新调试效率提升十倍不止。那为什么特别强调NFS v3因为NFS v4在嵌入式环境中有几个坑。第一NFS v4引入了伪文件系统和复杂的状态管理很多嵌入式内核的NFS客户端配置默认只支持v3用v4挂载会直接报错。第二v4对用户ID映射和权限管理更严格而嵌入式系统往往用root跑一切v3的宽松策略反而更方便。第三uboot的NFS引导参数对v3的支持最成熟nfsroot192.168.1.100:/home/rootfs,v3,tcp这种写法几乎是标配。具体操作上主机端需要安装NFS服务编辑/etc/exports文件加入类似/home/rootfs *(rw,sync,no_root_squash)的配置然后重启NFS服务。板子端在uboot中设置bootargs指定root/dev/nfs nfsroot主机IP:路径,v3,tcp ip板子IP:主机IP:网关:掩码::eth0:off。这里有个细节no_root_squash是必须的否则板子以root身份访问NFS时会被映射成nobody导致权限拒绝。另一个细节是内核配置中要确保CONFIG_ROOT_NFS和CONFIG_NFS_V3是打开的很多默认配置只开了v4需要手动改。注意NFS挂载根文件系统只适合开发阶段量产产品绝对不能这么干因为网络断了系统就起不来。另外主机和板子必须在同一网段且防火墙要放行NFS相关的端口2049、111等。2.3 驱动开发在21天里能学到什么程度“嵌入式linux驱动开发”是热搜词里另一个高频词。很多人对驱动开发有畏难情绪觉得要懂内核源码、要会写复杂的回调函数。但实际上入门级的驱动开发是有固定套路的21天里完全可以把常用外设的驱动框架摸清楚。书里大概率会从字符设备驱动入手因为这是最简单的驱动模型。字符设备驱动的核心就是实现open、read、write、ioctl、release这几个文件操作函数然后通过register_chrdev或者更现代的alloc_chrdev_regioncdev_add注册到内核。这个过程中会涉及到file_operations结构体、copy_to_user/copy_from_user、ioremap等基础概念。把这些搞明白大概需要2-3天。接下来是设备树。现代Linux内核3.x以后已经全面采用设备树来描述硬件驱动代码里不再硬编码寄存器地址而是通过of_match_table匹配设备树节点用platform_get_resource获取资源。设备树的语法本身不难难的是理解“设备树描述硬件、驱动匹配设备树、两者通过compatible属性关联”这个机制。书里应该会以GPIO驱动为例展示如何在设备树中添加一个节点然后在驱动中解析这个节点。再往后是中断和并发控制。中断处理涉及request_irq、中断上半部和下半部tasklet、工作队列、中断共享等概念。并发控制则涉及自旋锁、互斥锁、原子操作这些在驱动中无处不在因为内核是多线程环境驱动代码可能被多个进程同时调用。这部分内容比较抽象但书里如果有实际的按键中断例程理解起来会容易很多。最后是I2C和SPI子系统。这两个是嵌入式最常用的外设总线内核已经提供了完整的子系统框架驱动开发者只需要实现i2c_driver或spi_driver结构体填充probe、remove等函数然后通过设备树匹配。以I2C为例一个典型的传感器驱动大概100-200行代码核心就是i2c_smbus_read_byte_data和i2c_smbus_write_byte_data这两个函数。21天里能把I2C传感器驱动跑通已经算是入门了。3. 从零搭建开发环境虚拟机、工具链与串口调试3.1 主机环境的选择与配置嵌入式Linux开发的第一步是搭建主机环境。虽然理论上可以在Windows下用WSL或者直接装双系统但最稳妥的方案还是用虚拟机跑Ubuntu。我推荐Ubuntu 18.04或20.04因为这两个版本的软件源里交叉编译工具链比较全而且很多嵌入式SDK的官方支持也是基于这两个版本。虚拟机的配置有几个关键点。第一磁盘至少给80GB因为交叉编译工具链、内核源码、根文件系统加起来很容易超过30GB再加上虚拟机快照空间不够会很痛苦。第二内存至少4GB如果主机有16GB建议给虚拟机8GB因为编译内核的时候内存不足会直接报错。第三网络模式建议用桥接这样虚拟机会获得一个独立的IP和开发板通信更方便。如果用NAT模式需要额外配置端口转发比较麻烦。安装完Ubuntu后需要安装一些基础工具build-essential、git、vim、minicom、nfs-kernel-server、tftp-hpa、libncurses5-dev、flex、bison等。这些工具在后续的编译和调试中都会用到。特别是minicom它是Linux下最常用的串口终端工具配置好之后可以通过串口线和开发板交互。提示安装完Ubuntu后第一件事是换软件源国内用户建议换成清华或阿里的镜像源否则下载速度会让你怀疑人生。换源之后执行sudo apt update sudo apt upgrade确保系统是最新的。3.2 交叉编译工具链的安装与验证交叉编译是嵌入式开发的核心概念。简单来说就是在x86的PC上编译出能在ARM架构的开发板上运行的程序。这需要一套专门的工具链通常由芯片厂商或者第三方提供比如arm-linux-gnueabihf-前缀的gcc、g、ld等。安装工具链的方式有两种一种是直接用apt安装比如sudo apt install gcc-arm-linux-gnueabihf这种方式简单但版本可能比较旧另一种是从厂商官网下载工具链压缩包解压后添加到PATH环境变量中。飞凌的SDK通常会附带推荐的工具链版本建议按照书中的说明来因为工具链版本和内核版本、库版本之间有兼容性要求。验证工具链是否安装成功可以执行arm-linux-gnueabihf-gcc -v如果能看到版本信息就说明OK了。然后写一个最简单的hello.c用交叉编译器编译再用file命令查看生成的可执行文件如果显示“ARM aarch64”或“ARM, EABI5”之类的信息就说明交叉编译成功了。把这个可执行文件拷贝到开发板上运行如果能打印出“Hello, Embedded Linux”那整个工具链和开发板的连通性就验证完毕了。这里有个常见的坑工具链的浮点ABI要和开发板的根文件系统匹配。比如gnueabihf表示硬件浮点gnueabi表示软件浮点如果编译出来的程序在板子上跑不起来报“No such file or directory”或者“Illegal instruction”大概率就是ABI不匹配。解决办法是查看开发板rootfs中/lib目录下的动态链接器名称比如ld-linux-armhf.so.3对应硬件浮点ld-linux.so.3对应软件浮点。3.3 串口终端与网络调试通道的建立串口是嵌入式开发中最重要的调试通道因为系统启动阶段网络还没起来只能通过串口看打印信息。连接串口需要一根USB转TTL线开发板上通常有调试串口的排针一般是3针或4针GND、TX、RX、VCC只需要接GND、TX、RX三根线VCC不要接因为板子自己供电。在Ubuntu下插入USB转TTL线后用dmesg | tail可以看到识别出的串口设备通常是/dev/ttyUSB0。然后用minicom配置sudo minicom -s进入串口设置波特率一般选115200数据位8停止位1无校验流控选None。配置好后保存退出就能看到开发板的串口输出了。如果乱码检查波特率是否正确如果没有任何输出检查TX/RX是否接反了TX接RXRX接TX。网络通道的建立有两种方式TFTP用于传输文件NFS用于挂载根文件系统。TFTP的配置比较简单安装tftpd-hpa后编辑/etc/default/tftpd-hpa设置TFTP_DIRECTORY为你的共享目录然后重启服务。在uboot中可以用tftp命令从主机下载内核镜像或设备树文件。NFS的配置前面已经讲过这里不再重复。这两个通道建立好之后开发效率会有质的提升因为你可以随时更新内核、设备树、应用程序而不需要反复插拔SD卡或烧录Flash。4. 系统启动流程与根文件系统构建4.1 从上电到登录嵌入式Linux的启动链路理解启动流程是嵌入式Linux开发的基本功。很多人调驱动调了半天没反应最后发现是设备树没编译进去或者内核根本没加载这个驱动根源就在于对启动链路不清楚。嵌入式Linux的启动大致分为四个阶段。第一阶段是BootROM这是芯片内部固化的一段代码上电后首先运行它会根据启动引脚的电平决定从哪个介质启动SD卡、eMMC、NAND等然后把SPLSecondary Program Loader加载到内部SRAM中运行。第二阶段是SPL它负责初始化DDR内存然后把uboot加载到DDR中运行。第三阶段是uboot这是功能最丰富的引导程序它会初始化网络、显示、存储等外设然后根据环境变量加载内核镜像和设备树到DDR中最后跳转到内核入口。第四阶段是内核启动内核会打印大量信息初始化各个子系统最后挂载根文件系统执行/sbin/init进入用户空间。这个过程中串口打印是唯一的观测手段。uboot阶段会打印版本号、DRAM大小、环境变量等信息内核阶段会打印CPU信息、内存布局、驱动加载情况等。如果启动卡在某个阶段可以根据最后的打印信息定位问题。比如卡在“Starting kernel ...”之后没有任何输出可能是内核镜像格式不对、加载地址不对、或者串口波特率不对如果内核打印到“VFS: Cannot open root device”就停了说明根文件系统挂载失败需要检查bootargs中的root参数。4.2 BusyBox构建最小根文件系统根文件系统是嵌入式Linux的“灵魂”没有它系统起不来。构建根文件系统最常用的工具是BusyBox它把ls、cd、cp、mv、ifconfig等常用命令集成到一个可执行文件中通过符号链接的方式提供各种命令非常适合资源受限的嵌入式环境。构建BusyBox的流程大概是下载源码、配置make menuconfig、编译make、安装make install。配置的时候要注意选择交叉编译器在Settings→Build Options中设置Cross Compiler prefix为你的工具链前缀比如arm-linux-gnueabihf-。编译完成后_install目录下会生成bin、sbin、usr等目录这就是根文件系统的雏形。但光有BusyBox还不够还需要添加一些必要的目录和文件。比如/dev目录下需要有console和null设备节点/etc目录下需要有inittab、fstab、profile等配置文件/lib目录下需要放动态链接库如果BusyBox是动态编译的。这些内容书里应该会有详细的步骤我在这里只强调一个容易忽略的点/etc/inittab文件决定了系统启动后执行哪些脚本如果这个文件配置错了系统可能起不来或者起来后没有shell。注意BusyBox编译时建议选择静态编译Settings→Build Options→Build static binary这样就不需要拷贝动态链接库根文件系统会更简单。代价是可执行文件会大一些但对于入门学习来说简单比省空间更重要。4.3 使用NFS挂载根文件系统的完整配置前面讲了NFS挂载的原理这里给出一个完整的配置流程。主机端安装nfs-kernel-server创建/home/rootfs目录把BusyBox构建的根文件系统拷贝进去编辑/etc/exports加入/home/rootfs *(rw,sync,no_root_squash,no_subtree_check)然后执行sudo exportfs -ra和sudo systemctl restart nfs-kernel-server。板子端在uboot中设置bootargs格式为root/dev/nfs rw nfsroot192.168.1.100:/home/rootfs,v3,tcp ip192.168.1.200:192.168.1.100:192.168.1.1:255.255.255.0::eth0:off consolettyS0,115200。这里的IP地址要根据实际网络环境修改192.168.1.100是主机IP192.168.1.200是板子IP192.168.1.1是网关。consolettyS0,115200指定串口控制台确保启动信息能打印出来。内核配置方面需要确保以下选项是打开的CONFIG_NFS_FSy、CONFIG_NFS_V3y、CONFIG_ROOT_NFSy、CONFIG_IP_PNPy、CONFIG_IP_PNP_DHCPy如果用DHCP或CONFIG_IP_PNP_BOOTPy。如果内核不支持NFS根文件系统启动时会报“VFS: Cannot open root device”错误。配置完成后重启板子如果一切正常你应该能看到内核启动日志最后出现BusyBox的shell提示符。这时候你在主机上修改/home/rootfs中的文件板子重启后就能看到变化。这个流程跑通之后后续的驱动调试和应用开发都会方便很多。5. 驱动开发实战从字符设备到I2C传感器5.1 字符设备驱动的最小框架字符设备驱动是Linux驱动开发的“Hello World”。一个最小的字符设备驱动包含以下几个部分定义file_operations结构体实现open、read、write、release等函数在模块初始化函数中注册设备号添加cdev在模块退出函数中注销设备号和cdev。设备号的注册有两种方式静态指定和动态分配。静态指定用register_chrdev_region需要自己选一个未被占用的主设备号容易冲突动态分配用alloc_chrdev_region内核会自动分配一个可用的主设备号更推荐。注册完设备号后需要初始化cdev结构体用cdev_init绑定file_operations然后用cdev_add添加到内核。file_operations中的函数是驱动和用户空间交互的接口。open函数在用户调用open(/dev/xxx, ...)时被调用通常用来初始化硬件或增加引用计数read函数在用户调用read时被调用需要用copy_to_user把内核空间的数据拷贝到用户空间write函数相反用copy_from_user把用户空间的数据拷贝到内核空间release函数在用户调用close时被调用用来释放资源。编译这个驱动需要编写Makefile指定内核源码路径和交叉编译器。编译生成.ko文件后通过TFTP或NFS拷贝到板子上用insmod加载用rmmod卸载。加载成功后用dmesg可以看到驱动的打印信息用cat /proc/devices可以看到注册的设备号然后用mknod创建设备节点就可以用用户空间的程序来读写这个设备了。5.2 设备树语法与GPIO驱动实例设备树是理解现代Linux驱动的关键。它的本质是一棵描述硬件拓扑的树每个节点代表一个设备或总线节点中的属性描述了这个设备的地址、中断号、时钟、引脚等信息。驱动代码通过compatible属性来匹配设备树节点匹配成功后调用probe函数。一个典型的GPIO设备树节点长这样my_led { compatible mycompany,my-led; led-gpios gpio1 3 GPIO_ACTIVE_HIGH; status okay; };这个节点定义了一个LED设备使用gpio1控制器的第3号引脚高电平有效。驱动代码中通过of_match_table匹配mycompany,my-led然后在probe函数中用gpio_get_value或gpiod_get获取GPIO用gpio_direction_output设置为输出用gpio_set_value控制电平。设备树的编译需要用内核源码中的dtc工具把.dts文件编译成.dtb文件然后由uboot加载到内核。修改设备树后需要重新编译.dtb并更新到板子上通常放在TFTP目录下在uboot中用tftp命令下载。这里有个容易犯的错误设备树节点的status属性默认是disabled如果忘记改成okay驱动不会匹配成功。5.3 I2C传感器驱动开发与调试I2C是嵌入式最常用的传感器接口很多温湿度传感器、加速度计、EEPROM都是I2C接口。Linux内核提供了完整的I2C子系统驱动开发者只需要实现i2c_driver结构体填充probe、remove、id_table等字段。以一款常见的I2C温度传感器为例驱动的大致流程是在设备树中定义传感器节点指定I2C总线号和从机地址驱动中定义of_device_id和i2c_device_id匹配表在probe函数中用i2c_smbus_read_word_data读取温度寄存器用i2c_smbus_write_byte_data配置传感器把读取到的温度值通过字符设备或sysfs接口暴露给用户空间。调试I2C驱动时最常用的工具是i2cdetect和i2cget/i2cset。i2cdetect -y 1可以扫描I2C总线1上的所有设备如果传感器的地址出现在列表中说明硬件连接和总线驱动是正常的。i2cget -y 1 0x48 0x00 w可以读取地址为0x48的传感器的0x00寄存器的值。如果i2cdetect扫不到设备检查SDA/SCL是否接反、上拉电阻是否焊接、从机地址是否正确。提示I2C从机地址通常是7位但在i2cget等工具中需要左移一位变成8位地址。比如传感器手册上写地址是0x48实际使用时是0x900x481。这个坑我踩过不止一次明明地址没错但就是读不到数据最后发现是移位问题。6. 常见问题排查与避坑经验6.1 启动类问题速查表现象可能原因排查方法串口无任何输出串口线接反、波特率不对、板子没供电检查TX/RX是否交叉连接确认波特率115200测量板子电压uboot启动后卡住环境变量错误、存储介质损坏进入uboot命令行用printenv查看环境变量用mmc info检查存储内核启动后无输出内核镜像格式不对、加载地址错误确认内核是zImage还是Image检查bootargs中的console参数根文件系统挂载失败NFS配置错误、内核不支持NFS检查/etc/exports、防火墙、内核CONFIG_ROOT_NFS选项驱动加载失败设备树未匹配、依赖模块未加载用dmesg查看错误信息用lsmod查看模块依赖这张表里的问题我在实际开发中至少遇到过一半。特别是“内核启动后无输出”这一条有一次我调了一下午最后发现是设备树的加载地址和内核重叠了导致内核被覆盖。这种问题没有捷径只能根据串口打印的信息一步步缩小范围。6.2 驱动调试中的典型坑与解决思路驱动调试最让人头疼的是“没有报错但就是不工作”。比如加载了GPIO驱动insmod成功dmesg也有打印但LED就是不亮。这时候需要分步排查首先确认设备树节点是否被内核解析可以用ls /proc/device-tree/查看节点是否存在其次确认probe函数是否被调用可以在probe函数入口加printk然后确认GPIO号是否正确用cat /sys/kernel/debug/gpio查看GPIO状态最后确认硬件连接用万用表测量引脚电平。另一个常见的坑是内核版本和设备树语法的兼容性。比如旧版本内核用gpio gpio1 3 0这种写法新版本内核推荐用led-gpios gpio1 3 GPIO_ACTIVE_HIGH。如果语法用错了编译可能不报错但驱动匹配不上。解决办法是参考内核源码中Documentation/devicetree/bindings/目录下的示例或者直接看同类型驱动的设备树节点。还有一点容易被忽略内核配置。很多驱动依赖特定的内核配置选项比如I2C驱动需要CONFIG_I2Cy和CONFIG_I2C_CHARDEVySPI驱动需要CONFIG_SPIy和CONFIG_SPI_SPIDEVy。如果这些选项没打开驱动编译都过不了。建议在make menuconfig之后用make savedefconfig保存一个精简配置方便后续对比。6.3 交叉编译与部署环节的避坑清单交叉编译环节的坑主要集中在工具链和库的匹配上。我整理了一个清单供参考工具链前缀要和开发板架构匹配ARMv7用arm-linux-gnueabihf-ARMv8用aarch64-linux-gnu-。动态链接库要从工具链的sysroot中拷贝不能直接用主机的/lib目录。编译应用程序时-I和-L参数要指向工具链的include和lib目录避免误用主机的头文件和库。部署到板子上的可执行文件用file命令确认架构正确用ldd确认依赖库都能找到。如果程序在板子上报“Permission denied”检查文件是否有可执行权限用chmod x添加。如果报“No such file or directory”但文件明明存在大概率是动态链接器路径不对检查readelf -l中的interpreter字段。这些经验都是我在实际项目中一点点积累的书里可能不会全部覆盖但对于初学者来说提前知道这些坑可以省下大量时间。嵌入式Linux开发就是这样理论知识只是基础真正的能力是在一次次排查问题的过程中锻炼出来的。飞凌这本书的价值在于提供了一条经过验证的路径和一个可运行的平台剩下的就是动手实践了。
返回列表