6502.org Forum  Projects  Code  Documents  Tools  Forum
It is currently Fri Nov 01, 2024 12:30 am

All times are UTC




Post new topic Reply to topic  [ 12 posts ] 
Author Message
PostPosted: Mon Dec 28, 2020 3:32 am 
Offline
User avatar

Joined: Wed Aug 05, 2020 8:41 pm
Posts: 47
Location: Montreal, QC, Canada
Initially, I wanted to use an old matrix keyboard but later decided to implement Daryl Rictor's PS/2 code (https://sbc.rictor.org/pckb6522.html) on my 65C02 SBC using 2 pins on a 65C22. Everything works fine (with no interrupts). Better than expected compared to other code I've toyed with.

Alas, I do use regular interrupt, every 10ms, to refresh my display, and eventually perform other tasks. Of course, if an interrupt pops up in the middle of bit-banging the scan code, it messes up the timing. My idea is to use the SEI and CLI opcodes in between the kbscan routine (which does not wait indefinitely like the kbinput routine does). But when I do use these opcodes, the interrupts stop altogether. I tried variations and permutations to no avail.

Obviously, my code is at fault but was wondering if there were any particularities I should know about SEI and CLI? I even thought of embedding keyboard polling within the above-mentionned interrupt routine. Also, I was not, yet, successful in implementing CA1 to generate an interrupt on the first clock cycle. The datasheet is poor in proper explanations. Anybody care to share their experiences with PS/2 keyboards and 6502s?

Thanks,

https://6502sbc.blogspot.com/

_________________
Fred Segard
A.K.A. The Micro Hobbyist
https://6502sbc.blogspot.com/


Top
 Profile  
Reply with quote  
PostPosted: Mon Dec 28, 2020 8:20 am 
Offline
User avatar

Joined: Sun Jun 30, 2013 10:26 pm
Posts: 1948
Location: Sacramento, CA, USA
Welcome, Fred! Are you willing to post your source (or at least a link to it) for us to check it out?

_________________
Got a kilobyte lying fallow in your 65xx's memory map? Sprinkle some VTL02C on it and see how it grows on you!

Mike B. (about me) (learning how to github)


Top
 Profile  
Reply with quote  
PostPosted: Mon Dec 28, 2020 1:34 pm 
Offline
User avatar

Joined: Fri Dec 11, 2009 3:50 pm
Posts: 3367
Location: Ontario, Canada
fredericsegard wrote:
Of course, if an interrupt pops up in the middle of bit-banging the scan code, it messes up the timing.
Good morning, Fred! I'm only on my first cup of coffee, but let's see if I can succeed in saying something useful. :wink:

Firstly we need to acknowledge that keypresses arrive without warning -- the time of their arrival isn't under our control. Thus it's necessary to either...
  1. poll the 65C22 inputs continuously
  2. have an interrupt only on the first clock edge of a keypress, and stay in the Interrupt Service Routine until all 8 clock edges have occurred, or...
  3. have an interrupt with every clock edge, and exit the ISR immediately after inputting and storing each bit of data. Compared with (b), this will be tidier and easier in the long run.

You mentioned there's already a regular display-refresh interrupt in place, but of course its timing will only very rarely match the arrival of a keypress. Thus you're better off to let the display-refresh interrupt mind its own business, and the keypress interrupt likewise -- keep the two independent.

Keypress interrupts are time sensitive (and the associated ISR will execute very rapidly assuming approach c is used); therefore I suggest that keypress interrupts be assigned higher priority than the display refresh interrupt. There are two ways to enforce this. NMI is one way to get higher priority, but it's perhaps better and easier for you to arrange that the lower priority ISR can in turn be interrupted by the higher priority interrupt.

According to my cheat sheet below, for CA1 to generate an interrupt you need Bit1 of IER to equal 1, and Bit4 of PCR to =1 or =0 as required.

I hope that's enough to get you pointed in the right direction. For further info, see in the interrupt section of Garth's Primer.

Cheers,
Jeff


Attachments:
VIA cheatsheet1.png
VIA cheatsheet1.png [ 875.19 KiB | Viewed 2007 times ]

_________________
In 1988 my 65C02 got six new registers and 44 new full-speed instructions!
https://laughtonelectronics.com/Arcana/ ... mmary.html
Top
 Profile  
Reply with quote  
PostPosted: Mon Dec 28, 2020 5:04 pm 
Offline
User avatar

