该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
第02节_字符设备驱动的传统写法
在上一节视频里我们介绍了三种编写驱动的方法,也对比了它们的优缺点,后面我们将使用比较快速的方法写出驱动程序,因为写驱动程序不是我们这套视频的重点,所以尽快的把驱动程序写出来,给大家展示一下。
这节视频我们使用传统的方法编写字符驱动程序,以最简单的点灯驱动程序为示例。
先回顾下写字符设备驱动的五个步骤:
1.2.3.分配/设置/注册file_operations
4.入口
5.出口
所谓分配file_operations,我们可以定义一个file_operations结构体,就不需要分配了。
static struct file_operations myled_oprs = {
.owner = THIS_MODULE, //表示这个模块本身
.open = led_open,
.write = led_write,
.release = led_release,
};
定义好了file_operations结构体,再去入口函数注册结构体。
static int myled_init(void)
{
major = register_chrdev(0, "myled", &myled_oprs); return 0;
}
第一个参数:主设备号写0,让系统为我们分配;
第二个参数:设置名字,没有特殊要求;
第三个参数:file_operations结构体;
对应的出口操作进行相反向操作:
static void myled_exit(void)
{
unregister_chrdev(major, "myled");
}
然后用宏module_init对入口、出口函数进行修饰,表示它们和普通函数不一样:
module_init(myled_init);
module_exit(myled_exit);
module_init(myled_init)实际就是int init_module(void) __attribute__((alias("myled_init"))),表示myled_init的别名是init_module,以后就可以使用init_module来引用myled_init。
此外,还要加上GPL协议:
MODULE_LICENSE("GPL");
写到这里,驱动程序的框架已经搭建起来了,接下来实现具体的硬件操作函数:led_open()和led_write()。
在led_open()里把对应的引脚配置为输出引脚,在led_write()根据应用程序传入的数据点灯,让其输出高电平或低电平。
为了让程序更具有扩展性,把GPIO的寄存器放在一个数组里:
static unsigned int gpio_base[] = {
0x56000000, /* GPACON */
0x56000010, /* GPBCON */
0x56000020, /* GPCCON */
0x56000030, /* GPDCON */
0x56000040, /* GPECON */
0x56000050, /* GPFCON */
0x56000060, /* GPGCON */
0x56000070, /* GPHCON */
0, /* GPICON */
0x560000D0, /* GPJCON */
};
定义好了引脚的组,还得确定使用该组的哪个引脚,使用宏来确定哪个引脚:
#define S3C2440_GPA(n) (0<<16 | n)
#define S3C2440_GPB(n) (1<<16 | n)
#define S3C2440_GPC(n) (2<<16 | n)
#define S3C2440_GPD(n) (3<<16 | n)
#define S3C2440_GPE(n) (4<<16 | n)
#define S3C2440_GPF(n) (5<<16 | n)
#define S3C2440_GPG(n) (6<<16 | n)
#define S3C2440_GPH(n) (7<<16 | n)
#define S3C2440_GPI(n) (8<<16 | n)
#define S3C2440_GPJ(n) (9<<16 | n)
后面就可以向对应宏传入对应位,得到对应组的对应引脚。
查看原理图,知道我们要使用的引脚是GPF5,因此定义 led_pin = s3c2440_GPF(5)。
static int led_open (struct inode *node, struct file *filp)
{
/* 把LED引脚配置为输出引脚 */
/* GPF5 - 0x56000050 */
int bank = led_pin >> 16;
int base = gpio_base[bank]; int pin = led_pin & 0xffff;
gpio_con = ioremap(base, 8);
if (gpio_con) {
printk("ioremap(0x%x) = 0x%x\n", base, gpio_con);
}
else {
return -EINVAL;
}
gpio_dat = gpio_con + 1; *gpio_con &= ~(3<
*gpio_con |= (1<
}
在Linux中,不能直接操作基地址,需要使用ioremap()映射。
对于基地址,定义全局指针来表示,gpio_con表示控制寄存器,gpio_dat表示数据寄存器。
这里将GPF5的第二个引脚先清空,再设置为1,表示输出引脚。
接下来是写函数:
static ssize_t led_write (struct file *filp, const char __user *buf, size_t size, loff_t *off)
{
/* 根据APP传入的值来设置LED引脚 */
unsigned char val;
int pin = led_pin & 0xffff;
copy_from_user(&val, buf, 1); if (val)
{
/* 点灯 */
*gpio_dat &= ~(1<
}
else
{
/* 灭灯 */
*gpio_dat |= (1<
} return 1; /* 已写入1个数据 */
}
注意这里的__user宏起强调作用,告诉你buf来自应用空间,在内核里不能直接使用。
使用copy_from_user()将用户空间的数据拷贝到内核空间。
再根据传入的值,设置gpio_dat的值,来点亮或者熄灭pin所对应的灯。
至此,这个驱动程序已经具备操作硬件的功能,但我们还要增加一些内容,比如我们先注册驱动后,自动创建节点信息。
在入口函数里,使用class_create()创建class,并且使用device_create()创建设备。
static int myled_init(void)
{
major = register_chrdev(0, "myled", &myled_oprs); led_class = class_create(THIS_MODULE, "myled");
device_create(led_class, NULL, MKDEV(major, 0), NULL, "led"); /* /dev/led */ return 0;
}
出口函数需要进行相反操作:
static void myled_exit(void)
{
unregister_chrdev(major, "myled");
device_destroy(led_class, MKDEV(major, 0));
class_destroy(led_class);
}
还有在release函数里,释放前面的iormap()的资源
static int led_release (struct inode *node, struct file *filp)
{
printk("iounmap(0x%x)\n", gpio_con);
iounmap(gpio_con);
return 0;
}
最后把以前的测试程序拷贝过来,简单修改一下,见网盘led_driver/001_led_drv_traditional/ledtest.c。
可以看出,这种传统写驱动程序的方法把硬件资源写在了代码里,换个LED,换个引脚,就得去修改 led_pin = s3c2440_GPF(5),然后重新编译,加载。