【嵌入式开发学习】第29天:嵌入式 Linux 入门实战(交叉编译 + 应用开发 + 驱动初探)

核心目标:衔接前 28 天嵌入式基础,聚焦嵌入式 Linux 核心实战(贴合工业 / AIoT 工作场景),掌握 “交叉编译、根文件系统构建、嵌入式 Linux 应用开发、简单驱动入门”,解决 “裸机 / RTOS 过渡到 Linux” 的适配问题,理解嵌入式 Linux 与传统 RTOS 的差异,实现 “嵌入式全栈 + Linux” 的能力闭环 —— 毕竟实际工作中,工业网关、边缘计算节点、智能终端等主流场景,均以嵌入式 Linux 为核心。

一、核心定位:嵌入式 Linux vs 裸机 / RTOS vs 桌面 Linux

首先明确嵌入式 Linux 的核心场景和差异,避免混淆:

对比维度嵌入式 Linux裸机 / FreeRTOS桌面 Linux(Ubuntu)
核心优势多任务、丰富协议栈(TCP/IP/MQTT)、支持复杂应用(AI 推理、GUI)实时性强(≤1ms)、资源占用少(RAM≥8KB)图形化友好、软件生态丰富、无需交叉编译
硬件要求至少 ARM9 级以上(RAM≥64MB,Flash≥128MB)微控制器(RAM≥8KB,Flash≥32KB)x86/x86_64 架构(资源充足)
典型场景工业网关、边缘计算节点、智能机顶盒传感器节点、电机控制器、低功耗终端办公电脑、服务器
开发重点交叉编译、根文件系统、驱动适配、应用部署外设驱动、RTOS 调度、低功耗优化软件安装、图形化应用开发

核心结论:嵌入式 Linux 是 “复杂嵌入式系统” 的首选 —— 当需要运行多进程、复杂协议(如 HTTP/HTTPS)、AI 模型、GUI 时,比 RTOS 更高效;但实时性要求极高(如≤1ms)的场景(如电机精密控制),仍需 RTOS 或 “Linux+RTOS 异构”(如 STM32MP1 的 A7 核跑 Linux,M4 核跑 FreeRTOS)。

二、嵌入式 Linux 开发环境搭建(40 分钟)

嵌入式 Linux 开发的核心是 “交叉编译”(在 x86 桌面机编译 ARM/Linux 平台的程序),以STM32MP157(ARM Cortex-A7 双核,支持 Linux+FreeRTOS 异构) 为例,搭建完整开发环境:

(一)核心工具清单

  1. 交叉编译工具链:ARM 官方工具链(arm-linux-gnueabihf-gcc),用于编译 ARM 架构程序;
  2. 根文件系统(RootFS):Buildroot 构建(轻量级、可定制,适合嵌入式);
  3. 开发板:STM32MP157 开发板(自带 Linux 镜像,支持 SD 卡启动);
  4. 调试工具:SecureCRT(串口终端)、SSH(远程登录)、ADB(设备调试);
  5. 编译构建工具:Makefile、CMake(管理复杂项目)。

(二)实操:环境搭建步骤

1. 安装交叉编译工具链

bash

# 1. 下载工具链(ARM官方,支持ARM Cortex-A系列)
wget https://developer.arm.com/-/media/Files/downloads/gnu-a/10.3-2021.07/binrel/gcc-arm-10.3-2021.07-x86_64-arm-linux-gnueabihf.tar.xz

# 2. 解压到/usr/local/
sudo tar -xvf gcc-arm-10.3-2021.07-x86_64-arm-linux-gnueabihf.tar.xz -C /usr/local/

# 3. 配置环境变量(~/.bashrc末尾添加)
echo "export PATH=/usr/local/gcc-arm-10.3-2021.07-x86_64-arm-linux-gnueabihf/bin:\$PATH" >> ~/.bashrc
source ~/.bashrc

# 4. 验证(出现版本信息则成功)
arm-linux-gnueabihf-gcc -v
2. 用 Buildroot 构建根文件系统

Buildroot 是嵌入式 Linux 的根文件系统构建工具,可定制内核、驱动、软件包(如 MQTT 客户端、Python):

bash

