Введение
Пару дней назад мне попался на глаза мой самый первый компьютер – старичок 386й, на процессоре от АМД, с 4 мегабайтами RAM, VGA-видеокартой и мульти-платой, берущей на себя функции контроллера дисковода, харда, параллельного и последовательного портов.
Конечно, он давно уже был избавлен от своего корпуса и от древнего, почившего харда – теперь он представлял собой просто материнскую плату с парой плат расширения. Несколько лет назад я подключал к нему более новый хард, на 10 ГБ (изначально в нем стоял диск всего на 200 мегабайт), на который я поставил FreeDOS.
Однако на этот раз он грузиться дальше БИОСа отказался – судя по звукам, 10 ГБ хард за несколько лет лежания в шкафу успел отправиться вслед за двухсотметровым.
И тогда у меня проснулось острое желание что-нибудь с этим компьютером сделать, прикоснуться к этой древности, с которой я начинал знакомство с IT, уже как разработчик, а не как пользователь. В идеале хотелось бы, конечно, сделать эмулятор жесткого диска, работающий с SD-карточкой, но к этой цели будем идти постепенно. Начнем с более простой задачи – соберем устройство, висящее параллельно с настоящим контроллером жесткого диска, и логирующее обмен данными, чтобы узнать, как именно старый БИОС детектит жесткие диски. Изначально я предполагал сделать то же самое, но для контроллера дискет, однако, после того как мой последний хард помер, все что у меня осталось работающего – это БИОС, который никак не проверяет наличие флоппи. Зато у него есть пара пунктов относящихся к хардам – детект жестких дисков и средства для их форматирования.
Разумеется, на ПЛИС сие делается очень легко в силу их архитектуры, но будем придерживаться бюджетного варианта и попробуем сделать это на контроллере STM32F103 и нескольких микросхемах дискретной логики. И так, начнем.
Железо
Традиционно пойдем от хардварных низов. Давайте вспомним, что же такое шина ISA, лежащая в основе старых компьютеров, и как к ней можно подключиться. Для тех, кто не очень представляет схемотехнику внутри х86 машин, это поможет пролить свет на архитектуру подобных систем. На самом деле все очень просто – в «чистой» ISA нету никаких средств Plug&Play – они появились только в следующем стандарте – а, следовательно – никаких средств выдачи адреса устройствам.
Таким образом, ISA-карты представляют собой устройства с аппаратно заданным адресом (жестко определенным схемотехникой, в лучшем случае – с возможностью выбрать базовый адрес джамперами). Сама шина содержит 20 линий адреса, 16 линий данных, несколько сигналов питания, несколько линий IRQ, и набор управляющих сигналов.
Как все это работает? Предположим, нам нужна возможность зажигать/гасить несколько светодиодов на нашем устройстве. Для этого мы разместим на нашей ISA-плате микросхему регистра, например, такую, как 74HC273.
Это самая обычная 8-битная «защелка», запоминающая то, что ей подали на вход по сигналу. Выходы регистра подключим к светодиодам и забудем о них. С программной точки зрения взаимодействие с устройством на ISA шине может быть реализовано двумя методами.
- При помощи мапирования на память – тогда мы будем декодировать сигналы чтения/записи памяти и выдавать результаты на шину вместо контроллера DRAM – так поступает видеокарта, ее видеопамять мапируется на адресное пространство памяти компьютера. Таким образом, запись в память видеокарты для компьютера ничем не отличается от записи в свою оперативную память и выполняется обычной командой MOV.
- Для тех устройств, где не требуется передавать большие блоки данных, используется так называемое «пространство ввода-вывода» — отдельное адресное пространство, выделенное для периферийных устройств и ограниченное 16 битами адреса. Обращение к нему выполняется командами IN и OUT (чтение и запись в порты ввода-вывода)
На деле, за словами «отдельное адресное пространство» кроется физически простая сущность: в шине ISA присутствуют 4 сигнала – MEMW, MEMR, IOR, IOW. При исполнении команды чтения/записи в память (MOV) либо чтения/записи в IO (IN, OUT), искомый адрес выставляется на одной и той же шине, линиях A0-A19 ISA. Данные также идут по одним и тем же линиям – D0-D15. Разница состоит лишь в том, что при чтении из памяти выставляется активный уровень на линии MEMR, при записи в память – MEMW, при чтении из порта IO – IOR, при записи в него – IOW.
Таким образом, чтобы сделать простейшее устройство с одним регистром и светодиодами, нам нужно определить, когда на шине выставлен нужный нам адрес (мы ведь помним, адреса нам никто не выдает, мы должны сами выбрать адрес, который не будет конфликтовать с имеющейся периферией), и по сигналу IOW разрешить запись данных с линий D0-D8 в наш регистр.
В более сложных устройствах, содержащих несколько регистров, старшие линии адреса идут на декодер, формируя активный выходной сигнал при совпадении с некоторым «базовым» адресом устройства, младшие же формируют номер регистра, к которому следует обратиться.
Перейдем к более конкретному примеру – нашему контроллеру ATA. Для большего понимания принципов его работы рекомендую ознакомиться со статьей из OSDev wiki.
Управляется он девятью IO регистрами, восемь из которых расположены подряд, начиная с базового адреса 0x1F0. Девятый, к сожалению, расположен по адресу 0x3F6, что несколько усложняет схему декодирования.
Разумеется, не будем заводить на контроллер все линии адреса и делать декодер на нем, иначе мы ничего не успеем – частота тактового сигнала шины 8 МГц, цикл IO, согласно спецификации, длится 4 такта, что при частоте 72 МГц контроллера дает нам всего 36 тактов на раздумья. Поэтому воспользуемся дешевыми микросхемами дискретной логики.
Если бы не было этого девятого регистра, который торчит в 0x3F6, то нам нужно было бы построить схему, которая выдает активный сигнал, когда на линиях A9 и A3 установлен ноль, а на A4-A8 – единица ( то есть, для адресов 0x1F(..) ). Биты старше A9 в ISA картах обычно не декодят, не обращая внимания на возможность доступа к одному и тому же устройству по адресам, расположенным выше.
Обработку трех младших бит уже можно было бы поручить контроллеру. Увы, у нас остается неохваченный регистр 0х3F6.
Исходные условия (активны линии А4-А8 и неактивна А3) выполняются всегда, так как эти биты находятся в указанных состояниях и для 0x1F(..) и для 0x3F6. К ним добавляется условие, которое можно сформулировать так: при активном А9 – должны быть активные уровни на А1 и А2 (адрес 0x3F6)
То есть,
CS0 = A8 & A7 & A6 & A5 & A4 & ~A3 CS1 = A1 & A2 & A9 CS2 = CS0 & (~A9 | CS1)
Воспользовавшись онлайн симулятором логических схем Logic.Ly, я построил эту схему базируясь на микросхемах, которые были у меня в наличии – 74HC04, четверной элемент NOT, 74HC30 — восьмивходовый NAND и 74HC10, тройной трехвходовый NAND.
Так как элемента OR у нас нет, вспоминаем правила Де Моргана – отрицание конъюнкции есть дизъюнкция отрицаний и отрицание дизъюнкции есть конъюнкция отрицаний, или, в виде логических равенств
~(A&B) = ~A | ~B ~(A|B) = ~A & ~B
Этим и воспользуемся:
~( ~ (~A9 | CS1))) = ~(A9&~CS1) - = (A9 NAND ~CS1) CS2 = CS0 & (A9 NAND ~CS1)
Чистого AND у нас тоже нет, поэтому подадим его составляющие на трехвходовый блок NAND и будем входить в прерывание по спаду.
Как видно, вся логика вмещается ровно в три корпуса.
К этим условиям добавляется наличие активного уровня на IOR или IOW (не забываем, что на них, согласно стандарту, активный уровень низкий, то есть нам приходят уже инвертированные сигналы, ~IOR и ~IOW):
CS = CS2 & (IOR | IOW) (IOR|IOW) = ~(~(IOR & IOW) ) = ~(~IOR & ~ IOW) = (IOR NAND IOW) CS = CS2 & (IOR NAND IOW)
Итоговая схема выглядит так:
Теперь начинаем собирать ее в железе, пользуясь макеткой. Сначала расставим три первые микросхемы, к которым идет больше всего входных сигналов, и озаботимся их соединением с шинами питания и земли.
Внимательно добавим соединим выходные цепи, после чего добавим входные в виде достаточно длинных щупов, которые в последствие воткнем в материнскую плату компьютера:
Для удобства я временно закрепил старшие адресные входы слева (А3-А9), младшие справа (А0-А2), а посередине вывел сигнал CS2.
Давайте временно отвлечемся от сборки и попробуем посмотреть осциллографом, что же у нас вышло. Итак, соединяем адресные входы с шиной – так как ISA это именно шина, нам не нужно пытаться воткнуться щупами в тот же разъем, куда вставлена плата контроллера ATA, выбираем любой нам удобный. Щупы, к сожалению, оказались маловаты для таких отверстий, поэтому сверху я воткнул еще и гребенку прямых пинов – по отдельности и щупы и пины выпадают, а вот вместе держатся довольно неплохо.
Также не забываем подключить землю и питание схемы к ISA, а заодно – землю щупов осциллографа.
Включаем осциллограф и компьютер(я сразу зашел в меню настройки БИОСа) и тыкаемся в сигнал CLK. Мы должны увидеть что-то типа этого:
Это, понятное дело, тактовый сигнал шины, частота которого обычно равна 8 МГц. На моей материнской плате его частота равна 7.19 МГц, что отражено в настройках БИОСа. Видимо, это особенность железа – БИОС не позволил мне понизить эту частоту, или хотя бы выставить ее ровно в 8 МГц, упорно выставляя 7.19 МГц. Ну да ладно.
Проверяем контакты входов нашей схемы – тыкнувшись в любой из них мы получим хаотичный сигнал на экране осциллографа, так как система постоянно обращается к разным адресам и портам. Так что если на входе тишина, это значит, что у вас отошел контакт, и его необходимо перепроверить.
Теперь подключаемся к нашему сигналу CS2 и наблюдаем следующую картину:
Вполне ожидаемо – сигналы IOR и IOW не участвуют в формировании CS2, так что он становится активным при совпадении адреса на шине с заданным нами (0x1F0-0x1F7 и 0x3F6). Система проводит регулярную регенерацию DRAM, поэтому мы получаем красивый периодический сигнал. Сейчас самое время поднастроить развертку и уровни осциллографа, чтобы видеть сигналы во всей красе.
Убедившись, что все работает, обесточиваем схему и дособираем ее доконца, получив адскую мешанину проводов типа этого:
Снова включаем компьютер, заходим в меню настройки БИОСа, включаем осциллограф.
Никаких сигналов! Что ж, пришла пора проверить правильность наших выкладок – выбираем пункт “Autodetect hard drive”. Первый диск определяется быстро, и, скорее всего, на экране осциллографа мы ничего не успеем заметить, если только не включили одиночный режим.
А вот второй диск (по причине его отсутствия) будет детектиться достаточночно долго, чтобы мы увидеть на экране компьютера это:
А на экране осциллографа – вот это:
Чтобы совсем убедиться в нашей правоте, выходим из детекта дисков, включаем одиночный режим осциллографа и внимательно смотрим на его экран – ничего! Сколько бы мы не ждали, CS не становится активным! Но стоит только зайти в детектирование дисков, как мы снова ловим знакомую картинку, которая вполне соответствует стандартам — цикли I/O длится четыре такта шины.
Что ж, самое время взять плату с STM32 и подключить ее к системе!
Я подключил следующим образом:
Шина данных ISA (D0 – D7) подключены к GPIOD.0 – GPIOD.7,
Три младших линии адреса (A0 – A2) – к GPIOD.8 – GPIOD.10,
Линию адреса A9 – к GPIOD.11 (нам ведь нужен будет этот бит, чтобы понять, что обращение идет не к 0x1F6, а к 0x3F6!)
Линии IOW и IOR к GPIOD.12 и GPIOD.13.
Сигнал CS – к GPIOB.0
Теперь при прерывании на GPIOB.0 нам необходимо будет просто прочесть GPIOD->IDR (Input Data Register), в котором младшие 8 бит будут искомыми данными, следующие четыре – адресом (причем, возможными комбинациями будут 0000 – 0111 и 1011, соответствующие портам 0x1F0 – 0x1F7 и 0x3F6), следующие два – режимом (чтения при 01 либо записи при 10).
Тут важно отметить следующее – если вдруг мы получим результат с битами режима, находящимися в невалидном состоянии – 00 либо 11, это будет сигнализировать нам об ошибке работы – этот факт нам очень скоро пригодится.
Итак, переходим к софту.
Софт
С софтом все предельно просто – мы настраиваем GPIOD на вход, как и GPIOB.0, после чего настраиваем прерывание по спаду на линии EXTI, соединенной с GPIOB.0.
В обработчике прерывания мы будем только читать значение из GPIOD и инкрементировать указатель на буфер. Этот буфер потом можно будет послать на компьютер для анализа по любому интерфейсу, либо вовсе не заморачиваться с этим и посмотреть его прямо в дебаге.
Код настройки представлен ниже:
GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD|RCC_APB2Periph_GPIOB|RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3| GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7 |GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10|GPIO_Pin_11| GPIO_Pin_12|GPIO_Pin_13; GPIO_Init(GPIOD, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0); EXTI_InitStructure.EXTI_Line = EXTI_Line0; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); NVIC_InitTypeDef NVIC_InitStructure; NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x0); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0); NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);
А вот — код обработчика прерывания:
uint16_t Log[1024]; uint16_t ptr=0; void EXTI0_IRQHandler() { Log[ptr]=GPIOD->IDR; ptr++; EXTI_ClearITPendingBit(EXTI_Line0); }
Тесты, отладка и допиливание
Пришло время проверить, что у нас получилось! Запускаем компьютер, заходим в настройку БИОСа. Запускаем дебаг STMки. Заходим в детектирование дисков, и, после детекта диска C, приостанавливаем выполнение программы контроллера. В окне дебага мы видим, что какие-то данные наловились, и их немало!
Дальше я поступил следующим образом – из окна дебага скопировал содержимое буфера в MS Exel, чтобы разбить на колонки и избавиться от первой, содержащей имя переменной, после чего скопировал столбец со значениями в новый текстовый файл, и получил что-то вроде этого:
58453 54527 42069 38143 42069 38143 ...
Теперь пришло время написать программу для обработки результатов на любом удобном языке, я использовал для этого C#. Нам нужно каждый входной uint разбить на данные, адрес и режим доступа, сформировав удобочитаемый отчет. Это делается очень просто, обычными битовыми сдвигами и побитовыми операциями, например, вот так:
var busData = uint.Parse(entry); uint data = (busData & 0xFF); uint address = ((busData & 0xFF00) >> 8); uint rw = (address & 0x30)>>4; address = (address & 0x0F);
Однако, запустим программу, я столкнулся с большой проблемой – очень многие записи из файла содержали режим доступа 11, что означало отсутствие сигналов чтения/записи. Так как вход в интеррапт был возможен только при наличии одного из этих сигналов, я сделал вывод, что интеррапт длится дольше, чем идет цикл шины, и я просто не успеваю считать валидные данные.
Для проверки этой гипотезы я решил выставлять пин GPIOB.2 в 1 при входе в интеррапт, и сбрасывать его в 0 при выходе, после чего повесил на него щуп осциллографа.
Результат был удручающий:
Как видно, система входит в интеррапт уже у самого конца цикла I/O, несмотря на обещанные 12 тактов на вход. Не помог даже атрибут (naked), разница была совершенно незначительной.
Это меня огорчило, но я решил попробовать разогнать контроллер – те же AVRки очень хорошо поддавались разгону, почему бы не проверить, как с этим обстоит дело у STM32. Для этого нам необходимо пойти в файл system_stm32f10x.c, в процедуру инициализации системного тактового сигнала SetSysClockTo72, и найти строку
RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9);
В этой строке следует поменять константу RCC_CFGR_PLLMULL9 на что-нибудь побольше. Скажу сразу – я попробовал все варианты и в итоге остановился на максимальном значении, RCC_CFGR_PLLMULL16. Таким образом, контроллер совершенно спокойно завелся на частоте 128 МГц вместо 72, даже не греясь.
Кстати, совсем хорошо было бы привязать тактовый сигнал контроллера к CLK шины ISA, чтобы работать с ней синхронно, но мне очень не хотелось отпаивать кварц на отладочной плате, поэтому я не стал этого делать.
Посмотрим, что выдает осциллограф теперь:
Наконец-то мы стали входить в интеррапт достаточно рано для того чтобы успеть его обработать! Давайте снова наберем входных данных и попробуем их проанализировать.
Я несколько дополнил программу, чтобы отчет выдавался форматированным и сразу с именами регистров, к которым идет обращение. В случае с неправильным режимом доступа, в отчет добавляется строка о невалидности данных.
Вот результат работы программы:
WRITE: Cylinder Low [0x1F4] VALUE: 0x55 READ: Cylinder Low [0x1F4] VALUE: 0x55 WRITE: Cylinder Low [0x1F4] VALUE: 0xAA READ: Cylinder Low [0x1F4] VALUE: 0xAA WRITE: Cylinder Low [0x1F4] VALUE: 0x0F READ: Cylinder Low [0x1F4] VALUE: 0x0F WRITE: Cylinder Low [0x1F4] VALUE: 0x00 READ: Cylinder Low [0x1F4] VALUE: 0x00 READ: Status [0x1F7] VALUE: 0x50 WRITE: Drive/Head [0x1F6] VALUE: 0xA0 READ: Status [0x1F7] VALUE: 0x50 WRITE: Drive/Head [0x1F6] VALUE: 0x04 WRITE: Drive/Head [0x1F6] VALUE: 0x00 READ: Status [0x1F7] VALUE: 0x50 WRITE: Drive/Head [0x1F6] VALUE: 0xA0 READ: Status [0x1F7] VALUE: 0x50 WRITE: Drive/Head [0x1F6] VALUE: 0xA0 READ: Status [0x1F7] VALUE: 0x50 WRITE: Command [0x1F7] VALUE: 0x10 READ: Status [0x1F7] VALUE: 0x50 READ: Status [0x1F7] VALUE: 0x50 WRITE: Drive/Head [0x1F6] VALUE: 0xA0 READ: Status [0x1F7] VALUE: 0x50 WRITE: Drive/Head [0x1F6] VALUE: 0xA0 READ: Status [0x1F7] VALUE: 0x50 WRITE: Command [0x1F7] VALUE: 0xEC READ: Status [0x1F7] VALUE: 0x58 READ: Data [0x1F0] VALUE: 0x5A READ: Data [0x1F0] VALUE: 0xFF READ: Data [0x1F0] VALUE: 0x00 READ: Data [0x1F0] VALUE: 0x10 READ: Data [0x1F0] VALUE: 0x00 READ: Data [0x1F0] VALUE: 0x00 READ: Data [0x1F0] VALUE: 0x3F READ: Data [0x1F0] VALUE: 0x00 READ: Data [0x1F0] VALUE: 0x00 READ: Data [0x1F0] VALUE: 0x00 READ: Data [0x1F0] VALUE: 0x45 ...
Как мы видим, невалидных данных больше нет.
Попробуем разобраться, как БИОС проводит детектирование.
В начале он настойчиво пишет-читает в регистры, задающие адрес – удостоверяясь, что вычитывается то же значение, которое было записано. Если контроллер ATA отсутствует в системе, то БИОС так и будет долго-долго пытаться записать-прочитать этот самый регистр, 0x1F4 – вот пример отчета при вытащенной плате контроллера:
WRITE: Cylinder Low [0x1F4] VALUE: 0x55 READ: Cylinder Low [0x1F4] VALUE: 0xFF WRITE: Cylinder Low [0x1F4] VALUE: 0x55 READ: Cylinder Low [0x1F4] VALUE: 0xFF ... WRITE: Cylinder Low [0x1F4] VALUE: 0x55 READ: Cylinder Low [0x1F4] VALUE: 0xFF WRITE: Cylinder Low [0x1F4] VALUE: 0x55
Потом подает команду 0x10, значение которой я так и не нашел, и на которую хард, похоже, никак не реагирует, и наконец – команду 0xEC, DRIVE IDENTIFY, в ответ на которую хард отдает 256 16-битных слов информации о диске.
Прежде чем начать их считывать из порта 0x1F0, БИОС запрашивает байт статуса из регистра 0x1F7, ожидая готовности диска.
Вот тут, к сожалению, я понял свой промах – я решил, что данные выдаются по 8 бит, так как управляющие регистры 8-битные. Однако, как оказалось, данные выдаются по 16 бит, поэтому я получил только 256 младших байт. Для получения полной информации придется немного переделать схему, отдав весь GPIOD под данные, а адрес и режим доступа выводить на другие пины, что, конечно, увеличит задержку в их обработке.
Поэтому на данный момент я приостановился, хотя, возможно, в ближайшее время я и продолжу работу и попробую сесть на шину уже не как монитор, а как устройство. У ISA шины есть замечательный сигнал IOCHRDY, выставляя неактивный уровень на котором, устройство сигнализирует о необходимости увеличить длительность IO-цикла, а значит, возможно, у меня хватит времени переключить пины на вывод и выдать свое состояние.
На этом пока все, спасибо за внимание.
ссылка на оригинал статьи http://habrahabr.ru/post/161617/
Добавить комментарий