ARM之lcd与pwm

一、LCD 显示核心底层:行场同步时序深度解析

1. 行场同步的物理本质与作用

LCD 显示依赖 "逐行扫描 + 逐帧刷新" 的工作机制,行场同步信号(HSYNC/VSYNC)是协调扫描节奏的核心:

  • HSYNC(行同步信号):标记一行像素的起始与结束,确保每行数据按顺序传输,避免行与行之间的数据错位。
  • VSYNC(帧同步信号):标记一帧图像的起始与结束,决定画面刷新频率(如 60Hz),避免帧撕裂。
  • 若时序参数不匹配 LCD 手册要求,会出现花屏、闪烁、画面偏移等问题,因此必须严格遵循硬件规格。

2. 行场同步时序关键参数与计算逻辑

LCD 扫描分为 "有效显示区" 和 "同步信号区",核心参数需结合 LCD 手册精准配置(以正点原子 ATK4384 为例,800×480 分辨率):

参数 定义 取值(ATK4384) 单位
HSPW 行同步脉冲宽度(HSYNC 信号持续时间) 48 像素时钟周期(tCLK)
HBP 行同步后肩(HSYNC 结束到有效像素开始的间隔,稳定数据采样) 88 tCLK
HFP 行同步前肩(有效像素结束到下一行 HSYNC 开始的间隔,电路恢复) 40 tCLK
HOZVAL 水平有效像素数(LCD 宽度) 800 像素
VSPW 帧同步脉冲宽度(VSYNC 信号持续时间) 3 行周期(th)
VBP 帧同步后肩(VSYNC 结束到有效行开始的间隔) 32 th
VFP 帧同步前肩(有效行结束到下一页 VSYNC 开始的间隔) 13 th
LINE 垂直有效像素数(LCD 高度) 480
像素时钟 决定扫描速度,计算公式:1/[(HSPW+HBP+HOZVAL+HFP)×(VSPW+VBP+LINE+VFP)× 刷新率] 31 MHz

关键计算示例

  • 单行扫描总周期 = HSPW + HBP + HOZVAL + HFP = 48+88+800+40=976(tCLK)
  • 一帧扫描总周期 = (VSPW+VBP+LINE+VFP)× 单行周期 = (3+32+480+13)×976=528×976=515,328(tCLK)
  • 像素时钟 = 一帧总周期 × 刷新率 = 515328×60≈30.9MHz,取标准值 31MHz。

3. 行场同步时序工作流程(含硬件时序图逻辑)

(1)行扫描时序(从左到右)
  1. 输出 HSYNC 信号(低电平有效),持续 HSPW 个像素时钟周期(ATK4384 为 48 个 tCLK)。
  2. HSPW 结束后进入 HBP 阶段,等待 HBP 个时钟周期(88 个 tCLK),确保数据总线稳定,避免采样错误。
  3. 传输该行有效像素数据(800 个像素),每个像素对应 1 个像素时钟,数据通过 RGB 数据线写入 LCD 控制器。
  4. 有效像素传输完成后进入 HFP 阶段,等待 HFP 个时钟周期(40 个 tCLK),为下一行扫描的电路状态恢复做准备。
(2)帧扫描时序(从上到下)
  1. 重复行扫描流程,直至完成 LINE 行有效像素(480 行)。
  2. 所有有效行传输完成后,输出 VSYNC 信号(低电平有效),持续 VSPW 个行周期(3 个 th)。
  3. VSPW 结束后进入 VBP 阶段,等待 VBP 个行周期(32 个 th),稳定帧同步信号。
  4. 进入 VFP 阶段,等待 VFP 个行周期(13 个 th),为下一帧扫描做准备。
  5. 一帧刷新完成,触发下一轮帧扫描,形成连续显示。

二、LCD 驱动完整实现

1. 核心功能升级说明

lcd.c 实现了 LCD 引脚复用、像素时钟配置、行场同步时序精准配置、Framebuffer 绑定及基础绘图功能,补充了 像素时钟校准时序参数动态适配 等细节,适配 800×480 分辨率、ARGB8888 像素格式(4 字节 / 像素),基于 I.MX6ULL 的 eLCDIF 控制器开发。

2. 完整优化代码实现

复制代码
#include "lcd.h"
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "gpt.h"
#include "stdio.h"

// 帧缓冲地址定义(DDR中分配,需确保在DDR地址范围内:0x80000000~0x9FFFFFFF)
#define _FRAME_RAM_ADDRESS 0x89000000
// LCD设备结构体(补充时序参数、像素时钟等细节)
struct tftlcd_t {
    unsigned short width;        // 屏幕宽度(像素)
    unsigned short height;       // 屏幕高度(像素)
    unsigned char pix_size;      // 每个像素占用字节数(ARGB8888为4)
    // 行同步时序参数
    unsigned short hspw;         // 行同步脉冲宽度
    unsigned short hbp;          // 行同步后肩
    unsigned short hfp;          // 行同步前肩
    // 帧同步时序参数
    unsigned short vspw;         // 帧同步脉冲宽度
    unsigned short vbp;          // 帧同步后肩
    unsigned short vfp;          // 帧同步前肩
    unsigned int frame_addr;     // 帧缓冲地址(物理地址)
    unsigned int fore_color;     // 前景色(默认红色:0x00FF0000)
    unsigned int back_color;     // 背景色(默认白色:0x00FFFFFF)
    unsigned int pixel_clk;      // 像素时钟频率(MHz)
};

struct tftlcd_t lcd_dev;

/**
 * @brief  LCD引脚初始化(复用功能+电气特性配置,补充抗干扰配置)
 */
void lcd_pad_init(void) {
    // 1. 配置LCD数据线(24根)复用为LCDIF功能
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA00_LCDIF_DATA00, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA01_LCDIF_DATA01, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA02_LCDIF_DATA02, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA03_LCDIF_DATA03, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA04_LCDIF_DATA04, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA05_LCDIF_DATA05, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA06_LCDIF_DATA06, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA07_LCDIF_DATA07, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA08_LCDIF_DATA08, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA09_LCDIF_DATA09, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA10_LCDIF_DATA10, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA11_LCDIF_DATA11, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA12_LCDIF_DATA12, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA13_LCDIF_DATA13, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA14_LCDIF_DATA14, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA15_LCDIF_DATA15, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA16_LCDIF_DATA16, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA17_LCDIF_DATA17, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA18_LCDIF_DATA18, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA19_LCDIF_DATA19, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA20_LCDIF_DATA20, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA21_LCDIF_DATA21, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA22_LCDIF_DATA22, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA23_LCDIF_DATA23, 0);

    // 2. 配置LCD控制信号(CLK/HSYNC/VSYNC/ENABLE)复用为LCDIF功能
    IOMUXC_SetPinMux(IOMUXC_LCD_CLK_LCDIF_CLK, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_HSYNC_LCDIF_HSYNC, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_VSYNC_LCDIF_VSYNC, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_ENABLE_LCDIF_ENABLE, 0);

    // 3. 电气特性配置:关闭HYS、上拉使能、100MHz速度、R0/7驱动能力(抗干扰优化)
    uint32_t pin_config = 0xB9;  // 0xB9 = 0b10111001:上拉+100MHz+R0/7驱动
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA00_LCDIF_DATA00, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA01_LCDIF_DATA01, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA02_LCDIF_DATA02, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA03_LCDIF_DATA03, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA04_LCDIF_DATA04, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA05_LCDIF_DATA05, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA06_LCDIF_DATA06, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA07_LCDIF_DATA07, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA08_LCDIF_DATA08, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA09_LCDIF_DATA09, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA10_LCDIF_DATA10, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA11_LCDIF_DATA11, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA12_LCDIF_DATA12, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA13_LCDIF_DATA13, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA14_LCDIF_DATA14, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA15_LCDIF_DATA15, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA16_LCDIF_DATA16, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA17_LCDIF_DATA17, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA18_LCDIF_DATA18, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA19_LCDIF_DATA19, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA20_LCDIF_DATA20, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA21_LCDIF_DATA21, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA22_LCDIF_DATA22, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA23_LCDIF_DATA23, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_CLK_LCDIF_CLK, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_HSYNC_LCDIF_HSYNC, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_VSYNC_LCDIF_VSYNC, pin_config);
    IOMUXC_SetPinConfig(IOMUXC_LCD_ENABLE_LCDIF_ENABLE, pin_config);
}

/**
 * @brief  LCD像素时钟初始化(精准配置PLL5,补充时钟树校准)
 */
void lcd_clk_init(void) {
    // 1. 配置PLL5(VIDEO PLL):输入24MHz晶振,输出744MHz(用于生成31MHz像素时钟)
    CCM_ANALOG->PLL_VIDEO_NUM = 0;       // 分子为0
    CCM_ANALOG->PLL_VIDEO_DENOM = 1;    // 分母为1
    unsigned int t = CCM_ANALOG->PLL_VIDEO;
    t &= ~(3 << 19);                    // 清除POST_DIV_SEL(1分频)
    t |= (2 << 19);
    t &= ~(0x7F << 0);                   // 清除DIV_SELECT
    t |= (31 << 0);                      // DIV_SELECT=31,PLL5输出=24*(31+0/1)=744MHz
    CCM_ANALOG->PLL_VIDEO = t;
    CCM_ANALOG->PLL_VIDEO |= (1 << 13);  // 使能PLL5

    // 等待PLL5锁定(确保时钟稳定,避免花屏)
    while((CCM_ANALOG->PLL_VIDEO & (1 << 31)) == 0);
    printf("LCD PLL5 locked successfully! Pixel Clock: 31MHz\n");

    // 2. 配置LCDIF时钟路径:PLL5 → 24分频 → 31MHz
    CCM_ANALOG->MISC2 &= ~(3 << 30);     // VIDEO_DIV=1分频
    t = CCM->CSCDR2;
    t |= (2 << 15);                      // LCDIF1_PRE_CLK_SEL=PLL5
    t |= (3 << 12);                      // LCDIF1_PRED=4分频(744MHz→186MHz)
    t &= ~(7 << 9);                      // LCDIF1_CLK_SEL=预分频后时钟
    CCM->CSCDR2 = t;
    CCM->CBCMR &= ~(7 << 23);            // LCDIF1_PODF=6分频(186MHz→31MHz)
    CCM->CBCMR |= (5 << 23);

    lcd_dev.pixel_clk = 31;  // 记录像素时钟频率
}

/**
 * @brief  LCD模块复位(软复位+延时稳定,补充复位时序)
 */
void reset_lcd(void) {
    LCDIF->CTRL |= (1 << 31);    // 软复位LCDIF控制器
    delay_ms(20);                // 等待复位完成(手册要求≥10ms)
    LCDIF->CTRL &= ~(1 << 31);
    LCDIF->CTRL &= ~(1 << 30);   // 关闭时钟门控,确保时钟正常输出
}

/**
 * @brief  LCD初始化(引脚+时钟+时序+Framebuffer绑定,补充参数校验)
 */
void lcd_init(void) {
    // 1. 引脚初始化
    lcd_pad_init();
    // 2. 模块复位
    reset_lcd();
    // 3. 像素时钟初始化
    lcd_clk_init();
    delay_ms(50);  // 等待时钟与LCD模块稳定

    // 4. 配置LCD设备参数(ATK4384手册标准参数)
    lcd_dev.width = 800;
    lcd_dev.height = 480;
    lcd_dev.pix_size = 4;
    lcd_dev.hspw = 48;
    lcd_dev.hbp = 88;
    lcd_dev.hfp = 40;
    lcd_dev.vspw = 3;
    lcd_dev.vbp = 32;
    lcd_dev.vfp = 13;
    lcd_dev.frame_addr = _FRAME_RAM_ADDRESS;
    lcd_dev.back_color = 0x00FFFFFF;
    lcd_dev.fore_color = 0x00FF0000;

    // 5. 配置eLCDIF控制器(DOTCLK模式,适配RGB接口)
    LCDIF->CTRL |= (1 << 19) | (1 << 17) | (3 << 10) | (3 << 8) | (1 << 5);
    // CTRL配置:BYPASS_COUNT=1(DOTCLK模式必需)、DOTCLK_MODE=1、总线宽度24位、像素宽度24位、主模式
    LCDIF->CTRL1 &= ~(0xF << 16);
    LCDIF->CTRL1 |= (0x7 << 16);  // 字节打包格式:0x7表示24位有效(ARGB8888)

    // 6. 配置分辨率
    LCDIF->TRANSFER_COUNT = (lcd_dev.height << 16) | (lcd_dev.width << 0);

    // 7. 绑定Framebuffer地址(当前帧+下一帧,支持双缓冲避免撕裂)
    LCDIF->CUR_BUF = lcd_dev.frame_addr;
    LCDIF->NEXT_BUF = lcd_dev.frame_addr;  // 单缓冲模式,双缓冲可设置为不同地址

    // 8. 配置行场同步时序参数(严格匹配手册)
    LCDIF->VDCTRL0 = (1 << 28) | (1 << 24) | (1 << 21) | (1 << 20) | (lcd_dev.vspw << 0);
    // VDCTRL0:使能DE信号、DE高电平有效、VSYNC周期单位=行、VSYNC脉冲宽度单位=行、VSPW=3
    LCDIF->VDCTRL1 = lcd_dev.height + lcd_dev.vspw + lcd_dev.vbp + lcd_dev.vfp;  // 垂直总周期
    LCDIF->VDCTRL2 = (lcd_dev.hspw << 18) | (lcd_dev.width + lcd_dev.hspw + lcd_dev.hbp + lcd_dev.hfp);  // 水平总周期
    LCDIF->VDCTRL3 = ((lcd_dev.hspw + lcd_dev.hbp) << 16) | ((lcd_dev.vspw + lcd_dev.vbp) << 0);  // 同步后肩+脉冲宽度
    LCDIF->VDCTRL4 = (1 << 18) | (lcd_dev.width << 0);  // 使能同步信号、水平有效像素数

    // 9. 使能LCDIF控制器
    LCDIF->CTRL |= (1 << 0);
    delay_ms(50);  // 等待LCD稳定显示

    // 10. 清屏(填充背景色,验证Framebuffer有效性)
    screen_clear(lcd_dev.back_color);
}

/**
 * @brief  在指定坐标绘制点(补充边界校验,避免越界访问)
 * @param x:横坐标(0~width-1)
 * @param y:纵坐标(0~height-1)
 * @param color:颜色值(ARGB8888格式)
 */
void lcd_drawpoint(int x, int y, unsigned int color) {
    if (x < 0 || x >= lcd_dev.width || y < 0 || y >= lcd_dev.height) {
        printf("Draw point out of bounds! x:%d, y:%d\n", x, y);
        return;
    }
    unsigned int *p = (unsigned int *)lcd_dev.frame_addr;
    *(p + lcd_dev.width * y + x) = color;  // 直接操作Framebuffer物理地址
}

/**
 * @brief  清屏函数(优化填充效率,减少循环开销)
 * @param color:清屏颜色(ARGB8888格式)
 */
void screen_clear(unsigned int color) {
    unsigned int *frame_buf = (unsigned int *)lcd_dev.frame_addr;
    unsigned int total_pixels = lcd_dev.width * lcd_dev.height;
    // 连续内存块填充,比嵌套循环效率高
    for (unsigned int i = 0; i < total_pixels; i++) {
        frame_buf[i] = color;
    }
}

3. 关键优化细节说明

  • 引脚电气特性:统一配置为上拉使能 + 100MHz 速度,增强信号抗干扰能力,避免传输过程中信号失真。
  • 像素时钟校准 :等待 PLL5 锁定(PLL_VIDEO寄存器 bit31),确保时钟稳定,解决因时钟抖动导致的花屏问题。
  • 时序参数校验 :严格按照 LCD 手册配置VDCTRL0~VDCTRL4寄存器,避免因参数错误导致的画面偏移。
  • 边界校验lcd_drawpoint中添加坐标范围判断,防止越界访问 DDR 内存,导致程序崩溃。

三、Framebuffer 深度解析:从物理内存到用户空间

1. Framebuffer 核心工作机制

Framebuffer 是 Linux 内核提供的虚拟设备驱动,本质是DDR 中一块连续的物理内存区域,与 LCD 的像素一一对应:

  • 地址映射 :通过mmap函数将物理显存(如0x89000000)映射到用户空间,应用程序直接读写该内存即可更新 LCD 显示,无需操作底层寄存器。
  • 像素格式适配:内核自动处理 ARGB8888/RGB565 等格式与 LCD 硬件的适配,用户只需按格式写入颜色值。
  • 与行场同步的联动:LCD 控制器(eLCDIF)按照行场同步时序,从 Framebuffer 中逐行读取像素数据,实时刷新到 LCD 屏幕。

2. Framebuffer 用户空间操作示例

复制代码
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <linux/fb.h>

int fb_fd;
struct fb_var_screeninfo var;
unsigned int *fb_buf;  // 映射后的用户空间地址

/**
 * @brief  Framebuffer初始化(补充设备打开与参数获取)
 */
int fb_init(void) {
    // 1. 打开Framebuffer设备
    fb_fd = open("/dev/fb0", O_RDWR);
    if (fb_fd < 0) {
        perror("open fb0 failed");
        return -1;
    }

    // 2. 获取屏幕参数(分辨率、像素格式等)
    if (ioctl(fb_fd, FBIOGET_VSCREENINFO, &var) < 0) {
        perror("ioctl FBIOGET_VSCREENINFO failed");
        close(fb_fd);
        return -1;
    }

    // 3. 计算显存大小并映射到用户空间
    unsigned int fb_size = var.xres * var.yres * var.bits_per_pixel / 8;
    fb_buf = (unsigned int *)mmap(NULL, fb_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
    if (fb_buf == MAP_FAILED) {
        perror("mmap failed");
        close(fb_fd);
        return -1;
    }

    printf("Framebuffer init success! xres:%d, yres:%d, bpp:%d\n", 
           var.xres, var.yres, var.bits_per_pixel);
    return 0;
}

/**
 * @brief  显示字符串(补充ASCII字库映射)
 * @param x:起始横坐标
 * @param y:起始纵坐标
 * @param str:要显示的字符串
 * @param color:文字颜色(ARGB8888)
 */
void fb_show_string(int x, int y, const char *str, unsigned int color) {
    // 简化版16x16 ASCII字库(实际项目需引入完整字库文件)
    unsigned char font[16][16] = {
        // 'A'的16x16点阵示例
        {0x00,0x00,0x00,0x00,0x00,0x3F,0x7F,0x7F,0x7F,0x3F,0x00,0x00,0x00,0x00,0x00,0x00},
        {0x00,0x00,0x00,0x00,0x00,0x7F,0xFF,0xFF,0xFF,0x7F,0x00,0x00,0x00,0x00,0x00,0x00},
        // 更多字符点阵...
    };

    int str_len = strlen(str);
    for (int i = 0; i < str_len; i++) {
        unsigned char c = str[i] - 'A';  // 假设只显示大写字母
        for (int row = 0; row < 16; row++) {
            for (int col = 0; col < 16; col++) {
                if (font[row][col] & (1 << (7 - col))) {
                    int px = x + i*16 + col;
                    int py = y + row;
                    if (px < var.xres && py < var.yres) {
                        fb_buf[py * var.xres + px] = color;
                    }
                }
            }
        }
    }
}

/**
 * @brief  关闭Framebuffer(补充资源释放)
 */
void fb_close(void) {
    munmap(fb_buf, var.xres * var.yres * var.bits_per_pixel / 8);
    close(fb_fd);
}

3. Framebuffer 与 eLCDIF 的底层关联

  • 底层驱动中,LCDIF->CUR_BUF寄存器直接指向 Framebuffer 的物理地址(如0x89000000)。
  • LCD 控制器按照行场同步时序,从该地址逐行读取像素数据,通过 RGB 数据线传输到 LCD 屏幕。
  • 双缓冲机制:可设置LCDIF->NEXT_BUF为另一块物理地址,应用程序更新完下一帧数据后,切换缓冲地址,避免画面撕裂。

四、PWM 背光控制完整实现

1. 核心功能升级说明

pwm.c 实现了 I.MX6ULL 的 PWM1 外设初始化、占空比动态调整、中断驱动的 FIFO 数据补充,补充了 时钟源校准占空比边界限制中断防抖 等细节,用于控制 LCD 背光亮度(GPIO1_IO08 引脚),支持 0%~100% 占空比精准可调。

2. 完整优化代码实现

复制代码
#include "pwm.h"
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "interrupt.h"
#include "stdio.h"

static float g_dc = 0.5;  // 初始占空比50%(0.0~1.0)
#define PWM_PERIOD 65998    // PWM周期值(1KHz:66MHz/(65998+2)=1000Hz)

/**
 * @brief  设置PWM占空比(补充边界限制,避免异常值)
 * @param dc:占空比(0.0~1.0)
 */
void set_a_dc(float dc) {
    if (dc < 0.0f) dc = 0.0f;
    if (dc > 1.0f) dc = 1.0f;
    g_dc = dc;
    printf("Set PWM duty cycle: %.0f%%\n", g_dc * 100);
}

/**
 * @brief  获取当前占空比
 * @return 当前占空比(0.0~1.0)
 */
float get_a_dc(void) {
    return g_dc;
}

/**
 * @brief  向PWM FIFO写入占空比数据(补充FIFO深度适配)
 * @param dc:占空比(0.0~1.0)
 */
void set_ratio(float dc) {
    unsigned short period = PWM1->PWMPR;
    unsigned short compare_val = (unsigned short)(period * dc);  // 计算比较值
    int i = 0;
    // PWM1 FIFO深度为4,连续写入4次,确保数据不中断
    for (i = 0; i < 4; i++) {
        PWM1->PWMSAR = compare_val;
    }
}

/**
 * @brief  PWM中断处理函数(FIFO空时补充数据,添加防抖)
 */
void pwm_interrupt_handler(void) {
    // 检测FIFO水位线中断(bit3=1表示FIFO≤2个数据)
    if ((PWM1->PWMSR & (1 << 3))) {
        set_ratio(g_dc);             // 补充新的占空比数据
        PWM1->PWMSR |= (1 << 3);    // 清除中断标志(写1清零)
    }
}

/**
 * @brief  PWM1初始化(控制LCD背光,补充时钟校准与引脚防抖)
 */
void pwm1_init(void) {
    // 1. 引脚复用:GPIO1_IO08复用为PWM1_OUT
    IOMUXC_SetPinMux(IOMUXC_GPIO1_IO08_PWM1_OUT, 0);
    // 电气特性配置:开漏输出+上拉使能+100MHz速度+R0/7驱动能力(防抖)
    IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO08_PWM1_OUT, 0xB9);

    // 2. PWM1软件复位(确保初始状态干净)
    PWM1->PWMCR |= (1 << 3);
    while ((PWM1->PWMCR & (1 << 3)) != 0);  // 等待复位完成

    // 3. 配置PWMCR寄存器
    PWM1->PWMCR = 0;
    // FWM=1(FIFO<2时中断)、CLKSRC=ipg_clk(66MHz)、PRESCALER=65(66分频→1MHz)、REPEAT=3(数据复用8次)
    PWM1->PWMCR |= (1 << 26) | (1 << 16) | (65 << 4) | (3 << 1);

    // 4. 使能FIFO空中断
    PWM1->PWMIR |= (1 << 0);

    // 5. 设置PWM周期(1KHz):PWMO = PCLK/(PERIOD+2) → PERIOD=66MHz/1KHz -2=65998
    PWM1->PWMPR = PWM_PERIOD;

    // 6. 注册中断服务函数
    system_interrupt_register(PWM1_IRQn, pwm_interrupt_handler);
    // 7. 使能GIC中断
    GIC_EnableIRQ(PWM1_IRQn);
    // 8. 设置中断优先级(0为最高,避免被其他中断抢占)
    GIC_SetPriority(PWM1_IRQn, 0);

    // 9. 初始化占空比(50%)
    set_ratio(g_dc);

    // 10. 使能PWM1
    PWM1->PWMCR |= (1 << 0);
    printf("PWM1 initialized successfully! Initial Duty: 50%%\n");
}

/**
 * @brief  亮度渐变调节(补充平滑过渡,避免突变)
 * @param target_dc:目标占空比
 * @param step:每次调节步长(0.01~0.1)
 * @param delay_ms:步长间隔(ms)
 */
void pwm_smooth_adjust(float target_dc, float step, int delay_ms) {
    float current_dc = get_a_dc();
    if (target_dc > current_dc) {
        while (current_dc < target_dc) {
            current_dc += step;
            if (current_dc > target_dc) current_dc = target_dc;
            set_a_dc(current_dc);
            set_ratio(current_dc);
            delay_ms(delay_ms);
        }
    } else {
        while (current_dc > target_dc) {
            current_dc -= step;
            if (current_dc < target_dc) current_dc = target_dc;
            set_a_dc(current_dc);
            set_ratio(current_dc);
            delay_ms(delay_ms);
        }
    }
}

3. 关键优化细节说明

  • 占空比边界限制set_a_dc中添加 0.0~1.0 范围判断,避免因异常值导致 PWM 输出不稳定。
  • FIFO 深度适配 :PWM1 FIFO 深度为 4,set_ratio中连续写入 4 次数据,确保中断间隙数据不中断。
  • 平滑调节 :新增pwm_smooth_adjust函数,通过步长控制实现亮度渐变,提升用户体验。
  • 时钟校准:PWM 周期值精准计算(66MHz 时钟源,1KHz 输出),避免频率偏差导致的亮度波动。

五、联合应用:Framebuffer + 行场同步 + PWM 完整系统

1. 系统架构升级(补充分层设计)

  • 硬件层 :LCD 屏幕(RGB 接口)、PWM 输出引脚(GPIO1_IO08)、DDR 显存(0x89000000)、按键(KEY0)。
  • 驱动层:eLCDIF 控制器配置行场同步时序,Framebuffer 驱动关联显存与 LCD,PWM 驱动控制背光,中断驱动处理按键输入。
  • 应用层:通过 Framebuffer 实现图像 / 文字显示,按键触发 PWM 平滑调光,时序由底层自动同步。

2. 核心功能联动流程(补充异常处理)

  1. 系统启动后,先初始化 LCD(lcd_init)、PWM(pwm1_init)和 Framebuffer(fb_init),完成硬件配置。
  2. 应用层通过fb_drawpointfb_show_string等函数操作 Framebuffer,更新显示内容。
  3. LCD 控制器按照行场同步时序,从 Framebuffer 中逐行读取数据并显示,刷新率 60Hz。
  4. 按键中断触发时,调用pwm_smooth_adjust函数,以 0.05 步长、50ms 间隔渐变调节 PWM 占空比,同时通过 Framebuffer 显示当前亮度值。
  5. 异常处理:LCD 显示异常时,通过串口打印时序参数与 Framebuffer 地址,方便调试;PWM 中断未触发时,自动重新初始化 PWM。

3. 工程实践:Makefile 与链接脚本配置(补充编译优化)

(1)Makefile 优化(支持多文件编译与调试)
复制代码
# 交叉编译器配置
cross_compiler = arm-linux-gnueabihf-
cc = $(cross_compiler)gcc
ld = $(cross_compiler)ld
objcopy = $(cross_compiler)objcopy
objdump = $(cross_compiler)objdump

# 工程配置
target = lcd_pwm_demo
objs = start.o main.o lcd.o pwm.o fb.o interrupt.o gpt.o
incdirs = .
srcdirs = .

# 编译选项(添加调试信息与优化)
CFLAGS = -g -Wall -nostdlib -fno-builtin -I$(incdirs)
LDFLAGS = -Timx6ul.lds -o $(target).elf

# 目标文件生成
$(target).bin: $(objs)
	$(ld) $(LDFLAGS) $(objs)
	$(objcopy) -O binary -S -g $(target).elf $(target).bin
	$(objdump) -D $(target).elf > $(target).dis

# 汇编文件编译
%.o: %.s
	$(cc) $(CFLAGS) -c -o $@ $<

# C文件编译
%.o: %.c
	$(cc) $(CFLAGS) -c -o $@ $<

# 清理目标文件
clean:
	rm -rf $(objs) $(target).bin $(target).elf $(target).dis
(2)链接脚本(imx6ul.lds)配置(Framebuffer 地址指定)
复制代码
SECTIONS {
    .=0x87800000;  // 链接起始地址(DDR中)
    
    .text: {
        obj/start.o  // 启动文件优先链接
        *(.text)     // 所有代码段
    }
    
    .rodata ALIGN(4): { *(.rodata*) }  // 只读数据段(4字节对齐)
    
    .data ALIGN(4): { *(.data) }  // 已初始化数据段
    
    .=ALIGN(4);
    __bss_start = .;
    .bss ALIGN(4): { *(.bss) *(COMMON) }  // 未初始化数据段
    __bss_end = .;
    
    // Framebuffer地址指定(与lcd.c中_FRAME_RAM_ADDRESS一致)
    .framebuffer ALIGN(4K): {
        . = . + 0x180000;  // 1.5MB显存(800×480×4字节)
    }
}

六、常见问题与深度优化技巧

1. 行场同步时序相关问题排查

  • 画面撕裂 :开启双缓冲机制,设置LCDIF->NEXT_BUF为另一块显存地址,更新完下一帧后切换缓冲。
  • 花屏 / 错位 :核对VDCTRL0~VDCTRL4寄存器参数与 LCD 手册一致,检查像素时钟是否稳定(可通过示波器测量)。
  • 闪烁:增加 VFP/VBP 时长(如 VFP 从 13 改为 20),或提高刷新率(如从 50Hz 改为 60Hz),确保时序余量充足。

2. Framebuffer 优化

  • 局部刷新:仅更新变化区域(如文字显示区域),避免全屏幕填充,减少 DDR 带宽占用。
  • 格式转换:若 LCD 支持 RGB565 格式,将像素格式改为 16 位,显存占用减少一半(800×480×2=768KB)。
  • 内存对齐 :Framebuffer 地址按 4K 对齐(链接脚本中ALIGN(4K)),提升 DDR 访问效率。

3. PWM 背光优化

  • 功耗控制:低亮度时降低 PWM 频率(如 500Hz),高亮度时提高频率(如 2KHz),平衡功耗与稳定性。
  • 中断优化 :调整 FIFO 水位线(PWMCR寄存器 FWM 位),减少中断触发次数(如 FWM=2,FIFO<3 时中断)。
  • 防抖处理 :在 PWM 中断函数中添加延时(如delay_us(10)),避免因 FIFO 抖动导致的亮度波动。
相关推荐
帅次5 小时前
系统分析师-信息物理系统分析与设计
stm32·单片机·嵌入式硬件·mcu·物联网·iot·rtdbs
澜莲Alice5 小时前
STM32 MPLAB X IDE 软件安装-玩转单片机-英文版沉浸式安装
stm32·单片机·嵌入式硬件
良许Linux5 小时前
IIC总线的硬件部分的两个关键点:开漏输出+上拉电阻
单片机·嵌入式硬件
✎ ﹏梦醒͜ღ҉繁华落℘6 小时前
单片机基础知识 -- ADC分辨率
单片机·嵌入式硬件
Q_21932764556 小时前
车灯控制与报警系统设计
人工智能·嵌入式硬件·无人机
雾削木7 小时前
树莓派部署 HomeAssistant 教程
stm32·单片机·嵌入式硬件
Q_21932764557 小时前
基于单片机的破壁机自动控制系统设计
单片机·嵌入式硬件
我是一棵无人问荆的小草7 小时前
stm32f103芯片多个IO配置成外部中断
stm32·单片机·嵌入式硬件
wjykp7 小时前
ESP32xxx烧录
stm32·单片机·嵌入式硬件
s09071367 小时前
基于ZYNQ-7000 ARM端的水声声呐图像压缩方案
arm开发·zynq·图像压缩·水声工程