
В процессе обсуждения первой части пришло понимание, что контроллер 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
input CLK, // Такты 56МГц
inout [15:0]FSMC_AD, // Мультиплексированная шина адреса и данных
input FSMC_NL, // Защёлка адреса
input FSMC_NE1, // Выбор устройства
input FSMC_NOE, // Строб чтения
input FSMC_NWE, // Строб записи
output reg FSMC_NWAIT, // Сигнал готовности устройства
output reg STAT_INTRQ, // Проброс статуса: INTRQ
output reg STAT_DMARQ, // Проброс статуса: DMARQ
output reg STAT_DASP, // Проброс статуса: DASP
output reg STAT_PDIAG, // Проброс статуса: PDIAG
// Шина IDE
inout [15:0]DD, // Шина данных IDE
output reg [2:0]DA, // Шина адреса
output CS1X, // Основные регистры
output CS3X, // Дополнительные регистры
output DIOR, // Сигнал чтения
output DIOW, // Сигнал записи
output DMACK, // Сигнал подтверждения DMA
output reg RESET, // Сигнал сброса
inout CSEL, // Сигнал выбора по кабелю
input IORDY, // Сигнал готовности
input INTRQ, // Сигнал запроса прерывания
input DMARQ, // Сигнал запроса DMA
input DASP, //
input PDIAG, //
// Лампочка
output reg 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_t DATA_PIO; // Регистр 0: DATA, режим PIO, 16 бит
__IO uint16_t FEATURE; // Регистр 1: STATUS / FEATURES, 8 бит
__IO uint16_t SEC_COUNT; // Регистр 2: SECTOR COUNT, 8 бит
__IO uint16_t SEC_NUMBER; // Регистр 3: SECTOR NUMBER, 8 бит
__IO uint16_t CYL_LOW; // Регистр 4: CYLINDER LOW, 8 бит
__IO uint16_t CYL_HIGH; // Регистр 5: CYLINDER HIGH, 8 бит
__IO uint16_t CONTROL; // Регистр 6: CONTROL/DEVICE&HEAD, 8 бит
__IO uint16_t COMMAND; // Регистр 7: STATUS / COMMAND, 8 бит
// DMACK
__IO uint16_t DATA_DMA; // Регистр 0: DATA, режим DMA, 16 бит
// Резерв
__IO uint16_t RES0[ 3 ]; // Резерв
// Управление сигналом CSEL
__IO uint16_t CSEL; // Регистр управления сигналом CSEL
// Резерв
__IO uint16_t RES1[ 1 ]; // Резерв
// CS3X
__IO uint16_t ALT_STATUS; // Регистр 6: ALT STATUS, 8 бит
// Управление сигналом RESET
__IO uint16_t RESET; // Регистр управления сигналом 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, но это уже будет "за кадром". Не переключайтесь.
Комментарии (4)
kenomimi
03.09.2025 12:49Ох, прямо стариной повеяло... Похожим был один из моих первых проектов на МК, еще не было ардуины тогда, все паяли сами. Atmel Studio + atmega8535 + ЛУТовая плата + программатор на LPT-порту. Задача - читать и писать диск через RS232 плюс ставить/убирать пароль, и в принципе, оно даже получилось - на скорости 115200 работало на ура.
HardWrMan Автор
03.09.2025 12:49В середине-конце 00х ходили самодельные устройства на PICе как раз для низкоуровневого управления IDE HDD. Вот эту схему я собирал человеку, который занимался ремонтом HDD у нас в городе:
Правда, не прошивал - он сам всё прошивал, я только собирал физическое устройство. Прошивки на подобные устройства у них ходили в закрытых кругах ремонтников HDD.
sintech
Отличное продолжение, но это все еще контроллер а не оконечное устройство поэтому FSMC тут подходит идеально.
Интересно, как планируется реализовать обратное, чтобы устройство выступало приводом и желательно без EOL CPLD.
HardWrMan Автор
Это предмет следующей статьи.