본문 바로가기
Sensor/온도센서(Temperature)

RTD 온도 센서 보상 알고리즘 (RTD Temperature Sensor Compensation Algorithm)

by linuxgo 2025. 8. 5.

이 문서는 STM32L432KC 마이크로컨트롤러와 MAX31865를 사용한 RTD 온도 센서의 보상 알고리즘을 설명합니다 (This document explains the compensation algorithm for RTD temperature sensors using STM32L432KC and MAX31865).

RTD+MAX31865

1. RTD 온도 센서 개요 (RTD Temperature Sensor Overview)

RTD는 온도에 따라 저항이 변화하는 센서입니다 (RTD is a sensor whose resistance changes with temperature). 백금(Pt100: 0°C에서 100Ω, Pt1000: 1000Ω, Pt10: 10Ω)과 니켈(Ni120: 120Ω)이 주로 사용됩니다 (Platinum (Pt100: 100Ω at 0°C, Pt1000: 1000Ω, Pt10: 10Ω) and nickel (Ni120: 120Ω) are commonly used).

1.1 표준별 사양 (Standards Specifications)

  • IEC 60751 (α=0.00385):
    • 범위 (Range): -200°C ~ +850°C
    • 저항-온도 관계 (Resistance-Temperature Relationship): 
    • 0°C 이상 (Above 0°C): \( R_t = R_0 \cdot [1 + A \cdot t + B \cdot t^2] \)
      0°C 미만 (Below 0°C): \( R_t = R_0 \cdot [1 + A \cdot t + B \cdot t^2 + C \cdot (t - 100) \cdot t^3] \)
    • 계수 (Coefficients): \( A = 3.9083 \times 10^{-3} \), \( B = -5.775 \times 10^{-7} \), \( C = -4.183 \times 10^{-12} \)
  • 미국 표준 (American Standard, α=0.00392):
    • 범위 (Range): -200°C ~ +850°C
    • 계수 (Coefficients): \( A = 3.926 \times 10^{-3} \), \( B = -5.92 \times 10^{-7} \), \( C = -4.0 \times 10^{-12} \)
  • Ni120 (ASTM E1137):
    • 범위 (Range): -80°C ~ +320°C
    • 계수 (Coefficients): \( A = 5.485 \times 10^{-3} \), \( B = 6.650 \times 10^{-6} \), \( C = 2.805 \times 10^{-11} \)
  • Pt10 (극저온, Cryogenic):
    • 범위 (Range): -260°C ~ +500°C
    • 계수 (Coefficients): IEC 60751과 동일 (Same as IEC 60751), \( R_0 = 10\Omega \)

1.2 보상 알고리즘 (Compensation Algorithm)

  1. 저항-온도 변환 (Resistance-to-Temperature Conversion)
    •   0°C 이상 (Above 0°C): 이차 방정식 풀이 (Quadratic equation solution)
    •   0°C 미만 (Below 0°C): 뉴턴-랩슨 반복법 (Newton-Raphson iteration)
    •   선형 근사 (Linear Approximation, -50°C ~ 150°C): 
    • \( t \approx \frac{R_t / R_0 - 1}{\alpha} \)
    •   LUT: 1°C 간격 저항 저장, 선형 보간 (Store resistances at 1°C intervals, linear interpolation)
  2. 리드선 저항 보상 (Lead Resistance Compensation):
    •   2선식 (2-Wire): \( R_t = R_{\text{measured}} - 2 \cdot R_{\text{lead}} \)
    •   3선식 (3-Wire): \( R_t = R_{\text{measured}} - \frac{R_{\text{L1}} + R_{\text{L3}}}{2} \)
    •   4선식 (4-Wire): \( R_t = \frac{V_{\text{measured}}}{I_{\text{excitation}}} \)
  3. 자가 발열 보상 (Self-Heating Compensation):
    \( \Delta T = \frac{I^2 \cdot R_t \cdot 1000}{\text{Dissipation Constant (2 mW/°C)}} \)
    여기 전류 (Excitation Current): 0.1mA~1mA
  4. 노이즈 필터링 (Noise Filtering):
    •   이동 평균 (Moving Average): 5개 샘플 (5 samples)
    •   칼만 필터 (Kalman Filter): Q=0.01, R=0.1
    •   MAX31865 50Hz 필터 (50Hz Filter): 65ms 변환 시간 (Conversion time)
  5. 캘리브레이션 (Calibration): 알려진 온도-저항 쌍으로 오프셋 보정 (Offset correction using known temperature-resistance pairs)
  6. 정확도 등급 (Accuracy Class):
    •   Class A: \( \pm(0.15 + 0.002|t|) \) °C
    •   Class B: \( \pm(0.3 + 0.005|t|) \) °C
  7. 에러 처리 (Error Handling): 저항(0Ω ~ 400Ω) 및 온도 범위 검증 (Validate resistance and temperature ranges)
  8. 로깅 (Logging): USART2로 온도, 저항, 오차 출력 (Output temperature, resistance, error via USART2)

1.3 추가 고려사항 (Additional Considerations)

  • 메모리 (Memory): STM32L432KC(256KB 플래시, 64KB SRAM) 내 동작 (Operates within 256KB Flash, 64KB SRAM). LUT: Pt100(4KB), Ni120(1.6KB), Pt10(3KB)
  • 저전력 (Low Power): Sleep 모드, WFI 사용 (Sleep mode, Wait For Interrupt)
  • 환경 (Environment): 방수/방진 케이스, EMI 차폐 (Waterproof/dustproof case, EMI shielding)
  • 확장 가능성 (Scalability): 다중 RTD, 시각화, 극저온 센서 (Multiple RTDs, visualization, cryogenic sensors)

2. 하드웨어 설정 (Hardware Configuration)

  • STM32L432KC:
    •   클럭 (Clock): HSI 80MHz
    •   SPI1: PA5(SCK), PA6(MISO), PA7(MOSI), PA4(CS, GPIO Output)
    •   USART2: PA2(TX), PA3(RX), 115200 baud
  • MAX31865:
    •   기준 저항 (Reference Resistance): 430Ω(Pt100), 4300Ω(Pt1000), 43Ω(Pt10), 480Ω(Ni120)
    •   필터 (Filter): 50Hz, 65ms 변환 (Conversion)

3. C 코드 구현 (C Code Implementation)

코드는 rtd.h, rtd.c, main.c로 분리됩니다 (Code is separated into rtd.h, rtd.c, main.c).

3.1 rtd.h

#ifndef RTD_H
#define RTD_H

#include "stm32l4xx_hal.h"
#include <stdint.h>

// MAX31865 레지스터 정의 (Register Definitions)
#define MAX31865_CONFIG_REG         0x00
#define MAX31865_RTD_MSB_REG        0x01
#define MAX31865_RTD_LSB_REG        0x02
#define MAX31865_FAULT_STATUS_REG   0x07
#define MAX31865_READ_ADDR(x)       (x)
#define MAX31865_WRITE_ADDR(x)      (x | 0x80)

// MAX31865 설정 (Configuration)
#define CONFIG_2WIRE        0xC0 // Vbias ON, 1-shot, 50Hz
#define CONFIG_3WIRE        0xD0
#define CONFIG_4WIRE        0xC0
#define R_REF_PT100         430.0f
#define R_REF_PT1000        4300.0f
#define R_REF_PT10          43.0f
#define R_REF_NI120         480.0f
#define ADC_MAX             32768.0f

// 필터 설정 (Filter Settings)
#define FILTER_SIZE         5
#define KALMAN_Q            0.01f
#define KALMAN_R            0.1f

// LUT 크기 (LUT Sizes)
#define LUT_SIZE_PT         1051 // -200°C ~ 850°C
#define LUT_SIZE_NI120      401  // -80°C ~ 320°C
#define LUT_SIZE_PT10       761  // -260°C ~ 500°C

typedef struct {
    float r0;              // 기준 저항 (Reference Resistance)
    float alpha;           // 온도 계수 (Temperature Coefficient)
    char wire_type[8];     // 연결 방식 (Wire Type)
    char accuracy_class;   // 'A', 'B'
    char sensor_type[8];   // "Pt100", "Pt1000", "Ni120", "Pt10"
    float A, B, C;         // RTD 계수 (RTD Coefficients)
    float max_temp;        // 최대 온도 (Max Temperature)
    float min_temp;        // 최소 온도 (Min Temperature)
    float dissipation_constant; // 자가 발열 상수 (Self-Heating Constant, mW/°C)
    float calibration_offset;   // 캘리브레이션 오프셋 (Calibration Offset)
    float resistance_buffer[FILTER_SIZE]; // 이동 평균 버퍼 (Moving Average Buffer)
    uint8_t buffer_index;
    uint8_t buffer_count;
    float kalman_x;        // 칼만 필터 상태 (Kalman Filter State)
    float kalman_p;        // 칼만 필터 공분산 (Kalman Filter Covariance)
    const float* lut;      // LUT 포인터 (LUT Pointer)
    uint16_t lut_size;     // LUT 크기 (LUT Size)
} RTD;

// 함수 프로토타입 (Function Prototypes)
void RTD_Init(RTD* rtd, float r0, float alpha, const char* wire_type, char accuracy_class, const char* sensor_type);
float RTD_Linear_Approximation(RTD* rtd, float r_measured);
float RTD_LUT_Lookup(RTD* rtd, float r_measured);
float RTD_To_Temperature(RTD* rtd, float r_measured, uint8_t use_linear, uint8_t use_lut);
float RTD_Self_Heating_Compensation(RTD* rtd, float i_excitation, float r_measured);
float RTD_Moving_Average_Filter(RTD* rtd, float r_measured);
float RTD_Kalman_Filter(RTD* rtd, float r_measured);
void RTD_Calibrate(RTD* rtd, float r_known, float t_known);
float RTD_Compensate_Lead_Resistance(RTD* rtd, float v_measured, float i_excitation, float r_lead1, float r_lead3);
float RTD_Check_Accuracy(RTD* rtd, float temperature);
float MAX31865_Read_Resistance(RTD* rtd, SPI_HandleTypeDef* hspi);
void RTD_Read_MAX31865(RTD* rtd, SPI_HandleTypeDef* hspi, float i_excitation, float r_lead1, float r_lead3, 
                       uint8_t use_linear, uint8_t use_lut, float* temperature, float* tolerance);
void log_message(const char* message, UART_HandleTypeDef* huart);

#endif
            

3.2 rtd.c

#include "rtd.h"
#include <string.h>
#include <math.h>
#include <stdio.h>

// LUT (샘플로 Pt100만 포함, Sample includes only Pt100)
static const float pt100_lut[LUT_SIZE_PT] = {
    18.52f, 22.83f, /* ... */ 390.48f // -200°C ~ 850°C
    // 실제 LUT는 Python으로 생성 (Generate actual LUT using Python)
};

// RTD 초기화 (RTD Initialization)
void RTD_Init(RTD* rtd, float r0, float alpha, const char* wire_type, char accuracy_class, const char* sensor_type) {
    rtd->r0 = r0;
    rtd->alpha = alpha;
    strncpy(rtd->wire_type, wire_type, 8);
    rtd->accuracy_class = (accuracy_class == 'A' || accuracy_class == 'B') ? accuracy_class : 'B';
    strncpy(rtd->sensor_type, sensor_type, 8);
    rtd->dissipation_constant = 2.0f;
    rtd->calibration_offset = 0.0f;
    rtd->buffer_index = 0;
    rtd->buffer_count = 0;
    rtd->kalman_x = r0;
    rtd->kalman_p = 1.0f;

    if (strcmp(sensor_type, "Ni120") == 0) {
        rtd->A = 5.485e-3f;
        rtd->B = 6.650e-6f;
        rtd->C = 2.805e-11f;
        rtd->max_temp = 320.0f;
        rtd->min_temp = -80.0f;
        rtd->lut = NULL;
        rtd->lut_size = LUT_SIZE_NI120;
    } else if (strcmp(sensor_type, "Pt10") == 0) {
        rtd->A = 3.9083e-3f;
        rtd->B = -5.775e-7f;
        rtd->C = -4.183e-12f;
        rtd->max_temp = 500.0f;
        rtd->min_temp = -260.0f;
        rtd->lut = NULL;
        rtd->lut_size = LUT_SIZE_PT10;
    } else {
        if (alpha == 0.00385f) { // IEC 60751
            rtd->A = 3.9083e-3f;
            rtd->B = -5.775e-7f;
            rtd->C = -4.183e-12f;
        } else if (alpha == 0.00392f) { // 미국 표준 (American Standard)
            rtd->A = 3.926e-3f;
            rtd->B = -5.92e-7f;
            rtd->C = -4.0e-12f;
        } else {
            char msg[64];
            snprintf(msg, sizeof(msg), "Error: Unsupported alpha: %.6f\r\n", alpha);
            log_message(msg, NULL);
            return;
        }
        rtd->max_temp = 850.0f;
        rtd->min_temp = -200.0f;
        rtd->lut = pt100_lut;
        rtd->lut_size = LUT_SIZE_PT;
    }

    if (strcmp(rtd->wire_type, "2wire") != 0 && strcmp(rtd->wire_type, "3wire") != 0 && strcmp(rtd->wire_type, "4wire") != 0) {
        char msg[64];
        snprintf(msg, sizeof(msg), "Error: Invalid wire type: %s\r\n", rtd->wire_type);
        log_message(msg, NULL);
    }
}

