ATA для дZенствующих. Часть 2: PCI DMA — Архив WASM.RU

Все статьи

ATA для дZенствующих. Часть 2: PCI DMA — Архив WASM.RU

Глендауэр: Я духов вызывать могу из бездны!

Хотспер: И я могу, и каждый это может, вопрос лишь, явятся  ль они на зов?

В. Шекспир, Генрих IV

Приветствую, уважаемый читатель!

BUGFIX к первой части:

1) В первой части шла речь о том, что в случае отсутствия устройства, можно ждать до бесконечности пока BSY будет 0. Это неверно. Если на канале есть хотя бы одно устройство, BSY будет 0, когда оно освободится. Речь шла о DRDY, после выбора устройства через бит DEV, мы не дождемся пока DRDY будет 1, в случае если устройства нет.

2) Команде READSECTOR (код 20h) обязательно нужно указывать количество секторов для обмена (устанавливать 1F2h). Меня ввела в заблуждение команда READMULTIPLE, эти две команды отличаются не тем, что одна считывает один сектор, а вторая - несколько, а тем, что в первом случае прерывание возникает после каждого прочитанного сектора, а во втором - только после чтения всей группы.

После долгого перерыва, связанного с отсутствием у меня компа, возвращаемся к теме программирования современных винчестеров на полностью аппаратном уровне.

В данной части мы рассмотрим использование режима DMA.

Следует сразу приготовиться к тому, что мануал будет большого размера, потому как описать DMA, так чтобы это было понятно, в 2 страницы не получится. Кто не прочитал первую часть, советую сделать это сейчас, потому как здесь предполагается, что формат регистров 1F0-1F7 и функции битов BSY, DRDY, DRQ и других вы уже знаете.

Зачем нужен DMA?

Для начала поговорим о том, что такое DMA и «зачем вообще все это надо». DMA - DirectMemoryAccess, прямой доступ к памяти. «Непрямым» доступом к памяти называется обмен под управлением процессора, мы его рассмотрели в первой части, суть его, как вы поняли, в том, что процессор забирает у устройства данные, потом помещает их в память, и так в цикле до того пока устройство не закончит обмен. Чтобы освободить процессор от перекачек данных в память и обратно (процессору и так есть чем заняться кроме этого) и был придуман DMA. Важно понимать, что в системе вроде MS-DOS, где процессор занят только одной задачей, PIO может оказаться быстрее DMA, последний режим дает выигрыш только в многозадачных системах.

Кажется, с тем «зачем это нужно» разобрались. Теперь, следуя пожеланиям читателей первой части, рассмотрим вопрос «как это работает».

Аппаратная реализация DMA.

Для начала, немного истории. Как известно, когда в компьютер пришла шина ISA, на системной плате появился контроллер прямого доступа к памяти i8237, который был представлен в двух экземплярах (каскад), аналогично контроллеру прерываний. В слоты ISA были введены контакты DRQi#  (DMARequest) и DACKi# (DMAAcknowledge) которые и использовались для прямого доступа к памяти под управлением контроллера. Любопытно, что возможность управления шиной (Busmastering) была заложена в ISA изначально, ISA-карты могли сами адресоваться к памяти, но эта модель не получила широкого распространения, все предпочли использовать контроллер DMA, так как это позволяло упростить (и удешевить) сами устройства.

Когда в компьютеры пришел интерфейс ATA, оказалось что 8-разрядный контроллер DMA шины ISA не может обеспечить приемлемой скорости передачи, кроме того, у него была еще куча проблем с адресацией памяти. Уже в IBMPCAT разработчики отказались от его использования - ввод-вывод данных с диска должен был осуществляться только в режиме PIO под управлением процессора (в настоящее время контроллер i8237 (точнее, не сам контроллер, а его эмулятор, встроенный в чипсет) используется только дисководом).

Когда была изобретена шина PCI, а контроллер IDE был к ней успешно подключен, появилась возможность реализовать режим DMA, используя ресурсы этой шины. Механизм PCIDMA в корне отличается от ISADMA -  отсутствует какой бы то ни было контроллер DMA, для того чтобы осуществлять обмен, устройство должно уметь захватывать шину (функция Busmaster) и самостоятельно организовывать обмен с памятью.

Когда винчестеру нужно передать слово данных в режиме DMA, он устанавливает запрос DMARQ# (один из проводов IDE-шлейфа), после приема данных хост отвечает сигналом DMACK#, устройство снимает DMARQ#, если у него есть еще слова для передачи, снова устанавливается DMARQ# и все повторяется сначала, пока не будет передан весь сектор. Такой режим называется SinglewordDMA. Есть еще и MultiwordDMA, это когда устройство устанавливает DMARQ# на время всей передачи, а хост в определенном темпе передает или принимает данные, сопровождая каждое слово сигналом DMACK#, если устройство не справляется с потоком, оно может снять DMARQ#, подготовить следующую порцию данных, после чего снова установить запрос. Темп, в котором происходит обмен, задается режимом MultiwordDMA (например, MultiwordDMAMode 1).

В режиме UltraDMA часть проводов шлейфа меняет свое назначение на время передачи пакета. Стробирование информации осуществляется по обоим фронтам тактового сигнала. Выделяются также специальные сигналы управления потоком (как со стороны хоста, так и устройства). Выбор конкретного режима MultiwordDMA или UltraDMA выполняется BIOS на этапе начальной загрузки системы, на основании информации полученной по команде IDENTIFYDEVICE. Теоретически, программист может вмешаться в настройки используя команду SETFEATURES, но делать этого категорически не рекомендуется: принудительное понижение скорости выглядит нелепо уже само по себе, а установка параметров неподдерживаемых винчестером может привести к потере целостности данных (когда устройство не будет справляться с потоком данных от хоста при записи). Таким образом, программисту нужно только сделать выбор: PIO или DMA, обо всем остальном должен заботиться BIOS в процессе загрузки.

Когда PCIIDE контроллер получил от диска данные (вышеописанным способом SinglewordDMA, MultiwordDMA или UltraDMA) он захватывает шину и передает данные в память самостоятельно, подробнее этот процесс рассмотрен ниже.

Теперь займемся изучением структуры типичного компьютера, это очень поможет в понимании принципа DMA. Авторы книг очень любят демонстрировать работу DMA используя следующую абстракцию: между процессором и памятью протянута общая «системная» шина, а к ней подключаются все контроллеры и устройства. Эта концепция была актуальна для процессоров 80386 и 80486 которые имели сигналы HOLD и HDLA с помощью которых устройства «просили» процессор не занимать шину пока идет DMA-обмен, сегодня все материнские платы имеют хабовую архитектуру, и вышеупомянутых контактов у современных процессоров вы не найдете. Поэтому мы здесь будем рассматривать все так как есть на самом деле. Структуру типичного компьютера с одним процессором можно увидеть на рисунке.

Центральное место в архитектуре занимает Северный мост чипсета, связывающий процессор, память, AGP и PCI. Теперь видно, почему так важно, чтобы у его разработчиков были как можно более прямые руки.  В литературе он может называться по-разному: традиционно - NorthBridge, в многопроцессорных системах процессоры объединены шиной Hostbus и мост может называться Host-to-PCIBridge, фирма AMD любит называть его SystemController, но как бы он ни назывался, суть дела от этого не меняется. В состав Южного моста (он же PCI-to-ISABridge, он же PeripheralController) входят контроллеры различных периферийных устройств, контроллер прерываний и «эмулятор ISA», кроме того, именно в него интегрируется PCIIDE контроллер.

Небольшое отступление: структуру изображенную на рисунке не следует воспринимать буквально, это тоже абстракция. Чипсет может состоять из одной-единственной микросхемы (это дело любит SIS), кроме того, шина PCI связывающая оба моста уже не всегда справляется со своими обязанностями и начиная с интеловской линейки i8xx шина PCI «начинается» с южного моста, а между собой мосты связаны высокопроизводительной шиной HubInterfaceA со скоростью обмена 266 Мб/c. Примеру Intel последовала и VIA со своей шиной V-link связывающей мосты. А еще,  всегда существует интерфейс между процессором и южным мостом, на рисунке он не показан. Так что возможны очень большие расхождения между этим рисунком и реальной структурой компьютера.

PCIDMA обмен начинается по инициативе IDE контроллера, который захватывает шину, получив на это право от арбитра, и адресуется к памяти напрямую, используя механизмы адресации памяти устройствами PCI. Данные передаются Южным мостом (в состав которого входит IDE контроллер) Северному, последний передает данные в память. Memorybus, в данном случае, обслуживает процессор и IDE контроллер поочередно, всем этим процессом рулит Северный мост, в задачи которого входит управление памятью. Именно он должен обеспечить эффективное «разделение» памяти процессором и BusMaster-контроллерами.

