Стандартным алгоритмом работы с новой железкой на этом форуме считается «скачать библиотеку», а если не подошла, спрашивать «какую бы скачать получше». Хотя, сплошь и рядом библиотеки дают слишком общее решение, а для конкретного проекта гораздо лучше написать простой код самому.
Вот такой пример сегодня и разберём.
Понадобилось тут использовать нестандартный семисегментник, который мне очень понравился. Задача по сути сводилась к тому, чтобы включать/выключать 14 светодиодов. Использовать два 8-битовых сдвиговых регистра, да к ним ещё кучу токоограничивающих резисторов – как-то «много всего». Решил попробовать замечательную микросхему TLC5940. В даташите от TI её называют 16-канальным LED-драйвером. В других местах можно встретить название «16-канальный ШИМ-контроллер». Правильно и то, и другое, т.к. она действительно и то, и другое. Так вот, это важно для дальнейшего, мне нужен был именно LED драйвер, т.е. возможность включать светодиоды с контролем тока. ШИМ как таковой мне нужен не был.
В чём проблема с библиотекой для TLC5940?
Взял библиотеку, которой пользуются во всех примерах работы с этой микросхемой и слегка прифигел. Не, библиотека вовсе неплохая, но она занимает ДВА таймера! Екарный бабай, а как же всё остальное? Ну, ладно ещё ATmega328 – у неё хотя бы три таймера имеются, но мне нужно было ещё и на ATtiny45 эту микросхему использовать, а там таймеров всего два. Получается, займу оба и вообще никакой временной интервал без танцев с бубнами измерить не смогу. Это было совершенно неприемлемо.
Стал изучать микросхему. Быстро понял, почему используются два таймера. Действительно, использовать эту микросхему на полную катушку без двух таймеров (или без внешнего осциллятора) не получится. Дело в том, что она тактируется внешним сигналом, а на ноги выдаёт 12-тиразрядный ШИМ.
Выглядит это так:
-
В память микросхемы посредством SPI закачивается 16 (по одному на ШИМ-канал) 12-тиразрядных числа;
-
Защёлкивается «защёлка»
-
Подаётся сигнал «начало нового периода»
-
При первом после этого восходящем фронте тактирующего сигнала, все каналы, для которых закачан не 0 включаются в HIGH и из их чисел вычитается 1;
-
При каждом последующем восходящем фронте тактирующего сигнала, все каналы для которых число стало нулём, переводятся в LOW, а из чисел всех остальных каналов вычитается 1;
-
И так 4096 раз. После этого всё затухает, если не придёт новый сигнал «начало нового периода». Если же он придёт, то, веселье продолжается с п.4
Таким образом, каждые 4096 тактов необходимо подать сигнал «начало нового периода». Именно этот сигнал и подаёт второй таймер. Период этого сигнала и есть период ШИМа на ногах TLC5940.
И что делать?
Использовать вместо второго таймера WatchDog? Можно, но минимальный период WatchDog – 16мс, а значит, частота ШИМа на ногах будет всего лишь 1000/16 = 62,5Гц – негусто.
Генерировать сигнал «начала нового периода» программно тоже не вариант. Например, для частоты ШИМа на ногах хотя бы в 100Гц, тактирующий сигнал должен иметь частоту 409600 герц, а нам надо на каждом такте прибавить единичку к счётчику, сравните его с 4096 и, если равно, подать сигнал начала нового периода. На всё про всё у нас при этом 39 тактов процессора (это при 16МГц, а если, не дай Бог, на 8-ми работаем?). За 39 тактов, конечно можно всё это сделать, но процессор будет занят этим более, чем на треть – не жирно ли? А когда он будет что-то другое делать?
Можно попробовать уменьшить разрешение ШИМ (скажем до 8 бит). Это несложно. Никто же не обязывает нас посылать сигнал «начало нового периода» именно через 4096 тактов. Можно делать это каждые 256 тактов – кто не даёт? Тогда уже можно программно всё организовать при разумной загрузке процессора.
Но тут появилась идея пойти дальше. А почему бы не уменьшить разрядность ШИМ до 1? Сделать ей фиксированную сважность равную 1, т.е. просто выбросить ШИМ, а использовать постоянный сигнал? Мне ведь вовсе не нужен ШИМ! Мне надо включать светодиоды и не более того. Так нафига мне таймеры вообще? Хоть тактирующий, хоть «начала периода». Итак, переходим к основной теме:
Используем TLC5940 вовсе без таймеров
Это оказалось проще, чем могло бы показаться. Итак, вспоминаем порядок действий, описанный выше, и делаем вывод: если просто подать сигнал «начало нового периода», а после этого дать только один единственный тактирующий сигнал, то на его восходящем фронте зажгутся все светодиоды, для которых закачанное число не 0. А больше никаких тактирующих сигналов не подавать! Что будет со светодиодами? Правильно, будут продолжать гореть, куда они денутся?
Когда же нам захочется поменять конфигурацию включённых/выключенных светодиодов, мы спокойно, перезакачаем числа и «начнём новый период» так же как начинали прошлый, и опять не будем гнать туда тактирующие сигналы.
Таким образом, мы превратили 16-канальный ШИМ-контроллер в 16-канальный ключ! А для нашей задачи больше ничего и не нужно.
Ниже приведён простой код – бегающий огонёк. Сначала загорается светодиод на ноге OUT0, потом на OUT1 и т.д. до OUT15. После этого – назад – OUT14, OUT13 и так до OUT0, ну и до бесконечности.
Подключение микросхемы (название, пин (DIP), куда подключён). Пины SPI указаны для UNO-подобных Ардуин.
// BLANK 23 - на PIN_BLANK и притянуть к питанию через 10-20кОм
// DCPRG 19 - на Vcc
// GND 22 - на Gnd
// GSCLK 18 - на PIN_GSCLK
// IREF 20 - на землю через резистор 2кОм (для 20мА)
// SCLK 25 - на пин 13 (SCLK)
// SIN 26 - на пин 11 (MOSI)
// SOUT 17 - не подключён
// VCC 21 - на Vcc
// VPRG 27 - на Gnd
// XERR 16 - не подключен
// XLAT 24 - PIN_XLAT
// OUT0-15 1-15, 28 на катоды светодиодов
Код:
13 | void setPin(int8_t channel, bool val) { |
14 | if (val) tlcData |= ((uint16_t)1 << channel); |
15 | else tlcData &= ~((uint16_t)1 << channel); |
20 | SPI.beginTransaction(SPISettings(F_CPU/2, MSBFIRST, SPI_MODE0)); |
22 | for (int16_t triada = tlcData; n < 8; (triada <<= 2), n++) { |
23 | SPI.transfer(highByte(triada) & 0x80); |
25 | SPI.transfer(highByte(triada) & 0x40); |
28 | digitalWrite(PIN_GSCLK,LOW); |
29 | digitalWrite(PIN_BLANK, HIGH); |
30 | digitalWrite(PIN_XLAT, HIGH); |
31 | digitalWrite(PIN_BLANK, LOW); |
32 | digitalWrite(PIN_GSCLK,HIGH); |
33 | digitalWrite(PIN_XLAT, LOW); |
37 | inline void clearAll( void ) { tlcData = 0; } |
38 | inline void setAll( void ) { tlcData = UINT_MAX; } |
43 | pinMode(PIN_BLANK,OUTPUT); |
44 | pinMode(PIN_XLAT,OUTPUT); |
45 | pinMode(PIN_GSCLK,OUTPUT); |
46 | digitalWrite(PIN_BLANK, HIGH); |
47 | digitalWrite(PIN_XLAT, LOW); |
51 | for (int8_t channel = 0, dir = 1;; channel += dir) { |
53 | setPin(channel, HIGH); |
54 | if (channel == 0) dir = 1; |
55 | else if (channel == 15) dir = -1; |
Заключение:
Коллеги, хочу быть правильно понятым. Я здесь никоим образом не говорю, что библиотека плохая. Она неплохая, она реально использует микросхему как ШИМ-контроллер и старается выжать из неё всё.
Но в данном конкретном проекте это просто не нужно. В итоге, библиотека, которая ради ненужных мне возможностей отжирает у меня ДВА таймера оказалась совершенно неприменимой. Простенький код в десяток строк, который варварски использует ШИМ-контроллер в качестве переключателя, решил все проблемы проекта и не занял практически никаких ресурсов.
И такие вещи случаются очень часто. Имейте это в виду и всегда думайте, что тащите в свой проект. Не уподобляйтесь малышам, которые тянут в рот всё, до чего могут дотянуться.
жаль , что утром на сутки - некогда уже вникать.... хотя всё ПОЧТИ понятно :)
ЕвгенийП, писал: "Хотя, сплошь и рядом библиотеки дают слишком общее решение, а для конкретного проекта гораздо лучше написать простой код самому."
Вау! Вот и Вы доперли что Атмел сделал всё, чтобы программировать МК напрямую было проще самому. Ай, маладца. Прямо бальзам на душу. Пасибки.
Далее вникать не стал, ибо без надобности, думаю что там всё верно. Смотрел на этот девайс, возможно тоже прикуплю его для своей Нано.
Коллеги,
пост и код примера писался вчера поздно вечером, по горячим следам пробы на живой микросхеме, а сегодня, на свежую голову, я заметил в нём не то, чтобы ошибку, но идеологическое несоответствие, на которое хочу обратить Ваше внимание.
Вот смотрите, в функции loop() я использую бесконечный цикл.
01
void
loop
() {
02
for
(int8_t channel = 0, dir = 1;; channel += dir) {
03
clearAll();
04
setPin(channel, HIGH);
05
if
(channel == 0) dir = 1;
06
else
if
(channel == 15) dir = -1;
07
update();
08
delay(50);
09
}
10
}
Само по себе это не плохо, в коротких на коленке сделанных скетчах, написанных для проверки какой-нибудь идеи, я всегда так поступаю, но я не прав в том, что использовал такой код в примере для начинающих.
Дело в том, что использовать бесконечные циклы в сколько-нибудь отличном от нуля проекте в среде Ардуино – очень плохая идея. Это противоречит самой философии Ардуиновской среды, которая предполагает бесконечный цикл вызовов loop() в котором всё и делается.
Давайте, я сначала объясню, почему это плохая идея, а потом избавлюсь от цикла.
Итак, почему плохо использовать бесконечные циклы в среде Ардуино (ну, кроме того, что это просто ей идеологически противоречит)? Для ответа на этот вопрос, надо посмотреть как именно вызывается loop() из среды. Посмотрим на ардуиновскую. функцию main().
01
int
main(
void
) {
02
init();
03
initVariant();
04
#if defined(USBCON)
05
USBDevice.attach();
06
#endif
07
setup
();
08
for
(;;) {
09
loop
();
10
if
(serialEventRun) serialEventRun();
11
}
12
return
0;
13
}
Обратите внимание на строку 10. В цикле вызова loop() ардуиновская среда не только вызывает loop(), но и делает что-то ещё - нужное для неё. Сейчас не важно, что именно там делается, более того, в новой версии среды там может делаться что-то ещё. Важно только то, что при каждом вызове loop() среда хочет делать ещё что-то, что важно и нужно для её (среды) нормальной работы.
А мы своим бесконечным циклом прервали вызовы loop() и лишили среду возможности выполнять эти действия. Ведь при нашем подходе, единожды попав в loop() программа будет вечно крутиться в нём и никогда из него не выйдет!
Понятно почему это плохая идея? Раз уж пользуемся средой, так давайте дадим ей шанс нормально работать. А бесконечный цикл в loop() не особо то и нужен – мы можем просто воспользоваться тем фактом, что сам loop() вызывается в бесконечном цикле.
Примерно, вот так:
01
void
loop
() {
02
static
int8_t channel = 0, dir = 1;
03
clearAll();
04
setPin(channel, HIGH);
05
if
(channel == 0) dir = 1;
06
else
if
(channel == 15) dir = -1;
07
channel += dir;
08
update();
09
delay(50);
10
}
Вот, теперь другое дело.
Не смотрел код, но НЕ осуждаю. :)
Меня ваще мало интересует "идеология среды Ардуино ИДЕ" .. важно то, что Вы указали ранее: "гораздо проще написать код самому". Это шаг и большой к понимаю программирования МК "в целом" и особенно в том, что программировать МК фирмы Atmel действительно часто проще напрямую, чем с использованием "библиотек" и/или "среды Ардуино ИДЕ".
Это шаг и большой к понимаю программирования МК "в целом"
Я рад, что Вы регулярно отмечаете мой прогресс. В прошлой теме кто-то сказал, что это я запоем читаю код всем известной библиотеки arhat и именно поэтому прогрессирую. Я тогда подтвердил, что так оно и есть. Так, что, всё благодаря Вам!
Кстати, я тут скачал с гитхаба свежую версию (исключительно в качестве учебного пособия, разумеется) и посмотрел. Слушайте, исправьте Вы наконец школярскую ошибку в 26-ой строке файла tsc.h, а то ведь скоро уж все куры со смеху передохнут :)
Ок, посмотрю. Мы этим файлом сейчас не пользуемся ..
Не только малыши, но и вполне взрослые барышни имеют такую дурную привычку.
(Д. Ржевский, поручик Лейб-гвардии Гусарского полка)
Как то так...