|
一般很多人做電子鐘都是那種純數(shù)字顯示的,這種比較容易實現(xiàn),而且51黑電子論壇也有很多例子借鑒,重復(fù)去實現(xiàn)也沒有什么新意。我花了2天的時間,寫程序?qū)崿F(xiàn)一個像樣的電子鐘表盤的制作,再加上DS3231時鐘芯片之后,就實現(xiàn)一個電子鐘的制作。
接下來,我就詳細(xì)講解一下電子鐘盤的實現(xiàn)過程。
鐘表是由圓形的鐘盤和三條指針構(gòu)成,要在OLED上實現(xiàn)繪制,需要用到畫點函數(shù)、取點函數(shù)、畫線函數(shù)、畫圓函數(shù)、刷新函數(shù)。那首先講一下OLED的圖形驅(qū)動部分。
一般我們要操作OLED顯示內(nèi)容,都是直接將往OLED的某行某頁寫數(shù)據(jù),這種方法簡單直接,效率比較高。但是,對于像素的細(xì)致操作是很不友好的。比如無法通過SPI讀取某個像素點的狀態(tài),那就無法實現(xiàn)部分自定義像素點的反轉(zhuǎn)。為了更方便操作像素點,這里用使用了一個1024字節(jié)的大數(shù)組來作為OLED的顯存,每個字節(jié)可以操控8個像素點,總共可以操控1024×8=8192個像素點,剛好對應(yīng)0.96寸OLED的128×64的分辨率。實現(xiàn)顯存的好處是方便修改操作像素點,每個像素點的狀態(tài)都一目了然。修改好顯存的內(nèi)容后,直接將顯存的數(shù)據(jù)傳到OLED就可以刷新畫面了。顯存數(shù)組的定義如下:
uint8 OLED_DISPLAY[8][128]; //顯示緩沖數(shù)組(總共可以表示8192個像素點)
為了方便定位和操作像素點,使用坐標(biāo)軸的思想,引入x軸和y軸,其中x軸的范圍為0-127(128個像素點),y軸的范圍為0-63(64個像素點)。畫點函數(shù)實現(xiàn)如下:
/**************************************************************************************************
*@函數(shù) OLED_SetPixel
*
*@簡述 OLED設(shè)置坐標(biāo)像素點狀態(tài)
*
*@輸入?yún)?shù) x - x坐標(biāo)
* y - y坐標(biāo)
* PixelValue - 像素點狀態(tài)(1:填充,0:清空)
*
*@參數(shù) 無
*
*輸出參數(shù)
*
*無
*
*@返回 無
*說明:規(guī)定OLED顯示區(qū)域左上角頂點處為坐標(biāo)原點(0,0),
* x坐標(biāo)增長方向:向右→
* y坐標(biāo)增長方向:向下↓
* 坐標(biāo)原點(0,0)對應(yīng)OLED_DISPLAY[0][0],即第零頁第一個像素點
* 坐標(biāo)(127,63)為OLED屏幕又下角頂點
**************************************************************************************************
*/
void OLED_SetPixel(int32 x, int32 y, int32 PixelValue)
{
int32 pos,bx,temp=0;
pos = y/8;//計算y坐標(biāo)所在頁
bx = y%8;//計算位偏移
temp = 1<<bx;
if(PixelValue)
{
OLED_DISPLAY[pos][x]|=temp;
}
else
{
OLED_DISPLAY[pos][x]&=~temp;
}
}
很容易看出,OLED_SetPixel函數(shù)操作的對象就是顯存數(shù)組,要點亮某個點,就是將顯存的某個字節(jié)某個位置1。當(dāng)然,由于只是修改顯存的內(nèi)容,還沒有將顯存更新到OLED中,所以不會在OLED不會顯示點亮某個點。有畫點函數(shù),那肯定也要有取點函數(shù)。取點函數(shù),實際上就是讀取顯存的某個字節(jié)某個位的狀態(tài)。取點函數(shù)的實現(xiàn)如下所示:
/**************************************************************************************************
*@函數(shù) OLED_GetPixel
*
*@簡述 獲取坐標(biāo)像素點狀態(tài)
*
*@輸入?yún)?shù) x - x坐標(biāo)
* y - y坐標(biāo)
*
*@參數(shù) 無
*
*輸出參數(shù)
*
*無
*
*@返回 PixelValue - 像素點狀態(tài)
**************************************************************************************************
*/
int32 OLED_GetPixel(int32 x, int32 y)
{
int32 pos,bx,temp=0;
pos = y/8;//計算y坐標(biāo)所在頁
bx = y%8;//計算位偏移
temp = 1<<bx;
if(OLED_DISPLAY[pos][x] & temp)
{
return 1;
}
else
{
return 0;
}
}
接下來是刷新函數(shù),刷新函數(shù)是將顯存的數(shù)據(jù)傳輸?shù)絆LED中,以便實現(xiàn)OLED畫面的更新。刷新函數(shù)和實現(xiàn)如下所示:
/**************************************************************************************************
*@函數(shù) OLED_Refresh_Display
*
*@簡述 OLED更新顯示
*
*@輸入?yún)?shù)
*
*@參數(shù) 無
*
*輸出參數(shù)
*
*無
*
*@返回 無
**************************************************************************************************
*/
void OLED_Refresh_Display(void)
{
uint8 *value;
value = (uint8*)OLED_DISPLAY;//二級指針轉(zhuǎn)為一級指針
OLED_WR_Byte (0xb0,OLED_CMD);//開始頁:0
OLED_WR_Byte (0x00,OLED_CMD); //開始列低地址為0
OLED_WR_Byte (0x10,OLED_CMD); //開始列高地址為0
OLED_WR_Bytes(value,1024);
}
先設(shè)定好起始頁和起始列地址,然后一次性將1024個字節(jié)寫入到OLED中。由于OLED_DISPLAY為二維指針,需要強制轉(zhuǎn)成一維指針才能傳入OLED_WR_Bytes。接下來是畫線函數(shù),這里的畫線函數(shù)涉及到了計算機圖形學(xué)的內(nèi)容,采用Bresenham直線算法思想。畫線函數(shù)的實現(xiàn)如下:
/**************************************************************************************************
*@函數(shù) OLED_DrawLine
*
*@簡述 OLED畫一條線
*
*@輸入?yún)?shù) iStartX - 起點x坐標(biāo)
* iStartY - 起點y坐標(biāo)
* iEndX - 終點x坐標(biāo)
* iEndY - 終點y坐標(biāo)
* fill - 填充(0:不填充,1:填充)
*
*@參數(shù) 無
*
*輸出參數(shù)
*
*無
*
*@返回 無
**************************************************************************************************
*/
void OLED_DrawLine(int16 iStartX, int16 iStartY, int16 iEndX, int16 iEndY, int16 fill)
{
/*----------------------------------*/
/* Variable Declaration */
/*----------------------------------*/
int16 iDx, iDy;
int16 iIncX, iIncY;
int16 iErrX = 0, iErrY = 0;
int16 iDs;
int16 iCurrentPosX, iCurrentPosY;
/*----------------------------------*/
/* Initialize */
/*----------------------------------*/
iErrX = 0;
iErrY = 0;
iDx = iEndX - iStartX; //X軸差值
iDy = iEndY - iStartY; //Y軸差值
iCurrentPosX = iStartX;
iCurrentPosY = iStartY;
if(iDx > 0) //X軸差值大于0
{
iIncX = 1;
}
else
{
if(iDx == 0) //X軸差值等于0
{
iIncX = 0;
}
else //X軸差值小于0
{
iIncX = -1;
iDx = -iDx; //iDx取反
}
}
if(iDy > 0) //Y軸差值大于0
{
iIncY = 1;
}
else
{
if(iDy == 0) //Y軸差值等于0
{
iIncY = 0;
}
else //Y軸差值小于0
{
iIncY = -1;
iDy = -iDy;
}
}
if(iDx > iDy) //斜率小于45°
{
iDs = iDx;
}
else //斜率大于等于45°
{
iDs = iDy;
}
/*----------------------------------*/
/* Process */
/*----------------------------------*/
for(uint8 i = 0; i <= iDs+1; i++)
{
OLED_SetPixel(iCurrentPosX,iCurrentPosY, fill);//當(dāng)前位置畫點
iErrX += iDx; //X軸偏移
if(iErrX > iDs)
{
iErrX -= iDs;
iCurrentPosX += iIncX;
}
iErrY += iDy; //Y軸偏移
if(iErrY > iDs)
{
iErrY -= iDs;
iCurrentPosY += iIncY;
}
}
}
Bresenham直線算法實現(xiàn)直線的繪制只用到了簡單的加法運算,計算機可以快速生成直線。這里不花篇幅講解Bresenham直線算法,感興趣的可以百度查詢。OLED_DrawLine函數(shù)只要傳入起點、終點和填充狀態(tài)就可以繪制一條實線或者空線。鐘表盤指針的顯現(xiàn)和消除就可以用這個函來實現(xiàn)。 接下來是畫圓函數(shù),畫圓函數(shù)是基于中點畫圓法思想實現(xiàn)的,畫圓函數(shù)實現(xiàn)如下所示:
/**************************************************************************************************
*@函數(shù) OLED_DrawCircle
*
*@簡述 OLED畫圓
*
*@輸入?yún)?shù) uiCx - 圓心x坐標(biāo)
* uiCy - 圓心y坐標(biāo)
* uiRadius - 半徑
* eEdgeColor - 邊緣填充(0:不填充,1:填充)
* eFillColor - 圓內(nèi)區(qū)域填充(0:不填充,1:填充)
*
*@參數(shù) 無
*
*輸出參數(shù)
*
*無
*
*@返回 無
**************************************************************************************************
*/
void OLED_DrawCircle(uint8 uiCx, uint8 uiCy, uint8 uiRadius, uint8 eEdgeColor, uint8 eFillColor)
{
/*----------------------------------*/
/* Variable Declaration */
/*----------------------------------*/
uint8 uiPosXOffset, uiPosYOffset;
int16 uiPosXOffset_Old, uiPosYOffset_Old;
int16 iXChange, iYChange, iRadiusError;
/*----------------------------------*/
/* Initialize */
/*----------------------------------*/
uiPosXOffset = uiRadius;
uiPosYOffset = 0;
uiPosXOffset_Old = 0xFFFF;
uiPosYOffset_Old = 0xFFFF;
iXChange = 1 - 2 * uiRadius;
iYChange = 1;
iRadiusError = 0;
/*----------------------------------*/
/* Process */
/*----------------------------------*/
if(uiRadius < 1) //半徑小于1
{
OLED_SetPixel(uiCx, uiCy, eEdgeColor);
}
else
{
while(uiPosXOffset >= uiPosYOffset)
{
if((uiPosXOffset_Old != uiPosXOffset) || (uiPosYOffset_Old != uiPosYOffset) )
{
// Fill the circle 填充圈圈
if((uiRadius > 1) && (eFillColor != 2) && (uiPosXOffset_Old != uiPosXOffset))
{
OLED_DrawLine(uiCx-uiPosXOffset, uiCy-uiPosYOffset+1, uiCx-uiPosXOffset, uiCy+uiPosYOffset-1, eFillColor);
OLED_DrawLine(uiCx+uiPosXOffset, uiCy-uiPosYOffset+1, uiCx+uiPosXOffset, uiCy+uiPosYOffset-1, eFillColor);
uiPosXOffset_Old = uiPosXOffset;
}
OLED_DrawLine(uiCx-uiPosYOffset, uiCy-uiPosXOffset+1, uiCx-uiPosYOffset, uiCy+uiPosXOffset-1, eFillColor);
OLED_DrawLine(uiCx+uiPosYOffset, uiCy-uiPosXOffset+1, uiCx+uiPosYOffset, uiCy+uiPosXOffset-1, eFillColor);
uiPosYOffset_Old = uiPosYOffset;
// Draw edge.
OLED_SetPixel(uiCx+uiPosXOffset, uiCy+uiPosYOffset, eEdgeColor);
OLED_SetPixel(uiCx-uiPosXOffset, uiCy+uiPosYOffset, eEdgeColor);
OLED_SetPixel(uiCx-uiPosXOffset, uiCy-uiPosYOffset, eEdgeColor);
OLED_SetPixel(uiCx+uiPosXOffset, uiCy-uiPosYOffset, eEdgeColor);
OLED_SetPixel(uiCx+uiPosYOffset, uiCy+uiPosXOffset, eEdgeColor);
OLED_SetPixel(uiCx-uiPosYOffset, uiCy+uiPosXOffset, eEdgeColor);
OLED_SetPixel(uiCx-uiPosYOffset, uiCy-uiPosXOffset, eEdgeColor);
OLED_SetPixel(uiCx+uiPosYOffset, uiCy-uiPosXOffset, eEdgeColor);
}
uiPosYOffset++;
iRadiusError += iYChange;
iYChange += 2;
if ((2 * iRadiusError + iXChange) > 0)
{
uiPosXOffset--;
iRadiusError += iXChange;
iXChange += 2;
}
}
}
}
這個畫圓函數(shù)也只是用到簡單的加法運算,不涉及到浮點運算,不依賴于<math.h>數(shù)學(xué)庫。除了可以畫圓圈的邊緣線,還可以填充圓形內(nèi)部區(qū)域?梢杂卯媹A函數(shù)繪制鐘表盤的外形。
上面介紹了OLED圖形驅(qū)動的幾個基本圖形操作函數(shù),接下來就利用上面的函數(shù)來制作電子鐘表盤。電子鐘表盤的制作難點是指針的位置處理,兩點可以確定一條直線,指針的一端是轉(zhuǎn)軸,另一端指向刻度點的方向。轉(zhuǎn)軸的坐標(biāo)是保持不變的,要實現(xiàn)指針的轉(zhuǎn)動,需要將指針的另一端指向一下一個刻度點的坐標(biāo)。鐘表有60個刻度點,只要找出60個刻度點對應(yīng)的坐標(biāo),就能實現(xiàn)指針的轉(zhuǎn)動。圓是上下左右對稱的,只要找出四分之一圓區(qū)域的15個刻度點,就不難算出其他45個刻度點,F(xiàn)在的問題是如何計算出這15個刻度點的坐標(biāo)?
很容易想到聯(lián)立直線方程和圓方向就可以交點計算出來。直線方程有15條,每條傾斜角度相差6°,直線斜率可以用tanθ表示,將查表后記錄到數(shù)組中方便查詢:
uint32 angle_tan[] = {
//0° 6° 12° 18° 24° 30° 36° 42° 48° 54° 60° 66° 72° 78° 84° 90°
0,1051,2126,3249,4452,5773,7265,9004,11106,13763,17320,22460,30776,47046,95143,0xffffffff
};//tanθ擴大1000倍
通過調(diào)用OLED_DrawCircle函數(shù),可以將圓形繪制出來,然后調(diào)用OLED_GetPixel函數(shù),遍歷顯存的數(shù)據(jù),可以把構(gòu)成圓形的像素點的坐標(biāo)找出來。只要將像素點的坐標(biāo)帶入到直線方程,就能確定哪個像素點可以作為刻度點。直線點斜式方程y=kx+b,為了消除b常量方便計算,將圓心定為坐標(biāo)原點。那么直線方程就是正比例函數(shù)y=kx,其中斜率k可用tanθ表示。尋找刻度點的實現(xiàn)如下所示:
/**************************************************************************************************
*@函數(shù) clock_calculate_coordinate
*
*@簡述 計算針軌跡右下部數(shù)字15-30的坐標(biāo)
*
*@輸入?yún)?shù) clock_hand - 類別:時針、分針、秒針
* length - 指針長度
*
*@參數(shù) 無
*
*@返回
**************************************************************************************************
*/
static void clock_calculate_coordinate(uint8 clock_hand,uint8 length)
{
uint8 x,y;
uint8 rad = 0;
uint16 *coordinate_array;
double value = 0;
double angel = 0;
double cal_x,cal_y;
double all_value;
OLED_Clear_Ram();//清顯存
OLED_DrawCircle(CIRCLE_X,CIRCLE_Y,length,1,0);//繪制軌跡
rad = length;
switch(clock_hand) //判斷指針類型
{
case SECOND_HAND: //秒針
coordinate_array = second_coordinate;
break;
case MINUTE_HAND: //分針
coordinate_array = minute_coordinate;
break;
case HOUR_HAND: //時針
coordinate_array = hour_coordinate;
break;
default:
break;
}
for(uint8 k = 0; k < 16; k++) //計算軌跡右下部15-30的坐標(biāo)
{
angel = angle_tan[k];
all_value = 977889999;
for(uint8 i = CIRCLE_X - 1; i <= CIRCLE_X + rad; i++)
{
for(uint8 j = CIRCLE_Y - 1; j <= CIRCLE_Y + rad; j++)
{
if(OLED_GetPixel(i,j)) //掃描針的軌跡
{
cal_x = i-CIRCLE_X;
cal_y = j-CIRCLE_Y;
value =cal_y*10000 - angel*cal_x;
if(value < 0)//負(fù)數(shù)處理
{
value = -value;
}
if(all_value - value> 0) //尋找最合適的坐標(biāo)
{
all_value = value;
x = i;
y = j;
}
}
}
}
coordinate_array[k] = y; //記錄y坐標(biāo)
coordinate_array[k] <<= 8;
coordinate_array[k] |= x; //記錄x坐標(biāo)
}
OLED_Clear_Ram();//清顯存
}
選擇計算數(shù)字15-30的坐標(biāo)的原因主要是這個區(qū)域坐標(biāo)x和坐標(biāo)y都是正數(shù),比較好處理。程序分別計算了不同長度的秒針、分針和時針的刻度點坐標(biāo),后面就可以根據(jù)刻度點來實現(xiàn)秒針、分針和時針的轉(zhuǎn)動。接下來是根據(jù)計算保持的刻度點坐標(biāo),計算出剩下45個刻度點坐標(biāo)。區(qū)域指圓形的四個扇形區(qū)域,鐘表上就是0-15、15-30、30-45、45-60這四個區(qū)域。對傳入的數(shù)字先做區(qū)域標(biāo)記,再轉(zhuǎn)成15-30之間的數(shù),這樣方便對應(yīng)刻度點坐標(biāo)。再根據(jù)區(qū)域,將刻度點坐標(biāo)做對稱換算即可。
下面動圖測試了一下秒針,可以看到可以360°旋轉(zhuǎn)。說明刻度點坐標(biāo)都在圓形邊沿。
圓形主要是顯示秒針跑的軌跡而特意繪制出來的,實際在計算出15個坐標(biāo)后,可以把圓形軌跡刪除。分針和時針也是類似的操作,只是軌跡是不同半徑的圓而已?梢酝ㄟ^修改如下定義的宏:
#define SECOND_HAND_LENGTH 28 //秒針長度
#define MINUTE_HAND_LENGTH 27 //分針長度
#define HOUR_HAND_LENGTH 18 //時針長度
再根據(jù)秒針、時針和分針的簡單關(guān)系,程序?qū)崿F(xiàn)如下:
/**************************************************************************************************
*@函數(shù) clock_show_time
*
*@簡述 表盤顯示時間函數(shù)
*
*@輸入?yún)?shù) time - 時間結(jié)構(gòu)體
* state - 狀態(tài)
*
*@參數(shù) 無
*
*輸出參數(shù) 無
*
*@返回
**************************************************************************************************
*/
void clock_show_time(clock_time time,uint8 state)
{
uint8 x,y;
if(time.hours > 12) //如果為24小時
{
time.hours -= 12;//轉(zhuǎn)為12小時制
}
if(state)
{
OLED_ShowChar(CIRCLE_X-5,2,'1',12); //數(shù)字1
OLED_ShowChar(CIRCLE_X,2,'2',12); //數(shù)字2
OLED_ShowChar(CIRCLE_X-2,50,'6',12);//數(shù)字6
OLED_ShowChar(CIRCLE_X+24,25,'3',12);//數(shù)字3
OLED_ShowChar(CIRCLE_X-29,25,'9',12);//數(shù)字9
OLED_DrawCircle(CIRCLE_X,CIRCLE_Y,2,1,1);//繪制轉(zhuǎn)軸
}
clock_get_coordinate(time.hours*5+time.minutes/12,HOUR_HAND,&x,&y);
OLED_DrawLine(CIRCLE_X,CIRCLE_Y,x,y,state); //繪制時針
clock_get_coordinate(time.minutes,MINUTE_HAND,&x,&y);
OLED_DrawLine(CIRCLE_X,CIRCLE_Y,x,y,state); //繪制分針
clock_get_coordinate(time.seconds,SECOND_HAND,&x,&y);
OLED_DrawLine(CIRCLE_X,CIRCLE_Y,x,y,state); //繪制秒針
}
clock_show_time可以根據(jù)所傳入的時間結(jié)構(gòu)體來繪制三條指針,參數(shù)state是繪制和擦除指針用的。下面是時鐘運行函數(shù):
/**************************************************************************************************
*@函數(shù) clock_run
*
*@簡述 運行表盤函數(shù)
*
*@輸入?yún)?shù) time - 時間結(jié)構(gòu)體
*
*@參數(shù) 無
*
*輸出參數(shù) 無
*
*@返回
**************************************************************************************************
*/
void clock_run(clock_time time)
{
clock_show_time(pre_time,0);//擦除前一次時間軌跡
clock_show_time(time,1);//顯示當(dāng)前時間
pre_time = time;//記錄最新時間
}
定義一個全局結(jié)構(gòu)體變量pre_time 來記錄上次時間。每次更新時間前,會先把上次的指針軌跡擦除,再繪制新的指針軌跡。下面的動圖是秒針、分針和時針的運行過程。
外圍有12個小點,分別代表數(shù)字1-數(shù)字12,其中4個小點被數(shù)字擋住了。要生成這12個小點,也是比較簡單。先將其看成指針的軌跡來生成計算坐標(biāo),再從60個坐標(biāo)中取出數(shù)字1-12數(shù)字這12個坐標(biāo)點,把12個坐標(biāo)點畫上,就可以了。效果如下圖所示:
鐘表盤的制作基本算完成了,再加上DS3231時鐘芯片,就可以實時顯示時間了。效果如下圖所示:
|
|