9.2 Linux LED 驱动开发

一、Linux 下的 LED 驱动原理

  Linux 下的任何驱动,最后都是要配置相应的硬件寄存器。

1. 地址映射

  MMU 全称叫做 MemoryManage Unit,也就是内存管理单元。 现在的 Linux 支持无 MMU 处理器。MMU 主要完成的功能为:

  1、完成虚拟空间到物理空间的映射。

  2、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。

  虚拟空间到物理空间的映射其实就是 地址映射。 虚拟地址(VA,Virtual Address)、物理地址(PAPhyscical Address)。对于 32位的处理器来说,虚拟地址范围是 2^32=4GB,我们的开发板上有 1GB DDR3,这 1GB 的内存就是物理内存,经过 MMU 可以将其映射到整个 4GB 的虚拟空间:

  Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟地址。比如 STM32MP157 PI0 引脚的端口模式寄存器 GPIOI_MODER 物理地址为0x5000A000。如果没有开启 MMU 的话直接向 0x5000A000 这个寄存器地址写入数据就可以配置 PI0 的引脚功能(输入、输出、复用或模拟等)

  那有人会有疑惑,那为什么要开启MMU呢?开启 MMU 使得操作系统能够更好地管理内存资源、提供虚拟内存、实施内存保护和权限控制,并提供了更高效、安全、灵活的内存访问方式。总而言之,开启 MMU 的好处大大滴。

  现在开启了 MMU,并且设置了内存映射,因此就不能直接向 0x5000A000 这个地址写入数据了。我们必须得到 0x5000A000 这个物理地址在Linux 系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到
两个函数:
ioremap iounmap 

 

① ioremap 函数

/*
 * @description : 获取指定物理地址空间对应的虚拟地址空间
 * @param - res_cookie : 映射的物理起始地址
 * @param - size : 映射的内存空间大小
 * @return : __iomem 类型的指针,指向映射后的虚拟空间首地址
 * __iomen:可以修饰指针类型,将其标记为 I/O 内存指针, I/O 内存指针目的是告知编译器该指针指向的内存区域用于与 I/O 设备进行直接交互,需要采取特殊的读写方式和对齐规则
 */
void __iomem *ioremap(resource_size_t res_cookie, size_t size)
{
    return arch_ioremap_caller(res_cookie, size, MT_DEVICE, __builtin_return_address(0));
}

 如果需要 STM32MP157-ATK GPIOI_MODER 寄存器对应的虚拟地址 ,代码如下:

#define GPIOI_MODER        (0X5000A000)
static void __iomen        *GPIO_MODER_PI;
GPIO_MODER_PI = ioremap(GPIOI_MODER, 4);

  宏 GPIOI_MODER 是寄存器物理地址, GPIO_MODER_PI 是映射后的虚拟地址。对于 STMP32MP157 来说一个寄存器是 4 字节(32 ),因此映射的内存长度为 4。映射完成以后直接对 GPIO_MODER_PI 进行读写操作即可。 

 

② iounmap 函数

  卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射:

/*
 * @description : 释放 ioremap 所做的映射
 * @param - addr : 取消映射的虚拟地址空间首地址
 */
void iounmap (volatile void __iomem *addr);

  比如现在要卸载 GPIO_MODER_PI 寄存器的地址映射:

iounmap(GPIO_MODER_PI);

 

2. I/O 内存访问函数

  I/O 是输入/输出的意思, 这里涉及到两个概念: I/O 端口和 I/O 内存。当外部寄存器或内存映射到 IO 空间时,称为 I/O 端口。当外部寄存器或内存映射到内存空间时,称为 I/O 内存。 但 ARM 空间只有 I/O 内存。Linux 内核建议使用一组操作函数来对映射后的内存进行读写操作。 

① 读操作函数

u8 readb(const volatile void __iomem *addr);
u16 readw(const volatile void __iomem *addr);
u32 readl(const volatile void __iomem *addr);

/*
readb、 readw 和 readl 这三个函数分别对应 8bit、 16bit 和 32bit 读操作。
参数 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);

/*
writeb、 writew 和 writel 这三个函数分别对应 8bit、 16bit 和 32bit 写操作。
参数 value 是要写入的数值, addr 是要写入的地址。
 */

 

二、硬件原理图分析

  LED0 接到了 PI0 上, PI0 就是 GPIOI 组的第 0 个引脚,当 PI0 输出低电平(0)的时候发光二极管 LED0 就会导通点亮,当 PI0 输出高电平(1)的时候发光二极管LED0 不会导通,因此 LED0 也就不会点亮。所以 LED0 的亮灭取决于 PI0 的输出电平,输出 0 就亮,输出 1 就灭。 

 

三、实验程序编写

1. LED 驱动程序编写

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define LED_MAJOR 200 /* 主设备号 */
#define LED_NAME "led" /* 设备名字 */

#define LEDOFF 0 /* 关灯 */
#define LEDON 1 /* 开灯 */

/* 寄存器物理地址 */
#define PERIPH_BASE (0x40000000)
#define MPU_AHB4_PERIPH_BASE (PERIPH_BASE + 0x10000000)
#define RCC_BASE (MPU_AHB4_PERIPH_BASE + 0x0000)
#define RCC_MP_AHB4ENSETR (RCC_BASE + 0XA28)
#define GPIOI_BASE (MPU_AHB4_PERIPH_BASE + 0xA000)
#define GPIOI_MODER (GPIOI_BASE + 0x0000)
#define GPIOI_OTYPER (GPIOI_BASE + 0x0004)
#define GPIOI_OSPEEDR (GPIOI_BASE + 0x0008)
#define GPIOI_PUPDR (GPIOI_BASE + 0x000C)
#define GPIOI_BSRR (GPIOI_BASE + 0x0018)

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *MPU_AHB4_PERIPH_RCC_PI;
static void __iomem *GPIOI_MODER_PI;
static void __iomem *GPIOI_OTYPER_PI;
static void __iomem *GPIOI_OSPEEDR_PI;
static void __iomem *GPIOI_PUPDR_PI;
static void __iomem *GPIOI_BSRR_PI;

void led_switch(u8 sta)
{
    u32 val = 0;
    if(sta == LEDON) {
        val = readl(GPIOI_BSRR_PI);
        val |= (1 << 16);
        writel(val, GPIOI_BSRR_PI);
    }else if(sta == LEDOFF) {
        val = readl(GPIOI_BSRR_PI);
        val |= (1 << 0);
        writel(val, GPIOI_BSRR_PI);
    }
}

void led_unmap(void)
{
    iounmap(MPU_AHB4_PERIPH_RCC_PI);
    iounmap(GPIOI_MODER_PI);
    iounmap(GPIOI_OTYPER_PI);
    iounmap(GPIOI_OSPEEDR_PI);
    iounmap(GPIOI_PUPDR_PI);
    iounmap(GPIOI_BSRR_PI);
}

static int led_open(struct inode *inode, struct file *filp)
{
    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);
    } else if(ledstat == LEDOFF) {
        led_switch(LEDOFF);
    }
    return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}

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

static int __init led_init(void)
{
    int retvalue = 0;
    u32 val = 0;

    MPU_AHB4_PERIPH_RCC_PI = ioremap(RCC_MP_AHB4ENSETR, 4);
    GPIOI_MODER_PI = ioremap(GPIOI_MODER, 4);
    GPIOI_OTYPER_PI = ioremap(GPIOI_OTYPER, 4);
    GPIOI_OSPEEDR_PI = ioremap(GPIOI_OSPEEDR, 4);
    GPIOI_PUPDR_PI = ioremap(GPIOI_PUPDR, 4);
    GPIOI_BSRR_PI = ioremap(GPIOI_BSRR, 4);

    val = readl(MPU_AHB4_PERIPH_RCC_PI);
    val &= ~(0X1 << 8);
    val |= (0X1 << 8);
    writel(val, MPU_AHB4_PERIPH_RCC_PI);

    val = readl(GPIOI_MODER_PI);
    val &= ~(0X3 << 0);
    val |= (0X1 << 0);
    writel(val, GPIOI_MODER_PI);

    val = readl(GPIOI_OTYPER_PI);
    val &= ~(0X1 << 0);
    writel(val, GPIOI_OTYPER_PI);

    val = readl(GPIOI_OSPEEDR_PI);
    val &= ~(0X3 << 0);
    val |= (0x2 << 0);
    writel(val, GPIOI_OSPEEDR_PI);

    val = readl(GPIOI_PUPDR_PI);
    val &= ~(0X3 << 0);
    val |= (0x1 << 0);
    writel(val,GPIOI_PUPDR_PI);

    val = readl(GPIOI_BSRR_PI);
    val |= (0x1 << 0);
    writel(val, GPIOI_BSRR_PI);

    retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
    if(retvalue < 0){
        printk("register chrdev failed!\r\n");
        goto fail_map;
    }
    return 0;

fail_map:
    led_unmap();
    return -EIO;
}

static void __exit led_exit(void)
{
    led_unmap();
    unregister_chrdev(LED_MAJOR, LED_NAME);
}

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("LXS");
MODULE_INFO(intree, "Y");

 

2. 测试 APP

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

#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];

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

    databuf[0] = atoi(argv[2]);

    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;
}

 

三、运行测试

1. 编译驱动程序

  依然是利用 Makefile 文件把 chrdevbase.c 编译为 chrdevbase.ko 模块:

KERNELDIR := /home/alientek/linux/atk-mpl/linux/my_linux/linux-5.4.31	# Linux内核源码路径
CURRENT_PATH := $(shell pwd)		# 获取当前所处路径
obj-m := led.o		

build: kernel_modules

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

  之后在 /linux/atk-mpl/Drivers/2_led 路径下输入:

make -j32

 

2. 编译测试 APP

  因为要在 ARM 上面运行,所以要用交叉编译器:

arm-none-linux-gnueabihf-gcc ledApp.c -o ledApp

 

3. 运行测试

  将 led.ko 和 ledApp 拷贝到 /linux/nfs/rootfs/lib/modules/5.4.31 路径下:

sudo cp ledApp led.ko /home/alientek/linux/nfs/rootfs/lib/modules/5.4.31/ -f

  加载 led.ko 驱动模块:ls

depmod
modprobe led    # 加载驱动
lsmod           # 查看加载的驱动

 

  驱动加载成功后创建 "/dev/led" 设备节点:

mknod /dev/led c 200 0

  输入以下命令测试 LED:

# 打开 LED
./ledApp /dev/led 1

# 关闭 LED
./ledApp /dev/led 0

  如果开发版的红色 LED 能打开和关闭说明测试成功。卸载驱动:

rmmod led.ko

本文作者:烟儿公主

本文链接:https://www.cnblogs.com/toutiegongzhu/p/17623058.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   烟儿公主  阅读(164)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
  1. 1 夏日大冒险 暴躁的兔子
夏日大冒险 - 暴躁的兔子
00:00 / 00:00
An audio error has occurred.

作词 : 暴躁的兔子

作曲 : 暴躁的兔子

编曲 : IOF

混音:Gfanfan

出品:网易飓风

夏天 不要再浪费时间

实现 你承诺过的改变

别再 找一堆借口拖延

现在就和我一起飞向海边

人生苦短 你应该学会如何作乐

低着头还怎么应对挫折

人应该为自己活着

不用去迎合

要去寻欢作乐

撮合我的浪漫和悲欢

把这荒诞人生都塞满

生活难免磕磕绊绊

对抗生活的平庸就是浪漫

学会取悦自己逆风翻盘

去反抗变态的三观

把条条框框都砸烂

建立新的规则推翻谈判

无可救药的人呐

和我一起去海边

看那日出和晚霞 海天一线

看阳光穿越地平线

现实交织的明天

就在这个夏天

为自己改变

别怕山高路远

去冒险

我真的不care你是否会喜欢我

不跟风被定义的美 全都是灾祸

我才不讨好大多数绝不与示弱

过好你的生活

你管我应该怎么快活

没有人能有资格审判

别人的生活和牵绊

快闭上你的高谈阔论

乘风破浪吧 理想的风帆

我就是肆意张扬又如何

我就是锋芒毕露又如何

我就是离经叛道又如何

我就是要出格 你管我要如何

我就是与众不同又如何

我就是特立独行又如何

我就是不知好歹又如何

你管我怎样出格 你管我如何

无可救药的人呐

和我一起去海边

看那日出和晚霞 海天一线

看阳光穿越地平线

现实交织的明天

就在这个夏天

为自己改变

别怕山高路远

不知进退的人呐

和我一起去海边

聊聊曾经的理想 一起想当年

那曾想改变世界的人

是否还满腔热忱

不羁的我们放肆着

反抗那命运的指针

解放灵魂

推广:网易飓风

企划:贾焱祺

监制:徐思灵

出品人:谢奇笛