当前位置: 首页 > article >正文

Linux LED 实验

一、Linux 下 LED 灯驱动原理

其实跟裸机实验很相似,只不过要编写符合 Linux 的驱动框架。

1. 地址映射

MMU全称 Memory Manage Unit,即内存存储单元。

MMU主要功能为:

1)完成虚拟空间到物理空间的映射;

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

Linux 内核启动的时候会初始化 MMU,设置好内存映射。

原来的裸机实验,是直接对 GPIO1_IO03 引脚的复用寄存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 的地址0X020E0068写入数据。

但是现在开启了 Linux 内核后,MMU设置了内存映射,不能向原先的地址写入数据了。那么必须要得到 0X020E0068 对应的虚拟地址。

1.1 ioremap 函数

该函数用于获取物理地址空间对应的虚拟地址空间,定义在 arch/arm/include/asm/io.h 文件中。

我们查看 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:要映射的物理起始地址。

size:要映射的内存空间大小。

mtype:ioremap 的类型,可以选择 MT_DEVICE、MT_DEVICE_NONSHARED、MT_DEVICE_CACHED 和 MT_DEVICE_WC,ioremap 函数选择 MT_DEVICE。

返回值:__iomem 类型的指针,指向映射后的虚拟空间首地址。

要获取IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器对应 的虚拟地址,使用如下代码即可:

#define SW_MUX_GPIO1_IO03_BASE (0X020E0068) 
static void __iomem* SW_MUX_GPIO1_IO03; 
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4); 

宏 SW_MUX_GPIO1_IO03_BASE 是寄存器物理地址,SW_MUX_GPIO1_IO03 是映射后 的虚拟地址。对于 I.MX6ULL 来说一个寄存器是 4 字节(32 位)的,因此映射的内存长度为 4。

1.2 iounmap 函数

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

比如上面的驱动进行卸载

iounmap(SW_MUX_GPIO1_IO03); 

2. IO内存访问函数

Linux 内核提供一组操作函数对映射后的内存进行读写操作。

2.1 读操作函数

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 就是要 读取写内存地址,返回值就是读取到的数据。 。

2.2 写操作函数

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 是要写入的地址。

二、 硬件原理

STM32和IM6ULL的裸机实验有介绍了,就不赘述了。

三、实验程序编写

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; 

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

关闭/释放设备

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

设备操作函数

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; 

    /* 初始化 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、设置 GPIO1_IO03 的复用功能,将其复用为 
    * GPIO1_IO03,最后设置 IO 属性。 
    */ 
    writel(5, SW_MUX_GPIO1_IO03); 

    /* 寄存器 SW_PAD_GPIO1_IO03 设置 IO 属性 */ 
    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); 

    /* 6、注册字符设备驱动 */ 
    retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops); 
    if(retvalue < 0){ 
        printk("register chrdev failed!\r\n"); 
        return -EIO; 
    } 
    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); 

    /* 注销字符设备驱动 */ 
    unregister_chrdev(LED_MAJOR, LED_NAME); 
    } 

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

2. 编写测试程序

这时候需要用的 Linux C了,在 Linux 系统下,一切皆文件。应用层如何操控底层硬件,同样也是通过文件 I/O 的方式来实现。设备文件通常在/dev/目录下,我们也把/dev 目录下的文件称为设备节点。除此之外,我们还可以通过 sysfs 文件系统对硬件设备进行 操控。

对于 ALPHA/Mini I.MX6U 开发板出厂系统来说,此 LED 设备使用的是 Linux 内核标准 LED 驱动框架 注册而成,在/dev 目录下并没有其对应的设备节点,其实现使用 sysfs 方式控制。

2.1 sysfs 文件系统

请注意,驱动开发先别看这里,因为需要先 mfgtool 把系统文件烧好,然后直接进入系统,而不是和平时驱动开发那样,根文件是挂载的情况。

sysfs 是一个基于内存的文件系统,同 devfs、proc 文件系统一样,称为虚拟文件系统;它的 作用是将内核信息以文件的方式提供给应用层使用。

sysfs 文件系统挂载在/sys 目录下

进入到/sys/class/leds 目录 下,如果找不到 sys-led 文件,说明你移植的根文件系统是原产的。

brightness、max_brightness 以及 trigger 三个文件,这三个文件都是 LED 设备的 属性文件:

brightness:翻译过来就是亮度的意思,该属性文件可读可写;

max_brightness:该属性文件只能被读取,不能写,用于获取 LED 设备的最大亮度等级。

trigger:触发模式,该属性文件可读可写,读表示获取 LED 当前的触发模式,写表示设置 LED 的 触发模式。通过 cat 命令查看该属性文件。

方括号([heartbeat])括起来的表示当前 LED 对应的触发模式,none 表示无触发,常用的触发模式包括 none(无触发)、mmc0(当对 mmc0 设备发起读写操作的时候 LED 会闪烁)、timer(LED 会有规律的一 亮一灭,被定时器控制住)、heartbeat(心跳呼吸模式,LED 模仿人的心跳呼吸那样亮灭变化)。

通过 echo 命令进行控制:

echo timer > trigger //将 LED 触发模式设置为 timer 
 
echo none > trigger //将 LED 触发模式设置为 none 
echo 1 > brightness //点亮 LED echo 0 > brightness//熄灭 LED

编写应用程序

#include <stdio.h> 
#include <stdlib.h> 
#include <sys/types.h> 
#include <sys/stat.h> 
#include <fcntl.h> 
#include <unistd.h> 
#include <string.h> 
 
#define LED_TRIGGER "/sys/class/leds/sys-led/trigger" 
#define LED_BRIGHTNESS "/sys/class/leds/sys-led/brightness" 
#define USAGE() fprintf(stderr, "usage:\n" \ 
 " %s <on|off>\n" \ 
 " %s <trigger> <type>\n", argv[0], argv[0]) 
 
int main(int argc, char *argv[]) 
{ 
 int fd1, fd2; 
 
 /* 校验传参 */ 
 if (2 > argc) { 
 USAGE(); 
 exit(-1); 
 } 
 /* 打开文件 */ 
 fd1 = open(LED_TRIGGER, O_RDWR); 
 if (0 > fd1) { 
 perror("open error"); 
 exit(-1); 
 } 
 
 fd2 = open(LED_BRIGHTNESS, O_RDWR); 
 if (0 > fd2) { 
 perror("open error"); 
 exit(-1); 
 } 
 
 /* 根据传参控制 LED */ 
 if (!strcmp(argv[1], "on")) { 
 write(fd1, "none", 4); //先将触发模式设置为 none 
 write(fd2, "1", 1); //点亮 LED 
 } 
 else if (!strcmp(argv[1], "off")) { 
 write(fd1, "none", 4); //先将触发模式设置为 none 
 write(fd2, "0", 1); //LED 灭 
 } 
 else if (!strcmp(argv[1], "trigger")) { 
 if (3 != argc) { 
 USAGE(); 
 exit(-1); 
 } 
 
 if (0 > write(fd1, argv[2], strlen(argv[2]))) 
 perror("write error"); 
 } 
 else 
 USAGE(); 
 
 exit(0); 
} 

然后使能编译环境

source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi

CC变量就是交叉编译工具。

编译程序,并把可执行文件拷贝到开发板上。然后运行并添加参数。

2.2 dev 文件系统

这里是正统的驱动方式,跟上个字符设备驱动开发的方式是一样的。

用内核源码的 include,以及开发板的内核和驱动是同源的。

编译成功以后就会生成一个名为“led.ko”的驱动模块文件。

led 驱动加载后,手动创建 /dev/led 节点,然后向 /dev/led 文件写 0 关闭灯,写 1 开启灯。

ledApp.c 的源码如下:

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


#define LEDOFF 	0
#define LEDON 	1

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
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;
}

以及 Makefile 文件

KERNELDIR := /home/prover/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
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

测试文件同样使用交叉编译器来编译。

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

最后,将 .ko 和 测试文件 拷贝到 rootfs 的 lib/modules/4.1.15 下。

sudo cp ledApp led.ko /home/prover/linux/nfs/rootfs/lib/modules/4.1.15/

由于是第一次加载驱动,先 depmod 然后再 modprobe led.ko。

然后创建 /dev/led 设备节点:

mknod /dev/led c 200 0 

 随后,测试:

./ledApp /dev/led 1

最后把驱动卸载

rmmod led.ko 

 

2.3 Qt 应用

其实就是通过 sysfs 文件系统来控制 led 的亮灭。

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QPushButton>
#include <QFile>

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private:
    /* 按钮 */
    QPushButton *pushButton;

    /* 文件 */
    QFile file;

    /* 设置lED的状态 */
    void setLedState();

    /* 获取lED的状态 */
    bool getLedState();

private slots:
    void pushButtonClicked();
};
#endif // MAINWINDOW_H

mainwindow.cpp

#include "mainwindow.h"
#include <QDebug>
#include <QGuiApplication>
#include <QScreen>
#include <QRect>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    /* 获取屏幕的分辨率,Qt官方建议使用这
     * 种方法获取屏幕分辨率,防上多屏设备导致对应不上
     * 注意,这是获取整个桌面系统的分辨率
     */
    QList <QScreen *> list_screen =  QGuiApplication::screens();

    /* 如果是ARM平台,直接设置大小为屏幕的大小 */
#if __arm__
    /* 重设大小 */
    this->resize(list_screen.at(0)->geometry().width(),
                 list_screen.at(0)->geometry().height());
    /* 默认是出厂系统的LED心跳的触发方式,想要控制LED,
     * 需要改变LED的触发方式,改为none,即无 */
    system("echo none > /sys/class/leds/sys-led/trigger");
#else
    /* 否则则设置主窗体大小为800x480 */
    this->resize(800, 480);
#endif

    pushButton = new QPushButton(this);

    /* 居中显示 */
    pushButton->setMinimumSize(200, 50);
    pushButton->setGeometry((this->width() - pushButton->width()) /2 ,
                            (this->height() - pushButton->height()) /2,
                            pushButton->width(),
                            pushButton->height()
                            );
    /* 开发板的LED控制接口 */
    file.setFileName("/sys/devices/platform/leds/leds/sys-led/brightness");

    if (!file.exists())
        /* 设置按钮的初始化文本 */
        pushButton->setText("未获取到LED设备!");

    /* 获取LED的状态 */
    getLedState();

    /* 信号槽连接 */
    connect(pushButton, SIGNAL(clicked()),
            this, SLOT(pushButtonClicked()));
}

MainWindow::~MainWindow()
{
}

void MainWindow::setLedState()
{
    /* 在设置LED状态时先读取 */
    bool state = getLedState();

    /* 如果文件不存在,则返回 */
    if (!file.exists())
        return;

    if(!file.open(QIODevice::ReadWrite))
        qDebug()<<file.errorString();

    QByteArray buf[2] = {"0", "1"};

    /* 写0或1 */
    if (state)
        file.write(buf[0]);
    else
        file.write(buf[1]);

    /* 关闭文件 */
    file.close();

    /*重新获取LED的状态 */
    getLedState();
}

bool MainWindow::getLedState()
{
    /* 如果文件不存在,则返回 */
    if (!file.exists())
        return false;

    if(!file.open(QIODevice::ReadWrite))
        qDebug()<<file.errorString();

    QTextStream in(&file);

    /* 读取文件所有数据 */
    QString buf = in.readLine();

    /* 打印出读出的值 */
    qDebug()<<"buf: "<<buf<<endl;
    file.close();
    if (buf == "1") {
        pushButton->setText("LED点亮");
        return true;
    } else {
        pushButton->setText("LED熄灭");
        return false;
    }
}

void MainWindow::pushButtonClicked()
{
    /* 设置LED的状态 */
    setLedState();
}


通过 Qt 编译(或者直接编写Makefile)的可执行文件,传输到开发板上。

在串口 xtem 终端上运行可执行文件。会发现界面直接覆盖了开发板的整个 UI。


http://www.kler.cn/a/537458.html

相关文章:

  • Vue 3 30天精进之旅:Day 17 - 样式和动画
  • 【东莞常平】戴尔R710服务器不开机维修分享
  • 赛博算命之 ”梅花易数“ 的 “JAVA“ 实现 ——从玄学到科学的探索
  • 【iOS自动化】Xcode配置WebDriverAgent
  • 退格法记单词(类似甘特图)
  • 今日AI和商界事件(2025-02-07)
  • 4.python+flask+SQLAlchemy+达梦数据库
  • 【Pytorch实战教程】PyTorch中的Dataset用法详解
  • Redis企业开发实战(二)——点评项目之商户缓存查询
  • ​PDFsam Basic是一款 免费开源的PDF分割合并工具
  • 学习threejs,使用Lensflare模拟镜头眩光
  • hook so层实例流程
  • Linux内核数据结构之链表
  • Spring Boot 和Tomcat的关系
  • Oracle中TAF与SCANIP全面解析
  • Docker 容器 Elasticsearch 启动失败完整排查记录
  • 基于机器学习的DDoS检测系统实战
  • react-native fetch在具有http远程服务器后端的Android设备上抛出“Network request failed“错误
  • 基于Flask的医保数据可视化分析系统的设计与实现
  • day 40 复习makefile以及51单片机
  • 深度解析全钢陶瓷防静电地板在机房装修中应用较多的原因
  • vue-vite axios bug
  • 13.4 使用 LangChain Chat Model 实现专业级双语翻译
  • 大模型的主要漏洞探究
  • Netty初学五 客户端与服务端通信协议编解码
  • JUnit 5 条件测试注解详解