J1: стековый процессор для ПЛИС.

Увидев как затейники с сайта marsohod.org заталкивают незаталкиваемое в свою плату(конкретно — упрощенный процессор) решил тоже с чем-то подобным поэкспериментировать. Не придумать, конечно, свой собственный, но попытаться написать прошивку для готового варианта, прицепить к нему какую-нибудь периферию. В конце концов, ПЛИС, как Москва — нерезиновая и ее ресурсы нужно использовать экономно. То есть «Другого нет у нас пути. В руках у нас винтовка.», кроме как осваивать процессоры, хорошие и разные.
Портировать марсоходовский вариант на свою плату с наскоку не удалось из-за различия в типах памяти которые доступны на чипах. Там «Макс» с флэшью и последовательным доступом, у меня «Циклон» с рамом и параллельным. Нужно либо прикрутить интерфейс к памяти, либо модифицировать ядро. Пока не понятно как эту задачу решить.
Я решил поискать другие варианты и недавно мне попался один удобный для изучения с нуля процессор — J1. Как непрофессионал, не могу оценить его в плане возможностей, могу сказать только, что авторы создавали его для решения практических задач. Если я правильно понял, то этот процессор используется для управления камерами на подобном роботе. Для скачивания доступен проект для для платы XESS FPGA board, в котором есть исходники SoC на основе J1 и кросскомпилятор.(j1demo.tar.gz) Предполагается, что компилятор должен работать везде, где присутствуют gforth и python. Подробное описание процессора находится в этом документе.
Исходный код ядра был написан для чипов Xilinx в нем задействуются библиотечные модули для настройки памяти.

`define RAMS 3

  genvar i;

`define w (16 >> `RAMS)
`define w1 (`w - 1)

  generate 
    for (i = 0; i < (1 << `RAMS); i=i+1) begin : ram
      // RAMB16_S18_S18
      RAMB16_S2_S2
      ram(
        .DIA(0),
        // .DIPA(0),
        .DOA(insn[`w*i+`w1:`w*i]),
        .WEA(0),
        .ENA(1),
        .CLKA(sys_clk_i),
        .ADDRA({_pc}),

        .DIB(st1[`w*i+`w1:`w*i]),
        // .DIPB(2'b0),
        .WEB(_ramWE & (_st0[15:14] == 0)),
        .ENB(|_st0[15:14] == 0),
        .CLKB(sys_clk_i),
        .ADDRB(_st0[15:1]),
        .DOB(ramrd[`w*i+`w1:`w*i]));
    end
  endgenerate 

Не знаю почему память конфигурируется именно так — делится на восемь блоков вместо того, чтобы сделать один. Наверно это связано со способом организации памяти на чипах Xilinx. Не зря же в существует такой вот даташит с описанием нескольких десятков вариантов модулей памяти.
Для переноса кода на ПЛИС Альтеры естественно нужно изменить этот участок кода. Сначала я оставил все как есть — с разделением на 8 блоков, но уперся в то, что все сгенерированные блоки ссылались на один и тот же mif файл, который вряд ли бы корректно записался в эту память потому, что у них у всех шина данных два бита. У Xilinx это похоже решается путем описания 8-ми 2-х битных шин как одной 16-и битной в файле с расширением bmm. У Альтеры я аналога не нашел и поэтому плюнул и вставил один блок. Вроде на работоспособности это никак не отразилось.

parameter RAMS = 16;
parameter w = RAMS - 1;

  generate 
      RAM16
      ram(
        .data_a(0),
        .q_a(insn[w:0]),
        .wren_a(0),
        .en_a(1),
        .clock_a(sys_clk_i),
        .address_a({_pc}),

        .data_b(st1[w:0]),
        .wren_b(_ramWE & (_st0[15:14] == 0)),
        .en_b(|_st0[15:14] == 0),
        .clock_b(sys_clk_i),
        .address_b(_st0[15:1]),
        .q_b(ramrd[w:0]));
  endgenerate

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

module top (input        sys_clk,
            input  [7:0]  sw_bus,
            output [7:0]     led);

  reg sw_en;
  wire j1_io_rd;
  wire j1_io_wr;
  wire [15:0] j1_io_addr;
  reg  [15:0] j1_io_din;
  wire [15:0] j1_io_dout;

  
  reg [7:0] led_reg; assign led = led_reg;

  reg [10:0] reset_count = 1000;
  wire sys_rst_i = |reset_count;
   
  always @(posedge sys_clk) begin
    if (sys_rst_i)
      reset_count <= reset_count - 1;
  end

  j1 j1(
       // Inputs
       .sys_clk_i(sys_clk),
       .sys_rst_i(sys_rst_i),

       .io_rd(j1_io_rd),
       .io_wr(j1_io_wr),
       .io_addr(j1_io_addr),
       .io_din(j1_io_din),
       .io_dout(j1_io_dout)
       );


  always @(posedge sys_clk)
  begin
    if (j1_io_wr) begin
      case (j1_io_addr)
      16'h4000: led_reg <= j1_io_dout;
      endcase
    end
  end

  always @*
  begin
    case (j1_io_addr)
      16'h5000: j1_io_din = sw_bus;
	endcase
  end
  
endmodule 


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

1@1-� /cygdrive/c/Users/1/Desktop/FPGA/projects/BANK/C3/SoC/J1/code/J1_translator
$ cat test.j1 # cat, само собой, не обязателен \\
                     просто демонстрация содержимого файла
\\      ***
h#5000 [T]    \\ Помещаем в стек данных литерал(адресс),
              \\ считываем значение по данному адресу.
              \\ В данном случае это порт ввода/вывода
              \\ (в процессоре используется ввод/вывод с отображением в память).
              \\ В стеке остается считанное значение.
h#4000 N->[T] \\ Добавляем еще один литерал с адресом порта, к которому
              \\ подключены светодиоды. Записываем значение в порт.
\\      ***

1@1-� /cygdrive/c/Users/1/Desktop/FPGA/projects/BANK/C3/SoC/J1/code/J1_translator
$ python j1t.py test.j1 #Вывод на экран 
    0000 : D000;
    0001 : 6C00;
    0002 : C000;
    0003 : 6020;

1@1-� /cygdrive/c/Users/1/Desktop/FPGA/projects/BANK/C3/SoC/J1/code/J1_translator
$ python j1t.py test.j1 > test.hex  #Вывод в текстовый файл

Вывод в файл я пока не делал. Можно применить копипасту или применить древнее кун-фу с перенаправлением вывода.
  • 0
  • 15 декабря 2011, 21:22
  • digides
  • 1
Файлы в топике: top (2).zip

Комментарии (6)

RSS свернуть / развернуть
reset_count это как я понял часть от watchdog системы.
0
  • avatar
  • wowa
  • 15 декабря 2011, 23:39
Нет.
Смотрим код.
reg [10:0] reset_count = 1000;
wire sys_rst_i = |reset_count;
always @(posedge sys_clk) begin
if (sys_rst_i)
reset_count <= reset_count — 1;
end
При включении поднимается ресет, отсчитывается 1000 тактов и ресет снимается.
В переменную reset_count при запуске ПЛИС пишется 1000, и пока reset_count не равно нулю, держим ресет в высоком уровне (wire sys_rst_i = |reset_count;) и делаем декремент (if (sys_rst_i) reset_count <= reset_count — 1;)
|reset_count означает логическое ИЛИ между всеми битами reset_count и равно нулю, когда все биты reset_count равны нулю
0
Так же это просто цифровая версия конденсатора на ресету? :) Я просто не изучал каким уровнем оно ресетует. Я похожим способом делал watchdog…
0
Да-да, вы правы. Это чисто задержка запуска процессора при подаче питания
0
То есть, задача этого фрагмента — гарантированно привести схему в исходное состояние при старте?
0
Ну, в принципе, да, но тут немного другой смысл.
На плате, помимо самой ПЛИС, может быть что угодно. Акселерометр, память какая-нибудь, АЦП. Примеров масса.
У меня вон стоит акселерометр с интерфейсом SPI. У него по даташиту есть некоторый период времени после подачи питания, который следует переждать.
Ну так вот, включаем плату, ПЛИС загружается и ждет 1000 тактов(чтобы все остальные устройства успели включиться)
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.