Reading the Keyboard

If you want to ask questions about how the machine works, peculiar details, the differences between models, here it is !
How to program the oric hardware (VIA, FDC, ...) is also welcome.
User avatar
Twilighte
Game master
Posts: 819
Joined: Sat Jan 07, 2006 12:07 am
Location: Luton, UK
Contact:

Reading the Keyboard

Post by Twilighte »

The keyboard is composed of a matrix of switches.
The matrix is indexed by Rows and Columns
At each matrix intersection (at a specific row and column) a key resides.
Both Row and Column are accessed through the memory mapped Versatile Interface Adapter
or VIA for short.

The row is simply set by writing the row number into the lower 3 bits of VIA Port B at
location $0300.

The Column is less accessable because it is the IO port(AY Register $0E) on the
AY-3-8912 Sound chip.
The column register is also 8 bits wide, and to set Column 5 requires one to Clear
(not set) Bit 5 whilst setting all other bits (It's the way the hardware works!).

To work out how to write to the column Register is to work out how to write to
an AY Register.
The AY has four commands, physically passed by 2 VIA control lines, known as CA2 and CB2.
The logic on these lines dictate the type of information that will appear on VIA Port A.

Code: Select all

CA2 CB2 Type
 0   0  Port A is not connected to AY
 0   1  Port A data is taken as Data for the currently selected AY Register
 1   0  Port A is deposited with Data from the currently selected AY Register
 1   1  Port A data is taken as the AY Register Number which should range $00-$0E
Setting CA2 and CB2 is accomplished in the Peripheral Control Register or via_pcr at
location $030C.

The Peripheral Control Register not only controls CA2 and CB2 but also CA1 and CB1.
CA1 and CB1 are controlled by a single bit Register(in B0 and B4) whereas CA2 and CB2 are
controlled by two 3 bit registers(B1-3 for CA2 and B5-7 for CB2).
CA1 and CB1 can be ignored since the Register bit for both is always set to 1.

Both CA2 and CB2 have the same format of settings in the 3 bit registers.
The first 4 values in the 3 Bit registers (0-3) can be ignored since they treat the lines
as inputs rather than outputs.

Code: Select all

CA2 and CB2 Setting
   100      Handshake output
   101	  Pulse Mode (Set Line momentarily high when Port B read or written to)
   110	  Set Line Low
   111	  Set Line High
So to set AY Register $0E via_pcr should be written with 111 1 111 1 or $FF
And to Write the column value via_pcr should be written with 111 1 110 1 or $FD

However, the lines should be set inactive between operations to ensure Register
number doesn't get taken as Register Data and vice versa.
The Real AY also doesn't like it if CB2 is set high for too long and tends to crash
the system in some weird way. However the Euphoric does not Emulate this behaviour
and will happily run as normal regardless how long CB2 is held high.
To set both lines low, we would write 110 1 110 1 or $DD to via_pcr

After setting Column and Row on the real Oric, we need to wait a few microns for
the curcuit to "Settle". Again Euphoric behaves differently in that it will
instantaneously return the key condition.
Finally we can read the key condition in Bit 4 (Value 8) of Port B.

Lets try this in code

Code: Select all

ReadFunctKey
	;Store AY Register $0E to Port A
	lda #$0E
	sta via_porta
	;Set Lines to Register data
	lda #%11111111	;CB2 CA2 = 11
	sta via_pcr
	;Set lines inactive
	lda #%11011101	;CB2 CA2 = 00
	sta via_pcr
	;Store Column 4 to Port A
	lda #%11110111
	sta via_porta
	;Set lines to Write Data
	lda #%11111101	;CB2 CA2 = 10
	sta via_pcr
	;Set Lines inactive again
	lda #%11011101      ;CB2 CA2 = 00
	sta via_pcr
	;Store Row number 5 to Port B
	lda #5
	sta via_portb
	;Wait a 4 cycles for the curcuit to settle
	nop
	nop
	;Read back the key condition bit
	lda via_portb
	and #8
	rts
Overview:-
The routine will return 8 in the accumulator if the function key was pressed and 0 if
it wasn't.
User avatar
waskol
Flight Lieutenant
Posts: 414
Joined: Wed Jun 13, 2007 8:20 pm
Location: FRANCE, Paris

Post by waskol »

Is it possible to hook the keyboard ?

I explain what I would like to do :

I have a PASE Joystick interface, here is my reading routine in C for Atmos :

Code: Select all

#include <lib.h>
//procedure that read joystick status
void readjoy(int *ljoy,int *rjoy)
{
int V1,V2;
//Read Joysticks
poke(782,64);  //782=#30E
V1=peek(771);  //771=#303
V2=peek(769);  //769=#301
poke(771,192);
poke(769,128);
*ljoy=peek(769)-128;
poke(769,64);
*rjoy=peek(769)-64;
poke(771,V1);
poke(769,V2);
poke(782,192);
}

void main()
{
  char left_joy,right_joy;
  //initialization of the printer port
  poke(618,2);  //618=#26A : Oric status byte (see Atmos user manual, annex 9)
  cls();
  while (key()!=32) {
     //We read Joystick ports
     readjoy(&left_joy,&right_joy);
     //We print the values
     printf("%d  %d\n",left_joy,right_joy);     
  }
  //"release" of the printer port
  poke(618,3);
}
What I would like to do is to "trick" at this moment the ATMOS by "POKING the keyboard" or hooking the Keyboard read access in order to return the Key state of my choice to the calling program.
No need to say that there is a concurrent access


Making it short, imagine that a game is using directional keys, I would like to, with my little routine previously loaded, make it think that I pressed the Left key when I moved the joystick to the left...

To trick a BASIC program should be fairly simple by POKING address #208, but to trick a program that scans directly the Keyboard by accessing the VIA.

I would like also to know how programmable joystick interfaces, like the Proteck or the Downsway, plugged to the expansion port (and this time, not to the printer port), were sending the desired key to the Atmos ?

Thank you !
User avatar
Chema
Game master
Posts: 3014
Joined: Tue Jan 17, 2006 10:55 am
Location: Gijón, SPAIN
Contact:

Post by Chema »

A question for the technical people out there (Twilighte?),

As you know I am trying to do my own routines to handle the keyboard. Taking the code made by Twilighte for Space:1999 and his recomendations I came up with a routine to read the alphanumeric characters, including RETURN and DEL. This should be part of the routines for the Elite Clone, whenever reading those keys is needed, but no need to read simultaneous keypresses (entering names, accessing screens,... everything but flying). This can be called every 8th interrupt, for instance, and it needs to keep quick but not too big.

The idea is checking each row (setting the column register to zero) to see if there is a keypress there. If no keypress it is very quick. If there is a keypress, then loop and check the column. The ascii code is returned (using a table) in a global variable _gKey2.

This is the code:

Code: Select all

#define        via_porta              $030f 
#define        via_portb              $0300 
#define        via_pcr                 $030c 

.zero
row .byt 0

.text
proc_keyboard2
.(
    ldx #00 
    stx _gKey2
 
    ;Setup ay to point to column register 
    ;Note that the write to the column register cannot simply be permanent 
    ;(Which would reduce amount of code) because some orics freeze(crash). 
    lda #$0E        ;AY Column register 
    sta via_porta 
    lda #$FF 
    sta via_pcr 
    ldy #$dd 
    sty via_pcr 

    ldx #7
loopcol
    lda #0   ; Check if there is a keypress in this row
    sta via_porta 
    lda #$fd 
    sta via_pcr 
    sty via_pcr 

    stx row
    lda via_portb
    and #%11111000
    ora row
    sta via_portb

    ;Whilst not needed on Euphoric, this time delay is required for 
    ;some Real Orics otherwise no key will be returned! 
    nop 
    nop 
    nop 
    nop 
    lda via_portb 
    and #08 
    beq skip    ; Nothing in this row
    
    ; A key press in this row
    ; Scan through it

    ldx #7    
looprow
    lda MaskCol,x
    sta via_porta 
    lda #$fd 
    sta via_pcr 
    sty via_pcr 

    lda via_portb
    and #%11111000
    ora row
    sta via_portb

    ;Whilst not needed on Euphoric, this time delay is required for 
    ;some Real Orics otherwise no key will be returned! 
    nop 
    nop 
    nop 
    nop 
    lda via_portb 
    and #08 
    bne keydet 
    dex
    bpl looprow
    
skip
    dex
    bpl loopcol

    lda #0
    sta _gKey2
    rts

keydet
    lda row
    asl
    asl
    asl
    stx row
    clc
    adc row
    tay
    lda tab_ascii,y
    sta _gKey2
    rts
.)

MaskCol 
    .byt $fe,$fd,$fb,$f7,$ef,$df,$bf,$7f

tab_ascii
    .asc "7","N","5","V",0,"1","X","3"
    .asc "J","T","R","F",0,0,"Q","D"
    .asc "M","6","B","4",0,"Z","2","C"
    .asc "K","9",0,0,0,0,0,0
    .asc " ",0,0,0,0,0,0,0
    .asc "U","I","O","P",0,$7f,0,0
    .asc "Y","H","G","E",0,"A","S","W"
    .asc "8","L","0",0,0,$0d,0,0

Obviously there is a part which is repeated and could be put on a subroutine to save some memory, but appart from that... Can any other optimizations be done?

The MaskCode table is used to get the bitmask. Probably it could be set in a variable in page 0 and then rotate it at each loop.

Also checking column 4 ($ef) can be skipped, as it contains no alphanumeric chars as someone suggested in the game forum.

Please any advice is welcome.

Last, there will be another routine to check inflight keys (those that have to be scanned simultaneously). It will be called every 4th interrupt. Can I rely on having both running at the same time and both detecting the same key? Imagine that 'A' is used for fire. It should be detected by the inflight routine (a bit will be set in a memory position) and also by the other routine in case we are in a screen that needs it.

Update: this seems to work in the emulator. I have just tested it.

Cheers.
User avatar
Twilighte
Game master
Posts: 819
Joined: Sat Jan 07, 2006 12:07 am
Location: Luton, UK
Contact:

Post by Twilighte »

One of the (still) slow aspects of reading the keyboard is the writing of the column value to the i/o port of the AY.
However (unlike the above routine) there is another technique that can be used to accelerate the routine further..
Since we don't use the AY (during key reading) for anything other than setting the column we can set the register to $0E and set Port B CB2 to pulse mode in the PCR register. :twisted:
Then everytime we read or write to Port B we will send a pulse down CB2 line and trigger it to take the data in Port A as Register data.

So instead of this..

Code: Select all

LDA Register data
STA VIA_PORTA
LDA #$FD
STA VIA_PCR
LDA #$DD
STA VIA_PCR
We could do this..

Code: Select all

LDA Register data
STA VIA_PORTA
LDA VIA_PORTB
If you have the AUG then refer to the 6522 section on the PCR register to set this mode up but remember to set CB2 back to Disabled after everything.

Note: We cannot (should not) set CB2 permanently high since on some orics (around 50%) this crashes the AY. Strange but true.

I'll try to rewrite the routine using the above technique for you and optimise as much as possible before next weekend :P

The other big question (which will have a big bearing on the size of any key read routine) is what keys you actually want to detect?
If (in no part of the game) you will require people to type names (like hiscore etc.) then the alphanumeric table can be reduced to just the keys you want to detect. Also the selection of keys can have a bearing.

For example in ZipnZap i chose only one key from every row, so reducing the need to keep setting the column. An incredibly fast single sweep row scan could detect all keys for the game (8 keys).
User avatar
Chema
Game master
Posts: 3014
Joined: Tue Jan 17, 2006 10:55 am
Location: Gijón, SPAIN
Contact:

Post by Chema »

Twilighte wrote:Since we don't use the AY (during key reading) for anything other than setting the column we can set the register to $0E and set Port B CB2 to pulse mode in the PCR register. :twisted:
Then everytime we read or write to Port B we will send a pulse down CB2 line and trigger it to take the data in Port A as Register data.
That would be great! I suppose the key point is keeping the IRQ routine as small and fast as possible and defer all the work to outside the routine. That is why Dbug suggested to make the translation to ascii code outside the routine.
I'll try to rewrite the routine using the above technique for you and optimise as much as possible before next weekend :P
Thanks indeed. That would help a lot. Till now I am just trying to clear things in my head and test how it could be done, to keep it as simple as possible to the main code of the game.

That is why I suggested to keep a bitmap with simultaneous keypresses for ship control and something like the above code to read other keys, which do not need simultaneous detection and do not need to be fast.
The other big question (which will have a big bearing on the size of any key read routine) is what keys you actually want to detect?
If (in no part of the game) you will require people to type names (like hiscore etc.) then the alphanumeric table can be reduced to just the keys you want to detect. Also the selection of keys can have a bearing.
That is the problem, really. You control your ship down/up, turn left/right, roll left/right, accelerate/deccelerate and fire. Probably also add targetting or launching a missile (still did not decide how to implement this).

But you also need to scan many other keys to access screens (front/rear view, general status, maps, market, equipment, save/restore, hyperspace, galactic hyperspace and many others...) and will also need to scan all the alphanumeric chars to let the user enter input (commander's name, searching a planet by its name,...). All this can be scanned at a lower pace, and we don't need to care about simultaneous keypresses, so having the key code somewhere is more than enough. Then I could write a small routine to get the character with no bounce (simply keeping the old value and comparing).

The main code will test the bitstate for inflight control performing all actions for which the bit is set to 1. Then it can get the keycode of the last key pressed and react the same as it is currently doing with the ROM routine, which is already working.

Dbug came up with a very nice idea I did not implement yet, but that might be considered as a good option. The thing (if I understood correctly) is keeping a virtual keyboard matrix of 8 bytes in memory, where bits set to 1 indicate a keypress.

The IRQ routine will run at 100Hz and at each call would scan a given row. We can first write 0 to the column register to see if there is a keypress there. If not then return quickly, else scan each column and set the correct bits into the virtual matrix.

After 8 IRQs you have all the matrix updated.

This idea is a very general routine that could be used nearly in any situation; we could write a set of routines to work with the matrix (such as readKey, to return the ascii code of the first 1 bit encountered, or readKey_nobounce), but still keep a way to detect simultaneous keypresses by simply testing the corresponding bits in the matrix. If we come up with a good solution, we could even post a general method for scanning the keyboard efficiently.

Ideas/comments?

Cheers.
User avatar
carlsson
Pilot Officer
Posts: 127
Joined: Thu Jan 12, 2006 11:26 pm
Location: Västerås, Sweden

Post by carlsson »

If you want to optimize away the MaskCol table, I believe you can use something like this:

Code: Select all

    lda #$80
looprow
    pha
    eor #$ff
    sta via_porta
    lda #$fd
    sta via_pcr
    sty via_pcr

    lda via_portb
    and #%11111000
    ora row
    sta via_portb

    nop
    nop
    nop
    nop
    lda via_portb
    and #08
    bne keydet
    pla
    lsr
    bne looprow

skip:
    dex
    bpl loopcol
    ...

keydet:
    pla
    ...
If it works, it would save a whopping .. six bytes! :lol: I don't know about clock cycles though, if PHA, PLA, EOR, LSR will take more time than LDA $NNNN,X and DEX.
Anders Carlsson
User avatar
Twilighte
Game master
Posts: 819
Joined: Sat Jan 07, 2006 12:07 am
Location: Luton, UK
Contact:

Post by Twilighte »

There is also a method that effectively places the column register in PortA itself. Unfortunately whilst this works on 100% of real Orics it won't work on Emulators :(

The method involves setting up the Shift register to shift out cyclically then storing 10101010 in the shift register and setting PCR CB2 to disabled (Shift register takes over CB2).

This mean the column is simply chosen by writing to VIA_PORTA

However it takes a few more bytes to setup and disable after.
Anyway its the same technique as playing a sample at maximum speed (something around 44Khz) :P
User avatar
Chema
Game master
Posts: 3014
Joined: Tue Jan 17, 2006 10:55 am
Location: Gijón, SPAIN
Contact:

Post by Chema »

carlsson wrote: If it works, it would save a whopping .. six bytes! :lol: I don't know about clock cycles though, if PHA, PLA, EOR, LSR will take more time than LDA $NNNN,X and DEX.
Thanks for your input. About the cycles, that's easy to calculate: your option takes 10 cycles and the lookup table 6 (adding an extra one if a page is crossed, but that can be avoided), so it is only 4 cycles. I guess that is not very important (is it?), although 6 bytes extra aren't either (well, it depends).

More will be wasted if we put the common code in a subroutine and perform the jsr/rts (12 cycles per call!).
Twilighte wrote:There is also a method that effectively places the column register in PortA itself. Unfortunately whilst this works on 100% of real Orics it won't work on Emulators :(
That is indeed a very nice trick! However I need it to work on the emulator for developing :(

Dbug suggested to remove the conversion to ascii code, and I agree. That can be done in the calling program.

Cheers.
User avatar
Twilighte
Game master
Posts: 819
Joined: Sat Jan 07, 2006 12:07 am
Location: Luton, UK
Contact:

Post by Twilighte »

Chema wrote:That is indeed a very nice trick! However I need it to work on the emulator for developing :(
Sure, i thought you might say that.
Chema wrote:Dbug suggested to remove the conversion to ascii code, and I agree. That can be done in the calling program.
Oh absolutely, the only reason for ascii is to ease the entering of names in hiscores otherwise you don't need to know the ascii code the key represents.
As i say, i'll try to get this done asap.
User avatar
Dbug
Site Admin
Posts: 4444
Joined: Fri Jan 06, 2006 10:00 pm
Location: Oslo, Norway
Contact:

Post by Dbug »

The basic idea I had was to have two main matrix tables representing the instant state of the keys as read by the IRQ. For 64 potential keys, that's 8 bytes for each table.

When the IRQ triggers, it updates the "new keyboard state table". The previous content is in the "previous keyboard state table".

With these two 8 bytes tables, you can:
- Know which keys are currently pressed, that's the type of information you want to handle flight control
- Know which key just changed state (just do a EOR between the previous table and the new table), and in particular if the key just got pressed or released by comparing the previous/current tables.

Actually it is even probably possible to insert the current to previous state update in the sery of nops you all got in the irq code, with that the table update become basically free.

That was for the irq side.

The rest (convert to ascii, keyboard buffer with ascii characters and auto-repeat, ...) can be handled in the game main loop.

Hope I was clear, had a bad headache this morning...
User avatar
Twilighte
Game master
Posts: 819
Joined: Sat Jan 07, 2006 12:07 am
Location: Luton, UK
Contact:

Post by Twilighte »

Dbug, i have used this matrix idea before, basically creating a virtual map of all keys?

The trouble is this is quite processor intensive since a delay must exist when reading every single key(58 or 59 i think).

The trick is to place the code that would calculate the bitpos and byte in the delay (nop's) rather than use nop's and tables so improving the speed.

Note: Be aware that PC keyboards only permit up to 4 or 5 simultaneous key presses.

Obviously using the Column code Zero technique improves the speed somewhat.
Another point i'll make here is that key repeats are better handled in an irq routine, though they can exist outside if a slave timer exists in the irq like this one..

Code: Select all

      LDA Timer1
      BEQ SKIP1
      DEC Timer1
SKIP1 RTS
Otherwise controlling the delay between repeats and the initial delay (which is usually different) can be tricky :P
User avatar
Dbug
Site Admin
Posts: 4444
Joined: Fri Jan 06, 2006 10:00 pm
Location: Oslo, Norway
Contact:

Post by Dbug »

Not sure about why we have to compute a bitpos and byte ?
The idea is just to have a in-memory copy of the keyboard matrix state, so basically 8 bytes of 8 bits each, so technically one if the column value, and one is the row value, and that's about it, no ?

Something like that:

Code: Select all

PrevTableMatrix .dsb 8
TableMatrix .dsb 8

 ; 8 columns
 ldx #7
loop_col
 ; Update old table
 lda TableMatrix,x
 sta PrevTableMatrix,x
 ; Nothing pressed
 lda #0
 sta TableMatrix,x
 ; 8 rows
 ldy #7
loop_row
 testbit
 bcc .skip
_patch
 lda #1
 ora TableMatrix,x
.skip
 lsl _patch+1
 dey
 bpl loop_row
 dex
 bpl loop_col
User avatar
Twilighte
Game master
Posts: 819
Joined: Sat Jan 07, 2006 12:07 am
Location: Luton, UK
Contact:

Post by Twilighte »

Timothy walked across the long, long dessert with his friends by his side, the water they had brought quickly ran out and gradually, one by one his friends either fell behind him or found refuge in the many ice cream vans on either side of his path. However Timothy continued believing only his path was the true path to wisdom and salvation. Soon he collapsed in a heap believing his friends had only seen mirages..


It took me a while to realise the wisdom of the way you did above and am happy to say it is actually a very efficient way of doing it. I have added some of the techniques above in the code below.

Code: Select all

;ChemaRead.s

Driver  ;Write Column Register Number to PortA
        lda #$0E
        sta VIA_PORTA

        ;Tell AY this is Register Number
        lda #$FF
        sta VIA_PCR

        ;Setup PB read pulsing CB2
        ldy #%10111100
        sty VIA_PCR

        ldx #7
.(
loop2   ;Clear relevant bank
        lda #00
        sta KeyBank,x

        ;Write 0 to Column Register
        sta VIA_PORTA

        ;Send Column and write Row
        stx VIA_PORTB

        ;Wait 10 cycles for curcuit to settle on new row
        ;Use time to load inner loop counter and load Bit
        ldy #$80
        nop
        nop
        nop
        lda #8

        ;Sense Row activity
        and VIA_PORTB
        beq skip2

loop1   ;Store Column
        tya
        eor #$FF
        sta VIA_PORTA
        stx VIA_PORTB

        ;Use delay(10 cycles) for setting up bit in Keybank and loading Bit
        tya
        ora KeyBank,x
        sta zpTemp01
        lda #8

        ;Sense key activity
        and VIA_PORTB
        beq skip1

        ;Store key
        lda zpTemp01
        sta KeyBank,x

skip1   ;Proceed to next column
        tya
        lsr
        tay
        bcc loop1

skip2   ;Proceed to next row
        dex
        bpl loop2
.)
        ;Disable Pulse mode in PCR
        lda #$DD
        sta VIA_PCR

        rts	


Please note that the above code has not been tested but should work, however i cannot remember if Euphoric supports CB2 Pulse mode.
User avatar
Dbug
Site Admin
Posts: 4444
Joined: Fri Jan 06, 2006 10:00 pm
Location: Oslo, Norway
Contact:

Post by Dbug »

Twilighte wrote:Timothy walked across the long, long dessert with his friends by his side, the water they had brought quickly ran out and gradually, one by one his friends either fell behind him or found refuge in the many ice cream vans on either side of his path. However Timothy continued believing only his path was the true path to wisdom and salvation. Soon he collapsed in a heap believing his friends had only seen mirages..
Interesting, who wrote this?
Twilighte wrote:It took me a while to realise the wisdom of the way you did above and am happy to say it is actually a very efficient way of doing it. I have added some of the techniques above in the code below.
Cool !
Hopefuly by mixing everybody's ideas we will manage to ge something nice :)
User avatar
Twilighte
Game master
Posts: 819
Joined: Sat Jan 07, 2006 12:07 am
Location: Luton, UK
Contact:

Post by Twilighte »

Dbug wrote:Interesting, who wrote this?
umm.. me :)
Post Reply