第二章 开发环境搭建:嵌入式Linux开发环境搭建与传感器驱动基础

好,咱们直接进入正题。这一章要解决的核心问题就一个——怎么让你的电脑跟嵌入式板子“对上话”。说白了,就是搭个能让代码跑在ARM芯片上的环境。

我刚开始做嵌入式Linux时,光搭环境就折腾了两天。后来发现,其实就三件事:交叉编译工具链、根文件系统、还有驱动开发的基础知识。咱们一个一个来。

2.1 交叉编译工具链——为什么不能直接在板子上编译?

你可能会问:我电脑上装个gcc不就能编译吗?嗯,问题在于——你的电脑是x86架构,而嵌入式板子通常是ARM架构。两种CPU的指令集完全不同。

交叉编译,说白了就是:在x86电脑上编译出能在ARM上运行的二进制文件

核心概念:交叉编译工具链 = 编译器 + 链接器 + 库文件 + 调试工具

我个人习惯用Linaro提供的工具链,稳定且社区活跃。以ARM Cortex-A7为例,安装步骤其实很简单:

# 下载ARM交叉编译工具链
wget https://releases.linaro.org/components/toolchain/binaries/latest-7/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz

# 解压到指定目录
tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt/

# 配置环境变量
export PATH=$PATH:/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin
export CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH=arm

这里有个坑——环境变量一定要写到.bashrc里,不然每次开终端都得重新export。我曾经因为这个浪费了半小时,后来学乖了。

小技巧:验证工具链是否安装成功,可以运行 arm-linux-gnueabihf-gcc --version。看到版本号就说明OK了。

2.2 根文件系统——板子启动后能看到什么?

交叉编译完内核和驱动后,板子还得有个“家”——这就是根文件系统。它决定了板子启动后你能看到哪些目录、哪些命令。

我建议初学者直接用Buildroot来生成文件系统。它能把工具链、内核、文件系统打包在一起,省心不少。

组件 说明 我的建议
BusyBox 提供常用Linux命令(ls、cp、vi等) 必选,选最新稳定版
glibc / uClibc C运行库 存储够用选glibc,空间紧张选uClibc
设备节点 /dev目录下的设备文件 用udev或mdev动态管理
启动脚本 /etc/init.d/下的初始化脚本 按需配置,别贪多

用Buildroot生成文件系统,核心配置就几步:

# 下载Buildroot
git clone https://git.buildroot.net/buildroot
cd buildroot

# 配置目标架构
make menuconfig
# 选择 Target options -> Target Architecture = ARM (little endian)
# 选择 Toolchain -> 使用外部工具链(指向刚才装的Linaro)

# 编译
make

# 编译完成后,output/images/下会生成rootfs.tar和zImage

注意:第一次编译Buildroot会下载很多源码包,耗时较长。建议用代理或提前下载好源码包放到dl/目录下。我当年在学校实验室没网,硬是拷了U盘来回跑。

2.3 传感器驱动开发基础——I2C和SPI协议回顾

好,环境搭好了,接下来聊聊传感器怎么跟处理器通信。嵌入式传感器最常用的两种总线就是I2C和SPI。你想想看,一个温度传感器、一个加速度计,它们怎么把数据传给CPU?

2.3.1 I2C协议——两根线搞定一切

I2C只需要两根线:SCL(时钟线)和SDA(数据线)。所有设备都挂在这两根线上,靠地址区分。

核心要点:

  • 主从模式:处理器是主机,传感器是从机
  • 7位地址:每个从机有唯一地址,比如0x48
  • 速率:标准模式100kHz,快速模式400kHz
  • 起始/停止条件:SCL高电平时SDA下降沿=起始,上升沿=停止

在Linux驱动里操作I2C,通常用i2c-dev用户态接口。我写个简单的读取示例:

#include <linux/i2c-dev.h>
#include <fcntl.h>
#include <unistd.h>

int read_sensor_data(int addr, char reg) {
    int file = open("/dev/i2c-1", O_RDWR);
    if (file < 0) return -1;

    // 设置从机地址
    ioctl(file, I2C_SLAVE, addr);

    // 发送寄存器地址
    write(file, &reg, 1);

    // 读取数据
    char buf[2] = {0};
    read(file, buf, 2);

    close(file);
    return (buf[0] << 8) | buf[1];
}

避坑指南:我曾经遇到一个I2C传感器死活读不到数据,折腾了半天才发现是地址写错了——7位地址左移了一位。记住:Linux驱动里用的是7位地址,不是8位!

2.3.2 SPI协议——四根线,速度更快

SPI比I2C快得多,适合高速传感器(比如陀螺仪、AD转换器)。它用四根线:

  • SCLK:时钟线,由主机产生
  • MOSI:主机输出,从机输入
  • MISO:主机输入,从机输出
  • CS:片选线,低电平有效

SPI没有地址概念,靠片选线选通设备。每个设备独占一根CS线。

Linux下SPI驱动开发,我习惯用spidev接口:

#include <linux/spi/spidev.h>

int spi_read_write(int fd, unsigned char *tx, unsigned char *rx, int len) {
    struct spi_ioc_transfer tr = {
        .tx_buf = (unsigned long)tx,
        .rx_buf = (unsigned long)rx,
        .len = len,
        .speed_hz = 1000000,  // 1MHz
        .delay_usecs = 0,
        .bits_per_word = 8,
    };

    return ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
}

关键区别:I2C适合低速、多设备、连线少的场景;SPI适合高速、单设备、实时性要求高的场景。选哪个?看传感器数据手册,它支持哪个就用哪个。

2.4 实战:点亮一个I2C温度传感器

理论说完了,咱们动手试试。以LM75温度传感器为例,它的I2C地址是0x48,寄存器0x00存温度值。

完整流程:

  1. 确认板子上I2C总线编号(通常是i2c-0或i2c-1)
  2. 用i2cdetect扫描设备:i2cdetect -y 1
  3. 看到0x48地址出现,说明硬件连接正常
  4. 编写用户态程序读取温度
// lm75_read.c
#include <stdio.h>
#include <linux/i2c-dev.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd = open("/dev/i2c-1", O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }

    // 设置LM75地址
    ioctl(fd, I2C_SLAVE, 0x48);

    // 读取温度寄存器
    char reg = 0x00;
    write(fd, &reg, 1);

    char buf[2];
    read(fd, buf, 2);

    // 温度值 = (高字节 << 8 | 低字节) >> 7 * 0.5
    short raw = (buf[0] << 8) | buf[1];
    float temp = (raw >> 7) * 0.5;

    printf("当前温度: %.1f°C\n", temp);

    close(fd);
    return 0;
}

编译时记得用交叉编译器:

arm-linux-gnueabihf-gcc -o lm75_read lm75_read.c

把生成的可执行文件拷贝到板子上,运行就能看到温度了。

常见问题:如果运行时报错“No such device”,先检查设备树里I2C控制器有没有使能。我遇到过好几次,都是设备树配置漏了。

嗯,这一章的内容就这些。环境搭好了,I2C和SPI也回顾了,下一章咱们开始写真正的传感器驱动。到时候你会看到,驱动开发其实没那么神秘——说白了就是跟寄存器打交道。