Русский
Русский
English
Статистика
Реклама

Микроконтроллеры stm

Per aspera ad astra, или как я строил ракету. Часть 2. Собираем альтиметр на STM32 и BMP280

26.09.2020 16:11:14 | Автор: admin


Всем привет!

В предыдущей части я остановился на том, что мои ракеты удачно взлетели и приземлились, а на одной даже был установлен альтиметр. В этой статье я и расскажу о том как сделать простой высотомер на основе STM32 Nucleo L031K6 и датчика давления BMP 280 , который к тому же хранит все данные во Flash памяти.

Выбор железа


Основные требования к альтиметру:
  1. Высокая скорость считывания высоты, так как ракета в апогее находится не слишком долго, а я хотел узнать именно максимальную высоту;
  2. Низкое энергопотребление, чтобы не ставить большой аккумулятор;
  3. Небольшие размеры всей конструкции.

Исходя из них в качестве микроконтроллера взял STM32 Nucleo L031K6 (высокая скорость работы, низкое потребление тока, малый размер). Высоту решил измерять с помощью барометра BMP280 (те же резоны, что и у МК). Также добавил кнопку, при нажатии которой начиналась запись высоты. Ну и питала всю электронику батарейка CR2032, подключенная через адаптер. В итоге получилась такая схема:


Использованные модули

STM32 Nucleo L031K6


BMP280


Адаптер для CR2032

Разработка кода


Код вы можете найти на моем гитхабе . Пины STM32 были сконфигурированы в CubeMX под IAR. Для работы с BMP280 использовал вот эту библиотеку, добавил в нее функцию расчета высоты над уровнем моря с помощью барометрической формулы и инициализацию датчика с нужными мне параметрами частоты считывания, фильтрации и тд. Так как я хотел измерить высоту полета относительно земли, мне нужно было сначала вычислить высоту над уровнем моря в моей местности, взять ее за ноль и относительно нее измерять высоту полета. Частота измерений равнялась 10 Гц.

Запись во Flash память происходила следующим образом так:
Организация памяти в STM32 L031K6


  • Для всех измерений выделил 8 Кбайт с 0x08006000 по 0x08007FFF адреса
  • На одно измерение выделил 2 байта
  • Во Flash записывал по 4 байта, то есть сразу два измерения
  • Максимальное количество измерений 4096, этого хватало на запись примерно 7-ми минут полета
  • Высоту записывал в сантиметрах для большей точности

А происходила запись следующим образом:
  1. Если итератор записи четный, то в переменную с данными для записи во Flash сохраняем текущую высоту в младшую половину слова;
  2. Если итератор записи нечетный, то в переменную с данными для записи во Flash добавляем текущую высоту в старшую половину слова и сохраняем эту переменную в ячейку Flash

В итоге алгоритм работы программы следующий:
  1. После включения 5 секунд ждем нажатия кнопки для старта измерений высоты.
  2. Если кнопка не была нажата, то зажигаем встроенный светодиод и начинаем передачу по UART данных о высоте, записанных во Flash памяти
  3. Если кнопка была нажата, то два раза моргаем встроенным светодиодом и вычисляем высоту местности.
  4. После вычисления нуля два раза моргаем встроенным светодиодом и записываем во Flash-память высоту ракеты над землей.
  5. Когда выполнили передачу по UART или завершили измерения высоты, бесконечно моргаем встроенным светодиодом;
  6. Ждем пока нас найдут люди и выключат.


При питании STMки от CR2032 через пин 3.3V обнаружил, что код не работает. Проблема была в том, что при подаче питания через эту ногу необходимо было отпаять SB9 (расположен рядом с выводами RX и TX на обратной стороне МК) иначе плата постоянно перезагружалась.

Теперь необходимо было проверить точность работы альтиметра. Взяв рулетку, я стал поднимать альтиметр на разные высоты и смотреть, что он измеряет. Результаты тестов лежат в соответствующей папке на гитхабе. В текстовых файлах сырые данные с STMки, а в Excelевских таблицах находятся красивые графики всех тестов. Точность соответствовала заявленной 10см. Следует помнить, что высоту я измерял в сантиметрах, поэтому в таблице такие большие числа.

Сборка альтиметра


Так как во время приземления ракета может сильно ударится о землю, необходимо было хорошо зафиксировать всю электронику, чтобы при тряске не отваливались проводки, или, того хуже, сами модули. Альтиметр разместил в головном обтекателе (места там было достаточно, и стабильности за счет смещения центра тяжести к головному обтекателю прибавилось) в 3D-печатном креплении. STMка стояла вертикально, BMP280 контактами вверх и под крепление приклеил адаптер для CR2032. Из-за того, что он не помещался в корпус ракеты, пришлось немного сточить контакты минуса. Рядом с контактами в боковой стенке 3D-печатного крепления проточил вертикальную канавку, чтобы протянуть через нее минус от CR2032, а под плюсом просверлил отверстие и пустил провод через него. Думал крепить альтиметр к головному обтекателю с помощью самореза, поэтому в корпусе есть отверстие, но потом от этой идеи отказался.


Модель крепления, напечатанного на 3D-принтере

Собранный блок альтиметра

Вид сверху


Вид снизу

Кнопку приклеил рядом с BMP280, установил остальные модули в свои места, припаял все провода и замотал все синей изолентой залил для надежности термоклеем.

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


Собранный альтиметр. Вид спереди


Вид сзади. Видна резинка, соединяющая альтиметр с ракетой

Альтиметр был готов! Теперь предстояло его испытать, а это значит, что я снова отправился на полигон!

Запуск альтиметра и результаты измерений


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

В итоге график получился таким:


По горизонтали номер измерения. Каждые 10 измерений 1 секунда. По вертикали высота в сантиметрах

Ракета взлетела на 15м, затем устремилась в землю. После прохождения апогеячерез 1 секунду началась какая-то аномалия: после значения 12м почему-то показания упали до -8м. Это произошло в момент второго запуска двигателя (которого не должно было быть), так что не исключаю, что неисправный двигатель как-то повлиял на альтиметр. Во всех остальных тестах он работал отлично, так что это была проблема явно не в электронике. В общем, те испытания альтиметра были успешными лишь наполовину, так как во вторую половину полета произошла аномалия. Сам график вы можете найти на гитхабе, он называется rocket_flight_fall_test.

После ремонта ракеты я снова отправился на полигон и в этот раз испытания прошли успешно. Полет был отличным, показания альтиметра были стабильными и соответствовали полету. График полета получился вот такой:


По горизонтали номер измерения. Каждые 10 измерений 1 секунда. По вертикали высота в сантиметрах

Ракета поднялась на 150м и успешно приземлилась. Таким образом это испытание было полностью успешным. Я удостоверился в том, что альтиметр работает и приступил к разработке новой бортовой аппаратуры.

Заключение


В итоге, я полностью собрал компактный альтиметр, который помещается в небольшую модель ракеты. Испытания прошли успешно, электроника пережила взлет и посадку и измерила высоту полета. На этом проект разработки альтиметра я закончил, возможно в будущем использую его в одной из ракет, потому что знать высоту полета иногда очень полезно (например, если вы запускаете ракеты ради достижения максимальной или какой-нибудь определенной высоты). Сейчас, как я уже говорил, я занимаюсь разработкой бортовой камеры с радиопередатчиком, потому что ракету с такой серьезной электроникой я терять не намерен.

Спасибо за внимание!
Подробнее..

Солнце в коробке

04.10.2020 22:08:33 | Автор: admin
Это мой лучший друг: всегда в хорошем настроении и не задаёт лишних вопросов Леон о растении.


Не секрет, что зеленые друзья расползлись по подоконникам многих из нас, вот и мне захотелось вырастить у себя что-нибудь. Да не просто что-нибудь, а что-нибудь плодоносящее. К сожалению, в квартирных условиях обеспечить необходимые для этого условия весьма непросто. Однако попытаться стоит. Так, и взбрела мне в голову мысль разработки устройства для решения этой задачи умного гроубокса. Конечно, идея не нова, но найти что-нибудь интересное, да еще и настольного формата среди готовой продукции мне не удалось и захотелось соорудить кое-что своё. Дело дошло до версии 2.0 и вот, я готов поделиться полученными результатами.

Вначале был прототип.


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


[ функциональная схема устройства (версия 1.0) ]

Получилась весьма увесистая конструкция, внушительных размеров, тем не менее, работающая и пригодная для эксплуатации. В качестве корпуса использовался вырезанный деревянный бокс, напичканный бюджетной электроникой:
для освещения были выбраны светодиодные ленты красно-синего и тепло-белого спектров свечения; для вентиляции стандартные 12 В кулеры; потенциометр как регулятор скорости оборотов кулеров с возможностью отключения; датчик температуры и влажности с выводом на ЖК-дисплей; розетка с таймером на освещение и сетевой блок питания. Открытием стала система капельного автополива Blumat. Под спойлером фото первой версии гроубокса. Основная цель заключалась в получении плодов какого-либо сорта.


[ внешний вид гроубокса 1.0 ]

Собрав этого Франкенштейна, я отправил острых перцев с боевым названием Огонёк на разведку. Было интересно понаблюдать за полным циклом роста растений поэтому в начале марта я высаживал их семена, накрывая почву пленкой на несколько дней. Режимы освещения были следующими: во время роста световой день длился 18 часов, а во время цветения 12. Примерно через три месяца я увидел первые цветочки. Опылять пришлось искусственно (кисточкой) из-за того, что завязи сами никак не образовывались. В итоге они успешно отцвели два раза за полгода в начале лета и осенью.


