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是啥東西,可以看看我寫的簡介。
Raise your cup, say cheers to the moon, look down on the ground, the shadow is also drinking with me. I'm not a lonely drinker.
2012/02/29
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軟體開發環境下)。
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;
}
}
}
完成後的影片如下:
參考資料:
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;
}
}
}
完成後的影片如下:
參考資料:
- Arduino官方文件,數位腳位(digital pins),裡頭說明上拉電阻(pullup resistor)。
- mpilchfamily的Arduino Simon Says。
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的腳位說明:
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 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官方文件,LiquidCrystal程式庫,以及各個範例。
- Cooper Maa的Arduino教學文章,Lab9 在 2x16 LCD 上顯示 "Hello World" 訊息、Lab14 使用 74HC595 控制 HD44780 相容 LCD。
- Arduino控制2x16 LCD (使用LCD4Bit Library)。
- SpikenzieLabs的LCD - 101。
- ladyada的Character LCDs。
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++){
//...
}
如此即可。
參考資料:
平時寫程式時,在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官方文件:int、unsigned int、long、unsigned long。
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就會亮起。
參考資料:
電路圖(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版。
然後,因為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,會出現make: Nothing to be done for `all'.,太好啦。
這是我執行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板子是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官方網站的文件,Arduino Build Process。
- Arduino Playground(維基),Arduino from the Command Line,Windows command line build。
- Johannes Hoff: Arduino on the command line。
- Martin's Atelier: Arduino from the command line。
- Wikiid: Command line Arduino。
- AVRDUDE - AVR Downloader/UploaDEr,avrdude的參數。
- avr-gcc、avr-g++、avr-ar、avr-objdump、avr-objcopy、avr-size。
- Dependency Generation with Subdirectories using gcc。
- GNU make, how to determine a path is a file or a directory?。
- GNU Make Manual。
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同理。
// 寫好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的,但我沒找到,而且那種一組四個七段顯示器的產品,要用掃描的方式驅動。
參考資料:
首先使用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的,但我沒找到,而且那種一組四個七段顯示器的產品,要用掃描的方式驅動。
參考資料:
- Arduino官方範例Examples > Communication > ASCII Table,Examples > Control Structures > Switch (case) Statement, used with serial input。
- 4511,Doctronics的4511B BCD to 7-segment decoder driver。
- Cooper Maa的Lab7 使用七段顯示器製作倒數功能、Lab13 使用 74HC595 與七段顯示器製作倒數功能、Lab15 使用四位數七段顯示器製作計數器。