嵌入式系统与单片机|技术阅读
登录|注册

您现在的位置是:嵌入式系统与单片机 > 技术阅读 > 【玩转Arm-2D】三、资源贴图(Child Tile)和汉字显示

【玩转Arm-2D】三、资源贴图(Child Tile)和汉字显示

玩过Arm-2D的同学都知道,官方的lcd_printf函数只提供了一种6*8点阵的字库,而且是不支持显示汉字的。想想也是,Arm-2D是给小资源单片机提供显示驱动的,一个汉字字库需要的flash少则也得几十上百KB。

可是,我们又有显示汉字的需求怎么办?

贴图片好像也很占空间,

有没有又能省空间又可以显示汉字的方法呢?

哈哈哈,这种两全其美的事应该没有,

不过,

如果我们只是显示几个特定的汉字而不是整个字库的话倒是可以,大家应该已经想到了,那就是把这几个特定的汉字做成字库,然后调用lcd_printf函数显示。为了和官方的函数名有所区别,我们就起名为 My_lcd_printf。

以后就可以这样显示汉字了,如下所示

My_lcd_text_location( 162);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_listva_startva_end

    还不知道他们怎么用的请看下面这篇文章



  • 把格式化好的字符串放到了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;"嵌",00x20,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;"入",10x00,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;"式",20x80,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;"小",30x40,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;"书",40x80,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;"虫",50x00,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》

原创不易,如果你喜欢我的公众号、觉得我 文章对你有所启发,请务必“点赞、收藏、转发”,这对我很重要,谢谢!

欢迎订阅    嵌入式小书虫