前言
主要是在嵌入式Linux(树莓派)中如何使用已有的函数库编写应用程序操纵GPIO,如何编写字符设备驱动程序在内核程序中使用GPIO
硬件连接图
虚拟文件系统操作GPIO
Linux可以通过访问sys/class/gpio下的一些文件,通过对这些文件的读写来实现对于GPIO的访问。
树莓派下面的可用的GPIO如下图所示,需要注意树莓派一代和二代的区别
首先用一个小灯来测试下操作。首先向export中写入18,表示启用18号gpio端口,执行之后,可以看到该目录下多出了一个gpio18的目录。进入该目录后,先direction中写入out,表示该gpio作为输出,然后向value文件中写入1,表示其输出1,小灯的一端接gpio18,另一端接地,就可以看到小灯点亮。实验成功。
将其改装成用C操作文件的方式进行,这里我写了一个简单的库,包括gpio的export和unexport以及read和write
1 //gpio_sys.h 2 #ifndef _GPIO_SYS_H_ 3 #define _GPIO_SYS_H_ 4 5 #include <stdio.h> 6 #include <stdlib.h> 7 8 #define BUFFMAX 3 9 #define SYSFS_GPIO_EXPORT "/sys/class/gpio/export" 10 #define SYSFS_GPIO_UNEXPORT "/sys/class/gpio/unexport" 11 #define SYSFS_GPIO_DIR_IN 0 12 #define SYSFS_GPIO_DIR_OUT 1 13 #define SYSFS_GPIO_VAL_HIGH 1 14 #define SYSFS_GPIO_VAL_LOW 0 15 16 #define ERR(args...) fprintf(stderr, "%s\n", args); 17 18 int GPIOExport(int pin); 19 int GPIOUnexport(int pin); 20 int GPIODirection(int pin, int dir); 21 int GPIORead(int pin); 22 int GPIOWrite(int pin, int value); 23 24 #endif
1 //gpio_sys.c 2 #include "gpio_sys.h" 3 #include <sys/stat.h> 4 #include <sys/types.h> 5 #include <fcntl.h> 6 #include <unistd.h> 7 #include <string.h> 8 9 int GPIOExport(int pin) 10 { 11 char buff[BUFFMAX]; 12 13 int fd; 14 if((fd=open(SYSFS_GPIO_EXPORT, O_WRONLY)) == -1) 15 { 16 ERR("Failed to open export for writing!\n"); 17 return -1; 18 } 19 20 int len = snprintf(buff, sizeof(buff), "%d", pin); 21 if(write(fd, buff, len) == -1) 22 { 23 ERR("Failed to export gpio!\n"); 24 return -1; 25 } 26 27 if(close(fd) == -1) 28 { 29 ERR("Failed to close export!\n"); 30 return -1; 31 } 32 return 0; 33 } 34 35 int GPIOUnexport(int pin) 36 { 37 char buff[BUFFMAX]; 38 int fd; 39 if((fd=open(SYSFS_GPIO_UNEXPORT, O_WRONLY)) == -1) 40 { 41 ERR("Failed to open unexport for writing!\n"); 42 return -1; 43 } 44 45 int len = snprintf(buff, sizeof(buff), "%d", pin); 46 if(write(fd, buff, len) == -1) 47 { 48 ERR("Failed to unexport gpio!\n"); 49 return -1; 50 } 51 52 if(close(fd) == -1) 53 { 54 ERR("Failed to close unexport!\n"); 55 return -1; 56 } 57 return 0; 58 } 59 60 int GPIODirection(int pin, int dir) 61 { 62 char dirCh[][5] = {"in", "out"}; 63 char path[64]; 64 65 int fd; 66 snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/direction", pin); 67 printf(path); 68 if((fd = open(path, O_WRONLY)) == -1) 69 { 70 ERR("Failed to open direction for writing!\n"); 71 return -1; 72 } 73 74 if(write(fd, dirCh[dir], strlen(dirCh[dir])) == -1) 75 { 76 ERR("Failed to set direction!\n"); 77 return -1; 78 } 79 80 if(close(fd) == -1) 81 { 82 ERR("Failed to close direction!\n"); 83 return -1; 84 } 85 return 0; 86 } 87 88 int GPIORead(int pin) 89 { 90 char path[64]; 91 char buff[BUFFMAX]; 92 93 snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", pin); 94 95 int fd; 96 if((fd == open(path, O_RDONLY)) == -1) 97 { 98 ERR("Failed to open value for reading!\n"); 99 return -1; 100 } 101 102 if(read(fd, buff, sizeof(buff)) == -1) 103 { 104 ERR("Failed to read value!\n"); 105 return -1; 106 } 107 108 if(close(fd) == -1) 109 { 110 ERR("Failed to close value!\n"); 111 return -1; 112 } 113 114 return atoi(buff); 115 } 116 117 int GPIOWrite(int pin, int value) 118 { 119 char path[64]; 120 char valuestr[][2] = {"0", "1"}; 121 122 if(value != 0 && value != 1) 123 { 124 fprintf(stderr, "value = %d\n", value); 125 ERR("Writing erro value!\n"); 126 return -1; 127 } 128 snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", pin); 129 int fd; 130 if((fd = open(path, O_WRONLY)) == -1) 131 { 132 ERR("Failed to open value for writing!\n"); 133 return -1; 134 } 135 136 if(write(fd, valuestr[value], 1) == -1) 137 { 138 ERR("Failed to write value!\n"); 139 return -1; 140 } 141 142 if(close(fd) == -1) 143 { 144 ERR("Failed to close value!\n"); 145 return -1; 146 } 147 return 0; 148 }
然后编写MAX_7219的显示程序。
MAX_7219其输出引DIG0-7和SEG A-G连接了8*8的LED矩阵,而我们关心的则是其输入的5个引脚,分别是5V的VCC和接地GND,以及时钟信号CLK,片选信号CS,串行数据输入端口DIN。
MAX_7219使用前需要对一些寄存器进行初始化设置,包括编码寄存器,亮度寄存器,模式寄存器以及显示检测寄存器。
MAX_7219的数据的写入是在时钟上升沿写入的,连续数据的后16位被移入寄存器中,其中8-11为地址,0-7位为数据。
在了解了MAX_7219的工作原理之后就可以使用上面的gpio的操纵函数进行字母的显示了。下面给出的是在8*8的矩阵上依次显示0-9,a-z以及中国字样的程序。
采用的GPIO为gpio18,gpio23和gpio24分别连接CLK,CS以及DIN。
首先需要对于树莓派的GPIO口进行一些配置
1 void Init_MAX7219(void) 2 { 3 Write_Max7219(0x09, 0x00); //encoding with BCD 4 Write_Max7219(0x0a, 0x03); //luminance 5 Write_Max7219(0x0b, 0x07); //scanning bound 6 Write_Max7219(0x0c, 0x01); //mode: normal 7 Write_Max7219(0x0f, 0x00); 8 }
初始化MAX_7219的一些寄存器,设置其译码方式为BCD译码,亮度为3,扫描界限为8个数码管显示,采用普通模式,正常显示。
1 void Init_MAX7219(void) 2 { 3 Write_Max7219(0x09, 0x00); //encoding with BCD 4 Write_Max7219(0x0a, 0x03); //luminance 5 Write_Max7219(0x0b, 0x07); //scanning bound 6 Write_Max7219(0x0c, 0x01); //mode: normal 7 Write_Max7219(0x0f, 0x00); 8 }
然后编写数据写入函数,其首先将地址写入,然后在将数据写入。其中写入的顺序为从高位到低位按位依次写入即可。写入的时候首先将CLK设为低电平,然后使得数据有限,然后将CLK设为高电平,让数据在上升沿时写入。
1 void Write_Max7219_byte(uchar DATA) 2 { 3 uchar i; 4 GPIOWrite(pinCS, SYSFS_GPIO_VAL_LOW); 5 for(i=8;i>=1;i--) 6 { 7 GPIOWrite(pinCLK, SYSFS_GPIO_VAL_LOW); 8 GPIOWrite(pinDIN, (DATA&0x80) >> 7); 9 DATA <<= 1; 10 GPIOWrite(pinCLK, SYSFS_GPIO_VAL_HIGH); 11 } 12 } 13 14 void Write_Max7219(uchar address,uchar dat) 15 { 16 GPIOWrite(pinCS, SYSFS_GPIO_VAL_LOW); 17 Write_Max7219_byte(address); //writing address 18 Write_Max7219_byte(dat); //writing data 19 GPIOWrite(pinCS, SYSFS_GPIO_VAL_HIGH); 20 } 21 22 void Init_MAX7219(void) 23 { 24 Write_Max7219(0x09, 0x00); //encoding with BCD 25 Write_Max7219(0x0a, 0x03); //luminance 26 Write_Max7219(0x0b, 0x07); //scanning bound 27 Write_Max7219(0x0c, 0x01); //mode: normal 28 Write_Max7219(0x0f, 0x00); 29 }
最后将上述过程拼接起来就可以了。
连接线路然后运行就可以看到字符输出了,下面列出了8, W,中的显示图像。
1 #define uchar unsigned char 2 #define uint unsigned int 3 4 #define pinCLK 18 5 #define pinCS 23 6 #define pinDIN 24 7 8 uchar codeDisp[38][8]={ 9 {0x3C,0x42,0x42,0x42,0x42,0x42,0x42,0x3C},//0 10 {0x10,0x18,0x14,0x10,0x10,0x10,0x10,0x10},//1 11 {0x7E,0x2,0x2,0x7E,0x40,0x40,0x40,0x7E},//2 12 {0x3E,0x2,0x2,0x3E,0x2,0x2,0x3E,0x0},//3 13 {0x8,0x18,0x28,0x48,0xFE,0x8,0x8,0x8},//4 14 {0x3C,0x20,0x20,0x3C,0x4,0x4,0x3C,0x0},//5 15 {0x3C,0x20,0x20,0x3C,0x24,0x24,0x3C,0x0},//6 16 {0x3E,0x22,0x4,0x8,0x8,0x8,0x8,0x8},//7 17 {0x0,0x3E,0x22,0x22,0x3E,0x22,0x22,0x3E},//8 18 {0x3E,0x22,0x22,0x3E,0x2,0x2,0x2,0x3E},//9 19 {0x8,0x14,0x22,0x3E,0x22,0x22,0x22,0x22},//A 20 {0x3C,0x22,0x22,0x3E,0x22,0x22,0x3C,0x0},//B 21 {0x3C,0x40,0x40,0x40,0x40,0x40,0x3C,0x0},//C 22 {0x7C,0x42,0x42,0x42,0x42,0x42,0x7C,0x0},//D 23 {0x7C,0x40,0x40,0x7C,0x40,0x40,0x40,0x7C},//E 24 {0x7C,0x40,0x40,0x7C,0x40,0x40,0x40,0x40},//F 25 {0x3C,0x40,0x40,0x40,0x40,0x44,0x44,0x3C},//G 26 {0x44,0x44,0x44,0x7C,0x44,0x44,0x44,0x44},//H 27 {0x7C,0x10,0x10,0x10,0x10,0x10,0x10,0x7C},//I 28 {0x3C,0x8,0x8,0x8,0x8,0x8,0x48,0x30},//J 29 {0x0,0x24,0x28,0x30,0x20,0x30,0x28,0x24},//K 30 {0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x7C},//L 31 {0x81,0xC3,0xA5,0x99,0x81,0x81,0x81,0x81},//M 32 {0x0,0x42,0x62,0x52,0x4A,0x46,0x42,0x0},//N 33 {0x3C,0x42,0x42,0x42,0x42,0x42,0x42,0x3C},//O 34 {0x3C,0x22,0x22,0x22,0x3C,0x20,0x20,0x20},//P 35 {0x1C,0x22,0x22,0x22,0x22,0x26,0x22,0x1D},//Q 36 {0x3C,0x22,0x22,0x22,0x3C,0x24,0x22,0x21},//R 37 {0x0,0x1E,0x20,0x20,0x3E,0x2,0x2,0x3C},//S 38 {0x0,0x3E,0x8,0x8,0x8,0x8,0x8,0x8},//T 39 {0x42,0x42,0x42,0x42,0x42,0x42,0x22,0x1C},//U 40 {0x42,0x42,0x42,0x42,0x42,0x42,0x24,0x18},//V 41 {0x0,0x49,0x49,0x49,0x49,0x2A,0x1C,0x0},//W 42 {0x0,0x41,0x22,0x14,0x8,0x14,0x22,0x41},//X 43 {0x41,0x22,0x14,0x8,0x8,0x8,0x8,0x8},//Y 44 {0x0,0x7F,0x2,0x4,0x8,0x10,0x20,0x7F},//Z 45 {0x8,0x7F,0x49,0x49,0x7F,0x8,0x8,0x8},//中 46 {0xFE,0xBA,0x92,0xBA,0x92,0x9A,0xBA,0xFE},//国 47 }; 48 49 void Delay_xms(uint x) 50 { 51 uint i,j; 52 for(i=0;i<x;i++) 53 for(j=0;j<50000;j++); 54 } 55 int main(void) 56 { 57 uchar i,j; 58 Delay_xms(50); 59 if(GPIOConfig() == -1) 60 { 61 ERR("Can not configure the gpio!\n") 62 return 0; 63 } 64 Init_MAX7219(); 65 while(1) 66 { 67 for(j=0;j<38;j++) 68 { 69 for(i=1;i<9;i++) 70 Write_Max7219(i,codeDisp[j][i-1]); 71 Delay_xms(1000); 72 } 73 } 74 75 if(GPIORelease() == -1) 76 { 77 ERR("Release the gpio error!\n") 78 return 0; 79 } 80 }
字符驱动程序
对于通过寄存器操作GPIO,我们就需要知道树莓派上各个GPIO端口的寄存器的地址,在树莓派的CPU(bcm2825)的芯片手册上(https://www.raspberrypi.org/wp-content/uploads/2012/02/BCM2835-ARM-Peripherals.pdf)可以查到
且由于树莓派的IO的空间的起始地址是0xF2000000,并且GPIO的偏移地址为0x200000,那么真实的GPIO的开始地址为0xF2200000,这个数据可以通过<mach/platform.h>这个头文件中的GPIO_BASE获得。
根据上面说列出的信息,就可以编写我们的对于GPIO端口的操作函数了,这里定义了对于GPIO口的function selection, set以及clear三个函数。
在网上的一篇博客中还看到可以通过树莓派提供的一些列的gpiochip的包装的函数进行操作,但是尝试了很久没有成功,驱动程序报错。编写这个程序也可以参考网上可以下载到的bcm2835的对于GPIO操作的一个bcm2835的一个开源的函数库。
1 //0->input 1-<output 2 static void bcm2835_gpio_fsel(int pin, int functionCode) 3 { 4 int registerIndex = pin / 10; 5 int bit = (pin % 10) * 3; 6 7 unsigned oldValue = s_pGpioRegisters-> GPFSEL[registerIndex]; 8 unsigned mask = 0b111 << bit; 9 printk("Changing function of GPIO%d from %x to %x\n", 10 pin, 11 (oldValue >> bit) & 0b111, 12 functionCode); 13 14 s_pGpioRegisters-> GPFSEL[registerIndex] = 15 (oldValue & ~mask) | ((functionCode << bit) & mask); 16 } 17 18 static void bcm2835_gpio_set(int pin) 19 { 20 printk("GPIO set %d\n oldValue=%d", pin, s_pGpioRegisters->GPSET[0]); 21 s_pGpioRegisters-> GPSET[pin / 32] = (1 << (pin % 32)); 22 printk("GPIO set %d\n oldValue=%d", pin, s_pGpioRegisters->GPSET[0]); 23 } 24 25 static void bcm2835_gpio_clr(int pin) 26 { 27 printk("GPIO clear %d\n oldValue=%d", pin, s_pGpioRegisters->GPCLR[0]); 28 s_pGpioRegisters-> GPCLR[pin / 32] = (1 << (pin % 32)); 29 printk("GPIO clear %d\n newValue=%d", pin, s_pGpioRegisters->GPCLR[0]); 30 }
在完成对于GPIO的操作函数之后就可以开始编写字符驱动程序了,Linux的字符驱动程序主要以模块的形式装载到内核中,然后应用程序通过文件的操作的方式对于硬件进行操作。编写一个Linux的字符驱动程序主要如下图所示:
如上图所示,首先需要做的是对于驱动进行初始化设置,在模块的初始化函数中编写如下,首先获得一个设备号(静态或者动态获取),然后进行字符设备的初始化,主要是将file_operation这个结构体中的函数和我们的定义的函数进行一个绑定,注册字符设备,最后创建得到设备。然后在进行设备的初始化,这里首先获取的是GPIO寄存器的基地址,然后通过fsel函数设置相关的三个GPIO口的模式为OUT,然后通过这三个GPIO去初始化MAX7219(同上)。
1 static struct file_operations MAX7219_cdev_fops = { 2 .owner = THIS_MODULE, 3 .open = MAX7219_open, 4 .write = MAX7219_write, 5 .release = MAX7219_release, 6 }; 7 8 static int MAX7219_init(void) 9 { 10 int ret; 11 12 MAX7219_dev_id = MKDEV(major, 0); 13 if(major) //static allocate 14 ret = register_chrdev_region(MAX7219_dev_id, 1, DRIVER_NAME); 15 else //dynamic allocate 16 { 17 ret = alloc_chrdev_region(&MAX7219_dev_id, 0, 1, DRIVER_NAME); 18 major = MAJOR(MAX7219_dev_id); 19 } 20 21 if(ret < 0) 22 return ret; 23 24 cdev_init(&MAX7219_cdev, &MAX7219_cdev_fops); //initialize character dev 25 cdev_add(&MAX7219_cdev, MAX7219_dev_id, 1); //register character device 26 MAX7219_class = class_create(THIS_MODULE, DRIVER_NAME); //create a class 27 device_create(MAX7219_class, NULL, MAX7219_dev_id, NULL, DRIVER_NAME); //create a dev 28 29 s_pGpioRegisters = (struct GpioRegisters *)__io_address(GPIO_BASE); 30 31 printk("address = %x\n", (int)__io_address(GPIO_BASE)); 32 33 //gpio configure 34 bcm2835_gpio_fsel(pinCLK, 1); 35 bcm2835_gpio_fsel(pinCS, 1); 36 bcm2835_gpio_fsel(pinDIN, 1); 37 38 //initialize the MAX7219 39 Init_MAX7219(); 40 41 printk("MAX7219 init successfully"); 42 return 0; 43 }
当这个内核模块退出的时候,需要通过device_destroy函数将这个设备删除,以便设备号可以提供给其他的设备宋,并且将其从注册中删除。
1 void MAX7219_exit(void) 2 { 3 device_destroy(MAX7219_class, MAX7219_dev_id); 4 class_destroy(MAX7219_class); 5 unregister_chrdev_region(MAX7219_dev_id, 1); 6 printk("MAX7219 exit successfully\n"); 7 }
然后就需要编写file_operations中的函数,这里主要定义了用的open,write和release(close)函数,对于read,iocnl等等就没有进行定义。
Open函数比较简单,主要就是判断下文件是否已经打开,如果已经被打开了,那么其将不能被再次打开
1 static int MAX7219_open(struct inode *inode, struct file *flip) 2 { 3 printk("Open the MAX7219 device!\n"); 4 if(state != 0) 5 { 6 printk("The file is opened!\n"); 7 return -1; 8 } 9 state++; 10 printk("Open MAX7219 successfully!\n"); 11 return 0; 12 }
Release函数和Open相反,将打开的文件关闭即可。
1 static int MAX7219_release(struct inode *inode, struct file *flip) 2 { 3 printk("Close the MAX7219 device!\n"); 4 if(state == 1) 5 { 6 state = 0; 7 printk("Close the file successfully!\n"); 8 return 0; 9 } 10 else 11 { 12 printk("The file has closed!\n"); 13 return -1; 14 } 15 }
这里主要是write函数,其将用户送来的一个字符需要显示在MAX7219上,其显示的方法和前面的虚拟文件操作基本相同,只是GPIO口操作调用的函数不同。其首先需要将用户空间传递过来的参数通过copy_from_user函数拷贝到内核空间上,然后将调用相关的显示函数进行显示即可。
对于MAX7219的显示函数这里就没有详细叙述,整个过程都和虚拟文件操作一样,只要将上面的接口改成寄存器操作的接口即可。
1 static ssize_t MAX7219_write(struct file *filp, const char __user *buf, size_t len, loff_t *off) 2 { 3 printk("Write %s into the MAX7219\n", buf); 4 int ret, i; 5 char ch; 6 if(len == 0) 7 return 0; 8 9 if(copy_from_user(&ch, (void *)buf, 1)) 10 ret = -EFAULT; 11 else 12 { 13 int index; 14 if(ch >= ‘0‘ && ch <= ‘9‘) 15 index = ch - ‘0‘; 16 else if(ch >= ‘A‘ && ch <= ‘Z‘) 17 index = ch - ‘A‘ + 10; 18 else if(ch >= ‘a‘ && ch <= ‘z‘) 19 index = ch - ‘a‘ + 10; 20 else 21 index = 36; //unknown display 中 22 printk("Write character %c, index=%d\n", ch, index); 23 for(i=0;i<8;i++) 24 Write_Max7219(i+1, codeDisp[index][i]); 25 26 ret = 1; //write a character 27 } 28 29 return ret; 30 }
编写完最后,编写Makefile文件进行编译,这个和上次实验内容相同,这里不再赘述。需要注意的是内核的模块版本号必须要和编译的相同,否则会出现ukonwn parameters之类的错误。上次做好久把内核删掉的只能重新编译一次内核了。
1 ARCH := arm 2 CROSS_COMPILE := arm-linux-gnueabi- 3 4 CC := $(CROSS_COMPILE)gcc 5 LD := $(CROSS_COMPILE)ld 6 7 obj-m := gpio_chdev.o 8 9 KERNELDIR := /home/jack/Documents/course/EmbededSystem/RaspberrySource/modules/lib/modules/4.4.11/build 10 PWD = $(shell pwd) 11 12 all: 13 make -C $(KERNELDIR) M=$(PWD) modules 14 clean: 15 rm -f *.o *.mod.c *.symvers *.order
编写如下的测试程序,以此显示A-Z以及0-9在MAX7219上面。
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <sys/ioctl.h> 5 #include <sys/time.h> 6 #include <sys/fcntl.h> 7 #include <sys/stat.h> 8 9 void Delay_xms(uint x) 10 { 11 uint i,j; 12 for(i=0;i<x;i++) 13 for(j=0;j<50000;j++); 14 } 15 16 int main(int argc, char **argv) 17 { 18 int fd; 19 int ret; 20 21 fd = open("/dev/MAX7219", O_WRONLY); 22 if (fd < 0) 23 { 24 fprintf(stderr, "Fail to open /dev/MAX7219!\n"); 25 exit(1); 26 } 27 char buff[1]; 28 int i=0; 29 for(i=0;i<26;i++) 30 { 31 buff[0] = ‘a‘ + i; 32 if((ret = write(fd, buff, 1))<0) 33 { 34 fprintf(stderr, "Fail to write /dev/MAX7219! %d\n", ret); 35 break; 36 } 37 Delay_xms(1000); 38 } 39 for(i=0;i<10;i++) 40 { 41 buff[0] = ‘0‘ + i; 42 if((ret = write(fd, buff, 1))<0) 43 { 44 fprintf(stderr, "Fail to write /dev/MAX7219! %d\n", ret); 45 break; 46 } 47 Delay_xms(1000); 48 } 49 fprintf(stdout, "Write /dev/MAX7219 successfully! %d\n", ret); 50 close(fd); 51 return 0; 52 }
参考链接:
Creating a Basic LED Driver for Raspberry Pi