混杂设备存在自己的结构体:
struct device;
struct miscdevice {
int minor;
const char *name;
const struct file_operations *fops;
struct list_head list;
struct device *parent;
struct device *this_device;
};
其中struct device*可以联想到自动加载设备文件中的class_create()和device_create()两个函数。因此乐意推测混杂字符设备是自动加载设备文件的设备驱动。
其中主要的两个函数分别是misc_register()和misc_deregister(),分别用来添加和去除混杂设备。这两个函数分别在初始化函数和卸载函数中调用。
static struct class *misc_class;
static const struct file_operations misc_fops = {
.owner= THIS_MODULE,
.open= misc_open,
};
int misc_register(struct miscdevice * misc)
{
struct miscdevice *c;
dev_t dev;
int err = 0;
INIT_LIST_HEAD(&misc->list);
mutex_lock(&misc_mtx);
list_for_each_entry(c, &misc_list, list) {
if (c->minor == misc->minor) {
mutex_unlock(&misc_mtx);
return -EBUSY;
}
}
if (misc->minor == MISC_DYNAMIC_MINOR) {
int i = DYNAMIC_MINORS;
while (--i >= 0)
if ( (misc_minors[i>>3] & (1 << (i&7))) == 0)
break;
if (i<0) {
mutex_unlock(&misc_mtx);
return -EBUSY;
}
misc->minor = i;
}
if (misc->minor < DYNAMIC_MINORS)
misc_minors[misc->minor >> 3] |= 1 << (misc->minor & 7);
dev = MKDEV(MISC_MAJOR, misc->minor);
/*创建设备*/ misc->this_device = device_create(misc_class, misc->parent, dev, NULL,
"%s", misc->name);
if (IS_ERR(misc->this_device)) {
err = PTR_ERR(misc->this_device);
goto out;
}
list_add(&misc->list, &misc_list);
out:
mutex_unlock(&misc_mtx);
return err;
}
int misc_deregister(struct miscdevice *misc)
{
int i = misc->minor;
if (list_empty(&misc->list))
return -EINVAL;
mutex_lock(&misc_mtx);
list_del(&misc->list);
/*释放设备*/
device_destroy(misc_class, MKDEV(MISC_MAJOR, misc->minor));
if (i < DYNAMIC_MINORS && i>0) {
misc_minors[i>>3] &= ~(1 << (misc->minor & 7));
}
mutex_unlock(&misc_mtx);
return 0;
}
static int __init misc_init(void)
{
int err;
...
/*创建一个设备类*/
misc_class = class_create(THIS_MODULE, "misc");
...
return err;
}
从源码中可以知道混杂字符设备就是自动创建设备文件的设备驱动。
LED 的字符设备驱动,由于没有读写操作,只需要完成最控制操作,也就是ioctl函数的实现。由于open函数默认情况下就是打开,所以不去实现也是可以的。 ioctl函数的实现主要包含两个步骤,分别是定义命令和实现命令。其中的定义命令包含类型、方向、数据大小、以及命令序号,这些都可以按着一定宏定义实现。实现控制也就是ioctl函数的定义,其中包含,三部分:(1)、命令的检查,类型和序号;(2)、指针参数的可读可写检查;(3)具体命令的实现 (switch-case)。
LED的实现主要就是控制全亮、全灭,某一个亮,某一个灭。我的开发板是TQ2440,利用了GPB5-GPB8来控制4个LED,只要当端口为低电平时,LED亮,高电平时,LED灭。
具体的实现如下:
#include<linux/module.h>
#include<linux/types.h>
#include<linux/fs.h>
#include<linux/sched.h>
#include<linux/init.h>
#include<linux/cdev.h>
#include<linux/device.h>
#include<linux/mm.h>
#include<linux/miscdevice.h>
/*平台相关的头文件*/
#include<mach/regs-gpio.h>
#include<mach/hardware.h>
#include<linux/errno.h>
#include<linux/gpio.h>
#include<linux/cdev.h>
#include<linux/slab.h>
#include<linux/string.h>
#include<linux/kernel.h>
/*定义自己的命令*/
/*定义幻数,表示具体的设备*/
#define LED_MAGIC_NUMBER 'k'
#define LED_ALL_ON _IO(LED_MAGIC_NUMBER,0)
#define LED_ALL_OFF _IO(LED_MAGIC_NUMBER,1)
#define LED_ON _IO(LED_MAGIC_NUMBER,2)
#define LED_OFF _IO(LED_MAGIC_NUMBER,3)
#define LED_MAX_CMD 4
/*设备名*/
#define DEVICE_NAME "GP_LED"
/*具体的端口号*/
static unsigned int led_table[] =
{
S3C2410_GPB5, S3C2410_GPB6,
S3C2410_GPB7,
S3C2410_GPB8,
};
/*端口的功能数组*/ static unsigned int led_cfg_table[]=
{
S3C2410_GPB5_OUTP,
S3C2410_GPB6_OUTP,
S3C2410_GPB7_OUTP,
S3C2410_GPB8_OUTP,
/*或者采用通用功能*/ /* S3C2410_GPIO_OUTPUT, S3C2410_GPIO_OUTPUT, S3C2410_GPIO_OUTPUT, S3C2410_GPIO_OUTPUT, */ };
static int s3c2440_led_ioctl(
struct inode * inode,
struct file *file,
unsigned int cmd,
unsigned long arg
)
{
int i = 0;
/*检测参数的正确性*/
if(_IOC_TYPE(cmd)!=LED_MAGIC_NUMBER)
return -EINVAL;
/*检查命令是否超过一定的界限*/
if(_IOC_NR(cmd) >= LED_MAX_CMD)
return -EINVAL;
/*检查arg参数的正确性*/
if(arg<0 || arg >4)
{
return -EINVAL;
}
/*命令控制语句*/ switch(cmd)
{
case LED_ALL_ON:
{
for(i = 0; i < 4; ++ i)
s3c2410_gpio_setpin(led_table[arg-i-1],0);
break;
}
case LED_ALL_OFF:
{
for(i = 0; i < 4; ++ i)
s3c2410_gpio_setpin(led_table[arg-i-1],1);
break;
}
case LED_ON:
{
s3c2410_gpio_setpin(led_table[arg],0);
break;
}
case LED_OFF:
{
s3c2410_gpio_setpin(led_table[arg],1);
break;
}
default:
{
return -EINVAL;
break;
}
}
return 0;
}
/*具体函数*/
static const struct file_operations led_fops =
{
.owner = THIS_MODULE,
.ioctl = s3c2440_led_ioctl,
};
/*混杂设备类*/
static const struct miscdevice misc =
{
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME,
/*此处是一个地址,而不是一个数*/
.fops = &led_fops,
};
/*初始化*/
static int __init dev_init(void)
{
int ret;
int i;
for(i = 0; i<4; ++i)
{
s3c2410_gpio_cfgpin(led_table[i],led_cfg_table[i]);
s3c2410_gpio_setpin(led_table[i],1);
}
ret = misc_register(&misc);
printk(DEVICE_NAME"\tinitialized\n");
return ret;
}
/*退出*/
static void __exit dev_exit(void)
{
misc_deregister(&misc);
}
module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("GP");
测试应用程序如下:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/ioctl.h>
#define LED_MAGIC_NUMBER 'k'
#define LED_ALL_ON _IO(LED_MAGIC_NUMBER,0)
#define LED_ALL_OFF _IO(LED_MAGIC_NUMBER,1)
#define LED_ON _IO(LED_MAGIC_NUMBER,2)
#define LED_OFF _IO(LED_MAGIC_NUMBER,3)
int main(int argc,char *argv[])
{
int fd,cmd;
unsigned int arg;
if(argc != 3)
{
printf("parameter is not right");
exit(-1);
}
cmd = atoi(argv[1]);
arg = atoi(argv[2]);
if(cmd > 3 || cmd < 0 || arg > 4 || arg < 0)
{
printf("The style of command is not right\n");
exit(-1);
}
fd = open("/dev/GP_LED",O_RDWR);
if(fd == -1)
{
printf("Open File wrong!!\n");
exit(-1);
}
switch(cmd)
{
case 0:
cmd = LED_ALL_ON;
arg = 4;
break;
case 1:
cmd = LED_ALL_OFF;
arg = 4;
break;
case 2:
cmd = LED_ON;
break;
case 3:
cmd = LED_OFF;
break;
default:
exit(-1);
}
int isOk = ioctl(fd,cmd,arg);
printf("%d",isOk);
close(fd);
exit(0);
}
分析代码:
应用程序没什么好分析的,关键是驱动代码中的几个重要的数据结构S3C2410_GPB5-S3C2410_GPB8以及 S3C2410_GPB5_OUTP--S3C2410_GPB8_OUTP和两个函数 s3c2410_gpio_cfgpin(),s3c2410_gpio_setpin()。
其中3C2410_GPB5- S3C2410_GPB8是指GPB5-GPB8这四个IO口,Linux中对端口都进行了编号,给予每一个IO口唯一的端口号。同时又将端口分成了很多块GPx,包括GPA,GPB,...,GPH等。每一块的起始端口号为(x-1)*32+0,也就是GPA的起始端口号为0,而GPB的起始端口号为 32,依此类推。而S3C2410_GPB5_OUTP是指将GPB5配置为输出口,每一个IO口都是多功能IO,使用前都需要进行配置。
具体的源码如下:
... /*得到端口号,每一个IO口的端口号是唯一的*/ #define S3C2410_GPB5 S3C2410_GPIONO(S3C2410_GPIO_BANKB, 5)
/*定义端口的功能,其中的<<10,是因为在GPBCON的第10bit开始是配置端口B的功能,其他的也类似,只是位不同*/ #define S3C2410_GPB5_INP (0x00 << 10)
#define S3C2410_GPB5_OUTP (0x01 << 10)
#define S3C2410_GPB5_nXBACK (0x02 << 10)
#define S3C2443_GPB5_XBACK (0x03 << 10)
#define S3C2400_GPB5_DATA21 (0x02 << 10)
#define S3C2400_GPB5_nCTS1 (0x03 << 10)
#define S3C2410_GPB6 S3C2410_GPIONO(S3C2410_GPIO_BANKB, 6)
#define S3C2410_GPB6_INP (0x00 << 12)
#define S3C2410_GPB6_OUTP (0x01 << 12)
#define S3C2410_GPB6_nXBREQ (0x02 << 12)
#define S3C2443_GPB6_XBREQ (0x03 << 12)
#define S3C2400_GPB6_DATA22 (0x02 << 12)
#define S3C2400_GPB6_nRTS1 (0x03 << 12)
#define S3C2410_GPB7 S3C2410_GPIONO(S3C2410_GPIO_BANKB, 7)
#define S3C2410_GPB7_INP (0x00 << 14)
#define S3C2410_GPB7_OUTP (0x01 << 14)
#define S3C2410_GPB7_nXDACK1 (0x02 << 14)
#define S3C2443_GPB7_XDACK1 (0x03 << 14)
#define S3C2400_GPB7_DATA23 (0x02 << 14)
#define S3C2410_GPB8 S3C2410_GPIONO(S3C2410_GPIO_BANKB, 8)
#define S3C2410_GPB8_INP (0x00 << 16)
#define S3C2410_GPB8_OUTP (0x01 << 16)
#define S3C2410_GPB8_nXDREQ1 (0x02 << 16)
#define S3C2400_GPB8_DATA24 (0x02 << 16)
...
...
#define S3C2410_GPIONO(bank,offset) ((bank) + (offset))
/*将IO分成8块,便于管理同一类型的端口*/ #define S3C2410_GPIO_BANKA (32*0)
#define S3C2410_GPIO_BANKB (32*1)
#define S3C2410_GPIO_BANKC (32*2)
#define S3C2410_GPIO_BANKD (32*3)
#define S3C2410_GPIO_BANKE (32*4)
#define S3C2410_GPIO_BANKF (32*5)
#define S3C2410_GPIO_BANKG (32*6)
#define S3C2410_GPIO_BANKH (32*7)
两个函数s3c2410_gpio_cfgpin(),s3c2410_gpio_setpin()分别表示配置端口(配置功能寄存器)和设置端口(写读数据寄存器)。
在linux内核中,通常将将CPU和外设的寄存器从物理地址静态的映射到了虚拟地址空间中以固定地址开始的一段内存空间上。
S3C24XXCPU的CPU和外设寄存器映射关系分布如下图所示:
具体的实现参看源码:
/*配置端口的功能寄存器*/ void s3c2410_gpio_cfgpin(unsigned int pin, unsigned int function)
{
void __iomem *base = S3C24XX_GPIO_BASE(pin);
/*mask可以用来清零,或者置位*/ unsigned long mask;
unsigned long con;
unsigned long flags;
/*针对GPA的偏移量,因为只有两种功能,所以只有1bit表示,偏移量也只需要1*/ if (pin < S3C2410_GPIO_BANKB) {
/*用于将当前端口所在的位置位或者清零 清零:con &= ~mask; 置位:con |= mask; */ mask = 1 << S3C2410_GPIO_OFFSET(pin);
} /*针对GPB开始的端口,因为功能比较多,需要两个bit描述一个端口的功能,所以偏移量乘以2*/ else {
/*同样也可以实现将对应的两个位置位或者清零*/ mask = 3 << S3C2410_GPIO_OFFSET(pin)*2;
}
/*根据功能设置相应的设置相应的位操作,这一部分主要是为了实现对常规命令解析*/
switch (function) {
case S3C2410_GPIO_LEAVE:
mask = 0;
function = 0;
break;
/*采用通用命令的形式,需要对相应的位进行解析,因此驱动中也可以将S3C2410_GPB5_OUTP置换为S3C2410_GPIO_OUTPUT*/
case S3C2410_GPIO_INPUT:
case S3C2410_GPIO_OUTPUT:
case S3C2410_GPIO_SFN2:
case S3C2410_GPIO_SFN3:
/*从通用方法中提取对应端口的功能*/ if (pin < S3C2410_GPIO_BANKB) {
function -= 1;
function &= 1;
function <<= S3C2410_GPIO_OFFSET(pin);
} else {
/*根据function确定2个bits的值*/
function &= 3;
/*将function设置到相应的位置,此处是简单的位操作*/
function <<= S3C2410_GPIO_OFFSET(pin)*2;
}
}
/* modify the specified register wwith IRQs off */
/*写操作都是先读再写*/
/*保存中断*/
local_irq_save(flags);
/*读寄存器,base是寄存器基地址,而0x0是表示第一个寄存器GPBCON*/
con = __raw_readl(base + 0x00);
/*清零对应的位*/
con &= ~mask;
/*设置相应的位为对应的功能*/
con |= function;
/*写入寄存器*/
__raw_writel(con, base + 0x00);
/*恢复中断*/
local_irq_restore(flags);
}
/*写端口的数据寄存器,也就是GPxDAT的某一端口*/
void s3c2410_gpio_setpin(unsigned int pin, unsigned int to)
{
/*得到端口所在块的寄存器基地址*/
void __iomem *base = S3C24XX_GPIO_BASE(pin);
/*得到端口在所在寄存器中具体的偏移量*/
unsigned long offs = S3C2410_GPIO_OFFSET(pin);
unsigned long flags;
unsigned long dat;
/*中断保存*/ local_irq_save(flags);
/*读寄存器GPxDAT,base是所在块寄存器的基地址,0x4是当前寄存器的偏移量*/ dat = __raw_readl(base + 0x04);
/*清零该端口当前的数据*/ dat &= ~(1 << offs);
/*一般是保存在另一个值中*/ dat |= to << offs;
__raw_writel(dat, base + 0x04);
local_irq_restore(flags);
}
/*关于推到地址的方法按照上面的分布图对照分析可能比较方便*/ #ifdef CONFIG_CPU_S3C2400
#define S3C24XX_GPIO_BASE(x) S3C2400_GPIO_BASE(x)
#define S3C24XX_MISCCR S3C2400_MISCCR
#else
#define S3C24XX_GPIO_BASE(x) S3C2410_GPIO_BASE(x)
#define S3C24XX_MISCCR S3C24XX_GPIOREG2(0x80)
#endif
#define S3C2410_GPIO_BASE(pin) ((((pin) & ~31) >> 1) + S3C24XX_VA_GPIO)
#define S3C2410_GPIO_OFFSET(pin) ((pin) & 31)
/*GPIO的物理地址起始*/
#define S3C2410_PA_GPIO (0x56000000)
/*GPIO的虚拟地址起始,实现的方法是在两个物理地间隔加上UART 虚拟地址的起始*/
#define S3C24XX_VA_GPIO ((S3C24XX_PA_GPIO - S3C24XX_PA_UART) + S3C24XX_VA_UART)
/*大小*/
#define S3C24XX_SZ_GPIO SZ_1M
#define S3C24XX_VA_UART S3C_VA_UART
#define S3C2410_PA_UART (0x50000000)
#define S3C24XX_SZ_UART SZ_1M
/*映射的虚拟地址的固定起始地址*/
#define S3C_ADDR_BASE(0xF4000000)
#ifndef __ASSEMBLY__
#define S3C_ADDR(x)((void __iomem __force *)S3C_ADDR_BASE + (x))
#else
#define S3C_ADDR(x)(S3C_ADDR_BASE + (x))
#endif
#define S3C_VA_IRQS3C_ADDR(0x00000000)/* irq controller(s) */
#define S3C_VA_SYSS3C_ADDR(0x00100000)/* system control */
#define S3C_VA_MEMS3C_ADDR(0x00200000)/* system control */
#define S3C_VA_TIMERS3C_ADDR(0x00300000)/* timer block */
#define S3C_VA_WATCHDOGS3C_ADDR(0x00400000)/* watchdog */
#define S3C_VA_UARTS3C_ADDR(0x01000000)/* UART */
static unsigned int __raw_readl(unsigned int ptr)
{
/*volatile表示ptr中的值是易变性的,*((volatileunsignedint*)ptr)是对地址取值*/ return *((volatile unsigned int *)ptr);
}
static void __raw_writel(unsigned int value, unsigned int ptr)
{
/*((volatileunsignedint*)ptr)是对地址赋值*/ *((volatile unsigned int *)ptr) = value;
}
映射虚拟地址的关系建议自己绘图,可能更加的直观,快捷。在这段代码中的两个宏语句是比较难以理解的。这两句宏定义充分利用了位操作的优势。
/*求端口所在块寄存器的起始虚拟地址*/
#define S3C2410_GPIO_BASE(pin) ((((pin) & ~31) >> 1) + S3C24XX_VA_GPIO)
/*求端口在所在块的偏移量*/
#define S3C2410_GPIO_OFFSET(pin) ((pin) & 31)
我们加以分析。我们以GPB5、GPA5作为分析对象。
GPB5的端口ID号是32*(2-1)+5=37,GPA5的端口ID号是32*(1-1)+5=5;
根据上面的图可知,S3C24XX_VA_GPIO的值为0xFB000000.
S3C2410_GPIO_BASE(GPB5) = ((37&~31)>>1)+0xFB000000=0xFB000010 (刚好对应于GPBCON的虚拟地址)
S3C2410_GPIO_BASE(GPA5) = ((5&~31)>>1)+0xFB000000=0xFB000000 (刚好对应于GPBACON的虚拟地址)
也就是相当于得到每一块IO口的基地址也就是GPA,GPB,GPC,...等对应的地址。
S3C2410_GPIO_OFFSET(GPB5) = 37 & 31 = 5;相当于求32的余数,也就是得到端口在所在块中的偏移量。
LED的驱动很简单,该驱动是按着ioctl实现的一般步骤实现的,具有延续性。