31.12.2008

Семисегментный индикатор для Arduino (4)

Часть IV

(окончание, см. Часть III, Часть II и Часть I)


Итак, время брать в руки паяльник. Используем стандартную макетную плату для пайки с шагом 2,54мм (шаг важен, поскольку надо установить вилку PLS, чтобы попасть ей в розетку PBS на Arduino - а там именно такое расстояние между контактами). Выбираем размер так, чтобы она не закрывала второй ряд колодок с цифровыми pin-ами, например, у меня получился размер 96 x 52 мм.

Я использовал двустороннюю макетку без дорожек, но зато с металлизацией всех отверстий. Вот что получилось в итоге:




Чтобы не паять лишний разъем ICSP для ATMEGA8, я поставил панельку. Кнопка сброса - чисто декоративная, и на самом деле, ее можно и не ставить вовсе. Длинная гребенка голубого цвета - это резисторная сборка (6 резисторов по 330 Ом). Вещь удобная, но, будучи поставлено вертикально, постоянно гнется при неосторожном обращении с бескорпусным изделием. Зато - опять-таки - экономия места.

Паяем проводники, глядя в принципиальную схему. Поскольку LED-ы индикаторов соединены в шину, напрашивается тривиальное решение по расположению:



Конденсатор по питанию довольно компактно влез между вилкой PLS и панелькой:



Если вы попробуете сразу запустить плату после пайки - у вас ничего не выйдет. Причина - отсутствие кварца, на который рассчитывает ATMEGA. Если вы уже отлаживались с ним в составе Arduino, программатор вместе с bootloader-ом прошил весьма определенные FUSE-биты, в том числе отвечающие за выбор схемы тактирования: внешний кварц на 16 МГц. У нас используется внутренний на 8 МГц, поэтому FUSE-биты надо изменить.

Поместите ATMEGA8 в устройство с ICSP-гребенкой (тот же Arduino), подключитесь к ней с помощью программатора и поменяйте fuses следующим образом (подробное описание битов - в документации):

Fuse High Byte: SPIEN=0, CKOPT = 1, BOOTSZ = 01, BOOTRST = 0;
Fuse Low Byte: SUT = 01, CKSEL = 0100;

Остальные биты стоит оставить незапрограммированными, получаем шестнадцатиричные значения: hfuse = 0xda, lfuse = 0xd4.

Например, если вы используете Parallel Programmer и Arduino IDE, надо запустить avrdude со следующими параметрами:

avrdude -p m8 -c dapa -t

где m8 - это тип контроллера, ATMEGA8; dapa - это тип программатора, а -t - войти в терминальный режим. Находясь в оном (avrdude> - это приглашение), набираем:

avrdude> write hfuse 0 0xda
avrdude> write lfuse 0 0xd4
avrdude> q

Вот теперь - точно должно заработать! Но будьте терпеливы: Arduino bootloader понятия не имеет, что теперь его тактовая частота понизилась в 2 раза. Его алгоритм предполагает ожидание около 10 секунд после старта (мало ли - думает он - может новый sketch сейчас зальют?!). Теперь же, после подачи питания, bootloader будет ждать 20 секунд. Дальше на экране появится три минуса, затем будет отображаться то, что передается по TWI.

Если ничего не отобразилось, то надо смотреть, правильно ли собрана схема. Например, я умудрился инвертировать сброс, в итоге он работал только при нажатой кнопке ;)

Последние штрихи: вернемся к fuse-битам. Наверное, стоит сделать так, чтобы bootloader не запускался вовсе, а сразу стартовал наш sketch. Это довольно просто: достаточно сообщить об этом ATMEGA через hfuse: установить флаг BOOTRST в 1. Проделываем всё тоже самое, что и выше, только вместо 0xda пишем 0xdb.

Конечно же, первым делом я соорудил градусник. Датчик подключается к цифровым pin-ам, которые не перекрыты макетной платой:






Подведем итоги.
  1. Контроллером дисплея служит ATMEGA8, как и хотелось;
  2. Программа написана на Wiring-е, отлажена в Arduino, легко менятеся, расширяется по числу индикаторов, а также типу (переделывается под общий анод).
  3. Число деталей минимизировано - кроме контроллера восемь резисторов 330 Ом и один - 10К, а также сами индикаторы.
  4. Интерфейс взаимодействия по TWI (I2C) получился относительно простым, хотя и пришлось в итоге поставить задержку 100 мс между посылками информационных блоков. Для человеческого глаза - вполне приемлемо.
Чего хотелось бы еще?
  1. Неплохо было бы сделать печатную плату, повысив компактность;
  2. Оформить ввод-вывод в виде библоитеки и подключать ее, чтобы не писать каждый раз одно и то же;
  3. Сделать полноценный алфавитно-цифровой дисплей.

23.12.2008

Семисегментный индикатор для Arduino (3)

Часть III

(продолжение, см. Часть II и Часть I)


Очень удобно экспериментировать с макеткой без пайки, например:



Воткните в нее LED-ы и нагрузочные резисторы, затем соедините проводами по принципиальной схеме из второй части. Подключите все это к Arduino, на котором будет моделироваться программа отображения через 11 проводов: 8 для шины, 3 для общих катодов. Получится некий колтун из проводов (для простоты надо брать кроссировку - одним концом в плату, другим - в колодку на Arduino).

Готово, самое время написать sketch для ATMEGA8, который будет у нас исполнять роль последовательного приемника, защелки, знакогенератора, декодера и отображателя ;)

Для начала надо определить массив знакогенератора. Будем хранить в нем карту отображения сегментов a,b,c,d,e,f,g побитно (я выбрал старший бит с весом 0x80 для сегмента a, 0x40 для сегмента b и так далее):

byte d_map[MAX_SYMBOLS];

Также потребуется защелка - для хранения кодов символов, которые мы отображаем в настоящий момент:

byte latch[SEG_NUM];

Чтобы ввести немного универсальности, я сделал промежуточные массивы, в которых задаются pin-ы Arduino, к которым подключены сегменты шины:

byte l_pin[8];

(восемь - так как есть еще сегмент DP - десятичная точка). Ну и тогда уж пины общих катодов каждого индикатора:

byte s_pin[SEG_NUM];

Теперь надо выбрать способ отображения. На ум приходит самый тривиальный - как в калькуляторе. Принятый по TWI-шине код символа помещается в самый младший разряд (справа), остальные сдвигаются влево, самый старший навсегда покидает индикатор. Но что же делать с точкой? В описанную схему она никак не монтируется, введение точки в код символа а) удваивает знакогенератор (с точкой и без точки - отдельные символы) б) делает менее интуитивным кодирование и передачу символов из "старшего" Arduino.

Ну и ладно, давайте сделаем специальный код символа, который не надо хранить в знакогенераторе - код точек. При его получении ничего никуда не сдвигается, а просто определяется, на каких местах зажигаются точки (да хоть на всех). Будем хранить точки в отдельном массиве:

boolean point[SEG_NUM];

Массив обновляется только при поступлении кода символа с включенным старшим битом (0x80), после чего остальные биты трактуются как включение или выключение точки на соответствующем знакоместе и переписываются в массив point.

Самое время определиться с символами, которые мы собрались загнать в знакогенератор (содержимое массива d_map и значение константы MAX_SYMBOLS). Для простоты (а чего экономить-то?) определяем каждому символу 8 бит (старший занят на признак позиций точек, см. выше, так что максимум 128 символов). У меня хватило фантазии на такой набор:





Последним угадывается истинно пелевинский символ-пустота.

Самое время - показать программу по мотивам вышеизложенного:

#define CPU_FREQ 8000000L
#include <Wire.h>

#define ADDR 0x69 // I2C device address (change if need, note: 7-bit)
#define SEG_NUM 3 // total digits in a display
#define delayms 1 // time for displaying one digit, ms

byte latch[SEG_NUM]; // защелка состояний
boolean point[SEG_NUM]; // десятичные точки
byte s_pin[SEG_NUM]; // пины общих катодов
byte l_pin[8]; // мап пинов на сегменты знакоместа
byte d_map[24]; // память знакогенератора

void receive_handler(int numbytes) {
for (unsigned int i=0;i<numbytes;i++) {
byte r = Wire.receive();
if (r<0x80) {
// ordinary symbol
for (byte k=(SEG_NUM-1);k!=0;k--) latch[k] = latch[k-1];
latch[0] = r;
} else {
// DP code
for (byte k=0;k<SEG_NUM;k++) point[k] = (r >> k) & 0x1;
}
}
}

void setup() {
s_pin[0] = 4; // 2;
s_pin[1] = 12; //12;
s_pin[2] = 2; // 4;

l_pin[0] = 5;
l_pin[1] = 6;
l_pin[2] = 7;
l_pin[3] = 8;
l_pin[4] = 9;
l_pin[5] = 10;
l_pin[6] = 11;
l_pin[7] = 3;

for (byte i=0;i<SEG_NUM;i++) {
point[i] = false;
pinMode(s_pin[i],OUTPUT);
digitalWrite(s_pin[i],LOW);
}
for (byte i=0;i<8;i++) pinMode(l_pin[i],OUTPUT);

//
d_map[0] = 0b00000011; // 0
d_map[1] = 0b10011111; // 1
d_map[2] = 0b00100101; // 2
d_map[3] = 0b00001101; // 3
d_map[4] = 0b10011001; // 4
d_map[5] = 0b01001001; // 5
d_map[6] = 0b01000001; // 6
d_map[7] = 0b00011111; // 7
d_map[8] = 0b00000001; // 8
d_map[9] = 0b00001001; // 9
d_map[10] = 0b00010001; // A
d_map[11] = 0b11000001; // b
d_map[12] = 0b01100011; // C
d_map[13] = 0b10000101; // d
d_map[14] = 0b01100001; // E
d_map[15] = 0b01110001; // f
d_map[16] = 0b10010001; // H
d_map[17] = 0b11110111; // i
d_map[18] = 0b11100011; // L
d_map[19] = 0b00110001; // P
d_map[20] = 0b10000011; // U
d_map[21] = 0b00111001; // градус
d_map[22] = 0b11111101; // минус
d_map[23] = 0xff; // пустой

// initial display state - empty
latch[0] = 22;
latch[1] = 22;
latch[2] = 22;

// start wiring library
Wire.begin(ADDR);
Wire.onReceive(receive_handler);
}

//
// Set LED bus in order to display symbol 'd'
//

void set_digit(byte d) {
byte i=0;
for (byte mask=0x80;mask!=1;mask>>=1)
digitalWrite(l_pin[i++],(d_map[d]&mask) ? HIGH : LOW );
}

void indicate() {
for (byte k=0;k<SEG_NUM;k++) {
digitalWrite(s_pin[k],HIGH);
set_digit(latch[k]);
digitalWrite(l_pin[7],point[k] ? LOW : HIGH); // DP segment
delay(delayms);
digitalWrite(s_pin[k],LOW);
}
}

void loop() {
indicate();
}


Теперь рассмотрим программу на передающем "конце". Эксперименты с TWI показывают, что для стабильной работы надо снабдить окончание передачи небольшими задержками. Соответственно, получаем две функции:

void od(byte d) {
Wire.beginTransmission(ADDR);
Wire.send(d);
Wire.endTransmission();
delayMicroseconds(100);
}

void od3(byte d1, byte d2, byte d3) {
Wire.beginTransmission(ADDR);
Wire.send(d1);
Wire.send(d2);
Wire.send(d3);
Wire.endTransmission();
delayMicroseconds(100);
}


Первая выводит один символ, вторая - сразу три, заполняя весь наш дисплей за один прием.
Теперь макетирование окончено, и можно брать в руки паяльник...

(окончание см. в Части IV)

21.12.2008

Семисегментный индикатор для Arduino (2)

Часть II

(продолжение, см. начало в Части I)

Первым делом, нарисуем схему. TWI работает на аналоговых пинах A4, A5 - именно там разместили аппаратную поддержку авторы ATMEGA8 (это 27 и 28 выводы микроконтроллера в корпусе PDIP).

Что внутри? Три семисегментных LED-знакоместа, у которых pin-ы светодиодов соединены между собой в шину на 8 проводов (не забудем про десятичную точку).

Надо иметь ввиду, что семисегментные индикаторы бывают двух типов - там, где у LED-ов один общий анод и, наоборот, где один общий катод. Я для своих экспериментов выбрал индикаторы Agilent HDSP-5501 и HDSP-5521 с общим катодом, оставляю желающим возможность доработать микропрограмму для общего анода - ничего сложного, а в качестве тренировки очень даже полезно.

Так вот, общие катоды (аноды) сегментов надо повесить отдельно, поскольку в процессе отображения потребуется поочередно гасить все индикаторы кроме одного, выставляя при этом на общую шину код символа.

Микроконтроллеру нужно питание, сброс, тактирование, микропрограмма.

Кварц стоит недорого, но зачем? Экономим место и деньги - используем внутренний
неточный откалиброванный, для частоты 8МГц. Точность его не скажется на работе TWI, поскольку тактовые импульсы генерирует master, а мы будем работать в режиме slave.

Питание - стандартно, не забываем про конденсатор 0,1 мкФ между Vcc и GND. Схема сброса - тоже стандартная, притянуть к Vcc через R=10К. Получается элементарная схема (прошу прощения за качество, это мой первый опыт с Eagle, кликабельно для увеличения):


Микропрограмму напишем при помощи Arduino, так быстрее, хоть и менее эффективно с точки зрения энергопотребления и объема программы. Главное - чтобы заработало, а потом при желании энтузиасты улучшат, не правда ли?

Вернемся к схеме. Надо прикинуть, как бы не сжечь оконечные каскады pin-ов ATMEGA прожорливыми светодиодами. Поскольку знакоместа зажигаются по-очереди, то в один момент времени больше 1-го светодиода на шине не будет: это нормально. Но вот общие катоды (или аноды) могут за один раз собрать ток от 8 светодиодов, и это уже становится опасным.

Токоограничительные резисторы надо либо рассчитать по закону Ома (зная сопротивление источника питания, LED-ов, схему каскадов на I/O pin-ах ATMEGA и максимальный ток - 40mA), либо практическим путем - амперметром, последовательно в цепь. Например, у меня при подключении нагрузочного R=1К последовательно в цепь каждого из 8-ми выводов, ток на одном светодиоде составлял 2 mA, следовательно максимально - 16mA. В итоге, я остановился на 330 Ом, но видел схемы и со 100 Ом. Можно подстраховаться: подавать групповой уровень через транзистор. Но зачем нам лишние элементы на плате? Стоит заморачиваться только если предельное значение нагрузочного резистора не соответствует желаемой яркости свечения (например, надо рассматривать индикатор в солнечную погоду с пяти метров).

Теперь надо написать программу. Конечно, удобнее всего иметь под рукой два Arduino (часто один из них заменяет набор ArduinoMinimum, например такой). Соединяем их a4, a5, GND и снабжаем программами:

Приемник (slave):

#include <Wire.h>
#define ADDR 0x69

void receive_handler(int numbytes) {
  for (int i=0;i<numbytes;i++) {
    Serial.print(Wire.receive());
  }
}

void setup() {
  Serial.begin(38400);
  Wire.begin(ADDR);
  Wire.onReceive(receive_handler);
}

void loop() {}

Передатчик (master):

#include <Wire.h>
#define ADDR  0x69

void setup() {
  Wire.begin();
}

void loop() {
  Wire.beginTransmission(ADDR);
  Wire.send("Hello, world!");
  Wire.endTransmission();
  delayMicroseconds(1000);
}

После запуска обоих sketch-ей, подключитесь к serial-соединению на приемнике. Если все правильно, то раз в секунду будет приезжать "Hello, world!". 

Если нет - проверьте, выставили ли вы порт на скорость 38400, правильно ли соединены провода. Адрес 0x69 я выбрал произвольно, не исключено, что он пересекается скаким-либо существующим устройством. В любом случае, его всегда можно поменять. 

Заработало? Можно переходить на следующий уровень - сборка на макетной плате и написанию полноценной микропрограммы отображения для приемника.

(см. продолжение в Части III)

14.12.2008

Семисегментный индикатор для Arduino

Часть I

Цифровой индикатор может пригодиться во многих приложениях, особенно связанных с робототехникой. Библиотека Serial позволяет Arduino передавать и принимать данные через последовательный порт, но носить с собой компьютер просто для того, чтобы иметь возможность посмотреть пару цифр - мягко говоря, неудобно.

Допустим, у вас под рукой случайно оказались семисегментные светодиодные дисплеи - почему бы их не пустить в дело? Знатоки неодобрительно покачают головой: довольно расточительно тратить целых 8 пинов микроконтроллера (семь сегментов и одна десятичная точка) на такое удовольствие.



Но что такое индикатор на один разряд? Смех, да и только: даже температуру не показать. Поэтому, хочется хотя бы три:



Вырисовывается довольно прозрачная задача: разработка компактной Shield-платы со светодиодными индикаторами. Однако, прежде чем перейти к фантазированию проектированию, поставим себе ограничения:
  1. Контроллер Arduino должен быть разгружен от тупой работы по поддержанию информации на дисплее => один раз вывел и дальше занимается своими делами, а данные удерживаются до обновления в "защелке" (latch) на Shield-е.
  2. Использовать минимальное количество корпусов микросхем на Shield-е.
  3. Использовать минимальное число ножек для взаимодействия Arduino с Shield, при минимальных издержках. Например, неплохой вариант с последовательным интерфейсом (SPI, TWI).
  4. По-возможности, минимизировать стоимость Shield-а.
Есть масса микросхем-дешифраторов, уже заточенная под индикацию на семисегментном дисплее: те же 541ИД1, ИД2. У них параллельный ввод в BCD-коде - это 4 бита, да еще по биту на каждый разряд, итого 7.



Многовато, да и от циклического сканирования не избавляет, а ставить регистр-защелку - лишний корпус. Зато дешево стоят - 4-5 руб. за штуку.

Наиболее полно нашим условиям удовлетворяют микросхемы MAX7219 и MAX7221: DIP24, порт SPI, защелка присутствует. Однако в розницу они стоят порядка 150 и 250 рублей соответственно, и дешевого решения уже не получается.

А что, если?

Использовать для этих целей ATMEGA8? Связать с контроллером по TWI (Two Wire Interface, полностью совместимый с шиной I2C) - при использовании библиотеки Wire это не представляет труда, к тому же можно расширить диапазон выводимых символов (не только цифры, но и какие-то буквы и прочие символы).

Плюсы:
  • стоит минимум в 2 раза дешевле MAX7219;
  • температурный диапазон шире MAX7221 (а это уже в 4 раза дешевле, можно использовать при температурах ниже нуля, например в непрогретом авто ;)
  • использование I2C дает шанс сделать полноценную Shield, потому что не надо использовать колодку с цифровыми пинами 9-12 - следовательно подойдет макетка со стандартным шагом 2,54 мм!
Минусы:
  • ног больше на 4
  • придется повозиться с программированием ;)

Если жалко времени - однозначно MAX, или вообще готовый последовательный дисплей купить. Но мы же - "бедные студенты" ;) времени - много, денег лишних платить не хочется, а уж программирование мы как-нибудь одолеем!

(продолжение следует - Часть II)