RT-Thread RTOS
An open source embedded real-time operating system
SCSI host and commands

SCSI subsystem

RT-Thread provides a small SCSI core for storage backends: a host (rt_scsi_host) scans targets, issues commands through **rt_scsi_ops::transfer**, and attaches type-specific upper drivers (disk sd*, CD-ROM, …) that register **rt_blk_disk** devices.

The core is not a device-model bus (see comment in scsi.c) — there is no rt_scmi-style match/probe bus. Host adapters call **rt_scsi_host_register()** after filling **rt_scsi_host**.

Piece Path
API / CDB layouts components/drivers/include/drivers/scsi.h
Core components/drivers/scsi/scsi.c
Block disk (type 0) components/drivers/scsi/scsi_sd.c
CD-ROM (type 5) components/drivers/scsi/scsi_cdrom.c

Kconfig: RT_USING_SCSI (needs RT_USING_DM). Options RT_SCSI_SD / RT_SCSI_CDROM (need RT_USING_BLK).


End-to-end flow

Host adapter (AHCI / UFS / VirtIO-SCSI / …)
fills rt_scsi_host { dev, ops, max_id, max_lun, parallel_io }
calls rt_scsi_host_register()
For each (id, lun): INQUIRY → devtype / removable
unknown type or inquiry fail → skip LUN
devtype >= SCSI_DEVICE_TYPE_MAX → shrink scan, stop
scsi_device_setup(sdev): optional reset → TEST UNIT READY (5 s) → driver_table[devtype].probe
scsi_sd_probe / scsi_cdrom_probe: READ CAPACITY → rt_hw_blk_disk_register ("sd*a", …)
Applications use blk API (@ref page_device_blk, @ref page_device_disk)

Unregister: rt_scsi_host_unregister() walks **lun_nodes**, optional **ops->reset**, type **remove**, frees each **rt_scsi_device**.


Data structures (<tt>scsi.h</tt>)

Host and device

struct rt_scsi_host {
struct rt_device *dev; /* required; DMA / naming */
const struct rt_scsi_ops *ops;
rt_size_t max_id; /* both must be non-zero */
rt_size_t max_lun;
rt_bool_t parallel_io; /* hint for blk layer */
rt_list_t lun_nodes; /* filled by register */
};
struct rt_scsi_device {
struct rt_scsi_host *host;
rt_list_t list;
rt_size_t id;
rt_size_t lun;
rt_uint32_t devtype; /* from INQUIRY */
rt_uint32_t removable;
rt_size_t last_block; /* filled by READ CAPACITY helpers */
rt_size_t block_size;
void *priv; /* scsi_sd / scsi_cdrom */
};
struct rt_scsi_ops {
rt_err_t (*reset)(struct rt_scsi_device *sdev); /* optional */
rt_err_t (*transfer)(struct rt_scsi_device *sdev, struct rt_scsi_cmd *cmd); /* required */
};
Definition: rtdef.h:1369
Definition: rttypes.h:133

Command envelope

struct rt_scsi_cmd {
union { /* CDB: inquiry, read10, write16, … */ } op;
rt_size_t op_size;
union {
struct { /* fixed response structs */ } ;
struct { void *ptr; rt_size_t size; } data; /* DMA buffer for read/write */
} data;
};

Build CDBs with packed structs in **scsi.h** (rt_scsi_inquiry, rt_scsi_read10, …) or fill **cmd.op** manually. Set **op_size** to the CDB length your transport accepts.

High-level helpers in **scsi.c** (rt_scsi_inquiry, rt_scsi_read10, rt_scsi_write16, rt_scsi_read_capacity10, …) assemble **rt_scsi_cmd** and call **host->ops->transfer**. On failure they often issue **rt_scsi_request_sense**.

**rt_scsi_cmd_is_write(cmd)** — used by VirtIO-SCSI to pick IN vs OUT virtqueue buffers.


<tt>rt_scsi_host_register</tt> / <tt>unregister</tt>

Before register (caller)

Field Rule
dev Non-NULL
ops Non-NULL; **transfer** required
max_id / max_lun Both non-zero; define scan grid
parallel_io Set if adapter can pipeline multiple LUN commands
lun_nodes Do not pre-fill; core initializes the list

Scan behavior (<tt>scsi.c</tt>)

  1. Nested loop **id ∈ [0, max_id)**, **lun ∈ [0, max_lun)**.
  2. **rt_scsi_inquiry(&tmp_sdev, NULL)** — failure skips the LUN.
  3. If **devtype >= SCSI_DEVICE_TYPE_MAX**, scan stops and **max_id / max_lun** are clipped to the current index (simple bus assumption).
  4. Allocated **rt_scsi_device** is copied from the temp device (id, lun, devtype, removable).
  5. **scsi_device_setup**: optional **reset** → wait TEST UNIT READY (up to ~5 s) → **driver_table[devtype].probe**.

Returns **RT_EOK** if at least one LUN probed; **-RT_EEMPTY** if none; **-RT_EINVAL** / **-RT_ENOMEM** on bad args or OOM (partial LUNs may remain on ENOMEM mid-scan).

<tt>transfer</tt> contract

  • Fill or consume **cmd->data** (ptr + size for READ/WRITE; embedded structs for INQUIRY / capacity).
  • Return **RT_EOK** on command completion; RT error codes on transport failure.
  • Sense: adapters may copy sense into **cmd->data.request_sense**; helpers already call REQUEST SENSE after some failures.

Upper-layer drivers (<tt>driver_table</tt>)

devtype (SCSI_DEVICE_TYPE_*) Kconfig Probe Block device
0x00 DIRECT RT_SCSI_SD scsi_sd_probe sd*a — read/write/sync/erase
0x05 CDROM RT_SCSI_CDROM scsi_cdrom_probe sr*a — read-only

Other inquiry types log not supported and are skipped.

**scsi_sd_probe**: READ CAPACITY 10; if last LBA is 0xffffffff, READ CAPACITY 16 and use READ/WRITE 16 ops; sets geometry from **sdev->block_size** / **last_block**; **rt_hw_blk_disk_register**. Honors **host->parallel_io** on the disk.

**scsi_cdrom_probe**: capacity + read-only **rt_blk_disk** (uses READ 10/12/16 by LBA width).


Built-in SCSI hosts (examples)

These embed **struct rt_scsi_host parent** and call **rt_scsi_host_register** after hardware init:

Backend Source Notes
AHCI ata/ahci.c SATA/ATAPI → ahci_scsi_ops.transfer; see ATA / AHCI
UFS ufs/ufs.c UTP carries SCSI CDBs; ufs_host_ops, often parallel_io = RT_TRUE
VirtIO SCSI virtio/virtio-scsi.c RT_VIRTIO_DRIVER_EXPORT; 3 queues; hotplug via config_changed + work queue re-register

Typical host setup (pattern):

struct my_hba {
struct rt_scsi_host parent;
/* private */
};
static rt_err_t my_scsi_transfer(struct rt_scsi_device *sdev, struct rt_scsi_cmd *cmd)
{
/* map id/lun, push CDB + data to hardware */
return RT_EOK;
}
static const struct rt_scsi_ops my_scsi_ops = {
.reset = my_scsi_reset, /* optional */
.transfer = my_scsi_transfer,
};
/* after controller ready */
hba->parent.dev = &controller_dev;
hba->parent.ops = &my_scsi_ops;
hba->parent.max_id = 1;
hba->parent.max_lun = 8;
hba->parent.parallel_io = RT_FALSE;
rt_scsi_host_register(&hba->parent);

Teardown: **rt_scsi_host_unregister(&hba->parent)** before freeing the controller.


Command reference (<tt>scsi.h</tt>)

Opcodes and structs are defined for common storage commands:

Opcode macro Struct Helper
RT_SCSI_CMD_INQUIRY rt_scsi_inquiry rt_scsi_inquiry()
RT_SCSI_CMD_TEST_UNIT_READY rt_scsi_test_unit_ready rt_scsi_test_unit_ready()
RT_SCSI_CMD_READ_CAPACITY10/16 capacity structs rt_scsi_read_capacity10/16()
RT_SCSI_CMD_READ10/12/16 read structs rt_scsi_read10/12/16()
RT_SCSI_CMD_WRITE10/12/16 write structs rt_scsi_write10/12/16()
RT_SCSI_CMD_SYNCHRONIZE_CACHE10/16 sync cache rt_scsi_synchronize_cache10/16()
RT_SCSI_CMD_WRITE_SAME10/16 write same rt_scsi_write_same10/16() (erase path in scsi_sd)
Mode select/sense mode structs rt_scsi_mode_* (auto-refresh in scsi_sd)

Use these when implementing **transfer** for a new HBA or when extending **driver_table** for another device type.


Debugging

Symptom Checks
-RT_EEMPTY No LUN answered INQUIRY — wiring, power, max_id/max_lun, firmware
-RT_ETIMEOUT in setup Media not ready — TEST UNIT READY loop
-RT_ENOSYS Unsupported devtype — enable RT_SCSI_SD / RT_SCSI_CDROM or add driver_table entry
READ/WRITE fail Sense via rt_scsi_request_sense; verify cmd->data.size is in sectors for READ/WRITE helpers
VirtIO hotplug virtio_scsi_config_changed → rescan; log virtio.dev.scsi

See also