[ цветение перцев и первый урожай ]

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


Версия 2.0.


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

Про электронику


Функциональная схема, по сравнению с первой версией, претерпела несколько важных изменений: добавился датчик жидкости, а контроль и управление системой было решено перенести на WEB-интерфейс. В остальном, функционал сохранился, однако реализация существенно поменялась. Так, работа над новой версией началась с выбора электронных компонентов. Архитектура новой электронной начинки приведена на рисунке ниже. Блок питания на 48 В выбран из расчета применения цепей 3 Вт светодиодов.


[ структурно-функциональная схема электроники (версия 2.0) ]

Хост-плата


Для связи с внешним миром новый гроубокс имеет встроенный WEB-сервер. Честно говоря, до данного проекта я не имел никакого опыта разработки пользовательских интерфейсов (UI), но такой подход мне показался целесообразным из-за возможности отображения интерфейса как локально (браузером на дисплее), так и удаленно (в браузере подключенных устройств), ничего при этом не переписывая. Таким образом, на UI были возложены две группы задач:

Управление:
  • настройками хост-платы;
  • освещением;
  • вентиляцией.

Мониторинг:
  • состояния освещения (вкл\выкл);
  • температуры и влажности;
  • уровня воды (есть\нет);
  • состояния вентиляции.


WEB-сервер располагается на хост-плате (см. структурно-функциональную схему), которая, в свою очередь, осуществляет обмен данными с остальными компонентами системы.
Признаюсь, в качестве первой реализации, я взял разработанную ранее платформу CNCU-01 на базе микроконтроллера STM32F407, где, подняв WEB-сервер, я уткнулся в проблему недостатка памяти для хранения картинок и html-страниц. Возможно, в целях энергоэффективности, я вернусь к этому варианту т.к. данные можно хранить на microSD карте, но пока я решил взять платформу на базе микропроцессора с Linux. Выбор пал на Khadas VIM1, как на плату с лучшим, по-моему мнению, соотношением функционала и стоимости. Основными критериями выбора были:

  • работа с известными дистрибутивами (Ubuntu \ Debian \ Armbian);
  • наличие eMMC для хранения образа ОС и данных;
  • наличие отдельного разъема (помимо USB) для питания;
  • нормальная работа при пассивном охлаждении;
  • наличие UART, I2C, WiFi, ETH, HDMI, USB(900mA), GPIO.

Также преимуществом данного одноплатного компьютера было наличие широкого выбора уже собранных образов разных дистрибутивов Linux. В этом проекте я использовал Ubuntu LTS 18.04. Большинство необходимого уже было в составе образа, оставалось лишь все это настроить и запрограммировать. Первым делом потребовалось сконфигурировать шину I2C. По умолчанию частота SCK, выставленная производителем в 400 кГц, не подходила для моей длины линии (~5м) фронты тактовых сигналов оказывались заваленными из-за паразитной емкости провода и корректный обмен данными был невозможен. Выход был в понижении частоты до 100 кГц исправлением файла конфигурации платы devicetreeblob (.dtb). Отдельно хочется отметить живой форум и тех. поддержку Khadas, оперативно реагирующую на возникающие вопросы.

Для реализации WEB-интерфейса мне понадобилось знакомство с HTML, CSS и JS. Усложнять их изучение постижением популярных фреймворков я не стал (да и времени на это не было), поэтому фронтенд получился пусть и уродливым внутри, зато, как по мне, миловидным снаружи (благо, с дизайном мне очень помогла девушка). Бэкенд представляет из себя связку NGINX WEB-сервера, FastCGI и С++ приложения. На момент написания статьи пользовательский интерфейс выглядит следующим образом:


[ WEB-интерфейс гроубокса 2.0 ]

Рабочие настройки и показания датчиков хранятся в sqlite базе данных. Визуализация температуры и влажности реализована при помощи модифицированной библиотеки Chart.js (мне потребовалась поддержка двух Y-осей). Режимы освещения и вентиляции задаются расписаниями каждому устройству соответствует свой таймер и статическое значение рабочей мощности. Более хитрые алгоритмы автоматического управления пока находятся в разработке.


WiFi vs HDMI



Как говорилось ранее, гроубокс 2.0 задумывался как сетевое устройство: для регистрации в сети он поддерживает как статическое присваивание IP адреса, так и динамическое, но прежде, чем получить доступ к его WEB-серверу (UI), необходимо собственно этот адрес определить. Так, для пользователя стал необходим интерфейс для первоначальной настройки устройства, чтобы затем вывести его в локальную сеть. Кроме того, такой интерфейс пригодился бы для работы в оффлайн режиме.

Изначально, для решения этой задачи, я планировал использовать HDMI TouchScreen дисплей. UI отображался бы на дисплее через chromium в режиме киоска и управлялся бы при помощи виртуальной клавиатуры. Но все оказалось не так просто сенсорная панель по умолчанию не поддерживалась в предлагаемых на сайте Khadas образах Ubuntu Desktop, однако, за пару дней общения на форуме, разработчики и сообщество помогли мне собрать новый образ с необходимыми модулями и тач заработал. Для реализации виртуальной клавиатуры в Ubuntu нашлось предостаточно готовых средств, среди которых я выбирал между florence и onboard. Данные клавиатуры позволяют гибко менять свой внешний вид, что дает возможность качественно настроить их под свое приложение.
Прим. Недавно разработчики Khadas опубликовали версию Ubuntu Desktop с поддержкой Gnome, где есть уже встроенная весьма удобная виртуальная клавиатура.


Тем не менее, несмотря на проделанную работу, все это мне не пригодилось. Очевидной проблемой при работе с дисплеем было повышенное энергопотребление хост-платы и больший ее нагрев из-за работы графического процессора. Без активного охлаждения с дисплеем работать все же не рекомендуется, а мне очень хотелось избежать лишних шумов кулера. Конец моим сомнениям пришел, как мне кажется, в виде знака. Дело в том, что пока я работал с одноплатным компьютером без дисплея он был запитан от блока питания 48 В через 48VDC-5VDC преобразователь. Я подозревал, что такое сочетание падения напряжения и тока потребления платы близки к максимальным возможностям преобразователя, но забыл об этом, когда стал подключать дисплей. Стоит уточнить, что дисплей запитывается от USB порта хост-платы. Таким образом, увлеченный кастомизацией виртуальной клавиатуры, вскоре я почуял характерный запах горелого пластика и экран отключился. Отключился, как оказалось, навсегда. DC-DC Преобразователь сгорел, успев, по видимости, выдать аномальный импульс на выход, убивший кулер хост-платы и матрицу дисплея. Одноплатный компьютер сначала тоже пал замертво и находился в коме минут 20, после чего входной самовосстанавливающийся предохранитель оправдал свое название, и плата ожила, чему я был несказанно рад. К несчастью, лишнего дисплея у меня не было, и произошедшее я воспринял как знак для пересмотра выбранного решения. Обнаружив, что большинство людей уже давно носит беспроводной дисплей смартфон в кармане, я подумал, что необходимости в еще одном дисплее в составе устройства на самом деле и нет. Вместо этого можно создать подключение по воздуху и транслировать UI на экран телефона. Так я и поступил. На выбор имелось два канала связи: Bluetooth и WiFi, но т.к. браузеры смартфонов, как известно, уже работают по WiFi, то на нем я и остановился, дабы не городить лишнее приложение. На хост-плате я настроил точку доступа (WiFi AP), к которой можно подключаться со смартфона и через браузер открывать WEB-интерфейс гроубокса. WEB-интерфейс, в свою очередь, имеет адаптивную версию для мобильных устройств. Такой подход мне показался оптимальным по удобству использования и трудозатратам. Кроме того, уход от решения с дисплеем избавил от необходимости прокладки в корпусе двух кабелей и снизил рабочую температуру одноплатного компьютера, что позволило перейти на пассивное охлаждение.

image

[ подключение смартфона к гроубоксу 2.0 ]


Фитосветильник и контроллер FLC-01



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

Размеры освещаемой площади в этой версии стали 400 х 400 мм, поэтому в качестве корпуса фитосветильника я выбрал алюминиевый радиатор 300 х 400, с запасом, чтобы распределить светодиоды как можно свободнее. Радиатор крепится к конструкционному профилю и выполняет функцию верхней крышки бокса. На китайском рынке я нашел готовые алюминиевые панели для светодиодов подходящий габаритов. Их я закрепил на радиаторе, предварительно промазанном термопастой, и запаял светодиоды по схеме. Из расчета 250 реально излучаемых Вт на 1 облучаемый м2 (или 40 Вт / 0,16 м2 в моем случае) я взял 41 трехваттный светодиод. Стоит уточнить, что я использовал 3 независимые группы светодиодов (красные 660 нм, синие 445 нм и белые 2900К/4000К), в которых падение напряжения на отдельном светодиоде отличается. Так, например, на синем светодиоде падает в районе 3,4 В, тогда как на красном 2,4 В. Кроме того, насиловать светодиоды номинальным током в 700 мА я не стал и выбрал 350 мА в качестве рабочего значения (чтобы продлить им жизнь) при максимуме в 500 мА (максимум драйвера). По измерениям пришедших ко мне светодиодов получилась следующая таблица:



Исходя из вышесказанного, считаю максимальную реальную полученную мощность фитосветильника:

Pmax = 6 (0,5 3,4) + 5 (0,5 4,5) + 12 (0,5 3,4) + 18 (0,5 2,4) = 63,45 [Вт]

и реальную рабочую мощность при токе 350 мА:

Pраб = 6 (0,35 3,4) + 5 (0,35 4,5) + 12 (0,35 3,4) + 18 (0,35 2,4) = 44,42 [Вт]

При этом каждой линии светодиодов (красной, синей и белой) хватает 45 В для питания (3 В падает на драйвере для светодиодов, согласно документации), так что блока питания на 48 В 4.4 А как раз достаточно. Распределяя светодиоды, я старался обеспечить максимальную однородность излучаемого света. Получившееся в итоге представлено на фото ниже.


[ схема распределения светодиодов][ вмонтированная в крышку лампа ]

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

Для такого управления светодиодами понадобился специальный контроллер с трехканальным диммируемым светодиодным драйвером, да еще и с цифровым каналом связи. Собрать его я решил сам, не найдя ничего подходящего на рынке. В качестве драйверов для светодиодов были найден источники питания серии LDD-H, а именно LDD-500H на 500 мА. Конструкция платы разработанного контроллера позволяет установить любой необходимый драйвер этой серии, т.к. они имеют одинаковые корпуса и для них предусмотрены специальные посадочные места типа кроватка. Таким образом, при желании можно еще повысить мощность фито-светильника, заменив драйверы на LDD-700H с током 700 мА. Мозгом же платы был выбран популярный микроконтроллер STM32F103. Именно он управляет светодиодными драйверами посредством ШИМ-сигналов, устанавливая тем самым выходной ток. Для связи контроллера с внешним миром поддерживается несколько каналов связи: UART, RS232 и Bluetooth 4.0 BLE(в зависимости от сферы применения).Bluetooth канал построен на основе модуля HM-10.Также контроллер имеет встроенный таймер включения и отключения драйверов светодиодов для установки длительности светового дня растений. Чтобы таймер не сбивался при перепадах напряжения питания, он работает на основе часов реального времени (RTC) и на плате установлен разъем под батарейку CR2032 для резервирования питания часов. Сама же плата питается в диапазоне 1550 В, подавая входное напряжение в том числе и на светодиодные драйверы. В процессе эксплуатации от 48 В я столкнулся со значительным нагревом линейного стабилизатора напряжения LM317HVT и не стал дожидаться его выхода из строя, сделав отдельный вывод для питания логической части от DC-DC преобразователя на базе LM2596HV. В следующей ревизии платы планируется исправить данный недочет.


[ плата FLC-01: вид сверху ][ плата FLC-01: вид снизу ]

Разработанная для контроллера прошивка позволяет регулировать выходной ток каждого драйвера в пределах от 0 до 100% с шагом 1%, то есть для рабочего значения в 350 мА необходимо выставить 70% (5000.7). Протокол общения максимально прост (есть общая структура текущего состояния контроллера, нужно что-то поменять пишешь ему об этом), и построен на формате JSON:


[ пример обмена данными ]

и даже имеет простенький GUI на Python3, если вдруг понадобится подключать контроллер к ПК.


[ графический интерфейс FLC-01 ]

Для управления по Bluetooth я использую Serial Bluetooth Terminal. Это оказалось не очень удобным в использовании (отдельное приложение все же предпочтительнее, однако его еще нет), но очень удобным при отладке, когда плата уже встроена, а подключиться и посмотреть происходящее надо. Для связи с хост-платой используется классический UART.


Соединительная плата (SensorsBoard)



Ни для кого не секрет, что у разных датчиков разные интерфейсы связи. Для своей системы я выбрал следующие сенсоры:

  • датчик температуры и влажности SHT30 питание 3.3 В, работает по I2C;
  • бесконтактный датчик жидкости XKC-Y26-V питание 12 В, подает на выход уровень питания если жидкость есть и GND в противном случае;
  • 4-pin кулеры Noctua NF-A12x15 питание 12 В, скорость вращения регулируется ШИМ-сигналом.


Как видно из этого перечня, напрямую к хост-плате Khadas VIM1 можно подключить разве что датчик температуры-влажности т.к. кроме 3.3 В и 5 В на хост-плате напряжений нет. Для подключения датчика жидкости и вовсе нужен преобразователь 12 3.3 В чтобы подать сигнал на порт процессора. В связи с этим, потребовалось сделать еще одну плату для стыковки датчиков и кулеров с одноплатным компьютером. В качестве канала связи я использовал шину I2C, где ведущим устройством выступила хост-плата, а двумя ведомыми стали датчик SHT30 и мозг соединительной платы уже известный микроконтроллер STM32F103. На этот раз задачей микроконтроллера стала генерация ШИМ-сигналов частотой 25 кГц для двух кулеров, скважность которых задается командами по I2C. Плату собрал дешево и сердито на макетке, используя STM32-Bluepill. Питание осуществляется от общего блока питания 48 В поэтому на входе используется DC-DC преобразователь LM2596HV-ADJ на 12 В.

Не обошлось и без подводных камней даже в столь простой схеме. Когда я рассчитывал делитель напряжения 12 3.3 В я не учел один неочевидный момент на выходе датчика жидкости уже был встроен токоограничивающий резистор. Этот резистор становился плечом моего делителя и значительно влиял на выходное напряжение. При помощи мультиметра я определил его номинал (20 кОм) и внес коррективы в схему. Выход данного делителя подается на входной порт хост-платы, поэтому для надежности я добавил стабилитрон на 3.3 В, который на схеме не показан.


[ схема подключения датчика жидкости ]


Про корпус



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

Каркас было решено собирать из конструкционного алюминиевого профиля серии 20 с V-пазом. Работать с ним оказалось крайне приятно все размеры подобраны, крепления аккуратно закручиваются и сходятся с пазами. Главное было заранее определиться с габаритами и набором необходимого крепежа. Так, проектирование корпуса началось с 3D-моделирования.Честно говоря, сперва я думал, что модель ограничится лишь каркасом и понадобится лишь для определения основных размеров и типов соединителей, но по мере проектирования стали проявляться неочевидные нюансы, о которых просто не задумываешься без картинки перед глазами. В итоге я стал моделировать каждую деталь и соединение, что помогло избежать многих ошибок в дальнейшем. В этом проекте я познакомился с Autodesk Inventor 2020 и остался очень доволен этой системой автоматизированного проектирования. В ней детали легко рисуются по эскизам, а затем объединяются в единую сборку модели. Кроме того, без лишних манипуляций я сразу мог подготовить нужную деталь для печати на 3D-принтере. Рабочая модель представлена на изображении ниже:


[ 3D-модель корпуса версии 2.0 ]

Внешние габариты конструкции: 440 Ш х 503 В х 540 Г[мм]
Пространство для растений: 400 Ш х 370 В х 400 Г [мм]

В качестве стенок используется прозрачное оргстекло. Чтобы излучаемый фито-лампой свет не выбивался наружу, оргстекло обклеено полузеркальной пленкой (коэффициент отражения 92%, пропускания 8%). Таким образом, становится возможным наблюдение за растениями при наличии света внутри бокса, а при отсутствии внутреннего освещения бокс превращается в зеркальный.


[ эффект полупрозрачных стенок ]


Подача воды производится в подпольный бак объемом 10 л по выведенной нейлоновой трубке. Имея внешний диаметр 7 мм, она плотно втиснута в паз алюминиево профиля. Горлышко трубки пролезает через соединительный куб и выглядывает из края верхней крышки корпуса. Для удобства пополнения бака я напечатал своеобразную воронку под диаметр отверстия трубки.

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


[ система подачи воды ]

Немаловажной характеристикой гроубокса 2.0 для меня была громкость работы. Как известно, вентиляторы и блоки питания с активным охлаждением могут издавать раздражающее жужжание. Пытаясь максимально этого избежать, я поставил кулеры Noctua с адаптером Low Noise и блок питания с пассивным охлаждением, что обеспечило нужный эффект бесшумной работы.

Для закрепления плат на задней стенке я напечатал на 3D-принтере специальные крепления, которые приклеиваются к оргстеклу и имеют монтажные стойки под винтики. Таким образом, электроника за исключением датчиков расположилась в задней части корпуса (все провода брались с запасом поэтому вышло не так красиво, как хотелось бы):


[ расположение электроники на задней стенке ]

И напоследок небольшой демо-ролик получившегося устройства:





Итоги



Чем дальше проект развивается, тем интереснее он становится. Новые ответы порождают новые вопросы. У текущей версии все еще немало недостатков и особенностей, требующих доработок. Диапазон решаемых задач растет и, по понятным причинам, все сложнее удается с ним справляться в одиночку. Не знаю как далеко я зайду, но эксперименты продолжаются и надеюсь принесут плоды.

P.S. Высадил подопытную зелень в начале сентября взошла за дне недели. Продолжаю наблюдения. Всем хорошего урожая!

Подробнее..

Делаем бесконечную карту памяти для PS1

22.04.2021 20:09:52 | Автор: admin

PS1 (она же PSX, она же PS One) это первое поколение игровых консолей PlayStation от Sony и относится к пятому поколению игровых консолей вообще. Она использует 2х скоростной привод для чтения CD. Такой большой объём данных по меркам актуального для приставки времени позволял игроделам особо не оглядываться на ограничения при создании контента для игр, что делало последних более качественными, по сравнению с играми предыдущего поколения приставок. А ещё, игры теперь могут быть длинными. И если любая игра за редким исключением на консолях предыдущих поколений вполне себе могла быть пройдена за одну игровую сессию, то с играми PS1 всё обстояло иначе. Для сохранения прогресса у PlayStation предусмотрены карты памяти: маленькие сменные модули энергонезависимой памяти.

Если вам интересно, как именно устроена карта памяти PlayStation 1, как она работает и как можно создать свою добро пожаловать под кат.

Итак, карта памяти PS1 является стандартным периферийным устройством, как и весь зоопарк джойпадов, джойстиков и прочих аксессуаров. Чтобы понять, как именно она работает, нужно для начала посмотреть, что у неё внутри.


Фотография печатной платы стандартной карты памяти на 15 блоков

Как видно из фото, устройство карты очень простое: контроллер, который обслуживает запросы системы, и, собственно, энергонезависимая память, которая представлена стандартной NOR FLASH. Логически, карта памяти разбита на 15 блоков, которые игры могут использовать. Может показаться, что 15 не логично для двоичной системы, но тут противоречия нет: один блок отдан под файловую систему, там хранятся имена файлов и даже анимированные иконки, прям как потоки у NTFS. Каждый блок имеет размер 8 КиБ, 16 блоков в сумме это 128 КиБ, что и видно по маркировке FLASH памяти на фото выше.

На первых порах этого хватало всем, но потом стали появляться игры, которые использовали более одного блока за раз. Например, некоторые симуляторы, вроде Sega GT, используют 4-5 блоков, а Constructor так вообще всю карту памяти на 15 блоков. Это вынуждало покупать больше карт и ситуация грозила стать как с дискетами или картриджами. Но потом подтянулись пираты и стали выпускать карты на 2, 4 или 8 страниц разом. И переключались страницы либо по хитрой комбинации на джойпаде, либо явной кнопкой на самой карте памяти. Правда, в картах более 2х страниц применялось сжатие, и фактическое число страниц было значительно меньше, а некоторые карты могли тупо заблокироваться. И вывести их из этого состояния было очень трудно, но на что только не шли игроки ради своих сохранений. Вот типичные представители многостраничных карт памяти:


Слева карта памяти на 2 страницы, справа на 8. У правой есть аппаратная кнопка перелистывания страниц и индикатор, отображающий число от 1 до 8, который скрыт за тёмным стеклом

Небольшое лирическое отступление.


Всё началось в 2001м году, когда я купил чудо диск для ПК под названием Все эмуляторы, на котором были эмуляторы PS1 в том числе: это были Bleem! и ранний ePSXe. И мой тогдашний комп даже смог играбельно запускать мои диски от PS1! А чуть позже у меня появился модем и я узнал про DirectPad Pro. Подключение родного джойстика к компьютеру (пусть и через LPT) многого стоит. И работала эта система как на 9х так и на XP! А ещё чуть позже, уже в 2002м я узнал про Memory Card Capture Sakura! Эта программа позволяла работать с настоящими картами памяти, используя всё ту же схему подключения DirectPad Pro. Именно тогда у меня появилась идея сделать бесконечную карту памяти, которая бы позволяла обмениваться информацией с компьютером без необходимости дополнительных устройств. Но на тот момент у меня не было достаточно информации и доступной элементной базы, и идея оставалась лишь идеей, теплясь где-то на задворках сознания.

Прошло почти 9 лет как я осознал, что уже знаю достаточно и имею возможность, чтобы реализовать хоть какой-то вариант бесконечной карты памяти. Однако тут вступил уже другой фактор возраст и всё что с этим связано. Времени на хобби всё меньше, забот всё больше. И вот только сейчас я могу предоставить общественности хоть какой-то результат, полноценный Proof of Concept.

Физический интерфейс.


Итак, карта памяти и джойпады работают через общий интерфейс. Количество сигналов в нём 6, вот их названия и назначения:
  • SEL0 Сигнал выбора первого порта, активный уровень низкий
  • SEL1 Сигнал выбора второго порта, активный уровень низкий;
  • CLK Тактовый сигнал интерфейса, пассивное состояние высокий уровень, по спаду сдвиг, по фронту защёлкивание;
  • CMD Сигнал данных от консоли к периферии;
  • DAT Сигнал данных от периферии к консоли;
  • ACK Аппаратный хэндшейк, активный уровень низкий.

Так же на интерфейсе присутствует два разных напряжения питания: 3.3в и 7.6в. Все сигналы, кроме SEL0 и SEL1 являются общими для всех подключаемых устройств. Именно поэтому нерабочая карта памяти или джойпад во втором слоту влияли на рабочие в первом, хотя после 16ти битных приставок это казалось странным. Я думаю, что многие уже узнали в интерфейсе стандартный SPI всё верно, так и есть. Только добавлен сигнал ACK для подтверждения операции ввода/вывода. Вот назначения сигналов на контактах карты памяти:


Отремонтированная карта памяти с 5ти вольтовым FLASH

Технические характеристики интерфейса такие:
        ___   ___________________________   ____Данные     \ /                           \ /     или        X                             XКоманда ___/ \___________________________/ \____        ___                  ____________                  \                /            \      Такты       \              /              \                  \____________/                \____            |                             |            |           tck               |            |<--------------------------->|+-------+-------+------+-------+|       | мин.  | тип. | макс. |+-------+-------+------+-------+| tck   | 1мкс  | 4мкс |   -   |+-------+-------+------+-------+Тайминг ACK:     ____                                               SEL-     |______________________________________________     ______        __________        ___________        CLK        ||||||||          ||||||||           ||||||||                  |                 |ACK- -----------------------|_|-------------|_|---------                  |   ta1   | |     |  ta2  |                  |<------->| |     |<----->|                            | |  ap                           >|-|<-----+-----+------+-------+--------+|     | мин. |  тип. |  макс. |+-----+------+-------+--------+| ta1 | 0мкс |   -   | 100мкс | Первый байт-подтверждение+-----+------+-------+--------+| ta2 |      | 10мкс |   1мс  | Остальные+-----+------+-------+--------+|  ap | 2мкс |       |        | Длительность ACK+-----+------+-------+--------+

Измеренная частота сигнала CLK является 250кГц, что составляет 4 мкс на период. С физическими параметрами интерфейса разобрались, теперь транспортный уровень. Опытный инженер уже заметил, что джойпад и карта памяти подключены полностью параллельно и могут конфликтовать между собой. Так и есть, для этого присутствует программный арбитраж. После активации сигнала SELn периферия продолжает молчать, но слушает первый присланный байт. Если этот байт равен 0x01, то далее активируется джойпад, а карта памяти продолжает молчать до деактивации сигнала выбора. А если байт был 0x81, то всё наоборот: карта памяти активируется, а джойпад молчит. Естественно, что хост ждёт сигнала ACK на этот байт арбитража и ждёт недолго. Это нужно для того, чтобы успеть опросить остальную периферию, если часть этой периферии отсутствует. Дело в том, что операционная система опрашивает контроллеры и карты памяти строго по сигналу обратного хода луча, или более известного как VBlank. Так принято, что игры в приставках до 5-го поколения завязаны на этот тайминг, который равен частоте кадров. А частота кадров строго стабильна и нормирована: 50Гц для PAL и 60Гц для NTSC. Т.е., период опроса джойстиков и карт памяти равен 20мс для PAL или 16мс для NTSC.

Итак, с арбитражем разобрались, теперь собственно верхний уровень. Какие команды понимает стандартная карта памяти PS1? Да, собственно, их всего 3.
  • R 0x52 или Read. Чтение сектора карты памяти;
  • W 0x57 или Write. Запись сектора карты памяти;
  • S 0x53 или Status. Чтение статуса карты памяти.

Вся карта памяти разбита на сектора. Один сектор 128 байт. Таким образом, в 128КиБ помещается 0x400 или 1024 сектора. При этом стирать сектор перед записью не нужно. Но система гарантированно даёт время на целый следующий кадр при записи. Т.е., читать карту памяти она может каждый кадр, а записывает через один. К слову, всякие Взломщики кодов для ускорения не придерживаются данных таймингов. Разберём каждую команду более детально.

Протокол работы с картой памяти.


Порядок передаваемых данных в каждой команде выглядит вот так:
Чтение:
CMD 0x81 0x52 0x00 0x00 MSB LSB 0x00 0x00 0x00 0x00 0x00 ... 0x00 0x00 0x00DAT ---- FLAG 0x5A 0x5D PRV PRV 0x5C 0x4D  MSB  LSB DATA ... DATA  CHK  ACK

Запись:
CMD 0x81 0x57 0x00 0x00 MSB LSB DATA ... DATA CHK 0x00 0x00 0x00 DAT ---- FLAG 0s5A 0x5D PRV PRV  PRV ...  PRV PRV 0x5C 0x5D  ACK

Статус:
CMD 0x81 0x53 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00DAT ---- FLAG 0x5A 0x5D 0x5C 0x5D 0x04 0x00 0x00 0x80

Легенда:
CMD Данные, которые хост посылает карте.
DAT Данные, которые карта посылает хосту.
FLAG Текущие флаги состояния карты и результат предыдущей команды.
PRV Предыдущие принятые данные, результат упрощения схемы в карте.
MSB Старший байт номера сектора.
LSB Младший байт номера сектора.
DATA Полезные данные.
CHK Контрольная сумма блока.
ACK Флаг подтверждения.

Байт флагов FLAG использует следующие биты:
  • D5 Устанавливается некоторыми картами памяти не от Sony. Назначение неизвестно.
  • D3 Устанавливается при подаче питания и сбрасывается при любой записи. Используется для обнаружения смены карты памяти.
  • D2 Устанавливается при ошибках записи, актуален на следующее обращение после самой операции.

После подачи питания FLAG равен 0x08. И после первой же записи он обнуляется. Операционная система PS1 всегда делает запись в сектор 0x003F для этого, тем самым вызывая износ этого сектора. Но в рамках разметки карты памяти системой какой-либо полезной информации в этом секторе нет. Номер сектора MSB:LSB 10 бит и составляет число от 0x0000 до 0x03FF. Контрольная сумма CHK это обычный XOR всех 128 байт данных + MSB и LSB. Подтверждение ACK может принимать всего 3 значения: G 0x47, E 0x43 и 0xFF. G = Good или ОК. E = Error. Собственно, при чтении из карты ACK всегда равен G, а при записи G = ОК, E = ошибка контрольной суммы а 0xFF означает неправильный номер сектора. Правда, большинство карт просто откидывают неиспользуемые биты в старшем байте номера сектора и поэтому никогда не отвечают 0xFF. Числа 0x0400 и 0x0080 в команде статуса наводят на определённые мысли, что это количество секторов и размер сектора в байтах, но доподлинно это не известно. Ну вот мы и мы подошли к главному:

Реализация своей карты памяти.


Итак, эта вся информация, которая необходима для создания своей карты памяти для PS1. Потенциальные узкие места следующие:
  1. При чтении необходимо время на актуализацию данных. Между номером сектора и фактической передачей данных у нас есть 4 байта, у которых мы можем немного растянуть ACK. К слову, у оригинальной карты памяти на NOR FLASH все ACK идут равномерно, у карт памяти с SPI FLASH после передачи LSB происходит задержка ACK, во время которой контроллер выставляет команду в SPI FLASH и вычитывает первый байт, а остальные он вычитывает по ходу обмена.
  2. При записи после передачи всего пакета и начала самой записи в массив требуется время, но тут система сама даёт необходимую задержку.

Что касается питания, то у джойпадов 3,3в используется для логики а 7,6в для питания моторчиков. У карт памяти обычно используется только одно питание. Если внутри стоит 5в FLASH, то используется 7,6в и стабилизатор. Если стоит 3,3в FLASH, то используется сразу 3,3в.

Первый вариант я собрал на STM32F407VG, который питается от 3,3в, имеет SPI для PSIO, быстрый SDIO и достаточно памяти, чтобы хранить весь образ внутри себя, решая вышеупомянутые проблемы. Фотография готового устройства:


Первая версия моей карты памяти на STM32F407

Получилось быстро, надежно, но дорого. А можно сделать дешевле? Ну, что-ж, вызов принят. Учитывая специфику задачи, я выбрал STM32F042F6. Вот что получилось:


Вторая версия моей карты памяти на STM32F042

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

Программная реализация.


Первое, что надо сделать это работу с SD картой в режиме SPI. Я не буду особо останавливаться на этом, это уже давно разжёвано и растаскано по интернету. Код инита, чтения и записи сектора приведён ниже.
Card_Init()
// Инициализация карты памятиTCardType Card_Init( void ){// Локальные переменныеTCardType Res;uint32_t Cnt,OCR;uint8_t Dat, Resp;// Отключаем картуCARD_OFF; Res = ctNone;// Настраиваем SPI на медленную скорость PCLK/128: 48/128 = 0,375МГцSPI1->CR1 &= ~SPI_CR1_SPE;SPI1->CR1 = SPI_CR1_MSTR | SPI_LOW_SPEED;SPI1->CR1 |= SPI_CR1_SPE;// Топчемся на местеHAL_Delay( 1 );// Посылаем инит 256 байтfor (Cnt = 0;Cnt < 256;Cnt++ ){// Послыаем словоCard_SPI( 0xFF );}// Начинаем инициализацию картыCARD_ON;// Ожидаем готовности картыdo{// Посылаем 0xFFDat = Card_SPI( 0xFF );} while ( Dat != 0xFF );// CMD0: GO_IDLE_STATECard_SendCMD( &CARD_CMD0[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, DISABLE, 128 );// Какой ответ получен?if ( Resp == 0x01 ){// Карта вошла в IDLE_STATE, посылаем CMD8: SEND_IF_CONDCard_SendCMD( &CARD_CMD8[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, ENABLE, 128 );// Если был дан адекватный респонсif ( Resp != 0x01 ){// Это ветка SDv1/MMCdo{// Посылаем ACMD41: APP_SEND_OP_CONDCard_SendCMD( &CARD_ACMD41[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, ENABLE, 128 );} while ( Resp == 0x01 );// Каков был ответ?if ( Resp == 0x00 ){// Обнаружена карта SD v1Res = ctSD1;}else{// Это ветка MMC, нам её некуда втыкатьRes = ctUnknown;}}else{// Это ветка SDv2if ( (OCR & 0x0001FF) == 0x0001AA ){// Это карта SDv2do{// Посылаем ACMD55: APP_CMDCard_SendCMD( &CARD_CMD55[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, DISABLE, 128 );// Если ответ правильныйif ( Resp == 0x01 ){// Посылаем ACMD41: APP_SEND_OP_CONDCard_SendCMD( &CARD_ACMD41[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, ENABLE, 128 );}} while ( Resp == 0x01 );// Каков был ответ?if ( Resp == 0x00 ){// Посылаем CMD58: READ_OCRCard_SendCMD( &CARD_CMD58[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, ENABLE, 128 );// Каков ответ?if ( Resp == 0x00 ){// Анализируем OCRif ( (OCR & 0x40000000) == 0x00000000 ){// Карта обычной ёмкостиRes = ctSD2;}else{// Карта повышенной ёмкостиRes = ctSD3;}}else{// Эта карта неисправнаRes = ctUnknown;}}else{// Эта карта неисправнаRes = ctUnknown;}}else{// Эта карта неисправнаRes = ctUnknown;}}}else{// Карта ответила неправильноif ( Res != 0xFF ) { Res = ctUnknown; }}// Только для карт обычной ёмкостиif ( (Res == ctSD1) || (Res == ctSD2) ){// Устанавливаем размер блока 512 байт// CMD16: SET_BLOCKLENCard_SendCMD( &CARD_CMD16[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, DISABLE, 128 );// Каков ответ?if ( Resp != 0x00 ){// Эта карта неисправнаRes = ctUnknown;}}// Выключаем картуwhile ( (SPI1->SR & SPI_SR_BSY) != 0x0000 ) { }CARD_OFF;// Если карта инициализированаif ( (Res != ctNone) && (Res != ctUnknown) ){// Настраиваем SPI на быструю скорость PCLK/2: 48/2 = 24МГцSPI1->CR1 &= ~SPI_CR1_SPE;SPI1->CR1 = SPI_CR1_MSTR;SPI1->CR1 |= SPI_CR1_SPE;}// Выходимreturn Res;}

Card_Read()
// Чтение сектора карты памяти без DMAFunctionalState Card_Read( TCardType CardType, uint8_t *Buf, uint32_t *Loaded, uint32_t Addr ){// Локальные переменныеFunctionalState Res;uint8_t Cmd[ 6 ];uint8_t Dat,Resp;uint32_t Cnt;// ИнитRes = DISABLE;// Посмотрим, у нас в буфере уже загружено?if ( *(Loaded) != Addr ){// Сохраняем новый номер сектора*(Loaded) = Addr;// Корректируем адрес для старых картif ( (CardType == ctSD1) || (CardType == ctSD2) ){// У старых карт адрес вместо LBAAddr *= 0x00000200;}// Работаемwhile ( 1 ){// Если тип карты неправильный - выходимif ( CardType == ctNone ) { break; }if ( CardType == ctUnknown ) { break; }// Готовим команду на чтение сектораCmd[ 0 ] = CARD_CMD17;Cmd[ 1 ] = Addr >> 24;Cmd[ 2 ] = Addr >> 16;Cmd[ 3 ] = Addr >> 8;Cmd[ 4 ] = Addr;Cmd[ 5 ] = 0xFF;// Включаем картуCARD_ON;// Ожидаем готовности картыdo{// Посылаем 0xFFDat = Card_SPI( 0xFF );} while ( Dat != 0xFF );// Посылаем команду чтенияCard_SendCMD( &Cmd[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( (uint32_t *)&Cmd[ 0 ], DISABLE, 128 );// Анализируем ответ на командуif ( Resp != 0x00 ) { break; }// Ожидаем токен данныхCnt = 2048;do{// Считываем данныеDat = Card_SPI( 0xFF );// СчитаемCnt--;} while ( (Dat == 0xFF) && (Cnt > 0) );// Таймаут?if ( Cnt == 0 ) { break; }// Ошибка в токене?if ( Dat != CARD_DATA_TOKEN ) { break; }// Начались данные, загружаемfor (Cnt = 0;Cnt < 512;Cnt++){// Считываем данные*(Buf) = Card_SPI( 0xFF ); Buf++;}// Дочитываем CRCCmd[ 0 ] = Card_SPI( 0xFF );Cmd[ 1 ] = Card_SPI( 0xFF );// Без ошибокRes = ENABLE;// Выходbreak;}}else{// Без ошибокRes = ENABLE;}// Выключаем картуwhile ( (SPI1->SR & SPI_SR_BSY) != 0x0000 ) { }CARD_OFF;// Если была ошибка, обнулим номерif ( Res == DISABLE ) { *(Loaded) = 0xFFFFFFFF; }// Выходreturn Res;}

Card_Write()
// Запись сектора карты памяти без DMAFunctionalState Card_Write( TCardType CardType, uint8_t *Buf, uint32_t *Loaded, uint32_t Addr ){// Локальные переменныеFunctionalState Res;uint8_t Cmd[ 6 ];uint8_t Dat,Resp;uint32_t Cnt;// ИнитRes = DISABLE;// Корректируем адрес для старых картif ( (CardType == ctSD1) || (CardType == ctSD2) ){// У старых карт адрес вместо LBAAddr *= 0x00000200;}// Работаемwhile ( 1 ){// Если тип карты неправильный - выходимif ( CardType == ctNone ) { break; }if ( CardType == ctUnknown ) { break; }// Готовим команду на чтение сектораCmd[ 0 ] = CARD_CMD24;Cmd[ 1 ] = Addr >> 24;Cmd[ 2 ] = Addr >> 16;Cmd[ 3 ] = Addr >> 8;Cmd[ 4 ] = Addr;Cmd[ 5 ] = 0xFF;// Включаем картуCARD_ON;// Ожидаем готовности картыdo{// Посылаем 0xFFDat = Card_SPI( 0xFF );} while ( Dat != 0xFF );// Посылаем команду чтенияCard_SendCMD( &Cmd[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( (uint32_t *)&Cmd[ 0 ], DISABLE, 128 );// Анализируем ответ на командуif ( Resp != 0x00 ) { break; }// Посылаем токен данныхCard_SPI( CARD_DATA_TOKEN );// Посылаем данные в цикле// Начались данные, загружаемfor (Cnt = 0;Cnt < 512;Cnt++){// Считываем данныеCard_SPI( *(Buf) ); Buf++;}// Досылаем CRCCard_SPI( 0xFF );Card_SPI( 0xFF );// Без ошибокRes = ENABLE;// Выходbreak;}// Выключаем картуwhile ( (SPI1->SR & SPI_SR_BSY) != 0x0000 ) { }CARD_OFF;// Успешно?if ( Res == ENABLE ){// Сохраняем новый номер сектора*(Loaded) = Addr;}else{// Обнуляем*(Loaded) = 0xFFFFFFFF;}// Выходreturn Res;}

Карта инициализируется на скорости 375кГц (PCLK/128), а работает на 24МГц (PCLK/2). При таких скоростях замеры показали, что SDv1 и SDHC отдают сектор в рамках 2,8мс на всю транзакцию полностью. Это следует запомнить, т.к. важно для операции чтения PSIO.

Теперь посмотрим на PSIO. Как было уже сказано выше, он у нас в любом случае программный. Отслеживать надо только два сигнала: SEL и CLK. Первый мы будем отслеживать по обоим фронтам и делать приготовления к обмену данными:
EXTI2_3_IRQHandler()
// Прерывание по перепаду SELvoid EXTI2_3_IRQHandler( void ){// Подтверждаем прерываниеEXTI->PR = 0x00000004;// Анализируем состояние SELif ( MEM_SEL ){// SEL = 1EXTI->IMR &= 0xFFFFFFFE;State.PSIO.Mode = mdSync;// Тушим зелёную лампочкуLED_GREEN_OFF;}else{// SEL = 0EXTI->IMR |= 0x00000001;State.PSIO.Bits = 7;// Тушим лампочкиLED_GREEN_OFF; LED_RED_OFF;}// ОбесточиваемMEM_DAT1; MEM_nACK;}

Сигнал CLK будем ловить только по фронту. Дело в том, что STM32F042 работает всего лишь на 48МГц и его производительность маловата для нашей задачи. И если делать прерывание по обоим фронтам, то во время пересылки байта он практически не вылезает из обработчика прерывания и всё работает прямо на грани возможности, иногда давая сбои. А если реагировать только на фронт, а ту работу, что должна быть сделана по спаду сделать в конце прерывания, то всё отлично успевает меньше, чем за 55% от периода CLK, ведь несколько проверок при этом можно выкинуть. Уверен, что если этот обработчик написать на ассемблере максимально оптимально, то он смог бы работать даже по обоим перепадам. Вот код обработчика:
EXTI0_1_IRQHandler()
// Прерывание по фронту CLKvoid EXTI0_1_IRQHandler( void ){// Подтверждаем прерываниеEXTI->PR = 0x00000001;// Локальные переменныеuint16_t AckTime;// ИнитAckTime = 0;// Считываем данныеState.PSIO.DataIn >>= 1;if ( MEM_CMD ){// Принята 1State.PSIO.DataIn |= 0x80;}else{// Принят 0State.PSIO.DataIn &= 0x7F;}// Считаем битыif ( State.PSIO.Bits > 0 ){// Ещё есть битыState.PSIO.Bits--;}else{// Кончились биты?if ( State.PSIO.Bits == 0 ){// Биты кончилисьState.PSIO.Bits = 7;// Значение по умолчаниюState.PSIO.DataOut = State.PSIO.DataIn;// Анализируем ответswitch ( State.PSIO.Mode ){// Режим синхронизацииcase mdSync : {// Принят первый байт командыif ( State.PSIO.DataIn == 0x81 ){// Команда активации картыState.PSIO.Mode = mdCmd;// Текущий ответState.PSIO.DataOut = State.MemCard.Status;// Посылаем ACKAckTime = AckNormal;}elseif ( State.PSIO.DataIn == 0x01 ){// Команда активации джойстика}// Выходbreak;}// Получаем командуcase mdCmd : {// Меняем режимState.PSIO.Mode = mdParam;// Сохраняем байт в команду и подготовим буферState.MemCard.Cmd = State.PSIO.DataIn;State.MemCard.Bytes = 0;// ОтвечаемState.PSIO.DataOut = 0x5A;// Посылаем ACKAckTime = AckNormal;// Выходbreak;}// Режим получения параметровcase mdParam : {// Почти каждый ответ требует ACKAckTime = AckNormal;// Принимаем параметрыswitch ( State.MemCard.Cmd ){// Команда чтения: Rcase 0x52 : {// Анализируем байтыswitch ( State.MemCard.Bytes ){// Просто все вариантыcase 0 : { State.PSIO.DataOut = 0x5D; break; }case 1 : { break; }case 2 : { State.MemCard.Sector = State.PSIO.DataIn * 0x0100; State.MemCard.Check = State.PSIO.DataIn; break; }case 3 : { State.MemCard.Sector += State.PSIO.DataIn; State.MemCard.Check ^= State.PSIO.DataIn; State.PSIO.DataOut = 0x5C;   State.SDCard.CardOp = coRead; AckTime = AckDelayed; break; }case 4 : { State.PSIO.DataOut = 0x5D; AckTime = AckDelayed; break; }case 5 : { State.PSIO.DataOut = State.MemCard.Sector >> 8; AckTime = AckDelayed; break; }case 6 : { State.PSIO.DataOut = State.MemCard.Sector; AckTime = AckDelayed;   State.PSIO.Mode = mdRdData; State.MemCard.Bytes = 0; break; }default : { State.PSIO.Mode = mdDone; AckTime = 0; break; }}// Сигнализируем чтениемLED_GREEN_ON;// Выходbreak;}// Команда записи: Wcase 0x57 : {// Анализируем байтыswitch ( State.MemCard.Bytes ){// Просто все вариантыcase 0 : { State.PSIO.DataOut = 0x5D; break; }case 1 : { break; }case 2 : { State.MemCard.Sector = State.PSIO.DataIn * 0x0100; State.MemCard.Check = State.PSIO.DataIn; break; }case 3 : { State.MemCard.Sector += State.PSIO.DataIn; State.MemCard.Check ^= State.PSIO.DataIn; // break; }   State.PSIO.Mode = mdWrData; State.MemCard.Bytes = 0; break; }default : { State.PSIO.Mode = mdDone; AckTime = 0; break; }}// Сигнализируем записьюLED_RED_ON;// Выходbreak;}// Команда параметров: Scase 0x53 : {// Выставляем байт согласно номеруswitch ( State.MemCard.Bytes ){// Просто все вариантыcase 0 : { State.PSIO.DataOut = 0x5D; break; }case 1 : { State.PSIO.DataOut = 0x5C; break; }case 2 : { State.PSIO.DataOut = 0x5D; break; }case 3 : { State.PSIO.DataOut = 0x04; break; }case 4 : { State.PSIO.DataOut = 0x00; break; }case 5 : { State.PSIO.DataOut = 0x00; break; }case 6 : { State.PSIO.DataOut = 0x80; break; }default : { State.PSIO.Mode = mdDone; AckTime = 0; break; }}// Выходbreak;}// По умолчаниюdefault : { State.PSIO.Mode = mdDone; break; }}// Считаем номерif ( State.PSIO.Mode == mdParam ) { State.MemCard.Bytes++; }// Выходbreak;}// Режим передачи данных для чтенияcase mdRdData : {// Почти каждый ответ требует ACKAckTime = AckNormal;// Счётчик байтif ( State.MemCard.Bytes < 128 ){// Это передача данныхState.PSIO.DataOut = State.MemCard.Data[ State.MemCard.Bytes ]; State.MemCard.Check ^= State.PSIO.DataOut;}else{// Это хвостик за пределами данныхswitch ( State.MemCard.Bytes ){// Передача контрольной суммыcase 128 : { State.PSIO.DataOut = State.MemCard.Check; break; }// Передача завершающего статусаcase 129 : { State.PSIO.DataOut = 0x47; break; }// Завершение работыdefault : { State.PSIO.Mode = mdDone; AckTime = 0; LED_GREEN_OFF; break; }}}// СчитаемState.MemCard.Bytes++;// Выходbreak;}// Режим приёма данных для записиcase mdWrData : {// Почти каждый ответ требует ACKAckTime = AckNormal;// Счётчик байтif ( State.MemCard.Bytes < 128 ){// Это приём данныхState.MemCard.Data[ State.MemCard.Bytes ] = State.PSIO.DataIn; State.MemCard.Check ^= State.PSIO.DataIn;}else{// Это хвостик за пределамы данныхswitch ( State.MemCard.Bytes ){// Это приём контрольной суммыcase 128 : {// Сравниваем контрольную сумму и выносим решениеif ( State.MemCard.Check == State.PSIO.DataIn ) { State.MemCard.Check = 0x47; } else { State.MemCard.Check = 0x4E; }// Начинаем подтверждать приёмState.PSIO.DataOut = 0x5C;// Выходимbreak;}// Это хвостик данныхcase 129 : { State.PSIO.DataOut = 0x5D; break; }// Это вывод результата командыcase 130 : {// Сначала проверим, что сектор задан верноif ( State.MemCard.Sector < 0x4000 ){// Сектор верен, отдаём результат проверкиState.PSIO.DataOut = State.MemCard.Check;// Какой результат проверки?if ( State.MemCard.Check == 0x47 ){// Заказываем запись сектора в карту памятиState.SDCard.CardOp = coWrite;// После успешной записи обнуляется флагState.MemCard.Status &= ~StateNew;}}else{// Сектор ошибочен, выдаём ошибку сектораState.PSIO.DataOut = 0xFF;}// Выходbreak;}// Завершение работыdefault : { State.PSIO.Mode = mdDone; AckTime = 0; break; }}}// СчитаемState.MemCard.Bytes++;// Выходbreak;}// Заглушка, тупим до конца пакетаcase mdDone : { break; }// По умолчанию - откатываемся в началоdefault : { State.PSIO.Mode = mdSync; break; }}}}// Выставляем свои данныеif ( State.PSIO.Mode != mdSync ){// Выставляем текущий бит выводного байтаif ( State.PSIO.DataOut & 0x01 ){// Выставляем 1MEM_DAT1;}else{// Выставляем 0MEM_DAT0;}// Сдвигаем данныеState.PSIO.DataOut >>= 1;}// Требуется ACK?if ( AckTime > 0 ){// Установим CNTTIM3->CNT = AckTime;// Устанавливаем флагState.PSIO.Ack = DISABLE;// Сбросим событияTIM3->SR = 0x0000;// Включаем таймерTIM3->CR1 |= TIM_CR1_CEN;}}

Таймер TIM3 буде отвечать за генерацию ACK. Это нужно для того, чтобы во время этой задержки ядро было свободно для работы с SD картой. Обработчик прерывания от таймера вот такой:
TIM3_IRQHandler()
// Прерывание таймера TIM3void TIM3_IRQHandler( void ){// Снимаем флагTIM3->SR = 0x0000;// Анализируем режимif ( State.PSIO.Ack == ENABLE ){// Выключаем сигнал ACKMEM_nACK;}else{// Включаем сигнал ACKMEM_ACK;// Перекидываем режимState.PSIO.Ack = ENABLE;// Новый таймаутTIM3->CNT = 0;// Включаем таймерTIM3->CR1 |= TIM_CR1_CEN;}}


Код достаточно комментирован и я думаю, что в особом разборе не нуждается. Отмечу лишь тот момент, что после получения второго байта номера сектора в команде чтения мы устанавливаем флаг для операции чтения с SD карты для кода, который крутится в вечном цикле функции main(). И сразу после этого 4 следующих ACK выдаются с удлинённым временем. В интерфейсе это выглядит вот так:


Скриншот из программы логического анализатора, выделяются 4 большие задержки в транзакции

В сумме набирается порядка 3,5мс и этого с запасом хватает, чтобы код в основном коде успел считать сектор. Более того, тот код может работать только когда нет прерывания, т.е. как раз в эти большие паузы. Во время записи флаг устанавливается в самом конце и из-за того, что система даёт карте памяти отработать запись, основной код работает без помех со стороны прерываний. А теперь глянем в код основного цикла.
main()
// Основной циклwhile ( 1 ){// Обрабатываем сигнал вытаскивания картыif ( CARD_nCD == 0 ){// Карта вставленаif ( State.SDCard.CardType == ctNone ){// Включаем зелёную лампочкуLED_GREEN_ON; LED_RED_OFF;// Карту только что поменяли, пытаемся обнаружитьState.SDCard.CardType = Card_Init();// Карта обнаружена?if ( State.SDCard.CardType != ctUnknown ){// Анализируем файловую систему картыif ( Card_FSInit( &State.SDCard, &CARD_IMAGE[ 0 ] ) == ENABLE ){// Файлоавая система опознана, разрешаем работуEXTI->IMR |= 0x00000004;// Выключаем лампочкиLED_GREEN_OFF; LED_RED_OFF;}else{// Файловая система не опознанаState.SDCard.CardType = ctUnknown;// Зажигаем обе лампочкиLED_GREEN_ON; LED_RED_ON;}}else{// Зажигаем обе лампочкиLED_GREEN_ON; LED_RED_ON;}}}else{// Карта отсутствуетif ( State.SDCard.CardType != ctNone ){// Только вытащили, отключаем PSIOEXTI->IMR &= 0xFFFFFFFA;// Обнуляем все переменныеState.PSIO.Mode = mdSync; State.PSIO.Bits = 0; State.PSIO.DataIn = 0x00; State.PSIO.DataOut = 0; State.PSIO.Ack = DISABLE;State.MemCard.Status = StateNew;State.SDCard.CardType = ctNone; State.SDCard.CardOp = coIdle; State.SDCard.LoadedLBA = 0xFFFFFFFF;}// Потушим обе лампочкиLED_GREEN_OFF; LED_RED_OFF;}// Если карта естьif ( (State.SDCard.CardType != ctNone) && (State.SDCard.CardType != ctUnknown) ){// Заказана запись?if ( State.SDCard.CardOp == coWrite ){// Вычисляем сектор чтения и смещение в блокеOfs = State.MemCard.Sector & 0x03FF;LBA = (Ofs >> 2) & 0x000000FF;Ofs = (Ofs << 7) & 0x00000180;// Считываем сектор в буферCard_Read( State.SDCard.CardType, &State.SDCard.CardBuf[ 0 ], &State.SDCard.LoadedLBA, State.SDCard.CardList[ LBA ] );// Подменяем наш секторfor (Cnt = 0;Cnt < 128;Cnt++){// Переносим данныеState.SDCard.CardBuf[ Ofs + Cnt ] = State.MemCard.Data[ Cnt ];}// Пишем сетор назадCard_Write( State.SDCard.CardType, &State.SDCard.CardBuf[ 0 ], &State.SDCard.LoadedLBA, State.SDCard.CardList[ LBA ] );// Потушем лампочкуLED_RED_OFF;// Снимаем флагState.SDCard.CardOp = coIdle;}// Заказано чтение?if ( State.SDCard.CardOp == coRead ){// Вычисляем сектор чтения и смещение в блокеOfs = State.MemCard.Sector & 0x03FF;LBA = (Ofs >> 2) & 0x000000FF;Ofs = (Ofs << 7) & 0x00000180;// Считываем сектор в буферCard_Read( State.SDCard.CardType, &State.SDCard.CardBuf[ 0 ], &State.SDCard.LoadedLBA, State.SDCard.CardList[ LBA ] );// Копируем нужный секторfor (Cnt = 0;Cnt < 128;Cnt++){// Переносим данныеState.MemCard.Data[ Cnt ] = State.SDCard.CardBuf[ Ofs + Cnt ];}// Снимаем флагState.SDCard.CardOp = coIdle;}}}

В вечном цикле постоянно анализируется сигнал вставления SD карты. Если её вытащить на ходу, то код отключит PSIO и PS1 потеряет карту. Если же карту вставить обратно (или просто подать питание со вставленной картой), то сначала будет попытка инициализировать карту функцией Card_Init(), которая вернёт тип обнаруженной карты. Это важно, потому что у SDv1 и остальных SDHC/SDXC адресация идёт различными методами. Сам код инициализации никаких секретов не несёт и подсмотрен в куче доступных в интернете примеров про FatFS и подобных проектов.
Следом за инициализацией карты вызывается хитрая функция Card_FSInit(). Это самая главная фишка данного проекта. Дело в том, что STM32F042 скромный по возможностям и потянуть полную поддержку FatFS на необходимой скорости не сможет. Поэтому, я придумал такой метод: файл образа у нас всегда 128КиБ, поэтому, необходимо знать только 256 секторов по 512 байт, в каждом из которых будет ровно 4 сектора нашей карты памяти PS1. Таким образом, мы делаем следующее:

  1. Анализируем сектор LBA=#0 на предмет MBR. Если это действительно MBR, то получаем новый сектор, где находится MBS.
  2. Получив адрес предполагаемого MBS (это может быть #0, если нет MBR или какое-то число, если MBR есть) мы начинаем его анализ на предмет принадлежности одной из FAT: FAT12, FAT16, FAT32 или vFAT.
  3. Если сектор прошёл проверку, то мы забираем из него информацию о структуре и в корневом каталоге ищем элемент с именем файла. В данном случае это MEMCRD00.BIN.
  4. Если такой файл находится, то проверяем его размер он должен быть строго фиксирован 0x20000 байт. Если всё так получаем номер первого кластера.
  5. Если мы дошли до этого пункта, то у нас уже есть вся необходимая информации для построения списка физических LBA секторов, где расположен наш образ. Пробегая по цепочке FAT и используя информацию о структуре из MBS, заполняем таблицу из 256 номеров LBA секторов.

В случае успеха запускается PSIO и PS1 увидит свою карту как обычную. Если на каком-либо этапе произошла ошибка, то работа прерывается, загораются оба светодиода и всё остаётся в таком состоянии до снятия питания или замены SD карты. Вот код этой процедуры:
Card_FSInit()
// Инициализация таблицы секторов по имени файла, поддерживается пока только FAT16FunctionalState Card_FSInit( TSDCard *SDCard, const uint8_t *FName ){// Локальные переменныеFunctionalState Res;uint8_t *Buf;uint8_t Pos;uint16_t ClustSize,Reserv,RootSize,FATSize,Cluster;uint32_t Cnt,LBA,SysOrg,FATOrg,RootOrg,DataOrg;int Compare;// ИнитRes = DISABLE; SysOrg = 0; Cluster = 0xFFFF;// Начинаем с самого сначалаwhile ( 1 ){// Вычитываем сектор 0if ( Card_Read( SDCard->CardType, &SDCard->CardBuf[ 0 ], &SDCard->LoadedLBA, SysOrg ) == DISABLE ) { break; }// Анализируем сектор #0 на MBRif ( *((uint16_t *)&SDCard->CardBuf[ 0x01FE ]) != 0xAA55 ) { break; }// Проверим косвенные признаки MBRif ( ((SDCard->CardBuf[ 0x01BE ] == 0x00) || (SDCard->CardBuf[ 0x01BE ] == 0x80)) && ((SDCard->CardBuf[ 0x01CE ] == 0x00) || (SDCard->CardBuf[ 0x01CE ] == 0x80)) && ((SDCard->CardBuf[ 0x01DE ] == 0x00) || (SDCard->CardBuf[ 0x01DE ] == 0x80)) && ((SDCard->CardBuf[ 0x01EE ] == 0x00) || (SDCard->CardBuf[ 0x01EE ] == 0x80)) ){// Похоже на MBR, анализируем таблицу разделовfor (Cnt = 0;Cnt < 4;Cnt++){// Анализируем признак разделаif ( (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C2 ] == 0x01) ||// Сигнатура 0x01: FAT12 (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C2 ] == 0x04) ||// Сигнатура 0x04: FAT16 (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C2 ] == 0x06) ||// Сигнатура 0x06: Big FAT16 (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C2 ] == 0x0E) )// Сигнатура 0x0E: vFAT{// Сигнатура подошла, забираем адрес MBS разделаSysOrg = SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C6 ];SysOrg += (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C7 ] * 0x00000100);SysOrg += (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C8 ] * 0x00010000);SysOrg += (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C9 ] * 0x01000000);// Выходимbreak;}}}// Загружаем сектор предполагаемого MBSif ( Card_Read( SDCard->CardType, &SDCard->CardBuf[ 0 ], &SDCard->LoadedLBA, SysOrg ) == DISABLE ) { break; }// Анализируем сектор на MBSif ( *((uint16_t *)&SDCard->CardBuf[ 0x01FE ]) != 0xAA55 ) { break; }if ( SDCard->CardBuf[ 0x000D ] == 0x00 ) { break; }if ( (SDCard->CardBuf[ 0x0010 ] == 0x00) || (SDCard->CardBuf[ 0x0010 ] > 0x02) ) { break; }if ( SDCard->CardBuf[ 0x0015 ] != 0xF8 ) { break; }if ( *((uint32_t *)&SDCard->CardBuf[ 0x001C ]) != SysOrg ) { break; }if ( SDCard->CardBuf[ 0x0026 ] != 0x29 ) { break; }if ( *((uint16_t *)&SDCard->CardBuf[ 0x0036 ]) != 0x4146 ) { break; }if ( *((uint16_t *)&SDCard->CardBuf[ 0x0038 ]) != 0x3154 ) { break; }if ( SDCard->CardBuf[ 0x003A ] != 0x36 ) { break; }// Заполняем локальные переменные, которые нужны для математикиClustSize = SDCard->CardBuf[ 0x000D ];Reserv = *((uint16_t *)&SDCard->CardBuf[ 0x000E ]);RootSize = (SDCard->CardBuf[ 0x0012 ] * 0x0100) + SDCard->CardBuf[ 0x0011 ];FATSize = *((uint16_t *)&SDCard->CardBuf[ 0x0016 ]);// Вычисляем координаты FAT и ROOTFATOrg = SysOrg + Reserv;RootOrg = FATOrg + (FATSize * 2);DataOrg = RootOrg + (RootSize / 16 );// Все данные получены, приступаем к поиску имени файла нужного имиджаfor (LBA = 0;LBA < (RootSize / 16);LBA++){// Загружаем сектор корневой папкиif ( Card_Read( SDCard->CardType, &SDCard->CardBuf[ 0 ], &SDCard->LoadedLBA, RootOrg + LBA ) == ENABLE ){// Перебираем 16 элементов, которые могут находиться в сектореfor (Cnt = 0;Cnt < 16;Cnt++){// Сравниваем имяCompare = memcmp( &SDCard->CardBuf[ Cnt * 32 ], &CARD_IMAGE[ 0 ], 11 );if (  Compare == 0 ){// Файл найден, проверим размерif ( *((uint32_t *)&SDCard->CardBuf[ (Cnt * 32) + 0x001C ]) == 0x00020000 ){// Размер подходит, копируем номер кластераCluster = *((uint16_t *)&SDCard->CardBuf[ (Cnt * 32) + 0x001A ]);// Без ошибокRes = ENABLE;// Выходимbreak;}}}// Если файл найден - выходим экстренноif ( Res == ENABLE ) { break; }}else{// ошибка загрузки - вываливаемсяbreak;}}// Файл найден, данные получены, начинаем построение таблицы доступаif ( Res == ENABLE ){// У нас есть номер кластера, готовимся заполнять табличкуPos = 0;do{// Проверяем номер кластераif ( Cluster < 0x0002 ){// Ошибка, выходимRes = DISABLE; break;}// Вычисляем LBA данных кластераLBA = DataOrg + ((Cluster - 2) * ClustSize);// В цикле по размерку кластера заполняем элементы таблицыfor (Cnt = 0;Cnt < ClustSize;Cnt++){// Вычисляем LBA сектроа внутри кластераSDCard->CardList[ Pos ] = LBA + Cnt;// Следующий элементPos++; if ( Pos == 0 ) { break; }}// Если есть ещё элементы, надо получить новый номер кластера// А для этого надо вычислить номер сектора, где этот кластер находится и загрузить его по цепочкеif ( Pos != 0 ){// Вычисляем сектор нахождения кластераLBA = FATOrg; Reserv = Cluster;while ( Reserv > 256 ) { LBA++; Reserv -= 256; }// Загружаем этот сектор в памятьif ( Card_Read( SDCard->CardType, &SDCard->CardBuf[ 0 ], &SDCard->LoadedLBA, LBA ) == ENABLE ){// Забираем новый номер кластераCluster = *((uint16_t *)&SDCard->CardBuf[ Reserv * 2 ]);}else{// Ошибка загрузкиRes = DISABLE; break;}}} while ( (Cluster != 0xFFFF) && (Pos != 0) );}// Выходbreak;}// Выходreturn Res;}

Скажу честно, так как это всего лишь PoC, то здесь реализован поиск только у FAT16. FAT12, наверное, и не надо поддерживать microSD таких малых объёмов не бывает. А вот FAT32 или vFAT добавить возможно, если это кому-нибудь понадобится в будущем.

Имя образа MEMCRD00.BIN выбрано не случайно. Дело в том, что в будущем я планирую добавить выбор образа через стандартную для многостраничных карт памяти комбинацию кнопок на джойпаде: при зажатом SELECT следует однократное нажатие на L1/R1. И меняя последние 2 символа можно поддержать 100 образов в корневой директории, от MEMCRD00.BIN до MEMCRD99.BIN. Для этого есть задел в обработчике прерывания по SCK в интерфейса PSIO, ветка где анализируется обращение к джойпаду. Сделать сниффер проблем нет, но периферия контроллеров у PS1 богатая и придётся практически всех их поддерживать.

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

P.S. Я бы очень хотел указать здесь список всех источников информации, которые я использовал в создании этого проекта, но увы это очень затруднительно. Многое было подслушано случайно. Кое-что ходило в виде TXT файлов с общей информацией про PS1 более 15 лет назад, для тех, кто хотел написать свой эмулятор. И теперь всё это существует в виде нескольких текстовых файлов на моём жёстком диске. Можно сказать, что источником информации служил весь интернет на протяжении последних 15 лет.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru