The QCPU 2 microprocessor is a 16 bit RISC architecture with fixed-length instructions, inspired by RISC-V as well as few features of the Motorola 68000 design philosophy. It features a classic five stage pipeline. Each instruction strictly uses the ALU and/or memory system once to reduce complexity and to run on deterministic, 16 bit hardware. Structurally, the CPU consists of:
- 8 Data Registers (DRs) (including zero register)
- 8 Address Registers (ARs) (including zero register)
- Control & Status Registers (CSRs), including:
- System information (e.g. hart)
- System control (e.g. features, clock time base)
- Flag register
- Interrupt control
- Virtual memory control
- Branch Target Buffer (BTB)
- Translation Look-Aside Buffer (TLB)
- Arithmetic Logic Unit (ALU)
- Barrel Shifter (BS)
- Address Register comparators (i.e. over-/underflow exceptions, stack boundaries)
- Asynchronous memory instructions (i.e. scoreboarding)
- Atomic memory instructions
- Timer
This blog post aims to explain the architecture and the rationale behind certain design decisions. The QCPU 2 architecture is documented on its own web page. Please note that the QCPU 2 architecture is still being researched, designed, and developed, and may still change over time.
Programming model
Fundamentally, the programming model of the QCPU 2 architecture consists of three typed register domains:
- Data Registers
- Address Registers
- Machine-State Registers (CSRs)
Data Registers are fully general-purpose, except for the return pointer. Address Registers are partially special-purpose (i.e. the instruction pointer) and contain reserved purposes as well (i.e. the frame pointer). Data can be moved freely between these two register domains, but they support different operations and adopt different behaviours. Control & Status Registers cannot be addressed in normal operation and are limited to kernel mode.
| Domain | Register | Identifier |
|---|---|---|
| Data Register | r0 | zr (zero) |
| Data Register/Async | r1 | rp (return ptr) |
| Data Register/Async | r2 | d1 |
| Data Register/Async | r3 | d2 |
| Data Register/Async | r4 | d3 |
| Data Register/Async | r5 | d4 |
| Data Register/Async | r6 | d5 |
| Data Register/Async | r7 | d6 |
| Address Register | r8 | zr (zero) |
| Address Register | r9 | ip (instruction ptr) |
| Address Register | r10 | sp (stack ptr) |
| Address Register | r11 | fp (frame ptr) |
| Address Register | r12 | a1 |
| Address Register | r13 | a2 |
| Address Register | r14 | a3 |
| Address Register | r15 | a4 |
QCPU 2 deliberately divides the computational data domain with the computational address
domain to provide a couple of benefits. Firstly, a second set of general-purpose registers
increases the total system registers whilst keeping the operand bit size the same for
operations on DRs. For a subset of operations, ARs may be used. Data can be transferred
freely between DRs and ARs. Secondly, with the introduction of ARs, the CPU contextualises
the address domain.
Special-purpose registers of the CPU are collapsed into the ARs where primitive address
arithmetic of the ISA can be used rather than special control flow introduced into the ISA.
QCPU 2 has a striking absence of these special instructions.
For example, for the instruction pointer ip, a relative jump is performed with
an add-immediate addi ip, imm instruction on the instruction pointer.
Once addresses have their own register domain, architectural features may differ from
operations on DRs and include special semantics. For instance, operations which overflow ARs
may emit hardware exceptions, which is not always desirable for DRs due to the lack of
informational context by the CPU in traditional programming models.
ARs are particularly useful within subroutines where memory needs to be accessed:
function:
movew -(sp), d1 ; predecrement word stride 2
movew -(sp), d2 ; predecrement word stride 2
move fp, sp ; pseudoinstruction for `add fp, zr, sp`
addi sp, -16 ; allocate 16 bytes on stack
move sp, fp ; pseudoinstruction for `add sp, zr, fp`
movew d2, (sp)+ ; postincrement word stride 2
movew d1, (sp)+ ; postincrement word stride 2
ret ; pseudoinstruction for `add ip, zr, rp`
That being said, all ARs have access to the memory addressing modes as with the stack pointer
sp. To do a simple memory move, two increment instructions are saved:
mem_move:
movew -(sp), d1
.loop: move d1, (a1)+ ; move (a1) to d1
move (a2)+, d1 ; move d1 to (a2)
dec d2 ; pseudoinstruction for `addi d2, -1`
brh nz, .loop ; loop if not zero
movew d1, (sp)+
ret
The zero register is a small but powerful feature in the architecture. Firstly, it enables
simple primitive operations to be done for multiple motives due to a statically-known source
operand. For example, register-to-register move operations are pseudoinstructions for
addition with zero. Similarly, the set-one-if-negative sneg pseudoinstruction
is composed of the set-one-if-less-than slt instruction with zero.
Secondly, results of operations may be discarded by writing into the zero register, having
no architectural effect other than the flag register or other side effects, such as cache
behaviour. This is most evident in the prefetch data prfd pseudoinstruction,
which is composed of a memory move into the zero register.
Generally, QCPU 2's design philosophy prioritises simple and reusable primitives rather than
specialised instructions. This has been discussed regarding the instruction pointer.
However, this can also be seen with the stack pointer sp. The ISA does not
implement push, pop or peek instructions, as the CPU itself mostly disregards the stack as
an architectural feature (except for the CSR-configurable low/high boundaries of the stack
pointer AR). Instead, memory move primitives are used. These move instructions
contain various addressing modes such as predecrement (push), postincrement (pop), and
static offsets (peek). This allows these instructions to be reused for generic memory
operations and arrays as well.
CSRs and MMIO in SMP
An undiscussed register domain are the Machine-State Registers. These CSRs reflect the internal state and configuration of the CPU. The flag register, for example, can be read from and written to as a CSR for use in interrupt context switches. Additionally, features of the CPU can be enabled or disabled. These features are:
- Kernel protection active
- Branch Target Buffer active
- Asynchronous memory active (as opposed to fully synchronous)
- Interrupts
Features such as the BTB consume more energy, and it may be preferred to substitute it for a
loss in performance. As a last example, the clock-time-base ctb CSR allows the
CPU to determine its own clock speed.
In a Symmetric Multiprocessing (SMP) system, each CPU has its own CSRs. Each CPU is
configured individually through these CSRs. Memory is used for CPUs to communicate with each
other. In the same manner, access to the same devices in an SMP system is done through
Memory-Mapped I/O (MMIO), which uses the same memory move primitives as seen before. The
ISA adds instructions such as fence and atomic exchange xch to
allow synchronisation between these CPUs.
To synchronise CPUs with a lock, a mutex can be implemented using xch:
lua a1, .mutex_ptr'u
lli d1, 1
.try_again: xch d1, .mutex_ptr'l(a1) ; atomically swap 1 into mutex
test d1 ; if 0 is returned, the mutex is now ours
brh nz, .try_again
; critical section...
xch zr, .mutex_ptr'l(a0) ; unlock mutex
For interrupts, they can be configured on both a local and global level. Individual CPUs can enable or disable the reception of certain interrupts. The I/O-mapped global interrupter routes platform-level interrupts to available CPUs. However, local interrupts such as the CPU timer, address faults, and memory faults are not managed by the global interrupter. These are instead enabled and configured by CSRs, such as the CPU timer:
lli d1, 16
csrw itb, d1 ; interrupt time base = default / 16
lli d1, 0b0000_0001
csrw ien, d1 ; enable timer interrupt
Privilege levels
Perhaps the most distinct architectural feature of the QCPU 2 microprocessor is its approach on privilege levels.
The CPU does not hold the privilege level as an architectural state. Instead, the location of the instruction pointer determines the effective privilege level. In virtual memory, a kernel region is created above an address watermark. Entering this kernel region from userland emits a hardware exception. It is only possible to enter predefined locations in the kernel region through interrupts and system calls. The effective privilege level is implicitly demoted once the instruction pointer leaves the kernel region.
The discussed design provides one main advantage in safety. Memory ownership determines the effective privilege level of the CPU. This reduces the attack surface of diverted control flow from within kernel mode to unauthorised text sections in userland.