文章目录
-
- 一、嵌入式Linux开发环境搭建(必备)
-
- [1.1 主机环境配置(Ubuntu 20.04 LTS推荐)](#1.1 主机环境配置(Ubuntu 20.04 LTS推荐))
- [1.2 交叉编译工具链安装](#1.2 交叉编译工具链安装)
- [1.3 内核源码获取与配置](#1.3 内核源码获取与配置)
- 二、Linux驱动/内核核心资料汇总
-
- [2.1 官方核心文档](#2.1 官方核心文档)
- [2.2 优质实战教程](#2.2 优质实战教程)
- [2.3 工具链与调试工具](#2.3 工具链与调试工具)
- 三、实战1:字符设备驱动开发(入门必学)
-
- [3.1 字符设备驱动核心框架](#3.1 字符设备驱动核心框架)
- [3.2 驱动编译Makefile](#3.2 驱动编译Makefile)
- [3.3 测试程序(应用层)](#3.3 测试程序(应用层))
- [3.4 编译与测试步骤](#3.4 编译与测试步骤)
- 四、实战2:GPIO/I2C/SPI外设驱动开发
-
- [4.1 GPIO驱动开发(LED控制)](#4.1 GPIO驱动开发(LED控制))
- [4.2 I2C驱动开发(EEPROM 24C02)](#4.2 I2C驱动开发(EEPROM 24C02))
- [4.3 SPI驱动开发(SPI Flash)](#4.3 SPI驱动开发(SPI Flash))
- 五、实战3:DeviceTree设备树配置与解析
-
- [5.1 DeviceTree核心语法](#5.1 DeviceTree核心语法)
- [5.2 关键属性解析](#5.2 关键属性解析)
- [5.3 设备树编译与使用](#5.3 设备树编译与使用)
- [5.4 驱动解析设备树示例](#5.4 驱动解析设备树示例)
- 六、实战4:多线程开发示例(应用层)
-
- [6.1 多线程核心API](#6.1 多线程核心API)
- [6.2 实战示例:多线程控制LED+读取I2C传感器](#6.2 实战示例:多线程控制LED+读取I2C传感器)
- [6.3 编译与运行](#6.3 编译与运行)
- 七、综合实战:多模块联动项目
-
- [7.1 项目架构](#7.1 项目架构)
- [7.2 核心步骤](#7.2 核心步骤)
- [7.3 项目效果](#7.3 项目效果)
- 八、常见问题排查指南
-
- [8.1 驱动开发问题](#8.1 驱动开发问题)
- [8.2 DeviceTree问题](#8.2 DeviceTree问题)
- [8.3 多线程问题](#8.3 多线程问题)
一、嵌入式Linux开发环境搭建(必备)
嵌入式Linux开发需搭建"主机(Ubuntu)+ 目标板(如ARM开发板)"的交叉开发环境,核心包括交叉编译工具链、内核源码、调试工具三大组件,步骤如下:
1.1 主机环境配置(Ubuntu 20.04 LTS推荐)
Ubuntu LTS版本稳定性强,资源丰富,是嵌入式开发的首选。首先安装必备开发工具:
bash
# 更新软件包列表
sudo apt update
# 安装核心开发工具(编译、管理、依赖库)
sudo apt install -y gcc make libncurses5-dev git build-essential
sudo apt install -y openssh-server minicom # 远程登录、串口调试工具
# 验证安装
gcc --version # 输出版本信息即安装成功
make --version
1.2 交叉编译工具链安装
交叉编译工具链用于在x86主机上编译ARM架构的程序,需根据目标板CPU型号选择(如ARM Cortex-A7选择arm-linux-gnueabihf-gcc):
bash
# 下载工具链(以ARMv7为例)
wget https://releases.linaro.org/components/toolchain/binaries/7.5-2019.12/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz
# 解压到/opt目录
sudo tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt
# 配置环境变量(永久生效)
echo "export PATH=/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin:\$PATH" >> ~/.bashrc
# 生效环境变量
source ~/.bashrc
# 验证
arm-linux-gnueabihf-gcc --version # 输出ARM架构编译器版本即成功
1.3 内核源码获取与配置
驱动开发需与目标板内核版本匹配,优先从开发板厂商官网下载内核源码(如正点原子、韦东山开发板),也可从Linux官方仓库获取:
bash
# 从官方仓库克隆内核源码(以5.4版本为例)
git clone --depth=1 -b v5.4 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git
# 进入源码目录
cd linux-stable
# 配置内核(针对目标板)
# 1. 导入厂商默认配置(如正点原子IMX6ULL:make imx_v7_defconfig)
# 2. 图形化配置(按需调整)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
# 编译内核(生成zImage镜像和dtb设备树文件)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j4
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs -j4
⚠️ 注意:ARCH指定架构(arm/arm64),CROSS_COMPILE指定交叉编译器前缀,-j4表示4线程编译(根据主机CPU核心数调整)。
二、Linux驱动/内核核心资料汇总
整理嵌入式Linux驱动/内核开发必备资料,涵盖官方文档、实战教程、工具链,助力高效学习:
2.1 官方核心文档
-
Linux内核官方文档:https://www.kernel.org/doc/html/latest/(驱动开发、设备树、内存管理等权威指南)
-
DeviceTree官方规范:https://devicetree.org/specifications/(设备树语法、节点配置权威参考)
-
ARM官方手册:https://developer.arm.com/documentation/(Cortex-A系列CPU架构、外设手册)
2.2 优质实战教程
-
韦东山嵌入式Linux驱动开发:涵盖字符设备、外设驱动、设备树全流程实战,适合新手入门。
-
正点原子Linux驱动开发手册:配套开发板,代码可直接编译运行,实战性极强。
-
掘金嵌入式Linux专栏:汇总大量驱动开发实战案例,问题解析详细。
2.3 工具链与调试工具
-
交叉编译工具链:Linaro Toolchain(通用)、厂商定制工具链(如NXP、Rockchip)。
-
调试工具:gdb-multiarch(交叉调试)、minicom(串口调试)、strace(系统调用跟踪)。
-
辅助工具:DeviceTree Compiler(dtc,编译dts文件)、kconfig-frontends(内核配置工具)。
三、实战1:字符设备驱动开发(入门必学)
字符设备是嵌入式Linux中最基础的设备类型(如LED、按键、串口),按字节流访问,驱动开发核心是实现file_operations结构体接口,完成驱动注册与注销。
3.1 字符设备驱动核心框架
驱动核心流程:驱动加载(insmod)→ 注册字符设备 → 实现文件操作接口 → 驱动卸载(rmmod)
c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
// 设备号(主设备号+次设备号)
dev_t dev_num;
// cdev结构体(字符设备核心结构体)
struct cdev chr_dev;
// 设备类
struct class *chr_class;
// 设备节点
struct device *chr_device;
// 设备名称(/dev/chrdev)
#define DEV_NAME "chrdev"
// 主设备号(0表示自动分配)
#define MAJOR_NUM 0
// 读函数(应用层read调用)
ssize_t chrdev_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
char kernel_buf[] = "Hello, Char Device!";
int ret;
// 内核空间→用户空间拷贝数据(避免直接访问用户空间地址)
ret = copy_to_user(buf, kernel_buf, sizeof(kernel_buf));
if (ret < 0) {
printk(KERN_ERR "copy_to_user failed!\n");
return -EFAULT;
}
printk(KERN_INFO "chrdev read success!\n");
return sizeof(kernel_buf);
}
// 写函数(应用层write调用)
ssize_t chrdev_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
char kernel_buf[128] = {0};
int ret;
// 用户空间→内核空间拷贝数据
ret = copy_from_user(kernel_buf, buf, count);
if (ret < 0) {
printk(KERN_ERR "copy_from_user failed!\n");
return -EFAULT;
}
printk(KERN_INFO "chrdev write data: %s\n", kernel_buf);
return count;
}
// 打开函数(应用层open调用)
int chrdev_open(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "chrdev open success!\n");
return 0;
}
// 关闭函数(应用层close调用)
int chrdev_release(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "chrdev release success!\n");
return 0;
}
// 文件操作结构体(驱动核心,关联应用层系统调用)
struct file_operations chr_fops = {
.owner = THIS_MODULE,
.open = chrdev_open,
.release = chrdev_release,
.read = chrdev_read,
.write = chrdev_write,
};
// 驱动加载函数(insmod时执行)
static int __init chrdev_init(void)
{
int ret;
// 1. 分配设备号(自动分配)
ret = alloc_chrdev_region(&dev_num, 0, 1, DEV_NAME);
if (ret < 0) {
printk(KERN_ERR "alloc_chrdev_region failed!\n");
return ret;
}
printk(KERN_INFO "alloc dev num: major=%d, minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
// 2. 初始化cdev结构体
cdev_init(&chr_dev, &chr_fops);
chr_dev.owner = THIS_MODULE;
// 3. 注册字符设备到内核
ret = cdev_add(&chr_dev, dev_num, 1);
if (ret < 0) {
printk(KERN_ERR "cdev_add failed!\n");
unregister_chrdev_region(dev_num, 1); // 失败回滚
return ret;
}
// 4. 创建设备类(/sys/class/chrdev)
chr_class = class_create(THIS_MODULE, DEV_NAME);
if (IS_ERR(chr_class)) {
printk(KERN_ERR "class_create failed!\n");
cdev_del(&chr_dev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(chr_class);
}
// 5. 创建设备节点(/dev/chrdev)
chr_device = device_create(chr_class, NULL, dev_num, NULL, DEV_NAME);
if (IS_ERR(chr_device)) {
printk(KERN_ERR "device_create failed!\n");
class_destroy(chr_class);
cdev_del(&chr_dev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(chr_device);
}
printk(KERN_INFO "chrdev driver init success!\n");
return 0;
}
// 驱动卸载函数(rmmod时执行)
static void __exit chrdev_exit(void)
{
// 反向释放资源
device_destroy(chr_class, dev_num);
class_destroy(chr_class);
cdev_del(&chr_dev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "chrdev driver exit success!\n");
}
// 模块加载/卸载宏
module_init(chrdev_init);
module_exit(chrdev_exit);
// 模块信息(必须,否则编译警告)
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux");
MODULE_DESCRIPTION("Char Device Driver Example");
MODULE_VERSION("1.0");
3.2 驱动编译Makefile
makefile
# 内核源码路径(需根据实际修改)
KERNELDIR ?= /home/ubuntu/linux-stable
# 当前驱动目录
PWD := $(shell pwd)
# 目标文件
obj-m += chrdev_drv.o
# 编译规则
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
# 清理规则
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- clean
rm -rf .tmp_versions *.mod.c *.o *.ko *.symvers *.order
3.3 测试程序(应用层)
c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char *argv[])
{
int fd;
char buf[128] = {0};
// 1. 打开设备节点
fd = open("/dev/chrdev", O_RDWR);
if (fd < 0) {
perror("open /dev/chrdev failed");
return -1;
}
// 2. 读设备
read(fd, buf, sizeof(buf));
printf("Read from chrdev: %s\n", buf);
// 3. 写设备
char write_buf[] = "Test char device write!";
write(fd, write_buf, strlen(write_buf));
// 4. 关闭设备
close(fd);
return 0;
}
3.4 编译与测试步骤
bash
# 1. 编译驱动模块(生成.ko文件)
make
# 2. 编译应用层测试程序(交叉编译)
arm-linux-gnueabihf-gcc test_chrdev.c -o test_chrdev
# 3. 拷贝.ko文件和测试程序到目标板(通过scp或U盘)
scp chrdev_drv.ko test_chrdev root@192.168.1.100:/root
# 4. 目标板上执行测试
# 加载驱动
insmod chrdev_drv.ko
# 查看驱动信息(主设备号、设备节点)
lsmod | grep chrdev
ls /dev/chrdev
# 运行测试程序
./test_chrdev
# 查看内核打印信息
dmesg | grep chrdev
# 卸载驱动
rmmod chrdev_drv
四、实战2:GPIO/I2C/SPI外设驱动开发
嵌入式系统中,GPIO、I2C、SPI是最常用的外设接口,本节以"LED(GPIO)、EEPROM(I2C)、SPI Flash(SPI)"为例,讲解外设驱动开发流程。
4.1 GPIO驱动开发(LED控制)
GPIO驱动核心是配置引脚为输入/输出模式,通过设置电平控制外设(如LED点亮/熄灭),结合DeviceTree配置硬件信息。
c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/of.h>
// 设备私有数据(存储GPIO号)
struct gpio_led_data {
int led_gpio;
};
// 驱动probe函数(设备与驱动匹配成功后执行)
static int gpio_led_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct device_node *node = dev->of_node;
struct gpio_led_data *led_data;
int ret;
// 分配私有数据内存
led_data = devm_kzalloc(dev, sizeof(*led_data), GFP_KERNEL);
if (!led_data)
return -ENOMEM;
// 从设备树获取GPIO号(compatible属性匹配)
led_data->led_gpio = of_get_named_gpio_flags(node, "led-gpios", 0, NULL);
if (!gpio_is_valid(led_data->led_gpio)) {
dev_err(dev, "invalid led gpio!\n");
return -EINVAL;
}
// 申请GPIO资源
ret = devm_gpio_request(dev, led_data->led_gpio, "led-gpio");
if (ret < 0) {
dev_err(dev, "gpio request failed!\n");
return ret;
}
// 配置GPIO为输出模式,默认低电平(LED熄灭)
gpio_direction_output(led_data->led_gpio, 0);
// 设置高电平(LED点亮)
gpio_set_value(led_data->led_gpio, 1);
dev_info(dev, "gpio led driver probe success! led gpio: %d\n", led_data->led_gpio);
platform_set_drvdata(pdev, led_data);
return 0;
}
// 驱动remove函数(驱动卸载时执行)
static int gpio_led_remove(struct platform_device *pdev)
{
struct gpio_led_data *led_data = platform_get_drvdata(pdev);
// 释放GPIO资源,设置低电平
gpio_set_value(led_data->led_gpio, 0);
dev_info(dev, "gpio led driver remove success!\n");
return 0;
}
// 设备树匹配表(compatible属性匹配)
static const struct of_device_id gpio_led_of_match[] = {
{ .compatible = "embedded,gpio-led" },
{ }
};
MODULE_DEVICE_TABLE(of, gpio_led_of_match);
// platform驱动结构体
static struct platform_driver gpio_led_driver = {
.probe = gpio_led_probe,
.remove = gpio_led_remove,
.driver = {
.name = "gpio-led-driver",
.of_match_table = gpio_led_of_match,
},
};
// 模块加载/卸载
module_platform_driver(gpio_led_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux");
MODULE_DESCRIPTION("GPIO LED Driver Example");
4.2 I2C驱动开发(EEPROM 24C02)
I2C驱动采用"控制器驱动+设备驱动"架构,控制器驱动由内核提供,设备驱动只需实现i2c_driver结构体,结合DeviceTree配置I2C设备地址。
c
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of.h>
#define EEPROM_ADDR 0x50 // 24C02 I2C设备地址
#define EEPROM_SIZE 256 // 24C02容量(256字节)
// I2C设备读函数
int eeprom_read(struct i2c_client *client, u8 reg, u8 *buf, int len)
{
struct i2c_msg msgs[2];
int ret;
// 第一个消息:发送寄存器地址
msgs[0].addr = client->addr;
msgs[0].flags = 0; // 写模式
msgs[0].buf = ®
msgs[0].len = 1;
// 第二个消息:读取数据
msgs[1].addr = client->addr;
msgs[1].flags = I2C_M_RD; // 读模式
msgs[1].buf = buf;
msgs[1].len = len;
// 执行I2C传输
ret = i2c_transfer(client->adapter, msgs, 2);
return ret == 2 ? 0 : ret;
}
// I2C设备写函数
int eeprom_write(struct i2c_client *client, u8 reg, u8 *buf, int len)
{
u8 data[len + 1];
struct i2c_msg msg;
int ret;
// 拼接寄存器地址和数据
data[0] = reg;
memcpy(&data[1], buf, len);
// 发送写消息
msg.addr = client->addr;
msg.flags = 0; // 写模式
msg.buf = data;
msg.len = len + 1;
// 执行I2C传输
ret = i2c_transfer(client->adapter, &msg, 1);
return ret == 1 ? 0 : ret;
}
// probe函数(I2C设备匹配成功后执行)
static int eeprom_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
u8 buf[4] = {0};
int ret;
dev_info(&client->dev, "eeprom probe success! i2c addr: 0x%x\n", client->addr);
// 测试:写数据到EEPROM(地址0x00)
u8 write_data[] = {0x12, 0x34, 0x56, 0x78};
ret = eeprom_write(client, 0x00, write_data, 4);
if (ret < 0) {
dev_err(&client->dev, "eeprom write failed!\n");
return ret;
}
// 测试:从EEPROM读取数据(地址0x00)
ret = eeprom_read(client, 0x00, buf, 4);
if (ret < 0) {
dev_err(&client->dev, "eeprom read failed!\n");
return ret;
}
dev_info(&client->dev, "eeprom read data: 0x%x 0x%x 0x%x 0x%x\n",
buf[0], buf[1], buf[2], buf[3]);
return 0;
}
// remove函数(驱动卸载时执行)
static int eeprom_remove(struct i2c_client *client)
{
dev_info(&client->dev, "eeprom driver remove success!\n");
return 0;
}
// I2C设备ID表
static const struct i2c_device_id eeprom_id[] = {
{"24c02", 0},
{ }
};
MODULE_DEVICE_TABLE(i2c, eeprom_id);
// 设备树匹配表
static const struct of_device_id eeprom_of_match[] = {
{ .compatible = "atmel,24c02" },
{ }
};
MODULE_DEVICE_TABLE(of, eeprom_of_match);
// I2C驱动结构体
static struct i2c_driver eeprom_driver = {
.probe = eeprom_probe,
.remove = eeprom_remove,
.id_table = eeprom_id,
.driver = {
.name = "eeprom-24c02-driver",
.of_match_table = eeprom_of_match,
},
};
module_i2c_driver(eeprom_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux");
MODULE_DESCRIPTION("I2C EEPROM 24C02 Driver");
4.3 SPI驱动开发(SPI Flash)
SPI驱动与I2C类似,采用"控制器+设备"架构,核心是通过spi_transfer结构体实现数据传输。
c
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/of.h>
#define SPI_FLASH_CMD_READ_ID 0x9F // 读取Flash ID命令
// SPI Flash读ID函数
int spi_flash_read_id(struct spi_device *spi, u8 *id, int len)
{
struct spi_transfer t[2];
struct spi_message m;
u8 cmd = SPI_FLASH_CMD_READ_ID;
int ret;
// 初始化SPI消息
spi_message_init(&m);
memset(t, 0, sizeof(t));
// 第一个传输:发送读ID命令
t[0].tx_buf = &cmd;
t[0].len = 1;
spi_message_add_tail(&t[0], &m);
// 第二个传输:读取ID数据
t[1].rx_buf = id;
t[1].len = len;
spi_message_add_tail(&t[1], &m);
// 执行SPI传输
ret = spi_sync(spi, &m);
return ret;
}
// probe函数
static int spi_flash_probe(struct spi_device *spi)
{
u8 id[3] = {0};
int ret;
// 配置SPI参数(时钟频率、模式)
spi->max_speed_hz = 10000000; // 10MHz
spi->mode = SPI_MODE_0; // CPOL=0, CPHA=0
spi_setup(spi);
dev_info(&spi->dev, "spi flash probe success! bus num: %d, chip select: %d\n",
spi->master->bus_num, spi->chip_select);
// 读取SPI Flash ID
ret = spi_flash_read_id(spi, id, 3);
if (ret < 0) {
dev_err(&spi->dev, "spi flash read id failed!\n");
return ret;
}
dev_info(&spi->dev, "spi flash id: 0x%x 0x%x 0x%x\n", id[0], id[1], id[2]);
return 0;
}
// remove函数
static int spi_flash_remove(struct spi_device *spi)
{
dev_info(&spi->dev, "spi flash driver remove success!\n");
return 0;
}
// 设备树匹配表
static const struct of_device_id spi_flash_of_match[] = {
{ .compatible = "winbond,w25q64" }, // W25Q64 SPI Flash
{ }
};
MODULE_DEVICE_TABLE(of, spi_flash_of_match);
// SPI驱动结构体
static struct spi_driver spi_flash_driver = {
.probe = spi_flash_probe,
.remove = spi_flash_remove,
.driver = {
.name = "spi-flash-driver",
.of_match_table = spi_flash_of_match,
},
};
module_spi_driver(spi_flash_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux");
MODULE_DESCRIPTION("SPI Flash Driver Example");
五、实战3:DeviceTree设备树配置与解析
DeviceTree(DTS)是Linux内核用于描述硬件信息的标准,取代了传统的板级C文件,实现"硬件描述与内核代码分离",核心是通过节点和属性描述硬件资源。
5.1 DeviceTree核心语法
设备树基本组成:根节点(/)→ 子节点(如cpu、i2c、spi)→ 属性(compatible、reg、interrupts等)
dts
// 设备树源文件(.dts)
#include "imx6ull.dtsi" // 包含通用设备树文件
/ {
model = "Embedded Linux Development Board";
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
// 内存节点
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; // 地址0x80000000,大小512MB
};
// LED设备节点(对应前面的GPIO驱动)
led@0 {
compatible = "embedded,gpio-led"; // 与驱动of_match_table匹配
led-gpios = <&gpio1 3 0>; // GPIO1_3,低电平有效
status = "okay"; // 启用设备
};
// I2C1总线节点
i2c1: i2c@21a0000 {
#address-cells = <1>; // 子节点地址用1个32位整数表示
#size-cells = <0>; // 子节点无大小
status = "okay";
// I2C设备节点(24C02 EEPROM)
eeprom@50 {
compatible = "atmel,24c02"; // 与I2C驱动匹配
reg = <0x50>; // I2C设备地址
};
};
// SPI1总线节点
spi1: spi@2008000 {
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
// SPI设备节点(W25Q64 Flash)
flash@0 {
compatible = "winbond,w25q64"; // 与SPI驱动匹配
reg = <0>; // SPI片选号(CS0)
spi-max-frequency = <10000000>; // 最大时钟频率10MHz
spi-cpol = <0>;
spi-cpha = <0>;
};
};
};
5.2 关键属性解析
-
compatible:兼容属性,用于驱动与设备匹配(驱动of_match_table需包含该值); -
reg:寄存器地址属性,格式为<地址 大小>,描述设备在内存中的地址范围; -
#address-cells/#size-cells:指定子节点reg属性的地址/大小字段个数; -
status:设备状态,"okay"表示启用,"disabled"表示禁用; -
gpio-leds:GPIO属性,格式为<&gpio控制器 引脚号 有效电平>。
5.3 设备树编译与使用
bash
# 1. 编译设备树(生成.dtb二进制文件)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs
# 2. 拷贝.dtb文件到目标板(与内核镜像放在同一目录)
scp arch/arm/boot/dts/freescale/imx6ull-14x14-evk.dtb root@192.168.1.100:/boot
# 3. 目标板修改启动参数(指定设备树文件)
# 编辑/boot/uEnv.txt,添加:
dtb_name=imx6ull-14x14-evk.dtb
# 4. 重启目标板,验证设备树
cat /proc/device-tree/model # 查看设备树model属性
ls /sys/firmware/devicetree/base/led@0 # 查看LED设备节点
5.4 驱动解析设备树示例
驱动中通过of函数解析设备树属性(前面GPIO/I2C/SPI驱动已包含,此处单独提炼核心接口):
c
#include <linux/of.h>
#include <linux/of_gpio.h>
// 1. 根据节点名查找设备树节点
struct device_node *node = of_find_node_by_name(NULL, "led@0");
if (!node) {
printk(KERN_ERR "find led node failed!\n");
return -ENODEV;
}
// 2. 读取compatible属性
const char *compatible;
of_property_read_string(node, "compatible", &compatible);
printk(KERN_INFO "led compatible: %s\n", compatible);
// 3. 读取GPIO属性
int led_gpio = of_get_named_gpio_flags(node, "led-gpios", 0, NULL);
if (!gpio_is_valid(led_gpio)) {
printk(KERN_ERR "invalid led gpio!\n");
return -EINVAL;
}
// 4. 读取status属性
const char *status;
of_property_read_string(node, "status", &status);
if (strcmp(status, "okay") != 0) {
printk(KERN_ERR "led device disabled!\n");
return -ENODEV;
}
六、实战4:多线程开发示例(应用层)
嵌入式Linux应用层多线程开发基于pthread库,核心用于处理多任务并发(如传感器数据采集、外设控制、数据传输),需注意线程同步与互斥。
6.1 多线程核心API
-
pthread_create:创建线程; -
pthread_join:等待线程结束; -
pthread_mutex_init/pthread_mutex_lock/pthread_mutex_unlock:互斥锁(线程同步); -
pthread_exit:线程退出。
6.2 实战示例:多线程控制LED+读取I2C传感器
实现两个线程:线程1控制LED周期性闪烁,线程2读取I2C EEPROM数据,通过互斥锁保证共享资源安全。
c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
// 互斥锁(保护共享资源)
pthread_mutex_t mutex;
// 共享变量(LED状态)
int led_status = 0;
// 线程1:控制LED闪烁
void *led_thread(void *arg)
{
int fd = open("/dev/chrdev", O_RDWR);
if (fd < 0) {
perror("open led device failed");
pthread_exit(NULL);
}
while (1) {
// 加互斥锁
pthread_mutex_lock(&mutex);
led_status = !led_status;
// 控制LED电平(1点亮,0熄灭)
write(fd, &led_status, sizeof(led_status));
printf("LED status: %s\n", led_status ? "ON" : "OFF");
// 解锁
pthread_mutex_unlock(&mutex);
sleep(1); // 1秒切换一次状态
}
close(fd);
pthread_exit(NULL);
}
// 线程2:读取I2C EEPROM数据
void *i2c_thread(void *arg)
{
int fd = open("/dev/eeprom", O_RDWR);
if (fd < 0) {
perror("open eeprom device failed");
pthread_exit(NULL);
}
u8 buf[4] = {0};
while (1) {
// 加互斥锁(避免与LED线程冲突)
pthread_mutex_lock(&mutex);
// 读取EEPROM数据
lseek(fd, 0x00, SEEK_SET);
read(fd, buf, 4);
printf("EEPROM data: 0x%x 0x%x 0x%x 0x%x\n", buf[0], buf[1], buf[2], buf[3]);
// 解锁
pthread_mutex_unlock(&mutex);
sleep(2); // 2秒读取一次
}
close(fd);
pthread_exit(NULL);
}
int main()
{
pthread_t tid1, tid2;
int ret;
// 初始化互斥锁
ret = pthread_mutex_init(&mutex, NULL);
if (ret != 0) {
perror("pthread_mutex_init failed");
return -1;
}
// 创建LED线程
ret = pthread_create(&tid1, NULL, led_thread, NULL);
if (ret != 0) {
perror("create led thread failed");
return -1;
}
// 创建I2C线程
ret = pthread_create(&tid2, NULL, i2c_thread, NULL);
if (ret != 0) {
perror("create i2c thread failed");
return -1;
}
// 等待线程结束(阻塞)
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
6.3 编译与运行
bash
# 交叉编译(需链接pthread库)
arm-linux-gnueabihf-gcc multithread_demo.c -o multithread_demo -lpthread
# 拷贝到目标板运行
scp multithread_demo root@192.168.1.100:/root
./multithread_demo
# 运行结果(示例)
LED status: ON
EEPROM data: 0x12 0x34 0x56 0x78
LED status: OFF
LED status: ON
EEPROM data: 0x12 0x34 0x56 0x78
...
七、综合实战:多模块联动项目
串联前面的核心模块,实现"多线程+GPIO+I2C+DeviceTree"综合项目:通过DeviceTree配置GPIO和I2C硬件,驱动层实现LED和EEPROM驱动,应用层多线程控制LED闪烁并读取EEPROM数据,同时通过串口打印日志。
7.1 项目架构
-
硬件层:LED(GPIO1_3)、EEPROM 24C02(I2C1总线,地址0x50);
-
设备树:配置GPIO和I2C节点,与驱动匹配;
-
驱动层:GPIO LED驱动、I2C EEPROM驱动;
-
应用层:两个线程(LED控制、EEPROM读取)+ 串口日志打印。
7.2 核心步骤
bash
# 1. 编写设备树(添加LED和I2C节点,参考5.1节)
# 2. 编译设备树和内核,烧写到目标板
# 3. 编译LED和EEPROM驱动,加载到目标板
insmod gpio_led_drv.ko
insmod eeprom_drv.ko
# 4. 编译应用层多线程程序,运行
./multithread_demo
# 5. 查看串口日志(通过minicom)
minicom -D /dev/ttyUSB0
7.3 项目效果
-
LED每1秒闪烁一次;
-
EEPROM每2秒读取一次数据并打印;
-
串口终端实时输出LED状态和EEPROM数据;
-
卸载驱动或停止应用程序后,LED熄灭,数据读取停止。
八、常见问题排查指南
汇总嵌入式Linux实战中高频问题,提供排查思路和解决方案,快速避坑。
8.1 驱动开发问题
-
问题1:驱动加载失败(insmod: ERROR: could not insert module)→ 排查:内核版本不匹配、依赖模块未加载、编译选项错误;解决方案:重新编译驱动(匹配内核版本)、加载依赖模块、检查Makefile中的KERNELDIR路径;
-
问题2:设备节点未生成(/dev/xxx不存在)→ 排查:class_create/device_create调用失败、设备号分配失败;解决方案:查看dmesg日志,检查驱动中资源申请流程,确保ret值判断正确;
-
问题3:应用层读写设备失败(Permission denied)→ 排查:设备节点权限不足;解决方案:修改节点权限(chmod 777 /dev/xxx)或切换root用户。
8.2 DeviceTree问题
-
问题1:设备树编译失败(error: syntax error)→ 排查:语法错误(如括号不匹配、属性格式错误);解决方案:检查.dts文件语法,参考官方示例;
-
问题2:驱动与设备树不匹配(probe函数不执行)→ 排查:compatible属性不一致、设备树节点status为disabled;解决方案:确保驱动of_match_table与设备树compatible属性一致,设置status="okay";
-
问题3:无法读取设备树属性(返回-ENOENT)→ 排查:节点名或属性名错误、节点未启用;解决方案:通过cat /proc/device-tree查看节点属性,确认属性名拼写正确。
8.3 多线程问题
-
问题1:线程创建失败(pthread_create返回非0)→ 排查:未链接pthread库、栈空间不足;解决方案:编译时添加-lpthread参数、增大线程栈空间;
-
问题2:线程同步冲突(数据错乱)→ 排查:未使用互斥锁或锁使用不当;解决方案:在访问共享资源前后添加互斥锁,避免死锁(确保锁的获取与释放成对);
-
问题3:线程无法退出(程序卡死)→ 排查:线程进入死循环、未处理信号;解决方案:添加退出条件(如全局标志位)、处理SIGINT信号。