2012/02/29

Raspberry Pi的第一批10000片板子開始販售也已經賣光了

Raspberry Pi,在2012.02.29 06:00 GMT(台灣時間下午兩點)開始販賣,第一批有10000片板子(Model B)。原本我還以為會在Raspberry Pi的線上商店銷售,其實是透過Premier Farnell與RS Components,這兩家銷售商的網站,一下子就被灌爆了,我想連都連不上,唉,而且,還要註冊帳號填資料才行,我剛開始還不太清楚,不知道要到該銷售商的台灣網站還是英國網站才正確,到RS台灣網站,到現在還搜尋不到Raspberry Pi,到RS英國網站就有,不過,都不要緊了,我應該沒搶到吧,真是可惡啊。

不知道下一批要等多久。

想買的人,可以到這裡,看看台灣地區要到哪個網站下單。

至於價錢怎麼算,可以看看官方網站整理的資料

更新消息:根據官方網站上寫的,Model A原本設計為128 MB RAM,從新設計後,將會有256 MB。

如果不知道Raspberry Pi是啥東西,可以看看我寫的簡介

2012/02/27

Arduino小知識:enum與函式

之前的Arduino練習:Simon Says請你跟我這樣做,我發現一件事情,記錄如下。

一支很簡單的程式,居然無法通過編譯(在Arduino軟體開發環境下)。

typedef enum{
  Foo_E0,
  Foo_E1,
  Foo_E2,
} FooEnum;

void bar(FooEnum fe){
}

void setup(){
}

void loop(){
}

錯誤訊息:
Test:-1: error: variable or field 'bar' declared void
Test:-1: error: 'FooEnum' was not declared in this scope

什麼跟什麼啊?看不懂。

函式若以enum為參數,或是回傳型別為enum,都會出現此問題。

不過,如果我自己在cygwin命令列模式下進行編譯,則不會有問題。

上網搜尋後,在Arduino Playground發現這篇,How can I use enums as function returntypes or as parameters to functions?

大意是說,Arduino軟體開發環境在把程式檔交給編譯器之前,會在先進行一些"處理",其中一項是找出函式並加以宣告放到最前面,於是會造成上述的問題,總之,解決方法就是把enum的定義,搬到另一支.h檔,然後在.ino檔include那支.h檔,這樣就可解決了。

可以參考這一份文件,解說Arduino軟體開發環境的建置過程。

Arduino練習:Simon Says請你跟我這樣做

我看了這篇在Instructables的文章Arduino Simon Says,這邊有他完成後的影片

Simon Says這個遊戲是這麼玩的,有四個按鈕對應四個LED,程式會亂數點亮LED,你要記住順序,然後依序按下那四個按鈕,若對了就會發出成功的音效,錯了則發出失敗的音效,然後再繼續下一次。

電路很簡單,但程式落落長,讀別人的code很痛苦,所以我決定自己寫寫看。

首先練習一下Arduino板內建的上拉電阻(pullup resistor),如果你已經知道這是什麼,可以跳過。

Arduino板子上的數位腳位(digital pin),以pinMode()設定為INPUT模式時,若該腳位沒有連接到任何東西,那麼以digitalRead()讀取時,會讀到空氣、環境的值,也就是說是亂數,通常我們不希望如此,我們希望該腳位能有個明確的值,HIGH或LOW。之前的Arduino練習:以開關切換LED明滅狀態,也是如此,開關的一邊接到5V,另一邊則接地,這麼一來,不是HIGH就是LOW,不會讀到亂數。

接線如下:

從Arduino板的GND接到麵包板。 
Arduino腳位5接開關右上腳,開關左下腳接地。
Arduino腳位13接LED長腳,LED短腳接GND。



按下開關的話,開關左右兩邊連接在一起,所以腳位3會讀到LOW,而在不按下開關的情況,我們希望讀到HIGH,但是開關右下腳並沒有連接5V啊?可以利用Arduino板子內建的上拉電阻,以程式控制,如下:

void setup(){
    Serial.begin(115200);
    pinMode(13, OUTPUT); // LED,平常點亮,按下開關則熄滅
    pinMode(5, INPUT); // 接到開關
    digitalWrite(5, HIGH); // 開啟上拉電阻
}

void loop(){
    int status = digitalRead(5); // 讀取開關狀態
    digitalWrite(13, status); // 根據開關狀態控制LED明滅
    Serial.println(status);
}

其中,當腳位5設為INPUT模式時,可用digitalWrite(5, HIGH)開啟上拉電阻,如此一來,在平常時(不按下開關),就會讀到HIGH。

執行程式後,開啟序列埠監控視窗,平常會看到1,若按下開關,則為0。


好,接下來,要開始製作Simon Says。

電路圖(Fritzing格式)與程式原始碼,可在此下載

完整的電路圖如下,接下來會一部份一部份慢慢接。


從Arduino板的GND接到麵包板。

先插上四個瞬時型開關,左下腳接地。右上腳,從左到右,接到腳位5、4、3、2。


然後,開關的左上腳接LED的短腳,LED的長腳接到100 ohm電阻。


然後,從左到右,電阻的另一腳,接到Arduino板的腳位11、10、9、8。


聲音的部份。從Arduino板的腳位A0接到1k ohm可變電阻的左腳,中腳接蜂鳴器的紅線,蜂鳴器的黑線接地。將以這個可變電阻調整音量。


程式碼,我將以功能區塊劃分說明。

#define TONE_PIN A0 // 定義蜂鳴器的腳位
#define NUMBER 4 // 共有四個LED、四個開關

// 定義開關與LED的腳位
const int switches[NUMBER] = {5, 4, 3, 2};
const int leds[NUMBER] = {11, 10, 9, 8};

// 定義各LED對應的音符頻率
const int notes[NUMBER] = {
    NOTE_C4, NOTE_D4, NOTE_E4,NOTE_F4,
};

