├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── game-issue-template.md └── workflows │ └── main.yml ├── README.md ├── case ├── 3dprint-angryhelder │ ├── GB Interceptor Bottom Base.step │ ├── GB Interceptor Bottom Cap.step │ ├── GB Interceptor Bottom.step │ ├── GB Interceptor Button.step │ └── GB Interceptor Top.step ├── 3dprint │ ├── LICENSE │ ├── case.blend │ ├── case_bottom.stl │ ├── case_button.stl │ └── case_top.stl └── README.md ├── firmware ├── CMakeLists.txt ├── LICENSE ├── README.md ├── cpubus.c ├── cpubus.h ├── debug.c ├── debug.h ├── font │ └── font8x8_basic.h ├── gamedb │ ├── build_games_h.py │ ├── game_detection.c │ ├── game_detection.h │ ├── games.csv │ └── games.h ├── jpeg │ ├── base.jpg │ ├── base_jpeg.h │ ├── base_jpeg_no_chroma.h │ ├── base_no_chroma.jpg │ ├── generateBaseJpeg.py │ ├── jpeg.c │ ├── jpeg.h │ ├── jpeg_encoding.pio │ └── jpeg_prepare.pio ├── main.c ├── main.h ├── memory-bus.pio ├── opcodes.c ├── opcodes.h ├── osd.c ├── osd.h ├── pico_sdk_import.cmake ├── ppu.c ├── ppu.h ├── screens │ ├── default.h │ ├── default.png │ ├── default.svg │ ├── error.h │ ├── error.png │ ├── error.svg │ ├── off.h │ ├── off.png │ ├── off.svg │ └── png2header.sh ├── tusb_config.h ├── usb_descriptors.c └── usb_descriptors.h └── pcb ├── LICENSE ├── README.md ├── fabrication ├── bom │ └── gb-stream-cart.csv ├── gerber │ ├── gb-stream-cart-B_Cu.gbl │ ├── gb-stream-cart-B_Mask.gbs │ ├── gb-stream-cart-B_Paste.gbp │ ├── gb-stream-cart-B_Silkscreen.gbo │ ├── gb-stream-cart-Edge_Cuts.gm1 │ ├── gb-stream-cart-F_Cu.gtl │ ├── gb-stream-cart-F_Mask.gts │ ├── gb-stream-cart-F_Paste.gtp │ ├── gb-stream-cart-F_Silkscreen.gto │ ├── gb-stream-cart-NPTH-drl_map.gbr │ ├── gb-stream-cart-NPTH.drl │ ├── gb-stream-cart-PTH-drl_map.gbr │ └── gb-stream-cart-PTH.drl └── position │ ├── gb-stream-cart-bottom-pos.csv │ ├── gb-stream-cart-top-pos-fixed.csv │ └── gb-stream-cart-top-pos.csv └── kicad ├── MCU_RaspberryPi_RP2040.kicad_sym ├── RP2040_minimal.pretty ├── Crystal_SMD_HC49-US.kicad_mod ├── RP2040-QFN-56.kicad_mod └── USB_Micro-B_Amphenol_10103594-0001LF_Horizontal_modified.kicad_mod ├── SN74LVC4245APWR.kicad_sym ├── gb-connector.kicad_sym ├── gb-stream-cart.kicad_pcb ├── gb-stream-cart.kicad_pro ├── gb-stream-cart.kicad_sch └── gb-stream-cart.pretty ├── Fiducial_1mm_Mask1.7mm.kicad_mod ├── SN74LVC4245APWR.kicad_mod ├── cartridge-slot.kicad_mod ├── cartridge_PCBedge.kicad_mod ├── half-mouse-bite-3mm-slot.kicad_mod ├── mouse-bite-3mm-slot.kicad_mod └── rail-mount.kicad_mod /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Staacks] 4 | buy_me_a_coffee: there.oughta.be 5 | 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Host software (please complete the following information):** 27 | - OS: [e.g. Windows, MacOS, Linux, ...] 28 | - Software: [e.g. OBS, VLC, ffmpeg, Zoom, ...] 29 | 30 | **Game Boy:** 31 | - Model: [e.g. Game Boy Advance AGB-001] 32 | 33 | **Game:** 34 | - Title: [e.g. Tetris] 35 | - Version: [Usually printed on the cartridge, e.g. DMG-ZL-NOE] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/game-issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Game issue 3 | about: Bugs and problems with specific games 4 | title: '' 5 | labels: Game issue 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Type of issue** 11 | Interceptor reports error / Game looks different from Game Boy screen / other 12 | 13 | **Describe the bug** 14 | A clear and concise description of what the bug is. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Host software (please complete the following information):** 27 | - OS: [e.g. Windows, MacOS, Linux, ...] 28 | - Software: [e.g. OBS, VLC, ffmpeg, Zoom, ...] 29 | 30 | **Game Boy:** 31 | - Model: [e.g. Game Boy Advance AGB-001] 32 | 33 | **Game:** 34 | - Title: [e.g. Tetris] 35 | - Version: [Usually printed on the cartridge, e.g. DMG-ZL-NOE] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build gbinterceptor firmware 2 | on: 3 | push: 4 | pull_request: 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@main 11 | - name: Setup arm-none-eabi-gcc 12 | uses: carlosperate/arm-none-eabi-gcc-action@v1 13 | - name: Setup ninja 14 | uses: seanmiddleditch/gha-setup-ninja@master 15 | - name: Clone pico-sdk dependency (pinned versions of pico-sdk and tinyusb) 16 | run: | 17 | git clone https://github.com/raspberrypi/pico-sdk --recurse-submodules 18 | cd pico-sdk && sudo git checkout tags/2.0.0 && cd .. 19 | cd pico-sdk/lib/tinyusb && sudo git checkout 0.16.0 && cd ../../.. 20 | - name: Build firmware 21 | run: | 22 | arm-none-eabi-gcc --version 23 | cd ./firmware 24 | mkdir build && cd build 25 | cmake .. -G Ninja -DPICO_SDK_PATH=../../pico-sdk 26 | ninja 27 | - name: 'Upload artifacts' 28 | uses: actions/upload-artifact@v4 29 | with: 30 | name: firmware 31 | path: | 32 | ./firmware/build/gb_interceptor.* 33 | retention-days: 30 34 | - name: Build base video mode firmware 35 | run: | 36 | cd ./firmware/build 37 | rm -rf -- ..?* .[!.]* * 38 | cmake .. -G Ninja -DBASE_VIDEO_MODE=1 -DPICO_SDK_PATH=../../pico-sdk 39 | ninja 40 | - name: 'Upload base video mode artifacts' 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: firmware_base_video_mode 44 | path: | 45 | ./firmware/build/gb_interceptor.* 46 | retention-days: 30 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GB Interceptor 2 | Capture or stream Game Boy gameplay footage via USB without modifying the Game Boy. 3 | 4 | This open source and open hardware Game Boy adapter uses an rp2040 to capture the communication on the cartridge bus. This data is fed into an emulator, rendered and then offered as a USB video class device. 5 | 6 | Details about the concept can be found at https://there.oughta.be/a/game-boy-capture-cartridge and an overview is given in the following video: 7 | 8 | [![Youtube Video: GB Interceptor: A Game Boy capture cartridge](https://img.youtube.com/vi/6mOJtrFnawk/0.jpg)](https://youtu.be/6mOJtrFnawk) 9 | 10 | If you have pull-requests, bug reports or want to contribute new case designs, please do not hesitate to open an issue. For generic discussions, "show and tell" and if you are looking for support for something that is not a problem in the code here, I would recommend . 11 | 12 | Buy Me A Coffee 13 | 14 | # Warnings 15 | 16 | Please make sure to understand what this device does and what its limitations are. Most importantly, there is a little lag which will make it unsuitable for playing on a bis screen. It might be ok for slow games, but its focus is on recording and streaming. 17 | 18 | Also check out the compatibility tables for host software (for example OBS) and particular games. In principle this is a USB video class device and does not require drivers, but not all software supports the unsusual video format of the Interceptor. Similarly, games should usually work, but sometimes there are some details that the Interceptor does not yet support properly and some rare things that cannot work based on the principle of this device. 19 | 20 | * [Game Boy compatibility](https://github.com/Staacks/gbinterceptor/wiki/Game-Boy-compatibility) 21 | * [Host software compatibility](https://github.com/Staacks/gbinterceptor/wiki/Host-software-compatibility) 22 | * [Game compatibility](https://github.com/Staacks/gbinterceptor/wiki/Game-compatibility) 23 | 24 | # Building the Interceptor 25 | 26 | There is an [order and build video](https://youtu.be/Lg92tVkEE98) to guide you through the build process which I highly recommend, especially if you are not used to ordering PCBs. A [written guide can be found in this repository's wiki](https://github.com/Staacks/gbinterceptor/wiki/Build-guide). 27 | 28 | # Usage 29 | 30 | Simply plug the Interceptor into your Game Boy and the Game into the Interceptor. Connect the Interceptor to your computer before turning on the Game Boy. It should show up as a webcam in any software that supports USB Video devices (for example OBS Studio). Note that some software is not entirely happy with the exotic format, but it should work in most cases (see [Host software compatibility](https://github.com/Staacks/gbinterceptor/wiki/Host-software-compatibility)). 31 | 32 | Then just turn on the Game Boy and play. 33 | 34 | Note that you do not need to open the video stream first. The only important step is that the Interceptor is powered via USB before the Game Boy is turned on. 35 | 36 | ## Buttons 37 | 38 | The [Mode] button (next to the LEDs) allows to switch between grayscale/green colors and toggle frame blending. Each press switches colors and frame blending is toggled each time it returns to grayscale. 39 | 40 | The [Flash] button (near the center, usually inside the case) is only used to flash a new firmware version. Simply hold it while plugging in the USB cable. The Interceptor should appear as a storage device (like a USB stick) and you can simply drag the new firmware file (a uf2 file) into it. 41 | 42 | ## LEDs 43 | 44 | The red LED indicates that the Interceptor is powered. 45 | 46 | The blue LED stays on as long as the Interceptor is in sync with the game. (It may shortly turn off for a fraction of a second when the Game Boy turns the display off, i.e. when loading a level.) 47 | 48 | # Support 49 | 50 | While I do not give any guarantees I (and probably others too) am happy to help if I have the time. 51 | 52 | If you are having difficulties to order, build or use your Interceptor, please do not contact me directly but use [r/thereoughtabe on reddit](https://www.reddit.com/r/thereoughtabe/) as this allows others to help and the answer might help others, too. 53 | 54 | If you found a bug and in particular if you find glitches in a game, please check the [compatibility lists](https://github.com/Staacks/gbinterceptor/wiki) in the Wiki first to see if it is a known problem and open an issue here on Github otherwise. 55 | 56 | # Building the firmware 57 | 58 | Most users should just install the latest release uf2 as described in the [Build guide](https://github.com/Staacks/gbinterceptor/wiki/Build-guide), but if you want to build it yourself, you will need the [Raspberry Pi Pico SDK](https://github.com/raspberrypi/pico-sdk). Don't forget to set PICO_SDK_PATH accordingly. 59 | 60 | Make sure to check out the submodules too and specifically make sure that the [TinyUSB](https://github.com/hathach/tinyusb) submodule is at least on version 0.14.0 as the Pico SDK currently references an older version. (Also see [#3](https://github.com/Staacks/gbinterceptor/issues/3)) 61 | 62 | # License 63 | The code is released under the GNU General Public Licence 3 and the design files (PCB layout and 3d printed case) are released under the Creative Commons licence CC-BY 4.0. 64 | -------------------------------------------------------------------------------- /case/3dprint-angryhelder/GB Interceptor Button.step: -------------------------------------------------------------------------------- 1 | ISO-10303-21; 2 | HEADER; 3 | /* Generated by software containing ST-Developer 4 | * from STEP Tools, Inc. (www.steptools.com) 5 | */ 6 | 7 | FILE_DESCRIPTION( 8 | /* description */ (''), 9 | /* implementation_level */ '2;1'); 10 | 11 | FILE_NAME( 12 | /* name */ 13 | 'C:/Users/Helder/Documents/eagle/projects/Gameboy Interceptor- USB Vid 14 | eo/GB Interceptor Button.step', 15 | /* time_stamp */ '2022-12-25T21:37:25-06:00', 16 | /* author */ (''), 17 | /* organization */ (''), 18 | /* preprocessor_version */ 'ST-DEVELOPER v18.1', 19 | /* originating_system */ 'Autodesk Translation Framework v10.13.0.1454', 20 | 21 | /* authorisation */ ''); 22 | 23 | FILE_SCHEMA (('AUTOMOTIVE_DESIGN { 1 0 10303 214 3 1 1 }')); 24 | ENDSEC; 25 | 26 | DATA; 27 | #10=MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',(#13),#167); 28 | #11=SHAPE_REPRESENTATION_RELATIONSHIP('SRR','None',#174,#12); 29 | #12=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#14),#166); 30 | #13=STYLED_ITEM('',(#183),#14); 31 | #14=MANIFOLD_SOLID_BREP('Button',#85); 32 | #15=FACE_BOUND('',#36,.T.); 33 | #16=PLANE('',#106); 34 | #17=PLANE('',#110); 35 | #18=PLANE('',#111); 36 | #19=LINE('',#154,#21); 37 | #20=LINE('',#160,#22); 38 | #21=VECTOR('',#128,1.95); 39 | #22=VECTOR('',#135,3.); 40 | #23=CYLINDRICAL_SURFACE('',#104,1.95); 41 | #24=CYLINDRICAL_SURFACE('',#107,3.); 42 | #25=FACE_OUTER_BOUND('',#31,.T.); 43 | #26=FACE_OUTER_BOUND('',#32,.T.); 44 | #27=FACE_OUTER_BOUND('',#33,.T.); 45 | #28=FACE_OUTER_BOUND('',#34,.T.); 46 | #29=FACE_OUTER_BOUND('',#35,.T.); 47 | #30=FACE_OUTER_BOUND('',#37,.T.); 48 | #31=EDGE_LOOP('',(#60,#61,#62,#63,#64)); 49 | #32=EDGE_LOOP('',(#65,#66,#67,#68,#69)); 50 | #33=EDGE_LOOP('',(#70)); 51 | #34=EDGE_LOOP('',(#71,#72,#73,#74)); 52 | #35=EDGE_LOOP('',(#75)); 53 | #36=EDGE_LOOP('',(#76)); 54 | #37=EDGE_LOOP('',(#77)); 55 | #38=CIRCLE('',#100,1.45); 56 | #39=CIRCLE('',#101,0.5); 57 | #40=CIRCLE('',#102,1.95); 58 | #41=CIRCLE('',#103,1.95); 59 | #42=CIRCLE('',#105,1.95); 60 | #43=CIRCLE('',#108,3.); 61 | #44=CIRCLE('',#109,3.); 62 | #45=VERTEX_POINT('',#144); 63 | #46=VERTEX_POINT('',#146); 64 | #47=VERTEX_POINT('',#148); 65 | #48=VERTEX_POINT('',#152); 66 | #49=VERTEX_POINT('',#157); 67 | #50=VERTEX_POINT('',#159); 68 | #51=EDGE_CURVE('',#45,#45,#38,.T.); 69 | #52=EDGE_CURVE('',#45,#46,#39,.T.); 70 | #53=EDGE_CURVE('',#46,#47,#40,.T.); 71 | #54=EDGE_CURVE('',#47,#46,#41,.T.); 72 | #55=EDGE_CURVE('',#48,#48,#42,.T.); 73 | #56=EDGE_CURVE('',#48,#47,#19,.T.); 74 | #57=EDGE_CURVE('',#49,#49,#43,.T.); 75 | #58=EDGE_CURVE('',#49,#50,#20,.T.); 76 | #59=EDGE_CURVE('',#50,#50,#44,.T.); 77 | #60=ORIENTED_EDGE('',*,*,#51,.T.); 78 | #61=ORIENTED_EDGE('',*,*,#52,.T.); 79 | #62=ORIENTED_EDGE('',*,*,#53,.T.); 80 | #63=ORIENTED_EDGE('',*,*,#54,.T.); 81 | #64=ORIENTED_EDGE('',*,*,#52,.F.); 82 | #65=ORIENTED_EDGE('',*,*,#55,.F.); 83 | #66=ORIENTED_EDGE('',*,*,#56,.T.); 84 | #67=ORIENTED_EDGE('',*,*,#53,.F.); 85 | #68=ORIENTED_EDGE('',*,*,#54,.F.); 86 | #69=ORIENTED_EDGE('',*,*,#56,.F.); 87 | #70=ORIENTED_EDGE('',*,*,#51,.F.); 88 | #71=ORIENTED_EDGE('',*,*,#57,.F.); 89 | #72=ORIENTED_EDGE('',*,*,#58,.T.); 90 | #73=ORIENTED_EDGE('',*,*,#59,.F.); 91 | #74=ORIENTED_EDGE('',*,*,#58,.F.); 92 | #75=ORIENTED_EDGE('',*,*,#59,.T.); 93 | #76=ORIENTED_EDGE('',*,*,#55,.T.); 94 | #77=ORIENTED_EDGE('',*,*,#57,.T.); 95 | #78=TOROIDAL_SURFACE('',#99,1.45,0.5); 96 | #79=ADVANCED_FACE('',(#25),#78,.T.); 97 | #80=ADVANCED_FACE('',(#26),#23,.T.); 98 | #81=ADVANCED_FACE('',(#27),#16,.T.); 99 | #82=ADVANCED_FACE('',(#28),#24,.T.); 100 | #83=ADVANCED_FACE('',(#29,#15),#17,.T.); 101 | #84=ADVANCED_FACE('',(#30),#18,.F.); 102 | #85=CLOSED_SHELL('',(#79,#80,#81,#82,#83,#84)); 103 | #86=DERIVED_UNIT_ELEMENT(#88,1.); 104 | #87=DERIVED_UNIT_ELEMENT(#169,-3.); 105 | #88=( 106 | MASS_UNIT() 107 | NAMED_UNIT(*) 108 | SI_UNIT(.KILO.,.GRAM.) 109 | ); 110 | #89=DERIVED_UNIT((#86,#87)); 111 | #90=MEASURE_REPRESENTATION_ITEM('density measure', 112 | POSITIVE_RATIO_MEASURE(7850.),#89); 113 | #91=PROPERTY_DEFINITION_REPRESENTATION(#96,#93); 114 | #92=PROPERTY_DEFINITION_REPRESENTATION(#97,#94); 115 | #93=REPRESENTATION('material name',(#95),#166); 116 | #94=REPRESENTATION('density',(#90),#166); 117 | #95=DESCRIPTIVE_REPRESENTATION_ITEM('Steel','Steel'); 118 | #96=PROPERTY_DEFINITION('material property','material name',#176); 119 | #97=PROPERTY_DEFINITION('material property','density of part',#176); 120 | #98=AXIS2_PLACEMENT_3D('placement',#142,#112,#113); 121 | #99=AXIS2_PLACEMENT_3D('',#143,#114,#115); 122 | #100=AXIS2_PLACEMENT_3D('',#145,#116,#117); 123 | #101=AXIS2_PLACEMENT_3D('',#147,#118,#119); 124 | #102=AXIS2_PLACEMENT_3D('',#149,#120,#121); 125 | #103=AXIS2_PLACEMENT_3D('',#150,#122,#123); 126 | #104=AXIS2_PLACEMENT_3D('',#151,#124,#125); 127 | #105=AXIS2_PLACEMENT_3D('',#153,#126,#127); 128 | #106=AXIS2_PLACEMENT_3D('',#155,#129,#130); 129 | #107=AXIS2_PLACEMENT_3D('',#156,#131,#132); 130 | #108=AXIS2_PLACEMENT_3D('',#158,#133,#134); 131 | #109=AXIS2_PLACEMENT_3D('',#161,#136,#137); 132 | #110=AXIS2_PLACEMENT_3D('',#162,#138,#139); 133 | #111=AXIS2_PLACEMENT_3D('',#163,#140,#141); 134 | #112=DIRECTION('axis',(0.,0.,1.)); 135 | #113=DIRECTION('refdir',(1.,0.,0.)); 136 | #114=DIRECTION('center_axis',(0.,0.,1.)); 137 | #115=DIRECTION('ref_axis',(1.,0.,0.)); 138 | #116=DIRECTION('center_axis',(0.,0.,-1.)); 139 | #117=DIRECTION('ref_axis',(-1.,0.,0.)); 140 | #118=DIRECTION('center_axis',(0.,-1.,0.)); 141 | #119=DIRECTION('ref_axis',(-1.,0.,0.)); 142 | #120=DIRECTION('center_axis',(0.,0.,1.)); 143 | #121=DIRECTION('ref_axis',(-1.,0.,0.)); 144 | #122=DIRECTION('center_axis',(0.,0.,1.)); 145 | #123=DIRECTION('ref_axis',(-1.,0.,0.)); 146 | #124=DIRECTION('center_axis',(0.,0.,-1.)); 147 | #125=DIRECTION('ref_axis',(-1.,0.,0.)); 148 | #126=DIRECTION('center_axis',(0.,0.,-1.)); 149 | #127=DIRECTION('ref_axis',(-1.,0.,0.)); 150 | #128=DIRECTION('',(0.,0.,1.)); 151 | #129=DIRECTION('center_axis',(0.,0.,1.)); 152 | #130=DIRECTION('ref_axis',(-1.,0.,0.)); 153 | #131=DIRECTION('center_axis',(0.,0.,-1.)); 154 | #132=DIRECTION('ref_axis',(-1.,0.,0.)); 155 | #133=DIRECTION('center_axis',(0.,0.,-1.)); 156 | #134=DIRECTION('ref_axis',(-1.,0.,0.)); 157 | #135=DIRECTION('',(0.,0.,1.)); 158 | #136=DIRECTION('center_axis',(0.,0.,1.)); 159 | #137=DIRECTION('ref_axis',(-1.,0.,0.)); 160 | #138=DIRECTION('center_axis',(0.,0.,1.)); 161 | #139=DIRECTION('ref_axis',(-1.,0.,0.)); 162 | #140=DIRECTION('center_axis',(0.,0.,1.)); 163 | #141=DIRECTION('ref_axis',(-1.,0.,0.)); 164 | #142=CARTESIAN_POINT('',(0.,0.,0.)); 165 | #143=CARTESIAN_POINT('Origin',(-30.3052050582665,55.6170748852214,-4.11763990114109)); 166 | #144=CARTESIAN_POINT('',(-31.7552050582665,55.6170748852214,-3.61763990114109)); 167 | #145=CARTESIAN_POINT('Origin',(-30.3052050582665,55.6170748852214,-3.61763990114109)); 168 | #146=CARTESIAN_POINT('',(-32.2552050582664,55.6170748852214,-4.11763990114109)); 169 | #147=CARTESIAN_POINT('Origin',(-31.7552050582665,55.6170748852214,-4.11763990114109)); 170 | #148=CARTESIAN_POINT('',(-28.3552050582665,55.6170748852214,-4.11763990114109)); 171 | #149=CARTESIAN_POINT('Origin',(-30.3052050582665,55.6170748852214,-4.11763990114109)); 172 | #150=CARTESIAN_POINT('Origin',(-30.3052050582665,55.6170748852214,-4.11763990114109)); 173 | #151=CARTESIAN_POINT('Origin',(-30.3052050582665,55.6170748852214,-3.61763990114109)); 174 | #152=CARTESIAN_POINT('',(-28.3552050582665,55.6170748852214,-6.61764002035038)); 175 | #153=CARTESIAN_POINT('Origin',(-30.3052050582665,55.6170748852214,-6.61764002035038)); 176 | #154=CARTESIAN_POINT('',(-28.3552050582665,55.6170748852214,-3.61763990114109)); 177 | #155=CARTESIAN_POINT('Origin',(-30.3052050582665,55.6170748852214,-3.61763990114109)); 178 | #156=CARTESIAN_POINT('Origin',(-30.3079463432022,55.617195175656,-6.61764002035038)); 179 | #157=CARTESIAN_POINT('',(-27.3079463432022,55.617195175656,-8.61764002035038)); 180 | #158=CARTESIAN_POINT('Origin',(-30.3079463432022,55.617195175656,-8.61764002035038)); 181 | #159=CARTESIAN_POINT('',(-27.3079463432022,55.617195175656,-6.61764002035038)); 182 | #160=CARTESIAN_POINT('',(-27.3079463432022,55.617195175656,-6.61764002035038)); 183 | #161=CARTESIAN_POINT('Origin',(-30.3079463432022,55.617195175656,-6.61764002035038)); 184 | #162=CARTESIAN_POINT('Origin',(-30.3079463432022,55.617195175656,-6.61764002035038)); 185 | #163=CARTESIAN_POINT('Origin',(-30.3079463432022,55.617195175656,-8.61764002035038)); 186 | #164=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(0.01),#168, 187 | 'DISTANCE_ACCURACY_VALUE', 188 | 'Maximum model space distance between geometric entities at asserted c 189 | onnectivities'); 190 | #165=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(0.01),#168, 191 | 'DISTANCE_ACCURACY_VALUE', 192 | 'Maximum model space distance between geometric entities at asserted c 193 | onnectivities'); 194 | #166=( 195 | GEOMETRIC_REPRESENTATION_CONTEXT(3) 196 | GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#164)) 197 | GLOBAL_UNIT_ASSIGNED_CONTEXT((#168,#170,#171)) 198 | REPRESENTATION_CONTEXT('','3D') 199 | ); 200 | #167=( 201 | GEOMETRIC_REPRESENTATION_CONTEXT(3) 202 | GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#165)) 203 | GLOBAL_UNIT_ASSIGNED_CONTEXT((#168,#170,#171)) 204 | REPRESENTATION_CONTEXT('','3D') 205 | ); 206 | #168=( 207 | LENGTH_UNIT() 208 | NAMED_UNIT(*) 209 | SI_UNIT(.MILLI.,.METRE.) 210 | ); 211 | #169=( 212 | LENGTH_UNIT() 213 | NAMED_UNIT(*) 214 | SI_UNIT($,.METRE.) 215 | ); 216 | #170=( 217 | NAMED_UNIT(*) 218 | PLANE_ANGLE_UNIT() 219 | SI_UNIT($,.RADIAN.) 220 | ); 221 | #171=( 222 | NAMED_UNIT(*) 223 | SI_UNIT($,.STERADIAN.) 224 | SOLID_ANGLE_UNIT() 225 | ); 226 | #172=SHAPE_DEFINITION_REPRESENTATION(#173,#174); 227 | #173=PRODUCT_DEFINITION_SHAPE('',$,#176); 228 | #174=SHAPE_REPRESENTATION('',(#98),#166); 229 | #175=PRODUCT_DEFINITION_CONTEXT('part definition',#180,'design'); 230 | #176=PRODUCT_DEFINITION('GB Interceptor Shell', 231 | 'GB Interceptor Shell v11',#177,#175); 232 | #177=PRODUCT_DEFINITION_FORMATION('',$,#182); 233 | #178=PRODUCT_RELATED_PRODUCT_CATEGORY('GB Interceptor Shell v11', 234 | 'GB Interceptor Shell v11',(#182)); 235 | #179=APPLICATION_PROTOCOL_DEFINITION('international standard', 236 | 'automotive_design',2009,#180); 237 | #180=APPLICATION_CONTEXT( 238 | 'Core Data for Automotive Mechanical Design Process'); 239 | #181=PRODUCT_CONTEXT('part definition',#180,'mechanical'); 240 | #182=PRODUCT('GB Interceptor Shell','GB Interceptor Shell v11',$,(#181)); 241 | #183=PRESENTATION_STYLE_ASSIGNMENT((#184)); 242 | #184=SURFACE_STYLE_USAGE(.BOTH.,#185); 243 | #185=SURFACE_SIDE_STYLE('',(#186)); 244 | #186=SURFACE_STYLE_FILL_AREA(#187); 245 | #187=FILL_AREA_STYLE('Steel - Satin',(#188)); 246 | #188=FILL_AREA_STYLE_COLOUR('Steel - Satin',#189); 247 | #189=COLOUR_RGB('Steel - Satin',0.627450980392157,0.627450980392157,0.627450980392157); 248 | ENDSEC; 249 | END-ISO-10303-21; 250 | -------------------------------------------------------------------------------- /case/3dprint/case.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Staacks/gbinterceptor/14213d507fb056389c3be0bd35f8b57b359a52ea/case/3dprint/case.blend -------------------------------------------------------------------------------- /case/3dprint/case_bottom.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Staacks/gbinterceptor/14213d507fb056389c3be0bd35f8b57b359a52ea/case/3dprint/case_bottom.stl -------------------------------------------------------------------------------- /case/3dprint/case_button.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Staacks/gbinterceptor/14213d507fb056389c3be0bd35f8b57b359a52ea/case/3dprint/case_button.stl -------------------------------------------------------------------------------- /case/3dprint/case_top.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Staacks/gbinterceptor/14213d507fb056389c3be0bd35f8b57b359a52ea/case/3dprint/case_top.stl -------------------------------------------------------------------------------- /case/README.md: -------------------------------------------------------------------------------- 1 | # GB Interceptor Case 2 | 3 | These are various resources and design files for cases for the GB Interceptor. The 3d-printed one referenced in the Youtube videos and the build guide [build guide](https://github.com/Staacks/gbinterceptor/wiki/Build-guide) can be found in the folder 3dprint. 4 | 5 | **You can find more options by the community by looking at the [case guides](https://github.com/Staacks/gbinterceptor/wiki#case-options) on the Wiki.** 6 | 7 | # Important note about the break-out extension board 8 | 9 | If you want to create a custom case using the extension board to place the cartridge slot in a more flexible manner, please take special care to correctly match the pins. For this, ignore the silklayer (printing) ob the PCB and look at the position of pin one, which is the one with the square pad (the others are circular). The printed outline on the PCB may work fine for jumper wires or special connectors, but with the normal ribbon cable connectors (known from old disk drives) the pin headers of either the extension board or the GB Interceptor need to be soldered onto the opposite side of the board in order for the pins to match. Therefore, keep track of pin one when creating your own solution. 10 | 11 | # Folder list 12 | 13 | Please, find details about each option in the [case guides](https://github.com/Staacks/gbinterceptor/wiki#case-options) on the Wiki. 14 | 15 | * __3dprint__ 16 | The original 3d printed case as discussed in the videos. 17 | * __3dprint-angryhelder__ 18 | Variant by AngryHelder (Helder Silva) of the original 3d printed case. These are step files derived from the original 3dprint case to allow for easy adaption into new case projects. It also includes a version that is easier and nicer to print but requires glueing. 19 | 20 | 21 | # License 22 | 23 | The design files are licensed under the Creative Commons Attribution License (CC-BY 4.0). 24 | -------------------------------------------------------------------------------- /firmware/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | 3 | include(pico_sdk_import.cmake) 4 | 5 | project(gb_interceptor C CXX ASM) 6 | set(CMAKE_C_STANDARD 11) 7 | set(CMAKE_CXX_STANDARD 17) 8 | 9 | pico_sdk_init() 10 | 11 | add_executable(gb_interceptor) 12 | 13 | target_sources(gb_interceptor PUBLIC 14 | ${CMAKE_CURRENT_LIST_DIR}/main.c 15 | ${CMAKE_CURRENT_LIST_DIR}/cpubus.c 16 | ${CMAKE_CURRENT_LIST_DIR}/opcodes.c 17 | ${CMAKE_CURRENT_LIST_DIR}/ppu.c 18 | ${CMAKE_CURRENT_LIST_DIR}/jpeg/jpeg.c 19 | ${CMAKE_CURRENT_LIST_DIR}/osd.c 20 | ${CMAKE_CURRENT_LIST_DIR}/debug.c 21 | ${CMAKE_CURRENT_LIST_DIR}/usb_descriptors.c 22 | ${CMAKE_CURRENT_LIST_DIR}/gamedb/game_detection.c 23 | ) 24 | 25 | target_link_libraries(gb_interceptor PUBLIC pico_stdlib hardware_pio pico_multicore hardware_interp hardware_dma tinyusb_device tinyusb_board pico_unique_id) 26 | 27 | target_include_directories(gb_interceptor PUBLIC ${CMAKE_CURRENT_LIST_DIR}) 28 | 29 | target_compile_definitions(gb_interceptor PUBLIC PICO_XOSC_STARTUP_DELAY_MULTIPLIER=64) 30 | 31 | pico_add_extra_outputs(gb_interceptor) 32 | 33 | pico_enable_stdio_usb(gb_interceptor 1) 34 | pico_enable_stdio_uart(gb_interceptor 0) 35 | 36 | pico_set_binary_type(gb_interceptor copy_to_ram) 37 | 38 | pico_generate_pio_header(gb_interceptor 39 | ${CMAKE_CURRENT_LIST_DIR}/memory-bus.pio 40 | ) 41 | 42 | pico_generate_pio_header(gb_interceptor 43 | ${CMAKE_CURRENT_LIST_DIR}/jpeg/jpeg_prepare.pio 44 | ) 45 | 46 | pico_generate_pio_header(gb_interceptor 47 | ${CMAKE_CURRENT_LIST_DIR}/jpeg/jpeg_encoding.pio 48 | ) 49 | 50 | if (DEFINED BASE_VIDEO_MODE) 51 | MESSAGE(STATUS "Building BASE_VIDEO_MODE variant") 52 | add_compile_definitions("BASE_VIDEO_MODE") #Uncomment for base video mode version with fixed 30fps and no frame blending 53 | endif() 54 | -------------------------------------------------------------------------------- /firmware/README.md: -------------------------------------------------------------------------------- 1 | # GB Interceptor Firmware 2 | 3 | This folder contains the source code for the firmware of the GB Interceptor. It requires the Raspberry Pi Pico SDK as well as TinyUSB (which is usually a submodule of the Pico SDK repository). If you want to build the firmware yourself, remember to set `PICO_SDK_PATH` correctly. 4 | 5 | The subfolder screens contains the info screens shown when no game is running. The `sh` script in the same folder can be used to convert new images to header files. 6 | 7 | # License 8 | 9 | This code is licensed under GNU General Public License v3. 10 | -------------------------------------------------------------------------------- /firmware/cpubus.h: -------------------------------------------------------------------------------- 1 | #ifndef GBINTERCEPTOR_CPUBUS 2 | #define GBINTERCEPTOR_CPUBUS 3 | 4 | #include "pico/stdlib.h" 5 | #include "hardware/pio.h" 6 | #include "ppu.h" 7 | 8 | extern uint32_t cycleRatio; 9 | 10 | extern volatile bool running; 11 | extern volatile const char * error; 12 | extern volatile int errorOpcode; 13 | 14 | extern PIO busPIO; 15 | extern uint32_t busPIOemptyMask, busPIOstallMask; 16 | extern uint busSM; 17 | 18 | extern uint32_t volatile rawBusData; 19 | extern uint8_t volatile * opcode; 20 | extern uint16_t volatile * address; 21 | extern uint8_t volatile * extra; 22 | 23 | #define HISTORY_READAHEAD 5 24 | extern uint32_t history[]; 25 | extern uint volatile cycleIndex; 26 | extern uint8_t volatile * historyIndex; //Index for history array, lowest byte of cycleIndex 27 | extern uint volatile div; 28 | 29 | extern uint ignoreCycles; 30 | extern bool cartridgeDMA; 31 | extern uint cartridgeDMAsrc; 32 | extern uint cartridgeDMAdst; 33 | 34 | extern mutex_t cpubusMutex; 35 | #define DUMPMORE 10 //Additional lines to dump after error 36 | 37 | 38 | extern volatile uint8_t memory[]; 39 | 40 | //CPU registers 41 | extern uint8_t registers[]; 42 | extern uint8_t * b; 43 | extern uint8_t * c; 44 | extern uint8_t * d; 45 | extern uint8_t * e; 46 | extern uint8_t * h; 47 | extern uint8_t * l; 48 | // +6 is unused to align af with opcode order 49 | extern uint8_t * a; 50 | extern uint8_t * f; 51 | extern uint16_t * bc; 52 | extern uint16_t * de; 53 | extern uint16_t * hl; 54 | extern uint16_t sp; 55 | 56 | extern uint32_t flags; 57 | extern uint8_t *Z, *N, *H, *C; 58 | 59 | extern bool interruptsEnabled; 60 | extern uint interruptsEnableCycle; 61 | 62 | void handleMemoryBus(); 63 | 64 | void getNextFromBus(); 65 | 66 | void dmaToOAM(uint16_t source); 67 | 68 | void stop(const char* errorMsg); 69 | 70 | #endif -------------------------------------------------------------------------------- /firmware/debug.c: -------------------------------------------------------------------------------- 1 | #include "debug.h" 2 | 3 | #include 4 | #include "pico/stdlib.h" 5 | 6 | #include "cpubus.h" 7 | 8 | #ifdef DEBUG_LOG_REGISTERS 9 | uint32_t volatile registerHistory32[64][2]; 10 | uint16_t volatile spHistory[64]; 11 | uint32_t volatile flagHistory[64]; 12 | #endif 13 | 14 | #ifdef DEBUG_PPU_TIMING 15 | bool recordPPUTiming = false; 16 | bool ppuTimingReady = false; 17 | bool recordPPUTimingStarted = false; 18 | uint ppuTiming[LINES][4];//OAM search start, OAM search done, rendering start, rendering done 19 | struct PPUTimingEvents ppuTimingEvents; 20 | #endif 21 | 22 | #ifdef DEBUG_EVENTS 23 | const char * opcodeNames[256] = { 24 | "NOP ","LD BC,d16 ","LD (BC),A ","INC BC ","INC B ","DEC B ","LD B,d8 ","RLCA ","LD (a16),SP","ADD HL,BC ","LD A,(BC) ","DEC BC ","INC C ","DEC C ","LD C,d8 ","RRCA ", 25 | "STOP 0 ","LD DE,d16 ","LD (DE),A ","INC DE ","INC D ","DEC D ","LD D,d8 ","RLA ","JR r8 ","ADD HL,DE ","LD A,(DE) ","DEC DE ","INC E ","DEC E ","LD E,d8 ","RRA ", 26 | "JR NZ,r8 ","LD HL,d16 ","LD (HL+),A ","INC HL ","INC H ","DEC H ","LD H,d8 ","DAA ","JR Z,r8 ","ADD HL,HL ","LD A,(HL+) ","DEC HL ","INC L ","DEC L ","LD L,d8 ","CPL ", 27 | "JR NC,r8 ","LD SP,d16 ","LD (HL-),A ","INC SP ","INC (HL) ","DEC (HL) ","LD (HL),d8 ","SCF ","JR C,r8 ","ADD HL,SP ","LD A,(HL-) ","DEC SP ","INC A ","DEC A ","LD A,d8 ","CCF ", 28 | "LD B,B ","LD B,C ","LD B,D ","LD B,E ","LD B,H ","LD B,L ","LD B,(HL) ","LD B,A ","LD C,B ","LD C,C ","LD C,D ","LD C,E ","LD C,H ","LD C,L ","LD C,(HL) ","LD C,A ", 29 | "LD D,B ","LD D,C ","LD D,D ","LD D,E ","LD D,H ","LD D,L ","LD D,(HL) ","LD D,A ","LD E,B ","LD E,C ","LD E,D ","LD E,E ","LD E,H ","LD E,L ","LD E,(HL) ","LD E,A ", 30 | "LD H,B ","LD H,C ","LD H,D ","LD H,E ","LD H,H ","LD H,L ","LD H,(HL) ","LD H,A ","LD L,B ","LD L,C ","LD L,D ","LD L,E ","LD L,H ","LD L,L ","LD L,(HL) ","LD L,A ", 31 | "LD (HL),B ","LD (HL),C ","LD (HL),D ","LD (HL),E ","LD (HL),H ","LD (HL),L ","HALT ","LD (HL),A ","LD A,B ","LD A,C ","LD A,D ","LD A,E ","LD A,H ","LD A,L ","LD A,(HL) ","LD A,A ", 32 | "ADD A,B ","ADD A,C ","ADD A,D ","ADD A,E ","ADD A,H ","ADD A,L ","ADD A,(HL) ","ADD A,A ","ADC A,B ","ADC A,C ","ADC A,D ","ADC A,E ","ADC A,H ","ADC A,L ","ADC A,(HL) ","ADC A,A ", 33 | "SUB B ","SUB C ","SUB D ","SUB E ","SUB H ","SUB L ","SUB (HL) ","SUB A ","SBC A,B ","SBC A,C ","SBC A,D ","SBC A,E ","SBC A,H ","SBC A,L ","SBC A,(HL) ","SBC A,A ", 34 | "AND B ","AND C ","AND D ","AND E ","AND H ","AND L ","AND (HL) ","AND A ","XOR B ","XOR C ","XOR D ","XOR E ","XOR H ","XOR L ","XOR (HL) ","XOR A ", 35 | "OR B ","OR C ","OR D ","OR E ","OR H ","OR L ","OR (HL) ","OR A ","CP B ","CP C ","CP D ","CP E ","CP H ","CP L ","CP (HL) ","CP A ", 36 | "RET NZ ","POP BC ","JP NZ,a16 ","JP a16 ","CALL NZ,a16","PUSH BC ","ADD A,d8 ","RST 00H ","RET Z ","RET ","JP Z,a16 ","PREFIX CB ","CALL Z,a16 ","CALL a16 ","ADC A,d8 ","RST 08H ", 37 | "RET NC ","POP DE ","JP NC,a16 ","ERROR ","CALL NC,a16","PUSH DE ","SUB d8 ","RST 10H ","RET C ","RETI ","JP C,a16 ","ERROR ","CALL C,a16 ","ERROR ","SBC A,d8 ","RST 18H ", 38 | "LDH (a8),A ","POP HL ","LD (C),A ","ERROR ","ERROR ","PUSH HL ","AND d8 ","RST 20H ","ADD SP,r8 ","JP (HL) ","LD (a16),A ","ERROR ","ERROR ","ERROR ","XOR d8 ","RST 28H ", 39 | "LDH A,(a8) ","POP AF ","LD A,(C) ","DI ","ERROR ","PUSH AF ","OR d8 ","RST 30H ","LD HL,SP+r8","LD SP,HL ","LD A,(a16) ","EI ","ERROR ","ERROR ","CP d8 ","RST 38H " 40 | }; 41 | 42 | const char * opcodeNames16bit[256] = { 43 | "RLC B ","RLC C ","RLC D ","RLC E ","RLC H ","RLC L ","RLC (HL) ","RLC A ","RRC B ","RRC C ","RRC D ","RRC E ","RRC H ","RRC L ","RRC (HL) ","RRC A ", 44 | "RL B ","RL C ","RL D ","RL E ","RL H ","RL L ","RL (HL) ","RL A ","RR B ","RR C ","RR D ","RR E ","RR H ","RR L ","RR (HL) ","RR A ", 45 | "SLA B ","SLA C ","SLA D ","SLA E ","SLA H ","SLA L ","SLA (HL) ","SLA A ","SRA B ","SRA C ","SRA D ","SRA E ","SRA H ","SRA L ","SRA (HL) ","SRA A ", 46 | "SWAP B ","SWAP C ","SWAP D ","SWAP E ","SWAP H ","SWAP L ","SWAP (HL) ","SWAP A ","SRL B ","SRL C ","SRL D ","SRL E ","SRL H ","SRL L ","SRL (HL) ","SRL A ", 47 | "BIT 0,B ","BIT 0,C ","BIT 0,D ","BIT 0,E ","BIT 0,H ","BIT 0,L ","BIT 0,(HL) ","BIT 0,A ","BIT 1,B ","BIT 1,C ","BIT 1,D ","BIT 1,E ","BIT 1,H ","BIT 1,L ","BIT 1,(HL) ","BIT 1,A ", 48 | "BIT 2,B ","BIT 2,C ","BIT 2,D ","BIT 2,E ","BIT 2,H ","BIT 2,L ","BIT 2,(HL) ","BIT 2,A ","BIT 3,B ","BIT 3,C ","BIT 3,D ","BIT 3,E ","BIT 3,H ","BIT 3,L ","BIT 3,(HL) ","BIT 3,A ", 49 | "BIT 4,B ","BIT 4,C ","BIT 4,D ","BIT 4,E ","BIT 4,H ","BIT 4,L ","BIT 4,(HL) ","BIT 4,A ","BIT 5,B ","BIT 5,C ","BIT 5,D ","BIT 5,E ","BIT 5,H ","BIT 5,L ","BIT 5,(HL) ","BIT 5,A ", 50 | "BIT 6,B ","BIT 6,C ","BIT 6,D ","BIT 6,E ","BIT 6,H ","BIT 6,L ","BIT 6,(HL) ","BIT 6,A ","BIT 7,B ","BIT 7,C ","BIT 7,D ","BIT 7,E ","BIT 7,H ","BIT 7,L ","BIT 7,(HL) ","BIT 7,A ", 51 | "RES 0,B ","RES 0,C ","RES 0,D ","RES 0,E ","RES 0,H ","RES 0,L ","RES 0,(HL) ","RES 0,A ","RES 1,B ","RES 1,C ","RES 1,D ","RES 1,E ","RES 1,H ","RES 1,L ","RES 1,(HL) ","RES 1,A ", 52 | "RES 2,B ","RES 2,C ","RES 2,D ","RES 2,E ","RES 2,H ","RES 2,L ","RES 2,(HL) ","RES 2,A ","RES 3,B ","RES 3,C ","RES 3,D ","RES 3,E ","RES 3,H ","RES 3,L ","RES 3,(HL) ","RES 3,A ", 53 | "RES 4,B ","RES 4,C ","RES 4,D ","RES 4,E ","RES 4,H ","RES 4,L ","RES 4,(HL) ","RES 4,A ","RES 5,B ","RES 5,C ","RES 5,D ","RES 5,E ","RES 5,H ","RES 5,L ","RES 5,(HL) ","RES 5,A ", 54 | "RES 6,B ","RES 6,C ","RES 6,D ","RES 6,E ","RES 6,H ","RES 6,L ","RES 6,(HL) ","RES 6,A ","RES 7,B ","RES 7,C ","RES 7,D ","RES 7,E ","RES 7,H ","RES 7,L ","RES 7,(HL) ","RES 7,A ", 55 | "SET 0,B ","SET 0,C ","SET 0,D ","SET 0,E ","SET 0,H ","SET 0,L ","SET 0,(HL) ","SET 0,A ","SET 1,B ","SET 1,C ","SET 1,D ","SET 1,E ","SET 1,H ","SET 1,L ","SET 1,(HL) ","SET 1,A ", 56 | "SET 2,B ","SET 2,C ","SET 2,D ","SET 2,E ","SET 2,H ","SET 2,L ","SET 2,(HL) ","SET 2,A ","SET 3,B ","SET 3,C ","SET 3,D ","SET 3,E ","SET 3,H ","SET 3,L ","SET 3,(HL) ","SET 3,A ", 57 | "SET 4,B ","SET 4,C ","SET 4,D ","SET 4,E ","SET 4,H ","SET 4,L ","SET 4,(HL) ","SET 4,A ","SET 5,B ","SET 5,C ","SET 5,D ","SET 5,E ","SET 5,H ","SET 5,L ","SET 5,(HL) ","SET 5,A ", 58 | "SET 6,B ","SET 6,C ","SET 6,D ","SET 6,E ","SET 6,H ","SET 6,L ","SET 6,(HL) ","SET 6,A ","SET 7,B ","SET 7,C ","SET 7,D ","SET 7,E ","SET 7,H ","SET 7,L ","SET 7,(HL) ","SET 7,A " 59 | }; 60 | #endif 61 | 62 | void dumpBus() { //Dump opcode history 63 | bool pastIndicator = false; 64 | uint8_t dumpIndicator = *historyIndex - DUMPMORE + HISTORY_READAHEAD; 65 | *historyIndex += HISTORY_READAHEAD; 66 | printf("\n===============================\n"); 67 | bool isPrefixOpcode = false; 68 | for (int i = 0; i < 256; i++) { 69 | (*historyIndex)++; 70 | const uint32_t b = history[*historyIndex]; 71 | const char * event = " "; 72 | #ifdef DEBUG_EVENTS 73 | if (!pastIndicator) { 74 | if ((b & 0x01000000) != 0) { 75 | isPrefixOpcode = ((uint8_t)(b >> 16) == 0xcb); 76 | event = opcodeNames[(uint8_t)(b >> 16)]; 77 | } else if ((b & 0x02000000) != 0) { 78 | event = "IRQ "; 79 | } else if (isPrefixOpcode) { 80 | event = opcodeNames16bit[(uint8_t)(b >> 16)]; 81 | isPrefixOpcode = false; 82 | } 83 | } 84 | #endif 85 | printf("%s %02x %s %s %s %04x %02x %s", 86 | *historyIndex == dumpIndicator ? ">" : " ", 87 | (uint8_t)(b >> 24), 88 | b & 0x20000000 ? "nWR" : " WR", 89 | b & 0x40000000 ? "nRD" : " RD", 90 | b & 0x80000000 ? "nCS" : " CS", 91 | (uint16_t)b, 92 | (uint8_t)(b >> 16), 93 | event); 94 | #ifdef DEBUG_LOG_REGISTERS 95 | if ((b & 0x01000000) != 0 && i >= 0xc0) { 96 | uint8_t volatile * reg = (uint8_t volatile *)®isterHistory32[*historyIndex & 0x3f]; 97 | uint8_t volatile * flags = (uint8_t volatile*)&flagHistory[*historyIndex & 0x3f]; 98 | printf(" A=%02x BC=%02x%02x DE=%02x%02x HL=%02x%02x SP=%04x %s%s%s%s", 99 | reg[6], reg[1], reg[0], reg[3], reg[2], reg[5], reg[4], //See registers in cpubus.c about the uninstuitive order 100 | spHistory[*historyIndex & 0x3f], 101 | flags[0] ? "Z" : "-", flags[1] ? "N" : "-", flags[2] ? "H" : "-", flags[3] ? "C" : "-" 102 | ); 103 | } 104 | #endif 105 | if (*historyIndex == dumpIndicator) 106 | pastIndicator = true; 107 | printf("\n"); 108 | } 109 | printf("\n===============================\n\n"); 110 | 111 | printf("Registers:\n"); 112 | printf("A B C D E H L SP Flags\n"); 113 | printf("%02x %02x %02x %02x %02x %02x %02x %04x %s %s %s %s\n", *a, *b, *c, *d , *e, *h, *l, sp, *Z ? "Z" : "-", *N ? "N" : "-", *H ? "H" : "-", *C ? "C" : "-"); 114 | 115 | printf("\n===============================\n\n"); 116 | 117 | if (error != NULL) 118 | printf("%s\n", error); 119 | if (errorOpcode >= 0) 120 | printf("Opcode: %02x\n", errorOpcode); 121 | } 122 | 123 | #ifdef DEBUG_PPU_TIMING 124 | 125 | void printPPUTiming() { 126 | ppuTimingReady = false; 127 | printf("\n ===============================\n"); 128 | printf("| y | OAM SEARCH | RENDERING |\n"); 129 | printf("|-----|------------|------------|\n"); 130 | for (int line = 0; line < LINES; line++) { 131 | printf("|% 4d |% 4d ..% 4d |% 4d ..% 4d |\n", line, ppuTiming[line][0], ppuTiming[line][1], ppuTiming[line][2], ppuTiming[line][3]); 132 | } 133 | printf(" ===============================\n"); 134 | printf("Frame: %d .. %d => %d (Should be %d), vblank adjusted by %d\n\n", ppuTimingEvents.frameStartCycle, ppuTimingEvents.frameEndCycle, ppuTimingEvents.frameEndCycle - ppuTimingEvents.frameStartCycle, CYCLES_PER_FRAME, ppuTimingEvents.vblankOffset); 135 | } 136 | 137 | #endif 138 | 139 | void dumpMemory() { 140 | //Dump our copy of the memory 141 | bool skipping = false; 142 | for (int baseAddr = 0x8000; baseAddr < 0x010000; baseAddr += 0x10) { 143 | bool skipThis = true; 144 | for (int subAddr = 0x00; subAddr < 0x10; subAddr++) 145 | if (memory[baseAddr | subAddr] != 0) 146 | skipThis = false; 147 | if (skipThis) { 148 | if (!skipping) { 149 | skipping = true; 150 | printf("....\n"); 151 | } 152 | } else { 153 | skipping = false; 154 | printf("%04x ", baseAddr); 155 | for (int subAddr = 0x00; subAddr < 0x10; subAddr++) { 156 | printf("%02x ", memory[baseAddr | subAddr]); 157 | if (subAddr == 0x07) 158 | printf(" "); 159 | } 160 | printf("\n"); 161 | } 162 | } 163 | printf("\n"); 164 | } -------------------------------------------------------------------------------- /firmware/debug.h: -------------------------------------------------------------------------------- 1 | #ifndef GBINTERCEPTOR_DEBUG 2 | #define GBINTERCEPTOR_DEBUG 3 | 4 | #include "pico/stdlib.h" 5 | #include "ppu.h" 6 | 7 | #define DEBUG_EVENTS //Mark events like opcodes or interrupts in the bus history. This is mostly harmless, but disabling it might save a few cycles in a problematic part of a game. 8 | //#define DEBUG_GAME_DETECTION //Periodically outputs the current hashes used for game detection 9 | //#define DEBUG_PPU_TIMING //Periodically measures the timing of PPU rendering and outputs it via USB serial 10 | //#define DEBUG_MEMORY_DUMP //Disables rendering and periodically dumps the emulated memory to USB serial 11 | //#define DEBUG_BREAKPOINT_AT_ADDRESS 0x3926 //Trigger a breakpoint if an opcode at the given address is about to be executed and dump memory and opcode history 12 | //#define DEBUG_BREAKPOINT_AT_ADDRESS_IGNORE 73 //The break at DEBUG_BREAKPOINT_AT_ADDRESS will be ignored n times. 13 | //#define DEBUG_BREAKPOINT_AT_WRITE_TO_ADDRESS 0xc854 //Trigger a breakpoint if data is written to a specific address 14 | //#define DEBUG_BREAKPOINT_AT_WRITE_TO_ADDRESS_IGNORE 3 //The break at DEBUG_BREAKPOINT_AT_WRITE_TO_ADDRESS will be ignored n times. 15 | //#define DEBUG_BREAKPOINT_AT_READ_FROM_ADDRESS 0xa007 //Trigger a breakpoint if data is read from a specific address 16 | //#define DEBUG_BREAKPOINT_AT_READ_FROM_ADDRESS_IGNORE 0 //The break at DEBUG_BREAKPOINT_AT_READ_FROM_ADDRESS will be ignored n times. 17 | //#define DEBUG_LOG_REGISTERS //Log register values in the history, this takes a few cycles from the critical rp2040 core and might cause PIO stall problems. Note, that for some reason I don't understand this messes badly with vsync. 18 | 19 | #ifdef DEBUG_BREAKPOINT_AT_ADDRESS 20 | #define DEBUG_TRIGGER_BREAKPOINT_AT_ADDRESS \ 21 | static uint debug_breakpoint_at_address_counter = DEBUG_BREAKPOINT_AT_ADDRESS_IGNORE; \ 22 | if (*address == DEBUG_BREAKPOINT_AT_ADDRESS) { \ 23 | if (debug_breakpoint_at_address_counter) { \ 24 | debug_breakpoint_at_address_counter--; \ 25 | } else { \ 26 | running = false; \ 27 | error = "Address breakpoint."; \ 28 | } \ 29 | } 30 | #else 31 | #define DEBUG_TRIGGER_BREAKPOINT_AT_ADDRESS 32 | #endif 33 | 34 | #ifdef DEBUG_BREAKPOINT_AT_WRITE_TO_ADDRESS 35 | #define DEBUG_TRIGGER_BREAKPOINT_AT_WRITE_TO_ADDRESS \ 36 | static uint debug_breakpoint_at_write_to_address_counter = DEBUG_BREAKPOINT_AT_WRITE_TO_ADDRESS_IGNORE; \ 37 | if (address == DEBUG_BREAKPOINT_AT_WRITE_TO_ADDRESS) { \ 38 | if (debug_breakpoint_at_write_to_address_counter) { \ 39 | debug_breakpoint_at_write_to_address_counter--; \ 40 | } else { \ 41 | running = false; \ 42 | error = "Write breakpoint."; \ 43 | } \ 44 | } 45 | #else 46 | #define DEBUG_TRIGGER_BREAKPOINT_AT_WRITE_TO_ADDRESS 47 | #endif 48 | 49 | #ifdef DEBUG_BREAKPOINT_AT_READ_FROM_ADDRESS 50 | #define DEBUG_TRIGGER_BREAKPOINT_AT_READ_FROM_ADDRESS \ 51 | static uint debug_breakpoint_at_read_from_address_counter = DEBUG_BREAKPOINT_AT_READ_FROM_ADDRESS_IGNORE; \ 52 | if (*address == DEBUG_BREAKPOINT_AT_READ_FROM_ADDRESS) { \ 53 | if (debug_breakpoint_at_read_from_address_counter) { \ 54 | debug_breakpoint_at_read_from_address_counter--; \ 55 | } else { \ 56 | running = false; \ 57 | error = "Read breakpoint."; \ 58 | } \ 59 | } 60 | #else 61 | #define DEBUG_TRIGGER_BREAKPOINT_AT_READ_FROM_ADDRESS 62 | #endif 63 | 64 | 65 | #ifdef DEBUG_PPU_TIMING 66 | extern bool recordPPUTiming, recordPPUTimingStarted, ppuTimingReady; 67 | 68 | extern uint ppuTiming[LINES][4]; 69 | 70 | struct PPUTimingEvents { 71 | uint frameStartCycle; 72 | uint frameEndCycle; 73 | int vblankOffset; 74 | }; 75 | 76 | extern struct PPUTimingEvents ppuTimingEvents; 77 | 78 | void printPPUTiming(); 79 | 80 | #define DEBUG_MARK_YRESET \ 81 | if (recordPPUTiming) { \ 82 | if (recordPPUTimingStarted) { \ 83 | ppuTimingEvents.frameEndCycle = cycleIndex; \ 84 | recordPPUTiming = false; \ 85 | recordPPUTimingStarted = false; \ 86 | ppuTimingReady = true; \ 87 | } else { \ 88 | recordPPUTimingStarted = true; \ 89 | ppuTimingEvents.vblankOffset = 0; \ 90 | ppuTimingEvents.frameStartCycle = cycleIndex; \ 91 | for (int i = 0; i < LINES; i++) { \ 92 | ppuTiming[i][0] = 0; \ 93 | ppuTiming[i][1] = 0; \ 94 | ppuTiming[i][2] = 0; \ 95 | ppuTiming[i][3] = 0; \ 96 | } \ 97 | } \ 98 | } 99 | 100 | #define DEBUG_MARK_OAMSEARCHSTART \ 101 | if (recordPPUTiming) { \ 102 | ppuTiming[y][0] = lineCycle; \ 103 | } 104 | 105 | #define DEBUG_MARK_OAMSEARCHSTOP \ 106 | if (recordPPUTiming) { \ 107 | if (ppuTiming[y][1] == 0) \ 108 | ppuTiming[y][1] = lineCycle; \ 109 | } 110 | 111 | #define DEBUG_MARK_RENDERSTART \ 112 | if (recordPPUTiming) { \ 113 | ppuTiming[y][2] = lineCycle; \ 114 | } 115 | 116 | #define DEBUG_MARK_RENDERSTOP \ 117 | if (recordPPUTiming) { \ 118 | ppuTiming[y][3] = lineCycle; \ 119 | } 120 | 121 | #define DEBUG_MARK_VBLANK_ADJUST \ 122 | if (recordPPUTiming && ppuTimingEvents.vblankOffset == 0) { \ 123 | ppuTimingEvents.vblankOffset = vblankOffset; \ 124 | } 125 | 126 | #else 127 | 128 | #define DEBUG_MARK_YRESET 129 | #define DEBUG_MARK_OAMSEARCHSTART 130 | #define DEBUG_MARK_OAMSEARCHSTOP 131 | #define DEBUG_MARK_RENDERSTART 132 | #define DEBUG_MARK_RENDERSTOP 133 | #define DEBUG_MARK_VBLANK_ADJUST 134 | 135 | #endif 136 | 137 | #ifdef DEBUG_LOG_REGISTERS 138 | extern uint32_t volatile registerHistory32[64][2]; //We only log the last 64 events to save memory 139 | extern uint16_t volatile spHistory[64]; 140 | extern uint32_t volatile flagHistory[64]; 141 | #define DEBUG_TRIGGER_LOG_REGISTERS \ 142 | registerHistory32[*historyIndex & 0x3f][0] = *((uint32_t *)(®isters[0])); \ 143 | registerHistory32[*historyIndex & 0x3f][1] = *((uint32_t *)(®isters[4])); \ 144 | spHistory[*historyIndex & 0x3f] = sp; \ 145 | flagHistory[*historyIndex & 0x3f] = flags; 146 | 147 | #else 148 | 149 | #define DEBUG_TRIGGER_LOG_REGISTERS 150 | 151 | #endif 152 | 153 | void dumpMemory(); 154 | void dumpBus(); 155 | 156 | #endif -------------------------------------------------------------------------------- /firmware/font/font8x8_basic.h: -------------------------------------------------------------------------------- 1 | /** 2 | * 8x8 monochrome bitmap fonts for rendering 3 | * Author: Daniel Hepper 4 | * 5 | * License: Public Domain 6 | * 7 | * Based on: 8 | * // Summary: font8x8.h 9 | * // 8x8 monochrome bitmap fonts for rendering 10 | * // 11 | * // Author: 12 | * // Marcel Sondaar 13 | * // International Business Machines (public domain VGA fonts) 14 | * // 15 | * // License: 16 | * // Public Domain 17 | * 18 | * Fetched from: http://dimensionalrift.homelinux.net/combuster/mos3/?p=viewsource&file=/modules/gfx/font8_8.asm 19 | **/ 20 | 21 | // Constant: font8x8_basic 22 | // Contains an 8x8 font map for unicode points U+0000 - U+007F (basic latin) 23 | char font8x8_basic[128][8] = { 24 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0000 (nul) 25 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0001 26 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0002 27 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0003 28 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0004 29 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0005 30 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0006 31 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0007 32 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0008 33 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0009 34 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000A 35 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000B 36 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000C 37 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000D 38 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000E 39 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000F 40 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0010 41 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0011 42 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0012 43 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0013 44 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0014 45 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0015 46 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0016 47 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0017 48 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0018 49 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0019 50 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001A 51 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001B 52 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001C 53 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001D 54 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001E 55 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001F 56 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0020 (space) 57 | { 0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00}, // U+0021 (!) 58 | { 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0022 (") 59 | { 0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00}, // U+0023 (#) 60 | { 0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00}, // U+0024 ($) 61 | { 0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00}, // U+0025 (%) 62 | { 0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00}, // U+0026 (&) 63 | { 0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0027 (') 64 | { 0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00}, // U+0028 (() 65 | { 0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00}, // U+0029 ()) 66 | { 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00}, // U+002A (*) 67 | { 0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00}, // U+002B (+) 68 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // U+002C (,) 69 | { 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00}, // U+002D (-) 70 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // U+002E (.) 71 | { 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00}, // U+002F (/) 72 | { 0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00}, // U+0030 (0) 73 | { 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00}, // U+0031 (1) 74 | { 0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00}, // U+0032 (2) 75 | { 0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00}, // U+0033 (3) 76 | { 0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00}, // U+0034 (4) 77 | { 0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00}, // U+0035 (5) 78 | { 0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00}, // U+0036 (6) 79 | { 0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00}, // U+0037 (7) 80 | { 0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00}, // U+0038 (8) 81 | { 0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00}, // U+0039 (9) 82 | { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // U+003A (:) 83 | { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // U+003B (;) 84 | { 0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00}, // U+003C (<) 85 | { 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00}, // U+003D (=) 86 | { 0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00}, // U+003E (>) 87 | { 0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00}, // U+003F (?) 88 | { 0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00}, // U+0040 (@) 89 | { 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00}, // U+0041 (A) 90 | { 0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00}, // U+0042 (B) 91 | { 0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00}, // U+0043 (C) 92 | { 0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00}, // U+0044 (D) 93 | { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00}, // U+0045 (E) 94 | { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00}, // U+0046 (F) 95 | { 0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00}, // U+0047 (G) 96 | { 0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00}, // U+0048 (H) 97 | { 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0049 (I) 98 | { 0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00}, // U+004A (J) 99 | { 0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00}, // U+004B (K) 100 | { 0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00}, // U+004C (L) 101 | { 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00}, // U+004D (M) 102 | { 0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00}, // U+004E (N) 103 | { 0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00}, // U+004F (O) 104 | { 0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00}, // U+0050 (P) 105 | { 0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00}, // U+0051 (Q) 106 | { 0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00}, // U+0052 (R) 107 | { 0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00}, // U+0053 (S) 108 | { 0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0054 (T) 109 | { 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00}, // U+0055 (U) 110 | { 0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0056 (V) 111 | { 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00}, // U+0057 (W) 112 | { 0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00}, // U+0058 (X) 113 | { 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00}, // U+0059 (Y) 114 | { 0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00}, // U+005A (Z) 115 | { 0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00}, // U+005B ([) 116 | { 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00}, // U+005C (\) 117 | { 0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00}, // U+005D (]) 118 | { 0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00}, // U+005E (^) 119 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF}, // U+005F (_) 120 | { 0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0060 (`) 121 | { 0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00}, // U+0061 (a) 122 | { 0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00}, // U+0062 (b) 123 | { 0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00}, // U+0063 (c) 124 | { 0x38, 0x30, 0x30, 0x3e, 0x33, 0x33, 0x6E, 0x00}, // U+0064 (d) 125 | { 0x00, 0x00, 0x1E, 0x33, 0x3f, 0x03, 0x1E, 0x00}, // U+0065 (e) 126 | { 0x1C, 0x36, 0x06, 0x0f, 0x06, 0x06, 0x0F, 0x00}, // U+0066 (f) 127 | { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0067 (g) 128 | { 0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00}, // U+0068 (h) 129 | { 0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0069 (i) 130 | { 0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E}, // U+006A (j) 131 | { 0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00}, // U+006B (k) 132 | { 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+006C (l) 133 | { 0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00}, // U+006D (m) 134 | { 0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00}, // U+006E (n) 135 | { 0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00}, // U+006F (o) 136 | { 0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F}, // U+0070 (p) 137 | { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78}, // U+0071 (q) 138 | { 0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00}, // U+0072 (r) 139 | { 0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00}, // U+0073 (s) 140 | { 0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00}, // U+0074 (t) 141 | { 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00}, // U+0075 (u) 142 | { 0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0076 (v) 143 | { 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00}, // U+0077 (w) 144 | { 0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00}, // U+0078 (x) 145 | { 0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0079 (y) 146 | { 0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00}, // U+007A (z) 147 | { 0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00}, // U+007B ({) 148 | { 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, // U+007C (|) 149 | { 0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00}, // U+007D (}) 150 | { 0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+007E (~) 151 | { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // U+007F 152 | }; 153 | -------------------------------------------------------------------------------- /firmware/gamedb/build_games_h.py: -------------------------------------------------------------------------------- 1 | ### 2 | # This Python script generates the content of games.h. 3 | # Do not edit games.h directly, but change games.csv and run this script to 4 | # update games.h (on Linux systems just redirect the output to games.h) 5 | # The reason for not editing games.h directly is that the data has to be 6 | # sorted by vramHash2, the precompiler define for the list length has to be 7 | # set correctly and the entries of gameInfosDirectory have to point at the 8 | # right indices. 9 | ### 10 | 11 | import csv 12 | import re 13 | import sys 14 | 15 | gameInfos = [] 16 | gameInfosDirectory = [] 17 | maxBranchBasedFixCount = 0 18 | maxRegisterDuringDMACount = 0 19 | 20 | with open('games.csv') as csvfile: 21 | reader = csv.DictReader(csvfile, delimiter=',', quotechar='"', skipinitialspace=True) 22 | currentDirectoryIndex = -1 23 | for row in sorted(reader, key=lambda d: d["vramHash2"]): 24 | hashIndex = (int(row["vramHash2"], 16) >> 24) & 0xff 25 | while hashIndex > currentDirectoryIndex: 26 | gameInfosDirectory.append(len(gameInfos)) 27 | currentDirectoryIndex += 1 28 | gameInfo = {} 29 | gameInfo["vramHash1"] = row["vramHash1"] 30 | gameInfo["vramHash2"] = row["vramHash2"] 31 | gameInfo["title"] = row["title"] 32 | gameInfo["branchBasedFixes"] = [] 33 | if row["fixes"] != "": 34 | for fix in row["fixes"].split("|"): 35 | parts = re.search("\s*([a-zA-Z]+)\((.+)\)\s*", fix) 36 | if parts == None: 37 | print("Parse error: " + fix, file=sys.stderr) 38 | if parts[1].lower() == "dmaFix".lower(): 39 | gameInfo["dmaFix"] = parts[2] 40 | elif parts[1].lower() == "useImmediateIRQ".lower(): 41 | gameInfo["useImmediateIRQ"] = parts[2] 42 | elif parts[1].lower() == "disableStatSyncs".lower(): 43 | gameInfo["disableStatSyncs"] = parts[2] 44 | elif parts[1].lower() == "disableLySyncs".lower(): 45 | gameInfo["disableLySyncs"] = parts[2] 46 | elif parts[1].lower() == "windowLineAlwaysPauses".lower(): 47 | gameInfo["windowLineAlwaysPauses"] = parts[2] 48 | elif parts[1].lower() == "branchBasedFix".lower(): 49 | branchBasedFixParameters = parts[2].split(",") 50 | branchBasedFix = {} 51 | branchBasedFix["jumpAddress"] = branchBasedFixParameters[0].strip() 52 | branchBasedFix["fixTarget"] = branchBasedFixParameters[1].strip() 53 | branchBasedFix["takenMethod"] = branchBasedFixParameters[2].strip() 54 | branchBasedFix["takenValue"] = branchBasedFixParameters[3].strip() 55 | branchBasedFix["notTakenMethod"] = branchBasedFixParameters[4].strip() 56 | branchBasedFix["notTakenValue"] = branchBasedFixParameters[5].strip() 57 | gameInfo["branchBasedFixes"].append(branchBasedFix) 58 | if len(gameInfo["branchBasedFixes"]) > maxBranchBasedFixCount: 59 | maxBranchBasedFixCount = len(gameInfo["branchBasedFixes"]) 60 | elif parts[1].lower() == "writeRegistersDuringDMA".lower(): 61 | registerDuringDMAParameters = parts[2].split(",") 62 | if len(registerDuringDMAParameters) > maxRegisterDuringDMACount: 63 | maxRegisterDuringDMACount = len(registerDuringDMAParameters) 64 | gameInfo["writeRegistersDuringDMA"] = parts[2] 65 | else: 66 | print("Unknown fix: " + fix, file=sys.stderr) 67 | gameInfo["comment"] = row["comment"] 68 | gameInfos.append(gameInfo) 69 | 70 | while 256 > currentDirectoryIndex: 71 | gameInfosDirectory.append(len(gameInfos)) 72 | currentDirectoryIndex += 1 73 | 74 | print("// Do not edit this file directly!") 75 | print("// Edit games.csv instead and use the Python script build_games_h.py to regenerate games.c."); 76 | print("") 77 | print("#ifndef GBINTERCEPTOR_GAMES") 78 | print("#define GBINTERCEPTOR_GAMES") 79 | print("#define GAME_LIST_SIZE " + str(len(gameInfos))) 80 | print("") 81 | print("GameInfo __in_flash(\"games\") gameInfos[GAME_LIST_SIZE] = {") 82 | for gameInfo in gameInfos: 83 | print(" {", end="") 84 | print(".vramHash1 = " + gameInfo["vramHash1"] + ", ", end="") 85 | print(".vramHash2 = " + gameInfo["vramHash2"] + ", ", end="") 86 | print(".dmaFix = " + gameInfo.get("dmaFix", "0x0000") + ", ", end="") 87 | print(".useImmediateIRQ = " + gameInfo.get("useImmediateIRQ", "false") + ", ", end="") 88 | print(".disableStatSyncs = " + gameInfo.get("disableStatSyncs", "false") + ", ", end="") 89 | print(".disableLySyncs = " + gameInfo.get("disableLySyncs", "false") + ", ", end="") 90 | print(".windowLineAlwaysPauses = " + gameInfo.get("windowLineAlwaysPauses", "false") + ", ", end="") 91 | print(".branchBasedFixes = {", end="") 92 | for branchBasedFix in gameInfo["branchBasedFixes"]: 93 | print("{", end="") 94 | print(".jumpAddress = " + branchBasedFix["jumpAddress"] + ", ", end="") 95 | print(".fixTarget = " + branchBasedFix["fixTarget"] + ", ", end="") 96 | print(".takenMethod = " + branchBasedFix["takenMethod"] + ", ", end="") 97 | print(".takenValue = " + branchBasedFix["takenValue"] + ", ", end="") 98 | print(".notTakenMethod = " + branchBasedFix["notTakenMethod"] + ", ", end="") 99 | print(".notTakenValue = " + branchBasedFix["notTakenValue"], end="") 100 | print("}, ", end="") 101 | print("}, ", end="") 102 | print(".writeRegistersDuringDMA = {" + gameInfo.get("writeRegistersDuringDMA", "") + "}, ", end="") 103 | print(".title = \"" + gameInfo["title"] + "\", " + " "*(18-len(gameInfo["title"])), end="") 104 | print("}, // " + gameInfo["comment"]) 105 | 106 | print("};") 107 | print("") 108 | print("uint16_t gameInfoDirectory[257] = {") 109 | print(" " + ", ".join(map(str, gameInfosDirectory))) 110 | print("};") 111 | print("#endif") 112 | print("All done. Make sure that BRANCH_BASED_FIX_LIST_SIZE is at least " + str(maxBranchBasedFixCount) + " and DMA_REGISTER_MAP_SIZE is at least " + str(maxRegisterDuringDMACount) + " in game_detection.h.", file=sys.stderr) 113 | 114 | -------------------------------------------------------------------------------- /firmware/gamedb/game_detection.c: -------------------------------------------------------------------------------- 1 | #include "game_detection.h" 2 | #include "games.h" 3 | 4 | #include 5 | 6 | #include "debug.h" 7 | 8 | volatile uint vramHash1, vramHash2; 9 | 10 | GameInfo gameInfo; 11 | volatile bool gameDetected = false; 12 | 13 | void resetHashes() { 14 | gameDetected = false; 15 | gameInfo.dmaFix = 0x0000; 16 | gameInfo.useImmediateIRQ = false; 17 | gameInfo.disableStatSyncs = false; 18 | gameInfo.disableLySyncs = false; 19 | gameInfo.windowLineAlwaysPauses = false; 20 | gameInfo.branchBasedFixes[0].jumpAddress = 0x0000; 21 | gameInfo.writeRegistersDuringDMA[0] = 0x00; 22 | 23 | vramHash1 = 0; 24 | vramHash2 = 0; 25 | } 26 | 27 | bool detectGame() { 28 | #if defined(DEBUG_GAME_DETECTION) 29 | printf("Hashes: VRAM Hash1=0x%08x Hash2=0x%08x\n", vramHash1, vramHash2); 30 | #endif 31 | uint start = gameInfoDirectory[vramHash2 >> 24]; 32 | uint end = gameInfoDirectory[(vramHash2 >> 24) + 1]; 33 | while (start < end) { 34 | uint mid = start + (end - start) / 2; 35 | if (gameInfos[mid].vramHash2 == vramHash2) { 36 | if (gameInfos[mid].vramHash1 == vramHash1) { 37 | gameDetected = true; 38 | gameInfo = gameInfos[mid]; 39 | printf("Detected %s\n", gameInfo.title); 40 | return true; 41 | } else if (gameInfos[mid].vramHash1 < vramHash1) { 42 | start = mid + 1; 43 | } else { 44 | end = mid; 45 | } 46 | } else if (gameInfos[mid].vramHash2 < vramHash2) { 47 | start = mid + 1; 48 | } else { 49 | end = mid; 50 | } 51 | } 52 | return false; 53 | } 54 | -------------------------------------------------------------------------------- /firmware/gamedb/game_detection.h: -------------------------------------------------------------------------------- 1 | #ifndef GBINTERCEPTOR_GAME_DETECTION 2 | #define GBINTERCEPTOR_GAME_DETECTION 3 | 4 | #include "pico/stdlib.h" 5 | 6 | #define BRANCH_BASED_FIX_LIST_SIZE 3 7 | #define DMA_REGISTER_MAP_SIZE 20 8 | 9 | typedef enum {nop, set, and, or, xor, sync} FixMethod; 10 | 11 | typedef struct { 12 | uint16_t jumpAddress; 13 | uint16_t fixTarget; 14 | FixMethod takenMethod; 15 | uint8_t takenValue; 16 | FixMethod notTakenMethod; 17 | uint8_t notTakenValue; 18 | } BranchBasedFix; 19 | 20 | typedef struct { 21 | uint vramHash1, vramHash2; 22 | uint16_t dmaFix; // Address that recognizes return after DMA (if not 0x0000) 23 | bool useImmediateIRQ; //Use vblank IRQ to sync the PPU even if it occured immediately after enabling interrupts, so it might have been delayed. 24 | bool disableStatSyncs; //Do not use stat register related tight loops for sync 25 | bool disableLySyncs; //Do not use stat register related tight loops for sync 26 | bool windowLineAlwaysPauses; //Used if window is disabled so close to the y=0 reset that we might miss that it has been enabled. In this case its internal counter still has to be initialized to zero so that its line counter actually pauses until the window is enabled again 27 | BranchBasedFix branchBasedFixes[BRANCH_BASED_FIX_LIST_SIZE]; //List of memory addresses of conditional jumps and how their branching behavior should set values in memory 28 | uint8_t writeRegistersDuringDMA[DMA_REGISTER_MAP_SIZE]; //Sequence of HRAM/IO addresses. Write the first to the second, the third to the fourth etc. during DMA 29 | char title[19]; 30 | } GameInfo; 31 | 32 | extern volatile uint vramHash1, vramHash2; 33 | extern GameInfo gameInfo; 34 | extern volatile bool gameDetected; 35 | 36 | void resetHashes(); 37 | bool detectGame(); 38 | 39 | #define VRAM_HASH(ADDR, DATA) \ 40 | if (memory[ADDR] != DATA) { \ 41 | vramHash1 += ((ADDR << 8) | DATA); \ 42 | vramHash2 += vramHash1; \ 43 | } 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /firmware/jpeg/base.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Staacks/gbinterceptor/14213d507fb056389c3be0bd35f8b57b359a52ea/firmware/jpeg/base.jpg -------------------------------------------------------------------------------- /firmware/jpeg/base_no_chroma.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Staacks/gbinterceptor/14213d507fb056389c3be0bd35f8b57b359a52ea/firmware/jpeg/base_no_chroma.jpg -------------------------------------------------------------------------------- /firmware/jpeg/generateBaseJpeg.py: -------------------------------------------------------------------------------- 1 | #This is a simple Python script that creates a JPEG file in the desired format and creates a header file with the data. 2 | #Note that I do not expect any changes here, so this is mostly here for reference and not properly automated. 3 | #If you change anything, you will very likely need to change offsets in the Pico code. 4 | 5 | def generateSOI(): 6 | return bytearray([0xff, 0xd8]) 7 | 8 | def generateAPP0(): 9 | data = bytearray([0xff, 0xe0]) 10 | data.extend([0x00, 0x10]) #length 16 11 | data.extend(b"JFIF\x00") #JFIF 12 | data.extend([0x01, 0x01]) #Version 1.1 13 | data.extend([0x01]) #Units DPI 14 | data.extend([0x00, 0x48, 0x00, 0x48]) #Density 72x72 15 | data.extend([0x00, 0x00]) #Thumbnail 0x0 16 | return data 17 | 18 | def generateAPP0NoChroma(): 19 | data = bytearray([0xff, 0xe0]) 20 | data.extend([0x00, 0x11]) #length 17 21 | data.extend(b"JFIF\x00") #JFIF 22 | data.extend([0x01, 0x01]) #Version 1.1 23 | data.extend([0x01]) #Units DPI 24 | data.extend([0x00, 0x48, 0x00, 0x48]) #Density 72x72 25 | data.extend([0x00, 0x00]) #Thumbnail 0x0 26 | data.extend([0x00]) #No meaning, we just need to pad it, so that the data block aligns with a multiple of 32bit 27 | return data 28 | 29 | def generateDQT(destination): 30 | data = bytearray([0xff, 0xdb]) 31 | data.extend([0x00, 0x43]) #Length 67 32 | data.extend([destination]) #destination 33 | data.extend([0xff]*64) #QUantization table entirely filled with 0xff 34 | return data 35 | 36 | def generateSOF(): 37 | data = bytearray([0xff, 0xc0]) 38 | data.extend([0x00, 0x11]) #Length 17 39 | data.extend([0x08]) #Precision 40 | data.extend([0x04, 0x80]) #height 8*144 41 | data.extend([0x05, 0x00]) #width 8*160 42 | data.extend([0x03]) #Components 43 | data.extend([0x01, 0x22, 0x00]) #Luminance channel setup 44 | data.extend([0x02, 0x11, 0x00]) #Chrominance Cb channel setup 45 | data.extend([0x03, 0x11, 0x00]) #Chrominance Cr channel setup 46 | return data 47 | 48 | def generateSOFNoChroma(): 49 | data = bytearray([0xff, 0xc0]) 50 | data.extend([0x00, 0x0b]) #Length 11 51 | data.extend([0x08]) #Precision 52 | data.extend([0x04, 0x80]) #height 8*144 53 | data.extend([0x05, 0x00]) #width 8*160 54 | data.extend([0x01]) #Components 55 | data.extend([0x01, 0x22, 0x00]) #Luminance channel setup 56 | return data 57 | 58 | def generateDHT_DC(): 59 | data = bytearray([0xff, 0xc4]) 60 | data.extend([0x00, 0x17]) #Length 23 61 | data.extend([0x00]) #DC Huffman table, destination 0 62 | 63 | #Huffman table 64 | #0 -> 0x03 65 | #10 -> 0x02 66 | #110 -> 0x01 67 | #1110 -> 0x00 68 | data.extend([0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x01, 0x00]) 69 | 70 | return data 71 | 72 | def generateDHT_DC_chrominance(): 73 | data = bytearray([0xff, 0xc4]) 74 | data.extend([0x00, 0x15]) #Length 21 75 | data.extend([0x01]) #DC Huffman table, destination 1 76 | 77 | #Huffman table 78 | #0 -> 0x00 79 | #10 -> 0x01 80 | data.extend([0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]) 81 | 82 | return data 83 | 84 | def generateDHT_AC(): 85 | data = bytearray([0xff, 0xc4]) 86 | data.extend([0x00, 0x14]) #Length 20 87 | data.extend([0x10]) #AC Huffman table, destination 0 88 | 89 | #Huffman table 90 | data.extend([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) 91 | 92 | return data 93 | 94 | def generateSOS(): 95 | data = bytearray([0xff, 0xda]) 96 | data.extend([0x00, 0x08]) #Length 8 97 | data.extend([0x01]) #Components: 1 98 | data.extend([0x01, 0x00]) #Component one uses tables 0/0 99 | data.extend([0x00, 0x3f]) #Spectral select 100 | data.extend([0x00]) #successive approx 101 | return data 102 | 103 | def generateSOS_chrominance(): 104 | data = bytearray([0xff, 0xda]) 105 | data.extend([0x00, 0x0a]) #Length 10 106 | data.extend([0x02]) #Components: 2 107 | data.extend([0x02, 0x10]) #Component two uses tables 1/0 108 | data.extend([0x03, 0x10]) #Component three uses tables 1/0 109 | data.extend([0x00, 0x3f]) #Spectral select 110 | data.extend([0x00]) #successive approx 111 | return data 112 | 113 | def encodeVal(v): 114 | lookup = [0b00000, 0b00010, 0b00100, 0b00110, 0b10000, 0b10010, 0b11000, 0b11100, 0b11010, 0b10100, 0b10110, 0b01000, 0b01010, 0b01100, 0b01110] 115 | return lookup[v+7] 116 | 117 | def generateData(): 118 | last = 0 119 | pixelData = [0x00]*(160*144*5//8) 120 | for y in range(144): 121 | for x in range(160): 122 | lum = -3 if (x % 8 == 0 or y % 8 == 0) else -3+((x + y) % 8) 123 | diff = lum - last 124 | last = lum 125 | code = encodeVal(diff) 126 | offset = 5*(160*y+x) 127 | byteOffset = offset // 8 128 | bitOffset = offset % 8 129 | if bitOffset <= 3: 130 | pixelData[byteOffset] |= (code << (3-bitOffset)) 131 | else: 132 | pixelData[byteOffset] |= (code >> (bitOffset-3)) 133 | pixelData[byteOffset+1] |= ((code << (11-bitOffset)) & 0xff) 134 | return pixelData 135 | 136 | def generateData_chrominance(): 137 | last = 0 138 | pixelData = [0x00]*(160*144*2*2//8//4+1) #160 * 144 pixels, 2 channels, one bit for DC and AC entry each, 8 bit per byte, 4 pixels per entry due to undersampling plus 1 byte that is not always used, but leaves headroom to set a DC offset in the beginning for green color mode 139 | #pixelData[0] = 0b10001000 #Set entire DC offset to make the image green 140 | return pixelData 141 | 142 | def generateEOI(): 143 | return bytearray([0xff, 0xd9]) 144 | 145 | def baseJpeg(): 146 | data = bytearray() 147 | data.extend(generateSOI()) 148 | data.extend(generateAPP0()) 149 | data.extend(generateDQT(0x00)) #DQT 150 | data.extend(generateSOF()) 151 | data.extend(generateDHT_DC()) 152 | data.extend(generateDHT_DC_chrominance()) 153 | data.extend(generateDHT_AC()) 154 | data.extend(generateSOS()) 155 | data.extend(generateData()) 156 | data.extend(generateSOS_chrominance()) 157 | data.extend(generateData_chrominance()) 158 | data.extend(generateEOI()) 159 | return data 160 | 161 | def baseJpegNoChroma(): 162 | data = bytearray() 163 | data.extend(generateSOI()) 164 | data.extend(generateAPP0NoChroma()) 165 | data.extend(generateDQT(0x00)) #DQT 166 | data.extend(generateSOFNoChroma()) 167 | data.extend(generateDHT_DC()) 168 | data.extend(generateDHT_AC()) 169 | data.extend(generateSOS()) 170 | data.extend(generateData()) 171 | data.extend(generateEOI()) 172 | return data 173 | 174 | def generateFiles(jpg, h, var, data): 175 | with open(jpg, "wb") as f: 176 | f.write(data) 177 | 178 | with open(h, "w") as f: 179 | f.write("unsigned char __in_flash(\"jpeg\") " + var + "[] = {") 180 | for i in range(len(data)): 181 | if i % 16 == 0: 182 | f.write("\n") 183 | f.write(' 0x{:02x},'.format(data[i])) 184 | f.write("\n};\n") 185 | 186 | generateFiles("base.jpg", "base_jpeg.h", "base_jpeg", baseJpeg()) 187 | generateFiles("base_no_chroma.jpg", "base_jpeg_no_chroma.h", "base_jpeg_no_chroma", baseJpegNoChroma()) 188 | -------------------------------------------------------------------------------- /firmware/jpeg/jpeg.h: -------------------------------------------------------------------------------- 1 | #ifndef GBINTERCEPTOR_JPEG 2 | #define GBINTERCEPTOR_JPEG 3 | 4 | #include "pico/stdlib.h" 5 | #include "ppu.h" 6 | 7 | #define JPEG_DATA_SIZE (SCREEN_SIZE * 5 / 8) //5bit per pixel, see https://github.com/Staacks/gbinterceptor/issues/17 8 | #define JPEG_HEADER_SIZE 188 9 | #define JPEG_HEADER_SIZE_NO_CHROMA 160 10 | #define JPEG_END_SIZE 2895 11 | #define JPEG_END_SIZE_NO_CHROMA 2 12 | #define FRAME_SIZE (JPEG_DATA_SIZE + JPEG_HEADER_SIZE + JPEG_END_SIZE) 13 | #define FRAME_SIZE_NO_CHROMA (JPEG_DATA_SIZE + JPEG_HEADER_SIZE_NO_CHROMA + JPEG_END_SIZE_NO_CHROMA) 14 | 15 | #define JPEG_CHROMA_OFFSET (JPEG_HEADER_SIZE + JPEG_DATA_SIZE + 12) 16 | 17 | void prepareJpegEncoding(); 18 | void startBackbufferToJPEG(bool allowFrameBlend); 19 | void continueBackbufferToJPEG(); 20 | 21 | #endif -------------------------------------------------------------------------------- /firmware/jpeg/jpeg_encoding.pio: -------------------------------------------------------------------------------- 1 | .program jpegEncoding 2 | 3 | //The JPEG data uses a Huffman table that is designed such that every pixel can be 4 | //encoded in 5 bit (see https://github.com/Staacks/gbinterceptor/issues/17). This SM 5 | //takes bytes that contain differential values that have already been prepared to be 6 | //easily convertible to the Huffman encoding. 7 | 8 | //This is the second step in the encoding process: 9 | // jpeg_prepare -> jpeg_encoding 10 | 11 | //-7 => into SM: 0b0000 => encoded output: 0b 0 000 0 12 | //-6 => into SM: 0b0001 => encoded output: 0b 0 001 0 13 | //-5 => into SM: 0b0010 => encoded output: 0b 0 010 0 14 | //-4 => into SM: 0b0011 => encoded output: 0b 0 011 0 15 | //-3 => into SM: 0b0100 => encoded output: 0b 10 00 0 16 | //-2 => into SM: 0b0101 => encoded output: 0b 10 01 0 17 | //-1 => into SM: 0b0110 => encoded output: 0b 110 0 0 18 | 19 | // 0 => into SM: 0b1000 => encoded output: 0b 1110 0 20 | // 1 => into SM: 0b1001 => encoded output: 0b 110 1 0 21 | // 2 => into SM: 0b1010 => encoded output: 0b 10 10 0 22 | // 3 => into SM: 0b1011 => encoded output: 0b 10 11 0 23 | // 4 => into SM: 0b1100 => encoded output: 0b 0 100 0 24 | // 5 => into SM: 0b1101 => encoded output: 0b 0 101 0 25 | // 6 => into SM: 0b1110 => encoded output: 0b 0 110 0 26 | // 7 => into SM: 0b1111 => encoded output: 0b 0 111 0 27 | 28 | .wrap_target 29 | start: 30 | SET y 2 // Counter for remaining bits (-1) 31 | 32 | OUT x 1 //0: negative number, 1: positive number 33 | JMP !x negative 34 | 35 | ///POSITIVE/// 36 | positive: //position of first one determines Huffman code 37 | 38 | codeP: 39 | OUT x 1 40 | JMP !x morecodeP 41 | IN null 1 42 | IN x 1 43 | JMP y-- remainder 44 | JMP end 45 | 46 | morecodeP: 47 | SET x 1 48 | IN x 1 49 | JMP y-- codeP 50 | IN null 1 51 | JMP end 52 | 53 | 54 | 55 | ///NEGATIVE/// 56 | negative: //position of first zero determines Huffman code 57 | 58 | codeN: 59 | OUT x 1 60 | IN x 1 61 | JMP !x codeDoneN 62 | JMP y-- codeN 63 | 64 | codeDoneN: 65 | IN null 1 66 | JMP y-- remainder 67 | JMP end 68 | 69 | 70 | ///REMAINDER/// 71 | 72 | remainder: //Remaining bits can just be added once the Huffman code has been chosen. 73 | OUT x 1 74 | IN x 1 75 | JMP y-- remainder 76 | 77 | ///END/// 78 | end: 79 | IN null 1 80 | 81 | .wrap 82 | 83 | % c-sdk { 84 | 85 | void jpegEncoding_program_init(PIO pio, uint sm, uint offset) { 86 | pio_sm_config c = jpegEncoding_program_get_default_config(offset); 87 | sm_config_set_clkdiv(&c, 1); 88 | 89 | sm_config_set_in_shift(&c, false, true, 8); 90 | sm_config_set_out_shift(&c, false, true, 32); 91 | 92 | pio_sm_init(pio, sm, offset, &c); 93 | pio_sm_set_enabled(pio, sm, true); 94 | } 95 | 96 | %} 97 | -------------------------------------------------------------------------------- /firmware/jpeg/jpeg_prepare.pio: -------------------------------------------------------------------------------- 1 | .program jpegPrepare 2 | 3 | //The JPEG data uses a Huffman table that is designed such that every pixel can be 4 | //encoded in 5 bit (see https://github.com/Staacks/gbinterceptor/issues/17). This SM 5 | //takes the differences plus eight from the CPU and subtracts one from the negative 6 | //values for easier encoding in the next step. 7 | 8 | //This is the first step in the encoding process: 9 | // jpeg_prepare -> jpeg_encoding 10 | 11 | //-7 => into SM: 0b00000001 => encoded output: 0b0000 12 | //-6 => into SM: 0b00000010 => encoded output: 0b0001 13 | //-5 => into SM: 0b00000011 => encoded output: 0b0010 14 | //-4 => into SM: 0b00000100 => encoded output: 0b0011 15 | //-3 => into SM: 0b00000101 => encoded output: 0b0100 16 | //-2 => into SM: 0b00000110 => encoded output: 0b0101 17 | //-1 => into SM: 0b00000111 => encoded output: 0b0110 18 | // Note that there is no 0b00000111 as output and no 0b00000000 as input, so we do not even have to treat edge cases 19 | // 0 => into SM: 0b00001000 => encoded output: 0b1000 20 | // 1 => into SM: 0b00001001 => encoded output: 0b1001 21 | // 2 => into SM: 0b00001010 => encoded output: 0b1010 22 | // 3 => into SM: 0b00001011 => encoded output: 0b1011 23 | // 4 => into SM: 0b00001100 => encoded output: 0b1100 24 | // 5 => into SM: 0b00001101 => encoded output: 0b1101 25 | // 6 => into SM: 0b00001110 => encoded output: 0b1110 26 | // 7 => into SM: 0b00001111 => encoded output: 0b1111 27 | 28 | .wrap_target 29 | start: 30 | //Incoming data is shifted to the RIGHT to counter endianess. Therefore... 31 | OUT y 3 //...we read the three least significant bits first 32 | OUT x 1 //...then read the sign... 33 | OUT null 4 //...and discard the zeros last. 34 | 35 | //Output happens in reverse order... 36 | IN x 1 //...by sending the sign first... 37 | 38 | JMP !x changed //...and then deciding if the... 39 | 40 | unchanged: 41 | JMP done //...least significant bits remain unchanged... 42 | 43 | changed: 44 | JMP y-- done //...or need to be decremented before... 45 | 46 | done: 47 | IN y 3 //...sending them. 48 | 49 | .wrap 50 | 51 | % c-sdk { 52 | 53 | void jpegPrepare_program_init(PIO pio, uint sm, uint offset) { 54 | pio_sm_config c = jpegPrepare_program_get_default_config(offset); 55 | sm_config_set_clkdiv(&c, 1); 56 | 57 | sm_config_set_in_shift(&c, false, true, 32); 58 | sm_config_set_out_shift(&c, true, true, 32); 59 | 60 | pio_sm_init(pio, sm, offset, &c); 61 | pio_sm_set_enabled(pio, sm, true); 62 | } 63 | 64 | %} 65 | -------------------------------------------------------------------------------- /firmware/main.h: -------------------------------------------------------------------------------- 1 | #ifndef GBINTERCEPTOR_MAIN 2 | #define GBINTERCEPTOR_MAIN 3 | 4 | #define GBSENSE_PIN 0 5 | 6 | #define LED_SWITCH_PIN 1 7 | #define LED_PIN_MASK 0x02 8 | 9 | #define BASEVERSION "1.2.0" 10 | 11 | #ifdef BASE_VIDEO_MODE 12 | #define VERSION BASEVERSION "B" 13 | #else 14 | #define VERSION BASEVERSION 15 | #endif 16 | 17 | //On-screen display 18 | #define MODE_INFO_DURATION 100 //Duration of the mode info in frames 19 | #define GAME_DETECTED_INFO_DURATION 200 //Duration of the mode info in frames 20 | 21 | #endif -------------------------------------------------------------------------------- /firmware/memory-bus.pio: -------------------------------------------------------------------------------- 1 | .program memoryBus 2 | 3 | .wrap_target 4 | wait 1 pin 28 ;As we set GPIO6 as pin0, CLK can be found on GPIO 2 which is now wrapped around to pin 28 5 | wait 0 pin 28 ;Wait for falling flank of CLK 6 | mov isr pins 7 | push 8 | .wrap 9 | 10 | % c-sdk { 11 | 12 | void memoryBus_program_init(PIO pio, uint sm, uint offset, float div) { 13 | pio_sm_config c = memoryBus_program_get_default_config(offset); 14 | sm_config_set_clkdiv(&c, div); //Clock 15 | 16 | //GPIO setup 17 | //See schematic: Pin 2 is CLK, followed by RD/WR/CS, then 16bit address and 8bit data 18 | //However, since we read all pins as 32bit, we want to allign the address to pin 6, so we get 19 | //0x0000ffff as address 20 | //0x00ff0000 as data 21 | //0xf0000000 containing the bits for CLK (should be 0 when reading), nWR, nRD and nCS 22 | //0x0f000000 containing garbage (our status LEDs and virtual padding GPIOs) 23 | 24 | sm_config_set_in_pins(&c, 6); 25 | pio_sm_set_consecutive_pindirs(pio, sm, 2, 28, false); 26 | 27 | sm_config_set_in_shift(&c, true, false, 32); 28 | 29 | pio_sm_init(pio, sm, offset, &c); 30 | pio_sm_set_enabled(pio, sm, true); 31 | } 32 | 33 | %} -------------------------------------------------------------------------------- /firmware/opcodes.h: -------------------------------------------------------------------------------- 1 | #ifndef GBINTERCEPTOR_OPCODES 2 | #define GBINTERCEPTOR_OPCODES 3 | 4 | #include "pico/stdlib.h" 5 | 6 | extern void (*opcodes[])(); 7 | void toMemory(uint16_t address, uint8_t data); 8 | 9 | #endif -------------------------------------------------------------------------------- /firmware/osd.c: -------------------------------------------------------------------------------- 1 | #include "osd.h" 2 | #include "ppu.h" 3 | #include 4 | 5 | #include "font/font8x8_basic.h" 6 | 7 | uint8_t osdBuffer[OSD_HEIGHT * SCREEN_W]; 8 | 9 | uint osdPosition = SCREEN_H; 10 | uint timeRemaining = 0; 11 | 12 | void inline renderOSDCharacter(char i, uint x, uint y, volatile uint8_t * targetBuffer, uint8_t fgColor, uint8_t bgColor) { 13 | for (uint yi = 0; yi < 8; yi++) { 14 | char line = font8x8_basic[i][yi]; 15 | uint8_t volatile * bufferindex = targetBuffer + (y + yi) * SCREEN_W + x; 16 | uint8_t mask = 0x01; 17 | for (uint xi = 0; xi < 8; xi++) { 18 | if (line & mask) 19 | *bufferindex = fgColor; 20 | else 21 | *bufferindex = bgColor; 22 | mask <<= 1; 23 | bufferindex++; 24 | } 25 | } 26 | } 27 | 28 | void renderOSDFillLine(uint fromX, uint toX, uint y0, volatile uint8_t * targetBuffer, uint8_t color) { 29 | uint8_t volatile * bufferindex = targetBuffer + y0 * SCREEN_W + fromX; 30 | for (uint y = y0; y < y0 + 8; y++) { 31 | for (uint x = fromX; x < toX; x++) { 32 | *bufferindex = color; 33 | bufferindex++; 34 | } 35 | bufferindex += SCREEN_W - toX + fromX; 36 | } 37 | } 38 | 39 | 40 | void animateOSD() { 41 | if (timeRemaining) { 42 | timeRemaining--; 43 | if (timeRemaining < OSD_HEIGHT) 44 | osdPosition = SCREEN_H - timeRemaining; 45 | else if (osdPosition > SCREEN_H - OSD_HEIGHT) 46 | osdPosition--; 47 | } 48 | } 49 | 50 | uint renderText(const char * text, uint8_t fgColor, uint8_t bgColor, uint8_t * buffer, uint x, uint y) { 51 | const char *i = text; 52 | uint w = 0; 53 | uint xi = x; 54 | while (*i != '\0') { 55 | if (*i == '\n' || w >= 19) { 56 | y += 9; 57 | xi = x; 58 | w = 0; 59 | if (*i == '\n') 60 | i++; 61 | } else { 62 | renderOSDCharacter(*i, xi, y, buffer, fgColor, bgColor); 63 | xi += 8; 64 | w++; 65 | i++; 66 | } 67 | } 68 | return xi; 69 | } 70 | 71 | void renderOSD(const char * text, uint8_t fgColor, uint8_t bgColor, uint duration) { 72 | //Padding 73 | uint8_t volatile * topborder = osdBuffer; 74 | for (uint i = 0; i < SCREEN_W; i++) { 75 | *topborder = bgColor; 76 | topborder++; 77 | } 78 | 79 | //Text line 80 | uint x = 1; 81 | uint y = 1; 82 | renderOSDFillLine(0, x, y, osdBuffer, bgColor); 83 | x = renderText(text, fgColor, bgColor, osdBuffer, x, y); 84 | renderOSDFillLine(x, SCREEN_W, y, osdBuffer, bgColor); 85 | 86 | timeRemaining = duration; 87 | osdPosition = SCREEN_H; 88 | } -------------------------------------------------------------------------------- /firmware/osd.h: -------------------------------------------------------------------------------- 1 | #ifndef OSD_H 2 | #define OSD_H 3 | 4 | #include "pico/stdlib.h" 5 | 6 | #define OSD_HEIGHT 9 7 | 8 | extern uint osdPosition; 9 | extern uint8_t osdBuffer[]; 10 | 11 | void animateOSD(); 12 | uint renderText(const char * text, uint8_t fgColor, uint8_t bgColor, uint8_t * buffer, uint x, uint y); 13 | void renderOSD(const char * text, uint8_t fgColor, uint8_t bgColor, uint duration); 14 | 15 | #endif -------------------------------------------------------------------------------- /firmware/pico_sdk_import.cmake: -------------------------------------------------------------------------------- 1 | # This is a copy of /external/pico_sdk_import.cmake 2 | 3 | # This can be dropped into an external project to help locate this SDK 4 | # It should be include()ed prior to project() 5 | 6 | if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) 7 | set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) 8 | message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") 9 | endif () 10 | 11 | if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) 12 | set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) 13 | message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") 14 | endif () 15 | 16 | if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) 17 | set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) 18 | message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") 19 | endif () 20 | 21 | set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") 22 | set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") 23 | set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") 24 | 25 | if (NOT PICO_SDK_PATH) 26 | if (PICO_SDK_FETCH_FROM_GIT) 27 | include(FetchContent) 28 | set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) 29 | if (PICO_SDK_FETCH_FROM_GIT_PATH) 30 | get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") 31 | endif () 32 | # GIT_SUBMODULES_RECURSE was added in 3.17 33 | if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") 34 | FetchContent_Declare( 35 | pico_sdk 36 | GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk 37 | GIT_TAG master 38 | GIT_SUBMODULES_RECURSE FALSE 39 | ) 40 | else () 41 | FetchContent_Declare( 42 | pico_sdk 43 | GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk 44 | GIT_TAG master 45 | ) 46 | endif () 47 | 48 | if (NOT pico_sdk) 49 | message("Downloading Raspberry Pi Pico SDK") 50 | FetchContent_Populate(pico_sdk) 51 | set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) 52 | endif () 53 | set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) 54 | else () 55 | message(FATAL_ERROR 56 | "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." 57 | ) 58 | endif () 59 | endif () 60 | 61 | get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") 62 | if (NOT EXISTS ${PICO_SDK_PATH}) 63 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") 64 | endif () 65 | 66 | set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) 67 | if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) 68 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") 69 | endif () 70 | 71 | set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) 72 | 73 | include(${PICO_SDK_INIT_CMAKE_FILE}) 74 | -------------------------------------------------------------------------------- /firmware/ppu.h: -------------------------------------------------------------------------------- 1 | #ifndef GBINTERCEPTOR_PPU 2 | #define GBINTERCEPTOR_PPU 3 | 4 | #include "pico/stdlib.h" 5 | #include "pico/sync.h" 6 | #include "jpeg/jpeg.h" 7 | 8 | #define SCREEN_W 160 9 | #define SCREEN_H 144 10 | #define SCREEN_SIZE (SCREEN_W * SCREEN_H) 11 | 12 | #define CYCLES_PER_FRAME 17556 13 | #define CYCLES_PER_LINE 114 14 | #define CYCLES_MODE_0 51 //max, actually 87 to 204 dots 15 | #define CYCLES_MODE_1 1140 16 | #define CYCLES_MODE_2 20 17 | #define CYCLES_MODE_3 43 //min, actually 172 to 289 dots 18 | #define CYCLES_LATEST_HBLANK (CYCLES_PER_LINE - 21) //At that point we are certainly in hblank 19 | #define LINES 154 20 | 21 | bool swapFrontbuffer(); 22 | void ppuInit(); 23 | void ppuStep(uint advance); 24 | 25 | extern uint8_t volatile * frontBuffer; 26 | extern uint8_t volatile * readyBuffer; 27 | extern uint8_t volatile * backBuffer; 28 | extern uint8_t volatile * lastBuffer; 29 | 30 | extern bool frameBlending; 31 | 32 | enum RenderState {done = 0, start, oamSearchDone, rendering}; 33 | extern volatile enum RenderState renderState; 34 | 35 | extern uint lineCycle; 36 | extern int y; 37 | 38 | extern bool readyBufferIsNew; 39 | extern int volatile vblankOffset; 40 | 41 | extern volatile bool windowTileMap9C00; 42 | extern volatile bool windowEnable; 43 | extern volatile bool tileData8000; 44 | extern volatile bool bgTileMap9C00; 45 | extern volatile uint objSize; 46 | extern volatile bool objEnable; 47 | extern volatile bool bgAndWindowDisplay; 48 | extern volatile bool lcdAndPpuEnable; 49 | 50 | extern volatile uint8_t paletteBG[]; 51 | extern volatile uint8_t paletteOBP0[]; 52 | extern volatile uint8_t paletteOBP1[]; 53 | 54 | struct __attribute__((__packed__)) SpriteAttribute { 55 | uint8_t y; 56 | uint8_t x; 57 | uint8_t tileIndex; 58 | uint8_t attributes; 59 | }; 60 | 61 | #endif -------------------------------------------------------------------------------- /firmware/screens/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Staacks/gbinterceptor/14213d507fb056389c3be0bd35f8b57b359a52ea/firmware/screens/default.png -------------------------------------------------------------------------------- /firmware/screens/default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 41 | 43 | 47 | 54 | 61 | GBInterceptor 81 | https://there.oughta.be 97 | 98 | 99 | -------------------------------------------------------------------------------- /firmware/screens/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Staacks/gbinterceptor/14213d507fb056389c3be0bd35f8b57b359a52ea/firmware/screens/error.png -------------------------------------------------------------------------------- /firmware/screens/error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 41 | 43 | 47 | 54 | 61 | GBInterceptor 81 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /firmware/screens/off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Staacks/gbinterceptor/14213d507fb056389c3be0bd35f8b57b359a52ea/firmware/screens/off.png -------------------------------------------------------------------------------- /firmware/screens/off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 42 | 44 | 48 | 55 | 62 | GBInterceptor 82 | 93 | https://there.oughta.be 109 | 110 | 111 | -------------------------------------------------------------------------------- /firmware/screens/png2header.sh: -------------------------------------------------------------------------------- 1 | BASE=`echo $1 | cut -f 1 -d '.'` 2 | convert $1 -brightness-contrast -98x-98 gray:$BASE.raw 3 | 4 | xxd -i $BASE.raw | sed '1 s/unsigned char/unsigned char __in_flash("screens")/' > ${BASE}.h 5 | rm $BASE.raw 6 | 7 | -------------------------------------------------------------------------------- /firmware/tusb_config.h: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2019 Ha Thach (tinyusb.org) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | */ 25 | 26 | #ifndef _TUSB_CONFIG_H_ 27 | #define _TUSB_CONFIG_H_ 28 | 29 | #ifdef __cplusplus 30 | extern "C" { 31 | #endif 32 | 33 | //--------------------------------------------------------------------+ 34 | // Board Specific Configuration 35 | //--------------------------------------------------------------------+ 36 | 37 | // RHPort number used for device can be defined by board.mk, default to port 0 38 | #ifndef BOARD_TUD_RHPORT 39 | #define BOARD_TUD_RHPORT 0 40 | #endif 41 | 42 | // RHPort max operational speed can defined by board.mk 43 | #ifndef BOARD_TUD_MAX_SPEED 44 | #define BOARD_TUD_MAX_SPEED OPT_MODE_DEFAULT_SPEED 45 | #endif 46 | 47 | //-------------------------------------------------------------------- 48 | // Common Configuration 49 | //-------------------------------------------------------------------- 50 | 51 | // defined by compiler flags for flexibility 52 | #ifndef CFG_TUSB_MCU 53 | #error CFG_TUSB_MCU must be defined 54 | #endif 55 | 56 | #ifndef CFG_TUSB_OS 57 | #define CFG_TUSB_OS OPT_OS_NONE 58 | #endif 59 | 60 | #ifndef CFG_TUSB_DEBUG 61 | #define CFG_TUSB_DEBUG 0 62 | #endif 63 | 64 | // Enable Device stack 65 | #define CFG_TUD_ENABLED 1 66 | 67 | // Default is max speed that hardware controller could support with on-chip PHY 68 | #define CFG_TUD_MAX_SPEED BOARD_TUD_MAX_SPEED 69 | 70 | /* USB DMA on some MCUs can only access a specific SRAM region with restriction on alignment. 71 | * Tinyusb use follows macros to declare transferring memory so that they can be put 72 | * into those specific section. 73 | * e.g 74 | * - CFG_TUSB_MEM SECTION : __attribute__ (( section(".usb_ram") )) 75 | * - CFG_TUSB_MEM_ALIGN : __attribute__ ((aligned(4))) 76 | */ 77 | #ifndef CFG_TUSB_MEM_SECTION 78 | #define CFG_TUSB_MEM_SECTION 79 | #endif 80 | 81 | #ifndef CFG_TUSB_MEM_ALIGN 82 | #define CFG_TUSB_MEM_ALIGN __attribute__ ((aligned(4))) 83 | #endif 84 | 85 | //-------------------------------------------------------------------- 86 | // DEVICE CONFIGURATION 87 | //-------------------------------------------------------------------- 88 | 89 | #ifndef CFG_TUD_ENDPOINT0_SIZE 90 | #define CFG_TUD_ENDPOINT0_SIZE 64 91 | #endif 92 | 93 | //------------- CLASS -------------// 94 | // The number of video control interfaces 95 | #define CFG_TUD_VIDEO 1 96 | #define CFG_TUD_CDC 0 97 | #define CFG_TUD_MSC 0 98 | #define CFG_TUD_HID 0 99 | #define CFG_TUD_MIDI 0 100 | #define CFG_TUD_VENDOR 0 101 | 102 | // The number of video streaming interfaces 103 | #define CFG_TUD_VIDEO_STREAMING 1 104 | 105 | //Bulk transfer mode (in contrast to isochronous mode) 106 | #define CFG_TUD_VIDEO_STREAMING_BULK 1 107 | 108 | // video streaming endpoint size 109 | #if CFG_TUD_VIDEO_STREAMING_BULK == 1 110 | #define CFG_TUD_VIDEO_STREAMING_EP_BUFSIZE 2048 111 | #else 112 | #define CFG_TUD_VIDEO_STREAMING_EP_BUFSIZE 1023 113 | #endif 114 | 115 | #define CFG_TUD_CDC_RX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) 116 | #define CFG_TUD_CDC_TX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) 117 | 118 | // CDC Endpoint transfer buffer size, more is faster 119 | #define CFG_TUD_CDC_EP_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) 120 | 121 | #ifdef __cplusplus 122 | } 123 | #endif 124 | 125 | #endif /* _TUSB_CONFIG_H_ */ -------------------------------------------------------------------------------- /firmware/usb_descriptors.c: -------------------------------------------------------------------------------- 1 | #include "tusb.h" 2 | #include "usb_descriptors.h" 3 | #include "pico/unique_id.h" 4 | 5 | /* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. 6 | * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. 7 | * 8 | * Auto ProductID layout's Bitmap: 9 | * [MSB] VIDEO | AUDIO | MIDI | HID | MSC | CDC [LSB] 10 | */ 11 | #define _PID_MAP(itf, n) ( (CFG_TUD_##itf) << (n) ) 12 | #define USB_PID (0x4000 | _PID_MAP(CDC, 0) | _PID_MAP(MSC, 1) | _PID_MAP(HID, 2) | \ 13 | _PID_MAP(MIDI, 3) | _PID_MAP(AUDIO, 4) | _PID_MAP(VIDEO, 5) | _PID_MAP(VENDOR, 6) ) 14 | 15 | //--------------------------------------------------------------------+ 16 | // Device Descriptors 17 | //--------------------------------------------------------------------+ 18 | tusb_desc_device_t const desc_device = { 19 | .bLength = sizeof(tusb_desc_device_t), 20 | .bDescriptorType = TUSB_DESC_DEVICE, 21 | .bcdUSB = 0x0200, 22 | 23 | // Use Interface Association Descriptor (IAD) for Video 24 | // As required by USB Specs IAD's subclass must be common class (2) and protocol must be IAD (1) 25 | .bDeviceClass = TUSB_CLASS_MISC, 26 | .bDeviceSubClass = MISC_SUBCLASS_COMMON, 27 | .bDeviceProtocol = MISC_PROTOCOL_IAD, 28 | 29 | .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, 30 | 31 | .idVendor = 0xCafe, 32 | .idProduct = USB_PID, 33 | .bcdDevice = 0x0100, 34 | 35 | .iManufacturer = 0x01, 36 | .iProduct = 0x02, 37 | .iSerialNumber = 0x03, 38 | 39 | .bNumConfigurations = 0x01 40 | }; 41 | 42 | // Invoked when received GET DEVICE DESCRIPTOR 43 | // Application return pointer to descriptor 44 | uint8_t const * tud_descriptor_device_cb(void) { 45 | return (uint8_t const *) &desc_device; 46 | } 47 | 48 | //--------------------------------------------------------------------+ 49 | // Configuration Descriptor 50 | //--------------------------------------------------------------------+ 51 | 52 | #if CFG_TUD_VIDEO_STREAMING_BULK == 1 53 | #define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_VIDEO_CAPTURE_DESC_BULK_LEN + CFG_TUD_CDC*TUD_CDC_DESC_LEN ) 54 | #else 55 | #define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_VIDEO_CAPTURE_DESC_LEN + CFG_TUD_CDC*TUD_CDC_DESC_LEN ) 56 | #endif 57 | 58 | #define EPNUM_VIDEO_IN 0x81 59 | 60 | #define EPNUM_CDC_NOTIF 0x83 61 | #define EPNUM_CDC_OUT 0x02 62 | #define EPNUM_CDC_IN 0x82 63 | 64 | uint8_t const desc_fs_configuration[] = { 65 | // Config number, interface count, string index, total length, attribute, power in mA 66 | TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0, 500), 67 | // IAD for Video Control 68 | #if CFG_TUD_VIDEO_STREAMING_BULK == 1 69 | TUD_VIDEO_CAPTURE_DESCRIPTOR_BULK(4, EPNUM_VIDEO_IN, 70 | FRAME_WIDTH, FRAME_HEIGHT, 71 | 64), 72 | #else 73 | TUD_VIDEO_CAPTURE_DESCRIPTOR(4, EPNUM_VIDEO_IN, 74 | FRAME_WIDTH, FRAME_HEIGHT, 75 | CFG_TUD_VIDEO_STREAMING_EP_BUFSIZE), 76 | #endif 77 | // Interface number, string index, EP notification address and size, EP data address (out, in) and size. 78 | #if CFG_TUD_CDC == 1 79 | TUD_CDC_DESCRIPTOR(ITF_NUM_CDC, 4, EPNUM_CDC_NOTIF, 8, EPNUM_CDC_OUT, EPNUM_CDC_IN, 64), 80 | #endif 81 | }; 82 | 83 | // Invoked when received GET CONFIGURATION DESCRIPTOR 84 | // Application return pointer to descriptor 85 | // Descriptor contents must exist long enough for transfer to complete 86 | uint8_t const * tud_descriptor_configuration_cb(uint8_t index) { 87 | (void) index; // for multiple configurations 88 | return desc_fs_configuration; 89 | } 90 | 91 | //--------------------------------------------------------------------+ 92 | // String Descriptors 93 | //--------------------------------------------------------------------+ 94 | 95 | // array of pointer to string descriptors 96 | char * string_desc_arr [] = 97 | { 98 | (char[]) { 0x09, 0x04 }, // 0: is supported language is English (0x0409) 99 | "there.oughta.be", // 1: Manufacturer 100 | "GB Interceptor", // 2: Product 101 | "1234567890123456", // 3: Serials, should use chip ID 102 | "GB Interceptor Video", // 4: UVC Interface 103 | }; 104 | 105 | static uint16_t _desc_str[32]; 106 | 107 | void setUniqueSerial() { 108 | pico_get_unique_board_id_string(string_desc_arr[3], 17); 109 | } 110 | 111 | // Invoked when received GET STRING DESCRIPTOR request 112 | // Application return pointer to descriptor, whose contents must exist long enough for transfer to complete 113 | uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) { 114 | (void) langid; 115 | 116 | uint8_t chr_count; 117 | 118 | if ( index == 0) { 119 | memcpy(&_desc_str[1], string_desc_arr[0], 2); 120 | chr_count = 1; 121 | } else { 122 | // Note: the 0xEE index string is a Microsoft OS 1.0 Descriptors. 123 | // https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-defined-usb-descriptors 124 | 125 | if ( !(index < sizeof(string_desc_arr)/sizeof(string_desc_arr[0])) ) return NULL; 126 | 127 | const char* str = string_desc_arr[index]; 128 | 129 | // Cap at max char 130 | chr_count = (uint8_t) strlen(str); 131 | if ( chr_count > 31 ) chr_count = 31; 132 | 133 | // Convert ASCII string into UTF-16 134 | for(uint8_t i=0; i