N32G45x系列微控制器作为国民技术推出的高性能MCU,在嵌入式GUI开发中展现出独特优势。我最近在项目中使用了N32G455芯片驱动240x320分辨率的ST7789 LCD屏,实测SPI+DMA方案能显著降低CPU负载。硬件连接上,SPI2的SCK、MOSI引脚分别接LCD的SCL和SDA,注意CS和DC控制线需要额外GPIO控制。
SPI初始化是第一步关键操作,这里有个容易踩坑的点:时钟极性(CPOL)和相位(CPHA)必须与屏幕规格匹配。ST7789通常需要CPOL=1/CPHA=1的SPI模式3配置。初始化代码示例:
c复制void SPI2_Init(void)
{
SPI_InitType SPI_InitStructure;
RCC_EnableAPB1PeriphClk(RCC_APB1_PERIPH_SPI2, ENABLE);
SPI_InitStructure.DataDirection = SPI_DIR_SINGLELINE_TX;
SPI_InitStructure.SpiMode = SPI_MODE_MASTER;
SPI_InitStructure.DataLen = SPI_DATA_SIZE_8BITS;
SPI_InitStructure.CLKPOL = SPI_CLKPOL_HIGH; // CPOL=1
SPI_InitStructure.CLKPHA = SPI_CLKPHA_SECOND_EDGE; // CPHA=1
SPI_InitStructure.NSS = SPI_NSS_SOFT;
SPI_InitStructure.BaudRatePres = SPI_BR_PRESCALER_4; // 18MHz @72MHz PCLK
SPI_Init(SPI2, &SPI_InitStructure);
SPI_Enable(SPI2, ENABLE);
}
LCD初始化序列需要严格按照数据手册的时序要求。有个实用技巧:在发送0x11(SLEEP OUT)命令后必须延迟120ms以上,否则后续配置可能失效。我在调试时曾因这个延迟不足导致花屏,后来用逻辑分析仪抓取时序才发现问题。
LVGL作为轻量级开源图形库,其移植核心是实现disp_flush接口。原始实现通常采用CPU搬运数据,这在320x240分辨率下会导致明显卡顿。我们先看基础移植框架:
c复制void lv_port_disp_init(void)
{
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf1[LV_HOR_RES_MAX * 10]; // 行缓冲
lv_disp_draw_buf_init(&draw_buf, buf1, NULL, LV_HOR_RES_MAX * 10);
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);
}
disp_flush的基础实现直接调用SPI发送数据,这种方式的性能瓶颈很明显。实测在72MHz主频下,全屏刷新帧率仅能达到15fps左右,且CPU占用率超过80%。这主要是因为:
DMA直接内存访问是解决CPU负载问题的银弹。N32G45x的DMA控制器支持内存到外设的数据搬运,正好适配SPI发送场景。具体实现时需要注意几个关键点:
首先配置DMA通道参数,这里以DMA1通道5为例(对应SPI2_TX):
c复制void DMA1_CH5_Init(uint32_t src_addr, uint32_t size)
{
DMA_InitType DMA_InitStructure;
RCC_EnableAHBPeriphClk(RCC_AHB_PERIPH_DMA1, ENABLE);
DMA_InitStructure.PeriphAddr = (uint32_t)&SPI2->DAT;
DMA_InitStructure.MemAddr = src_addr;
DMA_InitStructure.Direction = DMA_DIR_PERIPH_DST;
DMA_InitStructure.BufSize = size;
DMA_InitStructure.PeriphInc = DMA_PERIPH_INC_DISABLE;
DMA_InitStructure.DMA_MemoryInc = DMA_MEM_INC_ENABLE;
DMA_InitStructure.PeriphDataSize = DMA_PERIPH_DATA_SIZE_16BIT;
DMA_InitStructure.MemDataSize = DMA_MEM_DATA_SIZE_16BIT;
DMA_InitStructure.CircularMode = DMA_MODE_NORMAL;
DMA_InitStructure.Priority = DMA_PRIORITY_HIGH;
DMA_InitStructure.Mem2Mem = DMA_M2M_DISABLE;
DMA_Init(DMA1_CH5, &DMA_InitStructure);
}
在api_lcd_fill函数中实现分块传输策略,这是处理大块数据传输的关键技巧。由于DMA最大传输长度限制(65535),对于320x240=76800像素的场景需要分块:
c复制void api_lcd_fill(uint16_t xsta, uint16_t ysta, uint16_t xend, uint16_t yend, uint16_t* color)
{
uint32_t total_pixels = (xend-xsta+1)*(yend-ysta+1);
uint16_t block_size;
drv_lcd_addr_set(xsta, ysta, xend, yend);
while(total_pixels > 0) {
block_size = (total_pixels > 32767) ? 32767 : total_pixels;
DMA1_CH5_Init((uint32_t)color, block_size);
SPI_I2S_EnableDma(SPI2, SPI_I2S_DMA_TX, ENABLE);
DMA_EnableChannel(DMA1_CH5, ENABLE);
while(!DMA_GetFlagStatus(DMA1_FLAG_TC5, DMA1))
__NOP();
DMA_ClearFlag(DMA1_FLAG_TC5, DMA1);
color += block_size;
total_pixels -= block_size;
}
}
实测显示,采用DMA后同样分辨率下帧率提升到45fps,CPU占用率降至20%以下。这个优化效果在需要复杂UI交互的场景中尤为明显。
除了基础DMA配置,还有几个提升显示性能的实用技巧:
双缓冲机制:为LVGL配置两个帧缓冲区,当DMA传输前缓冲区时,GUI可以渲染后缓冲区。这需要修改lv_conf.h中的配置:
c复制#define LV_DISP_DOUBLE_BUF 1
#define LV_DISP_DEF_REFR_PERIOD 30
SPI时钟优化:在保证信号完整性的前提下尽可能提高SPI时钟。ST7789最高支持62.5MHz,但实际布线中建议先测试40MHz。修改SPI分频系数:
c复制SPI_InitStructure.BaudRatePres = SPI_BR_PRESCALER_2; // 36MHz @72MHz PCLK
内存布局优化:将LVGL缓冲区和DMA源地址放在SRAM1(核心耦合内存)可以进一步提升传输效率。通过修改链接脚本实现:
ld复制MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
}
SECTIONS
{
.dma_buffer : {
. = ALIGN(4);
*(.dma_buffer)
} >CCMRAM
}
动态刷新区域:在disp_flush中根据脏区域面积决定使用DMA还是CPU传输。对于小面积更新(<100像素),CPU传输反而更快:
c复制static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
uint32_t pixel_count = (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1);
if(pixel_count > 100) {
api_lcd_fill_dma(area->x1, area->y1, area->x2, area->y2, color_p);
} else {
api_lcd_fill_cpu(area->x1, area->y1, area->x2, area->y2, color_p);
}
lv_disp_flush_ready(disp_drv);
}
在实际项目中,我遇到过几个典型问题值得分享:
DMA传输不完整:表现为屏幕部分区域花屏。这通常是DMA缓冲区未对齐导致的,解决方案是确保传输地址和长度都4字节对齐:
c复制uint32_t aligned_addr = (uint32_t)color & ~0x03;
uint32_t padding = (uint32_t)color - aligned_addr;
DMA_InitStructure.MemAddr = aligned_addr;
DMA_InitStructure.BufSize = (size + padding + 3) / 4 * 4;
SPI时钟异常:当SPI时钟超过30MHz时,建议将GPIO配置为高速模式:
c复制GPIO_InitType GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_MODE_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_SPEED_HIGH; // 关键配置
GPIO_InitStructure.GPIO_Pull = GPIO_PULLUP;
GPIO_Init(GPIOB, GPIO_PIN_13|GPIO_PIN_15, &GPIO_InitStructure);
LVGL渲染卡顿:除了显示优化,还需要检查LVGL的任务周期。建议将lv_timer_handler()放在5ms定时器中调用,而非主循环:
c复制void TIM3_IRQHandler(void)
{
if(TIM_GetIntStatus(TIM3, TIM_INT_UPDATE) != RESET) {
lv_timer_handler();
TIM_ClearIntPendingBit(TIM3, TIM_INT_UPDATE);
}
}
屏幕闪烁问题:这可能是由于DMA传输完成中断处理不及时造成的。可以在DMA完成后立即禁用通道:
c复制while(!DMA_GetFlagStatus(DMA1_FLAG_TC5, DMA1));
DMA_EnableChannel(DMA1_CH5, DISABLE);
SPI_I2S_EnableDma(SPI2, SPI_I2S_DMA_TX, DISABLE);