§ Баг верилятора
Сегодня моя задача состоит в том, чтобы что-то запустить, чтобы процессор уже как-то начал работать, в том числе, и в реальной отладочной плате. Пора запускать!В процессе работы над этим материалом обнаружился очень неприятный баг в вериляторе, который надо было учесть. Когда я отлаживал код, то понял, что надо переписать функцию tick25().
1// Один такт 25 Mhz 2void tick25() { 3 4 // Если есть запись, записать, и потом прочесть новое значение 5 if (cpu_mod->we) memory[ cpu_mod->address & 0xFFFFF ] = cpu_mod->o_data; 6 cpu_mod->i_data = memory[ cpu_mod->address & 0xFFFFF ]; 7 8 ... 9 10 // Сначала ставится 0 для всех 11 ps2_mod->clock = 0; ps2_mod->eval(); 12 vga_mod->clock = 0; vga_mod->eval(); 13 cpu_mod->clock = 0; cpu_mod->eval(); 14 15 // Потом ставится 1 16 ps2_mod->clock = 1; ps2_mod->eval(); 17 vga_mod->clock = 1; vga_mod->eval(); 18 cpu_mod->clock = 1; cpu_mod->eval(); 19 20 ... 21 22 vga(vga_mod->hs, vga_mod->vs, cl); 23}В чем суть? Оказывается, верилятор как-то неправильно считает такты, и когда происходит исполнение такта при clock=0, то защелкивается i_data, а потом никак не меняется при защелкивании на 1, так что я поменял код, что данные начинают извлекаться до того, как будет исполнен "тик-так".
На самом деле, для целей отладки я добавил в код дизассемблер, но приводить в статье его не буду, потому что это займет огромную "простыню" кода, так что этого делать не стоит. Если что, в коде, который прикреплен к статье, это все есть.
Еще важная вещь, о которой я совершенно забыл, так это выставление 1 на вход cpu:
1cpu_mod->reset_n = 1; 2cpu_mod->locked = 1;
§ Добавление новых инструкций
Сегодня я добавлю только те инструкции, которые потребуются, чтобы запустить простую программу. Список новых инструкции:- Условные переходы на короткие (short) ссылки из одного байта
- Короткий переход
- Дальний переход
- АЛУ с непосредственным операндом
- Перемещение (MOV) из регистра в память, регистра в регистр, из памяти в регистр
- Перемещение в сегментные и из сегментных регистров
- MOV в регистр из непосредственного значения
Некоторые из этих инструкции требуют считывания байта modrm, потому часть из них пойдет в
t <= tmodrm
вместо exec
. К таким относятся инструкции АЛУ и перемещения данных.1// Предвычисление modrm 28'b00_xxx_0xx, 3// MOV rmr 48'b10_001_0xx: t <= tmodrm; 5// ALU rm,i 68'b1000_00xx: begin t <= tmodrm; dir <= 1'b0; end 7// MOV sreg, r16 88'b1000_11x0: begin t <= tmodrm; size <= 2'b1; end 9// HLT 108'b1111_0100: begin t <= init; ip <= ip; endКстати интересной особенностью для некоторых инструкции является то, что в них есть отход от определенного порядка, например, dir = 0 для АЛУ, где левый операнд всегда будет r/m, а правый - непосредственным значением, либо же при перемещении между сегментными префиксами размер всегда 16 бит.
Здесь есть также инструкция HLT, которая делает, фактически, ничего — то есть оставляет ip где был с помощью
ip <= ip
, и переходит к стадии инициализации. Уже там, в той стадии, будет решена судьба дальнейшего исполнения. А обычно это делает любое пришедшее прерывание. Прерываний в процессоре пока что нет, так что эта инструкция будет полностью завешивать процессор и ему может помочь только HARD RESET (как говорят в народе, семь бед — один ресет).§ Условный переход
Начну с одной из самых важных инструкции, это условный переход, который происходит при некоторых конфигурациях флагов.70 JO | 71 JNO -- переход, если O=1 (JO) или если O=0 (JNO) 72 JB | 73 JNB -- CF=1 74 JZ | 75 JNZ -- ZF=1 76 JBE | 77 JA -- CF=1 OR ZF=1 78 JS | 79 JNS -- SF=1 7A JP | 7B JNP -- PF=1 7C JL | 7D JNL -- (SF != OF) 7E JLE | 7F JG -- (SF != OF) || ZF=1К примеру, при JL переход будет выполнен в том случае, если флаг SF не равен флагу OF (то есть, если SF ^ OF = 1). При JNL переход выполнится в обратном случае, то есть когда SF=OF, или SF ^ OF = 0.
Получается, что условий перехода всего лишь 8, остальные 8 - это лишь инверсия условия. Чтобы выбирать необходимые условия, я сформирую провод битностью 8:
1wire [7:0] branches = { 2 3 /*7*/ (flags[SF] ^ flags[OF]) | flags[ZF], 4 /*6*/ (flags[SF] ^ flags[OF]), 5 /*5*/ flags[PF], 6 /*4*/ flags[SF], 7 /*3*/ flags[CF] | flags[ZF], 8 /*2*/ flags[ZF], 9 /*1*/ flags[CF], 10 /*0*/ flags[OF] 11};Этот провод выступает в качестве всех возможных состоянии мультиплексора на 3 бита. А вот и реализация уже непосредственно в фазе exec:
18'b0111_xxxx: begin 2 3 t <= init; 4 if (branches[ opcode[3:1] ] ^ opcode[0]) 5 ip <= ip + 1'b1 + signex; 6 else 7 ip <= ip + 1'b1; 8endЧтобы с ходу не запутаться:
1wire [15:0] signex = {{8{i_data[7]}}, i_data[7:0]};Поскольку signex — это знакорасширенный i_data с 8 до 16 бит. Знакорасширение является копированием старшего бита в старший байт. Если в старшем бите младшего байта 1, то весь старший байт будет целиком из единиц (или FFh).
Инструкции $70-$7F работают так:
- Номер условия получается из битов [3:1] опкода
- По номеру условия (это 3 бита) проверяется указанный бит в проводе branches
- Если там 1, то условие выполняется, но при это opcode[0]=0; если же opcode[0]=1, то условие выполняется, если получен 0
А вот безусловный переход работает так же как и условный с коротким операндом в 1 байт, просто никакие условия не проверяются:
18'b1110_1011: begin t <= init; ip <= ip + 1'b1 + signex; endСразу переходя к метке.
§ АЛУ с непосредственным операндом
Исполнение инструкции уже многоступенчатое, потому применяется m:18'b1000_00xx: case (m) 2 3 // 8 bit 4 0: begin 5 6 m <= opcode[1:0] == 2'b11 ? 2 : (size ? 1 : 2); 7 op2 <= opcode[1:0] == 2'b11 ? signex : i_data; 8 alu <= modrm[5:3]; 9 ip <= ip + 1; 10 11 end 12 13 // 16 bit 14 1: begin m <= 2; op2[15:8] <= i_data; ip <= ip + 1; end 15 16 // Запись результата 17 2: begin 18 19 flags <= alu_f; 20 t <= alu == alu_cmp ? init : wback; 21 wb <= alu_r; 22 23 end 24 25endcaseЭта инструкция имеет довольно-таки интересное исполнение.
-
80 <ALU> rm, i8
-
81 <ALU> rm, i16
-
82 <ALU> rm, i8
(дубликат 80) -
83 <ALU> rm16, i8
Такт m=0:
- Во второй операнд записывается лишь 1 байт, либо знакорасширенный байт.
- Если знакорасширение, то переходит к m=2, иначе либо к m=1, если size=1, либо m=2 при указанном 8-битном значении
- Записывается номер функции АЛУ, полученный из reg-части байта modrm, так что здесь эта часть как раз используется не как выбор регистра в качестве операнда, а как выбор функции АЛУ
Такт m=2: записывает флаги результата, и, если АЛУ не CMP, то записывает в регистр или память полученный результат
Так что функция кажется и сложной, но на самом деле все просто. Единственная ее проблема в том, что все эти инструкции кушают много тактов.
§ Перемещения
А вот реализация инструкции перемещения данных из регистра в память и т.д. довольно просто:18'b1000_10xx: begin t <= wback; wb <= op2; endТо есть просто пишется правый операнд в левый и все. Не составляет труда понять схему работу перемещения из сегмента в регистр или память:
18'b1000_1100: begin 2 3 t <= wback; 4 case (modrm[5:3]) 5 3'h0: wb <= es; 3'h1: wb <= cs; 6 3'h2: wb <= ss; 3'h3: wb <= ds; 7 endcase 8 9endИ наоборот, из памяти/регистра в сегментный регистр:
1// MOV sreg, r16 28'b1000_1110: begin 3 4 t <= init; 5 case (modrm[5:3]) 6 3'h0: es <= op2; 7// 3'h1: cs <= op2; -- можно только на 8088 8 3'h2: ss <= op2; 9 3'h3: ds <= op2; 10 endcase 11 12endВ первом случае результат уходит в регистр wb и далее происходит процедура обратной записи wback, а во втором случае правый операнд записывается в допустимые регистры es, ss, ds. А вот cs нельзя! Хотя... на 8088 как раз и можно было, но лучше уж запретить.
Теперь идет блок с реализацией достаточно часто используемых инструкции перемещения непосредственного значения в один из 8 регистров:
1// MOV r,i 28'b1011_xxxx: case (m) 3 4 // Byte 5 0: begin 6 7 t <= opcode[3] ? exec : wback; 8 m <= 1; 9 dir <= 1'b1; 10 wb <= i_data; 11 size <= opcode[3]; 12 ip <= ip + 1'b1; 13 modrm[5:3] <= opcode[2:0]; 14 15 end 16 17 // Word 18 1: begin 19 20 t <= wback; 21 ip <= ip + 1'b1; 22 wb <= {i_data, wb[7:0]}; 23 24 end 25 26endcaseПри получении первого такта, на всякий случай записываются все необходимые регистры:
- m=1, чтобы перейти на этот этап, но происходит это лишь при t=exec, который появляется лишь при size=1 (то есть, чтение 16 бит)
- dir=1 и modrm[5:3] - номер регистра, куда будет записан результат
- Обязательно указываем size, поскольку он формируется нестандартным способом, находясь в 3-м бите опкода
- Ну и записывая в wb полученный младший байт непосредственного значения
§ Дальний переход
Ну и последнее, что на сегодня я рассмотрю по поводу инструкции, это будет важная инструкция дальнего перехода, которая исполняется при старте процессора:18'b1110_1010: case (m) 2 3 0: begin ip <= ip + 1; m <= 1; wb[ 7:0] <= i_data; end 4 1: begin ip <= ip + 1; m <= 2; wb[15:8] <= i_data; end 5 2: begin ip <= ip + 1; m <= 3; segment[7:0] <= i_data; end 6 3: begin {cs, ip} <= {i_data, segment[7:0], wb}; t <= init; end 7 8endcaseЭта инструкция последовательно забирает 4 байта и после их приема отсылает на новый CS:IP.
§ Небольшая программа
Приведу текст программы на ассемблере, которая будет способна на выполнение простых действий - очистки экрана и пропечатывание надписи "Hello World":1 ; Установка сегментов 2 mov ax, $b800 3 mov es, ax 4 mov ax, cs 5 mov ds, ax 6 7 ; Очистка экрана 8 mov ax, $17F9 9 mov bx, $0000 10 mov cx, 2000 11@@: mov [es:bx], ax 12 add bx, 2 13 sub cx, 1 14 jne @b 15 16 ; Надпись 17 mov si, master 18 mov bx, 2*80+4 19@@: mov al, [cs:si] 20 cmp al, 0 21 je $ 22 add si, 1 23 mov [es:bx], ax 24 add bx, 2 25 jmp @b 26 27master: db "Hello World!", 0Результатом исполнения программы будет следующее:
§ Синтез на ПЛИС
После проверки на вериляторе, всегда хочется проверить и то, как это заработает на ПЛИС, потому, после того, как создана схема из шаблона, добавляю блоки памяти:1wire [7:0] i_data_bios; 2 3bios BiosMemory 4( 5 .clock (clock_100), 6 .address_a (address[15:0]), 7 .q_a (i_data_bios), 8 .data_a (o_data), 9 .wren_a (we & we_bios) 10);Здесь будет храниться 64 Кб BIOS, к пинам на выход подключено на провод i_data_bios. Для памяти видеоадаптера будет так:
1wire [ 7:0] i_data_vga; 2wire [12:0] address_vga; 3 4font UnitFont 5( 6 .clock (clock_100), 7 .address_a (address_vga), 8 .q_a (data), 9 .address_b (address[12:0]), 10 .q_b (i_data_vga), 11 .data_b (o_data), 12 .wren_b (we & we_vga), 13);Эта память двухпортовая, в блоке памяти может читаться и писаться одновременно как из модуля видеадаптера (выбирается через адрес address_vga), так и из процессора address[12:0] и i_data_vga.
В обоих этих модулях есть запись в память через o_data, we и разрешающего запись провода we_bios и we_vga.
1wire we_bios = address >= 20'hF0000; 2wire we_vga = address >= 20'hB8000 && address < 20'hBA000;На них будет 1 или 0 в зависимости от того, куда указывает сейчас адрес. Соответственно, в общее i_data будет выбрано соответственно:
1wire [ 7:0] i_data = 2 we_bios ? i_data_bios : 3 we_vga ? i_data_vga : 8'h00;Что не попадает в эти области памяти, будет равно 0. Получается, что на ПЛИС рабочие области памяти будут такие пока что:
- F0000-FFFFF BIOS
- B8000-B9FFF VIDEOMEM
1cpu CPU8088 2( 3 .clock (clock_25), 4 .reset_n (1'b1), 5 .locked (locked), 6 .address (address), 7 .i_data (i_data), 8 .o_data (o_data), 9 .we (we) 10);Тут все достаточно просто, процессор работает на 25 мгц, когда память работает в 4 раза быстрее. Частоты задаются через PLL:
1de0pll unit_pll 2( 3 .clkin (CLOCK_50), 4 .m25 (clock_25), 5 .m100 (clock_100), 6 .locked (locked) 7);Все эти коды будут находится в прикрепленном файле, в том числе в файле map.html будет обновленная таблица доступных инструкции.