字體:小 中 大 | |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
2019/01/03 01:24:03瀏覽1821|回應0|推薦0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
單晶片MCU STC12C5608AD的時鐘程式 使用MCU單晶片微處理機撰寫時鐘程式,是練習程式撰寫的常用題目,當然有使用RTC晶片DS1302來負責年,月,日,時,分,秒的計數,但是也有可以使用內部的Timer0計時器中斷計時,來完成時鐘的功能,但是本次撰寫此文的目的,是在強化時鐘的準確性,無論是使用DS1302或是Timer0,都牽涉到石英晶體的振盪頻率的偏移率,像DS1302的RTC晶片均需使用32.768KHz的頻率,而時鐘的準確性就與此振盪頻率有直接的關係,然而每一顆石英晶片的頻率多少都有些偏移,累積一段時間後,就會產生每天誤差數秒至幾分鐘,而如果使用MCU內部的Timer0計時器中斷計時,更會因為MCU的中斷時序的時間差,也更會產生時間累積誤差,再者是石英晶體本身為會因為工作溫度之高低產生振盪頻率的偏移,所以本次就是在構思在程式中加寫補償修正變數來修正差異,而此修正的絞數利用UART指令的方式來下達。 也許同好會覺得何以用UART下達指令的方式來修正補償變數,而不用單純的按鍵開關來設定,實在因為撰寫程式的重點在提供補償參數,所以就省略用按鍵掃瞄的方式,另外在時鐘顯示部份,也不用LED 7劃顯示器,而採用之前的I2C LCM模組的方式來製作此實驗,希望同好能諒解之,期望無論先進或後輩參考的是如何修正時間累積誤差,與UART指令接收的方法. 以下是介紹所定義的指令集: 1)設定時間—時,分,秒 設定時,分,秒的時間 指令集頭碼(2byte) 0x20,0xa5 指令碼(1byte) 0x80 參數一(1byte:秒) 0x00~0x59 參數二(1byte:分) 0x00~0x59 參數三(1byte:時) 0x00~0x23 參數四~八(5byte預留) 0x00,0x00,0x00,0x00,0x00, 校閱總和碼(1byte) 就是從頭碼~參數八的11bytes的總和 舉例:一)設定時間23時59分0秒 20 a5 80 00 59 23 00 00 00 00 00 c1 2)設定時間—年,月,日 設定年,月,日的日期 指令集頭碼(2byte) 0x20,0xa5 指令碼(1byte) 0x81 參數一(1byte:日) 0x00~0x31 參數二(1byte:分) 0x00~0x12 參數三(1byte:時) 0x00~0x18 參數四~八(5byte預留) 0x00,0x00,0x00,0x00,0x00, 校閱總和碼(1byte) 就是從頭碼~參數八的11bytes的總和 舉例:設定日期18年12月31日 20 a5 81 31 12 18 00 00 00 00 00 a1 3)設定時間補償參數—CTx,CTy,CTz 目前定義三個變數,分別是CTx,CTy,CTz CTx:正值,所增加Timer0計時數,每增加1,就是增加1個t週期 ,也就是T0中斷時間增加一個1/XTAL頻率,以22.1184MHz,也就是45.211nSEC(每一個nSEC是負9次方秒),相對的就是時間變慢 CTy:負值,減少Timer0計時數,每設定1值,就是減少1個t週期 ,相對的就是時間變快 CTz:補秒數,這是在擴寫程式時,發現雖然有CTx與CTy兩個正負變數來修正Timer0計時數,但是仍然會有一天之累積誤差的情形,為了再進一步修正,可以利用此變數來補償之,使用的方式,就是先用CTx與CTy設定參數,但是讓時鐘走完一天時,有略為慢23秒之內,再將所慢的秒數由此變數補償之,程式會依所設定的0~23秒分配至每小時之中,舉例之,如果此CTz為12,那麼0時~12時,在每一個滿59分後,就自動加一秒,一直到13時就不再補償加秒,如此就可以將時鐘所走慢的秒數加回來。 指令集頭碼(2byte) 0x20,0xa5 指令碼(1byte) 0x82 參數一(1byte:秒) 0x00~0x99 參數二(1byte:分) 0x00~0x99 參數三(1byte:時) 0x00~0x23 參數四~八(5byte預留) 0x00,0x00,0x00, 0x00,0x00, 校閱總和碼(1byte) 就是從頭碼~參數八的11bytes的總和 舉例:一)設定CTx=99,CTy=0,CTz=12 20 a5 82 99 00 12 00 00 00 00 00 f2 4)設定顯示補償參數—CT,CTx,CTy,CTz 有了以上三個指令,當然前兩個指令,設定時,分,秒與設定年,月,日後,都會在設定後直接顯示在LCM的第一行,而第三個指令,因為將異動Timer0計時數,所以需要第四個指令來將目前MCU程式內所設定的變數顯示出來,讓操作者參考,而顯示會於LCM第二行CT,CTx,CTy與CTz,請看圖示,如此顯示會在下一次新的指令被接收時清除之,而顯示所收到的uart指令,而CT的值的基數58800,因為超過32768,所以顯示—6736,也就是補數值65536—58800。 指令集頭碼(2byte) 0x20,0xa5 指令碼(1byte) 0x83 參數一(1byte:秒) 0x00 參數二(1byte:分) 0x00 參數三(1byte:時) 0x00 參數四~八(5byte預留) 0x00,0x00,0x00, 0x00,0x00, 校閱總和碼(1byte) 就是從頭碼~參數八的11bytes的總和 舉例:指令0x83 20 a5 83 00 00 00 00 00 00 00 00 48 <<各位同好,本實作的程式仍然不斷的增加新功能,因此會不定時的更新,請見諒>> 5)設定ALARM時間,ALM_hrs與ALM_min 再次增加新功能,就是增加ALARM的設定,如果設定了ALARM時間後,就會在年月日顯示區,採用交錯的方式顯示"年月日"與"ALARM時分",如果ALARM未設定,也就是為0時0分時,就會停止交錯顯示的功能,只顯示年月日而已。 指令集頭碼(2byte) 0x20,0xa5 指令碼(1byte) 0x84 參數一(0x00) 0x00 參數二(1byte:分) 0x00 參數三(1byte:時) 0x08 參數四~八(5byte預留) 0x00,0x00,0x00,0x00,0x00 校閱總和碼(1byte) 就是從頭碼~參數八的11bytes的總和 舉例:指令0x84 20 a5 84 00 00 08 00 00 00 00 00 51 本次單晶片時鐘實作所採用STC125608AD,而非STC89C54或ATMAEL89C4052,是在介紹另一個小包裝,又具大容量程式空間,而最主要的可以利用簡單的ISP工具,就可以將撰寫好的程式直接燒錄至單晶片微處理機內,而像ATMEL89C4052則需有特定的燒錄程式,而且容量只有4096個BYTES,不像STC1256XXAD系列,容量最大可以達30KBYTES,並且有不同之包裝下,I/O數也增加了許多,並且工作頻率可達35MHZ,足足於相當於標準的8051的420MHZ之高,可以說是工作速度快8~12倍,另外的就是有新增加了PWM,A/D轉換,同系列不同型號又有SPI,ADC,6個TIMER等功能,讓有心玩玩單晶片的人來說,非常容易入手,唯一的是在實作的過程,有些不明確的功能無法執行,只能暫且用取功的方式來完成實作。 這次的實作,著重於UART下達指令集來設定年,月,日,時,分,秒與計時器的補償修正變數,而採用的傳輸速率BAUDRATE是115200,8,N,1。同時為了配合UART傳輸速率,實作的MCU工作頻率在22。1184MHZ,為了簡化製作的過程,在顯示器的部份則是使用之前有介紹的I2C LCM1602轉換介面。 以下表格是利用EXECL來試算石英振盪頻率偏移下,相對產生Timer0計時器中斷等的誤差值,再加上CT,CTx,CTy數值補償可修正的時間差
/*----------------------------------------------- 檔案名稱:LT-TEST_04.c 程式說明: 此程式在撰寫一個由MCU內部的 Timmer 0 計時器來實現時鐘, 而重點在構思不使用 RTC DS1302 晶片下, 只利用計時器定時中斷的功能, 並且在程式責寫時加入三個 CTx,CTy,CTz變數,來修正石英振盪晶體的頻率 之差異,而且可以利用UART指令來設定修正的數值. 附註 : STC12C5608AD 單晶片MCU @2019/01/05 added BUZZ & ALARM command ------------------------------------------------*/ #include “reg52.h” #include “stdio.h” #include “stdlib.h” #include “intrins.h” #include “string.h” #include "delay.c" #include "I2CPCF8574.c" //I2C LCM 副程式 unsigned char* RC ="TEST_04 2019-01-01"; #define XTAL 22118400 //XTAL 外部石英晶體 22.1184 Mhz #define BAUDRATE 115200 //UART BAUDRATE 115200,8,N,1 #define UARTSIZE 12 //Uart buffer size #define BS 0x08 //\b backspace code #define WAIT 10 #define WAIT5 5 typedef unsigned char BYTE; typedef unsigned int WORD; typedef unsigned long DWORD; //sbit SW_P32 = P3^2; //SWITCH P32 sbit LED_P33 = P3^3; //SWITCH P33 //sbit LCM_SDA = P3^4; //PCF8574AT SDA //sbit LCM_SCL = P3^5; //PCF8574AT SCL sbit LED_BLU = P3^7; //WIFI ENABLE sbit LED_RED = P1^0; // sbit LED_YEW = P1^1; // sbit OUT_P12 = P1^2; // sbit OUT_P13 = P1^3; // sbit OUT_P14 = P1^4; // sbit OUT_P15 = P1^5; //
sbit OUT0 =P2^0; sbit OUT1 =P2^1; sbit OUT2 =P2^2; sbit OUT3 =P2^3; sbit OUT4 =P2^4; sbit OUT5 =P2^5; sbit OUT6 =P2^6; sbit OUT7 =P2^7; /*------------------------------------------------ 整體變數宣告 ------------------------------------------------*/ bit Turn_flag=0,SetFlag=0,busy=0,LCD_flag,DISP_flag=0,BUZZ=0; unsigned char temp[24]; unsigned char UART_buf[UARTSIZE]; //UART RCV Buffer unsigned xdata AC_ms=0,Rcv_idx,AC_sec,AC_min,AC_hrs,AC_day,AC_mth,AC_yer,CTx,CTy,CTz; unsigned xdata ALM_hrs,ALM_min; unsigned short CT; /*------------------------------------------------ 函數聲明 ------------------------------------------------*/ void Init_Timer0(void); void Init_Timer1(void); void Uart_SendStr(unsigned char *value,unsigned int leng); void Uart_PutChar(unsigned char dat); void Uart_ISR_Handle(void); void Uart_cmd(void); // UART接收指令處理副程式 void Sent_Uart(void); // UART送出訊息 void CHECK_year(void); // 萬年曆檢查副程式 /*------------------------------------------------ 主程式 ------------------------------------------------*/ void main(void) { unsigned char i=0,k; //
P1 = P2 = P3 =0xff; AUXR &= 0xbf; CT=58921; // Timer 0 count down參數基值 Init_Timer0(); // 計時器初始化 Init_Timer1(); // UART baus rate LCD1602_Device_Init(XIO_ID); //啟動I2C LCD介面板與 1602液晶螢幕 DelayMs(WAIT5); //需延遲待待LCD螢幕內部運作
for(i=0;i SetFlag=0; // 完成 I2C-LCM的初啟程式.清除旗號 AC_ms=0; LCD1602_Write_Command(XIO_ID,0x01); //清除螢幕內容, XIO_ID 需依 I2C PCF8574T(0x40) 或 PCF8574AT(0x70) DelayMs(WAIT5); AC_sec=AC_min=AC_hrs=0;AC_day=1;AC_mth=1;AC_yer=19; ET0=1; TR0=1; ES=1; EA=1; //打開總中斷 //********************************** Uart_SendStr(RC,20); //system start, sent the system log to WIFI DelayMs(WAIT5); // if(AC_hrs>=10) { sprintf(temp," %2d/%2d ",(int)AC_mth,(int)AC_day); } // else { sprintf(temp,"%2d/%d/%d ",(int)AC_yer,(int)AC_mth,(int)AC_day); } // 顯示 年/月/日 時間; if(RI==0) { LCD1602_Write_String(XIO_ID,0,0,temp); } Rcv_idx=0;CTx=CTy=0; // sprintf(temp,"CT%5d x%2d y%2d ",(short)(CT+CTx),(int)CTx,(int)CTy); //顯示 CT, CTx 與 CTy 的數值 // if(RI==0) {LCD1602_Write_String(XIO_ID,0,1,temp);} DelayMs(2000); while(1) //主程式 { BUZZ=1;ALM_hrs=0;ALM_min=0; START: //************************** ES=1; if(SetFlag==1) //UART HAD COMMAND RECIVED { DISP_flag=0; k=0; //****** UART CHECKSUM for(i=0;i<10;i++) k="k%256+(int)UART_buf[i];}" p=""> if((int)UART_buf[11] != k) //比對CHECKSUM值 { SetFlag=0;Rcv_idx=0;goto START_A;} else { Uart_cmd(); } START_A: //顯示接收到的 UART DATA if(DISP_flag==0) { sprintf(temp,"%02x%02x%02x%02x",(int)UART_buf[2],(int)UART_buf[3],(int)UART_buf[4],(int)UART_buf[5]); LCD1602_Write_String(XIO_ID,0,1,temp); sprintf(temp,"%02x %02x<%02x",(int)uart_buf[6],(int)uart_buf[11],(int)k); p=""> LCD1602_Write_String(XIO_ID,8,1,temp); } SetFlag=0; } //**************************** if(Turn_flag==1 ) { LED_RED=~LED_RED; if(AC_min>=60 ) // 是否時間"分"已滿60分 { AC_min=0;AC_hrs++; if(CTz >= AC_hrs) // 判斷是否需要補加"秒"數 { AC_min++; } } if(AC_hrs>=24 ) // 判斷是否已滿24小時 { AC_hrs=0;AC_day++; CHECK_year(); //萬年曆檢核 } if((AC_sec%2)==0 && (ALM_hrs!=0 || ALM_min!=0)) { sprintf(temp,"A%d/%2d ",(int)ALM_hrs,(int)ALM_min); } // 顯示 ALARM時間 else { sprintf(temp,"%2d/%d/%d ",(int)AC_yer,(int)AC_mth,(int)AC_day); } // 顯示 年/月/日 時間; if(RI==0) { LCD1602_Write_String(XIO_ID,0,0,temp); } sprintf(temp,"%2d:%2d:%2d",(int)AC_hrs,(int)AC_min,(int)AC_sec); //顯示 CLOCK 時間 if(RI==0) { LCD1602_Write_String(XIO_ID,(16-strlen(temp)),0,temp); } if(ALM_hrs==AC_hrs && ALM_min==AC_min && (AC_sec%2)==0) { BUZZ=1;} // 比對 ALARM時間是否吻合 else BUZZ=0; Turn_flag=0; } goto START; } } //******************************** void CHECK_year(void) { if((AC_mth==4 || AC_mth==6 || AC_mth==9 || AC_mth==11) && AC_day>=31) {AC_day=1;AC_mth++;goto C0;} //判斷有31日的月份 else { if(AC_mth==2 && AC_day>=29 && (AC_yer%4)!=0) {AC_day=1;AC_mth++;goto C0;} //判斷是否閏年之2月28日 else { if(AC_mth==2 && AC_day>=30 && (AC_yer%4)==0) {AC_day=1;AC_mth++;goto C0;} //判斷是否閏年之2月29日 else { if((AC_mth==1 || AC_mth==3 || AC_mth==5 || AC_mth==7 || AC_mth==8 || AC_mth==10 || AC_mth==12) && AC_day>=32) {AC_day=1;AC_mth++;goto C0;} } //判斷有30日的月份 } } C0: if(AC_mth>=13) {AC_yer++;AC_mth=1;} // 判斷月份是否已滿12月 return; } //****************** void Uart_cmd(void) { unsigned int i,j; switch((int)UART_buf[2]) { case 0x80: //WIFI下傳時間. AC_hrs=(UART_buf[5]/16)*10+(UART_buf[5]%16); //16進制轉為10進制 AC_min=(UART_buf[4]/16)*10+(UART_buf[4]%16); AC_sec=(UART_buf[3]/16)*10+(UART_buf[3]%16); break; case 0x81: //WIFI下傳日期 AC_yer=(UART_buf[5]/16)*10+(UART_buf[5]%16); //16進制轉為10進制 AC_mth=(UART_buf[4]/16)*10+(UART_buf[4]%16); AC_day=(UART_buf[3]/16)*10+(UART_buf[3]%16); // if(AC_hrs>=10) { sprintf(temp," %2d/%2d ",(int)AC_mth,(int)AC_day); } // else { sprintf(temp,"%2d/%d/%d ",(int)AC_yer,(int)AC_mth,(int)AC_day); } // 顯示 年/月/日 時間; if(RI==0) { LCD1602_Write_String(XIO_ID,0,0,temp); } break; //******************* case 0x82: CTx=(UART_buf[3]/16)*10+(UART_buf[3]%16); // CTx參數, 補正差 CTy=(UART_buf[4]/16)*10+(UART_buf[4]%16); // CTy參數, 扣負差 CTz=(UART_buf[5]/16)*10+(UART_buf[5]%16); // CTz參數, 補秒數 DISP_flag=0; break; case 0x83: i=(CT-(CT%1000))/1000;j=CT%1000; sprintf(temp,"CT%2d%3d ",(int)i,(int)j); if(RI==0) {LCD1602_Write_String(XIO_ID,0,1,temp);} sprintf(temp,"%02x %02x %02x",(int)CTx,(int)CTy,(int)CTz); //顯示 CT, CTx, CTy與 CTz 的數值 if(RI==0) {LCD1602_Write_String(XIO_ID,8,1,temp);} DISP_flag=1; //設定顯示時間修正參數值 break; case 0x84: ALM_hrs=(UART_buf[5]/16)*10+(UART_buf[5]%16); // ALARM 小時 ALM_min=(UART_buf[4]/16)*10+(UART_buf[4]%16); // ALARM 分鐘 break; default: break; } } /*------------------------------------------------ 計數器0的初始化 for w/o RTC ------------------------------------------------*/ void Init_Timer0(void) // 計數器0的初始化 { TMOD |= 0x01; // 選擇為計時器0模式,工作方式 1,僅用TR0打開啟動。 AUXR &= ~0x80; TH0 = (65536-(CT+CTx-CTy))>>8; // 設置計時器初始值, 4ms TL0 = (65536-(CT+CTx-CTy)); //
TR0 = 1; //打開計時器 } /*------------------------------------------------ 計時器 TIMER0 中斷處理 for w/o RTC ------------------------------------------------*/ void TIMER0(void) interrupt 1 using 1 { unsigned int i; TH0 = (65536-(CT+CTx-CTy))>>8; // 重設計時器初始值 TL0 = (65536-(CT+CTx-CTy)); // i++;LED_BLU=~LED_BLU; if(BUZZ==1)OUT_P12= ~OUT_P12; // 如何ALARM旗號被啟動,就輸出 BUZZL信號 else OUT_P12=0; if(i>=250) // 確認是否滿足1秒鐘 { AC_sec++;if(AC_sec>=60) {AC_sec=0;AC_min++;} i=0;Turn_flag=1; // 設定足秒旗號 } } /*------------------------------------------------ 計數器 1 的初始化 115200,8,N,1 @ 1T / 22.1184Mhz ------------------------------------------------*/ void Init_Timer1(void) { SCON = 0x5a; AUXR |= 0x40; TMOD = 0x20; // setting Timer1 to 8bits autoload TL1 = (256-(XTAL/32/BAUDRATE)); // @22.1184Mhz, for 115200bps TH1 = (256-(XTAL/32/BAUDRATE)); TR1 = 1; // Timer1 running enable ES = 1; // serial port interrupt enable EA = 1; // interrupt enable } /*------------------------------------------------ 發送一個位元組 ------------------------------------------------*/ void Uart_PutChar(unsigned char dat) { while(busy); busy=1; SBUF = dat; } /*------------------------------------------------ 發送一個字串 ------------------------------------------------*/ void Uart_SendStr(unsigned char *value,unsigned int leng) { while(leng>0) // { Uart_PutChar(*value); value++; leng--; } } /*------------------------------------------------ 串口中斷程式 ---------------------------------------------*/ void Uart_ISR_Handle (void) interrupt 4 { if(RI==1) { UART_buf[Rcv_idx]=SBUF; if(UART_buf[Rcv_idx-1]==0x20 && UART_buf[Rcv_idx]==0xa5) {Rcv_idx=1;UART_buf[0]=0x20;UART_buf[1]=0xa5;} Rcv_idx++; if(Rcv_idx>=UARTSIZE) //連續接收16個字元資訊 { Rcv_idx=0; SetFlag=1; //接收完成,標誌旗號設定 1 } RI = 0; } if(TI==1) //如果是發送標誌位元,清零 { TI=0; busy=0; //清除傳送旗號 } } //*********************************
以下之圖是STC ISP的操作畫面,分別用不同的色框來標示應注意的選項,其中要留意的是勾選外部石英晶體,然而即使在實作上用的零件為22。1184MHZ,但是在下載程式碼後,ISP軟體會測出目前的振盪頻率為22。090MHZ,不過此數值應該是平均值,實際上由示波器量測時,此頻率會上下偏移。
線路圖如下:
照片圖一:整體實作的圖示
照片二:I2C—LCM1602背面的I2C轉換介面
照片三:照片三:在第一行的年月日顯示區,將會依ALARM是否有被設定鬧鐘時間,來決定是否 要交錯顯示年月日,或是ALARM設定的鬧鐘時間。
照片四:這是當交錯顯示ALARM鬧鐘時間。
本文中的C程式,有幾個副程式在此特別說明如下: A)void CHECK_year(void) 此程式是在用來判斷年,月,日的進階,因為目前的實作未使用DS1302的RTC晶片,所以在每日23時59分59秒時將進階至隔一天,而當日期加一以後,就需要程或去判斷每個月份是否需要進階至下一個月份,而且需判斷2月份是否閏月,因此就每一次日期有被加一後。就經由CHECK_year(void)來判斷之,這後程式分作幾個判斷式,先判斷只有30日的月份,如果吻合,那就累加月份,並將日期重設為1,即第一天,如不符合,就跳至第二判斷式,就是判斷是否為為2月份的第28日與不是閏年的條件,第三個判斷式,就是判斷是否吻何2月29日與是閏年的條件,最後一個判斷式就是判斷是否吻合一年中幾個有31日的月份,而最後會再判斷月份是否已超過第12個月,如果是就累加年份即可,如此就完成萬年曆的判斷。
//******************************** void CHECK_year(void) { if((AC_mth==4 || AC_mth==6 || AC_mth==9 || AC_mth==11) && AC_day>=31) {AC_day=1;AC_mth++;goto C0;} //判斷有30日的月份 else { if(AC_mth==2 && AC_day>=29 && (AC_yer%4)!=0) {AC_day=1;AC_mth++;goto C0;} //判斷是否閏年之2月28日 else { if(AC_mth==2 && AC_day>=30 && (AC_yer%4)==0) {AC_day=1;AC_mth++;goto C0;} //判斷是否閏年之2月29日 else { if((AC_mth==1 || AC_mth==3 || AC_mth==5 || AC_mth==7 || AC_mth==8 || AC_mth==10 || AC_mth==12) && AC_day>=32) {AC_day=1;AC_mth++;goto C0;} } //判斷有31的月份 } } C0: if(AC_mth>=13) {AC_yer++;AC_mth=1;} // 判斷月份是否已滿12月 return; }
if(Turn_flag==1 )
/*------------------------------------------------
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
( 興趣嗜好|電腦3C ) |