
В процессе обсуждения первой части пришло понимание, что контроллер IDE всё же следует наделить некоторым «умом». Шина USB заточена под поточную передачу данных и поэтому Ping-Pong протоколы на ней откровенно тормозят, приводя к не оптимальному использованию самой шины. Это выливается и в тормоза на шине IDE, поэтому было принято волевое решение создать вторую версию контроллера, более умного, но в то же время не сильно усложнённого, чтобы он позволял как прямое управление шиной так и мог реализовывать базовые протоколы IDE без необходимости обращения к хосту. Если вам всё ещё интересна данная тема — добро пожаловать под кат.
При выборе контроллера преследовалась цель применить что-то, что уже есть в наличии и обязательно уйти от «ногодрыга», у которого есть свои известные проблемы. Выбор пал на STM32F407VGT в корпусе TSOP100 фирмы ST. У этого контроллера в этом корпусе доступен FSMC контроллер на 16 бит, что нам и нужно. А ещё, у него есть SDIO и можно прикрутить дополнительно uSD карту памяти как быстрый и практически безграничный буфер для данных. Так родился концепт второй версии контроллера IDE для изучения устройств IDE/ATAPI а так же для приближения к нашей цели: создать IDE ATAPI эмулятор CD/DVD привода. Вырисовалась вот такая схема:
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 и прочие процедуры, этого времени хватает приводам проинициализироваться. А у нас сброс может поступить в любой момент, поэтому необходимо выжидать.
Анализ логов, которые были приведены в первой статье, показал, что используется только 2 команды:
-
0xA1 — IDENTIFY_ATAPI_DEVICE
-
0xA0 — PACKET
Остальные команды шины IDE не замечены, даже в режиме DMA. Собственно, так и должно быть, ведь мы разбираем ATAPI привод, а он использует пакетные SCSI команды. Сами пакетные команды описаны в других стандартах, например, SCSI-3 и MMC-3. Буквари на них есть в интернете, к ним мы вернёмся когда дойдём до полноценного управления устройством и, собственно, когда начнём строить эмулятор. На данный момент, речь о шине IDE, которая хостит ATAPI. И начнём мы с команды 0xA1 — IDENTIFY_ATAPI_DEVICE. Для её активации следует заполнить регистры IDE следующим образом:
Мы же помним тот момент, что на одном кабеле IDE может быть 2 устройства, один в режиме MASTER/SINGLE а второй в режиме SLAVE. Вот именно бит DEV в регистре DEVICE/HEAD и указывает, к какому устройству в данный момент идёт обращение. Этот бит слушают оба устройства, но откликается лишь то, у которого настройка джампера MASTER/SLAVE соответствует. DEV=0 соответствует выбору MASTER. PC записывает сюда 0xA0, устанавливая оба бита «obs» (obsolete — устаревшие) в 1. Поступим так же. Эпюры выполнения команды:
Обнаружение устройства немного более сложная задача, чем просто подать команду 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 байт):
Код процедуры, которая обнаруживает подключённое устройство
// Обнаружение привода 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. Устройство сразу же ответило, что ждёт от нас данные пакета:
IDENTIFY_ATAPI_DEVICE показал, что у этого устройства используется пакет размером в 12 байт (6 слов). Структура пакета при этом следующая:
В логе видно, что команда чтения сектора 0x28, номер LBA = 0x00000011 а длина передачи 0x00000001 (1 сектор). После принятия устройством пакета не перекрываемой команды оно сразу же пытается её исполнить. При этом выставляя статусы по мере выполнения команды:
Необходимо следить за состоянием регистра STATUS или настроить приём сигнала INTRQ, последний позволяет устройству не торчать в ожидании а выполнять что-то полезное в процессе ожидания. Когда устройство выполнит запрос оно выставит INTRQ и соответствующие флаги: сначала установится флаг готовности данных DRQ а затем снимется флаг занятости устройства BSY:
В этот момент следует переходить к фазе чтения данных:
Как только получен разрешающий чтение данных статус, следует получить состояние регистров CYL_HI:CYL_LO, они будут содержать число фактически готовых данных в буфере устройства. В логе выше видно, что там 0x0800 или 2048 байт. Это стандартный сектор данных для CD. Имейте в виду, это количество байт, а так как мы считываем словами то количество транзакций должно быть вдвое меньше. При этом обменивать байты тут уже не требуется. Результат выполнения команды в CLI:
На данный момент, команда PACKET в CLI требует ввода всего пакета, поэтому она такая длинная. Однако, сейчас это же только проверка оборудования на работоспособность и оно уже позволяет считывать реальные данные с CD. Позднее будет введена команда автоматического формирования пакета по минимальному количеству параметров.
Команды в CLI можно объединять в пакет и устройство выполнит их последовательно друг за другом:
На этом можно завершать подготовительную часть и со следующей части статьи переходить уже к основной задаче. Контроллер получился неплохой, результатом его работы я прям доволен. Обратите внимание — все эпюры в этой статье сняты именно с него. Протестировал на трёх приводах разной степени древности — все работают как задумывалось. Добавлю позже работу с SD картой и соответствующих команд в CLI, но это уже будет «за кадром». Не переключайтесь.
ссылка на оригинал статьи https://habr.com/ru/articles/943558/
Добавить комментарий