├── README.md ├── access_virus ├── README.md ├── dump_bootstrap.py ├── dump_dsp_memory.py ├── lib.py ├── simulator │ ├── bits_high_repeat.io │ ├── bits_toggle.io │ ├── virus_a_v280g.cmd │ └── virus_c_vc650g.cmd ├── virus_c │ └── README.md └── virus_ti │ ├── README.md │ ├── chunks.py │ ├── firmware.py │ └── vti.py └── dsp56k ├── be2le.py ├── bin2io.py ├── bin2lod.py ├── lod2bin.py └── patch_omr_sr.py /README.md: -------------------------------------------------------------------------------- 1 | # DSP563 Community Tools 2 | 3 | This repository contains technical documentation and a set of tools that 4 | facilitate research of various virtual analog synthesizers. 5 | 6 | ## Access Virus 7 | 8 | All technical information we discovered on the architecture of 9 | the Access Virus synthesizer series is documented here. 10 | 11 | See the `access_virus` directory for a guide on how to use the tools 12 | and for additional information on specific hardware versions. 13 | 14 | ### Hardware versions 15 | 16 | * 1997 Virus A - 1 x Motorola DSP 56303, 1x SAB 80C535-N 17 | * 1999 Virus B - 1 x Motorola DSP 56311, 1x SAB 80C535-N 18 | * 2002 Virus C - 1 x Freescale DSP 56362, 1x SAF 80C515-L24N 19 | * 2005 Virus TI - 2 x Freescale DSP 56367 - 2x150 MHz, 1x ST UPSD3212CV 20 | * 2009 Virus TI2 - 2 x Freescale DSP 56321 - 2x275 MHz 21 | 22 | ### Motorola DSP563xx 23 | 24 | Each synthesizer has at least one 24-bit DSP (Motorola DSP563xx series) 25 | that performs the audio processing. Detailed information on the 26 | device and instruction set can be found in the corresponding Motorola 27 | User Manual and DSP56300 Family Manual. 28 | 29 | ### i8051 microcontroller 30 | 31 | Handling user inputs from knobs and showing data on an LED display is 32 | handled by an 8-bit MCU with the Intel i8051 instruction set. 33 | This microcontroller also takes care of initializing the DSP. The DSP 34 | is connected to the microcontroller via the Host Interface (HDI08). 35 | 36 | ### Flash memory 37 | 38 | The flash memory contains all the code and data the synthesizer needs to 39 | operate. All legacy models have 512K flash memory, the newer TI and TI2 40 | models feature a total of 1M. 41 | 42 | #### Contents 43 | * i8051 operating system 44 | * i8051 flash programming routines 45 | * DSP563xx code (PRAM) and memory (XRAM, YRAM) 46 | * Presets and other data 47 | 48 | #### Layout 49 | 50 | Flash memory is split up in banks of 32K (`0x8000`) bytes. This allows the 51 | memory to be directly addressable by the microcontroller, because the i8051 52 | microcontroller only has 16 bits of address space for XRAM (`0x0 - 0xFFFF`). 53 | 54 | The lower half of this address space (`0x0 - 0x8000`) always points to the 55 | first bank of the flash memory, while the upper half (`0x8000 - 0xFFFF`), 56 | can point to any other bank of the flash memory. This is achieved by a 57 | bank switching routine of the microcontroller. 58 | 59 | The bank switching routine accepts one argument A, which can be mapped to 60 | a file offset in flash using: `offset = (A & 0xF0) << 11`. The low nibble 61 | is ignored. Note that we only verified this on legacy virus models (with 62 | 512K flash). 63 | 64 | Example for `A = 0x10`: `(0x10 & 0xF0) << 11 == 0x8000` 65 | 66 | #### Banks 67 | 68 | The exact structure of the banks differs from synthesizer model to model, 69 | but for 512K flash it roughly looks like this: 70 | 71 | ``` 72 | [ bank 0 - 2 ] i8051 operating system code 73 | [ bank 3 - 7 ] DSP563xx BootROM + chunks 74 | [ bank 7 ] i8051 flash programming routines 75 | [ bank 8 - 15 ] preset data 76 | ``` 77 | 78 | ### Execution flow 79 | 80 | The i8051 microcontroller will go through the following initialization phases: 81 | 82 | 1. Flash memory is bank switched into the i8051 program address space. 83 | 2. i8051 execution starts at 0x0, which is an `ljmp RESET_0`. 84 | 3. RESET_0 routine will: 85 | * Perform generic initialization routines 86 | * Start DSP initialization (see below) 87 | * ... (TODO) 88 | 89 | ### DSP initialization 90 | 91 | The microcontroller will initialize the DSP by reading 24bit words from 92 | several banks in flash. For legacy models, the DSP data starts at bank 3 93 | (offset 0x18000). For TI and TI2, the DSP data starts at bank 14 94 | (offset 0x70000). 95 | 96 | #### Bank parsing 97 | 98 | Each bank is structured like this: 99 | 100 | ` 101 | [ 1 byte index ] [ 1 byte size1 ] [ 1 byte size2 ] [ 3 byte words ... ] 102 | ` 103 | 104 | The number of words to be read can be calculated as follows: 105 | `word_count = (size1 - 1) << 8 | size2` 106 | 107 | With each bank read, the index will decrement. The last bank to be read 108 | has an index of 0. At the end of the last bank there is a string terminated 109 | with a null-byte that represents the version of the firmware. 110 | 111 | #### Bank data stream 112 | The resulting data stream is structured like this (each element is one word 113 | / 24bits): 114 | 115 | ` 116 | [ bootrom_size ] [ bootrom_offset ] 117 | [ bootrom_data ... ] [ chunk_data ... ] 118 | ` 119 | 120 | This datastream is sent over the HDI08 port which is connected to the DSP. 121 | The built-in bootstrap program of the DSP running at `0xffff00` will read 122 | `bootrom_size` words and writes them to PRAM memory at offset `bootrom_offset`. 123 | (usually `0x100`). Execution will start there and the BootROM 124 | will be retrieving the remaining `chunk_data` from the same HDI08 port. 125 | 126 | The assembly source code of the built-in bootstrap program can be found in 127 | Appendix A of the Motorola User Manual. 128 | 129 | #### DSP chunk data 130 | 131 | The BootROM can be disassembled to see exactly how it processes the remaining 132 | chunk data, but the process is briefly described here. 133 | 134 | Chunk structure: `[ cmd ] [ addr ] [ size ] [ words ... ] ` 135 | 136 | The `size` element indicates the number of words in the chunk. 137 | 138 | The `addr` element indicates the destination address for the words. 139 | 140 | ##### Commands 141 | ``` 142 | 000000: Write to P memory 143 | 000001: Write to X memory 144 | 000002: Write to Y memory 145 | 000003: Write to Y memory (split up each word in two 12-bit values) 146 | 000004: Jump to address (start execution) 147 | ``` 148 | 149 | ### Reverse engineering 150 | Both the microcontroller code and the DSP code can be disassembled using 151 | your favourite disassembler (we are using IDA Pro). 152 | 153 | #### Microcontroller 154 | Just load the entire flash rom into IDA and select Intel 8051 as the processor 155 | type (and select the correct device name, see the hardware list). 156 | The RESET_0 routine should be visible (which is the main entrypoint). 157 | 158 | Note that some routines are not recognized automatically (such as the 159 | flash programming ones). You can go to the correct offset and press `c` 160 | to instruct IDA to disassemble those. 161 | 162 | ### DSP code 163 | You can use the tools in this repository to extract the DSP program 164 | from any Access Virus flash dump file. 165 | 166 | It performs the BootROM method described above to extract the DSP memory and 167 | write it to a file. Then load up IDA and select dsp563xx as the processor type. 168 | 169 | The entrypoint can be determined by looking at command 4. 170 | 171 | We stumbled upon an issue with the cross-references in IDA. With the `jclr` 172 | instruction for example, IDA seems to ignore the high bits of the destination 173 | address, causing a wrongly displayed x-ref. Unfortunately, we did not yet 174 | find a good solution to this problem. 175 | 176 | Some helpful scripts and configuration files are available in the `ida` 177 | directory. 178 | 179 | Keep in mind that IDA Pro loads dsp563xx binaries using little endian, while 180 | the DSP563 itself uses big endian byte order (we included a tool be2le.py to 181 | make conversion easy :) 182 | 183 | ## Contributing 184 | 185 | Feel free to join us on [Discord](https://discord.gg/x6epkzvHXx) if you'd like to help out! 186 | 187 | ### External Resources 188 | 189 | * https://github.com/mamedev/mame/blob/master/src/mame/drivers/acvirus.cpp 190 | * https://adriangin.wordpress.com/2018/09/27/virus-ti-hardware-firmware/ 191 | * https://www.chameleon.synth.net/ 192 | -------------------------------------------------------------------------------- /access_virus/README.md: -------------------------------------------------------------------------------- 1 | # Access Virus Tools 2 | 3 | This document will show how to extract DSP memory regions from a flash 4 | memory dump of any Access Virus model (both legacy and TI models 5 | are supported). 6 | 7 | It will also provide some guidelines on how to run the firmware in the 8 | Motorola Simulator. 9 | 10 | An additional extraction tool necessary for Access Virus TI can be found 11 | in the `virus_ti` directory. 12 | 13 | ## Extracting BootROM and CommandStream 14 | 15 | The CommandStream contains the binary data we need to extract the DSP code. 16 | 17 | Use the `dump_bootstrap.py` script to extract both the BootROM and the 18 | CommandStream: 19 | 20 | ```bash 21 | $ python3 dump_bootstrap.py 22 | INFO:__main__:Usage: python3 dump_bootstrap.py 23 | ``` 24 | 25 | Example for Access Virus C: 26 | 27 | ```bash 28 | $ python3 dump_bootstrap.py access_virus/virus_c/vc_650g/access_virus_c_am29f040b_6v6.bin dsp 29 | INFO:__main__:Flash version: (C)ACCESS [12-14-2009-18:22:54][vc_650g], Size: 0xb8d4 30 | INFO:__main__:BootROM size: 0x51 31 | INFO:__main__:BootROM offset: 0x100 32 | INFO:__main__:CommandStream size: 0xb881 33 | INFO:__main__:Successfully wrote BootROM to dsp.bootrom.be.bin 34 | INFO:__main__:Successfully wrote BootROMStream to dsp.bootrom_stream.be.bin 35 | INFO:__main__:Successfully wrote CommandStream to dsp.command_stream.be.bin 36 | ``` 37 | 38 | Example for Access Virus TI: 39 | 40 | ```bash 41 | $ python3 dump_bootstrap.py access_virus/virus_ti/51700/roms/vti_2/F.bin virus_ti_2 42 | INFO:__main__:Flash version: wvd119.lod 08/05/12-15:34:15 (C)ACCESS MUSIC, Size: 0x27d62 43 | INFO:__main__:BootROM size: 0x47 44 | INFO:__main__:BootROM offset: 0x100 45 | INFO:__main__:CommandStream size: 0x27d19 46 | INFO:__main__:Successfully wrote BootROM to virus_ti_2.bootrom.be.bin 47 | INFO:__main__:Successfully wrote BootROMStream to virus_ti_2.bootrom_stream.be.bin 48 | INFO:__main__:Successfully wrote CommandStream to virus_ti_2.command_stream.be.bin 49 | ``` 50 | 51 | ## Extracting the DSP memory regions 52 | 53 | Once we have the command stream, we could start the simulator and have 54 | BootROM prepare the memory sections. But we also provide a tool that mimics 55 | this behavior and allows you to extract the P, X and Y memory regions. 56 | 57 | ```bash 58 | $ python3 dump_dsp_memory.py 59 | INFO:__main__:Usage: python3 dump_dsp_memory.py 60 | ``` 61 | 62 | Example for Access Virus C: 63 | 64 | ```bash 65 | $ python3 dump_dsp_memory.py dsp.command_stream.be.bin dsp_memory 66 | ... 67 | INFO:__main__:Discovered entrypoint (command = 4) @ 0xda6 68 | INFO:__main__:Writing region p to dsp_memory.p.be.bin 69 | INFO:__main__:Writing region x to dsp_memory.x.be.bin 70 | INFO:__main__:Writing region y to dsp_memory.y.be.bin 71 | ``` 72 | 73 | Remember to use our tool `be2le.py` in the `dsp56k` directory to convert 74 | it to little endian so that you can load it up into IDA Pro. 75 | 76 | ## Running in the Motorola DSP Simulator 77 | 78 | We can perform static binary analysis on the code we extracted earlier, 79 | but we can also try to run the device into the Motorola Simulator, 80 | to get even more insight in how it works. 81 | 82 | Check out the simulator documentation `DSPS56SIMUM.pdf` for details 83 | on how to use it (included in the Chameleon SDK). 84 | 85 | ### Macros 86 | 87 | We recommend to create macro files to setup the simulator. 88 | Macros are text files that contain the commands that are 89 | provided to the simulator. They can be used to define 90 | breakpoints or other setup instructions. 91 | 92 | You can use a macro as follows: 93 | ``` 94 | .\sim56300.exe macro.cmd 95 | ``` 96 | 97 | We'll be showing some example macros below. 98 | 99 | ### Peripherals 100 | 101 | The DSP will communicate with external chips via special memory 102 | addresses that you can simulate as follows: 103 | `input x:$ local_file.io` 104 | 105 | The `local_file.io` can contain a series of values that will 106 | be provided each time the DSP reads a value from ``. 107 | 108 | Use the tool `bin2io.py` to create such an IO file for data 109 | such as the CommandStream. 110 | 111 | ### Byte-patching 112 | 113 | When running code in the simulator, you will run into several 114 | issues due to changes to the OMR and SR registers. These are 115 | special register types that influence specific behavior of the 116 | DSP. During our tests, we had to patch these instructions to 117 | disable some bits such as Instruction Cache and Address Priority 118 | Disable (APD). 119 | 120 | Another tool `patch_omr_sr.py` is provided to take care of this. 121 | 122 | ### Shared memory space 123 | 124 | We ran into an issue where a pointer was being access from Y memory, 125 | but this memory did not contain a valid pointer. Jumping to 126 | this nullpointer results in a crash. We found out that in X memory, 127 | there *was* a valid pointer at the given memory location. 128 | This probably has something to do with memory regions that are shared 129 | between X and Y (and possibly P). Could have something to do with how 130 | the AAR registers are configured, but we havent been able to figure it 131 | out yet. 132 | 133 | The way we addressed this issue is by duplicating some X memory to Y 134 | memory space by duplicating the last entry of the CommandStream and 135 | changing the cmd (01) to (02) so that this data will be copied to both 136 | X and Y memory. 137 | 138 | ### Approach 1: Motorola DSP56362 (SHI Bootstrapped) 139 | 140 | The simulator comes with a device type 56362 that we will use to load up 141 | the Virus C. This device has a built-in bootstrap program that will read 142 | the BootROM from a specific peripheral based on the operating mode. 143 | 144 | We will use the SHI interface which is part of mode 7. 145 | 146 | Byte-patch the instructions that set the OMR and SR registers: 147 | 148 | ``` 149 | ./dsp56k/patch_omr_sr.py dsp.bootrom_stream.be.bin dsp.bootrom_stream.patched.be.bin 150 | ./dsp56k/patch_omr_sr.py dsp.command_stream.be.bin dsp.command_stream.patched.be.bin 151 | ``` 152 | This will change the following instructions 153 | 154 | * `05f43a 004080` -> `05f43a 000080` (disable APD) 155 | * `05f439 080000` -> `05f439 000000` (disable IC) 156 | 157 | Create a `bits_high_repeat.io` file: 158 | ``` 159 | ($FFFFFF) 160 | ``` 161 | 162 | Create a `bits_toggle.io` file: 163 | ``` 164 | (FFFFFF#2 000000#2) 165 | ``` 166 | 167 | Create the BootROM IO file: 168 | ``` 169 | ./dsp56k/bin2io.py dsp.bootrom_stream.patched.be.bin dsp.bootrom_stream.patched.be.io 170 | ``` 171 | 172 | Create the CommandStream IO file (repeat values twice): 173 | ``` 174 | ./dsp56k/bin2io.py dsp.command_stream.patched.be.bin dsp.command_stream_repeat.patched.be.io 2 175 | ``` 176 | 177 | Run the following macro: 178 | ``` 179 | ; Motorola DSP56362 Access Virus C [vc_650g] 180 | device dv00 56362 181 | 182 | ;;; Load BootROM via SHI 183 | input x:$FFFF91 bits_high_repeat.io 184 | input x:$FFFF94 .\output\virus_c_vc_650g.bootrom_stream.patched.be.io 185 | 186 | ;;; Load CommandStream via HDI08 187 | input x:$FFFFC3 .\bits_high_repeat.io 188 | input x:$FFFFC6 .\output\virus_c_vc_650g.command_stream.patched.be.io 189 | 190 | ;;; Make ESAI available 191 | input x:$FFFFB3 bits_toggle.io 192 | 193 | ;;; Set operation mode to load from SHI 194 | reset D m7 195 | 196 | ;;; Breakpoints 197 | break p:$100 ; BootROM entrypoint 198 | ;break p:$30024 ; Process new command 199 | break p:$da6 ; DSP entrypoint 200 | 201 | go 202 | 203 | ``` 204 | 205 | The simulator will go through the following phases: 206 | 1. Runs built-in bootstrap program @ 0xFF0000 207 | 2. Loads BootROM from SHI and writes to specified offset (0x100) 208 | 3. Jumps to BootROM at offset 0x100 209 | 4. BootROM will copy itself to offset 0x30000 and jumps there 210 | 5. Loads CommandStream via Host interface and prepares P, X and Y RAM 211 | 6. Jumps to DSP entrypoint at offset 0xda6 212 | 213 | 214 | ### Approach 2: Motorola DSP56303 (HDI08 Bootstrapped, i8051 mode) 215 | 216 | After we got the first example working, we noticed some ROMs are using the 217 | SHI interface to receive data. Because we already mapped SHI IO to the 218 | `dsp.bootrom_stream.io`, we might run into some conflicts here. 219 | We could turn off the I/O after the BootROM finished, but instead we 220 | tried to load both the BootROM and the CommandStream via HDI08 (this is 221 | also how the original hardware does it). 222 | 223 | We got it to work by tricking the Simulator in a reserved mode and preparing 224 | the IO file in the right way. 225 | 226 | Prepare both the BootROM and CommandStream in the same way as Example 1, and 227 | concatenate the two files: 228 | `cat dsp.bootrom_stream.patched.be.io dsp.command_stream_repeat.patched.be.io >> dsp.bootrom_command_stream.patched.be.io` 229 | 230 | Use the following macro: 231 | ``` 232 | ; Motorola DSP56303 Access Virus A [v280g] 233 | device dv00 56303 234 | 235 | ;;; Load BootROM and CommandStream via HDI08 236 | input x:$FFFFC3 .\bits_high_repeat.io 237 | input x:$FFFFC6 .\output\dsp.bootrom_command_stream.patched.be.io 238 | 239 | ;;; Make audio available 240 | input x:$FFFFB7 .\bits_toggle.io 241 | 242 | ;;; Put simulator into i8051 mode 243 | reset D m1 ; Use $ffff00 as entrypoint 244 | change omr $30e ; Enable DMA and HDI08 bootstrap 245 | 246 | ;;; Breakpoints 247 | break p:$100 ; bootrom entrypoint 248 | ;break p:$2543e ; bootrom command handler 249 | break p:$9a6 ; main entrypoint 250 | 251 | go 252 | 253 | ``` 254 | 255 | ### Approach 3: Motorola DSP56362 (NON-Bootstrapped, OMF/LOD loading) 256 | 257 | We also tried creating an LOD file using the tool `be2lod.py` (combining 258 | P, X and Y) and loading that directly into the Simulator, basically bypassing 259 | the entire BootROM procedure, but we kept running into crashes and couldn't 260 | get it working properly. 261 | 262 | 263 | ### Troubleshooting 264 | 265 | #### Simulator steps half in between instructions 266 | 267 | Did you correctly patch the OMR/SR instructions? 268 | Disabling the Instruction Cache in the SR register should fix this. 269 | 270 | #### During simulation the process jmps to p:$0 271 | 272 | This is probably caused by missing data in Y memory space. 273 | It appears memory from X is shared with Y. During runtime, 274 | the process accesses pointers from Y memory where it jumps to. 275 | If these memory regions are empty, a jmp to p:$0 occurs. 276 | 277 | We fixed this by duplicated the one before last command (cmd=1) 278 | and changing cmd to 2 so that it also writes these values to 279 | Y memory. 280 | 281 | #### Simulator crashes when writing to P memory 282 | 283 | Make sure the Memory Switch Mode bit in the OMR is enabled. 284 | We found that without this bit, writing to the lower region 285 | of P memory results in a crash. 286 | 287 | You can try if this helps in the Simulator with this command: 288 | ``` 289 | change omr $80 290 | ``` 291 | 292 | #### Illegal instruction encountered: pflush 293 | 294 | Did you correctly patch the OMR instructions? 295 | Disabling the ADP in the OMR register should fix this. 296 | -------------------------------------------------------------------------------- /access_virus/dump_bootstrap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import logging 3 | import pathlib 4 | import sys 5 | 6 | import lib 7 | 8 | logging.basicConfig(level=logging.INFO) 9 | logger = logging.getLogger(__name__) 10 | 11 | """ 12 | Extraction utility for all Access Virus EEPROM flash dumps. 13 | 14 | Usage: python3 dump_bootstrap.py 15 | 16 | Can be used with flash memory dumps of any Access Virus model. 17 | This tool will export both the DSP BootROM as well as the CommandStream 18 | data that the BootROM will use to initialize DSP P, X and Y memory. 19 | 20 | The BootROM stream file uses the following format that the DSP563xx chip 21 | accepts to write the BootROM to the given offset in program memory: 22 | [ size ] [ offset ] [ data ... ] 23 | 24 | Resulting files: 25 | output.bootrom.be.bin: BootROM only 26 | output.bootrom_stream.be.bin: BootROMStream (including size,offset header) 27 | output.command_stream.be.bin: CommandStream (used by the BootROM) 28 | 29 | The last file can be used as input for `dump_dsp_memory.py`. 30 | 31 | All files will be written in big endian. Use `be2le.py` to convert to 32 | little endian for loading in IDA Pro. 33 | 34 | Note: This command will overwrite existing output files. 35 | 36 | Requires python version 3.8+. 37 | """ 38 | 39 | if len(sys.argv) != 3: 40 | logger.info("Usage: python3 %s ", sys.argv[0]) 41 | sys.exit(-1) 42 | 43 | # Make sure the input file exists 44 | src = pathlib.Path(sys.argv[1]) 45 | if not src.exists(): 46 | logger.error("Input file %s does not exist!", src) 47 | sys.exit(-1) 48 | 49 | # Find the model type based on the file size 50 | file_size = src.stat().st_size 51 | model_type = lib.AccessVirusType.from_size(file_size) 52 | if not model_type: 53 | logger.error("Could not determine Access Virus model type for file size %sK", file_size // 1024) 54 | sys.exit(-1) 55 | 56 | # Parse the banks that contain DSP code/data 57 | with open(src, "rb") as fp: 58 | bank_data = lib.get_dsp_bank_data(fp, model_type) 59 | 60 | logger.info("Flash version: %s, Size: 0x%x", bank_data.version, len(bank_data.data)) 61 | 62 | # Parse the bank data into BootROM and CommandStream 63 | chunk_data = lib.get_dsp_chunk_data(bank_data) 64 | logger.info("BootROM size: 0x%x", chunk_data.bootrom_size) 65 | logger.info("BootROM offset: 0x%x", chunk_data.bootrom_offset) 66 | logger.info("CommandStream size: 0x%x", len(chunk_data.data)) 67 | 68 | # Write BootROM 69 | file_path = "%s.bootrom.be.bin" % sys.argv[2] 70 | with open(file_path, "wb") as fp: 71 | for word in chunk_data.bootrom_data: 72 | fp.write(word.to_bytes(3, 'big')) 73 | 74 | logger.info("Successfully wrote BootROM to %s", file_path) 75 | 76 | # Write BootROMStream 77 | file_path = "%s.bootrom_stream.be.bin" % sys.argv[2] 78 | with open(file_path, "wb") as fp: 79 | fp.write(chunk_data.bootrom_size.to_bytes(3, 'big')) 80 | fp.write(chunk_data.bootrom_offset.to_bytes(3, 'big')) 81 | for word in chunk_data.bootrom_data: 82 | fp.write(word.to_bytes(3, 'big')) 83 | 84 | logger.info("Successfully wrote BootROMStream to %s", file_path) 85 | 86 | # Write CommandStream 87 | file_path = "%s.command_stream.be.bin" % sys.argv[2] 88 | with open(file_path, "wb") as fp: 89 | for word in chunk_data.data: 90 | fp.write(word.to_bytes(3, 'big')) # BootROM expects data in big endian 91 | 92 | logger.info("Successfully wrote CommandStream to %s", file_path) 93 | -------------------------------------------------------------------------------- /access_virus/dump_dsp_memory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import io 3 | import logging 4 | import pathlib 5 | import sys 6 | from dataclasses import dataclass 7 | from typing import Callable, List, Optional 8 | 9 | import lib 10 | 11 | logging.basicConfig(level=logging.INFO) 12 | logger = logging.getLogger(__name__) 13 | 14 | PAD_CHAR = b'\0' # padding used between non-consecutive sections in one region 15 | 16 | """ 17 | DSP memory dump utility for all Access Virus DSP command streams. 18 | 19 | Usage: python3 dump_dsp_memory.py 20 | 21 | Writes P, X, Y memory regions for the Motorola DSP563xx. 22 | 23 | Can be used with command stream dumps made with `dump_bootstrap.py`. 24 | This tool will mimic the bootstrapping done by the BootROM, and 25 | export the following files: 26 | 27 | output.p.bin: P memory 28 | output.x.bin: X memory 29 | output.y.bin: Y memory 30 | 31 | All files will be written in big endian. Use `be2le.py` to convert to 32 | little endian for loading in IDA Pro. 33 | 34 | Note: This command will overwrite existing output files. 35 | 36 | Requires python version 3.8+. 37 | """ 38 | 39 | if len(sys.argv) != 3: 40 | logger.info("Usage: python3 %s ", sys.argv[0]) 41 | sys.exit(-1) 42 | 43 | 44 | @dataclass 45 | class CommandHandler: 46 | region: str 47 | converter: Optional[Callable] = None 48 | 49 | def convert(self, value) -> List[int]: 50 | if self.converter: 51 | return self.converter(value) 52 | return [value] 53 | 54 | 55 | def split_12bit(value) -> List[int]: 56 | val1 = value & 0xFFF000 57 | val2 = value << 0xC & 0xFFF000 58 | return [val1, val2] 59 | 60 | 61 | # Make sure the input file exists 62 | src = pathlib.Path(sys.argv[1]) 63 | if not src.exists(): 64 | logger.error("Input file %s does not exist!", src) 65 | sys.exit(-1) 66 | 67 | # Check if the file contains 24bit words 68 | file_size = src.stat().st_size 69 | if file_size % 3 != 0: 70 | logger.error("Expected a binary file with 24bit words!") 71 | sys.exit(-1) 72 | 73 | # Read the 24bit words 74 | with open(src, "rb") as fp: 75 | words = [] 76 | for i in range(file_size // 3): 77 | words.append(int.from_bytes(fp.read(3), 'big')) 78 | 79 | # Command handlers 80 | handlers = { 81 | 0: CommandHandler(region='p'), 82 | 1: CommandHandler(region='x'), 83 | 2: CommandHandler(region='y'), 84 | 3: CommandHandler(region='y', converter=split_12bit), 85 | } 86 | 87 | commands = lib.get_dsp_commands(words) 88 | 89 | # The last command should be 4, representing a jmp to the entrypoint 90 | jump = commands.pop() 91 | assert jump.cmd == 4, "Did you provide a valid command stream file?" 92 | logger.info("Discovered entrypoint (command = 4) @ 0x%x", jump.addr) 93 | 94 | # Sort the remaining commands by address to allow writing consecutively 95 | commands = sorted(commands, key=lambda c: c.addr) 96 | 97 | # Create a memory buffer for each region 98 | buffers = {} 99 | for region in ['p', 'x', 'y']: 100 | buffers[region] = io.BytesIO() 101 | 102 | # Parse the commands 103 | for cmd in commands: 104 | logger.debug("Writing command %d, addr=0x%06x, size=0x%06x", cmd.cmd, cmd.addr, cmd.size) 105 | handler = handlers.get(cmd.cmd) 106 | buffer = buffers.get(handler.region) 107 | buffer.write(PAD_CHAR * ((cmd.addr * 3) - buffer.tell())) # write padding 108 | for word in cmd.data: 109 | values = handler.convert(word) 110 | for value in values: 111 | buffer.write(value.to_bytes(3, 'big')) 112 | 113 | # Write the output files 114 | for region, buffer in buffers.items(): 115 | file_path = "%s.%s.be.bin" % (sys.argv[2], region) 116 | logger.info("Writing region %s to %s", region, file_path) 117 | buffer.seek(0) 118 | with open(file_path, "wb") as fp: 119 | fp.write(buffer.read()) 120 | buffer.close() 121 | -------------------------------------------------------------------------------- /access_virus/lib.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import logging 3 | from dataclasses import dataclass 4 | from typing import BinaryIO, List, Optional 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | @dataclass 10 | class DSPBankData: 11 | version: str 12 | data: List[int] 13 | 14 | 15 | @dataclass 16 | class DSPChunkData: 17 | bootrom_size: int 18 | bootrom_offset: int 19 | bootrom_data: List[int] 20 | data: List[int] 21 | 22 | 23 | @dataclass 24 | class DSPCommandData: 25 | cmd: int 26 | addr: int 27 | size: Optional[int] 28 | data: Optional[List[int]] 29 | 30 | 31 | @dataclass 32 | class AccessVirusSpec: 33 | size: int 34 | dsp_offset: int 35 | 36 | def matches_size(self, size: int): 37 | return size == self.size 38 | 39 | 40 | class AccessVirusType(enum.Enum): 41 | LEGACY = AccessVirusSpec(size=512*1024, dsp_offset=0x18000) 42 | TI = AccessVirusSpec(size=1024*1024, dsp_offset=0x70000) 43 | 44 | @classmethod 45 | def from_size(cls, size: int) -> Optional['AccessVirusType']: 46 | """ Get either LEGACY or TI based on the given file size. """ 47 | return next((t for t in cls if t.value.matches_size(size)), None) 48 | 49 | 50 | def read_until(fp: BinaryIO, c: bytes): 51 | """ Read from the binary stream until the given character is encountered. """ 52 | buf = b'' 53 | while (match := fp.read(1)) != c: 54 | buf += match 55 | return buf 56 | 57 | 58 | def get_dsp_bank_data(fp: BinaryIO, model_type: AccessVirusType) -> DSPBankData: 59 | """ 60 | Retrieve the dsp related data from the flash memory banks. 61 | """ 62 | data = [] 63 | idx = 0xff 64 | offset = model_type.value.dsp_offset 65 | while idx > 0: 66 | logger.debug("Seeking to 0x%x", offset) 67 | fp.seek(offset) 68 | 69 | idx = int.from_bytes(fp.read(1), 'big') 70 | size1 = int.from_bytes(fp.read(1), 'big') 71 | size2 = int.from_bytes(fp.read(1), 'big') 72 | word_count = (size1 - 1) << 8 | size2 73 | 74 | logger.debug("Index: %d, size=0x%x", idx, word_count) 75 | for i in range(word_count): 76 | data.append(int.from_bytes(fp.read(3), 'big')) 77 | 78 | offset += 0x8000 79 | 80 | # Read version 81 | version = read_until(fp, b'\xFF').decode('ascii') 82 | logger.debug("Version: %s", version) 83 | 84 | return DSPBankData(version, data) 85 | 86 | 87 | def get_dsp_chunk_data(bank_data: DSPBankData) -> DSPChunkData: 88 | """ 89 | Parse the BootROM and command stream from the dsp bank data. 90 | """ 91 | bootrom_size = bank_data.data[0] 92 | bootrom_offset = bank_data.data[1] 93 | bootrom_data = bank_data.data[2:2+bootrom_size] 94 | chunk_data = bank_data.data[2+bootrom_size:] 95 | assert len(bootrom_data) == bootrom_size 96 | 97 | return DSPChunkData(bootrom_size, bootrom_offset, bootrom_data, chunk_data) 98 | 99 | 100 | def get_dsp_commands(chunk_data: List[int]) -> List[DSPCommandData]: 101 | """ 102 | Parse the command stream into individual command entries. 103 | """ 104 | commands = [] 105 | idx = 0 106 | while idx < len(chunk_data): 107 | cmd = chunk_data[idx] 108 | addr = chunk_data[idx+1] 109 | size = next(iter(chunk_data[idx+2:idx+3]), 0) # size can be empty for cmd = 4 110 | data = chunk_data[idx+3:idx+3+size] 111 | logger.info("Command %d, addr=0x%06x, size=0x%06x", cmd, addr, size) 112 | commands.append(DSPCommandData(cmd, addr, size, data)) 113 | idx += 3 + size 114 | 115 | return commands 116 | -------------------------------------------------------------------------------- /access_virus/simulator/bits_high_repeat.io: -------------------------------------------------------------------------------- 1 | ($FFFFFF) 2 | -------------------------------------------------------------------------------- /access_virus/simulator/bits_toggle.io: -------------------------------------------------------------------------------- 1 | (FFFFFF#2 000000#2) 2 | -------------------------------------------------------------------------------- /access_virus/simulator/virus_a_v280g.cmd: -------------------------------------------------------------------------------- 1 | ; Motorola DSP56303 Access Virus A [v280g] 2 | device dv00 56303 3 | 4 | ;;; Load BootROM and CommandStream via HDI08 5 | input x:$FFFFC3 .\bits_high_repeat.io 6 | input x:$FFFFC6 .\output\virus_a_v280g.bootrom_command_stream.patched.be.io 7 | 8 | ;;; Make audio available 9 | input x:$FFFFB7 .\bits_toggle.io 10 | 11 | ;;; Put simulator into i8051 mode 12 | reset D m1 ; Use $ffff00 as entrypoint 13 | change omr $30e ; Enable DMA and HDI08 bootstrap 14 | 15 | ;;; Enable logging 16 | ;log S session.log -O 17 | 18 | ;;; BootROM breakpoints 19 | break p:$100 ; bootrom entrypoint 20 | ;break p:$2543e ; bootrom command handler 21 | 22 | ;; Program breakpoints 23 | break p:$0 ; in case something goes wrong (nullptr jmp) 24 | break p:$9a6 ; main entrypoint 25 | break p:$2c184 ; audio ready 26 | break p:$2c186 ; setup 27 | break p:$2c189 ; main loop 28 | 29 | go 30 | -------------------------------------------------------------------------------- /access_virus/simulator/virus_c_vc650g.cmd: -------------------------------------------------------------------------------- 1 | ; Motorola DSP56362 Access Virus C [vc_650g] 2 | device dv00 56362 3 | 4 | ;;; Load BootROM via SHI 5 | input x:$FFFF91 bits_high_repeat.io 6 | input x:$FFFF94 .\output\virus_c_vc_650g.bootrom_stream.patched.be.io 7 | 8 | ;;; Load CommandStream via HDI08 9 | input x:$FFFFC3 .\bits_high_repeat.io 10 | input x:$FFFFC6 .\output\virus_c_vc_650g.command_stream.patched.be.io 11 | 12 | ;;; Make audio available 13 | input x:$FFFFB3 bits_toggle.io 14 | 15 | ;;; Set operation mode to load from SHI 16 | reset D m7 17 | 18 | ;;; BootROM breakpoints 19 | break p:$100 ; BootROM entrypoint 20 | ;break p:$30024 ; Process new command 21 | break p:$da6 ; DSP entrypoint 22 | 23 | 24 | go 25 | -------------------------------------------------------------------------------- /access_virus/virus_c/README.md: -------------------------------------------------------------------------------- 1 | # Access Virus C Analysis [vc_650g] 2 | 3 | ## Peripheral and operation configuration 4 | | Addr | Direction | Bit | Value | Description | Usage | 5 | |------------|-----------|-----|-------|--------------|--------------| 6 | | `0xfffff5` | Read | 0 | 1 | Debugging | Enable debug | 7 | 8 | ### Configure Operation Mode Register (OMR) & Status Register (SR) 9 | ``` 10 | ROM:0105 movec #$4080,omr ; BootROM 11 | ROM:0107 movec #$80000,sr 12 | .. 13 | ROM:0DB2 movec #$4380,omr ; Default OMR 14 | ROM:0DB4 movec #$80000,sr 15 | ... 16 | ROM:2C3F9 movec #$4080,omr ; turns of Core-DMA (not sure when) 17 | ROM:2C3FB movec #$80000,sr 18 | ... 19 | ROM:2C46C movec #$438E,omr ; before reset and jmps to boot program 20 | 21 | >>> '{:b}'.format(0x4380) # Default OMR 22 | '1000011 10000000' 23 | bit 7: MS => Memory Switch Mode 24 | bit 8: CPD0 => Core-DMA Priority 0 25 | bit 9: CPD1 => Core-DMA Priority 1 26 | bit 14: Address Priority Disable 27 | 28 | >>> '{:b}'.format(0x438E) # OMR during boot program 29 | '1000011 10001110' 30 | bit 1: MB \ 31 | bit 2: MC => HDI08 Bootstrap in 8051 multiplexed bus mode 32 | bit 3: MD / 33 | bit 7: MS => Memory Switch Mode 34 | bit 8: CPD0 => Core-DMA Priority 0 35 | bit 9: CPD1 => Core-DMA Priority 1 36 | bit 14: APD => Address Priority Disable 37 | 38 | Status register bit == instruction cache 39 | 40 | ``` 41 | 42 | ### Configure Address Attribute Registers 43 | 44 | ``` 45 | ROM:0109 movep #$20739,x:<>> '{:b}'.format(0x20739) # AAR0 in BootROM 55 | '10 00000111 00111001' 56 | bit 0: BAT0 => Bus Access Type == SRAM 57 | bit 3: BPEN => Bus Program Memory Enable 58 | bit 4: BXEN => Bus X Data Memory Enable 59 | bit 5: BYEN => Bus Y Data Memory Enable 60 | bit 8: BNC0 61 | bit 9: BNC1 62 | bit 10: BNC2 63 | bit 17: BAC5 64 | 65 | >>> '{:b}'.format(0x30839) # AAR1 in BootROM 66 | '11 00001000 00111001' 67 | bit 0: BAT0 => Bus Access Type == SRAM 68 | bit 3: BPEN => Bus Program Memory Enable 69 | bit 4: BXEN => Bus X Data Memory Enable 70 | bit 5: BYEN => Bus Y Data Memory Enable 71 | bit 11: BNC3 72 | bit 16: BAC4 73 | bit 17: BAC5 74 | 75 | >>> '{:b}'.format(0x539) # AAR0 in Entrypoint 76 | '101 00111001' 77 | bit 0: BAT0 => Bus Access Type == SRAM 78 | bit 3: BPEN => Bus Program Memory Enable 79 | bit 4: BXEN => Bus X Data Memory Enable 80 | bit 5: BYEN => Bus Y Data Memory Enable 81 | bit 8: BNC0 82 | bit 10: BNC2 83 | ``` 84 | 85 | ### Configure Bus Control 86 | ``` 87 | ROM:0DAB movep #$12421,x:<>> '{:b}'.format(0x12421) 90 | '1 00100100 00100001' 91 | bit 0: BA0W0 92 | bit 5: BA1W0 93 | bit 10: BA2W0 94 | bit 13: BA3W0 95 | bit 16: BDFW0 96 | ``` 97 | 98 | ### Configure Interrupt Priority 99 | ``` 100 | ROM:0DAD movep #$E07,x:<<$FFFFFF 101 | ... 102 | ROM:2C1BC movep #$555000,x:<<$FFFFFF ; IRQ is disabled here? 103 | ROM:2C1BE movep #$35B,x:<<$FFFFFE 104 | 105 | >>> '{:b}'.format(0xe07) 106 | '1110 00000111' 107 | bit 0: IAL0 = IRQA Priority 2 108 | bit 1: IAL1 109 | bit 2: IAL2 = IRQA Trigger Mode: Neg. Edge 110 | bit 9: IDL0 = IRQD Priority 2 111 | bit 10: IDL1 112 | bit 11: IDL2 = IRQD Trigger Mode: Neg. Edge 113 | 114 | >>> '{:b}'.format(0x555000) 115 | '1010101 01010000 00000000' 116 | bit 12: D0L0 = DMA0 Priority 0 117 | bit 14: D1L0 = DMA1 Priority 0 118 | bit 16: D2L0 = DMA2 Priority 0 119 | bit 18: D3L0 = DMA3 Priority 0 120 | bit 20: D4L0 = DMA4 Priority 0 121 | bit 22: D5L0 = DMA5 Priority 0 122 | 123 | >>> '{:b}'.format(0x35b) 124 | '11 01011011' 125 | bit 0: ESL0 = ESAI Priority 2 126 | bit 1: ESL1 127 | bit 3: SHL1 = SHI Priority 1 128 | bit 4: HDL0 = HDI08 Priority 0 129 | bit 6: DAL0 = DAX Priority 0 130 | bit 8: TAL0 = TEC Priority 2 131 | bit 9: TAL1 132 | ``` 133 | 134 | ### Configure HDI08 (Host Port) 135 | Connection to: SAF i8051 microcontroller. 136 | ``` 137 | ROM:280E3 movep #$1C1E,x:<>> '{:b}'.format(0x1C1E) 140 | '11100 00011110' => I8051HOSTLD 141 | ``` 142 | 143 | ### Configure SHI (Serial Host Interface) 144 | 145 | Connection to: Unknown 146 | ``` 147 | ROM:2BFD5 movep #$183,x:<>> '{:b}'.format(0x183) # HCKR, SHI Clock Control Register 153 | '1 10000011' 154 | bit 0: CPOL, Clock Polarity 155 | bit 1: CPHA, Clock Phase 156 | bit 7: HCKR Divider Modulus Select 157 | bit 8: HCKR Divider Modulus Select 158 | 159 | >>> '{:09b}'.format(0x41) # HCSR, SHI Control/Status Register 160 | '0 01000001' 161 | bit 0: HCSR Host Enable 162 | bit 6: HCSR Master Mode 163 | 164 | ``` 165 | 166 | ### Configure ESAI Receive 167 | Connection to: Digital to Analog Converter 168 | ``` 169 | ROM:2C1C8 movep #$40200,x:<<$FFFFB8 # ESAI RECEIVE CLOCK CONTROL REGISTER (RCCR) 170 | ROM:2C1CA movep #$7D00,x:<<$FFFFB7 # ESAI RECEIVE CONTROL REGISTER (RCR) 171 | ... 172 | ROM:2C1D9 bset #0,x:<<$FFFFB7 173 | 174 | '{:b}'.format(0x40201) # RCCR 175 | '100 00000010 00000001' 176 | bit 1: RPM0 -> prescale divider = 2 177 | bit 9: RDC0 Rx -> frame rate divider = 2 178 | bit 18: RCKP -> polarity 179 | 180 | >>> '{:b}'.format(0x7D00) # RCR 181 | '1111101 00000000' 182 | bit 8: RMOD0 -> Network Mode (RDC{0:4} == 0x1) 183 | bit 10: RSWS0 \ 184 | bit 11: RSWS1 \ 185 | bit 12: RSWS2 => Slot length: 32, Word length: 24 186 | bit 13: RSWS3 / 187 | bit 14: RSWS4 / 188 | ``` 189 | 190 | ### Configure ESAI Transmit 191 | Connection to: Digital to Analog Converter 192 | ``` 193 | ROM:2C1C4 movep #$440200,x:<>> '{:b}'.format(0x440200) 203 | '1000100 00000010 00000000' 204 | bit 9: TDC0 Tx -> frame rate divider = 2 205 | bit 18: TCKP -> polarity 206 | bit 22: TFSD -> FST as output 207 | 208 | >>> '{:b}'.format(0xD07D07) 209 | '11010000 01111101 00000111' 210 | bit 0: TE0 211 | bit 1: TE1 212 | bit 2: TE2 213 | bit 8: TMOD0 => Network Mode (TDC{0-4} == 0x1) 214 | bit 10: TSWS0 \ 215 | bit 11: TSWS1 \ 216 | bit 12: TSWS2 => Slot length: 32, Word length: 24 217 | bit 13: TSWS3 / 218 | bit 14: TSWS4 / 219 | bit 20: TEIE => Transmit Exception Interrupt Enable 220 | bit 22: TIE => Transmit Interrupt Enable 221 | bit 23: TLIE => Transmit Last Slot Interrupt Enable 222 | ``` 223 | 224 | ### Configure ESAI Status 225 | Connection to: Digital to Analog Converter 226 | ``` 227 | ROM:0DC0 bclr #14,x:< SAISR Transmit Frame Sync Flag 235 | bit 14: TUE => SAISR Transmit Underrun Error Flag 236 | ``` 237 | 238 | ### Configure Port C 239 | According to DSP56362.pdf, Port C is used for ESAI. 240 | ``` 241 | ROM:00F1 bset #2,x:< 33 [connected] MOSI -> 143 -- 276 | * HA2 -> 31 -- SS -> 2 -- 277 | * SDA -> 144 [connected] MISO -> 144 [connected] 278 | * SCL -> 1 [connected] SCK -> 1 [connected] 279 | * HREQ -> 3 -- HREQ -> 3 -- 280 | ``` 281 | 282 | ### JTAG / OnCE 283 | ``` 284 | * TCK -> 141 [ connected ] Test Clock 285 | * TDI -> 140 [ connected ] Test Data In 286 | * TDO -> 139 [ -- ] Test Data Out 287 | * TMS -> 142 [ -- ] Test Mode Select 288 | * TRST -> 138 [ connected ] Test Reset (Optional) 289 | * DE -> 53 [ connected ] Debug Event (In/Out) ! not 5v tolerant ! 290 | ``` -------------------------------------------------------------------------------- /access_virus/virus_ti/README.md: -------------------------------------------------------------------------------- 1 | # VirusTI Install Extraction Tool 2 | 3 | This tool will help extract files from the Access Virus TI 4 | installation media. 5 | 6 | You will end up with at least two ROM files: 7 | 8 | * F.bin: contains the flash dump for the Access Virus TI EEPROM 9 | * P.bin: contains preset data 10 | 11 | ## Requirements 12 | 13 | * python3 14 | 15 | ## Extracting firmware files 16 | 17 | Extract all the files from `firmware_bin64` to `./output64/` folder: 18 | 19 | ```bash 20 | $ python3 firmware.py firmware_bin64 output64 21 | INFO:__main__:Opening firmware file firmware_bin64 22 | INFO:__main__:Writing entry lcd_backup_000.bin 23 | ... (omitted for brevity) 24 | INFO:__main__:Writing entry vti.bin 25 | INFO:__main__:Writing entry vti_2.bin 26 | INFO:__main__:Writing entry vti_snow.bin 27 | INFO:__main__:Writing entry Changes.htm 28 | INFO:__main__:Writing entry Welcome.htm 29 | INFO:__main__:Writing entry BTN_Help.png 30 | INFO:__main__:Writing entry KemperAmps-Banner.png 31 | INFO:__main__:Writing entry KemperAmps-Head.png 32 | INFO:__main__:Writing entry vcstyles.css 33 | INFO:__main__:Successfully wrote chunks to folder output64 34 | ``` 35 | 36 | ## Extracting VTI binary files 37 | 38 | ```bash 39 | $ python3 vti.py output64/vti.bin output64_vti/ 40 | INFO:__main__:Opening vti file output64/vti.bin 41 | INFO:__main__:Successfully wrote ROM to output64_vti//F.bin 42 | INFO:__main__:Successfully wrote ROM to output64_vti//P.bin 43 | ``` 44 | 45 | ## Known issues 46 | 47 | The vti_2.bin file seems to use a different format for P.bin. 48 | This will result in an error when running `vti.py`. 49 | See the code for more details. 50 | 51 | Extraction of F.bin and S.bin will still work. 52 | -------------------------------------------------------------------------------- /access_virus/virus_ti/chunks.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | import logging 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | @dataclass 8 | class InstallChunk: 9 | name: str 10 | size: int 11 | data: bytes 12 | 13 | 14 | def get_chunks(fp): 15 | """ Helper function to deal with the install chunk format. """ 16 | chunks = [] 17 | file_id = fp.read(4).decode() 18 | file_size = int.from_bytes(fp.read(4), 'big') 19 | logger.debug('Reading %s, size=0x%x', file_id, file_size) 20 | 21 | while fp.tell() < file_size: 22 | chunk_id = fp.read(4).decode() 23 | chunk_size = int.from_bytes(fp.read(4), 'big') 24 | data = fp.read(chunk_size) 25 | logger.debug("Chunk %s, size=0x%x", chunk_id, chunk_size) 26 | chunks.append(InstallChunk(name=chunk_id, size=chunk_size, data=data)) 27 | 28 | return chunks 29 | -------------------------------------------------------------------------------- /access_virus/virus_ti/firmware.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | from chunks import get_chunks 6 | 7 | logging.basicConfig(level=logging.INFO) 8 | logger = logging.getLogger(__name__) 9 | 10 | """ 11 | Extraction tool for VirusTI firmware files. 12 | 13 | Usage: python3 firmware.py 14 | 15 | Can be used with firmware_bin and firmware_bin64 from 16 | Virus TI installation media. 17 | """ 18 | 19 | 20 | if len(sys.argv) != 3: 21 | logger.info("Usage: python3 %s ", sys.argv[0]) 22 | sys.exit(-1) 23 | 24 | logger.info("Opening firmware file %s", sys.argv[1]) 25 | 26 | # Create output folder 27 | try: 28 | os.mkdir(sys.argv[2]) 29 | except FileExistsError: 30 | logger.error("Output folder %s already exists", sys.argv[2]) 31 | sys.exit(-1) 32 | 33 | # Parse chunks from firmware file 34 | with open(sys.argv[1], "rb") as fp: 35 | chunks = get_chunks(fp) 36 | 37 | # Get file table 38 | tables = [c for c in chunks if c.name == "TABL"] 39 | if not tables: 40 | logger.error("TABL not found, did you supply a firmware file?") 41 | sys.exit(-1) 42 | 43 | # Parse file table 44 | tabl = tables[0] 45 | count = tabl.data[0] 46 | entries = [e.decode() for e in tabl.data[1:].split(b"\0") if e] 47 | assert len(entries) == count 48 | 49 | # Write entries to files 50 | for idx, entry in enumerate(entries): 51 | logger.info("Writing entry %s", entry) 52 | chunk = chunks[idx+1] 53 | with open("%s/%s" % (sys.argv[2], entry), "wb") as fpout: 54 | fpout.write(chunk.data) 55 | 56 | logger.info("Successfully wrote chunks to folder %s", sys.argv[2]) 57 | -------------------------------------------------------------------------------- /access_virus/virus_ti/vti.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | from chunks import get_chunks 6 | 7 | logging.basicConfig(level=logging.INFO) 8 | logger = logging.getLogger(__name__) 9 | 10 | """ 11 | Extraction tool for VirusTI vti bin files. 12 | 13 | Usage: python3 firmware.py 14 | 15 | Can be used with vti.bin, vti2.bin and vti_snow.bin which 16 | can be extracted from the Virus TI installation media 17 | using firmware.py. 18 | 19 | Known issue: 20 | With vti_2.bin, the extraction tool does not work properly 21 | for the P.bin ROM. It seems that it uses a different format, 22 | maybe concatenating the files will be sufficient. 23 | The F.bin and S.bin can still be extracted though. 24 | """ 25 | 26 | 27 | if len(sys.argv) != 3: 28 | logger.info("Usage: python3 %s ", sys.argv[0]) 29 | sys.exit(-1) 30 | 31 | logger.info("Opening vti file %s", sys.argv[1]) 32 | 33 | # Create output folder 34 | try: 35 | os.mkdir(sys.argv[2]) 36 | except FileExistsError: 37 | logger.error("Output folder %s already exists", sys.argv[2]) 38 | sys.exit(-1) 39 | 40 | # Parse chunks from firmware file 41 | with open(sys.argv[1], "rb") as fp: 42 | chunks = get_chunks(fp) 43 | 44 | # Extract the different ROMs 45 | for rom_prefix in ['F', 'S', 'P']: 46 | parts = [c for c in chunks if c.name.startswith(rom_prefix)] 47 | if not parts: 48 | logger.debug("Skipping not included ROM %s.bin", rom_prefix) 49 | continue 50 | 51 | output = b'' 52 | for chunk in parts: 53 | assert chunk.size % 0x23 == 2 # 2 bytes extra at the end (checksum?) 54 | logger.debug("Processing chunk %s", chunk.name) 55 | idx = 0 56 | for i in range(0, chunk.size - 2, 0x23): 57 | int.from_bytes(chunk.data[i:i+1], 'big') # chunk id? 58 | header = int.from_bytes(chunk.data[i+1:i+3], 'big') 59 | assert idx == header 60 | output += chunk.data[i+3:i+0x23] 61 | idx += 32 62 | 63 | output_path = "%s/%s.bin" % (sys.argv[2], rom_prefix) 64 | with open(output_path, "wb") as fpout: 65 | fpout.write(output) 66 | 67 | logger.info("Successfully wrote ROM to %s", output_path) 68 | -------------------------------------------------------------------------------- /dsp56k/be2le.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import logging 4 | import pathlib 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | logger = logging.getLogger(__name__) 8 | 9 | """ 10 | Convert big endian to little endian for 24bit files. 11 | """ 12 | 13 | if len(sys.argv) != 3: 14 | logger.info("Usage: python3 %s ", sys.argv[0]) 15 | sys.exit(-1) 16 | 17 | # Make sure the input file exists 18 | src = pathlib.Path(sys.argv[1]) 19 | if not src.exists(): 20 | logger.error("Input file %s does not exist!", src) 21 | sys.exit(-1) 22 | 23 | file_size = src.stat().st_size 24 | if file_size % 3 != 0: 25 | logger.error("Expected a multiple of 24-bit words, size=%d", file_size) 26 | sys.exit(-1) 27 | 28 | # Convert into little endian 29 | file_path = sys.argv[2] 30 | with open(file_path, "wb") as output: 31 | with open(sys.argv[1], "rb") as fp: 32 | for i in range(0, file_size // 3): 33 | word = int.from_bytes(fp.read(3), 'big') 34 | output.write(word.to_bytes(3, 'little')) 35 | 36 | logger.info("Successfully wrote %s", file_path) 37 | -------------------------------------------------------------------------------- /dsp56k/bin2io.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import logging 4 | import pathlib 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | logger = logging.getLogger(__name__) 8 | 9 | """ 10 | Convert binary image to an I/O stream that can be used 11 | by the DSP56k simulator to provide peripheral input. 12 | 13 | An optional argument `repeat` allows you to repeat 14 | each value multiple times. 15 | 16 | Example for simulator: 17 | load program.lod 18 | input x:$FFFFC6 stream.io 19 | """ 20 | 21 | if len(sys.argv) < 3: 22 | logger.info("Usage: python3 %s [repeat]", sys.argv[0]) 23 | sys.exit(-1) 24 | 25 | # Make sure the input file exists 26 | src = pathlib.Path(sys.argv[1]) 27 | if not src.exists(): 28 | logger.error("Input file %s does not exist!", src) 29 | sys.exit(-1) 30 | 31 | # Check if repeat argument is a valid number 32 | repeat = 1 33 | if len(sys.argv) > 3: 34 | repeat = int(sys.argv[3], 10) 35 | 36 | file_size = src.stat().st_size 37 | if file_size % 3 != 0: 38 | logger.error("Expected a multiple of 24-bit words, size=%d", file_size) 39 | sys.exit(-1) 40 | 41 | # Convert into IO 42 | file_path = sys.argv[2] 43 | with open(file_path, "wb") as lod: 44 | with open(sys.argv[1], "rb") as fp: 45 | for i in range(0, file_size // 3): 46 | w = int.from_bytes(fp.read(3), 'big') 47 | 48 | # Simulator supports # suffix to repeat a value 49 | repeat_suffix = b"" 50 | if repeat > 1: 51 | repeat_suffix = b"#%d" % repeat 52 | 53 | # Write the word 54 | lod.write(b'$%06x%s\n' % (w, repeat_suffix)) 55 | 56 | logger.info("Successfully wrote %s", file_path) 57 | -------------------------------------------------------------------------------- /dsp56k/bin2lod.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import logging 4 | import pathlib 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | logger = logging.getLogger(__name__) 8 | 9 | """ 10 | Convert binary image to an OMF file that can be loaded 11 | by the DSP56k simulator as source binary. 12 | """ 13 | 14 | if len(sys.argv) < 3: 15 | logger.info("Usage: python3 %s [hex_offset]", sys.argv[0]) 16 | sys.exit(-1) 17 | 18 | # Make sure the input file exists 19 | src = pathlib.Path(sys.argv[1]) 20 | if not src.exists(): 21 | logger.error("Input file %s does not exist!", src) 22 | sys.exit(-1) 23 | 24 | # Check the offset 25 | offset = 0 26 | if len(sys.argv) > 3: 27 | offset = int(sys.argv[3], 16) 28 | 29 | file_size = src.stat().st_size 30 | if file_size % 3 != 0: 31 | logger.error("Expected a multiple of 24-bit words, size=%d", file_size) 32 | sys.exit(-1) 33 | 34 | # Convert into OMF 35 | file_path = sys.argv[2] 36 | with open(file_path, "wb") as lod: 37 | lod.write(b'_DATA P %06x\n' % offset) 38 | with open(sys.argv[1], "rb") as fp: 39 | for i in range(0, file_size // 3): 40 | w = int.from_bytes(fp.read(3), 'big') 41 | lod.write(b'%06x ' % w) 42 | if (i + 1) % 8 == 0: 43 | lod.write(b'\n') 44 | lod.write(b'\n\n') 45 | lod.write(b'_END 000000\n') 46 | 47 | logger.info("Successfully wrote %s", file_path) 48 | -------------------------------------------------------------------------------- /dsp56k/lod2bin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import logging 4 | import pathlib 5 | from dataclasses import dataclass 6 | from typing import List 7 | 8 | logging.basicConfig(level=logging.INFO) 9 | logger = logging.getLogger(__name__) 10 | 11 | PAD_CHAR = b'\0' # padding used between non-consecutive sections in one region 12 | 13 | """ 14 | Convert an OMF file into a binary image that can be loaded 15 | into a disassembler. 16 | 17 | Will create files for each region that exists in the OMF. 18 | """ 19 | 20 | if len(sys.argv) != 3: 21 | logger.info("Usage: python3 %s ", sys.argv[0]) 22 | sys.exit(-1) 23 | 24 | 25 | @dataclass 26 | class Section: 27 | name: str 28 | offset: int 29 | data: List[int] 30 | 31 | 32 | # Make sure the input file exists 33 | src = pathlib.Path(sys.argv[1]) 34 | if not src.exists(): 35 | logger.error("Input file %s does not exist!", src) 36 | sys.exit(-1) 37 | 38 | # Parse all the sections 39 | sections = [] 40 | with open(sys.argv[1], "rb") as fp: 41 | lines = fp.read().decode().split("\n") 42 | 43 | section = None 44 | for line in lines: 45 | line = line.strip() 46 | if not line: 47 | continue # skip empty lines 48 | 49 | if line.startswith("_"): 50 | if section: 51 | # Flush previous section 52 | sections.append(section) 53 | 54 | # Section start 55 | if line.startswith("_DATA"): 56 | _, name, offset = line.split(" ") 57 | section = Section(name, int(offset, 16), []) 58 | else: 59 | words = line.split(" ") 60 | for word in words: 61 | assert 0 < len(word) <= 6 62 | section.data.append(int(word, 16)) 63 | 64 | # Get unique section names 65 | section_names = set([s.name for s in sections]) 66 | for name in section_names: 67 | subsections = sorted([s for s in sections if s.name == name], key=lambda s: s.offset) 68 | 69 | file_path = "%s.%s.bin" % (sys.argv[2], name) 70 | with open(file_path, "wb") as fp: 71 | for section in subsections: 72 | logger.info("Section %s, offset=0x%x, size=0x%x", section.name, section.offset, len(section.data)) 73 | fp.write(PAD_CHAR * ((section.offset * 3) - fp.tell())) # write padding 74 | print('0x%x' % fp.tell()) 75 | for word in section.data: 76 | fp.write(word.to_bytes(3, 'big')) 77 | 78 | logger.info("Successfully wrote %s", file_path) 79 | -------------------------------------------------------------------------------- /dsp56k/patch_omr_sr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import logging 3 | import pathlib 4 | import sys 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | logger = logging.getLogger(__name__) 8 | 9 | OMR_MASK = 0xffffff ^ 0x4000 # disable address priority 10 | SR_MASK = 0xffffff ^ 0x80000 # disable instruction cache 11 | 12 | """ 13 | Binary patch utility for DSP56k binary files. 14 | 15 | This utility will change the instructions that change the OMR 16 | and SR register so that the Motorola Simulator will work 17 | properly. 18 | 19 | Usage: python3 patch_omr_sr.py 20 | 21 | """ 22 | 23 | if len(sys.argv) != 3: 24 | logger.info("Usage: python3 %s ", sys.argv[0]) 25 | sys.exit(-1) 26 | 27 | # Make sure the input file exists 28 | src = pathlib.Path(sys.argv[1]) 29 | if not src.exists(): 30 | logger.error("Input file %s does not exist!", src) 31 | sys.exit(-1) 32 | 33 | file_size = src.stat().st_size 34 | if file_size % 3 != 0: 35 | logger.error("Expected a multiple of 24-bit words, size=%d", file_size) 36 | sys.exit(-1) 37 | 38 | # Instruction mapping 39 | OMR = b"\x05\xf4\x3a" # move OP,omr 40 | SR = b"\x05\xf4\x39" # move OP,sr 41 | 42 | file_path = pathlib.Path(sys.argv[2]) 43 | with open(file_path, "wb") as output: 44 | with open(src, "rb") as fp: 45 | while fp.tell() < file_size: 46 | word = fp.read(3) 47 | if word == OMR: 48 | # Patch OMR 49 | operand = int.from_bytes(fp.read(3), 'big') 50 | patch = operand & OMR_MASK 51 | output.write(word) 52 | output.write(patch.to_bytes(3, 'big')) 53 | logger.info("Patched OMR from 0x%06x to 0x%06x", operand, patch) 54 | elif word == SR: 55 | # Patch SR 56 | operand = int.from_bytes(fp.read(3), 'big') 57 | patch = operand & SR_MASK 58 | output.write(word) 59 | output.write(patch.to_bytes(3, 'big')) 60 | logger.info("Patched SR from 0x%06x to 0x%06x", operand, patch) 61 | else: 62 | # Nothing to do, just forward the input data 63 | output.write(word) 64 | 65 | logger.info("Successfully wrote %s", file_path) 66 | --------------------------------------------------------------------------------