主要講述本人在學習Linux內核input子系統的全部過程,如有分析不當,多謝指正。以下交流方式,文章歡迎轉載,保留聯繫信息,以便交流。
主頁:www.ielife.cn(愛嵌論壇——嵌入式技術學習交流)
1 mini2440的ADC驅動實例
這節與輸入子系統無關,出現在這裏是因爲後面的章節會講到觸摸屏輸入子系統驅動,由於觸摸屏也使用ADC,因此本節是爲了說明ADC通過驅動代碼是如何控制的。
本節重點:
- 如何通過原理圖查找ADC硬件使用的資源
- 如何通過芯片手冊查找ADC硬件的操作方法
- ADC設備驅動程序的初始化流程
- ADC設備驅動程序的中斷處理流程
本節難點:
- ADC的控制寄存器的操作方法
- ADC驅動程序的控制邏輯
1.1 模數轉換(ADC)簡介
ADC是把模擬信號轉化爲計算機能夠處理的數字信號的過程。
模擬信號一般爲電壓,或者是電流。有些時候也可以是非電信號,如溫度、溼度、聲音、位移等,它們通過傳感器轉換爲電壓信號傳遞給A/D轉換器纔可以進行A/D轉換。
1.2 mini2440上的可調電阻
由mini2440的用戶手冊的1.3.8節A/D輸入測試可知,S3C2440的AIN0引腳接到了開發板的可調電阻W1上,原理圖如下圖3所示:
圖3 mini2440可調電阻原理圖
上圖中,1、2電路的狀態是能夠確定的,一個接3.3V電壓,一個接地,中間接可變電阻W1(10K)。而引腳3接AIN0,它是什麼?可以通過mini2440開發板原理圖來查找:
圖4 mini2440可調電阻與S3C2440接口電路
通過上圖可知,開發板的AIN0引腳與S3C2440 CPU芯片上的AIN0引腳相連接。因此需要進一步查看S3C2440芯片手冊獲得AIN0引腳的作用。
下圖5是S3C2440芯片手冊的第16章對A/D轉換器和觸摸屏接口的介紹。S3C2440內部共有8個通道的模擬輸入接口,其轉換的模擬信號爲10位的二進制數字編碼。
A[3:0]分別代表AIN0、AIN1、AIN2、AIN3,觸摸屏接口可以控制/選擇觸摸屏X、Y方向的引腳(XP,XM,YP,YM)的變換。
圖5 A/D轉換器和觸摸屏的功能結構圖
那麼ADC如何實現模擬信號到數字信號的轉換呢,由上圖可知,模擬信號通過8個通道的任意一個輸入,然後通過分頻器決定A/D轉換器的頻率,最後通過ADC將模擬信號轉換爲數字信號保存在ADCDAT0中,ADCDAT0中的數據可以通過查詢或者中斷的方式來獲得。
S3C2440模數轉換器的控制邏輯可由以下寄存器來進行操作:
ADCCON ADC控制寄存器
ADCTSC ADC觸摸屏控制寄存器器
ADCDLY ADC啓動初始化延遲寄存器
ADCDAT0 ADC轉換數據寄存器
ADCDAT1 ADC轉換數據寄存器
ADCUPDN 筆尖擡起或落下中斷狀態寄存器
由以上內容,開發板可以通過W1可變電阻的阻值變化產生電壓的變化,由AIN0引腳傳遞給ADC控制器轉化爲數字信號,我們通過驅動來獲得可調電阻W1硬件的變化。
1.3 可調電阻的ADC驅動程序
既然需要寫驅動,首先先確定可調電阻的ADC驅動屬於什麼設備。由於是順序讀取寄存器ADCDAT0的過程,所以把它看成一個字符設備,而且對於這個設備來說,更簡單的實現方法是通過misc雜項設備來實現。
代碼實現的非常簡單,通過中斷的方式獲取ADCDAT0的前10位的值就可以了。代碼如下:
/*
* mini2440 ADC驅動程序
*
* Kevin Lee <www.ielife.cn>
*/
#include<linux/kernel.h> /* 提供prink等內核特有屬性 */
#include<linux/module.h> /* 提供如MODULE_LICENSE()、EXPORT_SYMBOL() */
#include<linux/init.h> /* 設置段,如_init、_exit,設置初始化優先級,如__initcall */
#include<linux/wait.h> /* 等待隊列wait_queue */
#include<linux/interrupt.h> /* 中斷方式,如IRQF_SHARED */
#include<linux/fs.h> /* file_operations操作接口等 */
#include<linux/clk.h> /* 時鐘控制接口,如struct clk */
#include<linux/miscdevice.h> /* 雜項設備 */
#include<asm/io.h> /* 提供readl、writel */
#include<asm/irq.h> /* 提供中斷號,中斷類型等,如IRQ_ADC中斷號 */
#include<asm/arch/regs-adc.h> /* 提供控制器的寄存器操作,如S3C2410_ADCCON */
#include<asm/uaccess.h> /* 提供copy_to_user等存儲接口 */
/* 定義設備名稱,用戶訪問接口/dev/adc */
#defineDEVICE_NAME "adc"
/* 定義adc時鐘,通過adc_clock接口獲得adc輸入時鐘,adc轉換器需要 */
staticstruct clk *adc_clock;
/* 定義虛擬地址訪問硬件寄存器,__iomem只是用於表示指針將指向I/O內存 */
staticvoid __iomem *base_addr;
/* 定義並初始化一個等待隊列adc_waitqueue,對ADC資源進行阻塞訪問 */
staticwait_queue_head_t adc_waitqueue;
/* 定義並初始化信號量adc_lock,用於控制共享中斷IRQ_ADC資源的使用 */
DECLARE_MUTEX(adc_lock);
EXPORT_SYMBOL(adc_lock);
/* 定義等待隊列的條件,當is_read_ok=1時,ADC轉換完畢,數據可讀 */
staticvolatile int is_read_ok = 0;
/* 定義ADC轉換的數據內容 */
staticvolatile int adc_data;
staticint adc_open(struct inode *inode, struct file *file);
staticssize_t adc_read(struct file *filp, char *buffer, size_t count, loff_t *ppos);
staticint adc_close(struct inode *inode, struct file *filp);
/* 實現字符設備操作接口 */
staticstruct file_operations adc_fops =
{
.owner = THIS_MODULE,
.open = adc_open,
.read = adc_read,
.release = adc_close,
};
/* 實現misc雜項設備操作接口 */
staticstruct miscdevice adc_miscdev =
{
.minor = MISC_DYNAMIC_MINOR, /* 動態獲取雜項設備的次設備號 */
.name = DEVICE_NAME, /* 雜項設備的設備名稱,這裏爲adc */
.fops = &adc_fops, /* 雜項設備子系統接口,指向adc_fops操作接口 */
};
/*ADC中斷服務程序,獲取ADC轉換後的數據 */
staticirqreturn_t adc_irq(int irq, void *dev_id)
{
/* 僅當is_read_ok=0時才進行轉換,防止多次中斷 */
if(!is_read_ok)
{
/* 讀取ADCCON[9:0]的值,0x3ff爲只獲取[9:0]位,ADCCON爲轉換後的數據 */
adc_data = readl(base_addr +S3C2410_ADCDAT0) & 0x3ff;
/* 設置標識爲1,喚醒讀等待進程可以拷貝數據給用戶空間了 */
is_read_ok = 1;
wake_up_interruptible(&adc_waitqueue);
}
return IRQ_RETVAL(IRQ_HANDLED);
}
/*ADC設備打開,並註冊IRQ_ADC中斷處理函數 */
staticint adc_open(struct inode *inode, struct file *file)
{
int ret;
/* 由於IRQ_ADC爲共享中斷,因此中斷類型選擇IRQF_SHARED,最後一個參數需要設置NULL以外的值 */
ret = request_irq(IRQ_ADC, adc_irq,IRQF_SHARED, DEVICE_NAME, (void *)1);
if (ret)
{
printk(KERN_ERR "Could notallocate ts IRQ_ADC !\n");
return -EBUSY;
}
return 0;
}
/*設置ADC控制寄存器,開啓AD轉換*/
staticvoid adc_run(void)
{
volatile unsigned int adccon;
/* ADCCON的位[14]=1爲使能A/D預分頻器,位[13:6]=32表示設置的分頻值,ADC的轉換頻率需要在2.5MHZ以下
* 我們使用的ADC輸入時鐘爲PCLK=50MHZ,50MHZ/32<2.5MHZ,滿足條件
* 位[5:3]=000,表示模擬輸入通道選擇AIN0
*/
adccon = (1 << 14) | (32 << 6);
writel(adccon, base_addr + S3C2410_ADCCON);
/* 位[0]=1表示使能ADC轉換,當轉換完畢後此位被ADC控制器自動清0 */
adccon = readl(base_addr + S3C2410_ADCCON)| (1 << 0);
writel(adccon, base_addr + S3C2410_ADCCON);
}
/*ADC設備驅動讀函數 */
staticssize_t adc_read(struct file *filp, char *buff, size_t count, loff_t *offp)
{
int err;
/* 獲取信號量,如果被佔用,睡眠等待持有者調用up喚醒
* 這樣做的原因是,有可能其他進程搶佔執行或是觸摸屏驅動搶佔執行
*/
down_interruptible(&adc_lock);
/* 啓動adc轉換,調用中斷處理函數adc_irq*/
adc_run();
/* 如果is_read_ok爲假,則睡眠等待條件爲真,由中斷處理函數喚醒 */
wait_event_interruptible(adc_waitqueue,is_read_ok);
/* 執行到此說明中斷處理程序獲得了ADC轉換後的值,清除爲0等待下一次的讀 */
is_read_ok = 0;
/* 將轉換後的數據adc_data提交給用戶 */
err = copy_to_user(buff, (char*)&adc_data, min(sizeof(adc_data),count));
/* 釋放信號量,並喚醒因adc_lock而睡眠的進程 */
up(&adc_lock);
return err ? -EFAULT : sizeof(adc_data);
}
/*ADC設備關閉函數 */
staticint adc_close(struct inode *inode, struct file *filp)
{
/*釋放中斷*/
free_irq(IRQ_ADC, (void *)1);
return 0;
}
staticint __init adc_init(void)
{
int ret;
/* 獲得adc的時鐘源,通過arch/arm/mach-s3c2410/clock.c獲得提供的時鐘源爲PCLK */
adc_clock = clk_get(NULL, "adc");
if (!adc_clock)
{
printk(KERN_ERR "failed to get adcclock source\n");
return -ENOENT;
}
/* 在時鐘控制器中給adc提供輸入時鐘,ADC轉換需要輸入時鐘 */
clk_enable(adc_clock);
/* 使用ioremap獲得操作ADC控制器的虛擬地址
* S3C2410_PA_ADC=ADCCON,是ADC控制器的基地址,寄存器組的長度=0x1c
*/
base_addr = ioremap(S3C2410_PA_ADC, 0x1c);
if (base_addr == NULL)
{
printk(KERN_ERR "Failed to remapregister block\n");
return -ENOMEM;
goto fail1;
}
/* 初始化等待隊列 */
init_waitqueue_head(&adc_waitqueue);
/* 註冊雜項設備 */
ret = misc_register(&adc_miscdev);
if (ret)
{
printk(KERN_ERR "Failed toregister miscdev\n");
goto fail2;
}
printk(DEVICE_NAME "initialized!\n");
return 0;
fail2:
iounmap(base_addr);
fail1:
clk_disable(adc_clock);
clk_put(adc_clock);
return ret;
}
staticvoid __exit adc_exit(void)
{
/* 釋放虛擬地址 */
iounmap(base_addr);
/* 禁止ADC的時鐘源 */
if (adc_clock)
{
clk_disable(adc_clock);
clk_put(adc_clock);
adc_clock = NULL;
}
/*註銷misc設備*/
misc_deregister(&adc_miscdev);
}
module_init(adc_init);
module_exit(adc_exit);
MODULE_AUTHOR("KevinLee <www.ielife.cn>");
MODULE_DESCRIPTION("Mini2440ADC Misc Device Driver");
MODULE_VERSION("MINI2440ADC 1.0");
MODULE_LICENSE("GPL");
由於驅動程序不同於應用程序main函數,因此讀者觀看以上程序的順序應該如下所示:
首先執行的代碼是__init adc_init函數,它會被insmod加載進內核,當然也可以在內核初始化的時候加載,加載成功,應用層訪問接口“/dev/adc”被創建;
其次,由於應用層會首先打開“/dev/adc”設備,進而操作ADC設備,因此需要查看adc_open函數做了什麼。由於打開設備意味着要使用設備,所以在adc_open中註冊IRQ_ADC中斷資源;
最後,用戶會調用read函數讀取ADC轉換的值,會調用到adc_read。因此,在adc_read函數中需要設置好AIN0引腳的模擬輸入,並啓動ADC,把讀取的任務交給adc_irq函數去完成,最後由adc_read函數把數據提交給應用層。
如果使用insmod的方式加載,需要編寫Makefile函數,如下:
MODULENAME:= adc.o
ifneq($(KERNELRELEASE),)
#call from kernel build system
obj-m := $(MODULENAME)
else
#KERNELDIR?= /lib/modules/$(shell uname -r)/build
KERNELDIR?= /work/system/linux-2.6.22.6
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko*.mod.c .tmp_versions module* Module* $(APPNAME)
depend.depend dep:
$(CC) $(CFLAGS) -M *.c > .depend
ifeq(.depend,$(wildcard .depend))
include.depend
endif
adc.c與Makefile文件放在同一目錄下,執行make就可以了。Makefile中使用的編譯器的名稱爲arm-linux-gcc,根據自己的情況修改即可。
編譯成功,在當前目錄下得到adc.ko驅動模塊,使用命令modinfo adc.ko,獲取信息如下:
stu@stu-desktop:adc$modinfo adc.ko
filename: adc.ko
license: GPL
version: MINI2440 ADC 1.0
description: Mini2440 ADC Misc Device Driver
author: Kevin Lee <www.ielife.cn>
srcversion: 901D02B007F9D53D9C54EA3
depends: built-in,built-in,built-in,built-in,built-in
vermagic: 2.6.22.6mod_unload ARMv4
以上信息也是我們在adc.c代碼中添加的,還有的是在編譯過程中得到的。
把adc.ko文件放到開發板中,執行insmod adc.ko,看到如下信息則說明啓動正常:
adc initialized!
並且可以查看/dev目錄下,已經有adc設備文件
# ls -l /dev/adc
crw-rw---- 1 0 0 10, 61 Jul 27 23:17 /dev/adc
1.4 可調電阻的測試程序
編寫測試程序adc_test.c文件,源代碼如下:
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/types.h>
#include<errno.h>
#defineDEVICE_NAME "/dev/adc"
intmain()
{
int fd,ret,value;
fd = open(DEVICE_NAME, O_RDONLY);
if(fd < 0) {
perror("open ADC : ");
exit(EXIT_FAILURE);
}
ret = read(fd, &value, sizeof(value));
if(ret < 0) {
perror("read ADC:");
close(fd);
exit(EXIT_FAILURE);
}
printf("read from ADC : %d\n",value);
close(fd);
return 0;
}
源代碼簡單不做說明,編譯源代碼的命令:
arm-linux-gcc -Wall -O2 adc_test.c -o adc_test
arm-linux-strip adc_test
拷貝adc_test文件到開發板,執行命令./adc_test,顯示如下:
#./adc_test
readfrom ADC : 736
調節(旋轉)電位器即轉動變阻器,再次執行./adc_test,顯示如下:
#./adc_test
readfrom ADC : 886
讀到的數值隨電阻值的變化而變化,由此說明驅動及硬件工作正常。