Skip to content

Project Firmware

Dmitry edited this page May 10, 2026 · 31 revisions

Архитектура прошивки

Уровень платформы

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

io_mapping.h

Базовый файл всего проекта. В нем задаются различные символические константы для произведения настройки и взаимодействия. Основной их шаблон:

  • *_BASE -> указатель на структуру регистров, используемую для непосредственной конфигурации и управления периферийным модулем или устройством.
  • *_PIN -> номер вывода (пина) на отладочной плате.
  • *_PIN_MASK -> вычисляется по формуле (1u << *_PIN) - задает маску вывода (пина).
  • *_PAD_CFG, *_PAD_PS, *_PAD_PUPD - символические константы доступа к определенным регистрам конфигурации выводов PAD_CONFIG.
  • *_CLOCK_MASK -> маска тактирования устройства/периферии.
  • *_IRQ_INDEX -> индекс в таблице прерывания EPIC.

Стоит отметить, что некоторые поля в данной таблице являются опциональными и имеются не у каждого периферийного блока/устройства.

clock_control.h clock_control.c

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

  • Clock_Status — перечисление, определяющее результат инициализации системы тактирования микроконтроллера.
  • Clock_SystemSource — перечисление доступных источников системного тактирования.
  • Clock_init() — выполняет инициализацию системы тактирования.
  • Clock_enable_PAD_Config() — включает тактирование модуля конфигурации выводов PAD_CONFIG.
  • Clock_enable_EPIC() — включает тактирование контроллера прерываний EPIC.
  • Clock_enable_UART0() — включает тактирование интерфейса UART0.
  • Clock_enable_I2C1() — включает тактирование интерфейса I2C1.
  • Clock_enable_RTC() — включает тактирование модуля часов реального времени RTC.
  • Clock_enable_TIMER32_1() — включает тактирование таймера TIMER32_1.
  • Clock_enable_BUZZER() — включает тактирование периферии, используемой для управления активным зуммером.
  • Clock_enable_DHT11() — включает тактирование интерфейса взаимодействия с датчиком DHT11.
  • Clock_get_UART0_FREQ() — возвращает текущую тактовую частоту интерфейса UART0.
  • Clock_get_TIMER32_1_FREQ() — возвращает текущую тактовую частоту таймера TIMER32_1.

Файл clock_control.c содержит также статические функции, которые выполняют роль вспомогательных для уже перечисленных. Взаимодействие в данных файлах производится с блоками регистров WU и PM. Более подробно можно ознакомится в главах 5.1 и 5.2: MIK32_datasheet_v2.2.2.pdf

pad_control.h pad_control.c

Данная связка файлов предназначена для мультиплексирования выводов микроконтроллера.

  • PAD_init() — выполняет базовую инициализацию подсистемы конфигурации выводов.
  • PAD_UART0_settings() — настраивает выводы, используемые интерфейсом UART0.
  • PAD_I2C1_settings() — настраивает выводы интерфейса I2C1.
  • PAD_BUZZER_settings() — настраивает выводы для управления звуковым сигнализатором.
  • PAD_DHT11_settings() — настраивает выводы для взаимодействия с датчиком DHT11.

Настройка определенной периферии производится в соответствии с ее даташитом. Дополнительно следует обратить внимание на конфигурацию выводов интерфейса I2C, в частности на активацию внутренних подтягивающих резисторов, необходимых для корректного функционирования шины. Необходимость данной настройки обусловлена требованиями временных диаграмм и особенностями работы интерфейса, представленными на соответствующих схемах и рисунках:

image image

Более подробно с PAD_CONFIG можно ознакомиться в главе 3.14, раздел 3.14.1: MIK32_datasheet_v2.2.2.pdf

epic_irq.h epic_irq.c

Данные файлы обеспечивают взаимодействие с контроллером прерываний EPIC. Контроллер прерываний выполняет роль промежуточного звена между ядром микропроцессора и периферийными устройствами, формирующими запросы на прерывание.

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

Общая картина работы прерывания данной системы представлена на рисунке ниже:

Interrupt

