Четверг, 21 Ноября 2024, 13:51

Приветствую Вас Гость

[ Новые сообщения · Игроделы · Правила · Поиск ]
  • Страница 1 из 1
  • 1
ОБРАБОТКА ПРЕРЫВАНИЯ ЗВУКОВОЙ КАРТЫ НА ВЕДОМОМ КОНТРОЛЛЕРЕ
GradiusДата: Понедельник, 15 Июля 2019, 05:21 | Сообщение # 1
был не раз
Сейчас нет на сайте
Занимаюсь программированием на ПК(и не только) на низком уровне.
Довольно часто приходится писать программы или кидать порты под DOS + DPMI.

Многие сейчас усмехнутся, сказав, что ДОС умер...
Может быть это и так, но DOS по крайней мере живёт на моём компьютере и в голове :)

Ближе к делу...

Дано:

1) Звуковая карта PCI Sound Blaster Live (насколько я понял, от саундбластера там осталось лишь одно название). Чип EMU10K.
2) Для ДОС были найдены драйверы, которые эмулируют режим SB16 через эту карту (с помощью VCPI EMM386). Тоесть - это софтовая эмуляция SB16, а не Legacy, как это сделано, к примеру, на картах Cmedia CM8738.
3) С помощью этих драйверов можно конфигурировать SB16: назначить порт, прерывание и номер DMA-канала.
4) DOS-Extender DOS4G (коммерческая версия самого популярного расширителя ДОС: DOS4GW. Почему используется именно он - см. ниже).

Надо:

Реализовать непрерывное воспроизведение аудио-потока с помощью кольцевого буфера с подгрузкой новых данных по прерыванию, вызывающегося после проигрывания очередной половины буфера.

Проблема:

Когда эмуляция SB16 настроена на одно из прерываний ведомого контроллера прерываний (IRQ8 - IRQ15), к примеру - IRQ10, то обработчик прерывания НЕ вызывается!

Если же настроить карту на одно из прерываний ведущего контроллера (IRQ0 - IRQ7), к примеру - IRQ5, то прерывание происходит и всё работает как надо.

Данное поведение, честно говоря, ввело меня в ступор... Попробовал тоже самое сделать в реальном режиме на Turbo Pascal 7.0 - алгоритм работает на IRQ5 и на IRQ10 !!!

Использую компилятор Open Watcom ASM/C/C++ последней официальной версии. Платформа - DOS32(sys dos4g).

Поиск в интернете:

... привел меня к товарищам по несчастью (несколько лет назад):

1) http://forum.codenet.ru/q8115/ - точечное попадание

2) https://groups.google.com/forum....ytOYydw - здесь аналогичная проблема: не могут обработать IRQ15 от жёсткого диска.

Был найден сайтик с ценной информацией, на котором объяснены причины такого поведения: http://rgmroman.narod.ru/Dos4g.htm

Цитата

DOS/4(W) has auto passup range for interrupts 08h-2Eh, i.e. if any of that interrupt occured in real mode, extender reflects it to protected mode.
For interrupts occured in protected mode externder searches interrupt handler in protected and if none found pass interrupt down to real mode.


Если же вспомнить, что Master-контроллер прерываний по умолчанию настроен на INT 0x8..INT 0xF (IRQ0..IRQ7), а Slave-контроллер на INT 0x70..INT 0x77 (IRQ8..IRQ15),
то становится очевидным, что DOS4GW не может работать с IRQ8..IRQ15, если просто установить обработчик прерывания стандартной C-функцией:

Код

_dos_setvect(0x72,SB16_New); // INT 0x72 - IRQ10


или с помощью DPMI функции 0x205 (установка обработчика прерывания защищённого режима).

Решение номер 1:

Не совсем красивое с точки зрения технической эстетики.

Решение возникло на основе знаний о том, что прерывание IRQ10 от звуковой карты всё-же обрабатывалось, если оно было установлено программой РЕАЛЬНОГО режима.

На Turbo Pascal, а позже на Flat Assembler, был написан небольшой резидентик, устанавливающий обработчик прерывания в реальном режиме,
сигнализирующий о том, что карта завершила чтение буфера:

Код

ORG    0x100

jmp    int0x72

IRQSlave:
push    ax
push    dx
mov    al,1                ;флаг SB_RM_FLAG
mov    BYTE [cs:SBFlag],al ;узнаём порт (заполняется 32-битной программой, см. ниже)
mov    dx,WORD [cs:SBPort]
add    dx,0x00F
in    al,dx
mov    al,0x20
out    0x20,al
out    0xA0,al
pop    dx
pop    ax
iret

SBPort    dw 0x220
SBFlag    db 0

int0x72:
mov    ah,0x25
mov    al,0x72
mov    dx,IRQSlave
int    21h                 ;установка вектора прерывания в реальном режиме
mov    dx,int0x72          ;завершить работу программы, оставив её часть резидентной (от IRQSlave до int0x72)
int    27h


Как видно из программы, ничего особенного - обработчик просто подтверждает прерывание чтением из порта и
записывает 1 в ячейку памяти со смещением SBFlag от начала кода обработчика (это байт со смещением 0x1C).

Далее собственно, мы проверяем этот флаг из нашей программы защищённого режима в другом обработчике прерываний, с которым проблем не возникает.
Это прерывания таймера IRQ0:

Код

volatile u8 *SB_RM_FLAG; //флаг завершения чтения данных от обработчика прерывания реального режима

SB_RM_FLAG=(u8*)(((*(u16*)((0x72UL*4)+2))<<4)+(*(u16*)(0x72UL*4))); //указатель указывает на физический адрес вектора прерывания INT 0x72 реального режима

void interrupt far TimerNew(void)
{
if(SB_RM_FLAG)  //если флаг установлен
{
  SB_RM_FLAG=0;  //то сбрасываем его
  MIXER();       //подгружаем новые данные в свобродную половину буфера
}

out8(0x20,0x20); //шлём EOI ведущему и ведомому контроллеру прерываний
out8(0xA0,0x20);
}


Решение блестяще работает как на ведущих, так и на ведомых прерываниях.

Недостатки решения:

1) Требуется постоянно запускать программу-резидент перед запуском основной программы

2) Постоянный опрос флага по прерыванию таймера

Решение номер 2:

Избавляемся от программы-резидента.

В стандарте DPMI (dpmispec1.0.pdf) есть интересная функция: 0x201 - Set Real Mode Interrupt Vector.
Она требует на входе указания номера вектора прерывания, а также адрес обработчика реального режима в формате сегмент:смещение.

Попутно понадобилась ещё одна функция - выделить блок памяти ниже 1 МБ (для размещения кода обработчика прерывания РЕАЛЬНОГО режима).
Это DPMI функция 0x100 - Allocate DOS Memory Block.

Обработчик прерывания был взят с программы-резидента и закодирован в C-массив констант:

Код

/*
Real Mode Interrupt Handler Code for SoundBlaster16
*/
const u8 SB_New_RM_Handler[0x1D]=
{
0x50,                        //push    ax
0x52,                        //push    dx
0xB0,0x01,                   //mov    al,1
0x2E,0xA2,0x1C,0x00,         //mov    BYTE [cs:SBFlag],al
0x2E,0x8B,0x16,0x1A,0x00,    //mov    dx,WORD [cs:SBPort]
0x83,0xC2,0x0F,              //add    dx,0x00F
0xEC,                        //in    al,dx
0xB0,0x20,                   //mov    al,0x20
0xE6,0x20,                   //out    0x20,al
0xE6,0xA0,                   //out    0xA0,al
0x5A,                        //pop    dx
0x58,                        //pop    ax
0xCF,                        //iret
0x20,0x02,                   //SBPort    dw 0x220
0x00,                        //SBFlag    db 0
};


Но так как данный массив скорее всего будет размещён линковщиком по адресу выше первого мегабайта,
то его необходимо скопировать в заранее выделенную область памяти с помощью вышеназванной функции 0x100.

Целиком набор функций инжектора кода обработчика прерывания реального режима выглядит так:

Код

s8 DOS_Allocate(u16 paragraph,u16 *segment,u16 *selector) //выделяет память в адресном пространстве ниже 1 МБ
{
s8 r=0;
u16 _seg;
u16 _sel;
_asm
{
  mov ax,0x100
  mov bx,paragraph
  int 0x31
  jnc OK
  mov r,-1
  OK:
  mov _seg,ax
  mov _sel,dx
}
*segment=_seg;
*selector=_sel;
return r;
}

s8 Set_RM_Handler(u8 i,u16 s,u16 o) //установка обработчика прерывания реального режима (по номеру прерывания и по адресу сегмент:смещение)
{
s8 r=0;
_asm
{
  mov ax,0x201
  mov bl,i
  mov cx,s
  mov dx,o
  int 0x31
  jnc OK
  mov r,-1
  OK:
}
return r;
}

u16 SB_RM_SELECTOR; //селектор на участок памяти ниже 1 МБ (нужен для освобождения памяти по завершению работы программы)

s8 SB_Set_RM_Handler(u8 i,u8 *handler,u16 size)
{
u16 segment; //сегмент участка памяти ниже 1 МБ, полученный функцияе 0x100

if(DOS_Allocate((size+15)>>4,&segment,&SB_RM_SELECTOR))return -1; //просим необходимое число параграфов

u32 physical=((u32)segment)<<4; //вычисляем физический адрес (необходим для считывания флага, установленного обработчиком прерывания)

SB_RM_FLAG=(u8*)(physical+0x1C); //находим физический адрес нашего флага

*(u16*)&handler[0x1A]=SB_IO; //set SB I/O Port - записываем номер порта подтверждения прерывания (помогаем обработчику)

memcpy((void*)physical,handler,size); //копируем код обработчика в выделенную память ниже 1 МБ

if(Set_RM_Handler(i,segment,0))return -1; //устанавливаем обработчик прерывания РЕАЛЬНОГО режима из-под ЗАЩИЩЁННОГО.  Смещение = 0.

return 0;
}


Вызов:

Код

SB_DisableIRQ(); //запрещаем прерывание от SB16 на время установки нового обработчика

if(SB_IRQ<8) //настройка прерывания от ведущего контроллера(Master PIC, IRQ0..7)
{
if(SB_Set_RM_Handler(SB_IRQ+0x08,(u8*)SB_New_RM_Handler,sizeof(SB_New_RM_Handler)))DPMI_Error();
}
else  //настройка прерывания от ведомого контроллера(Slave PIC, IRQ 8..15)
{
if(SB_Set_RM_Handler(SB_IRQ-8+0x70,(u8*)SB_New_RM_Handler,sizeof(SB_New_RM_Handler)))DPMI_Error();
}

SB_EnableIRQ(); //разрешаем прерывания Sound Blaster 16


Труды увенчались успехом!!! Программа работает с любым IRQ от любого контроллера - ведущего или ведомого.
Проверял: в MS DOS 7.1, Free DOS, Win98, WinXP - всё работает.

Следует участь одну важную деталь: в DOS4GW база всегда =0, поэтому 32-битное смещение(ближний указатель) равен физическому адресу.
Поэтому мы просто можем обращатся к ячейкам памяти, умножив сегмент на 16 и сложив со смещением (для реального режима).
В других расширителях ДОС база может быть ненулевой, поэтому код прийдётся скорректировать!

Важное замечание:

Здесь и далее опущены моменты освобождения памяти и восстановления прежних обработчиков, адреса/селекторы которых были заранее получены специальными функциями DPMI.
Сделано это с целью упростить объём и изложение материала, сфокусировавшись на главном.

И наконец -

Решение 3.

Обрабатываем всё в прерывании от звуковой карты! Освобождаем обработчик таймера от постоянного опроса флага.

Полезная функция DPMI: 0x303 - Allocate Real Mode Callback Address.
Функция даёт адрес реального режима сегмент:смещение - точка входа в РЕАЛЬНОМ режиме, в которой будет вызов функции ЗАЩИЩЁННОГО режима.
Коллбэк позволяет вызвать процедуру ЗАЩИЩЁННОГО режима, если процессор находится РЕАЛЬНОМ (или V86) режиме!

Объединив это с предыдущим решением, мы получим обработку прерывания в реальном режиме с заходом в функцию защищённого режима.

Следует отметить, что в процедуре колбэка нам нужно будет восстановить виртуальные регистры (V86 mode) Flags, CS:IP обработчика реального режима.

Обработчик коллбэка написан на Watcom Assembler:

Код

.CODE

EXTRN    __GETDS:PROC        ;это ваткомовский прикол, без него вызов функций внутри обработчика приводит к аварийному вылету программы
EXTRN    UserCallBack_:PROC  ;это пользовательская функция коллбэка

PUBLIC    SystemCallBack_

SystemCallBack_:
    pushad
    push  ds
    push  es
    push  fs
    push  gs
    cld
    lodsw
    mov  es:[edi+0x2A],ax ;IP
    lodsw
    mov  es:[edi+0x2C],ax ;CS
    lodsw
    mov  es:[edi+0x20],ax ;FLAGS
    call  __GETDS
    push  ds
    pop  es
    call  UserCallBack_
    pop  gs
    pop  fs
    pop  es
    pop  ds
    popad
    iretd

END


Отказ от написания обработчика на C/C++ вызван здесь тем, что компилятор C/C++ затирает регистры ds,es,esi,edi, используя их для своих нужд.
А они нам нужны для получения виртуального контекста.
Подробнее см. в спецификации DPMI (функция 0x303; передача и приём виртуального контекста регистров V86).

Пользовательская функция коллбэка:

Код

void UserCallBack(void)
{
MIXER(); //загружаем новые данные

IO u8 ack=in8(SB_IO+0xF); //подтверждаем прерывание

out8(0x20,0x20); //EOI для Master и Slave PIC
out8(0xA0,0x20);
}


