Рассказываю опыт разработки сканера 35-мм киноплёнки со звуковой дорожкой за 150$ в картинках наглядно.
Привет Хабр! По реакциям на предыдущую статью стало понятно что народ требует продолжения. Ну, раз требуете то будет =) По личным ощущениям в предыдущей части было маловато текста и я попробую это исправить. В этой части в качестве эксперимента увеличу количество текста и оптимизирую картинки расположив их парами где это возможно. После прочтения статьи напишите в комментариях, что думаете о таком изменении. Как и в предыдущей статье, в этой также будет много практики, но она будет несколько разбавлена теорией(листинги)
Краткий обзор статьи: я покажу свой первый epic fail, объясню причину такого происшествия(торопиться нужно только при ловле блох) и после необходимых исправлений продолжаю процесс разработки. Вторая половина статьи будет посвящена переходу на микроконтроллер семейства STM32 и первые попытки в UI. В качестве завершения автоматизирую работу с камерой.
В предыдущей части: для ЛЛ — приведу изложение синопсисом без учёта абзацев с историей формата 35мм(прочитаете в предыдущей части). Был получен подгон кинокопий ералаша в удовлетворительном состоянии, я первые пару дней смотрел на них, пошебуршил интернет, покумекал над результатами и принял решение сделать свой сканер с блекдежком и... с автоматикой и низким бюджетом(несколько ящиков балтики). Далее по ходу предыдущей части я начал работать над несколькими ключевыми компонентами: над окошком для просветки кадра(слева на КДПВ), приводом и подставкой для бобины, заимел локальный успех с этой авантюры и доработав привод завершил предыдущую статью.
Навигация по статье:
Часть вторая. Развитие идеи и переход на МК
Последствия ошибок конструирования
Мои небезопасные подходы к проектированию, которые я показывал в предыдущей части не прошли бесследно. Да, я делал работу над ошибками — я изменил тонкий штырь 4х4 мм на звёздочку в узле стыковки валика и шестерни, я изменил конфигурацию валиков сделав все 3 валика приводящими, но я не все ещё не принимал во внимание главную проблему — ломкость слоёв пластика. И в первую очередь это отразилось на состояние подопытной бобины с плёнкой:
Это моё первое и последнее серьёзное ЧП. Я взял паузу и пару дней не занимался проектом - анализировал произошедшую ситуацию и причины, по которым она возникла и сделал своё заключение. Первая серьёзная ошибка была допущена в расположении валиков с учётом первоначального замысла сделать приводящим только центральный, а позже сделать их всех приводящими. Так как я сначала думал что мне хватит одного валика, я не стал рассчитывать положение соседних, располагая их из соображений как будет красивее а не как правильнее — это привело к тому что зубчики крайних валиков, как бы я не пытался сориентировать их относительно плёнки — не вставали точно в дырки перфорации(см правое фото выше), а где-то рядом из-за чего зубчики выдавливали плёнку в области перемычек между отверстиями, что приводило к трещинам или собственно отрыву перемычек между отверстиями с последующим нарастающим повреждением в области:

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

На примере фото с плёнкой и фото кадра титров — произошёл второй вариант. Валик начало клинить и он начал рвать всё подряд пока я не услышал странные звуки и не остановил привод. Но! Ещё нужно рассказать о третьем варианте, который является жирной предпосылкой ко второму — я ещё был зелёным и не знал что в пр��цессе трения деталей механизма, полностью состоящего из АБС происходит сильный нагрев с последующим размягчением пластика и детали могут слипнуться и заклинить. У меня и такое было, позже начал в места трения добавлять по капле машинного масла(отработанного) и это решало проблему.
Работа над ошибками
Исходя из этого прослеживается закономерный вопрос: А почему ты просто не изменишь чертёж и не исправишь? Вообще я и собирался так сделать, но так как бюджет диктовал мне условия, то я пошёл другим путём — придумал и применил костыль заключающийся в изменении количества зубчиков на шестерёнках, к которым прикрепляется звёздочка. По итогу количество зубчиков увеличилось с 36 до 72, благодаря чему я мог позволить выставлять более точную ориентацию валиков относительно центрального. Напоследок я решил «импортозаместить» чужую модельку заменив её своим аналоГовнетом, но с двумя изменениями — я убрал сугубо декоративное утончение к центру(надо сказать, старый вариант я по прежнему продолжал использовать но в менее критичных узлах) избавив валик от уязвимости на разлом по середине от чего его форма стала больше похожей на скалку и переделал форму зубчиков снизив их высоту от поверхности валика и изменил длину/ширину зубчика так чтобы он заполнял ~70% площади отверстия:

Тут у вас может возникнуть вопрос насчёт способов стыковки валика с другими деталями — со одной стороны звёздочка а с другой паз для штыря, казалось бы очевидный вопрос — почему ты не доделал? Тут не всё так просто, как можно было видеть на “рентгене” привода в предыдущей части, вращение валику передаётся только с одной стороны(справа если смотреть рисунок выше), а с другой валик крепится к стенке через шарнирную опору. Отсюда можно заключить что нагрузка со стороны опоры либо минимальная, либо вообще отсутствует и следовательно, необходимости заменять устаревший способ крепления с одной из сторон не возникает. Теперь можно отправить на печать новые детали и посмотреть что получилось:

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

Апгрейд модуля с кадровым окном
Теперь, когда опасность успешно закупорена миновала, можно возобновить свои изыски. Как я говорил в прошлой части — кадровое окно хоть и работает как задумано, но оно не даёт возможности что-то к нему пристыковать и закрепить тем самым является бесперспективным элементом, к тому же мне не даёт покоя мысль что я использую чужую наработку. Если на второе ещё можно забить, то первый факт этого не позволит, поэтому встала острая необходимость провести обновку. Покажу однослайдовую псевдо-3D презентацию, которую подготовили тараканы в моей голове:

Центральные два блока являются по сути своей одной моделькой с углублением в длина_блока[X]*~35.5[Y]*~0.25мм[Z] под плёнку в центре но напечатанной два раза на 3D-принтере. Внутренняя высота получившегося фильмового канала составляет всего ~0.5мм, а ширина — ~35.5мм. Теперь посмотрим на то что напечатал принтер, и какой будет вид на плёнку в окошке
Выглядит не так красиво по сравнению с предыдущим вариантом, но его главное преимущество в возможности модульной сборки и замены модуля не переделывая с нуля всю конструкцию. Также после этого мне в голову пришла идея заменить подсветку экраном дисплея на самодельную светодиодную, но пусть это пока будет в виде замысла, я ещё не готов — теперь посмотрим что там с электронной частью...
RGB подсветка
В предыдущей статье я остановился на том что приводом управлял блок на логике К155, но так как я в проект собираюсь заложить гораздо больше возможностей и сделать его портативным, то выбор из чего делать электронику нужно пересмотреть иначе она будет слишком громоздкой по итогу. Со всей задачей может справиться Arduino, но вот беда что у меня нет опыта работы с ним и собственно нет Arduino в наличии(а это и причина почему нет опыта), поэтому я пришёл к решению использовать серию STM32F1 из отладочного набора STM32VL Discovery который у меня был в наличии, благо хоть чуть-чуть но умею на нём говнокодить писать код, ну и работа с МК будет вестись в Keil(исторически так вышло, что Keil и STM32 были моей первой практикой в программировании контроллеров). И тут моя идея со светодиодной подсветкой получила развитие — если можно управлять подсветкой с контроллера, то хорошим решением будет использовать RGB подсветку, ведь так можно будет на лету компенсироват�� уход плёнки в красноту(да и в целом перекос баланса белого), но использование аналоговой RGB потребует наличия внешнего ЦАП(обоснуй для нытья — он есть в STM32(продолжаю нытье - там всего 2 канала а надо 3)) и ключей для него — нежелательное усложнение, поэтому идея с аналоговой RGB плавно но быстро перетекла в идею с адресной RGB, а что — она дешевая, в интернете наверняка есть библиотека для работы с ней, а если и нет, то по даташиту можно сделать свою. Звучит довольно просто, особенно в теории. Для ЛЛ ссылка на даташит.
Перед тем, как садиться за клавиатуру надо изготовить минимальное рабочее исполнение модуля подсветки чтобы можно было на ходу проверять как работает код. У меня будет использовано 32 светодиода на WS2812b с током до 60мА(а где-то говорят до 40), что в сумме даст потребление до ~1.9А при максимальных значениях. Собираю рамочку:
Рамка изготовлена из 4 модулей по 8 светодиодов(на озоне набор из 5 шт стоил в районе 360 рублей). На первых порах я не буду выкручивать яркость на максимум, чтобы питание можно было брать прямо из USB, ну или с выкручиванием до предела, но тогда питаться буду с внешнего блока питания — в общем, как пойдёт. Ещё хочу добавить преждевременную объясниловку насчёт конденсаторов — да, на фото выше их нет и я не сразу следовал рекомендации ставить их для фильтрации питания, но позже когда начну безуспешные попытки побороть мерцание, я их добавлю.
Первые полтора вечера я пытался реализовать собственную библиотеку по гайду с narodstream параллельно ища готовые либы, но затем отказался от плана в пользу уже существующего решения, о котором на хабре есть отдельная статья, советую прочитать — в статье очень хорошо разжёвана работа библиотеки.
В качестве предварительной настройки периферии контроллера и генерации кода(не одобряю автоген) буду использовать CubeMX — позже когда прошивка приобретёт законченный вид, можно будет отказаться от HAL(я до сих пор считаю решение остаться на HAL своей крупной ошибкой), что я скорее всего и сделаю т.к не люблю HAL. В качестве таймера, для формирования псевдо-SPI назначу TIM2. Теперь можно сгенерировать проект для Keil и начать говнокодить. И для начала в хидере ARGB.h надо настроить тип контроллера подсветки, пина с которого будет заливаться инфа и количество светодиодов в конфигурации:
#define WS2812 ///< Family: {WS2811S, WS2811F, WS2812, SK6812} // WS2811S — RGB, 400kHz; // WS2811F — RGB, 800kHz; // WS2812 — GRB, 800kHz; //У меня WS2812b(буква b не роляет здесь никак) // SK6812 — RGBW, 800kHz #define DMA_HANDLE hdma_tim2_ch2_ch4 //TIM2, 2й/4й канал #define NUM_PIXELS 32 ///< Pixel quantity //32 светодиода
Теперь можно зажечь белый цвет:
ARGB_FillRGB(0x02,0x02,0x02); //Запускаем на минимальной яркости чтобы не слепить глаза ARGB_Show();
Just for lulz можно сделать дискотеку используя простой софтовый генератор псевдорандома и после с его помощью в цикле задавать персональное значение для каждого светодиода.
У меня получился следующий набросок кода:
uint8_t pseudoRNG(void); inline uint8_t pseudoRNG(void) { static uint8_t key; //Делаем типа рандом, в голову лучшей идеи не пришло key += 103; //Прибавляем что-нибудь key ^= 89; //Для повышения уникальности return key; } uint8_t colorR, colorG, colorB;//Байты для каждой компоненты while(1) { for(volatile uint8_t idx = 0; idx < 32; ++idx) { //В цикле заполняем значение для каждого светодиода colorR = pseudoRNG(); colorG = pseudoRNG(); colorB = pseudoRNG(); ARGB_SetRGB(idx, colorR, colorG, colorB); } ARGB_Show(); //Теперь можно отправить инфу в модуль }
И этот код будет работать примерно так:
Делаем первый UI

Но каждый раз хардкодить настройки цвета под бобину неудобно и убивает ресурс флешки в контроллере — стоит задуматься над возможностью задания нужных значений в процессе работы, и это хорошая подводка для того чтобы начать думать над UI. Возвращаюсь в CubeMX и подключаю пины для клавиатуры — 4 пина будут опрашивать столбцы, ещё 4 будут ожидать бит из строк — пусть это будет PB0,PB1,PB2 и PC4 для сканирующих пинов столбцов и PA4,PA5,PA6,PA7 для пинов строк, на входе которых ожидается лог.1. Не забываем про подтяжку к земле чтобы не ловить глюки от клавиатуры. PC4 лежит особняком и на него можно будет посадить сервисные кнопочки. Настраиваем пины в кубе

Чего-то тут не хватает, не думаете? Ну конечно же! Допустим юзер будет нажимать кнопки и даже выучит что в каком порядке сначала записывается(ну или прочитает мурзилку к девайсу), но вдруг кнопка не нажалась а юзер этого не заметил? Например он хочет записать 190,240,210 но по каким-то причинам “4” не нажалась и в итоге будет записано 190,20,210 и в лучшем случае юзер заметит это сразу, матюкнётся и рестартнув машину прожмёт кнопки заново, а в худшем будет руина всего скана и тогда юзер уже скажет интересные слова про родственников создателя проекта. Интересные слова не очень хочется слышать, поэтому сыграю в дальновидного разраба и подключу простой экранчик, который позволит проверить всё перед запуском процесса. Пусть этим экранчиком на первые пора будет изученный вдоль и поперёк HD44780. Пинов у меня ещё много(LQFP64), поэтому и на экран ножек жалеть не буду и выделю ему полноценную 8 битную шину и 2 управляющих пина за исключением пина R/W — в дисплей будем только писать поэтому там всегда будет W. Код библиотеки для 44780 приводить здесь не буду потому что в интернете есть миллион вариантов этой библиотеки и вставляя сюда свои 5 копеек я ничего нового не покажу но для упрощения понимания что я делаю с дисплеем названия функций будут осмысленные(в принципе так и надо).
Листинг main.c
const char template[16] = “ R000 G000 B000 ”;//Шаблон для настройки цвета const char readyMsg[16] = “ READY “; //Сообщение о готовности к работе const char framesTemplate[16] = “Frames: “;//Шаблон для счётчика кадров const char eiMsgUpper[16] = “ User “;//Мессага если работа прервана юзером const char eiMsgBottom[16] = “ Interrupt “;//Вторая строка struct keyInfo{ bool pressed; uint8_t keyType; }keyInf; bool service = 0;//Сервисные функции uint16_t addDigit(uint16_t input, uint8_t num) { return input*10 + num; //Вставляем цифру в начало } charCodeStr[4];//Для отображения уровня цвета uint8_t RGB[3] = {0x00,0x00,0x00};//Массив с данными цвета const uint8_t visualBrackets[4] = {0,5,10,15};//Позиции для вставки декоративного визуала const uint8_t RGBPos[3] = {2,7,12};//Позиции на дисплее для уровня цвета const bool userSymbol1[8][5]{ //Пользовательский символ {1,1,1,1,1}, {1,1,1,1,1}, {1,1,1,1,1}, {1,1,1,1,1}, {1,1,1,1,1} {1,1,1,1,1} {1,1,1,1,1} {1,1,1,1,1} }; const bool userSymbol2[8][5]{ //Пользовательский символ плёнки {1,0,0,0,1}, {1,1,1,1,1}, {1,0,0,0,1}, {1,0,0,0,1}, {1,0,0,0,1} {1,1,1,1,1} {1,0,0,0,1} {0,0,0,0,0} }; const bool userSymbol3[8][5]{ //Пользовательский символ часов {0,0,0,0,0}, {0,1,1,1,0}, {1,0,1,0,1}, {1,1,1,0,1}, {1,0,0,0,1} {0,1,1,1,0} {0,0,0,0,0} {0,0,0,0,0} }; const bool userSymbol4[8][5]{ //Пользовательский символ стрелочки {0,0,1,0,0}, {0,0,1,0,0}, {0,0,1,0,0}, {1,0,1,0,1}, {1,1,1,1,1} {0,1,1,1,0} {0,0,1,0,0} {0,0,0,0,0} }; uint8_t colorType = 0; //0 – R, 1 – G, 2 – B; int main(void) { //Прописывать GPIO_init тут не нужно, это уже есть в коде, я опущу часть кода с автогеном куба //Чистим структуру uint8_t digitPos = 0; //Счётчик записанных разрядов keyInf.pressed = 0; //Кнопка не была нажата keyInf.keyType = 0; //На 0 кнопки не биндим _HD44780_SetUp(); //Настраиваем дисплей _HD44780_SetPos(0,0); //Первая позиция, первая строка _HD44780_SendStr(template); //Заливаем шаблон _HD44780_LoadUserSymbol(0x00, userSymbol1); //Кешируем в CGRAM пользовательский символ на знакоместо 0x00 //Кешируем символы и присваиваем им адреса 0x01...0x03 _HD44780_LoadUserSymbol(0x01, userSymbol2); _HD44780_LoadUserSymbol(0x02, userSymbol3); _HD44780_LoadUserSymbol(0x03, userSymbol4); //С подготовительной частью покончено, теперь надо запустить сканирование клавиатуры HAL_TIM_Base_Start_IT(&htim7); //Запускаем TIM7, который будет каждые 200 мс генерировать прерывание по которому контроллер пробежится по клавиатуре uint16_t colorComponentCode = 0; //В процессе обрежем до 8 бит //Начинаем заполнение полей while(1) { if(keyInf.pressed && keyInf.keyType > 0 && keyInf.keyType <= 10 && digitPos < 3) //Если юзер кнопку нажимал и это кнопка цифры, то зайдём внутрь условия иначе игнор { keyInf.keyType == 10 ? colorComponentCode = addDigit(colorComponentCode, 0) : colorComponentCode = addDigit(colorComponentCode, keyInf.keyType); //Кнопка нуля висит на ИД 10 ++digitPos; //Прибавляем количество записанных разрядов keyInf.pressed=0; //Скидываем флаг чтобы не было повторного срабатывания keyInf.keyType = 0; //Скидываем ИД кнопки } //Если юзер просто нажал кнопку решетки то переходим ниже else if(keyInf.pressed && keyInf.keyType == 0 && colorType < 3) //Проверяем что нажата кнопка "#" { switch(colorType) //Добавляем декорации { case 0: //[Rxxx]G000 B000 { _HD44780_SetPos(0,visualBrackets[0]); _HD44780_SendChar('['); _HD44780_SetPos(0,visualBrackets[1]); _HD44780_SendChar(']'); _HD44780_SetPos(0,visualBrackets[2]); _HD44780_SendChar(' '); _HD44780_SetPos(0,visualBrackets[3]); _HD44780_SendChar(' '); break; } case 1: // R123[Gxxx]B000 { _HD44780_SetPos(0,visualBrackets[0]); _HD44780_SendChar(' '); _HD44780_SetPos(0,visualBrackets[1]); _HD44780_SendChar('['); _HD44780_SetPos(0,visualBrackets[2]); _HD44780_SendChar(']'); _HD44780_SetPos(0,visualBrackets[3]); _HD44780_SendChar(' '); break; } case 2: // R123 G234[Bxxx] { _HD44780_SetPos(0,visualBrackets[0]); _HD44780_SendChar(' '); _HD44780_SetPos(0,visualBrackets[1]); _HD44780_SendChar(' '); _HD44780_SetPos(0,visualBrackets[2]); _HD44780_SendChar('['); _HD44780_SetPos(0,visualBrackets[3]); _HD44780_SendChar(']'); break; } } colorComponentCode > 255 ? RGB[colorType] = 255 : RGB[colorType] = colorComponentCode; //Если больше 255 то пишем 255 ARGB_FillRGB(RGB[0],RGB[1],RGB[2]); ARGB_Show(); //Обновляем отображаемый цвет sprintf(charCodeStr, "%03d", RGB[colorType]); //Ничего умнее в голову не пришло _HD44780_SetPos(RGBPos[colorType],0); //Перемещаем указатель на позицию для записи _HD44780_SendStr(charCodeStr); //Записываем циферку уровня цвета ++colorType; //Перемещаем указатель на элемент массива digitPos = 0; //Сброс количества записанных разрядов } else if(colorType == 3) { for(uint8_t _pos = 0; _pos < 5; ++_pos) { _HD44780_SetPos(0,visualBrackets[_pos]); //Перемещаем курсор на экране _HD44780_SendChar('/000'); //Прописываем пользовательский символ(тут просто заполняется всё знакоместо) } break; //Выходим из цикла } } }
Вроде код должен выглядеть рабочим(всё равно лучше того кринжа, что я писал первый раз). Перед main объявляется структура keyInfo, с которой будет работать функция сканирования клавиатуры и собственно цикл в main, следом буфер char, в который будет помещаться результат конвертации уровня цвета в человеко-читаемый формат, массив RGB[3] хранит в себе данные о текущих настройках RGB подсветки. Вспомогательная RGBPos нужна для выставления позиции курсора и обновления содержимого экрана по шаблону, который будет выведен на дисплей в процессе старта. colorType служит в роли указателя на элемент массива RGB[3]. visualBrackets содержит в себе позиции для добавления декоративного визуала на дисплей вокруг областей с инфой о цветах — квадратных скобочек, кастомных символов и т.д. В main инициализируются поля структуры keyInfo и счётчик разрядов, и на дисплей выводится шаблон template с надписями “ R000 G000 B000 ” и контроллер переходит в ожидание ввода. Но пока возможности ввода нет — и я это исправлю прямо сейчас.
Сканер клавиатуры
Теперь надо прописать функцию, которая будет сканировать клавиатуру. Здесь я добавлю юзеру возможность прощения ошибки мисклика(на самом деле я пока плохо понимаю как сделать нормальный код опроса клавиатуры, sorry) и поэтому запись цифры будет идти в два этапа — сначала надо нажать кнопку с нужной цифрой — она запишется в переменную keyType структуры keyInf и отобразится в правом нижнем углу экрана(я так и не решил будет ли это в качестве дебага или как постоянная фича), затем “#” — подымается флаг pressed. Таким образом, пользователь перед «подтверждением» может убедиться, что нажата нужная кнопка, но при этом сервисные кнопки будут срабатывать мгновенно. Сканер будет висеть на отдельном прерывании и дёргаться каждые 140 мс, насколько хорошим или плохим будет такой поступок я не знаю, но мне так проще держать в голове что откуда запускается. Перед тем как показывать ещё один всратый листинг нужно продемонстрировать табличку маппинга кнопок, чтобы потом не было непонятных ситуаций:

ИД 0 я избегаю, так как для себя решил что нули обозначают либо неопределенное состояние либо какая-то переменная не задана. PC4 как я раньше говорил является столбцом сервисных кнопок, которые я позже добавлю в проект(например — крутить двигателем ЛПМ ручками для точного расположения кадра в окне). Нажатие на кнопки с ИД 13-16 будет подымать флаг service, нажатие любой другой кнопки — сбрасывать. ИД кнопки “#” пока что обозначен условно, эта кнопка особая и не имеет своего ИД. Пустая кнопка с ИД 15 не должна вызывать беспокойства — я так и не придумал функционала для неё.
Сокращённый листинг функции сканера клавиатуры
//Функцию TIM7_IRQHandler расписывать не буду, всё равно она состоит только из двух строчек – одна //это вызов собсно функции ниже а вторая это хандлер на прерывание(автоген куба) //HAL ужасен. Код который я приведу здесь будет заменён когда я из прошивки уберу HAL //Сначала я хотел привести код под CMSIS но позже передумал. CMSIS позволил бы исполнить //всё более красиво – в два цикла(один вложен в другой) extern struct keyInfo keyInf; extern bool service; void scan_keyb(void) { //YandereDev moment HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_SET); if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_4) == GPIO_PIN_SET) { keyInf. keyType = 3; //Прописываем тип кнопки service = 0; //Сбрасываем флаг т.к кнопка не сервисная _HD44780_SetPos(1,15); //Устанавливаем позицию курсора дисплея _HD44780_SendChar('3'); //Печатаем символ ассоциированный с кнопкой HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET); //Опускаем ногу return; //Сваливаем из функции } if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == GPIO_PIN_SET) { keyInf. keyType = 6; //Аналогично service = 0; _HD44780_SetPos(1,15); _HD44780_SendChar('6'); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET); return; } if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_6) == GPIO_PIN_SET) { keyInf. keyType = 9; //Аналогично service = 0; _HD44780_SetPos(1,15); _HD44780_SendChar('9'); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET); return; } if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_7) == GPIO_PIN_SET) { keyInf. pressed = 1; //Здесь только подымаем флаг т.к кнопка ничего не записывает _HD44780_SetPos(1,15); _HD44780_SendChar('#'); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET); return; } HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); //4 ифа по примеру выше HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); //4 ифа аналогично HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); //… //Сокращу, т.к дальше нужно просто ctrl c ctrl v кода изменяя циферки в поле keyType //Код копипастится между парой HAL_GPIO_WritePin с параметрами GPIO_PIN_SET //и GPIO_PIN_RESET //… HAL_GPIO_WritePin(GPIOC, GPIO_PIN_4, GPIO_PIN_SET); //3 ифа... //... if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_7) == GPIO_PIN_SET) { //У сервисных кнопок меняется механика - они срабатывают мгновенно keyInf. keyType = 16; keyInf. pressed = 1; service = 1; HAL_GPIO_WritePin(GPIOC, GPIO_PIN_4, GPIO_PIN_RESET); return; } HAL_GPIO_WritePin(GPIOC, GPIO_PIN_4, GPIO_PIN_RESET); }
Собираем проект, запускаем и смотрим(я уже нажал на кнопку “#”):

Переезд шаговика с логики на МК
Пока буду считать, что всё работает нормально. Теперь надо разобраться с шаговиком. Как я говорил раньше(в 3й раз, хах) — шаговик приводился в движение схемой на логике К155, но так как у меня наметился массовый переезд периферии на контроллер то теперь управление шаговиком можно сделать более гибким. Управлять можно как софтверно, так и железно. Я предпочту управление железно. А как можно управлять железно с контроллера? Ну например с помощью ШИМ накручивать шаги, и двумя другими пинами управлять направлением и состоянием драйвера — включён/отключён. В STM32 практически каждый таймер может генерировать ШИМ, но таймеры в STM32 все разные — первые по номерам самые крутые и богатые по функционалу, последние — самые простые. Так как от таймера мне нужна всего лишь генерация ШИМ, то я конечно же выберу простой таймер. Возвращаюсь в куб и активирую таймер TIM15 задав скважность ШИМ импульсов в 50%:

С текущими настройками драйвера — шаг раздроблен на 16 микрошагов — для совершения полного оборота нужно 3200 шагов(с редуктором 1:2 в ЛПМ). На совершение полуоборота понадобится 1600 микрошагов(без редуктора в ЛПМ). Можно узнать период импульса, просуммировать периоды 1600 или 3200 раз и прикинуть — через сколько времени нужно остановить таймер но это ненадёжно — контроллер в нужный момент может быть занят прерыванием — и это много довольно таки сложного кода. И поэтому можно воспользоваться следующим фокусом, который позволят провернуть «крутые» таймеры — первый таймер как и раньше гоняет ШИМ, но второй будет тактироваться от ШИМ импульсов, идущих с первого таймера(просто кинуть перемычку с выхода на вход другого) и по достижении значения в 3200 или 1600(попугаев?) генерировать прерывание с высоким приоритетом, останавливающее двигатель. В качестве ведомого таймера в STM32F100 доступны TIM1-TIM3. Мой выбор остановился на TIM1. Настраиваем тактирование TIM1 извне — через PA12, выбираем тип события Update Event и в NVIC(менеджер прерываний) включаем прерывание, общее с TIM1 и TIM16 и задаём ему высший приоритет, чтобы оно всегда срабатывало вовремя:

Теперь надо запустить генерацию кода. Приложение довольно бережно обращается с кодом, поэтому беспокоиться о потере кода не стоит если конечно код написан в областях, обозначенными комментариями код-генератора, за другие области он не ручается. После обновления проекта Keil в нём появляется функция перехвата прерывания void TIM1_UP_TIM16_IRQHandler(void). Функция будет вызываться каждый раз, когда значение TIM1 достигнет 3200 попугаев и в теле этой функции уже можно остановить генерацию ШИМ и например вызывать функции управления камерой, подсчитывать количество отснятых кадров. Одним словом — не машина а подлодка. В NVIC задаю наивысший приоритет прерыванию, все остальные приоритеты по возможности снижаю. Ещё один листинг:
Листинг обработчика прерывания от TIM1
void TIM1_UP_TIM16_IRQHandler(void) { HAL_TIM_PWM_Stop(&htim15, TIM_CHANNEL_1);//Первым делом останавливаем генерацию ШИМ static uint16_t frameCnt;//То как с этой строчкой обойдётся компилятор оставим это компилятору, будем считать что он зануляет при инициализации char framChar[6];//Аналогично примеру из main - для конвертации в человеко-читаемый формат _HD44780_SetPos(1,13); _HD44780_SendChar('/002'); //Ставим значок часов(сканер занят снимком) //Здесь будет какая-то функция которая работает с камерой //shotFrame(); HAL_Delay(100);//ИБД со стороны контроллера ++frameCnt; //Приращиваем кадр sprintf(framChar, "%05d", frameCnt);//Конвертируем в человеко-читаемый формат _HD44780_SetPos(1,7);//Передвигаем курсор с оглядкой на шаблон _HD44780_SendStr(framChar);//Печатаем счётчик кадра _HD44780_SetPos(1,13); _HD44780_SendChar('/003'); //Ставим значок стрелки HAL_TIM_PWM_Start(&htim15, TIM_CHANNEL_1);//Запускаем генерацию ШИМ }
Но без доработки main.c таймер никогда не будет давать прерывания, так как никто никаких импульсов посылать не будет, исправим это добавив следующие строчки в main.c:
//Тут находится старый код //Запускаем таймер который будет генерировать прерывание полного оборота двигателя HAL_TIM_Base_Start_IT(&htim1); //Тут находится старый код //Начинаем опрашивать кнопку по кд while(1) { //Проверяем нажатие кнопки, пусть запускать процесс будет кнопка PA0(есть на плате) if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET) { _HD44780_SetPos(1,0);//Ставим курсор и выводим шаблон _HD44780_SendStr(framesTemplate); _HD44780_SetPos(1,12);//Печатаем статичный символ плёнки _HD44780_SendChar('/001'); //Запускаем таймер и выходим из цикла HAL_TIM_PWM_Start(&htim15, TIM_CHANNEL_1); break; } }
После таких изменений при нажатии кнопки надпись на второй строке заменяется счётчиком кадров который срабатывает каждый раз, когда таймер насчитывает 3200 попугаев импульсов
У HAL есть один значимый плюс — на нём можно быстро склепать код, который будет работать как надо, но его минус это например нельзя оптимизировать размер листинга ��ода запрятав некоторые его фрагменты в циклы + кода просто больше. Я считаю что HAL хорош когда надо быстро сделать набросок и посмотреть как он будет работать. На текущий момент говнокода достаточно, чтобы контроллер выполнял заложенные функции — настраивал подсветку и управлял двигателем пусть и приведенные листинги нельзя без оговорок собрать в полноценный проект. Пора сделать n шагов назад к началу истории. Подсветка уже собрана, светит, и теперь надо задуматься о том как её более-менее грамотно прикрепить к окну. Пока я рожал код, в голове снова возникла лайфхачная идея. Пора открыть форточку и подышать воздухом сменив вид деятельности.
Возвращаясь к теме кадрового окна...
Старое кадровое окно мне не даёт покоя, поэтому его надо заменить. Опишу новый замысел: хоть светодиоды и светят довольно ярко, но напрямую просвечивать ими плёнку нельзя так как в первую очередь это точечный источник освещения, и он не сможет равномерно просвечивать всю площадь кадра. Ему надо светить на что-то, что превращает свет в рассеянный и не просто рассеянный а ещё с равномерным распределением силы света. Почему бы не попробовать обычную бумагу в роли отражателя? А что, это дёшево, вылезать из кожи вон придумывая что-то мозговыносящее не нужно, бумага отражает свет вполне себе равномерно(но если светить на просвет то всё тлён). По итогу рождается модуль подсветки с отражателем:

Как выглядит модуль внутри с включённой подсветкой(+10 если выбрать цвет как на фото, +20 если выключить комнатный свет):
И с накинутым кадровым окном(чёрным закрашено чтобы не было отражений от стёкол макро-переходника на стенки и обратно):
Ничего гениального, просто мне снова удалось применить немного нестандартный подход. На фото может показаться что слева чуть пересвечено, но поверьте — если смотреть строго перпендикулярно к поверхности отражателя(бумаги) — светит очень равномерно. Разумеется свет отражается не только в окно, но и вообще во все стороны, я не стал показывать стрелочками на схеме отражения света во все стороны, оставив только главное. Помните оцифровку ещё через старое окно, которую я показал в первой части? Тогда я пользовался макро-переходником, заказанным с Ozon который себя не оправдал. Но я всё же провёл ещё одну попытку оцифровки с этим же переходником, и на этот раз оцифровал эпизод полностью:
Обновляем главный элемент
Скан вы��е в том виде, в каком я его сделал непригоден для комфротного просмотра. Во первыйх warped-звук из-за трудностей сканирования дорожки, вызванных дисторсией от линзы(даже если потом вторым проходом сканировать только дорожку поместив её по центру кадра то проблему это не особо исправит), во вторых изображение к краям расфокусируется и это не исправить одной лишь сенсухой с макро-насадкой за 300 грiвен рублей. Решение как всегда лежит на поверхности — можно использовать фотоаппарат. Так как кадров нужно делать много и делать часто, то приоритет поставлен в сторону беззеркальных решений но они раздувают бюджет по сравнению с зеркалками(почему так — я без понятия, по идее беззеркальное исполнение должно быть дешевле т.к меньше сложных деталей[вызываю пояснительную бригаду в комменты]). Я некоторое время бродил по тредам различных форумов ища информацию о недорогой модели с большим количеством мегапикселей — 15 и больше, довольно большой матрицей — хотя бы половину от 35мм и в беззеркальном исполнении, ну или на крайний случай — чтобы зеркало можно было поднять и просто снимать как мыльницей. Как вы уже поняли — такого решения за небольшой кэш не оказалось и я забил болт на свои требования найдя на авито незадорого(8000) народную зеркалку Nikon D3200, которая удовлетворила практически все мои требования кроме зеркала. Знакомьтесь, в студии будущий трудяга, который за короткий промежуток времени сделает более 150000 снимков:
Я довольно быстро(за 30-40 минут стояния с фотиком над плёнкой протянутой через кадровое окно) понял что с наскока сделать снимок кадра плёнки на весь размер фотографии не получится и нужно либо менять стоковый объектив, либо придумывать макро-переходник самому. Цены на телеобъективы быстро отбили желание проводить розыскные мероприятия в этом направлении и поэтому я продолжу свой суровый колхозинг. Моему воображению подаётся новая задача — сделать так чтобы либо картинка перед объективом была больше либо чтобы камера могла лучше фокусироваться(я думаю что по сути это одно и то же), и так как практика моя сильная сторона, то конструировать переходник буду также экспериментируя на ходу. Так родилась следующая задумка переходника в моей голове:

Я не стал заниматься расчётами так как уже просто забыл оптику, и даже если я проведу расчёт то нужная линза будет либо недоступна либо окажется слишком дорогой и я решил пользоваться тем что попадёт под руку, ну или просто что смогу найти у себя. У себя я смог найти лупу Урал 1.5х, ей и воспользуюсь. Но я не хочу терять возможность пользоваться увеличилкой в быту, поэтому для неё надо придумать решение, которое позволит без труда вытаскивать лупу — например платформу с выемкой, в которую её можно вложить. Оптимальное расстояние от поверхности плёнки до линзы увеличительного стекла я подобрал балансируя между размером увеличенного изображения и возможностями Kit-объектива, осталось замоделить коробочку, распечатать и вместе с ней распечатать платформу для лупы:
Теперь включим подсветку кадра, протащим плёнку до дисклеймера студии и оценим эффективность переходника на примере звуковой дорожки как самой чувствительной к чёткости области плёнки:
Как можно заметить, звуковая дорожка видна крайне хорошо. По краям пропал расфокус и остались только цветовые дисторсии, но это вообще не проблема — можно преобразовать фото в ч/б отбросив синий и красные компоненты, но впереди ещё предстоит работа. Даже если сократить продолжительность сканирования до 750 кадров(интрошка ералаша) то держа камеру руками я просто не смогу всё время помещать её в одно и то же положение с точностью хотя бы до сантиметра, поэтому надо придумать способ удержания камеры статично в течении долгого времени. Набросок я сделал сразу, и само решение я подготовил за полтора вечера:
За винт не переживайте, он ничего не замкнул. При закручивании болт натыкается на заглушку — её невозможно сломать не приложив диких усилий, чего я конечно делать не стану. Финальным штрихом тут будет крепление через какую-нибудь опору к специально сделанным для этого площадкам на корпусе переходника, но нельзя торопиться! Подумайте, что можно ещё добавить? Прям хорошо так подумайте? Ладно, не буду томить и подскажу — камера с высокой вероятностью, а скорее всего не с высокой а именно так и будет — будет закреплена с перекосом по одной или двум сторонам. Если я сейчас задумаю и исполню жесткое крепление, то не смогу потом компенсировать перекос быстрым путём и это добавит головной боли на этапе обработки полученных фотографий кадров(несложно конечно, но лучше чтобы поменьше проблем было). Поэтому добавлю ещё одну фичу:

Это решение простое, но эффективное по своему действию. Идеально ровно закрепить камеру я не могу, для этого нужно менять технологию изготовления деталей, но компенсировать кривизну мне под силу. Также с этим решением сокращается время, необходимое чтобы выровнять камеру относительно окна — достаточно будет подкрутить ту или иную гайку. Можно добавить ещё одно красивое решение — растянуть ушки чтобы платформа могла скользить не только по высоте но и вдоль, однако эту идею я так и не привёл в исполнение(лень было). Теперь, когда все необходимые элементы продуманы, представляется эксклюзивно для подписчиков возможность полностью собрать первый работающий образец кадрового окна:
Пройдусь по чек-листу:
Сделать подставку для бобины — Готово
Сделать привод ЛПМ — Готово
Сделать новое кадровое окно — Готово
Сделать компактную подсветку — Готово
Сделать макропереходник — Готово
Сделать крепление камеры — Готово
Перевести управление на МК — Готово
Сделать возможность сматывания плёнки после скана — Гот.. OH SHI!~
#@^%…
Ну так то, если не учитывать что после работы с проектором от аккуратно сложенной бобины останется что-то такое...
Решаем проблему с кучей плёнки около стола
...можно сказать что по основным компонентам работа завершена, и теперь осталось только причесать и подправить. Формально, проектор и правда готов к работе, но тратить 10 минут сматывая плёнку обратно как-то не комильфо, поэтому надо идти дальше. В предыдущей части, делая узел с посадочным местом под шпиндели бобины я добавил узлам ушки под винтики для крепления блоков к узлу. Так вот, теперь эти ушки мне пригодятся. Сейчас первый этап задачи сводится к повторной печати всех компонентов подставки для бобины за исключением одной из “муфт” — я уберу крестовину(в первой части я упоминал про крестовину) так как она сама по себе хрупкая и заменю редуктором 1:10(потом придётся ещё больше увеличивать коэффициент редуктора), и поставлю хорошо зарекомендовавший себя шаговый тип двигателя(не потому что именно он рекомендуется, а потому что я уже знаю как с ним работать). Но одного этого мало. Как известно с каждым витком увеличивается и длина витка а это значит, что этот факт тоже нужно учитывать, иначе я рискую либо порвать плёнку либо сломать привод принимающей бобины либо всё вместе(дайте два). Передо мной опять предстала вилка из двух выборов — пойти сложным путём придумывая какие-то матановские алгоритмы, либо пойти простым путём и придумать очередной хитрый датчик. Что я выберу угадать несложно. Мысль с датчиком у меня выглядела следующим образом. В пути не было возможности сесть за комп, поэтому рисовал от руки:
Прокомментирую рисунок «курица лапой». Слева сверху показан общий вид спереди. Датчик натяжения работает по типу гильотины, но вместо “лезвия” — параллелепипед скруглённый с одной стороны с одним слоем скотча для лучшего скольжения плёнки через него, по краям к нему добавлены лапки для крепления пружин, тянущих всю конструкцию вниз. До конца “гильотина” не падает а попадает на подпорки чтобы не прижимать плёнку и не создавать риска стереть или повредить эмульсию. По центру сверху тот же общий вид, но только сбоку. Справа пример прохождения плёнки — плёнка от валика, который заведомо расположен выше, проходит через датчик, расположенный прямо под принимающей бобиной и после сразу на бобину. Тем самым когда все излишки намотаны на бобину, создаётся натяжение, которое начинает подымать “гильотину”, которая в свою очередь подымает шторку, шторка в свою очередь утолщаясь снижает силу светового потока на фоторезистор и замедляет работу двигателя до полной остановки и когда натяжение ослабляется — двигатель снова приходит в движение, тем самым такие качели поддерживают заданное натяжение. Натяжение можно регулировать подстроечным резистором в одном из плеч резистивного делителя(второе плечо - сам фоторезистор) Реализация в solidworks несколько отличается — но в лучшую сторону:

Забегая вперёд покажу пример работы этого узла:
Ну и в качестве приятного бонуса надо отдельно показать фотку “шторки” как самой красивой детали в моём проекте:
Фоторезистор подключался к контроллеру как одно из плеч резистивного делителя напряжения(выше описал), средняя точка которого уже будет подключена ко входу АЦП на борту контроллера. В STM32 есть встроенный АЦП, функций которого хватит сполна. Перед тем как снова начать бредокодить, надо подготовить механизм датчика натяжения — подключить резисторный делитель ко входу, который будет обозначен как АЦП, напечатать на принтере комплект подставки под бобину и прикрепить шаговик. По итогу конструкция, которая будет принимать "отработанную" плёнку практически не отличается от раздающей, за исключением что под ней будет помещён датчик натяжения и сбоку прикреплён двигатель вращающий колесо бобины через редуктор:

Теперь, когда всё готово, можно начинать работу с контроллером. Я повторяю свой подход, и назначаю для работы с АЦП также отдельное прерывание и отдельный таймер — TIM17 с периодом срабатывания прерывания в 10 мс. В NVIC назначается пониженный приоритет чтобы не мешать работе прерывания от таймера-счётчика импульсов. На двигатель назначаю таймер TIM3 с генерацией ШИМ по 1 каналу(PB4 нога). В коде появляется новая ф-ция void TIM1_TRG_COM_TIM17_IRQHandler(void) которую я сейчас и пропишу
Листинг с кодом задающим натяжение от показаний датчика
void ADC_Select_Channel(uint32_t ch); void set_speed(uint16_t speed); #define tableSize 17 //Пресет скоростей uint16_t speedTable[18][2] = { {285, 60000}, {310, 59000}, {330, 57500}, {360, 54000}, {390, 50000}, {440, 45000}, {480, 39000}, {550, 33000}, {590, 28000}, {650, 24000}, {700, 19500}, {750, 16000}, {790, 13000}, {820, 11000}, {865, 10500}, {900, 9000}, {950, 8500}, {1023,0}//Значение не используется } //Функция взята с сайта eax.me(сейчас вроде страница доступа с веб архива) void ADC_Select_Channel(uint32_t ch) { //Функция выбирает активный канал АЦП что позволяет из кода //обслуживать все возможные каналы АЦП ADC_ChannelConfTypeDef conf = { .Channel = ch, .Rank = 1, .SamplingTime = ADC_SAMPLETIME_28CYCLES_5, }; if (HAL_ADC_ConfigChannel(&hadc1, &conf) != HAL_OK) { Error_Handler(); } } //Ф-ция настройки скорости двигателя void set_speed(uint16_t speed) { htim3.Init.Prescaler = speed; if (HAL_TIM_Base_Init(&htim3) != HAL_OK) { //Если не получится задать скорость по какой-то причине - останавливаем работу HD44780_ClearLCD(); HD44780_SetPos(0,0); HD44780_SendString("TIM3 PWM ERR"); Error_Handler(); } } void TIM1_TRG_COM_TIM17_IRQHandler(void) { //Запускаем АЦП, ждём завершения преобразования и выключаем ADC_Select_Channel(ADC_CHANNEL_1); HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1,10); HAL_ADC_Stop(&hadc1); //Будем считать что компилятор зануляет при инициализации static bool engStop; //АЦП присылает 12 битные значения, обрезаем до 10 бит uint16_t level = (uint32_t)HAL_ADC_GetValue(&hadc1)/4; //Порог остановки двигателя if(level < 285) { engStop = 1; //Остановка двигателя HAL_TIM_PWM_Stop(&htim3, TIM_CHANNEL_1); } else if(level >= 285 && level <= 950) { if(engStop) { HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); engStop = 0; } //Проход по циклу пока на будет найдено попадание в диапазон for(uint8_t _idx = 0; _idx < tableSize; ++_idx) { if(level > speedTable[_idx][0] && level < speedTable[_idx+1][0]) { set_speed(speedTable[_idx][1]);//Задаём скорость двигателя break;' } } } else if(level > 950) { set_speed(8000); } }
И теперь работа датчика, но поближе — со стороны пружин
В качестве бонуса приведу альтернативную функцию электрической части датчика натяжения — если изменить таблицу значений с целью превышения максимально возможной скорости двигателя, при текущем уровне питания силовой части драйвера в 12 вольт, то им можно озвучить например Atari 2600. Послушайте сами:
Прототип сканера практически готов, если не учитывать разложенные на столе кишки:
Автоматизируем управление камерой
Всё готово к запуску, но запуск надо ещё раз отложить — кое-чего всё же не хватает. А именно — кто будет нажимать на кнопку затвора? Любезно предоставлю это контроллеру, тем более у камеры есть разъем для подключения внешнего управления затвором — слева с краю на фото:
Управление затвором предельно простое — всего от разъема идёт 3 контакта, 1 из них обозначим условно массой(на самом деле неясно масса ли это, я не видел схемы и утверждать не берусь), 2й контакт это управление автофокусом и 3й конт��кт это затвор. Исследование поведения чёрного ящика камеры при замыкании контактов натолкнул меня на теорию, что внутри камеры к разъему подведён следующий эквивалент цепи:

При этом автофокус у меня отключён, так как настройку фокусировки я провожу вручную и затем фиксирую стекло малярным скотчем, чтобы со временем фокус не ушёл, поэтому мне достаточно замыкать оба контакта одновременно. И теперь возникает такая ситуация — Камера питается от 7-8 вольт(зависит от заряда аккумулятора), контроллер питается от 5 вольт. У камеры и контроллера свои собственные источники питания. Насколько безопасным будет напрямую присоединять разъем управления камерой к пинам контроллера учитывая что я не знаю что куда внутри ведёт? Не сожгу ли я что-то внутри? Ведь иначе к моим спискам бед присоединится в лучшем случае убитый разъем затвора, а в худшем – минус камера, которая ещё и месяца не пробыла в моих руках. Будет максимально обидно если это случится, и чтобы предупредить такое, нужно гальванически развязать цепь, например через реле. В таком варианте контроллер вообще никак не будет связан с камерой и это даёт гарантию что пины контроллера и пины разъема внешнего управления камеры не сгорят так как соединяются только сами с собой. В качестве реле выбрано миниатюрное реле AXICOM IM06 срабатывающее от 12 вольт, которое может замыкать две различные цепи не связанные друг с другом что ещё лучше так как входы затвора и фокуса не будут между собой связаны в бездействии. Управление реле будет осуществляться обычным транзисторным ключом КТ814+КТ316 с диодом подключённым параллельно выходу чтобы ЭДС катушки реле не сожгла выходной ключ обратным током. Схема крайне популярная, поэтому здесь показана не будет. В качестве управляющего затвором пина будет назначен PB8. Теперь нужно вернуться в код, а именно в тело функции void TIM1_UP_TIM16_IRQHandler(void), добавить две строки и увеличить задержку после спуска затвора, чтобы исключить шанс возникновения смазанного изображения и дать камере время записать снимок на карту памяти:
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET); HAL_Delay(50);//Удержание сигнала чтобы камера успела среагировать HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET); //Увеличиваем время ожидания чтобы дать камере время записать фото на карту памяти HAL_Delay(400)
Прошиваем контроллер, запускаем и смотрим:
Подведение итогов
И взглянем на то, что получилось по итогу:
Сравним с предыдущей попыткой:
Комментарии с моей стороны тут явно излишни. Пройдусь по ключевым моментам:
Значительное улучшение качества изображения за счёт новой камеры с большей матрицей и более лучшей оптикой
Благодаря предыдущему пункту мне открылась возможность сканировать звуковую дорожку без вреда для ушей
Проведена автоматизация ключевых действий сканера таких как: протяжка плёнки, сматывание отсканированной плёнки в бобину, съемка кадра
Добавление дисплея открыло возможность вести статистику сканирования записывая отдельно параметры баланса белого и количества отснятых кадров
В следующей части уже будет проведена доработка сканера, увеличение полезной площади фотографии за счёт доработки макро-переходника, маленький экранчик HD44780 будет заменён на VFD дисплей, в прошивку будут добавлены сервисные функции и возможность менять настройки подсветки на ходу а также ещё плюшки. По аналогии с предыдущей частью в конце добавлю что контент из конца 2й части был создан ближе началу 5-го месяца разработки.
Продолжение следует…
Если вам понравилась статья, вы можете отблагодарить автора:
ЮМани 410012072475999
Если у вас есть какие-то вопросы насчёт проекта, или идеи напишите пожалуйста в комментариях
UPD Я поправил листинг с HD44780
UPD 25.11.2025 Добавлены ссылки на прошивку к сканеру – в конце 4й части
