Рассказываю опыт разработки сканера 35-мм киноплёнки со звуковой дорожкой за 150$ в картинках наглядно.

 Сканер, о котором будет идти речь в статье
Сканер, о котором будет идти речь в статье

Привет Хабр! По реакциям на предыдущую статью стало понятно что народ требует продолжения. Ну, раз требуете то будет =) По личным ощущениям в предыдущей части было маловато текста и я попробую это исправить. В этой части в качестве эксперимента увеличу количество текста и оптимизирую картинки расположив их парами где это возможно. После прочтения статьи напишите в комментариях, что думаете о таком изменении. Как и в предыдущей статье, в этой также будет много практики, но она будет несколько разбавлена теорией(листинги)

Часть 1

Краткий обзор статьи: я покажу свой первый epic fail, объясню причину такого происшествия(торопиться нужно только при ловле блох) и после необходимых исправлений продолжаю процесс разработки. Вторая половина статьи будет посвящена переходу на микроконтроллер семейства STM32 и первые попытки в UI. В качестве завершения автоматизирую работу с камерой.

В предыдущей части: для ЛЛ — приведу изложение синопсисом без учёта абзацев с историей формата 35мм(прочитаете в предыдущей части). Был получен подгон кинокопий ералаша в удовлетворительном состоянии, я первые пару дней смотрел на них, пошебуршил интернет, покумекал над результатами и принял решение сделать свой сканер с блекдежком и... с автоматикой и низким бюджетом(несколько ящиков балтики). Далее по ходу предыдущей части я начал работать над несколькими ключевыми компонентами: над окошком для просветки кадра(слева на КДПВ), приводом и подставкой для бобины, заимел локальный успех с этой авантюры и доработав привод завершил предыдущую статью.

Навигация по статье:

  1. Последствия ошибок конструирования

  2. Работа над ошибками

  3. Апгрейд модуля с кадровым окном

  4. RGB подсветка

  5. Делаем первый UI

  6. Сканер клавиатуры

  7. Переезд шаговика с логики на МК

  8. Возвращаясь к теме кадрового окна...

  9. Обновляем главный элемент

  10. Решаем проблему с кучей плёнки около стола

  11. Автоматизируем управление камерой

  12. Подведение итогов


Часть вторая. Развитие идеи и переход на МК

Последствия ошибок конструирования

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

Epic fail
Epic fail

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

Крайние валики создавали высокую вероятность порвать плёнку
Крайние валики создавали высокую вероятность порвать плёнку

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

Трещина в кадре уже "моих рук" дело по той самой причине - шестерёнка развалилась в области стыка с валиком и он заклинил
Трещина в кадре уже "моих рук" дело по той самой причине - шестерёнка развалилась в области стыка с валиком и он заклинил
Типичная картина, которую я наблюдал почти каждый раз разбирая привод ЛПМ при аварийной остановке
Типичная картина, которую я наблюдал почти каждый раз разбирая привод ЛПМ при аварийной остановке

На примере фото с плёнкой и фото кадра титров — произошёл второй вариант. Валик начало клинить и он начал рвать всё подряд пока я не услышал странные звуки и не остановил привод. Но! Ещё нужно рассказать о третьем варианте, который является жирной предпосылкой ко второму — я ещё был зелёным и не знал что в процессе трения деталей механизма, полностью состоящего из АБС происходит сильный нагрев с последующим размягчением пластика и детали могут слипнуться и заклинить. У меня и такое было, позже начал в места трения добавлять по капле машинного масла(отработанного) и это решало проблему.

Работа над ошибками

Исходя из этого прослеживается закономерный вопрос: А почему ты просто не изменишь чертёж и не исправишь? Вообще я и собирался так сделать, но так как бюджет диктовал мне условия, то я пошёл другим путём — придумал и применил костыль заключающийся в изменении количества зубчиков на шестерёнках, к которым прикрепляется звёздочка. По итогу количество зубчиков увеличилось с 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А при максимальных значениях. Собираю рамочку:

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

Рамка изготовлена из 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();
Примерно такой уровень яркости при параметрах RGB 0x02, 0x02, 0x02. Я считаю что это слишком ярко
Примерно такой уровень яркости при параметрах RGB 0x02, 0x02, 0x02. Я считаю что это слишком ярко

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

char template[16] = “ R000 G000 B000 ”;//Шаблон для настройки цвета
char readyMsg[16] = “     READY      “;	//Сообщение о готовности к работе
char framesTemplate[16] = “Frames:         “;//Шаблон для счётчика кадров
char eiMsgUpper[16] = “      User      “;//Мессага если работа прервана юзером
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[5][8]{	//Пользовательский символ
	{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[5][8]{	//Пользовательский символ плёнки
	{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[5][8]{	//Пользовательский символ часов
	{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[5][8]{	//Пользовательский символ стрелочки
	{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 мс, насколько хорошим или плохим будет такой поступок я не знаю, но мне так проще держать в голове что откуда запускается. Перед тем как показывать ещё один всратый листинг нужно продемонстрировать табличку маппинга кнопок, чтобы потом не было непонятных ситуаций:

Слева маппинг ИД кнопок, справа функциональные обозначения. Кнопка с ИД 15 пустая т.к я не придумал ей назначения
Слева маппинг ИД кнопок, справа функциональные обозначения. Кнопка с ИД 15 пустая т.к я не придумал ей назначения

ИД 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 и задаём ему высший приоритет, чтобы оно всегда срабатывало вовремя:

Настройка таймера на внешнее тактирование и срабатывание после 3200 тактов
Настройка таймера на внешнее тактирование и срабатывание после 3200 тактов

Теперь надо запустить генерацию кода. Приложение довольно бережно обращается с кодом, поэтому беспокоиться о потере кода не стоит если конечно код написан в областях, обозначенными комментариями код-генератора, за другие области он не ручается. После обновления проекта 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 снимков:

Nikon D3200 Kit(комплект) со стоковым объективом
Nikon D3200 Kit(комплект) со стоковым объективом

Я довольно быстро(за 30-40 минут стояния с фотиком над плёнкой протянутой через кадровое окно) понял что с наскока сделать снимок кадра плёнки на весь размер фотографии не получится и нужно либо менять стоковый объектив, либо придумывать макро-переходник самому. Цены на телеобъективы быстро отбили желание проводить розыскные мероприятия в этом направлении и поэтому я продолжу свой суровый колхозинг. Моему воображению подаётся новая задача — сделать так чтобы либо картинка перед объективом была больше либо чтобы камера могла лучше фокусироваться(я думаю что по сути это одно и то же), и так как практика моя сильная сторона, то конструировать переходник буду также экспериментируя на ходу. Так родилась следующая задумка переходника в моей голове:

Я не стал заниматься расчётами так как уже просто забыл оптику, и даже если я проведу расчёт то нужная линза будет либо недоступна либо окажется слишком дорогой и я решил пользоваться тем что попадёт под руку, ну или просто что смогу найти у себя. У себя я смог найти лупу Урал 1.5х, ей и воспользуюсь. Но я не хочу терять возможность пользоваться увеличилкой в быту, поэтому для неё надо придумать решение, которое позволит без труда вытаскивать лупу — например платформу с выемкой, в которую её можно вложить. Оптимальное расстояние от поверхности плёнки до линзы увеличительного стекла я подобрал балансируя между размером увеличенного изображения и возможностями Kit-объектива, осталось замоделить коробочку, распечатать и вместе с ней распечатать платформу для лупы:

Платформа для увеличительного стекла сделана с расчётом что стекло можно будет быстро вытащить. Причём сама платформа напечатана отдельно от коробочки
Платформа для увеличительного стекла сделана с расчётом что стекло можно будет быстро вытащить. Причём сама платформа напечатана отдельно от коробочки
Конструктору пива больше не наливать))
Конструктору пива больше не наливать))

Теперь включим подсветку кадра, протащим плёнку до дисклеймера студии и оценим эффективность переходника на примере звуковой дорожки как самой чувствительной к чёткости области плёнки:

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

Как можно заметить, звуковая дорожка видна крайне хорошо. По краям пропал расфокус и остались только цветовые дисторсии, но это вообще не проблема — можно преобразовать фото в ч/б отбросив синий и красные компоненты, но впереди ещё предстоит работа. Даже если сократить продолжительность сканирования до 750 кадров(интрошка ералаша) то держа камеру руками я просто не смогу всё время помещать её в одно и то же положение с точностью хотя бы до сантиметра, поэтому надо придумать способ удержания камеры статично в течении долгого времени. Набросок я сделал сразу, и само решение я подготовил за полтора вечера:

Набросок платформы для крепления камеры, таким я его себе по началу представлял...
Набросок платформы для крепления камеры, таким я его себе по началу представлял...
И во что моя мысль в итоге превратилась
И во что моя мысль в итоге превратилась

За винт не переживайте, он ничего не замкнул. При закручивании болт натыкается на заглушку — её невозможно сломать не приложив диких усилий, чего я конечно делать не стану. Финальным штрихом тут будет крепление через какую-нибудь опору к специально сделанным для этого площадкам на корпусе переходника, но нельзя торопиться! Подумайте, что можно ещё добавить? Прям хорошо так подумайте? Ладно, не буду томить и подскажу — камера с высокой вероятностью, а скорее всего не с высокой а именно так и будет — будет закреплена с перекосом по одной или двум сторонам. Если я сейчас задумаю и исполню жесткое крепление, то не смогу потом компенсировать перекос быстрым путём и это добавит головной боли на этапе обработки полученных фотографий кадров(несложно конечно, но лучше чтобы поменьше проблем было). Поэтому добавлю ещё одну фичу:

Рисунок из pbrush, снова) На самом деле я вообще не знаю других редакторов кроме паинта, даже с фотошопом на Вы
Рисунок из pbrush, снова) На самом деле я вообще не знаю других редакторов кроме паинта, даже с фотошопом на Вы

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

Посмотрите на количество блоков из которых состоит узел с окном. Сравните с узлом на КДПВ
Посмотрите на количество блоков из которых состоит узел с окном. Сравните с узлом на КДПВ

Пройдусь по чек-листу:

  1. Сделать подставку для бобины — Готово

  2. Сделать привод ЛПМ — Готово

  3. Сделать новое кадровое окно — Готово

  4. Сделать компактную подсветку — Готово

  5. Сделать макропереходник — Готово

  6. Сделать крепление камеры — Готово

  7. Перевести управление на МК — Готово

  8. Сделать возможность сматывания плёнки после скана — Гот.. 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. Послушайте сами:

Прототип сканера практически готов, если не учитывать разложенные на столе кишки:

Дальше будет ещё больше хлама на столе =)
Дальше будет ещё больше хлама на столе =)

