Идея этого проекта пришла ко мне в феврале 2021 года, когда в Остине (штат Техас) произошёл сбой энергосистемы. К сожалению, нам надолго запомнилось то, как правительство справлялось с ситуацией. Когда единственным источником тепла и света остался только газовый камин, а единственным окном в мир — слабое телефонное Интернет-соединение, у меня было много времени поразмыслить о том, что бы новое и интересное мне хотелось разработать. Я взял калькулятор HP-41CV и начал нажимать на кнопки. Как обычно, ощущения от этого были самыми приятными. И мне захотелось самому создать нечто подобное!

В начальной школе мне представилась возможность поиграть с HP-41CV. Я наблюдал, как калькулятор загружает программу с магнитной ленты и запускает её. Жужжание считывателя карт и тонкая магнитная лента, втягиваемая в устройство с одной стороны и выходящая с другой, внезапно изменяли поведение калькулятора, что произвело на меня очень сильное впечатление. Я и не подозревал, что оно повлияет на всю мою жизнь. Спустя несколько лет у меня появился Sinclair ZX81, потом ZX Spectrum, на котором я при помощи дизассемблера HiSoft Devpac MONS взламывал разные игры. Эти два устройства (калькулятор HP и микрокомпьютеры Sinclair) подтолкнули меня к разработке, программному обеспечению и исследованию внутренностей разных машин. Во многом я стал разработчиком именно благодаря этому.

Когда-то я изучал сам чип Z80, воссоздав его в виде A-Z80 и написав визуальный инструмент Z80 Explorer, отображающий его список связей. В каком-то смысле это ощущалось как закрытие темы одержимости Sinclair. Проект калькулятора ощущался как закрытие темы HP. Это не клон, не эмуляция, а реализация с нуля на основе тех же принципов. Мне хотелось изнутри разобраться в том, почему эти машины работали именно так.

The HP-41C, the calculator that started my journey.
Калькулятор HP-41C, с которого начался мой путь

Как работает научный калькулятор? Не в общих чертах, а в подробностях. Как он хранит числа? Какой алгоритм вычисляет sin(x)? Как функционирует его очень простой CPU?

В серии моих статей мы получим ответы на эти вопросы: в конечном итоге мы получим полностью работающий научный калькулятор, спроектированный и изготовленный с нуля, на собственном CPU, созданном на FPGA, с написанным вручную микрокодом, эталонными реализациями на C++ и физическим «железом», которое лежит у меня на столе и может вычислять точные ответы. И всё это в опенсорсе: вы можете увидеть это и попробовать самостоятельно.

The finished calculator. Custom CPU, 16-digit BCD arithmetic, 35 keys, OLED display. Designed and built from scratch.
Готовый калькулятор: собственный CPU, 16-разрядная BCD-арифметика, 35 клавиш, OLED-дисплей. Спроектирован и собран с нуля.

Насколько я понимаю, этот проект уникален: мне неизвестен ни один другой научный калькулятор (с тем же уровнем функциональности), реализованный на FPGA с собственным CPU и оригинальным рукописным ПО для его работы.

Правила

Первым делом я решил определиться, что же значит «с нуля» в этом проекте. Вот правила, которые я для себя установил:

  • Никаких готовых CPU. Настоящие мужики создают собственные процессоры. CPU будет создан самостоятельно, спроектирован на SystemVerilog и реализован в FPGA.

  • Никаких библиотек для работы с числами с плавающей запятой и специализированных IP-блоков. Вся арифметика BCD (двоично-десятичная), создаваемая из примитивных операций с нибблами (полубайтами).

  • Никаких эмулируемых ROM. Поведение калькулятора полностью задаётся написанным вручную ассемблерным кодом, не скопированным ни из одного готового калькулятора.

  • Никаких компромиссов в точности. Каждый алгоритм сверяется с эталонной реализацией на C++ при помощи тестовых векторов, не на глазок.

Стоит сказать о том, почему была выбрана FPGA. Можно было изготовить калькулятор на основе готового RISC, ARM или любого другого процессора CPU, использовать SoC и писать код на удобном C++, но уже существует много таких реализаций. В этом нет ничего нового; это даже не так сложно сделать. А если я выберу FPGA, то моим проектом станет сам CPU: при помощи собственного микрокода я создам набор команд, регистровый файл, операции АЛУ и все детали работы машины. Именно в этом и заключается удовольствие от творчества.

Выбор

BCD против двоичных чисел с плавающей точкой. В карманных калькуляторах традиционно использовалась двоично-десятичная арифметика, при которой каждый разряд хранится в виде 4-битного ниббла. HP-35, HP-41, HP-15C — всё это BCD-машины. BCD обеспечивает точное десятичное представление ценой усложнения арифметики. Двоичные числа с плавающей запятой (IEEE 754) не могут точно представить многие десятичные дроби. В случае калькулятора BCD — это верный выбор. В моём калькуляторе используется 16 разрядов BCD мантиссы (больше, чем в любом калькуляторе HP, и достаточно для корректного округления до последней отображаемой цифры). У каждой мантиссы есть соответствующая двухразрядная BCD-экспонента, обеспечивающая общий диапазон величин от 1.0e-99 до 9.999999999999999e+99 («денормализованные» числа отсутствуют).

RPN против алгебраической записи. RPN (Reverse Polish Notation, обратная польская запись) означает, что мы вводим операнды перед операторами. Для сложения 3 и 4 нужно нажать 3 ENTER 4 +. Скобки, приоритет операторов и клавиша «=» отсутствуют. Вместо этого есть четырёхуровневый стек (X, Y, Z, T), где хранятся промежуточные результаты. В калькуляторах HP использовалась RPN со времён HP-35 (1972 год). Она быстрее при длинных вычислениях, чётче даёт понять, что делает машина, и её предпочитают многие пользователи. Я тоже создаю RPN-калькулятор.

Фиксированные возможности против программируемых. Мой калькулятор имеет фиксированные функции: пользователь нажимает клавиши, получает результаты, но не может писать программы. Добавление программируемости было бы интересным расширением, и я занёс его в свой список пожеланий (понадобится ещё одна плата с дополнительной FRAM или другим типом постоянного хранилища). В моём проекте ближе всего к программируемой модели слой скриптинга (выражающий функции калькулятора в виде последовательности токенов), но он целиком расположен в прошивке.

Соотношение аппаратной реализации и микрокода. Возможные решения находятся в спектре от «каждая функция — это отдельная логическая цепь» (много вентилей, отсутствие ROM) до «каждая функция — это программная процедура» (минимум «железа», большой ROM). Мой проект ближе к программному краю спектра: CPU остаётся относительно простым и практически всё, от сложения до CORDIC, написано на языке ассемблера. Оборудование занимается сканированием клавиатуры, управлением ЖК-дисплеем и декодированием адресов; микрокод занимается арифметикой, форматированием отображения и стеком RPN. Это ещё и сильно упрощает отладку.

CORDIC для трансцендентных функций. Тригонометрические функции, логарифмы и степени вычисляются при помощи алгоритмов CORDIC — того же метода сдвига и сложения, который применялся в HP-35 и практически во всех карманных научных калькуляторах с 1972 года. Для них нужны только сложения, вычитания и сдвиг разрядов (во внутреннем цикле умножения нет) и они прекрасно работают с десятичной арифметикой. В частях 3 и 7 будет подробно рассмотрен этот алгоритм и его история.

Что мы будем рассматривать в этой серии

В серии постов мы пошагово рассмотрим проект от первооснов до готового оборудования:

Часть 2 — поиск пути: прототипирование алгоритмов, расположение клавиш, конечный автомат ввода и принтер.

Часть 3 — подробное описание численных методов: BCD-арифметика, CORDIC, разряды защиты и что же такое на самом деле «корректное округление».

Часть 4 — фреймворк разработки: мы поговорим о том, как один и тот же исходный код на Verilog работает в ModelSim, Verilator, в десктопном симуляторе Qt, в браузерном демо на WebAssembly и в реальной FPGA, не требуя при этом никаких модификаций.

Часть 5 — первое оборудование: проектирование печатных плат в EasyEDA и их изготовление в JLCPCB.

Часть 6 — проектирование CPU: набор команд, АЛУ, структура памяти и итеративный процесс проектирования архитектуры набора команд.

Часть 7 — написание микрокода: ассемблерные файлы, слой скриптинга и ощущения от написания ПО для изобретённой тобой машины.

Часть 8 — физическая сборка: Rev A (две платы, соединённые кабелем), Rev B (одна плата) и долгая дорога к изготовлению корпуса.

Часть 9 — пересмотр проекта в 2025 году: переписывание арифметического движка под полную 16-разрядную точность, добавление полного набора тригонометрических функций и новые команды CPU.

Часть 10 — Заключение и выводы.

Каждую часть я писал и тестировал до начала следующей, однако в них есть множество повторяющейся информации. На написание всего цикла понадобилось много вечеров и выходных. Самым важным инструментом была настойчивость, на втором месте с небольшим отрывом — хороший фреймворк тестирования.

Прежде, чем мы приступим

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

Весь исходный код выложен на GitHub.

Часть 2: Поиск пути

У каждого серьёзного проекта есть этап, о котором никто не говорит и не пишет. Он наступает, когда у вас нет ни структуры, ни архитектуры, и вы даже не знаете, возможно ли его реализовать. Вы выбираете направление и движетесь по пути, пока не наткнётесь на стену, потом поворачиваете и пытаетесь идти в другую сторону. Вы создаёте черновики, чтобы просто разобраться, а потом выкидываете их. Иногда инструмент, который вы написали за несколько часов, остаётся полезным долгие годы. Иногда неделя работы отправляется прямиком в мусорную корзину. Иногда вы радуетесь тому, чему научились, и полностью забрасываете проект. Это поиск пути и без него не обойтись.

Альтернативой было бы создание всего проекта целиком, после чего вы бы уже при разработке оборудования обнаружили какие-нибудь фундаментальные ошибки.

В этой части задокументирован такой этап развития проекта калькулятора. Хоть я писал и выбрасывал множество других, для меня остались полезными три программы, по одной на каждый заданный вопрос. Пост завершается неожиданным уходом в тему инфракрасного светодиода и древнего принтера HP.

Немного расскажу об инструментах. Обычно код, создаваемый в процессе поиска пути, считают одноразовым: некрасивым, незадокументированным и пригодным только для того, чтобы ответить на какой-то вопрос. Фред Брукс в своей книге «Мифический человеко-месяца» назвал этот паттерн так: «план, рассчитанный на выкидывание; рано или поздно вы это сделаете». Он утверждал, что прототипы неизбежны, и вопрос только в том, планируете ли вы их, или вас ждут сюрпризы. Я планировал и писал всё аккуратно.

Доказательство: смогу ли я это сделать?

Я знал, что смогу реализовать четыре основные операции (+,-,*,/), в этом сомнений не было. Но логарифмические и трансцендентные функции — дело другое. Обычно при разработке ПО для них используют библиотеки, но мне было любопытно, как различные библиотеки вычисляют операции высокого порядка, как это делают калькуляторы и как они делали это в 70-х. Чтобы понять эти алгоритмы полностью, мне нужно было реализовать их.

Мои исследования закодированы в проекте Proof. Это грязный исследовательский код на C++, проверяющий конкретные алгоритмы для каждой функции высокого порядка. Он не имеет качества продакшена, это не окончательная реализация, она нужна только для проверки того, работает ли такой подход.

В нём охвачены следующие функции:

  • Квадратный корень: множество потенциальных решений со сравнением сходимости и погрешности. Логичным кандидатом был метод Ньютона-Рафсона: x_{n+1} = (x_n + N/x_n)/2. Это один из самых старых численных методов. На вавилонской глиняной табличке YBC 7289 из коллекции Йельского университета, датированной примерно 1800 годом до нашей эры, показаны вычисления √2 с точностью до шести знаков после запятой. Вероятно, для них использовался тот же итеративный подход.

  • Логарифм и антилогарифмln(x) и exp(x), реализованные при помощи только основных арифметических операций.

  • Тригонометрические функции: базовые tan(x) и atan(x), которые нам нужны.

Оказывается, эти три категории не так отделены друг от друга, как можно подумать. В процессе исследований я выяснил, что все эти функции имеют глубокую алгоритмическую связь — один итеративный метод CORDIC, охватывающий тригонометрические, показательные и логарифмические функции. Благодаря тому, что я понял эту связь на ранних этапах проекта, и было выбрано семейство алгоритмов. Подробно об этом будет написано в части 3.

На этом этапе меня ещё не волновало конкретное представление BCD и окончательная точность, я делал упор только на самих алгоритмах: их сходимости и вычислительных затратах. Если один способ вычисления ln(x) для получения приемлемого результата с 16 разрядами точности требовал 16 итераций многочленов, а другой сходился за 8, то второй был лучше; выяснить это можно как раз на этом этапе проекта. Я измерял сходимость эмпирически, наблюдая, сколько итераций требуется каждому алгоритму и как снижается погрешность на каждом из шагов. Приемлемая скорость схождения, даже в арифметике с плавающей запятой, обеспечивала уверенность, что BCD-реализацию можно итерировать с достаточной точностью. Вторым критерием была алгоритмическая сложность каждой функции: я мог полагаться только на основные операции.

