############################################## # $Id$ # Credits: # Texas Instruments for the Chip - Documentation at https://www.ti.com/lit/ds/sbas444d/sbas444d.pdf (ADS111x) # and https://www.ti.com/lit/ds/sbas473e/sbas473e.pdf (ADS101x) # Karsten Grüttner - for the initial ADS1x1x implementation # Klaus Wittstock: for the PCF8574 module that I used as a basis for this revised ADS1x1x implementation # # package main; use strict; use warnings; use SetExtensions; use Scalar::Util qw(looks_like_number); use List::Util qw(sum); my %I2C_ADS1x1x_Config = ( 'State' => # Bit [15] { 'SINGLE' => 1 << 15, # Write: Begin a single conversion (when in power-down mode) 'BUSY' => 0, # Read: Bit = 0 Device is currently performing a conversion 'NOT_BUSY' => 1 << 15 # Read: Bit = 1 Device is not currently performing a conversion }, 'Mux' => # Bits [14:12] { 'COMP_0_1' => 0 , # AINP = AIN0 and AINN = AIN1 , default 'COMP_0_3' => 1 << 12, # AINP = AIN0 and AINN = AIN3 'COMP_1_3' => 2 << 12 , # AINP = AIN1 and AINN = AIN3 'COMP_2_3' => 3 << 12 , # AINP = AIN2 and AINN = AIN3 'SINGLE_0' => 4 << 12 , # AINP = AIN0 and AINN = GND 'SINGLE_1' => 5 << 12 , # AINP = AIN1 and AINN = GND 'SINGLE_2' => 6 << 12 , # AINP = AIN2 and AINN = GND 'SINGLE_3' => 7 << 12 # AINP = AIN3 and AINN = GND }, 'Gain' => # Bits [11:9] { '6V' => { code => 0, refVoltage => 6.144 }, '4V' => { code => 1 << 9, refVoltage => 4.096 }, # default '2V' => { code => 2 << 9, refVoltage => 2.048 }, '1V' => { code => 3 << 9, refVoltage => 1.024 }, '0.5V' => { code => 4 << 9, refVoltage => 0.512 }, '0.25V' => { code => 5 << 9, refVoltage => 0.256 } }, 'Data_Rate' => # Bits [7:5] "delay" refers to ADS111x only, but not used at all currently { '1/16x' => { code => 0, delay => 1.0/8 }, '1/8x' => { code => 1 << 5, delay => 1.0/16 }, '1/4x' => { code => 2 << 5, delay => 1.0/32 }, '1/2x' => { code => 3 << 5, delay => 1.0/64 }, '1x' => { code => 4 << 5, delay => 1.0/128 }, # default '2x' => { code => 5 << 5, delay => 1.0/250 }, '4x' => { code => 6 << 5, delay => 1.0/475 }, '8x' => { code => 7 << 5, delay => 1.0/860 } }, 'Operation_Mode' => # Bit [8] { 'Continuously' => 0, # einmalig initialisiert, kann immer gelesen werden, geeignet für Dauerüberwachung 'SingleShot' => 1 << 8 # wacht zum einmaligen Lesen auf und legt sich wieder schlafen, geeignet für Messungen mit großen Pausen dazwischen }, 'Comparator_Mode' => # Bit [4] { 'Traditional' => 0, 'Window' => 1 << 4 }, 'Comparator_Polarity' => # Bit [3] { 'ActiveLow' => 0, # default 'ActiveHigh' => 1 << 3 }, 'Latching_Comparator' => # Bit [2] { 'off' => 0, , # default 'on' => 1 << 2 }, 'Comparator_Queue_Disable' => # Bits [1:0] { 'AfterOneConversion' => 0, 'AfterTwoConversions' => 1, 'AfterFourConversions' => 2, 'disable' => 3 # default } ); sub I2C_ADS1x1x_Initialize($) { my ($hash) = @_; $hash->{DefFn} = "I2C_ADS1x1x_Define"; $hash->{InitFn} = 'I2C_ADS1x1x_Init'; $hash->{AttrFn} = "I2C_ADS1x1x_Attr"; $hash->{SetFn} = "I2C_ADS1x1x_Set"; $hash->{StateFn} = "I2C_ADS1x1x_State"; $hash->{GetFn} = "I2C_ADS1x1x_Get"; $hash->{UndefFn} = "I2C_ADS1x1x_Undef"; $hash->{I2CRecFn} = "I2C_ADS1x1x_I2CRec"; $hash->{AttrList} = "IODev do_not_notify:1,0 ignore:1,0 showtime:1,0 ". "a0_mode:RTD,NTC,RAW,RES,off ". "a1_mode:RTD,NTC,RAW,RES,off ". "a2_mode:RTD,NTC,RAW,RES,off ". "a3_mode:RTD,NTC,RAW,RES,off ". "a0_res a1_res a2_res a3_res ". "a0_r0 a1_r0 a2_r0 a3_r0 ". "a0_avg a1_avg a2_avg a3_avg ". "a0_bval a1_bval a2_bval a3_bval ". "a0_gain:6V,4V,2V,1V,0.5V,0.25V ". "a1_gain:6V,4V,2V,1V,0.5V,0.25V ". "a2_gain:6V,4V,2V,1V,0.5V,0.25V ". "a3_gain:6V,4V,2V,1V,0.5V,0.25V ". "decimals:0,1,2,3,4,5 ". "sys_voltage " . "data_rate:1/16x,1/8x,1/4x,1/2x,1x,2x,4x,8x ". "mux:SINGLE ". "device:ADS1013,ADS1014,ADS1015,ADS1113,ADS1114,ADS1115 ". #"comparator_polarity:ActiveLow,ActiveHigh ". #"operation_mode:SingleShot,Continuously ". ### Does not make sense with multiple inputs #"comparator_mode:Traditional,Window ". #"latching_comparator:on,off ". #"comparator_queue_disable:AfterOneConversion,AfterTwoConversion,AfterFourConversion,disable ". "poll_interval ". "poll_interleave ". "$readingFnAttributes"; } ################################### Todo: Set or Attribute for Mode? Other sets needed? sub I2C_ADS1x1x_Set($@) { # my ($hash, @a) = @_; my $name =$a[0]; my $cmd = $a[1]; my $val = $a[2]; if ( $cmd && $cmd eq "Update") { #Make sure there is no reading cycle running and re-start polling (which starts with an inital read) RemoveInternalTimer($hash) if ( defined (AttrVal($hash->{NAME}, "poll_interval", undef)) ); $hash->{helper}{state}=0; #Reset state machine InternalTimer(gettimeofday() + 1, 'I2C_ADS1x1x_Execute', $hash, 0); return undef; } else { my $list = "Update:noArg"; return "Unknown argument $a[1], choose one of " . $list if defined $list; return "Unknown argument $a[1]"; } if (!defined $hash->{IODev}) { readingsSingleUpdate($hash, 'state', 'No IODev defined',0); return "$name: no IO device defined"; } return undef; } ################################### sub I2C_ADS1x1x_Get($@) { #Nothing to be done here, let all updates run asychroniously with timers return undef; } sub I2C_ADS1x1x_Execute($@) { my ($hash) = @_; my $state=$hash->{helper}{state}; my $channels=$hash->{helper}{channels}; #Default time between reading channels my $nexttimer=AttrVal($hash->{NAME}, 'poll_interleave', 0.008); my $interleave=$nexttimer; if (!defined($state)) {$state=0}; if ($state%2 == 0) {$nexttimer=0.008;} #8 ms conversiontime for even numbers if ($state<($channels*2-1)) { $hash->{helper}{state}+=1; } else { $hash->{helper}{state}=0; #Interleave to next complete read cycle is poll interval $nexttimer = AttrVal($hash->{NAME}, 'poll_interval', 5)*60 - $channels*(0.008+$interleave); #Substract channel timers to have more or less constant interval } Log3 $hash->{NAME}, 5, $hash->{NAME}." => Processing state $state timer $nexttimer channels: $channels newstate:".$hash->{helper}{state}; if (!defined AttrVal($hash->{NAME}, "IODev", undef)) {return;} if ($state==0) { I2C_ADS1x1x_InitConfig($hash,0); } elsif ($state==1) { I2C_ADS1x1x_ReadData($hash,0); } elsif ($state==2) { I2C_ADS1x1x_InitConfig($hash,1); } elsif ($state==3) { I2C_ADS1x1x_ReadData($hash,1); } elsif ($state==4) { I2C_ADS1x1x_InitConfig($hash,2); } elsif ($state==5) { I2C_ADS1x1x_ReadData($hash,2); } elsif ($state==6) { I2C_ADS1x1x_InitConfig($hash,3); } elsif ($state==7) { I2C_ADS1x1x_ReadData($hash,3); } #Initalize next Timer for Reading Results in 8ms (time required for conversion to be ready) InternalTimer(gettimeofday()+$nexttimer, \&I2C_ADS1x1x_Execute, $hash,0) unless $nexttimer<=0; return undef; } sub I2C_ADS1x1x_InitConfig(@) { my ($hash, $sensor) = @_; my $phash = $hash->{IODev}; my $pname = $phash->{NAME}; my $mux=AttrVal($hash->{NAME}, "mux", "SINGLE"); return undef if ($mux ne "SINGLE"); #Only SINGLE mode supported my $mode=AttrVal($hash->{NAME}, "a".$sensor."_mode", "RAW"); if ($mode ne "off") { my $sensval=$mux."_".$sensor; my $gain=AttrVal($hash->{NAME}, "a".$sensor."_gain", "4V"); my $config = $hash->{helper}{configword}| $I2C_ADS1x1x_Config{'Mux'}{$sensval}| $I2C_ADS1x1x_Config{'Gain'}{$gain}{code}; my $low_byte = $config & 0xff; my $high_byte = ($config & 0xff00) >> 8; my %sendpackage = ( i2caddress => $hash->{I2C_Address}, direction => "i2cwrite", reg=> 1, sensor=>$sensor, data => $high_byte. " " .$low_byte); Log3 $hash->{NAME}, 4, $hash->{NAME}." => $pname CONFIG adr:".$hash->{I2C_Address}." Sensor $sensor Byte0:$high_byte Byte1:$low_byte"; CallFn($pname, "I2CWrtFn", $phash, \%sendpackage); } } sub I2C_ADS1x1x_ReadData(@) { my ($hash, $sensor) = @_; my $phash = $hash->{IODev}; my $pname = $phash->{NAME}; #Gain needs to be passed through for calculation my $gain=AttrVal($hash->{NAME}, "a".$sensor."_gain", "4V"); my %sendpackage = ( i2caddress => $hash->{I2C_Address}, direction => "i2cread", reg=> 0, sensor=>$sensor, gain=>$gain, nbyte => 2); Log3 $hash->{NAME}, 5, $hash->{NAME}." => $pname READ adr:".$hash->{I2C_Address}." Sensor $sensor Gain $gain"; CallFn($pname, "I2CWrtFn", $phash, \%sendpackage); } ################################### sub I2C_ADS1x1x_Attr(@) { # my ($command, $name, $attr, $val) = @_; my $hash = $defs{$name}; my $msg = undef; if ($command && $command eq "set" && $attr && $attr eq "IODev") { if ($main::init_done and (!defined ($hash->{IODev}) or $hash->{IODev}->{NAME} ne $val)) { main::AssignIoPort($hash,$val); my @def = split (' ',$hash->{DEF}); I2C_ADS1x1x_Init($hash,\@def) if (defined ($hash->{IODev})); } } if ($attr eq 'poll_interval') { if ( defined($val) ) { if ( looks_like_number($val) && $val >= 0) { RemoveInternalTimer($hash); InternalTimer(gettimeofday()+1, 'I2C_ADS1x1x_Execute', $hash, 0) if $val>0; } else { $msg = "$hash->{NAME}: Wrong poll intervall defined. poll_interval must be a number >= 0"; } } else { RemoveInternalTimer($hash); } } elsif ($attr eq 'device') { my $channels=1; if (!defined $val or $val =~ m/^ADS1[0|1]15$/i ) { $channels=4; # Only these two devices have 4 channels } $hash->{helper}{channels}=$channels; } #check for correct values while setting so we need no error handling later foreach ('sys_voltage','a0_res','a1_res','a2_res','a3_res', 'a0_r0', 'a1_r0', 'a2_r0', 'a3_r0', 'a0_bval', 'a1_bval', 'a2_bval', 'a3_bval') { if ($attr eq $_) { if ( defined($val) ) { if ( !looks_like_number($val) || $val <= 0) { $msg = "$hash->{NAME}: ".$attr." must be a number > 0"; } } } } I2C_ADS1x1x_Prepare($hash); #Update predefined variables so any attribute changes are reflected return $msg; } ################################### sub I2C_ADS1x1x_Define($$) { # my ($hash, $def) = @_; my @a = split("[ \t]+", $def); if ($main::init_done) { eval { I2C_ADS1x1x_Init( $hash, [ @a[ 2 .. scalar(@a) - 1 ] ] ); }; return I2C_ADS1x1x_Catch($@) if $@; } return undef; } ################################### sub I2C_ADS1x1x_Init($$) { # my ( $hash, $args ) = @_; #my @a = split("[ \t]+", $args); my $name = $hash->{NAME}; if (defined $args && int(@$args) != 1) { return "Define: Wrong syntax. Usage:\n" . "define I2C_ADS1x1x "; } if (defined (my $address = shift @$args)) { $hash->{I2C_Address} = $address =~ /^0.*$/ ? oct($address) : $address; } else { readingsSingleUpdate($hash, 'state', 'Invalid I2C Adress',0); return "$name I2C Address not valid"; } AssignIoPort($hash); readingsSingleUpdate($hash, 'state', 'Initialized',0); I2C_ADS1x1x_Set($hash, $name, "setfromreading"); I2C_ADS1x1x_Prepare($hash); RemoveInternalTimer($hash); my $pollInterval = AttrVal($hash->{NAME}, 'poll_interval', 5)*60; InternalTimer(gettimeofday() + $pollInterval, 'I2C_ADS1x1x_Execute', $hash, 0) if ($pollInterval > 0); return; } sub I2C_ADS1x1x_Prepare($) { my ($hash)=@_; $hash->{helper}{state}=0; #initalize state machine $hash->{helper}{channels}=4; #for default ADS1115, will be overwritten with different ATTR setting my $mux=AttrVal($hash->{NAME}, "mux", "SINGLE"); my $device=AttrVal($hash->{NAME}, "device", "ADS1115"); my $rate=AttrVal($hash->{NAME}, "data_rate", "1x"); my $opmode=AttrVal($hash->{NAME}, "operation_mode", "SingleShot"); my $cmode=AttrVal($hash->{NAME}, "comparator_mode", "Traditional"); my $lcomp=AttrVal($hash->{NAME}, "latching_comparator", "on"); my $cqueue=AttrVal($hash->{NAME}, "comparator_queue_disable", "AfterOneConversion"); my $cpol=AttrVal($hash->{NAME}, "comparator_polarity", "ActiveLow"); my $config = $I2C_ADS1x1x_Config{'State'}{SINGLE}| $I2C_ADS1x1x_Config{'Data_Rate'}{$rate}{code}| $I2C_ADS1x1x_Config{'Operation_Mode'}{$opmode}| $I2C_ADS1x1x_Config{'Comparator_Mode'}{$cmode}| $I2C_ADS1x1x_Config{'Latching_Comparator'}{$lcomp}| $I2C_ADS1x1x_Config{'Comparator_Queue_Disable'}{$cqueue}| $I2C_ADS1x1x_Config{'Comparator_Polarity'}{$cpol}; $hash->{helper}{configword}=$config; } ################################### sub I2C_ADS1x1x_Catch($) { my $exception = shift; if ($exception) { $exception =~ /^(.*)( at.*FHEM.*)$/; return $1; } return undef; } ################################### sub I2C_ADS1x1x_State($$$$) { #reload readings at FHEM start my ($hash, $tim, $sname, $sval) = @_; #No persistant data needed, using only attributes return undef; } ################################### sub I2C_ADS1x1x_Undef($$) { # my ($hash, $name) = @_; RemoveInternalTimer($hash) if ( defined (AttrVal($hash->{NAME}, "poll_interval", undef)) ); return undef; } # Calculate temperature for PT1000/PT100 platinum temperature sensors # ax_r0 = Resistance in Ohm at zero degrees C sub I2C_ADS1x1x_RTD($@) { my ($resistance,$sensor,$r0) = @_; #my $aa=0.003851; #Deutscher Standard? my $aa=0.0039083; #ITU-90 Standard my $bb=-5.05E-08; #my own value #my $bb=-5.7750E-07; #ITU-90 Standard my $temperature=0; my $root = $aa*$aa*$r0*$r0-4*$bb*$r0*($r0-$resistance); if ($root>=0) { $temperature=(-$aa*$r0+sqrt($root))/(2*$bb*$r0); } return $temperature; } # Calculate temperature for NTC Sensors # ax_r0 = Resistance in Ohm at 25 degrees C (typically 50K) # ax_b = B-Value according to datasheet (for 50K often 3950) sub I2C_ADS1x1x_NTC($@) { my ($resistance,$sensor,$r0,$bval) = @_; if ($resistance<0) {return 0;} # Prevent issue in error case my $steinhart; $steinhart = $resistance / $r0; # (R/Ro) $steinhart = log($steinhart); # ln(R/Ro) $steinhart = $steinhart/$bval; # 1/B * ln(R/Ro) $steinhart = $steinhart+ 1.0 / (25.0 + 273.15); # + (1/To) $steinhart = 1.0 / $steinhart; # Invert $steinhart = $steinhart-273.15; # convert to C return $steinhart; } ################################### sub I2C_ADS1x1x_I2CRec($@) { # ueber CallFn vom physical aufgerufen my ($hash, $clientmsg) = @_; my $name = $hash->{NAME}; my $phash = $hash->{IODev}; my $pname = $phash->{NAME}; my $clientHash = $defs{$name}; my $msg = ""; while ( my ( $k, $v ) = each %$clientmsg ) { #erzeugen von Internals fuer alle Keys in $clientmsg die mit dem physical Namen beginnen $hash->{$k} = $v if $k =~ /^$pname/ ; $msg = $msg . " $k=$v"; } Log3 $hash,5 , "$name: I2C reply:$msg"; my $sval; if ($clientmsg->{direction} && $clientmsg->{$pname . "_SENDSTAT"} && $clientmsg->{$pname . "_SENDSTAT"} eq "Ok") { readingsBeginUpdate($hash); if ($clientmsg->{direction} eq "i2cread" && defined($clientmsg->{received})) { my ($high,$low) = split(/ /, $clientmsg->{received}); my $value= $high<<8|$low; Log3 $hash,5 , "$name:value:$value"; my $gain=$clientmsg->{gain}; my $refvoltage=$I2C_ADS1x1x_Config{'Gain'}{$gain}{refVoltage}; my $device=AttrVal($hash->{NAME}, "device", "ADS1115"); my $mask=0x7fff; my $bits=16; #No differentiation for 12bit since those devices still submit 16bits with the 4 lower bits set to zero my $voltage = ($value & $mask) * # filtere Bit 2^15 (0x8000) raus, das ist Vorzeichenmerkmal ( $refvoltage/$mask) * # normiere anhand der Auflösung 2^15 im positiven Bereich ( 1.0 - (2.0 * (($value & ($mask+1)) >> ($bits-1)))); # bei gesetzten Bit 2^15 Faktor -1, ansonsten +1 ($mask+1 = 0x8000/0x800) my $sensor= $clientmsg->{sensor}; #Build average with floating windows to smoothe shaky sensors my @avg=(); my $avgs=$hash->{helper}{"a".$sensor}; if (defined $avgs) { @avg=@$avgs; } my $avgmax=AttrVal($name,"a".$sensor."_avg",1); push @avg,$voltage; while (@avg>$avgmax) { shift @avg; } $hash->{helper}{"a".$sensor}=[@avg]; $voltage=sum(@avg)/@avg; #rounded voltage only for reading, continue calculation will full precision my $voltager = sprintf( '%.' . AttrVal($clientHash->{NAME}, 'decimals', 3) . 'f', $voltage ); Log3 $hash,5 , "$name:voltage=$voltage, ref=".$I2C_ADS1x1x_Config{'Gain'}{$gain}{refVoltage}; readingsBulkUpdate($hash, "a".$sensor."_voltage", $voltager) if (ReadingsVal($name,"a".$sensor."_voltage",0) != $voltager); my $divider=AttrVal($name,"a".$sensor."_res",1000); my $highvoltage=AttrVal($name,"sys_voltage",3.3); #Always calculate resistance but only write to reading in case of "RES" mode my $resistance=$divider*$voltage/($highvoltage-$voltage); my $resistancer = sprintf( '%.' . AttrVal($name, 'decimals', 3) . 'f', $resistance ); my $temperature=0; my $mode=AttrVal($name,"a".$sensor."_mode",""); Log3 $hash,5 , "$name:resistance=$resistance, with divider=$divider system_voltage=$highvoltage"; if ($mode eq "RES") { readingsBulkUpdate($hash, "a".$sensor."_resistance", $resistancer) if (ReadingsVal($name,"a".$sensor."_resistance",0) != $resistancer); } elsif ($mode eq "RTD") { $temperature=sprintf( '%.1f', I2C_ADS1x1x_RTD($resistance,$sensor,AttrVal($name,"a".$sensor."_r0",1000.0))); Log3 $hash,5 , "$name:RTD Temp=$temperature °C"; readingsBulkUpdate($hash, "a".$sensor."_temperature", $temperature) if (ReadingsVal($name,"a".$sensor."_temperature",0) != $temperature); } elsif ($mode eq "NTC") { $temperature=sprintf( '%.1f', I2C_ADS1x1x_NTC($resistance,$sensor,AttrVal($name,"a".$sensor."_res",50000.0),AttrVal($name,"a".$sensor."_b",3950.0))); Log3 $hash,5 , "$name:NTC Temp=$temperature °C"; readingsBulkUpdate($hash, "a".$sensor."_temperature", $temperature) if (ReadingsVal($name,"a".$sensor."_temperature",0) != $temperature); } } elsif ($clientmsg->{direction} eq "i2cwrite" && defined($clientmsg->{data})) { #reply from write - ignore } readingsEndUpdate($hash, 1); } } 1; #Todo Write update documentation =pod =item device =item summary reads/converts data from an via I2C connected ADS1x1x A/D converter =item summary_DE liest/konvertiert Daten eines via angeschlossenen ADS1x1x A/D Wandlers =begin html

I2C_ADS1x1x

(en | de) =end html =begin html_DE

I2C_ADS1x1x

(en | de) =end html_DE =cut