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

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.