diff --git a/clem_device.h b/clem_device.h index a0dbfb5..d9d4282 100644 --- a/clem_device.h +++ b/clem_device.h @@ -288,40 +288,6 @@ void clem_iwm_debug_stop(struct ClemensDeviceIWM *iwm); bool clem_smartport_bus(struct ClemensSmartPortUnit *unit, unsigned unit_count, unsigned *io_flags, unsigned *out_phase, clem_clocks_time_t ts, unsigned delta_ns); -/** - * @brief - * - * @param scc - */ -void clem_scc_reset(struct ClemensDeviceSCC *scc); - -/** - * @brief - * - * @param scc - * @param clock - */ -void clem_scc_glu_sync(struct ClemensDeviceSCC *scc, struct ClemensClock *clock); - -/** - * @brief - * - * @param scc - * @param ioreg - * @param value - */ -void clem_scc_write_switch(struct ClemensDeviceSCC *scc, uint8_t ioreg, uint8_t value); - -/** - * @brief - * - * @param scc - * @param ioreg - * @param flags - * @return uint8_t - */ -uint8_t clem_scc_read_switch(struct ClemensDeviceSCC *scc, uint8_t ioreg, uint8_t flags); - /** * @brief * diff --git a/clem_mmio.c b/clem_mmio.c index 760d03f..b34c4d0 100644 --- a/clem_mmio.c +++ b/clem_mmio.c @@ -6,6 +6,7 @@ #include "clem_device.h" #include "clem_mmio_types.h" +#include "clem_scc.h" #include "clem_types.h" #include "clem_util.h" #include "clem_vgc.h" @@ -1081,7 +1082,7 @@ void clem_mmio_write(ClemensMMIO *mmio, struct ClemensTimeSpec *tspec, uint8_t d case CLEM_MMIO_REG_SCC_A_CMD: case CLEM_MMIO_REG_SCC_B_DATA: case CLEM_MMIO_REG_SCC_A_DATA: - clem_scc_write_switch(&mmio->dev_scc, ioreg, data); + clem_scc_write_switch(&mmio->dev_scc, tspec, ioreg, data); break; case CLEM_MMIO_REG_AUDIO_CTL: clem_sound_write_switch(&mmio->dev_audio, ioreg, data); @@ -1752,7 +1753,7 @@ void clem_mmio_reset(ClemensMMIO *mmio, struct ClemensTimeSpec *tspec) { clem_sound_reset(&mmio->dev_audio); clem_vgc_reset(&mmio->vgc); clem_iwm_reset(&mmio->dev_iwm, tspec); - clem_scc_reset(&mmio->dev_scc); + clem_scc_reset(&mmio->dev_scc, tspec); } void clem_mmio_init(ClemensMMIO *mmio, struct ClemensDeviceDebugger *dev_debug, diff --git a/clem_mmio_defs.h b/clem_mmio_defs.h index 5a81bd6..d762814 100644 --- a/clem_mmio_defs.h +++ b/clem_mmio_defs.h @@ -647,6 +647,10 @@ #define CLEM_MONITOR_COLOR_RGB 0 #define CLEM_MONITOR_COLOR_MONO 1 +/* Mose SCC defines are in clem_scc.h - the few that are here are required by + clem_mmio_types.h */ +#define CLEM_SCC_RECV_QUEUE_SIZE 3 + /* NTSC scanlines start at counter 7 and end at 198 (192 lines) VBL begins at 199 (scanline 192) see technote 39, 40 and clem_vgc.c for links @@ -708,14 +712,6 @@ #define CLEM_VGC_SCANLINE_COLORFILL_MODE (0x20) #define CLEM_VGC_SCANLINE_PALETTE_INDEX_MASK (0x0f) -#define CLEM_SCC_PORT_DTR 0x01 -#define CLEM_SCC_PORT_HSKI 0x02 -#define CLEM_SCC_PORT_TX_DATA_LO 0x04 -#define CLEM_SCC_PORT_TX_DATA_HI 0x08 -#define CLEM_SCC_PORT_RX_DATA_LO 0x10 -#define CLEM_SCC_PORT_RX_DATA_HI 0x20 -#define CLEM_SCC_PORT_GPI 0x40 - #define CLEM_ENSONIQ_OSC_CTL_FREE_MODE 0x00 #define CLEM_ENSONIQ_OSC_CTL_M0 0x02 #define CLEM_ENSONIQ_OSC_CTL_SYNC 0x04 diff --git a/clem_mmio_types.h b/clem_mmio_types.h index 2df74fb..3d41cea 100644 --- a/clem_mmio_types.h +++ b/clem_mmio_types.h @@ -109,14 +109,45 @@ struct ClemensDeviceADB { uint32_t irq_line; /**< IRQ flags passed to machine */ }; -struct ClemensDeviceSCC { - clem_clocks_time_t ts_last_frame; - - /** Internal state that drives how the cmd/data registers are interpreted */ +struct ClemensDeviceSCCChannel { + unsigned serial_port; + + /** Register set */ + uint8_t regs[16]; + uint8_t rr0, rr1; + uint8_t rr3, rr8; + uint8_t selected_reg; + uint8_t txd_internal; /* Used for loopback*/ + uint8_t rxd_error; /* Tracks received byte and acts as the error byte */ + uint8_t pad; + + /** Applies clock mode (/1, /16, /32, /64) to calculate the master step and edges. */ + clem_clocks_time_t master_clock_ts; + clem_clocks_time_t xtal_edge_ts; + clem_clocks_time_t pclk_edge_ts; + clem_clocks_duration_t master_clock_step; + + /** Data buffers - FIFO queues that mimic the Z8530 recv/xmit buffers */ + uint8_t recv_queue[CLEM_SCC_RECV_QUEUE_SIZE]; + uint8_t recv_err_queue[CLEM_SCC_RECV_QUEUE_SIZE]; + uint8_t tx_byte; + uint8_t rx_condition; /* Special Receive Condition */ + uint32_t tx_register; + uint32_t tx_shift_ctr; + uint32_t rx_queue_pos; + uint32_t rx_shift_ctr; + uint32_t rx_register; + uint32_t brg_counter; /* Bit 31 = high bit is the flip-flop state*/ unsigned state; - unsigned selected_reg[2]; +}; + +struct ClemensDeviceSCC { + /** Clocks, including the XTAL oscillator @ 3.6864 mhz, CREF is defined in + clem_shared.h */ + clem_clocks_duration_t xtal_step; + clem_clocks_duration_t pclk_step; - uint8_t serial[2]; /**< See CLEM_SCC_PORT_xxx */ + struct ClemensDeviceSCCChannel channel[2]; uint32_t irq_line; /**< IRQ flags passed to machine */ }; diff --git a/clem_scc.c b/clem_scc.c index c0cb20d..2d043a9 100644 --- a/clem_scc.c +++ b/clem_scc.c @@ -1,49 +1,1021 @@ +#include "clem_scc.h" + #include "clem_debug.h" #include "clem_device.h" -#include "clem_mmio_defs.h" #include "clem_mmio_types.h" +#include "clem_shared.h" + +#include +#include + +/* SCC Implementation + ================== + This module implements communication between the machine and an emulated + Zilog 8530 (NMOS). Though not called out in the docs - the data/command per + port interface is very similar to how the system communicates with the Ensoniq. + + Commands and data are funneled between the machine and emulated Zilog unit. + Command sent from machine with data: SCC_x_CMD, SCC_x_DATA + Zilog responds with data: SCC_x_DATA + + The "GLU" will expect a command byte and a data byte + + The emulated SCC has the following components: + + - Command Data registers + - Interrupt control logic + - Channel controllers (I/O) + + These ports are used to communicate with a printer and modem (A, B) + These "peripherals" will expect tx/rx from this module. + + ~SYNCA, ~RTXCA, RTXCB = 3.6864 mhz, or the XTAL oscillator clock + PCLK = Mega 2 CREF + + Peripheral (Plug to SCC Pin) + ========== + 1:CLEM_SCC_PORT_DTR - DTR + 2:CLEM_SCC_PORT_HSKI - CTS, TRxC + 3:CLEM_SCC_PORT_TX_D_LO - TxD + 5:CLEM_SCC_PORT_RX_D_LO - RxD + 6:CLEM_SCC_PORT_TX_D_HI - RTS + 7:CLEM_SCC_PORT_GPI - DCD + + + Timing + ====== + Compromise between accuracy and efficiency when emulating the clock + Rely on clock mode and the multiplier, which determines the effective + clock frequency for the transmitter and receiver. + + This means encoding beyond NRZ/NRZI will likely not work since higher + effective clock frequencies are required to sample the I/O data stream. + As a side effect, it's unlikely synchronous mode will work correctly. + + NRZ encoding will be supported by default. Will evaluate other modes as + needed. + + BRG baud constant = (Clock Freq) / (2 * BAUD * ClockMode) - 2 + CTS, TRxC, XTAL (RTxC, SYNC), or Baud-rate-generator (BRG via PCLK/RTxC) + + Interrupts + ========== + See 4.2 Z8530 1986 documentation (page 4-1), figure 4-2 (page 4-4) + Note, since /INTACK is always high as referenced in the IIGS schemeatic, + this means 'Interrupt Without Acknowledge' is the only approach that needs + to be implemented. This method relies on register reads to extract details. + + Each channel will trigger an interrupt based on recv/xmit status. + + Prioritization may be required (page 4-2, Interrupt Sources) so that high + priority interrupts don't erase lower-priority ones of interest. + + Not Supported (Yet?) + ==================== + - Synchronous mode unless it's really needed on the IIGS + - Many features associated with Synchronous Mode (SDLC, Hunt, etc) + - Most interrupt features that aren't needed on the IIGS +*/ + +#define CLEM_SCC_DEBUG + +#ifdef CLEM_SCC_DEBUG +#define CLEM_SCC_LOG(...) CLEM_LOG(__VA_ARGS__) +#else +#define CLEM_SCC_LOG(...) CLEM_DEBUG(__VA_ARGS__) +#endif + +#define CLEM_CLOCKS_SCC_XTAL_MHZ 3.6864 + +#define CLEM_SCC_STATE_READY 0 +#define CLEM_SCC_STATE_REGISTER 1 + +#define CLEM_SCC_CLOCK_MODE_PCLK 0 +#define CLEM_SCC_CLOCK_MODE_XTAL 1 + +#define CLEM_SCC_BRG_STATUS_FF_FLAG 0x80000000 +#define CLEM_SCC_BRG_STATUS_PULSE_FLAG 0x40000000 +#define CLEM_SCC_BRG_STATUS_COUNTER_MASK 0x0000ffff + +static unsigned s_scc_clk_x_speeds[] = {1, 16, 32, 64}; +static unsigned s_scc_bit_count[] = {5, 7, 6, 8}; + +static inline bool _clem_scc_is_xtal_on(struct ClemensDeviceSCCChannel *channel) { + return (channel->regs[CLEM_SCC_WR11_CLOCK_MODE] & CLEM_SCC_CLK_XTAL_ON) != 0; +} + +static inline bool _clem_scc_is_tx_enabled(struct ClemensDeviceSCCChannel *channel) { + return (channel->regs[CLEM_SCC_WR5_XMIT_CONTROL] & CLEM_SCC_TX_ENABLE) != 0; +} + +static inline bool _clem_scc_is_rx_enabled(struct ClemensDeviceSCCChannel *channel) { + return (channel->regs[CLEM_SCC_WR3_RECV_CONTROL] & CLEM_SCC_RX_ENABLE) != 0; +} + +static inline bool _clem_scc_is_synchronous_mode(struct ClemensDeviceSCCChannel *channel) { + return (channel->regs[CLEM_SCC_WR4_CLOCK_DATA_RATE] & 0x0c) == CLEM_SCC_STOP_SYNC_MODE; +} + +static inline bool _clem_scc_is_auto_enable(struct ClemensDeviceSCCChannel *channel) { + return (channel->regs[CLEM_SCC_WR3_RECV_CONTROL] & CLEM_SCC_TX_RX_AUTO_ENABLE) != 0; +} + +static bool _clem_scc_is_auto_echo_enabled(struct ClemensDeviceSCCChannel *channel) { + return (channel->regs[CLEM_SCC_WR14_MISC_CONTROL] & CLEM_SCC_AUTO_ECHO) != 0; +} + +static inline bool _clem_scc_is_local_loopback_enabled(struct ClemensDeviceSCCChannel *channel) { + return (channel->regs[CLEM_SCC_WR14_MISC_CONTROL] & CLEM_SCC_LOCAL_LOOPBACK) != 0; +} + +static inline bool _clem_scc_check_port_cts_txrc(struct ClemensDeviceSCCChannel *channel) { + return (channel->serial_port & CLEM_SCC_PORT_HSKI) != 0; +} + +static inline bool _clem_scc_check_port_dcd(struct ClemensDeviceSCCChannel *channel) { + return (channel->serial_port & CLEM_SCC_PORT_GPI) != 0; +} + +static inline void _clem_scc_set_port_txd(struct ClemensDeviceSCCChannel *channel, bool txd) { + if (txd) + channel->serial_port |= CLEM_SCC_PORT_TX_D_LO; + else + channel->serial_port &= ~CLEM_SCC_PORT_TX_D_LO; +} + +static inline bool _clem_scc_check_port_rxd(struct ClemensDeviceSCCChannel *channel) { + return (channel->serial_port & CLEM_SCC_PORT_RX_D_LO) != 0; +} + +static inline bool _clem_scc_interrupts_enabled(struct ClemensDeviceSCC *scc) { + return (scc->channel[0].regs[CLEM_SCC_WR9_MASTER_INT] & 0x80) != 0; +} + +static inline bool _clem_scc_is_interrupt_enabled(struct ClemensDeviceSCCChannel *channel, + uint8_t iflag) { + return (channel->regs[CLEM_SCC_WR15_INT_ENABLE] & iflag) != 0; +} + +static inline bool _clem_scc_is_interrupt_pending(struct ClemensDeviceSCC *scc, uint8_t iflag) { + return (scc->channel[0].rr3 & iflag) != 0; +} + +static inline void _clem_scc_set_interrupt_pending(struct ClemensDeviceSCC *scc, uint8_t iflag) { + scc->channel[0].rr3 |= iflag; +} + +static inline void _clem_scc_clear_interrupt_pending(struct ClemensDeviceSCC *scc, uint8_t iflag) { + scc->channel[0].rr3 &= ~iflag; +} + +static inline clem_clocks_duration_t +_clem_scc_clock_data_step_from_mode(clem_clocks_duration_t clock_step, uint8_t mode) { + return clock_step * s_scc_clk_x_speeds[mode >> 6]; +} + +static inline clem_clocks_duration_t +_clem_scc_channel_calc_clock_step(struct ClemensDeviceSCCChannel *channel, + clem_clocks_duration_t clock_step) { + return _clem_scc_clock_data_step_from_mode(clock_step, + channel->regs[CLEM_SCC_WR4_CLOCK_DATA_RATE] & 0xc0); +} + +static void _clem_scc_channel_set_master_clock_step(struct ClemensDeviceSCCChannel *channel, + clem_clocks_duration_t clock_step) { + + channel->master_clock_step = _clem_scc_channel_calc_clock_step(channel, clock_step); +} + +static inline void _clem_scc_brg_counter_reset(struct ClemensDeviceSCCChannel *channel, bool ff) { + channel->brg_counter = (ff ? CLEM_SCC_BRG_STATUS_FF_FLAG : 0x0) | + ((unsigned)channel->regs[CLEM_SCC_WR13_TIME_CONST_HI] << 16) | + channel->regs[CLEM_SCC_WR12_TIME_CONST_LO]; +} + +static inline bool _clem_scc_is_xtal_enabled(struct ClemensDeviceSCCChannel *channel) { + return (channel->regs[CLEM_SCC_WR14_MISC_CONTROL] & CLEM_SCC_CLK_XTAL_ON) != 0; +} + +static inline bool _clem_scc_is_brg_on(struct ClemensDeviceSCCChannel *channel) { + return (channel->regs[CLEM_SCC_WR14_MISC_CONTROL] & CLEM_SCC_CLK_BRG_ON) != 0; +} + +static inline bool _clem_scc_is_brg_clock_mode(struct ClemensDeviceSCCChannel *channel, + uint8_t mode) { + bool brg_pclk = (channel->regs[CLEM_SCC_WR14_MISC_CONTROL] & CLEM_SCC_CLK_BRG_PCLK) != 0; + if (_clem_scc_is_xtal_enabled(channel)) { + return mode == CLEM_SCC_CLOCK_MODE_XTAL && !brg_pclk; + } else { + return mode == CLEM_SCC_CLOCK_MODE_PCLK && brg_pclk; + } +} + +static bool _clem_scc_brg_tick(struct ClemensDeviceSCCChannel *channel) { + // every tick decreases the counter - if 0, then load in a new counter + // when 0, toggle the ff bit and reload the counter + if (!_clem_scc_is_brg_on(channel)) + return false; + + // the caller should've handled the pulse after the last call to tick() + channel->brg_counter &= ~CLEM_SCC_BRG_STATUS_PULSE_FLAG; + + if (!(channel->brg_counter & CLEM_SCC_BRG_STATUS_COUNTER_MASK)) { + // a transition from high to low indicates a BRG pulse on the transmitter or receiver + bool old_ff_status = (channel->brg_counter & CLEM_SCC_BRG_STATUS_FF_FLAG) != 0; + _clem_scc_brg_counter_reset(channel, !old_ff_status); + if (old_ff_status && !(channel->brg_counter & CLEM_SCC_BRG_STATUS_FF_FLAG)) { + channel->brg_counter |= CLEM_SCC_BRG_STATUS_PULSE_FLAG; + } + } else { + channel->brg_counter--; + } + return (channel->brg_counter & CLEM_SCC_BRG_STATUS_PULSE_FLAG) != 0; +} + +inline unsigned _clem_scc_count_1_bits(uint8_t v, unsigned bps) { + // TODO: this could be a generated lookup table that returns even/odd + unsigned cnt = 0; + while (bps--) { + if (v & 1) + cnt++; + v >>= 1; + } + return cnt; +} + +inline bool _clem_scc_do_parity_even(uint8_t v, unsigned bps) { + // if odd number of 1 bits, return true so the encoder can add another 1 bit (even total) + return _clem_scc_count_1_bits(v, bps) & 1; +} + +inline bool _clem_scc_do_parity_odd(uint8_t v, unsigned bps) { + // if even number of 1 bits, return true so the encoder can add another 1 bit (odd total) + return !(_clem_scc_count_1_bits(v, bps) & 1); +} + +inline void _clem_scc_channel_error(struct ClemensDeviceSCCChannel *channel, uint8_t err, + bool special) { + channel->rxd_error |= err; // the error latch + if (special) { + channel->rx_condition |= err; + } +} + +static void _clem_scc_do_rx(struct ClemensDeviceSCCChannel *channel) { + // manages the tx_register from tx_byte to the transmitter + unsigned bits_per_character; + unsigned parity_bit_index; + unsigned stop_bit_index; + uint8_t rxd; + // if auto enable is on, then DCD will drive the receiver which is not the + // but local loopback overrides DCD. + if (_clem_scc_is_auto_enable(channel) && !_clem_scc_check_port_dcd(channel)) { + if (!_clem_scc_is_local_loopback_enabled(channel)) + return; + } + // data bits per received character not including parity/stop *but* including start + bits_per_character = + s_scc_bit_count[(channel->regs[CLEM_SCC_WR3_RECV_CONTROL] & 0xc0) >> 6] + 1; + rxd = _clem_scc_is_local_loopback_enabled(channel) ? channel->txd_internal + : (uint8_t)_clem_scc_check_port_rxd(channel); + + // TODO?! + if (_clem_scc_is_synchronous_mode(channel)) + return; + + if (channel->rx_shift_ctr == 0) { + // expecting a start bit which will be rxd=0 (and do not shift in bits until start + // encountered) + if (!rxd) { + channel->rx_register = 0; + channel->rxd_error = 0; + channel->rx_shift_ctr++; + } + return; + } + parity_bit_index = (channel->regs[CLEM_SCC_WR4_CLOCK_DATA_RATE] & CLEM_SCC_PARITY_ENABLED) + ? bits_per_character + 1 + : bits_per_character; + if ((channel->regs[CLEM_SCC_WR4_CLOCK_DATA_RATE] & 0x0c) == CLEM_SCC_STOP_BIT_2) { + stop_bit_index = parity_bit_index + 2; + } else { + // TODO: and 1.5 bit only if we ever support non NRZ encoding - highly unlikely + stop_bit_index = parity_bit_index + 1; + } + if (channel->rx_shift_ctr < bits_per_character) { + if (rxd) { + channel->rx_register |= (0x1 << (channel->rx_shift_ctr - 1)); + } + } else { + if (channel->rx_shift_ctr < parity_bit_index) { + // rxd is the parity bit + if (channel->regs[CLEM_SCC_WR4_CLOCK_DATA_RATE] & CLEM_SCC_PARITY_EVEN) { + if (!_clem_scc_do_parity_even((uint8_t)(channel->rx_register & 0xff), + bits_per_character) || + !rxd) { + _clem_scc_channel_error(channel, CLEM_SCC_RR1_PARITY_ERROR, + (channel->regs[CLEM_SCC_WR1_IN_TX_RX] & + CLEM_SCC_ENABLE_PARITY_SPECIAL_COND) != 0); + } + } else { + if (!_clem_scc_do_parity_odd((uint8_t)(channel->rx_register & 0xff), + bits_per_character) || + !rxd) { + _clem_scc_channel_error(channel, CLEM_SCC_RR1_PARITY_ERROR, + (channel->regs[CLEM_SCC_WR1_IN_TX_RX] & + CLEM_SCC_ENABLE_PARITY_SPECIAL_COND) != 0); + } + } + } else if (channel->rx_shift_ctr < stop_bit_index) { + if (!rxd) { + _clem_scc_channel_error(channel, CLEM_SCC_RR1_FRAMING_ERROR, true); + } + } + } + channel->rx_shift_ctr++; + if (channel->rx_shift_ctr < stop_bit_index) + return; + + // will be a break if a framing error detected with a zero byte + if (channel->rx_register != 0 || !(channel->rxd_error & CLEM_SCC_RR1_FRAMING_ERROR)) { + if (channel->rx_queue_pos < CLEM_SCC_RECV_QUEUE_SIZE) { + channel->recv_err_queue[channel->rx_queue_pos] = channel->rxd_error; + channel->recv_queue[channel->rx_queue_pos] = (uint8_t)(channel->rx_register & 0xff); + ++channel->rx_queue_pos; + } else { + // overrun - docs are hazy on whether this item overwrites the top + // but we'll just flag an error for now + _clem_scc_channel_error(channel, CLEM_SCC_RR1_RECV_OVERRUN, true); + } + channel->rr0 |= CLEM_SCC_RR0_RECV_AVAIL; + // RR1 and RR0 updates + // pull data from queue when reading RR8 or data port + // error at queue position 0 is used for RR1 (and updated when the queue changes) + // overrun stays in RR1 until cleared by interrupt/reset + CLEM_SCC_LOG("SCC: recv %03x", channel->rx_register); + channel->rx_shift_ctr = 0; + } else { + // Unsure if we need to push this zero byte onto the queue since the tech docs + // mention the byte is 'read and discarded' after being placed on the queue. + // But we do need to set the RR0 break status bit here + if (channel->rr0 & CLEM_SCC_RR0_BREAK_ABORT) { + if (rxd) { + channel->rr0 &= ~CLEM_SCC_RR0_BREAK_ABORT; + CLEM_SCC_LOG("SCC: break OFF"); + channel->rx_shift_ctr = 0; + } + } else { + if (!rxd) { + channel->rr0 |= CLEM_SCC_RR0_BREAK_ABORT; + CLEM_SCC_LOG("SCC: break ON"); + } + } + } +} + +static void _clem_scc_do_tx(struct ClemensDeviceSCCChannel *channel) { + // manages the tx_register from tx_byte to the transmitter + unsigned bits_per_character; + + // if auto enable is on, then CTS will drive the transmitted + // which is not the case with auto-echo or local loopback + if (_clem_scc_is_auto_enable(channel) && !_clem_scc_check_port_cts_txrc(channel)) { + if (!_clem_scc_is_local_loopback_enabled(channel) && + !_clem_scc_is_auto_echo_enabled(channel)) + return; + } + + bits_per_character = s_scc_bit_count[(channel->regs[CLEM_SCC_WR5_XMIT_CONTROL] & 0x60) >> 5]; + + // TODO: if send_break? + if (channel->tx_shift_ctr == 0) { + if (channel->rr0 & CLEM_SCC_RR0_TX_EMPTY) { + channel->txd_internal = 1; + } else if (!_clem_scc_is_synchronous_mode(channel)) { + channel->rr1 &= ~CLEM_SCC_RR1_ALL_SENT; + channel->tx_register = channel->tx_byte; + channel->tx_shift_ctr = bits_per_character; + if (channel->regs[CLEM_SCC_WR4_CLOCK_DATA_RATE] & CLEM_SCC_PARITY_ENABLED) { + if (channel->regs[CLEM_SCC_WR4_CLOCK_DATA_RATE] & CLEM_SCC_PARITY_EVEN) { + if (_clem_scc_do_parity_even(channel->tx_byte, bits_per_character)) { + channel->tx_register |= (1 << channel->tx_shift_ctr); + } + } else { + if (_clem_scc_do_parity_odd(channel->tx_byte, bits_per_character)) { + channel->tx_register |= (1 << channel->tx_shift_ctr); + } + } + channel->tx_shift_ctr++; + } + // add stop bits 1.5 can't be supported unless we support more advanced + // encoding. + if ((channel->regs[CLEM_SCC_WR4_CLOCK_DATA_RATE] & 0x0c) == CLEM_SCC_STOP_BIT_2) { + channel->tx_register |= (0x03 << channel->tx_shift_ctr); + channel->tx_shift_ctr++; + channel->tx_shift_ctr++; + } else { + // TODO: warn about 1.5 stop bits on the WR4 set so we know this won't work + // so this will be 1 bit! + channel->tx_register |= (0x01 << channel->tx_shift_ctr); + channel->tx_shift_ctr++; + } + // start bit (lo) + channel->tx_register <<= 1; + channel->tx_shift_ctr++; + channel->rr0 |= CLEM_SCC_RR0_TX_EMPTY; + } else { + CLEM_ASSERT(false); + } + } + if (channel->tx_shift_ctr > 0) { + channel->txd_internal = (uint8_t)(channel->tx_register & 1); + channel->tx_register >>= 1; + channel->tx_shift_ctr--; + if (channel->tx_shift_ctr == 0) { + channel->rr1 |= CLEM_SCC_RR1_ALL_SENT; + } + } + + // TxD will not receive the transmitted bit if auto-echo enabled + // which takes priority over local loopback + if (_clem_scc_is_auto_echo_enabled(channel)) { + _clem_scc_set_port_txd(channel, _clem_scc_check_port_rxd(channel)); + } else { + _clem_scc_set_port_txd(channel, channel->txd_internal); + } +} + +static void _clem_scc_do_rx_tx(struct ClemensDeviceSCCChannel *channel, bool trxc_pulse, + bool rtxc_pulse, bool brg_pulse) { + uint8_t mode = channel->regs[CLEM_SCC_WR11_CLOCK_MODE] & 0x18; + switch (mode) { + case CLEM_SCC_CLK_TX_SOURCE_BRG: + if (brg_pulse) + _clem_scc_do_tx(channel); + break; + case CLEM_SCC_CLK_TX_SOURCE_TRxC: + if (trxc_pulse) + _clem_scc_do_tx(channel); + break; + case CLEM_SCC_CLK_TX_SOURCE_RTxC: + if (rtxc_pulse) + _clem_scc_do_tx(channel); + break; + case CLEM_SCC_CLK_TX_SOURCE_DPLL: + // TODO? + break; + } + mode = channel->regs[CLEM_SCC_WR11_CLOCK_MODE] & 0x60; + switch (mode) { + case CLEM_SCC_CLK_RX_SOURCE_BRG: + if (brg_pulse) + _clem_scc_do_rx(channel); + break; + case CLEM_SCC_CLK_RX_SOURCE_TRxC: + if (trxc_pulse) + _clem_scc_do_rx(channel); + break; + case CLEM_SCC_CLK_RX_SOURCE_RTxC: + if (rtxc_pulse) + _clem_scc_do_rx(channel); + break; + case CLEM_SCC_CLK_RX_SOURCE_DPLL: + // TODO? + break; + } +} + +static void _clem_scc_channel_port_states(struct ClemensDeviceSCC *scc, unsigned ch_idx) { + // reports the various status pin states + // updates status pins based on register values + // status interrupts + struct ClemensDeviceSCCChannel *channel = &scc->channel[ch_idx]; + uint8_t rr0 = channel->rr0; + uint8_t xrr0; + ch_idx *= 3; + + if (!_clem_scc_is_interrupt_pending(scc, CLEM_SCC_RR3_EXT_STATUS_IP_A >> ch_idx)) { + if (channel->serial_port & CLEM_SCC_PORT_HSKI) { + channel->rr0 |= CLEM_SCC_RR0_CTS_STATUS; + } else { + channel->rr0 &= ~CLEM_SCC_RR0_CTS_STATUS; + } + if (channel->serial_port & CLEM_SCC_PORT_GPI) { + channel->rr0 |= CLEM_SCC_RR0_DCD_STATUS; + } else { + channel->rr0 &= ~CLEM_SCC_RR0_DCD_STATUS; + } + if (_clem_scc_is_brg_on(channel) && channel->brg_counter == 0) { + channel->rr0 |= CLEM_SCC_RR0_ZERO_COUNT; + } else { + channel->rr0 &= ~CLEM_SCC_RR0_ZERO_COUNT; + } + } + xrr0 = rr0 ^ channel->rr0; + if (xrr0) { + if (_clem_scc_is_interrupt_enabled(channel, CLEM_SCC_INT_CTS_INT_ENABLE) && + (xrr0 & CLEM_SCC_RR0_CTS_STATUS)) { + _clem_scc_set_interrupt_pending(scc, CLEM_SCC_RR3_EXT_STATUS_IP_A >> ch_idx); + } + if (_clem_scc_is_interrupt_enabled(channel, CLEM_SCC_INT_DCD_INT_ENABLE) && + (xrr0 & CLEM_SCC_RR0_DCD_STATUS)) { + _clem_scc_set_interrupt_pending(scc, CLEM_SCC_RR3_EXT_STATUS_IP_A >> ch_idx); + } + if (_clem_scc_is_interrupt_enabled(channel, CLEM_SCC_INT_ZERO_COUNT_INT_ENABLE) && + (channel->rr0 & CLEM_SCC_RR0_ZERO_COUNT)) { + _clem_scc_set_interrupt_pending(scc, CLEM_SCC_RR3_EXT_STATUS_IP_A >> ch_idx); + } + // TODO: do something with BREAK_ABORT here - even if it means duplicating status so the + // logic + // here can set or clear the RR0 bit and issue status interrupts. + } +} + +//////////////////////////////////////////////////////////////////////////////// + +void clem_scc_reset_channel(struct ClemensDeviceSCC *scc, clem_clocks_time_t ts, unsigned ch_idx, + bool hw) { + struct ClemensDeviceSCCChannel *channel = &scc->channel[ch_idx]; + if (hw) { + memset(channel->regs, 0, sizeof(channel->regs)); + channel->regs[CLEM_SCC_WR11_CLOCK_MODE] |= CLEM_SCC_CLK_TX_SOURCE_TRxC; + channel->master_clock_ts = ts; + channel->xtal_edge_ts = + channel->master_clock_ts + _clem_scc_channel_calc_clock_step(channel, scc->xtal_step); + channel->pclk_edge_ts = + channel->master_clock_ts + _clem_scc_channel_calc_clock_step(channel, scc->pclk_step); + } + _clem_scc_channel_set_master_clock_step(channel, scc->xtal_step); + channel->regs[CLEM_SCC_WR4_CLOCK_DATA_RATE] |= CLEM_SCC_STOP_BIT_1; + + _clem_scc_brg_counter_reset(channel, true); + + channel->txd_internal = 1; + channel->tx_shift_ctr = 0; + channel->rx_shift_ctr = 0; + channel->rx_queue_pos = 0; + channel->rxd_error = 0; + channel->rx_condition = 0; + + channel->rr0 |= (CLEM_SCC_RR0_TX_EMPTY + CLEM_SCC_RR0_TX_UNDERRUN); + // NOTE: reset RR1 to this and likely won't change unless synchronous mode is really implemented + channel->rr1 = 0x06; +} + +void clem_scc_sync_channel_uart(struct ClemensDeviceSCC *scc, unsigned ch_idx, + clem_clocks_time_t next_ts) { + struct ClemensDeviceSCCChannel *channel = &scc->channel[ch_idx]; + bool rx_enabled = _clem_scc_is_rx_enabled(channel); + bool tx_enabled = _clem_scc_is_tx_enabled(channel); -#define CLEM_SCC_STATE_REGISTER_WAIT 0 -#define CLEM_SCC_STATE_REGISTER_SELECTED 1 + // device and scc combined poll + // multiple serial ops from a potential peer can occur multiple times per + // call to this sync() function by design (since instructions can be several + // 1 mhz cycles, though realistically that implies something like 200-300k baud rate) + // so likely not...? + // + bool trxc_pulse; + bool brg_pulse; -void clem_scc_reset(struct ClemensDeviceSCC *scc) {} + if (!rx_enabled && !tx_enabled) { + channel->master_clock_ts = next_ts; + channel->xtal_edge_ts = + channel->master_clock_ts + _clem_scc_channel_calc_clock_step(channel, scc->xtal_step); + channel->pclk_edge_ts = + channel->master_clock_ts + _clem_scc_channel_calc_clock_step(channel, scc->pclk_step); + return; + } + + trxc_pulse = false; + brg_pulse = false; + + while (channel->master_clock_ts < next_ts) { + while (channel->master_clock_ts >= channel->xtal_edge_ts || + channel->master_clock_ts >= channel->pclk_edge_ts) { + if (channel->master_clock_ts >= channel->xtal_edge_ts) { + if (_clem_scc_is_xtal_enabled(channel)) { + trxc_pulse = _clem_scc_check_port_cts_txrc(channel); + if (_clem_scc_is_brg_clock_mode(channel, CLEM_SCC_CLOCK_MODE_XTAL)) { + brg_pulse = _clem_scc_brg_tick(channel); + } + _clem_scc_do_rx_tx(channel, trxc_pulse, true, brg_pulse); + } + + channel->xtal_edge_ts += _clem_scc_channel_calc_clock_step(channel, scc->xtal_step); + } + if (channel->master_clock_ts >= channel->pclk_edge_ts) { + if (_clem_scc_is_brg_clock_mode(channel, CLEM_SCC_CLOCK_MODE_PCLK)) { + brg_pulse = _clem_scc_brg_tick(channel); + } + if (_clem_scc_is_xtal_enabled(channel)) { + trxc_pulse = false; + } else { + trxc_pulse = (channel->serial_port & CLEM_SCC_PORT_HSKI) != 0; + } + _clem_scc_do_rx_tx(channel, trxc_pulse, false, brg_pulse); + + channel->pclk_edge_ts += _clem_scc_channel_calc_clock_step(channel, scc->xtal_step); + } + _clem_scc_channel_port_states(scc, ch_idx); + } + + channel->master_clock_ts += channel->master_clock_step; + } +} + +void clem_scc_reset(struct ClemensDeviceSCC *scc, struct ClemensTimeSpec *tspec) { + // equivalent to a hardware reset + memset(scc, 0, sizeof(*scc)); + scc->xtal_step = (clem_clocks_duration_t)floor( + (14.31818 / CLEM_CLOCKS_SCC_XTAL_MHZ) * CLEM_CLOCKS_14MHZ_CYCLE + 0.5); + scc->pclk_step = CLEM_CLOCKS_CREF_CYCLE; + + clem_scc_reset_channel(scc, tspec->clocks_spent, 0, true); + clem_scc_reset_channel(scc, tspec->clocks_spent, 1, true); +} void clem_scc_glu_sync(struct ClemensDeviceSCC *scc, struct ClemensClock *clock) { - scc->ts_last_frame = clock->ts; + // channel A and B xmit/recv functionality is driven by either the PCLK + // or the XTAL (via configuration) + // synchronization between channel and GLU occurs on PCLK intervals + + clem_scc_sync_channel_uart(scc, 0, clock->ts); + clem_scc_sync_channel_uart(scc, 1, clock->ts); + + if (_clem_scc_interrupts_enabled(scc)) { + } else { + scc->irq_line = 0; + } } -void clem_scc_write_switch(struct ClemensDeviceSCC *scc, uint8_t ioreg, uint8_t value) { +void clem_scc_reg_command_wr0(struct ClemensDeviceSCC *scc, unsigned ch_idx, uint8_t value) { + struct ClemensDeviceSCCChannel *channel = &scc->channel[ch_idx]; + // register commands are unnecessary (bits 0-2), and CRC related settings (bits 6,7) + switch (value & CLEM_SCC_CMD_MASK) { + case CLEM_SCC_CMD_RESET_ERROR: + channel->rr1 &= ~CLEM_SCC_RR1_ERROR_MASK; + channel->rx_condition = 0; + break; + default: + CLEM_UNIMPLEMENTED("SCC: WR0 unimplemented command %02x", (value & CLEM_SCC_CMD_MASK) >> 3); + break; + } + + channel->regs[CLEM_SCC_WR0_COMMAND] = value; +} + +void clem_scc_reg_interrupts_wr1(struct ClemensDeviceSCC *scc, unsigned ch_idx, uint8_t value) { + struct ClemensDeviceSCCChannel *channel = &scc->channel[ch_idx]; + + channel->regs[CLEM_SCC_WR1_IN_TX_RX] = value; +} + +void clem_scc_reg_interrupt_vector_wr2(struct ClemensDeviceSCC *scc, uint8_t value) { + // a single value replicated on both channels + CLEM_SCC_LOG("SCC: WR2 Interrupt Vector %02x", value); + scc->channel[0].regs[CLEM_SCC_WR2_INT_VECTOR] = value; + scc->channel[1].regs[CLEM_SCC_WR2_INT_VECTOR] = value; +} + +void clem_scc_reg_recv_control_wr3(struct ClemensDeviceSCC *scc, struct ClemensTimeSpec *tspec, + unsigned ch_idx, uint8_t value) { + uint8_t xvalue = scc->channel[ch_idx].regs[CLEM_SCC_WR3_RECV_CONTROL] ^ value; + if (!xvalue) + return; + + CLEM_SCC_LOG("SCC: WR3 %c RX:%s, %02X", 'A' + ch_idx, value & CLEM_SCC_RX_ENABLE ? "ON" : "OFF", + value); + scc->channel[ch_idx].regs[CLEM_SCC_WR3_RECV_CONTROL] = value; +} + +void clem_scc_reg_clock_data_rate_wr4(struct ClemensDeviceSCC *scc, unsigned ch_idx, + uint8_t value) { + uint8_t xvalue = scc->channel[ch_idx].regs[CLEM_SCC_WR4_CLOCK_DATA_RATE] ^ value; + if (!xvalue) + return; + + if (xvalue & 0x0c) { + if ((value & 0xc) == CLEM_SCC_STOP_SYNC_MODE) { + CLEM_UNIMPLEMENTED("SCC: WR4 %c Synchronous Mode Set", 'A' + ch_idx); + } else { + CLEM_SCC_LOG("SCC: stop bits %u.%u", (value & 0xc) == CLEM_SCC_STOP_BIT_2 ? 2 : 1, + (value & 0xc) == CLEM_SCC_STOP_BIT_1_5 ? 5 : 0); + if ((value & 0xc) == CLEM_SCC_STOP_BIT_1_5) { + CLEM_WARN("SCC: stop bits of 1.5 may not work!!!"); + } + } + } + CLEM_SCC_LOG("SCC: WR4 %c X%u CLK, %02X", ch_idx ? 'B' : 'A', s_scc_clk_x_speeds[value >> 6], + value); + + scc->channel[ch_idx].regs[CLEM_SCC_WR4_CLOCK_DATA_RATE] = value; +} + +void clem_scc_reg_xmit_control_wr5(struct ClemensDeviceSCC *scc, struct ClemensTimeSpec *tspec, + unsigned ch_idx, uint8_t value) { + uint8_t xvalue = scc->channel[ch_idx].regs[CLEM_SCC_WR5_XMIT_CONTROL] ^ value; + if (!xvalue) + return; + + CLEM_SCC_LOG("SCC: WR5 %c TX:%s, %02X", 'A' + ch_idx, value & CLEM_SCC_TX_ENABLE ? "ON" : "OFF", + value); + + scc->channel[ch_idx].regs[CLEM_SCC_WR5_XMIT_CONTROL] = value; +} + +void clem_scc_reg_xmit_byte(struct ClemensDeviceSCC *scc, unsigned ch_idx, uint8_t value) { + // this can always be written to, but it's emptied by clem_scc_sync_channel_uart() + // this byte is shifted into the tx_register when the tx_shift_ctr is 0 + // additional bits may be prepended/appended based on various settings detailed + // in clem_scc_sync_channel_uart. + struct ClemensDeviceSCCChannel *channel = &scc->channel[ch_idx]; + if (!(channel->rr0 & CLEM_SCC_RR0_TX_EMPTY)) { + CLEM_WARN("SCC: WR8 %c tx buffer full", ch_idx + 'A'); + } + channel->tx_byte = value; + channel->rr0 &= ~CLEM_SCC_RR0_TX_EMPTY; + if (channel->tx_shift_ctr == 0) { + // write buffer clear, so loading the register here means we're effectively + // starting a new transmission. + channel->rr1 &= ~CLEM_SCC_RR1_ALL_SENT; + } + CLEM_SCC_LOG("SCC: WR8 %c tx %02X", ch_idx + 'A', value); +} + +void clem_scc_reg_master_interrupt_wr9(struct ClemensDeviceSCC *scc, clem_clocks_time_t ts, + uint8_t value) { + uint8_t reset = (value & 0xc0); + + switch (reset) { + case 0x40: + CLEM_SCC_LOG("SCC: WR9 B reset"); + clem_scc_reset_channel(scc, ts, 1, false); + break; + case 0x80: + CLEM_SCC_LOG("SCC: WR9 A reset"); + clem_scc_reset_channel(scc, ts, 0, false); + break; + case 0xc0: + CLEM_SCC_LOG("SCC: WR9 Hardware Reset"); + clem_scc_reset_channel(scc, ts, 0, true); + clem_scc_reset_channel(scc, ts, 1, true); + break; + } +} + +void clem_scc_reg_clock_mode_wr11(struct ClemensDeviceSCC *scc, unsigned ch_idx, uint8_t value) { + static const char *s_clock_sources[] = {"RTxC", "TRxC", "BRG", "DPLL"}; + uint8_t xvalue = scc->channel[ch_idx].regs[CLEM_SCC_WR11_CLOCK_MODE] ^ value; + if (!xvalue) + return; + + CLEM_SCC_LOG("SCC: WR11 %c XTAL:%s TRxC:%02X", ch_idx ? 'B' : 'A', + value & CLEM_SCC_CLK_XTAL_ON ? "ON" : "OFF", value & 0x3); + if (xvalue & 0x60) { // recv clk changed + CLEM_SCC_LOG("SCC: WR11 %c rx_clock %s", ch_idx ? 'B' : 'A', + s_clock_sources[(value & 0x60) >> 5]); + } + if (xvalue & 0x18) { // send clk changed + CLEM_SCC_LOG("SCC: WR11 %c tx_clock %s", ch_idx ? 'B' : 'A', + s_clock_sources[(value & 0x18) >> 3]); + } + // TODO: TRxC O/I and special case overrides + + scc->channel[ch_idx].regs[CLEM_SCC_WR11_CLOCK_MODE] = value; +} + +void clem_scc_reg_dpll_command_wr14(struct ClemensDeviceSCC *scc, unsigned ch_idx, uint8_t value) { + static const char *mode_names[] = {"NUL", "SEARCH", "RSTCM", "NODPLL", + "BRGDPLL", "DPLLRTxC", "DPLLFM", "DPLLNRZI"}; + uint8_t xvalue = (scc->channel[ch_idx].regs[CLEM_SCC_WR14_MISC_CONTROL] & 0xe0) ^ value; + if (!xvalue) + return; + CLEM_SCC_LOG("SCCL WR14 %c %s", 'A' + ch_idx, mode_names[value >> 5]); + scc->channel[ch_idx].regs[CLEM_SCC_WR14_MISC_CONTROL] &= 0x1f; + scc->channel[ch_idx].regs[CLEM_SCC_WR14_MISC_CONTROL] |= value; +} + +void clem_scc_reg_baud_gen_enable_wr14(struct ClemensDeviceSCC *scc, unsigned ch_idx, + uint8_t value) { + struct ClemensDeviceSCCChannel *channel = &scc->channel[ch_idx]; + uint8_t xvalue = (channel->regs[CLEM_SCC_WR14_MISC_CONTROL] & 0x03) ^ value; + if (!xvalue) + return; + + CLEM_SCC_LOG("SCC: WR14 %c BRG %s %s", 'A' + ch_idx, + value & CLEM_SCC_CLK_BRG_PCLK ? "PCLK" : "XTAL", + value & CLEM_SCC_CLK_BRG_ON ? "ON" : "OFF"); + + if ((xvalue & CLEM_SCC_CLK_BRG_ON) && (value & CLEM_SCC_CLK_BRG_ON)) { + // BRG enabled, start pulse on high + _clem_scc_brg_counter_reset(channel, true); + } + + channel->regs[CLEM_SCC_WR14_MISC_CONTROL] &= 0xfc; + channel->regs[CLEM_SCC_WR14_MISC_CONTROL] |= value; +} + +void clem_scc_reg_misc_control_wr14(struct ClemensDeviceSCC *scc, unsigned ch_idx, uint8_t value) { + uint8_t xvalue = (scc->channel[ch_idx].regs[CLEM_SCC_WR14_MISC_CONTROL] & 0x1c) ^ value; + if (!xvalue) + return; + + CLEM_SCC_LOG("SCC: WR14 %c LOOPBACK", 'A' + ch_idx, + value & CLEM_SCC_LOCAL_LOOPBACK ? "ON" : "OFF"); + CLEM_SCC_LOG("SCC: WR14 %c AUTO ECHO", 'A' + ch_idx, value & CLEM_SCC_AUTO_ECHO ? "ON" : "OFF"); + CLEM_SCC_LOG("SCC: WR14 %c DTR_FN", 'A' + ch_idx, value & CLEM_SCC_DTR_FUNCTION ? "ON" : "OFF"); + + scc->channel[ch_idx].regs[CLEM_SCC_WR14_MISC_CONTROL] &= 0xe3; + scc->channel[ch_idx].regs[CLEM_SCC_WR14_MISC_CONTROL] |= value; +} + +void clem_scc_reg_set_interrupt_enable_wr15(struct ClemensDeviceSCC *scc, unsigned ch_idx, + uint8_t value) { + CLEM_SCC_LOG("SCC: WR15 %c mode %02x", ch_idx ? 'B' : 'A', value); + scc->channel[ch_idx].regs[CLEM_SCC_WR15_INT_ENABLE] = value; +} + +void clem_scc_write_switch(struct ClemensDeviceSCC *scc, struct ClemensTimeSpec *tspec, + uint8_t ioreg, uint8_t value) { + unsigned ch_idx; switch (ioreg) { case CLEM_MMIO_REG_SCC_B_CMD: case CLEM_MMIO_REG_SCC_A_CMD: + ch_idx = CLEM_MMIO_REG_SCC_A_CMD - ioreg; + if (scc->channel[ch_idx].state == CLEM_SCC_STATE_READY) { + scc->channel[ch_idx].selected_reg = value; + scc->channel[ch_idx].state = CLEM_SCC_STATE_REGISTER; + } else if (scc->channel[ch_idx].state == CLEM_SCC_STATE_REGISTER) { + // command write register + switch (scc->channel[ch_idx].selected_reg) { + case CLEM_SCC_WR0_COMMAND: + clem_scc_reg_command_wr0(scc, ch_idx, value); + break; + case CLEM_SCC_WR1_IN_TX_RX: + clem_scc_reg_interrupts_wr1(scc, ch_idx, value); + break; + case CLEM_SCC_WR2_INT_VECTOR: + clem_scc_reg_interrupt_vector_wr2(scc, value); + break; + case CLEM_SCC_WR3_RECV_CONTROL: + clem_scc_reg_recv_control_wr3(scc, tspec, ch_idx, value); + break; + case CLEM_SCC_WR4_CLOCK_DATA_RATE: + clem_scc_reg_clock_data_rate_wr4(scc, ch_idx, value); + break; + case CLEM_SCC_WR5_XMIT_CONTROL: + clem_scc_reg_xmit_control_wr5(scc, tspec, ch_idx, value); + break; + case CLEM_SCC_WR6_SYNC_CHAR_1: + scc->channel[ch_idx].regs[CLEM_SCC_WR6_SYNC_CHAR_1] = value; + CLEM_SCC_LOG("SCC: WR6 %c SYNC %02X", 'A' + ch_idx, value); + break; + case CLEM_SCC_WR7_SYNC_CHAR_2: + scc->channel[ch_idx].regs[CLEM_SCC_WR7_SYNC_CHAR_2] = value; + CLEM_SCC_LOG("SCC: WR7 %c SYNC %02X", 'A' + ch_idx, value); + break; + case CLEM_SCC_WR8_XMIT_BUFFER: + clem_scc_reg_xmit_byte(scc, ch_idx, value); + break; + case CLEM_SCC_WR9_MASTER_INT: + clem_scc_reg_master_interrupt_wr9(scc, tspec->clocks_spent, value); + break; + case CLEM_SCC_WR11_CLOCK_MODE: + clem_scc_reg_clock_mode_wr11(scc, ch_idx, value); + break; + case CLEM_SCC_WR12_TIME_CONST_LO: + scc->channel[ch_idx].regs[CLEM_SCC_WR12_TIME_CONST_LO] = value; + CLEM_SCC_LOG("SCC: WR12 %c TC LO %02x", 'A' + ch_idx, value); + break; + case CLEM_SCC_WR13_TIME_CONST_HI: + scc->channel[ch_idx].regs[CLEM_SCC_WR13_TIME_CONST_HI] = value; + CLEM_SCC_LOG("SCC: WR13 %c TC HI %02x", 'A' + ch_idx, value); + break; + case CLEM_SCC_WR14_MISC_CONTROL: + clem_scc_reg_dpll_command_wr14(scc, ch_idx, value & 0xe0); + clem_scc_reg_baud_gen_enable_wr14(scc, ch_idx, value & 0x3); + clem_scc_reg_misc_control_wr14(scc, ch_idx, value & 0x1c); + break; + case CLEM_SCC_WR15_INT_ENABLE: + clem_scc_reg_set_interrupt_enable_wr15(scc, ch_idx, value); + break; + default: + CLEM_SCC_LOG("SCC: WR%u %c <= %02x", scc->channel[ch_idx].selected_reg, + 'A' + ch_idx, value); + break; + } + scc->channel[ch_idx].selected_reg = 0x00; + scc->channel[ch_idx].state = CLEM_SCC_STATE_READY; + } break; case CLEM_MMIO_REG_SCC_B_DATA: case CLEM_MMIO_REG_SCC_A_DATA: - if (scc->state == CLEM_SCC_STATE_REGISTER_WAIT) { - scc->selected_reg[CLEM_MMIO_REG_SCC_A_DATA - ioreg] = value; - scc->state = CLEM_SCC_STATE_REGISTER_SELECTED; - } else { - scc->state = CLEM_SCC_STATE_REGISTER_WAIT; - } + ch_idx = CLEM_MMIO_REG_SCC_A_DATA - ioreg; + clem_scc_reg_xmit_byte(scc, ch_idx, value); break; } // CLEM_LOG("clem_scc: %02X <- %02X", ioreg, value); } +uint8_t clem_scc_reg_get_tx_rx_status(struct ClemensDeviceSCC *scc, unsigned ch_idx) { + struct ClemensDeviceSCCChannel *channel = &scc->channel[ch_idx]; + + return channel->rr0; +} + +uint8_t clem_scc_reg_get_error_status(struct ClemensDeviceSCC *scc, unsigned ch_idx) { + struct ClemensDeviceSCCChannel *channel = &scc->channel[ch_idx]; + channel->rr1 &= ~CLEM_SCC_RR1_FRAMING_ERROR; // errors not latched + channel->rr1 |= + channel->recv_err_queue[0]; // apply new error bits that are cleared on error_reset. + return channel->rr1; +} + +uint8_t clem_scc_recv_byte(struct ClemensDeviceSCC *scc, unsigned ch_idx, uint8_t flags) { + struct ClemensDeviceSCCChannel *channel = &scc->channel[ch_idx]; + uint8_t value = channel->recv_queue[0]; + + if (!CLEM_IS_IO_NO_OP(flags)) { + channel->recv_err_queue[0] = 0x00; + + if (channel->rx_queue_pos > 0) { + unsigned qpos; + CLEM_SCC_LOG("SCC: RR8 %c rx %02X", ch_idx + 'A', value); + for (qpos = 1; qpos < channel->rx_queue_pos; qpos++) { + channel->recv_queue[qpos - 1] = channel->recv_queue[qpos]; + channel->recv_err_queue[qpos - 1] = channel->recv_err_queue[qpos]; + } + --channel->rx_queue_pos; + } + if (channel->rx_queue_pos > 0) { + channel->rr0 |= CLEM_SCC_RR0_RECV_AVAIL; // redundant? + } else { + channel->rr0 &= ~CLEM_SCC_RR0_RECV_AVAIL; + } + } + + return value; +} + uint8_t clem_scc_read_switch(struct ClemensDeviceSCC *scc, uint8_t ioreg, uint8_t flags) { uint8_t value = 0; - bool is_noop = (flags & CLEM_OP_IO_NO_OP) != 0; + unsigned ch_idx; + switch (ioreg) { case CLEM_MMIO_REG_SCC_B_CMD: case CLEM_MMIO_REG_SCC_A_CMD: + // always read from current register - this will reset selected register + // to 0x00 which is one way for the app to sync with the SCC. + ch_idx = CLEM_MMIO_REG_SCC_A_CMD - ioreg; + if (!CLEM_IS_IO_NO_OP(flags)) { + // CLEM_LOG("SCC: Read Reg %u => ??", scc->channel[ch_idx].selected_reg); + // Some later Zilog docs mention the NMOS version will return + // an 'image' of another register - and we'll try that here. + switch (scc->channel[ch_idx].selected_reg) { + case CLEM_SCC_RR0_STATUS: + value = clem_scc_reg_get_tx_rx_status(scc, ch_idx); + break; + case CLEM_SCC_RR1_SPECIAL_RECEIVE: + value = clem_scc_reg_get_error_status(scc, ch_idx); + break; + case CLEM_SCC_RR2_INT_VECTOR: + value = scc->channel[ch_idx].regs[CLEM_SCC_WR2_INT_VECTOR]; + break; + case CLEM_SCC_RR3_INT_PENDING: + value = ch_idx == 0 ? scc->channel[ch_idx].rr3 : 0x00; + break; + case CLEM_SCC_RR12_TIME_CONST_LO: + value = scc->channel[ch_idx].regs[CLEM_SCC_WR12_TIME_CONST_LO]; + break; + case CLEM_SCC_RR8_RECV_QUEUE: + value = clem_scc_recv_byte(scc, ch_idx, flags); + break; + case 0x09: // returns an image of RR13/WR13 + case CLEM_SCC_RR13_TIME_CONST_HI: + value = scc->channel[ch_idx].regs[CLEM_SCC_WR13_TIME_CONST_HI]; + break; + case 0x0B: // returns an image of RR15/WR15 + case CLEM_SCC_RR15_INT_ENABLE: + // bits 0 and 2 are always 0 + value = scc->channel[ch_idx].regs[CLEM_SCC_WR15_INT_ENABLE] & 0xfa; + break; + default: + // CLEM_SCC_LOG("SCC: RR%u unhandled", scc->channel[ch_idx].selected_reg); + break; + } + scc->channel[ch_idx].selected_reg = 0x00; + scc->channel[ch_idx].state = CLEM_SCC_STATE_READY; + } + break; case CLEM_MMIO_REG_SCC_B_DATA: case CLEM_MMIO_REG_SCC_A_DATA: + // Remember, reading data will pop both the error and the recv bytes + // per Sec 4.2.2 of Z8530.pdf + ch_idx = CLEM_MMIO_REG_SCC_A_DATA - ioreg; + value = clem_scc_recv_byte(scc, ch_idx, flags); break; } - // if (!is_noop) { - // CLEM_LOG("clem_scc: %02X -> %02X", ioreg, value); - // } return value; } diff --git a/clem_scc.h b/clem_scc.h new file mode 100644 index 0000000..dcb178b --- /dev/null +++ b/clem_scc.h @@ -0,0 +1,179 @@ +#ifndef CLEM_SCC_H +#define CLEM_SCC_H + +#include "clem_mmio_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define CLEM_SCC_WR0_COMMAND 0x0 +#define CLEM_SCC_WR1_IN_TX_RX 0x1 +#define CLEM_SCC_WR2_INT_VECTOR 0x2 +#define CLEM_SCC_WR3_RECV_CONTROL 0x3 +#define CLEM_SCC_WR4_CLOCK_DATA_RATE 0x4 +#define CLEM_SCC_WR5_XMIT_CONTROL 0x5 +#define CLEM_SCC_WR6_SYNC_CHAR_1 0x6 +#define CLEM_SCC_WR7_SYNC_CHAR_2 0x7 +#define CLEM_SCC_WR8_XMIT_BUFFER 0x8 +#define CLEM_SCC_WR9_MASTER_INT 0x9 +#define CLEM_SCC_WR11_CLOCK_MODE 0xB +#define CLEM_SCC_WR12_TIME_CONST_LO 0xC +#define CLEM_SCC_WR13_TIME_CONST_HI 0xD +#define CLEM_SCC_WR14_MISC_CONTROL 0xE +#define CLEM_SCC_WR15_INT_ENABLE 0xF + +#define CLEM_SCC_RR0_STATUS 0x0 +#define CLEM_SCC_RR1_SPECIAL_RECEIVE 0x1 +#define CLEM_SCC_RR2_INT_VECTOR 0x2 +#define CLEM_SCC_RR3_INT_PENDING 0x3 +#define CLEM_SCC_RR8_RECV_QUEUE 0x8 +#define CLEM_SCC_RR12_TIME_CONST_LO 0xC +#define CLEM_SCC_RR13_TIME_CONST_HI 0xD +#define CLEM_SCC_RR15_INT_ENABLE 0xF + +// Command options WR0 +#define CLEM_SCC_CMD_RESET_STATUS_INT (0x2 << 3) +#define CLEM_SCC_CMD_ENABLE_RX_NEXT_INT (0x4 << 3) +#define CLEM_SCC_CMD_RESET_TX_PENDING_INT (0x5 << 3) +#define CLEM_SCC_CMD_RESET_ERROR (0x6 << 3) +#define CLEM_SCC_CMD_RESET_IUS (0x7 << 3) +#define CLEM_SCC_CMD_MASK (0x7 << 3) + +// Interrupt options WR1 +#define CLEM_SCC_ENABLE_PARITY_SPECIAL_COND 0x04 + +// Receiver options WR3 +#define CLEM_SCC_RX_ENABLE 0x01 +#define CLEM_SCC_TX_RX_AUTO_ENABLE 0x20 +#define CLEM_SCC_RX_BITS_5_OR_LESS 0x00 +#define CLEM_SCC_RX_BITS_7 0x40 +#define CLEM_SCC_RX_BITS_6 0x80 +#define CLEM_SCC_RX_BITS_8 0xc0 + +// Misc Data Format/Rate WR4 +#define CLEM_SCC_PARITY_ENABLED 0x01 +#define CLEM_SCC_PARITY_EVEN 0x02 +#define CLEM_SCC_STOP_SYNC_MODE 0x00 +#define CLEM_SCC_STOP_BIT_1 0x04 +#define CLEM_SCC_STOP_BIT_1_5 0x08 +#define CLEM_SCC_STOP_BIT_2 0x0c +#define CLEM_SCC_MONO_SYNC_MODE 0x00 +#define CLEM_SCC_BI_SYNC_MODE 0x10 +// #define CLEM_SCC_SDLC_SYNC_MODE 0x20 +#define CLEM_SCC_EXT_SYNC_MODE 0x30 +#define CLEM_SCC_CLOCK_X1 0x00 +#define CLEM_SCC_CLOCK_X16 0x40 +#define CLEM_SCC_CLOCK_X32 0x80 +#define CLEM_SCC_CLOCK_X64 0xc0 + +// Transmit protocol WR5 +#define CLEM_SCC_TX_RTS 0x02 +#define CLEM_SCC_TX_ENABLE 0x08 +#define CLEM_SCC_TX_SEND_BREAK 0x10 +#define CLEM_SCC_TX_BITS_5_OR_LESS 0x00 +#define CLEM_SCC_TX_BITS_7 0x20 +#define CLEM_SCC_TX_BITS_6 0x40 +#define CLEM_SCC_TX_BITS_8 0x60 +#define CLEM_SCC_TX_DTR 0x80 + +// WR10 +#define CLEM_SCC_SYNC_SIZE_8BIT 0x00 +#define CLEM_SCC_SYNC_SIZE_6BIT 0x01 + +// Clock Mode (WR11) +#define CLEM_SCC_CLK_TRxC_OUT_XTAL 0x00 +#define CLEM_SCC_CLK_TRxC_OUT_XMIT 0x01 +#define CLEM_SCC_CLK_TRxC_OUT_BRG 0x02 +#define CLEM_SCC_CLK_TRxC_OUT_DPLL 0x03 +#define CLEM_SCC_TRxC_OUT_ENABLE 0x04 +#define CLEM_SCC_CLK_TX_SOURCE_RTxC 0x00 +#define CLEM_SCC_CLK_TX_SOURCE_TRxC 0x08 +#define CLEM_SCC_CLK_TX_SOURCE_BRG 0x10 +#define CLEM_SCC_CLK_TX_SOURCE_DPLL 0x18 +#define CLEM_SCC_CLK_RX_SOURCE_RTxC 0x00 +#define CLEM_SCC_CLK_RX_SOURCE_TRxC 0x20 +#define CLEM_SCC_CLK_RX_SOURCE_BRG 0x40 +#define CLEM_SCC_CLK_RX_SOURCE_DPLL 0x60 +#define CLEM_SCC_CLK_XTAL_ON 0x80 + +// WR14 +#define CLEM_SCC_CLK_BRG_ON 0x01 +#define CLEM_SCC_CLK_BRG_PCLK 0x02 +#define CLEM_SCC_DTR_FUNCTION 0x04 +#define CLEM_SCC_AUTO_ECHO 0x08 +#define CLEM_SCC_LOCAL_LOOPBACK 0x10 + +// WR15 Status Interrupt Control +#define CLEM_SCC_INT_ZERO_COUNT_INT_ENABLE 0x02 +#define CLEM_SCC_INT_DCD_INT_ENABLE 0x08 +#define CLEM_SCC_INT_CTS_INT_ENABLE 0x20 +#define CLEM_SCC_INT_BREAK_INT_ENABLE 0x80 + +// RR0 +#define CLEM_SCC_RR0_RECV_AVAIL 0x01 +#define CLEM_SCC_RR0_ZERO_COUNT 0x02 +#define CLEM_SCC_RR0_TX_EMPTY 0x04 +#define CLEM_SCC_RR0_DCD_STATUS 0x08 +#define CLEM_SCC_RR0_CTS_STATUS 0x20 +#define CLEM_SCC_RR0_TX_UNDERRUN 0x40 +#define CLEM_SCC_RR0_BREAK_ABORT 0x80 + +// RR1 +#define CLEM_SCC_RR1_ALL_SENT 0x01 +#define CLEM_SCC_RR1_PARITY_ERROR 0x10 +#define CLEM_SCC_RR1_RECV_OVERRUN 0x20 +#define CLEM_SCC_RR1_FRAMING_ERROR 0x40 +#define CLEM_SCC_RR1_ERROR_MASK 0x70 + +#define CLEM_SCC_RR3_EXT_STATUS_IP_B 0x01 +#define CLEM_SCC_RR3_EXT_TX_IP_B 0x02 +#define CLEM_SCC_RR3_EXT_RX_IP_B 0x04 +#define CLEM_SCC_RR3_EXT_STATUS_IP_A 0x08 +#define CLEM_SCC_RR3_EXT_TX_IP_A 0x10 +#define CLEM_SCC_RR3_EXT_RX_IP_A 0x20 + +// Port settings +void clem_scc_channel_port_lo(struct ClemensDeviceSCCChannel *channel, uint8_t flags); +void clem_scc_channel_port_hi(struct ClemensDeviceSCCChannel *channel, uint8_t flags); + +/** + * @brief + * + * @param scc + */ +void clem_scc_reset(struct ClemensDeviceSCC *scc, struct ClemensTimeSpec *tspec); + +/** + * @brief + * + * @param scc + * @param clock + */ +void clem_scc_glu_sync(struct ClemensDeviceSCC *scc, struct ClemensClock *clock); + +/** + * @brief + * + * @param scc + * @param ioreg + * @param value + */ +void clem_scc_write_switch(struct ClemensDeviceSCC *scc, struct ClemensTimeSpec *tspec, + uint8_t ioreg, uint8_t value); + +/** + * @brief + * + * @param scc + * @param ioreg + * @param flags + * @return uint8_t + */ +uint8_t clem_scc_read_switch(struct ClemensDeviceSCC *scc, uint8_t ioreg, uint8_t flags); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/clem_shared.h b/clem_shared.h index 389c070..a798ade 100644 --- a/clem_shared.h +++ b/clem_shared.h @@ -36,11 +36,15 @@ typedef uint8_t *(*ClemensSerializerAllocateCb)(unsigned /* type */, unsigned /* * * If you divide the CLEM_CLOCKS_PHI0_CYCLE by the CLEM_CLOCKS_PHI2_FAST_CYCLE * the value will be the effective maximum clock speed in Mhz of the CPU. + * + * Ref: https://www.kansasfest.org/wp-content/uploads/2011-krue-fpi.pdf */ +#define CLEM_CLOCKS_28MHZ_CYCLE 100U // 28.636 Mhz #define CLEM_CLOCKS_14MHZ_CYCLE 200U // 14.318 Mhz #define CLEM_CLOCKS_7MHZ_CYCLE 400U // 7.159 Mhz #define CLEM_CLOCKS_4MHZ_CYCLE 700U // 4.091 Mhz +#define CLEM_CLOCKS_CREF_CYCLE 800U // 3.580 Mhz #define CLEM_CLOCKS_PHI2_FAST_CYCLE (CLEM_CLOCKS_14MHZ_CYCLE * 5) // 2.864 Mhz #define CLEM_CLOCKS_2MHZ_CYCLE (CLEM_CLOCKS_14MHZ_CYCLE * 7) // 2.045 Mhz #define CLEM_CLOCKS_Q3_CYCLE CLEM_CLOCKS_2MHZ_CYCLE // 2Mhz cycle with stretch @@ -129,6 +133,33 @@ typedef struct { const char *(*io_name)(void *context); } ClemensCard; +/* Serial port interface */ +#define CLEM_SCC_PORT_DTR 0x01 +#define CLEM_SCC_PORT_HSKI 0x02 +#define CLEM_SCC_PORT_TX_D_LO 0x04 +#define CLEM_SCC_PORT_TX_D_HI 0x08 +#define CLEM_SCC_PORT_RX_D_LO 0x10 +#define CLEM_SCC_PORT_RX_D_HI 0x20 +#define CLEM_SCC_PORT_GPI 0x40 + +/** Limit of 57600 baud is based on the maximum baud rate that can be generated + by the SCC (3.6884mhz / 16) with proven settings. It may be possible to + go higher with an accelerated GS and a x1 clock mode vs x16 as supported + on the Z8530 (though unsure of the NMOS version can operate that high.) +*/ +enum ClemensSerialBaudRate { + kClemensSerialBaudRate_None, + kClemensSerialBaudRate_300, + kClemensSerialBaudRate_1200, + kClemensSerialBaudRate_2400, + kClemensSerialBaudRate_4800, + kClemensSerialBaudRate_9600, + kClemensSerialBaudRate_19200, + kClemensSerialBaudRate_38400, + kClemensSerialBaudRate_57600, + kClemensSerialBaudRate_Clock +}; + #ifdef __cplusplus } #endif diff --git a/docs/SCC.md b/docs/SCC.md index 77f40bd..a4859bd 100644 --- a/docs/SCC.md +++ b/docs/SCC.md @@ -1,62 +1,49 @@ -# Serial Communications Chip Emulation +# SCC Zilog 8530 Emulation for the Apple IIGS -https://ia801901.us.archive.org/30/items/bitsavers_zilogdataBUSSCCZ8530SCCSerialCommunicationsControl_13081094/00-2057-03_Z8030_Z-BUS_SCC_Z8530_SCC_Serial_Communications_Controller_Technical_Manual_Sep1986.pdf +## Serial Communications -http://www.1000bit.it/support/manuali/apple/technotes/iigs/tn.iigs.018.html +### Basics + +Three simple 'wires' connecting two devices - Transmit, Receive and Ground - +represent a minimal serial connection. Ground (GND) is ignored for emulation +purposes. Transmit (TxD) and Receive (RxD) lines perform just those tasks for a +serial endpoint. + +The SCC controls the signaling on these wires with additional pins. To control +these pins, applications send commands to the SCC via I/O register access. + +The Zilog 8530 provides two serial channels A and B. On the Apple IIGS, these +channels are used for Slot 1/Printer (A) and Slot 2/Modem (B) control. + +### Control + +Basic flow control is offered through the Zilog 'Modem Control Logic' unit for +each channel. As mentioned above, these pins are both read and controlled +using the command interface. The Clemens emulator calls such interfaces, +"GLU's" - glue? - borrowed from various IIGS references. + +#### Signals +Signal names followed by emulated port pin with peripheral. +* TxD (DB8-3) - transmit bit at current baud rate +* RxD (DB8-5) - receive bit at current baud rate +* RTS (DB8-7) - request to send (send me more data so I can receive it) +* CTS (DB8-7) - clear to send (i should transmit more data when possible) +* DTR (DB8-1) - data terminal ready (signals to initiate comms) +* DSR (DB8-2) - data set ready / handshake (signals to another computer that it's ready) +* DCD (NONE) - carrier signal from a modem + - Zilog supports this for RS-232 + - IIGS port doesn't have this connection -### Frontend -* Two channels A and B -* Each channel communicates via a Command and Data register - -### Backend -* 9 read registers + 15 write registers per channel - - -## Notes - -* 2 Serial Ports on the IIgs system - * provide sync/async communications - * channel A and channel B -* 7 pins per port (+ GND = 8) - * DTR - * HSKI (handshake in) - * TX data - - * TX data + - * RX data - - * RX data + - * GPI (general purpose input) -* Apple II emulation limited - * asynchronous - * ACIA not supported (the older serial interface) -* Zilog 8530 serial communications controller -* Each port has two MMIO registers - * Command - * Data - * Table 7-11 has a list of SCC read register functions - * Table 7-12 has a list of SCC write register functions -* Controller hardware I/O 8530 - * Channel Select (A or B) - * Chip Enable - SCC is active when enabled - * Data (8-bits) bidirectional I/O - * Control/Data I/O flag - * Read flag and Write flag (combined = reset) - * Many of the following are per channel - * CTS (clear to send) to port - * DCD (data carrier detect input) - * DTR (data carrier detect output) - * RTS (request to send outputs) - * RTxC (recv/xmit clocks input) - * RxD (recv data inputs) - * SYNC (synchronization input or output) - * TRxC (xmit/recv clocks input) - * TxD (xmit data output) - * W/REQ (wait/request outputs) - -* Accessing registers - * Set current register - * Read or Write to register - * communication modes are established by programming write registers - * as data is received or transmitted, read register values may change - * \ No newline at end of file +The Zilog supports more signals that may be unused by the IIGS (i.e. like DCD.) +Implementation will depend on practical need. + +## Interrupt and Timings + + +## References + +https://ia801901.us.archive.org/30/items/bitsavers_zilogdataBUSSCCZ8530SCCSerialCommunicationsControl_13081094/00-2057-03_Z8030_Z-BUS_SCC_Z8530_SCC_Serial_Communications_Controller_Technical_Manual_Sep1986.pdf + +http://www.1000bit.it/support/manuali/apple/technotes/iigs/tn.iigs.018.html diff --git a/emulator_mmio.c b/emulator_mmio.c index 4678d60..7105ccc 100644 --- a/emulator_mmio.c +++ b/emulator_mmio.c @@ -14,6 +14,7 @@ #include "clem_debug.h" #include "clem_device.h" #include "clem_drive.h" +#include "clem_scc.h" #include "clem_util.h" #include "clem_vgc.h" @@ -499,7 +500,7 @@ void clemens_emulate_mmio(ClemensMachine *clem, ClemensMMIO *mmio) { } mmio->irq_line = (mmio->dev_adb.irq_line | mmio->dev_timer.irq_line | mmio->dev_audio.irq_line | - mmio->vgc.irq_line | card_irqs); + mmio->vgc.irq_line | mmio->dev_scc.irq_line | card_irqs); mmio->nmi_line = card_nmis; clem_iwm_speed_disk_gate(mmio, &clem->tspec); diff --git a/iocards/CMakeLists.txt b/iocards/CMakeLists.txt index 37ed9d2..e8f80a5 100644 --- a/iocards/CMakeLists.txt +++ b/iocards/CMakeLists.txt @@ -1,13 +1,16 @@ -set(IOCARDS_SOURCES - "${CMAKE_CURRENT_SOURCE_DIR}/mockingboard.c") - -add_library(clemens_65816_iocards STATIC - ${IOCARDS_SOURCES}) - get_filename_component( _CLEM_INCLUDE_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY ) +add_library(clemens_65816_iocards STATIC + mockingboard.c) + target_include_directories(clemens_65816_iocards PUBLIC $) + +add_library(clemens_65816_serial_devices STATIC + clem_peer.c) + +target_include_directories(clemens_65816_serial_devices + PUBLIC $) diff --git a/iocards/clem_peer.c b/iocards/clem_peer.c new file mode 100644 index 0000000..832f82d --- /dev/null +++ b/iocards/clem_peer.c @@ -0,0 +1,88 @@ +#include "clem_peer.h" +#include "clem_shared.h" + +#include + +/* This is generally a prototype of the eventual SCC implementation minus the + interface and more esoteric functionalty. +*/ + +static unsigned s_baud_rates[] = {0, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 0}; + +static double s_xtal_frequency = 28.636e6; + +void clem_serial_peer_init(ClemensSerialPeer *peer, struct ClemensClock *clock) { + peer->recv_queue_head = peer->recv_queue_tail = 0; + peer->send_queue_head = peer->send_queue_tail = 0; + peer->last_transact_time = clock->ts; + peer->leftover_baud_gen_clocks_dt = 0; + peer->xmit_shift_reg = 0; + peer->recv_shift_reg = 0; + clem_serial_peer_set_baud_rate(peer, kClemensSerialBaudRate_Clock); +} + +void clem_serial_peer_set_baud_rate(ClemensSerialPeer *peer, enum ClemensSerialBaudRate baud_rate) { + // tc = ( xtal / (2 * baud_rate * clock_div) ) - 2 + if (baud_rate == kClemensSerialBaudRate_Clock) { + peer->baud_gen_clocks_dt = CLEM_CLOCKS_28MHZ_CYCLE; + } else if (baud_rate == kClemensSerialBaudRate_None) { + peer->baud_gen_clocks_dt = 0; + } else { + peer->baud_gen_clocks_dt = + (s_xtal_frequency / s_baud_rates[baud_rate]) * CLEM_CLOCKS_28MHZ_CYCLE; + } +} + +unsigned clem_serial_peer_send_bytes(ClemensSerialPeer *peer, const uint8_t *bytes, + unsigned byte_cnt) { + unsigned remaining = CLEM_PERI_PEER_QUEUE_SIZE - peer->send_queue_tail; + unsigned i; + if (remaining < byte_cnt) { + for (i = peer->send_queue_head; i < peer->send_queue_tail; i++) { + peer->send_queue[i - peer->send_queue_head] = peer->send_queue[i]; + } + peer->send_queue_tail = i - peer->send_queue_head; + peer->send_queue_head = 0; + remaining = CLEM_PERI_PEER_QUEUE_SIZE; + } + for (i = 0; i < byte_cnt && remaining > 0; i++, bytes++, remaining--) { + peer->send_queue[peer->send_queue_tail++] = *bytes; + } + return i; +} + +unsigned clem_serial_peer_recv_byte(ClemensSerialPeer *peer, uint8_t *bytes, unsigned bytes_limit) { + uint8_t *bytes_end = bytes + bytes_limit; + unsigned cnt = 0; + while (bytes < bytes_end && peer->recv_queue_head < peer->recv_queue_tail) { + *(bytes++) = peer->recv_queue[peer->recv_queue_head++]; + cnt++; + } + return cnt; +} + +/* + * Starting from a no connection, we have xmit,recv queues that are controlled + * by the signals from `serial_port`. + * + * The send queue will be emptied when CTS is signaled + * The recv queue will be emptied if there is content + * + * The UART here will fill in the shift registers with start/stop/parity bits + * one bit at a time when enough time (measured by peer->baud_gen_clocks_dt) + * has passed. + * + */ +void clem_serial_peer_transact(ClemensSerialPeer *peer, struct ClemensClock *clock, + unsigned *serial_port) { + clem_clocks_duration_t dt = clock->ts - peer->last_transact_time; + + while (peer->baud_gen_clocks_dt > 0 && dt >= peer->baud_gen_clocks_dt) { + bool cts = (*serial_port & CLEM_SCC_PORT_HSKI) != 0; + + dt -= peer->baud_gen_clocks_dt; + } + + peer->leftover_baud_gen_clocks_dt = dt; + peer->last_transact_time = clock->ts; +} diff --git a/iocards/clem_peer.h b/iocards/clem_peer.h new file mode 100644 index 0000000..8d7eb4a --- /dev/null +++ b/iocards/clem_peer.h @@ -0,0 +1,57 @@ +#ifndef CLEM_PERIPHERAL_PEER_DEVICE_H +#define CLEM_PERIPHERAL_PEER_DEVICE_H + +#include "clem_shared.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define CLEM_PERI_PEER_QUEUE_SIZE 16 + +/** + * @brief Defines a peer for serial operations that can be built upon for devices + * + * The clocks timing is based on the relative clocks defined in clem_shared.h. + * For serial operations to work, both the peer and the emulator needs to run + * at the same clock rate. + */ +typedef struct { + /* time of last call to transact */ + clem_clocks_time_t last_transact_time; + /* number of clocks until the next bit is sent or read */ + clem_clocks_duration_t baud_gen_clocks_dt; + clem_clocks_duration_t leftover_baud_gen_clocks_dt; + + uint8_t send_queue[CLEM_PERI_PEER_QUEUE_SIZE]; + int send_queue_head; + int send_queue_tail; + uint8_t recv_queue[CLEM_PERI_PEER_QUEUE_SIZE]; + int recv_queue_head; + int recv_queue_tail; + + unsigned xmit_shift_reg; + unsigned recv_shift_reg; +} ClemensSerialPeer; + +void clem_serial_peer_init(ClemensSerialPeer *peer, struct ClemensClock *clock); + +void clem_serial_peer_set_baud_rate(ClemensSerialPeer *peer, enum ClemensSerialBaudRate baud_rate); + +// queues bytes +unsigned clem_serial_peer_send_bytes(ClemensSerialPeer *peer, const uint8_t *bytes, + unsigned byte_cnt); + +// returns the number of bytes received in the supplied bytes buffer (0 if none) +unsigned clem_serial_peer_recv_byte(ClemensSerialPeer* peer, uint8_t* bytes, unsigned bytes_limit); + +void clem_serial_peer_transact(ClemensSerialPeer *peer, struct ClemensClock *clock, + unsigned *serial_port); + +// TODO: serialization and unserialization + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/serializer.c b/serializer.c index 756e69c..bfccd5a 100644 --- a/serializer.c +++ b/serializer.c @@ -150,13 +150,40 @@ struct ClemensSerializerRecord kAudio[] = { CLEM_SERIALIZER_RECORD_UINT32(struct ClemensDeviceAudio, irq_line), CLEM_SERIALIZER_RECORD_EMPTY()}; +struct ClemensSerializerRecord kSCCChannel[] = { + CLEM_SERIALIZER_RECORD_UINT32(struct ClemensDeviceSCCChannel, serial_port), + CLEM_SERIALIZER_RECORD_ARRAY(struct ClemensDeviceSCCChannel, kClemensSerializerTypeUInt8, regs, + 16, 0), + CLEM_SERIALIZER_RECORD_UINT8(struct ClemensDeviceSCCChannel, rr0), + CLEM_SERIALIZER_RECORD_UINT8(struct ClemensDeviceSCCChannel, rr1), + CLEM_SERIALIZER_RECORD_UINT8(struct ClemensDeviceSCCChannel, rr3), + CLEM_SERIALIZER_RECORD_UINT8(struct ClemensDeviceSCCChannel, rr8), + CLEM_SERIALIZER_RECORD_UINT8(struct ClemensDeviceSCCChannel, selected_reg), + CLEM_SERIALIZER_RECORD_UINT8(struct ClemensDeviceSCCChannel, txd_internal), + CLEM_SERIALIZER_RECORD_UINT8(struct ClemensDeviceSCCChannel, rxd_error), + CLEM_SERIALIZER_RECORD_CLOCKS(struct ClemensDeviceSCCChannel, master_clock_ts), + CLEM_SERIALIZER_RECORD_CLOCKS(struct ClemensDeviceSCCChannel, xtal_edge_ts), + CLEM_SERIALIZER_RECORD_CLOCKS(struct ClemensDeviceSCCChannel, pclk_edge_ts), + CLEM_SERIALIZER_RECORD_DURATION(struct ClemensDeviceSCCChannel, master_clock_step), + CLEM_SERIALIZER_RECORD_ARRAY(struct ClemensDeviceSCCChannel, kClemensSerializerTypeUInt8, + recv_queue, 3, 0), + CLEM_SERIALIZER_RECORD_ARRAY(struct ClemensDeviceSCCChannel, kClemensSerializerTypeUInt8, + recv_err_queue, 3, 0), + CLEM_SERIALIZER_RECORD_UINT8(struct ClemensDeviceSCCChannel, tx_byte), + CLEM_SERIALIZER_RECORD_UINT32(struct ClemensDeviceSCCChannel, tx_register), + CLEM_SERIALIZER_RECORD_UINT32(struct ClemensDeviceSCCChannel, tx_shift_ctr), + CLEM_SERIALIZER_RECORD_UINT32(struct ClemensDeviceSCCChannel, rx_queue_pos), + CLEM_SERIALIZER_RECORD_UINT32(struct ClemensDeviceSCCChannel, rx_shift_ctr), + CLEM_SERIALIZER_RECORD_UINT32(struct ClemensDeviceSCCChannel, rx_register), + CLEM_SERIALIZER_RECORD_UINT32(struct ClemensDeviceSCCChannel, brg_counter), + CLEM_SERIALIZER_RECORD_UINT32(struct ClemensDeviceSCCChannel, state), + CLEM_SERIALIZER_RECORD_EMPTY()}; + struct ClemensSerializerRecord kSCC[] = { - CLEM_SERIALIZER_RECORD_CLOCKS(struct ClemensDeviceSCC, ts_last_frame), - CLEM_SERIALIZER_RECORD_UINT32(struct ClemensDeviceSCC, state), - CLEM_SERIALIZER_RECORD_ARRAY(struct ClemensDeviceSCC, kClemensSerializerTypeUInt32, - selected_reg, 2, 0), - CLEM_SERIALIZER_RECORD_ARRAY(struct ClemensDeviceSCC, kClemensSerializerTypeUInt8, serial, 2, - 0), + CLEM_SERIALIZER_RECORD_DURATION(struct ClemensDeviceSCC, xtal_step), + CLEM_SERIALIZER_RECORD_DURATION(struct ClemensDeviceSCC, pclk_step), + CLEM_SERIALIZER_RECORD_ARRAY_OBJECTS(struct ClemensDeviceSCC, channel, 2, + struct ClemensDeviceSCCChannel, kSCCChannel), CLEM_SERIALIZER_RECORD_UINT32(struct ClemensDeviceSCC, irq_line), CLEM_SERIALIZER_RECORD_EMPTY()}; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c540c74..85da2a7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -37,6 +37,9 @@ target_link_libraries(test_disk_2img clemens_disktypes clem_test_utils unity) add_executable(test_disk_woz test_disk_woz.c ${CLEMENS_TEST_COMMON_ASSETS}) target_link_libraries(test_disk_woz clemens_disktypes clem_test_utils unity) +add_executable(test_scc test_scc.c) +target_link_libraries(test_scc clemens_65816_serial_devices clemens_65816_mmio unity) + add_test(NAME minimal COMMAND test_emulate_minimal) add_test(NAME cpu_adc COMMAND test_adc) add_test(NAME disk_nib COMMAND test_disk_nib) @@ -44,6 +47,7 @@ add_test(NAME disk_2img COMMAND test_disk_2img) add_test(NAME disk_woz COMMAND test_disk_woz) add_test(NAME gameport COMMAND test_gameport) add_test(NAME mmio_video_switches COMMAND test_mmio_video_switches) +add_test(NAME scc COMMAND test_scc) # add_library(test_lib util.c) diff --git a/tests/test_scc.c b/tests/test_scc.c new file mode 100644 index 0000000..859f4ad --- /dev/null +++ b/tests/test_scc.c @@ -0,0 +1,62 @@ +#include "clem_mmio_types.h" +#include "clem_scc.h" +#include "emulator.h" +#include "unity.h" + +#include "clem_device.h" +#include "clem_mmio_defs.h" + +#include +#include + +static struct ClemensDeviceSCC scc_dev; + +void setUp(void) { + memset(&scc_dev, 0, sizeof(scc_dev)); + // clem_scc_reset(&scc_dev); +} + +void tearDown(void) {} + +void test_scc_reset(void) {} + +void test_scc_transmit_sync(void) {} + +void test_scc_tdd_null_modem(void) { + /* An attempt at a simple null-modem connection between the 'test' and + the SCC to iterate on the initial functionality. RTS/CTS/Tx/Rx + functionality will be tested + + Send bytes from the test bit generator to the SCC + Send bytes from the SCC to the test bit generator + + DTE/SCC PORT DCE/Peer + ============================================================ + 1. Peer -> SCC tranmission + RTS TX_D_HI ---------------------> CTS + RxD RX_D_LO <--------------------- TxD + . . + . . + + + CTS HSKI <------------------ RTS + DTR DTR ---------------------> CD + DCD GPI <--------------------- DTR + */ + + // mini_db8 = 0 + // db9 = 0 + // clem_comms_peer_init(&serial_peer); + // clem_comms_peer_queue_send_text(&serial_peer, "Hello World"); + // while clem_comms_peer_can_send(&serial_peer): + // db9 = clem_comms_peer_communicate(&serial_peer, mini_db8_to_db9(mini_db8)) + // mini_db8 = clem_scc_serial_communicate(&scc, db9_to_mini_db8(db9)) +} + +int main(void) { + UNITY_BEGIN(); + RUN_TEST(test_scc_reset); + RUN_TEST(test_scc_transmit_sync); + RUN_TEST(test_scc_tdd_null_modem); + return UNITY_END(); +}