# 1. 下载Buildroot(2023.02稳定版)
wget https://buildroot.org/downloads/buildroot-2023.02.tar.gz
tar -xvf buildroot-2023.02.tar.gz
cd buildroot-2023.02

# 2. 配置Buildroot(针对STM32MP157)
make stm32mp157a_dk1_defconfig  # 官方默认配置(适配STM32MP1开发板)
make menuconfig  # 图形化配置(可选,如添加MQTT客户端、Python3)

# 3. 编译(耗时1-2小时,依赖网络下载软件包)
make -j4  # 4线程编译

# 4. 生成产物(输出目录:output/images/)
# - rootfs.tar:根文件系统压缩包
# - zImage:Linux内核镜像
# - stm32mp157a-dk1.dtb:设备树文件
3. 开发板烧录与启动
  1. 将 Buildroot 生成的zImagestm32mp157a-dk1.dtbrootfs.tar烧录到 SD 卡(用 Etcher 工具);
  2. 开发板插 SD 卡,上电,通过 SecureCRT 连接串口(波特率 115200),启动后进入 Linux 终端:

    bash

    # 成功启动后,终端显示Linux登录提示符(默认用户名root,无密码)
    root@stm32mp1:~# 
    

三、嵌入式 Linux 应用开发实战(50 分钟)

基于 STM32MP157 Linux 平台,开发 “温湿度采集 + MQTT 上传” 应用 —— 复用之前的 AHT10 传感器驱动,移植到 Linux 环境(重点是 “用户态应用开发”,区别于裸机驱动)。

(一)核心差异:Linux 用户态 vs 裸机 / RTOS

  • 裸机 / RTOS:直接操作寄存器、中断;
  • Linux 用户态:通过 “文件系统接口” 操作外设(如 I2C 设备映射为/dev/i2c-1,通过读写文件操作 I2C),无需直接操作寄存器。

(二)实操 1:AHT10 传感器应用开发(用户态)

1. 硬件准备:AHT10 接入 STM32MP1 I2C1
  • AHT10 SCL → STM32MP1 PB8(I2C1_SCL);
  • AHT10 SDA → STM32MP1 PB9(I2C1_SDA);
  • Linux 下 I2C 设备节点:/dev/i2c-1(由设备树配置)。
2. 应用代码(aht10_linux.c)

c

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

#define AHT10_ADDR 0x38  // AHT10 I2C地址
#define I2C_DEV_PATH "/dev/i2c-1"  // Linux I2C设备节点

// 初始化AHT10
int aht10_init(int fd) {
    uint8_t init_cmd[3] = {0xE1, 0x00, 0x00};
    // 设置I2C从设备地址
    if (ioctl(fd, I2C_SLAVE, AHT10_ADDR) < 0) {
        perror("ioctl set slave addr failed");
        return -1;
    }
    // 发送初始化命令
    if (write(fd, init_cmd, 3) != 3) {
        perror("write init cmd failed");
        return -1;
    }
    usleep(10000);  // 等待初始化完成
    return 0;
}

// 读取温湿度
int aht10_read(int fd, float *temp, float *humi) {
    uint8_t measure_cmd[3] = {0xAC, 0x33, 0x00};
    uint8_t recv_buf[6] = {0};
    
    if (ioctl(fd, I2C_SLAVE, AHT10_ADDR) < 0) {
        perror("ioctl set slave addr failed");
        return -1;
    }
    
    // 发送测量命令
    if (write(fd, measure_cmd, 3) != 3) {
        perror("write measure cmd failed");
        return -1;
    }
    usleep(80000);  // 等待测量完成(80ms)
    
    // 读取数据
    if (read(fd, recv_buf, 6) != 6) {
        perror("read data failed");
        return -1;
    }
    
    // 解析数据(与裸机逻辑一致)
    uint32_t humi_raw = (recv_buf[1] << 12) | (recv_buf[2] << 4) | ((recv_buf[3] & 0xF0) >> 4);
    *humi = (humi_raw / 1048576.0) * 100.0;
    
    uint32_t temp_raw = ((recv_buf[3] & 0x0F) << 16) | (recv_buf[4] << 8) | recv_buf[5];
    *temp = (temp_raw / 1048576.0) * 200.0 - 50.0;
    
    return 0;
}

