wwarthen / RomWBW

System Software for Z80/Z180/Z280 Computers
GNU Affero General Public License v3.0
339 stars 99 forks source link

hbios - freertos - scz180 - access to z180 interrupt table #87

Closed feilipu closed 4 years ago

feilipu commented 4 years ago

I'm interested to use the PRT1 in the z180 to generate regular timing interrupts for my program. But I can't find in the documentation where the z180 interrupt address table is stored, or identify which (if any) HBIOS call is used to insert an interrupt handler into the z180 interrupt address table.

An alternative (perhaps supported?) would be to have the regular HBIOS tick (50Hz) call a function in my program on every tick. This would save consuming the PRT1 for essentially the same function that is provided by the HBIOS tick.

Any guidance on the best way to provide a timing interrupt within HBIOS? Or, without breaking HBIOS?

feilipu commented 4 years ago

The first thing that HBX_INVOKE does is save the callers SP. If an interrupt fires immediately after this and RTOS does a context switch, the new context could call HBX_INVOKE and clobber the SP value stored by the original context, right?

Yes. You're right! But a flag won’t be sufficient. We need a SRA mutex. Same, but different.

Since we've got multiple Tasks all trying it use HBIOS and it is not reentrant, we need to ensure that only one Task is actively in a HBIOS API call at any one time. And the tricky thing is to avoid the risk of the scheduler swapping contexts between reading a flag, and setting it busy. The atomic mutex is the way to do this.

Some background for those reading along. A MUTual EXclusion semaphore (used to protect a resource) has to do two things; 1) be uniquely testable, and 2) record a new state, and it has to do this atomically (indivisibly). With the z80 processor the SRA (HL) instruction does this perfectly. A quick search will turn up further reading.

The mutex needs to be set as free initially, and this is done by setting 0xFE in a memory location. When competing threads or Tasks wish to use the resource they issue a SRA (HL) on the location.

So in the HBX_INVOKE function, we have to "take" and "give" its mutex to ensure that only one Task is is using the HBIOS API at any time. Doing the SRA (HL) instruction gives us a testable value in Carry, and atomically takes the mutex to prevent others using it.

Perhaps the HBX_LOCK byte can go here?


HB_LOCK  .DB  $FE ; <- the mutex is stored somewhere in high memory

HBX_INVOKE:
    push hl   ; <- assuming HL is needed here, otherwise no push/pop.
    ld hl,HB_LOCK
    sra (hl)   ; get HBIOS mutex
    jr C,$-2   ; just keep trying the sra (hl) until it is free. (I think it is '$-2', but I don't have an assembler here to check
    pop hl
   ;  ...
   ;  all the normal INVOKE code
   ; ...
    ld hl,HB_LOCK  ; give HBIOS mutex ; <- assuming we don't need to return HL here.
    ld (hl),$FE
    ret

Doing this ensures that multiple Tasks all competing for HBIOS will just spin waiting until they can get in.

It is a pity that it is such a "big lock" on all of the HBIOS API. It would be nice to break this down into little locks on each of the resources. But, that would take a redesign of HBIOS invocation process and is out of scope.

IIRC, there was the time that Linux also moved from a "big lock" to "little locks" on individual resources to enhance responsiveness, but @EtchedPixels is qualified to comment on that story, not me.

wwarthen commented 4 years ago

Hi Phillip,

Got it -- makes sense. OK, so naturally there are a couple issues. First is that this is going to use too much space and reduce the interrupt stack space to an inadequate size for some platforms. The second is the performance overhead.

The mutex is only needed for multi-processing environments. I am wondering if it would make sense to "hook" the HBIOS invocation vector at 0xFFF0?

Currently, 0xFFF0 contains a JP instruction that targets HBX_INVOKE. Optionally, the OS can also setup RST 08 to also point to HBX_INVOKE. I am wondering if FreeRTOS can replace the JP at 0xFFF0 to point to it's own code that wraps the call to HBX_INVOKE with the mutex. If RST 08 is setup, then that would need to be updated also. Obviously, this routine would need to be in high memory.

Thoughts?

-Wayne

feilipu commented 4 years ago

This is going to use too much space and reduce the interrupt stack space to an inadequate size for some platforms. I am wondering if FreeRTOS can replace the JP at 0xFFF0 to point to it's own code that wraps the call to HBX_INVOKE with the mutex?

Yes, certainly possible to do that.

But, what about (for expediency) just wrapping this HBX_LOCK code by #IF (MEMMGR == MM_Z180), and keeping this for the Z180 systems that have a simple/small HBX_BNKSEL implementation?

IIRC, moving the HBX_TMPSTK created an extra 20 bytes of interrupt stack space, and for the SCZ180 platform there's 58 Bytes free in my builds.

We can revisit this decision later for other platforms, (but probably not soon).

wwarthen commented 4 years ago

The N8 is actually a Z180. It just has a bizarre bank selection mechanism that provides RAM beyond the normal 1MB addressing space of the Z180.

Regardless, the code is simple, so I am going to go ahead and add it and see what we wind up with. I will make it a config setting, so easy to modify for now. Hang tight, should have this in an hour or so...

Thanks!

-Wayne

feilipu commented 4 years ago

A further thought... from reading this comment

; HBX_INVOKE IS NOT RE-ENTRANT!  HB_INVBNK CAN BE USED GLOBALLY TO DETERMINE
; IF HBIOS IS ALREADY ACTIVE.  HB_INVBNK WILL HAVE A VALUE != $FF WHEN HBIOS
; IS ACTIVE.  ON RETURN, HB_INVBNK IS SET TO $FF TO INDICATE HBIOS IS NOT
; ACTIVE.

Would it be possible to modify this code to be the mutex? It would save double code. It would look like this...

EDIT No, not easily.

wwarthen commented 4 years ago

Yeah, I think that would be a little messy. Let me put the straight-forward solution in and we can go from there.

EtchedPixels commented 4 years ago

The Linux locks are about multi-processing and different.

You don't want to be spinning or you'll deadlock with an RTOS. I would suggest that you do the wrapping in the RTOS because you don't want to spin, you want to yield in the RTOS so you immediately make progress on something else and you need your locking to be at the RTOS level so it can handle priority inversion and yielding.

None of this works at all if you make an HBIOS call that blocks of course!

feilipu commented 4 years ago

I would suggest that you do the wrapping in the RTOS because you don't want to spin, you want to yield in the RTOS so you immediately make progress on something else.

Good point. I think that HBIOS still needs to do the mutex check though, because it is the arbiter of whether it is being used or not. And, HBIOS doesn't know about the RTOS.

What I can do is check the mutex byte before doing a Yield in the Timer interrupt, which will keep the Task using HBIOS active.

(I need to think further about this though... it has some complexities that I'm not sure about. Your point about inversion is relevant to this too.)

wwarthen commented 4 years ago

I also understand @EtchedPixels points. Regardless, the code change implementing the MUTEX is checked in and does not seem to hurt anything for normal CP/M operation. So, I will leave it this way for now.

It does result in an INT stack size of 13 bytes for N8, so N8 will definitely not work. Will worry about that once we clear up the bigger picture.

-Wayne

feilipu commented 4 years ago

Ok. Thanks I'll do some testing on configSWITCH_CONTEXT using the stack pointer location vs. testing the mutex byte tonight.

Although this is not the end of the story, because it should be possible to Yield to a Task that isn't using HBIOS API, while another Task is using the API. But the complexity is that Yielding a Task using the API will keep it locked for others. This is outside my comfort zone 😱

Initial thought is that since a Yield will cycle through available ready Tasks, yielding at every opportunity (as it stands today) will be the right thing to do, irrespective of the HBIOS API mutex.

Probably the "hospital pass" here is to push the problem to the application writer to manage how they use the HBIOS API. FreeRTOS has Task Notification which works like a counting semaphore. This could be used to manage access to HBIOS API, and provide the best application performance.

I'd see our job is to keep pressing on regardless, or simply not crash.

wwarthen commented 4 years ago

Initial thought is that since a Yield will cycle through available ready Tasks, yielding at every opportunity (as it stands today) will be the right thing to do, irrespective of the HBIOS API mutex.

That's what I was sort of thinking. Clearly, not ideal, but there's no way HBIOS is going to become reentrant in the near future.

feilipu commented 4 years ago

I've done some testing using different Task priorities. TBH, acting maliciously I can get the system to crash, by driving the DIO API with low priority and pushing a high priority CIO API task in every few Ticks. But for normal reasonable usage, I'd say that we've covered everything possible at this stage.

So, whenever you're ready for a point release... and then I'll write it up for UK time zone. Thanks so much for driving to get the right outcome. I'd have probably have given up. :100:

Phillip

wwarthen commented 4 years ago

So, for the actual point release, I need to do something with the conditional compilation of the MUTEX. It must be removed for the N8 due to space restrictions. Since most people will not be using FreeRTOS, my inclination is to default the use of the MUTEX to FALSE for all configs. This would mean using FreeRTOS requires setting that config variable to TRUE.

Are you OK with that?

Thanks,

Wayne

feilipu commented 4 years ago

I’m sure if you move it to your standard config process it won’t be a problem to leave off by default.

It would be helpful to leave it enabled by default for SCZ180 platforms, since there’s sufficient interrupt stack available. And today I can’t test other platforms. I think there are 32 bytes free.

EDIT Although it doesn’t matter to leave it off either. Since anyone using a point release would need to roll-their-own anyway.