Статьи: Тягаем электронное железо
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.

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 на другое устройство.
avrdude -pm328p -carduino -P/dev/ttyUSB0 -b57600 -Uflash:w:firmware.hex:i

На самом деле, если будете клонировать проект из репозитория, то там уже есть два готовых shell-скрипта: build.sh собирает проект, а upload.sh загружает прошивку в микроконтроллер (если загружать нечего, автоматически вызывает сборку). Так что для сборки и прошивки микроконтроллера просто нужно выполнить следующую команду:

./upload.sh USB0    # или другой порт, к которому подключена Ардуина
Сборка и прошивка на Debian 8

Дополнено 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 и сэкономить выходы микроконтроллера (и не запутаться в проводах!).

Схема подключения I2C-адаптера

Изначально я рассматривал вариант и тут пойти по сложному пути и написать всё самому самом низком уровне, но, почитав даташит, почувствовал себя немного overwhelmed. Да и сроки уже поджимали. Поэтому я всё-таки сдался, пожалел себя и решил срезать, установив Arduino IDE, для которой есть готовая библиотека LiquidCrystal-I2C.

Arduino IDE с набранной программой

Важно устанавливать библиотеку из архива и подключать к проекту через саму среду разработки (если её можно таковой назвать), чтобы она распаковала файлы в нужное место и правильно расставила пути для сборки.

В результате получаем следующее:

Моё имя, выведенное на дисплей

Если текста не видно, то нужно покрутить отвёрткой потенциометр на адаптере и настоить контрастность дисплея.

В качестве домашнего задания можете вывести классическое программистское приветствие из заголовка.

Немного усложним задачу. Выводимые символы жёстко заданы прямо в контроллере дисплея. Покупая экран у китайцев, глупо ожидать увидеть там русские буквы. Вместо них там находятся какие-то иероглифы. Чтобы убедиться в этом, напишем небольшую программу, которая будет перебирать все 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