一、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。