Close
0%
0%

ROM Disassembly - Cefucom-21

Peering into the soul of this obscure machine

Similar projects worth following
Reference to an obscure early 80's era Computer Aided Instruction machine came across my desk, and there were ROM dumps, so as they say: "have ROM dumps; will disassemble" (well, if "they" is "me", at least...).
I suspect that the resulting disassembly is less interesting than the voyage of discovery I'm logging here. "It's not the destination, it's the journey." They say that, too, though this time "they" is not "me".

Overtly, this project provides a disassembly listing of the Cefucom-21 system, which is a vintage Computer-Aided lnstruction device (c. 1982) for teaching English to native Japanese speakers.

Who would care about such?  Maybe no one, unless you have one of these rarities and want to hack.

Less overtly, this project is a chronicle of of my activities and discoveries along the way, which is useful for me to record, but I though could be interesting to someone else to see the process, so I decided to publish it here instead of in my usual private journal.

It will be a little bit of a challenge in that I do not have the hardware, nor schematics.  Just the ROM dumps.  Well, and some photos of the boards, which are immensely helpful.  And also now knowledge that this system is based on a then-existing general-purpose system -- the Sanyo PHC-25.  That system itself is pretty obscure as it seems to have done poorly in the marketplace.

But the fun is in figuring things out, so those handicaps add something....

  • 20241119 - Setting Up SDCC for the Z-80

    ziggurat2911/20/2024 at 17:00 3 comments

    As mentioned before, I am attempting to make a custom ROM for the Cefucom PCU to help in reversing the hardware.  I have no idea whether the folks that have the actual unit in their possession would be game for such, but I'm continuing on as a mental exercise, and maybe, who knows, they might take me up on it.

    I also mentioned that I am going to make my life harder by trying to bring up a C-based environment rather than simply cranking out some bespoke assembler like a professional would do in the real world.  To do that, I am going to use SDCC [https://sdcc.sourceforge.net/].  This project has been around for a very long time and realizes a pretty rich C toolchain for old-school CPUs, including the Z-80.  It is a labor of love, however, and as such there are some things that can be frustrating.  E.g. there is pretty rich documentation, but it often does not explain what I am trying to find out or seems a little out of date.  But hey that's what you get for 'free, and uncompensated work'.  So we have to soldier on...

    If you're familiar with Z80, you'll know that reset is always at address 0000h, that there are special locations for 'restarts' on 8-byte boundaries, that there is location 0066h which is the NMI handler, that IM1 uses rst38h, and that IM2 uses a vector table you put in memory somewhere on a 256-byte boundary.  So your code has to honor that stuff.  Also you have to do the usual things of informing the tools of your memory map.

    I found the process to be quirky, but long story short I did get something working correctly.  As to whether this is the orthodox way, I do not know, because I did not see a lot of tribal wisdom during my searches.  So I want to document my findings for posterity.  (Which is possibly just me, and that's fine, too.)

    • The tools are apparently meant to be run by invoking 'sdcc', which seems to be a front end that will select the actual compiler and invoke the tools in some ways.  This works, but there are caveats:
    • There is a default target processor of 8051 and if you forget to specify, say, -mz80, you will encounter the sadness of inscrutable error messages about pdata sections etc.  I'd rather there be no default, but I'm sure that's legacy behaviour because this project started as an 8051 toolchain before it grew legs.
    • Another caveat is that sdcc will compile one source file only.  So if you need more than one (what project doesn't) then you will need to use -c to 'compile only' and then a separate invocation to link the object files, which are named *.REL.  And you will need to specify the target processor again lest you get the inscrutable error messages for a different cpu.

    If you compile a single source file as described in the docs:
        sdcc -mz80 main.c

    the tool will compile that, generate a 'linker script' of sorts, and link with a pre-built crt0.rel and standard library.  I will make all sorts of assumptions about your platform, like that you have 32 KiB RAM and 32 KiB ROM, have no interrupts, etc.  Maybe you do, maybe you don't.

    The tool does emit assembler, but not of the final linked binary.  So I disassembled that myself as I went along to truly know what was going on, despite it being a chore.  But I'm glad I did because it was clear that out-of-box the tool definitely did not place things were I needed them to be.  I'm going to save you the story of the journey, and just provide my end findings.

    • You will almost surely want to customize crt0.s.  So grab a local copy out of installed toolchain and hack that.
    • The default crt0.s puts stub ISRs at all the RSTs.  I find this vexing.  So I commented all that out.
    • You will also set up the stack pointer.  This is done right away, and has a hard-coded default at 0000h.  Maybe you want it somewhere else.  Maybe there's no RAM at top of memory.  E.g. I am modelling an existing system with...
    Read more »

  • 20241116 - Front Panel LEDs and Buttons

    ziggurat2911/19/2024 at 13:54 0 comments

    Today I received from [Nigel] an alternative ROM dump of another instance of the machine's PCU board.  This is from a later revision than [Rees'] and I refer to it as the [Tony] version.

    There was some concern that Rees' dumps may have suffered bit rot since there were unrepeatable reads of the last ROM 8-2.  I did a casual diff and there were over 5,000 differences, so I had to dig in.

    At length I found that there were a couple bytes of new code added fairly early in the image, but just that small change causes all the subsequent routines to move out a bit, and so there were a huge number of "benign" differences.

    Locating the material change was a bit of a needle-in-a-haystack, but what I found was that [Tony's]
    version simply disables 'mode 5' -- the planned-for-but-incomplete 'information' feature.  There was also some removal of a section of data lump later in an undecoded witchcraft script, but I suspect that it will be perhaps some menu text.  (This system is Japanese, so I can't read it with my eyes.  Oh, yes, btw, I'm pretty sure the character encoding is "JIS X 0201".)

    While the loss of the feature is in some way sad (though the implementation is all still right there, and also you could simply put in the old roms to get it back), the stopper that was added put me on the track of the mode selection menu.  What was added was a test for a case of '5', which is ignored.  There were existing rejected cases of 0, 7, and 8.  So the coincidence of the number, plus the case 7 and 8, but no more, made me think that these numbers directly associate with the front panel buttons.  The buttons are soft keys with different meanings in different context, such as when you're using the 'mode 4' tape recorder.

    Following along the path of variable-with-the-button number back to the hardware, I was able to piece together with high certainty that the buttons are simple SPST w/pullup on port 68h (8255c-a).  I also found the associated key scanner and debounce routine in sub_1A5F, which is invoked from isrCTCb0_1A48 -- the periodic timer ISR.

    There is a debounce count of 4.  I tried to game out plausible debounce periods (e.g. I usually go for 50 ms myself) backwards to a sensible input clock, and the best solution I could find was 1 MHz.  I'm a little disappointed I didn't make it to the system clock of 4 MHz (as I was able to with CTCa-0 and -1), but there are some 7474's on board so maybe there is external division.  Just a guess.

    As an added bonus, there are also front panel LEDs that light to indicate mode, so I was able to correlate them to another port 65h (8255b-b).  That is 5 of the bits, and since there are three more LEDs on the panel (indicating learning progress), I suspect that is the function of those other three.  (I don't know enough about the learning system yet to be conclusive.)

    So!  Two PPI ports fully deciphered, and all without a physical sample.  Just puzzling though code.  (And OK the operating manual provide critical context.)

    But I will hit a limit on what I can decipher from code alone, so I'd really like it if some of the folks with the hardware can do some buzzing-out and whatnot.  This gave me one of my kooky ideas:

    • I should be able to make a custom firmware that puts the ports in a known state which can easily be verified with a multimeter.  E.g. There are three PPIs which I call 'a', 'b', and 'c' solely based on their port number in the code, but what physical chip is that?  What is the part designation?  Same with the CTCs.  (There's only one PIO, so that's unambiguous.)
    • I wouldn't presume to configure any of the ports differently than what the actual firmware does in fear of causing some damage (though I doubt it based on the board shots), but each of those PPIs has at least one output port.  So notionally I could, say, set b7 high on one device, b6 high on another device, and b5...
    Read more »

  • 20241115 - Witchcraft

    ziggurat2911/15/2024 at 20:48 0 comments

    As mentioned yesterday, there is a section of code that is confusing.  There are a few call levels deep with return address manipulation and indirection that obscure the flow of execution.  But it needs to be figured out because the mechanism is used with some frequency (16 known instances) and operates on sizeable hunks of embedded data.

    4ECC  sub_4ECC:
    4ECC CD 14 5A  call    sub_5A14
    4ECF 0C        db  0Ch      ; lump o data
    4ED0 0A        db  0Ah
    4ED1 D7        db 0D7h
    4ED2 4E        db  4Eh
    4ED3 00        db    0
    4ED4 C3        db 0C3h
    4ED5 E2        db 0E2h
    ...

    The routine eats the return address into HL (which points to the lump o data) and then calls something else, and then jumps to whatever was left in HL. 

    5A14  sub_5A14:
    5A14 E1        pop     hl
    5A15 CD 19 5A  call    sub_5A19
    5A18 E9        jp      (hl)

    The 'something else' it calls, calls yet something else, and apparently does an infinite loop.  I.e., not returning to the caller to provide HL to which to jump.

    5A19  sub_5A19:
    5A19 CD 1E 5A  call    sub_5A1E     ; do something with HL pointing to a lump o data
    5A1C 18 FB     jr      sub_5A19     ; infinite loop?

    The 'yet something else' it calls gets a code at the start of the lump o data, and increments the pointer past that, and does some checking and mapping of value before invoking my old friend 'sub_5905' which is known to dispatch via a 'subsequent table'.

    5A1E  sub_5A1E:
    5A1E 7E        ld      a, (hl)
    5A1F 47        ld      b, a         ; remember the code
    5A20 23        inc     hl           ; adjust data pointer to after the code
    5A21 FE B0     cp      0B0h
    5A23 38 08     jr      c, loc_5A2D
    5A25 FE C0     cp      0C0h
    5A27 3E 10     ld      a, 10h
    5A29 38 02     jr      c, loc_5A2D
    5A2B 3E 01     ld      a, 1
    5A2D  loc_5A2D:
    5A2D CD 05 59  call    sub_5905
    5A30 52 5A     dw sub_5A52
    5A32 56 5A     dw sub_5A56
    5A34 6C 5A     dw sub_5A6C
    5A36 71 5A     dw sub_5A71
    ... 17 of such subroutines

    I had seen sub_5905 late last week when I was cataloguing the various direct dispatch tables.  The gist being that a table of functions follows the call, and A indexes into that table and finally a jump (effectively) is made to the selected address.

    5905  sub_5905:
    5905 E3        ex      (sp), hl                    ; HL now has the table address (which followed the call site)
    5906 CD 0B 59  call    sub_590B                    ; HL = * ((WORD*)HL) [A]
    5909 E3        ex      (sp), hl
    590A C9        ret                                 ; effectively a jump

    Those were simpler times.  The 'witchcraft' method uses a similar 'lomp o data follows what looks like a call' technique, but the data is not a simple table.  And the interstitial infinite loop adds a twist.  My limited mental faculties required that I page out to paper, and kept track of the call stack on the way down to the dispatched functions:

    stack:
         ret 0 = witchcraft_01, sub_5A19, infinite loop
         ret 1 = witchcraft_00, sub_5A14, thunk via HL
    on entry:
         B = code dispatched upon
         HL = pointer into data after the code

    So, it seems this witch knows 17 'spells', and is given a lump of data as an 'incantation' specifying a sequence of spells to cast along with contextual data.  So it's time to take a closer look at her spell repertoire.

    The first 'spell', referenced by code 0, is brief:

    5A52  witchspell00_5A52:
    5A52 33        inc     sp
    5A53 33        inc     sp
    5A54 C9        ret

    By bumping SP twice, we effectively eat a return address.  Often times we do that with a pop, but I guess the author did not want to clobber any registers at all and chose the two increments instead.  The net effect is to return to the caller's caller, 'ret 1', which gets us past the infinite loop to where we thunk over via HL. 

    The second spell involves more stuff than I want to dig into right now, and moreover can be entered multiple ways so I'm going to save that for later.

    The third 'spell', referenced by code 2, is straightforward:

    5A6C  witchspell02_5A6C:
    5A6C 5E        ld      e, (hl)
    5A6D 23        inc     hl
    5A6E 56        ld      d, (hl)
    5A6F 23        inc     hl
    5A70 C9        ret

    So it loads DE (little-endian) from the data pointer, and advances the data pointer past that.  Notably it does a ret, so it goes back to 'ret 0', which is the infinite loop. ...

    Read more »

  • 20241114 - Virtual Machine Summary

    ziggurat2911/14/2024 at 21:54 0 comments

    Yesterday I mentioned this apparent 'virtual machine' embedded in the Cefucom product, and I spent some time figuring how the it do.  The machine workings are pretty clear now, and I have done 66 of the opcodes (10 more to figure out).  Here's the scoop so far:

    Overview

    *  basic unit of execution is a 'block'

    *  block structure is:
      *  opcode
      *  parameters...  Number of parameters is opcode-specific.  So a block is between 1 and 7 bytes.
    *  a 'program' is a series of blocks
    *  rst 8 is the 'primitive block executive'.  It is not typically used directly.
    *  rst 10 is the 'sequenced block executive', and is the primary way of executing 'programs'
    *  is BIG-ENDIAN
    *  has absolute addresses
    *  indices are 1-relative
    *  has various functional groups:
      *  load/store; 8 and 16 bit, references and constants
      *  memset/memcpy
      *  arithmetic; addition/subtraction, usually accumulator form (e.g. *parm1 += *param2)
      *  bitwise; and, or, xor (note, no 'not', though I suppose you can synthesize that from xor)
      *  shift
      *  goto
      *  computed goto (well, 'indexed'; the computation would be done separately)
      *  if (and if-not)
      *  next
      *  'usr' (call out to an assembly routine)
      *  'run' (another program)
      *  some Cefucom specific opcodes; probably added by the company


    other notable aspects

    *  The C register is used to store flags, where appropriate.  80h = carry, 1 = non-zero/no-carry, 0 = zero.  The C register is actively preserved between block execution.

    *  A couple instrucstion place the result in B or DE, but these are not preserved between blocks.  (I need to look more into this when I find a program that invokes them; and maybe there are no instances of such.)
    *  the RST 10 implementation realizes a few more outside of the dispatch table:
      *  7F - NOP
      *  7E - exit on no-carry
      *  7D - exit on carry
      *  7C - exit on non-zero (or carry)
      *  7B - exit on zero
    *  the implementation speculatively loads param1 into HL and param2 into DE, which is useful for most instructions, but some do have less parameters (e.g. END).  This is a potential out-of-bounds read, but will not cause problems on this hardware.
    *  there are two opcodes 62h and 63h which have little-endian parameters param1 and param2, while param3 is big-endian.  I think this is a bug, though it might originate from a detail of their build process.
    *  opcode 43 is also little-endian, which runs a rst 10 program.  This is interesting because there is already opcode 3e that does the same thing with a big-endian program pointer, so there must have been felt a special need.

    The remaining opcodes deal with Cefucom structures which I do not yet understand, so I might put those off for the moment in the interest of making progress.

    One vexing thing has been some functions that do some stack manipulation such that the execution is no longer linear.  I figured out some of those, like the ones that have a dispatch table that follows a call.  But there are some others that have more convoluted shenanigans.  I marked those as 'witchcraft' so that I can find them more easily when I come back to them.  Perhaps now is the time to look into this witchcraft.

  • 20241113 - An Embedded P-Code-esque Virtual Machine?

    ziggurat2911/13/2024 at 16:05 0 comments

    As mentioned before there are a lot of tables, and even a couple of tables of tables.  Some are lists of 'described text' (having a header indicating position), some are indexed dispatch tables, some are double-dispatch, and the others are presently unknown.

    I had previously thought the table-of-tables form was a double-dispatch mechanism, but it's not.  Rather it's a list
    Often they are processed by RST 10, which delegates processing of the elements to RST 8.  The elements are variably-sized blocks.  They seem to have the structure:

    uint8_t     fxn;
    uint16le_t  param1;
    uint16le_t  param2;
    ...         payload;

    (I am coining the term 'uint16le_t' because I have found many places that are big-endian! So elsewhere I use 'uint16be_t' for that.)

    Often the blocks are short, like this:

    7817 62      byte_7817:db 62h
    7818 95 7A   dw unk_7A95
    781A 11 00   dw 11h
    781C 00      db 0

    but others can be quite long.  I originally thought param1 is a pointer, and params is a length, because the RST 8 code that processed them immediately loads param1 into HL and param2 into DE.  Indeed sometimes those are used as pointers and lengths, but in other cases they are not.  But for starters I look at the ones at off_7803, which is a list of these things which have just one element in them, and seem to follow the (ptr,length) assumption.  The trailing zero is interesting.  All the ptrs and lengths worked out sanely when updating the the disassembly. 

    These blocks are ultimately processed by RST 10.  This is a block sequencer, feeding individual blocks to RST 8 (which expects the block pointer in IX).  A null function code terminates processing, so that explains the trailing 'db 0' above.  So there is no explicit payload length of a block; it depends on function code.

    RST 8 dispatches servicing through dispatch_4002, which has 128(!) entries.  I went through and labelled each of them like 'fxn00'.  Many of them are apparently unimplemented as their slot directs to 'fxn00', which was found to be used as the block sequence terminator.  It does have an implementation, though, which is to back up IX by 4.  This is interesting because fxn00 will not be dispatched by RST 10, but it would for the ostensibly unimplemented function codes.  Turns out that RST is optimistically loading param1 and param2 and incrementing IX just past.  So the IX-=4 is to undo that optimistic loading and continue at the byte following the unimplemented function code.  So 'ignore unimplemented function code'.

    In the end, there are 76 implemented and 52 unimplemented functions.

    After labelling, I looked at fxn62h, since that was the the code used in the blocks above.  The implementation boggled my mind a bit.  It did some sort of queueing into c200, which is structured as 32 8-byte entries.  A gave up with that and scrolled through the nearby disassembly casually and found fxn63 just below it, that also did something similar with c200, and then invoked my buddy RST 10 -- the 'block list dispatcher'.  So if fxn62 puts it in, and fxn63 takes it out and processes it, this seems evocative of a 'load' and 'run' functionality.  Anyway, there was still too many unknowns so I started to look for smaller fish to fry.

    Scrolling through I found some shorter ones that I could comprehend, and coincidentally these tended to be the smaller numbered function codes.  The first one I found was:

    43FD  ; XXX 3f: dispatch to param1 (no param2)
    43FD  fxn3f_43FD: 
    43FD DD 2B  dec     ix  ; (no param2)
    43FF DD 2B  dec     ix
    4401 E9     jp  (hl)    ; thunk over

    So this would run arbitrary external code.

    Another is a conditional 'goto' of sorts, where the block processing is directed to another place (hopefully within the list!) if --*((uint8_t*)param2) != 0:

    43D3  ; XXX 3c: goto block @ param1
    43D3  fxn3c_43D3:
    43D3 EB     ex  de, hl
    43D4 35     dec     (hl)
    43D5 28 03  jr  z, leave_43DA   ; leave if --*((uint8_t*)param2) == 0
    43D7 D5     push    de
    43D8 DD...
    Read more »

  • 20241111 - Catching Up and Publishing Work

    ziggurat2911/13/2024 at 11:16 0 comments

    Last week's PCU push put me further behind in posting, so I caught up those log entries.

    Also, I don't know why I didn't already, but I put the listing in a github so others can check it out if they are curious.  There's a link in the project's 'link' section, and also here if you happen to be reading this entry:

    https://github.com/ziggurat29/cefucom-21.git

    It's a pity I didn't start this at the beginning so that I could see the history of this past 3 week's work, but oh well.  I was moving fast then.  Still am.

    Now that all the puzzle pieces are on the table, it's time to see what picture emerges when solved.  There's no depiction on the packaging. ;)

  • 20241110 -- PPI (8255) Catalogue

    ziggurat2911/12/2024 at 22:55 0 comments

    Today I went though all the references to the 3 Programmable Peripheral Interface (PPI; 8255) devices.  To my relief they are configured once and stay that way.  Also, they are all set to be garden-variety gpio -- no special modes.

    8255 Summary:

    8255a:
    Port A: in
    Port B: out
    Port C: in

    8255b:
    Port A: out
    Port B: out
    Port C (upper): out
    Port C (lower): in

    8255c:
    Port A: in
    Port B: in
    Port C: out

    Mostly I don't know what these do, though, because I have no schematic.  It's worth noting that my nomenclature of "8255a" and "CTCa" is purely made up.  I don't know what actual device on the board these are associated with.  Otherwise I'd use the parts marking on the board.  If I ever find out, then I'll update the info.  This did send me on a side activity of going through the screen shot and collecting all the parts.  (Well, just the IC's.)  So I made a BOM for future reference.  The BOM occasionally gives me some ideas; e.g. there are two 7474's on board.  Maybe clock dividers?  And if someone has a physical board and is willing, I can be specific about requests to buzz out specific pins.  (Especially the address decoders to the various chip selects would be handy!)

    I can say that 8255b-a is unused, and that the unusual 8255b-c is used for the bit-bang serial, and possibly RTS/CTS.

    Watchdog?

    In the course of this, I found a bit which is polled periodically, and will reset a down counter.  If the counter reaches zero, a request for reboot is flagged.  This check is registered in the CTCb2 task list (entry 1).

    0D7D  isrCTCb2task01_watchdog_D7D:
    D7D DB 69      in  a, (69h)              ; 8255c-b; checking b6
    0D7F 47        ld  b, a
    0D80 DB 69     in  a, (69h)              ; 8255c-b; checking b6
    0D82 B8        cp  b
    0D83 20 14     jr  nz, resetWatchdog_D99 ; transitioned during subsequent reads; lets try again
    0D85 CB 77     bit     6, a
    0D87 20 10     jr  nz, resetWatchdog_D99 ; b6 = 1 == system ready?
    0D89 3A 69 C0  ld  a, (byte_C069)        ; XXX a watchdog timer; initted to 5 on 69h b6 set (system ready)
    0D8C 3D        dec     a
    0D8D 32 69 C0  ld  (byte_C069), a        ; XXX a watchdog timer; initted to 5 on 69h b6 set (system ready)
    0D90 20 0C     jr  nz, nullsub_10        ; leave
    0D92 3E FF     ld  a, 0FFh
    0D94 32 68 C0  ld  (byte_C068), a        ; XXX flag causing warm boot (or maybe sleep) in main task 00
    0D97 18 05     jr  nullsub_10
    0D99         resetWatchdog_D99:
    0D99 3E 05     ld  a, 5                  ; give it 5 chances to come back
    0D9B 32 69 C0  ld  (byte_C069), a        ; XXX a watchdog timer; initted to 5 on 69h b6 set (system ready)
    0D9E         nullsub_10:
    0D9E C9        ret

    I'm calling this a 'watchdog' for now, but it might actually be something else that isn't supposed to stay low for too long.  Maybe a sensor of some sort.

    Ring Counter?

    Maybe the most interesting find was three bits that are configured in what appears to be a ring counter:

    5D44         sub_5D44:
    5D44 3A 02 81  ld  a, (byte_8102)
    5D47 32 03 81  ld  (byte_8103), a
    5D4A 21 B0 80  ld  hl, byte_80B0
    5D4D CB 96     res     2, (hl)             ; clear
    5D4F CB 8E     res     1, (hl)
    5D51 CB 86     res     0, (hl)
    5D53 3D        dec     a
    5D54 28 07     jr  z, was0_5D5D
    5D56 3D        dec     a
    5D57 28 07     jr  z, was1_5D60
    5D59 3D        dec     a
    5D5A 28 07     jr  z, was2_5D63
    5D5C C9        ret
    5D5D        was0_5D5D:
    5D5D CB CE     set     1, (hl)
    5D5F C9        ret
    5D60        was1_5D60:
    5D60 CB D6     set     2, (hl)
    5D62 C9        ret
    5D63        was2_5D63:
    5D63 CB C6     set     0, (hl)
    5D65 C9        ret

    and later: 

    047D F3        di                  ; critical section
    047E 3A B0 80  ld  a, (byte_80B0)  ; XXX a bitfield; also goes out 8255b-b
    0481 E6 07     and     7           ; just the lower 3 bits
    0483 32 B0 80  ld  (byte_80B0), a  ; XXX a bitfield; also goes out 8255b-b
    0486 FB        ei                  ; end critical section
    0487 D3 65     out     (65h), a    ; 8255b-b

    A ring counter is a bit peculiar and in this product maybe drives a 4-wire 3-phase stepper motor.  Such a motor might be useful for advancing the paper roll display.

  • 20241109 -- Tables, and Tables of Tables

    ziggurat2911/12/2024 at 18:04 0 comments

    There's still a fair amount of unexplored data and code.  Can't say much for data, but code you can figure out where functions demarcate in many cases just by looking for the C9 that is the RET that often punctuates a function end.  I found several 'orphan' functions this way.

    As a new tack, I searched the binary for the orphaned function's address.  Ostensibly to find a call site, but many times I found it in a list of addresses that mapped to other orphaned functions.  So I had found some dispatch tables.  Oftentimes is was easy to work backwards from such to find the start of the function table because it would abut some code ending.  Then I could do a similar address search to find the code that dispatches through the table.

    It was a bit tedious, but there are presently 42 such dispatch tables currently found!  And just for fun, it turns out that there are dispatch tables of dispatch tables in some cases!  There is a huge table of 128 entries at 4002h, a double-dispatch table at 2DA6 (with associated paramters table at 2DE0) and another double-dispatch table at 2200.

    With that many dispatch tables, this surely is some state machine design.  To think I was daunted by the one in ROM 4!  This is proportionately larger.  But who knows, this might be a gift as it might make the intent clearer.

    Bloopers

    Some amusing treats were nullsubs that have a subsequent jump to the nullsub.  (The code in the subsequent jump is not referenced anywhere.)

    3E9C  nullsub_4:
    3E9C C9        ret
    3E9D C3 9C 3E  jp      nullsub_4

    or

    3EA8  nullsub_5:
    3EA8 C9        ret
    3EA9 C3 A8 3E  jp      nullsub_5

     And a dispatch table of jump addresses that ends with a ret; lol.

    ...
    40FC 9E 41  dw sub_419E    ; XXX IX -= 4
    40FE 9E 41  dw sub_419E    ; XXX IX -= 4
    4100 9E 41  dw sub_419E    ; XXX IX -= 4
    4102 C9     ret

    My personal favorite: 

    ...
    0475 18 00  jr  loc_477  ; well let's jump right on that!
    0477      loc_477:
    ...

    I think more symptoms of machine generated code.

    It took a day, but it got me to my desired 100% code coverage milestone.  Now that I have the puzzle pieces, it's time to see what picture emerges.

  • 20241108 -- PCU; RSTs and Rendering and Tasks and Serial

    ziggurat2911/12/2024 at 15:03 0 comments

    Desultorily rummaging through the code...

    RSTs

    There are only a few RSTs implemented in this unit.  Some are covered with FF and so don't do anything (well, they'll reboot since FF is coincidentally 'RST 0').

    RST 0 starts the party with 'ld  sp, C800h' and 'jp  boot_100'.  Seems OK, except boot_100 immediately changes it's mind to 'ld  sp, C7ffh'; lol.  Incidentally c800 is ostensibly more sane since SP is pre-decrement.  So C7ffh wastes the last byte.  But this is a fun system.

    RST 8, 10, and 18 do something.  RST 20, 28, 30, and 38 are in FF padding.  That padding continues out to the NMI 66h which I already describe it's special-ness.  It is immediately followed by one of these:

    0082  nullsub_7:
    0082 C9  ret

    "One of these" because there are nearly 40 such empty functions throughout the code.  So surely there's one within a relative jump range if you need it!  Joking aside, this is giving me the vibe that at least some of this code is machine-generated -- i.e. compiled or some other generation tool.  It was the 80s.  Throwing out redundant code is the linker's job, and there were business selling fancy linkers that had the smarts to do that.  Phar Lap is one that comes to mind.  I haven't discerned an obvious calling convention, though.

    RST 18 is the easiest to understand.  I simply vectors to an instruction indexed by A, 0-4.

    Function 01 was the first easiest as I was able to use my recent experience with the RTC to see that it was reading the clock and updating the values in RAM as BCD.  It confused me for a moment at the end where it OR'd all the values on top of each other and then masked that with 0f and conditionally invoked sub_DF9, until I realized that it was just checking for midnight.  So sub_DF9 does things at midnight.  It's only checking hours and minutes, so this needs to be invoked at least once a minute or it will miss it.  Also, it needs to be invoked not more than once a minute unless the midnight tasks are idempotent.

    RST 8 and 10 are more involved.  They operate on blocks of data.  The blocks have the form:

    00  code    8-bit
    01  param1  16-bit
    03  param2  16-bit
    04  ... data

    RST 8 expects the block to be referenced through IX
    RST 10 does a little more, expecting a sequence of blocks in HL, and then dispatching them via RST 8.

    What is amusing is that most of the code does not invoke these functions via the RST instruction.  Rather, they make the traditional 3-byte call to the underlying implementation.  There are no invocations of RST 8, and only one of RST 10.  All the invocations of rst 8 are direct (OK, there's only 3) and 24 direct invocations of rst 10.  So why bother?  Again, it seems to be more likely some machine generated code -- humans don't code this way.

    Tasks

    [Nigel] made a little headway getting past the memory test failure, but is stuck in a new loop.  His instruction trace ended up looping around here:

    ...
    01FC 3E C0     ld      a, 0C0h        ; IM2 vector base is C000h
    01FE ED 47     ld      i, a
    0200 ED 5E     im      2
    0202 FB        ei
    
    0203         again_203:
    0203 AF          xor     a
    0204 32 62 C0  ld      (byte_C062), a  ; XXX index into function table @ C040
    0207         next_207:
    0207 3A 62 C0        ld      a, (byte_C062)      ; XXX index into function table @ C040
    020A 07        rlca
    020B 06 00     ld      b, 0
    020D 4F        ld      c, a
    020E 21 40 C0  ld      hl, word_C040  ; XXX an array of 16 fxnptrs indexed by C062
    ... stuff
    022E 3C        inc     a
    022F FE 08     cp      8
    0231 28 D0     jr      z, again_203
    0233 32 62 C0  ld      (byte_C062), a  ; XXX index into function table @ C040
    0236 18 CF     jr      next_207
    ...

    Turns out this is exactly where it should be.  This is the main() loop!  What it does is whizz through a list of function pointers, invoking them, and then repeating that forever.

    The first task is mainTask00_default_2C3, and that is the one that check for the 'do warm boot' flag set by the NMI that we looked at yesterday!

    A curiousity is that there is room for 16 tasks, but only ever...

    Read more »

  • 20241107 -- Back to PCU Grindstone

    ziggurat2911/11/2024 at 23:28 0 comments

    Reflecting on yesterday's experience with ROM 4, I'm a little daunted by what lays ahead.  That was a 2 KiB ROM.  This is a 32 KiB ROM, so 16 x as much to cover.  At the same time, the understandings I got from ROM 4 might provide context that speeds things along.

    The PIO A is configured much like on the MCU2, with differences in bit 7 and 6.  On the MCU2 b7 is the interrupt in, and b6 is unused.  On the PCU they are both outputs.  B7 does something.  B6 seems unused so far.

    PIO A does not provide an interrupt source as it does on MCU2, but there are plenty of timers, and in particular CTCb0 is configured free running. It prescales by 256 and has a time constant of 3fh, so that would imply a total division of 16384.  I don't know its clock source.  If it was the 4 MHz system clock, then that would be 244 per second, and if that were externally prescaled by 4, then that would be 61 per second.  Again, I don't know if any of that is the case.

    What I did negatively confirm is that there seems to be no systick as with the other board.  There is a counter that is incremented, but it is 8 bits and it saturates rather than rolling over.

    I was eager to find such a tick, because my assumption was that PIO Port B would be serviced similarly here -- and it is, except for the timeout.  Had it been the case I might have been able to work out what the clock rate likely is.  Oh well.

    The PIO B ISR is freaky.  It starts off sane:

    3D48  isrPIOb_3D48:
    3D48 FB      ei                  ; allow nesting
    3D49 F5      push    af
    3D4A C5      push    bc
    3D4B D5      push    de
    3D4C E5      push    hl
    3D4D CD 56 3D    call    impl_isrPIOb_3D56       ; XXX PIO B isr implementation; invoked after saving all regs
    3D50 E1      pop     hl
    3D51 D1      pop     de
    3D52 C1      pop     bc
    3D53 F1      pop     af
    3D54 ED 4D   reti

    and then things get weird:

    3D56  impl_isrPIOb_3D56:
    3D56 4F        ld  c, a         ; XXX freaky; where was A set? non-isr code? 'expect'?
    3D57 DB 10     in  a, (10h)     ; XXX PIO A data; ostensibly 'state' (though we aren't masking off high bits for some reason)
    3D59 B9        cp  c
    3D5A 20 FA     jr  nz, impl_isrPIOb_3D56  ; loop, there it is!
    3D5C E6 07     and     7        ; mask only b2,b1,b0 ('state')
    3D5E C2 68 3E  jp  nz, sub_3E68

    So, two things: 

    1. a spin-wait in an ISR for anything tweaks my spidey-sense
    2. we are checking for a value that is specified in A, but we never explicitly set A ourselves.  A will be whatever it was in the pre-interrupt environment.  All the more curious because Port B I/O is not synchronous to this system.

    And I guess a minor thing is that we didn't mask off bits 7 and 6.

    This is a head-scratcher.  I'll have to come back to it later.  (maybe some weird coordination with non-isr code:  "wait for this and let me know".  I'll keep an eye out for that pattern.)

    Another treat:

    7FA9 3E 4F  ld      a, 4Fh
    7FAB D3 E3  out     (0E3h), a       ; XXX set PIO B mode 1 (input)
    7FAD 3E 87  ld      a, 87h
    7FAF D3 E2  out     (0E2h), a       ; XXX wut? it's now an input port, so what is write doing?

    Since [Nigel] was concerned about the port 30/38/39 stuff I took a little look at that.  On cold boot, port 38h is read, and then jumps into the warm boot routine which immediately writes it out to port 39h.  Warm boot ("boot_100") may be entered in other ways, and those ways a value left over from a previous write to both 30h and 38h is the one that is written to 39h.

    And that is the end of the story for those ports.  They don't affect code flow in direct way (conceivably they might indirectly by causing some unknown hardware to do something different).  So looking into what the 'leftover value' is that comes in on the warm boot path led me to some RTC stuff.

    02C3  sub_2C3:
    02C3 3A 6D C0  ld      a, (byte_C06D)   ; a flag set in NMI
    02C6 B7        or      a
    02C7 20 0D     jr      nz, loc_2D6      ; horror
    02C9 3A 68 C0  ld      a, (byte_C068)   ; another flag that could cause us to reboot
    02CC B7        or      a
    02CD C8        ret     z
    02CE F3        di
    02CF CD A2 0E  call    sub_EA2      ; XXX RTC alarm stuff; leaves something in A
    02D2 D3 38     out     (38h), a     ; hmm!
    02D4 D3 30...
    Read more »

View all 24 project logs

Enjoy this project?

Share

Discussions

Nigel Barnes wrote 11/04/2024 at 13:20 point

I'm currently working on emulation of the CEFUCOM, and have the majority of the hardware mapped to IO ports. I'm interested in your interrupt findings, and anything you can work out about how the two Z80 boards communicate with each other via the Z80PIO's.

  Are you sure? yes | no

ziggurat29 wrote 11/04/2024 at 13:59 point

Oh sure!  We think alike that the added PIO is for inter-board communications, and I haven't spent much time on the second board yet.  I figured this board controlled the second, and so would be better to go top-down since I don't have schematics.

Sorry I haven't posted the listing itself yet; every time I have an epiphany I get excited to disassemble more, and the writing the log posts takes time as well, and then there is so little left for the rest of life.  I'll clean up what I have presently and send it along.

Interesting you mention the emulator because I was just about to break down and make an emulator for my own purposes.  In my case it was to be just for research so I could single-step some code to help follow the data. (My mind sometimes gets boggled trying to track that mentally with just the disassembly.)

  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