少女祈祷中...

使用寄存器对GPIO进行操作

树莓派GPIO寄存器相关

首先我们要了解对GPIO操作的相关寄存器,树莓派4B使用的芯片是bcm2711,通过树莓派官网下载芯片手册。
通过查看芯片手册
了解到有如下几个寄存器

  • GPFSELx:选择gpio的功能。例如FSEL9就是GPIO9,我们还可以看到通过设置某几位为000输入,001输出。
  • GPSETx :将gpio置1
  • GPCLRx :将GPIO清0

下面我们来使用这些寄存器,首先是GPFSELx:如果我们要将GPIO3设置成输出,根据手册上的描述只需要将寄存器GPSEL0的【11:9】位设置为 001 即可。通过查看开始寄存器偏移地址可知GPFSEL0的地址为 0x7e200000+0x00

1
2
3
//val  0xfe200000      
val &= ~(7<<9); //将val的 11~9位置0
val |= 1<< 9; //将val的 11~9位置1

设置输出的电平就更简单了,根据手册:

  • 输出高电平,寄存器GPSET0 的第n位置1,GPIOn就输出高电平。(n范围0~31)
  • 输出低电平,寄存器GPSET0 的第n位置1,GPIOn就输出低电平。(n范围0~31)其他手册上更详细。

Linux对寄存器操控

实际上Linux是不能对寄存器直接操作的,下面是我所理解到的:
Linux对寄存器操作,首先需要将那段物理地址映射进来(映射成虚拟地址~~~应该是~~~),然后你对那段逻辑地址进行操作–就像上面的位操作。操作完后需要通过一个函数将你的操作变成对寄存器操作。
我觉得我说的好抽象,能力太差,请见谅。下面我给一个简单的例子进行说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 将GPIO3设置成输出高电平
#define GPIO_BASE 0xfe200000
#define GPFSEL0 0xfe200000 + 0x00
#define GPSET0 0xfe200000 + 0x1c
// 获取GPIO对应的Linux虚拟内存地址
int gpio = gpio = ioremap(GPIO_BASE, 0xf0);

int val = ioread32(GPFSEL0); //ioread32函数从指定地址读取一个32位的值,并将其作为返回值返回。
//设置成输出模式
val &= ~(7<<9); //将val的 11~9位置0
val |= 1<< 9; //将val的 11~9位置1
//这里我其实还有点疑问,照iowrite32()的函数功能这么说,为什么不直接写到GPFSEL0里面??
//会不会我这样写是有问题的,只不过刚好GPFSEL0与gpio地址刚好一样,所有我没发现
//·有道理,后面有时间一定测试一下·
iowrite32(val, gpio); //iowrite32() 是 Linux 内核编程中用于向特定 IO 内存地址写入一个 32 位值的函数

iowrite32(1<<3, GPSET0); //设置高电平

控制任意gpio输出高低电平以及读取任意GPIO

驱动层代码如下

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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#include <linux/types.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>

#include <linux/uaccess.h>
#include <linux/io.h>

#define GPIO_BASE 0xfe200000
#define GPFSEL(n) 0x04*n // GPIO功能选择寄存器n:0~5
#define GPSET(n) 0x1c+0x04*n // GPIO置位寄存器n:0~1
#define GPCLR(n) 0x28+0x04*n // GPIO清零寄存器n:0~1
#define GPLEV(n) 0x34+0x04*n //get lev pin n:0~1
static void * gpio = 0;
static int WRITE_LED = 3;
static void gpioctl(int pin,bool statue)
{
//set pin2 output
if(pin >57||pin<0)
{
printk("pin error\n");
return;
}

int reg_num,val;

reg_num = pin/10; //get GPFSEL register idnex
val = ioread32(gpio+GPFSEL(reg_num)); //set GPFSELn base

val &= ~(7 << (pin*3));
val |= 1 << (pin*3);
// GPIO bit pin 输出1
iowrite32(val, gpio);

int set_clr_Rnum = pin/32;
iowrite32(1<<pin, gpio+ (statue ?GPSET(set_clr_Rnum):GPCLR(set_clr_Rnum)));

}
int gpio_get(int pin){
int lev_Rnum = pin / 32;
int val = ioread32(gpio+GPLEV(lev_Rnum));
val &= (1<<pin);

return (val>>pin)&1;
}
#define KMAX_LEN 1024
static char readbuf[KMAX_LEN]; /*读缓冲 */
static char writebuf[KMAX_LEN]; /*写缓冲 */

static int opera_open(struct inode *inode, struct file *filp)
{
printk("printk:-helloworld_open\r\n");

return 0;
}

static int opera_release(struct inode *inode, struct file *filp)
{
printk("printk:-helloworld_release\r\n");

return 0;
}
static ssize_t opera_read(struct file *filp, __user char *buf, size_t size,
loff_t *ppos)
{
int ret = 0;
printk("printk:-helloworld_read\r\n");
if (size > KMAX_LEN)
size = KMAX_LEN;

int re = gpio_get(WRITE_LED);
if(re==1)readbuf[0] = '1';
else if(re == 0)readbuf[0] = '2';
else readbuf[0] = '3';

ret = copy_to_user(buf, readbuf, size);
if(ret < 0) {
return -EFAULT;
pr_err("copy_to_user failed!");
}

return size;
}

static ssize_t opera_write(struct file *filp, const char __user *buf,
size_t size, loff_t *ppos)
{
int ret = 0;
printk("printk:-helloworld_write\r\n");
ret = copy_from_user(writebuf, buf, size);
if(ret == 0) {
printk("printk:-kernel recevdata:%s\r\n", writebuf);
}

printk("get value\n");
*ppos = 0;

printk("%s\n",writebuf);
WRITE_LED = (int)writebuf[0];
if(writebuf[1] == '1')
{
printk("set 1\n");
gpioctl(WRITE_LED,1);
}
else if(writebuf[1] == '0')
{
printk("set 0\n");
gpioctl(WRITE_LED,0);
}
else
printk("undo\n");

return size;
}
/*
* 字符设备 操作集合
*/
static struct file_operations my_fops={
.owner = THIS_MODULE,
.open = opera_open,
.release = opera_release,
.read = opera_read,
.write = opera_write,
};

#define DEV_NAME "led" //名字
#define CLASS_NAME "led_class"
static struct cdev cdev;
static struct class *class_dev;
static struct device *device;
static dev_t devno = 0;

static int __init led_init(void)
{

printk("printk:-led init\r\n");
if(alloc_chrdev_region(&devno, 0, 1, "led")) //alloc major and minor
{
printk(KERN_ERR"failed to register kernel module!\n");
return -1;
}

cdev.owner = THIS_MODULE;
cdev_init(&cdev,&my_fops); // init cdev
cdev_add(&cdev,devno,1); //regeist to kernel
class_dev = class_create(CLASS_NAME); //regeist class
device = device_create(class_dev,NULL,devno,NULL,DEV_NAME);//regest device
printk("dev/%s create success:major is %d,minor is %d\n",DEV_NAME,MAJOR(devno),MINOR(devno));
// 获取GPIO对应的Linux虚拟内存地址
gpio = ioremap(GPIO_BASE, 0xf0);

//gpioctl(1);
return 0;
}
static void __exit led_exit(void)
{
//gpioctl(0);
iounmap(gpio);
/* 注销字符设备 */
unregister_chrdev_region(devno,1);
cdev_del(&cdev);
device_destroy(class_dev,devno);
class_destroy(class_dev);
printk("printk:-led_exit\r\n");
}

/*
模块入口0与出口
*/
module_init(led_init); /* 入口 */
module_exit(led_exit); /* 出口 */

MODULE_LICENSE("GPL");
MODULE_AUTHOR("chtlx");

应用层代码

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
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
int fd,data;

