Рассказываю опыт разработки сканера 35-мм киноплёнки со звуковой дорожкой за 150$ в картинках наглядно.
Привет Хабр! По реакциям на предыдущую статью стало понятно что народ требует продолжения. Ну, раз требуете то будет =) По личным ощущениям в предыдущей части было маловато текста и я попробую это исправить. В этой части в качестве эксперимента увеличу количество текста и оптимизирую картинки расположив их парами где это возможно. После прочтения статьи напишите в комментариях, что думаете о таком изменении. Как и в предыдущей статье, в этой также будет много практики, но она будет несколько разбавлена теорией(листинги)
Краткий обзор статьи: я покажу свой первый epic fail, объясню причину такого происшествия(торопиться нужно только при ловле блох) и после необходимых исправлений продолжаю процесс разработки. Вторая половина статьи будет посвящена переходу на микроконтроллер семейства STM32 и первые попытки в UI. В качестве завершения автоматизирую работу с камерой.
В предыдущей части: для ЛЛ — приведу изложение синопсисом без учёта абзацев с историей формата 35мм(прочитаете в предыдущей части). Был получен подгон кинокопий ералаша в удовлетворительном состоянии, я первые пару дней смотрел на них, пошебуршил интернет, покумекал над результатами и принял решение сделать свой сканер с блекдежком и... с автоматикой и низким бюджетом(несколько ящиков балтики). Далее по ходу предыдущей части я начал работать над несколькими ключевыми компонентами: над окошком для просветки кадра(слева на КДПВ), приводом и подставкой для бобины, заимел локальный успех с этой авантюры и доработав привод завершил предыдущую статью.
Навигация по статье:
Часть вторая. Развитие идеи и переход на МК
Последствия ошибок конструирования
Мои небезопасные подходы к проектированию, которые я показывал в предыдущей части не прошли бесследно. Да, я делал работу над ошибками — я изменил тонкий штырь 4х4 мм на звёздочку в узле стыковки валика и шестерни, я изменил конфигурацию валиков сделав все 3 валика приводящими, но я не все ещё не принимал во внимание главную проблему — ломкость слоёв пластика. И в первую очередь это отразилось на состояние подопытной бобины с плёнкой:
Это моё первое и последнее серьёзное ЧП. Я взял паузу и пару дней не занимался проектом - анализировал произошедшую ситуацию и причины, по которым она возникла и сделал своё заключение. Первая серьёзная ошибка была допущена в расположении валиков с учётом первоначального замысла сделать приводящим только центральный, а позже сделать их всех приводящими. Так как я сначала думал что мне хватит одного валика, я не стал рассчитывать положение соседних, располагая их из соображений как будет красивее а не как правильнее — это привело к тому что зубчики крайних валиков, как бы я не пытался сориентировать их относительно плёнки — не вставали точно в дырки перфорации(см правое фото выше), а где-то рядом из-за чего зубчики выдавливали плёнку в области перемычек между отверстиями, что приводило к трещинам или собственно отрыву перемычек между отверстиями с последующим нарастающим повреждением в области:
![Крайние валики создавали высокую вероятность порвать плёнку Крайние валики создавали высокую вероятность порвать плёнку](https://habrastorage.org/getpro/habr/upload_files/4ef/0b9/bf4/4ef0b9bf442211b652f795974930115d.png)
Дальше события могли развиться так — либо фрагмент плёнки с оторванной перемычкой проходит дальше, либо отрываются перемычки с обоих сторон, валик перестаёт выполняет свою функцию и нагрузка с него переходит на следующий за ним, что в свою очередь с высоким шансом вызовет отрыв стенок паза в узле стыка валика с шестерёнкой из-за перегрузки, валик с высоким шансом клинит и уже отрывает своими зубчиками все последующие перемычки а то и просто рвёт плёнку:
![Трещина в кадре уже "моих рук" дело по той самой причине - шестерёнка развалилась в области стыка с валиком и он заклинил Трещина в кадре уже "моих рук" дело по той самой причине - шестерёнка развалилась в области стыка с валиком и он заклинил](https://habrastorage.org/getpro/habr/upload_files/cad/8a8/067/cad8a80676b215e799257603397c1d68.png)
На примере фото с плёнкой и фото кадра титров — произошёл второй вариант. Валик начало клинить и он начал рвать всё подряд пока я не услышал странные звуки и не остановил привод. Но! Ещё нужно рассказать о третьем варианте, который является жирной предпосылкой ко второму — я ещё был зелёным и не знал что в процессе трения деталей механизма, полностью состоящего из АБС происходит сильный нагрев с последующим размягчением пластика и детали могут слипнуться и заклинить. У меня и такое было, позже начал в места трения добавлять по капле машинного масла(отработанного) и это решало проблему.
Работа над ошибками
Исходя из этого прослеживается закономерный вопрос: А почему ты просто не изменишь чертёж и не исправишь? Вообще я и собирался так сделать, но так как бюджет диктовал мне условия, то я пошёл другим путём — придумал и применил костыль заключающийся в изменении количества зубчиков на шестерёнках, к которым прикрепляется звёздочка. По итогу количество зубчиков увеличилось с 36 до 72, благодаря чему я мог позволить выставлять более точную ориентацию валиков относительно центрального. Напоследок я решил «импортозаместить» чужую модельку заменив её своим аналоГовнетом, но с двумя изменениями — я убрал сугубо декоративное утончение к центру(надо сказать, старый вариант я по прежнему продолжал использовать но в менее критичных узлах) избавив валик от уязвимости на разлом по середине от чего его форма стала больше похожей на скалку и переделал форму зубчиков снизив их высоту от поверхности валика и изменил длину/ширину зубчика так чтобы он заполнял ~70% площади отверстия:
![Обновлённые модели валика и зубчиков с "багфиксами" Обновлённые модели валика и зубчиков с "багфиксами"](https://habrastorage.org/getpro/habr/upload_files/327/180/cfa/327180cfa4627199cda4550ec687928c.png)
Тут у вас может возникнуть вопрос насчёт способов стыковки валика с другими деталями — со одной стороны звёздочка а с другой паз для штыря, казалось бы очевидный вопрос — почему ты не доделал? Тут не всё так просто, как можно было видеть на “рентгене” привода в предыдущей части, вращение валику передаётся только с одной стороны(справа если смотреть рисунок выше), а с другой валик крепится к стенке через шарнирную опору. Отсюда можно заключить что нагрузка со стороны опоры либо минимальная, либо вообще отсутствует и следовательно, необходимости заменять устаревший способ крепления с одной из сторон не возникает. Теперь можно отправить на печать новые детали и посмотреть что получилось:
![Обратите внимание на зубчики крайних валиков — теперь они встают в дырки перфорации Обратите внимание на зубчики крайних валиков — теперь они встают в дырки перфорации](https://habrastorage.org/getpro/habr/upload_files/ee4/5cf/e0f/ee45cfe0f7c9f7573bd92a164629ab6b.jpg)
Если сравнивать с фотографией раннее то можно отметить что на этот раз на всех трёх валиках есть довольно точное попадание зубчиков в дырки перфорации и таким образом значительно снижается риск повредить плёнку.
![](https://habrastorage.org/getpro/habr/upload_files/60b/f8c/7ea/60bf8c7eab215cc44c493d00678bbe7a.png)
Апгрейд модуля с кадровым окном
Теперь, когда опасность успешно закупорена миновала, можно возобновить свои изыски. Как я говорил в прошлой части — кадровое окно хоть и работает как задумано, но оно не даёт возможности что-то к нему пристыковать и закрепить тем самым является бесперспективным элементом, к тому же мне не даёт покоя мысль что я использую чужую наработку. Если на второе ещё можно забить, то первый факт этого не позволит, поэтому встала острая необходимость провести обновку. Покажу однослайдовую псевдо-3D презентацию, которую подготовили тараканы в моей голове:
![В теории этот бутерброд можно наращивать бесконечно В теории этот бутерброд можно наращивать бесконечно](https://habrastorage.org/getpro/habr/upload_files/bef/622/451/bef622451f251945b11a63286db4ed37.png)
Центральные два блока являются по сути своей одной моделькой с углублением в длина_блока[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
![Если моё умение писать код выразить картинкой, то эта — наилучший выбор Если моё умение писать код выразить картинкой, то эта — наилучший выбор](https://habrastorage.org/getpro/habr/upload_files/e31/14b/b9d/e3114bb9d1fa0f713354e7350b15d67b.png)
Но каждый раз хардкодить настройки цвета под бобину неудобно и убивает ресурс флешки в контроллере — стоит задуматься над возможностью задания нужных значений в процессе работы, и это хорошая подводка для того чтобы начать думать над UI. Возвращаюсь в CubeMX и подключаю пины для клавиатуры — 4 пина будут опрашивать столбцы, ещё 4 будут ожидать бит из строк — пусть это будет PB0,PB1,PB2 и PC4 для сканирующих пинов столбцов и PA4,PA5,PA6,PA7 для пинов строк, на входе которых ожидается лог.1. Не забываем про подтяжку к земле чтобы не ловить глюки от клавиатуры. PC4 лежит особняком и на него можно будет посадить сервисные кнопочки. Настраиваем пины в кубе
![Пины, которые будут участвовать в опросе клавиатуры Пины, которые будут участвовать в опросе клавиатуры](https://habrastorage.org/getpro/habr/upload_files/78e/8c3/ee7/78e8c3ee70f691f26ae23ca5c8c349f6.png)
Чего-то тут не хватает, не думаете? Ну конечно же! Допустим юзер будет нажимать кнопки и даже выучит что в каком порядке сначала записывается(ну или прочитает мурзилку к девайсу), но вдруг кнопка не нажалась а юзер этого не заметил? Например он хочет записать 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 пустая т.к я не придумал ей назначения](https://habrastorage.org/getpro/habr/upload_files/c98/412/758/c9841275859cbcc0776625f2990a93a5.png)
ИД 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);
}
Собираем проект, запускаем и смотрим(я уже нажал на кнопку “#”):
![Примерно это и ожидалось увидеть Примерно это и ожидалось увидеть](https://habrastorage.org/getpro/habr/upload_files/740/f0d/925/740f0d9252489c46de5d5d876e5d7a06.png)
Переезд шаговика с логики на МК
Пока буду считать, что всё работает нормально. Теперь надо разобраться с шаговиком. Как я говорил раньше(в 3й раз, хах) — шаговик приводился в движение схемой на логике К155, но так как у меня наметился массовый переезд периферии на контроллер то теперь управление шаговиком можно сделать более гибким. Управлять можно как софтверно, так и железно. Я предпочту управление железно. А как можно управлять железно с контроллера? Ну например с помощью ШИМ накручивать шаги, и двумя другими пинами управлять направлением и состоянием драйвера — включён/отключён. В STM32 практически каждый таймер может генерировать ШИМ, но таймеры в STM32 все разные — первые по номерам самые крутые и богатые по функционалу, последние — самые простые. Так как от таймера мне нужна всего лишь генерация ШИМ, то я конечно же выберу простой таймер. Возвращаюсь в куб и активирую таймер TIM15 задав скважность ШИМ импульсов в 50%:
![Настройка таймера в качестве генератора импульсов шагового двигателя Настройка таймера в качестве генератора импульсов шагового двигателя](https://habrastorage.org/getpro/habr/upload_files/51b/0a7/697/51b0a76973148fb0d288f050dd2837b4.png)
С текущими настройками драйвера — шаг раздроблен на 16 микрошагов — для совершения полного оборота нужно 3200 шагов(с редуктором 1:2 в ЛПМ). На совершение полуоборота понадобится 1600 микрошагов(без редуктора в ЛПМ). Можно узнать период импульса, просуммировать периоды 1600 или 3200 раз и прикинуть — через сколько времени нужно остановить таймер но это ненадёжно — контроллер в нужный момент может быть занят прерыванием — и это много довольно таки сложного кода. И поэтому можно воспользоваться следующим фокусом, который позволят провернуть «крутые» таймеры — первый таймер как и раньше гоняет ШИМ, но второй будет тактироваться от ШИМ импульсов, идущих с первого таймера(просто кинуть перемычку с выхода на вход другого) и по достижении значения в 3200 или 1600(попугаев?) генерировать прерывание с высоким приоритетом, останавливающее двигатель. В качестве ведомого таймера в STM32F100 доступны TIM1-TIM3. Мой выбор остановился на TIM1. Настраиваем тактирование TIM1 извне — через PA12, выбираем тип события Update Event и в NVIC(менеджер прерываний) включаем прерывание, общее с TIM1 и TIM16 и задаём ему высший приоритет, чтобы оно всегда срабатывало вовремя:
![Настройка таймера на внешнее тактирование и срабатывание после 3200 тактов Настройка таймера на внешнее тактирование и срабатывание после 3200 тактов](https://habrastorage.org/getpro/habr/upload_files/e07/e81/978/e07e819782a3d2fd1f011d6e899713a3.png)
Теперь надо запустить генерацию кода. Приложение довольно бережно обращается с кодом, поэтому беспокоиться о потере кода не стоит если конечно код написан в областях, обозначенными комментариями код-генератора, за другие области он не ручается. После обновления проекта 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 шагов назад к началу истории. Подсветка уже собрана, светит, и теперь надо задуматься о том как её более-менее грамотно прикрепить к окну. Пока я рожал код, в голове снова возникла лайфхачная идея. Пора открыть форточку и подышать воздухом сменив вид деятельности.
Возвращаясь к теме кадрового окна...
Старое кадровое окно мне не даёт покоя, поэтому его надо заменить. Опишу новый замысел: хоть светодиоды и светят довольно ярко, но напрямую просвечивать ими плёнку нельзя так как в первую очередь это точечный источник освещения, и он не сможет равномерно просвечивать всю площадь кадра. Ему надо светить на что-то, что превращает свет в рассеянный и не просто рассеянный а ещё с равномерным распределением силы света. Почему бы не попробовать обычную бумагу в роли отражателя? А что, это дёшево, вылезать из кожи вон придумывая что-то мозговыносящее не нужно, бумага отражает свет вполне себе равномерно(но если светить на просвет то всё тлён). По итогу рождается модуль подсветки с отражателем:
![От бумаги свет отражается во все стороны, я обозначил только важные направления отражения От бумаги свет отражается во все стороны, я обозначил только важные направления отражения](https://habrastorage.org/getpro/habr/upload_files/26c/f16/323/26cf163234d6ef942789e9444f4d6835.png)
Как выглядит модуль внутри с включённой подсветкой(+10 если выбрать цвет как на фото, +20 если выключить комнатный свет):
И с накинутым кадровым окном(чёрным закрашено чтобы не было отражений от стёкол макро-переходника на стенки и обратно):
Ничего гениального, просто мне снова удалось применить немного нестандартный подход. На фото может показаться что слева чуть пересвечено, но поверьте — если смотреть строго перпендикулярно к поверхности отражателя(бумаги) — светит очень равномерно. Разумеется свет отражается не только в окно, но и вообще во все стороны, я не стал показывать стрелочками на схеме отражения света во все стороны, оставив только главное. Помните оцифровку ещё через старое окно, которую я показал в первой части? Тогда я пользовался макро-переходником, заказанным с Ozon который себя не оправдал. Но я всё же провёл ещё одну попытку оцифровки с этим же переходником, и на этот раз оцифровал эпизод полностью:
Обновляем главный элемент
Скан выше в том виде, в каком я его сделал непригоден для комфротного просмотра. Во первыйх warped-звук из-за трудностей сканирования дорожки, вызванных дисторсией от линзы(даже если потом вторым проходом сканировать только дорожку поместив её по центру кадра то проблему это не особо исправит), во вторых изображение к краям расфокусируется и это не исправить одной лишь сенсухой с макро-насадкой за 300 грiвен рублей. Решение как всегда лежит на поверхности — можно использовать фотоаппарат. Так как кадров нужно делать много и делать часто, то приоритет поставлен в сторону беззеркальных решений но они раздувают бюджет по сравнению с зеркалками(почему так — я без понятия, по идее беззеркальное исполнение должно быть дешевле т.к меньше сложных деталей[вызываю пояснительную бригаду в комменты]). Я некоторое время бродил по тредам различных форумов ища информацию о недорогой модели с большим количеством мегапикселей — 15 и больше, довольно большой матрицей — хотя бы половину от 35мм и в беззеркальном исполнении, ну или на крайний случай — чтобы зеркало можно было поднять и просто снимать как мыльницей. Как вы уже поняли — такого решения за небольшой кэш не оказалось и я забил болт на свои требования найдя на авито незадорого(8000) народную зеркалку Nikon D3200, которая удовлетворила практически все мои требования кроме зеркала. Знакомьтесь, в студии будущий трудяга, который за короткий промежуток времени сделает более 150000 снимков:
Я довольно быстро(за 30-40 минут стояния с фотиком над плёнкой протянутой через кадровое окно) понял что с наскока сделать снимок кадра плёнки на весь размер фотографии не получится и нужно либо менять стоковый объектив, либо придумывать макро-переходник самому. Цены на телеобъективы быстро отбили желание проводить розыскные мероприятия в этом направлении и поэтому я продолжу свой суровый колхозинг. Моему воображению подаётся новая задача — сделать так чтобы либо картинка перед объективом была больше либо чтобы камера могла лучше фокусироваться(я думаю что по сути это одно и то же), и так как практика моя сильная сторона, то конструировать переходник буду также экспериментируя на ходу. Так родилась следующая задумка переходника в моей голове:
![](https://habrastorage.org/getpro/habr/upload_files/eff/7f2/c62/eff7f2c62ebeebc954772e14984cc977.png)
Я не стал заниматься расчётами так как уже просто забыл оптику, и даже если я проведу расчёт то нужная линза будет либо недоступна либо окажется слишком дорогой и я решил пользоваться тем что попадёт под руку, ну или просто что смогу найти у себя. У себя я смог найти лупу Урал 1.5х, ей и воспользуюсь. Но я не хочу терять возможность пользоваться увеличилкой в быту, поэтому для неё надо придумать решение, которое позволит без труда вытаскивать лупу — например платформу с выемкой, в которую её можно вложить. Оптимальное расстояние от поверхности плёнки до линзы увеличительного стекла я подобрал балансируя между размером увеличенного изображения и возможностями Kit-объектива, осталось замоделить коробочку, распечатать и вместе с ней распечатать платформу для лупы:
Теперь включим подсветку кадра, протащим плёнку до дисклеймера студии и оценим эффективность переходника на примере звуковой дорожки как самой чувствительной к чёткости области плёнки:
Как можно заметить, звуковая дорожка видна крайне хорошо. По краям пропал расфокус и остались только цветовые дисторсии, но это вообще не проблема — можно преобразовать фото в ч/б отбросив синий и красные компоненты, но впереди ещё предстоит работа. Даже если сократить продолжительность сканирования до 750 кадров(интрошка ералаша) то держа камеру руками я просто не смогу всё время помещать её в одно и то же положение с точностью хотя бы до сантиметра, поэтому надо придумать способ удержания камеры статично в течении долгого времени. Набросок я сделал сразу, и само решение я подготовил за полтора вечера:
За винт не переживайте, он ничего не замкнул. При закручивании болт натыкается на заглушку — её невозможно сломать не приложив диких усилий, чего я конечно делать не стану. Финальным штрихом тут будет крепление через какую-нибудь опору к специально сделанным для этого площадкам на корпусе переходника, но нельзя торопиться! Подумайте, что можно ещё добавить? Прям хорошо так подумайте? Ладно, не буду томить и подскажу — камера с высокой вероятностью, а скорее всего не с высокой а именно так и будет — будет закреплена с перекосом по одной или двум сторонам. Если я сейчас задумаю и исполню жесткое крепление, то не смогу потом компенсировать перекос быстрым путём и это добавит головной боли на этапе обработки полученных фотографий кадров(несложно конечно, но лучше чтобы поменьше проблем было). Поэтому добавлю ещё одну фичу:
![Рисунок из pbrush, снова) На самом деле я вообще не знаю других редакторов кроме паинта, даже с фотошопом на Вы Рисунок из pbrush, снова) На самом деле я вообще не знаю других редакторов кроме паинта, даже с фотошопом на Вы](https://habrastorage.org/getpro/habr/upload_files/3a0/3fb/e8d/3a03fbe8dc7d55c390e369572b147b74.png)
Это решение простое, но эффективное по своему действию. Идеально ровно закрепить камеру я не могу, для этого нужно менять технологию изготовления деталей, но компенсировать кривизну мне под силу. Также с этим решением сокращается время, необходимое чтобы выровнять камеру относительно окна — достаточно будет подкрутить ту или иную гайку. Можно добавить ещё одно красивое решение — растянуть ушки чтобы платформа могла скользить не только по высоте но и вдоль, однако эту идею я так и не привёл в исполнение(лень было). Теперь, когда все необходимые элементы продуманы, представляется эксклюзивно для подписчиков возможность полностью собрать первый работающий образец кадрового окна:
Пройдусь по чек-листу:
Сделать подставку для бобины — Готово
Сделать привод ЛПМ — Готово
Сделать новое кадровое окно — Готово
Сделать компактную подсветку — Готово
Сделать макропереходник — Готово
Сделать крепление камеры — Готово
Перевести управление на МК — Готово
Сделать возможность сматывания плёнки после скана — Гот.. OH SHI!~
#@^%…
Ну так то, если не учитывать что после работы с проектором от аккуратно сложенной бобины останется что-то такое...
Решаем проблему с кучей плёнки около стола
...можно сказать что по основным компонентам работа завершена, и теперь осталось только причесать и подправить. Формально, проектор и правда готов к работе, но тратить 10 минут сматывая плёнку обратно как-то не комильфо, поэтому надо идти дальше. В предыдущей части, делая узел с посадочным местом под шпиндели бобины я добавил узлам ушки под винтики для крепления блоков к узлу. Так вот, теперь эти ушки мне пригодятся. Сейчас первый этап задачи сводится к повторной печати всех компонентов подставки для бобины за исключением одной из “муфт” — я уберу крестовину(в первой части я упоминал про крестовину) так как она сама по себе хрупкая и заменю редуктором 1:10(потом придётся ещё больше увеличивать коэффициент редуктора), и поставлю хорошо зарекомендовавший себя шаговый тип двигателя(не потому что именно он рекомендуется, а потому что я уже знаю как с ним работать). Но одного этого мало. Как известно с каждым витком увеличивается и длина витка а это значит, что этот факт тоже нужно учитывать, иначе я рискую либо порвать плёнку либо сломать привод принимающей бобины либо всё вместе(дайте два). Передо мной опять предстала вилка из двух выборов — пойти сложным путём придумывая какие-то матановские алгоритмы, либо пойти простым путём и придумать очередной хитрый датчик. Что я выберу угадать несложно. Мысль с датчиком у меня выглядела следующим образом. В пути не было возможности сесть за комп, поэтому рисовал от руки:
Прокомментирую рисунок «курица лапой». Слева сверху показан общий вид спереди. Датчик натяжения работает по типу гильотины, но вместо “лезвия” — параллелепипед скруглённый с одной стороны с одним слоем скотча для лучшего скольжения плёнки через него, по краям к нему добавлены лапки для крепления пружин, тянущих всю конструкцию вниз. До конца “гильотина” не падает а попадает на подпорки чтобы не прижимать плёнку и не создавать риска стереть или повредить эмульсию. По центру сверху тот же общий вид, но только сбоку. Справа пример прохождения плёнки — плёнка от валика, который заведомо расположен выше, проходит через датчик, расположенный прямо под принимающей бобиной и после сразу на бобину. Тем самым когда все излишки намотаны на бобину, создаётся натяжение, которое начинает подымать “гильотину”, которая в свою очередь подымает шторку, шторка в свою очередь утолщаясь снижает силу светового потока на фоторезистор и замедляет работу двигателя до полной остановки и когда натяжение ослабляется — двигатель снова приходит в движение, тем самым такие качели поддерживают заданное натяжение. Натяжение можно регулировать подстроечным резистором в одном из плеч резистивного делителя(второе плечо - сам фоторезистор) Реализация в solidworks несколько отличается — но в лучшую сторону:
![](https://habrastorage.org/getpro/habr/upload_files/158/e32/eaf/158e32eaf3ac1de9dde068eede4194f5.png)
Забегая вперёд покажу пример работы этого узла:
Ну и в качестве приятного бонуса надо отдельно показать фотку “шторки” как самой красивой детали в моём проекте:
Фоторезистор подключался к контроллеру как одно из плеч резистивного делителя напряжения(выше описал), средняя точка которого уже будет подключена ко входу АЦП на борту контроллера. В STM32 есть встроенный АЦП, функций которого хватит сполна. Перед тем как снова начать бредокодить, надо подготовить механизм датчика натяжения — подключить резисторный делитель ко входу, который будет обозначен как АЦП, напечатать на принтере комплект подставки под бобину и прикрепить шаговик. По итогу конструкция, которая будет принимать "отработанную" плёнку практически не отличается от раздающей, за исключением что под ней будет помещён датчик натяжения и сбоку прикреплён двигатель вращающий колесо бобины через редуктор:
![Принимающая бобина. Такой вид в итоге станет финальным для моего проекта Принимающая бобина. Такой вид в итоге станет финальным для моего проекта](https://habrastorage.org/getpro/habr/upload_files/a1e/0c9/eb1/a1e0c9eb16364057025ca4049853adc2.png)
Теперь, когда всё готово, можно начинать работу с контроллером. Я повторяю свой подход, и назначаю для работы с АЦП также отдельное прерывание и отдельный таймер — 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й контакт это затвор. Исследование поведения чёрного ящика камеры при замыкании контактов натолкнул меня на теорию, что внутри камеры к разъему подведён следующий эквивалент цепи:
![Эквивалент внутренних цепей фотоаппарата для разъема внешнего управления. Мб это всё прямо к пинам проца подключено, не знаю честно Эквивалент внутренних цепей фотоаппарата для разъема внешнего управления. Мб это всё прямо к пинам проца подключено, не знаю честно](https://habrastorage.org/getpro/habr/upload_files/0d0/d98/409/0d0d98409bb639e7078a7098b6bab0c5.png)
При этом автофокус у меня отключён, так как настройку фокусировки я провожу вручную и затем фиксирую стекло малярным скотчем, чтобы со временем фокус не ушёл, поэтому мне достаточно замыкать оба контакта одновременно. И теперь возникает такая ситуация — Камера питается от 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
Если у вас есть какие-то вопросы насчёт проекта, или идеи напишите пожалуйста в комментариях
radiolok
Потрясающая работа проведена! У самого лежит горстка научно-технических слайдов 35мм - их тоже бы оцифровать, ибо многие ленты я в интернете так и не нашел. Кадров там сильно меньше и многие операции можно выполнить вручную, но протяжку, подсветку, юстировку - стоит немного моторизировать.
Часть наработок перетащу себе в задумку. Какими-то чертежами поделитесь?
d2ab
Так для фотопленки все гораздо проще и есть готовые слайд-сканеры за разумные деньги. Я когда-то покупал Nikon за 1000$, а сейчас полно на озоне китайских за сущие копейки или поприличнее типа Plustek.