驅動一般分為字符設備驅動、塊設備驅動與網絡驅動三種類型。本文主要是一個簡單字符驅動程序的實現,主要涉及三個部分,即外圍驅動、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函數提供模式的選取。
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位 }
該函數實現對模式的選取,不用編譯進內核,不需要考慮幻數、序列、傳送方向以及參數的大小,代碼如下:
switch(cmd) { case 0: port = 0; //FID break; case 1: port = 1; //VSK break; default: return -EINVAL; }
主要實現設備的注冊(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);
主要實現設備在系統中的刪除(cdev_del函數)、釋放注冊的設備(unregister_chrdev_region函數)以及移除操作(device_destroy及class_destroy函數),你會發現函數的執行順序與s3c6410_tlv5618aid_init裡相對應的函數執行順利一致。
該結構體用來存儲驅動內核模塊提供的對設備各種操作的函數指針,file_operations結構體本該有許多函數,但只需實現需要用到的函數。本文只實現了2個函數:tlv5618aid_ioctl與tlv5618aid_write函數。
驅動源代碼如下:
#include#include #include #include #include #include #include #include #include #include #include #include #include #include #include #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表示字符設備的一個結構體,頭文件 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); //目的得到設備編號(注冊編號),第一個參數為分配設備編號范圍的起始值,第二個參數請求連續的設備編號的數量,第三個參數為與設備范圍關聯的設備名稱。函數頭文件 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的編寫可參考《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#include #include #include #include #include #include #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; }
把在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命令,會發現該模塊成功加載:
同樣在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。