Bu bölüme kadar FPGA akışında Xilinx Vivado yazılımını kullanmıştık. Fakat Xilinx FPGA kartlarının maliyeti, öğrencilerin ve hobicilerin bu iş için ayıracağı bütçeyi aşabilir. Bu noktada FPGA çalışmalarına giriş yapabileceğimiz uygun maliyetli kartlara ihtiyaç duyuyoruz. Yakın döneme kadar birkaç üreticinin tekelinde bulunan FPGA pazarında, son dönemde bir çok küçük çaplı üreticileri görmeye başladık. Bu sayede büyük üreticilerin neredeyse CPLD kartlarının fiyatlarında giriş seviyesi FPGA kartları piyasaya sürülmüş oldu. Gowin de bu girişimlerden biri olan Çin menşeili bir FPGA yongası üreticisi. Gowin FPGA’lara Sipeed Tang Nano serisi kartlarda rastlıyoruz. Tang Nano FPGA kartları nispeten uygun maliyetli ve -bu yazı yazıldığı esnada- ülkemiz üzerinden de temin edilebiliyor. Ayrıca Tang Nano kartları açık kaynaklı araçları destekliyor. Tabii ki Gowin FPGA’ların da sahipli (ücretsiz lisanslı) IDE’leri var, ancak biz bu yazıda açık kaynaklı araçlar üzerinde duracağız.
📌 Açık kaynaklı FPGA araçlarının üretici tarafından resmi olarak desteklenmediğini belirtelim. Bu durum bazı uyumsuzluklar ve sorunlar yaşayabileceğimiz anlamına gelebilir.
Tang Nano FPGA geliştirme kartlarının 1K, 4K, 9K ve 20K gibi bir çok versiyonu var. Ben Matrak işlemcimizi çalıştırmak için Tang Nano 9K kartını temin ettim. 9K ibaresi kartta yer alan FPGA’nın 8640 LUT4 (Look Up Table) içerdiğine atıf yapıyor. Eğer elinizde 20K versiyonu mevcutsa muhtemelen küçük değişikliklerle bu projeyi çalıştırabilirsiniz.
Sipeed Tang Nano 9K
Tang Nano 9K kartının merkezinde Gowin GW1NR-9 FPGA yongası bulunuyor. GW1NR-9 yongası, 8640 LUT4’ün yanı sıra 6480 flip-flop, çeşitli boyutlarda SRAM yapıları ve dahili flash içeriyor. Ayrıca bu yonga System in Package (SiP) yapısında bir SDRAM de içeriyor.
Duyduklarımız güzel. Bir FPGA için önemli bileşenler tek yongada toplanmış. Geliştirme kartında bulunan diğer parçalar şunlar:
- HDMI konnektörü,
- USB-C konnektörü
- LCD konnektörleri
- 2 adet buton
- 6 adet LED
- USB JTAG-UART yongası
- 32 Mbit SPI flash
- 27 Mhz kristal osilatör
FPGA’yı harici programlayıcıya ihtiyaç duymadan USB-C üzerinden programlayabiliyoruz. FPGA’yı programlamak için kullandığımız bitstream dosyasını dahili veya harici flash belleğe yazabiliriz.
Soldan sağa; Arduino Nano, Deneyap Mini, Raspberry Pi Pico, Tang Nano 9K
Gowin FPGA’ları kullanmak için açık kaynaklı sentez ve place and route araçlarını bilgisyarımıza kuracağız. Kurulum işlemi, tıpkı 5. bölümde değindiğimiz RISC-V Toolchain paketinin kurulumu gibi derleme gerektiriyor. Eğer uğraşmak istemezseniz derlenmiş araçların bulunduğu oss-cad-suite paketini indirebilirsiniz. Paketi arşivden çıkarttıktan sonra dizini (kurulum_dizini/oss-cad-suite/bin
) Path’e eklemeyi unutmayın.
Eğer sadece ihtiyacımız olanları derleyerek kurmayı tercih edersek; Yosys, nextpnr-gowin, OpenFPGALoader araçlarına ihtiyacımız olacak. Ben daha pratik olacağı için önceden derlenmiş paketi tercih ettim.
Son olarak Apicula projesini kuracağız. Kurulum için uçbirime pip install apycula
komutunu giriyoruz.
Yosys: Sentez aracı
nextpnr-gowin: Gowin FPGA için Place ve Route araçları
Apicula: Gowin FPGA bitstream formatı için araçlar
OpenFPGALoader: FPGA programlama aracı
📌 OpenFPGALoader aracının FPGA kartına erişebilmesi için aşağıdaki adımları uygulamış olduğunuzdan emin olun.
99-openfpgaloader.rules dosyasını indirin ve dosyanın olduğu dizinde uçbirimi açıp aşağıdaki komutları girin.
$ sudo cp 99-openfpgaloader.rules /etc/udev/rules.d/
$ sudo udevadm control --reload-rules && sudo udevadm trigger # force udev to take new rule
$ sudo usermod -a $USER -G plugdev # add user to plugdev group
İşlemcinin Düzenlenmesi
Matrak işlemcimizi Tang Nano FPGA kartında çalıştırabilmek için donanımda bazı değişiklikler yapacağız. Bunlardan birincisi, sentezden geçebilmek için bellek boyutunu azaltmak olacak. Belleğimizin kapasitesini 640 bayt olacak şekilde güncelliyoruz.
memory.v:
// Matrak M10 RV32I RISC-V Processor
// Gülpare II Architechture 2023
// Main Memory Module
module memory (
input clk_i,
input wen_i, // Yazma yetkilendirme girişi
input [3:0] stb_i, // Bayt seçim girişi
input [31:0] addr_i, // Adres girişi
input [31:0] data_i, // Yazılacak veri
input [31:0] data_o // Okunan veri
);
// 160x32 bit = 5120 bit = 640 bayt
reg [31:0] mem [159:0];
// Programı belleğe yükle
initial begin
$readmemh("program.mem", mem);
end
// Asenkron okuma
assign data_o = mem[addr_i[31:2]];
// Senkron yazma
always @(posedge clk_i) begin
if (wen_i) begin
if (stb_i[0]) mem[addr_i[31:2]][0+:8] <= data_i[0+:8];
if (stb_i[1]) mem[addr_i[31:2]][8+:8] <= data_i[8+:8];
if (stb_i[2]) mem[addr_i[31:2]][16+:8] <= data_i[16+:8];
if (stb_i[3]) mem[addr_i[31:2]][24+:8] <= data_i[24+:8];
end
end
endmodule
Tang Nano kartı üzerinde 27 MHz saat frekansında çalışan bir osilatör bulunuyor. İşlemcimiz bu saat frekansında sentezden geçemediği için PLL yardımıyla saat frekansını düşürmemiz gerekiyor. Neyse ki açık kaynak araçlarla PLL IP’si üretebiliyoruz. Frekans tam olarak bölünebildiği için saati 18 MHz’e düşüren bir PLL tercih ettik. PLL modülünü oluşturmak için uçbirime gowin_pll -i 27 -o 18
komutunu girebilirsiniz.
⚠️ gowin_pll aracından ürettirilen PLL modülü bazen garip bir şekilde placement aşamasında sorunlara neden olabiliyor.
pll.v:
/**
* PLL configuration
*
* This Verilog module was generated automatically
* using the gowin-pll tool.
* Use at your own risk.
*
* Target-Device: GW1NR-9 C6/I5
* Given input frequency: 27.000 MHz
* Requested output frequency: 18.000 MHz
* Achieved output frequency: 18.000 MHz
*/
module pll(
input clock_in,
output clock_out,
output locked
);
rPLL #(
.FCLKIN("27.0"),
.IDIV_SEL(2), // -> PFD = 9.0 MHz (range: 3-400 MHz)
.FBDIV_SEL(1), // -> CLKOUT = 18.0 MHz (range: 400-600 MHz)
.ODIV_SEL(32) // -> VCO = 576.0 MHz (range: 600-1200 MHz)
) pll (.CLKOUTP(), .CLKOUTD(), .CLKOUTD3(), .RESET(1'b0), .RESET_P(1'b0), .CLKFB(1'b0), .FBDSEL(6'b0), .IDSEL(6'b0), .ODSEL(6'b0), .PSDA(4'b0), .DUTYDA(4'b0), .FDLY(4'b0),
.CLKIN(clock_in), // 27.0 MHz
.CLKOUT(clock_out), // 18.0 MHz
.LOCK(locked)
);
endmodule
Tang Nano kartı üzerinde S1 ve S2 olarak isimlendirilen iki adet buton bulunuyor. İşlemcimizin reset sinyalini S1 butonuna bağlayacağız. Bu butona basıldığında ilgili pin sıfıra çekiliyor, bu sebepten ötürü daha önceden “active high” olarak tasarladığımız reset sinyalini terse çevirerek “active low” haline getiriyoruz.
Tang Nano 9K buton bağlantı şeması
Saat ve reset sinyallerimizdeki değişikliklerin ardından üst modülümüz aşağıdaki gibi güncelleniyor.
top.v:
// Matrak M10 RV32I RISC-V Processor
// Gülpare II Architechture 2023
// Top Module
module top (
input clk_i,
input rst_i,
output [7:0] gpio_o,
output uart_tx_o
);
// Reset tersleniyor
wire rst_n = ~rst_i;
// PLL bağlantıları
wire clk;
pll p1 (
.clock_in(clk_i),
.clock_out(clk),
.locked()
);
// İşlemci bağlantıları
wire stall;
wire wen;
wire ren;
wire [3:0] stb;
wire [31:0] inst_addr;
wire [31:0] data_addr;
wire [31:0] wdata;
wire [31:0] rdata;
matrak mt1 (
.clk_i(clk),
.rst_i(rst_n),
.stall_i(stall),
.inst_i(mem_rdata),
.data_i(rdata),
.wen_o(wen),
.ren_o(ren),
.stb_o(stb),
.inst_addr_o(inst_addr),
.data_addr_o(data_addr),
.data_o(wdata)
);
// Bellek bağlantıları
wire [31:0] mem_addr;
wire [31:0] mem_rdata;
wire mem_wen;
memory me1 (
.clk_i(clk),
.wen_i(mem_wen),
.stb_i(stb),
.addr_i(mem_addr),
.data_i(wdata),
.data_o(mem_rdata)
);
// GPIO bağlantıları
wire [31:0] gpio_rdata;
wire gpio_request;
gpio g1 (
.clk_i(clk),
.rst_i(rst_n),
.sel_i(gpio_request),
.wen_i(wen),
.data_i(wdata),
.data_o(gpio_rdata),
.gpio_o(gpio_o)
);
// UART bağlantıları
wire uart_request;
wire [31:0] uart_rdata;
uart u1 (
.clk_i(clk),
.rst_i(rst_n),
.sel_i(uart_request),
.wen_i(wen),
.addr_i(data_addr),
.data_i(wdata),
.data_o(uart_rdata),
.uart_tx_o(uart_tx_o)
);
// Saat sayacı bağlantıları
wire clock_counter_request;
wire [31:0] clock_counter_rdata;
clock_counter cc1(
.clk_i(clk),
.rst_i(rst_n),
.sel_i(clock_counter_request),
.data_o(clock_counter_rdata)
);
wire loadstore_request;
wire peripheral_access;
wire memory_ls_access;
wire [31:0] periph_rdata;
// Load Store isteği kontrol ediliyor.
assign loadstore_request = wen | ren;
// Adres: 0x8XXXXXXX, çevrebirimlere yönlendiriliyor.
assign peripheral_access = loadstore_request & data_addr[31];
// Okuma yazma isteği belleğe yönlendiriliyor.
assign memory_ls_access = loadstore_request & !peripheral_access;
// Çevrebirimlerden okunan veri seçiliyor.
assign periph_rdata = gpio_request ? gpio_rdata : (uart_request ? uart_rdata :
(clock_counter_request ? clock_counter_rdata : 32'b0));
// Adrese göre seçilen çevrebirim belirleniyor.
assign gpio_request = peripheral_access & data_addr[12] & !data_addr[13]; // 0x80001000
assign uart_request = peripheral_access & data_addr[13] & !data_addr[12]; // 0x80002000
assign clock_counter_request = peripheral_access & data_addr[12] & data_addr[13]; // 0x80003000
// Belleğe yazma isteği gönderiliyor.
assign mem_wen = wen & memory_ls_access;
// Belleğe aktarılacak adres seçiliyor. (veri adresi veya buyruk adresi)
assign mem_addr = memory_ls_access ? data_addr : inst_addr;
// İşlemciye gönderilecek veri seçiliyor. (bellekten veya çevrebirimlerinden okunan veri)
assign rdata = memory_ls_access ? mem_rdata : periph_rdata;
// Bellek meşgulse işlemciyi durdur.
assign stall = memory_ls_access;
endmodule
Hatırlayacak olursak işlemcimize çevrebirimi olarak bağlı 8 adet çıkış pinimiz mevcuttu. Bu pinlerin 6’sını kartta yer alan LED’lere bağlayacağız, kalan 2 pini ise kartın boştaki herhangi bir pinine bağlayabiliriz. LED’leri yakmak için ilgili pini sıfıra çekmek gerekiyor. Bu yüzden çıkış birimi modülümüz sıfırlandığında artık pinlere 0 yerine 1 göndermeli.
Tang Nano 9K LED bağlantı şeması
gpio.v:
// Matrak M10 RV32I RISC-V Processor
// Gülpare II Architechture 2023
// GPIO Output Module
module gpio (
input clk_i,
input rst_i,
input sel_i, // Seçim sinyali
input wen_i, // Yazma yetkilendirme
input [31:0] data_i, // Veri girişi, işlemciden geliyor.
output [31:0] data_o, // Veri çıkışı, işlemciye gidiyor.
output reg [7:0] gpio_o // GPIO çıkış pinleri
);
// Çevrebirim seçilmişse ve yazma etkin değilse (okuma etkin) çıkış kaydedicisinin değerini işlemciye gönder.
assign data_o = (sel_i & !wen_i) ? {24'b0, gpio_o} : 32'b0;
always @(posedge clk_i, posedge rst_i) begin
if (rst_i) begin // Çıkışlar sıfırlanıyor.
gpio_o <= 8'b1111_1111;
end else begin
// Çevrebirim seçilmişse ve yazma etkinse işlemciden gelen değeri çıkış kaydedicisine aktar.
if (sel_i & wen_i) begin
gpio_o <= data_i[7:0];
end
end
end
endmodule
Diğer modüllerde herhangi bir değişiklik yapmıyoruz.
Donanımda yapılması gereken değişiklikleri halletik. Üst modül sinyallerimiz ile FPGA pinlerini eşleyecek “Constraint” dosyamızı aşağıdaki gibi oluşturabiliriz.
tangnano9k.cst:
// 27 Mhz oscillator
IO_LOC "clk_i" 52;
// Onboard button
IO_LOC "rst_i" 3;
// Onboard LEDS
IO_LOC "gpio_o[0]" 10;
IO_LOC "gpio_o[1]" 11;
IO_LOC "gpio_o[2]" 13;
IO_LOC "gpio_o[3]" 14;
IO_LOC "gpio_o[4]" 15;
IO_LOC "gpio_o[5]" 16;
IO_LOC "gpio_o[6]" 25;
IO_LOC "gpio_o[7]" 26;
// BL702 chip
IO_LOC "uart_tx_o" 17;
IO_PORT "uart_tx_o" IO_TYPE = LVCMOS33;
// IO_LOC "uart_rx_o" 18;
// IO_PORT "uart_rx_o" IO_TYPE = LVCMOS33;
FPGA Akışı
Verilog kodlarımız hazır olduğuna göre aşağıdaki komutla sentezleme işlemini başlatabiliriz.
$ yosys -p "read_verilog matrak.v memory.v clock_counter.v gpio.v uart.v pll.v top.v; synth_gowin -top top -json top.json"
Ardından placement ve routing işlemlerini gerçekleştiriyoruz. --freq 18
ifadesini PLL çıkışından elde edilen 18 MHz’lik saat frekansımızı belirtmek için ekledik.
$ nextpnr-gowin --json top.json --freq 18 --write top_pnr.json --device GW1NR-LV9QN88PC6/I5 --family GW1N-9C --cst tangnano9k.cst
Son olarak bitstream oluşturuyoruz.
$ gowin_pack -d GW1N-9C -o top.fs top_pnr.json
Şimdi tasarımızı FPGA kartına yüklemeye hazırız. Aşağıdaki komutu kullanarak bitstream dosyasını FPGA’nın SRAM hücrelerine yükleyebiliriz. Bu şekilde yüklediğimizde FPGA’nın gücü kesilirse konfigürasyon uçacaktır.
$ openFPGALoader -b tangnano9k top.fs
Dahili flash belleği programlamak istersek aşağıdaki komutu girmeliyiz.
$ openFPGALoader -b tangnano9k top.fs -f
Harici flash belleği programlamak için de aşağıdaki komutu girebiliriz.
$ openFPGALoader -b tangnano9k --external-flash top.fs
Gerekli komutların bir Makefile içinde toplanmış hali aşağıda verilmiştir. make all
komutuyla bitstream üretebilir, make program
komutuyla FPGA’yı programlayabiliriz.
makefile:
BOARD = tangnano9k
FAMILY = GW1N-9C
DEVICE = GW1NR-LV9QN88PC6/I5
all: top.fs
top.json: top.v
yosys -p "read_verilog matrak.v memory.v clock_counter.v gpio.v uart.v pll.v top.v; synth_gowin -top top -json top.json"
top_pnr.json: top.json
nextpnr-gowin --json top.json --freq 18 --write top_pnr.json --device ${DEVICE} --family ${FAMILY} --cst ${BOARD}.cst
top.fs: top_pnr.json
gowin_pack -d ${FAMILY} -o top.fs top_pnr.json
program: top.fs
openFPGALoader -b ${BOARD} top.fs
clean:
rm -f top.json top_pnr.json top.fs
FPGA Üzerinde Test
Donanımda yaptığımız değişikliklere uyum sağlamak için yazılımda da ufak düzenlemeler yapmalıyız. Bellek boyutumuz değiştiği için crt0.s başlangıç kodumuzda ve linker scriptimizde hafıza alanımızın üst değerini 640 (0x280) olarak güncelliyoruz.
crt0.s:
.section .text
.global _start
_start:
# Kaydedicileri sıfırla
mv x1, x0
mv x2, x0
mv x3, x0
mv x4, x0
mv x5, x0
mv x6, x0
mv x7, x0
mv x8, x0
mv x9, x0
mv x10, x0
mv x11, x0
mv x12, x0
mv x13, x0
mv x14, x0
mv x15, x0
mv x16, x0
mv x17, x0
mv x18, x0
mv x19, x0
mv x20, x0
mv x21, x0
mv x22, x0
mv x23, x0
mv x24, x0
mv x25, x0
mv x26, x0
mv x27, x0
mv x28, x0
mv x29, x0
mv x30, x0
mv x31, x0
# Stack değerini yükle
la x2, 0x280
# Main fonksiyonuna atla
jump_main:
addi a0, x0, 0
addi a1, x0, 0
jal x1, main
mem.ld:
OUTPUT_ARCH( "riscv" )
MEMORY
{
RAM : ORIGIN = 0x00000000, LENGTH = 0x280
}
SECTIONS
{
.text : {
. = ALIGN(4);
*(.text)
*(.text.*)
. = ALIGN(4);
} > RAM
.rodata : {
. = ALIGN(4);
*(.srodata)
*(.srodata.*)
*(.rodata);
*(.rodata.*)
. = ALIGN(4);
} > RAM
.data : {
. = ALIGN(4);
*(.sdata)
*(.sdata.*)
*(.data);
*(.data.*)
. = ALIGN(4);
} > RAM
}
Bellek boyutumuzun kısıtlı olması sebebiyle kütüphanemizi de sadece çıkış ve saat sayacı çevrebirimlerini destekleyecek şekilde kırpıyoruz.
matrak.h:
// Matrak Çevrebirim Kütüphanesi
#include <stdint.h>
#define GPIO0_BASE (0x80001000U)
#define GPIO0 ((GPIO*) GPIO0_BASE)
#define CYC_C0_BASE (0x80003000U)
#define CYC_C0 ((CYC_C*) CYC_C0_BASE)
#define CLK_FREQ 18000000
typedef struct {
volatile uint32_t gpio_output; // 0x80001000 (RW) OUTPUT REGISTER
} GPIO;
typedef struct {
volatile uint32_t clock_counter; // 0x80003000 (R) CLOCK COUNTER REGISTER
} CYC_C;
void gpio_write(int gpio_pin, int value) {
if (value) {
GPIO0->gpio_output |= (1<<gpio_pin);
}
else {
GPIO0->gpio_output &= ~(1<<gpio_pin);
}
}
void delay_ms(int time) {
uint32_t clk_value = CYC_C0->clock_counter + (time * CLK_FREQ) / 1000;
while (CYC_C0->clock_counter < clk_value);
}
Test için daha önce hazırlamış olduğumuz kara şimşek kodunu kullanacağız. Buradaki kodda farklı olarak, LED’lerin yakılması için ilgili pinlerin sıfıra çekilmesi gerektiğinden HIGH ifadesini 0 olarak tanımlıyoruz.
knight.c:
#include "matrak.h"
#define HIGH 0
#define LOW 1
void main(void) {
while (1) {
for (int i = 0; i < 6; i++) {
gpio_write(i, HIGH);
delay_ms(100);
gpio_write(i, LOW);
}
for (int j = 5; j > -1; j--) {
gpio_write(j, HIGH);
delay_ms(100);
gpio_write(j, LOW);
}
}
}
Test kodumuzu derlemek için hazırladığımız Makefile dosyamız ise aşağıdaki gibi görünüyor.
makefile:
CC = riscv32-unknown-elf-gcc
OBJCOPY = riscv32-unknown-elf-objcopy
CFLAGS = -march=rv32i -mabi=ilp32 -nostdlib -Wl,-T,mem.ld
all: knight.hex
crt0.o: crt0.s
$(CC) $(CFLAGS) -c crt0.s -o crt0.o
knight.elf: crt0.o knight.c
$(CC) $(CFLAGS) crt0.o knight.c -o knight.elf
knight.bin: knight.elf
$(OBJCOPY) -O binary knight.elf knight.bin
knight.hex: knight.bin
python3 bin2hex.py -i knight.bin -o knight.hex
clean:
rm -f crt0.o knight.elf knight.bin knight.hex
Şimdi Matrak işlemcimizi FPGA üzerinde çalıştırmaya hazırız. Bunun için test kodumuzu make all
komutunu vererek derliyoruz. Bu işlemin ardından üretilen knight.hex
dosyasını işlemcimizin bellek dosyası olan program.mem
dosyasına aktarıyoruz. Daha sonra FPGA akışı için hazırladığımız makefile dosyasının bulunduğu dizine geçip yine make all
komutu ile bitstream dosyasını elde ediyoruz. Bu noktada make program
komutu ile FPGA kartını programlayabilir ve işlemcimizi test edebiliriz.
Bellek alanı bizi oldukça sınırladığı için bir sonraki aşamada yapmamız gereken ilk şey, kart üzerinde yer alan SPI Flash’ı ROM olarak kullanmak olmalı.
📜 Bu bölümün kodlarına erişmek için tıklayın.