Как стать автором
Обновить

Старые ISA-видеокарты и AVR

Время на прочтение28 мин
Количество просмотров47K
Лет десять тому назад достались мне предназначенные на помойку ISA-видеокарты от 286...486 машин. Видеокарты были опробованы и с тех пор пылились в ящике. Пару лет назад появилась у меня мысль, а не подключить ли такую видеокарту к микроконтроллеру? Вот об этом я и расскажу в статье.

Чтобы подключить старую ISA-видеокарту достаточно 8-битной шины данных и 20-битной шины адреса. Микроконтроллеры я люблю семейства AVR за их простоту, так что взял я Atmega16. Но вы можете взять любой удобный для вас — в этом случае у того же stm32 ножек точно хватит и без внешней обвязки. А вот у Atmega16 ножек на все эти шины не хватит, так что шина адреса была собрана на трёх ещё советских параллельных регистрах (у меня есть их большой запас) К588ИР1. Микроконтроллер по-очереди задаёт в этих трёх регистрах части адреса. Большего и не требуется.


На ISA-разъёме выходы этой схемы нужно подключить так:

+5,
+12,
GND,
REFRESH (притянут к +5В через резистор),
A0-A19,
D0-D7,
RESET,
MEMW,
MEMR,
IOW,
IOR,
ALE,
RDY,
AEN (подключается к GND).


На картинке красным я отметил требующие подключения контакты разъёма ISA.

Для некоторых видеокарт нужно подключить -5 В и -12 В (их придётся вам откуда-то взять — например, из источников TracoPower) и сигнал OSC (14.318 МГц) — его можно генерировать простейшим генератором на К155ЛН1. Другим видеокартам эти линии не требуются. Тут уж как повезёт. В общем, если на видеокарте соответствующая ножка на ISA висит в воздухе — её точно можно не подключать. Имейте в виду, потребление видеокартой по линии +5В довольно существенное — если вы будете использовать для питания что-то типа LM7805, то обязательно поставьте её на радиатор (лучше с вентилятором).

Лично у меня собранная конструкция выглядит вот так:

image

Осталось дело за малым — как-то инициализировать видеокарту и начать с ней работать. В интернете есть подобные проекты — я нашёл один (ссылка), откуда и взял код инициализации видеокарты Trident 9000i. В той же программе из интернета есть и код инициализации для Trident9000C, но в комментариях указано, что он не работает. Я проверил. Действительно не работает — на экране мусор и видеокарта не реагирует на запись данных в ОЗУ.

Видео работы (картинка передавалась по SPI на Atmega16 (как видите, на схеме эти линии оставлены свободными) через LPT-порт компьютера):


(В видео я оговорился — режим 320x200, а не 320x240)

Объединив данный модуль с оптической мышкой (статья про использование сенсора мышки) я получил вот это:



Если же у вас есть желание запустить любую имеющуюся видеокарту ISA, то для этого вам следует в интернете найти BIOS требуемой видеокарты (скажем, тут) и с помощью IDA дизассемблировать его. Там обычный X86 код. Только начинается он не с 0 адреса — там сигнатура (2 байта) и контрольная сумма (1 байт). Итого, начинать надо с 3-его байта. И последовательно выяснить в какие порты что нужно записать, чтобы карта заработала. Скажу честно, мне терпения не хватило, чтобы понять, что не так с Trident9000C.

Для работы с шиной ISA был написан модуль:

Модуль для работы с шиной ISA
//****************************************************************************************************
//основные системные функции для работы с шиной ISA
//****************************************************************************************************


//****************************************************************************************************
//макроопределения
//****************************************************************************************************

//шина адреса
#define ADDR_BUS_PORT PORTA
#define ADDR_BUS_DDR  DDRA

//переключение адреса A0-A7
#define ADDR_BUS_0_7_SELECT_PORT PORTC
#define ADDR_BUS_0_7_SELECT_DDR  DDRC
#define ADDR_BUS_0_7_SELECT      7

//переключение адреса A8-A15
#define ADDR_BUS_8_15_SELECT_PORT PORTC
#define ADDR_BUS_8_15_SELECT_DDR  DDRC
#define ADDR_BUS_8_15_SELECT      6

//переключение адреса A16-A19
#define ADDR_BUS_16_19_SELECT_PORT PORTC
#define ADDR_BUS_16_19_SELECT_DDR  DDRC
#define ADDR_BUS_16_19_SELECT      5

//шина данных
#define DATA_BUS_PORT PORTB
#define DATA_BUS_DDR  DDRB
#define DATA_BUS_PIN  PINB

//RESET
#define RESET_DDR  DDRD
#define RESET_PORT PORTD
#define RESET      0

//MEMW
#define MEMW_DDR  DDRD
#define MEMW_PORT PORTD
#define MEMW      1

//MEMR
#define MEMR_DDR  DDRD
#define MEMR_PORT PORTD
#define MEMR      2

//IOW
#define IOW_DDR  DDRD
#define IOW_PORT PORTD
#define IOW      3

//IOR
#define IOR_DDR  DDRD
#define IOR_PORT PORTD
#define IOR      4

//ALE
#define ALE_DDR  DDRD
#define ALE_PORT PORTD
#define ALE      5

//RDY
#define RDY_DDR  DDRD
#define RDY_PORT PORTD
#define RDY_PIN  PIND
#define RDY      6

//****************************************************************************************************
//прототипы функций
//****************************************************************************************************

void System_Init(void);//инициализация устройства
void System_SetAddr(unsigned long addr);//установить адрес на шину
void System_RESET_HI(void);//установить RESET в 1
void System_RESET_LO(void);//установить RESET в 0
void System_MEMW_HI(void);//установить MEMW в 1
void System_MEMW_LO(void);//установить MEMW в 0
void System_MEMR_HI(void);//установить MEMR в 1
void System_MEMR_LO(void);//установить MEMR в 0
void System_IOW_HI(void);//установить IOW в 1
void System_IOW_LO(void);//установить IOW в 0
void System_IOR_HI(void);//установить IOR в 1
void System_IOR_LO(void);//установить IOR в 0
void System_ALE_HI(void);//установить ALE в 1
void System_ALE_LO(void);//установить ALE в 0
void System_WaitReady(void);//ждать готовности устройства
void System_Out8(unsigned short port,unsigned char value);//записать в порт значение UInt8
void System_Out16(unsigned short port,unsigned short value);//записать в порт значение UInt16
unsigned char System_In8(unsigned short port);//считать из порта значение UInt8
unsigned short System_In16(unsigned short port);//считать из порта значение UInt16
void System_Poke8(unsigned long addr,unsigned char value);//записать в память значение UInt8
void System_Poke16(unsigned long addr,unsigned short value);//записать в память значение UInt16
unsigned char System_Peek8(unsigned long addr);//считать из памяти значение UInt8
unsigned short System_Peek16(unsigned long addr);//считать из памяти значение UInt16

//****************************************************************************************************
//реализация функций
//****************************************************************************************************


//----------------------------------------------------------------------------------------------------
//инициализация устройства
//----------------------------------------------------------------------------------------------------
void System_Init(void)
{
 //настраиваем порты
 DDRA=0;
 DDRB=0;
 DDRD=0;
 DDRC=0;

 ADDR_BUS_DDR=0xff;
 DATA_BUS_DDR=0;
 
 ADDR_BUS_0_7_SELECT_DDR|=(1<<ADDR_BUS_0_7_SELECT);
 ADDR_BUS_8_15_SELECT_DDR|=(1<<ADDR_BUS_8_15_SELECT);
 ADDR_BUS_16_19_SELECT_DDR|=(1<<ADDR_BUS_16_19_SELECT);
 
 RESET_DDR|=(1<<RESET);
 MEMW_DDR|=(1<<MEMW);
 MEMR_DDR|=(1<<MEMR);
 IOW_DDR|=(1<<IOW);
 IOR_DDR|=(1<<IOR);
 ALE_DDR|=(1<<ALE);
 RDY_DDR&=0xff^(1<<RDY);
 RDY_PORT|=1<<RDY;
 //задаём состояние портов  
 asm volatile ("sbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(ADDR_BUS_0_7_SELECT_PORT)),[bit]"M"(ADDR_BUS_0_7_SELECT));
 asm volatile ("sbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(ADDR_BUS_8_15_SELECT_PORT)),[bit]"M"(ADDR_BUS_8_15_SELECT));
 asm volatile ("sbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(ADDR_BUS_16_19_SELECT_PORT)),[bit]"M"(ADDR_BUS_16_19_SELECT));
 asm volatile ("cbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(RESET_PORT)),[bit]"M"(RESET));
 asm volatile ("sbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(IOW_PORT)),[bit]"M"(IOW));
 asm volatile ("sbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(IOR_PORT)),[bit]"M"(IOR));
 asm volatile ("sbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(MEMW_PORT)),[bit]"M"(MEMW));
 asm volatile ("sbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(MEMR_PORT)),[bit]"M"(MEMR));
 asm volatile ("cbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(ALE_PORT)),[bit]"M"(ALE));
}
//----------------------------------------------------------------------------------------------------
//установить адрес на шину
//----------------------------------------------------------------------------------------------------
void System_SetAddr(unsigned long addr)
{
 unsigned char al=addr&0xff;
 unsigned char am=(addr>>8)&0xff;
 unsigned char ah=(addr>>16)&0xff;

 System_ALE_LO();//снимаем сигнал ALE
 
 //ставим на шину адреса адрес
 ADDR_BUS_PORT=ah;
 asm volatile ("cbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(ADDR_BUS_16_19_SELECT_PORT)),[bit]"M"(ADDR_BUS_16_19_SELECT));
 asm volatile ("sbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(ADDR_BUS_16_19_SELECT_PORT)),[bit]"M"(ADDR_BUS_16_19_SELECT));

 ADDR_BUS_PORT=am;
 asm volatile ("cbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(ADDR_BUS_8_15_SELECT_PORT)),[bit]"M"(ADDR_BUS_8_15_SELECT));
 asm volatile ("sbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(ADDR_BUS_8_15_SELECT_PORT)),[bit]"M"(ADDR_BUS_8_15_SELECT));

 ADDR_BUS_PORT=al;
 asm volatile ("cbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(ADDR_BUS_0_7_SELECT_PORT)),[bit]"M"(ADDR_BUS_0_7_SELECT));
 asm volatile ("sbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(ADDR_BUS_0_7_SELECT_PORT)),[bit]"M"(ADDR_BUS_0_7_SELECT));
 
 System_ALE_HI();//ставим сигнал ALE
}
//----------------------------------------------------------------------------------------------------
//установить RESET в 1
//----------------------------------------------------------------------------------------------------
inline void System_RESET_HI(void)
{
 asm volatile ("sbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(RESET_PORT)),[bit]"M"(RESET));
}
//----------------------------------------------------------------------------------------------------
//установить RESET в 0
//----------------------------------------------------------------------------------------------------
inline void System_RESET_LO(void)
{
 asm volatile ("cbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(RESET_PORT)),[bit]"M"(RESET)); 
}
//----------------------------------------------------------------------------------------------------
//установить MEMW в 1
//----------------------------------------------------------------------------------------------------
inline void System_MEMW_HI(void)
{
 asm volatile ("sbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(MEMW_PORT)),[bit]"M"(MEMW));
}
//----------------------------------------------------------------------------------------------------
//установить MEMW в 0
//----------------------------------------------------------------------------------------------------
inline void System_MEMW_LO(void)
{
 asm volatile ("cbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(MEMW_PORT)),[bit]"M"(MEMW));
}
//----------------------------------------------------------------------------------------------------
//установить MEMR в 1
//----------------------------------------------------------------------------------------------------
inline void System_MEMR_HI(void)
{
 asm volatile ("sbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(MEMR_PORT)),[bit]"M"(MEMR));
}
//----------------------------------------------------------------------------------------------------
//установить MEMR в 0
//----------------------------------------------------------------------------------------------------
inline void System_MEMR_LO(void)
{
 asm volatile ("cbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(MEMR_PORT)),[bit]"M"(MEMR));
}
//----------------------------------------------------------------------------------------------------
//установить IOW в 1
//----------------------------------------------------------------------------------------------------
inline void System_IOW_HI(void)
{
 asm volatile ("sbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(IOW_PORT)),[bit]"M"(IOW));
}
//----------------------------------------------------------------------------------------------------
//установить IOW в 0
//----------------------------------------------------------------------------------------------------
inline void System_IOW_LO(void)
{
 asm volatile ("cbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(IOW_PORT)),[bit]"M"(IOW));
}
//----------------------------------------------------------------------------------------------------
//установить IOR в 1
//----------------------------------------------------------------------------------------------------
inline void System_IOR_HI(void)
{
 asm volatile ("sbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(IOR_PORT)),[bit]"M"(IOR));
}
//----------------------------------------------------------------------------------------------------
//установить IOR в 0
//----------------------------------------------------------------------------------------------------
inline void System_IOR_LO(void)
{
 asm volatile ("cbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(IOR_PORT)),[bit]"M"(IOR));
}
//----------------------------------------------------------------------------------------------------
//установить ALE в 1
//----------------------------------------------------------------------------------------------------
inline void System_ALE_HI(void)
{
 asm volatile ("sbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(ALE_PORT)),[bit]"M"(ALE));
}
//----------------------------------------------------------------------------------------------------
//установить ALE в 0
//----------------------------------------------------------------------------------------------------
inline void System_ALE_LO(void)
{
 asm volatile ("cbi %[port],%[bit]"::[port]"I"(_SFR_IO_ADDR(ALE_PORT)),[bit]"M"(ALE));
}
//----------------------------------------------------------------------------------------------------
//ждать готовности устройства
//----------------------------------------------------------------------------------------------------
void System_WaitReady(void)
{ 
 asm volatile ("nop"::);
 asm volatile ("nop"::);
 while(1)
 {
  if (RDY_PIN&(1<<RDY)) break;
 }
}

//----------------------------------------------------------------------------------------------------
//записать в порт значение UInt8
//----------------------------------------------------------------------------------------------------
void System_Out8(unsigned short port,unsigned char value)
{
 //ставим на шину данных данные
 DATA_BUS_DDR=0xff;
 DATA_BUS_PORT=value;
 System_SetAddr(port);
 //даём сигнал записи
 System_IOW_LO();
 System_WaitReady();//ждём готовности
 System_IOW_HI();
 DATA_BUS_DDR=0;
}
//----------------------------------------------------------------------------------------------------
//записать в порт значение UInt16
//----------------------------------------------------------------------------------------------------
void System_Out16(unsigned short port,unsigned short value)
{
 System_Out8(port,value&0xff);
 System_Out8(port+1,(value>>8)&0xff);
}
//----------------------------------------------------------------------------------------------------
//считать из порта значение UInt8
//----------------------------------------------------------------------------------------------------
unsigned char System_In8(unsigned short port)
{
 unsigned char byte;
 System_SetAddr(port);
 DATA_BUS_DDR=0;
 //даём сигнал чтения
 System_IOR_LO();
 System_WaitReady();//ждём готовности
 byte=DATA_BUS_PIN;
 System_IOR_HI();
 return(byte);
}
//----------------------------------------------------------------------------------------------------
//считать из порта значение UInt16
//----------------------------------------------------------------------------------------------------
unsigned short System_In16(unsigned short port)
{
 unsigned short ret=System_In8(port+1);
 ret<<=8;
 ret|=System_In8(port);
 return(ret);
}
//----------------------------------------------------------------------------------------------------
//записать в память значение UInt8
//----------------------------------------------------------------------------------------------------
void System_Poke8(unsigned long addr,unsigned char value)
{
 //ставим на шину данных данные
 DATA_BUS_DDR=0xff;
 DATA_BUS_PORT=value;
 System_SetAddr(addr);
 //даём сигнал записи
 System_MEMW_LO();
 System_WaitReady();//ждём готовности
 System_MEMW_HI();
 DATA_BUS_DDR=0;
}
//----------------------------------------------------------------------------------------------------
//записать в память значение UInt16
//----------------------------------------------------------------------------------------------------
void System_Poke16(unsigned long addr,unsigned short value)
{
 System_Poke8(addr,value&0xff);
 System_Poke8(addr+1,(value>>8)&0xff);
}
//----------------------------------------------------------------------------------------------------
//считать из памяти значение UInt8
//----------------------------------------------------------------------------------------------------
unsigned char System_Peek8(unsigned long addr)
{
 unsigned char byte;
 System_SetAddr(addr);
 DATA_BUS_DDR=0;
 //даём сигнал чтения
 System_MEMR_LO();
 System_WaitReady();//ждём готовности
 byte=DATA_BUS_PIN;
 System_MEMR_HI();
 return(byte);
}
//----------------------------------------------------------------------------------------------------
//считать из памяти значение UInt16
//----------------------------------------------------------------------------------------------------
unsigned short System_Peek16(unsigned long addr)
{
 unsigned short ret=System_Peek8(addr+1);
 ret<<=8;
 ret|=System_Peek8(addr);
 return(ret);
}


Инициализация видеокарты Trident 9000i выполняется вот так:

Инициализация видеокарты Trident 9000i
//****************************************************************************************************
//функции для работы с видеокартой trident
//****************************************************************************************************


//****************************************************************************************************
//макроопределения
//****************************************************************************************************



//****************************************************************************************************
//прототипы функций
//****************************************************************************************************

CHIP_TYPE TRIDENT_Init(void);//тест видеокарты как Trident
unsigned char TRIDENT_TestInx2(unsigned short rg,unsigned char ix,unsigned char msk);//возвращает true, если бит MSK регистра PT по индексу RG равен 1
void TRIDENT_TR9000i_Init(void);//инициализация микросхемы Trident9000i
void TRIDENT_TR8900D_Init(void);//инициализация микросхемы Trident8900D
bool TRIDENT_Unit_594(void);//модуль 594
void TRIDENT_Unit_5A6(void);//модуль 5A6
bool TRIDENT_Unit_4D9(void);//модуль 4D9
void TRIDENT_Unit_51A(void);//модуль 51A
unsigned char TRIDENT_Unit_26A(void);//модуль 26A
void TRIDENT_Unit_179(void);//модуль 179
void TRIDENT_Unit_2EA(void);//модуль 2EA
unsigned char TRIDENT_Unit_292(void);//модуль 292

//****************************************************************************************************
//реализация функций
//****************************************************************************************************

//----------------------------------------------------------------------------------------------------
//тест видеокарты как Trident
//----------------------------------------------------------------------------------------------------
CHIP_TYPE TRIDENT_Init(void)
{ 
 VGA_Reset();//делаем сброс видеокарты
 CHIP_TYPE chiptype=UNKNOWN;//тип микросхемы пока что неизвестен
 
 //Trident VGA (за исключением 8800BR) может работать в 2 режимах:
 //старый режим, с окном 128k, отображаемый в памяти в адресах A000h - BFFFh
 //и новый режим с окном 64k, в памяти в адресах A000h - AFFFh.
 
 System_Out8(VGAENABLE_ADDR,0x00);
 System_Out8(0x46E8,0x16);//регистр управления включением видеосистемы (D3=0)
 System_Out8(0x46E9,0x00);
 System_Out8(0x0102,0x01);
 System_Out8(0x0103,0x00);
 System_Out8(0x46E8,0x0E);//регистр управления включением видеосистемы (D3=1)
 System_Out8(0x46E9,0x00);
 System_Out8(0x4AE8,0x00);
 System_Out8(0x4AE9,0x00);
 
 VGA_OutReg8(SEQ_ADDR,0x0B,0x00);//пробуем прочесть ID микросхемы старым способом
 unsigned char chip=System_In8(0x3C5);//читаем значение новым способом
 unsigned char old=VGA_InReg8(SEQ_ADDR,0x0E);
 System_Out8(0x3C5,0x00);
 unsigned char value=System_In8(0x3C5)&0x0F;
 System_Out8(0x3C5,old);
 //определяем тип микросхемы 
 if (value==2)
 {
  System_Out8(0x3C5,old^2);
  switch(chip)
  {
   case 0x01:
    chiptype=TR8800BR;
    break;
   case 0x02:
    chiptype=TR8800CS;
    break;
   case 0x03:
    chiptype=TR8900;
    break;
   case 0x04:
    chiptype=TR8900C;
    TRIDENT_TR8900D_Init();
    break;
   case 0x13:
    chiptype=TR8900C;
    TRIDENT_TR8900D_Init();
    break;
   case 0x23:
    chiptype=TR9000;
    TRIDENT_TR9000i_Init();
    break;
   case 0x33:
    chiptype=TR8900CLD;
    TRIDENT_TR8900D_Init();
    break;
   case 0x43:
    chiptype=TR9000i;
    TRIDENT_TR9000i_Init();
    break;
   case 0x53:
    chiptype=TR8900CXr;
    break;
   case 0x63:
    chiptype=LCD9100B;
    break;
   case 0x83:
    chiptype=LX8200;
    break;
   case 0x93:
    chiptype=TVGA9400CXi;
    break;
   case 0xA3:
    chiptype=LCD9320;
    break;
   case 0x73:
    chiptype=GUI9420;
	break;
   case 0xF3:
    chiptype=GUI9420;
	break;
  }
 }
 else 
 {
  if ((chip==1)&TRIDENT_TestInx2(SEQ_ADDR,0x0E,6)) chiptype=TVGA8800BR;
 }
 return(chiptype);
}

//----------------------------------------------------------------------------------------------------
//возвращает true, если бит MSK регистра PT по индексу RG равен 1
//----------------------------------------------------------------------------------------------------
unsigned char TRIDENT_TestInx2(unsigned short rg,unsigned char ix,unsigned char msk)
{
 unsigned char old,nw1,nw2;
 old=VGA_InReg8(rg,ix);
 VGA_OutReg8(rg,ix,old&(~msk));
 nw1=VGA_InReg8(rg,ix)&msk;
 VGA_OutReg8(rg,ix,old|msk);
 nw2=VGA_InReg8(rg,ix)&msk;
 VGA_OutReg8(rg,ix,old);
 return((nw1==0)&(nw2==msk));
}

//----------------------------------------------------------------------------------------------------
//инициализация микросхемы Trident9000i
//----------------------------------------------------------------------------------------------------
void TRIDENT_TR9000i_Init(void)
{
 unsigned short i=0;
 System_Out8(VGAENABLE_ADDR,0x00);
 if (TRIDENT_Unit_4D9()==false) return;//ошибка
 do
 {
  System_Out8(0x3C9,0x00);
  i++;
 }
 while (i<768);
 System_Out8(MISC_ADDR,0x23);
 TRIDENT_Unit_51A();
}
//----------------------------------------------------------------------------------------------------
//инициализация микросхемы Trident8900D
//----------------------------------------------------------------------------------------------------
void TRIDENT_TR8900D_Init(void)
{
 if (TRIDENT_Unit_594()==true)
 {
  //задаём палитру 256 цветов 
  System_Out8(0x3C8,0x00);
  for(unsigned short n=0;n<256;n++)
  {
   System_Out8(0x3C9,n);//R
   System_Out8(0x3C9,0);//G
   System_Out8(0x3C9,0);//B
  }
  System_Out8(MISC_ADDR,0x23);
  TRIDENT_Unit_5A6();
 }
 if (TRIDENT_Unit_594()==false)
 {
  System_In8(STATUS_ADDR);
  System_Out8(ATTRCON_ADDR,0x20);
  System_In8(STATUS_ADDR);
  VGA_OutReg8(CRTC_ADDR,0x020,VGA_InReg8(CRTC_ADDR,0x20)&0xDF);
 }
 System_Out8(0x3D8,0x00);
 VGA_OutReg8(CRTC_ADDR,0x23,0x10);
 System_Out8(0x3C6,0xff);
}
//----------------------------------------------------------------------------------------------------
//модуль 594
//----------------------------------------------------------------------------------------------------
bool TRIDENT_Unit_594(void)
{
 VGA_OutReg8(SEQ_ADDR,0x0B,0x00);//установка старого режима
 if ((VGA_InReg8(SEQ_ADDR,0x0D)&0x0E)!=0x0C) return(true);
 if ((System_In8(0x3CC)&0x67)!=0x67) return(true);
 return(false);
}
//----------------------------------------------------------------------------------------------------
//модуль 5A6
//----------------------------------------------------------------------------------------------------
void TRIDENT_Unit_5A6(void)
{
 unsigned char al,bh;

 VGA_InReg8(SEQ_ADDR,0x0B);//установка нового режима
 VGA_OutReg8(SEQ_ADDR,0x0E,(VGA_InReg8(SEQ_ADDR,0x0E)|0x80)^2);
 bh=(VGA_InReg8(SEQ_ADDR,0x0C)&0xFE)|0x80;
 if (VGA_InReg8(SEQ_ADDR,0x0B)==0x53)//установка нового режима
 {
  VGA_InReg8(CRTC_ADDR,0x29);
  VGA_OutReg8(CRTC_ADDR,0x29,0x44);
  VGA_OutReg8(CRTC_ADDR,0x2B,0x03);
  VGA_OutReg8(CRTC_ADDR,0x2C,0x3D);
  VGA_OutReg8(CRTC_ADDR,0x25,0x27);
 }
 if (!(!VGA_InReg8(CRTC_ADDR,0x28)&1))
 {
  bh&=0xCE;
  bh|=0x80;
  VGA_OutReg8(MISC_ADDR,0x01,0x00);//выбираем адрес 0x3D2 (в оригинале 0x01)
  al=(VGA_InReg8(CRTC_ADDR,0x28))&0x0C;
  if (al==0)
  {
   al|=0x04;
   VGA_OutReg8(CRTC_ADDR,0x28,al);
  }
  VGA_OutReg8(SEQ_ADDR,0x0F,VGA_InReg8(SEQ_ADDR,0x0F)&0x7F);
  VGA_InReg8(SEQ_ADDR,0x0C);
  VGA_OutReg8(SEQ_ADDR,0x0C,bh);
  VGA_OutReg8(SEQ_ADDR,0x0E,(VGA_InReg8(SEQ_ADDR,0x0E)&0x7F)^2);
  if (VGA_InReg8(SEQ_ADDR,0x0F)&0x08)
  {
   VGA_InReg8(SEQ_ADDR,0x0B);//установка нового режима
   VGA_OutReg8(SEQ_ADDR,0x0E,(VGA_InReg8(SEQ_ADDR,0x0E)|0x80)^2);
   al=(VGA_InReg8(SEQ_ADDR,0x0C)&0xFE)|0x80;
   al&=0xFE;
   VGA_OutReg8(SEQ_ADDR,0x0C,al);
   VGA_OutReg8(GRACON_ADDR,0x0F,0x00);
   al=VGA_InReg8(SEQ_ADDR,0x0C);//отключаем тестирование
   VGA_OutReg8(SEQ_ADDR,0x0F,VGA_InReg8(SEQ_ADDR,0x0F)|0x80);
   VGA_OutReg8(SEQ_ADDR,0x0E,(VGA_InReg8(SEQ_ADDR,0x0E)&0x7F)^0x02);   
  }
 } 
 VGA_OutReg8(SEQ_ADDR,0x0B,0x00);//старый режим
 VGA_OutReg8(SEQ_ADDR,0x0D,0x20);
 VGA_OutReg8(SEQ_ADDR,0x0E,0xA0);
 VGA_InReg8(SEQ_ADDR,0x0B);//установка нового режима
 VGA_OutReg8(SEQ_ADDR,0x0E,0x02);
 al=VGA_InReg8(GRACON_ADDR,0x06);//atvizsgбlni
 if (al==0)
 {
  al&=0xF3;
  al|=0x04;
  VGA_OutReg8(GRACON_ADDR,0x06,al);
 }
 VGA_OutReg8(SEQ_ADDR,0x0D,0x00);
 VGA_InReg8(CRTC_ADDR,0x1E);
 VGA_OutReg8(CRTC_ADDR,0x1E,0x00);
 if (VGA_InReg8(SEQ_ADDR,0x0B)==0x53) VGA_OutReg8(CRTC_ADDR,0x20,0x1D);//установка нового режима
                                  else VGA_OutReg8(CRTC_ADDR,0x20,0x1C);
 VGA_OutReg8(CRTC_ADDR,0x29,0x44);  
}

//----------------------------------------------------------------------------------------------------
//модуль 4D9
//----------------------------------------------------------------------------------------------------
bool TRIDENT_Unit_4D9(void)
{
 unsigned char al;
 al=TRIDENT_Unit_292()&0x0E;
 if (al!=0x0C) return(true);
 al=System_In8(0x3CC)&0x67;
 if (al!=0x67) return(true);
 return(false);
}
//----------------------------------------------------------------------------------------------------
//модуль 51A
//----------------------------------------------------------------------------------------------------
void TRIDENT_Unit_51A(void)
{
 unsigned char al,bh;
 bh=(TRIDENT_Unit_26A()|0x80)&0xFE;
 VGA_OutReg8(SEQ_ADDR,0x07,0x24);
 System_Out8(MISC_ADDR,0x01);
 if (!((al=VGA_InReg8(CRTC_ADDR,0x28))&0x0C))
 {
  al|=0x04;
  VGA_OutReg8(CRTC_ADDR,0x28,al);
 }
 VGA_OutReg8(SEQ_ADDR,0x0F,VGA_InReg8(SEQ_ADDR,0x0F)&0x7F);
 VGA_OutReg8(SEQ_ADDR,0x0C,bh);
 VGA_OutReg8(SEQ_ADDR,0x0E,(VGA_InReg8(SEQ_ADDR,0x0E)&0x7F)^2);
 if (VGA_InReg8(SEQ_ADDR,0x0F)&0x08) TRIDENT_Unit_179();
 TRIDENT_Unit_2EA();//старый режим
 VGA_OutReg8(SEQ_ADDR,0x0D,0x20);
 VGA_OutReg8(SEQ_ADDR,0x0E,0xA0);
 VGA_InReg8(SEQ_ADDR,0x0B);//новый режим
 VGA_OutReg8(SEQ_ADDR,0x0E,0x02);
 if (!((al=VGA_InReg8(GRACON_ADDR,0x06))&0x0C)) VGA_OutReg8(GRACON_ADDR,0x06,(al&0xF3)|0x04);
 VGA_OutReg8(SEQ_ADDR,0x0D,0x00);
 al=VGA_InReg8(CRTC_ADDR,0x1E);
 VGA_OutReg8(CRTC_ADDR,0x1E,0x00);
}
//----------------------------------------------------------------------------------------------------
//модуль 26A
//----------------------------------------------------------------------------------------------------
unsigned char TRIDENT_Unit_26A(void)
{
 VGA_InReg8(SEQ_ADDR,0x0B);//установка нового режима
 VGA_OutReg8(SEQ_ADDR,0x0E,(VGA_InReg8(SEQ_ADDR,0x0E)|0x80)^2);
 return(VGA_InReg8(SEQ_ADDR,0x0C));
}
//----------------------------------------------------------------------------------------------------
//модуль 179
//----------------------------------------------------------------------------------------------------
void TRIDENT_Unit_179(void)
{
 //настрйоки SP и BP
 VGA_OutReg8(SEQ_ADDR,0x0C,(TRIDENT_Unit_26A()|0x42)&0xFE);
 VGA_OutReg8(SEQ_ADDR,0x0F,VGA_InReg8(SEQ_ADDR,0x0F)|0x80);
 VGA_OutReg8(SEQ_ADDR,0x0E,(VGA_InReg8(SEQ_ADDR,0x0E)&0x7F)^2);
}
//----------------------------------------------------------------------------------------------------
//модуль 2EA
//----------------------------------------------------------------------------------------------------
void TRIDENT_Unit_2EA(void)
{
 VGA_OutReg8(SEQ_ADDR,0x0B,0x00);//установка старого режима
}
//----------------------------------------------------------------------------------------------------
//модуль 292
//----------------------------------------------------------------------------------------------------
unsigned char TRIDENT_Unit_292(void)
{
 TRIDENT_Unit_2EA();//установка старого режима
 return(VGA_InReg8(SEQ_ADDR,0x0D));//старый режим, читаем SEQ_ADDR 0x0D
}


Также я запускал и видеокарту OAK OTI077 (пока я случайно не подал на неё 12 В и она не сгорела):

Инициализация видеокарты OAK OTI077
//****************************************************************************************************
//функции для работы с видеокартой oak
//****************************************************************************************************


//****************************************************************************************************
//макроопределения
//****************************************************************************************************


//****************************************************************************************************
//глобальные переменные
//****************************************************************************************************

unsigned char OAK_InitData[77] PROGMEM=
{
 0x01,0x29,0x04,0x10,0x07,0x00,0x01,0x3E,
 0x00,0x00,0x80,0x70,0xFF,0x01,0xFF,0xFF,
 0x7F,0x00,0xFF,0xC8,0x00,0x00,0xFF,0x00,
 0x01,0x0F,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
 0x01,0x00,0x08,0x00,0x00,0x00,0xFF,0xFF,
 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
 0x00,0x00,0x00,0x00,0x00,0x00,0xD8,0xFF,
 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
 0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x00,
 0x00,0x00,0x00,0x00,0x10
};


//****************************************************************************************************
//прототипы функций
//****************************************************************************************************

CHIP_TYPE OAK_Init(void);//тест видеокарты как Trident
unsigned char OAK_TestInx2(unsigned short rg,unsigned char ix,unsigned char msk);//возвращает true, если бит MSK регистра PT по индексу RG равен 1
void OAK_OTI077_Init(void);//инициализация OAK077
void OAK_OTI087_Init(void);//инициализация OAK087
void OAK_Unit218C(void);//модуль 218C
void OAK_Unit201B(void);//модуль 201B
void OAK_Unit50F(void);//модуль 50F
void OAK_Unit58F(unsigned char ah,unsigned char al);//модуль 58F
void OAK_Unit552(void);//модуль 552
unsigned char OAK_Unit53D(void);//модуль 53D
void OAK_Unit21A8(void);//модуль 21A8
void OAK_Fill3DE(void);//модуль Fill3DE
bool OAK_Unit21C9(void);//модуль 21C9
void OAK_Unit2167(unsigned short addr,unsigned char v);//модуль 2167
void OAK_Unit2148(unsigned char v);//модуль 2148
void OAK_Unit2A0F(void);//модуль 2A0F
void OAK_Unit28E2(unsigned char al,unsigned char ah);//модуль 28E2
void OAK_Unit28AC(unsigned char al,unsigned char ah);//модуль 28AC
void OAK_Unit29E0(void);//модуль 29E0
unsigned char OAK_Unit1CED(void);//модуль 1CED
void OAK_Unit1E12(void);//модуль 1E12
void OAK_Unit292D(unsigned char al,unsigned char bl,unsigned char bh);//модуль 292D

//****************************************************************************************************
//реализация функций
//****************************************************************************************************

//----------------------------------------------------------------------------------------------------
//тест видеокарты как Trident
//----------------------------------------------------------------------------------------------------
CHIP_TYPE OAK_Init(void)
{ 
 CHIP_TYPE chiptype=UNKNOWN;
 VGA_Reset();
 
 System_Out8(0x46E8,0x17);
 System_Out8(0x0102,System_In8(0x0102)|1);
 System_Out8(0x46E8,0x0F);
 if (OAK_TestInx2(0x3DE,0x0D,0x38))
 {
  if (OAK_TestInx2(0x3DE,0x23,0x1F))
  {
   if((VGA_InReg8(0x3DE,0x00)&0x02)==0)
   {
    chiptype=OAK_087;
    OAK_OTI087_Init();
   }
   else chiptype=OAK_083;
  }
  else
  {
   switch(System_In8(0x3DE)/32)
   {
    case 0:
     chiptype=OAK_037C;
     break;
    case 2:
     chiptype=OAK_067;
     break;
    case 5:
     chiptype=OAK_077;
     OAK_OTI077_Init();
     break;
    case 7:
     chiptype=OAK_057;
	 break;
   }
  }
 }
 return(chiptype);
}


//----------------------------------------------------------------------------------------------------
//возвращает true, если бит MSK регистра PT по индексу RG равен 1
//----------------------------------------------------------------------------------------------------
unsigned char OAK_TestInx2(unsigned short rg,unsigned char ix,unsigned char msk)
{
 unsigned char old,nw1,nw2;
 old=VGA_InReg8(rg,ix);
 VGA_OutReg8(rg,ix,old&(~msk));
 nw1=VGA_InReg8(rg,ix)&msk;
 VGA_OutReg8(rg,ix,old|msk);
 nw2=VGA_InReg8(rg,ix)&msk;
 VGA_OutReg8(rg,ix,old<<8);
 return((nw1==0)&(nw2==msk));
}
//----------------------------------------------------------------------------------------------------
//инициализация OAK087
//----------------------------------------------------------------------------------------------------
void OAK_OTI087_Init(void)
{
 System_Out8(0x320,1);
 System_In8(0x320);
 OAK_Unit218C();
 VGA_OutReg8(0x3DE,0x13,VGA_InReg8(0x3DE,0x13)&0xBF);
 VGA_OutReg8(0x3DE,0x0B,((VGA_InReg8(0x3DE,0x0B)&0x0F)|(VGA_InReg8(0x3DE,0x10)))&0xF0);
 OAK_Unit201B();
 OAK_Unit50F();
 OAK_Unit552();
 System_Out8(0x320,2);
 OAK_Fill3DE(); 
 OAK_Unit21A8();
 OAK_Unit218C();
 System_Out8(0x320,0x0c);
 System_Out8(0x46E8,0x17);
 System_Out8(0x0102,System_In8(0x0102)|1);
 System_Out8(0x46E8,0x0F);
 OAK_Fill3DE();
}
//----------------------------------------------------------------------------------------------------
//инициализация OAK077
//----------------------------------------------------------------------------------------------------
void OAK_OTI077_Init(void)
{
 OAK_Unit2A0F();
 VGA_OutReg8(0x3DE,0x0E,VGA_InReg8(0x3DE,0x0E)&0xDE);
 OAK_Unit1E12();
 System_Out8(0x46E8,0x17);
 System_Out8(0x0102,System_In8(0x0102)|1);
 System_Out8(0x46E8,0x0F);
}
//----------------------------------------------------------------------------------------------------
//модуль 218C
//----------------------------------------------------------------------------------------------------
void OAK_Unit218C(void)
{
 OAK_Unit2148(0x0F);
 if (OAK_Unit21C9()==false) return;
 OAK_Unit2167(0x0094,0x01);
 if (OAK_Unit21C9()==false) return;
 OAK_Unit2167(0x0394,0x01);
 return;
}
//----------------------------------------------------------------------------------------------------
//модуль 201B
//----------------------------------------------------------------------------------------------------
void OAK_Unit201B(void)
{
 unsigned char ah;
 System_In8(0x3C8);
 ah=System_In8(0x3C6);
 System_In8(0x3C8);
 System_In8(0x3C6);
 System_In8(0x3C6);
 System_In8(0x3C6);
 System_In8(0x3C6);
 System_Out8(0x3C6,0);
 System_In8(0x3C8);
 System_Out8(0x3C6,ah);
 System_In8(0x3C8);
}
//----------------------------------------------------------------------------------------------------
//модуль 50F
//----------------------------------------------------------------------------------------------------
void OAK_Unit50F(void)
{
 if ((VGA_InReg8(0x3DE,0x07)&0x04)) return;
 OAK_Unit58F(0xFF,0x05);
}
//----------------------------------------------------------------------------------------------------
//модуль 58F
//----------------------------------------------------------------------------------------------------
void OAK_Unit58F(unsigned char ah,unsigned char al)
{
 System_Out8(0x1E,al);
 System_Out8(0x1F,ah);
}
//----------------------------------------------------------------------------------------------------
//модуль 552
//----------------------------------------------------------------------------------------------------
void OAK_Unit552(void)
{
 if (OAK_Unit53D()) return;
 OAK_Unit58F(0x07,0x04);
 OAK_Unit58F(0xFF,0x05);
 OAK_Unit58F((VGA_InReg8(0x1E,0xFE)|0x40),0xFE);
}
//----------------------------------------------------------------------------------------------------
//модуль 53D
//----------------------------------------------------------------------------------------------------
unsigned char OAK_Unit53D(void)
{
 if (VGA_InReg8(0x3DE,0x07)&0x04) return(1);
 return(VGA_InReg8(0x3DE,0x08)&0x03);
}
//----------------------------------------------------------------------------------------------------
//модуль 21A8
//----------------------------------------------------------------------------------------------------
void OAK_Unit21A8(void)
{
 if (VGA_InReg8(0x3DE,0x07)&0x04) OAK_Unit2148(0x00);
 else
 {
  OAK_Unit2167(0x0094,0x00);
  OAK_Unit2167(0x0394,0x00);
 }
}
//----------------------------------------------------------------------------------------------------
//модуль Fill3DE
//----------------------------------------------------------------------------------------------------
void OAK_Fill3DE(void)
{
 unsigned char i;
 for(i=0;i<77;i++)
 {
  unsigned short byte=pgm_read_byte(&(OAK_InitData[i]));
  VGA_OutReg8(0x3DE,i,byte);
 }
}
//----------------------------------------------------------------------------------------------------
//модуль 21C9
//----------------------------------------------------------------------------------------------------
bool OAK_Unit21C9(void)
{
 unsigned char al=0x55,ah=al;
 while(1)
 {
  System_Out8(0x3DE,al);
  if (al==ah) return(true);
  al^=0xFF;
  ah^=0xFF;
  if (al==0x55 && ah==0x55) return(false);
 }
}
//----------------------------------------------------------------------------------------------------
//модуль 2167
//----------------------------------------------------------------------------------------------------
void OAK_Unit2167(unsigned short addr,unsigned char v)
{
 unsigned char ah;
 ah=System_In8(addr);
 System_Out8(addr,0x80);
 System_Out8(0x102,0x01);
 System_Out8(addr,ah);
 System_Out8(0x3C3,v);
}
//----------------------------------------------------------------------------------------------------
//модуль 2148
//----------------------------------------------------------------------------------------------------
void OAK_Unit2148(unsigned char v)
{
 System_Out8(0x46E8,0x17);
 System_Out8(0x0102,System_In8(0x0102)|1);
 System_Out8(0x46E8,v);
}
//----------------------------------------------------------------------------------------------------
//модуль 2A0F
//----------------------------------------------------------------------------------------------------
void OAK_Unit2A0F(void)
{
 OAK_Unit28E2(0,8);
 OAK_Unit28AC(0,0xBE);
 OAK_Unit29E0();
 OAK_Unit292D(0,0x11,0x06);
 OAK_Unit292D(1,0x11,0x07);
 OAK_Unit292D(2,0x06,0x1F);
 OAK_Unit292D(3,0x06,0x15);
}
//----------------------------------------------------------------------------------------------------
//модуль 28E2
//----------------------------------------------------------------------------------------------------
void OAK_Unit28E2(unsigned char al,unsigned char ah)
{
 unsigned char bl=al,bh=ah;
 VGA_OutReg8(0x3DE,0x0D,VGA_InReg8(0x3DE,0x0D)|0x20);
 System_Out8(0x3C7,0x0E);
 al=System_In8(0x3C9)&bh;
 ah^=0xFF;
 bl&=ah;
 bl|=al;
 System_Out8(0x3C8,0x0E);
 System_Out8(0x3C9,bl);
 VGA_OutReg8(0x3DE,0x0D,VGA_InReg8(0x3DE,0x0D)&0xDF);
}
//----------------------------------------------------------------------------------------------------
//модуль 28AC
//----------------------------------------------------------------------------------------------------
void OAK_Unit28AC(unsigned char al,unsigned char ah)
{
 unsigned char bl=al;
 VGA_OutReg8(0x3DE,0x0D,VGA_InReg8(0x3DE,0x0D)|0x20);
 al=System_In8(0x3C6)&ah;
 ah^=0xFF;
 bl&=ah;
 al|=bl;
 System_Out8(0x3C6,al);
 VGA_OutReg8(0x3DE,0x0D,VGA_InReg8(0x3DE,0x0D)&0xDF);
}
//----------------------------------------------------------------------------------------------------
//модуль 29E0
//----------------------------------------------------------------------------------------------------
void OAK_Unit29E0(void)
{
 if ((System_In8(0x3DE)&0xE0)==0xA0)
 {
  if (!(OAK_Unit1CED()&0x20)) OAK_Unit292D(0x0A,0x02,0x0C);
                          else OAK_Unit292D(0x0A,0x03,0x0D);
 }
 else OAK_Unit292D(0x0A,0x03,0x0D);
}
//----------------------------------------------------------------------------------------------------
//модуль 1CED
//----------------------------------------------------------------------------------------------------
unsigned char OAK_Unit1CED(void)
{
 return((VGA_InReg8(0x3DE,0x10)&0x4F)|(VGA_InReg8(0x3DE,0x0B)&0xB0));
}
//----------------------------------------------------------------------------------------------------
//модуль 1E12
//----------------------------------------------------------------------------------------------------
void OAK_Unit1E12(void)
{
 System_Out8(0x46E8,0x17);
 System_Out8(0x0102,System_In8(0x0102)|1);
 System_Out8(0x46E8,0x00);
}
//----------------------------------------------------------------------------------------------------
//модуль 292D
//----------------------------------------------------------------------------------------------------
void OAK_Unit292D(unsigned char al,unsigned char bl,unsigned char bh)
{
 VGA_OutReg8(0x3DE,0x0D,VGA_InReg8(0x3DE,0x0D)|0x20);
 System_Out8(0x3C8,al);
 System_Out8(0x3C9,bh);
 System_Out8(0x3C9,bl);
 VGA_OutReg8(0x3DE,0x0D,VGA_InReg8(0x3DE,0x0D)&0xDF);
}


Кстати, а нет ли тут специалистов по регистрам адаптера VGA? Я вижу в коде смены видеорежима при инициализации видеокарты странные вещи:

//регистры контроллера атрибутов
#define ATTRCON_ADDR 0x03C0

System_In8(0x03DA);
System_Out8(ATTRCON_ADDR,0x20);

Тут в общем-то, ничего особого нет. Запись в регистры контроллера атрибутов делается за 2 шага: записываем сперва номер регистра, а затем данные. Чтобы всегда начинать с записи номера, читаем ISR1 (из 0x03DA) — так сделано.

Но вот что странно. У контроллера атрибутов нет регистра 0x20! У него регистр последний 0x14. И даже если такой регистр бы был, почему нет записи значения? Должно же быть две записи в порт. А тут она одна. Я поискал в интернете и нашёл, что почему-то (в книгах я этого не нашёл) можно записать, скажем, в регистр 0x10 значение 0x20 за один раз просто объединив биты: System_Out8(ATTRCON_ADDR,0x10|0x20); Тогда указанная запись пишет 0x20 в регистр 0x00? Но почему это работает? И так ли это? Мне это интересно вот почему — всё дело в том, что иногда отваливается цвет после инициализации. Палитру просто не задать. Видно, что она меняется, но цвета вовсе не те, что должны быть. Если инициализацию сделать повторно, то всё восстанавливается. На каком этапе это происходит — не понятно. Экспериментально я выяснил, что с большой вероятностью, это как раз установка видеорежима. Но вот что там именно не то, я не понимаю.

Ссылка на архив с печатной платой
Ссылка на архив с прошивкой
Теги:
Хабы:
Всего голосов 76: ↑76 и ↓0+76
Комментарии58

Публикации

Истории

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань