5

Objective Summary: I need to write a sleep() function to be used in 8088 (PC/XT ISA) ROM code with 1 ms resolution. Though the question can be more generalized to chaining INTs with data in different segments.

I'm writing procedure that will create delay with 1 ms resolution for the purpose of waiting for I/O device settling. The plan is:

  1. Save the running INT 08H (0000:0020) vector, and keep it in a safe place.
  2. Speed up PIT timer 0 to 1 kHz, where 1,193,180 / 1000 = divisor of 1193.
  3. Install new handler at 0000:0020 that calls the original INT 08H once every 55 ms, to make sure not to muck things up with the standard timer.
  4. Set up a counter in memory for the desired number of ms, decrementing each time the new 1 ms INT 08H completes, and wait for 0. Note: since this is BIOS ROM code this counter must be stored in temp space in BDA segment 0040H.
  5. After ms timer has elapsed, put it all back the way you found it.

This all seems fairly straightforward, and the code I wrote for it works as expected. At the point in step #3, where the new interrupt is to jump/chain to the old one I am running into a little issue. That is, in order to make this FAR jump I would need DS to be the memory in the BDA segment (0040H) containing the vector of the original interrupt. However since this is jumping straight to that code for it to complete and then IRET, I cannot change DS to be anything other than how it was called, nor can I just expect that it will be called with DS being set to BDA. Now, in practice that will probably be the cause since the "sleep" function is really just a loop checking a counter, where DS is the BDA segment. However, this seems like a bug waiting to happen.

Other examples of doing something similar, mostly using a DOS INT 21H / TSR pattern do the far jump to CS:INT_08_OLD since that data will live in the TSR's read/write memory space. In the case of ROM of course, CS is read-only so that won't work.

Is there something I'm missing here or another way to look at it that I'm not seeing?

Below is the vector code:

;----------------------------------------------------------------------------; 
; New INT 08 vector to be installed at 0000:0020H
;----------------------------------------------------------------------------; 
NEW_INT_08:
    STI 
    PUSH  DS 
    PUSH  AX 
    MOV   AX, 40H 
    MOV   DS, AX                    ; DS = BDA (0040H)

;----------------------------------------------------------------------------;
; Check if sleep counter is zero
;
    CLI
    DEC   SLEEP_COUNTER             ; counter at BDA (0040:00ACH)
    JZ    INT_DELAY_TIMER_DONE

;----------------------------------------------------------------------------; 
; Check if 18.2 Hz INT 08 counter is zero
; 
    CLI 
    CMP   TICK_COUNTER              ; counter at BDA (0040:00AEH)
    JZ    INT_DELAY_PIT_INT8 
                 
;----------------------------------------------------------------------------;
; Not elapsed, send EOI and exit 
; 
INT_DELAY_TIMER_DONE: 
    STI 
    MOV   AL, 20H                   ; End of Interrupt OCW 
    OUT   20H, AL                   ; write EOI to port 0 
    POP   AX 
    POP   DS 
    IRET 

;----------------------------------------------------------------------------; 
; Counter is done. Reset counter and call previous INT 08 interrupt. 
; 
INT_DELAY_PIT_INT8: 
    MOV   TICK_COUNTER, 55          ; restart counter 

; 
; Original Code
; 
;   POP   AX 
;   POP   DS                        ; must restore DS from caller 
;   JMP   DWORD PTR DS:INT_08_OLD   ; jump to original INT 8, let it EOI and IRET

; 
; Suggestion from @justme:
;
    PUSHF                           ; save flags to simulate INT/IRET
    CALL  DWORD PTR DS:INT_08_OLD   ; jump to original INT 8, let it EOI
    POP   AX                        ; call preserve registers
    POP   DS
    IRET

Edit 1: Added suggestion from @justme above

Edit 2: I had left out the sleep/re-vectoring/un-vectoring code from the original post for the sake of keeping the post shorter, but I realize that might have left it a little unclear as to the rest of it. For the sake of completeness here is the "sleep" portion of it.

        SEGMENT AT 40H
        ORG 0ACH
