Some background: I want to create a CPC MicroSD interface, so my first step is trying to hook the CAS* jumpblock entries just like AMSDOS and other DOSes do.

My first goal is to override CAS CATALOG so that Basic's CAT does not try reading from the cassette and instead prints something and returns immediately to Basic's prompt (just like CAT does when AMSDOS is loaded).

After inspecting Basic 1.0's listing (see D246 for Basic's CAT code) I discovered Basic also calls the CAS IN/OUT ABANDON functions (in D2AD) so I'll hook those too just in case.

I made a ROM (see the listing at the bottom of the question) that hooks the CAS CATALOG, CAS IN ABANDON and CAS OUT ABANDON functions and prints their names instead.

Everything seems to load and run fine (I'm using WinAPE), but when I type CAT in Basic, after the hooked CAS* functions are called (and their names are printed, so the hook worked)... the CPC is stuck reading from tape! I get no other message, and the only way to get the "Ready" message and keyboard response is to either press ESC to trigger a *break* or play a tape. Playing a tape from start to finish produces no output at all, but once finished cataloguing returns to Basic's prompt.

What am I doing wrong?

Here's AMSDOS which instantly goes back to Basic prompt (indicated by the Ready message) as I want mine to do:

enter image description here

And here's my ROM waiting for tape data:

enter image description here

My ROM after pressing ESC:

enter image description here

My ROM Listing

hello.rasm (works with RASM).

save "hello.rom",#c000,#ffff-#c000

org #c000

os_reset equ #0000
os_txt_output equ #bb5a
os_cas_in_abandon equ #bc7d
os_cas_catalog equ #bc9b
os_cas_out_abandon equ #bc92
kl_curr_selection equ #b912

  db 1
  db 0,0,0
  dw rsx_commands

  jp init
  jp hello
  jp reset

  str "TEST ROM"
  str "HELLO"
  str "RESET"
  db 0


  push ix,bc

  ;; Reserve 9 bytes for RST 18h args
  ld bc, -9
  add hl, bc
  inc hl ;; HL = first available byte

  push af
    push hl ;; load hl into ix
    pop ix

    call kl_curr_selection ;; A = current upper ROM number

    ;; BB AA XX -- FAR CALL to AABB in ROM XX
    ld (ix+0), lo(my_cas_catalog)
    ld (ix+1), hi(my_cas_catalog)
    ld (ix+2), a
    ld (ix+3), lo(my_cas_in_abandon)
    ld (ix+4), hi(my_cas_in_abandon)
    ld (ix+5), a
    ld (ix+6), lo(my_cas_out_abandon)
    ld (ix+7), hi(my_cas_out_abandon)
    ld (ix+8), a
  pop af

  ;; Patch jump block entries with FAR CALL into our ROM
  ;; (HL) = DF LL HH = RST 18h (HHLL)
  ld bc, 3
  ld ix, os_cas_catalog
  ld (ix+0), #df
  ld (ix+1), l
  ld (ix+2), h
  add hl, bc
  ld ix, os_cas_in_abandon
  ld (ix+0), #df
  ld (ix+1), l
  ld (ix+2), h
  add hl, bc
  ld ix, os_cas_out_abandon
  ld (ix+0), #df
  ld (ix+1), l
  ld (ix+2), h
  ld bc, -6
  add hl, bc

  ;; Print copyright message
  push hl
    ld hl, str_init
    call print_str
  pop hl

  ;; HL = last available address for next ROM
  dec hl

  pop bc,ix


  push hl
    ld hl, str_hello
    call print_str
  pop hl

  jp os_reset

;; ----

    ld a, (hl)
  or a
  ret z
  call os_txt_output
  inc hl
  jr print_str

  push hl
    ld hl, str_cas_catalog
    call print_str
  pop hl

  ;; reset Z (preserving A)
  push bc
    ld b, a
    ld a, 1
    cp 0
    ld a, b
  pop bc

  ;; set carry flag

  ;; !Z && C == success

  ld hl, str_cas_in_abandon
  call print_str

  ld hl, str_cas_out_abandon
  call print_str

str_init: db " kaoD testing",10,13,10,13,0
str_hello: db "Hello world!",10,13,10,13,0
str_cas_catalog: db "CAS CATALOG",10,13,10,13,0
str_cas_in_abandon: db "CAS IN ABANDON",10,13,10,13,0
str_cas_out_abandon: db "CAS OUT ABANDON",10,13,10,13,0
  • Have you been able to try this on hardware? – knol Oct 15 '20 at 15:51
  • @knol unfortunately nope :( I can provide a binary if anyone with a M4 or similar is willing to try. – kaoD Oct 15 '20 at 15:58
  • Hi! Congrats about your approach, very interesting. Not sure what it could be. Perhaps some return state is different between what caller expects and what your code returns. What I would do in this situation is trace in debugger what happens after my command returns. Please keep us informed! – Stéphane Gourichon Oct 16 '20 at 6:57
  • @StéphaneGourichon I tried that too (been staring at the debugger for days), but I couldn't find a minimal working example and following AMSDOS is not easy (still trying, though!) Thank for the encouragement! – kaoD Oct 16 '20 at 8:51
  • Hmm, I see you've taken care of setting Carry to true, Zero to false, and even preserved the other registers, even though Soft968 says "BC, DE, HL, IX and other flags corrupt.". Same for CATALOG, IN ABANDON, OUT ABANDON. Don't despair, keep up the good work! – Stéphane Gourichon Oct 16 '20 at 8:58

As I was expecting, it was an error in my code :) pelrun was very kind and found the bug (see my question on CPCWiki's forum) so props to him.

I'll try to explain the issue:

Notice I'm hooking into the jump entry by setting up a FAR CALL. Notice how that's a FAR CALL and not a JUMP? There's an extra return address there that shouldn't be pushed into the stack!

So, as soon as the FAR CALL ends, the return address is popped and it arrives right into the CAS WRITE jump entry (which I didn't hook) so the cassette routine starts, which explains the behavior.

So why am I using FAR CALL and not a jump? Because it's the only way to call into an arbitrary upper ROM (where my code resides).

Both AMSDOS (listing, see CD30) and M4 (listing, see fio_jvec) do a double trick there: first they remove the unwanted return address from the stack, and then they use it to address their own jump table.

The second trick is not needed (useful nonetheless) since I'm not hooking many CAS entries (for now), but the fixing of the stack is needed and indeed does fix my use.

Now, I'm not sure how to proceed but AFAICT these are my options:

1. Don't use FAR CALL and instead just jump

But this would require storing the routine in RAM (and will probably end up reimplementing a poor-man's version of FAR CALL).

2. Keep using FAR CALL (using the AMSDOS trick, or individual hooks) and manipulate the stack manually

I'm going to guess that, since AMSDOS and M4 use this, every known firmware implements FAR CALL as having the bad return address on SP+6.

3. Patch a jump into a FAR CALL immediately followed by a ret (thanks andycadley at CPCWiki forum)

This is slightly longer in RAM (3 bytes for the FAR CALL buffer + 3 bytes for the RST and in-band address + 1 for the RET) but much simpler.

I asked the same question on CPCWiki's forum in case I missed an alternative, and will update this answer accordingly.

  • Thanks for sharing the answer, this will be useful for others trying to hook the filesystem drivers. – Mark Williams Oct 25 '20 at 10:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.