alarm-tegra-x1 / device-tree/switch-fb.c

raw file 3542 bytes
// SPDX-License-Identifier: GPL-2.0-only
/*
 * switch-fb.c - Minimal framebuffer driver for Nintendo Switch
 *
 * The Switch bootloader (hekate/U-Boot) initializes the Tegra display
 * hardware and places the framebuffer at 0xf5a00000 (4MB, 720x1280x32bpp).
 * This module just maps that memory and registers it as /dev/fb0 so the
 * kernel console and ply-image can use it.
 *
 * Built as a module: insmod switch-fb.ko
 */

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

#define SWITCH_FB_PHYS   0xf5a00000
#define SWITCH_FB_SIZE   (4 * 1024 * 1024)  /* 4MB */
#define SWITCH_FB_WIDTH  720
#define SWITCH_FB_HEIGHT 1280
#define SWITCH_FB_BPP    32
#define SWITCH_FB_STRIDE (SWITCH_FB_WIDTH * (SWITCH_FB_BPP / 8))  /* 2880 */

static struct fb_info *fb_info;
static void __iomem *fb_virt;

static void switch_fb_defio(struct fb_info *info, struct list_head *objs)
{
	/* No deferred I/O needed — direct writes work fine */
}

static const struct fb_var_screeninfo switch_fb_var = {
	.xres		= SWITCH_FB_WIDTH,
	.yres		= SWITCH_FB_HEIGHT,
	.xres_virtual	= SWITCH_FB_WIDTH,
	.yres_virtual	= SWITCH_FB_HEIGHT,
	.bits_per_pixel	= SWITCH_FB_BPP,
	.red		= { .length = 8, .offset = 16 },
	.green		= { .length = 8, .offset = 8 },
	.blue		= { .length = 8, .offset = 0 },
	.transp		= { .length = 8, .offset = 24 },
	.nonstd		= 0,
	.width		= 77,   /* mm — physical panel width */
	.height		= 137,  /* mm — physical panel height */
	.accel_flags	= FB_ACCEL_NONE,
	.vmode		= FB_VMODE_NONINTERLACED,
};

static const struct fb_fix_screeninfo switch_fb_fix = {
	.id		= "switch-fb",
	.smem_start	= SWITCH_FB_PHYS,
	.smem_len	= SWITCH_FB_SIZE,
	.type		= FB_TYPE_PACKED_PIXELS,
	.visual		= FB_VISUAL_TRUECOLOR,
	.xpanstep	= 0,
	.ypanstep	= 0,
	.ywrapstep	= 0,
	.line_length	= SWITCH_FB_STRIDE,
	.mmio_start	= 0,
	.mmio_len	= 0,
	.accel		= FB_ACCEL_NONE,
};

static int switch_fb_probe(void)
{
	int ret;

	fb_virt = ioremap_wc(SWITCH_FB_PHYS, SWITCH_FB_SIZE);
	if (!fb_virt) {
		pr_err("switch-fb: failed to map framebuffer at 0x%x\n",
		       SWITCH_FB_PHYS);
		return -ENOMEM;
	}

	fb_info = framebuffer_alloc(0, NULL);
	if (!fb_info) {
		iounmap(fb_virt);
		return -ENOMEM;
	}

	fb_info->var = switch_fb_var;
	fb_info->fix = switch_fb_fix;
	fb_info->screen_base = (char __iomem *)fb_virt;
	fb_info->screen_size = SWITCH_FB_SIZE;
	fb_info->fbops = &(struct fb_ops){
		.owner		= THIS_MODULE,
		.fb_fillrect	= cfb_fillrect,
		.fb_copyarea	= cfb_copyarea,
		.fb_imageblit	= cfb_imageblit,
	};

	ret = fb_alloc_cmap(&fb_info->cmap, 256, 0);
	if (ret) {
		pr_err("switch-fb: failed to allocate cmap\n");
		framebuffer_release(fb_info);
		iounmap(fb_virt);
		return ret;
	}

	ret = register_framebuffer(fb_info);
	if (ret) {
		pr_err("switch-fb: failed to register framebuffer: %d\n", ret);
		fb_dealloc_cmap(&fb_info->cmap);
		framebuffer_release(fb_info);
		iounmap(fb_virt);
		return ret;
	}

	pr_info("switch-fb: registered %dx%d@%dbpp at 0x%x -> %px\n",
		SWITCH_FB_WIDTH, SWITCH_FB_HEIGHT, SWITCH_FB_BPP,
		SWITCH_FB_PHYS, fb_virt);

	return 0;
}

static void __exit switch_fb_exit(void)
{
	unregister_framebuffer(fb_info);
	fb_dealloc_cmap(&fb_info->cmap);
	framebuffer_release(fb_info);
	iounmap(fb_virt);
	pr_info("switch-fb: removed\n");
}

module_init(switch_fb_probe);
module_exit(switch_fb_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALARM Project");
MODULE_DESCRIPTION("Minimal framebuffer driver for Nintendo Switch bootloader-provided display");