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

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

3天内不再提示

如何驱动Linux开发板LED灯

CHANBAEK 来源:嵌入式攻城狮 作者:安迪西 2023-04-14 11:41 次阅读

Linux下的任何外设驱动,最终都是要配置相应的硬件寄存器。 前面的文章中介绍了新旧字符设备的驱动开发框架,也介绍了IMX6ULL处理器GPIO的工作原理及配置方法,本篇我们将实际操作一个GPIO,点亮Linux驱动开发路上的第一个灯

1. 地址映射

1.1 MMU介绍

MMU (Memory Manage Unit),即内存管理单元,它提供统一的内存空间抽象,程序访问虚拟内存中的地址,MMU将虚拟地址翻译成实际的物理地址,之后CPU即可操作实际的物理地址

MMU具有如下功能:

  • 保护内存:MMU给一些指定的内存块设置了读、写以及可执行的权限,这些权限存储在页表当中,MMU会检查CPU当前所处的是特权模式还是用户模式,只有权限匹配才可以访问
  • 提供方便统一的内存空间抽象,实现虚拟地址到物理地址的转换:CPU可以运行在虚拟的内存当中,虚拟内存一般比物理内存大很多,使得CPU可以运行比较大的应用程序

图片

1.2 IO映射函数

Linux内核启动时会初始化MMU,设置内存映射,之后CPU访问的都是虚拟地址。 在程序编写时,可使用下面两个函数进行物理内存和虚拟内存之间的转换

ioremap():将物理地址映射为虚拟地址

#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE) 
void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype){ 
    return arch_ioremap_caller(phys_addr, size, mtype, __builtin_return_address(0)); 
} 
//phys_addr:被映射的IO起始地址(物理地址)
//size:需要映射的空间大小,以字节为单位
//mtype: ioremap的类型
//return: __iomem类型的指针,指向映射成功后返回的虚拟空间起始地址

ioremap():将物理地址映射为虚拟地址

void iounmap (volatile void __iomem *addr) 
//addr: 要取消映射的虚拟地址空间首地址
//return: void

1.3 IO内存访问函数

使用ioremap函数将寄存器的物理地址映射到虚拟地址以后,理论上就可以直接通过指针访问这些地址了,但是为了符合驱动的跨平台以及可移植性,推荐使用一组操作函数来对映射后的内存进行读写操作

u8  readb(const volatile void __iomem *addr);    /*读取一个字节*/
u16 readw(const volatile void __iomem *addr);    /*读取一个字*/
u32 readl(const volatile void __iomem *addr);    /*读取一个双字*/
    
void writeb(u8 value,  volatile void __iomem *addr);  /*写入一个字节*/
void writew(u16 value, volatile void __iomem *addr);  /*写入一个字*/
void writel(u32 value, volatile void __iomem *addr);  /*写入一个双字*/

2. 程序编写

本实验目的:编写Linux下的LED灯驱动,通过应用程序对I.MX6U开发板上的LED灯(GPIO1_IO03)进行开关操作

2.1 驱动程序编写

LED驱动属于字符设备驱动,之前介绍了新旧两种字符驱动的写法,本篇中按照新字符设备驱动的框架来编写

图片

接下来分步骤完善具体的驱动代码:

GPIO寄存器宏定义、设备结构体定义

#define NEWCHRLED_CNT  1             //设备号个数
#define NEWCHRLED_NAME "newchrled"   //名字
#define LEDOFF    0                  //关灯
#define LEDON     1                  //开灯 
/* 寄存器物理地址 */
#define CCM_CCGR1_BASE            (0X020C406C) 
#define SW_MUX_GPIO1_IO03_BASE    (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE    (0X020E02F4)
#define GPIO1_DR_BASE             (0X0209C000)
#define GPIO1_GDIR_BASE           (0X0209C004)
/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
/* newchrled设备结构体 */
struct newchrled_dev{
    dev_t devid;               //设备号
    struct cdev cdev;          //cdev
    struct class *class;       //类  
    struct device *device;     //设备
    int major;                 //主设备号
    int minor;                 //次设备号
};

struct newchrled_dev newchrled; //led设备

控制LED亮灭函数

void led_switch(u8 sta){
    u32 val = 0;
    if(sta == LEDON) {
        val = readl(GPIO1_DR);
        val &= ~(1 << 3); 
        writel(val, GPIO1_DR);
    }else if(sta == LEDOFF) {
        val = readl(GPIO1_DR);
        val|= (1 << 3); 
        writel(val, GPIO1_DR);
    } 
}

设备操作函数集合

/* 打开设备 */
static int led_open(struct inode *inode, struct file *filp){
    filp->private_data = &newchrled;  //设置私有数据
    return 0;
}
/* 从设备读取数据 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){
    return 0;
}
/* 向设备写数据 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt){
    int retvalue;
    unsigned char databuf[1];
    unsigned char ledstat;

    retvalue = copy_from_user(databuf, buf, cnt);
    if(retvalue < 0) {
        printk("kernel write failed!\\r\\n");
        return -EFAULT;
    }

    ledstat = databuf[0];  //获取状态值

    if(ledstat == LEDON) { 
        led_switch(LEDON);  //打开LED灯
    } else if(ledstat == LEDOFF) {
        led_switch(LEDOFF);  //关闭LED灯
    }
    return 0;
}
/* 关闭设备 */
static int led_release(struct inode *inode, struct file *filp){
    return 0;
}
/* 设备操作函数 */
static struct file_operations newchrled_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release =  led_release,
};

在驱动入口函数中:初始化GPIO外设

/* 驱动入口函数 */
static int __init led_init(void){
    u32 val = 0;
    /* 初始化LED */
    /* 1、寄存器地址映射 */
    IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
    SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
    SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
    GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
    GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
    /* 2、使能GPIO1时钟 */
    val = readl(IMX6U_CCM_CCGR1);
    val &= ~(3 << 26);     //之前的设置 
    val |= (3 << 26);      //设置新值
    writel(val, IMX6U_CCM_CCGR1);
    /* 3、设置复用功能,并设置IO属性 */
    writel(5, SW_MUX_GPIO1_IO03);
    writel(0x10B0, SW_PAD_GPIO1_IO03);
    /* 4、设置GPIO1_IO03为输出功能 */
    val = readl(GPIO1_GDIR);
    val &= ~(1 << 3);     //清除之前的设置
    val |= (1 << 3);      //设置为输出
    writel(val, GPIO1_GDIR);
    /* 5、默认关闭LED */
    val = readl(GPIO1_DR);
    val |= (1 << 3); 
    writel(val, GPIO1_DR);

在驱动入口函数中:注册字符设备

/* 注册字符设备驱动 */
    /* 1、创建设备号 */
    if (newchrled.major) {      //定义了设备号
        newchrled.devid = MKDEV(newchrled.major, 0);
        register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
    } else {                    //没有定义设备号
        alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME); 
        newchrled.major = MAJOR(newchrled.devid);
        newchrled.minor = MINOR(newchrled.devid);
    }
    printk("newcheled major=%d,minor=%d\\r\\n",newchrled.major, newchrled.minor);  
    /* 2、初始化cdev */
    newchrled.cdev.owner = THIS_MODULE;
    cdev_init(&newchrled.cdev, &newchrled_fops); 
    /* 3、添加一个cdev */
    cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);
    /* 4、创建类 */
    newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
    if (IS_ERR(newchrled.class)) {
        return PTR_ERR(newchrled.class);
    }
    /* 5、创建设备 */
    newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);
    if (IS_ERR(newchrled.device)) {
        return PTR_ERR(newchrled.device);
    }
 
    return 0;
}

在驱动出口函数中:取消地址映射,注销字符设备驱动

