Arduino computer science

Часы для обнаружения жестов на основе машинного обучения, ESP8266 и Arduino

Шаг 1. Рассказываю!

Так как я убрал ряд изначально планировавшихся мной функций часов (таких как мониторинг пульса и микро-USB для зарядки), то решил уделить больше времени доведению до ума программной части. Я подумал о том, чтобы добавить в проект немного машинного обучения; это должно быть легко по сравнению с электроникой: я буду иметь дело с простым набором кода – копипастой со StackOverflow. По крайней мере, я так думал.

Но я ошибался: реализовав обнаружение жестов на основе машинного обучения, я просто сошёл с ума. Я ненавидел всё это. Не потому, что это было сложно, просто было очень неприятно собирать данные и обучать модель.

Если вы хотите узнать больше о внедрении машинного обучения во встраиваемую систему, ознакомьтесь с двумя ссылками: TinyML и определение жестов. Первая объясняет, как использовать TensorFlow в TinyML с Arduino, а вторая – как использовать базовые алгоритмы ML на Arduino. Я много раз упоминал последнюю ссылку, потому что материал просто понять, а также это работает с микроконтроллерами с очень небольшим количеством памяти, такими как Arduino NANO и UNO.

Шаг 2. Сборка печатной платы

В этот раз сборка должна быть очень простой и быстрой благодаря печатным платам, и за это я благодарю PCBWay за спонсирование этой великолепной чёрной печатной платы для этого проекта.

Я собрал все SMD-компоненты для проекта, а затем расположил их таким образом, чтобы можно было легко, без возни получить к ним доступ. Остальное я потом просто паял!

Просто следуйте принципиальной схеме и соответствующим образом припаяйте компоненты на печатной плате. Чтобы упростить пайку, переходите от пайки небольших SMD-компонентов [резистор, конденсатор, регулятор] к более крупным компонентам со сквозными отверстиями [MPU6050, OLED]. Во время пайки я также с помощью ленты 3M закрепил батарею Lipo между платой и OLED-дисплеем.

Мне сложно найти подходящий регулятор для проектов, поэтому в своих прошлых проектах я просто использовал AMS1117: он дешёвый и его легко найти. На плате я дал два варианта, вы можете воспользоваться MCP1700 или LSD3985. В моём случае я использую LSD3985 и игнорирую MCP1700, а вы можете использовать любой вариант. Если вам нужна точная схема, посмотрите здесь.

Шаг 3. Программирование часов

Чтобы упростить программирование, я выделил немного места на печатной плате, так что вы можете просто подключить модуль FTDI, чтобы начать программировать. Для программирования платы необходимо сначала перевести esp8266 в мигающий режим, поэтому при подключении к ПК просто удерживайте кнопку, которая подключена к GPIO-0 esp12E.

Чтобы проверить, работает ли плата, просто загрузите код Github и проверьте, работают ли все функции, такие как время NTP, пробуждение по щелчку и изменение работы экрана. Если всё работает нормально, то вы закончили с аппаратной частью.

Шаг 4. Машинное обучение? Часть 1

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

В большинстве случаев, когда алгоритму нужно найти ответ на вопрос, ему нужно знать последовательность процессов, которые он должен выполнить, чтобы получить конечный результат. Простой пример – умножение.

Если вы хотите найти ответ на задачу умножения (скажем, 2 умножить на 5), то можно просто сказать компьютеру выполнить несколько сложений, чтобы получить ответ. Как видите, мы говорим компьютеру, что делать, чтобы получить ответ.

Шаг 5. Машинное обучение? Часть 2

ML работает немного иначе: мы просто задаём компьютеру кучу вопросов и соответствующих ответов и просим его выяснить способ или найти процесс, чтобы он мог ответить на любой новый вопрос без программирования вручную.

Возьмём, к примеру, поиск яблока на фотографии, это очень легко сделать человеку, но довольно сложно написать алгоритм и заставить компьютер понимать все функции, которые есть у яблока; это возможно, но крайне утомительно и сложно. Вместо этого, если бы мы могли написать алгоритм, который может обучаться сам по себе, просто глядя на тысячи изображений яблок, и учиться самостоятельно, разве такое не было бы здорово? Есть ещё один положительный момент использования алгоритма машинного обучения: они могут придумать новый способ поиска яблок на фотографии, о котором мы даже не думали. Итак, ML – довольно интересная область, чтобы её исследовать.

Примечание! Я не самый подходящий человек, чтобы объяснить машинное обучение и искусственный интеллект, я просто пишу то, что узнал.

Шаг 6. Классификация

В машинном обучении много способов и техник, чтобы решить задачу, и в нашем случае, чтобы обнаружить жесты, я воспользуюсь одним из методов под названием классификация. Зачем нужна классификация? Смотрите на картинку выше, чтобы увидеть данные акселерометра и гироскопа на последовательном плоттере. Данные кажутся предсказуемыми, с точки зрения человека, как только понять, что движение долго повторяется. Теперь, если я сделаю то же движение за пределами экрана, вы всё равно сможете угадать, что это за движение, просто взглянув на график, а самое приятное – то, что вы можете сделать это даже для других жестов и движений. Так происходит потому, что наш мозг даёт разные названия разным паттернам.

Точно так же, если мы можем показать этот паттерн данных алгоритму ML несколько раз, он попытается понять эти данные и распределит их по разным группам. Или, как сейчас модно, вы можете сказать, что алгоритм классифицирует выборку данных по различным классам. Итак, в следующий раз, когда алгоритм ML увидит похожую закономерность в данных, он выяснит, что это за движение или жест. По этой причине нам нужно сделать классификацию. Я выбрал алгоритм случайного леса для этой задачи классификации.

Шаг 7. Сбор данных для обучения модели с датчиков

Поскольку теперь у нас есть базовое представление о машинном обучении, мы можем начать со сбора данных, которыми воспользуемся, чтобы обучить наш алгоритм ML. В руководстве, которому я следовал, сбор данных был неуклюжим: через монитор порта. Мне было бы очень сложно сделать то же самое, потому что пришлось бы носить устройство на запястье во время жеста. Чтобы решить эту проблему, я сделал сбор данных беспроводным. Я воспользовался встроенной флеш-памятью esp8266 и, чтобы было удобнее, отображал статус сбора и сохранения данных на OLED-дисплее. Если вы хотите сделать то же самое, скомпилируйте и загрузите файл Data_collection.ino на свои часы.

После того как вы загрузили код, чтобы проверить его, держите руку неподвижно, как только устройство запускается; оно сначала калибрует акселерометр и гироскоп. Как только калибровка закончится, вы можете начать сбор данных! Просто нажмите кнопку, к которой подключён GPIO-0, и устройство создаст новы признак, а затем просто двигайте руками, чтобы записать движение. Попытки сделать сбор данных беспроводным определённо того стоили! Стал намного проще сбор каждого движения (примерно 25–30 раз), он не доставил проблем. Чем больше у вас будет выборок, тем лучше будет работать алгоритм.

Шаг 8. Обработка данных

Теперь вы можете сбросить собранные данные на монитор порта: просто выключите схему, подключите FTDI и снова нажмите кнопку программы, пока монитор открыт на вашем ПК. Действия сбрасывают все данные на монитор. Затем просто скопируйте и вставьте их в текстовый файл. Каждое движение будет отделено фразой «New feature», чтобы вы знали, какие данные с каким движением связаны.

Затем разделите текстовый файл на 3 файла CSV, используя Excel, на жест свайпа влево, жест свайпа вправо и хлопка. Мы закончили сбор данных.

Эти данные не должны использоваться напрямую; их необходимо обработать: удалить шум, чтобы алгоритм прогнозировал точнее. Но я не делаю ничего такого, что усложняет весь проект, поэтому просто пропускаю всё это и сразу перехожу к обучению алгоритма.

Шаг 9. Обучение модели

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

Для обучения я использую скрипт python, который обучает модель и конвертирует её в файл C, а этот файл, в свою очередь, мы можем использовать с IDE arduino. Вы можете скачать этот файл из моего репозитория на github и открыть файл Classifier.py внутри папки Python training code. В этом коде мы прочитаем файлы csv и обучим модель изучению жестов. мы записывали раньше. Если у вас другие имена файлов, просто измените список Python, который называется fileName, чтобы он обучал модель на основе собранных вами данных.

Этот код создаст файл model.h. Он содержит обученную модель, которая определяет три зафиксированных нами жеста. Если вы хотите просто протестировать модель, вставьте файл model.h в папку «Testing gesture detection» и откройте файл arduino в этой папке. Затем просто скомпилируйте и загрузите код на часы.

Шаг 10. Выводы модели

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

После успешной загрузки кода выполните любой жест. Жест, который вы выполняете, должен быть указан на OLED-дисплее. В моём случае модель работала в 95 % случаев, иногда затрудняясь обнаружить свайп вправо. Возможно, данные, которые я собрал, были зашумлены, или, может быть, когда собирал данные, я делал жесты неправильно.

Как бы то ни было, для меня 95 % – это хорошо, с таким распознанием можно многое сделать!

Шаг 11. Заключительные мысли

Есть так много вещей, которые вы могли бы улучшить, если хотите реализовать это для проекта более высокого уровня. Например, можно увеличить частоту дискретизации во время сбора данных, увеличить количество образцов, которые вы собираете, очистить данные после их сбора, использовать обработку сигналов, чтобы удалить шум и т. д. Но с точки зрения начинающего любителя, даже такая реализация модели машинного обучения в микроконтроллере имеет большое значение! Хотите научиться использовать машинное обучение — приходите учиться, а промокод HABR, дающий 10 % дополнительно к скидке на баннере, вам в этом поможет.

Источник

Создаем современный ретро компьютер на Arduino

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

Меня зовут Евгений, я студент 4 курса МатМеха УрГУ(урфу). Примерно на первом курсе я понял, что меня завораживают старые компьютеры и старые технологии. Примерно тогда же я купил себе советский клон ZX Spectrum`а — Урал-8/64K.

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

Немного раньше я делал небольшие проекты на Arduino и думал над каким то достаточно большим проектом, но сильно не хотел делать еще одну «умную поилку для собаки«, которых в интернете наплодили достаточно, чтобы потерять всякий интерес к этому, не то, чтобы он до этого был большой. Я подумал, мне нравится Spectrum, так почему бы не сделать его эмулятор, софтверных эмуляторов полно, да и физических тоже хватает, но я решил посмотреть что есть и сделать свое. Я погуглил и нашел статью, которая и толкнула меня к первым шагам.

Что мне не понравилось в уже готовых аналогичных проектах, чтобы просто их реализовать:

Абсолютно нечитаемый код на 2000+ строк кода в одном файле с кучей комментариев — иногда не только пояснительных но и кода.

Мощные платы — в моей задумке было использовать простенькую Arduino Nano, а точно не Mega!

Много плат — опять же хотелось использовать всего одну простую плату и вместить на нее все необходимое.

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

Начинаем разбор

Посмотрев на различные проекты и доступное оборудование, я решил сделать симулятор не ZX Spectrum, а полностью придуманного вычислителя — со своей системой инструкций, способом коммуникации с оборудованием, и языком ассемблера, соответственно! Приступим к описанию того, как строилась система. Но прежде рекомендую ознакомиться с инструкциями процессора — это основной репозиторий проекта, дальше информация оттуда будет упоминаться.

Одну из проблем, которые я пытался решить — это износ внутренней flash памяти, но оперативной памяти мало для хранения программ написанных для нашего компьютера — её всего 2 кб! Так что я решил использовать SD карту. Изначально я думал хранить на карточке файловую систему и в ней файлы и открывать нужную программу, выгружая её в оперативку. Однако для такой схемы нужна библиотека SD, использование которой не оставит от этой самой оперативки ничего. Я подумал — если понадобится ФС, напишем ее поддержку уже на нашем коде, а не будем занимать память Arduino для этого. Вполне хорошей альтернативой для работы с картой послужила библиотека sd_raw, которая предоставляет доступ к любому байту на карте на чтение и запись туда сырых данных, т.е. просто набора байт. В качестве основного устройства вывода текста я взял черно-белый дисплей Nokia 5110 — точное попадание в ретро стиль, как мне кажется. В качестве клавиатуры, понятно, возьмем PS/2. Остались свободные пины — заткнем пьезо-пищалкой. Мой хороший знакомый нарисовал и напечатал мне корпус, как многие заметили по картинке выше, похожий на корпус Macintosh.

Instruction Set Architecture

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

Т.е. на манипуляции с устройствами выделены некоторые инструкции, такие как:

gkey — получает сканкод нажатой клавиши, если ее не было, то 0.

line x1, y1, x2, y2 — рисует линию по указанным координатам на экране.

play label — играет музыку по смещению label в памяти.

Как же будут устроены обработчики этих инструкций и их опкоды? Я решил этот вопрос очень просто — создал массив функций, теперь в основном цикле работы процессора байт, на который указывает указатель инструкции (IP) — будет восприниматься как номер функции в этом массиве. Если же у инструкции есть какие то аргументы, например операнды инструкции сложения add r0, r0, то соответствующая функция сама об этом знает и дочитывает их. Кстати говоря, поскольку регистров 16, то эту пару из инструкции выше можно упаковать в 1 байт, чем я и воспользовался, чтобы сократить код.

Регистры!

С ISA разобрались, немного о регистрах, их 2 набора по 16 штук — для целых чисел и чисел с плавающей запятой, все 32-битные. Есть инструкции кастинга из одного типа в другой, при этом сами регистры при программировании ничем не отличаются, инструкция сама понимает из какого именно регистра читать/писать (хотя можно сделать псевдонимы, чтобы не путаться). Кстати о плавающих числах, тут нам можно работать с плавающими числами как с целыми в плане использования их в инструкциях, не нужно использовать регистровый стек и обращаться по ссылке в память для получения константы, как это есть в x87. Однако вводя эти числа в свою систему, я не знал как безболезненно преобразовывать их, т.е. без битового разбора числа в формате IEEE754. Оказывается такой способ есть и он очень прост, нам не потребуются никакие логические операции в огромном количестве, а только 2 строчки:

Тут мы просто записали в некоторую область памяти целое число (4 байта), изначально смотрим на них именно так, потом после взятия указателя на эту область (целое число), производим каст его к указателю на вещественное число и разыменовываем. Вся основная работа происходит во второй строчке и читать ее нужно справа налево. В итоге в переменной y окажется число, байтовое представление которого в формате IEEE754 равно числу в переменной x.

Пройдемся по устройствам.

Дисплей — с ним особых проблем не возникало, открыл datasheet, написал библиотеку и все работает, возникла только одна проблема с тем, что необходимо сохранять содержимое буфера экрана (508 байт) в какой-то памяти для изменения отдельных пикселей. Хорошо бы можно было в оперативке, но места мало — примерно столько же ест массив инструкций и столько же библиотека sd_raw, в совокупности с прочими расходами на регистры и другие переменные и массивы. Это оставляет около 150-200 байт на локальные переменные, чего может оказаться мало. Но быстро стало ясно, что инструкции никто не будет менять (хотя идея интересная — налету подменять инструкции процессора), поэтому было решено перенести их во внутреннюю flash память, используя ключевое слово PROGMEM в Arduino IDE, которое позволяет сохранять константы любых типов в неизменяемую память, освобождая оперативную. Таким образом решилась проблема нехватки памяти для экрана.

Клавиатура — тут интереснее то, что происходило в программном эмуляторе нашего девайса, но об этом позже. В остальном, я пока что с реальной клавиатурой разбираюсь — там нужно выбрать хорошую (для наших целей) таблицу сканкодов (их 3 в PS/2) и понять как легко транслировать их в символы ASCII, или хотя бы как это делает DOS.

Карта памяти — в какой то момент мне пришло осознание, что даже, если у нас регистры 32-битные и карта не меньше 4 Гб, но мы все равно не можем адресовать больше 2 Гб — это связано с тем, какую плату картридера я установил в наш вычислитель — она не поддерживает карты большего объема. Это налагает ряд проблем — теперь мы можем использовать только карты на которых указан объем 2Гб, они всегда на самом деле меньше и даже так — они различаются по объему. Но так как раньше и IP и SP — специальные регистры были по умолчанию установлены в 0, то теперь для адекватного использования стека (который растет вниз) нужно знать верхнюю границу памяти. И очень кстати в библиотеке имелась функция для чтения заводской информации, а помимо производителя там было поле capacity (емкость). Вот именно в это значение мы и устанавливаем теперь SP перед началом основного цикла процессора.

Пьезо-пищалка — здесь было совершено большое открытие для меня — что delay в Arduino не такой уж блокирующий, как нам все говорят. Остановимся поподробнее.

Начну с того, какая задача стояла. У нас есть набор пар частот и задержек, мы хотим перебирать их и проигрывать на пищалке функцией tone, которая как раз принимает пин, частоту и задержку, пищит с нужными параметрами и отключается по прохождении задержки. Перебирать нужно не абы как, а когда исполнится инструкция play, которая раньше упоминалась. Она укажет, где лежат эти ноты и запустит проигрывание, но стандартная функция tone не умеет по циклу ходить и изымать частоты с задержками. Значит нужно как-то детектировать то, что текущая нота должна уже закончиться и пора бы включить следующую. Самым простым решением будет в основном цикле процессора следить за этим с помощью millis, которая возвращает время прошедшее с запуска контроллера в миллисекундах. Но самое простое — не самое эффективное, у нас инструкции не имеют фиксированного времени исполнения, та же delay может занимать достаточно длительное время, не давая переключить ноту. Дальнейшим решением для меня было — покопаться в исходниках функции tone и создать аналог, который принимает обработчик завершения тона, обычно это была функция, которая отключает таймер, но теперь мы ее подменяем и вместо отключения мы включаем новый тон. После того, как проигрывание завершается нужно позвать noTone, чтобы вызвать правильный обработчик и подменить его обратно. Вроде все хорошо, все работает! Однако не совсем. Я загрузил следующий код:

Оказалось, что проиграв одну ноту, он выключал проигрывание. Почему же. Обратите внимание на delay, если зайти в её исходники, то мы замечаем функцию, которая вызывается перед циклом задержки — yield, я мало чего про нее нашел, но, как я понял, это макрос, в который мы оборачиваем код и как-то можем параллельно его исполнять с основным кодом, если кто знает точно — поделитесь. Я сам попробовал решить эту проблему и у меня получилось! Покажу на примере. У нас есть такие строчки в функции noTone —

Первая строка тела записывает логический 0 в указанный выход, а вторая подменяет обратно обработчик. Что будет если их поменять? Все сломается, конечно. Вспомним, что у нас delay не совсем блокирует, а именно, код будет исполняться до «digitalWrite«, перед чем заблокируется. Если строчки будут в другом порядке, то вот эта замена приведет к тому что у нас старый обработчик вернется и будет отключать таймер, на котором работает пищалка. Таким образом, экспериментально проверено, что delay блокирует код, если он изменяет состояние пинов, иначе он исполняется. Каким образом это происходит — на это у меня ответов пока что нет.

Прочий софт

Вот такие были пироги при работе с периферией нашего компьютера. Помимо его самого, для него я сделал транслятор ассемблера в машинные коды, чтобы писать начальные программы было удобнее — работает на Python. На нем начал реализовывать оболочку, которая умеет разбирать FAT32, ходить по ее директориям и исполнять файлы. Так же для удобства отладки был написан эмулятор девайса — он использует общий код с самим компом. Рассмотрим подробнее оболочку и эмулятор.

Оболочка

Компьютер начинает исполнение инструкций с 0 байта. На карте памяти было решено создать файловую систему FAT32 и использовать ее резервные секторы для нашего загрузчика. Как правило, начало самого первого сектора содержит служебную информацию, но самые первые байты не критичны к изменению, а именно нам надо изменить первые 5 байт на инструкцию перехода к первому байту загрузчика. Ок, мы попали в загрузочную секцию, разобрали все байты, рассчитали константы в удобном виде. Сохранили это все, теперь, чтобы не пересчитывать это при каждом старте и не перезаписывать подменим еще раз инструкцию перехода на блок непосредственной загрузки. Можно видеть это в этом коде:

Про FAT ничего не буду рассказывать, можно посмотреть какие там есть поля, как строятся файлы и про терминологию в этом видео:

У нас в программе есть поле «текущая директория«, она обозначает номер первого кластера директории. Таким образом если мы в какой то директории начали работу при выключении компьютера, то при включении мы в ней и начнем работу. Выбирать файл в директории можно клавишами стрелками вверх-вниз, исполнять по Enter, при этом если это директория, то она становится в качестве текущей. Кстати говоря, под исполнение нужно выделить место. Поскольку доступно максимум 2 Гб, то 1 нижний гигабайт мы отдадим файловой системе, а старшую область под исполнение. Поскольку экран у нас может вмещать 6 строк текста по 14 символов, то мы можем отображать имя и тип файла (файл или директория) в каждой строке, а между строками перемещаться, используя скользящее окно.

Эмулятор

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

Экран изначально был приложением на SFML, но это влияло на работу с клавиатурой, создавая две проблемы. Во-первых, нужно понимать, что SFML это библиотека для игр, поэтому там не получить никаких нормальных сканкодов, значит надо их получать откуда-то еще, но если фокус на окне SFML, то оно получает все события клавиатуры, поэтому приходится переключать фокус на консоль, но так чтобы и окно было видно. Во-вторых, SFML это графическая библиотека, т.е. в текстовой консоли (одной из тех, что доступны по ALT-CTRL-Fx) ее нельзя использовать для рисования чего-то на экране.

Хотя хотелось бы перейти в текстовую консоль, поскольку есть способ получения совсем хороших сканкодов — прямо таких, какие приходят от клавиатуры. Это показывает утилита showkey, но работает она только в текстовой консоли, так как графика в Linux для обработки хоткеев всегда читает /dev/console. Суть этого метода — перейти в неканонический вид консоли, где мы будем получать сырые сканкоды. Я просто нашел исходники утилиты showkey и подправил их для своих нужд, заодно научился компоновать программу на С++ с функциями на С — это нужно поскольку showkey написана на С, а весь проект мы пишем на плюсах. Ну а для отрисовки экрана мы воспользуемся популярной библиотекой для консольной графики — ncurses. Чтобы все отображалось хорошо, нужно настроить консоль так, чтобы размер шрифта вместо 8×16 был 8×8 — красиво, как пиксели на экране.

Заключение

Вот, вроде бы все основное рассказал. Вообще говоря, там еще много всего можно рассказать, с чем сталкивался по пути, но это, скорее, просто занимательные мелочи, чем что то важное. Столько удачных костылей было вставлено — о некоторых рассказал, о других молчу.

Сейчас делаю сетевой интерфейс для передачи по встроенному UART — нашел способ без дополнительных проводов, только UART и диоды, собрать общую шину из таких компьютеров и передавать между ними данные. Так же думаю как сделать транслятор из, скажем, wav в описанный формат музыкальных файлов. Еще поглядываю в сторону нормального компилятора на основе LLVM, но пока только мысленно, потому что итак есть чем заняться с этим проектом.

Спасибо за внимание, присоединяйтесь к проекту. Оставлю ссылки.

Источник

Adblock
detector