Onlangs heb ik me enige stuks GY-HR002 aangeschaft. De
datasheet is erg beknopt, en laat vermoeden dat het om een
vocht-afhankelijke weerstand gaat.
Zo eenvoudig is het echter niet. Een eerste poging om de
GY-HR002 op te nemen in een R/C-oscillator, zodat de frequentie
hiervan een maat zou zijn voor de relatieve vochtigheid,
mislukte jammerlijk.
Al snel werd duidelijk, dat er echt een meetbrug aan te pas
moest komen. Zoiets kan je bouwen met enkele opamps, maar een
kleine microcontroller kan het net zo goed.
Laten we beginnen met PB0. Deze pin levert een blokspanning met een frequentie van (ongeveer) 1kHz. R5 en R6 doen dienst als spanningsdeler, en C3 maakt van de gedeelde blokspanning een echte wisselspanning.
Links in het schema zien we een tweede spanningsdeler: op het knoopunt R2/R3 komt 2.5V te staan. C2 maakt deze spanning “hard” voor wisselstromen.
Meer is niet nodig voor onze meetbrug. De GY-HR002 wordt belast met een weerstand van 31k, en krijgt een wisselspanning van 1.14V aangeboden. PB3 doet dienst als ADC-ingang, zodat we de spanning over R4 kunnen meten. Als we die spanning kennen, kunnen we de weerstand van de GY-HR002 berekenen.
Tenslotte zijn er nog PB1 en PB2. Deze pinnen worden gebruikt om meetresultaten door te geven aan een andere AVR. PB1 levert een aantal klokpulsen, waarna PB2 één puls levert om het einde te markeren.
/*
RH002 - vochtsensor-sturing met een ATtiny45
Pros 2013
*/
#define F_CPU 4000000UL
#include <avr/io.h>
#include <avr/sleep.h>
#include <util/delay.h>
#include <avr/interrupt.h>
volatile unsigned int adch = 527, adcl = 527;
ISR(ADC_vect)
{
unsigned int myadc = ADC;
if (myadc > 527) {
adch -= (adch / 8);
adch += (myadc / 8); // Uitmiddelen
} else { // myadc < 527
adcl -= (adcl / 8);
adcl += (myadc / 8);
}
}
void pulsen(unsigned int aantal)
{
unsigned int cnt;
for (cnt = 0; cnt < aantal; cnt++) {
PORTB &= ~(_BV(PB1));
_delay_us(100);
PORTB |= _BV(PB1);
_delay_us(100);
}
PORTB &= ~(_BV(PB2)); // Een neergaande flank geeft aan
_delay_ms(100); // dat alle pulsen doorgegeven zijn
PORTB |= _BV(PB2);
}
int main(void)
{
unsigned int myadch, myadcl;
CLKPR = _BV(CLKPCE);
CLKPR = _BV(CLKPS0); // CLK = 9.6MHz / 2 = 4.8MHz
ADMUX = _BV(REFS1) | _BV(REFS2) | 3; // Ref = 2.56V; PB3 = ADC IN
// Start de ADC in free-running mode, met interrupt toegestaan
// CLK = 4MHz / 64 = 62500Hz = 4807 ADC-samples/sec
ADCSRA = _BV(ADEN) | _BV(ADIE) | _BV(ADATE) | _BV(ADPS1) | _BV(ADPS2);
ADCSRB = 0;
ADCSRA |= _BV(ADSC); // 1 ADC-bit = 2.5mV
_delay_ms(1000);
TCCR0A = _BV(COM0A0) | _BV(WGM01); // Toggle OC0A on Compare Match - CTC mode
TCCR0B = _BV(CS00) | _BV(CS01); // CLK = 4MHz / 64 = 62500z
OCR0A = 30; // OC0A = 62500 / (2 * 31) = 1008Hz
DDRB = _BV(PB0) | _BV(PB1) | _BV(PB2);
PORTB = _BV(PB1) | _BV(PB2);
MCUCR |= _BV(SM0); // Sleep-mode = ADC noise-reduction
sei();
while (1) {
_delay_ms(1000); // Een seconde wachten
MCUCR |= _BV(SE); // Wachten tot de volgende ADC-omzetting ten einde is
sleep_cpu(); // Dutje doen
MCUCR &= ~(_BV(SE));
myadch = adch; // Hoogste spanning noteren
_delay_ms(1000);
MCUCR |= _BV(SE);
sleep_cpu();
MCUCR &= ~(_BV(SE));
myadcl = adcl; // Laagste spanning noteren
myadch -= myadcl; // myadch = verschil tussen max- en min-waarde
pulsen(myadch); // Resultaat doorgeven
}
}
De code is vrij eenvoudig.
Allereerst wordt de ADC-convertor in free-running mode gestart.
Telkens een conversie afgelopen is, wordt er naar een
interrupt-routine gesprongen, waar het gemiddelde van zowel de
hoogste als van de laagste ingangsspanningen wordt
bijgehouden.
Al wat de hoofdroutine nu nog moet doen, is het verschil tussen
deze gemiddelden berekenen om te weten welke wisselspanning er
over R4 staat, en deze door te geven via PB1 en PB2.
We hebben nu wel een spanning, maar hoe moeten we dat vertalen
naar een percentage? De HY-RH002 is verre van lineair -
berekenen is zowat uitgesloten.
Dan maar vergelijken met een tabel. Nu kunnen we hiervoor de
tabel uit de datasheet gebruiken, maar als we die eerst omzetten
naar een serie spanningen, nemen we de AVR een boel werk uit
zijn handen.
unsigned int RH_cnt; // De spanning in mV
unsigned int temperatuur; // De temperatuur in °C
unsigned char RH; // De relatieve vochtigheid in percent
const unsigned int U[7][15] PROGMEM = {
{9, 17, 29, 55, 97, 147, 227, 295, 380, 491, 579, 673, 763, 858, 908}, // 5°C
{9, 21, 39, 71, 115, 179, 255, 330, 433, 535, 624, 717, 807, 884, 929}, // 10°C
{15, 28, 52, 92, 138, 218, 285, 368, 487, 579, 668, 758, 847, 908, 962}, // 15°C
{19, 36, 69, 111, 172, 247, 320, 421, 528, 630, 713, 801, 875, 941, 989}, // 20°C
{24, 48, 91, 134, 201, 280, 357, 478, 570, 680, 755, 841, 902, 971, 1013}, // 25°C
{32, 64, 109, 162, 244, 322, 410, 519, 614, 713, 801, 876, 940, 993, 1032}, // 30°C
{44, 85, 131, 195, 276, 368, 465, 561, 656, 744, 845, 908, 974, 1013, 1050} // 35°C
};
void calc_rh(void)
{
unsigned int cnt, step, base, row, rhcnt;
unsigned char myrh;
rhcnt = RH_cnt; // Spanning overnemen
RH_cnt = 0; // Terug op 0 zetten
row = temperatuur + 2; // Voorbeeld: temperatuur = 18 + 2 = 20
row /= 5; // 20 / 5 = 4
if (row > 0) {
row―; // 4 - 1 = 3, want eerste row = 0
}
if (row > 6) { // Er zijn maar 7 rows
row = 6;
}
myrh = 20;
for (cnt = 0; cnt < 14; cnt++) {
if (rhcnt < pgm_read_word(&U[row][cnt])) {
break;
}
myrh += 5;
}
// We kennen nu de RH met een resolutie van 5%, dat willen we omzetten naar 1%
base = pgm_read_word(&U[row][cnt]);
step = (base - pgm_read_word(&U[row][cnt - 1])) / 5; //
for (cnt = 0; cnt < 5; cnt++) {
base -= step;
if (rhcnt > base) {
break;
}
myrh―;
}
RH = myrh;
}
Het spreekt voor zich, dat de waarden in de tabel niet met de hand zijn uitgerekend. Ook daar hebben we een brokje code voor geschreven, dat op de PC uitgevoerd wordt.