led驱动

来源:互联网 发布:人工智能强国 编辑:程序博客网 时间:2024/04/28 18:11
#include <linux/module.h>/*它定义了模块的 API、类型和宏(MODULE_LICENSEMODULE_AUTHOR等等),所有的内核模块都必须包含这个头文件/
#include <linux/kernel.h>/*使用内核信息优先级时要包含这个文件,一般在使用printk函数时使用到优先级信息*/
#include <linux/init.h>//头文件:module_initmodule_exit等宏定义。
#include <linux/fs.h>////struct file_operations
#include <asm/irq.h>
#include <mach/regs-gpio.h>// S3C2410 GPIO寄存器定义
#include <mach/hardware.h>// s3c2410_gpio_setpin, s3c2410_gpio_cfgpin
#include <linux/device.h>//class_create device_create(注意,有些2.6.27以前是的可能是class_device_create,如果出现implicate 错误时,看一下这个头问题里边是哪一个)udev,自动在/dev下创建设备节点
#include <linux/cdev.h>//字符设备节点注册,函数有cdev_init,cdev_add,cdev_del等早期的办法是register_chrdev,unregister_chrdev这种方法应避免使用。
#define DEVICE_NAME "leds"       /* 加载模式后,执行”cat /proc/devices”命令看到的设备名称 */
#define LED_MAJOR 231                       /* 主设备号 */
/* 应用程序执行ioctl(fd, cmd, arg)时的第2个参数 */
#define IOCTL_LED_ON      1
#define IOCTL_LED_OFF     0
/* 用来指定LED所用的GPIO引脚 */
static unsigned long led_table [] =
{
         S3C2410_GPB5,
         S3C2410_GPB6,
         S3C2410_GPB7,
         S3C2410_GPB8,
};
/* 用来指定GPIO引脚的功能:输出 */
static unsigned int led_cfg_table [] =
{
         S3C2410_GPB5_OUTP,
         S3C2410_GPB6_OUTP,
         S3C2410_GPB7_OUTP,
         S3C2410_GPB8_OUTP,
};
struct leds_type 
{
 struct cdev cdev;
};
struct leds_type *my_leds_dev;
/* 应用程序对设备文件/dev/EmbedSky-leds执行open(...)时,
 * 就会调用EmbedSky_leds_open函数
 */
static int EmbedSky_leds_open(struct inode *inode, struct file *file)
{
         int i;
         for (i = 0; i < 4; i++)
         {
              // 设置GPIO引脚的功能:本驱动中LED所涉及的GPIO引脚设为输出功能
                 s3c2410_gpio_cfgpin(led_table[i], led_cfg_table[i]);
         }
         return 0;
}
/* 应用程序对设备文件/dev/EmbedSky-leds执行ioclt(...)时,
 * 就会调用EmbedSky_leds_ioctl函数
 */
static int EmbedSky_leds_ioctl( struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
         if (arg > 4)
         {
                 return -EINVAL;
         }
         switch(cmd)
         {
                 case IOCTL_LED_ON:
                          // 设置指定引脚的输出电平为0
                          s3c2410_gpio_setpin(led_table[arg], 0);
                          return 0;
                 case IOCTL_LED_OFF:
                          // 设置指定引脚的输出电平为1
                          s3c2410_gpio_setpin(led_table[arg], 1);
                          return 0;
                 default:
                          return -EINVAL;
         }
}
/* 这个结构是字符设备驱动程序的核心
 * 当应用程序操作设备文件时所调用的openreadwrite等函数,
 * 最终会调用这个结构中指定的对应函数
 */
static struct file_operations EmbedSky_leds_fops =
{
         .owner  =  THIS_MODULE,  /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
         .open   =   EmbedSky_leds_open,     
         .ioctl  =   EmbedSky_leds_ioctl,
};
static char __initdata banner[] = "TQ2440/SKY2440 LEDS, (c) 2008,2009 www.embedsky.net/n";
static struct class *led_class;
/*
 * 执行“insmod EmbedSky_leds.ko”命令时就会调用这个函数
 */
static int __init EmbedSky_leds_init(void)
{
         int ret;
        dev_t devno=MKDEV(LED_MAJOR,0);
        printk("init led/n");
         printk(banner);
/* 注册字符设备驱动程序
* 参数为主设备号、设备名字、file_operations结构;
* 这样,主设备号就和具体的file_operations结构联系起来了,
* 操作主设备为LED_MAJOR的设备文件时,就会调用EmbedSky_leds_fops中的相关成员函数
         ret = register_chrdev_region(devno, 1,DEVICE_NAME);//获得设备编号
         my_leds_dev=kmalloc(sizeof(struct leds_type),GFP_KERNEL);
/*这个必须有不然会在加载模块时出现Unable to handle kernel NULL pointer dereference at virtual addres 00000000 错误,这是由于在这里my_leds_dev仅仅是个指针,没有相应大小的分配内存,所以使用时会出错,,,寻找这个错误是比较麻烦的*/
        if(!my_leds_dev)
        {
           ret=-ENOMEM;
          goto fail_malloc;
        }
       memset(my_leds_dev,0,sizeof(struct leds_type));
       cdev_init(&(my_leds_dev->cdev),&EmbedSky_leds_fops);
       ret=cdev_add(&(my_leds_dev->cdev),devno,1);
    /*注意:与早期的设备注册方法不同,早期的直接register_chrdev()就可以,*/
       if(ret)printk(KERN_NOTICE"ERROR %d",ret);    
         //注册一个类,使mdev可以在"/dev/"目录下面建立设备节点
         led_class = class_create(THIS_MODULE, DEVICE_NAME);
         if(IS_ERR(led_class))
         {
                 printk("Err: failed in EmbedSky-leds class. /n");
                 return -1;
         }
         //创建一个设备节点,节点名为DEVICE_NAME
         device_create(led_class, NULL, MKDEV(LED_MAJOR, 0), NULL, DEVICE_NAME);
         printk(DEVICE_NAME " initialized/n");
         return 0;
        fail_malloc: unregister_chrdev_region(devno,1);
        return ret;
}
/*
 * 执行”rmmod EmbedSky_leds.ko”命令时就会调用这个函数 
 */
static void __exit EmbedSky_leds_exit(void)
{
         /* 卸载驱动程序 */
         unregister_chrdev(LED_MAJOR, DEVICE_NAME);
         device_destroy(led_class, MKDEV(LED_MAJOR, 0));     //删掉设备节点
         class_destroy(led_class);                                    //注销类
}
/* 这两行指定驱动程序的初始化函数和卸载函数 */
module_init(EmbedSky_leds_init);
module_exit(EmbedSky_leds_exit);
/* 描述驱动程序的一些信息,不是必须的 */
MODULE_AUTHOR("http://www.embedsky.net");           // 驱动程序的作者
MODULE_DESCRIPTION("TQ2440/SKY2440 LED Driver");    // 一些描述信息
MODULE_LICENSE("GPL");                                       // 遵循的协议
上面代码中,led_table数组相当于对应了GPB的四个IO口的索引,通过这四个值,对这四个IO口进行相关操作。例如:
        S3C2410_GPB5 S3C2410_GPIONO(S3C2410_GPIO_BANKB, 5)
                      S3C2410_GPIO_BANKB  5
                      32*1 + 5
s3c2410_gpio_setpin(S3C2410_GPB50)中,该函数首先通过S3C2410_GPB5获得GPB虚拟地址和偏移地址,再对GPB5GPBDAT寄存器进行操作,具体
void s3c2410_gpio_setpin(unsigned int pin, unsigned int to)
{
    void __iomem *base = S3C2410_GPIO_BASE(pin);
    unsigned long offs = S3C2410_GPIO_OFFSET(pin);
    unsigned long flags;
unsigned long dat;
local_irq_save(flags);
    dat = __raw_readl(base + 0x04);//读取GPIODAT数据到dat
    dat &= ~(1 << offs);           //先将要设置的IO口拉低
dat |= to << offs;             //再将形参的to值赋给dat
    __raw_writel(dat, base + 0x04);//最后将DAT值写进GPIODAT
    local_irq_restore(flags);
}
 

上面的 函数调用了两个子函数,具体定义如下

#define S3C2410_GPIO_BASE(pin)   ((((pin) & ~31) >> 1) + S3C24XX_VA_GPIO)
#define S3C2410_GPIO_OFFSET(pin) ((pin) & 31)

其中S3C24XX_VA_GPIO定义如下:

#define S3C24XX_VA_GPIO    S3C2410_ADDR(0x00E00000)
#define S3C2410_ADDR(x)   (0xF0000000 + (x))

这里S3C2410_ADDR的基地址为0xF0000000(??),也即2440所有寄存器的虚拟地址的基地址。0x00E00000表示2440GPIO的偏移地址,也就是说其GPIO的虚拟地址首地址为0xF0E00000

再看看S3C2410_GPIO_BASE(pin)的定义,我们不仿把S3C2410_GPB5的值放进去计算,可以得到(S3C2410_GPB5&~31)=32。其目的就是去掉GPB的偏移值,然后再右移一位,和GPIO的虚拟地址首地址相加。因此,S3C2410_GPIO_BASE(pin)只代表了对应GPIO组的虚拟地址,如GPB的虚拟地址为10000(B)+0xF0E00000=0xF0E00010。依此类推,可以得到所有GPIO的偏移地址,具体如下表:

 

BANK

pin&~31

pin&~31>>1

S3C2410_GPIO_BASE(pin)

GPA

32*0

0000,0000

0x00

0xF0E00000

GPB

32*1

0010,0000

0x10

0xF0E00010

GPC

32*2

0100,0000

0x20

0xF0E00020

GPD

32*3

0110,0000

0x30

0xF0E00030

GPE

32*4

1000,0000

0x40

0xF0E00040

GPF

32*5

1010,0000

0x50

0xF0E00050

GPG

32*6

1100,0000

0x60

0xF0E00060

GPH

32*7

1110,0000

0x70

0xF0E00070

S3C2410_GPIO_OFFSET用于获得具体GPIO的偏移地址。如GPB5,则S3C2410_GPIO_OFFSET(pin)  (pin)&31 = (32*1 + 5) & 31 = 5。有了*baseoff,就可以操作具体的寄存器了。
函数s3c2410_gpio_cfgpin()用于配置GPCON寄存器。具体代码
void s3c2410_gpio_cfgpin(unsigned int pin, unsigned int function)
{
    void __iomem *base = S3C2410_GPIO_BASE(pin);
    unsigned long mask;
    unsigned long con;
    unsigned long flags;
     if (pin < S3C2410_GPIO_BANKB) 
    {
        mask = 1 << S3C2410_GPIO_OFFSET(pin);//GPA的寄存器只占一位
    }
  else 
    {
        mask = 3 << S3C2410_GPIO_OFFSET(pin)*2;//GPA的寄存器占两位
    }
local_irq_save(flags);
    con  = __raw_readl(base + 0x00);//先保留GPCON的值
    con &= ~mask;                   //再将要设置的管脚的CON值清零
con |= function;                //然后将形参传进来的配置赋给CON
    __raw_writel(con, base + 0x00); //最后将CON值写进GPCON寄存器
     local_irq_restore(flags);
}

上面的LED驱动程序中,led_cfg_table数组给出了GPB相应管脚的属性设置,调用上面的函数后即设置为Output

到此为止,整个S3C2440IO口操作,应该就一目了然了

#define __raw_writel(v,a) (__chk_io_ptr(a), *(volatile unsigned int __force *)(a) = (v))
# define __chk_io_ptr(x) (void)0

__chk_io_ptr()是编译器为了更细致地检查参数的属性,用于调试,正常编译时没有作用。
volatile
为了防止Compiler优化。

内核中,对所有的地址都是通过虚拟地址进行访问的.因此,不能直接访问0x56000010的物理地址,如果要对0x56000010的物理地址进行访问(一般是外设寄存器),那么需要把0x56000010的物理地址映射为虚拟地址,然后对该虚拟地址进行访问就是对实际的物理地址进行访问了。
需要注意的是:在进行映射时,该虚拟地址需要disable cacheWrite Buffer, 否则就是加了volatile也是没有用的

这个IOREMAP的实现过程中
/*
* figure out the physical address offset in a page size
* PAGE_MASK = (1 << 10)
*/
offset = phys_addr &~ PAGE_MASK;
/*
* figure out physical address with page align
*/
phys_addrs &= PAGE_MASK;
/*
* get the real size with page align
*/
size = PAGE_ALIGN(last_addr) - phys_addrs;
下面是通过vmlist中查找以size大小的空闲块,所以从这里可以看出,已经做过了页的对齐,只以映射的大小

杂项设备(misc device

杂项设备也是在嵌入式系统中用得比较多的一种设备驱动。在 Linux 内核的include/linux目录下有Miscdevice.h文件,要把自己定义的misc device从设备定义在这里。其实是因为这些字符设备不符合预先确定的字符设备范畴,所有这些设备采用主编号10 ,一起归于misc device,其实misc_register就是用主标号10调用register_chrdev()的。

也就是说,misc设备其实也就是特殊的字符设备。

字符设备(char device)

使用register_chrdev(LED_MAJOR,DEVICE_NAME,&dev_fops)注册字符设备驱动程序时,如果有多个设备使用该函数注册驱动程序,LED_MAJOR不能相同,否则几个设备都无法注册(我已验证)。如果模块使用该方式注册并且 LED_MAJOR0(自动分配主设备号 ),使用insmod命令加载模块时会在终端显示分配的主设备号和次设备号,在/dev目录下建立该节点,比如设备leds,如果加载该模块时分配的主设备号和次设备号为2530,则建立节点:mknod leds c 253 0。使用register_chrdev (LED_MAJOR,DEVICE_NAME,&dev_fops)注册字符设备驱动程序时都要手动建立节点 ,否则在应用程序无法打开该设备。

__raw_readl__raw_writel

LinuxI/O的操作都定义在asm/io.h中,相应的在arm平台下,就在asm-arm/io.h中。

#define __raw_writeb(v,a)   (__chk_io_ptr(a), *(volatile unsigned char __force  *)(a) = (v))

#define __raw_writew(v,a)   (__chk_io_ptr(a), *(volatile unsigned short __force *)(a) = (v))

#define __raw_writel(v,a)   (__chk_io_ptr(a), *(volatile unsigned int __force   *)(a) = (v))include/linux/compiler.h中:

#ifdef __CHECKER__……
extern void __chk_io_ptr(void __iomem *);
#else……
# define __chk_io_ptr(x) (void)0……
#endif

__raw_readl(a)展开是:((void)0, *(volatile unsigned int _force *)(a))。在定义了__CHECKER__的时候先调用__chk_io_ptr检查该地址,否则__chk_io_ptr什么也不做,*(volatile unsigned int _force *)(a)就是返回地址为a处的值。(void)xx的做法有时候是有用的,例如编译器打开了检查未使用的参数的时候需要将没有用到的参数这么弄一下才能编译通过。

CPUI/O的物理地址的编程方式有两种:一种是I/O映射,一种是内存映射。__raw_readl__raw_writel等是原始的操作I/O的方法,由此派生出来的操作方法有:inboutb_memcpy_fromioreadbwritebioread8iowrite8等。

原创粉丝点击