Технологии древних: ATAPI IDE, часть вторая, мы всё ещё не готовы

от автора

В процессе обсуждения первой части пришло понимание, что контроллер IDE всё же следует наделить некоторым «умом». Шина USB заточена под поточную передачу данных и поэтому Ping-Pong протоколы на ней откровенно тормозят, приводя к не оптимальному использованию самой шины. Это выливается и в тормоза на шине IDE, поэтому было принято волевое решение создать вторую версию контроллера, более умного, но в то же время не сильно усложнённого, чтобы он позволял как прямое управление шиной так и мог реализовывать базовые протоколы IDE без необходимости обращения к хосту. Если вам всё ещё интересна данная тема — добро пожаловать под кат.


При выборе контроллера преследовалась цель применить что-то, что уже есть в наличии и обязательно уйти от «ногодрыга», у которого есть свои известные проблемы. Выбор пал на STM32F407VGT в корпусе TSOP100 фирмы ST. У этого контроллера в этом корпусе доступен FSMC контроллер на 16 бит, что нам и нужно. А ещё, у него есть SDIO и можно прикрутить дополнительно uSD карту памяти как быстрый и практически безграничный буфер для данных. Так родился концепт второй версии контроллера IDE для изучения устройств IDE/ATAPI а так же для приближения к нашей цели: создать IDE ATAPI эмулятор CD/DVD привода. Вырисовалась вот такая схема:

Типовое включение контроллера и CPLD

Типовое включение контроллера и CPLD
Фото готового устройства, в этот раз никакого ЛУТа, увы...

Фото готового устройства, в этот раз никакого ЛУТа, увы…

CPLD выполняет роль буфера, чтобы усилить сигналы контроллера. Почему не поставить простые согласователи уровня? Потому, что в корпусе TSOP100 у STM32F407 только 1 сигнал FSMC_NE1, а для IDE требуется их 2. Так же, шина данных тут мультиплексирована и 16 младших адресов используют те же ножки что и 16 бит данных. Правда, при этом есть несколько старших адресов, но учитывая что доступен только один сигнал выбора смысла в них нет — всё равно надо делать внешнюю по отношению к контроллеру логику. Поэтому, это будет просто CPLD, которая будет демультиплексировать шину адреса и уже из неё формировать CS1X и CS3X. К тому же, в CPLD будет синхронизатор входящих сигналов, в том числе арбитражный IORDY, который можно напрямую подключить к FSMC_nWAIT. А так же, там можно расположить регистры управления сигналами RESET и CSEL на шине IDE. Проект CPLD достаточно прост:

Код для CPLD
// Шина IDE на STM32F4 // Адрес     Выбор  Чтение   Запись // 0000 : 0  CS1X  DataPIO   DataPIO // 0001 : 1  CS1X  Error     Features // 0010 : 2  CS1X  SecCount  SecCount // 0011 : 3  CS1X  SecNumber SecNumber // 0100 : 4  CS1X  CylLow    CylLow // 0101 : 5  CS1X  CylHigh   CylHigh // 0110 : 6  CS1X  DevHead   DevHead // 0111 : 7  CS1X  Status    Command  // 1000 : 8  ----  DataDMA   DataDMA // 1001 : 9  ---- // 1010 : A  ---- // 1011 : B  ---- // 1100 : C  ---- CSEL // 1101 : D  ---- // 1110 : E  CS3X  AltStatus Control // 1111 : F  ---- RESET module IDE_STM32( // Шина STM32F4 inputCLK,// Такты 56МГц inout[15:0]FSMC_AD,// Мультиплексированная шина адреса и данных inputFSMC_NL,// Защёлка адреса inputFSMC_NE1,// Выбор устройства inputFSMC_NOE,// Строб чтения inputFSMC_NWE,// Строб записи outputreg FSMC_NWAIT,// Сигнал готовности устройства outputreg STAT_INTRQ,// Проброс статуса: INTRQ outputreg STAT_DMARQ,// Проброс статуса: DMARQ outputreg STAT_DASP,// Проброс статуса: DASP outputreg STAT_PDIAG,// Проброс статуса: PDIAG // Шина IDE inout[15:0]DD,// Шина данных IDE outputreg [2:0]DA,// Шина адреса outputCS1X,// Основные регистры outputCS3X,// Дополнительные регистры outputDIOR,// Сигнал чтения outputDIOW,// Сигнал записи outputDMACK,// Сигнал подтверждения DMA outputreg RESET,// Сигнал сброса inoutCSEL,// Сигнал выбора по кабелю inputIORDY,// Сигнал готовности inputINTRQ,// Сигнал запроса прерывания inputDMARQ,// Сигнал запроса DMA inputDASP,// inputPDIAG,// // Лампочка outputreg DRIVE// Лампочка привода );  // Шины assign CSEL = (rCSEL) ? 1'b0 : 1'bZ; assign DD[15:0]  = (~FSMC_NE1 & ~FSMC_NWE) ? FSMC_AD[15:0] : 16'hZZ; assign FSMC_AD[15:0] = (~FSMC_NE1 & ~FSMC_NOE) ? DD[15:0] : 8'hZZ;  // Переменные reg BANK;// Банк адресации reg rCSEL; wire REG_CSEL; wire REG_RESET;  // Комбинаторика assign REG_CSEL  = ~FSMC_NE1 & BANK & DA[2] & ~DA[1] & ~DA[0]; assign REG_RESET = ~FSMC_NE1 & BANK & DA[2] & DA[1] & DA[0]; assign CS1X = ~(~FSMC_NE1 & ~BANK); assign CS3X = ~(~FSMC_NE1 & BANK & DA[2] & DA[1] & ~DA[0]); assign DMACK = ~(DMARQ & ~FSMC_NE1 & BANK & ~DA[2] & ~DA[1] & ~DA[0]); assign DIOR = ~(~FSMC_NE1 & ~FSMC_NOE & (~BANK | (~(DA[2] ^ DA[1]) & ~DA[0]))); assign DIOW = ~(~FSMC_NE1 & ~FSMC_NWE & (~BANK | (~(DA[2] ^ DA[1]) & ~DA[0])));  // Синхронизация адреса always @(posedge FSMC_NL) begin // Сохраняем адрес {BANK,DA[2:0]} <= FSMC_AD[3:0]; end  // Синхронная логика always @(posedge CLK) begin // Лампочка DRIVE <= DASP; // Статусы {STAT_PDIAG,STAT_DASP,STAT_DMARQ,STAT_INTRQ} <= {PDIAG,DASP,DMARQ,INTRQ}; // Синхронизируем IORDY FSMC_NWAIT <= REG_CSEL | REG_RESET | FSMC_NE1 | IORDY; // Сохраняем CSEL if (REG_CSEL & ~FSMC_NWE) rCSEL <= FSMC_AD[0]; // Сохраняем RESET if (REG_RESET & ~FSMC_NWE) RESET <= ~FSMC_AD[0]; end  // Выход endmodule 

Как видно из кода CPLD реализует 16 адресных ячеек на 16 бит каждая, которые доступны и на чтение и на запись. Первые 8 реализуют 8 ячеек с активацией сигнала CS1X на шине IDE. Вторые 8 разделены на 3 секции:

  • Первая секция это доступ к регистру данных (ADR=0) IDE, но с использованием сигнала DMACK (при наличии сигнала DMARQ). Это позволяет имитировать DMA транзакцию на шине.

  • Вторая секция это регистр альтернативного статуса, который вызывается при ADR=6 и CS3X. Альтернативный статус используется для чтения статуса устройства без сброса запроса прерывания и DMA.

  • Третья секция это внутренние ресурсы CPLD. Обращение к ним не вызывает сигналов строба чтения или записи данных на шине IDE. Используется для управления сигналами RESET и CSEL.

Таким образом, CPLD просто согласующий мост между FSMC и шиной IDE. Может показаться, что требуется согласование уровней между ногами FSMC и CPLD, однако букварь на чип нам говорит, что у него все ноги типа FT.

Выборка из букваря

Подключение остальной периферии (USB и SDIO) сделано типично по букварю от ST и интереса не вызывает.

Переходим к программной части. Как уже было сказано выше, USB будет использовать для CLI. Поэтому, будет использоваться обычный драйвер ST vCOM а в качестве программы — любой терминал, умеющий работать с COM портом. Всё остальное будет запрограммировано в контроллер.

Доступ к шине IDE будет через FSMC контроллер, который отражается прямо в память ядра и его ресурсы доступны обычными командами чтения и записи LDR/STR. При этом тайминг доступа настраивается в самом контроллере и обращение получается атомарным. Отсутствие атомарности главный недостаток «ногодрыга». Настройка контроллера предельно простая:

__HAL_RCC_FMCEN_CLK_ENABLE(); FSMC_Bank1->BTCR[ 0 ] = /*| FSMC_BCR1_ASYNCWAIT | FSMC_BCR1_WAITEN |*/ FSMC_BCR1_WREN /*| FSMC_BCR1_WAITCFG*/ | 0x80 | FSMC_BCR1_FACCEN | FSMC_BCR1_MWID_0 | FSMC_BCR1_MTYP_1 | FSMC_BCR1_MUXEN | FSMC_BCR1_MBKEN; // Тайминги чтения FSMC_Bank1->BTCR[ 1 ] = FSMC_BTR1_BUSTURN_2 | FSMC_BTR1_DATAST_5 | FSMC_BTR1_DATAST_4 | FSMC_BTR1_DATAST_3 | FSMC_BTR2_ADDHLD_1 | FSMC_BTR2_ADDSET_3; // Тайминги записи FSMC_Bank1E->BWTR[ 0 ] = FSMC_BWTR1_BUSTURN_2 | FSMC_BWTR1_DATAST_5 | FSMC_BWTR1_DATAST_4 | FSMC_BWTR1_DATAST_3 | FSMC_BWTR1_ADDHLD_1 | FSMC_BWTR1_ADDSET_3; 

Здесь временно отключен сигнал FSMC_nWAIT, он будет задействован позже. А время доступа настроено на 340 нс и для чтения и для записи. Этого должно хватать для большинства приводов, как старых так и новых. Адрес у FSMC_NE1 равен 0x60000000, поэтому объявляем структуру доступа к регистрам и прикручиваем её к этому адресу:

// Структура порта IDE typedef struct {// CS1X __IO uint16_tDATA_PIO;// Регистр 0: DATA, режим PIO, 16 бит __IO uint16_tFEATURE;// Регистр 1: STATUS / FEATURES, 8 бит __IO uint16_tSEC_COUNT;// Регистр 2: SECTOR COUNT, 8 бит __IO uint16_tSEC_NUMBER;// Регистр 3: SECTOR NUMBER, 8 бит __IO uint16_tCYL_LOW;// Регистр 4: CYLINDER LOW, 8 бит __IO uint16_tCYL_HIGH;// Регистр 5: CYLINDER HIGH, 8 бит __IO uint16_tCONTROL;// Регистр 6: CONTROL/DEVICE&HEAD, 8 бит __IO uint16_tCOMMAND;// Регистр 7: STATUS / COMMAND, 8 бит // DMACK __IO uint16_tDATA_DMA;// Регистр 0: DATA, режим DMA, 16 бит // Резерв __IO uint16_tRES0[ 3 ];// Резерв // Управление сигналом CSEL __IO uint16_tCSEL;// Регистр управления сигналом CSEL // Резерв __IO uint16_tRES1[ 1 ];// Резерв // CS3X __IO uint16_tALT_STATUS;// Регистр 6: ALT STATUS, 8 бит // Управление сигналом RESET __IO uint16_tRESET;// Регистр управления сигналом RESET } tIDE;  #define IDE_BASE_ADR(uint32_t) 0x60000000 #define IDE((tIDE *) IDE_BASE_ADR) 

Теперь для доступа к нужным регистрам достаточно написать, например, IDE->DATA_PIO, прямо как со стандартными структурами у ST. Управление сбросом идёт через IDE->RESET, куда нужно записать 0x0001 для активации сброса и 0x0000 для деактивации его. Так что сброс устройства происходит такой последовательностью:

IDE->RESET = 0xFFFF; HAL_Delay( 500 ); IDE->RESET = 0x0000; HAL_Delay( 3000 );

Второе ожидание на 3 секунды обусловлено тем, что устройству необходимо время, прежде чем оно будет способно адекватно принимать команды. В PC это достигается тем, что аппаратный сброс происходит вместе со всем PC и пока происходит POST и прочие процедуры, этого времени хватает приводам проинициализироваться. А у нас сброс может поступить в любой момент, поэтому необходимо выжидать.

Результат выполнения команды сброса в CLI

Результат выполнения команды сброса в CLI

Анализ логов, которые были приведены в первой статье, показал, что используется только 2 команды:

  • 0xA1 — IDENTIFY_ATAPI_DEVICE

  • 0xA0 — PACKET

Остальные команды шины IDE не замечены, даже в режиме DMA. Собственно, так и должно быть, ведь мы разбираем ATAPI привод, а он использует пакетные SCSI команды. Сами пакетные команды описаны в других стандартах, например, SCSI-3 и MMC-3. Буквари на них есть в интернете, к ним мы вернёмся когда дойдём до полноценного управления устройством и, собственно, когда начнём строить эмулятор. На данный момент, речь о шине IDE, которая хостит ATAPI. И начнём мы с команды 0xA1 — IDENTIFY_ATAPI_DEVICE. Для её активации следует заполнить регистры IDE следующим образом:

Как видно, имеет значение только бит DEV и сама команда

Как видно, имеет значение только бит DEV и сама команда

Мы же помним тот момент, что на одном кабеле IDE может быть 2 устройства, один в режиме MASTER/SINGLE а второй в режиме SLAVE. Вот именно бит DEV в регистре DEVICE/HEAD и указывает, к какому устройству в данный момент идёт обращение. Этот бит слушают оба устройства, но откликается лишь то, у которого настройка джампера MASTER/SLAVE соответствует. DEV=0 соответствует выбору MASTER. PC записывает сюда 0xA0, устанавливая оба бита «obs» (obsolete — устаревшие) в 1. Поступим так же. Эпюры выполнения команды:

Вся транзакция занимает порядка 356 микросекунд

Вся транзакция занимает порядка 356 микросекунд

Обнаружение устройства немного более сложная задача, чем просто подать команду IDENTIFY_ATAPI_DEVICE. Для начала следует убедиться, что устройство существует физически. Для этого делается 2 парных транзакции записи и чтения тестового значения в регистр SEC_COUNT (счётчик секторов), который доступен всегда как ячейка памяти. Затем, если регистр существует (возвращает записанные данные) делается чтение статуса STATUS для сброса всех запросов, которые могут быть в устройстве. И после чего посылается команда IDENTIFY_ATAPI_DEVICE последовательной записью 0xA0 в регистр DEVICE/HEAD и 0xA1 в регистр COMMAND/STATUS.

Установка команды видна как две подряд идущих записи максимально близко друг к другу в правой части графика

Установка команды видна как две подряд идущих записи максимально близко друг к другу в правой части графика

После установки команды нужно вычитывать статус до тех пор, пока устройство не снимет бит статуса BSY и не установит бит статуса DRQ. Вот карта битов для ожидания результата команды:

Все значащие биты статуса

Все значащие биты статуса

Установка других флагов не допускается. Собственно, установка DF (DRIVE_FAULT) или ERR (ERROR) говорит об неисправности устройства или его прошивки, потому что команда IDENTIFY_ATAPI_DEVICE является внутренней и не использует механику с носителем. А вот так выглядит вычитка ответа, которая составляет 256 слов (512 байт):

Плотно идущие транзакции чтения данных - недостижимая вещь для первой версии контроллера!

Плотно идущие транзакции чтения данных — недостижимая вещь для первой версии контроллера!
Результат команды в CLI с частичным парсингом ответа

Результат команды в CLI с частичным парсингом ответа
Код процедуры, которая обнаруживает подключённое устройство
// Обнаружение привода FunctionalState IDE_DriveDetect( uint8_t *pBuf, uint32_t Size ) {// Локальные переменные FunctionalState Res; tATAPI_ID *pATAPI_ID; uint32_t Cnt; uint16_t *PBuf,Data; // Инит Res = DISABLE; Size /= 2; PBuf = (uint16_t *)pBuf; pATAPI_ID = (tATAPI_ID *)pBuf; // Начинаем проверку привода while ( Size > 0 ) {// Пробуем запись 0x0A в регистр сектора IDE->SEC_COUNT = 0x000A; // Задержка 2 мкс IDE_uDelay( 1 ); // Пробуем считать записанное if ( (IDE->SEC_COUNT & 0x00FF) != 0x000A ) { break; } // Задержка 2 мкс IDE_uDelay( 1 ); // Пробуем запись 0x05 в регистр сектора IDE->SEC_COUNT = 0x0005; // Задержка 2 мкс IDE_uDelay( 1 ); // Пробуем считать записанное if ( (IDE->SEC_COUNT & 0x00FF) != 0x0005 ) { break; } // Задержка 2 мкс IDE_uDelay( 1 ); // Пробуем запись 0xA0 в регистр управления IDE->CONTROL = 0x00A0; // Задержка 2 мкс IDE_uDelay( 1 ); // Пробуем считать записанное if ( (IDE->CONTROL & 0x00FF) != 0x00A0 ) { break; } // Задержка 2 мкс IDE_uDelay( 1 ); // Считываем статус if ( (IDE->COMMAND & 0x0081) != 0x0000 ) { break; } // Задержка 2 мкс IDE_uDelay( 1 ); // Устанавливаем команду чтения ID IDE->CONTROL = 0x00A0; IDE->COMMAND = 0x00A1; // Ожидаем устройство Cnt = 0; while ( ((IDE->COMMAND & STATUS_BSY) != 0x0000) && (Cnt < 1000) ) { IDE_uDelay( 1 ); Cnt++; } // Произошёл запрос или снятие BUSY? if ( Cnt > 0 ) {// Ожидаем данные while ( (IDE->COMMAND & STATUS_DRDY) == 0x0000 ) { __NOP(); } // Считываем данные Cnt = 0; while ( (Cnt < 256) && (Cnt < Size) ) {// Считываем данные Data = IDE->DATA_PIO; // Меняем местами байты if ( ((Cnt >= 10) && (Cnt <= 19)) || ((Cnt >= 23) && (Cnt <= 26)) || ((Cnt >= 27) && (Cnt <= 46)) ) {// Если мы на строках - обмениваем байты Data = (Data >> 8) | (Data << 8); } // Сохраняем данные *(PBuf) = Data; // Следующее слово Cnt++; PBuf++; } } else {// Выход с ошибкой break; } // Всё пучком Res = ENABLE; // Выход break; } // Выход return Res; }

Обратите внимание, что для некоторых полей ответа происходит обмен старшего и младшего байта в слове. Это обусловлено тем, что строки тут хранятся в big endian:

Вот мы и добрались до команды PACKET. Её карта тоже достаточно проста:

Флагов то уже побольше, да?

Флагов то уже побольше, да?

Так как это уже полноценная команда, то и параметров у неё побольше. Биты OVL и DMA относятся к арбитражу, OVL говорит о том, что эта команда может перекрывать по времени предыдущую/следующую а DMA говорит о том, что результат этой команды будет передан через DMA транзакции. Т.е., оба бита относятся к оптимизации времени использования шины и привода. TAG (метка) указывает на очерёдность команды в очереди (при использовании OVL). А содержимое регистров CYL_HI:CYL_LOW указывает на максимальный размер пакета пересылаемых данных за 1 транзакцию DRQ (и для DMA, и для PIO). Это полезно если у хоста небольшой буфер в драйвере устройства. Ну и может пригодиться нам, т.к. памяти в контроллере тоже не бесконечное количество. Результат выполнения команды тоже отличается:

Больше флагов Богу флагов!

Больше флагов Богу флагов!

I/O указывает на направление передачи данных (0 — запись в привод), C/D показывает, что ожидает привод (0 — команда, 1 — данные). SERV взводится если перекрываемая команда в очереди исполнена. Остальные флаги ведут себя так же, как и у IDENTIFY_ATAPI_DEVICE. Перекрытием команд мы пока пользоваться не будем, поэтому рассмотрим обычную транзакцию чтения сектора с CD.

Чтение сектора занимаем приличное время из-за ожидания носителя

Чтение сектора занимаем приличное время из-за ожидания носителя

Устанавливаем параметры для команды PACKET: FEATURES = 0x00, CYL_HI:CYL_LO = 0xC00, DEVICE = 0xA0. Затем вычитываем STATUS для очистки запросов и подаём команду COMMAND = 0xA0. Устройство сразу же ответило, что ждёт от нас данные пакета:

Подача команды PACKET и, собственно самого пакета

Подача команды PACKET и, собственно самого пакета

IDENTIFY_ATAPI_DEVICE показал, что у этого устройства используется пакет размером в 12 байт (6 слов). Структура пакета при этом следующая:

Байты пакета группируются в слова в порядке little endian

Байты пакета группируются в слова в порядке little endian

В логе видно, что команда чтения сектора 0x28, номер LBA = 0x00000011 а длина передачи 0x00000001 (1 сектор). После принятия устройством пакета не перекрываемой команды оно сразу же пытается её исполнить. При этом выставляя статусы по мере выполнения команды:

Поллинг регистра статуса с интервалом в 2 мс

Поллинг регистра статуса с интервалом в 2 мс

Необходимо следить за состоянием регистра STATUS или настроить приём сигнала INTRQ, последний позволяет устройству не торчать в ожидании а выполнять что-то полезное в процессе ожидания. Когда устройство выполнит запрос оно выставит INTRQ и соответствующие флаги: сначала установится флаг готовности данных DRQ а затем снимется флаг занятости устройства BSY:

Вычитка данных занимает всего 653 микросекунды

Вычитка данных занимает всего 653 микросекунды

В этот момент следует переходить к фазе чтения данных:

Бодро вычитываем данные

Бодро вычитываем данные

Как только получен разрешающий чтение данных статус, следует получить состояние регистров CYL_HI:CYL_LO, они будут содержать число фактически готовых данных в буфере устройства. В логе выше видно, что там 0x0800 или 2048 байт. Это стандартный сектор данных для CD. Имейте в виду, это количество байт, а так как мы считываем словами то количество транзакций должно быть вдвое меньше. При этом обменивать байты тут уже не требуется. Результат выполнения команды в CLI:

Считали сектор с описанием структуры ISO

Считали сектор с описанием структуры ISO
Считали сектор с данными директории

Считали сектор с данными директории

На данный момент, команда PACKET в CLI требует ввода всего пакета, поэтому она такая длинная. Однако, сейчас это же только проверка оборудования на работоспособность и оно уже позволяет считывать реальные данные с CD. Позднее будет введена команда автоматического формирования пакета по минимальному количеству параметров.

Команды в CLI можно объединять в пакет и устройство выполнит их последовательно друг за другом:

Добавлены команды I (чтение регистра) и * (пауза в секундах)

Добавлены команды I (чтение регистра) и * (пауза в секундах)

На этом можно завершать подготовительную часть и со следующей части статьи переходить уже к основной задаче. Контроллер получился неплохой, результатом его работы я прям доволен. Обратите внимание — все эпюры в этой статье сняты именно с него. Протестировал на трёх приводах разной степени древности — все работают как задумывалось. Добавлю позже работу с SD картой и соответствующих команд в CLI, но это уже будет «за кадром». Не переключайтесь.


ссылка на оригинал статьи https://habr.com/ru/articles/943558/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *