2015年6月22日 星期一

用 Raspberry pi 寫驅動程式 -- 範例1:LED


寫驅動程式的時候,我們要先定義他的規格(specification):
  1. 透過3個gpio(General Purpose I/O)去控制3個LED,gpio的位置在載入模組(module)的時候(modprobe, insmod)可以額外設定而不需要重新編譯模組。
  2. 控制方法:寫入/dev/LED_n (n=0,1,2),若是寫入1則啟動LED,寫入0則關閉。
  3. 每個連入系統的使用者都可以控制
接起來就像這樣:

模組就先從最簡單的Hello World開始:
hello.c
#include 
#include 
int hello_init(void)
{
    printk("hello world!\n");
    return 0;
}
void hello_exit(void)
{
    printk("goodbye world!\n");
}
MODULE_LICENSE("GPL");
module_init(hello_init);
module_exit(hello_exit);
用以下的Makefile編譯:
obj-m += hello.o

EXTRA_CFLAGS += -I$(PWD)

all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean


用insmod或modprobe載入後,module_init會呼叫hello_init函式,然後在dmesg(display message/driver message)顯示"hello world!"。用rmmod卸載的話,module_exit會呼叫hello_exit函式,然後在dmesg(display message/driver message)顯示"goodbye world!"。

現在進入寫LED驅動程式的部份。
現在linux kernel已經有支援gpio控制的部份,記得加入標頭檔<linux/gpio.h>就好,這樣就不用像參考資料一樣還要查記憶體位址。(雖然現在kernel也有支援LED控制的部份,不過這個以後有空再說)

一個能控制gpio的模組,在載入(init)的時候大概要做這些事:
  1. 要求系統使用指定的gpio
    使用gpio_request(還不知道要用input或output的時候)或gpio_request_one(只需要一個gpio的時候) 或gpio_request_array(要使用多個gpio時)
  2. 要求系統給予一個裝置號碼(device number)或自己提供一個裝置號碼要求系統登錄
    使用alloc_chrdev_region(動態配置裝置號碼)或register_chrdev_region(自己提供裝置號碼)
  3. 分配(allocate)並初始(initial)一個字元裝置(character device)並加入系統
    cdev_alloc, cdev_init(在此步驟要定義裝置使用者嘗試開啟關閉或讀寫裝置檔案時的動作), 與cdev_add
  4. 在/dev下新增裝置檔案(device file)以便讓使用者進行系統呼叫
    class_create與device_create
卸載(exit)的時候,就把載入的步驟反過來做就好:
  1. 把在/dev下新增的裝置檔案移除
    class_destroy
  2. 把加入系統的字元裝置移除
    cdev_del
  3. 把登錄的裝置號碼取消
    unregister_chrdev_region
  4. 取消指定使用的gpio
    gpio_free或gpio_free_array

以下是整個模組內需要用到的自訂變數跟巨集(macro)

#define LED_DRIVER_NAME "LED"        // 驅動程式名稱
#define BUF_SIZE 5           // 用來讀取使用者寫入設備檔案的buffer

static dev_t driverno ;               // 裝置編號
static struct cdev *gpio_cdev;        // 字元裝置設備
static struct class *gpio_class;      // 裝置群組

static struct gpio leds[] = {                  //3個欄位依序是gpio號碼、
    {  2, GPIOF_OUT_INIT_LOW, "LED_0" },       //輸入輸出模式(這裡GPIOF_OUT_INIT_LOW代表輸出但值為0)、
    {  3, GPIOF_OUT_INIT_LOW, "LED_1" },       //在/dev 內顯示的裝置名稱
    {  4, GPIOF_OUT_INIT_LOW, "LED_2" },
};

// 三個gpio的預設值
static int gpio0 = 2;           
static int gpio1 = 3;
static int gpio2 = 4;

// gpio的值是可以在insmod的時候更改的
module_param(gpio0, int, S_IRUGO);
MODULE_PARM_DESC(gpio0, "GPIO-0 pin to use");
module_param(gpio1, int, S_IRUGO);
MODULE_PARM_DESC(gpio1, "GPIO-1 pin to use");
module_param(gpio2, int, S_IRUGO);
MODULE_PARM_DESC(gpio2, "GPIO-2 pin to use");

