嵌入式Linux系列实战项目:驱动开发+DeviceTree+多线程全解析

文章目录

    • 一、嵌入式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 官方核心文档

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 = &reg;
    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 项目架构

  1. 硬件层:LED(GPIO1_3)、EEPROM 24C02(I2C1总线,地址0x50);

  2. 设备树:配置GPIO和I2C节点,与驱动匹配;

  3. 驱动层:GPIO LED驱动、I2C EEPROM驱动;

  4. 应用层:两个线程(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信号。

相关推荐
小白同学_C14 小时前
Lab4-Lab: traps && MIT6.1810操作系统工程【持续更新】 _
linux·c/c++·操作系统os
今天只学一颗糖14 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
青云计划14 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿14 小时前
Jsoniter(java版本)使用介绍
java·开发语言
探路者继续奋斗15 小时前
IDD意图驱动开发之意图规格说明书
java·规格说明书·开发规范·意图驱动开发·idd
不做无法实现的梦~15 小时前
ros2实现路径规划---nav2部分
linux·stm32·嵌入式硬件·机器人·自动驾驶
消失的旧时光-194316 小时前
第十九课:为什么要引入消息队列?——异步系统设计思想
java·开发语言
A懿轩A16 小时前
【Java 基础编程】Java 面向对象入门:类与对象、构造器、this 关键字,小白也能写 OOP
java·开发语言
乐观勇敢坚强的老彭16 小时前
c++寒假营day03
java·开发语言·c++
biubiubiu070616 小时前
谷歌浏览器无法访问localhost:8080
java