/* * usbcas.c * * Emulator for the ABC80/800 cassette port * Based in part on USBtoSerial.c from the LUFA library * * Copyright (C) 2021 H. Peter Anvin */ /* LUFA Library Copyright (C) Dean Camera, 2021. dean [at] fourwalledcubicle [dot] com www.lufa-lib.org */ /* Copyright 2021 Dean Camera (dean [at] fourwalledcubicle [dot] com) Permission to use, copy, modify, distribute, and sell this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and that both that the copyright notice and this permission notice and warranty disclaimer appear in supporting documentation, and that the name of the author not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. The author disclaims all warranties with regard to this software, including all implied warranties of merchantability and fitness. In no event shall the author be liable for any special, indirect or consequential damages or any damages whatsoever resulting from loss of use, data or profits, whether in an action of contract, negligence or other tortious action, arising out of or in connection with the use or performance of this software. */ #include "usbcas.h" /* * Input and output ring buffers */ static RingBuffer_t u2c_ringbuf; /* USB -> CAS */ static uint8_t u2c_buffer[512]; static RingBuffer_t c2u_ringbuf; /* CAS -> USB */ static uint8_t c2u_buffer[128]; /** * LUFA CDC Class driver interface configuration and state * information. This structure is passed to all CDC Class driver * functions, so that multiple instances of the same class within a * device can be differentiated from one another. * * "vcpif" stands for "Virtual COM Port Interface". */ USB_ClassInfo_CDC_Device_t vcpif = { .Config = { .ControlInterfaceNumber = INTERFACE_ID_CDC_CCI, .DataINEndpoint = { .Address = CDC_TX_EPADDR, .Size = CDC_TXRX_EPSIZE, .Banks = 1, }, .DataOUTEndpoint = { .Address = CDC_RX_EPADDR, .Size = CDC_TXRX_EPSIZE, .Banks = 1, }, .NotificationEndpoint = { .Address = CDC_NOTIFICATION_EPADDR, .Size = CDC_NOTIFICATION_EPSIZE, .Banks = 1, }, }, }; /* * Motor relay sense. The motor relay is connected to PD1/INT1#, but * the interrupt isn't being used for that purpose. Rather, it is used * to latch an upward flank as on ABC800 this will have the output data * fed back to it. Thus, we can check if *either* it is grounded *or* * there has been a flank. * * Set up timer 3 to wrap around at 100 Hz, use this to poll and * debounce/deglitch the relay, and to delay the sensing of the * motor relay switch with ~160 ms. */ static void motor_sense_init(void) { /* * Set up INT1# to latch a rising edge, but mask the resulting * interrupt request. */ EIMSK = 0; /* all external interrupts masked */ EICRA = 3 << ISC10; /* rising edge */ EIFR = -1; /* clear all external interrupt flags */ /* * Set up timer 3 to count at fosc/8 in CTC mode, * with wraparound at 20000 = 100 Hz. */ TCCR3B = 0; /* Disable timer */ TCCR3A = 0; /* No capture, no waveforms */ ICR3 = (F_CPU/(8*100)) - 1; /* Top of counter */ TCNT3 = 0; TIMSK3 = TIFR3 = (1 << ICF3); /* Interrupt on ICR3 match */ TCCR3B = (3 << WGM32)|(2 << CS30); /* fosc/8, CTC mode, TOP=ICR3 */ } bool _motor_relay; static inline ATTR_ALWAYS_INLINE void motor_led_on(void) { //PORTB &= ~(1 << 0); } static inline ATTR_ALWAYS_INLINE void motor_led_off(void) { //PORTB |= (1 << 0); } ISR(TIMER3_CAPT_vect) { static uint8_t relay_ctr; if (!(PIND & 1) || (EIFR & 2)) { /* Relay active */ EIFR = 2; /* Clear edge detect */ if (relay_ctr < 16) { relay_ctr++; } else if (!_motor_relay) { _motor_relay = true; motor_led_on(); fmtx_enable(); } } else { /* Relay not active */ if (relay_ctr) { relay_ctr--; } else { _motor_relay = false; motor_led_off(); } } } /* * Probe for modem control status bits and send them if changed, * or if forced. DSR is always set, DCD indicates if the cassette * motor relay is active. */ static void update_modem_status(USB_ClassInfo_CDC_Device_t * const cii, bool forced) { uint16_t lines = 0; if (motor_relay()) /* Cassette relay active */ lines |= CDC_CONTROL_LINE_IN_DCD | CDC_CONTROL_LINE_IN_DSR; if (forced || cii->State.ControlLineStates.DeviceToHost != lines) { cii->State.ControlLineStates.DeviceToHost = lines; CDC_Device_SendControlLineStateChange(cii); } } /* * This is called in the main loop, but also any time we are busy-waiting * on something. */ static void usb_run_stack(void) { /* Update modem flags if needed */ update_modem_status(&vcpif, false); CDC_Device_USBTask(&vcpif); USB_USBTask(); } /* * Try to send the next byte of data to the host, abort without * dequeuing if there is an error. Returns true on error. */ static bool usb_send_next_byte(void) { uint8_t byte = RingBuffer_Peek(&c2u_ringbuf); if (CDC_Device_SendByte(&vcpif, byte) != ENDPOINT_READYWAIT_NoError) return true; /* * Dequeue the already sent byte from the buffer now we have * confirmed that no transmission error occurred */ RingBuffer_Remove(&c2u_ringbuf); return false; } static void usb_recv_data(void) { int byte; uint8_t nbytes = CDC_TXRX_EPSIZE - 1; /* Max bytes to receive */ while (nbytes--) { /* * Only try to read in bytes from the CDC interface if * the transmit buffer is not full */ if (RingBuffer_IsFull(&u2c_ringbuf)) return; byte = CDC_Device_ReceiveByte(&vcpif); if (byte < 0) return; /* No data */ /* * Store received byte into the transmit buffer, then * enable the transmit ISR if disabled */ RingBuffer_Insert(&u2c_ringbuf, byte); fmtx_enable(); } } static void usb_send_data(void) { uint16_t outbytes = RingBuffer_GetCount(&c2u_ringbuf); if (!outbytes) return; Endpoint_SelectEndpoint(vcpif.Config.DataINEndpoint.Address); /* * Check if a packet is already enqueued to * the host - if so, we shouldn't try to send * more data until it completes as there is a * chance nothing is listening and a lengthy * timeout could occur */ if (!Endpoint_IsINReady()) return; /* * Never send more than one bank size * less one byte to the host at a * time, so that we don't block while * a Zero Length Packet (ZLP) to * terminate the transfer is sent if * the host isn't listening */ outbytes = MIN(outbytes, (CDC_TXRX_EPSIZE - 1)); /* * Transfer output bytes to the USB * endpoint. Abort without dequeuing if * there is an error. */ while (outbytes--) if (usb_send_next_byte()) break; } void do_usb_tasks(void) { usb_recv_data(); usb_send_data(); usb_run_stack(); } /* * Main program entry point. This routine contains the overall program * flow, including initial setup of * all components and the main program * loop. */ int main(void) { SetupHardware(); RingBuffer_InitBuffer(&u2c_ringbuf, u2c_buffer, sizeof(u2c_buffer)); RingBuffer_InitBuffer(&c2u_ringbuf, c2u_buffer, sizeof(c2u_buffer)); GlobalInterruptEnable(); while (1) { do_usb_tasks(); } } static uint32_t current_baudrate; /** Configures the board hardware and chip peripherals for the demo's functionality. */ void SetupHardware(void) { /* Disable watchdog if enabled by bootloader/fuses */ MCUSR &= ~(1 << WDRF); wdt_disable(); /* Disable clock division */ clock_prescale_set(clock_div_1); /* * Configure ports. * NC: not connected, but have pads on board. Input/pullup. * NT: not terminated on board. Output/GND. */ /* * PORT B: * PB0 - RXLED# * PB1 - NC * PB2 - NC * PB3 - NC * PB4 - NC * PB5 - NC * PB6 - NC * PB7 - NT */ DDRB = 0x81; PORTB = 0x7f; /* * PORT C: * PC6 - DIN pin 3 (ABC800 data out) * PC7 - NP */ DDRC = 0x80; PORTC = 0x40; /* * PORT D: * PD0 - motor sense unused (GND) * PD1 - motor sense# * PD2 - NC * PD3 - data out * PD4 - data in (main) * PD5 - TXLED# * PD6 - NT * PD7 - NC */ DDRD = 0x68; PORTD = 0x93; /* * PORT E: * PE6 - NC * PE2 - hardware bootloader, tied to GND on board */ DDRE = 0x00; PORTE = 0x40; /* * PORT F: * PF0 - NT * PF1 - NT * PF4 - NC * PF5 - NC * PF6 - NC * PF7 - DIN pin 4 (ABC800 motor sense#) */ DDRF = 0x03; PORTF = 0xf0; /* Hardware Initialization */ USB_Init(); /* Initialize motor sense setup */ motor_sense_init(); /* Initialize receiver and transmitter */ fmrx_init(); current_baudrate = fmtx_real_baudrate(CAS_BAUDRATE_ABC80); fmrx_set_speed(current_baudrate); fmtx_init_speed(current_baudrate); } /** Event handler for the library USB Connection event. */ void EVENT_USB_Device_Connect(void) { } /** Event handler for the library USB Disconnection event. */ void EVENT_USB_Device_Disconnect(void) { } /** Event handler for the library USB Configuration Changed event. */ void EVENT_USB_Device_ConfigurationChanged(void) { bool success = true; success &= CDC_Device_ConfigureEndpoints(&vcpif); if (success) update_modem_status(&vcpif, true); } /** Event handler for the library USB Control Request reception event. */ void EVENT_USB_Device_ControlRequest(void) { CDC_Device_ProcessControlRequest(&vcpif); } /* * Called from the demodulation ISR to push a new byte into the buffer */ void fmrx_recv_byte(uint8_t byte) { if (USB_DeviceState == DEVICE_STATE_Configured && !RingBuffer_IsFull(&c2u_ringbuf)) RingBuffer_Insert(&c2u_ringbuf, byte); } /* * Called from the transmit register empty ISR to fetch a new byte; * returns -1 if the ring buffer is empty. */ int fmtx_next_byte(void) { if (USB_DeviceState != DEVICE_STATE_Configured || RingBuffer_IsEmpty(&u2c_ringbuf)) return -1; return RingBuffer_Remove(&u2c_ringbuf); } /* * Event handler for the CDC Class driver Line Encoding Changed event. */ void EVENT_CDC_Device_LineEncodingChanged(USB_ClassInfo_CDC_Device_t* const cii) { uint32_t baudrate = cii->State.LineEncoding.BaudRateBPS; /* This is a hack to give a sensible default */ baudrate = CAS_BAUDRATE_ABC80; baudrate = fmtx_real_baudrate(baudrate); if (1 || baudrate != current_baudrate) { current_baudrate = baudrate; fmtx_drain(); fmrx_set_speed(current_baudrate); fmtx_init_speed(current_baudrate); } } /* * Event handler for control line changes */ void EVENT_CDC_Device_ControLineStateChanged(USB_ClassInfo_CDC_Device_t* const cii) { /* Currently not used */ }