傻孩子(GorgonMeducer) · 2022年01月25日

【玩转Arm-2D】如何重现“活字印刷”术

以下文章来源于嵌入式小书虫 ,作者FledgingSu 支离苏
image.png
玩过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 的功能。

汉字字库制作

那我们先开始制作字库。字符取模软件的使用我就不讲了,网上有很多。字符取模可以分为横向取模、竖向取模等,为了和官方保持一致,我们采用横向取模。

好,我们讲一下怎么把一个汉字转换成十六进制数组,如下图
image.png
横向取模原理如下,
image.png

  • 这样一行一行的转换,最后保存到数组中就是字库了。

用子tile表示资源

接下来讲一下待会要用到的Arm-2D的知识,那就是上一篇我们没有讲到的子Tile的另一种用法,资源也可以用子tile来建立(即取一个大图的局部作为新的资源)

如下图所示:
image.png

  • 父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用法。

如下图:
image.png

  • 有了这个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

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

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  +  汉字字库制作了一个简单的小虫吃草的小游戏,视频演示如下
image.png

补充

我们现在去看看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 支离苏

专栏推荐文章

如果你喜欢我的思维,欢迎订阅裸机思维
推荐阅读
关注数
1476
内容数
116
探讨嵌入式系统开发的相关思维、方法、技巧。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息