初次接触嵌入式Linux时,可能会感到困惑:
- • 为什么不能直接操作硬件? 在MCU上,你可以直接写
GPIOA->ODR |= 0x01,但在Linux中却要通过文件系统操作 - • 什么是内核空间和用户空间? MCU上所有代码都在同一地址空间运行
核心原因:MCU是"裸机"系统,拥有完全控制权;而Linux是一个多任务操作系统,需要遵循操作系统的规则和抽象。
差异理解
MMU与虚拟内存:地址不再是"真实"的
MCU的思维(直接映射)
在MCU上,地址是"真实"的:
// STM32中,0x40020000就是GPIOA的物理地址GPIOA->ODR = 0x01; // 直接操作物理地址
特点:
- • 地址0x40020000就是硬件寄存器的真实位置
Linux的思维(虚拟内存)
在Linux中,地址是"虚拟"的:
// Linux中,你看到的地址0x40020000可能是虚拟地址// 实际物理地址可能完全不同
MMU(Memory Management Unit)的作用:
- • 地址转换:将程序使用的虚拟地址转换为实际的物理地址
形象比喻:
- • MCU:就像你直接住在房子里,门牌号就是真实地址
- • Linux:就像你住在酒店,房间号(虚拟地址)和实际楼层(物理地址)是分开的,前台(MMU)负责转换
为什么需要虚拟内存?
- 1. 安全性:程序无法直接访问其他程序的内存或硬件
- 2. 多任务:每个程序都认为自己在使用完整的地址空间
对开发的影响:
- • 地址0x40020000在你的程序中可能指向完全不同的位置
进程与线程:从"单任务"到"多任务"
MCU的思维(前后台系统或RTOS)
在MCU上,通常是:
// 前后台系统voidmain() {while(1) { task1(); // 任务1 task2(); // 任务2 task3(); // 任务3 }}// 或者RTOSvoidtask1(void *pvParameters) {while(1) {// 任务代码 vTaskDelay(100); }}
特点:
Linux的思维(进程和线程)
在Linux中,有进程和线程的概念:
进程(Process):
- • 进程间通信需要特殊机制(管道、共享内存、消息队列等)
线程(Thread):
形象比喻:
- • MCU任务:就像同一间办公室里的不同员工,共享所有资源
- • Linux进程:就像不同的公司,各自有独立的办公室和资源
- • Linux线程:就像同一公司里的不同部门,共享公司资源但各自工作
对开发的影响:
内核空间与用户空间:权限的分离
MCU的思维(统一空间)
在MCU上:
// 所有代码都在同一权限级别voidgpio_init() {// 直接操作寄存器 GPIOA->MODER |= 0x01;}voiduser_function() { gpio_init(); // 用户代码可以直接调用}
特点:
Linux的思维(空间分离)
在Linux中,系统分为两个空间:
用户空间(User Space):
内核空间(Kernel Space):
┌─────────────────────────────────────┐│ 用户空间 (User Space) ││ ┌─────────┐ ┌─────────┐ ││ │ 应用1 │ │ 应用2 │ ││ └────┬────┘ └────┬────┘ ││ │ │ ││ └──────┬─────┘ ││ │ 系统调用 │├──────────────┼──────────────────────┤│ 内核空间 (Kernel Space) ││ ┌──────────────────────────────┐ ││ │ 设备驱动、文件系统、网络栈 │ ││ └──────────────────────────────┘ ││ │ ││ │ 直接访问 │├──────────────┼──────────────────────┤│ 硬件层 (Hardware) ││ GPIO、UART、I2C等 │└─────────────────────────────────────┘
形象比喻:
- • MCU:就像所有人都在同一个房间里,可以随意操作任何设备
- • Linux:就像有普通员工(用户空间)和管理员(内核空间),普通员工需要通过管理员才能操作设备
对开发的影响:
为什么需要这种分离?
开发流程的转变
交叉编译:为什么不能在目标板上编译?
MCU的思维(本地编译)
在MCU开发中:
PC (开发环境) ↓ 编译.hex / .bin 文件 ↓ 烧录MCU (目标板)
特点:
- • 编译器和目标平台架构相同(都是ARM Cortex-M)
Linux的思维(交叉编译)
在Linux嵌入式开发中:
PC (x86_64架构) ↓ 交叉编译工具链ARM架构的可执行文件 ↓ 传输到目标板MPU (ARM架构,运行Linux)
为什么需要交叉编译?
- 1. 性能差异:PC性能强大,编译速度快;目标板资源有限,编译慢
交叉编译工具链组成:
- • gcc:交叉编译器(如
arm-linux-gnueabihf-gcc) - • binutils:二进制工具(如
objdump、objcopy)
常用交叉编译工具链:
- • ARM:
arm-linux-gnueabihf-gcc(硬浮点) - • ARM64:
aarch64-linux-gnu-gcc
实际使用示例:
# 在PC上编译ARM程序arm-linux-gnueabihf-gcc hello.c -o hello# 将编译好的程序传输到目标板scp hello root@192.168.1.100:/home/root/# 在目标板上运行./hello
根文件系统:Linux的"文件组织方式"
MCU的思维(简单存储)
在MCU上:
Flash存储├── Bootloader (启动代码)├── Application (应用程序)└── Data (数据区,可选)
特点:
Linux的思维(文件系统)
在Linux中,一切皆文件:
根文件系统 (/)├── bin/ (基本命令)├── sbin/ (系统命令)├── etc/ (配置文件)├── dev/ (设备文件)├── proc/ (进程信息)├── sys/ (系统信息)├── usr/ (用户程序)├── var/ (可变数据)└── home/ (用户目录)
根文件系统的作用:
常见的根文件系统类型:
- • Buildroot:自动化构建工具,可以定制根文件系统
- • Debian/Ubuntu:完整的Linux发行版,功能丰富
构建根文件系统的步骤:
对开发的影响:
- • 应用程序通常放在
/usr/bin或/home目录
内核裁剪与编译:定制你的Linux内核
MCU的思维(固定固件)
在MCU上:
选择芯片型号 ↓使用官方固件库 ↓编译生成固件
特点:
Linux的思维(可定制内核)
在Linux中,内核是可以裁剪和定制的:
内核配置选项:
内核编译流程:
# 1. 获取内核源码git clone https://github.com/raspberrypi/linux.git# 2. 配置内核make menuconfig # 图形化配置界面# 或make defconfig # 使用默认配置# 3. 编译内核make -j4 # 使用4个线程并行编译# 4. 安装内核模块make modules_install# 5. 安装内核make install
内核裁剪的原则:
- 2. 驱动可以编译成模块:需要时加载,不需要时卸载
常用配置工具:
- • menuconfig:基于ncurses的文本界面
对开发的影响:
驱动开发:从寄存器操作到file_operations
MCU驱动开发思维
在MCU上,驱动通常是这样的:
// STM32 GPIO驱动示例voidgpio_init(GPIO_TypeDef* GPIOx, uint16_t pin) {// 直接操作寄存器 GPIOx->MODER |= (1 << (pin * 2)); // 设置为输出模式}voidgpio_set(GPIO_TypeDef* GPIOx, uint16_t pin) { GPIOx->BSRR = (1 << pin); // 设置引脚为高}voidgpio_clear(GPIO_TypeDef* GPIOx, uint16_t pin) { GPIOx->BSRR = (1 << (pin + 16)); // 设置引脚为低}// 使用gpio_init(GPIOA, 5);gpio_set(GPIOA, 5);
特点:
Linux驱动开发思维
在Linux中,驱动必须遵循操作系统的框架:
字符设备驱动基本框架
Linux字符设备驱动的核心是file_operations结构体:
#include<linux/module.h>#include<linux/fs.h>#include<linux/cdev.h>// 设备结构体structmy_device {structcdevcdev;// 其他设备特定数据};// 打开设备staticintmy_open(struct inode *inode, struct file *file) {// 初始化设备return0;}// 关闭设备staticintmy_release(struct inode *inode, struct file *file) {// 清理资源return0;}// 读取数据staticssize_tmy_read(struct file *file, char __user *buf, size_t count, loff_t *pos) {// 从设备读取数据到用户空间return count;}// 写入数据staticssize_tmy_write(struct file *file, constchar __user *buf,size_t count, loff_t *pos) {// 从用户空间写入数据到设备return count;}// 控制操作(ioctl)staticlongmy_ioctl(struct file *file, unsignedint cmd, unsignedlong arg) {// 设备特定的控制操作return0;}// file_operations结构体:定义驱动支持的操作staticstructfile_operationsmy_fops = { .owner = THIS_MODULE, .open = my_open, .release = my_release, .read = my_read, .write = my_write, .unlocked_ioctl = my_ioctl,};// 模块初始化staticint __init my_init(void) {// 注册字符设备// 创建设备节点return0;}// 模块退出staticvoid __exit my_exit(void) {// 注销设备// 删除设备节点}module_init(my_init);module_exit(my_exit);MODULE_LICENSE("GPL");
关键概念理解
1. file_operations结构体
- • 应用程序通过系统调用(如
open、read、write)访问设备 - • 内核将这些系统调用路由到对应的
file_operations函数
2. 用户空间和内核空间的数据交换
// 从内核空间复制数据到用户空间copy_to_user(user_buf, kernel_buf, size);// 从用户空间复制数据到内核空间copy_from_user(kernel_buf, user_buf, size);
为什么需要copy_to_user/copy_from_user?
3. 设备节点(/dev/xxx)
应用程序如何使用驱动:
// 应用程序代码int fd = open("/dev/mydevice", O_RDWR); // 打开设备read(fd, buffer, size); // 读取数据write(fd, data, size); // 写入数据ioctl(fd, CMD, arg); // 控制操作close(fd); // 关闭设备
驱动开发的关键差异
驱动开发的实际流程
步骤1:编写驱动代码
- • 实现
file_operations中的必要函数
步骤2:编译驱动
# 编写Makefileobj-m += mydriver.o# 编译make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
步骤3:加载驱动
# 加载模块insmod mydriver.ko# 查看加载的模块lsmod# 卸载模块rmmod mydriver
步骤4:创建设备节点
# 驱动加载后,可能需要手动创建设备节点mknod /dev/mydevice c 250 0# 或使用udev自动创建设备节点
步骤5:测试驱动
// 编写测试程序intmain() {int fd = open("/dev/mydevice", O_RDWR);// 测试读写操作 close(fd);return0;}
总结
从MCU转向Linux嵌入式开发,不仅仅是学习新的技术,更是一次思维方式的转变。MCU开发强调直接控制和实时响应,而Linux开发强调系统抽象和资源管理。