intvnut Posted February 11, 2012 Share Posted February 11, 2012 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: Construct a grid of "row lines" and "column lines". Connect your keyboard switches so that each one connects one "row line" to one "column line" when closed. Connect an output port to the "rows", so that you can selectively drive one row at a time. 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.) 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. So far, so good. Now what happens when you also press B in addition to A and E? Let's scan Row 0: 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: 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: Now if we press A, B and E, everything works like you expect, because the diodes block the ghost paths: 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: 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.] 6 2 Quote Link to comment Share on other sites More sharing options...
intvnut Posted February 11, 2012 Author Share Posted February 11, 2012 (edited) 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: 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: 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 February 11, 2012 by intvnut 5 1 Quote Link to comment Share on other sites More sharing options...
intvnut Posted February 11, 2012 Author Share Posted February 11, 2012 (edited) 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 MVI@ 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 February 11, 2012 by intvnut 6 Quote Link to comment Share on other sites More sharing options...
intvnut Posted February 11, 2012 Author Share Posted February 11, 2012 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 MVO@ R0, R4 MVO@ R0, R4 MVO@ R0, R4 MVO@ R0, R4 NOP MVO@ R0, R4 MVO@ R0, R4 MVO@ 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 MVI@ R3, R2 ; Get previous status of row MVO@ 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 MVI@ 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 MVI@ 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 4 Quote Link to comment Share on other sites More sharing options...
intvnut Posted February 11, 2012 Author Share Posted February 11, 2012 (edited) 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: 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 October 25, 2015 by intvnut 4 Quote Link to comment Share on other sites More sharing options...
GroovyBee Posted February 11, 2012 Share Posted February 11, 2012 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. Quote Link to comment Share on other sites More sharing options...
intvnut Posted February 12, 2012 Author Share Posted February 12, 2012 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: MVO@ R0, R4 ; clear STIC shadow MVO@ 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. 4 Quote Link to comment Share on other sites More sharing options...
GroovyBee Posted February 12, 2012 Share Posted February 12, 2012 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. Quote Link to comment Share on other sites More sharing options...
Arnauld Posted February 16, 2012 Share Posted February 16, 2012 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. 2 Quote Link to comment Share on other sites More sharing options...
intvnut Posted February 16, 2012 Author Share Posted February 16, 2012 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. Quote Link to comment Share on other sites More sharing options...
dualcam Posted April 24, 2012 Share Posted April 24, 2012 Does anyone have an electrical diagram showing which pins of the DB9 connectors goes to which rows/columns of the keyboard? Thanks, Tom Quote Link to comment Share on other sites More sharing options...
intvnut Posted April 26, 2012 Author Share Posted April 26, 2012 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: 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. Quote Link to comment Share on other sites More sharing options...
dualcam Posted April 27, 2012 Share Posted April 27, 2012 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 Quote Link to comment Share on other sites More sharing options...
intvnut Posted April 29, 2012 Author Share Posted April 29, 2012 (edited) 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/ Edited April 29, 2012 by intvnut Quote Link to comment Share on other sites More sharing options...
intvnut Posted April 30, 2012 Author Share Posted April 30, 2012 (edited) 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. Edited April 30, 2012 by intvnut Quote Link to comment Share on other sites More sharing options...
intvnut Posted April 30, 2012 Author Share Posted April 30, 2012 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. 1 Quote Link to comment Share on other sites More sharing options...
dualcam Posted April 30, 2012 Share Posted April 30, 2012 Thanks!!! Tom Quote Link to comment Share on other sites More sharing options...
+DZ-Jay Posted May 15, 2013 Share Posted May 15, 2013 Is there no need to debounce and filter the ECS keyboard signal prior to decoding? Quote Link to comment Share on other sites More sharing options...
intvnut Posted December 19, 2014 Author Share Posted December 19, 2014 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. Quote Link to comment Share on other sites More sharing options...
freewheel Posted December 19, 2014 Share Posted December 19, 2014 You "came back to review" a thread from two and a half years ago? Methinks someone has too much spare time on his hands... 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. 1 Quote Link to comment Share on other sites More sharing options...
+DZ-Jay Posted December 19, 2014 Share Posted December 19, 2014 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... 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. 1 Quote Link to comment Share on other sites More sharing options...
intvnut Posted December 19, 2014 Author Share Posted December 19, 2014 You "came back to review" a thread from two and a half years ago? Methinks someone has too much spare time on his hands... 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.) Quote Link to comment Share on other sites More sharing options...
flickertail Posted July 17, 2020 Share Posted July 17, 2020 This is a good thread. It gave me a good understanding of the ECS keyboard and convinced me to add support for it to my project. 2 Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.