1. 程式人生 > >mini2440 led驅動程式經典分析

mini2440 led驅動程式經典分析

*************************轉載請註明出處*****************************************************

Linux核心為2.6.32.2

原始碼分析工具source insight

前言:在裸機中操作幾個gpio口很簡單,對控制暫存器和資料暫存器進行配置即可,但要在linux系統中實現同樣的功能還是得費上一番周折的。

以下是驅動的原始碼。

#include <……>
#define DEVICE_NAME "leds"        
//裝置名

static unsigned long led_table [] = {
 S3C2410_GPB(5),
 S3C2410_GPB(6),
 S3C2410_GPB(7),
 S3C2410_GPB(8),
};

static unsigned int led_cfg_table [] = {
 S3C2410_GPIO_OUTPUT,
 S3C2410_GPIO_OUTPUT,
 S3C2410_GPIO_OUTPUT,
 S3C2410_GPIO_OUTPUT,
};

static int sbc2440_leds_ioctl(
 struct inode *inode,
 struct file *file,
 unsigned int cmd,
 unsigned long arg)
{
 switch(cmd) {
 case 0:
 case 1:
  if (arg > 4) {
   return -EINVAL;
  }
  s3c2410_gpio_setpin(led_table[arg], !cmd);
  return 0;
 default:
  return -EINVAL;
 }
}

static struct file_operations dev_fops = {
 .owner = THIS_MODULE,
 .ioctl = sbc2440_leds_ioctl,
};

static struct miscdevice misc = { //雜項裝置結構體
 .minor = MISC_DYNAMIC_MINOR,     
//次裝置號
 .name = DEVICE_NAME,
 .fops = &dev_fops,
};

static int __init dev_init(void)
{
 int ret;

 int i;
 
 for (i = 0; i < 4; i++) {
  s3c2410_gpio_cfgpin(led_table[i], led_cfg_table[i]);
  s3c2410_gpio_setpin(led_table[i], 0);
 }

 ret = misc_register(&misc);                   //混雜設備註冊

 printk (DEVICE_NAME"\tinitialized\n");

 return ret;
}

static void __exit dev_exit(void)
{
 misc_deregister(&misc);                       //
混雜設備註銷                             
}

module_init(dev_init);                       //模組載入函式              

module_exit(dev_exit);                       //模組解除安裝函式
MODULE_LICENSE("GPL");                           //
證書
MODULE_AUTHOR("FriendlyARM Inc.");               //作者

以上列出了加入簡單註釋的MINI2440led驅動程式的原始碼,標頭檔案已被省去。對於上述的驅動,我們主要分析這個驅動的整體框架以及裡面涉及到的一些重要的函式和巨集定義內容。

1、misc裝置

    misc裝置裝置被譯成雜項裝置或者是混雜裝置,關於雜項裝置在我的一篇文章裡面已經專門介紹,這裡根據本驅動在詳細說明下。Linux裝置驅動主要分為字元裝置,塊裝置和網路裝置。但是上面的驅動程式並不屬於上面三大類中的常見形式,我們把上述驅動程式中的稱為雜項裝置。什麼是雜項裝置呢?linux包含了很多的裝置型別,但不管怎麼分類,總不能完全概括所有裝置,我們把這些不屬於上述三大形式的歸為一類叫做雜項裝置。雜項裝置共用相同的主裝置號(MISC_MAJOR,也就是10),但次裝置號不同,所有的雜項裝置形成一個連結串列,對裝置訪問的時候,核心根據此裝置號找到其對應的裝置,然後呼叫其相應的file_operations結構體中註冊的各個函式。對於雜項裝置,linux核心專門提供了這樣的一個結構體miscdevice,其有很強的包容性。結構如下:

以下檔案定義在/linux2.6.32.2/include/linux/Miscdivice.h

struct miscdevice {

int minor;

const char *name;

const struct file_operations *fops;

struct list_head list;

struct device *parent;

struct device *this_device;

const char *nodename;

mode_t mode;

};

同時提供的miscdevice註冊和登出函式如下所示。

int misc_register(struct miscdevice * misc);

