问答 店铺
热搜: ZYNQ FPGA discuz

QQ登录

只需一步,快速开始

微信登录

微信扫码,快速开始

2

LINUX篇 基于debian9系统 CH12-LED字符驱动程序

摘要: 12.1概述 本节课开始,我们一起学习基于ZYNQ的驱动程序开发。首先声明由于本人能力有限作为一名LINUX驱动初学者,教程更多以笔记形势暂时,笔者对于LINUX驱动程序开发的理解,而不是一名资深的LINUX驱动开发者。笔 ...

12.1概述

     本节课开始,我们一起学习基于ZYNQ的驱动程序开发。首先声明由于本人能力有限作为一名LINUX驱动初学者,教程更多以笔记形势暂时,笔者对于LINUX驱动程序开发的理解,而不是一名资深的LINUX驱动开发者。笔记从自生学习的角度,记录如何学习基于ZYNQ的驱动开发。想必学习过单片机的‘童鞋们’都很怀念这个词:“点灯”。一闪一闪的灯亮瞎你的猫眼…咳咳…言归正传,笔者毫不夸张地说:“点灯这件事看似简单,实则贯穿着无数的知识点。”只是一千个读者有一千个哈姆雷特罢了。好了好了,我们回到今天的内容。

12.2在设备树中添加设备信息

       本章节先不使用设备树,所以读者可以跳过此步骤。 虽然不使用设备树,但我们也简单介绍一下设备树的作用吧,读者可以先简单理解为:“linux内核通过读取设备树来了解它到底能掌控哪些设备,这些设备有什么样的参数”。

12.3 MISCDEVICE驱动开发概述

       在linux驱动中,需要提供主设备号和次设备号号,通常使用的主设备号是从 0到 255之间的数,仅仅使用主设备号,还是不叫紧张的,因此需要利用次设备号。linux中,提供了miscdevice这种杂项设备,指定主设备号为10,次设备号可以设置为系统动态分配。在具体分析miscdevice之前,先给出miscdevice的核心设计思想。应用层,打开/dev/xxx 节点,会得到 文件描述符 fd,通过fd,可以执行 read()、wriete()、ioctl()、mmap()等对这个文件的操作。

       应用层调用 open()函数,经过系统调用后,会调用驱动程序的 open()函数。在前边分析调用过程时,分析过这个open()的调用过程。

       驱动程序中,调用file_opeartions的open()的时候,此时,调用到另外一个 file_operations的open(),此时应用层会得到这个open()时的 文件描述符fd,此时应用层 read()、write()等操作,就调用到了这个file_operations。故在cdev_init时的 fops 充当一个中转的作用,转到具体的转到具体的fops来操作。

12.4 创建驱动开发路径的文件夹

       首先我们创建一个名为driver_app的文件夹,在文件夹内部创建app_code和drv_code两个文件夹,再把共享文件夹内的本课运行代码和驱动程序相关文件拷过来。

led_dev.c

#include <linux/module.h>

#include <linux/kernel.h>

#include <linux/fs.h>

#include <linux/device.h>

#include <asm/io.h>

#include <linux/init.h>

#include <linux/platform_device.h>

#include <linux/miscdevice.h>

#include <linux/ioport.h>

#include <linux/of.h>

#include <linux/uaccess.h>


static void __iomem *led_base; //led寄存器基地址


//打开设备触发的服务函数

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

{

printk("dev is open!");

return 0;

}


//写设备触发的服务函数 file:设备  buf:数据缓存区 count:数据个数单位字节

static int led_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)

{

int val,ret;


//把buff缓存数据拷贝到val地址空间

ret = copy_from_user(&val, buf, count);


//把val的值写进led_base寄存器

iowrite32(val, led_base);


printk("led : Write 0x%x to 0x%x.\n", val, (unsigned int)led_base);

return 0;

}


//LED函数接口结构体

static const struct file_operations led_fops =

{

.owner = THIS_MODULE,

.open = led_open,

.write = led_write,   

};


//LED设备结构体

static struct miscdevice led_dev=

{

.minor = MISC_DYNAMIC_MINOR,

.name = "led_dev", // /dev/目录下的设备节点名

.fops = &led_fops,

};


//LED设备初始化

static int led_init(void)

{

int ret;


//地址映射:把物理地址转换为虚拟地址

led_base = ioremap(0x41200000, 4);

printk("LED: Access address to device is:0x%x\n", (unsigned int)led_base);


//注册设备到/dev/目录

ret = misc_register(&led_dev);

if(ret)

{

printk("led:[ERROR] Misc device register failed.\n");

return ret;

}


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

return 0;

}


//LED设备退出

static void led_exit(void)

{

printk("Module exit!\n");

iounmap(led_base); //取消物理地址的映射

misc_deregister(&led_dev); //删除/dev/目录下的设备节点

}


module_init(led_init); //模块初始化接口

module_exit(led_exit); //模块推出接口


//以下代码可解决一些错误信息

MODULE_AUTHOR("osrc@ ");

MODULE_DESCRIPTION("ledDriver");

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

MODULE_LICENSE("Dual BSD/GPL");

12.5 驱动函数分析

12.5.1驱动的注册

       驱动编译后执行insmod ./led_dev.ko把驱动注册到内核中,当驱动被注册到内核总的时候系统会对驱动做一些的初始化操作。首先是操作系统对led_dev的驱动程序地址空间的分配,包括定义的结构体,变量,指针,和函数。分配完成后,首先执行static int led_init(void)。

        在linux kernel 中,物理地址是不能直接使用的,必须通过转换才可以。转换分为两种, 静态和动态。不过,静态的地址转换,还需要在kernel 初始化的时候作映射。动态映射是使用 ioremap 函数。本节课程中使用 ioremap 函数动态映射。led_base = ioremap(0x41200000, 4);映射了AXI_GPIO的物理地址0x41200000到用户地址空间,地址长度为BYTE。之后misc_register函数注册led_dev的驱动到内核。

。//LED设备初始化

static int led_init(void)

{

int ret;


//地址映射:把物理地址转换为虚拟地址

led_base = ioremap(0x41200000, 4);

printk("LED: Access address to device is:0x%x\n", (unsigned int)led_base);


//注册设备到/dev/目录

ret = misc_register(&led_dev);

if(ret)

{

printk("led:[ERROR] Misc device register failed.\n");

return ret;

}


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

return 0;

}

12.5.2驱动的打开

        当应用程序通过调用open("/dev/led_dev", O_RDWR);函数打开设备的时候,此函数被调用。

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

{

printk("dev is open!");

return 0;

}

12.5.3驱动的卸载

       执行rmmod led_dev的时候卸载驱动

//LED设备退出

static void led_exit(void)

{

printk("Module exit!\n");

iounmap(led_base); //取消物理地址的映射

misc_deregister(&led_dev); //删除/dev/目录下的设备节点

}

12.5.4通过led_write函数

       用户程序通过led_write对GPIO进行操作,控制LED.ret = copy_from_user(&val, buf, count);函数实现了把用于空间的程序拷贝到内核空间。iowrite32 可以直接范围我们刚刚ioremap后的地址空间。

//写设备触发的服务函数 file:设备  buf:数据缓存区 count:数据个数单位字节

static int led_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)

{

int val,ret;


//把buff缓存数据拷贝到val地址空间

ret = copy_from_user(&val, buf, count);


//把val的值写进led_base寄存器

iowrite32(val, led_base);


printk("led : Write 0x%x to 0x%x.\n", val, (unsigned int)led_base);

return 0;

}

12.5.5导出的模块函数

      Insmod 命令的时候执行module_init()函数,rmmod的时候执行module_exit()函数

module_init(led_init); //模块初始化接口

module_exit(led_exit); //模块推出接口


12.5.5 GPL v2许可及驱动描述信息 

       其中读者注意最后一条必须这么写,才能符合GPL 的规范,否则驱动可能无法工作。

MODULE_AUTHOR("osrc@ ");

MODULE_DESCRIPTION("ledDriver");

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

MODULE_LICENSE("Dual BSD/GPL");

12.5.6应用程序

       应用程序中fd = open("/dev/led_dev", O_RDWR); //打开设备。

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <stdio.h>

#include <unistd.h>


int main(int argc, char **argv)

{

int i;

int fd;

int val=1;


fd = open("/dev/led_dev", O_RDWR); //打开设备

   if (fd <= 0) {

                Printf(“open %s error!\n”, filename)

                return;;

        }

//LED流水灯

while(1)

{

for(i=0;i<4;i++)

{

val = (1<<i);

write(fd,&val,4); //把val的值写到fd设备中,大小为4字节

sleep(1);

}

}

return 0;

}

      这个open函数不简单 ,而且如果你不阅读下面的话,你一定理解为我们驱动里面的open函数范围的0.open函数是我们开发中经常会遇到的,这个函数是对文件设备的打开操作,这个函数会返回一个句柄fd,我们通过这个句柄fd对设备文件读写操作。我们在对这个fd作判断的时候,经常会用到:

fd = open(filename, O_RDONLY);

  if (fd <= 0) {

   Printf(“open %s error!\n”, filename)

   return;;

}

 我们先来看看open函数的原型:

         int open(constchar*pathname,intflags);

         int open(constchar*pathname,intflags,mode_tmode);

 函数参数:

        pathname:打开文件的路径名

        flags:用来控制打开文件的模式

        mode:用来设置创建文件的权限(rwx)。当flags中带有O_CREAT时才有效。

        返回值:调用成功时返回一个文件描述符fd,调用失败时返回-1,并修改errno

        正确的判断应该是 if(fd < 0),那我们什么时候会fd=0呢,如果fd=0,那么已经正常打开了,但是我们判断了打开错误了。

       open函数返回的文件描述符fd一定是未使用的最小的文件描述符,那么如果0没有使用,那么我们open的时候,首先就会获取到fd=0的情况。默认情况下,0,1,2这三个句柄对应的是标准输入,标准输出,标准错误,系统进程默认会打开0,1,2这三个文件描述符,而且指向了键盘和显示器的设备文件。所以通常我们open的返回值是从3开始的。

       如果我们在open之前,close其中的任何一个,则open的时候,则会用到close的最小的值:

       close(0);

       fd = open(filename,O_RDONLY);

       printf(“fd = %d\n”, fd);

       则可以发现我们就可以open的时候,返回了0的fd.

12.5.7 Makefile

ifneq ($(KERNELRELEASE),)


#param-objs := file1.o file1.o

obj-m := led_dev.o


else


KERN_DIR = /mnt/workspace/osrc-lab/sources/kernel


all:

make -C $(KERN_DIR) M=`pwd` modules


clean:

make -C $(KERN_DIR) M=`pwd` modules clean

rm -rf modules.order


endif

说明:
     当我们在模块的源代码目录下运行make时,make是怎么执行的呢?
     假设模块的源代码目录是driver_app/axi_gpio/dev下。

先说明以下makefile中一些变量意义:
(1)KERNELRELEASE是在内核源码的顶层Makefile中定义的一个变量,在第一次读取执行此Makefile时,KERNELRELEASE没有被定义,所以make将读取执行else之后的内容,如果make的目标是clean,直接执行clean操作,然后结束。

(2)-C $(KERN_DIR) 当make的目标为all时,-C $( KERN_DIR)指明跳转到内核源码目录下读取那里的Makefile;

(3)M=`pwd`当从内核源码目录返回时,KERNELRELEASE已被定义,kbuild也被启动去解析kbuild语法的语句,make将继续读取else之前的内容。else之前的内容为kbuild语法的语句,指明模块源码中各文件的依赖关系,以及要生成的目标模块名。

(4)KERN_DIR变量便是当前内核(嵌入式LINUX内核)的源代码目录。

在上面的例子中,我们将该变量设置成了led_dev.o。

关于make modules的更详细的过程可以在内核源码目录下的scripts/Makefile.modpost文件的注释中找到。

   如果把led_dev模块移动到内核源代码中。例如放到..kernel/linux/driver/中, KERNELRELEASE就有定义了。

   在..kernel/linux/Makefile中有

KERNELRELEASE=$(VERSION).$(PATCHLEVEL).$(SUBLEVEL)$(EXTRAVERSION)$(LOCALVERSION)。

这时候,led_dev模块也不再是单独用make编译,而是在内核中用make modules进行编译,此时驱动模块便和内核编译在一起。


12.7程序测试

1>首先我们执行settings64.sh。

2>修改Makefile如下图,主要是修改驱动名和内核文件的路径。

3>执行make编译驱动程序。

4>确认开发板的ip地址和电脑是同一网段,执行ssh连接开发板。

5>在开发板下启用交叉编译工具。

6>拷贝更新后的driver_app文件,使用scp –r命令。

7>进入驱动路径,执行insmod,加载led驱动。

8>成功后驱动已经注册内核,查看这个驱动设备,串口控制台输出也可以看到module init已经完成。

9>进入app_code,使用g++编译run_led.c。


10>执行程序a.out,开发板看到流水灯循环闪烁,串口看到写不同的内容到地址上。

11>使用rmmod卸载驱动,串口打印module exit。


路过

雷人

握手

鲜花

鸡蛋
发表评论

最新评论

引用 qorjoasj 2023-5-22 21:24
请问,0x41200000这个地址是怎么产生的?我使用的是ZYNQ-XC7030核心板,想操作PS端MIO PIN7点亮LED灯,应该如何操作?
引用 记得小时候 2020-11-13 14:21
fd = open("/dev/led_dev", O_RDWR); //打开设备

   if (fd <= 0) {

                Printf(“open %s error!\n”, filename)

                return;;

        }
你好 这段代码编译出错,请问视屏中后来是如何修改的呢?把filename换成了fileno吗?两者有什么区别呢?麻烦大神有空时回复下,谢谢!

查看全部评论(2)

本文作者
2019-10-9 17:07
  • 1
    粉丝
  • 1824
    阅读
  • 2
    回复

关注米联客

扫描关注,了解最新资讯

联系人:汤经理
电话:0519-80699907
EMAIL:270682667@qq.com
地址:常州溧阳市天目云谷3号楼北楼201B
热门评论
排行榜

  • 微信公众平台

  • 扫描访问手机版

  • QQ: 270682667

    客服电话

    0519-80699907

    电子邮件

    admin@uisrc.com

    在线时间:8:30-21:00(周一~周六)

  • 返回顶部