想必学习过单片机的‘童鞋们’都很怀念这个词:“点灯”。一闪一闪的灯亮瞎你的猫眼…咳咳…言归正传,笔者毫不夸张地说:“点灯这件事看似简单,实则贯穿着无数的知识点。”只是一千个读者有一千个哈姆雷特罢了。好了好了,我们回到今天的内容。
首先说一下文章的内容排布,驱动篇的内容安排几乎都一样的,具体安排如下5个步骤:
①在vivado中建立硬件工程。
②在设备树中添加设备信息(非必要)。
③针对添加的硬件设备编写驱动程序。
④编写应用层的控制程序。
⑤下载到板子调试验证。
好了,大家大致了解了我们驱动的编写步骤之后,我们就开始干活吧。
我们使用第一章CH01中的工程,添加ip核axi gpio , 设置为4位的输出模式(输出位数以自己实际板子为本),见图3.1.
图3.1
添加完ip核后,配置引脚,重新生成工程,然后你就会在Address Editor窗口看到ip核axi gpio在实际内存中的映射地址了,见图3.2.
图3.2
笔者的axi gpio的设备基地址是0x4120_0000,一定要记住这个地址,在编写linux驱动的时候需要知道该地址才能点灯啊!
本章节先不使用设备树,所以读者可以跳过步骤2。 虽然不使用设备树,但我们也简单介绍一下设备树的作用吧,读者可以先简单理解为:“linux内核通过读取设备树来了解它到底能掌控哪些设备,这些设备有什么样的参数”。
刚学习完SOC跑裸机的“童鞋们已经迫不及待了”,终于可以穿上一件衣服了,以后不用再裸奔了。
首先声明,linux驱动需要在Ubuntu下编译的,所以大家先把你们移植linux内核的Ubuntu系统打开吧,然后再切换到移植linux内核的目录,我们先在该目录下建立一个driver_app目录,我知道读者可能有点疑惑,直接上图吧,见图3.3.
图3.3
注意:跟linux内核是在同一级目录的,别弄错了,因为驱动程序的编译需要用到内核的支持,而笔者的Makefile文件指定的路径就是图3.3中的linux-xlnx-master文件夹。。读者如果不按照笔者的顺序来的话,就需要自己改Makefile文件了,想必读者也是一位懒人吧。
我们在./ driver_app/axi_gpio/dev/目录下新建一个驱动文件led_dev.c
驱动程序如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
static volatile unsigned int *led_base; //led寄存器基地址
//打开设备触发的服务函数
static int led_open(struct inode * inode, struct file * filp)
{
printk(KERN_ERR "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(KERN_ERR "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(KERN_ERR "LED: Access address to device is:0x%x\n", (unsigned int)led_base);
//注册设备到/dev/目录
ret = misc_register(&led_dev);
if(ret)
{
printk(KERN_ERR "led:[ERROR] Misc device register failed.\n");
return ret;
}
printk(KERN_ERR "Module init complete!\n");
return 0;
}
//LED设备退出
static void led_exit(void)
{
printk(KERN_ERR "Module exit!\n");
iounmap(led_base); //取消物理地址的映射
misc_deregister(&led_dev); //删除/dev/目录下的设备节点
}
module_init(led_init); //模块初始化接口
module_exit(led_exit); //模块推出接口
//以下代码可解决一些错误信息
MODULE_AUTHOR("Nightmare@EEFOCUS");
MODULE_DESCRIPTION("ledDriver");
MODULE_ALIAS("It's only a test");
MODULE_LICENSE("Dual BSD/GPL");
代码已经贴出来了,我们来了解一下代码的结构:1-11行是相关头文件的定义, 第13行是设备基地址的定义,通过第59行的地址映射,获取设备的虚拟基地址。16-35行是open、read、write函数,这个三个函数是映射到应用层的,当应用层在该设备下使用这三个函数时,其实它会调用底层的这三个函数,也就是16-35行的open、read、write函数。这就是应用层与底层的交互。38-43行是一个函数接口的结构体,它管理着一个设备与底层交互的接口函数。46-51行是一个设备节点,linux内核通过该节点来识别和控制设备。54-72行在加载模块时会自动调用,主要是设备基地址映射和设备结点的注册,就是通知内核,我要添加一个设备。74-79行在卸载模块时调用,主要是取消设备地址映射和撤销设备结点。
读者可以看到,映射的地址不正是我们在添加axi gpio时记录下来的地址吗,然后通过映射地址来配置axi gpio设备里面的寄存器。这个配置过程跟裸机一模一样,笔者就不多说了。哦!还有一点就是:在映射地址时,要选择映射地址的长度,单位是字节。
好了,笔者提供了makefile文件,读者在命令行下直接执行make生成.ko可加载模块。
应用层没啥好说的,直接上代码,makefile文件笔者会打包发出来的,注意:makefile里面的交叉编译器要改成自己现在使用的交叉编译器。
#include
#include
#include
#include
#include
int main(int argc, char **argv)
{
int i;
int fd;
int val=1;
fd = open("/dev/led_dev", O_RDWR); //打开设备
//LED流水灯
while(1)
{
for(i=0;i<4;i++)
{
val = (1<
①重新生成boot.bin文件。
②驱动程序和应用程序在linux系统下编译完成后,把应用程序.o 和驱动程序.ko拉进开发板,使用insmod命令加载.ko模块,然后运行应用程序.o 然后就会看到开发板的led灯跑起来了。