open-story-teller/core/chip32/chip32_vm.h
anthony@rabine.fr eb08627029
Some checks are pending
Build-StoryEditor / build_linux (push) Waiting to run
Build-StoryEditor / build_win32 (push) Waiting to run
Add TAC in compile pipeline
2025-10-06 10:24:25 +02:00

233 lines
8.3 KiB
C

/*
The MIT License
Copyright (c) 2022 Anthony Rabine
Copyright (c) 2018 Mario Falcao
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#ifndef CHIP32_H
#define CHIP32_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
// General form: instruction destination, source
// coded as: instr dst, src
typedef enum
{
// system:
OP_NOP = 0, ///< do nothing
OP_HALT = 1, ///< halt execution
OP_SYSCALL = 2, ///< system call handled by user-registered function, 4 arguments (R0 - R3) passed by value
// constants:
OP_LCONS = 3, ///< store a value in a register, e.g.: lcons r0, 0xA2 0x00 0x00 0x00
///< can also load a variable address: lcons r2, $DataInRam
// register operations:
OP_MOV = 4, ///< copy a value between registers, e.g.: mov r0, r2
// stack:
OP_PUSH = 5, ///< push a register onto the stack, e.g.: push r0
OP_POP = 6, ///< pop the first element of the stack to a register, e.g.: pop r0
// memory get/set from/to an address. The last argument is a size (1, 2 or 4 bytes)
OP_STORE = 7, ///< copy a value from a register to a ram address located in a register, specific size e.g. : store @r4, r1, 2 (2 bytes)
OP_LOAD = 8, ///< copy a value from a ram address located in a register to a register, specific size e.g.: load r0, @r3, 1 (1 byte)
// arithmetic:
OP_ADD = 9, ///< sum and store in first reg, e.g.: add r0, r2
OP_ADDI = 10, ///< Add immediate value (8 bits max), e.g.: addi r4, 2 ; r4 = r4 + 2
OP_SUB = 11, ///< subtract and store in first reg, e.g.: sub r0, r2
OP_SUBI = 12, ///< substract immediate value (8 bits max), e.g.: subi r2, 8 ; r2 = r2 - 8
OP_MUL = 13, ///< multiply and store in first reg, e.g.: mul r0, r2
OP_DIV = 14, ///< divide and store in first reg, remain in second, e.g.: div r0, r2
OP_SHL = 15, ///< logical shift left, e.g.: shl r0, r1
OP_SHR = 16, ///< logical shift right, e.g.: shr r0, r1
OP_ISHR = 17, ///< arithmetic shift right (for signed values), e.g.: ishr r0, r1
OP_AND = 18, ///< and two registers and store result in the first one, e.g.: and r0, r1
OP_OR = 19, ///< or two registers and store result in the first one, e.g.: or r0, r1
OP_XOR = 20, ///< xor two registers and store result in the first one, e.g.: xor r0, r1
OP_NOT = 21, ///< not a register and store result, e.g.: not r0
// branching/functions
OP_CALL = 22, ///< set register RA to the next instruction and jump to subroutine, e.g.: call 0x10 0x00
OP_RET = 23, ///< return to the address of last callee (RA), e.g.: ret
OP_JUMP = 24, ///< jump to address (can use label or address), e.g.: jump .my_label
OP_JUMPR = 25, ///< jump to address contained in a register, e.g.: jumpr t9
OP_SKIPZ = 26, ///< skip next instruction if zero, e.g.: skipz r0
OP_SKIPNZ = 27, ///< skip next instruction if not zero, e.g.: skipnz r2
// Comparison
OP_CMP_EQ = 28, ///< compare two registers for equality, result in first e.g.: eq r4, r0, r1 (r4 = (r0 == r1 ? 1 : 0)
OP_CMP_GT = 29, ///< compare if first register is greater than the second, result in first e.g.: gt r4, r0, r1
OP_CMP_LT = 30, ///< compare if first register is less than the second, result in first e.g.: lt r4, r0, r1
INSTRUCTION_COUNT
} chip32_instruction_t;
/*
| name | number | type | preserved on function call |
|-------|--------|----------------------------------|-----------|
| r0-r9 | 0-9 | general-purpose | N |
| t0-t9 | 10-19 | temporary registers | Y |
| pc | 20 | program counter | Y |
| sp | 21 | stack pointer | Y |
| ra | 22 | return address | N |
*/
typedef enum
{
// preserved across a call, use them for argument passing
R0,
R1,
R2,
R3,
R4,
R5,
R6,
R7,
R8,
R9,
// Temporaries are automatically saved on stack across a call
T0,
T1,
T2,
T3,
T4,
T5,
T6,
T7,
T8,
T9,
// special
PC,
SP,
RA,
// count
REGISTER_COUNT
} chip32_register_t;
typedef enum
{
VM_READY, // VM Ready to be started
VM_FINISHED, // execution completed (i.e. got halt instruction)
VM_SKIPED, // skipped instruction
VM_WAIT_EVENT, // execution paused since we hit the maximum instructions
VM_OK, // execution ok
VM_ERR_UNKNOWN_OPCODE, // unknown opcode
VM_ERR_UNSUPPORTED_OPCODE, // instruction not supported on this platform
VM_ERR_INVALID_REGISTER, // invalid register access
VM_ERR_UNHANDLED_INTERRUPT, // interrupt triggered without registered handler
VM_ERR_STACK_OVERFLOW, // stack overflow
VM_ERR_STACK_UNDERFLOW, // stack underflow
VM_ERR_INVALID_ADDRESS, // tried to access an invalid memory address
} chip32_result_t;
typedef struct {
uint8_t opcode;
uint8_t nbAargs; //!< Number of arguments needed in assembly
uint8_t bytes; //!< Size of bytes arguments
} OpCode;
#define OPCODES_LIST { { OP_NOP, 0, 0 }, { OP_HALT, 0, 0 }, { OP_SYSCALL, 1, 1 }, { OP_LCONS, 2, 5 }, \
{ OP_MOV, 2, 2 }, { OP_PUSH, 1, 1 }, {OP_POP, 1, 1 }, \
{ OP_STORE, 3, 3 }, { OP_LOAD, 3, 3 }, \
{ OP_ADD, 2, 2 }, { OP_ADDI, 2, 2 }, { OP_SUB, 2, 2 }, { OP_SUBI, 2, 2 }, { OP_MUL, 2, 2 }, \
{ OP_DIV, 2, 2 }, { OP_SHL, 2, 2 }, { OP_SHR, 2, 2 }, { OP_ISHR, 2, 2 }, { OP_AND, 2, 2 }, \
{ OP_OR, 2, 2 }, { OP_XOR, 2, 2 }, { OP_NOT, 1, 1 }, { OP_CALL, 1, 1 }, { OP_RET, 0, 0 }, \
{ OP_JUMP, 1, 2 }, { OP_JUMPR, 1, 1 }, { OP_SKIPZ, 1, 1 }, { OP_SKIPNZ, 1, 1 }, \
{ OP_CMP_EQ, 3, 3 }, { OP_CMP_GT, 3, 3 }, { OP_CMP_LT, 3, 3 } }
/**
Whole memory is 64KB
The RAM and ROM segments can be placed anywhere:
- they must not overlap
- there can have gaps between segments (R/W to a dummy byte if accessed)
----------- 0xFFFF
| |
| |
|---------|<-- stack start
| |
| RAM |
|_________|
| |
|_________|
| ROM |
|_________|
| |
----------- 0x0000
*/
typedef struct
{
uint8_t *mem; //!< Pointer to a real memory location (ROM or RAM)
uint16_t size; //!< Size of the real memory
uint16_t addr; //!< Start address of the virtual memory
} virtual_mem_t;
typedef struct chip32_ctx_t chip32_ctx_t;
typedef uint8_t (*syscall_t)(chip32_ctx_t *, uint8_t);
#define SYSCALL_RET_OK 0 ///< Default state, continue execution immediately
#define SYSCALL_RET_WAIT_EV 1 ///< Sets the VM in wait for event state
#define SYSCALL_RET_ERROR 2
struct chip32_ctx_t
{
virtual_mem_t rom;
virtual_mem_t ram;
uint16_t stack_size;
uint32_t instrCount;
uint16_t prog_size;
uint32_t max_instr;
uint32_t registers[REGISTER_COUNT];
syscall_t syscall;
void* user_data;
};
// =======================================================================================
// VM RUN
// =======================================================================================
void chip32_initialize(chip32_ctx_t *ctx);
chip32_result_t chip32_run(chip32_ctx_t *ctx); // loop until the end or max instructions
chip32_result_t chip32_step(chip32_ctx_t *ctx); // one instruction
#ifdef __cplusplus
}
#endif
#endif // CHIP32_H