一、项目背景
本文基于GD32F527单片机的显示系统,需要驱动480x272分辨率的TFT-LCD屏幕。最初使用内部SRAM作为帧缓冲区,分辨率只能限制在240x136。为了实现全屏显示,决定移植外部SDRAM作为帧缓冲区。
本文将详细介绍从裸机代码到Zephyr RTOS的完整移植过程,以及在SRAM和SDRAM实现中积累的经验心得。
二、硬件平台
MCU:GD32F527 (ARM Cortex-M33, 200MHz)LCD:480x272 TFT显示屏,RGB565格式存储:外部SDRAM (16Mbit, 通过EXMC接口)开发板:GD32F527I-EVAL
三、第一阶段:系统RAM帧缓冲区实现3.1 方案选择GD32F527内部SRAM为256KB,理论上可以容纳480x272x2=261KB的帧数据。但直接使用全部SRAM会影响其他代码的运行空间,因此最初采用240x136的分辨率进行验证。 3.2 关键配置步骤3.2.1. TLI接口配置
3.3 经验心得
经验1:编译器段属性很重要不同编译器的段属性语法不同:
经验2:分辨率与带宽的平衡- 240x136 @ 60fps:带宽需求约 240×136×2×60 ≈ 4.7MB/s- 480x272 @ 60fps:带宽需求约 480×272×2×60 ≈ 15.8MB/s
内部SRAM的带宽足够,但全屏需要外部SDRAM。
四、第二阶段:SDRAM帧缓冲区实现
4.1 硬件连接GD32F527通过EXMC (External Memory Controller) 接口连接SDRAM:
4.2 EXMC SDRAM初始化
4.3 移植到Zephyr RTOS
4.3.1 时钟配置
Zephyr默认使用IRC8M (8MHz) 时钟,需要在系统启动早期配置PLL为200MHz:
4.3.2 SystemCoreClock更新
HAL库需要正确的SystemCoreClock值进行延时计算:
4.3.3 Zephyr配置
五、调试经验与踩坑记录
坑1:系统时钟为0导致崩溃
现象:程序运行到LCD初始化时崩溃
原因:Zephyr默认使用IRC8M,但代码中使用了基于200MHz的延时函数
解决:在main()开头调用update_system_clock()更新SystemCoreClock
坑2:SDRAM初始化后崩溃
现象:SDRAM测试通过,但LCD初始化时崩溃
原因:可能是时钟配置在SDRAM操作之前尚未稳定
解决:确保PLL时钟完全稳定后再进行SDRAM操作
经验3:TLI图层配置要点
经验4:背光控制
六、测试图案代码
八、总结
关键要点
1. 时钟配置是根本:200MHz系统时钟需要正确的PLL配置和Flash等待周期2. SystemCoreClock不可忽略:HAL库延时函数依赖此变量3. 浮点数需谨慎:Zephyr默认可能不支持硬件浮点,避免在驱动代码中使用%f4. 缓存策略:对于帧缓冲区,禁用数据缓存可避免显示异常5. MPU配置:外设内存访问建议禁用MPU或正确配置内存区域
后续优化方向
1. 使用DMA传输提高显示效率2. 实现双缓冲机制避免画面撕裂3. 添加图形库支持 (lvgl)4. 实现触摸屏驱动
本文基于GD32F527单片机的显示系统,需要驱动480x272分辨率的TFT-LCD屏幕。最初使用内部SRAM作为帧缓冲区,分辨率只能限制在240x136。为了实现全屏显示,决定移植外部SDRAM作为帧缓冲区。
本文将详细介绍从裸机代码到Zephyr RTOS的完整移植过程,以及在SRAM和SDRAM实现中积累的经验心得。
二、硬件平台
MCU:GD32F527 (ARM Cortex-M33, 200MHz)LCD:480x272 TFT显示屏,RGB565格式存储:外部SDRAM (16Mbit, 通过EXMC接口)开发板:GD32F527I-EVAL
三、第一阶段:系统RAM帧缓冲区实现3.1 方案选择GD32F527内部SRAM为256KB,理论上可以容纳480x272x2=261KB的帧数据。但直接使用全部SRAM会影响其他代码的运行空间,因此最初采用240x136的分辨率进行验证。 3.2 关键配置步骤3.2.1. TLI接口配置
// tli.c - TLI初始化
void lcd_config(void)
{
// 配置像素时钟
tli_init_param tli_init_struct;
tli_init_struct.clock_divider = 2; // 根据PLL输出计算
tli_init_struct.rgb_mode = RGB565;
tli_init_struct.width = 480;
tli_init_struct.height = 272;
// ... 其他参数配置
tli_init(&tli_init_struct);
// 配置图层0
tli_layer_param layer;
layer.layer_addr = (uint32_t)framebuffer; // 帧缓冲区地址
layer.layer_width = 480;
layer.layer_height = 272;
layer.layer_pixel_format = LAYER0_PIXEL_FORMAT;
tli_layer_config(LAYER0, &layer);
}3.2. 2. 帧缓冲区定义// 使用内部SRAM (限制分辨率)
// 注意:__attribute__((at(address))) 是ARM特有语法
#if defined(__CC_ARM)
#define FB_SECTION __attribute__((at(0x20000000 + 0x10000)))
#elif defined(__GNUC__)
#define FB_SECTION __attribute__((section(".bss.$RAM3")))
#endif
static uint16_t framebuffer[240 * 136] FB_SECTION;3.3 经验心得
经验1:编译器段属性很重要不同编译器的段属性语法不同:
ARM Compiler (Keil):`__attribute__((at(0x20000000 + offset)))`
GCC:`__attribute__((section(".bss.$RAM3")))`
IAR:`__attribute__((section("RAM3")))`经验2:分辨率与带宽的平衡- 240x136 @ 60fps:带宽需求约 240×136×2×60 ≈ 4.7MB/s- 480x272 @ 60fps:带宽需求约 480×272×2×60 ≈ 15.8MB/s
内部SRAM的带宽足够,但全屏需要外部SDRAM。
四、第二阶段:SDRAM帧缓冲区实现
4.1 硬件连接GD32F527通过EXMC (External Memory Controller) 接口连接SDRAM:
| EXMC引脚 | SDRAM引脚 | 功能 |
| EXMC_D0-D15 | DQ0-D15 | 数据总线 |
| EXMC_A0-A12 | A0-A12 | 地址总线 |
| EXMC_NWE | WE | 写使能 |
| EXMC_NRAS | RAS | 行地址选通 |
| EXMC_NCAS | CAS | 列地址选通 |
| EXMC_CKE | CKE | 时钟使能 |
| EXMC_CLK | CLK | 时钟 |
4.2 EXMC SDRAM初始化
#define SDRAM_BASE 0xC0000000
void exmc_synchronous_dynamic_ram_init(uint32_t sdram_device)
{
// 1. 配置EXMC时钟
rcu_periph_clock_enable(RCU_EXMC);
// 2. 配置SDRAM时序参数
exmc_sdram_timing_parameter_struct timing;
timing.read_write_delay = 2;
timing.row_to_column_delay = 2;
timing.row_to_row_delay = 3;
timing.release_delay = 2;
timing.setup_time = 2;
timing.wait_config_time = 3;
timing.access_mode = EXMC_ACCESS_MODE_A;
// 3. 配置SDRAM控制参数
exmc_sdram_parameter_struct sdram_config;
sdram_config.sdram_device = sdram_device;
sdram_config.column_address_bit_num = EXMC_SDRAM_COLUMN_BIT_NUM_9;
sdram_config.row_address_bit_num = EXMC_SDRAM_ROW_BIT_NUM_13;
sdram_config.data_width = EXMC_SDRAM_DATABUS_WIDTH_16B;
sdram_config.internal_bank_number = EXMC_SDRAM_INTERN_BANK_NUM_4;
sdram_config.cas_latency = EXMC_SDRAM_CAS_LATENCY_2;
sdram_config.write_protection = DISABLE;
sdram_config.sdclock_config = EXMC_SDRAM_SDCLK_2MHZ;
sdram_config.burst_read = ENABLE;
sdram_config.burst_write = DISABLE;
sdram_config.wait_config = DISABLE;
sdram_config.timing = &timing;
// 4. 初始化
exmc_sdram_init(&sdram_config);
// 5. 发送初始化命令序列
uint32_t cmd = EXMC_SDRAM_CMD_NOP;
exmc_sdram_command(&cmd);
// 6. 等待200us
delay_ms(1);
// 7. 发送预充电命令
cmd = EXMC_SDRAM_CMD_PRECHARGE;
exmc_sdram_command(&cmd);
// 8. 自动刷新
cmd = EXMC_SDRAM_CMD_AUTO_REFRESH;
exmc_sdram_command(&cmd);
// 9. 加载模式寄存器
cmd = EXMC_SDRAM_CMD_LOAD_MODE_REG;
exmc_sdram_command(&cmd);
}4.3 移植到Zephyr RTOS
4.3.1 时钟配置
Zephyr默认使用IRC8M (8MHz) 时钟,需要在系统启动早期配置PLL为200MHz:
void soc_early_init_hook(void)
{
// 1. 配置Flash等待周期 (200MHz需要5个等待状态)
FMC->WS |= 0x05;
// 2. 启用外部晶振HXTAL
RCU->CTL |= RCU_CTL_HXTALEN;
while (!(RCU->CTL & RCU_CTL_HXTALSTB));
// 3. 配置PMU高驱动模式
PMU->CTL |= PMU_CTL_HDEN;
// 4. 配置PLL: HXTAL * 400 / 2 = 200MHz
RCU->CFG0 &= ~(RCU_CFG0_PLLDV | RCU_CFG0_PLLSEL | RCU_CFG0_PLLMF);
RCU->CFG0 |= (25 << 17) | // PSC = 25
(400 - 1) | // N = 400
(2 << 14); // P = 2
// 5. 启用PLL
RCU->CTL |= RCU_CTL_PLLEN;
while (!(RCU->CTL & RCU_CTL_PLLSTB));
// 6. 切换到PLL时钟
RCU->CFG0 |= RCU_CK_SYS_SRC_PLL;
while (((RCU->CFG0 >> 2) & 0x03) != 2);
}4.3.2 SystemCoreClock更新
HAL库需要正确的SystemCoreClock值进行延时计算:
// main.c
extern uint32_t SystemCoreClock;
static void update_system_clock(void)
{
volatile uint32_t *rcu_cfg0 = (volatile uint32_t *)0x40023808;
uint32_t scs = (*rcu_cfg0) & 0x03;
if (scs == 2) {
SystemCoreClock = 200000000;
} else if (scs == 1) {
SystemCoreClock = 16000000;
} else if (scs == 3) {
SystemCoreClock = 25000000;
} else {
SystemCoreClock = 8000000;
}
printk("SystemCoreClock: %d Hz\r\n", SystemCoreClock);
}4.3.3 Zephyr配置
# prj.conf CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC=200000000 CONFIG_MAIN_STACK_SIZE=4096 CONFIG_GPIO=y CONFIG_MPU=n # 禁用MPU简化SDRAM访问 CONFIG_DCACHE=n # 禁用数据缓存 CONFIG_CACHE_MANAGEMENT=n
五、调试经验与踩坑记录
坑1:系统时钟为0导致崩溃
现象:程序运行到LCD初始化时崩溃
原因:Zephyr默认使用IRC8M,但代码中使用了基于200MHz的延时函数
解决:在main()开头调用update_system_clock()更新SystemCoreClock
坑2:SDRAM初始化后崩溃
现象:SDRAM测试通过,但LCD初始化时崩溃
原因:可能是时钟配置在SDRAM操作之前尚未稳定
解决:确保PLL时钟完全稳定后再进行SDRAM操作
经验3:TLI图层配置要点
// TLI图层配置关键参数 layer.layer_window_left = 0; layer.layer_window_right = 479; layer.layer_window_top = 0; layer.layer_window_bottom = 271; layer.layer_offset_x = 0; layer.layer_offset_y = 0; layer.layer_palette_color_addr = 0; layer.layer_layer_addr = (uint32_t)framebuffer; // 帧缓冲地址
经验4:背光控制
// 开启背光 gpio_bit_set(GPIOB, GPIO_PIN_15); // 或者使用PWM调光 gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_15); gpio_af_set(GPIOB, GPIO_PIN_15, GPIO_AF_1); // 复用为定时器功能
六、测试图案代码
// 完整测试图案
void display_test_patterns(void)
{
while (1) {
// 1. 红色填充
fill_screen(COLOR_RED);
k_msleep(1000);
// 2. 绿色填充
fill_screen(COLOR_GREEN);
k_msleep(1000);
// 3. 蓝色填充
fill_screen(COLOR_BLUE);
k_msleep(1000);
// 4. 十字线测试
fill_screen(COLOR_BLACK);
draw_horizontal_line(LCD_HEIGHT / 2, COLOR_WHITE);
draw_vertical_line(LCD_WIDTH / 2, COLOR_RED);
k_msleep(2000);
// 5. 边框测试
fill_screen(COLOR_BLACK);
for (int x = 50; x < LCD_WIDTH - 50; x++) {
draw_pixel(x, 50, COLOR_WHITE);
draw_pixel(x, LCD_HEIGHT - 51, COLOR_WHITE);
}
for (int y = 50; y < LCD_HEIGHT - 50; y++) {
draw_pixel(50, y, COLOR_WHITE);
draw_pixel(LCD_WIDTH - 51, y, COLOR_WHITE);
}
k_msleep(2000);
// 6. 渐变效果
fill_screen(COLOR_BLACK);
for (int y = 0; y < LCD_HEIGHT; y++) {
uint16_t color = ((y * 31 / LCD_HEIGHT) << 5) |
((LCD_HEIGHT - y) * 31 / LCD_HEIGHT);
draw_horizontal_line(y, color);
}
k_msleep(2000);
}
} 七、代码结构lcd_sdram/ ├── CMakeLists.txt # 构建配置 ├── prj.conf # Zephyr配置 ├── src/ │ ├── main.c # 主程序 │ ├── graphics.c # 绘图函数 │ ├── graphics.h # 图形接口 │ ├── tli.c # TLI驱动 │ └── exmc_sdram.c # SDRAM驱动 └── README.md
八、总结
关键要点
1. 时钟配置是根本:200MHz系统时钟需要正确的PLL配置和Flash等待周期2. SystemCoreClock不可忽略:HAL库延时函数依赖此变量3. 浮点数需谨慎:Zephyr默认可能不支持硬件浮点,避免在驱动代码中使用%f4. 缓存策略:对于帧缓冲区,禁用数据缓存可避免显示异常5. MPU配置:外设内存访问建议禁用MPU或正确配置内存区域
后续优化方向
1. 使用DMA传输提高显示效率2. 实现双缓冲机制避免画面撕裂3. 添加图形库支持 (lvgl)4. 实现触摸屏驱动
附视频效果:
|
我要赚赏金
