Pull to refresh

Медленный CrossWorks for ARM?

Reading time5 min
Views2.4K
Original author: Vladimir Petrigo


На моей текущей работе мы используем CrossWorks for ARM IDE со встроенным GCC в качестве среды разработки приложений для встраиваемых систем. До недавнего времени никто не замечал проблем с этим, пока мы не начали работать над проектом у которого требования к выходу системы из спящего режима оказались «выше обычного».


Упомянутая система работала под управлением процессора STM32L4 (ядро ARM Cortex-M4) и имела в качестве одного из источников пробуждения пьезо-кнопку. Кнопка подключена к линии MCU и пользовательское нажатие на неё генерирует прерывание, от которого происходит пробуждение. Необходимость в ускорении времени пробуждения системы возникла по нескольким причинам:


  • наша пьезо-кнопка притягивает сенсорную линию к уровню нуля на 100-200 мс в случае, когда пользователь делает естественное касание и не пытается её продавить. Если бы использовалась обычная механическая кнопка, то наверняка проблема, о которой я хочу рассказать, осталась бы незамеченной, так как те экземпляры, с которыми я работал, прижимали сенсорную линию на 500+ мс.
  • схема включения пьезо-кнопки не предусматривала какую-либо аппаратную защиту от дребезга (причина сейчас уже не важна и к теме статьи не относится, поэтому эти детали опустим) и, как следствие, это привело к тому, что случались ложные пробуждения, которые нужно было отлавливать

О том, как мы обнаружили проблему и как с помощью небольших правок в реализации CRT (C Run-time) стандартной библиотеки CrossWorks for ARM добились ощутимого ускорения дальше.


Эта статья может быть особенна интересна тем, кто пользуется следующими средами разработки:


  • CrossWorks for ARM (очень вероятно, что все проекты для ARM Cortex-M ядер будут подвержены задержкам при старте)
  • Segger Embedded Studio (SES)

Когда приходит проблема


Наиболее точно проблему опишет осциллограмма:


Figure_with_issue


На ней можно увидеть два маркера:


  • Маркер A показывает примерное время генерации сигнала на пробуждение (если быть совсем точным, то этот сигнал будет сгенерирован, когда уровень напряжения на линии упадёт ниже $V_{IL} = 0.3 \cdot V_{DDIOx} \approx 1 \; V$)
  • Маркер B установлен на точку, когда прошла начальная инициализация MCU (память, GPIO, последовательные интерфейсы, АЦП, и т.д.) и логика основного приложения начинает выполняться

Видно, что это почти 300 мс до того момента, как наше приложение начинает что-то делать! Проблема была в следующем: мы хотим быть уверены, что событие сгенерировано кнопкой, но её состояние за время пробуждения системы стало таким, как будто нажатия не было вовсе. И всё это привело к тому, что кроме ложноположительных срабатываний, вызванных дребезгом на линии, обычные пользовательские действия тоже расценивались как дребезг:


  • пользователь нажимает на кнопку
  • MCU пробуждается и начинается инициализация
  • на данном этапе мы хотим понять причину пробуждения и её корректность: запускается debounce-алгоритм для проверки события
  • debounce-алгоритм знает, что причиной пробуждения была кнопка, но она в данный момент уже не нажата — «Это дребезг», — говорит алгоритм
  • система уходит в спящий режим
  • пользователь в недоумении

Это не совсем то, что ожидает пользователь.


Заметка


Эту проблему можно было бы игнорировать и просто предположить, что пользователи приспособятся и будут жать на кнопку дольше. «Тактильность» используемой пьезо-кнопки не совсем такая, какая вкладывается в это определение — она не нажимается, как обычная механическая кнопка, и регулировать силу/длительность нажатия не совсем тривиальная задача. Тем более нельзя ожидать, что всем пользователям это придётся по вкусу.

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

Для локализации и устранения проблемы в первом приближении потребовалось несколько шагов.


Шаг 1. Найти периферию, которая долго инициализируется


Этот шаг был не очень эффективным:


  • инициализация GPIO, АЦП, последовательных интерфейсов, криптографические блоки и т.д. не требовали длительного времени на инициализацию
  • инициализация USB-стека от ST отнимала ~20 мс, но оптимизация вызова не позволяла существенно сократить время старта — получить 280 мс вместо 300 приятно, но недостаточно

Шаг 2. Найти код, который так сильно влияет на время старта


После предыдущего шага стало очевидно, что причина столь длительного пробуждения находится за пределами того, что мы писали, так как инициализация периферии STM32L4 стояла в самом начале функции main(). Анализ файла startup.s из стандартной поставки CrossWorks for ARM ничего необычного не показал, кроме того, что по сравнению с бесплатным GNU GCC for ARM в коде пристутствуют вызовы к функциям memory_copy и memory_set, которые реализованы в CRT от CrossWorks (справедливо для версий 4.4.0 и 4.5.0):


Функция копирования памяти


memory_copy:
    cmp r0, r1
    beq 2f
    subs r2, r2, r1
    beq 2f
1:
    ldrb r3, [r0] ;ПОДОЗРИТЕЛЬНАЯ ЧАСТЬ
    adds r0, r0, #1
    strb r3, [r1]
    adds r1, r1, #1
    subs r2, r2, #1
    bne 1b
2:
    bx lr

Функция установки значения памяти


memory_set:
    cmp r0, r1
    beq 1f
    strb r2, [r0] ;ПОДОЗРИТЕЛЬНАЯ ЧАСТЬ
    adds r0, r0, #1
    b memory_set
1:
    bx lr

Подозрительные части отмечены и в них видно, что копирование и установка значения памяти реализована с помощью побайтной операции установки. Соответственно, количество необходимых итераций возрастает в 4 раза, так как инструкции STRB (установка 1 байта) и STR (установка машинного слова — для ARM32 это 4 байта) имеют одинаковое время исполнения — 1 такт. Это странно потому, что данные должны быть выровнены по границе машинного слова (исключения могут быть, но без использования явных атрибутов/опций компилятора, такого не происходит).


Если компилятор CrossWorks for ARM или их стандартная библиотека отходит от этого правила, то, на мой взгляд, это должно быть явно указано, почему memory_copy и memory_set реализованы именно так. И уж точно это не то, за что вы захотите платить.


Что касается нашего приложения, реализация CRT выше повлияла на систему и мы это заметили по той причине, что объем данных в RAM на момент нахождения проблемы уже был достаточно велик — ~265 Кбайт (сторонние библиотеки в виде TCP/IP-стека, TLS/SSL-библиотека, RTOS + статические буферы в нашем коде сделали своё дело):


  • секция .data занимала ~3 Кбайт
  • секция .bss занимала ~262 Кбайт

Как известно, секция .bss обнуляется в процессе инициализации приложения, а данные из секции .data во Flash-памяти MCU копируются в RAM.


Немного расчётов для MCU STM32L4A6VG


Просто чтобы показать разницу в скорости реализации функций копирования/установки памяти по словам и по байту я покажу расчёт для MCU, используемого в системе. STM32L4A6VG на старте работает от встроенных часов MSI (multispeed internal RC oscillator), которые по умолчанию настроены на частоту 4 МГц. Это значит, что время выполнения одной инструкции до переключения на более высокую частоту составляет:
$ t_{instruction} = \frac{1}{f_{CPU}} = \frac{1}{4 \cdot 10^6} = 250 \cdot 10^{-9} \; s = 250 \; ns $
Для простоты вычислений предположим, что мы используем только функцию memory_set, содержащую 5 инструкций, которые исполняются за 1 такт процессора (beq, cmp, strb, adds, b). При записи 1 байта за итерацию общее число тактов, необходимых для очистки 265 Кбайт = 271360 байт составит:
$ t_{set} = C_{MEM} \cdot N = 271360 \cdot 5 = 1356800 \; cycles$
Итого, общее время выполнения:
$ T_{set} = t_{instruction} \cdot t_{set} = 250 \cdot 10^{-9} \; s \cdot 1356800 \; cycles = 0.3392 \; s \approx 0.34 \; s = 340 \; ms $

Это время очень близко к тому, что мы увидели на осциллограмме в самом начале

Ожидаемое время исполнения аналогичных действий при использовании инструкции установки значения, оперирующей машинными словами:
$ t_{set} = C_{MEM} \cdot N = \frac{271360}{4} \cdot 5 = 339200 \; cycles \rightarrow \\ \rightarrow T_{set} = t_{instruction} \cdot t_{set} = 250 \cdot 10^{-9} \; s \cdot 339200 \; cycles = 0.0848 \; s \approx 0.085 \; s = 85 \; ms$

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


Функция копирования памяти


memory_copy:
    cmp r1, r2
    itte lt
    ldrlt r3, [r0], #4
    strlt r3, [r1], #4
    bge 1f
    b memory_copy
1:
    bx lr

Функция установки значения памяти


memory_set:
    cmp r0, r1
    ite lt
    strlt r2, [r0], #4
    bge 1f
    b memory_set
1:
    bx lr

Как и ожидалось, ускорение времени старта составило ~4 раз, что также соответствует расчётам:


Figure_optimized_boot_time


Заключение


Очень печально, что в платном продукте нашлась не совсем оптимальная реализация startup-кода. Повторюсь, описанная выше реализация встречалась для всех ARM Cortex-M процессоров в CrossWorks for ARM v4.4 и v4.5.


Вероятнее всего пользователи Segger Embedded Studio также могут столкнуться с указанной проблемой, так как реализации CRT и стандартной библиотеки идентичны.


Поэтому призываю не бояться заглянуть во внутренности библиотек из поставки IDE/компилятора, если они доступны, сделать необходимые изменения и проверить их.

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 8: ↑8 and ↓0+8
Comments25

Articles