顯示具有 8051 標籤的文章。 顯示所有文章

一些網站資料

Posted by: 邱小新 at 凌晨12:40 in
  1. 大鳥實驗室
  2. 雷兒電子電機工程協會
  3. 南台科大機電科-單晶片原理與應用
  4. 崑山科大電子系-單晶片8051教學網頁
  5. 工程師的家 (KEIL C Compiler)

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 時會產生觸發。

利用 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(); } }

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; }

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 也是可以。

8051 pin define

Posted by: 邱小新 at 上午11:42 in

ALE/PROG 地址使能信号端

  1. 8051外接RAM/ROM时,ALE接地址器(8282)的STB脚,(74373)的EN脚,当CPU对外部存储器进行存取时,用以锁住地址的低位地址。
  2. 8051未外接RAM/ROM时,ALE脚会有1/6晶体振荡频率,可作为外部时钟。
  3. 在烧写EPROM时,ALE作为烧写时钟的输入端。

PSEN 程序储存使能端

  1. 内部程序存储器读取,不动作。
  2. 外部程序存储器读取(ROM),在每个机器周期会动作两次。
  3. 外部数据存储器读取(RAM),两个/PSEN脉冲被跳过不会输出。
  4. 外接ROM时,与ROM的/OE脚连接。

EA/VPP

  1. 接高电平时:CPU 读取内部程序存储器(ROM)。扩充外部 ROM,当读取内部程序存储器超过0FFFH(8051)、1FFFH(8052)时,自动读取外部ROM。
  2. 接低电平时:CPU 读取外部程序存储器(ROM)。
  3. 8751 烧写内部 EPROM 时,利用此脚 21V 的烧写电压。

RESET 复位引脚

  1. 为高电平时(约2个机器周期),可将CPU复位。

P0

  1. 外部扩充存储器时,作数据总线(D0~D7)。
  2. 外部扩充存储器时,作地址总线(A0~A7)。
  3. 不扩充时,作一般 I/O 使用,内部无上拉电阻,作为输出/输入使用时应加上拉电阻。

P1

  1. 只作 I/O 口使用,有内部上拉电阻。

P2

  1. 扩充外部存储器时,作地址总线(A8~A15)使用。
  2. 不扩充时,作一般 I/O 口使用,有内部上拉电阻。

P3

  1. 作一般 I/O 口使用,有内部上拉电阻。
  2. 有一些特殊功能。

8051 UART

Posted by: 邱小新 at 下午3:04 in
點圖看原始圖

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

  1. baudrate 的單位為 bps 即每秒傳送多少 bits,反過來說傳送一個 bit 要花掉 1/baudrate 時間。
  2. 根據上圖來看,每次 timer 溢位一次,總共要經下列關卡才會接收一個 bit。
    1. 假如 SMOD=0 要經過一個二的除法器。
    2. 再進入 clock 前要先經過一個 16 的除法器。
    3. 當 SMOD=0 需 16 * 2 的計時器溢位才會接收一個 bit。
    4. 當 SMOD=1 需 16 的計時器溢位才會接收一個 bit。
    5. 推導出,需 16 * 2 / 2^SMOD 的計時器溢位才會接收一個 bit。
  3. 以 8-bit auto reload mode 來說,每一次溢位時間為 (256-TH1) * 12 / Fosc。
  4. 每接受一個 bit 要花掉時間為 (16 * 2 / 2^SMOD) * ((256-TH1) * 12 / Fosc)。
  5. 1/baudrate = (16 * 2 / 2^SMOD) * ((256-TH1) * 12 / Fosc)。
  6. 1/baudrate = (32 / 2^SMOD) * ((256-TH1) * 12 / Fosc)。
  7. baudrate = (2^SMOD * Fosc) / (384 * (256-TH1))。
  8. 以 40MHz 為例,baudrate 最大值為 208333.33,最小值為 406.90。
  9. Th1 = 256 - (2^SMOD * Fosc / baudrate / 384)。
  10. 以 11.0592MHz 為例,baudrate=19200 及 SMOD=1 時,TH1 = 256 - (2*11059200/19200/384) = 256-3 = 253。
  11. 以 11.0592MHz 為例,baudrate=9600 及 SMOD=0 時,TH1 = 256 - (11059200/9600/384) = 256-3 = 253。

8051 Serial Port Control Register (SCON)

Posted by: 邱小新 at 下午2:01 in
  1. SMO, SM1: serial port mode。

    SM0SM1modelength同步baudrate
    0008同步Fosc/12
    01110異步time1
    10211異步Fosc/32 or Fosc/64
    11311異步time1

  2. SM2: multiple processors communication。

    modeSM2action
    00x
    11收到有效的停止位元才會設定 RI。
    2,31當 RB8=1 才會將值送入 SBUF,並設定 RI。

  3. REN (Receive enable): REN=1 才會開啟 uart 接收功能。
  4. TB8: 在模式 2,3 時,在 bit 9 要傳送的數值,軟體要手動設定。
  5. RB8: 在模式 2,3 時,在 bit 9 所接收的數值。
  6. TI: Transmit interrupt flag。 uart 送完資料就會設定這個旗標,須由軟體手動清除此旗標。
  7. RI: Receive interrupt flag。 uart 收到資料就會設定這個旗標,須由軟體手動清除此旗標。

8051 Timer

Posted by: 邱小新 at 上午9:45 in
TMOD register 0x89 (timer/counter mode control register)

  1. GATE = 1 需要 INTx 接腳為高電位才計時,GATE = 0 則不需要。

    也就是當 TRx=1 and GATE=0 開始計時,或者當 TRx=1 and GATE=1 and INTx=1 開始計時,其它條件都不計時。

  2. C/T = 0 使用內部時脈,C/T = 1 使用外部時脈。

    Timer 的計時時脈來源有兩種,一種是 8051 單晶片的內部時脈也就是從 XTAL1 與 XTAL2 接腳所輸入的內部時脈,一種是從 T0 與 T1 接腳所輸入的外部時脈。所以一般來說設定成內部時脈時,稱為計時器,因為振盪器頻率固定可以當計時器用;而設定成外部時脈時,稱為計數器,因為不知使用者會接什麼裝置,但是都有計算次數功能。如果外部時脈也是接振盪器時,也可以算是計時器,只是沒人會如此做,因為直接使用內部時脈即可,可以省成本又可以多些腳位使用。

  3. M1, M0: Mode Select bits:

    M1M0MODE
    0013 位元計時器
    0116 位元計時器
    108 位元自動重新載入
    11兩個8位元計時器

  4. 在 8051 單晶片使用內部時脈計時,會在每個機械週期值由〝1〞變為〝0〞時,將 Timer 的值累加1。也就是每一個 count 的時間為一個機械週期的時間,時間算法請見 8051 clock。

  5. 當計時器溢位時(0xff->0x00),會設置 TFx 為 1;如果 ETx 被設置成 1,則接著會進入中斷常式,並且把 TFx 設置為 0。

8 位元自動重新載入時間算法,以 40MHz 為例:


  1. 當 TLx 從 0xff 變成 0x00 時,會產一個中斷,並且把 THx 載入 TLx 再繼續計數。所以產生中斷的次數為 256-THx。

  2. 1 count = 12/40000000(s) = 12/40000(ms) = 12/40(us),所以產生中斷的時間為 (256-THx) * 12/40。

  3. 最大值為 (256-0)*12/40 = 76.8(us),最小值為 (256-255)*12/40 = 0.3(us)。

  4. 如果設定中斷時間為 60us 則 (256-THx) * 12/40 = 60,THx = 256 - 60*40/12 = 56。

8051 interrupt

Posted by: 邱小新 at 下午1:42 in ,
  1. 8051 的中斷服務功能,可使程式強制中斷去執行需要即時處理的副程式,進而提升執行效率。
  2. 在 8051 中總共提供五個中斷來源。INT0,INT1,Timer0,Timer1,UART。
  3. 中斷源在 8051 中都有相對應的旗標,當中斷條件產生時,中斷源就會使其相對應的旗標值設定為 1。8051 會在每一個機械週期檢查這些旗標的狀態,若系統允許相對的中斷源產生中斷,且該中斷相對應的旗標值亦為 1 時,則 8051 會在執行完目前正在執行的指令後,將程式在記憶體中的位址存入堆疊中,並產生中斷服務副程式的呼叫,跳到該中斷所對應之中斷向量位址去執行,8051 執行該中斷服務副程式,直到 RETI 指令後才結束中斷副程式,再從堆疊中取出先前存入的位址值繼續執行被中斷的程式。
  4. 當一個中斷要求發生時,若中斷是被致能的,則 8051 會執行該中斷服務副程式。然而在執行中斷副程式中若有較高優先權的中斷源要求中斷,則 8051 會先暫停目前正在執行的中斷服務副程式,而立即執行這個較高優先權的中斷服務副程式。如果相同優先權或優先權較低的中斷源要求中斷,則 8051 將不予理會。另外,若兩中斷同時發生,則高優先權中斷源優先執行;但若優先權相同時,則依 INT0、Timer0、INT1、Timer1、UART 之順序先後執行。

中斷產生的要件

  1. 沒有任何事情可以停止高優先權的中斷運行,即使是另一個擁有高優先權的中斷。
  2. 一個擁有高優先權的中斷發生,可以強制停止另一個低優先權的中斷運行,讓自己先行。
  3. 一個擁有低優先權的中斷發生,只能在沒有任何中斷運行的條件下,才可以執行。
  4. 如果二個中斷同時發生,高優先權的中斷會先執行;如果是同樣優先權的中斷,則依 INT0、Timer0、INT1、Timer1、UART 之順序先後執行。

中斷發生時所做的事

  1. Program Counter push 進堆疊裏,low-byte 最先。
  2. 同一優先權及低優先權的中斷被阻塞。
  3. 在時間及外部中斷發生時,相對應的中斷旗標會被自動清除。
  4. Program Counter 轉換成相對應的中斷向量位置。
  5. 執行中斷程式。

中斷結束時所做的事

  1. Program Counter 堆疊裏 pop 出來。
  2. 中斷旗標被恢復成中斷前的值。
  3. 所以在時間及外部中斷發生時,中斷程式中就不需要為了防止再次中斷發生,而自己去操作中斷旗標。
  4. 繼續執行中斷前的程式。

中斷必須保護的暫存器

  1. ACC
  2. B
  3. DPTR (DPH/DPL)
  4. PSW
  5. R0~R7

中斷時 register bank 的使用

  1. R0~R7 有 4 個 register bank 可以使用。
  2. SDCC 發現你在中斷使用 bank0 時,都會對 R0~R7 做 push/pop 動作。
  3. SDCC 發現你在中斷使用 bank1~3 時,不會對 R0~R7 做 push/pop 動作。
  4. SDCC 的中斷函數跟 main 函數沒有放在一起編譯時,則不會有任何最佳化作用,也就是中斷必須保護的暫存器通通都會做 push/pop,表示 register bank 的使用無效。
  5. 因為相同優先權不會互相中斷執行,所以相同優先權的 interrupt 可以使用同一個 bank。
  6. 因為高優先權會中斷低優先權的執行,所以不同優先權的 interrupt 不可使用同一個 bank(bank0 除外)。
  7. 最好按照下列方式使用:
    1. bank0: main routine
    2. bank1: low-priority interrupt
    3. bank2: high-priority interrupt
  8. 如果要節省記憶體的使用,就全部用 bank0 吧。

8051 clock

Posted by: 邱小新 at 上午11:00 in
  1. 一個機械週期 (Machine Cycle) 是由 6 個狀態週期 (State) S1-S6 組成。而每一個狀態週期包含 2 個振盪週期分別稱為 Pl 與 P2 。
  2. ALE (位址栓鎖致能)則是每 6 個振盪週期出現一次。
  3. 當震盪器頻率 (crystal frequency) 為 12Mhz 時,表示一秒鐘能震盪 12000000 次,所以每震盪一次時間 (1 clock time) 為 1/12000000,而一個 machine cycyle 的時間需要 12 個振盪週期則為 12*1/12000000=1/1000000。
  4. 當震盪器頻率 (crystal frequency) 為 40Mhz 時,表示一秒鐘能震盪 40000000 次,所以每震盪一次時間 (1 clock time) 為 1/40000000,而一個 machine cycyle 的時間需要 12 個振盪週期則為 12*1/40000000=3/10000000。
  5. 一般每個指令需要二到三個 machine cycle 不等,每個 machine cycle 費時 12 個 clock,因此如果接上 12Mhz 的震盪器,則有 1 MIPS 的運算量。1T 或 4T,代表可在 1 個 clock 或 4 個 clock 完成一個 machine cycle。
  6. 另一種說法,8051 的計數器是一個機器週期為一個 count,而一個機器週期費時 12 個 clock,所以說 counting at the rate of 1/12 of the clock speed,表示每秒只能計數 12000000/12 次;如果是 4T's 8051 則為 1/4 of the clock speed,表示每秒可以計數 12000000/4 次,比一般的 8051 快了三倍速度。
  7. MIPS 即 Million Instructions Per Second 的簡稱,衡量計算機性能的指標之一。它表示單字長定點指令的平均執行速度。