Created
February 24, 2021 23:56
-
-
Save mp035/09b4cf7e82c6f59ccf0363632726a375 to your computer and use it in GitHub Desktop.
STM32F030F4P6 App ADC Code
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* app_adc.c | |
* | |
* Created on: 20 Feb 2021 | |
* Author: mark | |
*/ | |
#include "main.h" // for access to hardware functions | |
#include "app.h" | |
#include <string.h> | |
#include <stdlib.h> | |
#include "build_timestamp.h" | |
uint16_t vrefIntCal; | |
uint16_t tsCal1; | |
//#define NUM_CHANNELS 6 // defined in app.h because it is needed elsewhere. | |
#define NUM_MAINS_VOLT_CHANNELS 3 | |
#define NUM_SAMPLES 80 | |
#define BUFFER_SIZE (NUM_CHANNELS * NUM_SAMPLES) | |
uint16_t adcBuffer[BUFFER_SIZE]; | |
uint64_t adcAccumulator[NUM_CHANNELS]; | |
uint32_t adcZeroAccumulator[NUM_CHANNELS]; | |
uint16_t adcZero[NUM_CHANNELS]; | |
int32_t adcReading[NUM_CHANNELS]; | |
uint8_t readingFlag = 2; // we start at 2 so that the first reading is discarded. | |
int64_t readingAccumulator[NUM_CHANNELS]; | |
int32_t readingCounter[NUM_CHANNELS]; | |
int32_t avgReading[NUM_CHANNELS]; | |
uint32_t readingTimestamp = 0; | |
# define PHASE_LOCK_TARGET 13 | |
int zeroCross = -1; | |
uint64_t cycleClock = 0; | |
uint32_t getTimestamp(){ | |
return cycleClock / 50; | |
} | |
uint32_t setTimestamp(uint32_t value){ | |
cycleClock = value * 50; | |
} | |
uint32_t packetId = 1; // used globally when sending serial packets to the internet controller. | |
#define I_CHAN 0 | |
#define VA_CHAN 1 | |
#define VB_CHAN 2 | |
#define VC_CHAN 3 | |
#define TEMP_CHAN 4 | |
#define VREF_CHAN 5 | |
/** | |
* @brief Integer square root for RMS | |
* @param sqrtAvg (sum(x squared) / count) | |
* @retval approximate square root | |
* | |
* Approximate integer square root, used for RMS calculations. | |
*/ | |
static uint16_t sqrtI( uint32_t sqrtArg ) | |
{ | |
uint16_t answer, x; | |
uint32_t temp; | |
if ( sqrtArg == 0 ) return 0; // undefined result | |
if ( sqrtArg == 1 ) return 1; // identity | |
answer = 0; // integer square root | |
for( x=0x8000; x>0; x=x>>1 ) | |
{ // 16 bit shift | |
answer |= x; // possible bit in root | |
temp = answer*answer; // fast unsigned multiply (hopefully) | |
if (temp == sqrtArg) break; // exact, found it | |
if (temp > sqrtArg) answer ^= x; // too large, reverse bit | |
} | |
return answer; // approximate root | |
} | |
// Single pole IIR filter see https://fiiir.com to design the correct decay value and | |
// https://tomroelandts.com/articles/low-pass-single-pole-iir-filter for a good explanation | |
// of how this filter works. | |
// this implementation uses Q15 format disguised as int16_t | |
#define DECAY 31130 // 0.95 in Q15 | |
#define FILT_B (32768 - DECAY) // (1.0 - DECAY) in Q15 | |
int32_t filt_y = 0; | |
static int16_t filter(int16_t filt_x){ | |
filt_y += ( FILT_B * ((int32_t)filt_x - filt_y)) >> 15; | |
return filt_y; | |
} | |
// compensate for variations in supply voltage | |
// and get the raw voltage output (in whatever units supplyVoltage is given) | |
static inline uint32_t compensateReading(uint32_t supplyVoltage, int32_t value){ | |
return supplyVoltage * value / 4095; | |
} | |
// the accumulate readings function takes 1 for the upper half of the buffer, and 0 for the lower. | |
void accumulateReadings( size_t upper){ | |
int chan, sample, lastFilter = -1; | |
const uint32_t totalSamples = BUFFER_SIZE / 2; | |
uint16_t *buffer = &adcBuffer[upper * totalSamples]; | |
for (sample = 0; sample < totalSamples; sample += NUM_CHANNELS){ | |
for (chan = 0; chan < NUM_CHANNELS; chan++){ | |
int index = chan + sample; | |
// for AC channels, zero reference, then square before averaging (RMS) | |
if((chan >= I_CHAN) && (chan <= VC_CHAN)){ | |
int32_t sampleValue = buffer[index] - adcZero[chan]; | |
// filter VA and look for zero cross so that we can phase synchronise | |
if (chan == VA_CHAN){ | |
int aval = filter(sampleValue); | |
if (lastFilter > 0 && aval < 0){ | |
zeroCross = (upper * totalSamples + sample) / NUM_CHANNELS; | |
} | |
lastFilter = aval; | |
} | |
adcAccumulator[chan] += sampleValue * sampleValue; | |
adcZeroAccumulator[chan] += buffer[index]; | |
} else { | |
adcAccumulator[chan] += buffer[index]; | |
} | |
} | |
} | |
// adjust for phase and frequency. | |
# define TIM3_ARR_VALUE 12000 | |
# define TIM3_ARR_MAX (TIM3_ARR_VALUE + TIM3_ARR_VALUE / 50)// +2% | |
# define TIM3_ARR_MIN (TIM3_ARR_VALUE - TIM3_ARR_VALUE / 50)// -2% | |
static int lastZc = PHASE_LOCK_TARGET; | |
static int freqCount = 0; | |
if (zeroCross > 0){ | |
// adjust for frequency. | |
if (zeroCross != lastZc){ | |
//decide whether the transition is up or down. | |
int compZc, compLzc; | |
if (abs(zeroCross - lastZc) > NUM_SAMPLES / 2){ | |
// the sample positions are in different hemispheres. | |
// rotate them into the same hemisphere. | |
compZc = (zeroCross + NUM_SAMPLES / 2) % NUM_SAMPLES; | |
compLzc = (lastZc + NUM_SAMPLES / 2) % NUM_SAMPLES; | |
} else { | |
compZc = zeroCross; | |
compLzc = lastZc; | |
} | |
if (compZc > compLzc && TIM3->ARR < TIM3_ARR_MAX){ | |
TIM3->ARR++; | |
} else if (compZc < compLzc && TIM3->ARR > TIM3_ARR_MIN) { | |
TIM3->ARR--; | |
} | |
avgReading[NUM_CHANNELS] = freqCount; | |
freqCount = 0; | |
} else { | |
freqCount++; | |
if (freqCount > 20){ | |
freqCount = 0; | |
// frequency is relatively in close, so if phase is out apply a frequency error to bring it back. | |
int compZc = zeroCross + (NUM_SAMPLES/2 - PHASE_LOCK_TARGET); // compensate the zero cross so that the target sits on NUM_SAMPLES/2 | |
compZc %= NUM_SAMPLES; // roll over out-of-range values (if present) | |
if (compZc > NUM_SAMPLES/2 && TIM3->ARR < TIM3_ARR_MAX){ | |
TIM3->ARR++; | |
} else if (compZc < NUM_SAMPLES/2 && TIM3->ARR > TIM3_ARR_MIN) { | |
TIM3->ARR--; | |
} | |
} | |
} | |
lastZc = zeroCross; | |
} | |
} | |
void calculateReadings(){ | |
// first calculate VDDA because we use it to compensate | |
// for changes in supply voltage | |
// NOTE: supplyVoltage is ~ 33,000 due to (vrefIntCal*10) | |
int32_t supplyVoltage = 3300 * (vrefIntCal * 10) / (adcAccumulator[VREF_CHAN] / NUM_SAMPLES); | |
for (int chan = 0; chan < NUM_CHANNELS; chan++){ | |
if (chan == VREF_CHAN){ | |
adcReading[VREF_CHAN] = supplyVoltage; | |
} else if (chan == TEMP_CHAN){ | |
uint32_t AVG_SLOPE = 5336; //AVG_SLOPE in ADC conversion step multiplied by 1000 | |
uint32_t VDD_CALIB = 33000; // temperature is calibrated at 3.3V (not 3.0V) on F030F4P6 series. | |
// convert the temperature directly to degrees. | |
adcReading[chan] = adcAccumulator[chan] / NUM_SAMPLES; | |
adcReading[chan] = (tsCal1 - (adcReading[chan] * adcReading[VREF_CHAN] / VDD_CALIB)) * 1000; // convert to calibrated AVCC and multiply by 1000 for use with AVG_SLOPE | |
adcReading[chan] = (adcReading[chan] / AVG_SLOPE) + 30; | |
} else { | |
// calclulate RMS | |
adcReading[chan] = compensateReading(supplyVoltage, sqrtI(adcAccumulator[chan]/NUM_SAMPLES)); | |
adcZero[chan] = adcZeroAccumulator[chan] / NUM_SAMPLES; | |
} | |
adcAccumulator[chan] = 0; | |
adcZeroAccumulator[chan] = 0; | |
} | |
if (readingFlag) readingFlag--; | |
} | |
// ********************************************************************************* | |
void app_adc_calibrate(){ | |
// calibrate ADC | |
/* Ensure that ADEN = 0 */ | |
if ((ADC1->CR & ADC_CR_ADEN) != 0) | |
{ | |
/* Clear ADEN by setting ADDIS*/ | |
ADC1->CR |= ADC_CR_ADDIS; | |
} | |
while ((ADC1->CR & ADC_CR_ADEN) != 0); | |
/* Clear DMAEN */ | |
ADC1->CFGR1 &= ~ADC_CFGR1_DMAEN; | |
/* Launch the calibration by setting ADCAL */ | |
ADC1->CR |= ADC_CR_ADCAL; | |
/* Wait until ADCAL=0 */ | |
while ((ADC1->CR & ADC_CR_ADCAL) != 0); | |
} | |
void app_adc_startup(){ | |
// read vref cal value from eeprom | |
vrefIntCal = *(uint16_t*)0x1FFFF7BA;// and 0x1FFFF7BB; | |
// read temperature cal from eeprom | |
tsCal1 = *(uint16_t*)0x1FFFF7B8;// and 0x1FF800F9; | |
// initialise DMA values | |
DMA1_Channel1->CPAR = (uint32_t)&(ADC1->DR); | |
DMA1_Channel1->CMAR = (uint32_t)adcBuffer; | |
DMA1_Channel1->CNDTR = BUFFER_SIZE; | |
DMA1_Channel1->CCR |= (DMA_CCR_HTIE | DMA_CCR_TCIE | DMA_CCR_EN); | |
// calibrate ADC | |
HAL_Delay(100); | |
app_adc_calibrate(); | |
// restore DMAEN after cal. | |
ADC1->CFGR1 |= ADC_CFGR1_DMAEN; | |
// start ADC | |
/* (1) Ensure that ADRDY = 0 */ | |
/* (2) Clear ADRDY */ | |
/* (3) Enable the ADC */ | |
/* (4) Wait until ADC ready */ | |
if ((ADC1->ISR & ADC_ISR_ADRDY) != 0) /* (1) */ | |
{ | |
ADC1->ISR |= ADC_ISR_ADRDY; /* (2) */ | |
} | |
ADC1->CR |= ADC_CR_ADEN; /* (3) */ | |
while ((ADC1->ISR & ADC_ISR_ADRDY) == 0); | |
// start timer 3 for trigger | |
TIM3->CR1 |= TIM_CR1_CEN; | |
// enable timer3 interrupt for sync output | |
TIM3->DIER |= TIM_DIER_UIE; | |
// trigger selection is only effective once adstart has been set | |
ADC1->CR |= ADC_CR_ADSTART; | |
} | |
int64_t accumulator = 0; | |
uint16_t counter = 0; | |
int64_t fundAccumulator = 0; | |
int64_t aflcAccumulator = 0; | |
uint64_t angleAccumulator = 0; | |
uint16_t safeWindowCounter = 0; | |
void app_adc_mainloop(){ | |
if (! readingFlag){ | |
for (int i = 0; i < NUM_CHANNELS; i++){ | |
readingAccumulator[i] += adcReading[i]; | |
readingCounter[i] += 1; | |
} | |
readingFlag = 1; | |
} | |
// this would normally be triggered from an independent timer, but for now, | |
// we will just use the ADC timer to trigger a reading | |
if (readingCounter[0] >= 250) { | |
for (int i = 0; i < NUM_CHANNELS; i++){ | |
avgReading[i] = readingAccumulator[i] / readingCounter[i]; | |
readingAccumulator[i] = 0; | |
readingCounter[i] = 0; | |
} | |
readingTimestamp = 0; | |
} | |
} | |
void app_adc_isr(){ | |
if(DMA1->ISR & DMA_ISR_HTIF1){ | |
if (abs(zeroCross - PHASE_LOCK_TARGET) < 2){ | |
GPIO_SET(TP1); | |
} | |
DMA1->IFCR |= DMA_IFCR_CHTIF1; | |
accumulateReadings(0); | |
} | |
if(DMA1->ISR & DMA_ISR_TCIF1){ | |
cycleClock++; | |
GPIO_CLR(TP1); | |
DMA1->IFCR |= DMA_IFCR_CTCIF1; | |
accumulateReadings(1); | |
calculateReadings(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment