Псевдослучайные и случайные сигналы применяются для различных целей. В этой статье будет рассмотрено несколько способов их генерации на микроконтроллерах серии STM32.
Суть проблемы
Допустим, нужно построить АЧХ линии связи или компонента, подключённого к устройству. Или применить шум для электронной музыки (чиптюны?). Или для чего-то ещё.
Обычно для этого используют специализированные микросхемы или делают свои схемы на базе стабилитронов или биполярных транзисторов. Однако, если амплитуды сигнала в 3.3 Вольта (зависит от напряжения питания микроконтроллера) и малой нагрузочной способности вывода микроконтроллера достаточно, то можно обойтись без дополнительных компонентов.
В статье будет рассмотрено пять методов генерации случайных и псевдослучайных сигналов на выводе микроконтроллера. Четыре из них аппаратные, один — программный. Первые четыре способа будут продемонстрированы на микроконтроллере STM32F303RE, последний — на STM32H743VI.
Способ №1 — использование встроенного ЦАП
Во многих микроконтроллерах серии STM32 есть как минимум один двухканальный ЦАП. Помимо простого преобразования данных в напряжение на выводе, он умеет генерировать треугольную волну или псевдослучайный сигнал.
Псевдослучайный сигнал генерируется при помощи 12-разрядного РСЛОС (регистра сдвига с линейной обратной связью):
Таким образом, максимальный период псевдослучайной последовательности равен 4095 (212-1). Поэтому на слух такой шум будет восприниматься повторяющимся, а при высокой частоте тактирования регистра появятся тональные составляющие.
Итак, приступим к программированию. Для начала приведу код функции, настраивающей тактирование от внешнего кварца на 8 МГц. Она будет применяться во всех примерах, кроме последнего:
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();
}
Этот код оставлю без дополнительных пояснений, поскольку к теме статьи он не относится.
Теперь перейдём к настройке периферии. Для начала необходимо включить тактирование. Нам понадобится тактирование порта A GPIO, поскольку PA4 и PA5 — единственные выводы, которые можно превратить в выходы ЦАПов. Также нам потребуется таймер для тактирования ЦАПа, в качестве него используем TIM6, который, согласно reference manual, как раз и является одним из двух таймеров, предназначенных для тактирования каналов ЦАПа. Ну и, конечно же, нужно включить тактирование самого ЦАПа.
void init_clocks()
{
RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
RCC->APB1ENR |= RCC_APB1ENR_DAC1EN | RCC_APB1ENR_TIM6EN;
}
После включения тактирования напишем функцию main:
#include "stm32f3xx.h"
int main(void)
{
init_clocks();
set_72MHz();
//настройка периферии здесь
//ожидание в бесконечном цикле
while (1) { asm("nop"); }
return 0;
}
Обратите внимание на порядок. Для того, чтобы тактирование периферии действительно стало нормально работать, необходимо подождать несколько тактов шины, на которой расположена периферия. Поэтому сначала включается тактирование, а потом уже настраивается его источник. За время настройки эти несколько тактов гарантированно пройдут.
Теперь настроим периферию:
//аналоговая функция вывода PA4
GPIOA->MODER |= GPIO_MODER_MODER4;
Здесь мы выставили оба бита поля, отвечающего за режим вывода PA4, тем самым переведя его в аналоговую функцию.
Теперь настроим таймер:
//генерируем событие TRGO по событию переполнения счётчика
TIM6->CR2 |= TIM_CR2_MMS_1;
TIM6->ARR = 72 - 1;
TIM6->PSC = 2 - 1;
Таймер будет выдавать сигнал TRGO при каждом переполнении, т.е. каждые 144 такта системной частоты (поскольку предделитель и сам счётчик таймера считают с нуля). Таким образом, частота тактирования ЦАПа составит 500 кГц.
Теперь самое интересное — настроим ЦАП:
//Настраиваем ЦАП: максимальная амплитуда шума, выбор шума в
//качестве типа генерируемой волны, включаем trigger
//DAC_CR_TSEL1 = 0 для TIM6 в качестве источника тактирования
//генератора шума
DAC->CR = DAC_CR_MAMP1 | DAC_CR_WAVE1_0 | DAC_CR_TEN1;
DAC_CR_MAMP1
означает, что выставлена максимальная амплитуда сигнала. Записав другие значения, например, DAC_CR_MAMP1_0
, мы сможем контролировать амплитуду сигнала. Однако эта настройка довольно грубая — можно выбрать либо полную амплитуду, либо половину, либо четверть, и т.д. до 1/4096 максимальной амплитуды. DAC_CR_WAVE1_0
выбирает шум в качестве генерируемой волны:
Теперь всё готово, осталось только включить ЦАП и таймер:
//включаем первый канал ЦАПа
DAC->CR |= DAC_CR_EN1;
//включаем таймер
TIM6->CR1 |= TIM_CR1_CEN;
После этого на выводе PA4 будет постоянно выводиться «шум».
За неимением лучшего инструмента записал сигнал при помощи встроенной звуковой карты. В Audacity построил амплитудный спектр:
Эти пики есть не что иное, как гармоники основной частоты, приблизительно равной 122 Гц. Откуда взялась эта частота? А взялась она из малого периода последовательности, которая генерируется сдвиговым регистром. Действительно,
Мне не очень понятно, зачем STM сделали такой короткий сдвиговый регистр. Ведь даже при вдвое большей длине регистра период последовательности стал бы равен уже примерно 16 млн, что давало бы равномерный спектр, крайне близкий к белому шуму.
Достоинством данного метода является использование всего одного вывода (остальные выводы порта могут быть сконфигурированы любым образом) и возможность регулировки амплитуды сигнала. Недостатком является неравномерность спектра (хотя, если понизить частоту тактирования, то полоса относительной равномерности амплитудного спектра станет шире).
Способ №2 — использование модуля CRC
В некоторых микроконтроллерах STM32 реализован модуль расчёта CRC. По сути своей это всего лишь 32-разрядный сдвиговый регистр с «подмешиванием» через XOR в цепь обратной связи проверяемых данных. Если постоянно слать в качестве этих данных нули, то такой сдвиговый регистр превратится в РСЛОС (см. таблицу истинности XOR'а). После этого остаётся всего лишь подобрать нужный полином для получения последовательности максимальной длины.
Для того чтобы воплотить эту задумку, потребуется использовать сам модуль расчёта CRC и два канала DMA. Один канал будет постоянно класть в модуль CRC нули, тем самым заставляя РСЛОС сдвигаться на один бит при каждой записи, а второй будет «доставать» из модуля посчитанную «CRC» (по сути — текущее состояние РСЛОС) и класть её в регистр ODR (output data register) порта GPIO. В результате теряется возможность использовать все выводы этого порта GPIO в режиме именно выводов GPIO (т.е. их всё ещё можно будет использовать как входы, а также в аналоговом режиме и в режиме альтернативной функции).
Стоит отметить, что этот способ и способ №3 не работает для микроконтроллеров семейств G0 и L0, поскольку там GPIO не находятся на общей шине. Они подключены к процессорному ядру по отдельной шине, из-за чего DMA к регистрам GPIO доступа не имеет.
Настроим тактирование:
void init_clocks()
{
RCC->AHBENR |= RCC_AHBENR_GPIOAEN | RCC_AHBENR_CRCEN | RCC_AHBENR_DMA2EN;
RCC->APB1ENR |= RCC_APB1ENR_TIM6EN | RCC_APB1ENR_TIM7EN;
}
Функция main остаётся такой же. Конечно, меняется сама настройка периферии:
//настройка вывода PA4 на выход
GPIOA->MODER &= ~GPIO_MODER_MODER4;
GPIOA->MODER |= GPIO_MODER_MODER4_0;
Здесь мы настроили всё тот же вывод PA4 на выход.
Теперь настроим таймеры. Используем таймеры TIM6 и TIM7:
//генерируем событие TRGO по событию переполнения счётчика
TIM6->CR2 |= TIM_CR2_MMS_1;
TIM6->DIER = TIM_DIER_UDE;
TIM6->ARR = 10 - 1;
TIM6->PSC = 0;
//генерируем событие TRGO по событию переполнения счётчика
TIM7->CR2 |= TIM_CR2_MMS_1;
TIM7->DIER = TIM_DIER_UDE;
TIM7->ARR = 10 - 1;
TIM7->PSC = 0;
TIM6->DIER = TIM_DIER_UDE;
включает генерацию события TIM6_UP, аналогично для TIM7_UP. Эти события нужны для инициирования пересылок контроллера DMA:
Теперь настроим сам модуль CRC:
//сброс
CRC->CR = CRC_CR_RESET;
//полином 32 бита, без инвертирования данных
CRC->CR = 0;
//пишем ненулевое исходное значение
CRC->INIT = 0xAAAAAAAA;
//полином, РСЛОС 32 бита, максимальный период
CRC->POL = 0x000000C5;
Полином здесь «перевёрнут» относительно «традиционного» варианта расположения отводов РСЛОС, поскольку сдвиг происходит вправо, а не влево.
Осталось настроить DMA:
//пересылать по 32 бита в режиме циклического буфера, из памяти в периферию,
//длина одного цикла равна одной посылке, без прерываний. Пересылка по
//сигналу от TIM6
DMA2_Channel3->CCR = DMA_CCR_PSIZE_1 | DMA_CCR_MSIZE_1 | DMA_CCR_CIRC | DMA_CCR_DIR;
DMA2_Channel3->CNDTR = 1;
DMA2_Channel3->CMAR = (uint32_t)&zero_var;
DMA2_Channel3->CPAR = (uint32_t)&(CRC->DR);
//то же самое, но из "периферии" в "память" (из регистра расчёта CRC CRC->DR
//в регистр GPIOA->ODR). Пересылка по сигналу от TIM7
DMA2_Channel4->CCR = DMA_CCR_PSIZE_1 | DMA_CCR_MSIZE_1 | DMA_CCR_CIRC;
DMA2_Channel4->CNDTR = 1;
DMA2_Channel4->CMAR = (uint32_t)&(GPIOA->ODR);
DMA2_Channel4->CPAR = (uint32_t)&(CRC->DR);
Третий канал по каждому сигналу от TIM6 будет постоянно перекладывать значение переменной zero_var
в CRC->DR
(регистр входных данных модуля CRC). Четвёртый канал будет работать по сигналу от TIM7 и перекладывать значение из CRC->DR
(который при чтении возращает текущее состояние нашего «РСЛОС») в GPIOA->ODR
.
Таким образом, на выходе будет длинная (примерно 232) псевдослучайная последовательность, сгенерированная этим кустарным РСЛОС. Осталось всё это в правильной последовательности включить:
//включаем каналы DMA
DMA2_Channel3->CCR |= DMA_CCR_EN;
DMA2_Channel4->CCR |= DMA_CCR_EN;
//включаем таймеры
TIM6->CR1 |= TIM_CR1_CEN;
TIM7->CR1 |= TIM_CR1_CEN;
Теперь «шум», если слушать его через звуковую карту или наушники, поубавит в амплитуде по сравнению с предыдущим способом (поскольку высокие частоты устройство воспроизведения и провода «обрубят»), но исчезнет тональная составляющая. CRC тактируется на частоте 7.2 МГц, то есть более чем в 14 раз быстрее, чем тактировался ЦАП в предыдущем примере, однако не слышно никаких повторений этого «шума».
Вновь тем же способом построил амплитудный спектр:
Спектр идеально равномерный, по крайней мере, в доступном для записи диапазоне. Такой сигнал гораздо ближе к настоящему белому шуму, и он может быть использован везде, где требуется такая равномерность спектра.
Достоинство этого метода очевидно — мы получаем почти настоящий белый шум, не используя процессорное ядро или какие-то внешние компоненты и схемы. Однако теряется весь порт GPIO, в который мы шлём данные. Другие выводы того же порта нельзя будет настроить на выход и «вручную» задавать на них сигналы. Однако их всё ещё можно будет использовать как цифровые входы, входы АЦП или для SPI/UART/I2C/etc.
Заметка на полях: если в полиноме оставить включённым только один бит (с младшим не работает, надо второй после младшего включать), то положенные в CRC->INIT
данные будут бегать в этом сдвиговом регистре по кругу! Не знаю, зачем такое может пригодиться, но тесты показали, что оно так действительно работает.
Способ №3 — использование АЦП
Практически все STM32 имеют хотя бы один АЦП. Если настроить малое время зарядки внутренней ёмкости этого АЦП и заставить его мерить какой-нибудь канал (например, температурный датчик), то младший бит измеренного значения будет гарантированно «дрожать», то есть случайным образом становиться то нулём, то единицей. Если такой АЦП заставить работать от таймера и настроить его на подачу сигналов контроллеру DMA, то можно, как и в прошлом способе, просто постоянно пересылать измеренное значение в регистр ODR какого-либо порта GPIO, и на нулевом выводе порта будет формироваться случайный сигнал.
Сразу хочу сказать, что с АЦП существуют некоторые тонкости. Я заметил различия между АЦП в микроконтроллерах серии F0 и F3. В каком-нибудь H7 вообще может быть что-то третье. Поэтому сразу оговорюсь, что АЦП просто настроен на однократные измерения одного и того же канала по сигналу от таймера и подачу контроллеру DMA сигнала при каждом преобразовании. Конкретные настройки регистров могут быть другими в других семействах микроконтроллеров.
Настраиваем тактирование:
void init_clocks()
{
RCC->AHBENR |= RCC_AHBENR_GPIOAEN | RCC_AHBENR_DMA1EN | RCC_AHBENR_ADC12EN;
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
}
Для запуска преобразований АЦП будем использовать TIM2. Выводить сигнал теперь будем на ножку A0:
//настройка вывода PA0 на выход
GPIOA->MODER &= ~GPIO_MODER_MODER0;
GPIOA->MODER |= GPIO_MODER_MODER0_0;
Так же, как и для ЦАПа в первом примере, настраиваем TIM2:
//настройка TIM2: TRGO по переполнению счётчика
TIM2->CR2 = TIM_CR2_MMS_1;
TIM2->ARR = 72;
TIM2->PSC = 0;
Преобразования будут происходить с частотой 1 МГц.
Теперь самая неочевидная часть: настройка АЦП. Он в STM32F303 довольно странно взаимодействует с DMA — для того, чтобы DMA работал в циклическом режиме (circular mode), необходимо этот режим включить и в регистре настройки АЦП, и в регистре настроек канала DMA! ADC_CFGR_DMACFG
задаёт работу в circular mode.
//настройка АЦП: DMA в циклическом режиме, разрешение 12 бит, преобразование
//по сигналу от TIM2_TRGO, включаем и выбираем внутренний температурный датчик (16-ый канал).
//Discontinuous mode для работы с DMA. Работаем только с одним каналом (температурный датчик).
//Запуск преобразования по сигналу от TIM2
ADC1->CFGR = ADC_CFGR_DMACFG | ADC_CFGR_DMAEN | ADC_CFGR_DISCEN | ADC_CFGR_EXTEN_0 |
ADC_CFGR_EXTSEL_3 | ADC_CFGR_EXTSEL_1 | ADC_CFGR_EXTSEL_0 | ADC_CFGR_DISCEN;
ADC1->SQR1 = ADC_SQR1_SQ1_4;
ADC1_2_COMMON->CCR = ADC12_CCR_TSEN | ADC12_CCR_CKMODE_1;
ADC_CFGR_DISCEN
включает режим преобразования группы каналов по сигналу (в нашем случае сигнал от таймера, и канал всего один). ADC_CFGR_EXTEN_0
задаёт начало преобразования по переднему фронту сигнала. Биты ADC_CFGR_EXTSEL
задают в качестве сигнала начала преобразования TIM2_TRGO:
ADC_SQR1_SQ1_4
выбирает 16-ый канал (внутренний температурный датчик) в качестве первого канала в последовательности преобразований (regular sequence). По умолчанию длина последовательности равна единице, то есть каждый раз по сигналу от таймера будет производиться преобразование только для этого канала.
Теперь перейдём к регистрам, общим для первого и второго АЦП (ADC1_2_COMMON
). Здесь нас интересуют две вещи — включение внутреннего температурного датчика (ADC12_CCR_TSEN
) и настройка тактирования АЦП (ADC12_CCR_CKMODE_1
задаёт тактирование с частотой, равной системной частоте, т.е. максимально возможную скорость работы АЦП). Вообще, благодаря этому регистру, можно настроить совместную работу первого и второго АЦП, по факту объединив их в «один» АЦП с вдвое большей частотой сэмплирования, но мы сейчас не об этом...
После настройки АЦП можно настроить DMA. Смотрим, какой канал может обслуживать запросы АЦП:
И настраиваем его на циклическую пересылку из регистра данных АЦП во всё тот же GPIOA->ODR
:
//настройка DMA: по 16 бит, циклический режим, без инкремента адресов, из ADC1->DR в GPIOA->ODR
DMA1_Channel1->CCR = DMA_CCR_PSIZE_0 | DMA_CCR_MSIZE_0 | DMA_CCR_CIRC;
DMA1_Channel1->CPAR = (uint32_t)&(ADC1->DR);
DMA1_Channel1->CMAR = (uint32_t)&(GPIOA->ODR);
DMA1_Channel1->CNDTR = 1;
Теперь главное — всё это включить в правильном порядке. Сначала DMA, потом АЦП (включить сам АЦП и потом запустить преобразования), а потом уже таймер!
//включаем DMA
DMA1_Channel1->CCR |= DMA_CCR_EN;
//включаем АЦП
ADC1->CR |= ADC_CR_ADEN;
ADC1->CR |= ADC_CR_ADSTART;
//включаем таймер
TIM2->CR1 |= TIM_CR1_CEN;
Получаем на выходе шум, примерно такой же, как и в способе с CRC. Спектр такой же. Однако в этот раз шум даже можно назвать относительно настоящим, по крайней мере, в определённой полосе. Я не силён в статистиках и терверах, поэтому АКФ строить не буду и доказывать, действительно ли поток битов в младшем разряде АЦП случайный и недетерминированный, не хочу. «На слух» (и на спектр) оно похоже на шум. Дальше уже решайте сами, подойдёт ли такой способ.
Достоинства и недостатки те же, что и в способе с CRC. Естественно, если нужно мерить температуру правильно, то лучше для генерации шума выбрать другой канал АЦП. Я выбрал температурный датчик, поскольку про него отдельно написаны рекомендуемые времена зарядки внутренней ёмкости АЦП (что косвенно означает сильную зашумлённость потока данных при несоблюдении этого времени...).
Способ №4 — прерывание от таймера, программный РСЛОС
Данный способ добавлен в основном для контроллеров серии L0 и G0, в которых DMA не может класть данные в регистр GPIO. Способ до ужаса прост: настраиваем периодические прерывания от таймера, и в обработчике прерывания обсчитываем один такт программно реализованного РСЛОС. После чего пишем в регистр BSRR, переставляя сигнал на выводе в 0 или в 1 в соответствии с, например, состоянием младшего бита нашего РСЛОС.
Это единственный в этой подборке «программный» способ, т.е. он требует постоянного участия процессорного ядра при генерации сигнала.
Настраиваем тактирование:
void init_clocks()
{
RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
}
Теперь объявляем глобальные переменные и маску битов-отводов РСЛОС:
//маска битов, которые являются отводами РСЛОС
//https://docs.amd.com/v/u/en-US/xapp052 для 32 бит
//биты в обратном порядке, поскольку РСЛОС сдвигается
//в обратном направлении
#define LFSR_MASK (1 | (1 << 10) | (1 << 30) | (1 << 31))
//переменная, которая хранит состояние РСЛОС
uint32_t lfsr;
//переменная, которая хранит состояние
//младшего бита РСЛОС до сдвига
uint32_t feedback;
Объявляем обработчик прерывания. Название функции берём из startup-файла:
//обработчик прерывания TIM2
void TIM2_IRQHandler()
{
//если выставлен флаг прерывания по переполнению счётчика
if(TIM2->SR & TIM_SR_UIF)
{
//то сбрасываем флаг
TIM2->SR &= ~TIM_SR_UIF;
//сохраняем младший бит
feedback = lfsr & 1;
//сдвиг содержимого РСЛОС
lfsr >>= 1;
//если младший бит равен единице
if(feedback)
{
//XOR с маской битов отводов
//все биты, кроме битов отводов,
//не меняются.
//Биты отводов "переворачиваются"
lfsr ^= LFSR_MASK;
}
//записываем в регистр BSRR бит в зависимости от состояния младшего бита РСЛОС
//*(volatile uint32_t*)... по причине того, что в версии CMSIS под этот
//микроконтроллер регистр BSRR объявлен как два 16-битных поля, в то время
//как обращаться к нему удобнее как к 32-битному регистру.
//пишем для вывода PA0
*(volatile uint32_t*)&(GPIOA->BSRRL) = feedback ? 1 : (1 << 16);
}
}
Обратите внимание, что исполнение одного шага работы РСЛОС занимает фиксированное число тактов (не зависит от числа и расположения битов-отводов).
Теперь настроим в функции main вывод PA0:
//настройка вывода PA0 на выход
GPIOA->MODER &= ~GPIO_MODER_MODER0;
GPIOA->MODER |= GPIO_MODER_MODER0_0;
И таймер:
//настройка TIM2: прерывание по переполнению счётчика
TIM2->DIER = TIM_DIER_UIE;
TIM2->ARR = 72 - 1;
TIM2->PSC = 10 - 1;
Не забудем записать ненулевое число в переменную РСЛОС, иначе РСЛОС останется в нулевом состоянии навечно:
//записываем ненулевое число в переменную, хранящую состояние РСЛОС
lfsr = 222;
Включаем прерывание и запускаем таймер:
//включаем прерывание
NVIC_EnableIRQ(TIM2_IRQn);
//запуск таймера
TIM2->CR1 |= TIM_CR1_CEN;
После этого прерывание будет вызываться с частотой 720 кГц. Это довольно часто (на каждое прерывание приходится всего 100 тактов процессора), особенно если вспомнить, что на такой частоте при доступе к флэш-памяти нужна задержка в два такта.
Шум теперь опять ненастоящий, если строго говорить. Но его период опять составляет примерно 232, то есть спектр равномерный.
Достоинство метода в том, что мы пишем в регистр атомарного доступа к выводу, т.е. остальные выводы порта можно использовать как угодно. Недостаток же в том, что метод программный. На представленной частоте ядро как минимум половину времени проводит в прерывании, что не есть хорошо. При этом чем ниже частота исполнения прерывания, тем больше будет в спектре шума неравномерностей.
Способ №5 — использование генератора случайных чисел
В «старших» семействах STM32 присутствует настоящий генератор случайных чисел (RNG, random number generator), который успешно проходит страшные статистические тесты, т.е. числа у него гарантированно случайные. Осталось применить всё ту же отработанную схему — натравить DMA на пересылку данных из регистра RNG в тот же GPIOA->ODR
. Главное, чтобы RNG успевал генерировать новые числа.
Из reference manual'а можно узнать, с какой скоростью RNG выдаёт новые случайные числа:
Поскольку настройка тактирования на STM32H743 довольно сложна, оставим тактирование от внутреннего генератора на 64 МГц. Тогда получится, что RNG выдаёт новые случайные числа с частотой примерно 1.2 МГц.
У RNG его основная часть тактируется от отдельного источника. Посмотрим, какой источник можно выбрать:
По умолчанию выбран внутренний генератор на 48 МГц. Остальные варианты требуют более сложной настройки или слишком медленные, поэтому остановимся на нём.
В функцию включения тактирования добавим включение и ожидание готовности этого генератора:
void init_clocks()
{
//включаем HSI48, внутренний генератор 48 МГц, от которого
//по умолчанию тактируется генератор
//случайных чисел
RCC->CR |= RCC_CR_HSI48ON;
//ждём готовности HSI48
while(!(RCC->CR & RCC_CR_HSI48RDY)) { asm("nop"); }
После этого включаем тактирование RNG, DMA, порта GPIO и таймера:
RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN;
RCC->AHB2ENR |= RCC_AHB2ENR_RNGEN;
RCC->AHB4ENR |= RCC_AHB4ENR_GPIOAEN;
RCC->APB1LENR |= RCC_APB1LENR_TIM6EN;
Делаем сброс RNG:
//сброс генератора случайных чисел
RCC->AHB2RSTR |= RCC_AHB2RSTR_RNGRST;
RCC->AHB2RSTR &= ~RCC_AHB2RSTR_RNGRST;
И ждём несколько тактов, чтобы тактирование периферии действительно включилось:
//ждём несколько тактов, чтобы для периферии
//правильно включилось тактирование
asm("nop");
asm("nop");
asm("nop");
}
В функции main, очевидно, уберём вызов set_72MHz()
.
Теперь в ней же займёмся настройкой периферии. Поскольку первый набор случайных чисел RNG генерирует дольше, чем последующие, имеет смысл его настроить и включить в самом начале:
//включение генератора случайных чисел
//заранее, поскольку время генерации первого
//набора случайных чисел выше, чем время
//генерации следующих наборов
RNG->CR |= RNG_CR_RNGEN;
Отключение обнаружения ошибок тактирования не требуется согласно этой строчке в reference manual'е:
Теперь настраиваем PA0 на выход, а TIM6 на генерацию события TIM6_UP (для инициирования пересылок DMA):
//настройка вывода PA0 на выход
GPIOA->MODER &= ~GPIO_MODER_MODER0;
GPIOA->MODER |= GPIO_MODER_MODER0_0;
//настройка таймера
//генерируем событие TRGO по событию переполнения счётчика
TIM6->CR2 |= TIM_CR2_MMS_1;
TIM6->DIER = TIM_DIER_UDE;
TIM6->ARR = 64 - 1;
TIM6->PSC = 10 - 1;
Частота пересылок DMA будет равна 640 кГц, что примерно вдвое меньше частоты генерации случайных чисел, т.е. RNG гарантированно будет успевать генерировать новые числа.
Теперь нужно настроить DMA. В старших семействах STM32 каналы DMA называются потоками (streams) и имеют расширенный функционал. При этом настройка не сильно отличается от того, что мы делали на F303. По умолчанию двойная буферизация и прочие навороты выключены, поэтому нужно просто записать адреса, указать размер данных и включить циклический режим:
//настраиваем DMA:
//нулевой канал, пересылать по 32 бита в режиме циклического буфера,
//из "периферии" в "память", циклический режим по одной посылке, без
//прерываний, пересылаем из RNG->DR в GPIOA->ODR
DMA1_Stream0->CR = DMA_SxCR_CIRC | DMA_SxCR_MSIZE_1 |
DMA_SxCR_PSIZE_1;
DMA1_Stream0->PAR = (uint32_t)&RNG->DR;
DMA1_Stream0->M0AR = (uint32_t)&GPIOA->ODR;
DMA1_Stream0->NDTR = 1;
Регистр M0AR
используется по умолчанию для адреса «памяти». По умолчанию идёт пересылка из «периферии» в «память».
Это ещё не всё. Обратите внимание, что я выбрал первый канал первого контроллера DMA. В STM32H7 можно настроить, какой канал DMA работает по какому сигналу. Для этого используется периферия DMAMUX.
Тактирование DMAMUX не нужно включать отдельно, поскольку тактирование этой периферии включается при включении тактирования DMA.
Поскольку у нас довольно простая задача — просто подключить сигнал к определённому каналу DMA — то и решение тоже простое. Открываем reference manual и смотрим регистр конфигурации:
Нам нужно всего лишь записать нужное число в поле DMAREQ_ID
. Смотрим в таблицу:
Теперь, когда мы знаем номер сигнала, можно завершить настройку:
//настраиваем DMAMUX:
//нулевой канал по запросу от TIM6
//69 - TIM6_UP
DMAMUX1_Channel0->CCR = 69;
Всё настроено. Осталось только включить таймер и канал DMA:
//включаем DMA
DMA1_Stream0->CR |= DMA_SxCR_EN;
//включаем таймер
TIM6->CR1 |= TIM_CR1_CEN;
На выводе PA0 появится шум. Это настоящий случайный сигнал, поскольку данные генерируются настоящим (true) RNG. Спектр тот же, что и во втором, третьем и четвёртом способах.
Достоинство способа в получении настоящих случайных чисел, а следовательно настоящего случайного сигнала на выводе. Недостатки всё те же, что и у других способов с использованием DMA: весь порт GPIO (точнее, все выводы порта, сконфигурированные на выход в режиме GPIO) будут выдавать случайный сигнал. Ещё в качестве недостатка можно привести использование самого RNG — вдруг он нужен для чего-то более важного и полезного в конкретной прошивке.
Заключение
Если честно, я ни разу не видел реальной задачи, в которой пригодилась бы возможность генерировать такие сигналы на выводах микроконтроллера. Однако в теории это поможет упростить устройство, в котором могут понадобиться такие сигналы. Поскольку четыре из пяти продемонстрированных способов аппаратные, можно один раз включить генерацию шума и «забыть». Хотя, конечно, это может вносить небольшие задержки из-за того, что для пересылок DMA используется общая шина, а это может несколько замедлять процессорное ядро или другие каналы DMA.
В любом случае, реализовывать такой шум средствами самого микроконтроллера или использовать отдельную схему или устройство — решать Вам.
Весь исходный код доступен на GitHub.
Комментарии (5)
S1onnach
02.01.2025 16:18RNG единственно правильный путь. Жаль чипы с ним ни один заказчик тебе в руки не даст, поскольку дорогие они. Так что,.. ладно хоть (псевдо) в название вставили.
LTVA Автор
02.01.2025 16:18Опять же, АЦП в теории тоже случайные должен давать в младшем бите. Но проверить их случайность оборудования и знаний не хватает, поэтому написал аккуратно.
ABATAPA
А Хабр-то ещё торт...