STM32. CMSIS. Использование UART в качестве таймера для генерации периодических прерываний

В своём курсовом проекте я столкнулся с нехваткой таймеров. Выгреб всё, включая SysTick! И призадумался, как же обойти это ограничение. Необходимо было найти некую периферию, которая смогла бы заменить собой таймер.

Условия: генерация прерываний с частотой от ~100 Гц до ~20 кГц. Точность настрйки периода срабатывания около 1 Гц во всём диапазоне.

В микроконтроллере STM32F303RE обнаружилось аж пять контроллеров UART. Один из них в моём проекте использовался для связи с ПК, а вот остальные четыре можно было использовать в качестве таймеров.

Как же их использовать? Всё очень просто. У каждого контроллера UART есть настройка скорости отправки данных:

40956b08016ee263da25f0001075f282.png

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

Способ №1 — Без контроллера DMA

Данный способ я использовал в курсовом проекте, поскольку в F303 нет гибкой настройки каналов DMA и источников сигналов для них (нет DMAMUX), да к тому же 9 из 12-ти каналов DMA уже были заняты.

Способ заключается в следующем: включаем UART, причём совершенно необязательно настраивать выводы микроконтроллера под него, т.е. UART будет слать данные «в никуда». Настраиваем формат данных и регистр BRR, после чего включаем прерывание по окончанию отправки кадра.

Итак, для начала настроим тактирование от внешнего кварца:

void set_72MHz()
{
	RCC->CR |= ((uint32_t)RCC_CR_HSEON);

	/* Ждем пока HSE не выставит бит готовности */
	while(!(RCC->CR & RCC_CR_HSERDY)) {}

	/* Конфигурируем Flash на 2 цикла ожидания */
	/* Flash не может работать на высокой частоте */
	FLASH->ACR &= ~FLASH_ACR_LATENCY;
	FLASH->ACR |= FLASH_ACR_LATENCY_2;

	/* HCLK = SYSCLK */
	RCC->CFGR |= RCC_CFGR_HPRE_DIV1;

	/* PCLK2 = HCLK */
	RCC->CFGR |= RCC_CFGR_PPRE2_DIV1;

	/* PCLK1 = HCLK / 2 */
	RCC->CFGR |= RCC_CFGR_PPRE1_DIV2;

	/* Конфигурируем множитель PLL configuration: PLLCLK = HSE * 9 = 72 MHz */
	/* При условии, что кварц на 8МГц! */
	RCC->CFGR &= ~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLMUL);
	RCC->CFGR |= RCC_CFGR_PLLSRC_HSE_PREDIV | RCC_CFGR_PLLMUL9;

	/* Включаем PLL */
	RCC->CR |= RCC_CR_PLLON;

	/* Ожидаем, пока PLL выставит бит готовности */
	while((RCC->CR & RCC_CR_PLLRDY) == 0) { asm("nop"); }

	/* Выбираем PLL как источник системной частоты */
	RCC->CFGR &= ~RCC_CFGR_SW;
	RCC->CFGR |= RCC_CFGR_SW_PLL;

	/* Ожидаем, пока PLL выберется как источник системной частоты */
	while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) { asm("nop"); }

	SystemCoreClockUpdate();
}

Теперь включим тактирование и подготовим шаблон функции main. Нам понадобится включить только сам UART (я выбрал USART1) и какой-либо порт GPIO, чтобы дрыгать ножкой в обработчике прерывания:

void init_clocks()
{
	RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
	RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
}
int main(void)
{
	init_clocks();
	set_72MHz();

	//настройка периферии тут

	while (1) { asm("nop"); }

	return 0;
}

Пока выполняется функция set_72MHz(), тактирование периферии точно успеет включиться. Поэтому set_72MHz() стоит после init_clocks().

Теперь завершим настройку периферии. Во-первых, приготовим макросы для дрыганья ножкой (вывод PA0). Поскольку для STM32F303 регистр GPIOx->BSRR зачем-то объявлен как два 16-битных поля (в отличие от, например, STM32F0x), нужно привести указатель к типу volatile uint32_t*, чтобы потом уже обращаться к регистру как к 32-битному полю:

#define PA0_HIGH *(volatile uint32_t*)&(GPIOA->BSRRL) = (1 << (0));
#define PA0_LOW *(volatile uint32_t*)&(GPIOA->BSRRL) = (1 << (0 + 16));

Теперь настроим вывод PA0:

//настройка вывода PA0 на выход
GPIOA->MODER &= ~GPIO_MODER_MODER0;
GPIOA->MODER |= GPIO_MODER_MODER0_0;

//устанавливаем низкий логический уровень на выводе PA0
PA0_LOW

Теперь посмотрим на настройки UART. Нам необходимо заполучить как можно больший диапазон возможных частот срабатывания. Для этого надо максимально «растянуть» каждый кадр UART. Посмотрим на структуру кадра в reference manual’е:

Максимальная длина слова с данными 9 бит.

Максимальная длина слова с данными 9 бит.

То есть имеем стартовый бит + 9 бит данных + стоповый бит. Итого 11 бит. А можно ли ещё длиннее? Ответ — да! Посмотрим на настройку стопового бита:

Максимум два стоповых бита.

Максимум два стоповых бита.

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

По умолчанию приёмопередатчик USART1 тактируется от периферийной шины. В нашем случае это шина APB2, и мы выставили максимально возможную тактовую частоту согласно даташиту:

315591b258da0353d96922a85d18dea6.png

По умолчанию каждый отправленный/принятый бит UART соответствует одному такту этой частоты, делёной на число в регистре BRR. В каждом кадре таких битов 12. Из чего вытекает формула частоты прерываний:

f_{int}=\frac{72000000}{12B}=\frac{6000000}{B},

где B— число в регистре BRR. Следует заметить, что B\in [16; 65 527],иначе UART работать не будет. Очевидно, что для других микроконтроллеров и даже для других UART’ов этого же микроконтроллера частота будет другая, плюс можно настроить тактирование приёмопередатчика от других источников в регистрах RCC:

Следует помнить, что не во всех микроконтроллерах STM32 есть такая возможность.

Следует помнить, что не во всех микроконтроллерах STM32 есть такая возможность.

Для простоты оставим всё как есть. Пора настраивать сам UART:

//прерывание по пустому регистру отправляемых данных,
//9 бит данных в кадре (максимум)
USART1->CR1 |= USART_CR1_TXEIE | USART_CR1_M0;
//2 стоповых бита в кадре (максимум)
USART1->CR2 |= USART_CR2_STOP_1;
//включаем UART (но не включаем передачу)
USART1->CR1 |= USART_CR1_UE;

//срабатывание
//6000000 / 1000 = 6000 раз в секунду
//период 166.(6) мкс
USART1->BRR = 1000;

//записываем отправляемые данные
USART1->TDR = 0;

//включаем передачу
USART1->CR1 |= USART_CR1_TE;

//включаем прерывания от USART1
//(включение таймера)
NVIC_EnableIRQ(USART1_IRQn);

Теперь необходимо написать обработчик прерывания, который будет класть новые данные в USART1->TDR (иначе микроконтроллер зависнет в пустом обработчике прерывания). Название функции берём из startup-файла:

void USART1_IRQHandler()
{
    //дрыг ножкой вверх
	PA0_HIGH

	//если прерывание по пустому
	//регистру отправляемых
	//данных
	if (USART1->ISR & USART_ISR_TXE)
	{
		//сброс флага прерывания
		//при помощи записи новых
		//данных
		USART1->TDR = 0;
	}
    //дрыг ножкой вниз
	PA0_LOW
}

В этот обработчик можно добавить то, что Вам нужно (ведь не будем же мы вызывать его просто так). Я ограничился дёрганьем вывода, чтобы можно было увидеть частоту срабатывания прерывания.

Теперь вернёмся в функцию main. Достоинством использования UART в качестве таймера является возможность изменения частоты прерываний «на лету» (причём частоту можно как уменьшать, так и увеличивать). Это можно продемонстрировать, дописав код изменения прямо в функцию main:

//ждём
for(int j = 0; j <= 60000; j++) { asm("nop"); }

for(int i = 1000; i >= 100; i -= 1)
{
    //увеличиваем частоту срабатывания таймера
    USART1->BRR = i;

    //ждём
    for(int j = 0; j <= 600; j++) { asm("nop"); }
}

for(int i = 100; i <= 1000; i += 1)
{
    //уменьшаем частоту срабатывания таймера
    USART1->BRR = i;

    //ждём
    for(int j = 0; j <= 600; j++) { asm("nop"); }
}

//выключаем прерывания от USART1
//(выключение таймера)
NVIC_DisableIRQ(USART1_IRQn);

//выключаем передачу
USART1->CR1 &= ~USART_CR1_TE;

Теперь тестовая программа готова. Подключаем логический анализатор к выводу PA0, запускаем программу и смотрим:

Первый временной интервал чуть-чуть «уплыл».

Первый временной интервал чуть-чуть «уплыл».

А вот второй и последующие крайне близки к расчётному значению.

А вот второй и последующие крайне близки к расчётному значению.

Промотаем чуть вперёд:

Частота повышается, а импульсы продолжают идти равномерно и непрерывно.

Частота повышается, а импульсы продолжают идти равномерно и непрерывно.

Дошли до минимального периода. Дальше частота будет уменьшаться.

Дошли до минимального периода. Дальше частота будет уменьшаться.

Импульсы заканчиваются. Поскольку после установки максимального периода в конце не добавлено ожидание, частота вышла чуть больше расчётной (не прошло одного полного периода с минимальной частотой).

Импульсы заканчиваются. Поскольку после установки максимального периода в конце не добавлено ожидание, частота вышла чуть больше расчётной (не прошло одного полного периода с минимальной частотой).

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

Способ №2 — С контроллером DMA

Предложенный выше способ содержит один недостаток. Поскольку новые данные кладутся в прерывании, реальный период следования прерываний больше, чем расчётный. Это обуславливается задержкой записи нового значения. Действительно, микроконтроллеру нужно как минимум 12 тактов системной частоты для ухода в прерывание, плюс несколько тактов на ветвление и проверку флага. Добавим к этому два цикла ожидания флэш-памяти при работе на частоте 72 МГц, которые сказываются и при чтении вектора прерывания, и при исполнении кода.

Попробуем избавиться от этих недостатков при помощи использования контроллера DMA.

Контроллер DMA так же даёт одно важное преимущество — возможность снизить частоту следования прерываний. По формуле, приведённой выше, минимальная частота прерываний составляет

f_{int.min}=\frac{6000000}{65527}\approx91.565 Hz

при условии, что источник тактирования не меняется. Да, можно переключиться на LSE, но часовой кварц не везде стоит. DMA же может позволить генерировать прерывание в n раз реже, где n может доходить до 65535.

Изменим наш код. Для начала добавим включение тактирования контроллера DMA:

void init_clocks()
{
	RCC->AHBENR |= RCC_AHBENR_GPIOAEN | RCC_AHBENR_DMA1EN;
	RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
}

Теперь изменим инициализацию UART. Уберём включение прерывания и добавим включение работы с контроллером DMA:

//9 бит данных в кадре (максимум)
USART1->CR1 |= USART_CR1_M0;
//2 стоповых бита в кадре (максимум)
USART1->CR2 |= USART_CR2_STOP_1;
//включаем DMA на передачу
USART1->CR3 |= USART_CR3_DMAT;
//включаем UART (но не включаем передачу)
USART1->CR1 |= USART_CR1_UE;

//срабатывание
//6000000 / 1000 = 6000 раз в секунду
//период 166.(6) мкс
USART1->BRR = 1000;

Посмотрим в reference manual’е, какой канал контроллера DMA может работать на передачу USART1:

Нам подходит 4-ый канал.

Нам подходит 4-ый канал.

Настраиваем его. Для начала заведём глобальную переменную с нулевым значением, из которой контроллер DMA постоянно будет слать в UART значение:

uint8_t zero_var = 0;

Теперь настроим контроллер DMA:

//по байту, из zero_var в USART1->TDR,
//в циклическом режиме, по 2 посылки
//на цикл, прерывание по окончанию
//цикла
DMA1_Channel4->CCR = DMA_CCR_CIRC | DMA_CCR_DIR |
        DMA_CCR_TCIE;
DMA1_Channel4->CMAR = (uint32_t)&zero_var;
DMA1_Channel4->CPAR = (uint32_t)&(USART1->TDR);
DMA1_Channel4->CNDTR = 2;

Делитель частоты прерывания записывается в регистр CNDTR нашего канала DMA. Здесь он равен двум, т.е. сначала частота следования прерываний будет равна 3 кГц.

Включаем прерывания, UART и DMA:

//включаем прерывания от канала DMA
//(включение таймера)
NVIC_EnableIRQ(DMA1_Channel4_IRQn);

//включаем передачу
USART1->CR1 |= USART_CR1_TE;
//включаем DMA
DMA1_Channel4->CCR |= DMA_CCR_EN;

Теперь перепишем обработчик прерывания. Название, опять же, берём из startup-файла:

void DMA1_Channel4_IRQHandler()
{
    //дрыг ножкой вверх
	PA0_HIGH

	//если прерывание по окончанию цикла
	if (DMA1->ISR & DMA_ISR_TCIF4)
	{
		//сброс флага прерывания
		DMA1->IFCR = DMA_IFCR_CTCIF4;
	}
    //дрыг ножкой вниз
	PA0_LOW
}

В таком состоянии уже будет работать и давать импульсы на PA0. Однако надо проверить, работает ли переключение «на лету» частоты. Для этого надо оставить тот же тестовый код, разве что надо будет чуть-чуть увеличить задержки:

//ждём
for(int j = 0; j <= 120000; j++) { asm("nop"); }

for(int i = 1000; i >= 100; i -= 1)
{
    //увеличиваем частоту срабатывания таймера
    USART1->BRR = i;

    //ждём
    for(int j = 0; j <= 1800; j++) { asm("nop"); }
}

for(int i = 100; i <= 1000; i += 1)
{
    //уменьшаем частоту срабатывания таймера
    USART1->BRR = i;

    //ждём
    for(int j = 0; j <= 1800; j++) { asm("nop"); }
}

//ждём
for(int j = 0; j <= 120000; j++) { asm("nop"); }

//выключаем прерывания от канала DMA
//(выключение таймера)
NVIC_DisableIRQ(DMA1_Channel4_IRQn);

//выключаем передачу
USART1->CR1 &= ~USART_CR1_TE;

Опять подключаем анализатор и смотрим:

6472d93f1ab7933c7735be2f7e3b6bb3.pngЧастота выдерживается гораздо точнее.

Частота выдерживается гораздо точнее.

В середине происходит то же самое. Заглянем в конец:

38d8f09a70908c1653e46cfc1ef636ab.png

Видим, что частота выдерживается с большей точностью. При этом сохраняется возможность изменения частоты «на лету».

Заключение

В теории для тех же целей можно использовать АЦП и SPI, но у них достаточно неточная настройка скорости генерации прерываний. UART подходит лучше всего.

В старших семействах есть LPUART, у которого регистр BRR на несколько бит длиннее. Там можно обойтись и без DMA.

Весь исходный код и файлы с логического анализатора доступны на GitHub.

© Habrahabr.ru