SDCC 資料型態 (data type)

Posted by: 邱小新 at 下午3:15 in
typewidthdefaultsigned rangeunsigned range
bool1 bitunsignedx0 ~ 1
char1 bytesigned-128 ~ 1270 ~ 255
short2 bytessigned-32768 ~ 327670 ~ 65535
int2 bytessigned-32768 ~ 327670 ~ 65535
long4 bytessigned-2147483648 ~ 21474836470 ~ 4294967295
float4 bytessigned?1.175494351E-38 ~ 3.402823466E+38
pointer1~4 bytesgenericxx

PWM (Pulse Width Modulation) 原理與應用

Posted by: 邱小新 at 下午3:09 in

PWM (Pulse Width Modulation) 稱為脈波寬度調變,常用於直流馬達的控制、電源變換器之穩壓控制、甚至是直流轉換交流弦波的控制等,是控制直流馬達轉速最常見的方法。

duty cycle

  1. duty cycle D = DT / T
  2. 也就是指 Ymax 的部份在一個 period(T) 佔有多少比率。
  3. 台灣稱 duty cycle 為「責任週期」。
  4. 大陆称 duty cycle 为「占空比」。

duty cycle 不同說法

  1. duty cycle high time = TH / T。
  2. duty cycle low time = TL / T。
  3. duty cycle high/low = TH / TL。
  4. TH = 維持在 HIGH 的時間。
  5. TL = 維持在 LOW 的時間。
  6. T = TH+TL。

紅外線感應器

Posted by: 邱小新 at 下午4:11 in
MOTION SENSOR (PASSIVE INFRARED TYPE) MP MOTION SENSOR‘NaPiOn’

紅外線感應器的種類非常之多,通常可以區分為量子型與熱型兩大類。而量子型感應器主要動作原理是利用光電效應,例如光二極體,以及利用光電效應的 CdS、PbS 等元件,此類型最為一般所常見。

此外尚有利用焦電效應的焦電型紅外線感應器,其動作原理是熱型的代表作,而所謂焦電效應(pyroelectric effect)簡單地說就是利用溫度的變化而產生電荷之現象。因此,我們可以說若是沒有溫度變化,即無法生電荷輸出訊號。

焦電型紅外線感應器俗稱 PIR 是 Passive Infrared Sensor 的英文簡寫,它主要的感應方法為利用溫度之變化,來感應待測物體之移動。舉例來說,人在 PIR 的感應範圍之內移動,PIR 上的感應點會因為溫度的變化而轉化成電流訊號輸出去,這個訊號就可以使電燈開啟、警報器作響……,達成我們所要的需求。

但是上述情形的感應方式會有一些問題,那就是 PIR 的感應距離太近(約 1~2 公尺),而且感應點會不夠多,所以最簡單的解決方法就是為 PIR 戴眼鏡,我們可以利用凸透鏡可以將光線聚焦的原理,將較遠處的人體的紅外線能量聚焦至 PIR 元件的感應點上,如此有可大大增加了感應距離,另一方面我們再把凸透鏡集合在一起,形成一個透鏡陣列 (Lens Arrays),這樣就可以增加許多感應點。至於透鏡的材質,必須要讓人體所發出的紅外光能夠順利通過,為了避免誤動,我們選擇對可見光阻絕性好的高密度 PE 可以避掉許多光線之干擾,而減少誤動之情形。




在自然界,任何高於絕對溫度(-273℃)時物體都將產生紅外光譜,不同溫度的物體,其釋放的紅外能量的波長是不一樣的,因此紅外波長與溫度的高低是相關的。

人體輻射的紅外線中心波長為 9~10μm,而探測元件的波長靈敏度在 0.2~20μm 範圍內幾乎穩定不變。在熱釋感測器頂端開設了一個裝有濾光鏡片的視窗,這個濾光片可通過光的波長範圍為 7~10μm,正好適合於人體紅外輻射的探測,而對其他波長的紅外線由濾光片予以吸收,這樣便形成了一種專門用作探測人體輻射的紅外線感測器。

在被動紅外探測器中有兩個關鍵性的元件,一個是熱釋電紅外感測器,它能將波長為 8-12μm 之間的紅外信號變化轉變為電信號,並能對自然界中的白光信號具有抑制作用,因此在被動紅外探測器的探測範圍內,當無人體移動時,熱釋電紅外感應器感應到的只是背景溫度,當人體進人探測範圍,通過菲涅爾透鏡,熱釋電紅外感應器感應到的是人體溫度與背景溫度的差異信號,因此,紅外探測器的紅外探測的基本概念就是感應移動物體與背景物體的溫度的差異。

另外一個元件就是菲涅爾透鏡,菲涅爾透鏡有兩種形式,即折射式和反射式。菲涅爾透鏡作用有兩個:一是聚焦作用,即將熱釋的紅外信號折射(反射)在熱釋電紅外感測器上,第二個作用是將探測範圍內分為若干個明區和暗區,使進入探測範圍的移動物體能以溫度變化的形式在熱釋電紅外感測器上產生變化熱釋紅外信號,這樣熱釋電紅外感測器就能產生變化的電信號。

人體都有恒定的體溫,一般在 37 度,所以會發出特定波長 10 微米左右的紅外線,被動式紅外探頭就是靠探測人體發射的 10 微米左右的紅外線而進行工作的。人體發射的 10 微米左右的紅外線通過菲泥爾濾光片增強後聚集到紅外感應源上。紅外感應源通常採用熱釋電元件,這種元件在接收到人體紅外輻射溫度發生變化時就會失去電荷平衡,向外釋放電荷,後續電路經檢測處理後就能產生信號。




紅外線 (Infrared) 在防盜系統用途上,可區分為主動式紅外線 twin-bean infrared sensor (感應器本身會發射紅外線光束偵測物體移動),與被動式紅外線 PIR (感應器本身不會發射紅外線光束,而是靠物體之熱源移動觸發感應器)。

一般而言,主動式紅外線大多數皆為2個(壹組)一起使用,一為發射紅外線光束 (Transimter),另一為接收紅外線光束 (Reciver),適用於室內或室外點對點之直線距離使用 (例如:對照式圍牆用紅外線投射器)。

被動式紅外線則適用於室內封閉空間使用,例如:一般室內防盜常用之紅外線感應器。之所以會有此區別用途,乃因室外溫差氣候變化大,例如:馬路溫度、汽車溫度、下雨天、刮風天.....等等外在因素,皆有可能會導致被動式紅外線誤報。而主動式紅外線因本身會持續發射紅外線光束,故受干擾誤報機會不大,相對的,其所能偵測的範圍僅限於點對點之直線距離,又不如被動式紅外線 PIR 有著整個空間的大涵蓋警戒範圍。




  1. PIR sensor 啟動時,需要有一段時間做 Calibration,依據不同的 sensor 有不同的時間,大約 10~60 秒,這個時間所輸出的值不具參考值。
  2. 沒有任何物體被偵測到移動時,輸出為 LOW。
  3. 當有物體被偵測到移動時,輸出為 HIGH,然後又回到 LOW。
  4. 如果有物體不斷的移動,輸出就會變成 HIGH LOW 不停變換,而不是維持在 HIGH 喔
  5. 使用 parallax PIR Sensor (#555-28027) 則可以設定成輸出一直維持在 HIGH。



  1. 純硬體設計線路不用軟體來解,使用 KC7788B IC。
  2. PIR 輸出 PWM (digital) -> 經過積分器輸出 DC (analog) -> 經過 ADC 輸出 0~255 -> MCU,這樣才可以控制 PIR 的靈敏度,不然只依據 HIGH/LOW 來判斷,只要一隻小鳥飛過也會觸發。
  3. 如果要用類比的電路更複雜,而且不穩定,會飄。更複雜的還有溫度的補償,PIR 很容易受溫度影響。

ir remote control NEC protocol

Posted by: 邱小新 at 下午4:32 in

一般指令時脈圖

  1. 圖一:0 與 1 的時脈圖。

  1. 圖二:傳送一個完整指令的時脈圖。
  1. 根據圖一,每次都會先送出 560us 的紅外線,再停止一段時間,再送出 560us 紅外線........直到資料送完。
  2. 根據圖一,送出一個 bit 1 需時 2.25ms 的時間。
  3. 根據圖一,送出一個 bit 0 需時 1.12ms 的時間。
  4. 根據圖二,會先送出一個 start sync (13.5ms),再依序送出 32bit 的資料,總共耗時 67.42ms。
  5. 一般來說 address 我們稱為 custom code,遙控器上的每一個按鍵送出的 address 都一樣,而只有 command 不一樣,以做區別。

重複指令時脈圖

  1. 圖三:傳送一個重複指令的時脈圖。
  1. 圖四:重複指令放大的時脈圖。
  1. 根據圖四,傳送一個重複指令需時 11.25ms。
  2. 根據圖三,傳送一個完整重複指令需時 110ms,所以後面空白時間為 110-11.25-0.56=98.19ms。
  3. 但是實際拿了三支不同的遙控器來試,結果發現三支的空白時間均不同,分別為 96.18ms、96.78ms、97.26ms。所以切記不要拿空白時間來當 repeat 訊號。

紅外線接收圖

  1. 圖五:紅外線接受器的訊號輸出圖。
  1. 根據圖五可以知道,當收到紅外線時,會產生一個 falling edge-triggered,剛好可以配合 8051 的 external interrupt。
  2. 只要利用二個 falling edge-triggered,就可以算出一個 pulse 所需的時間,進而知道是那一種訊號。
  3. 另外再利用 timer 8-bit auto-reload mode 的功能,可以精確計時。

範例程式

原始碼下載 void start_timer(void) { TR0 = LOW; timer_count = 0; TL0 = TIMER_COUNTER_60us; TR0 = HIGH; } void stop_timer(void) { TR0 = LOW; } int get_timer0(void) { int count; stop_timer(); count = timer_count; start_timer(); return count; } void timer0_isr(void) interrupt (1) { if(timer_count < 1500) { timer_count++; } else { stop_timer(); ir_control = 0; } } void init_timer0(void) { TR0 = LOW; TF0 = LOW; TMOD = (TMOD & TIMER1_MASK) | TIMER0_MODE_8BIT_AUTO; TH0 = TIMER_COUNTER_60us; PT0 = HIGH; ET0 = HIGH; } 發現了一個大 bug,i=ir_pos>>3; 要改成 i=(ir_pos&0x1f)>>3; 才可以,不然當接收到太多錯誤訊號時,會造成 ir_pos 溢位,而修改到其它變數。過了好久才發現這個問題,還是不小心發現的,還要感謝同事的遙控器溢波干擾,讓我看到這個大 bug。 void ext0_isr(void) interrupt (0) { static char ir_pos; int pulse; char i; if (ir_control == 0) { ir_control = IR_START; start_timer(); return; } pulse = get_timer0(); if (IS_IR_START(pulse)) { ir_control |= IR_READ; ir_pos = 0; return; } if (IS_IR_REPEAT(pulse)) { if (ir_control == IR_START) { printf_tiny("\n\rrepeat(%d)", pulse); } return; } i = (ir_pos & 0x1f) >> 3; if (IS_BIT_0(pulse)) { ir_code[i] >>= 1; ir_pos++; } else if (IS_BIT_1(pulse)) { ir_code[i] >>= 1; ir_code[i] |= 0x80; ir_pos++; } else { printf_tiny("\n\rerror(%d)", pulse); ir_control = 0; stop_timer(); return; } if(ir_pos >= 32) { ir_control ^= IR_READ; printf_tiny("\n\rcode = %x,%x,%x,%x", ir_code[0], ir_code[1], ir_code[2], ir_code[3]); ir_control = 0; stop_timer(); } } void ir_init(void) { init_timer0(); ir_control = 0; IT0 = HIGH; EX0 = 1; P3_2= HIGH; }

8051 外部中斷 (external interrupt)

Posted by: 邱小新 at 下午1:05 in
  1. 每一個 machine cycle 採樣一次訊號。
  2. 當中斷腳 (INT0) 被觸發後,若已在中斷致能 (EX0=1) 情況下,則中斷要求旗標 (IE0) 會設立為 1,再產生中斷。當 CPU 呼叫中斷服務程式,即自動清除 IE0 並執行中斷程式。嚴格的說,是由 IE0 旗標要求中斷,而非 INT0 腳直接要求,故 8051 亦可直接以指令設定 IE0 而產生中斷 0。
  3. 當我使用 falling edge-triggered 時,把 P3_2(INT0) 設成 0 時,發現中斷不再產生;重設成 1 後,中斷就會產生,不知啥原因。

triggered mode (觸發模式)

modeITxIEx說明
falling edge-triggered1clear只有當訊號從 high 變成 low 時會產生觸發。
low level-triggered0hold當訊號一直為 low 時會一直產生觸發,直到訊號變成 high。

edge-triggered (邊綠觸發)

  1. rising edge-triggered (正緣觸發)
    當訊號從 low 變成 high 時會產生觸發。
  2. falling edge-triggered (負緣觸發)
    當訊號從 high 變成 low 時會產生觸發。

SDCC 高低階型別轉換問題

Posted by: 邱小新 at 下午5:10 in
void main(void) { unsigned char a=143, b=121; unsigned long c; c = a * 256 + b; printf("\n\r143 * 256 + 121 = %ld", c); }

當執行上述程式碼時,理論上輸出值應為 "143 * 256 + 121 = 36729",但是輸出的值卻為 "143 * 256 + 121 = -28807"。為什麼會如此呢?主要是因為 SDCC 在做型別轉換時出了點問題。如下所示:

  1. (int) (unsigned char) 143 * (int) 256 + (int) (unsigned char) 121=
  2. (int) 143 * (int) 256 + (int)121 =
  3. (int) 36729 = (int) 0x8F79 = (long) 0xFFFF8F79 = (long) -28807
  4. 因為 0x8F79 第一個 bit 為 1,所以被當成負數來處理。

void main(void) { unsigned char a=143, b=121; unsigned long c; c = a * 256L + b; printf("\n\r143 * 256 + 121 = %ld", c); }

只要改成上列程式就沒有問題了,原因如下:

  1. 運算式中若有使用混合的資料型別,編譯器在計算之前會先將變數轉換為相同的資料型別,其規則是將佔記憶空間較小的變數轉換成佔記憶空間較大的資料型別。
  2. SDCC 在處理四則運算處理時,都會先轉換成 int 型態來處理,最後再轉換成儲存型態。
  3. 所以當四則運算裏有 long 常數或變數時,全部數值都會轉換成 long 來處理。

SDCC 有號無號型別轉換問題

Posted by: 邱小新 at 下午4:51 in
void main(void) { unsigned char a=-12; char b=-3; printf("\n\r-12 / -3 = %d", a/b); }

當執行上述程式碼時,理論上輸出值應為 "-12 / -3 = 4",但是輸出的值卻為 "-12 / -3 = -81"。為什麼會如此呢?主要是因為 SDCC 在做型別轉換時出了點問題。如下所示:

  1. (int) (unsigned char) -12 / (int) (signed char) -3 =
  2. (int) (unsigned char) 0xf4 / (int) (signed char) 0xfd =
  3. (int) 0x00f4 / (int) 0xfffd =
  4. (int) 244 / (int) -3 =
  5. (int) -81 = (int) 0xffaf;

只要改成下列程式就沒有問題了。

void main(void) { unsigned char a=-12; char b=-3; printf("\n\r-12 / -3 = %d", (char)a/b); }


void main(void) { unsigned short n=100; int l = -1; printf("sizeof(int) %d\n\r", sizeof(int)); if (n > l) printf("%u > %d\n\r", n, l); else printf("%u <= %d\n\r", n, l); }

當執行上述程式碼時,理論上輸出值應為 "100 > -1",但是輸出的值卻為 "100 <= -1"。但是在 linux 用 gcc 編譯執行的結果卻是 "100 > -1"。為什麼會如此呢?主要是因為 SDCC int 為 2bytes 而 gcc int 為 4bytes。

在 SDCC 中 unsigned short 與 int 都是 2bytes,根據一般算術型別轉換,unsigend 級別比較低時,signed 的資料型別 bit 數跟 unsigned 一樣多,會把 signed 轉成 unsigned,也就是 -1 變成 65535。如此 100 當然小於 -1 (65535) 囉。




一般算術型別轉換規則

  1. 整數型別的級別排序是 char < short < int < long < long long。
  2. 兩個運算元的型別相同時,不做轉換。
  3. 兩個運算元的型別不同時,兩邊都一樣是 signed 或 unsigned 的話,就轉換成高級別的型別。
  4. 兩個運算元的型別不同時,一個是 signed,一個 unsigned 的話。
    unsigned 這邊的級別比較高或相等時,會把 signed 轉成 unsigned。
    unsigend 級別比較低時,signed 的資料型別 bit 數比另一邊多時,會把 unsigned 轉成 signed,反之會把 signed 轉成 unsigned。

SDCC 數字常數表示方法及資料型態符號

Posted by: 邱小新 at 下午4:14 in

整數常數表示方式

  1. 二進位:由 0、1 所構成,開頭必須為 0b 或 0B。
    例如:0b00110011、0B11000011 等。
    SDCC 2.9.0 開始支援。
  2. 八進位:由 0、1、... 7 所構成,但第一個數字必須為 0
    例如:-012、0231、032767 等。
  3. 十進位:由 0、1、... 9 所構成,但第一個數字不可為 0
    例如:-12、231、32767 等。
  4. 十六進位:由 0、1、... 9、A、B、C、D、E、F (或 a、b、c、d、e、f) 所構成,開頭必須為 0x 或 0X。
    例如:-0x12、0X231、0xFFFF 等。

浮點常數表示方式

  1. 十進位:如 12.4、3.1415926 等,SDCC 不支援負數喔。
  2. 科學符號:如 2.34E+02、0.34e-12 等。

整數資料型態表示方式

  1. int:範圍 -32.768, +32.767,整數常數預設值,不加任何指示元。
    例如:32767、0231、0xFFFF 等。
  2. long:範圍 -2.147.483.648, +2.147.483.647,常數尾須加上指示元 l 或 L。
    例如:32767l、0231L、0xFFFFL 等。
  3. float:範圍 1.175494351E-38, 3.402823466E+38,常數尾須加上指示元 f 或 F。
    例如:32767f、12345F 等。
    SDCC 不支援。

SDCC interrupt 編碼注意事項

Posted by: 邱小新 at 下午2:14 in
  1. 變數沒有宣告成 volatile (variable not declared volatile)
    在中斷副程式裏改變一個全域變數,且這個全域變數也會被其它函數使用,則這個全域變數必須宣告成 volatile。
  2. 非原子型態變數的存取 (non-atomic access)
    非原子型態變數即 MCU 需要一個以上的指令去存取變數。當存取非原子型態變數時發生中斷,且在中斷函數中也會去存取此變數,則會發生不可預期的問題,而且很難被複制重現。所以當函數中存取到在中斷函數中用到的非原子型態變數時,需關閉所有中斷。例如:
    1. 16/32 bit 變數在 8bit MCU 是屬於非原子型態變數。
    2. 8bit 變數 flags 使用 "flags |= 0x80;" 這個指令,當 flags 位於 xdata 是需要一個以上指令來完成。
    3. 8bit 變數 counter 使用 "counter += 8;" 這個指令,不管 counter 位於那個記憶體,都需要一個以上指令來完成。
  3. 堆疊溢位 (stack overflow)
    返回位址 (return address) 及在中斷函數會用到的 register 都會被中斷函數先放進堆疊,離開時再從堆疊裏取出恢復。但是當堆疊空間不足時,又發生中斷,就會造成不可預期的問題。這種問題常常發生在遞迴函數的運行。
  4. 非可再進入函數的使用 (use of non-reentrant functions)
    在中斷函數中呼叫其它函數是不建議的,最好避免如此做。因為當執行此函數時,又發生中斷時,中斷函數又去呼叫此函數,造成重復進入的問題。所以如果要在中斷函數中呼叫函數,必須是呼叫可再進入的函數。
  5. int/long/float 的四則運算使用
    當你在中斷函數中使用 int/long 變數的乘法、除法、餘數運算,及 float 變數的四則運算時,需要注意非可再進入函數的使用。因為這些運算都是另外依靠函式庫來提供運算,並非 MCU 所提供的,而預設都是非可再進入函數。如果需要在中斷函數內使用,需要加上 --stack-auto 重新編譯,而且函數庫需要加上 --int-long-reent 及 --float-reent 再重新編譯一次。

SDCC printf 函數介紹

Posted by: 邱小新 at 上午10:41 in
  1. printf 會使用到 putchar 函數,而 SDCC 沒有實作 putchar,需要使用者自己定義。
  2. 預設的 printf 是使用 printf_large.c 的函數,不支援 float 輸出。
  3. 如果要讓 printf 支援 float 輸出,需加上 -DUSE_FLOATS=1 才可以,另外最好使用 --model-large,因為會使用到很多 memory。
  4. printf_fast 函數可以設定 #define 去除 long 及 field widths 的支援,並縮小 code size 並加快速度。
  5. printf_fast_f float 只支援到 +/- 4294967040,小數點後 8 位。
  6. %e 及 %g 二種 float 都不支援。
  7. %d 顯示有符號十進位整數(signed),%u 顯示無符號十進位整數(unsigned)。

mcs51printfprintf
USE_FLOATS=1
printf_smallprintf_fastprintf_fast_fprintf_tiny
filenameprintf_large.cprintf_large.cprintfl.cprintf_fast.cprintf_fast_f.cprintf_tiny.c
"HelloWorld" size small/large1.7k/2.4k4.3k/5.6k1.2k/1.8k1.3k/1.3k1.9k/1.9k0.44k / 0.44k
code size
small / large
1.4k / 2.0k2.8k / 3.7k0.45k / 0.47k (+_ltoa)1.2k / 1.2k1.6k / 1.6k0.26k / 0.26k
byte arguments on stackyesyesnononono
formatscdiopsuxcdfiopsuxcdosxcdsuxcdfsuxcdsux
long (32 bit) supportyesyesyesyesyesno
float format--%f----%f--
field widthyesyesnoyesyesno
string speed
small/large
1.52 / 2.59 ms1.53 / 2.62 ms0.92 / 0.93 ms0.45 / 0.45 ms0.46 / 0.46 ms0.45 / 0.45 ms
int speed
small / large
3.01 / 3.61 ms3.01 / 3.61 ms3.51 / 18.13 ms0.22 / 0.22 ms0.23 / 0.23 ms0.25 / 0.25 ms
long speed
small / large
5.37 / 6.31 ms5.37 / 6.31 ms8.71 / 40.65 ms0.40 / 0.40 ms0.40 / 0.40 ms--
float speed
small / large
--7.49 / 22.47 ms----1.04 / 1.04 ms--

  1. Execution time of printf("%s%c%s%c%c%c", "Hello", ’ ’, "World", ’!’, ’\r’, ’\n’); standard 8051 @ 22.1184 MHz, empty putchar()
  2. Execution time of printf("%d", -12345); standard 8051 @ 22.1184 MHz, empty putchar()
  3. Execution time of printf("%ld", -123456789); standard 8051 @ 22.1184 MHz, empty putchar()
  4. Execution time of printf("%.3f", -12345.678); standard 8051 @ 22.1184 MHz, empty putchar()

利用 printf 來算出 delay 1ms 的函數

Posted by: 邱小新 at 下午4:38 in , ,
一般來說,要取得 delay_1ms 的函數有幾種方法。
  1. 利用 timer 中斷來取得。
  2. 利用 while loop 來取得,但是要算出 while loop 的數值,通常使用示波器來輔助求得。

使用第一項方式會浪費掉一個 timer,使用第二個方法又很麻煩(因為不會用示波器)。所以我就想利用 printf 加上 timer 來算出 while loop 的數值。程式如下:

#define CNT 100 void delay_1ms(int msec) { int i; while(msec--) { i = CNT; while(i--); } } void main(void) { long time, cnt; uart_init(); while (1) { TMOD = 0x01; TH0 = 0; TL0 = 0; TR0 = 1; delay_1ms(1); TR0 = 0; time = (TH0*256L + TL0) * 12L / 40L; cnt = 1000l * (long)CNT / time; printf("\n\rtime0 = %d,%d,%ld,%ld\n\r", TH0, TL0, time, cnt); getchar(); } }

計算過程

  1. 首先 delay_1ms 中的 i 值先給 100,燒進去執行。
  2. 取得 TH0, TL0,利用 timer 公式 (TH0*256+TL0)*12/Fosc,計算出 100 次要花多少 t 時間。
  3. 再利用公式 t/i0 = 1ms/i1,i1=1ms*i0/t,取得下一次要填入的 i1 值。
  4. 再次重覆計算,直到 i 值不變或 t>1ms 為止。

範例:W79L632A@40MHz

  1. TH0=2, TL0=75, t=176.1 us, i1=567.8591 取 568。
  2. TH0=12, TL0=168, t=972 us, i1=584.3621 取 584。
  3. TH0=13, TL0=3, t=999.3 us, i1=584.4090 取 585。
  4. TH0=13, TL0=8, t=1000.8 us, i 值設定成 585
  5. 同樣的硬體,同樣的 source code,使用 keil C51,計算出來的值卻是 711,由此可見 keil C51 跑得比較快??
  6. 最近又從別人的 code 發現,其實在 i=CNT 前加入 nop 指令可以用來增加準確度到 1us 喔,當然 nop 數量的多寡要經由測試才能得知,有興趣的人自己試看看吧。

測試誤差

void delay_1ms(int msec) { int i; while(msec--) { i = 585; while(i--); } } void main(void) { long time; int cnt=20; uart_init(); while (cnt--) { TMOD = 0x01; TH0 = 0; TL0 = 0; TR0 = 1; delay_1ms(cnt); TR0 = 0; time = (TH0*256L + TL0) * 12L / 40L; printf("\n\rtime0 = %d,%d,%ld,%d", TH0, TL0, time, cnt); } }

Keil-C 怪怪的問題

最近玩 mstar 的晶片,改用 Keil C 來測這個程式,結果發現一個怪異現象,就是每次 printf 出來的 TH0 都是 16bit 的數值,而 TL0 卻總是 0,time,cnt 算出的值也不對,真是莫明奇怪。後來發現把 TH0, TL0 放入 int 變數就正常了,如果放入 char 變數也是不對。由此證明 keil 在 char 變數的四則運算有點問題喔。

#define CNT 100 void delay_1ms(int msec) { int i; while(msec--) { i = CNT; while(i--); } } void main(void) { long time, cnt; int a, b; uart_init(); while (1) { TMOD = 0x01; TH0 = 0; TL0 = 0; TR0 = 1; delay_1ms(1); TR0 = 0; a=TH0; b=TL0; time = (a*256L + b) * 12L / 40L; cnt = 1000l * (long)CNT / time; printf("\n\rtime0 = %d,%d,%ld,%ld\n\r", a, b, time, cnt); getchar(); } }

一個簡單的 uart debug 輸出程式

Posted by: 邱小新 at 下午3:52 in , ,

下面這支程式的 baudrate 是 38400 使用 40MHz 的震盪器。主要功能就是你輸入什麼,就顯示什麼。依此下列程式再加上 SDCC 裏的 printf 就可以用來做 debug 輸出功能了,短小精幹好用。

PS: 由於沒有使用到 ES 中斷,所以可以很放心的在中斷函數中使用 printf 來輸出訊息。

#include <8052.h> void putchar(char value) { while (!TI); TI = 0; SBUF = value; } char getchar(void) { while(!RI); RI= 0; return SBUF; } void uart_init(void) { RCAP2H = 0xff; RCAP2L = 0xe1; TH2 = RCAP2H; TL2 = RCAP2L; T2CON = 0x34; SCON = 0x52; } void main(void) { uart_init(); while (1) { putchar(getchar()); } }

PS: 因為我不會用 8051 的模擬器,所以都用 printf 在做 debug,有人說這樣不方便,其實用久習慣就好了。不管黑貓白貓,可以抓到老鼠就是好貓。

I2C protocol 原理及應用

Posted by: 邱小新 at 下午4:14 in
原始碼下載

I²C (Inter-Integrated Circuit) 是內部整合電路的稱呼,是一種串列通訊匯流排,使用多主從架構,由飛利浦公司在 1980 年代為了讓主機板、嵌入式系統或手機用以連接低速週邊裝置而發展。I²C 的正確讀法為 "I-squared-C" ,而 "I-two-C" 則是另一種錯誤但被廣泛使用的讀法,在大陸地區則多以 "I方C" 稱之。截至 2006 年 11 月 1 日為止,使用 I²C 協定不需要為其專利付費,但製造商仍然需要付費以獲得 I²C Slave (從屬裝置位址)。

I²C的參考設計使用一個7位元長度的位址空間但保留了16個位址,所以在一組匯流排最多可和112個節點通訊。常見的I²C匯流排依傳輸速率的不同而有不同的模式:標準模式(100 Kbit/s)、低速模式(10 Kbit/s),但時脈頻率可被允許下降至零,這代表可以暫停通訊。而新一代的I²C匯流排可以和更多的節點(支援10位元長度的位址空間)以更快的速率通訊:快速模式(400 Kbit/s)、高速模式(3.4 Mbit/s)。

I2C 的啟動條件及停止條件

  1. I2C start condition 有二種情況,如上圖所示,虛線表示 read 動作時的第二次 start condition,實線表示 r/w 時的第一次 start condition。
  2. I2C stop condition 只有一種情況,如上圖所示。
void i2c_start(void) { // for second start signal on i2c_read I2C_SDA = HIGH; I2C_SCL = HIGH; i2c_wait(); // send start signal I2C_SDA = LOW; i2c_wait2(); I2C_SCL = LOW; } void i2c_stop(void) { i2c_wait2(); I2C_SDA = LOW; i2c_wait2(); I2C_SCL = HIGH; i2c_wait2(); I2C_SDA = HIGH; }

I2C 的讀寫動作

  1. 當 SCL=HIGH 時,表示 SDA 穩定,可以做讀取動作。
  2. 當 SCL=LOW 時,表示 SDA 混亂,不可以讀取;因為此時可以設定 SDA 的值,也就是做寫入動作。
  3. master 每一次傳送八個 bit,最後 slave 會回傳一個 ack bit,表示接受是否完成。
  4. master 每一次接受八個 bit,最後 master 要傳送一個 ack bit,表示接受已經完成。
  5. 在傳送完第八個 bit 之後,再等待 slave 接受完成後,需將 SDA 設成 HIGH,此時 slave 會將 SDA 拉回 LOW,表示接受動作完成。如果 acknowledge=HIGH,也就是 slave 沒有拉成 LOW 則表示傳送失敗。
bit i2c_write(unsigned char value) { char i=9; bit ack; // upload data while(--i) { i2c_wait2(); I2C_SDA = (value & 0x80) ? HIGH : LOW; i2c_wait2(); // send data I2C_SCL = HIGH; i2c_wait(); value <<= 1; I2C_SCL = LOW; } // get acknowledgement i2c_wait2(); I2C_SDA = HIGH; i2c_wait2(); I2C_SCL = HIGH; i2c_wait2(); ack = I2C_SDA; i2c_wait2(); I2C_SCL = LOW; // return acknowledge return ack; } unsigned char i2c_read(bit acknowledge) { char i=9; unsigned char value=0; // read data while(--i) { value <<= 1; i2c_wait(); I2C_SCL = HIGH; i2c_wait2(); value |= I2C_SDA; i2c_wait2(); I2C_SCL = LOW; } // send acknowledge i2c_wait2(); I2C_SDA = acknowledge; i2c_wait2(); I2C_SCL = HIGH; i2c_wait(); I2C_SCL = LOW; // return data return value; }

標準 I2C 讀寫流程 by Philips

  1. 讀取完最後一個 byte 時,記得要回傳 no acknowledge(SDA=HIGH),表示已經沒有要繼續讀取資料。
  2. 讀取完最後一個 byte 時,回傳 acknowledge(SDA=LOW),再傳送 stop signal,則會造成後續的讀寫動作失敗。(這是在讀取 PCF8593 的經驗)
完整寫入流程

完整讀取流程

複合式讀寫流程

常用 I2C NVRAM 讀寫流程 by Philips PCF8953

  1. 一般 RTC 都有帶一些 NVRAM 或是讀寫 EEPROM,需要先指定讀寫的 register address 才可以。所以在寫時,先寫入 slave address 後,需再寫入 register address,讓 chip 知道你要寫入的起始位址,接下來才能寫入 data。
  2. 由於讀取前也要先寫入 register address,所以一般 NVRAM 讀取動作都是使用標準複合式流程,也就是先寫入 register address,再下一次 start conditon,再做讀取動作。
void i2c_write_byte(unsigned char slave_addr, unsigned char reg_addr, unsigned char value) { char i=10; EA = 0; while (--i) { i2c_start(); if(i2c_write(slave_addr)) continue; if(i2c_write(reg_addr)) continue; if(i2c_write(value)) continue; i2c_stop(); break; } EA = 1; } unsigned char i2c_read_byte(unsigned char slave_addr, unsigned char reg_addr) { char i=10; unsigned char value=0; EA = 0; while (--i) { // send register address i2c_start(); if(i2c_write(slave_addr&0xfe)) continue; if(i2c_write(reg_addr)) continue; // read data i2c_start(); if(i2c_write(slave_addr | 1)) continue; value = i2c_read(1); i2c_stop(); break; } EA = 1; return value; }

8052 console (command line) interface for SDCC

Posted by: 邱小新 at 下午2:04 in , , ,
這裏演示一下 8052 console interface 的做法,包含解析 ANSI escape code 的做法。
原始碼下載

全域變數

  1. char g_cmd_line[SZ_CMD_LINE+1];
    記錄指令輸入的暫存字串,SZ_CMD_LINE 不可大於 128。
  2. char g_position;
    記錄游標位置。
  3. char g_ansi_digit;
    記錄 ANSI escape code 的數字部位。
  4. bit g_ansi_mode;
    記錄是否進入 ANSI escape sequence 解析模式。
  5. char (*fp_parse_ansi)(char escape);
    記錄使用 ANSI 解析的函數。

console init

  1. 清除營幕所有東西,並回到左上角的位置,最後顯示提示字元。
void console_init(void) { g_cmd_line[0] = 0; g_ansi_mode = 0; g_position = 0; printf_small("\033[2J"); printf_small("\033[H"); printf_small("\r\n # "); }

console routine process

  1. 讀取輸入字元。
  2. 進入 ANSI escape code parse 或解析輸入字元。
  3. '\r': 輸入完成,解析輸入字串。
  4. 0x07: beep char,輸出錯誤嗚叫聲。
  5. '\t': 忽略定位字元。
  6. '\b', 0x7f: 刪除字元。
  7. '\033': 進入 ANSI escape sequence mode。
  8. default: 記錄字元並顯示在營幕上。
void console_process(void) { char ch=getchar(); if (g_ansi_mode) { ch = fp_parse_ansi(ch); } switch (ch) { case '\r': if (g_position > 0) { console_parse(); g_position = 0; } printf_small("\r\n # "); break; case 0x07: /* beep */ putchar(ch); break; case '\t': /* tab */ break; case '\b': /* backspace */ case 0x7f: /* delete */ delete_char(); break; case '\033':/* escape */ g_ansi_mode = 1; fp_parse_ansi = parse_ansi; break; default: if (isprint(ch)) { putchar(ch); insert_char(ch); } } }

console parse

  1. 取得 command id,並依 command id 執行相關程式。
void console_parse(void) { char cmid; char *next; g_cmd_line[g_position] = 0; next = parse_command(&cmid, g_cmd_line, g_main_command); switch (cmid) { case -1: // null string printf_small(c_return); break; case CMID_HELP: printf_small("\n\r\tHELP\t\t\thelp message"); printf_small("\n\r\tDUMP [addr] [len]\tdump code data"); break; case CMID_DUMP: dump_codedata(next); break; default: printf_small(c_error); break; } }

parse command

  1. 取得 command id,並回傳下一個參數字串指標。
char *parse_command(char *cmid, char *src, struct s_cmd *command) { char i; char *cmd, *next; // check null string cmd = clear_space(src); if (*cmd == 0) { *cmid = -1; return src; } // get command id next = next_string(cmd); for (i=0; command->cmd_id != CMID_MAX; i++) { if (strcmp(cmd, command->cmd_string) == 0) break; command++; } *cmid = command->cmd_id; return next; }

next string

  1. 取得參數字串並補上 \0 字元,並回傳下一個參數字串指標。
char *next_string(char *next) { while (*next) { if (*next == ' ') break; next++; } if (*next == ' ') { *next = 0; next++; } return next; }

clear space

  1. 清除空白字元,並回傳參數字串指標。
char *clear_space(char *src) { while (*src) { if (*src != ' ') break; src++; } return src; }

parse ansi code

  1. 解析 ANSI escape code 第一個字元。
char parse_ansi(char ch) { if (ch == 'O') { fp_parse_ansi = parse_func; } else if (ch == '[') { fp_parse_ansi = parse_bracket; } else { return 0x07; } return 0; }

parse bracket

  1. 解析 ANSI escape code 第二個字元。
char parse_bracket(char ch) { if (isdigit(ch)) { g_ansi_digit = ch - '0'; fp_parse_ansi = parse_digit; return 0; } // parse bracket g_ansi_mode = 0; switch (ch) { case 'A': // move up case 'B': // move down break; case 'C': // move right putchar(' '); insert_char(' '); break; case 'D': // move left delete_char(); break; default: return 0x07; } return 0; }

parse bracket

  1. 解析 ANSI escape code 第二個字元,這一個函數是針對 windows 的超級終端機所做的,標準 ANSI escape code 好像沒有。
char parse_func(char ch) { g_ansi_mode = 0; switch (ch) { case 'P': /* F1 */ case 'Q': /* F2 */ case 'R': /* F3 */ case 'S': /* F4 */ break; default: return 0x07; } return 0; }

parse bracket

  1. 解析 ANSI escape code 數字。
char parse_digit(char ch) { if (ch != 0x7e) { if (isdigit(ch)) { g_ansi_digit = g_ansi_digit * 10 + ch - '0'; return 0; } else { g_ansi_mode = 0; return 0x07; } } // parse digit g_ansi_mode = 0; switch (g_ansi_digit) { case 1: /* home */ g_position = 0; puts("\r # "); puts("\033[K"); break; case 3: /* delete */ delete_char(); break; case 11: /* F1 */ case 24: /* F12 */ break; default: return 0x07; } return 0; }

SDCC 指標變數 (pointers to specific memory spaces)

Posted by: 邱小新 at 上午9:49 in
由於 mcs51 的記憶體分成五大部份(註一),所以搞的 SDCC 的指標也要分成好幾種。以下先簡介指標定義準則:

located
指向位址
type
變數型態
pointerphysical
變數位址
name
__xdataunsigned char*__datap

指標分類

  1. __xdata unsigned char * __data p;
    指向外部記憶體的指標變數,變數存放在內部記憶體。
  2. __data unsigned char * __xdata p;
    指向內部記憶體的指標變數,變數存放在外部記憶體。
  3. __code unsigned char * __xdata p;
    指向程式記憶體的指標變數,變數存放在外部記憶體。
  4. __code unsigned char * __code p;
    指向程式記憶體的指標變數,變數存放在程式記憶體,指標變數是唯讀,所以必須先設定初始值
  5. unsigned char * __xdata p;
    指向任何記憶體的指標變數 (generic pointer),變數存放在外部記憶體。
  6. unsigned char * p;
    指向任何記憶體的指標變數 (generic pointer),變數存放由 memory model 決定。
  7. char (* __data fp)(void);
    指向程式記憶體的函數指標變數,變數存放在內部記憶體。函數指標變數只能指向程式記憶體,無法修改喔
  8. 由於內部記憶體指標讀取都是使用間接定址 (indirect addressing),所以無法存取 SFR 空間的記憶體內容。

指標變數大小

__data1
__xdata2
__code2
generic pointer3

  1. generic pointer 是設計用來可以做通用指標變數使用,可以任意變換指標指向位址,但是相對的需要佔用 3 個 bytes 的位置,而且在做讀取寫入時,需要另外執行一個 __gptrget 或 __gptrput 來轉換位址,沒事就少用一點吧。

SDCC link error

Posted by: 邱小新 at 下午4:55 in
一開始開發專案時,我們習慣把 source code 都放在 work root directory。漸漸的,檔案越來越多,我想要做分類,新建幾個目錄把檔案搬進去。這裏卻發現無法正常編譯,明明都沒有錯誤,卻發生 link error。這時只要做二個步驟就可以解決。

  1. 把 Release 目錄下的東西全部砍掉。
  2. 做一次 Clean Project 即可。

SDCC 記憶體模式 (memory model)

Posted by: 邱小新 at 下午4:49 in
modeldefaultsize
SMALL__data128使用直接定址內部記憶體當變數
Medium__pdata256使用間接定址外部記憶體當變數
Large__xdata64K使用間接定址外部記憶體當變數
8052 記憶體定址

SDCC command line options 命令列參數

Posted by: 邱小新 at 下午2:52 in

處理器參數 (Processor Selection Options)

  1. -mmcs51 (default)
    產生 Intel MCS51 系列的執行碼。
  2. -mgbz80
    產生 GameBoy Z80 處理器的執行碼。
  3. -mpic14
    產生 Microchip 14-bit 系列的執行碼。
  4. -mpic16
    產生 Microchip 16-bit 系列的執行碼。

前置處理器參數 (Preprocessor Options)

  1. -I<path>
  2. -D<macro[=value]>
  3. ...
  4. ...
  5. ...

連結器參數 (Linker Options)

  1. -L --lib-path <absolute path to additional libraries>
  2. --xram-loc <Value>
    外部記憶體的起始位置,預設值為 0,value 可以用十進位或 16 進位,如 --xram-loc 0x8000 or --xram-loc 32768。
  3. --code-loc <Value>
    程式記憶體的起始位置,預設值為 0,value 可以用十進位或 16 進位,如 --xram-loc 0x8000 or --xram-loc 32768。
  4. --out-fmt-ihx (default)
    產生 IntelHex 格式的燒錄檔案。
  5. --out-fmt-s19
    產生 Motorola S19 格式的燒錄檔案。
  6. --out-fmt-elf
    產生 ELF 格式的執行檔。
  7. ...

MCS51 參數 (MCS51 Options)

  1. --model-small (default)
  2. --model-medium
  3. --model-large
  4. --xstack
  5. --iram-size <Value>
    設定內部記憶體 (internal ram) 大小,預設值為 256。
  6. --xram-size <Value>
    設定外部記憶體 (external ram) 大小,預設值為 64K。
  7. --code-size <Value>
    設定程式記憶體 (code memory) 大小,預設值為 64K。
  8. --stack-size <Value>
    設定堆疊 (stack) 大小。
  9. --pack-iram (default)
  10. --no-pack-iram
  11. --acall-ajmp

最佳化參數 (Optimization Options)

  1. ...
  2. ...
  3. --no-xinit-opt
    不要拷貝 code 的初始資料到 xdata。當你沒有任何初始資料時,可以使用此選項來減少 code size。
  4. --nooverlay
    傳遞參數及局部變數不要重疊使用,這樣可能會加大記憶體耗損,但是可以保障程式不出問題。
  5. --opt-code-speed
    編譯出最快的執行檔,執行檔會變大。
  6. --opt-code-size
    編譯出最小的執行檔,執行檔會變慢。

其它參數 (Other Options)

  1. -c --compile-only
    只有編譯而已,不做連結,不產生燒錄檔案。
  2. -E
    只有執行前置處理器,並把結果顯示到標準輸出即營幕上。
  3. -o <path/file>
    輸出目錄路徑。
  4. --stack-auto
    所有的函數都被編譯成可重復進入 (reentrant) 的函數。
  5. --Werror
    把所有的警告當成錯誤。
  6. --print-search-dirs
    顯示編譯器的預設搜尋路徑。
  7. --vc
    使用 MSVC 樣式來顯示警告及錯誤,預設是使用 GCC 樣式。
  8. --std-sdcc89 (default)
    使用 C89 標準,並允許 SDCC 的擴充指令。
  9. --std-c89
    使用 C89 標準,不允許 SDCC 的擴充指令。
  10. --std-sdcc99
    使用 C99 標準,並允許 SDCC 的擴充指令。
  11. --std-c99
    使用 C99 標準,不允許 SDCC 的擴充指令。
  12. ...

8051/8052 memory addressing

Posted by: 邱小新 at 下午4:06 in ,

記憶體種類

typeSDCCinstructionsizeaddressing
(A) program__codeMOVC64Kindex direct
(B) external__xdataMOVX64Kindirect
(C) internal(80~FF)__idataMOV128indirect
(C) SFR(80~FF)__sfrMOV128direct
(C) internal(0~7F)__dataMOV128direct/indirect

記憶體定址

typereadwriteregister
directMOV Rn,direct
MOV direct,direct
MOV direct,Rn
MOV direct,@DPTR
MOV direct,#data
R0~R7,A
indirectMOV direct,@Ri
MOVX A,@DPTR
MOV @Ri,direct
MOVX @DPTR,A
MOV @Ri,#data
R0,R1,DPTR,A
index directMOV A,#30h
MOV DPTR,#300H
MOVC A,@A+DPTR
XA,DPTR
  1. Ri=R0,R1,Rn=R0~R7。
  2. 索引定址法 (index direct) 只能用在程式記憶體,而且不能寫入,只能讀取到 A。
  3. 外部記憶體只能使用間接定址法 (indirect),而且只能透過 Ri/DPTR/A 三者傳遞數值,如果要寫入數值,只能把值先存入 A,再由 A 寫入 DPTR/Ri。
  4. 內部記憶體不論間接或直接都可以直接把數值寫入,不需要透過 A,當然要經由 A 也是可以。

SDCC autobaud function

Posted by: 邱小新 at 下午3:56 in , ,

測試原由

SDCC 的 serial.h 內有一行 void autobaud(); 宣告,表示有自動鮑率偵測功能。所以就在 W79L632A 上實驗是否可行。它的原理是假定使用者第一次會輸入 return 鍵,也就是 ascii code 13。依照這個值利用 timer1 算時間差來做運算,進而取得 timer1 8-bit auto reload 的 TH 值。以下是測試的程式碼,大多是參考 SDCC 內的 serial.c 函數功能。

變數宣告

#include <8051.h> extern void autobaud();

輸出函數

void putchar(char c) { while(!TI); TI=0; SBUF=c; }

輸入函數

char getchar(void) { char c; while(!RI); RI=0; c=SBUF; return c; }

主要函數

void main(void) { ES=0; /* kill SIO IRQ */ TI=0; /* prepare sending */ RI=0; /* prepare reading */ autobaud(); /* automatically detect and set baud rate */ getchar(); /* discard the CR character from the autobaud routine */ // echo while(1) { putchar(getchar()); } }

測試結果

結果非常失望,完全沒有作用,功能無效。不知是不是因為我的 W79L632A 使用 40MHz 的震盪器。也不知 winbond LD 的 autobaund 功能怎麼寫的?只好放棄這個功能囉。

8052 UART

Posted by: 邱小新 at 上午9:26 in

baudrate 推導過程,以 timer2 為 baudrate 產生器

  1. baudrate 的單位為 bps 即每秒傳送多少 bits,反過來說傳送一個 bit 要花掉 1/baudrate 時間。
  2. 根據上圖來看,每次 timer 溢位一次,要先經過一個 16 的除法器,所以需 16 次溢位才接收一個 bit。
  3. 根據上圖來看,計數器每次加一需先經過一個 2 的除法器,所以每一次溢位時間為 (65536-RCAP2) * 2 / Fosc。
  4. 每接受一個 bit 要花掉時間為 16 * (65536-RCAP2) * 2 / Fosc。
  5. 1/baudrate = 16 * (65536-RCAP2) * 2 / Fosc = 32 * (65536-RCAP2) / Fosc。
  6. baudrate = Fosc / 32 / (65536-RCAP2)
  7. 以 40MHz 為例,baudrate 最大值為 1250000,最小值為 19。
  8. RCAP2 = 65536 - (Fosc / 32 / baudrate)
  9. 以 11.0592MHz 為例,baudrate=115200 時,RCAP2 = 65536-(11059200 / 32 / 115200)=65536-3=65533。
  10. 以 22.1184MHz 為例,baudrate=115200 時,RCAP2 = 65536-(22118400 / 32 / 115200)=65536-6=65530。