Применение Arm Mbed OS. Тонкая настройка
После того как с помощью Arm Mbed OS удалось помигать светодиодом, настало время протестировать и настроить другие важные сервисы. Далее рассказывается:
- Технология конфигурирование Mbed
- Почему сложно перейти на C++ в обычных RTOS
- Как экономить память в RTOS
- Как организуются прерывания в Mbed OS
- Чем удобно отлаживать Mbed OS
- Как избавиться от лишнего слоя абстракции SDK
Продолжаем знакомство с технологией программирования микроконтроллеров семейства MKE18F c использованием ARM Mbed OS
Технология конфигурирования Mbed
Важной составляющей проекта Mbed является система автоматического конфигурирования и сборки, как в online режиме так и в offline, т.е. локально на компьютере пользователя. Конфигурировать предлагается путем редактирования файлов в формате .json со специальными именами. Затем находящиеся в проекте Python скрипты преобразуют эти файлы в заголовочные файлы, файлы рабочих пространств выбранной пользователем IDE, командные файлы линкеров и прочие вспомогательные файлы.
Но проблема описанного метода в непрозрачности с точки зрения исходных текстов, так как нам очень трудно отследить где и что в исходниках меняет система конфигурирования. С другой стороны в нашем случае нет никаких мотивов поддерживать способность проекта автоматически переноситься на разные IDE.
Поэтому от такого подхода решено было отказаться. Как писалось в предыдущей статье, был просто сформирован в online проект для IDE IAR, получена неструктурированная куча файлов в рабочем пространстве IDE, затем выполнена их систематизация и отброшено ненужное. В результате больше не нужно делать конфигурирование через .json файлы и осталось только три конкретных места где находятся параметры влияющие на конфигурацию Mbed:
- Опции компилятора в среде IDE
- Командный файл MKE18F512xxx16_flash.icf линкера
- Заголовочный файл mbed_config.h
В файле mbed_config.h можно насчитать около 130 дефайнов, что поначалу очень напрягает. Но к счастью большинство из них относится к стекам беспроводных протоколов, которые к данному моменту в проекте не используются. Для удобства записи были отсортированы чтобы актуальные разместились вверху. Исходно дефайны для разных модулей в файле mbed_config.h расположены хаотично, но после сортировки он выглядит так:
#ifndef __MBED_CONFIG_DATA__
#define __MBED_CONFIG_DATA__
// Configuration parameters
#define MBED_CONF_RTOS_PRESENT 1 // set by library:rtos
#define MBED_ALL_STATS_ENABLED 1
//#define DEVICE_SLEEP=1 Снять комментарий если нужно переводить в SLEEP в случае отсутствия активных задач
#define MBED_CONF_APP_MAIN_STACK_SIZE 1024
#define MBED_CONF_APP_TIMER_THREAD_STACK_SIZE 512
#define MBED_CONF_APP_IDLE_THREAD_STACK_SIZE 512
#define MBED_CONF_PLATFORM_DEFAULT_SERIAL_BAUD_RATE 3000000 // set by library:platform
#define MBED_CONF_PLATFORM_ERROR_ALL_THREADS_INFO 0 // set by library:platform
#define MBED_CONF_PLATFORM_ERROR_FILENAME_CAPTURE_ENABLED 0 // set by library:platform
#define MBED_CONF_PLATFORM_ERROR_HIST_ENABLED 0 // set by library:platform
#define MBED_CONF_PLATFORM_ERROR_HIST_SIZE 4 // set by library:platform
#define MBED_CONF_PLATFORM_FORCE_NON_COPYABLE_ERROR 0 // set by library:platform
#define MBED_CONF_PLATFORM_MAX_ERROR_FILENAME_LEN 16 // set by library:platform
#define MBED_CONF_PLATFORM_POLL_USE_LOWPOWER_TIMER 0 // set by library:platform
#define MBED_CONF_PLATFORM_STDIO_BAUD_RATE 3000000 // set by library:platform
#define MBED_CONF_PLATFORM_STDIO_BUFFERED_SERIAL 0 // set by library:platform
#define MBED_CONF_PLATFORM_STDIO_CONVERT_NEWLINES 1 // set by library:platform
#define MBED_CONF_PLATFORM_STDIO_CONVERT_TTY_NEWLINES 0 // set by library:platform
#define MBED_CONF_PLATFORM_STDIO_FLUSH_AT_EXIT 1 // set by library:platform
#define MBED_CONF_DRIVERS_UART_SERIAL_RXBUF_SIZE 256 // set by library:drivers
#define MBED_CONF_DRIVERS_UART_SERIAL_TXBUF_SIZE 256 // set by library:drivers
#define MBED_CONF_EVENTS_PRESENT 1 // set by library:events
#define MBED_CONF_EVENTS_SHARED_DISPATCH_FROM_APPLICATION 0 // set by library:events
#define MBED_CONF_EVENTS_SHARED_EVENTSIZE 256 // set by library:events
#define MBED_CONF_EVENTS_SHARED_HIGHPRIO_EVENTSIZE 256 // set by library:events
#define MBED_CONF_EVENTS_SHARED_HIGHPRIO_STACKSIZE 1024 // set by library:events
#define MBED_CONF_EVENTS_SHARED_STACKSIZE 1024 // set by library:events
#define MBED_CONF_EVENTS_USE_LOWPOWER_TIMER_TICKER 0 // set by library:events
#define MBED_CONF_CELLULAR_DEBUG_AT 0 // set by library:cellular
#define MBED_CONF_CELLULAR_RANDOM_MAX_START_DELAY 0 // set by library:cellular
#define MBED_CONF_CELLULAR_USE_APN_LOOKUP 1 // set by library:cellular
#define MBED_CONF_FILESYSTEM_PRESENT 1 // set by library:filesystem
#define MBED_CONF_KINETIS_EMAC_RX_RING_LEN 16 // set by library:kinetis-emac
#define MBED_CONF_KINETIS_EMAC_TX_RING_LEN 8 // set by library:kinetis-emac
#define MBED_CONF_LORA_ADR_ON 1 // set by library:lora
#define MBED_CONF_LORA_APP_PORT 15 // set by library:lora
#define MBED_CONF_LORA_APPLICATION_EUI {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // set by library:lora
#define MBED_CONF_LORA_APPLICATION_KEY {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // set by library:lora
#define MBED_CONF_LORA_APPSKEY {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // set by library:lora
#define MBED_CONF_LORA_AUTOMATIC_UPLINK_MESSAGE 1 // set by library:lora
#define MBED_CONF_LORA_DEVICE_ADDRESS 0x00000000 // set by library:lora
#define MBED_CONF_LORA_DEVICE_EUI {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // set by library:lora
#define MBED_CONF_LORA_DUTY_CYCLE_ON 1 // set by library:lora
#define MBED_CONF_LORA_LBT_ON 0 // set by library:lora
#define MBED_CONF_LORA_NB_TRIALS 12 // set by library:lora
#define MBED_CONF_LORA_NWKSKEY {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // set by library:lora
#define MBED_CONF_LORA_OVER_THE_AIR_ACTIVATION 1 // set by library:lora
#define MBED_CONF_LORA_PHY EU868 // set by library:lora
#define MBED_CONF_LORA_PUBLIC_NETWORK 1 // set by library:lora
#define MBED_CONF_LORA_TX_MAX_SIZE 64 // set by library:lora
#define MBED_CONF_LWIP_ADDR_TIMEOUT 5 // set by library:lwip
#define MBED_CONF_LWIP_ADDR_TIMEOUT_MODE 1 // set by library:lwip
#define MBED_CONF_LWIP_DEBUG_ENABLED 0 // set by library:lwip
#define MBED_CONF_LWIP_DEFAULT_THREAD_STACKSIZE 512 // set by library:lwip
#define MBED_CONF_LWIP_ENABLE_PPP_TRACE 0 // set by library:lwip
#define MBED_CONF_LWIP_ETHERNET_ENABLED 1 // set by library:lwip
#define MBED_CONF_LWIP_IP_VER_PREF 4 // set by library:lwip
#define MBED_CONF_LWIP_IPV4_ENABLED 1 // set by library:lwip
#define MBED_CONF_LWIP_IPV6_ENABLED 0 // set by library:lwip
#define MBED_CONF_LWIP_MEM_SIZE 36560 // set by library:lwip[Freescale]
#define MBED_CONF_LWIP_PPP_THREAD_STACKSIZE 768 // set by library:lwip
#define MBED_CONF_LWIP_SOCKET_MAX 4 // set by library:lwip
#define MBED_CONF_LWIP_TCP_ENABLED 1 // set by library:lwip
#define MBED_CONF_LWIP_TCP_SERVER_MAX 4 // set by library:lwip
#define MBED_CONF_LWIP_TCP_SOCKET_MAX 4 // set by library:lwip
#define MBED_CONF_LWIP_TCPIP_THREAD_STACKSIZE 1200 // set by library:lwip
#define MBED_CONF_LWIP_UDP_SOCKET_MAX 4 // set by library:lwip
#define MBED_CONF_LWIP_USE_MBED_TRACE 0 // set by library:lwip
#define MBED_CONF_MBED_MESH_API_6LOWPAN_ND_CHANNEL 0 // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_6LOWPAN_ND_CHANNEL_MASK 0x7fff800 // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_6LOWPAN_ND_CHANNEL_PAGE 0 // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_6LOWPAN_ND_DEVICE_TYPE NET_6LOWPAN_ROUTER // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_6LOWPAN_ND_PANID_FILTER 0xffff // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_6LOWPAN_ND_PSK_KEY {0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf} // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_6LOWPAN_ND_PSK_KEY_ID 1 // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_6LOWPAN_ND_SEC_LEVEL 5 // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_6LOWPAN_ND_SECURITY_MODE NONE // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_HEAP_SIZE 32500 // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_THREAD_CONFIG_CHANNEL 22 // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_THREAD_CONFIG_CHANNEL_MASK 0x7fff800 // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_THREAD_CONFIG_CHANNEL_PAGE 0 // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_THREAD_CONFIG_COMMISSIONING_DATASET_TIMESTAMP 0x10000 // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_THREAD_CONFIG_EXTENDED_PANID {0xf1, 0xb5, 0xa1, 0xb2,0xc4, 0xd5, 0xa1, 0xbd } // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_THREAD_CONFIG_ML_PREFIX {0xfd, 0x0, 0x0d, 0xb8, 0x0, 0x0, 0x0, 0x0} // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_THREAD_CONFIG_NETWORK_NAME "Thread Network" // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_THREAD_CONFIG_PANID 0x0700 // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_THREAD_CONFIG_PSKC {0xc8, 0xa6, 0x2e, 0xae, 0xf3, 0x68, 0xf3, 0x46, 0xa9, 0x9e, 0x57, 0x85, 0x98, 0x9d, 0x1c, 0xd0} // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_THREAD_DEVICE_TYPE MESH_DEVICE_TYPE_THREAD_ROUTER // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_THREAD_MASTER_KEY {0x10, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff} // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_THREAD_PSKD "ABCDEFGH" // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_THREAD_SECURITY_POLICY 255 // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_THREAD_USE_STATIC_LINK_CONFIG 1 // set by library:mbed-mesh-api
#define MBED_CONF_MBED_MESH_API_USE_MALLOC_FOR_HEAP 0 // set by library:mbed-mesh-api
#define MBED_CONF_NANOSTACK_CONFIGURATION nanostack_full // set by library:nanostack
#define MBED_CONF_NANOSTACK_HAL_CRITICAL_SECTION_USABLE_FROM_INTERRUPT 0 // set by library:nanostack-hal
#define MBED_CONF_NANOSTACK_HAL_EVENT_LOOP_DISPATCH_FROM_APPLICATION 0 // set by library:nanostack-hal
#define MBED_CONF_NANOSTACK_HAL_EVENT_LOOP_THREAD_STACK_SIZE 6144 // set by library:nanostack-hal
#define MBED_CONF_NANOSTACK_HAL_EVENT_LOOP_USE_MBED_EVENTS 0 // set by library:nanostack-hal
#define MBED_CONF_NANOSTACK_HAL_NVM_CFSTORE 0 // set by library:nanostack-hal
#define MBED_CONF_NSAPI_DEFAULT_MESH_TYPE THREAD // set by library:nsapi
#define MBED_CONF_NSAPI_DEFAULT_STACK LWIP // set by library:nsapi
#define MBED_CONF_NSAPI_DEFAULT_WIFI_SECURITY NONE // set by library:nsapi
#define MBED_CONF_NSAPI_DNS_CACHE_SIZE 3 // set by library:nsapi
#define MBED_CONF_NSAPI_DNS_RESPONSE_WAIT_TIME 5000 // set by library:nsapi
#define MBED_CONF_NSAPI_DNS_RETRIES 0 // set by library:nsapi
#define MBED_CONF_NSAPI_DNS_TOTAL_ATTEMPTS 3 // set by library:nsapi
#define MBED_CONF_NSAPI_PRESENT 1 // set by library:nsapi
#define MBED_CONF_PPP_CELL_IFACE_APN_LOOKUP 1 // set by library:ppp-cell-iface
#define MBED_CONF_PPP_CELL_IFACE_AT_PARSER_BUFFER_SIZE 256 // set by library:ppp-cell-iface
#define MBED_CONF_PPP_CELL_IFACE_AT_PARSER_TIMEOUT 8000 // set by library:ppp-cell-iface
#define MBED_CONF_PPP_CELL_IFACE_BAUD_RATE 115200 // set by library:ppp-cell-iface
#define MBED_CONF_TARGET_NETWORK_DEFAULT_INTERFACE_TYPE ETHERNET // set by target:K66F
#define MBED_LFS_BLOCK_SIZE 512 // set by library:littlefs
#define MBED_LFS_ENABLE_INFO 0 // set by library:littlefs
#define MBED_LFS_INTRINSICS 1 // set by library:littlefs
#define MBED_LFS_LOOKAHEAD 512 // set by library:littlefs
#define MBED_LFS_PROG_SIZE 64 // set by library:littlefs
#define MBED_LFS_READ_SIZE 64 // set by library:littlefs
#define NSAPI_PPP_AVAILABLE 0 // set by library:lwip
#define NSAPI_PPP_IPV4_AVAILABLE 1 // set by library:lwip
#define NSAPI_PPP_IPV6_AVAILABLE 0 // set by library:lwip
#define NVSTORE_ENABLED 1 // set by library:nvstore
#define NVSTORE_MAX_KEYS 16 // set by library:nvstore
// Macros
#define _RTE_ // defined by library:rtos
#define NS_USE_EXTERNAL_MBED_TLS // defined by library:nanostack
#define UNITY_INCLUDE_CONFIG_H // defined by library:utest
#endif
Особенности перехода на C++ в применении к RTOS
API верхнего уровня в Mbed написано на C++, поэтому этот язык приходится использовать и в прикладном коде. Но тут есть нюансы о которых необходимо знать.
Использование C++ для RTOS в малых встраиваемых системах еще сравнительная редкость. Проблема здесь в том, что успешные проекты RTOS стремятся быть мультиплатформенными, а С++ предъявляет повышенные требования к менеджменту ресурсов платформы по сравнению с С. Причина в стремлении скрыть от пользователя детали низкоуровневого управления ресурсами. Речь прежде всего о ресурсах памяти. Конструкторы, деструкторы, потоки, исключения c автоматической деструкцией, шаблоны объектов структур данных и др. используют неявные операции с динамической памятью. Но ресурс оперативной памяти RAM в малых системах очень ограничен. Оперативная память — самый дефицитный ресурс в таких системах и особенно в RTOS. В RTOS каждой задаче выделяется стек, его точный размер разработчик заранее спрогнозировать не может и поэтому выбирает с запасом. Таким образом наличие RTOS с десятком задач сразу вызывает необходимость в RAM размером от 10 до 30 кБ. Еще много памяти нужно для различных парсеров и протоколов (HTTP, HTML…) и файловых систем. Если применяется дисплей, то еще более увеличиваются требования к свободной RAM.
Библиотеки сред разработки типа IAR оснащаются неплохими менеджерами динамической памяти, но они рассчитаны на однопоточную среду исполнения. Чтобы они начали работать в RTOS нужно написать дополнительный код. Этот процесс называется retargeting.
В RTOS написанных на C ретаргетинг как правило не производится. Поскольку там нет неявных операций с динамической памятью на уровне языка, то все операции выполняются явно вызовом собственных потокобезопасных вариантов функций malloc и free. Программист имеет полный контроль над операциями с динамической памятью и легко может применять все возможные меры по ее экономии.
В случае C++ если мы хотим использовать все возможности этого языка нам придется делать retargeting. Но retargeting в каждой среде разработки процесс сугубо индивидуальный. Это и усложняет жизнь разработчикам RTOS.
На рисунке ниже пример структуры вызовов с ретаргетингом. Функции __write, __lseek, __read могут пользователем и не реализовываться, но тогда их функциональность остается на усмотрение IDE. И уж точно printf и scanf не будут многопоточными.
Mbed одна из немногих если не единственная RTOS, которая предоставляет исходники с уже проделанным ретаргетингом под триаду известных средств разработки: GCC, IAR, Keil
Несмотря на все сказанное выше можно встретить статьи о портировании RTOS на C++ без выполнения ретаргетинга, например решая проблему простой заменой некоторых распространненных стандартных функций на свои. Это работать может, но программисту тогда нужно помнить о разных неявных и недокументированных ограничениях при использовании конструкций C++ в IAR(только статические конструкторы, проверять все шаблоны на использование new, отказаться от исключений и т.д.). Это уже будет сложно назвать C++. Mbed как система дружелюбная к пользователю снимает многие такие ограничения приближаясь по простоте к Arduino.
Помимо всего в свежих версиях IAR есть трудности перехода на C11 и C++14 о чем написано здесь — https://www.iar.com/support/resources/articles/exploring-c11-and-c14/
Как организуются прерывания в Mbed
Как ни странно, но найти класс или функцию или что-то подходящее для организации обслуживания прерываний в API Mbed не удастся. Можно обнаружить только класс InterruptIn, который предначзначен только для внешних портов.
Ответы на такие вопросы надо искать в CMSIS-RTOS, а именно в хидерах CMSIS Cortex-M4 Core Peripheral Access Layer. Там определены макросы:
- NVIC_SetVector — устанавливает для заданного вектора процедуру обслуживания прерываний (interrupt service routine, ISR)
- NVIC_SetPriority — устанавливает приоритет для заданного вектора прерываний.
- NVIC_EnableIRQ — разрешает вызов прерываний по заданному вектору
Вот как инициализируется организация прерыванияй от таймера SysTick:
NVIC_SetVector(mbed_get_m0_tick_irqn(), (uint32_t)SysTick_Handler);
// Здесь функция mbed_get_m0_tick_irqn возвращает номер вектора прерывания от таймера SysTick.
// Тут можно было бы сразу вставить имя SysTick_IRQn их заголовочного файла MKE18F16.h
// Функция SysTick_Handler является обработчиком прерывания.
NVIC_SetPriority(mbed_get_m0_tick_irqn(), 0xFF); // 0xFF здесь назначает минимальный приоритет. Чем больше число тем меньше приоритет. В контроллере NVIC реализованном в семействые MKE18F16 возможны только 16 уровней приоритетов. Т.е. значащими будут 4-е младших бита.
NVIC_EnableIRQ(mbed_get_m0_tick_irqn()); // Разрешает вызов обработчика прерывания.
Нельзя путать приоритеты задач Mbed и приоритеты прерываний.
Если приоритет не назначается, то по умолчанию он устанавливается максимальным.
Из назначенных таким образом обработчиков прерываний можно вызывать любые сервисы RTOS, которые не вызывают ожиданий. Т.е. передавать флаги, семафоры, сообщения, майлбоксы и проч. Сервисы вызываются однако не из самого ISR, а путем вызова программного прерываний установкой бита PENDSVSET в регистре Interrupt Control and State Register (ICSR) блока System control block (SCB) ядра Cortex-M. Т.е. после завершения текущего обработчика прерываний если нет других приоритетных прерываний произойдет вызов системного обработчика по вектору PendSV где и будет проведено обслуживание.
Как в Mbed происходит работа с динамической памятью
Динамическая память или иначе куча или heap — обязательный компонент при программировании на C++. В нашем проекте Mbed под IAR определение размера этой области памяти производится в файле настроек линкера MKE18F512xxx16_flash.icf с помощью записи в переменную __size_heap__. Размер ставим так чтобы он занял всю оставшуюся свободную память. Сколько осталось свободной памяти узнаем из .map файла после компиляции, т.е. определение размера heap — процесс итеративный.
Вызов статических конструкторов объектов C++
Важным вопросом при использовании C++ является где и как вызываются конструкторы глобальных объектов. Даже в RTOS претендующих на серьезность, например МАКС, это обойдено вниманием, т.е. пущено на самотек. Там конструкторами занимается стандартная однопоточная библиотека среды разработки с обычным однопоточным механизмом выделения памяти. Но после старта распространенные RTOS создают свой механизм управления динамической памятью, а память занятая глобальными объектами остается забытой. Это дыра в наших усилиях по экономии памяти и контролю над всем.
В Mbed к данному вопросу подошли гораздо серьезней. Там для каждой среды разработки имеется собственный подход. В IAR это делается так:
- поток инициализации еще до вызова конструкторов перехватывается пользовательским кодом
- в стандартную библиотеку подставляются методы блокировки потоков из API RTOS
- перенаправляются стандартные функции new, delete, malloc, free… на обращения к API работы с динамической памятью RTOS.
Mbed использует адаптеры библиотек IAR для работы в много-поточном режиме
Об адаптации IAR под многопоточность можно почитать здесь — http://supp.iar.com/FilesPublic/UPDINFO/005691/arm/doc/infocenter/DLIBThreadSupport.html
В Mbed адаптированы как системные блокировки (System locks) так и поточные блокировки (File stream locks) библиотеки IAR. Они реализованы в файле mbed_boot.c и используют мьютексы OS.
В функции __iar_program_start исполняемой в первых же строчках файла startup_MKE18F16.s происходит инициализация стека и динамической памяти OS с помощью вызова mbed_set_stack_heap
Назначение размеров стеков задач
Урезание стеков задач до минимума — самый привлекательный вариант экономии RAM.
Чтобы задачи требовали меньше стека применяются разные техники. Например, на стек сильно влияют библиотечные функции для вывода и форматирования строк printf, sprintf, scanf и т.д. Они имеют особенность выделять в стеке большие временные области для хранения данных. Если мы откажемся в задаче использовать эти функции, то сможем сократить стек задачи на каких-нибудь добрых пару сотен байт.
Mbed OS при старте сразу создает три задачи с именами: «main_thread», «timer_thread», «idle_thread». Размер стека по умолчанию для них определялся макросами в заголовочном файле mbed_rtx_conf.h. Мы перенесли объявления этих стеков в файл конфигурации mbed_config.h и сократили размер стеков. Теперь определения выглядят так:
- Стек задачи «main_thread» определяется макросом MBED_CONF_APP_MAIN_STACK_SIZE = 1024 байт
- Стек задачи «timer_thread» определяется макросом MBED_CONF_APP_TIMER_THREAD_STACK_SIZE = 512 байт
- Стек задачи «idle_thread» определяется макросом MBED_CONF_APP_IDLE_THREAD_STACK_SIZE = 512 байт
Средства контроля за использованием ресурсов памяти в Mbed
Динамическая память равно как и стек — это ресурсы требующие постоянного внимания. Чтобы видеть насколько использована динамическая память и какова интенсивность запросов к ней, сколько осталось стека у каждой задачи и какова была пиковая загрузка стеков в Mbed есть специальные счетчики. По умолчанию они отключены директивой условной компиляции, для их включения надо объявить дефайн MBED_ALL_STATS_ENABLED. Когда дефайн объявлен, нужно написать свою процедуру для вывода информации пользователю. Мы написали специальную процедуру для вывода статистики в эмулятор терминала VT100, о чем будет рассказано позже.
Помимо средств предоставляемых OS среда разработки IAR в последних версиях добавляет новую возможность — стековые канарейки. Прочитать о них можно здесь — https://www.iar.com/support/resources/articles/stack-protection-in-iar-embedded-workbench-for-arm/
Общие вопросы защиты стека от переполнения рассмотрены здесь — https://www.iar.com/support/resources/articles/detecting-and-avoiding-stack-overflow-in-embedded-systems/
и здесь — https://www.iar.com/support/resources/articles/mastering-stack-and-heap-for-system-reliability/
Средства отладки и анализа кода Mbed
По настоящему глубоко изучить работу Mbed на новой платформе можно только с использованием JTAG/SWD отладчика.
Исходники Mbed насыщены многоуровневыми макросами и инструкциями условной компиляции. Просто рассматривая исходники нельзя сказать какие функции работают, а какие нет, куда выполнение программы заходит, а куда не заходит. Единственным выходом остается подключить отладчик и по шагам анализировать путь выполнения программы. Уже на этапе портирования практически невозможно обойтись без пошаговой отладки.
Я рекомендовал бы отладчики фирмы Segger версии J-Link Pro и J-Link Ultra. Их отличает высокая пропускная способность, в несколько раз выше чем у распространенных дешевых моделей. Для отладки систем с жестким реальным временем это немаловажно. При трассировке быстрых событий такие отладчики меньше подвержены переполнениям и требуют меньше отладочных итераций. Помимо пошаговой отладки они способны выполнять лог прерываний, вести статистику прерываний, включая их длительности выполнения, поддерживают технологии отладки RTT и ITM, перехватывают аппаратные исключения и другие функции.
Ниже вид окна отладчика IAR при работе через адаптер J-Link.
Экономить не стоит, 90% времени разработки уходит на отладку. Хотя более дорогие трассировщики J-Trace от Segger уже не дают большого преимущества, поскольку серия MKE18F не оснащена специальным трассировочным интерфейсом.
Вторым средством отладки, конечно, является ввод-вывод через UART. В Mbed реализовано как минимум четыре разных технологии доступа к последовательному каналу обмена данными через UART,
Это:
- перенаправление через класс DirectSerial стандартных функций ввода-вывода языка C/C++
- класс RawSerial
- класс Serial
- класс UARTSerial
Довольно необычное разнообразие. Все четыре могут быть использованы одновременно для вывода в один и тот же порт. Какие-то из них более функциональны, какие-то менее, какие-то документированы, а какие-то нет. В дальнейшем в нашем проекте будем использовать RawSerial, как наименее затратный по ресурсам класс.
Сколько мы теряем таймеров портировав Mbed
Для мигания светодиодом использовалась функция wait из API Mbed OS. В прошлой статье было рассказано о проблемах ее портирования. Но это не все, кроме этого Mbed ведет счетчик своей работы (его можно прочитать функцией mbed_stats_cpu_get) и есть API таймера. Эти сервисы используют низкоуровневые функции из файла lp_ticker.c. В этом файле для организации счета времени используется таймер LPTMR из набора периферии Kinetis. В процессе портирования этот файл был отредактирован для соответствия способам тактирования примененным в микроконтроллере MKE18F512VLL16.
Таким образом порт Mbed полностью захватывает два модуля счетчиков — PIT и LPTMR и таймер ядра SysTick. Об этом надо помнить планируя ресурсы для прикладной задачи.
Особенности начальной загрузки MKE18F
Чипы семейства MKE18F имеют встроенный ROM с начальным загрузчиком через последовательные интерфейсы: UART, CAN, SPI, I2C. Но на нашей плате планируется использовать свой защищенный загрузчик, поэтому работа штатного загрузчика нежелательна.
В этом случае у чипов Kinetis надо обращать внимание на содержимое области Program Flash по адресу 0×040D. Там хранится константа определяющая порядок начальной загрузки. В нашем случае там должна быть записана константа 0×7B, указывающая на старт из Flash памяти, а не из ROM и отключающая функцию NMI на внешнем выводе. При ином значении этой константы программа может зависнуть в случайно вызванном из ROM встроенном загрузчике.
Еще важно помнить, что запись во Flash память контроллера возможна только на частоте ядра 120 МГц и не выше, т.е. в режиме HRUN запись во Flash невозможна.
Активизируем Watchdog
Плата нашего контроллера предназначена для промышленного применения и значит без Watchdog-а обойтись нельзя.
Изначально макрос DISABLE_WDOG в файле system_MKE18F16.h был установлен для отключения watchdog-а. Чтобы исправить ситуацию этот макрос был стерт и реализована своя инициализация WDOG.
Инициализация Watchdog-а производится в функции SystemInit. Обновление счетчика watchdog-а сделано в задаче IDLE.
Такой подход требует чтобы более высокоприоритетные задачи не захватывали монопольно процессор более чем на 100 мс. Но это легко может произойти, например, при выводе на терминал больших дампов данных. Поэтому везде разбиваем длительные процедуры в задачах с приоритетом больше чем у IDLE на короткие фрагменты перемежаемые паузами c использованием функции wait.
Вопрос драйверов из поставки SDK
Драйвера SDK имеют префикс fsl находятся в директории NXP_MKE18F_drivers и являются своеобразным слоем абстракции периферии. Они по идее, должны облегчать программирование сложной для изучения периферии, но к сожалению минимально документированы. Вернее их документирование ограничивается комментариями в заголовках функций. Возникает недоумении для кого тогда они написаны, и как они могут освободить нас от изучения мануалов на периферию микроконтроллере. Мой ответ — никак. Драйвера всего лишь помогают легче переносить программы на разные микроконтроллеры в пределах одного семейства. Но чтобы их эффективно применять нужно очень хорошо разобраться с документацией на периферию. Таким образом драйвера SDK решают довольно частную проблему разработчиков самих драйверов, далекую от нужд пользователей начинающих изучать Kinetis.
Драйвера также предназначены для работы на всех чипах семейства т.е. быть универсальными и поэтому насыщены условными макросами и проверками которые для каждого конкретного чипа не несут никакой полезной функции.
Драйвера еще некоторым образом могут помочь лучше понять как следует обращаться с периферией, но после прихода понимания драйвера можно смело игнорировать.
В связи со сказанным не должно удивлять, что в данном проекте обращение к периферии не затронутой портом Mbed идет напрямую минуя драйвера.
Могут однако возникнуть опасения по поводу зависимостей работы той или иной периферии от наличия драйверов SDK. Тут приходится исследовать исходный код драйверов. Главная опасность зависимостей исходит от разделения функций DMA драйверами. Чтобы этого не случилось нужно отследить и запретить штатным сервисам Mbed использовать DMA при работе с периферией. Если DMA остается нетронутым драйверами SDK, то практически все, что не касается 2-х упомянутых модулей таймеров (PIT и LPTMR) и отладочного UART-а можно из директории NXP_MKE18F_drivers выкинуть или игнорировать.
Меньшая опасность, но тоже существенная может исходить от назначения приоритетов задействованной через драйвера SDK периферии. Например надо знать какой приоритет назначен прерываниям SysTick, отладочного UART-а, и таймеров. В ситуации когда их приоритет равен или больше приоритету периферии используемой в realtime управлении это может привести к деградации качества управления.
Помним, что порт Mbed для MKE18F инициализирует прерывания UART и таймеров без назначения приоритета, т.е. они по умолчанию получают максимальный уровень.