This commit is contained in:
cfif 2026-04-01 10:37:02 +03:00
parent ef6183ec0d
commit 35a477a7dd
3 changed files with 148 additions and 198 deletions

View File

@ -9,6 +9,10 @@
// Константы // Константы
#define ADC_MAX 4095.0f // 12-битный АЦП #define ADC_MAX 4095.0f // 12-битный АЦП
#define R1 3300.0f // Сопротивление делителя напряжения
#define TABLE_START_TEMP (-40)
#define TABLE_END_TEMP 85
#define TABLE_SIZE 38
// Параметры Steinhart-Hart для термистора // Параметры Steinhart-Hart для термистора
#define koef_A 0.001741624168166423 #define koef_A 0.001741624168166423
@ -17,19 +21,6 @@
#define TABLE_SIZE_LOOKUP 1024 #define TABLE_SIZE_LOOKUP 1024
// Типы таблиц NTC
typedef enum {
NTC_TYPE_KST45 = 0, // Таблица из документа KST45 (25°C = 3kΩ)
NTC_TYPE_INCAR = 1, // Таблица из документа Incar (25°C = 2795Ω)
NTC_TYPE_CUSTOM = 2 // Пользовательская таблица
} eNtcTableType;
// Структура для хранения табличных данных
typedef struct {
int16_t temp_c; // Температура (°C)
float r_nom; // Номинальное сопротивление (Ω)
} ntc_table_entry;
typedef enum { typedef enum {
ALG_STEINHART = 0, ALG_STEINHART = 0,
ALG_STEINHART_FULL = 1, ALG_STEINHART_FULL = 1,
@ -38,24 +29,16 @@ typedef enum {
// Предварительно вычисленная таблица для быстрого доступа // Предварительно вычисленная таблица для быстрого доступа
typedef struct { typedef struct {
uint16_t adc_value; // Значение АЦП uint16_t adc_value; // Значение АЦП
int16_t temp_c; // Температура в °C * 10 (для фиксированной точки) int16_t temp_c; // Температура в °C * 10 (для фиксированной точки)
float resistance; // Сопротивление термистора в Омах float resistance_ohm; // Сопротивление в Ом, соответствующее значению АЦП
} adc_temp_lookup; } adc_temp_lookup;
// Функции инициализации и конфигурации void init_fast_lookup_table(eAlg use_alg);
void init_fast_lookup_table(eAlg use_alg, eNtcTableType table_type, float custom_r1);
void set_custom_ntc_table(const ntc_table_entry* table, uint16_t size, float r1);
void select_ntc_table(eNtcTableType table_type, float custom_r1);
// Основные функции
float get_temperature_from_adc(uint16_t adc_value, eAlg alg); float get_temperature_from_adc(uint16_t adc_value, eAlg alg);
int16_t get_temperature_fast(uint16_t adc_value); int16_t get_temperature_log_fast(uint16_t adc_value);
float get_resistance_from_adc(uint16_t adc_value); int16_t get_temperature_linear_fast(uint16_t adc_value);
float get_resistance_fast(uint16_t adc_value); float get_resistance_from_adc(uint16_t adc_value); // Новая функция для получения сопротивления
// Функции для работы с текущей таблицей
eNtcTableType get_current_table_type(void);
float get_current_r1(void);
#endif //MDF_ADC_TEMP_KST45_14_2_H #endif //MDF_ADC_TEMP_KST45_14_2_H

View File

@ -1,45 +1,31 @@
#include <stdio.h> #include <stdio.h>
#include <stdint.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
#include "ADC_Temp.h" #include "ADC_Temp.h"
// value 0..4095 - Это диапазон АЦП, Нужно проверить крайние значения и смоделировать таблицы
extern adc_temp_lookup fast_lookup[TABLE_SIZE_LOOKUP]; extern adc_temp_lookup fast_lookup[TABLE_SIZE_LOOKUP];
int main() { int main() {
// Инициализация таблицы быстрого поиска
init_fast_lookup_table(ALG_STEINHART);
init_fast_lookup_table(ALG_STEINHART, NTC_TYPE_INCAR, 3300.0f);
// for (int i = 0; i < TABLE_SIZE_LOOKUP; ++i) {
// printf("{%u,%d}, \n", fast_lookup[i].adc_value, fast_lookup[i].temp_c);
// }
//uint16_t value = 4095 / 2; // Должно быть 25 градусов. Это середина диапазона АЦП
uint16_t value = 1980; uint16_t value = 1980;
// float T_ALG_LINEAR = get_temperature_from_adc(value, ALG_LINEAR); // Получение температуры различными методами
//float T_ALG_STEINHART = get_temperature_from_adc(value, ALG_STEINHART); float T_ALG_STEINHART = get_temperature_from_adc(value, ALG_STEINHART);
float T_ALG_STEINHART = get_temperature_fast(value); int16_t T_FAST = get_temperature_log_fast(value);
float T_ALG_STEINHART_1 = get_resistance_fast(value); float resistance = get_resistance_from_adc(value);
// float T_ALG_STEINHART_FULL = get_temperature_from_adc(value, ALG_STEINHART_FULL); printf("T_ALG_STEINHART = %f °C\n", T_ALG_STEINHART);
// float T_FAST = get_temperature_fast(value); printf("T_FAST = %.1f °C\n", T_FAST / 10.0f);
// float T_ATERLUX = calc_temperature(value); printf("Resistance = %.2f Ω\n", resistance);
// printf("T_ALG_LINEAR = %f \n", T_ALG_LINEAR);
printf("T_ALG_STEINHART = %f %f\n", T_ALG_STEINHART, T_ALG_STEINHART_1);
// printf("T_ALG_STEINHART_FULL = %f \n", T_ALG_STEINHART_FULL);
// printf("T_FAST = %f \n", T_FAST / 10);
// printf("T_ATERLUX = %f \n", T_ATERLUX / 10);
// Пример доступа к таблице
printf("\nПример данных из таблицы быстрого поиска:\n");
for(int i = 0; i < 5; i++) {
printf("ADC: %u, Temp: %.1f °C, Resistance: %.2f Ω\n",
fast_lookup[i].adc_value,
fast_lookup[i].temp_c / 10.0f,
fast_lookup[i].resistance_ohm);
}
return 0; return 0;
} }

View File

@ -1,13 +1,19 @@
// //
// Created by cfif on 02.12.2025. // Created by cfif on 02.12.2025.
// //
#include "stdint.h"
#include "ADC_Temp.h" #include "ADC_Temp.h"
#include <math.h> #include <math.h>
#include <string.h>
// Таблица из документа KST45 (25°C = 3000Ω) // Структура для хранения табличных данных
static const ntc_table_entry ntc_table_kst45[] = { typedef struct {
int temp_c; // Температура (°C)
float r_nom; // Номинальное сопротивление (Ω)
} ntc_table_entry;
/*
// Таблица из документа KST45
static const ntc_table_entry ntc_table[] = {
{-40, 100950.0f}, {-40, 100950.0f},
{-35, 72777.0f}, {-35, 72777.0f},
{-30, 53100.0f}, {-30, 53100.0f},
@ -35,9 +41,10 @@ static const ntc_table_entry ntc_table_kst45[] = {
{80, 377.4f}, {80, 377.4f},
{85, 321.7f} {85, 321.7f}
}; };
*/
// Таблица из документа Incar (25°C ≈ 2795Ω) // Таблица из документа Incar
static const ntc_table_entry ntc_table_incar[] = { static ntc_table_entry ntc_table[] = {
{-40, 101000.0f}, {-40, 101000.0f},
{-35, 72600.0f}, {-35, 72600.0f},
{-30, 52720.0f}, {-30, 52720.0f},
@ -45,12 +52,12 @@ static const ntc_table_entry ntc_table_incar[] = {
{-20, 28620.0f}, {-20, 28620.0f},
{-15, 21390.0f}, {-15, 21390.0f},
{-10, 16120.0f}, {-10, 16120.0f},
{-5, 12260.0f}, {-5, 12260.0f},
{-4, 11620.0f}, {-4, 11620.0f},
{-3, 11010.0f}, {-3, 11010.0f},
{-2, 10440.0f}, {-2, 10440.0f},
{-1, 9907.0f}, {-1, 9907.0f},
{0, 9399.0f}, {0, 9399.0f},
{1, 8924.0f}, {1, 8924.0f},
{2, 8473.0f}, {2, 8473.0f},
{3, 8048.0f}, {3, 8048.0f},
@ -78,45 +85,38 @@ static const ntc_table_entry ntc_table_incar[] = {
{85, 283.0f} {85, 283.0f}
}; };
// Глобальные переменные для текущей конфигурации // Предварительно вычисленная таблица для быстрого доступа
static const ntc_table_entry* current_ntc_table = ntc_table_kst45;
static uint16_t current_table_size = sizeof(ntc_table_kst45) / sizeof(ntc_table_entry);
static float current_r1 = 3300.0f; // Сопротивление делителя по умолчанию
static eNtcTableType current_table_type = NTC_TYPE_KST45;
// Глобальная таблица для быстрого доступа
adc_temp_lookup fast_lookup[TABLE_SIZE_LOOKUP]; adc_temp_lookup fast_lookup[TABLE_SIZE_LOOKUP];
// Функция расчёта сопротивления NTC из значения АЦП // Функция расчёта сопротивления NTC из значения АЦП
static float calculate_resistance(uint16_t adc_value, float r1) { static float calculate_resistance(uint16_t adc_value) {
if (adc_value == 0 || adc_value >= (uint16_t) ADC_MAX) { if (adc_value == 0 || adc_value >= (uint16_t) ADC_MAX) {
return 0.0f; return 0.0f;
} }
// Формула делителя напряжения: R_ntc = R1 * adc_value / (ADC_MAX - adc_value) float R_ntc = R1 * (float) adc_value / (ADC_MAX - (float) adc_value);
float R_ntc = r1 * (float) adc_value / (ADC_MAX - (float) adc_value);
return R_ntc; return R_ntc;
} }
// Бинарный поиск в таблице // Бинарный поиск в таблице
static int find_interval_index(float resistance, const ntc_table_entry* table, uint16_t size) { static int find_interval_index(float resistance) {
int left = 0; int left = 0;
int right = size - 1; int right = TABLE_SIZE - 1;
// Проверка границ // Проверка границ
if (resistance >= table[0].r_nom) return 0; if (resistance >= ntc_table[0].r_nom) return 0;
if (resistance <= table[right].r_nom) return right - 1; if (resistance <= ntc_table[right].r_nom) return right - 1;
// Бинарный поиск // Бинарный поиск
while (left <= right) { while (left <= right) {
int mid = left + (right - left) / 2; int mid = left + (right - left) / 2;
if (mid < size - 1 && if (mid < TABLE_SIZE - 1 &&
resistance <= table[mid].r_nom && resistance <= ntc_table[mid].r_nom &&
resistance >= table[mid + 1].r_nom) { resistance >= ntc_table[mid + 1].r_nom) {
return mid; return mid;
} }
if (resistance > table[mid].r_nom) { if (resistance > ntc_table[mid].r_nom) {
right = mid - 1; right = mid - 1;
} else { } else {
left = mid + 1; left = mid + 1;
@ -127,12 +127,12 @@ static int find_interval_index(float resistance, const ntc_table_entry* table, u
} }
// Интерполяция с использованием уравнения Стейнхарта-Харта между точками // Интерполяция с использованием уравнения Стейнхарта-Харта между точками
static float interpolate_steinhart(float resistance, int index, const ntc_table_entry* table) { static float interpolate_steinhart(float resistance, int index) {
// Берем две соседние точки из таблицы // Берем две соседние точки из таблицы
float t1 = (float) table[index].temp_c + 273.15f; // в Кельвинах float t1 = (float) ntc_table[index].temp_c + 273.15f; // в Кельвинах
float t2 = (float) table[index + 1].temp_c + 273.15f; float t2 = (float) ntc_table[index + 1].temp_c + 273.15f;
float r1 = table[index].r_nom; float r1 = ntc_table[index].r_nom;
float r2 = table[index + 1].r_nom; float r2 = ntc_table[index + 1].r_nom;
// Вычисляем коэффициент B для интервала // Вычисляем коэффициент B для интервала
float B = logf(r1 / r2) / (1.0f / t1 - 1.0f / t2); float B = logf(r1 / r2) / (1.0f / t1 - 1.0f / t2);
@ -145,11 +145,11 @@ static float interpolate_steinhart(float resistance, int index, const ntc_table_
} }
// Линейная интерполяция в логарифмическом масштабе // Линейная интерполяция в логарифмическом масштабе
static float interpolate_log_linear(float resistance, int index, const ntc_table_entry* table) { static float interpolate_log_linear(float resistance, int index) {
float t1 = (float) table[index].temp_c; float t1 = (float) ntc_table[index].temp_c;
float t2 = (float) table[index + 1].temp_c; float t2 = (float) ntc_table[index + 1].temp_c;
float r1 = table[index].r_nom; float r1 = ntc_table[index].r_nom;
float r2 = table[index + 1].r_nom; float r2 = ntc_table[index + 1].r_nom;
float log_r1 = logf(r1); float log_r1 = logf(r1);
float log_r2 = logf(r2); float log_r2 = logf(r2);
@ -159,7 +159,7 @@ static float interpolate_log_linear(float resistance, int index, const ntc_table
} }
// Более надежная версия с проверкой параметров // Более надежная версия с проверкой параметров
static float interpolate_steinhart_full(float resistance, int index, const ntc_table_entry* table) { static float interpolate_steinhart_full(float resistance, int index) {
// Проверка корректности входных данных // Проверка корректности входных данных
if (resistance <= 0.0f) { if (resistance <= 0.0f) {
return -273.15f; // Абсолютный ноль при некорректном сопротивлении return -273.15f; // Абсолютный ноль при некорректном сопротивлении
@ -169,158 +169,139 @@ static float interpolate_steinhart_full(float resistance, int index, const ntc_t
// но основное вычисление - по уравнению с коэффициентами // но основное вычисление - по уравнению с коэффициентами
double L = logf(resistance); double L = logf(resistance);
// Для термисторов NTC коэффициент C обычно очень маленький (порядка 1e-7...1e-8)
// Убедимся, что кубический член вычислен корректно
double L3 = L * L * L; double L3 = L * L * L;
double inv_T = koef_A + koef_B * L + koef_C * L3; double inv_T = koef_A + koef_B * L + koef_C * L3;
// Проверка на физическую реализуемость // Проверка на физическую реализуемость (температура должна быть положительной в Кельвинах)
if (inv_T <= 0.0f || inv_T > 1.0f) { if (inv_T <= 0.0f || inv_T > 1.0f) { // 1/T не может быть <= 0 (T < 0K) или слишком большим
// В случае ошибки возвращаем температуру из таблицы по индексу // В случае ошибки возвращаем температуру из таблицы по индексу
return (float)table[index].temp_c; return (float) ntc_table[index].temp_c;
} }
double temp_K = 1.0f / inv_T; double temp_K = 1.0f / inv_T;
// Дополнительная проверка диапазона // Дополнительная проверка диапазона (например, для NTC обычно -50...+150°C)
if (temp_K < 223.15f || temp_K > 423.15f) { if (temp_K < 223.15f || temp_K > 423.15f) { // -50°C...150°C в Кельвинах
// Возвращаем значение из таблицы как запасной вариант // Возвращаем значение из таблицы как запасной вариант
return (float)table[index].temp_c; return (float) ntc_table[index].temp_c;
} }
return (float)(temp_K - 273.15f); return (float) (temp_K - 273.15f);
} }
// Основная функция для получения температуры // Основная функция для получения температуры
float get_temperature_from_adc(uint16_t adc_value, eAlg use_alg) { float get_temperature_from_adc(uint16_t adc_value, eAlg use_alg) {
float resistance = calculate_resistance(adc_value, current_r1); float resistance = calculate_resistance(adc_value);
if (resistance <= 0.0f) { if (resistance <= 0.0f) {
return -273.15f; // Ошибка return -273.15f; // Ошибка
} }
int index = find_interval_index(resistance, current_ntc_table, current_table_size); int index = find_interval_index(resistance);
if (index < 0 || index >= current_table_size - 1) { if (index < 0 || index >= TABLE_SIZE - 1) {
// Вне диапазона таблицы // Вне диапазона таблицы
if (resistance >= current_ntc_table[0].r_nom) return current_ntc_table[0].temp_c; if (resistance >= ntc_table[0].r_nom) return TABLE_START_TEMP;
if (resistance <= current_ntc_table[current_table_size - 1].r_nom) return current_ntc_table[current_table_size - 1].temp_c; if (resistance <= ntc_table[TABLE_SIZE - 1].r_nom) return TABLE_END_TEMP;
return -273.15f; // Ошибка return -273.15f; // Ошибка
} }
if (use_alg == ALG_STEINHART) { if (use_alg == ALG_STEINHART) {
return interpolate_steinhart(resistance, index, current_ntc_table); return interpolate_steinhart(resistance, index);
} else if (use_alg == ALG_STEINHART_FULL) { } else if (use_alg == ALG_STEINHART_FULL) {
return interpolate_steinhart_full(resistance, index, current_ntc_table); return interpolate_steinhart_full(resistance, index);
} else { } else {
return interpolate_log_linear(resistance, index, current_ntc_table); return interpolate_log_linear(resistance, index);
} }
} }
// Функция для получения сопротивления по значению АЦП // Функция для получения сопротивления из значения АЦП
float get_resistance_from_adc(uint16_t adc_value) { float get_resistance_from_adc(uint16_t adc_value) {
return calculate_resistance(adc_value, current_r1); return calculate_resistance(adc_value);
} }
// Выбор активной таблицы NTC void init_fast_lookup_table(eAlg use_alg) {
void select_ntc_table(eNtcTableType table_type, float custom_r1) {
switch (table_type) {
case NTC_TYPE_KST45:
current_ntc_table = ntc_table_kst45;
current_table_size = sizeof(ntc_table_kst45) / sizeof(ntc_table_entry);
current_r1 = (custom_r1 > 0) ? custom_r1 : 3300.0f;
current_table_type = NTC_TYPE_KST45;
break;
case NTC_TYPE_INCAR:
current_ntc_table = ntc_table_incar;
current_table_size = sizeof(ntc_table_incar) / sizeof(ntc_table_entry);
current_r1 = (custom_r1 > 0) ? custom_r1 : 3300.0f;
current_table_type = NTC_TYPE_INCAR;
break;
case NTC_TYPE_CUSTOM:
// Пользовательская таблица должна быть установлена через set_custom_ntc_table
if (current_ntc_table == NULL) {
// Если пользовательская таблица не установлена, используем KST45
current_ntc_table = ntc_table_kst45;
current_table_size = sizeof(ntc_table_kst45) / sizeof(ntc_table_entry);
current_r1 = 3300.0f;
current_table_type = NTC_TYPE_KST45;
}
if (custom_r1 > 0) {
current_r1 = custom_r1;
}
break;
}
}
// Установка пользовательской таблицы NTC
void set_custom_ntc_table(const ntc_table_entry* table, uint16_t size, float r1) {
if (table != NULL && size > 1) {
current_ntc_table = table;
current_table_size = size;
current_r1 = (r1 > 0) ? r1 : 3300.0f;
current_table_type = NTC_TYPE_CUSTOM;
}
}
// Инициализация таблицы быстрого доступа
void init_fast_lookup_table(eAlg use_alg, eNtcTableType table_type, float custom_r1) {
// Выбираем таблицу NTC
select_ntc_table(table_type, custom_r1);
// Создаем таблицу для быстрого преобразования АЦП->температура // Создаем таблицу для быстрого преобразования АЦП->температура
for (uint16_t i = 0; i < TABLE_SIZE_LOOKUP; i++) { for (uint16_t i = 0; i < TABLE_SIZE_LOOKUP; i++) {
uint16_t adc = (uint16_t)(i * (ADC_MAX / (TABLE_SIZE_LOOKUP - 1))); // Правильное распределение значений АЦП от 0 до ADC_MAX
uint16_t adc = (uint16_t) ((float) i * ADC_MAX / (TABLE_SIZE_LOOKUP - 1));
float temp = get_temperature_from_adc(adc, use_alg); float temp = get_temperature_from_adc(adc, use_alg);
float resistance = calculate_resistance(adc, current_r1); float resistance = calculate_resistance(adc);
fast_lookup[i].adc_value = adc; fast_lookup[i].adc_value = adc;
fast_lookup[i].temp_c = (int16_t) (temp * 10.0f); // Храним с точностью 0.1°C fast_lookup[i].temp_c = (int16_t) (temp * 10.0f); // Храним с точностью 0.1°C
fast_lookup[i].resistance = resistance; fast_lookup[i].resistance_ohm = resistance;
} }
} }
// Быстрое получение температуры из таблицы int16_t get_temperature_log_fast(uint16_t adc_value) {
int16_t get_temperature_fast(uint16_t adc_value) { // Защита от выхода за границы
// Простой поиск в таблице с линейной интерполяцией if (adc_value >= fast_lookup[TABLE_SIZE_LOOKUP - 1].adc_value) {
uint16_t step = (uint16_t)(ADC_MAX / (TABLE_SIZE_LOOKUP - 1)); return fast_lookup[TABLE_SIZE_LOOKUP - 1].temp_c;
if (step == 0) step = 1; }
uint16_t index = adc_value / step; // Бинарный поиск интервала
if (index >= (TABLE_SIZE_LOOKUP - 1)) return fast_lookup[TABLE_SIZE_LOOKUP - 1].temp_c; int left = 0;
int right = TABLE_SIZE_LOOKUP - 1;
uint16_t adc1 = fast_lookup[index].adc_value; while (left <= right) {
uint16_t adc2 = fast_lookup[index + 1].adc_value; int mid = left + (right - left) / 2;
int16_t temp1 = fast_lookup[index].temp_c;
int16_t temp2 = fast_lookup[index + 1].temp_c;
// Линейная интерполяция температуры if (fast_lookup[mid].adc_value <= adc_value &&
return temp1 + ((int32_t)(temp2 - temp1) * (adc_value - adc1)) / (adc2 - adc1); (mid == TABLE_SIZE_LOOKUP - 1 || adc_value < fast_lookup[mid + 1].adc_value)) {
// Нашли интервал
if (mid == TABLE_SIZE_LOOKUP - 1) {
return fast_lookup[mid].temp_c;
}
// Линейная интерполяция
uint16_t adc1 = fast_lookup[mid].adc_value;
uint16_t adc2 = fast_lookup[mid + 1].adc_value;
int16_t temp1 = fast_lookup[mid].temp_c;
int16_t temp2 = fast_lookup[mid + 1].temp_c;
// Избегаем деления на ноль
if (adc2 == adc1) {
return temp1;
}
return temp1 + ((int32_t) (temp2 - temp1) * (adc_value - adc1)) / (adc2 - adc1);
}
if (adc_value < fast_lookup[mid].adc_value) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return fast_lookup[0].temp_c; // Если не нашли, возвращаем первое значение
} }
// Быстрое получение сопротивления из таблицы
float get_resistance_fast(uint16_t adc_value) {
// Простой поиск в таблице с линейной интерполяцией
uint16_t step = (uint16_t)(ADC_MAX / (TABLE_SIZE_LOOKUP - 1));
if (step == 0) step = 1;
uint16_t index = adc_value / step; // Альтернативная простая версия
if (index >= (TABLE_SIZE_LOOKUP - 1)) return fast_lookup[TABLE_SIZE_LOOKUP - 1].resistance; int16_t get_temperature_linear_fast(uint16_t adc_value) {
// Находим интервал
uint16_t i = 0;
while (i < TABLE_SIZE_LOOKUP - 1 && adc_value > fast_lookup[i + 1].adc_value) {
i++;
}
uint16_t adc1 = fast_lookup[index].adc_value; // Линейная интерполяция
uint16_t adc2 = fast_lookup[index + 1].adc_value; if (i >= TABLE_SIZE_LOOKUP - 1) {
float r1 = fast_lookup[index].resistance; return fast_lookup[TABLE_SIZE_LOOKUP - 1].temp_c;
float r2 = fast_lookup[index + 1].resistance; }
// Линейная интерполяция сопротивления uint16_t adc1 = fast_lookup[i].adc_value;
return r1 + (r2 - r1) * (adc_value - adc1) / (adc2 - adc1); uint16_t adc2 = fast_lookup[i + 1].adc_value;
} int16_t temp1 = fast_lookup[i].temp_c;
int16_t temp2 = fast_lookup[i + 1].temp_c;
// Получение текущего типа таблицы
eNtcTableType get_current_table_type(void) { if (adc2 == adc1) {
return current_table_type; return temp1;
} }
// Получение текущего сопротивления делителя return temp1 + ((int32_t) (temp2 - temp1) * (adc_value - adc1)) / (adc2 - adc1);
float get_current_r1(void) {
return current_r1;
} }