尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

嵌入式Linux驱动入门:通过FIFO与tmpfs模拟设备文件操作

嵌入式Linux驱动入门:通过FIFO与tmpfs模拟设备文件操作 1. 项目概述为什么嵌入式开发需要“每日一练”搞嵌入式开发的朋友尤其是从单片机转向Linux平台的朋友应该都经历过一个阶段看理论书感觉都懂一上手实操就懵。驱动怎么加载设备树节点怎么绑定一个简单的字符设备驱动编译出来怎么就是 insmod 失败这些问题光靠看书和看视频是解决不了的必须得动手而且是持续地、有体系地动手。这就是我发起这个“基于Linux平台的嵌入式开发每日一练”系列的初衷。这个系列不是系统地讲解某个庞大的知识体系而是聚焦于一个个具体的、微小的、但实际开发中高频出现的“技能点”。第二期我们继续深挖。假设你已经跟着第一期完成了最基本的交叉编译环境搭建并成功在开发板上运行了一个“Hello World”。那么接下来我们要面对的就是嵌入式Linux开发真正的核心如何让我们的程序与硬件打交道答案就是驱动。但今天我们不直接写一个完整的驱动那太庞大了。我们从驱动的基础也是Linux内核与用户空间通信的基石之一——系统调用和文件操作开始练起。本期每日一练的目标是在Linux应用层通过标准的文件I/O接口去操作一个模拟的硬件设备。我们会自己创建一个简单的“设备文件”并编写用户态程序去读写它。这个过程能让你深刻理解“一切皆文件”的哲学在驱动层面是如何体现的为后续学习真正的字符设备驱动打下坚实的认知基础。无论你是嵌入式新手还是想巩固基础的熟手跟着练一遍都会有收获。2. 核心思路拆解从“文件”到“设备”的桥梁在开始敲代码之前我们必须把思路理清楚。在桌面Linux上我们读写一个文本文件使用open,read,write,close这些函数感觉天经地义。但在嵌入式领域当我们说“要操作一个LED”或“读取一个温度传感器”很多新手会下意识地去寻找专门的“LED控制函数”或“传感器读取库”。这个思路需要扭转。在Linux的视角里LED、传感器、按键、蜂鸣器这些硬件设备在应用层程序员看来都应该是一个“文件”。操作LED就是向某个“文件”写入“开”或“关”的命令读取传感器就是从某个“文件”中读出数据。这个充当硬件的“文件”就是设备文件通常位于/dev目录下。例如/dev/ttyUSB0代表一个串口/dev/gpiochip0代表GPIO控制器。那么我们如何创建一个这样的“文件”并让它背后执行我们自定义的逻辑呢完整的路径涉及驱动开发但今天我们可以用一个取巧的、完全在用户空间实现的方法来模拟这个过程使用FIFO命名管道和内存文件系统。这听起来可能和硬件不直接相关但它所体现的“打开-读写-关闭”范式以及内核与用户空间的数据流思想是完全相通的。2.1 为什么选择FIFO和tmpfs来模拟降低入门门槛真正的字符设备驱动需要编写内核模块涉及file_operations结构体、注册注销函数等对初学者来说信息量过大。而FIFO是Linux内核提供的一种进程间通信机制它也在/dev下呈现为一个文件支持标准的open/read/write操作行为上与简单的字符设备高度相似。聚焦核心概念我们暂时抛开复杂的内核机制先专注于理解“应用层程序如何通过文件接口与一个‘事物’交互”。FIFO完美地扮演了这个“事物”的角色。tmpfs内存文件系统的作用我们将在一个由tmpfs挂载的目录如/tmp/mydev下创建FIFO。tmpfs的所有内容都存在于内存中速度极快且系统重启后自动清除非常适合做这种临时性的实验不会污染你的根文件系统。所以本期的练习路线图是先在内存中创建一个目录然后在该目录下创建一个FIFO文件最后编写两个C程序。一个程序模拟“驱动”或“服务端”负责打开这个FIFO并等待读取数据另一个程序模拟“应用程序”或“客户端”负责打开这个FIFO并写入数据。通过这个过程你将亲眼看到数据如何通过一个“文件”从一个进程传递到另一个进程这正是驱动工作的最简化模型。注意这只是一个用于理解原理的模拟。真正的硬件驱动其“后端”是直接操作硬件寄存器或通过内核子系统如GPIO、I2C框架与硬件通信而不是另一个用户进程。但“前端”给应用层提供的文件操作接口逻辑是完全一致的。3. 环境准备与基础操作工欲善其事必先利其器。这个练习可以在任何Linux环境下进行包括你的Ubuntu PC、树莓派、或者其他的ARM开发板。如果是在X86电脑上练习我们同样使用GCC编译如果目标是在ARM开发板上运行则需要使用交叉编译工具链。为了普适性我们先以本地编译为例。3.1 创建实验工作区首先我们创建一个干净的工作目录所有文件都放在这里。mkdir -p ~/embedded_daily_practice/02 cd ~/embedded_daily_practice/023.2 创建内存中的设备文件目录我们需要一个位置来存放模拟的设备文件。使用tmpfs挂载一个目录到内存中是最佳实践。# 创建一个用于挂载的目录 sudo mkdir -p /mnt/tmp_mydev # 将tmpfs文件系统挂载到该目录。size10m 限制了最大使用内存为10MB sudo mount -t tmpfs -o size10m tmpfs /mnt/tmp_mydev # 为了操作方便我们更改目录所有者到当前用户 sudo chown $USER:$USER /mnt/tmp_mydev # 进入这个目录 cd /mnt/tmp_mydev现在/mnt/tmp_mydev目录下的所有文件都存在于内存中。你可以用df -h命令查看会发现有一个tmpfs挂载在/mnt/tmp_mydev容量约为10MB。3.3 创建FIFO设备文件FIFO文件可以通过mkfifo命令创建。我们给它起一个听起来像设备的名字比如my_embedded_device。mkfifo my_embedded_device执行ls -l查看你会看到类似下面的输出prw-rw-r-- 1 user user 0 Apr 10 15:30 my_embedded_device注意第一个字符是p代表这是一个管道pipe文件也就是FIFO。这与普通文件-、目录d或字符设备文件c是不同的类型但对于open、read、write等系统调用其行为模式是我们需要关注的焦点。4. 模拟“驱动”端程序编写现在我们来编写第一个C程序它扮演的是“设备驱动”或“服务”的角色。它的任务是以只读方式打开我们创建的FIFO文件。阻塞等待直到有“应用程序”向这个文件写入数据。读取数据并打印出来。循环执行持续服务。创建文件device_simulator.c#include stdio.h #include stdlib.h #include fcntl.h #include unistd.h #include string.h #include errno.h #define DEVICE_PATH /mnt/tmp_mydev/my_embedded_device #define BUFFER_SIZE 128 int main() { int fd; char buffer[BUFFER_SIZE]; ssize_t bytes_read; printf(嵌入式设备模拟器驱动端启动...\n); printf(正在等待应用程序连接并发送数据...\n); // 1. 以只读方式打开FIFO。注意默认情况下只读打开会阻塞直到有另一个进程以写方式打开这个FIFO。 fd open(DEVICE_PATH, O_RDONLY); if (fd -1) { perror(打开设备文件失败); exit(EXIT_FAILURE); } printf(设备文件打开成功文件描述符: %d\n, fd); // 2. 循环读取数据 while (1) { memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区 bytes_read read(fd, buffer, BUFFER_SIZE - 1); // 留一个位置给字符串结束符 if (bytes_read -1) { perror(读取数据失败); break; } else if (bytes_read 0) { // read返回0通常意味着写端已经关闭了文件描述符。 printf(应用程序已断开连接。等待新的连接...\n); // 注意当写端关闭后读端再次read会立即返回0而不是阻塞。 // 为了模拟驱动持续服务我们需要关闭当前fd然后重新打开以等待新的连接。 close(fd); fd open(DEVICE_PATH, O_RDONLY); if (fd -1) { perror(重新打开设备文件失败); break; } printf(重新打开设备文件等待新数据...\n); continue; } // 成功读取到数据 buffer[bytes_read] \0; // 确保字符串正确终止 printf([驱动端] 接收到 %zd 字节数据: %s\n, bytes_read, buffer); // 这里可以添加对数据的解析和处理逻辑。 // 例如如果收到LED_ON就模拟点亮LED打印一条消息。 if (strncmp(buffer, LED_ON, 6) 0) { printf( 执行操作模拟点亮LED\n); } else if (strncmp(buffer, LED_OFF, 7) 0) { printf( 执行操作模拟关闭LED\n); } else if (strncmp(buffer, GET_TEMP, 8) 0) { // 模拟读取传感器数据并“写回”给应用层。 // 注意这是一个FIFO是单向的。要回复数据需要另一个FIFO或改用双向通信机制。 // 这里我们先只打印。 printf( 执行操作模拟读取温度传感器值假设为25.5°C\n); } } // 清理工作虽然上面的循环理论上无限但我们可以用CtrlC打断 close(fd); printf(设备模拟器退出。\n); return 0; }关键点解析与实操心得阻塞打开open(DEVICE_PATH, O_RDONLY)这一行在FIFO的上下文中是阻塞的。程序会停在这里直到有另一个进程我们的“应用端”程序以写方式O_WRONLY打开同一个FIFO文件。这完美模拟了驱动等待应用程序来“打开设备”的场景。读取与写端关闭read函数在FIFO上的行为很关键。当有数据时读取数据当没有数据但写端仍打开时会阻塞等待当写端关闭了所有文件描述符时read会返回0。我们的代码通过检查bytes_read 0来处理“应用断开连接”的情况并尝试重新打开FIFO以等待下一个连接。这是一个重要的细节在真正的驱动编程中也需要处理用户空间程序突然关闭的情况release函数。数据处理我们在代码中加入了对特定字符串如LED_ON的解析。这模拟了驱动根据应用层下发的不同指令IOCTL命令或写入的不同数据来执行不同的硬件操作。在实际驱动中这部分逻辑会放在驱动的write或ioctl函数实现里。5. 模拟“应用”端程序编写接下来是“应用程序”它代表用户空间需要操作硬件的程序。它的任务是以只写方式打开FIFO文件。从标准输入或预设指令获取要发送的数据。将数据写入FIFO文件。关闭文件。创建文件application.c#include stdio.h #include stdlib.h #include fcntl.h #include unistd.h #include string.h #include errno.h #define DEVICE_PATH /mnt/tmp_mydev/my_embedded_device #define BUFFER_SIZE 128 int main(int argc, char *argv[]) { int fd; char buffer[BUFFER_SIZE]; ssize_t bytes_written; // 1. 以只写方式打开FIFO。同样会阻塞直到有进程我们的驱动端以读方式打开它。 printf(应用程序启动尝试连接设备...\n); fd open(DEVICE_PATH, O_WRONLY); if (fd -1) { perror(打开设备文件失败); exit(EXIT_FAILURE); } printf(设备文件打开成功文件描述符: %d\n, fd); // 2. 获取要发送的数据。 // 为了演示我们支持两种方式命令行参数输入或交互式输入。 if (argc 1) { // 方式一使用命令行参数 strncpy(buffer, argv[1], BUFFER_SIZE - 1); buffer[BUFFER_SIZE - 1] \0; printf(将从命令行参数发送数据: %s\n, buffer); } else { // 方式二交互式输入 printf(请输入要发送给设备的数据 (或输入 quit 退出): ); if (fgets(buffer, BUFFER_SIZE, stdin) NULL) { perror(读取输入失败); close(fd); exit(EXIT_FAILURE); } // 去掉末尾的换行符 buffer[strcspn(buffer, \n)] 0; if (strcmp(buffer, quit) 0) { printf(退出程序。\n); close(fd); exit(EXIT_SUCCESS); } } // 3. 写入数据到设备文件 bytes_written write(fd, buffer, strlen(buffer)); if (bytes_written -1) { perror(写入数据失败); } else { printf([应用端] 成功发送 %zd 字节数据: %s\n, bytes_written, buffer); } // 4. 短暂延迟确保数据被读取非必需仅用于演示观察 sleep(1); // 5. 关闭文件描述符。这将导致驱动端的read返回0。 close(fd); printf(应用程序关闭设备连接。\n); return 0; }关键点解析与实操心得阻塞打开的对称性应用端的open(DEVICE_PATH, O_WRONLY)也会阻塞直到驱动端以读方式打开。这确保了通信双方在开始传输前都已就绪。在实际硬件操作中open设备文件可能会触发驱动里的open函数进行硬件初始化。数据写入write函数将数据从用户空间缓冲区buffer通过内核传递到FIFO中。如果驱动端读端的缓冲区已满write调用也会阻塞。这体现了流量控制。在真实驱动中驱动的write函数会收到用户传递过来的数据缓冲区指针和长度。连接的生命周期应用端close(fd)后驱动端会收到“文件结束”信号read返回0。这模拟了应用程序退出或关闭设备文件的情景。在真实驱动中这会触发驱动里的release函数进行资源清理。6. 编译与运行测试现在我们让整个模拟系统跑起来。6.1 编译程序打开两个终端窗口都进入到工作目录~/embedded_daily_practice/02。在第一个终端用于运行驱动端gcc device_simulator.c -o device_simulator在第二个终端用于运行应用端gcc application.c -o application6.2 运行与交互测试首先启动驱动端服务端在第一个终端执行./device_simulator你会看到输出嵌入式设备模拟器驱动端启动... 正在等待应用程序连接并发送数据...程序此时阻塞在open函数等待应用端连接。然后启动应用端客户端在第二个终端执行./application LED_ON应用端输出应用程序启动尝试连接设备... 设备文件打开成功文件描述符: 3 将从命令行参数发送数据: LED_ON [应用端] 成功发送 6 字节数据: LED_ON 应用程序关闭设备连接。与此同时观察驱动端终端你会看到设备文件打开成功文件描述符: 3 [驱动端] 接收到 6 字节数据: LED_ON 执行操作模拟点亮LED 应用程序已断开连接。等待新的连接... 重新打开设备文件等待新数据...成功了你通过一个“文件”成功地从应用进程向“驱动”进程发送了一条指令“LED_ON”并且“驱动”识别并处理了这条指令。测试交互式输入。在应用端终端不带参数运行./application程序会提示你输入。输入GET_TEMP并回车。 驱动端会显示接收到GET_TEMP命令并模拟读取温度传感器。测试多个连续操作。你可以保持驱动端运行在应用端多次执行./application LED_OFF等命令。每次驱动端处理完一个连接后都会重新进入等待状态就像一个常驻的硬件服务。7. 问题排查与深度思考在实际操作中你可能会遇到一些问题。下面是一些常见情况及其解决方法。7.1 常见问题速查表问题现象可能原因解决方案open失败提示No such file or directoryFIFO文件my_embedded_device不存在。检查是否在/mnt/tmp_mydev目录下执行了mkfifo my_embedded_device命令。open失败提示Permission denied当前用户对FIFO文件或上级目录没有读写权限。使用ls -l检查文件权限。确保/mnt/tmp_mydev目录的所有者是当前用户。可以用sudo chown修改。驱动端或应用端在open处长时间阻塞无反应另一端没有启动。FIFO要求读写两端同时打开才能建立连接。确保两个程序都已启动。检查路径DEVICE_PATH在两个程序中是否完全一致大小写、路径。应用端write后立即退出但驱动端没收到数据或显示“应用程序已断开连接”。应用端写入数据后立即close数据可能还在内核缓冲区未及时被驱动端read取出。在应用端write后可以加一个短暂的sleep(1)给驱动端留出读取时间。或者让应用端在close前也等待一下输入。我们的代码已经添加了sleep(1)。驱动端收到数据后无法再次进入等待。代码中处理read返回0的逻辑有误未能成功重新打开FIFO。检查驱动端代码中在bytes_read 0的分支里是否先close(fd)再重新open。确保重新打开时的路径和模式正确。编译时提示warning: implicit declaration of function ‘strncmp’没有包含string.h头文件。在C文件开头添加#include string.h。7.2 从模拟到真实的跨越通过这个练习我们体验了“文件操作接口”作为软硬件交互桥梁的抽象魅力。但必须清楚这只是一个高度简化的模型。真实的Linux字符设备驱动与这个模型的主要区别在于后端是内核与硬件而非另一个用户进程真实驱动的read/write函数内部是通过ioremap、readl/writel针对MMIO或i2c_transfer、spi_sync等内核API与物理硬件通信或者处理DMA传输。需要内核模块驱动代码是编译成内核模块.ko文件的通过insmod加载到内核空间。它需要实现file_operations这个结构体并将自己注册到内核的字符设备子系统使用register_chrdev或更现代的cdev接口。设备节点由内核创建/dev下的设备文件不是手动mkfifo创建的而是驱动在注册成功后由内核或配合udev规则自动创建的。mknod命令可以手动创建设备节点但需要提供主设备号和次设备号这些信息来自驱动注册。更复杂的控制接口除了read/write真实驱动通常需要实现ioctl函数用于实现那些不适合用简单字节流表示的操作比如设置波特率、读取寄存器、控制GPIO方向等。并发与同步真实驱动必须考虑多个应用进程同时打开设备、同时读写的情况需要使用信号量、互斥锁等内核同步机制来保护共享数据如硬件寄存器状态。7.3 下一步的练习方向理解了今天的模拟练习你的嵌入式Linux驱动学习之路就可以正式启航了。建议的下一步练习顺序编写最简单的内核模块写一个除了打印日志什么也不做的hello.ko模块学习Makefile编写、模块编译、加载(insmod)、卸载(rmmod)、查看日志(dmesg)的完整流程。实现一个简单的字符设备驱动框架创建一个虚拟的字符设备实现file_operations中的open、release、read、write等函数的基本骨架比如在read中返回一个固定的字符串。学习使用register_chrdev和class_create/device_create来自动创建设备节点。为虚拟设备增加ioctl控制学习定义自己的IOCTL命令号并在驱动中实现unlocked_ioctl函数处理来自应用层的控制命令。结合具体硬件找一个简单的硬件比如一个通过GPIO控制的LED。学习在驱动中调用GPIO子系统gpio_request,gpio_direction_output,gpio_set_value将write或ioctl命令与具体的硬件操作关联起来。每天解决一个小问题拆解一个知识点坚持下去你对嵌入式Linux系统的理解就会从应用层深入到驱动层最终贯通整个软硬件体系。今天的练习就是构建这个体系坚实的第一步。
返回列表