0%

简单的LED驱动程序

1.学会看芯片手册

1.1 GPIO的通用操作方法

参考stm32的GPIO引脚的使用过程:

  1. 使能GPIOx时钟
  2. 设置GPIOx引脚的工作模式(输入、输出、复用、重定向
  3. 读取/设置GOPIOx引脚的电平

一般的,芯片GPIO引脚使能的操作如下:

  1. 芯片手册一般有相关章节,用来介绍:power/clock 可以设置对应寄存器使能某个 GPIO 模块(Module) 。有些芯片的 GPIO 是没有使能开关的,即它总是使能的
  2. 一个引脚可以用于 GPIO、串口、USB 或其他功能, 有对应的寄存器来选择引脚的功能
  3. 对于已经设置为 GPIO 功能的引脚,有方向寄存器用来设置它的方向:输出、输入
  4. 对于已经设置为 GPIO 功能的引脚,有数据寄存器用来写、读引脚电平状态

对于GPIO写电平状态根据硬件也有两种方式:

  1. 仅有GPIO数据寄存器:先读,设置位,再写
  2. 使用set-and-clear protocol:这种方式,硬件上有三个寄存器set_reg,clr_eg,data_reg,对于置位和复位寄存器,均是在相应位写1执行置位/复位功能,然后由硬件来完成置位/复位。

1.2 操作IMX6ULL的GPIO点亮LED

在本节,通过查询开发板原理图和芯片手册来获取点亮LED所需要的所有编程所需信息

1.2.1 查看LED对应的GPIO引脚

image-20231030205647245

在原理图中发现,LED2对应引脚GPIO5_3

1.2.2 GPIO的整体框图

image-20231106193130990

第一次没看懂,当了解完图中的每一个模块后,再来解读图中所表达的内容。

这张图描述了如何定义一个引脚(PAD)的功能。

  • IOMUXC(IO Mulitplexing Control)用来定义每一个PAD的功能
    • IOMUX作为一个cell用来控制一个PAD的多路复用
    • 通过配置寄存器SW_MUX_CTL_PAD_*来定义PAD选择哪一路功能
    • 通过配置寄存器SW_PAD_CTL_PAD_*来定义有关PAD的具体设置,例如引脚的边沿速率,驱动能力等
  • GPIO(General-purpose IO)
    • GPIO.DR:数据寄存器,输出模式下用来控制引脚输出电平,输入模式下可以用来读取输入电平
    • GPIO.GDIR(GPIO direction register):用来控制GPIO的输入输出方向
    • GPIO.PSR(Pad status register):用来读取引脚的电平状态
  • CCM(Clock Control Manage)
    • 主要就是使能IOMUXC和GPIO模块

1.2.3 IOMUXC

IOMUX控制器(IOMUXC)与IOMUX一起使IC能够共享一个焊盘到多个功能块。这种共享是通过多路复用焊盘的输入和输出信号来实现的。每个模块都需要特定的焊盘设置(如上拉或保持器),每个焊盘最多有8个多路复用选项(称为ALT模式)。极板设置参数由IOMUXC控制。IOMUX仅由几个基本IOMUX单元组合而成的组合逻辑组成。每个基本IOMUX单元仅处理一个焊盘信号的复用。

(1)IOMUX_cell

IOMUX由多个(大约是SoC中焊盘的数量)基本IOMUX_cell单元组成。如果特定焊盘只需要一种功能模式,则不需要IOMUX,并且信号可以直接从模块连接到I/O。每当特定焊盘需要两个或多个功能模式时,或者当需要一个功能模式和一个测试模式时,都需要IOMUX单元。iomux_cell的基本设计允许两个级别的HW信号控制(在ALT6和ALT7模式下,ALT7获得最高优先级),如图32-2所示。

ALT7和ALT6扩展复用模式允许系统中的任何信号(如保险丝、焊盘输入、JTAG或软件寄存器)覆盖任何软件配置并强制ALT6/ALT7复用模式。它还允许IOMUX软件寄存器控制一组焊盘。

image-20231101122916164

(2)寄存器

多路复用控制寄存器

image-20231106113954556

  • 4-SION该位用于配置引脚的loopback回环模式,在测试时可以使能方便调试。使能之后会强制将引脚的真实电平状态输入到相应寄存器中,例如GPIOx_PSR寄存器。
  • 这个引脚的多路复用仅有功能ALT5,一般会有ALT0~`ALT7`八路复用可选。

引脚控制寄存器

image-20231106114027051

image-20231106114042999

  • 16-Hysteresis:输入方式,施密特触发器或者CMOS输入模式。

  • 13-pull/Keep Enable Field:拉电阻/保持电阻,拉电阻就是上拉或者下拉。保持电阻是在引脚浮空时能够使引脚保持在引脚上一时刻的电平状态。

  • 5-3-Drive Strength Field:用于控制输出引脚的驱动能力。驱动能力指的是芯片输出引脚能够提供的电流或功率,它决定了该引脚可以驱动外部电路的能力。

  • 0-Slew Rate Field:用于配置输出引脚的边沿速率(slew rate)。边沿速率指的是输出信号在从一个电压状态迅速变化到另一个电压状态时的变化速度。

1.2.4 GPIO

GPIO通用框图:

image-20231030212700304

  • Data register (GPIO_DR)
  • Pad status register (GPIO_PSR)
  • GPIO direction register (GPIO_GDIR)
  • Edge select register (GPIO_EDGE_SEL)
  • Interrupt control registers (GPIO_ICR1, GPIO_ICR2)
  • Interrupt mask register (GPIO_IMR)
  • Interrupt status register (GPIO_ISR)

查询GPIO寄存器的基地址:

image-20231129163423384

(1) GPIOx_GDIR

image-20231101120322237

(2) GPIOx_DR

读取DR位的结果取决于IOMUXC输入模式设置和相应的GDIR位,如下所示:

  • 如果设置了GDIR[n],并且IOMUXC输入模式为GPIO,则读取DR[n]将返回DR[n]的内容。
  • 如果设置了GDIR[n],并且IOMUXC输入模式不是GPIO,则读取DR[n]将返回DR[n]的内容。
  • 如果GDIR[n]被清除,IOMUXC输入模式为GPIO,则读取DR[n]返回相应的输入信号值。
  • 如果GDIR[n]被清除,并且IOMUXC输入模式不是GPIO,那么读取DR[n]总是返回零。

image-20231129170159399

(3) GPIOx_PSR

GPIO_PSR是一个只读寄存器。每个比特存储对应的输入信号的值(如在IOMUX中配置的)。此寄存器使用ipg_clk_s时钟进行计时,这意味着只有在访问此位置时才会对输入信号进行采样。每当访问此寄存器进行同步时,都需要两种等待状态。

image-20231106202237538

image-20231106120228246

1.2.5 CCM

(1) GPIO使能

image-20231030215135900

(2) IOMUXC使能

image-20231106202806993

(3) 如何操作CCGR寄存器

对于每一个CGx配置值及其对应功能如下:

image-20231106203816118

例如CCGR1:

image-20231106203915142

还可以知道默认几乎所有模块的时钟再上电时都是打开的

1.2.6 总结配置过程

  1. 在CCG模块中使能GPIO和IOMUXC模块的时钟(默认已经使能)
  2. 在IOMUXC模块中设置GPIO5_3(SNVS_TAMPER3)焊盘的多路复用和焊盘本身相关配置
  3. 在GPIP模块的寄存器GPIOx_GDIR中配置GPIO是输出模式还是输入模式
  4. 配置GPIO模块的寄存器GPIOx_DR,配置引脚的电平状态

2.编写LED驱动程序

根据上一篇“编写第一个驱动程序“了解了驱动程序的基本框架,本篇先通过阅读数据手册得知如何配置寄存器才能够点亮LED灯。现在还差的是:如何在驱动程序中读写寄存器。

2.1 如何在驱动程序中读写寄存器

将寄存器物理地址映射到虚拟地址

1
2
3
4
5
6
7
/* GPIO5_GDIR 地址:0x020AC000 + 0x04 */
static volatile unsigned int *GPIO5_GDIR;
/* ioremap:将寄存器的物理地址映射为虚拟地址 */
GPIO5_GDIR = ioremap(0x020AC000 + 0x04, 4);

/* iounmap:取消虚拟地址的映射 */
iounmap(GPIO5_GDIR);

根据虚拟地址读写寄存器

1
*GPIO5_GDIR |= (1 << 3);

2.1.1 问题

问题1:为什么需要将寄存器物理地址映射为虚拟地址?

在Linux系统中,驱动程序在读写硬件寄存器时通常需要将寄存器的物理地址映射成虚拟地址。这涉及到操作系统的虚拟内存管理机制和硬件访问的相关性。

  1. 抽象硬件访问: 操作系统的设计目标之一是提供对硬件的抽象,使应用程序和驱动程序能够在不同硬件平台上运行而无需修改。通过将物理地址映射为虚拟地址,驱动程序可以使用虚拟地址来访问硬件资源,而不必担心底层硬件的物理地址。
  2. 虚拟内存隔离: 操作系统使用虚拟内存来提供对内存的隔离和保护。将硬件寄存器的物理地址映射到虚拟地址空间中,使得驱动程序可以利用虚拟内存管理机制,确保不会越界访问或者非法修改其他进程的内存。
  3. 内存映射: 内存映射是一种将文件或设备映射到进程地址空间的技术。通过内存映射,驱动程序可以将硬件寄存器映射到自己的地址空间中,从而使得访问硬件寄存器就像访问内存一样简单。
  4. 缓存管理: 操作系统可以通过虚拟内存管理来优化对硬件寄存器的访问,例如使用缓存。虚拟内存管理可以确保合适的缓存策略用于访问硬件寄存器,提高数据访问的效率。

总体而言,将硬件寄存器的物理地址映射为虚拟地址是为了提供更高层次的抽象和隔离,使得驱动程序能够以更安全和通用的方式访问硬件资源。这也有助于确保驱动程序的可移植性和可维护性。

问题2:地址指针为什么需要使用volatile关键字进行修饰?

1
static volatile unsigned int *GPIO5_GDIR;

在这个声明中,volatile 关键字的作用是告诉编译器,该指针所指向的内存位置的值是可以在程序的控制之外被改变的,因此编译器在优化时应该小心处理这个指针所指向的内存。

在嵌入式系统和驱动程序开发中,经常会涉及到与硬件直接交互的情况。硬件寄存器的值可能会在程序的正常控制流程之外被外部因素更改,比如中断服务程序、其他设备或者直接由硬件。使用 volatile 关键字告诉编译器不要对这些变量的读写进行优化,以确保编译器不会在代码中插入不必要的优化,例如缓存变量的值,而直接使用缓存中的值,而不是每次都从内存中读取。

在上述代码中,GPIO5_GDIR 是一个指向寄存器的指针,这个寄存器的值可能会被硬件或者中断服务程序在程序的控制之外修改。因此,使用 volatile 关键字是为了确保编译器不会对这个指针进行过度的优化,从而保证读写该寄存器的操作是可见且正确的。

2.2 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#include "asm/io.h"
#include "asm/uaccess.h"
#include "linux/printk.h"
#include "linux/stddef.h"
#include <linux/module.h>
#include <linux/major.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/delay.h> /* guess what */
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/mutex.h>
#include <linux/firmware.h>
#include <linux/platform_device.h>
#include <linux/uaccess.h> /* For put_user and get_user */

/* 函数声明 */
static ssize_t led_drv_open(struct inode *node, struct file *file);
static ssize_t led_drv_write(struct file *file, const char __user *buf, size_t size, loff_t *ppos);
static int led_drv_close(struct inode *node, struct file *file);

/* registers */
// IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 地址:0x02290000 + 0x14
static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
// IOMUXC_SNVS_SW_PAD_CTL_PAD_SNVS_TAMPER3 地址:0x02290000 + 0x58
static volatile unsigned int *IOMUXC_SNVS_SW_PAD_CTL_PAD_SNVS_TAMPER3;
// GPIO5_GDIR 地址:0x020AC000 + 0x04
static volatile unsigned int *GPIO5_GDIR;
// GPIO5_DR 地址:0x020AC000 + 0x00
static volatile unsigned int *GPIO5_DR;

/* 全局变量 */
int major;
struct class *myled_class;
static const struct file_operations myled_fops = {
.owner = THIS_MODULE,
.open = led_drv_open,
.write = led_drv_write,
.release = led_drv_close,
};

static int led_drv_open(struct inode *inode, struct file *file)
{
//当前所在的源代码文件的文件名 当前所在的函数的名称 代码行号
printk("file:%s func:%s line:%d\n", __FILE__ , __FUNCTION__, __LINE__);
// 注意修改寄存器的值时不要影响其他位
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 &= ~0xf; //先清除
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 |= 0x5; //再赋值,失能测试模式,配置复用为ALT5
*GPIO5_GDIR |= (1 << 3); // 配置引脚为输出模式
*GPIO5_DR |= (1 << 3); // 先熄灭LED

return 0;
}

static int led_drv_close(struct inode *node, struct file *file)
{
printk("file:%s func:%s line:%d\n", __FILE__ , __FUNCTION__, __LINE__);
return 1;
}

static ssize_t led_drv_write(struct file *file, const char __user *buf, size_t size, loff_t *ppos)
{
char c;
int ret;
//当前所在的源代码文件的文件名 当前所在的函数的名称 代码行号
printk("file:%s func:%s line:%d\n", __FILE__ , __FUNCTION__, __LINE__);
ret = copy_from_user(&c, buf, 1);
if(c == '1')
/* set led on*/
*GPIO5_DR &= ~(1 << 3);
else
/* set led off */
*GPIO5_DR |= (1 << 3);
return 1;
}

static int __init led_init_driver(void)
{
printk("file:%s func:%s line:%d\n", __FILE__ , __FUNCTION__, __LINE__);
major = register_chrdev(0, "myled", &myled_fops);

/* ioremap:将寄存器的物理地址映射为虚拟地址 */
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x02290000 + 0x14, 4);
IOMUXC_SNVS_SW_PAD_CTL_PAD_SNVS_TAMPER3 = ioremap(0x02290000 + 0x58, 4);
GPIO5_GDIR = ioremap(0x020AC000 + 0x04, 4);
GPIO5_DR = ioremap(0x020AC000 + 0x00, 4);

myled_class = class_create(THIS_MODULE, "myled");
device_create(myled_class, NULL, MKDEV(major, 0), NULL, "myled");

return 0;
}

static void __exit led_exit_driver(void)
{
/* iounmap:取消虚拟地址的映射 */
iounmap(IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3);
iounmap(IOMUXC_SNVS_SW_PAD_CTL_PAD_SNVS_TAMPER3);
iounmap(GPIO5_DR);
iounmap(GPIO5_GDIR);

device_destroy(myled_class, MKDEV(major, 0));
class_destroy(myled_class);
unregister_chrdev(major, "myled");
}

module_init(led_init_driver);
module_exit(led_exit_driver);
MODULE_LICENSE("GPL");

然后,简单编写一个APP用来测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int fd;
int main()
{
char c = '1';
int ret;
fd = open("/dev/myled", O_RDWR);
if(fd == -1)
{
perror("open error ");
return -1;
}

while(1)
{
printf("input one char (1:led on, q:quit, other: led off):");
scanf("%c", &c);
getchar();
if(c == 'q') return 0;
ret = write(fd, &c, 1);
if(ret != 1)
{
perror("write error ");
return -1;
}
}
return 0;
}