agrobot_base/Firmware/Modulos/SensorTemperaturaNtcModel.h

1092 lines
22 KiB
C++

#include "SerialService.h"
#include "Pinout.h"
#include "Utils.h"
#include "IComponenteCAN.h"
#include "I2CService.h"
#include <Arduino.h>
#include <vector>
#include <math.h>
#include <limits.h>
#ifndef SensorTemperaturaNtcModel
#define SensorTemperaturaNtcModel
/*
* SensorTemperaturaNtc - versão produto
*
* Topologia física da placa:
*
* Retorno do motor ---- Rserie 4k7 ----+---- ADC / ADS1115
* |
* Rgnd 6k8
* |
* GND
*
* Logo, visto pelo retorno do motor, a placa adiciona uma carga:
*
* Rcarga = Rserie + Rgnd = 11k5
*
* e a tensão realmente lida pelo ADC é:
*
* Vadc = Vretorno * Rgnd / (Rserie + Rgnd)
*
* A classe reconstrói Vretorno antes de calcular o NTC.
*
*
* ntcGnd = false:
*
* 5 V
* |
* NTC
* |
* Vretorno ------- Rserie ------- ADC
* | |
* Rfixo 10k Rgnd
* | |
* GND GND
*
*
* ntcGnd = true:
*
* 5 V
* |
* Rfixo 10k
* |
* Vretorno ------- Rserie ------- ADC
* | |
* NTC Rgnd
* | |
* GND GND
*
*
* Para EXPANSOR_ADS:
* - usa geração do Wire/I2C;
* - faz uma única aquisição lógica por ciclo;
* - preserva a última amostra válida em falhas;
* - invalida o lifecycle após falhas consecutivas;
* - o retry externo pode reinicializar o sensor.
*/
class SensorTemperaturaNtc : public ComponenteCAN {
public:
String ID;
uint8_t ID_Num = 0;
PinoModel _pino;
// Mantido público para compatibilidade com código legado.
bool Iniciado = false;
int leitura = -1;
double temperatura = NAN;
// Tensão REAL medida no pino do ADC/ADS, depois do divisor 4k7/6k8.
double tensao = NAN;
float OffsetTemperatura = 0.0f;
SensorTemperaturaNtc() {}
/*
* Mantém exatamente o contrato histórico do construtor.
*
* ntcGnd:
* false = NTC no lado de 5 V
* true = NTC no lado de GND
*/
SensorTemperaturaNtc(
const String& id,
uint8_t idNum,
int pino,
TiposBarramentos tipoBarramento,
int canalMux = -1,
float offsetTemperatura = 0.0f,
bool ntcGnd = false
) :
ID(id),
ID_Num(idNum),
_pino(
PinoModel(
pino,
tipoBarramento,
_INPUT,
ANALOGICO,
idNum,
canalMux
)
),
OffsetTemperatura(offsetTemperatura),
_ntcGnd(ntcGnd),
_tipoBarramento(tipoBarramento)
{
}
bool EstaIniciado() {
bool iniciado;
uint32_t geracaoInicializacao;
portENTER_CRITICAL(&_snapshotLock);
iniciado = Iniciado;
geracaoInicializacao = _geracaoI2CInicializacao;
portEXIT_CRITICAL(&_snapshotLock);
if (!iniciado) {
return false;
}
// ADC interno não depende do lifecycle do Wire.
if (_tipoBarramento != TiposBarramentos::EXPANSOR_ADS) {
return true;
}
const uint32_t geracaoAtual =
I2CService::GeracaoBarramento();
return
I2CService::I2CIniciado &&
I2CService::AdsIniciado &&
geracaoAtual != 0 &&
geracaoInicializacao == geracaoAtual;
}
bool PossuiAmostraValida() {
bool valor;
portENTER_CRITICAL(&_snapshotLock);
valor = _possuiAmostraValida;
portEXIT_CRITICAL(&_snapshotLock);
return valor;
}
uint8_t FalhasConsecutivas() {
uint8_t valor;
portENTER_CRITICAL(&_snapshotLock);
valor = _falhasConsecutivas;
portEXIT_CRITICAL(&_snapshotLock);
return valor;
}
uint32_t TotalFalhasLeitura() {
uint32_t valor;
portENTER_CRITICAL(&_snapshotLock);
valor = _totalFalhasLeitura;
portEXIT_CRITICAL(&_snapshotLock);
return valor;
}
uint32_t IdadeUltimaAmostraValidaMs() {
uint32_t ultima;
bool possui;
portENTER_CRITICAL(&_snapshotLock);
ultima = _ultimaAmostraValidaMs;
possui = _possuiAmostraValida;
portEXIT_CRITICAL(&_snapshotLock);
if (!possui || ultima == 0) {
return UINT32_MAX;
}
return (uint32_t)(millis() - ultima);
}
void Inicializar() {
if (EstaIniciado()) {
MostrarLog("Sensor ja inicializado");
return;
}
// Limpa lifecycle, mas preserva a última leitura válida.
AtualizarEstadoIniciado(false);
if (_tipoBarramento == TiposBarramentos::EXPANSOR_ADS) {
if (!I2CService::I2CIniciado) {
MostrarLog(
"Sensor nao iniciado | I2C indisponivel"
);
return;
}
if (!I2CService::AdsIniciado) {
MostrarLog(
"Sensor nao iniciado | ADS1115 indisponivel"
);
return;
}
}
_pino.Conectar();
if (!_pino.Definido() || !_pino.Conectado) {
MostrarLog(
"Sensor nao iniciado | pino/barramento indisponivel"
);
return;
}
/*
* Se algum dia esta classe voltar ao ADC interno,
* preserva a configuração histórica de 12 bits / 11 dB.
*
* Para o ADS1115 isso não é usado.
*/
if (_tipoBarramento == TiposBarramentos::CONTROLADOR) {
analogReadResolution(12);
analogSetPinAttenuation(
_pino.num,
ADC_11db
);
}
AtualizarEstadoIniciado(true);
portENTER_CRITICAL(&_snapshotLock);
_falhasConsecutivas = 0;
portEXIT_CRITICAL(&_snapshotLock);
MostrarLog(
"Sensor iniciado"
" | canal=" + String(_pino.num) +
" | ntc_gnd=" + String(_ntcGnd ? 1 : 0) +
" | geracao=" +
String(_geracaoI2CInicializacao)
);
}
void Desligar() {
_pino.Desconectar();
AtualizarEstadoIniciado(false);
MostrarLog("Sensor desligado");
}
void RequisitarDados() {
if (!EstaIniciado()) {
/*
* Caso a geração tenha mudado, força Iniciado=false
* para que o retry periódico da firmware enxergue
* corretamente a necessidade de reinicialização.
*/
bool estavaMarcadoIniciado;
portENTER_CRITICAL(&_snapshotLock);
estavaMarcadoIniciado = Iniciado;
portEXIT_CRITICAL(&_snapshotLock);
if (
estavaMarcadoIniciado &&
_tipoBarramento ==
TiposBarramentos::EXPANSOR_ADS
) {
AtualizarEstadoIniciado(false);
}
return;
}
AferirTemperatura();
}
std::vector<uint8_t> MontarMensagemCAN(
CanMessagePosicaoDados posicao
) {
std::vector<uint8_t> data;
data.push_back(
static_cast<uint8_t>(posicao)
);
data.push_back(ID_Num);
const bool iniciado =
EstaIniciado();
int snapLeitura;
double snapTemperatura;
double snapTensao;
bool snapValido;
portENTER_CRITICAL(&_snapshotLock);
snapLeitura = leitura;
snapTemperatura = temperatura;
snapTensao = tensao;
snapValido = _possuiAmostraValida;
portEXIT_CRITICAL(&_snapshotLock);
switch (posicao) {
case CanMessagePosicaoDados::Status: {
data.push_back(
iniciado ? 1 : 0
);
break;
}
case CanMessagePosicaoDados::Dados1: {
/*
* Mantém o contrato CAN atual:
*
* [2..3] raw ADC normalizado 0..4095
* [4..5] temperatura * 100
* [6..7] tensão no PINO ADC em mV
*/
uint16_t leituraAdc =
snapValido &&
snapLeitura >= 0
? static_cast<uint16_t>(
snapLeitura
)
: 0xFFFF;
int16_t temperaturaInt =
snapValido &&
isfinite(snapTemperatura)
? static_cast<int16_t>(
constrain(
(long)lround(
snapTemperatura *
100.0
),
(long)INT16_MIN + 1,
(long)INT16_MAX
)
)
: INT16_MIN;
uint16_t tensaoMv =
snapValido &&
isfinite(snapTensao) &&
snapTensao >= 0.0
? static_cast<uint16_t>(
constrain(
(long)lround(
snapTensao *
1000.0
),
0L,
65534L
)
)
: 0xFFFF;
data.push_back(
static_cast<uint8_t>(
leituraAdc >> 8
)
);
data.push_back(
static_cast<uint8_t>(
leituraAdc & 0xFF
)
);
data.push_back(
static_cast<uint8_t>(
(uint16_t)temperaturaInt >> 8
)
);
data.push_back(
static_cast<uint8_t>(
(uint16_t)temperaturaInt & 0xFF
)
);
data.push_back(
static_cast<uint8_t>(
tensaoMv >> 8
)
);
data.push_back(
static_cast<uint8_t>(
tensaoMv & 0xFF
)
);
break;
}
default:
break;
}
return data;
}
private:
bool DebugMode = false;
/*
* false:
* 5V -> NTC -> retorno -> Rfixo -> GND
*
* true:
* 5V -> Rfixo -> retorno -> NTC -> GND
*/
bool _ntcGnd = false;
TiposBarramentos _tipoBarramento =
TiposBarramentos::CONTROLADOR;
portMUX_TYPE _snapshotLock =
portMUX_INITIALIZER_UNLOCKED;
uint8_t _falhasConsecutivas = 0;
uint32_t _totalFalhasLeitura = 0;
uint32_t _geracaoI2CInicializacao = 0;
bool _possuiAmostraValida = false;
uint32_t _ultimaAmostraValidaMs = 0;
/*
* Hardware real.
*/
static constexpr float Vs = 5.0f;
static constexpr float Rfixo =
10000.0f;
static constexpr float Rserie =
4700.0f;
static constexpr float Rgnd =
6800.0f;
/*
* Do ponto de vista do Vretorno:
*
* retorno -> 4k7 -> ADC -> 6k8 -> GND
*
* equivale a 11k5 para GND.
*/
static constexpr float Rcarga =
Rserie + Rgnd;
/*
* ADS1115 atual:
* I2CService usa GAIN_ONE.
*
* readADC_SingleEnded() é convertido no service
* de 0..32767 para 0..4095.
*
* Portanto:
*
* 4095 <=> 4.096 V
*/
static constexpr float
FullScaleAdsV = 4.096f;
/*
* O máximo físico no ADC devido ao divisor 4k7/6k8 é:
*
* 5V * 6k8 / (4k7 + 6k8)
* ~= 2.9565 V
*/
static constexpr float
TensaoAdcMaxFisicaV =
Vs * Rgnd / Rcarga;
static constexpr float
MargemTensaoV = 0.010f;
static constexpr float Beta =
3950.0f;
static constexpr float T0K =
298.15f;
static constexpr float Rntc25 =
10000.0f;
static constexpr float
TemperaturaMinC = -40.0f;
static constexpr float
TemperaturaMaxC = 150.0f;
/*
* ADS1115 já é delta-sigma.
* 4 amostras por NTC são suficientes para uma grandeza lenta.
*
* 4 NTC * 4 conversões * 2 Hz = 32 conversões/s.
*/
static constexpr int
QuantidadeAmostras = 4;
/*
* Filtro final leve.
* O valor só entra aqui depois de passar por todas as validações.
*/
static constexpr float
AlphaTemperatura = 0.35f;
static constexpr uint8_t
LimiteFalhasAntesReinit = 3;
void MostrarLog(
const String& mensagem
) {
if (DebugMode) {
PrintTela(
"[TMP " +
ID +
"] " +
mensagem
);
}
}
void AtualizarEstadoIniciado(
bool iniciado
) {
const uint32_t geracao =
iniciado &&
_tipoBarramento ==
TiposBarramentos::EXPANSOR_ADS
? I2CService::GeracaoBarramento()
: 0;
portENTER_CRITICAL(&_snapshotLock);
Iniciado = iniciado;
_geracaoI2CInicializacao =
geracao;
portEXIT_CRITICAL(&_snapshotLock);
}
void RegistrarFalhaAquisicao(
const String& motivo
) {
uint8_t falhas;
portENTER_CRITICAL(&_snapshotLock);
if (
_falhasConsecutivas <
UINT8_MAX
) {
_falhasConsecutivas++;
}
_totalFalhasLeitura++;
falhas =
_falhasConsecutivas;
portEXIT_CRITICAL(&_snapshotLock);
MostrarLog(
"Falha de leitura " +
String(falhas) +
"/" +
String(
LimiteFalhasAntesReinit
) +
" | " +
motivo
);
/*
* Nunca sobrescreve o último valor bom com NaN.
*
* Se a falha persistir, o sensor perde o estado
* Iniciado e entra no retry controlado da firmware.
*/
if (
falhas >=
LimiteFalhasAntesReinit
) {
AtualizarEstadoIniciado(false);
MostrarLog(
"Sensor marcado para "
"reinicializacao controlada"
);
}
}
void RegistrarSucessoAquisicao() {
portENTER_CRITICAL(&_snapshotLock);
_falhasConsecutivas = 0;
portEXIT_CRITICAL(&_snapshotLock);
}
/*
* Converte o raw 0..4095 recebido do I2CService
* para a tensão REAL no pino do ADS1115.
*/
bool ConverterRawParaTensaoAdc(
int raw,
float& vAdc
) {
if (
raw < 0 ||
raw > 4095
) {
return false;
}
if (
_tipoBarramento ==
TiposBarramentos::EXPANSOR_ADS
) {
vAdc =
((float)raw / 4095.0f) *
FullScaleAdsV;
}
else {
/*
* Fallback histórico para ADC de 12 bits.
* Esta versão produto está otimizada para ADS.
*
* Se voltar ao ADC interno, recomenda-se usar
* analogReadMilliVolts() no PinoModel para calibração absoluta.
*/
vAdc =
((float)raw / 4095.0f) *
3.3f;
}
if (!isfinite(vAdc)) {
return false;
}
/*
* Com o divisor físico, o ADS jamais deveria receber
* mais que aproximadamente 2.9565 V.
*
* Deixamos pequena tolerância para resistores/medição.
*/
if (
_tipoBarramento ==
TiposBarramentos::EXPANSOR_ADS &&
(
vAdc <
-MargemTensaoV ||
vAdc >
TensaoAdcMaxFisicaV +
0.050f
)
) {
return false;
}
return true;
}
/*
* Reconstrói a tensão do nó original do sensor,
* antes do divisor 4k7/6k8.
*
* Vadc =
* Vretorno *
* Rgnd /
* (Rserie + Rgnd)
*
* logo:
*
* Vretorno =
* Vadc *
* (Rserie + Rgnd) /
* Rgnd
*/
bool CalcularTensaoRetorno(
float vAdc,
float& vRetorno
) {
if (
!isfinite(vAdc) ||
vAdc < 0.0f
) {
return false;
}
vRetorno =
vAdc *
Rcarga /
Rgnd;
if (!isfinite(vRetorno)) {
return false;
}
if (
vRetorno <=
MargemTensaoV ||
vRetorno >=
Vs -
MargemTensaoV
) {
return false;
}
return true;
}
/*
* Calcula Rntc considerando também a carga
* criada pelo divisor 4k7 + 6k8.
*/
bool CalcularResistenciaNtc(
float vRetorno,
float& resistenciaNtc
) {
if (
!isfinite(vRetorno) ||
vRetorno <=
MargemTensaoV ||
vRetorno >=
Vs -
MargemTensaoV
) {
return false;
}
if (_ntcGnd) {
/*
* 5V -> Rfixo -> retorno -> NTC -> GND
*
* Abaixo do retorno temos:
*
* NTC || Rcarga
*
* Primeiro calculamos a resistência equivalente
* inferior do divisor:
*
* Req =
* Rfixo *
* Vretorno /
* (Vs - Vretorno)
*/
const float rEquivalente =
Rfixo *
vRetorno /
(Vs - vRetorno);
if (
!isfinite(rEquivalente) ||
rEquivalente <= 0.0f
) {
return false;
}
/*
* 1 / Req =
* 1 / Rntc +
* 1 / Rcarga
*/
const float inversoRntc =
(1.0f / rEquivalente) -
(1.0f / Rcarga);
if (
!isfinite(inversoRntc) ||
inversoRntc <= 0.0f
) {
return false;
}
resistenciaNtc =
1.0f /
inversoRntc;
}
else {
/*
* 5V -> NTC -> retorno -> Rfixo -> GND
*
* Abaixo do retorno temos:
*
* Rfixo || Rcarga
*/
const float rInferior =
(Rfixo * Rcarga) /
(Rfixo + Rcarga);
if (
!isfinite(rInferior) ||
rInferior <= 0.0f
) {
return false;
}
/*
* Vretorno =
*
* Vs *
* rInferior /
* (Rntc + rInferior)
*
* logo:
*
* Rntc =
* rInferior *
* (Vs - Vretorno) /
* Vretorno
*/
resistenciaNtc =
rInferior *
(Vs - vRetorno) /
vRetorno;
}
return
isfinite(resistenciaNtc) &&
resistenciaNtc > 0.0f;
}
bool CalcularTemperaturaC(
float resistenciaNtc,
float& temperaturaC
) {
if (
!isfinite(resistenciaNtc) ||
resistenciaNtc <= 0.0f
) {
return false;
}
const float termoLog =
logf(
resistenciaNtc /
Rntc25
);
if (!isfinite(termoLog)) {
return false;
}
const float inversoT =
(1.0f / T0K) +
(termoLog / Beta);
if (
!isfinite(inversoT) ||
inversoT <= 0.0f
) {
return false;
}
temperaturaC =
(1.0f / inversoT) -
273.15f +
OffsetTemperatura;
if (
!isfinite(temperaturaC) ||
temperaturaC <
TemperaturaMinC ||
temperaturaC >
TemperaturaMaxC
) {
return false;
}
temperaturaC =
roundf(
temperaturaC *
100.0f
) /
100.0f;
return true;
}
void PublicarAmostraValida(
int novaLeitura,
float novaTensaoAdc,
float novaTemperatura
) {
float temperaturaPublicada =
novaTemperatura;
portENTER_CRITICAL(&_snapshotLock);
if (
_possuiAmostraValida &&
isfinite(temperatura)
) {
temperaturaPublicada =
AlphaTemperatura *
novaTemperatura +
(
1.0f -
AlphaTemperatura
) *
(float)temperatura;
}
leitura =
novaLeitura;
/*
* Mantemos no CAN a tensão que realmente
* chegou ao ADC, depois do divisor 4k7/6k8.
*/
tensao =
novaTensaoAdc;
temperatura =
roundf(
temperaturaPublicada *
100.0f
) /
100.0f;
_possuiAmostraValida =
true;
_ultimaAmostraValidaMs =
millis();
portEXIT_CRITICAL(&_snapshotLock);
}
void AferirTemperatura() {
/*
* UMA única aquisição lógica.
*
* No EXPANSOR_ADS:
* PinoModel::get(N) ->
* I2CService::RealizarLeituraADS()
*
* O ownership do I2C fica mantido durante
* todas as N conversões.
*/
const int novaLeitura =
_pino.get(
QuantidadeAmostras
);
if (novaLeitura < 0) {
RegistrarFalhaAquisicao(
"falha ao obter amostra ADC"
);
return;
}
float novaTensaoAdc = NAN;
if (
!ConverterRawParaTensaoAdc(
novaLeitura,
novaTensaoAdc
)
) {
RegistrarFalhaAquisicao(
"raw/tensao ADC invalido"
" | raw=" +
String(novaLeitura)
);
return;
}
float vRetorno = NAN;
if (
!CalcularTensaoRetorno(
novaTensaoAdc,
vRetorno
)
) {
RegistrarFalhaAquisicao(
"tensao de retorno invalida"
" | raw=" +
String(novaLeitura) +
" | Vadc=" +
String(
novaTensaoAdc,
3
)
);
return;
}
float resistenciaNtc = NAN;
if (
!CalcularResistenciaNtc(
vRetorno,
resistenciaNtc
)
) {
RegistrarFalhaAquisicao(
"resistencia NTC invalida"
" | Vadc=" +
String(
novaTensaoAdc,
3
) +
" | Vret=" +
String(
vRetorno,
3
)
);
return;
}
float novaTemperatura = NAN;
if (
!CalcularTemperaturaC(
resistenciaNtc,
novaTemperatura
)
) {
RegistrarFalhaAquisicao(
"temperatura fora da faixa"
" | Rntc=" +
String(
resistenciaNtc,
1
) +
" | Vret=" +
String(
vRetorno,
3
)
);
return;
}
RegistrarSucessoAquisicao();
PublicarAmostraValida(
novaLeitura,
novaTensaoAdc,
novaTemperatura
);
MostrarLog(
"Raw=" +
String(novaLeitura) +
" | Vadc=" +
String(
novaTensaoAdc,
3
) +
" V | Vret=" +
String(
vRetorno,
3
) +
" V | Rntc=" +
String(
resistenciaNtc,
1
) +
" ohm | T=" +
String(
novaTemperatura,
2
) +
" C"
);
}
};
#endif