From de6256dd4521b6993891c945aacdf2136b2fc48c Mon Sep 17 00:00:00 2001 From: Clyne Sullivan Date: Wed, 12 Apr 2023 21:12:31 -0400 Subject: [PATCH] add DAC/IIR example --- 02-dac/Makefile | 5 +++ 02-dac/README.md | 71 +++++++++++++++++++++++++++++++++++++++ 02-dac/main.c | 87 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 02-dac/Makefile create mode 100644 02-dac/README.md create mode 100644 02-dac/main.c diff --git a/02-dac/Makefile b/02-dac/Makefile new file mode 100644 index 0000000..4a7074c --- /dev/null +++ b/02-dac/Makefile @@ -0,0 +1,5 @@ +COMMON_DIR := ../common + +CFILES += main.c + +include $(COMMON_DIR)/rules.mk diff --git a/02-dac/README.md b/02-dac/README.md new file mode 100644 index 0000000..731dcfe --- /dev/null +++ b/02-dac/README.md @@ -0,0 +1,71 @@ +# 2. DAC output with an IIR filter + +## DAC initialization + +This example uses the DAC with minimal configuration. + +Enable the DAC's clock through the RCC: + +```cpp +RCC->APB1ENR1 |= RCC_APB1ENR1_DAC1EN; +``` + +Then, just enable DAC channel one. This will output over pin PA4: + +```cpp +DAC1->CR = DAC_CR_EN1; +``` + +We rely on the default/reset configuration of the other DAC registers. In this +configuration, the DAC is running without a hardware or software trigger; +instead, the DAC will update its output whenever we provide a new output value. + +There is a variety of output registers available for different bit-sizes and +alignments. We will stick to the basic 12-bit, right-aligned register +`DHR12R1`. + +## IIR filter + +The filter will run in a `while` loop, reading samples from the ADC and using +them to produce new DAC output data. + +The first-order recursive equation is `y[n] = a*y[n-1] + b*x[n]`. Constants `a` +and `b` set the weights for the previous output sample and current input +sample; their sum should not be greater than one. + +```cpp +const float a = 0.5f; +const float b = 0.5f; +``` + +In the `while` loop, we read our input sample from the ADC (`x[n]`) and +calculate the new output value `y`: + +```cpp +unsigned int yprev = 0; // y[n-1] +while (1) { + unsigned int x = adc_read(); + unsigned int y = a * yprev + b * x; +``` + +The new output value is then sent out over the DAC and stored in `yprev` for +the next calculation/iteration. + +```cpp + DAC1->DHR12R1 = y; + yprev = y; +``` + +Finally, we add a small delay to have some basic control over the sampling +rate. Increasing the loop's max value may make the filter's effect more +visible, while decreasing it will minimize the observed effect. + +Ideally, the sampling rate would instead be controlled by setting the speed of +the clock given to the ADC and configuring the registers responsible for the +conversion and sampling timings. + +```cpp + for (int i = 0; i < 100; ++i); +} +``` + diff --git a/02-dac/main.c b/02-dac/main.c new file mode 100644 index 0000000..4c38595 --- /dev/null +++ b/02-dac/main.c @@ -0,0 +1,87 @@ +#include + +// Initializes the ADC for reading from channel one. +void adc_init(void); +// Does an ADC conversion on channel one and returns the result. +unsigned int adc_read(void); + +int main(void) +{ + adc_init(); + + // DAC initialization: + // Enables DAC channel one on PA4 + // Does not configure a hardware or software trigger, so DAC updates + // when its data register is written. + RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN; + RCC->APB1ENR1 |= RCC_APB1ENR1_DAC1EN; + DAC1->CR = DAC_CR_EN1; + + // Execute a first-order IIR filter in a loop: y[n] = a*y[n-1] + b*x[n] + const float a = 0.5f; + const float b = 0.5f; + unsigned int yprev = 0; // y[n-1] + while (1) { + unsigned int x = adc_read(); + unsigned int y = a * yprev + b * x; + + DAC1->DHR12R1 = y; + yprev = y; + + // Add some delay between samples + for (int i = 0; i < 100; ++i); + } +} + +void adc_init(void) +{ + // Use the CCIPR register to select the system clock as a source for the + // ADC clock, then enable the ADC clock through the AHB. + RCC->CCIPR &= ~(RCC_CCIPR_ADCSEL_Msk); + RCC->CCIPR |= 3 << RCC_CCIPR_ADCSEL_Pos; + RCC->AHB2ENR |= RCC_AHB2ENR_ADCEN; + + // Enable the ADC's internal voltage regulator, with a delay for its + // startup time. + // ADVREGEN requires some bits in the CR register to be zero before being + // set, so we clear CR to zero first. + ADC1->CR = 0; + ADC1->CR = ADC_CR_ADVREGEN; + for (int i = 0; i < 100; ++i); + + // Clear DIFSEL to do single-ended conversions. + ADC1->DIFSEL = 0; + + // Begin a single-ended calibration and wait for it to complete. + // If the clock source for the ADC is not correctly configured, the + // processor may get stuck in the while loop. + ADC1->CR &= ~(ADC_CR_ADCALDIF); // Select single-ended calibration + ADC1->CR |= ADC_CR_ADCAL; + while ((ADC1->CR & ADC_CR_ADCAL) != 0); + + // Enable the ADC and wait for it to be ready. + ADC1->CR |= ADC_CR_ADEN; + while ((ADC1->ISR & ADC_ISR_ADRDY) == 0); + + // GPIO pin PC3 will be used as our ADC input. + // In order: enable the GPIOC clock, set PC3 to analog input mode, + // connect ADC channel one to its GPIO pin (PC3). + RCC->AHB2ENR |= RCC_AHB2ENR_GPIOCEN; + GPIOC->MODER |= 3 << GPIO_MODER_MODE0_Pos; + GPIOC->ASCR |= 1 << 0; + + // Make channel one the first in the ADC's read sequence. + // Using '=' here also clears the L field of the register, giving the + // sequence a length of one. + ADC1->SQR1 = 1 << ADC_SQR1_SQ1_Pos; +} + +unsigned int adc_read(void) +{ + ADC1->CFGR &= ~(ADC_CFGR_CONT); // Single conversion + ADC1->CR |= ADC_CR_ADSTART; // Start converting + while ((ADC1->ISR & ADC_ISR_EOC) == 0); // Wait for end-of-conversion + ADC1->ISR &= ~(ADC_ISR_EOC); // Clear the status flag + return ADC1->DR; // Read the result +} +