本栏目主要是想整理和归纳一下本人所学的网络开源嵌入式硬件项目,并做一些个人对项目的实现与整理。限制于等级当日发文限制,可能更新顺序不定,待到本栏目发文量达到一定数量后再做整理,感谢各位看客的关注与理解,如有需要可以关注收藏一下本栏目。

1. 项目硬件简介

        液晶显示屏(LCD,Liquid Crystal Display)是一种利用液晶材料光电特性实现图像显示的平板显示技术,具有功耗低、厚度薄、显示效果稳定等优点,被广泛应用于嵌入式设备、工业控制终端、消费电子产品以及智能终端中。在嵌入式系统中,LCD 通常作为人机交互界面的核心组件,用于显示文字、图形、图像以及实时状态信息。本项目采用一块分辨率为 240×320 的彩色 TFT LCD 模块,通过串行接口与微控制器连接,实现基础图形绘制、图片显示以及多语言文字输出等功能。

        ST7789 是一款常用的 TFT LCD 显示控制器芯片,内部集成了显存(GRAM)、驱动电路和丰富的显示控制指令,支持 RGB565 等多种颜色格式,并可通过 SPI 接口与主控芯片通信。相比并行接口 LCD,ST7789 具有连线简单、资源占用少、兼容性强等优势,非常适合中低资源的嵌入式平台。本项目基于 ST7789 控制芯片完成 LCD 的初始化、显示窗口设置、像素数据写入以及文本渲染等底层驱动开发,并在此基础上实现图片显示、英文与中文字符输出以及动态信息更新,构建了一套完整的嵌入式图形显示系统。

        本项目的硬件是基于B站梅花七月香的RTOS智能天气时钟硬件开发。

2.项目需求与效果展示

需求:

  • 在LCD上显示指定图片
  • 在图片左上角显示英文字体:"It's you who healed the sky.",背景白色字体设置粉色
  • 在图片左下角设置时间,由于没有外接STM32做不到RTC实时时钟功能,因此时间采用软件计时方式进行模拟更新,以验证动态文本显示功能的可行性,并为后续接入硬件 RTC 提供基础。
  • 在图片左下角显示中文字体:当前时间,背景白色字体蓝色

3. 项目实现

        接下来我们先看ST7789的代码是怎么实现的,我们将代码实现拆解为了三个步骤

3.1. SPI 通信与 ST7789 初始化流程

        在嵌入式系统中,LCD 屏幕并不是上电后即可直接显示内容的设备,而是需要通过通信接口向显示控制器发送初始化指令,配置工作模式后才能正常显示。本项目采用 SPI 接口驱动 ST7789 TFT LCD,通过 STM32 作为主设备向 LCD 发送命令和数据,实现对屏幕的完全控制。

(1)SPI 接口与控制引脚配置

        ST7789 支持多种通信方式,其中 SPI 接口因连线简单、资源占用少,在嵌入式系统中应用最为广泛。除了 SPI 时钟(SCK)和数据线(MOSI)外,还需要若干控制引脚配合完成通信,包括片选(CS)、数据/命令选择(DC)、硬件复位(RESET)以及背光控制(BL)。

        在本项目中,这些引脚通过 GPIO 输出控制,例如:

#define ST7789_CS_Clr()    GPIO_ResetBits(GPIOB, GPIO_Pin_12)
#define ST7789_CS_Set()    GPIO_SetBits(GPIOB, GPIO_Pin_12)

#define ST7789_DC_Clr()    GPIO_ResetBits(GPIOB, GPIO_Pin_1)
#define ST7789_DC_Set()    GPIO_SetBits(GPIOB, GPIO_Pin_1)

#define ST7789_RST_Clr()   GPIO_ResetBits(GPIOB, GPIO_Pin_0)
#define ST7789_RST_Set()   GPIO_SetBits(GPIOB, GPIO_Pin_0)

#define ST7789_BL_Set()    GPIO_SetBits(GPIOB, GPIO_Pin_10)

其中,DC 引脚尤为关键:

  • DC = 0 :发送命令
  • DC = 1 :发送数据

通过这一机制,单一 SPI 总线即可同时完成控制指令和像素数据的传输。

(2)命令与数据发送机制

ST7789 的所有操作都通过写寄存器完成。驱动中封装了一个通用函数,用于发送命令及其参数:

void st7789_write_register(uint8_t reg, uint8_t *data, uint32_t len)
{
    ST7789_CS_Clr();

    ST7789_DC_Clr();               // 命令模式
    SPI2_WriteByte(reg);

    ST7789_DC_Set();               // 数据模式
    for (uint32_t i = 0; i < len; i++)
    {
        SPI2_WriteByte(data[i]);
    }

    ST7789_CS_Set();
}

执行流程如下:

  1. 拉低 CS,选中 LCD
  2. DC 置低,发送寄存器地址(命令)
  3. DC 置高,发送参数数据
  4. 释放 CS,结束通信

