וואלהארצות הברית: "יותר ממיליארד חביות נפט גולמי עברו דרך מצר הורמוז"CNN Türk19 EYLÜL GÜNÜN MAÇLARI: Bugün kimin maçı var, hangi kanalda? Bugünkü maç programı…ESPN DeportesBarcelona: tres bajas en la convocatoria para viajar a SevillaESPNOregon wins in 84-0 rout despite missing 2 key players on offenseThe Jerusalem Post'I am pro-Abraham': Meet the unlikely allies risking everything for peace with IsraelRTP DesportoFC Porto chama Froholdt, Samu e Pietu-ze-vski para o clássico com o BenficaBollywood HungamaRambola: Mahaveer Jain and Amit Upadhyay pledge 100% of personal earnings and savings from Tulsidas film to Seva projectsBBC NewsCNN, MS NOW and Politico say reporters denied White House access after Trump banned some media outletsPunchEPL: Tottenham score first goals of season but lose 3-2 to VillaDigital SpyEmmerdale reveals first look at Dawn's funeral as Joe and Graham hide a secretPremium TimesWhy sharing my November 18 birthday with Rudeboy became difficult — Mr PRai NewsRitrovate le sorelle di 12 e 8 anni scomparse a San Cataldo: stanno bene
The Daily Newsstand · Free, Always
Saturday, September 19, 2026

RISC-V c ZX-Spectum like адаптером с 12 битным цветом и атомиками на плате DE0-CV

Translate

Любая ретро компьютероподобная DIYконструкция должна иметь видеовыход. А выбор видеорежима - это одно из ключевых решений проекта. Можно получить высокую четкость и потратить много ресурсов, можно сделать просто, но будет страшно. Что выбрать? Наверное это определяется первыми детскими впечатлениями: кого-то вдохновили тайлы MSX, а кому-то графика ZX-Spectrum оставила непоправимую детскую травму.

Есть еще старый добрый VGA , он же 320х200 на 256 цветов , он же mode 13, знакомый по игрушкам времен DOS. Очень комфортен из-за своей линейной адресации, достаточно большому количеству цветов, и совместимостью со множеством алгоритмов и библиотек, которые накопились за многие годы.

VGA mode 13

VGA mode 13

В тоже время, огромным недостатком является то, что этот режим требует огромное количество BRAM, а результирующий пиксель очень толстый, что делает фонты некрасивыми, палитра в 256 цветов избыточна для простых вещей и недостаточна для сложных. Да и сам по себе режим 320x200x256 это уже эпоха IBM PC и вряд ли является настоящим тру ретро.

Как показала практика, много цветов не надо(к примеру Anоther World с 16 цветами), а вот точку, чтобы шрифты были аккуратными, надо поменьше . Поэтому возникла мысль, что монохромный адаптер возможно будет приемлемым вариантом для таких поделок (Hercules тоже был в свое время неплох, но я его мельком видел), а как накинуть цвета – там придумаем, сделаем, если что, зеленым в стиле Matrix или жёлто-белым как на топовых монохромных мониторах 80-х.

Черно-белый режим

Черно-белый режим

Надо отметить, что есть желание остаться в ресурсах ретро-эпохи, то есть объём компонента ОЗУ не должен быть сильно больше 64К. VGA 640x480 с частой в 25 МГц удобен для поделок на FPGA и один бит на пиксель это всего 38К BRAM. Я использую плату E0-CV которую мне подарил @KeisN13. В ней памяти 300Кб. Так что на все хватит.

В отличии от ленивого mode 13, у подхода с чёрно-белыми пикселями есть одна очень-очень бесячая штука – невозможно взять и просто поставить пиксель который представлен битом используя только операции записи. Потому как меньше байта в микросхему запихать нельзя.

Чтобы изменить пиксель, нужно сначала считать байт из видеопамяти, (а это значит надо еще и реализовать чтение на уровне шины), наложить маску и записать обратно. Из-за этого какой-нибудь алгоритм Брезенхема превращается в кошмар. Жу-у-уть! Конечно тут есть подвох, в стародавние времена x86 команда OR установила бы маску непосредственно в памяти и никакой команды чтения не надо.

                       or es:[di], al
                       #где di байт, а al устанавливаема маска.

А как же RISC-V с его явным Load-Store ? В спецификации есть расширение атомарных операций (Atomic Instructions). Команды, изначально созданные для синхронизации и параллельных вычислений, показались мне тем инструментом, который и нужен для ретро-видеоадаптера. Ведь используя инструкции AMO.OR и AMO.AND , мы можем полностью избавиться от явного чтения памяти в коде, подобно x86. (На шине-то понятно чтение будет).

Возникла мысль взглянуть, а как будет выглядеть монохромный адаптер с атомиками от RISC-V, тем более что мой любимый YRV поддерживает атомарные операции, правда с одним исключением: первоначальный стандарт предусматривал атомики в виде 32-битных (или 64 битный) слов, а вот 8-битный атомики появились только в 2024 году в стандарте Zabha, который YRV не поддерживает, так что придется вычитывать 32 бита, что несколько отличалось от моих первоначальных намерений.

Для реализации я взял проект на котором делал рейтрейсер. Сам модуль VGA я использую из проекта Basic Graphic Music. С точки зрения шины тут ничего не меняется, со стороны процессора все равно будет приходить байт (или два, или четыре, и не забываем, что у AHB-Lite данные прилетают в следующем такте, поэтому адрес сохраняем в регистре ).

always @(posedge clk) begin
    if (vga_wr_byte[3]) vga_mem3[mem_addr_reg[15:2]] <= mem_wdata[31:24];
    if (vga_wr_byte[2]) vga_mem2[mem_addr_reg[15:2]] <= mem_wdata[23:16];
    if (vga_wr_byte[1]) vga_mem1[mem_addr_reg[15:2]] <= mem_wdata[15:8];
    if (vga_wr_byte[0]) vga_mem0[mem_addr_reg[15:2]] <= mem_wdata[7:0];

    if (mem_trans[0]) begin
        vga_rdata[31:24] <= vga_mem3[mem_addr[15:2]];
        vga_rdata[23:16] <= vga_mem2[mem_addr[15:2]];
        vga_rdata[15:8]  <= vga_mem1[mem_addr[15:2]];
        vga_rdata[7:0]   <= vga_mem0[mem_addr[15:2]];
    end
end

И чтобы добавить чтение из видеопамяти, нужно в слелектор добавить новый флаг чтения. Для этого добавил флаг vga_rdata.

  assign mcu_rdata  = (mem_rd_reg) ? mem_rdata              :
                      (vga_rd_reg) ? vga_rdata              :
                      (port10_dec) ? {port1_reg, port0_reg} :
                      (port32_dec) ? {port3_reg, port2_reg} :
                      (port54_dec) ? {port5_reg, port4_reg} :
                      (port76_dec) ? {port7_dat, port6_reg} : 32'h0;

Сделал я все по образу и подобию оригинального YRV. У меня была мысль сделать более похоже на шину AHB-Lite, но оказывается бесплатной она является только для нищебродов учебных заведений, а так стоит денег.

Так как у Cyclone V память двухпортовая, то проблемы с одновременным чтением данных со стороны процессора для атомиков и для отображения на дисплее не возникает. Я использовал предыдущий проект c VGA и оставил вычисление адреса для чтения по-байтово (что не совсем хорошо, так как реально все равно вычитывается слово).

reg [9:0] x_prev;

always @(posedge clk) x_prev <= x;

wire x_trigger = (x != x_prev) && (x[2:0] == 3'b111);

always @(posedge clk) begin
  if(x_trigger)
    begin
    if (x == 799) begin
        if (y == 524) begin
            pixel_addr <= 17'd0;
        end else begin 
            pixel_addr <= ((y+1) << 6) + ((y+1) << 4);
        end
    end else  begin
        pixel_addr <= (y << 6) + (y << 4) + ((x+1) >> 3);
    end
    end
end

Тактовая проекта 50Мгц, а пиксельная тактовая частота 25Мгц, на каждый пиксель выходит по два такта. На последнем пикселе (младший бит в байте) в предпоследнем такте, вычисляем адрес следующего байта для этого используем флаг x_trigger, и затем на втором такте защелкиваем все слово и адрес следующего байта в слове pixel_addr_low_reg.

always @(*) begin
    case(pixel_addr_low_reg)
        2'b00: byte_comb = vga_word_out[7:0];
        2'b01: byte_comb = vga_word_out[15:8];
        2'b10: byte_comb = vga_word_out[23:16];
        2'b11: byte_comb = vga_word_out[31:24];
    endcase
end

assign color_reg =  (byte_comb & (8'h80 >> x[2:0])) ? 16'hffff : 8'h00;

assign  vga_r = display_on ? color_reg[12:8] : 4'b0000;
assign  vga_g = display_on ? color_reg[7:4]  : 4'b0000;
assign  vga_b = display_on ? color_reg[3:0]  : 4'b0000;
                                     

Пиксель выводится так. Вообще можно и сдвиговый регистр поставить, но на этом этапе я решил сделать логику работы с пикселем и в C коде и в Verilog одинаково - двигаем единичку из старшего бита. Координаты считаются от левого верхнего угла, поэтому старший бит содержит младшую координату, пиксель с адресом 0 находится в 8м бите.

Небольшая симуляция, а куда ж без нее

Небольшая симуляция, а куда ж без нее

И так адаптер реализован и простейший вид записи будет такой:

// Код оформлен моделью Qwen 3.7
volatile char *VGA=(char *)0xA0000000L;

void plot(int x, int y, char color) {
    // 1. Находим адрес байта. Строка занимает 80 байт: (y * 64) + (y * 16) + (x / 8)
    int byte_offset = (y << 6) + (y << 4) + (x >> 3);
    
    // 2. Вычисляем маску для бита внутри байта.
    // Берем крайний левый бит (128 или 0x80) и сдвигаем его вправо на остаток от деления x на 8.
    // x % 8 = 0 -> 128 >> 0 = 128 (10000000b) -> крайний левый пиксель
    // x % 8 = 7 -> 128 >> 7 = 1   (00000001b) -> крайний правый пиксель
    char bit_mask = 128 >> (x & 7); 
    
    // Читаем текущее значение из памяти VGA
    char cur_byte = VGA[byte_offset];

    if (color) {    
        // Устанавливаем нужный бит в 1 (белый)
        VGA[byte_offset] = cur_byte | bit_mask;  
    } else {
        // Сбрасываем нужный бит в 0 (черный)
        VGA[byte_offset] = cur_byte & ~bit_mask; 
    }
}

Если посмотрим ассемблерный код, то увидим ту самую операцию чтения из видеопамяти. В коде можем заметить некоторый бонус в виде команды not, так как YRV поддерживает расширение операции с битами.

plot:
                    slli    a5,a1,0x2
                    lui     a4,0x2
                    lw      a4,548(a4) # 2224 <VGA>
                    add     a5,a5,a1
                    srai    a3,a0,0x3
                    slli    a5,a5,0x4
                    add     a5,a5,a3
                    add     a4,a4,a5
                    lbu     a5,0(a4)
                    andi    a0,a0,7
                    li      a3,128
                    sra     a3,a3,a0
                    zext.b  a5,a5
                    beqz    a2,328 <mtvec+0x23>
                    or      a5,a5,a3
                    zext.b  a5,a5
                    sb      a5,0(a4)
                    ret
                    not     a3,a3
                    and     a5,a5,a3
                    sb      a5,0(a4)
                    ret
     

А теперь посмотрим на вариант с атомиками,.

// Код офрмлен моделью Qwen 3.7
volatile char *VGA=(char *)0xA0000000L;

void plota(int x, int y, char color) {
    int byte_offset = (y << 6) + (y << 4) + (x >> 3);
    
    // 2. Получаем выровненный по 4 байтам адрес 32-битного слова
    // Операция & ~3 сбрасывает два младших бита адреса
    volatile unsigned int *word_ptr = (unsigned int *)((uintptr_t)VGA + (byte_offset & ~3));
    
    // 3. Вычисляем позицию бита в 32-битном слове (с учетом Little-Endian)
    // (byte_offset & 3) * 8  -> сдвиг байта внутри слова
    // 7 - (x & 7)            -> позиция пикселя внутри байта (от старшего к младшему)
    int bit_shift = ((byte_offset & 3) << 3) + (7 - (x & 7));
    unsigned int bit_mask = 1U << bit_shift;

    // 4. Модификация памяти происходит без промежуточного чтения в регистры CPU
    if (color) {
        // OR: устанавливает нужный бит в 1
        __asm__ __volatile__ (
            "amoor.w zero, %1, (%0)"
            :
            : "r"(word_ptr), "r"(bit_mask)
            : "memory"
        );
    } else {
        // AND: сбрасывает нужный бит в 0 с помощью инвертированной маски
        unsigned int inv_mask = ~bit_mask;
        __asm__ __volatile__ (
            "amoand.w zero, %1, (%0)"
            :
            : "r"(word_ptr), "r"(inv_mask)
            : "memory"
        );
    }
}

Операция чтения из памяти видеоадаптера отсутствует. Упс, лучше так не использовать использовать адрес VGA так как он вызывает операцию чтения памяти [lw a4,548(a4)] , это не та память которая нам нужна, задача кейса - использовать только команды записи.

plota:
               slli    a5,a1,0x2
               add     a1,a5,a1
               slli    a1,a1,0x4
               srai    a5,a0,0x3
               add     a1,a1,a5
               lui     a4,0x2
               slli    a5,a1,0x3
               lw      a4,548(a4) # 2224 <VGA>
               not     a0,a0
               andi    a0,a0,7
               andi    a5,a5,24
               or      a5,a5,a0
               andi    a1,a1,-4
               bset    a5,zero,a5
               add     a4,a4,a1
               beqz    a2,3b8 <plota+0x48>
               amoor.w zero,a5,(a4)
               ret
               not     a5,a5
               amoand.w        zero,a5,(a4)
               ret

Лучше использовать константу:

#define VGA_BASE_ADDR ((volatile unsigned int *)0xA0000000L)

 volatile unsigned int *word_ptr = &VGA_BASE_ADDR[byte_offset >> 2];

И все это можно реализовать не вызывая жуткий ассемблерный макрос, через встроенные функции GCC (оказывается это стандарт С++11).

void plot(int x, int y, int color) {
    int byte_offset = (y * 80) + (x >> 3);
    int bit_pos = ((byte_offset & 3) << 3) | (~x & 7);
    
    // Получаем выровненный указатель, используя константный адрес
    volatile unsigned int *word_ptr = &VGA_BASE_ADDR[byte_offset >> 2];
    
    unsigned int bit_mask = 1U << bit_pos;

    // Используем встроенные атомики GCC, которые развернутся в AMO-инструкции
    if (color) {
        __atomic_fetch_or(word_ptr, bit_mask, __ATOMIC_RELAXED);
    } else {
        __atomic_fetch_and(word_ptr, ~bit_mask, __ATOMIC_RELAXED);
    }
}

А если еще добавить немного магии, то можно убрать ветвление. Это еще одна задача кейса, так как RISC-V не любит ветвление, и при записи байта в память VGA в старом проекте никакого ветвления не было.


void plot(int x, int y, bool color) {
    int byte_offset = (y * 80) + (x >> 3);
    int bit_pos = ((byte_offset & 3) << 3) | (~x & 7);
    
    // Адрес вычисляется на базе константы (в ассемблере превратится в lui)
    volatile unsigned int *word_ptr = &VGA_BASE_ADDR[byte_offset >> 2];
    
    unsigned int bit_mask = 1U << bit_pos;
    
    // Branchless-магия для генерации масок без if-else:
    // Если color == 0, то c_mask = 0x00000000. Если color != 0, то c_mask = 0xFFFFFFFF
    unsigned int c_mask = -(unsigned int)(color != 0); 
    
    // Маска для ИЛИ: bit_mask если цвет 1, иначе 0
    unsigned int or_mask = bit_mask & c_mask;
    
    // Маска для И: инвертированная bit_mask если цвет 0, иначе 0xFFFFFFFF (не меняет биты)
    unsigned int and_mask = or_mask | ~bit_mask;
    
    // Вызов встроенных атомиков,
    __atomic_fetch_and(word_ptr, and_mask, __ATOMIC_RELAXED);
    __atomic_fetch_or(word_ptr, or_mask, __ATOMIC_RELAXED);
}

Мы убрали все обращения к памяти и ветвления.

plot:
               slli       a5,a1,0x2
               add        a5,a5,a1
               srai       a4,a0,0x3
               slli       a5,a5,0x4
               add        a5,a5,a4
               slli       a4,a5,0x3
               not        a0,a0
               andi       a0,a0,7
               andi       a4,a4,24
               or         a4,a4,a0
               bset       a3,zero,a4
               sll        a2,a2,a4
               not        a3,a3
               lui        a4,0xa0000
               andi       a5,a5,-4
               or         a3,a3,a2
               add        a5,a5,a4
               amoand.w   zero,a3,(a5)
               amoor.w    zero,a2,(a5)
               ret

Если мы сравним с оригинальной(VGA) функцией установки пикселя.

void plot(int x, int y, char color) {
    VGA[(y<<8) + (y<<6) + x] = color;
}

В три раза больше, но мы и пиксель в байте считали, на это нужны такты.

plot:
               slli    a5,a1,0x2
               add     a1,a5,a1
               slli    a1,a1,0x6
               add     a1,a1,a0
               lui     a5,0xa0000
               add     a1,a1,a5
               sb      a2,0(a1)
               ret

А потом у меня возникла мысль - а не добавить ли мне цвета. Интересно посмотреть как будет выглядеть ZX-Spectrum фрейм-буфер только в разрешении 640х480 с 12 битным цветом и спрайтом 8х8 .

Реализация тут совсем простая и ничем не отличается от VGA, так как процессору читать цвет из памяти не надо. Так как на плате ЦАП 12 битный, я решил использовать 16 бит на цвет (у ZX еще там вроде моргание было, но в данных условиях штука странная). У спрайта есть цвет фона и цвет изображения на каждый цвет по 16 бит - одно слово в 32 бита. Итого сверху по ресурсам еще 20Кб, в условия задачи укладываемся.

Делаем отдельную область памяти в адресах 0xA0010000. Так же как и в VGA считаем адрес в памяти: первый такт - защелкиваем адрес, второй такт защелкиваем слово содержащее цвет фона и цвет пикселей. Тут вся сложность что спрайт 8 на 8 и адрес по-другому считается, но такой расчет у меня был в самом первом проекте, для текстового видеоадаптера.

always @(posedge clk) begin
  if(x_trigger)
    begin
    if (x == 799) begin
        if (y == 524) begin
            color_addr <= 13'd0;
        end else begin 
            color_addr <= (((y+1) >> 3) << 6) + (((y+1) >> 3) << 4);
        end
    end else  begin
        color_addr <= ((y >> 3) << 6) + ((y >> 3) << 4) + ((x+1) >> 3);
    end
    end
end

На монитор выводим так: если слово цвета пустое '0 то значит это чёрно-белый режим - чтобы не заморачиваться с первоначальной установкой цвета, когда нужен просто текст:


assign color_reg = sprite_color =='0 ? (byte_comb & (8'h80 >> x[2:0])) ? 16'hffff : 8'h00
                                     : (byte_comb & (8'h80 >> x[2:0])) ? sprite_color[15:0] 
                                                                       : sprite_color[31:16];

Итого, я получил разрешение 640х480 с 12 битным цветом и нормальным использованием ресурсов платы. Так как художник я так себе, этого мне достаточно.

Где процессе работы у меня получилась вот такая картинка с Шоном Бином в матрице, которая частично ушла на КДПВ, и я решил ее использовать как тестовое изображение (уж лучше чем девица в шляпе с перьями) для вывода на монитор. Изображение было сконвертировано при помощи GIMP в однобитный XBM. Код написала модель Qwen 3.8 (иногда приятно пообщаться с другими разработчиками).

Нельзя просто взять и выйти из матрицы.

Нельзя просто взять и выйти из матрицы.

Изображение на мониторе

Изображение на мониторе

    drawXBM(60, 40);
    drawStringElvish(200, 380, "One does not simply place a Pixel", true);
    set_screen_color(240);

Шрифт реальный - модель сделала английские глифы 8x5, но вот русские глифы сгенерировать не смогла, но не страшно они у меня и так уже есть.

Или зелёный вариант, зря что-ли цвет добавляли:

Канонический монохром

Канонический монохром

Вот так выглядит все вместе.

Вот так выглядит все вместе.

Ссылка на видео с палитрой: https://youtube.com/shorts/baBjAgAbFRA?feature=share

Или можно с gitHub скачать: https://github.com/DmitryZlobec/yrv-spectrum/rawspectrum/demo.mp4

Ссылка на репу:https://github.com/DmitryZlobec/yrv-spectrum/blob/spectrum/Plus/design/yrv_mcu.v

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.