fix(ntc): the temperature sensor read wrong on 3.3V boards, and three examples decoded backwards
Two independent defects, both visible as "the NTC reports the wrong temperature" (issue #233). 1. The part built its divider against a hard-coded 5 V rail, while the SPICE path used the board's real rail. They only agreed on AVR. On a 3.3 V board the injected node sat above the ADC's full scale: the slider at 25 C read ~1.4 C, and every slider value below ~11 C saturated to the same number, so -40, 0 and 10 C were indistinguishable. The rail now comes from the board (analogRailVolts), matching setAdcVoltage's own AVR/RP2040/ESP32 split. 2. The divider convention was corrected in608c2538(VCC -> R_pull -> OUT -> R_NTC -> GND) but data/examples.ts was not updated with it, so three of the four bundled NTC sketches still decoded the OTHER topology. Their readings came out mirrored about 25 C: the uno-ntc example printed -3.42 C with the slider at 60 C - the exact symptomdd22bcfehad recorded as the bug. Fixed in uno-ntc, pico-ntc and attiny85-ntc-sensor; examples-circuits.ts was already right. The sibling analog sensors (gas, flame, heart-beat, sound) map their sliders 0..1023 onto the same hard-coded 5 V and will saturate the same way on 3.3 V boards. Left alone here: that is not what #233 reports, and each one needs its examples re-checked before changing.
This commit is contained in:
parent
0d9a26d951
commit
d4ec4fe04d
|
|
@ -152,6 +152,43 @@ describe('ntc-temperature-sensor — attachEvents', () => {
|
|||
// ADC should remain zeroed
|
||||
expect(adc.channelValues[0]).toBe(0);
|
||||
});
|
||||
|
||||
/**
|
||||
* Issue #233. The divider used to be computed against a hard-coded 5 V rail,
|
||||
* so on a 3.3 V board it handed the ADC a voltage above full scale: 25 C came
|
||||
* back as ~1.4 C and everything below ~11 C saturated at the same reading.
|
||||
* The rail now follows the board, so the midpoint of a 10k/10k divider is
|
||||
* half of THAT rail.
|
||||
*/
|
||||
it('scales the divider to a 3.3V board instead of assuming 5V', () => {
|
||||
const logic = PartSimulationRegistry.get('ntc-temperature-sensor')!;
|
||||
const injected: number[] = [];
|
||||
// A simulator exposing setAdcVoltage is the ESP32 bridge shim.
|
||||
const sim = {
|
||||
...makeSimulator(makeADC()),
|
||||
setAdcVoltage: (_pin: number, volts: number) => {
|
||||
injected.push(volts);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
logic.attachEvents!(makeElement(), sim as any, pinMap({ OUT: 34 }));
|
||||
|
||||
expect(injected).toHaveLength(1);
|
||||
// R_ntc(25 C) == R_pull == 10k, so OUT sits at exactly half the rail.
|
||||
expect(injected[0]).toBeCloseTo(1.65, 2);
|
||||
expect(injected[0]).toBeLessThan(3.3); // never above the board's full scale
|
||||
});
|
||||
|
||||
it('keeps the 5V midpoint on an AVR board', () => {
|
||||
const logic = PartSimulationRegistry.get('ntc-temperature-sensor')!;
|
||||
const adc = makeADC();
|
||||
const sim = makeSimulator(adc);
|
||||
|
||||
logic.attachEvents!(makeElement(), sim as any, pinMap({ OUT: 14 }));
|
||||
|
||||
expect(adc.channelValues[0]).toBeCloseTo(2.5, 2);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Gas Sensor ──────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -7921,13 +7921,11 @@ const float NOM_TEMP_K = 298.15; // 25 °C in Kelvin
|
|||
float readTempC() {
|
||||
int raw = analogRead(NTC_PIN);
|
||||
float v = raw * (VCC / 1023.0);
|
||||
// Voltage divider topology used by standard NTC modules and by Velxio's
|
||||
// wokwi-ntc-temperature-sensor: VCC → R_NTC → A1 → R_pull (10k) → GND.
|
||||
// Higher temperature → R_NTC drops → V rises, so the NTC resistance is
|
||||
// r = R_pull * (VCC - v) / v
|
||||
// Using the inverted form (r = R_pull * v / (VCC - v)) gives the wrong
|
||||
// sign and Steinhart-Hart returns negative temperatures for hot inputs.
|
||||
float r = SERIES_R * (VCC - v) / v;
|
||||
// Divider modelled by Velxio's ntc-temperature-sensor:
|
||||
// VCC → R_pull (10k) → A1 → R_NTC → GND.
|
||||
// Higher temperature → R_NTC drops → V FALLS, so
|
||||
// r = R_pull * v / (VCC - v)
|
||||
float r = SERIES_R * v / (VCC - v);
|
||||
// Steinhart–Hart simplified equation
|
||||
float st = log(r / NOM_R) / B_COEFF + 1.0 / NOM_TEMP_K;
|
||||
return (1.0 / st) - 273.15;
|
||||
|
|
@ -8256,8 +8254,8 @@ const float NOM_TEMP_K = 298.15; // 25 °C
|
|||
float readTempC() {
|
||||
int raw = analogRead(NTC_PIN);
|
||||
float v = raw * (VCC / 1023.0);
|
||||
// VCC → R_NTC → A1 → R_pull → GND (standard NTC module topology)
|
||||
float r = SERIES_R * (VCC - v) / v;
|
||||
// VCC → R_pull → A1 → R_NTC → GND (the divider Velxio models)
|
||||
float r = SERIES_R * v / (VCC - v);
|
||||
float st = log(r / NOM_R) / B_COEFF + 1.0 / NOM_TEMP_K;
|
||||
return (1.0 / st) - 273.15;
|
||||
}
|
||||
|
|
@ -9954,7 +9952,9 @@ const float B_COEFF = 3950.0;
|
|||
|
||||
float readTemperature() {
|
||||
int raw = analogRead(NTC_PIN);
|
||||
float resistance = SERIES_R * (1023.0 / raw - 1.0);
|
||||
// R_NTC sits on the LOW side, so raw/1023 = R_NTC / (R_NTC + SERIES_R):
|
||||
// R_NTC = SERIES_R / (1023/raw - 1)
|
||||
float resistance = SERIES_R / (1023.0 / raw - 1.0);
|
||||
float steinhart = resistance / NOM_R;
|
||||
steinhart = log(steinhart) / B_COEFF;
|
||||
steinhart += 1.0 / (NOM_TEMP + 273.15);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import { PartSimulationRegistry } from './PartSimulationRegistry';
|
||||
import { setAdcVoltage, emitPropertyChange } from './partUtils';
|
||||
import { setAdcVoltage, emitPropertyChange, analogRailVolts } from './partUtils';
|
||||
import { registerSensorUpdate, unregisterSensorUpdate } from '../SensorUpdateRegistry';
|
||||
|
||||
// ─── Tilt Switch ─────────────────────────────────────────────────────────────
|
||||
|
|
@ -64,11 +64,17 @@ PartSimulationRegistry.register('tilt-switch', {
|
|||
*
|
||||
* The injected OUT voltage uses the SAME β-model voltage divider the circuit
|
||||
* (and the example sketch) assume: a 10k pull-up from VCC to OUT and the NTC
|
||||
* from OUT to GND, so V_OUT = 5 · R_ntc / (R_ntc + R_pull) with
|
||||
* from OUT to GND, so V_OUT = VCC · R_ntc / (R_ntc + R_pull) with
|
||||
* R_ntc(T) = R0 · exp(β · (1/T − 1/T0)). This makes the injected ADC voltage
|
||||
* decode straight back to the slider value (set 50°C → read 50°C). When the
|
||||
* electrical (SPICE) engine is active it drives A0 from the ngspice solve
|
||||
* instead, using the matching topology in componentToSpice.ts — both agree.
|
||||
*
|
||||
* VCC is the rail of the board the sensor is wired to, not a constant 5. The
|
||||
* divider used to assume 5 V everywhere, which on a 3.3 V board put the node
|
||||
* above the ADC's full scale: the slider read 1.4°C where it said 25, and
|
||||
* everything below ~11°C pinned at the same saturated value (issue #233). The
|
||||
* SPICE path already used the board's rail, so the two only agreed on AVR.
|
||||
*/
|
||||
PartSimulationRegistry.register('ntc-temperature-sensor', {
|
||||
attachEvents: (_element, simulator, getArduinoPinHelper, componentId) => {
|
||||
|
|
@ -77,9 +83,10 @@ PartSimulationRegistry.register('ntc-temperature-sensor', {
|
|||
const NTC_R0 = 10_000;
|
||||
const NTC_BETA = 3950;
|
||||
const R_PULL = 10_000;
|
||||
const vcc = analogRailVolts(simulator);
|
||||
const tempToVolts = (temp: number) => {
|
||||
const rNtc = NTC_R0 * Math.exp(NTC_BETA * (1 / (temp + 273.15) - 1 / 298.15));
|
||||
return Math.max(0, Math.min(5, 5 * (rNtc / (rNtc + R_PULL))));
|
||||
return Math.max(0, Math.min(vcc, vcc * (rNtc / (rNtc + R_PULL))));
|
||||
};
|
||||
|
||||
// Room temperature default
|
||||
|
|
|
|||
|
|
@ -36,6 +36,25 @@ export function getADC(avrSimulator: AnySimulator): any | null {
|
|||
return (avrSimulator as any).getADC?.() ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supply rail the analog front end of THIS board runs on, in volts.
|
||||
*
|
||||
* A part that builds a resistive divider has to use the rail it is actually
|
||||
* wired to. Assuming 5 V everywhere is not a small error on a 3.3 V board: it
|
||||
* pushes the computed node voltage above the ADC's full scale, so the reading
|
||||
* saturates and a whole stretch of the control slider reports the same value.
|
||||
* (An NTC divider assumed at 5 V reads 1.4 C on an ESP32 when the slider says
|
||||
* 25 C, and pins at full scale for everything below ~11 C — issue #233.)
|
||||
*
|
||||
* The split matches setAdcVoltage's own branches: AVR converts against a 5 V
|
||||
* reference, RP2040 and the ESP32 bridge against 3.3 V.
|
||||
*/
|
||||
export function analogRailVolts(simulator: AnySimulator): number {
|
||||
if (typeof (simulator as any).setAdcVoltage === 'function') return 3.3; // ESP32 bridge
|
||||
if (simulator instanceof RP2040Simulator) return 3.3;
|
||||
return 5.0; // AVR
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an analog voltage to an ADC channel, supporting AVR, RP2040, and ESP32.
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in New Issue