前言:
经过一系列的摸索,终于也是配置好了开发板的交叉编译环境,接下来我将把《原子Linux驱动开发》书上的LED驱动例程分享给大家
介绍:
从单片机转到linux,想点个灯就没有那么简单了,在单片机中就直接配置寄存器,如果用STM32的直接在cubemx中图形化配置,完成外设的初始化,然后调用对应控制GPIO的函数即可。但是在linux中就没有那么简单了。
在Linux中点亮一个灯的方法有很多,裸机驱动点灯,Linux下的led驱动点灯,设备树下的led点灯,pinctrl 和 gpio 子系统点灯,虽然都是点灯,但是驱动的本质还是没变,都是配置 LED 灯 所使用的 GPIO 寄存器,驱动开发方式和裸机基本没啥区别。Linux 是一个庞大而完善的系统, 尤其是驱动框架,像 GPIO 这种最基本的驱动不可能采用“原始”的裸机驱动开发方式,否则 就相当于你买了一辆车,结果每天推着车去上班。Linux 内核提供了 pinctrl 和 gpio 子系统用于 GPIO 驱动,随着学习的不断深入,最后我们将学习如何借助 pinctrl 和 gpio 子系统来简化 GPIO 驱动开发。
但在这之前,还是得老老实实的编写led驱动,即本书的第二章:嵌入式Linux LED灯驱动开发实验
阅读:
先来学习理论部分,我们跟着目录来,学习顺序依次是了解Linux下LED灯驱动原理,硬件原理图分析,实验程序编写,运行测试
1)Linux下led灯驱动原理
Linux 下的任何外设驱动,最终都是要配置相应的硬件寄存器。所以本章的 LED 灯驱动最 终也是对 I.MX6ULL 的 IO 口进行配置,与裸机实验不同的是,在 Linux 下编写驱动要符合 Linux 的驱动框架。I.MX6U-ALPHA 开发板上的 LED 连接到 I.MX6ULL 的 GPIO1_IO03 这个引脚上, 因此本章实验的重点就是编写 Linux 下 I.MX6UL 引脚控制驱动。
1.地址映射
在编写驱动之前,我们需要先简单了解一下 MMU 这个神器,MMU 全称叫做 Memory Manage Unit,也就是内存管理单元。在老版本的 Linux 中要求处理器必须有 MMU,但是现在 Linux 内核已经支持无 MMU 的处理器了。MMU 主要完成的功能如下: ①、完成虚拟空间到物理空间的映射。 ②、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。 我们重点来看一下第①点,也就是虚拟空间到物理空间的映射,也叫做地址映射。首先了 解两个地址概念:虚拟地址(VA,Virtual Address)、物理地址(PA,Physcical Address)。对于 32 位 的处理器来说,虚拟地址范围是 2^32=4GB,我们的开发板上有 512MB 的 DDR3,这 512MB 的 内存就是物理内存,经过 MMU 可以将其映射到整个 4GB 的虚拟空间,如图 所示:
Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚 拟 地 址 。 比 如 I.MX6ULL 的 GPIO1_IO03 引 脚 的 复 用 寄 存 器 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 的地址为 0X020E0068。如果没有开启 MMU 的话 直接向 0X020E0068 这个寄存器地址写入数据就可以配置 GPIO1_IO03 的复用功能。现在开启 了 MMU,并且设置了内存映射,因此就不能直接向 0X020E0068 这个地址写入数据了。我们必 须得到 0X020E0068 这个物理地址在 Linux 系统里面对应的虚拟地址,这里就涉及到了物理内 存和虚拟内存之间的转换,需要用到两个函数:ioremap 和 iounmap。ioremap 函 数 用 于 获 取 指 定 物 理 地 址 空 间 对 应 的 虚 拟 地 址 空 间 , 定义arch/arm/include/asm/io.h 文件中,定义如下:
1 #define ioremap(cookie,size) __arm_ioremap((cookie), (size),
MT_DEVICE)
2
3 void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size,
unsigned int mtype)
4 {
5 return arch_ioremap_caller(phys_addr, size, mtype,
__builtin_return_address(0));
6 }
ioremap 是个宏,有两个参数:cookie 和 size,真正起作用的是函数__arm_ioremap,此函 数有三个参数和一个返回值,这些参数和返回值的含义如下: phys_addr:要映射的物理起始地址。 size:要映射的内存空间大小。 mtype:ioremap 的类型,可以选择 MT_DEVICE、MT_DEVICE_NONSHARED、 MT_DEVICE_CACHED 和 MT_DEVICE_WC,ioremap 函数选择 MT_DEVICE。 返回值:__iomem 类型的指针,指向映射后的虚拟空间首地址。 假如我们要获取 I.MX6ULL 的 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。 映射完成以后直接对 SW_MUX_GPIO1_IO03 进行读写操作即可。
iounmap 函数:卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射,iounmap 函数原 型如下:
void iounmap (volatile void __iomem *addr)
iounmap 只有一个参数 addr,此参数就是要取消映射的虚拟地址空间首地址。假如我们现 在要取消掉 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器的地址映射,使用如下代码 即可:
iounmap(SW_MUX_GPIO1_IO03);
2)I/O 内存访问函数
这里说的 I/O 是输入/输出的意思,并不是我们学习单片机的时候讲的 GPIO 引脚。这里涉 及到两个概念:I/O 端口和 I/O 内存。当外部寄存器或内存映射到 IO 空间时,称为 I/O 端口。 当外部寄存器或内存映射到内存空间时,称为 I/O 内存。但是对于 ARM 来说没有 I/O 空间这个 概念,因此 ARM 体系下只有 I/O 内存(可以直接理解为内存)。使用 ioremap 函数将寄存器的物 理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议 这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
读操作函数有如下几个:
readb、readw 和 readl 这三个函数分别对应 8bit、16bit 和 32bit 读操作,参数 addr 就是要 读取写内存地址,返回值就是读取到的数据。
写操作函数有如下几个:
writeb、writew 和 writel 这三个函数分别对应 8bit、16bit 和 32bit 写操作,参数 value 是要 写入的数值,addr 是要写入的地址。
3)硬件原理图
程序编写:
书中提到的MX6U阿尔法开发板我是没有搞来,用的是手头上这块STM32mp135dk
说一下点灯的思路
1编译:创建led文件目录编写led驱动代码文件和Makefile文件
2运行:在已经搭建好的交叉编译环境中make一下
3下载:把Ubuntu系统中编译好的程序拷贝到win主机上,在通过ssh下载到开发板usr/local目录下
4运行:通过ssh终端运行文件,灯亮
有了思路接下来我们一步一步操作就好了
按流程用到的命令大致就是终端中的几条,第一遍摸索的时候比较急也很乱就没有一一截图,这里偷下懒直接放出总步骤(没有cd到具体目录),记得要source一下环境。
led.c:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <linux/gpio.h>
int main(int argc, char **argv)
{
struct gpiohandle_request req;
struct gpiohandle_data data;
char chrdev_name[20];
int fd, ret;
strcpy(chrdev_name, "/dev/gpiochip9");
/* Open device: gpiochip9 for GPIO MCP */
fd = open(chrdev_name, 0);
if (fd == -1) {
ret = -errno;
fprintf(stderr, "Failed to open %s\n", chrdev_name);
return ret;
}
/* request GPIO line: GPIO_9_15 */
req.lineoffsets[0] = 15;
req.flags = GPIOHANDLE_REQUEST_OUTPUT;
memcpy(req.default_values, &data, sizeof(req.default_values));
strcpy(req.consumer_label, "led_gpio_mcp_15");
req.lines = 1;
ret = ioctl(fd, GPIO_GET_LINEHANDLE_IOCTL, &req);
if (ret == -1) {
ret = -errno;
fprintf(stderr, "Failed to issue GET LINEHANDLE IOCTL (%d)\n",
ret);
}
if (close(fd) == -1)
perror("Failed to close GPIO character device file");
/* Start led blinking */
while(1) {
data.values[0] = !data.values[0];
ret = ioctl(req.fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data);
if (ret == -1) {
ret = -errno;
fprintf(stderr, "Failed to issue %s (%d)\n",
"GPIOHANDLE_SET_LINE_VALUES_IOCTL", ret);
}
sleep(1);
}
/* release line */
ret = close(req.fd);
if (ret == -1) {
perror("Failed to close GPIO LINEHANDLE device file");
ret = -errno;
}
return ret;
}
Makefile代码:
PROG = led
SRCS = led.c
OBJS = $(SRCS:.c=.o)
CLEANFILES = $(PROG)
# Add / change option in CFLAGS if needed
# CFLAGS += <new option>
$(PROG): $(OBJS)
$(CC) $(CFLAGS) -o $(PROG) $(OBJS)
.c.o:
$(CC) $(CFLAGS) -c $< -o $@
all: $(PROG)
clean:
rm -f $(CLEANFILES) $(patsubst %.c,%.o, $(SRCS)) *~
这里我用FileZila实现win和Ubuntu直接的文件传输
MobaXterm登录ssh
然后在终端运行命令
su -l weston -c "/usr/local/led"
效果:
61d93b739652a1585dd285b22676cb26
红灯闪烁
其实还有更简单的st官方的GPIOLib点灯,我也玩了下,只需要在ssh终端中输入指令即可完成点灯,它是用户层的代码可以直接通过char device控制GPIOlib,从而控制GPIO,和单片机的玩法有点像。在串口终端直接输入:
gpioset -c gpiochip9 14=1
效果:
501484d7a882069349c47e994ab7401a
绿灯点亮
led闪烁程序:
led
(16.68 KB, 下载次数: 0)