Skip to content

SC2661_UART

Source: Verilog/Shared/support/SC2661_UART.v

Where it sits (Simulation): ND120_TOP > ND120_CORE > ND3202D > IO_37 > IO_UART_42 > SC2661_UART - instance path: CORE.CPU_BOARD.IO.UART.CHIP_32H

Used in: IO_UART_42 (all tops)

Contains: no other modules.

Module hierarchy - All modules

SC2661_UART symbol

Schematic

Drawn from the Verilog: the yosys netlist of the Simulation (Verilator) build, instance CORE.CPU_BOARD.IO.UART.CHIP_32H. Sub-modules are boxes (click the picture to open it full size; there every sub-module box links to its page, and every wire shows its Verilog name).

SC2661_UART schematic

Description

SC2661 UART The SC2661 is a UART (Universal Asynchronous Receiver/Transmitter) chip Last reviewed: 9-FEB-2025 Ronny Hansen

Ports

Direction Width Name Description
input 1 sysclk System clock in FPGA
input 1 sys_rst_n (active low) System reset in FPGA
input 1 BAUD_9600 1 = 9600 baud; 0 = build default (UART_BAUD_RATE, normally 115200) - runtime select
input [1:0] ADDRESS Address lines (used to select internal EPCI registers)
input 1 BRCLK Baud rate clock - Comes from the IO_DCD module. 4.9152Mhz
input 1 CE_n (active low) Chip enable (negated)
input 1 CTS_n (active low) Clear to send (negated)
input 1 DCD_n (active low) Data Carrier Detect (negated)
input 1 DSR_n (active low) Data Set Ready (negated)
input 1 READ_n (active low) Write /Read
input 1 RESET Reset - A high on this performs a master reset of the chip
input 1 RXC_n (active low) Receiver Clock (used for SYNC, and not implemented)
input 1 RXD Receive Data
input 1 TXC_n (active low) Transmitter Clock (used for SYNC, and not implemented)
input [7:0] D
output [7:0] D_OUT
output 1 DTR_n (active low) Data Terminal Ready
output 1 RTS_n (active low) Request to Send
output 1 RXDRDY_n (active low) Receive Data Ready (complement of status register bit SR1)
output 1 TXD Transmit Data
output 1 TXDRDY_n (active low) Transmit Data Ready
output 1 TXEMT_n (active low) Transmit Empty (complement of status register bit SR2)

Verilog source

Verilog/Shared/support/SC2661_UART.v on GitHub.

Show the Verilog of SC2661_UART (658 lines)
/****************************************************************************
** SC2661 UART                                                             **
**                                                                         **
** The SC2661 is a UART (Universal Asynchronous Receiver/Transmitter) chip **
**                                                                         **
** Last reviewed: 9-FEB-2025                                               **
** Ronny Hansen                                                            **
*****************************************************************************/

//
// SET YOUR TERMINAL TO 8N1. NOT 7E1.
//
// This implementation is FIXED at 8 data bits, no parity, 1 stop bit. Only the baud rate is
// configurable (BOARD_CLK_FREQ / UART_BAUD_RATE below, default 115200). The real chip's mode
// registers - which is where character length and parity would be selected - are not
// implemented; see the comment where they would have been.
//
// The state machines are the proof, not just that comment: TX_STATE_WRITE shifts out bits 0..7
// and goes straight to TX_STATE_STOP_BIT, RX_STATE_READ shifts in bits 0..7 and goes straight to
// RX_STATE_STOP_BIT. Neither has a parity state, and the word "parity" appears nowhere in this
// file. No parity bit is generated, and none is checked.
//
// WHY THIS WARNING IS HERE, 30-AUG-2026: a PC was set to 7E1 against this UART because the
// datasheet and HARDWARE.md both describe what the REAL chip could be programmed to do. The PC
// then validated a parity bit that is never sent, and the characters it judged bad were replaced
// with '?' scattered through the text - which read like the ND was sending them. An evening went
// into finding that. The full account, with the measurements, is in HARDWARE.md under
// "Serial Interface (UART)".
//

//
// Documentation
//
// http://www.norsk-data.com/hardware/nd-100/nd-350104.html
//
// SCN2661A UART
// http://www.norsk-data.com/library/libother/extern/SCN2661.pdf
//
// Enhanced Programmable Communication Interface EPCI
// https://datasheetspdf.com/pdf-file/1412058/SMSC/COM2661-3/1
//
// Note: Not all functionality is implemented. Just enough to have a simple UART interface for the ND-120 CPU

module SC2661_UART (
    input sysclk,    // System clock in FPGA
    input sys_rst_n, // System reset in FPGA
    input BAUD_9600, //! 1 = 9600 baud; 0 = build default (UART_BAUD_RATE, normally 115200) - runtime select

    input [1:0] ADDRESS,  // Address lines (used to select internal EPCI registers)
    input BRCLK,  // Baud rate clock - Comes from the IO_DCD module. 4.9152Mhz
    input CE_n,  // Chip enable (negated)
    input CTS_n,  // Clear to send (negated)
    input DCD_n,  // Data Carrier Detect (negated)
    input DSR_n,  // Data Set Ready (negated)
    input READ_n,  // Write /Read
    input RESET,  // Reset - A high on this performs a master reset of the chip
    input RXC_n,  // Receiver Clock (used for SYNC, and not implemented)
    input RXD,  // Receive Data
    input TXC_n,  // Transmitter Clock (used for SYNC, and not implemented)

    input  [7:0] D,
    output [7:0] D_OUT,

    output DTR_n,     // Data Terminal Ready
    output RTS_n,     // Request to Send
    output RXDRDY_n,  // Receive Data Ready (complement of status register bit SR1)
    output TXD,       // Transmit Data
    output TXDRDY_n,  // Transmit Data Ready
    output TXEMT_n    // Transmit Empty (complement of status register bit SR2)
);


  /*******************************************************************************
   ** The wires are defined here                                                 **
   *******************************************************************************/

  /* verilator lint_off UNUSEDSIGNAL */
  wire s_brkclk;
  wire s_txc_n;
  wire s_rxc_n;
  wire s_cts_n;
  /* verilator lint_on UNUSEDSIGNAL */

  wire s_ce_n;

  wire s_dcd_n;
  wire s_dsr_n;
  wire s_dtr_n;
  wire s_read_n;
  wire s_reset;
  wire s_rts_n;

  wire s_rxd;
  wire s_rxrdy_n;

  wire s_txd;
  wire s_txemt_n;
  wire s_txrdy_n;

  wire [1:0] s_address;
  wire [7:0] s_data_in;



  /*******************************************************************************
   ** Command register bits                                                      **
   *******************************************************************************/

  wire cmd_txEnabled;
  wire cmd_forceDTRLow;
  wire cmd_rxEnabled;

  /* verilator lint_off UNUSEDSIGNAL */
  wire cmd_CR3;  // ASYNC: 0=Normal, 1=Force Break. SYNC: 0=Normal, 1=Send DLE (not implemented)
  /* verilator lint_on UNUSEDSIGNAL */

  wire cmd_resetError;
  wire cmd_forceRTSLow;
  wire [1:0] cmd_OperatingMode;

  //localparam cmd_OperatingMode_NORMAL = 2'b00; // not used..in code.. yet
  //localparam cmd_OperatingMode_ASYNC = 2'b01;  // not used..in code.. yet

  localparam cmd_OperatingMode_LocalLoopback = 2'b10;
  localparam cmd_OperatingMode_RemoteLoopback = 2'b11;




  /*******************************************************************************
   ** Mode register 1 and 2 bits                                                 **
   *******************************************************************************/
  // Not implemented, we use constant 9600 8N1, or later 115200 8N1

  /*******************************************************************************
   ** State machine for the receiver and transmitter                             **
   *******************************************************************************/

  localparam RX_STATE_IDLE = 3'b000;  // 0 
  localparam RX_STATE_START_BIT = 3'b001;  // 1
  localparam RX_STATE_READ_WAIT = 3'b010;  // 2
  localparam RX_STATE_READ = 3'b011;  // 3
  localparam RX_STATE_STOP_BIT = 3'b101;  // 4
  localparam RX_STATE_DONE = 3'b110;  // 5

  localparam TX_STATE_IDLE = 3'b000;  //  0 
  localparam TX_STATE_START_BIT = 3'b001;  // 1
  localparam TX_STATE_WRITE = 3'b010;  // 2
  localparam TX_STATE_STOP_BIT = 3'b011;  // 3
  localparam TX_STATE_DONE = 3'b100;  //  4


  // Baud rate timing: DELAY_FRAMES = clock cycles per bit
  // Set via defines in Makefile/project:
  //   -DBOARD_CLK_FREQ=100000000 -DUART_BAUD_RATE=115200
  //
  // Examples:
  //   100 MHz / 115200 = 868 (Basys3/Arty)
  //    27 MHz / 115200 = 234 (Tang Nano 9K/20K)
  //    50 MHz / 115200 = 434 (50 MHz board)

