# TMS34010 Disassembler Project This is a (hopefully portable) TMS34010 disassembler which has built-in options to 'intelligently' analyze ROM contents and determine which areas are most likely to be executed, and then disassembles the corresponding areas as code or data. Also include are a couple of options to load multiple ROMs in 16- and 8-bit format. The skeleton with the code base and heuristics was human-written, but I had Claude Code fill in the gaps and test it extensively. The output format can be assembled with as34, which is a TMS34010 assembler that (as far as I remember) I got from a Texas Instruments' SDK/Devkit in the early 2000's. --- ## dis34 Usage ``` dis34 [-a] [-b base_addr] [-s skip] [file] dis34 [-a] -e file1 [file2 ...] dis34 [-a] -E lo1 hi1 [lo2 hi2 ...] -a analyze mode: heuristic code/data separation -b base bit-address (hex, default 0) -e EPROM mode: concatenate 16-bit files, last byte at top of address space -E 8-bit EPROM mode: interleave pairs (lo,hi), last byte at top -s skip bytes at start of file -p import code entry points from file (one hex address per line) -P export discovered entry points to file -l show address prefix on each output line (off by default) -o show opcode bytes as comment after each instruction -h show help reads stdin if no file given ``` Examples: ``` ./dis34 -s 80 -b fff00000 somefile.bin ./dis34 -a -E u4.bin u7.bin # single pair ./dis34 -a -E u4.bin u7.bin u6.bin u9.bin # multiple pairs ./dis34 -a -P points.txt -E u4.bin u7.bin # export entry points ./dis34 -a -p points.txt -E u4.bin u7.bin # import entry points ``` --- ## TMS34010 Instruction Set Architecture ### Registers - **A-file**: a0-a14, accessed when register bit 4 = 0 - **B-file**: b0-b14, accessed when register bit 4 = 1 - **sp**: register 15 in either file (stack pointer), encoded as `0x0F` (A15, printed `sp`) or `0x1F` (B15, printed `b15`). Both are the same physical stack pointer, but the encoding names a different file; a disassembler must not collapse them. - Register encoding: 5-bit field, bit 4 = file select, bits 3:0 = register number ### Instruction Word Format All instructions are 16-bit words (little-endian). Multi-word instructions follow with 16-bit or 32-bit immediates/addresses. ``` Bits 15:9 - Primary opcode (op & 0xFE00) Bits 8:5 - Sub-opcode or source register (RS) Bits 4:0 - Destination register (RD), bit 4 = file select Bit 9 - Field selector for MOVE/SETF (0 or 1) ``` ### Primary Decode: `op & 0xFE00` | Range | Instructions | |-------|-------------| | 0x0000 | rev, emu, exgpc, getpc, jump, getst, putst, popst, pushst | | 0x0200 | nop, clrc, movb @,@, dint, abs, neg, negb, not | | 0x0400-0x0600 | sext, zext, setf, move r/@, move @/r, move @/@, movb r/@, move @/r (with field) | | 0x0800 | trap, call, reti, rets, mmtm, mmfm, moviw, movi (32-bit) | | 0x0A00 | addiw, addi, cmpiw, cmpi, andi, ori, xori, subiw | | 0x0C00 | subi (32-bit), callr, calla, eint, dsj, dsjeq, dsjne, setc | | 0x0E00 | pixblt l/xy/b variants, fill l/xy | | 0x1000-0x1200 | addk / inc (k=1) | | 0x1400-0x1600 | subk / dec (k=1) | | 0x1800-0x1A00 | movk | | 0x1C00-0x1E00 | btst (K-form, 1's complement) | | 0x2000-0x2200 | sla (K-form) | | 0x2400-0x2600 | sll (K-form) | | 0x2800-0x2A00 | sra (K-form, 2's complement: 32-stored) | | 0x2C00-0x2E00 | srl (K-form, 2's complement: 32-stored) | | 0x3000-0x3200 | rl (K-form) | | 0x3800-0x3E00 | dsjs (5-bit displacement, bit 10 = negate) | | 0x4000-0x6E00 | Register-register ALU (add, sub, cmp, and, or, xor, shift, etc.) | | 0x8000-0xBC00 | MOVE/MOVB addressing modes | | 0xC000-0xCE00 | Conditional jumps (JR/JA) | | 0xD000-0xD600 | MOVE *rs(d),*rd+; MOVE @abs,*rd+; exgf | | 0xDE00 | LINE 0 / LINE 1 | | 0xE000-0xEE00 | addxy, subxy, cmpxy, cpw, cvxyl, movx, movy | | 0xF000-0xFC00 | PIXT / drav | ### Complete Opcode Table #### Register-Register (REGREG): `opval | (rs<<5) | rd` | Mnemonic | Opval | Notes | |----------|-------|-------| | add | 0x4000 | | | addc | 0x4200 | | | sub | 0x4400 | | | subb | 0x4600 | | | cmp | 0x4800 | | | btst | 0x4A00 | Register form | | and | 0x5000 | | | andn | 0x5200 | | | or | 0x5400 | | | xor | 0x5600 | clr when rs==rd | | divs | 0x5800 | | | divu | 0x5A00 | | | mpys | 0x5C00 | | | mpyu | 0x5E00 | | | sla | 0x6000 | Register form | | sll | 0x6200 | Register form | | sra | 0x6400 | Register form | | srl | 0x6600 | Register form | | rl | 0x6800 | Register form | | lmo | 0x6A00 | | | mods | 0x6C00 | | | modu | 0x6E00 | | | addxy | 0xE000 | | | subxy | 0xE200 | | | cmpxy | 0xE400 | | | cpw | 0xE600 | | | cvxyl | 0xE800 | | | movx | 0xEC00 | | | movy | 0xEE00 | | | drav | 0xF600 | | #### One-Register (ONEREG): `opval | rd` | Mnemonic | Opval | |----------|-------| | rev | 0x0020 | | exgpc | 0x0120 | | getpc | 0x0140 | | jump | 0x0160 | | getst | 0x0180 | | putst | 0x01A0 | | abs | 0x0380 | | neg | 0x03A0 | | negb | 0x03C0 | | not | 0x03E0 | | call | 0x0920 | | inc | 0x1020 | | dec | 0x1420 | #### Implied (no operands) | Mnemonic | Opval | |----------|-------| | emu | 0x0100 | | popst | 0x01C0 | | pushst | 0x01E0 | | nop | 0x0300 | | clrc | 0x0320 | | dint | 0x0360 | | reti | 0x0940 | | eint | 0x0D60 | | setc | 0x0DE0 | | pixblt l,l | 0x0F00 | | pixblt l,xy | 0x0F20 | | pixblt xy,l | 0x0F40 | | pixblt xy,xy | 0x0F60 | | pixblt b,l | 0x0F80 | | pixblt b,xy | 0x0FA0 | | fill l | 0x0FC0 | | fill xy | 0x0FE0 | | line 0 | 0xDF1A | | line 1 | 0xDF9A | #### Immediate-Long + Register (ILREG): `opval | rd`, then 32-bit immediate | Mnemonic | Opval | Notes | |----------|-------|-------| | addi | 0x0B20 | Also word form at 0x0B00 | | movi | 0x09E0 | Also word form at 0x09C0 | | ori | 0x0BA0 | | | xori | 0x0BC0 | | #### Negated Immediate-Long + Register (NILREG): stored as ~value | Mnemonic | Opval | Notes | |----------|-------|-------| | andi | 0x0B80 | 1's complement | | cmpi | 0x0B60 | 1's complement; also word form at 0x0B40 | | subi | 0x0D00 | 1's complement; also word form at 0x0BE0 | #### Constant + Register (KREG): `opval | (k<<5) | rd`, K range 1-32 (0=32) | Mnemonic | Opval | Notes | |----------|-------|-------| | addk | 0x1000 | inc when k=1 | | subk | 0x1400 | dec when k=1 | | movk | 0x1800 | | #### Constant/Register dual-form (KREGORREGREG) | Mnemonic | K-form | R-form | K encoding | |----------|--------|--------|------------| | sla | 0x2000 | 0x6000 | Direct | | sll | 0x2400 | 0x6200 | Direct | | rl | 0x3000 | 0x6800 | Direct | #### Negated Constant/Register dual-form (NKREGORREGREG) | Mnemonic | K-form | R-form | K encoding | |----------|--------|--------|------------| | btst | 0x1C00 | 0x4A00 | 1's complement: (~op>>5)&0x1F | | sra | 0x2800 | 0x6400 | 2's complement: 32-((op>>5)&0x1F) | | srl | 0x2C00 | 0x6600 | 2's complement: 32-((op>>5)&0x1F) | #### Special Instructions | Mnemonic | Opval | Format | |----------|-------|--------| | callr | 0x0D3F | Fixed opcode + 16-bit PC-relative displacement | | calla | 0x0D5F | Fixed opcode + 32-bit absolute address | | mmtm | 0x0980 | opval\|rd + 16-bit register bitmask (MSB=reg0) | | mmfm | 0x09A0 | opval\|rd + 16-bit register bitmask (LSB=reg0) | | trap | 0x0900 | opval\|(trap_num & 0x1F) | | rets | 0x0960 | opval\|(pop_count & 0x1F) | | setf | 0x0540/0x0740 | opval\|fs\|(fe<<5), field in bit 9 | | exgf | 0xD500 | opval\|rd, field in bit 9 | | dsj | 0x0D80 | opval\|rd + 16-bit displacement | | dsjeq | 0x0DA0 | opval\|rd + 16-bit displacement | | dsjne | 0x0DC0 | opval\|rd + 16-bit displacement | | dsjs | 0x3800+ | opval\|rd\|(disp<<5), bit 10 = negate | ### MOVE Addressing Modes (18 variants) | Opval | Format | Field | |-------|--------|-------| | 0x4C00 | move rs, rd | No | | 0x4E00 | move rs, rd (cross-file) | No | | 0x8000/0x8200 | move rs, *rd | Yes | | 0x8400/0x8600 | move *rs, rd | Yes | | 0x8800/0x8A00 | move *rs, *rd | Yes | | 0x9000/0x9200 | move rs, *rd+ | Yes | | 0x9400/0x9600 | move *rs+, rd | Yes | | 0x9800/0x9A00 | move *rs+, *rd+ | Yes | | 0xA000/0xA200 | move rs, -*rd | Yes | | 0xA400/0xA600 | move -*rs, rd | Yes | | 0xA800/0xAA00 | move -*rs, -*rd | Yes | | 0xB000/0xB200 | move rs, *rd(disp) | Yes | | 0xB400/0xB600 | move *rs(disp), rd | Yes | | 0xB800/0xBA00 | move *rs(d), *rd(d) | Yes | | 0xD000/0xD200 | move *rs(d), *rd+ | Yes | | 0x0580/0x0780 | move rd, @addr | Yes | | 0x05A0/0x07A0 | move @addr, rd | Yes | | 0x05C0/0x07C0 | move @addr1, @addr2 | Yes | | 0xD400/0xD600 sub0 | move @addr, *rd+ | Yes | "Field" column: `Yes` = appends `, 0` or `, 1` to select field register. Paired opcodes differ by bit 9 (field select): low = field 0, high = field 1. ### MOVB Addressing Modes (9 variants) | Opval | Format | |-------|--------| | 0x8C00 | movb rs, *rd | | 0x8E00 | movb *rs, rd | | 0x9C00 | movb *rs, *rd | | 0xAC00 | movb rs, *rd(disp) | | 0xAE00 | movb *rs(disp), rd | | 0xBC00 | movb *rs(d), *rd(d) | | 0x0340 | movb @addr1, @addr2 | | 0x05E0 | movb rd, @addr | | 0x07E0 | movb @addr, rd | ### PIXT Addressing Modes (6 variants) | Opval | Format | |-------|--------| | 0xF000 | pixt rs, *rd, xy | | 0xF200 | pixt *rs, xy, rd | | 0xF400 | pixt *rs, xy, *rd, xy | | 0xF800 | pixt rs, *rd | | 0xFA00 | pixt *rs, rd | | 0xFC00 | pixt *rs, *rd | ### Conditional Jump Encoding Condition code in bits 11:8 of the instruction word: | CC | Code | Meaning | |----|------|---------| | 0 | uc | Unconditional | | 1 | p | Plus (positive) | | 2 | ls | Low or same | | 3 | hi | High | | 4 | lt | Less than | | 5 | ge | Greater or equal | | 6 | le | Less or equal | | 7 | gt | Greater than | | 8 | c | Carry | | 9 | nc | No carry | | 10 | eq | Equal (zero) | | 11 | ne | Not equal | | 12 | v | Overflow | | 13 | nv | No overflow | | 14 | n | Negative | | 15 | nn | Not negative | Jump forms: - **JR cc** (relative, 2 words): `0xC000 | (cc<<8) | 0x00` + 16-bit displacement - **JR cc** (short, 2 words): `0xC000 | (cc<<8) | disp8` (8-bit signed displacement) - **JA cc** (absolute, 3 words): `0xC000 | (cc<<8) | 0x80` + 32-bit address Only low byte `0x80` is the absolute form. Low byte `0x00` carries a 16-bit *displacement* and is still `JRcc`; the word count must not be used to invent an absolute mnemonic for it. Displacement formula: - 16-bit: `target = pc + 32 + (disp << 4)` - 8-bit: `target = pc + 16 + (disp << 4)` ### The `issrl` Flag and NKREGORREGREG K-Encoding The NKREGORREGREG instruction class (BTST, SRA, SRL) uses negated K constants, but the negation method differs between instructions, controlled by the `issrl` flag in the as34 assembler: | Instruction | `issrl` | Negation | Encode formula | Decode formula | |-------------|---------|----------|----------------|----------------| | BTST | 0 | 1's complement | `(~K) & 0x1F` | `(~stored) & 0x1F` | | SRA | 1 | 2's complement | `(-K) & 0x1F` | `32 - stored` | | SRL | 1 | 2's complement | `(-K) & 0x1F` | `32 - stored` | The two methods produce different stored values for every K from 1-31: ``` K: 1 2 3 ... 27 28 29 30 31 1's: 30 29 28 ... 4 3 2 1 0 2's: 31 30 29 ... 5 4 3 2 1 ``` Using the wrong decode gives K±1 errors. In `dis34.c`: - BTST (0x1C00): `(~op >> 5) & 0x1f` — 1's complement — correct - SRA (0x2800): `32 - ((op >> 5) & 0x1f)` — 2's complement — correct - SRL (0x2C00): `32 - ((op >> 5) & 0x1f)` — 2's complement — correct --- ### Round-Trip Design Decisions 1. **`jrcc` for both relative forms** -- the 8-bit and 16-bit displacement forms are both `JRcc` (2 words); only the 3-word `0x80` form is `JAcc`. (Until 2026-10-18 the 16-bit form was printed as `jacc`, which made a linear disassembly's control flow wrong and forced the round-trip converter to rewrite it back to `jrcc`.) 2. **Branch-only labels** -- Only branch/call targets (jr, ja, callr, calla, dsj, dsjeq, dsjne, dsjs) get labels. MOVI and other data-reference instructions keep literal hex addresses to avoid address shifts when code section size changes. 3. **Raw data section** -- dis2asm.py reads the data section directly from the binary (-i flag) rather than from dis34 output, because the disassembler may misinterpret data words as instructions. ### Known Formatting Differences from tos.asm These are formatting-only differences that do not affect binary output: 1. **`move a0, a1` vs `move a0, a1, 1`** -- Field specifier not encoded in reg-to-reg MOVE; assembler accepts but ignores it 2. **Hex vs decimal** -- dis34 always uses `0x` prefix; tos.asm mixes decimal and hex 3. **MMTM/MMFM register order** -- dis34 outputs registers in bit order (ascending); assembler accepts any order 4. **Data section** -- Disassembles as `.word` directives and garbage instructions (expected for a linear disassembler) --- ## Analyze Mode (`-a`) Heuristic code/data separation using two passes: 1. **Pass 1 (sequential scan)**: Finds blocks of valid instructions terminated by unconditional transfers, validates them (min 4 instructions, has a branch, consistent register file usage, valid branch targets). 2. **Pass 2 (recursive descent)**: Seeds worklist from pass 1 branch targets and TMS34010 hardware vector table (traps 0-31 at 0xFFFFFFE0 downward). Traces execution paths, validating each block (min 2 instructions, register file consistency) before committing. ### Unified Address Space in EPROM Mode When multiple EPROM pairs are loaded (`-E lo1 hi1 lo2 hi2 ...`), the entire combined ROM is analyzed as a single unified address space. The last pair maps to the top of the 32-bit address space, where the TMS34010 hardware vector table lives (0xFFFFFFE0 downward). Branches between pairs are followed normally, allowing the recursive descent to trace execution across the full ROM. ### Heuristic Filters - **Register file consistency**: Real TMS34010 code uses either A-file or B-file registers consistently. Blocks with >=25% minority register file usage are rejected as likely data. - **Minimum trace length**: Single-instruction traces from recursive descent are rejected to prevent cascading false positives. ### Entry Point Import/Export (`-p` / `-P`) The `-P FILE` option exports all discovered code entry points (branch/call targets and vector table handlers) to a file, one hex address per line. This allows inspection and manual curation of the entry point list. The `-p FILE` option imports entry points from a file and uses them as additional seeds for the recursive descent analysis. The file format is one hex address per line; lines starting with `#` are comments, blank lines are ignored. Typical workflow: 1. Run with `-P points.txt` to export discovered entry points 2. Edit `points.txt` to add known code addresses or remove false positives 3. Re-run with `-p points.txt` to improve analysis coverage --- ## Labels and Vector Table ### Labels Branch and call targets are automatically labelled using the format `L` followed by the uppercase hexadecimal bit-address (e.g., `LFFF00820:`). Only addresses that are actually referenced by a branch, call, or vector table entry receive labels. The corresponding branch/call instructions use the label name instead of a raw hex address. ### Vector Table Display When the ROM maps to the top of the 32-bit address space (as in EPROM modes), the TMS34010 hardware vector table (traps 0–31 at 0xFFFFFFE0 downward) is displayed as labeled `.long` entries with standard vector names: ``` RESET: ffffffe0: .long LFFCEF750 INT1: ffffffc0: .long LFFD7D290 INT2: ffffffa0: .long LFFD7DDB0 ILLOP: fffffe80: .long LFF8632E0 ``` The handler addresses are shown as label references, and the corresponding handler code receives matching labels in the disassembly output. --- ## TMS34010 I/O Registers The TMS34010 has 28 memory-mapped I/O registers in the bit-address range `0xC0000000`–`0xC00001F0`, spaced 0x10 apart (16 bits each). The disassembler automatically annotates instructions that reference these addresses with the register name as a comment. | Bit Address | Register | Description | |----------------|----------|---------------------------------| | `0xC0000000` | HESYNC | Horizontal End Sync | | `0xC0000010` | HEBLNK | Horizontal End Blank | | `0xC0000020` | HSBLNK | Horizontal Start Blank | | `0xC0000030` | HTOTAL | Horizontal Total | | `0xC0000040` | VESYNC | Vertical End Sync | | `0xC0000050` | VEBLNK | Vertical End Blank | | `0xC0000060` | VSBLNK | Vertical Start Blank | | `0xC0000070` | VTOTAL | Vertical Total | | `0xC0000080` | DPYCTL | Display Control | | `0xC0000090` | DPYSTRT | Display Start Address | | `0xC00000A0` | DPYINT | Display Interrupt | | `0xC00000B0` | CONTROL | Control Register | | `0xC00000C0` | HSTDATA | Host Data | | `0xC00000D0` | HSTADRL | Host Address Low | | `0xC00000E0` | HSTADRH | Host Address High | | `0xC00000F0` | HSTCTLL | Host Control Low | | `0xC0000100` | HSTCTLH | Host Control High | | `0xC0000110` | INTENB | Interrupt Enable | | `0xC0000120` | INTPEND | Interrupt Pending | | `0xC0000130` | CONVSP | Conversion Source Pitch | | `0xC0000140` | CONVDP | Conversion Destination Pitch | | `0xC0000150` | PSIZE | Pixel Size | | `0xC0000160` | PMASK | Pixel/Plane Mask | | `0xC00001B0` | DPYTAP | Display Tap Point | | `0xC00001C0` | HCOUNT | Horizontal Counter (read-only) | | `0xC00001D0` | VCOUNT | Vertical Counter (read-only) | | `0xC00001E0` | DPYADR | Display Address | | `0xC00001F0` | REFCNT | DRAM Refresh Counter | Registers at `0xC0000170`–`0xC00001A0` are reserved/unused. References to these registers also serve as a heuristic signal in analyze mode: blocks that access I/O registers are almost certainly real code, so the validation requirements are relaxed for such blocks.