I hope this is the right forum, the question is about developing an emulator rather than using one, which seemed to fit into this group and into Projects.

I'm been working on a BBC B emulator as a hobby, and and all the pieces emulated to a state where keyboard handling should work, but it doesn't.
The matrix auto-scans, detects a keypress and signals CA2 on the system VIA. The CPU never gets the interrupt though, and after spending a while debugging my code, I realised that this is correct, the OS has not enabled keyboard interrupts during startup.

There are only five references to the system VIA interrupt flag (FE4E) in the entire code.
At D9D7 it is read to determine if this is a power on. The comment confirm that the system should start with all interrupts disabled.
At DA82 it is set, and keyboard interrupts are not enabled. (set to &F2 == 11110010, lowest bit is CA2.)
At DD0E it is read as part of interrupt handling
At EEE4 it is enabled for keyboard interrupts, but as far as I can see this is part of a routine which is only called in response to a keypress, which would require keyboard interrupts to be enabled.
At EF06 it is disabled for keyboard interrupts.

So I have a chicken and the egg problem, the interrupt is only enabled when handling the interrupt?

Clearly I'm missing something, but at the moment I can't see what. I had a quick look at the BASIC disassembly, and couldn't see an OSBYTE &97 call which could be setting it, and when I boot up and BASIC runs, program flow never hits EEE4, it calls OSWORD 0 to get a line from the buffer, but nothing ever gets put into the buffer, so cycles forever.

From this point keyboard processing will work. Note that IRQs are disabled, so the MOS will not respond to any interupts, which is why all languages have to start with:

LDX #&FF:TXS :\ Reset stack
CLI :\ Enable IRQs

Once you get past this point, the MOS will respond to interupts from the keyboard through the System VIA. Also, OSRDCH explicitly enables IRQs, so even if IRQs are disabled, calling OSRDCH will read from the keyboard - though no background processing will occur.

This thread is a bit old so not sure if this is still useful but I was also looking at wiring up keyboard handling in a new experiment. I took some notes on how the keyboard handling works (in hardware and MOS 1.2) and the different states things go through from bootup to key press getting back to BASIC. It's kind of quirky. These notes are pasted below.

What was interesting to me is that after a keyboard interrupt is handled, the key press doesn't appear to be put into the buffer until TWO 10ms timer ticks later. It doesn't look like a great way to do things so perhaps I have made an error, or b-em is emulating the keyboard in an interesting way, or perhaps it's a real quirk...

Cheers
Chris

Keyboard handling in Acorn MOS 1.2 and BBC B system VIA hardware.

The keyboard handling in hardware is relatively simple, but with quirks. The
handling in software is a little more complicated and uses the hardware in
various different modes when the state machine is in different states.

This quick walkthrough takes us through all the keyboard handling, from bootup
to a keystroke successfully read by BASIC.

1. Bootup.
- Memory is cleared to 0, by a loop that hits here:
da56: STA $00,X
This includes zeroing out &ed.

- &ed is initialized to 0x62, here:
da64: LDA #$62
da66: STA $ed
&ed is important, as is this initialization, as we'll see later.

- system VIA interrupt enable is initialized to 0x72, here:
da80: LDX #$f2
da82: STX $fe4e
The value 0x72 includes TIMER1 (0x40) and TIMER2 (0x20) but not the keyboard,
CA2 (0x01). This means that after initialization, you can hammer on the
keyboard all day long and it will assert CA2 to the system VIA but the system
VIA will not assert a 6502 interrupt.
So something else must be responsible for the keyboard actually working.

- BASIC calls in to OSWORD 0x00 at 0xbc1d.
This is "read line from current input to memory", and it starts at 0xe902,
which quickly calls OSRDCH (0xffe0) -> 0xdec5. At 0xdeed, 0xe577 is called to
get a byte from the buffer, which calls OSBYTE 145 (get byte from buffer) at
0xe460 -> 0xe464.
While there are no keypresses, this code ends up looping calling OSBYTE 145 and
getting nothing back. The check that "fails" is:
e466: LDA $02d8,X
e469: CMP $02e1,X
e46c: BEQ #$72 (taken: buffer empty)
(Note that before getting to the failure loop, it seems to read the byte 0xca
that expands to a special string of length 0 and little effect -- so not sure
what the deal is with that. It's put into the buffer at 0xdb2d.)

2. First TIMER1 (10ms) tick.
In general, The IRQ interrupt vector at 0xfffe bounces execution to 0xdc1c,
which has responsibility for working out what type of IRQ fired and redirecting
execution accordingly. TIMER1 ticks are sysvia interrupts, which get considered
at 0xdd08 and confirmed TIMER1 interrupt handing starts at 0xddcc.

- Decide that the keyboard needs a scan.
At 0xde31, the memory locations &ec and &ed are checked to see if they are
non-zero. At bootup, 0x62 was written to &ed and this makes the logic think
that there was a key event to look at and therefore the keyboard code is
entered.

- Keyboard housekeeping.
The call is made to 0xf065 -> 0xef02, which does some housekeeping including
checking to see if the shift / ctrl keys are pressed or not.
There is no current keypress &ec but there is a previous keypress &ed, so the
code at 0xefed calls 0xf02a with X=0x62 (code for the space bar key as it
happens) so see if it is still pressed by querying the hardware for a specific
key. Let's assume it's not pressed (because it was never pressed), in which
case 0x00 is written to &ed. Now there is no previous key press and no current
press.
At 0xeffe, a call is make to 0xf0cc.

- Scan keyboard.
We'll cover this in more detail later, for now let's assume a scan of the
keyboard decides that no keys are pressed.
After entry to the scan routine at 0xf0cc, control arrives at 0xf126 with
X=0xff, indicating nothing found in the scan.
At 0xf12e (called twice), the keyboard is put in autoscan mode, meaning that
any key down will attempt to trigger an interrupt. (Keyboard interrupt is still
disabled at this time.)

- Process scan results.
No new keys are pressed and control transfers to 0xeeda, which notices that
no new key is pressed &ec and no previous press is registered &ed.
And now the magic: at 0xeee2, the system VIA interrupt enable register is
modified to enable CA2 (0x01), the keyboard interrupt.

- Final housekeeping.
The status of the caps lock and shift lock LEDs is written to hardware at
0xeeec. 0xf12e is called again for good measure to put the keyboard in autoscan
mode.
Keyboard routine is exited.

- Why?
This setup may seem complicated but it's probably the only way to use the
hardware as presented with good efficiency and accuracy.
Scanning the keyboard for an unknown key press is a bunch of work and it's
certainly something to be avoided when we're sure there's no key down.
Conversely, it's not clear that it would work to rely on interrupts while there
is known to be a key down. One problem could be getting multiple simultaneous
key presses detected properly. Another problem is that common documentation
doesn't describe if leaving interrupts enabled would cause an interrupt storm,
i.e. will CA2 flip flop as the different keyboard matrix rows are scanned?

3. A keyboard interrupt fires.
Congratulations! You just pressed the A key.

- Keyboard interrupt fires.
The IRQ handling code determines it's a keyboard interrupt at 0xde78, which
promptly jumps into keyboard code at 0xf065 -> 0xef02.
You may notice this is the same entry location as called from the timer
interrupt. However, the flags are different. In this instance, the carry flag
is clear, causing a different code path.

- Keyboard code entered.
First, the keyboard interrupt, CA2 (0x01) is unconditionally disabled. Next,
0xf00f is called, which appears to do a superfluous keypress check with an
unclearly defined X register value?
At 0xf018, 0xf0cc is called to do a keyboard scan for which key is pressed.
Final key is determined at 0xf110 and is in the A register at that time.
Execution arrives at 0xf007, which stores the new key press in &ec, and
then calls 0xf01f to set the key repeat timer to 1 in &e7.

- Final housekeeping.
Execution arrives at 0xeeda -- see above for details of what this does.

4. First post keypress TIMER1 (10ms) tick.
As in the first timer tick, the keyboard routine at 0xf065 is called because one
or more of &ec, &ed is non-zero. In our case, &ec is 0x41.

- Check most recent key press.
At 0xef37, the more recent key press of A is loaded from &ec and 0xf02a is
called to determine whether the key is still pressed.
Let's assume it is, in which case we branch to 0xef50.
But a strange issue occurs: the stored &ec key of 0x41 is not equal to our
checked key value of 0xc1 (which is 0x41|0x80), so we end up at 0xef42 to
overwrite our stored key in &ec with 0xc1.

- Keyboard scan.
The 0x41 keypress is detected but it's not a new key press so nothing occurs.

- Final housekeeping.
As above.

5. Second post keypress TIMER1 (10ms) tick.
As before, the keyboard routine at 0xf065 is called because one or more of &ec,
&ed is non-zero. In our case, &ec is 0xc1.

- Check most recent key press.
At 0xef37, the more recent key press of A is loaded from &ec and 0xf02a is
called to determine whether the key is still pressed.
Let's assume it is, in which case we branch to 0xef50.
The current key press value of 0xc1 matches the key test value of 0xc1, so we
proceed to look at the key repeat value in &e7.
The key repeat value is 1, which we set in the keyboard interrupt, so we
consider the key fired at 0xef5c.

- Perform some key lookups.
At 0xef91, various considerations as to whether shift, ctrl or caps lock are
pressed might affect the character value.

- Put character into buffer!
Eventually, execution arrives at 0xefe6, with Y set to the ASCII character.
In our case, Y is 0x41, or ASCII capital A.
Routine 0xe4f1 is called, which loads 0x00 into X (keyboard buffer id) and
then falls through to OSBYTE 153 "Put byte in input Buffer checking for ESCAPE".
You can see where this is going but this branches to 0xe4a8 which calls
through to OSBYTE 138 "Put byte into Buffer".
At 0xe4c8, the incremented buffer end pointer is written:
e4c8: STA &02E1,X
And that is what unblocks the OSWORD 0 call that BASIC is waiting on. The
unblock will occur once we return from the current IRQ.

- Keyboard scan and final housekeeping.
As per above.

LATENCY
One interesting note is regarding key press response latency. As can be seen,
the act of actually making the key press available to BASIC is NOT done in
the keyboard interrupt handler. It is done in not the first but the second 10ms
timer tick after the keyboard interrupt. So that's up to a 20ms latency which
could easily miss a 50fps video frame opportunity. Lag!

NOT DOCUMENTED
- Key rollover is not described. The MOS code can keep track of two keys
down simultaneously.
- Key release handling is not described, although I believe it is pretty
simple (not based on keyboard interrupts, but based on noticing the key is no
longer down with a direct query).

(Note that before getting to the failure loop, it seems to read the byte 0xca that expands to a special string of length 0 and little effect -- so not sure what the deal is with that. It's put into the buffer at 0xdb2d.)

That's function key 10 - the Break key. The RESET code puts &CA into the keyboard buffer so that on pressing Break function key 10 is the first entry read from keyboard input.

Well, mostly. The zero'th byte of each page isn't cleared. This is because the OS initally writes RTI to &D00 so any stray NMIs return, and _then_ it cleans out memory...without clearing the RTI. See &D9CD, which is where (&FFFC) points to, for the initial RTI write. My vague old old organic memory (impaired by years of alcohol) is also telling me the wipe started at &0400, but I might be wrong there.

So something else must be responsible for the keyboard actually working.

As JGH mentions above, language ROMs are responsible for CLI to enable interrupt handling. The MOS does a SEI immediately after writing to &D00, but never re-enables them.