SLEEP_COUNTER   DW    ?             ; BDA counter for sleep (ms)
TICK_COUNTER    DB    ?             ; BDA counter for 18.2 Hz ticks

;----------------------------------------------------------------------------;
; "Sleep" using PIT channel 0 for ~1 ms delay
;----------------------------------------------------------------------------;
; Input:
;   AX = number of ms to delay
;----------------------------------------------------------------------------;
PIT_SLEEP:
    TEST  AX, AX                    ; do nothing if 0 delay
    JZ    IO_DELAY_PIT_EXIT

    PUSH  AX
    PUSH  SI
    PUSH  DI
    PUSH  DS
    PUSH  ES

    MOV   SI, SEG _BDA
    MOV   ES, SI                    ; ES = BDA segment (0040H)
    XOR   SI, SI
    MOV   DS, SI                    ; DS = IVT segment (0000H)

;----------------------------------------------------------------------------;
; Set up counters
;
    MOV   DI, OFFSET SLEEP_COUNTER  ; address in BDA memory for counters
    STOSW                           ; set SLEEP counter (in ms)
    MOV   AL, 55                    ; reset 18.2Hz INT 08 counter
    STOSB

;----------------------------------------------------------------------------;
; Save old INT 08 vector
;
    CLI
    MOV   SI, OFFSET _INT_08H       ; SI = INT 08 offset in IVT (0067H)
    MOV   DI, OFFSET INT_08_OLD     ; DI = temp location in BDA
    PUSH  DI                        ; save temp location
    PUSH  SI                        ; save INT 08 offset in IVT
    MOVSW                           ; move old INT 08 offset
    MOVSW                           ; move old INT 08 segment
    POP   DI                        ; DI = INT 08 offset in IVT
    PUSH  DI

;----------------------------------------------------------------------------;
; Write new INT 08 handler to IVT
;
    PUSH  ES                        ; save BDA segment (0040)
    PUSH  DS
    POP   ES                        ; ES = IVT segment (0000)
    MOV   AX, OFFSET NEW_INT_08     ; set up IVT for new INT 08
    STOSW                           ; write offset to IVT
    MOV   AX, CS                    ; AX = CS (F000H)
    STOSW                           ; write segment to IVT

;----------------------------------------------------------------------------;
; Set up timer 0 for 1ms (1 kHz)
;
    MOV   AL, 00110110B             ; Send CW to Counter 0
    OUT   43H, AL
    MOV   AL, LOW 1193              ; 1,193,180 / 1000 (1 ms) = ~1193
    OUT   40H, AL                   ; send low byte
    MOV   AL, HIGH 1193
    OUT   40H, AL                   ; send high byte

    POP   DS                        ; DS = BDA segment (0040)
;----------------------------------------------------------------------------;
; Wait until sleep counter has elapsed
;
    STI
IO_DELAY_PIT_WAIT:
    HLT                             ; wait for next (any) interrupt
    CMP   SLEEP_COUNTER, 0          ; check if sleep counter has elapsed
    JNZ   IO_DELAY_PIT_WAIT
;----------------------------------------------------------------------------;
; Uninstall new INT 08 vector
;
IO_DELAY_PIT_ELAPSED:
    POP   DI                        ; DI = INT 08 offset in IVT
    POP   SI                        ; SI = pointer to saved old INT 08
    CLI
    MOVSW                           ; copy BDA DS:[SI] to IVT ES:[DI]
    MOVSW
    CALL  INIT_TIMER_0              ; reset to standard 18.2Hz timer
    STI
    POP   ES
    POP   DS
    POP   DI
    POP   SI
    POP   AX
IO_DELAY_PIT_EXIT:
    RET

Note: this hasn't yet been fully optimized, commented or "golfed" yet, and sorry for the Dino-MASM syntax. :)

