§ Баг верилятора
Сегодня моя задача состоит в том, чтобы что-то запустить, чтобы процессор уже как-то начал работать, в том числе, и в реальной отладочной плате. Пора запускать!В процессе работы над этим материалом обнаружился очень неприятный баг в вериляторе, который надо было учесть. Когда я отлаживал код, то понял, что надо переписать функцию tick25().
// Один такт 25 Mhz void tick25() { // Если есть запись, записать, и потом прочесть новое значение if (cpu_mod->we) memory[ cpu_mod->address & 0xFFFFF ] = cpu_mod->o_data; cpu_mod->i_data = memory[ cpu_mod->address & 0xFFFFF ]; ... // Сначала ставится 0 для всех ps2_mod->clock = 0; ps2_mod->eval(); vga_mod->clock = 0; vga_mod->eval(); cpu_mod->clock = 0; cpu_mod->eval(); // Потом ставится 1 ps2_mod->clock = 1; ps2_mod->eval(); vga_mod->clock = 1; vga_mod->eval(); cpu_mod->clock = 1; cpu_mod->eval(); ... vga(vga_mod->hs, vga_mod->vs, cl); }В чем суть? Оказывается, верилятор как-то неправильно считает такты, и когда происходит исполнение такта при clock=0, то защелкивается i_data, а потом никак не меняется при защелкивании на 1, так что я поменял код, что данные начинают извлекаться до того, как будет исполнен "тик-так".
На самом деле, для целей отладки я добавил в код дизассемблер, но приводить в статье его не буду, потому что это займет огромную "простыню" кода, так что этого делать не стоит. Если что, в коде, который прикреплен к статье, это все есть.
Еще важная вещь, о которой я совершенно забыл, так это выставление 1 на вход cpu:
1; cpu_mod->locked = 1;cpu_mod->reset_n =
§ Добавление новых инструкций
Сегодня я добавлю только те инструкции, которые потребуются, чтобы запустить простую программу. Список новых инструкции:- Условные переходы на короткие (short) ссылки из одного байта
- Короткий переход
- Дальний переход
- АЛУ с непосредственным операндом
- Перемещение (MOV) из регистра в память, регистра в регистр, из памяти в регистр
- Перемещение в сегментные и из сегментных регистров
- MOV в регистр из непосредственного значения
Некоторые из этих инструкции требуют считывания байта modrm, потому часть из них пойдет в
t <= tmodrm
вместо exec
. К таким относятся инструкции АЛУ и перемещения данных.// Предвычисление modrm 8'b00_xxx_0xx, // MOV rmr 8'b10_001_0xx: t <= tmodrm; // ALU rm,i 8'b1000_00xx: begin t <= tmodrm; dir <= 1'b0; end // MOV sreg, r16 8'b1000_11x0: begin t <= tmodrm; size <= 2'b1; end // HLT 8'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:
wire [7:0] branches = { /*7*/ (flags[SF] ^ flags[OF]) | flags[ZF], /*6*/ (flags[SF] ^ flags[OF]), /*5*/ flags[PF], /*4*/ flags[SF], /*3*/ flags[CF] | flags[ZF], /*2*/ flags[ZF], /*1*/ flags[CF], /*0*/ flags[OF] };Этот провод выступает в качестве всех возможных состоянии мультиплексора на 3 бита. А вот и реализация уже непосредственно в фазе exec:
8'b0111_xxxx: begin t <= init; if (branches[ opcode[3:1] ] ^ opcode[0]) ip <= ip + 1'b1 + signex; else ip <= ip + 1'b1; endЧтобы с ходу не запутаться:
wire [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 байт, просто никакие условия не проверяются:
8'b1110_1011: begin t <= init; ip <= ip + 1'b1 + signex; endСразу переходя к метке.
§ АЛУ с непосредственным операндом
Исполнение инструкции уже многоступенчатое, потому применяется m:8'b1000_00xx: case (m) // 8 bit 0: begin m <= opcode[1:0] == 2'b11 ? 2 : (size ? 1 : 2); op2 <= opcode[1:0] == 2'b11 ? signex : i_data; alu <= modrm[5:3]; ip <= ip + 1; end // 16 bit 1: begin m <= 2; op2[15:8] <= i_data; ip <= ip + 1; end // Запись результата 2: begin flags <= alu_f; t <= alu == alu_cmp ? init : wback; wb <= alu_r; end endcaseЭта инструкция имеет довольно-таки интересное исполнение.
-
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, то записывает в регистр или память полученный результат
Так что функция кажется и сложной, но на самом деле все просто. Единственная ее проблема в том, что все эти инструкции кушают много тактов.
§ Перемещения
А вот реализация инструкции перемещения данных из регистра в память и т.д. довольно просто:8'b1000_10xx: begin t <= wback; wb <= op2; endТо есть просто пишется правый операнд в левый и все. Не составляет труда понять схему работу перемещения из сегмента в регистр или память:
8'b1000_1100: begin t <= wback; case (modrm[5:3]) 3'h0: wb <= es; 3'h1: wb <= cs; 3'h2: wb <= ss; 3'h3: wb <= ds; endcase endИ наоборот, из памяти/регистра в сегментный регистр:
// MOV sreg, r16 8'b1000_1110: begin t <= init; case (modrm[5:3]) 3'h0: es <= op2; // 3'h1: cs <= op2; -- можно только на 8088 3'h2: ss <= op2; 3'h3: ds <= op2; endcase endВ первом случае результат уходит в регистр wb и далее происходит процедура обратной записи wback, а во втором случае правый операнд записывается в допустимые регистры es, ss, ds. А вот cs нельзя! Хотя... на 8088 как раз и можно было, но лучше уж запретить.
Теперь идет блок с реализацией достаточно часто используемых инструкции перемещения непосредственного значения в один из 8 регистров:
// MOV r,i 8'b1011_xxxx: case (m) // Byte 0: begin t <= opcode[3] ? exec : wback; m <= 1; dir <= 1'b1; wb <= i_data; size <= opcode[3]; ip <= ip + 1'b1; modrm[5:3] <= opcode[2:0]; end // Word 1: begin t <= wback; ip <= ip + 1'b1; wb <= {i_data, wb[7:0]}; end endcaseПри получении первого такта, на всякий случай записываются все необходимые регистры:
- m=1, чтобы перейти на этот этап, но происходит это лишь при t=exec, который появляется лишь при size=1 (то есть, чтение 16 бит)
- dir=1 и modrm[5:3] - номер регистра, куда будет записан результат
- Обязательно указываем size, поскольку он формируется нестандартным способом, находясь в 3-м бите опкода
- Ну и записывая в wb полученный младший байт непосредственного значения
§ Дальний переход
Ну и последнее, что на сегодня я рассмотрю по поводу инструкции, это будет важная инструкция дальнего перехода, которая исполняется при старте процессора:8'b1110_1010: case (m) 0: begin ip <= ip + 1; m <= 1; wb[ 7:0] <= i_data; end 1: begin ip <= ip + 1; m <= 2; wb[15:8] <= i_data; end 2: begin ip <= ip + 1; m <= 3; segment[7:0] <= i_data; end 3: begin {cs, ip} <= {i_data, segment[7:0], wb}; t <= init; end endcaseЭта инструкция последовательно забирает 4 байта и после их приема отсылает на новый CS:IP.
§ Небольшая программа
Приведу текст программы на ассемблере, которая будет способна на выполнение простых действий - очистки экрана и пропечатывание надписи "Hello World":; Установка сегментов mov ax, $b800 mov es, ax mov ax, cs mov ds, ax ; Очистка экрана mov ax, $17F9 mov bx, $0000 mov cx, 2000 @@: mov [es:bx], ax add bx, 2 sub cx, 1 jne @b ; Надпись mov si, master mov bx, 2*80+4 @@: mov al, [cs:si] cmp al, 0 je $ add si, 1 mov [es:bx], ax add bx, 2 jmp @b master: db "Hello World!", 0Результатом исполнения программы будет следующее:
§ Синтез на ПЛИС
После проверки на вериляторе, всегда хочется проверить и то, как это заработает на ПЛИС, потому, после того, как создана схема из шаблона, добавляю блоки памяти:wire [7:0] i_data_bios; bios BiosMemory ( .clock (clock_100), .address_a (address[15:0]), .q_a (i_data_bios), .data_a (o_data), .wren_a (we & we_bios) );Здесь будет храниться 64 Кб BIOS, к пинам на выход подключено на провод i_data_bios. Для памяти видеоадаптера будет так:
wire [ 7:0] i_data_vga; wire [12:0] address_vga; font UnitFont ( .clock (clock_100), .address_a (address_vga), .q_a (data), .address_b (address[12:0]), .q_b (i_data_vga), .data_b (o_data), .wren_b (we & we_vga), );Эта память двухпортовая, в блоке памяти может читаться и писаться одновременно как из модуля видеадаптера (выбирается через адрес address_vga), так и из процессора address[12:0] и i_data_vga.
В обоих этих модулях есть запись в память через o_data, we и разрешающего запись провода we_bios и we_vga.
wire we_bios = address >= 20'hF0000; wire we_vga = address >= 20'hB8000 && address < 20'hBA000;На них будет 1 или 0 в зависимости от того, куда указывает сейчас адрес. Соответственно, в общее i_data будет выбрано соответственно:
wire [ 7:0] i_data = we_bios ? i_data_bios : we_vga ? i_data_vga : 8'h00;Что не попадает в эти области памяти, будет равно 0. Получается, что на ПЛИС рабочие области памяти будут такие пока что:
- F0000-FFFFF BIOS
- B8000-B9FFF VIDEOMEM
CPU8088 ( .clock (clock_25), .reset_n (1'b1), .locked (locked), .address (address), .i_data (i_data), .o_data (o_data), .we (we) );cpu Тут все достаточно просто, процессор работает на 25 мгц, когда память работает в 4 раза быстрее. Частоты задаются через PLL:
CLOCK_50), .m25 (clock_25), .m100 (clock_100), .locked (locked) );de0pll unit_pll ( .clkin (Все эти коды будут находится в прикрепленном файле, в том числе в файле map.html будет обновленная таблица доступных инструкции.