PIC16F1503. Тачка на прокачку — 2. Свет
Раньше было про звук.Прошлый пост я оставил незаконченным. Если вы помните, то мне никак не удавалось подобрать «то самое звучание». Попытки подобрать «циферки по наитию» получались куда хуже обычного «пиу-пиу»… С одной стороны все равно — от китайской пищалки звука не добиться, а с другой стороны — «нечистая работа, низкий класс». Опять же, загонять тактовую частоту на 16МГц ради такого…
В общем, я где-то что-то сделал неправильно. Устроенный очередным вечером ликбез по музыке и ее грамоте породил еще больше вопросов, чем было до этого (вроде почему есть до-диез, но нет ре-диез, а вместо него ми-бемоль?). Но мне не привыкать «сдавать японский по методичке», поэтому продолжал разбираться. Одновременно с заказчиком обсуждали изменение ТЗ (знакомая картина, не правда ли?), заключавшееся в добавлении «подсветки днища». На мои робкие попытки сказать, что это вообще-то полицейская машина был получен ответ, что это полицейская машина в негритянском квартале…Для начала решил добить звук. Сменил частоты и PWM, поставил делитель на 1 на 255, получил частоты 7692Гц и 62Гц. Диапазон немного изменился, но все равно — вполне.
И тут что-то меня дернуло измерить частоту при делителе 128. Я ожидал получить что-то типа (7692–62)/2=3815Гц, но получил почему-то всего 125… И тут меня, как моих любимых программистов в свитерах с оленями, осенило:, а кто сказал, что зависимость частота-делитель линейная? Пара лишних измерений показала, что я абсолютно прав. Немного трахтибидоха с осциллографом и экселем дали вот такую вот красивую картинку Hz-Timer (оригиналы как обычно, в архиве в конце поста):
Теперь мне стало понятно, почему у меня звуки выходили такие ужасные. Плюс в комментариях вспомнили про дверные звонки, где в 2Кб ПЗУ засовывали мелодии… Когда знаешь, что надо искать, то находится быстро.
Первая (вроде) такая схема была опубликована в журнале Радио, 92 год, 8й номер. Там же была шикарная (для меня) табличка
А на следующей странице — сама прошивка. Еще больше гуглежа и находим «Радиолюбитель», 93 год, февраль. Там абсолютно тот же принцип, но мелодий уже больше.
Опять заряжаем эксель (в архиве на второй вкладке) и переводим задержки из «железячных» в «софтовые».
Добавляем немного кода
// come cool sound const uint8_t m1[31]={49,49,52,52,58,58,58,58,38,38,28,52,52,52,52,52,52,52,52,38,38,36,38,36,38,52,52,49,49,49,49}; const uint8_t m2[31]={49,42,38,38,49,42,38,42,49,52,58,52,49,42,52,65,49,42,38,38,49,42,38,42,49,52,58,42,49,42,52}; // play like this uint8_t c; for (c=0; c<31;c++) { PWM3_LoadDutyValue(m1[c]*2); TMR2_LoadPeriodRegister(m1[c]); __delay_ms(120); } И ура! Из китайской пищалки доносятся вполне себе узнаваемые звуки, которые не оскорбляют даже мои уши. В принципе можно гордится собой — мы на новой элементной базе повторили 20-ти летнюю схему на «логике». Так что даже если машинка не будет играбельна, всегда можно будет переделать ее в дверной звонок :)В общем, пока процесс поиска наилучшего «вау-вау» продолжается, но в принципе проблему звука мы закрыли. Переходим к свету.
Как я писал в первом посту, родные светодиоды были синими и очень яркими. Плюс меня возмутило, что синий светит через красный светофильтр и нарушает всю гармонию. Значит надо поменять.
Достал из заначек красный светодиод, включил. Первая проблема: яркость разная. Синий ну очень яркий. Меняем на «из заначек» — все равно разная. Конечно, можно приглушить дополнительными резисторами, но это слишком просто. Наконец добрался до RGB светодиода и чудо свершилось! Он всеми цветами светит с одинаковой яркостью. В принципе, неудивительно, ибо он стоит как полсотни «самых дешевых».
Решено, беру на все нужды (красная и синяя часть люстры, фары и днище) четыре RGB светодиода. У моих светодиодов общий катод, поэтому мне потребуется 3 (RGB)*4 (штуки) — 12 ножек для управления. Опять проблема: у микроконтроллера всего 12 (вообще 14, но на двух питание) ножек, одна из которых выделена исключительно «на вход» (на ней ресет висит) и одну мы под «музыку» заняли. А еще куда-то надо выключатель повесить и запас оставить.
Обычно в таких случаях поступают просто: берут и ставят какой-нибудь «расширитель портов». Самый частый вариант — сдвиговый регистр типа 74HC595. Но ставить вторую микросхему рядом — это перебор и полный перерасход бюджета. Если все проекты так делать, то на пирожки ничего не останется… А мы в studiovsemoe.com очень любим поесть :)
Поэтому пойду другим путем и сделаю все средствами контроллера. Раскидываю ножки (все на выход) вот так
И подключаю светодиоды вот так (как обычно, полная схема в архиве)
На схеме я «разбил» каждый светодиод на три отдельных, что бы был понятен принцип действия. Ну и резисторы почему-то воткнул на 10к, хотя в реальности они 200–300 Ом. В результате у нас получилась светодиодная матрица 4×3.
Внимание: такая схема годится только для 1–2–3 светодиодов. Если вы планируете подключить больше — воспользуйтесь специальным драйвером типа L298 — иначе ток через вывод микросхемы будет слишком большой.
Добавляем немного кода
LATCbits.LATC0 = 1; // RED LATCbits.LATC1 = 1; // GREEN LATCbits.LATC2 = 1; // BLUE LATCbits.LATC3 = 0; LATCbits.LATC4 = 0; LATCbits.LATC5 = 0; LATAbits.LATA5 = 0; Здесь мы установили ножки С0-С2 в единичку, а С3-С5 и А5 — в нолик. Если все правильно собрано, то у вас должны загореться все светодиоды «белым светом».
Что произошло? Как известно, ток течет от места с высоким потенциалом в место с низким. А у нас на одних ножках 1 (или +3В), а на других — 0. Вот и загорелись светодиодики. Если на тех и на тех ножках подать 0 или 1 — ничего гореть не будет. Кстати, отсюда вырисовывается и универсальность схемы — если у вас светодиоды не с общим катодом, как у меня, а с общим анодом, то в коде вам будет достаточно сменить 0 на 1 и все заработает.
В результате комбинируя состояние ножек С0-С2 мы можем выбирать цвет свечения, а другими выбираем какой светодиод светится из 4х, итого вместо первоначальных 12 ножек обошлись 7ю. Не очень круто (со сдвиговым регистром можно обойтись 2 мя), но для наших целей более чем достаточно.
Но у нас как минимум два светодиода должны будут гореть одновременно и разными цветами (например, люстра и фара, фара и днище). Здесь я использую старый как мир трюк: буду быстро-быстро переключаться между светодиодами и включать нужные цвета. Если делать достаточно быстро, то глаз не заметит мерцания.
Пробуем помигать цветами (думаю, смысл можно понять из обозначений)
LATCbits.LATC3 = 0; LATCbits.LATC4 = 0; LATCbits.LATC5 = 0; LATAbits.LATA5 = 0; while (1) { LATCbits.LATC0 = 1; LATCbits.LATC1 = 0; LATCbits.LATC2 = 0; __delay_ms (50); LATCbits.LATC0 = 0; LATCbits.LATC1 = 1; LATCbits.LATC2 = 0; __delay_ms (50); LATCbits.LATC0 = 0; LATCbits.LATC1 = 0; LATCbits.LATC2 = 1; __delay_ms (50); } И смотрим, как красиво мигают светодиодики, перебирая цвета…
В принципе, можно переписать для работы напрямую с портом — так проще.
while (1) { LATC=0b00000001; __delay_ms (50); LATC=0b00000010; __delay_ms (50); LATC=0b00000100; __delay_ms (50); } В чем разница? Разница в том. что первый вариант занимает (со всей обвязкой) 89 байт, а второй 81. 8 байт разницы на ровном месте. Ну и второй работает быстрее:)
Если вам не понятно, то конструкция
LATC=0b00000001; Полностью аналогична
LATCbits.LATC0 = 1; LATCbits.LATC1 = 0; LATCbits.LATC2 = 0; LATCbits.LATC3 = 0; LATCbits.LATC4 = 0; LATCbits.LATC5 = 0; LATCbits.LATC6 = 0; LATCbits.LATC7 = 0; Где минусы? Минусы в том, что на некоторых контроллерах старшие биты могут быть отданы под что-нибудь полезное, а мы туда пихаем всякое и лезем грязными руками…
Где еще минус? Еще минус в том, что нам опять приходится заниматься «дрыгоножеством». Ну недостойное это дело для таких талантов, как мы. Нехай железка этим занимается.
У нас есть таймер, который отвечает за звук. Нет, трогать его не будем, но потрогаем его соседей. Для разнообразия возьмем TIM1
В чем его отличие от TIM2? Во-первых, более большая точность — если у TIM2 диапазон разбит на 255 частей, то тут — на 65535. Значит можно точнее попадать в частоту. Во-вторых, он ни к чему не привязан и ничего к нему не привязано. И наконец, его выход можно вывести на ножку микроконтроллера. Нам почти ничего из этого не надо, поэтому просто разрешим ему прерывания и в обработчике прерывания будем мигать светодиодиками.
В общем, настраиваем его вот так
Тут главное обратить внимание на включение прерываний и вызов каждое прерывание функции (самая нижняя строчка).
Теперь в файле tmr1.c в самом конце появилась функция, которая будет вызываться каждые 64 микросекунды (Кстати, благодаря широким возможностям (обратите внимание на поле Reload Value) мы можем сменить это значение до «раз в 4 секунды»).
void TMR1_ISR (void) {
// Clear the TMR1 interrupt flag PIR1bits.TMR1IF = 0;
TMR1H = (timer1ReloadVal >> 8); TMR1L = timer1ReloadVal;
// Add your TMR1 interrupt custom code } Так как мы любим расширяемый и конфигурируемый код, добавляем в проект два файла, отвечающие за работу светодиодов
led.h:
// basic colors #define RED 1 #define GREEN 2 #define BLUE 4 #define OFF 0
// set led N to color C void setLed (uint8_t n, uint8_t c); // get color of led N uint8_t getLed (uint8_t n);
extern volatile uint8_t lc[4];
// How LED connected #define _LED1_ LATCbits.LATC3 #define _LED2_ LATCbits.LATC4 #define _LED3_ LATCbits.LATC5 #define _LED4_ LATAbits.LATA5
#define _LEDR_ LATCbits.LATC0 #define _LEDG_ LATCbits.LATC1 #define _LEDB_ LATCbits.LATC2 Затем в led.c добавим «работу» с цветом
volatile uint8_t lc[4]; // current led colors 0 — off void setLed (uint8_t n, uint8_t c) { lc[n]=c; }
uint8_t getLed (uint8_t n) { return lc[n]; } И в код обработчика прерывания
switch (current_led) { case 0: _LED1_=0; _LED2_=1; _LED3_=1; _LED4_=1; break; case 1: _LED1_=1; _LED2_=0; _LED3_=1; _LED4_=1; break; case 2: _LED1_=1; _LED2_=1; _LED3_=0; _LED4_=1; break; case 3: _LED1_=1; _LED2_=1; _LED3_=1; _LED4_=0; break; } if ((lc[current_led]&RED)==RED) _LEDR_=1; else _LEDR_=0; if ((lc[current_led]&GREEN)==GREEN) _LEDG_=1; else _LEDG_=0; if ((lc[current_led]&BLUE)==BLUE) _LEDB_=1; else _LEDB_=0;
current_led++; if (current_led>3) current_led=0; Наконец, ничего не остается делать, как написать проверочный код.
setLed (0, RED); setLed (1, GREEN); setLed (2, BLUE); setLed (3, RED+GREEN+BLUE); __delay_ms (50); setLed (1, RED); setLed (2, GREEN); setLed (3, BLUE); setLed (0, RED+GREEN+BLUE); __delay_ms (50); setLed (2, RED); setLed (3, GREEN); setLed (0, BLUE); setLed (1, RED+GREEN+BLUE); __delay_ms (50); setLed (3, RED); setLed (0, GREEN); setLed (1, BLUE); setLed (2, RED+GREEN+BLUE); __delay_ms (50); Компилируем, запускаем и получаем полный облом. Ничего не мигает и не переливается. Почему? Я избавлю вас от размышлений и поисков. Потому что мы поставили генерировать прерывание каждые 64 микросекунды. А тактовую частоту процессору поставили 62 килогерца. То есть 1 такт у нас занимает 16 микросекунд. Кто-нибудь верит, что процессор способен за 4 такта исполнить все то нагромождение команд, нарисованное выше? Вот и я не верю. В итоге микроконтроллер навсегда зависает в обработчике прерывания таймера.
Путем логических размышлений (на самом деле банальным подбором) выясняем, что одно прерывание обрабатывается порядка 3х миллисекунд (или FFC0 в счетчике таймера).
Что видно? Видно меняющие цвета светодиоды, которые мерцают. И «чистого белого» не видно. В чем причина? Причина во времени, вернее в частоте. Как у нас сейчас работают светодиоды? 3 мс горим — 9 мс не горим. ШИМ частотой 80 герц с заполнением 25%. По опыту я знаю, что глаз перестает замечать шим где-то от 200 герц.
Что делать? Самый простой выход — поднять частоту. Рано или поздно глаз перестанет замечать мерцание. Что бы у нас не «уплыли» частоты звука, смотрим на возможные значения предварительного делителя у TIM2 — 1:1, 1:4, 1:16 и 1:64. У нас сейчас частота 62КГц, значит мы можем поставить 62×4=250КГц, 62×16=1МГц или 62×64=4МГц.
Лучшее — враг хорошего, поэтому выставляю частоту микроконтроллера в 1МГц, меняю делитель на 1:16 и снова заливаю прошивку. Ура! Мерцание исчезло.
Правда, цвета какие-то… не очень. Нет, это не глюки айфона, которым я сделал фотографию. Это на самом деле только красный похож на красный (на самом деле там должно быть белый-красный-синий-зеленый). И переключение какое-то вялое, совершенно не похожее на требуемые 50 мс.
В чем проблема? Опять нам подставила подножку злодейка физика: за 180 микросекунд (3 мс/16) светодиод не успевает «разгореться». В итоге получается то, что видно на фотографии.
Что можно сделать? Ну опять же напрашивается решение «в лоб» — повтыкать задержек после зажигания светодиода, что бы он успел загореться. В принципе решение хорошее. Но лучше сначала дать по башке программисту.
Зачем? Давайте проведем мысленный эксперимент и добавим еще 2 светодиода. Если у нас раньше ШИМ был с заполнением в 25%, то теперь станет в 16%. Добавление еще четырех снизит до 10%. Это как же надо будет задирать частоту и как сексоваться с задержками, что бы полученная конструкция заработала?
Поэтому еще раз смотрим на алгоритм. Он сейчас выглядит так
— включаем текущий светодиод и выключаем остальные— у текущего светодиода есть что-то в красном канале? если да, то включаем красный канал, иначе выключаем— у текущего светодиода есть что-то в зеленом канале? если да, то включаем зеленый канал, иначе выключаем— у текущего светодиода есть что-то в синем канале? если да, то включаем синий канал, иначе выключаем
А теперь мы собираемся еще задержек для каждого светодиода напихать… опять получим мерцание, опять частоту поднимать. Полная профнепригодность и рыдание в темном уголке ждут нас, если не придумаем выход :)
Так как рыдать я не хочу, меняю алгоритм на следующий
— Включаем канал Н (красный, зеленый, синий)— В светодиоде М (0,1,2,3,…) должен гореть текущий канал Н? Если да, то включаем. Если нет, то выключаем.— Повторяем цикл для М— Подождем, что бы последний включенный светодиод загорелся.— Повторяем цикл для Н
Вроде разница не большая, но почувствуйте разницу: при любом количестве светодиодов ШИМ для них будет с заполнением в 33%. Проверяем.
Код для одного канала (остальные полностью аналогичные). Определения тоже можно посмотреть в полной версии, но думаю по названиям будет понятно.
_LEDR_=_LEDON_; if ((lc[0] & RED) == RED) _LED1_ = _LEDOFF_; // INVERTED! else _LED1_ = _LEDON_;
if ((lc[1] & RED) == RED) _LED2_ = _LEDOFF_; // INVERTED! else _LED2_ = _LEDON_;
if ((lc[2] & RED) == RED) _LED3_ = _LEDOFF_; // INVERTED! else _LED3_ = _LEDON_;
if ((lc[3] & RED) == RED) _LED4_ = _LEDOFF_; // INVERTED! else _LED4_ = _LEDON_; for (leds=0; leds<80;leds++) current_led++; _LEDR_=_LEDOFF_; И любуемся.
На фотографии это видно не так хорошо, но в реальности зеленый стал зеленым, а синий — синим.
Ну теперь меняем на «почти как в финале».
setLed (3, RED+GREEN+BLUE); setLed (2, GREEN);
setLed (0, RED); __delay_ms (100); setLed (0, OFF); setLed (1, BLUE); __delay_ms (100); setLed (1, OFF); Люстра перемигивается! Фара светит! А днище все такое зеленое! В общем, полный и безоговорочный успех в наших начинаниях. Мы круты! Что там еще требуется для мотивации? :) Получить немного тишины :) Предварительный показ результатов заказчику получил полное одобрение и выделение дополнительных объемов ресурсов на работы («папа, я еще два вечера буду себя вести очень тихо» и прочий родительский шантаж)
Под это дело бонус: почти стандарт мигания NYPD
uint8_t c; setLed (3, RED + GREEN + BLUE); setLed (2, GREEN); while (1) { setLed (0, RED); setLed (1, OFF); __delay_ms (40); for (c = 0; c < 10; c++) { setLed(0, OFF); __delay_ms(10); setLed(0, RED); __delay_ms(10); }
setLed (1, BLUE); setLed (0, OFF); __delay_ms (40); for (c = 0; c < 10; c++) { setLed(1, OFF); __delay_ms(10); setLed(1, BLUE); __delay_ms(10); } } Наиболее вдумчивым вопрос для разборок: почему __delay_ms(_X_) в данном случае делает паузу раз в 5 больше положенных _X_ мс?
Но и это еще не все. Дальше рассмотрим самый главный вопрос: как выключать это дело?
Как обычно, полный комплект всего лежит тут multik.org/pic/policelight.rar