Kernel User Interrupt
Kernel User Interrupt
Hi all,
In my 6502 system I have kernel code located in the top 8k. (FLASH ROM)
The IRQ interrupt points to $0400 in RAM. 512 bytes has been allocated for future interrupt code, but could be extended.
The current interrupt code only uses 60 bytes.
The IRQ interrupt code is copied from ROM to RAM on start before interrupts are enabled.
The interrupt pseudo code looks something like this,
IF ACIA interrupt
process ACIA interrupt
IF 6522 timer interrupt
process 6522 timer interrupt
-- add USER interrupt here--
RTI
I load "USER" programs into RAM and run them from the kernel.
I want the USER programs to be able to make use of the 6522 timer interrupt.
What is the best way to create a "hook" into the kernel?
Here are my thoughts so far
add a JSR $0500 (in the pseudo code above)
add a RTS at $0500
Then, when the USER program runs it can replace the RTS at $0500 with actual interrupt code.
and when it finishes it can put back the RTS
Another thought was to place 3 NOP's at -- add USER interrupt here--
The USER program can simply replace them with a JSR to some code and then return the NOP's when finished.
The problem with this is finding the NOP's as the address moves if the kernel IRQ code is modified at a later date.
The USER program could scan for then assuming NOP's are not needed in the IRQ code.
Another possibility is to JSR to the RTI address + 1 and start the user code there.
This way the locations are not fixed and there is no unused memory between kernel interrupt and user interrupt code.
The USER program can scan from $0400 to the first RTI (there should only be one)
How did the C64 and other computers do this back in the day?
Any suggestions?
Thanks
Andre
In my 6502 system I have kernel code located in the top 8k. (FLASH ROM)
The IRQ interrupt points to $0400 in RAM. 512 bytes has been allocated for future interrupt code, but could be extended.
The current interrupt code only uses 60 bytes.
The IRQ interrupt code is copied from ROM to RAM on start before interrupts are enabled.
The interrupt pseudo code looks something like this,
IF ACIA interrupt
process ACIA interrupt
IF 6522 timer interrupt
process 6522 timer interrupt
-- add USER interrupt here--
RTI
I load "USER" programs into RAM and run them from the kernel.
I want the USER programs to be able to make use of the 6522 timer interrupt.
What is the best way to create a "hook" into the kernel?
Here are my thoughts so far
add a JSR $0500 (in the pseudo code above)
add a RTS at $0500
Then, when the USER program runs it can replace the RTS at $0500 with actual interrupt code.
and when it finishes it can put back the RTS
Another thought was to place 3 NOP's at -- add USER interrupt here--
The USER program can simply replace them with a JSR to some code and then return the NOP's when finished.
The problem with this is finding the NOP's as the address moves if the kernel IRQ code is modified at a later date.
The USER program could scan for then assuming NOP's are not needed in the IRQ code.
Another possibility is to JSR to the RTI address + 1 and start the user code there.
This way the locations are not fixed and there is no unused memory between kernel interrupt and user interrupt code.
The USER program can scan from $0400 to the first RTI (there should only be one)
How did the C64 and other computers do this back in the day?
Any suggestions?
Thanks
Andre
Re: Kernel User Interrupt
The indirect jump is rather a good tool here. JMP (somevector) allows you to put two bytes in a well-known location, and it will either do the usual thing (because the OS initialised those two bytes) or a new thing (because some program wrote its own handler address to those two bytes.)
It's good practice, just before writing your new value to the two-byte vector, to first read the value that's there and put it in your own exit routine, so your new code will fall through to the pre-existing code if it finds it has nothing to do, or if it's done with what it's doing.
Acorn's MOS actually chains IRQ handling through two vectors - an early one, which a user can use for very time-critical purposes, at the risk of delaying, say, ACIA handlings, and a late one, which comes after all the OS business, whereby the user can do things without adding latency to the OS.
See IRQ1V and IRQ2V in the MOS:
https://tobylobster.github.io/mos/mos/S-s11.html
Perhaps start at the explanation of vectors:
https://tobylobster.github.io/mos/mos/S-s2.html#SP13
Edit: for the C64's KERNAL, perhaps see here, and nearby
https://www.pagetable.com/c64ref/c64mem/#0314
It's good practice, just before writing your new value to the two-byte vector, to first read the value that's there and put it in your own exit routine, so your new code will fall through to the pre-existing code if it finds it has nothing to do, or if it's done with what it's doing.
Acorn's MOS actually chains IRQ handling through two vectors - an early one, which a user can use for very time-critical purposes, at the risk of delaying, say, ACIA handlings, and a late one, which comes after all the OS business, whereby the user can do things without adding latency to the OS.
See IRQ1V and IRQ2V in the MOS:
https://tobylobster.github.io/mos/mos/S-s11.html
Perhaps start at the explanation of vectors:
https://tobylobster.github.io/mos/mos/S-s2.html#SP13
Edit: for the C64's KERNAL, perhaps see here, and nearby
https://www.pagetable.com/c64ref/c64mem/#0314
Re: Kernel User Interrupt
BigEd wrote:
The indirect jump is rather a good tool here. JMP (somevector) allows you to put two bytes in a well-known location, and it will either do the usual thing (because the OS initialised those two bytes) or a new thing (because some program wrote its own handler address to those two bytes.)
It's good practice, just before writing your new value to the two-byte vector, to first read the value that's there and put it in your own exit routine, so your new code will fall through to the pre-existing code if it finds it has nothing to do, or if it's done with what it's doing.
Acorn's MOS actually chains IRQ handling through two vectors - an early one, which a user can use for very time-critical purposes, at the risk of delaying, say, ACIA handlings, and a late one, which comes after all the OS business, whereby the user can do things without adding latency to the OS.
It's good practice, just before writing your new value to the two-byte vector, to first read the value that's there and put it in your own exit routine, so your new code will fall through to the pre-existing code if it finds it has nothing to do, or if it's done with what it's doing.
Acorn's MOS actually chains IRQ handling through two vectors - an early one, which a user can use for very time-critical purposes, at the risk of delaying, say, ACIA handlings, and a late one, which comes after all the OS business, whereby the user can do things without adding latency to the OS.
Code: Select all
.myirq1v
BIT &FE6D \ Check for user VIA interrupt
BVS handletimer1irq \ Is it a timer 1 interrupt?
JMP (savedirq1v) \ Chain to the old irq1v handler
Acorn's MOS also has a third vector, in addition to the two BigEd mentioned above, called the "event vector", which is even easier for user code to deal with - alongside the OS's usual handling of interrupts, it also passes specific events through this vector. For example, one event is vsync, another is a 100Hz clock tick, another is a key being pressed. For each of these it sets the A register (I think) to a code that identifies the event type, and calls the user handler via the "eventv" vector. This means the user code can immediately process the events without having to interrogate hardware directly. See more notes here: https://tobylobster.github.io/mos/mos/S-s14.html#SP25
Finally, to reduce the latency when handling disc and network interrupts, these are connected to the NMI pin, and the NMI vector at FFFA points into RAM at 0D00, which is initialised with RTI on startup. The disc and network software copy various NMI handlers to that RAM location to deal with different types of events (e.g. reading from disc and writing to disc use different handlers). So it is possible to use this sort of system, but it has to be carefully managed.
- BigDumbDinosaur
- Posts: 9428
- Joined: 28 May 2009
- Location: Midwestern USA (JB Pritzker’s dystopia)
- Contact:
Re: Kernel User Interrupt
I have indirect vectors in the firmware of my POC units. An interrupt handler does some basic front end work and then jumps through the relevant vector, which normally returns right back to the interrupt handler. If I wish to add to or replace an interrupt handler I load the new code into RAM and then change the vector to point to my new code.
There were several software projects in which I “wedged” code into the interrupt subsystem in this fashion. It’s how I developed my interrupt-driven SCSI driver (lots of opportunities for crashes with that one) without having to constantly change the firmware.
There were several software projects in which I “wedged” code into the interrupt subsystem in this fashion. It’s how I developed my interrupt-driven SCSI driver (lots of opportunities for crashes with that one) without having to constantly change the firmware.
x86? We ain't got no x86. We don't NEED no stinking x86!
Re: Kernel User Interrupt
Thanks,
I get the idea of the JMP (indirect). Looks like the way to go.
What I don't quite see from all the BBC code is how one returns from the JMP to continue the original IRQ code.
Let's say I have a vector table looking something like this,
vector 1 at $200
vector 2 at $202
vector 3 at $204
simplified irq pseudo code
irq entry
kernel does something
JMP ($200 contains address of label_vec1)
label_vec1:
JMP ($202 contains address of label_vec2)
label_vec2:
JMP ($204 contains address of label_vec3)
label_vec3:
RTI
The above code "falls through" to the RTI as the JMP's jump to the next address.
Now, in my USER program I modify address $200 (address of my code) to point to my irq code.
upon completion of my code I can simply JMP ($202)
if no other vectors have been modified then the code falls through to the RTI
However if all 3 were modified then I need a 4th vector in my table to act as a return address.
This 4th one is never modified.
Is this the way it should work or am I still missing something?
Regards
Andre
I get the idea of the JMP (indirect). Looks like the way to go.
What I don't quite see from all the BBC code is how one returns from the JMP to continue the original IRQ code.
Let's say I have a vector table looking something like this,
vector 1 at $200
vector 2 at $202
vector 3 at $204
simplified irq pseudo code
irq entry
kernel does something
JMP ($200 contains address of label_vec1)
label_vec1:
JMP ($202 contains address of label_vec2)
label_vec2:
JMP ($204 contains address of label_vec3)
label_vec3:
RTI
The above code "falls through" to the RTI as the JMP's jump to the next address.
Now, in my USER program I modify address $200 (address of my code) to point to my irq code.
upon completion of my code I can simply JMP ($202)
if no other vectors have been modified then the code falls through to the RTI
However if all 3 were modified then I need a 4th vector in my table to act as a return address.
This 4th one is never modified.
Is this the way it should work or am I still missing something?
Regards
Andre
Re: Kernel User Interrupt
Andre wrote:
What I don't quite see from all the BBC code is how one returns from the JMP to continue the original IRQ code.
e.g.
Code: Select all
init:
PHP
SEI
LDA $200
STA mysavedvector200
LDA $201
STA mysavedvector200+1
LDA #<my200handler
STA $200
LDA #>my200handler
STA $201
PLP
RTS
my200handler:
; ... code here ...
JMP (mysavedvector200)
Quote:
irq entry
kernel does something
JMP ($200 contains address of label_vec1)
label_vec1:
JMP ($202 contains address of label_vec2)
label_vec2:
JMP ($204 contains address of label_vec3)
label_vec3:
RTI
kernel does something
JMP ($200 contains address of label_vec1)
label_vec1:
JMP ($202 contains address of label_vec2)
label_vec2:
JMP ($204 contains address of label_vec3)
label_vec3:
RTI
In Acorn's code, the default IRQ1V handler polls all the hardware for interrupts that it knows how to handle (e.g. the serial subsystem, one of the VIAs, the ADC port), proxies some of them to events via the EVENTV, and finally if the interrupt wasn't from a hardware source that IRQ1V is aware of, it offers the interrupt to various paged ROMs before ultimately calling IRQ2V if nobody knows what to do with it. So these vectors aren't called back-to-back, there's a lot of code in between, and in fact it's more like they're chained together. If a user IRQ1V handler just RTIs instead of chaining to the system handler, the system won't see the interrupt at all, IRQ2V won't get called, etc.
Re: Kernel User Interrupt
Hi
I understand that the user program will capture an replace the vectors in the table as required.
Apologies, bad example.
3 successive JMP's is not what I am looking for, but it does serve to illustrate the problem / solution.
I would currently only require 2 vectors as follows
1. Every time the 6522 timer interrupt occurs
2. Every time the irq occurs, but after the kernel has done it's thing.
irq entry
if ACIA has new data then process it
if 6522 timer interrupt then process it AND JMP(vector1)
;end of current kernel code
JMP(vector2)
RTI
Back to my question, after the JMP(vector2), how does one return to complete the RTI?
I could of course ignore the RTI and simply add the RTI at the end of my irq code, but that creates an exception.
Some user irq code requires the RTI and some do not.
Is a vector3 holding the address of the RTI the way to go or is there a better way?
Regards
Andre
I understand that the user program will capture an replace the vectors in the table as required.
Apologies, bad example.
3 successive JMP's is not what I am looking for, but it does serve to illustrate the problem / solution.
I would currently only require 2 vectors as follows
1. Every time the 6522 timer interrupt occurs
2. Every time the irq occurs, but after the kernel has done it's thing.
irq entry
if ACIA has new data then process it
if 6522 timer interrupt then process it AND JMP(vector1)
;end of current kernel code
JMP(vector2)
RTI
Back to my question, after the JMP(vector2), how does one return to complete the RTI?
I could of course ignore the RTI and simply add the RTI at the end of my irq code, but that creates an exception.
Some user irq code requires the RTI and some do not.
Is a vector3 holding the address of the RTI the way to go or is there a better way?
Regards
Andre
Re: Kernel User Interrupt
Andre wrote:
Back to my question, after the JMP(vector2), how does one return to complete the RTI?
I could of course ignore the RTI and simply add the RTI at the end of my irq code, but that creates an exception.
Some user irq code requires the RTI and some do not.
Is a vector3 holding the address of the RTI the way to go or is there a better way?
I could of course ignore the RTI and simply add the RTI at the end of my irq code, but that creates an exception.
Some user irq code requires the RTI and some do not.
Is a vector3 holding the address of the RTI the way to go or is there a better way?
Another option that may be better, especially for vector1, would be for your interrupt handler to JSR to the indirect JMP instruction, so that the user code just has to RTS when it's finished. It no longer allows the user code to prevent the OS from running (by not chaining) and doesn't allow multiple different bits of user code to all hook the event, but it's better in some situations.
- floobydust
- Posts: 1394
- Joined: 05 Mar 2013
Re: Kernel User Interrupt
There's multiple ways to handle entries and returns to and from the interrupt handler(s). For my systems, I use a set of 8 vectors located in page $03, which are assigned as follows:
The above addresses get copied to Page $03 during startup. There are 8 free vectors which can be used to extend any of the handlers (IRQ, NMI or BRK). The code below shows saving and restoring registers pre/post the handler routine and the indirect jumps to Page $03 to the actual interrupt handler.
As you can see, each interrupt function (NMI, IRQ and BRK) has a vector that points to the actual handler, and, a vector that is used when that routine exits. By having 8 additional soft vectors that are free, I can add to any of the three routines and insert a new routine pre or post to the main routine. An example is detecting an IDE device. If my BIOS finds an IDE device, it inserts an interrupt handler into the chain. It also inserts the IDE handler to run before the existing handler, which by default supports a single or dual UART with a timer.
The NMI routine for my BIOS basically resets only the console UART and re-initializes the base vectors and then does a warm start of the monitor code. Note that I also have soft vectors for the Monitor warm and cold start entries, so these can be changed as well if needed. Hope this helps....
Code: Select all
VEC_TABLE ;Vector table data for default ROM handlers
;
.DW NMI_VECTOR ;NMI Location in ROM
.DW BRKINSTR0 ;BRK Location in ROM
.DW INTERUPT0 ;IRQ Location in ROM
;
.DW M_WARM_MON ;NMI return handler in ROM
.DW M_WARM_MON ;BRK return handler in ROM
.DW IRQ_EXIT0 ;IRQ return handler in ROM
;
.DW M_COLD_MON ;Monitor Cold start
.DW M_WARM_MON ;Monitor Warm start
Code: Select all
;
IRQ_VECTOR ;This is the ROM start for the BRK/IRQ handler
PHA ;Save A Reg (3)
PHX ;Save X Reg (3)
PHY ;Save Y Reg (3)
TSX ;Get Stack pointer (2)
LDA $0100+4,X ;Get Status Register (4)
AND #$10 ;Mask for BRK bit set (2)
BNE DO_BRK ;If set, handle BRK (2/3)
JMP (IRQVEC0) ;Jump to Soft vectored IRQ Handler (6)
DO_BRK JMP (BRKVEC0) ;Jump to Soft vectored BRK Handler (6)
NMI_ROM JMP (NMIVEC0) ;Jump to Soft vectored NMI handler (6)
;
;This is the standard return for the IRQ/BRK handler routines
;
IRQ_EXIT0 PLY ;Restore Y Reg (4)
PLX ;Restore X Reg (4)
PLA ;Restore A Reg (4)
RTI ;Return from IRQ/BRK routine (6)
;
The NMI routine for my BIOS basically resets only the console UART and re-initializes the base vectors and then does a warm start of the monitor code. Note that I also have soft vectors for the Monitor warm and cold start entries, so these can be changed as well if needed. Hope this helps....
Regards, KM
https://github.com/floobydust
https://github.com/floobydust
- BigDumbDinosaur
- Posts: 9428
- Joined: 28 May 2009
- Location: Midwestern USA (JB Pritzker’s dystopia)
- Contact:
Re: Kernel User Interrupt
Andre wrote:
I get the idea of the JMP (indirect). Looks like the way to go.
What I don't quite see from all the BBC code is how one returns from the JMP to continue the original IRQ code.
What I don't quite see from all the BBC code is how one returns from the JMP to continue the original IRQ code.
Some example code for you that “wedges” into two different interrupt systems. This is for the 65C816, which has the COP software interrupt. That said, the principle is the same for all members of the 6502 family.
Code: Select all
;===============================================================================
;
;wedge: WEDGE PATCHES INTO INTERRUPT SYSTEM
;
wedge rep #m_setr ;16-bit registers
ldx !#hwstack ;set top of...
txs ;hardware stack
lda ivcop ;current COP jump vector
cmp !#newcop ;already wedged?
beq .0000010 ;yes, skip
;
sta ivspareb ;save old COP vector
lda !#newcop ;set...
sta ivcop ;new COP vector
;
.0000010 lda ivirq ;current IRQ jump vector
cmp !#newirq ;already wedged?
beq .0000020 ;yes, skip
;
sta ivsparea ;save old IRQ vector
lda !#newirq ;set...
sta ivirq ;new IRQ vector
;
.0000020 lda !#0 ;zero all registers
tax
txy
sep #m_setr ;8-bit registers
clc
brkNote that with the 65C816, 16-bit reads and writes are atomic operations, which is why I don't temporarily disable IRQs while changing the IRQ indirect vector. With the 8-bit 65C02, you have to disable IRQs while changing the vector. Otherwise, it’s possible to have an IRQ hit when only one of the two vector address bytes have been altered, likely sending the MPU out into the asteroid belt.
x86? We ain't got no x86. We don't NEED no stinking x86!
Re: Kernel User Interrupt
Andre wrote:
Back to my question, after the JMP(vector2), how does one return to complete the RTI?
I could of course ignore the RTI and simply add the RTI at the end of my irq code, but that creates an exception.
Some user irq code requires the RTI and some do not.
I could of course ignore the RTI and simply add the RTI at the end of my irq code, but that creates an exception.
Some user irq code requires the RTI and some do not.
So you read and save the existing pointer, replace it with your own, and at the end of your own code you JMP (savedPointer).
That way many different routines can insert themselves into this chain. (and remove at a later date, if required)
This way is used in other vectors too - e.g. the Apple II charin/charout vectors and the dozen or so more for the BBC Micro operating system for things like file operations as well as character IO.
-Gordon
--
Gordon Henderson.
See my Ruby 6502 and 65816 SBC projects here: https://projects.drogon.net/ruby/
Gordon Henderson.
See my Ruby 6502 and 65816 SBC projects here: https://projects.drogon.net/ruby/
Re: Kernel User Interrupt
drogon wrote:
Andre wrote:
Back to my question, after the JMP(vector2), how does one return to complete the RTI?
I could of course ignore the RTI and simply add the RTI at the end of my irq code, but that creates an exception.
Some user irq code requires the RTI and some do not.
I could of course ignore the RTI and simply add the RTI at the end of my irq code, but that creates an exception.
Some user irq code requires the RTI and some do not.
Code: Select all
; Relocated original Acorn MOS entrypoints.
__osRdch: jmp _osRdch ; FF85: (&0210) Get a byte from current input stream
__osAsci: cmp #$0D ; FF88: Output a byte to VDU stream expanding
bne __osWrch ; carriage returns (&0D) to LF/CR (&0A,&0D
)
__osNewl: lda #$0A ; FF8C: Output LF/CR to VDU stream
jsr __osWrch ; FF8E: Output A followed by CR to VDU stream
__osWrcr: lda #$0D ; FF91: Output a CR to VDU stream
__osWrch: jmp _osWrch ; FF93: (&020E) output a character to the VDU stream
__osWord: jmp _osWord ; FF96: (&020C) Perform operation using parameter table
__osByte: jmp _osByte ; FF99: (&020A) Perform operation with single bytes
__osCli: jmp _osCli ; FF9C: (&0208) Pass string to command line interpreter
Code: Select all
_osWrch:
jmp (wrchV)
_nvWrch: ; Non-vectored osWrch - always write to the host
Code: Select all
;*********************************************************************************
; indirectionVectors:
; These are the indirection vectors copied to page $02.
;*********************************************************************************
indVectors = $0200
indVectorsStart:
userV: .word _userV
brkV: .word _brkV
irq1V: .word _irq1V
irq2V: .word _irq2V
cliV: .word _nvCli
byteV: .word _nvByte
wordV: .word _nvWord
osWrchV: .word _nvWrch
osRdchV: .word _nvRdch
fileV: .word _nvFile
argsV: .word _nvArgs
bgetV: .word _nvBget
bputV: .word _nvBput
gbpbV: .word _nvGbpb
findV: .word _nvFind
fscV: .word _fscV
evntV: .word _evntV
uptV: .word _uptV
netV: .word _netV
vduV: .word _vduV
keyV: .word _keyV
insV: .word _insV
remV: .word _remV
cnpV: .word _cnpV
ind1V: .word _ind1V
ind2V: .word _ind2V
ind3V: .word _ind3V
IRQ is similar.
These do add a few more cycles to each routine but as it was OK on a 2Mhz BBC Micro in 1981, I think it's fine on my 16Mhz Ruby board today.
-Gordon
--
Gordon Henderson.
See my Ruby 6502 and 65816 SBC projects here: https://projects.drogon.net/ruby/
Gordon Henderson.
See my Ruby 6502 and 65816 SBC projects here: https://projects.drogon.net/ruby/
Re: Kernel User Interrupt
Thanks for all the help everyone,
Gordon, I see that you are stating with
jsr __osWrch
This takes care of the return (via RTS) so that the original code continues from where the JSR occurred.
The function that you are calling takes care of the JMP (indirect)
_osWrch:
jmp (wrchV)
and wrchV is the table entry for the function you want.
This makes sense.
For others interested in this topic I will present what I have been doing in more detail. Perhaps it helps someone down the track.
I am actually coding 80% c (CC65) and 20% assembler which makes things a little more interesting.
I have some kernel code / boot loader that allows me to upload programs via the terminal.
It also provides other system services.
My interrupt code, in assembler, is copied to RAM on power up. It needs to run from RAM as I can bank switch the upper 8K kernel code.
The flash ROM device is 512K, so 64 x 8k blocks. (Each ROM bank must duplicate the interrupt vectors)
My vector table (at this stage) looks like this
vector1 $04F0 10ms timer interrupt
vector2 $04F2 1 second timer interrupt
vector3 $04F4 IRQ interrupt after kernel code has completed
Note that my irq code is located in the top 8K. In order to generate vectors that will work at a lower address the vector addresses need to be recalculated.
The kernel generates default vectors as follows, (inline assembler in c kernel code)
__asm__ ("LDA #<(_vector1_return - _irq + $0400)");
__asm__ ("STA $04F0");
__asm__ ("LDA #>(_vector1_return - _irq + $0400)");
__asm__ ("STA $04F1");
where irq is the start of the _irq code and _vector1_return is a label at the return address
The irq code looks like this
...yes it was a 10ms timer interrupt
JMP ($04F0) ; jump to vector1 interrupt
_vector1_return:
INC SYSTEM_TIMER
CLC
LDA SYSTEM_TIMER
CMP #$64 ; HAS TIMER REACHED 100 (0x64)
BCC _exit_irq
LDA #$00
STA SYSTEM_TIMER
INC SYSTEM_SECONDS
JMP ($04F2) ; jump to vector2 interrupt
_vector2_return:
_not_via:
JMP ($04F4) ; jump to vector3 interrupt
_vector3_return:
_exit_irq:
PLA
PLX
PLY
_irq_last_byte_address:
RTI ; Return from IRQ interrupt
In the USER program I save the old IRQ vector upon entry,
unsigned int old_irq_address;
*((unsigned char *)&old_irq_address) = *(unsigned char *) 0x04F0; // save the low byte of vector1
*((unsigned char *)&old_irq_address+1) = *(unsigned char *) 0x04F1; // save the high byte of vector1
and replace it with the address of my new code/function.
SEI(); // disable interrupts
__asm__ ("LDA #<_USER_Program_Timer_Interrupt");
__asm__ ("STA $04F0");
__asm__ ("LDA #>_USER_Program_Timer_Interrupt");
__asm__ ("STA $04F1");
CLI(); // enable interrupts
In this example _USER_Program_Timer_Interrupt is a c function as follows,(probably won't be in future)
void USER_Program_Timer_Interrupt(void)
{
temp_counter++;
__asm__ ("JMP (_old_irq_address)");
}
and when the USER program is finished I restore the old irq vector.
SEI(); // disable interrupts
*(unsigned char *) 0x04F0 = *((unsigned char *)&old_irq_address); // restore the low byte of vector1
*(unsigned char *) 0x04F1 = *((unsigned char *)&old_irq_address+1); // restore the high byte of vector1
CLI(); // enable interrupts
That's pretty much it.
Reagrds
Andre
Gordon, I see that you are stating with
jsr __osWrch
This takes care of the return (via RTS) so that the original code continues from where the JSR occurred.
The function that you are calling takes care of the JMP (indirect)
_osWrch:
jmp (wrchV)
and wrchV is the table entry for the function you want.
This makes sense.
For others interested in this topic I will present what I have been doing in more detail. Perhaps it helps someone down the track.
I am actually coding 80% c (CC65) and 20% assembler which makes things a little more interesting.
I have some kernel code / boot loader that allows me to upload programs via the terminal.
It also provides other system services.
My interrupt code, in assembler, is copied to RAM on power up. It needs to run from RAM as I can bank switch the upper 8K kernel code.
The flash ROM device is 512K, so 64 x 8k blocks. (Each ROM bank must duplicate the interrupt vectors)
My vector table (at this stage) looks like this
vector1 $04F0 10ms timer interrupt
vector2 $04F2 1 second timer interrupt
vector3 $04F4 IRQ interrupt after kernel code has completed
Note that my irq code is located in the top 8K. In order to generate vectors that will work at a lower address the vector addresses need to be recalculated.
The kernel generates default vectors as follows, (inline assembler in c kernel code)
__asm__ ("LDA #<(_vector1_return - _irq + $0400)");
__asm__ ("STA $04F0");
__asm__ ("LDA #>(_vector1_return - _irq + $0400)");
__asm__ ("STA $04F1");
where irq is the start of the _irq code and _vector1_return is a label at the return address
The irq code looks like this
...yes it was a 10ms timer interrupt
JMP ($04F0) ; jump to vector1 interrupt
_vector1_return:
INC SYSTEM_TIMER
CLC
LDA SYSTEM_TIMER
CMP #$64 ; HAS TIMER REACHED 100 (0x64)
BCC _exit_irq
LDA #$00
STA SYSTEM_TIMER
INC SYSTEM_SECONDS
JMP ($04F2) ; jump to vector2 interrupt
_vector2_return:
_not_via:
JMP ($04F4) ; jump to vector3 interrupt
_vector3_return:
_exit_irq:
PLA
PLX
PLY
_irq_last_byte_address:
RTI ; Return from IRQ interrupt
In the USER program I save the old IRQ vector upon entry,
unsigned int old_irq_address;
*((unsigned char *)&old_irq_address) = *(unsigned char *) 0x04F0; // save the low byte of vector1
*((unsigned char *)&old_irq_address+1) = *(unsigned char *) 0x04F1; // save the high byte of vector1
and replace it with the address of my new code/function.
SEI(); // disable interrupts
__asm__ ("LDA #<_USER_Program_Timer_Interrupt");
__asm__ ("STA $04F0");
__asm__ ("LDA #>_USER_Program_Timer_Interrupt");
__asm__ ("STA $04F1");
CLI(); // enable interrupts
In this example _USER_Program_Timer_Interrupt is a c function as follows,(probably won't be in future)
void USER_Program_Timer_Interrupt(void)
{
temp_counter++;
__asm__ ("JMP (_old_irq_address)");
}
and when the USER program is finished I restore the old irq vector.
SEI(); // disable interrupts
*(unsigned char *) 0x04F0 = *((unsigned char *)&old_irq_address); // restore the low byte of vector1
*(unsigned char *) 0x04F1 = *((unsigned char *)&old_irq_address+1); // restore the high byte of vector1
CLI(); // enable interrupts
That's pretty much it.
Reagrds
Andre
Re: Kernel User Interrupt
Thanks - it's much clearer, to anyone I think, when you show
- the labels
- the vector initialisation
I think there's something you might still be missing. Your vector2 and vector3 serve pretty much the same purpose, in terms of what happens before them and after them. Why two vectors? The point of the new handlers inserting themselves into a chain is that you can have any number of new handlers, from zero to many, without needing any pre-allocated series of vectors.
It should also be clear, although it's not important, that a JMP is a JMP, and it doesn't matter what code or data follow it. In your case, for convenience or by convention perhaps, you've got the intended subsequent code right after the JMP. There's nothing wrong with that, but I wonder if it says something about the way you're thinking about these mechanisms.
(Acorn's system has two vectors because one is early and the other is late - they have two different purposes.)
- the labels
- the vector initialisation
I think there's something you might still be missing. Your vector2 and vector3 serve pretty much the same purpose, in terms of what happens before them and after them. Why two vectors? The point of the new handlers inserting themselves into a chain is that you can have any number of new handlers, from zero to many, without needing any pre-allocated series of vectors.
It should also be clear, although it's not important, that a JMP is a JMP, and it doesn't matter what code or data follow it. In your case, for convenience or by convention perhaps, you've got the intended subsequent code right after the JMP. There's nothing wrong with that, but I wonder if it says something about the way you're thinking about these mechanisms.
(Acorn's system has two vectors because one is early and the other is late - they have two different purposes.)
Re: Kernel User Interrupt
Hi
Yes, the vector arrangement looks a little odd. That's why the previous (poor) example showed 3 vectors in a row, which makes no sense out of context.
But this arrangement does make sense.
Vector1 triggers if a 10ms event has occurred
Vector2 triggers if a 1 second event has occurred
Vector3 triggers every time an irq interrupt occurs
Vectror3 is effectively ALWAYS triggered
It is possible for all 3 vectors to trigger in one pass through the irq interrupt.
So a user program can
do something every 10ms in vector1
do something every second in vector2
handle a new device not recognized by the kernel in vector3
It is just a coincidence that vector2 happens to follow vector3 in the sequence of the code.
As new code is added the gap between vector2 and vector3 may be filled.
Hope that makes sense.
Regards
Andre
Yes, the vector arrangement looks a little odd. That's why the previous (poor) example showed 3 vectors in a row, which makes no sense out of context.
But this arrangement does make sense.
Vector1 triggers if a 10ms event has occurred
Vector2 triggers if a 1 second event has occurred
Vector3 triggers every time an irq interrupt occurs
Vectror3 is effectively ALWAYS triggered
It is possible for all 3 vectors to trigger in one pass through the irq interrupt.
So a user program can
do something every 10ms in vector1
do something every second in vector2
handle a new device not recognized by the kernel in vector3
It is just a coincidence that vector2 happens to follow vector3 in the sequence of the code.
As new code is added the gap between vector2 and vector3 may be filled.
Hope that makes sense.
Regards
Andre