GPIO, Physical Computing

添加字符设备驱动程序

Posted by Yym on June 3, 2016

GPIO

GPIO 引脚是树莓派与外部世界之间的物理接口,树莓派可以通过这些引脚连接外部电路来控制和监视外部世界,我们称之为物理计算。在 Pi3 上有 40 个 GPIO 的引脚,它们提供不同的功能,在树莓派网站有详细的介绍。

对于 GPIO 的控制和访问,树莓派平台有丰富的库函数提供支持,Wiring Pi 和 RPi.GPIO 等都提供了很好的封装,同时 Linux 内核中也包含了 GPIO 的驱动程序,可以很方便的使用。

Wiring Pi

WiringPi 是应用于树莓派平台的 GPIO 控制库函数,它遵守 GUN Lv3 并且适用于 C/C++ 开发。

Blink 相当于 GPIO 接口的世界里的 “Hello World” ,因为可以以最简单的程序和电路让你了解发生了什么。

编写一个测试程序:

#include <wiringPi.h>
int main (void)
{
  wiringPiSetup () ;
  pinMode (0, OUTPUT) ;
  for (;;)
  {
    digitalWrite (0, HIGH) ; delay (500) ;
    digitalWrite (0,  LOW) ; delay (500) ;
  }
  return 0 ;
}

编译并执行:

gcc -Wall -o blink blink.c -lwiringPi
sudo ./blink

连接电路,将一个 LED 灯与 GPIO 引脚相连并接地,如果一切正常,就可以看到 LED 每秒闪烁一次。

MAX7219

MAX7219 是一种集成化的串行输入输出共阴极显示驱动器,它连接微处理器与 8 位数字的 7 段数字 LED 显示,也可以连接条线图显示器或者64个独立的LED。

寻址

数据寄存器由一个在片上的 8*8 的双向 SRAM 来实现。它们可以直接寻址,每个数据都可以独立的修改或保存。控制寄存器包括编码模式、显示亮度、扫描限制、关闭模式以及显示检测五个寄存器。

串行地址格式

DIN 引脚串行输入数据,CLK 上升沿控制每一位的写入。

void Write_Max7219_byte(uchar DATA) {
	for (int i = 0; i < 8 ; i++) {
		digitalWrite(Max7219_pinCLK, LOW);
		digitalWrite (Max7219_pinDIN, DATA & 0x80) ;
		DATA = DATA << 1;
		digitalWrite(Max7219_pinCLK, HIGH);
	}
}

每次输入的数据分为 8 位选址和 8 位数据,16 位数据在 CS 上升沿被写入数据寄存器或控制寄存器。

//功能:向 MAX7219 写入数据
//参数:地址,数据
void Write_Max7219(uchar address, uchar dat) {
	digitalWrite (Max7219_pinCS, LOW) ;
	Write_Max7219_byte(address);           //写入地址
	Write_Max7219_byte(dat);               //写入数据
	digitalWrite (Max7219_pinCS, HIGH) ;
}
初始化

程序开始,首先写控制寄存器,设置一些必要的参数:

void Init_MAX7219(void) {
	Write_Max7219(0x09, 0x00);       //译码方式:BCD码
	Write_Max7219(0x0a, 0x03);       //亮度
	Write_Max7219(0x0b, 0x07);       //扫描界限;8个数码管显示
	Write_Max7219(0x0c, 0x01);       //掉电模式:0,普通模式:1
	Write_Max7219(0x0f, 0x00);       //显示测试:1;测试结束,正常显示:0
}
显示字库

使用 8*8 点阵字模,循环显示 0-9/a-z:

int  main() {
	// 配置 GPIO
	wiringPiSetup () ;
	pinMode (Max7219_pinCLK, OUTPUT) ;
	pinMode (Max7219_pinCS, OUTPUT) ;
	pinMode (Max7219_pinDIN, OUTPUT) ;

	// 配置 MAX7219
	Init_MAX7219();

	// 循环显示字符
	while (1) {
		for (int j = 0; j < 36; j++) {
			for (int i = 0; i < 8; i++)
				Write_Max7219(i + 1, disp1[j][i]);
			delay(1000);
		}
	}
	return 0;
}

GPIO 驱动

Linux 内核中的驱动程序实现了一套函数,内核中的其他部分可以利用它们来建立使用 GPIO 某种具体的硬件设备的驱动程序。

引入头文件:

#include <linux/gpio.h>

每个 GPIO 引脚被赋予唯一的正整数值,可以测试某个引脚是否可用:

int gpio_is_valid(int number);

如果测试某个引脚不在可用状态,首先要分配这个引脚:

int gpio_request(unsigned gpio, const char *label);

之后要规定引脚的方向,即输入还是输出:

/* set as input or output, returning 0 or negative errno */
int gpio_direction_input(unsigned gpio);
int gpio_direction_output(unsigned gpio, int value);

