Compare commits
6 Commits
codex/tren
...
ds2480
| Author | SHA1 | Date | |
|---|---|---|---|
| 96ce6f8759 | |||
| 20dbc2c721 | |||
| e95338d781 | |||
| daa3466a41 | |||
| c9cf797132 | |||
| de5dc18a43 |
@@ -33,7 +33,9 @@ templates/
|
||||
| [`c/eeprom-ft24c256`](c/eeprom-ft24c256) | EEPROM 24Cxx по I²C с нарезкой записи по страницам | `stdint.h` | две I²C-транзакции, задержка |
|
||||
| [`c/can-sensor`](c/can-sensor) | однокадровые SETCAN SETTINGS для 64-битных ROM | ядро: `stdint.h`; порт F1: CMSIS | callbacks либо готовый bxCAN STM32F1 |
|
||||
| [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порты STM32F103, STM32G431 и STM32G474 в комплекте** |
|
||||
| [`c/ds18b20-ds2480`](c/ds18b20-ds2480) | DS18B20 через DS2480B: поиск ROM, температура, EEPROM, паразитное питание | C99 | UART 9600 8N1, сброс моста, задержка |
|
||||
| [`c/set-protocol`](c/set-protocol) | единое ядро SETProtocol: SET v2, совместимые ProtoCAN/GUI v1, GAS, телеметрия, firmware flow и стабильный host ABI | C99 | COM/SLCAN/SocketCAN/USB/Ethernet или callbacks — **Windows, Android и STM32F4-порты в комплекте** |
|
||||
| [`c/set-protocol/ports/stm32f407-devboard-v1`](c/set-protocol/ports/stm32f407-devboard-v1) | доступ SETGUI к Modbus-регистрам F407 через CAN485 DevBoard_V1 | `pcan_modbus_server`, STM32 HAL CAN | bxCAN FIFO0 и callbacks карты регистров |
|
||||
| [`c/set-protocol/ports/stm32-bxcan`](c/set-protocol/ports/stm32-bxcan) | порт прикладного ProtoCAN для STM32, бывший SETCAN; сохранён API `PROTOCAN_*` | STM32 HAL CAN/RTC/TIM + общее ядро `pcan_id` | classic bxCAN; настройки платы предоставляет прошивка |
|
||||
| [`c/protocan-boot`](c/protocan-boot) | адресная прошивка по ProtoCAN: A/B-слоты, сессия, CRC32, verify и rollback-контракт | C99 | CAN TX, erase/write Flash, boot metadata, проверка образа и reboot |
|
||||
| [`c/rs485-boot`](c/rs485-boot) | прошивка по RS-485 в формате SETGUI v1: потоковый parser, CRC32 и resume | C99 | UART TX/RX, DE, Flash — **порты STM32F103 и STM32G474VET в комплекте** |
|
||||
|
||||
17
c/ds18b20-ds2480/CMakeLists.txt
Normal file
17
c/ds18b20-ds2480/CMakeLists.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(ds18b20_ds2480 C)
|
||||
add_library(ds18b20_ds2480 STATIC ds2480.c ds18b20_ds2480.c)
|
||||
target_include_directories(ds18b20_ds2480 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_compile_features(ds18b20_ds2480 PUBLIC c_std_99)
|
||||
if(MSVC)
|
||||
target_compile_options(ds18b20_ds2480 PRIVATE /W4)
|
||||
else()
|
||||
target_compile_options(ds18b20_ds2480 PRIVATE -Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
option(DS18B20_DS2480_BUILD_TESTS "Build host tests" ON)
|
||||
if(DS18B20_DS2480_BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_executable(test_ds18b20_ds2480 tests/test_ds18b20_ds2480.c)
|
||||
target_link_libraries(test_ds18b20_ds2480 PRIVATE ds18b20_ds2480)
|
||||
add_test(NAME ds18b20_ds2480 COMMAND test_ds18b20_ds2480)
|
||||
endif()
|
||||
145
c/ds18b20-ds2480/README.md
Normal file
145
c/ds18b20-ds2480/README.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# DS18B20 через DS2480B
|
||||
|
||||
Переносимая библиотека C99: поиск DS18B20 на общей шине, температура,
|
||||
разрешение 9–12 бит, TH/TL и сохранение в EEPROM через UART-мост DS2480B.
|
||||
Поддерживаются внешнее и паразитное питание: преобразование и запись EEPROM
|
||||
используют strong pullup, включаемый мостом сразу после последнего бита команды.
|
||||
|
||||
Ядро не зависит от HAL, CMSIS, ОС или существующей GPIO-библиотеки `ds18b20`.
|
||||
Динамической памяти и глобального состояния нет. Каждый UART-мост имеет свой
|
||||
`ds2480`, каждый обход — свой `ds2480_search`.
|
||||
|
||||
```text
|
||||
Приложение → ds18b20_ds2480 → ds2480 → callbacks UART/задержки → платформа
|
||||
```
|
||||
|
||||
## Файлы
|
||||
|
||||
| Файл | Назначение | Зависимости |
|
||||
|---|---|---|
|
||||
| `ds2480.h`, `ds2480.c` | Калибровка, reset, обмен битами/байтами, поиск ROM, CRC, strong pullup | `stdint.h`, callbacks |
|
||||
| `ds18b20_ds2480.h`, `ds18b20_ds2480.c` | Команды термометра, проверка scratchpad, знаковая температура | `ds2480`, `string.h` |
|
||||
| `examples/read_first.c` | Полный цикл для первого найденного DS18B20 | ядро, порт приложения |
|
||||
| `tests/test_ds18b20_ds2480.c` | Модель UART-моста и нескольких устройств 1-Wire | ядро, стандартная библиотека C |
|
||||
|
||||
## Контракт порта
|
||||
|
||||
```c
|
||||
int prepare(void *user);
|
||||
int write(void *user, const uint8_t *data, uint32_t size, uint32_t timeout_ms);
|
||||
int read(void *user, uint8_t *data, uint32_t size, uint32_t timeout_ms);
|
||||
void delay_ms(void *user, uint32_t ms);
|
||||
```
|
||||
|
||||
Первые три callbacks возвращают `0` при успехе, иначе ошибку.
|
||||
`prepare` аппаратно сбрасывает DS2480B или формирует UART BREAK не короче 2 мс,
|
||||
настраивает **9600 бод, 8N1**, выдерживает минимум 2 мс после сброса и очищает RX
|
||||
и ошибки UART. Эта функция должна иметь собственный конечный таймаут.
|
||||
Нельзя просто посылать `C1` работающему мосту вместо сброса.
|
||||
|
||||
`write` передаёт ровно `size` байтов и ждёт окончания передачи; `read` получает
|
||||
ровно `size` байтов. Обе операции ограничены `timeout_ms`; при частичном обмене
|
||||
возвращается ошибка. Приём должен работать уже во время передачи: ответ может
|
||||
появиться до вызова `read`. `delay_ms` не должна возвращаться раньше заданного
|
||||
времени (учтите округление системного тика).
|
||||
|
||||
Управлять одним UART извне одновременно с библиотекой нельзя. В RTOS блокировка
|
||||
нужна на всю операцию верхнего уровня, включая поиск и ожидание преобразования.
|
||||
Тайминги 1-Wire формирует мост, запрещать прерывания на время слотов не требуется.
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
Функции `prepare`, `write`, `read`, `delay_ms` и `uart_context` предоставляет плата.
|
||||
Следующий код располагается внутри функции приложения:
|
||||
|
||||
```c
|
||||
ds2480 bus;
|
||||
ds2480_search search = {{0}, 0, 0};
|
||||
ds2480_port port = {uart_context, prepare, write, read, delay_ms};
|
||||
int16_t raw;
|
||||
ds2480_status status = ds2480_init(&bus, &port, 20);
|
||||
if (status != DS2480_OK) return;
|
||||
status = ds18b20_ds2480_next(&bus, &search);
|
||||
if (status != DS2480_OK) return;
|
||||
status = ds18b20_ds2480_convert(&bus, search.rom);
|
||||
if (status != DS2480_OK) return;
|
||||
status = ds18b20_ds2480_temperature(&bus, search.rom, &raw);
|
||||
if (status != DS2480_OK) return;
|
||||
float celsius = raw / 16.0f;
|
||||
(void)celsius;
|
||||
```
|
||||
|
||||
Вызывайте `next` повторно с тем же курсором до `DS2480_DONE`, сохраняя каждый
|
||||
найденный ROM в памяти приложения. Лимита количества датчиков в ядре нет.
|
||||
Пустая шина возвращает `DS2480_NO_PRESENCE`; обрыв поиска и ошибки CRC не
|
||||
маскируются под успешное завершение. Для нового обхода обнулите курсор.
|
||||
|
||||
`convert(bus, NULL)` одновременно запускает все устройства — допустимо только
|
||||
на шине исключительно с DS18B20. Затем читайте каждое по ROM. Для паразитного
|
||||
питания суммарный ток должен укладываться в возможности моста; при необходимости
|
||||
преобразуйте по одному датчику. Преобразование блокирует вызов на 750 мс плюс
|
||||
UART-обмен. EEPROM использует выдержку 12 мс, её содержимое после перезапуска
|
||||
этой функцией не проверяется. Чтение температуры само преобразование не запускает.
|
||||
Значение 85 °C после включения нельзя отличить от настоящих 85 °C без завершённого
|
||||
преобразования; не считайте его заведомой ошибкой.
|
||||
|
||||
## Ошибки и ограничения
|
||||
|
||||
- `IO` — таймаут/ошибка порта, `PROTOCOL` — неожиданный ответ DS2480B.
|
||||
После них экземпляр не готов к работе: повторите `ds2480_init`.
|
||||
- `SHORT` — короткое замыкание, `NO_PRESENCE` — нет presence.
|
||||
- `CRC` — неверная контрольная сумма, `DATA` — недопустимые данные или
|
||||
неподтверждённая запись. Выход температуры/scratchpad при ошибке не меняется.
|
||||
- При ошибке во время strong pullup ядро вызывает `prepare`, чтобы прекратить
|
||||
импульс, и остаётся неготовым. При отказе самого порта снятие питания гарантировать
|
||||
невозможно; восстановление UART/моста остаётся задачей приложения.
|
||||
|
||||
Используется стандартная скорость 1-Wire и только Command Mode: байт передаётся
|
||||
восьмью командами Single Bit. Поэтому любые значения, включая `E3`, передаются
|
||||
без экранирования. Это простая реализация для опроса температуры; повышенные
|
||||
скорости UART, Overdrive и Search Accelerator пока не реализованы. Параметры
|
||||
таймингов длинной линии остаются заводскими. Аппаратная проверка обязательна
|
||||
для выбранной топологии/нагрузки. Протокол проверен по документации **DS2480B**;
|
||||
старые ревизии DS2480 без суффикса B отдельно не проверялись.
|
||||
|
||||
## Подключение и тесты
|
||||
|
||||
Добавьте `ds2480.c`, `ds18b20_ds2480.c` и путь к заголовкам в сборку прошивки.
|
||||
Номер UART, GPIO и библиотеку платформы выбирает приложение. Готовый порт
|
||||
для конкретной платы пока не входит в библиотеку.
|
||||
|
||||
```cmake
|
||||
set(DS18B20_DS2480_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(third_party/templates/c/ds18b20-ds2480)
|
||||
target_link_libraries(firmware PRIVATE ds18b20_ds2480)
|
||||
```
|
||||
|
||||
Отдельная сборка хостовых тестов:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build
|
||||
cmake --build build --config Debug
|
||||
ctest --test-dir build -C Debug --output-on-failure
|
||||
```
|
||||
|
||||
Без CMake, из каталога библиотеки:
|
||||
|
||||
```sh
|
||||
clang -std=c99 -Wall -Wextra -Wpedantic -Werror -I . ds2480.c ds18b20_ds2480.c tests/test_ds18b20_ds2480.c -o build/test.exe
|
||||
./build/test.exe
|
||||
```
|
||||
|
||||
Тесты моделируют калибровку без ответа, несколько ROM с развилками, другие
|
||||
семейства, адресацию, все разрешения, отрицательную температуру, CRC, пустую
|
||||
и замкнутую шину, ошибки транспорта, strong pullup и восстановление после ошибок.
|
||||
Это программная модель; испытания на физическом DS2480B пока не проводились.
|
||||
|
||||
## Использование
|
||||
|
||||
Добавлена в сабмодуль `templates` проекта `john103C6T6NewVer`, ветка `ds2480`.
|
||||
В рабочую прошивку ещё не включена: для этого требуется порт выбранного UART.
|
||||
|
||||
## Источники
|
||||
|
||||
- [DS2480B datasheet, таблицы команд и ответов](https://www.analog.com/media/en/technical-documentation/data-sheets/ds2480b.pdf)
|
||||
- [DS18B20 datasheet, команды, питание и scratchpad](https://www.analog.com/media/en/technical-documentation/data-sheets/ds18b20.pdf)
|
||||
108
c/ds18b20-ds2480/ds18b20_ds2480.c
Normal file
108
c/ds18b20-ds2480/ds18b20_ds2480.c
Normal file
@@ -0,0 +1,108 @@
|
||||
#include "ds18b20_ds2480.h"
|
||||
#include <string.h>
|
||||
|
||||
static ds2480_status write_byte(ds2480 *bus, uint8_t value)
|
||||
{
|
||||
uint8_t echoed;
|
||||
ds2480_status status = ds2480_byte(bus, value, &echoed);
|
||||
if (status != DS2480_OK) return status;
|
||||
return echoed == value ? DS2480_OK : DS2480_DATA;
|
||||
}
|
||||
|
||||
static ds2480_status select_rom(ds2480 *bus, const uint8_t *rom)
|
||||
{
|
||||
uint8_t i;
|
||||
ds2480_status status;
|
||||
if (rom && (rom[0] != 0x28 || ds2480_crc8(rom, 8))) return DS2480_ARGUMENT;
|
||||
status = ds2480_reset(bus);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = write_byte(bus, rom ? 0x55 : 0xCC);
|
||||
if (status != DS2480_OK || !rom) return status;
|
||||
for (i = 0; i < 8; ++i) {
|
||||
status = write_byte(bus, rom[i]);
|
||||
if (status != DS2480_OK) return status;
|
||||
}
|
||||
return DS2480_OK;
|
||||
}
|
||||
|
||||
ds2480_status ds18b20_ds2480_next(ds2480 *bus, ds2480_search *search)
|
||||
{
|
||||
ds2480_status status;
|
||||
do {
|
||||
status = ds2480_search_next(bus, search);
|
||||
if (status != DS2480_OK) return status;
|
||||
} while (search->rom[0] != 0x28);
|
||||
return DS2480_OK;
|
||||
}
|
||||
|
||||
ds2480_status ds18b20_ds2480_convert(ds2480 *bus, const uint8_t rom[8])
|
||||
{
|
||||
ds2480_status status = select_rom(bus, rom);
|
||||
if (status != DS2480_OK) return status;
|
||||
return ds2480_power_byte(bus, 0x44, 750);
|
||||
}
|
||||
|
||||
ds2480_status ds18b20_ds2480_read(ds2480 *bus, const uint8_t rom[8], uint8_t scratchpad[9])
|
||||
{
|
||||
uint8_t data[9], i;
|
||||
ds2480_status status;
|
||||
if (!rom || !scratchpad) return DS2480_ARGUMENT;
|
||||
status = select_rom(bus, rom);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = write_byte(bus, 0xBE);
|
||||
if (status != DS2480_OK) return status;
|
||||
for (i = 0; i < 9; ++i) {
|
||||
status = ds2480_byte(bus, 0xFF, &data[i]);
|
||||
if (status != DS2480_OK) return status;
|
||||
}
|
||||
if (ds2480_crc8(data, 9)) return DS2480_CRC;
|
||||
/* All-zero data has valid CRC but cannot be a DS18B20 scratchpad. */
|
||||
if ((data[4] & 0x9F) != 0x1F) return DS2480_DATA;
|
||||
memcpy(scratchpad, data, sizeof data);
|
||||
return DS2480_OK;
|
||||
}
|
||||
|
||||
ds2480_status ds18b20_ds2480_temperature(ds2480 *bus, const uint8_t rom[8], int16_t *raw)
|
||||
{
|
||||
uint8_t data[9], undefined;
|
||||
uint16_t value;
|
||||
int32_t signed_value;
|
||||
ds2480_status status;
|
||||
if (!raw) return DS2480_ARGUMENT;
|
||||
status = ds18b20_ds2480_read(bus, rom, data);
|
||||
if (status != DS2480_OK) return status;
|
||||
undefined = (uint8_t)(3 - ((data[4] >> 5) & 3));
|
||||
value = (uint16_t)((uint16_t)data[0] | ((uint16_t)data[1] << 8));
|
||||
value &= (uint16_t)~((1U << undefined) - 1U);
|
||||
signed_value = value & 0x8000 ? (int32_t)value - 65536 : (int32_t)value;
|
||||
if (signed_value < -55 * 16 || signed_value > 125 * 16) return DS2480_DATA;
|
||||
*raw = (int16_t)signed_value;
|
||||
return DS2480_OK;
|
||||
}
|
||||
|
||||
ds2480_status ds18b20_ds2480_configure(ds2480 *bus, const uint8_t rom[8],
|
||||
int8_t th, int8_t tl, uint8_t resolution, uint8_t save)
|
||||
{
|
||||
uint8_t data[9], config;
|
||||
ds2480_status status;
|
||||
if (!rom || resolution < 9 || resolution > 12) return DS2480_ARGUMENT;
|
||||
config = (uint8_t)(0x1F | ((resolution - 9) << 5));
|
||||
status = select_rom(bus, rom);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = write_byte(bus, 0x4E);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = write_byte(bus, (uint8_t)th);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = write_byte(bus, (uint8_t)tl);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = write_byte(bus, config);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = ds18b20_ds2480_read(bus, rom, data);
|
||||
if (status != DS2480_OK) return status;
|
||||
if (data[2] != (uint8_t)th || data[3] != (uint8_t)tl || data[4] != config)
|
||||
return DS2480_DATA;
|
||||
if (!save) return DS2480_OK;
|
||||
status = select_rom(bus, rom);
|
||||
if (status != DS2480_OK) return status;
|
||||
return ds2480_power_byte(bus, 0x48, 12);
|
||||
}
|
||||
32
c/ds18b20-ds2480/ds18b20_ds2480.h
Normal file
32
c/ds18b20-ds2480/ds18b20_ds2480.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#ifndef DS18B20_DS2480_H
|
||||
#define DS18B20_DS2480_H
|
||||
#include "ds2480.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Find next DS18B20, skipping other families; same cursor rules as bridge. */
|
||||
ds2480_status ds18b20_ds2480_next(ds2480 *bus, ds2480_search *search);
|
||||
/** Start conversion and hold strong pullup for 750 ms (all resolutions).
|
||||
* rom=NULL broadcasts: use only on a bus containing exclusively DS18B20.
|
||||
* Supports external and parasite power within the bridge's current budget.
|
||||
*/
|
||||
ds2480_status ds18b20_ds2480_convert(ds2480 *bus, const uint8_t rom[8]);
|
||||
/** Read and validate all 9 scratchpad bytes. Output is unchanged on error.
|
||||
* rom is mandatory, must have family 0x28 and valid CRC.
|
||||
*/
|
||||
ds2480_status ds18b20_ds2480_read(ds2480 *bus, const uint8_t rom[8], uint8_t scratchpad[9]);
|
||||
/** Read last conversion in signed 1/16 Celsius units; masks undefined bits
|
||||
* for 9/10/11-bit resolution. Call convert first: power-on 85 C is ambiguous.
|
||||
* Output is unchanged on error. Does not start a conversion itself.
|
||||
*/
|
||||
ds2480_status ds18b20_ds2480_temperature(ds2480 *bus, const uint8_t rom[8], int16_t *raw);
|
||||
/** Set TH/TL and resolution (9..12); verify by reading back.
|
||||
* save!=0 copies to EEPROM with 12 ms strong pullup; avoid frequent writes.
|
||||
*/
|
||||
ds2480_status ds18b20_ds2480_configure(ds2480 *bus, const uint8_t rom[8],
|
||||
int8_t th, int8_t tl, uint8_t resolution, uint8_t save);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
164
c/ds18b20-ds2480/ds2480.c
Normal file
164
c/ds18b20-ds2480/ds2480.c
Normal file
@@ -0,0 +1,164 @@
|
||||
#include "ds2480.h"
|
||||
|
||||
static ds2480_status fault(ds2480 *bus, ds2480_status status)
|
||||
{
|
||||
bus->ready = 0;
|
||||
return status;
|
||||
}
|
||||
|
||||
static ds2480_status exchange(ds2480 *bus, uint8_t command, uint8_t *reply)
|
||||
{
|
||||
if (!bus || !reply) return DS2480_ARGUMENT;
|
||||
if (!bus->ready) return DS2480_NOT_READY;
|
||||
if (bus->port.write(bus->port.user, &command, 1, bus->timeout_ms) ||
|
||||
bus->port.read(bus->port.user, reply, 1, bus->timeout_ms))
|
||||
return fault(bus, DS2480_IO);
|
||||
return DS2480_OK;
|
||||
}
|
||||
|
||||
ds2480_status ds2480_init(ds2480 *bus, const ds2480_port *port, uint32_t timeout_ms)
|
||||
{
|
||||
uint8_t calibration = 0xC1, reply;
|
||||
ds2480_status status;
|
||||
ds2480_port copy;
|
||||
if (!bus) return DS2480_ARGUMENT;
|
||||
if (!port || !port->prepare || !port->write || !port->read ||
|
||||
!port->delay_ms || !timeout_ms) {
|
||||
bus->ready = 0;
|
||||
return DS2480_ARGUMENT;
|
||||
}
|
||||
copy = *port; /* Also permit reinitialization with &bus->port. */
|
||||
bus->ready = 0;
|
||||
bus->port = copy;
|
||||
bus->timeout_ms = timeout_ms;
|
||||
if (copy.prepare(copy.user) ||
|
||||
copy.write(copy.user, &calibration, 1, timeout_ms)) return DS2480_IO;
|
||||
/* First C1 calibrates only: there is NO response byte. */
|
||||
copy.delay_ms(copy.user, 2);
|
||||
bus->ready = 1;
|
||||
/* Strong pullup duration = infinite, command-mode operations only. */
|
||||
status = exchange(bus, 0x3F, &reply);
|
||||
if (status != DS2480_OK) return status;
|
||||
if (reply != 0x3E) return fault(bus, DS2480_PROTOCOL);
|
||||
return DS2480_OK;
|
||||
}
|
||||
|
||||
ds2480_status ds2480_reset(ds2480 *bus)
|
||||
{
|
||||
uint8_t reply;
|
||||
ds2480_status status = exchange(bus, 0xC1, &reply);
|
||||
if (status != DS2480_OK) return status;
|
||||
/* DS2480B revision pattern from table 2; bit 5 is unspecified. */
|
||||
if ((reply & 0xDC) != 0xCC) return fault(bus, DS2480_PROTOCOL);
|
||||
switch (reply & 3) {
|
||||
case 0: return DS2480_SHORT;
|
||||
case 3: return DS2480_NO_PRESENCE;
|
||||
default: return DS2480_OK; /* Ordinary or alarming presence. */
|
||||
}
|
||||
}
|
||||
|
||||
static ds2480_status slot(ds2480 *bus, uint8_t bit, uint8_t power, uint8_t *received)
|
||||
{
|
||||
uint8_t reply, command = (uint8_t)(0x81 | (bit ? 0x10 : 0) | (power ? 2 : 0));
|
||||
ds2480_status status = exchange(bus, command, &reply);
|
||||
if (status != DS2480_OK) return status;
|
||||
if ((reply & 0xFC) != (command & 0xFC) ||
|
||||
((reply & 3) != 0 && (reply & 3) != 3))
|
||||
return fault(bus, DS2480_PROTOCOL);
|
||||
*received = reply & 1;
|
||||
return DS2480_OK;
|
||||
}
|
||||
|
||||
ds2480_status ds2480_bit(ds2480 *bus, uint8_t bit, uint8_t *received)
|
||||
{
|
||||
if (!received) return DS2480_ARGUMENT;
|
||||
return slot(bus, bit, 0, received);
|
||||
}
|
||||
|
||||
ds2480_status ds2480_byte(ds2480 *bus, uint8_t value, uint8_t *received)
|
||||
{
|
||||
uint8_t i, bit, result = 0;
|
||||
ds2480_status status;
|
||||
if (!received) return DS2480_ARGUMENT;
|
||||
for (i = 0; i < 8; ++i) {
|
||||
status = ds2480_bit(bus, (uint8_t)((value >> i) & 1), &bit);
|
||||
if (status != DS2480_OK) return status;
|
||||
result |= (uint8_t)(bit << i);
|
||||
}
|
||||
*received = result;
|
||||
return DS2480_OK;
|
||||
}
|
||||
|
||||
ds2480_status ds2480_power_byte(ds2480 *bus, uint8_t value, uint32_t hold_ms)
|
||||
{
|
||||
uint8_t i, bit, reply, echoed = 0;
|
||||
ds2480_status status;
|
||||
if (!bus || !hold_ms || hold_ms > 1000) return DS2480_ARGUMENT;
|
||||
for (i = 0; i < 8; ++i) {
|
||||
status = slot(bus, (uint8_t)((value >> i) & 1), (uint8_t)(i == 7), &bit);
|
||||
if (status != DS2480_OK) {
|
||||
/* A lost reply may leave an infinite pulse active: reset hardware. */
|
||||
if (i == 7 && !bus->ready) (void)bus->port.prepare(bus->port.user);
|
||||
return status;
|
||||
}
|
||||
echoed |= (uint8_t)(bit << i);
|
||||
}
|
||||
bus->port.delay_ms(bus->port.user, hold_ms);
|
||||
status = exchange(bus, 0xF1, &reply);
|
||||
if (status == DS2480_OK && reply != 0xEC && reply != 0xEF)
|
||||
status = fault(bus, DS2480_PROTOCOL);
|
||||
if (status != DS2480_OK) (void)bus->port.prepare(bus->port.user);
|
||||
if (status == DS2480_OK && echoed != value) return DS2480_DATA;
|
||||
return status;
|
||||
}
|
||||
|
||||
uint8_t ds2480_crc8(const uint8_t *data, uint32_t size)
|
||||
{
|
||||
uint8_t crc = 0, bit;
|
||||
uint32_t i;
|
||||
for (i = 0; i < size; ++i) {
|
||||
crc ^= data[i];
|
||||
for (bit = 0; bit < 8; ++bit)
|
||||
crc = (uint8_t)((crc >> 1) ^ ((crc & 1) ? 0x8C : 0));
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
ds2480_status ds2480_search_next(ds2480 *bus, ds2480_search *search)
|
||||
{
|
||||
ds2480_search next;
|
||||
ds2480_status status;
|
||||
uint8_t pos, a, b, direction, ignored, last_zero = 0;
|
||||
if (!bus || !search || search->discrepancy > 64) return DS2480_ARGUMENT;
|
||||
if (search->done) return DS2480_DONE;
|
||||
next = *search;
|
||||
status = ds2480_reset(bus);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = ds2480_byte(bus, 0xF0, &ignored);
|
||||
if (status != DS2480_OK) return status;
|
||||
for (pos = 1; pos <= 64; ++pos) {
|
||||
uint8_t index = (uint8_t)((pos - 1) / 8);
|
||||
uint8_t mask = (uint8_t)(1U << ((pos - 1) % 8));
|
||||
status = ds2480_bit(bus, 1, &a);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = ds2480_bit(bus, 1, &b);
|
||||
if (status != DS2480_OK) return status;
|
||||
if (a && b) return DS2480_DATA; /* Devices disappeared mid-search. */
|
||||
if (a != b) direction = a;
|
||||
else {
|
||||
direction = pos < search->discrepancy ? (uint8_t)!!(search->rom[index] & mask)
|
||||
: (uint8_t)(pos == search->discrepancy);
|
||||
if (!direction) last_zero = pos;
|
||||
}
|
||||
if (direction) next.rom[index] |= mask;
|
||||
else next.rom[index] &= (uint8_t)~mask;
|
||||
status = ds2480_bit(bus, direction, &ignored);
|
||||
if (status != DS2480_OK) return status;
|
||||
}
|
||||
if (!next.rom[0]) return DS2480_DATA;
|
||||
if (ds2480_crc8(next.rom, 8)) return DS2480_CRC;
|
||||
next.discrepancy = last_zero;
|
||||
next.done = (uint8_t)(last_zero == 0);
|
||||
*search = next;
|
||||
return DS2480_OK;
|
||||
}
|
||||
69
c/ds18b20-ds2480/ds2480.h
Normal file
69
c/ds18b20-ds2480/ds2480.h
Normal file
@@ -0,0 +1,69 @@
|
||||
#ifndef DS2480_H
|
||||
#define DS2480_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
DS2480_OK = 0, DS2480_DONE, DS2480_NO_PRESENCE, DS2480_SHORT,
|
||||
DS2480_IO, DS2480_PROTOCOL, DS2480_CRC, DS2480_ARGUMENT,
|
||||
DS2480_NOT_READY, DS2480_DATA
|
||||
} ds2480_status;
|
||||
|
||||
/** Blocking UART callbacks: 0 = success, nonzero = error/timeout.
|
||||
* prepare resets the bridge (BREAK >= 2 ms or hardware reset), sets UART
|
||||
* to 9600 8N1, waits >= 2 ms after reset, clears RX/errors before returning.
|
||||
* write waits for physical TX completion; read receives exactly size bytes.
|
||||
* Both enforce timeout_ms. RX must remain enabled while transmitting.
|
||||
* delay_ms waits AT LEAST the requested duration. No callback may reenter.
|
||||
*/
|
||||
typedef struct {
|
||||
void *user;
|
||||
int (*prepare)(void *user);
|
||||
int (*write)(void *user, const uint8_t *data, uint32_t size, uint32_t timeout_ms);
|
||||
int (*read)(void *user, uint8_t *data, uint32_t size, uint32_t timeout_ms);
|
||||
void (*delay_ms)(void *user, uint32_t ms);
|
||||
} ds2480_port;
|
||||
|
||||
/** One instance per UART/bridge; serialize access for the whole operation. */
|
||||
typedef struct {
|
||||
ds2480_port port;
|
||||
uint32_t timeout_ms;
|
||||
uint8_t ready;
|
||||
} ds2480;
|
||||
|
||||
/** Independent ROM search cursor. Zero-initialize before each enumeration. */
|
||||
typedef struct {
|
||||
uint8_t rom[8];
|
||||
uint8_t discrepancy;
|
||||
uint8_t done;
|
||||
} ds2480_search;
|
||||
|
||||
/** Reset/calibrate DS2480B and verify configuration. Empty bus is allowed.
|
||||
* On IO/protocol errors call init again before further transactions.
|
||||
*/
|
||||
ds2480_status ds2480_init(ds2480 *bus, const ds2480_port *port, uint32_t timeout_ms);
|
||||
/** Standard-speed reset; distinguish short, empty bus and transport failure. */
|
||||
ds2480_status ds2480_reset(ds2480 *bus);
|
||||
/** Exchange one slot (write 1 to read); received must be non-NULL. */
|
||||
ds2480_status ds2480_bit(ds2480 *bus, uint8_t bit, uint8_t *received);
|
||||
/** Exchange a byte, LSB first, in command mode (eight UART transactions). */
|
||||
ds2480_status ds2480_byte(ds2480 *bus, uint8_t value, uint8_t *received);
|
||||
/** Write a byte and start strong pullup immediately after its last slot.
|
||||
* Blocks for hold_ms (1..1000), then terminates the pulse and consumes reply.
|
||||
*/
|
||||
ds2480_status ds2480_power_byte(ds2480 *bus, uint8_t value, uint32_t hold_ms);
|
||||
/** CRC8 Dallas/Maxim. data must address size bytes (NULL allowed for size=0). */
|
||||
uint8_t ds2480_crc8(const uint8_t *data, uint32_t size);
|
||||
/** Search ALL families. OK yields ROM with valid CRC; DONE ends enumeration.
|
||||
* Cursor changes only on success. After an error retry or zero it to restart.
|
||||
*/
|
||||
ds2480_status ds2480_search_next(ds2480 *bus, ds2480_search *search);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
19
c/ds18b20-ds2480/examples/read_first.c
Normal file
19
c/ds18b20-ds2480/examples/read_first.c
Normal file
@@ -0,0 +1,19 @@
|
||||
#include "ds18b20_ds2480.h"
|
||||
|
||||
/* board_port supplies the four callbacks documented in ds2480.h.
|
||||
* Blocking example: includes bridge reset, scan and 750 ms conversion.
|
||||
*/
|
||||
ds2480_status ds18b20_example_read_first(const ds2480_port *board_port, int16_t *raw)
|
||||
{
|
||||
ds2480 bus;
|
||||
ds2480_search search = {{0}, 0, 0};
|
||||
ds2480_status status;
|
||||
if (!raw) return DS2480_ARGUMENT;
|
||||
status = ds2480_init(&bus, board_port, 20);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = ds18b20_ds2480_next(&bus, &search);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = ds18b20_ds2480_convert(&bus, search.rom);
|
||||
if (status != DS2480_OK) return status;
|
||||
return ds18b20_ds2480_temperature(&bus, search.rom, raw);
|
||||
}
|
||||
228
c/ds18b20-ds2480/tests/test_ds18b20_ds2480.c
Normal file
228
c/ds18b20-ds2480/tests/test_ds18b20_ds2480.c
Normal file
@@ -0,0 +1,228 @@
|
||||
#include "ds18b20_ds2480.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define CHECK(x) do { if (!(x)) { fprintf(stderr, "line %d: %s\n", __LINE__, #x); exit(1); } } while (0)
|
||||
|
||||
enum { ROM_COMMAND, MATCH, FUNCTION, SEARCH, READ, CONFIG };
|
||||
typedef struct {
|
||||
uint8_t rom[3][8], scratch[9];
|
||||
unsigned count, active, state, phase, position, bits, value, match_index;
|
||||
unsigned prepared, calibrated, pending, reply, pulse, hold, convert, copy;
|
||||
unsigned fail_read, fail_write, fail_prepare, corrupt_reply, reset_reply;
|
||||
unsigned fail_power, fail_stop, reject_config;
|
||||
} fake;
|
||||
|
||||
static void crc_scratch(fake *f) { f->scratch[8] = ds2480_crc8(f->scratch, 8); }
|
||||
|
||||
static void setup(fake *f)
|
||||
{
|
||||
unsigned i;
|
||||
static const uint8_t scratch[9] = {0x91, 0x01, 0x4B, 0x46, 0x7F, 0xFF, 0x0C, 0x10, 0x70};
|
||||
memset(f, 0, sizeof *f);
|
||||
f->count = 3;
|
||||
f->reset_reply = 0xCD;
|
||||
memcpy(f->scratch, scratch, 9);
|
||||
for (i = 0; i < 3; ++i) {
|
||||
f->rom[i][0] = i == 2 ? 0x10 : 0x28;
|
||||
f->rom[i][1] = (uint8_t)(i + 1);
|
||||
f->rom[i][7] = ds2480_crc8(f->rom[i], 7);
|
||||
}
|
||||
}
|
||||
|
||||
static void byte_in(fake *f, uint8_t byte)
|
||||
{
|
||||
unsigned i;
|
||||
if (f->state == ROM_COMMAND) {
|
||||
if (byte == 0xF0) { f->state = SEARCH; f->position = f->phase = 0; }
|
||||
else if (byte == 0x55) { f->state = MATCH; f->match_index = 0; }
|
||||
else { CHECK(byte == 0xCC); f->state = FUNCTION; }
|
||||
} else if (f->state == MATCH) {
|
||||
for (i = 0; i < f->count; ++i)
|
||||
if (f->rom[i][f->match_index] != byte) f->active &= ~(1U << i);
|
||||
if (++f->match_index == 8) f->state = FUNCTION;
|
||||
} else if (f->state == FUNCTION) {
|
||||
CHECK(f->active != 0);
|
||||
switch (byte) {
|
||||
case 0xBE: f->state = READ; f->position = 0; break;
|
||||
case 0x4E: f->state = CONFIG; f->position = 2; break;
|
||||
case 0x44: ++f->convert; break;
|
||||
case 0x48: ++f->copy; break;
|
||||
default: CHECK(0);
|
||||
}
|
||||
} else {
|
||||
CHECK(f->state == CONFIG);
|
||||
if (!f->reject_config) f->scratch[f->position] = byte;
|
||||
if (++f->position == 5) { crc_scratch(f); f->state = FUNCTION; }
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t wire_bit(fake *f, uint8_t bit)
|
||||
{
|
||||
unsigned i, result = 1;
|
||||
if (f->state == SEARCH) {
|
||||
for (i = 0; i < f->count; ++i) {
|
||||
unsigned v = (f->rom[i][f->position / 8] >> (f->position % 8)) & 1;
|
||||
if (!(f->active & (1U << i))) continue;
|
||||
if (f->phase < 2) result &= f->phase ? !v : v;
|
||||
else if (v != bit) f->active &= ~(1U << i);
|
||||
}
|
||||
if (++f->phase == 3) { f->phase = 0; ++f->position; }
|
||||
return (uint8_t)result;
|
||||
}
|
||||
if (f->state == READ) {
|
||||
CHECK(f->position < 72 && bit == 1);
|
||||
result = (f->scratch[f->position / 8] >> (f->position % 8)) & 1;
|
||||
++f->position;
|
||||
return (uint8_t)result;
|
||||
}
|
||||
f->value |= (unsigned)bit << f->bits;
|
||||
if (++f->bits == 8) {
|
||||
uint8_t byte = (uint8_t)f->value;
|
||||
f->bits = f->value = 0;
|
||||
byte_in(f, byte);
|
||||
}
|
||||
return bit;
|
||||
}
|
||||
|
||||
static int prepare(void *user)
|
||||
{
|
||||
fake *f = user;
|
||||
++f->prepared;
|
||||
f->pending = f->pulse = f->calibrated = 0;
|
||||
return (int)f->fail_prepare;
|
||||
}
|
||||
|
||||
static int transmit(void *user, const uint8_t *data, uint32_t size, uint32_t timeout)
|
||||
{
|
||||
fake *f = user;
|
||||
uint8_t command = *data, bit;
|
||||
CHECK(size == 1 && timeout == 20 && !f->pending);
|
||||
if (f->fail_write) return -1;
|
||||
if (!f->calibrated) { CHECK(command == 0xC1); f->calibrated = 1; return 0; }
|
||||
CHECK(!f->pulse || command == 0xF1);
|
||||
if (command == 0x3F) f->reply = 0x3E;
|
||||
else if (command == 0xC1) {
|
||||
f->reply = f->count ? f->reset_reply : 0xCF;
|
||||
f->state = ROM_COMMAND; f->bits = f->value = 0;
|
||||
f->active = (1U << f->count) - 1;
|
||||
} else if (command == 0xF1) {
|
||||
CHECK(f->pulse && (f->hold == 750 || f->hold == 12));
|
||||
f->pulse = 0; f->reply = 0xEC;
|
||||
if (f->fail_stop) f->fail_read = 1;
|
||||
} else {
|
||||
CHECK(command == 0x81 || command == 0x91 || command == 0x83 || command == 0x93);
|
||||
bit = wire_bit(f, (uint8_t)((command >> 4) & 1));
|
||||
f->reply = (command & 0xFC) | (bit ? 3U : 0U);
|
||||
if (command & 2) {
|
||||
CHECK(f->bits == 0 && (f->convert || f->copy));
|
||||
f->pulse = 1; f->hold = 0;
|
||||
if (f->fail_power) f->fail_read = 1;
|
||||
}
|
||||
}
|
||||
f->pending = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int receive(void *user, uint8_t *data, uint32_t size, uint32_t timeout)
|
||||
{
|
||||
fake *f = user;
|
||||
CHECK(size == 1 && timeout == 20 && f->pending);
|
||||
if (f->fail_read) return -1;
|
||||
*data = (uint8_t)(f->corrupt_reply ? 0 : f->reply);
|
||||
f->pending = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void delay(void *user, uint32_t ms)
|
||||
{
|
||||
fake *f = user;
|
||||
if (f->pulse) f->hold += ms;
|
||||
else CHECK(ms == 2);
|
||||
}
|
||||
|
||||
static void init(fake *f, ds2480 *bus)
|
||||
{
|
||||
ds2480_port port = { f, prepare, transmit, receive, delay };
|
||||
CHECK(ds2480_init(bus, &port, 20) == DS2480_OK);
|
||||
CHECK(!f->pending);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
fake f, other;
|
||||
ds2480 bus, second;
|
||||
ds2480_search search = {{0}, 0, 0}, saved;
|
||||
uint8_t bit, scratch[9], rom[8];
|
||||
int16_t raw = 999;
|
||||
unsigned found = 0, i;
|
||||
setup(&f); init(&f, &bus);
|
||||
CHECK(ds2480_crc8(f.scratch, 9) == 0); /* Datasheet vector, not generated. */
|
||||
while (ds18b20_ds2480_next(&bus, &search) == DS2480_OK) {
|
||||
if (!memcmp(search.rom, f.rom[0], 8)) found |= 1;
|
||||
else if (!memcmp(search.rom, f.rom[1], 8)) found |= 2;
|
||||
else CHECK(0);
|
||||
}
|
||||
CHECK(found == 3 && search.done);
|
||||
CHECK(ds18b20_ds2480_next(&bus, &search) == DS2480_DONE);
|
||||
CHECK(ds18b20_ds2480_convert(&bus, NULL) == DS2480_OK);
|
||||
CHECK(f.convert == 1 && f.hold == 750 && !f.pulse && !f.pending);
|
||||
CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_OK && raw == 401);
|
||||
CHECK(ds18b20_ds2480_convert(&bus, f.rom[1]) == DS2480_OK && f.active == 2);
|
||||
for (i = 9; i <= 12; ++i) {
|
||||
CHECK(ds18b20_ds2480_configure(&bus, f.rom[0], -5, -20, (uint8_t)i, 1) == DS2480_OK);
|
||||
CHECK(f.hold == 12 && !f.pulse && !f.pending);
|
||||
f.scratch[0] = 0x5F; f.scratch[1] = 0xFF; crc_scratch(&f);
|
||||
CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_OK);
|
||||
CHECK(raw == (i == 9 ? -168 : i == 10 ? -164 : i == 11 ? -162 : -161));
|
||||
}
|
||||
CHECK(f.copy == 4);
|
||||
f.reject_config = 1;
|
||||
CHECK(ds18b20_ds2480_configure(&bus, f.rom[0], 1, 2, 9, 1) == DS2480_DATA && f.copy == 4);
|
||||
f.scratch[8] ^= 1; raw = 999;
|
||||
CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_CRC && raw == 999);
|
||||
memset(f.scratch, 0, 9);
|
||||
CHECK(ds18b20_ds2480_read(&bus, f.rom[0], scratch) == DS2480_DATA);
|
||||
memset(f.scratch, 0xFF, 9);
|
||||
CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_CRC);
|
||||
memcpy(rom, f.rom[0], 8); rom[7] ^= 1;
|
||||
CHECK(ds18b20_ds2480_convert(&bus, rom) == DS2480_ARGUMENT);
|
||||
CHECK(ds18b20_ds2480_read(&bus, NULL, scratch) == DS2480_ARGUMENT);
|
||||
CHECK(ds18b20_ds2480_configure(&bus, f.rom[0], 0, 0, 8, 0) == DS2480_ARGUMENT);
|
||||
CHECK(ds2480_bit(&bus, 1, NULL) == DS2480_ARGUMENT);
|
||||
f.reset_reply = 0xEC; CHECK(ds2480_reset(&bus) == DS2480_SHORT);
|
||||
f.reset_reply = 0xEE; CHECK(ds2480_reset(&bus) == DS2480_OK);
|
||||
f.count = 0; CHECK(ds2480_reset(&bus) == DS2480_NO_PRESENCE);
|
||||
memset(&search, 0, sizeof search);
|
||||
CHECK(ds2480_search_next(&bus, &search) == DS2480_NO_PRESENCE);
|
||||
setup(&f); init(&f, &bus); f.count = 1; f.rom[0][7] ^= 1;
|
||||
saved = search;
|
||||
CHECK(ds2480_search_next(&bus, &search) == DS2480_CRC);
|
||||
CHECK(!memcmp(&search, &saved, sizeof search));
|
||||
f.fail_read = 1; CHECK(ds2480_reset(&bus) == DS2480_IO);
|
||||
CHECK(ds2480_reset(&bus) == DS2480_NOT_READY);
|
||||
setup(&f); init(&f, &bus); f.corrupt_reply = 1;
|
||||
CHECK(ds2480_reset(&bus) == DS2480_PROTOCOL && !bus.ready);
|
||||
setup(&f); init(&f, &bus); CHECK(ds2480_reset(&bus) == DS2480_OK); f.corrupt_reply = 1;
|
||||
CHECK(ds2480_bit(&bus, 1, &bit) == DS2480_PROTOCOL);
|
||||
setup(&f); init(&f, &bus); f.fail_write = 1;
|
||||
CHECK(ds2480_reset(&bus) == DS2480_IO && !bus.ready);
|
||||
setup(&f); init(&f, &bus); f.fail_power = 1;
|
||||
CHECK(ds18b20_ds2480_convert(&bus, NULL) == DS2480_IO);
|
||||
CHECK(!f.pulse && !bus.ready && f.prepared == 2);
|
||||
setup(&f); init(&f, &bus); f.fail_stop = 1;
|
||||
CHECK(ds18b20_ds2480_convert(&bus, NULL) == DS2480_IO);
|
||||
CHECK(!f.pulse && !bus.ready && f.prepared == 2);
|
||||
setup(&f); init(&f, &bus);
|
||||
setup(&other); init(&other, &second);
|
||||
CHECK(ds18b20_ds2480_convert(&second, other.rom[1]) == DS2480_OK);
|
||||
CHECK(f.convert == 0 && other.convert == 1);
|
||||
CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_OK && raw == 401);
|
||||
CHECK(ds2480_init(&bus, &bus.port, 20) == DS2480_OK);
|
||||
f.fail_prepare = 1;
|
||||
CHECK(ds2480_init(&bus, &bus.port, 20) == DS2480_IO && !bus.ready);
|
||||
CHECK(ds2480_init(&bus, NULL, 20) == DS2480_ARGUMENT);
|
||||
puts("DS18B20/DS2480: all host tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -26,6 +26,7 @@ set(SETPROTOCOL_LEGACY_SOURCES
|
||||
src/pcan_gas.c
|
||||
src/pcan_id.c
|
||||
src/pcan_link.c
|
||||
src/pcan_modbus_server.c
|
||||
src/pcan_ring.c
|
||||
)
|
||||
|
||||
@@ -88,6 +89,10 @@ if(SETP_BUILD_TESTS)
|
||||
target_link_libraries(test_transport PRIVATE setprotocol_static)
|
||||
add_test(NAME legacy_transport COMMAND test_transport)
|
||||
|
||||
add_executable(test_modbus_server tests/test_modbus_server.c)
|
||||
target_link_libraries(test_modbus_server PRIVATE setprotocol_static)
|
||||
add_test(NAME protocan_modbus_server COMMAND test_modbus_server)
|
||||
|
||||
add_executable(test_gui tests/test_gui.c)
|
||||
target_link_libraries(test_gui PRIVATE setprotocol_static)
|
||||
add_test(NAME legacy_gui COMMAND test_gui)
|
||||
|
||||
60
c/set-protocol/include/pcan_modbus_server.h
Normal file
60
c/set-protocol/include/pcan_modbus_server.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @file pcan_modbus_server.h
|
||||
* @brief Modbus register windows transported in one classic ProtoCAN frame.
|
||||
*/
|
||||
#ifndef PCAN_MODBUS_SERVER_H
|
||||
#define PCAN_MODBUS_SERVER_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "pcan_frame.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
PCAN_MODBUS_COIL = 0x4U,
|
||||
PCAN_MODBUS_DISCRETE = 0x5U,
|
||||
PCAN_MODBUS_HOLDING = 0x6U,
|
||||
PCAN_MODBUS_INPUT = 0x7U
|
||||
} pcan_modbus_bank_t;
|
||||
|
||||
typedef bool (*pcan_modbus_read_fn)(void *user, pcan_modbus_bank_t bank,
|
||||
uint16_t address, uint16_t *value);
|
||||
typedef bool (*pcan_modbus_write_fn)(void *user, pcan_modbus_bank_t bank,
|
||||
uint16_t address, uint16_t value);
|
||||
|
||||
typedef struct {
|
||||
uint8_t device_type;
|
||||
uint8_t device_id;
|
||||
pcan_modbus_read_fn read;
|
||||
pcan_modbus_write_fn write;
|
||||
void *user;
|
||||
uint32_t requests;
|
||||
uint32_t responses;
|
||||
uint32_t rejected;
|
||||
} pcan_modbus_server_t;
|
||||
|
||||
typedef enum {
|
||||
PCAN_MODBUS_NOT_FOR_US = 0,
|
||||
PCAN_MODBUS_HANDLED_NO_RESPONSE,
|
||||
PCAN_MODBUS_RESPONSE
|
||||
} pcan_modbus_result_t;
|
||||
|
||||
bool pcan_modbus_server_init(pcan_modbus_server_t *server,
|
||||
uint8_t device_type, uint8_t device_id,
|
||||
pcan_modbus_read_fn read,
|
||||
pcan_modbus_write_fn write, void *user);
|
||||
|
||||
/** Zero DLC reads; data writes COIL/HOLDING. Register words are little-endian. */
|
||||
pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server,
|
||||
const pcan_frame_t *request,
|
||||
pcan_frame_t *response);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PCAN_MODBUS_SERVER_H */
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "pcan_gas.h"
|
||||
#include "pcan_id.h"
|
||||
#include "pcan_link.h"
|
||||
#include "pcan_modbus_server.h"
|
||||
#include "pcan_ring.h"
|
||||
|
||||
#endif /* PROTOCAN_TRANSPORT_H */
|
||||
|
||||
22
c/set-protocol/ports/stm32f407-devboard-v1/README.md
Normal file
22
c/set-protocol/ports/stm32f407-devboard-v1/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# STM32F407 + CAN485 DevBoard_V1
|
||||
|
||||
Порт даёт SETGUI доступ к существующим Modbus-регистрам STM32F407 через
|
||||
классический CAN. ПК подключён к USB-UART платы CAN485 DevBoard_V1, команда
|
||||
`T,E` платы отправляет ProtoCAN-кадр, а этот порт отвечает тем же типом кадра.
|
||||
При маршруте `Q3` плата одновременно ретранслирует поток по RS-485.
|
||||
|
||||
Порт не настраивает GPIO, bitrate, фильтры и не запускает CAN: это остаётся за
|
||||
CubeMX-проектом. Необработанные кадры передаются callback-у приложения, поэтому
|
||||
загрузчик ProtoCAN и регистровый сервис могут делить FIFO0.
|
||||
|
||||
Слой приложения предоставляет callback-и чтения и записи. Нулевой DLC означает
|
||||
чтение; данные в кадре `HOLDING`/`COIL` — запись. За кадр передаются до четырёх
|
||||
16-битных регистров или до пятнадцати coils (предел 4-битного `RegCount`).
|
||||
|
||||
```c
|
||||
static set_devboard_v1_stm32f407_t gui_can;
|
||||
set_devboard_v1_stm32f407_init(&gui_can, &hcan, 7, 3,
|
||||
app_read_register, app_write_register, NULL,
|
||||
app_handle_boot_frame, NULL);
|
||||
for (;;) set_devboard_v1_stm32f407_poll(&gui_can);
|
||||
```
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "set_devboard_v1_stm32f407.h"
|
||||
|
||||
static bool transmit(set_devboard_v1_stm32f407_t *port,
|
||||
const pcan_frame_t *frame)
|
||||
{
|
||||
CAN_TxHeaderTypeDef header = {0};
|
||||
uint32_t mailbox = 0U;
|
||||
uint8_t data[8] = {0U};
|
||||
|
||||
header.ExtId = frame->id & 0x1FFFFFFFUL;
|
||||
header.IDE = CAN_ID_EXT;
|
||||
header.RTR = ((frame->flags & PCAN_FLAG_RTR) != 0U) ?
|
||||
CAN_RTR_REMOTE : CAN_RTR_DATA;
|
||||
header.DLC = frame->dlc;
|
||||
header.TransmitGlobalTime = DISABLE;
|
||||
for (uint8_t index = 0U; index < frame->dlc; ++index) {
|
||||
data[index] = frame->data[index];
|
||||
}
|
||||
if (HAL_CAN_AddTxMessage(port->can, &header, data, &mailbox) != HAL_OK) {
|
||||
port->tx_errors++;
|
||||
return false;
|
||||
}
|
||||
port->tx_frames++;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool set_devboard_v1_stm32f407_init(
|
||||
set_devboard_v1_stm32f407_t *port, CAN_HandleTypeDef *can,
|
||||
uint8_t device_type, uint8_t device_id,
|
||||
pcan_modbus_read_fn read, pcan_modbus_write_fn write, void *register_user,
|
||||
set_devboard_v1_unhandled_fn unhandled, void *unhandled_user)
|
||||
{
|
||||
if ((port == NULL) || (can == NULL)) {
|
||||
return false;
|
||||
}
|
||||
port->can = can;
|
||||
port->unhandled = unhandled;
|
||||
port->unhandled_user = unhandled_user;
|
||||
port->rx_frames = 0U;
|
||||
port->tx_frames = 0U;
|
||||
port->tx_errors = 0U;
|
||||
return pcan_modbus_server_init(&port->server, device_type, device_id,
|
||||
read, write, register_user);
|
||||
}
|
||||
|
||||
size_t set_devboard_v1_stm32f407_poll(set_devboard_v1_stm32f407_t *port)
|
||||
{
|
||||
size_t handled = 0U;
|
||||
if ((port == NULL) || (port->can == NULL)) {
|
||||
return 0U;
|
||||
}
|
||||
while (HAL_CAN_GetRxFifoFillLevel(port->can, CAN_RX_FIFO0) != 0U) {
|
||||
CAN_RxHeaderTypeDef header;
|
||||
uint8_t data[8] = {0U};
|
||||
pcan_frame_t request = {0};
|
||||
pcan_frame_t response;
|
||||
pcan_modbus_result_t result;
|
||||
|
||||
if (HAL_CAN_GetRxMessage(port->can, CAN_RX_FIFO0, &header, data) != HAL_OK) {
|
||||
break;
|
||||
}
|
||||
port->rx_frames++;
|
||||
request.id = (header.IDE == CAN_ID_EXT) ? header.ExtId : header.StdId;
|
||||
request.flags = (header.IDE == CAN_ID_EXT) ? PCAN_FLAG_IDE : 0U;
|
||||
if (header.RTR == CAN_RTR_REMOTE) {
|
||||
request.flags |= PCAN_FLAG_RTR;
|
||||
}
|
||||
request.dlc = (header.DLC > 8U) ? 8U : (uint8_t)header.DLC;
|
||||
for (uint8_t index = 0U; index < request.dlc; ++index) {
|
||||
request.data[index] = data[index];
|
||||
}
|
||||
|
||||
result = pcan_modbus_server_handle(&port->server, &request, &response);
|
||||
if (result == PCAN_MODBUS_RESPONSE) {
|
||||
(void)transmit(port, &response);
|
||||
handled++;
|
||||
} else if ((result == PCAN_MODBUS_NOT_FOR_US) &&
|
||||
(port->unhandled != NULL)) {
|
||||
port->unhandled(port->unhandled_user, &header, data);
|
||||
}
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/** STM32F407 bxCAN port for SETGUI through CAN485 DevBoard_V1. */
|
||||
#ifndef SET_DEVBOARD_V1_STM32F407_H
|
||||
#define SET_DEVBOARD_V1_STM32F407_H
|
||||
|
||||
#include "stm32f4xx_hal.h"
|
||||
#include "pcan_modbus_server.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void (*set_devboard_v1_unhandled_fn)(void *user,
|
||||
const CAN_RxHeaderTypeDef *header,
|
||||
const uint8_t data[8]);
|
||||
|
||||
typedef struct {
|
||||
CAN_HandleTypeDef *can;
|
||||
pcan_modbus_server_t server;
|
||||
set_devboard_v1_unhandled_fn unhandled;
|
||||
void *unhandled_user;
|
||||
uint32_t rx_frames;
|
||||
uint32_t tx_frames;
|
||||
uint32_t tx_errors;
|
||||
} set_devboard_v1_stm32f407_t;
|
||||
|
||||
bool set_devboard_v1_stm32f407_init(
|
||||
set_devboard_v1_stm32f407_t *port, CAN_HandleTypeDef *can,
|
||||
uint8_t device_type, uint8_t device_id,
|
||||
pcan_modbus_read_fn read, pcan_modbus_write_fn write, void *register_user,
|
||||
set_devboard_v1_unhandled_fn unhandled, void *unhandled_user);
|
||||
|
||||
size_t set_devboard_v1_stm32f407_poll(set_devboard_v1_stm32f407_t *port);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
144
c/set-protocol/src/pcan_modbus_server.c
Normal file
144
c/set-protocol/src/pcan_modbus_server.c
Normal file
@@ -0,0 +1,144 @@
|
||||
#include "pcan_modbus_server.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "pcan_id.h"
|
||||
|
||||
static bool is_bank(uint8_t value)
|
||||
{
|
||||
return (value >= (uint8_t)PCAN_MODBUS_COIL) &&
|
||||
(value <= (uint8_t)PCAN_MODBUS_INPUT);
|
||||
}
|
||||
|
||||
bool pcan_modbus_server_init(pcan_modbus_server_t *server,
|
||||
uint8_t device_type, uint8_t device_id,
|
||||
pcan_modbus_read_fn read,
|
||||
pcan_modbus_write_fn write, void *user)
|
||||
{
|
||||
if ((server == NULL) || (read == NULL) || (device_type > 7U) ||
|
||||
(device_id > 15U)) {
|
||||
return false;
|
||||
}
|
||||
server->device_type = device_type;
|
||||
server->device_id = device_id;
|
||||
server->read = read;
|
||||
server->write = write;
|
||||
server->user = user;
|
||||
server->requests = 0U;
|
||||
server->responses = 0U;
|
||||
server->rejected = 0U;
|
||||
return true;
|
||||
}
|
||||
|
||||
static pcan_modbus_result_t reject(pcan_modbus_server_t *server)
|
||||
{
|
||||
server->rejected++;
|
||||
return PCAN_MODBUS_HANDLED_NO_RESPONSE;
|
||||
}
|
||||
|
||||
pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server,
|
||||
const pcan_frame_t *request,
|
||||
pcan_frame_t *response)
|
||||
{
|
||||
pcan_id_t id;
|
||||
uint16_t address;
|
||||
uint8_t count;
|
||||
pcan_modbus_bank_t bank;
|
||||
|
||||
if ((server == NULL) || (request == NULL) || (response == NULL) ||
|
||||
((request->flags & PCAN_FLAG_IDE) == 0U)) {
|
||||
return PCAN_MODBUS_NOT_FOR_US;
|
||||
}
|
||||
pcan_id_unpack(request->id, &id);
|
||||
if ((id.route != PCAN_ROUTE_FROM_PM) ||
|
||||
(id.device_type != server->device_type) ||
|
||||
(id.device_id != server->device_id) || !is_bank(id.msg_type)) {
|
||||
return PCAN_MODBUS_NOT_FOR_US;
|
||||
}
|
||||
|
||||
server->requests++;
|
||||
address = (uint16_t)(id.msg_body >> 4U);
|
||||
count = (uint8_t)(id.msg_body & 0x0FU);
|
||||
bank = (pcan_modbus_bank_t)id.msg_type;
|
||||
if (count == 0U) {
|
||||
return reject(server);
|
||||
}
|
||||
|
||||
*response = *request;
|
||||
id.route = PCAN_ROUTE_FROM_DEVICE;
|
||||
response->id = pcan_id_pack(&id);
|
||||
response->flags = PCAN_FLAG_IDE;
|
||||
response->seq = 0U;
|
||||
|
||||
if (request->dlc == 0U) {
|
||||
uint16_t packed = 0U;
|
||||
if ((bank == PCAN_MODBUS_COIL) || (bank == PCAN_MODBUS_DISCRETE)) {
|
||||
if (count > 16U) {
|
||||
return reject(server);
|
||||
}
|
||||
for (uint8_t index = 0U; index < count; ++index) {
|
||||
uint16_t value = 0U;
|
||||
if (!server->read(server->user, bank,
|
||||
(uint16_t)(address + index), &value)) {
|
||||
return reject(server);
|
||||
}
|
||||
if (value != 0U) {
|
||||
packed |= (uint16_t)(1U << index);
|
||||
}
|
||||
}
|
||||
response->dlc = 2U;
|
||||
response->data[0] = (uint8_t)packed;
|
||||
response->data[1] = (uint8_t)(packed >> 8U);
|
||||
} else {
|
||||
if (count > 4U) {
|
||||
return reject(server);
|
||||
}
|
||||
response->dlc = (uint8_t)(count * 2U);
|
||||
for (uint8_t index = 0U; index < count; ++index) {
|
||||
uint16_t value = 0U;
|
||||
if (!server->read(server->user, bank,
|
||||
(uint16_t)(address + index), &value)) {
|
||||
return reject(server);
|
||||
}
|
||||
response->data[index * 2U] = (uint8_t)value;
|
||||
response->data[(index * 2U) + 1U] = (uint8_t)(value >> 8U);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ((bank == PCAN_MODBUS_INPUT) || (bank == PCAN_MODBUS_DISCRETE) ||
|
||||
(server->write == NULL)) {
|
||||
return reject(server);
|
||||
}
|
||||
if (bank == PCAN_MODBUS_COIL) {
|
||||
uint16_t bits;
|
||||
if ((count > 16U) || (request->dlc != 2U)) {
|
||||
return reject(server);
|
||||
}
|
||||
bits = (uint16_t)((uint16_t)request->data[0] |
|
||||
((uint16_t)request->data[1] << 8U));
|
||||
for (uint8_t index = 0U; index < count; ++index) {
|
||||
if (!server->write(server->user, bank,
|
||||
(uint16_t)(address + index),
|
||||
(uint16_t)((bits >> index) & 1U))) {
|
||||
return reject(server);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ((count > 4U) || (request->dlc != (uint8_t)(count * 2U))) {
|
||||
return reject(server);
|
||||
}
|
||||
for (uint8_t index = 0U; index < count; ++index) {
|
||||
uint16_t value = (uint16_t)(request->data[index * 2U] |
|
||||
((uint16_t)request->data[(index * 2U) + 1U] << 8U));
|
||||
if (!server->write(server->user, bank,
|
||||
(uint16_t)(address + index), value)) {
|
||||
return reject(server);
|
||||
}
|
||||
}
|
||||
}
|
||||
response->dlc = request->dlc;
|
||||
}
|
||||
|
||||
server->responses++;
|
||||
return PCAN_MODBUS_RESPONSE;
|
||||
}
|
||||
90
c/set-protocol/tests/test_modbus_server.c
Normal file
90
c/set-protocol/tests/test_modbus_server.c
Normal file
@@ -0,0 +1,90 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "pcan_id.h"
|
||||
#include "pcan_modbus_server.h"
|
||||
|
||||
static uint16_t holding[32];
|
||||
static uint16_t input[32];
|
||||
static uint16_t coils;
|
||||
|
||||
static bool read_word(void *user, pcan_modbus_bank_t bank,
|
||||
uint16_t address, uint16_t *value)
|
||||
{
|
||||
(void)user;
|
||||
if (address >= 32U) return false;
|
||||
if (bank == PCAN_MODBUS_HOLDING) *value = holding[address];
|
||||
else if (bank == PCAN_MODBUS_INPUT) *value = input[address];
|
||||
else if (bank == PCAN_MODBUS_COIL) *value = (uint16_t)((coils >> address) & 1U);
|
||||
else return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool write_word(void *user, pcan_modbus_bank_t bank,
|
||||
uint16_t address, uint16_t value)
|
||||
{
|
||||
(void)user;
|
||||
if (address >= 32U) return false;
|
||||
if (bank == PCAN_MODBUS_HOLDING) holding[address] = value;
|
||||
else if (bank == PCAN_MODBUS_COIL && address < 16U) {
|
||||
if (value) coils |= (uint16_t)(1U << address);
|
||||
else coils &= (uint16_t)~(1U << address);
|
||||
} else return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static pcan_frame_t request(uint8_t type, uint16_t address, uint8_t count)
|
||||
{
|
||||
pcan_id_t id = {0};
|
||||
pcan_frame_t frame = {0};
|
||||
id.priority = PCAN_PRIORITY_STANDARD;
|
||||
id.route = PCAN_ROUTE_FROM_PM;
|
||||
id.device_type = 7U;
|
||||
id.device_id = 3U;
|
||||
id.msg_type = type;
|
||||
id.msg_body = pcan_body_modbus(address, count);
|
||||
frame.id = pcan_id_pack(&id);
|
||||
frame.flags = PCAN_FLAG_IDE;
|
||||
return frame;
|
||||
}
|
||||
|
||||
#define CHECK(x) do { if (!(x)) { \
|
||||
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #x); return 1; \
|
||||
} } while (0)
|
||||
|
||||
int main(void)
|
||||
{
|
||||
pcan_modbus_server_t server;
|
||||
pcan_frame_t req;
|
||||
pcan_frame_t rsp;
|
||||
pcan_id_t id;
|
||||
memset(holding, 0, sizeof(holding));
|
||||
memset(input, 0, sizeof(input));
|
||||
input[10] = 0x1234U;
|
||||
input[11] = 0xABCDU;
|
||||
|
||||
CHECK(pcan_modbus_server_init(&server, 7U, 3U, read_word, write_word, NULL));
|
||||
req = request(PCAN_MODBUS_INPUT, 10U, 2U);
|
||||
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE);
|
||||
CHECK(rsp.dlc == 4U && rsp.data[0] == 0x34U && rsp.data[1] == 0x12U);
|
||||
CHECK(rsp.data[2] == 0xCDU && rsp.data[3] == 0xABU);
|
||||
pcan_id_unpack(rsp.id, &id);
|
||||
CHECK(id.route == PCAN_ROUTE_FROM_DEVICE);
|
||||
|
||||
req = request(PCAN_MODBUS_HOLDING, 3U, 2U);
|
||||
req.dlc = 4U;
|
||||
req.data[0] = 0x22U; req.data[1] = 0x11U;
|
||||
req.data[2] = 0x44U; req.data[3] = 0x33U;
|
||||
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE);
|
||||
CHECK(holding[3] == 0x1122U && holding[4] == 0x3344U);
|
||||
|
||||
req = request(PCAN_MODBUS_COIL, 2U, 3U);
|
||||
req.dlc = 2U; req.data[0] = 0x05U; req.data[1] = 0U;
|
||||
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE);
|
||||
CHECK(coils == 0x0014U);
|
||||
|
||||
req = request(PCAN_MODBUS_INPUT, 0U, 5U);
|
||||
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_HANDLED_NO_RESPONSE);
|
||||
CHECK(server.requests == 4U && server.responses == 3U && server.rejected == 1U);
|
||||
return 0;
|
||||
}
|
||||
@@ -23,7 +23,7 @@ SOURCES = [
|
||||
"set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c", "set_plot.c", "set_trends.c", "set_spectrum.c",
|
||||
"balsam_can.c", "gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c",
|
||||
"pcan_frame.c", "pcan_id.c", "pcan_link.c", "pcan_ring.c",
|
||||
"pcan_gas.c",
|
||||
"pcan_gas.c", "pcan_modbus_server.c",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
379
doc/CAN_FRAME_PARSE_V1_V2.md
Normal file
379
doc/CAN_FRAME_PARSE_V1_V2.md
Normal file
@@ -0,0 +1,379 @@
|
||||
# Разбор CAN-кадров ProtoCAN Boot v1 и SETProtocol v2
|
||||
|
||||
Документ описывает wire-форматы двух протоколов обновления прошивки:
|
||||
|
||||
- **v1** — `templates/c/protocan-boot`, одна команда или 8 байт образа в одном
|
||||
Extended CAN-кадре;
|
||||
- **v2** — `templates/c/set-protocol`, полный кадр SETProtocol разбивается на
|
||||
несколько Extended CAN-кадров.
|
||||
|
||||
Все многобайтные поля payload передаются **little-endian**. CAN ID — 29-битный.
|
||||
Для рабочего кода нужно использовать канонические реализации из `templates`,
|
||||
а приведённый ниже Python-парсер удобен для анализатора, логов и отладки.
|
||||
|
||||
## 1. ProtoCAN Boot v1
|
||||
|
||||
### 1.1. Разметка Extended CAN ID
|
||||
|
||||
```text
|
||||
bits size field
|
||||
28 1 Priority
|
||||
27 1 Route: 0 = host -> device, 1 = device -> host
|
||||
26..24 3 Device Type
|
||||
23..20 4 Device ID
|
||||
19..16 4 Message Type
|
||||
15..0 16 Message Body
|
||||
```
|
||||
|
||||
Формула:
|
||||
|
||||
```text
|
||||
ID = Priority << 28 |
|
||||
Route << 27 |
|
||||
DeviceType << 24 |
|
||||
DeviceID << 20 |
|
||||
MessageType << 16 |
|
||||
MessageBody
|
||||
```
|
||||
|
||||
Типы загрузочных сообщений:
|
||||
|
||||
| Message Type | Имя | Message Body | CAN payload |
|
||||
|---:|---|---|---|
|
||||
| `0x9` | `BOOT_CONTROL` | `SessionID << 8 \| Command` | параметры команды |
|
||||
| `0xA` | `BOOT_DATA_A` | индекс блока | 8 байт слота A |
|
||||
| `0xB` | `BOOT_DATA_B` | индекс блока | 8 байт слота B |
|
||||
| `0xC` | `BOOT_STATUS` | `SessionID << 8 \| Command` | статус и прогресс |
|
||||
| `0xD` | `BOOT_DISCOVERY` | подтип | информация об устройстве |
|
||||
|
||||
Команды `BOOT_CONTROL`:
|
||||
|
||||
| Код | Команда | Payload |
|
||||
|---:|---|---|
|
||||
| `0x01` | `IDENTIFY` | пустой |
|
||||
| `0x02` | `ENTER_BOOT` | пустой |
|
||||
| `0x03` | `BEGIN_IMAGE` | `image_size u32`, `image_crc32 u32` |
|
||||
| `0x04` | `BEGIN_COMPAT` | `product u16`, `hw_min u8`, `hw_max u8`, `version u32` |
|
||||
| `0x05` | `ERASE` | пустой |
|
||||
| `0x06` | `VERIFY` | пустой |
|
||||
| `0x07` | `COMMIT` | пустой |
|
||||
| `0x08` | `CONFIRM` | пустой |
|
||||
| `0x09` | `REBOOT` | пустой |
|
||||
| `0x0A` | `ABORT` | пустой |
|
||||
| `0x0B` | `QUERY_PROGRESS` | пустой |
|
||||
|
||||
`BOOT_STATUS` всегда содержит 8 байт:
|
||||
|
||||
```text
|
||||
offset size field
|
||||
0 1 status
|
||||
1 1 target_slot
|
||||
2 2 next_block u16 LE
|
||||
4 4 running_crc32 u32 LE
|
||||
```
|
||||
|
||||
`BOOT_DISCOVERY` с body `1` содержит:
|
||||
|
||||
```text
|
||||
offset size field
|
||||
0 2 product_type u16 LE
|
||||
2 1 hardware_revision
|
||||
3 1 protocol_version = 1
|
||||
4 4 firmware_version u32 LE
|
||||
```
|
||||
|
||||
Пример запроса `IDENTIFY` для `DeviceType=7`, `DeviceID=13`:
|
||||
|
||||
```text
|
||||
CAN ID: 17D90001
|
||||
DLC: 0
|
||||
```
|
||||
|
||||
### 1.2. Python-парсер v1
|
||||
|
||||
```python
|
||||
def parse_v1(can_id: int, data: bytes) -> dict:
|
||||
if not 0 <= can_id <= 0x1FFFFFFF:
|
||||
raise ValueError("неверный Extended CAN ID")
|
||||
if len(data) > 8:
|
||||
raise ValueError("DLC больше 8")
|
||||
|
||||
result = {
|
||||
"version": 1,
|
||||
"priority": (can_id >> 28) & 0x01,
|
||||
"route": (can_id >> 27) & 0x01,
|
||||
"device_type": (can_id >> 24) & 0x07,
|
||||
"device_id": (can_id >> 20) & 0x0F,
|
||||
"message_type": (can_id >> 16) & 0x0F,
|
||||
"message_body": can_id & 0xFFFF,
|
||||
"data": bytes(data),
|
||||
}
|
||||
|
||||
msg_type = result["message_type"]
|
||||
body = result["message_body"]
|
||||
if msg_type in (0x9, 0xC):
|
||||
result["session_id"] = (body >> 8) & 0xFF
|
||||
result["command"] = body & 0xFF
|
||||
elif msg_type in (0xA, 0xB):
|
||||
result["slot"] = msg_type - 0xA
|
||||
result["block_index"] = body
|
||||
|
||||
if msg_type == 0xC:
|
||||
if len(data) != 8:
|
||||
raise ValueError("BOOT_STATUS должен содержать 8 байт")
|
||||
result.update({
|
||||
"status": data[0],
|
||||
"target_slot": data[1],
|
||||
"next_block": int.from_bytes(data[2:4], "little"),
|
||||
"running_crc32": int.from_bytes(data[4:8], "little"),
|
||||
})
|
||||
elif msg_type == 0xD and body == 1:
|
||||
if len(data) != 8:
|
||||
raise ValueError("BOOT_DISCOVERY должен содержать 8 байт")
|
||||
result.update({
|
||||
"product_type": int.from_bytes(data[0:2], "little"),
|
||||
"hardware_revision": data[2],
|
||||
"protocol_version": data[3],
|
||||
"firmware_version": int.from_bytes(data[4:8], "little"),
|
||||
})
|
||||
return result
|
||||
```
|
||||
|
||||
## 2. SETProtocol v2 поверх classic CAN
|
||||
|
||||
В v2 CAN-кадр является только транспортным сегментом. Сначала нужно собрать
|
||||
полный SETP-пакет, и только затем разбирать его заголовок, payload и CRC32.
|
||||
|
||||
### 2.1. Разметка Extended CAN ID
|
||||
|
||||
```text
|
||||
bits size field
|
||||
28..24 5 Prefix = 0x12
|
||||
23..16 8 Destination node
|
||||
15..8 8 Source node
|
||||
7 1 Priority
|
||||
6..0 7 Channel
|
||||
```
|
||||
|
||||
Формула:
|
||||
|
||||
```text
|
||||
ID = 0x12 << 24 |
|
||||
Destination << 16 |
|
||||
Source << 8 |
|
||||
Priority << 7 |
|
||||
Channel
|
||||
```
|
||||
|
||||
### 2.2. CAN-сегменты
|
||||
|
||||
Первый байт CAN payload — PCI:
|
||||
|
||||
| PCI | Назначение | Формат CAN payload |
|
||||
|---:|---|---|
|
||||
| `0x10` | первый сегмент | `10`, `total_length u16 LE`, первые 5 байт SETP |
|
||||
| `0x20..0x2F` | продолжение | `2N`, следующие 1–7 байт SETP |
|
||||
| `0x30..0x32` | flow control | `3S`, `block_size`, `st_min_ms` |
|
||||
|
||||
`N` — циклический номер сегмента `1..15,0..`; следующий сегмент обязан иметь
|
||||
ожидаемый номер, тот же CAN ID и прийти до тайм-аута сборки 500 мс.
|
||||
|
||||
### 2.3. Внутренний кадр SETProtocol v2
|
||||
|
||||
```text
|
||||
offset size field
|
||||
0 2 SOF = A5 5A
|
||||
2 1 version = 02
|
||||
3 1 flags
|
||||
4 2 message_type u16 LE
|
||||
6 2 source u16 LE
|
||||
8 2 destination u16 LE
|
||||
10 2 sequence u16 LE
|
||||
12 2 payload_length u16 LE
|
||||
14 N payload
|
||||
14+N 4 CRC32 IEEE u32 LE
|
||||
```
|
||||
|
||||
CRC32 считается по байтам от `version` на offset 2 до конца payload. Поля
|
||||
`source`, `destination` и `priority` внутреннего заголовка должны совпадать с
|
||||
CAN ID.
|
||||
|
||||
Флаги:
|
||||
|
||||
| Бит | Значение |
|
||||
|---:|---|
|
||||
| `0x01` | RESPONSE |
|
||||
| `0x02` | EVENT |
|
||||
| `0x04` | ERROR |
|
||||
| `0x08` | ACK_REQUIRED |
|
||||
| `0x10` | MORE |
|
||||
| `0x20` | PRIORITY |
|
||||
|
||||
Каждый response начинается с `status u16 LE`. Основные firmware message types:
|
||||
`FW_BEGIN=0x0100`, `FW_DATA=0x0101`, `FW_END=0x0102`, `FW_ABORT=0x0103`,
|
||||
`FW_STATUS=0x0104`, `FW_ACTIVATE=0x0105`.
|
||||
|
||||
Пример `PING` к BALZAM node `13`, source `0`, sequence `1`, priority `1`,
|
||||
channel `1`:
|
||||
|
||||
```text
|
||||
Полный SETP:
|
||||
A5 5A 02 28 01 00 00 00 0D 00 01 00 00 00 E7 29 51 40
|
||||
|
||||
CAN ID 120D0081, сегменты:
|
||||
10 12 00 A5 5A 02 28 01
|
||||
21 00 00 00 0D 00 01 00
|
||||
22 00 00 E7 29 51 40
|
||||
```
|
||||
|
||||
### 2.4. Python-парсер и сборщик v2
|
||||
|
||||
```python
|
||||
import binascii
|
||||
|
||||
|
||||
def parse_v2_can_id(can_id: int) -> dict:
|
||||
if not 0 <= can_id <= 0x1FFFFFFF:
|
||||
raise ValueError("неверный Extended CAN ID")
|
||||
if (can_id >> 24) & 0x1F != 0x12:
|
||||
raise ValueError("не SETProtocol v2 CAN ID")
|
||||
return {
|
||||
"destination": (can_id >> 16) & 0xFF,
|
||||
"source": (can_id >> 8) & 0xFF,
|
||||
"priority": (can_id >> 7) & 0x01,
|
||||
"channel": can_id & 0x7F,
|
||||
}
|
||||
|
||||
|
||||
def parse_setp(packet: bytes, can_id: int) -> dict:
|
||||
if len(packet) < 18 or packet[:2] != b"\xA5\x5A":
|
||||
raise ValueError("нет полного SETP-кадра")
|
||||
if packet[2] != 2:
|
||||
raise ValueError("неподдерживаемая версия SETP")
|
||||
flags = packet[3]
|
||||
if flags & 0xC0:
|
||||
raise ValueError("установлены зарезервированные флаги")
|
||||
payload_length = int.from_bytes(packet[12:14], "little")
|
||||
if len(packet) != 14 + payload_length + 4:
|
||||
raise ValueError("не совпадает payload_length")
|
||||
expected_crc = int.from_bytes(packet[-4:], "little")
|
||||
actual_crc = binascii.crc32(packet[2:-4]) & 0xFFFFFFFF
|
||||
if actual_crc != expected_crc:
|
||||
raise ValueError("ошибка CRC32 SETP")
|
||||
|
||||
address = parse_v2_can_id(can_id)
|
||||
source = int.from_bytes(packet[6:8], "little")
|
||||
destination = int.from_bytes(packet[8:10], "little")
|
||||
priority = int(bool(flags & 0x20))
|
||||
if (source, destination, priority) != (
|
||||
address["source"], address["destination"], address["priority"]
|
||||
):
|
||||
raise ValueError("SETP header не совпадает с CAN ID")
|
||||
|
||||
payload = packet[14:-4]
|
||||
result = {
|
||||
"version": 2,
|
||||
"flags": flags,
|
||||
"message_type": int.from_bytes(packet[4:6], "little"),
|
||||
"source": source,
|
||||
"destination": destination,
|
||||
"sequence": int.from_bytes(packet[10:12], "little"),
|
||||
"payload": payload,
|
||||
"can": address,
|
||||
}
|
||||
if flags & 0x01:
|
||||
if len(payload) < 2:
|
||||
raise ValueError("response не содержит status")
|
||||
result["status"] = int.from_bytes(payload[:2], "little")
|
||||
result["body"] = payload[2:]
|
||||
return result
|
||||
|
||||
|
||||
class V2CanReassembler:
|
||||
def __init__(self, timeout_ms: int = 500):
|
||||
self.timeout_ms = timeout_ms
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.can_id = None
|
||||
self.total = 0
|
||||
self.data = bytearray()
|
||||
self.next_sequence = 1
|
||||
self.deadline_ms = 0
|
||||
|
||||
def feed(self, can_id: int, data: bytes, now_ms: int):
|
||||
parse_v2_can_id(can_id)
|
||||
if not 1 <= len(data) <= 8:
|
||||
raise ValueError("DLC вне диапазона 1..8")
|
||||
if self.can_id is not None and now_ms >= self.deadline_ms:
|
||||
self.reset()
|
||||
raise ValueError("тайм-аут сборки SETP")
|
||||
|
||||
pci_type = data[0] & 0xF0
|
||||
if pci_type == 0x10:
|
||||
if len(data) != 8:
|
||||
raise ValueError("первый сегмент должен иметь DLC 8")
|
||||
total = int.from_bytes(data[1:3], "little")
|
||||
if not 18 <= total <= 530:
|
||||
raise ValueError("неверный размер SETP")
|
||||
self.can_id = can_id
|
||||
self.total = total
|
||||
self.data = bytearray(data[3:])
|
||||
self.next_sequence = 1
|
||||
self.deadline_ms = now_ms + self.timeout_ms
|
||||
return None
|
||||
|
||||
if pci_type == 0x20:
|
||||
sequence = data[0] & 0x0F
|
||||
if (
|
||||
self.can_id is None
|
||||
or can_id != self.can_id
|
||||
or sequence != self.next_sequence
|
||||
or len(data) < 2
|
||||
):
|
||||
self.reset()
|
||||
raise ValueError("ошибка последовательности CAN-сегментов")
|
||||
if len(data) - 1 > self.total - len(self.data):
|
||||
self.reset()
|
||||
raise ValueError("лишние байты CAN-сегмента")
|
||||
self.data.extend(data[1:])
|
||||
self.next_sequence = (self.next_sequence + 1) & 0x0F
|
||||
self.deadline_ms = now_ms + self.timeout_ms
|
||||
if len(self.data) == self.total:
|
||||
packet = bytes(self.data)
|
||||
packet_can_id = self.can_id
|
||||
self.reset()
|
||||
return parse_setp(packet, packet_can_id)
|
||||
return None
|
||||
|
||||
if pci_type == 0x30:
|
||||
return {"flow_control": data[0] & 0x0F, "data": data[1:]}
|
||||
raise ValueError("неизвестный PCI")
|
||||
```
|
||||
|
||||
В SETGUI эти операции уже реализованы в
|
||||
`third_party/templates/python/setprotocol/can.py`; собственный parser нужен
|
||||
только внешнему анализатору или диагностическому скрипту.
|
||||
|
||||
## 3. Как отличать v1 от v2
|
||||
|
||||
Для используемых сейчас адресов достаточно следующих признаков:
|
||||
|
||||
- v2: верхние пять бит CAN ID равны `0x12`, PCI начинается с `0x10`, `0x2N`
|
||||
или `0x3S`, после reassembly присутствует `A5 5A 02`;
|
||||
- v1: `MessageType` в битах `19..16` равен `0x9..0xD`, каждый кадр разбирается
|
||||
самостоятельно.
|
||||
|
||||
Однако универсальное автоопределение только по одному CAN ID невозможно:
|
||||
комбинация `Priority/Route/DeviceType` v1 теоретически тоже может дать верхнее
|
||||
поле `0x12`, а первый байт firmware data v1 может случайно совпасть с PCI.
|
||||
Надёжный анализатор должен учитывать настроенный режим узла либо подтвердить v2
|
||||
только после сборки кадра с корректными `A5 5A 02`, длиной и CRC32.
|
||||
|
||||
## 4. Канонические исходники
|
||||
|
||||
- v1 ID и state machine: `third_party/templates/c/protocan-boot/src/pcan_boot.c`;
|
||||
- v2 CAN transport: `third_party/templates/c/set-protocol/src/set_can.c`;
|
||||
- v2 frame/CRC: `third_party/templates/c/set-protocol/src/set_protocol.c`;
|
||||
- v2 firmware payload: `third_party/templates/c/set-protocol/src/set_firmware.c`;
|
||||
- Python v2 CAN: `third_party/templates/python/setprotocol/can.py`.
|
||||
@@ -4,9 +4,22 @@ param()
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$outputPath = Join-Path $PSScriptRoot 'setprotocol.html'
|
||||
$sourcePath = Join-Path $PSScriptRoot '..\c\set-protocol\docs\SETPROTOCOL.md'
|
||||
$canFrameSourcePath = Join-Path $PSScriptRoot 'CAN_FRAME_PARSE_V1_V2.md'
|
||||
|
||||
$markdown = Get-Content -Raw -LiteralPath $sourcePath -Encoding UTF8
|
||||
$html = (ConvertFrom-Markdown -InputObject $markdown).Html
|
||||
function Convert-DocumentationMarkdown {
|
||||
param([string]$Path)
|
||||
|
||||
$markdown = Get-Content -Raw -LiteralPath $Path -Encoding UTF8
|
||||
$html = (ConvertFrom-Markdown -InputObject $markdown).Html
|
||||
|
||||
# Wide protocol tables must scroll horizontally instead of squeezing their
|
||||
# contents into unreadable one-character columns on a narrow viewport.
|
||||
$html = $html -replace '<table>', '<div class="table-wrap"><table>'
|
||||
$html = $html -replace '</table>', '</table></div>'
|
||||
return $html
|
||||
}
|
||||
|
||||
$html = Convert-DocumentationMarkdown -Path $sourcePath
|
||||
$generatedBlock = @"
|
||||
<!-- SETPROTOCOL:START -->
|
||||
<article class="card full-doc" data-source="c/set-protocol/docs/SETPROTOCOL.md">
|
||||
@@ -15,6 +28,15 @@ $html
|
||||
<!-- SETPROTOCOL:END -->
|
||||
"@
|
||||
|
||||
$canFrameHtml = Convert-DocumentationMarkdown -Path $canFrameSourcePath
|
||||
$canFrameBlock = @"
|
||||
<!-- CAN-FRAME-PARSE:START -->
|
||||
<article class="card full-doc" data-source="doc/CAN_FRAME_PARSE_V1_V2.md">
|
||||
$canFrameHtml
|
||||
</article>
|
||||
<!-- CAN-FRAME-PARSE:END -->
|
||||
"@
|
||||
|
||||
$page = Get-Content -Raw -LiteralPath $outputPath -Encoding UTF8
|
||||
$pattern = '(?s)<!-- SETPROTOCOL:START -->.*?<!-- SETPROTOCOL:END -->'
|
||||
if ($page -notmatch $pattern) {
|
||||
@@ -26,6 +48,16 @@ $page = [regex]::Replace($page, $pattern, [System.Text.RegularExpressions.MatchE
|
||||
$generatedBlock
|
||||
}, 1)
|
||||
|
||||
$canFramePattern = '(?s)<!-- CAN-FRAME-PARSE:START -->.*?<!-- CAN-FRAME-PARSE:END -->'
|
||||
if ($page -notmatch $canFramePattern) {
|
||||
throw 'Не найдены маркеры CAN-FRAME-PARSE:START/END в doc/setprotocol.html.'
|
||||
}
|
||||
|
||||
$page = [regex]::Replace($page, $canFramePattern, [System.Text.RegularExpressions.MatchEvaluator]{
|
||||
param($match)
|
||||
$canFrameBlock
|
||||
}, 1)
|
||||
|
||||
$page = $page.TrimEnd("`r", "`n") + [Environment]::NewLine
|
||||
$utf8WithoutBom = [System.Text.UTF8Encoding]::new($false)
|
||||
[System.IO.File]::WriteAllText($outputPath, $page, $utf8WithoutBom)
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
60
python/protocan/devboard_v1.py
Normal file
60
python/protocan/devboard_v1.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""CAN485 DevBoard_V1 USB command and frame helpers (no GUI/serial dependency)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
FRAME_RE = re.compile(
|
||||
r"^\[\s*(\d+)\.(\d{3})\]\s+(?:(RS485)\s+)?"
|
||||
r"(EXT|STD)\s+0x([0-9A-Fa-f]+)\s+"
|
||||
r"(RTR\s+)?DLC=(\d)(?:\s+DATA=([0-9A-Fa-f ]*))?"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class BoardFrame:
|
||||
timestamp: float
|
||||
extended: bool
|
||||
can_id: int
|
||||
data: bytes
|
||||
rtr: bool = False
|
||||
source: str = "CAN"
|
||||
|
||||
|
||||
def parse_frame_line(line: str) -> BoardFrame | None:
|
||||
match = FRAME_RE.match(line.strip())
|
||||
if match is None:
|
||||
return None
|
||||
seconds, millis, source, frame_type, raw_id, raw_rtr, raw_dlc, raw_data = match.groups()
|
||||
dlc = int(raw_dlc)
|
||||
data = bytes.fromhex(raw_data or "")
|
||||
rtr = bool(raw_rtr)
|
||||
if (not rtr and len(data) != dlc) or (rtr and data):
|
||||
return None
|
||||
extended = frame_type == "EXT"
|
||||
can_id = int(raw_id, 16)
|
||||
if can_id > (0x1FFFFFFF if extended else 0x7FF):
|
||||
return None
|
||||
return BoardFrame(int(seconds) + int(millis) / 1000.0, extended, can_id,
|
||||
data, rtr, source or "CAN")
|
||||
|
||||
|
||||
def command_transmit(frame: BoardFrame) -> bytes:
|
||||
if len(frame.data) > 8:
|
||||
raise ValueError("DLC cannot exceed 8")
|
||||
kind = "E" if frame.extended else "S"
|
||||
return f"T,{kind},{frame.can_id:X},{len(frame.data)},{frame.data.hex().upper()}\n".encode("ascii")
|
||||
|
||||
|
||||
def command_setup(*, can_bitrate: int = 500, rs485_baud: int = 512000,
|
||||
route: int = 3) -> tuple[bytes, ...]:
|
||||
if can_bitrate not in (25, 50, 100, 125, 250, 500, 800, 1000):
|
||||
raise ValueError("unsupported CAN bitrate")
|
||||
if not 1200 <= rs485_baud <= 4_000_000:
|
||||
raise ValueError("unsupported RS485 baud")
|
||||
if route not in range(4):
|
||||
raise ValueError("route must be 0..3")
|
||||
return (f"S{can_bitrate}\n".encode("ascii"), b"M0\n",
|
||||
f"B{rs485_baud}\n".encode("ascii"), f"Q{route}\n".encode("ascii"))
|
||||
14
python/tests/test_devboard_v1.py
Normal file
14
python/tests/test_devboard_v1.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from protocan.devboard_v1 import BoardFrame, command_setup, command_transmit, parse_frame_line
|
||||
|
||||
|
||||
def test_parse_can_and_rs485_lines():
|
||||
can = parse_frame_line("[ 12.345] EXT 0x073700A2 DLC=2 DATA=34 12 |4.|")
|
||||
rs = parse_frame_line("[ 12.346] RS485 STD 0x123 RTR DLC=4")
|
||||
assert can and can.can_id == 0x073700A2 and can.data == b"\x34\x12"
|
||||
assert rs and rs.source == "RS485" and rs.rtr and rs.data == b""
|
||||
|
||||
|
||||
def test_commands_are_firmware_compatible_and_lf_terminated():
|
||||
frame = BoardFrame(0.0, True, 0x073700A2, b"\x34\x12")
|
||||
assert command_transmit(frame) == b"T,E,73700A2,2,3412\n"
|
||||
assert command_setup() == (b"S500\n", b"M0\n", b"B512000\n", b"Q3\n")
|
||||
Reference in New Issue
Block a user