The 16-bit-displacement jump (low byte 0x00, two words) is JRcc, not the three-word JAcc (low byte 0x80). Printing it as ja made linear disassembly report the wrong control flow and made every such branch grow by a word when reassembled. Register 15 is the stack pointer in either file: 0x0F is A15 and 0x1F is B15. The disassembler printed both as sp, hiding B-file stack-pointer code. Print b15 when the R bit selects the B file. Validated with the 1987 TI GSPA/GSPLNK/GSPROM round trip: the 32 KB region at bit 3F208000 still reassembles byte-for-byte (md5 43adb6669571041985f3e26fd9ff7cb9), as do the 4 KB and 16 KB spans and the 0x8000/3F240000 region. The converter no longer needs its ja->jr or sp->b15 rewrites (5 fixups -> 3); a whole-ROM diff contains only those two output categories. The ROM is unchanged (b46a3e442ccb86134d2356bd70d22fd2).
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, printedsp) or0x1F(B15, printedb15). 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
-
jrccfor both relative forms -- the 8-bit and 16-bit displacement forms are bothJRcc(2 words); only the 3-word0x80form isJAcc. (Until 2026-10-18 the 16-bit form was printed asjacc, which made a linear disassembly's control flow wrong and forced the round-trip converter to rewrite it back tojrcc.) -
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.
-
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:
move a0, a1vsmove a0, a1, 1-- Field specifier not encoded in reg-to-reg MOVE; assembler accepts but ignores it- Hex vs decimal -- dis34 always uses
0xprefix; tos.asm mixes decimal and hex - MMTM/MMFM register order -- dis34 outputs registers in bit order (ascending); assembler accepts any order
- Data section -- Disassembles as
.worddirectives and garbage instructions (expected for a linear disassembler)
Analyze Mode (-a)
Heuristic code/data separation using two passes:
-
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).
-
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:
- Run with
-P points.txtto export discovered entry points - Edit
points.txtto add known code addresses or remove false positives - Re-run with
-p points.txtto 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.