一、前言
驱动一般分为字符设备驱动、块设备驱动与网络驱动三种类型。本文主要是一个简单字符驱动程序的实现,主要涉及三个部分,即外围驱动、Makefile以及测试程序的编写;在《LDD3》一书中有提到,用户空间的驱动程序有以下优缺点:
优势:
①可以和整个C库链接;②可使用普通的调试器调试驱动程序代码,不用调试正在运行的内核;③程序的崩溃不会影响系统的正常运行,简单地kill掉就OK;④和内核的内存不同,用户内存可以换出;良好设计的驱动程序仍然支持对设备的并发访问;⑤如果须编写闭源码的驱动程序,则用户空间驱动程序可更加容易地避免因修改内核接口而导致的不明确的许多问题。
缺陷:
①中断在用户空间不可用;②只能通过mmap映射才能直接访问内存;③只能调用ioperm或iopl后才能访问I/O端口;④客户端与硬件之间传递数据和动作需要上下文切换,即响应时间很慢;⑤ 如果驱动被换出到磁盘,响应速度会更慢;⑥用户空间不能处理像网络接口、块设备等重要的设备;
二、外围驱动模块部分
关于一个数模转换芯片外围驱动、Makefile以及测试程序的编写,通过向芯片写入数据来控制模拟电压的输出,由于开始使用的2片max5141/max5142(14 bit_data)芯片,每pcs只有一路输出,而且还要用到i2c总线,所以用一片2路输出的TLV5648AID(12 bit_data)替换2片max5141/max5142更方便,节约硬件资源。它们PIN如图:
驱动代码主要由4个函数构成,它们分别是:
①ssize_t tlv5618aid_write (struct file *filp, const short __user *buf_user, size_t size, loff_t *f_pos);
②static int tlv5618aid_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg);
③static int __init (void);
④static void __exit s3c6410_tlv5618aid_exit(void);
其中tlv5618aid_ioctl函数提供模式的选取。
1、tlv5618aid_write函数主要功能
5618芯片DIN传输的格式:
data为写入的数据,参考以上数据的传输格式:获取两种模式下各要处理的数据,代码如下:
data_fid_wtoa = data | 0xc000;//A端输出高位数据格式
data_vsk_wtob = data | 0x4000;//B端输出高位数据格式
ARM的GPIO与5618芯片连接的电路原理图:
5618数据传输的时序图:
注意每次操作完数据有个从低电平到高电平的转换,5141的时序就没有此要求,这算个细节问题,处理8bit数据的代码如下:
for(i = 8; i; i--)//高8位数据处理,遍历一次处理一位数据
{
gpio_set_value(S3C64XX_GPQ(6), 1);//时钟信号高电平
udelay(20);
tmp = data_high & 0x80;//取8位数据中的最高位
gpio_set_value(S3C64XX_GPQ(4), tmp);//数据在高低电平切换时保持不变,参考芯片datasheet的时序图
udelay(20);
gpio_set_value(S3C64XX_GPQ(6), 0);//时钟信号低电平
udelay(20);
data_high = data_high <<1;//数据左移1位
}
2、tlv5618aid_ioctl函数主要功能
该函数实现对模式的选取,不用编译进内核,不需要考虑幻数、序列、传送方向以及参数的大小,代码如下:
switch(cmd) {
case 0: port = 0; //FID
break;
case 1: port = 1; //VSK
break;
default:
return -EINVAL;
}
3、s3c6410_tlv5618aid_init函数主要功能
主要实现设备的注册(register_chrdev_region函数)、初始化(cdev_init函数)、添加(cdev_add函数)及创建(class_create及device_create函数),以及GPIO的配置,下面主要讲下GPIO的配置,其他部分后面的源码有注释。6410ARM板的GPIO功能配置说明书如下:
①先配置上拉:
代码如下:
tmp = __raw_readl(S3C64XX_GPOPUD);//读取原来GPIO的数据
tmp &= (~0xc00);//先把位[2*5+1,2*5]清0,其他位不变tmp=tmp & 0x0011 1111 1111
tmp |= 0x800;//再把位[2*5+1,2*5]的置为10,tmp=tmp | 0x1000 0000 0000上拉配置成功,
__raw_writel(tmp,S3C64XX_GPOPUD)
②配置成输出功能:
代码如下:
tmp = __raw_readl(S3C64XX_GPOCON);
tmp &= (~0xc00);//先把位[11,10]清0,其他位不变tmp=tmp & 0x0011 1111 1111
tmp |= 0x400;//再把位[11,10]的置为01,tmp=tmp | 0x0100 0000 0000输出模式配置成功,
__raw_writel(tmp,S3C64XX_GPOCON);
③数据的传输:
代码如下:
tmp = __raw_readl(S3C64XX_GPODAT);
tmp &= (~0x20);
tmp |= 0x20;//输出高电平1,0x10 0000,第5位输出为1(从0开始计算)
__raw_writel(tmp,S3C64XX_GPODAT);
3、s3c6410_tlv5618aid_exit函数主要功能
主要实现设备在系统中的删除(cdev_del函数)、释放注册的设备(unregister_chrdev_region函数)以及移除操作(device_destroy及class_destroy函数),你会发现函数的执行顺序与s3c6410_tlv5618aid_init里相对应的函数执行顺利一致。
4、重要的file_operations数据结构
该结构体用来存储驱动内核模块提供的对设备各种操作的函数指针,file_operations结构体本该有许多函数,但只需实现需要用到的函数。本文只实现了2个函数:tlv5618aid_ioctl与tlv5618aid_write函数。
驱动源代码如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/timer.h>
#include <linux/miscdevice.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <asm/irq.h>
#include <mach/gpio.h>
#include <plat/regs-gpio.h>
#include <plat/gpio-cfg.h>
#include <mach/hardware.h>
#include <linux/io.h>
#define DEVICE_NAME "tlv5618aid" //设备名称
#define TLV5618AID_MAJOR 231 //主设备号
static char port;
//size_t表示无符号的,即typedef unsigned (long/int) size_t(64位机/32位机,具体表示long还是int看是几位机)
//而ssize_t 对应有符号的,即typedef signed (long/int)ssize_t(64位机/32位机)
ssize_t tlv5618aid_write (struct file *filp, const short __user *buf_user, size_t size, loff_t *f_pos)
//filp为文件指针,buf_user为指向用户空间的缓冲区(传输的数据为16位,用short型,8位用char就够用了),size为请求数据的长度,offp为指向“long offset type”对象的指针,该对象为用户在文件中进行存取操作的位置
{
unsigned long tmp;
unsigned short data, data_fid_wtoa, data_vsk_wtob;
unsigned char i;//写数据的遍历次数
unsigned char data_high, data_low;//数据的高八位与低八位
data = *buf_user;//获取需要写的数据
//printk("the dataddd of buf_user = %d\n", data);
data_fid_wtoa = data | 0xc000;//A端输出高位数据格式
data_vsk_wtob = data | 0x4000;//B端输出高位数据格式
#ifdef TIMER
struct timeval start, stop;//计时结构体
unsigned int usec;
do_gettimeofday(&start);//获取以下代码执行的开始时间
printk("time = %02d.%06d\n", start.tv_sec, start.tv_usec);
#endif
if(port == 0) // 模式0_ FID
{
data_low = (char)data_fid_wtoa;//short强制转换为char型(16位数据转换为8位),得到低8位数据
data_high = (char)(data_fid_wtoa >> 8);//右移8位后再强制转换得到高8位数据
}
if(port == 1)//模式1_ VSK
{
data_low = (char)data_vsk_wtob;
data_high = (char)(data_vsk_wtob >> 8);
}
gpio_set_value(S3C64XX_GPO(5), 0);//片选该芯片,CS=0,低电平有效
udelay(20);
for(i = 8; i; i--)//高8位数据处理,遍历一次处理一位数据
{
gpio_set_value(S3C64XX_GPQ(6), 1);//时钟信号高电平
udelay(20);
tmp = data_high & 0x80;//取8位数据中的最高位
gpio_set_value(S3C64XX_GPQ(4), tmp);//数据在高低电平切换时保持不变,参考芯片datasheet的时序图
udelay(20);
gpio_set_value(S3C64XX_GPQ(6), 0);//时钟信号低电平
udelay(20);
data_high = data_high <<1;//数据左移1位
}
for(i = 8; i; i--)//低8位数据的处理
{
gpio_set_value(S3C64XX_GPQ(6), 1);
udelay(20);
tmp = data_low & 0x80;
gpio_set_value(S3C64XX_GPQ(4), tmp);
udelay(20);
gpio_set_value(S3C64XX_GPQ(6), 0);
udelay(20);
data_low = data_low << 1;
}
gpio_set_value(S3C64XX_GPQ(6), 1);//结束需要向高电平跳变一次
gpio_set_value(S3C64XX_GPO(5), 1);
#ifdef TIMER
do_gettimeofday(&stop);//获取以上代码运行结束的时间
printk("time = %02d.%06d\n", stop.tv_sec, stop.tv_usec);
if(stop.tv_usec >= start.tv_usec) {
usec = stop.tv_usec - start.tv_usec;
} else {
usec = stop.tv_usec + 1000000 - start.tv_usec;
}
printk("time lapse: = %d us\n", usec);
#endif
return 0;
}
static int tlv5618aid_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
//cmd为用户空间传递给驱动程序的命令,可选arg参数无论用户程序使用的是指针还是整数值,它都以unsigned long的形式传递给驱动程序
{
switch(cmd) {
case 0: port = 0; //FID
break;
case 1: port = 1; //VSK
break;
default:
return -EINVAL;
}
return 0;
}
static struct file_operations s3c6410_tlv5618aid_fops = {
.owner = THIS_MODULE,
.ioctl = tlv5618aid_ioctl,
.write = tlv5618aid_write,
};
//该结构体用来存储驱动内核模块提供的对设备各种操作的函数指针,file_operations结构体本该有许多函数,但只需实现需要用到的函数。
//cdev表示字符设备的一个结构体,头文件<linux/cdev.h>
static struct cdev cdev_tlv5618aid;
struct class * my_class;
static int __init s3c6410_tlv5618aid_init(void)
{
int ret;
unsigned long tmp;
dev_t devno;//dev_t表示一个32位的数,其中12位为主设备号,剩下20位为次设备号
printk(KERN_NOTICE "enter tlv5618aid_init\n");
devno = MKDEV(TLV5618AID_MAJOR,0);
//将主设备号为TLV5618AID_MAJOR,次设备号为0转换成dev_t型
ret = register_chrdev_region(devno,1,DEVICE_NAME);
//目的得到设备编号(注册编号),第一个参数为分配设备编号范围的起始值,第二个参数请求连续的设备编号的数量,第三个参数为与设备范围关联的设备名称。函数头文件<linux/fs.h>
if(ret<0)//该函数执行失败时返回小于0的值,成功执行后返回0,
{
printk(KERN_NOTICE "can not register tlv5618aid device");
return ret;
}
cdev_init(&cdev_tlv5618aid,&s3c6410_tlv5618aid_fops);
cdev_tlv5618aid.owner = THIS_MODULE;
ret =cdev_add(&cdev_tlv5618aid,devno,1);
if(ret)
{
printk(KERN_NOTICE "can not add tlv5618aid device");
return ret;
}
my_class = class_create(THIS_MODULE,"my_class_01");
//在/sys/class/下创建相对应的类目录
if(IS_ERR(my_class))
{
printk("Err: Failed in creating class\n");
return -1;
}
device_create(my_class,NULL,MKDEV(TLV5618AID_MAJOR,0),NULL,DEVICE_NAME);//完成设备节点的自动创建,当加载模块时,就会在/dev下自动创建设备文件
/* GPO5 pull up */
tmp = __raw_readl(S3C64XX_GPOPUD);//读取原来GPIO的数据
tmp &= (~0xc00);//先把位[2*5+1,2*5]清0,其他位不变tmp=tmp & 0x0011 1111 1111
tmp |= 0x800;//再把位[2*5+1,2*5]的置为10,tmp=tmp | 0x1000 0000 0000上拉配置成功,
__raw_writel(tmp,S3C64XX_GPOPUD);
/* GPO5 output mode */
tmp = __raw_readl(S3C64XX_GPOCON);
tmp &= (~0xc00);//先把位[11,10]清0,其他位不变tmp=tmp & 0x0011 1111 1111
tmp |= 0x400;//再把位[11,10]的置为01,tmp=tmp | 0x0100 0000 0000输出模式配置成功,
__raw_writel(tmp,S3C64XX_GPOCON);
/* GPO5 output 1 */
tmp = __raw_readl(S3C64XX_GPODAT);
tmp &= (~0x20);
tmp |= 0x20;//输出高电平1,0x10 0000,第5位输出为1(从0开始计算)
__raw_writel(tmp,S3C64XX_GPODAT);
/* GPQ4 and GPQ6 pull up */
tmp = __raw_readl(S3C64XX_GPQPUD);
tmp &= (~0x3300);
tmp |= 0x2200;
__raw_writel(tmp,S3C64XX_GPQPUD);
/* GPQ4 and GPQ6 output mode */
tmp = __raw_readl(S3C64XX_GPQCON);
tmp &= (~0x3300);
tmp |= 0x1100;
__raw_writel(tmp,S3C64XX_GPQCON);
/* GPQ4 and GPQ6 output 1 */
tmp = __raw_readl(S3C64XX_GPQDAT);
tmp &= (~0x50);
tmp |= 0x50;
__raw_writel(tmp,S3C64XX_GPQDAT);
//printk("S3C64XX_GPQCON is0x%08x\n",__raw_readl(S3C64XX_GPQCON));
//printk("S3C64XX_GPQDAT is0x%08x\n",__raw_readl(S3C64XX_GPQDAT));
//printk("S3C64XX_GPQPUD is0x%08x\n",__raw_readl(S3C64XX_GPQPUD));
printk(DEVICE_NAME " initialized\n");
return 0;
}
static void __exit s3c6410_tlv5618aid_exit(void)
//与s3c6410_tlv5618aid_init()函数相对应,清除函数,在设备被移除前注销接口并向系统中返回所有资源
{
cdev_del(&cdev_tlv5618aid);
//与函数cdev_add(&cdev_tlv5618aid,devno,1)相对应,从系统中移除设备
//与register_chrdev_region()相对应,释放该设备编号的函数,第一个参数为分配设备编号范围的起始值,第二个参数请求连续的设备编号的数量
device_destroy(my_class, MKDEV(TLV5618AID_MAJOR,0));//对应device_create(my_class,NULL,MKDEV(TLV5618AID_MAJOR,0),NULL,DEVICE_NAME);
class_destroy(my_class);
//对应class_create(THIS_MODULE,"my_class_01");
printk(KERN_NOTICE "tlv5618aid_exit\n");
}
module_init(s3c6410_tlv5618aid_init);
module_exit(s3c6410_tlv5618aid_exit);
MODULE_LICENSE("GPL");
三、Makefile部分
makefile的编写可参考《LDD3》一书,源码如下
obj-m := tlv5618aid.o
//模块需要从目标文件tlv5618aid.o中构建
KERNELDIR := /opt/htx-linux-2.6.xxxx
//内核树的路径,为构造可装载的模块
PWD := $(shell pwd)
CROSS := /usr/local/arm/4.2.2-eabi/usr/bin/arm-linux-
CC := $(CROSS)gcc
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
test:
$(CC) -g -Wall tlv5618aid.c -o tlv5618aid
clean:
rm -fr *mod.c *.o *.ko modules.order Module.symvers
四、测试程序部分
源码如下:
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <signal.h>
#define FID_CS 0x00
#define VSK_CS 0x01
int main(int argc, char *argv[])
{
int fd, ret;
int cmd;
char buf;
short val;// change char to short ,由传输8bit数据变成16bit
unsigned int tmp;
if (argc == 1) {
printf("Usage: ./tlv5618_test [port] [data] port:0--fid, 1--vsk. data:0~4095\n");
exit(1);
}
if(sscanf(argv[1], "%d", &cmd) != 1 ) {
printf("Check argument!\n");
exit(1);
}
if(cmd != 0 && cmd != 1) {
printf("Check the first argument!\n");
printf("Usage: ./tlv5618aid_test [port] [data] port:0--fid, 1--vsk. data:0~4095\n");
exit(1);
}
if(argc != 3) {
printf("Usage: ./tlv5618aid_test [port] [data] port:0--fid, 1--vsk. data:0~4095\n");
exit(1);
}
if((argv[2][0]=='0') && (argv[2][1]=='x')) {
sscanf(argv[2], "0x%x", &tmp);
val = tmp&0xFFF;//最大只能传输12bit数据
} else
val = atoi(argv[2]);
fd = open("/dev/tlv5618aid", O_RDWR);//change /dev/max5141
if(fd<0) {
perror("open device tlv5618aid:");
exit(1);
}
if(cmd == 0) {
ret = ioctl(fd, FID_CS, NULL);
if(ret < 0) {
perror("ioctl:");
exit(1);
}
printf("fid = %d\n", val);
} else if(cmd ==1) {
ret = ioctl(fd, VSK_CS, NULL);
if(ret < 0) {
perror("ioctl:");
exit(1);
}
printf("vsk = %d\n", val);
}
write(fd, &val, sizeof(val));
close(fd);
return 0;
}
五、代码Test
1、驱动模块的加载
把在linux上的tlv5618aid.c目录里make得到的tlv5618aid.ko文件拷贝至windows系统,再从windows通过串口上传在ARM开发板上,一般位于drivers目录下,把ko文件添加为可执行状态
chmod +x tlv5618aid.ko
insmod tlv5618aid.ko
由于手里没有新版的tlv5618aid开发板,但之前测试该驱动功能都是OK的,因此还是用老版的max5141截图演示,原理都是一样的,后期有新版的板子我再更新截图。装载之后,在/dev设备目录下就会有该设备,这是device_create函数的作用,
以及用lsmod命令,会发现该模块成功加载:
2、测试程序的运行
同样在linux可直接用arm-linux-gcc把测试程序tlv5618aid_test.c编译成可执行文件,我是在测试代码的目录下make的,其Makefile为:
tlv5618aid: tlv5618aid_test.o
/usr/local/arm/4.2.2-eabi/usr/bin/arm-linux-gcc tlv5618aid_test.c -o tlv5618aid_test
clean:
rm *.o tlv5618aid_test -rf
把可执行文件拷贝到ARM板的测试目录按命令格式执行:./tlv5618_test [port] [data],如图所示:
根据测试代码的编写即可把该功能添加到应用层界面QT。