From 2f6d97bfe00b79c2038860d1c3e9ba9ba6ed1aa5 Mon Sep 17 00:00:00 2001 From: Clyne Sullivan Date: Mon, 3 Apr 2023 13:28:43 -0400 Subject: [PATCH] add adc example --- 01-adc/Makefile | 5 ++ 01-adc/README.md | 173 +++++++++++++++++++++++++++++++++++++++++++++++ 01-adc/main.c | 89 ++++++++++++++++++++++++ 3 files changed, 267 insertions(+) create mode 100644 01-adc/Makefile create mode 100644 01-adc/README.md create mode 100644 01-adc/main.c diff --git a/01-adc/Makefile b/01-adc/Makefile new file mode 100644 index 0000000..4a7074c --- /dev/null +++ b/01-adc/Makefile @@ -0,0 +1,5 @@ +COMMON_DIR := ../common + +CFILES += main.c + +include $(COMMON_DIR)/rules.mk diff --git a/01-adc/README.md b/01-adc/README.md new file mode 100644 index 0000000..665a180 --- /dev/null +++ b/01-adc/README.md @@ -0,0 +1,173 @@ +# 1. Reading ADC inputs through polling + +## ADC configuration and initialization + +### 1. Starting the ADC clock + +First, the ADC clock's source must be selected since it defaults to none. We +will use the system clock as our source; if we were using the PLL, we could use +one of the PLL's "R" clocks instead. + +```cpp +RCC->CCIPR &= ~(RCC_CCIPR_ADCSEL_Msk); +RCC->CCIPR |= 3 << RCC_CCIPR_ADCSEL_Pos; // Select system clock for ADC clock +``` + +Then, we simply enable the ADC clock through the AHB: +```cpp +RCC->AHB2ENR |= RCC_AHB2ENR_ADCEN; +``` + +### 2. Power on the ADC's internal voltage regulator + +The ADC's internal regulator must be powered on before enabling the ADC. If the +ADC is disabled later, this regulator can be turned off to save power. + +```cpp +ADC1->CR = 0; // The CR register must be zero before setting ADVREGEN +ADC1->CR = ADC_CR_ADVREGEN; +``` + +The internal regulator should be given time to warm up after turning on so that +its output stabilizes. A delay of around 20 microseconds should be enough; a +very crude delay function is implemented in the example. + +### 3. Calibrating the ADC + +The ADC should be calibrated when first powered on. The calibration process is +something internal (undocumented) to the microcontroller, so we just tell the +calibration to begin and wait for it to finish. + +The microcontroller has the option to do a differential calibration, but we +will skip this since we are only using single-ended channels. This is also a +good time to select single-ended channels for our conversions. + +```cpp +ADC1->DIFSEL = 0; // Clear DIFSEL for single-ended conversions. + +ADC1->CR &= ~(ADC_CR_ADCALDIF); // Select single-ended calibration +ADC1->CR |= ADC_CR_ADCAL; // Begin calibration +while ((ADC1->CR & ADC_CR_ADCAL) != 0); // Wait for ADCAL to clear +``` + +If execution gets stuck in the `while` loop, there is a chance that the ADC's +clock was incorrectly configured. + +### 4. Enable the ADC + +Now, the ADC is ready to be enabled: + +```cpp +ADC1->CR |= ADC_CR_ADEN; +while ((ADC1->ISR & ADC_ISR_ADRDY) == 0); // Wait for the ADC to be ready +``` + +## Prepare GPIO pin for analog reading + +We will use pin PC0 since it is wired to the first ADC channel. See the +STM32L476RG datasheet for a table of connections between pins and ADC channels. + +Make sure the GPIO port's clock is enabled: + +```cpp +RCC->AHB2ENR |= RCC_AHB2ENR_GPIOCEN; +``` + +Then, set the GPIO pin's mode to analog input: + +```cpp +GPIOC->MODER |= 3 << GPIO_MODER_MODE0_Pos; +``` + +Finally, STM32L47x/L48x processors require an additional register setting to +complete the pin's connection to the ADC. We set bit zero for ADC channel one: + +```cpp +GPIOC->ASCR |= 1 << 0; +``` + +## Prepare and execute the analog conversion + +### Conversion sequencing + +First we need to set up the ADC's conversion sequence. The ADC can take +multiple conversions in a row, with channels arranged in order according to +the `SQR` registers. + +We are doing a single conversion of a single channel, channel one. So, we +write a `1` for the first sequence channel. By setting the register with an +`=` instead of just setting bits, we clear the L field of the register to zero +which sets a sequence length of one. + +```cpp +ADC1->SQR1 = 1 << ADC_SQR1_SQ1_Pos; +``` + +### Other configuration + +The `CFGR` register can be used to adjust the ADC's resolution (8 to 12 bits). + +The `CFGR2` register can be configured if hardware oversampling will be used. + +The `SMPR` registers can be configured to select sampling times. We will leave +this unconfigured to use the default (fastest) time. + +The `OFR` register can be configured to have an offset subtracted from the +analog reading during conversion. + +### Running the conversion + +Below are the steps for completing a single channel conversion. In the example, +this code is contained within the `adc_read` function. + +Set `CFGR` for single conversion, then initiate the conversion: + +```cpp +ADC1->CFGR &= ~(ADC_CFGR_CONT); + +ADC1->CR |= ADC_CR_ADSTART; +``` + +The ADC will set the end-of-conversion (EOC) bit of its status register `ISR` +once the conversion is complete. If we were using interrupts, we could write +and enable an interrupt handler to detect this; instead, we will simply poll +the register. + +Once the EOC bit is set, we need to clear it before the next conversion. + +```cpp +while ((ADC1->ISR & ADC_ISR_EOC) == 0); +ADC1->ISR &= ~(ADC_ISR_EOC); +``` + +The conversion result is now available in the data register `DR`. + +## Getting feedback with an LED + +We will use the Nucleo's on-board LED to produce feedback on our ADC +reading. The below code will turn the LED on or off depending on if the +ADC reading goes above a certain threshold. + +Configure pin PA5 for output to control the LED: + +```cpp +RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN; +GPIOA->MODER &= ~(GPIO_MODER_MODE5_Msk); +GPIOA->MODER |= 1 << GPIO_MODER_MODE5_Pos; +``` + +Now, we run a simple loop. Our threshold will be 2048, half of the ADC's 12- +bit range. With the default voltage reference of 3.3V, the threshold will be +equal to `3.3V * 2048 / (2^12 - 1) = 1.65V`. + +```cpp +while (1) { + unsigned int reading = adc_read(); + + if (reading > 2048) + GPIOA->BSRR |= 1 << 5; + else + GPIOA->BRR |= 1 << 5; +} +``` + diff --git a/01-adc/main.c b/01-adc/main.c new file mode 100644 index 0000000..0f9eb1b --- /dev/null +++ b/01-adc/main.c @@ -0,0 +1,89 @@ +#include + +// Reads the configured ADC channel and returns its value. +unsigned int adc_read(void); + +// Delays for approximately the given amount of microseconds. +void delay_us(int); + +int main(void) +{ + // Disable interrupts + asm("cpsid i"); + + // 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; + delay_us(20); + + // 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; + + // Configure the Nucleo's LED (pin PA5) for output. + RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN; + GPIOA->MODER &= ~(GPIO_MODER_MODE5_Msk); + GPIOA->MODER |= 1 << GPIO_MODER_MODE5_Pos; + + // In a loop, read from the ADC channel and set the LED according to + // the result. + while (1) { + unsigned int reading = adc_read(); + + if (reading > 2048) + GPIOA->BSRR |= 1 << 5; + else + GPIOA->BRR |= 1 << 5; + } +} + +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 +} + +void delay_us(int amount) +{ + // This loop should take around a microsecond to execute per iteration + // when running at the default speed of 4MHz. + while (amount > 0) { + asm volatile("nop"); + --amount; + } +} +