В общем случае обмен по протоколу SCSI происходит так: хост посылает командный блок (CBW), далее, исходя из команды, хост может посылать блок данные, хост может принимать данные, или устройство возвращает состояние (CSW).
Рисунок 1 – Процесс обмена по протоколу SCSI
Структура командного блока (CBW):
typedef struct {
uint16_t dCBWSignatureL;
uint16_t dCBWSignatureH;
uint16_t dCBWTagL;
uint16_t dCBWTagH;
uint16_t dCBWDataTransferLengthL;
uint16_t dCBWDataTransferLengthH;
uint8_t bmCBWFlags;
uint8_t bCBWLUN;
uint8_t bCBWCBLength;
uint8_t CBWCB[16];
} scsi_cbw_t;
32-битные поля структуры разбиты по 16 бит, для того, чтобы эту структуру можно было «натянуть» на приемный буфер конечной точки. А там, как я уже говорил, 32-битный доступ запрещен.
Размер, бит
Поле
Описание
32
dCBWSignature
Число 0x43425355 («CBSU»), служащее опознавательным признаком CBW. Младший байт (0x55) передаётся первым («USBC»)
32
dCBWTag
Число, которое должно совпасть со значением поля «dCSWTag» в ответном контейнере состояния команды (CSW)
32
dCBWDataTransferLength
Объём информации, передаваемой на этапе пересылки данных, в байтах
8
bmCBWFlags
Направление передачи на этапе пересылки данных. Бит 7 = 0 для направления OUT (от хоста к устройству). Бит 7 = 1 для направления IN (от устройства к хосту). Если этап передачи данных отсутствует, данный бит игнорируется. Все остальные биты должны быть равны нулю
8
bCBWLUN
Старшие 4 бита зарезервированы и должны быть равны нулю. Младшие биты задают номер логического накопителя (LUN) (для устройств, поддерживающих несколько логических накопителей) или равны нулю
8
bCBWCBLength
Старшие три бита зарезервированы и равны нулю. Младшие 5 бит задают длину команды (CDB) внутри поля «CBWCB» в байтах. Допустимы значения в диапазоне 1..16. Все определённые к настоящему моменту командные блоки имеют длину не менее шести байт
8*16
CBWCB[16]
Командный блок
После приёма командного блока (CBW) устройство должно приготовиться, в зависимости от команды, к приёму данных в оконечную точку, работающую в режиме OUT, или передаче данных или контейнера состояния (CSW) из точки в режиме IN.
Структура контейнера состояния (CSW):
typedef struct {
uint32_t dCSWSignature;
uint32_t dCSWTag;
uint32_t dCSWDataResidue;
uint8_t bCSWStatus;
} scsi_csw_t;
Размер, бит
Поле
Описание
32
dCSWSignature
Число 0x53425355 («SBSU»), служащее опознавательным признаком CBW. Младший байт (0x55) передаётся первым
32
dCSWTag
Число из поля «dCBWTag» принятого командного блока (CBW)
32
dCSWDataResidue
Разница между dCBWDataTransferLength и реально обработанными данными
8
bCSWStatus
0x00 = успешное выполнение. 0x01 = ошибка исполнения. 0x02 = ошибка протокольной последовательности. Хост должен провести процедуру сброса и восстановления
//сразу инициализируем
scsi_csw_t CSW = {
0x53425355,
0,
0,
0
};
Команды SCSI
Команды передаются внутри командного блока CBW. Реализуем следующий набор команд:
INQUIRY
Эта команда запрашивает структуру с информацией об устройстве.
Бит EVPD – если равен 0, устройство возвращает стандартный ответ на INQUIRY; если 1 – то хост запрашивает специфическую информацию, которую можно определить по полю PAGE CODE.
Мы будем отвечать лишь на запрос вида: 12 00 00 00 24 00 (EVPD и CMDDT равны 0), иначе будем отвечать ошибкой в CSW.
Стандартный ответ на INQUIRY имеет следующий вид:
Байт
Значение
Описание
0
00
Блочное устройство прямого доступа
1
80
Съемный носитель
2
04
Версия стандарта SPC-2
3
02
Формат ответа, должен быть 02
4
1F
Объём дополнительных данных ответа в байтах. Равен длине ответа минус 4. Для длины 36 следует устанавливать в «0x20» (На самом деле, ещё на единицу меньше — для 36 байт, т.е. без блока дополнительных параметров, длина данных равна «0x1F»)
5
00
6
00
7
00
8-15
Обозначение производителя. Выдаётся старшим байтом вперёд
16-31
Обозначение изделия. Выдаётся старшим байтом вперёд
32-35
Версия изделия. Выдаётся старшим байтом вперёд
uint8_t inquiry[36] = {
0x00, //Block device
0x80, //Removable media
0x04, //SPC-2
0x02, //Response data format = 0x02
0x1F, //Additional_length = length — 5
0x00,
0x00,
0x00,
‘S’, ‘O’, ‘B’, ‘ ‘, ‘i’, ‘n’, ‘c’, ‘.’,
‘M’, ‘a’, ‘s’, ‘s’, ‘ ‘, ‘S’, ‘t’, ‘o’, ‘r’, ‘a’, ‘g’, ‘e’, ‘ ‘, ‘ ‘, ‘ ‘, ‘ ‘,
‘0’, ‘0’, ‘0’, ‘1’
};
«Скелет» функции обработки команд SCSI, с обработкой команды INQUIRY:
void SCSI_Execute(uint8_t ep_number){
uint32_t i, n;
uint32_t status;
uint8_t j;
//Натягиваем scsi_cbw_t на приемный буфер
scsi_cbw_t *cbw = (scsi_cbw_t *)endpoints[ep_number].rx_buf;
//Если пакет успешно принят
if (endpoints[ep_number].rx_flag){
//Сразу копируем значение dCBWTag в CSW.dCSWTag
CSW.dCSWTag = (cbw -> dCBWTagH << 16) | cbw -> dCBWTagL;
//Определяем пришедшую команду
switch (cbw -> CBWCB[0]){
//Если INQUIRY
case INQUIRY:
//Проверка битов EVPD и CMDDT
if (cbw -> CBWCB[1] == 0){
//Передаем стандартный ответ на INQUIRY
EP_Write(ep_number, inquiry, cbw -> CBWCB[4]);
//Заполняем поля CSW
CSW.dCSWDataResidue = ((cbw -> dCBWDataTransferLengthH << 16) | cbw -> dCBWDataTransferLengthL) — cbw -> CBWCB[4];
//Команда выполнилась успешно
CSW.bCSWStatus = 0x00;
//Посылаем контейнер состояния
EP_Write(ep_number, (uint8_t *)&CSW, 13);
} else {
//Заполняем поля CSW
CSW.dCSWDataResidue = ((cbw -> dCBWDataTransferLengthH << 16) | cbw -> dCBWDataTransferLengthL);
//Сообщаем об ошибке выполнения команды
CSW.bCSWStatus = 0x01;
//Посылаем контейнер состояния
EP_Write(ep_number, (uint8_t *)&CSW, 13);
//Подтверждаем
CSW.bCSWStatus = 0x00;
//Посылаем контейнер состояния
EP_Write(ep_number, (uint8_t *)&CSW, 13);
}
break;
//Неизвестная команда
default:
//Заполняем поля CSW
CSW.dCSWDataResidue = ((cbw -> dCBWDataTransferLengthH << 16) | cbw -> dCBWDataTransferLengthL);
//Сообщаем об ошибке выполнения команды
CSW.bCSWStatus = 0x01;
//Посылаем контейнер состояния
EP_Write(ep_number, (uint8_t *)&CSW, 13);
//Подтверждаем
CSW.bCSWStatus = 0x00;
//Посылаем контейнер состояния
EP_Write(ep_number, (uint8_t *)&CSW, 13);
break;
}
status = USB -> EPnR[ep_number];
status = SET_VALID_RX(status);
status = SET_NAK_TX(status);
status = KEEP_DTOG_TX(status);
status = KEEP_DTOG_RX(status);
USB -> EPnR[ep_number] = status;
endpoints[ep_number].rx_flag = 0;
}
}
REQUEST_SENSE
Если хост принял CSW с полем bCSWStatus = 1, он может послать команду REQUEST_SENSE, чтобы запросить пояснительные данные (SENSE DATA).
Вот пояснительные данные, говорящие о неизвестной команде:
uint8_t sense_data[18] = {
0x70, //VALID = 1, RESRONSE_CODE = 0x70
0x00,
0x05, //S_ILLEGAL_REQUEST
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
Добавляем обработку команды REQUEST SENSE:
case REQUEST_SENSE:
//Отправляем пояснительные данные
EP_Write(ep_number, sense_data, 18);
//Заполняем поля CSW
CSW.dCSWDataResidue = ((cbw -> dCBWDataTransferLengthH << 16) | cbw -> dCBWDataTransferLengthL) — cbw -> CBWCB[4];
//Команда выполнилась успешно
CSW.bCSWStatus = 0x00;
//Посылаем контейнер состояния
EP_Write(ep_number, (uint8_t *)&CSW, 13);
break;
READ CAPACITY 10
Используется, для того чтобы определить объем памяти устройства. На этапе пересылки данных устройство возвращает структуру, содержащую логический адрес (LBA) последнего блока на носителе и размер блока в байтах. Отметим, что команда запрашивает логический адрес (LBA) последнего блока, а не количество блоков на носителе. Логический адрес первого блока равен нулю, таким образом, логический адрес последнего блока на единицу меньше количества блоков.
Возвращаемая структура:
uint8_t capacity[8] = {
0x00, 0x00, 0x0F, 0xFF, //Addr last blocks = 2M/512 — 1
0x00, 0x00, 0x02, 0x00 //Size blocks = 512 bytes
};
Обработка:
case READ_CAPACITY_10:
//Передаем структуру
EP_Write(ep_number, capacity, 8);
//Заполняем и передаем CSW
CSW.dCSWDataResidue = ((cbw -> dCBWDataTransferLengthH << 16) | cbw -> dCBWDataTransferLengthL) — cbw -> CBWCB[4];
CSW.bCSWStatus = 0x00;
EP_Write(ep_number, (uint8_t *)&CSW, 13);
break;
MODE SENSE 6
Ответ на нее всегда одинаковый:
uint8_t mode_sense_6[4] = {
0x03, 0x00, 0x00, 0x00,
};
case MODE_SENSE_6:
EP_Write(ep_number, mode_sense_6, 4);
CSW.dCSWDataResidue = ((cbw -> dCBWDataTransferLengthH << 16) | cbw -> dCBWDataTransferLengthL) — cbw -> CBWCB[4];
CSW.bCSWStatus = 0x00;
EP_Write(ep_number, (uint8_t *)&CSW, 13);
break;
READ 10
Здесь нас интересуют биты 2-5 – начальный адрес читаемого блока данных, и биты 7-8 – количество читаемых блоков.
case READ_10:
//записываем в I начальный адрес читаемого блока
i = ((cbw -> CBWCB[2] << 24) | (cbw -> CBWCB[3] << 16) | (cbw -> CBWCB[4] << 8) | (cbw -> CBWCB[5]));
//записываем в n адрес последнего читаемого блока
n = i + ((cbw -> CBWCB[7] << 8) | cbw -> CBWCB[8]);
//выполняем чтение и передачу блоков
for ( ; i < n; i++){
//Читаем блок из FLASH, помещаем в массив uint8_t buf[512]
AT45DB161_Read_Data(i, 0, 512, buf);
//Так как размер конечной точки 64 байта, передаем 512 байт за 8 раз
for (j = 0; j < 8; j++){
//Передаем часть буфера
EP_Write(ep_number, (uint8_t *)&buf[64*j], 64);
}
}
//Заполняем и посылаем CSW
CSW.dCSWDataResidue = ((cbw -> dCBWDataTransferLengthH << 16) | cbw -> dCBWDataTransferLengthL) — cbw -> CBWCB[4];
CSW.bCSWStatus = 0x00;
EP_Write(ep_number, (uint8_t *)&CSW, 13);
break;
WRITE 10
Здесь нас интересуют биты 2-5 – начальный адрес записываемого блока данных, и биты 7-8 – количество записываемых блоков.
case WRITE_10:
//записываем в I начальный адрес записываемого блока
i = ((cbw -> CBWCB[2] << 24) | (cbw -> CBWCB[3] << 16) | (cbw -> CBWCB[4] << 8) | (cbw -> CBWCB[5]));
//записываем в n адрес последнего записываемого блока
n = i + ((cbw -> CBWCB[7] << 8) | cbw -> CBWCB[8]);
//выполняем чтение и запись блоков
for ( ; i < n; i++){
//Так как размер конечной точки 64 байта, читаем 512 байт за 8 раз
for (j = 0; j < 8; j++){
EP_Read(ep_number, (uint8_t *)&buf[64*j]);
}
//Записываем прочитанный блок во FLASH
AT45DB161_PageProgram(i, buf, 512);
}
//Заполняем и посылаем CSW
CSW.dCSWDataResidue = ((cbw -> dCBWDataTransferLengthH << 16) | cbw -> dCBWDataTransferLengthL) — cbw -> CBWCB[4];
CSW.bCSWStatus = 0x00;
EP_Write(ep_number, (uint8_t *)&CSW, 13);
break;
TEST UNIT READY
Команда «TEST UNIT READY» используется хостом для выяснения степени готовности устройства к работе. Команда не предусматривает этапа пересылки данных. Если устройство готово, то оно возвращает контейнер состояния CSW со значением поля «bCSWStatus» равным «0x00». Если носитель не готов, устройство обновляет подробные данные о состоянии и возвращает контейнер состояния (CSW) со значением поля «bCSWStatus» равным «0x01» (ошибка исполнения). Хост может запросить подробную информацию о состоянии командой «REQUEST SENSE». Все блоковые устройства (SBC) должны поддерживать эту команду.
case TEST_UNIT_READY:
CSW.dCSWDataResidue = ((cbw -> dCBWDataTransferLengthH << 16) | cbw -> dCBWDataTransferLengthL);
CSW.bCSWStatus = 0x00;
EP_Write(ep_number, (uint8_t *)&CSW, 13);
break;
PREVENT ALLOW MEDIUM REMOVAL
Команда «PREVENT ALLOW MEDIUM REMOVAL» разрешает или запрещает извлечение носителя из устройства. 2-х битовое поле «PREVENT» команды устанавливается в состояние «00b» для разрешения или в состояние «01b» для запрета извлечения. Данная команда не подразумевает этап пересылки данных.
Так как у нас нет съемных носителей, то отвечаем успешным пакетом CSW.
case PREVENT_ALLOW_MEDIUM_REMOVAL:
CSW.dCSWDataResidue = 0;
CSW.bCSWStatus = 0x00;
EP_Write(ep_number, (uint8_t *)&CSW, 13);
break;
Заключение
Теперь если прошить микроконтроллер, то должно появиться запоминающее устройство. Диск может не открыться, потому что на флешке наверняка нет файловой системы, поэтому отформатируйте диск и пользуйтесь. Надеюсь эти статьи помогли разобраться с USB. Как-нибудь напишу про HID, там все проще.
Прикрепленные файлы:
- stm32f0_usb_msd.rar (119 Кб)