BrainfuckPC: различия между версиями
Radiolok (обсуждение | вклад) |
Radiolok (обсуждение | вклад) |
||
(не показаны 2 промежуточные версии этого же участника) | |||
Строка 5: | Строка 5: | ||
Релейная логика | Релейная логика | ||
== | == Вкратце, о релейной логике == | ||
Электромагнитное реле представляет собой электромагнит с блоком контактов, установленных на подвижном якоре. При подаче напряжения на катушку электромагнита якорь примагничивается к сердечнику и переключает состояние контактов. Соединяя контакты нескольких реле в различных комбинациях, мы получим логический элемент того или иного типа. | Электромагнитное реле представляет собой электромагнит с блоком контактов, установленных на подвижном якоре. При подаче напряжения на катушку электромагнита якорь примагничивается к сердечнику и переключает состояние контактов. Соединяя контакты нескольких реле в различных комбинациях, мы получим логический элемент того или иного типа. | ||
Строка 102: | Строка 102: | ||
* Габаритные размеры: 1100 × 650 × 140 мм | * Габаритные размеры: 1100 × 650 × 140 мм | ||
* Масса: 25 кг | * Масса: 25 кг | ||
=== WaveForms === | |||
New insctruction fetching | |||
<wavedrom> | |||
{signal: [ | |||
{name : 'Clock', wave: '1L..........H'}, | |||
{name : 'Sequencer', wave: '0===========0', data : ['0','1','2','3','4','5','6','7','8','9','10']}, | |||
[ | |||
'Source sel', | |||
{name: 'PTR_SEL', wave: 'x1...........'}, | |||
{name: 'DATA_SEL', wave: 'x0...........'} | |||
], | |||
{}, | |||
['ALU Registers', | |||
[ 'TMP', | |||
{name: 'WR', wave: '01.0.........'}, | |||
{name: 'Data', wave: 'x.3..........', data : ["Old IP"]}, | |||
{name: 'RD', wave: '01........0..'}, | |||
], | |||
{}, | |||
['BIAS', | |||
{name: 'WR', wave: '0........1.0.', node: '.........f...'}, | |||
{name: 'Data', wave: 'x.........5..', data : ["New Bias"]}, | |||
{name: 'RD', wave: '0............'}, | |||
{name: 'Insn', wave: '3.........5..', data : ["Old Instruction", "New Instruction"]}, | |||
{name: 'Insn_RD', wave: '10..........1'}, | |||
] | |||
], | |||
{}, | |||
[ 'Adder', | |||
{name: 'Sign', wave: '0............'}, | |||
{name: 'CarryIn', wave: '01......0....'} | |||
], | |||
{}, | |||
['Context Registers', | |||
['IP', | |||
{name: 'WR', wave: '0.....1.0....'}, | |||
{name: 'Data', wave: '3......4.....', data : ["Old IP", "New IP"]}, | |||
{name: 'RD', wave: '01........0..', node: '.a........c'}, | |||
], | |||
{}, | |||
['AP', | |||
{name: 'WR', wave: '0............'}, | |||
{name: 'Data', wave: '4............', data :["Old AP"]}, | |||
{name: 'RD', wave: '0............'}, | |||
] | |||
], | |||
{}, | |||
['Memory', | |||
{name: '!RD/WR', wave: '1.......0..1.'}, | |||
{name: 'MemSync', wave: '0.......10...', node: '.........e......'}, | |||
{name: 'DATA LINE', wave: 'x........5.x.', data : ["New Insn"]}, | |||
{name: 'ADDRESS LINE', wave: 'x=.....4..x..', data : ["old IP", "new IP"], node : '.b........d'} | |||
], | |||
], | |||
edge:[ | |||
'a->b', 'c->d', 'e->f'] | |||
, | |||
head:{ | |||
text:'Impulse sequencer' | |||
}, | |||
foot:{ | |||
text:'Instruction fetching waveform', | |||
tock:-1 | |||
},} | |||
</wavedrom> | |||
New Data retrieval | |||
<wavedrom> | |||
{signal: [ | |||
{name : 'Clock', wave: '0H..........L'}, | |||
{name : 'Sequencer', wave: '0===========0', data : ['0','1','2','3','4','5','6','7','8','9','10']}, | |||
[ | |||
'Source sel', | |||
{name: 'PTR_SEL', wave: 'x0...........'}, | |||
{name: 'DATA_SEL', wave: 'x1...........'} | |||
], | |||
{}, | |||
['ALU Registers', | |||
[ 'TMP', | |||
{name: 'WR', wave: '01.0.........'}, | |||
{name: 'Data', wave: 'x.3..........', data : ["Old Data"]}, | |||
{name: 'RD', wave: '01........0..'}, | |||
], | |||
{}, | |||
['BIAS', | |||
{name: 'WR', wave: '0............', node: '.............'}, | |||
{name: 'Data', wave: '5............', data : ["Bias"]}, | |||
{name: 'RD', wave: '01........0..'}, | |||
{name: 'Insn', wave: '=............', data : ["Instruction", "New Instruction"]}, | |||
{name: 'Insn_RD', wave: '01..........0'}, | |||
] | |||
], | |||
{}, | |||
[ 'Adder', | |||
{name: 'Sign', wave: '0x..........0'}, | |||
{name: 'CarryIn', wave: '0=..........0', data :["Same As Sign"]} | |||
], | |||
{}, | |||
['Context Registers', | |||
['IP', | |||
{name: 'WR', wave: '0............'}, | |||
{name: 'Data', wave: '4............', data :["IP"]}, | |||
{name: 'RD', wave: '0............'}, | |||
], | |||
['AP', | |||
{name: 'WR', wave: '0............'}, | |||
{name: 'Data', wave: '3............', data : ["AP", "New AP"]}, | |||
{name: 'RD', wave: '01........0..'}, ], | |||
{name: 'Data RD', wave: '0......1.0...'} | |||
], | |||
{}, | |||
['Memory', | |||
{name: '!RD/WR', wave: '10.1.........'}, | |||
{name: 'MemSync', wave: '01.0....10...', node: '................'}, | |||
{name: 'DATA LINE', wave: 'x=.x...5.x.x.', data : ["Data", "New data"]}, | |||
{name: 'ADDRESS LINE', wave: 'x4........x..', data : ["AP", "new AP"]} | |||
], | |||
], | |||
edge:[ | |||
'a->b', 'c->d', 'e->f'] | |||
, | |||
head:{ | |||
text:'Impulse sequencer' | |||
}, | |||
foot:{ | |||
text:'ADD/SUB Instruction', | |||
tock:-1 | |||
}, | |||
} | |||
</wavedrom> | |||
Timing diagram for new Address fetching | |||
<wavedrom> | |||
{signal: [ | |||
{name : 'Clock', wave: '0H..........L'}, | |||
{name : 'Sequencer', wave: '0===========0', data : ['0','1','2','3','4','5','6','7','8','9','10']}, | |||
[ | |||
'Source sel', | |||
{name: 'PTR_SEL', wave: 'x1...........'}, | |||
{name: 'DATA_SEL', wave: 'x0...........'} | |||
], | |||
{}, | |||
['ALU Registers', | |||
[ 'TMP', | |||
{name: 'WR', wave: '01.0.........'}, | |||
{name: 'Data', wave: 'x.3..........', data : ["Old AP"]}, | |||
{name: 'RD', wave: '01........0..'}, | |||
], | |||
{}, | |||
['BIAS', | |||
{name: 'WR', wave: '0............', node: '.............'}, | |||
{name: 'Data', wave: '5............', data : ["Bias"]}, | |||
{name: 'RD', wave: '01........0..'}, | |||
{name: 'Insn', wave: '=............', data : ["Instruction", "New Instruction"]}, | |||
{name: 'Insn_RD', wave: '01..........0'}, | |||
] | |||
], | |||
{}, | |||
[ 'Adder', | |||
{name: 'Sign', wave: '0x..........0'}, | |||
{name: 'CarryIn', wave: '0=..........0', data :["Same As Sign"]} | |||
], | |||
{}, | |||
['Context Registers', | |||
['IP', | |||
{name: 'WR', wave: '0............'}, | |||
{name: 'Data', wave: '4............', data :["Old IP"]}, | |||
{name: 'RD', wave: '0............'}, | |||
], | |||
{}, | |||
['AP', | |||
{name: 'WR', wave: '0.....1.0....'}, | |||
{name: 'Data', wave: '3......4.....', data : ["Old AP", "New AP"]}, | |||
{name: 'RD', wave: '01........0..', node: '.a........c'}, | |||
] | |||
], | |||
{}, | |||
['Memory', | |||
{name: '!RD/WR', wave: '1............'}, | |||
{name: 'MemSync', wave: '0............', node: '................'}, | |||
{name: 'DATA LINE', wave: 'x..........x.'}, | |||
{name: 'ADDRESS LINE', wave: 'x=.....4..x..', data : ["old AP", "new AP"], node : '.b........d'} | |||
], | |||
], | |||
edge:[ | |||
'a->b', 'c->d', 'e->f'] | |||
, | |||
head:{ | |||
text:'Impulse sequencer' | |||
}, | |||
foot:{ | |||
text:'ADA/ADS Instruction', | |||
tock:-1 | |||
},} | |||
</wavedrom> | |||
Branch Not Taken | |||
<wavedrom> | |||
{signal: [ | |||
{name : 'Clock', wave: '0H..........L'}, | |||
{name : 'Sequencer', wave: '0===========0', data : ['0','1','2','3','4','5','6','7','8','9','10']}, | |||
[ | |||
'Source sel', | |||
{name: 'PTR_SEL', wave: 'x1....0'}, | |||
{name: 'DATA_SEL', wave: 'x0.....'} | |||
], | |||
{}, | |||
['ALU Registers', | |||
[ 'TMP', | |||
{name: 'WR', wave: '0......'}, | |||
{name: 'Data', wave: 'x......', data : ["Old IP"]}, | |||
{name: 'RD', wave: '0......'}, | |||
], | |||
{}, | |||
['BIAS', | |||
{name: 'WR', wave: '0......', node: '.............'}, | |||
{name: 'Data', wave: '5......', data : ["Bias"]}, | |||
{name: 'RD', wave: '01..0..'}, | |||
{name: 'Insn', wave: '=......', data : ["Instruction", "New Instruction"]}, | |||
{name: 'Insn_RD', wave: '01....0'}, | |||
] | |||
], | |||
{}, | |||
[ 'Adder', | |||
{name: 'Sign', wave: '0x....0'}, | |||
{name: 'CarryIn', wave: '0x....0', data :["Same As Sign"]} | |||
], | |||
{}, | |||
['Context Registers', | |||
['IP', | |||
{name: 'WR', wave: '0......'}, | |||
{name: 'Data', wave: '4......', data :["IP", "new IP"]}, | |||
{name: 'RD', wave: '0......'}, | |||
], | |||
['AP', | |||
{name: 'WR', wave: '0......'}, | |||
{name: 'Data', wave: '3......', data : ["AP", "New AP"]}, | |||
{name: 'RD', wave: '01.0...'}, ] | |||
], | |||
{}, | |||
['Memory', | |||
{name: '!RD/WR', wave: '1.0..1.'}, | |||
{name: 'MemSync', wave: '0.10...', node: '................'}, | |||
{name: 'DATA LINE', wave: 'x.=..x.', data : ["Data", "New data"]}, | |||
{name: 'ZF', wave :'x..=.x.', data : ["Zero Flag"]}, | |||
{name: 'ADDRESS LINE', wave: 'x.4.x..', data : ["AP", "IP"]} | |||
], | |||
], | |||
edge:[ | |||
'a->b', 'c->d', 'e->f'] | |||
, | |||
head:{ | |||
text:'Impulse sequencer' | |||
}, | |||
foot:{ | |||
text:'JZ/JNZ Instruction - Not taken condition', | |||
tock:-1 | |||
}, | |||
} | |||
</wavedrom> | |||
<wavedrom> | |||
{signal: [ | |||
{name : 'Clock', wave: '0H..........L'}, | |||
{name : 'Sequencer', wave: '0===========0', data : ['0','1','2','3','4','5','6','7','8','9','10']}, | |||
[ | |||
'Source sel', | |||
{name: 'PTR_SEL', wave: 'x1..........0'}, | |||
{name: 'DATA_SEL', wave: 'x0...........'} | |||
], | |||
{}, | |||
['ALU Registers', | |||
[ 'TMP', | |||
{name: 'WR', wave: '0...1.0......'}, | |||
{name: 'Data', wave: 'x...3........', data : ["Old IP"]}, | |||
{name: 'RD', wave: '01........0..'}, | |||
], | |||
{}, | |||
['BIAS', | |||
{name: 'WR', wave: '0............', node: '.............'}, | |||
{name: 'Data', wave: '5............', data : ["Bias"]}, | |||
{name: 'RD', wave: '01........0..'}, | |||
{name: 'Insn', wave: '=............', data : ["Instruction", "New Instruction"]}, | |||
{name: 'Insn_RD', wave: '01..........0'}, | |||
] | |||
], | |||
{}, | |||
[ 'Adder', | |||
{name: 'Sign', wave: '0x..........0'}, | |||
{name: 'CarryIn', wave: '0=..........0', data :["Same As Sign"]} | |||
], | |||
{}, | |||
['Context Registers', | |||
['IP', | |||
{name: 'WR', wave: '0........1.0.'}, | |||
{name: 'Data', wave: '4.........5..', data :["IP", "new IP"]}, | |||
{name: 'RD', wave: '0...1.......0'}, | |||
], | |||
['AP', | |||
{name: 'WR', wave: '0............'}, | |||
{name: 'Data', wave: '3............', data : ["AP", "New AP"]}, | |||
{name: 'RD', wave: '01.0.........'}, ] | |||
], | |||
{}, | |||
['Memory', | |||
{name: '!RD/WR', wave: '1.0.........1'}, | |||
{name: 'MemSync', wave: '0.10.........', node: '................'}, | |||
{name: 'DATA LINE', wave: 'x.=........x.', data : ["Data", "New data"]}, | |||
{name: 'ZF', wave :'x..=.......x.', data : ["Zero Flag state"]}, | |||
{name: 'ADDRESS LINE', wave: 'x.4.5.......x', data : ["AP", "IP"]} | |||
], | |||
], | |||
edge:[ | |||
'a->b', 'c->d', 'e->f'] | |||
, | |||
head:{ | |||
text:'Impulse sequencer' | |||
}, | |||
foot:{ | |||
text:'JZ/JNZ Instruction - Taken condition', | |||
tock:-1 | |||
}, | |||
} | |||
</wavedrom> | |||
<wavedrom> | |||
{signal: [ | |||
{name : 'W/~R', wave: 'x0....x'}, | |||
{name : 'Sync', wave: '0.1..0.'}, | |||
['Memory Signals', | |||
{name: 'CS2', wave: '0.1..0.'}, | |||
{name: '~OE', wave: 'x.0..x.'}, | |||
{name: '~WR/RD', wave: 'x.1..x.'}, | |||
], | |||
{}, | |||
['', | |||
{name: 'DATA_OUT_EN', wave: 'x1....x'}, | |||
{name: 'DATA_IN_EN', wave: '0......', node: '................'}, | |||
{name: '~ADDR_IN_EN', wave: '1.0..1.', data : ["Data", "New data"]}, | |||
{name: 'DATA_OUT_SET', wave: '0..10..', data : ["AP", "IP"]} | |||
], | |||
], | |||
head:{ | |||
text:'Memory access diagramm' | |||
}, | |||
foot:{ | |||
text:'Read cycle', | |||
tock:-1 | |||
}, | |||
} | |||
</wavedrom> | |||
<wavedrom> | |||
{signal: [ | |||
{name : 'W/~R', wave: 'x1....x'}, | |||
{name : 'Sync', wave: '0.1..0.'}, | |||
['Memory Signals', | |||
{name: 'CS2', wave: '0.1..0.'}, | |||
{name: '~OE', wave: '1......'}, | |||
{name: '~WR/RD', wave: '1..01..'}, | |||
], | |||
{}, | |||
['', | |||
{name: 'DATA_OUT_EN', wave: 'x0....x'}, | |||
{name: 'DATA_IN_EN', wave: '0.1..0.', node: '................'}, | |||
{name: '~ADDR_IN_EN', wave: '1.0..1.', data : ["Data", "New data"]}, | |||
{name: 'DATA_OUT_SET', wave: '0......', data : ["AP", "IP"]} | |||
], | |||
], | |||
head:{ | |||
text:'Memory access diagramm' | |||
}, | |||
foot:{ | |||
text:'Write cycle', | |||
tock:-1 | |||
}, | |||
} | |||
</wavedrom> | |||
=== To be continued === | === To be continued === | ||
Строка 114: | Строка 521: | ||
== Ссылки == | == Ссылки == | ||
* https://github.com/radiolok/brainfuckpc - репозиторий проекта со всеми схемами | |||
* https://github.com/radiolok/bfutils - эмулятор и компилятор | |||
* https://xakep.ru/2019/09/19/brainfuckpc/ - Статья в журнале Хакер | * https://xakep.ru/2019/09/19/brainfuckpc/ - Статья в журнале Хакер | ||
* https://habr.com/ru/post/402629/ - Мои маленькие реле: Brainfuck компьютер это магия (2017) | * https://habr.com/ru/post/402629/ - Мои маленькие реле: Brainfuck компьютер это магия (2017) | ||
* https://habr.com/ru/post/411145/ - Мои маленькие реле: Brainfuck компьютер это реальность (2018) | * https://habr.com/ru/post/411145/ - Мои маленькие реле: Brainfuck компьютер это реальность (2018) | ||
* https://habr.com/ru/post/442732/ - Мои маленькие реле: Тройной Brainfuck, или что такое безумие (2019) | * https://habr.com/ru/post/442732/ - Мои маленькие реле: Тройной Brainfuck, или что такое безумие (2019) |
Текущая версия на 16:12, 14 апреля 2023
Компьютер на базе 600 герконовых реле с архитектурой фон-неймана и набором инструкций brainfuck++
Релейная логика
Вкратце, о релейной логике
Электромагнитное реле представляет собой электромагнит с блоком контактов, установленных на подвижном якоре. При подаче напряжения на катушку электромагнита якорь примагничивается к сердечнику и переключает состояние контактов. Соединяя контакты нескольких реле в различных комбинациях, мы получим логический элемент того или иного типа.
- Например, соединив нормально разомкнутые контакты двух реле последовательно, мы получим логический элемент 2И. Сигнал на выходе будет тогда, когда напряжение будет подано на оба реле.
- Соединив эти же самые контакты, но параллельно, получим 2ИЛИ — сигнал на выходе будет, если хотя бы на одно реле подано напряжение.
- Два последовательно соединенных нормально замкнутных контакта дадут логический элемент 2ИЛИ-НЕ.
- Они же, по параллельно соединенные — 2И-НЕ.
Элементов 2И-НЕ или 2ИЛИ-НЕ достаточно, чтобы создать любую логическую схему. Каждый из них образует функционально полный логический базис. Говоря простым языком, они «Тьюринг-полные» в понятиях булевой логики. А если можно создать такой логический элемент, то и полноценная ЭВМ — не проблема.
Язык программирования Brainfuck
Brainfuck — пожалуй, самый популярный из эзотерических языков программирования. А заодно самая настоящая Тьюринг-полная трясина. Всего лишь восемь инструкций, на которых можно писать все что угодно, но очень долго. К примеру, на то, чтобы написать и отладить программу деления двух целых чисел, которая печатает в терминал результат с шестью знаками после запятой, у меня ушло три дня.
Весь синтаксис языка строится вокруг ОЗУ на 30 тысяч ячеек памяти с разрядностью 8 бит.
- Двумя инструкциями + и - мы изменяем значение в текущей ячейке данных на единицу больше или меньше.
- Двумя инструкциями < и > мы изменяем на единицу указатель на текущую ячейку данных, тем самым перемещаясь по памяти.
- Еще две инструкции — [ и ] — позволяют нам организовать циклы. Все, что внутри скобок, является телом цикла. Вложенные циклы допускаются. Логика инструкции проста — если значение текущей ячейки данных не равно нулю, мы выполним одну итерацию цикла, если равно, то выходим из него.
- Последние две инструкции — . и ,. Они позволяют вывести значение текущей ячейки в терминал или ввести его с устройства ввода в ОЗУ. Это позволяет писать интерактивные программы.
Да, этого более чем достаточно для написания любой программы. Существование компиляторов из языка C в Brainfuck подтверждает это. Но плотность кода — никакущая. Для выполнения простейших операций, например сложения двух переменных, требуется исполнить сотни инструкций Brainfuck. В этом и заключается вся академическая прелесть языка, и в результате многие программисты практикуются в создании программ на нем. Они уже написали тысячи приложений, и, если мы сможем исполнять их на релейном компьютере, это будет хорошо.
Brainfuck++
Есть небольшой вариант оптимизации плотности кода на Brainfuck. Программы в большинстве своем состоят из последовательностей инструкций + — < >. Например, десять операций инкремента мы можем заменить на равноценную операцию +10. Двадцать операций сдвига указателя вправо — на операцию > 20 и так далее. В итоге некоторые программы потребуют на 20–30% меньше тактов, а какие-то будут ускорены в несколько раз.
Таким образом, в 2016 году мое техническое задание на разработку релейного компьютера приняло окончательный вид. Я решил создать полноценный компьютер на базе герконовых реле со следующими характеристиками.
- Набор инструкций Brainfuck++.
- Полноценная 16-разрядная архитектура фон Неймана — и шина адреса, и шина данных шириной 16 бит. Программы на Brainfuck, как правило, восьмиразрядные, так что требуется обеспечить обратную совместимость.
- Декодирование инструкций и все вычисления реализованы на релейной логике.
- Рабочая частота — многократно превышающая существующие решения на реле (то есть существенно выше пяти инструкций в секунду).
- ОЗУ на базе микросхем SRAM (как и у других самодельщиков).
BrainfuckPC
Центральным элементом релейного процессора в этом компьютере стал 16-разрядный полный сумматор с параллельным переносом. К нему на вход подключены два регистра. TMP — временный регистр, в который помещается старое значение, и CMD — командный регистр, где хранится инструкция и константа, на которую будет изменяться старое значение.
Поэтому я могу исполнять операции оптимизированного Brainfuck++, а заодно получить полноценные условные переходы — Jump If Zero и Jump If Not Zero — в любую сторону программы. Если значение текущей ячейки ноль (или не ноль) — прыгаем по коду вперед или назад на расстояние N.
Результат операции суммирования может быть выгружен в один из контекстных регистров — AP — с номером текущей ячейки данных, или IP — с номером текущей инструкции. Либо результат может быть выгружен в текущую ячейку ОЗУ, если речь идет об инструкциях + и -
Инструкция исполняется за один такт. По нарастающему фронту производится считывание очередной инструкции из памяти. Для этого старое значение регистра IP загружается во временный регистр, с помощью сумматора прибавляем к нему единичку и перезаписываем регистр IP новым значением. Строб по памяти — и в командном регистре лежит очередная инструкция.
По спадающему фронту сигнала тактирования начинается исполнение этой инструкции. Если она работает с сумматором, то этот процесс выглядит примерно так же, как и процесс загрузки новой инструкции — помещаем старое значение во временный регистр, прибавляем лежащую в младших битах регистра CMD константу, результат записываем обратно в регистр или текущую ячейку данных.
Модуль
В основе конструкции — небольшой модуль на печатной плате, который реализует простую логическую операцию.
- Модуль 2AND/2XOR — две независимые друг от друга логические операции — 2AND и 2XOR. 32 штуки используются в блоке сумматора, по два модуля на бит.
- Модуль D-триггера — 64 штуки уходят на два регистровых блока.
- Модуль диодный — просто восемь диодов на плате для реализации многовходового диодного OR либо однонаправленной линии данных. Это грязный хак, но он позволяет экономить как на реле, так и на времени. В отличие от реле, сигнал передается на выход такого логического элемента мгновенно. С другой стороны, в релейном калькуляторе «Вильнюс» вовсю использовались диоды, так что наличие диодов в связке с реле — вполне допустимый шаг.
- Модуль 2& — это базовый кирпичик. По сути — четыре независимых друг от друга реле с контактом на переключение для реализации абсолютно любой логической схемы.
- Универсальный модуль 2AND/2OR, который также позволяет реализовать практически какую угодно логическую функцию — 4AND, 4OR, 4AND-NOT, 4OR-NOT и так далее.
Все модули имеют одинаковые размеры 60 × 44 мм, 16-контактный разъем с одной стороны и светодиоды с другой стороны. Диоды показывают, какое реле включено, что очень помогает при пошаговой отладке машины. Ну и как минимум это красиво.
Блок
32 модуля по восемь модулей в четыре ряда объединяются в функциональный блок. Всего блоков пять.
- Блок сумматора (на фото в центре) — 16-разрядный полный сумматор. Два 16-разрядных входа для чисел, одна линия переноса нулевого разряда, два выхода. На одном — результат операции суммирования, на другом — результат XOR между входами. Может использоваться как самостоятельная операция (но не используется). Это побочная возможность, которая возникла в результате особенностей схемы суммирования. Операция вычитания производится в дополнительном коде. А точнее — прибавлением к старому значению большого числа, которое вызывает переполнение сумматора. Сигнал переполнения не используется, а остаток на выходе как раз соответствует необходимому результату.
- Блок контекстных регистров IP/AP (правый блок) — два независимых 16-разрядных регистра. Прямые выходы регистра Q через защелки подключаются к тем или иным шинам процессора.
- Блок регистров TMP/CMD (левый блок) — тоже содержит два регистра: временный регистр и командный регистр. У последнего младшие 12 разрядов расширяются до 16 (копированием 12-го бита) и подключаются ко входу сумматора. Старшие четыре разряда хранят тип инструкции.
- Два блока логики. Один блок используется для декодирования инструкций, второй — работает секвенсором импульсов и непосредственно исполняет команду, подавая сигналы управления на остальные блоки.
Все блоки имеют одинаковые размеры 200 × 150 мм и представляют собой напечатанные из пластика корзинки с «ушами» для крепления на раме. Модули втыкаются в плату с разъемами, на которой прошивается схема с помощью монтажа накруткой.
Собрав все блоки воедино парой десятков метров шлейфов, мы получаем BrainfuckPC — 16-разрядный компьютер с процессором на базе герконовых реле, архитектурой фон Неймана и набором инструкций Brainfuck++.
- Общее число реле: 578 штук
- Общее число логических элементов: 157 штук
- Разрядность шины адреса: 16 бит
- Адресация: пословная
- ОЗУ: 128 Кбайт (64 тысячи слов)
- Разрядность шины данных: 16-бит/8-бит
- Тактовая частота (текущая/максимальная): 25 Гц/40 Гц
- Потребляемая мощность: 70 Вт
- Габаритные размеры: 1100 × 650 × 140 мм
- Масса: 25 кг
WaveForms
New insctruction fetching
New Data retrieval
Timing diagram for new Address fetching
Branch Not Taken
To be continued
Хотя компьютер уже и способен самостоятельно исполнять некоторые программы, проект по его сборке еще не завершен. Однако после двух лет непрерывной работы над этим проектом у меня накопилась куча других задумок, которые ждут своего часа. Точных сроков завершения проекта я себе не назначал, а вот над списком того, что еще требуется сделать, подумал.
- Необходимо заменить плату памяти на новую, на которой установлены только микросхемы ОЗУ и цепи согласования с релейными уровнями. Печатная плата уже разведена и изготовлена, все комплектующие имеются — осталось только сесть и спаять. Правда, чтобы заменить текущую плату памяти на новую, надо будет дополнительно доделать программатор — ведь встроенный загрузчик на новой плате памяти отсутствует.
- Вместе с новой платой памяти будут установлены стрелочные индикаторы, нормальный крепеж терминального дисплея и самостоятельная логика обновления светодиодной панели (которая, к слову, отображает некоторую область памяти данных — что тоже сделано для наглядности работы компьютера).
- Программатор, вернее, разработка прошивки для него. Вообще, на компьютере имеется специальный разъем для программирования — и через него можно загружать программу в компьютер как с перфоленты, так и с панели тумблеров. Но так как программу необходимо грузить в ОЗУ каждый раз после включения ЭВМ, использовать полноценный программатор несколько проще.
- Логика самотактирования. В настоящий момент с микросхемы генерируется сигнал частотой 12–25 Гц и подается на вход системы тактирования. Однако можно сделать так, чтобы по завершении исполнения текущей инструкции компьютер сам выдавал сигнал для старта следующей, без использования внешнего сигнала тактирования. Тут надо докрутить буквально два-три модуля, и компьютер в этом режиме сможет исполнять инструкции на максимально возможной частоте. Но внешнее тактирование все равно пригодится, например если мы захотим тактировать ЭВМ от MIDI-клавиатуры.
- Инструкция чтения из консоли. Она завязана на логику тактирования (в синхронном режиме работы компьютер должен останавливать работу и ждать поступления данных).
- Отправить заявку в книгу рекордов Гиннесса… Как на самый быстрый релейный процессор и при этом одновременно самый медленно считающий. 16 миллифлопс — «это вам не шубу в трусы заправлять» (цитирую комментарий на YouTube).
Ссылки
- https://github.com/radiolok/brainfuckpc - репозиторий проекта со всеми схемами
- https://github.com/radiolok/bfutils - эмулятор и компилятор
- https://xakep.ru/2019/09/19/brainfuckpc/ - Статья в журнале Хакер
- https://habr.com/ru/post/402629/ - Мои маленькие реле: Brainfuck компьютер это магия (2017)
- https://habr.com/ru/post/411145/ - Мои маленькие реле: Brainfuck компьютер это реальность (2018)
- https://habr.com/ru/post/442732/ - Мои маленькие реле: Тройной Brainfuck, или что такое безумие (2019)