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)
- 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):
0xC000 | (cc<<8) | 0x00+ 16-bit displacement - JR cc (short):
0xC000 | (cc<<8) | disp8(8-bit signed displacement) - JA cc (absolute):
0xC000 | (cc<<8) | 0x80+ 32-bit address
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
-
jaccfor 16-bit displacement JR -- dis34 outputsjacc(notjrcc) for the 16-bit displacement form of conditional jumps. This ensures as34 emits the same 4-byte encoding. The 8-bit short form usesjrcc. The 32-bit absolute form usesjacc. -
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.