Более сложные функции калькулятора можно создать из основных. Как только они заработали, пусть даже грубо, я нашёл ответ на вопрос о реализуемости проекта.

Макет: как должны располагаться клавиши?

Разобравшись с вычислительной реализуемостью, можно было приступать к клавиатуре.

Кажется, что выбор раскладки клавиш — простая задача, но на самом деле это не так. Чтобы спроектировать качественную клавиатуру калькулятора, нужно в первую очередь избегать незначительных ошибок. Часто проблемы оказываются неочевидными: действия ощущаются неправильными, если рукам требуется совершать больше работы или нажимать лишние клавиши. Должен ли ENTER находиться с правого края или где-то ближе к центру? Должна ли быть RCL основной функцией или альтернативной? Где должна находиться клавиша десятичного разделителя относительно цифровых клавиш? HP, TI и Casio разработали собственные стандарты, и многие люди предпочитают определённого производителя, потому что уже привыкли к его раскладке.

Для удобства я написал на C# Windows-приложение Mockup, которое позволило мне экспериментировать с различными раскладками. Описания раскладок хранятся в текстовых файлах (layout1.txtlayout2.txt, etc.), которые приложение загружает через диалоговое окно открытия файла. Чтобы приложение казалось более реальным, в нём реализовано большинство калькуляторных функций, применён реальный шрифт HD44780 (тот же шрифт, который используется на аппаратном дисплее), поэтому экран выглядит попиксельно точным.

В процессе изучения расположения клавиш популярных коммерческих калькуляторов я выполнил множество итераций. Самым важным (и неудивительным) уроком стало то, что расположение клавиш альтернативных функций (включаемых клавишей Shift) значит больше, чем можно было бы ожидать: функция, которой часто пользуешься, но доступная только по нажатию Shift, начинает утомлять уже после часа работы. В итоге я остановился на раскладке с ENTER в правом нижнем углу, сгруппированными вместе основными операциями и самыми часто используемыми научными функциями (sin, cos, tan, log, exp). Ничего экстраординарного или новаторского, но, наверно, в этом и был смысл.

FPGA Calculator mockup

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

Ввод чисел сложнее, чем кажется

Третьим неотвеченным вопросом был ввод чисел. Кажется, что это тривиально: пользователь вводит цифры, мы храним их в буфере. Но в научном калькуляторе с BCD-мантиссами, экспонентами, сменой знаков, удалениями и потенциальными переполнениями при вводе конечный автомат имеет неожиданно много неочевидных пограничных случаев.

Допустим, что произойдёт, когда пользователь вводит 0.005e10? Мантисса создаётся в одном буфере, экспонента в другом, а реальная внутренняя экспонента (та, которая записывается в регистр) отличается от введённой пользователем. Это отображаемая экспонента (та, которую видит пользователь: +10), скомбинированная с виртуальной экспонентой, вычисленной по расположению десятичного разделителя в мантиссе (-3, потому что 0.005 = 5 × 10⁻³). Реальная хранимая экспонента равна +7.

Мантисса хранится в нормализованном виде: если это не истинный ноль, первый разряд числа должен быть ненулевым, даже если пользователь сначала введёт нули.

Что произойдёт, если пользователь нажмёт на «e», не введя сначала цифры мантиссы? (Ответ: мантиссе будет присвоено значение 1, то есть e5 даёт 1E+05.) А если мантисса такая длинная, что занимает на дисплее область экспоненты? (Ответ: игнорируем нажатия на e, пока пользователь не удалит несколько разрядов.) Как различается поведение backspace в режимах экспоненты и мантиссы? Как только начинаешь об этом размышлять, сразу всплывают всевозможные безумные пограничные случаи. Я протестировал имеющиеся у меня калькуляторы, и в конечном итоге решил в основном перенять поведение HP.

Проект Input — это написанный на C++ интерактивный симулятор, реализующий эвристику ввода. Пользователь вводит нажатия клавиш в терминал, который после каждого нажатия показывает полное внутреннее состояние: содержимое экрана, внутренние буферы мантисс, реальную экспоненту, отображаемую экспоненту, местоположение курсора и флаги.

S 0123456789012345   M 0123456789012345   E  P  Di=2 Si=7 Mi=5 FLAG_EDIT_EXP=1
   3.1415     E+24     3141500000000000  +24 24
         |                  |

Я изначально написал этот проект на C++, намеренно упрощая код и используя простые языковые конструкции, чтобы облегчить себе его портирование на язык ассемблера. Если конечный автомат реализован правильно, то его портирование превращается в трансляцию, а не становится процессом открытий.

FPGA Calculator: Input

Принтер: инфракрасный светодиод и древний принтер HP

Этот раздел будет более глубоким, чем другие, потому что принтер был первой частью реальной работы с FPGA в проекте — я впервые писал на Verilog для реальных аппаратных сигналов (инфракрасных), а не для симуляции.

У HP 82240A/B есть небольшой термопринтер, который HP разработала для своей линейки калькуляторов, созданной в 1980-х. Он получает данные по инфракрасному каналу, не требуя ни кабелей, ни разъёмов; только инфракрасный светодиод, направленный на датчик принтера. У меня есть дома такой принтер, поэтому я хотел, чтобы калькулятор мог печатать на нём.

В сообществе пользователей HP протокол получил прозвище «Красный глаз» (Red Eye). Он был разработан за семь лет до появления IrDA: Infrared Data Association основали в 1993 году, а первый свой стандарт она опубликовала в 1994 году. Red Eye и IrDA несовместимы: в них используется разное битовое кодирование, разные несущие частоты, разные структуры кадров. Несмотря на это, один и тот же протокол Red Eye без изменений применялся с HP-18C (1986 год) по HP-50g (на протяжении более двадцати лет и десятка моделей калькуляторов). Подобное долголетие протокола, впопыхах разработанного для выпуска новой линейки калькуляторов, — свидетельство или хорошей инженерной работы, или большой удачи; подозреваю, что дело и в том, и в другом.

Для реализации поддержки я исключительно работал с FPGA, на грубой тестовой плате с ИК-светодиодом, подключенным к контакту GPIO. Первый шаг заключался в анализе протокола.

В Technical Interfacing Guide HP 82240B задокументирован формат кадра данных: каждый байт передаётся в виде 12-битного кадра, состоящего из 4 бит коррекции ошибок, за которыми следуют 8 бит данных. Каждый бит закодирован в виде двух полубитов: 1 передаётся как импульс с последующей паузой (X_), а 0 — как пауза с последующим импульсом (_X). Данным предшествует начальная последовательность из трёх полубитных импульсов. Тайминги берутся от часов, работающих с частотой примерно 32 кГц.

Полубайт (ниббл) коррекции ошибок вычисляется из байта данных при помощи определённого паттерна XOR для отдельных бит; это простой, но эффективный способ, позволяющий принтеру распознавать повреждённые кадры без полной CRC.

FPGA Calculator: RevA with HP printer

Реализация на SystemVerilog представляет собой конечный автомат с пятью состояниями: READY (ожидание байта), START (отправка начальной последовательности из трёх полубитов), DATA и BIT (тактирование 12 бит по два полубита за раз) и DELAY (межкадровый пробел перед следующим байтом). Выходной сигнал busy сообщает CPU, что не нужно отправлять следующий байт, пока не завершён текущий кадр. Чтобы упростить конструкцию, я использовал опрос.

Я отлаживал систему на реальном «железе», наблюдая на экране осциллографа за реальными сигналами. В конструкции есть три тестовых точки GPIO: сигнал тайминга одной четвёртой такта, включение ИК-светодиода и сырой вывод светодиода. Наблюдая за этими тремя сигналами в осциллографе, я мог чётко видеть, что делает конечный автомат на каждом из этапов, и сравнивать показания с диаграммами таймингов протокола из руководства.

HP 82240B Infrared Printer Timing and Encoding

Получив первую распечатку на принтере HP (с голой платы FPGA, на GPIO которой висел светодиод), я убедился в правильности реализации протокола. Ура! Пока переходить к следующей задаче.

Что мне дал поиск пути

В конце этого этапа я получил ответы на все вопросы:

  • Реализуемость: да, tanexpln и sqrt вычисляемы из примитивов. Алгоритмы работают, и к тому же я понял, какие алгоритмы следует использовать.

  • Раскладка клавиш: было протестировано множество кандидатов; работа с одними ощущается более удобной, чем с другими.

  • Ввод чисел: полный конечный автомат ввода, корректный и протестированный, готовый к портированию на язык ассемблера.

  • Принтер: работающая реализация протокола инфракрасной передачи данных, проверенная на оборудовании.

Дальше мы приступим к реализации арифметического движка уровня продакшена — к настоящей реализации BCD с полной точностью, разрядами защиты, глубоким алгоритмом CORDIC и тысячами тестовых векторов.

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


  1. hw_store
    20.05.2026 21:27

    Это какой-то сюр.