Функция выдачи адреса коллбэка (РЕАЛЬНЫЙ режим сегмент:смещение) и установки его на обработчик прерывания РЕАЛЬНОГО режима:

Код

struct RMA //структура адреса реального режима
{
u16 s; //сегмент
u16 o; //смещение
};

RMA SB_RM_New;

s8 Set_RM_HandlerCallBack(u8 i,void far *handler)
{
if(CallBack_Allocate(handler,&SB_RM_New))return -1; //выделяем адрес коллбэка RM, при вызове которого будет происходить вызов PM функции handler
if(Set_RM_Handler(i,SB_RM_New))return -1;           //устанавливаем этот коллбэк в качестве вектора для обработчика RM прерывания!
return 0;
}


Теперь установка прерываний выглядит так:

Код

SB_DisableIRQ(); //запрещаем прерывания звуковой платы

if(SB_IRQ<8) //если IRQ от ведущего контроллера прерываний
{
if(Set_RM_HandlerCallBack(SB_IRQ+0x08,SystemCallBack))DPMI_Error();
}
else //если IRQ от ведомого
{
if(Set_RM_HandlerCallBack(SB_IRQ-8+0x70,SystemCallBack))DPMI_Error();
}

SB_EnableIRQ(); //разрешаем прерывания SoundBlaster


Функция выдачи адреса коллбэка на RM:

Код

#pragma pack(1) //запрещаем выравнивание членов структуры, чтобы не было дырок
struct RMI      //виртуальные регистры для RM V86 (контекст для реального обработчика прерывания)
{
u32 EDI,ESI,EBP,ReservedByRMI,EBX,EDX,ECX,EAX;
u16 flags,ES,DS,FS,GS,IP,CS,SP,SS;
};

s8 CallBack_Allocate(void far *c,RMA *a) //32-битный указатель на SystemCallBack, получаем сегмент:смещения для вызова PM коллбэка в RM
{
union REGS r;
struct SREGS sr;
static RMI rmi;

memset(&sr,0,sizeof(sr));
memset(&rmi,0,sizeof(rmi)); //здесь контекст виртуальных регистров (в обработчике коллбэка восстанавливаем Flags, CS, IP - см. выше)

r.w.ax=0x303;
sr.ds=FP_SEG(c);
r.x.esi=FP_OFF(c);
sr.es=FP_SEG(&rmi);
r.x.edi=FP_OFF(&rmi);
int386x(0x31,&r,&r,&sr);

if(r.w.cflag)return -1;

a->s=r.w.cx;
a->o=r.w.dx;

return 0;
}


Функция установки обработчика прерывания RM (изменён формат передачи данных - через структуру):

Код

s8 Set_RM_Handler(u8 i,RMA a)
{
s8 r=0;
u16 s=a.s;
u16 o=a.o;
_asm
{
  mov ax,0x201
  mov bl,i
  mov cx,s
  mov dx,o
  int 0x31
  jnc OK
  mov r,-1
  OK:
}
return r;
}


Решение успешно работает на ведущих и ведомых IRQ, проверял на: MS DOS 7.1, Free DOS, Win98, WinXP, эмуляторе DOSBox.

Необходима КОММЕРЧЕСКАЯ версия ДОС-экстендера - DOS/4G.
Так как некоммерческая версия НЕ поддерживает DPMI-функцию установки RM коллбэка(0x303) и имеет ограничение на максимально используемую память!

Следует отметить, что данный приём оказался совместимым и с другими DOS-extender'ами:

1) causeway
2) pmodew (для обработки прерываний он не использует V86, а переходит в настоящий RM)
3) dos32

И что самое интересное, они в СВЕЖИХ версиях - поддерживают обработку всех аппаратных прерываний (IRQ: ведущие и ведомые).

И можно просто сделать:

Код

_dos_setvect(0x72,SB16_New); // INT 0x72 - IRQ10


или с помощью DPMI функции 0x205 (установка обработчика прерывания PM).

И оно будет работать, в отличие от DOS4G, DOS4GW.

Так же следует отметить, что версии расширителей ДОС, входящие в состав Open Watcom - СТАРОЙ версии, поэтому описаный здесь трюк будет как нельзя кстати!

Надеюсь, выложенная здесь информация будет кому-то полезной/интересной.

Всем, кто дочитал - СПАСИБО! - за проявленный интерес! :)

Данный приём успешно работает в игре Gradius III Total Terror (порт для *DOS): https://gcup.ru/forum/28-101736-1

Ну и на последок, ссылка на FAQ по Watcom C/C++: http://www.codenet.ru/progr/cpp/wfaq.php
  • Страница 1 из 1
  • 1
Поиск:

Все права сохранены. GcUp.ru © 2008-2024 Рейтинг