├── .gitignore ├── README.md ├── assets ├── zig-icon.bmp ├── zig-icon.svg ├── zig-logo.bmp └── zig-logo.svg ├── boot ├── bootcode.bin ├── config.txt ├── fixup.dat └── start.elf ├── build.zig ├── src ├── bootloader.ld ├── bootloader.zig ├── debug.zig ├── linker.ld ├── main.zig ├── mmio.zig ├── qemu-gdb.ld ├── serial.zig ├── slice_iterator.zig ├── time.zig ├── video_core_frame_buffer.zig ├── video_core_mailboxes.zig ├── video_core_metrics.zig └── video_core_properties.zig └── tools └── send_image.zig /.gitignore: -------------------------------------------------------------------------------- 1 | /clashos.bin 2 | /zig-cache/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ClashOS 2 | 3 | A work-in-progress multiplayer arcade game that runs directly on the 4 | Raspberry Pi 3 B+ hardware, written entirely in [Zig](https://ziglang.org/). 5 | 6 | ## Current Status 7 | 8 | "Hello World" OS using the MiniUART. ~Tested and working on real hardware.~ 9 | It has regressed on real hardware, and I haven't fixed it yet. 10 | 11 | ## Building 12 | 13 | ``` 14 | zig build 15 | ``` 16 | 17 | ## Testing 18 | 19 | ### QEMU 20 | 21 | ``` 22 | zig build qemu 23 | ``` 24 | 25 | #### Debugging with GDB 26 | 27 | ``` 28 | zig build qemu -Dgdb 29 | ``` 30 | 31 | In another terminal: 32 | 33 | ``` 34 | gdb zig-cache/clashos-dbg -ex 'target remote localhost:1234' 35 | ``` 36 | 37 | ### Sending a New Kernel Image via Serial 38 | 39 | While the Raspberry Pi is running, you can use 40 | 41 | ``` 42 | zig build upload -Dtty=/dev/ttyUSB0 43 | ``` 44 | 45 | If using QEMU, use `zig build qemu -Dpty` and note the tty path. 46 | In another terminal window, `cat` the tty path. 47 | In yet another terminal window, you can use the `zig build upload` 48 | command above, with the tty path provided by QEMU. 49 | This is compatible with using GDB with QEMU, just make sure to pass 50 | the `-Dgdb` to both `zig build` commands. 51 | 52 | ### Actual Hardware 53 | 54 | 1. Mount an sdcard with a single FAT32 partition. 55 | 2. Copy `boot/*` to `/path/to/sdcard/*`. 56 | 3. `zig build` 57 | 4. Copy `clashos.bin` to `/path/to/sdcard/kernel7.img`. 58 | 59 | For further changes repeat steps 3 and 4. 60 | 61 | ## Roadmap 62 | 63 | * Interface with the file system 64 | * Get rid of dependency on binutils objcopy 65 | * Interface with the video driver 66 | * Get a simple joystick and button and use GPIO 67 | * Sound (should it be the analog or over HDMI)? 68 | * Make the game 69 | * Build arcade cabinets 70 | 71 | ## Documentation 72 | 73 | ### EZSync 012 USB Cable 74 | 75 | * Black: Pin 6, Ground 76 | * Yellow: Pin 8, BCM 14, TXD / Transmit 77 | * Orange: Pin 10, BCM 15, RXD / Receive 78 | 79 | ### How to view the serial data 80 | 81 | Where `/dev/ttyUSB0` is the device that represents the serial-to-USB cable: 82 | 83 | ``` 84 | sudo screen /dev/ttyUSB0 115200 cs8 85 | ``` 86 | 87 | ### Memory Layout 88 | 89 | ``` 90 | 0x0000000 ( 0 MiB) - boot entry point 91 | 0x0001000 - shortExceptionHandlerAt0x1000 function 92 | 0x0001100 - kernelMainAt0x1100 function 93 | 0x8000000 (128 MiB) - top of kernel stack, and bootloader_main function 94 | 0x8800000 (136 MiB) - top of bootloader stack 95 | ``` 96 | -------------------------------------------------------------------------------- /assets/zig-icon.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/clashos/8ca226eb088d2a29f9a4875fd1245abb9842940b/assets/zig-icon.bmp -------------------------------------------------------------------------------- /assets/zig-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assets/zig-logo.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/clashos/8ca226eb088d2a29f9a4875fd1245abb9842940b/assets/zig-logo.bmp -------------------------------------------------------------------------------- /assets/zig-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /boot/bootcode.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/clashos/8ca226eb088d2a29f9a4875fd1245abb9842940b/boot/bootcode.bin -------------------------------------------------------------------------------- /boot/config.txt: -------------------------------------------------------------------------------- 1 | # This file is the BIOS equivalent on the RPI. 2 | # https://www.raspberrypi.org/documentation/configuration/config-txt/ 3 | 4 | disable_commandline_tags=1 5 | kernel_old=1 6 | dtparam=audio=on 7 | arm_control=0x200 8 | disable_splash=1 9 | boot_delay=0 10 | cec_osd_name=Zig! 11 | -------------------------------------------------------------------------------- /boot/fixup.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/clashos/8ca226eb088d2a29f9a4875fd1245abb9842940b/boot/fixup.dat -------------------------------------------------------------------------------- /boot/start.elf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/clashos/8ca226eb088d2a29f9a4875fd1245abb9842940b/boot/start.elf -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Builder = std.build.Builder; 3 | const builtin = @import("builtin"); 4 | 5 | pub fn build(b: *Builder) !void { 6 | const mode = b.standardReleaseOptions(); 7 | const want_gdb = b.option(bool, "gdb", "Build for using gdb with qemu") orelse false; 8 | const want_pty = b.option(bool, "pty", "Create a separate TTY path") orelse false; 9 | const want_nodisplay = b.option(bool, "nodisplay", "No display for qemu") orelse false; 10 | 11 | const arch = builtin.Arch{ .aarch64 = .v8a }; 12 | const environ = builtin.Abi.eabihf; 13 | 14 | // First we build just the bootloader executable, and then we build the actual kernel 15 | // which uses @embedFile on the bootloader. 16 | const bootloader = b.addExecutable("bootloader", "src/bootloader.zig"); 17 | bootloader.setLinkerScriptPath("src/bootloader.ld"); 18 | bootloader.setBuildMode(builtin.Mode.ReleaseSmall); 19 | bootloader.setTarget(arch, builtin.Os.freestanding, environ); 20 | bootloader.strip = true; 21 | bootloader.setOutputDir("zig-cache"); 22 | 23 | const exec_name = if (want_gdb) "clashos-dbg" else "clashos"; 24 | const exe = b.addExecutable(exec_name, "src/main.zig"); 25 | exe.setOutputDir("zig-cache"); 26 | exe.setBuildMode(mode); 27 | exe.setTarget(arch, builtin.Os.freestanding, environ); 28 | const linker_script = if (want_gdb) "src/qemu-gdb.ld" else "src/linker.ld"; 29 | exe.setLinkerScriptPath(linker_script); 30 | exe.addBuildOption([]const u8, "bootloader_exe_path", b.fmt("\"{}\"", .{bootloader.getOutputPath()})); 31 | exe.step.dependOn(&bootloader.step); 32 | 33 | const run_objcopy = b.addSystemCommand(&[_][]const u8{ 34 | "llvm-objcopy", exe.getOutputPath(), 35 | "-O", "binary", 36 | "clashos.bin", 37 | }); 38 | run_objcopy.step.dependOn(&exe.step); 39 | 40 | b.default_step.dependOn(&run_objcopy.step); 41 | 42 | const qemu = b.step("qemu", "Run the OS in qemu"); 43 | var qemu_args = std.ArrayList([]const u8).init(b.allocator); 44 | try qemu_args.appendSlice(&[_][]const u8{ 45 | "qemu-system-aarch64", 46 | "-kernel", 47 | exe.getOutputPath(), 48 | "-m", 49 | "256", 50 | "-M", 51 | "raspi3", 52 | "-serial", 53 | "null", 54 | "-serial", 55 | if (want_pty) "pty" else "stdio", 56 | "-display", 57 | if (want_nodisplay) "none" else "gtk", 58 | }); 59 | if (want_gdb) { 60 | try qemu_args.appendSlice(&[_][]const u8{ "-S", "-s" }); 61 | } 62 | const run_qemu = b.addSystemCommand(qemu_args.toSliceConst()); 63 | qemu.dependOn(&run_qemu.step); 64 | run_qemu.step.dependOn(&exe.step); 65 | 66 | const send_image_tool = b.addExecutable("send_image", "tools/send_image.zig"); 67 | 68 | const run_send_image_tool = send_image_tool.run(); 69 | if (b.option([]const u8, "tty", "Specify the TTY to send images to")) |tty_path| { 70 | run_send_image_tool.addArg(tty_path); 71 | } 72 | 73 | const upload = b.step("upload", "Send a new kernel image to a running instance. (See -Dtty option)"); 74 | upload.dependOn(&run_objcopy.step); 75 | upload.dependOn(&run_send_image_tool.step); 76 | } 77 | -------------------------------------------------------------------------------- /src/bootloader.ld: -------------------------------------------------------------------------------- 1 | ENTRY(boot) 2 | 3 | SECTIONS { 4 | kernelMainAt0x1100 = 0x1100; /* Must match value from kernel linker script */ 5 | 6 | . = 0x8800000; /* Must match debug.bootloader_address */ 7 | 8 | .text : ALIGN(4K) { 9 | KEEP(*(.text.first)) 10 | *(.text) 11 | } 12 | 13 | .rodata : ALIGN(4K) { 14 | *(.rodata) 15 | } 16 | 17 | .data : ALIGN(4K) { 18 | *(.data) 19 | } 20 | 21 | .bss : ALIGN(4K) { 22 | __bss_start = .; 23 | *(COMMON) 24 | *(.bss) 25 | __bss_end = .; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/bootloader.zig: -------------------------------------------------------------------------------- 1 | // The bootloader is a separate executable, the hardware does not boot 2 | // directly into it. When the kernel wants to load a new image from the 3 | // serial port, it copies the bootloader executable code into memory at 4 | // address bootloader_address which matches the linker script for the bootloader 5 | // executable. Then the kernel jumps to bootloader_address. The bootloader then 6 | // overwrites the kernel's code, which is why a separate bootloader 7 | // executable is necessary. 8 | const std = @import("std"); 9 | const builtin = @import("builtin"); 10 | const debug = @import("debug.zig"); 11 | const serial = @import("serial.zig"); 12 | 13 | export fn bootloader_main(start_addr: [*]u8, len: usize) linksection(".text.first") noreturn { 14 | var i: usize = 0; 15 | while (i < len) : (i += 1) { 16 | start_addr[i] = serial.readByte(); 17 | } 18 | asm volatile ( 19 | \\mov sp,#0x08000000 20 | \\bl kernelMainAt0x1100 21 | ); 22 | unreachable; 23 | } 24 | 25 | pub fn panic(message: []const u8, stack_trace: ?*builtin.StackTrace) noreturn { 26 | serial.log("BOOTLOADER PANIC: {}", .{message}); 27 | debug.wfe_hang(); 28 | } 29 | -------------------------------------------------------------------------------- /src/debug.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | const assert = std.debug.assert; 4 | const serial = @import("serial.zig"); 5 | 6 | extern var __debug_info_start: u8; 7 | extern var __debug_info_end: u8; 8 | extern var __debug_abbrev_start: u8; 9 | extern var __debug_abbrev_end: u8; 10 | extern var __debug_str_start: u8; 11 | extern var __debug_str_end: u8; 12 | extern var __debug_line_start: u8; 13 | extern var __debug_line_end: u8; 14 | extern var __debug_ranges_start: u8; 15 | extern var __debug_ranges_end: u8; 16 | 17 | const source_files = [_][]const u8{ 18 | "src/debug.zig", 19 | "src/video_core_mailboxes.zig", 20 | "src/bootloader.zig", 21 | "src/serial.zig", 22 | "src/video_core_metrics.zig", 23 | "src/video_core_properties.zig", 24 | "src/video_core_frame_buffer.zig", 25 | "src/time.zig", 26 | "src/main.zig", 27 | "src/mmio.zig", 28 | "src/slice_iterator.zig", 29 | }; 30 | 31 | var already_panicking: bool = false; 32 | 33 | pub fn panic(stack_trace: ?*builtin.StackTrace, comptime fmt: []const u8, args: var) noreturn { 34 | @setCold(true); 35 | if (already_panicking) { 36 | serial.log("\npanicked during kernel panic", .{}); 37 | wfe_hang(); 38 | } 39 | already_panicking = true; 40 | 41 | serial.log("panic: " ++ fmt, args); 42 | 43 | const first_trace_addr = @returnAddress(); 44 | if (stack_trace) |t| { 45 | dumpStackTrace(t); 46 | } 47 | dumpCurrentStackTrace(first_trace_addr); 48 | wfe_hang(); 49 | } 50 | 51 | pub fn wfe_hang() noreturn { 52 | while (true) { 53 | asm volatile ("wfe"); 54 | } 55 | } 56 | 57 | fn dwarfSectionFromSymbolAbs(start: *u8, end: *u8) std.debug.DwarfInfo.Section { 58 | return std.debug.DwarfInfo.Section{ 59 | .offset = 0, 60 | .size = @ptrToInt(end) - @ptrToInt(start), 61 | }; 62 | } 63 | 64 | fn dwarfSectionFromSymbol(start: *u8, end: *u8) std.debug.DwarfInfo.Section { 65 | return std.debug.DwarfInfo.Section{ 66 | .offset = @ptrToInt(start), 67 | .size = @ptrToInt(end) - @ptrToInt(start), 68 | }; 69 | } 70 | 71 | fn getSelfDebugInfo() !*std.debug.DwarfInfo { 72 | const S = struct { 73 | var have_self_debug_info = false; 74 | var self_debug_info: std.debug.DwarfInfo = undefined; 75 | 76 | var in_stream_state = std.io.InStream(anyerror){ .readFn = readFn }; 77 | var in_stream_pos: u64 = 0; 78 | const in_stream = &in_stream_state; 79 | 80 | fn readFn(self: *std.io.InStream(anyerror), buffer: []u8) anyerror!usize { 81 | const ptr = @intToPtr([*]const u8, in_stream_pos); 82 | @memcpy(buffer.ptr, ptr, buffer.len); 83 | in_stream_pos += buffer.len; 84 | return buffer.len; 85 | } 86 | 87 | const SeekableStream = std.io.SeekableStream(anyerror, anyerror); 88 | var seekable_stream_state = SeekableStream{ 89 | .seekToFn = seekToFn, 90 | .seekByFn = seekByFn, 91 | 92 | .getPosFn = getPosFn, 93 | .getEndPosFn = getEndPosFn, 94 | }; 95 | const seekable_stream = &seekable_stream_state; 96 | 97 | fn seekToFn(self: *SeekableStream, pos: u64) anyerror!void { 98 | in_stream_pos = pos; 99 | } 100 | fn seekByFn(self: *SeekableStream, pos: i64) anyerror!void { 101 | in_stream_pos = @bitCast(usize, @bitCast(isize, in_stream_pos) +% pos); 102 | } 103 | fn getPosFn(self: *SeekableStream) anyerror!u64 { 104 | return in_stream_pos; 105 | } 106 | fn getEndPosFn(self: *SeekableStream) anyerror!u64 { 107 | return @as(u64, @ptrToInt(&__debug_ranges_end)); 108 | } 109 | }; 110 | if (S.have_self_debug_info) return &S.self_debug_info; 111 | 112 | S.self_debug_info = std.debug.DwarfInfo{ 113 | .dwarf_seekable_stream = S.seekable_stream, 114 | .dwarf_in_stream = S.in_stream, 115 | .endian = builtin.Endian.Little, 116 | .debug_info = dwarfSectionFromSymbol(&__debug_info_start, &__debug_info_end), 117 | .debug_abbrev = dwarfSectionFromSymbolAbs(&__debug_abbrev_start, &__debug_abbrev_end), 118 | .debug_str = dwarfSectionFromSymbolAbs(&__debug_str_start, &__debug_str_end), 119 | .debug_line = dwarfSectionFromSymbol(&__debug_line_start, &__debug_line_end), 120 | .debug_ranges = dwarfSectionFromSymbolAbs(&__debug_ranges_start, &__debug_ranges_end), 121 | .abbrev_table_list = undefined, 122 | .compile_unit_list = undefined, 123 | .func_list = undefined, 124 | }; 125 | try std.debug.openDwarfDebugInfo(&S.self_debug_info, kernel_panic_allocator); 126 | return &S.self_debug_info; 127 | } 128 | 129 | var serial_out_stream_state = std.io.OutStream(anyerror){ 130 | .writeFn = struct { 131 | fn logWithSerial(self: *std.io.OutStream(anyerror), bytes: []const u8) anyerror!void { 132 | serial.writeText(bytes); 133 | } 134 | }.logWithSerial, 135 | }; 136 | const serial_out_stream = &serial_out_stream_state; 137 | var kernel_panic_allocator_bytes: [5 * 1024 * 1024]u8 = undefined; 138 | var kernel_panic_allocator_state = std.heap.FixedBufferAllocator.init(kernel_panic_allocator_bytes[0..]); 139 | const kernel_panic_allocator = &kernel_panic_allocator_state.allocator; 140 | 141 | pub fn dumpStackTrace(stack_trace: *const builtin.StackTrace) void { 142 | const dwarf_info = getSelfDebugInfo() catch |err| { 143 | serial.log("Unable to dump stack trace: Unable to open debug info: {}", .{@errorName(err)}); 144 | return; 145 | }; 146 | writeStackTrace(stack_trace, dwarf_info) catch |err| { 147 | serial.log("Unable to dump stack trace: {}", .{@errorName(err)}); 148 | return; 149 | }; 150 | } 151 | 152 | pub fn dumpCurrentStackTrace(start_addr: ?usize) void { 153 | const dwarf_info = getSelfDebugInfo() catch |err| { 154 | serial.log("Unable to dump stack trace: Unable to open debug info: {}", .{@errorName(err)}); 155 | return; 156 | }; 157 | writeCurrentStackTrace(dwarf_info, start_addr) catch |err| { 158 | serial.log("Unable to dump stack trace: {}", .{@errorName(err)}); 159 | return; 160 | }; 161 | } 162 | 163 | fn printLineFromBuffer(out_stream: var, contents: []const u8, line_info: std.debug.LineInfo) anyerror!void { 164 | var line: usize = 1; 165 | var column: usize = 1; 166 | var abs_index: usize = 0; 167 | for (contents) |byte| { 168 | if (line == line_info.line) { 169 | try out_stream.writeByte(byte); 170 | if (byte == '\n') { 171 | return; 172 | } 173 | } 174 | if (byte == '\n') { 175 | line += 1; 176 | column = 1; 177 | } else { 178 | column += 1; 179 | } 180 | } 181 | return error.EndOfFile; 182 | } 183 | 184 | fn printLineFromFile(out_stream: var, line_info: std.debug.LineInfo) anyerror!void { 185 | inline for (source_files) |src_path| { 186 | if (std.mem.endsWith(u8, line_info.file_name, src_path)) { 187 | const contents = @embedFile("../" ++ src_path); 188 | try printLineFromBuffer(out_stream, contents[0..], line_info); 189 | return; 190 | } 191 | } 192 | try out_stream.print("(source file {} not added in std/debug.zig)\n", .{line_info.file_name}); 193 | } 194 | 195 | fn writeCurrentStackTrace(dwarf_info: *std.debug.DwarfInfo, start_addr: ?usize) !void { 196 | var it = std.debug.StackIterator.init(start_addr); 197 | while (it.next()) |return_address| { 198 | try dwarf_info.printSourceAtAddress( 199 | serial_out_stream, 200 | return_address, 201 | .escape_codes, 202 | printLineFromFile, 203 | ); 204 | } 205 | } 206 | 207 | fn writeStackTrace(stack_trace: *const builtin.StackTrace, dwarf_info: *std.debug.DwarfInfo) !void { 208 | var frame_index: usize = undefined; 209 | var frames_left: usize = undefined; 210 | if (stack_trace.index < stack_trace.instruction_addresses.len) { 211 | frame_index = 0; 212 | frames_left = stack_trace.index; 213 | } else { 214 | frame_index = (stack_trace.index + 1) % stack_trace.instruction_addresses.len; 215 | frames_left = stack_trace.instruction_addresses.len; 216 | } 217 | 218 | while (frames_left != 0) : ({ 219 | frames_left -= 1; 220 | frame_index = (frame_index + 1) % stack_trace.instruction_addresses.len; 221 | }) { 222 | const return_address = stack_trace.instruction_addresses[frame_index]; 223 | try dwarf_info.printSourceAtAddress( 224 | serial_out_stream, 225 | return_address, 226 | .escape_codes, 227 | printLineFromFile, 228 | ); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/linker.ld: -------------------------------------------------------------------------------- 1 | ENTRY(_start) 2 | 3 | SECTIONS { 4 | . = 0x0000; 5 | 6 | .text : ALIGN(4K) { 7 | KEEP(*(.text.boot)) 8 | __end_init = .; 9 | . = 0x1000; 10 | KEEP(*(.text.exception)) 11 | . = 0x1100; /* Must match address from bootloader.ld */ 12 | KEEP(*(.text.main)) 13 | *(.text) 14 | } 15 | 16 | .rodata : ALIGN(4K) { 17 | *(.rodata) 18 | __debug_info_start = .; 19 | KEEP(*(.debug_info)) 20 | __debug_info_end = .; 21 | __debug_abbrev_start = .; 22 | KEEP(*(.debug_abbrev)) 23 | __debug_abbrev_end = .; 24 | __debug_str_start = .; 25 | KEEP(*(.debug_str)) 26 | __debug_str_end = .; 27 | __debug_line_start = .; 28 | KEEP(*(.debug_line)) 29 | __debug_line_end = .; 30 | __debug_ranges_start = .; 31 | KEEP(*(.debug_ranges)) 32 | __debug_ranges_end = .; 33 | } 34 | 35 | .data : ALIGN(4K) { 36 | *(.data) 37 | } 38 | 39 | .bss : ALIGN(4K) { 40 | __bss_start = .; 41 | *(COMMON) 42 | *(.bss) 43 | __bss_end = .; 44 | } 45 | 46 | bootloader_main = 0x8800000; /* Must match bootloader linker script */ 47 | } 48 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const Bitmap = @import("video_core_frame_buffer.zig").Bitmap; 3 | const Color = @import("video_core_frame_buffer.zig").Color; 4 | const debug = @import("debug.zig"); 5 | const FrameBuffer = @import("video_core_frame_buffer.zig").FrameBuffer; 6 | const Metrics = @import("video_core_metrics.zig").Metrics; 7 | const serial = @import("serial.zig"); 8 | const time = @import("time.zig"); 9 | const std = @import("std"); 10 | 11 | // The linker will make the address of these global variables equal 12 | // to the value we are interested in. The memory at the address 13 | // could alias any uninitialized global variable in the kernel. 14 | extern var __bss_start: u8; 15 | extern var __bss_end: u8; 16 | extern var __end_init: u8; 17 | 18 | comptime { 19 | // .text.boot to keep this in the first portion of the binary 20 | // Note: this code cannot be changed via the bootloader. 21 | asm ( 22 | \\.section .text.boot 23 | \\.globl _start 24 | \\_start: 25 | \\ mrs x0,mpidr_el1 26 | \\ mov x1,#0xC1000000 27 | \\ bic x0,x0,x1 28 | \\ cbz x0,master 29 | \\ b hang 30 | \\master: 31 | \\ mov sp,#0x08000000 32 | \\ mov x0,#0x800 //exception_vector_table 33 | \\ msr vbar_el3,x0 34 | \\ msr vbar_el2,x0 35 | \\ msr vbar_el1,x0 36 | \\ bl kernelMainAt0x1100 37 | \\hang: 38 | \\ wfe 39 | \\ b hang 40 | \\exception_vector_table: 41 | \\.balign 0x800 42 | \\.balign 0x80 43 | \\ b shortExceptionHandlerAt0x1000 44 | \\.balign 0x80 45 | \\ b shortExceptionHandlerAt0x1000 46 | \\.balign 0x80 47 | \\ b shortExceptionHandlerAt0x1000 48 | \\.balign 0x80 49 | \\ b shortExceptionHandlerAt0x1000 50 | \\.balign 0x80 51 | \\ b shortExceptionHandlerAt0x1000 52 | \\.balign 0x80 53 | \\ b shortExceptionHandlerAt0x1000 54 | \\.balign 0x80 55 | \\ b shortExceptionHandlerAt0x1000 56 | \\.balign 0x80 57 | \\ b shortExceptionHandlerAt0x1000 58 | \\.balign 0x80 59 | \\ b shortExceptionHandlerAt0x1000 60 | \\.balign 0x80 61 | \\ b shortExceptionHandlerAt0x1000 62 | \\.balign 0x80 63 | \\ b shortExceptionHandlerAt0x1000 64 | \\.balign 0x80 65 | \\ b shortExceptionHandlerAt0x1000 66 | \\.balign 0x80 67 | \\ b shortExceptionHandlerAt0x1000 68 | \\.balign 0x80 69 | \\ b shortExceptionHandlerAt0x1000 70 | \\.balign 0x80 71 | \\ b shortExceptionHandlerAt0x1000 72 | \\.balign 0x80 73 | \\ b shortExceptionHandlerAt0x1000 74 | ); 75 | } 76 | 77 | export fn shortExceptionHandlerAt0x1000() linksection(".text.exception") void { 78 | exceptionHandler(); 79 | } 80 | 81 | pub fn panic(message: []const u8, trace: ?*builtin.StackTrace) noreturn { 82 | debug.panic(trace, "KERNEL PANIC: {}", .{message}); 83 | } 84 | 85 | fn exceptionHandler() void { 86 | serial.log("arm exception taken", .{}); 87 | var current_el = asm ("mrs %[current_el], CurrentEL" 88 | : [current_el] "=r" (-> usize) 89 | ); 90 | serial.log("CurrentEL {x} exception level {}", .{ current_el, current_el >> 2 & 0x3 }); 91 | var esr_el3 = asm ("mrs %[esr_el3], esr_el3" 92 | : [esr_el3] "=r" (-> usize) 93 | ); 94 | serial.log("esr_el3 {x} code 0x{x}", .{ esr_el3, esr_el3 >> 26 & 0x3f }); 95 | var elr_el3 = asm ("mrs %[elr_el3], elr_el3" 96 | : [elr_el3] "=r" (-> usize) 97 | ); 98 | serial.log("elr_el3 {x}", .{elr_el3}); 99 | var spsr_el3 = asm ("mrs %[spsr_el3], spsr_el3" 100 | : [spsr_el3] "=r" (-> usize) 101 | ); 102 | serial.log("spsr_el3 {x}", .{spsr_el3}); 103 | var far_el3 = asm ("mrs %[far_el3], far_el3" 104 | : [far_el3] "=r" (-> usize) 105 | ); 106 | serial.log("far_el3 {x}", .{far_el3}); 107 | serial.log("execution is now stopped in arm exception handler", .{}); 108 | while (true) { 109 | asm volatile ("wfe"); 110 | } 111 | } 112 | 113 | export fn kernelMainAt0x1100() linksection(".text.main") noreturn { 114 | // clear .bss 115 | @memset(@as(*volatile [1]u8, &__bss_start), 0, @ptrToInt(&__bss_end) - @ptrToInt(&__bss_start)); 116 | 117 | serial.init(); 118 | serial.log("\n{} {} ...", .{ name, version }); 119 | 120 | time.init(); 121 | metrics.init(); 122 | 123 | fb.init(&metrics); 124 | icon.init(&fb, &logo_bmp_file); 125 | logo.init(&fb, &logo_bmp_file); 126 | 127 | screen_activity.init(); 128 | serial_activity.init(); 129 | 130 | while (true) { 131 | screen_activity.update(); 132 | serial_activity.update(); 133 | } 134 | } 135 | 136 | const ScreenActivity = struct { 137 | height: u32, 138 | color: Color, 139 | color32: u32, 140 | top: u32, 141 | x: i32, 142 | y: i32, 143 | vel_x: i32, 144 | vel_y: i32, 145 | ref_seconds: f32, 146 | pixel_counter: u32, 147 | 148 | fn init(self: *ScreenActivity) void { 149 | self.color = color_yellow; 150 | self.color32 = self.color.to32(); 151 | self.height = logo.height; 152 | self.top = logo.height + margin; 153 | self.x = 0; 154 | self.y = 0; 155 | self.vel_x = 10; 156 | self.vel_y = 10; 157 | time.update(); 158 | self.ref_seconds = time.seconds; 159 | self.pixel_counter = 0; 160 | } 161 | 162 | fn update(self: *ScreenActivity) void { 163 | time.update(); 164 | const new_ref_secs = self.ref_seconds + 0.05; 165 | if (time.seconds >= new_ref_secs) { 166 | const clear_x = @intCast(u32, self.x); 167 | const clear_y = @intCast(u32, self.y); 168 | fb.clearRect(clear_x, clear_y, logo.width, logo.height, color_black); 169 | 170 | self.ref_seconds = new_ref_secs; 171 | self.x += self.vel_x; 172 | self.y += self.vel_y; 173 | 174 | if (self.x + @as(i32, logo.width) >= @intCast(i32, fb.virtual_width)) { 175 | self.x = @intCast(i32, fb.virtual_width - logo.width); 176 | self.vel_x = -self.vel_x; 177 | } 178 | if (self.y + @as(i32, logo.height) >= @intCast(i32, fb.virtual_height)) { 179 | self.y = @intCast(i32, fb.virtual_height - logo.height); 180 | self.vel_y = -self.vel_y; 181 | } 182 | if (self.x < 0) { 183 | self.x = 0; 184 | self.vel_x = -self.vel_x; 185 | } 186 | if (self.y < 0) { 187 | self.y = 0; 188 | self.vel_y = -self.vel_y; 189 | } 190 | const draw_x = @intCast(u32, self.x); 191 | const draw_y = @intCast(u32, self.y); 192 | logo.drawRect(logo.width, logo.height, 0, 0, draw_x, draw_y); 193 | } 194 | } 195 | }; 196 | 197 | const SerialActivity = struct { 198 | boot_magic_index: usize, 199 | 200 | fn init(self: *SerialActivity) void { 201 | self.boot_magic_index = 0; 202 | serial.log("now echoing input on uart1 ...", .{}); 203 | } 204 | 205 | fn update(self: *SerialActivity) void { 206 | if (!serial.isReadByteReady()) { 207 | return; 208 | } 209 | const boot_magic = [_]u8{ 6, 6, 6 }; 210 | const byte = serial.readByte(); 211 | if (byte == boot_magic[self.boot_magic_index]) { 212 | self.boot_magic_index += 1; 213 | if (self.boot_magic_index != boot_magic.len) 214 | return; 215 | 216 | // It's time to receive the new kernel. First 217 | // we skip over the .text.boot bytes, verifying that they 218 | // are unchanged. 219 | const new_kernel_len = serial.in.readIntLittle(u32) catch unreachable; 220 | serial.log("New kernel image detected, {Bi:2}", .{new_kernel_len}); 221 | const text_boot = @intToPtr([*]allowzero const u8, 0)[0..@ptrToInt(&__end_init)]; 222 | for (text_boot) |text_boot_byte, byte_index| { 223 | const new_byte = serial.readByte(); 224 | if (new_byte != text_boot_byte) { 225 | debug.panic(@errorReturnTrace(), "new_kernel[{}] expected: 0x{x} actual: 0x{x}", .{ 226 | byte_index, 227 | text_boot_byte, 228 | new_byte, 229 | }); 230 | } 231 | } 232 | const start_addr = @ptrToInt(shortExceptionHandlerAt0x1000); 233 | const bytes_left = new_kernel_len - start_addr; 234 | var pad = start_addr - text_boot.len; 235 | while (pad > 0) : (pad -= 1) { 236 | _ = serial.readByte(); 237 | } 238 | 239 | // Next we copy the bootloader code to the correct memory address, 240 | // and then jump to it. 241 | // Read the ELF 242 | var bootloader_code_ptr = @as([*]const u8, &bootloader_code); // TODO remove workaround `var` 243 | const ehdr = @ptrCast(*const std.elf.Elf64_Ehdr, bootloader_code_ptr); 244 | var phdr_addr = bootloader_code_ptr + ehdr.e_phoff; 245 | var phdr_i: usize = 0; 246 | while (phdr_i < ehdr.e_phnum) : ({ 247 | phdr_i += 1; 248 | phdr_addr += ehdr.e_phentsize; 249 | }) { 250 | const this_ph = @ptrCast(*const std.elf.Elf64_Phdr, @alignCast(@alignOf(std.elf.Elf64_Phdr), phdr_addr)); 251 | switch (this_ph.p_type) { 252 | std.elf.PT_LOAD => { 253 | const src_ptr = bootloader_code_ptr + this_ph.p_offset; 254 | const src_len = this_ph.p_filesz; 255 | const dest_ptr = @intToPtr([*]u8, this_ph.p_vaddr); 256 | const dest_len = this_ph.p_memsz; 257 | const pad_len = dest_len - src_len; 258 | const copy_len = dest_len - pad_len; 259 | @memcpy(dest_ptr, src_ptr, copy_len); 260 | @memset(dest_ptr + copy_len, 0, pad_len); 261 | }, 262 | std.elf.PT_GNU_STACK => {}, // ignore 263 | else => debug.panic(@errorReturnTrace(), "unexpected ELF Program Header load type: {}", .{this_ph.p_type}), 264 | } 265 | } 266 | serial.log("Loading new image...", .{}); 267 | asm volatile ( 268 | \\mov sp,#0x08000000 269 | \\bl bootloader_main 270 | : 271 | : [arg0] "{x0}" (start_addr), 272 | [arg1] "{x1}" (bytes_left) 273 | ); 274 | unreachable; 275 | } 276 | switch (byte) { 277 | '\r' => { 278 | serial.writeText("\n"); 279 | }, 280 | else => serial.writeByte(byte), 281 | } 282 | } 283 | }; 284 | 285 | const build_options = @import("build_options"); 286 | const bootloader_code align(@alignOf(std.elf.Elf64_Ehdr)) = @embedFile("../" ++ build_options.bootloader_exe_path).*; 287 | 288 | var screen_activity: ScreenActivity = undefined; 289 | var serial_activity: SerialActivity = undefined; 290 | var fb: FrameBuffer = undefined; 291 | var metrics: Metrics = undefined; 292 | var icon: Bitmap = undefined; 293 | var logo: Bitmap = undefined; 294 | 295 | var icon_bmp_file align(@alignOf(u32)) = @embedFile("../assets/zig-icon.bmp").*; 296 | var logo_bmp_file align(@alignOf(u32)) = @embedFile("../assets/zig-logo.bmp").*; 297 | 298 | const margin = 10; 299 | 300 | const color_red = Color{ .red = 255, .green = 0, .blue = 0, .alpha = 255 }; 301 | const color_green = Color{ .red = 0, .green = 255, .blue = 0, .alpha = 255 }; 302 | const color_blue = Color{ .red = 0, .green = 0, .blue = 255, .alpha = 255 }; 303 | const color_yellow = Color{ .red = 255, .green = 255, .blue = 0, .alpha = 255 }; 304 | const color_white = Color{ .red = 255, .green = 255, .blue = 255, .alpha = 255 }; 305 | const color_black = Color{ .red = 0, .green = 0, .blue = 0, .alpha = 255 }; 306 | 307 | const name = "ClashOS"; 308 | const version = "0.2"; 309 | -------------------------------------------------------------------------------- /src/mmio.zig: -------------------------------------------------------------------------------- 1 | const AtomicOrder = @import("builtin").AtomicOrder; 2 | 3 | pub fn write(reg: usize, data: u32) void { 4 | @fence(AtomicOrder.SeqCst); 5 | @intToPtr(*volatile u32, reg).* = data; 6 | } 7 | 8 | pub fn read(reg: usize) u32 { 9 | @fence(AtomicOrder.SeqCst); 10 | return @intToPtr(*volatile u32, reg).*; 11 | } 12 | 13 | pub fn dsb() void { 14 | asm volatile ("dsb st"); 15 | } 16 | 17 | pub fn bigTimeExtraMemoryBarrier() void { 18 | asm volatile ( 19 | \\ mcr p15, 0, ip, c7, c5, 0 @ invalidate I cache 20 | \\ mcr p15, 0, ip, c7, c5, 6 @ invalidate BTB 21 | \\ dsb 22 | \\ isb 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/qemu-gdb.ld: -------------------------------------------------------------------------------- 1 | ENTRY(_start) 2 | 3 | SECTIONS { 4 | . = 0x0000; 5 | 6 | .text : ALIGN(4K) { 7 | KEEP(*(.text.boot)) 8 | __end_init = .; 9 | . = 0x1000; /* Must match address from bootloader.ld */ 10 | KEEP(*(.text.main)) 11 | *(.text) 12 | } 13 | 14 | .rodata : ALIGN(4K) { 15 | *(.rodata) 16 | } 17 | 18 | .data : ALIGN(4K) { 19 | *(.data) 20 | } 21 | 22 | .bss : ALIGN(4K) { 23 | __bss_start = .; 24 | *(COMMON) 25 | *(.bss) 26 | __bss_end = .; 27 | __debug_info_start = .; 28 | __debug_info_end = .; 29 | __debug_abbrev_start = .; 30 | __debug_abbrev_end = .; 31 | __debug_str_start = .; 32 | __debug_str_end = .; 33 | __debug_line_start = .; 34 | __debug_line_end = .; 35 | __debug_ranges_start = .; 36 | __debug_ranges_end = .; 37 | } 38 | 39 | bootloader_main = 0x8800000; /* Must match bootloader linker script */ 40 | } 41 | -------------------------------------------------------------------------------- /src/serial.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fmt = std.fmt; 3 | const mmio = @import("mmio.zig"); 4 | 5 | pub const GPFSEL1 = 0x3F200004; 6 | pub const GPSET0 = 0x3F20001C; 7 | pub const GPCLR0 = 0x3F200028; 8 | pub const GPPUD = 0x3F200094; 9 | pub const GPPUDCLK0 = 0x3F200098; 10 | 11 | pub const AUX_ENABLES = 0x3F215004; 12 | pub const AUX_MU_IO_REG = 0x3F215040; 13 | pub const AUX_MU_IER_REG = 0x3F215044; 14 | pub const AUX_MU_IIR_REG = 0x3F215048; 15 | pub const AUX_MU_LCR_REG = 0x3F21504C; 16 | pub const AUX_MU_MCR_REG = 0x3F215050; 17 | pub const AUX_MU_LSR_REG = 0x3F215054; 18 | pub const AUX_MU_MSR_REG = 0x3F215058; 19 | pub const AUX_MU_SCRATCH = 0x3F21505C; 20 | pub const AUX_MU_CNTL_REG = 0x3F215060; 21 | pub const AUX_MU_STAT_REG = 0x3F215064; 22 | pub const AUX_MU_BAUD_REG = 0x3F215068; 23 | 24 | pub const in = &in_stream_state; 25 | pub const out = &out_stream_state; 26 | 27 | const NoError = error{}; 28 | 29 | var in_stream_state = std.io.InStream(NoError){ 30 | .readFn = struct { 31 | fn readFn(self: *std.io.InStream(NoError), buffer: []u8) NoError!usize { 32 | for (buffer) |*byte| { 33 | byte.* = readByte(); 34 | } 35 | return buffer.len; 36 | } 37 | }.readFn, 38 | }; 39 | 40 | var out_stream_state = std.io.OutStream(NoError){ 41 | .writeFn = struct { 42 | fn writeFn(self: *std.io.OutStream(NoError), bytes: []const u8) NoError!void { 43 | for (bytes) |byte| { 44 | writeByte(byte); 45 | } 46 | return buffer.len; 47 | } 48 | }.writeFn, 49 | }; 50 | 51 | pub fn writeByte(byte: u8) void { 52 | // Wait for UART to become ready to transmit. 53 | while ((mmio.read(AUX_MU_LSR_REG) & 0x20) == 0) {} 54 | mmio.write(AUX_MU_IO_REG, byte); 55 | } 56 | 57 | pub fn isReadByteReady() bool { 58 | return mmio.read(AUX_MU_LSR_REG) & 0x01 != 0; 59 | } 60 | 61 | pub fn readByte() u8 { 62 | // Wait for UART to have recieved something. 63 | while (!isReadByteReady()) {} 64 | return @truncate(u8, mmio.read(AUX_MU_IO_REG)); 65 | } 66 | 67 | pub fn write(buffer: []const u8) void { 68 | for (buffer) |c| 69 | writeByte(c); 70 | } 71 | 72 | /// Translates \n into \r\n 73 | pub fn writeText(buffer: []const u8) void { 74 | for (buffer) |c| { 75 | switch (c) { 76 | '\n' => { 77 | writeByte('\r'); 78 | writeByte('\n'); 79 | }, 80 | else => writeByte(c), 81 | } 82 | } 83 | } 84 | 85 | pub fn init() void { 86 | mmio.write(AUX_ENABLES, 1); 87 | mmio.write(AUX_MU_IER_REG, 0); 88 | 89 | mmio.write(AUX_MU_CNTL_REG, 0); 90 | mmio.write(AUX_MU_LCR_REG, 3); 91 | mmio.write(AUX_MU_MCR_REG, 0); 92 | mmio.write(AUX_MU_IER_REG, 0); 93 | mmio.write(AUX_MU_IIR_REG, 0xC6); 94 | mmio.write(AUX_MU_BAUD_REG, 270); 95 | var ra = mmio.read(GPFSEL1); 96 | ra &= ~@as(u32, 7 << 12); //gpio14 97 | ra |= 2 << 12; //alt5 98 | ra &= ~@as(u32, 7 << 15); //gpio15 99 | ra |= 2 << 15; //alt5 100 | mmio.write(GPFSEL1, ra); 101 | mmio.write(GPPUD, 0); 102 | delay(150); 103 | mmio.write(GPPUDCLK0, (1 << 14) | (1 << 15)); 104 | delay(150); 105 | mmio.write(GPPUDCLK0, 0); 106 | mmio.write(AUX_MU_CNTL_REG, 3); 107 | } 108 | 109 | pub fn log(comptime format: []const u8, args: var) void { 110 | fmt.format({}, NoError, logBytes, format ++ "\n", args) catch |e| switch (e) {}; 111 | } 112 | 113 | fn logBytes(context: void, bytes: []const u8) NoError!void { 114 | writeText(bytes); 115 | } 116 | 117 | pub fn dumpMemory(address: usize, size: usize) void { 118 | var i: usize = 0; 119 | while (i < size) : (i += 1) { 120 | const full_addr = address + i; 121 | 122 | if (i % 16 == 0) { 123 | log("\n0x{x8} ", full_addr); 124 | } else if (i % 8 == 0) { 125 | log(" "); 126 | } 127 | 128 | log(" {x2}", ((*const u8)(full_addr)).*); 129 | } 130 | log("\n"); 131 | } 132 | 133 | // Loop count times in a way that the compiler won't optimize away. 134 | fn delay(count: usize) void { 135 | var i: usize = 0; 136 | while (i < count) : (i += 1) { 137 | asm volatile ("mov w0, w0"); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/slice_iterator.zig: -------------------------------------------------------------------------------- 1 | pub fn of(comptime T: type) type { 2 | var Iterator = struct { 3 | const Self = @This(); 4 | data: []T, 5 | index: u32, 6 | 7 | pub fn init(data: []T) Self { 8 | var self: Self = undefined; 9 | self.data = data; 10 | self.index = 0; 11 | return self; 12 | } 13 | 14 | fn reset(self: *Self) void { 15 | self.index = 0; 16 | } 17 | 18 | fn add(self: *Self, item: T) void { 19 | self.advance(); 20 | self.data[self.index - 1] = item; 21 | } 22 | 23 | fn next(self: *Self) u32 { 24 | self.advance(); 25 | return self.data[self.index - 1]; 26 | } 27 | 28 | fn advance(self: *Self) void { 29 | if (self.index < self.data.len) { 30 | self.index += 1; 31 | } else { 32 | panic(@errorReturnTrace(), "BufferExhausted", .{}); 33 | } 34 | } 35 | }; 36 | 37 | return Iterator; 38 | } 39 | 40 | const panic = @import("debug.zig").panic; 41 | -------------------------------------------------------------------------------- /src/time.zig: -------------------------------------------------------------------------------- 1 | pub fn init() void { 2 | cntfrq = asm ("mrs %[cntfrq], cntfrq_el0" 3 | : [cntfrq] "=r" (-> usize) 4 | ); 5 | if (cntfrq == 0) { 6 | cntfrq = 1 * 1000 * 1000; 7 | } 8 | cntfrq_f32 = @intToFloat(f32, cntfrq); 9 | update(); 10 | } 11 | 12 | pub fn update() void { 13 | cntpct = asm ("mrs %[cntpct], cntpct_el0" 14 | : [cntpct] "=r" (-> usize) 15 | ); 16 | seconds = @intToFloat(f32, cntpct) / cntfrq_f32; 17 | milliseconds = cntpct / (cntfrq / 1000); 18 | } 19 | 20 | pub fn sleep(duration: f32) void { 21 | update(); 22 | const start = seconds; 23 | while (seconds - start < duration) { 24 | update(); 25 | } 26 | } 27 | 28 | pub var seconds: f32 = undefined; 29 | pub var milliseconds: usize = undefined; 30 | 31 | var cntfrq: usize = undefined; 32 | var cntfrq_f32: f32 = undefined; 33 | var cntpct: usize = undefined; 34 | -------------------------------------------------------------------------------- /src/video_core_frame_buffer.zig: -------------------------------------------------------------------------------- 1 | pub const FrameBuffer = struct { 2 | alignment: u32, 3 | alpha_mode: u32, 4 | depth: u32, 5 | physical_width: u32, 6 | physical_height: u32, 7 | pitch: u32, 8 | pixel_order: u32, 9 | words: [*]volatile u32, 10 | size: u32, 11 | virtual_height: u32, 12 | virtual_width: u32, 13 | virtual_offset_x: u32, 14 | virtual_offset_y: u32, 15 | overscan_top: u32, 16 | overscan_bottom: u32, 17 | overscan_left: u32, 18 | overscan_right: u32, 19 | 20 | fn clear(fb: *FrameBuffer, color: Color) void { 21 | var y: u32 = 0; 22 | while (y < fb.virtual_height) : (y += 1) { 23 | var x: u32 = 0; 24 | while (x < fb.virtual_width) : (x += 1) { 25 | fb.drawPixel(x, y, color); 26 | } 27 | } 28 | } 29 | 30 | fn clearRect(fb: *FrameBuffer, x2: u32, y2: u32, width: u32, height: u32, color: Color) void { 31 | var y: u32 = 0; 32 | while (y < height) : (y += 1) { 33 | var x: u32 = 0; 34 | while (x < width) : (x += 1) { 35 | fb.drawPixel(x + x2, y + y2, color); 36 | } 37 | } 38 | } 39 | 40 | fn drawPixel(fb: *FrameBuffer, x: u32, y: u32, color: Color) void { 41 | assert(x < fb.virtual_width); 42 | assert(y < fb.virtual_height); 43 | const offset = y * fb.pitch + x; 44 | fb.drawPixel32(x, y, color.to32()); 45 | } 46 | 47 | fn drawPixel32(fb: *FrameBuffer, x: u32, y: u32, color: u32) void { 48 | if (x >= fb.virtual_width or y >= fb.virtual_height) { 49 | panic(@errorReturnTrace(), "frame buffer index {}, {} does not fit in {}x{}", .{ x, y, fb.virtual_width, fb.virtual_height }); 50 | } 51 | fb.words[y * fb.pitch / 4 + x] = color; 52 | } 53 | 54 | pub fn init(fb: *FrameBuffer, metrics: *Metrics) void { 55 | var width: u32 = undefined; 56 | var height: u32 = undefined; 57 | if (metrics.is_qemu) { 58 | width = 1024; 59 | height = 768; 60 | } else { 61 | width = 1920; 62 | height = 1080; 63 | } 64 | fb.alignment = 256; 65 | fb.physical_width = width; 66 | fb.physical_height = height; 67 | fb.virtual_width = width; 68 | fb.virtual_height = height; 69 | fb.virtual_offset_x = 0; 70 | fb.virtual_offset_y = 0; 71 | fb.depth = 32; 72 | fb.pixel_order = 0; 73 | fb.alpha_mode = 0; 74 | 75 | var fb_addr: u32 = undefined; 76 | var arg = [_]PropertiesArg{ 77 | tag(TAG_ALLOCATE_FRAME_BUFFER, 8), 78 | in(&fb.alignment), 79 | out(&fb_addr), 80 | out(&fb.size), 81 | tag(TAG_SET_DEPTH, 4), 82 | set(&fb.depth), 83 | tag(TAG_SET_PHYSICAL_WIDTH_HEIGHT, 8), 84 | set(&fb.physical_width), 85 | set(&fb.physical_height), 86 | tag(TAG_SET_PIXEL_ORDER, 4), 87 | set(&fb.pixel_order), 88 | tag(TAG_SET_VIRTUAL_WIDTH_HEIGHT, 8), 89 | set(&fb.virtual_width), 90 | set(&fb.virtual_height), 91 | tag(TAG_SET_VIRTUAL_OFFSET, 8), 92 | set(&fb.virtual_offset_x), 93 | set(&fb.virtual_offset_y), 94 | tag(TAG_SET_ALPHA_MODE, 4), 95 | set(&fb.alpha_mode), 96 | tag(TAG_GET_PITCH, 4), 97 | out(&fb.pitch), 98 | tag(TAG_GET_OVERSCAN, 16), 99 | out(&fb.overscan_top), 100 | out(&fb.overscan_bottom), 101 | out(&fb.overscan_left), 102 | out(&fb.overscan_right), 103 | last_tag_sentinel, 104 | }; 105 | callVideoCoreProperties(&arg); 106 | 107 | fb.words = @intToPtr([*]volatile u32, fb_addr & 0x3FFFFFFF); 108 | log("fb align {} addr {x} alpha {} pitch {} order {} size {} physical {}x{} virtual {}x{} offset {},{} overscan t {} b {} l {} r {}", .{ fb.alignment, @ptrToInt(fb.words), fb.alpha_mode, fb.pitch, fb.pixel_order, fb.size, fb.physical_width, fb.physical_height, fb.virtual_width, fb.virtual_height, fb.virtual_offset_x, fb.virtual_offset_y, fb.overscan_top, fb.overscan_bottom, fb.overscan_left, fb.overscan_right }); 109 | if (@ptrToInt(fb.words) == 0) { 110 | panic(@errorReturnTrace(), "frame buffer pointer is zero", .{}); 111 | } 112 | } 113 | }; 114 | 115 | pub const Bitmap = struct { 116 | frame_buffer: *FrameBuffer, 117 | pixel_array: [*]u8, 118 | width: u31, 119 | height: u31, 120 | 121 | fn getU32(base: [*]u8, offset: u32) u32 { 122 | var word: u32 = 0; 123 | var i: u32 = 0; 124 | while (i <= 3) : (i += 1) { 125 | word >>= 8; 126 | word |= @intCast(u32, @intToPtr(*u8, @ptrToInt(base) + offset + i).*) << 24; 127 | } 128 | return word; 129 | } 130 | 131 | pub fn init(bitmap: *Bitmap, frame_buffer: *FrameBuffer, file: []u8) void { 132 | bitmap.frame_buffer = frame_buffer; 133 | bitmap.pixel_array = @intToPtr([*]u8, @ptrToInt(file.ptr) + getU32(file.ptr, 0x0A)); 134 | bitmap.width = @intCast(u31, getU32(file.ptr, 0x12)); 135 | bitmap.height = @intCast(u31, getU32(file.ptr, 0x16)); 136 | } 137 | 138 | fn getPixel(self: *Bitmap, x: u32, y: u32) Color { 139 | const rgba = getU32(self.pixel_array, ((self.height - 1 - y) * self.width + x) * @sizeOf(u32)); 140 | return Color{ 141 | .red = @intCast(u8, (rgba >> 16) & 0xff), 142 | .green = @intCast(u8, (rgba >> 8) & 0xff), 143 | .blue = @intCast(u8, (rgba >> 0) & 0xff), 144 | .alpha = @intCast(u8, (rgba >> 24) & 0xff), 145 | }; 146 | } 147 | 148 | fn drawRect(self: *Bitmap, width: u32, height: u32, x1: u32, y1: u32, x2: u32, y2: u32) void { 149 | var y: u32 = 0; 150 | while (y < height) : (y += 1) { 151 | var x: u32 = 0; 152 | while (x < width) : (x += 1) { 153 | self.frame_buffer.drawPixel(x + x2, y + y2, self.getPixel(x + x1, y + y1)); 154 | } 155 | } 156 | } 157 | }; 158 | 159 | const TAG_ALLOCATE_FRAME_BUFFER = 0x40001; 160 | 161 | const TAG_GET_OVERSCAN = 0x4000A; 162 | const TAG_GET_PITCH = 0x40008; 163 | 164 | const TAG_SET_ALPHA_MODE = 0x48007; 165 | const TAG_SET_DEPTH = 0x48005; 166 | const TAG_SET_PHYSICAL_WIDTH_HEIGHT = 0x48003; 167 | const TAG_SET_PIXEL_ORDER = 0x48006; 168 | const TAG_SET_VIRTUAL_OFFSET = 0x48009; 169 | const TAG_SET_VIRTUAL_WIDTH_HEIGHT = 0x48004; 170 | 171 | pub const Color = struct { 172 | red: u8, 173 | green: u8, 174 | blue: u8, 175 | alpha: u8, 176 | 177 | fn to32(color: Color) u32 { 178 | return (255 - @intCast(u32, color.alpha) << 24) | @intCast(u32, color.red) << 16 | @intCast(u32, color.green) << 8 | @intCast(u32, color.blue) << 0; 179 | } 180 | }; 181 | 182 | const log = @import("serial.zig").log; 183 | const Metrics = @import("video_core_metrics.zig").Metrics; 184 | const panic = @import("debug.zig").panic; 185 | usingnamespace @import("video_core_properties.zig"); 186 | const std = @import("std"); 187 | const assert = std.debug.assert; 188 | -------------------------------------------------------------------------------- /src/video_core_mailboxes.zig: -------------------------------------------------------------------------------- 1 | pub const mailboxes = [_]*Mailbox{ Mailbox.init(0), Mailbox.init(1) }; 2 | 3 | const Mailbox = packed struct { 4 | push_pull_register: u32, 5 | unused1: u32, 6 | unused2: u32, 7 | unused3: u32, 8 | unused4: u32, 9 | unused5: u32, 10 | status_register: u32, 11 | unused6: u32, 12 | 13 | fn init(index: u32) *Mailbox { 14 | if (index > 1) { 15 | panic(@errorReturnTrace(), "mailbox index {} exceeds 1", .{index}); 16 | } 17 | const PERIPHERAL_BASE = 0x3F000000; 18 | const MAILBOXES_OFFSET = 0xB880; 19 | assert(@sizeOf(Mailbox) == 0x20); 20 | return @intToPtr(*Mailbox, PERIPHERAL_BASE + MAILBOXES_OFFSET + index * @sizeOf(Mailbox)); 21 | } 22 | 23 | fn pushRequestBlocking(this: *Mailbox, request: u32) void { 24 | blockWhile(this, isFull); 25 | this.push(request); 26 | } 27 | 28 | fn pullResponseBlocking(this: *Mailbox, request: u32) void { 29 | blockWhile(this, isEmpty); 30 | const response = this.pull(); 31 | if (response != request) { 32 | panic(@errorReturnTrace(), "buffer address and channel response was {x} expecting {x}", .{ response, request }); 33 | } 34 | } 35 | 36 | fn push(this: *Mailbox, word: u32) void { 37 | mmio.dsb(); 38 | mmio.write(@ptrToInt(&this.push_pull_register), word); 39 | } 40 | 41 | fn pull(this: *Mailbox) u32 { 42 | return mmio.read(@ptrToInt(&this.push_pull_register)); 43 | } 44 | 45 | fn status(this: *Mailbox) u32 { 46 | return mmio.read(@ptrToInt(&this.status_register)); 47 | } 48 | 49 | fn blockWhile(this: *Mailbox, conditionFn: fn (*Mailbox) bool) void { 50 | time.update(); 51 | const start = time.seconds; 52 | while (conditionFn(this)) { 53 | time.update(); 54 | if (time.seconds - start >= 0.1) { 55 | panic(@errorReturnTrace(), "time out waiting for video core mailbox", .{}); 56 | } 57 | } 58 | } 59 | }; 60 | 61 | fn isFull(this: *Mailbox) bool { 62 | const MAILBOX_IS_FULL = 0x80000000; 63 | return this.status() & MAILBOX_IS_FULL != 0; 64 | } 65 | 66 | fn isEmpty(this: *Mailbox) bool { 67 | const MAILBOX_IS_EMPTY = 0x40000000; 68 | return this.status() & MAILBOX_IS_EMPTY != 0; 69 | } 70 | 71 | const assert = std.debug.assert; 72 | const mmio = @import("mmio.zig"); 73 | const panic = @import("debug.zig").panic; 74 | const std = @import("std"); 75 | const time = @import("time.zig"); 76 | -------------------------------------------------------------------------------- /src/video_core_metrics.zig: -------------------------------------------------------------------------------- 1 | pub const Metrics = struct { 2 | board_model: u32, 3 | board_revision: u32, 4 | firmware_revision: u32, 5 | is_qemu: bool, 6 | max_temperature: u32, 7 | temperature: u32, 8 | temperature_id: u32, 9 | arm_memory_address: u32, 10 | arm_memory_size: u32, 11 | vc_memory_address: u32, 12 | vc_memory_size: u32, 13 | usable_dma_channels_mask: u32, 14 | 15 | pub fn init(self: *Metrics) void { 16 | self.temperature_id = 0; 17 | var args = [_]PropertiesArg{ 18 | tag(TAG_GET_FIRMWARE_REVISION, 4), 19 | out(&self.firmware_revision), 20 | tag(TAG_GET_BOARD_MODEL, 4), 21 | out(&self.board_model), 22 | tag(TAG_GET_BOARD_REVISION, 4), 23 | out(&self.board_revision), 24 | tag(TAG_GET_MAX_TEMPERATURE, 8), 25 | set(&self.temperature_id), 26 | out(&self.max_temperature), 27 | tag(TAG_GET_ARM_MEMORY, 8), 28 | out(&self.arm_memory_address), 29 | out(&self.arm_memory_size), 30 | tag(TAG_GET_VC_MEMORY, 8), 31 | out(&self.vc_memory_address), 32 | out(&self.vc_memory_size), 33 | tag(TAG_GET_USABLE_DMA_CHANNELS_MASK, 4), 34 | out(&self.usable_dma_channels_mask), 35 | last_tag_sentinel, 36 | }; 37 | callVideoCoreProperties(&args); 38 | self.is_qemu = self.board_model == 0xaaaaaaaa; 39 | self.update(); 40 | } 41 | 42 | pub fn update(m: *Metrics) void { 43 | var args = [_]PropertiesArg{ 44 | tag(TAG_GET_TEMPERATURE, 8), 45 | set(&m.temperature_id), 46 | out(&m.temperature), 47 | last_tag_sentinel, 48 | }; 49 | callVideoCoreProperties(&args); 50 | } 51 | }; 52 | 53 | const TAG_GET_ARM_MEMORY = 0x10005; 54 | const TAG_GET_BOARD_MODEL = 0x10001; 55 | const TAG_GET_BOARD_REVISION = 0x10002; 56 | const TAG_GET_FIRMWARE_REVISION = 0x00001; 57 | const TAG_GET_MAX_TEMPERATURE = 0x3000A; 58 | const TAG_GET_TEMPERATURE = 0x30006; 59 | const TAG_GET_USABLE_DMA_CHANNELS_MASK = 0x60001; 60 | const TAG_GET_VC_MEMORY = 0x10006; 61 | 62 | const panic = @import("debug.zig").panic; 63 | usingnamespace @import("video_core_properties.zig"); 64 | -------------------------------------------------------------------------------- /src/video_core_properties.zig: -------------------------------------------------------------------------------- 1 | pub fn callVideoCoreProperties(args: []PropertiesArg) void { 2 | assert(args[args.len - 1].TagAndLength.tag == TAG_LAST_SENTINEL); 3 | 4 | var words: [1024]u32 align(16) = undefined; 5 | var buf = SliceIterator.of(u32).init(&words); 6 | 7 | var buffer_length_in_bytes: u32 = 0; 8 | buf.add(buffer_length_in_bytes); 9 | const BUFFER_REQUEST = 0; 10 | buf.add(BUFFER_REQUEST); 11 | var next_tag_index = buf.index; 12 | for (args) |arg| { 13 | switch (arg) { 14 | PropertiesArg.TagAndLength => |tag_and_length| { 15 | if (tag_and_length.tag != 0) { 16 | // log("prepare tag {x} length {}", tag_and_length.tag, tag_and_length.length); 17 | } 18 | buf.index = next_tag_index; 19 | buf.add(tag_and_length.tag); 20 | if (tag_and_length.tag != TAG_LAST_SENTINEL) { 21 | buf.add(tag_and_length.length); 22 | const TAG_REQUEST = 0; 23 | buf.add(TAG_REQUEST); 24 | next_tag_index = buf.index + tag_and_length.length / 4; 25 | } 26 | }, 27 | PropertiesArg.Out => {}, 28 | PropertiesArg.In => |ptr| { 29 | buf.add(ptr.*); 30 | }, 31 | PropertiesArg.Set => |ptr| { 32 | buf.add(ptr.*); 33 | }, 34 | } 35 | } 36 | buffer_length_in_bytes = buf.index * 4; 37 | buf.reset(); 38 | buf.add(buffer_length_in_bytes); 39 | 40 | var buffer_pointer = @ptrToInt(buf.data.ptr); 41 | if (buffer_pointer & 0xF != 0) { 42 | panic(@errorReturnTrace(), "video core mailbox buffer not aligned to 16 bytes", .{}); 43 | } 44 | const PROPERTY_CHANNEL = 8; 45 | const request = PROPERTY_CHANNEL | @intCast(u32, buffer_pointer); 46 | mailboxes[1].pushRequestBlocking(request); 47 | // log("pull mailbox response"); 48 | mailboxes[0].pullResponseBlocking(request); 49 | 50 | buf.reset(); 51 | check(&buf, buffer_length_in_bytes); 52 | const BUFFER_RESPONSE_OK = 0x80000000; 53 | check(&buf, BUFFER_RESPONSE_OK); 54 | next_tag_index = buf.index; 55 | for (args) |arg| { 56 | switch (arg) { 57 | PropertiesArg.TagAndLength => |tag_and_length| { 58 | if (tag_and_length.tag != 0) { 59 | // log("parse tag {x} length {}", tag_and_length.tag, tag_and_length.length); 60 | } 61 | buf.index = next_tag_index; 62 | check(&buf, tag_and_length.tag); 63 | if (tag_and_length.tag != TAG_LAST_SENTINEL) { 64 | check(&buf, tag_and_length.length); 65 | const TAG_RESPONSE_OK = 0x80000000; 66 | check(&buf, TAG_RESPONSE_OK | tag_and_length.length); 67 | next_tag_index = buf.index + tag_and_length.length / 4; 68 | } 69 | }, 70 | PropertiesArg.Out => |ptr| { 71 | ptr.* = buf.next(); 72 | }, 73 | PropertiesArg.In => {}, 74 | PropertiesArg.Set => |ptr| { 75 | check(&buf, ptr.*); 76 | }, 77 | } 78 | } 79 | // log("properties done"); 80 | } 81 | 82 | pub fn out(ptr: *u32) PropertiesArg { 83 | return PropertiesArg{ .Out = ptr }; 84 | } 85 | 86 | pub fn in(ptr: *u32) PropertiesArg { 87 | return PropertiesArg{ .In = ptr }; 88 | } 89 | 90 | const TAG_LAST_SENTINEL = 0; 91 | pub const last_tag_sentinel = tag(TAG_LAST_SENTINEL, 0); 92 | 93 | pub fn set(ptr: *u32) PropertiesArg { 94 | return PropertiesArg{ .Set = ptr }; 95 | } 96 | 97 | pub fn tag(the_tag: u32, length: u32) PropertiesArg { 98 | return PropertiesArg{ .TagAndLength = TagAndLength{ .tag = the_tag, .length = length } }; 99 | } 100 | 101 | pub const PropertiesArg = union(enum) { 102 | In: *u32, 103 | Out: *u32, 104 | Set: *u32, 105 | TagAndLength: TagAndLength, 106 | }; 107 | 108 | const TagAndLength = struct { 109 | tag: u32, 110 | length: u32, 111 | }; 112 | 113 | fn check(buf: *SliceIterator.of(u32), word: u32) void { 114 | const was = buf.next(); 115 | if (was != word) { 116 | panic(@errorReturnTrace(), "video core mailbox failed index {} was {}/{x} expected {}/{x}", .{ buf.index - 1, was, was, word, word }); 117 | } 118 | } 119 | 120 | const log = @import("serial.zig").log; 121 | const mailboxes = @import("video_core_mailboxes.zig").mailboxes; 122 | const panic = @import("debug.zig").panic; 123 | const SliceIterator = @import("slice_iterator.zig"); 124 | const std = @import("std"); 125 | const assert = std.debug.assert; 126 | -------------------------------------------------------------------------------- /tools/send_image.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn main() !void { 4 | var direct_allocator = std.heap.DirectAllocator.init(); 5 | defer direct_allocator.deinit(); 6 | 7 | var arena = std.heap.ArenaAllocator.init(&direct_allocator.allocator); 8 | defer arena.deinit(); 9 | 10 | const allocator = &arena.allocator; 11 | const args = try std.os.argsAlloc(allocator); 12 | if (args.len != 2) { 13 | std.debug.panic("expected 2 args, found {} args", args.len); 14 | } 15 | const file_path = args[1]; 16 | const new_image_data = try std.io.readFileAlloc(allocator, "clashos.bin"); 17 | const tty_fd = try std.os.posixOpen(file_path, std.os.posix.O_WRONLY, 0); 18 | const tty_file = std.os.File.openHandle(tty_fd); 19 | const out = &tty_file.outStream().stream; 20 | try out.write([]u8{ 6, 6, 6 }); 21 | try out.writeIntLittle(u32, @intCast(u32, new_image_data.len)); 22 | try out.write(new_image_data); 23 | } 24 | --------------------------------------------------------------------------------