;; Note: this program is not an official release. The serial bus code ;; doesn't seem to work as a controller yet. See the end of this file ;; for compilation information and some known errors. ;; Copyright © 2001-2003 Marko Mäkelä (msmakela@nic.funet.fi) ;; ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 2 of the License, or ;; (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program; if not, write to the Free Software ;; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. ;;; This program code can be compiled with the GNU assembler (see last lines). ;;; This program code is based on revision 4 of c2n232.s. It has been ;;; restructured, and the command dispatcher has been rewritten as ;;; suggested by Wolfgang Moser (cbm (at) d81 (dot) de). ;;; This version adds support for the Commodore serial bus and for the ;;; cassette motor control signal. ;;; The circuit board versions prior to smd2.brd or dil2.brd have to be ;;; rewired in order to fully utilise this firmware. Most importantly, ;;; CASS MOTOR (from the previously unconnected cassette connector pin 3) ;;; should be connected like this: MOTOR ---4k7---PB4---4k7--- GND. ;;; Here, -4k7- denotes a 4.7 kilo-ohm resistor. Some further discussion ;;; follows the macro definition "pullup". ;;; Here is a summary of the differences between board versions: ;;; dil smd smdcr smd2 smd2 smdcr smd dil ;;; ----- ----- ----- ----- ____ ____ ----- ----- ----- ----- ;;; RESET 1. \/ 20 VCC ;;; RxD RxD RxD RxD PD0 2 19 PB7 CTS CTS - RTS ;;; TxD TxD TxD TxD PD1 3 18 PB6 RTS RTS - - ;;; XTAL1 4 AT 17 PB5 RxD RxD DATA DATA ;;; XTAL2 5 90S 16 PB4 MOTOR - - CTS ;;; ATN ATN - ATN PD2 6 2313 15 PB3 READ READ READ READ ;;; CLK CLK - CLK PD3 7 14 PB2 RESET - - - ;;; - CTS - DATA PD4 8 13 PB1 - - SRQ SRQ ;;; - RTS - SRQ PD5 9 12 PB0 SENSE SENSE SENSE SENSE ;;; GND 10 11 PD6 WRITE WRITE WRITE WRITE ;;; ---------- ;;; Legend: ;;; dil: single-sided circuit board designed for solder-through components ;;; smd: first circuit board version for surface-mounted components; ;;; equipped with holes for mounting a programming cable ;;; (20 of these were made by Marko Mäkelä) ;;; smdcr: cost-reduced board version for surface-mounted components; ;;; the microcontroller is programmed via the RS-232 interface ;;; (110 of these were made by Marko Mäkelä) ;;; smd2,dil2: new board version that supports the serial bus extension ;;; The serial bus code is work in progress. Thanks to Wolfgang Moser and ;;; Juha Kouri (jtkouri (at) ktverkko.fi) for measurements with memory ;;; oscilloscopes. ;;; Future possibilities: fastloader support, with buffered RS-232 output: ;;; - Commodore burst mode transfers (1570, 1571, 1581, C128, modified C64) ;;; - the burst transfer clock SRQ could trigger OVF1 interrupts ;;; (timer 1 initialised to $ffff, counting PD5 pulses) ;;; - find out if the stream can be interrupted between bytes ;;; (as the 126-byte RS-232 output buffer may become the bottleneck) ;;; - CMD JiffyDOS protocol ;;; Revision history: ;;; 5 Initial release (based on revision 4 of c2n232.s) .equ REVISION, 5 ; adjust this to downgrade .iflt REVISION - 5 .err ; revisions 1 through 4 are in c2n232.s .endif ;;; Modes of operation: ;;; Idle mode (0) ;;; - entered upon RESET ;;; - entered upon receiving BREAK or NUL or an unknown command character ;;; - the communication buffers are cleared ;;; - the cassette lines READ and SENSE are set to the HIGH state ('1') ;;; - the serial bus is released ;;; Pulse width measurement mode (1) ;;; - entered upon receiving ctrl-a ;;; - the controller will clock the high-to-low transitions on WRITE ;;; and report them over RS-232 as 8-bit unsigned values, ;;; the unit being 8 microseconds ;;; - there is no flow control; the RS-232 device must be fast enough ;;; SAVE operation (2) ;;; - entered upon receiving ctrl-b SHORT MEDIUM LONG ;;; - three parameters: maximum pulse widths; unit=8 microseconds ;;; - the pulses are converted to characters on the RS-232 line: ;;; - A=pause (pulse width > LONG) ;;; - B=short (pulse width <= SHORT) ;;; - C=medium (SHORT < pulse width <= MEDIUM) ;;; - D=long (MEDIUM < pulse width <= LONG) ;;; - there is no flow control; the RS-232 device must be fast enough ;;; LOAD operation (3) ;;; - entered upon receiving ctrl-c PAUSE SHORT MEDIUM LONG ;;; - four parameters: half-pulse widths; unit=8 microseconds ;;; - received characters A..D from RS-232 indicate pulses with 50% duty cycle: ;;; - A=00=pause (pulse width = 2 * PAUSE) ;;; - B=01=short (pulse width = 2 * SHORT) ;;; - C=10=medium (pulse width = 2 * MEDIUM) ;;; - D=11=long (pulse width = 2 * LONG) ;;; - the characters E..P specify asymmetric pulses ;;; - these are treated as 4-bit numbers, e.g. 'J'-'A' = 9 = 1001 in binary ;;; - the LSB (01=short for 'J') specify the width of the LOW level ;;; - the MSB (10=medium for 'J') specify the width of the HIGH level ;;; - XON/XOFF flow control is used during input ;;; - NUL will enter idle mode once the data has been relayed ;;; send operation (4) ;;; - entered upon receiving ctrl-d LENGTH ;;; - one 16-bit parameter (little-endian): number of data bytes - 1 ;;; - after transceiving LENGTH+1 characters, an acknowledgement '@' is sent ;;; and the idle mode is entered ;;; - XON/XOFF flow control is used during input ;;; - the only way to abort this command is to send the BREAK signal ;;; receive operation (5) ;;; - entered upon receiving ctrl-e LENGTH ;;; - one 16-bit parameter (little-endian): number of data bytes - 1 ;;; - after transceiving LENGTH+1 characters, the controller enters idle mode ;;; - there is no flow control; the RS-232 device must be fast enough ;;; receive operation with calibration (6) ;;; - entered upon receiving ctrl-f LENGTH ;;; - one 16-bit parameter (little-endian): number of data bytes - 1 ;;; - the first byte received from READ must be 0xf0; this will calibrate ;;; the expected pulse widths ;;; - after transceiving LENGTH+1 characters, the controller enters idle mode ;;; - there is no flow control; the RS-232 device must be fast enough ;;; TAP operation (7) ;;; - entered upon receiving ctrl-g ;;; - no parameters ;;; - received characters from RS-232 indicate pulses with 50% duty cycle: ;;; - n=1..255 = n * 8 microseconds ;;; - NUL will enter idle mode once the data has been relayed ;;; - XON/XOFF flow control is used during input ;;; packed half-pulse LOAD operation (8) ;;; - entered upon receiving ctrl-h WIDTH00 WIDTH01 WIDTH10 WIDTH11 ;;; - four parameters: half-pulse widths; unit=8 microseconds ;;; - each character received from RS-232 contains four half-pulses ;;; - the least significant bit pairs of the character are sent first ;;; - XON/XOFF flow control is used during input ;;; - at the end of stream, '0' is sent and idle mode is entered automatically ;;; version identification (9) ;;; - entered upon receiving ctrl-i ;;; - the response is a version code, starting from 'a' ;;; - after the response is sent, the idle mode (0) is entered ;;; pay attention to the serial bus (10) ;;; - entered upon receiving ctrl-j DEV0..30 ;;; - one 32-bit parameter (little-endian): bitmask of device numbers ;;; - in the received data, literal $00 is replaced by $00 $00 ;;; - the response is one of the following: ;;; - $00 $01, ATN bytes, $00 $03 (when the device should TALK) ;;; - $00 $01, ATN bytes, $00 $02, bytes received, $00 $04 ;;; - there is no flow control; the RS-232 device must be fast enough ;;; receive data from the serial bus (11) ;;; - entered upon receiving ctrl-k ;;; - no parameters ;;; - in the received data, literal $00 is replaced by $00 $00 ;;; - the response is one of the following: ;;; - $00 $01, ATN bytes, $00 $03 (when the device should TALK) ;;; - $00 $01, ATN bytes, $00 $02, bytes received, $00 $04 ;;; - there is no flow control; the RS-232 device must be fast enough ;;; send data to the serial bus (12) ;;; - entered upon receiving ctrl-l DELAY ;;; - one 8-bit parameter: bit delay (1..256 microseconds) ;;; - all following bytes except $00 are literal data bytes ;;; - commands: $00 $00 = send a $00 byte ;;; $00 $01 = send next byte with EOI (acknowledged after send) ;;; $00 $02 = start an ATN transmission (no acknowledgement) ;;; $00 $03 = end ATN after next byte (ack'd) ;;; $00 $04 = talk-attention turn around after next byte (ack'd) ;;; $00 $0a = enter attend mode (acknowledged with ':') ;;; $00 $0b = enter listen mode (acknowledged with ';') ;;; $00 xx = enter idle mode (acknowledged with '0') ;;; - the acknowledgement or return status is one of the following: ;;; - $00: successful completion ;;; - $21 xx: device not present after xx bytes ;;; - $22 xx: end-of-frame handshake timeout after xx bytes ;;; - $23 xx: device not present after xx bytes sent under ATN ;;; - $30: entering idle mode ;;; - $3a: entering attend mode ;;; - $3b: entering listen mode ;;; - $80..$ff: interrupted by ATN after sending fxx bytes (mod 128) ;; initialize I/O port B ;; - CTS: (PB6) output '1' ;; - SENSE: input (open-collector output '0' when driven by DDRB) ;; - READ: (PB3) output '1' ;; - MOTOR: (PB4) input .macro ioinitb ldi 16, 0x48 ; output '1' on PB6 and PB3 out PORTB, 16 out DDRB, 16 .endm ;; enable or disable the pull-up resistor on CASS MOTOR .macro pullup op \op PORTB, 4 ; comment this line out if needed; see below .endm ;; Enabling the internal 35 to 120 kilo-ohm pull-up resistor works ;; with both the original board and with a board equipped with a ;; resistor divider MOTOR ---4k7---PB4---4k7--- GND. However, thus ;; modified boards will consume 3 to 6 mW of power from the cassette ;; motor power supply during emulated tape operations. The power ;; is consumed for roughly 1 µs per pulse character, which last for ;; at least 260 µs at 38400 bps. So, the worst case added power ;; consumption is 1 µs / 260 µs * 6 mW = 23 µW. ;; If you comment out the body of the "pullup" macro, the pull-up ;; resistor will remain disabled all the time. In this case, the ;; firmware will not work on the original hardware, but you can save ;; power by using bigger resistors, e.g., ;; MOTOR ---47k---PB4---100k--- GND or ;; MOTOR ---10k---PB4---|<--- GND, ;; where ---|<--- denotes a 0.6 to 5 volt zener diode. ;; Thanks to Wolfgang Moser and Gianmario Scotti for assistance ;; in calculations and measurements. ;; CTS control .macro cts op \op PORTB, 6 .endm ;; CASS MOTOR control .macro motor op \op PINB, 4 .endm ;; wait for UART Data Register Empty .macro waitdre sbis USR, 5 rjmp .-4 ; wait for UART Data Register Empty (DRE) .endm .equ SREG, 0x3F ; Status Register .equ SPL, 0x3D ; Stack Pointer Low .equ GIMSK, 0x3B ; General Interrupt MaSK register .equ GIFR, 0x3A ; General Interrupt Flag Register .equ TIMSK, 0x39 ; Timer/Counter Interrupt MaSK register .equ TIFR, 0x38 ; Timer/Counter Interrupt Flag register .equ MCUCR, 0x35 ; MCU general Control Register .equ TCCR0, 0x33 ; Timer/Counter 0 Control Register .equ TCNT0, 0x32 ; Timer/Counter 0 (8-bit) .equ TCCR1A, 0x2F ; Timer/Counter 1 Control Register A .equ TCCR1B, 0x2E ; Timer/Counter 1 Control Register B .equ TCNT1H, 0x2D ; Timer/Counter 1 High Byte .equ TCNT1L, 0x2C ; Timer/Counter 1 Low Byte .equ OCR1AH, 0x2B ; Output Compare Register 1 High Byte .equ OCR1AL, 0x2A ; Output Compare Register 1 Low Byte .equ ICR1H, 0x25 ; T/C 1 Input Capture Register High Byte .equ ICR1L, 0x24 ; T/C 1 Input Capture Register Low Byte .equ WDTCR, 0x21 ; Watchdog Timer Control Register .equ EEAR, 0x1E ; EEPROM Address Register .equ EEDR, 0x1D ; EEPROM Data Register .equ EECR, 0x1C ; EEPROM Control Register .equ PORTB, 0x18 ; Data Register, Port B .equ DDRB, 0x17 ; Data Direction Register, Port B .equ PINB, 0x16 ; Input Pins, Port B .equ PORTD, 0x12 ; Data Register, Port D .equ DDRD, 0x11 ; Data Direction Register, Port D .equ PIND, 0x10 ; Input Pins, Port D .equ UDR, 0x0C ; UART I/O Data Register .equ USR, 0x0B ; UART Status Register .equ UCR, 0x0A ; UART Control Register .equ UBRR, 0x09 ; UART Baud Rate Register .equ ACSR, 0x08 ; Analog Comparator Control and Status Register .equ BAUDRATE, 12 ; 38400 bps at 8 MHz ;; shared registers .equiv PARAM0, 0 ; first parameter .equiv PARAM1, 1 ; second parameter .equiv PARAM2, 2 ; third parameter .equiv PARAM3, 3 ; fourth parameter .equiv PFLO, 2 ; recv: least significant byte of pulse fall .equiv PFHI, 4 ; recv: most significant byte of pulse fall .equiv PCMP, 7 ; recv: binary search variable .equiv PSMAX, 8 ; recv: binary search maximum .equiv PSCNT, 8 ; send/sends/attend/recvs: bit count .equiv PDATA, 9 ; recv/send/sends/attend/recvs: data octet .equiv SENDCNT, 2 ; sends: number of octets sent .equiv PWIDTH, 4 ; load: pulse width being output .equiv PTAP, 7 ; load/tap/loadp (TCCR1B.7=0): transfer mode ; bit 0=end of stream ; bits 2,1=00 (load) ; 01 (TAP playback) ; 10 (packed load (low)) ; 11 (packed load (high)) .equiv SENDST, PTAP ; sends (TCCR1B.7=1, PTAP.2=1): transfer mode ; 4=normal bytes, ; or talk-attention turn around after ATN ; 5=EOI on next byte ; 6=sending under ATN ; 7=stop sending under ATN after next byte ; 12=ignore data bytes .equiv ATNST, PTAP ; attend: listener ATN status ; 0=no ATN (normal byte) ; 1=receiving first ATN byte ; 2=receiving subsequent ATN byte ; 4=sending data ; >64=receiving TALK request ; 255=EOI ;; exclusively reserved registers .equiv PMIN, 5 ; minimum pulse width (recv) .equiv PDIFF, 6 ; maximum-minimum (recv) .equiv DEV00, 10 ; attend: device numbers to listen to (0..7) .equiv DEV08, 11 ; attend: device numbers to listen to (8..15) .equiv DEV10, 12 ; attend: device numbers to listen to (16..23) .equiv DEV18, 13 ; attend: device numbers to listen to (24..30) ;; mode selection for the UART Receive Complete interrupt .equiv NPARAM, 15 ; number of parameter bytes to receive ; NPARAM.7 is set in the custom send mode .equiv GOTNUL, 15 ; sends, or attend (ZL=rxign): 1=got $00 byte ;; 16 and ZH are reserved for interrupts ;; 17..27 are currently unused ;; ZL is for receiving UART communications .equiv YL, 28 ; transmit pointer .equiv YH, 29 ; constantly zero (when interrupts enabled) .equiv ZL, 30 ; receive buffer pointer .equiv ZH, 31 ; temporary register used in interrupts ;; RAM map rxign = 32 ; magic address for ignoring incoming data rxbuf = 0x60 rxbufe = rxbuf + 124 stack = rxbufe stacke = stack + 4 .equ N_INTS, 11 ; number of entries in the interrupt table .equ N_CMDS, 13 ; number of commands .text ;; interrupt vector table (starts at zero, N_INTS entries) zero: rjmp reset rjmp int0 ; INT0 (external interrupt 0, serial ATN) rjmp int1 ; INT1 (external interrupt 1, serial CLK) rjmp icp1 ; ICP1 (input capture 1, cassette WRITE) rjmp oc1 ; OC1 (output compare 1, cassette READ) rjmp error ; OVF1 (timer 1 overflow) rjmp ovf0 ; OVF0 (timer 0 overflow) rjmp rxc ; RXC (UART receive complete) rjmp error ; DRE (UART data register empty) rjmp error ; TXC (UART transmit complete) rjmp error ; ACI (analog comparator) cmdtab: ; table of commands ;; number of parameters (N_CMDS entries) .byte 0, 0, 3, 4, 2, 2, 2, 0, 4, 0, 4, 0, 1 ;; The .if/.err/.endif lines cause problems on older versions of ;; the GNU assembler - comment out the lines if needed. .ifne . - cmdtab - N_CMDS .err ; wrong number of entries .endif .if N_CMDS & 1 .byte 0xff ; padding to force the rest at even addresses .endif .ifgt . - zero - 256 .err ; start of cmdtab exceeds 8-bit word addresses .endif ;; command dispatch table (N_CMDS entries) rjmp idle ; command 0: go idle rjmp pulse ; command 1: pulse stream capture rjmp save ; command 2: SAVE operation rjmp load ; command 3: LOAD operation rjmp send ; command 4: custom send mode rjmp recv ; command 5: custom receive mode rjmp recvcal ; command 6: custom receive mode, calibrate rjmp tap ; command 7: pulse stream playback rjmp loadp ; command 8: packed load operation rjmp version ; command 9: display firmware version rjmp attend ; command 10: pay attention to serial bus rjmp recvs ; command 11: receive data from serial bus rjmp sends ; command 12: send to serial bus .ifne cmdtab - zero - N_INTS * 2 .err ; cmdtab has been moved! .endif .ifne . - cmdtab - (N_CMDS & 1) - (N_CMDS * 3) .err ; cmdtab size mismatch .endif .ifgt . - zero - 512 .err ; end of cmdtab exceeds 8-bit word addresses .endif ;;; start of "main program area" ;;; This area is mostly entered via the command dispatch table, above. ;;; The routines must initialise ZL and enable interrupts. Most functions ;;; set up one or more interrupt handlers and enter the idle loop. ;; error: go to the RESET sequence error: ;; RESET handler reset: ldi 16, stacke-1 out SPL, 16 ; restore the stack pointer ldi 16, 0x80 out ACSR, 16 ; disable the analog comparator (saves power) ;; initialize the UART ldi 16, BAUDRATE out UBRR, 16 ;; enable idle mode sleep; INT1 on rising CLK and INT0 on falling ATN ldi 16, 0x2e out MCUCR, 16 clr ZL clr YH ;; initialize the I/O ports (PORTD is always zero) ioinitb clr PMIN clr PDIFF clr NPARAM ; no data to receive ;; fall through to idle mode on RESET ;; command 0: idle mode idle: out TIMSK, YH ; disable ICP and OC interrupts out GIMSK, YH ; disable external interrupts out DDRD, YH ; release the serial bus cbi DDRB, 0 ; SENSE=high idle_common0: clr ZL ; do not expect pulse characters idle_common: ldi 16, 0x98 out UCR, 16 ; enable RX and TX; enable RXC interrupts cts sbis rjmp waitirq ; jump if receiving was previously enabled ldi 16, 0x11 ; DC1, ctrl-q, permission to send waitdre out UDR, 16 cts cbi ; drop CTS (start receiving) waitirq: sei ; enable interrupts ;; idle loop sleep ; wait for interrupt rjmp .-4 ;; command 1: pulse stream capture pulse: ldi 16, 0xff mov PARAM2, 16 ; longest pulse width clr ZL ; await no data clr PARAM0 ; select pulse stream capture mode for icp1 cpse PARAM0, PARAM0 ; skip the first instruction of save ;; command 2: save data (quantised pulse stream capture) save: rcall waitparams ; wait for the parameters out OCR1AH, YH ; set the output compare value out OCR1AL, PARAM2 ; long pulse width sbi DDRB, 0 ; SENSE=low ldi 16, 0x48 out TIFR, 16 ; clear pending ICP and OC interrupts out TIMSK, 16 ; enable ICP and OC interrupts ldi 16, 0xcb ; activate ICP on rising edge, CK/64 out TCCR1B, 16 rjmp idle_common ; go to idle loop ;; command 7: pulse stream playback tap: ldi ZH, 2 mov PTAP, ZH ; PTAP.0=0 (no EOF), PTAP.1=1 (TAP playback) rjmp loadt ;; command 8: load data consisting of half-pulses loadp: ldi ZH, 4 mov PTAP, ZH ; PTAP.2=1 (packed load operation) ;; command 3: load data consisting of full pulses load: rcall waitparams ; wait for the parameters loadt: out OCR1AH, YH ; set the output compare value out OCR1AL, YH ; (16 bits) ldi ZL, rxbuf ; load the buffer pointers mov YL, ZL sbi DDRB, 0 ; SENSE=low ldi 16, 0x40 out TIFR, 16 ; clear pending OC interrupts out TIMSK, 16 ; enable OC interrupts rjmp idle_common ; go to idle loop ;; command 4: send data to the cassette interface send: rcall waitparams ldi 16, 0x02 ; activate ICP on falling edge, no CTC1, CK/8 out TCCR1B, 16 com NPARAM ; set NPARAM.7, a flag for rxc interrupt mov PSCNT, NPARAM ; negative bit count (start sending a new byte) ldi ZL, rxbuf ; load the buffer pointers mov YL, ZL rjmp idle_common ;; command 6: receive with calibration (read an 0xf0 byte first) recvcal: clr PMIN clr PDIFF ;; fall through ;; command 5: receive data from the cassette interface recv: rcall waitparams ; wait for the parameters ser 16 out OCR1AH, 16 ; set the output compare value out OCR1AL, 16 ; (16 bits, 0xffff) ldi 16, 0x08 out TIFR, 16 ; clear pending ICP interrupts out TIMSK, 16 ; enable ICP interrupts ldi 16, 0x09 ; activate ICP on falling edge, CK/1 out TCCR1B, 16 sei ; enable interrupts (reduce icp1 latency) cbi PORTB, 3 ; drop READ to initiate transfer rjmp idle_common ;; command 9: display firmware version version: ldi 16, 0x60+REVISION waitdre out UDR, 16 rjmp idle ;; command 10: pay attention to the serial bus attend: ldi ZL, DEV00 ; receive the parameters to the DEV registers rcall waitparams2 ldi 16, 0xc0 out GIFR, 16 ; acknowledge external interrupts (ATN, CLK) attending: ldi 16, 0x40 ; enable INT0 interrupts (ATN) attending2: out GIMSK, 16 out TCCR0, YH ; stop timer/counter 0 out TIMSK, YH ; disable timer/counter 0 overflow interrupt clr ATNST ; prepare to receive normal bytes clr PSCNT ; between bytes rjmp idle_common ;; command 11: receive data from the serial bus recvs: clr ZL ldi 16, 0xc0 out GIFR, 16 ; acknowledge external interrupts (ATN, CLK) rjmp attending2 ; enable INT0 and INT1 interrupts (ATN, CLK) ;; command 12: send to the serial bus (not interrupt-driven) sends: rcall waitparams ldi ZL, rxbuf ; load the buffer pointers mov YL, ZL clr GOTNUL ; clear GOTNUL status ldi 16, 0x80 ; set (TCCR1B & 0x87) for appropriate behaviour out TCCR1B, 16 ; on empty reception buffer in rxc_noconv ldi 16, 0x40 out GIMSK, 16 ; enable INT0 interrupts (ATN) sends_main: ldi 16, 4 ; transfer mode: normal bytes sends_again: mov SENDST, 16 ; set the transfer mode clr SENDCNT ; clear the byte counter sends_getch: sei ; enable interrupts ;;; The branch of the UART rxc interrupt handler that is taken during this ;;; mode of operation consumes approximately 55 clock cycles in worst case, ;;; or 7 microseconds. At 38400 bits per second, a character can arrive ;;; at most once every 260 microseconds, and due to a hardware buffer of one ;;; character, at most two UART interrupts can occur within a time frame of ;;; 260 microseconds. The Commodore serial bus timing is critical when the ;;; DATA pin is polled with interrupts enabled. As the minimum signal ;;; duration is about 20 microseconds, there is enough time to handle the ;;; two UART interrupts (7 microseconds each) while polling a single signal ;;; transition. sends_getdata: cpse YL, ZL ; buffer empty? rjmp sends_go sleep ; wait for UART rxc (Receive Complete) rjmp sends_getdata ; wait for data sends_go: cli ; disable interrupts (protect SREG and R16) ld PDATA, Y+ ; load a data byte cpi YL, rxbufe brne sends_nowrap ldi YL, rxbuf ; wrap the buffer pointer sends_nowrap: mov 16, YL ; dequeuing pointer sub 16, ZL ; enqueuing pointer dec 16 sbrc 16, 7 subi 16, rxbuf-rxbufe ; reduce the difference modulo the buffer cpi 16, (rxbufe-rxbuf)*3/4 brne sends_nodc1 ; buffer not 25% full cts sbis rjmp sends_nodc1 ; send at most one DC1 in a row ldi 16, 0x11 ; DC1, ctrl-q, permission to send out UDR, 16 cts cbi ; drop CTS (start receiving) sends_nodc1: cpse GOTNUL, YH ; got an escape code ($00) previously? rjmp sends_esc ; yes, process escaped data cpse PDATA, YH ; got an escape code ($00) now? rjmp sends_start ; no, process normal byte inc GOTNUL ; got escape ($00): rjmp sends_getch ; set the escape flag and get next byte sends_esc: ;; the previous byte was an escape ($00) clr GOTNUL ; clear the escape character status tst PDATA breq sends_start ; $00 $00 represents a literal $00 ;; got an escape code ($00 xx) mov 16, PDATA cpi 16, 5 ; got a command 1..4? brlo sends_modesel ; yes, set the mode cpi 16, 10 ; got $00 $0a? breq sends_end ; yes, switch to attend mode cpi 16, 11 breq sends_end ; got $00 $0b, go to listen mode clr 16 ; else go to idle mode sends_end: rjmp rxc_cmdok_params ; switch modes (no parameters) sends_modesel: ori 16, 4 ; set the mode rjmp sends_again ; set SENDST, clear SENDCNT and get next byte sends_start: sbrc SENDST, 3 rjmp sends_getch ; SENDST=12: ignore data bytes sbic DDRD, 3 rjmp sendsrfd ; already holding CLK low from last byte ;; handshake before sending the first byte sbrs SENDST, 1 ; SENDST=6 or SENDST=7: sending under ATN? rjmp sends_start_noatn ; SENDST=4 or SENDST=5: sending non-ATN data out GIMSK, YH ; disable INT0 interrupts ldi 16, 0x0c out DDRD, 16 ; drop ATN and CLK, release DATA ;; wait 1000 µs, then expect DATA=0 ldi 16, 3 ldi ZH, 256-125 rcall sendsdelay1 ; 125*64=8000 clock cycles=1000 µs sbis PIND, 4 rjmp sendsrfd ; got DATA=0 ldi 16, 0x23 rjmp sendserr ; device not present during ATN ;; Subroutine: wait for DATA=0 for 1000 µs ;; On timeout, report error code from R16 and do not return ;; Interrupts disabled on entry, enabled on exit sends_data1000: ldi ZH, 3 out TCCR0, ZH ; start timer/counter 0 at CK/64 out TCNT0, YH ; clear timer/counter 0 sends0: sei ; enable interrupts while waiting sbis PIND, 4 ; DATA=0? ret ; got DATA=0, return cli ; disable interrupts again (protect R16 and ZH) in ZH, TCNT0 cpi ZH, 125 ; 125*64=8000 clock cycles=1000 µs brlo sends0 ; wait 1000 µs for DATA=0 pop ZH ; discard the return address pop ZH ; discard the return address ;; Clean up and report error code from R16 ;; Interrupts disabled sendserr: out DDRD, YH ; release the bus (CLK and ATN) out TCCR0, YH ; stop timer/counter 0 ldi ZH, 0x40 out GIMSK, ZH ; enable INT0 interrupts (ATN) ldi ZH, 0xc0 out GIFR, ZH ; acknowledge external interrupts out UDR, 16 ; send error code in r16 waitdre out UDR, SENDCNT ; report the number of characters sent ldi 16, 12 ; SENDST=12: ignore data bytes until command rjmp sends_again ; set SENDST, clear SENDCNT and get next byte sends_start_noatn: sbi DDRD, 3 ; drop CLK ldi 16, 0x21 ; timeout error code: device not present rcall sends_data1000 ; wait until DATA=0 and enable interrupts sendsrfd: ; listener ready for data? sei ; enable interrupts (disabled at sends_nodc1) out TCCR0, YH ; stop timer/counter 0 cbi DDRD, 3 ; raise CLK sbis PIND, 4 ; wait for DATA=1 rjmp .-4 sbrc SENDST, 1 ; check if EOI handshake is needed rjmp sendsbits sbrs SENDST, 0 rjmp sendsbits ;; perform EOI handshake (SENDST=5) sbic PIND, 4 rjmp .-4 ; wait for DATA=0 sbis PIND, 4 rjmp .-4 ; wait for DATA=1 cli rcall sendsdelay ; extra delay after EOI (needed on VIC-20) sendsbits: ; send a bit cli ; protect r16 and SREG ldi 16, 8 mov PSCNT, 16 ; prepare to send 8 bits sbi DDRD, 3 ; drop CLK sendsbit: rcall sendsdelay ; delay for the bit set-up time sbrs PDATA, 0 sbi DDRD, 4 ; drop DATA if needed lsr PDATA cbi DDRD, 3 ; raise CLK rcall sendsdelay ; delay for the bit valid time sbi DDRD, 3 ; drop CLK cbi DDRD, 4 ; raise DATA dec PSCNT brne sendsbit ;; byte sent: wait for frame handshake ldi 16, 0x22 ; timeout error code: framing error rcall sends_data1000 ; wait until DATA=0 and enable interrupts out TCCR0, YH ; stop timer/counter 0 inc SENDCNT ; bump the byte count sbrs SENDST, 1 rjmp sendsfr45 ; SENDST=4 or SENDST=5 sbrs SENDST, 0 ; SENDST=6 or SENDST=7 rjmp sends_getch ; SENDST=6: get next byte ; SENDST=7: raise ATN (release the bus) cli ; disable interrupts (protect R16) ldi 16, 0xc0 rjmp sendsfrlr sendsfr45: sbrs SENDST, 0 ; SENDST=4 or SENDST=5 rjmp sendsfr4 ; SENDST=4 ;; SENDST=5: wait for last frame release (EOI) sbic PIND, 4 rjmp .-4 ; wait for DATA=1 cli ; disable interrupts (protect R16) ldi 16, 0x80 ; acknowledge INT1 (CLK) but not INT0 (ATN) sendsfrlr: out DDRD, YH ; release the bus out UDR, YH ; report successful status out GIFR, 16 ; acknowledge external interrupts ldi 16, 0x40 out GIMSK, 16 ; enable INT0 interrupts (ATN) rjmp sends_main ; go to main loop sendsfr4: sbis DDRD, 2 ; sending under ATN? rjmp sends_getch ; no, get next byte ;; SENDST=4, sending under ATN: talk-attention turn around cli ; disable interrupts (protect R16) sbi DDRD, 4 ; drop DATA cbi DDRD, 2 ; raise ATN cbi DDRD, 3 ; raise CLK ldi 16, 0xc0 out GIFR, 16 ; acknowledge external interrupts ldi 16, 0x40 out GIMSK, 16 ; enable INT0 interrupts (ATN) sei sbic PIND, 3 rjmp .-4 ; wait for CLK=0 rjmp sends_main ; go to main loop ;;; end of "main program area" ;;; start of subroutines ;; Subroutine: wait for parameters to a command. ;; When this subroutine is called, no other interrupts than ;; UART RxC (Receive Complete) should be running. Thus, there ;; is just enough space in the 4-byte stack for the return addresses of ;; the subroutine call and the interrupt handler. ;; The other case where the stack can contain two return addresses is ;; when the UART RxC interrupt handler is interrupted by the ICP1 ;; handler for the custom send mode. waitparams: ldi ZL, PARAM0 ; receive the parameters to registers waitparams2: sei ; enable interrupts sleep ; wait for interrupt (UART RxC) cli ; disable interrupts tst NPARAM brne waitparams2 ; wait until all parameters have been received ret ;; Subroutine: wait for PARAM0 microseconds ;; and disable interrupts. sendsdelay: mov ZH, PARAM0 com ZH ldi 16, 2 sendsdelay1: ; alternative entry point out TCCR0, 16 ; start timer/counter 0 at CK/8 out TCNT0, ZH ; set timer/counter 0 to 255-PARAM0 out TIFR, 16 ; clear timer/counter 0 overflow interrupt out TIMSK, 16 ; enable timer/counter 0 overflow interrupt rjmp waitirq ; wait for interrupt ;; the interrupt handler (ovf0) will return to the calling routine ;; Subroutine: consume enqueued characters (entry at sendsconsume) sendsconsume1: ld PDATA, Y+ ; load a data byte cpi YL, rxbufe brne sendsconsume_nowrap ldi YL, rxbuf ; wrap the buffer pointer sendsconsume_nowrap: cpse GOTNUL, YH ; got an escape code ($00) previously? rjmp sendsconsume_esc ; yes, process escaped data cpse PDATA, YH ; got an escape code ($00) now? rjmp sendsconsume ; no, process normal byte sendsconsume_esc: com GOTNUL ; got escape ($00): toggle the escape flag sendsconsume: cpse YL, ZL rjmp sendsconsume1 ;; everything consumed: normalise GOTNUL to 0..1 ;; and send DC1 if sender was stopped ldi ZH, 1 and GOTNUL, ZH cts sbis ret ldi ZH, 0x11 ; DC1, ctrl-q, permission to send out UDR, ZH cts cbi ; drop CTS (start receiving) ret ;;; end of subroutines ;;; start of interrupt handlers ;; Interrupt handler: timer/counter 0 overflow. ;; Return to the caller of sendsdelay, with interrupts disabled. ovf0: out TCCR0, YH ; stop the timer out TIMSK, YH ; disable timer/counter 0 overflow interrupt ldi 16, stacke-3 out SPL, 16 ; discard the return address ret ; return to the caller of sendsdelay, with I=0 ;; Interrupt handler: UART receive complete. ;; This is one of the longest interrupt handlers. ;; - reading commands (when NPARAM=0 and ZL=rxbuf ;; - reading data for the cassette load emulation or for the serial bus rxc_data: sbrc PTAP, 2 rjmp rxc_noconv ; PTAP.2 set: packed load operation sbrc PTAP, 0 rjmp rxc_error ; end of transfer: accept no more data cpse 16, YH ; NUL: enter idle mode at end of stream rjmp rxc_noend inc PTAP ; set bit 0 of PTAP to signal end of stream cpse YL, ZL reti ; buffer not yet empty: return clr 16 rjmp rxc_cmdok ; buffer empty: send '0' and go idle rxc_noend: sbrc PTAP, 1 rjmp rxc_noconv ; PTAP.1 set: TAP playback (no conversion) subi 16, 0x41 brlo rxc_error ; not a pulse character (smaller than 'A') cpi 16, 0x10 brsh rxc_error ; not a pulse character (greater than 'P') cpi 16, 0x04 brsh rxc_noconv ;; compatibility mode ('A'..'D') mov YH, 16 ; copy bits 0..1 to bits 2..3 lsl YH lsl YH or 16, YH clr YH rxc_noconv: cpse YL, ZL rjmp rxc_enqueue ;; the reception buffer was empty in ZH, TCCR1B andi ZH, 0x87 ; is the timer running or serial bus sending? brne rxc_enqueue ; yes, do not start output compare interrupts ;; start the timer for pulse stream output ldi ZH, 0x80 out TCCR1A, ZH ; clear OC1 on compare match ldi ZH, 0x0b out TCCR1B, ZH ; count pulses of CK/64, clear on compare match clr ZH out OCR1AH, ZH out OCR1AL, PARAM0 ; time base for pause rjmp rxc_enqueue ;; Part of interrupt handler: UART receive complete, NPARAM.7 clear. ;; - reading commands (when NPARAM=0 and ZL receive it tst PMIN brne icp1recvmax ;; got the minimum pulse width in PARAM3 mov PMIN, PARAM3 ; store the minimum pulse width rjmp icp1recvdone icp1recvd: ;; quantize a data pulse to a nibble (in ZL) ldi ZL, 0xf0 dec PARAM3 ; allow some jitter sub PARAM3, PMIN brlo icp1recvn ldi ZH, 16 ; number of alternatives mov PCMP, PDIFF ; indirectly set the maximum value (PSMAX) clr PARAM2 ; minimum value ;; match the pulse in a binary search icp1recvdl: dec PCMP mov PSMAX, PCMP ; alter the upper bound icp1recvdb: lsr ZH breq icp1recvn ; finished the search add PCMP, PARAM2 ror PCMP ; PCMP = (PSMAX + PARAM2) / 2 cp PARAM3, PCMP brlo icp1recvdl ;; at least that much: add the decision value add ZL, ZH inc PCMP mov PARAM2, PCMP ; alter the lower bound mov PCMP, PSMAX rjmp icp1recvdb ;; received a nibble in ZL (0xf0..0xff) icp1recvn: tst PDATA brne icp1recvb mov PDATA, ZL rjmp icp1recvdone icp1recvb: swap ZL and PDATA, ZL waitdre out UDR, PDATA ; output the data clr PDATA sec sbc PARAM0, PDATA ; decrement the number of bytes to receive sbc PARAM1, PDATA brcs icp1finish ; exit if no more bytes to receive icp1recvdone: clr ZL cbi PORTB, 3 ; drop READ to initiate next transfer ldi 16, 0x09 out TCCR1B, 16 ; activate ICP on falling edge, CK/1 reti ;; finished sending all bytes icp1finishsend: ldi ZL, 0x40 ; send '@' as an "end of transfer" flag out UDR, ZL ;; finished receiving or sending all bytes icp1finish: sbi PORTB, 3 ; raise READ clr PDATA clr NPARAM ldi ZL, stacke-1 out SPL, ZL ; restore the stack pointer clr ZL clr YH out TCCR1A, YH ; timer 1 disconnected from OC1 output out TCCR1B, YH ; timer 1 stopped out TCNT1H, YH ; clear timer 1 counter out TCNT1L, YH ; (16 bits) rjmp idle ;; Interrupt handler: input capture 1 (cassette write line) ;; - custom send function (TCCR1B.3 clear) ;; - custom receive function (TCCR1B.7 clear, TCCR1B.3 set) ;; - pulse capture ("pulse" or "save" operation, TCCR1B.7 and .3 set) icp1: in PARAM3, TCCR1B ; PARAM3 is otherwise unused during these ops sbrc PARAM3, 3 rjmp icp1nosend ;; input capture (custom send function) sbrc PSCNT, 7 rjmp icp1send0 ; set up next data byte (PSCNT was negative) sbi PORTB, 3 ; raise READ (1+2+2+2=7 cycles after request) ldi YH, 0x40 eor PARAM3, YH out TCCR1B, PARAM3 ; trigger on the opposite edge of WRITE clr YH dec PSCNT ; decrement the bit count brmi icp1send8 ; all done (all bits sent) sbrs PDATA, 0 ; read the data bit cbi PORTB, 3 ; lower READ (7+9 cycles after req) lsr PDATA ; shift the data register reti ; return from interrupt ;; all bits sent: decrement and compare the byte counter icp1send8: sec sbc PARAM0, YH ; decrement the number of bytes to send sbc PARAM1, YH brcs icp1finishsend ; all bytes sent: switch to idle mode reti ;; prepare for sending a byte icp1send0: cpse YL, ZL ; buffer empty? rjmp icp1send_nonempty out TIMSK, YH ; no more data -> disable input capture reti icp1send_nonempty: ld PDATA, Y+ ; load a data byte cpi YL, rxbufe brne icp1send_nowrap ldi YL, rxbuf ; wrap the buffer pointer icp1send_nowrap: mov YH, YL ; dequeuing pointer sub YH, ZL ; enqueuing pointer dec YH sbrc YH, 7 subi YH, rxbuf-rxbufe ; reduce the difference modulo the buffer cpi YH, (rxbufe-rxbuf)*3/4 brlo icp1send_nodc1 ; buffer more than 25% full cts sbis rjmp icp1send_nodc1 ; exit if receiving was previously enabled ldi YH, 0x11 ; DC1, ctrl-q, permission to send out UDR, YH cts cbi ; drop CTS (start receiving) icp1send_nodc1: cbi PORTB, 3 ; lower READ to initiate the transfer ldi YH, 0x42 ; activate ICP on rising edge, no CTC1, CK/8 out TCCR1B, YH ldi YH, 8 mov PSCNT, YH ; initialize the bit counter clr YH reti ;; Part of interrupt handler: input capture 1, TCCR1B.3 set ;; - custom receive function (TCCR1B.7 clear) ;; - pulse capture ("pulse" or "save" operation, TCCR1B.7 set) icp1nosend: sbrs PARAM3, 7 rjmp icp1recv out TCNT1H, YH ; clear timer 1 counter out TCNT1L, YH ; (16 bits) ldi 16, 0xcb ; activate ICP on rising edge, ICNC, CK/64 out TCCR1B, 16 in 16, ICR1L ; read the input tst PARAM0 breq icp1pw ; raw pulse stream capture ;; save mode (quantize the pulse widths) mov ZH, 16 ldi 16, 0x41 ; use signals 'A','B','C','D' tst ZH breq icp1pw ; pause detected inc 16 cp ZH, PARAM0 brlo icp1pw ; short pulse inc 16 cp ZH, PARAM1 brlo icp1pw ; medium pulse inc 16 ; long pulse ;; fall through icp1pw: waitdre out UDR, 16 ; send out the pulse width reti ;; Interrupt handler: Output Compare 1 ;; - pulse stream playback (TCCR1A.7 set) ;; - stop the timer (TCCR1A.7 clear) oc1: in 16, TCCR1A sbrs 16, 7 rjmp oc1_stop ; stop the timer and exit the interrupt ;; get next pulse for tape load operation sbrs 16, 6 rjmp oc1_next ldi 16, 0x80 out TCCR1A, 16 ; lower OC1 on the following interrupt sbrc PTAP, 2 rjmp oc1_pload ; PTAP.2 set: packed load operation sbrc PTAP, 1 rjmp oc1_tap ; PTAP.1 set: TAP playback mov YH, ZL ; back up ZL mov ZL, PWIDTH ; load the next pulse oc1_load_shifted: lsr ZL lsr ZL ; get bits 2..3 of the current pulse oc1_load_out: ld 16, Z ; look up the pulse width from PARAM0..PARAM3 mov ZL, YH ; restore ZL clr YH ; restore YH out OCR1AH, YH out OCR1AL, 16 ; write the timer reti oc1_pload: ; packed load (4 half-pulses per byte) ldi YH, 2 eor PTAP, YH ; toggle PTAP.1, the nibble counter mov YH, ZL ; back up ZL mov ZL, PWIDTH swap PWIDTH ; swap the high and low nibble andi ZL, 12 rjmp oc1_load_shifted oc1_tap: ; raw pulse stream playback out OCR1AH, YH out OCR1AL, PWIDTH reti ;; Part of interrupt handler: Output Compare 1, TCCR1A.7, !TCCR1A.6 ;; - get next pulse oc1_next: ldi 16, 6 cpse PTAP, 16 rjmp oc1_dequeue ;; packed load operation, play back next half-pulse ldi 16, 0xc0 out TCCR1A, 16 ; raise OC1 on the following interrupt mov YH, ZL ; back up ZL mov ZL, PWIDTH andi ZL, 3 rjmp oc1_load_out ; output the pulse oc1_dequeue: pullup sbi ; enable the pull-up resistor on MOTOR ldi 16, 0xff motor sbis ; cassette motor running? rjmp oc1_dequeued ; no, send a bogus pulse to keep timer running cpse YL, ZL ; buffer empty? rjmp oc1_nonempty pullup cbi ; disable the pull-up resistor on MOTOR clr 16 sbrs PTAP, 2 ; end of packed load stream: go idle sbrc PTAP, 0 ; got NUL byte in rxc_data? rjmp rxc_cmdok ; end of stream: send '0' and go idle rjmp oc1_stop ; no more data -> stop the timer oc1_nonempty: ldi 16, 0xc0 out TCCR1A, 16 ; raise OC1 on the following interrupt ld 16, Y+ ; load the next pulse oc1_dequeued: pullup cbi ; disable the pull-up resistor on MOTOR mov PWIDTH, 16 sbrc PTAP, 1 rjmp oc1_noconv ; TAP operation: no pulse width conversion andi 16, 3 ; get bits 0..1 mov YH, ZL ; convert the pulse mov ZL, 16 ld 16, Z ; look up the pulse width from PARAM0..PARAM3 mov ZL, YH clr YH oc1_noconv: out OCR1AH, YH out OCR1AL, 16 ; write the timer cpi YL, rxbufe brne oc1_next_nowrap ldi YL, rxbuf ; wrap the buffer pointer oc1_next_nowrap: mov ZH, YL ; dequeuing pointer sub ZH, ZL ; enqueuing pointer dec ZH sbrc ZH, 7 subi ZH, rxbuf-rxbufe ; reduce the difference modulo the buffer cpi ZH, (rxbufe-rxbuf)*3/4 brlo oc1_ret ; buffer more than 25% full cts sbis reti ; exit if receiving was previously enabled ldi 16, 0x11 ; DC1, ctrl-q, permission to send out UDR, 16 cts cbi ; drop CTS (start receiving) oc1_ret: reti ;; Part of interrupt handler: Output Compare 1, TCCR1A.7 clear ;; - stop the timer and exit the interrupt oc1_stop: in 16, TCCR1B andi 16, 0xf8 out TCCR1B, 16 reti ;; Interrupt handler: INT0 (falling ATN on serial bus, for receiving) int0: ldi 16, 0x10 out DDRD, 16 ; drop DATA, release other lines ldi 16, 0xc0 ; acknowledge INT1 (CLK) [and INT0 (ATN)] out GIFR, 16 out GIMSK, 16 ; enable INT0 and INT1 ldi 16, stacke-1 out SPL, 16 ; restore the stack pointer ldi 16, 1 cpi ZL, rxbuf brsh int0sends ; was a "sends" operation interrupted by ATN? tst PSCNT breq int0ok ; awaiting any data bits? ;; got ATN during something else: send $00 $01 clr PSCNT ; not awaiting any data bits (yet) waitdre out UDR, YH ; send 00 waitdre out UDR, 16 ; send 01 int0ok: mov ATNST, 16 ; ATN status=1 (receiving first ATN byte) rjmp int1done ; return to main loop int0sends: rcall sendsconsume ; consume all enqueued characters ldi ZL, rxign clr PSCNT ; between bytes mov ZH, SENDCNT ori ZH, 0x80 waitdre out UDR, ZH rjmp int0ok ;; Interrupt handler: INT1 (rising CLK on serial bus, for receiving) int1: ldi 16, stacke-1 out SPL, 16 ; restore the stack pointer sbis DDRD, 4 ; are we holding DATA low? rjmp int1recv ; no, receive a bit ;; between bytes: raise DATA when ready for data sbis PIND, 2 rjmp int1atn ;; ATN=high: watch previous ATN status tst ATNST breq int1atn ; already receiving normal bytes sbrc ATNST, 6 rjmp int1ta ; got TALK request: talk-attention turn around clr ATNST ; set the status to "normal byte" ;; ATN=high for the first time: send $00 $02 status waitdre out UDR, YH ; send 00 ldi 16, 2 waitdre out UDR, 16 ; send 02 int1atn: cbi DDRD, 4 ; raise DATA ldi 16, 8 mov PSCNT, 16 ; await 8 bits cpse ATNST, YH rjmp waitirq ; no EOI handshake for ATN bytes ;; wait 200 µs for CLK=0 (EOI handshake?) ldi 16, 3 out TCCR0, 16 ; start timer/counter 0 at CK/64 out TCNT0, YH ; clear timer/counter 0 int1eoi0: sei ; enable interrupts while waiting sbis PIND, 3 ; CLK=0? rjmp int1done ; got CLK=0, return to main loop cli ; disable interrupts again in 16, TCNT0 cpi 16, 25 ; 25*64=1600 clock cycles=200 µs brlo int1eoi0 ; wait 200 µs for CLK=0 ;; listener EOI handshake sbi DDRD, 4 ; drop DATA ldi 16, 2 out TCCR0, 16 ; start timer/counter 0 at CK/8 out TCNT0, YH ; clear timer/counter 0 int1eoi1: in 16, TCNT0 cpi 16, 80 brlo int1eoi1 ; wait 80 µs cbi DDRD, 4 ; raise DATA dec ATNST ; set the receiver status (EOI handshake) int1done: out TCCR0, YH ; disable timer/counter 0 rjmp waitirq ;; respond to talk-attention turn around int1ta: cbi DDRD, 4 ; raise DATA sbi DDRD, 3 ; drop CLK ldi 16, 3 rjmp int1stop ; send $00 $03 response ;; receive a data bit int1recv: clc sbic PIND, 4 ; read the data bit into Carry flag sec ror PDATA dec PSCNT ; frame complete? brne int1done ; return to main loop if not ;; frame complete: drop DATA or disable CLK interrupts dec ATNST brne int1n1st ;; received first ATN byte => check if it is for this node mov 16, PDATA andi 16, 31 ; get the device number cpi 16, 31 breq int1me ; every device listens to number 31 mov YH, ZL ; back up ZL mov ZL, 16 ; convert the device number to bitmask byte lsr ZL lsr ZL lsr ZL clr ZH subi ZL, 256 - DEV00 ; there is no "addi" opcode ld 16, Z ; load the bitmask byte mov ZL, YH ; restore ZL clr YH ; restore YH mov ZH, PDATA andi ZH, 7 ; get the bit address int1f: lsr 16 ; shift the addressed bit to the carry flag dec ZH brpl int1f brcc int1ignore ; bit clear, ignore and wait for next ATN int1me: inc ATNST ; set the status to "subsequent ATN byte" int1n1st: ; not 1st ATN byte tst ATNST ; test ATNST-1 brmi int1echo ; receiving non-ATN bytes? mov 16, PDATA cpi 16, 0x5f ; UNTALK request? brne int1nut ; no ldi 16, 1 mov ATNST, 16 ; set the status to "subsequent ATN byte" ;; fall through (the brsh branch will be taken) int1nut: ; not an UNTALK request brsh int1echo ; not a TALK request ldi 16, 0x40 cp PDATA, 16 brlo int1echo ; not a TALK request or ATNST, 16 ; set the talk-attention turn around flag int1echo: ; echo the character received sbi DDRD, 4 ; drop DATA (frame handshaking) waitdre out UDR, PDATA ; echo the received data ldi 16, 3 out TCCR0, 16 ; start timer/counter 0 at CK/64 out TCNT0, YH ; clear timer/counter 0 tst PDATA brne int1echoed waitdre out UDR, PDATA ; escape $00 data as $00 $00 int1echoed: inc ATNST ; restore ATNST brpl int1done ; not at end of transmission ;; EOI handshake at end of frame int1eoi2: ; busy waiting with interrupts disabled in 16, TCNT0 cpi 16, 8 ; 64 µs (> 60 µs) brlo int1eoi2 ; wait until the time has elapsed cbi DDRD, 4 ; raise DATA out TCCR0, YH ; stop timer/counter 0 ldi 16, 4 ; send $00 $04 (EOI) int1stop: ; send $00 r16 and stop monitoring the CLK line waitdre out UDR, YH waitdre out UDR, 16 int1ignore: clr ATNST ; between bytes rjmp attending ; disable INT1 interrupts (CLK), wait for ATN ;;; end of interrupt handlers ;;; errors in the new "sends" function ;;; - sending UNTALK (C-l SPC C-@ C-b ?) when nothing connected hangs ;;; - BREAK is the only way out ;;; - sending TALK command to drive fails ;;; - device not present (^A^@) or framing error (^B^@) ;;; - test case: reading drive error channel ;;; *LISTN *SECOND *UNLSN *TALK *TKSA => read status channel ;;; - C-l 2 C-@ C-b (ÿ?H C-@ C-d o C-@ C-k ;;; fastest reliably working "sends" timing: ;;; - PAL vic-20: 26 µs (C-l C-z) ;;; - 1541: 50 µs (C-l 2) ; The code can be compiled with GNU Assembler, dated 2003-05-12. ; Local variables: ; compile-command: "avr-as c2n232i.s && objcopy -O ihex a.out && cisp -c c2n232 /dev/ttyS0 -e -l a.out -v a.out" ; End: