Skip to content

Instantly share code, notes, and snippets.

@mp035
Created February 24, 2021 23:56
Show Gist options
  • Save mp035/09b4cf7e82c6f59ccf0363632726a375 to your computer and use it in GitHub Desktop.
Save mp035/09b4cf7e82c6f59ccf0363632726a375 to your computer and use it in GitHub Desktop.
STM32F030F4P6 App ADC Code
/*
* 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