字符设备驱动基础篇5——驱动如何操控硬件(动静态映射操作LED)

来源:互联网 发布:好用的男士爽肤水 知乎 编辑:程序博客网 时间:2024/06/17 06:27

以下内容源于朱有鹏《物联网大讲堂》课程的学习,以及博客http://www.cnblogs.com/biaohc/p/6575074.html,如有侵权,请告知删除。


一、驱动如何操控硬件

1、操作系统下操作硬件,和裸机操作硬件的相同和不同点

(1)硬件不变

  • 硬件物理原理不变;
  • 硬件操作接口(寄存器)不变;
  • 硬件操作代码不变

(2)改变点

  • 寄存器地址不同。
  • 原来是直接用物理地址,现在需要用该物理地址在内核虚拟地址空间相对应的虚拟地址。寄存器的物理地址是CPU设计时决定的,从datasheet中查找到的。
  • 编程方法不同。
  • 裸机中习惯直接用函数指针操作寄存器地址,而kernel中习惯用封装好的io读写函数来操作寄存器,以实现最大程度可移植性。

2、内核的虚拟地址映射方法

(1)为什么需要虚拟地址映射?

(2)内核中有两套虚拟地址映射方法:动态和静态

静态映射方法的特点

  • 内核移植时以代码的形式硬编码,如果要更改必须改源代码后重新编译内核;
  • 在内核启动时建立静态映射表,到内核关机时销毁,中间一直有效;
  • 对于移植好的内核,你用不用它都在那里;

动态映射方法的特点:

  • 驱动程序根据需要随时动态的建立映射、使用、销毁映射;
  • 映射是短期临时的

(3)如何选择虚拟地址映射方法

  • 2种映射并不排他,可以同时使用;
  • 静态映射类似于C语言中全局变量,动态方式类似于C语言中malloc堆内存;
  • 静态映射的好处是执行效率高,坏处是始终占用虚拟地址空间;
  • 动态映射的好处是按需使用虚拟地址空间,坏处是每次使用前后都需要代码去建立映射、销毁映射。


二、静态映射操作LED理论

1、关于静态映射

  • 不同版本内核中静态映射表所在的目录位置、文件名可能不同;
  • 不同SoC的静态映射表所在的目录位置、文件名可能不同;
  • 所谓映射表其实就是头文件中的宏定义。

2、三星版本内核中的静态映射表

(1)主映射表位于:arch/arm/plat-s5p/include/plat/map-s5p.h


  • CPU在安排寄存器地址时不是随意乱序分布的,而是按照模块去区分的。
  • 每一个模块内部的很多个寄存器的地址是连续的。所以内核在定义寄存器地址时都是先找到某个模块的基地址,然后再用基地址+偏移量来寻找具体的一个寄存器。
  • map-s5p.h中定义的就是要用到的几个模块(GPIO、UART等)的寄存器基地址。
  • map-s5p.h中定义的是模块的寄存器基地址的虚拟地址。里面定义的都是虚拟地址。

(2)虚拟地址基地址定义在:arch/arm/plat-samsung/include/plat/map-base.h


  • #define S3C_ADDR_BASE   (0xFD000000)  // 三星移植时确定的静态映射表的基地址,表中的所有虚拟地址都是以这个地址+偏移量来指定的

(3)GPIO相关的主映射表位于:arch/arm/mach-s5pv210/include/mach/regs-gpio.h

  • 表中是GPIO的各个端口的虚拟基地址的定义

(4)GPIO的具体寄存器定义位于:arch/arm/mach-s5pv210/include/mach/gpio-bank.h


三、静态映射操作LED实践

1、参考裸机中的操作方法添加LED操作代码

(1)虚拟地址的宏定义,注意这个地址是虚拟地址,而且是这个开发板对应的移植后的内核。

(2)在init和exit函数中分别点亮和熄灭LED;

2、实践测试

(1)insmod和rmmod时观察LED亮灭变化;

(2)打印出寄存器的值和静态映射表中的分析相对比;

3、将点亮熄灭代码移动到open和close函数中去

4、添加驱动中的写函数

(1)先定义好应用和驱动之间的控制接口,这个是由自己来定义的。譬如定义为:应用向驱动写"on"则驱动让LED亮,应用向驱动写"off",驱动就让LED灭。

(2)应用和驱动的接口定义做的尽量简单,把业务逻辑放在应用层,而底层只是进行操作。譬如用1个字目来表示。譬如定义为:应用写"1"表示灯亮,写"0"表示让灯灭。

if (kbuf[0] == '1'){rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));}else if (kbuf[0] == '0'){rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));}/*// 真正的驱动中,数据从应用层复制到驱动中后,我们就要根据这个数据// 去写硬件完成硬件的操作。所以这下面就应该是操作硬件的代码if (!strcmp(kbuf, "on")){rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));}else if (!strcmp(kbuf, "off")){rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));}*/

5、写应用来测试写函数

6、驱动和应用中来添加读功能

#include <linux/module.h>// module_init  module_exit#include <linux/init.h>// __init   __exit#include <linux/fs.h>#include <asm/uaccess.h>#include <mach/regs-gpio.h>#include <mach/gpio-bank.h>// arch/arm/mach-s5pv210/include/mach/gpio-bank.h#include <linux/string.h>#define MYMAJOR200#define MYNAME"testchar"#define GPJ0CONS5PV210_GPJ0CON//这里已经是虚拟地址了,在内核中已经定义好S5PV210_GPJ0CON,但要包含头文件                                   //所谓的静态映射就体现在这里。   //这个是在内核中写好定死的,如果要修改,就要把内核代码相关部分修改,然后编译#define GPJ0DATS5PV210_GPJ0DAT#define rGPJ0CON*((volatile unsigned int *)GPJ0CON)#define rGPJ0DAT*((volatile unsigned int *)GPJ0DAT)int mymajor;char kbuf[100];// 内核空间的bufstatic int test_chrdev_open(struct inode *inode, struct file *file){// 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分printk(KERN_INFO "test_chrdev_open\n");rGPJ0CON = 0x11111111;rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));// 亮return 0;}static int test_chrdev_release(struct inode *inode, struct file *file){printk(KERN_INFO "test_chrdev_release\n");rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));return 0;}ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos){int ret = -1;printk(KERN_INFO "test_chrdev_read\n");ret = copy_to_user(ubuf, kbuf, count);if (ret){printk(KERN_ERR "copy_to_user fail\n");return -EINVAL;}printk(KERN_INFO "copy_to_user success..\n");return 0;}// 写函数的本质就是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件完成操作。static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf,size_t count, loff_t *ppos){int ret = -1;printk(KERN_INFO "test_chrdev_write\n");// 使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中//memcpy(kbuf, ubuf);// 不行,因为2个不在一个地址空间中memset(kbuf, 0, sizeof(kbuf));ret = copy_from_user(kbuf, ubuf, count);if (ret){printk(KERN_ERR "copy_from_user fail\n");return -EINVAL;}printk(KERN_INFO "copy_from_user success..\n");if (kbuf[0] == '1'){rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));}else if (kbuf[0] == '0'){rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));}return 0;}// 自定义一个file_operations结构体变量,并且去填充static const struct file_operations test_fops = {.owner= THIS_MODULE,// 惯例,直接写即可.open= test_chrdev_open,// 将来应用open打开这个设备时实际调用的.release= test_chrdev_release,// 就是这个.open对应的函数.write = test_chrdev_write,.read= test_chrdev_read,};// 模块安装函数static int __init chrdev_init(void){printk(KERN_INFO "chrdev_init helloworld init\n");// 在module_init宏调用的函数中去注册字符设备驱动// major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号// 内核如果成功分配就会返回分配的主设备好;如果分配失败会返回负数mymajor = register_chrdev(0, MYNAME, &test_fops);if (mymajor < 0){printk(KERN_ERR "register_chrdev fail\n");return -EINVAL;}printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);return 0;}// 模块卸载载函数static void __exit chrdev_exit(void){printk(KERN_INFO "chrdev_exit helloworld exit\n");// 在module_exit宏调用的函数中去注销字符设备驱动unregister_chrdev(mymajor, MYNAME);//rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));}module_init(chrdev_init);module_exit(chrdev_exit);// MODULE_xxx这种宏作用是用来添加模块描述信息MODULE_LICENSE("GPL");// 描述模块的许可证MODULE_AUTHOR("aston");// 描述模块的作者MODULE_DESCRIPTION("module test");// 描述模块的介绍信息MODULE_ALIAS("alias xxx");// 描述模块的别名信息


三、动态映射操作LED

1、如何建立动态映射

(1)request_mem_region,向内核申请(报告)需要映射的内存资源。见代码。

(2)ioremap,真正用来实现映射,传给它一个物理地址,它映射返回一个虚拟地址。

2、如何销毁动态映射?(一定要销毁)

(1)iounmap

(2)release_mem_region

(3)映射建立时,是要先申请再映射;然后使用;使用完要解除映射时要先解除映射再释放申请。

3、代码实践

(1)2个寄存器分开独立映射;

(2)2个寄存器在一起映射;(可以只映射其中的一个寄存器(一般是作为基地址的),然后用指针的算术操作来操作其他寄存器)

#include <linux/module.h>// module_init  module_exit#include <linux/init.h>// __init   __exit#include <linux/fs.h>#include <asm/uaccess.h>#include <mach/regs-gpio.h>#include <mach/gpio-bank.h>// arch/arm/mach-s5pv210/include/mach/gpio-bank.h#include <linux/string.h>#include <linux/io.h>#include <linux/ioport.h>#define MYNAME"testchar"//这两个宏在静态映射里才有意义,这里可以删除#define GPJ0CONS5PV210_GPJ0CON#define GPJ0DATS5PV210_GPJ0DAT#define rGPJ0CON*((volatile unsigned int *)GPJ0CON)#define rGPJ0DAT*((volatile unsigned int *)GPJ0DAT)#define GPJ0CON_PA0xe0200240#define GPJ0DAT_PA 0xe0200244unsigned int *pGPJ0CON;unsigned int *pGPJ0DAT;int mymajor;char kbuf[100];// 内核空间的bufstatic int test_chrdev_open(struct inode *inode, struct file *file){// 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分printk(KERN_INFO "test_chrdev_open\n");rGPJ0CON = 0x11111111;rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));// 亮return 0;}static int test_chrdev_release(struct inode *inode, struct file *file){printk(KERN_INFO "test_chrdev_release\n");rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));return 0;}ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos){int ret = -1;printk(KERN_INFO "test_chrdev_read\n");ret = copy_to_user(ubuf, kbuf, count);if (ret){printk(KERN_ERR "copy_to_user fail\n");return -EINVAL;}printk(KERN_INFO "copy_to_user success..\n");return 0;}// 写函数的本质就是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件完成操作。static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf,size_t count, loff_t *ppos){int ret = -1;printk(KERN_INFO "test_chrdev_write\n");// 使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中memset(kbuf, 0, sizeof(kbuf));ret = copy_from_user(kbuf, ubuf, count);if (ret){printk(KERN_ERR "copy_from_user fail\n");return -EINVAL;}printk(KERN_INFO "copy_from_user success..\n");if (kbuf[0] == '1'){rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));}else if (kbuf[0] == '0'){rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));}return 0;}// 自定义一个file_operations结构体变量,并且去填充static const struct file_operations test_fops = {.owner= THIS_MODULE,// 惯例,直接写即可.open= test_chrdev_open,// 将来应用open打开这个设备时实际调用的.release= test_chrdev_release,// 就是这个.open对应的函数.write = test_chrdev_write,.read= test_chrdev_read,};// 模块安装函数static int __init chrdev_init(void){printk(KERN_INFO "chrdev_init helloworld init\n");// 在module_init宏调用的函数中去注册字符设备驱动// major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号// 内核如果成功分配就会返回分配的主设备好;如果分配失败会返回负数mymajor = register_chrdev(0, MYNAME, &test_fops);if (mymajor < 0){printk(KERN_ERR "register_chrdev fail\n");return -EINVAL;}printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);// 使用动态映射的方式来操作寄存器if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))    //参数含义:起始地址(或者说寄存器的原始地址),寄存器的长度(一般32bit,因此4字节),这段空间的名字return -EINVAL;if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON"))return -EINVAL;pGPJ0CON = ioremap(GPJ0CON_PA, 4);//参数含义和上一样,返回一个指针pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);*pGPJ0CON = 0x11111111;*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));// 亮return 0;}// 模块卸载函数static void __exit chrdev_exit(void){printk(KERN_INFO "chrdev_exit helloworld exit\n");//想要熄灯,必要要在解除映射的前面*pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));// 解除映射iounmap(pGPJ0CON);iounmap(pGPJ0DAT);release_mem_region(GPJ0CON_PA, 4);release_mem_region(GPJ0DAT_PA, 4);// 在module_exit宏调用的函数中去注销字符设备驱动unregister_chrdev(mymajor, MYNAME);}module_init(chrdev_init);module_exit(chrdev_exit);// MODULE_xxx这种宏作用是用来添加模块描述信息MODULE_LICENSE("GPL");// 描述模块的许可证MODULE_AUTHOR("aston");// 描述模块的作者MODULE_DESCRIPTION("module test");// 描述模块的介绍信息MODULE_ALIAS("alias xxx");// 描述模块的别名信息


阅读全文
0 0