原文鏈結:http://blogold.chinaunix.net/u3/101649/showart.php?id=2163352
嵌入式Linux之我行,主要講述和總結了本人在學習嵌入式linux中的每個步驟。一為總結經驗,二希望能給想入門嵌入式Linux的朋友提供方便。如有錯誤之處,謝請指正。
一、開發環境
- 主 機:VMWare--Fedora 9
- 開發板:Mini2440--64MB Nand, Kernel:2.6.30.4
- 編譯器:arm-linux-gcc-4.3.2
二、硬體原理分析
S3C2440內部ADC結構圖
我們從上面的結構圖和資料手冊可以知道,該ADC模組總共有8個通道可以進行類比信號的輸入,分別是AIN0、AIN1、AIN2、AIN3、YM、YP、XM、XP。那麼ADC是怎麼實現類比信號到數位信號的轉換呢?首先類比信號從任一通道輸入,然後設定寄存器中預分頻器的值來確定AD轉換器頻率,最後ADC將類比信號轉換為數位信號保存到ADC資料寄存器0中(ADCDAT0),然後ADCDAT0中的資料可以通過中斷或查詢的方式來訪問。對於ADC的各寄存器的操作和注意事項請參閱資料手冊。
上圖是mini2440上的ADC應用實例,開發板通過一個10K的電位器(可變電阻)來產生電壓類比信號,然後通過第一個通道(即:AIN0)將類比信號輸入ADC。
三、實現步驟
ADC設備在Linux中可以看做是簡單的字元設備,也可以當做是一混雜設備(misc設備),這裏我們就看做是misc設備來實現ADC的驅動。注意:這裏我們獲取AD轉換後的資料將採用中斷的方式,即當AD轉換完成後產生AD中斷,在中斷服務程式中來讀取ADCDAT0的第0-9位的值(即AD轉換後的值)。
1、建立驅動程式檔my2440_adc.c,實現驅動的初始化和退出,代碼如下:
#include <linux/errno.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/init.h> #include <linux/input.h> #include <linux/serio.h> #include <linux/clk.h> #include <linux/miscdevice.h> #include <asm/io.h> #include <asm/irq.h> #include <asm/uaccess.h>
/*定義了一個用來保存經過虛擬映射後的記憶體位址*/ static void __iomem *adc_base;
/*保存從平臺時鐘佇列中獲取ADC的時鐘*/ static struct clk *adc_clk;
/*申明並初始化一個信號量ADC_LOCK,對ADC資源進行互斥訪問*/ DECLARE_MUTEX(ADC_LOCK);
static int __init adc_init(void) { int ret;
/*從平臺時鐘佇列中獲取ADC的時鐘,這裏為什麼要取得這個時鐘,因為ADC的轉換頻率跟時鐘有關。 系統的一些時鐘定義在arch/arm/plat-s3c24xx/s3c2410-clock.c中*/ adc_clk = clk_get(NULL, "adc"); if (!adc_clk) { /*錯誤處理*/ printk(KERN_ERR "failed to find adc clock source\n"); return -ENOENT; }
/*時鐘獲取後要使能後才可以使用,clk_enable定義在arch/arm/plat-s3c/clock.c中*/ clk_enable(adc_clk);
/*將ADC的IO埠佔用的這段IO空間映射到記憶體的虛擬位址,ioremap定義在io.h中。 注意:IO空間要映射後才能使用,以後對虛擬位址的操作就是對IO空間的操作, S3C2410_PA_ADC是ADC控制器的基底位址,定義在mach-s3c2410/include/mach/map.h中,0x20是虛擬位址長度大小*/ adc_base = ioremap(S3C2410_PA_ADC, 0x20); if (adc_base == NULL) { /*錯誤處理*/ printk(KERN_ERR "Failed to remap register block\n"); ret = -EINVAL; goto err_noclk; }
/*把看ADC註冊成為misc設備,misc_register定義在miscdevice.h中 adc_miscdev結構體定義及內部介面函數在第②步中講,MISC_DYNAMIC_MINOR是次設備號,定義在miscdevice.h中*/ ret = misc_register(&adc_miscdev); if (ret) { /*錯誤處理*/ printk(KERN_ERR "cannot register miscdev on minor=%d (%d)\n",MISC_DYNAMIC_MINOR, ret); goto err_nomap; }
printk(DEVICE_NAME " initialized!\n");
return 0;
//以下是上面錯誤處理的跳轉點 err_noclk: clk_disable(adc_clk); clk_put(adc_clk);
err_nomap: iounmap(adc_base);
return ret; }
static void __exit adc_exit(void) { free_irq(IRQ_ADC, 1); /*釋放中斷*/ iounmap(adc_base); /*釋放虛擬位址映射空間*/
if (adc_clk) /*遮罩和銷毀時鐘*/ { clk_disable(adc_clk); clk_put(adc_clk); adc_clk = NULL; }
misc_deregister(&adc_miscdev);/*登出misc設備*/ }
/*導出信號量ADC_LOCK在觸摸屏驅動中使用,因為觸摸屏驅動和ADC驅動公用 相關的寄存器,為了不產生資源競態,就用信號量來保證資源的互斥訪問*/ EXPORT_SYMBOL(ADC_LOCK);
module_init(adc_init); module_exit(adc_exit);
MODULE_LICENSE("GPL"); MODULE_AUTHOR("Huang Gang"); MODULE_DESCRIPTION("My2440 ADC Driver");
|
2、adc_miscdev結構體定義及內部各介面函數的實現,代碼如下:
#include <plat/regs-adc.h>
/*設備名稱*/ #define DEVICE_NAME "my2440_adc"
/*定義並初始化一個等待佇列adc_waitq,對ADC資源進行阻塞訪問*/ static DECLARE_WAIT_QUEUE_HEAD(adc_waitq);
/*用於標識AD轉換後的資料是否可以讀取,0表示不可讀取*/ static volatile int ev_adc = 0;
/*用於保存讀取的AD轉換後的值,該值在ADC中斷中讀取*/ static int adc_data;
/*misc設備結構體實現*/ static struct miscdevice adc_miscdev = { .minor = MISC_DYNAMIC_MINOR, /*次設備號,定義在miscdevice.h中,為255*/ .name = DEVICE_NAME, /*設備名稱*/ .fops = &adc_fops, /*對ADC設備檔操作*/ };
/*字元設備的相關操作實現*/ static struct file_operations adc_fops = { .owner = THIS_MODULE, .open = adc_open, .read = adc_read, .release = adc_release, };
/*ADC設備驅動的打開介面函數*/ static int adc_open(struct inode *inode, struct file *file) { int ret;
/*申請ADC中斷服務,這裏使用的是共用中斷:IRQF_SHARED,為什麼要使用共用中斷,因為在觸摸屏驅動中 也使用了這個中斷號。中斷服務程式為:adc_irq在下面實現,IRQ_ADC是ADC的中斷號,這裏注意: 申請中斷函數的最後一個參數一定不能為NULL,否則中斷申請會失敗,如果中斷服務程式中用不到這個 參數,就隨便給個值就好了,我這裏就給個1*/ ret = request_irq(IRQ_ADC, adc_irq, IRQF_SHARED, DEVICE_NAME, 1); if (ret) { /*錯誤處理*/ printk(KERN_ERR "IRQ%d error %d\n", IRQ_ADC, ret); return -EINVAL; }
return 0; }
/*ADC中斷服務程式,該服務程式主要是從ADC資料寄存器中讀取AD轉換後的值*/ static irqreturn_t adc_irq(int irq, void *dev_id) { /*保證了應用程式讀取一次這裏就讀取AD轉換的值一次, 避免應用程式讀取一次後發生多次中斷多次讀取AD轉換值*/ if(!ev_adc) { /*讀取AD轉換後的值保存到總體變數adc_data中,S3C2410_ADCDAT0定義在regs-adc.h中, 這裏為什麼要與上一個0x3ff,很簡單,因為AD轉換後的資料是保存在ADCDAT0的第0-9位, 所以與上0x3ff(即:1111111111)後就得到第0-9位元的資料,多餘的位元就都為0*/ adc_data = readl(adc_base + S3C2410_ADCDAT0) & 0x3ff;
/*將可讀標識為1,並喚醒等待佇列*/ ev_adc = 1; wake_up_interruptible(&adc_waitq); }
return IRQ_HANDLED; }
/*ADC設備驅動的讀介面函數*/ static ssize_t adc_read(struct file *filp, char *buffer, size_t count, loff_t *ppos) { /*試著獲取信號量(即:加鎖)*/ if (down_trylock(&ADC_LOCK)) { return -EBUSY; }
if(!ev_adc)/*表示還沒有AD轉換後的資料,不可讀取*/ { if(filp->f_flags & O_NONBLOCK) { /*應用程式若採用非阻塞方式讀取則返回錯誤*/ return -EAGAIN; } else/*以阻塞方式進行讀取*/ { /*設置ADC控制寄存器,開啟AD轉換*/ start_adc();
/*使等待佇列進入睡眠*/ wait_event_interruptible(adc_waitq, ev_adc); } }
/*能到這裏就表示已有AD轉換後的資料,則標識清0,給下一次讀做判斷用*/ ev_adc = 0;
/*將讀取到的AD轉換後的值發往到上層應用程式*/ copy_to_user(buffer, (char *)&adc_data, sizeof(adc_data));
/*釋放獲取的信號量(即:解鎖)*/ up(&ADC_LOCK);
return sizeof(adc_data); }
/*設置ADC控制寄存器,開啟AD轉換*/ static void start_adc(void) { unsigned int tmp;
tmp = (1 << 14) | (255 << 6) | (0 << 3);/* 0 1 00000011 000 0 0 0 */ writel(tmp, adc_base + S3C2410_ADCCON); /*AD預分頻器使能、模擬輸入通道設為AIN0*/
tmp = readl(adc_base + S3C2410_ADCCON); tmp = tmp | (1 << 0); /* 0 1 00000011 000 0 0 1 */ writel(tmp, adc_base + S3C2410_ADCCON); /*AD轉換開始*/ }
/*ADC設備驅動的關閉介面函數*/ static int adc_release(struct inode *inode, struct file *filp) { return 0; }
|
注意:在上面實現的每步中,為了讓代碼邏輯更加有條理和容易理解,就沒有考慮代碼的順序,比如函數要先定義後調用。如果要編譯此代碼,請嚴格按照C語言的規範來調整代碼的順序。
3、編寫用戶應用程式測試my2440_adc驅動。建立應用程式adc_test.c,代碼如下:
#include <stdio.h> #include <stdlib.h> #include <errno.h>
int main(int argc, char **argv) { int fd;
//以阻塞方式打開設備檔,非阻塞時flags=O_NONBLOCK fd = open("/dev/my2440_adc", 0);
if(fd < 0) { printf("Open ADC Device Faild!\n"); exit(1); }
while(1) { int ret; int data; //讀設備 ret = read(fd, &data, sizeof(data));
if(ret != sizeof(data)) { if(errno != EAGAIN) { printf("Read ADC Device Faild!\n"); }
continue; } else { printf("Read ADC value is: %d\n", data); } }
close(fd);
return 0; }
|
4、將驅動程式下載掛載到內核,下載應用程式到開發板上後,運行應用程式,扭動mini2440開發板上的定位器,可以觀察到ADC轉換值的變化,證明驅動程式工作正常。效果圖如下:
留言列表