这种设计使得 LCD 控制非常灵活,既可以写配置寄存器,也可以写显存数据。

(3)硬件复位流程

在初始化 LCD 之前,需要先进行硬件复位,使控制器进入已知状态:

void st7789_reset(void)
{
    ST7789_RST_Clr();
    cpu_delay(100 * 1000);

    ST7789_RST_Set();
    cpu_delay(100 * 1000);
}

通过短暂拉低 RESET 引脚后再拉高,ST7789 将重新启动,避免上电状态不确定导致显示异常。

(4)LCD 初始化序列

完成复位后,需要向 LCD 发送一系列配置命令,使其进入可显示状态。核心初始化流程如下:

void st7789_init_display(void)
{
    uint8_t data;

    st7789_write_register(0x11, NULL, 0);   // Sleep Out
    cpu_delay(120 * 1000);

    data = 0x00;
    st7789_write_register(0x36, &data, 1);  // 设置扫描方向

    data = 0x05;
    st7789_write_register(0x3A, &data, 1);  // 设置颜色格式 RGB565

    st7789_write_register(0x29, NULL, 0);   // Display ON
}

该初始化序列完成了以下关键配置:

  • 退出休眠模式(Sleep Out)
  • 设置屏幕扫描方向(横屏或竖屏)
  • 设置颜色格式为 RGB565
  • 开启显示输出

其中 RGB565 是嵌入式系统中常用的颜色格式,每个像素占 16 位,既能保证显示效果,又能减少 SPI 数据量。

(5)背光控制

即使 LCD 已经开始工作,如果背光未开启,屏幕仍然无法被观察到。因此初始化完成后需要打开背光:

void st7789_set_backlight(uint8_t state)
{
    if (state)
        ST7789_BL_Set();
    else
        ST7789_BL_Clr();
}

背光通常由 GPIO 控制,也可通过 PWM 实现亮度调节。

(6)完整初始化流程

最终的初始化函数将上述步骤整合:

void st7789_init(void)
{
    st7789_reset();           // 硬件复位
    st7789_init_display();    // 发送初始化命令
    st7789_set_backlight(1);  // 打开背光
}

执行该函数后,LCD 即进入可显示状态,后续即可进行图形绘制、文字输出和图片显示等操作。

3.2 显示窗口设置与基础绘图实现

        在完成 LCD 初始化后,屏幕已经具备基本显示能力,但仍无法直接绘制图形或显示内容。ST7789 采用内部显存(GRAM)存储像素数据,只有通过设置显示窗口并向该区域写入像素值,才能实现真正的图形显示。因此,“显示窗口设置 + 像素填充”构成了所有绘图操作的基础。

(1)坐标合法性检查

        在进行绘图之前,首先需要确保指定区域在屏幕范围内,否则可能导致显示异常甚至写入无效数据。本项目通过一个简单的函数对坐标进行边界判断:

static int in_screen_range(uint16_t xs, uint16_t ys, uint16_t xe, uint16_t ye)
{
    if (xs >= ST7789_WIDTH || ys >= ST7789_HEIGHT)
        return 0;

    if (xe >= ST7789_WIDTH)  xe = ST7789_WIDTH - 1;
    if (ye >= ST7789_HEIGHT) ye = ST7789_HEIGHT - 1;

    return 1;
}

该函数用于判断:

  • 起始坐标是否超出屏幕
  • 结束坐标是否越界
  • 保证后续绘图操作始终在有效区域内

这一步虽然简单,却能避免大量潜在错误,是嵌入式图形编程中常见的安全措施。

(2)设置显示窗口(核心操作)

        ST7789 并不是直接在指定坐标画点,而是需要先通过寄存器配置一个“显示窗口”,然后向该区域连续写入像素数据。该过程通过以下函数实现:

static int st7789_set_range_and_prepare_gram(uint16_t xs, uint16_t ys,
                                             uint16_t xe, uint16_t ye)
{
    if (!in_screen_range(xs, ys, xe, ye))
        return 0;

    uint8_t data[4];

    // 设置列地址范围
    data[0] = xs >> 8;
    data[1] = xs & 0xFF;
    data[2] = xe >> 8;
    data[3] = xe & 0xFF;
    st7789_write_register(0x2A, data, 4);

    // 设置行地址范围
    data[0] = ys >> 8;
    data[1] = ys & 0xFF;
    data[2] = ye >> 8;
    data[3] = ye & 0xFF;
    st7789_write_register(0x2B, data, 4);

    // 开始写入显存
    st7789_write_register(0x2C, NULL, 0);

    return 1;
}

该函数完成了三个关键步骤:

1. 设置列地址(X 方向):寄存器 0x2A 用于指定显示区域的列范围,即水平方向:起始列 xs → 结束列 xe。
2. 设置行地址(Y 方向):寄存器 0x2B 用于指定显示区域的行范围,即垂直方向:起始行 ys → 结束行 ye。
3. 启动显存写入:发送 0x2C 命令后,LCD 进入 GRAM 写入模式,随后发送的数据将被依次写入该窗口区域。

(3)区域填充实现

        在设置好窗口后,只需向 LCD 连续发送颜色数据,即可完成矩形区域的绘制。本项目通过如下函数实现纯色填充:

void st7789_fill_color(uint16_t xs, uint16_t ys,
                       uint16_t xe, uint16_t ye,
                       uint16_t color)
{
    if (!st7789_set_range_and_prepare_gram(xs, ys, xe, ye))
        return;

    uint32_t pixels = (xe - xs + 1) * (ye - ys + 1);

    ST7789_DC_Set();   // 数据模式
    ST7789_CS_Clr();

    for (uint32_t i = 0; i < pixels; i++)
    {
        SPI2_WriteByte(color >> 8);
        SPI2_WriteByte(color & 0xFF);
    }

    ST7789_CS_Set();
}

执行流程如下:

  1. 设置目标显示区域
  2. 计算区域内像素总数
  3. 循环发送 RGB565 颜色值
  4. 填满整个窗口

由于 ST7789 会自动递增显存地址,因此无需手动计算每个像素的位置,只需连续写入数据即可。

(4)RGB565 颜色格式

ST7789 默认使用 RGB565 格式表示颜色:16 位 = R(5bit) + G(6bit) + B(5bit)

项目中通过辅助函数生成颜色值,例如:

uint16_t mkcolor(uint8_t r, uint8_t g, uint8_t b)
{
    return ((r & 0xF8) << 8) |
           ((g & 0xFC) << 3) |
           (b >> 3);
}

        总之,显示窗口设置与区域填充是 ST7789 驱动的核心功能,它将 LCD 从“可通信设备”转变为“可绘图设备”。通过设置列地址、行地址并向显存连续写入像素数据,系统即可实现任意位置、任意大小的图形绘制,为后续的文字显示和图片渲染提供了基础支持。

3.3 基于字模与像素流的文字与图片显示实现

        在完成 LCD 初始化和基础绘图功能后,屏幕已经具备了像素级显示能力,但仍无法直接显示文字或图片。ST7789 并不提供字符渲染或图像解码功能,所有内容都必须以像素数据的形式写入显存。因此,无论是文本还是图像,其本质都是对像素点阵的绘制。本项目通过字模数据与位图像素流相结合,实现了英文、中文字符以及彩色图片的显示功能。

(1)点阵字体与字模渲染原理

        在嵌入式系统中,文字通常采用点阵字体(Bitmap Font)。每个字符由一个固定尺寸的二值矩阵表示,矩阵中的每一位对应一个像素点:

  • 1 表示绘制前景色
  • 0 表示绘制背景色

驱动中通过逐位扫描字模数据,将字符转换为屏幕上的像素点。

static void st7789_draw_font(uint16_t x, uint16_t y,
                             const uint8_t *font_data,
                             uint16_t color, uint16_t bg,
                             uint8_t width, uint8_t height)
{
    st7789_set_range_and_prepare_gram(x, y,
                                      x + width - 1,
                                      y + height - 1);

    for (uint16_t row = 0; row < height; row++)
    {
        for (uint16_t col = 0; col < width; col++)
        {
            if (font_data[row * width / 8 + col / 8] &
                (0x80 >> (col % 8)))
                write_pixel(color);
            else
                write_pixel(bg);
        }
    }
}

该函数完成了字符绘制的核心逻辑:

  1. 设置字符显示区域
  2. 按行扫描字模数据
  3. 判断当前像素是否点亮
  4. 输出前景色或背景色

通过这种方式,任何字符都可以转换为像素图形。

(2)ASCII 字符显示实现

        英文字符属于 ASCII 编码,通常为单字节字符,宽度约为字体高度的一半。本项目通过查找 ASCII 字库并调用通用字模绘制函数实现显示:

void st7789_write_ascii(uint16_t x, uint16_t y, char ch,
                        uint16_t color, uint16_t bg,
                        const font_t *font)
{
    const uint8_t *data = font->ascii[(uint8_t)ch - 32];
    st7789_draw_font(x, y, data,
                     color, bg,
                     font->size / 2, font->size);
}

这里的关键点在于:

  • ASCII 字符从空格(32)开始存储
  • 宽度通常为字体大小的一半
  • 使用统一的绘制函数输出

(3)中文字符显示实现

        中文字符通常为双字节编码,且每个字符都需要独立的字模数据。项目中采用点阵字库方式存储中文字符,并在显示时进行匹配查找:

void st7789_write_chinese(uint16_t x, uint16_t y,
                          char *str,
                          uint16_t color, uint16_t bg,
                          const font_t *font)
{
    const uint8_t *data = find_chinese_font(str, font);
    if (data)
    {
        st7789_draw_font(x, y, data,
                         color, bg,
                         font->size, font->size);
    }
}

与 ASCII 字符相比,中文字符具有以下特点:

  • 占用多个字节
  • 字符宽度等于字体高度
  • 必须在字库中查找对应字模

通过自定义字库,可以实现任意中文字符的显示。

(4)字符串显示机制

                为了支持混合文本(英文 + 中文),项目实现了字符串输出函数,根据字符编码自动选择显示方式:

void st7789_write_string(uint16_t x, uint16_t y,
                         char *str,
                         uint16_t color, uint16_t bg,
                         const font_t *font)
{
    while (*str)
    {
        int len = utf8_char_length(*str);

        if (len == 1)
        {
            st7789_write_ascii(x, y, *str,
                               color, bg, font);
            x += font->size / 2;
        }
        else
        {
            char ch[5];
            strncpy(ch, str, len);
            st7789_write_chinese(x, y, ch,
                                 color, bg, font);
            x += font->size;
        }

        str += len;
    }
}

该函数的工作流程如下:

  1. 判断当前字符的字节长度
  2. ASCII → 单字节显示
  3. 中文 → 多字节处理
  4. 根据字符宽度更新光标位置

这一机制使系统能够正确显示中英文混合字符串。

(5)图片显示原理

        图片显示的本质是向 LCD 显存连续写入像素数据。本项目使用 RGB565 格式存储图片,通过逐字节传输实现显示:

void st7789_draw_image(uint16_t x, uint16_t y,
                       const image_t *img)
{
    st7789_set_range_and_prepare_gram(
        x, y,
        x + img->width - 1,
        y + img->height - 1);

    uint32_t size = img->width * img->height;

    for (uint32_t i = 0; i < size; i++)
    {
        SPI2_WriteByte(img->data[i] >> 8);
        SPI2_WriteByte(img->data[i] & 0xFF);
    }
}

图片数据通常由取模软件将原始图像转换为 RGB565 数组,每个像素占 2 字节。由于 ST7789 会自动递增显存地址,因此只需连续写入即可完成整幅图像显示。

(6)文字与图片显示的本质

        无论是字符还是图像,其显示过程都遵循同一原则:所有内容最终都被转换为像素数据写入 LCD 显存。不同之处在于:

3.4. 图片取模

  • 找到你想放入的图片,打开图片取模工具Image2Lcd

  • 为了避免取模不全,需要用电脑自带的画图软件将分辨率设置为340x240,来匹配LCD的格式,要注意取模后一定是这个数:153600

由于 ST7789 LCD 采用 RGB565 色彩格式,每个像素需要 16 位(2 字节)存储。当屏幕分辨率为 320×240 时,整屏像素数为 76800,因此完整显示一幅图像需要传输 153600 字节的数据。图片取模工具必须生成与屏幕分辨率和颜色格式一致的数据,否则将导致显示异常或图像不完整

  • 最后点击保存,该文件会生成一个.c文件,该文件内为图片取模后的数组。将该文件添加到工程中即可。注意.c文件后面不要忘了定义图片参数
const image_t image_tv = 
{
    .width = 320,
    .height = 240,
    .data = gImage_tv,
};

3.5.文字取模

  • 打开文字取模软件PCtoLCD2002,输入想取模的字符

注意设置,不然可能无法显示字模

  • 复制文字编码写到font16.c文件,英文取模其实直接ASCII码就行,这个地方是中文取模,具体你可以看一下接下来的取模.c文件
  • 可以设置字号为16,32,48等,不过要记得字号一定要匹配,以下是这三种字号的计算公式,如若字体显示失败,重点检查这个地方

在嵌入式系统中,点阵字体通常采用单色位图方式存储,每个像素仅占 1 bit。对于宽度为 W、高度为 H 的字符,其字模数据大小为 W × H ÷ 8 字节。例如,16×16 字体需要 32 字节,32×32 字体需要 128 字节,48×48 字体需要 288 字节。由于中文字符通常为等宽方阵,而 ASCII 字符为半宽结构,因此两者的字模大小也存在差异。

具体的font16.c取模长这样

#include <stdint.h>
#include "font.h"

static const uint8_t ascii_model[] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*" ",0*/
0x00,0x00,0x00,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x00,0x00,0x10,0x10,0x00,0x00,/*"!",1*/
0x00,0x12,0x24,0x24,0x48,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*""",2*/
0x00,0x00,0x00,0x12,0x12,0x12,0x7E,0x24,0x24,0x24,0x7E,0x24,0x24,0x24,0x00,0x00,/*"#",3*/
0x00,0x00,0x08,0x3C,0x4A,0x4A,0x48,0x38,0x0C,0x0A,0x0A,0x4A,0x4A,0x3C,0x08,0x08,/*"$",4*/
0x00,0x00,0x00,0x44,0xA4,0xA8,0xA8,0xB0,0x54,0x1A,0x2A,0x2A,0x4A,0x44,0x00,0x00,/*"%",5*/
0x00,0x00,0x00,0x30,0x48,0x48,0x48,0x50,0x6E,0xA4,0x94,0x98,0x89,0x76,0x00,0x00,/*"&",6*/
0x00,0x60,0x20,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*"'",7*/
0x00,0x02,0x04,0x08,0x08,0x10,0x10,0x10,0x10,0x10,0x10,0x08,0x08,0x04,0x02,0x00,/*"(",8*/
0x00,0x40,0x20,0x10,0x10,0x08,0x08,0x08,0x08,0x08,0x08,0x10,0x10,0x20,0x40,0x00,/*")",9*/
0x00,0x00,0x00,0x00,0x10,0x10,0xD6,0x38,0x38,0xD6,0x10,0x10,0x00,0x00,0x00,0x00,/*"*",10*/
0x00,0x00,0x00,0x00,0x00,0x08,0x08,0x08,0x7F,0x08,0x08,0x08,0x00,0x00,0x00,0x00,/*"+",11*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x60,0x20,0x20,0x40,/*",",12*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*"-",13*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x60,0x60,0x00,0x00,/*".",14*/
0x00,0x00,0x02,0x04,0x04,0x04,0x08,0x08,0x10,0x10,0x10,0x20,0x20,0x40,0x40,0x00,/*"/",15*/
0x00,0x00,0x00,0x18,0x24,0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x24,0x18,0x00,0x00,/*"0",16*/
0x00,0x00,0x00,0x08,0x38,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x3E,0x00,0x00,/*"1",17*/
0x00,0x00,0x00,0x3C,0x42,0x42,0x42,0x02,0x04,0x08,0x10,0x20,0x42,0x7E,0x00,0x00,/*"2",18*/
0x00,0x00,0x00,0x3C,0x42,0x42,0x02,0x04,0x18,0x04,0x02,0x42,0x42,0x3C,0x00,0x00,/*"3",19*/
0x00,0x00,0x00,0x04,0x0C,0x0C,0x14,0x24,0x24,0x44,0x7F,0x04,0x04,0x1F,0x00,0x00,/*"4",20*/
0x00,0x00,0x00,0x7E,0x40,0x40,0x40,0x78,0x44,0x02,0x02,0x42,0x44,0x38,0x00,0x00,/*"5",21*/
0x00,0x00,0x00,0x18,0x24,0x40,0x40,0x5C,0x62,0x42,0x42,0x42,0x22,0x1C,0x00,0x00,/*"6",22*/
0x00,0x00,0x00,0x7E,0x42,0x04,0x04,0x08,0x08,0x10,0x10,0x10,0x10,0x10,0x00,0x00,/*"7",23*/
0x00,0x00,0x00,0x3C,0x42,0x42,0x42,0x24,0x18,0x24,0x42,0x42,0x42,0x3C,0x00,0x00,/*"8",24*/
0x00,0x00,0x00,0x38,0x44,0x42,0x42,0x42,0x46,0x3A,0x02,0x02,0x24,0x18,0x00,0x00,/*"9",25*/
0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,/*":",26*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10,0x00,0x00,0x00,0x00,0x00,0x10,0x10,0x10,/*";",27*/
0x00,0x00,0x00,0x02,0x04,0x08,0x10,0x20,0x40,0x20,0x10,0x08,0x04,0x02,0x00,0x00,/*"<",28*/
0x00,0x00,0x00,0x00,0x00,0x00,0x7E,0x00,0x00,0x7E,0x00,0x00,0x00,0x00,0x00,0x00,/*"=",29*/
0x00,0x00,0x00,0x40,0x20,0x10,0x08,0x04,0x02,0x04,0x08,0x10,0x20,0x40,0x00,0x00,/*">",30*/
0x00,0x00,0x00,0x3C,0x42,0x42,0x62,0x04,0x08,0x08,0x08,0x00,0x18,0x18,0x00,0x00,/*"?",31*/
0x00,0x00,0x00,0x38,0x44,0x5A,0xAA,0xAA,0xAA,0xAA,0xAA,0x5C,0x42,0x3C,0x00,0x00,/*"@",32*/
0x00,0x00,0x00,0x10,0x10,0x18,0x28,0x28,0x24,0x3C,0x44,0x42,0x42,0xE7,0x00,0x00,/*"A",33*/
0x00,0x00,0x00,0xF8,0x44,0x44,0x44,0x78,0x44,0x42,0x42,0x42,0x44,0xF8,0x00,0x00,/*"B",34*/
0x00,0x00,0x00,0x3E,0x42,0x42,0x80,0x80,0x80,0x80,0x80,0x42,0x44,0x38,0x00,0x00,/*"C",35*/
0x00,0x00,0x00,0xF8,0x44,0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x44,0xF8,0x00,0x00,/*"D",36*/
0x00,0x00,0x00,0xFC,0x42,0x48,0x48,0x78,0x48,0x48,0x40,0x42,0x42,0xFC,0x00,0x00,/*"E",37*/
0x00,0x00,0x00,0xFC,0x42,0x48,0x48,0x78,0x48,0x48,0x40,0x40,0x40,0xE0,0x00,0x00,/*"F",38*/
0x00,0x00,0x00,0x3C,0x44,0x44,0x80,0x80,0x80,0x8E,0x84,0x44,0x44,0x38,0x00,0x00,/*"G",39*/
0x00,0x00,0x00,0xE7,0x42,0x42,0x42,0x42,0x7E,0x42,0x42,0x42,0x42,0xE7,0x00,0x00,/*"H",40*/
0x00,0x00,0x00,0x7C,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x7C,0x00,0x00,/*"I",41*/
0x00,0x00,0x00,0x3E,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x88,0xF0,/*"J",42*/
0x00,0x00,0x00,0xEE,0x44,0x48,0x50,0x70,0x50,0x48,0x48,0x44,0x44,0xEE,0x00,0x00,/*"K",43*/
0x00,0x00,0x00,0xE0,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x42,0xFE,0x00,0x00,/*"L",44*/
0x00,0x00,0x00,0xEE,0x6C,0x6C,0x6C,0x6C,0x6C,0x54,0x54,0x54,0x54,0xD6,0x00,0x00,/*"M",45*/
0x00,0x00,0x00,0xC7,0x62,0x62,0x52,0x52,0x4A,0x4A,0x4A,0x46,0x46,0xE2,0x00,0x00,/*"N",46*/
0x00,0x00,0x00,0x38,0x44,0x82,0x82,0x82,0x82,0x82,0x82,0x82,0x44,0x38,0x00,0x00,/*"O",47*/
0x00,0x00,0x00,0xFC,0x42,0x42,0x42,0x42,0x7C,0x40,0x40,0x40,0x40,0xE0,0x00,0x00,/*"P",48*/
0x00,0x00,0x00,0x38,0x44,0x82,0x82,0x82,0x82,0x82,0x82,0xB2,0x4C,0x38,0x06,0x00,/*"Q",49*/
0x00,0x00,0x00,0xFC,0x42,0x42,0x42,0x7C,0x48,0x48,0x44,0x44,0x42,0xE3,0x00,0x00,/*"R",50*/
0x00,0x00,0x00,0x3E,0x42,0x42,0x40,0x20,0x18,0x04,0x02,0x42,0x42,0x7C,0x00,0x00,/*"S",51*/
0x00,0x00,0x00,0xFE,0x92,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x38,0x00,0x00,/*"T",52*/
0x00,0x00,0x00,0xE7,0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x3C,0x00,0x00,/*"U",53*/
0x00,0x00,0x00,0xE7,0x42,0x42,0x44,0x24,0x24,0x28,0x28,0x18,0x10,0x10,0x00,0x00,/*"V",54*/
0x00,0x00,0x00,0xD6,0x54,0x54,0x54,0x54,0x54,0x6C,0x28,0x28,0x28,0x28,0x00,0x00,/*"W",55*/
0x00,0x00,0x00,0xE7,0x42,0x24,0x24,0x18,0x18,0x18,0x24,0x24,0x42,0xE7,0x00,0x00,/*"X",56*/
0x00,0x00,0x00,0xEE,0x44,0x44,0x28,0x28,0x10,0x10,0x10,0x10,0x10,0x38,0x00,0x00,/*"Y",57*/
0x00,0x00,0x00,0x7E,0x84,0x04,0x08,0x08,0x10,0x20,0x20,0x42,0x42,0xFC,0x00,0x00,/*"Z",58*/
0x00,0x1E,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x1E,0x00,/*"[",59*/
0x00,0x00,0x40,0x20,0x20,0x20,0x10,0x10,0x10,0x08,0x08,0x04,0x04,0x04,0x02,0x02,/*"\",60*/
0x00,0x78,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x78,0x00,/*"]",61*/
0x00,0x18,0x24,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*"^",62*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,/*"_",63*/
0x00,0x60,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*"`",64*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x38,0x44,0x0C,0x34,0x44,0x4C,0x36,0x00,0x00,/*"a",65*/
0x00,0x00,0x00,0x00,0xC0,0x40,0x40,0x58,0x64,0x42,0x42,0x42,0x64,0x58,0x00,0x00,/*"b",66*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1C,0x22,0x40,0x40,0x40,0x22,0x1C,0x00,0x00,/*"c",67*/
0x00,0x00,0x00,0x00,0x06,0x02,0x02,0x3E,0x42,0x42,0x42,0x42,0x46,0x3B,0x00,0x00,/*"d",68*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3C,0x42,0x42,0x7E,0x40,0x42,0x3C,0x00,0x00,/*"e",69*/
0x00,0x00,0x00,0x00,0x0C,0x12,0x10,0x7C,0x10,0x10,0x10,0x10,0x10,0x7C,0x00,0x00,/*"f",70*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3E,0x44,0x44,0x38,0x40,0x3C,0x42,0x42,0x3C,/*"g",71*/
0x00,0x00,0x00,0x00,0xC0,0x40,0x40,0x5C,0x62,0x42,0x42,0x42,0x42,0xE7,0x00,0x00,/*"h",72*/
0x00,0x00,0x00,0x30,0x30,0x00,0x00,0x70,0x10,0x10,0x10,0x10,0x10,0x7C,0x00,0x00,/*"i",73*/
0x00,0x00,0x00,0x0C,0x0C,0x00,0x00,0x1C,0x04,0x04,0x04,0x04,0x04,0x04,0x44,0x78,/*"j",74*/
0x00,0x00,0x00,0x00,0xC0,0x40,0x40,0x4E,0x48,0x50,0x70,0x48,0x44,0xEE,0x00,0x00,/*"k",75*/
0x00,0x00,0x00,0x10,0x70,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x7C,0x00,0x00,/*"l",76*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x49,0x49,0x49,0x49,0x49,0xED,0x00,0x00,/*"m",77*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xDC,0x62,0x42,0x42,0x42,0x42,0xE7,0x00,0x00,/*"n",78*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3C,0x42,0x42,0x42,0x42,0x42,0x3C,0x00,0x00,/*"o",79*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xD8,0x64,0x42,0x42,0x42,0x64,0x58,0x40,0xE0,/*"p",80*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1A,0x26,0x42,0x42,0x42,0x26,0x1A,0x02,0x07,/*"q",81*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xEE,0x32,0x20,0x20,0x20,0x20,0xF8,0x00,0x00,/*"r",82*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3E,0x42,0x40,0x3C,0x02,0x42,0x7C,0x00,0x00,/*"s",83*/
0x00,0x00,0x00,0x00,0x00,0x10,0x10,0x7C,0x10,0x10,0x10,0x10,0x12,0x0C,0x00,0x00,/*"t",84*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC6,0x42,0x42,0x42,0x42,0x46,0x3B,0x00,0x00,/*"u",85*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xEE,0x44,0x44,0x28,0x28,0x10,0x10,0x00,0x00,/*"v",86*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xDB,0x89,0x4A,0x5A,0x54,0x24,0x24,0x00,0x00,/*"w",87*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x76,0x24,0x18,0x18,0x18,0x24,0x6E,0x00,0x00,/*"x",88*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE7,0x42,0x24,0x24,0x18,0x18,0x10,0x10,0x60,/*"y",89*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x7E,0x44,0x08,0x10,0x10,0x22,0x7E,0x00,0x00,/*"z",90*/
0x00,0x03,0x04,0x04,0x04,0x04,0x04,0x04,0x08,0x04,0x04,0x04,0x04,0x04,0x03,0x00,/*"{",91*/
0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,/*"|",92*/
0x00,0xC0,0x20,0x20,0x20,0x20,0x20,0x20,0x10,0x20,0x20,0x20,0x20,0x20,0xC0,0x00,/*"}",93*/
0x20,0x5A,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*"~",94*/
};

static const font_chinese_t chinese_font[] =
{
    {
        .name = "\xE5\xBD\x93",  // 当
        .model = (const uint8_t[]) {
            0x01,0x00,0x21,0x08,0x11,0x08,0x09,0x10,0x09,0x20,0x01,0x00,0x7F,0xF8,0x00,0x08,
            0x00,0x08,0x00,0x08,0x3F,0xF8,0x00,0x08,0x00,0x08,0x00,0x08,0x7F,0xF8,0x00,0x08,
        },
    },
    {
        .name = "\xE5\x89\x8D",   // 前
        .model = (const uint8_t[]) {
            0x10,0x10,0x08,0x10,0x08,0x20,0xFF,0xFE,0x00,0x00,0x3E,0x08,0x22,0x48,0x22,0x48,
            0x3E,0x48,0x22,0x48,0x22,0x48,0x3E,0x48,0x22,0x08,0x22,0x08,0x2A,0x28,0x24,0x10,
        },
    },
    {
        .name = "\xE6\x97\xB6",   // 时
        .model = (const uint8_t[]) {
            0x00,0x08,0x00,0x08,0x7C,0x08,0x44,0x08,0x45,0xFE,0x44,0x08,0x44,0x08,0x7C,0x08,
            0x44,0x88,0x44,0x48,0x44,0x48,0x44,0x08,0x7C,0x08,0x44,0x08,0x00,0x28,0x00,0x10,
        },
    },
    {
        .name = "\xE9\x97\xB4",   // 间
        .model = (const uint8_t[]) {
            0x20,0x00,0x13,0xFC,0x10,0x04,0x40,0x04,0x47,0xC4,0x44,0x44,0x44,0x44,0x44,0x44,
            0x47,0xC4,0x44,0x44,0x44,0x44,0x44,0x44,0x47,0xC4,0x40,0x04,0x40,0x14,0x40,0x08,
        },
    },
};

const font_t font16 =
{
    .size = 16,
    .ascii_model = ascii_model,
    .chinese = chinese_font,
};

3.5. 时间设置

        受限于我现在附近没有STM32F103芯片,还不能完成RTC的实时时间调控模块,所以我退而求其次弄了一个动态显示时间实现。

        整个时间显示功能放在主循环中执行,其基本思路可以概括为三步:首先在指定位置输出固定的中文前缀“当前时间”,然后将时、分、秒格式化为标准的 HH:MM:SS 字符串,最后通过延时和变量自增实现每秒刷新一次显示内容。对应代码如下:

while (1)
{
    // 先显示中文前缀:蓝字白底
    st7789_write_string(
        0, 200,
        "\xE5\xBD\x93\xE5\x89\x8D\xE6\x97\xB6\xE9\x97\xB4 ",   // “当前时间 ”
        blue,
        white,
        &font16
    );

    // 生成当前时间字符串 HH:MM:SS
    sprintf(time_str, "%02d:%02d:%02d", hour, min, sec);

    // 在“当前时间 ”后面继续显示时间
    st7789_write_string(
        72, 200,
        time_str,
        blue,
        white,
        &font16
    );

    // 延时 1 秒
    cpu_delay(1000 * 1000);

    // 时间递增
    sec++;
    if (sec >= 60)
    {
        sec = 0;
        min++;
    }
    if (min >= 60)
    {
        min = 0;
        hour++;
    }
    if (hour >= 24)
    {
        hour = 0;
    }
}

以上便是实现代码的具体部分,完整的实现项目也在对应资源部分放出。

4. 项目文件

#include <stdint.h>          // uint16_t、uint32_t 等标准整数类型
#include <stdio.h>           // sprintf
#include "stm32f4xx.h"       // STM32F4 标准外设库头文件
#include "cpu_delay.h"       // cpu_delay 延时函数
#include "st7789.h"          // ST7789 LCD 驱动
#include "font.h"            // 字体数据 font16 / font32 / font48
#include "image.h"           // 图片数据 image_tv

int main(void)
{
    // ==================== 外设时钟使能 ====================
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);   // 使能 GPIOB 时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE);   // 使能 GPIOC 时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);   // 使能 GPIOE 时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);    // 使能 SPI2 时钟

    // ==================== LCD 初始化 ====================
    st7789_init();                                          // 初始化 LCD

    // ==================== 显示背景图片 ====================
    st7789_draw_image(0, 0, &image_tv);                     // 在左上角显示背景图

    // ==================== 颜色定义 ====================
    const uint16_t white = mkcolor(255, 255, 255);          // 白色背景
    const uint16_t pink  = mkcolor(255, 105, 180);          // 英文粉色字体
    const uint16_t blue  = mkcolor(0, 102, 255);            // 中文/时间蓝色字体

    // ==================== 静态英文显示区域 ====================
    // 给英文区域铺白底,避免背景图影响文字清晰度
    //st7789_fill_color(0, 0, 319, 20, white);

    // 显示英文句子:粉色字,白底
    st7789_write_string(
        0, 0,
        "It's you who healed the sky.",
        pink,
        white,
        &font16
    );

    // ==================== 软件模拟时间初值 ====================
    int hour = 12;                                          // 小时初值
    int min  = 34;                                          // 分钟初值
    int sec  = 50;                                          // 秒钟初值

    // 用于显示“当前时间 ”和“HH:MM:SS”
    char time_str[16];                                      // 保存时间字符串,例如 12:34:50

    // ==================== 主循环:每秒刷新一次时间 ====================
    while (1)
    {
        // 先清除底部时间显示区域,防止文字重影
        //st7789_fill_color(0, 200, 319, 220, white);

        // 先显示中文前缀:蓝字白底
        st7789_write_string(
            0, 200,
            "\xE5\xBD\x93\xE5\x89\x8D\xE6\x97\xB6\xE9\x97\xB4 ",   // “当前时间 ”
            blue,
            white,
            &font16
        );

        // 生成当前时间字符串 HH:MM:SS
        sprintf(time_str, "%02d:%02d:%02d", hour, min, sec);

        // 在“当前时间 ”后面继续显示时间
        // “当前时间 ”共 5 个中文/空格字符:
        // 4 个中文,每个占 font16.size=16 像素;后面空格占 8 像素
        // 所以起始 x 可设为 72 更稳妥
        st7789_write_string(
            72, 200,
            time_str,
            blue,
            white,
            &font16
        );

        // 延时 1 秒
        cpu_delay(1000 * 1000);

        // 时间递增
        sec++;
        if (sec >= 60)
        {
            sec = 0;
            min++;
        }
        if (min >= 60)
        {
            min = 0;
            hour++;
        }
        if (hour >= 24)
        {
            hour = 0;
        }
    }
}

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