Русский
Русский
English
Статистика
Реклама

Разработка измерительного прибора ИРИС

image
Приветствую, сообщество Хабра. Недавно наша компания выпустила на рынок контрольно-измерительный прибор ИРИС. Являясь главным программистом этого проекта, хочу рассказать вам про разработку прошивки прибора (По оценке руководителя проекта прошивка составляет не более 30% от общего объема работ от идеи до серийного производства). Статья в первую очередь будет полезна начинающим разработчикам в плане понимания трудозатрат на реальный проект и пользователям, которые желают заглянуть под капот.

Назначение прибора


ИРИС многофункциональный измерительный прибор. Он умеет измеряет ток (амперметр), напряжение (вольтметр), мощность (ваттметр) и ряд других величин. КИП ИРИС запоминает их максимальные значения, пишет осциллограммы. С подробным описанием устройства можно ознакомиться на сайте компании.

Немножко статистики


Сроки


Первый коммит в SVN: 16 мая 2019.
Релиз: 19 июня 2020.
*это календарное время, а не фулл-тайм разработка на протяжении всего срока. Были отвлечения на другие проекты, ожидания ТЗ, итераций железа и т.д.

Коммиты


Количество в SVN: 928
Откуда столько?
1) Являюсь сторонником микрокоммитов при разработке
2) Дубли в ветках под железо и эмулятор
3) Документация
Так что, количество с полезной нагрузкой в виде нового кода (ветка trunk) не больше 300.
image

Количество строк кода


Статистика собиралась утилитой cloc с дефолтными параметрами без учета исходников HALa STM32 и ESP-IDF ESP32.
image
Прошивка STM32: 38334 строк кода. Из них:
60870-5-101: 18751
ModbusRTU: 3859
Осциллограф: 1944
Архиватор: 955
Прошивка ESP32: 1537 строк кода.

Аппаратные компоненты (задействованная периферия)


Основные функции прибора реализованы в прошивке STM32. За связь по Bluetooth отвечает прошивка ESP32. Общение между чипами осуществляется по UARTу (см. рисунок в шапке).
NVIC контроллер прерываний.
IWDG сторожевой таймер для перезапуска чипа в случае зависания прошивки.
Timers прерывания по таймеру обеспечивают сердцебиение проекта.
EEPROM память для хранения производственной информации, уставок, показаний максиметра, калибровочных коэффициентов АЦП.
I2C интерфейс для доступа к чипу EEPROM.
NOR память для хранения осциллограмм.
QSPI интерфейс для доступа к чипу NOR памяти.
RTC часы реального времени обеспечивают ход времени после выключения прибора.
ADC АЦП.
RS485 последовательный интерфейс для подключения по протоколам ModbusRTU и 60870-101.
DIN, DOUT дискретный вход и выход.
Button кнопка на передней панели прибора для переключения индикации между измерениями.

Архитектура ПО


Основные модули ПО


image

Поток данных измерений


image

Операционная система


С учетом ограничений объема флеш памяти (ОС вносит накладные расходы) и относительной простоты прибора было решено отказаться от использования операционной системы и обойтись прерываниями. Такой подход уже не раз освещался в статьях на хабре, поэтому приведу лишь блок-схемы задач внутри прерываний с их приоритетами.
image

Пример кода. Генерация отложенного прерывания в STM32.


// Инициализация прерывания с приоритетом 6  HAL_NVIC_SetPriority(CEC_IRQn, 6, 0);  HAL_NVIC_EnableIRQ(CEC_IRQn);// Генерация прерыванияHAL_NVIC_SetPendingIRQ(CEC_IRQn);// Обработчикvoid CEC_IRQHandler(void) {// user code}


ШИМ 7 сегментной индикации


В приборе две строки по 4 символа, всего 8 индикаторов. У 7-и сегментных индикаторов имеются 8 спараллеленных линий данных (A,B,C,D,E,F,G,DP) и по 2 линии выбора цвета (зеленый и красный) для каждого.
image

Хранилище осциллограмм


Хранилище организовано по принципу циклического буфера со слотами по 64 КБ на осциллограмму (фиксированный размер).

