В общем случае обмен по протоколу 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
- REQUEST SENSE
- READ CAPACITY(10)
- MODE SENSE(6)
- READ(10)
- WRITE(10)
- TEST UNIT READY
- PREVENT ALLOW MEDIUM REMOVAL
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, там все проще.