STM32开发如何设计界面,怎么做GUI?

4 阅读15分钟

大家好,我是良许。

在嵌入式开发中,特别是 STM32 项目里,我们经常需要为设备添加人机交互界面。

无论是工业控制设备的操作面板,还是智能家居的触摸屏,GUI(图形用户界面)的设计都是绕不开的话题。

今天我就结合自己多年的嵌入式开发经验,和大家聊聊 STM32 上如何设计界面、做 GUI 开发。

1. STM32 GUI 开发的硬件基础

在开始 GUI 开发之前,我们需要先了解硬件配置。

STM32 做 GUI 开发,核心硬件就是显示屏。

1.1 常见的显示屏类型

在 STM32 项目中,我们常用的显示屏主要有这几种:

OLED 屏幕:功耗低,对比度高,常见的有 0.96 寸、1.3 寸等小尺寸屏幕,分辨率一般是 128x64 或 128x128。

这种屏幕适合做一些简单的信息显示,比如智能手表、便携设备等。

它通过 I2C 或 SPI 接口与 STM32 通信,驱动相对简单。

TFT LCD 屏幕:色彩丰富,尺寸选择多,从 2.4 寸到 7 寸都有,分辨率从 240x320 到 800x480 不等。

这是做彩色 GUI 的主流选择,适合需要复杂界面的应用场景。

常见的驱动芯片有 ILI9341、ST7789 等,通过 SPI 或并口(8080、RGB 接口)与 STM32 连接。

电容触摸屏:很多 TFT LCD 会配备电容触摸功能,触摸芯片常见的有 FT6236、GT911 等,通过 I2C 接口读取触摸坐标。

有了触摸功能,用户交互体验会提升很多。

1.2 STM32 芯片的选择

做 GUI 开发,STM32 的选型很重要。

如果只是显示一些简单的文字和图标,STM32F103 这样的入门级芯片就够用了。

但如果要做复杂的彩色界面,特别是带动画效果的,建议选择性能更强的芯片:

STM32F4 系列:主频可达 180MHz,带 FPU(浮点运算单元),SRAM 充足,非常适合 GUI 开发。

F429 还集成了 LCD-TFT 控制器和色度控制器(Chrom-ART),可以硬件加速图形操作。

STM32F7 系列:主频达到 216MHz,性能更强,同样带有 LCD-TFT 控制器。

STM32H7 系列:这是目前 STM32 的高性能系列,主频达到 480MHz 甚至 550MHz,带有双核,内存也更大,适合做高端 GUI 应用。

选择芯片时,除了看主频,还要关注 SRAM 和 Flash 的大小。

GUI 开发很吃内存,特别是显示图片时,一张 240x320 的 16 位色图片就需要 150KB 的显存空间。

2. STM32 GUI 开发的软件方案

硬件准备好后,接下来就是软件开发了。

STM32 上做 GUI,有多种方案可选。

2.1 自己写底层驱动

对于简单的应用,我们可以自己编写显示驱动和 GUI 代码。

这种方式最灵活,代码量可控,适合资源受限的场景。

以 OLED 屏幕为例,我们需要先初始化 I2C 或 SPI 接口,然后编写 OLED 的初始化序列和基本绘图函数:

