diff --git a/c/ds18b20/CMakeLists.txt b/c/ds18b20/CMakeLists.txt new file mode 100644 index 0000000..10aec88 --- /dev/null +++ b/c/ds18b20/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.13) +project(ds18b20 C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# Переносимое ядро: ни регистров, ни задержек, ни stm32f10x.h. +add_library(ds18b20 STATIC + src/onewire.c + src/ds18b20.c +) + +target_include_directories(ds18b20 PUBLIC include) + +if(MSVC) + target_compile_options(ds18b20 PRIVATE /W4) +else() + target_compile_options(ds18b20 PRIVATE -Wall -Wextra -Wpedantic) +endif() + +# Порты (ports/) сюда не входят: им нужен CMSIS-заголовок целевого МК, +# они подключаются напрямую в проект прошивки. + +option(DS18B20_BUILD_TESTS "Собирать тесты на симуляторе шины" ON) + +if(DS18B20_BUILD_TESTS) + enable_testing() + add_executable(test_ds18b20 + tests/test_ds18b20.c + tests/fake_onewire.c + ) + target_link_libraries(test_ds18b20 PRIVATE ds18b20) + target_include_directories(test_ds18b20 PRIVATE tests) + add_test(NAME ds18b20 COMMAND test_ds18b20) +endif() diff --git a/c/ds18b20/README.md b/c/ds18b20/README.md new file mode 100644 index 0000000..c267a20 --- /dev/null +++ b/c/ds18b20/README.md @@ -0,0 +1,127 @@ +# ds18b20 + +Драйвер термометров **DS18B20** поверх программной шины **1-Wire**. +Вынесен из проекта `KONOR_ds18b20` и подключается в него сабмодулем. + +Ядро на C99: ни динамической памяти, ни ОС, ни `stm32f10x.h`. Всё +аппаратно-зависимое собрано в порте — на новую платформу переносятся +пять функций, остальное собирается как есть. + +``` + приложение + │ + ds18b20.c поиск датчиков, чтение и запись scratchpad + │ + onewire.c побайтовый обмен, CRC8, SEARCH ROM, MATCH/SKIP ROM + │ + ports/<платформа> Init, DelayUs, Reset, WriteBit, ReadBit +``` + +## Состав + +| Файл | Что делает | Зависимости | +|---|---|---| +| `include/ds18b20.h`, `src/ds18b20.c` | перечень датчиков, измерения, запись TH/TL/разрешения | `onewire.h`, `stdint.h` | +| `include/onewire.h`, `src/onewire.c` | байты и блоки, CRC8 Dallas/Maxim, обход SEARCH ROM | `stdint.h` | +| `ports/stm32f1/` | тайминги Maxim, вывод с открытым стоком, задержки по DWT | CMSIS `stm32f10x.h` | +| `tests/` | симулятор шины с виртуальными датчиками и host-тесты | — | + +## Порт: что нужно от платформы + +```c +void OneWire_Init(void); /* настроить вывод и источник задержек */ +void OneWire_DelayUs(uint32_t us); +uint8_t OneWire_Reset(void); /* сброс и проверка presence */ +void OneWire_WriteBit(uint8_t bit); +uint8_t OneWire_ReadBit(void); +``` + +Всё остальное — `OneWire_WriteByte`, `OneWire_ReadByte`, `OneWire_Crc8`, +`OneWire_SearchNext`, `OneWire_SelectRom`, `OneWire_SkipRom` и весь +`ds18b20.c` — уже написано и от платформы не зависит. + +### Готовый порт STM32F1 + +Вывод настроен как выход с открытым стоком; чтение идёт через `IDR`, +поэтому режим не переключается. Прерывания запрещаются только на +длительность одного тайм-слота, чтобы не терять байты USART. Источник +микросекундных задержек — счётчик тактов ядра `DWT->CYCCNT`. + +Привязку вывода порт берёт из `onewire_config.h`, который предоставляет +приложение — шаблон лежит рядом: + +```c +#include "stm32f10x.h" + +#define ONEWIRE_GPIO GPIOA +#define ONEWIRE_PIN 1U +#define ONEWIRE_GPIO_CLOCK RCC_APB2ENR_IOPAEN +``` + +Требуется внешняя подтяжка 4.7 кОм к питанию. + +## Использование + +```c +DS18B20_Bus bus; + +OneWire_Init(); +DS18B20_Scan(&bus); /* найти датчики на шине */ + +DS18B20_StartConversion(); /* SKIP ROM + CONVERT T */ +/* выдержать DS18B20_ConversionTimeMs(config) */ +DS18B20_ReadAll(&bus); /* обновить измерения */ + +for (uint8_t i = 0; i < bus.count; i++) { + if (bus.sensors[i].status & DS18B20_STATUS_VALID) { + float celsius = bus.sensors[i].raw / 16.0f; + } +} +``` + +Ошибка одного датчика не прерывает опрос остальных — она видна в поле +`status` соответствующей записи. + +## Тесты + +Симулятор шины реализует те же пять функций, что и настоящий порт, но +работает не с выводом, а с моделью: виртуальные датчики отвечают побитно, +включая полный алгоритм `SEARCH ROM`. Так на хосте проверяется именно то, +что иначе проверялось бы только осциллографом. + +```bash +cmake -B build && cmake --build build && ctest --test-dir build +``` + +```bash +clang -std=c99 -Wall -Wextra -Iinclude -Itests \ + tests/test_ds18b20.c tests/fake_onewire.c src/onewire.c src/ds18b20.c -o test && ./test +``` + +Покрыто: CRC8, обход шины с несколькими датчиками и пустой шины, отсев +чужих семейств по коду ROM, паразитное питание, битый scratchpad, +пропажа датчика, подтверждение и отказ записи, сохранение в EEPROM +датчика, пределы разрешения 9..12 бит. + +## Особенности, которые стоит знать + +* **Порядок датчиков задаёт шина, а не код.** `SEARCH ROM` обходит их в + порядке битов идентификатора, поэтому индекс в `bus.sensors[]` не + совпадает с порядком подключения. Искать датчик нужно по ROM через + `DS18B20_Find()`. +* **Пропажу датчика на живой шине не видно по presence.** Импульс + подтверждают соседи, и `MATCH ROM` к снятому датчику проходит без + признаков ошибки — она проявится как `DS18B20_STATUS_CRC_ERROR`. + `DS18B20_STATUS_NO_PRESENCE` означает, что молчит вся шина. +* **`DS18B20_MAX_SENSORS` равен 8** — столько датчиков помещается в один + кадр `SENSOR_DATA` протокола SETGUI. Для другого применения значение + можно поднять. +* **Обратное чтение после записи обязательно.** Датчик может ответить и + оставить регистры прежними; такой случай отличается от ошибки шины + кодом `DS18B20_WRITE_REJECTED`. + +## Лицензия и происхождение + +Код вынесен из `KONOR_ds18b20` (модуль термометрии на STM32F103C8T6) +без изменения поведения: разделение на ядро и порт сделано переносом +функций, тела не правились. diff --git a/c/ds18b20/include/ds18b20.h b/c/ds18b20/include/ds18b20.h new file mode 100644 index 0000000..3f59403 --- /dev/null +++ b/c/ds18b20/include/ds18b20.h @@ -0,0 +1,125 @@ +/** + * @file ds18b20.h + * @brief Драйвер термометров DS18B20 поверх шины 1-Wire. + * + * Модуль хранит перечень найденных датчиков и последние измерения, поэтому + * ответ на запрос GUI формируется без обращения к шине. + */ + +#ifndef DS18B20_H +#define DS18B20_H + +#include + +#include "onewire.h" + +/** Максимум датчиков, помещающихся в один кадр SENSOR_DATA. */ +#define DS18B20_MAX_SENSORS 8U + +/** Код семейства DS18B20 в первом байте ROM. */ +#define DS18B20_FAMILY_CODE 0x28U + +/** Размер scratchpad вместе с байтом CRC8. */ +#define DS18B20_SCRATCHPAD_SIZE 9U + +#define DS18B20_STATUS_VALID 0x01U /**< Измерение достоверно. */ +#define DS18B20_STATUS_CRC_ERROR 0x02U /**< Не сошёлся CRC8 scratchpad. */ +#define DS18B20_STATUS_NO_PRESENCE 0x04U /**< Датчик не ответил на сброс. */ +#define DS18B20_STATUS_PARASITE 0x08U /**< Датчик питается паразитно. */ + +#define DS18B20_WRITE_OK 0U /**< Запись подтверждена обратным чтением. */ +#define DS18B20_WRITE_BUS_ERROR 1U /**< Нет presence или не сошёлся CRC8. */ +#define DS18B20_WRITE_REJECTED 2U /**< Датчик ответил, но значение не изменил. */ + +/** + * @brief Состояние одного датчика на шине. + */ +typedef struct { + uint8_t rom[ONEWIRE_ROM_SIZE]; /**< Уникальный 64-битный идентификатор. */ + int16_t raw; /**< Код температуры из scratchpad. */ + uint8_t user_byte1; /**< Пользовательский байт 1 (регистр TH). */ + uint8_t user_byte2; /**< Пользовательский байт 2 (регистр TL). */ + uint8_t config; /**< Байт конфигурации с разрешением. */ + uint8_t status; /**< Битовая маска DS18B20_STATUS_*. */ +} DS18B20_Sensor; + +/** + * @brief Перечень датчиков шины и общее состояние питания. + */ +typedef struct { + DS18B20_Sensor sensors[DS18B20_MAX_SENSORS]; /**< Найденные датчики. */ + uint8_t count; /**< Число заполненных элементов массива. */ + uint8_t parasite; /**< 1, если хотя бы один датчик питается паразитно. */ +} DS18B20_Bus; + +/** + * @brief Ищет датчики DS18B20 и запоминает их идентификаторы. + * + * Устройства других семейств пропускаются; ранее накопленные измерения + * сбрасываются. + * + * @param bus Перечень датчиков, принадлежащий приложению. + * @return Количество найденных датчиков. + */ +uint8_t DS18B20_Scan(DS18B20_Bus *bus); + +/** + * @brief Запускает преобразование во всех датчиках командой SKIP ROM. + * + * @return 1 при наличии presence-импульса, иначе 0. + */ +uint8_t DS18B20_StartConversion(void); + +/** + * @brief Читает scratchpad всех известных датчиков и обновляет измерения. + * + * Ошибка отдельного датчика не прерывает опрос остальных: она отражается в + * поле status соответствующего элемента. + * + * @param bus Перечень датчиков, заполненный DS18B20_Scan. + * @return Количество датчиков с достоверным измерением. + */ +uint8_t DS18B20_ReadAll(DS18B20_Bus *bus); + +/** + * @brief Возвращает время преобразования для заданного разрешения. + * + * @param config Байт конфигурации scratchpad. + * @return Время преобразования в миллисекундах. + */ +uint16_t DS18B20_ConversionTimeMs(uint8_t config); + +/** + * @brief Собирает байт конфигурации из разрешения в битах. + * + * @param bits Разрешение 9..12 бит. + * @return Байт конфигурации scratchpad. + */ +uint8_t DS18B20_ConfigFromResolution(uint8_t bits); + +/** + * @brief Ищет датчик по идентификатору в накопленном перечне. + * + * @param bus Перечень датчиков. + * @param rom Искомый идентификатор. + * @return Указатель на элемент перечня либо 0, если ROM не найден. + */ +DS18B20_Sensor *DS18B20_Find(DS18B20_Bus *bus, const uint8_t *rom); + +/** + * @brief Записывает три байта scratchpad выбранного датчика. + * + * @param sensor Датчик из перечня; кэш обновляется при успешной записи. + * @param user_byte1 Новое значение пользовательского байта 1 (TH). + * @param user_byte2 Новое значение пользовательского байта 2 (TL). + * @param config Байт конфигурации с требуемым разрешением. + * @param save_to_eeprom Признак копирования scratchpad в EEPROM датчика. + * @return DS18B20_WRITE_OK при подтверждённой записи, DS18B20_WRITE_BUS_ERROR + * при молчании датчика или сбое CRC8, DS18B20_WRITE_REJECTED если + * датчик ответил, но оставил прежние значения. + */ +uint8_t DS18B20_WriteScratchpad(DS18B20_Sensor *sensor, uint8_t user_byte1, + uint8_t user_byte2, uint8_t config, + uint8_t save_to_eeprom); + +#endif /* DS18B20_H */ diff --git a/c/ds18b20/include/onewire.h b/c/ds18b20/include/onewire.h new file mode 100644 index 0000000..989e432 --- /dev/null +++ b/c/ds18b20/include/onewire.h @@ -0,0 +1,149 @@ +/** + * @file onewire.h + * @brief Программная шина 1-Wire на выводе с открытым стоком. + * + * Драйвер не зависит от типа датчика: он выдаёт тайминги, ищет ROM и считает + * CRC8. Работа с DS18B20 вынесена в ds18b20.c. + * + * Реализация разделена надвое: + * src/onewire.c переносимая часть - побайтовый обмен, CRC8, поиск ROM, + * MATCH ROM и SKIP ROM; ни регистров, ни задержек; + * ports/<платформа> пять функций, зависящих от железа: OneWire_Init, + * OneWire_DelayUs, OneWire_Reset, OneWire_WriteBit + * и OneWire_ReadBit. + * + * Чтобы перенести библиотеку на другой микроконтроллер, достаточно написать + * эти пять функций - остальное собирается как есть. + */ + +#ifndef ONEWIRE_H +#define ONEWIRE_H + +#include + +/** Размер идентификатора устройства 1-Wire в байтах. */ +#define ONEWIRE_ROM_SIZE 8U + +/** + * @brief Состояние обхода шины алгоритмом SEARCH ROM. + * + * Структура принадлежит вызывающему коду; параллельные обходы должны + * использовать разные экземпляры. + */ +typedef struct { + uint8_t rom[ONEWIRE_ROM_SIZE]; /**< Текущий найденный идентификатор. */ + uint8_t last_discrepancy; /**< Позиция последней развилки, 1..64. */ + uint8_t last_family_discrepancy; /**< Развилка внутри кода семейства. */ + uint8_t last_device; /**< Признак завершённого обхода. */ +} OneWire_Search; + +/** + * @brief Настраивает вывод шины и счётчик циклов для микросекундных задержек. + * + * Вызывается один раз после инициализации тактирования системы. + */ +void OneWire_Init(void); + +/** + * @brief Формирует импульс сброса и проверяет ответ устройств. + * + * @note На время выборки presence прерывания запрещаются примерно на 75 мкс; + * длительность самого импульса сброса на точность не влияет. + * + * @return 1, если хотя бы одно устройство выдало presence-импульс, иначе 0. + */ +uint8_t OneWire_Reset(void); + +/** + * @brief Передаёт один бит в тайм-слоте записи. + * + * @param bit Передаваемое значение, отличное от нуля означает единицу. + */ +void OneWire_WriteBit(uint8_t bit); + +/** + * @brief Считывает один бит в тайм-слоте чтения. + * + * @return Принятое значение бита: 0 или 1. + */ +uint8_t OneWire_ReadBit(void); + +/** + * @brief Передаёт байт младшим битом вперёд. + * + * @param value Передаваемый байт. + */ +void OneWire_WriteByte(uint8_t value); + +/** + * @brief Принимает байт младшим битом вперёд. + * + * @return Принятый байт. + */ +uint8_t OneWire_ReadByte(void); + +/** + * @brief Передаёт блок байтов подряд без промежуточных сбросов. + * + * @param data Указатель на передаваемые байты. + * @param size Количество байтов. + */ +void OneWire_WriteBytes(const uint8_t *data, uint32_t size); + +/** + * @brief Принимает блок байтов подряд. + * + * @param data Буфер приёмника. + * @param size Количество байтов. + */ +void OneWire_ReadBytes(uint8_t *data, uint32_t size); + +/** + * @brief Считает CRC8 Dallas/Maxim с полиномом 0x8C. + * + * @param data Проверяемые байты. + * @param size Количество байтов. + * @return Значение CRC8; для блока вместе с контрольным байтом равно нулю. + */ +uint8_t OneWire_Crc8(const uint8_t *data, uint32_t size); + +/** + * @brief Подготавливает состояние к новому полному обходу шины. + * + * @param search Состояние обхода, принадлежащее вызывающему коду. + */ +void OneWire_SearchInit(OneWire_Search *search); + +/** + * @brief Находит следующий идентификатор на шине. + * + * @param search Состояние обхода, ранее подготовленное OneWire_SearchInit. + * @return 1, если поле rom содержит новый проверенный ROM, иначе 0. + */ +uint8_t OneWire_SearchNext(OneWire_Search *search); + +/** + * @brief Адресует конкретное устройство командой MATCH ROM. + * + * @param rom Восьмибайтовый идентификатор устройства. + * @return 1 при наличии presence-импульса, иначе 0. + */ +uint8_t OneWire_SelectRom(const uint8_t *rom); + +/** + * @brief Адресует все устройства шины командой SKIP ROM. + * + * @return 1 при наличии presence-импульса, иначе 0. + */ +uint8_t OneWire_SkipRom(void); + +/** + * @brief Задерживает выполнение на заданное число микросекунд. + * + * Используется драйверами шины и модулем DS18B20 при ожидании EEPROM. + * + * @param microseconds Длительность задержки. + */ +void OneWire_DelayUs(uint32_t microseconds); + +#endif /* ONEWIRE_H */ diff --git a/c/ds18b20/ports/stm32f1/onewire_config.template.h b/c/ds18b20/ports/stm32f1/onewire_config.template.h new file mode 100644 index 0000000..edbd1ca --- /dev/null +++ b/c/ds18b20/ports/stm32f1/onewire_config.template.h @@ -0,0 +1,26 @@ +/** + * @file onewire_config.template.h + * @brief Шаблон привязки шины 1-Wire для порта STM32F1. + * + * Скопируйте в своё приложение под именем onewire_config.h и положите на + * путь поиска заголовков. Библиотека сама этот файл не подставляет: + * привязка выводов принадлежит плате, а не драйверу. + * + * Требуется внешняя подтяжка 4.7 кОм к питанию. + */ + +#ifndef ONEWIRE_CONFIG_H +#define ONEWIRE_CONFIG_H + +#include "stm32f10x.h" + +/** Порт вывода шины. */ +#define ONEWIRE_GPIO GPIOA + +/** Номер вывода в порту, 0..15. */ +#define ONEWIRE_PIN 1U + +/** Бит включения тактирования порта в RCC->APB2ENR. */ +#define ONEWIRE_GPIO_CLOCK RCC_APB2ENR_IOPAEN + +#endif /* ONEWIRE_CONFIG_H */ diff --git a/c/ds18b20/ports/stm32f1/onewire_stm32f1.c b/c/ds18b20/ports/stm32f1/onewire_stm32f1.c new file mode 100644 index 0000000..5a8e605 --- /dev/null +++ b/c/ds18b20/ports/stm32f1/onewire_stm32f1.c @@ -0,0 +1,158 @@ +/** + * @file onewire_stm32f1.c + * @brief Порт шины 1-Wire на STM32F1: тайминги, вывод и задержки. + * + * Вывод постоянно настроен как выход с открытым стоком: чтение уровня + * выполняется через регистр IDR, поэтому переключать режим не требуется. + * Прерывания запрещаются только на длительность одного тайм-слота, чтобы не + * терять байты USART во время обмена. + * + * Привязка вывода приходит из onewire_config.h, который предоставляет + * приложение. Шаблон - onewire_config.template.h рядом с этим файлом. + */ + +#include "onewire.h" + +#include "onewire_config.h" + +#if !defined(ONEWIRE_GPIO) || !defined(ONEWIRE_PIN) || !defined(ONEWIRE_GPIO_CLOCK) +#error "onewire_config.h должен задать ONEWIRE_GPIO, ONEWIRE_PIN и ONEWIRE_GPIO_CLOCK" +#endif + +/** Длительности тайм-слотов в микросекундах согласно рекомендациям Maxim. */ +#define OW_RESET_LOW_US 480U +#define OW_PRESENCE_WAIT_US 70U +#define OW_RESET_TAIL_US 410U +#define OW_WRITE1_LOW_US 6U +#define OW_WRITE1_TAIL_US 64U +#define OW_WRITE0_LOW_US 60U +#define OW_WRITE0_TAIL_US 10U +#define OW_READ_LOW_US 6U +#define OW_READ_SAMPLE_US 9U +#define OW_READ_TAIL_US 55U + +/** Число тактов ядра на микросекунду; заполняется в OneWire_Init. */ +static uint32_t g_cycles_per_us = 72U; + +/** + * @brief Переводит вывод шины в активный низкий уровень. + */ +static void ow_drive_low(void) +{ + ONEWIRE_GPIO->BRR = (uint32_t)(1UL << ONEWIRE_PIN); +} + +/** + * @brief Отпускает шину; уровень поднимает внешний резистор подтяжки. + */ +static void ow_release(void) +{ + ONEWIRE_GPIO->BSRR = (uint32_t)(1UL << ONEWIRE_PIN); +} + +/** + * @brief Читает текущий уровень на выводе шины. + * + * @return 1 для высокого уровня, 0 для низкого. + */ +static uint8_t ow_read_pin(void) +{ + return (uint8_t)((ONEWIRE_GPIO->IDR >> ONEWIRE_PIN) & 1U); +} + +void OneWire_DelayUs(uint32_t microseconds) +{ + const uint32_t start = DWT->CYCCNT; + const uint32_t ticks = microseconds * g_cycles_per_us; + + while ((DWT->CYCCNT - start) < ticks) { + __NOP(); + } +} + +void OneWire_Init(void) +{ + uint32_t config; + + g_cycles_per_us = SystemCoreClock / 1000000U; + if (g_cycles_per_us == 0U) { + g_cycles_per_us = 1U; + } + + /* Счётчик тактов ядра — единственный источник микросекундных задержек. */ + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + DWT->CYCCNT = 0U; + DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; + + RCC->APB2ENR |= ONEWIRE_GPIO_CLOCK; + + /* Выход с открытым стоком, 50 МГц: CNF = 01, MODE = 11. */ + if (ONEWIRE_PIN < 8U) { + config = ONEWIRE_GPIO->CRL; + config &= ~(0xFUL << (ONEWIRE_PIN * 4U)); + config |= (0x7UL << (ONEWIRE_PIN * 4U)); + ONEWIRE_GPIO->CRL = config; + } else { + config = ONEWIRE_GPIO->CRH; + config &= ~(0xFUL << ((ONEWIRE_PIN - 8U) * 4U)); + config |= (0x7UL << ((ONEWIRE_PIN - 8U) * 4U)); + ONEWIRE_GPIO->CRH = config; + } + ow_release(); + OneWire_DelayUs(OW_RESET_LOW_US); +} + +uint8_t OneWire_Reset(void) +{ + uint32_t primask; + uint8_t presence; + + /* Удлинение импульса сброса безопасно, поэтому прерывания не запрещаем. */ + ow_drive_low(); + OneWire_DelayUs(OW_RESET_LOW_US); + + primask = __get_PRIMASK(); + __disable_irq(); + ow_release(); + OneWire_DelayUs(OW_PRESENCE_WAIT_US); + presence = (uint8_t)(ow_read_pin() == 0U); + __set_PRIMASK(primask); + + OneWire_DelayUs(OW_RESET_TAIL_US); + return (uint8_t)(presence && (ow_read_pin() != 0U)); +} + +void OneWire_WriteBit(uint8_t bit) +{ + const uint32_t primask = __get_PRIMASK(); + + __disable_irq(); + ow_drive_low(); + if (bit != 0U) { + OneWire_DelayUs(OW_WRITE1_LOW_US); + ow_release(); + OneWire_DelayUs(OW_WRITE1_TAIL_US); + } else { + OneWire_DelayUs(OW_WRITE0_LOW_US); + ow_release(); + OneWire_DelayUs(OW_WRITE0_TAIL_US); + } + __set_PRIMASK(primask); +} + +uint8_t OneWire_ReadBit(void) +{ + const uint32_t primask = __get_PRIMASK(); + uint8_t bit; + + __disable_irq(); + ow_drive_low(); + OneWire_DelayUs(OW_READ_LOW_US); + ow_release(); + OneWire_DelayUs(OW_READ_SAMPLE_US); + bit = ow_read_pin(); + __set_PRIMASK(primask); + + OneWire_DelayUs(OW_READ_TAIL_US); + return bit; +} diff --git a/c/ds18b20/src/ds18b20.c b/c/ds18b20/src/ds18b20.c new file mode 100644 index 0000000..cf0a07e --- /dev/null +++ b/c/ds18b20/src/ds18b20.c @@ -0,0 +1,202 @@ +/** test + * @file ds18b20.c + * @brief Реализация драйвера DS18B20: поиск, чтение и запись scratchpad. + */ + +#include "ds18b20.h" + +#define DS_CMD_CONVERT_T 0x44U +#define DS_CMD_WRITE_SCRATCHPAD 0x4EU +#define DS_CMD_READ_SCRATCHPAD 0xBEU +#define DS_CMD_COPY_SCRATCHPAD 0x48U +#define DS_CMD_READ_POWER 0xB4U + +/** Задержка записи EEPROM датчика по datasheet, мкс. */ +#define DS_EEPROM_WRITE_US 12000U + +/** + * @brief Считывает scratchpad адресованного датчика с проверкой CRC8. + * + * @param rom Идентификатор датчика. + * @param scratchpad Буфер на DS18B20_SCRATCHPAD_SIZE байт. + * @return Битовая маска ошибок DS18B20_STATUS_* без бита VALID. + */ +static uint8_t ds_read_scratchpad(const uint8_t *rom, uint8_t *scratchpad) +{ + if (OneWire_SelectRom(rom) == 0U) { + return DS18B20_STATUS_NO_PRESENCE; + } + OneWire_WriteByte(DS_CMD_READ_SCRATCHPAD); + OneWire_ReadBytes(scratchpad, DS18B20_SCRATCHPAD_SIZE); + + if (OneWire_Crc8(scratchpad, DS18B20_SCRATCHPAD_SIZE) != 0U) { + return DS18B20_STATUS_CRC_ERROR; + } + return 0U; +} + +/** + * @brief Опрашивает тип питания датчика командой READ POWER SUPPLY. + * + * @param rom Идентификатор датчика. + * @return 1 для паразитного питания, иначе 0. + */ +static uint8_t ds_read_parasite(const uint8_t *rom) +{ + if (OneWire_SelectRom(rom) == 0U) { + return 0U; + } + OneWire_WriteByte(DS_CMD_READ_POWER); + /* Датчик с паразитным питанием прижимает шину к нулю. */ + return (uint8_t)(OneWire_ReadBit() == 0U); +} + +uint8_t DS18B20_ConfigFromResolution(uint8_t bits) +{ + if (bits < 9U) { + bits = 9U; + } else if (bits > 12U) { + bits = 12U; + } + return (uint8_t)(0x1FU | ((bits - 9U) << 5U)); +} + +uint16_t DS18B20_ConversionTimeMs(uint8_t config) +{ + switch ((config >> 5U) & 0x03U) { + case 0U: + return 94U; + case 1U: + return 188U; + case 2U: + return 375U; + default: + return 750U; + } +} + +uint8_t DS18B20_Scan(DS18B20_Bus *bus) +{ + OneWire_Search search; + uint8_t index; + + bus->count = 0U; + bus->parasite = 0U; + OneWire_SearchInit(&search); + + while ((bus->count < DS18B20_MAX_SENSORS) && (OneWire_SearchNext(&search) != 0U)) { + if (search.rom[0] != DS18B20_FAMILY_CODE) { + continue; + } + DS18B20_Sensor *sensor = &bus->sensors[bus->count]; + for (index = 0U; index < ONEWIRE_ROM_SIZE; index++) { + sensor->rom[index] = search.rom[index]; + } + sensor->raw = 0; + sensor->user_byte1 = 0U; + sensor->user_byte2 = 0U; + sensor->config = DS18B20_ConfigFromResolution(12U); + sensor->status = 0U; + if (ds_read_parasite(sensor->rom) != 0U) { + sensor->status |= DS18B20_STATUS_PARASITE; + bus->parasite = 1U; + } + bus->count++; + } + return bus->count; +} + +uint8_t DS18B20_StartConversion(void) +{ + if (OneWire_SkipRom() == 0U) { + return 0U; + } + OneWire_WriteByte(DS_CMD_CONVERT_T); + return 1U; +} + +uint8_t DS18B20_ReadAll(DS18B20_Bus *bus) +{ + uint8_t scratchpad[DS18B20_SCRATCHPAD_SIZE]; + uint8_t valid = 0U; + uint8_t index; + + for (index = 0U; index < bus->count; index++) { + DS18B20_Sensor *sensor = &bus->sensors[index]; + const uint8_t parasite = (uint8_t)(sensor->status & DS18B20_STATUS_PARASITE); + const uint8_t error = ds_read_scratchpad(sensor->rom, scratchpad); + + if (error != 0U) { + sensor->status = (uint8_t)(parasite | error); + continue; + } + sensor->raw = (int16_t)((uint16_t)scratchpad[0] | ((uint16_t)scratchpad[1] << 8U)); + sensor->user_byte1 = scratchpad[2]; + sensor->user_byte2 = scratchpad[3]; + sensor->config = scratchpad[4]; + sensor->status = (uint8_t)(parasite | DS18B20_STATUS_VALID); + valid++; + } + return valid; +} + +DS18B20_Sensor *DS18B20_Find(DS18B20_Bus *bus, const uint8_t *rom) +{ + uint8_t index; + uint8_t byte; + + for (index = 0U; index < bus->count; index++) { + for (byte = 0U; byte < ONEWIRE_ROM_SIZE; byte++) { + if (bus->sensors[index].rom[byte] != rom[byte]) { + break; + } + } + if (byte == ONEWIRE_ROM_SIZE) { + return &bus->sensors[index]; + } + } + return 0; +} + +uint8_t DS18B20_WriteScratchpad(DS18B20_Sensor *sensor, uint8_t user_byte1, + uint8_t user_byte2, uint8_t config, + uint8_t save_to_eeprom) +{ + uint8_t scratchpad[DS18B20_SCRATCHPAD_SIZE]; + + if (OneWire_SelectRom(sensor->rom) == 0U) { + sensor->status = (uint8_t)((sensor->status & DS18B20_STATUS_PARASITE) + | DS18B20_STATUS_NO_PRESENCE); + return DS18B20_WRITE_BUS_ERROR; + } + OneWire_WriteByte(DS_CMD_WRITE_SCRATCHPAD); + OneWire_WriteByte(user_byte1); + OneWire_WriteByte(user_byte2); + OneWire_WriteByte(config); + + /* Обратное чтение подтверждает, что датчик принял все три байта. */ + if (ds_read_scratchpad(sensor->rom, scratchpad) != 0U) { + return DS18B20_WRITE_BUS_ERROR; + } + if ((scratchpad[2] != user_byte1) || (scratchpad[3] != user_byte2) + || (scratchpad[4] != config)) { + /* Обмен состоялся, но регистры остались прежними: значение отвергнуто. */ + sensor->user_byte1 = scratchpad[2]; + sensor->user_byte2 = scratchpad[3]; + sensor->config = scratchpad[4]; + return DS18B20_WRITE_REJECTED; + } + sensor->user_byte1 = scratchpad[2]; + sensor->user_byte2 = scratchpad[3]; + sensor->config = scratchpad[4]; + + if (save_to_eeprom != 0U) { + if (OneWire_SelectRom(sensor->rom) == 0U) { + return DS18B20_WRITE_BUS_ERROR; + } + OneWire_WriteByte(DS_CMD_COPY_SCRATCHPAD); + /* Датчик с внешним питанием тоже допускает выдержку по времени. */ + OneWire_DelayUs(DS_EEPROM_WRITE_US); + } + return DS18B20_WRITE_OK; +} diff --git a/c/ds18b20/src/onewire.c b/c/ds18b20/src/onewire.c new file mode 100644 index 0000000..f65b2c2 --- /dev/null +++ b/c/ds18b20/src/onewire.c @@ -0,0 +1,177 @@ +/** + * @file onewire.c + * @brief Переносимая часть шины 1-Wire: побайтовый обмен, CRC8 и поиск ROM. + * + * Здесь нет ни регистров, ни задержек, ни запрета прерываний: всё это живёт + * в порте (ports/), а этот файл собирается любым C99-компилятором и + * проверяется host-тестами на симуляторе шины. + * + * От порта требуются пять функций: OneWire_Init, OneWire_DelayUs, + * OneWire_Reset, OneWire_WriteBit и OneWire_ReadBit. + */ + +#include "onewire.h" + +#define OW_CMD_SEARCH_ROM 0xF0U +#define OW_CMD_MATCH_ROM 0x55U +#define OW_CMD_SKIP_ROM 0xCCU + +void OneWire_WriteByte(uint8_t value) +{ + uint8_t index; + + for (index = 0U; index < 8U; index++) { + OneWire_WriteBit((uint8_t)(value & 1U)); + value = (uint8_t)(value >> 1U); + } +} + +uint8_t OneWire_ReadByte(void) +{ + uint8_t value = 0U; + uint8_t index; + + for (index = 0U; index < 8U; index++) { + value = (uint8_t)(value >> 1U); + if (OneWire_ReadBit() != 0U) { + value |= 0x80U; + } + } + return value; +} + +void OneWire_WriteBytes(const uint8_t *data, uint32_t size) +{ + uint32_t index; + + for (index = 0U; index < size; index++) { + OneWire_WriteByte(data[index]); + } +} + +void OneWire_ReadBytes(uint8_t *data, uint32_t size) +{ + uint32_t index; + + for (index = 0U; index < size; index++) { + data[index] = OneWire_ReadByte(); + } +} + +uint8_t OneWire_Crc8(const uint8_t *data, uint32_t size) +{ + uint8_t crc = 0U; + uint32_t index; + uint8_t bit; + + for (index = 0U; index < size; index++) { + crc ^= data[index]; + for (bit = 0U; bit < 8U; bit++) { + crc = (uint8_t)((crc & 1U) != 0U ? ((crc >> 1U) ^ 0x8CU) : (crc >> 1U)); + } + } + return crc; +} + +void OneWire_SearchInit(OneWire_Search *search) +{ + uint32_t index; + + for (index = 0U; index < ONEWIRE_ROM_SIZE; index++) { + search->rom[index] = 0U; + } + search->last_discrepancy = 0U; + search->last_family_discrepancy = 0U; + search->last_device = 0U; +} + +uint8_t OneWire_SearchNext(OneWire_Search *search) +{ + uint8_t id_bit_number = 1U; + uint8_t last_zero = 0U; + uint8_t rom_byte_number = 0U; + uint8_t rom_byte_mask = 1U; + uint8_t search_direction; + uint8_t id_bit; + uint8_t cmp_id_bit; + + if (search->last_device != 0U) { + OneWire_SearchInit(search); + return 0U; + } + if (OneWire_Reset() == 0U) { + OneWire_SearchInit(search); + return 0U; + } + OneWire_WriteByte(OW_CMD_SEARCH_ROM); + + do { + id_bit = OneWire_ReadBit(); + cmp_id_bit = OneWire_ReadBit(); + if ((id_bit != 0U) && (cmp_id_bit != 0U)) { + /* Ни одно устройство не ответило: обход прерван. */ + OneWire_SearchInit(search); + return 0U; + } + + if (id_bit != cmp_id_bit) { + search_direction = id_bit; + } else if (id_bit_number < search->last_discrepancy) { + search_direction = + (uint8_t)((search->rom[rom_byte_number] & rom_byte_mask) != 0U); + } else { + search_direction = (uint8_t)(id_bit_number == search->last_discrepancy); + } + + if ((id_bit == cmp_id_bit) && (search_direction == 0U)) { + last_zero = id_bit_number; + if (last_zero < 9U) { + search->last_family_discrepancy = last_zero; + } + } + + if (search_direction != 0U) { + search->rom[rom_byte_number] |= rom_byte_mask; + } else { + search->rom[rom_byte_number] &= (uint8_t)~rom_byte_mask; + } + OneWire_WriteBit(search_direction); + + id_bit_number++; + rom_byte_mask = (uint8_t)(rom_byte_mask << 1U); + if (rom_byte_mask == 0U) { + rom_byte_number++; + rom_byte_mask = 1U; + } + } while (rom_byte_number < ONEWIRE_ROM_SIZE); + + if (OneWire_Crc8(search->rom, ONEWIRE_ROM_SIZE) != 0U) { + OneWire_SearchInit(search); + return 0U; + } + + search->last_discrepancy = last_zero; + if (search->last_discrepancy == 0U) { + search->last_device = 1U; + } + return 1U; +} + +uint8_t OneWire_SelectRom(const uint8_t *rom) +{ + if (OneWire_Reset() == 0U) { + return 0U; + } + OneWire_WriteByte(OW_CMD_MATCH_ROM); + OneWire_WriteBytes(rom, ONEWIRE_ROM_SIZE); + return 1U; +} + +uint8_t OneWire_SkipRom(void) +{ + if (OneWire_Reset() == 0U) { + return 0U; + } + OneWire_WriteByte(OW_CMD_SKIP_ROM); + return 1U; +} diff --git a/c/ds18b20/tests/fake_onewire.c b/c/ds18b20/tests/fake_onewire.c new file mode 100644 index 0000000..eab668e --- /dev/null +++ b/c/ds18b20/tests/fake_onewire.c @@ -0,0 +1,305 @@ +/** + * @file fake_onewire.c + * @brief Симулятор шины 1-Wire с виртуальными датчиками DS18B20. + * + * Реализует те же пять функций, что и настоящий порт, но работает не с + * выводом, а с моделью шины. Этого достаточно, чтобы прогнать на хосте + * весь алгоритм SEARCH ROM и обмен с датчиком побитно - именно там + * прячутся ошибки, которых не видно в обзоре кода. + */ + +#include "fake_onewire.h" + +#include + +#include "onewire.h" + +/** Состояние разбора команд шины. */ +typedef enum { + FAKE_IDLE = 0, /**< После сброса, ждём код команды ROM. */ + FAKE_SEARCH, /**< Идёт SEARCH ROM. */ + FAKE_MATCH, /**< Принимаем 8 байт адреса. */ + FAKE_FUNCTION, /**< ROM-команда отработала, ждём функцию. */ + FAKE_READ_SCRATCH, /**< Отдаём scratchpad. */ + FAKE_WRITE_SCRATCH, /**< Принимаем TH, TL, config. */ + FAKE_READ_POWER /**< Отдаём один бит типа питания. */ +} fake_state_t; + +static fake_bus_t *g_bus; +static fake_state_t g_state; + +/* Сборка входящих и исходящих байтов по битам. */ +static uint8_t g_in_byte; +static uint8_t g_in_bits; +static uint8_t g_out_byte; +static uint8_t g_out_bits; +static uint8_t g_out_index; + +/* Состояние SEARCH ROM. */ +static uint8_t g_search_bit; /**< Номер бита ROM, 0..63. */ +static uint8_t g_search_phase; /**< 0 - прямой, 1 - обратный, 2 - выбор. */ +static uint8_t g_match_index; +static uint8_t g_write_index; + +/** Активен ли датчик в текущей транзакции. */ +static uint8_t g_selected[FAKE_MAX_DEVICES]; + +void fake_bus_attach(fake_bus_t *bus) +{ + g_bus = bus; + g_state = FAKE_IDLE; + g_in_byte = 0U; + g_in_bits = 0U; + g_out_bits = 0U; + g_out_index = 0U; + bus->reset_count = 0U; + bus->convert_count = 0U; + bus->copy_count = 0U; + bus->delay_us_total = 0U; +} + +uint8_t fake_bus_crc8(const uint8_t *data, uint32_t size) +{ + uint8_t crc = 0U; + for (uint32_t i = 0U; i < size; i++) { + crc ^= data[i]; + for (uint8_t bit = 0U; bit < 8U; bit++) { + crc = (uint8_t)((crc & 1U) ? ((crc >> 1) ^ 0x8CU) : (crc >> 1)); + } + } + return crc; +} + +void fake_device_init(fake_device_t *device, const uint8_t rom7[7], + int16_t raw, uint8_t parasite) +{ + memcpy(device->rom, rom7, 7U); + device->rom[7] = fake_bus_crc8(device->rom, 7U); + device->present = 1U; + device->parasite = parasite; + device->accept_writes = 1U; + device->corrupt_crc = 0U; + + device->scratchpad[0] = (uint8_t)((uint16_t)raw & 0xFFU); + device->scratchpad[1] = (uint8_t)(((uint16_t)raw >> 8) & 0xFFU); + device->scratchpad[2] = 0x4BU; /* TH по умолчанию */ + device->scratchpad[3] = 0x46U; /* TL по умолчанию */ + device->scratchpad[4] = 0x7FU; /* 12 бит */ + device->scratchpad[5] = 0xFFU; + device->scratchpad[6] = 0x0CU; + device->scratchpad[7] = 0x10U; + device->scratchpad[8] = fake_bus_crc8(device->scratchpad, 8U); +} + +static void refresh_crc(fake_device_t *device) +{ + device->scratchpad[8] = fake_bus_crc8(device->scratchpad, 8U); + if (device->corrupt_crc != 0U) { + device->scratchpad[8] ^= 0xFFU; + } +} + +/** Единственный выбранный датчик либо NULL, если их несколько или ноль. */ +static fake_device_t *sole_selected(void) +{ + fake_device_t *found = NULL; + for (uint8_t i = 0U; i < g_bus->count; i++) { + if ((g_selected[i] != 0U) && (g_bus->devices[i].present != 0U)) { + if (found != NULL) { + return NULL; + } + found = &g_bus->devices[i]; + } + } + return found; +} + +/* --- Порт: пять функций, которых ждёт переносимая часть ------------------- */ + +void OneWire_Init(void) +{ + g_state = FAKE_IDLE; +} + +void OneWire_DelayUs(uint32_t microseconds) +{ + if (g_bus != NULL) { + g_bus->delay_us_total += microseconds; + } +} + +uint8_t OneWire_Reset(void) +{ + uint8_t presence = 0U; + + g_state = FAKE_IDLE; + g_in_byte = 0U; + g_in_bits = 0U; + g_out_bits = 0U; + g_out_index = 0U; + g_search_bit = 0U; + g_search_phase = 0U; + + for (uint8_t i = 0U; i < g_bus->count; i++) { + g_selected[i] = g_bus->devices[i].present; + if (g_bus->devices[i].present != 0U) { + presence = 1U; + } + } + g_bus->reset_count++; + return presence; +} + +/** Обрабатывает собранный байт команды. */ +static void dispatch_byte(uint8_t value) +{ + switch (g_state) { + case FAKE_IDLE: + if (value == 0xF0U) { /* SEARCH ROM */ + g_state = FAKE_SEARCH; + g_search_bit = 0U; + g_search_phase = 0U; + } else if (value == 0x55U) { /* MATCH ROM */ + g_state = FAKE_MATCH; + g_match_index = 0U; + } else if (value == 0xCCU) { /* SKIP ROM */ + g_state = FAKE_FUNCTION; + } + break; + + case FAKE_MATCH: + for (uint8_t i = 0U; i < g_bus->count; i++) { + if (g_bus->devices[i].rom[g_match_index] != value) { + g_selected[i] = 0U; + } + } + g_match_index++; + if (g_match_index >= 8U) { + g_state = FAKE_FUNCTION; + } + break; + + case FAKE_FUNCTION: { + fake_device_t *device = sole_selected(); + if (value == 0x44U) { /* CONVERT T */ + g_bus->convert_count++; + } else if (value == 0xBEU) { /* READ SCRATCHPAD */ + if (device != NULL) { + refresh_crc(device); + g_state = FAKE_READ_SCRATCH; + g_out_index = 0U; + g_out_bits = 0U; + } + } else if (value == 0x4EU) { /* WRITE SCRATCHPAD */ + g_state = FAKE_WRITE_SCRATCH; + g_write_index = 0U; + } else if (value == 0x48U) { /* COPY SCRATCHPAD */ + g_bus->copy_count++; + } else if (value == 0xB4U) { /* READ POWER SUPPLY */ + g_state = FAKE_READ_POWER; + } + break; + } + + case FAKE_WRITE_SCRATCH: { + fake_device_t *device = sole_selected(); + if ((device != NULL) && (device->accept_writes != 0U)) { + device->scratchpad[2U + g_write_index] = value; + } + g_write_index++; + if (g_write_index >= 3U) { + if (device != NULL) { + refresh_crc(device); + } + g_state = FAKE_FUNCTION; + } + break; + } + + default: + break; + } +} + +void OneWire_WriteBit(uint8_t bit) +{ + if (g_state == FAKE_SEARCH) { + /* Третья фаза SEARCH ROM: мастер сообщает выбранную ветвь. */ + for (uint8_t i = 0U; i < g_bus->count; i++) { + if (g_selected[i] == 0U) { + continue; + } + uint8_t rom_bit = (uint8_t)((g_bus->devices[i].rom[g_search_bit / 8U] + >> (g_search_bit % 8U)) & 1U); + if (rom_bit != (bit != 0U)) { + g_selected[i] = 0U; + } + } + g_search_bit++; + g_search_phase = 0U; + if (g_search_bit >= 64U) { + g_state = FAKE_FUNCTION; + } + return; + } + + g_in_byte = (uint8_t)((g_in_byte >> 1) | ((bit != 0U) ? 0x80U : 0U)); + g_in_bits++; + if (g_in_bits >= 8U) { + uint8_t value = g_in_byte; + g_in_byte = 0U; + g_in_bits = 0U; + dispatch_byte(value); + } +} + +uint8_t OneWire_ReadBit(void) +{ + if (g_state == FAKE_SEARCH) { + uint8_t ones = 0U; + uint8_t zeros = 0U; + for (uint8_t i = 0U; i < g_bus->count; i++) { + if (g_selected[i] == 0U) { + continue; + } + uint8_t rom_bit = (uint8_t)((g_bus->devices[i].rom[g_search_bit / 8U] + >> (g_search_bit % 8U)) & 1U); + if (rom_bit != 0U) { + ones = 1U; + } else { + zeros = 1U; + } + } + if ((ones == 0U) && (zeros == 0U)) { + return 1U; /* никто не отвечает - обе фазы единичны */ + } + uint8_t result = (g_search_phase == 0U) ? (uint8_t)(zeros == 0U) + : (uint8_t)(ones == 0U); + g_search_phase = (uint8_t)(g_search_phase + 1U); + return result; + } + + if (g_state == FAKE_READ_POWER) { + fake_device_t *device = sole_selected(); + /* Паразитное питание прижимает шину к нулю. */ + return (uint8_t)((device != NULL) && (device->parasite != 0U) ? 0U : 1U); + } + + if (g_state == FAKE_READ_SCRATCH) { + fake_device_t *device = sole_selected(); + if (device == NULL) { + return 1U; + } + if (g_out_bits == 0U) { + g_out_byte = (g_out_index < 9U) ? device->scratchpad[g_out_index] : 0xFFU; + } + uint8_t bit = (uint8_t)((g_out_byte >> g_out_bits) & 1U); + g_out_bits++; + if (g_out_bits >= 8U) { + g_out_bits = 0U; + g_out_index++; + } + return bit; + } + return 1U; +} diff --git a/c/ds18b20/tests/fake_onewire.h b/c/ds18b20/tests/fake_onewire.h new file mode 100644 index 0000000..095bb72 --- /dev/null +++ b/c/ds18b20/tests/fake_onewire.h @@ -0,0 +1,41 @@ +/** + * @file fake_onewire.h + * @brief Модель шины 1-Wire с виртуальными датчиками для host-тестов. + */ + +#ifndef FAKE_ONEWIRE_H +#define FAKE_ONEWIRE_H + +#include + +#define FAKE_MAX_DEVICES 8U + +/** Виртуальный датчик: ROM, scratchpad и управляемые отказы. */ +typedef struct { + uint8_t rom[8]; + uint8_t scratchpad[9]; + uint8_t present; /**< 0 - датчик снят с шины. */ + uint8_t parasite; /**< 1 - отвечает как паразитно питаемый. */ + uint8_t accept_writes; /**< 0 - молча игнорирует запись. */ + uint8_t corrupt_crc; /**< 1 - портит CRC8 scratchpad. */ +} fake_device_t; + +typedef struct { + fake_device_t devices[FAKE_MAX_DEVICES]; + uint8_t count; + uint32_t reset_count; + uint32_t convert_count; + uint32_t copy_count; + uint32_t delay_us_total; +} fake_bus_t; + +/** Подключает модель к порту; после вызова работают функции OneWire_*. */ +void fake_bus_attach(fake_bus_t *bus); + +/** Заполняет датчик: семь байт ROM без CRC, температура и тип питания. */ +void fake_device_init(fake_device_t *device, const uint8_t rom7[7], + int16_t raw, uint8_t parasite); + +uint8_t fake_bus_crc8(const uint8_t *data, uint32_t size); + +#endif /* FAKE_ONEWIRE_H */ diff --git a/c/ds18b20/tests/test_ds18b20.c b/c/ds18b20/tests/test_ds18b20.c new file mode 100644 index 0000000..47007bc --- /dev/null +++ b/c/ds18b20/tests/test_ds18b20.c @@ -0,0 +1,356 @@ +/** + * Тесты драйвера DS18B20 и переносимой части 1-Wire на симуляторе шины. + * + * clang -std=c99 -Wall -Wextra -Iinclude -Itests \ + * tests/test_ds18b20.c tests/fake_onewire.c src/onewire.c src/ds18b20.c -o test + */ +#include +#include + +#include "ds18b20.h" +#include "fake_onewire.h" +#include "onewire.h" + +static int g_fail; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \ + g_fail++; \ + } \ + } while (0) + +/* Три датчика с ROM, которые расходятся в разных битах: обход должен + пройти по всем ветвям дерева SEARCH ROM. */ +static const uint8_t ROM_A[7] = { 0x28, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66 }; +static const uint8_t ROM_B[7] = { 0x28, 0x11, 0x22, 0x33, 0x44, 0x55, 0x67 }; +static const uint8_t ROM_C[7] = { 0x28, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x01 }; +/* Датчик чужого семейства: DS18B20_Scan обязан его пропустить. */ +static const uint8_t ROM_OTHER[7] = { 0x10, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 }; + +static fake_bus_t g_bus; + +/** + * @brief Находит запись по ROM виртуального датчика. + * + * Порядок обхода SEARCH ROM задаётся битами идентификаторов, а не порядком + * массива, поэтому опираться на индекс в тестах нельзя. + */ +static DS18B20_Sensor *by_device(DS18B20_Bus *bus, uint8_t device_index) +{ + return DS18B20_Find(bus, g_bus.devices[device_index].rom); +} + +static void setup(uint8_t count) +{ + memset(&g_bus, 0, sizeof g_bus); + g_bus.count = count; + fake_bus_attach(&g_bus); +} + +/* -- CRC8 ------------------------------------------------------------------ */ + +static void test_crc8(void) +{ + /* Контрольный ROM из datasheet Maxim: CRC блока вместе с байтом = 0. */ + static const uint8_t rom[8] = { 0x28, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x00 }; + uint8_t crc = OneWire_Crc8(rom, 7U); + uint8_t full[8]; + + memcpy(full, rom, 8U); + full[7] = crc; + CHECK(OneWire_Crc8(full, 8U) == 0U); + /* Симулятор и библиотека считают CRC8 одинаково. */ + CHECK(fake_bus_crc8(rom, 7U) == crc); +} + +/* -- Поиск ----------------------------------------------------------------- */ + +static void test_search_finds_all(void) +{ + setup(3U); + fake_device_init(&g_bus.devices[0], ROM_A, 0, 0U); + fake_device_init(&g_bus.devices[1], ROM_B, 0, 0U); + fake_device_init(&g_bus.devices[2], ROM_C, 0, 0U); + + OneWire_Search search; + uint8_t found[3][8]; + uint8_t count = 0U; + + OneWire_SearchInit(&search); + while ((count < 3U) && (OneWire_SearchNext(&search) != 0U)) { + memcpy(found[count], search.rom, 8U); + count++; + } + CHECK(count == 3U); + /* Обход завершён: следующий вызов не должен выдавать четвёртый ROM. */ + CHECK(OneWire_SearchNext(&search) == 0U); + + /* Каждый ROM найден ровно один раз. */ + for (uint8_t d = 0U; d < 3U; d++) { + uint8_t hits = 0U; + for (uint8_t f = 0U; f < count; f++) { + if (memcmp(found[f], g_bus.devices[d].rom, 8U) == 0) { + hits++; + } + } + CHECK(hits == 1U); + } +} + +static void test_search_on_empty_bus(void) +{ + setup(0U); + OneWire_Search search; + OneWire_SearchInit(&search); + CHECK(OneWire_SearchNext(&search) == 0U); +} + +static void test_search_single_device(void) +{ + setup(1U); + fake_device_init(&g_bus.devices[0], ROM_C, 0, 0U); + + OneWire_Search search; + OneWire_SearchInit(&search); + CHECK(OneWire_SearchNext(&search) == 1U); + CHECK(memcmp(search.rom, g_bus.devices[0].rom, 8U) == 0); + CHECK(search.last_device == 1U); + CHECK(OneWire_SearchNext(&search) == 0U); +} + +/* -- Сканирование и чтение ------------------------------------------------- */ + +static void test_scan_filters_family(void) +{ + setup(3U); + fake_device_init(&g_bus.devices[0], ROM_A, 0, 0U); + fake_device_init(&g_bus.devices[1], ROM_OTHER, 0, 0U); + fake_device_init(&g_bus.devices[2], ROM_C, 0, 1U); + + DS18B20_Bus bus; + CHECK(DS18B20_Scan(&bus) == 2U); + CHECK(bus.count == 2U); + CHECK(bus.sensors[0].rom[0] == DS18B20_FAMILY_CODE); + CHECK(bus.sensors[1].rom[0] == DS18B20_FAMILY_CODE); + /* Паразитное питание одного датчика поднимает флаг на всю шину. */ + CHECK(bus.parasite == 1U); + CHECK((by_device(&bus, 2U)->status & DS18B20_STATUS_PARASITE) != 0U); + CHECK((by_device(&bus, 0U)->status & DS18B20_STATUS_PARASITE) == 0U); + /* Чужое семейство в перечень не попало. */ + CHECK(DS18B20_Find(&bus, g_bus.devices[1].rom) == NULL); +} + +static void test_read_all(void) +{ + setup(2U); + /* +25.0625 C и -10.125 C в формате DS18B20. */ + fake_device_init(&g_bus.devices[0], ROM_A, 0x0191, 0U); + fake_device_init(&g_bus.devices[1], ROM_C, (int16_t)0xFF5E, 0U); + + DS18B20_Bus bus; + CHECK(DS18B20_Scan(&bus) == 2U); + CHECK(DS18B20_StartConversion() == 1U); + CHECK(g_bus.convert_count == 1U); + CHECK(DS18B20_ReadAll(&bus) == 2U); + + DS18B20_Sensor *warm = by_device(&bus, 0U); + DS18B20_Sensor *cold = by_device(&bus, 1U); + CHECK(warm != NULL && cold != NULL); + CHECK(warm->raw == 0x0191); + CHECK(cold->raw == (int16_t)0xFF5E); + CHECK((warm->status & DS18B20_STATUS_VALID) != 0U); + CHECK(warm->user_byte1 == 0x4BU); + CHECK(warm->user_byte2 == 0x46U); + CHECK(warm->config == 0x7FU); +} + +static void test_crc_error_does_not_stop_the_poll(void) +{ + setup(2U); + fake_device_init(&g_bus.devices[0], ROM_A, 0x0100, 0U); + fake_device_init(&g_bus.devices[1], ROM_C, 0x0200, 0U); + + DS18B20_Bus bus; + CHECK(DS18B20_Scan(&bus) == 2U); + g_bus.devices[0].corrupt_crc = 1U; + + /* Первый датчик отдаёт битый scratchpad, второй обязан быть опрошен. */ + CHECK(DS18B20_ReadAll(&bus) == 1U); + DS18B20_Sensor *broken = by_device(&bus, 0U); + DS18B20_Sensor *good = by_device(&bus, 1U); + CHECK((broken->status & DS18B20_STATUS_CRC_ERROR) != 0U); + CHECK((broken->status & DS18B20_STATUS_VALID) == 0U); + CHECK((good->status & DS18B20_STATUS_VALID) != 0U); + CHECK(good->raw == 0x0200); +} + +static void test_missing_device_is_reported(void) +{ + setup(2U); + fake_device_init(&g_bus.devices[0], ROM_A, 0x0100, 0U); + fake_device_init(&g_bus.devices[1], ROM_C, 0x0200, 0U); + + DS18B20_Bus bus; + CHECK(DS18B20_Scan(&bus) == 2U); + + /* Датчик сняли с шины между сканированием и опросом. */ + DS18B20_Sensor *gone = by_device(&bus, 0U); + DS18B20_Sensor *alive = by_device(&bus, 1U); + g_bus.devices[0].present = 0U; + CHECK(DS18B20_ReadAll(&bus) == 1U); + + /* NO_PRESENCE здесь не появится: presence-импульс выдаёт оставшийся + датчик, и MATCH ROM к пропавшему проходит без признаков ошибки. + Пропажа видна только по CRC8 пустого scratchpad - так устроена шина. */ + CHECK((gone->status & DS18B20_STATUS_CRC_ERROR) != 0U); + CHECK((gone->status & DS18B20_STATUS_VALID) == 0U); + /* Пропажа соседа не должна мешать опросу оставшихся. */ + CHECK((alive->status & DS18B20_STATUS_VALID) != 0U); + + /* А вот когда молчит вся шина, драйвер обязан сказать NO_PRESENCE. */ + g_bus.devices[1].present = 0U; + CHECK(DS18B20_ReadAll(&bus) == 0U); + CHECK((gone->status & DS18B20_STATUS_NO_PRESENCE) != 0U); + CHECK((alive->status & DS18B20_STATUS_NO_PRESENCE) != 0U); + CHECK(DS18B20_StartConversion() == 0U); +} + +/* -- Поиск по ROM и запись -------------------------------------------------- */ + +static void test_find(void) +{ + setup(2U); + fake_device_init(&g_bus.devices[0], ROM_A, 0, 0U); + fake_device_init(&g_bus.devices[1], ROM_C, 0, 0U); + + DS18B20_Bus bus; + CHECK(DS18B20_Scan(&bus) == 2U); + + DS18B20_Sensor *found = DS18B20_Find(&bus, g_bus.devices[1].rom); + CHECK(found != NULL); + CHECK(memcmp(found->rom, g_bus.devices[1].rom, 8U) == 0); + + static const uint8_t unknown[8] = { 0x28, 0, 0, 0, 0, 0, 0, 0 }; + CHECK(DS18B20_Find(&bus, unknown) == NULL); +} + +static void test_write_scratchpad(void) +{ + setup(1U); + fake_device_init(&g_bus.devices[0], ROM_A, 0x0100, 0U); + + DS18B20_Bus bus; + CHECK(DS18B20_Scan(&bus) == 1U); + + DS18B20_Sensor *sensor = by_device(&bus, 0U); + uint8_t config = DS18B20_ConfigFromResolution(10U); + CHECK(DS18B20_WriteScratchpad(sensor, 0x11U, 0x22U, config, 0U) + == DS18B20_WRITE_OK); + CHECK(sensor->user_byte1 == 0x11U); + CHECK(sensor->user_byte2 == 0x22U); + CHECK(sensor->config == config); + CHECK(g_bus.devices[0].scratchpad[2] == 0x11U); + CHECK(g_bus.copy_count == 0U); + + /* Сохранение в EEPROM датчика: COPY SCRATCHPAD плюс выдержка. */ + uint32_t before = g_bus.delay_us_total; + CHECK(DS18B20_WriteScratchpad(sensor, 0x33U, 0x44U, config, 1U) + == DS18B20_WRITE_OK); + CHECK(g_bus.copy_count == 1U); + CHECK((g_bus.delay_us_total - before) >= 12000U); +} + +static void test_write_rejected(void) +{ + setup(1U); + fake_device_init(&g_bus.devices[0], ROM_A, 0x0100, 0U); + + DS18B20_Bus bus; + CHECK(DS18B20_Scan(&bus) == 1U); + + /* Датчик отвечает, но регистры не меняет: обмен состоялся, значение нет. */ + g_bus.devices[0].accept_writes = 0U; + DS18B20_Sensor *sensor = by_device(&bus, 0U); + CHECK(DS18B20_WriteScratchpad(sensor, 0x77U, 0x88U, 0x3FU, 0U) + == DS18B20_WRITE_REJECTED); + /* Кэш обновлён тем, что реально лежит в датчике. */ + CHECK(sensor->user_byte1 == 0x4BU); + CHECK(sensor->config == 0x7FU); +} + +static void test_write_bus_error(void) +{ + setup(1U); + fake_device_init(&g_bus.devices[0], ROM_A, 0x0100, 0U); + + DS18B20_Bus bus; + CHECK(DS18B20_Scan(&bus) == 1U); + + DS18B20_Sensor *sensor = by_device(&bus, 0U); + g_bus.devices[0].present = 0U; + CHECK(DS18B20_WriteScratchpad(sensor, 0x11U, 0x22U, 0x7FU, 0U) + == DS18B20_WRITE_BUS_ERROR); + CHECK((sensor->status & DS18B20_STATUS_NO_PRESENCE) != 0U); +} + +/* -- Разрешение ------------------------------------------------------------ */ + +static void test_resolution(void) +{ + CHECK(DS18B20_ConfigFromResolution(9U) == 0x1FU); + CHECK(DS18B20_ConfigFromResolution(10U) == 0x3FU); + CHECK(DS18B20_ConfigFromResolution(11U) == 0x5FU); + CHECK(DS18B20_ConfigFromResolution(12U) == 0x7FU); + /* Выход за диапазон зажимается, а не заворачивается. */ + CHECK(DS18B20_ConfigFromResolution(0U) == 0x1FU); + CHECK(DS18B20_ConfigFromResolution(200U) == 0x7FU); + + CHECK(DS18B20_ConversionTimeMs(0x1FU) == 94U); + CHECK(DS18B20_ConversionTimeMs(0x3FU) == 188U); + CHECK(DS18B20_ConversionTimeMs(0x5FU) == 375U); + CHECK(DS18B20_ConversionTimeMs(0x7FU) == 750U); +} + +static void test_sensor_limit(void) +{ + /* На шине больше датчиков, чем помещается в кадр SENSOR_DATA. */ + setup(FAKE_MAX_DEVICES); + for (uint8_t i = 0U; i < FAKE_MAX_DEVICES; i++) { + uint8_t rom[7] = { 0x28, 0x10, 0x20, 0x30, 0x40, 0x50, 0x00 }; + rom[6] = (uint8_t)(0xA0U + i); + fake_device_init(&g_bus.devices[i], rom, (int16_t)(0x100 + i), 0U); + } + + DS18B20_Bus bus; + uint8_t count = DS18B20_Scan(&bus); + CHECK(count <= DS18B20_MAX_SENSORS); + CHECK(count == DS18B20_MAX_SENSORS); + CHECK(DS18B20_ReadAll(&bus) == DS18B20_MAX_SENSORS); +} + +int main(void) +{ + test_crc8(); + test_search_finds_all(); + test_search_on_empty_bus(); + test_search_single_device(); + test_scan_filters_family(); + test_read_all(); + test_crc_error_does_not_stop_the_poll(); + test_missing_device_is_reported(); + test_find(); + test_write_scratchpad(); + test_write_rejected(); + test_write_bus_error(); + test_resolution(); + test_sensor_limit(); + + if (g_fail == 0) { + printf("all ds18b20 tests passed\n"); + return 0; + } + printf("%d check(s) failed\n", g_fail); + return 1; +}