Процессору нужно лишь проинициализировать PCIIDE контроллер необходимыми параметрами, после чего он может переключиться на другую задачу. Когда обмен будет закончен, устройство установит запрос прерывания. На время всей передачи контроллер удерживает BSY=1 и DRQ=1. Обнуление этих битов говорит о том, что все данные с диска считаны и переданы контроллеру (и в ближайшее время будут в памяти, как только закончится DMA-обмен).

Что собой представляет DMA аппаратном уровне, я думаю, более-менее разобрались, теперь посмотрим, что он собой представляет в программном.

Программная модель PCIIDE.

Контроллер PCIIDE выглядит пристройкой к стандартному IDE контроллеру. Эта пристройка выражается в новом блоке из 8 регистров для каждого канала. Из-за того, что в AT не использовался контроллер DMA шины ISA, риска потерять совместимость со старым ПО не было. Это и развязало руки разработчикам.

Как и все PCI устройства, контроллер обладает возможностью произвольно перемещать свои регистры в пределах пространства ввода-вывода. Базовый адрес задается в заголовке конфигурационного пространства по следующим смещениям 32-битных слов:

10h - блок командных регистров первого канала IDE.

14h - блок управляющих регистров первого канала.

18h - блок командных регистров второго канала IDE.

1Ch - блок управляющих регистров второго канала.

Прежде чем что-то делать, нужно уточнить адреса этих регистров (это надо делать обязательно, так как какого-либо адреса использующегося «чаще всего» не существует).

На практике достаточно чтения одного базового адреса первого канала, для второго канала базовый адрес это базовый адрес первого канала+8. Байты со смещениями 1,3 зарезервированы для обоих каналов.

Смещение

Регистр

0

Командный регистр

1

Зарезервирован

2

Регистр состояния

3

Зарезервирован

4

Указатель на PRDT (регистр 32 битный)

Контроллер может работать в двух режимах: режиме совместимости (в этом случае регистры PCIIDE настроены на знакомые области 1F0h-1F7h и 170h-177h) и в «родном» (native) режиме. В «родном» режиме регистры могут иметь произвольные адреса. Важно твердо усвоить - перемещать  можно только регистры PCIIDE  контроллера, регистры 1F0h-1F7h и 170h-177h остаются на своих местах по любому, на их функции и расположение ничего не влияет.

Формат командного регистра следующий (смещение 0 от базового адреса):

0

0

0

0

D

0

0

E/D

Бит 0 (Enable/Disable) управляет работой BusMaster: 0 - запретить, 1 - разрешить.

Бит 3 (Direction) управляет направлением обмена (0 - из памяти, 1 - в память). Значение этого бита нужно устанавливать в соответствии с командой.

Регистр состояния выглядит так (смещение 2 от базового адреса):

Simp

DS1

DS0

0

0

INT

ERR

AC

AC - признак активности BusMaster. Он устанавливается в 1 когда начинается DMA обмен (сразу после установки в 1 бита E/D в командном регистре), после завершения обмена он сбрасывается в 0.

ERR - Признак ошибки передачи данных.

INT - признак прерывания. Если прерывания от контроллера разрешены, при поступлении прерывания от IDE устройства этот бит устанавливается в 1. (Чтобы его сбросить, нужно записать в него 1).

DS0 (DMASupport) поддержка DMA диском 0.

DS1 то же для диска 1.

Simp - признак симплексного режима (если 0 - первичный и вторичный каналы независимы и могут работать параллельно).

И наконец, 4 байтный порт (смещение 4 от базового адреса) в который загружается указатель на таблицу PRDT (см. ниже).

«Нормальный» DMA-обмен должен завершаться прерыванием от контроллера, тестировать никаких битов не нужно (иначе потеряется смысл в использовании DMA). По завершении выполнения операции, контроллер установит запрос прерывания, а его обработчик «скажет» операционной системе о том, что данные готовы. Я здесь в учебных целях (и чтобы не раздувать исходники обработчиками прерываний и их перехватом) приведу пример с запрещенными прерываниями, о готовности данных нам скажет бит AC.

С адресацией памяти надо будет разобраться поподробнее. Область обмена задается особым дескриптором PRD (PhysicalRegionDescriptor) который надо подготовить в памяти. Массив таких дескрипторов формирует таблицу PRDT, 4-байтный указатель на которую нужно загрузить в регистры котроллера PCIIDE. Никаких требований относительно описываемых областей не предъявляется, каждый дескриптор может описывать произвольный участок памяти. Сама таблица должна быть не более 64Кб размером, то есть может вмещать не более 8192 дескриптора.

Формат дескриптора PRD:

FLAG:WORD

COUNT:WORD

ADDRESS:DWORD

Поле ADDRESS размером 4 байта задает начальный физический адрес в памяти, с которого будет начинаться обмен.

Поле COUNT - размер области в байтах. Размер не должен быть меньше количества данных передаваемых диском, но может быть больше, в этом случае «лишняя» область не будет затронута. В команде «ReadsectorsDMA»  можно указать в регистре счетчика секторов значение большее 1, тогда будут прочитаны несколько секторов, начиная с заданного, значение поля COUNT следует подбирать так, чтобы туда «влезли» все прочитанные данные.

В поле FLAG используется только старший бит, все остальные зарезервированы. Если старший бит равен 1, текущий дескриптор последний в таблице. Это поле может иметь 2 значения - 8000h для последнего в таблице дескриптора, и 0000h для всех остальных.

С адресацией связан один очень тонкий момент, который служит благодатной почвой для многочасовых хождений по граблям. Грабли возникают в момент работы в защищенном режиме с включенной страничной переадресацией. Для тех, кто забыл, напомню, что процессор после формирования линейного адреса транслирует его по своим таблицам в физический адрес страницы, при этом «соседние» с точки зрения линейного адреса страницы могут иметь совершенно разные  физические адреса и располагаться в произвольных местах оперативной памяти. Программа работает именно с линейными адресами, она не знает, как они расположены физически, следовательно, адрес буфера, который вычисляет программа, будет линейным адресом этого буфера. Напротив, контроллер PCIIDE не подозревает, что в системе есть процессор, у которого есть механизмы страничной адресации. Загруженный адрес (как адрес самой таблицы PRDT, так и адреса всех описываемых ей областей) он всегда трактует как физический, именно этот адрес будет использоваться контроллером памяти. Таким образом, забота о соответствии адресов ложиться на широкие плечи программиста. Поэтому прежде чем загружать адреса, нужно уточнить у ОС физический адрес по линейному (все современные многозадачные ОС имеют такую функцию). В случае если страницы буфера физически «разорваны», приходится двигаться «перебежками», осуществляя обмен по 4Кб и каждый раз уточняя новый физический адрес (на практике обычно создают таблицу, состоящую из нескольких дескрипторов, которые описывают требуемые физические области). Еще один момент, который надо иметь ввиду – все процессоры, начиная с P6, поддерживают страничный режим PAE и 36-битную физическую адресацию памяти. Контроллер поддерживает только 32 бита.

Протокол DMA.

Как и в первой части, мы будем рассматривать DMA применительно к чтению одного сектора (загрузочного).

1) Запретить прерывания.

2) Дождаться BSY=0.

3) Выбрать устройство Master/Slave.

4) Дождаться BSY=0, DRDY=1.

5) Установить количество секторов.

6) Загрузить LBA адрес сектора.

7) Загрузить указатель на таблицу PRDT в соответствующий регистр.

8) Установить направление обмена.

9) Послать диску команду «чтение в режиме DMA».

10) Активизировать режим BusMaster записью 1 в бит E/D.

11) Дождаться AC=0, что свидетельствует о завершении DMA-обмена.

12) Запретить Bus Mastering

13) Разрешить прерывания.

Алгоритм достаточно простой, залог успеха - корректно подготовленная таблица PRDT. Все остальное не сложнее чем протокол PIO, рассмотренный в первой части.

И еще один момент, если количество переданных байт не соответствует записанному в дескрипторе PRD (если передано меньше), бит AC не сбросится. В этом случае надо отслеживать окончание обмена по биту INT (предварительно сбросив его, и не запрещая прерывания).

В случае записи на диск, изменяются пункты 8 и 9, бит направления надо ставить в 0, а код команды – WRITESECTORSDMA (код CAh).

Поскольку здесь не принципиально, каким образом считывать конфигурационное пространство IDE контроллера, я воспользовался функциями PCIBIOS (кому интересно как это все делать на уровне портов см. мою статью «Определение конфигурации на аппаратном уровне»). Функция B103h позволяет искать устройство по коду класса (она принимает в SI индекс устройства, в ECX код класса, AX=B103h. IDE контроллер может иметь одно из 3 значений класса - 010180h, 010185h, 01018Ah. В BX возвращаются координаты устройства на шине. В случае ошибок CF=1), после того как устройство найдено, функция B10Ah применяется для считывания слова по смещению 20h из конфигурационного пространства (координаты которого уже предоставлены в BX предыдущей функцией). Функция принимает в DI смещение слова (16-битного), в BX координаты устройства. Возвращает результат в ECX.

.386

data segment use16 ;Создаем таблицу PRDT из одной записи
 PRD_ADDR  dd 0
 PRD_COUNT dw 0
 PRD_FLAG  dw 0
 BUFFER db 512 dup (0)
data ends

base equ 1F0h ;Базовый адрес регистров контроллера первого канала

stck segment stack use16
 dw 1024 dup (?)
stck ends

code segment use16
assume cs:code,ss:stck,ds:data

PCI_DETECT PROC NEAR ;Процедура определяющая адрес регистров PCI IDE  
                                               ;с помощью функций PCI BIOS
 push esi
 push edi
 mov si,0
 mov ecx,010180h
try:
 mov ax,0B103h
 int 1Ah
 jnc next
 add ecx,5
 jmp try
next:
 mov ax,0B10Ah
 mov di,20h
 int 1Ah
 and cx,0FFF0h
 pop edi
 pop esi
ret
PCI_DETECT ENDP

;Входные параметры
;EDI[31:0] - указатель на PRDT
;ESI[27:0] - LBA адрес
;ESI[28] - Устройство (0 - Master, 1 - Slave)

READ_DMA PROC NEAR
 mov dx,base+206h ;Запрещаем прерывания
 mov al,2
 out dx,al

 mov dx,base+7 ;Ждем BSY=0
L1:
 in al,dx
 test al,80h
 jnz L1

 mov eax,esi ;Выбираем устройство и загружаем старшие разряды адреса
 shr eax,24
 or  al,0E0h
 mov dx,base+6
 out dx,al

 mov dx,base+7 ;Ждем BSY=0 и DRDY=1
L2:
 in al,dx
 test al,80h
 jnz L2
 test al,40h
 jz L2

 mov eax,esi ;Загружаем LBA адрес
 mov dx,base+3
 out dx,al
 shr eax,8
 inc dx
 out dx,al
 shr eax,8
 inc dx
 out dx,al

 mov dx,base+2 ; Количество секторов = 1
 mov al,1
 out dx,al

 call pci_detect ;В CX адрес блока регистров PCI IDE

 mov dx,cx ;Указатель на таблицу PRDT
 add dx,4
 mov eax,edi
 out dx,eax

 mov dx,cx ;Направление обмена - "в память"
 mov al,8
 out dx,al

 mov dx,base+7 ;Посылаем команду "READ SECTORS DMA"
 mov al,0C8h
 out dx,al

 mov dx,cx ;Разрешаем Bus Mastering (с этого момента начинается обмен)
 mov al,9
 out dx,al

 mov dx,cx ;Ждем AC=0 (DMA обмен закончен)
 add dx,2
L3:
 in al,dx
 test al,1
 jnz L3

mov dx,cx ;Запретить Bus Master
mov al,0
out dx,al

 mov dx,base+206h ;Разрешаем прерывания
 mov al,0
 out dx,al
ret
READ_DMA ENDP

start:
mov ax,data
mov ds,ax

mov eax,data
shl eax,4
mov edi,eax ;Получаем физический адрес таблицы и загружаем его в EDI
add eax,8
mov PRD_ADDR,eax ;Заполняем саму таблицу
mov PRD_COUNT,512
mov PRD_FLAG,08000h
mov esi,0 ;Загрузочный сектор Master-устройства
call read_dma

;Теперь в буфере лежит MBR

mov ax,4c00h
int 21h
code ends
end start

Заключение.

Вот мы и подошли к концу второй части. Как и в первой, вопросы/пожелания/предложения/возможные ляпы в статье (да, я тоже человек и могу ошибаться как и все) присылать сюда:

Dark_Master@tut.by

Много (очень много) доки по ATA можно найти на сайте WWW.T13.ORG

2002-2013 (c) wasm.ru