针对于嵌入式软件杂乱的知识点总结起来,提供给读者学习复习对下述内容的强化。
目录
1.字符设备 块设备 网络设备的区别并分别举例?
2.LCD驱动模型
3.总线驱动模型
4.输入子系统模型
5.总线模型匹配规则
6.framebuffer机制?
1.字符设备 块设备 网络设备的区别并分别举例?
特点:字符设备以字节为单位进行输入和输出。每个字节都是独立的,设备无法寻址或读取特定的块;块设备以块为单位进行输入和输出。块是设备中数据的固定大小的块,可以寻址和读取访间方式:字符设备通常是顺序访问;块设备支持随机访问实例:键盘、鼠标、串口设备;SSD(固态硬盘)USB 存储设备。
1.字符设备
字符设备指能够像字节流串行顺序依次进行访问的设备,对它的读写是以字节为单位。字符设备的上层没有磁盘文件系统,所以字符设备的file_operations成员函数就直接由字符设备驱动提供(一般字符设备都会实现相应的fops集),因此file_operations 也就成为了字符设备驱动的核心。
特点:
一个字节一个字节读写的设备
读取数据需要按照先后数据(顺序读取)
每个字符设备在/dev目录下对应一个设备文件,linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备。
常见的字符设备有鼠标、键盘、串口、控制台等
上层应用如何调用底层驱动:
1)应用层的程序open(“/dev/xxx”,mode,flags)打开设备文件,进入内核中,即虚拟文件系统中。
2)VFS层的设备文件有对应的struct inode,其中包含该设备对应的设备号,设备类型,返回的设备的结构体。
3)在驱动层中,根据设备类型和设备号就可以找到对应的设备驱动的结构体,用i_cdev保存。该结构体中有很重要的一个操作函数接口file_operations。
4)在打开设备文件时,会分配一个struct file,将操作函数接口的地址保存在该结构体中。
5)VFS层 向应用层返回一个fd,fd是和struct file相对应,这样,应用层可以通过fd调用操作函数,即通过驱动层调用硬件设备。
ps:可以讲字符设备和块设备归为一类,它们都是可以顺序/随机地进行读取和存储的单元,二者驱动主要在于块设备需要具体的burst实现,对访问也有一定的边界要求。其他的没有什么不同。 网络设备是特殊设备的驱动,它负责接收和发送帧数据,可能是物理帧,也可能是ip数据包,这些特性都有网络驱动决定。它并不存在于/dev下面,所以与一般的设备不同。网络设备是一个net_device结构,并通过register_netdev注册到系统里,最后通过ifconfig -a的命令就能看到。 不论是什么设备,设备级的数据传输都是基本类似的,内核里的数据表示只是一部分,更重要的是总线的访问,例如串行spi,i2c,并行dma等。
2.块设备
块设备以数据块的形式存放数据,如NAND Flash以页为单位存储数据,并采用mount方式挂载块设备。
块设备必须能够随机存取(random access),字符设备则没有这个要求。
块设备除了给内核提供和字符设备一样的接口外,还提供了专门面向块设备的接口,块设备的接口必须支持挂装文件系统,通过此接口,块设备能够容纳文件系统,因此应用程序一般通过文件系统来访问块设备上的内容,而不是直接和设备打交道。
对于块设备而言,上层ext2,jiffs2,fat等文件系统会 实现针对VFS的file_opertations成员函数,所以设备驱动层将看不到file_opeations的存在。磁盘文件系统和设备驱动会将对磁盘上文件的访问转换成对磁盘上柱面和扇区的访问。
特点:
数据以固定长度进行传输,比如512K
从设备的任意位置(可跳)读取,但实际上,块设备会读一定长度的内容,而只返回用户要求访问的内容,所以随机访问实际上还是读了全部内容。
块设备包括硬盘、磁盘、U盘和SD卡等
每个块设备在/dev目录下对应一个设备文件,linux用户程序可以通过设备文件(或称设备节点)来使用驱动程序操作块设备。
块设备可以容纳文件系统,所以一般都通过文件系统来访问,而不是/dev设备节点。
正点原子IMX6u文档中有提到,可以去看看!
大家如果仔细观察的话就会发现有些硬盘或者 NAND Flash 就会标明擦除次数(flash 的特性,写之前要先擦除),比如擦除 100000 次等。因此,为了提高块 设备寿命引入了缓冲区,数据先写入到缓冲区中,等满足一定条件后再一次性写入到真正的物 理存储设备中,这样就减少了对块设备的擦除次数,提高了块设备寿命
都知道字符驱动设备的核心主要在于file_opeations而块设备驱动模型框架主要是block_device_operations
块设备也有操作集,但是file_opeations中是涉及到read、write这些操作的,涉及文件IO内容嘛,但是块设备不一样,众所周知我们都知道他是挂载类型的,那如何从物理块中读写数据?通过request_queue,request和bio
请求队列,请求,遍历请求之类的,点到为止,具体可以去看正点原子文档即可。
3.网络设备
虽然在Linux系统存在一句话叫一切皆文件,无论是各种文本文件还是具体的硬件设备(硬件由设备文件来实现相应)。但是网络设备在Linux内核中却是唯一不体现一切皆设备思想的驱动架构,因为网络设备使用套接字来实现网数据的接受和发送。
网络设备驱动不同于字符设备和块设备,不在/dev下以文件节点代表,而是通过单独的网络接口来代表。
特点:
网络接口没有像字符设备和块设备一样的设备号和/dev设备节点,只有接口名,如eth0,eth1
通过socket操作,而不是open read write
核心在于net_device_ops
设备类型 | 访问方式 | 主要特点 | 典型示例 | 设备文件路径 |
---|---|---|---|---|
字符设备 | 按字节访问 | 无缓冲,顺序访问,直接与硬件交互 | 串口(UART)、I2C、SPI、键盘、鼠标 | /dev/ttyS0 (串口)、/dev/input/mouse0 (鼠标) |
块设备 | 按块(通常 512B 或 4KB)访问 | 缓冲读写,随机访问,有块缓存机制 | 硬盘(SATA、SSD)、SD 卡、U 盘 | /dev/sda (硬盘)、/dev/mmcblk0 (SD 卡) |
网络设备 | 按数据包(帧)访问 | 基于协议栈通信,使用套接字接口 | 以太网网卡(eth0)、WLAN、LoRa | /sys/class/net/eth0 (以太网) |
2.LCD驱动模型
在嵌入式 Linux 或 RTOS 系统中,LCD(液晶显示屏)驱动一般采用 帧缓冲(Framebuffer) 模型,或直接使用 SPI、I2C、MIPI、RGB 并行 接口进行数据传输。
不同 LCD 屏幕的驱动方式不同,主要有以下几种:
- 字符型 LCD(如 1602、2004 LCD) —— I2C / SPI / GPIO 控制
- 图形 LCD(如 ST7735、ILI9341) —— SPI / 并行总线控制
- TFT LCD(如 RGB888、MIPI DSI 屏幕) —— 并行 RGB / MIPI DSI / HDMI
Linux LCD 设备树示例
如果你的 LCD 采用 SPI 接口(如 ST7735、ILI9341),需要在设备树 dts
中定义:
&spi0 {st7735: lcd@0 {compatible = "sitronix,st7735";reg = <0>; // SPI 片选 0spi-max-frequency = <50000000>;dc-gpios = <&gpio3 12 0>; // 数据/命令引脚reset-gpios = <&gpio3 11 0>; // 复位引脚backlight = <&backlight>;};
};
这个设备树配置告诉内核:
- 使用 SPI0 进行通信
- 指定 GPIO 控制 DC、复位引脚
- 设置 SPI 频率
2. LCD 驱动代码示例(Linux Framebuffer 方式)
下面是一个 基本的 Framebuffer 驱动,适用于 并行接口 LCD:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/fb.h>
#include <linux/uaccess.h>
#include <linux/delay.h>
#include <linux/io.h>#define LCD_WIDTH 320
#define LCD_HEIGHT 240struct fb_info *lcd_info;
static u32 *lcd_buffer;/* 打开 LCD */
static int lcd_open(struct fb_info *info, int user)
{printk(KERN_INFO "LCD Opened\n");return 0;
}/* 释放 LCD */
static int lcd_release(struct fb_info *info, int user)
{printk(KERN_INFO "LCD Released\n");return 0;
}/* 写入 Framebuffer */
static ssize_t lcd_write(struct fb_info *info, const char __user *buf, size_t count, loff_t *ppos)
{if (copy_from_user(lcd_buffer, buf, count))return -EFAULT;return count;
}/* LCD 操作结构 */
static struct fb_ops lcd_ops = {.owner = THIS_MODULE,.fb_open = lcd_open,.fb_release = lcd_release,.fb_write = lcd_write,
};/* 初始化 LCD */
static int __init lcd_init(void)
{lcd_info = framebuffer_alloc(sizeof(u32) * LCD_WIDTH * LCD_HEIGHT, NULL);if (!lcd_info)return -ENOMEM;lcd_buffer = (u32 *)lcd_info->screen_base;lcd_info->var.xres = LCD_WIDTH;lcd_info->var.yres = LCD_HEIGHT;lcd_info->fix.line_length = LCD_WIDTH * 4;lcd_info->fbops = &lcd_ops;if (register_framebuffer(lcd_info) < 0) {framebuffer_release(lcd_info);return -EINVAL;}printk(KERN_INFO "LCD Driver Initialized\n");return 0;
}/* 卸载 LCD */
static void __exit lcd_exit(void)
{unregister_framebuffer(lcd_info);framebuffer_release(lcd_info);printk(KERN_INFO "LCD Driver Removed\n");
}module_init(lcd_init);
module_exit(lcd_exit);
MODULE_LICENSE("GPL");
驱动说明
注册 Framebuffer
提供 open
、write
等接口
用户空间可以通过 /dev/fb0
写入像素数据
数据最终存储到 lcd_buffer
,并映射到物理 LCD
#include <fcntl.h>
#include <unistd.h>
#include <string.h>int main()
{int fd = open("/dev/fb0", O_RDWR);if (fd < 0) return -1;char buffer[320 * 240 * 4]; // 320x240 RGBAmemset(buffer, 0xFF, sizeof(buffer)); // 填充白色write(fd, buffer, sizeof(buffer));close(fd);return 0;
}
3.总线驱动模型
在 Linux 内核中,设备驱动模型由 三大子系统 组成:
- 总线(Bus) —— 负责管理 设备(Device) 和 驱动(Driver) 之间的匹配与通信。
- 设备(Device) —— 具体的硬件设备,如 SPI 设备、I2C 设备、USB 设备等。
- 驱动(Driver) —— 负责控制 设备,提供接口供应用程序访问。
结构体 | 作用 |
---|---|
struct bus_type | 定义总线 |
struct device | 代表总线上的设备 |
struct device_driver | 代表驱动 |
struct bus_type {const char *name; // 总线名称struct device *dev_root; // 总线根设备struct list_head devices; // 设备链表struct list_head drivers; // 驱动链表int (*match)(struct device *dev, struct device_driver *drv); // 设备与驱动匹配规则
};
bus_type
负责管理 设备和驱动的匹配(match)
device
代表 硬件设备
device_driver
代表 驱动
4.输入子系统模型
Linux 输入子系统(Input Subsystem)用于处理各种 输入设备(如 键盘、鼠标、触摸屏、遥控器、加速度计等)。它为驱动程序提供统一的 事件传递机制,避免用户态程序直接操作底层硬件。
输入子系统的主要职责:
接收硬件输入事件
标准化输入设备的数据格式
通过 /dev/input/eventX
设备文件向用户空间提供统一接口
结构体 | 作用 |
---|---|
struct input_dev | 代表输入设备(Input Device),负责上报输入事件 |
struct input_handler | 事件处理器(Event Handler),处理不同类型的事件 |
struct input_event | 具体输入事件(如按键按下/松开、坐标变化) |
struct input_device_id | 设备和处理器的匹配表 |
struct input_dev {const char *name; // 设备名称struct input_id id; // 设备 ID(厂商、产品、版本)unsigned long evbit[EV_MAX]; // 支持的事件类型unsigned long keybit[KEY_MAX];// 支持的按键unsigned long relbit[REL_MAX];// 支持的相对事件(鼠标)unsigned long absbit[ABS_MAX];// 支持的绝对事件(触摸屏)void (*event)(struct input_dev *, unsigned int, unsigned int, int);
};
evbit[]
设备支持的 输入事件(如按键EV_KEY
、鼠标移动EV_REL
)。keybit[]
设备支持的 具体按键(如KEY_A
、KEY_ENTER
)。relbit[]
设备支持的 相对坐标(鼠标的REL_X
、REL_Y
)。absbit[]
设备支持的 绝对坐标(触摸屏的ABS_X
、ABS_Y
)。
#include <linux/module.h>
#include <linux/input.h>
#include <linux/timer.h>static struct input_dev *virtual_key_dev;
static struct timer_list key_timer;static void key_timer_func(struct timer_list *t)
{static int key_value = 0;key_value = !key_value; // 模拟按键按下/松开input_report_key(virtual_key_dev, KEY_A, key_value);input_sync(virtual_key_dev); // 同步事件mod_timer(&key_timer, jiffies + HZ); // 1 秒后触发
}static int __init virtual_key_init(void)
{virtual_key_dev = input_allocate_device(); // 分配 input_devif (!virtual_key_dev) {pr_err("Failed to allocate input device\n");return -ENOMEM;}virtual_key_dev->name = "Virtual Key Device";virtual_key_dev->evbit[0] = BIT_MASK(EV_KEY); // 设备支持按键virtual_key_dev->keybit[BIT_WORD(KEY_A)] |= BIT_MASK(KEY_A); // 设备支持 A 键if (input_register_device(virtual_key_dev)) {pr_err("Failed to register input device\n");input_free_device(virtual_key_dev);return -EINVAL;}timer_setup(&key_timer, key_timer_func, 0);mod_timer(&key_timer, jiffies + HZ); // 1 秒后触发pr_info("Virtual Key Device Registered\n");return 0;
}static void __exit virtual_key_exit(void)
{del_timer(&key_timer);input_unregister_device(virtual_key_dev);pr_info("Virtual Key Device Unregistered\n");
}module_init(virtual_key_init);
module_exit(virtual_key_exit);
MODULE_LICENSE("GPL");
input_dev | 代表输入设备 |
input_event | 代表输入事件(按键、触摸、鼠标等) |
input_handler | 处理输入事件 |
input_report_key() | 上报按键事件 |
input_register_device() | 注册输入设备 |
input_unregister_device() | 注销输入设备 |
5.总线模型匹配规则
总线,设备,驱动。匹配规则就是当有一个新的设备挂起时,总线被唤醒,match函数被调用,用device名字去跟本总线下的所有驱动名字去比较。相反就是用驱动的名字去device链表中和所有device的名字比较。如果匹配上,才会调用驱动中的probe函数,否则不调用。至于先后顺序,鉴于个人理解,不会有影响,不管谁先谁后,bus都会完成匹配工作。谈谈对Linux设备驱动模型的认识:设备驱动模型的出现主要有三个好处,设备与驱动分离,驱动可移植性增强:设备驱动抽象结构以总线结构表示看起来更加清晰明了,谁是属于哪一条bus的:最后,就是大家最熟悉的热插拔了,设备与驱动分离很好的奠定了热插拔机制。
在 Linux 设备驱动模型(Device Model)中,总线(Bus)用于连接 设备(Device) 和 驱动(Driver),并提供匹配、管理和电源控制等功能。
Linux 设备驱动采用 "设备-驱动-总线" 三层架构:
- 设备(Device):硬件设备,如 I2C 设备、SPI 设备、USB 设备等。
- 驱动(Driver):控制设备的代码,实现设备的初始化、数据传输等。
- 总线(Bus):管理设备和驱动程序,并负责匹配它们。
struct bus_type {const char *name; // 总线名称int (*match)(struct device *dev, struct device_driver *drv);int (*probe)(struct device *dev);int (*remove)(struct device *dev);
};
适用于 ARM、嵌入式设备(I2C、SPI、GPIO),匹配规则:
compatible
属性匹配of_match_table
- 设备树
compatible
字符串必须与驱动匹配
i2c_device: my_i2c@50 {compatible = "mycompany,my_i2c_device";reg = <0x50>;
};
static const struct of_device_id my_i2c_of_match[] = {{ .compatible = "mycompany,my_i2c_device" },{ }
};
MODULE_DEVICE_TABLE(of, my_i2c_of_match);static struct i2c_driver my_i2c_driver = {.driver = {.name = "my_i2c",.of_match_table = my_i2c_of_match, // 设备树匹配表},.probe = my_i2c_probe,
};
适用于 I2C、SPI、PCI、USB 设备,匹配规则:
- 设备驱动注册时提供 设备 ID 表
- 设备注册时 ID 需要匹配驱动中的 ID
static const struct i2c_device_id my_i2c_id[] = {{ "my_i2c_device", 0 },{ }
};
MODULE_DEVICE_TABLE(i2c, my_i2c_id);static struct i2c_driver my_i2c_driver = {.driver = {.name = "my_i2c",},.id_table = my_i2c_id, // ID 匹配表.probe = my_i2c_probe,
};
I2C 设备注册
struct i2c_board_info my_i2c_info = {I2C_BOARD_INFO("my_i2c_device", 0x50),
};
i2c_new_device(adapter, &my_i2c_info);
I2C_BOARD_INFO("my_i2c_device", 0x50)
设备名称 =my_i2c_id
i2c_register_driver()
遍历 ID 表 匹配成功后调用probe()
设备类型 | 匹配方式 |
---|---|
设备树设备(I2C、SPI、GPIO) | of_match_table (compatible 匹配) |
I2C、SPI 设备 | i2c_device_id / spi_device_id |
PCI 设备 | pci_device_id (Vendor ID + Device ID ) |
USB 设备 | usb_device_id (Vendor ID + Product ID ) |
ACPI 设备 | acpi_device_id |
平台设备 | platform_device.name 匹配 platform_driver.name |
Linux 总线模型使用 match()
进行设备和驱动的匹配,每种总线(I2C、PCI、USB)有不同的匹配方式。
I2C/SPI 设备常用 of_match_table
(设备树匹配)或 id_table
(ID 表匹配)。
USB/PCI 设备基于 Vendor ID + Product ID
进行匹配。
平台设备匹配 platform_device.name
和 platform_driver.name
。
6.framebuffer机制?
Linux抽象出FrameBuffer这个设备来供用户态进程实现直接写屏。用户可以将Framebuffer看成是显示内存的一个映像,通过mmap将其映射到进程地址空间之后,就可以直接进行读写操作,而写操作可以立即反应在屏幕上。这种操作是抽象的,统一的。用户不必关心物理内存、换贡机制等等具体细节,这些都是由Framebuffer设备驱动来完成的。通过mmap调用把显卡的物理内存空间映射到用户空间来进行操作。
ramebuffer(帧缓冲) 是 Linux 内核提供的一个 通用图形显示接口,用于驱动 LCD、OLED、HDMI 显示屏。
它允许用户空间进程直接向 内存中的缓冲区 写入像素数据,内核驱动程序会将这些数据映射到物理屏幕上。
Linux 下的 Framebuffer 设备通常在 /dev/fbX
目录下,比如:
/dev/fb0 # 第一个 Framebuffer 设备
/dev/fb1 # 第二个 Framebuffer 设备
用户可以通过 open()
、write()
或 mmap()
直接访问 Framebuffer,完成屏幕显示。
如果你的 LCD 采用 SPI 接口,需要在设备树 dts
中配置:
&spi0 {ili9341: lcd@0 {compatible = "sitronix,ili9341";reg = <0>; // 片选 0spi-max-frequency = <50000000>;dc-gpios = <&gpio3 12 0>; // DC 数据/命令引脚reset-gpios = <&gpio3 11 0>; // 复位引脚};
};
这告诉内核:
- 使用 SPI0 设备
- 指定 GPIO 控制 DC、复位引脚
- 设置 SPI 频率
完整的 Framebuffer 驱动(适用于并行 RGB LCD)
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/fb.h>
#include <linux/uaccess.h>
#include <linux/delay.h>
#include <linux/io.h>#define LCD_WIDTH 320
#define LCD_HEIGHT 240struct fb_info *lcd_info;
static u32 *lcd_buffer;/* 打开 LCD */
static int lcd_open(struct fb_info *info, int user)
{printk(KERN_INFO "LCD Opened\n");return 0;
}/* 释放 LCD */
static int lcd_release(struct fb_info *info, int user)
{printk(KERN_INFO "LCD Released\n");return 0;
}/* 写入 Framebuffer */
static ssize_t lcd_write(struct fb_info *info, const char __user *buf, size_t count, loff_t *ppos)
{if (copy_from_user(lcd_buffer, buf, count))return -EFAULT;return count;
}/* LCD 操作结构 */
static struct fb_ops lcd_ops = {.owner = THIS_MODULE,.fb_open = lcd_open,.fb_release = lcd_release,.fb_write = lcd_write,
};/* 初始化 LCD */
static int __init lcd_init(void)
{lcd_info = framebuffer_alloc(sizeof(u32) * LCD_WIDTH * LCD_HEIGHT, NULL);if (!lcd_info)return -ENOMEM;lcd_buffer = (u32 *)lcd_info->screen_base;lcd_info->var.xres = LCD_WIDTH;lcd_info->var.yres = LCD_HEIGHT;lcd_info->fix.line_length = LCD_WIDTH * 4;lcd_info->fbops = &lcd_ops;if (register_framebuffer(lcd_info) < 0) {framebuffer_release(lcd_info);return -EINVAL;}printk(KERN_INFO "LCD Driver Initialized\n");return 0;
}/* 卸载 LCD */
static void __exit lcd_exit(void)
{unregister_framebuffer(lcd_info);framebuffer_release(lcd_info);printk(KERN_INFO "LCD Driver Removed\n");
}module_init(lcd_init);
module_exit(lcd_exit);
MODULE_LICENSE("GPL");
LCD 类型 | 推荐驱动方式 |
---|---|
RGB 并行 LCD | Framebuffer (/dev/fb0 ) |
SPI LCD (如 ILI9341) | SPI 驱动 + Framebuffer |
MIPI DSI LCD | DRM/KMS |