signal generator for an stm32f446re nucleo board

WIP: 100 Hz sine wave

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

oscillatory.net 7c396545 3be648cf

verified
+131 -36
+124 -35
Core/Src/main.c
··· 1 1 #include "main.h" 2 + #include <math.h> 2 3 3 - /** 4 - * @brief The application entry point. 5 - * @retval int 6 - */ 7 - int main(void) 4 + #define SINE_SAMPLES 256 5 + 6 + static uint16_t sine_table[SINE_SAMPLES]; 7 + 8 + /* -------------------------------------------------------------------------- 9 + * clock_init 10 + * 11 + * HSI (16 MHz) -> PLL -> 84 MHz SYSCLK 12 + * PLL: M=16, N=336, P=4 => 16/16 * 336 / 4 = 84 MHz 13 + * APB1 = 84/2 = 42 MHz (DAC, TIM6, USART2) 14 + * APB2 = 84/1 = 84 MHz 15 + * APB1 timer clock = 84 MHz (doubled because APB1 prescaler > 1) 16 + * -------------------------------------------------------------------------- */ 17 + static void clock_init(void) 8 18 { 19 + /* Voltage scale 3 (supports up to 120 MHz, fine for 84 MHz). 20 + PWR clock must be enabled before touching PWR registers. */ 21 + RCC->APB1ENR |= RCC_APB1ENR_PWREN; 22 + PWR->CR = (PWR->CR & ~PWR_CR_VOS) | (1UL << PWR_CR_VOS_Pos); 9 23 10 - /* Infinite loop */ 11 - while (1) 12 - { 13 - } 24 + /* 2 wait states required for 84 MHz. Must be set before speeding up. */ 25 + FLASH->ACR = FLASH_ACR_PRFTEN | FLASH_ACR_LATENCY_2WS; 26 + 27 + /* Enable HSI and wait for it to be ready */ 28 + RCC->CR |= RCC_CR_HSION; 29 + while (!(RCC->CR & RCC_CR_HSIRDY)) {} 30 + 31 + /* Configure PLL (still off at this point) */ 32 + RCC->PLLCFGR = (16 << RCC_PLLCFGR_PLLM_Pos) | /* M: divide HSI down to 1 MHz */ 33 + (336 << RCC_PLLCFGR_PLLN_Pos) | /* N: multiply to 336 MHz VCO */ 34 + (1 << RCC_PLLCFGR_PLLP_Pos) | /* P=4 (01): 336/4 = 84 MHz */ 35 + (2 << RCC_PLLCFGR_PLLQ_Pos) | /* Q: USB etc, unused here */ 36 + (2 << RCC_PLLCFGR_PLLR_Pos); /* R: I2S etc, unused here */ 37 + /* PLLSRC bit = 0 => HSI source (default) */ 38 + 39 + RCC->CR |= RCC_CR_PLLON; 40 + while (!(RCC->CR & RCC_CR_PLLRDY)) {} 41 + 42 + /* APB1 prescaler /2 so APB1 peripherals run at 42 MHz. 43 + Timers on APB1 still get 84 MHz (hardware doubles it when prescaler > 1). */ 44 + RCC->CFGR = RCC_CFGR_PPRE1_DIV2; 45 + 46 + /* Switch SYSCLK to PLL */ 47 + RCC->CFGR |= RCC_CFGR_SW_PLL; 48 + while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) {} 49 + 50 + SystemCoreClock = 84000000; 14 51 } 15 52 53 + /* -------------------------------------------------------------------------- 54 + * dac_dma_init 55 + * 56 + * TIM6 fires at 256 kHz => DMA copies one sample per tick => 57 + * 256-sample table plays back at 256000/256 = 1000 Hz 58 + * -------------------------------------------------------------------------- */ 59 + static void dac_dma_init(void) 60 + { 61 + /* Enable clocks for everything we touch */ 62 + RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_DMA1EN; 63 + RCC->APB1ENR |= RCC_APB1ENR_DACEN | RCC_APB1ENR_TIM6EN; 16 64 17 - /** 18 - * @brief This function is executed in case of error occurrence. 19 - * @retval None 20 - */ 21 - void Error_Handler(void) 65 + /* PA4 = DAC_OUT1: set to analog mode (MODER = 0b11) so the DAC 66 + drives the pin directly without the GPIO output stage interfering */ 67 + GPIOA->MODER |= (3U << (4 * 2)); 68 + 69 + /* TIM6 --------------------------------------------------------------- 70 + Counter clock = APB1 timer clock = 84 MHz 71 + Sample rate = 84 MHz / (PSC+1) / (ARR+1) = 84 MHz / 1 / 328 = ~256 kHz 72 + TRGO fires on every update event, which triggers the DAC */ 73 + TIM6->PSC = 0; 74 + TIM6->ARR = 3279; /* reload value: 84MHz/3280 = ~25.6kHz, /256 samples = ~100Hz */ 75 + TIM6->CR2 = (2 << TIM_CR2_MMS_Pos); /* MMS=010: Update -> TRGO */ 76 + 77 + /* DMA1 Stream5 Channel7 (hardwired to DAC channel 1 in the DMA mux) -- 78 + Memory (sine_table) -> Peripheral (DAC->DHR12R1) 79 + Circular: wraps back to the start after SINE_SAMPLES transfers */ 80 + DMA1_Stream5->CR = 0; 81 + while (DMA1_Stream5->CR & DMA_SxCR_EN) {} /* wait for any prior EN to clear */ 82 + 83 + DMA1_Stream5->CR = (7 << DMA_SxCR_CHSEL_Pos) | /* channel 7 */ 84 + DMA_SxCR_DIR_0 | /* memory -> peripheral */ 85 + DMA_SxCR_CIRC | /* circular mode */ 86 + DMA_SxCR_MINC | /* increment memory pointer */ 87 + DMA_SxCR_MSIZE_0 | /* memory word = 16 bit */ 88 + DMA_SxCR_PSIZE_0; /* periph word = 16 bit */ 89 + 90 + DMA1_Stream5->NDTR = SINE_SAMPLES; /* number of transfers */ 91 + DMA1_Stream5->PAR = (uint32_t)&DAC->DHR12R1; /* destination: DAC data reg*/ 92 + DMA1_Stream5->M0AR = (uint32_t)sine_table; /* source: our table */ 93 + DMA1_Stream5->CR |= DMA_SxCR_EN; 94 + 95 + /* DAC channel 1 ------------------------------------------------------- 96 + TEN1 = trigger enable 97 + TSEL1 = 000 (default) = TIM6 TRGO triggers each conversion 98 + DMAEN1 = DAC issues a DMA request on each trigger 99 + BOFF1 = 0 (default) = output buffer ON, drives PA4 directly 100 + EN1 = channel enable */ 101 + DAC->CR = DAC_CR_TEN1 | DAC_CR_DMAEN1 | DAC_CR_EN1; 102 + 103 + /* Start the timer last so everything is ready when samples start flowing */ 104 + TIM6->CR1 = TIM_CR1_CEN; 105 + } 106 + 107 + int main(void) 22 108 { 23 - /* USER CODE BEGIN Error_Handler_Debug */ 24 - /* User can add his own implementation to report the HAL error return state */ 25 - __disable_irq(); 26 - while (1) 27 - { 28 - } 29 - /* USER CODE END Error_Handler_Debug */ 109 + /* Blink LD2 (PA5) 5 times to confirm the MCU is executing */ 110 + RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; 111 + GPIOA->MODER = (GPIOA->MODER & ~(3U << (5*2))) | (1U << (5*2)); 112 + for (int b = 0; b < 5; b++) { 113 + GPIOA->BSRR = (1U << 5); 114 + for (volatile int i = 0; i < 300000; i++) {} 115 + GPIOA->BSRR = (1U << (5+16)); 116 + for (volatile int i = 0; i < 300000; i++) {} 117 + } 118 + 119 + clock_init(); 120 + 121 + /* Build sine table at runtime: 12-bit unsigned, centred at mid-scale. 122 + Output swings 0-3.3V (or 0-Vref) peak-to-peak */ 123 + for (int i = 0; i < SINE_SAMPLES; i++) 124 + sine_table[i] = (uint16_t)(2048 + 2047.0f * sinf(2.0f * 3.14159265f * i / SINE_SAMPLES)); 125 + 126 + dac_dma_init(); 127 + 128 + while (1) {} 30 129 } 31 - #ifdef USE_FULL_ASSERT 32 - /** 33 - * @brief Reports the name of the source file and the source line number 34 - * where the assert_param error has occurred. 35 - * @param file: pointer to the source file name 36 - * @param line: assert_param error line source number 37 - * @retval None 38 - */ 39 - void assert_failed(uint8_t *file, uint32_t line) 130 + 131 + void Error_Handler(void) 40 132 { 41 - /* USER CODE BEGIN 6 */ 42 - /* User can add his own implementation to report the file name and line number, 43 - ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */ 44 - /* USER CODE END 6 */ 133 + __disable_irq(); 134 + while (1) {} 45 135 } 46 - #endif /* USE_FULL_ASSERT */
+3
build.sh
··· 1 + #!/bin/sh 2 + 3 + cmake --preset Debug && cmake --build --preset Debug
+1 -1
cmake/stm32cubemx/CMakeLists.txt
··· 60 60 target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE ${MX_LINK_DIRS}) 61 61 62 62 # Add libraries to the project 63 - target_link_libraries(${CMAKE_PROJECT_NAME} ${MX_LINK_LIBS}) 63 + target_link_libraries(${CMAKE_PROJECT_NAME} ${MX_LINK_LIBS} m) 64 64 65 65 # Add the map file to the list of files to be removed with 'clean' target 66 66 set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES ADDITIONAL_CLEAN_FILES ${CMAKE_PROJECT_NAME}.map)
+3
program.sh
··· 1 + #!/bin/sh 2 + 3 + STM32_Programmer_CLI -c port=SWD -w build/Debug/signal-generator.elf -rst