// 선형 근사 (Linear Approximation)
float RTD_Linear_Approximation(RTD* rtd, float r_measured) {
    return (r_measured / rtd->r0 - 1.0f) / rtd->alpha;
}

// LUT 조회 (LUT Lookup)
float RTD_LUT_Lookup(RTD* rtd, float r_measured) {
    if (!rtd->lut) {
        log_message("Warning: LUT not available, using linear approximation\r\n", NULL);
        return RTD_Linear_Approximation(rtd, r_measured);
    }
    int index = (int)((r_measured / rtd->r0 - 1.0f) / rtd->alpha + rtd->min_temp);
    if (index < 0 || index >= rtd->lut_size - 1) {
        char msg[64];
        snprintf(msg, sizeof(msg), "Error: LUT index out of range: %d\r\n", index);
        log_message(msg, NULL);
        return 0.0f;
    }
    float t1 = rtd->min_temp + (float)index;
    float t2 = t1 + 1.0f;
    float r1 = rtd->lut[index];
    float r2 = rtd->lut[index + 1];
    return t1 + (r_measured - r1) * (t2 - t1) / (r2 - r1);
}

// 저항-온도 변환 (Resistance-to-Temperature Conversion)
float RTD_To_Temperature(RTD* rtd, float r_measured, uint8_t use_linear, uint8_t use_lut) {
    if (r_measured <= 0.0f || r_measured > 400.0f) {
        char msg[64];
        snprintf(msg, sizeof(msg), "Error: Invalid resistance: %.2fΩ\r\n", r_measured);
        log_message(msg, NULL);
        return 0.0f;
    }

    float t;
    if (use_lut && rtd->lut) {
        t = RTD_LUT_Lookup(rtd, r_measured);
    } else if (use_linear && -50.0f <= (r_measured / rtd->r0 - 1.0f) / rtd->alpha && (r_measured / rtd->r0 - 1.0f) / rtd->alpha <= 150.0f) {
        t = RTD_Linear_Approximation(rtd, r_measured);
    } else {
        if (r_measured >= rtd->r0) {
            float a = rtd->B;
            float b = rtd->A;
            float c = 1.0f - r_measured / rtd->r0;
            float discriminant = b * b - 4.0f * a * c;
            if (discriminant < 0.0f) {
                log_message("Error: Invalid resistance value\r\n", NULL);
                return 0.0f;
            }
            t = (-b + sqrtf(discriminant)) / (2.0f * a);
        } else {
            t = 0.0f;
            for (int i = 0; i < 10; i++) {
                float rt_est = rtd->r0 * (1.0f + rtd->A * t + rtd->B * t * t + rtd->C * (t - 100.0f) * t * t * t);
                float drt_dt = rtd->r0 * (rtd->A + 2.0f * rtd->B * t + rtd->C * (4.0f * t * t * t - 300.0f * t * t));
                t -= (rt_est - r_measured) / drt_dt;
            }
        }
    }

    if (t > rtd->max_temp || t < rtd->min_temp) {
        char msg[64];
        snprintf(msg, sizeof(msg), "Error: Temperature out of range: %.2f°C\r\n", t);
        log_message(msg, NULL);
        return 0.0f;
    }
    return t + rtd->calibration_offset;
}

// 자가 발열 보상 (Self-Heating Compensation)
float RTD_Self_Heating_Compensation(RTD* rtd, float i_excitation, float r_measured) {
    if (i_excitation > 0.001f) {
        char msg[64];
        snprintf(msg, sizeof(msg), "Warning: High excitation current (%.2fmA)\r\n", i_excitation * 1000.0f);
        log_message(msg, NULL);
    }
    float power = i_excitation * i_excitation * r_measured;
    return power * 1000.0f / rtd->dissipation_constant;
}

// 이동 평균 필터 (Moving Average Filter)
float RTD_Moving_Average_Filter(RTD* rtd, float r_measured) {
    rtd->resistance_buffer[rtd->buffer_index] = r_measured;
    rtd->buffer_index = (rtd->buffer_index + 1) % FILTER_SIZE;
    if (rtd->buffer_count < FILTER_SIZE) rtd->buffer_count++;
    
    float sum = 0.0f;
    for (int i = 0; i < rtd->buffer_count; i++) {
        sum += rtd->resistance_buffer[i];
    }
    return sum / (float)rtd->buffer_count;
}

// 칼만 필터 (Kalman Filter)
float RTD_Kalman_Filter(RTD* rtd, float r_measured) {
    rtd->kalman_p += KALMAN_Q;
    float k = rtd->kalman_p / (rtd->kalman_p + KALMAN_R);
    rtd->kalman_x += k * (r_measured - rtd->kalman_x);
    rtd->kalman_p *= (1.0f - k);
    return rtd->kalman_x;
}

// 캘리브레이션 (Calibration)
void RTD_Calibrate(RTD* rtd, float r_known, float t_known) {
    float t_calculated = RTD_To_Temperature(rtd, r_known, 0, 0);
    rtd->calibration_offset = t_known - t_calculated;
    char msg[64];
    snprintf(msg, sizeof(msg), "Calibration: offset set to %.2f°C\r\n", rtd->calibration_offset);
    log_message(msg, NULL);
}

// 리드선 보상 (Lead Resistance Compensation)
float RTD_Compensate_Lead_Resistance(RTD* rtd, float v_measured, float i_excitation, float r_lead1, float r_lead3) {
    if (i_excitation <= 0.0f) {
        log_message("Error: Invalid excitation current\r\n", NULL);
        return 0.0f;
    }
    float r_t;
    if (strcmp(rtd->wire_type, "2wire") == 0) {
        float r_total = v_measured / i_excitation;
        r_t = r_total - 2.0f * r_lead1;
    } else if (strcmp(rtd->wire_type, "3wire") == 0) {
        float r_total = v_measured / i_excitation;
        r_t = r_total - (r_lead1 + r_lead3) / 2.0f;
    } else if (strcmp(rtd->wire_type, "4wire") == 0) {
        r_t = v_measured / i_excitation;
    } else {
        char msg[64];
        snprintf(msg, sizeof(msg), "Error: Invalid wire type: %s\r\n", rtd->wire_type);
        log_message(msg, NULL);
        return 0.0f;
    }
    if (r_t <= 0.0f || r_t > 400.0f) {
        char msg[64];
        snprintf(msg, sizeof(msg), "Error: Invalid compensated resistance: %.2fΩ\r\n", r_t);
        log_message(msg, NULL);
        return 0.0f;
    }
    return r_t;
}

// 정확도 검증 (Accuracy Verification)
float RTD_Check_Accuracy(RTD* rtd, float temperature) {
    return (rtd->accuracy_class == 'A') ? (0.15f + 0.002f * fabsf(temperature)) : (0.3f + 0.005f * fabsf(temperature));
}

// MAX31865 저항 읽기 (Read Resistance from MAX31865)
float MAX31865_Read_Resistance(RTD* rtd, SPI_HandleTypeDef* hspi) {
    uint8_t config = strcmp(rtd->wire_type, "3wire") == 0 ? CONFIG_3WIRE : 
                     strcmp(rtd->wire_type, "2wire") == 0 ? CONFIG_2WIRE : CONFIG_4WIRE;
    float r_ref = strcmp(rtd->sensor_type, "Pt100") == 0 ? R_REF_PT100 :
                  strcmp(rtd->sensor_type, "Pt1000") == 0 ? R_REF_PT1000 :
                  strcmp(rtd->sensor_type, "Pt10") == 0 ? R_REF_PT10 : R_REF_NI120;
    
    uint8_t tx_data[2] = { MAX31865_WRITE_ADDR(MAX31865_CONFIG_REG), config };
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
    HAL_SPI_Transmit(hspi, tx_data, 2, HAL_MAX_DELAY);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
    
    HAL_Delay(65);
    
    uint8_t rx_data[2];
    tx_data[0] = MAX31865_READ_ADDR(MAX31865_RTD_MSB_REG);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
    HAL_SPI_TransmitReceive(hspi, tx_data, rx_data, 2, HAL_MAX_DELAY);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
    
    uint16_t rtd_data = (rx_data[0] << 8) | (rx_data[1] >> 1);
    if (rtd_data == 0 || rtd_data == 0x7FFF) {
        log_message("Error: Invalid RTD data\r\n", NULL);
        return 0.0f;
    }
    return ((float)rtd_data * r_ref) / ADC_MAX;
}

// 통합 RTD 읽기 (Integrated RTD Reading)
void RTD_Read_MAX31865(RTD* rtd, SPI_HandleTypeDef* hspi, float i_excitation, float r_lead1, float r_lead3, 
                       uint8_t use_linear, uint8_t use_lut, float* temperature, float* tolerance) {
    float r_measured = MAX31865_Read_Resistance(rtd, hspi);
    if (r_measured == 0.0f) {
        *temperature = 0.0f;
        *tolerance = 0.0f;
        return;
    }
    
    float v_measured = r_measured * i_excitation;
    float r_compensated = RTD_Compensate_Lead_Resistance(rtd, v_measured, i_excitation, r_lead1, r_lead3);
    if (r_compensated == 0.0f) {
        *temperature = 0.0f;
        *tolerance = 0.0f;
        return;
    }
    
    float r_filtered = RTD_Moving_Average_Filter(rtd, r_compensated);
    r_filtered = RTD_Kalman_Filter(rtd, r_filtered);
    
    float delta_t = RTD_Self_Heating_Compensation(rtd, i_excitation, r_filtered);
    
    *temperature = RTD_To_Temperature(rtd, r_filtered, use_linear, use_lut) - delta_t;
    
    *tolerance = RTD_Check_Accuracy(rtd, *temperature);
    
    char msg[128];
    snprintf(msg, sizeof(msg), "R_measured: %.2fΩ, Temp: %.2f°C, Tolerance: ±%.2f°C\r\n", 
             r_filtered, *temperature, *tolerance);
    log_message(msg, hspi == &hspi1 ? &huart2 : NULL);
}

// UART 로깅 (UART Logging)
void log_message(const char* message, UART_HandleTypeDef* huart) {
    if (huart) {
        HAL_UART_Transmit(huart, (uint8_t*)message, strlen(message), HAL_MAX_DELAY);
    }
}
            

3.3 main.c

/* USER CODE BEGIN Header */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "rtd.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define CS_PIN GPIO_PIN_4
#define CS_PORT GPIOA
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
SPI_HandleTypeDef hspi1;
UART_HandleTypeDef huart2;

/* USER CODE BEGIN PV */
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_SPI1_Init(void);
static void MX_USART2_UART_Init(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */
  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */
  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_SPI1_Init();
  MX_USART2_UART_Init();
  /* USER CODE BEGIN 2 */
  
  // RTD 초기화 (RTD Initialization, Pt100, IEC 60751, 3선식, Class A)
  RTD rtd;
  RTD_Init(&rtd, 100.0f, 0.00385f, "3wire", 'A', "Pt100");
  
  // 캘리브레이션 (Calibration, 0°C에서 100Ω)
  RTD_Calibrate(&rtd, 100.0f, 0.0f);
  
  // Pt10 초기화 (Pt10 Initialization, 4선식, Class B)
  RTD rtd_pt10;
  RTD_Init(&rtd_pt10, 10.0f, 0.00385f, "4wire", 'B', "Pt10");
  RTD_Calibrate(&rtd_pt10, 10.0f, 0.0f);
  
  float temperature, tolerance;
  float i_excitation = 0.001f; // 1mA
  float r_lead1 = 0.3f; // 리드선 저항 (Lead Resistance)
  float r_lead3 = 0.3f;
  uint8_t use_linear = 1;
  uint8_t use_lut = 0;
  
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    // Sleep 모드 (Sleep Mode)
    HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
    
    // Pt100 읽기 (Read Pt100)
    RTD_Read_MAX31865(&rtd, &hspi1, i_excitation, r_lead1, r_lead3, use_linear, use_lut, &temperature, &tolerance);
    
    // Pt10 읽기 (Read Pt10)
    RTD_Read_MAX31865(&rtd_pt10, &hspi1, i_excitation, 0.0f, 0.0f, use_linear, use_lut, &temperature, &tolerance);
    
    HAL_Delay(1000);
    
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
  {
    Error_Handler();
  }

  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI;
  RCC_OscInitStruct.PLL.PLLM = 1;
  RCC_OscInitStruct.PLL.PLLN = 10;
  RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;
  RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;
  RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief SPI1 Initialization Function
  * @retval None
  */
static void MX_SPI1_Init(void)
{
  hspi1.Instance = SPI1;
  hspi1.Init.Mode = SPI_MODE_MASTER;
  hspi1.Init.Direction = SPI_DIRECTION_2LINES;
  hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
  hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
  hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
  hspi1.Init.NSS = SPI_NSS_SOFT;
  hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_64;
  hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
  hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
  hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
  hspi1.Init.CRCPolynomial = 7;
  hspi1.Init.CRCLength = SPI_CRC_LENGTH_DATASIZE;
  hspi1.Init.NSSPMode = SPI_NSS_PULSE_DISABLE;
  if (HAL_SPI_Init(&hspi1) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief USART2 Initialization Function
  * @retval None
  */
static void MX_USART2_UART_Init(void)
{
  huart2.Instance = USART2;
  huart2.Init.BaudRate = 115200;
  huart2.Init.WordLength = UART_WORDLENGTH_8B;
  huart2.Init.StopBits = UART_STOPBITS_1;
  huart2.Init.Parity = UART_PARITY_NONE;
  huart2.Init.Mode = UART_MODE_TX_RX;
  huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart2.Init.OverSampling = UART_OVERSAMPLING_16;
  huart2.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
  huart2.Init.ClockPrescaler = UART_PRESCALER_DIV1;
  huart2.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
  if (HAL_UART_Init(&huart2) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief GPIO Initialization Function
  * @retval None
  */
static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  __HAL_RCC_GPIOA_CLK_ENABLE();

  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);

  GPIO_InitStruct.Pin = GPIO_PIN_4;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
  GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  GPIO_InitStruct.Pin = GPIO_PIN_2|GPIO_PIN_3;
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
  GPIO_InitStruct.Alternate = GPIO_AF7_USART2;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

/* USER CODE BEGIN 4 */
/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  log_message("Error: System halted\r\n", &huart2);
  while (1)
  {
  }
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  char msg[128];
  snprintf(msg, sizeof(msg), "Assert failed: %s, line %lu\r\n", file, line);
  log_message(msg, &huart2);
}
#endif /* USE_FULL_ASSERT */
            

4. 코드 설명 (Code Explanation)

  • rtd.h: RTD 구조체와 함수 프로토타입 정의 (Defines RTD structure and function prototypes). Pt100, Pt1000, Ni120, Pt10 지원 (Supports Pt100, Pt1000, Ni120, Pt10).
  • rtd.c:
    •   RTD_Init: 센서별 계수 및 LUT 초기화 (Initialize sensor coefficients and LUT)
    •   RTD_Linear_Approximation: -50°C ~ 150°C 선형 근사 (Linear approximation for -50°C to 150°C)
    •   RTD_LUT_Lookup: LUT 선형 보간 (LUT linear interpolation)
    •   RTD_To_Temperature: 이차 방정식, 뉴턴-랩슨, LUT 지원 (Quadratic equation, Newton-Raphson, LUT support)
    •   RTD_Self_Heating_Compensation: 자가 발열 계산 (Self-heating calculation)
    •   RTD_Moving_Average_Filter: 5 샘플 평균 (5-sample average)
    •   RTD_Kalman_Filter: 노이즈 제거 (Noise reduction)
    •   RTD_Calibrate: 오프셋 보정 (Offset correction)
    •   RTD_Compensate_Lead_Resistance: 2/3/4선식 보상 (2/3/4-wire compensation)
    •   RTD_Check_Accuracy: Class A/B 허용 오차 (Class A/B tolerance)
    •   MAX31865_Read_Resistance: SPI 통신 (SPI communication)
    •   RTD_Read_MAX31865: 통합 처리 (Integrated processing)
  • main.c: STM32CubeMX 설정, Pt100/Pt10 읽기, Sleep 모드 (STM32CubeMX configuration, Pt100/Pt10 reading, Sleep mode)

5. 메모리 및 최적화 (Memory and Optimization)

  • 메모리 (Memory): 256KB 플래시, 64KB SRAM 내 동작 (Operates within 256KB Flash, 64KB SRAM). LUT: 약 8.6KB (Pt100: 4KB, Ni120: 1.6KB, Pt10: 3KB). 코드: 약 30KB 플래시, 12KB SRAM (Code: ~30KB Flash, 12KB SRAM).
  • 저전력 (Low Power): Sleep 모드, 인터럽트로 깨어남 (Sleep mode, wake by interrupt). Stop 모드 확장 가능 (Extendable to Stop mode).
  • 노이즈 필터링 (Noise Filtering): 50Hz 필터, 이동 평균, 칼만 필터 (50Hz filter, moving average, Kalman filter).
  • Pt10: 극저온(-260°C) 지원, R_REF=43Ω (Cryogenic support, R_REF=43Ω).

6. 사용 방법 (Usage Instructions)

  1. STM32CubeMX 설정 (STM32CubeMX Configuration):
    •   클럭 (Clock): HSI 80MHz
    •   SPI1: PA5(SCK), PA6(MISO), PA7(MOSI), PA4(CS)
    •   USART2: PA2(TX), PA3(RX), 115200 baud
  2. 코드 통합 (Code Integration):
    •   rtd.h, rtd.cCore/Inc, Core/Src에 추가 (Add to Core/Inc, Core/Src)
    •   main.c를 덮어쓰기 (Overwrite main.c)
  3. 하드웨어 연결 (Hardware Connection):
    •   MAX31865: SPI1, CS(PA4), RTD(Pt100/Pt10)
    •   UART: PA2, PA3를 USB-TTL로 연결 (Connect to USB-TTL)
  4. 빌드 및 실행 (Build and Run): STM32CubeIDE에서 빌드/플래시, PuTTY로 출력 확인 (Build/flash in STM32CubeIDE, check output in PuTTY)
  5. 입력 (Inputs):
    •   r0: 100.0(Pt100), 1000.0(Pt1000), 10.0(Pt10), 120.0(Ni120)
    •   alpha: 0.00385(IEC), 0.00392(미국, American)
    •   i_excitation: 0.001(1mA)
    •   r_lead1, r_lead3: 0.3Ω
  6. 출력 (Output): UART로 온도, 허용 오차 (Temperature, tolerance via UART)

7. 참고 자료 (References)

  • IEC 60751: 백금 RTD 표준 (Platinum RTD Standard)
  • ASTM E1137: Ni120 사양 (Ni120 Specification)
  • MAX31865 데이터시트 (Datasheet): Analog Devices
  • STM32L432KC 데이터시트 (Datasheet): STMicroelectronics
  • STM32CubeMX/IDE 문서 (Documentation): HAL 드라이버 (HAL Drivers)