第二章 开发环境搭建:嵌入式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, ®, 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存温度值。
完整流程:
- 确认板子上I2C总线编号(通常是i2c-0或i2c-1)
- 用i2cdetect扫描设备:
i2cdetect -y 1 - 看到0x48地址出现,说明硬件连接正常
- 编写用户态程序读取温度
// 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, ®, 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也回顾了,下一章咱们开始写真正的传感器驱动。到时候你会看到,驱动开发其实没那么神秘——说白了就是跟寄存器打交道。