天天看點

在pcduino開發闆上寫驅動控制闆載LED的閃爍

     由于關于pcduino的資料比較少,是以這篇文章是參考了pcduino愛好者論壇的一篇教程《手把手教你用A10點燈》,并且系統的結合了linux驅動的開發步驟。讀完這篇文章,你不但可以對pcduino開發闆的硬體結構有所了解,更重要的是可以對linux的驅動開發步驟有一個系統的認識。我也是一個linux驅動的新手,是以,寫的不對的地方,請大家指正。

1.Linux驅動架構

     這一部分将會手把手教你建立一個Linux的驅動程式架構,在下一部分,我們隻需要将控制pcduino硬體部分的代碼填入這個架構就可以了。像所有的應用程式都有一個main函數作為函數的入口一樣,linux驅動程式的入口是驅動的初始化函數。這個初始化函數是 module_init 來指定的,同樣,與初始化函數對應的驅動程式的退出函數是由  module_exit函數來指定的。下面就讓我們動手寫第一個版本的驅動程式吧。

#include <linux/module.h>
#include <linux/init.h>
static int __init led_init(void)
{
 printk("led init\n");
 return 0;
}

static void __exit led_exit(void)
{
 printk("led exit\n");
}

module_init( led_init );
module_exit( led_exit );
           

将上面代碼儲存為 led.c,接下來就要編寫Makefile檔案對剛剛編寫的驅動程式進行編譯了。建立Makefile檔案,在裡面輸入:

obj-m := led.o
all: 
     make -C /usr/src/linux-headers-3.8.0-35-generic/ M=/home/asus/drive/
clean: 
     rm *.o 
     rm *.ko 
     rm *.order 
     rm *.symvers 
     rm *.mod.c
           

注意,Makefile  中的第三行,-C 後面的參數為你目前使用的核心的頭檔案所在的目錄,你隻需要修改為  "/usr/src/linux-headers-你的核心版本/"  即可,如果你不知道,目前使用的核心版本,可以輸入:

uname -r
           

來進行檢視。M 後面表示你的驅動所在的目錄。改好之後儲存,注意,這個檔案的名字一定得是  "Makefile"  才行,make 和 rm指令前面一定是一個TAB符才行。輸入指令:

make
           

進行編譯,完成之後,使用ls檢視,可以看到得到的檔案如下:

built-in.o  led.c  led.ko  led.mod.c  led.mod.o  led.o  Makefile  modules.order  Module.symvers
           

這裡面的  led.ko  是我們得到的驅動檔案,使用:

sudo insmod led.ko
           

安裝驅動。使用

dmesg
           

指令,會看到最後一行輸出的是   “led init”    ,這句話就是在  led_init  函數中輸出的。使用指令:

sudo rmmod led.ko
           

來解除安裝  led  驅動。再使用: dmesg 指令,會發現,最後一行為  “led exit”。

     上面寫的這個驅動程式是沒有什麼作用的,在linux中,應用程式是通過裝置檔案來和驅動程式進行互動的。是以我們需要在驅動程式中建立裝置檔案,這個裝置檔案建立之後,就會存在于   /dev/   目錄下,應用程式就是通過對這個檔案的讀寫,來向驅動程式發送指令,并通過驅動程式控制硬體的動作。每一個驅動程式對應着一個裝置檔案。要建立一個裝置檔案,首先必須擁有裝置号才行,這個裝置号就需要我們向linux系統提出申請,由linux系統為我們配置設定。裝置号有主裝置号和從裝置号之分,主裝置号使用來表示驅動的類型,從裝置号表示使用同一個驅動的裝置的編号,這裡要申請的就是主裝置号。使用   alloc_chrdev_region   函數來申請一個裝置号。裝置号的類型為   dev_t   ,它是一個 32 位的數,其中 12 位用來表示主裝置号,另外 20 位用來表示從裝置号。可以使用   MAJOR   宏和   MINOR   宏來直接擷取主裝置号和從裝置号。我們第二個版本的程式如下:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>

//驅動名
#define DEV_NAME "led"
//從裝置的個數
#define DEV_COUNT 1

//聲明裝置号
static dev_t dev_number;

//初始化
static int __init led_init(void)
{
        //錯誤标記
        int err;
        printk("led init\n");

        //申請裝置号
        err = alloc_chrdev_region(&dev_number,0,DEV_COUNT,DEV_NAME);
        if(err)
        {
                printk("alloc device number fail\n");
                return err;
        }
        //如果申請成功,列印主裝置号
        printk("major number : %d\n",MAJOR(dev_number));

        return 0;
}

static void __exit led_exit(void)
{
        printk("led exit\n");
        //登出申請的裝置号
        unregister_chrdev_region(dev_number,DEV_COUNT);
}
           

這個程式申請了一個裝置号,并且列印出來,同樣使用   dmesg   指令來檢視,程式的注釋已經很詳細了,就不再多解釋了。 儲存之後,編譯,安裝新的驅動程式。在安裝新的驅動程式之前,需要使用指令   sudo  rmmod  led.ko   将之前安裝的驅動程式解除安裝,使用   dmesg   指令檢視輸出的結果:

[  384.225850] led init
[  384.225854] major number : 250
           

還可以使用指令   cat  /proc/devices | grep  ‘led’  檢視獲得的裝置号。

     裝置号申請完畢後,就可以在   /dev/   目錄下建立裝置檔案了。需要了解的是裝置在記憶體中,使用結構體   cdev   來表示,并且将我們申請的裝置号,以及對檔案操作的回調函數,統統的關聯起來。最後使用這個結構體,用函數   class_create   和   device_create   來建立一個裝置檔案。說了一下基本思路,還是先看程式吧:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>

//驅動名
#define DEV_NAME "led"
//從裝置的個數
#define DEV_COUNT 1

//三個回調函數,當在應用程式執行相應的操作時
//驅動程式會調用相應的函數來進行處理
ssize_t led_write(struct file *, const char __user *, size_t, loff_t *);
int led_open(struct inode *, struct file *);
int led_release(struct inode *, struct file *);

//聲明裝置号
static dev_t dev_number;
//裝置在記憶體中表示的結構體
static struct cdev* cdevp;
//注冊檔案操作的回調函數的結構體
static struct file_operations fops = 
{
	.owner = THIS_MODULE,
	//注冊相應的回調函數
	.open = led_open,
	.release = led_release,
	.write = led_write,
};
//用來建立裝置檔案的class
static struct class* classp;

//初始化
static int __init led_init(void)
{
	//錯誤标記
	int err;
	printk("led init\n");

     	//申請裝置号
	err = alloc_chrdev_region(&dev_number,0,DEV_COUNT,DEV_NAME);     	
	if(err)
	{
		printk("alloc device number fail\n");
		return err;
	}
	//如果申請成功,列印主裝置号
	printk("major number : %d\n",MAJOR(dev_number));

	//給cdev結構體在記憶體中配置設定空間
	cdevp = cdev_alloc();
	//如果配置設定失敗
	if( cdevp==NULL )
	{
		printk("cdev alloc failure\n");
		//登出前面申請的裝置号
		unregister_chrdev_region(dev_number,DEV_COUNT);
		return -1;
	}

	//将cdev結構體與
	//注冊檔案操作的回調函數的結構體file_operations關聯起來
	cdev_init(cdevp,&fops);
	
	//将cdev結構體和申請的裝置号關聯起來
	err = cdev_add(cdevp,dev_number,DEV_COUNT);
	if(err)
	{
		printk("cdev add failure\n");
		//釋放申請的cdev空間
		cdev_del(cdevp);
		//登出申請的裝置編号
		unregister_chrdev_region(dev_number,DEV_COUNT);
		return err;
	}

	//給class配置設定空間
	classp = class_create(THIS_MODULE,DEV_NAME);
	if( classp==NULL )
	{
		printk("class create failure\n");
		//釋放申請的cdev空間
		cdev_del(cdevp);
		//登出申請的裝置編号
		unregister_chrdev_region(dev_number,DEV_COUNT);
		return -1;
	}

	//建立裝置檔案
	device_create(classp,NULL,dev_number,"%s",DEV_NAME);
	printk("/dev/%s create success\n",DEV_NAME);

     	return 0;
}

static void __exit led_exit(void)
{
     	printk("led exit\n");
	//釋放配置設定的class空間
	if( classp )
	{
		device_destroy(classp,dev_number);
		class_destroy(classp);
	}
	//釋放配置設定的cdev空間
	if( cdevp )
	{
		cdev_del(cdevp);
	}
	//登出申請的裝置号
	unregister_chrdev_region(dev_number,DEV_COUNT);
}

module_init( led_init );
module_exit( led_exit );

//當在應用程式中執行  open  函數時,
//會調用下面的這個函數
int led_open(struct inode* pinode,struct file* pfile)
{
	printk("led open\n");
	return 0;
}

//當在應用程式中執行  close  函數時,
//會調用下面的函數
int led_release(struct inode* pinode,struct file* pfile)
{
	printk("led release\n");
	return 0;
}

//當在應用程式中調用   write   函數時,
//會調用下面的這個函數
ssize_t led_write(struct file* pfile,const char __user* buf,size_t count,loff_t* l)
{
	printk("led write");
	return 0;
}

//指定采用的協定
MODULE_LICENSE("GPL");
           

最後一行是指定采用的協定,一定得寫上,否則會造成雖然編譯通過,但是在安裝時,會出現

insmod: error inserting 'led.ko': -1 Unknown symbol in module
           

這個錯誤。編譯,安裝好,之後,我們就可以在   /dev/   目錄下找到   led    檔案,使用指令:

ls -l /dev/led
           

結果如下:

crw------- 1 root root 250, 0 Dec 26 10:52 /dev/led
           

     至此我們的linux裝置驅動架構,已經完全建立起來了。接下來要做的工作,就是對   pcduino   開發闆進行程式設計了。

2.對   pcduino   進行程式設計,控制  LED  閃爍

     所使用的開發闆是pcduino開發闆,如下圖:

在pcduino開發闆上寫驅動控制闆載LED的閃爍

這是一款開源硬體,采用的是cortex-A8的核心,闆上可以安裝ubuntu,android系統,我們使用的闆子已經安裝了   ubuntu   系統,通過   HDMI轉VGA   線連接配接螢幕,并且通過usb接口,連接配接鍵盤和滑鼠,直接在其自帶的ubuntu系統上,編寫驅動并運作。我們仔細的檢視闆子,會發現闆上一共帶有 3 個led燈,分别是  RX_LED,TX_LED,ON_LED,分别用來訓示接收,發送和電源的狀态。這裡我們隻控制  TX_LED  燈進行閃爍。檢視  pcduino  的硬體原理圖,查找  TX_LED  的連接配接位置,如下圖:

在pcduino開發闆上寫驅動控制闆載LED的閃爍

會看到第三行   TX_LED   連接配接到  CPU  的PH15引腳,并且  L  即低電平時為激活狀态,H 高電平時,為熄滅狀态。得到這個資訊說明,我們隻需要控制  CPU  的引腳  PH15  的狀态,就可以控制  TX_LED  的狀态了。

     是以接下來就需要我們去檢視  A10 的晶片手冊,來看一看到底怎麼控制  PH15  這個引腳。

在pcduino開發闆上寫驅動控制闆載LED的閃爍

可以看到  A10  晶片的引腳有很多,而我們隻關注  PH,因為我們要控制的就是  PH15  這個引腳。這裡需要的一個概念就是,對一個引腳的控制至少需要有兩個寄存器,一個是控制寄存器,一個是資料寄存器。控制寄存器用來控制引腳的工作模式,比如輸出或者輸入;資料寄存器用來向引腳輸出資料或者從引腳讀入資料。是以我們要先檢視一下  PH15  的配置寄存器,如下圖:

在pcduino開發闆上寫驅動控制闆載LED的閃爍

我們發現   PH15   控制寄存器一共有3位28-30,共有 8 種工作模式,由于要控制 led 的狀态,我們将它設定為輸出模式,是以  PH15  控制寄存器的内容應該為 001。那麼這個寄存器在哪個位置呢,在表上有   Offset:0x100   我們知道,PH寄存器的偏移位址是  0x100,但是基位址是多少呢。再往前面查閱就會發現

在pcduino開發闆上寫驅動控制闆載LED的閃爍

是以基位址就是  0x01C20800。基位址和偏移位址都有了,我們就可以定位  PH_CFG1  寄存器的位址就是(0x01C20800+0x100),我們隻需要将這個寄存器的第28-30位置為:

30  29  28
0    0   1
           

就可以了。

     當控制寄存器配置完成之後,我們就需要向資料寄存器寫入資料來控制  led  的閃爍。我們同樣檢視晶片手冊:

在pcduino開發闆上寫驅動控制闆載LED的閃爍

可以看到,PH的資料寄存器用每一位來表示一個引腳的狀态。我們要控制  PH15 引腳,就需要對這個寄存器的第15位進行操作。是以,接下來就是,開始動手向驅動架構中添加對硬體操作的時候:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/io.h>
#include <asm/uaccess.h>

//驅動名
#define DEV_NAME "led"
//從裝置的個數
#define DEV_COUNT 1

//定義與硬體相關的宏
//基位址
#define BASE_ADDRESS 0x01C20800
//PH_CFG1寄存器的位址
#define PH_CFG1     (BASE_ADDRESS+0x100)
//PH_DAT寄存器的位址
#define PH_DAT	    (BASE_ADDRESS+0x10C)

//三個回調函數,當在應用程式執行相應的操作時
//驅動程式會調用相應的函數來進行處理
ssize_t led_write(struct file *, const char __user *, size_t, loff_t *);
int led_open(struct inode *, struct file *);
int led_release(struct inode *, struct file *);

//聲明裝置号
static dev_t dev_number;
//裝置在記憶體中表示的結構體
static struct cdev* cdevp;
//注冊檔案操作的回調函數的結構體
static struct file_operations fops = 
{
	.owner = THIS_MODULE,
	//注冊相應的回調函數
	.open = led_open,
	.release = led_release,
	.write = led_write,
};
//用來建立裝置檔案的class
static struct class* classp;

//聲明用來表示PH_CFG1記憶體位址的變量
volatile static unsigned long* __ph_cfg1;
//用來表示PH_DAT記憶體位址的變量
volatile static unsigned long* __ph_dat;

//初始化
static int __init led_init(void)
{
	//錯誤标記
	int err;
	printk("led init\n");

     	//申請裝置号
	err = alloc_chrdev_region(&dev_number,0,DEV_COUNT,DEV_NAME);     	
	if(err)
	{
		printk("alloc device number fail\n");
		return err;
	}
	//如果申請成功,列印主裝置号
	printk("major number : %d\n",MAJOR(dev_number));

	//給cdev結構體在記憶體中配置設定空間
	cdevp = cdev_alloc();
	//如果配置設定失敗
	if( cdevp==NULL )
	{
		printk("cdev alloc failure\n");
		//登出前面申請的裝置号
		unregister_chrdev_region(dev_number,DEV_COUNT);
		return -1;
	}

	//将cdev結構體與
	//注冊檔案操作的回調函數的結構體file_operations關聯起來
	cdev_init(cdevp,&fops);
	
	//将cdev結構體和申請的裝置号關聯起來
	err = cdev_add(cdevp,dev_number,DEV_COUNT);
	if(err)
	{
		printk("cdev add failure\n");
		//釋放申請的cdev空間
		cdev_del(cdevp);
		//登出申請的裝置編号
		unregister_chrdev_region(dev_number,DEV_COUNT);
		return err;
	}

	//給class配置設定空間
	classp = class_create(THIS_MODULE,DEV_NAME);
	if( classp==NULL )
	{
		printk("class create failure\n");
		//釋放申請的cdev空間
		cdev_del(cdevp);
		//登出申請的裝置編号
		unregister_chrdev_region(dev_number,DEV_COUNT);
		return -1;
	}

	//建立裝置檔案
	device_create(classp,NULL,dev_number,"%s",DEV_NAME);
	printk("/dev/%s create success\n",DEV_NAME);

     	return 0;
}

static void __exit led_exit(void)
{
     	printk("led exit\n");
	//釋放配置設定的class空間
	if( classp )
	{
		device_destroy(classp,dev_number);
		class_destroy(classp);
	}
	//釋放配置設定的cdev空間
	if( cdevp )
	{
		cdev_del(cdevp);
	}
	//登出申請的裝置号
	unregister_chrdev_region(dev_number,DEV_COUNT);
}

module_init( led_init );
module_exit( led_exit );

//當在應用程式中執行  open  函數時,
//會調用下面的這個函數
int led_open(struct inode* pinode,struct file* pfile)
{
	//臨時變量
	unsigned long tmp; 
	printk("led open\n");
	
	//将PH15管腳設定為輸出狀态
	//将PH_CFG1這個硬體寄存器的位址,映射到linux記憶體,并擷取映射後的位址
	//通過對這個位址的操作,就可以控制PH_CFG1
	__ph_cfg1 = (volatile unsigned long*)ioremap(PH_CFG1,4);
	//将設定PH15寄存器
	tmp = *__ph_cfg1;
	tmp &= ~(0xf<<28);
	tmp |= (1<<28);
	*__ph_cfg1 = tmp;

	//将燈初始化為熄滅的狀态
	__ph_dat = (volatile unsigned long*)ioremap(PH_DAT,4);
	tmp = *__ph_dat;
	tmp |= (1<<15);
	*__ph_dat = tmp;	

	return 0;
}

//當在應用程式中執行  close  函數時,
//會調用下面的函數
int led_release(struct inode* pinode,struct file* pfile)
{
	printk("led release\n");
	//登出配置設定的記憶體位址
	iounmap(__ph_dat);
	iounmap(__ph_cfg1);

	return 0;
}

//當在應用程式中調用   write   函數時,
//會調用下面的這個函數
ssize_t led_write(struct file* pfile,const char __user* buf,size_t count,loff_t* l)
{
	int val;
	volatile unsigned long tmp;
	printk("led write\n");

	//從使用者空間讀取資料
	copy_from_user(&val,buf,count);	
	printk("write %d\n",val);
	
	//從應用程式讀取指令
	//來控制led燈
	tmp = *__ph_dat;
	if( val==1 )
	{
		//燈亮
		tmp &= ~(1<<15); 
	}
	else
	{
		//燈滅
		tmp |= (1<<15);
	}
	*__ph_dat = tmp;
	return 0;
}

MODULE_LICENSE("GPL");
           

     上面的是完整的控制pcduino上led閃爍的驅動程式,寫完這個驅動程式之後,再寫一個下面的測試程式就可以使 led 閃爍了,測試的代碼如下:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main(void)
{
	int fd;
	int val = 1;
	
	//打開驅動對應的裝置檔案
	fd = open("/dev/led",O_RDWR);
	if( fd<0 )
	{
		printf("open /dev/led error\n");
		return -1;
	}

	while(1)
	{
		//寫入高電平
		write(fd,&val,sizeof(int));
		//睡眠一秒
		sleep(1);
		//将電平反轉
		val = 0;
		//寫入低電平
		write(fd,&val,sizeof(int));
		//睡眠一秒
		sleep(1);
		val = 1;
	}

	close(fd);
	return 0;
}
           

使用  gcc testled.c 将該應用程式編譯,假設生成a.out,安裝新版的驅動程式後,使用

sudo ./a.out
           

就可以看到  pcduino  上的  led  就開始閃爍了。

繼續閱讀