linux input輸入子系統分析《二》:s3c2440的ADC簡單驅動實例分析

主要講述本人在學習Linux內核input子系統的全部過程,如有分析不當,多謝指正。以下交流方式,文章歡迎轉載,保留聯繫信息,以便交流。

郵箱:[email protected]

主頁:www.ielife.cn(愛嵌論壇——嵌入式技術學習交流)

博客:blog.csdn.net/ielife


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

讀到的數值隨電阻值的變化而變化,由此說明驅動及硬件工作正常。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章