字符设备驱动程序

2023年 1月 24日 68点热度 0人点赞 0条评论

引言

  1. 轮询: 好资源
  2. 中断, 陷入
  3. poll, 增加超时
  4. 异步通知:signal, 驱动程序通知应用程序

概念介绍

应用程序与内核, 驱动的关系

  • u-boot: 启动内核
  • 内核: 启动应用程序
  • 应用: 读写文件, 硬件操作 (点灯, 获取按键值), 写应用程序的不应该知道芯片手册, 以及怎么接线, 而是应用使用 open read write 这种标准的接口, 这种接口就是由驱动实现的
  • c 库: 包装了 open read write, swi val 引发一个中断, 进入异常处理程序
  • 系统调用接口 (system call interface): 根据发生中断的原因, 调用不同的处理函数: sys_open, sys_write, sys_read
  • VFS: 虚拟文件系统
  • 驱动:led_open, led_read, led_write
int main() {
    int fd1, fd2;
    int val = 1;

    fd1 = open("/dev/led", O_RDWR);
    write(fd1, &val, 4);

    fd2 = open("hello.txt", O_RDWR);
    write(fd2, &val, 4);
}

框架

file

第 1 个驱动程序: 点灯

电路图

file

file

file

  • 接地(0)可以使灯亮
  • GPF 接到电路上了,让 GPF 中的位为0,可使灯亮
  • GPFCON 的地址是 0x56000050, 占两个字节,用于控制 IO 端口复用的类型(输入,输出,中断还是保留)
  • GPFDAT 的地址是 0x56000054, 占一个字节,用于设置或者读取数据的

源码分析: 驱动框架, 硬件操作 (ioremap), 用户空间与内核空间的数据传输

  1. 写出对应的 led_write, led_open
  2. 定义结构 struct file_operations
  3. 通过结构注册到系统中 (告诉系统有这些操作),register_chrdev
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

static struct class *firstdrv_class;
static struct class_device  *firstdrv_class_dev;

volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;

static int first_drv_open(struct inode *inode, struct file *file)
{
    //printk("first_drv_open\n");
    /* 配置 GPF4, 5, 6 为输出 */
    *gpfcon &= ~((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)));
    *gpfcon |= ((0x1<<(4*2)) | (0x1<<(5*2)) | (0x1<<(6*2)));
    return 0;
}

static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
    int val;

    //printk("first_drv_write\n");

    copy_from_user(&val, buf, count); //    copy_to_user();

    if (val == 1)
    {
        // 点灯
        *gpfdat &= ~((1<<4) | (1<<5) | (1<<6));
    }
    else
    {
        // 灭灯
        *gpfdat |= (1<<4) | (1<<5) | (1<<6);
    }

    return 0;
}

static struct file_operations first_drv_fops = {
    .owner  =   THIS_MODULE,    /* 这是一个宏, 推向编译模块时自动创建的__this_module 变量 */
    .open   =   first_drv_open,     
    .write  =   first_drv_write,       
};

int major;
static int first_drv_init(void)
{
    major = register_chrdev(0, "first_drv", &first_drv_fops); // 注册, 告诉内核

    firstdrv_class = class_create(THIS_MODULE, "firstdrv");

    firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz"); /* /dev/xyz */

    gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
    gpfdat = gpfcon + 1;

    return 0;
}

static void first_drv_exit(void)
{
    unregister_chrdev(major, "first_drv"); // 卸载

    class_device_unregister(firstdrv_class_dev);
    class_destroy(firstdrv_class);
    iounmap(gpfcon);
}

module_init(first_drv_init);
module_exit(first_drv_exit);

MODULE_LICENSE("GPL");

怎么编译驱动:Makefile

测试

       设备名称  设备类型   主设备号   次设备号
mknode /dev/xxx    c         111        0

register_chrdev 第一个参数为 0 的话, 系统会自动分配一个空缺的主设备号.

insmod xxx.ko        # 安装新得驱动
cat /proc/devices    # 查看设备
rmmod xxx            # 卸载驱动, 后面没有 .ko
lsmod                # 查看安装的模块

ls -l /dev/xxx       # 查看设备的详细信息: 主设备号, 设备名称

如果 /dev/xxx 存在, 但是打不开, 就要看下主设备号和注册的是否一致.

  1. 驱动程序中可以自动分配主设备号, 也可以手动指定空闲的主设备号
  2. 应用程序打开文件的时候, 是没有的, 是手工建立的 mknod /dev/xxx c 主设备号 次设备号; 另外可以通过 udev, mdev 根据系统信息 (ls /sys/devices) 来自动创建
  3. cd /sys/class/firstdrv/xyz, cat dev
  4. 为啥可以自动创建设备呢? 因为在 rcS 中将 /sbin/mdev 这个路径写入了 /proc/sys/kernel/hotplug 这个文件中, 一旦有设备修改, 就会通知到 /sbin/mdev.
// 自动创建设备节点 /dev/xyz
firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz"); /* /dev/xyz */

完善硬件操作

  1. 查看原理图
  2. 看 2440 手册
  3. 写代码: 单片机是直接操作物理地址, 驱动中操作的是虚拟地址 (用 ioremap 将物理地址映射为虚拟地址)

ioremap, iounmap, copy_from_user, copy_to_user

  • GPFCON: 配置 GPF 4, 5, 6 为输出
  • GPFDAT: 设置

小结

  1. 写驱动框架
  2. 完善硬件的操作: 单片机是直接操作物理地址, Linux 是将物理地址映射到虚拟地址, 然后操作虚拟地址的
  3. 看原理图, 看芯片手册

可以看下 linux 源码中的 s3c2440 中的点灯驱动.

第 2 个驱动程序: 查询的方式获取按键值

驱动的框架整体和点灯的那个差不多

源码分析

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

static struct class *seconddrv_class;
static struct class_device  *seconddrv_class_dev;

volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;

volatile unsigned long *gpgcon;
volatile unsigned long *gpgdat;

static int second_drv_open(struct inode *inode, struct file *file)
{
    /* 配置 GPF0, 2 为输入引脚 */
    *gpfcon &= ~((0x3<<(0*2)) | (0x3<<(2*2)));

    /* 配置 GPG3, 11 为输入引脚 */
    *gpgcon &= ~((0x3<<(3*2)) | (0x3<<(11*2)));

    return 0;
}

ssize_t second_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
    /* 返回 4 个引脚的电平 */
    unsigned char key_vals[4];
    int regval;

    if (size != sizeof(key_vals))
        return -EINVAL;

    /* 读 GPF0, 2 */
    regval = *gpfdat;
    key_vals[0] = (regval & (1<<0)) ? 1 : 0;
    key_vals[1] = (regval & (1<<2)) ? 1 : 0;

    /* 读 GPG3, 11 */
    regval = *gpgdat;
    key_vals[2] = (regval & (1<<3)) ? 1 : 0;
    key_vals[3] = (regval & (1<<11)) ? 1 : 0;

    copy_to_user(buf, key_vals, sizeof(key_vals));

    return sizeof(key_vals);
}

static struct file_operations sencod_drv_fops = {
    .owner  =   THIS_MODULE,    /* 这是一个宏, 推向编译模块时自动创建的__this_module 变量 */
    .open   =   second_drv_open,     
    .read   =   second_drv_read,       
};

int major;
static int second_drv_init(void)
{
    major = register_chrdev(0, "second_drv", &sencod_drv_fops);

    seconddrv_class = class_create(THIS_MODULE, "second_drv");

    seconddrv_class_dev = class_device_create(seconddrv_class, NULL, MKDEV(major, 0), NULL, "buttons"); /* /dev/buttons */

    gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
    gpfdat = gpfcon + 1;

    gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);
    gpgdat = gpgcon + 1;

    return 0;
}

static void second_drv_exit(void)
{
    unregister_chrdev(major, "second_drv");
    class_device_unregister(seconddrv_class_dev);
    class_destroy(seconddrv_class);
    iounmap(gpfcon);
    iounmap(gpgcon);
    return 0;
}

module_init(second_drv_init);

module_exit(second_drv_exit);

MODULE_LICENSE("GPL");

测试


#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

/* seconddrvtest 
  */
int main(int argc, char **argv)
{
    int fd;
    unsigned char key_vals[4];
    int cnt = 0;

    fd = open("/dev/buttons", O_RDWR);
    if (fd < 0)
    {
        printf("can't open!\n");
    }

    while (1)
    {
        read(fd, key_vals, sizeof(key_vals));
        if (! key_vals[0] || ! key_vals[1] || ! key_vals[2] || ! key_vals[3])
        {
            printf("%04d key pressed: %d %d %d %d\n", cnt++, key_vals[0], key_vals[1], key_vals[2], key_vals[3]);
        }
    }

    return 0;
}

主要就是那个 while(1) 不断地循环读取.

第 3 个驱动程序: 中断的方式获取按键值

  1. 按键按下
  2. CPU 发生中断
  3. 跳转到异常向量的入口执行
  4. 保存被中断的现场; 执行中断处理函数; 恢复被中断的现场

Linux 中断体系架构

异常向量, 中断注册, 清中断, 上边沿, 下边沿, 外部中断, 中断屏蔽, 内部中断

  1. trap_init 拷贝异常向量
  2. vector_irq 是用一个宏来实现的
  3. irq_handler
  4. asm_do_IRQ

单片机里面的:

  1. 分辨是哪个中断
  2. 调用处理函数
  3. 清中断

asm_do_IRQ 中的流程:

  1. irq_desc[irq]
  2. set_irq_handler
  3. s3c24xx_init_irq
  4. chip->ack(irq)

free_irq 卸载中断

源码分析: 休眠, 唤醒

测试

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/irq.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

static struct class *thirddrv_class;
static struct class_device  *thirddrv_class_dev;

volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;

volatile unsigned long *gpgcon;
volatile unsigned long *gpgdat;

static DECLARE_WAIT_QUEUE_HEAD(button_waitq);

/* 中断事件标志, 中断服务程序将它置 1, third_drv_read 将它清 0 */
static volatile int ev_press = 0;

struct pin_desc{
    unsigned int pin;
    unsigned int key_val;
};

/* 键值: 按下时, 0x01, 0x02, 0x03, 0x04 */
/* 键值: 松开时, 0x81, 0x82, 0x83, 0x84 */
static unsigned char key_val;

struct pin_desc pins_desc[4] = {
    {S3C2410_GPF0, 0x01},
    {S3C2410_GPF2, 0x02},
    {S3C2410_GPG3, 0x03},
    {S3C2410_GPG11, 0x04},
};

/*
  * 确定按键值
  */
static irqreturn_t buttons_irq(int irq, void *dev_id)
{
    struct pin_desc * pindesc = (struct pin_desc *)dev_id;
    unsigned int pinval;

    pinval = s3c2410_gpio_getpin(pindesc->pin);

    if (pinval)
    {
        /* 松开 */
        key_val = 0x80 | pindesc->key_val;
    }
    else
    {
        /* 按下 */
        key_val = pindesc->key_val;
    }

    ev_press = 1;                  /* 表示中断发生了 */
    wake_up_interruptible(&button_waitq);   /* 唤醒休眠的进程 */

    return IRQ_RETVAL(IRQ_HANDLED);
}

static int third_drv_open(struct inode *inode, struct file *file)
{
    /* 配置 GPF0, 2 为输入引脚 */
    /* 配置 GPG3, 11 为输入引脚 */
    request_irq(IRQ_EINT0,  buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
    request_irq(IRQ_EINT2,  buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
    request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
    request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]); 

    return 0;
}

ssize_t third_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
    if (size != 1)
        return -EINVAL;

    /* 如果没有按键动作, 休眠 */
    wait_event_interruptible(button_waitq, ev_press);

    /* 如果有按键动作, 返回键值 */
    copy_to_user(buf, &key_val, 1);
    ev_press = 0;

    return 1;
}

int third_drv_close(struct inode *inode, struct file *file)
{
    free_irq(IRQ_EINT0, &pins_desc[0]);
    free_irq(IRQ_EINT2, &pins_desc[1]);
    free_irq(IRQ_EINT11, &pins_desc[2]);
    free_irq(IRQ_EINT19, &pins_desc[3]);
    return 0;
}

static struct file_operations sencod_drv_fops = {
    .owner   =  THIS_MODULE,    /* 这是一个宏, 推向编译模块时自动创建的__this_module 变量 */
    .open    =  third_drv_open,     
    .read    =  third_drv_read,    
    .release =  third_drv_close,       
};

int major;
static int third_drv_init(void)
{
    major = register_chrdev(0, "third_drv", &sencod_drv_fops);

    thirddrv_class = class_create(THIS_MODULE, "third_drv");

    thirddrv_class_dev = class_device_create(thirddrv_class, NULL, MKDEV(major, 0), NULL, "buttons"); /* /dev/buttons */

    gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
    gpfdat = gpfcon + 1;

    gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);
    gpgdat = gpgcon + 1;

    return 0;
}

static void third_drv_exit(void)
{
    unregister_chrdev(major, "third_drv");
    class_device_unregister(thirddrv_class_dev);
    class_destroy(thirddrv_class);
    iounmap(gpfcon);
    iounmap(gpgcon);
    return 0;
}

module_init(third_drv_init);

module_exit(third_drv_exit);

MODULE_LICENSE("GPL");

按键驱动深化: poll 或 select 操作

源码分析 poll 机制

测试

按键驱动深化: 同步, 互斥, 阻塞

源码分析: 机制分析

测试

按键驱动深化: 异步通知机制

源码分析: 机制分析

测试

定时器防抖

本文来自:https://blog.duhbb.com

本文链接地址:字符设备驱动程序,英雄不问来路,转载请注明出处,谢谢。

有话想说:那就赶紧去给我留言吧。

rainbow

这个人很懒,什么都没留下

文章评论