// 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");