Skocz do zawartości
Komentator

Zdobądź Proxxona FBS 240/E - #4 - Uniwersalna funkcja

Pomocna odpowiedź

Odpowiedzią jest jak zwykle datasheet procesora i wykresy pokazujące pracę przetwornika. Ja robię tak bardzo często: puszczam przetwornik z pełną prędkością (regulowaną zegarem procesora i bitami ADPS2..0) a w każdym przerwaniu uaktualniam multiplekser wg przyjętej metody skanowania kanałów. Nie zawsze sekwencja wygląda prosto 0, 1, 2, 3 itd, bo np. niektóre wejścia muszę próbkować często (np. sygnały audio) a inne znacznie rzadziej (np. temperatura). Wtedy robię licznik modulo długość całej sekwencji a jej szczegóły (numery kanałów) wpisuję do jakiejś tablicy umieszczonej na stałe np. w pamięci kodu. Szkoda RAMu gdy porządek skanowania jest zawsze taki sam. No i teraz najciekawsze pytanie: jaki wynik odczytuję? Zawsze z kanału wybranego MUXem sprzed dwóch przerwań. Wynika to wprost z zasady działania przetwornika pracującego w trybie free-run: multiplekser jest w rzeczywistości przełączany dopiero w momencie startu nowego pomiaru (zdarzenie "MUX & REFS update" na wykresach czasowych) i dokładnie w tym samym momencie dostajesz przerwanie. Tak więc procesor jest zawsze o jedno przerwanie spóźnony względem tego na co przełączony jest MUX a ADC oddaje wynik z jeszcze poprzedniego cyklu przetwarzania. Jeśli więc przykładowa tablica sekwencji skanowania kanałów będzie wyglądała następująco:

0,7,1,7,2,7,3,7..

czyli mamy 4 wolnozmienne wejścia 0, 1, 2 i 3 oraz kanał 7 mierzony co 2 cykle ADC, to dla kolejnych przerwań będziesz dostawał wyniki z kanałów:

3,7,0,7,1,7,2,7..

Wpisując w przerwaniu nowy adres MUXa odczytany z tablicy sekwencji, aby dowiedzieć się jaki wynik właśnie dostałeś możesz tablicę zaadresować licznikiem pomniejszonym o 2 (uwaga na przejście przez 0) lub zrobić sobie drugi równolegle pracujący licznik spóźniony o 2, pokazujący co się akurat odczytuje.

Metoda jest o tyle fajna, że masz deterministyczne działanie całego układu pomiarowego. Żadne opóźnienia programowe nie wpływają na cykl ADC co zmniejsza szumy sygnału powodowane jitterem czasu próbkowania.

Udostępnij ten post


Link to post
Share on other sites

marek1707, dzięki za zastrzyk wiedzy.

Wiem, że w nocie są te wykresy jednak nigdy nie uważałem, że w Linefollowerze czy Minisumo programowe wyzwalanie kolejnych pomiarów stanowi jakiś problem, a wg. mnie kod jest bardziej przejrzysty. W przypadku pomiarów audio jak najbardziej ma to sens.

Ale dla tych co chcieliby wykorzystać pełnię możliwość przetwornika ADC wstawiam kod uwzględniający powyższe uwagi.

Poniższy kod jest przeznaczony dla mikrokontrolerów ATmega innych niż: ATmega8, ATmega8A, ATmega128, ATmega406. Z oczywistych względów nie będzie działał tez na mikrokontrolerach, które nie są wyposażone w ADC.

void Init_ADC()
{
ADMUX |= (1<<REFS0);				//VCC jako napięcie odniesienia z pinem AREF podłączonym do GND przez kondensator
ADMUX |= (1<<ADLAR);				//przesunięcie wyniku do lewej -> 8-bitowy odczyt wyniku
ADCSRA |= (1<<ADPS2) | (1<<ADPS1);	//prescaler = 64 -> przy 16MHz daje to 250kHz dla ADC
ADCSRA |= (1<<ADIE);				//włączenie przerwań od ADC
ADCSRA |= (1<<ADATE);				//włączenie Free Runining mode *przy domyślnej wartości rejestru SFIOR
ADCSRA |= (1<<ADEN);				//włączenie przetwornika ADC
ADCSRA |= (1<<ADSC);				//start konwersji
sei();								//globalne zezwolenie na przerwania
}

ISR(ADC_vect)
{
static uint8_t i;					
reading[i] = ADCH;	
ADMUX = (ADMUX & 0b11111000) | ((i+2)%SENSORS);
i++;
i%=SENSORS;
}

Wojciech, drobna uwaga. Nie ma już bitu ADFR. Jest bit ADATE, który włącza wyzwolenie konwersji sygnałem wybieranym w rejestrze SFIOR. Domyśla wartość w tym rejestrze to Free Running mode, czyli nic więcej nie trzeba ustawiać.

W przerwaniu ustawiam MUX'a większego o 2 w stosunku do licznika elementu tablicy (IMO łatwiej ogarnąć przejście przez zero). Należy tylko zwrócić uwagę, że drugi pomiar będzie błędny. Pozwoliłem sobie to tak zostawić bo zazwyczaj od uruchomienia kontrolera do startu robota mija co najmniej sekunda, a w tym czasie błąd zostanie nadpisany prawidłowymi wartościami.

UWAGA! Kodu nie testowałem na mikrokontrolerze, bo nie mam już ATmeg. Jakby ktoś miał chwilę to proszę go sprawdzić.

Udostępnij ten post


Link to post
Share on other sites
Wojciech, drobna uwaga. Nie ma już bitu ADFR. Jest bit ADATE, który włącza wyzwolenie konwersji sygnałem wybieranym w rejestrze SFIOR. Domyśla wartość w tym rejestrze to Free Running mode, czyli nic więcej nie trzeba ustawiać.

To zależy od użytego mikrokontrolera. Nie napisałeś dla jakiego modelu jest ten kod, więc założyłem, że dla ATmega8 (bo jest najpopularniejsza), a w niej takowy bit występuje. Warto by więc napisać, że Twój kod jest dla ATmeg innych niż: ATmega8, ATmega8A, ATmega128, ATmega406 i oczywiście wszystkich tych, które nie mają ADC.

Udostępnij ten post


Link to post
Share on other sites

Witam.

Przedstawiam klasę obsługującą pada od Play Station 2 (napisaną na podstawie kodu stąd). Jest możliwość wyboru interfejsu SPI software'owego lub hardware'owego. Klasa powinna nadać się do użytku na większości AtMeg.

Jak korzystać? W nagłówku klasy odpowiednio poustawiać numery pinów i nazwy rejestrów w define'ach, a następnie w kodzie:

#include "Pad.h"
[...]
Pad pad = Pad();

pad.init();
pad.setMode(Pad::Analog);

while(1) {
   _delay_ms(50);

   pad.poll();

   if(pad.buttonPressed(Pad::R1)) {
       [...]
   }

   uint8_t value = pad.analogValue(Pad::LeftHorizontal);

   [...]
}

Nagłówek:

#ifndef __PAD_H__
#define __PAD_H__

#include <avr/io.h>

// DATA - input
#define DATA_DIR_IN DDRB &= ~(1<<4)
#define DATA_IN     (PINB&(1<<4))

// CMD - output
#define CMD_SET     PORTB |= (1<<3)
#define CMD_CLR     PORTB &= ~(1<<3)
#define CMD_DIR_OUT DDRB |= (1<<3)

// ATT - output
#define ATT_SET     PORTB |= (1<<2)
#define ATT_CLR     PORTB &= ~(1<<2)
#define ATT_DIR_OUT DDRB |= (1<<2)

// CLK - output
#define CLK_SET     PORTB |= (1<<5)
#define CLK_CLR     PORTB &= ~(1<<5)
#define CLK_DIR_OUT DDRB |= (1<<5)

// Short delay
#define _NOP_ asm volatile("nop\n\t""nop\n\t" "nop\n\t" "nop\n\t" ::)

// Comment below line to use software SPI
#define ATMEGA_SPI

class Pad
{

public:
Pad();

enum modes {
	Analog = 0
};

enum buttons {
	L1            = 42,
	L2            = 40,
	R1            = 43,
	R2            = 41,
	Up            = 34,
	Down          = 36,
	Left          = 37,
	Right         = 35,
	Select        = 30,
	Start         = 33,
	JotstickLeft  = 32,
	JotstickRight = 31,
	Triangle      = 44,
	Circle        = 45,
	Cross         = 46,
	Square        = 47
};

enum analogs {
	LeftHorizontal  = 7,
	LeftVertical    = 8,
	RightHorizontal = 5,
	RightVertical   = 6
};

void init();
void setMode(int);

void poll();
bool buttonPressed(int);
uint8_t analogValue(int);

protected:
unsigned char pollData[9];

void setAnalogMode();

unsigned char transferByte(unsigned char);
void transfer(unsigned char[], unsigned char);
};

#endif //__PAD_H__

Źródło:

#include "Pad.h"
#include <util/delay.h>

Pad::Pad()
{

}

/**
* Setups SPI
*
* @return void
*/
void Pad::init()
{
CMD_DIR_OUT;
ATT_DIR_OUT;
CLK_DIR_OUT;
ATT_SET;
CLK_SET;
CMD_SET;

#ifdef ATMEGA_SPI
	SPCR =(1<<SPE)|(1<<MSTR)|(1<<DORD)|(1<<CPOL)|(1<<CPHA)|(1<<SPR0)|(1<<SPR1);
#endif
}

/**
* Retrieves data from pad and store result
*
* @return void
*/
void Pad::poll()
{
this->pollData[0] = 0x01;
this->pollData[1] = 0x42; // Command 0x42 - Controller poll
this->pollData[2] = 0x00;

// If error occur, for example pad will not be connected with receiver,
// 0x80 (128) value wont change - change it if needed
this->pollData[3] = 0x80; // buttons
this->pollData[4] = 0x80; // buttons
this->pollData[5] = 0x80; // right horizontal joystick
this->pollData[6] = 0x80; // right vertical joystick
this->pollData[7] = 0x80; // left horizontal joystick
this->pollData[8] = 0x80; // left vertical joystick

// Send command and store poll result to pollData
this->transfer(this->pollData, 9);
}

/**
* Gets state of button.
*
* @param int|Pad::buttons button Buttons enum
* @return bool
*/
bool Pad::buttonPressed(int button)
{
// In which byte we should search bit
int byte = button / 10;
// Which bit is responsible for pad button
int bit = button % 10;

// Pressed button results bit equals 0
return ((this->pollData[byte] & (1<<bit)) >> bit) == 0;
}

/**
* Gets analog value of joystick.
*
* @param int|Pad::analogs analog Analogs enum
* @return uint8_t
*/
uint8_t Pad::analogValue(int analog)
{
return this->pollData[analog];
}

#ifdef ATMEGA_SPI

/**
* Sends/reads one byte to/from pad.
* Uses hardware SPI
*
* @param uchar byte Byte to transfer
* @return uchar
*/
unsigned char Pad::transferByte(unsigned char byte)
{
SPDR = byte;

while(!(SPSR & (1<<SPIF)));

_delay_us(20);

return SPDR;
}

#else

/**
* Sends/reads one byte to/from pad.
* Uses software SPI
*
* @param uchar byte Byte to transfer
* @return uchar
*/
unsigned char Pad::transferByte(unsigned char byte)
{
unsigned char i,r=0;

for(i=0; i<8; i++, byte>>=1) {
	CLK_CLR;
	if(byte& 0x01) CMD_SET; else CMD_CLR;
	_NOP_;
	CLK_SET;
	r>>=1;
	if(DATA_IN) r|=0x80;
}

CMD_SET;

_delay_us(20);

return r;
}

#endif

/**
* Sends/reads n bytes to/from pad.
*
* @param uchar bytes Bytes to transfer
* @param uchar size Size of bytes
* @return void
*/
void Pad::transfer(unsigned char bytes[],unsigned char size)
{
unsigned char i;

ATT_CLR;

for(i=0; i < size; i++) {
	bytes[i] = this->transferByte(bytes[i]);
}

ATT_SET;

_delay_us(50);
}

/**
* Sets pad mode
*
* @param int|Pad::modes mode Modes enum
* @return void
*/
void Pad::setMode(int mode)
{
switch(mode) {
	case Pad::Analog:
		this->setAnalogMode();
		break;

	default:
		break;
}
}

/**
* Sets analog mode
*
* @return void
*/
void Pad::setAnalogMode()
{
unsigned char data[9];

data[0]= 0x01;
data[1]= 0x43; // Command 0x43 - Go into configuration mode
data[2]= 0x00;
data[3]= 0x01;
data[4]= 0x00;

this->transfer(data, 5);

data[0]= 0x01;
data[1]= 0x44; // Command 0x44 - Turn on analog mode
data[2]= 0x00;
data[3]= 0x01;
data[4]= 0x03;
data[5]= 0x00;
data[6]= 0x00;
data[7]= 0x00;
data[8]= 0x00;

this->transfer(data, 9);

data[0] = 0x01;
data[1] = 0x43;  // Command 0x43 - Exit config mode
data[2] = 0x00;
data[3] = 0x00;
data[4] = 0x5A;
data[5] = 0x5A;
data[6] = 0x5A;
data[7] = 0x5A;
data[8] = 0x5A;

this->transfer(data, 9);
}

Udostępnij ten post


Link to post
Share on other sites

Podczas realizacji kilku projektów napisałem parę drobnych funkcji do mi. "bezuderzeniowej" zmiany SP dla regulatora PID oraz łagodnego wysterowania silników. Są one na tyle uniwersalne, że wykorzystuję je w większości swoich aplikacji 🙂

Funkcje te realizują inercję 1 i 2 rzędu, "rampę", zaokrąglanie do wybranego miejsca po przecinku oraz jako gratis dodaję popularne uśrednianie, które kiedyś wygrzebałem z internetów (nie wiem jak to się nazywa po polsku, ale nazwa z angielskiego to Rolling Average - działa bardzo fajnie i szybko, nie wymagając przy tym przechowywania długich tablic ze zmiennymi).

No ale do rzeczy:

Zaokrąglanie do określonego miejsca po przecinku:


#include <math.h>  //wymagana biblioteka do round(x)

[...]

double round_to_precision(double x, int precision)
{
x=x*precision;
x=round(x);
x=x/precision;
return x;
}

double x - zmienna którą chcemy zaokrąglić,

int precision - miejsce po przecinku do którego zaokrąglamy:

precision=10 - zaokrąglenie do 0.1

precision=100 - zaokrąglenie do 0.01

Uśrednianie (filtr uśredniający w oknie):

float approxRollingAverage (float avg, float input, int Filter_Size) {
avg -= avg/Filter_Size;
avg += input/Filter_Size;
return avg;
}

float avg - średnia,

float input - nowa zmienna, która ma zostać uśredniona,

int Filter_Size - długość okna filtra uśredniającego.

Inercja 1 rzędu:

Inercji I rzędu możemy użyć w celu łagodnego przejścia z jednej wartości do innej, np. przy zmienie PWM (SP) dla silników. Stosując tę funkcję możemy uniknąć gwałtownej zmiany wartości rejestru PWM (SP), ponieważ w każdej chwili czasowej (określonej przez czas próbkowania) funkcja będzie zwiększać swoją wartość od wartości początowej (np. nasze stare sterowanie silnikiem) do wartości końcowej (nowe sterowanie silnikiem). Szybkość zmiany ze starej do nowej wartości określa stała czasowa T (im jest mniejsza tym szybciej funkcja dojdzie do nowej wartości). Inercja jest pewnego rodzaju filtrem (można ją stosować też do sygnałów pomiarowych) ale, co najważniejsze, wprowadza opóźnienie do układu co oznacza, że nie zawsze chcemy ją stosować.

float inercja_1(float x,float T,float y_prev, float Tp)
{
//Inercja 1 rzedu: G(s)=1/(Ts+1)
float y=0;
y=(x*Tp+y_prev*T)/(T+Tp);
return y;
}

float x - wartość, którą ma osiągnąć inercja,

float T - stała czasowa inercji,

float y_prev - poprzednia wartość inercji,

float Tp - czas próbkowania.

Inercja 2 rzędu:

Inercja 2 rzędu pełni tę samą rolę co inercja 1 rzędu, różnią się "drogą" przejścia ze starej do nowej wartości (np. PWM (SP) dla silnika), oraz tym, że inercja 2 rzędu posiada większe opóźnienie, które wprowadza do naszego układu.

float inercja_2(float x,float T,float y_prev, float y_prev2, float Tp)
{
//Inercja 2 rzedu: G(s)=1/(Ts+1)^2
float y=0;

y=(Tp*Tp*x+y_prev*2*(T*T+T*Tp)-y_prev2*T*T)/(T*T+2*T*Tp+Tp*Tp);

return y;
}

float x - wartość, którą ma osiągnąć inercja,

float T - stała czasowa inercji,

float y_prev - poprzednia wartość inercji (w chwili k-1, gdzie k to "teraźniejszość"),

float y_prev2 - poprzednia, poprzednia wartość inercji (w chwili k-2),

float Tp - czas próbkowania.

W sieci można znaleźć dużo przykładów odpowiedzi inercji na wymuszenie skokowe (wykresów jak inercja reaguje na wprowadzenie skokowej zmiany np. dla pożądanego PWM silnika). Całkiem niezłym przykładem jest slajd z materiałów dydaktycznych AGH:

Na wykresie widać, w jaki sposób będzie zmieniać się nasza wartość.

PODSUMOWUJĄC:

Do funkcji inercja_1, inercja_2 lub ramp (omówione dalej) wprowadzamy na jaką wartość chcemy zmienić naszą zmienną, a funkcja sama "zadba" żeby przeprowadzić tę zmienną z wartości początkowej do wartości końcowej gładko i przyjemnie, bez gwałtownych skoków. Zmiana wartości zmiennej nastąpi stopniowo, wraz z kolejnymi iteracjami programu. Oczywiście należy poeksperymentować z doborem parametrów T i Tp.

Rampa:

Rampa służy w tym samym celu co inercja 1 i 2 rzędu, jest powszechnie stosowana (np. w falownikach (przemiennikach częstotliwości) do zmiany SP dla silników), ze względu na swoją prostotę i niewielki koszt obliczeniowy.

Funkcja będzie realizować przejście pomiędzy starą i nową zmienną w sposób jak na wykresie [źródło: http://silniki-krokowe.com.pl]:

Na przedstawionym wykresie zmieniono prędkość z Vmin do Vmax a następnie z Vmax do Vmin.

float ramp(float y_prev, float Steep_param, float Ramp_value)
{
float y=0;

y=y_prev+Steep_param*(Ramp_value-y_prev);

if ((Ramp_value-y_prev)>0)
{
	if (y>Ramp_value)
	{
		y=Ramp_value;
	}
}
if ((Ramp_value-y_prev)<0)
{
	if (y<Ramp_value)
	{
		y=Ramp_value;
	}
}

return y;
}

float y_prev - poprzednia wartość rampy,

float Steep_param - parametr określający stromość rampy: od 0.0001 do 1,

float Ramp_value - wartość, którą ma osiągnąć rampa.

Funkcje oparte o czas próbkowania do prawidłowego działania potrzebują być wyzwalane w określonych przez Tp odstępach czasu. Można to zrealizować za pomocą przerwań.

__________

Komentarz dodany przez: Treker

Udostępnij ten post


Link to post
Share on other sites

corsair, dzięki za zgłoszenia, jednak do 3 ostatnich funkcji przydałby się dokładniejszy opis. Nie każdy zrozumie, co tam się dzieje po samej nazwie funkcji. Do tego w przypadku ostatniej funkcji nie masz żadnych komentarzy.

Udostępnij ten post


Link to post
Share on other sites

@corsair: bardzo przydatne te Twoje funkcje, ale byłyby jeszcze lepsze gdyby to samo robiły na zmiennych stałoprzecinkowych - wtedy obliczenia idą szybciej.

Udostępnij ten post


Link to post
Share on other sites

Oczywiście, ale do swoich aplikacji używam floatów, lepiej mi się z nimi pracuje i do mojego zastosowania nie są ogranicznieniem, a jak ktoś chce to może zmienić float na uint czy coś 🙂

Udostępnij ten post


Link to post
Share on other sites

Rolling average to po naszemu średnia krocząca 🙂

Udostępnij ten post


Link to post
Share on other sites

Treker czy będzie jakaś baza odpowiedzi konkursowych np. baza nadesłanych kodów i schematów wzorcowych? Wszystko zebrane w jednym miejscu, żeby łatwiej było znaleźć

Udostępnij ten post


Link to post
Share on other sites

shaslyk135, tak jest plan, aby później zebrać rozwiązania od użytkowników, którzy wyrażą na to zgodę i stworzyć z tego artykuły.

Udostępnij ten post


Link to post
Share on other sites

Z mojej strony zestaw podwójny, robiący to samo - tylko, że jeden w C, drugi C++ z wykorzystaniem klas.

voltage_calc.h

#ifndef _VOLT_CAL_H_
#define _VOLT_CAL_H_

#endif

#include <stdint.h>

void VoltageCalcInit(uint32_t mul, uint32_t div, uint32_t Vcc, uin32_t ADCmax, uint32_t initialValue);
uint8_t VoltageCalcUpdate(uint32_t adcValue);
uint32_t VoltageCalcGet(void);

voltage_calc.c

/*
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

Name of the author of this software will be mentioned in any source file where 
part, or whole, of this code is used. 

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/*
*	Author: M. Matuszak
*	Date: 2015-06-05
*/
#include <stdint.h>
#include "voltage_calc.h"

uint32_t	calcMul = 10, calcDiv = 1;
uint32_t	calcVcc = 0, calcADCmax = 1023;
uin16_t		calcSums = 16, calcLastDiv = 512;
uin32_t		calcBuffer = 0, calcValue = 0, calcCounter = 0;

/*
*	Init library. Define Division value of Eg. resistor divider by (mul/div), reference voltage (Vcc) and maximum ADC value
* 	Errors (due to inaccuracy of components) can be best eliminated by calibration of Vcc.
*/
void VoltageCalcInit(uint32_t mul, uint32_t div, uint32_t Vcc, uin32_t ADCmax, uint32_t initialValue)
{
calcMul = mul;
calcDiv = div;
calcVcc = Vcc;
calcADCmax = ADCmax;

//below gives No of sums: (Vcc*MUL) / (ADCmax*DIV)
calcSums = Vcc*mul/div/ADCmax;	//calculate how many times adc value should be added
//calculate last addition od 8 bit shifted value
//- (calcSums>>1) is added, because rounding  is down, and should be to the closest
calcLastDiv = ((ADCmax*div)<<8) / (Vcc*mul-calcSums*ADCmax*div) - (calcSums>>1);

calcValue = initialValue;
}

/*
*	Add next ADC measurement to buffer. Update value if ready. 
*/
uint8_t VoltageCalcUpdate(uint32_t adcValue)
{
calcBuffer += adcValue;
calcCounter++;
if(calcCounter >= calcSums)
{
	calcValue = calcBuffer + (adcValue<<8)/calcLastDiv;
	calcBuffer = 0;
	calcCounter = 0;
	return 1;
}
return 0;
}

/*
*	Get measured value
*/
uint32_t VoltageCalcGet(void)
{
return calcValue;
}

Biblioteka ta pozwala na łatwe oraz dokładne mierzenie napięcia do wartości wygodnej. Wykorzystuje zjawisko "oversampling", dzięki czemu dokładność rośnie.

Funkcje inicjuje się za pomocą funkcji VoltageCalcInit. Należy do niej podać wartość dzielnika (np. rezystancyjneg), czyli ile razy napięcie wejściowe jest wyższe od mierzonego wg wzoru: mul/div.

Należy również podać napięcie referencyjne ADC (Vcc) oraz maksymalną wartość przyjmowaną przez przetwornik (np. dla 10b może być 1023). Wartość początkowa służy uniknięciu sytuacji, w której przy odczycie pomiaru przed zakończeniem pierwszej serii, byłaby obecna wartość '0'.

Uaktualnianie następuje poprzez wpisanie funkcją VoltageCalcUpdate() wyniku pomiaru z przetwornika. Odczyt poprzez VoltageCalcGet(); Po wykonaniu odpowiedniej ilości pomiarów, wartość pomiarowa jest uaktualniana a bufory czyszczone.

Zalety:

- duża rozdzielczość. Np. dla zakresu do 30V można uzyskać rzędu kilku-kilkunastu mV.

- większa dokładność niż w przypadku zwykłego mnożenia pojedynczego pomiaru.

- łatwość użycia,
- modularność,
- niezależny od architektury układu

Wady:

- opóźnienie w wykonaniu pomiaru,

- potrzeba więcej pamięci niż w przypadku mnożenia jednego wyniku,

- tylko jedno wejście pomiarowe. Tą wadę eliminuje wersja w formie klasy.

Przy okazji: dopisałem licencję (o której ludzie tutaj zapominają). Bez obaw, jest mniej restrykcyjna nawet niż MIT - pozwala właściwie chyba na wszystko.

[ Dodano: 13-06-2015, 13:19 ]

I druga część. To samo co powyżej, ale w formie klasy.

AdcCalc.hpp

/*
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

Name of the author of this software will be mentioned in any source file where 
part, or whole, of this code is used. 

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/*
*	Author: M. Matuszak
*	Date: 2015-06-05
*/
#include <stdint.h>

#ifndef _ADC_CALC_H_
#define _ADC_CALC_H_

class AdcCalc
{
uint32_t	Mul = 11, Div = 1;
uint32_t	Vcc = 3300, ADCmax = 1023;
uin16_t		Sums = 16, LastDiv = 512;
uin32_t		Buffer = 0, Value = 0, Counter = 0;

public:
uint8_t Update(uint32_t adcValue);
uint32_t Get(void);
/*
*	Init library. Define Division value of Eg. resistor divider by (mul/div), reference voltage (Vcc) and maximum ADC value
* 	Errors (due to inaccuracy of components) can be best eliminated by calibration of Vcc.
*/
AdcCalc(uint32_t mul = 11, uint32_t div = 1, uint32_t initialValue = 12000, uint32_t Vref = 3300, uin32_t ADCmaxValue = 1023): Mul(mul), Div(div), Vcc(Vref), ADCmax(ADCmaxValue)
{
	//below gives No of sums: (Vcc*MUL) / (ADCmax*DIV)
	Sums = Vcc*mul/div/ADCmax;	//calculate how many times adc value should be added
	//calculate last addition of 8 bit shifted value
	//- (Sums>>1) is added, because rounding  is down, and should be to the closest
	LastDiv = ((ADCmax*div)<<8) / (Vcc*mul-Sums*ADCmax*div) - (Sums>>1);

	Value = initialValue;
}
}
#endif

AdcCalc.cpp

/*
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

Name of the author of this software will be mentioned in any source file where 
part, or whole, of this code is used. 

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/*
*	Author: M. Matuszak
*	Date: 2015-06-05
*/

#include <stdint.h>
#include "AdcCalc.hpp"

/*
*	Add next ADC measurement to buffer. Update value and return 1 if ready.
*/
uint8_t AdcCalc::Update(uint32_t adcValue)
{
Buffer += adcValue;
Counter++;
if(Counter >= Sums)
{
	Value = Buffer + (adcValue<<8)/calcLastDiv;
	Buffer = 0;
	Counter = 0;
	return 1;
}
return 0;
}

/*
*	Get measured value
*/
uint32_t AdcCalc::Get(void)
{
return Value;
}

Biblioteka robi dokładnie to samo, ale umożliwia wykorzystywanie dla wielu pomiarów na raz. Reguła jest ta sama: podajemy "przekładnię" wartości mierzonej na napięcie, maksymalną wartość napięcia w wybranej jednostce (polecam mV - np. 3300 dla 3.3V) itd.

Przykładowo aby zainicjować pomiary napięcia i prądu:

AdcCalc napiecie(11, 1, 12600, 3300, 1023);

AdcCalc prad(20, 1, 0, 3300, 1023); //rezystor pomiarowy 50m Ohm

Przy czym nie wszystkie wartości podczas inicjowania trzeba wpisywać, gdyż C++ dopuszcza domyślne wartości danych wejściowych.

I tak samo można np. obsługiwać czujniki itp. działające proporcjonalnie.

Uaktualnianie:

napiecie.Update(adc1);

prad.Update(adc2);

Pobieranie za pomocą funkcji Get.

Ogromną zaletą jest to, że można biblioteki wykorzystać do bardzo wielu wejść pomiarowych - ogranicza nas tylko pamięć, ilość tych wejść oraz czas obsługi.

Zalety:

- ogromna elastyczność

- uniwersalność

- czytelność kodu

- bezpieczniejsze niż wersja w C jeśli chodzi o ochronę zmiennych. Trudniej popsuć.

Wady:

- nieco wolniejsze działanie

- nie każde środowisko dla mikrokontrolerów obsługuje C++

- nieco więcej pamięci przy wykorzystaniu dla tylko jednego kanału

Poza tym wady i zalety jak w poprzedniej wersji.

Komentarz do obu wersji:

- dokładność dzielnika ostatniej wartości jest ustalana z operatorem przesunięcia o 8 bitów. Dzięki temu dokładność jest większa, ale wymaga użycia zmiennych 32b, co może działać wolniej niż 16b na procesorach 8b

- pobieranie odbywa się za pomocą funkcji Get - dzięki temu przypadkowo nie nadpiszemy wartości

- wzory są wypisane w komentarzach

- obie wersje mają dopisaną licencję. Uważam, że jest ważna, a nie jest określona. Teoretycznie jeśli podajecie do publicznej wiadomości kod i ktoś go wykorzysta i przez błąd w jego działaniu, który nie był opisany coś się stanie to za niego odpowiadacie. Warto aby dopisać minimum zrzeczenia się odpowiedzialności. A inne zapisy po to aby ktoś mógł sobie z tego kodu w ogóle korzystać. W przeciwnym razie jest on po to aby sobie ktoś poczytał ale nie używał.

Edit: poprawiony błąd, dzięki Elvis.

Udostępnij ten post


Link to post
Share on other sites

kampo, może lepiej byłoby zamiast zmieniać define'y w pliku nagłówkowym twojej klasy wykorzystać do tego konstruktor? Użytkownik nie musi wtedy nawet otwierać plików nagłówkowych, o ile wie z jakich funkcji ma skorzystać.

OldSkull, w Twoim pierwszym nagłówku, voltage_calc.h niezależenie od tego, czy był już zainkludowany czy nie, zostanie dołączony ponownie - musisz przerzucić #endifa na koniec pliku. Teraz tak patrzę i w drugim masz dokładnie to samo.

EDIT: W ogóle WTF, w plikach .h/.hpp masz definicje funkcji : o Tam powinny znaleźć się tylko deklaracje. Coś czuję tak na oko, że twoje kody sypią warningami 😉

Udostępnij ten post


Link to post
Share on other sites
OldSkull, w Twoim pierwszym nagłówku, voltage_calc.h niezależenie od tego, czy był już zainkludowany czy nie, zostanie dołączony ponownie - musisz przerzucić #endifa na koniec pliku. Teraz tak patrzę i w drugim masz dokładnie to samo.

To jest wymuszone działaniem niektórych kompilatorów. Jeśli jeden plik dołączasz do wielu innych to preprocessor czasami zapamiętuje daną definicję, i potem czasami nie chce do tego innego pliku zrobić include pliku h. Niestety to przeżyłem.

Natomiast ważne co się includuje - jeśli robi się to z plikami, które są poprawnie chronione w sobie to nic się nie stanie. Tutaj #include jest chroniony. Ale jakiś nasz własny include mógłby nie być.

W ogóle WTF, w plikach .h/.hpp masz definicje funkcji : o Tam powinny znaleźć się tylko deklaracje. Coś czuję tak na oko, że twoje kody sypią warningami

Nie sypią. Poza tym nie przesadzajmy - tylko konstruktor klasy jest w pliku .hpp. Technicznie rzecz biorąc coś takiego jest dozwolone - i nie pogarsza czytelności. Ba, jest to spotykane nawet w przykładach - aczkolwiek zwykle tylko jako "return cośtam" w getterach. np. http://www.cplusplus.com/forum/articles/10627/ pozwalają na to aby wszystkie ciała funkcji klasy były w pliku .h.

To też zresztą wynika z własnych przeżyć z obsługą klas w praktyce. Spotkałem się z sytuacją, gdzie były problemy z czymś w stylu ": Mul(mul), Div(div), Vcc(Vref), ADCmax(ADCmaxValue) ". Innymi słowy: warningów nie ma, błędów tym bardziej, działa jak trzeba, jest czytelne i zgodne z poradnikami (przynajmniej niektórymi) - gra gitara.

W pliku .h definicji funkcji nie ma.

Udostępnij ten post


Link to post
Share on other sites

Dołącz do dyskusji, napisz odpowiedź!

Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!

Gość
Napisz odpowiedź...

×   Wklejony jako tekst z formatowaniem.   Przywróć formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.


×
×
  • Utwórz nowe...