OLEDMPU6050应用驱动编译

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 1 Likes: 0 Comments: 0

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.cMPU6050 的 I²C/字符设备驱动源码
oled_drv.cOLED 的 I²C/字符设备驱动源码
oledfont.hOLED 显示字符使用的字模数据,不是一个独立驱动
Makefile调用内核 Kbuild,把两个驱动源码分别编译成 .ko 模块

原笔记所说“驱动包含四个文件”指 drv 目录在编译前的四个课程文件,不代表一个驱动固定必须由四个文件组成,也不代表头文件会单独编译成模块。

2.2 开始实验前应确认

  1. arm-linux-gnueabihf-gcc 能在 Ubuntu 终端中直接执行。
  2. KERN_DIR 指向自己实际编译过的课程内核源码目录,不能机械照抄 /home/wfeng/...。
  3. 该内核源码的配置和构建结果与开发板当前运行的内核相匹配;仅有一份未准备的源码压缩包通常不够构建可加载模块。
  4. Ubuntu 的 ~/nfs 已经通过 NFS 导出,并确实作为开发板的根文件系统使用。
  5. 开发板串口终端和网络都正常,能够登录开发板 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 原理图针脚底板网络含义
GND4GND两块板的电位参考;没有共地,SCL/SDA 电平没有共同基准
VCC11DCDC_3V3给 OLED 模块提供 3.3 V;本实验不要误接到 J8 的 VSYS
SCL14I2C1_SCL,与 UART4_TXD 复用I²C 时钟线,由主控制器产生时钟
SDA15I2C1_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 的四个重点如下:

  1. KERN_DIR 必须改成自己的内核源码/构建目录。它不是随便一份系统头文件目录,而应对应开发板使用的那套内核。
  2. make -C $(KERN_DIR) 让外层 Make 进入内核目录,使用内核 Kbuild 规则。
  3. M=`pwd` 把当前 drv 目录传给 Kbuild,表示这里存放外部模块源码。反引号中的 pwd 会由 Shell 展开为绝对路径。
  4. 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.outUbuntu 开发主机本节仅用来证明 -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

结合课程配套源码可以准确解释这条错误:

  1. app.c 首先调用 open("/dev/oled", O_RDWR)。
  2. 打开失败后立即打印上述信息并 return -1,所以本次还没有继续打开 MPU6050 设备节点。
  3. oled_drv.c 只有在 OLED 设备与驱动匹配、oled_probe() 被调用后,才会执行 device_create(..., "oled"),由课程系统中的设备管理机制创建 /dev/oled。
  4. 本节日志只有模块入口打印,没有出现源码中的 oled match successed、create device file 'oled' succeed!,说明 probe() 尚未执行。
  5. 下一课确认根因是活动设备树尚未声明并启用课程所需的 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.hmath.h 提供声明,-lm 在链接阶段加入数学库
NFS 自动同步两套文件开发板把服务器目录挂成根文件系统,双方看到的是同一服务器文件
lsmod 能看到就表示驱动成功只表示模块已加载;还需检查设备匹配、probe()、设备节点和真实读写

完成本节后,应该能够说清楚三个边界:应用与驱动运行在不同空间;主机程序与 ARM 程序面向不同体系结构;模块加载与驱动绑定不是同一件事。