Раздел из главы 7 “Управление светодиодными устройствами вывода данных” из книги “Arduino. Большая книга рецептов, 3-е изд.” (авторы Джепсон Брайан, Марголис Майкл, Уэлдин Николас Роберт)
ЗАДАЧА
Требуется управлять светодиодной матрицей 8×8, используя минимальное количество контактов платы Arduino.
РЕШЕНИЕ
Подобно решению из разд. 7.13, количество контактов для управления светодиодной матрицей можно уменьшить, применив микросхему драйвера дисплея. В этом решении используется популярная микросхема драйвера светодиодного дисплея MAX7219 или MAX7221. Подключение драйвера дисплея к плате Arduino и светодиодной матрицы к драйверу показано на рис. 7.16, а в листинге 7.19 приводится скетч для работы с этой схемой.
Рис. 7.16. Подключение драйвера дисплея MAX72xx к плате Arduino для управления светодиодной матрицей 8×8
В скетче используется библиотека с обширными возможностями MD_MAX72XX, которая поддерживает отображение на дисплее текста и фигур, а также выполнение различных трансформаций отображаемого содержимого. Библиотека MD_MAX72XX устанавливается с помощью Менеджера библиотек среды Arduino IDE (подробная информация по установке библиотек приведена в разд. 16.2).
Листинг 7.19. Управление светодиодной матрицей
с помощью драйвера дисплея MAX72xx
/*
Скетч 7219 Matrix demo
*/#include <MD_MAX72xx.h>// Контакты платы Arduino для управления драйвером 7219#define LOAD_PIN 2#define CLK_PIN 3#define DATA_PIN 4// Конфигурируем устройство#define MAX_DEVICES 1#define HARDWARE_TYPE MD_MAX72XX::PAROLA_HW
MD_MAX72XX mx = MD_MAX72XX(HARDWARE_TYPE, DATA_PIN, CLK_PIN, LOAD_PIN, MAX_DEVICES);void setup(){
mx.begin();}void loop(){
mx.clear();// Очищаем дисплей// Отображаем строки и столбцыfor(int r =0; r <8; r++){for(int c =0; c <8; c++){
mx.setPoint(r, c,true);// Выключаем каждый светодиод модуля
delay(50);}// Проходим в цикле по всем возможным уровням яркостиfor(int k =0; k <= MAX_INTENSITY; k++){
mx.control(MD_MAX72XX::INTENSITY, k);
delay(100);}}}
/*
Скетч 7219 Matrix demo
*/
#include <MD_MAX72xx.h>
// Контакты платы Arduino для управления драйвером 7219
#define LOAD_PIN 2
#define CLK_PIN 3
#define DATA_PIN 4
// Конфигурируем устройство
#define MAX_DEVICES 1
#define HARDWARE_TYPE MD_MAX72XX::PAROLA_HW
MD_MAX72XX mx = MD_MAX72XX(HARDWARE_TYPE, DATA_PIN, CLK_PIN, LOAD_PIN, MAX_DEVICES);
void setup()
{
mx.begin();
}
void loop()
{
mx.clear(); // Очищаем дисплей
// Отображаем строки и столбцы
for (int r = 0; r < 8; r++)
{
for (int c = 0; c < 8; c++)
{
mx.setPoint(r, c, true); // Выключаем каждый светодиод модуля
delay(50);
}
// Проходим в цикле по всем возможным уровням яркости
for (int k = 0; k <= MAX_INTENSITY; k++)
{
mx.control(MD_MAX72XX::INTENSITY, k);
delay(100);
}
}
}
Обсуждение работы решения и возможных проблем
В начале скетча создается экземпляр mx объекта MD_MAX72XX, которому в параметрах передается тип устройства, номера контактов для данных загрузки и сигнала тактирования, а также максимальное количество устройств (при последовательном соединении модулей). В главном цикле loop() выполняется очистка дисплея, а затем с помощью функции setPoint() включаются пикселы (светодиоды) матрицы. Включив строку матрицы, скетч проходит в цикле через все возможные уровни яркости, а затем переходит к обработке следующей строки.
В скетче указаны номера контактов для зеленых светодиодов двухцветной светодиодной матрицы 8×8 компании Adafruit (артикул 458). При использовании другой светодиодной матрицы обратитесь к справочному листку на нее, чтобы определить контакты для ее строк и столбцов. Скетч также будет работать и с одноцветной матрицей, поскольку он использует только один из двух цветов матрицы. Если обнаружится, что матрица отображает текст в обратном направлении или не в ожидаемой ориентации, можно попробовать исправить эту ошибку, изменив тип устройства в строке:
1
#define HARDWARE_TYPE MD_MAX72XX::PAROLA_HW с PAROLA_HW
#define HARDWARE_TYPE MD_MAX72XX::PAROLA_HW с PAROLA_HW
на GENERIC_HW, ICSTATION_HW или FC16_HW. В примерах библиотеки MD_MAX72XX содержится тестовый скетч MD_MAX72xx_HW_Mapper, который выполняет тестирование и помогает определить, какой тип устройства использовать.
Резистор R1 на рис. 7.16 ограничивает максимальный ток, который может протекать через светодиод. В табл. 7.4 приведены данные номиналов токоограничивающих резисторов из справочного листка драйвера MAX72xx для нескольких значений прямого напряжения светодиода и величины протекающего через него тока.
Таблица 7.4. Номиналы токоограничивающих резисторов (из справочного листка драйвера MAX72xx)
Ток
Прямое напряжение светодиода
1,5 В
2,0 В
2,5 В
3,0 В
3,5 В
40 мА
12 кОм
12 кОм
11 кОм
10 кОм
10кОм
30 мА
18 кОм
17 кОм
16 кОм
15 кОм
14 кОм
20 мА
30 кОм
28 кОм
26 кОм
24 кОм
22 кОм
10 мА
68 кОм
64 кОм
60 кОм
56 кОм
51 кОм
Величина прямого напряжения зеленых светодиодов матрицы, представленной на рис. 7.16, составляет 2 вольта, а прямой ток — 20 мА. Согласно данным табл. 7.3, для этой комбинации напряжения и тока требуется резистор сопротивлением 38 кОм, но для надежности лучше использовать резистор номиналом 30 или 33 кОм. Конденсаторы емкостью 0,1 и 10 мкФ, подключенные параллельно линиям питания, требуются для того, чтобы предотвратить возникновение импульсных помех при включении и выключении светодиодов матрицы (дополнительная информация по таким конденсаторам приводится в разд. «Использование развязывающих конденсаторов»приложения 3).
Дополнительная информация
Подробная информация по микросхеме драйвера дисплея MAX72xx приведена в ее справочном листке (https://oreil.ly/IH7U7).
Раздел из главы 7 “Управление светодиодными устройствами вывода данных” из книги “Arduino. Большая книга рецептов, 3-е изд.” (авторы Джепсон Брайан, Марголис Майкл, Уэлдин Николас Роберт)
ЗАДАЧА
Требуется управлять светодиодной матрицей, задействовав для этого как можно меньшее количество контактов платы Arduino.
РЕШЕНИЕ
Одно из возможных решений этой задачи — использование чарлиплексирования. Чарлиплексирование — это особый вид мультиплексирования, позволяющий увеличить количество устройств (светодиодов), которыми можно управлять одной группой контактов. В листинге 7.13 приводится скетч для управления посредством чарлиплексирования шестью светодиодами, задействовав всего лишь три контакта платы Arduino. Подключение светодиодов показано на рис. 7.11 (методика вычисления значений токоограничивающих резисторов для светодиодов приводится в разд. 7.1).
Рис. 7.11. Использование метода чарлиплексирования для управления шестью светодиодами с помошью трех контактов платы
Листинг 7.13. Управление несколькими светодиодами методом чарлиплексирования
/*
* Скетч Charlieplexing
* Последовательно включает и выключает три светодиода.
*/int pins[]={2,3,4};// Номера контактов для управления светодиодами// Следующие две строки кода вычисляют количество контактов// и светодиодов на основе информации в массиве pinsconstint NUMBER_OF_PINS =sizeof(pins)/sizeof(pins[0]);constint NUMBER_OF_LEDS = NUMBER_OF_PINS *(NUMBER_OF_PINS-1);
byte pairs[NUMBER_OF_LEDS/2][2]={{2,1},{1,0},{2,0}};// Сопоставляем// контакты светодиодамvoid setup(){// Ничего не делаем}void loop(){for(int i=0; i < NUMBER_OF_LEDS; i++){
lightLed(i);// Последовательно включаем каждый светодиод
delay(1000);}}// Эта функция включает требуемый светодиод. Нумерация светодиодов// начинается с 0void lightLed(int led){// Следующие четыре строки кода преобразовывают номер светодиода// в номера контактовint indexA = pairs[led/2][0];int indexB = pairs[led/2][1];int pinA = pins[indexA];int pinB = pins[indexB];// Выключаем все контакты, не подключенные к заданному светодиодуfor(int i=0; i < NUMBER_OF_PINS; i++){if(pins[i]!= pinA && pins[i]!= pinB){// Если этот контакт не является одним из требуемых
pinMode(pins[i], INPUT);// Задаем для него входной режим работы
digitalWrite(pins[i],LOW);// Отключаем повышающий резистор}}// Теперь включаем контакты для требуемого светодиода
pinMode(pinA, OUTPUT);
pinMode(pinB, OUTPUT);if( led %2==0){
digitalWrite(pinA,LOW);
digitalWrite(pinB,HIGH);}else{
digitalWrite(pinB,LOW);
digitalWrite(pinA,HIGH);}}
/*
* Скетч Charlieplexing
* Последовательно включает и выключает три светодиода.
*/
int pins[] = {2,3,4}; // Номера контактов для управления светодиодами
// Следующие две строки кода вычисляют количество контактов
// и светодиодов на основе информации в массиве pins
const int NUMBER_OF_PINS = sizeof(pins)/ sizeof(pins[0]);
const int NUMBER_OF_LEDS = NUMBER_OF_PINS * (NUMBER_OF_PINS-1);
byte pairs[NUMBER_OF_LEDS/2][2] = { {2,1}, {1,0}, {2,0} }; // Сопоставляем
// контакты светодиодам
void setup()
{
// Ничего не делаем
}
void loop()
{
for(int i=0; i < NUMBER_OF_LEDS; i++)
{
lightLed(i); // Последовательно включаем каждый светодиод
delay(1000);
}
}
// Эта функция включает требуемый светодиод. Нумерация светодиодов
// начинается с 0
void lightLed(int led)
{
// Следующие четыре строки кода преобразовывают номер светодиода
// в номера контактов
int indexA = pairs[led/2][0];
int indexB = pairs[led/2][1];
int pinA = pins[indexA];
int pinB = pins[indexB];
// Выключаем все контакты, не подключенные к заданному светодиоду
for(int i=0; i < NUMBER_OF_PINS; i++)
{
if(pins[i] != pinA && pins[i] != pinB)
{
// Если этот контакт не является одним из требуемых
pinMode(pins[i], INPUT); // Задаем для него входной режим работы
digitalWrite(pins[i],LOW); // Отключаем повышающий резистор
}
}
// Теперь включаем контакты для требуемого светодиода
pinMode(pinA, OUTPUT);
pinMode(pinB, OUTPUT);
if( led % 2 == 0)
{
digitalWrite(pinA,LOW);
digitalWrite(pinB,HIGH);
}
else
{
digitalWrite(pinB,LOW);
digitalWrite(pinA,HIGH);
}
}
Обсуждение работы решения и возможных проблем
Этот метод мультиплексирования называется чарлиплексированием по имени Чарли Аллена (Charlie Allen) из компании Microchip Technology, Inc., который его разработал и опубликовал. Метод основан на том обстоятельстве, что светодиоды включаются только при правильном подключении: когда анод более положительный, чем катод. В табл. 7.3 приводится список комбинаций выходных сигналов на трех контактах управления и соответствующие состояния шести управляемых ими светодиодов (см. рис. 7.9). Обозначение L означает LOW (низкий уровень), H — HIGH (высокий уровень), а i — INPUT (входной режим работы). Контакт, находящийся во входном режиме работы, по сути, отключен от схемы.
Таблица 7.3. Комбинации уровней контактов и соответствующие состояния светодиодов
Контакты
Светодиоды
4
3
2
1
2
3
4
5
6
L
L
L
0
0
0
0
0
0
L
H
i
1
0
0
0
0
0
H
L
i
0
1
0
0
0
0
i
L
H
0
0
1
0
0
0
i
H
L
0
0
0
1
0
0
L
i
H
0
0
0
0
1
0
H
i
L
0
0
0
0
0
1
Количество светодиодов можно увеличить до 12, добавив всего лишь еще один контакт управления. В таком случае первые шесть светодиодов подключаются так же, как и в предыдущем примере (см. рис. 7.11), а дополнительные шесть, как показано на рис. 7.12.
Для управления удвоенным количеством светодиодов предыдущий скетч (см. листинг 7.13) нужно модифицировать, добавив дополнительный контакт в массив pins:
byte pins[] = {2,3,4,5}; // Номера контактов для управления светодиодами
Рис. 7.12. Использование метода чарлиплексирования для управления 12 светодиодами
с помошью четырех контактов платы
Также необходимо откорректировать строку кода, сопоставляющую контакты светодиодам, добавив в массив pairs дополнительные пары:
Остальной код остается таким же, но в цикле loop() будут обрабатываться 12 светодиодов, т. к. их количество определяется по количеству элементов массива pins.
Поскольку чарлиплексирование состоит в управлении контактами Arduino таким образом, что в единицу времени включается только один светодиод, создание впечатления одновременного включения нескольких светодиодов является в этом случае более сложной задачей. Однако несколько светодиодов можно включать одновременно с помощью метода мультиплексирования, модифицированного для использования с чарлиплексированием.
В листинге 7.14 приводится код скетча, в котором этот подход используется для создания линейного индикатора, включая последовательность светодиодов, количество которых пропорционально значению сигнала датчика, подаваемого на аналоговый контакт A0.
Листинг 7.14. Создание линейного индикатора с помощью модифицированного чарлиплексирования
/* Скетч создания линейного индикатора с использованием чарлиплексирования
*/
byte pins[]={2,3,4};constint NUMBER_OF_PINS =sizeof(pins)/sizeof(pins[0]);constint NUMBER_OF_LEDS = NUMBER_OF_PINS *(NUMBER_OF_PINS-1);
byte pairs[NUMBER_OF_LEDS/2][2]={{2,1},{1,0},{2,0}};// Сопоставляем// контакты светодиодамint ledStates =0;// Переменная для хранения состояний для 15 светодиодовint refreshedLed;// Светодиод, состояние которого обновляетсяvoid setup(){// Ничего не делаем}void loop(){constint analogInPin =0;// Входной аналоговый контакт// для подключения потенциометра// Код из решения для создания линейного индикатораint sensorValue = analogRead(analogInPin);// Считываем входной// аналоговый сигнал// Сопоставляем полученное значение количеству светодиодовint ledLevel = map(sensorValue,0,1023,0, NUMBER_OF_LEDS);for(int led =0; led < NUMBER_OF_LEDS; led++){if(led < ledLevel ){
setState(led, HIGH);// Включаем светодиоды ниже полученного уровня}else{
setState(led, LOW);// Выключаем светодиоды выше уровня}}
ledRefresh();}void setState(int led, bool state){
bitWrite(ledStates,led, state);}void ledRefresh(){// При каждом вызове обновляем состояние другого светодиодаif( refreshedLed++> NUMBER_OF_LEDS)// Переходим к следующему светодиоду
refreshedLed =0;// Повторяем, начиная с первого светодиода,// если все были обновленыif( bitRead(ledStates, refreshedLed )== HIGH)
lightLed( refreshedLed );elseif(refreshedLed ==0)// Выключаем все светодиоды, если контакт 0 выключенfor(int i=0; i < NUMBER_OF_PINS; i++)
digitalWrite(pins[i],LOW);}// Эта функция идентична функции из скетча решения// Она включает требуемый светодиод. Нумерация светодиодов// начинается с 0void lightLed(int led){// Следующие четыре строки кода преобразовывают номер светодиода// в номера контактовint indexA = pairs[led/2][0];int indexB = pairs[led/2][1];int pinA = pins[indexA];int pinB = pins[indexB];// Выключаем все контакты, не подключенные к заданному светодиодуfor(int i=0; i < NUMBER_OF_PINS; i++){if(pins[i]!= pinA && pins[i]!= pinB){// Если этот контакт не является одним из требуемых
pinMode(pins[i], INPUT);// Задаем для него входной режим работы
digitalWrite(pins[i],LOW);// Отключаем повышающий резистор}}// Теперь включаем контакты для требуемого светодиода
pinMode(pinA, OUTPUT);
pinMode(pinB, OUTPUT);if( led %2==0){
digitalWrite(pinA,LOW);
digitalWrite(pinB,HIGH);}else{
digitalWrite(pinB,LOW);
digitalWrite(pinA,HIGH);}}
/* Скетч создания линейного индикатора с использованием чарлиплексирования
*/
byte pins[] = {2,3,4};
const int NUMBER_OF_PINS = sizeof(pins)/ sizeof(pins[0]);
const int NUMBER_OF_LEDS = NUMBER_OF_PINS * (NUMBER_OF_PINS-1);
byte pairs[NUMBER_OF_LEDS/2][2] = { {2,1}, {1,0}, {2,0} }; // Сопоставляем
// контакты светодиодам
int ledStates = 0; // Переменная для хранения состояний для 15 светодиодов
int refreshedLed; // Светодиод, состояние которого обновляется
void setup()
{
// Ничего не делаем
}
void loop()
{
const int analogInPin = 0; // Входной аналоговый контакт
// для подключения потенциометра
// Код из решения для создания линейного индикатора
int sensorValue = analogRead(analogInPin); // Считываем входной
// аналоговый сигнал
// Сопоставляем полученное значение количеству светодиодов
int ledLevel = map(sensorValue, 0, 1023, 0, NUMBER_OF_LEDS);
for (int led = 0; led < NUMBER_OF_LEDS; led++)
{
if (led < ledLevel )
{
setState(led, HIGH); // Включаем светодиоды ниже полученного уровня
}
else
{
setState(led, LOW); // Выключаем светодиоды выше уровня
}
}
ledRefresh();
}
void setState( int led, bool state)
{
bitWrite(ledStates,led, state);
}
void ledRefresh()
{
// При каждом вызове обновляем состояние другого светодиода
if( refreshedLed++ > NUMBER_OF_LEDS) // Переходим к следующему светодиоду
refreshedLed = 0; // Повторяем, начиная с первого светодиода,
// если все были обновлены
if( bitRead(ledStates, refreshedLed ) == HIGH)
lightLed( refreshedLed );
else
if(refreshedLed == 0) // Выключаем все светодиоды, если контакт 0 выключен
for(int i=0; i < NUMBER_OF_PINS; i++)
digitalWrite(pins[i],LOW);
}
// Эта функция идентична функции из скетча решения
// Она включает требуемый светодиод. Нумерация светодиодов
// начинается с 0
void lightLed(int led)
{
// Следующие четыре строки кода преобразовывают номер светодиода
// в номера контактов
int indexA = pairs[led/2][0];
int indexB = pairs[led/2][1];
int pinA = pins[indexA];
int pinB = pins[indexB];
// Выключаем все контакты, не подключенные к заданному светодиоду
for(int i=0; i < NUMBER_OF_PINS; i++)
{
if(pins[i] != pinA && pins[i] != pinB)
{
// Если этот контакт не является одним из требуемых
pinMode(pins[i], INPUT); // Задаем для него входной режим работы
digitalWrite(pins[i],LOW); // Отключаем повышающий резистор
}
}
// Теперь включаем контакты для требуемого светодиода
pinMode(pinA, OUTPUT);
pinMode(pinB, OUTPUT);
if( led % 2 == 0)
{
digitalWrite(pinA,LOW);
digitalWrite(pinB,HIGH);
}
else
{
digitalWrite(pinB,LOW);
digitalWrite(pinA,HIGH);
}
}
В скетче используются значения битов переменной ledStates для представления состояния светодиодов (0 — выключен, 1 — включен). Функция refresh() проверяет значение каждого бита и включает светодиоды для битов со значением 1. Вызов функции должен осуществляться быстро и постоянно, чтобы избежать мигания светодиодов.
Добавление пауз в код может оказывать отрицательное воздействие на эффект инерционности зрительного восприятия, благодаря которому человеческий глаз не замечает мигания светодиодов, включающихся и выключающихся с высокой частотой, а воспринимает их как постоянно включенные.
Вместо явного вызова функции refresh() для обновления состояния светодиодов ее можно вызывать посредством прерывания. Прерывания по таймеру подробно рассматриваются в главе 18, но в листинге 7.15 приводится пример одного способа использования прерывания для обновления состояния светодиодов. Для создания прерываний в скетче задействована библиотека FrequencyTimer2, устанавливаемая с помощью Менеджера библиотек (установка библиотек сторонних разработчиков подробно рассматривается в разд. 16.2).
Листинг 7.15. Обновление состояния светодиодов с использованием прерывания
#include <FrequencyTimer2.h> // Библиотека для создания прерывания
byte pins[]={2,3,4};constint NUMBER_OF_PINS =sizeof(pins)/sizeof(pins[0]);constint NUMBER_OF_LEDS = NUMBER_OF_PINS *(NUMBER_OF_PINS-1);
byte pairs[NUMBER_OF_LEDS/2][2]={{2,1},{1,0},{2,0}};int ledStates =0;//Переменная для хранения состояний для 15 светодиодовint refreshedLed;// Светодиод, состояние которого обновляетсяvoid setup(){
FrequencyTimer2::setPeriod(20000/NUMBER_OF_LEDS);// Задаем период// Следующая строка кода указывает объекту FrequencyTimer2,// какую функцию нужно вызывать (ledRefresh)
FrequencyTimer2::setOnOverflow(ledRefresh);
FrequencyTimer2::enable();}void loop(){constint analogInPin =0;// Входной аналоговый контакт// для подключения потенциометра// Код из решения для создания линейного индикатораint sensorValue = analogRead(analogInPin);// Считываем входной аналоговый сигнал// Сопоставляем полученное значение количеству светодиодовint ledLevel = map(sensorValue,0,1023,0, NUMBER_OF_LEDS);for(int led =0; led < NUMBER_OF_LEDS; led++){if(led < ledLevel ){
setState(led, HIGH);// Включаем светодиоды ниже полученного уровня}else{
setState(led, LOW);// Выключаем светодиоды выше уровня}}// Состояние светодиода обновляется теперь не в цикле loop(),// а при обработке прерывания, создаваемого FrequencyTimer2}// Остальной код такой же, что и в предыдущем примере
#include <FrequencyTimer2.h> // Библиотека для создания прерывания
byte pins[] = {2,3,4};
const int NUMBER_OF_PINS = sizeof(pins)/ sizeof(pins[0]);
const int NUMBER_OF_LEDS = NUMBER_OF_PINS * (NUMBER_OF_PINS-1);
byte pairs[NUMBER_OF_LEDS/2][2] = { {2,1}, {1,0}, {2,0} };
int ledStates = 0; //Переменная для хранения состояний для 15 светодиодов
int refreshedLed; // Светодиод, состояние которого обновляется
void setup()
{
FrequencyTimer2::setPeriod(20000/NUMBER_OF_LEDS); // Задаем период
// Следующая строка кода указывает объекту FrequencyTimer2,
// какую функцию нужно вызывать (ledRefresh)
FrequencyTimer2::setOnOverflow(ledRefresh);
FrequencyTimer2::enable();
}
void loop()
{
const int analogInPin = 0; // Входной аналоговый контакт
// для подключения потенциометра
// Код из решения для создания линейного индикатора
int sensorValue = analogRead(analogInPin); // Считываем входной аналоговый сигнал
// Сопоставляем полученное значение количеству светодиодов
int ledLevel = map(sensorValue, 0, 1023, 0, NUMBER_OF_LEDS);
for (int led = 0; led < NUMBER_OF_LEDS; led++)
{
if (led < ledLevel )
{
setState(led, HIGH); // Включаем светодиоды ниже полученного уровня
}
else
{
setState(led, LOW); // Выключаем светодиоды выше уровня
}
}
// Состояние светодиода обновляется теперь не в цикле loop(),
// а при обработке прерывания, создаваемого FrequencyTimer2
}
// Остальной код такой же, что и в предыдущем примере
Объект FrequencyTimer2 устанавливает период прерывания величиной 1666 микросекунд (20 мс, разделенное на количество светодиодов, равное 12). Затем методу FrequencyTimer2::setOnOverflow указывается функция (ledRefresh), которую нужно вызывать при каждом активировании таймера. Библиотека FrequencyTimer2 совместима с ограниченным количеством плат: Arduino Uno (и, скорее всего, с большинством плат с микроконтроллером ATmega328), Arduino Mega и с несколькими версиями платы Teensy. Более подробная информация по этой библиотеке предлагается на веб-сайте PJRC (https://oreil.ly/e-KTE).
Дополнительная информация
Прерывания по таймеру более подробно рассматриваются в главе 18.
Потенциометром называется регулируемый делитель напряжения, который — в отличие от реостата — служит для регулировки напряжения при почти неизменном токе. Снимаемое с подвижного отводного контакта потенциометра напряжение (рис. S9.1) в зависимости от текущего положения подвижного контакта может изменяться от нуля до максимального значения, равного приложенному к потенциометру напряжению.
Проще всего считывать аналоговый сигнал, выдаваемый простым потенциометром. Скорей всего, вы даже не подозреваете, что буквально окружены потенциометрами, поскольку эти устройства имеются в стереосистемах, радиоприемниках, термостатах, автомобилях и во многих других бытовых устройствах. С электрической точки зрения потенциометр представляет собой делитель напряжения (которые рассматриваются далее в этой главе), а физически они обычно выглядят наподобие небольшого цилиндра с ручкой посредине. Но это относится только к традиционным потенциометрам, поскольку в целом они могут быть любого размера и любой формы. Но все потенциометры имеют три вывода. Один из крайних выводов подключается на землю, а другой на контакт положительного питания 5 В платы Arduino. Потенциометры симметричны, поэтому не имеет значения, какой из его крайних выводов подключать к земле, а какой к питанию 5 В. Средний вывод потенциометра подключается к контакту А0 разъема аналоговых входов платы Arduino. Это иллюстрируется монтажной схемой на рис. 3.3.
Рис. 3.3. Монтажная схема подключения потенциометра к плате Arduino (Рисунок создан в программе Fritzing)
Вращение ручки потенциометра меняет напряжение на его среднем выводе (которое подается на аналоговый вход 0 платы Arduino) в диапазоне от 0 до 5 В. В этом можно убедиться, подключив вольтметр к выводам потенциометра, как показано на рис. 3.4, и наблюдая за показаниями на дисплее при повороте ручки потенциометра. Красный (положительный) щуп вольтметра подключается к среднему выводу потенциометра, а черный (отрицательный) к тому крайнему выводу потенциометра, который соединен с землей. Ваш потенциометр и вольтметр могут отличаться от изображенных на рис. 3.4.
Рис. 3.4. Измерение варьирующегося напряжения на потенциометре
Прежде чем использовать выходной сигнал потенциометра для управления каким-либо другим устройством, мы реализуем более простой проект — передачу цифровых данных для текущих аналоговых значений выходного напряжения потенциометра на компьютер и вывод их на экран. Для этого нам потребуется средство последовательной связи платы Arduino. Соответствующая программа приведена в листинге 3.1. Откройте в среде IDE Arduino файл программы и загрузите его в Arduino. Функция analogRead() считывает входной сигнал, подаваемый с потенциометра на контакт Arduino, а функция Serial.println() выводит соответствующие цифровые значения в окно монитора порта среды IDE Arduino.
Листинг 3.1. Программа pot.ino для считывания выходного аналогового сигнала потенциометра
// Программа для считывания выходного аналогового сигнала// потенциометраconstint POT=0;// Сигнал с потенциометра подается на аналоговый// контакт 0int val =0;// Переменная для хранения аналогового значения,// полученного с контакта POTvoid setup(){
Serial.begin(9600);}void loop(){
val = analogRead(POT);
Serial.println(val);
delay(500);}
// Программа для считывания выходного аналогового сигнала
// потенциометра
const int POT=0; // Сигнал с потенциометра подается на аналоговый
// контакт 0
int val = 0; // Переменная для хранения аналогового значения,
// полученного с контакта POT
void setup()
{
Serial.begin(9600);
}
void loop()
{
val = analogRead(POT);
Serial.println(val);
delay(500);
}
Мы исследуем функциональность последовательного интерфейса более подробно в последующих главах. На данном же этапе нам нужно знать только то, что последовательный интерфейс с компьютером необходимо запустить в функции setup(). Функция запуска Serial.begin() принимает один аргумент, значение которого указывает скорость передачи данных в бодах, или в битах в секунду. Более высокая скорость передачи данных позволяет передавать больший объем данных за меньший период времени, но в некоторых системах может также вызывать искажения передаваемых данных Обычно применяют скорость 9600 бод, которую мы и будем назначать для большинства проектов в этой книге.
При каждой итерации цикла loop() переменной val присваивается текущее цифровое значение, выдаваемое АЦП в ответ на амплитуду аналогового сигнала на контакте А0. Команде analogRead() необходимо передавать номер контакта АЦП. В данном случае это контакт А0, так как именно к нему мы подключили вывод потенциометра. Вообще то, если действовать строго по правилам, то следовало бы указать аргумент A0, обозначив тем самым аналоговый контакт, но поскольку функция analogRead() может работать только с аналоговыми контактами, то она “знает”, что 0 обозначает A0. После получения цифрового значения уровня входного сигнала (числа в диапазоне от 0 до 1023) функция Serial.println() передает это значение в программу монитора порта среды Arduino IDE, которая отображает его в своем окне, с последующим выводом символа новой строки для перевода курсора на следующую строку. Затем исполнение кода в цикле приостанавливается на 500 мс (чтобы предотвратить слишком быструю прокрутку чисел в окне монитора порта), после чего процесс повторяется.
Вы можете обратить внимание, что при исполнении этой программы светодиод платы Arduino мигает каждые 500 мс (по крайней мере, он должен так мигать). Такое мигание этого светодиода означает, что плата Arduino передает через USB-интерфейс данные программе последовательного терминала на компьютере. Для приема передаваемых платой Arduino данных можно использовать одну из множества программ последовательного терминала, но проще всего работать с программой монитора порта среды Arduino IDE. Запустите эту программу, щелкнув по значку увеличительного стекла справа на панели инструментов редактора скетчей (рис. 3.5).
В открывшемся окне монитора порта должны отображаться числа, посылаемые ему платой Arduino. Поверните ручку потенциометра в ту или иную сторону. Отображаемые числа должны уменьшаться или увеличиваться, в зависимости от направления поворота. Если повернуть ручку до предела в одном направлении, то в окне монитора порта будет выводиться значение 0, а если в другом направлении — число 1023 (или наоборот). На рис. 3.6 показан пример окна монитора порта с отображаемыми в нем значениями.
Примечание
Если вместо аккуратного столбца чисел в окне монитора порта отображается набор бессмысленных символов, проверьте значение скорости передачи данных, установленное для монитора порта. Поскольку в программе мы задали скорость 9600 бод, в мониторе порта нужно задать такую же скорость. Для этого следует выбрать это значение в выпадающем списке в правом нижнем углу окна монитора порта.
Рис. 3.6. Окно монитора порта с отображаемыми в нем входными данными
Новизна управления внешним устройством, пусть хотя бы только с помощью отображения чисел на экране компьютера, наверное, вам вскоре приестся. Ничего страшного. Это всего лишь наш первый шаг в долгом процессе изучения датчиков. Следующим делом мы рассмотрим другие типы аналоговых датчиков и применение выдаваемых ими данных для управления другими устройствами. На данном этапе в качестве управляемого устройства мы опять возьмем знакомый нам светодиод, но в последующих главах мы заменим его электродвигателями и другими исполнительными устройствами.
Фоторезистор обычно представляет собой компонент в виде диска с двумя выводами. Когда освещенность поверхности этого диска увеличивается, сопротивление между выводами уменьшается. Некоторые фоторезисторы в темноте обладают сопротивлением до 10 мОм, а при ярком освещении у некоторых из них сопротивление может снижаться до 500 Ом. Вы всегда сможете определить эти параметры для вашего компонента опытным путем.
Рис. S10.1. Устройство фоторезистора (слева) и модуль KY-018 (справа)
Схема подключения
Для удобного подключения фоторезисторов существуют специальные платы (с отверстиями для крепления), на которых интегрирован фоторезистор и понижающий резистор (рис. 2.10, справа). Кроме того, там может быть размещен триммер настройки. Модуль может иметь кроме аналогового выхода дополнительный цифровой, которые будет фиксировать факт изменения освещенности выше «настроенного порога».
Пассивный инфракрасный датчик движений HC-SR501 (PIR, Passive Infrared) фиксирует движения объектов. Матрица из 15-ти небольших линз фокусирует ИК-излучение из разных участков окружающего пространства на пироэлектрический детектор, основу которого составляет пластина из танталата лития, вырабатывающая небольшое напряжение в ответ на поступающее тепловое излучение. При перемещении объекта из одной зоны в другую генерируется выходной сигнал (рис. S2.1, S2.2).
Основные характеристики
Наименование
Значение
Постоянное напряжение, В
4,5 ÷ 20
Ток потребления в режиме ожидания
менее 50 мкА
Наибольший потребляемый ток во время работы, мА
65
Напряжение логических уровней, В
3,3
Расстояние обнаружения
3 ÷ 7 м, по умолчанию 7 м
Максимальный угол обнаружения 110° на расстоянии 7 м
120°
Время поддержания высокого уровня выхода при присутствии
20 ÷ 300 с
Время игнорирования событий после фиксации
0,2 с
Температура окружающего воздуха при работе
–15 ÷ 70°C
Размеры, мм
32×24×28
Внешний вид, назначение контактов
Рис. S2.1. Датчик HC-SR501
Рис. S2.2. Поле зрения датчика PIR с линзой Френеля
Схема подключения
Рис. S2.3. Схема подключения датчика движения
Программный код
Листинг S2.1. Обнаружение перемещения датчиком движения HC-SR501
Датчики DHT11/DHT22 (рис. S1.1) предназначены для измерения температуры и влажности воздуха.
Внешний вид, назначение контактов
Рис. S1.1. Датчик DHT11
Основные характеристики
Наименование
Значение
DHT11
DHT22
Диапазон измерения температуры/точность измерения
0 ÷ 50°С /±2°C
–40 ÷ 125°С /±0,5°C
Диапазон измерения относительной влажности
20 ÷ 80% /±5°C
0 ÷ 100% /±2 ÷ 5°C
Частота опроса датчиков, Гц
1 (один раз в секунду)
0,5 (1 раз в две секунды)
Размеры, мм
15,5×12,0×5,5
15,1×25,0×7,7
Напряжение питания, В
3 ÷ 5
3 ÷ 5
Максимальный ток, мА
2,5
2,5
Схема подключения
Рис. S1.2. Подключение датчика DHT11
Внимание!
Если расстояние от датчика до Arduino небольшое, рекомендуемый номинал резистора R1 10 кОм (рис. S1.2), а для расстояния больше 20 метров, рекомендуется резистор номиналом 5,1 кОм. Если же датчик имеет только три контакта (5V, OUT и GND), то резистор R1 вообще не нужен.
Программный код
Загрузите библиотеку DHT-sensor-library для работы Arduino с датчиками DHT11 и DHT22. Для этого откройте Менеджер библиотек, выполнив команду Инструменты | Управлять библиотеками. Справа вверху в строке поиска введите DHT. В открывшемся списке выберите DHT sensor library by Adafruit версии 1.2.3 (в версиях 1.3.0 и новее возможна ошибка при компиляции). Нажмите кнопку Установка.
Датчик уровня жидкости предназначен для определения уровня жидкости в различных емкостях. На датчике расположен резисторы, транзистор и чередующиеся оголенные проводящие контакты (рис. S6.1). Чем глубже датчик погружен в воду (большая часть длины контактов находится в воде), тем меньше сопротивление между проводящими контактами.
Внешний вид, назначение контактов
Рис. S6.1. Датчик уровня воды (глубины)
Основные характеристики
Наименование
Значение
Зона обнаружения, мм
16 ÷ 40
Напряжение питания, В
3,3 ÷ 5
Ток потребления, мА
20
Размеры, мм
62×20×8
Рабочая температура, °С
10 ÷ 30
Схема подключения
Рис. S6.2. Схема подключения датчика уровня жидкости
#define aPin A0 // пин для подключения аналогового выхода датчикаint avalue=0;//переменнаяint levels[3]={600,500,400};// значение уровнейvoid setup(){
pinMode(aPin, INPUT);// настройка аналогового пина на вход
Serial.begin(9600);// инициализация последовательного порта}void loop(){// получение значения с аналогового вывода датчика
avalue=analogRead(aPin);// вывод значения в монитор последовательного порта Arduino
Serial.print("avalue=");
Serial.print(avalue);if(avalue>=levels[0]) Serial.println("->MAX");if((avalue>levels[2])&&(avalue<levels[0])) Serial.println("->NORM");if(avalue<=levels[2]) Serial.println("->MIN");// пауза перед следующим получением значения 1000 мс
delay(1000);}
#define aPin A0 // пин для подключения аналогового выхода датчика
int avalue=0; //переменная
int levels[3]={600,500,400}; // значение уровней
void setup(){
pinMode(aPin, INPUT); // настройка аналогового пина на вход
Serial.begin(9600); // инициализация последовательного порта
}
void loop(){
// получение значения с аналогового вывода датчика
avalue=analogRead(aPin);
// вывод значения в монитор последовательного порта Arduino
Serial.print("avalue=");
Serial.print(avalue);
if (avalue>=levels[0]) Serial.println("->MAX");
if ((avalue>levels[2])&&(avalue<levels[0])) Serial.println("->NORM");
if (avalue<=levels[2]) Serial.println("->MIN");
// пауза перед следующим получением значения 1000 мс
delay(1000);
}
Результаты измерений
Значение аналоговых сигналов на аналоговом входе Arduino вы можете определить экспериментальным путем. Они могут оказаться такими, как показано на рис. S6.3.
Рис. S6.3. Экспериментальные значения аналоговых сигналов для разных уровней погружения
Датчик пламени позволяет фиксировать наличие инфракрасного излучения (открытого пламени) в диапазоне волн 760 ÷ 1100 нм в прямой видимости на расстоянии до 1 м (рис. S4.1).
Рис. S4.1. Принцип действия ИК-датчика пламени YG1006
Основные характеристики
Наименование
Значение
Дальность обнаружения пламени, см
20 ÷ 100
Угол обнаружения пламени, град
60
Длина волны, нм
760 ÷ 1100
Пиковая длина волны, нм
940
Напряжение питания, В
3 ÷ 5,5
Потребляемый ток не более, мА
15
Размеры (длина × ширина), мм
36×16
Схема подключения
При подключении датчика только к цифровому выходу (D0) фиксируется лишь факт наличия пламени. А при подключении к аналоговому выходу (A0) можно оценить и яркость пламени.
Датчик звука, как следует из названия, предназначен для обнаружения звука (фиксирует появление громкого звука). На рис. S8.1 показаны наиболее широко применяемые в проектах Arduino датчики звука.
Внешний вид, назначение контактов
Рис. S8.1. Применяемые в проектах Arduino датчики звука
Схема подключения
Работая с датчиком звука, можно использовать как цифровой, так и аналоговый его выходы (рис. S8.2). Аналоговый выход выдает значение сигнала микрофона, а цифровой выход передает 1, если сигнал превысит пороговое значение, и 0 в противном случае. Пороговое значение можно настроить с помощью потенциометра, расположенного на плате датчика. Можно подключить одновременно и два выхода (например, для настройки порогового значения).
Рис. S8.2. Схема подключения датчика звука
Программный код
Листинг S1.8. Измерение громкости с помощью датчика звука
#define soundAnalogPin A0 // пин, к которому аналоговый выход#define soundDigitalPin 4 // пин, к которому цифровой выходint analogVal=0;// Объявляем переменные для хранения значенийint digitalVal=0;//с датчика и задаем ее начальное значение 0void setup(){
Serial.begin(9600);// Открываем монитор порта
pinMode(soundAnalogPin, INPUT);//Настройка аналогового пина на вход
pinMode(soundDigitalPin, INPUT);// Настройка цифрового пина на вход}void loop(){//присваиваем переменной аналоговое значение
analogVal =analogRead(soundAnalogPin);//присваиваем переменной цифровое значение
digitalVal=digitalRead(soundDigitalPin);//Выводим полученныес датчика значения
Serial.print("Sound value A0: ");//
Serial.print(analogVal,DEC);
Serial.print(" D0: ");
Serial.println(digitalVal,DEC);
delay(100);//задаем паузу}
#define soundAnalogPin A0 // пин, к которому аналоговый выход
#define soundDigitalPin 4 // пин, к которому цифровой выход
int analogVal=0; // Объявляем переменные для хранения значений
int digitalVal=0; //с датчика и задаем ее начальное значение 0
void setup()
{
Serial.begin(9600); // Открываем монитор порта
pinMode(soundAnalogPin, INPUT);//Настройка аналогового пина на вход
pinMode(soundDigitalPin, INPUT);// Настройка цифрового пина на вход
}
void loop()
{
//присваиваем переменной аналоговое значение
analogVal =analogRead(soundAnalogPin);
//присваиваем переменной цифровое значение
digitalVal=digitalRead(soundDigitalPin);
//Выводим полученныес датчика значения
Serial.print("Sound value A0: "); //
Serial.print(analogVal,DEC);
Serial.print(" D0: ");
Serial.println(digitalVal,DEC);
delay(100); //задаем паузу
}
Результат
Рис. S8.3. Фиксация хлопка в ладоши на мониторе последовательного порта
Датчик газа MQ-135 предназначен для измерения наличия в окружающем воздухе вредных примесей газа. В качестве чувствительного элемента в датчике служит пластина диоксида олова (Sn02), который имеет низкую проводимость в чистом воздухе. Когда датчик оказывается в среде с парами токсичных газов, его проводимость возрастает. Датчик MQ-135 очень чувствителен к аммиаку, сульфидам, парам бензола и алкоголя, СО2 и идеально подходит для мониторинга дыма и других вредных примесей в воздухе. На рис. S7.1 показано изменение сопротивления датчика в зависимости от концентрации различных газов в окружающем воздухе в миллионных долях (от общего объема газа).
Внешний вид, назначение контактов
Рис. S7.1. Изменение сопротивления датчика в зависимости от концентрации различных газов в окружающем воздухе
Основные характеристики
Наименование
Значение
Напряжение питания, В
5
Потребляемый ток, мА
160
Рабочая температура, °С
10 ÷ 45
Относительная влажность, %
менее 95
Концентрация кислорода в воздухе (стандартная), %
21
Стандартная температура измерения, °С
20
Влажность, %
65
Диапазон измерений
аммиак: 10 ppm ÷ 300 ppm
бензин: 10 ppm ÷ 1000 ppm
этиловый спирт: 10 ppm ÷ 300 ppm
Схема подключения
Рис. S7.2. Подключение датчика газа
Программный код
Листинг S7.1. Измерение концентрации газов с помощью датчика MQ-135
#define MQPin A0 //пин, к которому подключен датчик газа#define ledPin 13 //пин встроенного светодиодаint sensorValue =0;//переменная для хранения значенийvoid setup(){
Serial.begin(9600);
pinMode(ledPin, OUTPUT);
Serial.println("MQ135 Test");//Посылаем текст в монитор порта}void loop(){// Считываем значения с датчика
sensorValue = analogRead(MQPin);if(sensorValue >=400)// и, если превышен заданный порог,{
digitalWrite(ledPin, HIGH);// то включаем светодиод,}else// а если нет…{
digitalWrite(ledPin, LOW);// то выключаем}
Serial.print("MQ135 value= ");// Для отслеживания данных с датчиков// транслируем их в монитор порта
Serial.println(sensorValue);
delay(1000);}
#define MQPin A0 //пин, к которому подключен датчик газа
#define ledPin 13 //пин встроенного светодиода
int sensorValue = 0; //переменная для хранения значений
void setup() {
Serial.begin(9600);
pinMode(ledPin, OUTPUT);
Serial.println("MQ135 Test" ); //Посылаем текст в монитор порта
}
void loop() {
// Считываем значения с датчика
sensorValue = analogRead(MQPin);
if (sensorValue >= 400)
// и, если превышен заданный порог,
{
digitalWrite(ledPin, HIGH); // то включаем светодиод,
}
else // а если нет…
{
digitalWrite(ledPin, LOW); // то выключаем
}
Serial.print("MQ135 value= " );
// Для отслеживания данных с датчиков
// транслируем их в монитор порта
Serial.println(sensorValue);
delay(1000);
}