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

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

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

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

В данном случае это по сути 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, и мы выставили максимально возможную тактовую частоту согласно даташиту:

По умолчанию каждый отправленный/принятый бит 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;

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

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

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

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

Заключение

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

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

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

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


  1. placidity_master
    04.01.2025 20:17

    Выгреб всё, включая SysTick

    а может проще было использовать "сюстик" для работы диспетчера задач и тогда одного таймера хватило бы на 100к задач ?


    1. juramehanik
      04.01.2025 20:17

      Сустиком не задать что-то порядка пары микросекунд или наносекунд. Потому часто отдельные таймеры и нужны, это не редкие задачи. Так что тацмеры вполне могут закончится если еще и привязка к пинам и не очень большой корпус, у меня так было когда делал GPSDO на f103, таймеров хватило в притык на все потребности железки - кроме DPLL надо было еще через spi и сдвиговые регистры выводить сигналы точного времени и частоты (стробы это выходы таймеров). Если бы таймеров не хватило пришлось бы такое творить, повесив менее критичные по времени сигналы на uart или spi.


      1. mlnw
        04.01.2025 20:17

        Сустиком не задать что-то порядка пары микросекунд или наносекунд.

        Для отмера небольших интервалов хватает на всю прошивку одного таймера-молотилки, который никогда не останавливается, с проверкой достижения значения в глухом цикле, когда городить огород с прерываниями нет нужды (обычно, какого-нибудь 32-разрядного TIM2).


        1. juramehanik
          04.01.2025 20:17

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


    1. mlnw
      04.01.2025 20:17

      Вот мне тоже первое, что приходит на ум, и не даёт читать дальше: автор как-то неправильно использует таймеры, которых натыкано обычно с запасом.


      1. LTVA Автор
        04.01.2025 20:17

        Четыре таймера генерят ШИМ. Каждый такой сигнал должен иметь свою независимую частоту, поэтому четыре. Ещё один таймер делает ШИМ, который тактирует генератор шума. Два таймера используются для задания сэмплрейта того, что играет через внутренний двухканальный ЦАП, по одному таймеру на канал. Ещё один таймер задаёт тикрейт движка исполнения команд. Ещё два таймера делают костыльный ШИМ-ЦАП, один с фиксированным периодом молотит ШИМ, от второго идёт, опять же, сэмплрейт этого канала (он инициирует пересылки DMA). И после всего этого у меня остались только TIM20 и SysTick. Они были пущены на таймеры сброса фазы. И потом оказалось, что двух таких таймеров недостаточно, пришлось искать что-то, что смогло бы их заменить.


        1. rukhi7
          04.01.2025 20:17

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

          Задача конечно нетривиальная сделать несколько софтовых таймеров на одном аппаратном, но если получится то сможете оценить преимущества. Синхронизация и/или управление-взаимная настройка разных циклов-частот событий становится фактически тривиальной задачей.


          1. LTVA Автор
            04.01.2025 20:17

            В проекте не сильно нужна была синхронизация событий. А вот средняя частота должна была регулироваться очень точно, поскольку музыка. Если бы я сделал на одном таймере, то были бы прерывания с частотой минимум 10 кГц, а это уже ест довольно много процессорного времени. Посмотрите код, там в принципе описано, зачем сделано именно так.


            1. rukhi7
              04.01.2025 20:17

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

              Интересно вы намеренно пытаетесь обмануть аудиторию или сами не понимаете о чем пишите? Задержка ухода в прерывание не изменяется от прерывания к прерыванию(как минимум в среднем), соответственно это никак не влияет на частоту этих прерываний и/или заданных частотой этих прерываний записи значений. Реальный период следования прерываний не может быть больше, чем расчётный если этот расчетный период прерываний просто правильно посчитать.

              Может вы просто не правильно сформулировали то о чем хотели рассказать? Это бывает.


              1. LTVA Автор
                04.01.2025 20:17

                Я хотел сказать, что мы по формуле насчитали период, допустим, 1 мс. Запустили. Через 1 мс UART дал сигнал прерывания. Но микроконтроллер мгновенно отреагировать не может, поскольку ему требуется несколько тактов на уход в прерывание и несколько тактов в самом прерывании до записи непосредственно в регистр BRR, и именно с момента записи в регистр BRR пойдёт отсчёт следующей миллисекунды. Т.е. период будет равен не 1 мс, а 1 мс + некоторое время, около 100 нс — 1 мкс, пока микроконтроллер уходит в прерывание и кладёт новые данные. Причём это в идеальном случае. Что если контроллер DMA занял шину для транзакции? Что если код прерывания и сам вектор лежат в медленной памяти (флэш с тактами задержки доступа, QSPI-флэш?) Что если это произошло в момент исполнения обработчика прерывания более высокого приоритета? Всё это внесёт дополнительные задержки. В представленном случае они всё ещё достаточно малы по сравнению с периодом прерывания, однако они будут накапливаться с каждым срабатыванием, и 1000 прерываний произойдёт уже не за одну условную секунду (условную, поскольку неизвестна стабильность источника тактовых импульсов), а за, например, 1.001 секунду, что уже может быть существенно в каких-то задачах. Поскольку я заявляю данное решение как альтернативу таймеру, который этих недостатков лишён (ведь он считает постоянно и ничто его не задерживает в момент срабатывания прерывания переполнения счётчика), я попытался описать недостаток этого решения.


                1. rukhi7
                  04.01.2025 20:17

                  Т.е. период будет равен не 1 мс, а 1 мс + некоторое время, около 100 нс — 1 мкс, пока микроконтроллер уходит в прерывание и кладёт новые данные.

                  Ну замечательно, у нас получается что момент события в прерывании на который вы ориентируетесь (хоть запись в регист, хоть выполнение определенной инструкции) отложен на некоторый период dt. И в следующем прерывании этот момент определенной инструкции тоже будет отложен на то же самое время...

                  А, ну да! если эта контрольная инструкция запускает отсчет до следующего прерывания то это dt действительно дает добавку к периоду, хорошо, тут я что-то не додумал сразу! Но в этом случае, я думаю, будет проще измерить и учитывать эту дополнительную задержку dt чем добавлять еще один аппаратный модуль с его дополнительной временной неопределенностью (которую вы правильно упомянули относительно занятости шин), и которые вносят дополнительную погрешность.

                  Но вообще эти эксперименты с нештатной работой модулей переферии конечно полезны для саморазвития, но если вам не могут подсказать преподаватели как тут правильно поступать - использовать таймеры, это печально. Хотя... когда я учился программированию мы-ученики тоже за пределами курса обогащали преподователей новыми знаниями, а не они нас. Успехов вам!


                  1. LTVA Автор
                    04.01.2025 20:17

                    Но в этом случае, я думаю, будет проще измерить и учитывать эту дополнительную задержку dt

                    Вот с этим как раз сложности. В теории связка DMA+UART эту задержку если не устраняет, то хотя бы уменьшает и делает более предсказуемой.

                    но если вам не могут подсказать преподаватели как тут правильно поступать - использовать таймеры, это печально

                    Там с преподавателем вышла ситуация, я начал трассировать печатную плату как мезонину под его отладочную плату, а она на F303. И когда уже моя плата была отправлена на производство, я понял, что неплохо было бы добавить таймеры сброса фазы. Они заняли два оставшихся таймера на микроконтроллере. А потом оказалось, что хорошо бы иметь ещё таймеры сброса фазы, просто потому что. В результате пригодился третий таймер в демонстрационной музыке, ну а в тестах прекрасно работали все шесть, из которых четыре были сделаны как раз на UARTах. Если бы я заранее понял, к чему сойдётся функционал конечного устройства, то сразу бы трассировал под мою отладочную плату на H743, там бы таймеров как раз хватило с запасом. Преподаватель немного фалломорфировал от решения использовать UARTы ТАК, но в результате смирился, поскольку работает же.


            1. rukhi7
              04.01.2025 20:17

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

              если у вас уже есть этот таймер на 10 кГц он уже ЕСТ это процессорное время, один дополнительный IF это время особо не прибавит. (сколько тактов занимает у вас 10 кГц прерывание, и сколько тактов занимает не сработавший IF???)


              1. LTVA Автор
                04.01.2025 20:17

                У меня такого таймера нет. Но есть набор из n других таймеров, каждый из которых работает на частотах от 100 Гц до 1 кГц. И их частоты могут быть какими угодно в этом диапазоне. Так как речь о музыке и звуках, я должен очень точно выдерживать частоты. Пусть и будет небольшой джиттер, но средняя за время частота вызова определённой функции должна быть постоянна и выдерживаться настолько точно, что слушатель не заметит уплывания темпа (в случае с тикрейтом движка) и частоты (в случае с таймерами сброса фазы). В решении с одним таймером для обеспечения требуемого диапазона и точности задания частот нужно, навскидку, этот таймер запускать на частоте, которая хотя бы на порядок выше верхней границы, что для моего оптимистичного случая 10 кГц. И теперь представьте, кто кушает (в среднем) больше процессорного времени — шесть таймеров с прерываниями, каждое не чаще 1 кГц, или один таймер с прерываниями в 10 кГц и относительно сложным обработчиком, который как-то (например, имея шесть счётчиков и вызывая нужные функции по достижении каждым счётчиком определённого значения) в нужные срабатывания этого одного прерывания вызывает нужные функции. Я люблю в таком случае искать аппаратные решения вопроса, просто потому что вижу в этом некую красоту. Возможно, я бы обошёлся и без такого извращения, поставив один таймер на частое срабатывание.


                1. rukhi7
                  04.01.2025 20:17

                  Т.е. период будет равен не 1 мс, а 1 мс + некоторое время, около 100 нс — 1 мкс, пока микроконтроллер уходит в прерывание и кладёт новые данные.

                  ну как они какие угодно? они жене случайным образом меняются, я думаю можно придумать как их все таки использовать в зависимости от выбранных в данный момент параметров, а если нет ШИМ таймеры у вас тоже на произвольных частотах работают? В системе нет ничего детерменированного? не верю :)!


                  1. LTVA Автор
                    04.01.2025 20:17

                    Таймеры как раз хороши тем, что выставили сигнал прерывания, сбросили счётчик в ноль и им до лампочки, что там микроконтроллер тупит, они пошли следующий интервал считать :) То есть интервалы у него постоянные (с точностью до постоянства тактовой частоты, разумеется) Причём тут ШИМ вообще не понял, ШИМ в простейшем случае это тупо компаратор, на входе которого две шины — состояние счётчика таймера и значение регистра управления коэффициентом заполнения, и его выход a > b (или a < b, или a <= b, или a >= b, это уже детали) подключён к ножке. Это если сильно упрощать. Он же никак на работу самого таймера не влияет (опять же, в простейшем случае. Не знаю, что там STM накрутили в advanced control таймерах)


                    1. rukhi7
                      04.01.2025 20:17

                      Причём тут ШИМ вообще не понял, ШИМ в простейшем случае это тупо компаратор, на входе которого две шины — состояние счётчика таймера 

                      так ШИМ то тоже на таймере сделан, этому таймеру тоже можно прерывания включить наверно, он же с какой то постоянной частотой тоже долбит период ШИМ-а?

                      Для примера посмотрите как процессор работает у него вся переферия от примерно трех(максимум!) источников тактирования работает, не надо источники тактирования просто так размножать!


                      1. LTVA Автор
                        04.01.2025 20:17

                        Ну да. Я просто к тому, что таймер, опять же, молотит постоянно. А мой UART сам не крутится, его надо подпинывать постоянно, либо вручную, либо через DMA. В теории этого недостатка, кстати, можно избежать на тех микроконтроллерах, где у UART есть FIFO. Сделать прерывание по полупустому FIFO, например. UART из FIFO новое значение достаёт мгновенно (или же, по крайней мере, со строго определённой задержкой, на которую вышеописанные факторы не влияют). И всегда в FIFO есть запас, то есть пофиг, что проц немного потупит.


                1. rukhi7
                  04.01.2025 20:17

                  И теперь представьте, кто кушает (в среднем) больше процессорного времени — шесть таймеров с прерываниями, каждое не чаще 1 кГц, или один таймер с прерываниями в 10 кГц и относительно сложным обработчиком,

                  да это важный критерий, хорошо что вы это понимаете! Но это не единственный критерий который надо удовлетворять, инженерные решения это всегда компромис!

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


        1. nikolz
          04.01.2025 20:17

          Не понял. четыре ШИМ c независимыми частотами можно сделать на одном таймере. Можно и больше. Почему 4 таймера ?


          1. LTVA Автор
            04.01.2025 20:17

            Покажите или расскажите, как сделать четыре ШИМ-сигнала с полностью независимыми частотами и коэффициентами заполнения на каком-нибудь таймере в STM32F303. Я способа не нашёл. По блок-схемам счётчик там один, а вот ШИМ-контроллеров четыре и более. Но внутри ШИМ-контроллеров своих счётчиков нет, ну или я невнимательно смотрел. Я могу независимо управлять коэффициентом заполнения каждого канала, но частота у них будет общая.


            1. nikolz
              04.01.2025 20:17

              Возможно Вы правы. Но для более конкретного ответа надо знать требования к этим ШИМ генераторам.


              1. LTVA Автор
                04.01.2025 20:17

                Требования: настройка коэффициента заполнения от 0 до 100% с точностью не хуже 1%, диапазон частот от 10 Гц до пусть будет 4 кГц. При этом частота счёта счётчика, на базе которого ШИМ генерируется, не менее 500 кГц. Почему? Потому что послушайте сгенерированный на компьютере ШИМ сигнал 2 кГц с плавно меняющимся коэффициентом заполнения при сэмплрейте 44100 Гц. Услышите алиасинг. Для того, чтобы его уменьшить, нужно применить оверсэмплинг, желательно 16х. Так и приходим к вышеозвученной частоте. Таймеры микроконтроллера работают у меня на частоте 72 МГц, ну и остальным требованиям соответствуют. Единственный минус — на разных частотах разная сетка коэффициента заполнения (поскольку меняем значение в регистре ARR для смены частоты). Но на всех частотах она достаточно мелкая, чтобы выдерживалась точность в 1%.


                1. nikolz
                  04.01.2025 20:17

                  Если Вы используете ШИМ для формирования гармоник, то частота шим определяется частотой дискретизации сигнала и она у вас будет одинаковой для всех формируемых сигналов. более того, чтобы сформировать сумму четырех гармоник не надо 4 шим, достаточно одного. Аналогично и для более сложного сигнала. И возможно, что проще поставить чип цапа и не городить сумму гармоник как сумму сглаженных ШИМ сигналов


                  1. LTVA Автор
                    04.01.2025 20:17

                    Я не собираюсь никакие гармоники генерировать. Я говорил про восприятие ШИМ-сигнала на слух. Посмотрите верхние четыре канала здесь, чтобы понять, о чём я: https://youtu.be/L5HF_qdjZyU


                    1. nikolz
                      04.01.2025 20:17

                      Как я понял, это ваше:

                      https://github.com/LTVA1/STM32CrapSynth

                      Устройство способно синтезировать 8 аудиоканалов. Их сигналы суммируются, и полученная сумма выводится на стандартный аудиоразъём 3,5 мм.

                      Я про это и говорю. Вы создаете сигналы и суммируете из в аналоговой форме. А можно все сделать в цифре и на разъем выдать сигнал чипа ЦАП либо одного ШИМ.


                      1. LTVA Автор
                        04.01.2025 20:17

                        У меня курсовой проект называется «Устройство программно-аппаратного синтеза звука», если я правильно помню. Изначально приписки «программно-» вообще не было, она появилась по причине того, что можно получить новые тембры, если быстро переключать настройки канала. Но сам синтез строго аппаратный. Я пробовал сделать программный синтез: https://github.com/LTVA1/flizzer_tracker и https://youtu.be/dLWK_Au_vbM, однако там были проблемы с невозможностью добавить оверсэмплинг по причине ограниченных вычислительных мощностей. Без оверсэмплинга и ферритовых фильтров в проводе с аудиоджеком звучит не очень хорошо. Плюс преподаватель настаивал на проектировании какой-то достаточно сложной обвязки для полученич оценки «отлично», и простой аудиоджек явно на требуемую сложность не тянул. Поэтому решил попробовать с внешними DDS-генераторами, генератором шума и прочими аппаратными делами.


                      1. nikolz
                        04.01.2025 20:17

                        Понятно. Студент- это не профессия, а образ жизни.


                      1. LTVA Автор
                        04.01.2025 20:17

                        Если честно, всегда хотел такую штуку сделать. А тут как раз случай подвернулся. Вообще хотелось бы сделать свой звуковой чип, но это, понятное дело, невозможно из-за цены, плюс в ПЛИС и прочих смежных с этим делом вещах только начал разбираться.


      1. DrGluck07
        04.01.2025 20:17

        Если бы мы у себя в такое упёрлись, то взяли бы МК с большим количеством таймеров. Или подумали бы почему нам нужно так много таймеров и нет ли способа отказаться от этого. Как минимум рассмотреть зачем нам четыре таймера для ШИМ-ов, если на каждом таймере можно сделать четыре ШИМ-а на одной частоте.

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

        На STM32 (точнее на GD32, но не суть) мы даже используем SysTick для профилировки времени выполнения участков кода с точностью до десятых микросекунд. Потому что там 24-битный счётчик.


        1. LTVA Автор
          04.01.2025 20:17

           зачем нам четыре таймера для ШИМ-ов, если на каждом таймере можно сделать четыре ШИМ-а на одной частоте

          Четыре звуковых канала, и у каждого своя частота должна быть. Я бы тоже был рад повесить всё на один таймер, но нужна разная частота, причём точно настраиваемая (точность 1 Гц или выше)


          1. DrGluck07
            04.01.2025 20:17

            Значит рассмотреть другой контроллер, где этого будет хватать. Может быть взять не 48 пинов, а 64 или 80. Не, на самом деле сама по себе идея с UART-ом прикольная. Если уже всё сделано, но тут появилась необходимость добавить ещё одну фишку. Но если бы это проектировалось с нуля, то мы бы на такое использование UART не закладывались.


            1. LTVA Автор
              04.01.2025 20:17

              В курсовом проекте я на свою голову ограничил себя платой преподавателя, а там стоял STM32F303... Притом что у меня уже тогда валялась платка с H743 от WeAct. Знал бы, что придётся так извращаться — делал бы на H743.


              1. DrGluck07
                04.01.2025 20:17

                Тогда неплохо выкрутился


          1. nikolz
            04.01.2025 20:17

            зачем разные частоты дискретизации. В конечном сигнале у Вас частота дискретизации определяется по наивысшей частоте.

            Вам курс "Цифровая обработка сигналов" читали?


            1. LTVA Автор
              04.01.2025 20:17

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


              1. nikolz
                04.01.2025 20:17

                Главное, что "отлично" поставил пред. Но делать костыль из UART это что-то.


                1. LTVA Автор
                  04.01.2025 20:17

                  Ну работает же :)


                  1. nikolz
                    04.01.2025 20:17

                    Еще хотел заметить что у Вас есть DDS - а это уже аппаратный формирователь сложных сигналов и высокоточный ЦАП и с большой тактовой частотой. Зачем городить костыли на таймерах и суммировать ШИМ. Можно управлять DDS и делать сложный сигнал на его выходе


                    1. LTVA Автор
                      04.01.2025 20:17

                      Те DDS-генераторы, которые я использовал, не умеют генерировать прямоугольную волну с изменяемым коэффициентом заполнения. Умеют только меандр. И поскольку я по опыту изучения MOS Technology SID знаю, как изменяемый коэффициент заполнения может разнообразить звучание, я каждый из четырёх каналов сделал через аналоговый мультиплексор. Мультиплексор подключает на вход управляемого аттенюатора либо сигнал с DDS (треугольная волна или синус), либо сигнал с вывода микроконтроллера (ШИМ). После аттенюаторов сигналы суммируются аналогово на операционнике и подаются на джек


                      1. nikolz
                        04.01.2025 20:17

                        У DDS генераторов есть регистры приращения. Никто не запрещает изменять их динамически и тем самым изменять форму получаемого сигнала. Но это всего лишь идея возможного решения.

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


                      1. LTVA Автор
                        04.01.2025 20:17

                        Это уже будет не аппаратный синтез. Опять же, программный я уже делал. И я не понял, что за щелчки


    1. LTVA Автор
      04.01.2025 20:17

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


  1. juramehanik
    04.01.2025 20:17

    В целом хорошая абстрактная задача для собеса или на экзамене по мк


    1. LTVA Автор
      04.01.2025 20:17

      Для меня «абстрактная задача для собеса или экзамена» — выражение достаточно ругательное. Поскольку сам через такой экзамен проходил. Поэтому представил реальное применение описанного (ссылка в самом начале статьи).


  1. slog2
    04.01.2025 20:17

    Мне обычно UART-ов не хватает. А таймеров всегда избыток. Один таймер может и несколько задач выполнять.


    1. LTVA Автор
      04.01.2025 20:17

      Согласен. Но вот у меня конкретно в этом проекте получилось наоборот. Кустарный UART, по крайней мере, на передачу, можно сделать через GPIO+TIM+DMA. На небольших скоростях гарантированно будет работать передача. С приёмом сложнее, но можно через такую же схему с запуском в прерывании той же связки, только в обратную сторону, из GPIOx->IDR в оперативу, по заднему фронту (т.е. по стартовому биту). Конечно, на больших скоростях такая штука не будет успевать «разгребать» принятые данные. Но в теории можно извратиться.


  1. nikolz
    04.01.2025 20:17

    В своём курсовом проекте я столкнулся с нехваткой таймеров.

    Прикольно. А почему Вы не использовали программные таймеры? Их количество ограничено лишь объемом памяти.


    1. LTVA Автор
      04.01.2025 20:17

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


      1. nikolz
        04.01.2025 20:17

        Какие программные задержки внутри? Не надо ничего крутить. Это же практически счетчик тактовых импульсов и один условный оператор.


        1. LTVA Автор
          04.01.2025 20:17

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


          1. nikolz
            04.01.2025 20:17

            Относительно указанных вами требований к точности:

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

            у Вас они заданы некорректно. Понятие "частота" - число пересечений сигналом нулевого уровня в единицу времени и единица измерения Гц. А период это интервал между двумя пересечениями , и единица измерения секунда. Поэтому не может точность настройки периода измеряться в Гц.

            если вы полагаете, что интервал (период) 50 мкс , что соответствует периоду гармонического сигнала с частотой 20 кГц , должен задаваться с погрешностью 1/20000 , то это означает, что шаг формирования интервала должен быть 50/20000=2.5*10^-9, т е 2.5 nc. При этом у Вас стабильность частоты кварца в такой системе должна быль не хуже этих 2 ppm. У Вас в устройстве скорее всего стабильность кварца в 10 раз хуже.

            Т е ваши требования изначально не выполнимы и скорее всего не обоснованы.


            1. LTVA Автор
              04.01.2025 20:17

              Хорошо, возможно, требования слишком строгие. Можно уменьшить интервал частот, например до 100 Гц — 1 кГц. Если забить на реальную точность кварца, то всё равно получится, что прерывание-«диспетчер» придётся вызывать слишком часто. Я эту проблему решил путём использования аппаратных счётчиков, в том числе тех, которые не совсем для этого предназначены.


              1. nikolz
                04.01.2025 20:17

                Поясните, для чего вы используете ШИМ.


                1. LTVA Автор
                  04.01.2025 20:17

                  Вот для этого, внимание на четыре верхних канала: https://youtu.be/L5HF_qdjZyU


  1. Albert2009Zi
    04.01.2025 20:17

    В микроконтроллере STM32F303RE обнаружилось аж пять контроллеров UART. 

    Внезапно на первой странице даташита к этому контроллеру обнаружилось
    • Up to 14 timers:

    – One 32-bit timer and two 16-bit timers with up to four IC/OC/PWM or pulse counter and quadrature (incremental) encoder input

    – Three 16-bit 6-channel advanced-control timers, with up to six PWM channels, deadtime generation and emergency stop

    – One 16-bit timer with two IC/OCs, one OCN/PWM, deadtime generation and emergency stop

    – Two 16-bit timers with IC/OC/OCN/PWM, deadtime generation and emergency stop

    – Two watchdog timers (independent, window)

    – One SysTick timer: 24-bit downcounter

    – Two 16-bit basic timers to drive the DAC

    Ой сорри, всё увидел это:

    В своём курсовом проекте я столкнулся с нехваткой таймеров.


    1. LTVA Автор
      04.01.2025 20:17

      Здравствуйте, я выше уже пояснял, как я использовал все таймеры: https://habr.com/ru/articles/871564/comments/#comment_27744520. Также можете посмотреть исходники на гитхабе, ссылка на репозиторий в самом начале статьи.

      Да, смысл конкретно этой статьи был именно показать, как использовать. Вообще надо бы добавить, что лучший результат получится, если у UART есть FIFO, поскольку тогда период будет выдержан железобетонно (в статье и здесь же в комментариях описал, почему с обычным UARTом это может быть не так).

      Про ШИМ не очень понял. В курсовом проекте ШИМ генерируется на звуковых частотах...


      1. Albert2009Zi
        04.01.2025 20:17

        Вы успели прочитать мой неисправленный комментарий. Про ШИМ на старых МК. Допустим у PIC12F629 нет аппаратного ШИМ и нет UART, приходится генерировать ШИМ на таймере, которых там целых два. Берем из той же эпохи популярный PIC16F887, там и таймеров 3 и отдельный блок ШИМ и UART. Т.е. там где появился аппаратный UART, появился и аппаратный ШИМ.
        Естественно мои мысли из неотредактированного комментария были вне контекста вашей задачи. Я же написал - сорри, я всё увидел.
        Мне кажется легче уж тогда поискать МК под вашу задачу, где ресурсов (таймеров и модулей ШИМ и т.д.) будет достаточно. Моё личное мнение.


        1. LTVA Автор
          04.01.2025 20:17

          Да, и я в других ответах расписал, почему не получилось использовать H743