linux驱动之一、LED驱动(驱动代码小结附:github代码链接)


前言:最近在学习韦东山老师的arm驱动部分教学,本文记录一下自己对最基本的驱动框架结构的理解,以及最基本的led驱动实现的方法。本博客以米尔科技的rico board开发板为例,完成rico board的led驱动代码的实现过程。

备注:完整代码直接跳转到《led驱动代码(完整代码,亲测有效)》章节或跳转至《github工程链接》章节,中间各个小结的代码为实现过程(不完整),无法正确编译。


一、相关知识点(涉及接口、结构体、调用关系等)

一个软件系统可以分为以下四层:应用程序、库、内核、驱动,借用韦老师一副图,如下图:
即:应用层开发的功能,各个接口会通过库、通过内核调用到底层驱动程序的对应接口,从而执行对应的功能。
在这里插入图片描述

1.1 裸机开发步骤与驱动开发过程对比

1.1.1 裸机开发步骤

裸机开发时,我们需要按照如下过程进行开发。

步骤:
1、看电路图,查清楚led在哪个管脚,对应哪个GPIO口
2、看芯片手册,查看需要配置的相关寄存器地址——(主要查看需要配置哪些寄存器可以将GPIO配置成输出模式,地址可以在map地址映射章节找到)
3、看芯片手册,查清楚怎么样能将GPIO配置成输出高电平或低电平。
4、编译代码,烧写到板卡中,重新上电

1.1.2 Linux系统下LED驱动开发步骤

备注:裸机开发与带操作系统的驱动开发对寄存器的操作不同,裸机开发直接操作寄存器地址即可,而带linux操作系统时,必须要进行地址映射(有相关接口可以直接使用),映射完成后,再进行操作即可。

步骤:
1、撰写基本字符驱动框架代码(字符驱动代码框架实现步骤详见第2节:基本驱动框架实现流程
2、看电路图,查清楚led在哪个管脚,对应哪个GPIO口
3、看芯片手册,查看需要配置的相关寄存器地址——(主要查看需要配置哪些寄存器可以将GPIO配置成输出模式,地址可以在map地址映射章节找到)
4、在驱动入口函数与出口函数中完成寄存器地址映射与取消映射
5、将led操作功能代码填充到驱动框架中

1.2 预备知识:写驱动时涉及的接口、结构体、宏等

name含义
struct file_operations结构体:该结构体中告诉每一个接口成员指向某个函数指针,指向的函数指针则为要实现的功能
struct class
module_init(lxxx_init);注册驱动入口接口
module_exit(xxx_exit);注册驱动出口接口
MODULE_LICENSE(“Dual BSD/GPL”);声明
xxx_init()驱动入口函数
xxx_exit()驱动出口函数
register_chrdev()注册模块,告诉内核让内核知道有这个模块,并且返回值为自动分配的主设备号
unregister_chrdev()卸载驱动程序,告诉内核
class_create()创建一个类
device_create(major, …);创建一个设备,使用该接口创建设备以后,在板卡中insmod xxx之后,会自动生成/dev/设备,并且会将major主设备号分配给该设备
device_destroy()删除设备,删除设备号
class_destroy删除类
ioremap();映射寄存器地址
iounmap();删除映射关系

二、基本驱动框架

本小结,作为预备知识,先对字符驱动基本框架做一个简要描述,具体从两个方面进行描述,分别是:

  • 2.1 基本驱动框架实现流程(思路)
  • 2.2 基本驱动框架结构

2.1 基本驱动框架实现流程(思路)

撰写驱动框架时,按照以下驱动思路来写即可,后续代码实现,即可对比参照此流程来分析代码。

步骤如下:
1、实现结构体:【xxx_fops】。该结构体内部制定了相关的功能代码接口,从而应用能够知道哪个对应功能在哪个接口中实现。
2、实现步骤1中的相关函数(读写函数)框架,此时不需要实现函数具体细节,函数内部为空即可。
3、驱动入口函数:【xxx_init()】

xxx_init() {
    (a)在函数前面定义全局变量【int major】,下一步用该变量接收系统自动分配的主设备号。
    (b)驱动入口函数中实现注册函数,返回值为系统自动分配的主设备号:major = register_chrdev(0,设备名称,结构体);
}

4、驱动入口修饰函数:【modules_init();】
5、驱动出口函数:【xxx_exit();】

xxx_exit() {
    (a)驱动出口函数中实现卸载函数:【unregister_chrdev(主设备号,设备名称)】
}

6、驱动出口修饰函数:【modules_exit();】
7、给sysfs文件系统提供更多驱动信息,这样上层应用因为【udev机制】便可自动创建设备节点,跟底层驱动代码中自动生成的主设备号保持一致,从而关联起来。(备注:udev机制个mdev机制基本可以认为是同一个东西,mdev是udev的简化版本)

(a)定义结构体class:【static struct class *xxx_class;】
(b)驱动入口函数xxx_init()中增加两个接口:【创建类class_create()】和【创建设备device_create()】
(c)驱动出口函数xxx_exit()中增加两个接口:【删除设备device_destroy()】和【删除类class_destroy()】
(d)注意:步骤b和步骤c中两个接口顺序是相反的!!

8、添加Licence:【MODULE_LICENSE(“Dual BSD/GPL”);】。

2.2 基本驱动框架结构

/* 头文件 */
#include xxxxxxx

/* 功能接口1 */
x1()
{
    ...
}

/* 功能接口2 */
x2()
{
    ...
}

/* 功能接口3 */
x3()
{
    ...
}

/* 重要结构体:指定对应功能接口,该结构体中告诉每一个接口成员指向某个函数,指向的函数则为要实现的功能 */
static struct file_operations xxx_fops {
    .owner    = THIS_MODULE, /* 固定格式 */
    .其他接口1 = x1,          /* 函数指针:指向x1,功能实现接口1 */
    .其他接口2 = x2,          /* 函数指针:指向x2,功能实现接口2 */
    .其他接口3 = x3,          /* 函数指针:指向x3,功能实现接口3 */
    ...
};

/* 驱动入口函数 */
xxx_init()
{
    major = register_chrdev(...);  /* 注册模块,告诉内核让内核知道有这个模块,并且返回值为自动分配的主设备号 */

    class_create(...);             /* 创建一个类 */
    device_create(major, ...);     /* 创建一个设备,使用该接口创建设备以后,在板卡中insmod xxx之后,会自动生成/dev/设备,并且会将major主设备号分配给该设备 */

    ioremap();                     /* 映射寄存器地址 */
}

/* 驱动出口函数 */
xxx_exit()
{
    unregister_chrdev(major, ...);              /* 卸载驱动程序,告诉内核 */

    device_destroy(xxx_class, MKDEV(major, 0)); /* 删除设备,删除设备号 */
    class_destroy(xxx_class);                   /* 删除类 */

    iounmap(); /* 删除映射关系 */
}

module_init(xxx_init);
module_exit(xxx_exit);
MODULE_LICENSE("Dual BSD/GPL");

三、代码实现分析

不管是有系统的驱动代码,还是无系统裸机操作,说到底,最底层始终是对寄存器进行操作,裸机无非是直接操作寄存器,配置好相关寄存器,开始运行程序即可。有linux系统时,区别仅在于,不能直接操作寄存器,需要对寄存器地址进行映射,之后在操作映射后的地址(接口为:remap()和unmap())。

按照第一小节实现思路步骤,与上一章节的基本驱动框架,逐步往下实现

3.1 Led操作驱动框架代码

按照第二小节框架,写出led驱动框架代码如下:
我先计划功能实现如下:使用open、release、write接口实现对GPIO的操作

  • 1、在open函数中实现GPIO寄存器配置为输出功能
  • 2、在release函数中实现GPIO寄存器配置为输出低电平
  • 3、在write函数中实现GPIO寄存器配置为输出高电平
/* 头文件 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>

static int major = (-1); /* 初始化为无效值 */
static struct class *led_drv_class;

/* 功能接口1 */
static int led_drv_open(struct inode *inode, struct file *file)
{
    /* 功能:配置GPIO为输出功能,待填充 */
    return 0;
}

/* 功能接口2 */
static int led_drv_release(struct inode *pinode , struct file *pfile)
{
    /* 功能:配置GPIO为低电平,待填充 */
    return 0;
}

/* 功能接口3 */
static ssize_t led_drv_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
   /* 功能:配置GPIO为高电平,待填充 */
    return 0;
}

/* 结构体,对应成员分别指向各个功能接口 */
static struct file_operations led_drv_fops = {
    .owner   = THIS_MODULE,
    .open    = led_drv_open,     /* 函数指针:指向x1,功能实现接口1 */
    .release = led_drv_release,  /* 函数指针:指向x2,功能实现接口2 */
    .write   = led_drv_write,    /* 函数指针:指向x3,功能实现接口3 */
};

/* 驱动入口函数 */
static int led_drv_init(void)
{
    major = register_chrdev(0, "led_drv", &led_drv_fops); // 注册, 告诉内核,返回值major为自动分配的主设备号

    led_drv_class = class_create(THIS_MODULE, "leddrv");
    device_create(led_drv_class, NULL, MKDEV(major, 0), NULL, DEV_NAME); /* insmod xxx后,会自动生成/dev/xyz设备,并且自动给该设备分配主设备号major */

    /* 映射寄存器地址,待填充 */
    return 0;
}

/* 驱动出口函数 */
static void led_drv_exit(void)
{
    unregister_chrdev(major, "led_drv");               /* 卸载驱动程序,告诉内核 */

    device_destroy(led_drv_class, MKDEV(major, 0));
    class_destroy(led_drv_class);

    /* 删除映射关系,待填充 */
}

module_init(led_drv_init);
module_exit(led_drv_exit);

MODULE_LICENSE("Dual BSD/GPL");

3.2 看电路图,找到led分别对应哪个GPIO口

打开电路图,我们看到rico board开发板led分别为D23/D24/D25/D26分别对应GPIO1_24/GPIO1_25/GPIO1_26/GPIO1_27
在这里插入图片描述

在这里插入图片描述
对应关系如下表格:

led名字对应GPIO
D23GPIO1_24
D24GPIO1_25
D25GPIO1_26
D26GPIO1_27

3.3 查看芯片手册,查看需要配置那些寄存器,以及各个寄存器地址

看芯片手册第28.3.4.3章节,如下图:
在这里插入图片描述
大致意思是:当GPIO作为输出功能,我们需要配置以下三个寄存器从而让对应GPIO口输出高低电平:

寄存器名称对应功能
GPIO_OE配置GPIO口功能
GPIO_CLRDATAOUTGPIO输出低电平寄存器
GPIO_SETDATAOUTGPIO输出高电平寄存器

继续往下看手册,跳转到三个对应寄存器的描述:

3.3.1 GPIO_OE

在这里插入图片描述
可以看到两个信息:
1、该寄存器偏移地址为:0x134
2、含义:OE寄存器为使能GPIO输出功能,当复位以后,GPIO输入输出功能不可用,再看表格中的Description可知:当该寄存器写入0时,表示GPIO为输出功能,故我们需要将GPIO_OE的24/25/26/27bit配置为0。

3.3.2 GPIO_CLRDATAOUT

1、该寄存器偏移地址为:0x190
2、寄存器对应位写入0时不影响原功能,写入1时清除GPIO口输出寄存器(GPIO_DATAOUT),即GPIO口会输出低电平
在这里插入图片描述

3.3.3 GPIO_SETDATAOUT

1、该寄存器偏移地址为:0x194
2、寄存器对应位写入0时不影响原功能,写入1时置位GPIO口输出寄存器(GPIO_DATAOUT),即GPIO口会输出高电平

在这里插入图片描述

3.3.4 GPIO1基地址

在文档中搜索GPIO1,Memory Map章节中搜到GPIO1基地址为0x4804C000
在这里插入图片描述

3.3.5 宏定义寄存器地址

根据以上资料,为了代码中操作方便,使用宏定义寄存器地址。

/* 寄存器地址宏定义 */
#define GPIO1_BASE_ADDR           0x4804c000
#define GPIO1_OE_ADDR             ((GPIO1_BASE_ADDR) + (0x134))
#define GPIO1_DATA_OUT_ADDR       ((GPIO1_BASE_ADDR) + (0x13C))
#define GPIO1_CLEAR_DATA_OUT_ADDR ((GPIO1_BASE_ADDR) + (0x190))
#define GPIO1_SET_DATA_OUT_ADDR   ((GPIO1_BASE_ADDR) + (0x194))

3.4 完成寄存器地址映射与删除寄存器地址映射

volatile unsigned long *gpio1_oe = NULL;
volatile unsigned long *gpio1_dataout = NULL;
volatile unsigned long *gpio1_clrdata = NULL;
volatile unsigned long *gpio1_setdata = NULL;

/* 映射寄存器地址 */
gpio1_oe      = (volatile unsigned long *)ioremap(GPIO1_OE_ADDR, 32);
gpio1_dataout = (volatile unsigned long *)ioremap(GPIO1_DATA_OUT_ADDR, 32);
gpio1_clrdata = (volatile unsigned long *)ioremap(GPIO1_CLEAR_DATA_OUT_ADDR, 32);
gpio1_setdata = (volatile unsigned long *)ioremap(GPIO1_SET_DATA_OUT_ADDR, 32);

/* 删除映射关系 */
iounmap(gpio1_setdata);
iounmap(gpio1_clrdata);
iounmap(gpio1_dataout);
iounmap(gpio1_oe);

3.5 完成led操作功能代码

由于功能简单,此处不再赘述,直接看最后一章节完整代码即可,各个功能都有注释。

3.6 将上述步骤中各个功能代码填充到驱动框架中

可以将上述所有功能代码全部封装成接口,因为功能比较简单,这里我没有做封装,直接将代码填充到驱动框架中,完整代码如下一章节。

四、led驱动代码(完整代码,亲测有效)

备注:附github工程连接:https://github.com/Warrior-Asa/led_drv

4.1 led_drv.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>

#define DEV_NAME "led"

/* led */
#define GPIO1_BASE_ADDR           0x4804c000
#define GPIO1_OE_ADDR             ((GPIO1_BASE_ADDR) + (0x134))
#define GPIO1_DATA_OUT_ADDR       ((GPIO1_BASE_ADDR) + (0x13C))
#define GPIO1_CLEAR_DATA_OUT_ADDR ((GPIO1_BASE_ADDR) + (0x190))
#define GPIO1_SET_DATA_OUT_ADDR   ((GPIO1_BASE_ADDR) + (0x194))

volatile unsigned long *gpio1_oe = NULL;
volatile unsigned long *gpio1_dataout = NULL;
volatile unsigned long *gpio1_clrdata = NULL;
volatile unsigned long *gpio1_setdata = NULL;
/* led end */

static int major = (-1); /* 初始化为无效值 */
static struct class *led_drv_class;

static int led_drv_open(struct inode *inode, struct file *file)
{
    /* 将oe寄存器bit25、26、27置0:表示output功能 */
    *gpio1_oe &= ((~(1<<25)) | (~(1<<26)) | (~(1<<27)));

    return 0;
}

static int led_drv_release(struct inode *pinode , struct file *pfile)
{
    /* 将setdata寄存器bit25、26、27置1,表示输出高电平 */
    *gpio1_setdata |= ((1<<25) | (1<<26) | (1<<27));

    return 0;
}

static ssize_t led_drv_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    /* 将cleardata寄存器bit25、26、27置1,表示清除输出,则输出低电平 */
    *gpio1_clrdata |= ((1<<25) | (1<<26) | (1<<27));

    return 0;
}

static struct file_operations led_drv_fops = {
    .owner   = THIS_MODULE,
    .open    = led_drv_open,
    .release = led_drv_release,
    .write   = led_drv_write,
};

/* 驱动入口函数 */
static int led_drv_init(void)
{
    major = register_chrdev(0, "led_drv", &led_drv_fops); // 注册, 告诉内核,返回值major为自动分配的主设备号

    led_drv_class = class_create(THIS_MODULE, "leddrv");
    device_create(led_drv_class, NULL, MKDEV(major, 0), NULL, DEV_NAME); /* insmod xxx后,会自动生成/dev/xyz设备,并且自动给该设备分配主设备号major */

    /* 映射寄存器地址 */
    gpio1_oe      = (volatile unsigned long *)ioremap(GPIO1_OE_ADDR, 32);
    gpio1_dataout = (volatile unsigned long *)ioremap(GPIO1_DATA_OUT_ADDR, 32);
    gpio1_clrdata = (volatile unsigned long *)ioremap(GPIO1_CLEAR_DATA_OUT_ADDR, 32);
    gpio1_setdata = (volatile unsigned long *)ioremap(GPIO1_SET_DATA_OUT_ADDR, 32);

    return 0;
}

/* 驱动出口函数 */
static void led_drv_exit(void)
{
    unregister_chrdev(major, "led_drv"); /* 卸载驱动程序,告诉内核 */

    device_destroy(led_drv_class, MKDEV(major, 0));
    class_destroy(led_drv_class);

    /* 删除映射关系 */
    iounmap(gpio1_setdata);
    iounmap(gpio1_clrdata);
    iounmap(gpio1_dataout);
    iounmap(gpio1_oe);
}

module_init(led_drv_init);
module_exit(led_drv_exit);

MODULE_LICENSE("Dual BSD/GPL");

4.2 Makefile

KERN_DIR = ~/mier/Kernel/linux-3.12.10-ti2013.12.01

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

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order

obj-m	+= led_drv.o

4.3 编译生成.ko文件(make)

至此代码写完,执行make命令,会生成.ko文件
在这里插入图片描述

五、加载/卸载模块等常用操作(开发板命令行常用操作)

将上步骤生成【led_drv.ko】文件拷贝到开发板中。

5.1 加载驱动到内核(insmod xxx.ko)

在开发板中执行命令insmod led_drv.ko,使用lsmod命令查看当前已加载的模块,可以看到led_drv模块已经加载到内核中
在这里插入图片描述

5.2 cat /proc/devices查看设备

使用命令cat /proc/devices可以查看该模块分配的设备号为243,左边一列数字为主设备号,右边字符串为设备名称
在这里插入图片描述

5.3 命令rmmod xxx

使用命令rm led_drv可以卸载该模块
在这里插入图片描述

六、led测试程序【led_app.c】

6.1 led_app.c

现在回到linux开发环境中,写一套led驱动测试app代码,代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define DEV_NAME "/dev/led"

int main(int argc, char const *argv[])
{
    int fd = 0;
    int ret = 0;
    int led_status = 0;

    fd = open(DEV_NAME,O_RDWR); /* 根据设备名称打开设备 */
    if (fd<0) {
        printf("You are wrong!!!\n");
        exit(0);
    }

    ret = write(fd, &led_status, 1); /* 向设备中写入0,灯亮 */
    printf("Set led GPIO to %d\n",ret);

    sleep(5);
    close(fd);

    return 0;
} 

6.2 编译,生成可执行文件

使用交叉编译工具链进行编译,生成可执行文件led_app
在这里插入图片描述
将该可执行文件拷贝到开发板中。
在这里插入图片描述

七、测试效果

7.1 先将之前加载进内核的led_drv模块卸载掉,执行./led_app

如下图,因为内核中没有自己写的led_drv驱动模块,所以应用程序打开/dev/led时打开失败,提示you are wrong!
在这里插入图片描述

7.2 加载模块,再执行应用程序./led_app

先将自己写的驱动模块加载到内核中,在执行测试程序./led_app,可以看到应用正常执行,设备正常打开,观察开发板,led亮5秒之后熄灭,测试成功
在这里插入图片描述


八、github工程连接

github工程连接:https://github.com/Warrior-Asa/led_drv

  • 7
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值