12
  • That doesn't look much like what used to be needed if my old code is any indication. I just took a peak at my own INT 08h installer code, where I also reprogram the 82C54 PIT, and it shows using 40h and 43h to talk to it and always needed to do a jmp to flush the instruction cache. I am hesitating to post anything without doing further study, though. It may have been a custom job with the PIT located differently. So I'll need to check on that. (Just checked. This looks like real PC code. So not a custom system.)
    – jonk
    Commented Jul 20, 2022 at 4:54
  • Do you have DOS running? Or just the BIOS? Looks like you mention DOS in your writing. But I just want to be absolutely certain.
    – jonk
    Commented Jul 20, 2022 at 5:00
  • 1
    You can modify the stack. First, on entry to your handler push two words just to make space, eg push ax and another push ax. Then push ds and push bp and mov bp, sp. Retrieve your downlink address and store it in eg dword [bp + 4]. At the end just pop bp then pop ds then retf (if you want to chain this particular interrupt call).
    – ecm
    Commented Jul 20, 2022 at 7:45
  • Doesn't the PIT provide a ‘one-shot’ mode and a register bit you can check to see if the timeout expired? Why not use that instead? I happen to know GRUB 2 uses that to implement its delay function, although GRUB (usually) also turns off interrupts for the entire time it runs… Commented Jul 20, 2022 at 10:07
  • @jonk Yeah, just BIOS. I only mentioned DOS in that the way it's typically done there for TSRs wouldn't work in this application.
    – 640KB
    Commented Jul 20, 2022 at 13:39

5 Answers 5

-1

I might misunderstand the issue to be solved, but as far as I can tell the desired delay (sleep) function is supposed to stop a synchronous foreground process during execution. Right?

Is there something I'm missing here or another way to look at it that I'm not seeing?

Let's look at some points:

  1. This seems like a case of synchronous wait.

  2. Using interrupts is something that only makes sense if asynchronous operation is needed, usually as there are multiple tasks operating - which seems not the case.

  3. The other case to use interrupts would be build around a system that uses sleep modes to reduce power consumption, which the basic IBM-PC is most definitely not.

  4. While hooking an interrupt is usually a safe task, unhooking is only safe in a hierarchical single tasking environment - i.e. one where during the wait time noone else might manipulate that chain. Which again means the whole idea would only work safe if it's a synchronous structure.

  5. This seems to be intended to be part of a BIOS ROM, right? Hooking interrupts from 'below' is a sure way to insert really hard to find random errors in an OS/Application running above.

  6. Changing the divider on CTC channel#0 might make some programs/routines malfunction (*1). For example reading the joystick (Int 15h/84h DX=1).

  7. Last but not least, 1 ms is damn fast for an IBM PC. Keep in mind, the original PC delivers only about 0,3-0,4 MIPS, or 3-400 instructions per ms. That's not a lot, especially considering that calling interrupts, maybe even nested is an expensive task (*2).

Considering this I would think an active waiting for the desired time will work best. It'll be based on a much more straight and short code avoiding next to all possible pitfalls, especially anything about asynchronous operation. Wouldn't it?

How This May Work

The Default Setup

  • The IBM-PC's 'tick' is provided by channel 0 of the 8253 CTC chip run in mode 2 or 3 (*3)

  • In this mode it's clocked at 1.193182 MHz (0,838 µs per clock)

  • The counter is by default loaded with a count value of 65536.

  • This gives the well known roll over period of 54.93 ms.

  • When roll over is signalled an interrupt is issued

Using This

But one doesn't have to wait for this as

  • the actual counter value can be latch and read at any time.

  • Doing so allows to check time passed by simply subtracting those values

    • Since it's 16 bit, it will be helpful to keep rollover in mind
  • Doing this in quick succession will give a resolution of a few µs.

  • That's as least as exact as using interrupts, if not better.

Go Synchronized Swimming

Since this seems to be intended as part of a BIOS ROM, one can for one assume a certain environment, while at the same time need to abstain from any handling of resources that some OS may need/use. So instead of thinking like a DOS programmer, it's time for low level BIOS programming, and simply act synchronous.

What It Might Look Like

WaitMS:
; Wait for 1..25 milliseconds
; In:
; CX tme to wait in 0,1 ms steps (10 = 1 ms
; Out:
; AX = 0 -> error
; AX = 1 -> number of 1/10 ms waited
; Used
; Regs: AX,CX
; Stack: 2 words

    JCXZ   ParErr
    CMP    CX,250      ; Arbitrary value, needs to be less than 254
    JLE    ParGood
ParErr:
    XOR    AX,AX       ; Return 0 as error value
    RET
ParGood:

    MOV    AL,119      ; CTC clocks per 100µs
    MUL    AL,CL       ; Total clocks to wait
    MOV    CX,AX       ; in CX


    CALL   GetCtr0     ; Read counter 0 value at start
    SUB    AX,CX       ; New counter to wait for
                       ; Carry means there is an underflow,
                       ; i.e. we need to wait for the counter
                       ; to wrap first before our results are meaningful
    JNC    WaitForCnt  ; No underflow ->

                       ; A wrap happenes as soon as the system tick changes,
                       ; so let's look that up
    PUSH  AX
    PUSH  DS
    MOV   AX,40H 
    MOV   DS,AX        ; DS = BIOS Data Area
    MOV   AX,[6Ch]     ; Load lower word of BIOS tick count
WtTick:
    CMP   AX,[6Ch]     ; Still the same?
    JE    WtTick       ; Yes -> loop
    POP   DS           ; No -> Tick has occured
    POP   AX           ; Restore regs, go ahead and wait for the exact moment

WaitForCnt:            ; Now wait for clock count to reach value in CX
    CALL  GetCtr0      ; Read counter 0 
    CMP   AX,CX        ; New count equal or lower?
    JGT   WaitForCnt   ; No -> loop

    MOV   AX,1         ; Success :)
    RET


GetCtr0:
; Read counter value to AX
; In:
; -
; Out:
; AX = Counter Value
; Used
; Regs: AX
; Stack: 1 word

    PUSHF
    CLI
    MOV    AL,0      ; Latch Counter 0 command
    OUT    43h,al    ; Write command to CTC
    IN     AL,040h   ; Read low byte of Counter 0 latch
    MOV    AH,AL     ; Save it
    IN     AL,040h   ; Read high byte of Counter 0 latch
    XCHG   AL,AH     ; Reorder 16 bit value in AX
    POPF
    RET 

*1 - In fact, it might add problems in your setup if you're reprogramming the CTC, as, depending on BIOS, using a different mode and divider than the one preset may create some hickup.

*2 - INT+IRET alone is about 117 clock cycles. A rough guess will put your routine anywhere around 2-300 cycles, i.e. >5% system activity - not counting what else is going on.

*3 - In the original PC it's always mode 3, but some 486/Pentium BIOSes are known to use mode 2, but either will work the same for this (*1) - or the system tick. Difference is just the length of the IRQ signal, which with 486 and later machines is even in mode 2 more tan enough to trigger the PIC.

3
  • Comments are not for extended discussion; this conversation has been moved to chat.
    – Chenmunka
    Commented Jul 23, 2022 at 8:38
  • 2
    That wasn’t an ‘extended discussion’. Commented Jul 24, 2022 at 10:48
  • 1
    I think I fell into the trap of asking an "XY question" here thinking that the asynchronous INT was the only way to go. And thanks to everyone for answering that part of the question very well! However my stated objective was to write a sleep() function and as it turned out this answer proved to be exactly what I needed and met the need. Kudos @Raffzahn for helping me answer the real underlying question!
    – 640KB
    Commented Sep 8, 2022 at 15:41
3

Here's the full interrupt handler of my example, except for the business logic of actually handling the count. INT_08_OLD is assumed to be an offset in segment 40h. I'm writing in NASM syntax.

NEW_INT_08:
        push ax
        push ax          ; make space for far return address
        push bp
        mov bp, sp
        push ax
        push ds
        mov ax, 40h
        mov ds, ax
        mov ax, word [INT_08_OLD + 2]
        mov word [bp + 2 + 2], ax
        mov ax, word [INT_08_OLD]
        mov word [bp + 2], ax
        ...              ; set CY to chain, NC to iret
        pop ds
        pop ax
        pop bp
        jc .chain
        add sp, 4        ; discard far return address
        iret

