Найти тему

Делаем код понятным!

Оглавление

Введение

Одной из первых вещей, которой следует научиться — абстрагирование кода. То есть разделение мух от котлет и разных сущностей друг от друга.

Таким способом становится проще выявить ошибку, проще понимать логику программы и проще переносить код в перспективе. К хорошему лучше привыкать сразу.

Как выглядит приложение в таком случае?.. Например, термостат...

Пример разбиения приложения на уровни абстракции
Пример разбиения приложения на уровни абстракции

Уровень драйверов

Есть набор программ, которые отвечают за отдельные функции контроллера: управление ногами, приёмопередатчиками, измерителями, исполнительными устройствами и т.д. Они привязаны к конкретной серии контроллера, но более-менее унифицированы по интерфейсу. То есть по-хорошему, если их заменить на другой аналогичный набор от другой серии контроллеров, то приложение останется работоспособным. Из других уровней доступа к "железу" не имеет никто. Всё идёт через базовый уровень.

То есть, например, класс:

// Класс управления выводом контроллера
class
Pin {
public:
Pin(TPort Port,
int Pin);
void Low(); // низкий уровень
void High(); // высокий уровень
void Set(bool State); // Задать уровень (режим выхода)
bool Get(); // Узнать уровень (режим входа)

void Input(); // режим входа
void Output(); // режим выхода
};

По интерфейсу он не особо привязан к железу (разве что через привязку к ножкам)... Но её в любом случае при переносе переделывать приходится. Но весь алгоритм с использованием этого класса никак не зависит от того, где он выполняется: AVR это или STM32, или Raspberry Pi, или вообще ПК. Что угодно.

Уровень поддержки внешних устройств и цепей на плате

Этот уровень реализует управление основными блоками прибора, опираясь на драйвера. Дёргает ножками в нужном порядке, отправляет и принимает данные, формируя из них пакеты или находя их в принятых данных и т.д. По идее, перенеся этот код на другой контроллер с набором драйверов, его не придётся переделывать или разве что по-минимуму, изменив привязку к конкретным блокам и выводам.

Или же, если это невозможно, можно реализовать его на принципиально другой базе, сохранив интерфейс, чтобы не переделывать логику верхнего уровня.

Чем меньше переделок, тем быстрее, проще и дешевле.

Например, класс для подключения измерителя температуры:

class ThermalSensor {
public:
// Конструктор. На вход: вывод, к которому присоединена схема измерения температуры
ThermalSensor(TPin Pin);
// Получить результат измерения температуры в градусах
float Get();
};

И, в принципе, такие классы могут иметь множество разных реализаций: схем измерения много, как и различных микросхем.

Класс для светодиода:

class Led {
public: // Конструктор. На вход: вывод, к которому присоединён светодиод
Led(TPin Pin);

void On(); // Включить
void Off(); // Выключить
void Toggle(); // Переключить
void Set(bool State); // Установить состояние: включено или выключено
};

Реализация скрыта внутри. Нет нужды знать, как именно подключён светодиод, включается он высоким уровнем или низким, или отдаётся команда на другую микросхему, или посылается пакет по сети. Неважно. Главное, чтобы класс выполнял свою функцию.

Уровень приложения

На этом уровне описывается логика приложения, что устройство должно делать. Например, для термостата: мы должны проверять один или несколько термодатчиков, обрабатывать их показания согласно настройкам, и по результатам включать или выключать нагреватель. Или включать чуть-чуть. Это уж как получится.

Пользуясь функциями управления экраном и опроса кнопок, отображать состояние устройства и обеспечивать возможность изменения настроек.

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

Примитивный код термостата:

void main()
{
ThermalSensor sensor(PA, 5); // термодатчик на ножке РА5
Heater heater(PB, 0); // Нагреватель на ножке РВ0
while(1) {
// Включим нагреватель, если температура ниже 70 градусов.
Heater.Set(ThermalSensor.Get() < 70);
usleep(10000); // спим 10 мс (температура быстро всё равно не изменяется)
}
}

Пусть точность удержания не хирургическая, и колебания будут, но работать-то будет =)

Сравните с условным кодом без обёрток:

void main() {
DDRA &= ~(1 << 5);
DDRB |= (1 << 0);
PORTB |= (1 << 0);
while(1) {
int Temp = (read_adc() - 1245) * 124 / 4095 + 14;
// Включим нагреватель, если температура ниже 70 градусов.
if(Temp < 70)
PORTB &= ~(1 << 0);
else
PORTB |= (1 << 0);
usleep(10000); // спим 10 мс (температура быстро всё равно не изменяется)
}
}

Логика та же, но понять куда как сложнее. А если ещё и комментариев нет? И код на несколько десятков страниц, где всё перемешано.

Так пишут код в уроках чтобы можно было понять, что за что отвечает. Но после таких уроков хорошо бы всё это понимание запихивать в соответствующий интерфейс. В класс или функцию или дефайн. Что угодно, лишь бы было понятнее.

Заключение

И код простой, и блоки отдельные тестировать и отлаживать просто даже в отдельном приложении. Если работают запчасти — то и всё заведётся. Теоретически, такое можно отлаживать и в приложении для ПК, где можно реализовывать искусственно даже маловероятные сценарии. И проводить автоматическое тестирование.

Разбиение на уровни для примера. Это не единственный возможный вариант.