1、硬件设备
GPIO:英文全称为General-Purpose IO ports,也就是通用IO口。嵌入式系统中常常有数量众多,但是结构却比较简单的外部设备/电路,对这些设备/电路有的需要CPU为之提供控制手段,有的则需要被CPU用作输入信号。而且,许多这样的设备/电路只要求一位,即只要有开/关两种状态就够了,比如灯亮与灭。对这些设备/电路的控制,使用传统的串行口或并行口都不合适。所以在微控制器芯片上一般都会提供一个“通用可编程IO接口”,即GPIO。
从电路图中看出,GPIO低电平有效(LED亮)。
GPK共有16个端口,每个端口由三个寄存器控制,每个寄存器都是32位:
- 控制寄存器(GPKCON)
- 数据寄存器(GPKDAT)
- 上拉寄存器(GPKPUD)
GPIO接口至少有两个寄存器,即“通用IO控制寄存器”与“通用IO数据寄存器”。数据寄存器的各位都直接引到芯片外部,而对这种寄存器中每一位的作用,即每一位的信号流通方向,则可以通过控制寄存器中对应位独立的加以设置。GPxCONn 是 GPIO的控制寄存器,GPxDAT是GPIO数据寄存器。
GPK的控制寄存器分为两个,GPKCON0控制0~7号端口,GPKCON1控制8~15号端口。
控制寄存器连续的4位控制一个端口,如下图中LED对应的控制寄存器GPK4~GPK7。
GPK一共有16个控制位,每个控制位需要4位配置,所以共需要 16 * 4 / 32 = 2个32位的控制寄存器,即GPKCON0和GPKCON1。GPKDAT只有[0:15]是有用的。GPKPUD是上拉电阻寄存器。
上拉电阻作用在于,当GPIO 引脚处于第三种状态时候,既不是输出高电平,也不是输出低电平。而是呈现高阻态,相当于没有接芯片。它的电平状态由上下拉电阻决定。
要点亮LED灯,GPIO必须输出低电平。作为输出,要在GPK的相应配置位配置成Output, 即”0001”。然后将数据位置成0:
拉电阻配置:
2、驱动编程
tiny6410_leds.c
/*
* Filename: tiny6410_leds.c
* Description: led driver for tiny6410
* Author: Caijinping
* E-mail: caijinping220@gmail.com
* Compile: arm-linux-gcc
* Date: 2013-12-07
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/pci.h>
#include <mach/map.h>
#include <mach/regs-gpio.h>
#include <mach/gpio-bank-k.h>
#define DEVICE_NAME "tiny6410_leds"
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Caijinping");
static long tiny6410_leds_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
switch(cmd) {
unsigned int tmp;
case 0:
case 1:
if(arg > 4) return -EINVAL;
tmp = readl(S3C64XX_GPKDAT); //read GPK io data
tmp &= ~(1 << (4 + arg)); //clear bit
tmp |= ((!cmd) << (4 + arg)); //set bit with cmd
writel(tmp, S3C64XX_GPKDAT); //write data register
return 0;
default:
return -EINVAL;
}
}
static struct file_operations dev_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = tiny6410_leds_ioctl,
};
/*misc device:杂项设备,即主设备号为10的特殊字符设备*/
static struct miscdevice misc = {
//次设备号,注意不要与/proc/misc中已有杂项设备次设备号冲突
.minor = MISC_DYNAMIC_MINOR, //MISC_DYNAMIC_MINOR来动态获取次设备号
.name = DEVICE_NAME, //设备名称
.fops = &dev_fops,
};
static int __init dev_init(void)
{
int ret;
{
unsigned int tmp;
/* GPKCON配置GPK4到GPK7配置为0001输出 */
tmp = readl(S3C64XX_GPKCON);
tmp = (tmp & ~(0xffffU << 16)) | (0x1111U << 16);
writel(tmp, S3C64XX_GPKCON);
/* GPKDAT[7:4] = 1灯灭 */
tmp = readl(S3C64XX_GPKDAT);
tmp |= (0xF << 4);
writel(tmp, S3C64XX_GPKDAT);
/* 禁止上拉下拉 */
//tmp = readl(S3C64XX_GPKPUD);
//tmp &= (0x00 << 8);
//writel(tmp, S3C64XX_GPKPUD);
}
/* 该函数会自动创建设备节点,即设备文件 */
ret = misc_register(&misc);
printk (DEVICE_NAME"\tinitialized\n");
return ret;
}
static void __exit dev_exit(void)
{
misc_deregister(&misc);
}
module_init(dev_init);
module_exit(dev_exit);
2.1. 驱动结构分析
2.1.1. tiny6410_leds_ioctl()
- cmd:表示对led的操作命令,0|1表示点亮或者关闭
- arg:表示操作的是哪个led,取值0~3,分别表示四个led灯
2.1.2. file_operations
结构体file_operations在头文件 linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。
该结构体的每个域都对应着驱动内核模块用来处理某个被请求的事务的函数的地址。
举个例子,每个字符设备需要定义一个用来读取设备数据的函数。
结构体 file_operations中存储着内核模块中执行这项操作的函数的地址。
gcc还有一个方便使用这种结构体的扩展。你会在较现代的驱动内核模块中见到。新的使用这种结构体的方式如下:
指向结构体struct file_operations的指针通常命名为fops。
struct file_operations {
struct module *owner;
loff_t(*llseek) (struct file *, loff_t, int);
ssize_t(*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t(*aio_read) (struct kiocb *, char __user *, size_t, loff_t);
ssize_t(*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t(*aio_write) (struct kiocb *, const char __user *, size_t, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t(*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t(*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t(*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void __user *);
ssize_t(*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area) (struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
};
驱动内核模块不需要实现每个函数。像视频卡的驱动就不需要从目录的结构中读取数据。相对应的file_operations中的项为NULL。
gcc还有一个方便使用这种结构体的扩展。你会在较现代的驱动内核模块中见到。新的使用这种结构体的方式如下:
struct file_operations fops = {
read: device_read,
write: device_write,
open: device_open,
release: device_release
};
同样也有C99语法的使用该结构体的方法,并且它比GNU扩展更受推荐。我使用的版本为 2.95为了方便那些想移植你的代码的人,你最好使用这种语法。它将提高代码的兼容性:
struct file_operations fops = {
.read = device_read,
.write = device_write,
.open = device_open,
.release = device_release
};
这种语法很清晰,你也必须清楚的意识到没有显示声明的结构体成员都被gcc初始化为NULL。
指向结构体struct file_operations的指针通常命名为fops。
2.2.3. miscdevice
misc杂项设备是主设备号为10的驱动设备,在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 char *nodename;
mode_t mode;
};
extern int misc_register(struct miscdevice * misc);
extern int misc_deregister(struct miscdevice *misc);
杂项设备框架:
static ssize_t mydev_read(struct file *filp, char __user * buf, size_t count, loff_t *f_pos)
{
}
static int mydev_ioctl( struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
return 0;
}
static struct file_operations mydev_fops = { //虚拟文件系统
.owner = THIS_MODULE,
.ioctl = mydev_ioctl,
.read = mydev_read,
};
static struct miscdevice misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME,
.fops = &mydev_fops,
};
static int mydev_init(void)
{
int ret;
ret = misc_register(&misc);
return ret;
}
static void mydev_exit(void)
{
misc_deregister(&misc);
}
module_init(mydev_init); //模块加载函数
module_exit(mydev_exit); //模块卸载函数
MODULE_LICENSE("Dual BSD/GPL"); //模块声明
2.2. Makefile
ARCH := arm
COMPILE := arm-linux-
ifneq ($(KERNELRELEASE),)
obj-m := tiny6410_leds.o
else
KDIR := /opt/FriendlyARM/tiny6410/linux/linux-2.6.38
PWD := $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules ARCH=$(ARCH) CROSS_COMPILE=$(COMPILE)
clean:
rm -rf *.ko *.o *.mod.c *.symvers *.cmd *.order *~ .tiny6410_leds* .tmp_versions
endif
执行make,编译得到tiny6410_leds.ko文件。并把此文件拷贝到/lib/modules/2.6.38-FriendlyARM文件夹
安装杂项驱动:
检查设备驱动:
此时已经可以使用设备tiny6410_leds。
3、编写应用程序
led.c
/*
* Filename: led.c
* Description: led driver test for tiny6410
* Author: Caijinping
* E-mail: caijinping220@gmail.com
* Compile: arm-linux-gcc
* Date: 2013-12-07
*/
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/ioctl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#define DEVICE_NAME "/dev/tiny6410_leds"
int main(int argc, char* argv[])
{
int on;
int led_no;
int fd;
if (argc != 3 || sscanf(argv[1], "%d", &led_no) != 1 || sscanf(argv[2], "%d", &on) != 1 || on < 0|| on > 1 || led_no < 0 || led_no > 3) {
fprintf(stderr,"Usage: leds led_no 0|1\n");
exit(1);
}
fd =open(DEVICE_NAME, 0);
if (fd < 0) {
perror("open device leds");
exit(1);
}
ioctl(fd,on, led_no);
close(fd);
return 0;
}
用arm-linux-gcc编译后,生成led可执行文件,并把它拷贝到开发板上。
注意:在运行程序之前先关闭led服务器。
/etc/rc.d/init.d/leds stop