Выберите элементы, которые вы хотите скрыть на этой странице.
Сохранить
Отмена

## Sidebar

### Смотрите

### Тэги

игра жизнь Модуль приемника USB Verilog HDL NIOS II плата разъемов ALARM 7-Segment синус synchronizer меандр кормушка схемотехника MIPSfpga терминал VT100 демультиплексор ПЛИС Sigasi CodeBench Raspberry Pi4 Счетчик кода Грея новая плата Марсоход Serial MAX II солнечное затмение fpga Verilog FT4232 Синхронная логика машинка Altera RTLViewer ARM core FPGA JTAG Убить MAX Управление Marble Machine v2 цифровой осцилограф Python Yosys протокол Altera TAP Controller Active Serial MINGW Марсоход3 Начинаем изучать RISC-V RS232 симуляция Icarus Verilog Verilog VPI сумматор ЦАП

### Комментарии

- #### Плата MCY112

  Kiril 13.02.2024 17:30
  Для дельта-сигма достаточно и одного бита. Здесь даже лишнего с запасом. А 10кГц и вправду мало...

  Подробнее...

- #### Плата MCY112

  Артём2 13.02.2024 16:39
  > Двухканальный аудио выход для Дельта Сигма ЦАП (8бит); Что подразумевается под "8бит"? Для вывода PCM ...

  Подробнее...

- #### Плата Марсоход3GW (Gowin)

  Alex63 02.02.2024 11:50
  Здравствуйте! очень хочется такую плату с возможностью припайки добавочной микросхемы памяти IS62C1024AL ...

  Подробнее...

- #### Белая медведица на льдине.

  VladimirRunner 01.02.2024 17:46
  Привет! Нашел ваши статьи хотел узнать можно ли к вам обратиться помочь запрограммирова ть адресные ргб ...

  Подробнее...

- #### Программатор MBFTDI в среде Altera Quartus II , версия 1.1b

  Серый Волк 21.01.2024 18:48
  Подскажите пожалуйста - для чего выведены пины на программаторе "COM"?

  Подробнее...

### На форуме

- Нет сообщений для показа

# Процессор языка Forth в FPGA

Подробности
  nckm
  Проекты Quartus Prime для M02mini
  Создано: 06 октября 2020
  Просмотров: 7885

Empty

Наверное каждый разработчик FPGA рано или поздно решает создать свой процессор. Понятно, что есть много общеизвестных и распространенных процессоров у которых исходные тексты открыты для изучения и использования. Даже у нас на сайте мы уже запускали в плате Марсоход2 систему на кристалле ARMv2a и в плате Марсоход3 систему с процессором MIPS. Но и это не все! На opencores.org в разделе "процессоры" можно взять различные исходники более двухсот процессоров! Из широко известных там и Z80 и OpenRISC и RISC-V.

В этой статье я не буду изобретать свой процессор, однако, расскажу о довольно специфичном ядре - Forth-J1. Этот процессор специально разработан для исполнения "шитого кода" языка программирования Forth и хорошо подходит для использования внутри FPGA.

Автор этого ядра James Bowman, его страница на github здесь https://github.com/jamesbowman/j1
Я взял его Verilog исходники и адаптировал к нашим платам: Марсоход3, Марсоход3bis и новой плате M02mini. Вы можете взять мой адаптированный проект вот здесь: https://github.com/marsohod4you/M02mini/tree/master/forth_j1

Этот проект компилируется в среде САПР Intel Quartus Prime Lite для ПЛИС MAX10 наших плат и, в частности, для платы M02mini. При компиляции проекта смотрите, какая выбрана ревизия, для какой платы.

На плате M02mini стоит ПЛИС Intel MAX10 всего 2 тысячи логических элементов, но и здесь может работать 32х битный процессор J1 благодаря своей компактности.

Ядро процессора занимает меньше 150 строк кода на Verilog HDL:

    `include "common.h"

    module j1(
    	input wire clk,
    	input wire resetq,

    	output wire io_wr,
    	output wire [15:0] mem_addr,
    	output wire mem_wr,
    	output wire [`WIDTH-1:0] dout,
    	input  wire [`WIDTH-1:0] mem_din,

    	input  wire [`WIDTH-1:0] io_din,

    	output wire [12:0] code_addr,
    	input  wire [15:0] insn
    	);

    reg [12:0] pc, pcN;
    reg reboot = 1;
    wire [12:0] pc_plus_1 = pc + 1;

    //D stack
    reg  [`WIDTH-1:0] st0;	// Top of data stack
    wire [`WIDTH-1:0] st1;	// Next data on stack
    reg  [`WIDTH-1:0] st0N;	// Next calculated value of st0
    reg  [`DEPTH-1:0] dsp;	// Data stack pointer, points to st1 value
    reg  [`DEPTH-1:0] dspN;	// Next calculated value of dsp
    reg dstkW;					// D stack write enable signal

    assign mem_addr = st0N[15:0];
    assign code_addr = {pcN};

    stack #(.DEPTH(`DEPTH))
    	dstack(
    		.clk(clk),
    		.resetq(resetq),
    		.ra(dsp),
    		.rd(st1),
    		.wa(dspN),
    		.wd(st0),
    		.we(dstkW)
    		);

    //R stack
    wire [`WIDTH-1:0] rst0;	// Top value on return stack
    reg  [`DEPTH-1:0] rsp;	// Return stack pointer
    reg  [`DEPTH-1:0] rspN;	// Next calculated value of Return stack pointer
    wire [`WIDTH-1:0] rstkD;// R stack value for write
    reg rstkW;					// R stack write enable signal
    stack #(.DEPTH(`DEPTH))
    	rstack(
    		.clk(clk),
    		.resetq(resetq),
    		.ra(rsp),
    		.rd(rst0),
    		.wa(rspN),
    		.wd(rstkD),
    		.we(rstkW)
    		);

    always @*
    begin
    	// Compute the new value of st0
    	casez ({insn[15:8]})
    		8'b1??_?????: st0N = { {(`WIDTH - 15){1'b0}}, insn[14:0] };    // literal
    		8'b000_?????: st0N = st0;                     // jump
    		8'b010_?????: st0N = st0;                     // call
    		8'b001_?????: st0N = st1;                     // conditional jump
    		8'b011_?0000: st0N = st0;                     // ALU operations...
    		8'b011_?0001: st0N = st1;
    		8'b011_?0010: st0N = st0 + st1;
    		8'b011_?0011: st0N = st0 & st1;
    		8'b011_?0100: st0N = st0 | st1;
    		8'b011_?0101: st0N = st0 ^ st1;
    		8'b011_?0110: st0N = ~st0;
    		8'b011_?0111: st0N = {`WIDTH{(st1 == st0)}};
    		8'b011_?1000: st0N = {`WIDTH{($signed(st1) < $signed(st0))}};
    `ifdef NOSHIFTER // `define NOSHIFTER in common.h to cut slice usage in half and shift by 1 only
    		8'b011_?1001: st0N = st1 >> 1;
    		8'b011_?1010: st0N = st1 << 1;
    `else      // otherwise shift by 1-any number of bits
    		8'b011_?1001: st0N = st1 >> st0[4:0];
    		8'b011_?1010: st0N = st1 << st0[4:0];
    `endif
    		8'b011_?1011: st0N = rst0;
    		8'b011_?1100: st0N = mem_din;
    		8'b011_?1101: st0N = io_din;
    		8'b011_?1110: st0N = {{(`WIDTH - 8){1'b0}}, rsp, dsp};
    		8'b011_?1111: st0N = {`WIDTH{(st1 < st0)}};
    	default: st0N = {`WIDTH{1'bx}};
    	endcase
    end

    wire is_alu = (insn[15:13] == 3'b011);
    wire func_T_N =   (insn[6:4] == 1);
    wire func_T_R =   (insn[6:4] == 2);
    wire func_write = (insn[6:4] == 3);
    wire func_iow =   (insn[6:4] == 4);
    assign mem_wr = !reboot & is_alu & func_write;
    assign io_wr  = !reboot & is_alu & func_iow;
    assign dout = st1;

    assign rstkD = (insn[13]==1'b0) ? {{(`WIDTH - 14){1'b0}}, pc_plus_1, 1'b0} : st0;

      reg [`DEPTH-1:0] dspI, rspI;
      always @*
      begin
        casez ({insn[15:13]})
        3'b1??:   {dstkW, dspI} = {1'b1,      4'b0001};
        3'b001:   {dstkW, dspI} = {1'b0,      4'b1111};
        3'b011:   {dstkW, dspI} = {func_T_N,  {insn[1], insn[1], insn[1:0]}};
        default:  {dstkW, dspI} = {1'b0,      4'b0000};
        endcase
        dspN = dsp + dspI;

        casez ({insn[15:13]})
        3'b010:   {rstkW, rspI} = {1'b1,      4'b0001};
        3'b011:   {rstkW, rspI} = {func_T_R,  {insn[3], insn[3], insn[3:2]}};
        default:  {rstkW, rspI} = {1'b0,      4'b0000};
        endcase
        rspN = rsp + rspI;

        casez ({reboot, insn[15:13], insn[7], |st0})
        6'b1_???_?_?:   pcN = 0;
        6'b0_000_?_?,
        6'b0_010_?_?,
        6'b0_001_?_0:   pcN = insn[12:0];
        6'b0_011_1_?:   pcN = rst0[13:1];
        default:        pcN = pc_plus_1;
        endcase
      end

      always @(negedge resetq or posedge clk)
      begin
        if (!resetq) begin
          reboot <= 1'b1;
          { pc, dsp, st0, rsp } <= 0;
        end else begin
          reboot <= 0;
          { pc, dsp, st0, rsp } <= { pcN, dspN, st0N, rspN };
        end
      end
    endmodule

В процессоре имеются два стека: стек данных и стек возвратов. Глубина стека, впрочем, как и разрядность процессора определяется в файле include "common.h". Как я уже выше написал я буду компилировать 32х разрядный процессор. Глубина стека у меня будет 16 (для стека данных 17).

По внешним сигналам процессор имеет два интерфейса для доступа к памяти. Один интерфейс для чтения инструкций программы insn[15:0] по адресу code_addr[12:0]. Второй интерфейс для доступа к памяти данных и для доступа к портам ввода вывода. Процессор адресует 64К слов данных dout[`WIDTH-1:0]/mem_din[`WIDTH-1:0] через сигналы адресов mem_addr[15:0]. Запись в память или в устройство ввода вывода определяется сигналами mem_wr/io_wr.

Как понятно из вышесказанного, чтение инструкций происходит параллельно другим операциям с памятью и эти операции не мешают друг другу. В моей реализации проекта я использую для этого двухпортовую память.

В модуле стека так же чтение одного значения и запись другого происходят одновременно. Это обеспечивается вот таким кодом на Verilog HDL:

    `include "common.h"

    module stack #(parameter DEPTH=4) (
    	input wire clk,
    	/* verilator lint_off UNUSED */
    	input wire resetq,
    	/* verilator lint_on UNUSED */
    	input wire [DEPTH-1:0] ra,
    	output wire [`WIDTH-1:0] rd,
    	input wire we,
    	input wire [DEPTH-1:0] wa,
    	input wire [`WIDTH-1:0] wd);

    	reg [`WIDTH-1:0] store[0:(2**DEPTH)-1];

    	always @(posedge clk)
    		if(we)
    			store[wa] <= wd;

    	assign rd = store[ra];

    endmodule

Модулей стека в ядре процессора два. Стек данных и стек возвратов. В Форте все делается через стек. Ведь язык Forth стековый. Все операции выполняются с данными лежащими на стеке. Даже можно слово данных переместить с вершины стека данных на вершину стека возвратов и наоборот.

Вот типичный пример на Форте. Пусть, нужно сложить два числа:
> 2 3 + .
Интерпретатор Форта возьмет из консоли число 2 и положит на стек, потом возьмет число 3 и так же положит на стек. Далее интерпретатор найдет во входном потоке символов слово "+" и будет исполнять его. А слово "+" снимает со стека два операнда, складывает их и результат сложения кладет на стек. А вот Форт слово "." снимет одно значение со стека и выведет его в консоль, напечатает результат. После всех этих действий на стеке данных уже не останется ничего, но "полезная работа" будет выполнена.

Те, кто впервые видят такую запись, а она называется "обратная польская запись", очень удивляются и считают этот подход очень неудобным. Но на самом деле это все дело привычки. Мозг человека весьма гибок. Энтузиасты Форта с легкостью пишут код для него и не очень страдают от непривычной записи.

Кроме того, у обратной польской записи есть и несомненные преимущества - отсутствие скобок в выражениях и из-за этого нет необходимости анализировать приоритеты операций.

Возвращаясь к ядру процессора Forth можно отметить, что стековая организация данных сильно упрощает выбор операндов для АЛУ процессора, ведь в операции ALU всегда участвуют значения, находящиеся на стеке. Интересно, что в этом ядре J1 операнд на вершине стека находится не внутри модуля стека данных, а в отдельном регистре st0. И понятно почему так сделано - ведь для АЛУ обычно нужно два операнда, а как из стека считать одновременно сразу два верхних элемента? Никак. Вот и получается, что из регистра st0 берется Top, а из модуля стека данных берется значение st1 то есть Next. Из за этого как раз лубина стека не степень двойки: 16+1=17.

Довольно интересно рассмотреть кодирование команд процессора. Все команды процессора являются шестнадцатибитными. При этом разные битовые поля отвечают за разную логику:

Интересно, что числовые значения-операнды кодируются прямо в инструкции, если старший бит ее insn[15] в единице. К сожалению, диапазон хранимых значений при этом всего 0..32767 и это не очень много. Для 32х битного процессора это не очень хорошо. Если потребуется большее число, то его придется моделировать арифметическими операциями и сдвигами. Но тем не менее, такой подход тоже имеет право на жизнь.

Биты insn[14:13] определяют тип операции: безусловный переход, условный переход, вызов подпрограммы или типовая обычная операция ALU.

Операции ALU кодируются в битах insn[11:8]. В основном это те операции, которые определяют, новое значение, которое будет записано на вершину стека.

По коду можно понять, что делает та или иная операция в АЛУ:
4'b0000 - NOP
4'b0001 - NEXT
4'b0010 - ADD
4'b0011 - AND
4'b0100 - OR
4'b0101 - XOR
4'b0111 - EQUAL
4'b1000 - SIGNED COMPARE LESS
4'b1001 - SHIFT RIGHT
4'b1010 - SHIFT LEFT
4'b1011 - Top value on return stack
4'b1100 - MEM READ DATA
4'b1101 - IO READ DATA
4'b1110 - Get stack pointers
4'b1111 - UNSIGNED COMPARE / LESS

Нужно обратить внимание, что практически любая операция может быть скомбинирована с возвратом из подпрограммы. Если установлен бит инструкции insn[12], то на этом же такте, кроме всех других действий будет взято значение со стека возвратов и записано в регистр PC, который указывает на следующую исполняемую инструкцию.

Что еще осталось? Если установлен бит insn[6], то значение со стека данных будет записано в значение стека возвратов. При этом, движутся ли сами указатели обоих стеков определяется битами insn[1:0] для dsp и insn[3:2] для rsp.

Если установлен бит insn[5], то значение NEXT со стека данных будет записано по адресу TOP стека данных.

В этой таблице показано, как некоторые из базовых слов языка Forth могут быть выражены в ассемблерных инструкциях, какие поля инструкции и как должны быть установлены:

Вообще, цикл работы процессора можно описать вот такой простой схемой:

Слева показанно, считываются исходные значения для кода текущей инструкции insn, значение из памяти или I/O [T], текущие значения для вершины стека возвратов R и стека данных T и N. Эти исходные значения декодируются и используются для вычислений в ALU и справа показано, как такт завершается и производится запись новых значений в регистры и память.

На этой схеме слева и справа одна и та же память и стеки, это как бы временная диаграмма: слева чтение, справа запись результата. На следующем такте этот цикл повторяется снова и снова.

Как же написать и запустить программу для языка Forth в нашей плате?

Обычно, Форт это интерпретатор. Но для embedded систем логично использовать кросс-компиляцию, то есть собирать бинарные данные программы на компьютере, а запускать их уже в FPGA плате. Поэтому, для кросс компиляции в проекте используется gforth и специальные форт-скрипты.

В папке проекта есть папка toolchain и в н ей все нужные скрипты: make.bat, cross.fs

Скрипт make.bat просто запускает gforth:

>gforth cross.fs basewords.fs demo1.fs

В скприпте cross.fs написан кросскомпилятор на Форте. В скрипте basewords.fs описаны самые первые ассемблерные слова Forth-j1, те которые описывают инструкции процессора. Ну а в скрипте demo1.fs собственно моя программа на Forth.

Честно говоря я не большой специалист по языку Форт, но вот смог написать такую простую программу:

header 1+ : 1+ d# 1 + ;
header 1- : 1- d# -1 + ;
header 0= : 0= d# 0 = ;
header cell+ : cell+ d# 2 + ;

header <> : <> = invert ;
header > : > swap < ;
header 0< : 0< d# 0 < ;
header 0> : 0> d# 0 > ;
header 0<> : 0<> d# 0 <> ;
header u> : u> swap u< ;

( check serial rx port has byte )
header read?
: read?
d# 0 io@
d# 8 and
0<>
;

( read byte from serial port )
header read
: read
begin
read?
until
d# 0 io@ d# 8 rshift
d# 0 d# 2 io!
;

( check serial tx port busy )
: tx_busy
d# 0 io@
d# 4 and
0=
;

header emit
: emit
begin tx_busy until
h# 0 io!
;

header cr
: cr
d# 13 emit
d# 10 emit
;

header space
: spac e
d# 32 emit
;

: h dup d# 10 < if h# 30 else d# 55 then + emit ;
: hh dup d# 4 rshift d# 15 and h d# 15 and h ;

: main
noop
noop
noop
noop
noop
d# 0
begin
read dup hh space emit cr
d# 1 +
dup
d# 4 io!
again
;

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

Например, слово read? читает из I/O порта по адресу ноль. Для этого на стек кладется число-адрес 0 и вызывается слово io@

Слово io@ забирает со стека число-адрес, считывает из указанного порта значение и кладет его на стек. Потом на стек кладется число 8 и вызывается слово and, которое снимает два числа со стека вычисляет логическую функцию and и кладет результат на стек. Число 8 определяет битовую маску для сигнала готовности принятого байта в последовательном порту Форт-системы. Слово 0<> вычисляет равно ли нулю число на стеке или нет. Таким образом, слово read? возвращает на стеке значение, готов ли символ из последовательного порта или нет. Позволяет опрашивать последовательный порт.

Слово read в цикле begin read? until ожидает прихода символа из последовательного порта. Когда символ будет принят цикл прерывается и происходит реальное чтение из порта принятого байта.

Примерно так же работает пара слов tx_busy и emit. Слово tx_busy ждет когда последовательный порт будет свободен для отправки нового байта, а слово emit записывает передаваемый байт в порт.

Вся мощь языка Forth заключается в том, что более сложные слова определяются через более простые. Весь словарный запас формируется самим программистом и может быть фактически произвольным. При этом, достигается очень высокая компактность кода - весь код это ссылки на другие слова, которые ссылаются на другие слова.

Выше я определил слово h которое печатает в шестнадцатеричном виде одну тетраду из числа. А второе мое слово hh вызывает слово h два раза и печатает уже байт.

Слово main, определенное последним представляет собой вечный цикл. Последовательность read dup hh space emit cr читает из последовательного порта байт, печатает его в шестнадцатеричном виде, потом печатает пробел space и печатает сам принятый символ, потом переводит каретку консоли на следующую строку. При этом еще и подсчитывается число принятых байтов из последовательного порта и записывается в I/O порт 4, где располагается регистр светодиодов. То есть на светодиодах платы будет отображаться число принятых байт. Конечно, это очень простая программа, но она показывает, что процессор работает.

Я уверен, что такое ядро процессора Forth-J1 может успешно применяться в FPGA системах, ведь оно очень компактно по ресурсам и при этом является очень производительным.

Вот отчет компилятора при компиляции для платы M02mini:

Flow Status Successful - Sun Oct 04 18:09:51 2020
Quartus Prime Version 20.1.0 Build 711 06/05/2020 SJ Lite Edition
Revision Name max10_2
Top-level Entity Name jtop
Family MAX 10
Device 10M02DCV36C8G
Timing Models Final
Total logic elements 1,232 / 2,304 ( 53 % )
Total registers 276
Total pins 9 / 27 ( 33 % )
Total virtual pins 0
Total memory bits 33,792 / 110,592 ( 31 % )
Embedded Multiplier 9-bit elements 0 / 32 ( 0 % )
Total PLLs 1 / 1 ( 100 % )
UFM blocks 1 / 1 ( 100 % )
ADC blocks 0

В чипе всего 2 тысячи логических элементов, но и они заняты только на половину. Хочу еще обратить внимание, что в данной микросхеме 10M02 оказалось невозможным хранить код программы непосредственно в блоке памяти. Из-за этого пришлось делать дополнительную логику, которая считывает код программы из UFM (User Defined Memory) и записывает его в ОЗУ. Для более старших чипов MAX10 (платы Марсоход3 и Марсоход3bis ) этого делать конечно не требуется.

Я запустил процессор Forth-J1 на частоте 50МГц, но компилятор посчитал, что Fmax 72МГц. Очень приличный результат я считаю.

Думаю такой процессор вполне может использоваться там, где нужен софт процессор, может быть даже вместо NIOS или любого другого.

PS: сделал простой интерпретатор Форта.

- ВКонтакте
- Одноклассники
- Мой Мир
- Twitter
- Viber
- WhatsApp
- Skype
- Telegram

- Вперёд

Комментарии
0 #4 KPG 18.10.2020 10:54
Firmware IP камеры wge100
wiki.ros.org/.../
fforum.winglion.ru Рускоязычный форум пользователей Форт языка
0 #3 Chaosorg 10.10.2020 09:59
мне кажется, что если ради "спортивного интереса", то интереснее "велосипедить", а если из практического.. . кто-нибудь может поделиться ссылкой на какой-то ресурс в сети с исходниками на Forth каких-то полезных вещей? доступ к файловой системе, например, работа с сетью, с USB устройствами? В сети есть исходники для forth-систем запускаемых поверх какой-то ОС, но его прелесть, что он задумывался сам как ОС и самодостаточная среда исполнения
+1 #2 tiny10 06.10.2020 17:42
Два года хотел этот форт-процессор попробовать, узнать его параметры по занимаемому объему и скорости, спасибо очень!
0 #1 foxtail 06.10.2020 06:01
Давно мечтал реализовать этот процессор, спасибо за полезный материал!
Добавить комментарий

Имя (обязательное)

E-Mail (обязательное)

Осталось: 1000 символов

Подписаться на уведомления о новых комментариях

Отправить
Отменить