Pull to refresh

ARM-ы для самых маленьких: который час?

Reading time 12 min
Views 53K


Сегодня мы разберемся с двумя важными вопросами: как писать более эффективный код с CMSIS и как правильно рассчитывать скорость работы процессора. Начнем мы со второй части и изучим процессы, которые происходят в LPC1114 для генерации тактовой частоты.


Тактовая частота – основной источник «рабочей силы» в процессоре, ее генератор можно сравнить с сердцем у человека. В разных компонентах процессора может использоваться разная частота, которая, тем не менее, зарождается обычно в одном и том же кристалле (или резонаторе).

Большинство процессоров имеют встроенный резонатор и возможность подключить внешний резонатор или кристалл. Зачем это сделано? В основном, для удешевления процессора. Встроенный резонатор типично имеет погрешность около 1%, чего может хватить для многих задач, но есть еще больше задач, для которых такая точность неприемлема. В самом деле, если мы будем, например, считать время на встроенном резонаторе, то погрешность за сутки может достигнуть 14 минут. Если вы передаете пакет по сети где-то раз в полчаса – это совершенно не критичная погрешность. Другое дело, если вы делаете будильник.


(изображение из LPC111x User Manual)

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

⓵ Основная частота


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

Во-первых, это IRC – внутренний резонатор. Рабочая частота – 12 МГц (на самом деле, можно тюнинговать в небольших пределах), погрешность — около 1%. Именно отсюда генерируется тактовая частота процессора в момент старта, так что весь загрузочный код выполняется при тактовой частоте в 12 МГц. Вариант максимально простой (вам вообще ничего не надо делать, чтобы он работал), не требует дополнительных внешних компонентов. К сожалению, имеет свои проблемы: резонатор, как я уже упоминал, несколько неточен, кроме того, нам не особенно интересно гонять ядро на 12 МГц, когда оно отлично работает на 50 МГц.

Во-вторых, основную частоту можно задать еще с одного внутреннего генератора – watchdog oscillator, который обычно используется для работы watchdog. Этот осциллятор работает на скоростях (программно настраиваемо) от 9,4 кГц до 2,3 МГц с погрешностью ±40%, — казалось бы, не лучшее решение для основной частоты. С другой стороны – это именно то замечательное и энергоэффективное решение, если вам нужно перевести ядро в спящий режим, при этом оставив какую-то часть периферии рабочей.

В-третьих, мы можем получить основную частоту из системного осциллятора до или после ФАПЧ. Не будем сейчас вникать в специфику работы ФАПЧ, так как это достаточно объемная тема. Интересующимся советую изучить раздел «3.11 System PLL functional description».

⓶ Системный осциллятор


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

Конкретно LPC1114 (впрочем, как и остальные процессоры линейки LPC111x) поддерживает кристаллы с частотой осцилляции от 1 МГц до 25 МГц. Помимо самого кристалла, вам также понадобятся два конденсатора, значения которых зависят от параметров выбранного кристалла. Тут я отсылаю вас к datasheet, где в разделе 12.3 (XTAL input) есть и схема подключения, и таблица с рекомендованными емкостями конденсаторов. В тестовой схеме я пробовал использовать кристалл с частотой 12 МГц, емкостью нагрузки 20 пФ и двумя конденсаторами на 39 пФ, но этот режим работы далее рассматриваться не будет.

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

Системный осциллятор можно использовать непосредственно как генератор основной частоты, или же предварительно пропустить его через ФАПЧ.

⓷ ФАПЧ


Не вдаваясь в электромеханику, ФАПЧ – это устройство, которое сначала умножает, а потом делит входную тактовую частоту. На входе ФАПЧ может принять частоту от IRC или системного осциллятора, а выход будет использован для основной частоты.

Настройка параметров ФАПЧ потенциально опасна для внутренностей процессора, потому я рекомендую NXP-шную утилиту (успешно конвертируется и работает в Google Drive) для подбора необходимых параметров, просто задайте частоту осциллятора на входе и итоговую частоту, которую хотите получить, и она рассчитает возможные варианты.

В сети есть интересная заметка о том, как поднять частоту IRC для генерации 50 МГц на выходе ФАПЧ, но для отладки этого результата вам понадобится осциллоскоп.

⓸ Системная частота


Обычно ядро (то, что Cortex-M0) работает на основной частоте, но, при необходимости, основную частоту можно разделить (на значение вплоть до 255), получив в итоге системную частоту. Помимо непосредственно ядра, на этой частоте будут работать флеш-память, RAM и вся периферия, за исключением SPI и UART. Имейте в виду, что максимальная частота тут – это 50 МГц.

⓹ А что же со SPI и UART?


Из-за специфики этих интерфейсов у них есть свои выделенные делители частоты, например, у UART он позволяет выбрать необходимый битрейт.

Несмотря на некоторую неочевидность схемы, на вход делителя попадает не основная, а системная частота.

Расчет делителя для битрейта – достаточно сложная задача, потому в очередной раз отправляю вас в инструкцию – «13.5.15 UART Fractional Divider Register (U0FDR — 0x4000 8028)». Там есть и формула расчета, и объяснение дополнительного дробного аргумента, а также блок-схема для поиска нужных параметров для заданного битрейта и пара примеров.

У SPI все как-то существенно проще, скорее всего потому, что мастер на шине задает частоту, и остальные устройства работают на ней – заочной синхронизации не требуется. Так что единственное, что мы можем сделать – это задать делитель. Важный момент – когда процессор работает в мастер-режиме, то минимальный делитель – 2, т.е., при частоте системной частоты в 48 МГц скорость передачи данных на SPI будет 24 МГц.

UPD: как верно заметил valeriyk, этот делитель не единственное, что влияет на выходную частоту. У SPI, например, несущая частота расчитывается по формуле: PCLK / (CPSDVSR * (SCR + 1)), где PCLK — это частота периферии; CPSDVSR — «предделитель»; SCR — количество тактов предделителя на один бит вывода.

⓺ Watchdog на страже жизнедеятельности


Watchdog по своей специфике — компонент изолированный. Поэтому, в качестве ведущей частоты можно использовать системную, IRC или отдельный осциллятор. Точно так же, у watchdog есть свой выделенный делитель.

Зачем для watchdog нужен отдельный тактовый генератор? Если программа случайно поломает основной генератор, конечно же! Тогда у нее все еще будет шанс быть перезагруженной по таймеру watchdog.

⓻ На выход


Наконец, процессор может генерировать выходной сигнал тактовой частоты на пине CLKOUT (одна из альтернативных функций у GPIO 0.1). В качестве ведущей частоты мы можем использовать любую из доступных нам: из осцилляторов (IRC, системного или watchdog) или системную частоту (после ФАПЧ, если он включен). Ну и, конечно, свой делитель.

Немного о mbed


Мы детально рассмотрели процесс генерации тактовой частоты в LPC1114, но что же с LPC1768? На самом деле, у каждой линейки процессоров может быть (и скорее всего будет свой особый подход, потому инструкцию по этой теме надо изучить предельно внимательно. В LPC1768 также есть внутренний осциллятор – IRC, но работает он на частоте 12 МГц. Помимо него есть основной (main) осциллятор, идентичный системному осциллятору. На mbed к нему подключен кристалл на 12 МГц. Наконец, есть осциллятор часов реального времени (RTC), но кристалл к нему не подключен.

Также, помимо основного ФАПЧ, есть дополнительный, который используется для генерации рабочей частоты USB. Все компоненты периферии имеют независимые настраиваемые делители относительно рабочей частоты.

Практические нюансы изменения частоты


Изменение рабочей тактовой частоты влечет за собой несколько последствий. Самое очевидное — необходимость перенастраивать таймеры. Также, потребуется переинициализация периферии, работающей с протоколами где важно зафиксировать несущую частоту (UART, USB). Наконец, количество тактов для доступа к флеш-памяти тоже играет важную роль. У LPC1114 значение по умолчанию — 3 такта (рабочая частота до 50 МГц, см. документацию по регистру FLASHCFG), чего вполне хватает для наших задач. Но у LPC1768 значение по умолчанию — 4 такта, с рабочей частотой до 80 МГц, чего нам будет недостаточно.

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

За работу!


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

Как вы видели раньше, очень много задач выполняются однотипно – записью и чтением регистров (вообще, все задачи выполняются именно так). ARM позаботилась о том, чтобы задачи, не привязанные к конкретному процессору, можно было выполнять одним и тем же кодом на C, для этого и существует CMSIS – набор драйверов для ядра процессора. Вендоры обычно расширяют его драйверами для всей остальной периферии.

Сложный момент с CMSIS состоит в том, что иногда не совсем понятно, где найти актуальную версию. Базовый набор файлов можно скачать непосредственно у ARM, на момент написания там доступна версия 3.01. Помимо заголовочных файлов, ARM предоставляет библиотеку для разноплановых сложных расчетов на DSP (которого в нашем железе все равно нет). Хуже обстоит дело с драйверами от конкретных производителей. У NXP, например, CMSIS для LPC1114 основан на CMSIS 1.30, а для LPC1768 – на 2.10. Более того, в наборе драйверов периферии есть явные ошибки в коде. А уж драйверы для чипов TI приходится основательно искать в гугле.

Из этого можно сделать два важных вывода: во-первых, код драйверов почти весь открыт, так что «доверяй, но проверяй»: инструкция и даташит — это ваша основная литература по работе с периферией. Во-вторых, в драйверах нет почти ничего, что нельзя было бы написать самому, т.е., это отличный и, зачастую, рабочий справочный материал. Главное – не забывать относиться к нему критически, если что-то выглядит странно – раскуривайте инструкцию по процессору.

Исходный код теперь несколько более структурирован. Хотя в результате он существенно вырос в количестве фалов, теперь намного проще поддерживать несколько разных платформ. Исходники для сегодняшнего примера доступны на GitHub: farcaller/arm-demos (pull-реквесты для новых архитектур приветствуются!).

Дерево исходников еще не до конца причесано, в частности, я не избавился от примитивных boot.s и memmap.ld. Следующая часть будет целиком посвящена вопросам компоновщика (включая сборку мусора и правильную инициализацию .data и .bss), где мы займемся добиванием до конца всех спорных моментов. Весь код разбит на три категории: в app/ находятся файлы «приложения» – непосредственно рабочий код примера. Он оформлен в стиле arduino, через функции setup() и loop(). В platform/ хранятся описания разных платформ и платформозависимые функции (кроме platform/common, файлы которого линкуются во все платформы). Наконец, в cpu/ находятся CMSIS для конкретных процессоров.

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

Работа по часам


Для реализации нашей задачи (напомню, нам надо мигать светодиодом ровно раз в секунду) нам пригодился бы какой-то таймер. К счастью, таймеров в LPC-шных процессорах сразу несколько, мы будем работать с самым унифицированным – SysTick. Этот таймер описан непосредственно в CMSIS, т.е., есть большая вероятность того, что он будет и в любом другом процессоре. Его предполагается использовать для измерения квантов времени при переключении задач в ОС, но ничего не мешает использовать его для простых задач.

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

platform/common/systick.c:
void platform_systick_setup(unsigned int load)
{
    SysTick->CTRL = 0x04;
    SysTick->LOAD = load < 0xffffff ? load : 0xffffff;
    SysTick->VAL = 0;
    SysTick->CTRL = 0x05;
}


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

Для инициализации таймера мы записываем 4 в регистр контроля. Это выключает таймер, если он был включен, выключает прерывание и переводит SysTick на использование частоты процессора (напоминаю, что по умолчанию — это 12 МГц). Далее мы загружаем стартовую точку отсчета в регистр SYST_RVR, ограничивая максимум — 16777215, сбрасываем текущее значение регистра в ноль и запускаем таймер.

Теперь о том, как нам подождать одну секунду:
void platform_systick_wait()
{
    volatile int i;
    i = SysTick->CTRL;
    while((i & 0x00010000) == 0) {
       i = SysTick->CTRL;
    }
}


Мы считываем значение COUNTFLAG из регистра SYST_CSR. COUNTFLAG выставляется в единицу, когда счетчик идет на новый круг, и сбрасывается в ноль при чтении. Таким образом, мы будем в цикле, пока счетчик не переполнится.

Заглянем в другие файлы нашего проекта. app/systick-blink.c:
#include "platform.h"

void setup()
{
    platform_led_setup();
#if PLATFORM == MBED
    platform_systick_setup(4000000);
#elif PLATFORM == PROTOBOARD
    platform_systick_setup(12000000);
#else
    #error Unknown platform
#endif
}

void loop()
{
    platform_led_toggle(1);
    platform_systick_wait();
    platform_led_toggle(0);
    platform_systick_wait();
}


Тут все достаточно наглядно. Инициализируем «драйвер» светодиода и таймера, и в цикле включаем-выключаем светодиод с задержкой. В зависимости от платформы, используем разное стартовое значение таймера (IRC на mbed и protoboard у нас работают на разных частотах). А как же работает код самого светодиода?

platform/protoboard/led.c:
#include "LPC11xx.h"

#define LED_PIN (1<<9)

void platform_led_setup()
{
    LPC_GPIO1->DIR |= LED_PIN;
}

void platform_led_toggle(int on)
{
    LPC_GPIO1->MASKED_ACCESS[LED_PIN] = on ? LED_PIN : 0;
}


Как видите, с CMSIS все стало действительно читабельнее. Единственный интересный момент — это то, что вместо общего регистра GPIO мы сейчас используем регистр с маской. Он позволяет устанавливать биты GPIO для конкретных пинов с маской, т.е., можно просто писать нужное значение, не думая о том, что надо сохранять состояние соседних пинов. Детальнее (и в картинках) об этом можно прочитать в инструкции: «12.4.1 Write/read data operation».

Для сравнения вот код для mbed. platform/mbed/led.c:
#include "LPC17xx.h"

#define LED_PIN       (1<<18)
#define LED_PIN_IN_B2 (1<<2)

void platform_led_setup()
{
    LPC_GPIO1->FIODIR |= LED_PIN;
}

void platform_led_toggle(int on)
{
    LPC_GPIO1->FIOMASK2 |= ~LED_PIN_IN_B2;
    if (on) {
        LPC_GPIO1->FIOSET2 = LED_PIN_IN_B2;
    } else {
        LPC_GPIO1->FIOCLR2 = LED_PIN_IN_B2;
    }
}


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

Собрать проект можно командой rake build_protoboard или rake build_mbed. Можно даже сразу прошить устройство: rake upload_protoboard TTY=/dev/ftdi/tty/device или rake upload_mbed MOUNT=/Volumes/MBED соответственно. Сейчас светодиоды мигают идентично на обоих устройствах.

Поиграем частотой?


Вроде бы мы и решили поставленную задачу — светодиод мигает с корректным интервалом, но что-то еще осталось за кадром. Максимальная рабочая частота LPC1114 — 50 МГц, а у LPC1768 и того больше — 100 МГц, получается, мы гоняем их едва ли в треть силы!

Настало время заняться правильной инициализацией платформы. platform/protoboard/init.c:
#define CLOCK_MODE_IRC          0 // 12 MHz
#define CLOCK_MODE_IRC_WITH_PLL 1 // 48 MHz
#define CLOCK_MODE_SYS_WITH_PLL 2 // 48 MHz with external 12MHz crystal

#define CLOCK_MODE CLOCK_MODE_IRC


В исходном коде доступны три шаблона для LPC1114: стандартные 12 МГц от IRC, 48 МГц от IRC, пропущенного через ФАПЧ, и 48 МГц от системного осциллятора, пропущенного через ФАПЧ. Последний вариант требует дополнительной аппаратной поддержки, но мы рассматриваем его, так как это очень актуальный режим использования.

void platform_init()
{
// set up system oscillator and toggle PLL to point at it
#if CLOCK_MODE == CLOCK_MODE_SYS_WITH_PLL
    int i;

    // power up system oscillator
    LPC_SYSCON->PDRUNCFG &= ~(1 << 5);

    // oscillator is not bypassed, runs at 1-20MHz range
    LPC_SYSCON->SYSOSCCTRL = 0;

    // allow circutry to settle down
    for (i = 0; i < 200; ++i)
        __NOP();

    // set PLL clock source to system oscillator
    LPC_SYSCON->SYSPLLCLKSEL = 1;

    // wait for PLL clock source to be updated
    LPC_SYSCON->SYSPLLCLKUEN = 1;
    LPC_SYSCON->SYSPLLCLKUEN = 0;
    LPC_SYSCON->SYSPLLCLKUEN = 1;
    while (!(LPC_SYSCON->SYSPLLCLKUEN & 1))
        ;
#endif


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

После первичной инициализации следует сделать небольшую задержку. Далее мы переводим ФАПЧ на работу от системного осциллятора (вместо IRC), для этого существует интересный механизм: пишем 0, пишем 1, ждем — регистр начнет возвращать 1.

// set up PLL if it's used
#if CLOCK_MODE == CLOCK_MODE_IRC_WITH_PLL || CLOCK_MODE == CLOCK_MODE_SYS_WITH_PLL
    // set up PLL dividers
    LPC_SYSCON->SYSPLLCTRL = 0x23; // M = 3, P = 12MHz
                                   // PLLout = 12MHz * (M+1) / P = 48MHz

    // power up PLL
    LPC_SYSCON->PDRUNCFG &= ~(1 << 7);

    // wait until PLL is locked
    while (!(LPC_SYSCON->SYSPLLSTAT & 1))
        ;

    // switch main clock to be driven from PLL
    LPC_SYSCON->MAINCLKSEL = 3;

    // wait for main clock source to be updated
    LPC_SYSCON->MAINCLKUEN = 1;
    LPC_SYSCON->MAINCLKUEN = 0;
    LPC_SYSCON->MAINCLKUEN = 1;
    while (!(LPC_SYSCON->MAINCLKUEN & 1))
        ;
#endif


Вторая часть инициализирует ФАПЧ, который на данном этапе получает на входе сигнал или от IRC, или от системного осциллятора. Настраиваем делители по формуле из инструкции, включаем ФАПЧ и ждем, пока он заблокируется. Основная частота после загрузки работает от IRC, переводим ее на работу от выхода ФАПЧ и ждем, пока это изменение «устаканится».

На 48 МГц для SysTick нам понадобится 48000000 циклов, но это больше его максимального значения. Один из вариантов решения — ждать несколько циклов таймера, что реализовано в функции platform_systick_wait_loop (другим вариантом было бы использовать 32-разрядный таймер CT32B0).

У LPC1768 код, опять же, в целом похожий. Тут важный момент в том, что на выходе из PLL должно быть не менее 275 МГц, когда на входе в процессор — не более 100 МГц. В общем, внимательно проверяем делители. Также важно отметить, что мы повышаем количество тактов, необходимых для доступа к флеш-памяти, потому что мы будем работать на частоте выче чем значение по умолчанию.

platform/mbed/init.c:
// if we go for clock > 80 MHz, we need to set up flash access time
LPC_SC->FLASHCFG = (LPC_SC->FLASHCFG & 0xFFF) | 0x4000; // 4 cpu clocks


Код, приведенный в примере, актуален только для LPC1768 на mbed, так как привязан к конкретной частоте кристалла. Более того, если вы работаете с LPC1768 «напрямую», то его загрузчик стартует с IRC с включенным ФАПЧ, так что в своем инициализаторе его перед настройкой необходимо выключить.

Подводя итоги


Еще хотел сегодня рассказать про CLKOUT и о том, как можно контролировать частоту анализатором логики или осциллографом, но так статья получилась бы слишком большой. CLKOUT, 32-разрядные таймеры, прерывания и спящий режим — все это будет в следующих выпусках.

До меня доехала коробка со Stellaris LaunchPad, я подумаю над тем, как лучше всего будет добавить еще одну архитектуру, не раздувая повествование. В любом случае, LPC1114 становится основным целевым процессором, все примеры мы сначала будем обкатывать на нем.

Приношу свои извинения за «многобукаф», далее постараюсь писать более содержательно.

P.S. Как всегда, большое спасибо pfactum за вычитку текста и бесценные комментарии по электромеханике. И за то, что объяснил про ФАПЧ :-).

Лицензия Creative Commons Это произведение доступно по лицензии Creative Commons «Attribution-NonCommercial-NoDerivs» 3.0 Unported. Программный текст примеров доступен по лицензии Unlicense (если иное явно не указано в заголовках файлов). Это произведение написано исключительно в образовательных целях и никаким образом не аффилировано с текущим или предыдущими работодателями автора.
Only registered users can participate in poll. Log in, please.
О чем писать дальше?
48.88% Прочитал, пищи еще! 196
6.98% Прочитал, давай про компоновщик 28
2.99% Прочитал, хочу детальнее про ассемблерный листинг на выходе 12
19.45% Пролистал, если будет продолжение – тоже пролистаю 78
21.7% Не читал, но голосовать люблю 87
401 users voted. 62 users abstained.
Tags:
Hubs:
+50
Comments 21
Comments Comments 21

Articles