// 將使用狀態機的概念,所以要定義各種狀態
typedef enum{
    STATE_START, // 重新開始遊戲
    STATE_QUESTION, // 閃爍LED,給出問題
    STATE_ANSWER, // 等待使用者按下開關,回答問題
    STATE_CORRECT, // 正確,播放恭喜的音效,重置
    STATE_WRONG, // 錯誤,播放可惜的音效,重置
    STATE_MAX,
} State;

// 定義播放音效的資料,詳細資料請看原始碼
Melody melodys[MELODY_MAX] = {
    {noteStart, noteDurationsStart, 13},
    {noteCorrect, noteDurationsCorrect, 6},
    {noteWrong, noteDurationsWrong, 8},
};

// 以tone()播放音效,這裡就不解說了。
// 用法請看Arduino練習:loudspeaker揚聲器
void playtone(int *note, int *noteDurations, int num){
  for(int thisNote = 0; thisNote < num; thisNote++){
    int noteDuration = 3000 / noteDurations[thisNote];
    tone(TONE_PIN, note[thisNote], noteDuration);

    int pauseBetweenNotes = noteDuration * 1.30;
    delay(pauseBetweenNotes);
  }
}

// 這支程式裡有三種音效,開始遊戲、回答正確、回答錯誤
typedef enum{
    MELODY_START,
    MELODY_CORRECT,
    MELODY_WRONG,
    MELODY_MAX,
} Melody_Enum;
// 把playtone()包一層,用起來比較簡單
void playMelody(Melody_Enum me){
    playtone(melodys[me].note, melodys[me].duration, melodys[me].number);
}

// 初始化,開關的腳位,開啟上拉電阻,所以預設狀態會是HIGH,
// 之後讀取開關狀態時,若讀到LOW,代表按下按鍵
void setup(){
    for(int i = 0; i < NUMBER; i++){
        pinMode(switches[i], INPUT);
        digitalWrite(switches[i], HIGH);
       
        pinMode(leds[i], OUTPUT);
        digitalWrite(leds[i], LOW);
    }
   
    // 亂數,將以亂數製作問題
    randomSeed(analogRead(A1));
}

// 只播放一個音符的函式,秀出問題時可用,
// 使用者回答時也可用。
void playOneTone(int note, float delayScale){
    int noteDuration = 3000 / 8;
    tone(TONE_PIN, note, noteDuration);
   
    int pauseBetweenNotes = noteDuration * delayScale;
    delay(pauseBetweenNotes);

}

// 準備好問題後,放出音效、閃爍LED
void playQuestionsTone(){
    for(int i = 0; i < q_num; i++){
        digitalWrite(leds[questions[i]], HIGH);
        playOneTone(notes[questions[i]], 1.3);
        digitalWrite(leds[questions[i]], LOW);
    }
}

// 檢查問題跟使用者的回答是不是一樣
boolean check(){
    for(int i = 0; i < q_num; i++){
        if(questions[i] != answers[i]){
            return false;
        }
    }
    return true;
}

// 然後就是重頭戲了
void loop(){
    switch(state){ // 以state記錄目前狀態,根據狀態作事
        case STATE_START:{ // 開始遊戲,播放開始音效,進入問題狀態
            reset();
            playMelody(MELODY_START);
            state = STATE_QUESTION;
            break;
        }
       
        case STATE_QUESTION:{ // 問題狀態,以亂數製作問題
            questions = (int *)(malloc(sizeof(int) * q_num));
            answers = (int *)(malloc(sizeof(int) * q_num));
            for(int i = 0; i < q_num; i++){
                questions[i] = random(0, NUMBER);
            }
            answer_num = 0;
            playQuestionsTone(); // 顯示問題
            lastClickTime = millis(); // 記錄目前時間
            state = STATE_ANSWER;
            break;
        }
       
        case STATE_ANSWER:{
            // 看看使用者多久沒按開關了,超過10就宣布失敗
            const unsigned long nowTime = millis();
            if(nowTime >= lastClickTime + 10000UL){
                state = STATE_WRONG;
                break;
            }
           
            // 讀取每個開關的狀態
            for(int i = 0; i < NUMBER; i++){
                int ss = digitalRead(switches[i]);
                if(ss == LOW){
                    digitalWrite(leds[i], HIGH);
                    lastClickTime = nowTime;
                    answers[answer_num] = i; // 把使用者的輸入答案存起來
                    answer_num++;
                    playOneTone(notes[i], 1);
                    digitalWrite(leds[i], LOW);
                    delay(200);
                    break;
                }
               
            }
           
            if(answer_num >= q_num){ // 判斷是不是已經回答完了
                // 檢查回答正不正確
                state = check() ? STATE_CORRECT : STATE_WRONG;
            }
            break;
        }
       
        case STATE_CORRECT:{ // 回答正確,恭喜
            q_num++;
            playMelody(MELODY_CORRECT);
            delay(2000);
            state = STATE_START;
            break;
        }
       
        case STATE_WRONG:{ // 回答錯誤,可惜
            playMelody(MELODY_WRONG);
            delay(2000);
            state = STATE_START;
            break;
        }
       
        default:{
            state = STATE_START;
            break;
        }
    }
}

完成後的影片如下:



參考資料:


2012/02/23

Arduino練習:以LiquidCrystal程式庫控制LCD(相容於Hitachi HD44780)

之前用四個七段顯示器製作時鐘用蜂鳴器演奏周杰倫的青花瓷,這一篇是關於LCD(Liquid Crystal Display)。

我的板子是Arduino Uno Rev 3,軟體開發環境為1.0版、Windows版。

電路圖(Fritzing格式)與程式原始碼,可在此下載

LCD有文字型與圖像型,有大有小。


我買的是相容於Hitachi HD44780的文字型LCD,2x16(2列、一列16個字),無背光,14隻腳。有背光的會多2隻腳,一隻接電源,一隻接地。

可以顯示英文字母、希臘字母、標點符號以及數學符號,還能往左和往右捲動、顯示游標等等。

這是我買的LCD,型號是LMC-STC2A16DRG。接腳有兩排、一排7個針腳。(有些型號為一排14個針腳。)


HD44780相容型LCD的腳位說明:

腳位 說明
1(Vss) 接地(0V)
2(Vdd) 電源(+5V)
3(Vo) 對比(0~+5V),接可變電阻調整對比
4(RS) Register Select:
  若為1,把D0~D7放進資料暫存器
  若為0,把D0~D7放進指令暫存器
5(R/W) Read/Write mode:
  若為1,從LCD讀取資料
  若為0,寫入資料到LCD
6(E) Enable:1代表可寫入LCD
7(D0) Bit 0(LSB)
8(D1) Bit 1
9(D2) Bit 2
10(D3) Bit 3
11(D4) Bit 4
12(D5) Bit 5
13(D6) Bit 6
14(D7) Bit (MSB)
15(A+) 背光,電源
16(K-) 背光,接地

LCD的控制,頗為低階、複雜,還好已經有人寫好程式庫了,本篇會以LiquidCrystal這套程式庫控制LCD。

電路圖:

從Arduino板把5V與GND接到麵包板。

LCD腳位1(Vss)接地。
LCD腳位2(Vdd)接5V。

10k ohm可變電阻,左腳接5V,右腳接地,中腳接LCD腳位3(Vo)。控制對比。

LCD腳位4(RS)接Arduino板的12。

LCD腳位5(R/W)接地。本範例只寫入不讀取。

LCD腳位6(E)接Arduino板的11。

LCD腳位11(D4 )接Arduino板的2。
LCD腳位12(D5 )接Arduino板的3。
LCD腳位13(D6 )接Arduino板的4。
LCD腳位14(D7 )接Arduino板的5。

LCD若只顯示文字,則大部分的功能只須使用4-bit模式即可,LCD腳位7、8、9、10就不用接了。


接的很亂。你可以買那種針腳為一排的LCD,加上排針,比較好接。


不知為何,我買的LCD,它腳位1跟2,跟上面列表相反。


接下來是程式碼,先寫個Hello, World!吧。

// 匯入LiquidCrystal程式庫得標頭檔
#include <LiquidCrystal.h>

// 初始化LCD,建立物件lcd,指定腳位。
// 順序是RS、Enable、D4、D5、D6、D7
// 要配合前面的接線順序
LiquidCrystal lcd(12, 11, 2, 3, 4, 5);

void setup(){
  // 指定LCD的尺寸,我用的是一列16個字,2列。
  lcd.begin(16, 2);

  // 印出Hello , World!
  lcd.print("Hello, World!");
}

void loop(){
  // 欄跟列從0開始數
  // 所以第0列是上面那一列,第1列是下面那一列。
  // 第0欄是最左邊,第15欄是最右邊。
  lcd.setCursor(0, 1); // 將游標移動第0欄、第1列。

  // 顯示板子重置後經過的秒數
  lcd.print(millis() / 1000);
}

成功後,就可以在LCD上面那一列看到Hello, World!,下面那一列看到秒數。

如果看不到畫面,調整看看可變電阻。

底下這支程式,會從序列埠接收資料,顯示在LCD上。顯示游標,指出下一個字元的位置。

#include
#define ROW_NUM 2 // 2列
#define COL_NUM 16 // 16欄(一列可顯示16個字)

int index = 0; // 游標的位置,也是下一字元的位置,範圍是0~31。
#define INDEX_MAX (COL_NUM * ROW_NUM) // 最多可輸入32個字
#define ROW(x) (x / COL_NUM) // 傳入index,可算出游標的列,0或1。
#define COL(x) (x % COL_NUM) // 傳入index,可算出游標的欄,0~15。

LiquidCrystal lcd(12, 11, 2, 3, 4, 5);

void setup(){
  Serial.begin(115200);
  lcd.begin(COL_NUM, ROW_NUM);
  lcd.setCursor(0, 0); // 設定游標位置
  lcd.cursor(); // 顯示游標
  lcd.blink(); // 讓游標閃爍
}

void loop(){
  int b;
  while( (b = Serial.read()) != -1){ // 從序列埠讀取資料
    // 若是可視字元,ASCII碼從0x20到0x7E
    if(0x20 <= b && b <= 0x7E){
      if(index == INDEX_MAX - 1){
        // 如果游標已經停在最後一個位置
        // 跳回最開頭,並且清除LCD上的顯示資料
        index = 0;
        lcd.clear();
      }

      lcd.write(b); // 輸出從序列埠收到的資料
      index++; // 指向下一個位置
      lcd.setCursor(COL(index), ROW(index));
    }
  }
}

這支程式,最後一個位置(row 1, col 15)並不會用來顯示資料,所以最大可顯示的字元個數是32 - 1 = 31個。

LiquidCrystal程式庫還有很多其他函式,可參考官方文件

譬如以cursor()、noCursor()顯示∕不顯示游標,blink()、noBlink()閃爍∕不閃爍游標,display()、noDisplay()顯示∕不顯示整個畫面,但資料沒有消失還是存在的,除了這些以外,還能scrollDisplayLeft()、scrollDisplayRight()將整個畫面向左、向右捲動,甚至開啟自動捲動autoscroll(),設定捲動方向leftToRight()∕rightToLeft(),等等,可以玩玩看。


參考資料:

Arduino小知識:int為2 bytes

之前的Arduino練習:seven-segment display七段顯示器與時鐘,我發現一件事情,記錄如下。

平時寫程式時,在Windows上、在Linux、在iPhone上、等等,int或unsigned int通常會是4 bytes,但C語言標準並沒有嚴格定義,各平台與實作可根據需求調整。而在Arduino上,int與unsigned int為2 bytes。

一天總共有86400秒,我在程式裡,想從0數到86399,程式碼如下:

for(unsigned long i = 0; i < 24*60*60; i++){
  //...
}

可是卻發現,只會從0數到20863,咦,20864跑哪去了?unsigned long為4 bytes,明明空間足夠啊?

問題出在24*60*60:

24,為常數,型別會是int,沒問題。

24*60=1440,為常數,型別會是int,沒問題。

1440*60=86400,為常數,型別會是int,有問題,86400超過int能表示的範圍。

86400的二進位表示法為:
1 0101 0001 1000 0000
超過2 bytes,所以最前面那個1會不見,剩下:
0101 0001 1000 0000,也就是20864

解決方式是在整數常數後面加上L或UL,代表型別為long或unsigned long。

for(unsigned long i = 0; i < 24UL * 60UL * 60UL; i++){
  //...

}

如此即可。


參考資料:


Arduino練習:光敏電阻

之前以可變電阻控制呼吸燈的循環時間,這篇要以光敏電阻(photo resistor、photocell、light dependent resistor)控制LED的明滅。

電路圖(Fritzing格式)與程式原始碼,可在此下載

電路圖:

LED,長腳接腳位13,短腳接GND。

光敏電阻,一腳接5V,另一腳接到腳位A0與10k ohm電阻,電阻的另一腳接地。






// 從腳位A0讀取光敏電阻的值。
// 以腳位13控制LED。
void setup(){
  Serial.begin(115200);
  pinMode(A0, INPUT);
  pinMode(13, OUTPUT);
 
  digitalWrite(13, LOW);
}

int pr_min = 400;

void loop(){
  // 以analogRead()讀取光敏電阻的值,會回傳0~1023之間的值。


  int pr = analogRead(A0);

  // 並且把值輸出到序列埠,請用手遮蔽光敏電阻,看看變化。

  Serial.println(pr);

  // 若大於這個值,熄滅LED,若小於就點亮。
  // 請視需求修改pr_min。
  digitalWrite(13, pr > pr_min ? LOW : HIGH);
 
  delay(1000);
}

完成後,當外界光線強時,光敏電阻會讀到較大的值,LED就會熄滅;外界光線弱時,會讀到較小的值,LED就會亮起。


參考資料:

2012/02/21

Arduino在Windows的Cygwin命令列模式下進行編譯與上傳

我的電腦是Windows XP加上Cygwin。
我的Arduino板子是Uno(Rev 3),軟體開發環境為1.0版。

用了Arduino的軟體開發環境一陣子後,就會覺得好慢,每次執行Sketch-Verify/Compile(按鈕是個勾勾)或File-Upload(按鈕是個朝右的箭頭),都要花上一段時間。可以到File-Preferences,勾選Show verbose output during: compilation與upload,就可以清楚地看到它到底執行了哪些動作。

除了要編譯我們寫的程式碼外,它還需要編譯位於hardware\arduino\cores\arduino底下的Arduino核心程式,但是,讓我覺得納悶的是,為什麼每次都還要重新編譯那些核心呢,之前不是已經編譯過了嗎?我並沒有去更動那些東西啊?而且,如果我修改程式後先Sketch-Verify/Compile看看能不能通過編譯,通過後,再File-Upload,它居然又從頭開始執行那些已經執行過的編譯動作,真是夠了。


總之,Arduino的軟體開發環境,實在有點遜遜的,底下要介紹,怎麼在Cygwin命令列下,以make工具與Makefile檔進行編譯與上傳。

首先,還是要安裝Arduino的軟體開發環境,我們只是不用那套視覺化開發環境,改而使用命令列模式(終端機),但仍然需要裡頭各種工具與核心程式碼。

然後,要安裝Cygwin,到這裡下載Cygwin的setup.exe,安裝步驟如下:

執行setup.exe,我們將從網路下載、安裝,所以選第一個Install from Internet。


指定安裝目錄,預設為C:\cygwin,如果沒有特殊需求,就不要更動。


除了安裝,下載回來的東西也會另外存一份。請指定某路徑。


選取網路連線設定,請根據你上網的方式進行設定。


然後,選擇要從哪台伺服器下載,基本上都是國外的,速度慢,可以自行輸入"ftp://ftp.ntu.edu.tw/cygwin/",使用台灣的。


然後選擇要安裝哪些軟體,預設並不會安裝make,所以我們要自己手動勾選。


在Search欄位輸入"make",在Devel分類下,勾選make: The GNU version of the 'make' utility。應該要顯示3.82.90-1或類似的版本編號。


然後就是下載並安裝了,setup.exe可能會提示你,有些具有相依性的套件也要安裝,請選同意安裝。

我因為還灌了很多其他有的沒的,所以安裝後的C:\cygwin目錄共約700 MB。

不過,新版的make不知道為什麼,不能正確處理我底下要介紹的Makefile,所以,要把make改回3.80版。開啟cygwin終端機後,以底下步驟,改為3.80版。

cd /usr/bin
mv make.exe make_382.exe
wget http://geant4.cern.ch/support/extras/cygwin/make.exe
chmod +x make.exe

然後,因為make 3.80版需要某程式庫,所以請再執行一次setup.exe,加裝"libintl2"這個程式庫。


檢查一下,在cygwin終端機上執行make --version,應該要出現3.80字樣。

我已經提供一份範例,可到這裡下載,按下ZIP按鈕可打包下載。接下來我將使用這個範例作說明。


在BlinkMakefile目錄裡,總共有6支檔案BlinkMakefile.cpp、foo.c、foo.h、bar.cpp、bar.h、Makefile。(其他目錄是我的Arduino練習。)

BlinkMakefile.cpp,裡頭的程式就是讓腳位13的LED閃爍,不同的是,我們需要在檔案開頭加上#include <arduino.h>,這是因為,以前,arduino軟體開發環境會幫我們做這件事情,現在要自己來。

為了當示範,所以我還加了一支C程式檔foo.c與它的標頭檔foo.h,裡頭很簡單就是一個叫做delayDuration的函式,固定回傳1000,在BlinkMakefile.cpp裡,會把這個值當做點亮LED的時距。

還加了一支C++程式檔bar.cpp與它的標頭檔bar.h,裡頭就是個ClassBar類別定義,以成員函式getDelayDuration回傳一個值,但這個值每次呼叫會加100,第一次回傳100、第二次回傳200、等等,在BlinkMakefile.cpp裡,會把這個值當做熄滅LED的時距,所以熄滅時間會越來越長。

然後就是主角Makefile了,恕我無法詳細講解其語法了,底下說明你應該修改的地方:

原本Arduino軟體開發環境,程式檔使用.pde或.ino副檔名, 現在請全部改成.cpp。

PROJECT = BlinkMakefile
這是最後產出檔案(.elf、.hex、.eep)的檔名,可以改成任何你想要的名稱。

ifndef PROJ_SOURCE
PROJ_SOURCE := $(wildcard *.c) $(wildcard *.cpp)
endif
上面會找出你專案的原始程式碼(.c與.cpp),請把這些檔案放在跟Makefile同一個目錄裡即可。請不要使用子目錄。

MCU = atmega328p
板子上的微控制器是哪顆,填寫正確編譯器才知道要產出何種機械碼。我的板子是Arduino Uno Rev 3,所以填atmega328p。不僅編譯器需要這個參數,上傳時也需要。

F_CPU = 16000000L
微控制器的時脈。為了相容性,不管什麼板子,好像都是一樣的,所以應該不用改。

ARDUINO_VERSION = 100
Arduino軟體開發環境的版本編號,這會傳入編譯器,就可以在程式碼裡使用這個值。

FORMAT = ihex
輸出格式,應該不用改。

PORT = \\.\COM3
序列埠連接埠號,這就是軟體開發環境Tools-Serial Port的設定。你可能覺得奇怪\\.\是幹嘛的,這跟上傳工具的參數指定、Windows路徑、等等有的沒的有關係,總之這樣寫就不會遇到奇怪的問題。

UPLOAD_RATE = 115200
序列埠連接埠號的傳輸速率,一般都是設9600,但我的板子可以用115200。上傳時需要這個參數。

INSTALL_PATH = D:/Arduino/arduino-1.0
這個路徑要指向Arduino軟體開發環境的最上層目錄。也就是有arduino.exe執行檔、examples目錄、libraries目錄、等等的目錄。請依照你的安裝路徑修改。

CORE = $(INSTALL_PATH)/hardware/arduino/cores/arduino
這個路徑要指向Arduino的核心程式碼檔案的目錄,也就是有Arduino.h、wiring_analog.c、wiring_shift.c等等檔案的目錄。INSTALL_PATH設好後,這個應該就對了。

BUILTINLIB = $(INSTALL_PATH)/libraries
內建程式庫的路徑,裡頭應該含有EEPROM、Ethernet、LiquidCrystal、等目錄。INSTALL_PATH設好後,這個應該就對了。

SKETCHBOOK = D:/Arduino/sketchbook
自己寫的程式碼的路徑。

THIRDPARTYLIB = $(SKETCHBOOK)/libraries
第三方程式庫的路徑,也就是你自己額外安裝的程式庫。SKETCHBOOK設好後,這個應該就對了。

AVRDUDE_PROGRAMMER = arduino
設定要用什麼燒錄器上傳程式,arduino代表要使用微控制器裡的bootloader進行上傳。根據板子不同,可能要改成stk500、stk500v1、或stk500v2。可用參數請參考avrdude這隻工具程式的參數說明,avrdude是負責上傳(燒錄)程式的工具。

若要使用Arduino內建的程式庫,請修改Makefile裡的
BUILTINLIB_ENABLED =
在後面加入程式庫的名稱,例如
BUILTINLIB_ENABLED = EEPROM Ethernet Firmata LiquidCrystal
(需要設定BUILTINLIB指向內建程式庫的目錄)

若要使用額外安裝的程式庫,請修改Makefile裡的
THIRDPARTYLIB_ENABLED =
在後面加入程式庫的名稱,例如
THIRDPARTYLIB_ENABLED = BOUNCE
(當然,你要先下載程式庫、解壓縮、放進THIRDPARTYLIB指向的目錄。)

至於其他的,就屬於make的東西了,有興趣的話請自己看看囉。

接下來,開啟cygwin終端機,切換到BlinkMakefile目錄後,以底下指令進行編譯與上傳:

首先執行
make depend
根據上面設定的檔案與路徑,這會找出.c、.cpp、.h等檔案的相依關係,並附加在Makefile的後面。

make
這會開始編譯、連結,成功的話,最後會出現BlinkMakefile.elf、BlinkMakefile.eep.hex、BlinkMakefile.eep。(以及其他.o檔。)

若再執行一次make,會出現make: Nothing to be done for `all'.,太好啦。

make upload
這會開始上傳,將程式燒錄到板子裡。

make clean
這會把make產生的檔案通通刪除。我這支Makefile,會把中間目的檔.o、以及其他檔案,直接產生在原始程式檔的目錄裡。

這是我執行make的畫面。




這是我執行make upload的畫面。




 完成啦。

不過,有個小問題,原本的Tools-Serial Monitor序列埠監看視窗呢?沒關係,我們可以用Windows裡的超級終端機或是Tera Term取代。

先介紹超級終端機(開始-程式集-附屬應用程式-通訊-超級終端機),開啟後:

嗯,大家上ptt應該都用別的程式吧,這裡請勾選"請不要再詢問我這個問題",然後按"否"。


填入一個名稱,隨便選個圖案。


若要求你輸入區碼,隨便輸入一個數字。
設定連接Arduino的序列埠號。


底下是序列埠的傳輸設定。
每秒傳輸位元,我使用115200,如果不行,請試試看一般的9600設定值。
資料位元,8。
同位檢查,無(N )。
停止位元,1。
流量控制,無。


連線成功後,就可以看到BlinkMakefile傳過來的LED明滅時距,每次點亮固定為1秒,第一次滅掉為0.1秒,每次延長0.1秒,所以熄滅時間會越來越長。


以上步驟成功後,在「開始-程式集-附屬應用程式-通訊」裡會多出一個超級終端機目錄,裡面有將以上設定儲存起來後的捷徑。

成功啦。

不過Windows附的超級終端機HyperTerminal,年久失修,Windows Vista、Windows 7裡頭已經沒有它的身影了。還好,可以用Tera Term。

Tera Term網站下載,我下載的是teraterm-4.72.zip,只要解壓縮後就可使用,不用安裝。

執行ttermpro.exe,選Serial,選你Arduino板的序列埠號。我的是COM3。


畫面看起來怪怪的,因為還沒設定好序列埠的傳輸參數。選Setup-Serial port...進行設定。請根據你的情況設定,跟我不一樣的地方應該會是Port與Baud rate,其他應該都一樣。


成功啦。


然後,選Setup-Save setup...把設定儲存起來,下次執行ttermpro.exe就會直接連接。

還有其他可用的軟體,譬如PuTTY、Roger Meier's CoolTerm


後記:
我因為裝了免費版的avast!防毒軟體,每次執行make、avr-g++、avr-gcc時,都會進行防毒掃描,變得很慢,所以我到avast!的設定裡,將這些執行檔排除在掃描外。(關閉前超過一分鐘,關閉後5秒。)


參考資料:

Arduino練習:seven-segment display七段顯示器與時鐘

之前用LED製作呼吸燈霹靂車燈以揚聲器演奏青花瓷,這篇要使用seven-segment display七段顯示器製作時鐘。

首先使用Arduino的8個腳位,直接控制七段顯示器,讓七段顯示器從0數到9。然後改用4511控制七段顯示器,就只需要4個腳位即可。然後再加上三個七段顯示器、再加上三顆4511,製作時鐘。

電路圖(Fritzing格式)與程式原始碼,可在此下載

開始吧,先直接以Arduino的腳位控制,讓七段顯示器亮起來吧。

注意:我使用的是共陰極七段顯示器,之後的4511不能搭配共陽極。

共陰極七段顯示器的腳位圖。


電路圖:

左方是Arduino的腳位,右方是七段顯示器的腳位。連接時,都加上一個220 ohm電阻。
2連到7(A)。
3連到6(B)。
4連到4(C)。
5連到2(D)。
6連到1(E)。
7連到9(F)。
8連到10(G)。
9連到5(DP)。

從Arduino的GND接到麵包版上,讓七段顯示器的3與8接地。



接好後,先寫個小程式測試看看,讓七段顯示器閃爍。
#define NUM 8
int pins[NUM] = {2, 3, 4, 5, 6, 7, 8, 9};

void setup() {               
  for(int i = 0; i < NUM; i++){
    pinMode(pins[i], OUTPUT);   
  }
}

void loop() {
  for(int i = 0; i < NUM; i++){
    digitalWrite(pins[i], HIGH);
  }
  delay(1000);
  for(int i = 0; i < NUM; i++){
    digitalWrite(pins[i], LOW);
  } 
  delay(1000);
}



其實就跟閃爍單個LED一樣,只不過現在要閃爍8個LED罷了。

接下來,該讓七段顯示器作正事了,也就是顯示數字,底下程式碼要從0數到9。

首先要定義,顯示各數字時,要點亮、熄滅七段顯示器的哪些部分。

#define NUM 8
int pins[NUM] = {2, 3, 4, 5, 6, 7, 8, 9};

// 底下就是顯示某數字時,該點亮熄滅哪些腳位的定義,
// data是個二維陣列,其中10代表有10個數字(從數字0、1、2、到9),
// 然後,每個數字有8個boolean值,
// 依序代表該不該點亮七段顯示器的A、B、C、D、E、F、G、DP。
// 這部份的資料,依序對應上面pins陣例裡的腳位順序,
// 這裡的對應關係,要按照先前的接線予以定義。
// t代表true,要點亮,
// f代表false,不要點亮。
#define t true
#define f false
boolean data[10][NUM] = {
  {t, t, t, t, t, t, f, f}, // 0
  {f, t, t, f, f, f, f, f}, // 1
  {t, t, f, t, t, f, t, f}, // 2
  {t, t, t, t, f, f, t, f}, // 3
  {f, t, t, f, f, t, t, f}, // 4
  {t, f, t, t, f, t, t, f}, // 5
  {t, f, t, t, t, t, t, f}, // 6
  {t, t, t, f, f, f, f, f}, // 7
  {t, t, t, t, t, t, t, f}, // 8
  {t, t, t, t, f, t, t, f}, // 9
};

void setup(){               
  for(int i = 0; i < NUM; i++){
    pinMode(pins[i], OUTPUT);   
  }
}

// 把七段顯示器的顯示函式獨立出來。
// 輸入數字n(從0到9)
// 根據數字n,就可從data[n]找出七段顯示器的點亮、熄滅資料,
// 然後用迴圈,逐一設定8個腳位的HIGH或LOW,
// 也就等於點亮、熄滅那8個部分
void writeNumber(int n){
  for(int i = 0; i < NUM; i++){
    digitalWrite(pins[i], data[n][i] == t ? HIGH : LOW);
  }
}

// 寫好writeNumber()後,loop()就簡單了。
// 迴圈從0到9,以七段顯示器顯示該數字,中間延遲1秒。
// 也就是說,會從0數到9,然後再從頭從0開始數。
void loop(){
  for(int n = 0; n <= 9; n++){
    writeNumber(n);
    delay(1000);
  }
}


以4511控制:

上頭直接以Arduino板的8個腳位控制七段顯示器,底下改以4511控制,以4個腳位。(若以74HC595,就能以3個腳位控制8個輸出。之前的霹靂車燈以2顆74HC595控制16個LED。)

注意,4511必須跟共陰極的七段顯示器搭配。



4511的腳位說明:

7(input A)、1(input B)、2(input C)、6(input D),4個輸入腳位,從0(0x0000)到9(0x1001),若超過9,那麼7個輸出全部會是LOW,也就是都不亮。

3(lamp test),若為LOW,那麼輸出腳位全部為HIGH,不管輸入腳位的狀態。

4(ripple blanking),當lamp test為HIGH時,若此腳位為LOW,那麼輸出腳位全部為LOW,不管輸入腳位的狀態。

5(enable/store input),若為LOW,輸出腳位隨著輸入腳位而改變,若為HIGH,輸出腳位的狀態會被鎖住不變。

13(a)、12(b)、11(c)、10(d)、9(e)、15(f)、14(g),7個輸出腳位,控制七段顯示器。

8(VSS),接地。

16(VDD),可接+3~15V。

利用4511的ABCD四個腳位輸入,控制七段顯示器的abcdefg。


電路圖:

從Arduino板接5V與GND到麵包板。

4511的腳位接線:
16(VDD)接5V。
8(VSS)接地。
3(lamp test)接5V,本例不使用此功能。
4(ripple blanking)接5V,本例不使用此功能。
5(enable/store input)接地,本例不使用此功能。

13(a)、12(b)、11(c)、10(d)、9(e)、15(f)、14(g),7個輸出腳位,接到七段顯示器相對應的腳位。
7(input A)、1(input B)、2(input C)、6(input D),4個輸入腳位,接到Arduino板的腳位2、3、4、5。

七段顯示器的腳位5(DP)接地,本例不使用小數點。(其實之前從0數到9時,也不需要使用小數點。)
共陰極七段顯示器的腳位3與8要接地。

下圖先連接4511與七段顯示器之間的接線。



然後是4511的7(input A)、1(input B)、2(input C)、6(input D),4個輸入腳位,依序接到Arduino板的腳位2、3、4、5。。



接下來就是程式碼了,從0數到9。

// 以Arduino板的4個腳位2、3、4、5控制,
// 控制4511的輸入腳位7(A)、1(B)、2(C)、6(D)。
int pins[4] = {2, 3, 4, 5};

void setup(){
  for(int i = 0; i < 4; i++){
    pinMode(pins[i], OUTPUT);   
  }
}

// 以迴圈依序設定4個腳位。
// 參數n,其第0個bit,要輸出到4511的腳位7(input A),
// 它接到Arduino板的腳位2,也就是pins[0]。
// 而取得n的第i個bit的方式是,
// 先以>>運算子把n向右位移i個bit,然後與0x01進行作&運算。
// 其餘bit同理。

void writeDigit(int n){
  for(int i = 0; i < 4; i++){
    digitalWrite(pins[i], (n >> i) & 0x01);
  }
}

// 寫好writeDigit()後,loop()就簡單了。
// 以迴圈依序顯示0到9,中間延遲1秒。
void loop(){
  for(int n = 0; n <= 9; n++){
    writeDigit(n);
    delay(1000);
  }
}

從0數到9的影片在此

時鐘:

接下來,要製作時鐘,可以顯示幾點、幾分,總共需要4個七段顯示器,以及4個4511。

電路圖:

延續前面的電路,加上3個4511,把短的線接一接。
16(VDD)接5V。
8(VSS)接地。
3(lamp test)接5V,本例不使用此功能。
4(ripple blanking)接5V,本例不使用此功能。
5(enable/store input)接地,本例不使用此功能。

從右到左的4511,分別將要控制時鐘的分鐘個位數、分鐘十位數、小時個位數、小時十位數。

注意,小時十位數(最左邊)的4511,其腳位12(b)接電阻後接地,這是因為,小時十位數的七段顯示器,只會出現0、1、2這三個數字,你可以往前看看ABCD與abcdefg的對應表,若七段顯示器只需顯示0、1、2時,其b腳位只能為HIGH,所以這裡就把4511的12(b)腳位接地不用。



然後加上3個七段顯示器,把短的線以及電阻接一接。(我220 ohm電阻不夠了,有些改成別的差不多的。)
七段顯示器的腳位5(DP)接地,本例不使用小數點。
共陰極七段顯示器的腳位3與8要接地。

注意,小時十位數(最左邊)的七段顯示器,其腳位6(b)接地,理由之前說過了。




然後,4511與七段顯示器之間的連接線。
4511的13(a)、12(b)、11(c)、10(d)、9(e)、15(f)、14(g),7個輸出腳位,接到七段顯示器相對應的腳位。小心慢慢接。



然後,4511與Arduino板的連接線。

分鐘個位數的4511,7(input A)、1(input B)、2(input C)、6(input D),4個輸入腳位,依序接到Arduino板的腳位2、3、4、5。(這個之前應該就接好了。)

分鐘十位數的4511,7(input A)、1(input B)、2(input C)、6(input D),4個輸入腳位,依序接到Arduino板的腳位6、7、8、9

小時個位數的4511,7(input A)、1(input B)、2(input C)、6(input D),4個輸入腳位,依序接到Arduino板的腳位10、11、12、13

小時十位數的4511,7(input A)、1(input B)、2(input C)、6(input D),4個輸入腳位,依序接到Arduino板的腳位A0、A1、接地、接地

注意,小時十位數的4511的2(input C)、6(input D),接地。這是因為,小時十位數的七段顯示器,只會出現0、1、2這三個數字,你可以往前看看ABCD與abcdefg的對應表,若4511只需輸入0、1、2時,其C、D輸入腳位一定是LOW,所以這裡此兩腳位接地不用。




接下來,先寫個測試程式,測試看看Arduino是否正確控制4511的輸入腳位,測試七段顯示器是否能正常顯示每個數字。

// 定義腳位
const int minute_0[4] = {2, 3, 4, 5}; // 分鐘個位數
const int minute_1[4] = {6, 7, 8, 9}; // 分鐘十位數
const int hour_0[4] = {10, 11, 12, 13}; // 小時個位數
const int hour_1[2] = {A0, A1}; // 小時十位數

// 將數字n,輸出到腳位pins,bit_count是腳位的數量
void writeDigit(const int n, const int *pins, const int bit_count){
  for(int i = 0; i < bit_count; i++){
    digitalWrite(pins[i], (n >> i) & 0x01);
  }
}

// 在setup()裡初始化,讓四個七段顯示器都顯示數字0。
void setup(){

  for(int i = 0; i < 4; i++){
    pinMode(minute_0[i], OUTPUT);
    pinMode(minute_1[i], OUTPUT);
    pinMode(hour_0[i], OUTPUT);
  }
  pinMode(hour_1[0], OUTPUT);
  pinMode(hour_1[1], OUTPUT);
 
  writeDigit(0, minute_0, 4);
  writeDigit(0, minute_1, 4);
  writeDigit(0, hour_0, 4);
  writeDigit(0, hour_1, 2);
}

// 從0數到9,其中小時十位數只能顯示0、1、2。
void loop(){
  for(int i = 0; i <= 9; i++){
    writeDigit(i, minute_0, 4);
    writeDigit(i, minute_1, 4);
    writeDigit(i, hour_0, 4);
    writeDigit(i % 3, hour_1, 2);
    delay(1000);
  }
}

成功的話,右邊三個七段顯示器就會從0數到9,最左邊的會顯示0、1、2。


接下來,要製作時鐘,從0000、0001、一直到2359,然後回到0000。

程式碼:
const int minute_0[4] = {2, 3, 4, 5};
const int minute_1[4] = {6, 7, 8, 9};
const int hour_0[4] = {10, 11, 12, 13};
const int hour_1[2] = {A0, A1};

void writeDigit(const int n, const int *pins, const int bit_count);
void setTime(unsigned long h, unsigned long m, unsigned long s);

void setup(){
  Serial.begin(115200);
  for(int i = 0; i < 4; i++){
    pinMode(minute_0[i], OUTPUT);
    pinMode(minute_1[i], OUTPUT);
    pinMode(hour_0[i], OUTPUT);
  }
  pinMode(hour_1[0], OUTPUT);
  pinMode(hour_1[1], OUTPUT);
 
  setTime(7, 32, 0); // 在程式裡寫死某時間,7時32分0秒
  // 之後我們會從外界設定時間
}

void writeDigit(const int n, const int *pins, const int bit_count){
  for(int i = 0; i < bit_count; i++){
    digitalWrite(pins[i], (n >> i) & 0x01);
  }
}

// 代表一天的millisecond,在數字後加上UL,代表是unsigned long
// 若不加上UL,會發生溢位overflow
// 24*60*60 = 86400,大於int或unsigned int(2 bytes)的範圍,
// 若不加上UL,24*60*60,正確應該是0b10101000110000000,
// 溢位後,變成0b0101000110000000,也就是20864。
#define MILLISECOND_OF_ONE_DAY (24UL * 60UL * 60UL * 1000UL)

// 這會是目前顯示在七段顯示器上的時間,單位為millisecond
unsigned long currentTime;

// 用來記住上次更新currentTime時,是在何時
unsigned long previousTimeMark;

// 設定顯示時間的函式,h小時、m分鐘、s秒
void setTime(unsigned long h, unsigned long m, unsigned long s){
  currentTime = ((h * 60 + m) * 60 + s) * 1000;
  previousTimeMark = millis();
  updateTime(0);
}

// 負責更新顯示時間的函式,
// 參數timePassed代表,距離上次更新,經過了多少millisecond
void updateTime(const unsigned long timePassed){
  unsigned long h;
  unsigned long m;
  unsigned long s;

  // 加上經過的時間後,若大於一天,就減去。
  currentTime += timePassed;
  if(currentTime >= MILLISECOND_OF_ONE_DAY){
    currentTime -= MILLISECOND_OF_ONE_DAY;
  }

  // currentTime記錄的是millisecond,
  // 底下算出小時、分鐘、秒
  s = currentTime / 1000;
  m = s / 60;
  s = s % 60;
  h = m / 60;
  m = m % 60;

  // 顯示小時跟分鐘
  writeDigit(m % 10, minute_0, 4);
  writeDigit(m / 10, minute_1, 4);
  writeDigit(h % 10, hour_0, 4);
  writeDigit(h / 10, hour_1, 2);
}

void loop(){
  // millis()會回傳,這支程式開始執行後,所經過的時間,單位是millisecond。
  unsigned long now = millis();

  // 減去previousTimeMark上次更新顯示時間的時間,
  // 若大於1秒,就呼叫updateTime()更新,
  // 傳入距離上次更新所經過的時間
  if(now - previousTimeMark > 1000){
    updateTime(now - previousTimeMark);
    previousTimeMark = now;
  }
}

成功後,就可以看到下圖了。


不過,上面的程式碼,在程式碼寫死起始時間,這可不行啊,底下修改後,透過序列埠,從外界(電腦)進行時間設定。

在setup()裡要記得加入Serial.begin(115200);。

int b[4];
int len = 0;
void loop(){
  unsigned long now = millis();
  if(now - previousTimeMark > 1000){
    updateTime(now - previousTimeMark);
    previousTimeMark = now;
  }
 
  int btemp = Serial.read();
  if(btemp != -1){
    if(0x30 <= btemp && btemp <= 0x39){
      b[len] = btemp - 0x30;
      len++;
      if(len == 4){
        setTime(b[0] * 10 + b[1], b[2] * 10 + b[3], 0);
        len = 0;
      }
    }
    else{
      len = 0;
    }
  }
}

利用Arduino軟體開發環境的Tools-Serial Monitor,可以透過序列埠、傳送資料給Arduino板子,以腳位0(RX)接收。

呼叫Serial.read()讀取1個byte,放在btemp裡,若無資料,則btemp為-1,若有資料(傳來的會是ASCII碼、0x30是數字0、0x39是數字9),放進b陣列裡存起來,以len記錄收到了多少個byte。收到4個數字後,把前面兩個數字當做小時、後面兩個數字當做分鐘,進行時間設定。

若收到數字以外的資料,重置len為0,表示之前若有收到資料,通通捨棄。

輸入2302,代表要把時間設定為23時02分。


完成囉。

改進的地方,也可以加入設定時間的開關或按鈕,譬如說,某個按鈕按一下會增加一分鐘,另一個按鈕按一下會增加一小時,之類的。

改進的地方,右邊數來第二顆4511,跟Arduino板之間的接線,其實3條即可(因為它只需顯示0到5)。

改進的地方,這個時鐘只顯示小時與分鐘,沒有秒,可加入兩顆LED,每一秒閃爍一下。好像有那種一組裡有四個七段顯示器,並且中間有兩顆LED的,但我沒找到,而且那種一組四個七段顯示器的產品,要用掃描的方式驅動。


參考資料: