IMX6ULL嵌入式Linux开发踩坑实录:从启动到驱动的bug排查 我正式转到嵌入式Linux方向的时候身边不少朋友都建议先从IMX6ULL下手。这颗芯片的定位很明确入门级能跑Linux的ARM处理器主频不高资料全很多开发板的文档能把启动、内核、文件系统这几件事串起来。我听了劝买了一块主流的IMX6ULL板子结果还没点灯就被一连串bug折腾得够呛。这篇博客就是我学习IMX6ULL时记录的bug合集包含现象、排查过程、最终定位到的原因和具体解决方式。如果你也在这块板子上踩坑可以直接对着目录找你能对号入座的那一段。1. 环境搭建阶段先把基本盘打牢1.1 开发板和软件环境是怎么配的我手头这块IMX6ULL板子的核心配置是ARM Cortex-A7主频528MHz内存256MB DDR3存储上带8GB eMMC和一个TF卡座网络接口有一个10M/100M以太网口另外板载一个UART0串口用作调试台LCD接口可以接7寸RGB屏。这类配置在入门级板卡里非常常见内存不大但跑一个裁剪后的Linux系统、运行几个应用进程绰绰有余。软件方面我是这样准备的在笔记本上跑一个Linux虚拟机作为编译服务器交叉编译工具链按开发板给的约定放在固定目录源码分三块引导程序源码、内核源码、根文件系统通过一键脚本构建。调试阶段采用两种启动方式第一种是把编译好的引导程序直接烧进SD卡由板子从TF卡启动第二种是网络启动引导程序通过TFTP从主机下载内核和设备树根文件系统通过NFS挂载到主机目录。这套组合是我实际用下来比较顺手的方案。SD卡烧录适合验证引导程序本身能不能跑网络启动则解决了每次改内核都要重新烧卡的痛点。前期把这两条路都打通的话后面调试效率会高很多。1.2 交叉编译工具链带来的第一个大坑第一次编译引导程序我犯了一个很基础的错误。直接把源码解压到虚拟机共享目录执行make以后就开始等结果。编译过程没有报错输出文件也都生成了但烧到SD卡里插到板子上串口一个字符都没有。我反反复复查硬件最后才想起用file命令去看生成的二进制文件的属性。结果清清楚楚写着这是x86架构的可执行文件不是ARM架构。原因是当时共享环境变量里工具链路径没有生效make脚本调用了宿主机的gcc来编译整个工程。这种错误在编译阶段不一定会立刻暴露有些纯逻辑的代码用哪个架构编译都能过但是启动后CPU一执行第一条指令就崩了。正确做法是编译之前先确认环境变量确实指向交叉工具链然后执行make distclean清掉这次错误产物再重新编译。每次编译完用file命令确认一下生成的引导程序和内核镜像确实是ARM架构。这看起来是个笨办法但能省掉很多莫名其妙的启动问题。2. 引导程序阶段启动不了、存不住、下载不下来2.1 上电后串口一行字都不输出这个现象应该是最常见也最打击人的。板子刚到手串口接好打开串口终端上电屏幕一直空白。我心里第一反应是板子坏了后来排查下来绝大多数情况都是下面几个原因之一。排查顺序建议固定为“供电、启动模式、串口接线、终端设置”。先看板上电源指示灯是否正常用万用表量核心板电压排除供电不足。再看启动模式拨码开关IMX6ULL支持从SD卡、eMMC、USB等设备启动拨码拨错了ROM里的启动代码就不知道去哪找引导程序。接着检查串口接线板子的调试串口一般是3.3V TTL电平如果用USB转TTL模块TXD和RXD必须交叉连接板子的发送接模块的接收板子的接收接模块的发送。我有一半的“板子不输出”案例都是因为收发接反了。最后检查终端软件设置波特率1152008位数据位1位停止位无校验无硬件流控。有些终端软件默认启用了RTS/CTS流控这会导致接收端不发送数据看起来就像没有输出。这四步按顺序过一遍大部分问题都能解决。2.2 环境变量保存了重启后又回到老样子系统跑起来以后我开始调整引导参数用setenv命令修改bootargs执行saveenv一切正常。结果重启以后修改的变量全部消失恢复成出厂默认值。我反复试了几次都一样。后来定位到问题出在环境变量的存储位置和实际存储介质布局不匹配。IMX6ULL支持从多种介质启动而引导程序在编译时通过配置项指定了环境变量存在哪里以及偏移多少。我这块板子使用的是eMMC但编译引导程序时用的默认配置是NAND介质的偏移导致saveenv名义上写成功了实际上写到eMMC上一个不存在的地址段重启后自然读不到。解决办法是查看开发板手册里关于存储分区表的信息确认环境变量应该放在eMMC的哪个分区、偏移是多少然后重新配置引导程序里的环境变量偏移参数重新编译烧录。这里有个经验拿到一块新板子以后第一步应该是把原厂引导程序的配置里关于存储介质、环境变量偏移的部分全部看一遍不要等到出问题了才去翻。2.3 TFTP下载内核镜像反复超时引导程序起来以后我想通过网络下载内核镜像这样就不用频繁拔SD卡了。结果在引导程序控制台里执行tftp命令要么一直等待要么下到一半报超时。最奇怪的是板子和主机之间ping是通的所以网络物理链路和IP配置都没问题。问题出在几个地方。首先是主机的防火墙拦截了TFTP的UDP端口ping用的ICMP不会被拦但TFTP流量会直接被丢弃。其次如果TFTP服务配置的目录权限不对服务端会返回错误码表现为传输中断。还有一个容易忽略的原因就是把板子和主机都接到了路由器上中间经过了一层交换设备一些老旧交换机会对UDP广播做特殊处理导致TFTP不稳定。我的解决方式是把板子和主机用网线直连两边都配置固定IP关掉主机防火墙然后确认TFTP服务根的目录里确实有内核镜像和设备树文件。直连的好处是排除交换机干扰排查范围一下缩小了很多。TFTP稳定以后配合NFS根文件系统基本就不需要频繁插拔SD卡了。2.4 引导程序卡在“Starting kernel ……”这个问题非常经典。引导程序已经把内核镜像加载到内存执行启动命令以后屏幕输出“Starting kernel ……”就再也没有后续了。第一次遇到的时候我以为是内核死掉后面才发现其实引导程序压根没有把控制权顺利交到内核手里。最常用的排查方式是确认内存中各镜像的加载地址是否合理。IMX6ULL的内存起点是0x80000000引导程序把内核镜像加载到0x80800000、把设备树加载到0x83000000是常见做法。问题是内核镜像解压时可能会覆盖到设备树所在的内存区域尤其是当设备树加载地址紧挨着内核镜像尾部的时候。解决办法是拉大两者之间的地址间隔或者把设备树加载到更高的内存位置。还有一个容易被忽略的点就是设备树文件本身是否正确加载。可以在引导程序控制台里查看设备树地址处的内存内容确认文件头标记是否存在。如果设备树损坏或者格式不对引导程序不会明确报错而是直接卡死。出现这类卡死问题时不要急着怀疑内核源码先确认镜像加载地址和设备树有效性这两个点排干净以后再往内核方向查。3. 内核与设备树调试笔记3.1 引脚明明配置了GPIO就是不受控在设备树里给一个LED灯配置了GPIO引脚系统起来以后对应的LED控制节点出现在文件系统里但通过echo命令操作这个节点灯没有任何反应仔细一看寄存器状态输出电平根本没变。这个问题要从IMX6ULL的引脚复用机制说起。芯片的每个引脚都有多种功能比如同一个引脚可以当作GPIO也可以当作UART发送脚还可以当作I2C时钟脚。到底启用哪个功能由IOMUX控制寄存器决定。设备树里不仅要把某个引脚设置为GPIO模式还要配置引脚的上下拉、驱动强度和速率。如果设备树里没有正确配置引脚所属的pinctrl节点即使GPIO控制器的号码写对了引脚还是停留在复位后的默认功能上。排查时我在内核文件系统的调试接口下查看了当前引脚的复用状态发现它仍然被分配给UART功能。然后在设备树源码里检查pinctrl节点发现漏写了这个引脚的配置。补上对应的复用配置以后LED控制恢复正常。这里有个体会修改IO相关的设备树节点时一定要先确认这个引脚有没有被其他外设占用一块板子上UART、I2C、SPI、PWM、GPIO争用同一个引脚的情况非常普遍。3.2 驱动一访问寄存器内核就报Unable to handle kernel paging request自己在写一个简单的字符驱动时按照芯片手册上的寄存器地址直接去访问硬件寄存器。编译没问题加载驱动后一操作设备文件系统直接打印出一段“Unable to handle kernel paging request”的报错然后整个系统卡死。当时我很不理解单片机时代不都是直接读写寄存器地址吗为什么到了Linux里就不行。原因是Cortex-A7处理器运行Linux时MMU已经开启CPU发出的地址都会经过页表翻译。我手写的那个地址是物理地址内核没有建立对应的虚拟地址映射所以一访问就触发缺页异常。在Linux驱动里访问硬件寄存器不能直接使用物理地址必须先用ioremap申请一段虚拟地址映射到物理地址再用readl、writel这样的函数去读写。找到问题以后我在驱动初始化函数里对寄存器物理地址做了固定长度的映射并且检查映射返回值同时用ioread32和iowrite32替换了原来的直接指针访问。重新编译加载问题消失。这个问题的根源其实是裸机编程和Linux驱动开发的思维差异很多从单片机转过来的人都会在这里栽一次。3.3 LCD屏幕花屏、偏移、颜色错乱接上7寸RGB屏后背光亮了但画面上显示的图像位置整体偏移甚至出现花屏和颜色错乱。我当时的第一反应是屏坏了或者是排线接触不良。实际上问题出在设备树里LCD时序参数配置。RGB屏显示一帧图像需要扫描完一整行再扫下一行每一行的有效像素之后要留出一段“消隐”时间让LCD控制器和面板同步。设备树里配置了水平有效像素、水平前肩、水平后肩、水平同步脉冲宽度垂直方向也有对应的“同样”参数。这些参数和面板规格不一致画面就会偏移偏移得厉害就直接花了。还有一个很容易忽略的配置是像素时钟频率。屏幕显示所需的像素时钟要根据分辨率和刷新率计算如果时钟太高或太低画面会异常。比如1024x600的屏像素时钟大约是51.2MHz附近还要根据行场消隐参数做调整。我照着屏幕规格书把每个参数都核对了一遍画面位置就正常了。颜色错乱是另一条线的问题。IMX6ULL的LCD控制器支持多种RGB格式比如24位真彩、16位565格式。如果控制器输出的数据格式和面板期望的不一致就会出现颜色明显不对红色显示成蓝色之类的现象。检查并统一设备树里的位深配置和面板接口格式以后颜色正常了。遇到这类问题先看时序再看格式别急着怀疑屏坏了。4. 根文件系统和应用程序能启动才算真正入门4.1 NFS挂载根文件系统失败内核起来以后最后一步是把根文件系统挂载上来。我使用NFS方式时系统启动到一半报出挂载失败进不了控制台。日志显示“VFS: Unable to mount root fs”后面跟着一行网络相关错误。NFS挂载失败要分两层来排查。第一层是网络层面板子有没有拿到IP地址、能不能ping通NFS服务器这在引导参数里配置的IP获取方式有关。第二层是服务器配置层面NFS导出目录是否允许板子访问。我检查了主机上/etc/exports文件里面声明的导出目录确实允许读写但服务没有重新加载配置所以修改没有生效。执行导出命令刷新以后板子就能挂载上了。还有一个隐蔽的坑就是内核配置里没有打开NFS客户端和NFS根文件系统支持。有些初学者为了减小内核体积裁剪配置时把网络文件系统相关的驱动全关了内核起来以后自然挂不上NFS。这个问题在编译内核时就要注意裁剪配置的时候保留NFS相关选项否则后面折腾服务器配置都是白费功夫。4.2 交叉编译的程序放到板子上提示not found在宿主机上用交叉编译器编译了一个最简单的hello程序通过NFS直接运行结果提示not found。我当时很疑惑明明程序就在那里权限也对为什么找不到。这个not found不是文件不存在而是动态链接器找不到。交叉编译出来的程序默认使用目标板的动态链接库程序启动时内核会去指定路径加载动态链接器通常是/lib/ld-linux-armhf.so.3。如果板子的根文件系统里没有这个文件程序就会报出not found。排查时用交叉工具链里的readelf命令查看程序的interpreter字段可以看到它依赖的动态链接器和路径。解决办法有两种第一种是把工具链sysroot目录里对应的动态链接器复制到板子根文件系统对应的路径下顺便把依赖的共享库也复制过去第二种是编译时加-static参数做静态链接生成的程序不再依赖动态库文件体积会大一些但省去复制库的麻烦。我后来检查了一下发现这是因为构建根文件系统时只拷贝了基础命令没有把工具链运行库部分放进去。重新构建根文件系统时加入了工具链的sysroot目录后续所有交叉编译的程序都能直接运行。4.3 GUI应用启动时报缺少动态库在板子上跑一个GUI程序程序配置好以后启动控制台输出一连串“cant load library”的报错后面跟着一排.so文件名。我看得出来这是动态库路径找不到但奇怪的是检查系统目录那个库文件明明就在。问题出在动态链接器的搜索路径上。程序编译时链接库的搜索路径和程序运行时动态链接器搜索库的路径不一定一致尤其是交叉编译环境下库文件虽然被复制到了板子但搜索路径里没有包含那个目录。解决办法是在启动脚本里通过环境变量指定库搜索路径把库目录加进去。另外GUI程序还需要指定显示后端常见的是Linux帧缓冲后端需要在启动时通过环境变量指定帧缓冲设备。我当时两个因素叠加在一起同时指定了库路径和显示后端之后程序正常跑起来了。这类问题多试几次就能总结出规律先看报错里缺哪些.so逐个复制到统一目录再设置LD_LIBRARY_PATH。5. 调试工具和平时排查的习惯5.1 把日志级别调到最大调试内核驱动的时候最痛苦的事情之一是系统起来以后其他日志刷屏自己想看的打印信息却被淹没。IMX6ULL上内核日志按照级别过滤只有级别足够高的日志才会输出到控制台。有时候驱动里的打印函数用默认级别控制台偏偏不显示。解决办法是在引导参数里加loglevel7把控制台日志级别调成所有都显示。如果系统已经跑起来也可以直接修改内核printk参数或者执行echo 8 /proc/sys/kernel/printk把当前控制台级别设为最高。这个方法看似基础但能立刻看到驱动里的所有打印排查问题时很管用。还有一个细节是确认设备树里控制台串口的输出路径配置正确也就是说内核启动时的printk往哪个串口发送。如果这个配置不对日志可能被送到一个没接线的口上你在这边等半天什么都等不到。5.2 用逻辑分析仪而不是靠猜软件层排查不出来的时候我会接一个逻辑分析仪去抓物理层面的信号。比如某个GPIO点灯驱动看起来配置都对但就是没输出先用逻辑分析仪抓这个引脚的波形就能确认软件有没有真正把电平拉低。同样的方法也适合排查I2C、SPI、UART这类总线通信问题。用逻辑分析仪有两点要注意。第一是探头的参考地和目标板的地必须共地否则抓到的信号全是乱的。第二是采样率要至少高于信号频率的4倍最好到10倍以上不然波形失真根本看不出正常时序。我遇到过抓串口数据时采样率设置太低波形上看全是毛刺误以为是硬件损坏调高采样率以后发现实际数据完全正常。逻辑分析仪的价值在于把软件层和硬件层隔开一旦抓到了实际波形就能明确问题是出在软件逻辑还是硬件连接。这一点在嵌入式调试里非常关键。5.3 设备树和内核配置一定要用版本管理我调试过程中踩过最深的坑往往是改了一处配置过几天忘了改过什么出问题以后回不到之前的状态。后来我强制自己把设备树源码和内核配置文件都放进版本管理仓库每次修改提交时写下改动说明用来定位问题非常高效。另外每次启动时的日志我也会保存下来按日期和文件名标记。同样的报错信息不同时间出现可能原因不一样。有日志做对照能很明显地看到改动前后的差异。最后是改设备树之前先对当前能正常工作的版本做备份新配置出问题就立刻回退。这个方法让我排除了很多变量。最后说一点我自己的感受。这些bug现在看都不复杂但当时每一个都花了不少时间。真正让它们变得“不复杂”的不是我记忆力有多好而是我习惯了在动手之前先记录状态在排查过程中保留每一步的痕迹。如果你也在折腾IMX6ULL建议从今天开始给每一个启动日志存个档给每一次设备树修改留个记录。等到踩坑越来越多你会感谢这些当时觉得麻烦的笔记。