OLEDMPU6050应用驱动编译
340.4.1 OLED 与 MPU6050 应用、驱动编译
KERN_DIR = /home/wfeng/Linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga
# ↑ 保存课程所用 Linux 4.1.15 内核源码目录;外部模块必须借助这套内核的 Kbuild 和构建产物编译。
all:
# ↑ 定义默认目标;在当前目录直接执行 make 时,GNU Make 会先执行这个目标。
make -C $(KERN_DIR) M=`pwd` modules ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
# ↑ 先切到内核目录,再把当前 drv 目录作为外部模块目录,以 32 位 ARM 工具链编译下面 obj-m 指定的模块。
clean:
# ↑ 定义清理目标;只有显式执行 make clean 时才会进入这里。
make -C $(KERN_DIR) M=`pwd` modules clean ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
# ↑ 调用同一套内核构建系统,清理当前外部模块目录中的编译产物。
rm -rf modules.order
# ↑ 按课程原文件再次删除模块排序文件;-f 让文件不存在时也不报错。
obj-m += oled_drv.o mpu6050_drv.o
# ↑ 要求 Kbuild 分别把 oled_drv.c 和 mpu6050_drv.c 构建为 oled_drv.ko 与 mpu6050_drv.ko。
源码边界说明: 页首只收录本节逐行讲解并实际执行的课程原版
Makefile,有效语句保持不变,只增加初学者注释。视频曾快速滚动预览app.c、mpu6050_drv.c、oled_drv.c和oledfont.h,但讲师明确把应用与驱动源码分析留到后续课程,因此本节不提前重复抄入这些完整文件,也不把后续设备树内容补进源码块。
本节要走通的是“识别硬件连接 → 编译应用 → 编译内核模块 → 放入 NFS 根文件系统 → 在开发板加载并运行”的第一遍操作流程。视频结尾只完成到模块能够被 insmod 加载;执行应用后实际得到 can't open /dev/oled,OLED 没有显示数据。这个失败现象是本节的真实结果,不应改写成“实验运行成功”。下一课才通过设备树补齐 I²C 设备信息并完成驱动匹配。
一、任务设计
1.1 本节处在整套课程的什么位置
前面的课程已经依次准备了:
- Linux 常用命令和文件操作;
- SSH、Samba、SCP 等主机与虚拟机之间的文件传输方法;
- ARM 交叉编译器、U-Boot、Linux 内核、设备树和根文件系统的编译方法;
- TFTP 下载内核和设备树、NFS 挂载根文件系统的网络启动环境。
本节第一次把这些基础设施串成一条应用与驱动调试链。讲师口中的“前面必须完全搞明白”,重点不是要求背下所有命令,而是能分清 Windows 主机、Ubuntu 开发主机、U-Boot 和开发板 Linux 分别在做什么,遇到路径、网络或文件错误时知道应在哪一层检查。
Windows 课程资料
│ Samba 复制源码
▼
Ubuntu 开发主机:编辑源码、交叉编译 app、借助内核源码编译 *.ko
│ 复制到 ~/nfs/oled_6050
▼
NFS 服务器导出的根文件系统
│ 开发板启动时挂载为 /
▼
开发板 Linux:insmod 模块 → 驱动匹配设备 → 生成 /dev 节点 → ./app 访问硬件
这里必须把“模块加载”和“驱动真正控制设备”分成两步:.ko 出现在 lsmod 中,只证明模块代码已经进入内核;只有设备与驱动匹配并成功执行 probe(),后续资源注册和 /dev 设备节点创建才会发生。
1.2 本节实验对象
实验使用迅为 i.MX6ULL 终结者开发板:
- 底板上已经焊接 MPU6050 六轴惯性传感器;
- 外接一块 0.96 英寸、四针 I²C OLED 模块;
- 应用程序读取 MPU6050 的三轴加速度和三轴角速度原始数据,经课程算法计算姿态角,再通过 OLED 驱动显示;
mpu6050_drv.ko与oled_drv.ko是两个独立的 I²C 内核模块,app是运行在用户空间的 ARM 可执行文件。
本节只概览代码用途,不分析算法和驱动框架的每个函数。尤其不能把“应用有 main() 和 while (1)”理解成它与单片机裸机程序完全相同:Linux 应用运行在用户空间,需要通过 open()、read()、ioctl() 等系统调用访问内核驱动,不能直接操作 I²C 控制器寄存器。
二、准备部分
2.1 课程源码目录
视频把课程提供的 app_drv 目录通过 Samba 复制到 Ubuntu 用户 wfeng 的家目录,终端实际看到两个子目录:
app_drv/
├── app/
│ └── app.c
└── drv/
├── Makefile
├── mpu6050_drv.c
├── oled_drv.c
└── oledfont.h
各文件分工如下:
| 文件 | 本节确认的作用 |
|---|---|
app.c | 用户空间应用;打开 OLED 和 MPU6050 设备节点,读取传感器数据并请求 OLED 显示 |
mpu6050_drv.c | MPU6050 的 I²C/字符设备驱动源码 |
oled_drv.c | OLED 的 I²C/字符设备驱动源码 |
oledfont.h | OLED 显示字符使用的字模数据,不是一个独立驱动 |
Makefile | 调用内核 Kbuild,把两个驱动源码分别编译成 .ko 模块 |
原笔记所说“驱动包含四个文件”指 drv 目录在编译前的四个课程文件,不代表一个驱动固定必须由四个文件组成,也不代表头文件会单独编译成模块。
2.2 开始实验前应确认
arm-linux-gnueabihf-gcc能在 Ubuntu 终端中直接执行。KERN_DIR指向自己实际编译过的课程内核源码目录,不能机械照抄/home/wfeng/...。- 该内核源码的配置和构建结果与开发板当前运行的内核相匹配;仅有一份未准备的源码压缩包通常不够构建可加载模块。
- Ubuntu 的
~/nfs已经通过 NFS 导出,并确实作为开发板的根文件系统使用。 - 开发板串口终端和网络都正常,能够登录开发板 Linux。
Linux 内核的外部模块构建文档把标准入口写成 make -C <kernel_dir> M=$PWD:-C 指定要使用的内核构建目录,M 告诉 Kbuild 外部模块源码在哪里。本节 Makefile 正是这一模式的课程写法。
三、应用部分的入门实验
3.1 硬件选择
视频准备了以下硬件:
| 硬件 | 作用 | 需要注意的地方 |
|---|---|---|
| i.MX6ULL 终结者开发板 | 运行 Linux、应用程序和两个驱动 | MPU6050 已在底板上,不需另接一个 MPU6050 模块 |
| 0.96 英寸 I²C OLED | 显示传感器原始值和姿态角 | 必须确认模块四针的实际顺序;不能只凭线色接线 |
| 2.00 mm 2P 转 2.54 mm 1P 母对母线 | 连接开发板 J8 与普通 OLED 排针 | 开发板端是 2.00 mm 双位外壳,OLED 端是独立的 2.54 mm 单针外壳 |
| 绝缘垫片 | 防止 OLED 背面的焊点碰到底板焊盘 | 视频临时垫了一张纸;长期使用宜固定好模块并保留可靠的绝缘间隙 |
接线前必须先断电。OLED 模块背面有裸露焊点,直接压在通电的开发板上可能把不应相连的网络短接;绝缘层还应避免滑动或被尖锐焊脚刺穿。
3.2 硬件接法
3.2.1 为什么 OLED 接到 I²C1
课程配套 ITOPMX6UL_TERMINATOR_V1_1.pdf 原理图第 12 页显示:底板 MPU6050 的 SCL、SDA 分别接到 I2C1_SCL、I2C1_SDA,供电网络为 DCDC_3V3。第 4 页又显示这两根 I²C1 信号已经复用到 GPIO 扩展插座 J8,因此外接 OLED 可以和板载 MPU6050 共用同一组 SCL/SDA。
I²C 总线允许多个从设备共用时钟线与数据线,设备通过地址区分;这不等于“把两个器件直接互相连接”。主控仍是 i.MX6ULL 的 I²C1 控制器,MPU6050 和 OLED 都是该总线上的从设备。Linux 官方文档也强调,I²C 本身不会像 PCI/USB 那样自动枚举全部设备,软件必须知道某条总线上有哪些设备及其地址,嵌入式平台通常通过设备树声明这些信息,见 I²C 设备实例化说明。这正是本节结尾加载模块仍不能使用设备的原因之一。
3.2.2 四根线与原理图针脚
按课程 V1.1 底板原理图,OLED 四根线应连接为:
| OLED 引脚 | J8 原理图针脚 | 底板网络 | 含义 |
|---|---|---|---|
GND | 4 | GND | 两块板的电位参考;没有共地,SCL/SDA 电平没有共同基准 |
VCC | 11 | DCDC_3V3 | 给 OLED 模块提供 3.3 V;本实验不要误接到 J8 的 VSYS |
SCL | 14 | I2C1_SCL,与 UART4_TXD 复用 | I²C 时钟线,由主控制器产生时钟 |
SDA | 15 | I2C1_SDA,与 UART4_RXD 复用 | I²C 双向数据线 |
原理图符号把 J8 左侧画成 1~10、右侧画成 20~11,这只是原理图的排版;真实 2×10 插座按每排两个连续针脚排列。以视频照片相同的观察方向、GAP/缺口定位标记在上方时,课程手绘图对应关系如下:
第 1 排:J8-1 J8-2
第 2 排:J8-3 J8-4 ← GND
第 3 排:J8-5 J8-6
第 4 排:J8-7 J8-8
第 5 排:J8-9 J8-10
第 6 排:J8-11 ← VCC(3.3 V) J8-12
第 7 排:J8-13 J8-14 ← SCL
第 8 排:J8-15 ← SDA J8-16
第 9 排:J8-17 J8-18
第10排:J8-19 J8-20
因此,视频口述的“第 2、6、7、8 排”与原理图的“4、11、14、15 号针脚”并不矛盾:前者数的是物理排,后者是电气针脚编号。如果把开发板翻面、旋转 180°,左右关系都会改变;实际接线应同时核对缺口方向、J8 编号和原理图网络,不能照抄“左边/右边”或导线颜色。
3.2.3 上电前检查
- 用万用表通断档确认选中的 GND 针脚确实与板上地相通。
- 上电前检查 VCC 与 GND 没有短路;首次上电可再测 J8-11 对 GND 是否约为 3.3 V。
- 核对 OLED 板自身的
GND/VCC/SCL/SDA丝印。有些模块的四针顺序不同,不能按外观猜测。 - 确保 SCL 接 SCL、SDA 接 SDA,并让 OLED 背面与底板保持绝缘。
四、编译应用和内核模块
4.1 编译应用
视频先故意使用 Ubuntu 本机的 gcc:
cd ~/app_drv/app
gcc app.c
链接器随后报告 sqrt、asin、atan2 等符号的 undefined reference。这些函数在 app.c 中用于姿态计算,声明来自 math.h,实际链接时还需要数学库 libm:
gcc app.c -lm
file a.out
-lm 不是“加载 math.h”。头文件负责在编译阶段提供函数声明,-l 选项在链接阶段搜索库;-lm 中的 m 表示数学库。GCC 链接选项文档还说明,库与目标文件按命令行顺序处理,因此这里把 -lm 放在 app.c 后面是稳妥写法。
这次虽然生成了 a.out,但视频中的 file a.out 明确显示它是 x86-64 文件,只能用于 Ubuntu 主机,不能拿到 32 位 ARM 开发板运行。讲师删除它后改用课程交叉编译器:
rm a.out
arm-linux-gnueabihf-gcc app.c -o app -lm
file app
视频最终输出的关键字段为 ELF 32-bit LSB executable, ARM, EABI5,并显示动态解释器 /lib/ld-linux-armhf.so.3:
arm-linux-gnueabihf-gcc:在 x86-64 Ubuntu 上生成 ARM hard-float 目标程序;app.c:输入源码;-o app:把输出文件命名为app,而不是默认的a.out;-lm:链接数学库,解决sqrt()、asin()、atan2()等引用;- “dynamically linked”:开发板根文件系统还必须具有与工具链 ABI 相容的动态装载器和运行库。
4.2 编译内核模块
进入驱动目录后,视频先确认编译前只有四个课程文件,再打开页首 Makefile:
cd ~/app_drv/drv
ls
vim Makefile
Makefile 的四个重点如下:
KERN_DIR必须改成自己的内核源码/构建目录。它不是随便一份系统头文件目录,而应对应开发板使用的那套内核。make -C $(KERN_DIR)让外层 Make 进入内核目录,使用内核 Kbuild 规则。M=`pwd`把当前drv目录传给 Kbuild,表示这里存放外部模块源码。反引号中的pwd会由 Shell 展开为绝对路径。ARCH=arm选择 ARM 架构;CROSS_COMPILE=arm-linux-gnueabihf-是工具名前缀,末尾连字符属于前缀本身,Kbuild 会据此调用相应的gcc、ld等工具。
obj-m += oled_drv.o mpu6050_drv.o 表示生成两个独立模块,而不是把两个 .o 合成一个模块。课程用它说明 obj-m 与 obj-y:在内核 Kbuild 语境中,obj-m 选择模块方式并生成 .ko,obj-y 通常把对象纳入内建目标;页首这个外部模块 Makefile 实际只写了 obj-m。Linux 内核的Kbuild Makefile 文档和外部模块文档均给出了相同的基本关系。
实际编译和清理命令是:
make
ls
make clean
ls
make
- 直接执行
make会选择第一个目标all,生成mpu6050_drv.ko、oled_drv.ko以及.o、.mod.c、Module.symvers、modules.order等中间文件。 make clean执行clean目标,清理由这套外部模块构建产生的文件;随后再次make可重新生成模块。- 视频编译过程出现了
oled_detect未使用、忽略copy_from_user()返回值等 warning,但链接阶段仍生成了两个.ko。警告不能等同于编译失败,也不应被当成“代码完全没有问题”;本节保持课程源码原样,后续分析驱动时再讨论这些代码风险。
4.3 应用和驱动的运行条件
三类文件不能混为一谈:
| 产物 | 运行/加载位置 | 用途 |
|---|---|---|
x86-64 a.out | Ubuntu 开发主机 | 本节仅用来证明 -lm 可以解决链接符号;不能在开发板运行 |
ARM app | 开发板 Linux 用户空间 | 通过设备节点调用驱动 |
ARM 内核模块 *.ko | 开发板 Linux 内核空间 | 注册 I²C 驱动和字符设备操作 |
应用和模块都在 Ubuntu 中交叉编译,再通过 NFS 根文件系统让开发板看到。课程串口登录用户名为 root、密码为课程镜像预设值;密码属于特定教学镜像配置,不是 Linux 的通用默认密码,也不应在真实设备继续使用弱口令。
五、应用和驱动编译
5.1 在 NFS 根文件系统中准备目录
视频进入 Ubuntu 的 ~/nfs 后,第一次输入了:
mkdir oled&6050
这不是“文件名绝对不能包含 &”,而是 未加引号的 & 在 Shell 中具有后台运行含义。Shell 把命令解析成后台执行 mkdir oled,随后又把 6050 当成另一条命令,所以出现 6050: command not found;重复操作时还出现 mkdir: cannot create directory 'oled': File exists。课程随后改用不需要转义的下划线名称:
cd ~/nfs
mkdir oled_6050
理论上可用引号或反斜杠创建含 & 的目录,但这会让以后每条命令都需要正确转义;教学和工程目录采用 oled_6050 更清楚。
5.2 复制 ARM 应用和两个模块
视频中的实际目标目录是 ~/nfs/oled_6050/:
cd ~/app_drv/app
pwd
cp app ~/nfs/oled_6050/
cd ../drv
cp *.ko ~/nfs/oled_6050/
cd ~/nfs/oled_6050
ls
最终应看到三个要在开发板使用的文件:
app mpu6050_drv.ko oled_drv.ko
原笔记中的 cp ../app、cp . ./app 不是视频最终命令,也不能表达这里的复制关系。cp *.ko ... 中的 * 由 Shell 展开为当前目录里的两个 .ko 文件。
视频还确认并删除了先前误创建的空目录 ~/nfs/oled。画面使用 rm -R oled;实际操作前必须先用 pwd、ls -la oled 核对路径和内容。如果目录确定为空,rmdir oled 更能防止误删非空目录。
5.3 NFS 为什么能让开发板立即看到文件
这里不是 Ubuntu 与开发板各保存一份文件后再自动“同步”。前面配置的启动参数让开发板把 Ubuntu 导出的 ~/nfs 直接挂载为根文件系统 /,所以:
- Ubuntu 中的
~/nfs/oled_6050/app; - 开发板中的
/oled_6050/app
是服务器端同一份文件经 NFS 呈现出的两个访问视角。Linux 内核的 NFS 根文件系统文档说明了 root=/dev/nfs 和 nfsroot=... 的作用。若开发板本次实际从 eMMC 根文件系统启动,而没有挂载这份 NFS 根目录,Ubuntu ~/nfs 中的改动就不会自动出现在开发板 / 下。
六、应用和驱动网络挂载
6.1 在开发板上加载模块
开发板通过 NFS 根文件系统启动并登录后,视频在串口终端执行:
cd /oled_6050
ls
insmod mpu6050_drv.ko
insmod oled_drv.ko
lsmod
画面中先后打印:
mpu6050_driver_init
oled i2c_driver was added into the system.
lsmod 也列出了 oled_drv 和 mpu6050_drv。这些信息只能证明两个模块的入口函数运行、I²C 驱动结构已经注册到内核;它们没有证明驱动已经匹配到实际设备,更没有证明 OLED 已经完成初始化。
6.2 本节最终报错及真实原因
视频最后执行:
./app
实际输出是:
can't open /dev/oled
结合课程配套源码可以准确解释这条错误:
app.c首先调用open("/dev/oled", O_RDWR)。- 打开失败后立即打印上述信息并
return -1,所以本次还没有继续打开 MPU6050 设备节点。 oled_drv.c只有在 OLED 设备与驱动匹配、oled_probe()被调用后,才会执行device_create(..., "oled"),由课程系统中的设备管理机制创建/dev/oled。- 本节日志只有模块入口打印,没有出现源码中的
oled match successed、create device file 'oled' succeed!,说明probe()尚未执行。 - 下一课确认根因是活动设备树尚未声明并启用课程所需的 I²C 设备节点,因此模块虽然位于
lsmod列表中,驱动却没有绑定到设备。
Linux 驱动模型的绑定说明同样说明:注册设备或驱动会触发匹配,匹配成功后才调用驱动的 probe()。因此“insmod 没报错”和“设备可以被应用打开”是两个不同验收点。
6.3 初学者排查顺序
遇到本节现象时,可以按层检查,而不是反复执行 insmod:
lsmod # 模块代码是否已加载
dmesg | tail -n 50 # 是否出现 match/probe 或错误日志
ls -l /dev/oled /dev/I2C1_mpu6050 # 应用要打开的设备节点是否存在
ls /sys/bus/i2c/devices # 内核当前实例化了哪些 I²C 设备
file /oled_6050/app # 应用是否确为 ARM 可执行文件
判断关系是:
模块加载成功
≠ 驱动与设备匹配成功
≠ probe() 初始化成功
≠ /dev 节点存在
≠ 应用读写和 OLED 显示成功
本节只验证到第一层。设备树修改、I²C 地址、匹配成功后的运行效果属于下一课,不能把下一课的成功结果倒填成本节已经完成的现象。
七、本节容易混淆的术语与结论
| 转录或原笔记表述 | 精确含义 |
|---|---|
| “MP6050” | 正确器件名为 MPU6050 |
| “F1C”“iPhone C” | 都是语音识别错误,正确为 I²C |
| “设备数” | 正确为“设备树” |
| “更文件系统” | 正确为“根文件系统” |
| “TWTP” | 正确为 TFTP;它负责下载启动文件,不负责挂载根文件系统 |
| “JCC” | 正确为 gcc |
| “QO 文件” | 正确为 .ko 内核模块文件 |
| “伺服设备驱动框架” | 本节源码采用 Linux I²C 驱动和字符设备框架,不是“伺服” |
-lm 加载 math.h | math.h 提供声明,-lm 在链接阶段加入数学库 |
| NFS 自动同步两套文件 | 开发板把服务器目录挂成根文件系统,双方看到的是同一服务器文件 |
lsmod 能看到就表示驱动成功 | 只表示模块已加载;还需检查设备匹配、probe()、设备节点和真实读写 |
完成本节后,应该能够说清楚三个边界:应用与驱动运行在不同空间;主机程序与 ARM 程序面向不同体系结构;模块加载与驱动绑定不是同一件事。