2015年10月24日 星期六

PiQuadcopter(掰咖考特) -- 用Raspberry pi 做四軸飛行器(三) 四軸運動原理與控制

不是所有東西裝上馬達配螺旋槳就能飛起來的,就算飛得起來也不表示一定飛得穩!

一切都是從角動量守衡開始

想像一下,一個物體裝上有足夠馬力能把物體拉起來的螺旋槳,像這樣:

結果不會像漫畫一樣美好,因為角動量守衡的關係,如果這螺旋槳順時針方向轉,那這螺旋槳(跟接著螺旋槳的物體)會一起逆時針方向轉。所以哆啦A夢插上竹蜻蜓後應該會不停轉轉轉。

至於直昇機怎麼飛?它除了頭上那頂大螺旋槳以外,尾巴上還有個尾槳可以抵消上段中角動量守衡造成的力矩。

那四個螺旋槳要怎麼辦?如果全部逆時針轉,那根據角動量守衡,整個機體就得順時針轉,像這樣。
想要它機體不轉的話也簡單,對角線那兩對,一對轉順時針,一對轉逆時針,總角動量就是0了(假設四個軸的重量均勻分佈)。
那我要怎麼讓它移動?
假設要像下圖那樣走,就把那方向的相鄰兩個(或一個)馬達轉速降低,或提高反方向馬達轉速,這樣低速馬達相對位置降低,高速馬達相對位置升高,四個螺旋槳不垂直於地面,就產生一個水平分量,朝著低速馬達的方向前進。


如果要水平轉動,那就調整某對角線那組的馬達,讓角動量守衡造成的力矩轉動機身。



四軸本身需要的材料

首先你需要四組馬達+螺旋槳(廢話!),而且這四組馬達+螺旋槳不但需要提供足夠的升力去抵抗重力跟阻力(有風的情況下),還要有餘力去移動!一般的建議是馬達+螺旋槳能提供的最大升力必須大於四軸總重的一半:假設四軸總重為1.5kg,搭配的馬達+螺旋槳必須能提供3kg的最大升力,也就是說每一組馬達+螺旋槳的最大升力必須大於~770g。

根據前人的經驗,對於F450(對角線長度450mm,要用Raspberry Pi的話大概這是你可以用的"最小"機架了)的四軸來說,搭配2212 1000KV無刷馬達 + 1045螺旋槳 + 11.1V 電池的螺旋槳通常可以達到總升力為3kg。這裡2212是馬達的大小,表示馬達直徑22mm跟高12mm(馬達愈大通常出力愈大),1000KV表示電源每多增加1V則馬達多轉1000次/秒(馬達沒加螺旋槳的情況),1045則是螺旋槳的長度跟曲度。

鋰聚(LiPo)電池由於相對於別種電池來說同電量比較輕的緣故常被四軸選用。單節鋰聚電池的規格是3.7 V(最高到4.2 V),我用的是3S1P(就是三個單節並聯成一個)標準就是11.1V,它的放電速率跟電量各為35C跟5500mAh,意思就是說這電池可以在電流為5.5A的情況下維持1小時,然後放電速率為35*5500mA = 192.5A。

Electronic Speed Control (ESC,台灣好像叫電調還是電變)是用來控制馬達出力的,通常是使用脈衝寬度調變(PWM),藉由脈衝的寬度(1-2ms)來控制馬力大小(寬度愈高輸出愈大)。一般來說四軸要能飛最小需要50Hz的更新速率(每個脈衝訊號的頻率),要飛得好飛得穩就要更高的更新速率。要注意的是由於ESC直接輸出三相電壓給無刷馬達,所以要注意ESC本身可承受的電流大小。通常一個2212 100KV馬達接到11.1V電源時,最高可輸出10-12A電流,那ESC最好可以承受到兩倍以上,也就是20-25A(注意是每個馬達,不是四個加起來)。

四軸行進間的航向或姿態需要即時被系統所掌握,不然隨時有翻覆的危險。通常四軸是用慣性測量單元( inertial measurement unit,IMU) 的三軸加速度計(accelerometers)跟三軸角速度計(gyroscopes)得知當前姿態,若是需要知道當前航向則需使用電子羅盤(magnetometers)。想要知道高度的話,氣壓計(barometer)是一個選擇(不過精度要夠小,目前看到比較好的高度誤差可達10cm,做懸停還是很勉強)。GPS也是不錯的選擇,既可以知道位置也可以測量速度,可惜它反應比較慢(5-10 Hz)。

最後,少不了用來溝通遙控器跟四軸的無線通訊模組(RF module),通常我們使用的頻段為2.405GHz-2.485GHz,速度愈快有效距離愈短。

