Jump to content
intvnut

Reading the ECS Keyboards

Recommended Posts

Introduction

 

The ECS has two different keyboards you can connect to it: The alphanumeric keyboard and the synthesizer keyboard. This topic intends to describe the process of reading both, as well as to provide useful code for reading each. If you just want to use the code, you can skip most of this post and just download the code. You don't need the low level detail to use the code. I'm providing the low-level detail so that you can understand the code if you want to, or write your own routines if you want to.

 

Hardware Background

 

The ECS provides a second sound chip to the Intellivision (AY-3-8917), which is similar to the AY-3-8914 in the Master Component, only mapped at $00Fx rather than $01Fx. Like the Master Component's sound chip, the ECS's provides two 8-bit I/O ports.

 

(Side note: David "Papa Intellivision" Chandler's recently released documentation indicates that the AY-3-8914 was modified at some point to make its I/O ports input-only, unlike every other variant of the device with I/O ports. That is not relevant to this discussion; the AY-3-8917's I/O ports are bidirectional. It is an interesting historical footnote, though.)

 

Introduction to Matrix Scanning

 

Both keyboards connect to the AY-3-8917's 8-bit I/O ports. Both keyboards employ a basic technique called matrix scanning. The basic idea is simple. Making a matrix-scanned keyboard takes four steps:

  1. Construct a grid of "row lines" and "column lines".
  2. Connect your keyboard switches so that each one connects one "row line" to one "column line" when closed.
  3. Connect an output port to the "rows", so that you can selectively drive one row at a time.
  4. Connect an input port to the "columns", so that you can see what switches are closed on any given row when that row is driven.

That gives you a basic matrix-scanned keyboard. Here's a simple example of a 3x3 keyboard with the keys A through I. (Note: This is just meant to be an example; it is not the actual ECS keyboard matrix.)

 

simple_3x3.png

 

When you press 'A' in this example keyboard, it connects Row 0 to Col 0. When you press 'I', it connects Row 2 to Col 2.

 

To read such a keyboard, you need to scan each of the rows, one at a time, reading the columns after each. To start, you drive Row 0, and then read the columns. If one of the switches is closed in that row, then the signal on the Row 0 line will show up on the appropriate column. For example, if you press "B", then you'll see a signal on column 1. Next, you switch to driving Row 1 (and stop driving Row 0), and read the columns. And so forth.

 

Now, the observant will notice I left out a couple details in the diagram and description above. Now to add those back in. In the Intellivision, the keyboards use an "active low" scheme to scan the keyboard. That is, each row and column have pull-up resistors on them that cause them to read as 1 by default. So, to drive a row, you need to set that row's bit to 0. To detect a driven column, you need to look for a 0 in that column's bit.

 

Problems (and Solutions) with Matrix Scanning

 

Refer back to the example matrix above. What happens when you press multiple keys at the same time?

 

Let's say you press A and E at the same time. When you scan Row 0, you'll see Col 0 light up, and when you scan Row 1, you'll see Col 1 light up, as in the following diagrams.

 

simple_3x3_A_and_E_row0.png simple_3x3_A_and_E_row1.png

 

 

So far, so good. Now what happens when you also press B in addition to A and E? Let's scan Row 0:

 

simple_3x3_A_B_and_E_row0.png

 

We light up Row 0, and both Col 0 and Col 1 light up as we expect. But, the fuschia line above shows another line that gets lit up... Row 1! So far, that hasn't caused a problem. But if your Spidey Sense is tingling, telling you that things are about to go horribly wrong, you'd do well to listen to it. Let's scan Row 1 and watch it go pear shaped:

 

simple_3x3_A_B_and_E_row1.png

 

Since we've only pressed E in Row 1, we only want to see Col 1 light up. But, because the switches at B and A are also closed, we also have a complete circuit to Col 0, so both Col 0 and Col 1 light up. It looks to us as if both D and E are pressed at the same time. Oops!

 

This is sometimes called "ghosting," and can happen whenever you press keys on three corners of a rectangle in the keyboard matrix. It turns out that there's an easy fix for this: Diodes!

 

Now, partly because I'm lazy, partly because I'm still learning Inkscape and partly because I want to keep the diagram general, I'm just going to represent the diodes as rectangles below. The direction they face depends on whether you use an active-low scheme, like the Intellivision, or an active-high scheme. And, it depends on whether you scan by row (as we have so far) or scan by column. (More on this in a moment.)

 

With diodes, the keyboard matrix looks like this:

 

simple_3x3_diodes.png

 

Now if we press A, B and E, everything works like you expect, because the diodes block the ghost paths:

 

simple_3x3_diodes_A_B_and_E_row0.png simple_3x3_diodes_A_B_and_E_row1.png

 

Even though B lights up when we light up Row 1, the diode prevents that from inadvertently lighting up Row 0 and flowing (via the switch at A) to Col 0.

 

What's in the Intellivision's ECS Keyboards?

 

Both ECS keyboards are basic matrix scanned keyboards. The alphanumeric keyboard is a simple 48-key matrix with no diodes. There are 49 actual keys, but the "Shift" keys are wired to the same row/column in the matrix. If you press the wrong keys at the same time, you will see ghosting. The following diagram gives the matrix for this keyboard:

 

ecs_keyboard_matrix.png

 

You'll notice Row 6 only has the shift key, and Row 7 is empty. Also, there's no key associated with Row 0/Col 7.

 

The synthesizer keyboard, on the other hand, is a diode-filled 49-key matrix. This allows you to press whatever keys you like, without restrictions, and without ghost paths. The piano keys are mapped to matrix points in a very straight forward manner, so I won't bother with a matrix diagram. The lowest note on the keyboard corresponds to Col 0, Row 0, and the highest note is in Col 0, Row 6.

 

 

[TO BE CONTINUED: Since this post is getting fairly lengthy, I am going to save what I have, and then resume with an edit.]

  • Like 5

Share this post


Link to post
Share on other sites

Apparently, I can't edit the topic above, so my post will continue as comments.

 

Scanning the Alphanumeric Keyboard

 

As stated before, the alphanumeric keyboard uses a 48-position matrix, and has no diodes. Its matrix also has a peculiarity: "Shift" is in a row all by itself. We'll make use of that in a minute. First, let's look at how to program the hardware.

 

Programming the AY-3-8917 PSG I/O Ports

 

The keyboard connects to the two ports on the ECS's AY-3-8917 PSG. While we usually associate this chip with sound, the ECS also uses it for its keyboards. The PSG exposes its two I/O ports at locations $FE and $FF. The port at $FE connects to the rows in the diagram above, and the port at $FF connects to the columns.

 

Both ports are bidirectional. Bits 6 and 7 of the register at location $F8 control the I/O ports at $FE and $FF, respectively. To configure a port for input, set the corresponding bit to 0. To configure a port for output, set the bit to 1. Each 8-bit I/O port is either "entirely input" or "entirely output." There's no way to set some bits for input or some bits for output. The other bits at location $F8 control the tone and noise channel enables for the three sound channels. You will want to leave those lower bits undisturbed.

 

Both ports also have 8 pull-up resistors connected to them internally to the PSG. This causes input ports to return the value $FF by default if nothing is connected or if nothing happens to pull the line low. Also, to protect each port, the ECS includes 2KΩ current-limiting resistors in series with both ports--a total of 16 altogether! Here's a picture of that PSG with its current-limiting resistors, for the curious:

 

ecs_psg_closeup.jpg

 

 

 

Son of Ghosting: Peculiar Challenges Due To I/O Port Limitations

 

Recall the ghosting effect I described previously? It turns out, on the ECS, there's a related masking problem that works similarly, that arises due to the fact that the PSG's I/O ports are always "entirely input" or "entirely output".

 

In the previous discussion of matrix scanning, I indicated that you need to drive one row at a time. The implication is that the other rows remain undriven, and are free to float to whatever state they like. Pull-up resistors then float the undriven lines to 1. In an idyllic world, that's how you'd do things. Unfortunately, though, the ECS doesn't work that way.

 

Because of the internal pullups on the inputs, the ECS uses an active-low scheme to scan the keyboard. To read row 0, for example, you output $FE on port $FE, and read the result on port $FF. This drives row 0 low, and so if any keys in row 0 are pressed (left arrow, space, etc.), you'll see those bits go to 0. Here's the fun part, though: While row 0 gets driven to 0, rows 1 through 7 get driven to 1! Oops!

 

What fun does that cause?

 

Suppose you press the 'left arrow' key, which will set column 0, row 0. Next, also press the comma (column 0, row 1). What happens? Because Row 1 gets driven to 1 you now have both a 1 and a 0 driven onto column 0. It turns out that the 1 wins over the 0 and both key presses disappear, at least as far as the Intellivision is concerned. I've written a quick and dirty test program that will let you witness this. (BTW, jzIntv does mimic this behavior, if you want to experiment there.)

 

The test program scans the keyboard two ways: by row and by column. I'll discuss the latter in a moment. It displays two 8x8 tables full of 1s. In the left 8x8 table, each row of the table corresponds to one row in the keyboard matrix and each column represents one column of the keyboard matrix. The right-hand table transposes this, so each row corresponds to a column in the matrix, and vice versa. Below, you can see the output when no key is pressed, followed by pressing just the left arrow, followed by pressing left arrow plus comma:

 

shot0000.gif shot0001.gif shot0002.gif

 

As you can see in the left table, both keys "disappear" when they're both pressed. But, in the right table, they don't. That brings us to the next topic: Transposed scanning and a curious ECS bug.

 

 

Transposed Scanning, and the ECS's Shift Key Problem

 

Transposed scanning merely refers to switching the role of rows and columns in the keyboard matrix, where you drive individual columns, and read the input data via the rows.

 

As you see above, when you press two keys in the same column, they disappear from the row-oriented scan. But, they still appear in the column-oriented scan. Transposed scanning isn't magical though. If you press two keys in the same row, they'll disappear in the transposed scan but appear in the normal scan. Since both scanning modes have the same problem, and since you generally only press one key at a time anyway, why bother with a transposed scan?

 

The shift key, that's why. Refer back to the scanning matrix above, and you'll see that the L, J, G, D and A keys all share the same column as the shift key. But, the shift key has an entire row to itself. If you scan the ECS keyboard in the normal order (the same order the built in EXEC scans it, incidentally), you can not upper-case those letters! As it happens, the ECS BASIC was an all-caps environment, so you wouldn't notice it on the stock software. But, the bug is there.

 

Transposing the scan makes it possible to upper-case every key on the keyboard.

 

What About The CTL key?

 

The CTL key is the other modifier key on the board, and so far as I recall, nothing really made use of it. But, if we're to write our own keyboard scanning code, we may as well try to incorporate it too. CTL shares a row with "Space", "Down Arrow", "Up Arrow", Q, 1, "Right Arrow", and A. CTL shares a column with RTN, O, U, T, and E. In the transposed scan, CTL will get masked out with its row-mates. While we probably won't miss such dubious combinations as "CTL-Space" and "CTL-Down Arrow", we probably do still want CTL-Q and CTL-A. And, we certainly don't want to through CTL-O, CTL-U, CTL-T and CTL-E under the bus to get there.

 

What should we do then?

 

Easy: Scan for CTL twice, both with normal and transposed scans. (ie. light up row 5 and look at column 6, then light up column 6 and look at row 5) Take the AND of both scans and tada! Now you reliably know whether CTL is pressed.

 

"Not so fast!" you might say. "What about the other keys in the same row/column as CTL? You found CTL, but you've still lost its row-mates/column-mates!"

 

Ok, you got me. It's not that easy. To do CTL properly, you actually need to do a normal scan and decode all of Row 5 in addition to the transposed scan so you can see all of Col 6. I'll be honest: I haven't bothered yet. I did give up on CTL-Q and CTL-A for now. But, I know how to get them if I decide I want them back, and now you do too.

 

 

[TO BE CONTINUED -- I'm still writing more good stuff, but I want to save where I'm at.]

Edited by intvnut
  • Like 4

Share this post


Link to post
Share on other sites

How About Some Code, Already?

 

You mean that quick and dirty demo above wasn't good enough? Ok, ok....

 

This code applies most (but not all) of what we learned above to scan the ECS keyboard. It does a transposed scan. It does not, however, see CTL-Q, CTL-A or CTL with any other key in Row 5.

 

The code has two sections: The keyboard decoding tables, and the actual scanner. The decoding tables are fairly straightforward: Each column has 6 entries, one for each row's character for that column. I've included three decoder tables, one for unshifted input, one for shifted input, and one for CTL+letter. I don't have a separate table for CTL+Shift; rather, I just make Shift take precedence over CTL. Without further ado, here's the decoder tables:

 

KEY.LEFT    EQU     $1C     ; \   Can't be generated otherwise, so perfect
KEY.RIGHT   EQU     $1D     ;  |_ candidates.  Could alternately send 8 for
KEY.UP      EQU     $1E     ;  |  left... not sure...
KEY.DOWN    EQU     $1F     ; /   
KEY.ENTER   EQU     $A      ; Newline
KEY.ESC     EQU     27
KEY.NONE    EQU     $FF


KBD_DECODE  PROC
@@no_mods   DECLE   KEY.NONE, "ljgda"                       ; col 7
           DECLE   KEY.ENTER, "oute", KEY.NONE             ; col 6
           DECLE   "08642", KEY.RIGHT                      ; col 5
           DECLE   KEY.ESC, "97531"                        ; col 4
           DECLE   "piyrwq"                                ; col 3
           DECLE   ";khfs", KEY.UP                         ; col 2
           DECLE   ".mbcz", KEY.DOWN                       ; col 1
           DECLE   KEY.LEFT, ",nvx "                       ; col 0

@@shifted   DECLE   KEY.NONE, "LJGDA"                       ; col 7
           DECLE   KEY.ENTER, "OUTE", KEY.NONE             ; col 6
           DECLE   ")*-$\"/"                               ; col 5
           DECLE   KEY.ESC, "(/+#="                        ; col 4
           DECLE   "PIYRWQ"                                ; col 3
           DECLE   ":KHFS^"                                ; col 2
           DECLE   ">MBCZ?"                                ; col 1
           DECLE   "%_NVX "                                ; col 0

@@control   DECLE   KEY.NONE, $C, $A, $7, $4, $1            ; col 7
           DECLE   KEY.ENTER, $F, $15, $14, $5, KEY.NONE   ; col 6
           DECLE   "}~_!'", KEY.RIGHT                      ; col 5
           DECLE   KEY.ESC, "{&@`~"                        ; col 4
           DECLE   $10, $9, $19, $12, $17, $11             ; col 3
           DECLE   "|", $B, $8, $6, $13, KEY.UP            ; col 2
           DECLE   "]", $D, $2, $3, $1A, KEY.DOWN          ; col 1
           DECLE   KEY.LEFT, "[", $0E, $16, $18, $20       ; col 0
           ENDP

 

If you hold this up against the matrix I posted earlier, it should be obvious how the keyboard matrix relates to the mapping above. I'm usign a column-oriented transposed scan, so each DECLE contains the data for one column. I start at column 7 rather than column 0 mainly because I think backwards. ;-)

 

The KEY.xxx symbols define codes to return for various special keys on the keyboard, such as the arrow keys. KEY.NONE indicates invalid entries in the table. The keyboard scanner also returns KEY.NONE when there's no new input.

 

You might notice that neither Shift nor CTL actually show up in any of the three tables. Shift does not show up because it's in Row 6, and the table only has data for Row 0 through Row 5. CTL gets mapped to KEY.NONE because the code tests for CTL separately and explicitly. The reason will become more obvious when we get to the decoder loop.

 

If you wanted to change the keyboard map in some way -- I'm looking at you, Dvorak fans -- the tables above are where you'd change things.

 

Next up is the first part of the keyboard scanner. My scanner sets up the I/O ports for a transposed scan, and then looks immediately for CTL and Shift, in order to pick the right decoder table. It doesn't do the "check twice" trick I mentioned for CTL above. I deemed it too expensive and complicated for too little gain, at least for this version of the code.

 

           BYTEVAR ECS_KEY_LAST

SCAN_KBD    PROC

           ;; ------------------------------------------------------------ ;;
           ;;  Try to find CTRL and SHIFT first.                           ;;
           ;;  Shift takes priority over control.                          ;;
           ;; ------------------------------------------------------------ ;;
           MVII    #KBD_DECODE.no_mods, R3 ; neither shift nor ctrl

           ; maybe DIS here
           MVI     $F8,        R0
           ANDI    #$3F,       R0
           XORI    #$80,       R0          ; transpose scan mode
           MVO     R0,         $F8
           ; maybe EIS here

           MVII    #$7F,       R1          ; \_ drive column 7 to 0
           MVO     R1,         $FF         ; /
           MVI     $FE,        R2          ; \
           ANDI    #$40,       R2          ;  > look for a 0 in row 6
           BEQ     @@have_shift            ; /

           MVII    #$BF,       R1          ; \_ drive column 6 to 0
           MVO     R1,         $FF         ; /
           MVI     $FE,        R2          ; \
           ANDI    #$20,       R2          ;  > look for a 0 in row 5
           BNEQ    @@done_shift_ctrl       ; /

           MVII    #KBD_DECODE.control, R3
           B       @@done_shift_ctrl

@@have_shift:
           MVII    #KBD_DECODE.shifted, R3

@@done_shift_ctrl:

 

One quick note about the code above: When it sets up the I/O direction bits in $F8, it does a read-modify-write, preserving the lower 6 bits. This is generally fine. A problem could arise, though, if an interrupt happens during that sequence, and the interrupt handler modifies the PSG's channel enables. If your program has that characteristic, you either need to avoid calling this function where it can be interrupted, or add a DIS/EIS pair around the two places in the code that need it--the one above, and one near the end which you'll see soon. I've added comments above to highlight where I'm talking about.

 

Next up is the column-scanning loop along with the row decoder. At this point in the code, we have the PSG set up for $FF as output and $FE as input. R3 contains the pointer to our keyboard decoder table. So lets go!

 

           ;; ------------------------------------------------------------ ;;
           ;;  Start at col 7 and work our way to col 0.                   ;;
           ;; ------------------------------------------------------------ ;;
           CLRR    R2              ; col pointer
           MVII    #$FF7F, R1

@@col:      MVO     R1,     $FF
           MVI     $FE,    R0
           XORI    #$FF,   R0
           BNEQ    @@maybe_key

@@cont_col: ADDI    #6,     R2
           SLR     R1
           CMPI    #$FF,   R1
           BNEQ    @@col

           MVII    #KEY.NONE,  R0
           B       @@none

           ;; ------------------------------------------------------------ ;;
           ;;  Looks like a key is pressed.  Let's decode it.              ;;
           ;; ------------------------------------------------------------ ;;
@@maybe_key:
           MOVR    R2,     R4
           SARC    R0,     2
           BC      @@got_key       ; row 0
           BOV     @@got_key1      ; row 1
           ADDI    #2,     R4 
           SARC    R0,     2
           BC      @@got_key       ; row 2
           BOV     @@got_key1      ; row 3
           ADDI    #2,     R4 
           SARC    R0,     2
           BC      @@got_key       ; row 4
           BNOV    @@cont_col      ; row 5
@@got_key1: INCR    R4
@@got_key:
           ADDR    R3,     R4      ; add modifier offset
           [email protected]    R4,     R0

           CMPI    #KEY.NONE, R0   ; if invalid, keep scanning
           BEQ     @@cont_col

 

So how does this work? R1 contains the current "column strobe". Initially, it has bit 7 clear and all other bits set, including bits [15:8].

 

As I move through the columns, I shift this value right. The result is that port $FF goes through the values $7F, $BF, $DF, $EF, $F7, $FB, $FD, $FE. The register itself goes through the values $FF7F, $7FBF, $3FDF, $1FEF, $0FF7, $07FB, $03FD, $01FE, $00FF, which is why the loop termination looks for $FF in R1 to decide when to terminate.

 

R2 contains our "column offset", which is just our offset into the the decoder table. If I needed to conserve a register, I could probably eliminate this one and just update R3 directly as we move between columns, since we only scan once.

 

The code doesn't bother processing columns that look "empty", which helps speed up the scan. Furthermore, it stops scanning the moment it gets the first valid key. Note that CTL isn't considered "valid" by this loop, so if we hit it during the scan, we ignore it and keep scanning. As noted previously, the decoder table returns KEY.NONE for CTL.

 

The row decoder just shifts by pairs of bits, and branches out when it sees the first zero. The branch instruction comments indicate which row it's testing. I couldn't think of a faster way to look for the first 0 bit without using a large lookup table. R4 contains our "row plus column offset" into the decoder table once we exit the row decoder.

 

Notice that the row decoder can't see shift keys at all, because the code only looks at rows 0 - 5, as indicated above. That said, the branch labeled "row 5" only gets taken in the case where shift is pressed. I'll let you meditate on why...

 

And now for the very last part of the code: Filter out keystrokes we've already seen, so it only returns new keystrokes. Take the PSG out of scanning mode (ie. set both ports for input), and return whatever keystroke we got, if any.

 

           CMP     ECS_KEY_LAST, R0
@@none:     MVO     R0,         ECS_KEY_LAST
           BNEQ    @@new
           MVII    #KEY.NONE,  R0

@@new:      ; maybe DIS here
           MVI     $F8,        R1  ; \
           ANDI    #$3F,       R1  ;  > set both I/O ports to "input"
           MVO     R1,         $F8 ; /
           ; maybe EIS here
           JR      R5
           ENDP

 

And that's it!

 

For your convenience, I've written a very simple test program that accepts input and prints it to the screen. For control characters, I simply display them in red. Attached is the demo program, along with the scan_kbd.asm so you don't have to cut and paste it out of this post.

 

[NEXT UP: The Synthesizer Keyboard!]

 

[EDIT: AA munged up my post real nice, interpreting the less-than sign in my decoder table as the start of an XML tag or something. I've fixed it, I think, but to be safe I took the less-than sign out of the decoder table, replacing it with an underscore. Use the unadulterated ASM in the attachment for the definitive reference.]

kbd_test.zip

Edited by intvnut
  • Like 5

Share this post


Link to post
Share on other sites

Scanning the Synthesizer Keyboard

 

The synthesizer keyboard has 49 piano keys, covering a 4 octave range, from C to shining C. The keyboard matrix for the synthesizer keyboard is very simple, putting the lowest note in Row 0, Col 0, the next note in Row 0, Col 1, and so on all the way up the scale. Thus, you can compute note number directly as Row*8 + Col, which is rather convenient.

 

The synthesizer keyboard has a full complement of diodes. These solve all ghosting and masking issues, so that you can press any combination of keys with no restrictions, and the Intellivision can resolve them all. Unlike the alphanumeric keyboard, which can barely handle anything more than a single key at a time without a headache, the synthesizer keyboard is very capable and easy to work with. So what's the catch?

 

Well, because it's so versatile, you probably want more versatile software to read it. For the alphanumeric keyboard, returning key-down events and a generic key-up event is probably sufficient for most purposes. For the synthesizer, though, you want to keep track of every key that's pressed and released, and when.

 

Architecting the Software

 

To get the most out of the synthesizer keyboard, a good software driver needs to be able to do the following:

  • Keep track of what keys are pressed
  • Inform the program when a new key gets pressed
  • Inform the program when a pressed key gets released

In the worst case, someone could press or release all 49 keys on the synthesizer at once. This seems improbable, sure, but we'd like the software not to break even if it did happen. So, in any given call, we could generate up to 49 change events. We can't generate more than that, if we only look at each key once during a given scan.

 

What's the best way to handle this? One approach is to use an event queue. To absorb a huge, if improbable burst of events, you'd need a large queue -- large enough for 49 events in this case. You could go with a smaller queue and drop extra events, but what's the right size?

 

Another approach is to use callbacks. In this scheme, you register a function that gets called whenever a key changes state, and the driver calls it with each key change event. This way, you don't have to store all those events anywhere. That's the approach my driver takes.

 

Next, how do you detect what keys changed? The most straightforward approach (and the approach I've taken) is to store a bitmap of "pressed keys". That way, you can compare the new state against the previous state with a simple XOR, and know exactly what changed.

 

And Now, The Code!

 

Well, gee, that was quick. There was so much to think about when reading the other keyboard, it's surprising how little there is to say about the synthesizer keyboard. It's a dream to read and it behaves like an ideal matrix scan keyboard. So, let's just read it already.

 

Because there's an easy mapping from row/col to note number, this code has no decoder lookup table for that purpose. (There is another table that I'll discuss in a moment.) But, it does keep a table in RAM of what's pressed, as well as a couple vectors for the callback functions. Here's the bit that declares that and the initialization function you need to call to get it set up:

 

 

           WORDVAR     SYNDN
           WORDVAR     SYNUP
           BYTEARRAY   SYNSTAT,    7

;; ======================================================================== ;;
;;  INIT_SYN    Initialize the data structures used by SCAN_SYN             ;;
;; ======================================================================== ;;
INIT_SYN    PROC
           CLRR    R0
           MVII    #SYNSTAT,   R4
           [email protected]    R0,         R4
           [email protected]    R0,         R4
           [email protected]    R0,         R4
           [email protected]    R0,         R4
           NOP
           [email protected]    R0,         R4
           [email protected]    R0,         R4
           [email protected]    R0,         R4
           MVII    #@@jrr5,    R0
           MVO     R0,         SYNDN
           MVO     R0,         SYNUP
@@jrr5      JR      R5
           ENDP

 

Nothing too exciting. Just be sure you call it before you start calling SCAN_SYN. The SYNDN/SYNUP variables are where you put your callback pointers. By default, INIT_SYN sets SYNDN/SYNUP to a dummy callback, and all you need to is overwrite that.

 

The next part is also pretty straight-forward: Set the I/O ports for "normal scanning", and fall into the scanning loop:

 

;; ======================================================================== ;;
;;  SCAN_SYN    Actually scan the synthesizer, calling the key down and     ;;
;;              key up callbacks (SYNDN/SYNUP) as it detects changes.       ;;
;; ======================================================================== ;;
SCAN_SYN    PROC
           PSHR    R5

           CLRR    R0                  ; row offset; initially 0

           ; maybe DIS
           MVI     $F8,        R1      ; \
           ANDI    #$3F,       R1      ;  |_ turn IO ports on for scan.
           XORI    #$40,       R1      ;  |
           MVO     R1,         $F8     ; /
           ; maybe EIS

           MVII    #$FE,       R1      ; \_ initial row strobe
           MVO     R1,         $FE     ; /

           CMP     $FE,        R1      ; \_ short circuit if ECS unavail
           BNEQ    @@leave             ; /

 

The top of the row-loop is also pretty straight forward. It reads in the column information for the current row, and sees what bits changed. If none changed, it goes to the next row. The SYNSTAT array stores the previously-seen state of the synthesizer keyboard.

 

           MVII    #SYNSTAT,   R3

@@row_loop:
           MVI     $FF,        R1      ; Get new status of row
           XORI    #$FF,       R1      ; 1 means pressed; 0 means released

           MV[email protected]    R3,         R2      ; Get previous status of row
           [email protected]    R1,         R3      ; Store new status
           XORR    R1,         R2      ; What changed?
           BEQ     @@next_row          ; Nothing?  Go to the next row

 

This is where things start to get interesting. Once we see some changes, we need to send key-up and key-down events. So, the code next sees which it has, or both, sending key-up before key-down. (Sending key-up before key-down simplifies channel allocation, if you're allocating PSG channels to keys being pressed.)

 

           PSHR    R3
@@pr_deltas
           ANDR    R2,         R1      ; R1 contains "just pressed" vector
           XORR    R1,         R2      ; R2 contains "just released" vector

           BEQ     @@press_loop_init

 

The release loop walks through the "just released" bitmap, calling SYNUP for every key released. It uses a special lookup table (SYNDLTA) to quickly find the rightmost bit that's set, remove it, and report the bit number. The table is large -- 256 words -- but faster than the shifting approach the alphanumeric keyboard used. Also, shifting-by-two worked OK when the code didn't have to save where it was at and retsume processing. The dispatch structure, though, works better hen the state you have to save isn't so complex.

 

           ; Do "release" before "press", to simplify key-to-voice allocation

           PSHR    R1                  ; save "just pressed"

@@release_loop:

           ADDI    #SYNDLTA,   R2      ; \_ Find right-most set bit and 
           [email protected]    R2,         R2      ; /  remove it, returning its bit #

           MOVR    R2,         R1      ; \
           ANDI    #$FF,       R2      ;  |  Unpack:  MSB is bit #; LSB is
           XORR    R2,         R1      ;  |- bitfield minus rightmost 1 bit
           SWAP    R1                  ;  |  Bit # in R1, bitfield in R2
           ADDR    R0,         R1      ; /

           PSHR    R0                  ; \_ Save regs...
           PSHR    R2                  ; /

           MVII    #@@rls_rtn, R5      ; \_ Call syn-up
           MVI     SYNUP,      PC      ; /

@@rls_rtn:  PULR    R2
           PULR    R0

           TSTR    R2
           BNEQ    @@release_loop

           PULR    R1                  ; Restore "just pressed"

 

The press loop is very similar to the release loop, only it calls SYNDN:

 

@@press_loop_init:
           MOVR    R1,         R2
           BEQ     @@done_pr

@@press_loop:
           ADDI    #SYNDLTA,   R2      ; \_ Find right most set bit and 
           [email protected]    R2,         R2      ; /  remove it, returning its bit #

           MOVR    R2,         R1      ; \
           ANDI    #$FF,       R2      ;  |  Unpack:  MSB is bit #; LSB is
           XORR    R2,         R1      ;  |- bitfield minus rightmost 1 bit
           SWAP    R1                  ;  |  Bit # in R1, bitfield in R2
           ADDR    R0,         R1      ; /

           PSHR    R0                  ; \_ Save regs...
           PSHR    R2                  ; /

           MVII    #@@prs_rtn, R5      ; \_ Call syn-down
           MVI     SYNDN,      PC      ; /

@@prs_rtn:  PULR    R2
           PULR    R0

           TSTR    R2
           BNEQ    @@press_loop

@@done_pr:
           PULR    R3

 

And that brings us to the end of the row loop. This simply moves our row strobe to the next row and continues on. This code actually scans in 0..6 order, opposite of what the alphanumeric code did. The cleanup code after the loop just sets both ports back to "input" and returns. Because of the call-back structure, there's no return value.

 

@@next_row:
           INCR    R3                  ; Move to next state buffer entry
           ADDI    #8,         R0      ; Add 8 to row index

           MVI     $FE,        R1      ; \
           SETC                        ;  |_ Slide scan row over by 1.
           RLC     R1                  ;  |
           MVO     R1,         $FE     ; /

           CMPI    #$17F,      R1      ; Scanned 7 rows?
           BNEQ    @@row_loop          ; No:  Keep going

           ; maybe DIS
           MVI     $F8,        R1      ; \
           ANDI    #$3F,       R1      ;  |- Turn off output drivers.
           MVO     R1,         $F8     ; /
           ; maybe EIS

@@leave     PULR    PC
           ENDP

 

 

That brings us to the SYNDLTA array. The SYNDLTA array is 256 words long, indexed by the byte you want to operate on. The lower byte in each word returns the original number with its rightmost bit removed. The upper byte in each word gives the bit number of that rightmost bit. The following macros generate the table.

 

;; ======================================================================== ;;
;;  SYNDLTA     Decode table to speed finding the right-most set bit, as    ;;
;;              well as removing that bit.                                  ;;
;;                                                                          ;;
;;              MSB is bit #, LSB is new bitfield minus LSB.                ;;
;; ======================================================================== ;;

SYNDLTA     PROC
           LISTING "code"

@@b         QSET    $00

           REPEAT  256 / 4

           ; 00 case:  Go find actual right-most bit
@@nb        QSET    @@b AND (@@b - 1)
@@rmb       QSET    @@b XOR @@nb
@@num       QSET    0
           IF      @@rmb AND $F0
@@num       QSET    4
           ENDI
           IF      @@rmb AND $CC
@@num       QSET    2 + @@num
           ENDI
           IF      @@rmb AND $AA
@@num       QSET    1 + @@num
           ENDI
           DECLE   (@@num SHL  + @@nb

           ; 01 case: right most bit is bit 0
           DECLE   (0 SHL  + @@b

           ; 10 case: right most bit is bit 1
           DECLE   (1 SHL  + @@b

           ; 11 case: right most bit is bit 0, and bit 1 gets set
           DECLE   (0 SHL  + @@b + 2

@@b         QSET    @@b + 4
           ENDR

           LISTING "prev"
           ENDP

The four DECLE statements make use of the fact that 3 out of 4 numbers have their rightmost bit in position 0 or 1, so it's easy to compute those. Thus it only computes the right-most bit for every fourth number.

 

And that's the code.

 

As with the alphanumeric scanner, I've put together a very simple demo that shows key events. The attached zip file contains the demo along with the complete version of scan_syn.asm.

syn_test.zip

  • Like 4

Share this post


Link to post
Share on other sites

And One More Thing...

I also wrote a simple synthesizer that you can use to play music with the synth keyboard. The operation of the synth itself is obvious. If you want to transpose the keyboard up/down by octave, press [1]/[4] on the keypad. If you want to transpose the keyboard up/down by individual notes, press [2]/[5]. [3]/[6] adjust the volume. The code autodetects PAL vs. NTSC and will use different note tables for each.

It also shows a nice pic of what's going on, and what PSG channel each pressed key is assigned to:

simple_synth_0.gifsimple_synth_1.gif

 

EDIT 24-Oct-2015: It appears the actual Synth binary wasn't attached to this post! Here it is, source and all.

synth.zip

Edited by intvnut
  • Like 4

Share this post


Link to post
Share on other sites

Thanks for all the great information.

 

The code autodetects PAL vs. NTSC and will use different note tables for each.

 

I'd be interested in knowing how to detect the difference between PAL and NTSC too.

Share this post


Link to post
Share on other sites

I'd be interested in knowing how to detect the difference between PAL and NTSC too.

 

The best way Arnauld and I have come up with is to measure the time between interrupts with a delay loop. It's a bigger hammer, sure, but we haven't found a user-visible register or ROM location that looks different between the two.

 

Here's the code from synth.asm that does the detection. INIT_ISR is my generic initialization ISR. It sets the ISR to NTSC_PAL1 when it completes. NTSC_PAL1 then chains to NTSC_PAL2, which then compares the number of times a spin loop went around, and uses that to decide PAL vs. NTSC.

 

;; ======================================================================== ;;
;;  INIT_ISR    Clear up things, detect NTSC vs. PAL, and then fall into    ;;
;;              normal ISR duties.                                          ;;
;; ======================================================================== ;;
INIT_ISR    PROC

           ;; ------------------------------------------------------------ ;;
           ;;  Initialize the STIC and the STIC shadow                     ;;
           ;; ------------------------------------------------------------ ;;
           CLRR    R0
           CLRR    R5
           MVII    #STICSH,R4
           MVII    #24,    R1
@@loop1:
           [email protected]    R0,     R4          ; clear STIC shadow
           [email protected]    R0,     R5          ; clear STIC registers
           DECR    R1
           BNEQ    @@loop1

           MVO     R0,     $30         ; \
           MVO     R0,     $31         ;  |- border/scroll regs
           MVO     R0,     $32         ; /

           ;; ------------------------------------------------------------ ;;
           ;;  Unpack the synthesizer graphics into GRAM.                  ;;
           ;; ------------------------------------------------------------ ;;
           CALL    MEMUNPK
           DECLE   $3800, SYNGFX, SYNGFX.len

           ;; ------------------------------------------------------------ ;;
           ;;  Reset PSG channel enables to something sane.                ;;
           ;; ------------------------------------------------------------ ;;
           MVII    #$38,   R0
           MVO     R0,     $1F8
           MVO     R0,     $0F8

           ;; ------------------------------------------------------------ ;;
           ;;  Start PAL/NTSC autodetect.                                  ;;
           ;; ------------------------------------------------------------ ;;
           SETISR  NTSC_PAL1,  R0
           B       ISRRET
           ENDP


;; ======================================================================== ;;
;;  NTSC_PAL1   \_ Autodetect NTSC vs. PAL by counting cycles between       ;;
;;  NTSC_PAL2   /  interrupts.  NTSC is approx 14932; PAL is approx 20000.  ;;
;; ======================================================================== ;;
NTSC_PAL1   PROC
           SETISR  NTSC_PAL2,  R0
           EIS

           CLRR    R2
@@loop:     INCR    R2              ; \_ 15 cycles
           B       @@loop          ; /

           ENDP

NTSC_PAL2   PROC
           SUBI    #8,         SP

           ; NTSC should go around a little under 995 times;
           ; PAL should go around a little under 1333 times.
           ; Midpoint is 1164.  Above that, it's PAL, below is NTSC.
           CMPI    #1164,      R2
           BLT     @@ntsc

           MVII    #2,         R2
           B       @@common

@@ntsc      MVII    #1,         R2
@@common:   MVO     R2,         NTSC_PAL

           SETISR  ISR,        R0
           B       ISRRET
           ENDP

 

From what I remember, Arnauld does something similar, albeit with his own way of measuring time between interrupts. I think he just polls his VBlank Sync flag and counts how many times his polling loop iterated. I forget. Still, the basic idea is to measure time between interrupts.

  • Like 4

Share this post


Link to post
Share on other sites
The best way Arnauld and I have come up with is to measure the time between interrupts with a delay loop. It's a bigger hammer, sure, but we haven't found a user-visible register or ROM location that looks different between the two.

 

Its a shame that there are no differences in the EXEC ROM to simplify things.

Share this post


Link to post
Share on other sites

Joe, thank you for taking the time to write this valuable documentation. I should have a synthesizer keyboard soon and I'm excited to try your driver and example program on the real thing.

  • Like 1

Share this post


Link to post
Share on other sites
The best way Arnauld and I have come up with is to measure the time between interrupts with a delay loop. It's a bigger hammer, sure, but we haven't found a user-visible register or ROM location that looks different between the two.

 

Its a shame that there are no differences in the EXEC ROM to simplify things.

 

Now, I guess it's not 100% true there's no EXEC differences on some units. I don't think the Intellivision II or Sears Intellivisions were released outside the NTSC space. If that's indeed the case, then you could try to detect their EXECs and bypass the NTSC/PAL detect. But, if you have an Intellivision I, I think you're pretty much stuck, since they all seem to have the same EXEC and same GROM regardless of their refresh rate.

 

Ah well.

 

Another strategy you may be able to try is waiting for an interrupt, hitting $20 for active display, and then polling the GROM to see how long it's visible for. That takes less time than measuring interrupt-to-interrupt, but may be less reliable. For example, if you're running in an emulator that doesn't emulate the "bus isolation" mode for GROM reads, then this detection will fail. (I believe INTVPC falls into this category. GamePlyr.exe may also. I don't know.) And honestly, the savings are pretty miniscule. We're talking the difference between 1/60th of a second and 1/200th of a second, or something, at the very, very start of the game. Somehow I doubt anyone will notice the delay before the title screen.

Share this post


Link to post
Share on other sites

Does anyone have an electrical diagram showing which pins of the DB9 connectors goes to which rows/columns of the keyboard?

 

Thanks,

Tom

Share this post


Link to post
Share on other sites

Does anyone have an electrical diagram showing which pins of the DB9 connectors goes to which rows/columns of the keyboard?

 

Thanks,

Tom

 

Tom,

 

Here's all the information you need, but some assembly's required.

 

Start with this:

ecs_keyboard_matrix.png

 

 

Row 0 goes to pin 1, row 1 to pin 2, etc. on one controller port. Likewise for the columns on the other controller port. (I got this from my ECS cable documentation. I've noticed that others number the pins differently. I'm pretty sure I went with the standard numbering that comes on a Radio Shack DB-9 connector.)

 

As with the main console, port $FF is the left connector and $FE is the right connector. $FF / left connector connects to the columns. $FE / right connector connects to the rows.

Share this post


Link to post
Share on other sites

ECS cable documentation - hmmm, this sounds interesting. Is there a place I can find this? Or a schematic for the ECS?

 

I've noticed the DB9 pin numbering in console schematics differs from the current standard numbering. They show ground as pin 9 that is pin 5 on current connectors. I'm guessing they did -

1 3 5 7 9

2 4 6 8

instead of (current standard) -

1 2 3 4 5

6 7 8 9

Which do you think your numbering matches? If the latter, than row 4 should skip over pin 5 (ground) to pin 6. If no skip, then probably the first.

 

FYI - I've had an inquiry about making an USB adapter for these and why I'm asking about this.

 

Tom

Share this post


Link to post
Share on other sites

ECS cable documentation - hmmm, this sounds interesting. Is there a place I can find this? Or a schematic for the ECS?

 

I've noticed the DB9 pin numbering in console schematics differs from the current standard numbering. They show ground as pin 9 that is pin 5 on current connectors. I'm guessing they did -

1 3 5 7 9

2 4 6 8

instead of (current standard) -

1 2 3 4 5

6 7 8 9

Which do you think your numbering matches? If the latter, than row 4 should skip over pin 5 (ground) to pin 6. If no skip, then probably the first.

 

FYI - I've had an inquiry about making an USB adapter for these and why I'm asking about this.

 

Tom

 

Tom,

 

Looking inside the connectors on my naked ECS that just happens to be at arms length, I see the connectors are numbered internally in two rows like your second diagram. Pin 5 in that numbering definitely goes to ground.

 

So, I grabbed my ohm-meter... and the answer is "none of the above"?

 

Pin numbering, looking from above the ECS.

 1 2 3 4 5

  6 7 8 9


bit 3 => pin 1
bit 2 => pin 2
bit 1 => pin 3
bit 0 => pin 4
GND   => pin 5
bit 7 => pin 6
bit 6 => pin 7
bit 5 => pin 8
bit 4 => pin 9

 

I've attached some photos to show how the connector is numbered, and how pin 5 is GND.

 

As far as the "ECScable" goes -- that was a cable interface I made between my Intellivision and my PC's parallel port quite some time ago. It only ended up working on the one machine. Other machines' parallel ports didn't really work with it. You can read about it here:

 

http://spatula-city....mples/ecscable/

post-14113-0-13938000-1335737222_thumb.jpg

post-14113-0-91538500-1335737234_thumb.jpg

Edited by intvnut

Share this post


Link to post
Share on other sites

BTW, one followup: I did take a pic of my Intellicart cable, and its pin numbering does match the connector on the ECS. (Note that it reads right-to-left, to match the numbering that reads left-to-right on the ECS.) That suggests to me that the pin numbering I wrote down in the ECScable docs was mirror-imaged right to left from the actual. I'm pretty sure I used similar self-built connectors to the one Chad used for this Intellicart cable.

 

I can't actually find my old ECScable. It's lost in all my crap. I haven't used it in maybe 10 years or so, which was 2 or 3 moves ago. But, I never throw anything out, so maybe someday I'll find it.

post-14113-0-12114300-1335767927_thumb.jpg

Edited by intvnut

Share this post


Link to post
Share on other sites

Of course, I'd find my ECScable just minutes after posting that I have no idea where it could be. It too is numbered similarly to the Intellicart cable above.

 

I've attached an annotated image that matches what I continuity-tested out on the ECScable. I've also attached an image to show what the overall ECScable looked like. I forgot I took a female-to-female DB9 cable, cut it, and only used the RatShack hardware for the DB25 end.

post-14113-0-51330000-1335771296_thumb.jpg

post-14113-0-94691800-1335771321_thumb.jpg

  • Like 1

Share this post


Link to post
Share on other sites

Is there no need to debounce and filter the ECS keyboard signal prior to decoding?

Share this post


Link to post
Share on other sites

I realize this is a necro bump, but I realized I never answered this when I came back to review this thread the other night:

 

Is there no need to debounce and filter the ECS keyboard signal prior to decoding?

 

Not really. The most debouncing you need to do is to not scan again right away so you don't see a "down-up-down" transition. The scanning process is so slow that most of the "bouncing" will have gone away. And with the ECS keyboards, you don't have to wait for two switches to close to get a good reading, unlike the multi-layer mylar-sheet controllers that are trying to close two switches with each keypress.

Share this post


Link to post
Share on other sites

You "came back to review" a thread from two and a half years ago? Methinks someone has too much spare time on his hands... :rolling:

 

Also, not that anyone will ever bother, what with such a horrific keyboard, but we need an IntyBASIC version of this. Then maybe someone can use IntyBASIC to code up a cartridge with an actual BASIC on it, thereby a) making the ECS almost actually useful and b) completing a recursive loop of epic proportions.

Share this post


Link to post
Share on other sites

I realize this is a necro bump, but I realized I never answered this when I came back to review this thread the other night:

 

 

Not really. The most debouncing you need to do is to not scan again right away so you don't see a "down-up-down" transition. The scanning process is so slow that most of the "bouncing" will have gone away. And with the ECS keyboards, you don't have to wait for two switches to close to get a good reading, unlike the multi-layer mylar-sheet controllers that are trying to close two switches with each keypress.

 

Thanks for responding, better late than never. :)

 

 

You "came back to review" a thread from two and a half years ago? Methinks someone has too much spare time on his hands... :rolling:

 

Also, not that anyone will ever bother, what with such a horrific keyboard, but we need an IntyBASIC version of this. Then maybe someone can use IntyBASIC to code up a cartridge with an actual BASIC on it, thereby a) making the ECS almost actually useful and b) completing a recursive loop of epic proportions.

 

Personally, I am interested in order to make graphic adventures with a "parser interface," like the old Sierra Online games.

 

-dZ.

Share this post


Link to post
Share on other sites

You "came back to review" a thread from two and a half years ago? Methinks someone has too much spare time on his hands... :rolling:

 

Believe it or not, I had a good reason to come back to it. We've been trying to figure out why one of Lathe26's Intellivoices is b0rking the hand controllers, and I sent him a board to try things out with. Turns out it b0rks the ECS keyboard too, effectively pressing and holding the J key.

 

 

Also, not that anyone will ever bother, what with such a horrific keyboard, but we need an IntyBASIC version of this. Then maybe someone can use IntyBASIC to code up a cartridge with an actual BASIC on it, thereby a) making the ECS almost actually useful and b) completing a recursive loop of epic proportions.

 

Hmmm. Writing a BASIC interpreter using a compiled BASIC?

 

It'd probably be better than the double-interpreted TI-99/4A BASIC. (Its BASIC was written in a language called GPL, which was also interpreted.)

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...

  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...