1. 课程导学与环境准备:I2C协议基础回顾、Linux I2C子系统架构概览、开发板选型与交叉编译环境搭建

各位同学,欢迎来到《Linux驱动移植I2C传感器数据采集项目实战》的第一章。

做嵌入式Linux驱动开发,说白了就是让内核认识你的硬件,并且把数据乖乖交到用户空间。I2C传感器是这里面最常见的一类——温度、湿度、气压、加速度计,几乎每个物联网项目都离不开它们。

我个人习惯,在动手写代码之前,先把三件事理清楚:协议怎么跑、内核怎么管、板子怎么搭。这三件事搞定了,后面移植驱动就是水到渠成的事。

1.1 I2C协议基础回顾

I2C协议,1982年由Philips(现在的NXP)发明。你想想看,一个40多年前的协议,到现在还在大量使用,说明它确实够简单、够实用。

I2C只有两根线:SCL(时钟线)和SDA(数据线)。所有设备都挂在这两根线上,通过地址区分谁跟谁说话。

核心要点:I2C是主从结构,总线上的主设备(通常是SoC)发起通信,从设备(传感器)响应。每个从设备有一个7位或10位地址,同一总线上不能有重复地址。

一次完整的I2C传输包含以下几个阶段:

  1. 起始条件:SCL高电平时,SDA从高变低
  2. 发送从设备地址+读写位:7位地址+1位读写标志(0写1读)
  3. 等待应答:从设备拉低SDA表示ACK
  4. 数据传输:每8位数据后跟一个ACK/NACK
  5. 停止条件:SCL高电平时,SDA从低变高

我在项目中遇到过一个问题:某个温湿度传感器偶尔读不到数据,示波器一抓发现是起始条件时序不对。嗯,硬件上的小偏差,软件上就得靠重试机制来兜底。

参数 标准模式 快速模式 高速模式
最大时钟频率 100 kHz 400 kHz 3.4 MHz
最大总线电容 400 pF 400 pF 100 pF
上拉电阻典型值 4.7 kΩ 2.2 kΩ —

避坑指南:我曾经在调试一块板子时,I2C总线一直卡在忙状态。查了半天,发现是上拉电阻焊错了——用了10kΩ,总线上升沿太慢。换成4.7kΩ后一切正常。所以,硬件上拉电阻别随便选,手册上写的值是有道理的。

1.2 Linux I2C子系统架构概览

Linux内核的I2C子系统,分三层。我习惯从下往上理解:

  • I2C控制器驱动:最底层,直接操作SoC的I2C硬件寄存器。比如STM32MP157的I2C控制器驱动,负责收发数据、处理中断。
  • I2C核心层:中间层,提供统一的API和数据结构。它管理总线、设备、驱动的注册与匹配。
  • I2C设备驱动:最上层,我们写的就是这一层。比如BMP280气压传感器驱动,通过核心层提供的接口跟控制器驱动打交道。

说白了,你写一个I2C传感器驱动,就是实现一个i2c_driver结构体,注册到核心层。核心层帮你搞定跟硬件通信的细节。

注意:很多初学者搞不清i2c_adapter和i2c_client的区别。简单记:adapter是控制器(硬件),client是从设备(传感器)。一个adapter可以挂多个client。

下面这张图,是我画的本课程知识体系结构,你可以先有个整体印象:

Linux I2C 传感器驱动开发知识体系 用户空间应用 I2C 设备驱动(本章重点) I2C 核心层(i2c-core) I2C 控制器驱动(硬件相关) I2C 传感器硬件 本课程覆盖 • 驱动框架 • 设备树配置 • 数据读写 • 中断处理 • 调试技巧 • 实战移植 • 性能优化 • 问题排查

1.3 开发板选型与交叉编译环境搭建

选开发板,我建议你关注三点:芯片资料是否齐全、社区支持是否活跃、I2C外设是否丰富。

本课程选用STM32MP157作为主控芯片,搭配米尔科技MYD-YA157C开发板。为什么选它?

  • 双核Cortex-A7 + Cortex-M4,跑Linux毫无压力
  • 芯片手册和参考手册公开,不用签NDA
  • 有至少4个I2C控制器,足够挂各种传感器
  • 社区资料丰富,遇到问题容易搜到答案

传感器方面,我们选BMP280(气压+温度)和SHT30(湿度+温度)。这两个芯片都是I2C接口,数据手册清晰,驱动移植难度适中,非常适合练手。

硬件清单:

  • MYD-YA157C开发板 × 1
  • BMP280模块 × 1(I2C地址 0x76 或 0x77)
  • SHT30模块 × 1(I2C地址 0x44 或 0x45)
  • 杜邦线若干
  • USB转TTL串口模块 × 1(用于调试)

1.4 交叉编译环境搭建

交叉编译,说白了就是在你的PC上编译出能在ARM开发板上运行的程序。PC是x86架构,开发板是ARM架构,编译器得知道目标平台是谁。

我推荐用Linaro GCC工具链,版本选gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf。这个版本稳定,跟内核5.4配合得很好。

搭建步骤:

  1. 下载工具链:从Linaro官网或镜像站下载
  2. 解压到目录:比如/opt/toolchains/
  3. 设置环境变量:把工具链的bin目录加到PATH里
  4. 验证安装:运行arm-none-linux-gnueabihf-gcc --version
# 下载工具链(以Ubuntu 20.04为例)
wget https://releases.linaro.org/components/toolchain/binaries/9.2-2019.12/arm-linux-gnueabihf/gcc-linaro-9.2.1-2019.12-x86_64_arm-linux-gnueabihf.tar.xz

# 解压
sudo tar -xvf gcc-linaro-9.2.1-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt/toolchains/

# 设置环境变量(建议写到 ~/.bashrc)
export PATH=$PATH:/opt/toolchains/gcc-linaro-9.2.1-2019.12-x86_64_arm-linux-gnueabihf/bin

# 验证
arm-linux-gnueabihf-gcc --version

个人经验:我习惯把工具链路径写到~/.bashrc里,而不是每次手动export。另外,建议用which arm-linux-gnueabihf-gcc确认一下路径对不对。有一次我装了两个版本的工具链,结果编译时用的是旧版,折腾了半天才发现。

内核源码方面,我们使用Linux 5.4.31,这是STM32MP157官方BSP对应的内核版本。下载后配置交叉编译:

# 设置环境变量
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-

# 配置内核(使用默认配置)
make stm32mp1_defconfig

# 编译内核
make -j4 uImage LOADADDR=0xC2000040

# 编译设备树
make -j4 dtbs

注意:编译内核时,LOADADDR参数必须跟你的U-Boot配置一致。我曾经因为忘了设这个参数,导致内核无法启动,卡在"Starting kernel ..."那里。嗯,这种低级错误犯过一次就不会再犯了。