Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
477 lines (346 sloc) 17.3 KB
/******************************************************************************
* Модуль: adc_stat.c
* Автор: Celeron (c) 2018
* Назначение: Математическая постобработка потока данных с АЦП.
******************************************************************************/
#include <stdio.h> // Подключить стандартные библиотеки Си
#include <stdint.h>
//#include <math.h>
#include "adc_stat.h" // прототипы локальных функций и типов
//============================================================================
// Макроопределения и Прототипы функций
//============================================================================
// Макросы математических функций
#define MIN(X, Y) (((X) < (Y)) ? (X) : (Y))
#define MAX(X, Y) (((X) > (Y)) ? (X) : (Y))
#define AVG(X, Y) (((X) + (Y))/2)
#define ABS(X) (((X) < 0 ) ? -(X) : (X))
#define SIGN(X) (((X) < 0 ) ? (-1) : (+1))
#ifndef NUMBER_OF_ARRAY_ITEMS
// Стандартный вспомогательный приемчик: вычисляет количество элементов в массиве.
// См. справку по "Препроцессору Си"... http://ru-wiki.org/wiki/%D0%9F%D1%80%D0%B5%D0%BF%D1%80%D0%BE%D1%86%D0%B5%D1%81%D1%81%D0%BE%D1%80_%D0%A1%D0%B8
#define NUMBER_OF_ARRAY_ITEMS( array ) ( sizeof( array ) / sizeof( *(array) ) )
#endif
//============================================================================
// Усреднение выборки "скользящим окном"
//============================================================================
// Настройка: Размер "скользящего окна", в степенях двойки (например, 2^7 = 128 значений)
// Рекомендуется выбирать "Размер Окна" равный:
// = 2^3 = 8 значений, если нужно увеличить разрядность АЦП на еще один псевдоразряд;
// = 2^7 и более, как можно больше - для лучшего сглаживания/усреднения...
// Но чем больше окно, тем медленнее динамика реакции на изменения входного сигнала! Поэтому нужен компромис усреднения-динамики...
// Так что, "Размер Окна" болше определяется скоростью семплирования АЦП... И итоговое Окно следуется расчитывать от "Максимальной Скорости Семплирования"/"Скорость изменения Входного сигнала":
// Например: 15000 SPS * 1мс = 15 значений, т.е. ближе всего 16 значений = 2^4
#define ADC_AVG_ARRAY_COUNT_2POW 4
// Примечание: настройка степенью "7", вместо итогового числа "128", здесь, применяется для оптимизации (далее, в коде используется "сдвиг" вместо полного "деления")...
#define ADC_AVG_ARRAY_COUNT (1 << ADC_AVG_ARRAY_COUNT_2POW)
// Проверка: Величина "окна" должна быть до 32768 элементов
#if ((ADC_AVG_ARRAY_COUNT_2POW) > 15)
#error "Ошибка: слишком большое окно! Значение ADC_AVG_ARRAY_COUNT_2POW должно быть выбрано из диапазона [1..15]"
#endif
//-------------------------------------
// "Циклический буфер" для значений "скользящего окна"
volatile int32_t ADC_AVG_MovingAverageArray[ ADC_AVG_ARRAY_COUNT ];
// Индекс последнего добавленного элемента в "циклический буфер"
volatile uint16_t ADC_AVG_LastElementIndex = 0;
// Счетчик предварительной Заполнености массива-буфера (функция ADC_AVG_GetMovingAverage начинает возвращать результаты только после полного заполнения Массива после Reset)
volatile uint16_t ADC_AVG_ArrayFillingComplete = ADC_AVG_ARRAY_COUNT;
// Временная: Сумма всех элементов в массиве
volatile int32_t ADC_AVG_ArrayTotalSum = 0;
// Статистика: Исторический Максимум (за все время с последнего Сброса)
volatile int32_t ADC_AVG_HistoricalMax = INT32_MIN;
// Статистика: Исторический Минимум (за все время с последнего Сброса)
volatile int32_t ADC_AVG_HistoricalMin = INT32_MAX;
//-------------------------------------
// Обнулить накопительный массив и переинициализировать все счётчики - начать стат.обработку заново
// (код реентерабельный)
void ADC_AVG_ResetArray(void)
{
// Установить признак "обнуление окна"
// (Замечу, что фактическое "обнуление выборки и перерасчет среднего" будет произведено уже в приоритетной функции ADC_AVG_IncludeSample...)
ADC_AVG_ArrayFillingComplete = ADC_AVG_ARRAY_COUNT;
// Примечание: в этой функции не нужно делать сброс глобальных переменных!
// ADC_AVG_ArrayTotalSum = 0;
// ADC_AVG_HistoricalMax = INT32_MIN;
// ADC_AVG_HistoricalMin = INT32_MAX;
// Индекс текущего элемента, также, сбрасывать не нужно!
// (Поясню: не важно с какого элемента заполнять циклический массив... А чем меньше данных меняется неатомарно и асинхронно - тем меньше конфликтов в параллельном коде.)
//ADC_AVG_LastElementIndex = 0;
}
//-------------------------------------
// Добавить очередной Замер к Выборке (скользящее окно сдвигается на единицу, вытесняя самую раннюю запись)
// (Внимание: код этой функции НЕ РЕЕНТЕРАБЕЛЬНЫЙ - запускать только из одного Потока!)
// (Внимание: код этой функции НЕ БЛОКИРУЮЩИЙ - допустимо вызывать из Обработчика Прерывания.)
//
// (Желательно, но уже не обязательно, т.к. код модифицирован для минимизации вероятности нарушения целостности данных:
// ранее, код этой функции должен был выполняться АТОМАРНО - запускаться из Потока с наивысшим приоритетом, или покрыть вызов Критической секцией! Желательно, вызывать эту функцию из Обработчика Прерывания.)
//
void ADC_AVG_IncludeSample(const int32_t value)
{
int32_t TotalSum; //Сумма массива
uint16_t ElementIndex; //Индекс последнего элемента
uint16_t FillingComplete; //Счетчик предварительной заполненности Окна
int32_t HistoricalMax; //Исторический Максимум
int32_t HistoricalMin; //Исторический Минимум
do
{
// Значения из глобальных переменных помещаются во временные ВНУТРЕННИЕ переменные (Важно: для обеспечения атомарной коррекции значений глобальных переменных.)
TotalSum = ADC_AVG_ArrayTotalSum; //Сумма массива
ElementIndex = ADC_AVG_LastElementIndex; //Индекс последнего элемента
FillingComplete = ADC_AVG_ArrayFillingComplete; //Счетчик предварительной заполненности Окна
HistoricalMax = ADC_AVG_HistoricalMax; //Исторический Максимум
HistoricalMin = ADC_AVG_HistoricalMin; //Исторический Минимум
// Фактическое обнуление окна (реализация отложенного сброса)
if(FillingComplete == ADC_AVG_ARRAY_COUNT)
{
TotalSum = 0;
// Сбросить статистику
HistoricalMax = INT32_MIN;
HistoricalMin = INT32_MAX;
}
// Циклически перемещаем "индекс элемента" на "самый ранний из выборки"
//ElementIndex = (ElementIndex + 1) % ADC_AVG_ARRAY_COUNT;
ElementIndex++;
ElementIndex %= ADC_AVG_ARRAY_COUNT;
// Перерасчет Суммы массива
if(FillingComplete == 0)
{
// Если "Сумма окна" действительна, то вычесть из нее "самый ранний элемент"...
TotalSum -= ADC_AVG_MovingAverageArray[ ElementIndex ];
}
else
{
// Уменьшаем счетчик, т.к. только что учли очередную выборку
FillingComplete--;
}
//...и прибавить "новый элемент"
TotalSum += value;
// "Новый элемент" переписывает "самый ранний элемент"
ADC_AVG_MovingAverageArray[ ElementIndex ] = value;
// Обновить Статистику
if(HistoricalMax < value)
HistoricalMax = value;
if(HistoricalMin > value)
HistoricalMin = value;
// Значения из временных внутренних переменных обновляют ГЛОБАЛЬНЫЕ переменные (Важно: помещать в глобальные переменные следует уже конечные расчитанные значения, без промежуточных расчетов - обеспечиваем атомарность расчета!)
ADC_AVG_ArrayTotalSum = TotalSum; //Сумма массива
ADC_AVG_LastElementIndex = ElementIndex; //Индекс последнего элемента
ADC_AVG_HistoricalMax = HistoricalMax; //Исторический Максимум
ADC_AVG_HistoricalMin = HistoricalMin; //Исторический Минимум
}
while((ADC_AVG_ArrayFillingComplete == ADC_AVG_ARRAY_COUNT) && (FillingComplete < (ADC_AVG_ARRAY_COUNT-1)));
//Проверка на АТОМАРНОСТЬ: Если во время выполнения тела функции, асинхронно был вызван ADC_AVG_ResetArray(), то тело функции перезапускается.
//(Тогда, на повторной итерации цикла, Массив и счетчики будут обнулены - и влияние предыдущего изменения данных будет нивелировано.)
ADC_AVG_ArrayFillingComplete = FillingComplete; //Эта глобальная переменная является своеобразным Флагом атомарности.
}
//-------------------------------------
// Вернуть текущее "среднее арифметическое по выборке", собранное методом "скользящего окна"
// (код реентерабельный)
int32_t ADC_AVG_GetMovingAverage(void)
{
// Примечание: хитрая реализация кода, в этой функции, обеспечивает "ленивую (неблокирующую) проверку" атомарности доступа к данным... ТАК НАДО!
// Значения из глобальных переменных помещаются во временные ВНУТРЕННИЕ переменные
int32_t TotalSum;
if(ADC_AVG_ArrayFillingComplete == 0)
{
//Сумма массива
TotalSum = ADC_AVG_ArrayTotalSum;
// Примечание: повторная проверка, здесь, позволяет исключить "гонки"
// (на случай если параллельный запуск ADC_AVG_ResetArray() перебьет ход выполнения между двумя последними инструкциями -
// и сбросит счетчик ADC_AVG_ArrayFillingComplete, деактуализировав значение TotalSum...)
if(ADC_AVG_ArrayFillingComplete == 0)
{
// Поскольку Массив уже заполнен, то выдаем подсчитанное СРЕДНЕЕ
return (TotalSum >> ADC_AVG_ARRAY_COUNT_2POW);
}
}
// Массив скользящего среднего еще не заполнился полностью - пока не могу выдать правильный результат, возвращаю нейтральное значение.
return 0;
}
//-------------------------------------
// Вернуть максимальное значение, ЗА ВСЁ ВРЕМЯ с последнего "сброса"
// (код реентерабельный)
extern __inline int32_t ADC_AVG_GetHistoricalMax(void)
{
return ADC_AVG_HistoricalMax;
}
//-------------------------------------
// Вернуть минимальное значение, ЗА ВСЁ ВРЕМЯ с последнего "сброса"
// (код реентерабельный)
extern __inline int32_t ADC_AVG_GetHistoricalMin(void)
{
return ADC_AVG_HistoricalMin;
}
//============================================================================
// Стат.оценка выборки
//============================================================================
// Статистика: "максимальное значение" по Текущей Выборке
volatile int32_t ADC_AVG_ArrayMax = INT32_MIN;
// Статистика: "минимальное значение" по Текущей Выборке
volatile int32_t ADC_AVG_ArrayMin = INT32_MAX;
// Статистика: "Стандартное отклонение" по Текущей Выборке
volatile int32_t ADC_AVG_ArrayStdDev = 0;
//-------------------------------------
// Вычисление квадратного корня из целочисленного числа
// Реализация: см. "Пример 7" из статьи http://www.codenet.ru/progr/alg/sqrt.php
uint32_t sqrt_int32(int32_t l)
{
int32_t temp;
uint32_t div, rslt = l;
if (l <= 0)
return 0;
else if (l & 0xFFFF0000L)
if (l & 0xFF000000L)
div = 0x3FFF;
else
div = 0x3FF;
else
if (l & 0x0FF00L)
div = 0x3F;
else
div = (l > 4) ? 0x7 : l;
while (1)
{
temp = l / div + div;
div = temp >> 1;
div += temp & 1;
if (rslt > div)
rslt = div;
else
{
if (l / rslt == rslt - 1 && l % rslt == 0)
rslt--;
break;
}
}
return rslt;
}
//-------------------------------------
// Рассчитать статистические показатели по текущей выборке в Массиве
// (код НЕ реентерабельный, синхронный и долгий - использовать только для тестов!)
void ADC_AVG_CalcArrayStatistics(void)
{
// Проверка: пока массив не заполнен - никакой статистики не расчитываем...
if(ADC_AVG_ArrayFillingComplete > 0)
return;
// Временные переменные
int32_t ArrayMax = INT32_MIN;
int32_t ArrayMin = INT32_MAX;
int32_t ArrayAvg = ADC_AVG_GetMovingAverage();
uint32_t ArrayStdDev = 0;
int32_t value;
// Расчет статистики
for(uint16_t i=0; i<ADC_AVG_ARRAY_COUNT; i++)
{
value = ADC_AVG_MovingAverageArray[i];
if(ArrayMax < value)
ArrayMax = value;
if(ArrayMin > value)
ArrayMin = value;
ArrayStdDev += (ArrayAvg - value)*(ArrayAvg - value);
}
ArrayStdDev >>= ADC_AVG_ARRAY_COUNT_2POW;
ArrayStdDev = sqrt_int32(ArrayStdDev);
// Сохранить Статистику
ADC_AVG_ArrayMax = ArrayMax;
ADC_AVG_ArrayMin = ArrayMin;
ADC_AVG_ArrayStdDev = ArrayStdDev;
}
//-------------------------------------
// Получить "максимальное значение" по Текущей Выборке (долгая процедура)
// (код реентерабельный)
extern __inline int32_t ADC_AVG_GetArrayMax(void)
{
return ADC_AVG_ArrayMax;
}
//-------------------------------------
// Получить "минимальное значение" по Текущей Выборке (долгая процедура)
extern __inline int32_t ADC_AVG_GetArrayMin(void)
// (код реентерабельный)
{
return ADC_AVG_ArrayMin;
}
//-------------------------------------
// Получить "Стандартное отклонение" по Текущей Выборке (долгая процедура)
// (код реентерабельный)
extern __inline int32_t ADC_AVG_GetArrayStdDev(void)
{
return ADC_AVG_ArrayStdDev;
}
//============================================================================
// Калибровка и Конверсия в единицы измерения прикладной величины
//============================================================================
// Коэффициент: смещение нуля (в единицах измерения: дельта кода АЦП)
volatile int32_t ADC_CNV_OffsetCorrection = 0;
// Коэффициент: масштабирования и преобразования (в единицах измерения: "кодов АЦП" на "единицу реальной величины")
volatile int32_t ADC_CNV_FullscaleCorrection = 0;
//-------------------------------------
// Рассчитать коэффициенты калибровки (и сохранить во внутренние переменные)
// 1шаг) замкнуть Щуп на Землю - показания АЦП сунуть сюда...
extern __inline void ADC_CNV_RecalculateOffsetCoefficient (const int32_t ZeroCode)
{
ADC_CNV_OffsetCorrection = ZeroCode;
}
// 2шаг) подать на Щуп Образцовое напряжение (например 300V) относительно Земли - замер с АЦП и соответствующий эталон в Реальных единицах, сунуть сюда.
extern __inline void ADC_CNV_RecalculateFullscaleCoefficient(const int32_t SampleCode, const int32_t SampleReal)
{
ADC_CNV_FullscaleCorrection = (SampleCode - ADC_CNV_OffsetCorrection) / SampleReal;
}
//-------------------------------------
// Сохранение/восстановление коэффициентов в энергонезависимой памяти
//Получить значение внутренних переменных, хранящих коэффициенты (с целью сохранения во внешней энергонезависимой памяти, например)
extern __inline void ADC_CNV_GetCalibrationCoefficient(int32_t* Offset, int32_t* Fullscale)
{
*Offset = ADC_CNV_OffsetCorrection;
*Fullscale = ADC_CNV_FullscaleCorrection;
}
//Установить значения внутренних переменных, хранящих коэффициенты (с целью восстановления ранней калибровки при включении устройства, например)
extern __inline void ADC_CNV_SetCalibrationCoefficient(const int32_t Offset, const int32_t Fullscale)
{
ADC_CNV_OffsetCorrection = Offset;
ADC_CNV_FullscaleCorrection = Fullscale;
}
//-------------------------------------
// Конверсия измеренного значения: из кода АЦП в реальные единицы (предназначена для "абсолютных величин")
// Примечание: эта функция предназначена только для преобразования "абсолютных величин", типа замеров АЦП.
// Но эта функция неприменима для преобразования "дельт величин" (типа "стандартного отклонения"), поскольку применяет к величине "коррекцию абсолютной погрешности" (которая присутствует только в абсолютных величинах)!
// (код реентерабельный)
extern __inline int32_t ADC_CNV_ConvertCode2Real(const int32_t ValueCode)
{
return (ValueCode - ADC_CNV_OffsetCorrection) / ADC_CNV_FullscaleCorrection;
}
// Конверсия измеренного значения: из реальных единиц в код АЦП (предназначена для "абсолютных величин")
// Примечание: не знаю, зачем это может понадобиться?..
// Внимание: дает результат пониженной точности! (поскольку исходное разрешение 24-битного АЦП выше)
// (код реентерабельный)
extern __inline int32_t ADC_CNV_ConvertReal2Code(const int32_t ValueReal)
{
return ADC_CNV_FullscaleCorrection * ValueReal + ADC_CNV_OffsetCorrection;
}
//-------------------------------------
// Конверсия измеренного значения: из кода АЦП в реальные единицы (предназначена для "дельта величин")
// Примечание: эта функция предназначена только для "дельта величин", поскольку применяет к ним только "коррекцию относительной погрешности". Можно применять ее также для преобразования "стандартного отклонения"...
// (код реентерабельный)
extern __inline int32_t ADC_CNV_ConvertDeltaCode2Real(const int32_t DeltaValueCode)
{
return DeltaValueCode / ADC_CNV_FullscaleCorrection;
}
// Конверсия измеренного значения: из реальных единиц в код АЦП (предназначена для "дельта величин")
// Примечание: не знаю, зачем это может понадобиться?..
// Внимание: дает результат пониженной точности! (поскольку исходное разрешение 24-битного АЦП выше)
// (код реентерабельный)
extern __inline int32_t ADC_CNV_ConvertDeltaReal2Code(const int32_t DeltaValueReal)
{
return ADC_CNV_FullscaleCorrection * DeltaValueReal;
}
//============================================================================
// Служебные методы
//============================================================================
//-------------------------------------
// Инициализация модуля
// Параметры: Offset и Fullscale - значения калибровочных коэффициентов, восстановленные из энергонезависимой памяти
void ADC_AVG_Init(const int32_t Offset, const int32_t Fullscale)
{
ADC_AVG_ResetArray();
ADC_CNV_SetCalibrationCoefficient(Offset, Fullscale);
}