add adc example
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…
Reference in New Issue