int misc_deregister(struct miscdevice *misc);

其實,雜項裝置的本質仍然是字元裝置,只是將這種裝置驅動增加了一層封裝而已,雜項裝置中的主體還是file_operations結構的實現,所以,其並不神祕。但要注意是是雜項裝置在嵌入式系統中常被用到。其使用的基本形式如本驅動程式所示,不再敖述。

2.LED對應的GPIO埠列表

static unsigned long led_table [] = {
 S3C2410_GPB(5),
 S3C2410_GPB(6),
 S3C2410_GPB(7),
 S3C2410_GPB(8),
};

led裝置驅動程式中主要是對上述的幾個埠進行s3c2410_gpio_cfgpin(led_table[i], led_cfg_table[i]);(配置管腳功能)

s3c2410_gpio_setpin(led_table[i], 0);(設定管腳電平狀態)

操作。先來弄清楚這幾個埠的定義。

以下檔案定義在/arch/arm/mach-s3c2410/include/mach/gpio-nrs.h  

/* S3C2410 GPIO number definitions. */

#define S3C2410_GPA(_nr) (S3C2410_GPIO_A_START + (_nr))
#define S3C2410_GPB(_nr) (S3C2410_GPIO_B_START + (_nr))
#define S3C2410_GPC(_nr) (S3C2410_GPIO_C_START + (_nr))
#define S3C2410_GPD(_nr) (S3C2410_GPIO_D_START + (_nr))
#define S3C2410_GPE(_nr) (S3C2410_GPIO_E_START + (_nr))
#define S3C2410_GPF(_nr) (S3C2410_GPIO_F_START + (_nr))
#define S3C2410_GPG(_nr) (S3C2410_GPIO_G_START + (_nr))
#define S3C2410_GPH(_nr) (S3C2410_GPIO_H_START + (_nr))

enum s3c_gpio_number {
 S3C2410_GPIO_A_START = 0,
S3C2410_GPIO_B_START = S3C2410_GPIO_NEXT(S3C2410_GPIO_A),
 S3C2410_GPIO_C_START = S3C2410_GPIO_NEXT(S3C2410_GPIO_B),
 S3C2410_GPIO_D_START = S3C2410_GPIO_NEXT(S3C2410_GPIO_C),
 S3C2410_GPIO_E_START = S3C2410_GPIO_NEXT(S3C2410_GPIO_D),
 S3C2410_GPIO_F_START = S3C2410_GPIO_NEXT(S3C2410_GPIO_E),
 S3C2410_GPIO_G_START = S3C2410_GPIO_NEXT(S3C2410_GPIO_F),
 S3C2410_GPIO_H_START = S3C2410_GPIO_NEXT(S3C2410_GPIO_G),
};

