ВходНаше всё Теги codebook 无线电组件 Поиск Опросы Закон Вторник
10 февраля
1569676 Топик полностью
Бoмж (Вчера, 21:50, просмотров: 85) ответил mr-x на Поделитесь мнением, вопрос про модули. Имеется некий прибор у которого несколько режимов, до десятка. В разных режимах разная работа с событиями клавиатуры, вывод на индикатор, общение с внешним миром, обработка данных АЦП, звук и пр. Как бы вы стали разбивать программу на модули? Модули индикатора, клавиатуры и прочее или модуль режима 1, режима 2 и т.д. Может есть какая другая метода, более прогрессивная и правильная?
Ваш вопрос касается модуляризации программы для устройства с несколькими режимами (до десятка), где в каждом режиме по-разному обрабатываются клавиатура, индикатор, внешние интерфейсы, АЦП, звук и т.д. Это классическая задача в embedded-программировании или системном дизайне, где нужно балансировать между повторяемостью кода, удобством поддержки и производительностью. 

Давайте разберём варианты, которые вы упомянули, и я предложу, как бы я подошёл к этому, плюс более "прогрессивные" альтернативы.Вариант 1: Разбиение по компонентам (модули индикатора, клавиатуры и т.д.)Это подход, ориентированный на аппаратные абстракции (hardware abstraction layers, HAL). Здесь программа делится на модули, каждый из которых отвечает за конкретный периферийный блок или функцию, независимо от режима.Примеры модулей:keyboard.c/h: Обработка событий клавиатуры (чтение, дебаунсинг, генерация событий).
display.c/h: Вывод на индикатор (инициализация, отрисовка символов/графики).
adc.c/h: Обработка данных с АЦП (калибровка, фильтрация, преобразование).
external_io.c/h: Общение с внешним миром (UART, I2C, SPI и т.д.).
sound.c/h: Генерация звуков (через PWM или DAC).
utils.c/h: Общие утилиты (таймеры, буферы, математика).

Как режимы интегрируются? Логика режимов размещается в отдельном модуле, например modes.c/h, где используется конечный автомат (finite state machine, FSM) или switch-case для переключения между режимами. В каждом режиме вы вызываете функции из компонентных модулей с разными параметрами или коллбэками. Например:

// В modes.c
enum Mode { MODE_1, MODE_2, ... };
static enum Mode current_mode = MODE_1;

void handle_keyboard_event(int key) {
    switch (current_mode) {
        case MODE_1: process_key_mode1(key); break;
        case MODE_2: process_key_mode2(key); break;
        // ...
    }
}


Или, для большей гибкости, используйте коллбэки: каждый режим регистрирует свои обработчики для событий (например, register_keyboard_handler(mode1_keyboard_cb)).
Плюсы:Минимизирует дублирование кода: общая логика для hardware в одном месте.
Легко тестировать и отлаживать отдельные компоненты.
Масштабируемо: если добавится новый режим, меняете только логику в modes.c, не трогаете драйверы.

Минусы:Если режимы сильно различаются (например, в одном режиме клавиатура игнорируется, в другом — генерирует сложные события), то модуль режимов может разрастись и стать "богом-объектом".
Требует хорошей абстракции, чтобы компоненты были режим-независимыми.

Это мой предпочтительный базовый подход для большинства устройств, особенно если код на C или C++ для микроконтроллеров.Вариант 2: Разбиение по режимам (модуль режима 1, режима 2 и т.д.)Здесь каждый режим — это отдельный модуль, содержащий всю логику для этого режима, включая обработку клавиатуры, индикатора и т.д.Примеры модулей:mode1.c/h: Полная реализация для режима 1 (keyboard handling, display output, ADC processing и т.д. конкретно для этого режима).
mode2.c/h: Аналогично для режима 2.
И так для каждого из 10 режимов.
Плюс общий main.c или dispatcher.c для переключения между ними.

Плюсы:Легко понять и модифицировать один режим независимо от других.
Подходит, если режимы радикально разные (например, один — калькулятор, другой — осциллограф) и мало общего кода.

Минусы:Дублирование кода: если обработка АЦП похожа в нескольких режимах, то копипаста неизбежна, что усложняет поддержку.
Разрастание проекта: 10 модулей — это много файлов, и если режимов станет больше, хаос.
Труднее обеспечить consistency (например, единый стиль вывода на индикатор).

Я бы избегал этого, если только режимы не полностью изолированы (что редко).Более прогрессивные и правильные методыВ современном дизайне ПО для устройств (особенно с RTOS или без, как в FreeRTOS или bare-metal) лучше комбинировать подходы, используя паттерны, которые делают код модульным, testable и extensible. Вот несколько идей, от простых к более сложным:Finite State Machine (FSM) с компонентами:Базируйтесь на варианте 1, но сделайте FSM более структурированным. Каждый режим — состояние в автомате, с входами/выходами/переходами.
Используйте таблицу состояний или структуру с функциями:

typedef struct {
    void (*keyboard_handler)(int key);
    void (*display_update)(void);
    void (*adc_process)(float data);
    // ...
} ModeHandlers;

ModeHandlers modes[10] = {
    { mode1_keyboard, mode1_display, mode1_adc },
    // ...
};

// В main loop:
modes[current_mode].keyboard_handler(key_event);


Прогрессивно: Это позволяет легко добавлять/менять режимы без switch-case. Для C++ можно использовать классы с виртуальными методами (полиморфизм).

Layered Architecture (слои):Разделите на слои:Hardware Layer: Драйверы (как в варианте 1: keyboard, display и т.д.).
Service Layer: Абстракции над hardware (например, event queue для клавиатуры, data filters для АЦП).
Application Layer: Логика режимов, использующая сервисы.

Переключение режимов — через менеджер состояний. Это стандарт в embedded (см. UML Statecharts для вдохновения).
Плюс: Легко портировать на другой hardware, тестировать слои отдельно.

Event-Driven Architecture:Если устройство event-based (как в GUI или RTOS), используйте pub-sub модель: компоненты генерируют события (key press, ADC ready), а режимы подписываются на них.
Библиотеки вроде FreeRTOS queues или custom event bus. Каждый режим — subscriber с своими обработчиками.
Прогрессивно: Масштабируемо для многопоточных систем, снижает coupling между модулями.

Object-Oriented (если C++ или Rust):Классы для компонентов (Keyboard, Display), интерфейсы для режимов (IMode с методами handleKey(), updateDisplay()).
Текущий режим — объект, реализующий IMode, который компонует компоненты.
Это следует SOLID принципам: single responsibility для модулей, open-closed для拡張.

Конфигурация через данные (data-driven):Если режимы похожи, храните различия в конфигах (массивы, JSON-like structs). Например, таблица: для режима 1 — keyboard map A, display format B; для 2 — map C, format D.
Минимизирует код, но подходит не для всех случаев.

Мой рекомендацияЯ бы начал с варианта 1 (по компонентам) + FSM, потому что это балансирует простоту и гибкость. Если режимов много и они сложные, перейдите к layered или event-driven. Избегайте полного разбиения по режимам, чтобы не плодить дубликаты. Тестируйте на маленьком прототипе: напишите 2-3 режима и посмотрите, где bottlenecks.

memento mori