int main() {
    int fd;
    float temp, humi;
    
    // 1. 打开I2C设备(Linux文件系统接口)
    fd = open(I2C_DEV_PATH, O_RDWR);
    if (fd < 0) {
        perror("open i2c dev failed");
        return -1;
    }
    
    // 2. 初始化AHT10
    if (aht10_init(fd) < 0) {
        close(fd);
        return -1;
    }
    
    // 3. 循环读取并打印
    while (1) {
        if (aht10_read(fd, &temp, &humi) == 0) {
            printf("Temperature: %.1f℃, Humidity: %.1f%%RH\n", temp, humi);
        } else {
            printf("read aht10 data failed\n");
        }
        sleep(1);  // 1秒读取一次
    }
    
    close(fd);
    return 0;
}
3. 交叉编译与部署

bash

# 1. 交叉编译(x86桌面机编译ARM/Linux程序)
arm-linux-gnueabihf-gcc aht10_linux.c -o aht10_app -static  # -static:静态编译(避免依赖根文件系统库)

# 2. 部署到开发板(两种方式)
# 方式1:SD卡拷贝(将aht10_app复制到SD卡,插入开发板挂载后运行)
# 方式2:SSH传输(开发板联网后,通过scp传输)
scp aht10_app root@192.168.1.100:/root/  # 192.168.1.100是开发板IP

# 3. 开发板运行
root@stm32mp1:~# ./aht10_app  # 输出温湿度数据
Temperature: 25.3℃, Humidity: 48.5%RH
Temperature: 25.4℃, Humidity: 48.3%RH

(三)实操 2:MQTT 上传功能整合(Linux 用户态)

复用之前的 MQTT 协议,使用 Linux 下的开源 MQTT 客户端库paho.mqtt.c

1. Buildroot 添加 MQTT 库(编译根文件系统时)

bash

make menuconfig → 搜索"paho-mqtt" → 勾选"paho-mqtt-c" → 重新编译Buildroot(make -j4)
2. 整合 MQTT 的应用代码(核心片段)

c

#include <MQTTClient.h>

#define MQTT_BROKER "tcp://192.168.1.200:1883"  // 阿里云/本地MQTT服务器
#define MQTT_CLIENT_ID "stm32mp1_aht10"
#define MQTT_TOPIC "embedded/linux/data"

// MQTT初始化
MQTTClient connect_mqtt() {
    MQTTClient client;
    MQTTClient_create(&client, MQTT_BROKER, MQTT_CLIENT_ID, MQTTCLIENT_PERSISTENCE_NONE, NULL);
    
    MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
    conn_opts.keepAliveInterval = 60;
    conn_opts.cleansession = 1;
    
    if (MQTTClient_connect(client, &conn_opts) != MQTTCLIENT_SUCCESS) {
        printf("mqtt connect failed\n");
        return NULL;
    }
    return client;
}

// 主函数中添加MQTT上传逻辑
int main() {
    // ... 之前的AHT10初始化代码 ...
    MQTTClient mqtt_client = connect_mqtt();
    char payload[64];
    
    while (1) {
        if (aht10_read(fd, &temp, &humi) == 0) {
            // 构造MQTT消息
            sprintf(payload, "{\"temp\":%.1f,\"humi\":%.1f}", temp, humi);
            MQTTClient_message pub_msg = MQTTClient_message_initializer;
            pub_msg.payload = payload;
            pub_msg.payloadlen = strlen(payload);
            pub_msg.qos = 1;
            
            // 发布消息
            MQTTClient_publishMessage(mqtt_client, MQTT_TOPIC, &pub_msg, NULL);
            printf("publish: %s\n", payload);
        }
        sleep(1);
    }
    
    // ... 关闭资源 ...
}
3. 交叉编译(链接 MQTT 库)

bash

arm-linux-gnueabihf-gcc aht10_mqtt.c -o aht10_mqtt_app -static -lpaho-mqtt3c
# -lpaho-mqtt3c:链接MQTT客户端库(Buildroot编译后已包含在交叉编译工具链中)

四、嵌入式 Linux 驱动开发入门(30 分钟)