Функции данных файлов:

  • EPIC_IRQ_init() — выполняет базовую инициализацию механизма прерываний: включает тактирование EPIC, разрешает внешние машинные прерывания в регистре mie и глобально разрешает обработку прерываний через mstatus.

  • EPIC_IRQ_enable(uint32_t irq_index) — разрешает обработку прерывания по уровню с заданным индексом в контроллере EPIC.

  • EPIC_IRQ_disable(uint32_t irq_index) — запрещает обработку прерывания по уровню с заданным индексом.

  • EPIC_IRQ_clear(uint32_t irq_index) — очищает флаг прерывания по указанному индексу.

  • EPIC_IRQ_GetRawStatus() — возвращает текущее состояние регистра RAW_STATUS, содержащего таблицу прерываний.

  • EPIC_IRQ_IsPending(uint32_t irq_index) — проверяет, находится ли прерывание с заданным индексом в состоянии ожидания обработки в таблице прерываний.

Более подробно можно ознакомится в следующих источниках:

  1. MIK32_datasheet_v2.2.2.pdf глава 3.11.
  2. scr1_eas.pdf глава 3.2.4, разделы 3.2.4.5 и 3.2.4.7.
  3. https://github.com/MikronMIK32 (библиотека HAL).

irq_dispatcher.h irq_dispatcher.c

Из названия данных файлов понятно, что это диспетчер прерываний и он вызывается в стандартном обработчике исключений и прерываний trap_handler:

void trap_handler(void) {
    IRQ_Dispatch();
}

Основные функции диспетчера прерываний заключаются в следующем:

  1. Определение источника прерывания посредством анализа таблицы прерываний контроллера EPIC.
  2. Передача управления соответствующему обработчику прерывания устройства, инициировавшего запрос (данное устройство по окончании обработки должно очистить внутреннее прерывание).
  3. Очистка флага обработанного прерывания в контроллере EPIC.

Данные действия косвенно описаны на ломаных линиях предыдущего рисунка. Все они выполняются в функции IRQ_Dispatch().

mtimer.h mtimer.c

Внутренний таймер ядра scr1. Его настройка производилась в соответствии с документацией: scr1_eas.pdf глава 3.2.8.

Разберем основные функции системы:

  • MTIMER_init() — выполняет конфигурацию и запуск внутреннего таймера ядра SCR1.

  • MTIMER_get_TIME() — возвращает текущее значение 64-битного системного таймера MTIME.

  • MTIMER_delay_us(uint64_t us) — реализует программную задержку в микросекундах.

  • MTIMER_delay_ms(uint64_t ms) — реализует программную задержку в миллисекундах.

system.h system.c

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

Функция System_init() последовательно выполняет:

  • инициализацию системы тактирования;
  • запуск внутреннего системного таймера ядра SCR1;
  • настройку конфигурации выводов микроконтроллера;
  • инициализацию контроллера прерываний EPIC.

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

Перед началом описания каждого драйвера системы стоит отметить, что все они имеют структуру перечисления вида:

typedef enum __*_status {
    *_DRIVER_OK = 0,
    *_DRIVER_ERROR,
} *_Status;

Кажется, что список состояний описываемого драйвера является не полным - всего два состояния: ERROR и OK, но так как эти состояния возвращаются из отдельных функций драйвера, то на верхних уровнях системы возможно определить, по какой причине произошел тот или иной ERROR.

Стоит отметить, что файловая структура всех драйверов системы имеет вид *_driver.h и *_driver.c. Суммарно таких файлов: 14. Перейдем к описанию каждого из них.

uart_driver.h uart_driver.c

Реализация логики работы протокола представлена в файле uart_driver.c. uart_driver.h же представляет собой своеобразный интерфейс драйвера протокола UART (BAUDRATE 57600) и включает в себя следующие функции:

  • UART_init() — выполняет инициализацию интерфейса UART, включая настройку параметров передачи данных и конфигурацию периферийного модуля.

  • UART_Transmit(const char * src) — осуществляет передачу строки символов по интерфейсу UART.

  • UART_isCommandReady() — проверяет наличие безошибочно принятой команды в буфере приема.

  • UART_isOverflow() — проверяет наличие переполнения буфера приема данных.

  • UART_GetCommand(char * dst) — копирует принятую команду из внутреннего буфера в пользовательский буфер.

  • UART_ClearCommandReady() — сбрасывает флаг готовности безошибочно принятой команды.

  • UART_ClearOverflow() — сбрасывает флаг переполнения буфера приема.

  • UART_IRQHandler() — обработчик прерывания интерфейса UART, обеспечивающий прием входящих данных и обновление внутренних состояний драйвера.

Если с функциями UART_init(), UART_Transmit(), UART_IRQHandler все понятно и их реализация производится по документациям MIK32_datasheet_v2.2.2.pdf (глава 3.6) и https://github.com/MikronMIK32 (библиотека HAL), либо по логике взаимодействия с аппаратной частью системы (уровень платформа), рассмотренной ранее, то другие функции требуют дополнительного объяснения.

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

Непосредственный доступ к структуре из сервисного уровня системы не производится. Для взаимодействия с ней реализован набор специализированных функций-интерфейсов, которые перечислены выше.

Дополнительно рассмотрим вид пакета, передаваемого по протоколу UART:

[start bit][frame = 8][no parity][1 stop bit]

Также следует рассмотреть механизм формирования внутреннего буфера приема данных и сопутствующих флагов состояния скрытой структуры UART_RX_Data.

Прием символов осуществляется в обработчике прерывания посредством скрытой функции UART_Receive(). Каждый поступающий символ последовательно записывается во внутренний буфер размером 64 байта до момента получения символа конца строки '\n', который рассматривается как признак завершения UART-команды.

После успешного завершения приема:

  • на место символа '\n' записывается нуль-терминатор '\0';
  • устанавливается флаг buf_ready, сигнализирующий о готовности корректно принятой команды;
  • указатель текущей позиции буфера сбрасывается.

В случае переполнения буфера:

  • устанавливается флаг overflow;
  • активируется флаг discard_until_nl, переводящий драйвер в режим игнорирования входящих данных до получения символа конца строки.

Примеры корректно и некорректно принимаемых UART-команд в соответствии с реализованным механизмом обработки буфера представлены ниже:

SET_TIME 12 30 45\n buf_ready ✅
SHOW_TH\n buf_ready ✅
SET_ALARM_TIME 12 00\n buf_ready ✅
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n buf_ready ❌ overflow ✅ discard_until_nl ✅

buzzer_driver.h buzzer_driver.c

Данные файлы необходимы для взаимодействия с активным зуммером. Интерфейс данного драйвера (файл buzzer_driver.h) представлен в виде двух функций, которые описаны в файле buzzer_driver.c:

  • BUZZER_init() — выполняет инициализацию устройства.

  • BUZZER_set_value(bool value) — устанавливает логическое состояние вывода звукового сигнализатора, включая или отключая подачу сигнала.

В данных модулях активно используется взаимодействие с GPIO, подробнее ознакомится с которыми можно в соответствующей документации: MIK32_datasheet_v2.2.2.pdf (глава 3.14).

Стоит также отметить, что именно активный зуммер будет выполнять роль будильника и издавать неприятный звук 🙂.

В заключении скажу, что подключение данного электронного компонента осуществляется через резистор 220 Ом, чтобы ограничить поступающий ток (я прекрасно понимаю, что внутри зуммера есть токоограничивающий резистор, но еще один для подстраховки не помешает).

timer_driver.h timer_driver.c

В первоначальной задумке проекта данного устройства вообще не должно было существовать, но он появился по причине того, что в модуле RTC не было предусмотрено ежесекундное прерывание. Мое лицо в тот момент разработки было примерно таким:

cat-judgemental-thebeatenbush-judge-judging

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

Интерфейс представлен в файле timer_driver.h:

  • TIMER_init() — выполняет инициализацию и настройку таймера.

  • TIMER_IRQHandler() — обработчик прерывания таймера, вызываемый при возникновении события переполнения.

  • TIMER_get_overflow_flag() — возвращает состояние флага переполнения таймера.

Дополнительно детально разберем файл timer_driver.c — расчет одной секунды достигается за счет настройки параметров максимального значения и делителя входной частоты по формуле из книги Making Embedded Systems Elecia White (глава 4):

прескалер * сравнение = тактовая частота / частота прерывания

* тактовая частота таймера известна: 32 МГц.
* частота прерывания: 1 Гц.
* прескалер (делитель входной частоты) и параметр "сравнение" (в данном случае максимальное значение) необходимо определить методом подбора — я взял 32000 и 1000 соответственно.

Также в данном файле, аналогично драйверу UART, используется статическая переменная состояния timer_overflow_pending, фиксирующая факт возникновения прерывания переполнения таймера.

Доступ к данной переменной осуществляется через функцию TIMER_get_overflow_flag(), которая позволяет верхним уровням системы определить необходимость обработки соответствующего события.

Ранее это не упоминалось, однако такой подход позволяет не выполнять всю обработку непосредственно внутри прерывания. Обработчик прерывания только фиксирует факт произошедшего события, а основная логика выполняется позднее на верхнем уровне системы. Простыми словами, это необходимо, чтобы обработка прерывания не тормозила основную систему, а выполнялось в тандеме с ней.

dht11_driver.h dht11_driver.c

Данные файлы нужны для взаимодействия с датчиком температуры и влажности воздуха DHT11. Подробная документация находится тут: DHT11 Datasheet.

Файл интерфейса данного драйвера (dht11_driver.h) представлен двумя функциями:

  • DHT11_init() — выполняет инициализацию датчика DHT11.

  • DHT11_read(uint32_t* data) — осуществляет чтение данных с датчика DHT11 и сохраняет полученное значение в пользовательский буфер.

На файле dht11_driver.c остановимся чуть подробнее, так как его реализация может показаться непонятной.

Стоит отметить, что для взаимодействия с данным датчиком используется GPIO, с которыми мы уже познакомились при работе с активным зуммером. Дополнительно отмечу, что у меня датчик со встроенным подтягивающим резистором.

Инициализация драйвера производится как обычно, но добавляются некоторые ограничения, описанные в даташите (глава 7):

DHT11 on after power to wait 1S across the unstable state during this period can not send any instruction.

То есть, после подачи питания требуется выдержать задержку не менее одной секунды перед началом обмена данными с датчиком, поскольку в этот период его состояние считается нестабильным. Это учтено в функции DHT11_init() следующим образом:

MTIMER_delay_ms(DHT11_POWER_STABILIZE_DELAY_MS);

*где DHT11_POWER_STABILIZE_DELAY_MS = 1000u.

Функция DHT11_read(uint32_t* data) включает в себя несколько внутренних функций, обеспечивающих полный цикл обмена с датчиком DHT11. Эти функции скрыты внутри драйвера и не вызываются напрямую на верхних уровнях системы. Далее рассмотрим назначение каждой из них.

Функция DHT11_send_start_signal() выполняет функцию, описанную на рисунке ниже:

image

Функция DHT11_read_byte(uint8_t *byte) выполняет функцию, описанную на рисунках ниже:

image

image

Функция DHT11_read_data(uint8_t data[DHT11_DATA_BYTES]) вызывает ранее описанные функции в нужном порядке + выполняет функцию, указанную на рисунке ниже:

image

По итогу, функция DHT11_read(uint32_t* data) вызывает функцию DHT11_read_data(uint8_t data[DHT11_DATA_BYTES]), а после чего считывает полученное значение во внутренний буфер.

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

Получаемые с датчика данные соответствуют следующей структуре:

image

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

i2c_driver.h i2c_driver.c

Данные файлы описывают работу протокола данных I2C в соответствии с даташитом: MIK32_datasheet_v2.2.2.pdf (глава 3.7).

Следует отметить, что используемый интерфейс функционирует в режиме Standard (до 100 кГц). Также в рамках проекта микроконтроллер выступает в режиме ведущего устройства и только для передачи данных, потому что данный интерфейс необходим исключительно для взаимодействия с LCD-дисплеем.

Интерфейс данного драйвера (i2c_driver.h) представлен двумя функциями:

  • I2C_init() — выполняет инициализацию и настройку интерфейса I2C.

  • I2C_write(uint8_t byte) — осуществляет передачу одного байта данных по шине I2C по программно-заданному адресу.

Инициализация драйвера в файле i2c_driver.c производилась в соответствии с даташитом:

image

Сразу отвечу на возникающие вопросы:

  1. Что это?
uint8_t delay = 3u;
    while (delay--) {
        __asm__ volatile ("nop");
    }
  1. Откуда взять это?
#define I2C_TIMING_SCLL     (0x27u)
#define I2C_TIMING_SCLH     (0x27u)
#define I2C_TIMING_SDADEL   (0x2u)
#define I2C_TIMING_SCLDEL   (0x9u)
#define I2C_TIMING_PRESC    (0x3u)

Ответы:

  1. Из даташита:
Бит PE должен находиться в состоянии «0» как минимум три такта APB для осуществления сброса.
  1. На этот вопрос ответить простыми словами сложно. Но для начала разберемся, что значит каждый регистр:
  • SCLL - регистр, определяющий сколько времени линия удерживает сигнал на низком уровне;
  • SCLH - регистр, определяющий сколько времени линия удерживает сигнал на высоком уровне;
  • SDADEL - регистр, определяющий время удержания данных;
  • SCLDEL - регистр, определяющий время установки данных;
  • PRESC - регистр, определяющий значения делителя входной частоты.

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

image

image

Вариант 1: Использовать онлайн калькулятор для расчета значений этих регистров. Вариант хороший, я использовал от STM32 - не получилось, потому что слишком агрессивная настройка, такое железо не прощает.

Вариант 2: Считать самому. Плохой вариант, но рабочий, также стоит отметить, что он работает по факту методом подбора. Вот небольшой алгоритм, как это сделать:

  • выберем начальное значение регистра PRESC, желательно небольшое, например 3 = 0x3. Следовательно, один так I2C будет производится PRESC+1=4 такта шины APB_P, то есть 31.25 ns * 4 = 125 ns.
  • так как наша целевая частота 100 кГц, то SCLL и SCLH должны длится по 5000 ns. Следовательно, при текущей настройке PRESC 5000 ns / 125 ns = 40, следовательно, из-за того что кол-во тактов высчитывается как REG+1, то SCLL = SCLH = 39 = 0x27.
  • для определения значений регистра осталось посмотреть на соответствующую таблицу, указанную выше и выбрать необходимые значения SDADEL и SCLDEL: я взял 2 = 0x2 и 9 = 0x9 соотвественно.
  • проверим на соответствие:
Регистр Таблица Мой результат Единица измерения
tLOW min 4,7 5 мкс.
tHIGH min 4,0 5 мкс.
tHD min 0 250 нс.
tSU min 250 1125 нс.

Итоговая частота: 100 кГц.

Реализация отправки данных реализовано в соответствии с этой блок-схемой, представленной в даташите:

image

За исключением того, что в моей реализации включен AUTOEND, убрано условие с проверкой регистра TC, так как он не будет работать из-за включенного AUTOEND и добавлена проверка на занятость передатчика.

Дополнительно еще затрону момент, когда в случае прихода NACK система ожидает STOP. Это объясняется даташитом:

Если не получено подтверждение (NACK): флаг TXIE не взводится, и комбинация STOP автоматически отправляется после получения NACK.

В заключение отвечу на вопрос: как определить адрес устройства на линии? Ответ - в соответствии с даташитом на устройство. В моем случае это переходник, который встроен в LCD -> PCF8574T, вот картинка:

image

lcd_driver.h lcd_driver.c

Интерфейс данного драйвера представлен в файле lcd_driver.h:

  • LCD_init() — выполняет инициализацию LCD дисплея.

  • LCD_clear() — очищает экран дисплея и устанавливает курсор в начальную позицию.

  • LCD_home() — перемещает курсор в левый верхний угол без очистки экрана.

  • LCD_set_cursor(uint8_t row, uint8_t col) — устанавливает курсор в указанную позицию (строка и столбец).

  • LCD_write_char(char c) — выводит один символ в текущую позицию курсора.

  • LCD_write_string(const char *str) — выводит строку на дисплей начиная с текущей позиции курсора.

  • LCD_display_on() — включает отображение на дисплее.

  • LCD_display_off() — выключает отображение дисплея.

В файле lcd_driver.c находится реализация этих и вспомогательных функций. Я не буду разбирать каждую из них, поэтому просто разберу общую идею.

Для этого разберемся с общими положениями работы дисплея:

  1. Во-первых, рассмотрим электронную схему дисплея:

image

Как видно из данной схемы:

  • Вывод Р0 микросхемы соединен с выводом RS дисплея, отвечающего за то, принимает дисплей данные (1) или инструкции по работе дисплея (0);

  • Вывод Р1 соединен с R\W, если 0 – запись данных в дисплей, 1 – считывание;

  • Вывод Р2 соединен с CS (Enable) – вывод, по изменению состояния которого идет считывание данных дисплеем;

  • Вывод Р3 – управление подсветкой;

  • Выводы Р4 — Р7 служат для передачи данных дисплею.

Исходя из предоставленных данных можно сделать следующие выводы:

  • LCD дисплей работает в 4-битном режиме, то есть принимает только 4 бита полезной нагрузки за одну передачу (линии D4–D7).

  • Вывод P3 подключён к базе биполярного транзистора, который управляет подсветкой дисплея.

  • Для включённой подсветки необходимо поддерживать активный уровень сигнала на выводе P3 (подавать его постоянно).

  1. Во-вторых, из п.1 следует, что один пакет дисплея будет выглядеть так:
[DATA3 DATA2 DATA1 DATA0 BL E R\W RS]

Руководствуясь рисунком, представленным ниже, можно сделать вывод о том, как производится передача байта данных:

[DATA3 DATA2 DATA1 DATA0 1 0 0 RS]
              |
              v
[DATA3 DATA2 DATA1 DATA0 1 1 0 RS]
              |
              v
[DATA3 DATA2 DATA1 DATA0 1 0 0 RS]

image

  1. В-третьих, после того как мы разобрались, как работает дисплей, необходимо рассмотреть, из каких компонентов он состоит:

Виды памяти:

  • DDRAM - память, в которой хранятся отображаемые в данные момент на дисплее коды ASCII символов. В данную память мы из записываем данные по определенным адресам, представленным на рисунке ниже:

  • CGROM - память, которая содержит заранее заданные шаблоны символов. В неё на заводе-изготовителе загружаются начертания символов таблицы ASCII. Простыми словами, при отправке ASCII-кода в DDRAM контроллер сравнивает его с данными в CGROM и отображает соответствующий символ на экране.

  • CGRAM - память, в которую пользователь может записывать шаблоны пользовательских символов. Я этим, если честно, не пользовался и не разбирался как это работает за ненадобностью.

Виды команд:

image

Более подробно о каждой команде можно узнать тут: LCD Commands.

  1. В-четвертых, как заключение, разберем начало работы с дисплеем, а именно его инициализацию, которая выполняется в соответствии с данным рисунком:

image

rtc_driver.h rtc_driver.c

В данном разделе я как и с LCD-дисплеем не буду разбирать каждую функцию, а лишь разберу принцип работы данного драйвера и отвечу на неочевидные вопросы.

Как всегда файл rtc_driver.h является интерфейсом данного драйвера, с которым будет взаимодействовать соответствующий сервис. Он включает в себя следующие функции:

  • RTC_init() — выполняет инициализацию модуля RTC.

  • RTC_set_time(const RTC_Time *time) — устанавливает текущее время RTC.

  • RTC_get_time(RTC_Time *time) — получает текущее время RTC.

  • RTC_set_date(const RTC_Date *date) — устанавливает текущую дату RTC.

  • RTC_get_date(RTC_Date *date) — получает текущую дату RTC.

  • RTC_set_alarm_time(const RTC_Time *time) — устанавливает время срабатывания будильника RTC.

  • RTC_get_alarm_time(RTC_Time *time) — получает установленное время будильника RTC.

  • RTC_alarm_enable() — включает будильник RTC.

  • RTC_alarm_disable() — отключает будильник RTC.

  • RTC_IRQHandler() — обработчик прерывания будильника RTC.

  • RTC_get_alarm_flag() — возвращает флаг срабатывания будильника RTC.

Также в данном файле представлены две новые структуры: RTC_Time и RTC_Date, каждая из которых включает в себя необходимые поля для удобного взаимодействия с регистрами модуля RTC.

Далее перейдем к разбору файла rtc_driver.c. В данном файле, аналогично драйверам UART и TIMER, используется статическая переменная состояния alarm_flag, фиксирующая факт возникновения прерывания о срабатывании будильника. Доступ к данной переменной осуществляется через функцию RTC_get_alarm_flag(), которая позволяет верхним уровням системы определить необходимость обработки соответствующего события.

Вспомогательные функции о проверке корректно вводимых данных в регистры, а также функции "упаковки" и "распаковки" данных разбираться не будут, так как первые необходимы для корректной работы самих регистров - данный факт также затронут в даташите: MIK32_datasheet_v2.2.2.pdf (глава 3.10):

Установка недопустимых значений может привести к неопределённому результату.

А вторые, в свою очередь, просто необходимы для удобного управления данными внутри драйвера, так как регистры модуля RTC поддерживают исключительно код BCD (то есть каждая цифра кодируется отдельно, а не все число целиком). Вот, если что пример данного кода:

Обычное двоичное представление:
45 = 00101101
BCD представление:
45 = 0100 0101

Перейдем к более интересным функциям, в которых описание не так очевидно.

Функция обработки прерывания RTC_IRQHandler() первым действием поднимает флаг alarm_flag, о котором уже было сказано ранее, далее данная функция выполняет два очень непонятных действия по взаимодействию с внутренними регистрами:

RTC_BASE->CTRL &= ~RTC_CTRL_ALRM_M;
RTC_BASE->CTRL |= RTC_CTRL_RESET_STROBE_M; 

Выглядит, если честно, ужасающе, но все просто и объясняется даташитом. Вот картинка, где описаны биты регистра CTRL:

image

Данными действиями сбрасывается бит ALRM, который свидетельствовал о том, что будильник сработал, а только после него сбрасывается признак установки активного уровня ALRM_PAD. Данная последовательность действий не случайна и на самом деле очень важна. Этот факт объясняется следующими строками даташита:

При совпадении заданного и текущего времени формируется флаг будильника (текущий статус RRTC_CTRL.ALRM), прерывание (если оно разрешено) и формируется
активный уровень «1» на выводе RTC_ALARM. Продолжительность активного уровня - один период источника тактирования часов реального времени. Для сброса флага будильника необходимо записать «0» в поле RRTC_CTRL.ALRM. Для сброса признака установки активного уровня на выводе RTC_ALARM необходимо записать «1» в поле RRTC_CTRL.ALRM_PAD. Если при сбросе признака активного уровня вывода RTC_ALARM (запись «1» в RRTC_CNTL.ALRM_PAD) флаг будильника активный, то
будет сформирован еще один строб на выводе RTC_ALARM.

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

Также стоит обратить внимание и на функции установки времен/даты того или иного регистра, а именно на особенность, которая присуща не всем этим функциям:

В функциях RTC_set_date(const RTC_Date *date) и RTC_set_time(const RTC_Time *time), в отличие от функции RTC_set_alarm_time(const RTC_Time *time) модуль RTC отключается. Это связано также с даташитом, а именно с этим:

Для установки новых значений необходимо сбросить бит EN регистра RRTC_CTRL.

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

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

Теперь перейдем к странностям, которые находятся в функциях RTC_set_date(const RTC_Date *date) и RTC_set_time(const RTC_Time *time). Стоит отметить, что данные действия не предусмотрены даташитом и они добавлялись по мере тестирования устройства:

  1. Первая странность — при попытке установить только время значение регистра даты сбрасывалось к ранее записанному значению, а при установке только даты регистр времени возвращался к ранее записанному значению. Совместная запись даты и времени позволила избежать этой проблемы. Точную причину такого поведения я, если честно, так и не понял.

  2. Вторая странность — это задержка, ну или так называемая RTC_short_delay(), которая также используется в этих же функциях. Дело в том, что без данной задержки, пользовательские параметры регистров времени и даты устанавливались через раз. Простыми словами, иногда система не реагировала на изменения. По какой причине я тоже не понял, но данная задержка также решила эту проблему.

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

Уровень сервисов

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

В реализованной системе каждый сервис взаимодействует только со своим драйвером и предоставляет функции-обертки над функциями драйвера.

Кроме того, каждый сервис содержит getter для получения уровнем приложения особых флагов, возникающих при работе соответствующего драйвера или сервиса (в данном случае — флагов пользовательских команд). Значения этих флагов определяются в файлах app_events.h и app_commands.h. Указанные флаги используются на уровне приложения для корректной обработки событий контроллером или уведомления пользователя об ошибках.

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

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

app_init.h app_init.c

Интерфейс данного файла представлен в виде одной функции:

  • App_Init() — выполняет инициализацию драйверов.

Я думаю тут комментарии излишни.

app_commands.h

Данный файл уже был упомянут ранее. Он предназначен для организации взаимодействия между сервисным уровнем и уровнем приложения. В нём определён набор define'ов, где каждому отдельному биту соответствует конкретная команда. Приведу пример:

#define APP_COMMAND_ALARMOFF_S      (8u)

#define APP_COMMAND_ALARMOFF_M      (1u << APP_COMMAND_ALARMOFF_S)

app_events.h

Данный файл также уже ранее упоминался. Он предназначен для организации взаимодействия между сервисным уровнем и уровнем приложения. В нём определён набор define'ов, где каждому отдельному биту соответствует конкретное событие, произошедшее в системе. Приведу пример:

#define APP_EVENT_UART_DRIVER_OVERFLOW_ERROR_S      (12u)

#define APP_EVENT_UART_DRIVER_OVERFLOW_ERROR_M      (1u << APP_EVENT_UART_DRIVER_OVERFLOW_ERROR_S)

app_controller.h app_controller.c

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

В app_controller.h представлена единственная функция для взаимодействия:

  • App_Controller_Process() — выполняет обработку событий и пользовательских команд, координируя работу модулей системы.

В файле app_controller.c реализованы вспомогательные функции для взаимодействия с системой. В данном разделе будет рассмотрена только функция, описанная выше, поскольку остальные по сути представляют собой вызовы функций сервисного уровня в определенном порядке.

Функция App_Controller_Process() последовательно выполняет несколько функций:

  1. Считывает флаги прерываний (частный случай события в системе).

  2. Обрабатывает прерывания (если они есть).

  3. Считывает пришедшие команды.

  4. Выполняет полученные команды (если они есть).

  5. Считывает остальные события системы, которые могли произойти при выполнении команд.

  6. Обрабатывает эти события (если они есть).

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

Вспомогательные файлы

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

Тестирование

В рамках разработки программного обеспечения было проведено тестирование сервисного уровня системы с использованием фреймворка Ceedling. Тестирование выполнялось не на уровне аппаратных драйверов, а на уровне сервисов, реализующих основную логику обработки данных, команд и событий. Для изоляции тестируемых модулей вместо реальных драйверов использовались mock-заглушки, автоматически генерируемые средствами CMock. Это позволило исключить зависимость от аппаратной платформы и сосредоточиться на проверке корректности работы программной логики.

В процессе тестирования проверялись корректность вызова основных функций, правильность обработки входных данных и команд, корректность разбора аргументов, а также формирование событий, которые могли возникнуть при ошибках в системе. Для проверки результатов использовались assert-проверки.

🏠 Home

📘 Project Overview

🔧 Project Hardware

💻 Project Firmware

✅ Getting Started

📚 References

Clone this wiki locally