所有分类
主题 主题
平台 平台
我的工作台
userHead
注册时间 [[userInfo.create_time]]
创造力 [[userInfo.creativity]]
[[userInfo.remark]]
[[d.project_title]]
articleThumb
[[d.material_name]]
timelineThumb
进入工作台
折叠
所有分类 我的工作台
展开
恩尼格玛密码机
kalimov kalimov 2021-01-15 23:15:21
3
0
简单

凡事都有个动机

当看到REMAKE再造这个主题时,第一反应就是想到这个密码机。当初“从文字意义上进入创客圈”的其中一个作品就和它有关,虽然那是纯软件的算法。而网上爬虫搜索填充的各种科技文章中,写如何DIY恩尼格玛机的那篇,也许也加了来自oszine的,实际上是出自在下的翻译。话说当时闯北京的时候,这相当于半天房费的抵扣……

(英文原稿:https://www.instructables.com/Make-your-own-Enigma-Replica/

对加解密的兴趣,尤其是加密,主要来源于这是一种堂而皇之保密的技术。就比如我自己学英语的最大动机不是为了和老外交流,也并非为了看各种科技文档说明书,而是它能限制一部分人与我共享信息,而别人想要得知其中的秘密,就得付出学习成本或者翻译成本。同理,日语也是如此。总之,加密解密用起来,简直是“不管你懂不懂,反正我是懂了”这种贼兮兮偷着乐的乐趣。

现代常用的加密技术已经发展到非对称加密、公钥私钥这些算法,而加密学这学问,简单到只要拿着笔和纸就能开干。而稍微不怎么烧脑的经典对称密码当中,恩尼格玛绝对占据泰斗地位,它让加密解密变得像对话一样方便。特别在二战后期纳粹德国技术兵员打少减少的情况下,祭出一款配合电传打字机的恩尼格玛改型机,真将“自动化黑科技”和用户友好这两个完美结合在一起。

那就复刻吧。只是脑子当中有若干条复刻实现方式,实在是难以取舍。这里先挑一些特征性的和主干性的,让大家帮忙提个意见,觉得哪种比较合意。

这是一篇半互动的记录文,会充斥着各种细节考虑、推理过程。到最终定稿是否要修改成“卖相比较好”的规范文,还在纠结中。

先放上一幅恩尼格玛M4海军型机器的图,来源于密码博物馆。https://www.cryptomuseum.com/

projectImage

原理

恩尼格玛是对称密码算法的经典之一。每按一次按键,加密替换表会自动移位是它的关键。移位、替换这些方式在今天加密解密当中已经属于很基础的技巧,但是就如同“黑科技”是众多“白科技”叠加而来,叠加多了白的也变黑了那样,初代的恩尼格玛机器无论是材料上还是流程上,都决定了它的分量不轻,有50多公斤。哪怕后来军用版本简化了,到最后最轻便的海军型,也超过10公斤。这个分量块头当中,其实就包含了这部分不占重量的流程,需要占重量的结构零件来实现整个加解密过程。(具体详细流程可以找百科、百度上也能搜索到。在之后制作过程中,看时间我也会慢慢补全。)

·我们把加密过程看成一条算式,有点长。

每个密文字母=键盘跳线逆替换(第一位转子内字母逆替换(第二位转子内字母逆替换(第三位转子内字母逆替换(反射器(第三位转子内字母替换且如果第二位转子转满一周后自己就转一格(第二位转子内字母替换且如果第一位转子转满一周后自己就转一格((第一位转子内字母替换并转动一格(键盘跳线替换(每个明文字母))))))))))

有点头大了吧?我们缩短一下说法。

Cn=rp(r1(r2(r3(RF(R3(R2(R1((RP(Pn))))))))))    大写函数为正向,小写为逆向。RP是替换,R是转子,RF是反射器,C是密文,P是明文。

数一下括号,一共有9对。正好以反射器为中心两边流程一样,顺序对称。这在密码学当中实际上是犯忌的。假设指挥部一大清早就被生擒了,在没更改过转子顺序的情况下,当天所有电报就透明了。只要将无线电里监听到的密文顺着顺序打一遍,直接就能还原出明文。当然,考虑到用户友好,这个缺陷就被保留下来,反正当时德军一开始就把宝压在最初几个转子的复杂程度,一副根本不担心被盟军缴获的后果。

下面贴出从密码博物馆转载的简略电路流程,你就会惊叹,原来就用简单的开关回路就能完成这一系列转换。

projectImage

复刻方向

那么既然是REMAKE再造,用什么方式好呢?先说想到的三条路子。大家有什么意见的可以在评论区留言。

1.完全复刻古老德军版的密码机

基于上面的电路是如此那样的简单,完全可以撇开什么电子模块啊、硬件编程这些方式,光用激光切割机、3D打印机外加各种鳄鱼夹导线,用“三哥”布线法还是德国工程师布线法就看个人随意。为了注入灵魂,可以选用比较重的壳子、粗导线来增加重量。我能想象到的后果会是编程派深深的怨念。

2.外形和硬件编程相结合

就如同上面提到的我翻译过的教程原文那样,他拿Arduino Mega2560作为实现加密功能的算法零件。以我个人看法,拿Mega来做完全是浪费,并不能榨干这个板子的潜力。接线是比较好接了,甚至还能弄出个三四十年代没有的数码管冒充辉光管的效果。比起第一个,这似乎少了该有的灵魂。为了增加质感,在寻思着是不是要在里面放铅块水泥来配重……(如果这不是线上比赛,我可能干出把这五十多公斤的东西寄到浦软,到付……)

3.纯迷你小型化电子化

除了不是纯软件界面外,直接用开发板外加键盘,忽视所有复古外形,要多便携就多便携。这会让玩纯软算法的人狂喜,而且重量轻了,造价也低了。对比原始恩尼格玛当时的三万多美元,这东西完全在三十人民币内能打住。给多点制作经费甚至能造出阿瑟·谢尔比乌斯当时未成设想过的道路。

作为选择依据,我这里列出一些古老恩尼格玛没有的功能。

①加密解密时候密文或明文是用小灯泡显示的,在不加电传打字机的条件下并没有储存功能。

②电源采用4V直流电,可能是方便铅酸电池吧。而现在多数人使用的是5V、9V、12V。

③ 当时德国版本有29个字母按键,ASCII编码表当时完全没有影子。

④没有二极管,更没有三极管和芯片,连PCB电路板在当时也只是美军的专利。

⑤密码机复杂的程度看配套的转子数而定。量产下来一套就备有5个转子,然后跳出3个或4个使用。多几个转子在当时是要增加生产成本和复杂程度的。

个人选择可以采用的电路工艺

直插元器件,虽然比二战时期总体先进不少,还是有种古董收音机的感觉。

贴片元器件,能够紧凑尺寸,焊接也不是想象中那么困难。(一把热风枪外加锡浆就好)

 

经过若干小时的纠结、微信圈子内的各种意见,我采用了大部分类似第二种方向做法。为什么是“大部分类似”呢?之后看一张设计性能对比表就知道了。先说不采用其他两种的原因。

第一种最简单,我甚至都能想到如何减少重量加快进度的做法。在计算成本的时候就把它否决了,缺点在转子结构身上。即便使用了借助嘉立创生产PCB方式生产转子外形,把对方服务当成CNC来用,转子触点则是成本上升的主因——只能采用弹簧探针滑动,而每个探针价格比排母贵多了。

第三种其实最便宜,但是我不想做出这么“没有灵魂重量的东西”,就和手机APP恩尼格玛模拟器没差别了。

那么就说一下基于第二种思路而来的设计和上世纪原版恩尼格玛性能上的对比吧。(第一列是谢尔比乌斯的,第二列是我的。)


电源                     4V DC                                                                             5V DC,配边充边放电池模块

转子数量             海军型4个转子+1个反射器,陆军型3个转子+1个反射器 3个转子+1个反射器

备选转子数         5个                                                                                    5个存储在内存中,另可自行刷写26!(403,291,461,126,605,635,584,000,000,约403百亿兆)种可能的转子。

输入跳线板选择 0~13对两两字母交换方式                                                  0~13对两两字母交换方式

显示方式             灯泡对应密文显示                                                             LED对应密文显示,可选择用OLED输出

存储密文方式      另行笔记                                                                           搭载TF卡模块



在结构方面,上世纪的采用打字机方式的硬件外形结构,我这里用画电路板外加板件连接方式实现。将电路图用设计制版软件画好,预留板连接件——排针排母位置,用于结构连接和信号传输。,实现恩尼格玛的功能主体。事不宜迟,先从选材、电路等硬件方式叙述制作过程。作品一直在完善,所以现在先出个阶段性总结教程出来。

步骤1 步骤1
材料清单
材料清单 材料清单
1x
Arduino Nano
6x
74HC4067N
26x
10mm LED 白发红
27x
PBS-305复位按钮
33x
10K电阻
26x
1K电阻
26x
1N5819
26x
1N4730A
1x
有源蜂鸣器
1x
TP4067模块
60x
DC005插头5.5-2.1mm
52x
DC-022插座5.5-2.1mm
1x
DC-002插座
1x
USB-DC002电源线
1x
TF卡模块
1x
128MB TF卡
1x
1.3" OLED
2x
15P单排母
1x
4P单排母
1x
6P单排母
2x
6P单排针
1x
2*40P双排弯针
4x
2*3P双排母
2x
2*7P双排母
2x
2*6P双排母
6x
DIP-24插座
2x
SMTS-102C3
1x
锂电池400mAh
1x
PJ-313
6x
104瓷片电容
1x
470uF电解电容
6x
黑色导线
1x
PCB电路板(嘉立创打板)
1x
3mm厚木板(用于激光切割外壳)

这里可以弄个极限挑战。我自己做的时候,有部分用的是拆机件或者周边能搜集的零件。上面材料表里的只是参考出处。大家可以做个成本极限挑战。我的空电路板、外壳木板花费差不多90元,而上面这批东西的价格也就一百二十多元。(大的电路板做不起,还是在A4尺寸左右好了。)

步骤2 步骤2
电路图
附件 附件

这个电路总图比较复杂。大家可以看出26个字母键盘用了大量“按键+插座+二极管”的排列。具体设计思路在跳线板步骤说明。

步骤3 步骤3
插线板制作
projectImage
projectImage
projectImage
projectImage

跳线板的作用是将输入的按键预先进行一次字母交换达到初步迷惑的效果。在密码博物馆网站里,着重提到了跳线插头的结构。这个插头每端有两个接线柱,两边接线柱互相交叉绕接。而跳线板插座在没有插入跳线时,上下两个插座是导通信号的。当插入接线柱之后,接线柱把底部导通的弹簧板顶下,上下两个插孔不再导通信号,改走跳线,然后到达目的地的下插孔。这个设计,我直接想到的就是拿现在电子元件当中的DC插座替代,利用里面静触片和动触片之间的导通关系,配合DC电源插头做成跳线。不过,这个跳线里面只有一根导线,而不是现在DC线里的两根,所以我买插头,自行焊接导线。

德国人做的跳线插头外观比较好,密封外壳固定。我这里就简单用激光切割机切割木块进行固定,并标记好方向箭头。而用了立式三脚电源做的副产物,就是多出一批固定螺母闲置了。

projectImage
projectImage

在焊接完跳线板后,用万用电表测量上下两个排母位置是否导通,那一瞬间确实有点忐忑。万一当时检查设计图没看出来就损失大了……还好没发生。画线路图这件事,感觉真像在上海挖地铁……

附上这张跳线板的线路原理图。

步骤4 步骤4
主面板
projectImage

大家也许比较奇怪,为什么键盘的排列和常见的不同。这得考古……反正把Z放在键盘上排的,也只有德语输入法会这样,大家在电脑操作系统里可以试着找到这个历史的痕迹。我这里是仿古风,也就按着原版恩尼格玛键盘排列了。

左上角为重设按钮,按下去会将转子回归到初始设定状态。中间是OLED屏幕,现在设定为显示转子当前状态。中间是三排红色LED灯。

projectImage

同样为了仿古,我用激光切割机照着电路图标定的丝印范围,画了个切割模板。(其实是用来遮住下面电子元器件的猫腻。)

projectImage

为了更有仿古感觉,我选了工业上用的复位按钮,搞得好像古老打字机的机械键盘那样。然后把它们套在面板上。

projectImage
projectImage

焊接过程不表,整个恩尼格玛我设计的部分加起来就超过八百个焊点。这里看到的是键盘背后的下拉电阻、整流二极管、LED限流电阻、肖特基二极管。

旧版的恩尼格玛,因为是电流单一回路导通,是通过转子选择导通的灯泡,就不存在电流逆流问题。而我这里,为了榨干Arduino Nano的管脚性能,不光用了16通道复用器(74HC4067),就连输入输出的线路也共用了。这么一来,容易在轮询按键时候被反馈的电信号误导,导致一直处于“伪随机自动乱码输出”状态。为了解决这个问题,按键信号输入部分用3.3V输入,LED灯输出还是用5V电压,所以用整流二极管防止5V电压倒灌3.3V电压输出部分,用了肖特基二极管,防止3.3V输入对LED灯直接点亮(只有大于3.9V电压才能导通LED,而密文输出部分的电压是5V。即使如此,在按下按键时候还是会有很弱的辉光出现,不能排除是否元器件批次问题。)并且在LED灯点亮那端,我另外设置了两枚74HC4067复用器,用来检测到底是哪个LED被导通,用于写入TF卡存储数据。

projectImage

这是初步组装好的面板。不用上螺丝,那一堆复位按钮就能支撑起来。

步骤5 步骤5
主板

这里附上主板的原理图。

在主板上,安放着Arduino Nano,4个74HC4067复用器,蜂鸣器,输出耳机接口,初始化设定开关(只是这里还没加入这个功能),电源开关,内置电池等。

projectImage
projectImage
projectImage

将DIP-24插座、弯排针、TF模块座等焊接上去后,就可以把前面两大块安装起来,像图片里这个样子。后面之所以把Arduino Nano用杜邦线连接到排母上,是为了方便用万用电表检查系统总体的导通性。    

步骤6 步骤6
固件程序之前,设定出厂内置转子排列

和原版的恩尼格玛一样,出厂之前需要附带原始内置的转子。如果用单片机程序设置的话,只需要保存在内存即可。不少人会想,直接初始化的时候写入内存,但实际上是很浪费内存空间的做法。Arduino Nano的动态内存只有区区的2048字节,5个转子每个26字母下去,就用掉130字节,还有反射器另外26个字节。实际在运算过程中,这些转子是要时刻转动生成新的转子排列的,意味着这些就得翻倍……既然这些转子是开机就有的,“原厂自带”的,那我还不如直接写入不掉点保存的EEPROM里。虽然只有区区512字节空间,但足够做很多事。除了转子,日后升级菜单栏的时候还能把系统一些人性化设定写在里面。                                                               下面就附上一段小程序代码,可以用于将初始化转子数据写入EEPROM。每个转子的数组都经过随机排序。顺别介绍一个小技巧,使用搜索引擎搜索“在线随机排序”……                                     

代码 代码
	                    					#include <EEPROM.h>

char r1[26] = {10,2,1,16,15,4,9,21,8,22,19,0,23,7,18,5,25,14,12,20,6,17,3,11,24,13};
char r2[26] = {7,2,6,16,17,9,13,1,23,5,4,14,3,8,20,24,18,10,21,22,12,19,25,11,0,15};
char r3[26] = {12,4,22,20,25,2,3,17,1,15,0,10,14,5,7,18,9,8,6,11,24,13,16,23,19,21};
char r4[26] = {2,0,8,19,7,5,22,21,4,17,10,14,1,9,23,20,18,3,16,25,24,12,15,11,6,13};
char r5[26] = {5,17,3,19,15,11,9,2,1,21,7,0,4,16,22,6,14,25,18,10,12,8,24,13,23,20};
char ref[26] = {25,0,23,15,6,1,17,5,16,18,7,20,21,12,8,9,19,11,4,3,13,10,22,14,24,2};

void setup(){
  Serial.begin(9600);
  for (int i = 0; i <= 25; i++) {//转子数组五,每组廿六。另有反射器数组一,廿六员。依序排列,录入之。
    EEPROM.write(i, r1[i]);
    EEPROM.write(i+26, r2[i]);
    EEPROM.write(i+52, r3[i]);
    EEPROM.write(i+78, r4[i]);
    EEPROM.write(i+84, r5[i]);
    EEPROM.write(i+110, ref[i]);
  }
}

void loop()
{
  for (int i = 0; i <= 135; i++) {//反馈之
    Serial.print(EEPROM.read(i));
    Serial.print("\t");
  }
  Serial.println("");
}
	                    				
步骤7 步骤7
主程序部分

可能不少人会问我,为啥不用图形化编程。

这个回答也许比较伤人,不是不想用,而是用了图形化编程,速度更慢了……每次都找常用的模块就花费半天,复制粘贴摆位置比纯手打还费时间。而且,恩尼格玛里面的常量变量那么多,现有的图形化编程工具对变量类型分配实在是奢侈。我如果节省内存要做个布尔数组,在米思齐里就得用整数数组,每个元素就得花2个字节;而mind+脱胎于scratch 3(从扩展文件名.sb3可以看出来),则是用浮点数表示,更浪费,4个字节啊……就从这点来说,忍受不了内存的浪费。

还是拿“原始的”Arduino IDE进行代码编程(当然mind+也提供了手打代码编程,一样能用)。这个比喻有点像“手动挡终究比自动挡强”的论调……

为了方便阅读,我都加了注解。

代码 代码
	                    					#include <EEPROM.h> //动用EEPROM、SD卡、OLED显示屏的库文件
#include <SD.h>
#include "U8glib.h"
U8GLIB_SH1106_128X64 u8g(U8G_I2C_OPT_NO_ACK);

const String Key_mapping[] = {"Q","W","E","R","T","Z","U","I","O","A","S","D","F","G","H","J","K","P","Y","X","C","V","B","N","M","L"}; //键盘字符次序映射表
String final_save; //最终输出储存密文变量

byte Rotor1[26], Rotor2[26], Rotor3[26], Rotor4[26], Rotor5[26], Rotor6[26], Reflictor[26], Temp[26]; //转子、逆转子、反射器、临时数组
unsigned int counter; //字符计数器
byte Plain = 255; //明文变量
byte Crypto; //密文变量
boolean Key_pressed[26]; //按键状态数组变量
boolean Reflictor_Change = false; //用于初始设定是否有允许反射器和转子转动的设定
boolean Pressed_check = false; //总体按键状态变量
boolean Calculated = false; //加密流程锁变量

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  pinMode(1, OUTPUT);
  pinMode(2, OUTPUT);
  pinMode(3, OUTPUT);
  pinMode(4, OUTPUT);
  pinMode(5, OUTPUT);
  pinMode(6, OUTPUT);
  pinMode(7, OUTPUT);
  pinMode(8, OUTPUT);
  pinMode(9, OUTPUT);
  pinMode(10, OUTPUT);
  pinMode(A0, OUTPUT);
  pinMode(A1, OUTPUT);
  pinMode(A2, INPUT);
  pinMode(A3, INPUT);
  digitalWrite(A0, 0);
  digitalWrite(A1, 0);
  
  for (byte i = 0; i <= 25; i++) { //此循环用于抄录EEPROM内预装转子编码。
    Rotor1[i] = EEPROM.read(i); //有三个转子和一个反射器,各自都有26个字母,每次循环依次按间隔26个字节地址顺序读取。
    Rotor4[Rotor1[i]] = i; //同时生成几个转子各自的逆运算转子,也用于加密。
    Rotor2[i] = EEPROM.read(i+26);
    Rotor5[Rotor2[i]] = i;
    Rotor3[i] = EEPROM.read(i+52);
    Rotor6[Rotor3[i]] = i;
    Reflictor[i] = EEPROM.read(i+110);
  }

  if (!SD.begin(10)) { //报告TF卡是否就位
    Serial.println("Card failed");
    return;
  }
  Serial.println("card in"); 
  Serial.end(); //串口终结,其中的TX1口之后用于驱动蜂鸣器播放电码。
}

void Plain_Input_Polling(){ //用轮询监视字母按键按下情况。
  for (byte i = 0; i <= 15; i++){ //74HC4067十六通道复用器,以S0~S3收到的高低电平信号来决定某一刻哪个频道导通到公共信号输出端,然后单片机采集这个公共端的数据。为了精简代码节省内存,这里用了根据循环次数计数转化为S0~S3各自地址位是否通断的方式进行。
    digitalWrite(2, i % 2);
    digitalWrite(3, i / 2 % 2);
    digitalWrite(5, i /4 % 2);
    digitalWrite(4, i / 8);
    Key_pressed[i] = (analogRead(A6) >= 650 && analogRead(A6) < 800); //这一刻A6口监测模拟电平水平是否介于650~800之间(3.3V的作用下),记入按键状态数组内。
    if (i <= 9) Key_pressed[i + 16] = (analogRead(A7) >= 650 && analogRead(A7) <= 800); //如果循环次数小于9,则认为可以同时由A7口采集另一片74HC4067的数据,因为两者的S0~S3都是共用单片机的4个管脚的。记录的数据则编入第17~26个字母状态。
  }
}

void Key_To_Plain(){ //按键编号到明文生成的子程序
  Pressed_check = 0; //按键总体按下情况归零,这个变量用于检测26个按键是否有按下。
  for (byte i = 0; i <= 25; i++) {//轮询按键状态数组
    Pressed_check = Pressed_check || Key_pressed[i]; //将总体状态和按键状态某个元素进行或运算。只要后者曾经被按下,则总体状态就认定为有按键按下。
    if (Pressed_check == 1){ //如果有按键按下
      Plain = i; //明文就定义为这个按键编号,反正密码学搞得还是编号数字的学问。
      break; //跳出这个循环,反正已经有按键按下去了,怎么检测都是1.
    }
  }
  if (Pressed_check == 0) {//循环完了之后,如果还是检测不到按键。
    Plain = 255; //明文回归到初始设定状态
    Calculated = 0; //是否进行过加密过程的记录归零,允许一旦接到新的按键就启动加密过程。(避免按着按键不放然后一直自动加密下去。)
  }
  delay(2); //这一步是消抖消连短暂连击误判的代码。
}

void Rotor_Add(){ //这段子程序用于转子自增加数值
  for (byte i = 0; i <= 25; i++) { //首先将转子1转了一格的所有元素顺序存入临时数组内
    Temp[i] = Rotor1[(i + 1) % 26]; //其实就是取余运算
  }
  for (byte i = 0; i <= 25; i++) { //然后将转子1数组赋值为刚刚的临时数组。
    Rotor1[i] = Temp[i];
  }
  for (byte i = 0; i <= 25; i++) { //接着就把它的逆转子排列也生成出来
    Rotor4[Rotor1[i]] = i;
  }
  if (counter % 26 == 0) { //当转子1转了一周后,转子2转一格用来规避密钥重复。同样上面的操作。
    for (byte i = 0; i <= 25; i++) {
      Temp[i] = Rotor2[(i + 1) % 26];
    }
    for (byte i = 0; i <= 25; i++) {
      Rotor2[i] = Temp[i];
    }
    for (byte i = 0; i <= 25; i++) {
      Rotor5[Rotor2[i]] = i;
    }
  }
  if (counter % 676 == 0) { //当转子2也转了一周后,就轮到转子3转一格。同样上面的操作。
    for (byte i = 0; i <= 25; i++) {
      Temp[i] = Rotor3[(i + 1) % 26];
    }
    for (byte i = 0; i <= 25; i++) {
      Rotor3[i] = Temp[i];
    }
    for (byte i = 0; i <= 25; i++) {
      Rotor6[Rotor3[i]] = i;
    }
  }
  if (counter % 17576 == 0 && Reflictor_Change == 1) { //如果初始设定连反射器都可以转动的话,那么17576次转子转动后,就进位到反射器也转一格了。
    for (byte i = 0; i <= 25; i++) {
      Temp[i] = Reflictor[(i + 1) % 26];
    }
    for (byte i = 0; i <= 25; i++) {
      Reflictor[i] = Temp[i];
    }
  }
}

