Files
SDD af7d3565a5 dis34: print long relative JR as jr and surface B-file SP as b15
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).
2026-10-18 00:54:34 +03:00
..

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.