@JPLABS
VIN i VOUT se měří jako komplexní fázory pomocí synchronní detekce (lock-in / jednobinová DFT). To jsou přesně tyhle řádky:
vin.re += rawVin * cosf(phVin);
vin.im -= rawVin * sinf(phVin);
vout.re += rawVout * cosf(phVout);
vout.im -= rawVout * sinf(phVout);
Pak se dělá
complexSubtract(vin, vout) a
complexDivide(vout, delta) — tedy celý vztah Zx = Rref·VOUT/(VIN−VOUT) běží v komplexní rovině. Klíčové je, že (VIN − VOUT)
není |VIN| − |VOUT|; je to komplexní rozdíl, který nese amplitudu i fázi proudu rezistorem. Výsledek Z má proto reálnou i imaginární složku (
z.re,
z.im) a kód z nich počítá jak modul, tak fázi přes
atan2(z.im, z.re). Reálná i imaginární složka tam tedy jsou — kondenzátor vyjde s fází blízko −90°, cívka +90°.
K té „zmizelé frekvenci": to je ve skutečnosti správně a záměrně. Z = U/I je definice impedance při dané budicí frekvenci — frekvence do toho vzorce explicitně nepatří. Srovnej s Ohmovým zákonem: i čistý rezistor má Z = R bez jakéhokoli ω, a pořád je to impedance. ω se objeví, až když napíšeš uzavřený vzorec konkrétní součástky (Z = 1/jωC). Jenže analyzátor žádný uzavřený vzorec nepočítá — on prostě budí na frekvenci f, změří komplexní U a I a vydělí je. Frekvenční závislost je schovaná v naměřených fázorech a v tom, že se měří ve sweepu.
Konkrétně: 1µF kondenzátor má při 100 Hz Z ≈ −j1592 Ω, při 1 kHz Z ≈ −j159 Ω. Tenhle obvod oba případy rozliší správně, protože naměřené fázory VOUT a (VIN−VOUT) jsou na těch dvou frekvencích jiné. A sweep 100 Hz – 4 kHz přesně tohle dělá — dává ti Z(f), tedy spektrum. To je definice impedančního analyzátoru.
Kde má tvá připomínka praktický základ: rámeček „PRINCIP MĚŘENÍ" ve schématu píše vzorec ve skalárním tvaru, bez jakékoli zmínky, že VIN a VOUT jsou komplexní fázory ze synchronní detekce. Kdo vidí jen ten obrázek a ne kód, snadno to přečte jako stejnosměrný dělič a dojde k přesně té námitce, kterou jsi napsal. Takže chyba není v měření, ale v prezentaci.
Opravené schéma:
Opravený kód:
Kód: Vybrat vše
/*
Arduino Impedancni Analyzator v2.0.0 (opravena verze)
Deska: Arduino UNO R4 Minima / UNO R4 WiFi (nutny skutecny DAC na A0)
Displej: ILI9341 320x240 SPI, knihovny Adafruit_GFX + Adafruit_ILI9341
Zapojeni, ktere tento program ocekava:
A0/DAC OUT ---- VIN ---- RREF 1k ---- VOUT ---- Zx ---- GND
| |
R2 1k -+-> A1 R3 1k -+-> A2
| | | |
C1 100p D1 (->GND) C2 100p D2 (->GND)
ZMENY OPROTI PUVODNIMU NAVRHU:
- C1, C2 zmenseny ze 100 nF na 100 pF. Puvodnich 100 nF se 100k tvorilo
dolni propust se zlomem ~16 Hz a tlumilo cele merici pasmo 100 Hz - 4 kHz.
S R = 1k a C = 100 pF je zlom ~1,6 MHz, tj. mimo merici pasmo. Kondenzatory
tak slouzi jen jako RFI / anti-alias filtr a nemeni amplitudu ani fazi.
- R2, R3 zmenseny ze 100k na 1k -> rychle ustaleni vstupu ADC, stale
dostatecna seriova ochrana spolu s clampovacimi diodami D1/D2.
- C3 (100 nF) NENI v merici ceste u uzlu VOUT, ale jako blokovaci kondenzator
napajeni. U VOUT by zkratoval DUT (|Z|~400 Ohm pri 4 kHz).
- Demodulace probiha podle SKUTECNEHO casu odberu kazdeho vzorku. Tim se
automaticky odstrani fazovy skew mezi A1 a A2 (dve postupne konverze ADC)
i casovy jitter. To je nejdulezitejsi oprava pro spravnou fazi a pro
spravny modul u impedanci vyssich nez Rref.
Princip:
I = (VIN - VOUT) / RREF
Zx = VOUT / I = RREF * VOUT / (VIN - VOUT)
Program snima komplexni zakladni harmonickou pomoci synchronni demodulace,
takze zobrazuje modul |Z| i fazi impedance.
*/
#include <Arduino.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <math.h>
#define PROGRAM_VERSION "v2.0.0"
// ------------------------------ Piny ---------------------------------------
static constexpr uint8_t PIN_DAC = A0;
static constexpr uint8_t PIN_VIN = A1;
static constexpr uint8_t PIN_VOUT = A2;
static constexpr uint8_t TFT_CS = 10;
static constexpr uint8_t TFT_DC = 9;
static constexpr uint8_t TFT_RST = 8;
// SPI: SCK = D13, MOSI = D11 (hardwarove SPI piny UNO R4)
Adafruit_ILI9341 tft(TFT_CS, TFT_DC, TFT_RST);
// --------------------------- Merici parametry ------------------------------
static constexpr float RREF_OHM = 1000.0f;
static constexpr uint16_t DAC_MID = 2048; // 12bit DAC; stred signalu
static constexpr uint16_t DAC_AMPLITUDE = 1000; // bezpecne pod maximem 4095
static constexpr uint8_t SETTLE_CYCLES = 3;
static constexpr uint8_t MEASURE_CYCLES = 6;
static constexpr uint8_t MAX_POINTS_PER_CYCLE = 48;
// Volitelna softwarova korekce zbytkove faze (v radianech). Zjisti se tak, ze
// se zmeri znamy CISTY rezistor (napr. 1k) a hodnota se nastavi tak, aby jeho
// namerena faze byla ~0 deg. Ponech 0.0f, pokud korekci nepouzivas.
static constexpr float PHASE_TRIM_RAD = 0.0f;
static const float TEST_FREQUENCIES_HZ[] = {
100.0f, 160.0f, 250.0f, 400.0f, 630.0f,
1000.0f, 1600.0f, 2500.0f, 4000.0f
};
static constexpr uint8_t FREQ_COUNT = sizeof(TEST_FREQUENCIES_HZ) / sizeof(TEST_FREQUENCIES_HZ[0]);
// ------------------------------ Graf ---------------------------------------
static constexpr int16_t GRAPH_X = 42;
static constexpr int16_t GRAPH_Y = 49;
static constexpr int16_t GRAPH_W = 267;
static constexpr int16_t GRAPH_H = 130;
static constexpr float GRAPH_Z_MIN = 10.0f;
static constexpr float GRAPH_Z_MAX = 100000.0f;
struct ComplexValue {
float re;
float im;
};
struct Measurement {
float frequencyHz;
float magnitudeOhm;
float phaseDeg;
float vinMagnitude;
float voutMagnitude;
bool valid;
};
Measurement measurements[FREQ_COUNT];
bool continuousMode = true;
uint32_t lastSweepMs = 0;
// ------------------------- Komplexni matematika ----------------------------
ComplexValue complexSubtract(const ComplexValue &a, const ComplexValue &b) {
return {a.re - b.re, a.im - b.im};
}
float complexMagnitude(const ComplexValue &a) {
return sqrtf(a.re * a.re + a.im * a.im);
}
ComplexValue complexDivide(const ComplexValue &a, const ComplexValue &b) {
const float d = b.re * b.re + b.im * b.im;
if (d < 1.0e-12f) {
return {NAN, NAN};
}
return {(a.re * b.re + a.im * b.im) / d,
(a.im * b.re - a.re * b.im) / d};
}
// Otoceni komplexniho cisla o uhel (pro softwarovy fazovy trim).
ComplexValue complexRotate(const ComplexValue &a, float angleRad) {
const float c = cosf(angleRad);
const float s = sinf(angleRad);
return {a.re * c - a.im * s, a.re * s + a.im * c};
}
// ----------------------------- Pomocne funkce ------------------------------
uint8_t pointsPerCycleForFrequency(float frequencyHz) {
if (frequencyHz <= 1000.0f) return 48;
if (frequencyHz <= 2500.0f) return 32;
return 20;
}
uint16_t dacCodeForPhase(float phaseRad) {
const float value = static_cast<float>(DAC_MID) + static_cast<float>(DAC_AMPLITUDE) * sinf(phaseRad);
if (value < 0.0f) return 0;
if (value > 4095.0f) return 4095;
return static_cast<uint16_t>(value + 0.5f);
}
void waitUntilMicros(uint32_t targetUs) {
while (static_cast<int32_t>(micros() - targetUs) < 0) {
// Aktivni cekani zamerne: udrzuje pravidelnost buzeni a vzorkovani.
}
}
void setDacMidpoint() {
analogWrite(PIN_DAC, DAC_MID);
}
// ------------------------------- Mereni ------------------------------------
Measurement measureAtFrequency(float frequencyHz) {
Measurement result{};
result.frequencyHz = frequencyHz;
result.valid = false;
const uint8_t points = pointsPerCycleForFrequency(frequencyHz);
const uint16_t samples = static_cast<uint16_t>(points) * MEASURE_CYCLES;
const float stepPeriodUsF = 1000000.0f / (frequencyHz * static_cast<float>(points));
const uint32_t stepPeriodUs = static_cast<uint32_t>(stepPeriodUsF + 0.5f);
// Frekvence, kterou ve skutecnosti nastavime celociselnym casovanim.
const float actualFrequencyHz = 1000000.0f / (static_cast<float>(stepPeriodUs) * static_cast<float>(points));
result.frequencyHz = actualFrequencyHz;
// Uhlova frekvence v radianech na mikrosekundu (pro demodulaci podle casu).
const float omega = TWO_PI * actualFrequencyHz * 1.0e-6f;
uint32_t nextUs = micros() + 200;
// Ustaleni obvodu pred vlastnim snimanim.
for (uint16_t n = 0; n < static_cast<uint16_t>(points) * SETTLE_CYCLES; ++n) {
const uint8_t k = n % points;
const float phase = TWO_PI * static_cast<float>(k) / static_cast<float>(points);
analogWrite(PIN_DAC, dacCodeForPhase(phase));
nextUs += stepPeriodUs;
waitUntilMicros(nextUs);
}
ComplexValue vin{0.0f, 0.0f};
ComplexValue vout{0.0f, 0.0f};
// Casova reference faze buzeni: zacatek mericiho okna (krok 0).
const uint32_t windowStartUs = nextUs;
for (uint16_t n = 0; n < samples; ++n) {
const uint8_t k = n % points;
const float drivePhase = TWO_PI * static_cast<float>(k) / static_cast<float>(points);
// DAC drzi krokovany sinus podle drivePhase.
analogWrite(PIN_DAC, dacCodeForPhase(drivePhase));
// Vzorek se odebere priblizne uprostred nastaveneho DAC kroku.
waitUntilMicros(nextUs + stepPeriodUs / 2U);
// KLICOVA OPRAVA: kazdy kanal demoduluji podle SKUTECNEHO casu sveho
// odberu (tVin, tVout). Dve konverze ADC bezi za sebou, takze A2 je
// vzorkovan o jednu konverzi pozdeji nez A1. Pokud se oba kanaly
// demoduluji stejnou nominalni fazi (puvodni kod), vznika differencni
// fazova chyba ~2*pi*f*dt, ktera se NEvykrati a kazi fazi (a pres
// komplexni deleni i modul). Pouzitim realneho casu odberu se skew
// i pripadny jitter automaticky odstrani.
const uint32_t tVin = micros();
const float rawVin = static_cast<float>(analogRead(PIN_VIN));
const uint32_t tVout = micros();
const float rawVout = static_cast<float>(analogRead(PIN_VOUT));
const float phVin = omega * static_cast<float>(tVin - windowStartUs);
const float phVout = omega * static_cast<float>(tVout - windowStartUs);
vin.re += rawVin * cosf(phVin);
vin.im -= rawVin * sinf(phVin);
vout.re += rawVout * cosf(phVout);
vout.im -= rawVout * sinf(phVout);
nextUs += stepPeriodUs;
waitUntilMicros(nextUs);
}
setDacMidpoint();
const float scale = 2.0f / static_cast<float>(samples);
vin.re *= scale;
vin.im *= scale;
vout.re *= scale;
vout.im *= scale;
const ComplexValue delta = complexSubtract(vin, vout);
const float deltaMagnitude = complexMagnitude(delta);
result.vinMagnitude = complexMagnitude(vin);
result.voutMagnitude = complexMagnitude(vout);
// Pri velmi malem napeti na RREF nelze spolehlive urcit proud.
if (result.vinMagnitude < 2.0f || deltaMagnitude < 1.5f) {
result.magnitudeOhm = NAN;
result.phaseDeg = NAN;
return result;
}
ComplexValue z = complexDivide(vout, delta);
z.re *= RREF_OHM;
z.im *= RREF_OHM;
// Volitelna softwarova korekce zbytkove faze (z kalibrace na znamem rezistoru).
if (PHASE_TRIM_RAD != 0.0f) {
z = complexRotate(z, PHASE_TRIM_RAD);
}
result.magnitudeOhm = complexMagnitude(z);
result.phaseDeg = atan2f(z.im, z.re) * 180.0f / PI;
result.valid = isfinite(result.magnitudeOhm) && isfinite(result.phaseDeg) && result.magnitudeOhm > 0.0f;
return result;
}
// ------------------------------- Displej -----------------------------------
void drawHeader() {
tft.fillScreen(ILI9341_BLACK);
tft.setTextColor(ILI9341_CYAN);
tft.setTextSize(2);
tft.setCursor(6, 6);
tft.print(F("IMPEDANCE |Z|"));
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(1);
tft.setCursor(226, 10);
tft.print(PROGRAM_VERSION);
tft.setCursor(6, 28);
tft.print(F("UNO R4 DAC A0 | VIN A1 | VOUT A2 | Rref=1k"));
}
int16_t graphYForMagnitude(float magnitudeOhm) {
if (!isfinite(magnitudeOhm) || magnitudeOhm <= 0.0f) return GRAPH_Y + GRAPH_H;
float z = magnitudeOhm;
if (z < GRAPH_Z_MIN) z = GRAPH_Z_MIN;
if (z > GRAPH_Z_MAX) z = GRAPH_Z_MAX;
const float fraction = (log10f(z) - log10f(GRAPH_Z_MIN)) /
(log10f(GRAPH_Z_MAX) - log10f(GRAPH_Z_MIN));
return GRAPH_Y + GRAPH_H - static_cast<int16_t>(fraction * GRAPH_H + 0.5f);
}
int16_t graphXForIndex(uint8_t index) {
if (FREQ_COUNT <= 1) return GRAPH_X;
return GRAPH_X + static_cast<int16_t>((static_cast<long>(index) * GRAPH_W) / (FREQ_COUNT - 1));
}
void drawGraphBackground() {
tft.fillRect(0, 42, 320, 145, ILI9341_BLACK);
tft.drawRect(GRAPH_X, GRAPH_Y, GRAPH_W, GRAPH_H, ILI9341_DARKGREY);
const float labels[] = {10.0f, 100.0f, 1000.0f, 10000.0f, 100000.0f};
const char *labelText[] = {"10", "100", "1k", "10k", "100k"};
tft.setTextSize(1);
for (uint8_t i = 0; i < 5; ++i) {
const int16_t y = graphYForMagnitude(labels[i]);
tft.drawFastHLine(GRAPH_X, y, GRAPH_W, ILI9341_DARKGREY);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(4, y - 3);
tft.print(labelText[i]);
}
for (uint8_t i = 0; i < FREQ_COUNT; ++i) {
const int16_t x = graphXForIndex(i);
tft.drawFastVLine(x, GRAPH_Y, GRAPH_H, ILI9341_DARKGREY);
}
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(GRAPH_X, GRAPH_Y + GRAPH_H + 4);
tft.print(F("100Hz"));
tft.setCursor(GRAPH_X + 102, GRAPH_Y + GRAPH_H + 4);
tft.print(F("1k"));
tft.setCursor(GRAPH_X + GRAPH_W - 30, GRAPH_Y + GRAPH_H + 4);
tft.print(F("4k"));
}
void displayLatestValues(const Measurement &m) {
tft.fillRect(0, 195, 320, 45, ILI9341_BLACK);
tft.setTextSize(1);
tft.setTextColor(ILI9341_YELLOW);
tft.setCursor(6, 198);
tft.print(F("Posledni: "));
tft.print(m.frequencyHz, 0);
tft.print(F(" Hz"));
tft.setCursor(6, 212);
if (m.valid) {
tft.setTextColor(ILI9341_GREEN);
tft.print(F("|Z|="));
if (m.magnitudeOhm >= 1000.0f) {
tft.print(m.magnitudeOhm / 1000.0f, 2);
tft.print(F(" kOhm"));
} else {
tft.print(m.magnitudeOhm, 1);
tft.print(F(" Ohm"));
}
tft.setTextColor(ILI9341_MAGENTA);
tft.setCursor(172, 212);
tft.print(F("faze="));
tft.print(m.phaseDeg, 1);
tft.print(F(" deg"));
} else {
tft.setTextColor(ILI9341_RED);
tft.print(F("MIMO ROZSAH / bez proudu"));
}
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(6, 227);
tft.print(F("Serial: s=scan, c=continuous ON/OFF"));
}
void plotMeasurements() {
drawGraphBackground();
bool havePrevious = false;
int16_t previousX = 0;
int16_t previousY = 0;
for (uint8_t i = 0; i < FREQ_COUNT; ++i) {
if (!measurements[i].valid) {
havePrevious = false;
continue;
}
const int16_t x = graphXForIndex(i);
const int16_t y = graphYForMagnitude(measurements[i].magnitudeOhm);
if (havePrevious) {
tft.drawLine(previousX, previousY, x, y, ILI9341_GREEN);
}
tft.fillCircle(x, y, 2, ILI9341_YELLOW);
previousX = x;
previousY = y;
havePrevious = true;
}
}
// ------------------------------- Sweep -------------------------------------
void runSweep() {
tft.fillRect(0, 186, 320, 9, ILI9341_BLACK);
tft.setTextSize(1);
tft.setTextColor(ILI9341_CYAN);
tft.setCursor(6, 187);
tft.print(F("Merim..."));
Serial.println();
Serial.println(F("frequency_Hz;abs_Z_Ohm;phase_deg;Vin_DFT;Vout_DFT;status"));
for (uint8_t i = 0; i < FREQ_COUNT; ++i) {
measurements[i] = measureAtFrequency(TEST_FREQUENCIES_HZ[i]);
const Measurement &m = measurements[i];
Serial.print(m.frequencyHz, 2);
Serial.print(';');
if (m.valid) {
Serial.print(m.magnitudeOhm, 3);
Serial.print(';');
Serial.print(m.phaseDeg, 3);
Serial.print(';');
Serial.print(m.vinMagnitude, 3);
Serial.print(';');
Serial.print(m.voutMagnitude, 3);
Serial.println(F(";OK"));
} else {
Serial.print(F("NaN;NaN;"));
Serial.print(m.vinMagnitude, 3);
Serial.print(';');
Serial.print(m.voutMagnitude, 3);
Serial.println(F(";OUT_OF_RANGE"));
}
}
plotMeasurements();
displayLatestValues(measurements[FREQ_COUNT - 1]);
tft.fillRect(0, 186, 320, 9, ILI9341_BLACK);
tft.setTextColor(ILI9341_CYAN);
tft.setCursor(6, 187);
tft.print(continuousMode ? F("AUTO SWEEP") : F("MANUAL MODE"));
lastSweepMs = millis();
}
// ------------------------------ Arduino API --------------------------------
void setup() {
Serial.begin(115200);
delay(300);
analogWriteResolution(12);
analogReadResolution(12);
setDacMidpoint();
tft.begin();
tft.setRotation(1);
drawHeader();
drawGraphBackground();
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(6, 198);
tft.print(F("Start mereni..."));
Serial.println(F("Arduino Impedancni Analyzator " PROGRAM_VERSION));
Serial.println(F("UNO R4: DAC=A0, VIN=A1, VOUT=A2, RREF=1000 Ohm"));
Serial.println(F("Front-end: R2/R3=1k seriove, C1/C2=100pF RFI, D1/D2 clamp na GND."));
Serial.println(F("Poznamka: s jednim RREF=1k je prakticky rozsah zhruba 100 Ohm az 10 kOhm."));
Serial.println(F("Prikazy: s = jednorazovy sweep, c = prepnout automaticke mereni."));
runSweep();
}
void loop() {
if (Serial.available() > 0) {
const char command = static_cast<char>(Serial.read());
if (command == 's' || command == 'S') {
runSweep();
} else if (command == 'c' || command == 'C') {
continuousMode = !continuousMode;
Serial.print(F("Continuous mode: "));
Serial.println(continuousMode ? F("ON") : F("OFF"));
displayLatestValues(measurements[FREQ_COUNT - 1]);
}
}
if (continuousMode && millis() - lastSweepMs >= 1500UL) {
runSweep();
}
}