原文鏈結: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個通道可以進行類比信號的輸入,分別是AIN0AIN1AIN2AIN3YMYPXMXP。那麼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);

    /*ADCIO埠佔用的這段IO空間映射到記憶體的虛擬位址,ioremap定義在io.h中。
     注意:IO空間要映射後才能使用,以後對虛擬位址的操作就是對IO空間的操作,
     S3C2410_PA_ADCADC控制器的基底位址,定義在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");

 

2adc_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_ADCADC的中斷號,這裏注意:
     申請中斷函數的最後一個參數一定不能為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 = (<< 14) | (255 << 6) | (<< 3);/* 0 1 00000011 000 0 0 0 */
    writel(tmp, adc_base + S3C2410_ADCCON); /*AD預分頻器使能、模擬輸入通道設為AIN0*/

    tmp = readl(adc_base + S3C2410_ADCCON);
    tmp = tmp | (<< 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轉換值的變化,證明驅動程式工作正常。效果圖如下:

 

arrow
arrow
    全站熱搜

    戮克 發表在 痞客邦 留言(0) 人氣()