Обеспечение целостности данных при неожиданном выключении


В EEPROM данные пишутся в двух копиях с добавленной контрольной суммой в конце. Если в момент записи данных прибор будет выключен, то хотя бы одна копия данных останется целостной. Контрольной суммой также дополняется каждый срез данных осциллографа (значения измерений на входах АЦП), таким образом, невалидная контрольная сумма среза будет признаком окончания осциллограммы.

Автоматическая генерация версии ПО


1) Создать файл version.fmt:
#define SVN_REV ($WCREV$)
2) Перед сборкой проекта добавить команду (для System Workbanch):
SubWCRev ${ProjDirPath} ${ProjDirPath}/version.fmt ${ProjDirPath}/version.h
После выполнения этой команды будет создан файл version.h с номером последнего коммита.

Аналогичная утилита есть и для GITa: GitWCRev. /version.fmt ./main/version.h
#define GIT_REV ($WCLOGCOUNT$)
Это позволяет однозначно сопоставлять коммит и версию ПО.

Эмулятор


Т.к. разработка прошивки началась до появления первого экземпляра железа, то часть кода начал писать как консольное приложение на ПК.
image
Преимущества:
разработка и отладка под ПК проще, чем непосредственно на железе.
возможность генерации любых входных сигналов.
возможность отладки клиента на ПК без железа. На ПК ставится драйвер com0com, который создает пару com-портов. На одном из них запускается эмулятор, а на другом подключается клиент.
способствует красивой архитектуре, т.к. приходится выделять интерфейс аппаратно-зависимых модулей и писать две реализации

Пример кода. Две реализации чтения данных из eeprom.


uint32_t eeprom_read(uint32_t offset, uint8_t * buf, uint32_t len);ifdef STM32H7uint32_t eeprom_read(uint32_t offset, uint8_t * buf, uint32_t len){  if (diag_isError(ERR_I2C))    return 0;if (eeprom_wait_ready()) {HAL_StatusTypeDef status = HAL_I2C_Mem_Read(&I2C_MEM_HANDLE, I2C_MEM_DEV_ADDR, offset, I2C_MEMADD_SIZE_16BIT, buf, len, I2C_MEM_TIMEOUT_MS);if (status == HAL_OK)return len;}diag_setError(ERR_I2C, true);  return 0;}#endif#ifdef _WIN32static FILE *fpEeprom = NULL;#define EMUL_EEPROM_FILE "eeprom.bin"void checkAndCreateEpromFile() {if (fpEeprom == NULL) {fopen_s(&fpEeprom, EMUL_EEPROM_FILE, "rb+");if (fpEeprom == NULL)fopen_s(&fpEeprom, EMUL_EEPROM_FILE, "wb+");fseek(fpEeprom, EEPROM_SIZE, SEEK_SET);fputc('\0', fpEeprom);fflush(fpEeprom);}}uint32_t eeprom_read(uint32_t offset, uint8_t * buf, uint32_t len){checkAndCreateEpromFile();fseek(fpEeprom, offset, SEEK_SET);return (uint32_t)fread(buf, len, 1, fpEeprom);}#endif


Ускорение передачи данных (архивация)


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

Пример кода. Архивация данных.


#define ARCHIVER_HASH_BITS (12)uint8_t __RAM_288K archiver_hash_table[sizeof(uzlib_hash_entry_t) * (1 << ARCHIVER_HASH_BITS)];bool archive(const uint8_t* src, uint32_t src_len, uint8_t* dst, uint32_t dst_len, uint32_t *archive_len){struct uzlib_comp comp = { 0 };comp.dict_size = 32768;comp.hash_bits = ARCHIVER_HASH_BITS;comp.hash_table = (uzlib_hash_entry_t*)&archiver_hash_table[0];memset((void*)comp.hash_table, 0, sizeof(archiver_hash_table));comp.out.outbuf = &dst[10]; // skip header 10 bytescomp.out.outsize = dst_len - 10 - 8; // skip header 10 bytes and tail(crc+len) 8 bytescomp.out.is_overflow = false;zlib_start_block(&comp.out);uzlib_compress(&comp, src, src_len);zlib_finish_block(&comp.out);if (comp.out.is_overflow)comp.out.outlen = 0;dst[0] = 0x1f;dst[1] = 0x8b;dst[2] = 0x08;dst[3] = 0x00; // FLG// mtimedst[4] =dst[5] =dst[6] =dst[7] = 0;dst[8] = 0x04; // XFLdst[9] = 0x03; // OSunsigned crc = ~uzlib_crc32(src, src_len, ~0);memcpy(&dst[10 + comp.out.outlen], &crc, sizeof(crc));memcpy(&dst[14 + comp.out.outlen], &src_len, sizeof(src_len));*archive_len = 18 + comp.out.outlen;if (comp.out.is_overflow)return false;return true;}


Пример кода. Распаковка данных.


// byte[] res; // сжатые данные                        using (var msOut = new MemoryStream())                        using (var ms = new MemoryStream(res))                        using (var gzip = new GZipStream(ms, CompressionMode.Decompress))                        {                            int chunk = 4096;                            var buffer = new byte[chunk];                            int read;                            do                            {                                read = gzip.Read(buffer, 0, chunk);                                msOut.Write(buffer, 0, read);                            } while (read == chunk);                            //msOut.ToArray();// обработать распакованный массив данных                        }


Про постоянные изменения в ТЗ


Мем с просторов интернета:
Но вы же утвердили техническое задание!
Техническое задание? Мы думали ТЗ это Точка зрения, и у нас их несколько.

Пример кода. Обработка клавиатуры.


enum {IVA_KEY_MASK_NONE,IVA_KEY_MASK_ENTER = 0x1,IVA_KEY_MASK_ANY   = IVA_KEY_MASK_ENTER,}IVA_KEY;uint8_t keyboard_isKeyDown(uint8_t keyMask) {return ((keyMask & keyStatesMask) == keyMask);}

Посмотрев такой кусочек кода вы можете подумать зачем он все это нагородил, если в приборе только одна кнопка? В первой версии ТЗ было 5 кнопок и с помощью них планировалось реализовать редактирование уставок непосредственно на приборе:
enum {IVA_KEY_MASK_NONE  = 0,IVA_KEY_MASK_ENTER = 0x01,IVA_KEY_MASK_LEFT  = 0x02,IVA_KEY_MASK_RIGHT = 0x04,IVA_KEY_MASK_UP    = 0x08,IVA_KEY_MASK_DOWN  = 0x10,IVA_KEY_MASK_ANY   = IVA_KEY_MASK_ENTER | IVA_KEY_MASK_LEFT | IVA_KEY_MASK_RIGHT | IVA_KEY_MASK_UP | IVA_KEY_MASK_DOWN,}IVA_KEY;

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

Некоторые проблемы при разработке


Закончилась флеш


В микроконтроллере имеется 128 Кб флеш памяти. В какой-то момент отладочная сборка превысила этот объем. Пришлось включать оптимизацию по объему -Os. Если же требовалась отладка на железе, то делалась специальная сборка с отключением некоторых программных модулей (модбас, 101-й).

Ошибка данных по QSPI


Иногда, при чтении данных по qspi, появлялся лишний байт. Проблема пропала после увеличения приоритета прерываний qspi.

Ошибка данных в осциллографе


Т.к. данные пересылает DMA, то процессор может их не увидеть и прочитать старые данных из кэша. Нужно выполнять валидацию кэша.

Пример кода. Валидация кэша.


// сброс данных из кэша в озу перед записью их в QSPI/DMASCB_CleanDCache_by_Addr((uint32_t*)(((uint32_t)&data[0]) & 0xFFFFFFE0), dataSize + 32);// Обновление данных от ADC/DMA в кэше перед обращением из CPUSCB_InvalidateDCache_by_Addr((uint32_t*)&s_pAlignedAdcBuffer[0], sizeof(s_pAlignedAdcBuffer));


Проблемы с АЦП(разные показания от включения к включению)


От включения к включению в приборе появлялось разное смещение показаний тока (порядка 10-30 мА). Решение помогли найти коллеги из Компэла в лице Владислава Барсова и Александра Квашина за что им огромное спасибо.

Пример кода. Инициализация АЦП.


// Нужно запомнить параметры калибровки и выставлять их при включенииHAL_ADCEx_Calibration_SetValue (&hadc1, ADC_SINGLE_ENDED, myCalibrationFactor[0]);HAL_ADCEx_Calibration_SetValue (&hadc1, ADC_DIFFERENTIAL_ENDED, myCalibrationFactor[1]);HAL_ADCEx_LinearCalibration_SetValue (&hadc1, &myLinearCalib_Buffer[0]);


Засветка индикации


На пустых 7-и сегментных индикаторах вместо полного отключения появлялась слабая засветка. Причина в том, что в реальном мире форма сигнала не идеальна, и если вы выполнили код gpio_set_level(0), то это еще не значит, что уровень сигнала сразу же изменился. Устранить засветку удалось добавлением ШИМа к линиям данных.

Ошибка uart в HAL


После возникновения ошибки Over-Run UART переставал работать. Устранить проблему удалось патчем HALa:

Пример кода. Патч для HALa.


---    if (((isrflags & USART_ISR_ORE) != 0U)---        && (((cr1its & USART_CR1_RXNEIE_RXFNEIE) != 0U) ||---            ((cr3its & (USART_CR3_RXFTIE | USART_CR3_EIE)) != 0U)))+++    if ((isrflags & USART_ISR_ORE) != 0U)    {      __HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_OREF);


Доступ к невыровненным данным


Ошибка проявлялась только на железе в сборке с уровнем оптимизации -Os. Вместо реальных данных клиент по модбас читал нули.

Пример кода. Ошибка чтения невыровненных данных.


float f_value;uint16_t registerValue;// Вместо реальных данных в registerValue оказывался 0//registerValue = ((uint16_t*)&f_value)[(offsetInMaximeterData -//offsetof(mbreg_Maximeter, primaryValue)) / 2];// То же самое через memcpy работат корректно    memcpy(& registerValue, ((uint16_t*)&f_value) + (offsetInMaximeterData -        offsetof(mbreg_Maximeter, primaryValue)) / 2, sizeof(uint16_t));


Поиск причин HardFaultов


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

Пример кода. SET_DEBUG_POINT(__LINE__).


//debug.h#define USE_DEBUG_POINTS#ifdef USE_DEBUG_POINTS//инструмент для поиска места хардфолтов SET_DEBUG_POINT1(__LINE__)void SET_DEBUG_POINT1(uint32_t val);void SET_DEBUG_POINT2(uint32_t val);#else#define SET_DEBUG_POINT1(...)#define SET_DEBUG_POINT2(...)#endif//debug.c#ifdef USE_DEBUG_POINTSvolatile uint32_t dbg_point1 = 0;volatile uint32_t dbg_point2 = 0;void SET_DEBUG_POINT1(uint32_t val) {  dbg_point1 = val;}void SET_DEBUG_POINT2(uint32_t val) {  dbg_point2 = val;}#endif// В нескольких местах в коде:SET_DEBUG_POINT1(__line__);


Советы новичкам


1) Заглянуть в примеры кода. Для esp32 примеры ставятся вместе с SDK. Для stm32 в хранилище HALa STM32CubeMX \STM32Cube_FW_H7_V1.7.0\Projects\NUCLEO-H743ZI\Examples\
2) Гуглить: programming manual <ваш чип>, technical reference manual <ваш чип>, application note <ваш чип>, datasheet <ваш чип>.
3) Если возникли какие-то технические сложности и 2 верхних пункта не помогли, то не следует пренебрегать и обращением в поддержку, а лучше к дистрибьютерам, которые имеют прямой контакт с инженерами компании производителя.
4) Баги бывают не только в вашем коде, но и в HALe производителя.

Спасибо за внимание.
Источник: habr.com
К списку статей
Опубликовано: 01.10.2020 14:20:35
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Программирование микроконтроллеров

Микроконтроллеры

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru