由于關于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開發闆,如下圖:
這是一款開源硬體,采用的是cortex-A8的核心,闆上可以安裝ubuntu,android系統,我們使用的闆子已經安裝了 ubuntu 系統,通過 HDMI轉VGA 線連接配接螢幕,并且通過usb接口,連接配接鍵盤和滑鼠,直接在其自帶的ubuntu系統上,編寫驅動并運作。我們仔細的檢視闆子,會發現闆上一共帶有 3 個led燈,分别是 RX_LED,TX_LED,ON_LED,分别用來訓示接收,發送和電源的狀态。這裡我們隻控制 TX_LED 燈進行閃爍。檢視 pcduino 的硬體原理圖,查找 TX_LED 的連接配接位置,如下圖:
會看到第三行 TX_LED 連接配接到 CPU 的PH15引腳,并且 L 即低電平時為激活狀态,H 高電平時,為熄滅狀态。得到這個資訊說明,我們隻需要控制 CPU 的引腳 PH15 的狀态,就可以控制 TX_LED 的狀态了。
是以接下來就需要我們去檢視 A10 的晶片手冊,來看一看到底怎麼控制 PH15 這個引腳。
可以看到 A10 晶片的引腳有很多,而我們隻關注 PH,因為我們要控制的就是 PH15 這個引腳。這裡需要的一個概念就是,對一個引腳的控制至少需要有兩個寄存器,一個是控制寄存器,一個是資料寄存器。控制寄存器用來控制引腳的工作模式,比如輸出或者輸入;資料寄存器用來向引腳輸出資料或者從引腳讀入資料。是以我們要先檢視一下 PH15 的配置寄存器,如下圖:
我們發現 PH15 控制寄存器一共有3位28-30,共有 8 種工作模式,由于要控制 led 的狀态,我們将它設定為輸出模式,是以 PH15 控制寄存器的内容應該為 001。那麼這個寄存器在哪個位置呢,在表上有 Offset:0x100 我們知道,PH寄存器的偏移位址是 0x100,但是基位址是多少呢。再往前面查閱就會發現
是以基位址就是 0x01C20800。基位址和偏移位址都有了,我們就可以定位 PH_CFG1 寄存器的位址就是(0x01C20800+0x100),我們隻需要将這個寄存器的第28-30位置為:
30 29 28
0 0 1
就可以了。
當控制寄存器配置完成之後,我們就需要向資料寄存器寫入資料來控制 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 就開始閃爍了。