前言
LCD的驱动,我前面已经通过更改FSMC的配置,提高了屏幕的刷新率,现在我想要在屏幕上显示中文,但是屏幕官方并没有提供中文打印的函数,所以需要我自己去写,现在我要开始解析打印ASCII码的逻辑,顺着这个逻辑写一个打印汉字的驱动。

正篇
解析库函数
首先,让我们来看 lcd_show_string 函数,我已经给函数注释好了:
/**
* @brief 显示字符串
* @param x,y : 起始坐标
* @param width,height: 区域大小(字符串显示的限制区域)
* @param size : 字体大小 12/16/24/32
* @param p : 字符串指针
* @param color : 字符串的颜色
* @retval 无
*/
// 示例调用:lcd_show_string(20, 20, 999, 999, 16, "AOOFOO", BLACK);
void lcd_show_string(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t size, char *p, uint16_t color)
{
uint8_t x0 = x; // 保存起始 x 坐标
// 将 width 和 height 转换为显示区域的结束坐标
width += x;
height += y;
// 开始遍历字符串
while ((*p <= '~') && (*p >= ' ')) /* 判断是不是合法的 ASCII 字符 */
{
if (x >= width)
{
// 如果 x 超过了显示区域的宽度,移到下一行
x = x0;
y += size; // y 坐标增加一个字符的高度
}
if (y >= height)
{
// 如果 y 超过了显示区域的高度,退出循环
break;
}
// 显示单个字符
lcd_show_char(x, y, *p, size, 0, color);
x += size / 2; // x 坐标向右移动半个字符宽度(因为一个字符的宽度是 size / 2)
p++; // 移动到下一个字符
}
}
官方这段给的代码很好理解,就是通过给出的字符串来遍历。比如我们传参”AOOFOO”,那么”AOOFOO”这个字符串的首字符地址就是我们传进去的地址,然后代码里再对这个地址进行解引用,变成字符”A”,字符A。字符A在’ ‘ 与 ‘~’之间,所以可以进入到 lcd_show_char(x, y, *p, size, 0, color)这个函数中。
我们接下来看lcd_show_char 函数,我已经给函数注释好了:
/**
* @brief 在指定位置显示一个字符
* @param x,y : 显示坐标
* @param chr : 要显示的字符,取值范围为 ' ' 到 '~'
* @param size : 字体大小,可以是 12、16、24、32
* @param mode : 显示模式,1 为叠加模式(背景透明),0 为非叠加模式(背景不透明)
* @param color : 字符的颜色
* @retval 无
*/
void lcd_show_char(uint16_t x, uint16_t y, char chr, uint8_t size, uint8_t mode, uint16_t color)
{
uint8_t temp, t1, t;
uint16_t y0 = y; // 保存初始 y 坐标
uint8_t csize = 0; // 字符点阵所占的字节数
uint8_t *pfont = 0; // 字符点阵数据指针
// 计算字符点阵所占的字节数
// 每个字符的点阵数据字节数 = 每列的字节数 * 字符宽度的列数
// 每列的字节数 = 字体高度 / 8,若有余数则加 1
// 字符宽度的列数 = 字体宽度 = 字体大小 / 2
csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2);
chr = chr - ' '; // 将字符转换为字符点阵数组的索引
// ASCII 字符从空格(' ',编码 32)开始,因此减去空格的编码值 32
// 根据字体大小选择对应的字符点阵数据
switch (size)
{
case 12:
pfont = (uint8_t *)asc2_1206[chr]; // 12x6 字体
break;
case 16:
pfont = (uint8_t *)asc2_1608[chr]; // 16x8 字体
break;
case 24:
pfont = (uint8_t *)asc2_2412[chr]; // 24x12 字体
break;
case 32:
pfont = (uint8_t *)asc2_3216[chr]; // 32x16 字体
break;
default:
return ; // 字体大小不支持,直接返回
}
// 开始绘制字符
for (t = 0; t < csize; t++)
{
temp = pfont[t]; // 获取字符点阵数据的第 t 个字节
for (t1 = 0; t1 < 8; t1++) // 遍历该字节的 8 位
{
if (temp & 0x80) // 判断最高位是否为 1,即需要显示该点
{
lcd_draw_point(x, y, color); // 绘制该点,颜色为指定的字符颜色
}
else if (mode == 0) // 非叠加模式下,需要绘制背景色
{
lcd_draw_point(x, y, g_back_color); // 绘制背景色
}
temp <<= 1; // 左移一位,处理下一个像素点
y++; // 行坐标递增
if (y >= lcddev.height) return; // 超出屏幕高度,退出
if ((y - y0) == size) // 一列绘制完成
{
y = y0; // 行坐标重置为字符起始位置
x++; // 列坐标递增
if (x >= lcddev.width)
{
return; // 超出屏幕宽度,退出
}
break; // 退出内部循环,处理下一个字节的数据
}
}
}
}
再结合ASCII的字模库就能理解该函数了:
/* 16x8 ASCII字符集点阵 */
const unsigned char asc2_1608[95][16]={
// 每个字符占用 16 个字节的数据
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*" ",0*/
{0x00,0x00,0x00,0x00,0x00,0x00,0x1F,0xCC,0x00,0x0C,0x00,0x00,0x00,0x00,0x00,0x00},/*"!",1*/
// ...
};
ASCII的字模库是二维数组,例如char asc2_1608[95][16],95行,16列,是由95个一维数组组成的。lcd_show_char 函数按字节得到16行,每行8位,再由 lcd_draw_point把每一个像素点打印出来。
自己写库函数
看懂了上面的函数,我们自己就可以写自己的汉字函数了,我们需要先用字模软件生成汉字的字模:
const unsigned char chinese_char_data[][32] =
{
{0x00,0x00,0x00,0x00,0x47,0xFC,0x41,0x02,0x41,0x02,0x41,0x02,0x41,0x02,0x41,0x02,
0x41,0x02,0x41,0x02,0x41,0x02,0x7F,0x02,0x00,0x02,0x00,0x1E,0x00,0x00,0x00,0x00},/*"已",0*/
{0x40,0x08,0x7F,0xF8,0x49,0x10,0x49,0x10,0x7F,0xFF,0x40,0x20,0x00,0x81,0x08,0x82,
0x88,0x8C,0x68,0xB0,0x0F,0xC0,0x28,0xB0,0xC8,0x8C,0x08,0x82,0x00,0x81,0x00,0x00},/*"联",1*/
{0x00,0x00,0x7F,0xFF,0x40,0x08,0x44,0x10,0x42,0x60,0x41,0x80,0x4E,0x70,0x40,0x08,
0x44,0x10,0x42,0x60,0x41,0x80,0x4E,0x72,0x40,0x01,0x7F,0xFE,0x00,0x00,0x00,0x00},/*"网",2*/
};
// 汉字对应的编码(GB2312编码)
const uint16_t chinese_char_code[] =
{
0xD2D1, // "已"
0xC1AA, // "联"
0xCDF8 // "网"
};
然后编写我们自己的库函数:
查找汉字点阵数据:
const unsigned char *get_chinese_char_font(uint16_t code)
{
size_t num_chars = sizeof(chinese_char_code) / sizeof(chinese_char_code[0]);
for (size_t i = 0; i < num_chars; i++)
{
if (chinese_char_code[i] == code)
{
return chinese_char_data[i];
}
}
return NULL; // 未找到对应的汉字,返回NULL
}
显示汉字字符:
void lcd_show_chinese_char(uint16_t x, uint16_t y, uint8_t size, const unsigned char *font, uint16_t color)
{
uint16_t x0 = x;
uint16_t y0 = y;
uint8_t temp;
uint16_t i, j;
uint8_t bytes_per_line = size / 8; // 每一行占用的字节数
for (i = 0; i < size; i++) // 行
{
x = x0; // 每行开始时,x 重置为起始值
for (j = 0; j < bytes_per_line; j++) // 列字节
{
temp = font[i * bytes_per_line + j];
for (uint8_t k = 0; k < 8; k++)
{
if (temp & (0x80 >> k))
{
lcd_draw_point(x, y, color);
}
else
{
lcd_draw_point(x, y, g_back_color);
}
x++;
}
}
y++;
}
}
打印汉字字符串:
void lcd_show_chinese_string(uint16_t x, uint16_t y, uint8_t size, char *p, uint16_t color)
{
uint16_t x0 = x;
uint16_t y0 = y;
uint16_t code;
const unsigned char *font;
while (*p != '\0')
{
// 检查是否需要换行
if (x > lcddev.width - size)
{
x = x0; // x 重置为初始值
y += size; // y 增加
}
if (y > lcddev.height - size)
{
break; // 超出屏幕范围,退出
}
// 获取汉字编码
code = ((uint8_t)p[0] << 8) | (uint8_t)p[1];
// 获取汉字点阵数据
font = get_chinese_char_font(code);
if (font != NULL)
{
// 显示汉字
lcd_show_chinese_char(x, y, size, font, color);
}
else
{
// 未找到汉字,显示一个空白或替代字符
lcd_show_char(x, y, '*', size, 0, color);
}
x += size;
p += 2;
}
}