LCD显示中的SPI瓶颈分析与刷新率优化 | 技术集结

描述

本项目为LCD显示中的SPI瓶颈分析与刷新率优化。目前,恩智浦已有多款产品对RT-Thread完成了适配。近期,MCX A 系列产品的重要成员,FRDM-MCXA366也完成了适配,并在社区开发者的协作下完成了电子书《恩智浦FRDM-MCX A366开发实践指南》https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/tutorial/make-bsp/MCX-A366/%E6%81%A9%E6%99%BA%E6%B5%A6FRDM-MCXA366%E5%AE%9E%E8%B7%B5%E6%8C%87%E5%8D%97

目录


 

项目概述
 


 

硬件连接


 

软件架构


 

关键代码


 

性能测试数据(实测)


 

踩过的坑


 

MSH 命令行验证


 

项目目录


 

心得与总结


 

附录:源代码


 

《恩智浦FRDM-MCX A366开发实践指南》贡献名单

SPI

实物运行:FRDM-MCXA156 + 2.4” ILI9341 LCD。屏幕上同时显示标题栏、运行时间(1h23m)、内存占用、Tick 计数、基准测试结果(Yld:6us / Mem:72us / Fill:460ms),底部 4 个彩色小球反弹动画,板子上红色 LED 心跳闪烁。

1 项目概述
 

项目名称:基于 RT-Thread 的 FRDM-MCXA156 多线程 LCD 显示与性能测试 Demo

硬件平台:NXP FRDM-MCXA156 开发板(主控 MCXA156,Arm Cortex-M33)

软件平台:RT-Thread 5.3.0 RTOS

显示外设:2.4 寸 ILI9341 SPI LCD(240×320 像素,16 位色,正点原子 ATK-MD0240 V1.2)

开发工具:RT-Thread Studio + scons 构建系统 + pyocd 烧录(CMSIS-DAP / MCU-Link)

本项目在 NXP FRDM-MCXA156 开发板上完成 RT-Thread RTOS 移植,外接 2.4 寸 ILI9341 LCD,通过 GPIO 直接寄存器写实现高速软件 SPI,并使用多线程实现实时系统信息显示、性能测试结果展示、动画演示与 LED 心跳指示。重点验证了 RT-Thread 在 MCXA156 平台上的多线程能力,并通过 GPIO 操作优化实现了 10.1× 的 SPI 吞吐量提升(同板实测)。

2 硬件连接
 

LCD 外接到 FRDM-MCXA156 的 FlexIO/LCD 排针:

SPI

板载 LED:P3_12(板载三色 LED 中的红色一路)。

引脚定义见 drv_ili9341.c 顶部宏。

3 软件架构
 

3.1 线程划分

SPI

多线程证据:ps 命令可一次性看到 6 个线程并发运行。

SPI

3.2 屏幕布局(240×320 竖屏)

  •  

┌────────────────┐ y=0│  RT-Thread v5.3│ ← 蓝底白字 / 黄字标题栏│  FRDM-MCXA156  │├────────────────┤ y=36│ Up00:05    │ ← info 线程实时刷新│ Mem:118KB      ││ Tick:512       │├════════════════┤ y=92  白色分隔线│ Benchmark:     │ ← 启动时跑一次的基准(静态显示)│ Yld:6us        ││ Mem:72us       ││ Fill:460ms     │├════════════════┤ y=168 白色分隔线│                ││   • • • •      │ ← anim 线程动画区│   (4 个小球反弹)││                │└────────────────┘ y=320
 

4 关键代码
 

4.1 多线程创建(main.c)

  •  
  •  
  •  
  •  
  •  
  •  
  •  

rt_thread_t t1 = rt_thread_create("info", info_thread_entry, RT_NULL, 1024, 12, 10);rt_thread_t t2 = rt_thread_create("anim", anim_thread_entry, RT_NULL, 1024, 13, 10);rt_thread_t t3 = rt_thread_create("led",  led_thread_entry,  RT_NULL,  512, 20, 10);if (t1) rt_thread_startup(t1);if (t2) rt_thread_startup(t2);if (t3) rt_thread_startup(t3);
 

4.2 性能基准(main.c)

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

/* 1) 线程切换:5000 次 yield */t0 = rt_tick_get();for (int i = 0; i < 5000; i++) rt_thread_yield();bench_yield_us = (rt_tick_get() - t0) * 1000 / 5000;/* 2) 内存分配:1000 次 malloc+free */t0 = rt_tick_get();for (int i = 0; i < 1000; i++) {    void *p = rt_malloc(64);    if (p) rt_free(p);}bench_mem_us = (rt_tick_get() - t0) * 1000 / 1000;/* 3) 屏幕全屏填充耗时 */t0 = rt_tick_get();ili9341_fill(COLOR_BLACK);bench_fill_ms = rt_tick_get() - t0;
 

4.3 GPIO 直接寄存器写(关键性能优化)

LCD 驱动核心是把 SPI 字节通过 GPIO 一位一位”打”出去。最初使用 RT-Thread 标准 API:

  •  
  •  
  •  

/* 慢版:每次翻转引脚都要走一遍 RT-Thread 框架函数 */#define SCK_H()  rt_pin_write(PIN_SCK, PIN_HIGH)#define SCK_L()  rt_pin_write(PIN_SCK, PIN_LOW)

rt_pin_write 内部要做:参数检查 → 查 pin 表 → 调 NXP SDK 的 GPIO_PinWrite → 操作寄存器。一次翻转大约 3 条以上指令。

后来改成直接写 GPIO 寄存器:

  •  
  •  
  •  
  •  
  •  

/* 快版:直接往 GPIO 寄存器写一位即可 */#define SCK_H()  (GPIO1->PSOR = (1U << 9))#define SCK_L()  (GPIO1->PCOR = (1U << 9))#define SDA_H()  (GPIO1->PSOR = (1U << 8))#define SDA_L()  (GPIO1->PCOR = (1U << 8))

PSOR(Port Set Output Register):写入 1 把对应引脚拉高

PCOR(Port Clear Output Register):写入 1 把对应引脚拉低

这一步省去了所有中间层,单个 GPIO 翻转从 ~3 条指令降到 1 条指令。配合 static inline 让编译器把 spi_write_byte 内联到 fill_rect 的循环里,进一步消除函数调用开销。

4.4 ILI9341 驱动主要接口

  •  
  •  
  •  
  •  
  •  
  •  

voidili9341_init(void);                              /* 初始化 */voidili9341_fill(uint16_t color);                    /* 全屏填色 */voidili9341_fill_rect(x, y, w, h, color);            /* 矩形填充 */voidili9341_draw_pixel(x, y, color);                 /* 画点 */voidili9341_draw_string(x, y, str, fg, bg);          /* 12×16 像素 ASCII */voidili9341_backlight(int on);                       /* 背光控制 */

字体使用 5×7 column-major bitmap,渲染时 2× 放大到 12×16 像素,方便 240 像素宽的屏幕一行能容纳 20 个字符。

5 性能测试数据(实测)

5.1 GPIO 写法的速度对比(同板实测)

通过自定义 MSH 命令 bench_gpio,在同一块板子、同样多线程负载下,先后跑两遍全屏填充——一次用 rt_pin_write,一次用直接 GPIO 寄存器——直接对比:

SPISPI

关键观察:仅仅把 rt_pin_write(pin, val) 换成 GPIO1->PSOR = bit,整体吞吐量就提升到原来的 10 倍。这说明在 GPIO 翻转密集的场景下,框架函数调用开销才是真正的瓶颈,而不是 GPIO 硬件本身。

5.2 完整基准(启动单线程 vs MSH 多线程)

通过对比启动时(仅 main 线程)和系统跑起来后(main + info + anim + led + tshell + tidle 共 6 个线程并发)下的同一组测试,量化”多线程开销”:

SPISPI

5.3 数据分析

线程切换 6 → 8 µs:Cortex-M33 在 RT-Thread 调度下表现优秀,符合实时系统要求。多线程下增加 2 µs 是因为调度器要扫描更多的就绪队列。

malloc+free 72 → 95 µs:Small Heap 算法本身性能稳定,多线程下增加约 30% 是因为内存池有互斥保护,存在锁开销。

全屏刷新 460 → 614 ms:软件 SPI 在 240×320 大屏上仍是瓶颈,多线程下慢约 33% 是因为绘制中途被其他线程抢占。但通过直接寄存器写已将吞吐量提升到 127–167 px/ms。理论硬件 SPI(10 MHz LPSPI1 + DMA)可降至 ~120 ms。

6 踩过的坑
 

6.1 ILI9341 颜色反相

裸初始化序列后整个屏幕显示反色(黑变白、彩色全部反转)。原因:本面板(ATK-MD0240 V1.2)出厂默认开启色反转。

解决:在 init 序列中加入 0x21(INVON)命令,反向显示就回到正常色彩。

6.2 屏幕方向调试

ILI9341 通过 MADCTL(0x36)寄存器控制扫描方向,4 位组合(MY/MX/MV/BGR)共 16 种。本面板 FPC 排线方向特殊,调了 4 次才正:

SPI

6.3 BSP 硬件 SPI 配置坑

最初想用 LPSPI1 硬件 SPI 加速,启用 BSP_USING_SPI1 后编译能过、串口能跑(fill 也确实变成了 292 ms),但屏幕完全没反应、只有微弱背光。

排查过程:

怀疑 SPI 完全无信号 → 但 fill 时间真的在变(说明 LPSPI 寄存器在工作)

翻 BSP 的 pin_mux.c:LPSPI1_SCK = P2_12, LPSPI1_SDO = P2_13

翻 FRDM-MCXA156 Quick Start Guide 引脚图:P2_12 是 Arduino D13(OK),但 P2_13 没引到任何排针上

Arduino D11/SDO 在板上其实是 P3_15(P2_13 是 FRDM motor control 用途,悬空)

也就是说 NXP 的 BSP 把 LPSPI1 配错了引脚——硬件 SPI 走的是死路。

解决方案:退回软件 SPI,用直接 GPIO 寄存器写优化掉瓶颈,效果已经够用(460 ms)。如果以后真要上硬件 SPI,必须修改 pin_mux.c 把 LPSPI1_SDO 重新映射到 P3_15(同时也需要查 MCXA156 数据手册确认 P3_15 的 alt mux 值)。

6.4 启动时 fill 重复调用

最初版本在 splash → benchmark → draw_static_ui 各做了一次 ili9341_fill(COLOR_BLACK),相当于全屏刷了 3 遍。在还没优化的早期版本下启动等待 ~45 秒。

优化:splash 改成只填一小块文字区域;draw_static_ui 因为 benchmark 已经把屏幕填黑了,直接去掉重复的 fill。startup 砍到只跑一次全屏 fill。

6.5 空闲线程栈溢出

RT-Thread 默认 IDLE_THREAD_STACK_SIZE = 256 字节,在开了 ULOG/调试输出之后会栈溢出。改成 1024 字节后稳定。

7 MSH 命令行验证

通过串口连接(115200, 8N1)使用 FinSH/MSH:

SPI

8 项目目录
 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

frdm-mcxa156/├── applications/│   ├── main.c              ← 主程序、多线程、基准测试│   ├── drv_ili9341.c       ← ILI9341 软件 SPI 驱动(直接寄存器写)│   ├── drv_ili9341.h       ← LCD 接口头文件│   ├── drv_st7735.c/h      ← 旧版 ST7735 驱动(保留对照)│   └── SConscript├── board/├── drivers/└── rtconfig.h              ← RT-Thread 内核配置
 

9 心得与总结

RT-Thread 上手友好:scons 构建 + Kconfig 裁剪,配置直观,内核 API 与 Linux 风格相近。多线程编程逻辑清晰,rt_thread_create + rt_thread_startup 一目了然。

MCXA156 + RT-Thread 适配总体良好:NXP 官方 SDK 已集成,GPIO/UART 驱动开箱即用。但 BSP 里 LPSPI1 的引脚映射存在问题(P2_13 没引出),用之前要先核对 BSP 的 pin_mux.c 是不是和实际板子的 Quick Start Guide 引脚图对得上。

抽象层的代价:RT-Thread 的 rt_pin_write API 通用、可移植、可读性好,但在大量 GPIO 翻转的场景下函数调用开销显著。本项目通过直接写 GPIO 寄存器(PSOR/PCOR),像素吞吐量提升 10.1×(同板实测,bench_gpio 命令)。这是嵌入式开发中典型的 “易用性 vs 性能” 权衡——在性能关键路径上为速度让步、其他地方仍用标准 API 保持可移植性。

多线程编程关键点:

线程栈大小要根据实际调用栈预留充足;

共享外设(LCD/SPI 总线)需考虑互斥——本项目通过线程优先级隔离 + 同步绘制函数避免冲突;

不能假设 mdelay(30) 真的等 30 ms 就能跑出 30 ms 一帧的动画——绘制本身耗时也得算进去。

性能瓶颈定位的方法论:通过 bench 命令把”线程切换 / 内存分配 / 屏幕刷新”三个指标量化测出来,问题就显而易见——CPU 没瓶颈、内存没瓶颈,瓶颈完全在 SPI 这条窄通道上。这就为下一步优化(直接寄存器 → 硬件 SPI → DMA)提供了清晰的方向。

遇到的坑汇总:

空闲线程栈默认 256 字节过小 → 改 1024;

ILI9341 颜色反相 → 加 INVON(0x21);

MADCTL 调了 4 次才把方向调正(0xE8 = MY+MX+MV+BGR);

BSP 硬件 SPI 配错引脚(P2_13 未引出)→ 退回软件 SPI 优化;

启动时全屏 fill 调用了 3 次 → 优化到 1 次;

串口助手默认不发换行 → 勾选”发送新行”否则 MSH 不响应。

10 附录:源代码

源代码.zip

main.c — 主程序、多线程、基准测试

drv_ili9341.c — 当前驱动(直接寄存器写软件 SPI)

drv_ili9341.h — LCD 接口

rtconfig.h — RT-Thread 内核配置

旧版-ST7735/ — 旧版 ST7735 驱动(保留对照)

11 《恩智浦FRDM-MCX A366开发实践指南》贡献名单

RT-Thread社区携手恩智浦半导体联合发起 FRDM-MCXA366 开发板评测活动《恩智浦FRDM-MCX A366开发实践指南》详细列出了各个内容板块及其贡献者。在此,衷心感谢所有小伙伴的支持与贡献!

SPI

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分