大家好,我是程序員小哈。
問題由來
前兩天有網友留言,能否做一個顯示波形的實例,之前也有人提過類似問題,那么今天我們就來安排一下。
問題分析
我們在網上經常能看到一些大佬用0.96寸OLED制作的迷你示波器。
制作這個mini示波器,界面中的曲線繪制是一個難點。
小哈哥的主要工作是做上位機VC++開發(fā)的,由于要做譜圖顯示,所以也用到了曲線的繪制,下圖中就是使用VC++進行曲線繪制的部分代碼,我們可以看出,曲線是由一系列首尾相連的直線組成,所以要想繪制曲線,首先要實現(xiàn)移動到線條起點
的函數(shù)MoveTo,以及實現(xiàn)畫線
的函數(shù)LineTo。
因為直線(其實說成線段更好)是由多個點組成,所以我們要實現(xiàn)畫線的函數(shù),只要實現(xiàn)畫點的函數(shù)即可,然后在計算出來的位置依次畫點,即可實現(xiàn)直線的繪制。
實現(xiàn)目標
- 實現(xiàn)畫點函數(shù)封裝畫點函數(shù),進而實現(xiàn)畫線函數(shù)繪制一個三角形
顯示原理
OLED的核心驅動芯片是SSD1306,單片機與SSD1306通信,SSD1306再驅動OLED點亮對應的OLED像素點。
要想實現(xiàn)繪制三角形,我們就要先實現(xiàn)畫點和繪制直線的函數(shù),在這之前,我們先了解一下OLED的顯示原理。
OLED的構造
OLED由128*64個像素組成,64行和128列。
圖中每個晶格表示一位圖像數(shù)據(jù),這些像素點對應SSD1306內部的一個GDDRAM數(shù)據(jù)內存,它有128*8字節(jié),即128*64bit,每一個位對應一個像素點。
其中,每8行組成一個PAGE,該OLED一共分為8個PAGE(PAGE0~PAGE7)。
我們控制顯示的內容,只需要控制SSD1306的內部GDDRAM即可。下面是封裝的刷新GDDRAM的函數(shù),其中 unsigned char OLED_GRAM[128][8];
中緩存的就是待顯示的內容,我們先將要顯示的內容賦值給這個數(shù)組,然后將這個數(shù)組整體寫入GDDRAM即可,如果這個數(shù)組內的數(shù)據(jù)都為0,則相當于將顯示屏清屏(不顯示內容)。
void OLED_Refresh_Gram(void)
{
unsigned char i,n;
for(i=0;i<8;i++)
{
OLED_WR_Byte(0xb0+i,OLED_CMD); //設置頁地址(0~7)
OLED_WR_Byte(0x00,OLED_CMD); //設置顯示位置—列低地址
OLED_WR_Byte(0x10,OLED_CMD); //設置顯示位置—列高地址
for(n=0;n<128;n++) //寫一PAGE的GDDRAM數(shù)據(jù)
{
OLED_WR_Byte(OLED_GRAM[n][i],1);
}
}
}
畫點函數(shù)
由于 OLED_WR_Byte(OLED_GRAM[n][i],1);
函數(shù)一次操作一個字節(jié),所以我們不能一次控制一個像素點,只能8個像素點一起控制;而且是垂直方向掃描控制;如下圖所示。因此垂直方向坐標可選為0~7;(8*8=64);水平方向可選坐標0~127。
我們封裝的畫點函數(shù),即隨便給一個點的坐標(x,y),我們要計算出,這個像素點所屬的PAGE,然后看控制的是這列8個像素(對應一個字節(jié)數(shù)據(jù))中的哪一個(對應1 bit數(shù)據(jù))。
void OLED_DrawDot(unsigned char x,unsigned char y,unsigned char t)
{
unsigned char pos,bx,temp=0;
// 此OLED的分辨率為128*64,橫坐標大于127,縱坐標大于63,則參數(shù)非法
if(x>127||y>63) return;
// 因為此OLED是按頁顯示,每頁8個像素,所以/8用于計算待顯示的點在哪頁中
pos=(y)/8;
// 一列中有8個像素,所以計算一下待顯示的點,在當前列中的第幾個點
bx=y%8;
// 移位,讓temp的第bx位為1
temp=1<<(bx);
if(t)
OLED_GRAM[x][pos]|=temp; //第bx位,置1,其他位值不變
else
OLED_GRAM[x][pos]&=~temp; //第bx位,置0,其他位值不變
// 刷新整個液晶屏
OLED_Refresh_Gram();
}
參數(shù)說明:
- x:顯示的橫坐標,即一行128個像素中的哪一個像素點y:顯示的縱坐標,即一列64個像素中的哪一個像素點t:0表示該像素不顯示,1表示該像素顯示
畫線函數(shù)
畫點的函數(shù)我們已經實現(xiàn)了,那么要想畫一條直線,我們就要計算出直線上都有哪些點,將直線上的點依次用畫點函數(shù)繪制出來,即完成了直線的繪制。
那怎么來求得直線上的任意點的坐標呢?如下圖所示,一般繪制一個直線都會給兩個已知點(x1,y1),(x2,y2),有了這兩個點的坐標,我們就可以求出這條直線的斜率,然后根據(jù)這個斜率和橫軸的范圍(x1,x2),依次代入直線方程,即可求出所有直線上的點的坐標。
我們舉個栗子,加深一下理解:
如果(x1=32,y1=48),(x2=96,y2=16),那么:
曲線斜率:K =(16-48)/(96-32)= -0.5
==>該曲線上任意點坐標關系為:y = 48+K*(x – 32)
如果x=64, 因為 K=-0.5,所以 y=48-0.5*(64-32)=32 。
所以,(x=64,y=32)。
利用斜率法,我們封裝畫線函數(shù)如下:
void OLED_DrawLine(unsigned int x1, unsigned int y1, unsigned int x2,unsigned int y2)
{
unsigned int t;
int offset_x,offset_y;
int incx,incy,uRow,uCol;
float K = 0.0f;
offset_x=x2-x1;
offset_y=y2-y1;
uRow=x1;
uCol=y1;
if(offset_x>0)
incx=1;
else if(offset_x==0)
incx=0; //垂直線
else
{
incx=-1;
offset_x=-offset_x;
}
if(offset_y>0)
incy=1;
else if(offset_y==0)
incy=0; //水平線
else
{
incy=-1;
offset_y=-offset_y;
}
if(incx==0)
{
for(t=0;t<=offset_y+1;t++ )
{
OLED_DrawDot(uRow,uCol+t*incy,1);
}
}
else if(incy==0)
{
for(t=0;t<=offset_x+1;t++ )
{
OLED_DrawDot(uRow+t*incx,uCol,1);
}
}
else
{
K = (float)(((float)y2-(float)y1)*1.000/((float)x2-(float)x1));
printf("K=%.3frn",K);
for(t=0;t<=offset_x+1;t++ )
{
printf("X=%d,Y=%drn",uRow+t,(u8)(uCol+t*K));
OLED_DrawDot(uRow+t,(u8)(uCol+t*K),1);
}
}
}
注意:因為水平線和垂直線比較特殊,所以上面函數(shù)中對這兩種情況進行了單獨的繪制,沒有使用斜率法計算直線上的坐標。
結果展示
我們按如下坐標繪制一個三角形:
有了畫線函數(shù),我們只要將上面三個點的坐標依次代入畫線函數(shù)即可,繪制三角形的代碼具體如下所示:
OLED_DrawLine(32, 48, 96,16);
OLED_DrawLine(96, 16, 96,48);
OLED_DrawLine(96, 48, 32,48);
編譯代碼生成結果如下: