0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

linux驱动编写:从hello world到 LED驱动

454398 来源:AI加速 作者:AI加速 2020-11-29 10:28 次阅读

linux驱动是连接软件和硬件的一个中间介质,实现了对硬件的配置和控制。进一步将硬件抽象化,为软件操作硬件提供了简单的接口。不论硬件的具体形式如何,linux驱动都将其映射到一个文件,软件端对硬件的读写操作等都被抽象成文件操作了。本篇从hello world开始,简要介绍驱动的基本结构,然后再进一步介绍LED硬件的搭建,以及驱动的编写,设备树的修改。让大家对linux驱动有一个基本的认识。

1. Hello world驱动

hello world几乎成了所有编程书的第一个程序,用来介绍程序的大体结构。一个简单的hello world让人感觉这本书学起来真的很容易,不知不觉就进入圈套。被诱导入坑的我,也来用一个hello world诱别人入坑。先上程序:

#include
#include
MODULE_LICENSE("GPL v2");

static int hello_init(void){

printk(KERN_INFO "Hello world/n");
return 0;
}

static void hello_exit(void){
printk(KERN_INFO "Goodbye, cruel world/n");
}

module_init(hello_init);
module_exit(hello_exit);

一个驱动的使用过程包括:模块的装载,软件调用,模块卸载。程序中module_init和module_exit是内核中的宏,是一个驱动必须包含部分。当驱动被装载时,就会调用module_init指定的初始化函数hello_init,而当驱动被卸载时,就会调用hello_exit函数。初始化函数通常都是进行设备树检查,内存分配映射,硬件配置,文件和硬件关联等操作。清除函数用于释放内存,硬件的清零等操作。通常驱动还会定义一些文件IO操作,比如write,read,ioctrl等。Hello world只给出一个驱动编写格式和流程,文件操作在LED驱动中再介绍。MODULE_LISENCE用于告诉内核该模块采用的许可证类型,这个一般和linux内核采用

的许可证一致就好了。

现在来看驱动模块是如何编译的,看Makefile:

obj-m:=hello.o
ARCH=arm
CROSS_COMPILE=arm-xilinx-linux-gnueabi-
CC:=$(CROSS_COMPILE)gcc
LD:=$(CROSS_COMPILE)ld
KERNELDIR:=/home/anpingbo/Design/linux/linux-xlnx
PWD:=$(shell pwd)
all:
make -C $(KERNELDIR) M=$(PWD) modules

如果我们要构造的模块名称为hello.ko,那么就需要指定obj-m为hello.o,这个是hello.ko生成的依赖选项。是在zynq平台上编译模块,需要制定ARCH类型为arm,以及交叉编译工具,如果使用本机linux系统默认gcc就不能在zynq平台下加载模块,这里的交叉工具链为arm-xilinx-linux-gnueabi-gcc,arm-xilinx-linux-gnueabi-ld是交叉连接器,把程序链接成可以在arm平台运行的模块。同时还需要制定内核系统文件夹,因为模块编译要用到内核的库。All下就是编译模块了。设置好交叉工具的环境变量后,直接执行make,机会生成hello.ko,这个就是编译好的驱动模块。

图1.1 编译hello驱动过程

我们打开zynq系统,加载hello.ko,加载使用insmod命令,卸载使用rmmod命令。我们发现没有打印出任何信息,这是不是模块编译有错误,其实模块没有错误,流程也都对。因为printk打印函数是有级别的,只有低于这个级别值才能打印到terminal中。我们可以修改内核级别,比如我们将级别降低:

echo 8 > /proc/sys/kernel/printk

我们还可以使用dmesg来查看驱动打印的日志。其会打印所有驱动加载和卸载的打印日志。

图1.2 insmod hello.ko

2. LED驱动

2.1 vivado工程

我们通过axi_gpio来连接4个LED灯,通过linux驱动来点亮LED灯。Block如图2.1,我们设置gpio宽度为4。还有一个我们需要用到的就是LED映射到的内存,可以在address editor中看到,gpio0是LED的。接下来就是一般的流程,管脚约束,综合,编译。然后导出工程,打开SDK,新建fsbl,编译,生成设备树,制作boot.bin。



图2.1 LED硬件工程

2.2 LED驱动

LED属于字符类驱动,符合字符类驱动的写法。Linux内核中每个模块实际上是以文件形式存在的,这些文件存放于/dev目录下。而不同的驱动模块具有不同的主设备号和次设备号。主设备号用于区别不同的驱动,而次设备号用于更具体的指向驱动指向的设备。设备号就相当于门牌号,用于唯一区别不同驱动。编写驱动过程中就需要为驱动分配设备号。现在进一步分析LED驱动代码:

#define LED_DATA 0x41200000
#define LED_CTRL 0x41200004

这两行定义了LED数据控制寄存器和数据读写寄存器的内存地址。实际上驱动对LED硬件的配置和读写都是通过配置其寄存器实现的。具体要看GPIO的硬件信息。

dev_t led_devt;
void __iomem *baseaddr;

dev_t定义了设备编号,__iomem定义了linux内核的存储指针。硬件的内存需要映射到linux内核空间才能操作。在使用和配置LED时,需要先将其物理内存映射到linux内核的内存空间。映射函数ioremap就是专门用于IO端口内存映射的。

static int led_major=25;
struct cdev *led_dev;
int led_value;

上述定义了一个led主设备号。通过ls –l命令可以查看/dev下驱动的设备号。当然这是一种不方便的方式,正常情况下设备号也可以自动分配。Cdev是字符设备的结构体,定义如下:
struct cdev {
struct kobject kobj; // 内嵌的kobject对象
struct module *owner; // 所属模块
const struct file_operations *ops; // 文件操作结构体
struct list_head list; //linux内核所维护的链表指针
dev_t dev; //设备号
unsigned int count; //设备数目
};

文件操作结构体实际上提供了软件操作LED的接口,上文讲过驱动都被映射成/dev下的一个文件,软件调用驱动的时候,以打开,读写,关闭对应设备文件来进行操控。我们看一下LED驱动中的文件结构:

struct file_operations led_fops={
.owner=THIS_MODULE,
.read=led_read,
.write=led_write,
.unlocked_ioctl=led_ioctl,
.open=led_open,
.release=led_release,
};

其中包含了文件应该有的四种基本操作open, read, write, release实际上是close。文件结构体还提供了ioctrl函数,这个函数为软件提供了一种更为灵活的操纵底层硬件的方法。

接下来对文件结构体中的每个函数进行分析。

1) led_open
static int led_open(struct inode *inode, struct file *filp){
struct resource *res;
int reg;
printk("begin: open led/n");
filp->private_data=inode->i_cdev;
res=request_mem_region(LED_DATA, 0x10000, "LED");
if(!res){
printk("failed requesting resource/n");
return 0;
}

printk("begin: remap led/n");
baseaddr=ioremap(LED_DATA, 0x10000);
if(!baseaddr){
printk("ERROR: couldn't allocate baseaddr/n");
return 0;
}
printk("baseaddr is %x/n", baseaddr);
printk("begin: read led/n");
reg=ioread32(baseaddr);
printk("begin: write led %d/n", reg);
reg &= 0xFFFFFFF0;
iowrite32(reg, baseaddr+4);
printk("SUCCESS: gpio init/n");
return 0;
}

内核用inode结构在内部表示文件,inode结构中包含了大量有关文件的信息。当我们在linux中创建一个文件时,就会在相应的文件系统创建一个inode与之对应。文件实体和文件的inode是一一对应的。当打开文件时,就获得了inode。通过inode可以获得字符设备结构体i_cdev。Inode在驱动开发中很少进行填充,通常都是用于查看。在使用LED时,需要为其分配内存,首先通过函数request_mem_region来看是否有空闲linux内核内存可分配,如果可以就通过ioremap进行分配,返回linux内存首地址。之后读写LED的时候就可以通过向这个地址写数据来控制LED了。iowrite32(reg, baseaddr+4)是在配置LED,使能了LED。

2) led_write

因为点亮LED是输出数据,所以实际上用不上读操作,只有写操作。

ssize_t led_write(struct file * filp, const char __user *buf, size_t cnt, loff_t *f_ops){
if(copy_from_user(&led_value, buf, cnt))
return -EFAULT;

led_gpio_set();
return 1;
}
void led_gpio_set(void){
iowrite32(led_value, baseaddr);

}

