OrangePi zero(H2+)的GPIO操作

下面是驱动OrangePi zero(H2+)的GPIO操作方法汇总

一、使用wiringPi的操作方法

使用该方法前需要安装WiringPi。
WiringPi的使用方式和树莓派相关工具库的操作方式类似。用起来和STM32的库函数很像。
对应管脚采用wrip的数值。在SSH界面或者UART界面,输入命令"gpio readall"(安装wiringPi才能使用这个命令),即可见到标注wrip管脚号的表格。
①在命令行直接控制:
															#安装WiringPi后使用的命令
															#设置PA6为输出模式。PA6,wrip=2
															gpio mod 2 out
															
															#设置PA6为低电平和高电平
															gpio write 2 0
															gpio write 2 1
														
②编写C文件控制:
														//gpio.c文件内容
														#include <stdio.h>
														#include <wiringPi.h>
														
														int main()
														{
														    wiringPiSetup();//需要root权限
														    //int wiringPiSetupSys(void)不需要,用/sys/class/gpio接口实现
														    //管脚号采用Broadcom的GPIO管脚号。
														    
														    //设置GPIO模式。
														    //void pinMode(int pin,int mode)
														    //参数OUTPUT、INPUT、PWM_OUTPUT、GPIO_CLOCK
														
														    //选择PA6,wrip=2
														    pinMode(2,OUTPUT);
														
														    while(1)
														    {
														        digitalWrite(2,HIGH); //PA6置高
																delay(1000);
																digitalWrite(2,LOW); //PA6置低
																delay(1000);
														    }
														    return 0;
														}
														
														//编译命令:sudo gcc -o gpio gpio.c -lwiringPi
														
														//编译命令:①sudo ②gcc ③-o ④gpio ⑤gpio.c ⑥-lwiringPi
														//对应意义:①使用root权限 ②用gcc编译 ③指定后面这个文件的名称 ④编译出的文件叫做gpio ⑤编译叫做gpio.c的文件 ⑥链接用到了的wiringpi库
														
以上的方法往往受硬件开发板的限制。比如开发板已经用来做指示灯的GPIO,在库中就没有定义,不放出给用户使用。
下面两种方法形式上会比这个方法复杂,但是几乎可以自由使用所有的GPIO。

二、使用sysfs的操作方法

sysfs是一种虚拟文件系统,这个文件系统不仅可以把设备(devices)和驱动程序(drivers) 的信息从内核输出到用户空间,也可以用来对设备和驱动程序做设置。 使用GPIO前需要把对应引脚从内核空间导出到用户空间,使用完毕后还要归还回内核空间。
GPIO内核编号的计算方法:(字母在字母表中的位置-1)*32+引脚序号。
对于PA6:A在字母表的1号位置,引脚序号是6。对应内核编号:(1-1)*32+6=6
①在命令行直接控制:
															#首先要先到/sys/class/gpio目录。
															cd /sys/class/gpio
															
															#将GPIO释放到用户空间
															echo 6 > export
															
															#进入到导出的GPIO口对应的文件夹
															cd ./gpio6
															
															#给具体的GPIO6(PA6)设置方向
															echo out > direction
															
															#给具体的GPIO6(PA6)设置输出电平
															echo 1 > value #输出高电平
															echo 0 > value #输出低电平
															
															#归还导出的GPIO
															echo 6 > unexport
														
②编写C文件控制:
全部代码展示
																	//gpio.c文件内容
																	#include <stdio.h>
																	#include <stdlib.h>
																	#include <stdint.h>
																	#include <string.h>
																	#include <unistd.h> //包含了关于系统操作和底层应用的函数。
																						//如系统进程,文件操作,系统信息和系统常量
																	#include <fcntl.h> //关于文件的操作
																	
																	void delay(uint16_t ms)
																	{
																	   usleep(ms*1000);
																	}
																	
																	#define BUFF_SIZE 128
																	#define LED 6 //PA6的内核编号
																	
																	int main(int argc,char *argv[])
																	{
																		char buffer[BUFF_SIZE],path[128];
																		int fd,len;
																	
																		printf("orangepi sysfs drive gpio !\n");
																	
																		/********暴露gpio到用户空间********/
																		fd = open("/sys/class/gpio/export",O_WRONLY);
																		//打开文件,成功返回文件描述符,是个整数。
																		
																		len = snprintf(buffer,BUFF_SIZE,"%d",LED);
																		//返回值是写入缓冲区的字符数,不包括字符串结尾的空字符。
																		
																		write(fd,buffer,len);
																		//三个参数(文件描述符,要写入的数据,写入的字节数)
																	
																		close(fd); //暴露相关GPIO到用户空间完成,关闭对应文件。
																		/***********************************/
																		
																		/********配置GPIO方向********/
																		snprintf(path,BUFF_SIZE,"/sys/class/gpio/gpio%d/direction",LED);
																		fd = open(path,O_WRONLY);
																		len = snprintf(buffer,BUFF_SIZE,"out");
																		write(fd,buffer,len);
																		close(fd);
																		/****************************/
																		while(1)
																		{
																			snprintf(path,BUFF_SIZE,"/sys/class/gpio/gpio%d/value",LED);
																			fd = open(path,O_WRONLY);
																			len = snprintf(buffer,BUFF_SIZE,"%d",0); //PA6输出低电平
																			write(fd,buffer,len);
																			close(fd);
																	
																			delay(1000);
																	
																			snprintf(path,BUFF_SIZE,"/sys/class/gpio/gpio%d/value",LED);
																			fd = open(path,O_WRONLY);
																			len = snprintf(buffer,BUFF_SIZE,"%d",1); //PA6输出高电平
																			write(fd,buffer,len);
																			close(fd);
																	
																			delay(1000);
																		}
																		/********隐藏GPIO到内核空间********/
																		fd = open("/sys/class/gpio/unexport",O_WRONLY);
																		len = snprintf(buffer,BUFF_SIZE,"%d",LED);
																		write(fd,buffer,len);
																		close(fd);
																		/**********************************/
																	}
																	
																	//编译命令:sudo gcc -o gpio gpio.c
																	//没有涉及其他第三方库,不用链接额外的库。
																

三、使用mmap的操作方法

mmap可以将物理设备/dev/mem映射到内存,通过读写内存的方式操作GPIO寄存器,修改了映射到内存的数据会自动更新到对应的寄存器。使用起来速度比sysfs更快。
主要涉及到mmap和munmap函数。
															void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offset);
															int munmap(void *start,size_t length);
														
参数介绍:①start:映射到内存上的地址。设置为NULL,则系统自动在内存上分配。如果mmap函数执行成功,函数的返回值也是这个地址。
②length:映射区域的长度,个人喜欢设置为页大小的整数倍。
③prot:期望的内存保护标志,不能与文件的打开模式冲突。待定的可选参数通过或运算组合。
														PROT_EXEC //页内容可以被执行
														PROT_READ //页内容可以被读取
														PROT_WRITE //页可以被写入
														PROT_NONE //页不可访问
														
④flags:指定映射对象的类型,映射选项和映射页是否可以共享。其值可以是一下选项的组合。
															MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重复部分将会被丢弃。如果指定的地址不可用,操作将会失败。
															//并且起始地址必须落在页的边界上
															MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用。文件实际上不会被更新。
															MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
															MAP_DENYWRITE //这个标志被忽略。
															MAP_EXECUTABLE //同上
															MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
															MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。
															MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。
															MAP_ANONYMOUS//匿名映射,映射区不与任何文件关联。
															MAP_FILE //兼容标志,被忽略。
															MAP_32BIT //将映射区放在进程地址空间的低2GB, MAP_FIXED指定时会被忽略。当前这个标志只在X86-64平台上得到支持。
															MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
															MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。
														
⑤fd:有效的文件描述符。一般由open()函数返回。也可以设置为-1,此时需要设置flags的参数为MAP_ANON,表明进行的是匿名映射。
⑥offset:被映射对象内容的起点地址。设置为页的整数倍。对应的算法见下代码。
														#define PAGE_SIZE 4096UL
														#define PAGE_MASK (PAGE_SIZE-1)
														#define GPIO_BASE_ADDR 0x01C20800
														//GPIO_BASE_ADDR & ~PAGE_MASK
														//以上运算结果就是小于GPIO_BASE_ADDR且可以被4096整除的数字
														//GPIO_BASE_ADDR & PAGE_MASK
														//以上运算结果是映射地址到实际需要地址的距离
														
														//GPIO_BASE_ADDR & ~PAGE_MASK等效于下面函数的作用
														//即判断被映射的地址是否可以被页大小整除。如果不能则计算一个最近的,可以被页大小整除的地址。
														uint32_t UsedInMmap_target_address(uint32_t address)
														{
															uint32_t i,j;
															i = address % PAGE_SIZE;
															if(i==0) 
															{
																return address;
															}
															else if(i!=0)
															{
																j = address / PAGE_SIZE;
																return PAGE_SIZE * j;
															} 
														}
														
接下来需要通过芯片的数据手册获得被映射寄存器的地址和相关的偏移量。
全志H2+的GPIO寄存器信息在“Chapter 4 System”的“4.22 Port Controller(CPU-PORT)”项目下。下图是GPIO的基地址。

下面是各个gpio具体的寄存器表格的开始一个。下图可以得到PA配置寄存器的默认值0x77777777。寄存器对基地址的偏移0x00。以及每个引脚在寄存器中配置的数据含义。

找到所有所需的GPIO寄存器即可开始编程。每类GPIO大致对应4个寄存器:配置寄存器,数据寄存器,多驱动寄存器和上拉设置寄存器。
全部代码展示
																	//gpio.c文件内容
																	#include <stdio.h>
																	#include <stdint.h>
																	#include <fcntl.h>
																	#include <unistd.h>
																	#include <sys/mman.h> //用于内存映射
																	#include <errno.h> //提供了一种错误报告机制
																	
																	#define PAGE_SIZE 4096UL //UL表示“无符号长整型”
																	#define PAGE_MASK (PAGE_SIZE  - 1)
																	
																	
																	/**TEST PA6**/
																	#define GPIO_BASE_ADDR 0x01C20800
																	#define PA0_PA7_CFG0_REG_OFFSET 0x00
																	#define PA8_PA15_CFG1_REG_OFFSET 0x04
																	#define PA16_PA21_CFG2_REG_OFFSET 0x08
																	#define PA_CFG3_REG_OFFSET 0x0C //保留
																	#define PA_DATA_REG_OFFSET 0x10 //未了解功能和作用
																	#define PA0_PA15_DRV0_REG_OFFSET 0x14 //未了解功能和作用 对不同外设功能的驱动?
																	#define PA16_PA21_DRV1_REG_OFFSET 0x18 //未了解功能和作用
																	#define PA0_PA15_PULL0_REG_OFFSET 0x1C
																	#define PA16_PA21_PULL1_REG_OFFSET 0x20
																	
																	unsigned int *PA6_CFG0_REG;
																	unsigned int *PA6_DATA_REG;
																	unsigned int *PA6_DRV0_REG;
																	unsigned int *PA6_PULL0_REG;
																	
																	
																	unsigned long map_size(int multiple)
																	{
																		return (PAGE_SIZE * multiple);
																	}
																	
																	/*
																	uint32_t UsedInMmap_target_address(uint32_t address)
																	{
																		uint32_t i,j;
																		i = address % PAGE_SIZE;
																		if(i==0) 
																		{
																		return address;
																		}
																		else if(i!=0)
																		{
																			j = address / PAGE_SIZE;
																		return PAGE_SIZE * j;
																		} 
																	}
																	*/
																	//以上函数的简单替代
																	//GPIO_BASE_ADDR & ~PAGE_MASK
																	//结果就是小于GPIO_BASE_ADDR且可以被4096整除的数字
																	//GPIO_BASE_ADDR & PAGE_MASK
																	//结果是映射地址到实际需要地址的距离
																	
																	unsigned int SETorCLR_BIT_32(unsigned int data32,unsigned char bit,unsigned char set_data)
																	{
																		if(set_data == 1)
																		{
																			data32 |= (1 << bit);
																			return data32;
																		}
																		else if(set_data == 0)
																		{
																			data32 &= ~(1 << bit);
																			return data32;
																		}
																		else
																		{
																			printf("SETorCLR_BIT_32 set ERROR!\n");
																		}
																	}
																	
																	int main(int argc,char *argv[])
																	{
																		void *mmap_addr;
																		int mem_fd = -1;
																	
																		mem_fd = open("/dev/mem",O_RDWR | O_SYNC);
																		if(mem_fd < 0)
																		{
																			printf("Unable to open /dev/mem\n");
																			return -1;
																		}
																	
																		mmap_addr = mmap(NULL,map_size(1),PROT_READ | PROT_WRITE,MAP_SHARED,mem_fd,GPIO_BASE_ADDR & ~PAGE_MASK
																				);
																		if(mmap_addr == MAP_FAILED)
																		{
																			printf("Mmap failed errno = %d\n",errno);
																			close(mem_fd);
																			return -1;
																		}
																	
																		PA6_CFG0_REG = mmap_addr + (GPIO_BASE_ADDR & PAGE_MASK) + PA0_PA7_CFG0_REG_OFFSET;
																		PA6_DATA_REG = mmap_addr + (GPIO_BASE_ADDR & PAGE_MASK) + PA_DATA_REG_OFFSET;
																		PA6_DRV0_REG = mmap_addr + (GPIO_BASE_ADDR & PAGE_MASK) + PA0_PA15_DRV0_REG_OFFSET;
																		PA6_PULL0_REG = mmap_addr + (GPIO_BASE_ADDR & PAGE_MASK) + PA0_PA15_PULL0_REG_OFFSET;
																	
																		//32位数据清零某一n位
																		//32bit_data &= ~(1 << n);
																		//32位数据置一某一n位
																		//32bit_data |= (1 << n);
																		printf("PA6 Output setting...\n");
																		//设置PA6为输出模式
																		printf("Old CFG0_REG : %#X\n",*PA6_CFG0_REG);
																		*PA6_CFG0_REG = SETorCLR_BIT_32(SETorCLR_BIT_32(*PA6_CFG0_REG,25,1),26,1); //0x7177 7777
																		printf("New CFG0_REG : %#X\n",*PA6_CFG0_REG);
																		//设置PA6出输高电平
																		printf("Old PA6_DATA_REG : %#X\n",*PA6_DATA_REG);
																		*PA6_DATA_REG = SETorCLR_BIT_32(*PA6_DATA_REG,6,0);//0x0000 0020
																		printf("New PA6_DATA_REG : %#X\n",*PA6_DATA_REG);
																		//PA6_DRV_REG:多驱动寄存器,用于控制引脚的驱动强度。这里默认。
																		printf("Old DRV0_REG : %#X\n",*PA6_DRV0_REG);
																		*PA6_DRV0_REG = *PA6_DRV0_REG;
																		printf("New DRV0_REG : %#X\n",*PA6_DRV0_REG);
																		//PA6_PULL0_REG:用于控制IO的上拉和下拉。这里对于输出时默认即可。
																		printf("Old PULL0_REG : %#X\n",*PA6_PULL0_REG);
																		*PA6_PULL0_REG = SETorCLR_BIT_32(SETorCLR_BIT_32(*PA6_PULL0_REG,12,0),13,0);
																		printf("New PULL0_REG : %#X\n",*PA6_PULL0_REG);
																	
																		//最后一定记得关闭文件和解除内存映射
																		close(mem_fd);
																		munmap(mmap_addr,map_size(1));
																	
																		return 0;
																	}
																	
																	//编译命令:sudo gcc -o gpio gpio.c
																
可见有了mmap操作方法就可以操作各类寄存器了。