嵌入式 Linux 中,外设驱动分为 “内核态驱动” 和 “用户态驱动”:

  • 用户态驱动:如上面的 AHT10 应用,通过文件系统接口(/dev/i2c-1)操作,适合简单外设;
  • 内核态驱动:编译进 Linux 内核或作为模块加载,适合复杂外设(如 SPI、CAN、LCD)、需要中断的场景,实时性比用户态高。

(一)核心概念:Linux 字符设备驱动

最基础的内核驱动类型(如 LED、按键、简单传感器),核心是 “注册字符设备→实现文件操作接口(open/read/write/ioctl)→ 创建设备节点”。

(二)实操:LED 内核驱动开发(STM32MP1 LED)

1. 硬件:LED 接入 STM32MP1 PA5
  • LED 正极 → PA5(通过 220Ω 电阻),负极 → GND;
  • Linux 设备树中配置 PA5 为输出引脚。
2. LED 驱动代码(led_drv.c,内核模块形式)

c

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/gpio.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>

#define LED_DEV_NAME "stm32mp1_led"  // 设备名称
#define LED_CLASS_NAME "stm32mp1_led_class"  // 设备类名称

// 驱动私有数据
struct led_dev {
    dev_t dev_num;  // 设备号
    struct cdev cdev;  // 字符设备
    struct class *class;  // 设备类
    struct device *device;  // 设备
    int led_gpio;  // LED GPIO引脚号
};

struct led_dev led_dev;

// 打开设备(应用层调用open时触发)
static int led_open(struct inode *inode, struct file *file) {
    file->private_data = &led_dev;  // 关联私有数据
    return 0;
}

// 写设备(应用层调用write时触发,控制LED亮灭)
static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) {
    struct led_dev *dev = file->private_data;
    char val;
    
    // 从用户空间拷贝数据(用户态→内核态)
    if (copy_from_user(&val, buf, 1) != 0) {
        return -EFAULT;
    }
    
    // 控制GPIO(亮:1,灭:0)
    gpio_set_value(dev->led_gpio, val - '0');
    return count;
}

// 文件操作接口
static const struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .write = led_write,
};

// 驱动probe函数(设备匹配时执行)
static int led_probe(struct platform_device *pdev) {
    struct device_node *node = pdev->dev.of_node;
    int ret;
    
    // 1. 从设备树获取GPIO引脚号
    led_dev.led_gpio = of_get_named_gpio_flags(node, "led-gpios", 0, NULL);
    if (!gpio_is_valid(led_dev.led_gpio)) {
        dev_err(&pdev->dev, "invalid gpio pin\n");
        return -EINVAL;
    }
    
    // 2. 请求GPIO并设置为输出
    ret = gpio_request(led_dev.led_gpio, "led_gpio");
    if (ret < 0) {
        dev_err(&pdev->dev, "gpio request failed\n");
        return ret;
    }
    gpio_direction_output(led_dev.led_gpio, 0);  // 初始灭
    
    // 3. 分配设备号
    ret = alloc_chrdev_region(&led_dev.dev_num, 0, 1, LED_DEV_NAME);
    if (ret < 0) {
        dev_err(&pdev->dev, "alloc dev num failed\n");
        goto err_gpio_free;
    }
    
    // 4. 初始化字符设备
    cdev_init(&led_dev.cdev, &led_fops);
    led_dev.cdev.owner = THIS_MODULE;
    ret = cdev_add(&led_dev.cdev, led_dev.dev_num, 1);
    if (ret < 0) {
        dev_err(&pdev->dev, "cdev add failed\n");
        goto err_unregister_chrdev;
    }
    
    // 5. 创建设备类和设备节点(/dev/stm32mp1_led)
    led_dev.class = class_create(THIS_MODULE, LED_CLASS_NAME);
    if (IS_ERR(led_dev.class)) {
        ret = PTR_ERR(led_dev.class);
        dev_err(&pdev->dev, "class create failed\n");
        goto err_cdev_del;
    }
    
    led_dev.device = device_create(led_dev.class, &pdev->dev, led_dev.dev_num, NULL, LED_DEV_NAME);
    if (IS_ERR(led_dev.device)) {
        ret = PTR_ERR(led_dev.device);
        dev_err(&pdev->dev, "device create failed\n");
        goto err_class_destroy;
    }
    
    dev_info(&pdev->dev, "led driver probe success\n");
    return 0;
    
// 错误处理流程
err_class_destroy:
    class_destroy(led_dev.class);
err_cdev_del:
    cdev_del(&led_dev.cdev);
err_unregister_chrdev:
    unregister_chrdev_region(led_dev.dev_num, 1);
err_gpio_free:
    gpio_free(led_dev.led_gpio);
    return ret;
}

// 设备树匹配表(与设备树中的节点匹配)
static const struct of_device_id led_of_match[] = {
    { .compatible = "stm32mp1,led" },
    { /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of, led_of_match);

// 平台驱动结构体
static struct platform_driver led_driver = {
    .probe = led_probe,
    .driver = {
        .name = LED_DEV_NAME,
        .of_match_table = led_of_match,
    },
};

// 模块加载(insmod时执行)
module_platform_driver(led_driver);

MODULE_LICENSE("GPL");  // 必须声明GPL协议
MODULE_DESCRIPTION("STM32MP1 LED Kernel Driver");
MODULE_AUTHOR("Embedded Linux Developer");
3. 驱动编译(Makefile)

makefile

# 交叉编译内核模块的Makefile
KERNELDIR ?= /home/user/buildroot-2023.02/output/build/linux-5.15.100  # Buildroot编译的Linux内核目录
PWD := $(shell pwd)

obj-m += led_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
4. 驱动部署与测试

bash

# 1. 交叉编译驱动模块
make  # 生成led_drv.ko

# 2. 部署到开发板
scp led_drv.ko root@192.168.1.100:/root/

# 3. 开发板加载驱动
root@stm32mp1:~# insmod led_drv.ko  # 加载模块
root@stm32mp1:~# lsmod  # 查看加载的模块
root@stm32mp1:~# ls /dev/stm32mp1_led  # 查看设备节点(成功则存在)

# 4. 测试驱动(控制LED亮灭)
root@stm32mp1:~# echo "1" > /dev/stm32mp1_led  # LED亮
root@stm32mp1:~# echo "0" > /dev/stm32mp1_led  # LED灭

五、嵌入式 Linux 与前序知识的整合(20 分钟)

(一)FreeRTOS vs Linux 多任务对比

特性FreeRTOS嵌入式 Linux
任务调度抢占式 + 时间片轮转,支持优先级继承基于 CFS 调度器(完全公平调度),支持多进程 / 线程
任务类型线程(无进程概念)进程(独立地址空间)+ 线程(共享地址空间)
通信方式信号量、队列、事件标志组管道、消息队列、共享内存、信号、Socket
实时性高(≤1ms)中等(默认≥10ms,可通过内核裁剪优化至 1ms)
适用场景低功耗、实时控制(电机、传感器)复杂应用、多协议、AI 推理、GUI

(二)协议移植:Modbus RTOS→Linux

之前的 Modbus 协议代码(RTOS 版)可直接移植到 Linux,只需修改:

  1. 串口操作:从 RTOS 的串口驱动→Linux 的/dev/ttySTM0文件操作;
  2. 延时函数:vTaskDelay(100)usleep(100000)
  3. 多任务:RTOS 任务→Linux 线程(pthread 库)。

六、第二十九天必掌握的 3 个核心点

  1. 嵌入式 Linux 开发流程:会搭建交叉编译环境、用 Buildroot 构建根文件系统、将应用程序部署到开发板;
  2. 用户态应用开发:理解 Linux 文件系统接口,能开发传感器(如 AHT10)、MQTT 上传等用户态应用;
  3. 内核驱动入门:掌握字符设备驱动的核心流程(注册设备→实现文件操作→创建设备节点),能编译加载简单内核模块。

总结

第 29 天开启了 “嵌入式 + Linux” 的实战篇章,核心是理解 “嵌入式 Linux 的差异化定位”—— 它不是替代 RTOS,而是互补:简单实时场景用 RTOS,复杂多协议 / 多应用场景用 Linux,异构架构(如 STM32MP1)可实现 “Linux+RTOS” 协同。

嵌入式 Linux 是工业网关、边缘计算、智能终端等高薪场景的必备技能,后续将深入内核裁剪、设备树配置、工业总线(CAN/EtherCAT)Linux 驱动、边缘 AI 模型部署(TensorFlow Lite Linux 版)等实战内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值