Автоматизируем управление камерой

Всё готово к запуску, но запуск надо ещё раз отложить — кое-чего всё же не хватает. А именно — кто будет нажимать на кнопку затвора? Любезно предоставлю это контроллеру, тем более у камеры есть разъем для подключения внешнего управления затвором — слева с краю на фото:

Разъемы Nikon D3200. Слева-направо - удаленное управление затвором, HDMI, USB, A/V(PAL/NTSC)
Разъемы Nikon D3200. Слева-направо - удаленное управление затвором, HDMI, USB, A/V(PAL/NTSC)

Управление затвором предельно простое — всего от разъема идёт 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) 

Прошиваем контроллер, запускаем и смотрим:

Подведение итогов

И взглянем на то, что получилось по итогу:

Сравним с предыдущей попыткой:

Комментарии с моей стороны тут явно излишни. Пройдусь по ключевым моментам:

  1. Значительное улучшение качества изображения за счёт новой камеры с большей матрицей и более лучшей оптикой

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

  3. Проведена автоматизация ключевых действий сканера таких как: протяжка плёнки, сматывание отсканированной плёнки в бобину, съемка кадра

  4. Добавление дисплея открыло возможность вести статистику сканирования записывая отдельно параметры баланса белого и количества отснятых кадров

В следующей части уже будет проведена доработка сканера, увеличение полезной площади фотографии за счёт доработки макро-переходника, маленький экранчик HD44780 будет заменён на VFD дисплей, в прошивку будут добавлены сервисные функции и возможность менять настройки подсветки на ходу а также ещё плюшки. По аналогии с предыдущей частью в конце добавлю что контент из конца 2й части был создан ближе началу 5-го месяца разработки.


Продолжение следует…

Если вам понравилась статья, вы можете отблагодарить автора:

ЮМани 410012072475999

Если у вас есть какие-то вопросы насчёт проекта, или идеи напишите пожалуйста в комментариях

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


  1. radiolok
    07.02.2025 10:34

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


    1. d2ab
      07.02.2025 10:34

      Так для фотопленки все гораздо проще и есть готовые слайд-сканеры за разумные деньги. Я когда-то покупал Nikon за 1000$, а сейчас полно на озоне китайских за сущие копейки или поприличнее типа Plustek.


  1. NekitGeek
    07.02.2025 10:34

    Следующую версию можно сделать на ПЗС линейке и ПЛИС