2. 设置可变显示参数

任务说明

本案例实现Linux帧缓冲驱动中的 fb_set_var 回调,使应用层通过 FBIOPUT_VSCREENINFO ioctl 动态修改分辨率与像素格式时,驱动能校验参数、更新 fb_info 中的可变参数,并重新计算显存布局。完成目标是:用户程序修改分辨率后,重新mmap映射即可看到新尺寸的显示效果。

核心实现

#include <linux/fb.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/mm.h>

#define DRV_NAME "demo_fb"

struct demo_fb_par {
    u32 pseudo_palette[16];
    void __iomem *vram_base;
    size_t vram_size;
    u32 xres, yres, bits_per_pixel;
};

static int demo_fb_set_var(struct fb_var_screeninfo *var, struct fb_info *info)
{
    struct demo_fb_par *par = info->par;
    size_t new_size;
    u32 line_len;

    /* 1. 校验分辨率范围 */
    if (var->xres < 16 || var->xres > 1920 ||
        var->yres < 16 || var->yres > 1080)
        return -EINVAL;

    /* 2. 校验像素格式(仅支持 RGB565 和 XRGB8888) */
    if (var->bits_per_pixel == 16) {
        if (var->red.offset != 11 || var->red.length != 5 ||
            var->green.offset != 5 || var->green.length != 6 ||
            var->blue.offset != 0 || var->blue.length != 5)
            return -EINVAL;
    } else if (var->bits_per_pixel == 32) {
        if (var->red.offset != 16 || var->red.length != 8 ||
            var->green.offset != 8 || var->green.length != 8 ||
            var->blue.offset != 0 || var->blue.length != 8)
            return -EINVAL;
    } else {
        return -EINVAL;
    }

    /* 3. 计算新显存需求并检查是否超出分配大小 */
    line_len = var->xres * var->bits_per_pixel / 8;
    new_size = line_len * var->yres;
    if (new_size > par->vram_size)
        return -ENOMEM;

    /* 4. 同步到 fb_info 的可变参数区 */
    info->var.xres = var->xres;
    info->var.yres = var->yres;
    info->var.xres_virtual = var->xres;
    info->var.yres_virtual = var->yres;
    info->var.bits_per_pixel = var->bits_per_pixel;
    info->var.red = var->red;
    info->var.green = var->green;
    info->var.blue = var->blue;
    info->var.activate = FB_ACTIVATE_NOW;

    /* 5. 更新固定参数中的行长度 */
    info->fix.line_length = line_len;

    /* 6. 记录当前参数到私有结构 */
    par->xres = var->xres;
    par->yres = var->yres;
    par->bits_per_pixel = var->bits_per_pixel;

    return 0;
}

static int demo_fb_check_var(struct fb_var_screeninfo *var, struct fb_info *info)
{
    /* 复用 set_var 的校验逻辑,但不实际修改参数 */
    struct fb_var_screeninfo tmp = *var;
    return demo_fb_set_var(&tmp, info);
}

static int demo_fb_mmap(struct fb_info *info, struct vm_area_struct *vma)
{
    struct demo_fb_par *par = info->par;
    unsigned long start = (unsigned long)par->vram_base;
    unsigned long size = vma->vm_end - vma->vm_start;

    if (size > par->vram_size)
        return -EINVAL;

    vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);
    if (remap_pfn_range(vma, vma->vm_start, start >> PAGE_SHIFT,
                        size, vma->vm_page_prot))
        return -EAGAIN;
    return 0;
}

static struct fb_ops demo_fb_ops = {
    .owner = THIS_MODULE,
    .fb_check_var = demo_fb_check_var,
    .fb_set_var = demo_fb_set_var,
    .fb_mmap = demo_fb_mmap,
    .fb_fillrect = sys_fillrect,
    .fb_copyarea = sys_copyarea,
    .fb_imageblit = sys_imageblit,
};

static int demo_fb_probe(struct platform_device *pdev)
{
    struct fb_info *info;
    struct demo_fb_par *par;
    struct resource *res;
    int ret;

    info = framebuffer_alloc(sizeof(*par), &pdev->dev);
    if (!info)
        return -ENOMEM;

    par = info->par;
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res) {
        ret = -ENODEV;
        goto err_release;
    }

    par->vram_size = resource_size(res);
    par->vram_base = ioremap(res->start, par->vram_size);
    if (!par->vram_base) {
        ret = -ENOMEM;
        goto err_release;
    }

    /* 默认参数:800x600 RGB565 */
    info->var.xres = 800;
    info->var.yres = 600;
    info->var.xres_virtual = 800;
    info->var.yres_virtual = 600;
    info->var.bits_per_pixel = 16;
    info->var.red.offset = 11; info->var.red.length = 5;
    info->var.green.offset = 5; info->var.green.length = 6;
    info->var.blue.offset = 0; info->var.blue.length = 5;
    info->var.activate = FB_ACTIVATE_NOW;

    info->fix.smem_start = res->start;
    info->fix.smem_len = par->vram_size;
    info->fix.line_length = 800 * 2;
    info->fix.type = FB_TYPE_PACKED_PIXELS;
    info->fix.visual = FB_VISUAL_TRUECOLOR;
    strcpy(info->fix.id, DRV_NAME);

    info->fbops = &demo_fb_ops;
    info->pseudo_palette = par->pseudo_palette;
    info->screen_base = par->vram_base;
    info->screen_size = par->vram_size;

    ret = register_framebuffer(info);
    if (ret)
        goto err_unmap;

    platform_set_drvdata(pdev, info);
    return 0;

err_unmap:
    iounmap(par->vram_base);
err_release:
    framebuffer_release(info);
    return ret;
}

static int demo_fb_remove(struct platform_device *pdev)
{
    struct fb_info *info = platform_get_drvdata(pdev);
    struct demo_fb_par *par = info->par;

    unregister_framebuffer(info);
    iounmap(par->vram_base);
    framebuffer_release(info);
    return 0;
}

static struct platform_driver demo_fb_driver = {
    .probe = demo_fb_probe,
    .remove = demo_fb_remove,
    .driver = {
        .name = DRV_NAME,
    },
};
module_platform_driver(demo_fb_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Demo FB driver with fb_set_var support");

关键说明

  • 参数校验:demo_fb_set_var 首先检查分辨率范围(16×16 至 1920×1080)和像素格式的位域布局,防止非法参数破坏显存。
  • 显存容量检查:根据新分辨率计算所需字节数,若超过 par->vram_size(由平台资源决定)则返回 -ENOMEM,避免越界访问。
  • 同步更新:校验通过后同步更新 info->var、info->fix.line_length 和私有参数,保证后续 fb_read/fb_write 和 mmap 使用新布局。
  • check_var 复用:fb_check_var 调用同一套校验逻辑但不实际修改参数,供应用层查询可行性。

编译与加载

# 内核树外编译(需内核头文件)
make -C /lib/modules/$(uname -r)/build M=$PWD modules

# 加载驱动(需先注册平台设备,或使用设备树 compatible="demo_fb")
insmod demo_fb.ko

用户态验证

#include <fcntl.h>
#include <linux/fb.h>
#include <sys/ioctl.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
    int fd = open("/dev/fb0", O_RDWR);
    struct fb_var_screeninfo var;

    ioctl(fd, FBIOGET_VSCREENINFO, &var);
    printf("Before: %ux%u %ubpp\n", var.xres, var.yres, var.bits_per_pixel);

    /* 修改为 1024x768 RGB565 */
    var.xres = 1024;
    var.yres = 768;
    var.bits_per_pixel = 16;
    var.red.offset = 11; var.red.length = 5;
    var.green.offset = 5; var.green.length = 6;
    var.blue.offset = 0; var.blue.length = 5;

    if (ioctl(fd, FBIOPUT_VSCREENINFO, &var) == 0) {
        ioctl(fd, FBIOGET_VSCREENINFO, &var);
        printf("After: %ux%u %ubpp\n", var.xres, var.yres, var.bits_per_pixel);
    } else {
        perror("FBIOPUT_VSCREENINFO");
    }
    close(fd);
    return 0;
}

通过条件:编译运行上述用户程序,输出显示分辨率从 800x600 16bpp 变为 1024x768 16bpp,且 ioctl 返回 0。若尝试设置非法分辨率(如 2000×2000)或错误位域(如 24bpp),ioctl 应返回 -EINVAL。