#define S3C2410_GPIO_NEXT(__gpio) \
 ((__gpio##_START) + (__gpio##_NR) + CONFIG_S3C_GPIO_SPACE + 0)

CONFIG_S3C_GPIO_SPAC是核心配置選項,在.config中可以找到,我的配置為:

CONFIG_S3C_GPIO_SPACE = 0 

因此,以S3C2410_GPB(5)為例,其巨集展開為:

S3C2410_GPIO_NEXT(S3C2410_GPIO_A) +5 => 

(S3C2410_GPIO_A_START + S3C2410_GPIO_A_NR +  CONFIG_S3C_GPIO_SPACE + 0) + 5 =>

很顯然,S3C2410_GPB(5)就是從GPA的首地址+GPA個數+GPB的offset就是當前GPB的IO偏移量,即

0+32+5=37,同理

S3C2410_GPB(0)相當於 32

S3C2410_GPB(5)相當於 37   

S3C2410_GPB(6)相當於 38   

S3C2410_GPB(7)相當於 39   

S3C2410_GPB(8)相當於 40

到這裡我們應該明白,這個巨集的作用就是對埠進行編號,對於GPA其埠編號的範圍是0~31,GPB埠編號範圍是32~63,以此類推,當然這裡所有的編號不一定都被使用。因為每組的埠的個數不一樣,所以給每組都定義32個,以保證每組都夠用。在得到埠號後,除以32得到的結果就可以確定這個埠是哪組的了。比如得到埠編號38,除以32後得到1,就知道是屬於GPB裡面的I/O口了。這在後面進一步分析中會看到。

3.LED 對應埠將要輸出的狀態列表分析

static unsigned int led_cfg_table [] = {

S3C2410_GPIO_OUTPUT,

S3C2410_GPIO_OUTPUT,

S3C2410_GPIO_OUTPUT,

S3C2410_GPIO_OUTPUT,

};

S3C2410_GPIO_OUTPUT定義在mach/regs-gpio.h

這裡主要看最後的兩位,表示了埠的狀態。

00代表輸入,01代表輸出,10代表功能2,,1代表功能3.注意提示,GPA是沒有輸入功能的。

#define S3C2410_GPIO_LEAVE  (0xFFFFFFFF)

#define S3C2410_GPIO_INPUT  (0xFFFFFFF0) /* not available on A */

#define S3C2410_GPIO_OUTPUT (0xFFFFFFF1)

#define S3C2410_GPIO_IRQ    (0xFFFFFFF2)  /* not available for all */

#define S3C2410_GPIO_SFN2   (0xFFFFFFF2) /* bank A => addr/cs/nand */

#define S3C2410_GPIO_SFN3   (0xFFFFFFF3) /* not available on A */

4、s3c2410_gpio_cfgpin(led_table[i], led_cfg_table[i])分析

函式原始碼定義在linux/arch/arm/plat-s3c24xx/gpio.c

函式原型:

void s3c2410_gpio_cfgpin(unsigned int pin, unsigned int function)

{

   void __iomem *base = S3C24XX_GPIO_BASE(pin);

   unsigned long mask;

   unsigned long con;

   unsigned long flags;

   if (pin < S3C2410_GPIO_BANKB) {  //判斷I/O口是不是屬於GPA,

      mask = 1 << S3C2410_GPIO_OFFSET(pin);

   } else {

      mask = 3 << S3C2410_GPIO_OFFSET(pin)*2;

   }

   switch (function) {              //根據要設定的管腳的功能進行相應的操作

   case S3C2410_GPIO_LEAVE:

      mask = 0;

      function = 0;

      break;

   case S3C2410_GPIO_INPUT:

   case S3C2410_GPIO_OUTPUT:

   case S3C2410_GPIO_SFN2:

   case S3C2410_GPIO_SFN3:

      if (pin < S3C2410_GPIO_BANKB) {

        function -= 1;

        function &= 1;

        function <<= S3C2410_GPIO_OFFSET(pin);

      } else {

        function &= 3;

        function <<= S3C2410_GPIO_OFFSET(pin)*2;

      }

   }

   /* modify the specified register wwith IRQs off */

   local_irq_save(flags);

   con  = __raw_readl(base + 0x00);

   con &= ~mask;

   con |= function;

   __raw_writel(con, base + 0x00);

   local_irq_restore(flags);

}

先看一下主體框架,主體通過switch(function)找到要設定的相應的功能進行對應的操作。這個估計很容易看懂。下面將裡面幾個不好搞懂的地方具體說一下。

對於void __iomem *base = S3C24XX_GPIO_BASE(pin);先來看它的實現

以下內容定義在/linux-2.6.32.2/arch/arm/mach-s3c2410\include\mach\Regs-gpio.h

#define S3C24XX_GPIO_BASE(x) S3C2410_GPIO_BASE(x)

#define S3C2410_GPIO_BASE(pin)  ((((pin) & ~31) >> 1) + S3C24XX_VA_GPIO)

以下內容定義在/linux-2.6.32.2/arch/arm/plat-s3c24xx/include/plat/map.h

#define S3C24XX_VA_GPIO    ((S3C24XX_PA_GPIO - S3C24XX_PA_UART) + S3C24XX_VA_UART)

以下內容定義在/linux-2.6.32.2/arch/arm/mach-s3c2410/include/mach/map.h

#define S3C24XX_PA_GPIO    S3C2410_PA_GPIO

#define S3C24XX_PA_UART    S3C2410_PA_UART

以下內容定義在/linux-2.6.32.2/arch/arm/plat-s3c24xx/include/plat/map.h

#define S3C2410_PA_GPIO    (0x56000000)

#define S3C2410_PA_UART    (0x50000000)

以下內容定義在linux-2.6.32.2/arch/arm/plat-s3c24xx/include/plat/map.h

#define S3C24XX_VA_UART    S3C_VA_UART

以下內容定義在linux-2.6.32.2/arch/arm/plat-s3c/include/plat/map.h

#define S3C_VA_UARTS3C_ADDR(0x01000000)  /* UART */

以下內容定義在linux-2.6.32.2/arch/arm/plat-s3c/include/plat/Map-base.h

#ifndef __ASSEMBLY__

#define S3C_ADDR(x)((void __iomem __force *)S3C_ADDR_BASE + (x))

#else

#define S3C_ADDR(x)(S3C_ADDR_BASE + (x))

#endif

#define S3C_ADDR_BASE(0xF4000000)

到這找出了定義S3C24XX_GPIO_BASE(x)全部的巨集,從此處可以發現,linux中檔案的定義分佈是比較散亂的,這也是讓很多初學者頭疼的地方。接著分析

S3C24XX_VA_GPIO=((S3C24XX_PA_GPIO - S3C24XX_PA_UART) + S3C24XX_VA_UART)

               =((0x56000000 - 0x50000000) + (0xF4000000 + 0x01000000))

               = (0x06000000 + 0xF5000000)

               = (0xFB000000)

#define S3C_VA_UARTS3C_ADDR(0x01000000)  /* UART */

這句話看出在虛擬地址的基地址上偏移0x01000000

對下面兩個進行解釋:

#define S3C_ADDR_BASE(0xF4000000)   所有暫存器虛擬地址首地址

#S3C24XX_VA_GPIO                    GPIO的虛擬地址首地址

S3C2410_GPB(5)通過上面的計算其數值為37,

S3C24XX_GPIO_BASE(S3C2410_GPB(5))= S3C24XX_GPIO_BASE(37)

=((((37) & ~31) >> 1) + S3C24XX_VA_GPIO)

=((((37) & ~31) >> 1) + (0xFB000000))= 0xFB000010

所以最終*base =0xFB000010,這個就是GPBCON的虛擬地址,檢視其手冊我們知道GPBCON實體地址為0X56000010, GPACON的虛擬地址0xFB000000,檢視其手冊我們知道GPACON實體地址為0X56000000,下面的程式通過訪問這個虛擬地址,來訪問控制暫存器,實現對I/O埠的配置,此時有人就會問,我訪問這個虛擬地址,為什麼就能實現了對該實體地址的訪問?這是MMU的虛擬地址和實體地址的對映問題,關於這個的問題比較複雜,我專門找了一篇文章來介紹這個虛實對映關係,看本部落格裡的《2410下暫存器地址虛實對映的實現》的這一文章,因為內容較多,我不在這裡介紹。

還一個問題

((((pin) & ~31) >> 1)到底是神馬意思?這個主要靠理解,剛才上面說了每組埠定義為32個,((pin) & ~31)相當於就是把低五位全部清零,而第五位所能代表的範圍正好是32,有點以大小32進行對其的意思。如果將得到的數值右移5位的話,得到的數值(設為ppvalue)能正好代表是哪組I/O口。這裡為什麼右移1位呢,我們看下

幾個GPXCON暫存器的實體地址。

GPACON 0X56000000

GPBCON 0X56000010

GPCCON 0X56000020

GPDCON 0X56000030

其他的以此類推,可以看出這個I/O口控制暫存器的規律,如果將ppvalue左移四位,加上GPIO虛擬基地址,就能得到GPXCON控制暫存器的虛擬地址了。順便說下,這裡的虛實地址的對映只是相差了一個偏移量。

分析:if (pin < S3C2410_GPIO_BANKB)

S3C2410_GPIO_BANKB的定義如下

#define S3C2410_GPIO_BANKA  (32*0)

#define S3C2410_GPIO_BANKB  (32*1)

#define S3C2410_GPIO_BANKC  (32*2)

#define S3C2410_GPIO_BANKD  (32*3)

#define S3C2410_GPIO_BANKE  (32*4)

#define S3C2410_GPIO_BANKF  (32*5)

#define S3C2410_GPIO_BANKG  (32*6)

#define S3C2410_GPIO_BANKH  (32*7)

用於判斷此I/O口是否為GPA埠,這是為了區分開GPA與其他各組埠,因為GPA控制暫存器的操作和其他的有點區別,另外要注意,它是沒有輸入功能的。看datasheet能夠更好的瞭解。

分析:S3C2410_GPIO_OFFSET(pin)

#define S3C2410_GPIO_OFFSET(pin) ((pin) & 31)    //用此巨集能得出偏移量

   if (pin < S3C2410_GPIO_BANKB) {  //判斷I/O口是不是屬於GPA,     mask = 1 << S3C2410_GPIO_OFFSET(pin);//設定遮蔽碼

   } else {

      mask = 3 << S3C2410_GPIO_OFFSET(pin)*2;//設定遮蔽碼

   }

分析:local_irq_save(flags);

這個與下面出現的local_irq_restore(flags);成對使用,用於關閉、開啟中斷,同時將中斷的標誌儲存在flags中。

分析:__raw_readl(base + 0x00);  __raw_writel(con, base + 0x00);

con  = __raw_readl(base + 0x00);   //讀取控制暫存器資料

   con &= ~mask;                  //遮蔽掉相應的位

   con |= function;               //設定要設定的位

   __raw_writel(con, base + 0x00);//把改變後的資料寫回控制暫存器

上面的是兩個函式巨集,定義如下

#define __raw_writeb(v,a) (__chk_io_ptr(a), *(volatile unsigned char __force *)(a) = (v))
#define __raw_writew(v,a) (__chk_io_ptr(a), *(volatile unsigned short __force *)(a) = (v))
#define __raw_writel(v,a) (__chk_io_ptr(a), *(volatile unsigned int __force *)(a) = (v))
PS(ZXX):先檢查指標a是否合法,然後將數值v寫入a所指向的空間。

三種類型分別對應char,short,int
#define __raw_readb(a) (__chk_io_ptr(a), *(volatile unsigned char __force *)(a))
#define __raw_readw(a) (__chk_io_ptr(a), *(volatile unsigned short __force *)(a))
#define __raw_readl(a) (__chk_io_ptr(a), *(volatile unsigned int __force *)(a))
PS(ZXX):
先檢查指標a是否合法,然後讀取a所指向的空間的數值。
三種類型分別對應char,short,int

5.分析s3c2410_gpio_setpin(led_table[i], 0);

void s3c2410_gpio_setpin(unsigned int pin, unsigned int to)

{

void __iomem *base = S3C24XX_GPIO_BASE(pin);

unsigned long offs = S3C2410_GPIO_OFFSET(pin);

unsigned long flags;

unsigned long dat;

local_irq_save(flags);

dat = __raw_readl(base + 0x04);

dat &= ~(1 << offs);

dat |= to << offs;

__raw_writel(dat, base + 0x04);

local_irq_restore(flags);

}

有了上述的對s3c2410_gpio_cfgpin(led_table[i], led_cfg_table[i])的分析,上面的程式碼大同小異罷了,只是說一下__raw_readl(base + 0x04);這個,這是對資料暫存器進行操作,看datasheet就知道,每組的GPXDAT的地址值都比GPXCON的地址值大4。

總結:到此這個led驅動分析就結束了,程式雖小,裡面涉及到的東西還是不少的,主要是剛入門者感覺很亂主要是對剛入門的同志有一定的幫助。關於這個驅動程式有幾個版本,核心也在不斷改變。我同時貼出另一個轉載來的也是led驅動程式的文章,但裡面涉及的結構資料比這個要複雜點,在我的部落格裡面可以找到,看懂的話還是能學到不少東西的。