找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

634

积分

0

好友

92

主题
发表于 14 小时前 | 查看: 3| 回复: 0

在嵌入式开发中,输入设备驱动是使用率最高的模块之一。无论是按键、触摸屏还是鼠标键盘,其底层都依赖于输入设备驱动。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;
}



上一篇:K8s管理面板Kite:轻量级现代化K8s可视化管理,简化集群运维
下一篇:LangGraph源码分析:多智能体架构核心机制与底层实现解析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-10 20:07 , Processed in 0.092750 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表