所以要控制四軸,主要的流程就是:
  1. 讀取感測器數據 + 收到控制器訊號
  2. 將感測器數據做數據處理(濾掉雜訊或校正)
  3. 根據處理過的數據換算成目前機體角度(所謂的roll, pitch和yaw是也 https://en.wikipedia.org/wiki/Aircraft_principal_axes)或機體位置
  4. 根據控制器給的命令跟當前角度和位置控制馬達出力(PID是也)
這邊其實對於Raspberry Pi來說,比較麻煩的還是讀取感測器數據的timing。我每次讀取加速度+角速度數據後會設暫停3ms左右,讓量測週期達到4ms,可是不是總是成功,以下是我取了一分鐘數據量測的控制週期,最多可以到8ms (第6秒那邊的空白我也不知道是啥回事) ...
 這個希望以後又Preempt RT kernel或Xenomai改善。

Reference
http://csenichijou.blogspot.fr/2014/03/Quadcopter-2.html


2015年10月6日 星期二

PiQuadcopter(掰咖考特) -- 用Raspberry pi 做四軸飛行器(二) 機體本身的材料

由於RPi本身就很大,沒法裝在小台的四軸上,所以我一開始就選定對角線45cm長的機架。
由於我沒有相關經驗,所以材料選購大部份是參考以下網頁:
http://csenichijou.blogspot.fr/2014/03/Quadcopter-1.html

Amazon真是太強大了,要啥幾乎都找得到。以下是我所選擇的四軸材料。

機架

我選的是 JMT F450 Air multi-Rotor Frame,可裝10吋螺旋槳。

  
規格:
對角線長度:450mm
重量:272g

馬達/螺旋槳

我直接買一整組(馬達*4+螺旋槳*2對)給四軸用
Andoer 4 A2212/13T 1000KV Brushless Motor + 2 paires 1045 10 * 4.5 Helices
規格:
無刷馬達:4 * A2212 13T 1000KV(附子彈型螺帽組)
最大效率: 80%
馬達大小: 高2.5 x 直徑3cm
馬達轉軸直徑:2.5 cm (我用3cm的環箍上去是挺緊的)
螺旋槳:1045 * 4(兩正兩反)
根據經驗,螺旋槳跟螺帽最好多買幾組備用....(摔一次可能就得換)

電池

(老實說我買的有點太大太重,其實3000-4000mAh左右應該就夠了....)
ZOP Power 11.1V 5500mAh 3S1P 35C Lipo Battery For RC Model

 規格:
  • 容量:5500MAH
  • 放電速度: 35C
  • 尺寸: About 28.5*48*155mm
  • 重量:約381g

電調(ESC)

我是嫌焊接麻煩才用4合1的電調,其實四個分開裝在機架上可能好些(重量不會太過集中在中間)。一般來說我的馬達配20-25A就夠了,是沒找到更划算的才選這個。這電調有個好處是有一個5.3V輸出剛好可以給RPi或其他元件用。
HOSdog 30A 4 in 1 Brushless Speed Controleur ESC
 規格:
  • 電流輸出:30A/40A(10秒內)
  • 適用電池種類:2-6s Lipo 或 5-12 cellules NiMH
  • 支援設定更新速率:50Hz - 432Hz.

主控制器

四軸上用Raspberry pi 1 model B+ (用2代好像太大材小用了點,所以不考慮)
還加買了個case...
至於遙控器就用普通的Arduino...

通信模組

我本來買了APC220,可是在linux下他有夠不合作的(怎麼也起不來),後來放棄改用NRF24L01(四軸跟遙控器各一個),它是SPI介面,支援250kbps, 1Mbps, 2Mbps(速度愈快有效距離愈低,我試過2Mbps的情況下有效距離不到1m,這是沒有外加天線的情況下)


加速、氣壓、陀螺儀、磁場感測模組

相當多人用的GY80,I2C介面(支援400kHz傳輸速度)

PWM模組

PCA9685PW,I2C介面(支援400kHz傳輸速度),PWM輸出最高可達1.6kHz,精度為12bit。Adafruit有賣已經焊好的模組,大概兩個50元硬幣大,坊間也有layout差不多的模組可買(便宜多了)。

ADC(量測電池電量用)

MCP3008,10 bit很夠用了,SPI介面。

2015年10月4日 星期日

PiQuadcopter(掰咖考特) -- 用Raspberry pi 做四軸飛行器(一) 前言

想做四軸很久了。會想用RPi不外乎以下理由:
- 磨練C programming
- 了解kernel programming(雖然最後幾乎沒用上)
- 想體驗Real-Time Linux的威力(雖然還沒開始用)

一般來說RPi拿來做四軸有點不太適合,因為作業系統是Linux(雖然據說可以安裝其他的RTOS,不過我暫時不討論這個),而屬於通用型作業系統的Linux為了能在較短時間內完成較多任務,其即時性(從系統收到任務的時間到系統開始處理這任務的時間)比較差(正在處理的任務不能被優先權較高的任務插隊)。我希望用多執行緒程式+搶佔式即時核心(Preempt RT Linux kernel)來改善這個問題。

稍微搜尋了一下前人用Rpi做四軸的經驗:

RPi only
https://github.com/idavidstory/goPiCopter
使用Go程式語言,sensor更新週期50Hz,控制週期10Hz,9 DOF + PWM(PCA9685)
http://blog.pistuffing.co.uk/
看起來似乎沒完成

RPi + Arduino
https://github.com/vjaunet/QUADCOPTER_V2
使用MPU6050,Arduino用來控制SPI介面的硬體和PWM

https://github.com/rpicopter/AvrMiniCopter-wiki/wiki
10 DOF

https://www.raspberrypi.org/forums/viewtopic.php?f=37&t=35746
MPU6050,使用特殊ESC所以不用PWM,sensor更新週期在300-400Hz,以C++編寫程式,不用Real-time OS,Arduino用於RFM12無線模組(與筆電交換訊息控制機體)

RPi + 其他飛控
http://www.instructables.com/id/Autonomous-Cardboard-Rasberry-Pi-Controlled-Quad/
http://www.botched.co.uk/quadrocopter/
使用dsPIC30F(PWM+I2C),10 DOF

看起來完全用RPi不用其他MCU好像也不是不可能。

四軸的飛行控制需要的硬體有:
- 機架、電池、(無刷馬達+螺旋槳)*4 (廢話)
- ESC (electronic speed control) :控制馬達轉速,需要用四個PWM(Pulse-width modulation,脈衝寬度調變)個別控制
- IMU(Inertial measurement unit,慣性測量單元):三軸加速度計(測量重力加速度方向+加速度)+三軸角速度計(測量角速度)+三軸電子羅盤(測知方向)+氣壓計(換算海拔高度),3+3+3+1簡稱10軸 (10 DOF,一般來說6軸勉強夠用)。
- 無線通訊模組(wifi 或RF)
- 如果想知道電池電量是不是快到底了,最好加個ADC(類比轉數位輸出)測量電池電壓。

而我手上的RPi model B+上面有:
- I2C * 1 (可接多個裝置,支援400kHz以上傳輸速度)
- SPI * 1 (可接兩個裝置,支援8MB/s以上傳輸速度)
- PWM * 1 (不夠用...),雖然可以用DMA做軟體PWM,不過更新頻率不能太高(~100Hz差不多極限了)

根據以上需求,結論就是我需要買:
- 10軸IMU:GY80(I2C,400kHz,前6軸資料更新可達1600Hz)
- nRF24L01*2:無線模組(250Kbps-2Mbps,頻寬愈大有效距離愈小)一個給RPi用,一個給Arduino用(操縱四軸),SPI
- ADC:MCP3008:10bit(量電壓夠用了) ,SPI
- PWM:PCA9685(Adafruit有賣已經設計好的電路,坊間也有類似的),I2C,12bit,頻率可達1526Hz

然後就是程式部份,我選擇bcm2835( http://www.airspayce.com/mikem/bcm2835/ ) 而不是比較知名的wiringPi。因為bcm2835比較單純(從mmap找到address->讀寫bit控制),不需要任何驅動程式,而wiringPi的SPI跟I2C控制是建立在驅動程式上,我希望能藉此避免驅動程式的介入把操縱單純化,這樣以後若是用Preempt RT kernel也比較不受影響。

目前我的進度到PID Control(調的好累),希望不久後我的PiQuadcopter能很快飛起來。

參考資料

http://blog.oscarliang.net/build-a-quadcopter-beginners-tutorial-1/

2015年8月4日 星期二

Raspberry pi 外部裝置程式設計

先說基本規則

任何對於硬體的操作都必須經由「讀寫記憶體(memory)中的某個位址(address)」來完成。
舉例來說,想要使連在某gpio的LED亮起來,就要在對應在此gpio的位址上寫入"1"。
所以,想要操縱Raspberry pi,要嘛透過與驅動程式(driver)溝通來完成,像這樣:
使用者 <-> module(driver) <->記憶體
要嘛自己跟硬體溝通:
使用者 <->記憶體

這兩種方法各有其優缺點。用驅動程式當然方便很多,至少不用去看Manual。可是硬體的反應速度可能會因為透過驅動程式間接傳遞而變慢。直接跟記憶體溝通雖然方便,但是每次都要用sudo,而且少了驅動程式確認使用者行為可能會因為不小心而有燒壞硬體的風險。所以自己想一下喔。

如何找位址

Raspberry pi 使用的SoC是BCM2835(屬於BCM2708家族的一員,由於這個家族只有BCM2835可以跑linux,所以要在kernel上找相關模組的話請用bcm2708)。現在來看一下BCM2835的address mapping:

圖片來源:BCM2835 ARM Peripherals Manual

這邊只講Peripheral(外部裝置)的部份:I/O Peripherals的bus address(BCM2835 ARM Peripherals Manual 上面列的都屬於bus address)從0x7E000000~開始,但是經由VC/ARM MMU (Memory management unit) 將bus address 映照到ARM實體位址(physical address)的0x20000000~0x20FFFFFF,然後又被MMU映照到虛擬位址(virtual address) 的0xF2000000。

一般來說,Linux設備驅動程式是不能直接存取實體位址的,只能使用虛擬位址去控制硬體。至於使用者寫的軟體或程式,可以看情況使用實體或虛擬位址控制硬體,不過有兩個例外:當程式需要使用DMA去讀取Peripheral或RAM的時候必須用bus addresses;而當程式需要直接讀取RAM時必須要用實體位址。

如果只有一個使用者在操作peripherial就算了,如果有可能多人或多程序同時操作,必須避免一個程序進行寫入/讀取的同時,另一個程序也進行寫入/讀取的動作(尤其是使用中斷模式的時候),所以寫入/讀取的同時加上mutex lock(互斥鎖)是一定要的。

使用I2C

I2C主要靠兩條雙向訊號線完成溝通:SCL和SDA:SCL是由master端輸出的clock,SDA負責傳送data(雙向)。這兩條訊號線必須連接到"固定電壓(3.3V或5V都可)+上拉電阻"(raspberry pi應該有內建,不用擔心),因為SCL和 SDA2. 都是屬於"open drain"電路:只能調低為0不能調高為1。標準模式的SCL clock頻率為100kHz,快速模式為400kHz,高速模式可到3.4MHz(不過傳輸速度必須master跟slave兩端都支援同樣模式才可行,而Raspberry pi據我所知核心clock的頻率為250MHz,i2c的傳輸速度可以藉由調整CDIV(divider)這個位址來達成(Ex: CDIV = 2500,SCL clock = 250M/2500 = 100kHz))。

圖片來源:http://www.robot-electronics.co.uk/acatalog/I2C_Tutorial.html

當master端想要跟slave溝通時,必須先給一個開始訊號(start sequence):SCL=1,SDA由1變成0;若是想停止溝通則給結束訊號(stop sequence):SCL=1,SDA由0變成1

圖片來源:http://www.robot-electronics.co.uk/acatalog/I2C_Tutorial.html

通常一個I2C可以同時接很多個slave(也有多數master的情況,不過這邊不談),可是master在同一時間只能跟其中一個slave溝通。當開始訊號送出後,master必須送出一個slave address(slave address的data sheet通常會提供)以告知所有的slave它將跟哪個slave溝通。通常i2c address可使用7 bit或10 bit(我沒有可用10bit的slave device,所以這邊不談)。以我的RTC DS3231為例,他的slave address是0x68(1101000),而i2C傳輸資料的時候是從高位數開始傳,傳完7bit後加一個bit顯示寫入或讀出,然後slave會回傳一個bit表示接到資料了,所以以我的RTC DS3231來說就會像這樣: 1->1->0->1->0->0->0->1(1表示寫入,0表示讀取)->A,最後的A表示是從slave傳來的確認bit。

然後就是讀取/寫入slave資料的部份。通常一個slave可以讀取/寫入的位址不只一個,所以在做讀取/寫入的時候,必須要先指定一開始讀取/寫入的位址還有想要讀取的長度(byte)。以RTC DS3231為例,若是要讀取RTC DS3231的時間,就要從位址0開始,依序讀取秒、分、時、星期、日、月、年(各1 byte)。所以一個讀取時間的程序為:
  1. 開始訊號(start sequence)
  2. 傳送slave address(0x68) (寫入), slave 回傳ACT
  3. 傳送讀取開始位址(0)(寫入), slave 回傳ACT
  4. 開始讀資料,在每個byte完結後slave 回傳ACT
  5. 讀完傳送結束訊號
至於一個寫入時間的程序只有一點不一樣:
  1. 開始訊號(start sequence)
  2. 傳送slave address(0x68) (寫入), slave 回傳ACT
  3. 傳送寫入開始位址(0)(寫入), slave 回傳ACT
  4. 開始寫資料,在每個byte完結後slave 回傳ACT
  5. 寫完傳送結束訊號

使用SPI

一組SPI傳輸中只能有一個master,slave可以有好幾個。SPI有四條傳輸線,分別是:
  • SCLK : Serial Clock,由master輸出
  • MOSI : master 輸出到slave
  • MISO : slave輸出到master
  • SS : 選擇slave,由master輸出
參考資料:
  • Low Level Programming of the Raspberry Pi in C
    http://www.pieter-jan.com/node/15
  • BCM2835 ARM Peripherals Manual
  • Using the I2C Bus 
    http://www.robot-electronics.co.uk/acatalog/I2C_Tutorial.html
  • http://wiki.csie.ncku.edu.tw/embedded/SPI

2015年7月23日 星期四

使用 device tree overlay控制industrial i/o

終於把我第1個device tree overlay生出來了!要趕快把心得寫下來。

Device tree的功用

想像若是沒有device tree,為不同的處理器寫kernel modules會變成一件看起來複雜實際上很簡單的事,舉例來說,以raspberry pi model B+跟raspberry pi 2來說,這兩者只有SoC、CPU、記憶體大小不一樣,但是其他該外部設備(I2C、SPI等)都差不多。可是若是沒有device tree,寫kernel module的時候就必須把以下步驟各做一次
  1. 弄一個machine type id
  2. 在kernel裏面建立關於此id的相關文件,設定SoC的相關代碼(包括外部設備如interrupt、timer、memory mapping等等)還有board-specific文件
  3. 設定其他的driver
但是現今的SoC都大同小異,了不起就是pin(gpio、I2C等)的位置不一樣,為了這些小差異,要把上面那三件事重做一次,增加一倍的coding到kerenl,使得kernel最後越來越肥搞到Linus本人都出來罵。Device tree的作法就是把外設資訊(怎麼連接、哪個memory mapping等)以bootloader傳送給kernel,讓kernel把外設需要的module根據Device tree的訊息連接起來。

實際做起來還挺有趣的。我自己寫了兩個可以在raspberry pi model B+連接industrial i/o (iio) driver用的device tree

MCP3008(adc)

如果編譯kernel的時候有勾選industrial i/o driver的時候就可以使用
可以在/lib/modules/{uname -r}/modules.alias找到這個module :
alias spi:mcp3008 mcp320x

根據kernel document 的說明
https://www.kernel.org/doc/Documentation/devicetree/bindings/iio/adc/mcp320x.txt
我寫了mcp320x.dts:

/dts-v1/;
/plugin/;

/ {
    compatible = "brcm,bcm2835", "brcm,bcm2708";
    /* disable spi-dev for spi0.0 */
    fragment@0 {
        target = <&spi0>;
        __overlay__ {
            status = "okay";
            spidev@0{
                status = "disabled";
            };
        };
    };

    fragment@1 {
        target = <&spi0>;
        __overlay__ {
            /* needed to avoid dtc warning */
            #address-cells = <1>;
            #size-cells = <0>;
            mcp3x0x@0 {
                compatible = "mcp3008";
                reg = <0>;
                spi-max-frequency = <1000000>;
            };
        };
    };
};


dts寫好後用dtc編譯:
dtc -@ -I dts -O dtb -o mcp320x.dtb mcp320x.dts
然後把mcp320x.dtb copy到/boot/overlays/
最後在/boot/config.txt加上:dtoverlay=mcp320x  (跟我寫的mcp320x.dtb做連結)
重開機後,只要mcp3008有接對應該就沒問題了。

MPU6050(六軸陀螺儀)

一樣根據 https://www.kernel.org/doc/Documentation/devicetree/bindings/iio/imu/inv_mpu6050.txt 來編輯mpu6050.dts

// Definitions for MPU6050
/dts-v1/;
/plugin/;

/ {
        compatible = "brcm,bcm2708";

        fragment@0 {
                target = <&i2c1>;
                __overlay__ {
                        #address-cells = <1>;
                        #size-cells = <0>;
                        status = "okay";
                        clock-frequency = <400000>;

                        inv-mpu6050@68 {
                                compatible = "invensense,mpu6050";
                                reg = <0x68>;
                                interrupt-parent = <&intc>;
                                interrupts = <2 22>;
                        };
                };
        };
};


編譯後放到/boot/overlays,/boot/config.txt上加入:dtoverlay=mpu6050

如果想要debug,可以在/boot/config.txt上加入:dtdebug=1
重開機後執行sudo vcdbg log msg 就可看device tree載入訊息:

PS: 記得看看interrupt有沒有衝到(dmesg |tail),如果有衝到interrupts的值+1+2然後重開機應該就沒問題了。
參考資料:
  • Device Tree 背景介紹
    http://www.wowotech.net/linux_kenrel/why-dt.html
  • Device Trees, Overlays and Parameters
    https://www.raspberrypi.org/documentation/configuration/device-tree.md
  • https://patchwork.ozlabs.org/patch/464158/

2015年7月2日 星期四

使用 iio device driver 驅動 DHT11

先說結論:
  • 現在新版的kernel(我用3.18以上)都有附一些硬體的iio device driver,例如DHT11
  • 使用方式請看 /boot/overlays/README
  • 不過就DHT11的部份:它不是很好用(失敗率超高,還沒我自己寫的穩定)
大略說一下使用方法(根據/boot/overlays/README),假設DHT11已經接好,使用gpio pin2讀取資料:
  • 確認kernel已經含有Industrial I/O support (在Device Driver下)
(我是懶得看直接把Industrial I/O support內的細項都選了,因為以後可能用得到,不想花時間的話只選dht11也行)
  • 編輯/boot/config.txt (記得用sudo)
    • 加入:dtoverlay=dht11,gpiopin=2,存檔離開
    • 重新開機
如果一切OK,在/dev下應該會看到有裝置iio:device0,這個裝置就是dht11

然後進到/sys/bus/iio/devices/iio:device0:
可以看到這些variables,他們代表:
dev : device number
in_humidityrelative_input : 溼度(RH%)*1000 (老實說我不知道為何要*1000)
in_temp_input:溫度(C)*1000
name: 名稱(預設為dht11)
uevent:一些設定

用 Raspberry pi 寫驅動程式 -- 範例2:DHT11

這邊會用傳統kernel的code驅動DHT11(測量溫度跟溼度)
DHT11的規格與使用方法在此

PS: 其實Raspberry pi已有內建的溫度測量(不過是測量raspberry pi 機板的溫度,不是室溫喔):在terminal下執行以下命令即可。
/opt/vc/bin/vcgencmd measure_temp

DHT11有四個接腳,接法如下(圖片來源:www.aosong.com):
這邊VDD可以用raspberry pi 提供的3.3V或是5V source(我是用5V,據說會穩定點),Pin2的data out就接gpio。由於DHT11是使用1-Wire傳輸協定傳送data,所以只需要1個gpio。
1-Wire協定有點像這樣(以下為擬人化示範):
使用者:喂!把資料丟給我阿!(傳送一個已經約定好長度的訊號,然後等接收)
DHT11:OK!我要丟囉!(傳送一個已經約定好長度的訊號表示OK,然後準備資料)
DHT11:OO_OO___OOO__OOO(O表示0,_表示1,全部傳好後你自己翻譯吧)
這邊由於使用者要先送一個訊號出去,DHT11才會開始發送data。如果在DHT11發送data的時候,另一個白目的使用者也送訊號說要data,DHT11就會發生錯亂,這樣前後兩個使用者可能都不會收到正確data(嚴重時會造成crash)。所以我們必須要避免這種情況。

再說DHT11的一些優缺點。
優點:便宜、穩定、體積小、功耗低
缺點:測量範圍略小(20-90%RH,0-50°C)、反應慢(讀取間隔至少1s)、精度低(溼度+-5%RH,溫度+-2°C)、讀取失敗率有點高

基於這些優缺點,DHT11不適合以高頻率(>1/s)密集測量,所以這次的驅動程式規格為:
  • gpio pin預設值為2,載入時可更改設定
  • 將DHT11設成字元裝置/dev/DHT11
  • 當使用者"開啟"/dev/DHT11時,執行測量(並自動檢查數據結果,若是無效數據可以重試五次以內)
  • 當使用者"讀取"/dev/DHT11時,告訴使用者測量的結果 (與上一個步驟合併就是"cat"啦)
  • 當有另一個使用者想要開啟已經被開啟的/dev/DHT11時,告訴此使用者系統忙碌。
這邊又到了沒圖沒真相的時候:

所以在這個例子裡,struct file_operations需要定義的有:.owner(模組本身),.open(開啟字元設備檔案的動作),.read(讀取字元設備檔案的動作),.release(關閉字元檔案的動作)
static struct file_operations fops = {
        .owner = THIS_MODULE,
        .read = read_dht11,
        .open = open_dht11,
        .release = close_dht11
};



開啟字元檔案的時候,要做這些事:
  •  如果設備沒有被人佔用,那第一件事就要做個「佔用」的動作(mutex_lock)。
  •  重新初始一個「完成量」(completion):由於每讀取一次DHT11的資料至少需要4ms,必須要有一個機制告訴系統「我們做完了,可以繼續了!」,不然就是限時做完不然系統不等了。這邊的completion就是要做這件事。
  • 設定指定的gpio為output,停個20ms後(這邊用msleep為宜,這樣CPU就不用守在那)給個約40us的信號,然後轉成input並紀錄時間
  • 進入中斷(interrupt)處理程序以便接收資料(後面解釋)
  • 接收完就把中斷處理程序取消掉
  • 開始處理資料,如果資料不正確就等1s後重新讀取(從"設定指定的gpio為output"開始...)

static int open_dht11(struct inode *inode, struct file *file)
{
    char result[3];                     //To say if the result is trustworthy or not
    int retry = 0, ret;

    // 試著"鎖上",如果鎖成功表示沒人佔用,不成功則相反
    if (!mutex_trylock(&gpio_mutex)) {
        printk(KERN_ERR DHT11_DRIVER_NAME " another process is accessing the device\n");
        return -EBUSY;
    }

    // 重新初始完成量
    reinit_completion(&gpio_completion);
    printk(KERN_INFO DHT11_DRIVER_NAME " Start setup (read_dht11)\n");

start_read:
    nBit = 0;
    // gpio output
    gpio_direction_output(gpio_pin, 0);
    msleep(20);                                 // DHT11 needs min 18mS to signal a startup
    gpio_direction_output(gpio_pin, 1);
    udelay(40);                                 // Stay high for a bit before swapping to read mode
    // gpio轉成input
    gpio_direction_input(gpio_pin);

    //Start timer to time pulse length
    do_gettimeofday(&lasttv);

    // Set up interrupts
    setup_interrupts();

    //Give the dht11 time to reply
    //這邊的 HZ是系統給定,在RPi=100,在這裡也就是說過了1/100秒如果還沒完成就不等了
    ret = wait_for_completion_killable_timeout(&gpio_completion,HZ);
    //取消中斷
    clear_interrupts();
    //Check if the read results are valid. If not then try again!
    if(dht11_decode()==0) sprintf(result, "OK");    // 用checksum確認是否是正確的值
    else {
        retry++;
        sprintf(result, "BAD");
        if(retry == 10) goto return_result;             //We tried 5 times so bail out
        ssleep(1);
        goto start_read;
    }

return_result:
    sprintf(msg, "Humidity: %d.%d%%\nTemperature: %d.%dC\nResult:%s\n", dht11.Humidity[0], dht11.Humidity[1], dht11.Temperature[0], dht11.Temperature[1], result);
    msg_Ptr = msg;
    printk("strlen is %d", strlen(msg));
    return SUCCESS;
}


然後關閉檔案的步驟就超簡單,把互斥鎖解掉就好了:
static int close_dht11(struct inode *inode, struct file *file)
{
    mutex_unlock(&gpio_mutex);
    printk(KERN_INFO DHT11_DRIVER_NAME ": Device release (close_dht11)\n");

    return 0;
}


至於中斷(interrupt)是個超級好用的東西,我們可以想像一個CPU在沒有中斷的情況下要怎麼工作(以下擬人化):
CPU:賈爸鎂?        硬體A:阿鎂
CPU:賈爸鎂?        硬體B:阿鎂
CPU:賈爸鎂?        硬體C:阿鎂
CPU:賈爸鎂?        硬體D:阿鎂
....問到Z以後,重來
(以上就是所謂的「輪詢」(polling))

若是有中斷就好多了:
硬體A:挖巴豆夭阿      CPU:ㄆㄨㄣ來囉
硬體C:挖巴豆夭阿      CPU:ㄆㄨㄣ來囉
硬體F:挖巴豆夭阿      CPU:ㄆㄨㄣ來囉
......(以上就是誰先靠腰誰就先吃的中斷模式)

至於讀取DHT11為何要用到中斷呢?
看DHT11的使用說明,當系統/使用者給了他一個40us的訊號以後轉接收模式,約80us後DHT11會送個約80us長的訊號表示他收到了然後準備輸出。接下來每個bit訊號以50us(Off)開始,若是這50us Off後面接的是約70us的長訊號就是bit1,若是只有約24-26us的短訊號就是bit0(如圖,圖片來源:www.aosong.com)。

所以,當gpio接收的訊號從0轉成1或1轉成0,CPU都要去看看到底發生了啥事(看是給我bit0還是bit1),然後CPU又很忙不可能一天到晚問改變了沒,所以要使用「中斷」-- 當gpio接收的訊號改變,就同時送一個硬體訊號給CPU,告訴CPU來處理。

設定中斷的函式很簡單:
static int setup_interrupts(void)
{
    int result;
    // 跟系統提出使用中斷的請求
    // gpio_to_irq(gpio_pin) 是系統給的irq值
    // irq_handler 是告訴CPU如果中斷發生要怎麼處理
    result = request_irq(gpio_to_irq(gpio_pin), (irq_handler_t) irq_handler, IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING, DHT11_DRIVER_NAME, NULL);

    switch (result) {
        case -EBUSY:
            printk(KERN_ERR DHT11_DRIVER_NAME ": IRQ %d is busy\n", INTERRUPT_GPIO0);
            return -EBUSY;
        case -EINVAL:
            printk(KERN_ERR DHT11_DRIVER_NAME ": Bad irq number or handler\n");
            return -EINVAL;
        default:
            printk(KERN_INFO DHT11_DRIVER_NAME ": Interrupt %d obtained\n", gpio_to_irq(gpio_pin));
            break;
    };

    return 0;
}

irq_handler長這樣:
// IRQ handler - where the timing takes place
static irqreturn_t irq_handler(int i, void *blah)
{
    // 測量中斷發生的時間
    do_gettimeofday(&tv);
    // 進入前已經先mark好時間,所以訊號持續的時間只要相減就好
    int data = (int) ((tv.tv_sec-lasttv.tv_sec)*1000000 + (tv.tv_usec - lasttv.tv_usec));
    // 現在的訊號,如果現在是0,就表示剛才是1
    int signal = gpio_get_value(gpio_pin);
    // mark這次中斷的時間,下次中斷的時候就能用
    lasttv = tv;    //Save last interrupt time
    // use the GPIO signal level
    //如果剛才那是1,就把持續時間放在timeBit(資料大小40bit,還要加1bit的開始)裡
    if (signal==0) {
        timeBit[nBit++] = data;
    }
    // 傳完了就告訴系統已經完全了
    if (nBit >= TOTAL_INT_BLOCK) complete(&gpio_completion);
    return IRQ_HANDLED;
}

要注意的是進行中斷處理程序的時候,CPU必須先跳開手上的工作進行中斷,結束後跳回原先的工作。如果這樣不斷跳來跳去,會大幅降低整體系統效能,所以必須極力縮短中斷程序。所以我在中斷處理程序只有紀錄訊號持續時間,解碼等中斷處理程序結束之後再說。

接下來就是解碼了:

static unsigned char dht11_decode_byte(int *timing, int threshold)
{
    unsigned char ret = 0;
    int i;

    for (i = 0; i < 8; ++i) {
        ret <<= 1;
        if (timing[i] >= threshold) ++ret;
    }

    return ret;

}

static int dht11_decode(void) {
    // 通常如果訊號是50us Off -> ~70us On 表示1
    // 50us Off -> 24-26us On 表示0
    // 可是如果你嘗試很多次,你會發現DHT11的表現並不總是那樣,會有些bit不長也不短沒法分辨的情況
    // 所以我把長度小於短bit(26us)*1.5的定義為0,長度大於長bit(70us)*2/3的定義為1
    // 如果遇到非0也非1的case,我會顯示"Poor resolution"然後重試
    int i, bitM = DHT11_DATA_BIT_LOW * 3 / 2, bitN = DHT11_DATA_BIT_HIGH*2/3, wtime=0;

    for (i=0; ibitM && timeBit[i]0) {
        printk("Poor resolution \n");
        return 1;
    }



    // 第1個bit是資料傳送開始所以不算
    // 每個資料長8bits,依序為溼度整數位、溼度小數位、溫度整數位、溫度小數位、checksum(前面四個的總和,用以確定資料無誤)
    // 不過要注意的是基本上溼度小數位跟溫度小數位都會是0(我也不知道為何,既然沒數據你加了幹嘛?)
    dht11.Humidity[0] = dht11_decode_byte(&timeBit[1], bitM);
    dht11.Humidity[1] = dht11_decode_byte(&timeBit[9], bitM);
    dht11.Temperature[0] = dht11_decode_byte(&timeBit[17], bitM);
    dht11.Temperature[1] = dht11_decode_byte(&timeBit[25], bitM);
    dht11.checksum = dht11_decode_byte(&timeBit[33], bitM);

    if (dht11.Temperature[0]+dht11.Temperature[1]+dht11.Humidity[0]+dht11.Humidity[1] == dht11.checksum) return 0;
    else return 2;
}

有了結果就可以讀取了:
static ssize_t read_dht11(struct file *filp, // see include/linux/fs.h
      char *buffer, // buffer to fill with data
      size_t length, // length of the buffer
      loff_t * offset)
{
    // 這邊我們使用一個指標來結束read_dht11的動作
    // 如果沒有 return 0,那在cat /dev/DHT11的時候,read_dht11會不斷重複。
    // (一般使用的時候是該這樣,不過這邊我並不希望dht11被頻繁讀取,因為它不是那麼靈敏)
    if (*msg_Ptr == '\0') return 0;
    if (copy_to_user(buffer, msg_Ptr, strlen(msg)+1)!=0 ) return -EFAULT;
    msg_Ptr += strlen(msg);
    return strlen(msg);
}

至於init跟exit跟之前一樣,就不複習了。

編譯後載入模組,然後測試:
pi@raspberrypi ~/work/driver/DHT11 $ sudo insmod ./dht11.ko
pi@raspberrypi ~/work/driver/DHT11 $ sudo cat /dev/DHT11
Humidity: 39.0%
Temperature: 26.0C
Result:OK
pi@raspberrypi ~/work/driver/DHT11 $



原始碼在此:
https://gist.github.com/gnitnaw/744d237d6ea2fd84e756
(PS: 本原始碼參考自http://www.tortosaforum.com/raspberrypi/dht11km.tar)
參考資料:
http://www.tortosaforum.com/raspberrypi/dht11km.tar
台灣樹莓派 <sosorry@raspberrypi.com.tw>:用 Raspberry Pi 學 Linux 驅動程式

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

2015年5月28日 星期四

Raspberry Pi : 安裝Raspbian

準備好USB介面的鍵盤跟滑鼠(我是用Logitech MK240),接條網路線(RJ45),還有無線網卡(我買了白色TOTOLINK N150USM,是mt7601,不要跟黑的搞混了),電源就用我手機的旅充湊數,再把家裡的電視搬過來接,就可以準備安裝了。

安裝第一件事就要先準備已經有Raspbian的MicroSD,官方建議Micro SD的規格為需要大於4GB,且為Class 10 (讀寫速度10MB/s)以上

依照官方網頁應該會很簡單:
https://www.raspberrypi.org/documentation/installation/installing-images/linux.md
(不過我遇到的問題是SD card adapter挑片抓不到我的micro SD,換了台電腦才抓到)

然後是安裝無線網卡,這張在kernel <3.19是起不來的,所以要先抓kernel回來。
wget http://github.com/raspberrypi/linux/archive/rpi-3.19.y.tar.gz
然後按照官方網頁的說明照做即可,建議用cross compiling(因為用pi編譯要很久,我是讓他跑了一晚上。)
若是因為像我一樣讀卡機挑片所以無法直接讀取SD卡,可以用nfs:
pi這邊安裝nfs server:

#sudo apt-get install nfs-kernel-server nfs-common 
#sudo vi /etc/exports 

/      192.168.8.0/24(rw,sync,no_subtree_check,no_root_squash)
/boot  192.168.8.0/24(rw,sync,no_subtree_check,no_root_squash)

#sudo service rpcbind restart #sudo /etc/init.d/nfs-kernel-server restart


若是遇到如下的錯誤訊息:
rpc.mountd: svc_tli_create: could not open connection for tcp6
rpc.mountd: svc_tli_create: could not open connection for udp6
rpc.mountd: svc_tli_create: could not open connection for tcp6
rpc.mountd: svc_tli_create: could not open connection for udp6
rpc.mountd: svc_tli_create: could not open connection for tcp6

可以編輯/etc/netconfig,mark掉兩行:
udp        tpi_clts      v     inet     udp     -       -
tcp        tpi_cots_ord  v     inet     tcp     -       -
#udp6       tpi_clts      v     inet6    udp     -       -
#tcp6       tpi_cots_ord  v     inet6    tcp     -       -
rawip      tpi_raw       -     inet      -      -       -
local      tpi_cots_ord  -     loopback  -      -       -
unix       tpi_cots_ord  -     loopback  -      -       -
 
 


記得開啟nfs service:
pi@raspberrypi /var/log $ sudo /etc/init.d/nfs-kernel-server start
[ ok ] Exporting directories for NFS kernel daemon....
[ ok ] Starting NFS kernel daemon: nfsd mountd.


然後另外一台電腦(我用linux)mount pi的SD card,像這樣:
sudo mount 192.168.1.21:/ /mnt/pi -t nfs

如果遇到以下訊息,請用apt-get安裝nfs-common。
mount: wrong fs type, bad option, bad superblock on 192.168.1.19:/home/shared, missing codepage or helper program, or other error (for several filesystems (e.g. nfs, cifs) you might need a /sbin/mount. helper program)

掛載無誤就能用df看到它了
192.168.1.19:/ 15417856  4941184  9799936   34% /mnt/pi

最後就是無線網卡driver,可以從這邊下載:https://github.com/porjo/mt7601
照著上面的步驟應該就OK了。要記住kernel版本一定要3.19以上阿(3.18 try了N次都不過的人留)。






2015年5月25日 星期一

幾個有趣的問題(來源:PTT C_AND_CPP 精華區)

1.

long int a, b, c;
a= 40000L;
b= 60000L;
c= a*b;

printf("c= %ld\n", c);
// 請問,c= ???, why?


這題要注意的是:
- long int 的大小是 4 bytes (32 bits)或 8 bytes (64 bits),所以 signed long int能表現的最大值為 2^31-1 = 2147483647 (32 bits) 或 2^63-1 = 9223372036854775807L (64 bits)

- 所以如果是在64 bit 電腦上,c當然沒有疑問是2400000000L。可是在32 bit電腦上,這個數字已經超過該type可以表示的極限,所以就會造成溢位(overflow)。

- 可是overflow之後的表現屬於undefined behaviour,意思是說編譯器怎麼做都可以。

2.

long a, b, c;
a= 10;
b= a + 1;// b= 11, > a

while (b > a) {
   a++;
   b+= 1;
}

printf("a= %ld, b= (a+1)= %ld\n", a, b);
// 請問, a= ???, b= ???, why?


- 這個問題比較簡單,同上一題,會run到溢位為止。
- 至於溢位以後的動作一樣是undefined behaviour
(我的電腦跑出的結果是a= 9223372036854775807, b= (a+1)= -9223372036854775808 )

3.

double a, b, c;
a= 4.0*atan(1.0);// a= 3.1415926 ...
b= a + 1.0;// b= 4.1415926 ..., > a

while (b > a) {
   a*= 1.001;
   b= a + 1.0;
}

printf("a= %.3lf, b= (a + 1.0)= %.3lf\n", a, b);
// 請問:a= ???, b= ???, why?

- standard C 並沒有規定浮點數的儲存方式,這跟各家平台對於浮點數的儲存有關,也就是說跑出的結果是implementation defined。我的機器跑出的結果是
a= 9026451774346548.000, b= (a + 1.0)= 9026451774346548.000
- 若浮點數為 IEEE754 則答案會在剛超過 2^53 (9007199254740992) 時停止。

4.

1 不是質數,請問:從 1 to 1,000,000
一共有幾個質數?

使用 什麼方法最快?
你的計算時間是多少?精確度要 小於 0.1秒。


這題有幾個重點:
- 計算時間:C的time.h 只能提供精確度到秒級的時間處理。若需要毫秒級的時間處理可使用Linux提供的<sys/time.h>

http://jyhshin.pixnet.net/blog/post/26587986-linux-%E6%99%82%E9%96%93%E8%99%95%E7%90%86

- 找質數不外乎兩種方法:
循序搜尋法(Sequential Search):一個一個跟找到的質數比較看能不能整除,不能就加入質數List...時間複雜度為O(N^2)
篩法(Sieve of Eratosthenes):把已經找到的質數倍數先篩去,再從剩下的找質數再繼續篩(快多了)。時間複雜度為O(NloglogN)

以下為我用這兩種算法做的參考答案(使用C):
https://gist.github.com/gnitnaw/9db341d4588ff5431c45

5.

100 的 階乘是多少?
要精確到 每一位?

這裡要用大數進行計算(int和long都不夠位數)。
我是直接創造一個可以擁有無限位數的class
https://gist.github.com/gnitnaw/79ccbac48a22e14b67c4
得到結果:
100!= 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

6.

強迫使用遞回呼叫的方式求 費氏數列。
第幾項: 0, 1, 2, 3, 4, 5
答案:   1, 1, 2, 3, 5, 8, 13, . . .

請問:第 100項的答案是 多少?
必須精確到 每一個 位數,

請問:你的程式 需要花多少時間完成?
精確度要 小於 0.1秒

這題不但要用大數計算還要推估大概計算時間
我是算到N=35以後用T(N)=T(N-1)+T(N-2)推算N=100的情況。
答案:
F(100) = 354224848179261915075,
所需時間推估為247170233002558582087us ~ 247170233002558582 ms
(靠!要7837716年)

7.

使用 辛普森的方法求 數值積分,
y= sin(x), x= 0 to pi 的積分值是多少?
pi= 4.0*atan(1.0)

小數點的要求是 %.22lf

0 to pi 的等分 區間數量是 n

n= 10 to 50 step 2

求出 每一個 n 所對應的 積分值?

這個不算難,一下子就寫出來了
https://gist.github.com/gnitnaw/56c6686fcbeb0c9eb3ed
N = 10, Surface = 2.0000067844418008000673
N = 12, Surface = 2.0000032688771600675182
N = 14, Surface = 2.0000017635025444384667
N = 16, Surface = 2.0000010333694131503535
N = 18, Surface = 2.0000006449719771595142
N = 20, Surface = 2.0000004230931827109430
N = 22, Surface = 2.0000002889414907336629
N = 24, Surface = 2.0000002039921938035150
N = 26, Surface = 2.0000001480922562357989
N = 28, Surface = 2.0000001100950042243198
N = 30, Surface = 2.0000000835398590304237
N = 32, Surface = 2.0000000645300017865225
N = 34, Surface = 2.0000000506327708649223
N = 36, Surface = 2.0000000402833366663913
N = 38, Surface = 2.0000000324482267721748
N = 40, Surface = 2.0000000264287591811296
N = 42, Surface = 2.0000000217426348037009
N = 44, Surface = 2.0000000180506227742683
N = 46, Surface = 2.0000000151100518763769
N = 48, Surface = 2.0000000127446351250171
N = 50, Surface = 2.0000000108245044039279


參考資料:
- http://www.csie.ntnu.edu.tw/~u91029/Prime.html
- PTT C_AND_CPP 精華區

2015年5月19日 星期二

C/C++ 小知識(一)

參考資料:PTT C_AND_CPP 精華區

implementation-defined :
The consequence of some certain syntax is desided by compiler, not standard of C/C++. The compiler is obligated to tell you how to define it.
某些語法的執行結果並沒有在C/C++標準中定義,而是由編譯器決定。
但編譯器有義務告知這些定義。

unspecified :
standard C/C++ doesn't define the rule (or give a robust definition), but the compiler will decide it without obligation to inform you.
由編譯器決定如何處理某些語法以達到符合C/C++標準的結果,但無義務告知。

undefined behavior :
Some syntax which the compilers can do what ever they want with them because standard C/C++ doesn't define it.
指C/C++標準未定義其表現的敘述,可編譯但編譯器想怎樣就怎樣。
編譯器不會指出這些語法有問題,但這會是潛在的炸彈。

2015年5月15日 星期五

樹莓派 (Raspberry Pi) 簡介

拖了好久才補心得文...

樹莓派 (Raspberry Pi) 是一個廉價(我買的時候花了35歐元左右,現在二代出了應該更便宜)又只有信用卡大小的迷你電腦,由英國的樹莓派基金會開發。只要買一般usb介面的鍵盤滑鼠接上,在加上一台有hdmi接頭的螢幕或電視就能當電腦用了。

在樹莓派上,你可以寫程式、上網(不過如果要無線上網的話要另外買無線網卡)、看高畫質影片、做文書處理、甚至打電動。

我買的是model B+,所以以下就只針對這型號昨介紹。

系統單晶片(System on chip):Broadcom BCM2835
這是俱備多媒體與高畫質(1080p30 Full HD)應用以及低成本的系統單晶片,支援HDMI輸出與高速高解析度camera的影像解析。

CPU : 700 MHz single-core ARM1176JZF-S (ARM v6)

GPU : Broadcom VideoCore IV @ 250 MHz (支援1080p與高解析度camera,Nokia 808 PureView也是用這款GPU),OPENGL ES 2.0

記憶體(Memory):SDRAM 512MB (與GPU共享)

USB 2.0 接口:4個 (model B+的過流保護跟熱插拔支援較好)

影像輸入:15-pin MIPI camera interface (CSI) connector

影像輸出:HDMI,最高解析度可達1920*1200

音源輸入:I2S

音源輸出:3.5mm TRS端子(一般電腦輸出那種),I2S,或轉為HDMI輸出

資料儲存裝置:MicroSD

額定功率:600 mA (3.0 W)

網路:10/100Mpbs ethernet (RJ45)

外設(Low-level peripherals):40 pins GPIO,包含UART與I2C,+3.3 V, +5 V,接地端

電源:5V (Micro USB 或GPIO)

重量:45 g


參考資料:
http://en.wikipedia.org/wiki/Raspberry_Pi
https://www.raspberrypi.org/help/what-is-a-raspberry-pi/
https://www.raspberrypi.org/products/model-b-plus/

Google Code Prettify