Close
0%
0%

XiAleste

XiAleste Next is an 8-bit home computer, which is compatible with software for the Amstrad CPC6128 released on 13 June 1985

h2wh2w
Similar projects worth following
XiAleste is an 8-bit home computer, which is compatible with software and hardware for the 1985 Amstrad CPC6128. It also has enhanced capabilities. It is intended to appeal to retrocomputing enthusiasts and to anybody who like to build yourself a hardware unit.

The Concept

The concept of the project was based on the idea of a new version of Amstrad CPC6128 computer - using modern components and up-to-date technology. Subsequently, this concept underwent a number of significant changes. One of these changes was the idea to equip this clone with Aleste extensions.

For reference: Aleste 520EX (A520) - is a Russian clone of Amstrad CPC6128 computer, but with some extensions. The main advantage of this computer was its sound capabilities, which were comparable with Amiga 500. The Aleste computer could play 4-channel mod files - it had a sampler synthesizer. Among the disadvantages were the high complexity of the assembly, as well as the very rare components today. Another drawback was that the A520 had two completely incompatible Aleste and CPC modes. That is all cool features of A520 were not available for CPC programs. This was the flaw that I needed to fix first.

Re-reading the book may surprise you with interesting details. But your new interpretations can be even more interesting and dramatically different from previous ones. Along the way, the project changed and its final formula for the new computer, which is called XiAleste, became the following:

  • XiAleste is primarily an Amstrad CPC with extensions available to it from the A520.
  • The A520 extensions will be redesigned -- the spirit of the A520 will remain.
  • XiAleste is 100% open source. All source files of the project will be published on github.

The XiAleste computer is a sound synthesis platform which cannot be fully emulated on an FPGA, i.e. a MIST or MISTER type board. This computer will have its own unique architecture. This architecture will have:

  • A bus for extension boards -- primary for digital peripherals devices.
  • A basic sound synthesizer system that can be extended. 
  • Bus for sound extensions -- analog sound processing modules. 

The project is very ambitious and I can not give a 100% guarantee for its quick and complete completion. But if there are people who want to get involved and help, chances are better. 

The Spirit of Aleste

This is a retro computer project, so all changes must be made with great love and respect for the engineers who created the amazing Amsrad CPC6128 computer. 

One of the major extensions of the Aleste is the MagicSound sound card. You could say it is the cornerstone of the project. Having a board of this class requires many more signals on the extension connector - more than on the CPC6128 connector. These signals are used for direct access to the memory located on the motherboard. It is important to note that the ability to access the entire address space of the computer at will is another distinguishing feature of the A520.  Although it is not necessary if the sound system is located on the processor board. Nevertheless, the improved expansion slot is something valuable to realize. 

Precisely because this connector is not known to be CPC6128 compliant it can be radically modified. There are also no plans for compatibility with the A520.

Amstrad Expansion Bus

I chose the ITX (DTX) standard as the printed circuit board design. Along the way I noticed that the original Amstrad CPC6128 connector can only be placed on the right side of the PCB. So, unfortunately, I have to give it up. But you can modify the board design and return it to the system.

XiAleste Expansion Bus

The Amstrad CPC6128 connector can be classified as a Processor Direct Connector (PDC), and the Aleste expansion connector can also be classified as such. I plan to evolve this connector to an 8-bit parallel computer bus and install two or three sockets on the board. The Amstrad CPC6128 connector for hardware compatibility can be put on the board as the same PDC without any changes.

Memory Map

Another important feature of Aleste is the use of MSX Mapper as a memory manager. However, this has been done with certain limitations. The fact is that the highest two bits of data in the Gate Array...

Read more »

XiAleste.PDF

The main board schematic

Adobe Portable Document Format - 6.02 MB - 01/08/2023 at 11:54

