Как схем, так и программ вольтметров для контроллеров Arduino в интернете можно найти великое множество. Однако рискну предложить вниманию читателей сайта еще один вариант, который, возможно, кому-нибудь и пригодится.
Схема собрана на Arduino UNO, с использованием двухстрочного алфавитно-цифрового дисплея MT-16S2H и монтажной панели. Программирование ведется в среде Arduino Software IDE.
Сразу следует оговориться, что качество контактов панелей для "быстрого" монтажа совершенно отвратительное. Если на чисто "цифровых" схемах это проходит, в общем, незаметно, то при сборке такой конструкции, как вольтметр, малейшее шевеление вставленных контактов или элементов может привести к изменению показаний из-за увеличения падения напряжения на линии питания, делителя или измерения. Поэтому если предполагается использовать данную конструкцию (в усовершенствованном, конечно, виде) в домашней лаборатории, то монтировать элементы делителей, питания и проч., следует только пайкой на нормальной печатной плате с использованием штатных разъемов.
Перед тем, как приступить к работе, желательно иметь в наличии принципиальную схему контроллера Arduino UNO и описание на собственно контроллер ATmega328, т. к. как схема включения, так и описание работы вольтметра будет базироваться на этих документах.
Схема подключения дисплея приведена на рис. 1. Схема взята с сайта Амперки. Изменены только номера контактов, к которым подключена шина данных индикатора. Это сделано для того, чтобы освободить линии внешних прерываний INT0 и INT1. Питание на контроллер подается от линии USB компьютера.
Подключение подсветки дисплея производится через токоограничительный резистор, чтобы снизить ток потребления.
Программа работы с дисплеем приведена ниже. Подпрограмма PrintTitle() выводит на дисплей заставку программы.
// ================================================================
// подключаем стандартную библиотеку LiquidCrystal
#include <LiquidCrystal.h>
#define TITLE "Multimeter"
// инициализируем объект-экран, передаём использованные
// для подключения контакты на Arduino в порядке:
// RS, E, DB4, DB5, DB6, DB7
LiquidCrystal lcd(12, 11, 7, 6, 5, 4);
char s[16];
char Ver[] = "01.001";
void setup() {
PrintTitle();
}
void loop() {
}
// Подпрограмма печати заголовка программы
// =======================================
void PrintTitle() {
lcd.begin(16, 2);
lcd.print(TITLE); // печатаем первую строку на дисплее
lcd.setCursor(0, 1);
sprintf(s, "Version %s", Ver);
lcd.print(s); // печатаем вторую строку на дисплее
}
В результате получаем на экране следующее изображение (рис. 2):
Попробуем запустить какую-нибудь индикацию того, что контроллер "живет". Для этого заставим мигать расположенный на плате светодиод примерно раз в секунду.
Обычно для отсчета временных интервалов используется таймер. В нашем случае не будем занимать один из трех штатных таймеров, а воспользуемся таймером Watchdog. Он, конечно, не имеет хорошей точности, т. к. работает от RC-генератора, но для данного случая этого вполне достаточно.
Настраивается таймер через установку битов в регистре WDTCSR (Watchdog Timer Control Register). Адрес регистра — 0x60. Расположение бит регистра приведено в табл. 1.
Бит | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Обозначение | WDIF | WDIE | WDP3 | WDCE | WDE | WDP2 | WDP1 | WDP0 |
Режим | R/W | R/W | R/W | R/W | R/W | R/W | R/W | R/W |
Начальное значение | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Возможные режимы работы таймера приведены в табл. 2.
WDTON | WDE | WDIE | Режим | Действие по тайм-ауту |
1 | 0 | 0 | Остановлен | Нет действий |
1 | 0 | 1 | Режим прерываний | Прерывание |
1 | 1 | 0 | Режим системного сброса | Сброс |
1 | 1 | 1 | Режим прерываний и системного сброса |
Вырабатывается прерывание, а затем сигнал сброса |
0 | х | х | Режим системного сброса | Сброс |
Бит WDTON управляет переводом таймера в режим системного сброса. Этот бит находится в специальных конфигурационных байтах и по умолчанию установлен в "1", поэтому его программирование рассматривать не будем.
Бит WDCE (Watchdog Change Enable) регистра WDTCSR управляет возможностью изменения состояния остальных битов этого регистра и должен быть установлен в "1", если необходимо изменить остальные биты регистра. Если этого не сделать, то изменения не будут записаны в регистр.
В процессе отладки программы было обнаружено следующее. Если установить в "1" только бит WDCE, например, командой:
WDTCSR = (1 << WDCE);
то установить делитель не удастся. Следует одновременно установить в "1" и бит WDE, т. е. выполнить команду:
WDTCSR = (1 << WDCE) | (1 << WDE);
а затем уже устанавливать биты делителя. Сбрасывать биты WDCE и WDE не требуется — они сбрасываются аппаратно, как написано в инструкции на контроллер — через 4 такта. Именно такая последовательность установки предделителя таймера приведена в примере инструкции на контроллер.
Установка бит WDP0-WDP3 регистра WDTCSR для изменения делителя таймера приведена в табл. 3.
WDP3 | WDP2 | WDP1 | WDP0 | Число циклов генератора | Примерный тайм-аут при Vcc = 5 V |
0 | 0 | 0 | 0 | 2K (2048) циклов | 16 мс |
0 | 0 | 0 | 1 | 4K (4096) циклов | 32 мс |
0 | 0 | 1 | 0 | 8K (8192) циклов | 64 мс |
0 | 0 | 1 | 1 | 16K (16384) циклов | 0,125 с |
0 | 1 | 0 | 0 | 32K (32768) циклов | 0,25 с |
0 | 1 | 0 | 1 | 64K (65536) циклов | 0,5 с |
0 | 1 | 1 | 0 | 128K (131072) циклов | 1,0 с |
0 | 1 | 1 | 1 | 256K (262144) циклов | 2,0 с |
1 | 0 | 0 | 0 | 512K (524288) циклов | 4,0 с |
1 | 0 | 0 | 1 | 1024K (1048576) циклов | 8,0 с |
1 | 0 | 1 | 0 | Зарезервировано | |
1 | 0 | 1 | 1 | ||
1 | 1 | 0 | 0 | ||
1 | 1 | 0 | 1 | ||
1 | 1 | 1 | 0 | ||
1 | 1 | 1 | 1 |
После этих предварительных замечаний можно привести вариант программы, дополненной работой с Watchdog таймером. Подпрограмма SetWatchdogTimer() производит настройку таймера, а ISR(WDT_vect) — обрабатывает его прерывания.
// ================================================================
// подключаем стандартную библиотеку LiquidCrystal
#include <LiquidCrystal.h>
#define TITLE "Multimeter"
// инициализируем объект-экран, передаём использованные
// для подключения контакты на Arduino в порядке:
// RS, E, DB4, DB5, DB6, DB7
LiquidCrystal lcd(12, 11, 7, 6, 5, 4);
char s[16];
char Ver[] = "01.001";
void setup() {
pinMode(LED_BUILTIN, OUTPUT); // настройка светодиода индикации
PrintTitle();
// запрещаем прерывания перед настройкой
cli();
SetWatchdogTimer();
// разрешаем прерывания после настройки
sei();
}
void loop() {
}
// Подпрограмма печати заголовка программы
// =======================================
void PrintTitle() {
lcd.begin(16, 2);
lcd.print(TITLE); // печатаем первую строку на дисплее
lcd.setCursor(0, 1);
sprintf(s, "Version %s", Ver);
lcd.print(s); // печатаем вторую строку на дисплее
}
// Подпрограмма настройки прерываний сторожевого таймера
// ==================================================
void SetWatchdogTimer() {
// Настройка сторожевого таймера 0.5 sec
WDTCSR = (1 << WDCE) | (1 << WDE); // необходимо для изменения делителя таймера
WDTCSR = (0 << WDP3) | (1 << WDP2) | (0 << WDP1) | (1 << WDP0);// установка делителя 0,5 сек.
WDTCSR |= (1 << WDIE); // разрешение прерываний таймера
}
// Программа обработки прерывания сторожевого таймера
// 1. Обрабатывает светодиод
// ==================================================
ISR(WDT_vect) {
// мигаем светодиодом раз в секунду
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); }
И, загрузив программу в контроллер, увидеть, что контроллер все-таки "живет", подмигивая желтым глазком.
АЦП является устройством, совершенно необходимым для построения вольтметра. Схема с дополнительными навесными элементами приведена на рис. 3.
Дополнительных элементов всего два — делитель из резисторов R2 и R3. Конечно, резистор в 10 кОм слишком мал и входное сопротивление вольтметра будет как раз 10 кОм, что для точных измерений в высокоомных цепях, конечно, недостаточно. Но он просто оказался под руками, и при построении рабочей конструкции этот резистор может быть заменен. Входы АЦП рассчитаны на источники сигнала с сопротивлением до 10 кОм, значит сопротивление R3 может быть увеличено до 200-300 кОм с соответствующим увеличением R2 до 4,3-5,1 кОм.
Опорное напряжение АЦП может быть выбрано внешнее, подключаемое ко входу AREF контроллера, или внутреннее, которое может составлять точное 1,1 В или 5 В от стабилизатора. Внешнее напряжение требует дополнительных элементов в схеме для сборки опорного источника хорошего качества. Поэтому выбираем внутренний источник 1,1 В.
При таком опорном напряжении делитель 1:50 обеспечивает предел измерения около 50 вольт, а 10-разрядное АЦП обеспечивает точность измерения около 50 мВ, что вполне достаточно для пробной конструкции.
Естественно, любитель может применить делитель с меньшим коэффициентом, если требуется большая точность.
Для настройки источника опорного напряжения и входа, на который будет подаваться измеряемое напряжение, в контроллере имеется регистр ADMUX (ADC Multiplexer Selection Register). Адрес регистра — 0x7C, расположение бит в регистре приведено в табл. 4.
Бит | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Обозначение | REFS1 | REFS0 | ADLAR | — | MUX3 | MUX2 | MUX1 | MUX0 |
Режим | R/W | R/W | R/W | R | R/W | R/W | R/W | R/W |
Начальное значение | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Биты REFS1:0 отвечают за выбор источника опорного напряжения (см. табл. 5).
REFS1 | REFS0 | Источник опорного напряжения |
0 | 0 | AREF, внутренний источник VREF отключен |
0 | 1 | AVCC с внешним конденсатором на контакте AREF |
1 | 0 | Зарезервировано |
1 | 1 | Внутренний источник 1,1 В с конденсатором на контакте AREF |
Биты MUX3:0 определяют источник измеряемого напряжения (см. табл. 6).
MUX3 | MUX2 | MUX1 | MUX0 | Вход |
0 | 0 | 0 | 0 | ADC0 |
0 | 0 | 0 | 1 | ADC1 |
0 | 0 | 1 | 0 | ADC2 |
0 | 0 | 1 | 1 | ADC3 |
0 | 1 | 0 | 0 | ADC4 |
0 | 1 | 0 | 1 | ADC5 |
0 | 1 | 1 | 0 | ADC6 |
0 | 1 | 1 | 1 | ADC7 |
1 | 0 | 0 | 0 | ADC8 (для датчика температуры) |
1 | 0 | 0 | 1 | (зарезервировано) |
1 | 0 | 1 | 0 | (зарезервировано) |
1 | 0 | 1 | 1 | (зарезервировано) |
1 | 1 | 0 | 0 | (зарезервировано) |
1 | 1 | 0 | 1 | (зарезервировано) |
1 | 1 | 1 | 0 | 1,1 В (VBG) |
1 | 1 | 1 | 1 | 0 В (GND) |
Следующий регистр АЦП, который потребуется для настройки — ADCSRA (ADC Control and Status Register A). Адрес регистра — 0x7A (см. табл. 7).
Бит | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Обозначение | ADEN | ADSC | ADATE | ADIF | ADIE | ADPS2 | ADPS1 | ADPS0 |
Режим | R/W | R/W | R/W | R/W | R/W | R/W | R/W | R/W |
Начальное значение | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Бит ADEN (ADC Enable) отвечает за включение АЦП. Если бит установлен в "1", то АЦП включено, если в "0" — выключено.
Бит ADSC (ADC Start Conversion) производит запуск АЦП. В режиме однократного преобразования установка этого бита в "1" запускает один цикл преобразования. В режиме автоматической работы АЦП этот бит надо установить в "1" для запуска первого цикла.
Бит ADATE (ADC Auto Trigger Enable) переключает АЦП из режима однократного преобразования ("0") в режим автоматического преобразования ("1"), при котором запуск АЦП происходит автоматически, в зависимости от установки бит ADTS в регистре ADCSRB.
Бит ADIF (ADC Interrupt Flag) устанавливается в "1" при завершении цикла преобразования АЦП. Если установлен бит ADIE (ADC Interrupt Enable), то будет зпущена программа прерываний АЦП.
Бит ADIE (ADC Interrupt Enable) должен быть установлен в "1", если предполагается работа с прерываниями АЦП.
Биты ADPS2:0 (ADC Prescaler Select Bits) определяют делитель между тактовой частотой системы и тактовой частотой преобразования АЦП (см. табл. 8).
ADPS2 | ADPS1 | ADPS0 | Коэффициент деления |
0 | 0 | 0 | 2 |
0 | 0 | 1 | 2 |
0 | 1 | 0 | 4 |
0 | 1 | 1 | 8 |
1 | 0 | 0 | 16 |
1 | 0 | 1 | 32 |
1 | 1 | 0 | 64 |
1 | 1 | 1 | 128 |
Еще два регистра — регистры данных: ADCL и ADCH (The ADC Data Register). Размещение бит в регистрах в зависимости от состояния бита ADLAR в регистре ADMUX приведено в табл. 9. Адреса регистров: ADCL — 0x78 и ADCH — 0х79.
ADLAR = 0 |
|||||||||
Бит | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | |
Обозначение | — | — | — | — | — | — | ADC9 | ADC8 | ADCH |
ADC7 | ADC6 | ADC5 | ADC4 | ADC3 | ADC2 | ADC1 | ADC0 | ADCL | |
Бит | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
Режим | R | R | R | R | R | R | R | R | |
R | R | R | R | R | R | R | R | ||
Начальное значение | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ||
ADLAR = 1 |
|||||||||
Бит | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | |
Обозначение | ADC9 | ADC8 | ADC7 | ADC6 | ADC5 | ADC4 | ADC3 | ADC2 | ADCH | ADC1 | ADC0 | — | — | — | — | — | — | ADCL |
Бит | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
Режим | R | R | R | R | R | R | R | R | |
R | R | R | R | R | R | R | R | ||
Начальное значение | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Остальные регистры АЦП нам пока не понадобятся. В соответствии с изложенным выше, напишем подпрограмму установки АЦП и добавим ее в текст нашей программы.
// ================================================================
// подключаем стандартную библиотеку LiquidCrystal
#include <LiquidCrystal.h>
#define TITLE "Multimeter"
#define R2 220.0 // резисторы входного делителя
#define R3 10000.0 // в омах
// Ячейки, используемые АЦП
float ADC_Kadc;
float ADC_Vref;
// инициализируем объект-экран, передаём использованные
// для подключения контакты на Arduino в порядке:
// RS, E, DB4, DB5, DB6, DB7
LiquidCrystal lcd(12, 11, 7, 6, 5, 4);
char s[16];
char Ver[] = "01.003";
void setup() {
pinMode(LED_BUILTIN, OUTPUT); // настройка светодиода индикации
PrintTitle();
// запрещаем прерывания перед настройкой
cli();
SetWatchdogTimer();
SetADC();
// разрешаем прерывания после настройки
sei();
}
void loop() {
}
// Подпрограмма печати заголовка программы
// =======================================
void PrintTitle() {
lcd.begin(16, 2);
lcd.print(TITLE); // печатаем первую строку на дисплее
lcd.setCursor(0, 1);
sprintf(s, "Version %s", Ver);
lcd.print(s); // печатаем вторую строку на дисплее
}
// Подпрограмма настройки прерываний сторожевого таймера
// =====================================================
void SetWatchdogTimer() {
// Настройка сторожевого таймера 0.5 sec
WDTCSR = (1 << WDCE) | (1 << WDE); // необходимо для изменения делителя таймера
WDTCSR = (0 << WDP3) | (1 << WDP2) | (0 << WDP1) | (1 << WDP0);// установка делителя 0,5 сек.
WDTCSR |= (1 << WDIE); // разрешение прерываний таймера
}
// Подпрограмма установки режимов АЦП
// ==================================
void SetADC() {
// Настройка делителя входного напряжения АЦП
ADC_Vref = 1100;
ADC_Kadc = (Vref * (R2 + R3)) / (1024 * R2);
// Установка опорного напряжения 1.1 В.
// и входа А0
// Устанавливаем только те биты, которые должны быть установлены в "1"
ADMUX = (1 << REFS0) | (1 << REFS1);
// Установка частоты преобразования
// делитель на 128
ADCSRA = (1 << ADEN) | (1 << ADIE) | (1 << ADPS2) | (1 << ADPS1) |(1 << ADPS0);
}
// Программа обработки прерывания сторожевого таймера
// 1. Обрабатывает светодиод
// ==================================================
//
ISR(WDT_vect) {
// мигаем светодиодом раз в секунду
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); }
Программу можно загрузить в контроллер, но результата на экране мы не увидим — для этого надо разработать вывод на экран. И прежде всего используем программу прерываний Watchlog таймера для очистки экрана от заставки через 3 секунды. Обычно для этого используют функцию delay, но это не самый правильный (хотя и самый простой) вариант. Прежде всего введем дополнительную переменную WDT_count, которую каждый цикл прерываний таймера будет увеличивать на 1. Заметим, что переменная отмечена не только как int, но и как volatile. Это необходимо для правильного обращения к ячейкам из программ прерываний.
Не следует излишне увлекаться применением обозначением переменных как volatile. Это "съедает" память контроллера. На рис. 4 (а и б) приведено состояние памяти до и после отметки одной переменной как volatile.
Еще введем слово состояния программы: SYS_status, биты которого будут устанавливаться или сбрасываться в зависимости от режима работы. И бит SYS_STARTRDY (15-й бит в слове), будучи установлен в "1", будет служить индикатором того, что программа готова к работе. Для начала будем просто очищать экран командой lcd.clear().
// ================================================================
// подключаем стандартную библиотеку LiquidCrystal
#include <LiquidCrystal.h>
#define TITLE "Multimeter"
#define R2 220.0 // резисторы входного делителя
#define R3 10000.0 // в омах
// Ячейки, используемые АЦП
float ADC_Kadc;
float ADC_Vref;
float ADC_Vin;
volatile int ADC_result;
// Ячейки, используемые WDT_intr
volatile int WDT_count;
// Слово состояния программы
volatile unsigned int SYS_status;
#define SYS_STARTRDY 0x8000
#define SYS_SCREENRDY 0x4000
#define SYS_ADCRDY 0x2000
#define SYS_VOLTMETER 0x0004
// инициализируем объект-экран, передаём использованные
// для подключения контакты на Arduino в порядке:
// RS, E, DB4, DB5, DB6, DB7
LiquidCrystal lcd(12, 11, 7, 6, 5, 4);
char s[16];
char adc[16];
char Ver[] = "01.007";
void setup() {
SYS_status = 0;
pinMode(LED_BUILTIN, OUTPUT); // настройка светодиода индикации
PrintTitle();
cli(); // запрещаем прерывания перед настройкой
SetWatchdogTimer();
SetADC();
sei(); // разрешаем прерывания после настройки
WDT_count = 0;
SYS_status |= SYS_VOLTMETER;
}
void loop() {
// тайм-аут на 3 секунды на заставку
if((SYS_status & SYS_STARTRDY) == 0) {
if(WDT_count > 6) {
SYS_status |= SYS_STARTRDY;
WDT_count = 0;
lcd.clear(); } }
// тайм-аут прошел, можно выполнять программу
else {
// работа в режиме вольтметра
if((SYS_status & SYS_VOLTMETER) != 0) {
// выводим на экран статическую информацию
if((SYS_status & SYS_SCREENRDY) == 0) {
lcd.setCursor(0, 0);
lcd.print(" Vin = V");
SYS_status |= SYS_SCREENRDY; }
// экран готов, можно выводить данные
else {
if((SYS_status & SYS_ADCRDY) != 0) {
ADC_Vin = (ADC_Kadc * ADC_result) / 1000;
dtostrf(ADC_Vin, 7 3, adc);
lcd.setCursor(7, 0);
lcd.print(adc);
SYS_status &= ~SYS_ADCRDY;
}
}
}
}
}
// Подпрограмма печати заголовка программы
// =======================================
void PrintTitle() {
lcd.begin(16, 2);
lcd.print(TITLE); // печатаем первую строку на дисплее
lcd.setCursor(0, 1);
sprintf(s, "Version %s", Ver);
lcd.print(s); // печатаем вторую строку на дисплее
}
// Подпрограмма настройки прерываний сторожевого таймера
// =====================================================
void SetWatchdogTimer() {
// Настройка сторожевого таймера 0.5 sec
//
WDTCSR = (1 << WDCE) | (1 << WDE); // необходимо для изменения делителя таймера
WDTCSR = (0 << WDP3) | (1 << WDP2) | (0 << WDP1) | (1 << WDP0);// установка делителя 0,5 сек.
WDTCSR |= (1 << WDIE); // разрешение прерываний таймера
}
// Подпрограмма установки режимов АЦП
// ==================================
void SetADC() {
// Настройка делителя входного напряжения АЦП
ADC_Vref = 1100;
ADC_Kadc = (ADC_Vref * (R2 + R3)) / (1024 * R2);
// Установка опорного напряжения 1.1 В. и входа А0
// Устанавливаем только те биты, которые должны быть установлены в "1"
ADMUX = (1 << REFS0) | (1 << REFS1);
// Установка частоты преобразования
// делитель на 128
ADCSRA = (1 << ADEN) | (1 << ADIE) | (1 << ADPS2) | (1 << ADPS1) |(1 << ADPS0);
}
// Программа обработки прерывания сторожевого таймера
// 1. Обрабатывает светодиод
// 2. Обрабатывает запуск АЦП
// ==================================================
ISR(WDT_vect) {
// мигаем светодиодом раз в секунду
//
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
WDT_count++; // увеличиваем счетчик таймера
// В режиме вольтметра запускаем АЦП
if((SYS_status & SYS_VOLTMETER) != 0 & (SYS_status & SYS_ADCRDY) == 0) ADCSRA |= (1 << ADSC);
}
// Программа обработки прерывания АЦП
// ==================================
ISR(ADC_vect) {
SYS_status |= SYS_ADCRDY; // установка флага готовности данных АЦП
ADC_result = ADCL; // сохраняем младший байт результата АЦП
ADC_result += ADCH << 8; // сохраняем старший байт АЦП
}
В программу добавлены также: программа обработки прерываний АЦП ISR(ADC_vect), в программу обработки прерываний таймера вставлен запуск АЦП, а в основную программу loop кусок, отвечающий за вывод на экран результата измерения. Результат работы программы при измерении напряжения питания контроллера можно посмотреть на рис. 5.
На дисплее остается незадействованная строка. Используем ее для вывода, скажем, максимального измеренного напряжения и тому подобного.
Самое очевидное, что можно рассчитать — это максимальное и среднее измеренное напряжения. По ссылке приведена версия программы, в которой рассчитываются эти величины и на дисплей выводится максимальное значение.
Полученный результат приведен на рис. 6.
Однако, есть одна неприятнось. Vmid рассчитывается, но выводить его некуда — строчек на дисплее всего две. Можно, конечно, задействовать прерывания сторожевого таймера и менять Vmax и Vmin раз в несколько секунд. Желающие могут попробовать сделать это самостоятельно, а мы будем переключать режим вывода с помощью кнопки, которую подключим ко входу внешнего прерывания INT1.
Подключение кнопки через прерывания позводяет избавиться от постоянного опроса состояния входа основной программой. Схема подключения приведена на рис. 7.
Сначала рассмотрим регисты, которые будем использовать при настройке прерывания INT1. Один из них — EICRA (External Interrupt Control Register A), адрес — 0x69 (см. табл. 10).
Бит | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Обозначение | — | — | — | — | ISC11 | ISC10 | ISC01 | ISC00 |
Режим | R | R | R | R | R/W | R/W | R/W | R/W |
Начальное значение | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Биты этого регистра определяют режим срабатывания прерывания. В табл. 11 приведено соответствие состояния этих бит режиму входа внешних прерываний INT1.
ISC11 | ISC10 | Описание |
0 | 0 | Прерывание генерируется низким уровенем на входе INT1 |
0 | 1 | Прерывание генерируется любым изменением уровня на входе INT1 |
1 | 0 | Прерывание генерируется отрицательным фронтом на входе INT1 |
1 | 1 | Прерывание генерируется положительным фронтом на входе INT1 |
Второй регистр, необходимый для настройки — EIMSK (External Interrupt Mask Register). Адрес регистра — 0x1D (0x3D).
Бит | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Обозначение | — | — | — | — | — | — | INT1 | INT0 |
Режим | R | R | R | R | R | R | R/W | R/W |
Начальное значение | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
При установке бит INT1 и INT0 в "1" будут соответственно разрешены прерывания по входам 1 и 0. Казалось бы, достаточно установить биты регистров в нужное состояние (будем использовать режим прерываний при изменении уровня на входе) и можно в процессе выполнения программы прерываний считывать состояние входа и использовать его для измения режима индикации. Но есть одна неприятность — дребезг. При нажатии на кнопку как правило вырабатывается не одно изменение уровня, а несколько пиков, которые могут вызвать несколько последовательных прерываний как при нажатии, так и при отпускании кнопки.
Можно применить аппаратные средства для подавления дребезга (самый простой — шунтирование кнопки конденсатором). Однако попробуем использовать программные средства контроллера. А именно — используем один из таймеров (таймер 2) для подавления дребезга. Поэтому рассмотрим регистры управления этого таймера.
Контрольный регистр А таймера TCCR2A (Timer/Counter Control Register A), адрес регистра 0xB0 (см. табл. 13).
Бит | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Обозначение | COM2A1 | COM2A0 | COM2B1 | COM2B0 | — | — | WGM21 | WGM20 |
Режим | R/W | R/W | R/W | R/W | R | R | R/W | R/W |
Начальное значение | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Контрольный регистр B таймера TCCR2B (Timer/Counter Control Register B), адрес регистра 0xB1 (см. табл. 14).
Бит | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Обозначение | FOC2A | FOC2B | — | — | WGM22 | CS22 | CS21 | CS20 |
Режим | R/W | R/W | R | R | R/W | R/W | R/W | R/W |
Начальное значение | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
В регистре TCCR2A нас интересуют биты WGM21:0, остальные биты можно оставить равными "0" (значение по умолчанию), т. к. режим ШИМ таймера (pulse width modulation (PWM) modes) не используется. Эти биты, а также бит WGM22 регистра TCCR2B, устанавливают режим работы таймера (см. табл. 15).
Режим | WGM2 | WGM1 | WGM0 | Режим работы таймера |
TOP | обновление OCRx при |
Флаг TOV устанавливается при |
0 | 0 | 0 | 0 | Нормальный | 0xFF | Немедленно | MAX |
1 | 0 | 0 | 1 | PWM, phase correct | 0xFF | TOP | BOTTOM |
2 | 0 | 1 | 0 | CTC | OCRA | Немедленно | MAX |
3 | 0 | 1 | 1 | Fast PWM | 0xFF | BOTTOM | MAX |
4 | 1 | 0 | 0 | Резерв | — | — | — |
5 | 1 | 0 | 1 | PWM, phase correct | OCRA | TOP | BOTTOM |
6 | 1 | 1 | 0 | Резерв | — | — | — |
7 | 1 | 1 | 1 | Fast PWM | OCRA | BOTTOM | TOP |
Следующая таблица (табл. 16) проказывает установку коэффициента деления предварительного делителя для тактовой частоты таймера 2 установкой бит CS22:0 в регистре TCCR2B.
CS22 | CS21 | CS20 | Описание |
0 | 0 | 0 | Нет источника тактов (таймер остановлен) |
0 | 0 | 1 | clkT2S/(Без делителя) |
0 | 1 | 0 | clkT2S/8 |
0 | 1 | 1 | clkT2S/32 |
1 | 0 | 0 | clkT2S/64 |
1 | 0 | 1 | clkT2S/128 |
1 | 1 | 0 | clkT2S/256 |
1 | 1 | 1 | clkT2S/1024 |
Еще один регистр, который будет использоваться — регистр счетчика таймера 2 — TCNT2 (Timer/Counter Register), адрес 0xB2 (см. табл. 17).
Бит | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Обозначение | TCNT2[7:0] | |||||||
Режим | R/W | R/W | R/W | R/W | R/W | R/W | R/W | R/W |
Начальное значение | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Следующий регистр таймера 2 — регистр сравнения А OCR2A (Output Compare Register A), адрес регистра 0xB3 см. табл. 18).
Бит | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Обозначение | OCR2A[7:0] | |||||||
Режим | R/W | R/W | R/W | R/W | R/W | R/W | R/W | R/W |
Начальное значение | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
И последний регистр таймера 2, который мы будем использовать (но далеко не последний регистр этого таймера) — регистр маски прерываний TIMSK2 (Timer/Counter2 Interrupt Mask Register), адрес регистра 0x70 см. табл. 19).
Бит | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Обозначение | — | — | — | — | — | OCIE2B | OCIE2A | TOIE2 |
Режим | R | R | R | R | R | R/W | R/W | R/W |
Начальное значение | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Поскольку работать будем с прерываниями по сравнению счетчика таймера с регистром А, то в этом регистре требуется устанавливать бит OCIE2A.
Перейдем к программе обработки клавиши и дребезга. Добавляются функции настройки входа внешнего прерывания 1 SetINT1() и таймера 2 SetTimer2(int CountA). Внешнее прерывание настроено на работу при изменении уровня входного сигнада, чтобы определять как нажатие, так и отпускание кнопки.
Настройка таймера самая предварительная, т. к. до изменения состояния кнопки S1 он остановлен и запускается только после нажатия/отпускания кнопки в программе обработки внешних прерываний ISR(INT1_vect).
Принцип работы программного подавления дребезга очень прост. При нажатии/отпускании кнопки вырабатывается прерывание INT1, которое, в свою очередь, запускает таймер 2.
Если до завершения счета таймера 2 произойдет еще оно прерывание INT1, то таймер 2 будет перезапущен и начнет отсчет с нуля. Таким образом, код кнопки будет считаться достоверным только после того, как пройдет интервал ожидания таймера 2, и будет выработано прерывание ISR(TIMER2_COMPA_vect), которое установит бит готовности SYS_KEYRDY в слове состояния. По этому биту основная программа определяет нажатие/отпускание кнопки и может произвести необходимые действия.
Следует сделать одно замечание. В процессе отладки программы выяснилось следующее обстоятельство. Если считывание состояния входа INT1 производить с помощью команды digitalRead (), то при кратковременном нажатии кнопки состояние входа периодически оставалось равным "0", хотя кнопка была уже отжата. При "уверенном" фиксировании кнопки такого эффекта не наблюдалось, однако рассчитывать на это нельзя. Поэтому вместо команды digitalRead () для чтения порта применена команда KEY_value = PIND;. Скорее всего, из-за медленного выполнения, результат команды digitalRead () оставался от предыдущего прерывания, или происходил пропуск прервания на отжатие клавиши.
Однако попробуем вынести эту часть обработки в программу прерываний ISR(TIMER2_COMPA_vect). Конечно, нежелательно перегружать программу прерываний, т. к. на время обработки текущего прерывания остальные прерывания запрещены и можно пропустить важное событие (выше приведен пример, как излишнее время выполнения сказывается на достоверности данных), поэтому постараемся сделать минимальные дополнения.
Сначала рассмотрим прерывания таймера 2 ISR(TIMER2_COMPA_vect).
// Программа прерывания таймера 2 по сравнению
// ============================================
ISR(TIMER2_COMPA_vect) {
KEY_value &= 0x08;
if(KEY_value != 0) {
SYS_status &= ~SYS_KEYPRESS;
if((SYS_status & SYS_SCREEN1) == 0)
SYS_status |= SYS_SCREEN1;
else
SYS_status &= ~SYS_SCREEN1;
SYS_status &= ~SYS_SCREENRDY; }
else {
SYS_status |= SYS_KEYPRESS;
WDT_count = 0; }
SYS_status |= SYS_KEYRDY;
TIMSK2 = 0; // отключение прерываний по сравнению (CTC)
TCCR2B = 0; // останавливаем таймер
}
Если прерывание произошло по отпусканию клавиши (KEY_value != 0), то инвертируется состояние бита SYS_SCREEN1, который определяет что будет выводиться — Vmax или Vmid, и сбрасывается бит SYS_SCREENRDY, чтобы обновилась информация на дисплее и сбрасывается бит SYS_KEYPRESS.
Если прерывание произошло по нажатию клавиши, то устанавливается бит SYS_KEYPRESS и запускается таймер проверки времени нажатия.
// Программа обработки прерывания сторожевого таймера
// 1. Обрабатывает светодиод
// 2. Обрабатывает запуск АЦП
// 3. Обрабатывает нажатое состояние клавиши
// ==================================================
ISR(WDT_vect) {
// мигаем светодиодом раз в секунду
//
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
WDT_count++; // увеличиваем счетчик таймера
// сброс максимального и среднего при удержании клавиши
if(WDT_count > 8 && (SYS_status & SYS_KEYPRESS) != 0) {
SYS_status &= ~SYS_KEYPRESS;
SYS_status &= ~SYS_SCREENRDY;
ADC_Vmax = 0;
ADC_Vmid = 0;
ADC_Vsum = 0;
ADC_count = 0; }
// В режиме вольтметра запускаем АЦП
if((SYS_status & SYS_VOLTMETER) != 0 & (SYS_status & SYS_ADCRDY) == 0) ADCSRA |= (1 << ADSC); }
Нажатие кнопки обрабатывается следующим образом. Если удерживать кнопку нажатой более 1 сек., то во второй строке дисплея выводится текущее значение ADC_count. Если удерживать кнопку нажатой более 4 сек., то сбрасываются Vmax, Vmid, ADC_count, Vsum и расчет этих параметров начинается заново. Это может быть полезно при измерении напряжений в разных точках проверяемой схемы. Текст этого варианта программы приведен по ссылке.
Результат индикации после однократного нажатия кнопки показан на рис. 8.
Если кнопку удерживать более 1 сек., но меньше 4 сек., то на дисплей выводится чмсло отсчетов, которые используются для расчета среднего напряжения Vmid, что показано на рис. 9.
Если удерживать кнопку в нажатом состоянии более 4 сек., то производится сброс всех расчетных значений.
Последнее, что можно попробовать сделать — ввести в программу автоматический выбор диапазона измерения. Самый простой способ — сделать это при помощи изменения опорного напряжения Vref. При этом не потребуется вносить в схему никаких изменений, что очень удобно.
Изменения вносятся в функцию настройки АЦП void SetADC().
// Подпрограмма установки режимов АЦП
// ==================================
void SetADC() {
// Настройка делителя входного напряжения АЦП
if((SYS_status & SYS_VREF5) == 0) {
ADC_Vref = VREF1100;
// Установка опорного напряжения 1,1 В и входа А0
ADMUX = (1 << REFS0) | (1 << REFS1); }
else {
ADC_Vref = VREF5000;
// Установка опорного напряжения 5,0 В и входа А0
ADMUX = (1 << REFS0) | (0 << REFS1); }
ADC_Kadc = (ADC_Vref * (R2 + R3)) / (1024 * R2);
// Установка частоты преобразования
// делитель на 128
ADCSRA = (1 << ADEN) | (1 << ADIE) | (1 << ADPS2) | (1 << ADPS1) |(1 << ADPS0); }
Переключение опорного напряжения производится в основной программе.
// проверка на переключение диапазона измерения
if(ADC_result > 900 || ADC_result < 200) {
if(ADC_result > 900 && (SYS_status & SYS_VREF5) == 0) {
SYS_status |= SYS_VREF5;
SetADC(); }
else if(ADC_result < 200 && (SYS_status & SYS_VREF5) != 0) {
SYS_status &= ~SYS_VREF5;
SetADC(); } }
Полный текст программы приведен по ссылке, а результат, выражающийся в максимальном измеряемом напряжении — на рис. 10.
В заключение несколько замечаний.
1. О номиналах сопротивлений делителя сказано в примечании к предыдущему разделу. При создании реальной конструкции обязательно обратите на это внимание!
2. Три значащих цифры после запятой при указанных диапазонах измерений (50 и 250 В) конечно совершенно излишни. Погрешность измерения при этом составляет 50 и 250 мВ соответственно, поэтому использовать больше 2-х цифр после запятой не имеет смысла. Желающие могут самостоятельно изменить параметры вывода, или изменить параметры делителя для получения, например, диапазонов 5 и 25 вольт.
3. При измерении требуется соблюдать полярность, иначе не только данные будут недостоверны, но и есть шанс вывести из строя контроллер. Для защиты входа контроллера можно применить защитный диод, что показано на рис. 11.
4. Калибровка вольтметра (вообще говоря вещь совершенно обязательная, если предполагается производить измерения в реальных схемах) в данной заметке не рассматривается. О том, какими средствами можно провести калибровку, и как подстраивать показания прибора аппаратными и программными средствами будет рассказано в последующих заметках.
5. И еще о делителе и диапазонах измеряемого напряжения. Для того, чтобы вольтметр не вносил существенных искажений режимов работы схемы при проведении измерений, очень желательно иметь входное сопротивление вольтметра не менее 10-20 кОм/В. Вот из этих соображений и следует выбирать как номиналы резисторов делителя, так и диапазоны измеряемых напряжений. Для измерения высоких напряжений лучше использовать внешний делитель.