Joined: Wed Aug 05, 2020 8:41 pm
Posts: 47
Location: Montreal, QC, Canada
Thank you! :D

Right now, Daryl's driver is polling only. I studied it and the PS/2 protocol (as best I can), so it's only a matter of time and "headbanging-it" until I make things work as an interrupt driver.

Instead of embedding it into my "bios/monitor" code right away, I'll make a new proof of concept asm file, so it's easier and smaller to share. I'll post it very soon.

Quote:
Firstly we need to acknowledge that keypresses arrive without warning -- the time of their arrival isn't under our control. Thus it's necessary to either...
a. poll the 65C22 inputs continuously
b. have an interrupt only on the first clock edge of a keypress, and stay in the Interrupt Service Routine until all 8 clock edges have occurred, or...
c. have an interrupt with every clock edge and exit the ISR immediately after inputting and storing each bit of data. Compared with (b), this will be tidier and easier in the long run.

Option C sounds like an interesting solution. My initial instinct would have been option B. I'm not yet proficient in dealing with interrupts. Timer1 was easy enough, but CA1/2 and CB1/2 for some reason, seem to be a big mystery to me.

_________________
Fred Segard
A.K.A. The Micro Hobbyist
https://6502sbc.blogspot.com/


Top
 Profile  
Reply with quote  
PostPosted: Mon Dec 28, 2020 8:25 pm 
Offline
User avatar

Joined: Fri Dec 11, 2009 3:50 pm
Posts: 3367
Location: Ontario, Canada
Alright, for starters let's think about the polling driver. In order to know "when the action is" it actually reads I/O. But the interrupt version I'm imagining (based on option c) will instead examine a byte in memory (which I'll call the Status Byte) which is a simple message that gets updated by the ISR. The message has two parts, each just a single bit:

- a bit (let's say it's Bit7) that says a new interrupt has occurred, and...
- a bit (let's say it's Bit0) that equals the 1 or 0 just received by the new interrupt

Upon initialization of the SBC, Bit7 gets set to 0; you'll also want to set the 65C22 control registers as required.

Later, every time the driver goes to fetch a keypress, the process is as follows:
  1. to act as a bit counter, set a register or memory location to zero
  2. keep examining Bit7 of the status byte and wait until it equals 1.
  3. get Bit0 of the status byte and shift it into a register or memory location.
  4. set Bit7 of the status byte back to 0. (Or clear the entire byte.)
  5. increment the counter to see if all bits have been received yet. If not, loop back to 2.
  6. set the bit counter location to zero
  7. get the finished scan code that's now fully present in the shifter location

The silent partner on the team is the ISR. As you can probably guess, its job is simply to input the bit from I/O and copy it into Bit0 of the status byte. Then it sets Bit7 of the status byte and exits. As much as possible, we try to avoid putting a lot of smarts into the ISR -- we want it to execute as quickly as possible.

I've left some details for you to sort out, for example the CLI you'll need before the ball can get rolling. You'll also want a CLI somewhere near the beginning of the refresh ISR so that ISR can in turn be interrupted. And probably some other stuff I've forgotten.

To get the code built and working, it might be best to start with just a simple ISR which merely sets a bit. Then flesh things out from there.

Finally, I notice Daryl's driver includes features that involve outputting to the keyboard. I've ignored all that -- presumably these code snippets needn't be interrupt driven (or perhaps the features can be omitted).

-- Jeff

EDIT: Instead of passing along the bits one at a time (and relying on foreground code to do the shifting), it'd be better to have the ISR also do a one-bit shift with each of the 8 interrupts. It would also update the bit count. By watching the count, the foreground code would know when all bits have arrived -- it would then grab the complete scan code and do what's necessary to digest it. I suggest this approach because it may occasionally happen that two scan codes arrive in close succession, and using the one-bit-at-a-time approach there's a risk the program may still be digesting the first scan code when the first bits of the second scan code begin arriving. So, better to let the ISR shift those bits to ensure they'll be saved rather than getting ignored.

(Carrying this one step further, the ISR could enter completed scan codes into a circular FIFO buffer, and this would allow even more protection in case your application program is VERY slow digesting scan codes.)

_________________
In 1988 my 65C02 got six new registers and 44 new full-speed instructions!
https://laughtonelectronics.com/Arcana/ ... mmary.html


Top
 Profile  
Reply with quote  
PostPosted: Tue Dec 29, 2020 8:16 pm 
Offline
User avatar

Joined: Sun Dec 27, 2020 11:12 pm
Posts: 94
Location: France
Frederic,
I have spent quite a bit of time on a fully interrupt driven PS/2 driver, and while it's not 100% stable yet it does seem to behave rather well.
It uses option C above (one interrupt for every PS/2 clock edge)
After a while though I was getting framing errors and all subsequent keypresses would be wrong. To solve it I just implemented a VIA timer that resets the bit counter and the temp variable if there was no PS/2 interrupt for a while (0.1s I think) and this completely solved the framing errors.
Another issue I had was that my keyboard kept sending $AA over and over again (pressing the keys did nothing) until I sent it the reset command ($FF) So I also had to implement sending data from the host to the keyboard. That part has not been tested as well unfortunately.

I can share the code I have so far if you would like to take a look at it.

_________________
Jonathan Foucher

Take a look at the Planck 6502 computer.


Top
 Profile  
Reply with quote  
PostPosted: Tue Dec 29, 2020 9:13 pm 
Offline
User avatar

Joined: Fri Dec 11, 2009 3:50 pm
Posts: 3367
Location: Ontario, Canada
jfoucher wrote:
It uses option C above (one interrupt for every PS/2 clock edge)
After a while though I was getting framing errors and all subsequent keypresses would be wrong.
Thanks for posting about your experience, jfoucher.

Framing errors is a subject somewhat relevant to a few edits I recently made to my post above -- perhaps you've not seen the latest. There's now an additional option or two (perhaps they should be called d and e). I mention a shortcoming regarding option c, and the associated problem would trigger a framing error. d and e resolve that particular shortcoming.

-- Jeff

_________________
In 1988 my 65C02 got six new registers and 44 new full-speed instructions!
https://laughtonelectronics.com/Arcana/ ... mmary.html


Top
 Profile  
Reply with quote  
PostPosted: Tue Dec 29, 2020 9:33 pm 
Offline
User avatar

Joined: Sun Dec 27, 2020 11:12 pm
Posts: 94
Location: France
Well actually the code I used is similar to what you describe in your edit.

The ISR is a state machine with 4 states: START (for the start bit); DATA (where each incoming bit get shifted into position), PARITY (where parity is... well ignored actually) and STOP (the stop bit where the supposedly complete scan code is converted to ascii and placed in a buffer).

I just published my ISR code here: https://gist.github.com/jfoucher/d25ecf55c94f64f82b2308bac42d6653#file-ps2_irq-s

It seems to me that this would address the concerns you mention in your edit ?

_________________
Jonathan Foucher

Take a look at the Planck 6502 computer.


Top
 Profile  
Reply with quote  
PostPosted: Wed Dec 30, 2020 1:07 am 
Offline
User avatar

Joined: Fri Dec 11, 2009 3:50 pm
Posts: 3367
Location: Ontario, Canada
I were writing such a thing I think I'd just let the bit counter be the state machine. Offhand I don't see why it couldn't tell you everything you need to know about what's going on. But maybe I'm missing something.

Quote:
To solve it I just implemented a VIA timer that resets the bit counter and the temp variable if there was no PS/2 interrupt for a while
OK, if we need a recovery mechanism then that sounds workable... but I can think of two variations.

Maybe you don't need to dedicate a VIA timer. You mentioned a periodic interrupt in connection with display refresh (edit: oops, that was Fred! :oops: But I'll still leave this part in), so is there already a VIA timer running for that? Maybe you could re-use it as your reference. With each scancode-bit interrupt you'd read that timer and save the value as a time stamp. Looking at the previous time stamp would let you compute the elapsed time. During states for which you expect the elapsed time to be one bit period, check to see that it *is*. (Just be sure the elapsed-time computation corrects the hiccup when the timer reloads, or that that computation isn't acted upon.)

The other idea is this: You'll soon have a parity error if the scan codes are misframed, and parity is easy enough to check. The error response could be crude (probably using polling :roll: ) but all you need is to wait for a pause in the bitstream.

-- Jeff

_________________
In 1988 my 65C02 got six new registers and 44 new full-speed instructions!
https://laughtonelectronics.com/Arcana/ ... mmary.html


Top
 Profile  
Reply with quote  
PostPosted: Wed Dec 30, 2020 9:16 am 
Offline
User avatar

Joined: Sun Dec 27, 2020 11:12 pm
Posts: 94
Location: France
Dr Jefyll wrote:
I were writing such a thing I think I'd just let the bit counter be the state machine. Offhand I don't see why it couldn't tell you everything you need to know about what's going on. But maybe I'm missing something.

Yes that was the way I did it at first (saves one byte of memory too), but then one day I rewrote the whole thing out of frustration. I'm also not sure why the state machine would be more reliable, but I think it might be related to my hardware being "flaky" with interrupts (I need to investigate)

Dr Jefyll wrote:
Maybe you don't need to dedicate a VIA timer. You mentioned a periodic interrupt in connection with display refresh (edit: oops, that was Fred! :oops: But I'll still leave this part in), so is there already a VIA timer running for that?

That's what actually made me chime in and (hopefully not quite) hijack Fred's thread! This periodic interrupt could indeed be used to clear eventual framing errors.

_________________
Jonathan Foucher

Take a look at the Planck 6502 computer.


Top
 Profile  
Reply with quote  
PostPosted: Sat Jan 02, 2021 6:04 pm 
Offline
User avatar

Joined: Wed Aug 05, 2020 8:41 pm
Posts: 47
Location: Montreal, QC, Canada
I implemented option C. Managed to figure things out. Alone, the driver workers flawlessly. But if I implement another ISR with timer1 for example, I get framing errors, and my timer1 ISR doesn't execute as it should either. Maybe it's in my ISR setups that I falter? The kb_get routine may look simplistic, but it gets the job done. In the end KB_BYTE is ready when KB_BYTE_READY is $FF, else it's $00. That's when I know I have a valid scan code. In the IRQ vector, I try to discard other ISRs by checking the KB_BIT bucket counter. No other ISR should run if it's not zero.

Code:
interrupt_init:
               ; SETUP VIA1 TIMER
               ; ================
               ; Sets counter to $4E20 to perform 1/100th of a second interrupt @ 2MHz
               ;              or $9C40 to perform 1/50th of a second interrupt @ 2MHz
               ; To a maximum of $FFFF to perform 1/30.5th of a second interrupt @ 2MHz
               ; ---------------------------------------------------------------------
   lda   #$20         ; Put value in the VIAs timer 1 counter
   sta   VIA1_T1C_L      ; Loads data to low order
   lda   #$4E         ; Put value in VIA's timer 1 counter
   sta   VIA1_T1C_H      ; Loads data to hign order. This starts the countdown.

   lda   #%01000011      ; Set for continuous interrupts
   sta   VIA1_ACR      ; Store value into ACR

   lda   #%11000000      ; Enable timeout interrupt
   sta   VIA1_IER      ; Store it in the Interrupt Enable Register

   stz   INT_COUNT      ; Zero out interrupt timer counter

               ; SETUP VIA1 CB1
               ; ==============
   lda   #$00000000      ; Load PCR value (bit 5 = 0 for CB1, the rest is not used)
   sta   VIA1_PCR      ; Store value back into PCR

   lda   VIA1_IER      ; Load existing IER values
   ora   #%10010000      ; Enable CB1 interrupt (Falling Edge)
   sta   VIA1_IER      ; Store it in the Interrupt Enable Register

   cli

   rts

Code:
IRQ:

   pha
   phx
   phy
               
IRQ_is_via1_CB1:         ; Is it VIA2's IRQ?
   lda   VIA1_IFR      ; Read the Interrupt Flag Register from VIA1
   and   #$10         ; Isolate the CB1 flag?
   bne   ISR_kb         ; If it's the CB1 flag then execute keyboard routine
   lda   KB_BIT         ; Is there is data in KB_BIT bucket
   bne   IRQ_end         ; If not, then don't execute any other ISRs (high priority)

IRQ_is_via1_timer1:         ; Else, is it VIA1 timer1?
   lda   VIA1_IFR      ; Read the Interrupt Flag Register from VIA1
   and   #$40
   bne   ISR_VIA1_timer1      ; If it's the timer flag then execute timer routine

   jmp   IRQ_end         ; Else, end IRQ

               ; VIA1 INTERRUPT
ISR_VIA1_timer1:         ; --------------
   lda   VIA1_T1C_L      ; Read to clear Interrupt Flag Register *necessary*
   inc   INT_COUNT      ; Increment interrupt counter
   lda   INT_COUNT      ; Load interrupt counter into A
   cmp   #10         ; Has it reached the tenth time
   bne   ISR_VIA1_timer1_end   ; No? then don't execute the interrupt
   stz   INT_COUNT      ; Yes, then clear counter
   ldx   #0         ; Column (1st character)
   ldy   #3         ; Row 3 (4th line)
   jsr   lcd_set_cursor_xy   ; Goto XY on LCD
   jsr   lcd_print_datetime   ; Print's time and date on the LCD
   jsr   lcd_print_buffer
ISR_VIA1_timer1_end:
   jmp   IRQ_end

               ; VIA2 INTERRUPT
ISR_kb:               ; --------------
   lda   VIA1_PORT_B      ; Read to clear Interrupt Flag Register *necessary*
   inc   KB_BIT         ; Increment bit counter
   jsr   kb_get         ; Receive a bit from a scan code
   bra   IRQ_end         ; End ISR

IRQ_end:
   ply
   plx
   pla
   rti

Code:
kb_get:
   phx
   lda   VIA1_DDR_B      ; Load DDR
   and   #%00111111      ; Set KB_CLK and KB_DATA as inputs ***
   sta   VIA1_DDR_B      ; Save DDR
   ldx   KB_BIT         ; Load current bit number in X
kb_get_start_bit:
   cpx   #1         ; Check start bit, clock pulse 1
   bne   kb_get_parity      ; If it's not the 1st clock pulse, check the 10th (parity bit)
   lda   VIA1_PORT_B      ; Load port A
   and   #KB_DATA      ; Filter data bit
   cmp   #$00         ; Start bit must be zero
   bne   kb_get_error_handler   ; If the start bit is a one,
   stz   KB_BYTE         ; Reset KB_BYTE variable (contains the final scan code)
   stz   KB_BYTE_READY      ; Reset KB_BYTE_READY variable (when all the bits are settled in KB_BYTE)
   stz   KB_PARITY      ; Prepare KB_PARITY variable (to calculate parity)
   bra   kb_get_end      ; Exit routine
kb_get_parity:
   cpx   #10         ; Check parity bit, clock pulse 10
   bne   kb_get_stop_bit      ; If it's not the 10th clock pulse, check the 11th (stop bit)
   nop            ; For now, do nothing for parity checking, will come back to it later
   bra   kb_get_end      ; End routine
kb_get_stop_bit:
   cpx   #11         ; Check stop bit, clock pulse 11
   bne   kb_get_data      ; If it's not the 1st clock pulse, check the the remaining 2 to 9th (data bits)
   lda   VIA1_PORT_B      ; Load port A
   and   #KB_DATA      ; Filter data bit
   cmp   #KB_DATA      ; Stop bit must be one
   bne   kb_get_error_handler   ; If the start bit is a one,
   stz   KB_BIT         ; Clear bit (packet) counter for next scan code
   stz   KB_ERROR      ; Clear error flag
   lda   #$FF         ;
   sta   KB_BYTE_READY      ; Scan code fully assembled in KB_BYTE
   bra   kb_get_end      ; Exit routine
kb_get_data:
   lda   VIA1_PORT_B      ; Load data
   and   #KB_DATA      ; Filter data bit
   cmp   #KB_DATA      ; Carry is 1 if KB_DATA is 1, carry is 0 if KB_DATA is 0
   ror   KB_BYTE         ; Store bit
   bra   kb_get_end      ; Exit routine

kb_get_error_handler:         ; Currently no error handler
   lda   #$FF         ;
   sta   KB_ERROR      ; An error has occured
   lda   #"."
   jsr   print_char

kb_get_end:
   plx
   rts

Attachment:
File comment: The complete code of my BIOS up to date (complete with beginner mistakes)
bios-v0.10.asm [130.66 KiB]
Downloaded 113 times

EDIT: Forgot to mention a thank you to Daryl Rictor for his PS/2 code... modified to suit my needs.

_________________
Fred Segard
A.K.A. The Micro Hobbyist
https://6502sbc.blogspot.com/


Top
 Profile  
Reply with quote  
PostPosted: Sat Jan 02, 2021 6:08 pm 
Offline
User avatar

Joined: Wed Aug 05, 2020 8:41 pm
Posts: 47
Location: Montreal, QC, Canada
Oh, and a big thanks for your responses. It's greatly appreciated.

_________________
Fred Segard
A.K.A. The Micro Hobbyist
https://6502sbc.blogspot.com/


Top
 Profile  
Reply with quote  
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 12 posts ] 

All times are UTC


Who is online

Users browsing this forum: No registered users and 2 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to: