版主: 51FPGA

分享到:
共2条 1/1 1   

ZedBoard学习手记(五)为自定义外设编写Linux驱动

    [您是本帖的第1800位阅读者]
啸风
我是GG
高级会员

最后登陆时间:2015-01-28 20:37:59

直达楼层
1# 发表于 2013-06-08 16:04:07

写完上一篇博客后,部门开了新项目,虽然只是开始,但是兔子也不敢懈怠,加之北京气温骤降,又刮起大风,可能是天冷的原因吧,胃又不太舒服了,白天忙完了晚上回来就顿觉十分疲惫,因而这篇手记一直拖到现在才动笔。

经过前面的工作,现在终于可以开始为自定义外设编写驱动了。首先声明兔子不是搞软件的,而是个硬件工程师,有时候也肩负起逻辑的工作,因而做的有问题的地方还需要童鞋们指出,共同进步啊。其实Linux驱动程序与一般的单片机C程序差别不大,只是在调用硬件设备的同时,实现了一个与操作系统的标准接口,只要完成了驱动部分,上层软件就能通过Linux系统的标准接口来访问设备,而不用关心寄存器等具体的硬件问题。

不过为了让不熟悉驱动和软件的同学不至于一头雾水,就稍稍做些普及工作吧。

下面是一个简单的驱动模块:

#include

#include

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)

{

     printk("Module init complete!\n");

     return 0;

}

static void hello_exit(void)

{

     printk("Module exit!\n");

}

module_init(hello_init);

module_exit(hello_exit);

 

MODULE_AUTHOR("Nightmare@EEFOCUS");

MODULE_DESCRIPTION("HelloWorldDriver");

MODULE_ALIAS("It's only a test");

 

它为我们展现了驱动程序模块最基本的封装形式,init函数会在加载模块时执行,exit函数则在模块卸载时运行,在ZedBoard板上加载这个驱动,相应的字符串信息会通过串口打印出来。

之前我们已经提到了,对自定义外设的访问实际就是操作特定的物理地址空间,但Linux系统直接操作物理空间缺乏可移植性也不安全,因此需要对这个物理地址空间映射到虚拟地址空间(具体应该是通过MMU实现的吧,留作疑问),转变成系统能够直接访问的空间。映射是通过ioremap实现的,这个函数定义在Linux内核的asm/io.h文件中。

对设备进行映射的操作方法大体如下:

static void __iomem *GPIO_Regs;

GPIO_Regs = ioremap(MY_GPIO_PHY_ADDR,MY_GPIO_REG_NUM);

printk("my_gpio: Access address to device is:0x%x\n", (unsignedint)GPIO_Regs);

 

GPIO_Regs指针指向映射后的空间,通过ioremap函数,为I/O地址为MY_GPIO_PHY_ADDR的设备分配了MY_GPIO_REG_NUM大小的空间。映射后的虚拟地址将通过内核打印出来。

当有了可以直接操作的地址后,通过iowrite32ioread32就可以简单便捷地实现写入和读取数据的操作。

int val;

val = ioread32(GPIO_Regs);

printk("my_gpio: Read 0x%x from switches, writing to LED...", val);  

iowrite32(val, GPIO_Regs+4);

printk("OK!\n");

执行上述代码,GPIO_Regs开头地址处的32位数据——自定义外设寄存器0,也就是Switch状态——会被读取,并写入到偏移4字节(32位)之后的地址中,即自定义外设的寄存器1,控制LED的状态。将这段代码加入到上面提到的init函数中,当驱动模块加载后,8个开关的电平状态会被读取,并在LED8个管脚上输出,使LED亮灭与Switch开关保持一致。

这就是通过对静态I/O地址(MY_GPIO_PHY_ADDR就是我们在前几篇中设置的my_gpio外设地址0x75c80000)映射的方式,让Linux系统有能力访问设备。那么Device Tree又是做什么用的呢,我曾就这个问题在网上询问一位国外的工程师,现将他的回答摘录如下:

The standard way is to add an entry for your peripheral in Linux’ device tree, and have the driver fetch the physical address from there. But for a on-off project, hardcoding the physical address of the peripheral in the driver is fairly acceptable.

事实上,在MicroBlaze中,就可以通过of_platform实现读取DeviceTreecompatible信息,来匹配设备和驱动:

static struct of_device_id my_gpio_of_match[] __devinitdata = {

 { .compatible = "xlnx, my_gpio-1.00.a", },

 {}

};

 

只是兔子还没在Zynq平台上成功实现过,而项目的时间又那么紧,于是就采取了“acceptable”的方法了,哈哈。有兴趣的童鞋可以继续深究。

当然,只实现对设备的访问还是不够的,这样的驱动,只有在加载或卸载时会执行一些操作,而无法被Linux下的应用程序访问。因此,我们还要完成一个对上层的接口,这需要利用Linux的文件系统。下面的两个结构体正是实现了这样的功能:

static const struct file_operations my_gpio_fops =

{

     .owner = THIS_MODULE,

     .open = my_gpio_open,

     .release = my_gpio_release,

     .read = my_gpio_read,

     .unlocked_ioctl = my_gpio_ioctl,

};

 

static struct miscdevice my_gpio_dev =

{

     .minor = MISC_DYNAMIC_MINOR,

     .name = DEVICE_NAME,

     .fops = &my_gpio_fops,

};

 

兔子在这里定义了一个MISC型的设备,并为之建立了openreleasereadioctl的方法,openrelease是打开和释放设备的操作,这里并无实际功能。

static int my_gpio_open(struct inode * inode , struct file * filp)

{

  return 0;

}

 

static int my_gpio_release(struct inode * inode, struct file *filp)

{

  return 0;

}

 

ioctl就是写寄存器操作的封装函数了,使用的方法依旧是iowrite32,其中reg_num代表寄存器编号,arg是要写入的32位值。

static int my_gpio_ioctl(struct file *filp, unsigned int reg_num,unsigned long arg)

{

  if(reg_num>=0 && reg_num

  {

    iowrite32(arg, GPIO_Regs+reg_num*4);

    printk("my_gpio: Write 0x%x to 0x%x!\n", arg, GPIO_Regs+reg_num*4);

  }

  else

  {

    printk("my_gpio:[ERROR] Wrong register number!\n");

    return -EINVAL;

  }

 

  return 0;

}

 

read方法则可以将读取到的数据传递给上层,由于其返回值为char型,因此要分几次读取(当然使用ioread32再拆分也可以啦)。

static int my_gpio_read(struct file *filp, char *buffer, size_t length, loff_t * offset)

{

  int bytes_read = 0;

  int i=0;

 

  if (filp->f_flags & O_NONBLOCK)

            return -EAGAIN;

    

  if (length>0 && length<=(MY_GPIO_REG_NUM*4))

  {

    for(i=0;i

    {

      *(buffer+i)=(char)ioread8(GPIO_Regs+i);

    }

    bytes_read=i;

  }

  return bytes_read;

}

 

再使用misc_register函数对设备进行注册:

int ret;

ret = misc_register(&my_gpio_dev);

 

设备注册后,会在dev目录下生成一个以DEVICE_NAME值命名的文件(兔子这里是“my_gpio_dev”)。操作这个文件,就相当于操作我们的设备了,具体使用的也就是文件操作的opencloseread等函数。驱动写完后,还需要进行编译,兔子写了一个简单的Makefile文件,用于生成可被系统加载的.ko文件,在源文件和Makefile的目录下输入 make 指令即可。KERN_SRC指的是linux内核的路径Makefile内容如下:

# Cross compiler makefile for my_gpio

# By Nightmare @ EEFOCUS 2012-10-10

KERN_SRC=/arm/zed/linux-3.3-digilent

obj-m := my_gpio.o

 

all:

     make -C $(KERN_SRC) ARCH=arm M=`pwd` modules

clean:

     make -C $(KERN_SRC) ARCH=arm M=`pwd=` clean

 

下面是完整的驱动代码、Makefile文件和生成的.ko模块文件:

 

My_GPIO_Driver.rar (已重传)


 

 

将编译好的驱动文件通过U盘加载到ZedBoard中:

mount /dev/sda2 /mnt

insmod /mnt/my_gpio.ko

 

卸载驱动的指令为rmmod

rmmod my_gpio

 

加载设备时,LED会根据Switch的状态来实现亮灭。怎么样,Linux下的自定义外设驱动其实挺简单吧。上效果图:

 

 

 

 


——转自网友 懒兔子 




关键词:ZedBoard    手记    

一粒沙里看世界。

此贴由renazan2000于2013-06-09 10:41:46最后编辑

RE: ZedBoard学习手记(五)为自定义外设编写Linux驱动

zhaoweichao
我是GG
高级会员

最后登陆时间:2015-01-20 15:01:56

2# 发表于 2015-01-20 14:13:04
大神啊。写的相当好,真是受益匪浅。谢谢。。。分享。
共2条 1/1 1   
快速回复主题
  • 匿名不能发帖!请先 [ 登陆 注册 ]