以下文章来源于嵌入式小书虫 ,作者FledgingSu 支离苏
玩过Arm-2D的同学都知道,官方的lcd_printf函数只提供了一种6 * 8点阵的字库,而且是不支持显示汉字的。想想也是,Arm-2D是给小资源单片机提供显示驱动的,一个汉字字库需要的flash少则也得几十上百KB。
可是,我们又有显示汉字的需求怎么办?
贴图片好像也很占空间,
有没有又能省空间又可以显示汉字的方法呢?
哈哈哈,这种两全其美的事应该没有,
不过,
如果我们只是显示几个特定的汉字而不是整个字库的话倒是可以,大家应该已经想到了,那就是把这几个特定的汉字做成字库,然后调用lcd_printf函数显示。为了和官方的函数名有所区别,我们就起名为 My_lcd_printf。
以后就可以这样显示汉字了,如下所示
My_lcd_text_location( 16, 2);
My_lcd_printf("嵌入式%s","小书虫");
- 我们的字库虽然小,但是My_lcd_printf函数也可以实现 %s 的功能。
汉字字库制作
那我们先开始制作字库。字符取模软件的使用我就不讲了,网上有很多。字符取模可以分为横向取模、竖向取模等,为了和官方保持一致,我们采用横向取模。
好,我们讲一下怎么把一个汉字转换成十六进制数组,如下图
横向取模原理如下,
- 这样一行一行的转换,最后保存到数组中就是字库了。
用子tile表示资源
接下来讲一下待会要用到的Arm-2D的知识,那就是上一篇我们没有讲到的子Tile的另一种用法,资源也可以用子tile来建立(即取一个大图的局部作为新的资源)
如下图所示:
- 父Tile和子Tile都可以当做是图片资源使用,但是他们是共用同一张图片资源(节省空间)。
使用方法如下所示:
// 图片资源
extern const uint8_t c_bmpColours[];
// 父Tile,拥有完整的图片资源
const arm_2d_tile_t c_tileParent = {
.tRegion = {
.tSize = {
.iWidth = 100,
.iHeight = 100,
},
},
.tInfo.bIsRoot = true,
.phwBuffer = (uint16_t *)c_bmpColours,
};
// 子Tile,拥有父Tile的部分资源
const arm_2d_tile_t c_tileChild = {
.tRegion = {
.tLocation = {
.iX = 10,
.iY = 20,
},
.tSize = {
.iWidth = 30,
.iHeight = 30,
},
},
.tInfo = {
.bIsRoot = false,
.bDerivedResource = true,
.bHasEnforcedColour = true,
.tColourInfo = {
.chScheme = ARM_2D_COLOUR_16BIT,
},
},
.ptParent= (arm_2d_tile_t *)&c_tileParent ,
};
- 父Tile和子Tile共用同一张图片资源
c_bmpColours[]
- 此时子Tile的bDerivedResource属性必须为true
bHasEnforcedColour
属性也设置为true,且和父Tile的颜色信息要一致。
上面这个资源子贴图(Tile)和我们要显示汉字有什么关系呢?
我们又不显示图片......
hahaha
这就是Arm-2D的设计思想,它把所有的资源都看成是Tile,字库也是一个Tile,而且是一个大Tile,而里边的一个字符就是他的一个子Tile,所以会用到上面讲到的子Tile用法。
如下图:
- 有了这个Tile思想,还可以把一个汉字拆成多个。
用Arm-2D显示汉字程序实现
好了,那我们现在就去实现我们的My_lcd_printf函数,如下
int My_lcd_printf(const char *format, ...){
int real_size;
static char s_chBuffer[MAX(((GLCD_WIDTH/6)+1), 54)];
__va_list ap;
va_start(ap, format);
real_size = vsnprintf(s_chBuffer, sizeof(s_chBuffer)-1, format, ap);
va_end(ap);
real_size = MIN(sizeof(s_chBuffer)-1, real_size);
s_chBuffer[real_size] = '\0';
// 显示字符串
lcd_puts_chinese(s_chBuffer);
return real_size;
}
- 是不是看到了我们熟悉的C语言可变长参数使用的宏(
__va_list
、va_start
、va_end
)
还不知道他们怎么用的请看下面这篇文章
C语言-学习笔记(三五)可变长参数,printf函数是如何实现的
- 把格式化好的字符串放到了
s_chBuffer
里,然后调用lcd_puts_chinese
函数。
那接下来我们实现lcd_puts_chinese
函数,如下
void lcd_puts_chinese(const char *str){
while(*str) {
// 计算字符在屏幕中显示的坐标
int16_t iX = s_tLCDTextControl_su.tTextLocation.chX * GLCD_Font_16x16.width;
//int16_t iY = s_tLCDTextControl.tTextLocation.chY * GLCD_Font_16x16.height;
// 绘制一个汉字
mylcd_draw_char( s_tLCDTextControl_su.tRegion.tLocation.iX + iX,
s_tLCDTextControl_su.tRegion.tLocation.iY,
get_chinese_index(str));
// 右移一个字符的坐标,方便下个汉字显示
s_tLCDTextControl_su.tTextLocation.chX++;
// 一个汉字占两字节,所以要加2
str+=2;
}
}
uint8_t get_chinese_index(const char* chChar){
uint8_t num = 0;
if(0 == strncmp(chChar,"嵌",2)){
//num = 0;
}else if(0 == strncmp(chChar,"入",2)){
num = 1;
}else if(0 == strncmp(chChar,"式",2)){
num = 2;
}else if(0 == strncmp(chChar,"小",2)){
num = 3;
}else if(0 == strncmp(chChar,"书",2)){
num = 4;
}else if(0 == strncmp(chChar,"虫",2)){
num = 5;
}
return num;
}
- 在此函数中计算字符在屏幕中显示的坐标(iX,iY),然后传人
mylcd_draw_char
函数中。 - 注意我们多了一个函数
get_chinese_index
,因为我们的汉字字库只有几个字而不是整个汉字字库,所以获取一个汉字在字库中的位置需要自己实现,正常整个汉字字库编码是按一定顺序排放的,下标很容易计算。
最后实现我们的mylcd_draw_char函数,如下
static void mylcd_draw_char(int16_t iX, int16_t iY, char chChar)
{ // 显示到默认缓冲区
//! use default frame buffer
arm_2d_tile_t *ptFrameBuffer = (arm_2d_tile_t *) -1;
// 父Tile,汉字字库资源
const static arm_2d_tile_t s_tileFont16x16 = {
.tRegion = {
.tSize = {
.iWidth = 16,
.iHeight = 16 * 9,
},
},
.tInfo = {
.bIsRoot = true,
.bHasEnforcedColour = true,
.tColourInfo = {
.chScheme = ARM_2D_COLOUR_BIN,
},
},
.pchBuffer = (uint8_t *)Font_16x16_h,
};
// 子Tile,要显示的汉字
static arm_2d_tile_t s_tileChar = {
.tRegion = {
.tSize = {
.iWidth = 16,
.iHeight = 16,
},
},
.tInfo = {
.bIsRoot = false,
.bDerivedResource = true,
.bHasEnforcedColour = true,
.tColourInfo = {
.chScheme = ARM_2D_COLOUR_BIN,
},
},
.ptParent = (arm_2d_tile_t *)&s_tileFont16x16,
};
s_tileChar.tRegion.tLocation.iY = chChar * 16;
// 在屏幕中显示的区域
arm_2d_region_t tDrawRegion = {
.tLocation = {.iX = iX, .iY = iY},
.tSize = s_tileChar.tRegion.tSize,
};
// 绘制汉字
arm_2d_rgb16_draw_pattern( &s_tileChar,
ptFrameBuffer,
&tDrawRegion,
ARM_2D_DRW_PATN_MODE_COPY
//| ARM_2D_DRW_PATN_MODE_NO_FG_COLOR
//| ARM_2D_DRW_PATN_MODE_WITH_BG_COLOR
//| ARM_2D_DRW_PATH_MODE_COMP_FG_COLOUR
,
GLCD_COLOR_GREEN,
GLCD_COLOR_BLACK);
}
- 是不是看到了熟悉的
arm_2d_tile_t
类型了 - 首先找到了父Tile(
s_tileFont16x16
),它是整个字库资源,字库数组为pchBuffer
指向的Font_16x16_h
s_tileChar
就是我们今天的主角,要显示的字符子Tile,tColourInfo
颜色信息和父Tile保持一致。- 最后调用
arm_2d_rgb16_draw_pattern
接口绘制一个字符
程序中用到的结构体和字库我也贴到下边,如下
||设置汉字显示坐标
void My_lcd_text_location(uint8_t chY, uint8_t chX)
{
s_tLCDTextControl_su.tTextLocation.chX = 0;
s_tLCDTextControl_su.tTextLocation.chY = 0;
s_tLCDTextControl_su.tRegion.tLocation.iX = chX;
s_tLCDTextControl_su.tRegion.tLocation.iY = chY;
}
// 16*16的汉字字库
uint8_t Font_16x16_h[]={
0x80,0x00,0x84,0x10,0x84,0x10,0xFC,0x1F,0x00,0x04,0x44,0x04,0x44,0x7C,0xFF,0x42,//0;
0x44,0x29,0x44,0x08,0x7C,0x08,0x44,0x08,0x44,0x14,0x7C,0x14,0x44,0x22,0x00,0x41,//0;"嵌",0
0x20,0x00,0x40,0x00,0x80,0x00,0x80,0x00,0x80,0x00,0x40,0x01,0x40,0x01,0x40,0x01,//0;
0x20,0x02,0x20,0x02,0x10,0x04,0x10,0x04,0x08,0x08,0x04,0x08,0x02,0x10,0x01,0x60,//0;"入",1
0x00,0x12,0x00,0x22,0x00,0x22,0x00,0x02,0xFF,0x7F,0x00,0x02,0x00,0x02,0x7C,0x02,//0;
0x10,0x02,0x10,0x02,0x10,0x04,0x10,0x44,0xF0,0x48,0x1E,0x50,0x04,0x60,0x00,0x40,//0;"式",2
0x80,0x00,0x80,0x00,0x80,0x00,0x80,0x00,0x80,0x00,0x88,0x08,0x88,0x10,0x88,0x20,//0;
0x84,0x20,0x84,0x40,0x82,0x40,0x81,0x40,0x80,0x00,0x80,0x00,0xA0,0x00,0x40,0x00,//0;"小",3
0x40,0x04,0x40,0x08,0x40,0x10,0xFC,0x07,0x40,0x04,0x40,0x04,0x40,0x04,0x40,0x04,//0;
0xFF,0x3F,0x40,0x20,0x40,0x20,0x40,0x20,0x40,0x20,0x40,0x14,0x40,0x08,0x40,0x00,//0;"书",4
0x80,0x00,0x80,0x00,0x80,0x00,0xFC,0x1F,0x84,0x10,0x84,0x10,0x84,0x10,0x84,0x10,//0;
0xFC,0x1F,0x84,0x10,0x80,0x00,0x80,0x08,0x80,0x10,0x80,0x3F,0x7E,0x20,0x00,0x00,//0;"虫",5
0x00,0x00,0x00,0x00,0xBF,0x3F,0x08,0x00,0x08,0x00,0x08,0x00,0xC8,0x7F,0x3E,0x09,//0;
};
// 保存字库信息的结构体
/*******************************************
typedef struct _GLCD_FONT {
uint16_t width; ///< Character width
uint16_t height; ///< Character height
uint32_t offset; ///< Character offset
uint32_t count; ///< Character count
const uint8_t *bitmap; ///< Characters bitmaps
} const GLCD_FONT;
***********************************************/
GLCD_FONT GLCD_Font_16x16 = {
16, ///< Character width
16, ///< Character height
32, ///< Character offset
9, ///< Character count
Font_16x16_h ///< Characters bitmaps
};
// 保存和设置显示区域的结构体
static struct {
arm_2d_region_t tRegion;
struct {
uint8_t chX;
uint8_t chY;
} tTextLocation;
} s_tLCDTextControl_su = {
.tRegion = {
.tSize = {
.iWidth = GLCD_WIDTH,
.iHeight = GLCD_HEIGHT,
},
},
.tTextLocation={
.chX = 0,
.chY =0,
},
};
小结
lcd_puts_chinese
函数里计算字符显示坐标
mylcd_draw_char
函数里设置字库资源和要显示的字符资源(在这里设置我们的字库)
调用arm_2d_rgb16_draw_pattern
接口绘制一个字符(可以修改字体颜色)
GLCD_FONT
结构体很重要,修改字库就是在这个类型的变量中修改
下面我用Arm-2D + 汉字字库制作了一个简单的小虫吃草的小游戏,视频演示如下
补充
我们现在去看看Arm-2D的lcd_puts函数是怎么实现的吧,顺便也和arm公司的工程师学些编程的技巧,(* ̄︶ ̄)
lcd_puts函数,如下
void lcd_puts(const char *str)
{
while(*str) {
||处理特殊字符
if (*str == '\r') {
s_tLCDTextControl.tTextLocation.chX = 0;
} else if (*str == '\n') {
s_tLCDTextControl.tTextLocation.chX = 0;
s_tLCDTextControl.tTextLocation.chY++;
} else if (*str == '\t') {
s_tLCDTextControl.tTextLocation.chX += 8;
s_tLCDTextControl.tTextLocation.chX &= ~(_BV(3)-1);
if ( s_tLCDTextControl.tTextLocation.chX * GLCD_Font_6x8.width
>= s_tLCDTextControl.tRegion.tSize.iWidth ) {
s_tLCDTextControl.tTextLocation.chX = 0;
s_tLCDTextControl.tTextLocation.chY++;
}
}else if (*str == '\b') {
if (s_tLCDTextControl.tTextLocation.chX) {
s_tLCDTextControl.tTextLocation.chX--;
}
} else {
||计算字符显示坐标
int16_t iX = s_tLCDTextControl.tTextLocation.chX * GLCD_Font_6x8.width;
int16_t iY = s_tLCDTextControl.tTextLocation.chY * GLCD_Font_6x8.height;
||显示一个字符
lcd_draw_char( s_tLCDTextControl.tRegion.tLocation.iX + iX,
iY,
*str);
||右移一个字符的坐标,方便下个字符显示
s_tLCDTextControl.tTextLocation.chX++;
||判断是否显示到屏幕外面
if ( s_tLCDTextControl.tTextLocation.chX * GLCD_Font_6x8.width
>= s_tLCDTextControl.tRegion.tSize.iWidth ) {
s_tLCDTextControl.tTextLocation.chX = 0;
||显示到一行的末尾后换行
s_tLCDTextControl.tTextLocation.chY++;
if ( s_tLCDTextControl.tTextLocation.chY * GLCD_Font_6x8.height
>= s_tLCDTextControl.tRegion.tSize.iHeight) {
s_tLCDTextControl.tTextLocation.chY = 0;
}
}
}
||显示完一个字符,指向下一个字符
str++;
}
}
- 这个函数比我们的长多了,不过最主要的还是调用
lcd_draw_char
函数来显示一个字符 - 是不是看到了优秀的代码了,人家显示到一行的末尾还可以自动换行,这样是不是可以简单实现一个txt的电子书阅读器了,(* ̄︶ ̄)
- 更多精彩还需自己阅读源码哈,源码之下无秘密。
备注:本程序由官方的lcd_printf函数修改而来,感兴趣的也可以去看看官方的代码,地址如下:
官方Arm-2D开源地址如下:
https://github.com/ARM-software/EndpointAI
参考文件《lcd_printf.c》
首发:裸机思维
作者:FledgingSu 支离苏
专栏推荐文章
如果你喜欢我的思维,欢迎订阅裸机思维