23.02.2018
В далёком 3-ем семестре (то есть ещё в начале второго курса) у нас в институте организовали вечерние занятия по программированию микроконтроллеров на базе платформ LEGO Mindstorms и Arduino. Нам было слишком лень на них ходить (такая уж у нас группа ленивых раздолбаев ¯\_(ツ)_/¯ ), но на нескольких занятиях я всё же побывал и немного поиграться с Ардуинкой успел (хотя всё равно большую часть времени провёл в эмуляторе). С тех времён у меня осталась пара проектов на Circuits.io (которые потом переехали на Tinkercad).
Первым из них был простенький, но полноценный светофор с соблюдением всех фаз.
В то время я почему-то радовался получившемуся коду, но сейчас он выглядит очень примитивно:
// Константы с номерами портов
const int redLight = 8;
const int yellowLight = 9;
const int greenLight = 10;
// Счётчик секунд.
int counter = 0;
// Эта функция вызывается при запуске микроконтроллера...
void setup() {
// Настраиваем порты на вывод сигнала.
pinMode(redLight, OUTPUT);
pinMode(yellowLight, OUTPUT);
pinMode(greenLight, OUTPUT);
}
// ...а эта постоянно вызывается из вечного цикла во время его работы.
void loop() {
// Красный свет
if (counter >= 0 && counter < 15) {
digitalWrite(redLight, HIGH); // включаем светодиод
digitalWrite(yellowLight, LOW); // выключаем светодиод
digitalWrite(greenLight, LOW);
}
// Красный и жёлтый сигналы светофора
else if (counter >= 15 && counter < 20) {
digitalWrite(yellowLight, HIGH);
}
// Зелёный свет
else if (counter >= 20 && counter < 31) {
digitalWrite(redLight, LOW);
digitalWrite(yellowLight, LOW);
digitalWrite(greenLight, HIGH);
}
// Мигающий зелёный
else if (counter >= 31 && counter < 35) {
if (counter % 2 == 0) {
digitalWrite(greenLight, HIGH);
} else {
digitalWrite(greenLight, LOW);
}
}
// Жёлтый свет
else if (counter >= 35 && counter < 40) {
digitalWrite(yellowLight, HIGH);
digitalWrite(greenLight, LOW);
// Сбрасываем счётчик
if (counter == 39) {
counter = -1;
}
}
counter++;
delay(1000); // секундная задержка
}
Ссылка в заголовке ведёт на страницу проекта на Tinkercad'е. Этот сервис позволяет создавать несложные схемы, писать программы для них и запускать эмуляцию! К сожалению, чтоб запустить на выполнение чужой проект, нужно сначала зарегистрироваться и скопировать его на свой аккаунт. Печально, что всё так сложно, но ничего не поделаешь :(
Переливающийся цветами светодиод
После создания светофора мне захотелось поиграться с цветным светодиодом. У него всё-таки целых 4 ноги, управлять которыми надо через широтно-импульсную модуляцию. Однако у меня не хватило фантазии придумать что-то лучше светодиода, переливающегося из красного в зелёный, из зелёного в синий, а затем обратно в красный и так далее по кругу. Посмотреть, как это выглядит, можно опять же на Tinkercad'е (если есть аккаунт), а код я снова продублирую здесь с русскими комментариями:
// Константы с номерами портов
const int redPin = 5;
const int greenPin = 6;
const int bluePin = 10;
// Компоненты цвета
int red = 0;
int green = 0;
int blue = 0;
// Эта функция вызывается при запуске микроконтроллера...
void setup() {
pinMode(redPin, OUTPUT);
pinMode(greenPin, OUTPUT);
pinMode(bluePin, OUTPUT);
}
// ...а эта постоянно вызывается из вечного цикла во время его работы.
void loop() {
// Увеличиваем красную компоненту
if (red < 255 && green == 0) {
if (blue > 0) {
analogWrite(bluePin, --blue);
}
analogWrite(redPin, ++red);
}
// Увеличиваем зелёную и уменьшаем красную
else if (green < 255 && blue == 0) {
analogWrite(redPin, --red);
analogWrite(greenPin, ++green);
}
// Увеличиваем синию и уменьшаем зелёную
else if (blue < 255) {
analogWrite(greenPin, --green);
analogWrite(bluePin, ++blue);
}
// небольшая задержка
delay(10);
}
Collecting all things together...
А теперь мы резко переносимся на два года вперёд и попадаем на четвёртый курс. Я уже полгода-год состою во флудилке DC, а среди предметов числится «Микропроцессорная техника». Самое время тряхнуть стариной! Тем более, следуя советам знающих людей, ещё летом на Aliexpress я прикупил пару китайских Arduino Nano, ЖК-дисплей с I2C-адаптером, макетную плату и целую кучу всякой электронной мелочовки (светодиоды, «резюки» на 1 кОм, проводочки и т. п.).
Дополнительным усложняющим условием стал запрет на использование Arduino IDE. Ну ладно, на самом деле это оказалось не совсем так, как я понял изначально. Ей рекомендовали не пользоваться, потому что она неудобная и не умеет даже в автодополнение. Но совет всё равно не становится менее странным для новичка и «виндузятника», потому что с нуля настроить окружение для компиляции и сборки кода на C/C++ с последующей прошивкой микроконтроллера — ни разу не тривиальная задача. Тем более что надо ещё и как-то вручную библиотеки подключать. А без них нужно учиться не только писать на чистой «сишечке», но ещё и на довольно низком уровне, работая напрямую с регистрами микроконтроллера по его спецификации (даташиту).
Проект я реализовывал постепенно: сначала всё настроил и заставил работать светофор, затем уже прикрутил цветной светодиод. Поскольку мне было жалко разбирать схему первого, то когда они оба оказались на макетной плате, я решил заставить их работать вместе одновременно. Тем более, что к этому времени я уже много знал про таймеры и прерывания (осиливать даташит было непросто, но вполне реально, опираясь на множество туториалов и примеров готовых программ в интернете).
В итоге у меня получилась следующая схема:
inb4: ножки припаяны просто ужасно
Кодировать будем всё на чистом Си, как завещал батя. Весь проект оформлен в виде полноценного git-репозитория, ссылкой на который является заголовок раздела. Я работал над ним в основном под Windows, но собирать под Linux тоже довелось, так что рассмотрим сначала, какие инструменты нам потребуются:
ОС | Инструкции по настройке инструментария для сборки и прошивки |
---|---|
Windows |
Писать код и собирать прошивку я предлагаю в официальной IDE от разработчиков микроконтроллера — AtmelStudio, основанной на Visual Studio. Там есть все библиотеки, компилятор и линковщик. Как прошивать в ней саму Ардуину по USB я так и не понял, поэтому использовал внешнюю программу SinaProg, представляющую собой графическую оболочку для AVRDude. |
Linux (Debian 8) |
Для начала нужно поставить все необходимые пакеты: gcc-avr (компилятор), avr-libc (стандартная библиотека языка Си), binutils-avr (набор утилит, в который входят, например, линковщик и avr-objcopy, позволяющий сконвертировать elf-файл в hex-файл), avrdude (утилита для загрузки программы в микроконтроллер (прошивки)). Скомпилировать проект можно следующей командой: avr-gcc -Os -std=c99 -mmcu=atmega328p -o firmware.elf main.c initialization.c trafficlight.c
На выходе получится elf-файл, который для прошивки необходимо сконвертировать в hex-файл: avr-objcopy -j .text -j .data -O ihex firmware.elf firmware.hex
После чего наконец-то можно загрузить получившуюся программу в микроконтроллер:
# При наличии нескольких подключенных устройств может понадобиться смена ttyUSB0 на другое устройство.
На самом деле, если будете клонировать проект из репозитория, то там уже есть два готовых shell-скрипта: build.sh собирает проект, а upload.sh загружает прошивку в микроконтроллер (если загружать нечего, автоматически вызывает сборку). Так что для сборки и прошивки микроконтроллера просто нужно выполнить следующую команду:
|
Дополнено 24.02.2018: На всякий случай решил приложить файл с уже собранной прошивкой, который остаётся только загрузить в Arduino. Сборка производилась на 32-разрядной версии Debian 8.
Дополнено 26.09.2018: Обновлённая версия прошивки, где используется уход в спящий режим вместо холостого цикла в основном цикле. Сборка производилась на 64-разрядной версии Windows 10.
Дальше будет большая выжимка кусков кода с пояснениями из отчёта о лабораторном практикуме. Вполне допускаю, что начинающим многие вещи будут непонятны. Но поскольку я объяснять не умею, то могу только направить к другим веб-сайтам, которые подскажет поисковая система. Для отправной точки посоветую эту статью про порты и регистры.
В микроконтроллере ATmega328/P (согласно даташиту) 3 таймера и 6 пинов с аппаратным ШИМом (по 2 на каждый таймер). Использовать будем 8-битные таймеры под номерами 0 и 2. К первому относится пин D6, а ко второму – пины D11 и D3. При этом надо помнить, что пины D6 и D3 на уровне микроконтроллера относятся к порту D (PD6 и PD3 соответственно), а D11 – к порту B (PB3). Светофор же подключен к произвольно выбранным пинам A0-A2, привязанным к порту C (PC0-PC2). Чтоб использовать более говорящие названия пинов в коде, был написан следующий заголовочный файл pins.h:
#ifndef PINS_H_
#define PINS_H_
// ** Цветной светодиод **
#define RED_PIN PD6 // D6
#define RED_PIN_PORT PORTD
#define RED_PIN_DDR DDRD
#define GREEN_PIN PD3 // D3
#define GREEN_PIN_PORT PORTD
#define GREEN_PIN_DDR DDRD
#define BLUE_PIN PB3 // D11
#define BLUE_PIN_PORT PORTB
#define BLUE_PIN_DDR DDRB
#define RED_COMPONENT OCR0A // D6
#define GREEN_COMPONENT OCR2B // D3
#define BLUE_COMPONENT OCR2A // D11
// ** Светофор **
#define TRFFIC_LIGHT_DDR DDRC
#define TRAFFIC_LIGHT_PORT PORTC
#define RED_LIGHT PC2 // A2
#define YELLOW_LIGHT PC1 // A1
#define GREEN_LIGHT PC0 // A0
#endif
Регистр DDR* задаёт режим ввода/вывода. Если бит, соответствующий какому-то пину установлен в единицу, он работает на вывод. Иначе на ввод.
Регистр OCR** задаёт число, с которым сравнивается счётчик таймера на каждой итерации приращения. Цифра в имени означает таймер, а буква – пин. Его значение может быть использовано для создания прерывания, но здесь оно использовано для генерации широтно-импульсной модуляции, так как в режиме «Fast PWM» на выводе ставится высокий уровень сигнала, когда таймер обнуляется, и сбрасывается в ноль, когда таймер достигает значения в OCR**.
Текущее состояние светофора хранится в структуре TrafficLightState, в которую входит структура TrafficLightColor, которая определяет состояние включённых цветов. Определены они в заголовочном файле trafficlight.h:
#ifndef TRAFFICLIGHT_H_
#define TRAFFICLIGHT_H_
#include <stdbool.h>
struct TrafficLightColor {
bool red;
bool yellow;
bool green;
};
struct TrafficLightState {
struct TrafficLightColor color;
bool isBlinking;
unsigned int blinksToChange;
} trafficLightState;
void turnRedOn(void);
void turnRedOff(void);
void turnYellowOn(void);
void turnYellowOff(void);
void turnGreenOn(void);
void turnGreenOff(void);
void setFastTimer(void);
void setSlowTimer(void);
#endif
Также здесь определён ряд функций для управления состоянием светофора. Две последние используются для изменения режима таймера, чтобы мигание было быстрее, чем обычная смена цветов. Реализация данных функций находится в файле trafficlight.c:
#include <avr/io.h>
#include <stdbool.h>
#include "pins.h"
#include "trafficlight.h"
void turnRedOn(void) {
TRAFFIC_LIGHT_PORT |= _BV(RED_LIGHT);
trafficLightState.color.red = true;
}
void turnRedOff(void) {
TRAFFIC_LIGHT_PORT &= ~_BV(RED_LIGHT);
trafficLightState.color.red = false;
}
void turnYellowOn(void) {
TRAFFIC_LIGHT_PORT |= _BV(YELLOW_LIGHT);
trafficLightState.color.yellow = true;
}
void turnYellowOff(void) {
TRAFFIC_LIGHT_PORT &= ~_BV(YELLOW_LIGHT);
trafficLightState.color.yellow = false;
}
void turnGreenOn(void) {
TRAFFIC_LIGHT_PORT |= _BV(GREEN_LIGHT);
trafficLightState.color.green = true;
}
void turnGreenOff(void) {
TRAFFIC_LIGHT_PORT &= ~_BV(GREEN_LIGHT);
trafficLightState.color.green = false;
}
void setFastTimer(void) {
TCCR1B |= _BV(CS11) | _BV(CS10);
TCCR1B &= ~_BV(CS12);
}
void setSlowTimer(void) {
TCCR1B |= _BV(CS12) | _BV(CS10);
TCCR1B &= ~_BV(CS11);
}
_BV – макрос, превращающийся сдвиг на указанное количество битов влево. А под названиями портов как раз скрывается это смещение.
TCCR1B – регистр управления таймером 1. Это 16-битный таймер, который используется для медленной смены сигналов светофора, когда указан максимальный делитель тактовой частоты (1024), и более быстрой, когда он снижается (до 64).
В файле initialization.h определены две отдельные функции для настройки портов, таймеров, обнуления всех значений и установки прерываний:
#ifndef INITIALIZATION_H_
#define INITIALIZATION_H_
void initializeRgbLed(void);
void initializeTraficLight(void);
#endif
Они определены в initialization.c:
#include <avr/io.h>
#include "pins.h"
#include "trafficlight.h"
#include "initialization.h"
void initializeRgbLed(void) {
// Устанавливаем пины на вывод.
RED_PIN_DDR |= _BV(RED_PIN);
GREEN_PIN_DDR |= _BV(GREEN_PIN);
BLUE_PIN_DDR |= _BV(BLUE_PIN);
// Обнуляем значения на портах.
RED_PIN_PORT &= ~_BV(RED_PIN);
GREEN_PIN_PORT &= ~_BV(GREEN_PIN);
BLUE_PIN_PORT &= ~_BV(BLUE_PIN);
// Обнуляем Timer/Counter0 (пины D6 и D5) и Timer/Counter2 (D11, D3).
TCNT0 = 0;
TCNT2 = 0;
// Начинаем с полностью погашенного светодиода.
RED_COMPONENT = 0;
GREEN_COMPONENT = 0;
BLUE_COMPONENT = 0;
// Устанавливаем, что хотим получить на пине D6 ШИМ.
TCCR0A |= _BV(COM0A1) | _BV(WGM01) | _BV(WGM00);
// Устанавливаем делитель частоты равным 1024.
TCCR0B |= _BV(CS02) | _BV(CS00);
// То же самое, но для пинов D11 и D3.
TCCR2A |= _BV(COM2A1) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
TCCR2B |= _BV(CS22) | _BV(CS20);
// Включаем создание прерываний при переполнении счётчика.
TIMSK0 |= _BV(TOIE0);
}
void initializeTraficLight(void) {
// Устанавливаем пины на выход
TRFFIC_LIGHT_DDR |= _BV(RED_LIGHT) | _BV(YELLOW_LIGHT) | _BV(GREEN_LIGHT);
// Начинать будем с красного цвета.
turnRedOn();
turnYellowOff();
turnGreenOff();
// Сбрасываем Timer/Counter1.
TCNT1 = 0x00;
// Устанавливаем делитель частоты равным 1024.
setSlowTimer();
// Включаем создание прерываний при переполнении счётчика.
TIMSK1 |= _BV(TOIE1);
}
В процессе инициализации включаются прерывания переполнения для таймеров 0 и 1. Обработчик первого используется для изменения цвета через смену значения в соответствующем регистре OCR**. Обработчик второго выполняется гораздо реже и используется для смены состояния светофора. Все эти действия прописаны в основном файле main.c:
#include <avr/interrupt.h>
#include "pins.h"
#include "trafficlight.h"
#include "initialization.h"
#include
// Количество миганий зелёного цвета перед тем, как его сменит жёлтый.
// Должно быть нечётным числом!
#define BLINKS 7
#if BLINKS % 2 == 0
#error "BLINKS должно быть нечётным числом!"
#endif
/* Определяем переменные для хранения компонентов цвета многоцветного светодиода.
Причём просим компилятор всегда брать данные из памяти, а не использовать регистры
для оптимизации, чтобы обработчики прерываний всегда видели актуальные значения. */
volatile uint8_t red = 0;
volatile uint8_t green = 0;
volatile uint8_t blue = 0;
// Точка входа в программу.
int main(void)
{
initializeRgbLed();
initializeTraficLight();
// Устанавливаем регистр глобальных прерываний
// (то есть включаем их в принципе).
sei();
// Программа должна крутиться в цикле, даже если он ничего не делает!
// Иначе выполняться начнут данные и мусор в памяти.
// Но вместо того, чтобы просто гонять процессор зазря, воспользуемся самым
// "лёгким" режимом сна, который будет прерываться только по таймерам.
// Спасибо товарищу KivApple за подсказку (обновлено 26.09.2018)
set_sleep_mode(SLEEP_MODE_IDLE);
while (1) {
sleep_mode();
}
}
// Обработчик смены цвета многоцветного светодиода.
ISR(TIMER0_OVF_vect) {
// 1) Увеличиваем красную компоненту
// (и уменьшаем синюю, если это не первая итерация).
if (red < 0xFF && green == 0)
{
if (blue > 0) {
BLUE_COMPONENT = --blue;
}
RED_COMPONENT = ++red;
}
// 2) Увеличиваем зелёную и уменьшаем красную.
else if (green < 0xFF && blue == 0)
{
RED_COMPONENT = --red;
GREEN_COMPONENT = ++green;
}
// 3) Увеличиваем синюю и уменьшаем зелёную.
else if (blue < 0xFF)
{
GREEN_COMPONENT = --green;
BLUE_COMPONENT = ++blue;
}
}
// Обработчик изменения состояния светофора.
ISR(TIMER1_OVF_vect) {
// Если горит красный...
if (trafficLightState.color.red) {
// ...но не жёлтый, тогда они оба должны гореть.
if (!trafficLightState.color.yellow) {
turnYellowOn();
}
// Если оба, то должен загореться зелёный.
else {
turnRedOff();
turnYellowOff();
turnGreenOn();
}
}
// Если горит жёлтый без красного, то до этого был зелёный и
// следующим будет красный.
else if (trafficLightState.color.yellow) {
turnYellowOff();
turnRedOn();
}
// Так как все остальные состояния мы уже проверили, то здесь
// может либо гореть зелёный, либо мигать.
else {
// И если он не мигает, то сделаем, чтоб мигал!
if (!trafficLightState.isBlinking) {
turnGreenOff();
trafficLightState.blinksToChange = BLINKS;
trafficLightState.isBlinking = true;
// Мигания должны быть быстрее, чем смена цветов.
setFastTimer();
}
// Реализация миганий.
else if (trafficLightState.blinksToChange > 0) {
if (trafficLightState.blinksToChange % 2 == 0) {
turnGreenOn();
} else {
turnGreenOff();
}
trafficLightState.blinksToChange--;
}
// После миганий зажигаем жёлтый сигнал светофора.
else {
turnGreenOff();
turnYellowOn();
trafficLightState.isBlinking = false;
// И не забываем вернуть медленную скорость таймера.
setSlowTimer();
}
}
}
Результат работы программы выглядит следующим образом:
Видео с демонстрацией работы (как файл)
Для светодиодов разных цветов нужно разное сопротивление. Поскольку у меня резисторы одинаковые, то и горят они по-разному.
Hello World
Напоследок поработаем с жидкокристаллическим монохромным двустрочным дисплеем на 16 символов. Мне посоветовали сразу взять с припаянным адаптером PCF8574, чтобы работать с ним по шине I2C и сэкономить выходы микроконтроллера (и не запутаться в проводах!).
Изначально я рассматривал вариант и тут пойти по сложному пути и написать всё самому самом низком уровне, но, почитав даташит, почувствовал себя немного overwhelmed. Да и сроки уже поджимали. Поэтому я всё-таки сдался, пожалел себя и решил срезать, установив Arduino IDE, для которой есть готовая библиотека LiquidCrystal-I2C.
Важно устанавливать библиотеку из архива и подключать к проекту через саму среду разработки (если её можно таковой назвать), чтобы она распаковала файлы в нужное место и правильно расставила пути для сборки.
В результате получаем следующее:
Если текста не видно, то нужно покрутить отвёрткой потенциометр на адаптере и настоить контрастность дисплея.
В качестве домашнего задания можете вывести классическое программистское приветствие из заголовка.
Немного усложним задачу. Выводимые символы жёстко заданы прямо в контроллере дисплея. Покупая экран у китайцев, глупо ожидать увидеть там русские буквы. Вместо них там находятся какие-то иероглифы. Чтобы убедиться в этом, напишем небольшую программу, которая будет перебирать все 255 символов и выводить их поочерёдно на экран:
#include <LiquidCrystal_I2C.h>
// Задаём параметры дисплея: адрес на шине I2C (0x3F), количество символов (16) и строк (2).
LiquidCrystal_I2C lcd(0x3F, 16, 2);
// Счётчик, который будет обнуляться при инкрементировании после 255.
byte i = 0;
void setup() {
// Инициализируем экран.
lcd.begin();
// Неизменяющийся текст выводим один раз при старте программы.
lcd.print("Number:");
lcd.setCursor(0, 1);
lcd.print("Character:");
}
void loop() {
// Выводим значение счётчика.
// При этом надо не забывать про обновление всех трёх разрядов числа.
lcd.setCursor(9, 0);
lcd.print(" ");
lcd.setCursor(8, 0);
lcd.print(i);
// Выводим соответствующий ему символ, зашитый в дисплее.
lcd.setCursor(11, 1);
lcd.print((char) i);
i++;
// Установим задержку в полсекунды, чтоб успевать следить за символами.
delay(500);
}
Вообще, дисплей позволяет задать 8 пользовательских глифов. Существуют библиотеки, которые пользуются этим небольшим запасом и одинаковостью/похожестью кириллических и латинских символов, чтобы эмулировать хотя бы часть русских букв (не помню: то ли только заглавные, то ли только строчные). Но они сделаны либо жёстко под LiquidCrystal без I2C, либо не завелись у меня.
Бонус
Пост получился и так очень большим, но всё-таки не могу удержаться и не вставить ещё одну лабораторную работу, написанную под эмулятор процессора i8086 на чистом ассемблере.
Задание: разработать подпрограмму определения количества вхождений заданного числа в массив чисел, заданный начальным и конечным адресом памяти. Вызов подпрограммы сделать из основной программы.
mvi a, 02 ; Искомое число
lxi b, 0900 ; Начальный адрес
push b
lxi b, 090a ; Конечный адрес
push b
call count ; Вызов подпрограммы
hlt
count:
pop b ; Адрес возврата -> BC
pop d ; Адрес последнего значения -> DE
pop h ; Адрес первого значения -> HL
push b ; Возвращаем адрес возврата в стек
mov b, a
mvi c, 00
mov a, d
cmp h
jm swap ; Если передан сначала больший адрес, то меняем их местами
mov a, e
cmp l
jm swap ; Проверяем и для старшей, и для младшей части
dcx h
check:
inx h
mov a, h
cmp d
jnz do_work ; Для обеих частей проверяем, не последний ли это элемент
mov a, e
cmp l
jnz do_work ; Если нет, то выполняем сравнение
mov a, c ; Иначе размещаем результат в аккумуляторе
ret ; И выходим из подпрограммы
do_work:
mov a, b
cmp m
jnz check
inr c
jmp check
swap:
push d
mov d, h
mov e, l
pop h
jmp check