1、概述
自乐鑫公司的物联网芯片兴起后,诞生了较多的基于WiFi功能的作品,其中以基于WiFi的气象站作品居多,我们可以在网上找到较多的成品及源代码资源,但其源头只有一个,其作品链接及图片如下所示。
作品链接: 基于ESP8266的微型气象站
可以从上面图片观察到其OLED显示屏上的ASCII字符是非等宽的。我们可以通过下面两张图进行观察,第一张图片的每个字符的高度和宽度都是固定的,这样虽然很方便取模和编程,但实际上有些字符可以不需要这么多宽度(换句话说我们可以让两个字符之间的距离就为1个像素即可),比如下图的"i"这个字符,可以适当左移两个像素。这样如果字符多的话,挤一挤其实可以省出较多的空间,显示更多的字符,就如第二张图所示。
那么WiFi气象站的这种字体是如何实现的呢?
2、实现方式
我们可以看看其源程序,打开安装目录下的“Arduino\arduino-1.8.10\libraries\esp8266-oled-ssd1306-master\src”下的OLEDDisplayFonts.h文件,我们可以看到三个大型的数组,分别是ArialMT_Plain_10[]、ArialMT_Plain_16[]、ArialMT_Plain_24[],这也就对应其三种字体,而且除这些字符库还可以通过官方给定的网页定制一些其他的字符库代码文件。下图为其网页界面。
具体网址为:http://oleddisplay.squix.ch/。
它的结构和我们传统取模代码文件是不一样的,我会用一个小的类似的数组进行介绍,如下图所示。
const uint8_t ArialMT_Plain_10[] PROGMEM = {
0x0A, // Width: 10
0x0D, // Height: 13
0x20, // First Char: 32
0x03, // Numbers of Chars: 3
// Jump Table:
0xFF, 0xFF, 0x00, 0x03, // 32:65535
0x00, 0x00, 0x04, 0x03, // 33:0
0x00, 0x04, 0x05, 0x04, // 34:4
// Font Data:
0x00,0x00,0xF8,0x02, // 33
0x38,0x00,0x00,0x00,0x38, // 34
};
前四个字节分别表示:
0x0A --------->该种字符集的宽度(但每个字符实际显示的宽度只会小于或等于它)
0x0D --------->字符集的高度
0x20 ---------->字符集的第一个字符的ASCII值(此处是从ASCII表中的空格开始,空格的ASCII为32)
0x03 ---------->字符集的总共的字符个数(这里总共三个)
接下来给出的跳转表(Jump Table)很关键,前两个字节表示字符字模所在该数组的起始位置(具体计算为:(msbJumpToChar << 8) + lsbJumpToChar;若为0xFF,0xFF这表示无),第三个则表示字模所占的字节数,最后一个表示其要显示的宽度(这里才是程序最后真正要显示的宽度,也就说这种数据格式其实是记录每个字符字模的实际的宽度的)。我们以空格和数字2为例:
0xFF, 0xFF, 0x00, 0x03 (空格)
------>0xFF,0xFF表示没有其字模数据地址,也就是不存在(其实是0x00,我们默认OLED映射的RAM数据都为0x00,所以我们只需要先程序上跳过给定宽度即可),0x00表示该字模字节数为零,0x03表该字模宽度为3。
0x00, 0x04, 0x05, 0x04 (数字"2")
------>(0x00<<8 + 0x04) = 4,即从字模资源开始(从“// Font Data”开始),该字模资源的起始位于起始位+4,即 (字模段)(0x38,0x00,0x00,0x00,0x38)绿色这个0x38,而0x05则表示该字模端的字节数为5(可以自己拿这个表算一算),0x04则表示该字模宽度。
有了上述这些认知,其原理我们就明白了,其实它和传统的取模没有区别,只是它的数组里包含了这些字模宽度、字模字节数及该字模在数组里面的位置,而这些字模的高度固定,所以只需要读出上述数据,并把字模当做图片显示出来即可。
3、在SimpleGUI上移植
这个移植其实哪里都可以的(移植只要把OLEDDisplayFonts.h文件加入到我们的工程文件中,当然其实也可以直接移植气象站的整体UI代码。不过在这里我只移植它的字体库),只是近期我在码云上发现这个面向单色屏的GUI写的非常好,但是我又不是喜欢它自带的ASCII字符字模库,所以就在这上面该改了。
SimpleGUI地址:https://gitee.com/Polarix/simplegui
这个SimpleGUI的代码写的可以说是非常有质量的,用C语言实现了类似于C++的抽象接口,有关它的介绍大家可以看看作者写的文档(我这里也算给作者打一波广告了),在这里我就说说怎么把气象站的这种字体嵌到其GUI代码中去。
我们知道取模方式有四种:逐列式、逐行式、列行式、行列式。不巧的是这种天气气象站的取模方式用的是逐列式(作者用的是列行式)。所以这里需要修改一下SGUI_Basic_DrawBitMap这个函数(其实实现这些字符的显示就是画位图,所以需要先修改这个函数),原理很简单,一列一列的扫,扫完第一列后,地址跳跃一列所占的字节数,再进行下一列的扫描,直到结束(下面的这个代码看起来非常多,是因为其加了较多的限制条件,但是认真看看还是可以理解的)。当然为了方便四种方式我都写了,用四个条件编译(定义在SGUI_Config.h这个文件),大家只要打开逐列式即可(所有的代码我会在结束后贴出)。
void SGUI_Basic_DrawBitMap(SGUI_SCR_DEV* pstDeviceIF, SGUI_RECT* pstDisplayArea, SGUI_POINT* pstInnerPos, const SGUI_BMP_RES* pstBitmapData, SGUI_DRAW_MODE eDrawMode);
#ifdef _PER_COLUMN_TYPE_
// Set loop start parameter of x coordinate
iDrawPixX = RECT_X_START(*pstDisplayArea); //获取规定区域的x坐标
iBmpPixX = 0;
if(RECT_X_START(*pstInnerPos) > 0)
{
iDrawPixX += RECT_X_START(*pstInnerPos);
}
else
{
iBmpPixX -= RECT_X_START(*pstInnerPos);
}
uiDrawnWidthIndex = iBmpPixX;
// Loop for x coordinate;
while((uiDrawnWidthIndex<RECT_WIDTH(*pstBitmapData)) && (iDrawPixX<=RECT_X_END(*pstDisplayArea)) && (iDrawPixX<RECT_WIDTH(pstDeviceIF->stSize)))
{
// Redirect to data array for column.
pData = pstBitmapData->pData+ iBmpPixX * ((RECT_HEIGHT(*pstBitmapData) + 7) / 8);
// Set loop start parameter of y coordinate
iDrawPixY = RECT_Y_START(*pstDisplayArea);//获取规定区域的y坐标
iBmpPixY = 0;
if(RECT_Y_START(*pstInnerPos) > 0)
{
iDrawPixY += RECT_Y_START(*pstInnerPos);
}
else
{
iBmpPixY -= RECT_Y_START(*pstInnerPos);
}
uiDrawnHeightIndex = iBmpPixY;
uiPixIndex = iBmpPixY % 8;
pData += (iBmpPixY / 8); //所需跳跃的字节
// Loop for y coordinate;
while((uiDrawnHeightIndex<RECT_HEIGHT(*pstBitmapData)) && (iDrawPixY<=RECT_Y_END(*pstDisplayArea)) && (iDrawPixY<RECT_HEIGHT(pstDeviceIF->stSize)))
{
if(uiPixIndex == 8)
{
uiPixIndex = 0;
pData += 1;
//pData += RECT_WIDTH(*pstBitmapData); //列行式扫描
}
if(SGUI_GET_PAGE_BIT(*pData, uiPixIndex) != eDrawMode)
{
SGUI_Basic_DrawPoint(pstDeviceIF, iDrawPixX, iDrawPixY, SGUI_COLOR_FRGCLR);
}
else
{
SGUI_Basic_DrawPoint(pstDeviceIF, iDrawPixX, iDrawPixY, SGUI_COLOR_BKGCLR);
}
uiDrawnHeightIndex ++;
uiPixIndex ++;
iDrawPixY ++;
iBmpPixY ++;
}
uiDrawnWidthIndex ++;
iDrawPixX ++;
iBmpPixX ++;
}
#endif
当然这些还不够,我们主要需要修改的是SGUI_Text.c、SGUI_FontResource.c,主要编写获取该字符库字符字模位置、字模所占字节数及字模宽度的函数(同时还要修改作者本身编写的代码部分)。
为了获取字模数据,我们就有必要知道字模数据前有多少数据,我需要跳跃多少?才可以得到上面这些我需要的数据,比如我现在需要知道字符“R”(ASCII值82),我的第一个字符是“Space”(ASCII值32)。则偏移量为82-32=50,故字符“R”:
CurrentCharWidth(字符字模的宽度) = sStartAddr (字符库首地址)+ JUMPTABLE_START(跳跃前四个字节)+ Offest(偏移量) * JUMPTABLE_BYTES(固定为4) + JUMPTABLE_WIDTH(固定为3);
绿色部分是可以修改的,改成JUMPTABLE_SIZE时,我们就可以都出其字节数了。
而其字模数据位置的做法也就一样了,如下所示:
msbJumpToChar(高位) = sStartAddr(字符库首地址) + JUMPTABLE_START(跳跃前四个字节) + Offest(偏移量) * JUMPTABLE_BYTES(固定为4) ;
lsbJumpToChar (地位) = sStartAddr (字符库首地址)+ JUMPTABLE_START(跳跃前四个字节) + Offest (偏移量)* JUMPTABLE_BYTES(固定为4) + JUMPTABLE_LSB(固定为1);
sizeOfJumpTable = (sStartAddr + CHAR_NUM_POS(总的字符数)) * JUMPTABLE_BYTES;
charDataPosition = JUMPTABLE_START(跳跃前四个字节) + sizeOfJumpTable(跳跃跳转表) + ((msbJumpToChar << 8) + lsbJumpToChar);
// Header Values
#define JUMPTABLE_BYTES 4
#define JUMPTABLE_LSB 1
#define JUMPTABLE_SIZE 2
#define JUMPTABLE_WIDTH 3
#define JUMPTABLE_START 4
#define WIDTH_POS 0
#define HEIGHT_POS 1
#define FIRST_CHAR_POS 2
#define CHAR_NUM_POS 3
SGUI_INT SGUI_Resource_GetFontWidth_Default(SGUI_CBYTE* sStartAddr, SGUI_CHAR charCode)
{
SGUI_UINT8 charByteSize = 0;
SGUI_UINT8 CurrentCharWidth = 0;
SGUI_UINT8 firstChar = 0;
SGUI_UINT8 Offset = 0;
uint16_t sizeOfJumpTable = pgm_read_byte(sStartAddr + CHAR_NUM_POS) * JUMPTABLE_BYTES;
firstChar = pgm_read_byte( sStartAddr + FIRST_CHAR_POS); //读出第一个字符
Offset = charCode - firstChar;
CurrentCharWidth = pgm_read_byte( sStartAddr + JUMPTABLE_START + Offset * JUMPTABLE_BYTES + JUMPTABLE_WIDTH); // Width
return CurrentCharWidth;
}
有了这些数据,接下来的工作就非常简单了,我们只要把这些字模数据搬到OLED映射的缓冲区就可以,做法也非常简单,就是一个一个字符来,通过SGUI_FONT_RES_DATA_Default将传进来pDataBuffer地址所在位置传入字模数据即可。需要注意的是这个pDataBuffer一个简单的临时的缓存区地址,每一次把值传进去后,若上次字符字节长度比它长,那么将会导致本次的字模的数据多那么一些字节,而这些是完全不需要的,所以每次进来前是需要清零的,这点不要遗漏。
SGUI_SIZE SGUI_FONT_RES_DATA_Default(SGUI_CHAR charCode, SGUI_CBYTE* sStartAddr, SGUI_BYTE* pDataBuffer, SGUI_SIZE sReadSize)
{
const SGUI_BYTE* pSrc;
SGUI_BYTE* pDest = pDataBuffer;
SGUI_SIZE sReadCount = 0;
SGUI_UINT8 msbJumpToChar = 0;
SGUI_UINT8 lsbJumpToChar = 0;
SGUI_UINT16 charDataPosition = 0;
uint16_t sizeOfJumpTable = pgm_read_byte(sStartAddr + CHAR_NUM_POS) * JUMPTABLE_BYTES;
SGUI_CHAR i;
msbJumpToChar = pgm_read_byte( sStartAddr + JUMPTABLE_START + charCode * JUMPTABLE_BYTES ); // MSB \ JumpAddress
lsbJumpToChar = pgm_read_byte( sStartAddr + JUMPTABLE_START + charCode * JUMPTABLE_BYTES + JUMPTABLE_LSB); // LSB /
//字符表中最大长度为92,故设置为100
for (i=0; i<100; i++)
{
*(pDest + i) = 0x00;//清除上一次缓冲区字符
}
pDest = pDataBuffer;
if (!(msbJumpToChar == 255 && lsbJumpToChar == 255) && NULL != pDataBuffer)
{
charDataPosition = JUMPTABLE_START + sizeOfJumpTable + ((msbJumpToChar << 8) + lsbJumpToChar);
pSrc = sStartAddr + charDataPosition;
for(sReadCount=0; sReadCount<sReadSize; sReadCount++)
{
*pDest++ = *pSrc++;
}
}
return sReadCount;
}
最后我们来看看用这个GUI和字库制作的几个简单的效果:
最后,修改部分可能不止上面这些,大家可以结合程序看一下(不需要下载币的)
测试源码:https://download.csdn.net/download/Eaglewzw/12666525