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