fd = open("/dev/led",O_RDWR);
if(fd<0)
{
printf("open error\n");
perror("reson:\n");
}
else
printf("success\n");

char cmd[2],buf[32];
int tmp,pin;
printf("enter contrled gpio:"); //选择控制的GPIO
scanf("%d",&pin);
cmd[0] = (char)pin;
printf("enter 1 to set,0 to clr,2 to exit:");
scanf("%d",&tmp);

while(1){
cmd[1] ='0'+ tmp;
if(cmd[1] == '2')break;
int re = write(fd,cmd,2);
printf("write %d\n",re);
re = 0;
re = read(fd, buf, 32);
printf("re :%d, mesg:%s\n",re,buf);
printf("enter 1 to set,0 to clr,2 to exit:");
scanf("%d",&tmp);
}
close(fd);

return 0;
}

使用Linux内核提供的GPIO相关API

GPIO相关API介绍

旧的方法:sysfs 接口。在 Linux 版本 4.7 之前,在用户空间中管理 GPIO 行的接口始终通过导出在 /sys/class/gpio 中的文件在 sysfs 中。在写GPIO驱动的时候,大多数使用的是以gpio
开头的函数,例如gpio_request_one, Linux 版本 4.8 开始,GPIO驱动应该使用gpiod_xxx类函数。

  • 获取gpio。可以使用 gpiod_get() 来获取一个gpio,如果要获取多个gpio,请使用 gpiod_get_index
    struct gpio_desc *gpiod_get(struct device *dev, const char *con_id,enum gpiod_flags flags)
    struct gpio_desc *gpiod_get_index(struct device *dev,const char *con_id, unsigned int idx,enum gpiod_flags flags)

    gpiod_get 参数解释:

    • struct device *dev 表示当前将要控制GPIO的设备。
    • const char *con_id 是根据GPIO映射在设备树节点的定义,其中名为 <funtion>-gpio 的属性, <funtion> 就是 con_id 需要的,详细解释见 官方文档
    • enum gpiod_flags flags flags 参数用于指定 GPIO 的方向和初始值,它可以是以下取值:
      • GPIOD_ASIS 或 0 表示根本不初始化 GPIO。必须稍后使用其中一个专用函数设置方向。
      • GPIOD_IN 将 GPIO 初始化为输入。
      • GPIOD_OUT_LOW 将 GPIO 初始化为输出,值为 0。
      • GPIOD_OUT_HIGH 将 GPIO 初始化为输出,值为 1。
      • GPIOD_OUT_LOW_OPEN_DRAIN 与 GPIOD_OUT_LOW 相同,但也强制线路在电气上采用开漏使用。
      • GPIOD_OUT_HIGH_OPEN_DRAIN 与 GPIOD_OUT_HIGH 相同,但也强制线路在电气上采用开漏使用。

    gpiod_get 使用举例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    //--------------设备树有一个gpio节点如下----------------
    mled{
    compatible = "mled";
    mled-gpios = <&gpio 3 GPIO_ACTIVE_HIGH>;
    };
    //-------通过gpiod_get()获取gpio---------
    // platform_driver 的probe
    static int my_probe(struct platform_device *pdev)
    {
    printk(KERN_INFO "%s : enter\n", __func__);
    // 获取GPIO对应的Linux虚拟内存地址
    //con_id 就是 `-gpios` 或者 `-gpio` 前面的字符串。
    gpio_out = gpiod_get(&pdev->dev,"mled",GPIOD_OUT_HIGH);
    if(IS_ERR(gpio_out))
    {
    printk(KERN_ERR "cannot get gpio_out\n");
    return -1;
    }
    //设置gpio输出高电平
    gpiod_set_value(gpio_out,1);
    return 0;
    }

    gpiod_get_index 使用方法与 gpiod_get() 类似,但是需要指定索引,详细见 此处

其余函数官方文档有很清楚的 说明

树莓派实操

设备树添加节点

根据上面我们知道,在编写驱动对gpio进行操作涉及到设备树,需要我们在设备树上添加自己的节点。
添加节点一般有两种方法

  • 一种是直接在设备树相关文件上添加,对于树莓派4B而言就是修改文件 bcm2711-rpi-4-b.dts
  • 另外一种是通过 设备树叠加(Device Tree Overlay,简称DT Overlay)是一种在基本设备树(Base Device Tree)的基础上动态添加、修改或删除硬件节点的方法。可以参考 这篇文章,不过我没有成功。

我这里使用的是第一种方法

  • 首先我们到内核源码文件下找到设备树文件位置 ,注意如果你和我版本不一样可能位置也不同,比如 这篇文章
    的设备树文件是在内核下的 arch/arm/boot/dts/
  • 打开设备树文件 bcm2711-rpi-4-b.dts,找到根节点 ,在它结束 } 的上面添加你gpio的控制节点 ,保存好后回到内核根目录下。
  • 编译设备树文件,在内核根目录下执行 sudo make dtbs (如果你是交叉编译记得加上交叉编译的工具)。如果正常的话会生成新文件
  • 覆盖当前设备树。将刚才生成的文件覆盖当前设备树。注意覆盖位置好像也不一样,我覆盖的位置是 /boot/firmware/ ,你覆盖之前最好保存备份。
    sudo cp arch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dtb /boot/firmware/bcm2711-rpi-4-b.dtb
    sudo cp arch/arm64/boot/dts/broadcom/bcm2711-rpi-400.dtb /boot/firmware/bcm2711-rpi-400.dtb
    覆盖完后重启树莓派,如何查看设备树 ls /proc/device-tree/.如果编辑设备树时添加的名字,那么就成功一半了;没有就可能是设备树文件位置找错了,或者覆盖的时候覆盖错文件了。

驱动编写

我直接给代码了,大概思路就是通过 platform 总线,将设备树节点和驱动程序匹配起来,达到操作gpio的目的。

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
#include <linux/module.h>
#include <linux/gpio/consumer.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/errno.h>
#include <linux/platform_device.h>

#include <linux/of.h>
#define DRIVER_NAME "mled"

static struct gpio_desc *gpio_out = NULL;

static int my_probe(struct platform_device *pdev)
{
printk(KERN_INFO "%s : enter\n", __func__);
// 获取GPIO对应的Linux虚拟内存地址
gpio_out = gpiod_get(&pdev->dev,"mled",GPIOD_OUT_HIGH);
if(IS_ERR(gpio_out))
{
printk(KERN_ERR "cannot get gpio_out\n");
return -1;
}
gpiod_set_value(gpio_out,1);
return 0;
}

static int my_remove(struct platform_device *pdev)
{
gpiod_set_value(gpio_out,0);
gpiod_put(gpio_out);

printk(KERN_INFO "%s : enter\n", __func__);
return 0;
}

/* 设备树匹配:这里的节点,在相应的 dts 设备树文件中添加 */
static const struct of_device_id my_of_match[] = {
{
.compatible = "mled",
},
{},
};
/* 平台驱动 : 核心是 probe函数,设备树匹配后,会调用 probe */
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.owner = THIS_MODULE,
.name = DRIVER_NAME,
.of_match_table = my_of_match,
},
};

static int __init my_driver_init(void)
{
int ret = 0;
ret = platform_driver_register(&my_driver);
if(ret<0)
{
printk(KERN_ERR "cannot register\n");
return ret;
}
printk("register kernel\n");

return 0;
}
module_init(my_driver_init)

static void __exit my_driver_exit(void)
{

platform_driver_unregister(&my_driver);
printk("exit success\n");
}
module_exit(my_driver_exit);

MODULE_AUTHOR("tlxchen");
MODULE_DESCRIPTION("example driver");
MODULE_LICENSE("GPL");

效果就是你 insmod 模块的时候gpio3会给高电平,rmmod 模块的时候gpio3会给低电平。

待写

篇幅有点太长了,就放到下一篇吧—按键控制led&如何进行消抖