Týden 6 - Senzory
O projektu
Cílem tohot týdne bylo seznámit se s použitím senzorů a jejich zapojením. Jako projekt jsem si vybral práci na čtyřvodičovém měřiči odporu s použitím pro měření PT100 snímače teploty.
Zapojení
Nejdříve bylo třeba propojit měřící obvod s mikrokontrolerem. Z počátku jsem se snažil použít nucleo, ale vzheledem ke složitosti debugování jsem přešel na arduino (Mbed podpora končí, Keil neumní sledovat sériovou linku...). Po celém dnu snahy zporovznit nucleo jsem se rozhodl, že to nemá cenu a přešel jsem na arduino. Propojil jsem arduino s měřícím obvodem MAX 31865. K propojení bylo třeba otevřít v Kicadu schéma PCB desky.
Kicad schéma
Zapojení
Program
Zde nastal nevjětší kámen úrazu. Výstupy z měřícího obvodu jsem se snažil přečíst pomocí knihovny MAX31865. Bohužel jsem narazil na problém s připojením. Zatím senzor posílá LSB a MSB, ale nic jiného. Nejspíše knihovna uvnitř arduinoIDE nefunguje správně. Zatím jsem se snažil použít knihovnu Adafruit MAX31865, ale ani ta nefunguje. Zatím jsem se dostal k tomu, že mi senzor posílá LSB a MSB, ale nic jiného. Nejspíše knihovna uvnitř arduinoIDE nefunguje správně. Zatím jsem se snažil použít knihovnu Adafruit MAX31865, ale ani ta nefunguje. Zkusím se tedy podívat na výstupy MAX 31865 pomocí osciloskopu, zda dochází k odesílaní dat. Je možné, že bude třeba manuálně specifikovat HEX hodnotu pro nastavení senzoru pro čtyřvodičové zapojení a číst data přímo z registru a přepínat SCK pin na arduinu. Jakmile budu mít přístup k osciloskopu a možnost debugování, pokusím se senzor a jeho výstupy otestovat a zkalibrovat. To vše zde doplním.
Oprava
Po zkoumání Kicad návrhu jsem si všiml, že nesedí piny s popisem. Napsal jsem vedoucímu a zjistili jsme, že jsem omylem obdržel špatnou desku 🙂. Na nové desce jsem již správně zapojil MISO,SCK,MOSI a CS piny. Nakonec jsem použil Nucleo F303RE programované v arduino IDE. Napsal jsem nakonec jednoduchý kód pro čtení RTD a převodu na teplotu pro budoucí zapojení čidla PT100. Metodu jsem zvolil jako polling dat po prodlevě 70ms. V budoucnu bych rád přidal interrupt pro čtení dat, ovšem nedokázal jsem nastavit max převodník na kontinuální režim a interrupt fungoval pouze až po resetování převodníku. Polling je ale naprosto dostačující a funguje bez problémů.
#include <Adafruit_MAX31865.h>
#include <SPI.h> // Include SPI library
// CS pins
#define PT1_CS PA8
#define PT2_CS PB6
// DRDY pins
#define PT1_DRDY PA0 // TODO
#define PT2_DRDY PA1
Adafruit_MAX31865 sensor1 = Adafruit_MAX31865(PT1_CS);
Adafruit_MAX31865 sensor2 = Adafruit_MAX31865(PT2_CS);
// Definovane hodnoty pro pt100
const float RTD_NOMINAL = 100.0;
const float REF_RESISTOR = 400.0; // Reference na desce
void setup() {
// Init SPI
SPI.begin();
Serial.begin(115200);
while (!Serial) delay(10); // Delay
// 4 dratove zapojeni
sensor1.begin(MAX31865_4WIRE);
sensor2.begin(MAX31865_4WIRE);
sensor1.enableBias(true);
sensor1.autoConvert(true);
sensor1.enable50Hz(true);
}
void loop() { // TODO: check for data ready For reading the output. (around 6hzs)
// Read temperature from each sensor - adafruit library
float temp1 = sensor1.temperature(RTD_NOMINAL, REF_RESISTOR);
float temp2 = sensor2.temperature(RTD_NOMINAL, REF_RESISTOR);
// Read RTD raw values and calculate resistance
uint16_t rtd1 = sensor1.readRTD();
uint16_t rtd2 = sensor2.readRTD();
float resistance1 = ((float)rtd1 / 32768.0) * REF_RESISTOR; // Using 15bit ADC - MSB is 32768
float resistance2 = ((float)rtd2 / 32768.0) * REF_RESISTOR;
// Nemenit - appka ocekava tento vystup
Serial.print("T1:");
Serial.print(temp1);
Serial.print(",T2:");
Serial.print(temp2);
Serial.print(",R1:");
Serial.print(resistance1, 2);
Serial.print(",R2:");
Serial.println(resistance2, 2); // println ends the line
delay(70); // Delay
}
Finální obvod
Diagnostika na logickém analyzátoru
Vizualizace dat a ukládání do CSV
Vytvořil jsem jednoduché GUI pro vizualizaci dat a ukládání do CSV. GUI je napsáno v Processingu.
import processing.serial.*;
import grafica.*;
import controlP5.*;
Serial myPort;
ControlP5 cp5;
DropdownList portList;
Textlabel statusLabel;
GPlot plotTemp;
GPointsArray pointsT1, pointsT2;
float startTime;
PrintWriter csvWriter;
String logFileName = "temperature_log.csv";
boolean loggingEnabled = true;
// Latest values for display
float lastT1 = Float.NaN;
float lastT2 = Float.NaN;
// Visibility toggles
boolean showT1 = true;
boolean showT2 = true;
// Fixed y-axis parameters for single-channel
final float yStep = 0.1; // step size in °C
final int nYTicks = 11; // ticks for single-channel view
void setup() {
size(900, 700);
cp5 = new ControlP5(this);
// Serial ports dropdown
portList = cp5.addDropdownList("ports").setPosition(10, 10).setSize(120, 200);
for (int i = 0; i < Serial.list().length; i++) {
portList.addItem(Serial.list()[i], i);
}
// connect button
cp5.addButton("connect").setPosition(150, 10).setSize(50, 20);
// Status label
statusLabel = cp5.addTextlabel("status").setPosition(210,10).setSize(200,20)
.setText("Select a port and connect");
// channel selection
cp5.addToggle("showT1").setPosition(300,10).setSize(50,20).setLabel("T1").setValue(true);
cp5.addToggle("showT2").setPosition(360,10).setSize(50,20).setLabel("T2").setValue(true);
// temp plot
plotTemp = new GPlot(this);
plotTemp.setPos(20,50);
plotTemp.setDim(700,400);
plotTemp.setTitleText("Temperature over Time");
plotTemp.getXAxis().setAxisLabelText("Time (s)");
plotTemp.getXAxis().setNTicks(12);
plotTemp.getYAxis().setAxisLabelText("Temperature (°C)");
pointsT1 = new GPointsArray();
pointsT2 = new GPointsArray();
plotTemp.addLayer("T1", pointsT1);
plotTemp.addLayer("T2", pointsT2);
startTime = millis();
// CSV header
csvWriter = createWriter(logFileName);
csvWriter.println("Time(s),T1_deg,T2_deg");
}
void connect() {
if (myPort != null) {
myPort.stop(); myPort = null;
statusLabel.setText("Disconnected");
}
int sel = (int)portList.getValue();
if (sel >= 0 && sel < Serial.list().length) {
try {
myPort = new Serial(this, Serial.list()[sel], 112500);
myPort.bufferUntil('\n');
statusLabel.setText("Connected to " + Serial.list()[sel]);
} catch (Exception e) {
statusLabel.setText("Error: Port unavailable");
}
} else statusLabel.setText("Error: Invalid port");
}
void showT1(boolean val) { showT1 = val; }
void showT2(boolean val) { showT2 = val; }
void serialEvent(Serial p) {
String s = p.readStringUntil('\n');
if (s == null) return;
s = trim(s);
float t = (millis() - startTime) / 1000.0;
for (String pair : split(s, ',')) {
String[] kv = split(pair, ':');
if (kv.length != 2) continue;
float v;
try { v = float(kv[1]); } catch (Exception e) { continue; }
if (kv[0].equals("T1")) { pointsT1.add(t, v); lastT1 = v; }
else if (kv[0].equals("T2")) { pointsT2.add(t, v); lastT2 = v; }
}
// remove old points and write to csv
while (pointsT1.getNPoints() > 0 && pointsT1.getX(0) < t - 120) pointsT1.remove(0);
while (pointsT2.getNPoints() > 0 && pointsT2.getX(0) < t - 120) pointsT2.remove(0);
statusLabel.setText("Receiving data");
if (loggingEnabled) {
csvWriter.println(nf((millis() - startTime)/1000.0, 0, 2) + "," +
(Float.isNaN(lastT1) ? "" : nf(lastT1, 0, 2)) + "," +
(Float.isNaN(lastT2) ? "" : nf(lastT2, 0, 2)));
}
}
void draw() {
background(255);
float cur = (millis() - startTime) / 1000.0;
plotTemp.setXLim(max(0, cur - 120), cur);
// Autoscale when both channels visible
if (showT1 && showT2) {
float minY = Float.MAX_VALUE, maxY = -Float.MAX_VALUE;
for (int i = 0; i < pointsT1.getNPoints(); i++) {
minY = min(minY, pointsT1.getY(i));
maxY = max(maxY, pointsT1.getY(i));
}
for (int i = 0; i < pointsT2.getNPoints(); i++) {
minY = min(minY, pointsT2.getY(i));
maxY = max(maxY, pointsT2.getY(i));
}
if (minY < maxY) {
float margin = (maxY - minY) * 0.1;
plotTemp.setYLim(minY - margin, maxY + margin);
plotTemp.getYAxis().setNTicks(15);
}
} else {
// Fixed center for single channel
float centerY = 0;
int count = 0;
if (showT1 && !Float.isNaN(lastT1)) { centerY += lastT1; count++; }
if (showT2 && !Float.isNaN(lastT2)) { centerY += lastT2; count++; }
centerY = count > 0 ? centerY / count : 0;
float halfSpan = yStep * (nYTicks - 1) / 2.0;
plotTemp.setYLim(centerY - halfSpan, centerY + halfSpan);
plotTemp.getYAxis().setNTicks(nYTicks);
}
// Update layers
GPointsArray empty = new GPointsArray();
plotTemp.setPoints(showT1 ? pointsT1 : empty, "T1");
plotTemp.setPoints(showT2 ? pointsT2 : empty, "T2");
// Draw plot
plotTemp.beginDraw();
plotTemp.drawBackground(); plotTemp.drawBox();
plotTemp.drawXAxis(); plotTemp.drawYAxis(); plotTemp.drawTitle();
plotTemp.drawGridLines(GPlot.BOTH);
plotTemp.getLayer("T1").setLineColor(color(255,0,0));
plotTemp.getLayer("T2").setLineColor(color(0,0,255));
plotTemp.drawLines(); plotTemp.endDraw();
// show current values
fill(0); textSize(14); textAlign(LEFT);
int y0 = 570;
text("Current T1: " + (Float.isNaN(lastT1)?"N/A":nf(lastT1,0,2)) + " °C", 30, y0);
text("Current T2: " + (Float.isNaN(lastT2)?"N/A":nf(lastT2,0,2)) + " °C", 450, y0);
}
void exit() {
if (csvWriter != null) { csvWriter.flush(); csvWriter.close(); }
super.exit();
}
Měření
Jelikož jsem si o děkanském dnu konečně zprovoznil Ikea Lack enclosure na moji 3d tiskárnu, rozhodl jsem se otestovat na kolik se dokáže uvnitř bez izolace vytopit. K tomu jsem použil můj vyrobený senzor a následně zapnul měření. Komora byla zahřívána pomocí tiskové podložky nastavené na 80 °C
Měření
Výsledky se automaticky zapisovali do CSV souboru, ze kterého šel snadno vytvořit graf
Graf
Jak je vidět, tak se mi podařilo dosáhnout teploty pouhých 26 stupňů Celsia. Budu muset tedy lépe izolovat.