// OLED初始化(以SSD1306为例)
void OLED_Init(void)
{
    HAL_Delay(100);
    
    OLED_WriteCmd(0xAE); // 关闭显示
    OLED_WriteCmd(0x20); // 设置内存地址模式
    OLED_WriteCmd(0x10); // 水平地址模式
    OLED_WriteCmd(0xB0); // 设置页地址
    OLED_WriteCmd(0xC8); // 设置COM扫描方向
    OLED_WriteCmd(0x00); // 设置低列地址
    OLED_WriteCmd(0x10); // 设置高列地址
    OLED_WriteCmd(0x40); // 设置起始行地址
    OLED_WriteCmd(0x81); // 设置对比度
    OLED_WriteCmd(0xFF);
    OLED_WriteCmd(0xA1); // 设置段重映射
    OLED_WriteCmd(0xA6); // 正常显示
    OLED_WriteCmd(0xA8); // 设置多路复用比
    OLED_WriteCmd(0x3F);
    OLED_WriteCmd(0xA4); // 全局显示开启
    OLED_WriteCmd(0xD3); // 设置显示偏移
    OLED_WriteCmd(0x00);
    OLED_WriteCmd(0xD5); // 设置时钟分频
    OLED_WriteCmd(0xF0);
    OLED_WriteCmd(0xD9); // 设置预充电周期
    OLED_WriteCmd(0x22);
    OLED_WriteCmd(0xDA); // 设置COM引脚配置
    OLED_WriteCmd(0x12);
    OLED_WriteCmd(0xDB); // 设置VCOMH电压倍率
    OLED_WriteCmd(0x20);
    OLED_WriteCmd(0x8D); // 使能充电泵
    OLED_WriteCmd(0x14);
    OLED_WriteCmd(0xAF); // 开启显示
    
    OLED_Clear();
}
​
// 画点函数
void OLED_DrawPoint(uint8_t x, uint8_t y, uint8_t color)
{
    uint8_t page = y / 8;
    uint8_t bit = y % 8;
    
    if(color)
        OLED_GRAM[page][x] |= (1 << bit);
    else
        OLED_GRAM[page][x] &= ~(1 << bit);
}
​
// 显示字符
void OLED_ShowChar(uint8_t x, uint8_t y, char chr)
{
    uint8_t i;
    chr = chr - ' '; // 得到偏移后的值
    
    for(i = 0; i < 8; i++)
    {
        OLED_GRAM[y][x + i] = ASCII_8x16[chr * 16 + i];
    }
    for(i = 0; i < 8; i++)
    {
        OLED_GRAM[y + 1][x + i] = ASCII_8x16[chr * 16 + i + 8];
    }
}

对于 TFT LCD 屏幕,驱动会更复杂一些,但原理类似。

我们需要实现画点、画线、画矩形、显示字符、显示图片等基本功能。

这些函数构成了 GUI 的基础。

2.2 使用开源 GUI 库

如果项目需要更复杂的界面效果,自己从零写 GUI 会非常耗时。

这时候使用开源 GUI 库是更好的选择。

LVGL(Light and Versatile Graphics Library):这是目前最流行的嵌入式 GUI 库之一,完全开源免费,功能强大,支持各种控件(按钮、滑块、图表等),还支持动画效果。

LVGL 的优点是资源占用相对较小,文档完善,社区活跃。

很多 STM32 项目都在用 LVGL。

使用 LVGL 的基本流程是这样的:

// 1. 初始化LVGL
lv_init();
​
// 2. 初始化显示驱动
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf1[DISP_HOR_RES * 10];
lv_disp_draw_buf_init(&draw_buf, buf1, NULL, DISP_HOR_RES * 10);
​
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.draw_buf = &draw_buf;
disp_drv.flush_cb = disp_flush; // 显示刷新回调函数
disp_drv.hor_res = DISP_HOR_RES;
disp_drv.ver_res = DISP_VER_RES;
lv_disp_drv_register(&disp_drv);
​
// 3. 初始化输入设备(触摸屏)
static lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = touchpad_read; // 触摸读取回调函数
lv_indev_drv_register(&indev_drv);
​
// 4. 创建界面元素
lv_obj_t *btn = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn, 120, 50);
lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0);
​
lv_obj_t *label = lv_label_create(btn);
lv_label_set_text(label, "Click me!");
lv_obj_center(label);
​
// 5. 主循环中调用
while(1)
{
    lv_timer_handler();
    HAL_Delay(5);
}

LVGL 需要我们实现两个关键的回调函数:显示刷新函数和触摸读取函数。

显示刷新函数负责把 LVGL 的显存数据传输到 LCD 屏幕上,触摸读取函数负责读取触摸坐标并返回给 LVGL。

// 显示刷新回调函数
void disp_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p)
{
    int32_t x, y;
    
    // 设置显示窗口
    LCD_SetWindow(area->x1, area->y1, area->x2, area->y2);
    
    // 写入像素数据
    for(y = area->y1; y <= area->y2; y++)
    {
        for(x = area->x1; x <= area->x2; x++)
        {
            LCD_WriteData(color_p->full);
            color_p++;
        }
    }
    
    // 通知LVGL刷新完成
    lv_disp_flush_ready(disp_drv);
}
​
// 触摸读取回调函数
void touchpad_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data)
{
    static int16_t last_x = 0;
    static int16_t last_y = 0;
    
    if(Touch_Scan())
    {
        data->state = LV_INDEV_STATE_PRESSED;
        data->point.x = Touch_GetX();
        data->point.y = Touch_GetY();
        last_x = data->point.x;
        last_y = data->point.y;
    }
    else
    {
        data->state = LV_INDEV_STATE_RELEASED;
        data->point.x = last_x;
        data->point.y = last_y;
    }
}

emWin(现在叫 SEGGER emWin):这是 SEGGER 公司开发的商业 GUI 库,功能非常强大,性能优秀,支持各种高级特性。

ST 官方的 TouchGFX 就是基于 emWin 开发的。

emWin 的缺点是商业授权需要付费,但对于个人学习和非商业项目,可以使用免费版本。

uGUI:这是一个非常轻量级的 GUI 库,代码量很小,适合资源非常受限的场景。

它的功能相对简单,但对于一些基本的界面需求已经足够了。

TouchGFX:这是 ST 官方推出的 GUI 开发工具,专门为 STM32 优化,可以充分利用 STM32 的硬件加速功能。

TouchGFX 提供了图形化的界面设计工具,可以像做网页一样拖拽控件来设计界面,然后自动生成代码。

对于不想写太多 GUI 代码的开发者来说,这是个不错的选择。

2.3 使用 STM32CubeMX 配合 TouchGFX

如果你的项目使用 STM32F4、F7 或 H7 系列芯片,强烈推荐使用 STM32CubeMX 配合 TouchGFX 来开发 GUI。

这套工具链非常成熟,开发效率很高。

具体流程是这样的:

第一步,在 STM32CubeMX 中配置芯片的时钟、外设等基本参数,然后在 Additional Software 中选择 TouchGFX。

第二步,配置 LCD 接口。

如果使用的是带 LCD-TFT 控制器的芯片(如 F429、F746),可以直接配置 LTDC 外设。

如果使用 SPI 接口的 LCD,需要配置 SPI 和 DMA。

第三步,生成代码后,在 TouchGFX Designer 中设计界面。

TouchGFX Designer 是一个可视化的界面设计工具,你可以在里面拖拽各种控件,设置控件的属性、位置、动画效果等。

设计完成后,TouchGFX 会自动生成对应的 C++ 代码。

第四步,在生成的代码中添加业务逻辑。

比如按钮点击事件的处理、数据的更新显示等。

这种方式的优点是开发效率高,界面效果好,而且可以充分利用 STM32 的硬件加速功能。

缺点是生成的代码比较复杂,调试起来不如自己写的代码直观。

3. GUI 开发的关键技术点

无论使用哪种方案,GUI 开发都有一些共同的技术点需要掌握。

3.1 显存管理

GUI 开发最大的挑战之一就是内存管理。

一个彩色显示屏的显存占用是很大的。

比如一个 320x240 的 16 位色屏幕,完整的显存需要 320 * 240* 2 = 153600 字节,也就是 150KB。

而 STM32F103 的 SRAM 只有 20KB,根本放不下。

解决方案有几种:

使用外部 SRAM 或 SDRAM:对于高端的 STM32 芯片(如 F429、F746),可以外挂 SRAM 或 SDRAM 来扩展内存。

这样就可以有足够的空间存放显存了。

使用双缓冲或局部刷新:如果内存不够,可以只分配一部分内存作为缓冲区,每次只刷新屏幕的一部分。

LVGL 就是采用这种方式,它可以配置缓冲区大小,比如只用屏幕十分之一的内存作为缓冲。

直接写屏:对于简单的应用,可以不使用显存,直接把数据写到 LCD。

这种方式的缺点是刷新速度慢,而且容易出现闪烁。

3.2 刷新优化

GUI 的流畅度很大程度上取决于刷新速度。

优化刷新有几个技巧:

使用 DMA 传输:在向 LCD 传输数据时,使用 DMA 可以大大提高传输速度,而且不占用 CPU 时间。

HAL 库提供了 DMA 的接口,使用起来很方便。

// 使用DMA传输数据到LCD
HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)color_buffer, buffer_size);

局部刷新:不要每次都刷新整个屏幕,只刷新变化的区域。

LVGL 等 GUI 库都支持局部刷新,可以大大减少数据传输量。

使用硬件加速:如果使用的是 F429、F746 等带有 Chrom-ART 加速器的芯片,可以利用硬件加速来进行图形操作,比如矩形填充、图像拷贝等。

这比 CPU 软件实现快很多。

3.3 字体显示

中文字体是 GUI 开发的一个难点。

一个完整的中文字库(GB2312)包含 6763 个汉字,如果使用 16x16 点阵,需要 6763*32 = 216416 字节,也就是 200 多 KB。

这对于 Flash 容量有限的 STM32 来说是个不小的负担。

解决方案有几种:

使用外部 Flash 存储字库:可以把字库存储在外部 SPI Flash 中,需要显示时再读取。

这样不占用芯片内部 Flash。

只包含常用汉字:如果界面上的文字是固定的,可以只提取需要用到的汉字,生成一个小字库。

这样可以大大减少字库大小。

使用矢量字体:LVGL 支持 FreeType 字体,可以使用 TTF 字体文件。

矢量字体的优点是可以任意缩放,而且文件相对较小。

缺点是渲染速度慢,需要较强的 CPU 性能。

3.4 触摸处理

如果使用触摸屏,触摸处理也是一个重要环节。

触摸芯片一般通过 I2C 接口与 STM32 通信,我们需要定期读取触摸坐标。

// 读取触摸坐标(以FT6236为例)
uint8_t Touch_Scan(void)
{
    uint8_t buf[4];
    uint8_t touch_num;
    
    // 读取触摸点数量
    HAL_I2C_Mem_Read(&hi2c1, FT6236_ADDR, 0x02, I2C_MEMADD_SIZE_8BIT, &touch_num, 1, 100);
    
    if(touch_num > 0)
    {
        // 读取第一个触摸点的坐标
        HAL_I2C_Mem_Read(&hi2c1, FT6236_ADDR, 0x03, I2C_MEMADD_SIZE_8BIT, buf, 4, 100);
        
        touch_x = ((buf[0] & 0x0F) << 8) | buf[1];
        touch_y = ((buf[2] & 0x0F) << 8) | buf[3];
        
        return 1;
    }
    
    return 0;
}

触摸处理还需要考虑去抖动、多点触摸、手势识别等问题。

LVGL 等 GUI 库已经内置了这些功能,我们只需要提供原始的触摸坐标即可。

4. 实战案例:制作一个简单的温度显示界面

最后,我们来做一个实战案例,制作一个简单的温度显示界面。

假设我们使用 STM32F103 配合一个 2.4 寸的 TFT LCD(ILI9341 驱动芯片),通过 SPI 接口连接。

界面上显示当前温度值,以及一个温度曲线图。

首先,我们需要初始化 LCD 和 LVGL:

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    
    // 初始化外设
    MX_GPIO_Init();
    MX_SPI1_Init();
    MX_TIM2_Init();
    
    // 初始化LCD
    LCD_Init();
    
    // 初始化LVGL
    lv_init();
    
    // 配置显示驱动
    static lv_disp_draw_buf_t draw_buf;
    static lv_color_t buf1[240 * 10];
    lv_disp_draw_buf_init(&draw_buf, buf1, NULL, 240 * 10);
    
    static lv_disp_drv_t disp_drv;
    lv_disp_drv_init(&disp_drv);
    disp_drv.draw_buf = &draw_buf;
    disp_drv.flush_cb = disp_flush;
    disp_drv.hor_res = 240;
    disp_drv.ver_res = 320;
    lv_disp_drv_register(&disp_drv);
    
    // 创建界面
    create_ui();
    
    // 启动定时器,定期更新温度
    HAL_TIM_Base_Start_IT(&htim2);
    
    while(1)
    {
        lv_timer_handler();
        HAL_Delay(5);
    }
}

然后创建界面元素:

lv_obj_t *temp_label;
lv_obj_t *chart;
lv_chart_series_t *ser;
​
void create_ui(void)
{
    // 创建温度显示标签
    temp_label = lv_label_create(lv_scr_act());
    lv_label_set_text(temp_label, "Temperature: --°C");
    lv_obj_set_style_text_font(temp_label, &lv_font_montserrat_24, 0);
    lv_obj_align(temp_label, LV_ALIGN_TOP_MID, 0, 20);
    
    // 创建图表
    chart = lv_chart_create(lv_scr_act());
    lv_obj_set_size(chart, 220, 150);
    lv_obj_align(chart, LV_ALIGN_BOTTOM_MID, 0, -20);
    lv_chart_set_type(chart, LV_CHART_TYPE_LINE);
    lv_chart_set_range(chart, LV_CHART_AXIS_PRIMARY_Y, 0, 50);
    lv_chart_set_point_count(chart, 20);
    
    // 添加数据系列
    ser = lv_chart_add_series(chart, lv_palette_main(LV_PALETTE_RED), LV_CHART_AXIS_PRIMARY_Y);
}

最后,在定时器中断中更新温度数据:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if(htim->Instance == TIM2)
    {
        // 读取温度传感器(这里用随机数模拟)
        float temperature = 20.0 + (rand() % 100) / 10.0;
        
        // 更新标签
        char buf[32];
        sprintf(buf, "Temperature: %.1f°C", temperature);
        lv_label_set_text(temp_label, buf);
        
        // 更新图表
        lv_chart_set_next_value(chart, ser, (int32_t)temperature);
    }
}

这个案例展示了 GUI 开发的基本流程:初始化硬件和 GUI 库,创建界面元素,然后在主循环或中断中更新数据。

虽然代码不多,但已经实现了一个功能完整的温度监控界面。

5. 总结与建议

STM32 的 GUI 开发是一个系统工程,涉及硬件选型、软件架构、性能优化等多个方面。

对于初学者,我的建议是:

第一,从简单的开始。

先用 OLED 屏幕或小尺寸的 TFT LCD 练手,熟悉基本的显示原理和驱动方法。

不要一上来就想做复杂的界面。

第二,善用开源库。

LVGL 这样的开源 GUI 库已经非常成熟,功能强大,没必要什么都自己写。

把精力放在业务逻辑和用户体验上,而不是重复造轮子。

第三,注意性能优化。

GUI 开发很容易遇到性能瓶颈,要学会使用 DMA、硬件加速等技术,合理管理内存,优化刷新逻辑。

第四,多看示例代码。

无论是官方的例程,还是开源项目,都是很好的学习资源。

看懂别人的代码,理解设计思路,比自己摸索要快得多。

GUI 开发是嵌入式开发中很有意思的一个方向,做出一个漂亮流畅的界面,成就感是很强的。

希望这篇文章能帮助你入门 STM32 的 GUI 开发,在实际项目中做出优秀的人机交互界面。

更多编程学习资源