nedoPC.org

Electronics hobbyists community established in 2002
Atom Feed | View unanswered posts | View active topics It is currently 18 Mar 2024 20:56



Reply to topic  [ 31 posts ]  Go to page Previous  1, 2, 3  Next
TRCM - симулируем цифровое железо на C++ 
Author Message
Admin
User avatar

Joined: 08 Jan 2003 23:22
Posts: 22379
Location: Silicon Valley
Reply with quote
Вот более вменяемый тест - три полусумматора подключенные как инкремент трёх-битного числа:
Code:
#include "TRCMath.hpp"

using namespace std;

using namespace TRC;

class HalfAdder : public Entity
{
 protected:

// indecies:
  int iA,iB,iS,iC;

// inputs:
  Signal A,B;

// outputs:
  Signal S,C;

 public:

  HalfAdder(const char* s) : Entity(s)
  {

// empty constructor for generic unit

  }

  void step()
  {
    A = io(iA).read();
    B = io(iB).read();

    if(A==TRUE && B==TRUE)
       S = FALSE;
    else if(A==FALSE && B==TRUE)
       S = TRUE;
    else if(A==TRUE && B==FALSE)
       S = TRUE;
    else // if(A==FALSE && B==FALSE)
       S = FALSE;

    if(A==TRUE && B==TRUE)
       C = TRUE;
    else
       C = FALSE;

//    cout << name() << ":" << A << B << "->" << C << S << endl;

    io(iS) << S;
    io(iC) << C;
  }
};

class World : public Entity
{

// indecies:
  int i_increment,i_input,i_output,i_carry;

// internal counter:
  int counter;

 public:

  World() : Entity("World")
  {
    i_increment = at("INC");
    i_input     = at("I",3);
    i_output    = at("O",3);
    i_carry     = at("C2");

    counter = 0;
  }

  void step() // test cases
  {
    Wire<4> vec; // temporary vector
    vec[0] = io(i_output+0).read();
    vec[1] = io(i_output+1).read();
    vec[2] = io(i_output+2).read();
    vec[3] = io(i_carry).read();
    cout << "Case " << counter << " output=" << vec << endl;

    switch(counter++)
    {
       case 0:
       case 1:
       case 2:
       case 3:
          io(i_increment) << FALSE;
          io(i_input+0)   << TRUE;
          io(i_input+1)   << TRUE;
          io(i_input+2)   << TRUE;
          break;

       case 4:
       case 5:
       case 6:
       case 7:
          io(i_increment) << TRUE;
          io(i_input+0)   << TRUE;
          io(i_input+1)   << TRUE;
          io(i_input+2)   << TRUE;
          break;

       case 8:
       case 9:
       case 10:
       case 11:
          io(i_increment) << TRUE;
          io(i_input+0)   << FALSE;
          io(i_input+1)   << FALSE;
          io(i_input+2)   << FALSE;
          break;


    }
  }

};

int main()
{
  System *sys = System::getInstance();

  World world;

  class HalfAdder0 : public HalfAdder
  {
   public:
    HalfAdder0() : HalfAdder("HalfAdder0")
    {
      iA = at("INC");
      iB = at("I[0]");
      iS = at("O[0]");
      iC = at("C0");
    }
  } ha0;

  class HalfAdder1 : public HalfAdder
  {
   public:
    HalfAdder1() : HalfAdder("HalfAdder1")
    {
      iA = at("C0");
      iB = at("I[1]");
      iS = at("O[1]");
      iC = at("C1");
    }
  } ha1;

  class HalfAdder2 : public HalfAdder
  {
   public:
    HalfAdder2() : HalfAdder("HalfAdder2")
    {
      iA = at("C1");
      iB = at("I[2]");
      iS = at("O[2]");
      iC = at("C2");
    }
  } ha2;

  for(int i=0;i<=12;i++)
  {
    sys->prepare();
    ha0.step();
    ha1.step();
    ha2.step();
    world.step();
  }
}


Вывод программы:
Code:
INC <- World (idx=0)
I[0] <- World (idx=1)
I[1] <- World (idx=2)
I[2] <- World (idx=3)
O[0] <- World (idx=4)
O[1] <- World (idx=5)
O[2] <- World (idx=6)
C2 <- World (idx=7)
INC <- HalfAdder0 (idx=0)
I[0] <- HalfAdder0 (idx=1)
O[0] <- HalfAdder0 (idx=4)
C0 <- HalfAdder0 (idx=8)
C0 <- HalfAdder1 (idx=8)
I[1] <- HalfAdder1 (idx=2)
O[1] <- HalfAdder1 (idx=5)
C1 <- HalfAdder1 (idx=9)
C1 <- HalfAdder2 (idx=9)
I[2] <- HalfAdder2 (idx=3)
O[2] <- HalfAdder2 (idx=6)
C2 <- HalfAdder2 (idx=7)
Case 0 output=ZZZZ
Case 1 output=NNNN
Case 2 output=PPPN
Case 3 output=PPPN
Case 4 output=PPPN
Case 5 output=PPPN
Case 6 output=NPPN
Case 7 output=NNPN
Case 8 output=NNNP
Case 9 output=NNNP
Case 10 output=PPPN
Case 11 output=PNNN
Case 12 output=PNNN


Обратите внимание как считается 111+1:
Case 5 output=PPPN (0111 от старого результата)
Case 6 output=NPPN (0110)
Case 7 output=NNPN (0100)
Case 8 output=NNNP (1000)
Только на четвёртом такте бит переполнения прошёл через все три полусумматора :)

_________________
:dj: https://mastodon.social/@Shaos


15 Aug 2018 00:28
Profile WWW
Admin
User avatar

Joined: 08 Jan 2003 23:22
Posts: 22379
Location: Silicon Valley
Reply with quote
Всё таки решил добавить сишные макросы, чтобы спрятать кишки классов:
Code:
#define STRING(s) #s
#define INSTANCE(x,y) class x##y : public x { public: x##y() : x(STRING(x##y)) {
#define SUBINSTANCE(x,y) class x##y : public x { public: x##y(std::string s) : x(s.c_str()) {
#define NAMED(z) }}z


И вот тест на проверку 8-битного инкрементера, составленного из 8 двоичных полусумматоров:

Code:
#include "../TRCMath.hpp"
#undef DEBUG

#include <time.h>
#include <stdio.h>
#include <stdlib.h>

using namespace std;

using namespace TRC;

class HalfAdder : public Entity
{
 protected:

// indecies:
  int iA,iB,iS,iC;

// inputs:
  Signal A,B;

// outputs:
  Signal S,C;

 public:

  HalfAdder(const char* s) : Entity(s)
  {

// empty constructor for generic unit

  }

  void step()
  {
    A = io(iA).read();
    B = io(iB).read();

    if(A==TRUE && B==TRUE)
       S = FALSE;
    else if(A==FALSE && B==TRUE)
       S = TRUE;
    else if(A==TRUE && B==FALSE)
       S = TRUE;
    else // if(A==FALSE && B==FALSE)
       S = FALSE;

    if(A==TRUE && B==TRUE)
       C = TRUE;
    else
       C = FALSE;

//    cout << name() << ":" << A << B << "->" << C << S << endl;

    io(iS) << S;
    io(iC) << C;
  }
};

class World : public Entity
{

// indecies:
  int i_increment,i_input,i_output,i_carry;

 public:

// internal counter:
  long long counter;

  World() : Entity("World")
  {
    i_increment = at("INC");
    i_input     = at("I",8);
    i_output    = at("O",8);
    i_carry     = at("COUT");

    counter = 0;
  }

  void step() // test cases
  {
    Wire<9> vec; // temporary vector
    vec[8] = io(i_output+0).read();
    vec[7] = io(i_output+1).read();
    vec[6] = io(i_output+2).read();
    vec[5] = io(i_output+3).read();
    vec[4] = io(i_output+4).read();
    vec[3] = io(i_output+5).read();
    vec[2] = io(i_output+6).read();
    vec[1] = io(i_output+7).read();
    vec[0] = io(i_carry).read();
#ifdef DEBUG
    cout << "Case " << counter << " output=" << vec.binarize() << endl;
#endif

    io(i_increment) << TRUE;
    io(i_input+0)   << ((counter&(1<<4))?TRUE:FALSE);
    io(i_input+1)   << ((counter&(1<<5))?TRUE:FALSE);
    io(i_input+2)   << ((counter&(1<<6))?TRUE:FALSE);
    io(i_input+3)   << ((counter&(1<<7))?TRUE:FALSE);
    io(i_input+4)   << ((counter&(1<<8))?TRUE:FALSE);
    io(i_input+5)   << ((counter&(1<<9))?TRUE:FALSE);
    io(i_input+6)   << ((counter&(1<<10))?TRUE:FALSE);
    io(i_input+7)   << ((counter&(1<<11))?TRUE:FALSE);

    counter++;
  }

};

unsigned char BYTE = 0;

int main()
{
 unsigned long t1,t2;
 int i,n = 100000000;
 t1 = clock();
 for(i=0;i<n;i++)
 {
    BYTE++;
    BYTE++;
    BYTE++;
    BYTE++;
    BYTE++;
    BYTE++;
    BYTE++;
    BYTE++;
    BYTE++;
    BYTE++;
 }
 t2 = clock();

  System *sys = System::getInstance();

  World world;

  INSTANCE(HalfAdder,0);
      iA = at("INC");
      iB = at("I[0]");
      iS = at("O[0]");
      iC = at("C0");
  NAMED(ha0);

  INSTANCE(HalfAdder,1);
      iA = at("C0");
      iB = at("I[1]");
      iS = at("O[1]");
      iC = at("C1");
  NAMED(ha1);

  INSTANCE(HalfAdder,2);
      iA = at("C1");
      iB = at("I[2]");
      iS = at("O[2]");
      iC = at("C2");
  NAMED(ha2);

  INSTANCE(HalfAdder,3);
      iA = at("C2");
      iB = at("I[3]");
      iS = at("O[3]");
      iC = at("C3");
  NAMED(ha3);

  INSTANCE(HalfAdder,4);
      iA = at("C3");
      iB = at("I[4]");
      iS = at("O[4]");
      iC = at("C4");
  NAMED(ha4);

  INSTANCE(HalfAdder,5);
      iA = at("C4");
      iB = at("I[5]");
      iS = at("O[5]");
      iC = at("C5");
  NAMED(ha5);

  INSTANCE(HalfAdder,6);
      iA = at("C5");
      iB = at("I[6]");
      iS = at("O[6]");
      iC = at("C6");
  NAMED(ha6);

  INSTANCE(HalfAdder,7);
      iA = at("C6");
      iB = at("I[7]");
      iS = at("O[7]");
      iC = at("COUT");
  NAMED(ha7);

  printf("BYTE=0x%2.2X (%6.6fs or %2.2fns per increment)\n",BYTE,
        (double)(t2-t1)/CLOCKS_PER_SEC,
        (double)(t2-t1)/(n/1e8)/CLOCKS_PER_SEC
        );

  n = 0x100000;
  t1 = clock();
  while(world.counter!=n)
  {
    sys->prepare();
    ha0.step();
    ha1.step();
    ha2.step();
    ha3.step();
    ha4.step();
    ha5.step();
    ha6.step();
    ha7.step();
    world.step();
  }
  t2 = clock();

  printf("%4.4fs or %2.2fns per step\n",
         (double)(t2-t1)/CLOCKS_PER_SEC,
         (double)(t2-t1)/(n/1e9)/CLOCKS_PER_SEC
        );
}

Тут заодно проверка скорости идёт и сравнение с тупым инкрементом одного байта (unsigned char):

Code:
INC <- World (idx=0)
I[0] <- World (idx=1)
I[1] <- World (idx=2)
I[2] <- World (idx=3)
I[3] <- World (idx=4)
I[4] <- World (idx=5)
I[5] <- World (idx=6)
I[6] <- World (idx=7)
I[7] <- World (idx=8)
O[0] <- World (idx=9)
O[1] <- World (idx=10)
O[2] <- World (idx=11)
O[3] <- World (idx=12)
O[4] <- World (idx=13)
O[5] <- World (idx=14)
O[6] <- World (idx=15)
O[7] <- World (idx=16)
COUT <- World (idx=17)
INC <- HalfAdder0 (idx=0)
I[0] <- HalfAdder0 (idx=1)
O[0] <- HalfAdder0 (idx=9)
C0 <- HalfAdder0 (idx=18)
C0 <- HalfAdder1 (idx=18)
I[1] <- HalfAdder1 (idx=2)
O[1] <- HalfAdder1 (idx=10)
C1 <- HalfAdder1 (idx=19)
C1 <- HalfAdder2 (idx=19)
I[2] <- HalfAdder2 (idx=3)
O[2] <- HalfAdder2 (idx=11)
C2 <- HalfAdder2 (idx=20)
C2 <- HalfAdder3 (idx=20)
I[3] <- HalfAdder3 (idx=4)
O[3] <- HalfAdder3 (idx=12)
C3 <- HalfAdder3 (idx=21)
C3 <- HalfAdder4 (idx=21)
I[4] <- HalfAdder4 (idx=5)
O[4] <- HalfAdder4 (idx=13)
C4 <- HalfAdder4 (idx=22)
C4 <- HalfAdder5 (idx=22)
I[5] <- HalfAdder5 (idx=6)
O[5] <- HalfAdder5 (idx=14)
C5 <- HalfAdder5 (idx=23)
C5 <- HalfAdder6 (idx=23)
I[6] <- HalfAdder6 (idx=7)
O[6] <- HalfAdder6 (idx=15)
C6 <- HalfAdder6 (idx=24)
C6 <- HalfAdder7 (idx=24)
I[7] <- HalfAdder7 (idx=8)
O[7] <- HalfAdder7 (idx=16)
COUT <- HalfAdder7 (idx=17)
BYTE=0x00 (2.308623s or 2.31ns per increment)
1.7257s or 1645.74ns per step

С учётом того, что для 8-битного инкрементера распространение правильного значения проходит до 8 тактов мы считаем 1646ns*8=13168ns против 2.31ns у скомпилированной версии - в 5700 раз медленнее!!!

P.S. А если с ключами оптимизации собирать, то получается так:
Code:
-O1:
0.2054s or 195.87ns per step
-O2:
0.2087s or 199.00ns per step
-O3:
0.1822s or 173.73ns per step

почти в 10 раз быстрее чем было (и в 600 раз медленнее нативного байтового инкремента)

P.P.S. это получается около 700 тысяч байтовых инкрементов в секунду на моей машине - Intel 8080 в реальном времени не сэмулишь, но полюбому побыстрее будет чем в логисиме ;)

P.P.P.S. с другой стороны если эмулировать схему быстрого переноса, то икремент раза в 4 побыстрее должно быть, а так получилось почти 6 миллионов шагов симуляции в секунду ( для 18 точек коннекта, на одной коре что равносильно порядка 100 милионнов точек коннекта в секунду, но в теории можно и распараллелить на все доступные коры ; )

_________________
:dj: https://mastodon.social/@Shaos


16 Aug 2018 22:55
Profile WWW
Admin
User avatar

Joined: 08 Jan 2003 23:22
Posts: 22379
Location: Silicon Valley
Reply with quote
Вот чего существут из похожего кроме SystemC:
https://www.veripool.org/wiki/verilator - Verilator - транслятор из Verilog в C++ и SystemC
http://verilog2cpp.sourceforge.net/ - Verilog2C++ (по ссылке выше написано, что проект заброшен)
Надо попробовать их тоже...

P.S. вот тут достаточно простой тест скорости с цифрами:
https://www.veripool.org/boards/2/topics/1896-Verilator-Verilator-Speed
но чуваку отвечают, что его тест слишком простой, чтобы увидеть все чудеса ускорения Верилятора...

P.P.S. существует очень компактная реализация 8080 на Верилоге (с микрокодом):
https://opencores.org/project/light8080
Quote:
Xilinx XST on Spartan 3 (-5 grade):

204 LUTs plus 1 BRAM @ 80 MHz (optimized for area)
228 LUTs plus 1 BRAM @ 100 MHz (optimized for speed)
618 LUTs @ 53 MHz (optimized for area, no block ram)
надо её чтоли попробовать сконвертить да погонять...

P.P.P.S. есть более старая реализация 8080 потяжелее (без микрокода):
https://opencores.org/project/cpu8080

P.P.P.P.S. ещё есть софт-процы OPC (One Page Computing) - одна страница на верилоге для каждого процыка от OPC-1 до OPC-7:
https://hackaday.io/project/25357-opc-1-cpu-for-cpld
https://github.com/revaldinho/opc

_________________
:dj: https://mastodon.social/@Shaos


17 Aug 2018 16:10
Profile WWW
Admin
User avatar

Joined: 08 Jan 2003 23:22
Posts: 22379
Location: Silicon Valley
Reply with quote
Shaos wrote:
Короче - до этого момента я всегда все свои симуляшные движки писал отталкиваясь от идеи, что у нас есть входы и есть выходы, но в реальности всё не так - источники напряжений могут подключаться и отключаться от проводников (двунаправленные сигналы) - тот же FPGA например невозможно сэмулировать не имея возможности переподключаться то так, то эдак. Вобщем я решил идти от уже проверенных промышленных подходов (добавив немного своего троичного) - на проводник могут накладываться вот такие значения:
Code:
 const char TRUE     = 'P'; // прямое подключение к питанию
 const char MAYBE    = 'O'; // третье промежуточное состояние (для симуляции троичных схем)
 const char FALSE    = 'N'; // прямое подключение к земле
 const char NC       = 'Z'; // не подключено (высокий импенданс)
 const char PULLUP   = '1'; // слабая подтяжка к питанию
 const char PULLDOWN = '0'; // слабая подтяжка к земле
 const char INVALID  = '?'; // ошибка (конфликт)

Например на NC может наложиться PULLUP, на который может наложится скажем FALSE, а вот если на проводнике был FALSE, то при попытке применить к нему TRUE результирующее состояние получится INVALID

P.S. Возможно 'X' надо заменить скажем на знак вопроса, чтобы народ случайно не спутал с "любым значением", которое имеет смысл только в картах карно...

Наверное пока не поздно надо добавлять слабую подтяжку к промежуточному значению - PULLMID которая скажем символ '-' (минус)? Ну или '~' (тильда)

P.S. Любое значение 'X' кстати наверное таки надо добавить - для сравнений (и возможно следует разделить понятия "любое двоичное значение" и "любое троичное значение")

_________________
:dj: https://mastodon.social/@Shaos


17 Aug 2018 16:18
Profile WWW
Banned
User avatar

Joined: 29 Jun 2018 08:48
Posts: 413
Reply with quote
Shaos wrote:
следует разделить понятия "любое двоичное значение" и "любое троичное значение")

так или иначе - результатом всего этого будет: или "любое двоичное" или "любое".
любое значение вообще.

потому, как: почему троичная система, а не n-ричная, где n - бесконечность?


18 Aug 2018 00:46
Profile
Admin
User avatar

Joined: 08 Jan 2003 23:22
Posts: 22379
Location: Silicon Valley
Reply with quote
Клапауций wrote:
Shaos wrote:
следует разделить понятия "любое двоичное значение" и "любое троичное значение")

так или иначе - результатом всего этого будет: или "любое двоичное" или "любое".
любое значение вообще.

потому, как: почему троичная система, а не n-ричная, где n - бесконечность?

N-ричные системы где N>3 не имеют никаких преимуществ - только лишние сложности...

_________________
:dj: https://mastodon.social/@Shaos


18 Aug 2018 00:55
Profile WWW
Banned
User avatar

Joined: 29 Jun 2018 08:48
Posts: 413
Reply with quote
Shaos wrote:
N-ричные системы где N>3 не имеют никаких преимуществ - только лишние сложности...

ок. если теоретически N>3 не имеет смысла, то вопрос закрыт.


18 Aug 2018 01:33
Profile
Admin
User avatar

Joined: 08 Jan 2003 23:22
Posts: 22379
Location: Silicon Valley
Reply with quote
Клапауций wrote:
Shaos wrote:
N-ричные системы где N>3 не имеют никаких преимуществ - только лишние сложности...

ок. если теоретически N>3 не имеет смысла, то вопрос закрыт.

Я в том же 2010 когда нарисовал свой классический сдвоенный TRIMUX из двух DG403 также нарисовал четверичный мультиплексор на тех же DG403 - на каждую границу уровней надо 2 половинки DG403, т.е. чем больше "ричность" тем больше DG403-х...

_________________
:dj: https://mastodon.social/@Shaos


18 Aug 2018 09:48
Profile WWW
Admin
User avatar

Joined: 08 Jan 2003 23:22
Posts: 22379
Location: Silicon Valley
Reply with quote
Всё - перевалил за 1000 строк C++ кода :)

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

_________________
:dj: https://mastodon.social/@Shaos


18 Aug 2018 20:21
Profile WWW
Admin
User avatar

Joined: 08 Jan 2003 23:22
Posts: 22379
Location: Silicon Valley
Reply with quote
Попробую вручную переписать процык OPC1 с Верилога на C++
Code:
module opccpu( inout[7:0] data, output[10:0] address, output rnw, input clk, input reset_b);
   parameter FETCH0=0, FETCH1=1, RDMEM=2, RDMEM2=3, EXEC=4 ;
   parameter AND=5'bx0000,  LDA=5'bx0001, NOT=5'bx0010, ADD=5'bx0011;
   parameter LDAP=5'b01001, STA=5'b11000, STAP=5'b01000;
   parameter JPC=5'b11001,  JPZ=5'b11010, JP=5'b11011,  JSR=5'b11100;
   parameter RTS=5'b11101,  LXA=5'b11110;
   reg [10:0] OR_q, PC_q;
   reg [7:0]  ACC_q;
   reg [2:0]  FSM_q;
   reg [4:0]  IR_q;
   reg [2:0]  LINK_q; // bottom bit doubles up as carry flag
`define CARRY LINK_q[0]
   wire       writeback_w = ((FSM_q == EXEC) && (IR_q == STA || IR_q == STAP)) & reset_b ;
   assign rnw = ~writeback_w ;
   assign data = (writeback_w)?ACC_q:8'bz ;
   assign address = ( writeback_w || FSM_q == RDMEM || FSM_q==RDMEM2)? OR_q:PC_q;

   always @ (posedge clk or negedge reset_b )
     if (!reset_b)
       FSM_q <= FETCH0;
     else
       case(FSM_q)
         FETCH0 : FSM_q <= FETCH1;
         FETCH1 : FSM_q <= (IR_q[4])?EXEC:RDMEM ;
         RDMEM  : FSM_q <= (IR_q==LDAP)?RDMEM2:EXEC;
         RDMEM2 : FSM_q <= EXEC;
         EXEC   : FSM_q <= FETCH0;
       endcase

   always @ (posedge clk)
     begin
        IR_q <= (FSM_q == FETCH0)? data[7:3] : IR_q;
        // OR_q[10:8] is upper part nybble for address - needs to be zeroed for both pointer READ and WRITE operations once ptr val is read
        OR_q[10:8] <= (FSM_q == FETCH0)? data[2:0]: (FSM_q==RDMEM)?3'b0:OR_q[10:8];
        OR_q[7:0] <= data; //Lowest byte of OR is dont care in FETCH0 and at end of EXEC
        if ( FSM_q == EXEC )
          casex (IR_q)
            JSR    : {LINK_q,ACC_q} <= PC_q ;
            LXA    : {LINK_q,ACC_q} <= {ACC_q[2:0], 5'b0, LINK_q};
            AND    : {`CARRY, ACC_q}  <= {1'b0, ACC_q & OR_q[7:0]};
            NOT    : ACC_q <= ~OR_q[7:0];
            LDA    : ACC_q <= OR_q[7:0];
            LDAP   : ACC_q <= OR_q[7:0];
            ADD    : {`CARRY,ACC_q} <= ACC_q + `CARRY + OR_q[7:0];
            default: {`CARRY,ACC_q} <= {`CARRY,ACC_q};
          endcase
     end

   always @ (posedge clk or negedge reset_b )
     if (!reset_b) // On reset start execution at 0x100 to leave page zero clear for variables
       PC_q <= 11'h100;
     else
       if ( FSM_q == FETCH0 || FSM_q == FETCH1 )
         PC_q <= PC_q + 1;
       else
         case (IR_q)
           JP    : PC_q <= OR_q;
           JPC   : PC_q <= (`CARRY)?OR_q:PC_q;
           JPZ   : PC_q <= ~(|ACC_q)?OR_q:PC_q;
           JSR   : PC_q <= OR_q;
           RTS   : PC_q <= {LINK_q, ACC_q};
           default: PC_q <= PC_q;
         endcase
endmodule

У всех OPC есть тесты - так что можно легко проверить удачно сконвертилось или нет :roll:

_________________
:dj: https://mastodon.social/@Shaos


18 Aug 2018 21:16
Profile WWW
Admin
User avatar

Joined: 08 Jan 2003 23:22
Posts: 22379
Location: Silicon Valley
Reply with quote
Вобщем внутри каждого класса, описывающего пучёк проводов (Wire<N>, Uint<N>, Sint<N> и Tint<N>), каждый провод является символом в строке - т.е. их всегда можно заслать на печать т.к. это обычная сишная zero-terminated строка). Отсюда проблема - обращения к строке по индексу сначала (слева), а при представлении числа битами (тритами) как бы подразумевается, что слева самый старший, а справа - самый младший разряд. Получается строку надо переворачивать - вот я думаю переворачивать её только при печати в консоль и при создании объектов из строк, а внтури оно всё также будет идти от младшего к старшему. Пример:
Code:
Wire<5> w; // внутри "ZZZZZ"
w[0] = TRUE; // внутри "PZZZZ"
cout << w << endl; // тут должно будет напечатать "ZZZZP" (нулевой разряд - самый правый)
Wire<5> ww("PNNNN"); // внутри оно станет "NNNNP"
Uint<5> u = ww; // внутри оно всё также "NNNNP" (что будет означать двоичное число 1000)
Sint<5> s = ww; // внутри оно всё также "NNNNP" (что будет означать двоичное число 1000)
// и потом можно будет делать так:
cout << u << endl; // напечатает 16
cout << s << endl; // напечатает -16 (потому что старший разряд установлен в логическую единицу)

У кого какие соображения?

P.S. Всё сделал :mrgreen:
https://gitlab.com/ternary/trcm

P.P.S. Uint и Sint отличаются при конверсии из числовых типов си и в числовые типы (например для печати), а также тем в какой момент будет происходить переполнение при сложении/вычитании (Uint<16> например переполнится если сложить 65535 и 1 или вычесть 1 из 0, а Sint<16> если сложить 32767 и 1 или вычесть 1 из -32768)

_________________
:dj: https://mastodon.social/@Shaos


19 Aug 2018 14:58
Profile WWW
Admin
User avatar

Joined: 08 Jan 2003 23:22
Posts: 22379
Location: Silicon Valley
Reply with quote
Shaos wrote:
... ещё есть софт-процы OPC (One Page Computing) - одна страница на верилоге для каждого процыка от OPC-1 до OPC-7:
https://hackaday.io/project/25357-opc-1-cpu-for-cpld
https://github.com/revaldinho/opc

Пока вот как получается (комментарии с начала строки - это оригинальный верилоговский код OPC1):
Code:
// OPC1 test - based on https://github.com/revaldinho/opc/tree/master/opc1

#include <TRCMath.hpp>

using namespace std;

using namespace TRC;

// module opccpu( inout[7:0] data, output[10:0] address, output rnw, input clk, input reset_b); // SEE BELOW

class OPC1 : public Entity
{
  public:

//   parameter FETCH0=0, FETCH1=1, RDMEM=2, RDMEM2=3, EXEC=4 ;
   const unsigned FETCH0=0, FETCH1=1, RDMEM=2, RDMEM2=3, EXEC=4;

//   parameter AND=5'bx0000,  LDA=5'bx0001, NOT=5'bx0010, ADD=5'bx0011;
   const Wire<5> AND, LDA, NOT, ADD; // SEE BELOW

//   parameter LDAP=5'b01001, STA=5'b11000, STAP=5'b01000;
   const Wire<5> LDAP, STA, STAP; // SEE BELOW

//   parameter JPC=5'b11001,  JPZ=5'b11010, JP=5'b11011,  JSR=5'b11100;
   const Wire<5> JPC, JPZ, JP, JSR; // SEE BELOW

//   parameter RTS=5'b11101,  LXA=5'b11110;
   const Wire<5> RTS, LXA; // SEE BELOW

//   reg [10:0] OR_q, PC_q;
   Wire<11> OR_q;
   Uint<11> PC_q;

//   reg [7:0]  ACC_q;
   Uint<8> ACC_q;

//   reg [2:0]  FSM_q;
   Uint<3> FSM_q;

//   reg [4:0]  IR_q;
   Wire<5> IR_q;

//   reg [2:0]  LINK_q; // bottom bit doubles up as carry flag
   Wire<3> LINK_q;

//`define CARRY LINK_q[0]
#define CARRY LINK_q[0]

// IMPLEMENTATION OF INTERFACE (inout[7:0] data, output[10:0] address, output rnw, input clk, input reset_b);

   int i_data, i_address, i_rnw, i_clk, i_reset_b;

   OPC1() : Entity("OPC1"),
            // initialization for constant Wire<5>s:
            AND("X0000"), LDA("X0001"), NOT("X0010"), ADD("X0011"),
            LDAP("01001"), STA("11000"), STAP("01000"),
            JPC("11001"), JPZ("11010"), JP("11011"), JSR("11100"),
            RTS("11101"), LXA("11110")
   {
      // global attachements (we have the only instance of the class so it will work)
      i_data = at("DATA",8,PULLUP);
      i_address = at("ADDRESS",11);
      i_rnw = at("RNW");
      i_clk = at("CLK");
      i_reset_b = at("RESET_B");
   }

   void step()
   {

     Signal reset_b = io(i_reset_b).read();

//   wire       writeback_w = ((FSM_q == EXEC) && (IR_q == STA || IR_q == STAP)) & reset_b ;
     Signal writeback_w = ((FSM_q == EXEC) && (IR_q == STA || IR_q == STAP)) & reset_b ;

//   assign rnw = ~writeback_w ;
     Signal rnw = ~writeback_w;

//   assign data = (writeback_w)?ACC_q:8'bz ;
     Wire<8> data; // it's all Z by default
     if(writeback_w) data = ACC_q;
     else // below required for later logic
     {
        for(int i=0;i<8;i++) data[i] = io(i_data+i).read();
     }

//   assign address = ( writeback_w || FSM_q == RDMEM || FSM_q==RDMEM2)? OR_q:PC_q;
     Wire<11> address = ( (bool)writeback_w || FSM_q == RDMEM || FSM_q==RDMEM2 )? OR_q:PC_q;

//   always @ (posedge clk or negedge reset_b )
     if(posedge(i_clk) || negedge(i_reset_b))
     {

//     if (!reset_b)
       if (!reset_b)

//       FSM_q <= FETCH0;
         FSM_q = FETCH0;

//     else
       else
       {
//       case(FSM_q)
//         FETCH0 : FSM_q <= FETCH1;
         if(FSM_q == FETCH0) FSM_q = FETCH1;

//         FETCH1 : FSM_q <= (IR_q[4])?EXEC:RDMEM ;
         else if(FSM_q == FETCH1) FSM_q = (IR_q[4]==TRUE)?EXEC:RDMEM;

//         RDMEM  : FSM_q <= (IR_q==LDAP)?RDMEM2:EXEC;
         else if(FSM_q == RDMEM) FSM_q = (IR_q==LDAP)?RDMEM2:EXEC;

//         RDMEM2 : FSM_q <= EXEC;
         else if(FSM_q == RDMEM2) FSM_q = EXEC;

//         EXEC   : FSM_q <= FETCH0;
         else if(FSM_q == EXEC) FSM_q = FETCH0;

//       endcase
       }
     }

//   always @ (posedge clk)
     if(posedge(i_clk))

//     begin
     {

//        IR_q <= (FSM_q == FETCH0)? data[7:3] : IR_q;
          if(FSM_q == FETCH0)
          {
            IR_q[0] = data[3];
            IR_q[1] = data[4];
            IR_q[2] = data[5];
            IR_q[3] = data[6];
            IR_q[4] = data[7];
          }

//        // OR_q[10:8] is upper part nybble for address - needs to be zeroed
//        // for both pointer READ and WRITE operations once ptr val is read
//        OR_q[10:8] <= (FSM_q == FETCH0)? data[2:0]: (FSM_q==RDMEM)?3'b0:OR_q[10:8];
          if(FSM_q == FETCH0)
          {
             OR_q[8] = data[0];
             OR_q[9] = data[1];
             OR_q[10] = data[2];
          }
          else if(FSM_q == RDMEM)
          {
             OR_q[8] = FALSE;
             OR_q[9] = FALSE;
             OR_q[10] = FALSE;
          }

//        OR_q[7:0] <= data; //Lowest byte of OR is dont care in FETCH0 and at end of EXEC
          for(int i=0;i<8;i++) OR_q[i] = data[i];

//        if ( FSM_q == EXEC )
          if(FSM_q == EXEC)
          {

//          casex (IR_q)
//            JSR    : {LINK_q,ACC_q} <= PC_q ;
              if(IR_q==JSR)
              {
                 LINK_q = PC_q.part(8,10);
                 ACC_q  = PC_q.part(0,7);
              }

//            LXA    : {LINK_q,ACC_q} <= {ACC_q[2:0], 5'b0, LINK_q};
              else if(IR_q==LXA)
              {
                 LINK_q = ACC_q.part(0,2);
                 for(int i=7;i>=0;i--)
                 {
                     if(i>=3) ACC_q[i] = FALSE;
                     else ACC_q[i] = LINK_q[i];
                 }
              }

//            AND    : {`CARRY, ACC_q}  <= {1'b0, ACC_q & OR_q[7:0]};
              else if(IR_q==AND)
              {
                 CARRY = FALSE;
                 ACC_q = ACC_q & OR_q.part(0,7);
              }

//            NOT    : ACC_q <= ~OR_q[7:0];
              else if(IR_q==NOT)
              {
                 ACC_q = ~OR_q.part(0,7);
              }

//            LDA    : ACC_q <= OR_q[7:0];
              else if(IR_q==LDA)
              {
                 ACC_q = OR_q.part(0,7);
              }

//            LDAP   : ACC_q <= OR_q[7:0];
              else if(IR_q==LDAP)
              {
                 ACC_q = OR_q.part(0,7);
              }

//            ADD    : {`CARRY,ACC_q} <= ACC_q + `CARRY + OR_q[7:0];
              else if(IR_q==ADD)
              {
                 ACC_q += OR_q.part(0,7);
                 if(CARRY==TRUE) ACC_q++;
                 if(ACC_q.overflow())
                      CARRY = TRUE;
                 else CARRY = FALSE;
              }

//            default: {`CARRY,ACC_q} <= {`CARRY,ACC_q};
//          endcase
          }

//     end
     }

//   always @ (posedge clk or negedge reset_b )
     if(posedge(i_clk) || negedge(i_reset_b))
     {

//     if (!reset_b) // On reset start execution at 0x100 to leave page zero clear for variables
       if(!reset_b)

//       PC_q <= 11'h100;
         PC_q = 0x100;

//     else
       else

//       if ( FSM_q == FETCH0 || FSM_q == FETCH1 )
         if(FSM_q==FETCH0 || FSM_q==FETCH1)

//         PC_q <= PC_q + 1;
           PC_q++;

//       else
         else
         {

//         case (IR_q)
//           JP    : PC_q <= OR_q;
             if(IR_q==JP)
             {
                PC_q = OR_q;
             }

//           JPC   : PC_q <= (`CARRY)?OR_q:PC_q;
             else if(IR_q==JPC)
             {
                PC_q = (CARRY==TRUE)?OR_q:PC_q;
             }

//           JPZ   : PC_q <= ~(|ACC_q)?OR_q:PC_q;
             else if(IR_q==JPZ)
             {
                PC_q = (ACC_q==0)?OR_q:PC_q;
             }

//           JSR   : PC_q <= OR_q;
             else if(IR_q==JSR)
             {
                PC_q = OR_q;
             }

//           RTS   : PC_q <= {LINK_q, ACC_q};
             else if(IR_q==RTS)
             {
                for(int i=10;i>=0;i--)
                {
                   if(i>=8) PC_q[i]=LINK_q[i-8];
                   else PC_q[i]=ACC_q[i];
                }
             }

//           default: PC_q <= PC_q;
//         endcase
         }

         if(writeback_w)
         {
             for(int i=0;i<8;i++) io(i_data) << data[i];
         }
         for(int i=0;i<11;i++) io(i_address) << address[i];
         io(i_rnw) << rnw;
      }

//endmodule
   }
};

_________________
:dj: https://mastodon.social/@Shaos


20 Aug 2018 00:46
Profile WWW
Admin
User avatar

Joined: 08 Jan 2003 23:22
Posts: 22379
Location: Silicon Valley
Reply with quote
В коде OPC1 есть проблема - там аж ТРИ блока always @ (posedge clk ... которые как бы параллельно исполняются - там читаются и пишутся по сути один и тот же набор регистров. Это значит что выполнять эти блоки последовательно нельзя т.к. более поздняя логика будет использовать уже измененные в ранней логике значения, а оно всё должно меняться одномоментно - по изменению CLK из нуля в "1". Значит придётся хитрить и мухлевать, например вот так:
Code:
// OPC1 test - based on https://github.com/revaldinho/opc/tree/master/opc1

#include <TRCMath.hpp>

using namespace std;

using namespace TRC;

// module opccpu( inout[7:0] data, output[10:0] address, output rnw, input clk, input reset_b); // SEE BELOW

class OPC1 : public Entity
{
  public:

//   parameter FETCH0=0, FETCH1=1, RDMEM=2, RDMEM2=3, EXEC=4 ;
   const unsigned FETCH0=0, FETCH1=1, RDMEM=2, RDMEM2=3, EXEC=4;

//   parameter AND=5'bx0000,  LDA=5'bx0001, NOT=5'bx0010, ADD=5'bx0011;
   const Wire<5> AND, LDA, NOT, ADD; // SEE BELOW

//   parameter LDAP=5'b01001, STA=5'b11000, STAP=5'b01000;
   const Wire<5> LDAP, STA, STAP; // SEE BELOW

//   parameter JPC=5'b11001,  JPZ=5'b11010, JP=5'b11011,  JSR=5'b11100;
   const Wire<5> JPC, JPZ, JP, JSR; // SEE BELOW

//   parameter RTS=5'b11101,  LXA=5'b11110;
   const Wire<5> RTS, LXA; // SEE BELOW

//   reg [10:0] OR_q, PC_q;
   Wire<11> OR_q,OR_q_REG;
   Uint<11> PC_q,PC_q_REG;

//   reg [7:0]  ACC_q;
   Uint<8> ACC_q,ACC_q_REG;

//   reg [2:0]  FSM_q;
   Uint<3> FSM_q,FSM_q_REG;

//   reg [4:0]  IR_q;
   Wire<5> IR_q,IR_q_REG;

//   reg [2:0]  LINK_q; // bottom bit doubles up as carry flag
   Wire<3> LINK_q,LINK_q_REG;
// SHOUD WE HAVE A MACROS REG(Wire<3>,LINK_q)?

//`define CARRY LINK_q[0]
#define CARRY LINK_q[0]
// BUT FOR THE LEFT SIDE WE NEED TO USE TEMPORARY COPY!!!
#define CARRY_REG LINK_q_REG[0]

// TEMPORARY SIGNALS:
   Signal reset_b, writeback_w, rnw;
   Wire<11> address;
   Wire<8> data; // it's all Z by default

// IMPLEMENTATION OF INTERFACE (inout[7:0] data, output[10:0] address, output rnw, input clk, input reset_b);

   int i_data, i_address, i_rnw, i_clk, i_reset_b;

   OPC1() : Entity("OPC1"),
            // initialization for constant Wire<5>s:
            AND("X0000"), LDA("X0001"), NOT("X0010"), ADD("X0011"),
            LDAP("01001"), STA("11000"), STAP("01000"),
            JPC("11001"), JPZ("11010"), JP("11011"), JSR("11100"),
            RTS("11101"), LXA("11110")
   {
      // global attachements (we have the only instance of the class so it will work)
      i_data = at("DATA",8,PULLUP);
      i_address = at("ADDRESS",11);
      i_rnw = at("RNW");
      i_clk = at("CLK");
      i_reset_b = at("RESET_B");
   }

   void step()
   {

     reset_b = io(i_reset_b).read();

//   wire       writeback_w = ((FSM_q == EXEC) && (IR_q == STA || IR_q == STAP)) & reset_b ;
     writeback_w = ((FSM_q == EXEC) && (IR_q == STA || IR_q == STAP)) & reset_b ;

//   assign rnw = ~writeback_w ;
     rnw = ~writeback_w;

//   assign data = (writeback_w)?ACC_q:8'bz ;
     if(writeback_w) data = ACC_q;
     else // below required for later logic
     {
        for(int i=0;i<8;i++) data[i] = io(i_data+i).read();
     }

//   assign address = ( writeback_w || FSM_q == RDMEM || FSM_q==RDMEM2)? OR_q:PC_q;
     address = ( (bool)writeback_w || FSM_q == RDMEM || FSM_q == RDMEM2 )? OR_q:PC_q;

//   always @ (posedge clk or negedge reset_b )
     if(posedge(i_clk) || negedge(i_reset_b)) // WRITE FSM_q
     {

//     if (!reset_b)
       if (!reset_b)

//       FSM_q <= FETCH0;
         FSM_q_REG = FETCH0;

//     else
       else
       {
//       case(FSM_q)
//         FETCH0 : FSM_q <= FETCH1;
         if(FSM_q_REG == FETCH0) FSM_q = FETCH1;

//         FETCH1 : FSM_q <= (IR_q[4])?EXEC:RDMEM ;
         else if(FSM_q == FETCH1) FSM_q_REG = (IR_q[4]==TRUE)?EXEC:RDMEM;

//         RDMEM  : FSM_q <= (IR_q==LDAP)?RDMEM2:EXEC;
         else if(FSM_q == RDMEM) FSM_q_REG = (IR_q==LDAP)?RDMEM2:EXEC;

//         RDMEM2 : FSM_q <= EXEC;
         else if(FSM_q == RDMEM2) FSM_q_REG = EXEC;

//         EXEC   : FSM_q <= FETCH0;
         else if(FSM_q == EXEC) FSM_q_REG = FETCH0;

//       endcase
       }
     }

//   always @ (posedge clk)
     if(posedge(i_clk)) // WRITE IR_q, OR_q, LINK_q, ACC_q and CARRY

//     begin
     {

//        IR_q <= (FSM_q == FETCH0)? data[7:3] : IR_q;
          if(FSM_q == FETCH0)
          {
            IR_q_REG[0] = data[3];
            IR_q_REG[1] = data[4];
            IR_q_REG[2] = data[5];
            IR_q_REG[3] = data[6];
            IR_q_REG[4] = data[7];
          }

//        // OR_q[10:8] is upper part nybble for address - needs to be zeroed
//        // for both pointer READ and WRITE operations once ptr val is read
//        OR_q[10:8] <= (FSM_q == FETCH0)? data[2:0]: (FSM_q==RDMEM)?3'b0:OR_q[10:8];
          if(FSM_q == FETCH0)
          {
             OR_q_REG[8] = data[0];
             OR_q_REG[9] = data[1];
             OR_q_REG[10] = data[2];
          }
          else if(FSM_q == RDMEM)
          {
             OR_q_REG[8] = FALSE;
             OR_q_REG[9] = FALSE;
             OR_q_REG[10] = FALSE;
          }
          else
          {
             OR_q_REG[8] = OR_q[8];
             OR_q_REG[9] = OR_q[9];
             OR_q_REG[10] = OR_q[10];
          }

//        OR_q[7:0] <= data; //Lowest byte of OR is dont care in FETCH0 and at end of EXEC
          for(int i=0;i<8;i++) OR_q[i] = data[i];

//        if ( FSM_q == EXEC )
          if(FSM_q == EXEC)
          {

//          casex (IR_q)
//            JSR    : {LINK_q,ACC_q} <= PC_q ;
              if(IR_q==JSR)
              {
                 LINK_q_REG = PC_q.part(8,10);
                 ACC_q_REG  = PC_q.part(0,7);
              }

//            LXA    : {LINK_q,ACC_q} <= {ACC_q[2:0], 5'b0, LINK_q};
              else if(IR_q==LXA)
              {
                 LINK_q_REG = ACC_q.part(0,2);
                 for(int i=7;i>=0;i--)
                 {
                     if(i>=3) ACC_q_REG[i] = FALSE;
                     else ACC_q_REG[i] = LINK_q[i];
                 }
              }

//            AND    : {`CARRY, ACC_q}  <= {1'b0, ACC_q & OR_q[7:0]};
              else if(IR_q==AND)
              {
                 CARRY_REG = FALSE;
                 ACC_q_REG = ACC_q & OR_q.part(0,7);
              }

//            NOT    : ACC_q <= ~OR_q[7:0];
              else if(IR_q==NOT)
              {
                 ACC_q_REG = ~OR_q.part(0,7);
              }

//            LDA    : ACC_q <= OR_q[7:0];
              else if(IR_q==LDA)
              {
                 ACC_q_REG = OR_q.part(0,7);
              }

//            LDAP   : ACC_q <= OR_q[7:0];
              else if(IR_q==LDAP)
              {
                 ACC_q_REG = OR_q.part(0,7);
              }

//            ADD    : {`CARRY,ACC_q} <= ACC_q + `CARRY + OR_q[7:0];
              else if(IR_q==ADD)
              {
                 ACC_q_REG = ACC_q + (CARRY==TRUE)?1:0 + OR_q.part(0,7);
                 if(ACC_q.overflow())
                      CARRY_REG = TRUE;
                 else CARRY_REG = FALSE;
              }

//            default: {`CARRY,ACC_q} <= {`CARRY,ACC_q};
              else
              {
                 CARRY_REG = CARRY;
                 ACC_q_REG = ACC_q;
              }

//          endcase
          }

//     end
     }

//   always @ (posedge clk or negedge reset_b )
     if(posedge(i_clk) || negedge(i_reset_b)) // WRITE PC_q
     {

//     if (!reset_b) // On reset start execution at 0x100 to leave page zero clear for variables
       if(!reset_b)

//       PC_q <= 11'h100;
         PC_q_REG = 0x100;

//     else
       else

//       if ( FSM_q == FETCH0 || FSM_q == FETCH1 )
         if(FSM_q==FETCH0 || FSM_q==FETCH1)

//         PC_q <= PC_q + 1;
           PC_q_REG = PC_q + 1;

//       else
         else
         {

//         case (IR_q)
//           JP    : PC_q <= OR_q;
             if(IR_q==JP)
             {
                PC_q_REG = OR_q;
             }

//           JPC   : PC_q <= (`CARRY)?OR_q:PC_q;
             else if(IR_q==JPC)
             {
                PC_q_REG = (CARRY==TRUE)?OR_q:PC_q;
             }

//           JPZ   : PC_q <= ~(|ACC_q)?OR_q:PC_q;
             else if(IR_q==JPZ)
             {
                PC_q_REG = ((unsigned)ACC_q==0)?OR_q:PC_q;
             }

//           JSR   : PC_q <= OR_q;
             else if(IR_q==JSR)
             {
                PC_q_REG = OR_q;
             }

//           RTS   : PC_q <= {LINK_q, ACC_q};
             else if(IR_q==RTS)
             {
                for(int i=10;i>=0;i--)
                {
                   if(i>=8) PC_q_REG[i]=LINK_q[i-8];
                   else PC_q_REG[i]=ACC_q[i];
                }
             }

//           default: PC_q <= PC_q;
             else PC_q_REG = PC_q;

//         endcase
         }

         OR_q = OR_q_REG;
         PC_q = PC_q_REG;
         ACC_q = ACC_q_REG;
         FSM_q = FSM_q_REG;
         IR_q = IR_q_REG;
         LINK_q = LINK_q_REG;

         if(writeback_w)
         {
             for(int i=0;i<8;i++) io(i_data) << data[i];
         }
         for(int i=0;i<11;i++) io(i_address) << address[i];
         io(i_rnw) << rnw;
      }

//endmodule
   }
};

_________________
:dj: https://mastodon.social/@Shaos


20 Aug 2018 21:44
Profile WWW
Admin
User avatar

Joined: 08 Jan 2003 23:22
Posts: 22379
Location: Silicon Valley
Reply with quote
Короче суть мухлежа состоит в том, что каждый регистр, сохраняющий N сигналов, представляется парой пучков проводов:
Code:
Wire<11> OR_q,OR_q_REG;

и в обработчике шага эмуляции все изменения пишутся в *_REG и только в самом конце, когда все вычисления выполнены, *_REG копируют свои значения в основные переменные...

_________________
:dj: https://mastodon.social/@Shaos


22 Aug 2018 18:55
Profile WWW
Admin
User avatar

Joined: 08 Jan 2003 23:22
Posts: 22379
Location: Silicon Valley
Reply with quote
Чото я запустил вопрос - надо возвращаться в тему...

_________________
:dj: https://mastodon.social/@Shaos


13 Sep 2018 21:22
Profile WWW
Display posts from previous:  Sort by  
Reply to topic   [ 31 posts ]  Go to page Previous  1, 2, 3  Next

Who is online

Users browsing this forum: No registered users and 5 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to:  
Powered by phpBB® Forum Software © phpBB Group
Designed by ST Software.