Простой цифровой радиоприемник на базе контроллера STM32G4 своими руками

image-loader.svg

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

С выходом в серию семейства STM32G я наконец не выдержал и решил попробовать реализовать одну давнюю идею:  собрать простое радиоприемное устройство диапазона LW/MW с минимальным количеством внешних электронных компонентов. Конструкция и программное обеспечение должны быть достаточно простыми,  чтобы можно было на пальцах объяснить новичку основы цифровой обработки сигналов, не углубляясь в теорию.

Для реализации проекта была выбрана доступная демонстрационная плата NUCLEO-G431KB, содержащая контроллер STM32G431KBT6 (flash 128KB, ram 32KB,  тактовая частота процессора до 170 MHz),  минимально необходимую обвязку и интегрированный отладчик/программатор STLINK-V3E. Особенно ценным является наличие в контроллере 12 разрядного аналого-цифрового преобразователя ADC  с частотой дискретизации до 4 MHz, что теоретически позволяет обрабатывать сигналы с частотами до 2 MHz.

Излагать материал я буду от простого к сложному в виде описания последовательных экспериментов, постепенно усложняя программное обеспечение. Компиляция программ выполнялась в Windows 10 с использованием бесплатной интегрированной среды разработки STM32CubeIDE от компании STMicrielectronics (https://www.st.com/en/development-tools/stm32cubeide.html). Исходные коды экспериментальных проектов находятся по адресу https://github.com/OlegDyakov/simple-radio.

Для разработки программ использовались следующие официальные документы от компании STMicrielectronics:

  • «RM0440, Reference manual STM32G4 Series advanced Arm ® -based 32-bit MCUs» — подробное описание функциональных блоков контроллеров серии STM32G4.

  •  «DS12589 Rev 2 — Datasheet STM32G431×6, STM32G431×8, STM32G431xB» — описание контроллеров линейки STM32G431×6, STM32G431×8,  STM32G431xB.

  • «PM0214 Rev 10 — STM32 Cortex ® -M4 MCUs and MPUs programming manual» — руководство программиста.

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

Рисунок 1. Схема исследовательского стенда NUCLEO-G431KBРисунок 1. Схема исследовательского стенда NUCLEO-G431KB

1. Структура радиоприемника

Рисунок 2. Структурная схема радиоприемникаРисунок 2. Структурная схема радиоприемника

Радиоприемник построен по классической схеме приемника прямого усиления и предназначен для приема АМ радиостанций, вещающих в диапазонах длинных и средних волн (LW и MW). Сигнал с ферритовой магнитной антенны поступает на усилитель высокой частоты (УВЧ). Далее сигнал оцифровывается в аналого-цифровом преобразователе (ADC) и подается на перестраиваемый цифровой узкополосной полосовой фильтр, обеспечивающий настройку на нужную несущую частоту радиостанции. Выделенный сигнал передается на АМ демодулятор, реализованный по принципу диодного детектора.  Для уменьшения эффекта «замирания сигнала» после демодулятора стоит система автоматической регулировки усиления (АРУ). Полученный цифровой звуковой сигнал поступает на цифро-аналоговый преобразователь (DAC), далее — на усилитель низкой частоты (УНЧ) и динамик (наушники).

2. Базовый проект

Для разработки программного обеспечения для контроллеров STM32 существует большое количество очень хороших готовых профессиональных программных пакетов. К сожалению, новичкам достаточно сложно сразу осмыслить сложный мир профессиональных средств разработки. К тому же  стандартные библиотеки (CMSIS,  HAL) закрывают от программиста механизмы работы с функциональными модулями контроллера.

У разработчика имеется довольно подробное описание функциональных модулей контроллера, например:  «RM0440 Reference manual STM32G4 Series advanced Arm ® -based 32-bit MCUs», содержащее более 2000 страниц. Как правило,  программисты применяют стандартные библиотеки от производителей контроллера, которые, мягко говоря, довольно сильно изолируют разработчика от управления аппаратными блоками. Если что-то работает не так, или нам нужно «выжать» из железки максимум, приходиться «лезть» в код библиотеки, который написан на профессиональном C, предназначен для обеспечения переносимости на разные контроллеры и системы разработки и довольно сложен для понимания людям, которые делают первые шаги в embedded программировании.   Хотелось быть «поближе к транзисторам», поэтому был разработан простой базовый проект на C, который является основой для реализации всех экспериментов, описанных в данной статье.

Экспериментальные проекты носят исследовательский характер и не претендуют на демонстрацию профессиональных подходов при разработке встроенного программного обеспечения.

Рассмотрим простейшую программу, написанную на языке С, которая выполняет переключение светодиода LD2 и логических уровней на выводе CN4–7 (PB7) демонстрационной платы NUCLEO-G431KB. Вывод PB7 мы будем использовать для измерения с помощью осциллографа времени выполнения различных функций в экспериментальных проектах.  Проект простейшей программы находится в папке 01_Minimal.

Проект состоит из следующих файлов:

system_init.c

инициализация регистров контроллера и оперативной памяти при старте,

vectors.c

таблица прерываний,

gpio.c

настройка ножек контроллера,

main.c

основной цикл программы,

stm32g431.h

описание регистров контроллера, используемых в проекте,

stm32f4.ld

скрипт программы компоновщика.

Начнем анализ проекта с файла скрипта компоновщика «stm32f4.ld». Этот файл предназначен для предоставления информации компоновщику,  каким образом организована основная память контроллера (FLASH,  RAM), как необходимо размещать секции (области данных, с которыми работает компилятор С) в физической памяти контроллера, а также какую функцию программы необходимо вызывать сразу после рестарта.

Компилятор языка С размещает различные типы данных в разных стандартных секциях памяти:

 .text — исполняемый код программы,

 .rodata —  константы,

 .data — глобальные переменные, имеющие начальные константные значения (например int a=5;),  

.bss — неинициализированные глобальные переменные  (например int b;) .

Рисунок 3. Распределение памятиРисунок 3. Распределение памяти

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

Кроме описания памяти и определения схемы расположения секций, в файле скрипта задаются значения констант, отображающих начальные и конечные адреса секций с данными, соответственно:

_sdata_ram, _edata_ram для секции .data, располагающейся в RAM;

_sdata_flash, _edata_flash для секции .data, располагающейся во FLASH;

_sbss, _ebss для секции .bss;

_svector, _evector для секции .vectors;

_estack начальный адрес стека.

Данные константы используются при инициализации памяти в коде программы, расположенном в файле   system_init.c.

Контроллер STM32G431KBT6 является достаточно развитой системой на кристалле (SoC),  содержащей около 50 функциональных модулей, которые управляются через регистры. Для программного обеспечения регистры доступны как 32 битные ячейки памяти, расположенные в разделе адресного пространства «Периферия» (рисунок 3). Описание всех регистров контроллера (названия регистров, адреса регистров, название и расположение битовых полей внутри регистра) занимает более 13 тысяч строк (стандартный файл stm32g431xx.h из пакета CMSIS).

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

Функциональные модули в адресном пространстве контроллера представлены в виде последовательности 32 битных регистров (ячеек памяти). Согласно спецификации, для каждого модуля определен базовый адрес (адрес первого регистра в блоке) и смещение. Например, у модуля GPIOB базовый адрес GPIOB_BASE = 0×48000400. Смещение управляющего регистра GPIOB_ODR (output data register) равен 0×14, т.е. абсолютный адрес равен значению GPIOB_ODR = GPIOB_BASE + 0×14 = 0×48000414.

/*********************** GPIO (General-purpose I/Os) *********************************/

#define GPIOB_BASE     (0x48000400)
#define GPIOB_MODER    (*(volatile uint32_t *) (GPIOB_BASE + 0x00))
#define GPIOB_ODR      (*(volatile uint32_t *) (GPIOB_BASE + 0x14))

В файле stm32g431.h определены макросы, которые позволяют сделать более наглядной работу с регистрами контроллера.

//Макросы, упрощающие работу с регистрами, где
//REG - адрес регистра
//BIT - 32 битное слово - биты для которых будет выполнена данная функция.
//Например: INVERT(GPIOB_ODR,0x6)  проинвертирует биты 1 и 2 в регистре GPIOB_ODR

#define SET_BIT(REG, BIT)     ((REG) |= (BIT))
#define CLEAR_BIT(REG, BIT)   ((REG) &= ~(BIT))
#define INVERT_BIT(REG, BIT)  ((REG) ^= (BIT))
#define READ_BIT(REG, BIT)    ((REG) & (BIT))
#define CLEAR_REG(REG)        ((REG) = (0x0))
#define READ_REG(REG)         ((REG))
#define WRITE_REG(REG, VAL)   ((REG) = (VAL))
#define MODIFY_REG(REG, CLEARMASK, SETMASK)  WRITE_REG((REG), (((READ_REG(REG)) & (~(CLEARMASK))) | (SETMASK)))

Теперь перейдем к обсуждению последовательности действий контроллера при отработке аппаратного рестарта.

Контроллер имеет внутренний RC тактовый генератор на 16MHz (HSI RC 16). Сразу после рестарта выходной сигнал с данного генератора используется в качестве тактового сигнала для вычислительного ядра и периферийных модулей.  

При отработке аппаратного рестарта, после выполнения ряда настроечных процедур, ядро контроллера загружает содержимое ячейки 0×00000000 в регистр SP (указатель стека),  а содержимое ячейки 0×00000004 в регистр PC (счетчик команд). Другими словами, ячейка 0×00000000 должна содержать адрес начала стека, а ячейка 0×00000004 адрес, с которого начинается выполнение кода программы при рестарте (Reset handler«s address).

Область памяти в начале адресного пространства является виртуальной (0×00000000 — 0×00080000), т.е. в зависимости от настройки метода загрузки микроконтроллера, в данную область могут отображаться различные модули памяти: внутренний FLASH, RAM, системный ROM,  внешний FLASH. Метод загрузки определяется распайкой вывода PB8 контроллера.  Для платы NUCLEO-G431KB по умолчанию загрузка выполняется из внутреннего FLASH. Это означает, что ячейки FLASH памяти, расположенные по адресам 0×08000000 — 0×08020000, также доступны по адресам 0×00000000 — 0×00020000. Таким образом, для данного режима загрузки можно сказать, что микроконтроллер стартует с первого адреса FLASH памяти т.е. с адреса 0×08000000.

Как мы видели из файла «stm32f4.ld»,  первые 256 слов во внутреннем флэше занимает таблица векторов прерывания (секция. vectors). Элементы таблицы векторов прерывания содержат адреса специальных функций (обработчиков), которые автоматически вызываются при возникновении прерываний от различных модулей микроконтроллера или в случае аппаратных ошибок. За исключением начального элемента таблицы, который содержит адрес вершины стека.

Определение таблицы векторов прерывания находится в файле «vectors.c».

__attribute__ ((section(".vectors"),used)) //Таблицу размещаем в секции .vectors
  uint32_t * vectors[]  = {
  (uint32_t *) &_estack,               //Вершина стека
  (uint32_t *) start_up,              //Обработчик reset (точка входа)
  (uint32_t *) nmi_handler,           //Обработчик немаскируемого прерывания
  (uint32_t *) hardfault_handler      //Обработчик прерывания аппаратной ошибки
};

Как видно из кода таблицы, в базовом проекте определены только первые три обработчика, необходимые для работы проекта. Остальные элементы таблицы мы будем заполнять в следующих проектах по мере необходимости.

Функции nmi_handler и hardfault_handler обрабатывают аварийные ситуации. Они переводят вычислительное ядро в глухой цикл.

Функция start_up находиться в файле «system_init.c»  и выполняет действия по инициализации RAM, необходимые для работы программы, скомпилированной с языка С. Сначала выполняется копирование данных секции .data из FLASH в RAM, затем выполняется запись нулей в секцию .bss.

Для настройки линий PB7 и PB8 на режим логического выхода используется функция GpioInit, расположенная в файле gpio.c.

void GpioInit(void)

{
	// Подать тактовый сигнал на модуль GPIOA
	SET_BIT(RCC_AHB2ENR, RCC_AHB2ENR_GPIOBEN);
	//Включить PIN8 на вывод (светодиод LD2)
	MODIFY_REG(GPIOB_MODER,  GPIO_MODER_MODE8, 1<

Основные минимальные настройки (по умолчанию) загружаются в управляющие регистры на аппаратном уровне при рестарте контроллера. Для реализации функции переключения светодиода необходимо настроить модуль GPIO (General-purpose I/O). Для этого первым делом нужно подать тактовый сигнал на данный модуль. Эта операция выполняется через модуль RCC (Reset and clock control). 

Для того чтобы точно определить период цикла переключения светодиода с помощью осциллографа, дополнительно к PB8 также настроим на вывод линию PB7 и будем изменять значения на данных выводах одновременно.   

Основной цикл программы находиться в файле «main.c».

Измеренное осциллографом время между двумя переключениями на линии PB7 составляет 686 мс. Это время в основном расходуется на выполнение цикла:

                                                        while (i) i--;

Ассемблерный код, сгенерированный компилятором для данной строки, имеет следующий вид:  

a:    	ldr	r3, [r7, #4]	// Загрузить переменную i в регистр r3. (2 такта)
      	subs	r3, #1			// Вычесть 1 из регистра r3. (1 такт)			 
       	str	r3, [r7, #4]	// Сохранить регистр r3 в переменную i. (2 такта)
				ldr	r3, [r7, #4]	// Загрузить переменную i в регистр r3. (2 такта)
				cmp	r3, #0		// Сравнить регистр r3 с 0 (1 такт)
      	bne.n	a		// Если не равно перейти на метку a: (1 такт + 
									// 2 такта перестройка конвейера команд)

Данный код можно увидеть в файле »01_Minimal.list» в папке 01_Minimal\Debug\ проекта.

В комментариях для каждой ассемблерной инструкции указаны выполняемые действия и время выполнения в тактах. Общее время одного цикла равно 11 тактам. При частоте внутреннего RC генератора 16 MHz период одного такта составляет 62,5 нс. Время выполнения одной итерации цикла составляет 62,5×11 = 687,5 нс. На миллион итераций будет затрачено приблизительно 687 мс, что практически совпадает с результатом, полученным с помощью осциллографа при измерении сигнала на ножке PB7 (рис. 4). Данная техника оценки времени выполнения функций будет использоваться в следующих экспериментах.

Рисунок 4. Сигнал на выводе PB7 (DC) при тактовой частоте 16 MHzРисунок 4. Сигнал на выводе PB7 (DC) при тактовой частоте 16 MHz

3. Подключение внешнего кварцевого резонатора

Стабильность внутреннего RC генератора микроконтроллера составляет приблизительно 1%, что недостаточно для поддержания точной настройки радиоприемника. Демонстрационная плата NUCLEO-G431KB содержит кварцевый резонатор на 24MGz, который обеспечивает стабильность порядка 0,01%. Для физического подключения кварцевого резонатора к микроконтроллеру необходимо запаять две перемычки под номером 9 и 10, как показано на рисунке 5.

Рисунок 5. Перемычки для подключения кварцевого резонатораРисунок 5. Перемычки для подключения кварцевого резонатора

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

Тактовая частота вычислительного ядра микроконтроллера STM32G431KBT6 может достигать 170 MHz. Для подключения кварцевого резонатора к внутренним схемам микроконтроллера и увеличения тактовой частоты до 170 MHz необходимо выполнить настройку следующих модулей:

  • Power control (PWR),

  • Embedded Flash memory (FLASH),

  • Reset and clock control (RCC).

Выполняются перечисленные операции в функции SystemClock_Config () из файла «system_init.c»

void SystemClock_Config(void)
{

  //Включить режим повышения выходного напряжения главного регулятора до 1,28 вольт
  CLEAR_BIT(PWR_CR5, PWR_CR5_R1MODE);

  //Установка задержки чтения FLASH памяти до 4-х тактов
  FLASH_ACR=FLASH_ACR_DBG_SWEN  | FLASH_ACR_DCEN | FLASH_ACR_ICEN | FLASH_ACR_LATENCY_4WS ;
  while((FLASH_ACR & 0xf) != FLASH_ACR_LATENCY_4WS );

  //Подключение внешнего кварцевого резонатора (HSE ON)
  SET_BIT(RCC_CR, RCC_CR_HSEON);
  while(READ_BIT(RCC_CR, RCC_CR_HSERDY) != (RCC_CR_HSERDY));

  //Настройка PLL, используемого для SYSCLK домена
  //PLLM=6 (значение поля 5), PLLN=85 (значение поля 85), PLLR=2 (значение поля 0)
  MODIFY_REG(RCC_PLLCFGR, RCC_PLLCFGR_PLLSRC | RCC_PLLCFGR_PLLM| RCC_PLLCFGR_PLLN | RCC_PLLCFGR_PLLR, RCC_PLLCFGR_PLLSRC_HSE | 5 <

Повышение тактовой частоты до максимального значения 170 MHz требует принятия специальных мер по настройке внутренних блоков.

Во-первых, необходимо увеличить напряжение питание ядра микроконтроллера с 1,2 вольт до 1,28 вольт (boost mode). Управление питанием внутренних блоков микроконтроллера осуществляет специальный модуль Power control (PWR).

Во-вторых, следует увеличить количество тактов ожидания чтения данных из FLASH памяти. FLASH память является относительно медленным устройством и позволяет считывать информацию без дополнительных тактов ожидания со скоростью до 30 MHz. Если тактовая частота ядра повышается до 170 MHz, то необходимо увеличить количество тактов ожидания до 4.

 Для повышения тактовой частоты c 24MHz от кварцевого резонатора до 170 MHz используется блок PLL (phase-locked loop). Настройка  PLL выполняется с помощью регистра RCC_PLLCFGR (PLL configuration register). Для настройки частоты используется три поля:  PLLM,  PLLN, PLLR:

 Fclk = ((Finput / PLLM) * PLLN) / PLLR .

 В нашем случае PLLM=6, PLLN=85, PLLR=2 и тактовая частота равна 170 = ((24/6)*85)/2.

 После внесения всех перечисленных выше изменений, измеренное осциллографом на линии PB7 время между двумя переключениями составляет 64,7 мс, что соответствует 64,7 нс на одну итерацию цикла,  или 64,7/11 = 5,88 нс на такт (что приблизительно соответствует 170 MHz тактовой частоты). То есть все настройки выполнены правильно.

Рисунок 6. Сигнал на выводе PB7 (DC) при тактовой частоте 170 MHzРисунок 6. Сигнал на выводе PB7 (DC) при тактовой частоте 170 MHz

Базовый проект с подключенным внешним кварцевым генератором и увеличенной тактовой частотой процессора до 170 MHz содержится в папке 02_HSE.

4. Таймер TIM2

Для работы радиоприемника нам необходимо подать на ADC внутренний тактовый сигнал дискретизации, частота и стабильность которого в конечном счете будут определять точность настройки на радиостанцию. Обычно генератор тактовой частоты реализуется с помощью одного из таймеров контроллера. Будем использовать таймер общего назначения TIM2.

Для демонстрации работы настоим таймер на частоту 2 Гц (период 500 мс). Для этого нам необходимо записать в регистр TIM2_ARR (Auto-reload register) число K, которое определяет количество периодов входной тактовой частоты таймера (170 MGz), помещающихся в одном периоде выходной частоты таймера. Более точно: Fвых = Fтакт/(K+1). Если нам необходима частота 2 Гц, то коэффициент K должен быть равен 85000000 — 1.  По умолчанию таймер работает в режиме прямого циклического счета (считает от 0 до K).  При достижении значения K,  счетчик таймера сбрасывается в 0, генерируется аппаратный сигнал «Counter overflow» и счет вновь повторяется от 0 до K. В нашем случае сигнал «Counter overflow» будет вырабатываться каждые 500 мс.

Настроим таймер TIM2 таким образом, чтобы при генерации сигнала «Counter overflow» вызывался обработчик прерывания — функция TIM2_IRQHandler, в которой будем одновременно переключать значение сигналов на выводах PB7 (Test) и PB8 (LD2) контроллера.

Все описанные действия выполняются в функциях, расположенных в файле «tim2.c» (проект 03_TIM2).

void TIM2_Init(void)
{

  SET_BIT(RCC_APB1ENR1, RCC_APB1ENR1_TIM2EN); // Подаем на TIM2 тактовую частоту
  TIM2_ARR = 85000000 - 1;   // Загружаем Auto-reload register 
  													 // f = 170 000 000/(K+1), 2Hz -> (85000000 - 1)

  SET_BIT(NVIC_ISER0, (1 << 28));		// Разрешить в NVIC прерывание #28 (TIM2)
  SET_BIT(TIM2_DIER, TIM_DIER_UIE);	// Разрешить прерывание по переполнению таймера
  SET_BIT(TIM2_CR1, TIM_CR1_CEN);		// Включить таймер
}

void TIM2_IRQHandler(void)
{
	CLEAR_BIT(TIM2_SR, TIM_SR_UIF);	  // Сброс флага переполнения
	INVERT_BIT(GPIOB_ODR, GPIO_ODR_OD7);  // Инвертировать (PB7)
	INVERT_BIT(GPIOB_ODR, GPIO_ODR_OD8);  // Инвертировать LD2 (PB8)
}

Особо следует отметить процедуру подключения обработчика прерывания для таймера TIM2. По таблице 97 (STM32G4 Series vector table)  документа «RM0440 Reference manual STM32G4 Series advanced Arm-based 32-bit MCUs»   находим, что прерывание для устройства TIM2 имеет номер 28.  Для настройки прерывания необходимо выполнить следующие действия:

1.   Разрешить генерацию прерывания от устройства TIM2 (#28) в функциональном модуле NVIC (Nested vectored interrupt controller).

2.   Поместить адрес функции обработчика прерыванияв таблицу векторов прерывания (массив uint32_t *vectors[] в файле «vectors.c») в элемент массива под номером 28+16.

3.   Разрешить прерывание по сигналу «Counter overflow» в регистре TIM2_DIER (DMA/Interrupt enable register).

После компиляции и загрузки программы в контроллер сигнал на выводе PB7 должен иметь следующий вид:

Рисунок 7. Сигнал на выводе PB7 (DC) при K = 85000000 - 1Рисунок 7. Сигнал на выводе PB7 (DC) при K = 85000000 — 1

5. DAC

В следующем примере подключим к проекту цифро-аналоговый преобразователь (DAC) и будем выводить в него данные по прерыванию от TIM2.  

Проект программы находится в папке 04_TIM2_DAC. В данный проект добавлен файл «dac.c»

Файл содержит одну короткую функцию инициализации модуля DAC1. В данном случае нам даже не нужно настраивать вывод PA4, так как он настроен на аналоговый режим по умолчанию.   

Будем выводить информацию в DAC по прерыванию от TIM2. Для этого смодифицируем файл «tim2.c».

void TIM2_Init(void)
{

  SET_BIT(RCC_APB1ENR1, RCC_APB1ENR1_TIM2EN); // Подаем на TIM2 тактовую частоту
  // Загружаем Auto-reload register  f = 170 000 000/(TIM2_ARR + 1) (1 MHz -> 170 - 1)
  TIM2_ARR = 170 - 1;      
  SET_BIT(NVIC_ISER0, (1 << 28));		// Разрешить в NVIC прерывание #28 (TIM2)
  SET_BIT(TIM2_DIER, TIM_DIER_UIE);	// Разрешить прерывание по переполнению таймера
  SET_BIT(TIM2_CR1, TIM_CR1_CEN);		// Включить таймер
}


void TIM2_IRQHandler(void)
{

  // Переменная I увеличивается на 1 при каждом вызове обработчика прерывания
  static int	 I=0;
		
  CLEAR_BIT(TIM2_SR, TIM_SR_UIF);	// Сброс флага переполнения
  DAC1_DHR12R1 = I++;	 // Запись в регистр данных DAC 12-ти младших разрядов переменной I
}

Устанавливаем период генерации таймера TIM2 равным 1 мкс. Модифицируем обработчик прерывания TIM2_IRQHandler. Добавляем в файл «main.c» вызов функции инициализации DAC1 — DAC1_Init (). После компиляции и загрузки программы 04_TIM2_DAC получаем на выходе DAC (вывод PA4) следующий сигнал (рисунок 8).

Рисунок 8. Сигнал на выходе DAC (DC), период таймера TIM2 - 1 мксРисунок 8. Сигнал на выходе DAC (DC),  период таймера TIM2 — 1 мкс

Частота полученного пилообразного сигнала равна 244.1 Hz (колонка Average внизу картинки). Разрядность DAC равна 12 битам, то есть устройство может вывести 4096 уровней напряжения в диапазоне от 0 до 3,3 вольт. При записи в 32-х разрядный регистр   данных   DAC1_DHR12R1, старшие разряды с 12 по 31 отбрасываются. Таким образом в обработчике прерывания циклически перебираются значения DAC1_DHR12R1 от 0 до 4095 с интервалом одного шага в 1 мкс. Период полного перебора равен 4096 мкс, то есть частота равна 1/0.004096 = 244.14 Hz, что согласуется с измерением.

6. Генератор синусоидального сигнала

Теперь попробуем создать генератор синусоидального сигнала. Для этого будем использовать модель пружинного маятника.

Рисунок 9. Движение пружинного маятникаРисунок 9. Движение пружинного маятника

Из курса физики средней школы известно, что ускорение равно:

 a = F/m   (1).

По закону Гука:

F = — kX    (2).

 Подставив второе выражение в первое получим:

a = — (k/m)*X.

Обозначим k/m,  как K.  В этом случае предыдущее выражение примет вид:

a = — K*X   (3).

Ускорение — это вторая производная координаты по времени, в связи с чем выражение (3) можно записать:

X»(t) = — K*X (t),   или X»(t) +  K*X (t) = 0 (4).

Уравнение (4) является дифференциальным уравнением второго порядка, частным решением которого является функция:

 X (t) = A*Sin (2π*f*t) (5),

где A — амплитуда,  f — частота. Действительно, если взять вторую производную от выражения (5), получим:

X»(t) = — A*(2π*f)2*Sin (2π*f*t) (6).

Подставив (5) и (6) в (4) получим:

— A*(2π*f)2*Sin (2π*f*t) = -K* A*Sin (2π*f*t).

Сократив  справа и слева одинаковые сомножители получим:

(2π*f)2 = K    (7),

То есть, при K = (2π*f)2 выражение (7) является тождеством.  Таким образом пружинный маятник колеблется по синусоидальному закону, при этом частота колебаний на основании выражения (7) равна:

03a4286184f8221f21e8f743b8fc9407.PNG

Для моделирования пружинного маятника, заменим в выражении (4) вторую производную X»(t) дискретным эквивалентом. Сделаем это следующим образом.

По определению производной:

8215ffdd89bf6458061aacbe8c82934e.PNG

 Если взять достаточно малое ∆t = (t+ — t), то 

61cd6c038d0ba92590b4fa47735cc8a7.PNG

где t+ — это следующий отсчет времени. То есть для выражения X»(t)  = -K*X (t)  можно записать :

((X (t+) — X (t))/ds) — (X (t) — X (t-))/ds))/ds ≈ -K*X (t) (9),

где t — текущий отсчет времени,  t+ — следующий отсчет времени,  t- — предыдущий отсчет времени,  ds — шаг дискретизации.   Преобразовав выражение (9),  можно записать:

(X (t+) — X (t)) — (X (t) — X (t-)) = -K*ds2*X (t) (10),

Обозначим V1 = (X (t+) — X (t)) и V0 = (X (t) — X (t-)). При этом выражение (10) примет вид:

V1 = V0 — K*ds2*X (t)    (11).

Из формулы V1 = (X (t+) — X (t)) следует, что:

                                                            X (t+) = X (t) + V1   (12).

Выражения (11) и (12) можно использовать для генерации дискретного эквивалента функции A*Sin (2π*f*t) «налету»,  итерационно.  Если обозначить R = K*ds2 , то эквивалентом выражений (11) и (12) на языке C будет запись:

 V -= X*R;
 X += V;

При этом на основании выражения (7) R можно вычислить по формуле:

52e6051a96b8367b2891631241e737e6.PNG

где f — частота генерируемого сигнала в герцах,  ds — период дискретизации в секундах.

Изменим код обработчика прерывания в файле «tim2.c»:

#define ds  0.000001      // Период дискретизации в секундах
#define PI  3.1415926			// Число ПИ	
#define PW  0.031415926   // PI*f*ds при f = 10000 Hz


void TIM2_IRQHandler(void)
{

	static float R = 4 * PW * PW; //R=(2*PW)^2
	static float V=0;
	static float X = 4096/3.3 * 0.5; //Начальное значение - амплитуда сигнала 0.5 V
	static float S=1500; 						//Смещение DAC

	CLEAR_BIT(TIM2_SR, TIM_SR_UIF);  //Сброс флага прерывания по переполнению таймера

  //Осциллятор
	V -= X*R;
	X += V;

	DAC1_DHR12R1 = X + S;

}

Здесь мы используем математику с плавающей запятой, поэтому нам необходимо включить сопроцессор FPU. Это делается добавлением одной строчки в функцию start_up () файла «system_init.c»:

/* Запуск FPU */
SCB_CPACR |= ((3 << (10*2))|(3 << (11*2)));  /* set CP10 and CP11 Full Access */

DAC поддерживает только положительные целые значения от 0 до 4095. Переменная X принимает значения приблизительно от -620.0 до +620.0. Для того чтобы перенести значения X в положительную область, перед выводом в DAC будем прибавлять к X смещение S,  равное 1500.

После компиляции и загрузки программы на выходе DAC можно наблюдать следующий сигнал (рисунок 10).

Рисунок 10. Сигнал на выходе DAC, период таймера TIM2 - 1 мкс, f = 10 KHz, THD = 0,05%Рисунок 10. Сигнал на выходе DAC,  период таймера TIM2 — 1 мкс,  f = 10 KHz,  THD = 0,05%

К сожалению, на частотах, приближающихся к частоте дискретизации, формула (13) перестает давать точный результат для вычисления R. Это объясняется тем, что замена производной X»(t) разностным уравнением (9) становиться достаточно грубым приближением. Например, на рисунке 11 приведена осциллограмма выходного сигнала при значении f    в формуле (13) равной 200 KHz. Как видно из рисунка, реальный сигнал имеет частоту 216 KHz.

Рисунок 11. Сигнал на выходе DAC, период таймера TIM2 - 1 мкс, f = 200 KHzРисунок 11. Сигнал на выходе DAC,  период таймера TIM2 — 1 мкс,  f = 200 KHz

Более точная формула для вычисления величины R в зависимости от частоты имеет вид:

R = (2*sin (PI*W))2  (14),

где W — относительная частота равная f*ds. При этом W должна быть меньше 0.5 (частота Найквиста).

Проект программы по генерации синусоидального сигнала находиться в папке  05_TIM2_DAC_SIN.

7. Подключение ADC

Блок-схема экспериментальной программы работы с АЦП достаточно проста: активируется ADC1 и DAC1, таймер TIM2 используется в качестве генератора частоты дискретизации для ADC, по прерыванию от ADC (окончание преобразования) сигнал с выхода ADC записывается в DAC.

Рисунок 12. Структурная схема программы ADC -> DAC» />Рисунок 12. Структурная схема программы ADC → DAC</p>

<p>Проект программы находится в папке 04_ADC_DAC. В проект добавлен файл «adc.c» , который состоит из двух функций: функции инициализации, и функции обработчика прерывания от ADC.</p>

<p>Как и в случае с DAС, специально настраивать вывод PA0 нет необходимости, так как аналоговый режим выбирается по умолчанию после сброса контроллера.  Настройка прерывания выполняется таким же образом, как для таймера TIM2 в предыдущем примере. Прерывание происходит по окончании процесса преобразования текущего отсчета в ADC.</p>

<p>Некоторым изменениям подвергся файл «tim2.c». Вместо генерации прерывания теперь таймер работает в режиме мастера, то есть выдает сигнал «Counter overflow» на внутренние линии контроллера для использования в качестве триггера старта преобразования в ADC. Таким образом событие  «Counter overflow»  задает частоту дискретизации (Fd)  входного сигнала.</p>

<p>Если собрать исследовательский стенд в соответствие с рисунком 1 и подать переменное напряжение от генератора на вход ADC, то на выходе DAC можно увидеть сигнал, представленный на рисунке 13. На всех осциллограммах данного раздела вход осциллографа работал в режиме AC.</p>

<p><img src=Рисунок 13. Сигнал на выходе DAC,  частота входного сигнала ADC 1 KHz, амплитуда 0,5 V

При увеличении частоты генератора картинка будет меняться, на частоте 200 KHz будут отчетливо видны интервалы дискретизации сигнала (рисунок 14).

Рисунок 14. Сигнал на выходе DAC, частота входного сигнала ADC  200 KHz, амплитуда 0,5 VРисунок 14. Сигнал на выходе DAC, частота входного сигнала ADC  200 KHz, амплитуда 0,5 V

При увеличении частоты входного сигнала до значения Fn = Fd/2 = 500 KHz (частота Найквиста/Котельникова) сигнал на выходе DAC примет следующий вид (рисунок 15).

Рисунок 15. Сигнал на выходе DAC, частота входного сигнала ADC  500 KHz, амплитуда 0,5 VРисунок 15. Сигнал на выходе DAC, частота входного сигнала ADC  500 KHz, амплитуда 0,5 V

Это максимальная частота входного синусоидального сигнала, для которой выходной сигнал будет сохранять точную информацию о частоте.  При дальнейшем увеличении частоты входного сигнала Fin частота выходного сигнала Fout будет изменяться в соответствии с графиком, представленным на рисунке 16.

Рисунок 16. Частота сигнала на выходе DAC (Fout) при увеличении частоты сигнала на входе ADC (Fin) больше чем FnРисунок 16. Частота сигнала на выходе DAC (Fout) при увеличении частоты сигнала на входе ADC (Fin) больше чем Fn

График соответствует следующим выражениям:  

Если Fin % Fd <  Fn,  то Fout = Fin % Fn;                         (15)

Если  Fin % Fd >=  Fn, то Fout  = Fn — ( Fin % Fn);           (16)

где % — остаток от деления;  Fin — частота входного сигнала, подаваемого на ADC;  Fout — частота выходного сигнала с DAC;  Fd — частота дискретизации;  Fn — частота Найквиста,  равная Fd/2.

В качестве иллюстрации можно привести осциллограмму, изображенную на рисунке 17. В данном случае частота входного сигнала равна 3900 KHz, частота дискретизации -1000 KHz и соответственно частота Найквиста — 500 KHz. Осциллограф показывает частоту на выходе приблизительно 100 KHz. Проверяем условие Fin % Fd  = 3900% 1000 = 900. Полученный результат больше частоты Найквиста, поэтому выбираем выражение (16). Подставив значения в формулу (16), мы получим Fout = 500 — (3900% 500) = 500 — 400 = 100 KHz.

Рисунок 17. Сигнал на выходе DAC, частота входного сигнала ADC 3900 KHz, амплитуда 0,5 VРисунок 17. Сигнал на выходе DAC, частота входного сигнала ADC 3900 KHz, амплитуда 0,5 V

Максимальное значение частоты входного сигнала определяется частотными характеристиками блока выборки и хранения ADC.  Измерения показывают, что схема выборки и хранения контроллера STM32G431KBT6 работает вполне удовлетворительно вплоть до частот порядка 100 MHz.  Например, так выглядит осциллограмма сигнала c частотой 65.1 MHz (рисунок 18).

Рисунок 18. Сигнал на выходе DAC , частота входного сигнала 65.1 MHz, амплитуда 0,5 VРисунок 18. Сигнал на выходе DAC, частота входного сигнала 65.1 MHz, амплитуда 0,5 V

Данное обстоятельство определяет два следствия: 1) Входные цепи приемника, подключенные к ADC, нужно тщательно оберегать от просачивания высокочастотных внедиапазонных сигналов. 2) При соответствующем проектировании входных цепей радиоприемника можно обеспечить прием сигналов радиостанций SW и даже FM диапазонов (!).

8. Перестраиваемый полосовой цифровой фильтр

Структурная схема проекта для исследования фильтра представлена на рисунке 19.

Рисунок 19. Структурная схема программы ADC -> Фильтр –> DAC» />Рисунок 19. Структурная схема программы ADC → Фильтр –> DAC</p>

<p>Мы будем использовать простейший цифровой фильтр, построенны
    
            <p class=© Habrahabr.ru