Pull to refresh

Трясем стариной: перехват потока данных между i386 и контроллером ATA средствами STM32

Reading time 13 min
Views 40K
Добрый день, уважаемые хабровчане. В сегодняшней статье мы предадимся ностальгии, поработаем со старым добрым железом, и постараемся прикрутить к нему не менее доброе новое. А заодно вспомним, как работает шина ISA и как вообще происходит общение х86-процессора с периферией.

Введение


Пару дней назад мне попался на глаза мой самый первый компьютер – старичок 386й, на процессоре от АМД (Am386-DX), с 4 мегабайтами RAM, VGA-видеокартой и мульти-платой, берущей на себя функции контроллера дисковода, харда, параллельного и последовательного портов.
Конечно, он давно уже был избавлен от своего корпуса и от древнего, почившего харда – теперь он представлял собой просто материнскую плату с парой плат расширения. Несколько лет назад я подключал к нему более новый хард, на 10 ГБ (изначально в нем стоял диск всего на 200 мегабайт), на который я поставил FreeDOS.



Однако на этот раз он грузиться дальше БИОСа отказался – судя по звукам, 10 ГБ хард за несколько лет лежания в шкафу успел отправиться вслед за двухсотметровым.
И тогда у меня проснулось острое желание что-нибудь с этим компьютером сделать, прикоснуться к этой древности, с которой я начинал знакомство с IT, уже как разработчик, а не как пользователь. В идеале хотелось бы, конечно, сделать эмулятор жесткого диска, работающий с SD-карточкой, но к этой цели будем идти постепенно. Начнем с более простой задачи – соберем устройство, висящее параллельно с настоящим контроллером жесткого диска, и логирующее обмен данными, чтобы узнать, как именно старый БИОС детектит жесткие диски. Изначально я предполагал сделать то же самое, но для контроллера дискет, однако, после того как мой последний хард помер, все что у меня осталось работающего – это БИОС, который никак не проверяет наличие флоппи. Зато у него есть пара пунктов относящихся к хардам – детект жестких дисков и средства для их форматирования.
Разумеется, на ПЛИС сие делается очень легко в силу их архитектуры, но будем придерживаться бюджетного варианта и попробуем сделать это на контроллере STM32F103 и нескольких микросхемах дискретной логики. И так, начнем.

Железо


Традиционно пойдем от хардварных низов. Давайте вспомним, что же такое шина ISA, лежащая в основе старых компьютеров, и как к ней можно подключиться. Для тех, кто не очень представляет схемотехнику внутри х86 машин, это поможет пролить свет на архитектуру подобных систем. На самом деле все очень просто – в «чистой» ISA нету никаких средств Plug&Play – они появились только в следующем стандарте – а, следовательно – никаких средств выдачи адреса устройствам.
Таким образом, ISA-карты представляют собой устройства с аппаратно заданным адресом (жестко определенным схемотехникой, в лучшем случае – с возможностью выбрать базовый адрес джамперами). Сама шина содержит 20 линий адреса, 16 линий данных, несколько сигналов питания, несколько линий IRQ, и набор управляющих сигналов.

image

Как все это работает? Предположим, нам нужна возможность зажигать/гасить несколько светодиодов на нашем устройстве. Для этого мы разместим на нашей ISA-плате микросхему регистра, например, такую, как 74HC273.
Это самая обычная 8-битная «защелка», запоминающая то, что ей подали на вход по сигналу. Выходы регистра подключим к светодиодам и забудем о них. С программной точки зрения взаимодействие с устройством на ISA шине может быть реализовано двумя методами.

  1. При помощи мапирования на память – тогда мы будем декодировать сигналы чтения/записи памяти и выдавать результаты на шину вместо контроллера DRAM – так поступает видеокарта, ее видеопамять мапируется на адресное пространство памяти компьютера. Таким образом, запись в память видеокарты для компьютера ничем не отличается от записи в свою оперативную память и выполняется обычной командой MOV.
  2. Для тех устройств, где не требуется передавать большие блоки данных, используется так называемое «пространство ввода-вывода» — отдельное адресное пространство, выделенное для периферийных устройств и ограниченное 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, значение которой любезно подсказал mark_ablov и которая является устаревшей командой рекалибровки, заставляющей диск переставить свои магнитные головки на сектор 0. После чего БИОС проверяет статусный байт, ожидая завершения выполнения команды (можно заметить, что хард даже не входит в состояние BUSY, сразу отвечая, что выполнил).
И наконец – команду 0xEC, DRIVE IDENTIFY, в ответ на которую хард отдает 256 16-битных слов информации о диске.
Прежде чем начать их считывать из порта 0x1F0, БИОС запрашивает байт статуса из регистра 0x1F7, ожидая готовности диска.
Вот тут, к сожалению, я понял свой промах – я решил, что данные выдаются по 8 бит, так как управляющие регистры 8-битные. Однако, как оказалось, данные выдаются по 16 бит, поэтому я получил только 256 младших байт. Для получения полной информации придется немного переделать схему, отдав весь GPIOD под данные, а адрес и режим доступа выводить на другие пины, что, конечно, увеличит задержку в их обработке.
Поэтому на данный момент я приостановился, хотя, возможно, в ближайшее время я и продолжу работу и попробую сесть на шину уже не как монитор, а как устройство. У ISA шины есть замечательный сигнал IOCHRDY, выставляя неактивный уровень на котором, устройство сигнализирует о необходимости увеличить длительность IO-цикла, а значит, возможно, у меня хватит времени переключить пины на вывод и выдать свое состояние.

На этом пока все, спасибо за внимание.

Upd:
Только что обнаружил интересную вещь. Если в настройках проекта выставить оптимизацию O1 вместо O3, то время входа в интеррапт становится точно таким, каким и должно быть. После некоторых исследований, я выяснил, что при включенной оптимизации О2 либо О3 компилятор переставляет инструкции, из-за чего сигнальный пин устанавливается в 1 не сразу же после входа в интеррапт, а уже после выполнения части кода.
Таким образом, при оптимизации О1 на осциллографе видно, что система вошла в обработчик интеррапта раньше, но время выполнения этого обработчика больше, чем в случае с О3.
Tags:
Hubs:
+58
Comments 30
Comments Comments 30

Articles