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

alt
Рис. 1 - схема тактирования RP2040

Как видно из рис. 1, на котором показана схема тактирования, микроконтроллер имеет несколько источников тактового сигнала. Среди основных - это системный генератор PLL, который имеет широкую сетку частот. Каждый блок контроллера, такой как ядро, периферийные устройства, USB или АЦП может брать тактовый сигнал из любого источника. Также для каждого блока имеется предделитель тактового сигнала и возможность его выключить. Выключение тактового сигнала полностью останавливает блок и, соответственно, снижает энергопотребление.

Настройка источника PLL

Для начала давайте разберемся, как мы можем настроить блок PLL и какие частоты мы от него можем получить. Для этого, прежде всего, в код нужно включить заголовочный файл #include "hardware/clocks.h" и не забыть в файле CMakeLists.txt в разделе подключение библиотек target_link_libraries прописать библиотеку hardware_clocks. SDK предоставляет верхнеуровневую функцию для установки системного тактового сигнала set_sys_clock_hz() от источника PLL, которая принимают желаемую частоту в герцах и возвращает результат успешно она установлена или нет. Расковыряв эту функцию мы видим, что установка частоты производится функцией void set_sys_clock_pll(uint32_t vco_freq, uint post_div1, uint post_div2) Которая принимает на вход три параметра: желаемую частоту ГУНа и два постделителя. Однако не каждый набор параметров допустим. Для проверки допустимости параметров есть функция bool check_sys_clock_hz(uint32_t freq_hz, uint *vco_out, uint *postdiv1_out, uint *postdiv2_out), которая принимает на вход желаемую частоту в герцах и ссылки на результат для параметров PLL. Сама по себе эта функция (рис. 2) реализует просто перебор всех возможных допустимых значений параметров и проверки получаемой частоты на соответствие желаемой. Если соответствие не найдено, желаемая частота не является допустимой.

alt
Рис. 2 - функция проверки допустимости настроек PLL

Исходным параметром тут является частота кварцевого генератора которая в моём случае равна 12 МГц. Также ограничения накладывает диапазон частот генерации ГУНа, который для этого микроконтроллера составляет от 750 до 1600 МГц. Для того чтобы понять с какой сеткой частот мы можем работать я прогнал этот алгоритм отдельно и вывел параметры для всех доступных частот. Затем я попросил ChatGPT отсортировать эти данные по возрастанию частоты и выделил результаты кратные 1 МГц.

Пример настроек для PLL (out - значение выходной частоты, кГц)

vco_khz = 756000; postdiv1 = 7; postdiv2 = 6; out = 18000

vco_khz = 840000; postdiv1 = 7; postdiv2 = 6; out = 20000

vco_khz = 924000; postdiv1 = 7; postdiv2 = 6; out = 22000

vco_khz = 792000; postdiv1 = 6; postdiv2 = 6; out = 22000

vco_khz = 828000; postdiv1 = 6; postdiv2 = 6; out = 23000

vco_khz = 1176000; postdiv1 = 7; postdiv2 = 7; out = 24000

vco_khz = 1008000; postdiv1 = 7; postdiv2 = 6; out = 24000

vco_khz = 864000; postdiv1 = 6; postdiv2 = 6; out = 24000

vco_khz = 840000; postdiv1 = 7; postdiv2 = 5; out = 24000

vco_khz = 900000; postdiv1 = 6; postdiv2 = 6; out = 25000

vco_khz = 1092000; postdiv1 = 7; postdiv2 = 6; out = 26000

vco_khz = 936000; postdiv1 = 6; postdiv2 = 6; out = 26000

vco_khz = 780000; postdiv1 = 6; postdiv2 = 5; out = 26000

Настройки PLL для частот кратных 1 МГц можно посмотреть тут а настройки для всей сетки частот тут.

Как видим одну частоту можно получить несколькими наборами параметров. Забегая вперёд, сразу скажу, что по результатам тестирования выбирать нужно вариант с минимальной частотой ГУН, с целью минимизации энергопотребления.

контроль тактовой частоты

Прежде чем устанавливать тактовые частоты, давайте разберёмся, как производить контроль реально установленных частот. SDK предоставляет функцию uint32_t clock_get_hz(clock_handle_t clock), которая возвращает значение текущей частоты указанного тактового сигнала основываясь на предварительно сделанных настройках. В качестве параметра передаётся тип тактового сигнала, который может выбираться среди значений clk_ref, clk_sys, clk_peri, clk_usb, clk_adc и т. д., в соответствии с рис. 1. Также микроконтроллер имеет независимый счётчик тактовой частоты, которой может измерять значение тактовых частот напрямую. Для этого SDK предоставляет функцию uint32_t frequency_count_khz(uint src) на вход который нужно передать значение источника тактового сигнала для счётчика (рис. 3).

alt
Рис. 3 - доступные значения источников для измерителя тактовой частоты.

Код для вывода текущих значений тактовых сигналов может выглядеть следующим образом:

void print_clocks() {
    printf("System Clock Frequency is %d Hz\n", clock_get_hz(clk_sys));
    printf("Peripheral Clock Frequency is %d Hz\n", clock_get_hz(clk_peri));
    printf("USB Clock Frequency is %d Hz\n", clock_get_hz(clk_usb));
    printf("Reference Clock Frequency is %d Hz\n", clock_get_hz(clk_ref));

    printf("System Clock counted Frequency is %d kHz\n", frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_SYS));
    printf("Peripheral Clock counted Frequency is %d kHz\n\n", frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_PERI));
}

А настройки тактирования микроконтроллера по умолчанию показаны на рис. 3. Видим, что ядро и периферия работают на скорости 125 МГц.

alt
Рис. 4 - настройки тактирования по умолчанию

Выбор и настройка тактового сигнала блоков контроллера

Для выбора тактовых сигналов блоков контроллера SDK предоставляет функции:

  • bool clock_configure(clock_handle_t clock, uint32_t src, uint32_t auxsrc, uint32_t src_freq, uint32_t freq)

  • void clock_configure_undivided(clock_handle_t clock, uint32_t src, uint32_t auxsrc, uint32_t src_freq)

  • void clock_configure_int_divider(clock_handle_t clock, uint32_t src, uint32_t auxsrc, uint32_t src_freq, uint32_t int_divider)

Первый параметр задаёт блок, тактовую частоту которого мы хотим сконфигурировать. Его значение выбирается из следующих вариантов:

typedef enum clock_num_rp2040 {
    clk_gpout0 = 0, ///< Select CLK_GPOUT0 as clock source
    clk_gpout1 = 1, ///< Select CLK_GPOUT1 as clock source
    clk_gpout2 = 2, ///< Select CLK_GPOUT2 as clock source
    clk_gpout3 = 3, ///< Select CLK_GPOUT3 as clock source
    clk_ref = 4, ///< Select CLK_REF as clock source
    clk_sys = 5, ///< Select CLK_SYS as clock source
    clk_peri = 6, ///< Select CLK_PERI as clock source
    clk_usb = 7, ///< Select CLK_USB as clock source
    clk_adc = 8, ///< Select CLK_ADC as clock source
    clk_rtc = 9, ///< Select CLK_RTC as clock source
    CLK_COUNT
} clock_num_t;

Следующие два параметра определяют источник от которого нужно взять тактовый сигнал. Параметр src_freq указывает частоту на которой работает выбранный источник. Для корректной работы это значение должно соответствовать реальной частоте источника тактового сигнала. Кроме того функция clock_configure() позволяет через параметр freq задать делитель для выбранного тактового сигнала. Cоответственно, значение freq должно быть в целое число раз меньше значения src_freq. Также возможно задать делитель напрямую в вызове clock_configure_int_divider().

Настроить тактовый сигнал clk_sys на источник System PLL можно следующим образом:

clock_configure(clk_sys,
                        CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLKSRC_CLK_SYS_AUX,
                        CLOCKS_CLK_SYS_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS,
                        freq_khz * KHZ,
                        freq_khz * KHZ/1);

А вот при настройке тактового сигнала clk_peri я столкнулся с проблемой. Мне не удалось заставить работать делитель для этого тактового сигнала (для всех остальных делитель работает). Идея была в том, чтобы взять тактовую частоту от того же System PLL, поделить ее и заставить периферию работать на частоте ниже ядра. Но, похоже, делитель для clk_peri просто не работает. Соответственно, для тактирования периферии остается несколько обходных вариантов:

  • Взять тактовую частоту напрямую с System PLL (источник auxsrc = CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS). В этом случае можно заставить периферию работать на большей частоте, чем ядро, если использовать делитель для clk_sys;

  • Взять в качестве источника уже сформированный clk_sys (источник auxsrc = CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLK_SYS). В этом случае периферия всегда будет работать на частоте ядра (не разумно, если МК используется на максимальной скорости 100 - 150 МГц).

  • Выбрать источником USB PLL с фиксированной частотой 48 МГц (источник auxsrc = CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLKSRC_PLL_USB).

  • Взять тактовый сигнал напрямую с кварцевого генератора XOSC (источник auxsrc = CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_XOSC_CLKSRC). Это может быть полезно, если ядро у нас работает на полной скорости, а периферии нам достаточно медленной, например 12 МГц.

И во всех случаях нужно не забывать что в функции clock_configure_... в параметр src_freq нужно передавать реальную частоту источника. Варианты настроек тактирования периферии под катом:

раскрыть

    clock_stop(clk_peri);
    clock_configure_undivided(clk_peri,
                    0,
                    CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS,   // Источник System PLL
                    pll_freq_hz);



    clock_stop(clk_peri);
    clock_configure_undivided(clk_peri,
                    0,
                    CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLK_SYS,          // Источник sclk_sys
                    clock_get_hz(clk_sys));



    clock_stop(clk_peri);
    clock_configure_undivided(clk_peri,
                    0,
                    CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLKSRC_PLL_USB,   // Источник USB PLL
                    48000000);



    clock_stop(clk_peri);
    clock_configure_undivided(clk_peri,
                    0,
                    CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_XOSC_CLKSRC,      // Источник XOSC
                    XOSC_HZ);

Тестирование энергопотребления при тактировании от PLL на разных частотах

Для тестов я использую отладочную плату контроллера Waveshare RP2040 Zero. Для минимизации побочного энергопотребления я демонтировал светодиод WS2812B, который по схеме запитан постоянно, что позволит сэкономить несколько сотен микроампер. В таблице 1 приведены результаты измерений токов потребления по напряжению 5В.

alt
Рис. 2 - Энергопотребление на разных частотах тактирования

В колонках с первой по четвёртую показаны частоты тактирования clk_sys и clk_peri, а также источники этих тактовых сигналов. В пятой колонке показаны токи потребления при исполнении программы из Flash-памяти и с включённым блоком USB и, соответственно, USB PLL. Логи выводятся через USB. Необходимо отметить что если тактирование происходит напрямую от кварцевого генератора, то блок System PLL выключается. Видно, что System PLL добавляет к энергопотреблению от 1 до 1,3 мА.

В шестой колонке приведено энергопотребление для случая, когда программа выполняется из Flash-памяти и блок USB выключен (соответственно выключен и USB PLL). Как видно, блок USB накидывает к потреблению в районе 2,2 - 2,4 мА.

if (disable_usb) {
    // Выключаем тактирование ненужных блоков
    clock_stop(clk_adc);
    clock_stop(clk_usb);
        
    // Не забываем выключить генератор PLL 48МГц для USB
    pll_deinit(pll_usb);
}

В седьмой колонке представлен вариант, похожий на предыдущий. Кроме того, что программа выполняется из ОЗУ. Как видно, на низких скоростях разница в потреблении практически незаметна. Но на частотах выше 50 МГц она становится существенной. Последнюю колонку мы обсудим в разделе посвящённом спящему режиму.

Спящий режим микроконтроллера рп-2040

В микроконтроллере реализован режим глубокого сна называется DORMANT Mode, в котором выключаются все внутренние генераторы и останавливаются цифровые блоки. Пробуждение возможно либо от асинхронного внешнего сигнала на GPIO либо от блока RTC при условии тактирования его от внешнего генератора. Производитель обещает что в режиме глубокого сна среднее энергопотребление составит 180 мкА. Однако, забегая вперёд, можно сказать, что в реальных устройствах этот уровень недостижим.

Теперь давайте разберёмся как этот DORMANT режим настроить. Производитель предоставляет для этого официальный пример, однако, в нём есть подводные камни, о которых нигде в руководстве не написано. Используемые для работы со спящим режимом API не входят в стандартный SDK, а являются частью внешней библиотеки, которую нужно скачать, установить и подключить к проекту вручную. Библиотека называется pico-extras и доступна в github-репозитории. Необходимо клонировать этот репозиторий в любое место на ПК. Я использовал домашнюю папку, в которой у меня установлен SDK в папке .pico-sdk. Соответственно библиотеку я склонировал в папку .pico-extras. Далее необходимо путь к библиотеке pico-extras прописать в переменной окружения PICO_EXTRAS_PATH (по аналогии с PICO_SDK_PATH). Теперь нам нужно в папке .pico-extras/external найти файл pico_extras_import.cmake и скопировать его в корень нашего проекта по аналогии с уже существующим файлом pico_sdk_import.cmake. Скопированный файл нужно не забыть прописать в CMakeLists.txt следующим образом:

# Pull in Raspberry Pi Pico SDK (must be before project)
include(pico_sdk_import.cmake)

# We also need PICO EXTRAS
include(pico_extras_import.cmake)

И в завершении нужно прописать библиотеку hardware_sleep в секцию target_link_libraries файла CMakeLists.txt.

После всех этих подготовительных действий, нужно включить в исходный код заголовок #include "pico/sleep.h" и реализовать переход DORMANT Mode последовательностью вызовов:

    // Set the crystal oscillator as the dormant clock source, UART will be reconfigured from here
    // This is necessary before sending the pico into dormancy
    sleep_run_from_xosc();

    // Go to sleep until we see a high edge on GPIO 10
    sleep_goto_dormant_until_edge_high(WAKE_GPIO);

    // Re-enabling clock sources and generators.
    sleep_power_up();

Первый вызов переводит тактирование всех блоков на кварцевый генератор, поскольку в спящий режим нельзя уходить с тактированием от PLL. Соответственно, тут же отключается тактирование АЦП и USB и выключаются оба PLL генератора.

Второй вызов, собственно, переводит микроконтроллер в спящий режим и задаёт номер GPIO по переднему фронту на котором произойдёт пробуждение.

Третий вызов восстанавливает тактирование всех блоков после пробуждения. Однако, нужно понимать, что это будет тактирование по умолчанию, как будто микроконтроллер только загрузился. После этого нужно перенастроить всё тактирование в соответствии с потребностью приложения, а также переинициализировать UART для сохранения скорости передачи.

Пришло время измерить ток потребления платы в спящем режиме. И тут меня ждало огромное разочарование - ток потребления приближается в 6 мА. Поиск по сети не дал конкретного рецепта по лечению этой ситуации. В нескольких ветках на форумах лишь упоминалось, что это может быть связано с потреблением Flash-памяти, которая остается активной. На плате используется микросхема W25Q16JV, которая в режиме чтения потребляет 2 - 8 мА в зависимости от скорости. Можно попробовать перевести Flash-память в режим Power-down, в котором потребление должно быть 1 - 10 мкА. Для этого есть команда (B9h).

Подключаем в проект библиотеку для работы с Flash-памятью: #include "hardware/flash.h" и прописываем в скрипте сборки CMakeLists.txt в секции target_link_libraries библиотеку hardware_flash. Для отправки команд вo Flash-память по QSPI существует вызов, параметры которого очевидны без расшифровки:

void flash_do_cmd(const uint8_t *txbuf, uint8_t *rxbuf, size_t count);

// и как реализовать вызов
uint8_t cmd = 0xB9;
uint8_t rx = 0;
flash_do_cmd(&cmd, &rx, 1);

Теперь встает вопрос - а как же нам исполнять программу, если Flash-память остановлена? Для этого нужно исполнять программу из ОЗУ. В скрипте сборки можно настроить загрузчик, чтобы он при старте копировал всю прошивку в ОЗУ и передал управление на исполнение кода оттуда. Делается это следующим образом:

pico_set_binary_type(project-name copy_to_ram)

Минус этого подхода в том, что в таком случае не будет работать программный или аппаратный сброс. Поскольку после сброса Flash-память остается выключенной, микроконтроллер просто не сможет запуститься (поможет только выключение-включение питания). Однако, это ограничение можно обойти, если модифицировать пользовательский загрузчик второго уровня, чтобы он выдавал команду включения Flash-памяти: Release Power-down / Device ID (ABh).

Теперь можно посмотреть в Таблицу 1 на последнюю колонку. Там указано энергопотребление с исполнением программы из ОЗУ и с выключенной Flash-памятью (в скобках - разница с предыдущей колонкой). Как видно, выключение Flash-памяти позволяет экономить 4,3 - 4,4 мА.

А теперь можно все-таки измерить реальное потребление платы в спящем режиме и с выключенной Flash-памятью. Значение получилось в пределах 950 - 1050 мкА. Результат, конечно, не выдающийся, но с этим можно жить в некоторых проектах с батарейным питанием.

Комментарии (2)


  1. VelocidadAbsurda
    17.06.2026 11:06

    А не пробовали перед сном перейти не на внешний кварц, а на внутренний ROSC (а кварц остановить)? Он и медленнее, и по самой технологии должен быть менее прожорлив.


    1. svperchenko Автор
      17.06.2026 11:06

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