This change updates the 3B2 README.md file, and fixes all line endings on 3B2 source files.
1121 lines
35 KiB
C
1121 lines
35 KiB
C
/* 3b2_ni.c: CM195A Network Interface CIO Card
|
|
|
|
Copyright (c) 2018-2022, Seth J. Morabito
|
|
|
|
Permission is hereby granted, free of charge, to any person
|
|
obtaining a copy of this software and associated documentation
|
|
files (the "Software"), to deal in the Software without
|
|
restriction, including without limitation the rights to use, copy,
|
|
modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be
|
|
included in all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
|
|
Except as contained in this notice, the name of the author shall
|
|
not be used in advertising or otherwise to promote the sale, use or
|
|
other dealings in this Software without prior written authorization
|
|
from the author.
|
|
*/
|
|
|
|
/*
|
|
* NI is an intelligent feature card for the 3B2 that provides a
|
|
* 10BASE5 Ethernet interface.
|
|
|
|
* Overview
|
|
* --------
|
|
*
|
|
* The NI board is based on the Common I/O (CIO) platform. Like other
|
|
* CIO boards, it uses an 80186 embedded processor. The board and the
|
|
* 3B2 host communicate by reading and writing to the 3B2's main
|
|
* memory at locations established by the host via a series of job
|
|
* request and job completion queues. Only three interrupts are used:
|
|
* Two interrupts (80186 interrupts INT0 and INT1) are triggered by
|
|
* the 3B2 and tell the card when work is available in the request
|
|
* queue. One WE32100 interrupt (at a negotiated vector and predefined
|
|
* IPL) is used by the CIO board to tell the 3B2 that a new entry is
|
|
* available in the completion queue.
|
|
*
|
|
* The on-board ROM does not contain the full firmware required to
|
|
* perform all application-specific work. Rather, it is used only to
|
|
* bootstrap the 80186 and provide essential communication between the
|
|
* 3B2 host and the board's internal RAM. During initialization, the
|
|
* host must upload application-specific code to the board's RAM and
|
|
* cause the board to start running it. This is known as
|
|
* "pumping". The 80186 binary code for the NI board under System V
|
|
* Release 3 is stored in the file `/lib/pump/ni`
|
|
*
|
|
* Implementation Details
|
|
* ----------------------
|
|
*
|
|
* The 10BASE5 interface on the NI board is driven by an Intel 82586
|
|
* IEEE 802.3 LAN Coprocessor, controlled by the board's 80186
|
|
* CPU. The 82586 is completely opaque to the host due to the nature
|
|
* of the CIO protocol. Nevertheless, an attempt is made to simulate
|
|
* the behavior of the 82586 where appropriate and possible.
|
|
*
|
|
* The NI board uses a sanity timer to occasionally write a watchdog
|
|
* or heartbeat entry into the completion queue, indicating that the
|
|
* Ethernet interface is still alive and that all is well. If the UNIX
|
|
* driver has not seen this heartbeat after approximately 10 seconds,
|
|
* it will consider the board to be in an "DOWN" state and send it a
|
|
* TERM ioctl.
|
|
*
|
|
* The NI board does behave differently from the other CIO boards in
|
|
* one respect: Unlike other CIO boards, the NI board takes jobs from
|
|
* its two Packet Receive CIO request queues by polling them, and then
|
|
* stores the taken jobs in a small 4-entry internal cache. It polls
|
|
* these queues quite rapidly in the real NI so it always has a full
|
|
* cache available for future incoming packets. To prevent performance
|
|
* issues, this simulation polls rapidly ("fast polling mode") only
|
|
* when absolutely necessary. Typically, that means only after the
|
|
* card has been reset, but before the request queues have finished
|
|
* being built by the 3B2 host. The UNIX NI driver expects and
|
|
* requires this behavior!
|
|
*
|
|
* Open Issues
|
|
* -----------
|
|
*
|
|
* 1. The simulated card does not yet support setting or removing
|
|
* multicast Ethernet addresses. ioctl operations that attempt to
|
|
* set or remove multicast Ethernet addresses should silently
|
|
* fail. This will be supported in a future release.
|
|
*
|
|
*/
|
|
|
|
#include "3b2_ni.h"
|
|
|
|
#include "3b2_io.h"
|
|
#include "3b2_mem.h"
|
|
#include "3b2_stddev.h"
|
|
|
|
/* State container for the card */
|
|
NI_STATE ni;
|
|
|
|
t_bool ni_conf = FALSE;
|
|
|
|
/* Static Function Declarations */
|
|
static void dump_packet(const char *direction, ETH_PACK *pkt);
|
|
static void ni_enable();
|
|
static void ni_disable();
|
|
static void ni_cmd(uint8 slot, cio_entry *rentry, uint8 *rapp_data, t_bool is_exp);
|
|
|
|
/*
|
|
* A list of pumped code CRCs that will cause Force Function Call to
|
|
* respond with "Test Passed". Must be null-terminated.
|
|
*/
|
|
static const uint32 NI_DIAG_CRCS[] = {
|
|
0x795268a4,
|
|
0xfab1057c,
|
|
0x10ca00cd,
|
|
0x9b3ddeda,
|
|
0x267b19a0,
|
|
0x123f36c0,
|
|
0xc04ca0ab,
|
|
0x96d0506e, /* Rev 3 NI, SVR 3.2.3 */
|
|
0x9d3fafde, /* Rev 3 NI, SVR 3.2.3 */
|
|
0,
|
|
};
|
|
|
|
/*
|
|
* A list of pumped code CRCs that will cause the sysgen routine to
|
|
* respond with a full completion request instead of an express
|
|
* completion request. Must be null-terminated.
|
|
*/
|
|
static const uint32 NI_PUMP_CRCS[] = {
|
|
0xfab1057c, /* Rev 2 NI, SVR 3.x */
|
|
0xf6744bed, /* Rev 2 NI, SVR 3.x */
|
|
0x96d0506e, /* Rev 3 NI, SVR 3.2.3 */
|
|
0x9d3fafde, /* Rev 3 NI, SVR 3.2.3 */
|
|
0x3553230a, /* Rev 3 NI, SVR 3.2.3 */
|
|
0,
|
|
};
|
|
|
|
/*
|
|
* Unit 0: Packet reception.
|
|
* Unit 1: Sanity timer.
|
|
* Unit 2: Request Queue poller.
|
|
* Unit 3: CIO requests.
|
|
*/
|
|
UNIT ni_unit[] = {
|
|
{ UDATA(&ni_rcv_svc, UNIT_IDLE|UNIT_ATTABLE, 0) },
|
|
{ UDATA(&ni_sanity_svc, UNIT_IDLE|UNIT_DIS, 0) },
|
|
{ UDATA(&ni_rq_svc, UNIT_IDLE|UNIT_DIS, 0) },
|
|
{ UDATA(&ni_cio_svc, UNIT_DIS, 0) },
|
|
{ 0 }
|
|
};
|
|
|
|
static UNIT *rcv_unit = &ni_unit[0];
|
|
static UNIT *sanity_unit = &ni_unit[1];
|
|
static UNIT *rq_unit = &ni_unit[2];
|
|
static UNIT *cio_unit = &ni_unit[3];
|
|
|
|
MTAB ni_mod[] = {
|
|
{ MTAB_XTD|MTAB_VDV|MTAB_NMO, 0, "STATS", "STATS",
|
|
&ni_set_stats, &ni_show_stats, NULL, "Display or reset statistics" },
|
|
{ MTAB_XTD|MTAB_VDV|MTAB_VALR, 0, "POLL", NULL,
|
|
NULL, &ni_show_poll, NULL, "Display the current polling mode" },
|
|
{ MTAB_XTD|MTAB_VDV|MTAB_VALR|MTAB_NC, 0, "MAC", "MAC=xx:xx:xx:xx:xx:xx",
|
|
&ni_setmac, &ni_showmac, NULL, "MAC address" },
|
|
{ MTAB_XTD|MTAB_VDV|MTAB_NMO, 0, "FILTERS", NULL,
|
|
NULL, &ni_show_filters, NULL, "Display address filters" },
|
|
{ 0 }
|
|
};
|
|
|
|
static DEBTAB ni_debug[] = {
|
|
{ "TRACE", DBG_TRACE, "trace routine calls" },
|
|
{ "IO", DBG_IO, "debug i/o" },
|
|
{ "CACHE", DBG_CACHE, "debug job cache" },
|
|
{ "PACKET", DBG_DAT, "display packet data" },
|
|
{ "ERR", DBG_ERR, "display errors" },
|
|
{ "ETH", DBG_ETH, "debug ethernet device" },
|
|
{ 0 }
|
|
};
|
|
|
|
DEVICE ni_dev = {
|
|
"NI", /* name */
|
|
ni_unit, /* units */
|
|
NULL, /* registers */
|
|
ni_mod, /* modifiers */
|
|
4, /* #units */
|
|
16, /* address radix */
|
|
32, /* address width */
|
|
1, /* address incr. */
|
|
16, /* data radix */
|
|
8, /* data width */
|
|
NULL, /* examine routine */
|
|
NULL, /* deposit routine */
|
|
&ni_reset, /* reset routine */
|
|
NULL, /* boot routine */
|
|
&ni_attach, /* attach routine */
|
|
&ni_detach, /* detach routine */
|
|
NULL, /* context */
|
|
DEV_DISABLE|DEV_DIS|
|
|
DEV_DEBUG|DEV_ETHER, /* flags */
|
|
0, /* debug control flags */
|
|
ni_debug, /* debug flag names */
|
|
NULL, /* memory size change */
|
|
NULL, /* logical name */
|
|
&ni_help, /* help routine */
|
|
NULL, /* attach help routine */
|
|
NULL, /* help context */
|
|
&ni_description, /* device description */
|
|
NULL,
|
|
};
|
|
|
|
static void dump_packet(const char *direction, ETH_PACK *pkt)
|
|
{
|
|
char dumpline[82];
|
|
char *p;
|
|
uint32 char_offset, i;
|
|
|
|
if (!direction) {
|
|
return;
|
|
}
|
|
|
|
snprintf(dumpline, 10, "%08x ", 0);
|
|
char_offset = 9;
|
|
|
|
for (i = 0; i < pkt->len; i++) {
|
|
snprintf(dumpline + char_offset, 4, "%02x ", pkt->msg[i]);
|
|
snprintf(dumpline + 61 + (i % 16), 2, "%c", CHAR(pkt->msg[i]));
|
|
char_offset += 3;
|
|
|
|
if ((i + 1) % 16 == 0) {
|
|
|
|
snprintf(dumpline + 56, 5, " |");
|
|
snprintf(dumpline + 78, 2, "|");
|
|
|
|
for (p = dumpline; p < (dumpline + 80); p++) {
|
|
if (*p == '\0') {
|
|
*p = ' ';
|
|
}
|
|
}
|
|
*p = '\0';
|
|
sim_debug(DBG_DAT, &ni_dev,
|
|
"[%s packet]: %s\n", direction, dumpline);
|
|
memset(dumpline, 0, 80);
|
|
snprintf(dumpline, 10, "%08x ", i + 1);
|
|
char_offset = 9;
|
|
}
|
|
}
|
|
|
|
/* Finish any leftover bits */
|
|
if ((i + 1) % 16 != 0) {
|
|
snprintf(dumpline + 56, 5, " |");
|
|
snprintf(dumpline + 78, 2, "|");
|
|
|
|
for (p = dumpline; p < (dumpline + 80); p++) {
|
|
if (*p == '\0') {
|
|
*p = ' ';
|
|
}
|
|
}
|
|
*p = '\0';
|
|
|
|
sim_debug(DBG_DAT, &ni_dev,
|
|
"[%s packet]: %s\n", direction, dumpline);
|
|
}
|
|
}
|
|
|
|
static void ni_enable()
|
|
{
|
|
sim_debug(DBG_TRACE, &ni_dev,
|
|
"[ni_enable] Enabling the interface.\n");
|
|
|
|
/* Reset Statistics */
|
|
memset(&ni.stats, 0, sizeof(ni_stat_info));
|
|
|
|
/* Clear out job cache */
|
|
memset(&ni.job_cache, 0, sizeof(ni_job_cache) * 2);
|
|
|
|
/* Enter fast polling mode */
|
|
ni.poll_rate = NI_QPOLL_FAST;
|
|
|
|
/* Start the queue poller in fast poll mode */
|
|
sim_activate_abs(rq_unit, NI_QPOLL_FAST);
|
|
|
|
/* Start the sanity timer */
|
|
sim_activate_after(sanity_unit, NI_SANITY_INTERVAL_US);
|
|
|
|
/* Enable the interface */
|
|
ni.enabled = TRUE;
|
|
}
|
|
|
|
static void ni_disable()
|
|
{
|
|
sim_debug(DBG_TRACE, &ni_dev,
|
|
"[ni_disable] Disabling the interface.\n");
|
|
ni.enabled = FALSE;
|
|
sim_cancel(ni_unit);
|
|
sim_cancel(rcv_unit);
|
|
sim_cancel(rq_unit);
|
|
sim_cancel(cio_unit);
|
|
sim_cancel(sanity_unit);
|
|
CIO_CLR_INT(ni.slot);
|
|
}
|
|
|
|
static void ni_cmd(uint8 slot, cio_entry *rentry, uint8 *rapp_data, t_bool is_exp)
|
|
{
|
|
int i, j;
|
|
int32 delay;
|
|
uint16 hdrsize;
|
|
t_stat status;
|
|
int prot_info_offset;
|
|
cio_entry centry = {0};
|
|
uint8 app_data[4] = {rapp_data[0], rapp_data[1], rapp_data[2], rapp_data[3]};
|
|
|
|
/* Assume some default values, but let the handlers below
|
|
* override these where appropriate */
|
|
centry.opcode = CIO_SUCCESS;
|
|
centry.subdevice = rentry->subdevice;
|
|
centry.address = rentry->address;
|
|
|
|
cio[slot].op = rentry->opcode;
|
|
|
|
delay = NI_INT_DELAY;
|
|
|
|
switch(rentry->opcode) {
|
|
case CIO_DLM:
|
|
for (i = 0; i < rentry->byte_count; i++) {
|
|
ni.crc = cio_crc32_shift(ni.crc, pread_b(rentry->address + i, BUS_PER));
|
|
}
|
|
|
|
centry.address = rentry->address + rentry->byte_count;
|
|
sim_debug(DBG_TRACE, &ni_dev,
|
|
"[ni_cmd] CIO Download Memory: bytecnt=%04x "
|
|
"addr=%08x return_addr=%08x subdev=%02x (CRC=%08x)\n",
|
|
rentry->byte_count, rentry->address,
|
|
centry.address, centry.subdevice, ni.crc);
|
|
|
|
if (is_exp) {
|
|
cio_cexpress(slot, NIQESIZE, ¢ry, app_data);
|
|
} else {
|
|
cio_cqueue(slot, CIO_STAT, NIQESIZE, ¢ry, app_data);
|
|
}
|
|
|
|
break;
|
|
case CIO_FCF:
|
|
sim_debug(DBG_TRACE, &ni_dev,
|
|
"[ni_cmd] CIO Force Function Call (CRC=%08x)\n",
|
|
ni.crc);
|
|
|
|
/* If the currently running program is a diagnostics program,
|
|
* we are expected to write results into memory at address
|
|
* 0x200f000 */
|
|
for (i = 0; NI_DIAG_CRCS[i] != 0; i++) {
|
|
if (ni.crc == NI_DIAG_CRCS[i]) {
|
|
pwrite_h(0x200f000, 0x1, BUS_PER); /* Test success */
|
|
pwrite_h(0x200f002, 0x0, BUS_PER); /* Test Number */
|
|
pwrite_h(0x200f004, 0x0, BUS_PER); /* Actual */
|
|
pwrite_h(0x200f006, 0x0, BUS_PER); /* Expected */
|
|
pwrite_b(0x200f008, 0x1, BUS_PER); /* Success flag again */
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Store the sequence byte we were sent for later reply. */
|
|
ni.fcf_seq = rapp_data[3];
|
|
|
|
/* "Force Function Call" causes the CIO card to start running
|
|
* pumped code as a new process, taking over from its firmware
|
|
* ROM. As a result, a new sysgen is necessary to get the card
|
|
* in the right state. */
|
|
|
|
ni_disable();
|
|
cio[slot].sysgen_s = 0;
|
|
|
|
if (cio[slot].ivec == 0 || cio[slot].ivec == 3) {
|
|
cio_cexpress(slot, NIQESIZE, ¢ry, app_data);
|
|
} else {
|
|
cio_cqueue(slot, CIO_STAT, NIQESIZE, ¢ry, app_data);
|
|
}
|
|
|
|
break;
|
|
case CIO_DSD:
|
|
/* Determine Sub-Devices. We have none. */
|
|
sim_debug(DBG_TRACE, &ni_dev,
|
|
"[ni_cmd] Determine Sub-Devices.\n");
|
|
|
|
/* The system wants us to write sub-device structures at the
|
|
* supplied address */
|
|
pwrite_h(rentry->address, 0x0, BUS_PER);
|
|
|
|
if (is_exp) {
|
|
cio_cexpress(slot, NIQESIZE, ¢ry, app_data);
|
|
} else {
|
|
cio_cqueue(slot, CIO_STAT, NIQESIZE, ¢ry, app_data);
|
|
}
|
|
|
|
break;
|
|
case NI_SETID:
|
|
sim_debug(DBG_TRACE, &ni_dev,
|
|
"[ni_cmd] NI SETID Operation\n");
|
|
|
|
/* Try to read the mac from memory */
|
|
for (i = 0; i < MAC_SIZE_BYTES; i++) {
|
|
ni.mac_bytes[i] = pread_b(rentry->address + i, BUS_PER);
|
|
}
|
|
|
|
snprintf(ni.mac_str, MAC_SIZE_CHARS, "%02x:%02x:%02x:%02x:%02x:%02x",
|
|
ni.mac_bytes[0], ni.mac_bytes[1], ni.mac_bytes[2],
|
|
ni.mac_bytes[3], ni.mac_bytes[4], ni.mac_bytes[5]);
|
|
|
|
sim_debug(DBG_TRACE, &ni_dev,
|
|
"[ni_cmd] NI SETID: New MAC: %s\n",
|
|
ni.mac_str);
|
|
|
|
status = ni_setmac(ni_dev.units, 0, ni.mac_str, NULL);
|
|
|
|
cio_cqueue(slot, CIO_STAT, NIQESIZE, ¢ry, app_data);
|
|
|
|
break;
|
|
case NI_TURNOFF:
|
|
sim_debug(DBG_TRACE, &ni_dev,
|
|
"[ni_cmd] NI TURNOFF Operation\n");
|
|
|
|
ni_disable();
|
|
|
|
cio_cqueue(slot, CIO_STAT, NIQESIZE, ¢ry, app_data);
|
|
|
|
break;
|
|
case NI_TURNON:
|
|
sim_debug(DBG_TRACE, &ni_dev,
|
|
"[ni_cmd] NI TURNON Operation\n");
|
|
|
|
ni_enable();
|
|
|
|
cio_cqueue(slot, CIO_STAT, NIQESIZE, ¢ry, app_data);
|
|
|
|
break;
|
|
case NI_STATS:
|
|
sim_debug(DBG_TRACE, &ni_dev,
|
|
"[ni_cmd] NI STATS Operation\n");
|
|
|
|
cio_cqueue(slot, CIO_STAT, NIQESIZE, ¢ry, app_data);
|
|
|
|
break;
|
|
case NI_SEND:
|
|
case NI_SEND_A:
|
|
sim_debug(DBG_TRACE, &ni_dev,
|
|
"[ni_cmd] NI SEND Operation (opcode=%d)\n",
|
|
rentry->opcode);
|
|
|
|
/* TODO: Why is this always 4 for a send? */
|
|
centry.subdevice = 4;
|
|
|
|
/* TODO: On the real 3B2, this appears to be some sort of
|
|
* checksum. Perhaps the packet checksum? I'm not sure. I need
|
|
* to run Wireshark on the real 3B2 and investigate.
|
|
*
|
|
* However, we're in luck: The driver code doesn't seem to
|
|
* validate it or check against it in any way, so we can
|
|
* put anything in there.
|
|
*/
|
|
centry.address = rentry->address;
|
|
centry.byte_count = rentry->byte_count;
|
|
|
|
|
|
/* If the interface is not attached, we can't actually send
|
|
* any packets. */
|
|
if (!(rcv_unit->flags & UNIT_ATT)) {
|
|
ni.stats.tx_fail++;
|
|
centry.opcode = CIO_FAILURE;
|
|
sim_debug(DBG_TRACE, &ni_dev,
|
|
"[ni_cmd] NI SEND failure. Not attached. tx_fail=%d\n",
|
|
ni.stats.tx_fail);
|
|
break;
|
|
}
|
|
|
|
/* Reset the write packet */
|
|
ni.wr_buf.len = 0;
|
|
ni.wr_buf.oversize = NULL;
|
|
|
|
/* Read the size of the header */
|
|
hdrsize = pread_h(rentry->address + EIG_TABLE_SIZE, BUS_PER);
|
|
|
|
/* Read out the packet frame */
|
|
for (i = 0; i < rentry->byte_count; i++) {
|
|
ni.wr_buf.msg[i] = pread_b(rentry->address + PKT_START_OFFSET + i, BUS_PER);
|
|
}
|
|
|
|
/* Get a pointer to the buffer containing the protocol data */
|
|
prot_info_offset = 0;
|
|
i = 0;
|
|
do {
|
|
ni.prot.addr = pread_w(rentry->address + prot_info_offset, BUS_PER);
|
|
ni.prot.size = pread_h(rentry->address + prot_info_offset + 4, BUS_PER);
|
|
ni.prot.last = pread_h(rentry->address + prot_info_offset + 6, BUS_PER);
|
|
prot_info_offset += 8;
|
|
|
|
/* Fill in the frame from this buffer */
|
|
for (j=0; j < ni.prot.size; i++, j++) {
|
|
ni.wr_buf.msg[hdrsize + i] = pread_b(ni.prot.addr + j, BUS_PER);
|
|
}
|
|
} while (!ni.prot.last);
|
|
|
|
/* Fill in packet details */
|
|
ni.wr_buf.len = rentry->byte_count;
|
|
|
|
sim_debug(DBG_IO, &ni_dev,
|
|
"[XMT] Transmitting a packet of size %d (0x%x)\n",
|
|
ni.wr_buf.len, ni.wr_buf.len);
|
|
|
|
/* Send it */
|
|
status = eth_write(ni.eth, &ni.wr_buf, NULL);
|
|
|
|
if (status == SCPE_OK) {
|
|
if (ni_dev.dctrl & DBG_DAT) {
|
|
dump_packet("XMT", &ni.wr_buf);
|
|
}
|
|
ni.stats.tx_bytes += ni.wr_buf.len;
|
|
ni.stats.tx_pkt++;
|
|
} else {
|
|
ni.stats.tx_fail++;
|
|
centry.opcode = CIO_FAILURE;
|
|
}
|
|
|
|
/* Weird behavior seen on the real 3B2's completion queue: If
|
|
* the byte count value is < 0xff, shift it! I really wish I
|
|
* understood this card... */
|
|
if (centry.byte_count < 0xff) {
|
|
centry.byte_count <<= 8;
|
|
}
|
|
|
|
cio_cqueue(slot, CIO_STAT, NIQESIZE, ¢ry, app_data);
|
|
|
|
delay = 0;
|
|
|
|
break;
|
|
default:
|
|
sim_debug(DBG_TRACE, &ni_dev,
|
|
"[ni_cmd] Opcode %d Not Handled Yet\n",
|
|
rentry->opcode);
|
|
|
|
cio_cqueue(slot, CIO_STAT, NIQESIZE, ¢ry, app_data);
|
|
|
|
break;
|
|
}
|
|
|
|
sim_activate_abs(cio_unit, delay);
|
|
}
|
|
|
|
t_stat ni_setmac(UNIT *uptr, int32 val, CONST char* cptr, void* desc)
|
|
{
|
|
t_stat status;
|
|
|
|
UNUSED(val);
|
|
UNUSED(desc);
|
|
|
|
status = SCPE_OK;
|
|
status = eth_mac_scan_ex(&ni.macs[NI_NIC_MAC], cptr, uptr);
|
|
|
|
if (status == SCPE_OK) {
|
|
eth_filter(ni.eth, ni.filter_count, ni.macs, 0, 0);
|
|
} else {
|
|
sim_debug(DBG_ERR, &ni_dev,
|
|
"[ni_setmac] Error in eth_mac_scan_ex. status=%d\n", status);
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
t_stat ni_showmac(FILE* st, UNIT* uptr, int32 val, CONST void* desc)
|
|
{
|
|
char buffer[20];
|
|
|
|
UNUSED(uptr);
|
|
UNUSED(val);
|
|
UNUSED(desc);
|
|
|
|
eth_mac_fmt(&ni.macs[NI_NIC_MAC], buffer);
|
|
fprintf(st, "MAC=%s", buffer);
|
|
return SCPE_OK;
|
|
}
|
|
|
|
t_stat ni_show_filters(FILE* st, UNIT* uptr, int32 val, CONST void* desc)
|
|
{
|
|
char buffer[20];
|
|
int i;
|
|
|
|
UNUSED(uptr);
|
|
UNUSED(val);
|
|
UNUSED(desc);
|
|
|
|
eth_mac_fmt(&ni.macs[NI_NIC_MAC], buffer);
|
|
fprintf(st, "Physical Address=%s\n", buffer);
|
|
if (ni.filter_count > 0) {
|
|
fprintf(st, "Filters:\n");
|
|
for (i=0; i < ni.filter_count; i++) {
|
|
eth_mac_fmt((ETH_MAC *) ni.macs[i], buffer);
|
|
fprintf(st, "[%2d]: %s\n", i, buffer);
|
|
}
|
|
fprintf(st, "\n");
|
|
}
|
|
|
|
return SCPE_OK;
|
|
}
|
|
|
|
void ni_sysgen(uint8 slot)
|
|
{
|
|
int i;
|
|
t_bool pumped = FALSE;
|
|
cio_entry cqe = {0};
|
|
uint8 app_data[4] = {0};
|
|
|
|
ni_disable();
|
|
|
|
app_data[3] = 0x64;
|
|
cqe.opcode = CIO_SYSGEN_OK;
|
|
|
|
sim_debug(DBG_TRACE, &ni_dev,
|
|
"[ni_sysgen] CIO SYSGEN. rqp=%08x, cqp=%08x, nrq=%d, rqs=%d cqs=%d\n",
|
|
cio[slot].rqp, cio[slot].cqp, cio[slot].no_rque, cio[slot].rqs, cio[slot].cqs);
|
|
|
|
/* If the card has been successfully pumped, then we respond with
|
|
* a full completion queue entry. Otherwise, an express entry is
|
|
* used. */
|
|
for (i = 0; NI_PUMP_CRCS[i] != 0; i++) {
|
|
if (NI_PUMP_CRCS[i] == ni.crc) {
|
|
cio_cqueue(slot, CIO_STAT, NIQESIZE, &cqe, app_data);
|
|
pumped = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!pumped) {
|
|
cio_cexpress(slot, NIQESIZE, &cqe, app_data);
|
|
}
|
|
|
|
/* Now clear out the old CRC value, in case the card needs to be
|
|
* sysgen'ed again later. */
|
|
ni.crc = 0;
|
|
|
|
sim_activate_abs(cio_unit, NI_INT_DELAY);
|
|
}
|
|
|
|
/*
|
|
* Handler for CIO INT0 (express job) requests.
|
|
*/
|
|
void ni_express(uint8 slot)
|
|
{
|
|
cio_entry rqe = {0};
|
|
uint8 app_data[4] = {0};
|
|
|
|
sim_debug(DBG_TRACE, &ni_dev,
|
|
"[ni_express] Handling express CIO request.\n");
|
|
|
|
cio_rexpress(slot, NIQESIZE, &rqe, app_data);
|
|
ni_cmd(slot, &rqe, app_data, TRUE);
|
|
}
|
|
|
|
/*
|
|
* Handler for CIO INT1 (full job) requests.
|
|
*/
|
|
void ni_full(uint8 slot)
|
|
{
|
|
cio_entry rqe = {0};
|
|
uint8 app_data[4] = {0};
|
|
|
|
sim_debug(DBG_TRACE, &ni_dev,
|
|
"[ni_full] INT1 received. Handling full CIO request.\n");
|
|
|
|
while (cio_cqueue_avail(slot, NIQESIZE) &&
|
|
cio_rqueue(slot, GE_QUEUE, NIQESIZE, &rqe, app_data) == SCPE_OK) {
|
|
ni_cmd(slot, &rqe, app_data, FALSE);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Handler for CIO RESET requests.
|
|
*/
|
|
void ni_cio_reset(uint8 slot)
|
|
{
|
|
UNUSED(slot);
|
|
|
|
ni_disable();
|
|
}
|
|
|
|
t_stat ni_reset(DEVICE *dptr)
|
|
{
|
|
t_stat r;
|
|
uint8 slot;
|
|
char uname[16];
|
|
|
|
if (dptr->flags & DEV_DIS) {
|
|
cio_remove_all(NI_ID);
|
|
ni_conf = FALSE;
|
|
return SCPE_OK;
|
|
}
|
|
|
|
if (!ni_conf) {
|
|
r = cio_install(NI_ID, "NI", NI_IPL,
|
|
&ni_express, &ni_full, &ni_sysgen, &ni_cio_reset,
|
|
&slot);
|
|
if (r != SCPE_OK) {
|
|
return r;
|
|
}
|
|
/* Remember the card slot */
|
|
ni.slot = slot;
|
|
ni_conf = TRUE;
|
|
}
|
|
|
|
/* Set an initial MAC address in the AT&T NI range */
|
|
ni_setmac(ni_dev.units, 0, "80:00:10:03:00:00/32", NULL);
|
|
|
|
/* Set up unit names */
|
|
snprintf(uname, 16, "%s-RCV", dptr->name);
|
|
sim_set_uname(rcv_unit, uname);
|
|
snprintf(uname, 16, "%s-SANITY", dptr->name);
|
|
sim_set_uname(sanity_unit, uname);
|
|
snprintf(uname, 16, "%s-RQ", dptr->name);
|
|
sim_set_uname(rq_unit, uname);
|
|
snprintf(uname, 16, "%s-CIO", dptr->name);
|
|
sim_set_uname(cio_unit, uname);
|
|
|
|
/* Ensure that the broadcast address is configured, and that we
|
|
* have a minmimum of two filters set. */
|
|
memset(&ni.macs[NI_BCST_MAC], 0xff, sizeof(ETH_MAC));
|
|
ni.filter_count = NI_FILTER_MIN;
|
|
|
|
ni.poll_rate = NI_QPOLL_FAST;
|
|
|
|
/* Make sure the transceiver is disabled and all
|
|
* polling activity and interrupts are disabled. */
|
|
ni_disable();
|
|
|
|
return SCPE_OK;
|
|
}
|
|
|
|
t_stat ni_rcv_svc(UNIT *uptr)
|
|
{
|
|
t_stat read_succ;
|
|
|
|
UNUSED(uptr);
|
|
|
|
/* Since we cannot know which queue (large packet or small packet
|
|
* queue) will have room for the next packet that we read, for
|
|
* safety reasons we will not call eth_read() until we're certain
|
|
* there's room available in BOTH queues. */
|
|
while (ni.enabled && NI_BUFFERS_AVAIL) {
|
|
read_succ = eth_read(ni.eth, &ni.rd_buf, NULL);
|
|
if (!read_succ) {
|
|
break;
|
|
}
|
|
/* Attempt to process the packet that was received. */
|
|
ni_process_packet();
|
|
}
|
|
|
|
return SCPE_OK;
|
|
}
|
|
|
|
/*
|
|
* Service used by the card to poll for available request queue
|
|
* entries.
|
|
*/
|
|
t_stat ni_rq_svc(UNIT *uptr)
|
|
{
|
|
t_bool rq_taken;
|
|
int i, wp, no_rque;
|
|
cio_entry rqe = {0};
|
|
uint8 slot[4] = {0};
|
|
|
|
UNUSED(uptr);
|
|
|
|
rq_taken = FALSE;
|
|
no_rque = cio[ni.slot].no_rque - 1;
|
|
|
|
for (i = 0; i < no_rque; i++) {
|
|
while (NI_CACHE_HAS_SPACE(i) && cio_rqueue(ni.slot, i+1, NIQESIZE, &rqe, slot) == SCPE_OK) {
|
|
sim_debug(DBG_CACHE, &ni_dev,
|
|
"[cache - FILL] %s packet entry. lp=%02x ulp=%02x "
|
|
"slot=%d addr=0x%08x\n",
|
|
i == 0 ? "Small" : "Large",
|
|
cio_r_lp(ni.slot, i+1, NIQESIZE),
|
|
cio_r_ulp(ni.slot, i+1, NIQESIZE),
|
|
slot[3], rqe.address);
|
|
wp = ni.job_cache[i].wp;
|
|
ni.job_cache[i].req[wp].addr = rqe.address;
|
|
ni.job_cache[i].req[wp].slot = slot[3];
|
|
ni.job_cache[i].wp = (wp + 1) % NI_CACHE_LEN;
|
|
ni.stats.rq_taken++;
|
|
rq_taken = TRUE;
|
|
}
|
|
}
|
|
|
|
/* Somewhat of a kludge, unfortunately. */
|
|
if (ni.poll_rate == NI_QPOLL_FAST && ni.stats.rq_taken >= 6) {
|
|
sim_debug(DBG_TRACE, &ni_dev,
|
|
"[ni_rq_svc] Switching to slow poll mode.\n");
|
|
ni.poll_rate = NI_QPOLL_SLOW;
|
|
}
|
|
|
|
/* If any receive jobs were found, schedule a packet read right
|
|
away */
|
|
if (rq_taken) {
|
|
sim_activate_abs(rcv_unit, 0);
|
|
}
|
|
|
|
/* Reactivate the poller. */
|
|
if (ni.poll_rate == NI_QPOLL_FAST) {
|
|
sim_activate_abs(rq_unit, NI_QPOLL_FAST);
|
|
} else {
|
|
sim_clock_coschedule(rq_unit, 1000);
|
|
}
|
|
|
|
return SCPE_OK;
|
|
}
|
|
|
|
/*
|
|
* The NI card uses a sanity timer to poke the host every few seconds
|
|
* and let it know that it is still alive. This service handling
|
|
* routine is responsible for scheduling these notifications.
|
|
*
|
|
* The NI driver expects these notifications to happen no more than 15
|
|
* seconds apart. Unfortunately, I do not yet know the exact frequency
|
|
* with which the real hardware sends these updates, but it appears
|
|
* not to happen very frequently, so we'll have to settle for an
|
|
* educated guess of 10 seconds.
|
|
*/
|
|
t_stat ni_sanity_svc(UNIT *uptr)
|
|
{
|
|
cio_entry cqe = {0};
|
|
uint8 app_data[4] = {0};
|
|
|
|
UNUSED(uptr);
|
|
|
|
sim_debug(DBG_TRACE, &ni_dev,
|
|
"[ni_sanity_svc] Firing sanity timer.\n");
|
|
|
|
cqe.opcode = NI_SANITY;
|
|
cio_cqueue(ni.slot, CIO_STAT, NIQESIZE, &cqe, app_data);
|
|
|
|
CIO_SET_INT(ni.slot);
|
|
|
|
sim_activate_after(sanity_unit, NI_SANITY_INTERVAL_US);
|
|
|
|
return SCPE_OK;
|
|
}
|
|
|
|
t_stat ni_cio_svc(UNIT *uptr)
|
|
{
|
|
UNUSED(uptr);
|
|
|
|
sim_debug(DBG_TRACE, &ni_dev,
|
|
"[ni_cio_svc] Handling a CIO service (Setting Interrupt) for board %d\n", ni.slot);
|
|
CIO_SET_INT(ni.slot);
|
|
|
|
return SCPE_OK;
|
|
}
|
|
|
|
/*
|
|
* Do the work of trying to process the most recently received packet
|
|
*/
|
|
void ni_process_packet()
|
|
{
|
|
int i, rp;
|
|
uint32 addr;
|
|
uint8 slot;
|
|
cio_entry centry = {0};
|
|
uint8 capp_data[4] = {0};
|
|
int len = 0;
|
|
int que_num = 0;
|
|
uint8 *rbuf;
|
|
|
|
len = ni.rd_buf.len;
|
|
rbuf = ni.rd_buf.msg;
|
|
que_num = len > SM_PKT_MAX ? LG_QUEUE : SM_QUEUE;
|
|
|
|
sim_debug(DBG_IO, &ni_dev,
|
|
"[ni_process_packet] Receiving a packet of size %d (0x%x)\n",
|
|
len, len);
|
|
|
|
/* Availability of a job in the job cache was checked before
|
|
* calling ni_process_packet(), so there is no need to check it
|
|
* again. */
|
|
rp = ni.job_cache[que_num].rp;
|
|
addr = ni.job_cache[que_num].req[rp].addr;
|
|
slot = ni.job_cache[que_num].req[rp].slot;
|
|
ni.job_cache[que_num].rp = (rp + 1) % NI_CACHE_LEN;
|
|
sim_debug(DBG_CACHE, &ni_dev,
|
|
"[cache - DRAIN] %s packet entry. lp=%02x ulp=%02x "
|
|
"slot=%d addr=0x%08x\n",
|
|
que_num == 0 ? "Small" : "Large",
|
|
cio_r_lp(ni.slot, que_num+1, NIQESIZE),
|
|
cio_r_ulp(ni.slot, que_num+1, NIQESIZE),
|
|
slot, addr);
|
|
|
|
/* Store the packet into main memory */
|
|
for (i = 0; i < len; i++) {
|
|
pwrite_b(addr + i, rbuf[i], BUS_PER);
|
|
}
|
|
|
|
if (ni_dev.dctrl & DBG_DAT) {
|
|
dump_packet("RCV", &ni.rd_buf);
|
|
}
|
|
|
|
ni.stats.rx_pkt++;
|
|
ni.stats.rx_bytes += len;
|
|
|
|
/* Build a reply CIO message */
|
|
centry.subdevice = 4; /* TODO: Why is it always 4? */
|
|
centry.opcode = 0;
|
|
centry.address = addr + len;
|
|
centry.byte_count = len;
|
|
capp_data[3] = slot;
|
|
|
|
/* TODO: We should probably also check status here. */
|
|
cio_cqueue(ni.slot, CIO_STAT, NIQESIZE, ¢ry, capp_data);
|
|
|
|
/* Trigger an interrupt */
|
|
CIO_SET_INT(ni.slot);
|
|
}
|
|
|
|
t_stat ni_attach(UNIT *uptr, CONST char *cptr)
|
|
{
|
|
t_stat status;
|
|
char *tptr;
|
|
|
|
sim_debug(DBG_TRACE, &ni_dev, "ni_attach()\n");
|
|
|
|
tptr = (char *) malloc(strlen(cptr) + 1);
|
|
if (tptr == NULL) {
|
|
return SCPE_MEM;
|
|
}
|
|
strcpy(tptr, cptr);
|
|
|
|
ni.eth = (ETH_DEV *) malloc(sizeof(ETH_DEV));
|
|
if (!ni.eth) {
|
|
free(tptr);
|
|
return SCPE_MEM;
|
|
}
|
|
|
|
status = eth_open(ni.eth, cptr, &ni_dev, DBG_ETH);
|
|
if (status != SCPE_OK) {
|
|
sim_debug(DBG_ERR, &ni_dev, "ni_attach failure: open\n");
|
|
free(tptr);
|
|
free(ni.eth);
|
|
ni.eth = NULL;
|
|
return status;
|
|
}
|
|
|
|
status = eth_check_address_conflict(ni.eth, &ni.macs[NI_NIC_MAC]);
|
|
if (status != SCPE_OK) {
|
|
sim_debug(DBG_ERR, &ni_dev, "ni_attach failure: mac check\n");
|
|
eth_close(ni.eth);
|
|
free(tptr);
|
|
free(ni.eth);
|
|
ni.eth = NULL;
|
|
return status;
|
|
}
|
|
|
|
/* Ensure the ethernet device is in async mode */
|
|
/* TODO: Determine best latency */
|
|
status = eth_set_async(ni.eth, 1000);
|
|
if (status != SCPE_OK) {
|
|
sim_debug(DBG_ERR, &ni_dev, "ni_attach failure: eth_set_async\n");
|
|
eth_close(ni.eth);
|
|
free(tptr);
|
|
free(ni.eth);
|
|
ni.eth = NULL;
|
|
return status;
|
|
}
|
|
|
|
uptr->filename = tptr;
|
|
uptr->flags |= UNIT_ATT;
|
|
|
|
eth_filter(ni.eth, ni.filter_count, ni.macs, 0, 0);
|
|
|
|
return SCPE_OK;
|
|
}
|
|
|
|
t_stat ni_detach(UNIT *uptr)
|
|
{
|
|
sim_debug(DBG_TRACE, &ni_dev, "ni_detach()\n");
|
|
|
|
if (uptr->flags & UNIT_ATT) {
|
|
/* TODO: Do we really want to disable here? Or is that ONLY FCF's job? */
|
|
/* ni_disable(); */
|
|
eth_close(ni.eth);
|
|
free(ni.eth);
|
|
ni.eth = NULL;
|
|
free(uptr->filename);
|
|
uptr->filename = NULL;
|
|
uptr->flags &= ~UNIT_ATT;
|
|
}
|
|
|
|
return SCPE_OK;
|
|
}
|
|
|
|
t_stat ni_set_stats(UNIT* uptr, int32 val, CONST char* cptr, void* desc)
|
|
{
|
|
int init, elements, i;
|
|
uint32 *stats_array;
|
|
|
|
UNUSED(uptr);
|
|
UNUSED(val);
|
|
UNUSED(cptr);
|
|
UNUSED(desc);
|
|
|
|
if (cptr) {
|
|
init = atoi(cptr);
|
|
stats_array = (uint32 *)&ni.stats;
|
|
elements = sizeof(ni_stat_info) / sizeof(uint32);
|
|
|
|
for (i = 0 ; i < elements; i++) {
|
|
stats_array[i] = init;
|
|
}
|
|
} else {
|
|
memset(&ni.stats, 0, sizeof(ni_stat_info));
|
|
}
|
|
|
|
|
|
return SCPE_OK;
|
|
}
|
|
|
|
t_stat ni_show_stats(FILE* st, UNIT* uptr, int32 val, CONST void* desc)
|
|
{
|
|
const char *fmt = " %-15s%d\n";
|
|
|
|
UNUSED(uptr);
|
|
UNUSED(val);
|
|
UNUSED(desc);
|
|
|
|
fprintf(st, "NI Ethernet statistics:\n");
|
|
fprintf(st, fmt, "Recv:", ni.stats.rx_pkt);
|
|
fprintf(st, fmt, "Recv Bytes:", ni.stats.rx_bytes);
|
|
fprintf(st, fmt, "Xmit:", ni.stats.tx_pkt);
|
|
fprintf(st, fmt, "Xmit Bytes:", ni.stats.tx_bytes);
|
|
fprintf(st, fmt, "Xmit Fail:", ni.stats.tx_fail);
|
|
|
|
eth_show_dev(st, ni.eth);
|
|
|
|
return SCPE_OK;
|
|
}
|
|
|
|
t_stat ni_show_poll(FILE *st, UNIT *uptr, int32 val, CONST void *desc)
|
|
{
|
|
UNUSED(uptr);
|
|
UNUSED(val);
|
|
UNUSED(desc);
|
|
|
|
if (ni.poll_rate == NI_QPOLL_FAST) {
|
|
fprintf(st, "polling=fast");
|
|
} else {
|
|
fprintf(st, "polling=slow");
|
|
}
|
|
|
|
return SCPE_OK;
|
|
}
|
|
|
|
|
|
t_stat ni_help(FILE *st, DEVICE *dptr, UNIT *uptr, int32 flag, const char *cptr)
|
|
{
|
|
const char help_string[] =
|
|
/****************************************************************************/
|
|
" The Network Interface (NI) 10BASE5 controller is a Common I/O card for\n"
|
|
" the AT&T 3B2 that allows the 3B2 to connect to an Ethernet Local Area\n"
|
|
" Network (LAN).\n"
|
|
"1 Enabling\n"
|
|
" The simulator allows a single NI card to be configured in the first\n"
|
|
" available slot. The NI card is disabled at startup. To use the card\n"
|
|
" you must first enable it with the command:\n"
|
|
"\n"
|
|
"+sim> SET %D ENABLE\n"
|
|
"1 Configuration\n"
|
|
" By default, the card uses a self-assigned MAC address in the AT&T address\n"
|
|
" range (beginning with 80:00:10:03), however, another MAC may be set by\n"
|
|
" using the SET %D MAC command, e.g.:\n"
|
|
"\n"
|
|
"+sim> SET %D MAC=<mac-address>\n"
|
|
"\n"
|
|
" Please note, however, that the %D driver for AT&T System V R3 UNIX\n"
|
|
" always sets a MAC in the AT&T range through a software command.\n"
|
|
"1 Attaching\n"
|
|
" The %D card must be attached to a LAN device to communicate with systems\n"
|
|
" on the LAN.\n"
|
|
"\n"
|
|
" To get a list of available devices on your host platform, use the command:\n"
|
|
"\n"
|
|
"+sim> SHOW ETH\n"
|
|
"\n"
|
|
" After enabling the card, it can be attached to one of the host's\n"
|
|
" Ethernet devices with the ATTACH command. For example, depending on your\n"
|
|
" platform:\n"
|
|
"\n"
|
|
"+sim> ATTACH %D eth0\n"
|
|
"+sim> ATTACH %D en0\n"
|
|
"1 Dependencies\n"
|
|
#if defined(_WIN32)
|
|
" The WinPcap package must be installed in order to enable\n"
|
|
" communication with other computers on the local LAN.\n"
|
|
"\n"
|
|
" The WinPcap package is available from http://www.winpcap.org/\n"
|
|
#else
|
|
" To build simulators with the ability to communicate to other computers\n"
|
|
" on the local LAN, the libpcap development package must be installed on\n"
|
|
" the system which builds the simulator.\n"
|
|
#endif
|
|
"1 Performance\n"
|
|
" The simulated NI device is capable of much faster transfer speeds than\n"
|
|
" the real NI card in a 3B2, which was limited to a 10 Mbit pipe shared\n"
|
|
" between all hosts on the LAN.\n";
|
|
|
|
return scp_help(st, dptr, uptr, flag, help_string, cptr);
|
|
}
|
|
|
|
const char *ni_description(DEVICE *dptr)
|
|
{
|
|
UNUSED(dptr);
|
|
|
|
return "NI 10BASE5 Ethernet controller";
|
|
}
|