[STM32]LCD驱动逻辑

前言

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;
    }
}