void Encrypt(){ //这一步是加密顺序流程
  Crypto = Rotor1[Plain]; //先将明文经过转子1生成第一阶段的密文
  Crypto = Rotor2[Crypto]; //然后依次经过转子2和转子3
  Crypto = Rotor3[Crypto];
  Crypto = Reflictor[Crypto]; //再经过反射器处理
  Crypto = Rotor6[Crypto]; //然后逆回转子3、2、1处理(为了便捷预先生成的逆转子数组用上了)
  Crypto = Rotor5[Crypto];
  Crypto = Rotor4[Crypto]; //最终得到密文,当然这个密文还是要经过跳线器的,那个步骤不用代码就能在LED上面显示。
}

void Crypto_Indicator(){ //这段子程序决定灯亮哪一盏
  digitalWrite(6, (Crypto % 16) % 2); //因为用的还是74HC4067复用器,所以还是可以用取整取余方式进行输出频道控制。
  digitalWrite(7, (Crypto % 16) / 2 % 2);
  digitalWrite(8, (Crypto % 16) / 4 % 2);
  digitalWrite(9, (Crypto % 16) / 8);
  digitalWrite(A0, !(Crypto / 16)); //如果字母序号小于16,则这一句就会让A0输出高电平(密文用16除,然后看商是0还是1。用非的原因是需要在小于16的时候输出高电平)
  digitalWrite(A1, Crypto / 16); //如果字母序号大于16,则这一句就会让A1输出高电平
}

void Crypto_save(){ //这一段子程序用于将处理密文结果输出到TF卡储存起来
  for (byte i = 0; i <= 15; i++){ //因为跳线器的缘故,之前子程序只负责输出灯光,但不知道最后到底是什么灯亮起来。根据电路,额外安装两个74HC4067,用来监测是否有5V电压信号流进LED(经过肖特基二极管,实际到达的电压也就3伏左右。)
    digitalWrite(2, i % 2); //和之前控制频道通断机制一样
    digitalWrite(3, i / 2 % 2);
    digitalWrite(5, i /4 % 2);
    digitalWrite(4, i / 8);
    if (digitalRead(A2) == 1){ //如果A2检测到有信号
      Crypto = i; //那就认为最终密文是循环序号,并跳出循环
      break;
    }
    if (i <= 9 && digitalRead(A3) == 1){ //在前十次循环中,如果A3检测到信号,则认为是后十个字母中其中一个是最终密文。
      Crypto = i + 16;
      break;
    }
  }
  final_save = Key_mapping[Crypto]; //因为电路板实际上的序号不是真正的字母序号,所以需要重新查询回键盘字符映射数组的实际字符元素。
  File dataFile = SD.open("OUTPUT.txt", FILE_WRITE); //然后将最终密文字母输出到储存卡里
  if (dataFile) {
    dataFile.print(final_save);
    dataFile.close();
  }
}

void Clear(){ //清理按键数据状态数组变量,等待用于下次轮询
  for (byte i = 0; i <= 25; i++){
    Key_pressed[i] = 0;
  }
}

void dot(){ //摩斯电码的短信号
  digitalWrite(1, HIGH);
  delay(80);
  digitalWrite(1, LOW);
  delay(80);
}

void dash(){ //摩斯电码的长信号
  digitalWrite(1, HIGH);
  delay(320);
  digitalWrite(1, LOW);
  delay(80);
}

void Morse_output(byte code){ //摩斯电码实际输出程序调用,这里已匹配好键盘字符次序映射表
  switch (code){
    case 0:
      dash();dash();dot();dash();
      break;
    case 1:
      dot();dash();dash();
      break;
    case 2:
      dot();
      break;
    case 3:
      dot();dash();dot();
      break;
    case 4:
      dash();
      break;
    case 5:
      dash();dash();dot();dot();
      break;
    case 6:
      dot();dot();dash();dash();
      break;
    case 7:
      dot();dot();
      break;
    case 8:
      dash();dash();dash();
      break;
    case 9:
      dot();dash();
      break;
    case 10:
      dot();dot();dot();
      break;
    case 11:
      dash();dot();dot();
      break;
    case 12:
      dot();dot();dash();dot();
      break;
    case 13:
      dash();dash();dot();
      break;
    case 14:
      dot();dot();dot();dot();
      break;
    case 15:
      dot();dash();dash();dash();
      break;
    case 16:
      dash();dot();dash();
      break;
    case 17:
      dot();dash();dash();dot();
      break;
    case 18:
      dash();dot();dash();dash();
      break;
    case 19:
      dash();dot();dot();dash();
      break;
    case 20:
      dash();dot();dash();dot();
      break;
    case 21:
      dot();dot();dot();dash();
      break;
    case 22:
      dash();dot();dot();dot();
      break;
    case 23:
      dash();dot();
      break;
    case 24:
      dash();dash();
      break;
    case 25:
      dot();dash();dot();dot();
      break;
  }
}

void loop(){ //主程序
  u8g.firstPage(); //OLED屏循环大嵌套
  do{ 
    u8g.setFont(u8g_font_unifont); //显示当前转子顺序状态
    u8g.setPrintPos(10,40);
    u8g.print("                ");
    u8g.setPrintPos(10,40);
    u8g.print(Key_mapping[Reflictor[0]]);
    u8g.setPrintPos(30,40);
    u8g.print(Key_mapping[Rotor3[0]]);
    u8g.setPrintPos(50,40);
    u8g.print(Key_mapping[Rotor2[0]]);
    u8g.setPrintPos(70,40);
    u8g.print(Key_mapping[Rotor1[0]]);
    Plain_Input_Polling(); //进行轮询
    Key_To_Plain(); //转化为明文
    if (Plain == 255) { //如果没有按键按下,则认为已经进行过加密,并关闭所有LED电压输出
      Calculated = 0; //将加密流程锁解除
      digitalWrite(A0, LOW);
      digitalWrite(A1, LOW);
    }
    else { //而有按键按下的时候
      if (Calculated == 0) { //且加密流程锁已经解除,则进行下列流程
      counter++; //字符计数增加1
      Rotor_Add(); //转子转动
      Encrypt(); //加密
      Crypto_Indicator(); //面板显示密文
      Crypto_save(); //将密文储存到TF卡里
      Morse_output(Crypto); //然后摩斯电码输出
      Calculated = 1; //将加密流程锁上锁
    }
      Clear(); //清理按键状态数组变量
    } while(u8g.nextPage());
}
	                    				
步骤8 步骤8
最终外观

时间仓促,这里只有微信视频片段资料。大家可以下载下来参考研究。

附件 附件
projectImage
projectImage
projectImage
projectImage

最后作为成品,用丙烯颜料涂成黑色,制造一种厚重感。(其实内里真的是空的,很想塞水泥凑足五十公斤,发顺丰到付……)

第一阶段的制作到此结束,还有后续迭代、改进的空间,欢迎大家砸砖各种不服。(有人说这不是坑了,简直就是悬崖等人跳和给自己跳的……)

Makelog作者原创文章,未经授权禁止转载。
3
0
评论
[[c.user_name]] [[c.create_time]]
[[c.parent_comment.count]]
[[c.comment_content]]