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: узел списка может записать слово из памяти в регистр настройки блока. 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 Итоги
- LPBAM — это автономная периферия домена SmartRun, LPDMA1 со связным списком и аппаратные запуски. Работает в Stop 0, 1 и 2; в Stop 3 — нет.
- В Stop 2 автономны LPUART1, SPI3, I2C3, ADC4, DAC1, LPTIM1, LPTIM3, ADF1; память пересылок — только SRAM4, 16 КБ.
- Каждому блоку, LPDMA1 и SRAM4 нужны три бита:
EN,SMEN,AMEN. - Узел списка — до шести слов:
CTR1,CTR2,CBR1,CSAR,CDAR,CLLR. БитыUT1…ULLговорят, какие слова в узле есть. Кольцо — узел сULL= 0. - Узел с
SWREQ= 1 пишет слово из SRAM4 в регистр периферии — так блок перенастраивают без ядра. - Пример: LPTIM3 раз в секунду запускает ADC4, канал 0 копит 16 отсчётов. За 80 с — 5 пробуждений по блоку и 1 по порогу вместо 80. Таблица узлов — 56 байт, код — 828 байт.
- Запрос ADC4 — номер 7; запуск ADC4 от LPTIM3 —
adc_trg6, второй канал таймера. - Stop 2 — 9,05 мкА, SRAM4 — плюс 0,3 мкА, пробуждение — 30 нА·с. При 1 Гц и накладных 100 мкс LPBAM проигрывает 0,15 мкА; при 100 Гц потолок выигрыша — 14,70 мкА из 25,12.
Полные исходники
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.