字符设备驱动leds
内核版本:4.12.9
编译器:arm-linux-gcc-4.4.3
本驱动基于jz2440 v2开发板,实现3个led设备的驱动程序。
代码如下:
1 #include <linux/module.h> 2 #include <linux/kernel.h> 3 #include <linux/fs.h> 4 #include <linux/init.h> 5 #include <linux/delay.h> 6 #include <asm/uaccess.h> 7 #include <asm/irq.h> 8 #include <asm/io.h> 9 #include <linux/cdev.h> 10 #include <linux/uaccess.h> 11 12 #define DEVICE_NAME "leds" 13 #define LED_MAJOR 231 14 15 #define LED_COUNT (4) 16 int major; 17 int minor; 18 static struct cdev led_cdev; 19 20 static struct class *leds_class; 21 static struct device *leds_class_devs[4]; 22 23 24 /* bit0<=>D10, 0:亮, 1:灭 25 * bit1<=>D11, 0:亮, 1:灭 26 * bit2<=>D12, 0:亮, 1:灭 27 */ 28 static char leds_status = 0x0; 29 static DEFINE_SEMAPHORE(leds_lock);//定义赋值 30 31 volatile unsigned long *gpfcon = NULL; 32 volatile unsigned long *gpfdat = NULL; 33 34 35 #define S3C2410_GPF4 4 36 #define S3C2410_GPF5 5 37 #define S3C2410_GPF6 6 38 39 #define S3C2410_GPF4_OUTP 1 40 #define S3C2410_GPF5_OUTP 1 41 #define S3C2410_GPF6_OUTP 1 42 43 44 static void s3c2410_gpio_cfgpin(int pin, int type) 45 { 46 *gpfcon &= ~(0x3<<(pin*2)); 47 *gpfcon |= (1<<(pin*2)); 48 49 } 50 51 static void s3c2410_gpio_setpin(int pin, int data) 52 { 53 if(data == 0) 54 *gpfdat &= ~(1<<pin); 55 else 56 *gpfdat |= (1<<pin); 57 } 58 59 /* 应用程序对设备文件/dev/leds执行open(...)时, 60 * 就会调用s3c24xx_leds_open函数 61 */ 62 static int s3c24xx_leds_open(struct inode* inode, struct file* file) 63 { 64 int minor = MINOR(inode->i_rdev); 65 66 switch(minor) 67 { 68 case 0:/*/dev/leds*/ 69 { 70 /* 配置GPF4,5,6为输出*/ 71 *gpfcon &= ~((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2))); 72 *gpfcon |= ((0x1<<(4*2)) | (0x1<<(5*2)) | (0x01<<(6*2))); 73 74 // 都输出0 75 *gpfdat &= ~(1<<4); 76 *gpfdat &= ~(1<<5); 77 *gpfdat &= ~(1<<6); 78 79 down(&leds_lock); 80 leds_status = 0x0; 81 up(&leds_lock); 82 printk("/dev/leds open!\n"); 83 84 }break; 85 86 case 1: /* /dev/led1 */ 87 { 88 s3c2410_gpio_cfgpin(S3C2410_GPF4, S3C2410_GPF4_OUTP); 89 s3c2410_gpio_setpin(S3C2410_GPF4, 0); 90 91 down(&leds_lock); 92 leds_status &= ~(1<<0); 93 up(&leds_lock); 94 95 break; 96 } 97 98 case 2: /* /dev/led2 */ 99 { 100 s3c2410_gpio_cfgpin(S3C2410_GPF5, S3C2410_GPF5_OUTP); 101 s3c2410_gpio_setpin(S3C2410_GPF5, 0); 102 leds_status &= ~(1<<1); 103 break; 104 } 105 106 case 3: /* /dev/led3 */ 107 { 108 s3c2410_gpio_cfgpin(S3C2410_GPF6, S3C2410_GPF6_OUTP); 109 s3c2410_gpio_setpin(S3C2410_GPF6, 0); 110 111 down(&leds_lock); 112 leds_status &= ~(1<<2); 113 up(&leds_lock); 114 115 break; 116 } 117 default: 118 break; 119 120 } 121 122 return 0; 123 } 124 125 static int s3c24xx_leds_read(struct file *filp, char __user *buff, 126 size_t count, loff_t *offp) 127 { 128 int minor = MINOR(filp->f_inode->i_rdev); 129 char val; 130 long ret; 131 132 switch (minor) 133 { 134 case 0: /* /dev/leds */ 135 { 136 137 ret = copy_to_user(buff, (const void *)&leds_status, 1); 138 break; 139 } 140 141 case 1: /* /dev/led1 */ 142 { 143 down(&leds_lock); 144 val = leds_status & 0x1; 145 up(&leds_lock); 146 ret = copy_to_user(buff, (const void *)&val, 1); 147 break; 148 } 149 150 case 2: /* /dev/led2 */ 151 { 152 down(&leds_lock); 153 val = (leds_status>>1) & 0x1; 154 up(&leds_lock); 155 ret = copy_to_user(buff, (const void *)&val, 1); 156 break; 157 } 158 159 case 3: /* /dev/led3 */ 160 { 161 down(&leds_lock); 162 val = (leds_status>>2) & 0x1; 163 up(&leds_lock); 164 ret = copy_to_user(buff, (const void *)&val, 1); 165 break; 166 } 167 168 } 169 170 return 1; 171 } 172 173 static ssize_t s3c24xx_leds_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos) 174 { 175 int minor = MINOR(file->f_inode->i_rdev); 176 char val; 177 178 long ret = 0; 179 ret = copy_from_user(&val, buf, 1); 180 if(ret > 0) 181 { 182 printk("s3c24xx_leds_write():copy_from_user fail! ret:%ld", ret); 183 return -EFAULT; 184 } 185 186 switch (minor) 187 { 188 case 0: /* /dev/leds */ 189 { 190 if(val == 0)//点灯 191 { 192 printk("led_write led on\n"); 193 *gpfdat &= ~((1<<4) | (1<<5) | (1<<6)); 194 } 195 else//灭灯 196 { 197 printk("led_write led off\n"); 198 *gpfdat |= ((1<<4) | (1<<5) | (1<<6)); 199 } 200 201 202 down(&leds_lock); 203 leds_status = val; 204 up(&leds_lock); 205 printk("/dev/leds write val:%d!\n", val); 206 break; 207 } 208 209 case 1: /* /dev/led1 */ 210 { 211 s3c2410_gpio_setpin(S3C2410_GPF4, val); 212 213 if (val == 0) 214 { 215 down(&leds_lock); 216 leds_status &= ~(1<<0); 217 up(&leds_lock); 218 } 219 else 220 { 221 down(&leds_lock); 222 leds_status |= (1<<0); 223 up(&leds_lock); 224 } 225 break; 226 } 227 228 case 2: /* /dev/led2 */ 229 { 230 s3c2410_gpio_setpin(S3C2410_GPF5, val); 231 if (val == 0) 232 { 233 down(&leds_lock); 234 leds_status &= ~(1<<1); 235 up(&leds_lock); 236 } 237 else 238 { 239 down(&leds_lock); 240 leds_status |= (1<<1); 241 up(&leds_lock); 242 } 243 break; 244 } 245 246 case 3: /* /dev/led3 */ 247 { 248 s3c2410_gpio_setpin(S3C2410_GPF6, val); 249 if (val == 0) 250 { 251 down(&leds_lock); 252 leds_status &= ~(1<<2); 253 up(&leds_lock); 254 } 255 else 256 { 257 down(&leds_lock); 258 leds_status |= (1<<2); 259 up(&leds_lock); 260 } 261 break; 262 } 263 264 } 265 266 return 1; 267 } 268 269 270 /* 这个结构是字符设备驱动程序的核心 271 * 当应用程序操作设备文件时所调用的open、read、write等函数, 272 * 最终会调用这个结构中指定的对应函数 273 */ 274 static struct file_operations s3c24xx_leds_fops = { 275 .owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */ 276 .open = s3c24xx_leds_open, 277 .read = s3c24xx_leds_read, 278 .write = s3c24xx_leds_write, 279 }; 280 281 /* 282 * 执行insmod命令时就会调用这个函数 283 */ 284 static int __init s3c24xx_leds_init(void) 285 { 286 int minor = 0; 287 int result = 0; 288 dev_t devid; 289 290 major = LED_MAJOR; 291 292 devid = MKDEV(major, 0);//从主设备号major,次设备号0,得到dev_t类型 293 if(major) 294 { 295 result = register_chrdev_region(devid, LED_COUNT, "leds");//注册字符设备 296 minor = MINOR(devid); 297 } 298 else 299 { 300 result = alloc_chrdev_region(&devid, 0, LED_COUNT, "leds");//注册字符设备 301 major = MAJOR(devid); //从dev_t类型得到主设备 302 minor = MINOR(devid); 303 } 304 printk("led major=%d, minor=%d\r\n", major, minor); 305 306 cdev_init(&led_cdev, &s3c24xx_leds_fops); 307 cdev_add(&led_cdev, devid, LED_COUNT); 308 leds_class = class_create(THIS_MODULE, "myleds");//在/sys/class/下创建类目录 309 if (IS_ERR(leds_class)) 310 { 311 printk("class_create is error!\n"); 312 return PTR_ERR(leds_class); 313 } 314 printk("create leds class ok!\n"); 315 //在/dev目录下创建设备节点 316 leds_class_devs[0] = device_create(leds_class, NULL, MKDEV(LED_MAJOR, 0), NULL, "leds"); 317 printk("create device leds ok!\n"); 318 319 for (minor = 1; minor < 4; minor++) 320 { 321 leds_class_devs[minor] = device_create(leds_class, NULL, MKDEV(LED_MAJOR, minor), NULL, "led%d", minor); 322 printk("create led%d ok!\n", minor); 323 if (unlikely(IS_ERR(leds_class_devs[minor]))) 324 { 325 printk("unlikely is error!\n"); 326 return PTR_ERR(leds_class_devs[minor]); 327 } 328 } 329 330 gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16); //映射到虚拟地址 331 gpfdat = gpfcon + 1; 332 333 printk(DEVICE_NAME " initialized\n"); 334 return 0; 335 } 336 337 /* 338 * 执行rmmod命令时就会调用这个函数 339 */ 340 static void __exit s3c24xx_leds_exit(void) 341 { 342 int minor; 343 /* 卸载驱动程序 */ 344 345 //1.销毁设备 346 for (minor = 0; minor < 4; minor++) 347 { 348 device_destroy(leds_class, MKDEV(LED_MAJOR, minor)); 349 } 350 //2.销毁类 351 class_destroy(leds_class); 352 //3.销毁cdev 353 cdev_del(&led_cdev); 354 355 356 unregister_chrdev_region(MKDEV(LED_MAJOR, 0), LED_COUNT); 357 iounmap(gpfcon); 358 359 } 360 361 /* 这两行指定驱动程序的初始化函数和卸载函数 */ 362 module_init(s3c24xx_leds_init); 363 module_exit(s3c24xx_leds_exit); 364 365 /* 描述驱动程序的一些信息,不是必须的 */ 366 MODULE_AUTHOR("jz2440"); 367 MODULE_VERSION("0.1.0"); 368 MODULE_DESCRIPTION("S3C2410/S3C2440 LED Driver"); 369 MODULE_LICENSE("GPL");
测试代码:
1 #include <sys/types.h> 2 #include <sys/stat.h> 3 #include <fcntl.h> 4 #include <stdio.h> 5 6 /* 7 * ledtest <dev> <on|off> 8 */ 9 10 void print_usage(char *file) 11 { 12 printf("Usage:\n"); 13 printf("%s <dev> <on|off>\n",file); 14 printf("eg. \n"); 15 printf("%s /dev/leds on\n", file); 16 printf("%s /dev/leds off\n", file); 17 printf("%s /dev/led1 on\n", file); 18 printf("%s /dev/led1 off\n", file); 19 } 20 21 int main(int argc, char **argv) 22 { 23 int fd; 24 char* filename; 25 char val; 26 27 if (argc != 3) 28 { 29 print_usage(argv[0]); 30 return 0; 31 } 32 33 filename = argv[1]; 34 35 fd = open(filename, O_RDWR); 36 if (fd < 0) 37 { 38 printf("error, can't open %s\n", filename); 39 return 0; 40 } 41 42 if (!strcmp("on", argv[2])) 43 { 44 // 亮灯 45 val = 0; 46 write(fd, &val, 1); 47 } 48 else if (!strcmp("off", argv[2])) 49 { 50 // 灭灯 51 val = 1; 52 write(fd, &val, 1); 53 } 54 else 55 { 56 print_usage(argv[0]); 57 return 0; 58 } 59 60 61 return 0; 62 }
本驱动程序可实现对3个LED灯整体控制和单个控制。仔细分析该代码有利于理解linux对于字符驱动实现方式及架构的理解。
本文主要分析驱动代码中的两个关键函数s3c24xx_leds_init()和s3c24xx_leds_exit()。
1、注册字符设备
内核提供了三个函数来注册一组字符设备编号,这三个函数分别是 register_chrdev_region()、alloc_chrdev_region() 和 register_chrdev()。
(1)register_chrdev 比较老的内核注册的形式 早期的驱动
(2)register_chrdev_region/alloc_chrdev_region + cdev 新的驱动形式,需要配合下面两个函数使用:
cdev_init(&led_cdev, &s3c24xx_leds_fops);
cdev_add(&led_cdev, devid, LED_COUNT);
区别: register_chrdev函数是老版本里面的设备号注册函数,可以实现静态和动态注册两种方法,主要是通过给定的主设备号是否为0来进行区别,为0的时候为动态注册。register_chrdev_region以及alloc_chrdev_region就是将上述函数的静态和动态注册设备号进行了拆分的强化。
1.1 register_chrdev_region
静态注册方法,即已知需要注册的设备号,通过调用该函数进行注册。
函数原型:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
from:设备编号。
count:连续编号范围,是这组设备号的大小(也是次设备号的个数)。
name:编号相关联的设备名称. (/proc/devices); 本组设备的驱动名称。
对应卸载函数:
void unregister_chrdev_region(dev_t from, unsigned count)
1.2 alloc_chrdev_region
动态注册方法。
函数原型
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
dev:获得一个分配到的设备,可以用MAJOR宏和MINOR宏,将主设备号和次设备号提取出来。
baseminor:次设备号的基准,从第几个次设备号开始分配。
count:次设备号的个数。
name:驱动的名字。
说明:register_chrdev_region是在事先知道要使用的主、次设备号时使用的;要先查看cat /proc/devices去查看没有使用的。更简便、更智能的方法是让内核给我们自动分配一个主设备号,使用alloc_chrdev_region就可以自动分配了。自动分配的设备号,我们必须去知道他的主次设备号,否则后面没法去创建他对应的设备文件。
对应卸载函数:
void unregister_chrdev_region(dev_t from, unsigned count)
1.3 cdev_init初始化
执行注册后,需要实现cdev对象的初始化。以下是cdev结构的定义
struct cdev { struct kobject kobj; // 每个 cdev 都是一个 kobject struct module *owner; // 指向实现驱动的模块 const struct file_operations *ops; // 操纵这个字符设备文件的方法 struct list_head list; // 与 cdev 对应的字符设备文件的 inode->i_devices 的链表头 dev_t dev; // 起始设备编号 unsigned int count; // 设备范围号大小 };
本驱动代码使用的是静态内存定义的方式,使用以下两个函数。
1)、cdev_init 初始化函数
从以下代码中可见,该函数一方面对cdev的内部对象进行了初始化,另一方面,把fops与cdev进行了绑定。
/** * cdev_init() - initialize a cdev structure * @cdev: the structure to initialize * @fops: the file_operations for this device * * Initializes @cdev, remembering @fops, making it ready to add to the * system with cdev_add(). */ void cdev_init(struct cdev *cdev, const struct file_operations *fops) { memset(cdev, 0, sizeof *cdev); INIT_LIST_HEAD(&cdev->list); kobject_init(&cdev->kobj, &ktype_cdev_default); cdev->ops = fops; }
2)、cdev_add函数
该函数将p加入到当前系统中,并使其立即生效。如果失败,将返回一个负的错误码。
/** * cdev_add() - add a char device to the system * @p: the cdev structure for the device * @dev: the first device number for which this device is responsible * @count: the number of consecutive minor numbers corresponding to this * device * * cdev_add() adds the device represented by @p to the system, making it * live immediately. A negative error code is returned on failure. */ int cdev_add(struct cdev *p, dev_t dev, unsigned count) { int error; p->dev = dev; p->count = count; error = kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p); if (error) return error; kobject_get(p->kobj.parent); return 0; }
需要说明的是,无论是动态注册还是静态注册,都是需要通过设备号获取设备在设备表中的位置(一个dev_t类型变量),该变量在后续会用的到。
获取该设备号的方法如下:
MKDEV(MAJOR, MINOR);
MKDEV是一个宏,返回一个dev_t类型数据;
MAJOR:主设备号
MINOR:次设备号
2.创建设备节点
2.1 手动创建
在开发板执行insmod命令后,在执行"mknod /dev/led c 252 0",这样就创建出了主设备号252,次设备号0的/dev/led这个设备。
2.2自动创建
通过调用class_creat和device_create分别创建类和设备文件。
调用class_creat这个宏会在/sys/class目录下创建类目录,如下图所示,创建了myleds目录,并创建了4个连接。
/* This is a #define to keep the compiler from merging different * instances of the __key variable */ #define class_create(owner, name) \ ({ \ static struct lock_class_key __key; \ __class_create(owner, name, &__key); \ })
owner:一般使用 THIS_MODULE
name:本驱动使用“myleds”
__class_create函数实现可参见 \linux-4.12.9\drivers\base\class.c
调用device_create函数将在/dev目录下创建对应的设备节点。
效果如下:
有以下注释可知,该函数在sysfs中创建了一个device。注意,调用该函数前,必须先创建一个类。
/** * device_create - creates a device and registers it with sysfs * @class: pointer to the struct class that this device should be registered to * @parent: pointer to the parent struct device of this new device, if any * @devt: the dev_t for the char device to be added * @drvdata: the data to be added to the device for callbacks * @fmt: string for the device's name * * This function can be used by char device classes. A struct device * will be created in sysfs, registered to the specified class. * * A "dev" file will be created, showing the dev_t for the device, if * the dev_t is not 0,0. * If a pointer to a parent struct device is passed in, the newly created * struct device will be a child of that device in sysfs. * The pointer to the struct device will be returned from the call. * Any further sysfs files that might be required can be created using this * pointer. * * Returns &struct device pointer on success, or ERR_PTR() on error. * * Note: the struct class passed to this function must have previously * been created with a call to class_create(). */ struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...) { va_list vargs; struct device *dev; va_start(vargs, fmt); dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs); va_end(vargs); return dev; }
对应的卸载函数:
/** * device_destroy - removes a device that was created with device_create() * @class: pointer to the struct class that this device was registered with * @devt: the dev_t of the device that was previously registered * * This call unregisters and cleans up a device that was created with a * call to device_create(). */ void device_destroy(struct class *class, dev_t devt);
/** * class_destroy - destroys a struct class structure * @cls: pointer to the struct class that is to be destroyed * * Note, the pointer to be destroyed must have been created with a call * to class_create(). */ void class_destroy(struct class *cls)
3. ioremap
几乎每一种外设都是通过读写设备上的寄存器来进行的,通常包括控制寄存器、状态寄存器和数据寄存器三大类,外设的寄存器通常被连续地编址。根据CPU体系结构的不同,CPU对IO端口的编址方式有两种:
a -- I/O 映射方式(I/O-mapped)
典型地,如X86处理器为外设专门实现了一个单独的地址空间,称为"I/O地址空间"或者"I/O端口空间",CPU通过专门的I/O指令(如X86的IN和OUT指令)来访问这一空间中的地址单元。
b -- 内存映射方式(Memory-mapped)
RISC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,外设I/O端口成为内存的一部分。此时,CPU可以象访问一个内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。
一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。
Linux在io.h头文件中声明了函数ioremap,用来将I/O内存资源的物理地址映射到核心虚地址空间(3GB-4GB)中(这里是内核空间),原型如下:
static inline void __iomem *ioremap(phys_addr_t offset, size_t size)
phys_addr:要映射的起始的IO地址
size:要映射的空间的大小
对应卸载函数:
void iounmap(void * addr);
内核代码中关于这个函数挺复杂,看了下最终应该会调用到__iounmap这个函数,mmu这部分后面再看了。这里应该知道在驱动卸载时调用该函数。
总结:以上介绍了字符驱动安装和退出相关的函数,以及他们在linux系统的对应的行为。需要留意的一点是,无论init还是exit时对应的函数调用,都需要遵循一定的顺序。并且init和exit的顺序刚好是相反的。
字符驱动初始化:
1、注册字符驱动,cdev设备初始化。
2、创建对应的class。
3、创建设备device。
4、调用ioremap进行内存空间到物理空间地址的映射。
无论设备个数,类作为对设备的抽象,只有一个。但是表现在内核中/sys/class/classname的类对象实际上是由device_create函数创建的。在本例中就创建了4个led设备。