/* 驱动出口函数 */
static void __exit led_exit(void){
    /* 取消映射 */
    iounmap(IMX6U_CCM_CCGR1);
    iounmap(SW_MUX_GPIO1_IO03);
    iounmap(SW_PAD_GPIO1_IO03);
    iounmap(GPIO1_DR);
    iounmap(GPIO1_GDIR);
    /* 注销字符设备驱动 */
    cdev_del(&newchrled.cdev);
    unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);
    device_destroy(newchrled.class, newchrled.devid);
    class_destroy(newchrled.class);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

2.2 应用程序编写

LED驱动加载成功以自动创建设备节点,应用程序通过向节点文件写0或1,关闭/打开LED灯

#define LEDOFF  0
#define LEDON  1

int main(int argc, char *argv[]){
    int fd, retvalue;
    char *filename;
    unsigned char databuf[1];
 
    if(argc != 3){
        printf("Error Usage!\\r\\n");
        return -1;
    }

    filename = argv[1];

    /* 打开led驱动 */
    fd = open(filename, O_RDWR);
    if(fd < 0){
        printf("file %s open failed!\\r\\n", argv[1]);
        return -1;
    }

    databuf[0] = atoi(argv[2]); //要执行的操作:打开或关闭
    /* 向/dev/led文件写入数据 */
    retvalue = write(fd, databuf, sizeof(databuf));
    if(retvalue < 0){
        printf("LED Control Failed!\\r\\n");
        close(fd);
        return -1;
    }

    retvalue = close(fd); 
    if(retvalue < 0){
        printf("file %s close failed!\\r\\n", argv[1]);
        return -1;
    }
    return 0;
}

3. 编译测试

3.1 程序编译

驱动程序编译:创建Makefile文件,使用make命令,编译驱动程序

KERNELDIR := /home/andyxi/linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga_andyxi
CURRENT_PATH := $(shell pwd)
obj-m := newchrled.o

build: kernel_modules

kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

应用程序编译:无需内核参与,直接编译即可

arm-linux-gnueabihf-gcc newchrledApp.c -o newchrledApp

3.2 运行测试

为了方便,选择通过TFTP从网络启动,并使用NFS挂载网络根文件系统。 确保开发板能正常启动,在Ubuntu中将驱动和测试文件复制到modules/4.1.15目录中

加载驱动模块

depmod                 #第一次加载驱动的时候需要运行此命令
modprobe newchrled.ko  #加载驱动

驱动加载成功后会自动在/dev目录下创建设备节点文件/dev/newchrdev,输入如下命令查看

ls /dev/newchrled -l

之后就可使用应用程序来测试驱动是否能正常工作

./newchrledApp /dev/newchrled 1    #打开LED灯
./newchrledApp /dev/newchrled 0    #关闭LED灯

若要卸载驱动输入如下命令

rmmod newchrled.ko

至此,Linux 驱动开发路上的第一个灯被成功点亮。 本文主要是通过操作寄存器来点亮开发板上的LED,通过编写相应的驱动程序和应用程序,实现程序设计的分层

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

    关注

    22

    文章

    1592

    浏览量

    107950
  • Linux
    +关注

    关注

    87

    文章

    11292

    浏览量

    209330
  • 开发板
    +关注

    关注

    25

    文章

    5032

    浏览量

    97375
  • GPIO
    +关注

    关注

    16

    文章

    1204

    浏览量

    52054
  • MMU
    MMU
    +关注

    关注

    0

    文章

    91

    浏览量

    18283
收藏 人收藏

    评论

    相关推荐

    IMX6ULL正点原子开发板LED驱动

    用C语言裸机编程驱动正点原子I.MAX6ULL开发板上的LED
    的头像 发表于 05-02 15:22 25.9w次阅读
    IMX6ULL正点原子<b class='flag-5'>开发板</b><b class='flag-5'>LED</b><b class='flag-5'>驱动</b>

    基于STM32开发板LED

    硬件资源LEDSTM32开发板线缆LED驱动电路LED
    发表于 08-11 09:11

    如何配置Linux开发板的GPIO

    (I.MX6ULL)环境:Ubuntu 20.04 (LTS) (内核版本:Linux 5.4.0)交叉编译器:arm-linux-gnueabihf 4.9.4一、目的编写裸机代码(汇编)点亮LED
    发表于 12-15 07:46

    基于Study210开发板点亮LED的相关资料推荐

    Linux 嵌入式系列】点亮LED实战记录--基于Study210开发板Linux 嵌入式系列】点亮
    发表于 12-16 07:36

    TQ2440开发板按键点亮LED驱动开发详解

    记录了作者在TQ2440开发板上实现按键点亮LED驱动开发的详细过程,还记录了一些容易出现的错误,以及怎么解决这些错误。 一、驱动
    发表于 11-04 16:46 179次下载
    TQ2440<b class='flag-5'>开发板</b>按键点亮<b class='flag-5'>LED</b><b class='flag-5'>驱动</b><b class='flag-5'>开发</b>详解

    嵌入式开发板_iTOP-4412开发板linux系统存储空间

    [入式开发板]4412开发板linux 系统存储空间的修改
    发表于 02-29 16:58 13次下载

    开发板上的LED闪烁起来吧

    这节课给大家讲一下如何让开发板上网络编号为 D4 LED 闪烁起来,先看一下开发板上关于 LED 的原理图
    发表于 11-11 17:17 13次下载

    如何配置和操作Linux驱动程序开发板

    本文档概述了利用Linux开发板Linux 内核开发驱动程序的基础知识,并简单介绍了如何配置和操作
    的头像 发表于 02-15 13:36 3048次阅读
    如何配置和操作<b class='flag-5'>Linux</b><b class='flag-5'>驱动</b>程序<b class='flag-5'>开发板</b>

    利用Linux开发板为TLV320ADC5120开发Linux内核驱动的方法

    利用Linux开发板为TLV320ADC5120开发Linux内核驱动的方法
    发表于 10-28 11:59 0次下载
    利用<b class='flag-5'>Linux</b><b class='flag-5'>开发板</b>为TLV320ADC5120<b class='flag-5'>开发</b><b class='flag-5'>Linux</b>内核<b class='flag-5'>驱动</b>的方法

    通过Web网页控制开发板LED

    接下来将介绍如何通过Web网页来控制开发板上的LED,本文只是在网页上实现功能,并无交互功能,与开发板的交互功能实现将在《Web网页点灯二》中介绍
    的头像 发表于 04-25 15:05 1562次阅读
    通过Web网页控制<b class='flag-5'>开发板</b><b class='flag-5'>LED</b><b class='flag-5'>灯</b>

    汇编驱动IMX6ULL LED

    用汇编编写正点原子Linux开发板Led驱动
    的头像 发表于 05-01 09:19 25.9w次阅读
    汇编<b class='flag-5'>驱动</b>IMX6ULL <b class='flag-5'>LED</b><b class='flag-5'>灯</b>

    迅为基于RK3568开发板的嵌入式学习之Linux驱动视频

    迅为基于RK3568开发板的嵌入式学习之Linux驱动视频
    的头像 发表于 05-19 16:30 968次阅读
    迅为基于RK3568<b class='flag-5'>开发板</b>的嵌入式学习之<b class='flag-5'>Linux</b><b class='flag-5'>驱动</b>视频

    STM32 Linux开发板推荐 ,入门进阶必备!

    推荐一款适合入门进阶学习的Linux开发板:华清远见FS-MP1A开发板(STM32MP157开发板开发板介绍 FS-MP1A
    发表于 10-22 09:22 1次下载

    fpga开发板linux开发板区别

    FPGA开发板Linux开发板是两种不同的硬件开发平台,各自具有不同的特点和应用场景。在以下的文章中,我将详细介绍FPGA开发板
    的头像 发表于 02-01 17:09 2230次阅读

    linux开发板与树莓派的区别

    定义和用途 Linux开发板Linux开发板是一种基于Linux操作系统的嵌入式开发板,通常用
    的头像 发表于 08-30 15:34 937次阅读