The new SCSI subsystem

Introduction

The nscsi subsystem was created to allow an implementation to be closer to the physical reality, making it easier (hopefully) to implement new controller chips from the documentations.

Global structure

Parallel SCSI is built around a symmetric bus to which a number of devices are connected. The bus is composed of 9 control lines (for now, later SCSI versions may have more) and up to 32 data lines (but currently implemented chips only handle 8). All the lines are open collector, which means that either one or multiple chip connect the line to ground and the line, of course, goes to ground, or no chip drives anything and the line stays at Vcc. Also, the bus uses inverted logic, where ground means 1. SCSI chips traditionally work in logical and not physical levels, so the nscsi subsystem also works in logical levels and does a logical-or of all the outputs of the devices.

Structurally, the implementation is done around two main classes: nscsi_bus_devices represents the bus, and nscsi_device represents an individual device. A device only communicate with the bus, and the bus takes care of transparently handling the device discovery and communication. In addition the nscsi_full_device class proposes a SCSI device with the SCSI protocol implemented making building generic SCSI devices like hard drives or CD-ROM readers easier.

Plugging in a SCSI bus in a driver

The nscsi subsystem leverages the slot interfaces and the device naming to allow for a configurable yet simple bus implementation.

First you need to create a list of acceptable devices to plug on the bus. This usually comprises of cdrom, harddisk and the controller chip. For instance:


static SLOT_INTERFACE_START( next_scsi_devices )
SLOT_INTERFACE("cdrom", NSCSI_CDROM)
SLOT_INTERFACE("harddisk", NSCSI_HARDDISK)
SLOT_INTERFACE_INTERNAL("ncr5390", NCR5390)
SLOT_INTERFACE_END

The _INTERNAL interface indicates a device that is not user-selectable, which is useful for the controller.

Then in the machine config (or in a fragment config) you need to first add the bus, and then the (potential) devices as sub-devices of the bus with the SCSI ID as the name. For instance you can use:


MCFG_NSCSI_BUS_ADD("scsibus")
MCFG_NSCSI_ADD("scsibus:0", next_scsi_devices, "cdrom", 0, 0, 0, false)
MCFG_NSCSI_ADD("scsibus:1", next_scsi_devices, "harddisk", 0, 0, 0, false)
MCFG_NSCSI_ADD("scsibus:2", next_scsi_devices, 0, 0, 0, 0, false)
MCFG_NSCSI_ADD("scsibus:3", next_scsi_devices, 0, 0, 0, 0, false)
MCFG_NSCSI_ADD("scsibus:4", next_scsi_devices, 0, 0, 0, 0, false)
MCFG_NSCSI_ADD("scsibus:5", next_scsi_devices, 0, 0, 0, 0, false)
MCFG_NSCSI_ADD("scsibus:6", next_scsi_devices, 0, 0, 0, 0, false)
MCFG_NSCSI_ADD("scsibus:7", next_scsi_devices, "ncr5390", 0, &next_ncr5390_interface, 10000000, true)

That configuration puts as default a CD-ROM reader on SCSI ID 0 and a hard drive on SCSI ID 1, and forces the controller on ID 7. The parameters of add are:

  • device tag, comprised of bus-tag:scsi-id

  • the list of acceptable devices

  • the device name as per the list, if one is to be there by default

  • the device input config, if any (and there usually isn't one)

  • the device configuration structure, usually for the controller only

  • the frequency, usually for the controller only

  • "false" for a user-modifiable slot, "true" for a fixed slot

The full device name, for mapping purposes, will be bus-tag:scsi-id:device-type, i.e. "scsibus:7:ncr5390" for our controller here.

Creating a new SCSI device using nscsi_device

The base class "nscsi_device" is to be used for down-to-the-metal devices, i.e. SCSI controller chips. The class provides three variables and one method. The first variable, scsi_bus, is a pointer to the nscsi_bus_device. The second, scsi_refid, is an opaque reference to pass to the bus on some operations. Finally, scsi_id gives the SCSI ID as per the device tag. It's written once at startup and never written or read afterwards, the device can do whatever it wants with the value or the variable.

The virtual method scsi_ctrl_changed is called when watched-for control lines change. Which lines are watched is defined through the bus.

The bus proposes five methods to access the lines. The read methods are ctrl_r() and data_r(). The meaning of the control bits are defined in the S_* enum of nscsi_device. The bottom three bits (INP, CTL and MSG) are setup so that masking with 7 (S_PHASE_MASK) gives the traditional numbers for the phases, which are also available with the S_PHASE_* enum.

Writing the data lines is done with data_w(scsi_refid, value).

Writing the control lines is done with ctrl_w(scsi_refid, value, mask-of-lines-to-change). To change all control lines in one call use S_ALL as the mask.

Of course, what is read is the logical-or of all of what is driven by all devices.

Finally, the method ctrl_wait_w(scsi_id, value, mask-of-wait-lines-to-change) allows to select which control lines are watched. The watch mask is per-device, and the device method scsi_ctrl_changed is called whenever a control line in the mask changes due to an action of another device (not itself, to avoid an annoying and somewhat useless recursion).

Implementing the controller is then just a matter of following the state machines descriptions, at least if they're available. The only part often not described is the arbitration/selection, which is documented in the SCSI standard though. For an initiator (which is what the controller essentially always is), it goes like this:

  • wait for the bus to be idle

  • assert the data line which number is your scsi_id (1 << scsi_id)

  • assert the busy line

  • wait the arbitration time

  • check that the of the active data lines the one with the highest number is yours

    • if no, the arbitration was lost, stop driving anything and restart at the beginning

  • assert the select line (at that point, the bus is yours)

  • wait a short while

  • keep your data line asserted, assert the data line which number is the SCSI ID of the target

  • wait a short while

  • assert the atn line if needed, de-assert busy

  • wait for busy to be asserted or timeout

    • timeout means nobody is answering at that id, de-assert everything and stop

  • wait a short while for de-skewing

  • de-assert the data bus and the select line

  • wait a short while

and then you're done, you're connected with the target until the target de-asserts the busy line, either because you asked it to or just to annoy you. The de-assert is called a disconnect.

The ncr5390 is an example of how to use a two-level state machine to handle all the events.

Creating a new SCSI device using nscsi_full_device

The base class "nscsi_full_device" is used to create HLE-d SCSI devices intended for generic uses, like hard drives, CD-ROMs, perhaps scanners, etc. The class provides the SCSI protocol handling, leaving only the command handling and (optionally) the message handling to the implementation.

The class currently only support target devices.

The first method to implement is scsi_command(). That method is called when a command has fully arrived. The command is available in scsi_cmdbuf[], and its length is in scsi_cmdsize (but the length is generally useless, the command first byte giving it). The 4096-bytes scsi_cmdbuf array is then freely modifiable.

In scsi_command(), the device can either handle the command or pass it up with nscsi_full_device::scsi_command().

To handle the command, a number of methods are available:

  • get_lun(lua-set-in-command) will give you the LUN to work on (the in-command one can be overriden by a message-level one).

  • bad_lun() replies to the host that the specific LUN is unsupported.

  • scsi_data_in(buffer-id, size) sends size bytes from buffer buffer-id

  • scsi_data_out(buffer-id, size) receives size bytes into buffer buffer-id

  • scsi_status_complete(status) ends the command with a given status.

  • sense(deferred, key) prepares the sense buffer for a subsequent request-sense command, which is useful when returning a check-condition status.

The scsi_data_* and scsi_status_complete commands are queued, the command handler should call them all without waiting.

buffer-id identifies a buffer. 0, aka SBUF_MAIN, targets the scsi_cmdbuf buffer. Other acceptable values are 2 or more. 2+ ids are handled through the scsi_get_data method for read and scsi_put_data for write.

UINT8 device::scsi_get_data(int id, int pos) must return byte pos of buffer id, upcalling in nscsi_full_device for id < 2.

void device::scsi_put_data(int id, int pos, UINT8 data) must write byte pos in buffer id, upcalling in nscsi_full_device for id < 2.

scsi_get_data and scsi_put_data should do the external reads/writes when needed.

The device can also override scsi_message to handle SCSI messages other than the ones generically handled, and it can also override some of the timings (but a lot of them aren't used, beware).

A number of enums are defined to make things easier. The SS_* enum gives status returns (with SS_GOOD for all's well). The SC_* enum gives the scsi commands. The SM_* enum gives the SCSI messages, with the exception of identify (which is 80-ff, doesn't really fit in an enum).

What's missing in scsi_full_device

  • Initiator support We have no initiator device to HLE at that point.

  • Delays A scsi_delay command would help giving more realistic timings to the CD-ROM reader in particular.

  • Disconnected operation Would first require delays and in addition an emulated OS that can handle it.

  • 16-bits wide operation needs an OS and an initiator that can handle it.

What's missing in the ncr5390 (and probably future other controllers)

  • Bus free detection Right now the bus is considered free if the controllers isn't using it, which is true. This may change once disconnected operation is in.

  • Target commands We don't emulate (vs. HLE) any target yet.