.chain:
        retf

Some comments:

  • Does not need to call the downlink, enabling to chain instead

  • Does not need another interrupt vector

  • Does not need any particular register values, all registers of the interrupted context are don't care

  • Sets up a single stack frame that is entirely contained in the interrupt handler

  • Does not need the downlink to be stored in the same code segment as the handler

  • Replaces the desired far jump by a far return so that the destination address can come from the stack instead of a variable in memory

  • Reserves some space on the stack, before building the rest of the stack frame, by pushing ax twice as a placeholder, therefore not requiring expensive shuffling of the data on the stack

1
  • 1
    This looks really clean and nice. Will need to dig into it a bit work with it. Now that I see it I understand what you were saying about using the stack for the jump address. I had misunderstood that you were suggesting using stack for saving the old vector in the caller/re-vectoring function and somehow passing that pointer to the interrupt handler (wasn't sure how that would work). So yes, still using BDA for the old vector and then using the stack to hold the return address in the chain's jump may do the same thing with less code!
    – 640KB
    Commented Jul 21, 2022 at 15:10
1

One solution would be to not use JMP to old vector, but CALL. Another solution is to use INT and store old vector to interrupt table.

NewVector:
PUSH DS
PUSH AX

MOV AX, 40h
MOV DS, AX
; do stuff here
; decide to call old int or not

CallOld:
PUSHF
CALL DWORD PTR OldVector
POP AX
POP DS
IRET

NoOld:
MOV AL,20H
OUT 20H, AL
POP AX
POP DS
IRET

The INT solution would be to store the old int vector to an unused int vector like INT 60h, and no matter what DS contains, invoking INT 60h will work to execute old code properly and after that just exit with IRET.

11
  • 1
    @user3840170 You are correct. That's why I said it can be a solution. I did not say it is the most optimal you should definitely use. Many ROMs do hook interrupts this way. Such as IBM XT hard drive BIOS, which relocates original motherboard INT 13h vector to INT 40h. Or VGA which hooks the original INT 10h to INT 42h.
    – Justme
    Commented Jul 20, 2022 at 11:15
  • 1
    @Justme Now that is something worth including in the answer, I think. Commented Jul 20, 2022 at 15:50
  • 1
    @640KB True, but it still assumes that PIT period is set to 0 and it runs in Mode 2. Which is not true if you are running a game for instance. It might help if you would mention what is the purpose of delay, what driver/program for what device are you doing that goes into ROM? Will DOS be run, or DOS programs, DOS games, etc?
    – Justme
    Commented Jul 21, 2022 at 21:10
  • 1
    @Justme this is intended as a multi-purpose mostly accurate delay for handling I/O operations such as FD head settling, motor spin up, KBC read/reset and of course making a beep is the same duration no matter clock speed (just because). Basically to do better than the "delay 1000 LOOPs should be fine". So this could happen at any time during POST, boot or programs/games running.
    – 640KB
    Commented Jul 22, 2022 at 15:11
  • 1
    Are you saying though that it's possible/likely/known that games or DOS programs may reprogram PIT 0 from mode 3 to mode 2 for its own purposes? Do you have any specific examples of titles that do that? The counter polling method would obviously be fine if they messed with the timer divisor, but changing the mode might make a mess. I would have to think the authors would have known that doing so could screw up lots of other things too such as TSR's, hardware peripherals, and the like.
    – 640KB
    Commented Jul 22, 2022 at 15:22
1

Alternative method: Store an EAh opcode (jmp far immediate) in front of the doubleword holding your downlink. When setting up the interrupt handler use mov byte [INT_08_OLD - 1], 0EAh. To chain, do another far jump from your ROM handler like jmp 40h:INT_08_OLD - 1. This essentially creates a small relocated code piece at a constant position that's determined at build time so can be written to the ROM.

1
  • Ooh, that's nice. Very byte efficient too! Will definitely use that for something someday!
    – 640KB
    Commented Jul 21, 2022 at 21:01
0

For hooking the INT 08h, I did something like this:

;   ------------
    int08install    PROC    FAR C PUBLIC USES es di, p:FAR PTR
;   ---------------------------------------------------------------------------
;
;   ---------------------------------------------------------------------------
                    ASSUME  DS:DGROUP, ES:nothing, SS:nothing

            ;   Disable interrupts.

                    pushf
                    cli

            ;   Install the handler.

                    mov     ax, SEG int08handler
                    mov     es, ax
                    mov     di, OFFSET int08handler
                    mov     al, 08h
                    call    far ptr swapvec
                    mov     WORD PTR int08priorvec[0], di
                    mov     WORD PTR int08priorvec[2], es

            ;   Re-program the 82C54 timer.

                    mov     al, 00110100b
                    out     43h, al
                    jmp     @f
                @@: mov     al, LOW INTERVAL
                    out     40h, al
                    jmp     @f
                @@: mov     al, HIGH INTERVAL
                    out     40h, al

            ;   Remember the RTC stack.

                    les     di, p
                    mov     WORD PTR rtcstack, di
                    mov     WORD PTR rtcstack+2, es

            ;   Restore interrupt enable.

                    popf

                    ret

    int08install    ENDP

That routine is passed a far pointer to a small stack area that is used by the installed (added) INT 08h handler routine when it does its work. That stack pointer is saved away for use when the interrupt occurs and my handler runs.

A few global variables help out:

int08priorvec   dd      1 DUP(?)        ; Prior int 08 handler.
rtcstack        dd      1 DUP(?)        ; local RTC stack pointer.
savestack       dd      1 DUP(?)

The above installer uses DOS via this function to insert the new handler into the chain:

;   ------------
    swapvec         PROC    FAR SYSCALL PUBLIC USES ds
;   ---------------------------------------------------------------------------
;   Replaces the current handler for the specified interrupt vector with a
;   new, specified handler.  Returns the address of the old handler.
;
;   INPUTS:
;       AL = interrupt vector ID, 00h to FFh.
;       ES:DI = address of new handler routine.
;
;   OUTPUTS:
;       ES:DI = address of old handler routine.
;   ---------------------------------------------------------------------------
                    ASSUME  DS:nothing, ES:nothing, SS:nothing

                    pushf
                    cli
                    mov     dx, es
                    mov     ah, 35h
                    int     21h
                    mov     ds, dx
                    mov     dx, di
                    mov     ah, 25h
                    int     21h
                    popf
                    mov     di, bx
                    ret

    swapvec         ENDP

That pretty much completes what's involved in hooking into the INT 08h chain.

The uninstall looks like this:

;   --------------
    int08uninstall  PROC    FAR C PUBLIC USES es di
;   ---------------------------------------------------------------------------
;
;   ---------------------------------------------------------------------------
                    ASSUME  DS:DGROUP, ES:nothing, SS:nothing

            ;   Disable interrupts.

                    pushf
                    cli

            ;   Re-program the 82C54 timer.

                    mov     al, 00110100b
                    out     43h, al
                    jmp     @f
                @@: mov     al, -1
                    out     40h, al
                    jmp     @f
                @@: out     40h, al

            ;   Restore the old interrupt vector.

                    les     di, int08priorvec
                    mov     al, 08h
                    call    far ptr swapvec

            ;   Restore interrupt enable.

                    popf
                    ret

    int08uninstall  ENDP

The inserted handler has a skeleton that looks like this:

;   ------------
    int08handler    PROC    FAR SYSCALL USES ax ds
;   ---------------------------------------------------------------------------
;
;   ---------------------------------------------------------------------------
                    ASSUME  DS:nothing, ES:nothing, SS:nothing

            ;   We need access to DGROUP -- set it up.

                    cld
                    mov     ax, SEG DGROUP
                    mov     ds, ax

                    ASSUME  DS:DGROUP

            ;   Switch to local stack area.

                    mov     WORD PTR savestack+2, ss
                    mov     WORD PTR savestack, sp
                    mov     ss, WORD PTR rtcstack+2
                    mov     sp, WORD PTR rtcstack

            ;   Call all routines that need to hook into the real-time clock
            ;   event.  This currently includes three functional areas:
            ;
            ;     (1) the EEPROM state machine, which needs to have
            ;         access to a regular interval for driving its
            ;         serial communications process with the EEPROM; and,
            ;
            ;     (2) the process sleep queue timer handler, which
            ;         uses the regular interval to move processes back
            ;         onto the ready queue in a timely fashion; and,
            ;
            ;     (3) the key debouncing routine that reads/monitors
            ;         Dave's special widget board that he kindly built
            ;         for me.
            ;
            ;   Since the C code may modify 32-bit registers and since this
            ;   code is called from within an interrupt procedure, we need
            ;   to save and then restore 32-bit registers.  Also, since ES
            ;   is considered a scratch register by C functions, it must be
            ;   saved.

                    push    eax
                    push    ebx
                    push    ecx
                    push    edx
                    push    es
                    call    FAR PTR eeinterrupt
                    call    FAR PTR ptimer
                    call    NEAR PTR scanwidget
                    pop     es
                    pop     edx
                    pop     ecx
                    pop     ebx
                    pop     eax

            ;   Update watchdog timer.

                    .IF ( watchdog != 65535 )
                      .IF ( watchdog == 0 )
                        call    FAR PTR int08uninstall
                        call    FAR PTR pdump
                      .ENDIF
                      dec     watchdog
                    .ENDIF

            ;   Restore the stack pointer.

                    mov     ss, WORD PTR savestack+2
                    mov     sp, WORD PTR savestack

            ;   Update the DOS interval counter and handle appropriately.

                    mov     ax, INTERVAL
                    add     suminterval, ax
                    .IF ( CARRY? )
                      pushf
                      call    DWORD PTR int08priorvec
                    .ELSE
                      mov     al, SPECIFICEOI + 0
                      out     MASTERPIC, al
                    .ENDIF

                    iret

    int08handler    ENDP

I've left into it some of the specific tasks the code needed to achieve, just to show where the work takes place. But you can ignore those bits and focus on the prologue and epilogue.

This is taken from code I wrote two decades ago. I believe I can answer any questions that come up. But for some, I may need to do a little refreshing of my memory before I can pin down all the details to make a strong case. I'm mostly just offering this as something that did work well at the time (and, I believe, is still being used.)

7
  • Are you sure that an 8088 PC-XT as requested by the OP, is able to "save and then restore 32-bit registers"? Also, if it does, sure that rather bulky interrupt routine can finish in considerable less than 1 ms on a 4.77 MHz machine? That PUSH/CALL FAR/POP sequence may be already half a millisecond. Bit of an overkill :))
    – Raffzahn
    Commented Jul 20, 2022 at 8:28
  • @Raffzahn Of course not. But as I pointed out, that body code is not relevant. You and the OP should just ignore it. What remains is still accurate. I lifted this from an existing application, as I said. But it's model drives from still earlier code. The basics remain the same.
    – jonk
    Commented Jul 20, 2022 at 8:32
  • Well, if it's not relevant - or not working, then it might be helpful for future reader to not confuse OP (and future reader) looking for a PC-XT solution. Wouldn't it?
    – Raffzahn
    Commented Jul 20, 2022 at 8:36
  • 1
    @Raffzahn We can go back and forth. I made my call. It bothers you. I get that. Where else do you want to go with this? It either helps the OP or it doesn't.
    – jonk
    Commented Jul 20, 2022 at 8:44
  • 2
    I think this does answer the question, the main point is to use CALL instead of JMP to run the old interrupt handler, which is essentially identical to my answer, even if the surrounding code is 386-specific and the framework does other things like switches stacks.
    – Justme
    Commented Jul 20, 2022 at 9:10

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .