Embedded HandbookПриложения
Все разделы › II. Ядро и системные механизмы
STM32U585 · уровень продвинутый · 29 мин чтения
Открыть в интерактивном справочнике

10б. LPBAM на STM32U5: периферия работает в Stop 2, пока ядро спит

Раздел 10а кончился выводом: батарейку съедает не сон, а пробуждения. У L4 в Stop 2 живут LPUART1 и LPTIM1, но каждый принятый байт и каждый замер всё равно будят ядро. У U5 сделан следующий шаг: таймер запускает АЦП, АЦП отдаёт отсчёт контроллеру пересылок, тот кладёт его в память — и всё это в Stop 2, без единой команды программы. ST называет это LPBAM. Здесь — из чего он собран, как выглядит на регистрах и когда окупается.

Опоры: справочное руководство RM0456 (ред. 4) — разделы 10.7.2, 11.4.24, главы 18 (LPDMA), 34 (ADC4) и 58 (LPTIM); даташит DS13086 (ред. 8); заголовок stm32u585xx.h и драйвер HAL 1.3.0 из пакета STM32U5xx_DFP 2.2.1; база STM32CubeMX. Прошивка собрана arm-none-eabi-gcc 12.2.1 и armclang для Cortex-M33. Таблицу узлов из объектного файла исполняет модель канала на ПК. QEMU U5 не моделирует, на плате не запускалось, ток не мерился.

Как это называется в документации

Оригинал Перевод
Low-power background autonomous mode (LPBAM) фоновый автономный режим малого потребления
autonomous mode автономный режим блока: работает в Stop и сам просит такт
SmartRun domain (SRD) домен SmartRun: шины AHB3 и APB3, LPDMA1 и SRAM4
kernel clock такт самого блока, не зависящий от шины
bus clock request запрос такта шины: блок будит генератор на время обмена
linked-list item (LLI) узел связного списка: описание одной пересылки в памяти
link transfer подгрузка следующего узла в регистры канала
run-to-completion / link step mode пройти весь список / остановиться после одного узла
transfer complete event (TC) событие конца пересылки
analog watchdog (AWD) оконный порог АЦП
auto-off, deep-power-down автоотключение АЦП и отключение его регулятора между замерами

10б.1 Что такое LPBAM

LPBAM — не блок и не режим сна. Это три механизма, которые работают вместе, пока камень стоит в Stop 2 (RM0456, раздел 10.7.2).

Автономная периферия. Блок домена SmartRun сам просит такт, когда у него есть дело. Такт ядра блока включается только для него. Такт шины включается, когда нужна пересылка, и гаснет, когда никто его больше не просит. Генератор — HSI16 или MSI — стартует и останавливается сам.

LPDMA1 со связным списком. Контроллер пересылок на четыре канала живёт в том же домене. Описания пересылок — узлы — лежат в памяти, и канал проходит их сам, в том числе в Stop 2.

Аппаратные запуски. Таймер, будильник RTC, вывод, компаратор запускают АЦП или пересылку без программы.

Цепочка LPBAM на STM32U585: кварц LSE тактирует LPTIM3, его выход lptim3_ch2 раз в секунду запускает ADC4, запрос adc4_dma забирает канал 0 контроллера LPDMA1 и кладёт отсчёт в буфер в SRAM4, где лежат и узлы списка; HSI16 включается только на замер; ядро Cortex-M33 спит в Stop 2 и просыпается по событию TC раз в 16 отсчётов или по порогу AWD1

Ядро при этом не участвует. Его будят два события: канал собрал блок или отсчёт вышел за порог. Всё остальное время работают таймер на часовом кварце и, на микросекунды раз в секунду, АЦП с генератором.

У пересылки в регистр периферии и пересылки данных одна и та же механика. Отсюда приём, на котором держится LPBAM: узел списка может записать слово из памяти в регистр настройки блока. RM0456 говорит об этом прямо: цепочка пересылок может писать регистры периферии и менять её настройку, не выходя из Stop. Так без ядра меняют порог АЦП, период таймера, уровень вывода.

В Stop 3 этого нет: даташит пишет, что LPBAM в Stop 3 не поддерживается.


10б.2 Кто работает в Stop 2 и что для этого включить

Автономны в Stop 2 только блоки на шинах AHB3 и APB3 (RM0456, таблица 116):

Блок Что умеет в Stop 2
LPUART1, SPI3, I2C3 обмен с пересылкой через LPDMA1
ADC4 замер по запуску, запрос пересылки или порог
DAC1 вывод отсчётов из памяти
LPTIM1, LPTIM3 счёт, ШИМ, захват, запросы пересылки
LPTIM4 счёт и запуск других блоков; запросов пересылки нет
ADF1 звуковой фильтр: поток в память, пробуждение по звуку
LPDMA1 и SRAM4 пересылки и память для них

Кроме них, свой бит разрешения в RCC_SRDAMR есть у компараторов, операционных усилителей, VREFBUF, RTC и LPGPIO1. Остальные USART, SPI, I²C и GPDMA1 автономны только в Stop 0 и Stop 1.

Про LPTIM4 документы расходятся: даташит ставит его в список Stop 2, раздел 10.7.2 руководства — нет, а таблица 593 уточняет: автономного режима и запросов пересылки у него нет. Его выход lptim4_out при этом заведён на запуск LPDMA1.

Память. LPDMA1 — ведущий только на шине своего домена. Из памяти он видит одну SRAM4 — 16 КБ по адресу 0x2800 0000. Узлы списка, буферы и слова для записи в регистры лежат там. До обычного ОЗУ он не дотягивается ни в Stop 2, ни в Run.

Три бита на блок. xxEN даёт такт в Run. xxSMEN оставляет его в Sleep и Stop. xxAMEN в RCC_SRDAMR разрешает блоку просить такт в Stop 2. Нужны все три, и не только у периферии, но и у LPDMA1 и SRAM4.

Такт блока. В Stop 2 годятся только генераторы, которые умеют включаться по запросу или идут всегда. Для LPTIM3 это LSE или LSI (LPTIM34SEL), для ADC4 — HSI16 или MSIK (ADCDACSEL = 100 или 101). LSE отдаётся периферии только при LSESYSEN = 1 в RCC_BDCR: без этого бита кварц достаётся одному RTC.

Такт шины. Какой генератор проснётся для пересылки, выбирает STOPWUCK — тот же бит, что выбирает такт ядра после Stop (10а.5). Если задержка на запуск генератора мешает, SRDRUN в PWR_CR2 держит такт AHB3 и APB3 включённым, а HSIKERON и MSIKERON — генератор. За это платят током.


10б.3 LPDMA1: запрос, запуск и узел списка

У LPDMA1 четыре канала без буфера, адресация только линейная. Номер запроса ставят в REQSEL регистра CxTR2 (RM0456, таблица 146):

REQSEL Запрос REQSEL Запрос
0, 1 LPUART1: приём, передача 8, 9 DAC1: каналы 1 и 2
2, 3 SPI3: приём, передача 10 ADF1
4, 5, 6 I2C3: приём, передача, событие 11, 12, 13 LPTIM1: захват 1, захват 2, обновление
7 ADC4 14, 15, 16 LPTIM3: захват 1, захват 2, обновление

Пересылку можно ещё и придержать до запуска: TRIGSEL выбирает один из 32 сигналов, TRIGPOL — фронт. Среди них линии EXTI 0…4 (номера 0…4), lptim1_ch1 (8), lptim3_ch1 (10), lptim4_out (11), выходы компараторов (12, 13), будильники RTC A и B (14, 15), порог adc4_awd1 (17) и концы пересылок самих каналов LPDMA1 (18…21). Так один канал запускает другой.

Узел. У канала шесть регистров списка: CTR1, CTR2, CBR1, CSAR, CDAR, CLLR. Узел в памяти — те же слова в том же порядке, но не обязательно все. Что читать, говорят биты CLLR текущего узла:

Бит CLLR Что подгрузить из следующего узла
UT1, UT2 CTR1 (ширина, шаг адреса), CTR2 (запрос, запуск, событие)
UB1 CBR1 — длину блока в байтах
USA, UDA адреса источника и приёмника
ULL сам CLLR — ссылку дальше
LA смещение следующего узла от базы CLBAR, кратно 4

Полный узел — шесть слов. Если бит обновления снят, слова в узле нет, а регистр остаётся прежним. CBR1 при этом восстанавливается сам: счётчик блока возвращается к запрограммированному значению. Канал завершён, когда CLLR = 0. Если ULL = 0, а смещение не нулевое, ссылка не меняется, и последний узел повторяется вечно — так делают кольцо.

Событие. Поле TCEM в CTR2 решает, когда взводить флаг TCF: 00 — в конце каждого блока, 10 — в конце узла, 11 — в конце всего списка. У бесконечного списка с TCEM = 11 события нет вовсе. Раз CTR2 у каждого узла свой, одни узлы будят ядро, другие — нет.

Узел настройки. Ширина 4 байта, источник — слово в SRAM4, приёмник — адрес регистра, SWREQ = 1. Запроса такой узел не ждёт: канал просит такт шины сразу, пишет слово и идёт дальше. Это и есть запись регистра без ядра.


10б.4 Пример: раз в секунду замер, раз в шестнадцать — пробуждение

Датчик на входе ADC4_IN1. LPTIM3 от LSE считает до 32 767 — период секунда. Фронт его второго канала lptim3_ch2 — запуск adc_trg6 у ADC4 (RM0456, таблица 325). АЦП в автономном режиме: между замерами выключены и он, и его регулятор. Канал 0 LPDMA1 по запросу 7 кладёт отсчёт в буфер. Через 16 отсчётов — событие и пробуждение.

Чтобы показать узел настройки, список сделан из трёх узлов. Первый блок идёт с окном порога 0…4095: датчик прогревается, выбросы не будят. Потом узел настройки пишет в ADC4_AWD1TR рабочее окно 1024…3072, и дальше блок данных повторяется по кольцу. Менять AWD1TR на ходу руководство разрешает прямо.

Раскладка SRAM4 и таблица узлов:

#include <stddef.h>
#include "stm32u5xx.h"

#define N_SAMPLES   16u                         /* отсчётов в блоке: одно пробуждение на 16 замеров */
#define LSE_HZ      32768u
#define ARR_1HZ     (LSE_HZ - 1u)               /* период LPTIM3 — 1 с */
#define AWD_WIDE    0x0FFF0000u                 /* окно порога 0…4095: первый блок не будит */
#define AWD_TIGHT   0x0C000400u                 /* рабочее окно 1024…3072 */

/* Раскладка SRAM4: всё, что читает и пишет LPDMA1 в Stop 2, лежит здесь */
#define LLI_BASE    SRAM4_BASE                  /* в CLBAR — старшие 16 бит адреса узлов */
#define OFF_CFG     0x00u                       /* узел настройки: 6 слов */
#define OFF_DATA    0x18u                       /* узел данных: 6 слов */
#define OFF_RING    0x30u                       /* узел кольца: 1 слово, новый CDAR */
#define OFF_AWD     0x34u                       /* слово, которое узел настройки пишет в ADC4_AWD1TR */
#define OFF_BUF     0x40u                       /* буфер отсчётов, N_SAMPLES по 16 бит */

#define ADC4_DR_ADDR      (ADC4_BASE + offsetof(ADC_TypeDef, DR))
#define ADC4_AWD1TR_ADDR  (ADC4_BASE + offsetof(ADC_TypeDef, AWD1TR))

#define REQ_ADC4    7u                          /* adc4_dma, RM0456 таблица 146 */
#define TR1_DATA    (DMA_CTR1_SDW_LOG2_0 | DMA_CTR1_DDW_LOG2_0 | DMA_CTR1_DINC)   /* 16 бит -> 16 бит, приёмник с шагом */
#define TR2_DATA    (REQ_ADC4 << DMA_CTR2_REQSEL_Pos)                             /* TCEM = 00: событие в конце блока */
#define TR1_CFG     (DMA_CTR1_SDW_LOG2_1 | DMA_CTR1_DDW_LOG2_1)                   /* слово -> слово, адреса на месте */
#define TR2_CFG     (DMA_CTR2_SWREQ | DMA_CTR2_TCEM)                              /* без запроса; TCEM = 11: события нет */
#define LLR_ALL     (DMA_CLLR_UT1 | DMA_CLLR_UT2 | DMA_CLLR_UB1 | DMA_CLLR_USA | DMA_CLLR_UDA | DMA_CLLR_ULL)

/* Узлы связного списка. Порядок слов в узле — порядок регистров: TR1, TR2, BR1, SAR, DAR, LLR */
const uint32_t lli_tab[14] = {
    /* 0x00 узел настройки: 4 байта из SRAM4 в регистр порога АЦП, дальше — узел данных */
    TR1_CFG, TR2_CFG, 4u, LLI_BASE + OFF_AWD, ADC4_AWD1TR_ADDR, LLR_ALL | OFF_DATA,
    /* 0x18 узел данных: N отсчётов из ADC4_DR в буфер, дальше — узел кольца */
    TR1_DATA, TR2_DATA, 2u * N_SAMPLES, ADC4_DR_ADDR, LLI_BASE + OFF_BUF, DMA_CLLR_UDA | OFF_RING,
    /* 0x30 узел кольца: только CDAR. ULL = 0, поэтому CLLR остаётся на нём — блок повторяется вечно */
    LLI_BASE + OFF_BUF,
    /* 0x34 данные узла настройки */
    AWD_TIGHT,
};

volatile uint32_t lp_blocks;                    /* сколько блоков собрано */
volatile uint32_t lp_alarm;                     /* сработал порог */
volatile uint32_t lp_error;                     /* флаги ошибок канала */

Узел кольца занимает одно слово: у узла данных в CLLR стоит только UDA и смещение 0x30. После каждого блока канал читает оттуда адрес буфера, длину блока восстанавливает сам, а ссылку не трогает.

Такты и питание домена:

void srd_clocks_init(void)
{
    RCC->AHB3ENR |= RCC_AHB3ENR_PWREN;
    PWR->DBPR |= PWR_DBPR_DBP;                              /* открыть резервный домен: там LSE */
    RCC->BDCR |= RCC_BDCR_LSEON;
    while (!(RCC->BDCR & RCC_BDCR_LSERDY)) { }
    RCC->BDCR |= RCC_BDCR_LSESYSEN;                         /* без этого LSE достаётся только RTC */
    while (!(RCC->BDCR & RCC_BDCR_LSESYSRDY)) { }
    RCC->CR |= RCC_CR_HSION;                                /* HSI16: такт АЦП и шины в Stop 2 */
    while (!(RCC->CR & RCC_CR_HSIRDY)) { }
    RCC->CFGR1 |= RCC_CFGR1_STOPWUCK;                       /* шину в Stop будит HSI16 */

    RCC->CCIPR3 = (RCC->CCIPR3 & ~(RCC_CCIPR3_LPTIM34SEL | RCC_CCIPR3_ADCDACSEL))
                | RCC_CCIPR3_LPTIM34SEL                     /* 11: LPTIM3 от LSE */
                | RCC_CCIPR3_ADCDACSEL_2;                   /* 100: АЦП от HSI16 */

    /* три бита на каждый блок: EN — такт в Run, SMEN — в Sleep и Stop, AMEN — в Stop 2 */
    RCC->AHB3ENR   |= RCC_AHB3ENR_ADC4EN | RCC_AHB3ENR_LPDMA1EN | RCC_AHB3ENR_SRAM4EN;
    RCC->APB3ENR   |= RCC_APB3ENR_LPTIM3EN;
    RCC->AHB3SMENR |= RCC_AHB3SMENR_ADC4SMEN | RCC_AHB3SMENR_LPDMA1SMEN | RCC_AHB3SMENR_SRAM4SMEN;
    RCC->APB3SMENR |= RCC_APB3SMENR_LPTIM3SMEN;
    RCC->SRDAMR    |= RCC_SRDAMR_ADC4AMEN | RCC_SRDAMR_LPDMA1AMEN | RCC_SRDAMR_SRAM4AMEN
                    | RCC_SRDAMR_LPTIM3AMEN;

    PWR->CR2 &= ~PWR_CR2_SRAM4PDS;                          /* SRAM4 в Stop не выключать */
}

Таймер и АЦП:

void lptim3_init(void)
{
    LPTIM3->CFGR = 0;                                       /* внутренний такт, делитель 1, пуск программный */
    LPTIM3->CCMR1 = 0;                                      /* канал 2 — выход, полярность прямая */
    LPTIM3->CR = LPTIM_CR_ENABLE;                           /* ARR и CCR2 пишут только при ENABLE = 1 */
    LPTIM3->ICR = LPTIM_ICR_ARROKCF | LPTIM_ICR_CMP2OKCF;
    LPTIM3->ARR = ARR_1HZ;
    while (!(LPTIM3->ISR & LPTIM_ISR_ARROK)) { }
    LPTIM3->CCR2 = ARR_1HZ / 2u;                            /* фронт lptim3_ch2 раз в период */
    while (!(LPTIM3->ISR & LPTIM_ISR_CMP2OK)) { }
    LPTIM3->CCMR1 |= LPTIM_CCMR1_CC2E;
    LPTIM3->CR |= LPTIM_CR_CNTSTRT;                         /* непрерывный счёт */
}

void adc4_init(void)
{
    ADC4->ISR = ADC_ISR_LDORDY;                             /* флаги сбрасывают записью 1 */
    ADC4->CR = ADC_CR_ADVREGEN;
    while (!(ADC4->ISR & ADC_ISR_LDORDY)) { }
    ADC4->CR |= ADC_CR_ADCAL;                               /* калибровка: ADEN = 0, AUTOFF = 0, DMAEN = 0 */
    while (ADC4->CR & ADC_CR_ADCAL) { }

    ADC4->CFGR1 = ADC4_CFGR1_DMAEN | ADC4_CFGR1_DMACFG      /* запрос на каждый отсчёт, без остановки */
                | ADC_CFGR1_EXTEN_0                         /* запуск по фронту */
                | ADC4_CFGR1_EXTSEL_2 | ADC4_CFGR1_EXTSEL_1 /* 110: adc_trg6 = lptim3_ch2 */
                | ADC_CFGR1_AWD1EN | ADC_CFGR1_AWD1SGL | ADC_CFGR1_AWD1CH_0;   /* порог на канале 1 */
    ADC4->SMPR1 = ADC4_SMPR_SMP1_2;                         /* ADC_SMPR в RM0456; 100: выборка 19,5 такта */
    ADC4->CHSELR = ADC_CHSELR_CHSEL1;                       /* ADC4_IN1 */
    ADC4->AWD1TR = AWD_WIDE;
    ADC4->ISR = ADC_ISR_AWD1;
    ADC4->IER = ADC_IER_AWD1IE;                             /* будить по порогу, а не по каждому отсчёту */
    ADC4->PWRR = ADC4_PWRR_AUTOFF | ADC4_PWRR_DPD;          /* автономный режим: между замерами выключен */
    ADC4->CR |= ADC_CR_ADEN;                                /* ADRDY при AUTOFF = 1 не взводится — не ждём */
    ADC4->CR |= ADC_CR_ADSTART;                             /* дальше замеры идут по lptim3_ch2 */
    NVIC_EnableIRQ(ADC4_IRQn);
}

У ADC4 в заголовке CMSIS регистр выборки называется SMPR1, хотя в руководстве это ADC_SMPR: структура ADC_TypeDef у ADC1 и ADC4 общая. Поля ADC4 при этом идут с приставкой ADC4_: у EXTSEL в ADC4 три бита с шестого, у ADC1 — пять с пятого.

Канал и прерывания:

void lpdma_start(void)
{
    uint32_t *mem = (uint32_t *)LLI_BASE;
    uint32_t i;

    for (i = 0; i < sizeof lli_tab / sizeof lli_tab[0]; i++) {
        mem[i] = lli_tab[i];                                /* узлы — в SRAM4: другой памяти LPDMA1 не видит */
    }

    LPDMA1_Channel0->CCR = DMA_CCR_RESET;
    LPDMA1_Channel0->CFCR = DMA_CFCR_TCF | DMA_CFCR_HTF | DMA_CFCR_DTEF | DMA_CFCR_ULEF
                          | DMA_CFCR_USEF | DMA_CFCR_SUSPF | DMA_CFCR_TOF;
    LPDMA1_Channel0->CLBAR = LLI_BASE;
    /* нулевой элемент списка живёт прямо в регистрах канала: первый блок данных */
    LPDMA1_Channel0->CTR1 = TR1_DATA;
    LPDMA1_Channel0->CTR2 = TR2_DATA;
    LPDMA1_Channel0->CBR1 = 2u * N_SAMPLES;
    LPDMA1_Channel0->CSAR = ADC4_DR_ADDR;
    LPDMA1_Channel0->CDAR = LLI_BASE + OFF_BUF;
    LPDMA1_Channel0->CLLR = LLR_ALL | OFF_CFG;              /* после блока — узел настройки */
    LPDMA1_Channel0->CCR = DMA_CCR_TCIE | DMA_CCR_DTEIE | DMA_CCR_ULEIE | DMA_CCR_USEIE;
    NVIC_EnableIRQ(LPDMA1_Channel0_IRQn);
    LPDMA1_Channel0->CCR |= DMA_CCR_EN;
}

void LPDMA1_Channel0_IRQHandler(void)
{
    uint32_t sr = LPDMA1_Channel0->CSR;

    if (sr & DMA_CSR_TCF) {
        lp_blocks++;                                        /* буфер полон: N отсчётов */
    }
    if (sr & (DMA_CSR_DTEF | DMA_CSR_ULEF | DMA_CSR_USEF)) {
        lp_error = sr;                                      /* канал остановлен аппаратно */
    }
    /* пока флаг не сброшен, LPDMA1 держит запрос такта и Stop 2 остаётся с включённым генератором */
    LPDMA1_Channel0->CFCR = sr & (DMA_CFCR_TCF | DMA_CFCR_DTEF | DMA_CFCR_ULEF | DMA_CFCR_USEF);
}

void ADC4_IRQHandler(void)
{
    if (ADC4->ISR & ADC_ISR_AWD1) {
        ADC4->ISR = ADC_ISR_AWD1;
        lp_alarm = 1;
    }
}

Вход в Stop 2 и главный цикл:

void stop2_enter(void)
{
    PWR->CR1 = (PWR->CR1 & ~PWR_CR1_LPMS) | PWR_CR1_LPMS_1; /* 010 — Stop 2; в Stop 3 LPBAM нет */
    SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
    __DSB();
    __WFI();
    SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk;
}

uint32_t block_mean(void)
{
    const volatile uint16_t *buf = (const volatile uint16_t *)(LLI_BASE + OFF_BUF);
    uint32_t sum = 0, i;

    for (i = 0; i < N_SAMPLES; i++) {
        sum += buf[i];
    }
    return sum / N_SAMPLES;
}

int main(void)
{
    uint32_t done = 0;
    volatile uint32_t mean = 0;

    srd_clocks_init();
    lptim3_init();
    adc4_init();
    lpdma_start();

    for (;;) {
        stop2_enter();                                      /* 16 замеров проходят без ядра */
        if (lp_blocks != done) {
            done = lp_blocks;
            mean = block_mean();                            /* здесь — обработка блока */
        }
        if (lp_alarm || lp_error) {
            lp_alarm = 0;                                   /* здесь — реакция на порог или ошибку */
        }
        (void)mean;
    }
}

10б.5 Прогон: что канал делает по секундам

Плата не нужна, чтобы проверить сам список. Проверка собирает прошивку, достаёт из объектного файла массив lli_tab и отдаёт его модели канала: шесть регистров, подгрузка по битам CLLR, восстановление CBR1, TCEM, проверки выравнивания и того, что все адреса — в SRAM4 или в домене SmartRun. Компилятор сложил в таблицу 14 слов, 56 байт:

Узел TR1 TR2 BR1 SAR DAR LLR
настройки, 0x00 0x00020002 0xC0000200 4 0x28000034 0x46021020 0xF8010018
данных, 0x18 0x00090001 0x00000007 32 0x46021040 0x28000040 0x08000030
кольца, 0x30 — — — — 0x28000040 —

0x46021020 — это ADC4_AWD1TR, 0x46021040 — ADC4_DR. Модель подаёт отсчёт раз в секунду, на 5-й и 50-й секунде — выброс 3500:

  5 с  отсчёт 3500 в окне 0…4095: порог молчит
 16 с  блок из 16 отсчётов готов
    подгрузка узла 0x00: TR1=0x00020002, TR2=0xC0000200, BR1=0x00000004, SAR=0x28000034, DAR=0x46021020, LLR=0xF8010018
 16 с  событие TC: ядро разбужено
 16 с  узел настройки: ADC4_AWD1TR <- 0x0C000400 (окно 1024…3072)
    подгрузка узла 0x18: TR1=0x00090001, TR2=0x00000007, BR1=0x00000020, SAR=0x46021040, DAR=0x28000040, LLR=0x08000030
 32 с  блок из 16 отсчётов готов
    подгрузка узла 0x30: DAR=0x28000040
 32 с  событие TC: ядро разбужено
 48 с  блок из 16 отсчётов готов
    подгрузка узла 0x30: DAR=0x28000040
 48 с  событие TC: ядро разбужено
 50 с  отсчёт 3500 вне окна 1024…3072: прерывание AWD1, ядро разбужено
 64 с  блок из 16 отсчётов готов
    подгрузка узла 0x30: DAR=0x28000040
 64 с  событие TC: ядро разбужено
 80 с  блок из 16 отсчётов готов
    подгрузка узла 0x30: DAR=0x28000040
 80 с  событие TC: ядро разбужено

за 80 с: отсчётов 80, пробуждений по TC 5 (секунды 16,32,48,64,80), по порогу 1 (секунда 50)
без LPDMA1 пробуждений было бы 80
регистры канала в конце: BR1=32 DAR=0x28000040 LLR=0x08000030 EN=1

Восемьдесят замеров — пять пробуждений по блоку и одно по порогу. Выброс на 5-й секунде пришёлся на широкое окно и никого не разбудил. На 16-й узел настройки сузил окно, и такой же выброс на 50-й поднял ядро сразу, не дожидаясь конца блока. После пяти блоков в регистрах канала CLLR = 0x08000030 и CDAR = 0x28000040: кольцо замкнуто.

Прошивка собирается gcc и armclang без предупреждений; у gcc с -Os код вместе с таблицей — 828 байт, переменные — 12 байт. Таблицы узлов у обоих компиляторов совпали байт в байт.

Сверка с ST. Порядок слов в узле тот же, что в драйвере HAL (stm32u5xx_hal_dma_ex.h). Лицензия: BSD-3-Clause. Смещения там такие: CTR1 — 0, CTR2 — 1, CBR1 — 2, CSAR — 3, CDAR — 4, CLLR при линейной адресации — 5. Саму утилиту LPBAM пакет не содержит. В базе STM32CubeMX есть её вызовы для одиннадцати блоков — ADC, COMP, DAC, I2C, LPDMA, LPGPIO, LPTIM, LPUART, OPAMP, SPI, VREFBUF — трёх видов: …_SetConfigQ, …_SetDataQ и …_SetFullQ, например ADV_LPBAM_ADC_Conversion_SetConfigQ. По названиям это очереди узлов настройки, узлов данных и обоих вместе. Раскладку её узлов сверить не с чем: исходники утилиты в пакет не входят.


10б.6 Бюджет: когда LPBAM окупается

Сравним два прибора на одном камне. Оба спят в Stop 2 и мерят с частотой f. Первый будит ядро на каждый отсчёт, второй — раз в N отсчётов. Обработка отсчёта стоит одинаково, когда бы её ни делали, поэтому разница — только в накладных расходах на пробуждение:

Q_п  = Q_wu + I_run · t_н                 заряд одного пробуждения
I_А  = I_0 + f · Q_п                      будить на каждый отсчёт
I_Б  = I_0 + I_SRAM4 + f · Q_отсч + (f / N) · Q_п
I_А − I_Б = f · Q_п · (1 − 1/N) − I_SRAM4 − f · Q_отсч

Из даташита, 3 В, 25 °C, стабилизатор LDO: Stop 2 — 9,05 мкА; SRAM4 в Stop 2 — ещё 0,3 мкА; заряд пробуждения из Stop 2 на HSI16 — 30 нА·с; Run на 16 МГц в диапазоне 4 — 1,30 мА; LPTIM3 от независимого такта — 2,19 мкА/МГц, на LSE это 0,072 мкА. Общая часть I_0 — 9,12 мкА.

Двух чисел в даташите нет. t_н — сколько ядро не спит ради одного пробуждения сверх самой обработки: вход в прерывание, флаги, возврат в Stop. Это допущение, взяты 100 мкс и 1 мс. Q_отсч — заряд одного автономного замера с пересылкой; он не нормирован, и модель считает обратное: при каком Q_отсч второй прибор ещё выигрывает.

t_н и Q_п f Будить каждый отсчёт Потолок выигрыша, N = 16 Порог Q_отсч
100 мкс, 160 нА·с 1 Гц 9,28 мкА −0,15 мкА выигрыша нет
100 мкс, 160 нА·с 10 Гц 10,72 мкА +1,20 мкА 120,0 нА·с
100 мкс, 160 нА·с 100 Гц 25,12 мкА +14,70 мкА 147,0 нА·с
1 мс, 1330 нА·с 1 Гц 10,45 мкА +0,95 мкА 946,9 нА·с
1 мс, 1330 нА·с 10 Гц 22,42 мкА +12,17 мкА 1216,9 нА·с
1 мс, 1330 нА·с 100 Гц 142,12 мкА +124,39 мкА 1243,9 нА·с

Потолок — выигрыш при бесплатном замере, больше не получить.

Вывод тот же, что в 10а, только с другой стороны. При одном замере в секунду и коротком пробуждении будить ядро стоит 0,16 мкА, а одна SRAM4 — 0,3 мкА: LPBAM проигрывает, даже если сам замер ничего не стоит. Он окупается, когда пробуждений много или они длинные: при 100 Гц ядро, которое будят на каждый отсчёт, берёт 25,12 мкА против 9,12, и почти всё это возвращается.

О самом Q_отсч известна одна составляющая. Преобразование при выборке 19,5 такта длится (19,5 + 12 + 0,5) / 16 МГц = 2,0 мкс, регулятор АЦП запускается до 25 мкс, HSI16 берёт 150 мкА: это 4,05 нА·с. Ток АЦП и шины сюда не вошли. Настоящее число дают только замеры на плате — три замера из 10.8, с LPBAM и без.


10б.7 Грабли

Узлы или буфер не в SRAM4. LPDMA1 другой памяти не видит: по руководству, SRAM4 — единственное ОЗУ, доступное ему. Массив узлов, объявленный обычной переменной, ляжет не туда — поэтому прошивка копирует таблицу по адресу SRAM4 сама.

Забыт AMEN. Блок включён, в Sleep работает, а в Stop 2 молчит. Нужны EN, SMEN и AMEN — у периферии, у LPDMA1 и у SRAM4.

LSE есть, LPTIM3 стоит. Не выставлен LSESYSEN: кварц тактирует только RTC.

Такт АЦП от шины. ADCDACSEL = 000 — это такт HCLK. В Stop 2 ADC4 работает только от HSI16 или MSIK.

Флаг канала не сброшен. Пока TCF висит, LPDMA1 держит запрос такта: камень в Stop 2, а генератор идёт. То же, если прерывание блока разрешено, а в NVIC — нет: руководство это прямо запрещает.

Stop 2 не наступает сразу. Если в момент WFI блок просит такт, вход в Stop откладывается до конца запроса.

Ждать ADRDY при AUTOFF. В режиме автоотключения флаг готовности не взводится; цикл ожидания зависнет.

Калибровка после AUTOFF. Калибруют при AUTOFF = 0, DPD = 0 и DMAEN = 0, режим включают потом.

DMACFG = 0 с кольцом. В разовом режиме АЦП перестаёт просить пересылку, когда контроллер закончил блок. Для кольца нужен DMACFG = 1.

ARR до ENABLE. ARR и CCR2 у LPTIM пишут только при включённом таймере, CFGR — только при выключенном.

Менять узлом настройки любой регистр АЦП. На ходу руководство разрешает менять только пороги AWDxTR. Канал, выборку и запуск меняют при ADSTART = 0.

Stop 3 вместо Stop 2. Ток сна ниже, но LPBAM там нет.

TrustZone. При TZEN = 1 у защищённой программы SRAM4 и периферия лежат по другим адресам (SRAM4 — 0x3800 0000), а у пересылки появляются биты SSEC и DSEC. Пример написан для TZEN = 0.

LPBAM ради 1 Гц. При редких замерах одна SRAM4 стоит дороже сэкономленных пробуждений.


10б.8 Итоги

Полные исходники

lpbam10b.c — LPTIM3 → ADC4 → LPDMA1 → SRAM4 в Stop 2: ядро просыпается раз в блок отсчётов.

/* Раздел 10б. LPTIM3 -> ADC4 -> LPDMA1 -> SRAM4 в Stop 2: ядро просыпается раз в N отсчётов.
 * STM32U585, заголовок CMSIS stm32u585xx.h, TrustZone выключен (TZEN = 0).
 * Сборка: arm-none-eabi-gcc -mcpu=cortex-m33 -mthumb -Os -DSTM32U585xx -c lpbam10b.c */
#include <stddef.h>
#include "stm32u5xx.h"

#define N_SAMPLES   16u                         /* отсчётов в блоке: одно пробуждение на 16 замеров */
#define LSE_HZ      32768u
#define ARR_1HZ     (LSE_HZ - 1u)               /* период LPTIM3 — 1 с */
#define AWD_WIDE    0x0FFF0000u                 /* окно порога 0…4095: первый блок не будит */
#define AWD_TIGHT   0x0C000400u                 /* рабочее окно 1024…3072 */

/* Раскладка SRAM4: всё, что читает и пишет LPDMA1 в Stop 2, лежит здесь */
#define LLI_BASE    SRAM4_BASE                  /* в CLBAR — старшие 16 бит адреса узлов */
#define OFF_CFG     0x00u                       /* узел настройки: 6 слов */
#define OFF_DATA    0x18u                       /* узел данных: 6 слов */
#define OFF_RING    0x30u                       /* узел кольца: 1 слово, новый CDAR */
#define OFF_AWD     0x34u                       /* слово, которое узел настройки пишет в ADC4_AWD1TR */
#define OFF_BUF     0x40u                       /* буфер отсчётов, N_SAMPLES по 16 бит */

#define ADC4_DR_ADDR      (ADC4_BASE + offsetof(ADC_TypeDef, DR))
#define ADC4_AWD1TR_ADDR  (ADC4_BASE + offsetof(ADC_TypeDef, AWD1TR))

#define REQ_ADC4    7u                          /* adc4_dma, RM0456 таблица 146 */
#define TR1_DATA    (DMA_CTR1_SDW_LOG2_0 | DMA_CTR1_DDW_LOG2_0 | DMA_CTR1_DINC)   /* 16 бит -> 16 бит, приёмник с шагом */
#define TR2_DATA    (REQ_ADC4 << DMA_CTR2_REQSEL_Pos)                             /* TCEM = 00: событие в конце блока */
#define TR1_CFG     (DMA_CTR1_SDW_LOG2_1 | DMA_CTR1_DDW_LOG2_1)                   /* слово -> слово, адреса на месте */
#define TR2_CFG     (DMA_CTR2_SWREQ | DMA_CTR2_TCEM)                              /* без запроса; TCEM = 11: события нет */
#define LLR_ALL     (DMA_CLLR_UT1 | DMA_CLLR_UT2 | DMA_CLLR_UB1 | DMA_CLLR_USA | DMA_CLLR_UDA | DMA_CLLR_ULL)

/* Узлы связного списка. Порядок слов в узле — порядок регистров: TR1, TR2, BR1, SAR, DAR, LLR */
const uint32_t lli_tab[14] = {
    /* 0x00 узел настройки: 4 байта из SRAM4 в регистр порога АЦП, дальше — узел данных */
    TR1_CFG, TR2_CFG, 4u, LLI_BASE + OFF_AWD, ADC4_AWD1TR_ADDR, LLR_ALL | OFF_DATA,
    /* 0x18 узел данных: N отсчётов из ADC4_DR в буфер, дальше — узел кольца */
    TR1_DATA, TR2_DATA, 2u * N_SAMPLES, ADC4_DR_ADDR, LLI_BASE + OFF_BUF, DMA_CLLR_UDA | OFF_RING,
    /* 0x30 узел кольца: только CDAR. ULL = 0, поэтому CLLR остаётся на нём — блок повторяется вечно */
    LLI_BASE + OFF_BUF,
    /* 0x34 данные узла настройки */
    AWD_TIGHT,
};

volatile uint32_t lp_blocks;                    /* сколько блоков собрано */
volatile uint32_t lp_alarm;                     /* сработал порог */
volatile uint32_t lp_error;                     /* флаги ошибок канала */

void srd_clocks_init(void)
{
    RCC->AHB3ENR |= RCC_AHB3ENR_PWREN;
    PWR->DBPR |= PWR_DBPR_DBP;                              /* открыть резервный домен: там LSE */
    RCC->BDCR |= RCC_BDCR_LSEON;
    while (!(RCC->BDCR & RCC_BDCR_LSERDY)) { }
    RCC->BDCR |= RCC_BDCR_LSESYSEN;                         /* без этого LSE достаётся только RTC */
    while (!(RCC->BDCR & RCC_BDCR_LSESYSRDY)) { }
    RCC->CR |= RCC_CR_HSION;                                /* HSI16: такт АЦП и шины в Stop 2 */
    while (!(RCC->CR & RCC_CR_HSIRDY)) { }
    RCC->CFGR1 |= RCC_CFGR1_STOPWUCK;                       /* шину в Stop будит HSI16 */

    RCC->CCIPR3 = (RCC->CCIPR3 & ~(RCC_CCIPR3_LPTIM34SEL | RCC_CCIPR3_ADCDACSEL))
                | RCC_CCIPR3_LPTIM34SEL                     /* 11: LPTIM3 от LSE */
                | RCC_CCIPR3_ADCDACSEL_2;                   /* 100: АЦП от HSI16 */

    /* три бита на каждый блок: EN — такт в Run, SMEN — в Sleep и Stop, AMEN — в Stop 2 */
    RCC->AHB3ENR   |= RCC_AHB3ENR_ADC4EN | RCC_AHB3ENR_LPDMA1EN | RCC_AHB3ENR_SRAM4EN;
    RCC->APB3ENR   |= RCC_APB3ENR_LPTIM3EN;
    RCC->AHB3SMENR |= RCC_AHB3SMENR_ADC4SMEN | RCC_AHB3SMENR_LPDMA1SMEN | RCC_AHB3SMENR_SRAM4SMEN;
    RCC->APB3SMENR |= RCC_APB3SMENR_LPTIM3SMEN;
    RCC->SRDAMR    |= RCC_SRDAMR_ADC4AMEN | RCC_SRDAMR_LPDMA1AMEN | RCC_SRDAMR_SRAM4AMEN
                    | RCC_SRDAMR_LPTIM3AMEN;

    PWR->CR2 &= ~PWR_CR2_SRAM4PDS;                          /* SRAM4 в Stop не выключать */
}

void lptim3_init(void)
{
    LPTIM3->CFGR = 0;                                       /* внутренний такт, делитель 1, пуск программный */
    LPTIM3->CCMR1 = 0;                                      /* канал 2 — выход, полярность прямая */
    LPTIM3->CR = LPTIM_CR_ENABLE;                           /* ARR и CCR2 пишут только при ENABLE = 1 */
    LPTIM3->ICR = LPTIM_ICR_ARROKCF | LPTIM_ICR_CMP2OKCF;
    LPTIM3->ARR = ARR_1HZ;
    while (!(LPTIM3->ISR & LPTIM_ISR_ARROK)) { }
    LPTIM3->CCR2 = ARR_1HZ / 2u;                            /* фронт lptim3_ch2 раз в период */
    while (!(LPTIM3->ISR & LPTIM_ISR_CMP2OK)) { }
    LPTIM3->CCMR1 |= LPTIM_CCMR1_CC2E;
    LPTIM3->CR |= LPTIM_CR_CNTSTRT;                         /* непрерывный счёт */
}

void adc4_init(void)
{
    ADC4->ISR = ADC_ISR_LDORDY;                             /* флаги сбрасывают записью 1 */
    ADC4->CR = ADC_CR_ADVREGEN;
    while (!(ADC4->ISR & ADC_ISR_LDORDY)) { }
    ADC4->CR |= ADC_CR_ADCAL;                               /* калибровка: ADEN = 0, AUTOFF = 0, DMAEN = 0 */
    while (ADC4->CR & ADC_CR_ADCAL) { }

    ADC4->CFGR1 = ADC4_CFGR1_DMAEN | ADC4_CFGR1_DMACFG      /* запрос на каждый отсчёт, без остановки */
                | ADC_CFGR1_EXTEN_0                         /* запуск по фронту */
                | ADC4_CFGR1_EXTSEL_2 | ADC4_CFGR1_EXTSEL_1 /* 110: adc_trg6 = lptim3_ch2 */
                | ADC_CFGR1_AWD1EN | ADC_CFGR1_AWD1SGL | ADC_CFGR1_AWD1CH_0;   /* порог на канале 1 */
    ADC4->SMPR1 = ADC4_SMPR_SMP1_2;                         /* ADC_SMPR в RM0456; 100: выборка 19,5 такта */
    ADC4->CHSELR = ADC_CHSELR_CHSEL1;                       /* ADC4_IN1 */
    ADC4->AWD1TR = AWD_WIDE;
    ADC4->ISR = ADC_ISR_AWD1;
    ADC4->IER = ADC_IER_AWD1IE;                             /* будить по порогу, а не по каждому отсчёту */
    ADC4->PWRR = ADC4_PWRR_AUTOFF | ADC4_PWRR_DPD;          /* автономный режим: между замерами выключен */
    ADC4->CR |= ADC_CR_ADEN;                                /* ADRDY при AUTOFF = 1 не взводится — не ждём */
    ADC4->CR |= ADC_CR_ADSTART;                             /* дальше замеры идут по lptim3_ch2 */
    NVIC_EnableIRQ(ADC4_IRQn);
}

void lpdma_start(void)
{
    uint32_t *mem = (uint32_t *)LLI_BASE;
    uint32_t i;

    for (i = 0; i < sizeof lli_tab / sizeof lli_tab[0]; i++) {
        mem[i] = lli_tab[i];                                /* узлы — в SRAM4: другой памяти LPDMA1 не видит */
    }

    LPDMA1_Channel0->CCR = DMA_CCR_RESET;
    LPDMA1_Channel0->CFCR = DMA_CFCR_TCF | DMA_CFCR_HTF | DMA_CFCR_DTEF | DMA_CFCR_ULEF
                          | DMA_CFCR_USEF | DMA_CFCR_SUSPF | DMA_CFCR_TOF;
    LPDMA1_Channel0->CLBAR = LLI_BASE;
    /* нулевой элемент списка живёт прямо в регистрах канала: первый блок данных */
    LPDMA1_Channel0->CTR1 = TR1_DATA;
    LPDMA1_Channel0->CTR2 = TR2_DATA;
    LPDMA1_Channel0->CBR1 = 2u * N_SAMPLES;
    LPDMA1_Channel0->CSAR = ADC4_DR_ADDR;
    LPDMA1_Channel0->CDAR = LLI_BASE + OFF_BUF;
    LPDMA1_Channel0->CLLR = LLR_ALL | OFF_CFG;              /* после блока — узел настройки */
    LPDMA1_Channel0->CCR = DMA_CCR_TCIE | DMA_CCR_DTEIE | DMA_CCR_ULEIE | DMA_CCR_USEIE;
    NVIC_EnableIRQ(LPDMA1_Channel0_IRQn);
    LPDMA1_Channel0->CCR |= DMA_CCR_EN;
}

void LPDMA1_Channel0_IRQHandler(void)
{
    uint32_t sr = LPDMA1_Channel0->CSR;

    if (sr & DMA_CSR_TCF) {
        lp_blocks++;                                        /* буфер полон: N отсчётов */
    }
    if (sr & (DMA_CSR_DTEF | DMA_CSR_ULEF | DMA_CSR_USEF)) {
        lp_error = sr;                                      /* канал остановлен аппаратно */
    }
    /* пока флаг не сброшен, LPDMA1 держит запрос такта и Stop 2 остаётся с включённым генератором */
    LPDMA1_Channel0->CFCR = sr & (DMA_CFCR_TCF | DMA_CFCR_DTEF | DMA_CFCR_ULEF | DMA_CFCR_USEF);
}

void ADC4_IRQHandler(void)
{
    if (ADC4->ISR & ADC_ISR_AWD1) {
        ADC4->ISR = ADC_ISR_AWD1;
        lp_alarm = 1;
    }
}

void stop2_enter(void)
{
    PWR->CR1 = (PWR->CR1 & ~PWR_CR1_LPMS) | PWR_CR1_LPMS_1; /* 010 — Stop 2; в Stop 3 LPBAM нет */
    SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
    __DSB();
    __WFI();
    SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk;
}

uint32_t block_mean(void)
{
    const volatile uint16_t *buf = (const volatile uint16_t *)(LLI_BASE + OFF_BUF);
    uint32_t sum = 0, i;

    for (i = 0; i < N_SAMPLES; i++) {
        sum += buf[i];
    }
    return sum / N_SAMPLES;
}

int main(void)
{
    uint32_t done = 0;
    volatile uint32_t mean = 0;

    srd_clocks_init();
    lptim3_init();
    adc4_init();
    lpdma_start();

    for (;;) {
        stop2_enter();                                      /* 16 замеров проходят без ядра */
        if (lp_blocks != done) {
            done = lp_blocks;
            mean = block_mean();                            /* здесь — обработка блока */
        }
        if (lp_alarm || lp_error) {
            lp_alarm = 0;                                   /* здесь — реакция на порог или ошибку */
        }
        (void)mean;
    }
}

Дальше: раздел 11 «Память, компоновка, загрузка» — где что лежит, как устроен скрипт компоновщика и что происходит между подачей питания и вызовом main.

Открыть в интерактивном справочнике