// Forward declarations
static ssize_t write_LED( struct file *, const char *,size_t,loff_t *);
//Operations that can be performed on the device

// struct file_operations 決定怎麼跟設備檔案溝通的方式
// 在規格中我們希望只透過"寫入"來開關LED,所以只需定義.write
static struct file_operations fops = {
 .owner = THIS_MODULE,    // 這是指定當前模組是這個結構的擁有者,這樣可以避免使用中的模組被卸載
 .write = write_LED       // 當使用者寫入檔案時,執行write_LED
};

write_LED長這樣:

    
// 這四個引數會被module使用,
// struct file *filp : 設備檔案
// const char *buf : 使用者輸入的字串
// size_t count : 使用者輸入字串數
// loff_t *f_pos : 使用者輸入的值要從第幾個字元開始算
static ssize_t write_LED( struct file *filp, const char *buf,size_t count,loff_t *f_pos){
    char kbuf[BUF_SIZE];                                    
    unsigned int len=1, gpio;
    // f_path 是路徑,dentry是entry的位置,d_inode是那個entry的設備號碼(有major和minor)
    // 會這麼麻煩是因為我們有3個gpio
    gpio = iminor(filp->f_path.dentry->d_inode);    
    printk(KERN_INFO LED_DRIVER_NAME " GPIO: LED_%d is modified. \n", gpio);
    len = count < BUF_SIZE ? count-1 : BUF_SIZE-1; 
    // copy_from_user 是把使用者輸入的值(buf) copy "len"個字元到指定的buffer(kbuf)裡做後續處理
    if(copy_from_user(kbuf, buf, len) != 0) return -EFAULT;
    kbuf[len] = '\0';
    printk(KERN_INFO LED_DRIVER_NAME " Request from user: %s\n", kbuf);
    // 如果輸入1就打開,是0就關上
    if (strcmp(kbuf, "1") == 0) {
        printk(KERN_ALERT LED_DRIVER_NAME " LED_%d switch On \n", gpio);
        gpio_set_value(leds[gpio].gpio, 1);
    } else if (strcmp(kbuf, "0") == 0) {
        printk(KERN_ALERT LED_DRIVER_NAME " LED_%d switch Off \n", gpio);
        gpio_set_value(leds[gpio].gpio, 0);
    }
    // 這邊是停頓100 毫秒,注意"sleep"表示停頓期間cpu可以去做其他的事
    // 如果用mdelay也是停頓100毫秒,可是cpu在停頓期必須待命不能做其他事,這會降低系統效能
    msleep(100);
    return count;
}


再來就是module_init的部份(code prettyprint不知道為何在這一直出問題,所以就不用了):
static int __init LED_init_module(void)
{
    int ret, i;
    // Set gpio according to the parameters you give
    printk(KERN_INFO LED_DRIVER_NAME " %s\n", __func__);
    modify_gpio();              // 把gpio_pin0(1,2)代入leds中的gpio setting
    printk(KERN_INFO LED_DRIVER_NAME " gpio_request_array \n");
    ret = gpio_request_array(leds, ARRAY_SIZE(leds));    // 向系統要求gpio
    if(ret<0) {
        printk(KERN_ERR LED_DRIVER_NAME " Unable to request GPIOs: %d\n", ret);
    goto exit_gpio_request;
    }
    // Get driver number 向系統調用driver number
    printk(KERN_INFO LED_DRIVER_NAME " alloc_chrdev_region \n");
    ret=alloc_chrdev_region(&driverno,0,ARRAY_SIZE(leds),LED_DRIVER_NAME);
    if (ret) {
     printk(KERN_EMERG LED_DRIVER_NAME " alloc_chrdev_region failed\n");
    goto exit_gpio_request;
    }
    printk(KERN_INFO LED_DRIVER_NAME " DRIVER No. of %s is %d\n", LED_DRIVER_NAME, MAJOR(driverno));

    printk(KERN_INFO LED_DRIVER_NAME " cdev_alloc\n");
    // 配備cdev
    gpio_cdev = cdev_alloc();
    if(gpio_cdev == NULL){
        printk(KERN_EMERG LED_DRIVER_NAME " Cannot alloc cdev\n");
    ret = -ENOMEM;
        goto exit_unregister_chrdev;
    }

    printk(KERN_INFO LED_DRIVER_NAME " cdev_init\n");

    // 初始cdev並跟 file_operations連結
    cdev_init(gpio_cdev,&fops);
    gpio_cdev->owner=THIS_MODULE;

    printk(KERN_INFO LED_DRIVER_NAME " cdev_add\n");

    // 新增cdev並跟獲得的配備編號連結(leds有三個元件)
    ret=cdev_add(gpio_cdev,driverno,ARRAY_SIZE(leds));

    if (ret){
    printk(KERN_EMERG LED_DRIVER_NAME " cdev_add failed!\n");
    goto exit_cdev;
    }

    printk(KERN_INFO LED_DRIVER_NAME " Play blink\n");
    blink();    // 自己寫的小程式,確定gpio有起來

    printk(KERN_INFO LED_DRIVER_NAME " Create class \n");

    // 在/sys/class 內新增class
    gpio_class = class_create(THIS_MODULE, LED_DRIVER_NAME);

    if (IS_ERR(gpio_class)){
    printk(KERN_ERR LED_DRIVER_NAME " class_create failed\n");
    ret = PTR_ERR(gpio_class);
    goto exit_cdev;
    }

    // 新增node到 /dev/ 跟/sys/class/LED_DRIVER_NAME 下
    printk(KERN_INFO LED_DRIVER_NAME " Create device \n");
    for (i=0; i<ARRAY_SIZE(leds); ++i) {
    if (device_create(gpio_class,NULL, MKDEV(MAJOR(driverno), MINOR(driverno)+i), NULL,leds[i].label)==NULL) {
        printk(KERN_ERR LED_DRIVER_NAME " device_create failed\n");
            ret = -1;
        goto exit_cdev;
    }
    }

    return 0;

  exit_cdev:
    cdev_del(gpio_cdev);

  exit_unregister_chrdev:
    unregister_chrdev_region(driverno, ARRAY_SIZE(leds));

  exit_gpio_request:
    gpio_free_array(leds, ARRAY_SIZE(leds));
    return ret;
}

module_exit就是把module_init初始的東西都取消就好

   
static void __exit LED_exit_module(void)
{
    int i;
    printk(KERN_INFO LED_DRIVER_NAME " %s\n", __func__);
    // turn all off
    for(i = 0; i < ARRAY_SIZE(leds); i++) {
	gpio_set_value(leds[i].gpio, 0);
	device_destroy(gpio_class, MKDEV(MAJOR(driverno), MINOR(driverno) + i));  // 把node移掉
    }

    class_destroy(gpio_class);  // 取消class
    cdev_del(gpio_cdev);  // 移除cdev
    unregister_chrdev_region(driverno, ARRAY_SIZE(leds));  // 解登錄driverno
    gpio_free_array(leds, ARRAY_SIZE(leds));   // 釋放gpio
}

完整的程式碼在此(包含測試檔案)
https://gist.github.com/gnitnaw/b116f358fa688897fe00

 insmod完,/dev/下就會出現節點

pi@raspberrypi ~/work/driver/LED3 $ ls -l /dev/LED*
crw------- 1 root root 246, 0 juin  23 12:47 /dev/LED_0
crw------- 1 root root 246, 1 juin  23 12:47 /dev/LED_1
crw------- 1 root root 246, 2 juin  23 12:47 /dev/LED_2


權限這時還沒開,需要使用sudo 把/dev/LED_0(1,2)權限改成666

pi@raspberrypi ~/work/driver/LED3 $ ls -l /dev/LED*
crw-rw-rw- 1 root root 246, 0 juin  23 12:41 /dev/LED_0
crw-rw-rw- 1 root root 246, 1 juin  23 12:41 /dev/LED_1
crw-rw-rw- 1 root root 246, 2 juin  23 12:41 /dev/LED_2


這樣就能開始使用了。

參考資料:
  1. 台灣樹莓派 -- 用Raspberry Pi學Linux驅動程式
    http://fr.slideshare.net/raspberrypi-tw/write-adevicedriveronraspberrypihowto
  2. https://github.com/wendlers/rpi-kmod-samples

2015年6月17日 星期三

用 Raspberry pi 寫驅動程式 -- 基本觀念

當你要驅動一個硬體,無論是簡單的還是複雜的,必須要考慮:
- 使用者要如何去呼叫這個硬體,以便讓系統許可使用(system call, ioctl)
- 系統要怎麼初始硬體(module_init)
- 系統要怎麼脫離硬體(module_exit)
- 系統跟硬體的互動(interrupt, irq)
- 硬體跟使用者的互動(open, close, read, write, ioctl, copy_from_user, copy_to_user)
- 要如何解析硬體傳來的訊號(keyword : 傳輸協定,SPI, I2C, ...)
- 跟別的module的相依性
- 是否容許多人同時使用?如何分配資源?

以下是我自己考慮的部份:
- 盡可能用kernel已經有的支援以減少coding
- 盡可能考慮與不同系統搭配的可能性(如果你想把raspberry pi的驅動程式移植到BeagleBone Black上....)。

當使用者想要使用某個硬體時,必須要這樣做(以下為擬人化示範):
使用者:喂!那個webcam可不可以給我用一下?就是在/dev/video0的那個!(以開啟/dev/video0的方式去呼叫系統,所謂system call是也)
系 統:我先看一下你夠不夠格用(使用者是否有權限),然後我看有沒有別人在用(mutex)....嗯,應該可以讓你用,我先把設定開一開 (interrupt與irq等),這樣我想應該沒問題了,拿去用吧(使用者以mutex lock(互斥鎖)佔住/dev/video0),然後系統開始根據使用者的要求傳送畫面....
使用者:喂!我用完了,我把webcam放在那囉,你自己收一收吧(以關閉/dev/video0的方式通知系統)
系統:把設定(interrupt與irq等)關掉,互斥鎖也解掉,這樣別人就能用了。

下圖是補充擬人化敘述沒法說明的部份(圖片來源:參考資料2.)

當使用者要使用系統的一個裝置(device),系統必須有一個相應的字符裝置驅動程式(character device driver,這也就是我們現在要學的),而這個character device driver 會在虛擬檔案系統(virtual file system)創造一個字符裝置檔案(character device file),例如本文的/dev/video0或我們下一篇會建立的/dev/LED_0,使用者就能透過開啟此虛擬檔案的方式,告訴系統他想要這裝製作什麼,系統再依照字符裝置驅動程式的設定決定要怎麼回應。

參考資料:
  1. Linux 驅動程式觀念解析, #1: 驅動程式的大架構
    http://www.jollen.org/blog/2006/05/linux_1.html
  2. Character device drivers
    http://wr.informatik.uni-hamburg.de/_media/teaching/wintersemester_2014_2015/pk1415-char_drivers-oster-koenig-presentation.pdf


2015年6月9日 星期二

比較字串輸入的幾種方法:gets fgets

stdio.h提供了幾種方法輸入字串

- gets
範例:
char line1[80];
printf("Enter first string : ");
gets(line1);




然後compile的時候你會得到警告訊息
the `gets' function is dangerous and should not be used.

這是因為gets沒有停止溢位的機制,所以當你輸入的字元大於array大小,程式就會爆掉
所以不建議使用gets

用此法輸入的字串不含\n(gets會把\n換成\0)。


- fgets
範例:
char line1[MAXSIZE]="", line2[MAXSIZE]="";
printf("Enter first string : ");
fgets(line1, sizeof(line1), stdin);
printf("%s\n", line1);
printf("Enter second string : ");
fgets(line2, sizeof(line2), stdin);
printf("%s\n", line2); 



看似美好,可是當你printf輸入的字串時,會多一行出來
Enter first string : abcd efgh
abcd efgh

Enter second string : abcdefgh
abcdefgh

這是因為fgets會把\n包含進去(然後加個\0)。



- scanf
scanf的缺點是空格之後的就不會列進去
  printf ("Enter your family name: ");
  scanf ("%79s",str);  
  printf ("Enter your age: ");
  scanf ("%d",i);
  printf ("Mr. %s , %d years old.\n",str,i);
  printf ("Enter a hexadecimal number: ");
  scanf ("%x",i);
  printf ("You have entered %#x (%d).\n",i,i);
- 自己寫
char line1[MAXSIZE]="", line2[MAXSIZE]="";
printf("Enter first string : ");
read_string(line1);
printf("%s\n", line1);
printf("Enter second string : ");
read_string(line2);
printf("%s\n", line2);

void read_string(char *pt)
{
    int i, j=0;
    char c;
    for(i=0; i < MAXSIZE-1; i++){
        if ((c=getchar()) != '\n' && c != EOF){
            pt[j++] = c;
        } else {
            break;
        }
    }
}

2015年6月5日 星期五

在Raspberry pi 上建立自己的system call

正在讀恐龍本(OS聖經),第2章的程式設計作業就是自己弄個system call。
過恐龍本用的kernel版本很舊(2.x),實在沒法照做。
所以上網找了一個範例,用Raspberry pi來做。
  
使用kernel版本:linux-rpi-3.19.y 
 
1. 在 kernel 目錄下建立 helloworld.c, helloworld.h
 
helloworld.c:

#include <linux/linkage.h> 
#include <linux/kernel.h> 
#include <linux/random.h> 
#include "helloworld.h" 

asmlinkage long sys_helloworld(){ 
    printk (KERN_EMERG "hello world!"); return get_random_int()*4; 
}

helloworld.h:

#ifndef HELLO_WORLD_H 
#define HELLO_WORLD_H 
asmlinkage long sys_helloworld(void); 
#endif 

 
2. 修改 arch/arm/kernel/calls.S 
 
CALL(sys_helloworld)
 
3. 修改 arch/arm/include/uapi/asm/unistd.h
 
#define __NR_helloworld                 (__NR_SYSCALL_BASE+388)
 
4. 修改 arch/arm/include/asm/unistd.h
 
#define __NR_syscalls  (392)
 
 
5. Test file : test.c
 
#include <linux/unistd.h>
#include <stdio.h>
#include <sys/syscall.h>

int main (int argc, char* argv[])
{
    int i=atoi(argv[1]);
    int j=-1;
    printf("invocing kernel function %i\n", i);
    j=syscall(i); /* 350 is our system calls offset number */
    printf("invoked. Return is %i. Bye.\n", j);

    return 0;
}
 
6. compile後執行: 
 
gcc test.c -o test
./test 388

參考資料:
http://stackoverflow.com/questions/21554267/extending-the-rasbian-kernel-linux-kernel-3-10-28-for-arm-raspberry-pi-how
 
 
 

2015年6月2日 星期二

Raspberry pi GPIO 控制

大概的流程就是(N 為希望設定的GPIO 號碼(跟物理位置號碼不同喔)):

1. 設定:
echo "N" > /sys/class/gpio/export

2. 設定輸入或輸出(需接在上一步驟之後):
echo "in" > /sys/class/gpio/gpioN/direction
echo "out" > /sys/class/gpio/gpioN/direction

3. 讀取或輸出
讀取:cat /sys/class/gpio/gpioN/value
輸出:echo "1" > /sys/class/gpio/gpioN/value

4. 結束並取消設定 
echo "N" > /sys/class/gpio/unexport

如果想寫程式,可以考慮用pigpio函式庫,它支援所有GPIO的功能:
http://abyz.co.uk/rpi/pigpio/index.html
其中的piscope更可以監看各個pin的狀態,相當方便。

或是用BCM2835 C Library
http://www.airspayce.com/mikem/bcm2835/

可是最快的還是用C已經有的函式庫
http://codeandlife.com/2012/07/03/benchmarking-raspberry-pi-gpio-speed/

參考資料:
elinux.org/RPi_Low-level_peripherals#sysfs

Google Code Prettify