add adc example

main
Clyne 2 years ago
parent b8585da5c8
commit 2f6d97bfe0

@ -0,0 +1,5 @@
COMMON_DIR := ../common
CFILES += main.c
include $(COMMON_DIR)/rules.mk

@ -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;
}
```

@ -0,0 +1,89 @@
#include <stm32l476xx.h>
// 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;
}
}
Loading…
Cancel
Save