在嵌入式开发中,输入设备驱动是使用率最高的模块之一。无论是按键、触摸屏还是鼠标键盘,其底层都依赖于输入设备驱动。Linux内核已将其抽象为三层架构,简化了开发流程:核心层(Input Core)负责协调与提供API;事件处理层(Event Handler)负责与用户空间交互,生成/dev/input/eventX节点并将标准化事件上报给应用程序;驱动层(Device Driver)则需要开发者根据具体硬件实现,读取寄存器状态并向核心层上报事件。
开发一个输入设备驱动通常需要以下四步:1)分配input_dev结构体;2)设置设备能力(Capability),告知内核设备类型(如键盘、鼠标)及支持的按键;3)注册设备;4)在硬件中断服务函数中上报事件。
以下是一个基于Platform总线模型的按键Input驱动框架示例:
#include <linux/input.h>
/* ... 其他头文件 ... */
struct my_input_data {
struct input_dev *input; // 核心结构体
int irq;
};
/* 中断处理函数 */
static irqreturn_t my_key_handler(int irq, void *dev_id)
{
struct my_input_data *data = dev_id;
// 假设读取 GPIO 电平,0表示按下,1表示松开
int val = gpio_get_value(MY_GPIO_PIN);
/* 核心操作:上报事件 (Report Event)
* 参数: input设备, 事件类型(按键), 具体的键值(KEY_POWER), 状态(1按下/0松开)
*/
input_report_key(data->input, KEY_POWER, !val);
/* 千万别忘!同步事件 *
* 告诉内核:这次上报结束了,可以发给用户空间了。
*/
input_sync(data->input);
return IRQ_HANDLED;
}
static int my_probe(struct platform_device *pdev)
{
struct my_input_data *data;
int error;
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
/* 1. 分配 Input 设备 */
data->input = devm_input_allocate_device(&pdev->dev);
if (!data->input) return -ENOMEM;
/* 2. 设置能力 (我是个按键,我支持 KEY_POWER) */
data->input->name = “my_power_button“;
// EV_KEY 表示支持按键事件
// EV_ABS 表示支持绝对坐标 (触摸屏用)
// EV_REL 表示支持相对坐标 (鼠标用)
set_bit(EV_KEY, data->input->evbit);
set_bit(KEY_POWER, data->input->keybit);
/* 3. 注册 Input 设备 */
error = input_register_device(data->input);
if (error) return error;
/* 4. 申请中断 (略,同上一章) */
// ... request_irq(..., my_key_handler, ...);
return 0;
}
块设备驱动开发
块设备(Block Device)指以固定大小的数据块为单位进行随机访问的设备,例如硬盘、SSD或SD卡,这些是系统编程中管理存储的核心。与字符设备的流式访问不同,块设备驱动的核心在于块设备层(Block Layer),它负责I/O请求的调度、合并与队列管理。用户程序的读写请求并非直接交给驱动,而是先进入队列,经调度器优化后批量下发,从而实现高效的并发访问。
驱动的主要任务是注册设备并提供一个函数来处理来自内核I/O队列的请求。gendisk(通用磁盘)结构体代表一个逻辑磁盘,驱动需要创建实例并填充容量、分区等信息,然后将其与请求队列关联。request_queue是I/O调度的枢纽,驱动需分配并初始化它,并指定核心的request_fn请求处理函数。
现代Linux驱动(如NVMe)普遍采用blk-mq(多队列)架构以获得更高性能,但其核心思想依然是响应和处理I/O请求。
#include <linux/blkdev.h>
#include <linux/genhd.h>
/* 定义一个简单的设备结构体 */
struct my_block_dev {
struct gendisk *gd; // 磁盘结构体
struct request_queue *queue; // 请求队列
void *data_buffer; // 模拟磁盘的内存空间 (RAM Disk)
size_t size; // 磁盘大小
};
/* 核心:请求处理函数 (Request Function)
* 内核把一堆I/O请求排好队放在 q 里,调用这个函数让驱动处理
*/
static void my_request_func(struct request_queue *q)
{
struct request *req;
// 循环从队列里取出请求
while ((req = blk_fetch_request(q)) != NULL) {
// 1. 检查是不是不认识的命令
if (req->cmd_type != REQ_TYPE_FS) {
__blk_end_request_all(req, -EIO);
continue;
}
// 2. 处理请求 (核心逻辑)
// 这里的逻辑就是把 req 里的数据拷贝到 data_buffer,或者反过来
my_transfer(req);
// 3. 告诉内核:请求处理完毕
__blk_end_request_all(req, 0);
}
}
static int __init my_block_init(void)
{
// 1. 分配请求队列
my_dev.queue = blk_init_queue(my_request_func, &my_lock);
// 2. 分配 gendisk
my_dev.gd = alloc_disk(3); // 次设备号数量
// 3. 设置 gendisk 属性
my_dev.gd->major = my_major;
my_dev.gd->first_minor = 0;
my_dev.gd->fops = &my_bops; // 类似字符设备的 file_operations
my_dev.gd->queue = my_dev.queue; // 绑定队列
set_capacity(my_dev.gd, SECTORS_COUNT); // 设置容量
// 4. 注册 (此时 /dev/xxx 设备节点就会出现)
add_disk(my_dev.gd);
return 0;
}
网络设备驱动开发
网络设备驱动与字符、块设备有显著不同:首先,它在/dev目录下没有对应的设备节点,应用程序只能通过Socket接口访问;其次,它处理的是数据包(Packet);再者,网络设备的工作模式常是被动的,随时可能因收到远端数据而触发中断。
struct net_device代表一个网络接口(如eth0),而struct sk_buff是贯穿内核网络协议栈的核心数据结构,承载着从网卡到Socket的所有数据包,TCP/IP协议栈通过移动其内部的指针来高效添加或剥离各层协议头,而非复制数据。
网络驱动主要实现两个方向的数据流:发包(TX)和收包(RX)。
发包(TX) 由ndo_start_xmit函数实现。当用户调用send()时,内核协议栈将数据封装成sk_buff,然后调用驱动的此函数将其发送出去。
/* net_device_ops 操作集 */
static const struct net_device_ops my_netdev_ops = {
.ndo_start_xmit = my_start_xmit, // 发送函数
.ndo_set_mac_address = eth_mac_addr,
// ...
};
/* 发送函数 */
netdev_tx_t my_start_xmit(struct sk_buff *skb, struct net_device *dev)
{
// 1. 映射 DMA
// 告诉网卡:数据在 skb->data,长度是 skb->len,你去搬吧
// 2. 操作网卡寄存器,触发发送
writel(dma_addr, REG_TX_ADDR);
writel(CMD_SEND, REG_CMD);
// 3. 记录时间戳,释放 skb (由驱动负责释放)
dev_kfree_skb(skb);
return NETDEV_TX_OK;
}
收包(RX) 则通常采用NAPI(New API)机制,这是一种“中断+轮询”的混合模型。其工作流程为:第一个数据包到达触发硬件中断 -> 在中断处理函数中关闭接收中断 -> 调度软中断(SoftIRQ)进行轮询(Poll) -> 在轮询函数中批量收取所有数据包并提交给内核协议栈 -> 收包完毕则重新开启硬件中断。这种方式能有效避免在高流量下因频繁中断导致CPU负载过高。
/* NAPI 轮询函数 (在软中断上下文执行) */
static int my_poll(struct napi_struct *napi, int budget)
{
int work_done = 0;
while (work_done < budget) {
// 1. 读取网卡状态,看有没有包
if (!has_packet()) break;
// 2. 分配 sk_buff
skb = dev_alloc_skb(len);
// 3. 从硬件把数据拷入 skb (或 DMA)
memcpy(skb->data, hw_buf, len);
// 4. 提交给协议栈 (Hand over to Kernel)
netif_receive_skb(skb);
work_done++;
}
// 如果包都处理完了,重新开启硬件中断
if (work_done < budget) {
napi_complete_done(napi, work_done);
enable_irq_rx();
}
return work_done;
}
/* 中断处理函数 */
static irqreturn_t my_interrupt(int irq, void *dev_id)
{
if (is_rx_interrupt) {
// 1. 关闭接收中断 (防止 CPU 被淹没)
disable_irq_rx();
// 2. 调度 NAPI 轮询
napi_schedule(&priv->napi);
}
return IRQ_HANDLED;
}