プチパンチカードコンピュータを作る

# はじめに
昔は、紙に穴をあけてソースコードとしていたそうだ。実物は見たこともないが、おもちゃくらいのものなら作れるかな、と思ったので作ってみることにした。
実際のパンチカードコンピュータの仕様等一切調べていない。こんな感じかなー、っと作ってみた。

# 基本的なアイディア
複雑なパンチカードを作るのは難しいので、出来るだけコンパクトな言語仕様がよい。それには、Brainf\*ckが使えそうである。
Brainf\*ckは8つの文字でコードを記述できる。ということは、1行あたり穴をあける位置を3か所用意すれば、穴の開いている/いないで、Brainf\*ckの任意の命令を表現出来る($2^3=8$)。
コードを読み込むタイミングを制御するために（つまりクロックのようなもの）、もう一か所穴をあけることにして、1行あたり計4か所穴をあける場所を作ることにする。
インタプリタはArduino上で実行する。手元にProMicroがあったため、これを利用することにする。

# 穴の有無を検知する
フォトリフレクタRPR-220を使って穴の有無を検知する。
これは図のように、赤外線LEDとフォトトランジスタがペアになっているデバイスである。
![20240203_3207252.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520857/2d991392-55ef-842e-a8b3-ffa14a3cf2cb.png)
これを次のようにしてProMicroのアナログピンへ入力する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520857/e728d1dc-afe3-f4f1-3541-07999bd456bc.png)
白い物体があれば、光が反射してフォトトランジスタに大電流が流れ、+5Vとなる。

# 実行結果の表示
結果の表示はシリアル通信でPCモニタに表示してもよかったのだが、スタンドアローン感を出すため、Amazonで購入した2個で1000円のOLEDディスプレイを使うことにした。
SSD1306というICを使って制御しているものらしい。
SSD1306Asciiというライブラリを使って文字列の表示が出来ると説明があるので、Arduino IDEでライブラリをインストールしてみた。
何の苦労もなく動いた。

# 外観
![20240325_220915311.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520857/ab68454f-0c89-c3cf-c463-5e124602d948.jpeg)
![20240325_220926986.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520857/0fa4c249-e574-145f-7328-c8bd24b01665.jpeg)

スイッチは内蔵プルアップ抵抗を利用しているので、そのままGNDに落とすだけ。スイッチを押すとコードを実行し、プログラムメモリをリセットする。

# ProMicroのコード
無駄な部分も多いし、Brainf\*ckを完全再現しているわけでもない。以前PCで作成したインタプリタを強引に移植しただけなので、気が向けば直すかも。
```c++
// white --> High (+5V), Black --> Low (0V)

#include "SSD1306Ascii.h"
#include "SSD1306AsciiAvrI2c.h"
// 0X3C+SA0 - 0x3C or 0x3D
#define I2C_ADDRESS 0x3C

// Define proper RST_PIN if required.
#define RST_PIN -1

SSD1306AsciiAvrI2c oled;

const int Nch = 4;
float v0[Nch];
float v1[Nch];
const float vth0[Nch] = {2.5, 2.5, 2.5, 2.5};
float vth[Nch] = {2.5, 2.5, 2.5, 2.5};
short clk0, clk;
short state[4] = {0,0,0,0};
bool readcode;
char codestr[256];
int nstr;

void setup() 
{
  pinMode(A9, INPUT_PULLUP);
  Serial.begin( 9600 );
  for(int ch=0; ch<Nch; ch++)
  {
    v0[ch] = 0;
    v1[ch] = 5.0;
  }
  clk0 = 0;
  readcode = false;
  nstr = 0;

  #if RST_PIN >= 0
  oled.begin(&Adafruit128x64, I2C_ADDRESS, RST_PIN);
#else // RST_PIN >= 0
  oled.begin(&Adafruit128x64, I2C_ADDRESS);
#endif // RST_PIN >= 0
  // Call oled.setI2cClock(frequency) to change from the default frequency.

  oled.setFont(Adafruit5x7);
}
float analog(int ch) // ch = 0 - 3
{
  int value;
  float volt;
  value = analogRead(18+ch);
  volt = value * 5.0 / 1023.0;
  return volt;
}

int read_1digit(float *vth) // 4chの出力を0 - 15で返す BlackをHighとして扱う
{
  int res = 0;
    
  for(int ch=0; ch<Nch; ch++)
  {
    float v = analog(ch);
    if(v < vth[ch])
    {
      res += (int)(pow(2, ch)+0.5);
    }  
 //   delay(10);
  }
  return res;
}

char int2bf(int num)
{
    char bf[9] = "><+-.,[]";
    return bf[num];
}

void int2bits(int num, short *bits) // 0-15 --> 4bit
{
  bits[0] = 0b0001 & num;
  bits[1] = (0b0010 & num) >> 1;
  bits[2] = (0b0100 & num) >> 2;
  bits[3] = (0b1000 & num) >> 3;
}

void read_debug()
{
  for(int ch=0; ch<4; ch++)
  {
    float v = analog(ch);
    Serial.print(v);
    Serial.print(" ");
  //  delay(10);
  }
  Serial.println("");
  delay(10);
}

void read_all_ch(float *v)
{
  for(int ch=0; ch<Nch; ch++)
  {
    v[ch] = analog(ch);
 //   delay(10);
  }
}

void print_info(float *vth)
{
  Serial.print("v0: ");
  for(int ch=0; ch<Nch; ch++)
  {
    Serial.print(v0[ch]);
    Serial.print(" ");
  }
  Serial.print("\n");
  
  Serial.print("v1: ");
  for(int ch=0; ch<Nch; ch++)
  {
    Serial.print(v1[ch]);
    Serial.print(" ");
  }
  Serial.print("\n");

  Serial.print("vth: ");
  for(int ch=0; ch<Nch; ch++)
  {
    Serial.print(vth[ch]);
    Serial.print(" ");
  }
  Serial.print("\n");
}

void operate(char *cmd)
{
  int ptr = 0;
  const int MEM = 256;
  char mem[MEM];
  for(int i=0;i<MEM;i++) mem[i] = 0;
 for(int i=0;i<nstr;i++)//命令を実行していく
    {
        //printf("%d¥n",i);
        switch(cmd[i])
        {
            case '>' :
                ptr++;
                if(ptr > MEM)
                {
                    printf("メモリエラー1¥n");
                    return -1;
                }
                break;
            case '<' :
                ptr--;
                if(ptr < 0)
                {
                    printf("%d",i);
                    putchar(cmd[i]);
                    oled.print(cmd[i]);
                    printf("メモリエラー2¥n");
                    return -1;
                }
                break;
            case '+' :
                mem[ptr]++;
                break;
            case '-' :
                mem[ptr]--;
                break;
            case '.' :
                putchar(mem[ptr]);
                oled.set2X();
                oled.print(mem[ptr]);
                oled.set1X();
                Serial.print(mem[ptr]);
                break;
            case ',' :
                mem[ptr] = getchar();
                break;
            case '[' :
                if(!mem[ptr])
                {
                    int n1 = 1;//[の個数
                    int n2 = 0;//]の個数
                    i++;
                    while(n1!=n2)//右に走査していき、[の個数=]の個数になったらbreak
                    {
                        if(cmd[i]==']') n2++;
                        else if(cmd[i]=='[') n1++;
                        i++;
                        if(i > MEM)
                        {
                            printf("対応する]がありません¥n");
                            return -1;
                        }
                    }
                    i--;
                }
                break;
            case ']' :
                if(mem[ptr])
                {
                    int n1 = 0;//[の個数
                    int n2 = 1;//]の個数
                    i--;
                    while(n1!=n2)//左に走査していき、[の個数=]の個数になったらbreak
                    {
                        if(cmd[i]==']') n2++;
                        else if(cmd[i]=='[') n1++;
                        i--;
                        if(i < 0)
                        {
                            printf("対応する[がありません¥n");
                            return -1;
                        }
                    }
                    i += 2;
                    i--;
                }
                break;
        } 
}
}

void loop()
{
  static int code_sum[4];
  static int code[3];
  static int NCR = 0;
  float v[4];

  int sum = 0;
  short bits[Nch];
  
  for(int ch=0; ch<Nch; ch++)
  {
    code_sum[ch] = 0;
  }
  int T = 20;
  int tth = 8;
  for(int t=0; t<T; t++)
  {
    int num = read_1digit(vth);
    int2bits(num, bits);
    for(int ch=0; ch<Nch; ch++)
    {
      code_sum[ch] += bits[ch];
    }
    
  }

  if(code_sum[0]>tth) clk = 1;
  else clk = 0;

  if(clk - clk0 == 1) // 立ち上がり
  {
   readcode = true; 
  // Serial.println("U");
  
   NCR = 0;
   
  }
  else if(clk - clk0 == -1) // 立ち下がり
  {
    readcode = false;
  //  Serial.println("D");
    int num = 0;
    for(int ch=1; ch<Nch; ch++)
    {
      if(code[ch-1] > NCR/2)
      {
        num += (int)(pow(2, ch-1) + 0.5);  
      }
     
    }
    char str = int2bf(num);
    Serial.println(str);
    oled.print(str);
    
     codestr[nstr++] = str;
     
   //  Serial.println((num));
    
    for(int i=0; i<3; i++)
    {
      code[i] = 0;
    }
    
  }
  else if(readcode)
  {
    NCR++;
   for(int ch=1; ch<Nch; ch++)
   {
    if(code_sum[ch] > tth)
    {
      code[ch-1]++;
    }
   } 
  }
  clk0 = clk;

  int vol = digitalRead(A9);
  if(!vol) // ボタンが押されたのでコードを実行する
  {
    codestr[nstr] = '\0';
    Serial.println("code:");
   Serial.println (codestr);
   oled.clear();
   oled.println("code:");
   oled.println(codestr);
  // oled.println("result:");
   operate(codestr);
  // Serial.print(operate(codestr));
   nstr = 0;
   codestr[0] = '\0';
   delay(1500);
  }
}
```

# パンチカードの作成
手軽さをとって、紙に白黒を印刷することにした。
手書きも大変なので、Jw_cadの外部変形を使って、Brainf\*ckのコード->パンチカードの変換を自動化した。
Jw_cadからは次のプログラムを呼び出す。
```c++
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <iostream>
using Pos = std::pair<double, double>;
const double d_punch = 15.24;
const double l_punch = 10.0;
const double L = 70.0;

void square(FILE *fp, const Pos &center, double w, double h)
{
    fprintf(fp, "sl ");
    int num = 0;
    
    Pos pos;
    int di = -1, dj = -1;
    pos.first = center.first + di * w/2.;
    pos.second = center.second + dj * h/2.;
    fprintf(fp, "%f %f ", pos.first, pos.second);

    di = 1, dj = -1;
    pos.first = center.first + di * w/2.;
    pos.second = center.second + dj * h/2.;
    fprintf(fp, "%f %f ", pos.first, pos.second);
    
    di = 1, dj = 1;
    pos.first = center.first + di * w/2.;
    pos.second = center.second + dj * h/2.;
    fprintf(fp, "%f %f ", pos.first, pos.second);
            
    di = -1, dj = 1;
    pos.first = center.first + di * w/2.;
    pos.second = center.second + dj * h/2.;
    fprintf(fp, "%f %f\n", pos.first, pos.second);
}
void square2(FILE* fp, const Pos& center, double w, double h)
{
    Pos pos[4];
    int di = -1, dj = -1;
    pos[0].first = center.first + di * w / 2.;
    pos[0].second = center.second + dj * h / 2.;

    di = 1, dj = -1;
    pos[1].first = center.first + di * w / 2.;
    pos[1].second = center.second + dj * h / 2.;

    di = 1, dj = 1;
    pos[2].first = center.first + di * w / 2.;
    pos[2].second = center.second + dj * h / 2.;

    di = -1, dj = 1;
    pos[3].first = center.first + di * w / 2.;
    pos[3].second = center.second + dj * h / 2.;

    for (int i = 0; i < 4; i++)
    {
        if(i!=3) fprintf(fp, "%f %f %f %f\n", pos[i].first, pos[i].second, pos[i+1].first, pos[i+1].second);
        else fprintf(fp, "%f %f %f %f\n", pos[i].first, pos[i].second, pos[0].first, pos[0].second);
    }
}

void punch(FILE *fp, unsigned char code, int No) // code: 0 ~ 7
{
    Pos pos(0, -No * d_punch);
    square(fp, pos, l_punch, l_punch);
  //  printf("no: %d\n", No);
   // pos.first += d_punch;

    for (int i = 0; i < 3; i++)
    {
        pos.first += d_punch;
        if (((code >> i) & 1) == 1)
        {
            square(fp, pos, l_punch, l_punch);
        }
    }
}

int bf2int(char c)
{
    char bf[9] = "><+-.,[]";
    for (int i = 0; i < 8; i++)
    {
        if (c == bf[i]) return i;
    }
    
}

int main()
{
    //std::string code("+++[->++++<]");
    std::string code("+[----->+++<]>+.+."); // hi
    int N = code.size();

    if (FILE* fp = fopen("JWC_TEMP.TXT", "w"))
    {
        Pos posg;
        posg.first = (3 * d_punch) / 2.;
        posg.second = -(N-1) * d_punch / 2.;
        square2(fp, posg, L, 2 * d_punch + (N - 1) * d_punch);

        for (int i = 0; i < N; i++)
        {
            punch(fp, bf2int(code[i]), i);
        }
        /*for (int no = 0; no < 8; no++)
        {
            punch(fp, no, no);
        }*/
        fclose(fp);
    }
}
```
出来たパンチカードは次のようなもの。
左列がクロックに対応する。実行すると、"hi"と表示する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520857/0fd73672-e454-d7f7-b1dd-e34eef937a33.png)

# 動作している様子
<iframe width="560" height="315" src="https://www.youtube.com/embed/DxeuHUz8UOQ?si=M-DxJu0Qpj8NHwvh" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

# 終わりに
一応形にはなったので一区切り。
コードは完全にはほど遠いので気が向けば直すかも。
本当は、フォトリフレクタのキャリブレーション機能も付けたかったのだが、今回はオミット。
フォトリフレクタと紙との距離をうまく取らないとコードの読み取りが安定しないので、そのあたりも課題か。