STC89C51/STC89C52 Timer
- 內部不帶振盪源, 必須外接晶振
- 采用11.0592MHz,或22.1184MHz,可方便得到串口通訊的標准時鍾.
STC89和STC90系列為12T, STC11/STC12系列為1T, 也就是一個指令一個機器周期, 這些都需要外置晶振; STC15系列有內置晶振.
中斷
中斷允許控制寄存器 IE
字節地址A8H, CPU對中斷系統所有中斷以及某個中斷源的開放和屏蔽是由中斷允許寄存器IE控制的
D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
---|---|---|---|---|---|---|---|
EA | — | ET2 | ES | ET1 | EX1 | ET0 | EX0 |
EA
(IE.7): 整體中斷允許位, 1:允許ET2
(IE.5): T2中斷允許位, 1:允許(for C52)ES
(IE.4): 串口中斷允許位, 1:允許ET1
(IE.3): T1中斷允許位, 1:允許EX1
(IE.2): 外部中斷INT1允許位, 1:允許ET0
(IE.1): T0中斷允許位, 1:允許EX0
(IE.0): 外部中斷INT0允許位, 1:允許
52單片機一共有6個中斷源, 它們的符號, 名稱以及各產生的條件分別如下
- INT0 - 外部中斷0, 由P3.2端口線引入, 低電平或下降沿引起
- INT1 - 外部中斷1, 由P3.3端口線引入, 低電平或下降沿引起
- T0 - 定時器/計數器0中斷, 由T0計數器計滿回零引起
- T1 - 定時器/計數器1中斷, 由T1計數器計滿回零引起
- T2 - 定時器/計數器2中斷, 由T2計數器計滿回零引起 <--這個是52特有的
- TI/RI - 串行口中斷, 串行端口完成一幀字符發送/接收后引起
定時器中斷
51單片機內部共有兩個16位可編程的定時器,即定時器T0和定時器T1, 52單片機內部多一個T2定時器. 它們既有定時功能,也有計數功能。可通過設置與它們相關的特殊功能寄存器選擇啟用定時功能還是計數功能. 這個定時器系統是單片機內部一個獨立的硬件部分,它與CPU和晶振通過內部某些控制線連接並相互作用,CPU一旦設置開啟定時功能后,定時器便在晶振的作用下自動開始計時,但定時器的計數器計滿后,會產生中斷。
定時器/計數器的實質是加1計數器(16位), 由高8位和低8位兩個寄存器組成.
- TMOD: 定時器的工作方式寄存器,確定工作方式和功能
- TCON: 控制寄存器,控制T0,T1的啟動和停止及設置溢出標志
代碼例子
#include<reg52.h>
sbit led=P3^0;
/** 中斷的設置,首先設置中斷的觸發方式,再設置開啟終端,最后開啟總中斷。*/
void main() {
IT0=1; //設置外部中斷0的觸發方式為下降沿
EX0=1; //開啟外部中斷0
IT1=1; //設置外部中斷1的觸發方式為下降沿
EX1=1; //開啟外部中斷1
EA =1; //總中斷開關
while(1) {
P0=0xaa;
P0=0xff;
}
}
void EX0_ISR(void) interrupt 0 {
led=~led;
}
void EX1_ISR(void) interrupt 2 { //外部中斷1的中斷在此為2!
led=~led;
}
代碼例子二
#include <reg52.h>
// 定義I/0引腳名稱
sbit led1=P1^1;
sbit led2=P1^2;
sbit led3=P1^3;
sbit led4=P1^4;
sbit P32=P3^2;
//全局變量及位標志定義
bit FINT0;
bit FINT1;
bit FT0;
bit FT1;
bit FT2;
unsigned char T0_10ms;
unsigned char T0_50ms;
unsigned char T0_100ms;
//函數聲明
void int_0(); //外部中斷0
void int_1(); //外部中斷1
void timer_0(); //定時器中斷1
void timer_1(); //定時器中斷2
void serial_1(); //串行中斷1
void serial_2(); //串行中斷2
//用戶函數聲明
void initial(); //初始化
void main(){
initial();
while(1){
P32=0; //為了使按'取消'、'確定'鍵能夠產生INT0及INT1中斷
//led4=0; //上一句等價於此句
if(FINT0){ //中斷0來到要做什么事情
FINT0=0;
led1=0; //INT0中斷時點亮
led2=0;
led3=0;
led3=0; //可以在此設一個斷點
}
if(FINT1){ //中斷1來到要做什么事情
FINT1=0;
led1=1; //INT1中斷時熄滅
led2=1;
led3=1;
}
if(FT0){
FT0=0;
if(++T0_10ms > 30){
T0_10ms=0;
//定時多少做什么事,未初始化里定時器尚未設置
}
}
}
}
void initial(){
EA=1; // CPU所有中斷開(IE最高位MSB)
EX0=1; // INT0中斷開
IT0=0; // INT0 0:低電平觸發, 1:下降沿觸發
EX1=1; // INT1中斷開
IT1=0; // INT1 0:低電平觸發, 1:下降沿觸發
return;
}
//INT0中斷 由P3.2引腳產生
void int_0() interrupt 0 using 0 {
FINT0=1;
}
//INT1中斷 由P3.3引腳產生
void int_1() interrupt 2 using 1 {
FINT1=1;
}
//定時器0中斷
void timer_0() interrupt 1 using 2 {
FT0=1;
}
//定時器1中斷
void timer_1() interrupt 3 using 3 {
FT1=1;
}
//串行中斷1
void serial_1() interrupt 4 { }
//定時器2中斷
void timer_2() interrupt 5 {
FT2=1;
}
定時器
89C51有兩個計數器T0和T1, 89C52還有一個定時器T2
定時器T0和T1
控制寄存器TCON
字節地址88H
, 位尋址8FH - 88H
位地址 | 8F | 8E | 8D | 8C | 8B | 8A | 89 | 88 |
---|---|---|---|---|---|---|---|---|
位符號 | TF1 | TR1 | TF0 | TR0 | IE1 | IT1 | IE0 | IT0 |
TF0
,TF1
: 計數溢出標志位, 當計數溢出時產生中斷, 由硬件置1, 當轉向中斷服務時, 再由硬件自動清0. 計數溢出的標志位的使用有兩種情況: 采用中斷方式時作為中斷請求標志位來使用; 采用查詢方式時作為查詢狀態位來使用.TR0
,TR1
: 定時器運行控制位, 0:停止, 1:啟動IE0
,IE1
: 外部中斷請求標志位. 當CPU采樣到P3.2
和P3.3
出現有效中斷請求時, 此位由硬件置1, 在中斷響應完成后轉向中斷服務時, 再由硬件自動清0.IT0
,IT1
: 外部中斷請求信號方式控制位. 1:脈沖方式(后沿負跳有效), 0:電平方式(低電平有效), 此位由軟件置1或0.
TF0(TF1)——計數溢出標志位
模式控制寄存器 TMOD
逐位定義的8位寄存器, 只能使用字節尋址, 字節地址為89H
D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
---|---|---|---|---|---|---|---|
GATE | C/T | M1 | M0 | GATE | C/T | M1 | M0 |
GATE
: 門控位- GATE=0時, 僅用TCON中的
TRO
或TR1
為1, 就可以啟動T0, T1 - GATE=1時, 不僅TCON中的
TRO
或TR1
為1, 且需要INT0/INT1也為高電平,才能工作. Enable Timer/Counter only when the INT0/INT1 pin is high and TR0/TR1 is set.
- GATE=0時, 僅用TCON中的
D0
,D1
,D2
,D3
: 為T0的設置D4
,D5
,D6
,D7
: 為T1的設置C/T
: 模式選擇, 0:定時模式, 1:計數模式. 計數模式用於外部輸入計數M0
,M1
: 工作方式選擇, 一般使用都是采用16位的計時計數器
M1 | M0 | 工作方式 | 計數器模式 |
---|---|---|---|
0 | 0 | TMOD=0x00 | 13位計數器 (8192) 13-bit timer/counter, 8-bit of THx & 5-bit of TLx |
0 | 1 | TMOD=0x01 | 16位計數器 (65536) 16-bit timer/counter, THx cascaded with TLx |
1 | 0 | TMOD=0x02 | 自動重載8位計數器 8-bit timer/counter (auto-reload mode), TLx reload with the value held by THx each time TLx overflow |
1 | 1 | TMOD=0x03 | T0分為2個8位計數器,T1為波特率發生器. Split the 16-bit timer into two 8-bit timers i.e. THx and TLx like two 8-bit timer |
可以看到, TMOD=0x01 的情況下, the timer/counter is configured in 16-bit mode. To be specific, it’s counts all the way from 0x0000 to 0xffff. And this mode gives a maximum delay of 71.106 millisecond, again at a osc of 11.0592 MHz.
用11.0592MHz晶振的C52產生較精確的1秒定時中斷, 下面的代碼是基於SDCC的8052.h. 下面說明一下定時器初始值的計算
- 由晶振11.0592 MHz, 得到定時器時鍾為 11.0592 / 12 = 0.9216 MHz,
- 因此1ms對應 921.6 個時鍾周期,
- 因此50ms對應 46080 個時鍾周期,
- 將其設為一次中斷后, 20次中斷就對應1s
- 代碼中的13, 是用於扣減掉執行時產生的額外機器周期(machine cycles)
代碼
#include <8052.h>
#define d_time (65536 - 46080 + 13 + 1)
const unsigned char tl = d_time;
const unsigned char th = d_time >> 8;
volatile unsigned char i = 0;
void main() {
TMOD= 0x01; //工作方式為16位定時器
TH0 = th; //計數寄存器高8位
TL0 = tl; //計數寄存器低8位
EA = 1; //允許中斷
ET0 = 0x01; //允許T0中斷
TR0 = 1; //啟動T0
while(1);
}
void Timer0IRQ(void) __interrupt (1) // 中斷處理函數 T0 -> 中斷1
{
i++;
if(i > 20) {
P0_7 = (P0_7 == 1)? 0 : 1; //觸發P0.7 LED閃爍
i = 1; // 注意這邊不能初始化為0, 否則每次會多跑一個中斷
}
TH0 = th; //計數寄存器高8 位重新載入
TL0 = tl; //計數寄存器低8 位重新載入
}
定時器T2
控制寄存器TCON2
字節地址0C8H, 可位尋址
CF | CE | CD | CC | CB | CA | C9 | C8 |
---|---|---|---|---|---|---|---|
TF2 | EXF2 | RCLK | TCLK | EXEN2 | TR2 | C/T2 | CP/RT2 |
溢出標志位 | 定時器外部標志 | 接收時鍾標志 | 發送時鍾標志 | 外部使能 | 啟動、停止控制位 | 選擇位 | 捕獲重裝標志 |
TF2
: T2溢出標志, T2溢出時置位並申請中斷, 只能用軟件清除, 但T2作為波特率發生器使用的時候, (即RCLK=1或TCLK=1), T2溢出時不對TF2置位.EXF2
: 當EXEN2=1時, 且T2EX引腳P1.0
出現負跳變而造成T2的捕獲或重裝的時候, EXF2置位並申請中斷, EXF2也是只能通過軟件來清除.RCLK
: 串行接收時鍾標志, 只能通過軟件的置位或清除. 選擇T1或T2作為串行接收的波特率產生器, 0:選擇T1
, 1:選擇T2
.TCLK
: 串行發送時鍾標志, 只能通過軟件的置位或清除, 選擇T1或T2作為串行發送的波特率產生器, 0:選擇T1
, 1:選擇T2
.EXEN2
: T2的外部允許標志, 只能通過軟件的置位或清除- 0: 禁止外部時鍾觸發T2
- 1: 當T2未用作串行波特率發生器時, 允許外部時鍾觸發T2, 當T2EX引腳輸入一個負跳變的時候,將引起T2的捕獲或重裝,並置位EXF2,申請中斷.
TR2
: T2的啟動控制標志, 0:停止T2, 1:啟動T2C/T2
: T2的定時方式或計數方式選擇位, 只能通過軟件的置位或清除. 0:定時器方式, 1:計數器方式, 下降沿觸發.CP/RT2
: 捕獲/重裝載標志, 只能通過軟件的置位或清除.- 0: 重裝載方式, 這時若T2溢出(EXEN2=0), 或者T2EX引腳
P1.0
出現負跳變(EXEN2=1), 將會引起T2重裝載 - 1: 捕獲方式, 這時若T2EX引腳
P1.0
出現負跳變(EXEN2=1), 將會引起T2捕獲操作. - RCLK=1或TCLK=1時, CP/RT2控制位不起作用, 被強制工作於定時器溢出自動重裝載模式.
- 0: 重裝載方式, 這時若T2溢出(EXEN2=0), 或者T2EX引腳
模式控制寄存器T2MOD
字節地址0C9H, 不可位尋址
D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
---|---|---|---|---|---|---|---|
-- | -- | -- | -- | -- | -- | T2OE | DCEN |
T2OE
: T2輸出允許位, 當T2OE=1時, 允許時鍾輸出到P1.0
(僅對80C54/80C58有效)DCEN
: 向下計數允許位, DCEN=1允許T2向下計數, 否則向上計數.
使用 __nop();
精確定時
假如使用者想要產生精確的延遲時間,建議使用__nop()
函數來組合達成。__nop()
函數能夠產生 1 個精確的 CPU 頻率周期延遲時間。然而,由於 flash 的速度低於 CPU 的頻率速度,在 CPU 內部有緩存優化的技術,編譯程序也會自動針對程序做優化,造成__nop() 函數組合出來的時間會與預期的時間不同。因此,建議將程序放置於 SRAM 中執行,以避免優化造成的非預期延遲時間問題. 以產生 2 us 的延遲時間為例:
- CPU 頻率= 32MHz => 1 CPU 頻率周期花費 1/32000000 sec = 31.25 ns
- 2us 延遲時間 = 2000ns / 31.25 ns = 64 次 CPU 頻率周期
由於執行一次 for 循環需要花費 5 個 CPU 頻率周期的時間,因此可以使用以下的方式達到 2 us 的時間延遲
- 執行一次 for 循環需要 5 個 CPU 頻率周期
- 執行一次
__NOP()
指令需要 1 個 CPU 頻率周期 - 64 個 CPU 頻率周期 =
8 * ( 5 ( for 循環 ) + 3 * ( __NOP() ) )
void Delay_Test_Function(void) {
for(i = 0; i < 8 ; i++) { /* Delay for 2 us. */
__NOP();
__NOP();
__NOP();
}
}
例子2, 執行一次 PA = 0 需花費 11 CPU 指令周期,這意味着 I/O 會持續 (64+11) * 31.25 ns = 2343.75 ns 的時間才進行轉態。
void Delay_Test_Function(void) {
uint32_t i, DelayCNTofCPUClock = 8;
PA0 = 1;
for(i = 0; i < DelayCNTofCPUClock ; i++) { /* Delay for 2 micro seconds. */
__NOP();
__NOP();
__NOP();
}
PA0 = 0;
}
STC-ISP軟件提供的示例代碼
延時1ms
STC89Cxx, STC90Cxx
C語言
void Delay1ms() //@11.0592MHz
{
unsigned char i, j;
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
匯編實現
DELAY1MS: ;@11.0592MHz
NOP ; 1周期
PUSH 30H ; 入棧2周期
PUSH 31H ; 入棧2周期
MOV 30H,#2 ; 3周期
MOV 31H,#194 ; 3周期
NEXT:
DJNZ 31H,NEXT ; 2周期
DJNZ 30H,NEXT ; 2周期
POP 31H ; 2周期
POP 30H ; 2周期
RET ; 2周期
這里說明一下執行邏輯:
- 保存30H和31H的值到棧中
- 分別寫入十進制值2和194, 然后進入NEXT標下面的代碼
DJNZ 31H,NEXT
這行, 會對31H減一后判斷是否為0, 這里會執行194次直到值變為0DJNZ 30H,NEXT
到了這行, 會對30H減一后判斷是否為0, 初始值為2, 減一后為1, 繼續回到NEXT標DJNZ 31H,NEXT
這行, 31H已經歸0了, 減一回到FF, 這里會執行256次直到值變為0DJNZ 30H,NEXT
到了這行, 會對30H減一后判斷是否為0, 原值為1, 減一后為0, 往下執行- 30H和31H的值出棧, 要注意順序, 先入后出
- 返回
- 總共的指令周期是1 + 2 + 2 + 3 + 3 + 2(194+256) + 22 + 2 + 2 + 2 = 921
- 對於11.0592MHz的晶振, 對應12T單片機的指令周期為0.9216MHz, 對應一個毫秒為921.6個周期, 921是很接近的一個值
STC11, STC12
C
void Delay1ms() //@11.0592MHz
{
unsigned char i, j;
_nop_();
i = 11;
j = 190;
do
{
while (--j);
} while (--i);
}
匯編實現
DELAY1MS: ;@11.0592MHz
NOP ; 1周期
NOP
NOP
NOP
PUSH 30H ; 入棧4周期
PUSH 31H ; 入棧4周期
MOV 30H,#9 ; 直接地址MOV, 3周期
MOV 31H,#148 ; 直接地址MOV, 3周期
NEXT:
DJNZ 31H,NEXT ; 5周期
DJNZ 30H,NEXT ; 5周期
POP 31H ; 出棧3周期
POP 30H ; 出棧3周期
RET ; 4周期
分析一下邏輯, 指令周期長度可以查看芯片手冊的第五章, 指令系統
- 4個單周期NOP
- 30H和31H入棧
- 給30H賦值十進制9, 給31H賦值十進制148, 進入NEXT代碼段
DJNZ 31H,NEXT
, 31H初始值為148, 減一后為147, 不等於0, 因此跳回NEXT, 這里要經過148次DJNZ 30H,NEXT
, 30H初始值為9, 減一后為8, 不等於0, 因此跳回NEXTDJNZ 31H,NEXT
, 31H初始值為00H, 減一后為FFH, 不等於0, 因此跳回NEXT, 這里要經過256次DJNZ 30H,NEXT
, 30H值為8, 減一后為7, 不等於0, 因此跳回NEXT- ...
- 30H和31H的值出棧, 注意順序, 先入后出
- 返回
周期次數計算
- 4 + 4 + 4 + 3 + 3
- 5 * 148
- 5 * 256 * 8
- 5 * 9
- 3 + 3 + 4
- 總共是 11053, 接近晶振11.0592MHz的千分之一, 即11059.2個周期
參考
- C51 匯編寫的延時函數說明及時鍾頻率 http://www.51hei.com/mcuteach/247.html
- Very helpful SDCC C51 code examples https://github.com/hungtcs-lab/8051-examples
- 8051 DJNZ Instruction https://www.refreshnotes.com/2016/02/8051-djnz-instruction.html
- https://www.keil.com/support/man/docs/is51/is51_djnz.htm