基于tiny6410的led驱动程序

今天弄了一个晚上了 终于弄懂了第一个简单的linux驱动感觉很多人编写的第一个驱动也是拿led开刀的吧。本文的led驱动是基于tiny6410的四个led驱动,用字符设备的驱动模块,而不是混杂设备驱动模块来编写,本人在使用混杂设备方法编写的时候led灯第二个始终无法实现控制开和关现在还在纠结中。

一下图片是开发板手册关于四个用户led的硬件介绍。


下面是s3c6410关于gpio的寄存器说明。





编写字符驱动程序的流程为:

1.要先驱动模块加载函数。在改函数中要实现将本驱动模块添加到系统中用到函数register_chrdev(),同时还要对gpio控制寄存器与状态寄存器进行初始化。

2.要编写file_operatins 结构体的实现函数比如read()、write()、open()、release()、ioctl()等函数的具体实现。

3模块卸载函数 dev_exit(),用到unregister_chrdev(),向系统注销本驱动模块。

可能跟书本说的有点不一样比如没用到 设备结构体  struct  xxx_dev_t   与向系统注册模块的函数不是 register_chrdev_region(xxx_dev_no,1,DEV_NAME) 和alloc_chrdev_region(&xxx_dev_no,1,DEV_NAME) 这两个函数是为获取字符设备号,而注册设备用到函数cdev_add(&xxx_dev.cdev,xxx_dev_no,1)。在注销驱动模块的卸载函数为unregister_chrdev_region(xxx_dev_no,1)用于释放占用的设备号,cdev_del(&xxx_dev.cdev)用于注销设备。以上的都没用到这些函数和结构体,请高手指教。

一下是具体实现代码。

led_driver.c

#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/fs.h>
#include<linux/init.h>
#include<asm/uaccess.h>
#include<linux/pci.h>
#include<linux/pci.h>
#include<mach/map.h>
#include<mach/regs-gpio.h>
#include<mach/gpio-bank-k.h>
#include<mach/hardware.h>
#include<linux/cdev.h>
#include<asm/atomic.h>


#define LED_MAJOR 243


static ssize_t led_read(struct file *filp,char __user *buf,size_t count,loff_t *f_pos)
{
return count;
}


static ssize_t led_write(struct file *filp,const char __user *buf,size_t count,loff_t *f_pos)
{
char nbuf[10];
unsigned tmp;

if(copy_from_user(nbuf,buf,count))
return -EINVAL;

switch(nbuf[0])
{
case 0: /* 四个led全亮 */
tmp=readl(S3C64XX_GPKDAT);
tmp&=0x0f;
writel(tmp, S3C64XX_GPKDAT);  
break;

case 1 :/* 四个led全灭 */
tmp  = readl(S3C64XX_GPKDAT);       
                        tmp |= (0xf0);       
            writel(tmp, S3C64XX_GPKDAT);  
break;

case 2:/* 第一个led亮 */
tmp=readl(S3C64XX_GPKDAT);
tmp&=0xef;
writel(tmp, S3C64XX_GPKDAT);  
break;

case 3:/* 第一个led灭 */
tmp = readl(S3C64XX_GPKDAT);       
            tmp |= (0x10);       
            writel(tmp, S3C64XX_GPKDAT);   
break;

case 4:/* 第二个led亮 */
tmp=readl(S3C64XX_GPKDAT);
tmp&=0xdf;
writel(tmp, S3C64XX_GPKDAT);  
break;

case 5:/* 第二个led灭 */
tmp = readl(S3C64XX_GPKDAT);       
            tmp |= (0x20);       
            writel(tmp, S3C64XX_GPKDAT);    
break;

case 6:/* 第三个led亮 */
tmp=readl(S3C64XX_GPKDAT);
tmp&=0xbf;
writel(tmp, S3C64XX_GPKDAT);  
break;

case 7:/* 第三个led灭 */
tmp = readl(S3C64XX_GPKDAT);       
            tmp |= (0x40);       
            writel(tmp, S3C64XX_GPKDAT);  
break;

case 8:/* 第四个led亮 */
tmp=readl(S3C64XX_GPKDAT);
tmp&=0x7f;
writel(tmp, S3C64XX_GPKDAT);  
break;

case 9:/* 第四个led灭 */
tmp = readl(S3C64XX_GPKDAT);       
            tmp |= (0x80);       
            writel(tmp, S3C64XX_GPKDAT); 
break;

default :    
                break;
}
return 0;
}


struct file_operations led_fops = {
.owner = THIS_MODULE,
.read  = led_read,
.write = led_write,
};


static int __init dev_init(void)
{
int ret;
unsigned tmp;
   
printk ("Test led dev\n");    
ret = register_chrdev(LED_MAJOR,"led",&led_fops);      
if (ret <0){    
printk ("register %s char dev error\n","led");    
return -1;    
}  

/* 设置为GPK4-GPK7为输出方式*/
tmp = readl(S3C64XX_GPKCON);      
        tmp = (tmp&0x0000ffff)|0x1111ffff; 
        writel(tmp, S3C64XX_GPKCON);
        
        /* GPKDAT[7:4]=1 灯灭 */
tmp=readl(S3C64XX_GPKDAT);
tmp |=(0xF<<4);
writel(tmp,S3C64XX_GPKDAT);

/* 禁止上拉下拉   */
tmp=readl(S3C64XX_GPKPUD);
tmp &=(0x00<<8);
writel(tmp,S3C64XX_GPKPUD);

printk ("module install ok!\n");    

return 0;
}




static void __exit dev_exit (void)    
{    
    unregister_chrdev(LED_MAJOR,"led");    
    printk ("module exit\n");    
    return ;    
}    
    
module_init(dev_init);    
module_exit(dev_exit);


MODULE_LICENSE("GPL");
MODULE_AUTHOR("GAN WEIWANG");


Makefile函数

ifneq ($(KERNELRELEASE),)
obj-m := led_driver.o 
else
KDIR :=/home/part4/linux-2.6.38
PWD  :=$(shell pwd)
default:
$(MAKE) -C $(KDIR) M=$(PWD) modules


clean:
rm -f *.ko *.o *.mod.o *mod.c *.symvers
endif


测试程序

#include <stdio.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
#include <stdlib.h>  
   
int main (int argc,char **argv)    
{     
    int  fd;    

    int buf;
    if(argc!=2||sscanf(argv[1],"%d",&buf)!=1||buf<0||buf>9)
    {
    fprintf(stderr,"Usage:leds 0~9\n");
exit(1);
    }
    fd = open("/dev/led",O_RDWR);    
    if (fd < 0)    
    {    
        printf ("Open /dev/led file error\n");    
        return -1;    
    }       
 
    write(fd,&buf,1);   
    close (fd);    
    return 0;    
    
}

上述编译没有问题,就可以下到板子测试了。


加载驱动          insmod  led_driver.ko

创建设备文件    mknod /dev/led c 243 0  其中243要跟驱动文件中的设备号一致

运行测试文件    ./test_led

完成。


参考资料:


#include <linux/module.h>/*它定义了模块的 API、类型和宏(MODULE_LICENSE、MODULE_AUTHOR等等),所有的内核模块都必须包含这个头文件。/
#include <linux/kernel.h>/*使用内核信息优先级时要包含这个文件,一般在使用printk函数时使用到优先级信息*/
#include <linux/init.h>//头文件:module_init、module_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;
}
}
/* 这个结构是字符设备驱动程序的核心
* 当应用程序操作设备文件时所调用的open、read、write等函数,
* 最终会调用这个结构中指定的对应函数
*/
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_GPB5,0)中,该函数首先通过S3C2410_GPB5获得GPB的虚拟地址和偏移地址,再对GPB5的GPBDAT寄存器进行操作,具体
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);//读取GPIO的DAT数据到dat
dat &= ~(1 << offs); //先将要设置的IO口拉低
dat |= to << offs; //再将形参的to值赋给dat
__raw_writel(dat, base + 0x04);//最后将DAT值写进GPIO的DAT
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表示2440的GPIO的偏移地址,也就是说其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。有了*base和off,就可以操作具体的寄存器了。
函数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。

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

#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 cache和Write 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_MAJOR为0(自动分配主设备号 ),使用insmod命令加载模块时会在终端显示分配的主设备号和次设备号,在/dev目录下建立该节点,比如设备leds,如果加载该模块时分配的主设备号和次设备号为253和0,则建立节点:mknod leds c 253 0。使用register_chrdev (LED_MAJOR,DEVICE_NAME,&dev_fops)注册字符设备驱动程序时都要手动建立节点 ,否则在应用程序无法打开该设备。

__raw_readl和__raw_writel

Linux对I/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的做法有时候是有用的,例如编译器打开了检查未使用的参数的时候需要将没有用到的参数这么弄一下才能编译通过。

CPU对I/O的物理地址的编程方式有两种:一种是I/O映射,一种是内存映射。__raw_readl和__raw_writel等是原始的操作I/O的方法,由此派生出来的操作方法有:inb、outb、_memcpy_fromio、readb、writeb、ioread8、iowrite8等。



  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值