`ifndef BOARD_CLK_FREQ
  `define BOARD_CLK_FREQ 100_000_000  // Default: 100 MHz (Basys3/Arty)
`endif
`ifndef UART_BAUD_RATE
  `define UART_BAUD_RATE 115_200      // Default: 115200 baud
`endif

`ifdef ND120_UART_DELAY_FRAMES
  // Explicit override: real-timing UART in simulation. Set to the silicon
  // clocks-per-bit ratio (Nexys 16.67 MHz / 9600 = 1736) to reproduce
  // polling-loop pacing that the fast sim UART hides - the FILSYS
  // "DEVICE NEVER READY" retry path is only reachable when the TX-ready
  // poll actually spins (24-AUG LIST-FILE-NAMES campaign).
  localparam DELAY_DEFAULT = `ND120_UART_DELAY_FRAMES;
`elsif VERILATOR_SIM
  localparam DELAY_DEFAULT = 32'd16;   // Fast for simulation
`else
  localparam DELAY_DEFAULT = `BOARD_CLK_FREQ / `UART_BAUD_RATE;
`endif
`ifdef VERILATOR_SIM
  localparam DELAY_9600 = 32'd16;             // keep sim fast regardless of the switch
`else
  localparam DELAY_9600 = `BOARD_CLK_FREQ / 9600;
`endif
  //! Runtime baud select (BAUD_9600 input). High -> 9600, low -> the build
  //! default (UART_BAUD_RATE, normally 115200). A board switch can drop the
  //! console to 9600 for software that cannot take 115200, with no rebuild.
  //! The microcode baud thumbwheel is unaffected - only this divisor changes.
  wire [31:0] DELAY_FRAMES    = BAUD_9600 ? DELAY_9600[31:0] : DELAY_DEFAULT[31:0];
  wire [31:0] HALF_DELAY_WAIT = (DELAY_FRAMES >> 1);


  // Chip Registers
  reg  [ 7:0] regDataOut;
  reg  [ 7:0] regTransmitHoldingRegister;
  reg  [ 7:0] regStatusRegister;
  reg  [ 7:0] regModeRegister;
  reg  [ 7:0] regCommandRegister;


  // Transmitter local variables
  reg  [ 2:0] txState;
  reg  [31:0] txCounter;
  reg  [ 2:0] txBitNumber;
  reg         txBit;
  reg         regDataInSendRegister;  // 1=Data in send register (=>TxEmpty =0)

  // Receiver local variables
  reg  [ 2:0] rxState = 0;
  reg  [31:0] rxCounter = 0;
  reg  [ 2:0] rxBitNumber = 0;
  //! Live shift-in register for the byte currently arriving. Separate from
  //! the RX FIFO below on purpose - see the overrun fix note at the
  //! receiver state machine below (31-AUG-2026).
  reg  [ 7:0] regRxShift = 0;

  //! 16-byte RX FIFO between the receiver and the CPU-visible read port
  //! (added 31-AUG-2026, after the atomic-transfer fix above). The real
  //! 2661 holds exactly one received byte - no queue - and until now this
  //! model matched that. But a TDV2200 keyboard escape sequence can be up
  //! to 6 bytes with NO inter-byte gap (see key_tdv2200.v), and at 115200
  //! baud (~87us/byte) that arrives faster than SINTRAN's interrupt
  //! handler always keeps up with a single holding register: measured on
  //! real hardware 31-AUG-2026, Alt+H (meant to send ESC[46_) landed as
  //! just "6_" - each earlier byte was overwritten by the next one before
  //! the CPU's read reached it, exactly the single-register overrun this
  //! FIFO exists to absorb. Software sees NO register-level difference -
  //! address 0 is still one byte, SR1/RXRDY and SR4/Overrun mean the same
  //! things - only WHEN Overrun actually fires changes: past 16 unread
  //! bytes now, not past 1. Overrun policy: an arriving byte is dropped
  //! (not the oldest queued one) when the FIFO is full, so a burst that
  //! outruns the FIFO still preserves the earliest bytes, which is what a
  //! catching-up interrupt handler needs most - the OPPOSITE of the old
  //! single-register behavior (which always kept the newest byte). This is
  //! a deliberate departure from real 2661 silicon, not an authenticity
  //! bug - the user asked for it after this exact failure.
  reg  [ 7:0] s_rx_fifo[0:15];
  reg  [ 4:0] s_rx_wptr = 5'd0;
  reg  [ 4:0] s_rx_rptr = 5'd0;
  wire        s_rx_empty = (s_rx_wptr == s_rx_rptr);
  wire        s_rx_full  = (s_rx_wptr[4] != s_rx_rptr[4]) && (s_rx_wptr[3:0] == s_rx_rptr[3:0]);

  wire        receiver_input;

  /*******************************************************************************
   ** Here all input connections are defined                                     **
   *******************************************************************************/
  assign s_address = ADDRESS;
  assign s_data_in = D;

  assign s_brkclk = BRCLK;
  assign s_ce_n = CE_n;
  assign s_cts_n = CTS_n;
  assign s_dcd_n = DCD_n;
  assign s_dsr_n = DSR_n;
  assign s_read_n = READ_n;
  assign s_reset = RESET;
  assign s_rxc_n = RXC_n;
  assign s_rxd = RXD;
  assign s_txc_n = TXC_n;

  /*******************************************************************************
   ** Here all output connections are defined                                    **
   *******************************************************************************/

  // output pins
  assign s_dtr_n = !cmd_forceDTRLow;
  assign s_rts_n = !cmd_forceRTSLow;

  assign DTR_n = s_dtr_n;
  assign RTS_n = s_rts_n;
  assign RXDRDY_n = s_rxrdy_n;
  assign TXD = s_txd;
  assign TXDRDY_n = s_txrdy_n;
  assign TXEMT_n = s_txemt_n;



  assign D_OUT = (!s_ce_n & !s_read_n) ? regDataOut : 8'b0;
  /*
    /TxRDY

    This output is the complement of status register bit SR0.
    When Low, it indicates that the transmit data holding register (THR) is ready to accept a data character from the CPU.
    It goes High when the data character is loaded.
    This output is valid only when the transmitter is enabled. It is an open-drain output which can be used as an interrupt to the CPU.
   */
  assign s_txrdy_n = cmd_txEnabled ? ~regStatusRegister[0] : 1'b1;

  // A THR write happening THIS cycle (address 0, write, chip enabled, not yet
  // executed). Used to drop TBMT (status[0]) the SAME cycle the CPU loads the
  // holding register, so an interrupt driven off TBMT (BINT10 = IOC2 & TBMT,
  // level-sensitive) deasserts before the handler can re-enter. Without this
  // the level-10 "dummy output" stress floods and overwrites THR, because the
  // old code cleared TBMT only later in the TX state machine. Polled output
  // never saw it (software re-reads status after the TX machine has advanced).
  wire s_thr_write = !s_ce_n & !regCommandExecuted & s_read_n & (s_address == 2'b00);
  /*
    /RxRDY

    This output is the complement of status register bit SR1.

    When Low, it indicates that the receive data holding register (RHR) has a character ready for input to the CPU.
    It goes High when the RHR is read by the CPU, and also when the receiver is disabled.
    It is an open-drain output which can be used as an interrupt to the CPU

   */
  assign s_rxrdy_n = cmd_rxEnabled ? ~regStatusRegister[1] : 1'b1;

  //assign s_txemt_n = regDataInSendRegister;
  assign s_txemt_n = ~regStatusRegister[2];  // When SR2 is set, the /TxEMT/DSCHG output is Low


  // In Local Loopback - The transmitter output is connected to the receiver input.
  assign receiver_input = (cmd_OperatingMode == cmd_OperatingMode_LocalLoopback) ? s_txd : s_rxd;


  // Command Register helper bits
  assign cmd_txEnabled       = regCommandRegister[0];      // Transmit Control bit: 0 = Disable transmitter, 1 = Enable transmitter
  assign cmd_forceDTRLow     = regCommandRegister[1];      // 0 = Force /DTR output high, 1= Force /DTR output low
  assign cmd_rxEnabled       = regCommandRegister[2];      // Receive Control bit: 0 = Disable receiver, 1 = Enable receiver
  assign cmd_CR3             = regCommandRegister[3];      // Not used?
  assign cmd_resetError      = regCommandRegister[4];      // 1=Reset error flag in status (FE;OD; PE/DLE detect), 0=normal (no effect?)
  assign cmd_forceRTSLow     = regCommandRegister[5];      // 0 = Force /RTS output high, 1= Force /RTS output low
  assign cmd_OperatingMode   = regCommandRegister[7:6];    // Operating Mode bits. 00 = Normal operation, 01= Async (Automatic Echo mode), Synch: SYN AND/OR DLE STRIPPING MODE, 01 = LOCAL LOOPBACK, 11=REMOTE LOOPBACK


  reg regCommandExecuted;  // Flag set when read/write operation has been executed


  // FPGA timing fix (2026-07-06): the UART formerly clocked on ~sysclk (the
  // falling edge). That made EVERY UART output path a half-cycle path on FPGA
  // (regDataOut fans out through the IDB into the bus arbiter/MAC), which alone
  // accounted for 608 of 637 failing endpoints at 39 MHz. Clock on the normal
  // rising edge instead: with non-blocking assignments this is race-free in sim
  // (Verilator evaluates RHS on pre-edge values) and turns those half-cycle
  // paths into full-cycle paths on FPGA. Re-validated in runSim (OPCOM).
  wire uart_sysclk = sysclk;

  assign s_txd = txBit;

  // Clear everything on reset
  //always @(posedge RESET or posedge BRCLK) begin
  always @(posedge uart_sysclk) begin

    // Reset UART ?
    if (RESET | !sys_rst_n) begin
        //$display("Time: %0t | UART RESET!", $time);  //  debug

        regRxShift <= 8'b0;
        s_rx_wptr <= 5'd0;
        s_rx_rptr <= 5'd0;
        regTransmitHoldingRegister <= 8'b0;
        regStatusRegister <= 8'b00000101; // TX empty: THR(bit0)=1 AND TxEMT(bit2)=1 (idle)
        regModeRegister <= 8'b0;
        regCommandRegister <= 8'b0;
        regDataOut <= 8'b0;
        regDataInSendRegister <= 0;
        rxState <= RX_STATE_IDLE;
        txState <= TX_STATE_IDLE;
        regCommandExecuted <=0;
        txBit <= 1; // After reset, set TX signl to MARK
      end else begin
      // Latch Address and Data
        if (CE_n) begin
          // Chip is not enabled
          regCommandExecuted <= 0;  // Clear signal that address & data is latched
        end else begin

          //if (!RESET && !CE_n && !regCommandExecuted) begin  // _NOT RESET_ AND _CHIP ENABLED_ (and command not already executed)        
          if (!regCommandExecuted) begin
            // Read and Write to registers
            if (cmd_rxEnabled|cmd_txEnabled) begin // Only update status register SR2 if RX or TX is enabled              
                if ((regStatusRegister[6] == s_dcd_n) | (regStatusRegister[7] == s_dsr_n))
                regStatusRegister[2] <= 1; // Detected change in DSR or DCD   //SR2: 0=Normal, 1=Change in /DSR or /DCD or transmit shift register is empty
            end

            regStatusRegister[6] <= !s_dcd_n;  // DCD - 0=/DCD input is high. 1=/DCD input is low
            regStatusRegister[7] <= !s_dsr_n;  // DSR - 0=/DSR input is high. 1=/DSR input is low


            if (s_read_n) begin  // write to registers
              //$display("Time: %0t | UART Write=> Address: %h | Data: %h", $time, s_address, D);

              case (s_address)
                2'b00: begin
                  //Write to transmit holding register
                  regTransmitHoldingRegister <= s_data_in;  // Write transmit holding register
                  regDataInSendRegister <= 1;  // Send data to transmitter
                  regStatusRegister[0] <= 0;  // THR now BUSY -> TBMT drops same cycle
                  regStatusRegister[2] <= 0;  // TxEMT low (a character is pending)
                end

                2'b01: begin
                  regStatusRegister <= s_data_in;  // Write SYN1/SYN2/DLE registers
                end

                2'b10: begin
                  regModeRegister <= s_data_in;  // Write mode register 1 and 2
                end

                2'b11: begin
                  regCommandRegister <= s_data_in;  // Write command register

                  if (!cmd_rxEnabled) begin
                    regStatusRegister[1] <= 0;     // 0=Receive Holding Register Empty (Cleared if RX is disabled)
                  end

                  if (cmd_resetError)
                  begin
                    regStatusRegister[3] <= 0;  // 0=Clear Parity Error
                    regStatusRegister[4] <= 0;  // 0=Clear Overrun Error
                    regStatusRegister[5] <= 0;  // 0=Clear Frame Error
                  end
                end
                  default: ;  // Undefined state
              endcase
            end else begin
              // read
              regDataOut <=
                  (s_address == 2'b00) ? s_rx_fifo[s_rx_rptr[3:0]] :
                  (s_address == 2'b01) ? regStatusRegister         :
                  (s_address == 2'b10) ? regModeRegister           :
                  (s_address == 2'b11) ? regCommandRegister        : 8'b0;

              case (s_address)
                  2'b00: begin
                    // Pop the RX FIFO - see its declaration for why there is
                    // one now. RXRDY (SR1) only drops when this was the
                    // LAST queued byte, mirroring the real chip's "any byte
                    // ready" meaning rather than "just read one".
                    if (!s_rx_empty) begin
                      s_rx_rptr <= s_rx_rptr + 5'd1;
                      if (s_rx_wptr == s_rx_rptr + 5'd1) regStatusRegister[1] <= 0;
                    end
                  end
                  2'b01: begin
                    //regDataOut = regStatusRegister;  // Read status register
                    // TxEMT (bit2) is a LEVEL status (transmit shift register empty).
                    // Do NOT read-clear it: OPCOM polls it to know the buffer drained,
                    // and read-clearing made it stall waiting for it to re-assert
                    // (2026-07-07). SR2 is now driven as a level by the TX state machine.
                    // regStatusRegister[2] <= 0;  // (was: read-clear SR2 -- removed)
                  end
                  //2'b10:   regDataOut = regModeRegister;  // Read mode register 1 and 2
                  //2'b11:   regDataOut = regCommandRegister;  // Read command register
                  default: ;  // Undefined state
              endcase
              //$display("Time: %0t | UART READ <= Address: %h | Data: %h", $time, s_address, regDataOut);
            end
          end

          // Mark this command as executed until next Chip Select
          regCommandExecuted <=1;
        end



        // Receiver state machine
        // ----------------------
        // The 68661 is conditioned to receiver data when the DCD input is Low and the RxEN bit in the commands register is true.
        // In this code we just receive when the RxEN bit is set. (Ignore DCD input)
        if (!cmd_rxEnabled) begin
          rxState <= RX_STATE_IDLE;
        end else begin
          case (rxState)
            RX_STATE_IDLE: begin
              if (receiver_input == 0) begin
                // OVERRUN FIX (31-AUG-2026): this used to clear
                // regReceiveHoldingRegister right here, at the START of the new
                // byte - the SAME register the CPU reads directly. If the CPU
                // had not yet read the PREVIOUS byte (RXRDY still 1), that byte
                // was destroyed immediately, and any CPU read that landed while
                // the new byte was still shifting in (RX_STATE_READ, below) saw
                // a torn, partially-shifted value that matched neither the old
                // nor the new byte - not a lost/skipped character but a wrong
                // one. Measured 30-AUG-2026: sending "PED\r" over a real 115200
                // serial link with no gap between characters landed as a single
                // byte 0x28 '(' at the ND-120 - not P, E, D or CR, exactly the
                // signature of a mid-shift snapshot.
                //
                // The fix shifts the new byte into regRxShift (below), which
                // the CPU cannot see, and only pushes a COMPLETE byte into
                // the RX FIFO once at RX_STATE_STOP_BIT -> DONE (the FIFO
                // itself came later, 31-AUG-2026 - see its declaration).
                // The old byte therefore stays intact and readable for the
                // CPU right up until the instant it is genuinely overwritten -
                // matching how the real chip's overrun behaviour is documented
                // (previous byte lost, but never a torn value) - and that is
                // also the moment overrun is now actually flagged; setting the
                // flag here at start-of-frame was too early and, combined with
                // the immediate clear above, is what let the corruption reach
                // the CPU instead of just losing the earlier character cleanly.
                rxState              <= RX_STATE_START_BIT;
                //$display("-> RX START BIT");

                regRxShift  <= 0;
                rxCounter   <= 1;
                rxBitNumber <= 0;
              end
            end
            RX_STATE_START_BIT: begin
              if (rxCounter == HALF_DELAY_WAIT) begin
                rxState   <= RX_STATE_READ_WAIT;
                //$display("-> RX READ WAIT");
                rxCounter <= 1;
              end else rxCounter <= rxCounter + 1;
            end
            RX_STATE_READ_WAIT: begin
              rxCounter <= rxCounter + 1;
              if ((rxCounter + 1) == DELAY_FRAMES) begin
                rxState <= RX_STATE_READ;
                //$display("-> RX STATE READ");
              end
            end
            RX_STATE_READ: begin
              rxCounter <= 1;
              regRxShift <= {
                receiver_input, regRxShift[7:1]
              };  // Shift right and insert s_rxt at MSB. Live shift register,
                  // not CPU-visible - see the overrun fix note in RX_STATE_IDLE.
              rxBitNumber <= rxBitNumber + 1;
              //$display("-> RX STATE READ bit %d",receiver_input);

              if (rxBitNumber == 3'b111) begin
                rxState <= RX_STATE_STOP_BIT;
                //$display("-> RX STATE STOP BIT");
              end else begin
                rxState <= RX_STATE_READ_WAIT;
                //$display("-> RX STATE READ WAIT");
              end
            end
            RX_STATE_STOP_BIT: begin
              rxCounter <= rxCounter + 1;
              if ((rxCounter + 1) == DELAY_FRAMES) begin
                rxState <= RX_STATE_DONE;
                //$display("-> RX STATE DONE");
                rxCounter <= 0;

                // Atomic transfer of a COMPLETE byte into the RX FIFO - the
                // only place a byte is pushed. If the FIFO is already full
                // (16 unread bytes backed up), THIS byte is lost, cleanly,
                // and Overrun is flagged here - the true moment data is
                // discarded. See the fix notes at RX_STATE_IDLE and the
                // FIFO's declaration.
                if (s_rx_full) begin
                  regStatusRegister[4] <= 1;  // Overrun: 0=Normal, 1=Overrun
                end else begin
                  s_rx_fifo[s_rx_wptr[3:0]] <= regRxShift;
                  s_rx_wptr <= s_rx_wptr + 5'd1;
                  regStatusRegister[1] <= 1;  // Set RXRDY
                end
              end
            end
            RX_STATE_DONE: begin
              rxState <= RX_STATE_IDLE;
              //$display("-> RX STATE IDLE %h", regRxShift);
              //$display("-> RX READY_n FLAG %d",  s_rxrdy_n);

              // LOOPBACK?
              if (cmd_OperatingMode == cmd_OperatingMode_RemoteLoopback) begin
                // Data assembled by the receiver are automatically placed in the
                // transmit holding register and retransmitted by the transmitter on the TxD output.
                // regRxShift still holds the byte just completed - the FIFO
                // push above may have already advanced past it in the queue.
                //$display("RX -> TX LOOPBACK");
                regTransmitHoldingRegister <= regRxShift; // Write rx holding register
                regDataInSendRegister <= 1;  // Send data to transmitter
              end
            end
            default: begin
              rxState <= RX_STATE_IDLE;  // Very unexpected, go to IDLE
            end
          endcase
        end


  // Transmitter state machine
  // -------------------------
  // The EPCI is conditioned to transmit data when the CTS input is Low and the TxEN command register bit is set.
  // In this code we just transmit when the TxEN command register bit is set. (Ignore CTS input)


        // TxEN=0 gates STARTING a character, nothing else (real 2661
        // behavior: "the transmitter completes the character in progress",
        // and the THR content is never destroyed by disabling).
        // The old code here reset the whole TX machine on !TxEN:
        //   - a character in flight was chopped -> misframed garbage on the
        //     console whenever software wrote the command register during
        //     output (FILSYS does, every status poll);
        //   - regDataInSendRegister was cleared -> a THR character written
        //     in the same window was stranded forever (measured 24-AUG in
        //     the dmaSim real-timing LFN run: txhold=3E '>' pending,
        //     insend=0, TX idle, status claiming ready - console dead).
        // See fpga/nexys4ddr/HANDOFF-floppy-dma-investigation.md 24-AUG (git c4896a4).
        if (!cmd_txEnabled && txState == TX_STATE_IDLE) begin
          txBit <= 1;          // hold MARK while disabled and idle
          txCounter <= 0;      // pending THR (if any) waits for TxEN
        end else begin
          case (txState)
            TX_STATE_IDLE: begin
              if (regDataInSendRegister) begin
                regStatusRegister[0] <= 0;  // 0=Transmit Holding Register BUSY
                regStatusRegister[2] <= 0;  // 0=Transmit Shift Register BUSY (TxEMT low)
                txState              <= TX_STATE_START_BIT;
                txCounter            <= 0;
              end else if (!s_thr_write) begin
                // Do NOT re-assert THR-empty on the cycle a write lands, or
                // the write's status[0]<=0 above would be overridden and TBMT
                // would stay high for a cycle (the level-10 re-entry bug).
                txBit <= 1;
                regStatusRegister[0] <= 1; // tx empty (THR empty, TxRDY)
                regStatusRegister[2] <= 1; // TxEMT: shift register empty (level, idle)
              end
            end
            TX_STATE_START_BIT: begin
              txBit <= 0;
              if ((txCounter + 1) == DELAY_FRAMES) begin
                txState <= TX_STATE_WRITE;
                txBitNumber <= 0;
                txCounter <= 0;
              end else txCounter <= txCounter + 1;
            end
            TX_STATE_WRITE: begin
              txBit <= regTransmitHoldingRegister[txBitNumber];
              if ((txCounter + 1) == DELAY_FRAMES) begin
                if (txBitNumber == 3'b111) begin
                  txState <= TX_STATE_STOP_BIT;
                end else begin
                  txState <= TX_STATE_WRITE;
                  txBitNumber <= txBitNumber + 1;
                end
                txCounter <= 0;
              end else txCounter <= txCounter + 1;
            end
            TX_STATE_STOP_BIT: begin
              txBit <= 1;
              if ((txCounter + 1) == DELAY_FRAMES) begin
                txState   <= TX_STATE_DONE;
                txCounter <= 0;
              end else txCounter <= txCounter + 1;
            end
            TX_STATE_DONE: begin
              regDataInSendRegister <= 0;
              regStatusRegister[0] <= 1;  //1=Transmit Holding Register Empty
              regStatusRegister[2] <= 1;  // TxEMT: 1=Transmit shift register empty
              txState <= TX_STATE_IDLE;
            end
            default: begin
              txState <= TX_STATE_IDLE;  // Very unexpected, go to IDLE
            end
          endcase
        end
    end
  end

endmodule