An ARM60 you can drive from a clock, held to its own datasheet for every instruction and every cycle it spends, and to a filtered corpus for the whole processor state after each one.
1 part, 62 quoted sentences placed on the page they cite, 424,220 recorded cases compared, 0 disagreements, 1,074 tests, 100% statement and branch coverage, no dependencies
from arm6 import Cpu, Memory
cpu = Cpu("arm60", Memory(image=(0xE3A0002A).to_bytes(4, "little"), fill=0)).reset()
spent = cpu.step()
print(f"{cpu.registers.read(0):#010x} in {spent} cycle, {cpu.spent}")0x0000002a in 1 cycle, Cycles(s=1, n=0, i=0, c=0)
One instruction, loading 42 into the first register. It cost one sequential cycle, which is what Table 20 says a data operation costs.
pip install git+https://github.com/gufranco/arm6-python.gitPython 3.12 or newer. Nothing else.
Everything a caller touches. Nothing else is public.
| Call | Does | Returns |
|---|---|---|
Cpu(model, memory=None) |
Builds the part. The model is required and there is no default | Cpu |
reset() |
Section 6.3.6, including the cycles it costs | the part, so the call chains |
step() |
One instruction | the cycles it cost |
run_for(cycles) |
A budget, which an instruction may overshoot | what was really spent |
run_until(check, limit) |
Steps while check(cpu) is false |
the part; RunLimit if the limit is hit |
held() |
Whether the part has stopped advancing the program | False, always, and the reason is below |
irq(), fiq() |
Offer a line and say whether it would be taken | bool |
cycles, steps |
Cycles since construction, instructions since the last reset | int |
spent, tally |
The last instruction's cost and the running total, in S, N, I and C | Cycles |
bus.cycles |
What the last instruction drove on the pins, row by row | list[Cycle] |
This is the one place this member behaves differently from its siblings, and the difference is the datasheet's rather than a preference.
step reports cycles. It will not tell you how long they took:
from arm6 import Cpu, Waits
cpu = Cpu("arm60", fill=0).reset()
cpu.step()
print(cpu.spent.ticks(Waits(sequential=1, nonsequential=3, internal=1, coprocessor=1)))1
Section 8.6 says mclk may be stretched without limit and that Nwait may insert whole cycles instead, and the pin table adds that Nwait may be tied HIGH in a system that needs none. So how long a cycle takes is a fact about the board. Asking for ticks without describing one is refused rather than answered:
from arm6 import Cpu, WaitsRequired
cpu = Cpu("arm60", fill=0).reset()
cpu.step()
try:
cpu.spent.ticks(None)
except WaitsRequired as refused:
print(str(refused)[:53])ARM60 puts memory timing on the Nwait pin and section
The integer step returns is the cycle count, which is also the tick count in the one configuration the pin table names outright: Nwait tied HIGH, one mclk per cycle.
step runs a whole instruction because that is the unit a program is written in. A board has no such unit, so Clock runs the part on a thread and lets it block where the cycle is spent:
from arm6 import Clock, Cpu, Memory
image = (0xEB000000).to_bytes(4, "little") + (0xE1A00001).to_bytes(4, "little") * 8
cpu = Cpu("arm60", Memory(image=image, fill=0), fill=0).reset()
with Clock(cpu) as clock:
clock.run_for(2)
print(clock.cycles, cpu.registers.pc, len(cpu.bus.cycles))2 8 2
Two cycles into a three-cycle branch with link: the counter has been redirected and the third cycle, which refills the pipeline from the destination, has not happened. The part's own run_for cannot stop there, because an instruction is not divisible and it would have spent all three.
One part, and a catalogue anyway:
from arm6 import MODELS, Cpu
print(sorted(MODELS))
print(Cpu("arm60").model.name, Cpu("arm6").model.name, Cpu("ARM60").model.name)['arm60']
arm60 arm60 arm60
arm60 is the name and arm6 is the alias it answers to. A name no model goes by is refused with every model there is listed, which is what a catalogue buys even when it holds one entry.
The core is the same in ARM60, ARM600 and ARM610: the ARM610 datasheet says outright that the CPU within ARM610 is the ARM6, and gives the ARM600 delta as five bus and packaging differences. The bus is not the same, and the bus is what this family models a clocked part down to. Only ARM60 has a document giving it cycle by cycle, so only ARM60 is here. The other three are in OPEN-QUESTIONS.md with what each of their documents would settle.
A word can be read rather than executed. A routine that waits for a byte to arrive and then reads it:
from arm6 import disassemble
words = (0xE92D4010, 0xE3A04101, 0xE5D40020, 0xE3100001, 0x0AFFFFFC, 0xE5D40000, 0xE8BD8010)
image = b"".join(word.to_bytes(4, "little") for word in words)
for one in disassemble(image, address=0x100, stop_at_return=True):
print(f"0x{one.address:06X} {one.word:08X} {one.text}")0x000100 E92D4010 stmdb SP!, {R4, LR}
0x000104 E3A04101 mov R4, #0x40000000
0x000108 E5D40020 ldrb R0, [R4, #0x20]
0x00010C E3100001 tst R0, #0x1
0x000110 0AFFFFFC beq 0x000108
0x000114 E5D40000 ldrb R0, [R4]
0x000118 E8BD8010 ldmia SP!, {R4, PC}
Each one carries what it is as well as how it reads: condition and kind in the datasheet's own vocabulary, word, address, offset and size, and operand for the one number the operand field denotes. That last one is resolved to an address wherever the addressing is self-contained, so a branch gives its target and a load relative to the program counter gives the address its literal sits at.
disassemble goes straight through the image and follows nothing. Whether a word is code at all depends on how control reached it, so a caller who wants reachable code walks it themselves. stop_at_return is a convention rather than an encoding, because this part has no return instruction, only a program counter any instruction may write; it recognises the two shapes a compiler emits and says so rather than guessing at a computed jump.
The eleven rows are Figure 28's, and what that figure does not have decides as much as what it does: no branch-and-exchange row, no halfword transfer row, no signed load row and no long multiply row. An encoding matching none of them is reported as outside the figure rather than guessed at.
from arm6 import Cpu
cpu = Cpu("arm60", seed=7)
print(cpu.registers.read(0) != 0, cpu.registers.pc != 0)True True
Construction puts every register in the state the rail coming up leaves it, the program counter included, so a newly built part executes rubbish from a rubbish address exactly as the silicon would. Nothing arrives reset and nothing arrives cleared, because no board offers either.
reset() is therefore how a caller gets anywhere, and it is what every example above opens with. It writes only the three things section 6.3.6 lists, forcing supervisor mode, setting both interrupt disables and fetching from address zero, and the saved counter and status it writes are scrambled because the datasheet says their value is not defined. It is not free either: it costs the two cycles of dummy fetches the datasheet requires plus the three of exception entry, and those five appear in the tally like any others.
from arm6 import Cpu
cpu = Cpu("arm60", fill=0).reset()
print(cpu.registers.pc, cpu.mode.name, cpu.cycles, cpu.steps)0 svc32 5 0
The counter is at the reset vector, the clock has moved and the instruction count has not. One place in this repository sets the counter by hand instead: the corpus runner, because a recorded case declares the counter, the mode and the interrupt disables, and resetting would destroy the state under comparison.
fill=0 is how a caller asks for something quieter, and it exists for runs that have to get through a few dozen instructions rather than for convenience.
Three things hold it, and none of them is this package agreeing with itself.
The datasheet, quoted and checked. Every fact in conformance/hardware.json carries the sentence it came from and the page it was on, and conformance/quotes.py places all 62 of them on the page they cite. That check is harder here than in the sibling repositories and it matters more: the ARM60 datasheet carries no text layer at all, so pdftotext returns one newline per page and a search of it would find nothing while meaning nothing. Each page is rendered and recognised instead.
A filtered corpus, state only. conformance/singlestep.py runs 424,220 recorded cases from SingleStepTests/ARM7TDMI, pinned by commit, and compares the whole processor state after each one. There are 0 disagreements. That corpus was recorded from a later part, so 575,780 of its million ARM-state cases are filtered out and every exclusion in conformance/suites.json names the sentence behind it. No cycle, access or transaction-order field is ever read: ARM60's multiply worst case is 1S+16I and the ARM7TDMI's is 1S+4I, so that part's transaction stream is a different bus.
The cycle tables. Every instruction form's cost is held to Table 20, and the bus record is compared as well as the total, because a model can spend the right number of cycles driving the wrong addresses.
What is not settled is in OPEN-QUESTIONS.md, including the ceiling above all of it: nothing here rests on a measurement of real silicon.
ruff format --check . && ruff check .
mypy
pnpm run format:check
python3 -m coverage erase
for file in $(find arm6 conformance -name '*.test.py' | sort); do
python3 -m coverage run -a "$file"
done
python3 -m coverage reportThen the checks that read something this repository does not carry, each of which reports what it could not do rather than passing quietly:
python3 -m conformance.quotes
python3 -m conformance.singlestep path/to/ARM7TDMI/v1
python3 -m conformance.speed
python3 arm6/doctor.pyconformance/quotes.py needs pdftoppm and tesseract on the machine, because of the missing text layer. The doctor says whether they are there.
This repository carries no documents. Every claim is traced to something published elsewhere, listed here so a reader can fetch the same file and check the same page. Each row gives the page count and the first sixteen characters of the file's SHA-256, because links move and a link that has rotted into a different scan is easy to follow without noticing. Compute the full digest with shasum -a 256 <file>.
Every document below is copyrighted and not redistributable, which is why none is in this repository. Individual sentences are quoted in conformance/hardware.json with the page they are printed on.
| Document | Date | Pages | SHA-256 | Redistributable |
|---|---|---|---|---|
| GEC Plessey Semiconductors, ARM60 Datasheet, Issue 0.81 | 1993 | 83 | 315dbff7b6259a73… |
No |
| Advanced RISC Machines, ARM610 Data Sheet, ARM DDI 0004D | 1993-08 | 134 | 76f13ffe2bc20774… |
No |
| Advanced RISC Machines, Application Note 11: Differences Between ARM6 and Earlier ARM Processors | undated | 1 | 41239bc21c8e3375… |
No |
Fetching them is a command rather than an exercise. conformance/documents.json carries the full digest, the byte count and a fetchable address for each, and conformance/documents.py brings all three down into docs/, which git ignores, and refuses anything whose digest does not match.
python3 -m conformance.documents # fetch and verify every digest
python3 -m conformance.documents --check # verify what is already hereThe address for the ARM610 data sheet in that manifest is a mirror. The one linked above serves the file to a browser and answers a plain fetch with 403, and both serve the same bytes.
The first of the three is the one that decides. It is the only document found that gives this part per instruction and per cycle, and it is the only one here with no text layer at all: pdftotext returns eighty three characters for its eighty three pages. Every page a quotation cites is therefore rendered and read rather than searched, and the checker that does it is conformance/quotes.py.
The ARM610 datasheet is a second witness for the instruction set and the programmer's model, and for nothing else: its bus chapter describes two clocks, two cycle types and a cache, none of which the ARM60 has. Application Note 11 is the only document found that prints the four 26-bit mode encodings, which both datasheets leave out.
| Source | Used for |
|---|---|
| SingleStepTests/ARM7TDMI | The recorded cases the core is compared against, state only. Commit and filter in conformance/suites.json |
The family standard every repository here is built to is in FAMILY.md.
See CITATION.cff.
MIT. See LICENSE.