读取或设置引脚有两种方式,其中一种是利用自旋锁的方式,这种方式不会进入休眠状态:

/* GPIO INPUT:  return zero or nonzero */
int gpio_get_value(unsigned gpio);

/* GPIO OUTPUT */
void gpio_set_value(unsigned gpio, int value);

或者另一种允许睡眠的方式:

/* GPIO INPUT:  return zero or nonzero, might sleep */
int gpio_get_value_cansleep(unsigned gpio);

/* GPIO OUTPUT, might sleep */
void gpio_set_value_cansleep(unsigned gpio, int value);

在某个 GPIO 使用完毕后,可以释放之前请求的引脚:

/* release previously-claimed GPIO */
void gpio_free(unsigned gpio);

设备驱动

为系统添加一个设备驱动程序,直接访问 GPIO 控制寄存器,能将写入设备文件中的字符在矩阵上显示出来。

我们以内核模块的方式将驱动程序插入到内核,设备相当于 /dev/ 文件夹下的一个文件,向该文件中写入的内容会自动传送给设备驱动程序进行处理,设备驱动程序负责控制外部设备并交换数据。

由于内核态只能调用内核中的函数,正好 Linux 内核中就自带了 GPIO 驱动函数,可以很方便地使用。

模块初始化

在内核模块加载回调函数中,需要进行设备的注册和 GPIO 端口的初始化:

static int __init hello_init(void) {
    misc_register(&MAX7219_misc_device);
    gpio_request(Max7219_pinDIN, "sysfs");
    gpio_direction_output(Max7219_pinDIN, 0);
    gpio_request(Max7219_pinCS, "sysfs");
    gpio_direction_output(Max7219_pinCS, 1);
    gpio_request(Max7219_pinCLK, "sysfs");
    gpio_direction_output(Max7219_pinCLK, 0);
    MAX7219_init();
    printk("MAX7219 device has been registed.\n");
    return 0;
}
注册设备

在 Linux 驱动中把无法归类的五花八门的设备定义为杂项设备,杂项设备是在嵌入式系统中用得比较多。

杂项设备结构体声明在 include/linux/miscdevice.h 中:

struct miscdevice  {
    int minor;
    const char *name;
    const struct file_operations *fops;
    struct list_head list;
    struct device *parent;
    struct device *this_device;
    const struct attribute_group **groups;
    const char *nodename;
    umode_t mode;
};

定义一个杂项设备结构体,设置设备名称为 MAX7219,其中 file_operations 结构表示注册的文件操作接口。

static struct miscdevice MAX7219_misc_device = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "MAX7219",
    .fops = &MAX7219_fops
};

向设备输入时会调用 const struct file_operations *fops; 结构中注册的文件操作接口,将 write 操作注册为新的功能函数:

static struct file_operations MAX7219_fops = {
    .owner = THIS_MODULE,
    .write = MAX7219_write,
    .llseek = noop_llseek
};

int misc_register(struct miscdevice *misc) 函数注册一个设备,传入设备相关信息的结构体:

Write 操作

每次向设备文件的输入会调用文件操作结构中的 write 函数,因此我们覆盖这个函数,设置环形缓冲区,在该函数中将每次输入的字符存入缓冲区,并调用定时显示函数,将缓冲区中的字符输出到 LED 显示:

static int MAX7219_write(struct file *file, const char __user *buffer, size_t count, loff_t *ppos) {
    int i;
    for (i = 0; i < count; i++) {
        ptr_inc(&tail);
        if (tail == head)
            ptr_inc(&head);
        queue[tail] = buffer[i];
    }
  	display();
    return count;
}
显示字符

字符显示的方式与之前一样,根据 MAX7219 的规则操纵 GPIO 端口,此时应该使用内核中的 GPIO 驱动函数:

void Write_Max7219_byte(uchar DATA) {
	uchar i;
	for (i = 0; i < 8 ; i++) {
		gpio_set_value(Max7219_pinCLK, LOW);
		gpio_set_value (Max7219_pinDIN, DATA & 0x80) ;
		DATA = DATA << 1;
		gpio_set_value(Max7219_pinCLK, HIGH);
	}
}
void Write_Max7219(uchar address, uchar dat) {
	gpio_set_value(Max7219_pinCS, LOW);
	Write_Max7219_byte(address);          
	Write_Max7219_byte(dat);           
	gpio_set_value(Max7219_pinCS, HIGH);
}

void Render_MAX7219(unsigned char* matrix) {
    int i;
    for (i = 0; i < 8; i++) {
        Write_Max7219(i + 1, matrix[i]);
    }
}
加载模块

编写 Makefile,利用 make 命令编译内核模块,insmod 命令将模块插入内核,可以看到模块加载时的输出。

测试

/dev/MAX7219 文件中写入数据并保存,LED 会自动显示输入的字符串,至此设备驱动添加成功。