Copy_from_user是linux内核从用户空间获得要写入LED的数据。Led_gpio_set函数中通过iowrite函数将用户空间的数据写入LED。

3) led_release

static int led_release(struct inode *inode, struct file *filp){
iounmap(baseaddr);
release_mem_region(LED_DATA, 0x10000);
return 0;
}

这个是在调用close函数的时候会调用这个函数来释放内存,解除LED内存映射。

现在再来看初始化和驱动清除函数:
static void led_setup_dev(int index){
int err;
int devno;
devno=MKDEV(led_major, index);
printk("MKDEV devno %d/n", devno);
cdev_init(led_dev, &led_fops);
printk("cdev_init %d/n", devno);
led_dev->owner=THIS_MODULE;
led_dev->ops=&led_fops;
err=cdev_add(led_dev, devno, 1);
if(err)
printk(KERN_ERR "ERROR: %d adding LED%d", err, index);
printk("SUCCESS: add dev %d/n", devno);
}

static int __init led_init(void){
int result;
printk("INIT:------------/n");
result=alloc_chrdev_region(&led_devt, 0, 1, "LED");
led_major=MAJOR(led_devt);

if(result printk(KERN_ERR "ERROR: allocate chrdev %d", led_devt);
return result;
}
printk("SUCCESS: allocate chrdev %d/n", led_devt);

led_dev=cdev_alloc();
if(!led_dev){
printk(KERN_ERR "ERROR: allocate device mem %d", led_devt);
result=-ENOMEM;
goto fail_malloc;
}
printk("SUCCESS: allocate device mem %d/n", led_devt);

led_setup_dev(0);
printk("SUCCESS: init device %d/n", led_devt);
return 0;

fail_malloc:
unregister_chrdev_region(led_devt, 1);
return result;
}

static void __exit led_cleanup(void){
cdev_del(led_dev);
unregister_chrdev_region(led_devt, 1);
printk("SUCCESS: exit device %d/n", led_devt);
}

LED驱动初始化操作首先要为led_devt动态分配设备号,然后可以通过MAJOR来得到主设备号。这个主设备号在将字符设备添加到驱动中会用到。获得了设备号后就对led字符设备结构体led_dev分配空间,然后调用函数led_setup_sev完成对LED设备的初始化,添加等。这些就是一般过程了。作为初学者,先会用,以后在驱动的调试中会深入去理解。

清除函数就是释放LED字符设备空间。

2.3 LED软件操作

现在来看如何在用户端操作LED,上代码:

#include
#include
#include
#include

int main(int argc, char *argv[]){
int fd;
fd=open("/dev/LED", O_RDWR);
if(fd printf("ERROR: cannot open/n");
return 0;
}

int value=0xF;
write(fd, &value, 4);
//close(fd);
return 0;
}

Open函数打开LED字符设备,然后通过write函数来点亮LED。

2.4 实验

首先加载led.ko:

图2.2 加载led驱动

ls查看/dev下发现并没有LED文件,还需要通过mknod命令为其添加设备节点,我们才能在用户空间进行操作。但是如何知道主设备号呢?cat /proc/devices可以看到LED的主设备号为245。完成LED设备节点分配后,就看到/dev下有个文件LED,这时候就可以通过在用户空间进行写操作了。

图2.3 查看LED的主设备号

图2.4 分配设备节点

总结

本篇通过两个简单的驱动程序,介绍了驱动的基本结构,编写方法和编译过程。总结一下就是:通过init来进行设备的基本配置,和硬件配置,然后定义文件操作结构体,以及结构体中函数,通过open,read,write,release等函数来控制对LED的读写操作。

编辑:hfy


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • led
    led
    +关注

    关注

    237

    文章

    22445

    浏览量

    645848
  • Linux驱动
    +关注

    关注

    0

    文章

    43

    浏览量

    9881
收藏 人收藏

    评论

    相关推荐

    一文解析蜂鸣器驱动LED驱动编写

    编写LED驱动,首先的知道开发板的构造;开发板分为核心板与底板;编写驱动的第一步就是要看开发板,找到L
    的头像 发表于 09-23 15:30 4250次阅读
    一文解析蜂鸣器<b class='flag-5'>驱动</b>及<b class='flag-5'>LED</b><b class='flag-5'>驱动</b><b class='flag-5'>编写</b>

    Linux系统中驱动格式基本编写方法

    今天主要和大家聊一聊,编写Linux驱动格式与方法。
    发表于 12-02 09:34 451次阅读

    Linux模块相关命令 Linux驱动模块的编写与挂载

    Linux模块相关命令 Linux驱动模块的编写与挂载
    发表于 10-01 12:20 196次阅读
    <b class='flag-5'>Linux</b>模块相关命令 <b class='flag-5'>Linux</b><b class='flag-5'>驱动</b>模块的<b class='flag-5'>编写</b>与挂载

    【OK210试用体验】linux字符驱动框架

    app.ko打印出:“Hello World linux-driver-module”查看驱动信息:lsmod app/app.koapp 583 0 - Live 0xbf0180
    发表于 10-13 17:03

    【BPI-M64试用体验】linuxHELLO驱动编写

    linux下,对A53进行了简单的HELLO驱动编写!如下图:
    发表于 06-09 15:56

    Arduino Hello World实验

    的Arduino 听从你的指令了,我们再借用一下Arduino 自带的数字13 口LED,让Arduino 接受到指令时LED 闪烁一下,再显示“Hello World!”下面给大家一
    发表于 08-06 09:06

    Linux嵌入式驱动开发

    全部传送门Linux嵌入式驱动开发01——第一个驱动Hello World(附源码)Linux
    发表于 12-17 06:22

    第9章 Linux驱动程序设计

    9.1 Linux 设备驱动程序 9.2 Linux经典Hello world驱动程序 9.
    发表于 04-11 14:56 3次下载

    Qt图形编程基础之使用Qt编写HelloWorld”程序实验

    分享到:标签:Qt图形编程 Linux 操作系统 12.3 实验内容使用Qt编写HelloWorld程序 1.实验目的 通过编写一个跳动的
    发表于 10-18 14:44 1次下载
    Qt图形编程基础之使用Qt<b class='flag-5'>编写</b>“<b class='flag-5'>Hello</b>,<b class='flag-5'>World</b>”程序实验

    如何编写Linux 下Nand Flash驱动

    如何编写Linux 下Nand Flash驱动
    发表于 10-30 08:36 15次下载
    如何<b class='flag-5'>编写</b><b class='flag-5'>Linux</b> 下Nand Flash<b class='flag-5'>驱动</b>

    Linux系统网络驱动程序的编写

    驱动程序编写 一.Linux系统设备驱动程序概述 1.1 Linux设备驱动程序分类 1.2
    发表于 11-07 10:40 0次下载

    Linux嵌入式驱动开发01——第一个驱动Hello World(附源码)

    文章目录驱动介绍Hello World1. 包含头文件编译第一种方法第二种方法之前也算是一直在学习嵌入式Linux的开发,裸机开发,uboot配置,系统编译,
    发表于 11-03 14:51 12次下载
    <b class='flag-5'>Linux</b>嵌入式<b class='flag-5'>驱动</b>开发01——第一个<b class='flag-5'>驱动</b><b class='flag-5'>Hello</b> <b class='flag-5'>World</b>(附源码)

    HELLO WORLD!

    HELLO WORLD
    发表于 12-03 16:21 8次下载
    <b class='flag-5'>HELLO</b> <b class='flag-5'>WORLD</b>!

    如何编写第一个hello world程序

    本文简单介绍如何编写第一个hello world程序,以及程序是如何被执行的
    的头像 发表于 03-02 17:31 7888次阅读
    如何<b class='flag-5'>编写</b>第一个<b class='flag-5'>hello</b> <b class='flag-5'>world</b>程序

    c语言hello world程序编写

    C语言"Hello world"程序的编写过程,并提供一些实用技巧和注意事项。 首先,我们需要一个C语言开发环境来编写和运行代码。在这里,我们可以选择一款集成开发环境(IDE)或者一个
    的头像 发表于 11-26 09:23 1324次阅读