屏幕驱动颜色填充和栈大小问题

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

212.3.11 屏幕驱动颜色填充和栈大小问题

/* lcd.h:本节为新的逐像素区域刷新函数添加声明。 */
void LCD_Color_Fill(u16 sx, u16 sy, u16 ex, u16 ey, lv_color_t *color); /* sx/sy 是起点,ex/ey 是闭区间终点,color 指向连续像素数据。 */

/* lcd.c:课程本节实际加入的 LCD_Color_Fill(),保留配套源码中的变量与写法。 */
void LCD_Color_Fill(u16 sx, u16 sy, u16 ex, u16 ey, lv_color_t *color) /* 接收矩形坐标以及第一个像素的地址。 */
{                                                      /* 开始逐像素区域刷新。 */
    u16 i, j;                                          /* 配套源码保留的局部变量,本函数当前没有使用。 */
    u32 k = 0;                                         /* 配套源码保留的局部变量,本函数当前没有使用。 */
    uint32_t x = 0, y = 0;                             /* x 当前未使用,y 用作像素循环计数器。 */
    u16 height, width;                                 /* 保存闭区间矩形的高度和宽度。 */
    width = ex - sx + 1;                               /* 闭区间宽度必须包含起点和终点。 */
    height = ey - sy + 1;                              /* 闭区间高度同样要加 1。 */
    LCD_Address_Set(sx, sy, ex, ey);                   /* 设置 LCD 写入窗口,并进入显存写入命令。 */
    for(y = 0; y < width * height; y++)                /* 循环次数等于该矩形包含的像素总数。 */
    {                                                  /* 开始发送当前像素。 */
        LCD_WR_DATA(color->full);                      /* 取当前 lv_color_t 的完整 RGB565 值并写入 LCD。 */
        color++;                                       /* 指向下一个 lv_color_t,而不是重复第一个颜色。 */
    }                                                  /* 结束本次像素发送。 */
}                                                      /* 结束逐像素区域刷新。 */

/* lv_port_disp_template.c:刷新回调改用新函数,不再调用旧的纯色 LCD_Fill()。 */
static void disp_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) /* 接收驱动、闭区间矩形和连续像素缓冲区。 */
{                                                      /* 开始显示刷新回调。 */
    LCD_Color_Fill(area->x1, area->y1, area->x2, area->y2, color_p); /* 把矩形坐标和像素数据首地址交给 LCD 驱动。 */
    lv_disp_flush_ready(disp_drv);                     /* 阻塞发送完成后,通知 LVGL 可复用绘制缓冲区。 */
}                                                      /* 结束显示刷新回调。 */

/* startup_stm32f103xe.s:CubeMX 重新生成后保存的栈空间设置。 */
Stack_Size     EQU     0x700                           ; 将主栈由 0x400 字节增至 0x700 字节。

本节源码边界: LCD_Color_Fill()、对应头文件声明、disp_flush() 的调用替换,以及栈从 0x400 调到 0x700,都是本课实际完成的修改。源码中的 i、j、k、x 虽然未使用,仍按课程配套源码保留,没有擅自优化。后续课程才移植触摸输入和继续优化显示速度,本节不提前加入。

一、颜色填充问题查找与修改

上一课的 stress demo 已经能够运动,但颜色明显错误。问题最终定位在 lv_port_disp_template.c 的 disp_flush():第 210 课把 LVGL 的 color_p 当成“一个颜色的地址”,并复用了原来用于纯色清屏的 LCD_Fill();实际上,color_p 指向的是本次刷新矩形内的一串像素。

两种接口的语义完全不同:

接口第 5 个参数区域内的像素指针是否前进
旧 LCD_Fill()一个纯色值,后来被临时改成指针每个位置重复同一种颜色不前进
LVGL disp_flush()区域像素数组的首地址每个像素可以有不同颜色每发送一个像素后前进

例如缓冲区连续存放蓝、红、红、绿、黄五个像素时,正确逻辑是:

color ──► [蓝][红][红][绿][黄]
           ↑  第一次发送后 color++
              ↑  第二次发送后 color++
                 ↑  依次处理全部像素

上一课的实现没有 color_p++,无论循环多少次都只读取第一个单元,因此能正确刷白屏或黑屏,却无法显示由多种颜色组成的控件和动画。这也说明:纯色测试通过,只能验证 SPI 能反复发送一个颜色,不能验证 LVGL 区域刷新接口正确。

本课的排查过程经历了先怀疑其他移植步骤、再回到刷新接口比较数据语义的过程。课程还参考了题为“关于驱动中景园 LCD 和 LVGL 踩的一些坑”的网络文章,随后把其中适合当前驱动的区域写入思路移入工程。精校结论以课程最终源码及 LVGL v8.3 的刷新规则为准,而不是只凭文章描述。

1.1 驱动中景园LCD和LVGL踩的一些坑

课程使用的是中景园屏幕配套驱动。旧接口 LCD_Fill() 的用途是:给出矩形起止坐标和一个 RGB565 颜色,把整个区域填成同一个颜色。这对清屏、画纯色矩形很合适,却不能直接消费 LVGL 输出的多像素缓冲区。

本课没有继续硬改旧函数,而是在 lcd.c 顶部新增 LCD_Color_Fill(),同时在 lcd.h 中增加同名声明。旧 LCD_Fill() 仍保留在配套源码里,其他需要纯色填充的代码仍可能使用它;LVGL 的 disp_flush() 则改为调用新接口。

LCD_Color_Fill() 这个名字容易让初学者误以为它仍是“用一个颜色填满区域”。应按实现理解:它实际上是把一块连续的颜色数组逐像素写入指定矩形。

新函数的处理顺序为:

计算闭区间宽、高
→ LCD_Address_Set() 设置 ST7796 写入窗口
→ 读取 color->full
→ LCD_WR_DATA() 发送一个 16 位 RGB565 像素
→ color++ 指向下一个像素
→ 重复 width × height 次

本工程配置 LV_COLOR_DEPTH == 16,所以 lv_color_t 的当前有效类型含有 16 位 full 成员,一个元素占 2 字节。color++ 按指针类型自动前进 sizeof(lv_color_t),在这里就是移动到后续 2 字节像素,而不是只给数值地址加 1 字节。

1.2 颜色填充问题修改

LVGL 的 lv_area_t 使用闭区间坐标,x2、y2 所指的最后一列和最后一行都属于刷新区域。因此:

[ width=ex-sx+1 ]

[ height=ey-sy+1 ]

[ pixel_count=width\times height ]

例如区域 (0,0)~(479,9) 的宽度为 480、高度为 10,共 4800 个像素。上一课旧函数使用 (xend-xsta)*(yend-ysta),同时把窗口终点设成 xend-1/yend-1,那是右开区间接口,不能直接接收 LVGL 的闭区间坐标。

新函数直接执行:

LCD_Address_Set(sx, sy, ex, ey);

LCD_Address_Set() 向 ST7796 发送列地址、行地址和显存写入命令,使后续像素按控制器设定的窗口连续写入。接着每轮调用 LCD_WR_DATA(color->full):课程驱动会先发送 16 位值的高字节,再发送低字节,正好把一个 RGB565 像素送到 LCD。

刷新回调由:

LCD_Fill(area->x1, area->y1, area->x2, area->y2, color_p);

替换为:

LCD_Color_Fill(area->x1, area->y1, area->x2, area->y2, color_p);

由于当前函数使用阻塞式写入,返回时全部像素已经发送完成,所以紧接着调用 lv_disp_flush_ready(disp_drv) 是正确时机。如果以后改为异步 DMA,则应等 DMA 真正结束后再通知 LVGL,不能在刚启动传输时提前调用。

修改声明、实现和调用点后重新编译,工程没有 error;烧录后 stress demo 的控件颜色与课程开头的预期画面基本一致,证明多颜色区域刷新问题已经修正。

但运行到固定位置时,动画仍会停止。复位后可以重新开始,并在相近位置再次卡住。这种“输入规模或调用路径达到某处才稳定复现”的现象提示另有运行期资源问题,而不是颜色接口仍然错误。

二、修改栈大小

课程把卡死原因定位为主栈空间不足。裸机工程没有为每个任务建立独立栈,main() 调用链和中断上下文主要使用 Cortex-M3 的主栈(MSP)。栈常用于保存:

  • 函数调用的返回地址和需要保护的寄存器;
  • 无法全部放进寄存器的参数或中间值;
  • 非静态局部变量以及编译器产生的临时数据;
  • 中断进入时由硬件自动压栈的现场。

“所有参数和返回值都保存在栈中”并不严谨。ARM 调用约定会优先用寄存器传递若干参数和返回值,只有超出寄存器容量、发生寄存器溢出或需要保存现场时才会使用栈。函数嵌套越深、局部对象越大、中断嵌套越多,峰值栈需求通常越高。

默认启动文件中:

Stack_Size     EQU     0x400

0x400 是十六进制字节数:

[ 0x400=1024\text{ B}=1\text{ KiB} ]

课程在 STM32CubeMX 的 Project Manager → Project → Minimum Stack Size 中把它改为:

0x700 = 1792 B = 1.75 KiB

然后重新生成代码,使 .ioc 中保存:

ProjectManager.StackSize=0x700

生成后的 startup_stm32f103xe.s 同步变成 Stack_Size EQU 0x700。优先修改 .ioc 的理由是:直接编辑启动汇编文件虽然当次有效,但下次由 CubeMX 重新生成工程时可能恢复为配置文件中的旧值。

烧录修改后的程序,stress demo 越过了原先固定卡死的位置,并继续显示新的动画和图表。这个对照实验支持“原 1 KiB 栈在该 demo 路径上不足”的判断。不过课程没有使用栈填充、高水位统计或 HardFault 寄存器进一步测量,所以 0x700 应理解为本工程实测可运行的值,而不是所有 LVGL 项目的通用最小栈。

栈占用的是 SRAM,不是 Flash

原视频口述“Flash 只有 64 KB”有两个错误:

  1. 本工程目标器件是 STM32F103ZE,Keil 工程配置为 512 KiB Flash、64 KiB SRAM;
  2. 启动文件的 STACK, NOINIT, READWRITE 区域分配在 SRAM 中,增大 Stack_Size 消耗的是 SRAM,不是 Flash。

本课最终构建的链接结果可以直接验证内存压力:

SRAM 项目大小来源
LVGL 10 行绘制缓冲 buf_19600 B480×10×2
LVGL 内部内存池 work_mem_int49152 BLV_MEM_SIZE = 48×1024
启动文件 Heap512 BHeap_Size = 0x200
主栈 Stack_Mem1792 BStack_Size = 0x700
最终 RW-data + ZI-data 合计62904 B链接 map 文件

器件总 SRAM 为 65536 B,链接后只余:

[ 65536-62904=2632\text{ B} ]

这个余量还要容纳链接时已计入布局但运行时会共同竞争的各类数据边界,因此不能无依据地持续放大栈。若再增加静态数组、绘制缓冲或 LVGL 内存池,链接器可能直接报告 RAM 溢出;即使能链接,也应通过最坏调用路径测试和栈水位测量确定安全裕量。

还要区分三个名字相近但用途不同的区域:

区域本工程用途
主栈 Stack_Mem函数调用、局部变量和中断现场
启动文件 Heap_MemC 运行库动态分配预留区,本工程为 512 B
LVGL work_mem_intLV_MEM_CUSTOM == 0 时 LVGL 自己管理的 48 KiB 内存池

把栈增大只能解决栈不足,不能替代 LVGL 内存池,也不能修复数组越界、非法指针或刷新接口错误。实际调试应先观察 HardFault、调用栈、map 文件和可重复触发条件,再决定修改哪一类内存。

当前显示仍然较慢

增大栈后动画不再在原位置卡死,但视频明确观察到刷新速度仍慢。当前 LCD_Color_Fill() 对每个像素调用一次 LCD_WR_DATA(),后者又逐字节调用阻塞式 HAL_SPI_Transmit(),而且 LCD_Writ_Bus() 每发一个字节都会切换片选;一个 4800 像素的 10 行块因此产生大量小传输和函数调用。

本课只定位现象并预告以后优化,没有在这里改成批量 SPI 或 DMA。单纯把绘制缓冲改大也不保证成比例提速:当前底层仍是同步阻塞发送;双缓冲只有配合异步传输,让“LVGL 绘制下一块”和“硬件发送上一块”并行时才更有价值。

三、color_p指针讲解

color_p 的准确含义是:指向本次 area 刷新区域第一个 lv_color_t 像素的指针。它通常指向 LVGL 当前绘制缓冲中的某一段,不是永久保存完整屏幕的独立“显卡显存”。

本工程使用单个 10 行缓冲:

static lv_color_t buf_1[MY_DISP_HOR_RES * 10];

在 480 像素宽、RGB565 每像素 2 字节时,它占 9600 B SRAM。若待更新区域更大,LVGL 会拆成能放入该缓冲区的小块,逐块调用 disp_flush();一次刷新完成并调用 lv_disp_flush_ready() 后,同一块 RAM 会被后续绘制复用。

对宽度为 (w)、高度为 (h) 的区域,指针变化可以表示为:

[ color_p, color_p+1, color_p+2, \ldots, color_p+(w\times h-1) ]

每个位置保存一个 RGB565 像素。color->full 读取当前像素的 16 位完整值,color++ 再移动到下一个元素。因此新函数能够依次写出蓝、红、绿等不同像素,而旧函数只会不断重复首像素。

电脑显卡显存的类比只能帮助理解“绘图需要中间存储”,不能直接套用:

  • 本工程没有独立 GPU 显存,绘制缓冲位于 MCU 的普通 SRAM;
  • 它只有 10 行,不保存完整画面;
  • 缓冲更大通常能减少分块次数,但性能还受 SPI 带宽、每次调用开销和是否采用 DMA 影响;
  • 两个 480×320 RGB565 全屏缓冲需要 600 KiB,远超本机 64 KiB SRAM,不能采用模板的全屏双缓冲方案。

本课完成后的准确状态为:

disp_flush() 已改用 LCD_Color_Fill()
→ LVGL 区域内的 RGB565 像素能够逐个正确发送
→ stress demo 颜色恢复正常
→ 主栈由 1 KiB 增至 1.75 KiB
→ demo 不再在原固定位置卡死
→ 刷新速度仍慢,尚未优化
→ LVGL 触摸输入尚未移植,下一课继续