大家好,我是良许。
在嵌入式开发中,特别是 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 开发,在实际项目中做出优秀的人机交互界面。
更多编程学习资源