Pull to refresh

PIC16F1503. Тачка на прокачку — 2. Свет

Reading time10 min
Views51K
Раньше было про звук.

Прошлый пост я оставил незаконченным. Если вы помните, то мне никак не удавалось подобрать «то самое звучание». Попытки подобрать «циферки по наитию» получались куда хуже обычного «пиу-пиу»… С одной стороны все равно — от китайской пищалки звука не добиться, а с другой стороны — «нечистая работа, низкий класс». Опять же, загонять тактовую частоту на 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
Tags:
Hubs:
+31
Comments41

Articles

Change theme settings