├── .gitignore ├── .gitmodules ├── README.md ├── gb_cardputer.ino ├── glue.h └── minigb_apu_cardputer ├── LICENSE ├── minigb_apu.c └── minigb_apu.h /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /build 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "peanutgb"] 2 | path = peanutgb 3 | url = https://github.com/deltabeard/Peanut-GB 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GB Cardputer 2 | Run GameBoy games on your M5Stack Cardputer!* 3 | 4 | Uses [Peanut-GB](https://github.com/deltabeard/Peanut-GB), a super cool project that's basically an entire GameBoy emulator in a single C header! 5 | 6 | # Warning 7 | This "port" is not polished at all, the code needs major commenting, refactoring and other general cleanup. 8 | 9 | Audio also doesn't work, since I just didn't feel like making it work for now lol, though I might addreess that later on 10 | 11 | I just cobbled this together between yesterday (17/1/2024) and today just to mess around with the Cardputer a bit. 12 | -------------------------------------------------------------------------------- /gb_cardputer.ino: -------------------------------------------------------------------------------- 1 | // Sound isn't done yet 2 | // (or might never be) 3 | #define ENABLE_SOUND 0 4 | #define ENABLE_LCD 1 5 | 6 | #define MAX_FILES 256 7 | 8 | #include "M5Cardputer.h" 9 | #include "peanutgb/peanut_gb.h" 10 | #include "SD.h" 11 | 12 | #define DEST_W 240 13 | #define DEST_H 135 14 | 15 | #define DEBUG_DELAY 0 16 | 17 | #define DISPLAY_CENTER(x) x + (DEST_W/2 - LCD_WIDTH/2) 18 | 19 | // SD card SPI class. 20 | SPIClass SPI2; 21 | 22 | // Second framebuffer to check for changed pixels. 23 | uint32_t swap_fb[LCD_HEIGHT][LCD_WIDTH]; 24 | 25 | // Prints debug info to the display. 26 | void debugPrint(const char* str) { 27 | M5Cardputer.Display.clearDisplay(); 28 | M5Cardputer.Display.drawString(str, 0, 0); 29 | #if DEBUG_DELAY 30 | delay(500); 31 | #endif 32 | } 33 | 34 | // Penaut-GB structures and functions. 35 | struct priv_t 36 | { 37 | /* Pointer to allocated memory holding GB file. */ 38 | uint8_t *rom; 39 | /* Pointer to allocated memory holding save file. */ 40 | uint8_t *cart_ram; 41 | 42 | /* Frame buffer */ 43 | uint32_t fb[LCD_HEIGHT][LCD_WIDTH]; 44 | }; 45 | 46 | /** 47 | * Returns a byte from the ROM file at the given address. 48 | */ 49 | uint8_t gb_rom_read(struct gb_s *gb, const uint_fast32_t addr) 50 | { 51 | const struct priv_t * const p = (const struct priv_t *)gb->direct.priv; 52 | return p->rom[addr]; 53 | } 54 | 55 | /** 56 | * Returns a byte from the cartridge RAM at the given address. 57 | */ 58 | uint8_t gb_cart_ram_read(struct gb_s *gb, const uint_fast32_t addr) 59 | { 60 | const struct priv_t * const p = (const struct priv_t *)gb->direct.priv; 61 | return p->cart_ram[addr]; 62 | } 63 | 64 | /** 65 | * Writes a given byte to the cartridge RAM at the given address. 66 | */ 67 | void gb_cart_ram_write(struct gb_s *gb, const uint_fast32_t addr, 68 | const uint8_t val) 69 | { 70 | const struct priv_t * const p = (const struct priv_t *)gb->direct.priv; 71 | p->cart_ram[addr] = val; 72 | } 73 | 74 | /** 75 | * Returns a pointer to the allocated space containing the ROM. Must be freed. 76 | */ 77 | uint8_t *read_rom_to_ram(const char *file_name) { 78 | // Open file from the SD card. 79 | File rom_file = SD.open(file_name); 80 | size_t rom_size; 81 | char *readRom = NULL; 82 | 83 | rom_size = rom_file.size(); 84 | 85 | if(rom_size == NULL) 86 | return NULL; 87 | 88 | readRom = (char*)malloc(rom_size); 89 | 90 | if(rom_file.readBytes(readRom, rom_size) != rom_size) { 91 | free(readRom); 92 | rom_file.close(); 93 | return NULL; 94 | } 95 | 96 | uint8_t *rom = (uint8_t*)readRom; 97 | char debugRomStr[100]; 98 | // Print first and last byte of the ROM for debugging purposes. 99 | sprintf(debugRomStr, "f: %02x | l: %02x", rom[0], rom[rom_size-1]); 100 | debugPrint(debugRomStr); 101 | rom_file.close(); 102 | return rom; 103 | } 104 | 105 | /** 106 | * Ignore all errors. 107 | */ 108 | void gb_error(struct gb_s *gb, const enum gb_error_e gb_err, const uint16_t val) { 109 | const char* gb_err_str[GB_INVALID_MAX] = { 110 | "UNKNOWN", 111 | "INVALID OPCODE", 112 | "INVALID READ", 113 | "INVALID WRITE", 114 | "HATL FOREVER" 115 | }; 116 | 117 | struct priv_t * priv = (struct priv_t *)gb->direct.priv; 118 | 119 | free(priv->cart_ram); 120 | free(priv->rom); 121 | } 122 | 123 | #if ENABLE_LCD 124 | /** 125 | * Draws scanline into framebuffer. 126 | */ 127 | void lcd_draw_line(struct gb_s *gb, const uint8_t pixels[160], 128 | const uint_fast8_t line) 129 | { 130 | struct priv_t *priv = (priv_t*)gb->direct.priv; 131 | const uint32_t palette[] = { 0xFFFFFF, 0xA5A5A5, 0x525252, 0x000000 }; 132 | 133 | for(unsigned int x = 0; x < LCD_WIDTH; x++) 134 | priv->fb[line][x] = palette[pixels[x] & 3]; 135 | } 136 | 137 | // Draw a frame to the display while scaling it to fit. 138 | // This is needed as the Cardputer's display has a height of 135px, 139 | // while the GameBoy's has a height of 144px. 140 | void fit_frame(uint32_t fb[144][160]) { 141 | //M5Cardputer.Display.clearDisplay(); 142 | for(unsigned int i = 0; i < LCD_WIDTH; i++) { 143 | for(unsigned int j = 0; j < LCD_HEIGHT; j++) { 144 | if(fb[j * LCD_HEIGHT / DEST_H][i] != swap_fb[j][i]) { 145 | M5Cardputer.Display.drawPixel((int32_t)DISPLAY_CENTER(i), (int32_t)j, fb[j * LCD_HEIGHT / DEST_H][i]); 146 | } 147 | swap_fb[j][i] = fb[j * LCD_HEIGHT / DEST_H][i]; 148 | } 149 | } 150 | } 151 | 152 | // Draw a frame to the display without scaling. 153 | // Not normally called. Edit the code to use this function 154 | void draw_frame(uint32_t fb[144][160]) { 155 | for(unsigned int i = 0; i < LCD_WIDTH; i++) { 156 | for(unsigned int j = 0; j < LCD_HEIGHT; j++) { 157 | if(fb[j][i] != swap_fb[j][i]) { 158 | M5Cardputer.Display.drawPixel((int32_t)i, (int32_t)j, fb[j][i]); 159 | } 160 | swap_fb[j][i] = fb[j][i]; 161 | } 162 | } 163 | } 164 | 165 | #endif 166 | 167 | // Shorten ROM display names if they're too long. 168 | // Memory alloc with C strings is hard so this goes unused for now 169 | //char* clamp_str(char* input) { 170 | // if(strlen(input) > 10) { 171 | // char* output = (char*)malloc(sizeof(char)*4+sizeof(char)*strlen(input)); 172 | // for (int i = 0; i < 9; i++) { 173 | // output[i] = input[i]; 174 | // } 175 | // sprintf(output, "%s...", input); 176 | // return output; 177 | // } else { 178 | // return input; 179 | // } 180 | //} 181 | 182 | void set_font_size(int size) { 183 | int textsize = M5Cardputer.Display.height() / size; 184 | if(textsize == 0) { 185 | textsize = 1; 186 | } 187 | M5Cardputer.Display.setTextSize(textsize); 188 | } 189 | 190 | // Opens a ROM file picker menu 191 | // and returns a string containing 192 | // the path of the picked ROM. 193 | char* file_picker() { 194 | // Open SD card root dir. 195 | File root_dir = SD.open("/"); 196 | String file_list[MAX_FILES]; 197 | int file_list_size = 0; 198 | 199 | // Look for .gb files in the root dir. 200 | while(1) { 201 | File file_entry = root_dir.openNextFile(); 202 | // If we checked all files, stop searching 203 | if(!file_entry) { 204 | break; 205 | } 206 | 207 | // Chec if the entry is a file 208 | if(!file_entry.isDirectory()) { 209 | // Get the file extension 210 | String file_name = file_entry.name(); 211 | String file_extension = file_name.substring(file_name.lastIndexOf(".") + 1); 212 | 213 | // Convert the file extension to lowercase 214 | // 215 | // WARNING: major yapping ahead 216 | // 217 | // Note: SD cards might have to be formatted as FAT32 218 | // to work with this library; if that's the case then 219 | // doing this doesn't make a difference because FAT32 220 | // is case insensitive. However, the author of 221 | // https://github.com/shikarunochi/CardputerSimpleLaucher 222 | // (which much of the code in this function is edited from) 223 | // added this line so I guess I can't be too sure. 224 | // It's not like saving CPU cycles is important here 225 | // Since this is only called when the menu is first shown so 226 | // 227 | // (not like any of this code is particularly efficient) 228 | // 229 | // yapping sesh over 230 | file_extension.toLowerCase(); 231 | 232 | if(!file_extension.equals("gb")) { 233 | continue; 234 | } 235 | 236 | // Add the ROM's filename to the array 237 | file_list[file_list_size] = file_name; 238 | file_list_size++; 239 | } 240 | 241 | file_entry.close(); 242 | } 243 | 244 | root_dir.close(); 245 | 246 | // Boolean to check if a file has been picked. 247 | // If so we should start the game (ofc lol) 248 | bool file_picked = false; 249 | int select_index = 0; 250 | 251 | M5Cardputer.Display.clearDisplay(); 252 | 253 | // This might be kinda stupid but 254 | // File.name() returns an Arduino-style 255 | // String object, when Peanut-GB being 256 | // written in C expects a plain old 257 | // char array in its `read_rom_to_ram` 258 | // callback, so we'll need to "convert" these strings 259 | char* file_list_cstr[MAX_FILES]; 260 | for(int i = 0; i < file_list_size; i++) { 261 | file_list_cstr[i] = (char*)malloc(sizeof(char)*MAX_FILES); 262 | file_list[i].toCharArray(file_list_cstr[i], MAX_FILES); 263 | } 264 | 265 | // Menu loop 266 | while(!file_picked) { 267 | // Read Keyboard matrix 268 | M5Cardputer.update(); 269 | if(M5Cardputer.Keyboard.isPressed()) { 270 | Keyboard_Class::KeysState status = M5Cardputer.Keyboard.keysState(); 271 | M5Cardputer.Display.clearDisplay(); 272 | for(auto i : status.word) { 273 | // Controls: 274 | // Up: e 275 | // Down: s 276 | // Play: l 277 | switch(i) { 278 | case ';': 279 | select_index--; 280 | delay(300); 281 | break; 282 | case '.': 283 | select_index++; 284 | delay(300); 285 | break; 286 | default: 287 | break; 288 | } 289 | } 290 | 291 | if(status.enter) { 292 | file_picked = true; 293 | } 294 | } 295 | 296 | // Loop over list if we went over its bounds 297 | // e.g. user presses up on the first element 298 | // or down on the last one 299 | if(select_index < 0) { 300 | select_index = file_list_size-1; 301 | } else if(select_index > file_list_size-1) { 302 | select_index = 0; 303 | } 304 | 305 | // Render controls 306 | // M5Cardputer.Display.drawString("Up/Down: E/S; Select: L", 0, 0); 307 | 308 | //M5Cardputer.Display.setTextDatum(MC_DATUM); 309 | 310 | const int dispW = M5Cardputer.Display.width(); 311 | const int dispH = M5Cardputer.Display.height(); 312 | 313 | // Render list 314 | for(int i = 0; i < file_list_size; i++) { 315 | // Add an arrow to point to 316 | // the currently selected file 317 | 318 | if(select_index == i) { 319 | set_font_size(64); 320 | int textW = M5Cardputer.Display.textWidth(file_list_cstr[i]); 321 | int textH = M5Cardputer.Display.fontHeight(); 322 | 323 | M5Cardputer.Display.drawString(" > ", 0, (dispH/2)-(textH/2)); 324 | M5Cardputer.Display.drawString(file_list_cstr[i], (dispW/2)-(textW/2), (dispH/2)-(textH/2)); 325 | } else if(i == select_index-1) { 326 | set_font_size(128); 327 | int textW = M5Cardputer.Display.textWidth(file_list_cstr[i]); 328 | int textH = M5Cardputer.Display.fontHeight(); 329 | 330 | M5Cardputer.Display.drawString(file_list_cstr[i], (dispW/2)-(textW/2), (dispH/2)-(textH/2)-textH*2); 331 | } else if(i == select_index+1) { 332 | set_font_size(128); 333 | int textW = M5Cardputer.Display.textWidth(file_list_cstr[i]); 334 | int textH = M5Cardputer.Display.fontHeight(); 335 | 336 | M5Cardputer.Display.drawString(file_list_cstr[i], (dispW/2)-(textW/2), (dispH/2)-(textH/2)+textH*2); 337 | } 338 | } 339 | } 340 | 341 | // Return '/' + selected file path 342 | char* selected_path = (char*)malloc(sizeof(char)*MAX_FILES+sizeof(char)); 343 | sprintf(selected_path, "/%s", file_list_cstr[select_index]); 344 | return selected_path; 345 | } 346 | 347 | #if ENABLE_SOUND 348 | void audioSetup() { 349 | // headache. stopped here lol 350 | } 351 | #endif 352 | 353 | void setup() { 354 | // put your setup code here, to run once: 355 | 356 | // Init M5Stack and M5Cardputer libs. 357 | auto cfg = M5.config(); 358 | // Use keyboard. 359 | M5Cardputer.begin(cfg, true); 360 | 361 | #if ENABLE_SOUND 362 | M5Cardputer.Speaker.begin(); 363 | #endif 364 | 365 | // Set display rotation to horizontal. 366 | M5Cardputer.Display.setRotation(1); 367 | set_font_size(64); 368 | 369 | // Initialize SD card. 370 | // Some of this code is taken from 371 | // https://github.com/shikarunochi/CardputerSimpleLaucher 372 | debugPrint("Waiting for SD Card to Init..."); 373 | SPI2.begin( 374 | M5.getPin(m5::pin_name_t::sd_spi_sclk), 375 | M5.getPin(m5::pin_name_t::sd_spi_miso), 376 | M5.getPin(m5::pin_name_t::sd_spi_mosi), 377 | M5.getPin(m5::pin_name_t::sd_spi_ss)); 378 | while (false == SD.begin(M5.getPin(m5::pin_name_t::sd_spi_ss), SPI2)) { 379 | delay(1); 380 | } 381 | 382 | // Initialize GameBoy emulation context. 383 | static struct gb_s gb; 384 | static struct priv_t priv; 385 | enum gb_init_error_e ret; 386 | debugPrint("postInit"); 387 | 388 | 389 | debugPrint("Before filepick"); 390 | char* selected_file = file_picker(); 391 | debugPrint(selected_file); 392 | debugPrint("After filepick"); 393 | 394 | // Check for errors in reading the ROM file. 395 | if((priv.rom = read_rom_to_ram(selected_file)) == NULL) { 396 | // error reporting 397 | debugPrint("Error at read_rom_to_ram!!"); 398 | } 399 | 400 | ret = gb_init(&gb, &gb_rom_read, &gb_cart_ram_read, &gb_cart_ram_write, &gb_error, &priv); 401 | 402 | if(ret != GB_INIT_NO_ERROR) { 403 | // error reporting 404 | debugPrint("GB_INIT error!!!"); 405 | } 406 | 407 | priv.cart_ram = (uint8_t*)malloc(gb_get_save_size(&gb)); 408 | 409 | #if ENABLE_LCD 410 | gb_init_lcd(&gb, &lcd_draw_line); 411 | // Disable interlacing (this is default behaviour but ¯\_(ツ)_/¯) 412 | gb.direct.interlace = 0; 413 | #endif 414 | 415 | debugPrint("Before loop"); 416 | 417 | // Clear the display of any printed text before starting emulation. 418 | M5Cardputer.Display.clearDisplay(); 419 | 420 | // Target game speed. 421 | const double target_speed_us = 1000000.0 / VERTICAL_SYNC; 422 | while(1) { 423 | // Variables needed to get steady frametimes. 424 | int_fast16_t delay; 425 | unsigned long start, end; 426 | struct timeval timecheck; 427 | 428 | // Get current timer value 429 | gettimeofday(&timecheck, NULL); 430 | start = (long)timecheck.tv_sec * 1000000 + 431 | (long)timecheck.tv_usec; 432 | 433 | // Reset Joypad 434 | // This works because button status 435 | // is stored as a single 8-bit value, 436 | // with 1 being the non-pressed state. 437 | // This sets all bits to 1 438 | gb.direct.joypad = 0xff; 439 | 440 | // Read Keyboard matrix 441 | M5Cardputer.update(); 442 | if(M5Cardputer.Keyboard.isPressed()) { 443 | Keyboard_Class::KeysState status = M5Cardputer.Keyboard.keysState(); 444 | // The Cardputer can detect up to 3 key updates at one time 445 | for(auto i : status.word) { 446 | // Controls: 447 | // e 448 | // |=| [A] 449 | // a |=====| d [B] l 450 | // |=| // // k 451 | // s 2 1 452 | // 453 | // Might implement a config file to set these. 454 | switch(i) { 455 | case 'e': 456 | gb.direct.joypad_bits.up = 0; 457 | break; 458 | case 'a': 459 | gb.direct.joypad_bits.left = 0; 460 | break; 461 | case 's': 462 | gb.direct.joypad_bits.down = 0; 463 | break; 464 | case 'd': 465 | gb.direct.joypad_bits.right = 0; 466 | break; 467 | case 'k': 468 | gb.direct.joypad_bits.b = 0; 469 | break; 470 | case 'l': 471 | gb.direct.joypad_bits.a = 0; 472 | break; 473 | case '1': 474 | gb.direct.joypad_bits.start = 0; 475 | break; 476 | case '2': 477 | gb.direct.joypad_bits.select = 0; 478 | break; 479 | default: 480 | break; 481 | } 482 | } 483 | } 484 | 485 | /* Execute CPU cycles until the screen has to be redrawn. */ 486 | gb_run_frame(&gb); 487 | 488 | // Draw the current frame to the screen. 489 | fit_frame(priv.fb); 490 | 491 | // Get the time after running the CPU and rendering a frame. 492 | gettimeofday(&timecheck, NULL); 493 | end = (long)timecheck.tv_sec * 1000000 + 494 | (long)timecheck.tv_usec; 495 | 496 | // Subtract time taken to render a frame to the target speed. 497 | delay = target_speed_us - (end - start); 498 | 499 | /* If it took more than the maximum allowed time to draw frame, 500 | * do not delay. 501 | * Interlaced mode could be enabled here to help speed up 502 | * drawing. 503 | */ 504 | if(delay < 0) 505 | continue; 506 | 507 | usleep(delay); 508 | } 509 | } 510 | 511 | // Unused as I'm using an infinite while-loop 512 | // inside the main function because otherwise 513 | // I'd need to deal with global variables 514 | // which are stupid (doing that gave me an 515 | // ambiguous compiler error so I no no wanna) 516 | void loop() { 517 | 518 | } 519 | -------------------------------------------------------------------------------- /glue.h: -------------------------------------------------------------------------------- 1 | // glue minigb_apu and gb_cardputer together 2 | // very rough 3 | // matthew5pl.net 4 | 5 | typedef unsigned int uint32_t; 6 | typedef unsigned short uint16_t; 7 | typedef unsigned char uint8_t; 8 | 9 | typedef int int32_t; 10 | typedef short int16_t; 11 | typedef char int8_t; 12 | 13 | // who cares about efficiency 14 | #define bool char 15 | #define true 1 16 | #define false 0 -------------------------------------------------------------------------------- /minigb_apu_cardputer/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Alex Baines 2 | Copyright (c) 2019 Mahyar Koshkouei 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /minigb_apu_cardputer/minigb_apu.c: -------------------------------------------------------------------------------- 1 | /** 2 | * minigb_apu is released under the terms listed within the LICENSE file. 3 | * 4 | * minigb_apu emulates the audio processing unit (APU) of the Game Boy. This 5 | * project is based on MiniGBS by Alex Baines: https://github.com/baines/MiniGBS 6 | */ 7 | 8 | #include "../glue.h" 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include "minigb_apu.h" 15 | 16 | #define DMG_CLOCK_FREQ_U ((unsigned)DMG_CLOCK_FREQ) 17 | #define AUDIO_NSAMPLES (AUDIO_SAMPLES * 2u) 18 | 19 | #define AUDIO_MEM_SIZE (0xFF3F - 0xFF10 + 1) 20 | #define AUDIO_ADDR_COMPENSATION 0xFF10 21 | 22 | #define MAX(a, b) ( a > b ? a : b ) 23 | #define MIN(a, b) ( a <= b ? a : b ) 24 | 25 | #define VOL_INIT_MAX (INT16_MAX/8) 26 | #define VOL_INIT_MIN (INT16_MIN/8) 27 | 28 | /* Handles time keeping for sound generation. 29 | * FREQ_INC_REF must be equal to, or larger than AUDIO_SAMPLE_RATE in order 30 | * to avoid a division by zero error. 31 | * Using a square of 2 simplifies calculations. */ 32 | #define FREQ_INC_REF (AUDIO_SAMPLE_RATE * 16) 33 | 34 | #define MAX_CHAN_VOLUME 15 35 | 36 | /** 37 | * Memory holding audio registers between 0xFF10 and 0xFF3F inclusive. 38 | */ 39 | static uint8_t audio_mem[AUDIO_MEM_SIZE]; 40 | 41 | struct chan_len_ctr { 42 | uint8_t load; 43 | unsigned enabled : 1; 44 | uint32_t counter; 45 | uint32_t inc; 46 | }; 47 | 48 | struct chan_vol_env { 49 | uint8_t step; 50 | unsigned up : 1; 51 | uint32_t counter; 52 | uint32_t inc; 53 | }; 54 | 55 | struct chan_freq_sweep { 56 | uint16_t freq; 57 | uint8_t rate; 58 | uint8_t shift; 59 | unsigned up : 1; 60 | uint32_t counter; 61 | uint32_t inc; 62 | }; 63 | 64 | static struct chan { 65 | unsigned enabled : 1; 66 | unsigned powered : 1; 67 | unsigned on_left : 1; 68 | unsigned on_right : 1; 69 | unsigned muted : 1; 70 | 71 | uint8_t volume; 72 | uint8_t volume_init; 73 | 74 | uint16_t freq; 75 | uint32_t freq_counter; 76 | uint32_t freq_inc; 77 | 78 | int_fast16_t val; 79 | 80 | struct chan_len_ctr len; 81 | struct chan_vol_env env; 82 | struct chan_freq_sweep sweep; 83 | 84 | union { 85 | struct { 86 | uint8_t duty; 87 | uint8_t duty_counter; 88 | } square; 89 | struct { 90 | uint16_t lfsr_reg; 91 | uint8_t lfsr_wide; 92 | uint8_t lfsr_div; 93 | } noise; 94 | struct { 95 | uint8_t sample; 96 | } wave; 97 | }; 98 | } chans[4]; 99 | 100 | static int32_t vol_l, vol_r; 101 | 102 | static void set_note_freq(struct chan *c, const uint32_t freq) 103 | { 104 | /* Lowest expected value of freq is 64. */ 105 | c->freq_inc = freq * (uint32_t)(FREQ_INC_REF / AUDIO_SAMPLE_RATE); 106 | } 107 | 108 | static void chan_enable(const uint_fast8_t i, const bool enable) 109 | { 110 | uint8_t val; 111 | 112 | chans[i].enabled = enable; 113 | val = (audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] & 0x80) | 114 | (chans[3].enabled << 3) | (chans[2].enabled << 2) | 115 | (chans[1].enabled << 1) | (chans[0].enabled << 0); 116 | 117 | audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] = val; 118 | //audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] |= 0x80 | ((uint8_t)enable) << i; 119 | } 120 | 121 | static void update_env(struct chan *c) 122 | { 123 | c->env.counter += c->env.inc; 124 | 125 | while (c->env.counter > FREQ_INC_REF) { 126 | if (c->env.step) { 127 | c->volume += c->env.up ? 1 : -1; 128 | if (c->volume == 0 || c->volume == MAX_CHAN_VOLUME) { 129 | c->env.inc = 0; 130 | } 131 | c->volume = MAX(0, MIN(MAX_CHAN_VOLUME, c->volume)); 132 | } 133 | c->env.counter -= FREQ_INC_REF; 134 | } 135 | } 136 | 137 | static void update_len(struct chan *c) 138 | { 139 | if (!c->len.enabled) 140 | return; 141 | 142 | c->len.counter += c->len.inc; 143 | if (c->len.counter > FREQ_INC_REF) { 144 | chan_enable(c - chans, 0); 145 | c->len.counter = 0; 146 | } 147 | } 148 | 149 | static bool update_freq(struct chan *c, uint32_t *pos) 150 | { 151 | uint32_t inc = c->freq_inc - *pos; 152 | c->freq_counter += inc; 153 | 154 | if (c->freq_counter > FREQ_INC_REF) { 155 | *pos = c->freq_inc - (c->freq_counter - FREQ_INC_REF); 156 | c->freq_counter = 0; 157 | return true; 158 | } else { 159 | *pos = c->freq_inc; 160 | return false; 161 | } 162 | } 163 | 164 | static void update_sweep(struct chan *c) 165 | { 166 | c->sweep.counter += c->sweep.inc; 167 | 168 | while (c->sweep.counter > FREQ_INC_REF) { 169 | if (c->sweep.shift) { 170 | uint16_t inc = (c->sweep.freq >> c->sweep.shift); 171 | if (!c->sweep.up) 172 | inc *= -1; 173 | 174 | c->freq += inc; 175 | if (c->freq > 2047) { 176 | c->enabled = 0; 177 | } else { 178 | set_note_freq(c, 179 | DMG_CLOCK_FREQ_U / ((2048 - c->freq)<< 5)); 180 | c->freq_inc *= 8; 181 | } 182 | } else if (c->sweep.rate) { 183 | c->enabled = 0; 184 | } 185 | c->sweep.counter -= FREQ_INC_REF; 186 | } 187 | } 188 | 189 | static void update_square(int16_t* samples, const bool ch2) 190 | { 191 | uint32_t freq; 192 | struct chan* c = chans + ch2; 193 | 194 | if (!c->powered || !c->enabled) 195 | return; 196 | 197 | freq = DMG_CLOCK_FREQ_U / ((2048 - c->freq) << 5); 198 | set_note_freq(c, freq); 199 | c->freq_inc *= 8; 200 | 201 | for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) { 202 | update_len(c); 203 | 204 | if (!c->enabled) 205 | continue; 206 | 207 | update_env(c); 208 | if (!ch2) 209 | update_sweep(c); 210 | 211 | uint32_t pos = 0; 212 | uint32_t prev_pos = 0; 213 | int32_t sample = 0; 214 | 215 | while (update_freq(c, &pos)) { 216 | c->square.duty_counter = (c->square.duty_counter + 1) & 7; 217 | sample += ((pos - prev_pos) / c->freq_inc) * c->val; 218 | c->val = (c->square.duty & (1 << c->square.duty_counter)) ? 219 | VOL_INIT_MAX / MAX_CHAN_VOLUME : 220 | VOL_INIT_MIN / MAX_CHAN_VOLUME; 221 | prev_pos = pos; 222 | } 223 | 224 | if (c->muted) 225 | continue; 226 | 227 | sample += c->val; 228 | sample *= c->volume; 229 | sample /= 4; 230 | 231 | samples[i + 0] += sample * c->on_left * vol_l; 232 | samples[i + 1] += sample * c->on_right * vol_r; 233 | } 234 | } 235 | 236 | static uint8_t wave_sample(const unsigned int pos, const unsigned int volume) 237 | { 238 | uint8_t sample; 239 | 240 | sample = audio_mem[(0xFF30 + pos / 2) - AUDIO_ADDR_COMPENSATION]; 241 | if (pos & 1) { 242 | sample &= 0xF; 243 | } else { 244 | sample >>= 4; 245 | } 246 | return volume ? (sample >> (volume - 1)) : 0; 247 | } 248 | 249 | static void update_wave(int16_t *samples) 250 | { 251 | uint32_t freq; 252 | struct chan *c = chans + 2; 253 | 254 | if (!c->powered || !c->enabled) 255 | return; 256 | 257 | freq = (DMG_CLOCK_FREQ_U / 64) / (2048 - c->freq); 258 | set_note_freq(c, freq); 259 | 260 | c->freq_inc *= 32; 261 | 262 | for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) { 263 | update_len(c); 264 | 265 | if (!c->enabled) 266 | continue; 267 | 268 | uint32_t pos = 0; 269 | uint32_t prev_pos = 0; 270 | int32_t sample = 0; 271 | 272 | c->wave.sample = wave_sample(c->val, c->volume); 273 | 274 | while (update_freq(c, &pos)) { 275 | c->val = (c->val + 1) & 31; 276 | sample += ((pos - prev_pos) / c->freq_inc) * 277 | ((int)c->wave.sample - 8) * (INT16_MAX/64); 278 | c->wave.sample = wave_sample(c->val, c->volume); 279 | prev_pos = pos; 280 | } 281 | 282 | sample += ((int)c->wave.sample - 8) * (int)(INT16_MAX/64); 283 | 284 | if (c->volume == 0) 285 | continue; 286 | 287 | { 288 | /* First element is unused. */ 289 | int16_t div[] = { INT16_MAX, 1, 2, 4 }; 290 | sample = sample / (div[c->volume]); 291 | } 292 | 293 | if (c->muted) 294 | continue; 295 | 296 | sample /= 4; 297 | 298 | samples[i + 0] += sample * c->on_left * vol_l; 299 | samples[i + 1] += sample * c->on_right * vol_r; 300 | } 301 | } 302 | 303 | static void update_noise(int16_t *samples) 304 | { 305 | struct chan *c = chans + 3; 306 | 307 | if (!c->powered) 308 | return; 309 | 310 | { 311 | const uint32_t lfsr_div_lut[] = { 312 | 8, 16, 32, 48, 64, 80, 96, 112 313 | }; 314 | uint32_t freq; 315 | 316 | freq = DMG_CLOCK_FREQ_U / (lfsr_div_lut[c->noise.lfsr_div] << c->freq); 317 | set_note_freq(c, freq); 318 | } 319 | 320 | if (c->freq >= 14) 321 | c->enabled = 0; 322 | 323 | for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) { 324 | update_len(c); 325 | 326 | if (!c->enabled) 327 | continue; 328 | 329 | update_env(c); 330 | 331 | uint32_t pos = 0; 332 | uint32_t prev_pos = 0; 333 | int32_t sample = 0; 334 | 335 | while (update_freq(c, &pos)) { 336 | c->noise.lfsr_reg = (c->noise.lfsr_reg << 1) | 337 | (c->val >= VOL_INIT_MAX/MAX_CHAN_VOLUME); 338 | 339 | if (c->noise.lfsr_wide) { 340 | c->val = !(((c->noise.lfsr_reg >> 14) & 1) ^ 341 | ((c->noise.lfsr_reg >> 13) & 1)) ? 342 | VOL_INIT_MAX / MAX_CHAN_VOLUME : 343 | VOL_INIT_MIN / MAX_CHAN_VOLUME; 344 | } else { 345 | c->val = !(((c->noise.lfsr_reg >> 6) & 1) ^ 346 | ((c->noise.lfsr_reg >> 5) & 1)) ? 347 | VOL_INIT_MAX / MAX_CHAN_VOLUME : 348 | VOL_INIT_MIN / MAX_CHAN_VOLUME; 349 | } 350 | 351 | sample += ((pos - prev_pos) / c->freq_inc) * c->val; 352 | prev_pos = pos; 353 | } 354 | 355 | if (c->muted) 356 | continue; 357 | 358 | sample += c->val; 359 | sample *= c->volume; 360 | sample /= 4; 361 | 362 | samples[i + 0] += sample * c->on_left * vol_l; 363 | samples[i + 1] += sample * c->on_right * vol_r; 364 | } 365 | } 366 | 367 | /** 368 | * SDL2 style audio callback function. 369 | */ 370 | void audio_callback(void *userdata, uint8_t *stream, int len) 371 | { 372 | int16_t *samples = (int16_t *)stream; 373 | 374 | /* Appease unused variable warning. */ 375 | (void)userdata; 376 | 377 | memset(stream, 0, len); 378 | 379 | update_square(samples, 0); 380 | update_square(samples, 1); 381 | update_wave(samples); 382 | update_noise(samples); 383 | } 384 | 385 | static void chan_trigger(uint_fast8_t i) 386 | { 387 | struct chan *c = chans + i; 388 | 389 | chan_enable(i, 1); 390 | c->volume = c->volume_init; 391 | 392 | // volume envelope 393 | { 394 | uint8_t val = 395 | audio_mem[(0xFF12 + (i * 5)) - AUDIO_ADDR_COMPENSATION]; 396 | 397 | c->env.step = val & 0x07; 398 | c->env.up = val & 0x08 ? 1 : 0; 399 | c->env.inc = c->env.step ? 400 | (FREQ_INC_REF * 64ul) / ((uint32_t)c->env.step * AUDIO_SAMPLE_RATE) : 401 | (8ul * FREQ_INC_REF) / AUDIO_SAMPLE_RATE ; 402 | c->env.counter = 0; 403 | } 404 | 405 | // freq sweep 406 | if (i == 0) { 407 | uint8_t val = audio_mem[0xFF10 - AUDIO_ADDR_COMPENSATION]; 408 | 409 | c->sweep.freq = c->freq; 410 | c->sweep.rate = (val >> 4) & 0x07; 411 | c->sweep.up = !(val & 0x08); 412 | c->sweep.shift = (val & 0x07); 413 | c->sweep.inc = c->sweep.rate ? 414 | ((128 * FREQ_INC_REF) / (c->sweep.rate * AUDIO_SAMPLE_RATE)) : 0; 415 | c->sweep.counter = FREQ_INC_REF; 416 | } 417 | 418 | int len_max = 64; 419 | 420 | if (i == 2) { // wave 421 | len_max = 256; 422 | c->val = 0; 423 | } else if (i == 3) { // noise 424 | c->noise.lfsr_reg = 0xFFFF; 425 | c->val = VOL_INIT_MIN / MAX_CHAN_VOLUME; 426 | } 427 | 428 | c->len.inc = (256 * FREQ_INC_REF) / (AUDIO_SAMPLE_RATE * (len_max - c->len.load)); 429 | c->len.counter = 0; 430 | } 431 | 432 | /** 433 | * Read audio register. 434 | * \param addr Address of audio register. Must be 0xFF10 <= addr <= 0xFF3F. 435 | * This is not checked in this function. 436 | * \return Byte at address. 437 | */ 438 | uint8_t audio_read(const uint16_t addr) 439 | { 440 | static const uint8_t ortab[] = { 441 | 0x80, 0x3f, 0x00, 0xff, 0xbf, 442 | 0xff, 0x3f, 0x00, 0xff, 0xbf, 443 | 0x7f, 0xff, 0x9f, 0xff, 0xbf, 444 | 0xff, 0xff, 0x00, 0x00, 0xbf, 445 | 0x00, 0x00, 0x70, 446 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 447 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 448 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 449 | }; 450 | 451 | return audio_mem[addr - AUDIO_ADDR_COMPENSATION] | 452 | ortab[addr - AUDIO_ADDR_COMPENSATION]; 453 | } 454 | 455 | /** 456 | * Write audio register. 457 | * \param addr Address of audio register. Must be 0xFF10 <= addr <= 0xFF3F. 458 | * This is not checked in this function. 459 | * \param val Byte to write at address. 460 | */ 461 | void audio_write(const uint16_t addr, const uint8_t val) 462 | { 463 | /* Find sound channel corresponding to register address. */ 464 | uint_fast8_t i; 465 | 466 | if(addr == 0xFF26) 467 | { 468 | audio_mem[addr - AUDIO_ADDR_COMPENSATION] = val & 0x80; 469 | /* On APU power off, clear all registers apart from wave 470 | * RAM. */ 471 | if((val & 0x80) == 0) 472 | { 473 | memset(audio_mem, 0x00, 0xFF26 - AUDIO_ADDR_COMPENSATION); 474 | chans[0].enabled = false; 475 | chans[1].enabled = false; 476 | chans[2].enabled = false; 477 | chans[3].enabled = false; 478 | } 479 | 480 | return; 481 | } 482 | 483 | /* Ignore register writes if APU powered off. */ 484 | if(audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] == 0x00) 485 | return; 486 | 487 | audio_mem[addr - AUDIO_ADDR_COMPENSATION] = val; 488 | i = (addr - AUDIO_ADDR_COMPENSATION) / 5; 489 | 490 | switch (addr) { 491 | case 0xFF12: 492 | case 0xFF17: 493 | case 0xFF21: { 494 | chans[i].volume_init = val >> 4; 495 | chans[i].powered = (val >> 3) != 0; 496 | 497 | // "zombie mode" stuff, needed for Prehistorik Man and probably 498 | // others 499 | if (chans[i].powered && chans[i].enabled) { 500 | if ((chans[i].env.step == 0 && chans[i].env.inc != 0)) { 501 | if (val & 0x08) { 502 | chans[i].volume++; 503 | } else { 504 | chans[i].volume += 2; 505 | } 506 | } else { 507 | chans[i].volume = 16 - chans[i].volume; 508 | } 509 | 510 | chans[i].volume &= 0x0F; 511 | chans[i].env.step = val & 0x07; 512 | } 513 | } break; 514 | 515 | case 0xFF1C: 516 | chans[i].volume = chans[i].volume_init = (val >> 5) & 0x03; 517 | break; 518 | 519 | case 0xFF11: 520 | case 0xFF16: 521 | case 0xFF20: { 522 | const uint8_t duty_lookup[] = { 0x10, 0x30, 0x3C, 0xCF }; 523 | chans[i].len.load = val & 0x3f; 524 | chans[i].square.duty = duty_lookup[val >> 6]; 525 | break; 526 | } 527 | 528 | case 0xFF1B: 529 | chans[i].len.load = val; 530 | break; 531 | 532 | case 0xFF13: 533 | case 0xFF18: 534 | case 0xFF1D: 535 | chans[i].freq &= 0xFF00; 536 | chans[i].freq |= val; 537 | break; 538 | 539 | case 0xFF1A: 540 | chans[i].powered = (val & 0x80) != 0; 541 | chan_enable(i, val & 0x80); 542 | break; 543 | 544 | case 0xFF14: 545 | case 0xFF19: 546 | case 0xFF1E: 547 | chans[i].freq &= 0x00FF; 548 | chans[i].freq |= ((val & 0x07) << 8); 549 | /* Intentional fall-through. */ 550 | case 0xFF23: 551 | chans[i].len.enabled = val & 0x40 ? 1 : 0; 552 | if (val & 0x80) 553 | chan_trigger(i); 554 | 555 | break; 556 | 557 | case 0xFF22: 558 | chans[3].freq = val >> 4; 559 | chans[3].noise.lfsr_wide = !(val & 0x08); 560 | chans[3].noise.lfsr_div = val & 0x07; 561 | break; 562 | 563 | case 0xFF24: 564 | { 565 | vol_l = ((val >> 4) & 0x07); 566 | vol_r = (val & 0x07); 567 | break; 568 | } 569 | 570 | case 0xFF25: 571 | for (uint_fast8_t j = 0; j < 4; j++) { 572 | chans[j].on_left = (val >> (4 + j)) & 1; 573 | chans[j].on_right = (val >> j) & 1; 574 | } 575 | break; 576 | } 577 | } 578 | 579 | void audio_init(void) 580 | { 581 | /* Initialise channels and samples. */ 582 | memset(chans, 0, sizeof(chans)); 583 | chans[0].val = chans[1].val = -1; 584 | 585 | /* Initialise IO registers. */ 586 | { 587 | const uint8_t regs_init[] = { 0x80, 0xBF, 0xF3, 0xFF, 0x3F, 588 | 0xFF, 0x3F, 0x00, 0xFF, 0x3F, 589 | 0x7F, 0xFF, 0x9F, 0xFF, 0x3F, 590 | 0xFF, 0xFF, 0x00, 0x00, 0x3F, 591 | 0x77, 0xF3, 0xF1 }; 592 | 593 | for(uint_fast8_t i = 0; i < sizeof(regs_init); ++i) 594 | audio_write(0xFF10 + i, regs_init[i]); 595 | } 596 | 597 | /* Initialise Wave Pattern RAM. */ 598 | { 599 | const uint8_t wave_init[] = { 0xac, 0xdd, 0xda, 0x48, 600 | 0x36, 0x02, 0xcf, 0x16, 601 | 0x2c, 0x04, 0xe5, 0x2c, 602 | 0xac, 0xdd, 0xda, 0x48 }; 603 | 604 | for(uint_fast8_t i = 0; i < sizeof(wave_init); ++i) 605 | audio_write(0xFF30 + i, wave_init[i]); 606 | } 607 | } 608 | -------------------------------------------------------------------------------- /minigb_apu_cardputer/minigb_apu.h: -------------------------------------------------------------------------------- 1 | /** 2 | * minigb_apu is released under the terms listed within the LICENSE file. 3 | * 4 | * minigb_apu emulates the audio processing unit (APU) of the Game Boy. This 5 | * project is based on MiniGBS by Alex Baines: https://github.com/baines/MiniGBS 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | #define AUDIO_SAMPLE_RATE 32768 13 | 14 | #define DMG_CLOCK_FREQ 4194304.0 15 | #define SCREEN_REFRESH_CYCLES 70224.0 16 | #define VERTICAL_SYNC (DMG_CLOCK_FREQ/SCREEN_REFRESH_CYCLES) 17 | 18 | #define AUDIO_SAMPLES ((unsigned)(AUDIO_SAMPLE_RATE / VERTICAL_SYNC)) 19 | 20 | /** 21 | * Fill allocated buffer "data" with "len" number of 32-bit floating point 22 | * samples (native endian order) in stereo interleaved format. 23 | */ 24 | void audio_callback(void *ptr, uint8_t *data, int len); 25 | 26 | /** 27 | * Read audio register at given address "addr". 28 | */ 29 | uint8_t audio_read(const uint16_t addr); 30 | 31 | /** 32 | * Write "val" to audio register at given address "addr". 33 | */ 34 | void audio_write(const uint16_t addr, const uint8_t val); 35 | 36 | /** 37 | * Initialise audio driver. 38 | */ 39 | void audio_init(void); 40 | --------------------------------------------------------------------------------