Контроллер DMA
Контроллер DMA используется для обмена данными между внешними устройствами и памятью. Он нужен в работе с жесткими дисками и дисководами, звуковыми платами и другими устройствами, работающими со значительными объемами данных. Начиная с PC AT, в компьютерах присутствуют два DMA-котроллера — 8-битный (с каналами 0, 1, 2 и 3) и 16-битный (с каналами 4, 5, 6 и 7). Канал 2 используется для обмена данными с дисководами, канал 3 — для жестких дисков, канал 4 теряется при каскадировании контроллеров, а назначение остальных каналов может варьироваться.
DMA позволяет выполнить чтение или запись блока данных, начинающегося с линейного адреса, описываемого как 20-битное число для первого DMA-контроллера и как 24-битное — для второго, то есть данные для 8-битного DMA должны располагаться в пределах первого мегабайта памяти, а для второго — в пределах первых 16 Мб. Старшие четыре бита для 20-битных адресов и старшие 8 бит для 24-битных адресов хранятся в регистрах страниц DMA, адресуемых через порты 80h – 8Fh:
порт 81h: страничный адрес для канала 2 (биты 3 – 0 = биты 19 – 16 адреса)
порт 82h: страничный адрес для канала 3 (биты 3 – 0 = биты 19 – 16 адреса)
порт 83h: страничный адрес для канала 1 (биты 3 – 0 = биты 19 – 16 адреса)
порт 87h: страничный адрес для канала 0 (биты 3 – 0 = биты 19 – 16 адреса)
порт 89h: страничный адрес для канала 6 (биты 7 – 0 = биты 23 – 17 адреса)
порт 8Ah: страничный адрес для канала 7 (биты 7 – 0 = биты 23 – 17 адреса)
порт 8Bh: страничный адрес для канала 5 (биты 7 – 0 = биты 23 – 17 адреса)
Страничный адрес определяет начало 64 Кб/128 Кб участка памяти, с которым будет работать данный канал, поэтому при передаче данных через DMA обязательно надо следить за тем, чтобы не было выхода за границы этого участка, то есть чтобы не было попытки пересечения адреса 1000h:0, 2000h:0, 3000h:0 для первого DMA или 2000h:0, 4000h:0, 6000h:0 — для второго.
Младшие 16 бит адреса записывают в следующие порты:
00h: биты 15 – 0 адреса блока данных для канала 0
01h: счетчик переданных байт канала 0
02h – 03h: аналогично для канала 1
04h – 05h: аналогично для канала 2
06h – 07h: аналогично для канала 3
(для этих портов используются две операции чтения/записи — сначала передаются биты 7 – 0, затем биты 15 – 8)
C0h: биты 8 – 1 адреса блока данных для канала 4 (бит 0 адреса всегда равен нулю)
C1h: биты 16 – 9 адреса блока данных для канала 4
C2h: младший байт счетчика переданных слов канала 4
C3h: старший байт счетчика переданных слов канала 4
C4h – C7h: аналогично для канала 5
C8h – CBh: аналогично для канала 5
CCh – CFh: аналогично для канала 5
(эти порты рассчитаны на чтение/запись целыми словами)
Каждый из этих двух DMA-контроллеров также имеет собственный набор управляющих регистров — регистры первого контроллера адресуются через порты 08h – 0Fh, а второго — через D0 – DFh:
порт 08h/D0h для чтения: регистр состояния DMA
бит 7, 6, 5, 4: установлен запрос на DMA на канале 3/7, 2/6, 1/5, 0/4
бит 3, 2, 1, 0: закончился DMA на канале 3/7, 2/6, 1/5, 0/4
порт 08h/D0h для записи: регистр команд DMA (устанавливается BIOS)
бит 7: сигнал DACK использует высокий уровень
бит 6: сигнал DREQ использует высокий уровень
бит 5: 1/0 — расширенный/задержанный цикл записи
бит 4: 1/0 — приоритеты сменяются циклически/фиксированно
бит 3: сжатие во времени
бит 2: DMA-контроллер отключен
бит 1: разрешен захват канала 0 (для режима память-память)
бит 0: включен режим память-память (канал 0 – канал 1)
порт 09h/D2h для записи: регистр запроса DMA
бит 2: 1/0 — установка/сброс запроса на DMA
биты 1 – 0: номер канала (00, 01, 10, 11 = 0/4, 1/5, 2/6, 3/7)
порт 0Ah/ D4h для записи: регистр маски канала DMA
бит 2: 1/0 — установка/сброс маскирующего бита
биты 1 – 0: номер канала (00, 01, 10, 11 = 0/4, 1/5, 2/6, 3/7)
порт 0Bh/D6h для записи: регистр режима DMA
биты 7 – 6:
00 — передача по запросу
01 — одиночная передача (используется для звука)
10 — блочная передача (используется для дисков)
11 — канал занят для каскадирования
бит 5: 1/0 — адреса уменьшаются/увеличиваются
бит 4: режим автоинициализации
биты 3 – 2:
00 — проверка
01 — запись
10 — чтение
биты 1 – 0: номер канала (00, 01, 10, 11 = 0/4, 1/5, 2/6, 3/7)
порт 0Ch/D8h для записи: сброс переключателя младший/старший байт
Для чтения/записи 16-битных значений из/в 8-битные порты 00h – 08h. Следующий байт, переданный в эти порты, будет считаться младшим, следующий за ним — старшим.
порт 0Dh/DAh для записи: сброс контроллера DMA
Любая запись сюда приводит к полному сбросу DMA-контроллера, так что его надо инициализировать заново.
порт 0Dh/DAh для чтения: последний переданный байт/слово.
порт 0Eh/DCh для записи: любая запись снимает маскирующие биты со всех каналов
порт 0Fh/DEh для записи: регистр маски всех каналов:
биты 3 – 0: маскирующие биты каналов 3/7, 2/6, 1/5, 0/4
Чаще всего внешнее устройство само инициализирует передачу данных, и все, что необходимо сделать программе, — это записать адрес начала буфера в порты, соответствующие используемому каналу, длину передаваемого блока данных минус один в регистр счетчика соответствующего канала, установить нужный режим работы канала и снять маскирующий бит.
В качестве примера вернемся к программированию звуковых плат и изменим программу wavdir. asm так, чтобы она использовала DMA.
; wavdma.asm ; Пример программы, проигрывающей файл C:\WINDOWS\MEDIA\TADA.WAV ; на звуковой карте при помощи DMA FILESPEC equ "c:\windows\media\tada.wav" ; заменить на c:\windows\tada.wav ; для старых версий Windows SBPORT equ 220h ; SBDMA equ 1 ; процедура program_dma рассчитана только на канал 1 SBIRQ equ 5 ; только IRQ0 - IRQ7 .model tiny .code .186 org 100h ; СОМ-программа start: call dsp_reset ; инициализация DSP jc no_blaster mov bl,0D1h ; команда OD1h call dsp_write ; включить звук call open_file ; прочитать файл в буфер call hook_sbirq ; перехватить прерывание mov bl,40h ; команда 40h call dsp_write ; установка скорости передачи mov bl,0B2h ; константа для 11025Hz/Stereo call dsp_write call program_dma ; начать DMA-передачу данных
main_loop: ; основной цикл cmp byte ptr finished_flag,0 je main_loop ; выход, когда байт finished_flag = 1
call restore_sbirq ; восстановить прерывание no_blaster: ret
old_sbirq dd ? ; адрес старого обработчика finished_flag db 0 ; флаг окончания работы filename db FILESPEC,0 ; имя файла
; обработчик прерывания звуковой карты ; устанавливает флаг finished_flag в 1
sbirq_handler proc far push ax mov byte ptr cs:finished_flag,1 ; установить флаг mov al,20h ; послать команду EOI out 20h,al ; в контроллер прерываний pop ax iret sbirq_handler endp
; процедура dsp_reset ; сброс и инициализация DSP dsp_reset proc near mov dx,SBPORT+6 ; порт 226h - регистр сброса DSP mov al,1 ; запись в него единицы ; запускает инициализацию out dx,al mov cx,40 ; небольшая пауза dsploop: in al,dx loop dsploop mov al,0 ; запись нуля завершает инициализацию out dx,al ; теперь DSP готов к работе add dx,8 ; порт 22Eh - бит 7 при чтении ; указывает на занятость mov сх,100 ; буфера записи DSP check_port: in al,dx ; прочитать состояние буфера записи, and al,80h ; если бит 7 ноль, jz port_not_ready ; порт еще не готов, sub dx,4 ; иначе: порт 22Аh - чтение данных из DSP in al,dx add dx,4 ; порт снова 22Eh cmp al,0AAh ; проверить, что DSP возвращает 0AAh ; при чтении - это сигнал его готовности ; к работе je good_reset port_not_ready: loop check_port ; повторить проверку на 0AAh 100 раз, bad_reset: stc ; если Sound Blaster не откликается, ret ; вернуться с CF = 1, good_reset: clc ; если инициализация прошла успешно, ret ; вернуться с CF = 0 dsp_reset endp
; процедура dsp_write ; посылает байт из BL в DSP dsp_write proc near mov dx,SBPORT+0Ch ; порт 22Ch - ввод данных/команд DSP write_loop: ; подождать готовности буфера записи DSP, in al,dx ; прочитать порт 22Ch and al,80h ; и проверить бит 7, jnz write_loop ; если он не ноль - подождать еще, mov al,bl ; иначе: out dx,al ; послать данные ret dsp_write endp
; процедура hook_sbirq ; перехватывает прерывание звуковой карты и разрешает его hook_sbirq proc near mov ax,3508h+SBIRQ ; AH = 35h, AL = номер прерывания int 21h ; получить адрес старого обработчика mov word ptr old_sbirq,bx ; и сохранить его mov word ptr old_sbirq+2,es mov ax,2508h+SBIRQ ; AH = 25h, AL = номер прерывания mov dx,offset sbirq_handler ; установить новый обработчик int 21h mov cl,1 shl cl,SBIRQ not cl ; построить битовую маску in al,21h ; прочитать OCW1 and al,cl ; разрешить прерывание out 21h,al ; запиать OCW1 ret hook_sbirq endp
; процедура restore_sbirq ; восстанавливает обработчик и запрещает прерывание restore_sbirq proc near mov ax,3508h+SBIRQ ; AH = 25h, AL = номер прерывания lds dx,dword ptr old_sbirq int 21h ; восстановить обработчик mov cl,1 shl cl,SBIRQ ; построить битовую маску in al,21h ; прочитать OCW1 or al,cl ; запретить прерывание out 21h,al ; записать OCW1 ret restore_sbirq endp
; процедура open_file ; открывает файл filename и копирует звуковые данные из него, ; считая, что это - tada.wav, в буфер buffer open_file proc near mov ax,3D00h ; AH = 3Dh, AL = 00 mov dx,offset filename ; DS:DX - ASCIZ-строка с именем файла int 21h ; открыть файл для чтения, jc error_exit ; если не удалось открыть файл - выйти mov bx,ax ; идентификатор файла в ВХ mov ax,4200h ; АН = 42h, AL = 0 mov cx,0 ; CX:DX - новое значение указателя mov dx,38h ; по этому адресу начинаются данные ; в tada.wav int 21h ; переместить файловый указатель mov ah,3Fh ; АН = 3Fh mov cx,27459 ; это - длина данных в файле tada.wav push ds mov dx,ds and dx,0F000h ; выровнять буфер на границу ; 4-килобайтной страницы add dx,1000h ; для DMA mov ds,dx mov dx,0 ; DS:DX - адрес буфера int 21h ; чтение файла pop ds ret error_exit: ; если не удалось открыть файл, mov ah,9 ; АН = 09h mov dx,offset notopenmsg ; DS:DX = адрес сообщения об ошибке int 21h ; вывод строки на экран int 20h ; конец программы
; сообщение об ошибке notopenmsg db "Ошибка при открытии файла",0Dh,0Ah,'$'
open_file endp
; процедура program_dma ; настраивает канал 1 DMA program_dma proc near mov al,5 ; замаскировать канал 1 out 0Ah,al xor al,al ; обнулить счетчик out 0Ch,al mov al,49h ; установить режим передачи ; (используйте 59h для автоинициализации) out 0Bh,al push cs pop dx and dh,0F0h add dh,10h ; вычислить адрес буфера xor ax,ax out 02h,al ; записать младшие 8 бит out 02h,al ; записать следующие 8 бит mov al,dh shr al,4 out 83h,al ; записать старшие 4 бита
mov ax,27459 ; длина данных в tada.wav dec ax ; DMA требует длину минус один out 03h,al ; записать младшие 8 бит длины mov al,ah out 03h,al ; записать старшие 8 бит длины mov al,1 out 0Ah,al ; снять маску с канала 1
mov bl,14h ; команда 14h call dsp_write ; 8-битное простое DMA-воспроизведение mov bx,27459 ; размер данных в tada.wav dec bx ; минус 1 call dsp_write ; записать в DSP младшие 8 бит длины mov bl,bh call dsp_write ; и старшие ret program_dma endp end start
В этом примере задействован обычный DMA-режим работы, в котором звуковая плата проигрывает участок данных, вызывает прерывание, и, пока обработчик прерывания подготавливает новый буфер данных, программирует DMA и звуковую плату для продолжения воспроизведения, проходит некоторое время, что может звучать как щелчок. Этого можно избежать, если воспользоваться режимом автоинициализации, который позволяет обойтись без остановок при воспроизведении.
При использовании режима DMA с автоинициализацией нужно сделать следующее: загрузить начало воспроизводимого звука в буфер длиной, например, 8 Кб и запрограммировать DMA на его передачу с автоинициализацией. Затем сообщить DSP, что проигрывается звук с автоинициализацией и размер буфера равен 4 Кб. Теперь, когда придет прерывание от звуковой платы, она не остановится и продолжит воспроизведение из вторых 4 Кб буфера, так как находится в режиме автоинициализации. Далее запишем в первые 4 Кб следующий блок данных. Когда кончится 8-килобайтный буфер, DMA начнет посылать его сначала, потому что мы его тоже запрограммировали для автоинициализации (бит 4 порта 0Bh/D6h), DSP вызовет прерывание и тоже не остановится, продолжая воспроизводить данные, которые посылает ему DMA-контроллер, а мы тем временем запишем во вторые 4 Кб буфера следующий участок проигрываемого файла и т.д.
Содержание раздела