Preview

  • Methods and Functions in Meta-Assembler: A Practical Guide

    h2w02/22/2026 at 20:03 0 comments

    In previous posts, I discussed working with data containers. Now it's time to talk about methods and functions — the key elements of any program.

    Method and Function Signatures

    In the meta-assembler, every method and function has a strict signature that describes argument types and their passing registers. Let's look at a simple string copy procedure example:

    lisp

    (defproc test-copy-string ((dest string :reg de) (src string :reg hl))  
      (declare (once) 
      (asm-func none))  ;; Input: DE = dest, HL = src  
      (.label 'loop)  (.ld a (@ hl))      ;; Read character from source  
      (.ld (@ de) a)      ;; Write to destination  
      (.or a)             ;; Check for 0 (end of string)  
      (.ret 'z)           ;; If 0 — exit    
      (.inc hl)           ;; Next in source  
      (.inc de)           ;; Next in destination  
      (.jr 'loop))        ;; Repeat

    As seen in the example, the procedure (a function without a class) declares:

    • Argument names - for code readability
    • Argument types - for correctness checking
    • Passing registers - where each argument is expected on input

    The return value is currently declared in the (declare (asm-func none)) block.

    Calling Conventions

    The system follows these conventions:

    Register
    AF/HL, DE, BCObject pointerFor class methods
    IYReturn valueFunction result

    Call Primitives

    Several specialized primitives exist for calling procedures and methods:

    1. Direct Function Call (dcall)

    lisp

    (dcall vec3::new int16 int16 _type_)

    This call invokes the new function from the vec3 namespace. The compiler checks argument type compatibility (two int16s) and expected result (_type_).

    Call variants:

    • Via active table: vec3::new — table can be anywhere
    • Via vtable in type: vec3::vtable::new — explicit path specification
    • Direct call: vec3::new — without a table, if the function is statically known

    2. Binding Arguments to Registers (rlet)

    The special form rlet (or function definition macro) binds argument names to types and registers:

    lisp

    (rlet ((x int16 hl)        
      (y int16 de)
      (z int16 bc))
      (dcall vec3::set x y z none))

    The last line tells the compiler: call vec3::set with arguments x, y, z, which are already in their respective registers. The compiler only checks types and generates code:

    lisp

    0147:  CD 66 00    |     CALL $0166

    3. Object Method Call

    Calling an object method requires an object pointer in the IX register. Two approaches are possible:

    Sequential call with method loading into IY:

    lisp

    (obj-method->iy ix vec3::len)  ;; Load method address into IY
    (ycall vec3::len vec3 len)      ;; Call method via IY

    Full method call cycle (with all overhead):

    text

    012B:  DD 21 00 80    |     LD IX, $8000      ; object address
    012F:  E5             |     PUSH HL           ; save registers
    0130:  C5             |     PUSH BC
    0131:  DD 6E 00       |     LD L, (IX+0)      ; load vtable address
    0134:  DD 66 01       |     LD H, (IX+1)
    0137:  01 0F 00       |     LD BC, $000F      ; method offset
    013A:  09             |     ADD HL, BC        ; get method address
    013B:  E5             |     PUSH HL
    013C:  FD E1          |     POP IY            ; transfer to IY
    013E:  C1             |     POP BC            ; restore registers
    013F:  E1             |     POP HL
    0144:  FD E5          |     PUSH IY           ; method address on stack
    0146:  C9             |     RET               ; call
    0147:                 | [loc-exit]            ; return point

    If execution needs to continue after the call, the return address (in this case loc-exit) is additionally pushed onto the stack.

    Performance: The Bitter Truth

    I cannot avoid mentioning the elephant in the room. The compiler is written in an interpreter and, being quite complex, shows rather low performance. This raises legitimate doubts about its suitability for large projects.

    Time will tell whether the system can scale or will require optimization of critical sections. For now, it's a working tool for medium-sized projects where the flexibility of metaprogramming outweighs compilation costs.

    To be continued. In following posts: memory management and critical code optimization.

  • Subject: Runtime Type System on Z80: A Surprisingly Efficient Experimen

    h2w02/15/2026 at 21:29 0 comments

    Hi everyone, I wanted to share the results of a recent experiment. Initially, I didn’t plan on implementing deep typing, but while developing my own assembler (ZASM), a rather compact and powerful runtime type system started to crystallize.

    The Concept: We define the type hierarchy directly in the source code. The .type-descriptor macro handles all the "heavy lifting": it generates pointers to parent types, creates string names for debugging, and most importantly, constructs vtables with full inheritance support.

    Source Code (Lisp-style DSL):

    (defproc test-type-descriptor ()
      (declare (once) (asm-func none))
      (.type-descriptor object)
      (.type-descriptor structure)
      (.type-descriptor vec3))

    The Result (Generated Listing):

    The system automatically generates linked data structures. Notice how the hierarchy is preserved: vec3 correctly points to the structure parent, which in turn points to the base object.

    0051:  51 00        |  DW       ; Self-reference
    0053:  60 00        |  DW      ; "object" string
    0055:  02           |  DB  $02                     ; Type flags/ID
    ...
    0057:               | [object::vtable::new]
    0057:  C3 01 00     |  JP  object::new             ; Method dispatch
    ...
    007A:               | [type::def::vec3]
    007A:  67 00        |  DW    ; Pointer to Parent!
    0080:               | [vec3::vtable::new]
    0080:  C3 05 00     |  JP  vec3::new               ; Overridden method
    0083:  C3 02 00     |  JP  object::delete          ; Inherited method

    Why this works well:

    1. Compactness: Each descriptor occupies only a few bytes.
    2. Inheritance "for free": If a method isn't overridden in a child type, the vtable simply copies the jump (JP) to the parent's implementation.
    3. Runtime Reflection: Having type names available at runtime allows for robust type-checking directly on the hardware.

    Next Steps: The final polish of the parse-arg logic for method argument validation. The goal is to have the assembler "slap the programmer's wrist" during compilation if they try to pass an incompatible register or type to a method.

  • Methods and Structures

    h2w02/11/2026 at 16:32 0 comments

    In bare metal assembly, structures don't exist. There are only addresses and offsets you keep in your head. Add a field at the start — now you get to hunt down every `LD (IX+2)` and turn it into `(IX+4)`. On a good day, you'll only miss a few. The debugger won't care.

    We made structures real.

    Here's how it works. Define the type:

    ```lisp
    (deftype vec3 (structure)
      ((x int16) 
       (y int16) 
       (z int16))
     (:methods
      (clear () none)))
    ```

    Use it like you mean it:

    ```lisp
    (defmethod clear ((self vec3))
      (rlet ((v :reg ix :source self))
        (zasm 
          (.ld (-> v x) 0)
          (.ld (-> v y) 0)
          (.ld (-> v z) 0))))
    ```

    No magic numbers. The language knows `y` lives at offset 2, and `ix` currently points at the object. Compiler plugs in the right addresses, spits this out:

    ```
    DD 36 00 00 | LD (IX+0), $00
    DD 36 02 00 | LD (IX+2), $00
    DD 36 04 00 | LD (IX+4), $00
    ```

    **What you actually get:**

    - **No offset bookkeeping.** Write `(-> v y)`, compiler handles the math.
    - **Errors where they belong.** Typo in field name? Assembly fails right there, not after three hours of debugging.
    - **Register independence.** Want `iy` instead of `ix`? Change one line in `rlet`, the whole block retargets.

    This isn't about making Z80 look like Lisp. It's about using macros to bridge the gap between how humans think about code and what the CPU actually executes. Names, types, checks — without leaving the metal.

  • Implementing VTables in a Meta-Assembler: From Type Declaration to Code Generation

    h2w02/08/2026 at 08:58 0 comments

    I want to share some progress on my custom meta-assembler. The core focus here is automating the creation of Virtual Method Tables (VTables). This is essential when developing drivers or system components where data structures and methods must be tightly coupled but remain easily extensible.

    1. Type Declaration (Layout)

    At the source level, we define the data structure and method signatures. This allows the assembler to pre-calculate field offsets and method indices for the dispatch table.

    Lisp

    (deftype vec3 (structure)  
       ((x int16)   ;; offset 0   
        (y int16)   ;; offset 2   
        (z int16))  ;; offset 4  
       (:methods      
         (new (int16 int16) none) ;; Method ID 0      
         (len () int16)))         ;; Method ID 1
    

    2. Method Implementation

    Next, we write the actual method code. By using the $ prefix, the meta-assembler automatically binds these labels to the specific type's method slots.

    Lisp

                (org #x9000)
    $vec3::new      
                (ret)
    $vec3::len      
                (ret)
    

    3. VTable Generation

    The vtable command is a powerful meta-instruction. It inspects the vec3 type, locates all implemented methods, and arranges them in the correct order. It also handles "holes" (sparse tables) if certain methods aren't overridden in a hierarchy.

    Lisp

                (align 16)      
                (vtable position vec3)
    

    Compilation Result (Listing)

    The assembler doesn't just allocate space; it generates a functional jump table (using the jp opcode 0xC3 for the Z80).

    Plaintext

    9000 | --------                 | org 9000h
    9000 |                          | vec3::new:
    9000 | C9                       | ret
    9001 |                          | vec3::len:
    9001 | C9                       | ret
    9002 | --------                 | align 10h
    9010 | C3 00 90 00 00 00 00 00  | .vtable position, vec3, (quote default)
    9010 | 00 00 00 00 00 00 00 00  | 
    9010 | 00 00 00 00 00 00 00 00  | 
    9010 | 00 00 00 C3 01 90        | 

    Key Benefits:

    1. Type Safety: Manually calculating table indices is error-prone; the assembler now handles this automatically.
    2. Flexibility: If you add a method to the deftype, the VTable is rebuilt and re-aligned on the next pass.
    3. Clarity: The listing clearly shows exactly where each jump leads, making debugging significantly easier.

    Next step: Implementing high-level object declarations and automating method dispatch (dynamic dispatchers).

  • Type System Progress: Metadata is now baked into Z80 opcodes!

    h2w02/06/2026 at 19:07 0 comments

    Great progress on the compiler! I’ve finally bridged the gap between high-level type definitions and the Z80 assembler. The compiler now handles all offset and size calculations "under the hood," injecting pure constants directly into the generated instructions.

    The core benefit:

    No more manual byte-counting or maintaining a mess of X_OFFSET = 2 constants. You define the structure once, and from then on, you simply refer to fields by name directly within zasm blocks.

    Example implementation:

    We define a type and immediately use its metadata to generate assembly:

    (deftype vector (structure)  
      ((x int16)   ;; offset 0   
       (y int16)   ;; offset 2   
       (z int16)))) ;; offset 4
    
    (zasm
    $begin   
      (ld @hl (size-of vector))           ;; Get total structure size   
      (ld @hl (offset-of vector x))       ;; Get offset of field 'x'   
      (ld @hl (offset-of vector y))       ;; Get offset of field 'y'   
      (ld @hl (size-of vector x))         ;; Get size of the type used by field 'x' (int16)   
      (ld [+ iy (offset-of vector y)] 10) ;; Direct write to object field via IY
    )
    

    Compilation Result (Binary Dump):

    8000 | begin:           | 8000 | 21 06 00         | ld hl, 06h      ; Total size: 3 fields * 2 bytes
    8003 | 21 00 00         | ld hl, 00h      ; 'x' at offset 0
    8006 | 21 02 00         | ld hl, 02h      ; 'y' at offset 2
    8009 | 21 02 00         | ld hl, 02h      ; size of int16 = 2
    800C | 21 02 00         | ld hl, 02h      800F | FD 36 02 0A      | ld (iy+2), 0Ah  ; Indexed write (IY+2) with value 10 (0Ah)
    

    Why this matters:

    1. Refactoring Resilience: If I change a field from int16 to int32, the compiler automatically updates every offset across the entire project.
    2. Native IY/IX Support: Support for [+ iy displacement] syntax allows us to leverage the Z80’s indexed addressing mode as natively as possible.
    3. Readability: The code feels like a modern systems language, but the output is honest, tight assembly.

    Next steps:

    Implementing instances (memory allocation for objects) and beginning the port of existing code to this new workflow. Finally, I can focus on logic without turning my brain into an offset calculator!

  • Z80 Assembler in Common Lisp is Taking Shape

    h2w02/05/2026 at 12:54 0 comments

    The project is gradually approaching a more-or-less complete form. Not everything works yet. The assembler itself was originally taken from a CL library, but in the process, it had to be rewritten from scratch. The previous version was good but overly convoluted: several levels of macros generated a large set of methods, each for one specific case (operation signature).

    Quick Start

    You need to add a standard initialization at the beginning of the source file. This can be done once per build session.

    ;; Load the assembler file
    (load "common/xiff/soot/asm.sot")
    
    ;; Switch the type system to Z80
    (init-types 'z80)
    

     Now Z80 data types are available, for example, uint16 or pointer (2 bytes in size).

    Next, declare a module that will combine all segments into one logical block. This could be the entire program or a part of it. A module can consist of one or several segments, and segments can be written to one or more files.

    (defmodule test-module)
    


    Writing a Simple Program

    Now you can write a simple program.

    (zasm 
          (org #x0000)
    $zero      
          (org #x8000)
    $start
          (jp 'label3)
          (db 22)
          (db (list 22 33 44))
          (dw 64)
          (ds 8)
          (dw 'start)
          (dw '(1 2 3))
          (ascii "Hello World\n")
          (align 16)
    $label1
          (jmp 'label1)
          (jmp 'label2)
          (ld @a @b) 
    $label2
          (jmp 'label1)
          (jmp 'label3)
          (ld @c (+ 1 3)) 
          (subroutine label3
                (jmp 'label3)
                (jmp 'label2)
                (jmp 'label1)
                (ld @a @c)
                (jmp 0)
                (jmp 'zero)
                (ld @a [+ ix 5])    ; Should be: DD 7E 05
                (ld [- iy 10] #xAA) ; Should be: FD 36 F6 AA
          )
    )
    

    The org directive here is only for testing; in a real project, it might not be needed since each segment can have its own base address.

    Finally, add the build directive:

    (build test-module)
    

     
    Running the Build

    After launching:

    > xiff
    Loaded asm-z80
    ──────────────────────────────────────────
     BUILD SUCCESSFUL: test-module     
    ──────────────────────────────────────────
     Segment: MAIN       | Base: #0000 | Size: 32847 bytes
    ──────────────────────────────────────────

     Generating a Listing

    The following command saves a listing (with :print-listing #t, it also prints it to the screen):

    (generate-module-listing test-module :print-listing #t)

    The resulting listing:

    --- Segment: MAIN | Base: #0000 ---
    0000 | --------         | org 00h
    0000 | zero:            | 
    8000 | --------         | org 8000h
    8000 | start:           | 
    8000 | C3 3B 80         | jp label3
    8003 | 16               | db 16h
    8004 | 16 21 2C         | db (22 33 44)
    8007 | 40 00            | dw 40h
    8009 | 00 00 00 00 00 00 00 00  | ds 08h
    8011 | 00 80            | dw start
    8013 | 01 00 02 00 03 00  | dw (1 2 3)
    8019 | 48 65 6C 6C 6F 20 57 6F 72 6C 64 0A  | ascii Hello World
    
    8025 | --------         | align 10h
    8030 | label1:          | 
    8030 | 18 FE            | jr label1
    8032 | 18 01            | jr label2
    8034 | 78               | ld a, b
    8035 | label2:          | 
    8035 | 18 F9            | jr label1
    8037 | 18 02            | jr label3
    8039 | 0E 04            | ld c, 04h
    803B | label3:          | 
    803B | 18 FE            | jr label3
    803D | 18 F6            | jr label2
    803F | 18 EF            | jr label1
    8041 | 79               | ld a, c
    8042 | C3 00 00         | jp 00h
    8045 | C3 00 00         | jp zero
    8048 | DD 7E 05         | ld a, (ix+5)
    804B | FD 36 F6 AA      | ld (iy-10), AAh
    

    Exporting the Result

    You can save all or selected segments to a file:

    (save-module-hex test-module)

    This creates two files.

    1. Intel HEX format file:

    :10800000C33B801616212C40000000000000000039
    :1080100000008001000200030048656C6C6F20576F
    :108020006F726C640A000000000000000000000095
    :1080300018FE18017818F918020E0418FE18F61820
    :0F804000EF79C30000C30000DD7E05FD36F6AA10
    :00000001FF

     
    2. Symbol table file:

    ("test-module"
      (:export label3 label2 label1 start zero)
      (:symbols
        (:public 32827 0 "label3" :label "MAIN" :meta ())
        (:public 32821 0 "label2" :label "MAIN" :meta ())
        (:public 32816 0 "label1" :label "MAIN" :meta ())
        (:public 32768 0 "start" :label "MAIN" :meta ())
        (:public 0 0 "zero" :label "MAIN" :meta ())
      )
    )

    What's Next?

    The data structures can already be saved to a buffer, but they cannot yet be used directly in the assembler. I hope this functionality will be working within a week.

    To be continued...

  • Implementing Dot-Navigation (Introspection) for the Type System

    h2w01/29/2026 at 15:48 0 comments

    Today marks a major milestone: full dot-navigation support via the -> operator is now operational. The engine can now "traverse" the type metadata hierarchy, seamlessly moving from the global registry down to specific fields and their internal attributes.

    The Goal

    Once a type is declared in Lisp (for example, a structure):

    (deftype test-vector (basic) ((x int8 1) (y int8 2)) (:methods (new (int8 int8) object) (set (int8 int8) none) (len () int8))) 

    We needed a way to inspect this type using dot notation. This isn't just manipulating Lisp lists; it is Runtime Reflection over native C++ objects, exposed to the interpreter as NATIVE_REF.

    REPL Demonstration

    First, we can access the global *type-system* registry. For instance, we can query the pointer size for the current target architecture (in this case, Z80):

    soot> (-> *type-system* pointer-size)
    => 2 

    When querying the structure itself, the system retrieves the BasicType object, which contains vectors for fields and methods:

    soot> (-> *type-system* test-vector)
    => [BasicType] test-vector parent: basic size: 5 fields: Field: (type type :offset 0) ... Field: (x int8 :offset 2) ... Field: (y int8 :offset 3) ... methods: Method 0: new (function int8 int8 object) Method 1: set (function int8 int8 none) Method 2: len (function int8) 

    The -> operator is implemented as a Special Form in C++, allowing us to build deep chains without excessive quoting. We can "drill down" into a specific field to extract its native offset:

    ;; Access the 'x' field object
    soot> (-> *type-system* test-vector x)
    => Field: (x int8 :offset 2) ...
    
    ;; Extract a specific attribute (offset) from that field
    soot> (-> *type-system* test-vector x offset)
    => 2

    A key design detail: navigation respects object boundaries. A field returns a TypeSpec (a type specification/reference), not the type definition object itself. To find the parameters of the underlying data type, we perform a lookup back through the type system:

    ;; Get the type specification of the field
    soot> (-> *type-system* test-vector x type)
    => int8
    
    ;; Resolve that spec to the actual type definition to get its size
    soot> (-> *type-system* int8 size)
    => 1 
    

    Under the Hood

    This navigation is powered by the `Aliasable` system:

    • Chain of Responsibility: Virtual make_step_alias calls propagate correctly through the inheritance hierarchy (from Aliasable through StructureType to BasicType), ensuring data access is never lost at different levels of inheritance.
    • Unified Interface: The C++ step(key) method handles both meta-property lookups (like  size)
    • Architecture Agnostic: The system dynamically pulls data based on the initialized architecture (e.g., Z80 vs. Default).

    What's Next?

    With the code stabilized, we have a solid foundation for the next step: the Static Data Compiler. Now that we can programmatically "see" the offsets and types of every field via introspection, we can begin automating the assembly of structures, constants, and method tables directly into binary buffers.

  • Moving from C/ASM Drivers to a Custom Lisp-Based Assembler with a Type System

    h2w01/26/2026 at 21:28 0 comments

    I’d like to share my experience developing system software for a retro platform (Z80).

    The Problem: Synchronization Hell

    It all started with the driver specifications. Initially, I implemented them using a mix of C and ASM. After completing the core drivers, the sheer scale of the task became clear: the platform's numerous modes required a massive amount of redundant manual work.

    Worst of all was maintaining consistency. Any minor change to a data structure had to be manually propagated through the entire chain: .h -> .c -> .inc -> .asm. Field offsets would constantly drift, and keeping track of register assignments and method arguments became a nightmare.

    The Solution: From Annotations to Metaprogramming

    My first idea was to procedurally generate files from special annotations within the assembly code (which led to the XIFF specification). During development, the annotation syntax naturally evolved toward S-expressions.

    That’s when I asked myself: Why use annotations at all when I can write everything in Lisp? This allows for true metaprogramming—using Lisp macros to create a flexible DSL that describes the driver logic itself.

    The Experiment: An Assembler on SOOT

    As an experiment, I wrote a Z80 assembler on SOOT (my dialect of Lisp). It allows the power of macros to be used directly within the program body.

    Source Code Example:

    Lisp

    (defmacro subroutine (name &rest code)
      `(list (lab ',name) ,@code))
    
    (defparameter test 
      (lzasm-z80 
        $bla
          (jmp 'blo)
          (ld @a @b) 
        $bli
          (jmp 'blo)
          (ld @c (+ 1 3)) ; Compile-time math
        $ble
          (ld @c (+ 1 3)) 
          (ld @c (+ 1 3)) 
        $blu
          (ld @a @c)
        $blo
          (subroutine blubber
            (ld @a @c))
          (jmp 'blo)
          (jmp 'blu)))
    

    Compilation Result (The system automatically handles jp vs jr optimization and calculates offsets):

    Plaintext

    0000 | bla:             | 
    0000 |   18 0A          | jr blo
    0002 |   78             | ld a, b
    0003 | bli:             | 
    0003 |   18 07          | jr blo
    0005 |   0E 04          | ld c, 4
    0007 | ble:             | 
    0007 |   0E 04          | ld c, 4
    0009 |   0E 04          | ld c, 4
    000B | blu:             | 
    000B |   79             | ld a, c
    000C | blo:             | 
    000C | blubber:         | 
    000C |   79             | ld a, c
    000D |   18 FD          | jr blo
    000F |   18 FA          | jr blu
    

    I prioritized diagnostics. The system provides the exact error location in the source file, which is critical when using heavy macro generation:

    Plaintext

    ─── ERROR ──────────────────────────────────
      at common/xiff/soot/asm-test.sot:48
             (jmp 'blo-undefined)
             ^
    Error: Can't find label blo-undefined
    

    The Next Step: Integrating the Type System

    I am currently working on integrating a type system. This allows for declaring enums, bitfields, structures, and even class methods directly in the code.

    Type Declaration:

    Lisp

    (deftype test-vector (basic)
      ((x   int8 1) 
       (y   int8 2))
      (:methods
          (new (int8 int8) object)
          (set (int8 int8) none)
          (len () int8)))

    Now, the compiler environment has full introspection—it knows sizes, hierarchies, and field offsets adjusted for alignment:

    Lisp

    soot> (type-info '(test-vector))
    => ((name . "test-vector")
        (size . 5) ;; 2 bytes for type header + fields
        (fields ("y" 3 int8 ...) ("x" 2 int8 ...) ("type" 0 type ...))
        (methods ("len" 2 ...) ("set" 1 ...))
        ...)
    

    The Goal: To enable the assembler to work with structures directly via access macros. Instead of manual offset calculation (magic numbers), the code should look like this:

    Lisp

    (ld a (ix (-> cursor x)))
    (ld l (ix (-> cursor y)))
    

    There is still a lot of work to do to bridge the type system and the code generator before I attempt to migrate the actual driver code to this new design and build system. However, early tests show that this approach radically reduces "human factor" errors.


  • Aleste LX/BIOS Driver Architecture Specification (Draft)

    h2w01/08/2026 at 17:15 0 comments

    Following work on the Aleste LX hardware and its BIOS, a draft driver architecture specification has been compiled.

    The document describes a driver architecture for 8-bit banked memory systems, built on the "speed through predictability" principle. Solutions are focused on practical hardware work where every clock cycle counts.

    Key features:

    • Direct driver call: 17 cycles (JP + RET)
    • Zero-overhead polymorphism via VTable copying
    • Banked memory management with lazy switching
    • Clear separation between singleton and polymorphic drivers

    The specification provides direction for developing drivers for the platform: video, audio, filesystems.


    Read the specification (draft)

  • Alesta XI/LX: Software Simulator

    h2w12/31/2025 at 15:44 0 comments

    To debug the new BIOS and FPGA tests, we had to develop another software module for the entire system — a simple software simulator of the target Alesta LX platform. This entire lengthy development cycle was necessary to ultimately streamline the entire process.

    The initial task was to create embedded tests for FPGA debugging, which must run on the hardware platform itself. For these to work, in turn, we needed to write a new BIOS from scratch for about 90% of its code. This created a key problem: how to debug this very BIOS if the physical platform for testing was not yet available?

    The decision was made to break this vicious cycle by creating an emulator — a software replacement for the hardware. The development process is now structured into a clear sequence:

    1. The simulator itself is debugged on the main (host) platform. It is quite simple and has built-in debugging tools.
    2. Using this functional simulator as a "test bench," the new BIOS is debugged.
    3. Then, on the debugged BIOS, the tests for the target FPGA are verified and finalized.

    This entire toolchain is first debugged and run in simulation. Only when all components work together correctly do we move on to testing on real hardware. From the outside, this may seem like a complex, multi-stage path, but in reality, it is the most efficient and reliable option possible.

    The image below shows the Alesta LX simulator in operation during a debugging session. The next step will be the debugging of the new BIOS itself.

    Happy upcoming New Year to everyone!


View all 46 project logs

Enjoy this project?

Share

Discussions

h2w wrote 09/14/2025 at 06:33 point

I'm planning to work on cost optimization. This essentially means doing a PCB layout from scratch. I might do it in KiCAD, though it's unlikely.

  Are you sure? yes | no

h2w wrote 01/10/2023 at 18:35 point

In principle, this is interesting because KiCad makes the project more friendly to the OpenSource community. But it is embarrassing that this board has not been produced and tested, which creates a feeling of prematureness. Nevertheless, I suggest you decide for yourself.

With great respect,
h2w

  Are you sure? yes | no

paulvdh wrote 01/10/2023 at 16:17 point

If there is any interest in converting this to KiCad, then I can do a conversion (takes 10 minutes or so) and some cleanup (KiCad needs global or hierarchical labels to link between different sheets and more). Cleanup may take an hour or two, depending on how well the conversion goes.

  Are you sure? yes | no

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates