Compare commits

...

10 Commits

234 changed files with 28809 additions and 35 deletions

3
.gitignore vendored
View File

@@ -24,3 +24,6 @@ __pycache__/
Thumbs.db
Desktop.ini
.codex-build/
# Generated native library (built before packaging)
python/protocan/native/

View File

@@ -50,6 +50,9 @@ templates/
| Модуль | Что делает | Зависимости |
|---|---|---|
| [`python/setprotocol/firmware_database.py`](python/setprotocol/firmware_database.py) | база прошивок: HTTPS-каталог, скачивание с SHA-256 и кэшем, публикация в Gitea без SETGUI; [подключение и CLI](tools/firmware-publish/DATABASE.md) | stdlib, Python 3.10+ |
| [`python/setprotocol/firmware_publish.py`](python/setprotocol/firmware_publish.py) | общие метаданные публикации, SHA-256, release tag и обновление каталога прошивок; [подключение](tools/firmware-publish/PORTING.md) | stdlib; сетевой адаптер в SETGUI |
| [`python/setprotocol/firmware_catalog.py`](python/setprotocol/firmware_catalog.py) | модель и parser каталога `firmware.releases` | stdlib |
| [`python/protocan`](python/protocan) | разбор ProtoCAN, транспортный кадр моста, кадр SETGUI, кодеки каталога | stdlib, Python 3.9+ |
| [`python/protocan/trends.py`](python/protocan/trends.py) | общие настройки графиков, ограниченная история, ctypes-декодер GAS/raw CAN | stdlib, опционально SETProtocol DLL/SO |

9
c/candle/CMakeLists.txt Normal file
View File

@@ -0,0 +1,9 @@
cmake_minimum_required(VERSION 3.15)
project(candle C)
if(NOT WIN32)
message(FATAL_ERROR "Candle transport requires Windows WinUSB")
endif()
add_library(candle SHARED candle.c candle_ctrl_req.c candle.def)
target_compile_definitions(candle PRIVATE UNICODE _UNICODE)
target_link_libraries(candle PRIVATE setupapi winusb ole32 advapi32)
target_include_directories(candle PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

66
c/candle/LICENSE Normal file

File diff suppressed because one or more lines are too long

16
c/candle/README.md Normal file
View File

@@ -0,0 +1,16 @@
# Candle / gs_usb — порт WinUSB
Общий C-порт для адаптеров candleLight/gs_usb. Перенесён из
SETGUI `src/gui_desktop/native/candle_src`; исходные файлы и LGPLv3
`LICENSE` сохранены без изменений.
Сборка из Developer Command Prompt:
```bat
cmake -S c/candle -B build/candle -A x64
cmake --build build/candle --config Release
```
Для 32-битной библиотеки используйте `-A Win32` и отдельный каталог сборки.
Python Qt-порт находится в `python/set_devices/qt_ports/candle_adapter.py`;
путь к библиотеке задаётся через `CANDLE_LIBRARY`.

1151
c/candle/candle.c Normal file
View File

@@ -0,0 +1,1151 @@
/*
Copyright (c) 2016 Hubert Denkmair <hubert@denkmair.de>
This file is part of the candle windows API.
This library is free software: you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation, either
version 3 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#include "candle.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include "candle_defs.h"
#include "candle_ctrl_req.h"
#include "ch_9.h"
static bool candle_dev_interal_open(candle_handle hdev);
candle_log_fn_t candle_log_fn = NULL;
bool candle_log_verbose = false;
static void candle_logf(const wchar_t *fmt, ...)
{
if (candle_log_fn == NULL) {
return;
}
wchar_t buf[512];
va_list args;
va_start(args, fmt);
HRESULT hr = StringCchVPrintfW(buf, 512, fmt, args);
va_end(args);
if (SUCCEEDED(hr)) {
candle_log_fn(buf);
}
}
static void candle_logf_verbose(const wchar_t *fmt, ...)
{
if (candle_log_fn == NULL || !candle_log_verbose) {
return;
}
wchar_t buf[512];
va_list args;
va_start(args, fmt);
HRESULT hr = StringCchVPrintfW(buf, 512, fmt, args);
va_end(args);
if (SUCCEEDED(hr)) {
candle_log_fn(buf);
}
}
static bool candle_read_di(HDEVINFO hdi, SP_DEVICE_INTERFACE_DATA interfaceData, candle_device_t *dev)
{
/* get required length first (this call always fails with an error) */
ULONG requiredLength=0;
SetupDiGetDeviceInterfaceDetail(hdi, &interfaceData, NULL, 0, &requiredLength, NULL);
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
dev->last_error = CANDLE_ERR_SETUPDI_IF_DETAILS;
return false;
}
PSP_DEVICE_INTERFACE_DETAIL_DATA detail_data =
(PSP_DEVICE_INTERFACE_DETAIL_DATA) LocalAlloc(LMEM_FIXED, requiredLength);
if (detail_data != NULL) {
detail_data->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
} else {
dev->last_error = CANDLE_ERR_MALLOC;
return false;
}
bool retval = true;
ULONG length = requiredLength;
if (!SetupDiGetDeviceInterfaceDetail(hdi, &interfaceData, detail_data, length, &requiredLength, NULL) ) {
dev->last_error = CANDLE_ERR_SETUPDI_IF_DETAILS2;
retval = false;
} else if (FAILED(StringCchCopy(dev->path, sizeof(dev->path), detail_data->DevicePath))) {
dev->last_error = CANDLE_ERR_PATH_LEN;
retval = false;
}
LocalFree(detail_data);
if (!retval) {
return false;
}
/* try to open to read device infos and see if it is avail */
if (candle_dev_interal_open(dev)) {
dev->state = CANDLE_DEVSTATE_AVAIL;
candle_dev_close(dev);
} else {
dev->state = CANDLE_DEVSTATE_INUSE;
}
dev->last_error = CANDLE_ERR_OK;
return true;
}
/* Return true when path already appears in l->dev[0..count-1]. */
static bool candle_path_exists(const candle_list_t *l, unsigned count, const wchar_t *path)
{
for (unsigned i = 0; i < count; i++) {
if (wcscmp(l->dev[i].path, path) == 0)
return true;
}
return false;
}
/* Scan one GUID and append found devices to l->dev[] starting at offset.
* Returns the number of devices appended, or -1 on a hard error (l->last_error set). */
static int candle_scan_guid(candle_list_t *l, const wchar_t *guid_str, unsigned offset)
{
GUID guid;
if (CLSIDFromString(guid_str, &guid) != NOERROR) {
l->last_error = CANDLE_ERR_CLSID;
return -1;
}
HDEVINFO hdi = SetupDiGetClassDevs(&guid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
if (hdi == INVALID_HANDLE_VALUE) {
/* No devices with this GUID present — not a hard error. */
return 0;
}
int found = 0;
for (unsigned i = 0; (offset + i) < CANDLE_MAX_DEVICES; i++) {
SP_DEVICE_INTERFACE_DATA interfaceData;
interfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
if (!SetupDiEnumDeviceInterfaces(hdi, NULL, &guid, i, &interfaceData)) {
if (GetLastError() != ERROR_NO_MORE_ITEMS) {
l->last_error = CANDLE_ERR_SETUPDI_IF_ENUM;
found = -1;
}
break;
}
if (!candle_read_di(hdi, interfaceData, &l->dev[offset + i])) {
l->last_error = l->dev[offset + i].last_error;
found = -1;
break;
}
found++;
}
SetupDiDestroyDeviceInfoList(hdi);
return found;
}
/* Scan for WinUSB devices matching vid:pid whose device interface GUID was not
* covered by the GUID list above. For each matching USB device instance the
* function reads DeviceInterfaceGUIDs (or DeviceInterfaceGUID) from the Windows
* registry, re-uses candle_scan_guid() for each GUID found there, and appends
* only those devices that are not already present in l->dev[0..existing-1].
* Returns the number of new devices added. */
static int candle_scan_vidpid(candle_list_t *l, uint16_t vid, uint16_t pid, unsigned existing)
{
wchar_t hwid_prefix[32];
StringCchPrintfW(hwid_prefix, 32, L"USB\\VID_%04X&PID_%04X", vid, pid);
/* Enumerate USB device instances (not interfaces) so we can read hardware IDs. */
HDEVINFO hdi = SetupDiGetClassDevs(NULL, L"USB", NULL,
DIGCF_ALLCLASSES | DIGCF_PRESENT);
if (hdi == INVALID_HANDLE_VALUE) {
return 0;
}
int added = 0;
SP_DEVINFO_DATA devInfo;
devInfo.cbSize = sizeof(SP_DEVINFO_DATA);
for (DWORD i = 0;
SetupDiEnumDeviceInfo(hdi, i, &devInfo) && existing + added < CANDLE_MAX_DEVICES;
i++)
{
/* Hardware IDs are a REG_MULTI_SZ — check each string for our VID/PID prefix. */
wchar_t hwids[512];
memset(hwids, 0, sizeof(hwids));
if (!SetupDiGetDeviceRegistryPropertyW(hdi, &devInfo, SPDRP_HARDWAREID,
NULL, (PBYTE)hwids, sizeof(hwids) - sizeof(wchar_t), NULL)) {
continue;
}
bool matches = false;
const wchar_t *p;
for (p = hwids; *p; p += wcslen(p) + 1) {
if (_wcsnicmp(p, hwid_prefix, wcslen(hwid_prefix)) == 0) {
matches = true;
break;
}
}
if (!matches) {
continue;
}
/* Open the device's software registry key (Device Parameters) and read
* the WinUSB device interface GUID(s) stored by the driver INF. */
HKEY hKey = SetupDiOpenDevRegKey(hdi, &devInfo, DICS_FLAG_GLOBAL, 0,
DIREG_DEV, KEY_READ);
if (hKey == INVALID_HANDLE_VALUE) {
continue;
}
wchar_t guid_buf[256];
memset(guid_buf, 0, sizeof(guid_buf));
DWORD buf_len = sizeof(guid_buf) - sizeof(wchar_t);
/* Prefer DeviceInterfaceGUIDs (REG_MULTI_SZ, modern INFs); fall back to
* DeviceInterfaceGUID (REG_SZ, older/zadig-generated INFs). */
LONG reg_rc = RegQueryValueExW(hKey, L"DeviceInterfaceGUIDs", NULL, NULL,
(LPBYTE)guid_buf, &buf_len);
if (reg_rc != ERROR_SUCCESS) {
buf_len = sizeof(guid_buf) - sizeof(wchar_t);
RegQueryValueExW(hKey, L"DeviceInterfaceGUID", NULL, NULL,
(LPBYTE)guid_buf, &buf_len);
}
RegCloseKey(hKey);
if (!guid_buf[0]) {
continue;
}
/* Iterate GUID strings. Both REG_SZ and REG_MULTI_SZ are covered by the
* same NUL-terminated-string walk (REG_SZ just has one entry). */
const wchar_t *g;
for (g = guid_buf;
*g && existing + added < CANDLE_MAX_DEVICES;
g += wcslen(g) + 1)
{
unsigned base = existing + added;
int n = candle_scan_guid(l, g, base);
if (n <= 0) {
continue;
}
/* Remove any entries whose path was already found by the GUID scan. */
for (int ni = 0; ni < n; ) {
if (candle_path_exists(l, base, l->dev[base + ni].path)) {
memmove(&l->dev[base + ni], &l->dev[base + ni + 1],
(unsigned)(n - ni - 1) * sizeof(candle_device_t));
n--;
} else {
ni++;
}
}
added += n;
}
}
SetupDiDestroyDeviceInfoList(hdi);
return added;
}
bool __stdcall candle_list_scan(candle_list_handle *list)
{
if (list == NULL) {
return false;
}
candle_list_t *l = (candle_list_t *)calloc(1, sizeof(candle_list_t));
*list = l;
if (l == NULL) {
return false;
}
/* GUIDs for gs_usb-compatible devices on Windows.
* candleLight / CANable / most gs_usb devices: */
static const wchar_t *GUIDS[] = {
L"{c15b4308-04d3-11e6-b3ea-6057189e6443}" /* candleLight / CANable / gs_usb standard */
};
static const unsigned NUM_GUIDS = sizeof(GUIDS) / sizeof(GUIDS[0]);
unsigned total = 0;
for (unsigned g = 0; g < NUM_GUIDS; g++) {
int n = candle_scan_guid(l, GUIDS[g], total);
if (n < 0) {
return false;
}
total += (unsigned)n;
}
/* VID/PID scan for devices whose device interface GUID is not in the list
* above (e.g. CANnectivity which uses its own registered interface GUID). */
static const struct { uint16_t vid; uint16_t pid; } VIDPIDS[] = {
{ 0x1209, 0xCA01 }, /* CANnectivity (electronut-labs) */
};
static const unsigned NUM_VIDPIDS = sizeof(VIDPIDS) / sizeof(VIDPIDS[0]);
for (unsigned v = 0; v < NUM_VIDPIDS && total < CANDLE_MAX_DEVICES; v++) {
int n = candle_scan_vidpid(l, VIDPIDS[v].vid, VIDPIDS[v].pid, total);
if (n > 0)
total += (unsigned)n;
}
l->num_devices = (uint8_t)total;
l->last_error = CANDLE_ERR_OK;
return true;
}
bool __stdcall DLL candle_list_free(candle_list_handle list)
{
free(list);
return true;
}
bool __stdcall DLL candle_list_length(candle_list_handle list, uint8_t *len)
{
candle_list_t *l = (candle_list_t *)list;
*len = l->num_devices;
return true;
}
bool __stdcall DLL candle_dev_get(candle_list_handle list, uint8_t dev_num, candle_handle *hdev)
{
candle_list_t *l = (candle_list_t *)list;
if (l==NULL) {
return false;
}
if (dev_num >= CANDLE_MAX_DEVICES) {
l->last_error = CANDLE_ERR_DEV_OUT_OF_RANGE;
return false;
}
candle_device_t *dev = calloc(1, sizeof(candle_device_t));
*hdev = dev;
if (dev==NULL) {
l->last_error = CANDLE_ERR_MALLOC;
return false;
}
memcpy(dev, &l->dev[dev_num], sizeof(candle_device_t));
l->last_error = CANDLE_ERR_OK;
dev->last_error = CANDLE_ERR_OK;
return true;
}
bool __stdcall DLL candle_dev_get_state(candle_handle hdev, candle_devstate_t *state)
{
if (hdev==NULL) {
return false;
} else {
candle_device_t *dev = (candle_device_t*)hdev;
*state = dev->state;
return true;
}
}
wchar_t * __stdcall DLL candle_dev_get_path(candle_handle hdev)
{
if (hdev==NULL) {
return NULL;
} else {
candle_device_t *dev = (candle_device_t*)hdev;
return dev->path;
}
}
static bool candle_dev_interal_open(candle_handle hdev)
{
candle_device_t *dev = (candle_device_t*)hdev;
memset(dev->rxevents, 0, sizeof(dev->rxevents));
memset(dev->rxurbs, 0, sizeof(dev->rxurbs));
dev->deviceHandle = CreateFile(
dev->path,
GENERIC_WRITE | GENERIC_READ,
FILE_SHARE_WRITE | FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
NULL
);
if (dev->deviceHandle == INVALID_HANDLE_VALUE) {
dev->last_error = CANDLE_ERR_CREATE_FILE;
return false;
}
if (!WinUsb_Initialize(dev->deviceHandle, &dev->winUSBHandle)) {
dev->last_error = CANDLE_ERR_WINUSB_INITIALIZE;
goto close_handle;
}
USB_INTERFACE_DESCRIPTOR ifaceDescriptor;
if (!WinUsb_QueryInterfaceSettings(dev->winUSBHandle, 0, &ifaceDescriptor)) {
dev->last_error = CANDLE_ERR_QUERY_INTERFACE;
goto winusb_free;
}
dev->interfaceNumber = ifaceDescriptor.bInterfaceNumber;
bool has_in = false, has_out = false;
candle_logf(L"open path=%ls interface=%u endpoints=%u",
dev->path,
dev->interfaceNumber,
ifaceDescriptor.bNumEndpoints);
for (uint8_t i=0; i<ifaceDescriptor.bNumEndpoints; i++) {
WINUSB_PIPE_INFORMATION pipeInfo;
if (!WinUsb_QueryPipe(dev->winUSBHandle, 0, i, &pipeInfo)) {
dev->last_error = CANDLE_ERR_QUERY_PIPE;
goto winusb_free;
}
if (pipeInfo.PipeType == UsbdPipeTypeBulk && USB_ENDPOINT_DIRECTION_IN(pipeInfo.PipeId)) {
if (!has_in) {
dev->bulkInPipe = pipeInfo.PipeId;
has_in = true;
candle_logf(L"selected bulk IN pipe=0x%02x maxPacket=%u interval=%u",
pipeInfo.PipeId,
pipeInfo.MaximumPacketSize,
pipeInfo.Interval);
}
} else if (pipeInfo.PipeType == UsbdPipeTypeBulk && USB_ENDPOINT_DIRECTION_OUT(pipeInfo.PipeId)) {
if (!has_out) {
dev->bulkOutPipe = pipeInfo.PipeId;
has_out = true;
candle_logf(L"selected bulk OUT pipe=0x%02x maxPacket=%u interval=%u",
pipeInfo.PipeId,
pipeInfo.MaximumPacketSize,
pipeInfo.Interval);
}
}
}
if (!has_in || !has_out) {
dev->last_error = CANDLE_ERR_PARSE_IF_DESCR;
goto winusb_free;
}
char use_raw_io = 1;
if (!WinUsb_SetPipePolicy(dev->winUSBHandle, dev->bulkInPipe, RAW_IO, sizeof(use_raw_io), &use_raw_io)) {
dev->last_error = CANDLE_ERR_SET_PIPE_RAW_IO;
goto winusb_free;
}
if (!candle_ctrl_set_host_format(dev)) {
goto winusb_free;
}
if (!candle_ctrl_get_config(dev, &dev->dconf)) {
goto winusb_free;
}
candle_logf(L"device config channels=%u sw=0x%08x hw=0x%08x",
dev->dconf.icount + 1,
dev->dconf.sw_version,
dev->dconf.hw_version);
if (!candle_ctrl_get_capability(dev, 0, &dev->bt_const)) {
dev->last_error = CANDLE_ERR_GET_BITTIMING_CONST;
goto winusb_free;
}
candle_logf(L"cap ch0 feature=0x%08x fclk=%u tseg1=%u..%u tseg2=%u..%u sjw=%u brp=%u..%u inc=%u",
dev->bt_const.feature,
dev->bt_const.fclk_can,
dev->bt_const.tseg1_min,
dev->bt_const.tseg1_max,
dev->bt_const.tseg2_min,
dev->bt_const.tseg2_max,
dev->bt_const.sjw_max,
dev->bt_const.brp_min,
dev->bt_const.brp_max,
dev->bt_const.brp_inc);
/* Query capabilities for each channel on multi-channel devices */
uint8_t num_channels = dev->dconf.icount + 1;
if (num_channels > 8) num_channels = 8;
for (uint8_t ch = 0; ch < num_channels; ch++) {
if (!candle_ctrl_get_capability(dev, ch, &dev->ch_caps[ch])) {
/* Fall back to channel 0 capabilities for this channel */
memcpy(&dev->ch_caps[ch], &dev->bt_const, sizeof(candle_capability_t));
candle_logf(L"cap ch%u failed, falling back to ch0", ch);
} else {
candle_logf(L"cap ch%u feature=0x%08x fclk=%u",
ch,
dev->ch_caps[ch].feature,
dev->ch_caps[ch].fclk_can);
}
}
/* Pre-allocate a manual-reset event for timed overlapped writes. Reusing
* one event per device (writes are serialised by writeMutex) avoids
* per-frame CreateEvent overhead at high CAN frame rates. */
dev->txEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (!dev->txEvent) {
dev->last_error = CANDLE_ERR_MALLOC;
goto winusb_free;
}
dev->last_error = CANDLE_ERR_OK;
return true;
winusb_free:
WinUsb_Free(dev->winUSBHandle);
dev->winUSBHandle = NULL;
close_handle:
CloseHandle(dev->deviceHandle);
dev->deviceHandle = NULL;
return false;
}
static bool candle_prepare_read(candle_device_t *dev, unsigned urb_num)
{
if (dev->rxurbs[urb_num].pending) {
dev->last_error = CANDLE_ERR_PREPARE_READ;
return false;
}
if (dev->rxurbs[urb_num].ovl.hEvent == NULL) {
dev->last_error = CANDLE_ERR_PREPARE_READ;
return false;
}
ResetEvent(dev->rxurbs[urb_num].ovl.hEvent);
BOOL rc = WinUsb_ReadPipe(
dev->winUSBHandle,
dev->bulkInPipe,
dev->rxurbs[urb_num].buf,
sizeof(dev->rxurbs[urb_num].buf),
NULL,
&dev->rxurbs[urb_num].ovl
);
if (rc) {
/* Synchronous completion: data is already in buf and the event is
* signaled. WaitForMultipleObjects will return immediately on the
* next call and GetOverlappedResult will succeed, so this is fine. */
dev->rxurbs[urb_num].pending = true;
dev->last_error = CANDLE_ERR_OK;
return true;
}
DWORD err = GetLastError();
if (err == ERROR_IO_PENDING) {
dev->rxurbs[urb_num].pending = true;
dev->last_error = CANDLE_ERR_OK;
return true;
}
candle_logf(L"prepare read urb=%u failed winerr=%lu", urb_num, err);
dev->last_error = CANDLE_ERR_PREPARE_READ;
return false;
}
static bool candle_close_rxurbs(candle_device_t *dev)
{
if (dev->winUSBHandle != NULL) {
WinUsb_AbortPipe(dev->winUSBHandle, dev->bulkInPipe);
}
for (unsigned i=0; i<CANDLE_URB_COUNT; i++) {
if (dev->rxurbs[i].pending) {
CancelIoEx(dev->deviceHandle, &dev->rxurbs[i].ovl);
DWORD bytes_transfered;
WinUsb_GetOverlappedResult(dev->winUSBHandle,
&dev->rxurbs[i].ovl,
&bytes_transfered,
TRUE);
dev->rxurbs[i].pending = false;
}
if (dev->rxevents[i] != NULL) {
CloseHandle(dev->rxevents[i]);
dev->rxevents[i] = NULL;
memset(&dev->rxurbs[i].ovl, 0, sizeof(dev->rxurbs[i].ovl));
}
}
return true;
}
static void candle_release_open_handles(candle_device_t *dev)
{
candle_close_rxurbs(dev);
if (dev->txEvent) {
CloseHandle(dev->txEvent);
dev->txEvent = NULL;
}
if (dev->winUSBHandle) {
WinUsb_Free(dev->winUSBHandle);
dev->winUSBHandle = NULL;
}
if (dev->deviceHandle && dev->deviceHandle != INVALID_HANDLE_VALUE) {
CloseHandle(dev->deviceHandle);
dev->deviceHandle = NULL;
}
}
bool __stdcall DLL candle_dev_open(candle_handle hdev)
{
candle_device_t *dev = (candle_device_t*)hdev;
if (candle_dev_interal_open(dev)) {
for (unsigned i=0; i<CANDLE_URB_COUNT; i++) {
HANDLE ev = CreateEvent(NULL, true, false, NULL);
if (ev == NULL) {
dev->last_error = CANDLE_ERR_MALLOC;
candle_err_t last_error = dev->last_error;
candle_release_open_handles(dev);
dev->last_error = last_error;
return false;
}
dev->rxevents[i] = ev;
dev->rxurbs[i].ovl.hEvent = ev;
if (!candle_prepare_read(dev, i)) {
candle_err_t last_error = dev->last_error;
candle_release_open_handles(dev);
dev->last_error = last_error;
return false; // keep last_error from prepare_read call
}
}
dev->last_error = CANDLE_ERR_OK;
return true;
} else {
return false; // keep last_error from open_device call
}
}
bool __stdcall DLL candle_dev_get_timestamp_us(candle_handle hdev, uint32_t *timestamp_us)
{
return candle_ctrl_get_timestamp(hdev, timestamp_us);
}
bool __stdcall DLL candle_dev_close(candle_handle hdev)
{
candle_device_t *dev = (candle_device_t*)hdev;
candle_release_open_handles(dev);
dev->last_error = CANDLE_ERR_OK;
return true;
}
bool __stdcall DLL candle_dev_free(candle_handle hdev)
{
free(hdev);
return true;
}
candle_err_t __stdcall DLL candle_dev_last_error(candle_handle hdev)
{
candle_device_t *dev = (candle_device_t*)hdev;
return dev->last_error;
}
bool __stdcall DLL candle_channel_count(candle_handle hdev, uint8_t *num_channels)
{
// TODO check if info was already read from device; try to do so; throw error...
candle_device_t *dev = (candle_device_t*)hdev;
*num_channels = dev->dconf.icount+1;
return true;
}
bool __stdcall DLL candle_channel_get_capabilities(candle_handle hdev, uint8_t ch, candle_capability_t *cap)
{
candle_device_t *dev = (candle_device_t*)hdev;
uint8_t num_channels = dev->dconf.icount + 1;
if (ch < num_channels && ch < 8) {
memcpy(cap, &dev->ch_caps[ch], sizeof(candle_capability_t));
} else {
memcpy(cap, &dev->bt_const, sizeof(candle_capability_t));
}
return true;
}
bool __stdcall DLL candle_channel_get_state(candle_handle hdev, uint8_t ch, candle_can_state_t *state)
{
candle_device_t *dev = (candle_device_t*)hdev;
candle_device_state_t ds;
if (!candle_ctrl_get_state(dev, ch, &ds)) {
return false;
}
*state = (candle_can_state_t)ds.state;
return true;
}
bool __stdcall DLL candle_channel_bus_off_recover(candle_handle hdev, uint8_t ch)
{
candle_device_t *dev = (candle_device_t*)hdev;
return candle_ctrl_bus_off_recover(dev, ch);
}
bool __stdcall DLL candle_channel_set_timing(candle_handle hdev, uint8_t ch, candle_bittiming_t *data)
{
// TODO ensure device is open, check channel count..
candle_device_t *dev = (candle_device_t*)hdev;
return candle_ctrl_set_bittiming(dev, ch, data);
}
bool __stdcall DLL candle_channel_set_bitrate(candle_handle hdev, uint8_t ch, uint32_t bitrate)
{
// TODO ensure device is open, check channel count..
candle_device_t *dev = (candle_device_t*)hdev;
if (dev->bt_const.fclk_can != 48000000) {
/* this function only works for the candleLight base clock of 48MHz */
dev->last_error = CANDLE_ERR_BITRATE_FCLK;
return false;
}
candle_bittiming_t t;
t.prop_seg = 1;
t.sjw = 1;
t.phase_seg1 = 13 - t.prop_seg;
t.phase_seg2 = 2;
switch (bitrate) {
case 10000:
t.brp = 300;
break;
case 20000:
t.brp = 150;
break;
case 50000:
t.brp = 60;
break;
case 83333:
t.brp = 36;
break;
case 100000:
t.brp = 30;
break;
case 125000:
t.brp = 24;
break;
case 250000:
t.brp = 12;
break;
case 500000:
t.brp = 6;
break;
case 800000:
t.brp = 4;
t.phase_seg1 = 12 - t.prop_seg;
t.phase_seg2 = 2;
break;
case 1000000:
t.brp = 3;
break;
default:
dev->last_error = CANDLE_ERR_BITRATE_UNSUPPORTED;
return false;
}
return candle_ctrl_set_bittiming(dev, ch, &t);
}
bool __stdcall DLL candle_channel_start(candle_handle hdev, uint8_t ch, uint32_t flags)
{
// TODO ensure device is open, check channel count..
candle_device_t *dev = (candle_device_t*)hdev;
candle_capability_t *cap = (ch < 8) ? &dev->ch_caps[ch] : &dev->bt_const;
if (cap->feature & CANDLE_FEATURE_HW_TIMESTAMP) {
flags |= CANDLE_MODE_HW_TIMESTAMP;
} else {
candle_logf(L"channel %u has no HW timestamp capability; starting without timestamp flag", ch);
}
bool rc = candle_ctrl_set_device_mode(dev, ch, CANDLE_DEVMODE_START, flags);
candle_logf(L"channel %u start flags=0x%08x result=%u err=%u",
ch,
flags,
rc ? 1 : 0,
dev->last_error);
return rc;
}
bool __stdcall DLL candle_channel_stop(candle_handle hdev, uint8_t ch)
{
// TODO ensure device is open, check channel count..
candle_device_t *dev = (candle_device_t*)hdev;
return candle_ctrl_set_device_mode(dev, ch, CANDLE_DEVMODE_RESET, 0);
}
/* Write len bytes from buf to the OUT pipe, aborting after 300 ms.
* Writes are serialised by writeMutex in CandleApiInterface so dev->txEvent
* is never accessed by two threads simultaneously. */
static bool candle_write_pipe_timed(candle_device_t *dev, uint8_t *buf, DWORD len)
{
OVERLAPPED ovl;
memset(&ovl, 0, sizeof(ovl));
ovl.hEvent = dev->txEvent;
ResetEvent(dev->txEvent);
BOOL rc = WinUsb_WritePipe(dev->winUSBHandle, dev->bulkOutPipe,
buf, len, NULL, &ovl);
if (rc) {
return true; /* completed synchronously */
}
if (GetLastError() != ERROR_IO_PENDING) {
return false; /* hard error */
}
if (WaitForSingleObject(dev->txEvent, 150) != WAIT_OBJECT_0) {
/* Timed out: cancel the transfer and restore the pipe to a clean state. */
WinUsb_AbortPipe(dev->winUSBHandle, dev->bulkOutPipe);
DWORD dummy = 0;
WinUsb_GetOverlappedResult(dev->winUSBHandle, &ovl, &dummy, TRUE);
WinUsb_ResetPipe(dev->winUSBHandle, dev->bulkOutPipe);
return false;
}
DWORD transferred = 0;
return WinUsb_GetOverlappedResult(dev->winUSBHandle, &ovl, &transferred, FALSE) != FALSE;
}
bool __stdcall DLL candle_frame_send(candle_handle hdev, uint8_t ch, candle_frame_t *frame)
{
candle_device_t *dev = (candle_device_t*)hdev;
frame->echo_id = 0;
frame->channel = ch;
bool rc = candle_write_pipe_timed(dev, (uint8_t*)frame, sizeof(*frame));
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SEND_FRAME;
return rc;
}
bool __stdcall DLL candle_frame_read(candle_handle hdev, candle_frame_t *frame, uint32_t timeout_ms)
{
// TODO ensure device is open..
candle_device_t *dev = (candle_device_t*)hdev;
DWORD wait_result = WaitForMultipleObjects(CANDLE_URB_COUNT, dev->rxevents, false, timeout_ms);
if (wait_result == WAIT_TIMEOUT) {
dev->last_error = CANDLE_ERR_READ_TIMEOUT;
return false;
}
if ( (wait_result < WAIT_OBJECT_0) || (wait_result >= WAIT_OBJECT_0 + CANDLE_URB_COUNT) ) {
dev->last_error = CANDLE_ERR_READ_WAIT;
return false;
}
DWORD urb_num = wait_result - WAIT_OBJECT_0;
DWORD bytes_transfered;
if (!WinUsb_GetOverlappedResult(dev->winUSBHandle, &dev->rxurbs[urb_num].ovl, &bytes_transfered, false)) {
DWORD err = GetLastError();
if (err == ERROR_IO_INCOMPLETE) {
ResetEvent(dev->rxurbs[urb_num].ovl.hEvent);
} else {
dev->rxurbs[urb_num].pending = false;
candle_prepare_read(dev, urb_num);
}
candle_logf(L"classic read result failed urb=%u winerr=%lu", urb_num, err);
dev->last_error = CANDLE_ERR_READ_RESULT;
return false;
}
dev->rxurbs[urb_num].pending = false;
if (bytes_transfered < sizeof(*frame)-4) {
candle_prepare_read(dev, urb_num);
candle_logf(L"classic read too small urb=%u bytes=%lu min=%u",
urb_num,
bytes_transfered,
(unsigned)(sizeof(*frame) - 4));
dev->last_error = CANDLE_ERR_READ_SIZE;
return false;
}
memset(frame, 0, sizeof(*frame));
DWORD copy_len = (bytes_transfered < sizeof(*frame)) ? bytes_transfered : sizeof(*frame);
memcpy(frame, dev->rxurbs[urb_num].buf, copy_len);
candle_logf_verbose(L"classic read urb=%u bytes=%lu echo=0x%08x can_id=0x%08x dlc=%u ch=%u flags=0x%02x ts=%u",
urb_num,
bytes_transfered,
frame->echo_id,
frame->can_id,
frame->can_dlc,
frame->channel,
frame->flags,
frame->timestamp_us);
return candle_prepare_read(dev, urb_num);
}
candle_frametype_t __stdcall DLL candle_frame_type(candle_frame_t *frame)
{
if (frame->echo_id != 0xFFFFFFFF) {
return CANDLE_FRAMETYPE_ECHO;
};
if (frame->can_id & CANDLE_ID_ERR) {
return CANDLE_FRAMETYPE_ERROR;
}
return CANDLE_FRAMETYPE_RECEIVE;
}
uint32_t __stdcall DLL candle_frame_id(candle_frame_t *frame)
{
return frame->can_id & 0x1FFFFFFF;
}
bool __stdcall DLL candle_frame_is_extended_id(candle_frame_t *frame)
{
return (frame->can_id & CANDLE_ID_EXTENDED) != 0;
}
bool __stdcall DLL candle_frame_is_rtr(candle_frame_t *frame)
{
return (frame->can_id & CANDLE_ID_RTR) != 0;
}
uint8_t __stdcall DLL candle_frame_dlc(candle_frame_t *frame)
{
return frame->can_dlc;
}
uint8_t * __stdcall DLL candle_frame_data(candle_frame_t *frame)
{
return frame->data;
}
uint32_t __stdcall DLL candle_frame_timestamp_us(candle_frame_t *frame)
{
return frame->timestamp_us;
}
/* ---- CAN FD extensions ---- */
bool __stdcall DLL candle_channel_set_data_timing(candle_handle hdev, uint8_t ch, candle_bittiming_t *data)
{
candle_device_t *dev = (candle_device_t*)hdev;
return candle_ctrl_set_data_bittiming(dev, ch, data);
}
bool __stdcall DLL candle_fd_frame_send(candle_handle hdev, uint8_t ch, candle_fd_frame_t *frame)
{
candle_device_t *dev = (candle_device_t*)hdev;
frame->echo_id = 0;
frame->channel = ch;
bool rc = candle_write_pipe_timed(dev, (uint8_t*)frame, sizeof(*frame));
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SEND_FRAME;
return rc;
}
bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *frame, uint32_t timeout_ms)
{
candle_device_t *dev = (candle_device_t*)hdev;
DWORD wait_result = WaitForMultipleObjects(CANDLE_URB_COUNT, dev->rxevents, false, timeout_ms);
if (wait_result == WAIT_TIMEOUT) {
dev->last_error = CANDLE_ERR_READ_TIMEOUT;
return false;
}
if ( (wait_result < WAIT_OBJECT_0) || (wait_result >= WAIT_OBJECT_0 + CANDLE_URB_COUNT) ) {
dev->last_error = CANDLE_ERR_READ_WAIT;
return false;
}
DWORD urb_num = wait_result - WAIT_OBJECT_0;
DWORD bytes_transfered;
if (!WinUsb_GetOverlappedResult(dev->winUSBHandle, &dev->rxurbs[urb_num].ovl, &bytes_transfered, false)) {
DWORD err = GetLastError();
if (err == ERROR_IO_INCOMPLETE) {
ResetEvent(dev->rxurbs[urb_num].ovl.hEvent);
} else {
dev->rxurbs[urb_num].pending = false;
candle_prepare_read(dev, urb_num);
}
candle_logf(L"fd read result failed urb=%u winerr=%lu", urb_num, err);
dev->last_error = CANDLE_ERR_READ_RESULT;
return false;
}
dev->rxurbs[urb_num].pending = false;
/* Minimum: classic CAN header (12 bytes) + at least 8 data bytes = 20 bytes */
static const DWORD classic_min = sizeof(candle_frame_t) - 4;
if (bytes_transfered < classic_min) {
candle_prepare_read(dev, urb_num);
candle_logf(L"fd read too small urb=%u bytes=%lu min=%lu",
urb_num,
bytes_transfered,
classic_min);
dev->last_error = CANDLE_ERR_READ_SIZE;
return false;
}
memset(frame, 0, sizeof(*frame));
/*
* Detect frame type from the flags byte (offset 10 in both structs).
* Classic CAN frames: header(12) + data(8) + timestamp(4) = 24 bytes total.
*
* FD frames come in two wire formats:
* - Legacy fixed (candleLight/CANable 1.x): always 80 bytes — header(12) +
* data[64] + timestamp(4). The timestamp is ALWAYS at offset 76, regardless
* of the actual DLC. Identified by bytes_transferred == sizeof(candle_fd_frame_t).
* - Variable-length (CANnectivity/Zephyr): header(12) + actual_data(DLC) +
* timestamp(4). Identified by bytes_transferred < sizeof(candle_fd_frame_t).
*/
bool is_fd_frame = (dev->rxurbs[urb_num].buf[10] & CANDLE_FRAME_FLAG_FD) != 0;
if (is_fd_frame) {
/* can_dlc is at byte offset 8 in both classic and FD wire frames. */
const uint8_t raw_dlc = dev->rxurbs[urb_num].buf[8];
const DWORD data_len = candle_dlc_to_len(raw_dlc);
const DWORD min_size = 12 + data_len; /* header + data, without timestamp */
if (bytes_transfered < min_size) {
candle_prepare_read(dev, urb_num);
candle_logf(L"fd read FD frame too small urb=%u bytes=%lu min=%lu flags=0x%02x dlc=%u",
urb_num,
bytes_transfered,
min_size,
dev->rxurbs[urb_num].buf[10],
raw_dlc);
dev->last_error = CANDLE_ERR_READ_SIZE;
return false;
}
/* Copy the fixed 12-byte header (echo_id … reserved). */
memcpy(frame, dev->rxurbs[urb_num].buf, 12);
/* Copy data at offset 12 into the struct's data field. */
memcpy(frame->data, dev->rxurbs[urb_num].buf + 12, data_len);
/* Timestamp location depends on the wire format (see comment above). */
const DWORD fixed_ts_offset = (DWORD)(sizeof(candle_fd_frame_t) - sizeof(uint32_t)); /* = 76 */
const DWORD ts_offset = (bytes_transfered >= (DWORD)sizeof(candle_fd_frame_t))
? fixed_ts_offset
: min_size;
if (bytes_transfered >= ts_offset + (DWORD)sizeof(uint32_t)) {
memcpy(&frame->timestamp_us, dev->rxurbs[urb_num].buf + ts_offset, sizeof(uint32_t));
}
/* else: timestamp stays zero from memset above */
} else {
/* Classic CAN frame — copy into FD struct, fixing the timestamp position */
candle_frame_t classic;
DWORD copy_len = (bytes_transfered < sizeof(classic)) ? bytes_transfered : sizeof(classic);
memcpy(&classic, dev->rxurbs[urb_num].buf, copy_len);
frame->echo_id = classic.echo_id;
frame->can_id = classic.can_id;
frame->can_dlc = classic.can_dlc;
frame->channel = classic.channel;
frame->flags = classic.flags;
frame->reserved = classic.reserved;
memcpy(frame->data, classic.data, 8);
frame->timestamp_us = (bytes_transfered >= sizeof(classic)) ? classic.timestamp_us : 0;
}
candle_logf_verbose(L"fd read urb=%u bytes=%lu is_fd=%u echo=0x%08x can_id=0x%08x dlc=%u ch=%u flags=0x%02x ts=%u",
urb_num,
bytes_transfered,
is_fd_frame ? 1 : 0,
frame->echo_id,
frame->can_id,
frame->can_dlc,
frame->channel,
frame->flags,
frame->timestamp_us);
return candle_prepare_read(dev, urb_num);
}
candle_frametype_t __stdcall DLL candle_fd_frame_type(candle_fd_frame_t *frame)
{
if (frame->echo_id != 0xFFFFFFFF) {
return CANDLE_FRAMETYPE_ECHO;
}
if (frame->can_id & CANDLE_ID_ERR) {
return CANDLE_FRAMETYPE_ERROR;
}
return CANDLE_FRAMETYPE_RECEIVE;
}
uint32_t __stdcall DLL candle_fd_frame_id(candle_fd_frame_t *frame)
{
return frame->can_id & 0x1FFFFFFF;
}
bool __stdcall DLL candle_fd_frame_is_extended_id(candle_fd_frame_t *frame)
{
return (frame->can_id & CANDLE_ID_EXTENDED) != 0;
}
bool __stdcall DLL candle_fd_frame_is_rtr(candle_fd_frame_t *frame)
{
return (frame->can_id & CANDLE_ID_RTR) != 0;
}
bool __stdcall DLL candle_fd_frame_is_fd(candle_fd_frame_t *frame)
{
return (frame->flags & CANDLE_FRAME_FLAG_FD) != 0;
}
bool __stdcall DLL candle_fd_frame_is_brs(candle_fd_frame_t *frame)
{
return (frame->flags & CANDLE_FRAME_FLAG_BRS) != 0;
}
uint8_t __stdcall DLL candle_fd_frame_dlc(candle_fd_frame_t *frame)
{
return frame->can_dlc;
}
uint8_t * __stdcall DLL candle_fd_frame_data(candle_fd_frame_t *frame)
{
return frame->data;
}
uint32_t __stdcall DLL candle_fd_frame_timestamp_us(candle_fd_frame_t *frame)
{
return frame->timestamp_us;
}

29
c/candle/candle.def Normal file
View File

@@ -0,0 +1,29 @@
EXPORTS
candle_list_scan
candle_list_free
candle_list_length
candle_dev_get
candle_dev_get_state
candle_dev_get_path
candle_dev_open
candle_dev_get_timestamp_us
candle_dev_close
candle_dev_free
candle_channel_count
candle_channel_get_capabilities
candle_channel_get_state
candle_channel_bus_off_recover
candle_channel_set_timing
candle_channel_set_bitrate
candle_channel_start
candle_channel_stop
candle_frame_send
candle_frame_read
candle_frame_type
candle_frame_id
candle_frame_is_extended_id
candle_frame_is_rtr
candle_frame_dlc
candle_frame_data
candle_frame_timestamp_us
candle_dev_last_error

287
c/candle/candle.h Normal file
View File

@@ -0,0 +1,287 @@
/*
Copyright (c) 2016 Hubert Denkmair <hubert@denkmair.de>
Copyright (c) 2026 Schildkroet
This file is part of the candle windows API.
This library is free software: you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation, either
version 3 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef void* candle_list_handle;
typedef void* candle_handle;
typedef enum {
CANDLE_DEVSTATE_AVAIL,
CANDLE_DEVSTATE_INUSE
} candle_devstate_t;
typedef enum {
CANDLE_FRAMETYPE_UNKNOWN,
CANDLE_FRAMETYPE_RECEIVE,
CANDLE_FRAMETYPE_ECHO,
CANDLE_FRAMETYPE_ERROR,
CANDLE_FRAMETYPE_TIMESTAMP_OVFL
} candle_frametype_t;
enum {
CANDLE_ID_EXTENDED = 0x80000000,
CANDLE_ID_RTR = 0x40000000,
CANDLE_ID_ERR = 0x20000000
};
/* Feature flags reported in candle_capability_t.feature */
enum {
/** CAN channel supports listen-only mode, in which it is not allowed to send dominant bits. */
CANDLE_FEATURE_LISTEN_ONLY = 0x0001,
/** CAN channel supports loopback mode, in which it receives own frames. */
CANDLE_FEATURE_LOOP_BACK = 0x0002,
/** CAN channel supports triple sampling mode */
CANDLE_FEATURE_TRIPLE_SAMPLE = 0x0004,
/** CAN channel supports not retransmitting in case of lost arbitration or missing ACK. */
CANDLE_FEATURE_ONE_SHOT = 0x0008,
/** CAN channel supports hardware timestamping of CAN frames. */
CANDLE_FEATURE_HW_TIMESTAMP = 0x0010,
/** CAN channel supports visual identification. */
CANDLE_FEATURE_IDENTIFY = 0x0020,
/** CAN channel supports user IDs (unsupported). */
CANDLE_FEATURE_USER_ID = 0x0040,
/** CAN channel supports padding of host frames (unsupported). */
CANDLE_FEATURE_PAD_PKTS_TO_MAX = 0x0080,
/** CAN channel supports transmitting/receiving CAN FD frames. */
CANDLE_FEATURE_FD = 0x0100,
/** CAN channel support LPC546xx specific quirks (Unused) */
CANDLE_FEATURE_REQ_USB_QUIRK_LPC546XX = 0x0200,
/** CAN channel supports extended bit timing limits. */
CANDLE_FEATURE_BT_CONST_EXT = 0x0400,
/** CAN channel supports configurable bus termination. */
CANDLE_FEATURE_TERMINATION = 0x0800,
/** CAN channel supports bus error reporting (Unsupported, always enabled) */
CANDLE_FEATURE_BERR_REPORTING = 0x1000,
/** CAN channel supports reporting of bus state. */
CANDLE_FEATURE_GET_STATE = 0x2000,
/** Host-controlled recovery after the controller enters bus-off. */
CANDLE_FEATURE_BUS_OFF_RECOVERY = 0x40000,
};
/* Flags in the flags byte of received/transmitted frames */
enum {
CANDLE_FRAME_FLAG_OVERFLOW = 0x01,
CANDLE_FRAME_FLAG_FD = 0x02,
CANDLE_FRAME_FLAG_BRS = 0x04,
CANDLE_FRAME_FLAG_ESI = 0x08,
};
typedef enum {
CANDLE_STATE_ERROR_ACTIVE = 0,
CANDLE_STATE_ERROR_WARNING = 1,
CANDLE_STATE_ERROR_PASSIVE = 2,
CANDLE_STATE_BUS_OFF = 3,
CANDLE_STATE_STOPPED = 4,
CANDLE_STATE_SLEEPING = 5,
} candle_can_state_t;
typedef enum {
CANDLE_MODE_NORMAL = 0x0000,
CANDLE_MODE_LISTEN_ONLY = 0x0001,
CANDLE_MODE_LOOP_BACK = 0x0002,
CANDLE_MODE_TRIPLE_SAMPLE = 0x0004,
CANDLE_MODE_ONE_SHOT = 0x0008,
CANDLE_MODE_HW_TIMESTAMP = 0x0010,
CANDLE_MODE_PAD_PKTS_TO_MAX = 0x0080,
CANDLE_MODE_FD = 0x0100,
} candle_mode_t;
typedef enum {
CANDLE_ERR_OK = 0,
CANDLE_ERR_CREATE_FILE = 1,
CANDLE_ERR_WINUSB_INITIALIZE = 2,
CANDLE_ERR_QUERY_INTERFACE = 3,
CANDLE_ERR_QUERY_PIPE = 4,
CANDLE_ERR_PARSE_IF_DESCR = 5,
CANDLE_ERR_SET_HOST_FORMAT = 6,
CANDLE_ERR_GET_DEVICE_INFO = 7,
CANDLE_ERR_GET_BITTIMING_CONST = 8,
CANDLE_ERR_PREPARE_READ = 9,
CANDLE_ERR_SET_DEVICE_MODE = 10,
CANDLE_ERR_SET_BITTIMING = 11,
CANDLE_ERR_BITRATE_FCLK = 12,
CANDLE_ERR_BITRATE_UNSUPPORTED = 13,
CANDLE_ERR_SEND_FRAME = 14,
CANDLE_ERR_READ_TIMEOUT = 15,
CANDLE_ERR_READ_WAIT = 16,
CANDLE_ERR_READ_RESULT = 17,
CANDLE_ERR_READ_SIZE = 18,
CANDLE_ERR_SETUPDI_IF_DETAILS = 19,
CANDLE_ERR_SETUPDI_IF_DETAILS2 = 20,
CANDLE_ERR_MALLOC = 21,
CANDLE_ERR_PATH_LEN = 22,
CANDLE_ERR_CLSID = 23,
CANDLE_ERR_GET_DEVICES = 24,
CANDLE_ERR_SETUPDI_IF_ENUM = 25,
CANDLE_ERR_SET_TIMESTAMP_MODE = 26,
CANDLE_ERR_DEV_OUT_OF_RANGE = 27,
CANDLE_ERR_GET_TIMESTAMP = 28,
CANDLE_ERR_SET_PIPE_RAW_IO = 29
} candle_err_t;
#pragma pack(push,1)
typedef struct {
uint32_t echo_id;
uint32_t can_id;
uint8_t can_dlc;
uint8_t channel;
uint8_t flags;
uint8_t reserved;
uint8_t data[8];
uint32_t timestamp_us;
} candle_frame_t;
/* CAN FD frame: same header as candle_frame_t but with 64-byte data payload */
typedef struct {
uint32_t echo_id;
uint32_t can_id;
uint8_t can_dlc;
uint8_t channel;
uint8_t flags;
uint8_t reserved;
uint8_t data[64];
uint32_t timestamp_us;
} candle_fd_frame_t;
typedef struct {
uint32_t feature;
uint32_t fclk_can;
uint32_t tseg1_min;
uint32_t tseg1_max;
uint32_t tseg2_min;
uint32_t tseg2_max;
uint32_t sjw_max;
uint32_t brp_min;
uint32_t brp_max;
uint32_t brp_inc;
} candle_capability_t;
typedef struct {
uint32_t prop_seg;
uint32_t phase_seg1;
uint32_t phase_seg2;
uint32_t sjw;
uint32_t brp;
} candle_bittiming_t;
#pragma pack(pop)
/*
* CAN FD DLC encoding:
* DLC 0-8 → 0-8 bytes (same as classic CAN)
* DLC 9 → 12 bytes
* DLC 10 → 16 bytes
* DLC 11 → 20 bytes
* DLC 12 → 24 bytes
* DLC 13 → 32 bytes
* DLC 14 → 48 bytes
* DLC 15 → 64 bytes
*/
static inline uint8_t candle_dlc_to_len(uint8_t dlc)
{
static const uint8_t tbl[16] = { 0,1,2,3,4,5,6,7,8,12,16,20,24,32,48,64 };
return (dlc <= 15u) ? tbl[dlc] : 0u;
}
static inline uint8_t candle_len_to_dlc(uint8_t len)
{
if (len <= 8u) return len;
if (len <= 12u) return 9u;
if (len <= 16u) return 10u;
if (len <= 20u) return 11u;
if (len <= 24u) return 12u;
if (len <= 32u) return 13u;
if (len <= 48u) return 14u;
return 15u;
}
#define DLL
/* Optional log callback — set once at startup to receive diagnostic messages.
* If NULL (the default) no logging is performed. */
typedef void (*candle_log_fn_t)(const wchar_t *msg);
extern candle_log_fn_t candle_log_fn;
/* Set to true to enable per-frame and per-control-transfer trace logs.
* Off by default; only error and setup messages are logged. */
extern bool candle_log_verbose;
bool __stdcall DLL candle_list_scan(candle_list_handle *list);
bool __stdcall DLL candle_list_free(candle_list_handle list);
bool __stdcall DLL candle_list_length(candle_list_handle list, uint8_t *len);
bool __stdcall DLL candle_dev_get(candle_list_handle list, uint8_t dev_num, candle_handle *hdev);
bool __stdcall DLL candle_dev_get_state(candle_handle hdev, candle_devstate_t *state);
wchar_t * __stdcall DLL candle_dev_get_path(candle_handle hdev);
bool __stdcall DLL candle_dev_open(candle_handle hdev);
bool __stdcall DLL candle_dev_get_timestamp_us(candle_handle hdev, uint32_t *timestamp_us);
bool __stdcall DLL candle_dev_close(candle_handle hdev);
bool __stdcall DLL candle_dev_free(candle_handle hdev);
bool __stdcall DLL candle_channel_count(candle_handle hdev, uint8_t *num_channels);
bool __stdcall DLL candle_channel_get_capabilities(candle_handle hdev, uint8_t ch, candle_capability_t *cap);
bool __stdcall DLL candle_channel_get_state(candle_handle hdev, uint8_t ch, candle_can_state_t *state);
bool __stdcall DLL candle_channel_bus_off_recover(candle_handle hdev, uint8_t ch);
bool __stdcall DLL candle_channel_set_timing(candle_handle hdev, uint8_t ch, candle_bittiming_t *data);
bool __stdcall DLL candle_channel_set_bitrate(candle_handle hdev, uint8_t ch, uint32_t bitrate);
bool __stdcall DLL candle_channel_start(candle_handle hdev, uint8_t ch, uint32_t flags);
bool __stdcall DLL candle_channel_stop(candle_handle hdev, uint8_t ch);
bool __stdcall DLL candle_frame_send(candle_handle hdev, uint8_t ch, candle_frame_t *frame);
bool __stdcall DLL candle_frame_read(candle_handle hdev, candle_frame_t *frame, uint32_t timeout_ms);
candle_frametype_t __stdcall DLL candle_frame_type(candle_frame_t *frame);
uint32_t __stdcall DLL candle_frame_id(candle_frame_t *frame);
bool __stdcall DLL candle_frame_is_extended_id(candle_frame_t *frame);
bool __stdcall DLL candle_frame_is_rtr(candle_frame_t *frame);
uint8_t __stdcall DLL candle_frame_dlc(candle_frame_t *frame);
uint8_t * __stdcall DLL candle_frame_data(candle_frame_t *frame);
uint32_t __stdcall DLL candle_frame_timestamp_us(candle_frame_t *frame);
/* CAN FD extensions */
bool __stdcall DLL candle_channel_set_data_timing(candle_handle hdev, uint8_t ch, candle_bittiming_t *data);
bool __stdcall DLL candle_fd_frame_send(candle_handle hdev, uint8_t ch, candle_fd_frame_t *frame);
bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *frame, uint32_t timeout_ms);
candle_frametype_t __stdcall DLL candle_fd_frame_type(candle_fd_frame_t *frame);
uint32_t __stdcall DLL candle_fd_frame_id(candle_fd_frame_t *frame);
bool __stdcall DLL candle_fd_frame_is_extended_id(candle_fd_frame_t *frame);
bool __stdcall DLL candle_fd_frame_is_rtr(candle_fd_frame_t *frame);
bool __stdcall DLL candle_fd_frame_is_fd(candle_fd_frame_t *frame);
bool __stdcall DLL candle_fd_frame_is_brs(candle_fd_frame_t *frame);
uint8_t __stdcall DLL candle_fd_frame_dlc(candle_fd_frame_t *frame);
uint8_t * __stdcall DLL candle_fd_frame_data(candle_fd_frame_t *frame);
uint32_t __stdcall DLL candle_fd_frame_timestamp_us(candle_fd_frame_t *frame);
candle_err_t __stdcall DLL candle_dev_last_error(candle_handle hdev);
#ifdef __cplusplus
}
#endif

234
c/candle/candle_ctrl_req.c Normal file
View File

@@ -0,0 +1,234 @@
/*
Copyright (c) 2016 Hubert Denkmair <hubert@denkmair.de>
This file is part of the candle windows API.
This library is free software: you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation, either
version 3 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#include "candle_ctrl_req.h"
#include "ch_9.h"
#include <stdarg.h>
enum {
CANDLE_BREQ_HOST_FORMAT = 0,
CANDLE_BREQ_BITTIMING = 1,
CANDLE_BREQ_MODE = 2,
CANDLE_BREQ_BERR = 3,
CANDLE_BREQ_BT_CONST = 4,
CANDLE_BREQ_DEVICE_CONFIG = 5,
CANDLE_TIMESTAMP_GET = 6,
/* 7: IDENTIFY, 8: GET_USER_ID, 9: SET_USER_ID (not used here) */
CANDLE_BREQ_DATA_BITTIMING = 10,
/* 11: SET_TERMINATION, not used here */
CANDLE_BREQ_GET_STATE = 12,
CANDLE_BREQ_BUS_OFF_RECOVERY = 32,
};
static void candle_ctrl_logf(const wchar_t *fmt, ...)
{
if (candle_log_fn == NULL || !candle_log_verbose) {
return;
}
wchar_t buf[512];
va_list args;
va_start(args, fmt);
HRESULT hr = StringCchVPrintfW(buf, 512, fmt, args);
va_end(args);
if (SUCCEEDED(hr)) {
candle_log_fn(buf);
}
}
static bool usb_control_msg(WINUSB_INTERFACE_HANDLE hnd, uint8_t request, uint8_t requesttype, uint16_t value, uint16_t index, void *data, uint16_t size)
{
WINUSB_SETUP_PACKET packet;
memset(&packet, 0, sizeof(packet));
packet.Request = request;
packet.RequestType = requesttype;
packet.Value = value;
packet.Index = index;
packet.Length = size;
unsigned long bytes_sent = 0;
BOOL rc = WinUsb_ControlTransfer(hnd, packet, (uint8_t*)data, size, &bytes_sent, 0);
candle_ctrl_logf(L"ctrl req=0x%02x type=0x%02x value=%u index=%u size=%u rc=%u transferred=%lu winerr=%lu",
request,
requesttype,
value,
index,
size,
rc ? 1 : 0,
bytes_sent,
rc ? 0 : GetLastError());
return rc;
}
bool candle_ctrl_set_host_format(candle_device_t *dev)
{
candle_host_config_t hconf;
hconf.byte_order = 0x0000beef;
bool rc = usb_control_msg(
dev->winUSBHandle,
CANDLE_BREQ_HOST_FORMAT,
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
1,
dev->interfaceNumber,
&hconf,
sizeof(hconf)
);
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_HOST_FORMAT;
return rc;
}
bool candle_ctrl_set_device_mode(candle_device_t *dev, uint8_t channel, uint32_t mode, uint32_t flags)
{
candle_device_mode_t dm;
dm.mode = mode;
dm.flags = flags;
bool rc = usb_control_msg(
dev->winUSBHandle,
CANDLE_BREQ_MODE,
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
channel,
dev->interfaceNumber,
&dm,
sizeof(dm)
);
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_DEVICE_MODE;
return rc;
}
bool candle_ctrl_get_config(candle_device_t *dev, candle_device_config_t *dconf)
{
bool rc = usb_control_msg(
dev->winUSBHandle,
CANDLE_BREQ_DEVICE_CONFIG,
USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
1,
dev->interfaceNumber,
dconf,
sizeof(*dconf)
);
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_DEVICE_INFO;
return rc;
}
bool candle_ctrl_get_timestamp(candle_device_t *dev, uint32_t *current_timestamp)
{
bool rc = usb_control_msg(
dev->winUSBHandle,
CANDLE_TIMESTAMP_GET,
USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
1,
dev->interfaceNumber,
current_timestamp,
sizeof(*current_timestamp)
);
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_TIMESTAMP;
return rc;
}
bool candle_ctrl_get_capability(candle_device_t *dev, uint8_t channel, candle_capability_t *data)
{
bool rc = usb_control_msg(
dev->winUSBHandle,
CANDLE_BREQ_BT_CONST,
USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
channel,
0,
data,
sizeof(*data)
);
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_BITTIMING_CONST;
return rc;
}
bool candle_ctrl_set_bittiming(candle_device_t *dev, uint8_t channel, candle_bittiming_t *data)
{
bool rc = usb_control_msg(
dev->winUSBHandle,
CANDLE_BREQ_BITTIMING,
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
channel,
0,
data,
sizeof(*data)
);
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_BITTIMING;
return rc;
}
bool candle_ctrl_set_data_bittiming(candle_device_t *dev, uint8_t channel, candle_bittiming_t *data)
{
bool rc = usb_control_msg(
dev->winUSBHandle,
CANDLE_BREQ_DATA_BITTIMING,
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
channel,
0,
data,
sizeof(*data)
);
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_BITTIMING;
return rc;
}
bool candle_ctrl_get_state(candle_device_t *dev, uint8_t channel, candle_device_state_t *data)
{
bool rc = usb_control_msg(
dev->winUSBHandle,
CANDLE_BREQ_GET_STATE,
USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
channel,
dev->interfaceNumber,
data,
sizeof(*data)
);
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_DEVICE_INFO;
return rc;
}
bool candle_ctrl_bus_off_recover(candle_device_t *dev, uint8_t channel)
{
bool rc = usb_control_msg(
dev->winUSBHandle,
CANDLE_BREQ_BUS_OFF_RECOVERY,
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
channel,
dev->interfaceNumber,
NULL,
0
);
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_DEVICE_MODE;
return rc;
}

View File

@@ -0,0 +1,49 @@
/*
Copyright (c) 2016 Hubert Denkmair <hubert@denkmair.de>
Copyright (c) 2026 Schildkroet
This file is part of the candle windows API.
This library is free software: you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation, either
version 3 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "candle_defs.h"
enum {
CANDLE_DEVMODE_RESET = 0,
CANDLE_DEVMODE_START = 1
};
#pragma pack(push, 1)
typedef struct {
uint32_t state;
uint32_t rxerr;
uint32_t txerr;
} candle_device_state_t;
#pragma pack(pop)
bool candle_ctrl_set_host_format(candle_device_t *dev);
bool candle_ctrl_set_device_mode(candle_device_t *dev, uint8_t channel, uint32_t mode, uint32_t flags);
bool candle_ctrl_get_config(candle_device_t *dev, candle_device_config_t *dconf);
bool candle_ctrl_get_capability(candle_device_t *dev, uint8_t channel, candle_capability_t *data);
bool candle_ctrl_set_bittiming(candle_device_t *dev, uint8_t channel, candle_bittiming_t *data);
bool candle_ctrl_set_data_bittiming(candle_device_t *dev, uint8_t channel, candle_bittiming_t *data);
bool candle_ctrl_get_timestamp(candle_device_t *dev, uint32_t *current_timestamp);
bool candle_ctrl_get_state(candle_device_t *dev, uint8_t channel, candle_device_state_t *data);
bool candle_ctrl_bus_off_recover(candle_device_t *dev, uint8_t channel);

101
c/candle/candle_defs.h Normal file
View File

@@ -0,0 +1,101 @@
/*
Copyright (c) 2016 Hubert Denkmair <hubert@denkmair.de>
Copyright (c) 2026 Schildkroet
This file is part of the candle windows API.
This library is free software: you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation, either
version 3 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <stdint.h>
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0600
#endif
#include <windows.h>
#include <winbase.h>
#include <winusb.h>
#include <setupapi.h>
#include <devguid.h>
#include <regstr.h>
#undef __CRT__NO_INLINE
#include <strsafe.h>
#define __CRT__NO_INLINE
#include "candle.h"
#define CANDLE_MAX_DEVICES 32
#define CANDLE_URB_COUNT 30
#pragma pack(push,1)
typedef struct {
uint32_t byte_order;
} candle_host_config_t;
typedef struct {
uint8_t reserved1;
uint8_t reserved2;
uint8_t reserved3;
uint8_t icount;
uint32_t sw_version;
uint32_t hw_version;
} candle_device_config_t;
typedef struct {
uint32_t mode;
uint32_t flags;
} candle_device_mode_t;
#pragma pack(pop)
typedef struct {
OVERLAPPED ovl;
bool pending;
uint8_t buf[512];
} canlde_rx_urb;
typedef struct {
wchar_t path[256];
candle_devstate_t state;
candle_err_t last_error;
HANDLE deviceHandle;
WINUSB_INTERFACE_HANDLE winUSBHandle;
UCHAR interfaceNumber;
UCHAR bulkInPipe;
UCHAR bulkOutPipe;
HANDLE txEvent; /* pre-allocated event for timed overlapped writes */
candle_device_config_t dconf;
candle_capability_t bt_const;
/* Per-channel capabilities: index 0..dconf.icount, maximum 8 channels */
candle_capability_t ch_caps[8];
canlde_rx_urb rxurbs[CANDLE_URB_COUNT];
HANDLE rxevents[CANDLE_URB_COUNT];
} candle_device_t;
typedef struct {
uint8_t num_devices;
candle_err_t last_error;
candle_device_t dev[CANDLE_MAX_DEVICES];
} candle_list_t;

37
c/candle/ch_9.h Normal file
View File

@@ -0,0 +1,37 @@
/*
Copyright (c) 2016 Hubert Denkmair <hubert@denkmair.de>
This file is part of the candle windows API.
This library is free software: you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation, either
version 3 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#define USB_DIR_OUT 0 /* to device */
#define USB_DIR_IN 0x80 /* to host */
#define USB_TYPE_MASK (0x03 << 5)
#define USB_TYPE_STANDARD (0x00 << 5)
#define USB_TYPE_CLASS (0x01 << 5)
#define USB_TYPE_VENDOR (0x02 << 5)
#define USB_TYPE_RESERVED (0x03 << 5)
#define USB_RECIP_MASK 0x1f
#define USB_RECIP_DEVICE 0x00
#define USB_RECIP_INTERFACE 0x01
#define USB_RECIP_ENDPOINT 0x02
#define USB_RECIP_OTHER 0x03

View File

@@ -14,4 +14,9 @@ if(DS18B20_DS2480_BUILD_TESTS)
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)
add_executable(test_ds2480_stm32f4
ports/stm32f4/ds2480_stm32f4_hal.c ports/stm32f4/tests/test_port.c)
target_include_directories(test_ds2480_stm32f4 PRIVATE
. ports/stm32f4 ports/stm32f4/tests)
add_test(NAME ds2480_stm32f4 COMMAND test_ds2480_stm32f4)
endif()

View File

@@ -94,10 +94,10 @@ UART-обмен. EEPROM использует выдержку 12 мс, её со
импульс, и остаётся неготовым. При отказе самого порта снятие питания гарантировать
невозможно; восстановление UART/моста остаётся задачей приложения.
Используется стандартная скорость 1-Wire и только Command Mode: байт передаётся
восьмью командами Single Bit. Поэтому любые значения, включая `E3`, передаются
без экранирования. Это простая реализация для опроса температуры; повышенные
скорости UART, Overdrive и Search Accelerator пока не реализованы. Параметры
Используется стандартная скорость 1-Wire, UART 9600 бод. Байты передаются в
Data Mode с экранированием E3, reset/поиск/strong pullup — в Command Mode.
Переключения режима выполняет библиотека. Повышенные скорости UART,
Overdrive и Search Accelerator пока не реализованы. Параметры
таймингов длинной линии остаются заводскими. Аппаратная проверка обязательна
для выбранной топологии/нагрузки. Протокол проверен по документации **DS2480B**;
старые ревизии DS2480 без суффикса B отдельно не проверялись.
@@ -105,8 +105,7 @@ UART-обмен. EEPROM использует выдержку 12 мс, её со
## Подключение и тесты
Добавьте `ds2480.c`, `ds18b20_ds2480.c` и путь к заголовкам в сборку прошивки.
Номер UART, GPIO и библиотеку платформы выбирает приложение. Готовый порт
для конкретной платы пока не входит в библиотеку.
Номер UART, GPIO и библиотеку платформы выбирает приложение. Порт [STM32F407 / STM32F4 HAL](ports/stm32f4/README.md) входит в библиотеку.
```cmake
set(DS18B20_DS2480_BUILD_TESTS OFF CACHE BOOL "" FORCE)
@@ -137,7 +136,8 @@ clang -std=c99 -Wall -Wextra -Wpedantic -Werror -I . ds2480.c ds18b20_ds2480.c t
## Использование
Добавлена в сабмодуль `templates` проекта `john103C6T6NewVer`, ветка `ds2480`.
В рабочую прошивку ещё не включена: для этого требуется порт выбранного UART.
Подключена к опросу climate через USART6 PC6/PC7; администратор выбирает GPIO или DS2480.
Ожидание преобразования в climate неблокирующее: `ds2480_power_begin/end`.
## Источники

View File

@@ -8,8 +8,15 @@ static ds2480_status fault(ds2480 *bus, ds2480_status status)
static ds2480_status exchange(ds2480 *bus, uint8_t command, uint8_t *reply)
{
uint8_t mode = 0xE3;
if (!bus || !reply) return DS2480_ARGUMENT;
if (!bus->ready) return DS2480_NOT_READY;
if (bus->power_active && command != 0xF1) return DS2480_BUSY;
if (bus->data_mode) {
if (bus->port.write(bus->port.user, &mode, 1, bus->timeout_ms))
return fault(bus, DS2480_IO);
bus->data_mode = 0;
}
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);
@@ -29,6 +36,8 @@ ds2480_status ds2480_init(ds2480 *bus, const ds2480_port *port, uint32_t timeout
}
copy = *port; /* Also permit reinitialization with &bus->port. */
bus->ready = 0;
bus->power_active = 0;
bus->data_mode = 0;
bus->port = copy;
bus->timeout_ms = timeout_ms;
if (copy.prepare(copy.user) ||
@@ -77,23 +86,30 @@ ds2480_status ds2480_bit(ds2480 *bus, uint8_t bit, uint8_t *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);
uint8_t mode = 0xE1, reply;
if (!bus || !received) return DS2480_ARGUMENT;
if (!bus->ready) return DS2480_NOT_READY;
if (bus->power_active) return DS2480_BUSY;
if (!bus->data_mode) {
if (bus->port.write(bus->port.user, &mode, 1, bus->timeout_ms))
return fault(bus, DS2480_IO);
bus->data_mode = 1;
}
*received = result;
if (bus->port.write(bus->port.user, &value, 1, bus->timeout_ms))
return fault(bus, DS2480_IO);
if (value == 0xE3 && bus->port.write(bus->port.user, &value, 1, bus->timeout_ms))
return fault(bus, DS2480_IO);
if (bus->port.read(bus->port.user, &reply, 1, bus->timeout_ms))
return fault(bus, DS2480_IO);
*received = reply;
return DS2480_OK;
}
ds2480_status ds2480_power_byte(ds2480 *bus, uint8_t value, uint32_t hold_ms)
ds2480_status ds2480_power_begin(ds2480 *bus, uint8_t value)
{
uint8_t i, bit, reply, echoed = 0;
uint8_t i, bit, echoed = 0;
ds2480_status status;
if (!bus || !hold_ms || hold_ms > 1000) return DS2480_ARGUMENT;
if (!bus) 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) {
@@ -103,15 +119,39 @@ ds2480_status ds2480_power_byte(ds2480 *bus, uint8_t value, uint32_t hold_ms)
}
echoed |= (uint8_t)(bit << i);
}
bus->port.delay_ms(bus->port.user, hold_ms);
bus->power_active = 1;
if (echoed != value) {
status = ds2480_power_end(bus);
return status == DS2480_OK ? DS2480_DATA : status;
}
return DS2480_OK;
}
ds2480_status ds2480_power_end(ds2480 *bus)
{
uint8_t reply;
ds2480_status status;
if (!bus) return DS2480_ARGUMENT;
if (!bus->ready) return DS2480_NOT_READY;
if (!bus->power_active) return DS2480_OK;
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;
bus->power_active = 0;
return status;
}
ds2480_status ds2480_power_byte(ds2480 *bus, uint8_t value, uint32_t hold_ms)
{
ds2480_status status;
if (!hold_ms || hold_ms > 1000) return DS2480_ARGUMENT;
status = ds2480_power_begin(bus, value);
if (status != DS2480_OK) return status;
bus->port.delay_ms(bus->port.user, hold_ms);
return ds2480_power_end(bus);
}
uint8_t ds2480_crc8(const uint8_t *data, uint32_t size)
{
uint8_t crc = 0, bit;

View File

@@ -10,7 +10,7 @@ extern "C" {
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_NOT_READY, DS2480_DATA, DS2480_BUSY
} ds2480_status;
/** Blocking UART callbacks: 0 = success, nonzero = error/timeout.
@@ -33,6 +33,8 @@ typedef struct {
ds2480_port port;
uint32_t timeout_ms;
uint8_t ready;
uint8_t power_active;
uint8_t data_mode;
} ds2480;
/** Independent ROM search cursor. Zero-initialize before each enumeration. */
@@ -50,12 +52,20 @@ ds2480_status ds2480_init(ds2480 *bus, const ds2480_port *port, uint32_t timeout
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). */
/** Exchange a byte in Data Mode; escapes E3 and preserves one reply per byte. */
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);
/** Start indefinite strong pullup after the final bit; returns immediately
* after UART exchange. Until power_end, other bus operations return BUSY.
* Application MUST call power_end after the sensor's required hold time,
* or init to cancel/recover. No internal timer/interrupt releases the pulse.
*/
ds2480_status ds2480_power_begin(ds2480 *bus, uint8_t value);
/** Release strong pullup, consume its reply. Idempotent when ready/idle. */
ds2480_status ds2480_power_end(ds2480 *bus);
/** 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.

View File

@@ -0,0 +1,52 @@
# STM32F407 / STM32F4 HAL
Порт UART для `ds2480.c`. Платформа настраивает тактирование и GPIO, порт
формирует BREAK через TX GPIO, восстанавливает UART 9600 8N1 и проверяет
ошибки приёма. DMA и обработчики UART-прерываний не нужны. HAL tick должен
работать; вызывать из прерывания или при запрещённых прерываниях нельзя.
```c
ds2480 bridge;
ds2480_port io;
ds2480_stm32f4_hal platform = {&huart6, GPIOC, GPIO_PIN_6, GPIO_AF8_USART6};
/* Before this point: enable GPIOC/USART6 clocks, configure PC6/PC7 as AF8. */
if (ds2480_stm32f4_hal_bind(&platform, &io) != DS2480_OK) return;
if (ds2480_init(&bridge, &io, 20) != DS2480_OK) return;
```
В сборку добавляются `ds2480_stm32f4_hal.c`, ядро библиотеки, путь к этому
каталогу и STM32F4 HAL/CMSIS. UART выделяется только для моста. Порт принимает
по одному байту за операцию: ответ сохраняется в DR во время завершения TX,
затем читается без сброса RX. FE/NE/ORE/PE означают ошибку обмена даже при RXNE.
## Подключение в climate F407VET6
| STM32 / питание | DS2480B |
|---|---|
| PC6, USART6_TX | TXD, вывод 7 (вход моста) |
| PC7, USART6_RX | RXD, вывод 8 (выход моста) |
| Общая земля | GND, вывод 1 |
| +5 В | VDD, вывод 4; VPP, вывод 5; POL, вывод 6 |
| DQ датчиков DS18B20 | 1-W, вывод 2 |
DS2480B работает от 5 В. Проверьте согласование логических уровней по
электрическим характеристикам конкретной платы/модуля; не считайте питание
DS2480B от 3,3 В допустимым. Для внешнего питания DS18B20 подключите VDD;
при паразитном питании VDD датчика соединяется с GND. В обоих случаях общий GND.
PC6/PC7 выбраны в `climate` (AF8). UART1/2 используются Modbus, SDIO использует
4-битную шину PC8..PC12/PD2. Пины для другой платы задаются её приложением.
## Неблокирующее питание датчиков
`ds2480_power_begin` посылает команду датчика с strong pullup на последнем
слоте. Приложение возвращается в главный цикл, отсчитывает 750 мс для
преобразования либо минимум 10 мс для EEPROM, затем вызывает
`ds2480_power_end`. До завершения импульса остальной обмен возвращает BUSY.
При отмене вызывайте `power_end`, при потере синхронизации — `ds2480_init`.
Используется в `climate_control_f407vet6_f4`: `ds2480_app.c` связывает порт
из сабмодуля с существующим каталогом датчиков и диагностикой Modbus.
Источники: [STM32F407, таблица alternate functions](https://www.st.com/resource/en/datasheet/stm32f407ve.pdf),
[DS2480B, выводы и UART](https://www.analog.com/media/en/technical-documentation/data-sheets/ds2480b.pdf).

View File

@@ -0,0 +1,85 @@
#include "ds2480_stm32f4_hal.h"
#define RX_ERRORS (USART_SR_ORE | USART_SR_NE | USART_SR_FE | USART_SR_PE)
static int prepare(void *user)
{
ds2480_stm32f4_hal *p = (ds2480_stm32f4_hal *)user;
GPIO_InitTypeDef gpio = {0};
/* Abort resets HAL states after timeouts and disables UART IRQ/DMA. */
if (HAL_UART_Abort(p->uart) != HAL_OK) return -1;
__HAL_UART_DISABLE(p->uart);
HAL_GPIO_WritePin(p->tx_port, p->tx_pin, GPIO_PIN_RESET);
gpio.Pin = p->tx_pin;
gpio.Mode = GPIO_MODE_OUTPUT_PP;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(p->tx_port, &gpio);
HAL_Delay(2);
HAL_GPIO_WritePin(p->tx_port, p->tx_pin, GPIO_PIN_SET);
HAL_Delay(2);
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Alternate = p->tx_alternate;
HAL_GPIO_Init(p->tx_port, &gpio);
p->uart->Init.BaudRate = 9600;
p->uart->Init.WordLength = UART_WORDLENGTH_8B;
p->uart->Init.StopBits = UART_STOPBITS_1;
p->uart->Init.Parity = UART_PARITY_NONE;
p->uart->Init.Mode = UART_MODE_TX_RX;
p->uart->Init.HwFlowCtl = UART_HWCONTROL_NONE;
p->uart->Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(p->uart) != HAL_OK) return -1;
/* SR then DR: discard BREAK echo/stale RX and clear FE/NE/ORE/PE. */
__HAL_UART_CLEAR_OREFLAG(p->uart);
return 0;
}
static int write_byte(void *user, const uint8_t *data, uint32_t size, uint32_t timeout)
{
ds2480_stm32f4_hal *p = (ds2480_stm32f4_hal *)user;
if (!data || size != 1 || !timeout || timeout == HAL_MAX_DELAY) return -1;
/* One reply fits in DR while HAL waits for TX complete. Never flush RX
* here: a reply may already be present when HAL_UART_Transmit returns. */
if (p->uart->Instance->SR & RX_ERRORS) return -1;
return HAL_UART_Transmit(p->uart, data, 1, timeout) == HAL_OK ? 0 : -1;
}
static int read_byte(void *user, uint8_t *data, uint32_t size, uint32_t timeout)
{
ds2480_stm32f4_hal *p = (ds2480_stm32f4_hal *)user;
uint32_t start, flags;
if (!data || size != 1 || !timeout || timeout == HAL_MAX_DELAY) return -1;
start = HAL_GetTick();
for (;;) {
flags = p->uart->Instance->SR;
/* Test errors BEFORE reading DR, including when RXNE is already set. */
if (flags & RX_ERRORS) {
__HAL_UART_CLEAR_OREFLAG(p->uart);
return -1;
}
if (flags & USART_SR_RXNE) {
*data = (uint8_t)p->uart->Instance->DR;
return 0;
}
if ((uint32_t)(HAL_GetTick() - start) >= timeout) return -1;
}
}
static void delay_ms(void *user, uint32_t ms)
{
(void)user;
HAL_Delay(ms);
}
ds2480_status ds2480_stm32f4_hal_bind(ds2480_stm32f4_hal *p, ds2480_port *port)
{
if (!p || !port || !p->uart || !p->uart->Instance || !p->tx_port ||
!p->tx_pin || (p->tx_pin & (p->tx_pin - 1U)) || p->tx_alternate > 15U)
return DS2480_ARGUMENT;
port->user = p;
port->prepare = prepare;
port->write = write_byte;
port->read = read_byte;
port->delay_ms = delay_ms;
return DS2480_OK;
}

View File

@@ -0,0 +1,32 @@
#ifndef DS2480_STM32F4_HAL_H
#define DS2480_STM32F4_HAL_H
#include "ds2480.h"
#include "stm32f4xx_hal.h"
#ifdef __cplusplus
extern "C" {
#endif
/** Dedicated UART initialized by the board (clock, RX/TX AF, no IRQ/DMA).
* tx_port/pin/alternate let prepare generate a real >=2 ms BREAK, including
* recovery from a lost strong-pullup response. No reset pin is required.
* GPIO clock must stay enabled. UART must not be used by any other service.
*/
typedef struct {
UART_HandleTypeDef *uart;
GPIO_TypeDef *tx_port;
uint16_t tx_pin;
uint32_t tx_alternate;
} ds2480_stm32f4_hal;
/** Populate callbacks only; call ds2480_init afterwards to reset/calibrate.
* Thread/main-loop use only, with running HAL tick and interrupts enabled.
* UART baud rate is fixed at 9600 8N1; all transfers are single-byte.
*/
ds2480_status ds2480_stm32f4_hal_bind(ds2480_stm32f4_hal *context, ds2480_port *port);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,39 @@
/* Host-only HAL model, never add this directory to a firmware include path. */
#ifndef TEST_STM32F4_HAL_H
#define TEST_STM32F4_HAL_H
#include <stdint.h>
typedef struct { uint32_t SR, DR, CR1; } USART_TypeDef;
typedef struct { uint32_t pin, level, mode; } GPIO_TypeDef;
typedef struct { uint32_t BaudRate,WordLength,StopBits,Parity,Mode,HwFlowCtl,OverSampling; } UART_InitTypeDef;
typedef struct { USART_TypeDef *Instance; UART_InitTypeDef Init; } UART_HandleTypeDef;
typedef struct { uint32_t Pin, Mode, Pull, Speed, Alternate; } GPIO_InitTypeDef;
typedef enum { HAL_OK, HAL_ERROR } HAL_StatusTypeDef;
#define USART_SR_ORE 8U
#define USART_SR_NE 4U
#define USART_SR_FE 2U
#define USART_SR_PE 1U
#define USART_SR_RXNE 32U
#define GPIO_PIN_RESET 0U
#define GPIO_PIN_SET 1U
#define GPIO_MODE_OUTPUT_PP 1U
#define GPIO_MODE_AF_PP 2U
#define GPIO_NOPULL 0U
#define GPIO_SPEED_FREQ_HIGH 3U
#define UART_WORDLENGTH_8B 0U
#define UART_STOPBITS_1 0U
#define UART_PARITY_NONE 0U
#define UART_MODE_TX_RX 12U
#define UART_HWCONTROL_NONE 0U
#define UART_OVERSAMPLING_16 0U
#define HAL_MAX_DELAY UINT32_MAX
#define __HAL_UART_DISABLE(u) ((u)->Instance->CR1=0)
void test_clear(UART_HandleTypeDef *u);
#define __HAL_UART_CLEAR_OREFLAG(u) test_clear(u)
HAL_StatusTypeDef HAL_UART_Abort(UART_HandleTypeDef *u);
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *u);
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *u,const uint8_t *p,uint16_t n,uint32_t timeout);
void HAL_GPIO_WritePin(GPIO_TypeDef *g,uint16_t pin,uint32_t level);
void HAL_GPIO_Init(GPIO_TypeDef *g,const GPIO_InitTypeDef *i);
void HAL_Delay(uint32_t ms);
uint32_t HAL_GetTick(void);
#endif

View File

@@ -0,0 +1,41 @@
#include "ds2480_stm32f4_hal.h"
#include <assert.h>
#include <stdio.h>
static uint32_t now, low_ms, high_ms, clears;
static GPIO_TypeDef gpio;
static unsigned fail_init, fail_tx;
void test_clear(UART_HandleTypeDef *u){u->Instance->SR=0;++clears;}
HAL_StatusTypeDef HAL_UART_Abort(UART_HandleTypeDef *u){(void)u;return HAL_OK;}
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *u)
{assert(gpio.mode==GPIO_MODE_AF_PP && gpio.level==1);assert(u->Init.BaudRate==9600);return fail_init?HAL_ERROR:HAL_OK;}
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *u,const uint8_t *p,uint16_t n,uint32_t timeout)
{assert(n==1 && timeout==20);u->Instance->DR=*p;u->Instance->SR=USART_SR_RXNE;return fail_tx?HAL_ERROR:HAL_OK;}
void HAL_GPIO_WritePin(GPIO_TypeDef *g,uint16_t pin,uint32_t level){g->pin=pin;g->level=level;}
void HAL_GPIO_Init(GPIO_TypeDef *g,const GPIO_InitTypeDef *i){assert(i->Pin==64);g->mode=i->Mode;}
void HAL_Delay(uint32_t ms){now+=ms;if(gpio.level)high_ms+=ms;else low_ms+=ms;}
uint32_t HAL_GetTick(void){return now++;}
int main(void)
{
USART_TypeDef regs={0};UART_HandleTypeDef uart={0};
ds2480_stm32f4_hal ctx={&uart,&gpio,64,8};ds2480_port port;
uint8_t byte=0xCD,rx=0;
uart.Instance=&regs;
assert(ds2480_stm32f4_hal_bind(&ctx,&port)==DS2480_OK);
assert(port.prepare(port.user)==0 && low_ms>=2 && high_ms>=2 && clears==1);
assert(port.write(port.user,&byte,1,20)==0);
assert(regs.SR & USART_SR_RXNE); /* Early reply must survive TX completion. */
assert(port.read(port.user,&rx,1,20)==0 && rx==byte && clears==1);
regs.SR=USART_SR_RXNE|USART_SR_FE;rx=0xA5;
assert(port.read(port.user,&rx,1,20)!=0 && rx==0xA5 && clears==2);
regs.SR=USART_SR_ORE;assert(port.write(port.user,&byte,1,20)!=0);
regs.SR=0;now=UINT32_MAX-5;
assert(port.read(port.user,&rx,1,20)!=0); /* Wrap-safe bounded timeout. */
assert(port.write(port.user,&byte,2,20)!=0);
assert(port.read(port.user,&rx,1,HAL_MAX_DELAY)!=0);
fail_tx=1;assert(port.write(port.user,&byte,1,20)!=0);
fail_init=1;assert(port.prepare(port.user)!=0 && gpio.level==1);
ctx.tx_pin=3;assert(ds2480_stm32f4_hal_bind(&ctx,&port)==DS2480_ARGUMENT);
puts("STM32F4 UART port: OK");return 0;
}

View File

@@ -12,6 +12,7 @@ typedef struct {
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;
unsigned data_mode, escape;
} fake;
static void crc_scratch(fake *f) { f->scratch[8] = ds2480_crc8(f->scratch, 8); }
@@ -91,6 +92,7 @@ static int prepare(void *user)
fake *f = user;
++f->prepared;
f->pending = f->pulse = f->calibrated = 0;
f->data_mode = f->escape = 0;
return (int)f->fail_prepare;
}
@@ -101,6 +103,17 @@ static int transmit(void *user, const uint8_t *data, uint32_t size, uint32_t tim
CHECK(size == 1 && timeout == 20 && !f->pending);
if (f->fail_write) return -1;
if (!f->calibrated) { CHECK(command == 0xC1); f->calibrated = 1; return 0; }
if (f->data_mode) {
unsigned i, reply = 0;
if (command == 0xE3 && !f->escape) { f->escape=1; return 0; }
if (!f->escape || command == 0xE3) {
f->escape=0;
for (i=0; i<8; ++i) reply |= (unsigned)wire_bit(f,(uint8_t)((command>>i)&1)) << i;
f->reply=reply; f->pending=1; return 0;
}
f->escape=0; f->data_mode=0;
}
if (command == 0xE1) { f->data_mode=1; return 0; }
CHECK(!f->pulse || command == 0xF1);
if (command == 0x3F) f->reply = 0x3E;
else if (command == 0xC1) {
@@ -168,6 +181,16 @@ int main(void)
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(ds2480_reset(&bus) == DS2480_OK);
CHECK(ds2480_byte(&bus, 0xCC, &bit) == DS2480_OK);
CHECK(ds2480_power_begin(&bus, 0x44) == DS2480_OK);
CHECK(f.hold == 0 && f.pulse && bus.power_active);
CHECK(ds2480_reset(&bus) == DS2480_BUSY);
CHECK(ds2480_byte(&bus, 0xFF, &bit) == DS2480_BUSY);
CHECK(ds2480_power_begin(&bus, 0x44) == DS2480_BUSY);
delay(&f, 750);
CHECK(ds2480_power_end(&bus) == DS2480_OK && !bus.power_active);
CHECK(ds2480_power_end(&bus) == DS2480_OK);
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) {
@@ -178,6 +201,8 @@ int main(void)
CHECK(raw == (i == 9 ? -168 : i == 10 ? -164 : i == 11 ? -162 : -161));
}
CHECK(f.copy == 4);
CHECK(ds18b20_ds2480_configure(&bus, f.rom[0], (int8_t)-29, 2, 12, 0) == DS2480_OK);
CHECK(f.scratch[2] == 0xE3); /* Escaped byte followed by an ordinary byte. */
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;

View File

@@ -33,3 +33,5 @@ if(DS18B20_BUILD_TESTS)
target_include_directories(test_ds18b20 PRIVATE tests)
add_test(NAME ds18b20 COMMAND test_ds18b20)
endif()
add_subdirectory(instance)

View File

@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.13)
project(ds18b20_instance C)
add_library(ds18b20_instance STATIC Src/ds18b20.c)
target_include_directories(ds18b20_instance PUBLIC Inc)
set_target_properties(ds18b20_instance PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED YES)
enable_testing()
foreach(name copy_delay incremental_search)
add_executable(test_ds18b20_${name} Tests/test_ds18b20_${name}.c)
target_link_libraries(test_ds18b20_${name} PRIVATE ds18b20_instance)
add_test(NAME ds18b20_${name} COMMAND test_ds18b20_${name})
endforeach()

384
c/ds18b20/instance/HELP.md Normal file
View File

@@ -0,0 +1,384 @@
# DS18B20: справочник API
## Назначение
Публичный API ядра объявлен только в `Inc/ds18b20.h`. Приложение компилирует
`Src/ds18b20.c`, но не включает этот файл. Библиотека выполняет Search ROM,
проверяет Dallas CRC8, запускает Convert T, читает scratchpad, декодирует
температуру, меняет разрешение и TH/TL.
## Константы и состояние
- `DS18B20_ROM_SIZE` — 8 байт полного 64-битного ROM.
- `DS18B20_SCRATCHPAD_SIZE` — 9 байт scratchpad вместе с CRC.
- `DS18B20_DEFAULT_TIMEOUT_MS` — 750 мс, значение конфигурации по умолчанию.
- `DS18B20_FAMILY_CODE` — `0x28`.
- `ds18b20_t` — состояние одной шины. После инициализации его поля напрямую не
изменяют; список датчиков читают через `ds18b20_count()` и `ds18b20_rom()`.
## Platform callbacks
`ds18b20_onewire_ops_t` связывает переносимое ядро с аппаратурой:
| Callback | Обязателен | Контракт |
| --- | --- | --- |
| `drive_low(context)` | да | Активно притянуть open-drain линию к 0 |
| `release(context)` | да | Отпустить линию; внешний/внутренний pull-up поднимает её |
| `read(context)` | да | Вернуть текущий логический уровень `0` или `1` |
| `delay_us(context, us)` | да | Синхронная задержка с микросекундной точностью |
| `tick_ms(context)` | для `ds18b20_wait` | Монотонный, допускающий uint32 wrap tick |
| `critical_enter(context)` | нет | Начать защиту одного временного слота |
| critical_exit(context) | нет | Восстановить состояние после защиты слота |
| strong_pullup(context, enable) | для parasite Copy | Активно удерживать high и безопасно вернуть open-drain |
Обе функции critical section задаются парой либо обе оставляются `NULL`.
## Коды возврата
| Код | Значение | Значение для приложения |
| --- | ---: | --- |
| `DS18B20_OK` | 0 | Успех или преобразование готово |
| `DS18B20_E_ARGUMENT` | -1 | Неверный указатель, callback, размер или параметр |
| `DS18B20_E_IO` | -2 | Некорректная конфигурация scratchpad |
| `DS18B20_E_NO_DEVICE` | -3 | Нет presence pulse / подходящих устройств |
| `DS18B20_E_CRC` | -4 | CRC ROM или scratchpad не совпал |
| `DS18B20_E_TIMEOUT` | -5 | Истёк timeout блокирующего ожидания |
| `DS18B20_E_BUSY` | -6 | Преобразование ещё не готово |
| `DS18B20_E_CAPACITY` | -7 | Найдено больше ROM, чем помещается в storage |
| DS18B20_E_ROM | -8 | ROM имеет неверный family code либо отклонён адресной операцией |
| DS18B20_E_POWER | -9 | Parasite Copy запрошен без strong-pull-up callback |
## Инициализация и поиск
### `ds18b20_init`
```c
ds18b20_status_t ds18b20_init(
ds18b20_t *instance,
const ds18b20_onewire_ops_t *ops,
void *platform_context,
uint8_t (*rom_storage)[DS18B20_ROM_SIZE],
size_t rom_capacity);
```
Обнуляет состояние, сохраняет callback/context/storage и отпускает линию.
`rom_storage` — массив приложения, `rom_capacity` — число ROM, не число байт.
Core не выделяет и не освобождает память.
### `ds18b20_search`
```c
ds18b20_status_t ds18b20_search(ds18b20_t *instance);
```
Заново выполняет Search ROM и заменяет прежний список. Сохраняются только ROM с
family `0x28` и корректным CRC. Результаты поиска:
- `DS18B20_OK` — найден минимум один корректный DS18B20;
- `DS18B20_E_NO_DEVICE` — корректные DS18B20 не найдены;
- `DS18B20_E_CAPACITY` — storage заполнен; уже записанные ROM остаются доступны.
### `ds18b20_count` и `ds18b20_rom`
```c
size_t ds18b20_count(const ds18b20_t *instance);
const uint8_t *ds18b20_rom(const ds18b20_t *instance, size_t index);
```
`count` возвращает число сохранённых ROM либо 0 для неверного экземпляра.
`rom` возвращает указатель на 8 байт либо `NULL` для неверного индекса.
Указатель становится логически устаревшим после следующего поиска.
## Преобразование и чтение
### `ds18b20_start_all`
```c
ds18b20_status_t ds18b20_start_all(ds18b20_t *instance);
```
Посылает `Skip ROM + Convert T` всем устройствам шины. Возвращает
`DS18B20_E_NO_DEVICE`, если нет presence pulse.
### `ds18b20_start`
```c
ds18b20_status_t ds18b20_start(
ds18b20_t *instance,
const uint8_t rom[DS18B20_ROM_SIZE]);
```
Проверяет ROM и посылает `Match ROM + Convert T` одному датчику.
### `ds18b20_conversion_ready`
```c
ds18b20_status_t ds18b20_conversion_ready(ds18b20_t *instance);
```
Один раз читает 1-Wire ready bit: `DS18B20_OK` означает готовность,
`DS18B20_E_BUSY` — преобразование продолжается. Это предпочтительная
неблокирующая проверка для main loop/RTOS.
### `ds18b20_wait`
```c
ds18b20_status_t ds18b20_wait(
ds18b20_t *instance, uint32_t timeout_ms);
```
Блокирующе опрашивает ready bit до готовности или `DS18B20_E_TIMEOUT`. Требует
`tick_ms`. Функция не делает sleep/yield и не рекомендуется в основном цикле.
### `ds18b20_read_scratchpad`
```c
ds18b20_status_t ds18b20_read_scratchpad(
ds18b20_t *instance,
const uint8_t rom[DS18B20_ROM_SIZE],
uint8_t scratchpad[DS18B20_SCRATCHPAD_SIZE]);
```
Проверяет ROM, посылает `Match ROM + Read Scratchpad`, читает 9 байт и
проверяет CRC. Функция не запускает Convert T и не проверяет, что преобразование
ранее завершилось.
### `ds18b20_decode_temperature`
```c
ds18b20_status_t ds18b20_decode_temperature(
const uint8_t scratchpad[DS18B20_SCRATCHPAD_SIZE],
float *temperature_c);
```
Проверяет CRC, маскирует неопределённые младшие биты согласно разрешению 9–12
бит и возвращает градусы Цельсия. Неизвестная комбинация configuration bits
даёт `DS18B20_E_IO`.
## Конфигурация и User Bytes
### `ds18b20_set_resolution`
```c
ds18b20_status_t ds18b20_set_resolution(
ds18b20_t *instance,
const uint8_t rom[DS18B20_ROM_SIZE],
uint8_t bits);
```
`bits` принимает только `9`, `10`, `11` или `12`. Функция читает
scratchpad, сохраняет TH/TL, записывает новый configuration byte и посылает
`Copy Scratchpad`.
### `ds18b20_write_user_bytes`
```c
ds18b20_status_t ds18b20_write_user_bytes(
ds18b20_t *instance,
const uint8_t rom[DS18B20_ROM_SIZE],
int16_t bytes12,
int16_t bytes34,
uint8_t mask);
```
Текущий контракт отражает физические writable bytes DS18B20:
- `mask & 0x01` записывает младшие 8 бит `bytes12` в TH, scratchpad[2];
- `mask & 0x02` записывает старшие 8 бит `bytes12` в TL, scratchpad[3];
- `bytes34` зарезервирован и не используется: scratchpad[6]/[7] read-only;
- остальные биты `mask` игнорируются.
Перед записью функция читает scratchpad, поэтому невыбранный TH/TL и
configuration byte сохраняются. Затем выполняются `Write Scratchpad` и
`Copy Scratchpad`. Функция не выполняет no-op detection, Recall E2 и readback.
Application adapter обязан добавить эти шаги, если операция используется как
подтверждённая пользовательская запись.
## CRC и ROM
```c
uint8_t ds18b20_crc8(const void *data, size_t length);
ds18b20_status_t ds18b20_validate_rom(
const uint8_t rom[DS18B20_ROM_SIZE]);
```
`crc8` вычисляет Dallas/Maxim CRC-8. `validate_rom` отдельно проверяет family
`0x28` и CRC первых семи байтов против восьмого.
## Неблокирующий пример опроса
```c
enum poll_state { POLL_START, POLL_WAIT };
static enum poll_state state = POLL_START;
static uint8_t scratchpad[DS18B20_SCRATCHPAD_SIZE];
void poll_ds18b20(void)
{
if (state == POLL_START) {
if (ds18b20_start_all(&bus) == DS18B20_OK) {
state = POLL_WAIT;
}
return;
}
if (ds18b20_conversion_ready(&bus) != DS18B20_OK) {
return;
}
for (size_t i = 0; i < ds18b20_count(&bus); ++i) {
const uint8_t *rom = ds18b20_rom(&bus, i);
float temperature_c;
if (ds18b20_read_scratchpad(&bus, rom, scratchpad) == DS18B20_OK &&
ds18b20_decode_temperature(scratchpad, &temperature_c) ==
DS18B20_OK) {
publish_temperature(rom, temperature_c);
}
}
state = POLL_START;
}
```
Production adapter должен дополнительно иметь deadline: постоянный
`DS18B20_E_BUSY` не должен навсегда удерживать state machine.
## Диагностика
| Симптом | Проверка |
| --- | --- |
| `DS18B20_E_NO_DEVICE` | Питание, общий GND, pull-up, pin, presence pulse |
| Постоянный `DS18B20_E_BUSY` | Deadline, питание датчика, конфликт операций |
| `DS18B20_E_CRC` | Длина/топология шины, помехи, timing, pull-up |
| `DS18B20_E_CAPACITY` | Увеличить ROM storage или ограничить число устройств |
| Неверная температура | Не читать до ready; проверить CRC и resolution bits |
| Зависание в delay | Таймер должен быть запущен и считать непрерывно |
## Ограничения
- Вызовы одной шины не reentrant и не ISR-safe.
- Микросекундные 1-Wire-слоты синхронные; длительное преобразование должно быть
вынесено в state machine.
- Callback `strong_pullup` используется модулем UserByte для parasite-powered
`Copy Scratchpad`; аппаратная схема и timing должны быть проверены на плате.
- Low-level `Copy Scratchpad` намеренно разделён на `start/finish`; выдержку
10 ms, Recall E2, CRC/readback и освобождение шины обеспечивает state machine
`Modules/UserByte`, а не одиночный low-level вызов.
- Поиск синхронный и не имеет cancel callback; для большой шины или жёстких
realtime-требований нужен пошаговый автомат поиска.
## Тесты
Из корня репозитория:
```powershell
python -m unittest Libraries.PortableTests.test_portable_models
```
Модель проверяет CRC, независимость экземпляров и неблокирующую интеграцию.
`Libraries/PortableTests/test_portable_libraries.c` дополнительно проверяет C
API CRC, декодирование температуры и независимость двух шин. Для полной задачи
также обязательны `git diff --check` и целевая Keil-сборка `0/0`.
# User Byte и EEPROM
Для новой логики приложения используйте внутренний модуль `Modules/UserByte`
с явным selector `TH/TL`, а не legacy `ds18b20_write_user_bytes`. Low-level API `write_scratchpad`, `copy_*`,
`recall_e2`, `recall_ready` и `recover_bus` предназначен для его state machine.
`strong_pullup` обязателен только для parasite-powered Copy Scratchpad.
`ds18b20_search_retry_due` помогает приложению повторять поиск при `count=0`,
не вмешиваясь в активную температурную конверсию; период и tick задаёт adapter.
## Версия Modbus-контракта User Byte
### Явный неблокирующий поиск
```c
ds18b20_status_t status = ds18b20_search_begin(&bus);
while (status == DS18B20_E_BUSY ||
status == DS18B20_E_CRC ||
status == DS18B20_E_ROM) {
status = ds18b20_search_step(&bus);
/* Между шагами основной цикл продолжает обслуживать Modbus/RTC/SD. */
}
```
Не вызывайте `ds18b20_search_retry_due()` для GUI-команды: reconnect и polling
не должны автоматически менять таблицу ROM. В проектном адаптере команда
захватывается только по `apply=1`, а `sequence` защищает от старого ответа.
Legacy API `ds18b20_user_byte_submit()` остаётся TH-only. Для явного TL
используется selector `DS18B20_USER_BYTE_TL`; STM32 adapter принимает его только
с `contractVersion=2`, записанным атомарно с selector до APPLY. Это защищает GUI
от старого bridge/firmware, которое всегда маршрутизировало запрос как TH.
### Известные ROM и ошибка поиска detail 2
В F407 сохранённые SensorBindings восстанавливаются в список опроса после
AppStorage_Init. Если список непустой, стартовый SEARCH ROM не запускается.
Температура читается адресно через MATCH ROM; запуск преобразования общий.
Даже при отсутствии датчика во время старта его адрес остаётся доступен для
повторного чтения. Подключение подтверждается успешным чтением температуры.
Явный поиск добавляет новые ID. При успехе, ошибке, тайм-ауте и отмене
Dallas_FinishSearch объединяет результат с прежними ID, включая ещё не
сохранённые во Flash. Ёмкость списка — 32 ID; найденные ID занимают места
первыми. Сохранение использует существующий SensorBindings_SyncFound и
повтор при BUSY. Отключать питание следует после завершения сохранения.
MCU detail 2 = DS18B20_E_IO: поиск ROM получил недопустимую комбинацию
битов либо неполный ROM. Это не доказательство конкретной причины на кабеле.
Ошибка поиска остаётся видна, но известные ID продолжают опрашиваться.
Число в каталоге включает известные адреса; наличие определяется connected.
API ds18b20_add_known_rom проверяет family/CRC, возвращает E_ARGUMENT,
E_ROM, E_CRC или E_CAPACITY; дубликат возвращает OK. Функция не делает
I/O и не подтверждает наличие датчика. Вызывать вне поиска и конверсии.
Проверки: `Libraries/DS18B20/Tests/run_host_tests.ps1` и
`python -m unittest discover -s tests -p test_known_rom_host.py`.
Для аппаратной проверки: найти датчики по одному, дождаться сохранения,
подключить все на 20 м, перезапустить МК без команды поиска и проверить
обновление температур; затем повторить при ошибке/отмене поиска.
### Поиск с восстановлением после ошибок
Настройки в `Inc/ds18b20_config.h`: три прохода дерева, четыре дополнительных
повтора каждой неудачной ветки (пять попыток суммарно). Один вызов step делает
не более одной попытки ROM: Modbus и остальные сервисы работают между ними.
Перед попыткой сохраняются ROM-путь и discrepancy; после IO, отсутствия presence
или CRC они восстанавливаются. После пяти ошибок начинается следующий проход.
Результаты всех проходов объединяются; дубликаты не занимают ёмкость. CRC
проверяется до принятия пути. Чужое семейство с корректным CRC пропускается.
Успех означает, что хотя бы один проход полностью обошёл дерево и найден хотя
бы один DS18B20. Ранее восстановленные ошибки сохраняются в диагностике, но
не превращают успешный поиск в CRC_ERROR. Это не гарантия обнаружения каждого
физического датчика на нестабильной линии. Если ни один проход не завершён,
возвращается последняя ошибка; проверенные частичные результаты сохраняются.
Пустая линия ограничена 15 попытками reset. Общий предел 512 попыток защищает
и блокирующий API; приложение дополнительно ограничивает поиск 12000 мс.
Во время поиска после каждого слота добавляется 20 мкс высокого уровня:
после записи нуля получается минимум 30 мкс вместо 10 мкс. Импульсы 6/60 мкс,
read-init 3 мкс и выборка через дополнительные 10 мкс остаются прежними.
Добавочная пауза выполняется с разрешёнными прерываниями. Обычное чтение,
конверсия и Copy Scratchpad не получают эту добавку. Подбор паузы для кабеля
требует измерений; программная модель не подтверждает аналоговый фронт.
`bus.search_diagnostics` содержит attempts, retries, crc_errors, io_errors,
passes_finished, complete_passes и последнее место ошибки. Номер бита 1..64;
0 означает reset/общий лимит, pair=0xFF — пары нет. Диагностика сохраняется
после последующего успеха и обнуляется новым search_begin.
FC04: 1210–1217 совместимы, 1218 и 1219 используют прежний резерв:
- 1218: биты 0..6 — номер ROM-бита, бит 8 — complement, бит 9 — id,
биты 12..15 — положительный код последней ошибки; 0 — ошибок не было.
- 1219: биты 0..7 — число повторов с насыщением 255, биты 8..15 — число
законченных проходов, включая прерванные после исчерпания повторов.
Bridge читает 10 регистров и возвращает `diagnostics` в ответе поиска.
Для ошибки шины сообщение дополнено битом, id/complement, повторами и проходами.
Старые восемь полей сохранены. При bit=0 пара не интерпретируется.
Проверки: `run_host_tests.ps1`, `test_known_rom_host.py`,
`test_sensor_search_diagnostics_host.py`, `test_remote_ds18b20_host.py`.
На плате проверить 20 последовательных поисков трёх датчиков на 20 м,
отключение/подключение датчика при поиске, отмену и восстановление опроса.

View File

@@ -0,0 +1,126 @@
#ifndef PORTABLE_DS18B20_H
#define PORTABLE_DS18B20_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define DS18B20_ROM_SIZE 8U
#define DS18B20_SCRATCHPAD_SIZE 9U
typedef enum {
DS18B20_OK = 0,
DS18B20_E_ARGUMENT = -1,
DS18B20_E_IO = -2,
DS18B20_E_NO_DEVICE = -3,
DS18B20_E_CRC = -4,
DS18B20_E_TIMEOUT = -5,
DS18B20_E_BUSY = -6,
DS18B20_E_CAPACITY = -7,
DS18B20_E_ROM = -8,
DS18B20_E_POWER = -9
} ds18b20_status_t;
typedef struct {
void (*drive_low)(void *context);
void (*release)(void *context);
uint8_t (*read)(void *context);
void (*delay_us)(void *context, uint32_t us);
uint32_t (*tick_ms)(void *context);
void (*critical_enter)(void *context);
void (*critical_exit)(void *context);
/* Optional callback used only while parasite-powered EEPROM is copied. */
void (*strong_pullup)(void *context, uint8_t enable);
} ds18b20_onewire_ops_t;
typedef struct {
uint16_t attempts;
uint16_t retries;
uint16_t crc_errors;
uint16_t io_errors;
uint8_t passes_finished;
uint8_t complete_passes;
uint8_t last_error_bit; /* 1..64; 0 = reset or overall attempt limit. */
uint8_t last_error_pair; /* bit 1 = id, bit 0 = complement; 0xFF = no pair. */
ds18b20_status_t last_error;
} ds18b20_search_diagnostics_t;
typedef struct {
const ds18b20_onewire_ops_t *ops;
void *platform_context;
uint8_t (*roms)[DS18B20_ROM_SIZE];
size_t rom_capacity;
size_t rom_count;
uint8_t search_rom[DS18B20_ROM_SIZE];
uint8_t last_discrepancy;
uint8_t last_family_discrepancy;
uint8_t last_device;
uint8_t strong_pullup_active;
uint8_t search_active;
uint8_t search_retries;
ds18b20_status_t search_result;
ds18b20_search_diagnostics_t search_diagnostics;
} ds18b20_t;
ds18b20_status_t ds18b20_init(ds18b20_t *instance,
const ds18b20_onewire_ops_t *ops,
void *platform_context,
uint8_t (*rom_storage)[DS18B20_ROM_SIZE],
size_t rom_capacity);
ds18b20_status_t ds18b20_search(ds18b20_t *instance);
/* Incremental search keeps application services responsive by processing one
* physical ROM attempt per call. Begin clears the previous result table.
* BUSY includes internal retries/passes. All other results are terminal;
* a failed search may still contain validated partial results. */
ds18b20_status_t ds18b20_search_begin(ds18b20_t *instance);
ds18b20_status_t ds18b20_search_step(ds18b20_t *instance);
/* Add a validated known ROM without bus traffic; duplicates are idempotent.
* Call only outside an active search/conversion. Presence is checked by reads. */
ds18b20_status_t ds18b20_add_known_rom(ds18b20_t *instance,
const uint8_t rom[DS18B20_ROM_SIZE]);
size_t ds18b20_count(const ds18b20_t *instance);
const uint8_t *ds18b20_rom(const ds18b20_t *instance, size_t index);
uint8_t ds18b20_crc8(const void *data, size_t length);
ds18b20_status_t ds18b20_validate_rom(const uint8_t rom[DS18B20_ROM_SIZE]);
ds18b20_status_t ds18b20_start_all(ds18b20_t *instance);
ds18b20_status_t ds18b20_start(ds18b20_t *instance,
const uint8_t rom[DS18B20_ROM_SIZE]);
ds18b20_status_t ds18b20_conversion_ready(ds18b20_t *instance);
ds18b20_status_t ds18b20_wait(ds18b20_t *instance, uint32_t timeout_ms);
ds18b20_status_t ds18b20_read_scratchpad(
ds18b20_t *instance, const uint8_t rom[DS18B20_ROM_SIZE],
uint8_t scratchpad[DS18B20_SCRATCHPAD_SIZE]);
ds18b20_status_t ds18b20_decode_temperature(
const uint8_t scratchpad[DS18B20_SCRATCHPAD_SIZE], float *temperature_c);
ds18b20_status_t ds18b20_set_resolution(
ds18b20_t *instance, const uint8_t rom[DS18B20_ROM_SIZE], uint8_t bits);
ds18b20_status_t ds18b20_write_user_bytes(
ds18b20_t *instance, const uint8_t rom[DS18B20_ROM_SIZE],
int16_t bytes12, int16_t bytes34, uint8_t mask);
ds18b20_status_t ds18b20_write_scratchpad(
ds18b20_t *instance, const uint8_t rom[DS18B20_ROM_SIZE],
uint8_t th, uint8_t tl, uint8_t configuration);
ds18b20_status_t ds18b20_copy_scratchpad_start(
ds18b20_t *instance, const uint8_t rom[DS18B20_ROM_SIZE],
uint8_t parasite_power);
ds18b20_status_t ds18b20_copy_scratchpad_finish(
ds18b20_t *instance, uint8_t parasite_power);
ds18b20_status_t ds18b20_recall_e2(
ds18b20_t *instance, const uint8_t rom[DS18B20_ROM_SIZE]);
ds18b20_status_t ds18b20_recall_ready(ds18b20_t *instance);
void ds18b20_recover_bus(ds18b20_t *instance);
uint8_t ds18b20_search_retry_due(const ds18b20_t *instance,
uint8_t conversion_active,
uint32_t now_ms, uint32_t last_attempt_ms,
uint32_t retry_period_ms);
/* Экземпляр не синхронизируется внутри: один вызов на одной шине должен быть
* завершён до следующего. Из ISR API вызывать нельзя из-за задержек до 750 ms. */
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,17 @@
#ifndef PORTABLE_DS18B20_CONFIG_H
#define PORTABLE_DS18B20_CONFIG_H
#define DS18B20_DEFAULT_TIMEOUT_MS 750U
#define DS18B20_FAMILY_CODE 0x28U
/* Initial attempt + four retries for each tree branch, three full passes. */
#define DS18B20_SEARCH_RETRIES 4U
#define DS18B20_SEARCH_PASSES 3U
/* Bounds the blocking API too, even if noisy devices keep changing the tree. */
#define DS18B20_SEARCH_MAX_ATTEMPTS 512U
/* Extra released-high time AFTER each search slot; sampling is unchanged. */
#define DS18B20_SLOT_RECOVERY_EXTRA_US 20U
#if DS18B20_SEARCH_PASSES < 1U || DS18B20_SEARCH_PASSES > 255U || \
DS18B20_SEARCH_RETRIES > 254U || DS18B20_SEARCH_MAX_ATTEMPTS < 1U || \
DS18B20_SEARCH_MAX_ATTEMPTS > 65535U
#error Invalid DS18B20 search limits
#endif
#endif

View File

@@ -0,0 +1,235 @@
# DS18B20: перенос на другую платформу
## Что переносится без изменений
`Inc/ds18b20.h`, `Inc/ds18b20_config.h` и `Src/ds18b20.c` — переносимое
C-ядро. Оно использует `stdint.h`, `stddef.h`, `string.h` и аппаратные
callback. Не добавляйте в core HAL, RTOS, Modbus или глобальные дескрипторы
конкретного проекта.
## Шаг 1. Добавить файлы в сборку
Добавьте `Libraries/DS18B20/Inc` в include path и
`Libraries/DS18B20/Src/ds18b20.c` в список исходников. Создайте отдельные
каталоги `Port/<PLATFORM>/Inc` и `Port/<PLATFORM>/Src`.
Application adapter держите вне библиотеки либо в отдельном `Adapter`:
он планирует операции, переводит ошибки и связывает результат с остальным
проектом.
## Шаг 2. Создать platform context
Context содержит только ресурсы одного физического 1-Wire master:
```c
typedef struct {
gpio_handle_t gpio;
timer_handle_t timer;
uint32_t timer_ticks_per_us;
irq_state_t saved_irq_state;
} ds18b20_my_mcu_t;
```
Не используйте скрытый изменяемый singleton. Для двух шин создаются два context,
два `ds18b20_t` и два массива ROM.
## Шаг 3. Реализовать callbacks
```c
static void drive_low(void *context);
static void release_line(void *context);
static uint8_t read_line(void *context);
static void delay_us(void *context, uint32_t us);
static uint32_t tick_ms(void *context);
static void critical_enter(void *context);
static void critical_exit(void *context);
const ds18b20_onewire_ops_t ds18b20_my_mcu_ops = {
drive_low, release_line, read_line, delay_us, tick_ms,
critical_enter, critical_exit
};
```
### GPIO
- Линия 1-Wire работает только как open-drain: порт либо тянет её к 0, либо
переходит в высокоимпедансное состояние.
- Запрещён push-pull высокий уровень.
- `release_line` не должна ждать; `read_line` читает реальный pin level.
- Номинал внешнего pull-up и допустимая длина/ёмкость шины выбираются по
электрическим условиям конкретной платы.
STM32F4 port оставляет pin в `GPIO_MODE_OUTPUT_OD`, отпускает линию записью
единицы в BSRR и читает IDR без переключения MODER.
### Микросекундная задержка
`delay_us` обязана быть монотонной и достаточно точной для 1-Wire standard
speed. Не используйте scheduler sleep с миллисекундной гранулярностью. Если
задержка основана на hardware timer:
- таймер запускается до инициализации библиотеки;
- он считает непрерывно во всех вызывающих контекстах;
- учитывается переполнение счётчика;
- произведение `us * timer_ticks_per_us` не должно переполняться в диапазоне
используемых библиотекой задержек;
- частота таймера и `timer_ticks_per_us` должны совпадать.
### Tick и критическая секция
`tick_ms` нужен только для `ds18b20_wait()`; state-machine adapter может не
использовать блокирующий wait. Вычитание tick выполняется как `uint32_t` и
допускает wrap.
Critical callbacks должны сохранять и восстанавливать предыдущее состояние
прерываний, а не безусловно включать их. Если платформа гарантирует timing иначе,
оба callback можно оставить `NULL`.
## Шаг 4. Инициализировать экземпляр
```c
#define DS_CAPACITY 8U
static ds18b20_my_mcu_t port_context;
static ds18b20_t bus;
static uint8_t rom_storage[DS_CAPACITY][DS18B20_ROM_SIZE];
platform_gpio_timer_init(&port_context);
ds18b20_status_t status =
ds18b20_init(&bus, &ds18b20_my_mcu_ops, &port_context,
rom_storage, DS_CAPACITY);
```
Все три объекта должны жить столько же, сколько используется `bus`. Стековый
context или ROM storage нельзя передавать экземпляру, переживающему функцию.
## Шаг 5. Создать application adapter
Adapter должен:
1. выполнить явный поиск или восстановить/проверить известные ROM;
2. сериализовать Search ROM, Convert T, scratchpad и EEPROM-команды;
3. запустить Convert T и вернуть управление;
4. опрашивать ready с общим deadline;
5. читать каждый ROM и публиковать значение только после CRC;
6. восстановить state machine после disconnect, CRC error и timeout;
7. не запускать поиск/EEPROM-запись из обычного temperature polling;
8. синхронизировать доступ задач RTOS mutex-ом на уровне экземпляра.
Для циклического приложения вызывайте `ds18b20_search_step()` только когда
шина не занята преобразованием температуры или User Byte. Храните timeout,
отмену и sequence в адаптере приложения: portable core не зависит от HAL,
Modbus, GUI и глобального `hdallas`.
Не используйте critical callbacks библиотеки как mutex: они защищают короткий
1-Wire slot и могут запрещать прерывания.
## Память и выравнивание
- Core не использует heap.
- На каждую шину требуется `sizeof(ds18b20_t)` плюс
`rom_capacity * DS18B20_ROM_SIZE` байт ROM storage.
- Scratchpad — 9 байт у вызывающей стороны.
- Специального DMA-выравнивания core не требует; соблюдайте обычное выравнивание
C-типов для `ds18b20_t` и context.
- `rom_capacity` измеряется в элементах `uint8_t[8]`.
## Timing и питание
DS18B20 с внешним питанием может сигнализировать готовность через read slot.
Для parasite power требуется strong pull-up на всё время Convert T и
Copy Scratchpad. Контракт предоставляет optional callback
`strong_pullup(context, enable)`. Для поддержки режима порт обязан:
1. включить активный high не позднее 10 мкс после команды Copy Scratchpad;
2. удерживать его не менее 10 ms без другой активности 1-Wire;
3. безопасно отключать strong pull-up при success, timeout и error;
4. возвращать линию в open-drain idle перед Recall/readback;
5. проверить обычное и parasite-powered подключение на реальной плате.
## Перенос STM32F4 HAL port
Перед `ds18b20_stm32f4_hal_init()` заполните:
| Поле | Требование |
| --- | --- |
| `port` | Валидный `GPIO_TypeDef *` с включённым clock |
| `pin` | Одна ненулевая GPIO mask |
| `timer` | Запущенный свободно работающий `TIM_TypeDef *` |
| `timer_ticks_per_us` | Ненулевое число timer ticks за 1 мкс |
Порт использует `HAL_GetTick()` и CMSIS PRIMASK. При переносе на другую STM32
семью проверьте HAL-заголовок, разрядность/частоту timer, GPIO BSRR/IDR и способ
сохранения interrupt state.
## Проверки
Host/mock-проверки выполняются без платы:
```powershell
python -m unittest Libraries.PortableTests.test_portable_models
git diff --check
```
Для нового порта добавьте тесты reset/presence, write/read slots, timing bounds,
CRC error, нескольких экземпляров, timeout и восстановления после ошибки.
Затем выполните целевую сборку без ошибок/предупреждений.
## Перенос User Byte mailbox adapter
Core-модуль не зависит от Modbus. При переносе STM32 adapter сохраните
`selector + contractVersion=2` как одну транзакцию и APPLY как отдельную
последнюю запись. Input обязан возвращать selector/version echo. Legacy version
`0` можно принимать только для TH; TL без version `2` должен завершаться
`invalid`, а не значением TH.
Host-проверки adapter:
```powershell
powershell -ExecutionPolicy Bypass -File Modules/UserByte/Adapter/STM32_Modbus/Tests/run_host_tests.ps1
```
## Checklist
- [ ] Core собирается без HAL/RTOS/Modbus include.
- [ ] У каждой шины отдельные instance, context и ROM storage.
- [ ] GPIO физически open-drain и никогда не выдаёт push-pull high.
- [ ] Есть корректный внешний pull-up и общий GND.
- [ ] Таймер запущен, частота и overflow проверены.
- [ ] Critical section восстанавливает предыдущее состояние.
- [ ] Поиск, conversion, scratchpad и EEPROM сериализованы.
- [ ] У state machine есть deadline и восстановление после ошибок.
- [ ] CRC ROM и scratchpad проверяется до публикации данных.
- [ ] Host/mock-тесты и `git diff --check` проходят.
- [ ] Целевая сборка даёт 0 ошибок и 0 предупреждений.
- [ ] Работа проверена на реальной шине с 0, 1 и несколькими датчиками.
- [ ] Parasite power отмечен неподдерживаемым либо проверен со strong pull-up.
# Strong pull-up
Если порт поддерживает parasite power, добавьте callback
`strong_pullup(context, enable)`: включение должно немедленно активно держать
high после Copy Scratchpad, выключение — вернуть open-drain idle. Без безопасной
аппаратной реализации не разрешайте parasite mode вызывающему приложению.
### Импорт известных адресов
После ds18b20_init передайте каждый сохранённый ROM в
ds18b20_add_known_rom. Проверяйте возвращаемый статус и лимит ёмкости.
Хранилище ROM принадлежит вызывающему коду и живёт всё время работы шины.
Импорт не требует GPIO-транзакций и не заменяет проверку CRC температуры.
Сохраняйте список перед search_begin (он очищает результаты); по завершении
или отмене добавляйте прежние ID обратно вне активной конверсии.
Для нескольких шин храните снимки и происхождение ID отдельно для каждой.
### Перенос поиска с повторами
Пересоберите всех потребителей: ds18b20_t расширен состоянием поиска и
диагностикой. Рабочие буферы остаются caller-owned, HAL и Flash в ядре не нужны.
Только BUSY означает продолжение; остальные результаты терминальные.
Держите одного владельца шины до завершения/отмены поиска. Адаптер при отмене
сбрасывает search_active перед адресным опросом. Search begin нельзя запускать
во время strong pull-up. Пауза recovery действует только при search_active.
Новые callbacks не требуются; delay_us должен поддерживать добавочные 20 мкс.
Проверьте пределы config и внешний deadline. Для Modbus переносите два поля
диагностики лишь в свободные регистры, не сдвигая последующие окна.

View File

@@ -0,0 +1,97 @@
# Portable DS18B20
Переносимое ядро для поиска DS18B20 на шине 1-Wire, запуска преобразования,
чтения температуры, настройки разрешения и записи alarm/user bytes. Ядро не
зависит от STM32 HAL, не выделяет память динамически и поддерживает несколько
независимых экземпляров шин.
Подтверждённые неблокирующие операции `TH`/`TL` с Copy/Recall/readback находятся
во внутреннем модуле `Libraries/DS18B20/Modules/UserByte`. Основной core предоставляет ему
низкоуровневые операции scratchpad и optional callback `strong_pullup`.
## Подтверждённые EEPROM-байты
Официальный [datasheet Analog Devices/Maxim DS18B20](https://www.analog.com/media/en/technical-documentation/data-sheets/DS18B20.pdf)
определяет `TH=scratchpad[2]` и `TL=scratchpad[3]` как два независимо
программируемых alarm-регистра. `Write Scratchpad` принимает TH, TL и
configuration (`scratchpad[4]`), `Copy Scratchpad` сохраняет все три байта в
EEPROM, а `Recall E2` возвращает их в scratchpad. Заводские значения после
сброса: TH `+75` (`0x4B`), TL `+70` (`0x46`), configuration `0x7F`.
Scratchpad[5..7] зарезервированы/read-only и пользовательскими не считаются.
На реальном датчике пользователь отдельно прочитал TH `14`, TL `128` и config
`31`, подтвердив, что поля GUI должны оставаться независимыми. Проверка
сохранения после полного power-cycle и parasite-power всё ещё требует отдельной
аппаратной приёмки.
## Документация
- [HELP.md](HELP.md) — публичный API, инициализация, примеры, коды ошибок,
диагностика, ограничения и тесты.
- [PORTING.md](PORTING.md) — перенос на другой MCU/проект и checklist порта.
- [PROJECT_RELATIONS.md](PROJECT_RELATIONS.md) — слои, зависимости, владение
памятью и связи с текущей прошивкой.
## Структура
```text
DS18B20/
├── Inc/ публичный API и конфигурация
├── Src/ переносимое ядро 1-Wire/DS18B20
├── Port/STM32F4_HAL/Inc/ публичный API STM32F4-порта
├── Port/STM32F4_HAL/Src/ реализация GPIO/таймера STM32F4
├── README.md точка входа
├── HELP.md справочник API
├── PORTING.md руководство по переносу
└── PROJECT_RELATIONS.md место библиотеки в проекте
```
Адаптер текущего приложения находится отдельно:
`climate_control_f407vet6_f4/Core/Src/dallas_tools.c`.
## Минимальное подключение
```c
#include ds18b20.h
#include ds18b20_stm32f4_hal.h
#define DS_CAPACITY 8U
static ds18b20_t bus;
static uint8_t roms[DS_CAPACITY][DS18B20_ROM_SIZE];
static ds18b20_stm32f4_hal_t port = {
.port = GPIOE, .pin = GPIO_PIN_2,
.timer = TIM2, .timer_ticks_per_us = 72U
};
if (ds18b20_stm32f4_hal_init(&port) == DS18B20_OK &&
ds18b20_init(&bus, &ds18b20_stm32f4_hal_ops, &port,
roms, DS_CAPACITY) == DS18B20_OK) {
(void)ds18b20_search(&bus);
}
```
### Пошаговый поиск для GUI и Modbus
Для приложения с постоянно работающими сервисами используйте
`ds18b20_search_begin()` и `ds18b20_search_step()`. Один вызов `step`
обрабатывает не более одного кандидата ROM. `DS18B20_E_BUSY` означает, что
нужно вызвать функцию в следующем проходе цикла; `DS18B20_OK` завершает поиск.
`DS18B20_E_BUSY` также включает внутренние повторы и переходы между проходами.
Все остальные статусы терминальные; CRC_ERROR после исчерпания повторов
нельзя продолжать вызывать в цикле. Подробности — в [HELP.md](HELP.md).
Таймер должен быть заранее запущен и считать непрерывно. Значение
`timer_ticks_per_us` задаётся частотой счёта таймера, а не частотой ядра.
Преобразование температуры выполняйте неблокирующей парой
`ds18b20_start_all()` / `ds18b20_conversion_ready()`; полный сценарий приведён
в [HELP.md](HELP.md).
### Опрос по сохранённым ID
`ds18b20_add_known_rom(bus, rom)` добавляет проверенный ROM без обращения к
линии. Повторное добавление не создаёт дубликат. См. [HELP.md](HELP.md).
## Shared source
Canonical source: `templates/c/ds18b20/instance`. Used by `home/climate`; its old paths are compatibility includes. Board-specific ports remain in the application. Change this library, not the forwarding files.

View File

@@ -0,0 +1,353 @@
#include "ds18b20.h"
#include "ds18b20_config.h"
#include <string.h>
#define OW_SEARCH_ROM 0xF0U
#define OW_MATCH_ROM 0x55U
#define OW_SKIP_ROM 0xCCU
#define DS_CONVERT 0x44U
#define DS_READ_SCRATCHPAD 0xBEU
#define DS_WRITE_SCRATCHPAD 0x4EU
#define DS_COPY_SCRATCHPAD 0x48U
#define DS_RECALL_E2 0xB8U
static int valid_instance(const ds18b20_t *d)
{
return d && d->ops && d->ops->drive_low && d->ops->release &&
d->ops->read && d->ops->delay_us && d->roms && d->rom_capacity;
}
static void critical(ds18b20_t *d, int enter)
{
if (enter && d->ops->critical_enter) d->ops->critical_enter(d->platform_context);
if (!enter && d->ops->critical_exit) d->ops->critical_exit(d->platform_context);
}
static void write_bit(ds18b20_t *d, uint8_t bit)
{
critical(d, 1);
d->ops->drive_low(d->platform_context);
d->ops->delay_us(d->platform_context, bit ? 6U : 60U);
d->ops->release(d->platform_context);
d->ops->delay_us(d->platform_context, bit ? 64U : 10U);
critical(d, 0);
if (d->search_active)
d->ops->delay_us(d->platform_context, DS18B20_SLOT_RECOVERY_EXTRA_US);
}
static uint8_t read_bit(ds18b20_t *d)
{
uint8_t bit;
critical(d, 1);
d->ops->drive_low(d->platform_context);
d->ops->delay_us(d->platform_context, 3U);
d->ops->release(d->platform_context);
d->ops->delay_us(d->platform_context, 10U);
bit = d->ops->read(d->platform_context) ? 1U : 0U;
d->ops->delay_us(d->platform_context, 57U);
critical(d, 0);
if (d->search_active)
d->ops->delay_us(d->platform_context, DS18B20_SLOT_RECOVERY_EXTRA_US);
return bit;
}
static void write_byte(ds18b20_t *d, uint8_t value)
{
uint8_t i;
for (i = 0; i < 8U; ++i) { write_bit(d, value & 1U); value >>= 1U; }
}
static uint8_t read_byte(ds18b20_t *d)
{
uint8_t i, value = 0U;
for (i = 0; i < 8U; ++i) value |= (uint8_t)(read_bit(d) << i);
return value;
}
static ds18b20_status_t reset(ds18b20_t *d)
{
uint8_t level;
d->ops->drive_low(d->platform_context);
d->ops->delay_us(d->platform_context, 480U);
/* Protect release-to-presence sampling from interrupt latency. */
critical(d, 1);
d->ops->release(d->platform_context);
d->ops->delay_us(d->platform_context, 70U);
level = d->ops->read(d->platform_context);
critical(d, 0);
d->ops->delay_us(d->platform_context, 410U);
return level ? DS18B20_E_NO_DEVICE : DS18B20_OK;
}
static void match(ds18b20_t *d, const uint8_t *rom)
{
uint8_t i; write_byte(d, OW_MATCH_ROM);
for (i = 0U; i < 8U; ++i) write_byte(d, rom[i]);
}
uint8_t ds18b20_crc8(const void *data, size_t length)
{
const uint8_t *p = (const uint8_t *)data; uint8_t crc = 0U;
while (length--) { uint8_t in = *p++, i; for (i = 0U; i < 8U; ++i) {
uint8_t mix = (uint8_t)((crc ^ in) & 1U); crc >>= 1U;
if (mix) crc ^= 0x8CU; in >>= 1U; } }
return crc;
}
ds18b20_status_t ds18b20_validate_rom(const uint8_t rom[8])
{
if (!rom) return DS18B20_E_ARGUMENT;
if (rom[0] != DS18B20_FAMILY_CODE) return DS18B20_E_ROM;
return ds18b20_crc8(rom, 7U) == rom[7] ? DS18B20_OK : DS18B20_E_CRC;
}
ds18b20_status_t ds18b20_init(ds18b20_t *d, const ds18b20_onewire_ops_t *ops,
void *ctx, uint8_t (*roms)[8], size_t capacity)
{
if (!d || !ops || !roms || !capacity || !ops->drive_low || !ops->release ||
!ops->read || !ops->delay_us) return DS18B20_E_ARGUMENT;
memset(d, 0, sizeof(*d)); d->ops = ops; d->platform_context = ctx;
d->roms = roms; d->rom_capacity = capacity; ops->release(ctx);
return DS18B20_OK;
}
static ds18b20_status_t search_next(ds18b20_t *d)
{
uint8_t bit_no=1U,last_zero=0U,byte_no=0U,mask=1U;
ds18b20_status_t reset_status;
if (d->last_device) return DS18B20_OK;
d->search_diagnostics.last_error_bit = 0U;
d->search_diagnostics.last_error_pair = 0xFFU;
reset_status=reset(d);
if (reset_status != DS18B20_OK) return reset_status;
write_byte(d, OW_SEARCH_ROM);
while (byte_no < 8U) {
uint8_t id=read_bit(d), cmp=read_bit(d), dir;
d->search_diagnostics.last_error_bit = bit_no;
d->search_diagnostics.last_error_pair = (uint8_t)((id << 1U) | cmp);
if (id && cmp) break;
if (id != cmp) dir=id; else { dir=(bit_no<d->last_discrepancy) ?
((d->search_rom[byte_no]&mask)!=0U) : (bit_no==d->last_discrepancy);
if (!dir) { last_zero=bit_no; if (last_zero<9U) d->last_family_discrepancy=last_zero; } }
if (dir) d->search_rom[byte_no]|=mask; else d->search_rom[byte_no]&=(uint8_t)~mask;
write_bit(d,dir); ++bit_no; mask<<=1U; if (!mask) { ++byte_no; mask=1U; }
}
if (bit_no < 65U || !d->search_rom[0]) {
d->last_discrepancy=0; d->last_device=0; return DS18B20_E_IO;
}
d->last_discrepancy=last_zero;
if (!last_zero) d->last_device=1U;
return DS18B20_E_BUSY;
}
/* Reset only traversal state: the union of validated ROMs survives passes. */
static void search_tree_reset(ds18b20_t *d)
{
d->last_discrepancy = 0U;
d->last_family_discrepancy = 0U;
d->last_device = 0U;
d->search_retries = 0U;
memset(d->search_rom, 0, sizeof(d->search_rom));
}
static ds18b20_status_t search_stop(ds18b20_t *d, ds18b20_status_t result)
{
d->search_active = 0U;
d->search_result = result;
return result;
}
static ds18b20_status_t search_pass_finish(ds18b20_t *d, uint8_t complete)
{
ds18b20_search_diagnostics_t *diag = &d->search_diagnostics;
++diag->passes_finished;
if (complete) ++diag->complete_passes;
if (diag->passes_finished < DS18B20_SEARCH_PASSES) {
search_tree_reset(d);
return DS18B20_E_BUSY;
}
/* A completed traversal can recover earlier faults. Without one, retain
* the partial catalog but report the failure instead of claiming success. */
if (diag->complete_passes != 0U)
return search_stop(d, d->rom_count ? DS18B20_OK : DS18B20_E_NO_DEVICE);
return search_stop(d, diag->last_error);
}
ds18b20_status_t ds18b20_search_begin(ds18b20_t *d)
{
if (!valid_instance(d)) return DS18B20_E_ARGUMENT;
if (d->strong_pullup_active) return DS18B20_E_BUSY;
d->rom_count = 0U;
search_tree_reset(d);
memset(&d->search_diagnostics, 0, sizeof(d->search_diagnostics));
d->search_diagnostics.last_error_pair = 0xFFU;
d->search_active = 1U;
d->search_result = DS18B20_E_BUSY;
return DS18B20_E_BUSY;
}
ds18b20_status_t ds18b20_search_step(ds18b20_t *d)
{
uint8_t previous_rom[8], previous_discrepancy, previous_family;
ds18b20_search_diagnostics_t previous_diag;
ds18b20_status_t status;
if (!valid_instance(d)) return DS18B20_E_ARGUMENT;
if (!d->search_active) return d->search_result;
if (d->search_diagnostics.attempts >= DS18B20_SEARCH_MAX_ATTEMPTS) {
d->search_diagnostics.last_error = DS18B20_E_TIMEOUT;
d->search_diagnostics.last_error_bit = 0U;
d->search_diagnostics.last_error_pair = 0xFFU;
return search_stop(d, DS18B20_E_TIMEOUT);
}
/* Snapshot the preceding validated tree path BEFORE touching the wire.
* A corrupt ROM must never become the path for the following attempt. */
memcpy(previous_rom, d->search_rom, 8U);
previous_discrepancy = d->last_discrepancy;
previous_family = d->last_family_discrepancy;
previous_diag = d->search_diagnostics;
++d->search_diagnostics.attempts;
status = search_next(d);
if (status == DS18B20_E_BUSY) {
/* CRC first: a valid non-DS18B20 family is skipped without corrupting
* the traversal; a bad family caused by noise still gets retried. */
status = ds18b20_crc8(d->search_rom, 7U) == d->search_rom[7] ?
DS18B20_OK : DS18B20_E_CRC;
if (status == DS18B20_OK && d->search_rom[0] == DS18B20_FAMILY_CODE)
status = ds18b20_add_known_rom(d, d->search_rom);
}
if (status == DS18B20_OK) {
/* Keep the last FAILURE location, even when a later retry succeeds. */
d->search_diagnostics.last_error_bit = previous_diag.last_error_bit;
d->search_diagnostics.last_error_pair = previous_diag.last_error_pair;
d->search_retries = 0U;
return d->last_device ? search_pass_finish(d, 1U) : DS18B20_E_BUSY;
}
d->search_diagnostics.last_error = status;
if (status == DS18B20_E_CAPACITY) return search_stop(d, status);
if (status == DS18B20_E_CRC) ++d->search_diagnostics.crc_errors;
else ++d->search_diagnostics.io_errors;
memcpy(d->search_rom, previous_rom, 8U);
d->last_discrepancy = previous_discrepancy;
d->last_family_discrepancy = previous_family;
d->last_device = 0U;
if (d->search_retries < DS18B20_SEARCH_RETRIES) {
++d->search_retries;
++d->search_diagnostics.retries;
return DS18B20_E_BUSY;
}
return search_pass_finish(d, 0U);
}
ds18b20_status_t ds18b20_search(ds18b20_t *d)
{
ds18b20_status_t status;
if (!valid_instance(d)) return DS18B20_E_ARGUMENT;
if (d->strong_pullup_active) return DS18B20_E_BUSY;
status = ds18b20_search_begin(d);
while (status == DS18B20_E_BUSY) status = ds18b20_search_step(d);
return status;
}
ds18b20_status_t ds18b20_add_known_rom(ds18b20_t *d, const uint8_t rom[8])
{
size_t i;
ds18b20_status_t status;
if (!valid_instance(d)) return DS18B20_E_ARGUMENT;
status = ds18b20_validate_rom(rom);
if (status != DS18B20_OK) return status;
for (i = 0U; i < d->rom_count; ++i)
if (memcmp(d->roms[i], rom, 8U) == 0) return DS18B20_OK;
if (d->rom_count >= d->rom_capacity) return DS18B20_E_CAPACITY;
memcpy(d->roms[d->rom_count++], rom, 8U);
return DS18B20_OK;
}
size_t ds18b20_count(const ds18b20_t *d) { return valid_instance(d) ? d->rom_count : 0U; }
const uint8_t *ds18b20_rom(const ds18b20_t *d,size_t i) { return valid_instance(d)&&i<d->rom_count?d->roms[i]:NULL; }
ds18b20_status_t ds18b20_start_all(ds18b20_t *d)
{ if(!valid_instance(d))return DS18B20_E_ARGUMENT; if(reset(d))return DS18B20_E_NO_DEVICE; write_byte(d,OW_SKIP_ROM);write_byte(d,DS_CONVERT);return DS18B20_OK; }
ds18b20_status_t ds18b20_start(ds18b20_t *d,const uint8_t *rom)
{ ds18b20_status_t s;if(!valid_instance(d)||!rom)return DS18B20_E_ARGUMENT;if(ds18b20_validate_rom(rom))return DS18B20_E_ROM;s=reset(d);if(s)return s;match(d,rom);write_byte(d,DS_CONVERT);return DS18B20_OK; }
ds18b20_status_t ds18b20_conversion_ready(ds18b20_t *d)
{ if(!valid_instance(d))return DS18B20_E_ARGUMENT;return read_bit(d)?DS18B20_OK:DS18B20_E_BUSY; }
ds18b20_status_t ds18b20_wait(ds18b20_t *d,uint32_t timeout)
{ uint32_t start;if(!valid_instance(d)||!d->ops->tick_ms)return DS18B20_E_ARGUMENT;start=d->ops->tick_ms(d->platform_context);while(!read_bit(d))if((uint32_t)(d->ops->tick_ms(d->platform_context)-start)>timeout)return DS18B20_E_TIMEOUT;return DS18B20_OK; }
ds18b20_status_t ds18b20_read_scratchpad(ds18b20_t *d,const uint8_t *rom,uint8_t *sp)
{ uint8_t i;ds18b20_status_t s;if(!valid_instance(d)||!rom||!sp)return DS18B20_E_ARGUMENT;if(ds18b20_validate_rom(rom))return DS18B20_E_ROM;s=reset(d);if(s)return s;match(d,rom);write_byte(d,DS_READ_SCRATCHPAD);for(i=0;i<9U;++i)sp[i]=read_byte(d);return ds18b20_crc8(sp,8U)==sp[8]?DS18B20_OK:DS18B20_E_CRC; }
ds18b20_status_t ds18b20_decode_temperature(const uint8_t *sp,float *out)
{ int16_t raw;uint8_t cfg;if(!sp||!out)return DS18B20_E_ARGUMENT;if(ds18b20_crc8(sp,8U)!=sp[8])return DS18B20_E_CRC;cfg=sp[4]&0x60U;raw=(int16_t)((uint16_t)sp[0]|((uint16_t)sp[1]<<8));if(cfg==0)raw&=(int16_t)~7;else if(cfg==0x20)raw&=(int16_t)~3;else if(cfg==0x40)raw&=(int16_t)~1;else if(cfg!=0x60)return DS18B20_E_IO;*out=(float)raw/16.0f;return DS18B20_OK; }
static ds18b20_status_t write_config(ds18b20_t *d,const uint8_t *rom,uint8_t th,uint8_t tl,uint8_t cfg)
{
uint8_t chunk;
ds18b20_status_t s=ds18b20_write_scratchpad(d,rom,th,tl,cfg);
if(s)return s;s=ds18b20_copy_scratchpad_start(d,rom,0U);if(s)return s;
/* TIM1 counter is 16-bit: at 72 ticks/us a single 10 ms wait can never
* satisfy the port comparison. Short chunks preserve wrap-safe timing. */
for(chunk=0U;chunk<100U;++chunk)d->ops->delay_us(d->platform_context,100U);
return ds18b20_copy_scratchpad_finish(d,0U);
}
ds18b20_status_t ds18b20_set_resolution(ds18b20_t *d,const uint8_t *rom,uint8_t bits)
{ uint8_t sp[9],cfg;ds18b20_status_t s;if(bits<9U||bits>12U)return DS18B20_E_ARGUMENT;s=ds18b20_read_scratchpad(d,rom,sp);if(s)return s;cfg=(uint8_t)(0x1FU|((bits-9U)<<5));return write_config(d,rom,sp[2],sp[3],cfg); }
ds18b20_status_t ds18b20_write_user_bytes(ds18b20_t *d,const uint8_t *rom,int16_t b12,int16_t b34,uint8_t mask)
{ uint8_t sp[9];ds18b20_status_t s=ds18b20_read_scratchpad(d,rom,sp);(void)b34;if(s)return s;if(mask&1U)sp[2]=(uint8_t)b12;if(mask&2U)sp[3]=(uint8_t)(b12>>8);/* DS18B20 физически позволяет записать только TH/TL/config; байты 6/7 read-only. */return write_config(d,rom,sp[2],sp[3],sp[4]); }
ds18b20_status_t ds18b20_write_scratchpad(ds18b20_t *d,const uint8_t *rom,
uint8_t th,uint8_t tl,uint8_t cfg)
{
ds18b20_status_t s;
if(!valid_instance(d)||!rom)return DS18B20_E_ARGUMENT;
s=ds18b20_validate_rom(rom);if(s)return s;
s=reset(d);if(s)return s;match(d,rom);write_byte(d,DS_WRITE_SCRATCHPAD);
write_byte(d,th);write_byte(d,tl);write_byte(d,cfg);return DS18B20_OK;
}
ds18b20_status_t ds18b20_copy_scratchpad_start(ds18b20_t *d,const uint8_t *rom,
uint8_t parasite)
{
ds18b20_status_t s;
if(!valid_instance(d)||!rom)return DS18B20_E_ARGUMENT;
if(parasite && !d->ops->strong_pullup)return DS18B20_E_POWER;
s=ds18b20_validate_rom(rom);if(s)return s;
s=reset(d);if(s)return s;match(d,rom);write_byte(d,DS_COPY_SCRATCHPAD);
/* The pull-up must be asserted immediately after the command slot. */
if(parasite){d->ops->strong_pullup(d->platform_context,1U);
d->strong_pullup_active=1U;}
return DS18B20_OK;
}
ds18b20_status_t ds18b20_copy_scratchpad_finish(ds18b20_t *d,uint8_t parasite)
{
if(!valid_instance(d))return DS18B20_E_ARGUMENT;
if(parasite){if(!d->ops->strong_pullup)return DS18B20_E_POWER;
if(d->strong_pullup_active){d->ops->strong_pullup(d->platform_context,0U);
d->strong_pullup_active=0U;}}
d->ops->release(d->platform_context);return DS18B20_OK;
}
ds18b20_status_t ds18b20_recall_e2(ds18b20_t *d,const uint8_t *rom)
{
ds18b20_status_t s;if(!valid_instance(d)||!rom)return DS18B20_E_ARGUMENT;
s=ds18b20_validate_rom(rom);if(s)return s;s=reset(d);if(s)return s;
match(d,rom);write_byte(d,DS_RECALL_E2);return DS18B20_OK;
}
ds18b20_status_t ds18b20_recall_ready(ds18b20_t *d)
{if(!valid_instance(d))return DS18B20_E_ARGUMENT;return read_bit(d)?DS18B20_OK:DS18B20_E_BUSY;}
void ds18b20_recover_bus(ds18b20_t *d)
{
if(!valid_instance(d))return;
if(d->strong_pullup_active&&d->ops->strong_pullup){
d->ops->strong_pullup(d->platform_context,0U);d->strong_pullup_active=0U;}
d->ops->release(d->platform_context);
}
uint8_t ds18b20_search_retry_due(const ds18b20_t *d,uint8_t conversion_active,
uint32_t now,uint32_t last,uint32_t period)
{
if(!valid_instance(d)||d->rom_count!=0U||conversion_active||period==0U)return 0U;
/* Unsigned subtraction keeps the retry correct across HAL tick rollover. */
return ((uint32_t)(now-last)>=period)?1U:0U;
}

View File

@@ -0,0 +1,31 @@
$ErrorActionPreference = "Stop"
$out = Join-Path $env:TEMP "test_ds18b20_copy_delay.exe"
$test = Join-Path $PSScriptRoot "test_ds18b20_copy_delay.c"
$core = Join-Path $PSScriptRoot "..\Src\ds18b20.c"
$inc = Join-Path $PSScriptRoot "..\Inc"
$gcc = Get-Command gcc -ErrorAction SilentlyContinue
if ($gcc) {
& $gcc.Source -std=c99 -Wall -Wextra -Werror -I $inc $test $core -o $out
} else {
$vcvars = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"
if (-not (Test-Path $vcvars)) { throw "Neither gcc nor MSVC was found" }
$command = "call `"$vcvars`" >nul && pushd `"$env:TEMP`" && cl /nologo /std:c11 /W4 /WX /I `"$inc`" `"$test`" `"$core`" /Fe:`"$out`""
& cmd.exe /d /c $command
}
if ($LASTEXITCODE -ne 0) { throw "DS18B20 regression compilation failed" }
& $out
if ($LASTEXITCODE -ne 0) { throw "DS18B20 regression failed" }
Remove-Item -LiteralPath $out -Force
$searchOut = Join-Path $env:TEMP "test_ds18b20_incremental_search.exe"
$searchTest = Join-Path $PSScriptRoot "test_ds18b20_incremental_search.c"
if ($gcc) {
& $gcc.Source -std=c99 -Wall -Wextra -Werror -I $inc $searchTest $core -o $searchOut
} else {
$command = "call `"$vcvars`" >nul && pushd `"$env:TEMP`" && cl /nologo /std:c11 /W4 /WX /I `"$inc`" `"$searchTest`" `"$core`" /Fe:`"$searchOut`""
& cmd.exe /d /c $command
}
if ($LASTEXITCODE -ne 0) { throw "DS18B20 incremental search compilation failed" }
& $searchOut
if ($LASTEXITCODE -ne 0) { throw "DS18B20 incremental search failed" }
Remove-Item -LiteralPath $searchOut -Force

View File

@@ -0,0 +1,92 @@
#include "ds18b20.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
typedef struct {
uint8_t scratchpad[DS18B20_SCRATCHPAD_SIZE];
uint16_t read_call;
uint32_t max_delay_us;
uint16_t copy_delay_chunks;
uint8_t critical_active;
} mock_port_t;
static void drive_low(void *context) { (void)context; }
static void release_line(void *context) { (void)context; }
static uint8_t read_line(void *context)
{
mock_port_t *port = (mock_port_t *)context;
uint16_t call = port->read_call++;
assert(port->critical_active); /* Presence and data sampling must be protected. */
if (call == 0U) return 0U; /* Read Scratchpad reset presence. */
if (call <= 72U) {
uint16_t bit = (uint16_t)(call - 1U);
return (uint8_t)((port->scratchpad[bit / 8U] >> (bit % 8U)) & 1U);
}
/* Write Scratchpad and Copy Scratchpad reset presence pulses. */
return 0U;
}
static void delay_us(void *context, uint32_t us)
{
mock_port_t *port = (mock_port_t *)context;
if (us > port->max_delay_us) port->max_delay_us = us;
if (us == 100U) port->copy_delay_chunks++;
}
static void critical_enter(void *context)
{
mock_port_t *port = (mock_port_t *)context;
assert(!port->critical_active);
port->critical_active = 1U;
}
static void critical_exit(void *context)
{
mock_port_t *port = (mock_port_t *)context;
assert(port->critical_active);
port->critical_active = 0U;
}
int main(void)
{
static const ds18b20_onewire_ops_t ops = {
drive_low, release_line, read_line, delay_us, NULL, critical_enter, critical_exit, NULL
};
ds18b20_t bus;
mock_port_t port;
uint8_t roms[1][DS18B20_ROM_SIZE] = {{0}};
memset(&port, 0, sizeof(port));
roms[0][0] = 0x28U;
roms[0][1] = 0x11U;
roms[0][7] = ds18b20_crc8(roms[0], 7U);
port.scratchpad[0] = 0x50U;
port.scratchpad[1] = 0x05U;
port.scratchpad[2] = 0x4BU;
port.scratchpad[3] = 0x46U;
port.scratchpad[4] = 0x7FU;
port.scratchpad[5] = 0xFFU;
port.scratchpad[6] = 0x0CU;
port.scratchpad[7] = 0x10U;
port.scratchpad[8] = ds18b20_crc8(port.scratchpad, 8U);
assert(ds18b20_init(&bus, &ops, &port, roms, 1U) == DS18B20_OK);
bus.rom_count = 1U;
assert(ds18b20_set_resolution(&bus, roms[0], 9U) == DS18B20_OK);
/* Regression: a 10 ms callback overflows the 16-bit 72 MHz timer port. */
assert(port.max_delay_us <= 480U);
assert(port.copy_delay_chunks >= 100U);
bus.rom_count = 0U;
assert(ds18b20_search_retry_due(&bus, 0U, 1999U, 0U, 2000U) == 0U);
assert(ds18b20_search_retry_due(&bus, 0U, 2000U, 0U, 2000U) == 1U);
assert(ds18b20_search_retry_due(&bus, 1U, 4000U, 0U, 2000U) == 0U);
/* Wrap-safe deadline: 0x20 - 0xFFFFFF00 = 0x120 ms. */
assert(ds18b20_search_retry_due(&bus, 0U, 0x20U, 0xFFFFFF00U, 0x120U) == 1U);
bus.rom_count = 1U;
assert(ds18b20_search_retry_due(&bus, 0U, 4000U, 0U, 2000U) == 0U);
puts("DS18B20 copy delay regression: OK");
return 0;
}

View File

@@ -0,0 +1,152 @@
/* Model real SEARCH ROM participation: each branch filters the active slaves.
* Fault injection tests transport recovery, not analog cable characteristics. */
#include "ds18b20.h"
#include "ds18b20_config.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
typedef struct {
ds18b20_t *bus;
uint8_t roms[3][8];
uint8_t masks[3];
unsigned active, command_bits, bit, pair, presence, attempts;
unsigned low_time, elapsed, low, critical, recovery;
unsigned fault_attempt, fault_bit, persistent_fault, crc_fault;
} port_t;
static void low(void *ctx) {
port_t *p = ctx; p->low = 1U; p->low_time = 0U; p->elapsed = 0U;
}
static unsigned rom_bit(port_t *p, unsigned i) {
return (p->roms[i][p->bit / 8U] >> (p->bit % 8U)) & 1U;
}
static unsigned fault(port_t *p) {
return p->attempts == p->fault_attempt ||
(p->persistent_fault && p->attempts >= p->fault_attempt);
}
static void release_line(void *ctx) {
port_t *p = ctx; unsigned i, direction;
if (!p->low) return;
p->low = 0U;
if (p->low_time == 480U) {
++p->attempts;
p->active = p->masks[p->bus->search_diagnostics.passes_finished];
p->presence = 1U; p->command_bits = 0U; p->bit = 0U; p->pair = 0U;
} else if (p->low_time != 3U) {
assert(p->low_time == 6U || p->low_time == 60U);
if (p->command_bits < 8U) { ++p->command_bits; return; }
direction = p->low_time == 6U;
for (i = 0U; i < 3U; ++i)
if (rom_bit(p, i) != direction) p->active &= ~(1U << i);
++p->bit; p->pair = 0U;
}
}
static uint8_t read_line(void *ctx) {
port_t *p = ctx; unsigned i, zeros = 0U, ones = 0U, value;
assert(p->critical);
if (p->presence) {
assert(p->elapsed == 550U); p->presence = 0U;
return p->active ? 0U : 1U;
}
assert(p->elapsed == 13U); /* The read sample did not move. */
assert(p->bit < 64U);
for (i = 0U; i < 3U; ++i) if (p->active & (1U << i)) {
if (rom_bit(p, i)) ++ones; else ++zeros;
}
value = p->pair++ == 0U ? !zeros : !ones;
if (fault(p) && p->bit + 1U == p->fault_bit)
value = p->crc_fault ? !value : 1U;
return (uint8_t)value;
}
static void delay(void *ctx, uint32_t us) {
port_t *p = ctx; p->elapsed += us;
if (p->low) p->low_time += us;
if (us == DS18B20_SLOT_RECOVERY_EXTRA_US) {
assert(!p->critical && !p->low); ++p->recovery;
}
}
static void enter(void *ctx) { port_t *p = ctx; assert(!p->critical); p->critical = 1U; }
static void leave(void *ctx) { port_t *p = ctx; assert(p->critical); p->critical = 0U; }
static const ds18b20_onewire_ops_t ops = {low, release_line, read_line, delay, NULL, enter, leave, NULL};
static void setup(ds18b20_t *bus, port_t *p, uint8_t storage[][8], size_t cap) {
unsigned i;
memset(p, 0, sizeof(*p)); p->bus = bus;
for (i = 0U; i < 3U; ++i) {
p->roms[i][0] = 0x28U; p->roms[i][1] = (uint8_t)(i + 1U);
p->roms[i][7] = ds18b20_crc8(p->roms[i], 7U);
p->masks[i] = 7U;
}
assert(ds18b20_init(bus, &ops, p, storage, cap) == DS18B20_OK);
}
static ds18b20_status_t finish(ds18b20_t *bus, port_t *p) {
ds18b20_status_t s; unsigned calls = 0U, before;
do {
before = p->attempts; s = ds18b20_search_step(bus);
assert(p->attempts <= before + 1U); /* Never retries in a tight loop. */
assert(++calls <= DS18B20_SEARCH_MAX_ATTEMPTS + 1U);
} while (s == DS18B20_E_BUSY);
before = p->attempts;
assert(ds18b20_search_step(bus) == s && p->attempts == before);
return s;
}
int main(void) {
ds18b20_t bus; port_t p; uint8_t storage[3][8], saved[8], discrepancy, family;
setup(&bus, &p, storage, 3U);
assert(ds18b20_search(&bus) == DS18B20_OK);
assert(bus.rom_count == 3U && p.attempts == 9U);
assert(bus.search_diagnostics.complete_passes == 3U && p.recovery != 0U);
assert(bus.search_diagnostics.last_error == DS18B20_OK);
/* A fault on the second branch must restore the PREVIOUS tree path. */
setup(&bus, &p, storage, 3U); p.fault_attempt = 2U; p.fault_bit = 17U;
ds18b20_search_begin(&bus);
assert(ds18b20_search_step(&bus) == DS18B20_E_BUSY);
memcpy(saved, bus.search_rom, 8U); discrepancy = bus.last_discrepancy;
family = bus.last_family_discrepancy;
assert(ds18b20_search_step(&bus) == DS18B20_E_BUSY);
assert(memcmp(saved, bus.search_rom, 8U) == 0);
assert(bus.last_discrepancy == discrepancy && bus.last_family_discrepancy == family);
assert(finish(&bus, &p) == DS18B20_OK && bus.rom_count == 3U);
assert(bus.search_diagnostics.retries == 1U);
assert(bus.search_diagnostics.last_error_bit == 17U && bus.search_diagnostics.last_error_pair == 3U);
setup(&bus, &p, storage, 3U); p.fault_attempt = 2U; p.fault_bit = 64U; p.crc_fault = 1U;
assert(ds18b20_search(&bus) == DS18B20_OK && bus.rom_count == 3U);
assert(bus.search_diagnostics.crc_errors == 1U && bus.search_diagnostics.retries == 1U);
assert(bus.search_diagnostics.last_error_bit == 64U);
/* Each pass sees a different device; the validated union retains all three. */
setup(&bus, &p, storage, 3U); p.masks[0] = 1U; p.masks[1] = 2U; p.masks[2] = 4U;
assert(ds18b20_search(&bus) == DS18B20_OK && bus.rom_count == 3U);
assert(p.attempts == 3U);
setup(&bus, &p, storage, 3U); memset(p.masks, 0, sizeof(p.masks));
assert(ds18b20_search(&bus) == DS18B20_E_NO_DEVICE);
assert(p.attempts == 15U && bus.search_diagnostics.retries == 12U);
assert(bus.search_diagnostics.last_error_bit == 0U);
assert(ds18b20_add_known_rom(&bus, p.roms[0]) == DS18B20_OK);
assert(ds18b20_add_known_rom(&bus, p.roms[0]) == DS18B20_OK && bus.rom_count == 1U);
assert(ds18b20_add_known_rom(&bus, NULL) == DS18B20_E_ARGUMENT);
p.roms[1][7] ^= 1U;
assert(ds18b20_add_known_rom(&bus, p.roms[1]) == DS18B20_E_CRC);
setup(&bus, &p, storage, 3U); p.fault_attempt = 2U; p.fault_bit = 17U; p.persistent_fault = 1U;
assert(ds18b20_search(&bus) == DS18B20_E_IO && bus.rom_count == 1U);
assert(p.attempts == 16U && bus.search_diagnostics.retries == 12U);
assert(bus.search_diagnostics.complete_passes == 0U);
setup(&bus, &p, storage, 3U); p.fault_attempt = 1U; p.fault_bit = 64U;
p.persistent_fault = 1U; p.crc_fault = 1U;
assert(ds18b20_search(&bus) == DS18B20_E_CRC && bus.rom_count == 0U);
assert(bus.search_diagnostics.crc_errors == 15U);
setup(&bus, &p, storage, 1U);
assert(ds18b20_search(&bus) == DS18B20_E_CAPACITY && bus.rom_count == 1U);
setup(&bus, &p, storage, 3U); ds18b20_search_begin(&bus);
bus.search_diagnostics.attempts = DS18B20_SEARCH_MAX_ATTEMPTS;
assert(finish(&bus, &p) == DS18B20_E_TIMEOUT && p.attempts == 0U);
puts("DS18B20 robust search, retries, CRC, union, limits and timing: OK");
return 0;
}

View File

@@ -1,11 +1,144 @@
# PORTING
# Портирование firmware-info
Аппаратно-зависимый контракт ограничен `firmware_info_config.h`:
`FIRMWARE_VERSION_MAJOR`, `FIRMWARE_VERSION_MINOR`, `FIRMWARE_VERSION_PATCH` и
необязательный `FIRMWARE_BUILD_ID`. Выберите порт по семейству МК или создайте
его копию. Публикация — ответственность транспорта: 12 `uint16_t` для Modbus
либо 24 little-endian байта для SETGUI.
## Назначение и границы
При переносе проверьте: поддержку `__DATE__`/`__TIME__`, наличие generated в
include path, запуск генератора до компиляции и декодирование build ID как двух
ASCII-байтов в каждом логическом слове.
Это C-библиотека описания работающего образа: SemVer, дата/время компиляции,
8 символов build ID и сериализация контракта v1. Она не обращается к сети,
не читает версию из сервера и не записывает Flash. HAL, RTOS, UART и CAN
ядру не нужны. Отправку результата выполняет приложение.
Для размещения `.hex/.bin` в каталоге SETGUI уже существуют
[`setprotocol.firmware_publish`](../../python/setprotocol/firmware_publish.py)
и [`tools/firmware-publish`](../../tools/firmware-publish/README.md).
Связь компонентов и перенос публикации описаны в
[`tools/firmware-publish/PORTING.md`](../../tools/firmware-publish/PORTING.md).
## 1. Файлы и конфигурация
Пример ниже предполагает `lib/templates` внутри нового проекта:
```text
project/
inc/firmware_info_config.h
generated/firmware_build_id.h # создаётся до сборки
lib/templates/c/firmware-info/
src/
```
Добавьте в сборку оба файла:
```text
lib/templates/c/firmware-info/src/firmware_info.c
lib/templates/c/firmware-info/src/firmware_info_port.c
```
В include path добавьте `inc`, `generated` и
`lib/templates/c/firmware-info/include`. Встроенный `CMakeLists.txt` собирает
только ядро и host-тест; `firmware_info_port.c` и каталоги конфигурации нужно
добавлять к целевому firmware target самостоятельно.
Скопируйте `ports/<mcu>/firmware_info_config.template.h` как
`inc/firmware_info_config.h`. Есть варианты STM32F1/F4/G4 и К1921ВК028;
они не содержат регистров МК. Для другого МК с обычными 8-битными байтами
достаточно такого же config:
```c
#ifndef FIRMWARE_INFO_CONFIG_H
#define FIRMWARE_INFO_CONFIG_H
#define FIRMWARE_VERSION_MAJOR 1U
#define FIRMWARE_VERSION_MINOR 2U
#define FIRMWARE_VERSION_PATCH 3U
#include "firmware_build_id.h"
#endif
```
Обязательный include в этом примере позволяет обнаружить пропущенный pre-build.
В готовых шаблонах include опциональный через `__has_include`; если заголовок
не подключён, порт использует `LOCALDEV`. Для компилятора без `__has_include`
используйте явный include. Можно связать три версии с существующими макросами
проекта, как это сделано в `KONOR_ds18b20/inc/firmware_info_config.h`.
## 2. Build ID
Из корня нового проекта перед компиляцией запустите:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File "lib/templates/c/firmware-info/tools/make_build_id.ps1" -Repository "." -Output "generated/firmware_build_id.h"
```
Указывайте `-Repository` и `-Output` явно: расположение сабмодуля и рабочий
каталог IDE различаются между проектами. `-Repository` должен указывать на
исходники прошивки, а не на репозиторий `templates`. Для Keil из каталога `mdk`
соответственно используйте `..\lib\templates\...`, `-Repository ".."` и
`-Output ".\Generated\firmware_build_id.h"`.
При доступном Git чистый checkout получает 8 знаков commit, изменения tracked
файлов — 7 знаков и `+`. Если commit определить нельзя, используется `NOGIT000`.
Новые untracked-файлы текущий генератор при определении dirty не учитывает.
Дата/время берутся из `__DATE__`/`__TIME__` при компиляции `firmware_info_port.c`;
для выпуска выполняйте полный Rebuild, чтобы не оставить старый объектный файл.
## 3. Обработчик запроса версии
Пример функции подготовки полезной нагрузки, вызываемой вашим обработчиком:
```c
#include "firmware_info_port.h"
firmware_info_status_t app_make_firmware_info(uint8_t *payload, size_t capacity)
{
firmware_info_t info;
firmware_info_status_t status = firmware_info_port_describe(&info);
if (status != FIRMWARE_INFO_OK) return status;
return firmware_info_to_le_bytes(&info, payload, capacity);
}
```
При успехе передайте ровно `FIRMWARE_INFO_PAYLOAD_SIZE` (24) байта в собственный
транспорт. При ошибке верните ошибку протокола, а не содержимое буфера.
Не отправляйте `sizeof(firmware_info_t)`: структура не является wire format.
В KONOR обработчик `app_send_firmware_info()` отвечает на
`PROTO_MSG_FIRMWARE_INFO = 0x03`, сохраняя sequence запроса. Старый 32-байтовый
`DEVICE_INFO` остаётся отдельным сообщением. Для нового протокола сначала
согласуйте команду с клиентом: подключение библиотеки само по себе не добавит
декодер и отображение версии в GUI.
Для Modbus вызовите `firmware_info_to_registers()` с массивом из 12 `uint16_t`
и разместите его в выбранной карте регистров. Адреса и Modbus-порядок байтов
обеспечивает ваш Modbus-стек; LE-буфер в Modbus напрямую не копируйте.
## 4. Контракт v1
| Индекс слова | Содержимое |
|---|---|
| 0 | Версия контракта: 1 |
| 1, 2, 3 | major, minor, patch |
| 4 | Год |
| 5 | `(month << 8) \| day` |
| 6 | `(hour << 8) \| minute` |
| 7 | Секунды |
| 8–11 | По два ASCII-символа build ID: первый в старшем байте слова |
В LE-представлении младший байт каждого слова идёт первым. Например, build ID
`ab` в начале строки даёт слово `0x6162`, но байты `62 61`; декодируйте сначала
слово, затем символы из старшего/младшего байта. NUL-терминатор не передаётся.
Текущая проверка допускает major/minor до 255 и patch до 999. Если в каталоге
используется `(major << 16) | (minor << 8) | patch`, ограничьте **все три** части
до 255, иначе значения пересекутся. Версия каталога автоматически из C-config
не извлекается.
Для C2000 с 16-битным `char` нельзя считать готовым байтовый порт: отдельно
проверьте наличие `uint8_t` и представление октетов в транспорте. Публикация
TMS SCI8-файла с ПК поддерживается независимо от переноса этой C-библиотеки.
## 5. Проверка переноса
1. Соберите host-тест из `tests/test_firmware_info.c` вместе с ядром либо
используйте CMake/CTest. Он проверяет дату, регистры и порядок байтов build ID.
2. Соберите целевую прошивку с обоими `.c` и сгенерированным заголовком.
3. Запросите версию у устройства: сравните SemVer, build ID, время и 24-байтовый
ответ с конкретной сборкой. Проверьте, что прежний `DEVICE_INFO` не изменился.
4. Подключите выпуск по инструкции публикатора; сравните версию в config и
`firmware-release.cmd` перед Rebuild и `--preflight`.

View File

@@ -25,3 +25,7 @@ config, а не дублирования ядра.
KONOR публикует эти 24 байта ответом `FIRMWARE_INFO (0x03)`, сохраняя старый
32-байтовый `DEVICE_INFO` без изменений.
Подробная инструкция с кодом обработчика, форматом ответа и проверкой переноса:
[`PORTING.md`](PORTING.md). Для размещения файла прошивки в каталоге SETGUI
используется отдельный [общий публикатор](../../tools/firmware-publish/PORTING.md).

View File

@@ -32,6 +32,16 @@ typedef enum {
FIRMWARE_INFO_OUT_OF_RANGE
} firmware_info_status_t;
/* Compatibility names for climate firmware; same layout and wire contract. */
typedef firmware_info_t FirmwareInfo;
typedef firmware_info_status_t FirmwareInfoStatus;
#define FIRMWARE_INFO_REG_COUNT FIRMWARE_INFO_REGISTER_COUNT
#define FIRMWARE_INFO_BUILD_ID_CHARS FIRMWARE_INFO_BUILD_ID_SIZE
#define FirmwareInfo_Validate firmware_info_validate
#define FirmwareInfo_ToRegisters firmware_info_to_registers
#define FirmwareInfo_ParseBuildStamp firmware_info_parse_build_stamp
#define FirmwareInfo_SetBuildId firmware_info_set_build_id
firmware_info_status_t firmware_info_validate(const firmware_info_t *info);
firmware_info_status_t firmware_info_parse_build_stamp(
const char *date_text, const char *time_text, firmware_info_t *info);

View File

@@ -16,5 +16,11 @@ int main(void)
assert(words[8] == 0x6162U && words[9] == 0x6330U);
assert(firmware_info_to_le_bytes(&info, bytes, sizeof(bytes)) == FIRMWARE_INFO_OK);
assert(bytes[0] == 1U && bytes[1] == 0U && bytes[16] == 0x62U && bytes[17] == 0x61U);
{
FirmwareInfo climate = info;
uint16_t compat[FIRMWARE_INFO_REG_COUNT];
assert(FirmwareInfo_ToRegisters(&climate, compat, FIRMWARE_INFO_REG_COUNT) == FIRMWARE_INFO_OK);
assert(memcmp(words, compat, sizeof(words)) == 0);
}
return 0;
}

View File

@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.13)
project(flash_storage C)
add_library(flash_storage STATIC Src/flash_storage.c)
target_include_directories(flash_storage PUBLIC Inc)
set_target_properties(flash_storage PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED YES)
enable_testing()
add_executable(test_flash_storage Tests/test_flash_storage.c ../ds18b20/instance/Src/ds18b20.c)
target_link_libraries(test_flash_storage PRIVATE flash_storage)
target_include_directories(test_flash_storage PRIVATE ../ds18b20/instance/Inc)
add_test(NAME test_flash_storage COMMAND test_flash_storage)

View File

@@ -0,0 +1,104 @@
#ifndef PORTABLE_FLASH_STORAGE_H
#define PORTABLE_FLASH_STORAGE_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define FLASH_STORAGE_IMAGE_SIZE 512U
#define FLASH_STORAGE_RECORD_SIZE 544U
#define FLASH_STORAGE_V1_RECORD_SIZE 288U
typedef enum {
FLASH_STORAGE_OK = 0,
FLASH_STORAGE_E_ARGUMENT = -1,
FLASH_STORAGE_E_LAYOUT = -2,
FLASH_STORAGE_E_IO = -3,
FLASH_STORAGE_E_NOT_FOUND = -4,
FLASH_STORAGE_E_CORRUPT = -5,
FLASH_STORAGE_E_BUSY = -6,
FLASH_STORAGE_E_BOUNDS = -7
} flash_storage_status_t;
typedef struct {
flash_storage_status_t (*read)(void *context, uint32_t address,
void *data, size_t size);
flash_storage_status_t (*program)(void *context, uint32_t address,
const void *data, size_t size);
flash_storage_status_t (*erase)(void *context, uint32_t address,
size_t size);
uint32_t (*tick_ms)(void *context);
} flash_storage_ops_t;
typedef struct {
uint32_t bank_address[2];
uint32_t bank_size;
uint32_t program_alignment;
uint32_t erase_alignment;
uint32_t minimum_write_ms;
uint8_t reserve_percent;
} flash_storage_layout_t;
typedef struct {
const flash_storage_ops_t *ops;
void *platform_context;
flash_storage_layout_t layout;
uint32_t latest_address;
uint32_t latest_sequence;
uint32_t next_address;
uint32_t last_write_tick;
uint32_t valid_records;
uint32_t programmed_slots;
uint16_t latest_version;
uint8_t initialized;
uint8_t busy;
uint8_t wrote_this_boot;
/* Рабочая запись принадлежит экземпляру: Flash API не расходует малый
* embedded-стек на 544-байтные автоматические структуры. */
uint32_t workspace[FLASH_STORAGE_RECORD_SIZE / sizeof(uint32_t)];
} flash_storage_t;
typedef struct {
flash_storage_t journal;
uint8_t image[FLASH_STORAGE_IMAGE_SIZE];
uint8_t next_image[FLASH_STORAGE_IMAGE_SIZE];
uint8_t initialized;
} eeprom_store_t;
typedef struct {
uint32_t sequence;
uint32_t valid_records;
uint32_t programmed_slots;
uint32_t allocated_bytes;
uint32_t reserved_bytes;
} flash_storage_info_t;
flash_storage_status_t flash_storage_init(flash_storage_t *instance,
const flash_storage_ops_t *ops, void *platform_context,
const flash_storage_layout_t *layout);
flash_storage_status_t flash_storage_read_latest(flash_storage_t *instance,
void *data, size_t capacity, size_t *length, uint32_t *sequence);
flash_storage_status_t flash_storage_write(flash_storage_t *instance,
const void *data, size_t length);
flash_storage_status_t flash_storage_format(flash_storage_t *instance);
flash_storage_status_t flash_storage_get_info(flash_storage_t *instance,
flash_storage_info_t *info);
flash_storage_status_t eeprom_store_init(eeprom_store_t *instance,
const flash_storage_ops_t *ops, void *platform_context,
const flash_storage_layout_t *layout);
flash_storage_status_t eeprom_store_read(eeprom_store_t *instance,
size_t offset, void *data, size_t size);
flash_storage_status_t eeprom_store_write(eeprom_store_t *instance,
size_t offset, const void *data, size_t size);
flash_storage_status_t eeprom_store_format(eeprom_store_t *instance);
/* API не ISR-safe: erase/program могут надолго блокировать CPU. Один экземпляр
* обслуживается последовательно; разные экземпляры допустимы на независимых
* областях, а общий HAL Flash должен сериализовать платформенный адаптер. */
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,8 @@
#ifndef PORTABLE_FLASH_STORAGE_CONFIG_H
#define PORTABLE_FLASH_STORAGE_CONFIG_H
#define FLASH_STORAGE_MAGIC 0x474E4952UL
#define FLASH_STORAGE_COMMIT 0x54494D43UL
#define FLASH_STORAGE_VERSION 2U
#define FLASH_STORAGE_VERSION_V1 1U
#define FLASH_STORAGE_MIN_RESERVE_PERCENT 10U
#endif

43
c/flash-storage/README.md Normal file
View File

@@ -0,0 +1,43 @@
# FlashStorage
Переносимый журнал двух банков Flash с подтверждёнными записями и восстановлением
после прерванной записи. Предоставляет также EEPROM-подобный образ 512 байт.
Не содержит HAL, файловой системы, адресов конкретной платы или malloc.
Приложение → `flash_storage` / `eeprom_store` → callbacks → Flash-порт платы.
| Файл | Назначение |
|---|---|
| `Inc/flash_storage.h` | Контекст, раскладка банков, read/program/erase/tick callbacks |
| `Inc/flash_storage_config.h` | Версия формата, magic, commit marker |
| `Src/flash_storage.c` | Восстановление, запись, ротация банков; только стандартный C |
| `Tests/test_flash_storage.c` | Прерывание записи, восстановление, несколько экземпляров |
Порт получает контекст, адрес и длину. `read`/`program`/`erase` возвращают
`flash_storage_status_t`; `tick_ms` возвращает uint32_t. Буферы принадлежат
экземпляру. Стирание и программирование могут блокировать процессор: API не
предназначен для ISR. Раскладку и резерв места задаёт `flash_storage_layout_t`.
```c
/* board_ops, board_context и layout определяет порт конкретной платы. */
flash_storage_t journal;
uint8_t payload[512] = {0}, restored[512];
size_t length = 0;
flash_storage_status_t result;
result = flash_storage_init(&journal, &board_ops, board_context, &layout);
if (result == FLASH_STORAGE_OK) {
result = flash_storage_write(&journal, payload, sizeof(payload));
}
if (result == FLASH_STORAGE_OK) {
result = flash_storage_read_latest(&journal, restored,
sizeof(restored), &length, 0);
}
```
Существующий потребитель — climate. Порты STM32F4 и SPI NOR остаются там.
Проверка: `python tools/test_shared_libraries.py` из корня templates.
## Shared source
Canonical source: `templates/c/flash-storage`. Used by `home/climate`; its old paths are compatibility includes. Board-specific ports remain in the application. Change this library, not the forwarding files.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

9
c/modbus/CMakeLists.txt Normal file
View File

@@ -0,0 +1,9 @@
cmake_minimum_required(VERSION 3.13)
project(set_modbus C)
add_library(set_modbus STATIC src/modbus_data.c)
target_include_directories(set_modbus PUBLIC include)
set_target_properties(set_modbus PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED YES)
enable_testing()
add_executable(test_modbus_data tests/test_modbus_data.c)
target_link_libraries(test_modbus_data PRIVATE set_modbus)
add_test(NAME modbus_data COMMAND test_modbus_data)

41
c/modbus/README.md Normal file
View File

@@ -0,0 +1,41 @@
# Общее ядро команд Modbus
Обработка coils и регистров функций 01/03/04/05/06/0F/10 без HAL, UART,
таймеров, глобальной карты регистров и выделения памяти.
Приложение → декодированный запрос → проверка и отображение адреса портом →
`mb_data_transfer` → банк регистров/битов приложения.
| Файл | Назначение и зависимости |
|---|---|
| `include/modbus_data.h` | Контракт, только stdint/stddef |
| `src/modbus_data.c` | Общие ограничения количества, обработка coils и регистров |
| `ports/stm32-legacy/modbus.c` | Совместимый адаптер старого John; зависит от проектного `rs_message.h` |
| `tests/test_modbus_data.c` | Границы и проход через все смещения coils 0..15 |
Порт сначала вызывает `mb_data_validate`, затем отображает **весь** диапазон
адресов в память, и только после успешного отображения вызывает transfer.
Коды возврата соответствуют исключениям Modbus: 0 — успех, 1 — функция,
3 — значение. Ёмкость данных передаётся в 16-битных словах. Адресная карта,
права записи, RTU framing, CRC и таймеры остаются обязанностью транспорта.
```c
#include "modbus_data.h"
uint16_t registers[2] = {123, 456};
uint16_t response[2];
uint16_t response_bytes = 0;
uint8_t status = mb_data_validate(3, 2, 0, 2);
/* Пример банка уже отображён; реальный порт проверяет адрес до вызова. */
if (status == 0) {
status = mb_data_transfer(3, 2, 0, registers, 0,
response, 2, &response_bytes);
}
/* status == 0, response_bytes == 4; сериализацию выполняет транспорт. */
```
Используется в `home/climate/core/STM32_Modbus`, `john103C6T6` и
`ds18b20-c8t6test`. Два последних подключают один совместимый адаптер;
климат сохраняет свой master/slave транспорт и диагностические функции.
Это не замена `pcan_modbus_server`, у которого другой транспортный контракт.
Проверка: `python tools/test_shared_libraries.py` из корня templates.

View File

@@ -0,0 +1,15 @@
#ifndef SET_MODBUS_DATA_H
#define SET_MODBUS_DATA_H
#include <stddef.h>
#include <stdint.h>
/* Decoded Modbus values, independent of UART/HAL and register addresses.
* Coils in bank use bit 0 first in 16-bit words. Message words contain two
* wire bytes, first byte in bits 15..8 (same convention as register messages).
* The port validates/maps the complete address range before calling transfer.
* Return standard Modbus exceptions: 0 success, 1 function, 3 value. */
uint8_t mb_data_validate(uint8_t function, uint16_t quantity, uint16_t byte_count,
size_t data_capacity);
uint8_t mb_data_transfer(uint8_t function, uint16_t quantity, uint16_t byte_count,
uint16_t *bank, uint16_t bit_offset, uint16_t *data,
size_t data_capacity, uint16_t *response_bytes);
#endif

View File

@@ -0,0 +1,836 @@
/**
**************************************************************************
* @file modbus.c
* @brief Модуль для реализации MODBUS.
**************************************************************************
* @details Файл содержит реализацию функций работы с Modbus, включая:
* - доступ к coils и registers;
* - обработку команд протокола;
* - взаимодействие с RS (UART);
* - инициализацию.
*
* @section Функции и макросы
*
* ### Доступ к coils:
* - MB_Set_Coil_Local() — Установить coil по локальному адресу.
* - MB_Reset_Coil_Local() — Сбросить coil по локальному адресу.
* - MB_Toogle_Coil_Local() — Инвертировать coil по локальному адресу.
* - MB_Read_Coil_Local() — Прочитать coil по локальному адресу.
* - MB_Write_Coil_Global() — Установить/сбросить coil по глобальному адресу.
* - MB_Read_Coil_Global() — Прочитать coil по глобальному адресу.
*
* ### Обработка команд Modbus:
* - MB_DefineRegistersAddress() — Определить начальный адрес регистра.
* - MB_DefineCoilsAddress() — Определить начальный адрес coils.
* - MB_Check_Address_For_Arr() — Проверить, принадлежит ли адрес массиву.
* - Основные команды Modbus:
* - MB_Read_Coils()
* - MB_Read_Hold_Regs()
* - MB_Write_Single_Coil()
* - MB_Write_Miltuple_Coils()
* - MB_Write_Miltuple_Regs()
*
* ### Функции для работы с RS (UART):
* - RS_Parse_Message() / RS_Collect_Message() — Парсинг и сборка сообщения.
* - RS_Response() — Отправка ответа.
* - RS_Define_Size_of_RX_Message() — Определение размера принимаемого сообщения.
* - RS_Init() — Инициализация UART.
*
* ### Инициализация:
* - MODBUS_FirstInit() — Инициализация модуля Modbus.
*
* @section Структура данных Modbus
*
* #### Holding/Input Registers:
* - Регистры — 16-битные слова. Доступ к регистрам осуществляется через указатель.
* Таким образом, сами регистры могут представлять собой как массив так и структуру.
*
* #### Coils:
* - Coils — это биты, упакованные в 16-битные слова. Доступ к коилам осуществляется через указатель.
* Таким образом, сами коилы могут представлять собой как массив так и структуру.
*
* @section Инструкция по подключению
* Для корректной работы надо подключить обработчики RS_UART_Handler(), RS_TIM_Handler(),
* в соответствубщие низкоуровневые прерывания UART_IRQHandler, TIM_IRQHandler. После HAL'овского обработчика
*
* Также необходимо в modbus_config.h настроить дефайны для нужной работы UART
* После для запуска Modbus:
* @verbatim
//----------------Прием модбас----------------//
#include "rs_message.h"
#include "../../src/modbus_data.c"
MODBUS_FirstInit();
RS_Receive_IT(&hmodbus1, &MODBUS_MSG);
* @endverbatim
*
******************************************************************************/
#include "rs_message.h"
#include "../../src/modbus_data.c"
uint32_t dbg_temp, dbg_temp2, dbg_temp3; // for debug
/* MODBUS HANDLES */
extern UART_HandleTypeDef rs_huart;
extern TIM_HandleTypeDef rs_htim;
RS_HandleTypeDef hmodbus1;
/* DEFINE REGISTERS/COILS */
MB_DeviceIdentificationTypeDef MB_INFO;
MB_DataStructureTypeDef MB_DATA;
RS_MsgTypeDef MODBUS_MSG;
//-------------------------------------------------------------------
//-----------------------------FOR USER------------------------------
/**
* @brief First set up of MODBUS.
* @details Первый инит модбас. Заполняет структуры и инициализирует таймер и юарт для общения по модбас.
* @note This called from main
*/
void MODBUS_FirstInit(void)
{
MB_DevoceInentificationInit();
//-----------SETUP MODBUS-------------
// set up modbus: MB_RX_Size_NotConst and Timeout enable
hmodbus1.ID = MODBUS_DEVICE_ID;
hmodbus1.sRS_Timeout = MODBUS_TIMEOUT;
hmodbus1.sRS_Mode = SLAVE_ALWAYS_WAIT;
hmodbus1.sRS_RX_Size_Mode = RS_RX_Size_NotConst;
// INIT
hmodbus1.RS_STATUS = RS_Init(&hmodbus1, &rs_huart, &rs_htim, 0);
RS_EnableReceive();
}
/**
* @brief Set or Reset Coil at its global address.
* @param Addr - адрес коила.
* @param WriteVal - Что записать в коил: 0 или 1.
* @return ExceptionCode - Код исключения если коила по адресу не существует, и NO_ERRORS если все ок.
*
* @details Позволяет обратиться к любому коилу по его глобальному адрессу.
Вне зависимости от того как коилы размещены в памяти.
*/
MB_ExceptionTypeDef MB_Write_Coil_Global(uint16_t Addr, MB_CoilsOpTypeDef WriteVal)
{
//---------CHECK FOR ERRORS----------
MB_ExceptionTypeDef Exception = NO_ERRORS;
uint16_t *coils;
uint16_t start_shift = 0; // shift in coils register
//------------WRITE COIL-------------
Exception = MB_DefineCoilsAddress(&coils, Addr, 1, &start_shift, 1);
if(Exception == NO_ERRORS)
{
switch(WriteVal)
{
case SET_COIL:
*coils |= (1<<start_shift);
break;
case RESET_COIL:
*coils &= ~(1<<start_shift);
break;
case TOOGLE_COIL:
*coils ^= (1<<start_shift);
break;
}
}
return Exception;
}
/**
* @brief Read Coil at its global address.
* @param Addr - адрес коила.
* @param Exception - Указатель на переменную для кода исключения, в случа неудачи при чтении.
* @return uint16_t - Возвращает весь регистр с маской на запрошенном коиле.
*
* @details Позволяет обратиться к любому коилу по его глобальному адрессу.
Вне зависимости от того как коилы размещены в памяти.
*/
uint16_t MB_Read_Coil_Global(uint16_t Addr, MB_ExceptionTypeDef *Exception)
{
//---------CHECK FOR ERRORS----------
MB_ExceptionTypeDef Exception_tmp;
if(Exception == NULL) // if exception is not given to func fill it
Exception = &Exception_tmp;
uint16_t *coils;
uint16_t start_shift = 0; // shift in coils register
//------------READ COIL--------------
*Exception = MB_DefineCoilsAddress(&coils, Addr, 1, &start_shift, 0);
if(*Exception == NO_ERRORS)
{
return ((*coils)&(1<<start_shift));
}
else
{
return 0;
}
}
//-------------------------------------------------------------------
//----------------FUNCTIONS FOR PROCESSING MESSAGE-------------------
/**
* @brief Check is address valid for certain array.
* @param Addr - начальный адресс.
* @param Qnt - количество запрашиваемых элементов.
* @param R_ARR_ADDR - начальный адресс массива R_ARR.
* @param R_ARR_NUMB - количество элементов в массиве R_ARR.
* @return ExceptionCode - ILLEGAL DATA ADRESS если адресс недействителен, и NO_ERRORS если все ок.
*
* @details Позволяет определить, принадлежит ли адресс Addr массиву R_ARR:
* Если адресс Addr находится в диапазоне адрессов массива R_ARR, то возвращаем NO_ERROR.
* Если адресс Addr находится за пределами адрессов массива R_ARR - ILLEGAL_DATA_ADDRESSю.
*/
MB_ExceptionTypeDef MB_Check_Address_For_Arr(uint16_t Addr, uint16_t Qnt, uint16_t R_ARR_ADDR, uint16_t R_ARR_NUMB)
{
// if address from this array
if(Addr >= R_ARR_ADDR)
{
// if quantity too big return error
if ((Addr - R_ARR_ADDR) + Qnt > R_ARR_NUMB)
{
return ILLEGAL_DATA_ADDRESS; // return exception code
}
// if all ok - return no errors
return NO_ERRORS;
}
// if address isnt from this array return error
else
return ILLEGAL_DATA_ADDRESS; // return exception code
}
/**
* @brief Define Address Origin for Input/Holding Registers
* @param pRegs - указатель на указатель регистров.
* @param Addr - адрес начального регистра.
* @param Qnt - количество запрашиваемых регистров.
* @param WriteFlag - флаг регистр нужны для чтения или записи.
* @return ExceptionCode - Код исключения если есть, и NO_ERRORS если нет.
*
* @details Определение адреса начального регистра.
* @note WriteFlag пока не используется.
*/
MB_ExceptionTypeDef MB_DefineRegistersAddress(uint16_t **pRegs, uint16_t Addr, uint16_t Qnt, uint8_t RegisterType)
{
/* check quantity error */
if (Qnt > 125)
{
return ILLEGAL_DATA_VALUE; // return exception code
}
if(RegisterType == RegisterType_Holding)
{
// Default holding registers
if(MB_Check_Address_For_Arr(Addr, Qnt, R_HOLDING_ADDR, R_HOLDING_QNT) == NO_ERRORS)
{
*pRegs = MB_Set_Register_Ptr(&MB_DATA.HoldRegs, Addr); // указатель на выбранный по Addr регистр
}
// if address doesnt match any array - return illegal data address response
else
{
return ILLEGAL_DATA_ADDRESS;
}
}
else if(RegisterType == RegisterType_Input)
{
// Default input registers
if(MB_Check_Address_For_Arr(Addr, Qnt, R_INPUT_ADDR, R_INPUT_QNT) == NO_ERRORS)
{
*pRegs = MB_Set_Register_Ptr(&MB_DATA.InRegs, Addr); // указатель на выбранный по Addr регистр
}
// if address doesnt match any array - return illegal data address response
else
{
return ILLEGAL_DATA_ADDRESS;
}
}
else
{
return ILLEGAL_FUNCTION;
}
// if found requeried array return no err
return NO_ERRORS; // return no errors
}
/**
* @brief Define Address Origin for coils
* @param pCoils - указатель на указатель коилов.
* @param Addr - адресс начального коила.
* @param Qnt - количество запрашиваемых коилов.
* @param start_shift - указатель на переменную содержащую сдвиг внутри регистра для начального коила.
* @param WriteFlag - флаг коилы нужны для чтения или записи.
* @return ExceptionCode - Код исключения если есть, и NO_ERRORS если нет.
*
* @details Определение адреса начального регистра запрашиваемых коилов.
* @note WriteFlag используется для определния регистров GPIO: ODR или IDR.
*/
MB_ExceptionTypeDef MB_DefineCoilsAddress(uint16_t **pCoils, uint16_t Addr, uint16_t Qnt, uint16_t *start_shift, uint8_t WriteFlag)
{
/* check quantity error */
if (Qnt > 2000)
{
return ILLEGAL_DATA_VALUE; // return exception code
}
// Default coils
if(MB_Check_Address_For_Arr(Addr, Qnt, C_CONTROL_ADDR, C_CONTROL_QNT) == NO_ERRORS)
{
*pCoils = MB_Set_Coil_Reg_Ptr(&MB_DATA.Coils, Addr); // указатель на выбранный по Addr массив коилов
}
// if address doesnt match any array - return illegal data address response
else
{
return ILLEGAL_DATA_ADDRESS;
}
*start_shift = Addr % 16; // set shift to requested coil
// if found requeried array return no err
return NO_ERRORS; // return no errors
}
/**
* @brief Proccess command Read Coils (01 - 0x01).
* @param modbus_msg - указатель на структуру собщения modbus.
* @return fMessageHandled - статус о результате обработки комманды.
* @details Обработка команды Read Coils.
*/
uint8_t MB_Read_Coils(RS_MsgTypeDef *modbus_msg)
{
uint16_t *bank, shift = 0, response_bytes = modbus_msg->ByteCnt;
uint8_t status = mb_data_validate(1, modbus_msg->Qnt, modbus_msg->ByteCnt,
sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]));
if (!status) status = (uint8_t)MB_DefineCoilsAddress(&bank, modbus_msg->Addr, modbus_msg->Qnt, &shift, 0);
if (!status) status = mb_data_transfer(1, modbus_msg->Qnt, modbus_msg->ByteCnt,
bank, shift, modbus_msg->DATA, sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]), &response_bytes);
modbus_msg->Except_Code = (MB_ExceptionTypeDef)status;
if (status) return 0;
modbus_msg->ByteCnt = response_bytes;
return 1;
}
/**
* @brief Proccess command Read Holding Registers (03 - 0x03).
* @param modbus_msg - указатель на структуру собщения modbus.
* @return fMessageHandled - статус о результате обработки комманды.
* @details Обработка команды Read Holding Registers.
*/
uint8_t MB_Read_Hold_Regs(RS_MsgTypeDef *modbus_msg)
{
uint16_t *bank, shift = 0, response_bytes = modbus_msg->ByteCnt;
uint8_t status = mb_data_validate(3, modbus_msg->Qnt, modbus_msg->ByteCnt,
sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]));
if (!status) status = (uint8_t)MB_DefineRegistersAddress(&bank, modbus_msg->Addr, modbus_msg->Qnt, RegisterType_Holding);
if (!status) status = mb_data_transfer(3, modbus_msg->Qnt, modbus_msg->ByteCnt,
bank, shift, modbus_msg->DATA, sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]), &response_bytes);
modbus_msg->Except_Code = (MB_ExceptionTypeDef)status;
if (status) return 0;
modbus_msg->ByteCnt = response_bytes;
return 1;
}
/**
* @brief Proccess command Read Input Registers (04 - 0x04).
* @param modbus_msg - указатель на структуру собщения modbus.
* @return fMessageHandled - статус о результате обработки комманды.
* @details Обработка команды Read Input Registers.
*/
uint8_t MB_Read_Input_Regs(RS_MsgTypeDef *modbus_msg)
{
uint16_t *bank, shift = 0, response_bytes = modbus_msg->ByteCnt;
uint8_t status = mb_data_validate(4, modbus_msg->Qnt, modbus_msg->ByteCnt,
sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]));
if (!status) status = (uint8_t)MB_DefineRegistersAddress(&bank, modbus_msg->Addr, modbus_msg->Qnt, RegisterType_Input);
if (!status) status = mb_data_transfer(4, modbus_msg->Qnt, modbus_msg->ByteCnt,
bank, shift, modbus_msg->DATA, sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]), &response_bytes);
modbus_msg->Except_Code = (MB_ExceptionTypeDef)status;
if (status) return 0;
modbus_msg->ByteCnt = response_bytes;
return 1;
}
/**
* @brief Proccess command Write Single Coils (05 - 0x05).
* @param modbus_msg - указатель на структуру собщения modbus.
* @return fMessageHandled - статус о результате обработки комманды.
* @details Обработка команды Write Single Coils.
*/
uint8_t MB_Write_Single_Coil(RS_MsgTypeDef *modbus_msg)
{
uint16_t *bank, shift = 0, response_bytes = modbus_msg->ByteCnt;
uint8_t status = mb_data_validate(5, modbus_msg->Qnt, modbus_msg->ByteCnt,
sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]));
if (!status) status = (uint8_t)MB_DefineCoilsAddress(&bank, modbus_msg->Addr, 1, &shift, 1);
if (!status) status = mb_data_transfer(5, modbus_msg->Qnt, modbus_msg->ByteCnt,
bank, shift, modbus_msg->DATA, sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]), &response_bytes);
modbus_msg->Except_Code = (MB_ExceptionTypeDef)status;
if (status) return 0;
modbus_msg->ByteCnt = response_bytes;
return 1;
}
/**
* @brief Proccess command Write Single Register (06 - 0x06).
* @param modbus_msg - указатель на структуру собщения modbus.
* @return fMessageHandled - статус о результате обработки комманды.
* @details Обработка команды Write Single Register.
*/
uint8_t MB_Write_Single_Reg(RS_MsgTypeDef *modbus_msg)
{
uint16_t *bank, shift = 0, response_bytes = modbus_msg->ByteCnt;
uint8_t status = mb_data_validate(6, modbus_msg->Qnt, modbus_msg->ByteCnt,
sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]));
if (!status) status = (uint8_t)MB_DefineRegistersAddress(&bank, modbus_msg->Addr, 1, RegisterType_Holding);
if (!status) status = mb_data_transfer(6, modbus_msg->Qnt, modbus_msg->ByteCnt,
bank, shift, modbus_msg->DATA, sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]), &response_bytes);
modbus_msg->Except_Code = (MB_ExceptionTypeDef)status;
if (status) return 0;
modbus_msg->ByteCnt = response_bytes;
return 1;
}
/**
* @brief Proccess command Write Multiple Coils (15 - 0x0F).
* @param modbus_msg - указатель на структуру собщения modbus.
* @return fMessageHandled - статус о результате обработки комманды.
* @details Обработка команды Write Multiple Coils.
*/
uint8_t MB_Write_Miltuple_Coils(RS_MsgTypeDef *modbus_msg)
{
uint16_t *bank, shift = 0, response_bytes = modbus_msg->ByteCnt;
uint8_t status = mb_data_validate(15, modbus_msg->Qnt, modbus_msg->ByteCnt,
sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]));
if (!status) status = (uint8_t)MB_DefineCoilsAddress(&bank, modbus_msg->Addr, modbus_msg->Qnt, &shift, 1);
if (!status) status = mb_data_transfer(15, modbus_msg->Qnt, modbus_msg->ByteCnt,
bank, shift, modbus_msg->DATA, sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]), &response_bytes);
modbus_msg->Except_Code = (MB_ExceptionTypeDef)status;
if (status) return 0;
modbus_msg->ByteCnt = response_bytes;
return 1;
}
/**
* @brief Proccess command Write Multiple Registers (16 - 0x10).
* @param modbus_msg - указатель на структуру собщения modbus.
* @return fMessageHandled - статус о результате обработки комманды.
* @details Обработка команды Write Multiple Registers.
*/
uint8_t MB_Write_Miltuple_Regs(RS_MsgTypeDef *modbus_msg)
{
uint16_t *bank, shift = 0, response_bytes = modbus_msg->ByteCnt;
uint8_t status = mb_data_validate(16, modbus_msg->Qnt, modbus_msg->ByteCnt,
sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]));
if (!status) status = (uint8_t)MB_DefineRegistersAddress(&bank, modbus_msg->Addr, modbus_msg->Qnt, RegisterType_Holding);
if (!status) status = mb_data_transfer(16, modbus_msg->Qnt, modbus_msg->ByteCnt,
bank, shift, modbus_msg->DATA, sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]), &response_bytes);
modbus_msg->Except_Code = (MB_ExceptionTypeDef)status;
if (status) return 0;
modbus_msg->ByteCnt = response_bytes;
return 1;
}
void MB_WriteObjectToMessage(char *mbdata, unsigned *ind, MB_DeviceObjectTypeDef *obj)
{
mbdata[(*ind)++] = obj->length;
for (int i = 0; i < obj->length; i++)
{
mbdata[(*ind)++] = obj->name[i];
}
}
/**
* @brief Proccess command Read Device Identification (43/14 - 0x2B/0E).
* @param modbus_msg - указатель на структуру собщения modbus.
* @return fMessageHandled - статус о результате обработки комманды.
* @details Обработка команды Write Single Register.
*/
uint8_t MB_Read_Device_Identification(RS_MsgTypeDef *modbus_msg)
{
char *mbdata = (char *)modbus_msg->DATA;
unsigned ind = 0;
switch(modbus_msg->DevId.ReadDevId)
{
case MB_BASIC_IDENTIFICATION:
mbdata[ind++] = 0x00;
MB_WriteObjectToMessage(mbdata, &ind, &MB_INFO.VendorName);
mbdata[ind++] = 0x01;
MB_WriteObjectToMessage(mbdata, &ind, &MB_INFO.ProductCode);
mbdata[ind++] = 0x02;
MB_WriteObjectToMessage(mbdata, &ind, &MB_INFO.Revision);
modbus_msg->DevId.NumbOfObj = 3;
break;
case MB_REGULAR_IDENTIFICATION:
mbdata[ind++] = 0x03;
MB_WriteObjectToMessage(mbdata, &ind, &MB_INFO.VendorUrl);
mbdata[ind++] = 0x04;
MB_WriteObjectToMessage(mbdata, &ind, &MB_INFO.ProductName);
mbdata[ind++] = 0x05;
MB_WriteObjectToMessage(mbdata, &ind, &MB_INFO.ModelName);
mbdata[ind++] = 0x06;
MB_WriteObjectToMessage(mbdata, &ind, &MB_INFO.UserApplicationName);
modbus_msg->DevId.NumbOfObj = 4;
break;
default:
return 0;
}
modbus_msg->ByteCnt = ind;
return 1;
}
/**
* @brief Respond accord to received message.
* @param hRS - указатель на хендлер RS.
* @param RS_msg - указатель на структуру сообщения.
* @return RS_RES - статус о результате ответа на комманду.
* @details Обработка принятой комманды и ответ на неё.
*/
RS_StatusTypeDef RS_Response(RS_HandleTypeDef *hmodbus, RS_MsgTypeDef *modbus_msg)
{
RS_StatusTypeDef MB_RES = 0;
hmodbus->f.MessageHandled = 0;
hmodbus->f.EchoResponse = 0;
RS_Reset_TX_Flags(hmodbus); // reset flag for correct transmit
if(modbus_msg->Func_Code < ERR_VALUES_START)// if no errors after parsing
{
switch (modbus_msg->Func_Code)
{
// Read Coils
case MB_R_COILS:
hmodbus->f.MessageHandled = MB_Read_Coils(hmodbus->pMessagePtr);
break;
// Read Hodling Registers
case MB_R_HOLD_REGS:
hmodbus->f.MessageHandled = MB_Read_Hold_Regs(hmodbus->pMessagePtr);
break;
case MB_R_IN_REGS:
hmodbus->f.MessageHandled = MB_Read_Input_Regs(hmodbus->pMessagePtr);
break;
// Write Single Coils
case MB_W_COIL:
hmodbus->f.MessageHandled = MB_Write_Single_Coil(hmodbus->pMessagePtr);
if(hmodbus->f.MessageHandled)
{
hmodbus->f.EchoResponse = 1;
hmodbus->RS_Message_Size -= 2; // echo response if write ok (minus 2 cause of two CRC bytes)
}
break;
case MB_W_HOLD_REG:
hmodbus->f.MessageHandled = MB_Write_Single_Reg(hmodbus->pMessagePtr);
if(hmodbus->f.MessageHandled)
{
hmodbus->f.EchoResponse = 1;
hmodbus->RS_Message_Size -= 2; // echo response if write ok (minus 2 cause of two CRC bytes)
}
break;
// Write Multiple Coils
case MB_W_COILS:
hmodbus->f.MessageHandled = MB_Write_Miltuple_Coils(hmodbus->pMessagePtr);
if(hmodbus->f.MessageHandled)
{
hmodbus->f.EchoResponse = 1;
hmodbus->RS_Message_Size = 6; // echo response if write ok (withous data bytes)
}
break;
// Write Multiple Registers
case MB_W_HOLD_REGS:
hmodbus->f.MessageHandled = MB_Write_Miltuple_Regs(hmodbus->pMessagePtr);
if(hmodbus->f.MessageHandled)
{
hmodbus->f.EchoResponse = 1;
hmodbus->RS_Message_Size = 6; // echo response if write ok (withous data bytes)
}
break;
case MB_R_DEVICE_INFO:
hmodbus->f.MessageHandled = MB_Read_Device_Identification(hmodbus->pMessagePtr);
break;
/* unknown func code */
default: modbus_msg->Except_Code = 0x01; /* set exception code: illegal function */
}
if(hmodbus->f.MessageHandled == 0)
{
modbus_msg->Func_Code += ERR_VALUES_START;
}
else
{
}
}
// if we need response - check that transmit isnt busy
if( RS_Is_TX_Busy(hmodbus) )
RS_Abort(hmodbus, ABORT_TX); // if tx busy - set it free
// Transmit right there, or sets (fDeferredResponse) to transmit response in main code
MB_RES = RS_Handle_Transmit_Start(hmodbus, modbus_msg);
hmodbus->RS_STATUS = MB_RES;
return MB_RES;
}
/**
* @brief Collect message in buffer to transmit it.
* @param hRS - указатель на хендлер RS.
* @param RS_msg - указатель на структуру сообщения.
* @param msg_uart_buff - указатель на буффер UART.
* @return RS_RES - статус о результате заполнения буфера.
* @details Заполнение буффера UART из структуры сообщения.
*/
RS_StatusTypeDef RS_Collect_Message(RS_HandleTypeDef *hmodbus, RS_MsgTypeDef *modbus_msg, uint8_t *modbus_uart_buff)
{
int ind = 0; // ind for modbus-uart buffer
if(hmodbus->f.EchoResponse && hmodbus->f.MessageHandled) // if echo response need
ind = hmodbus->RS_Message_Size;
else
{
//------INFO ABOUT DATA/MESSAGE------
//-----------[first bytes]-----------
// set ID of message/user
modbus_uart_buff[ind++] = modbus_msg->MbAddr;
// set dat or err response
modbus_uart_buff[ind++] = modbus_msg->Func_Code;
if (modbus_msg->Func_Code < ERR_VALUES_START) // if no error occur
{
// fill modbus header
if(modbus_msg->Func_Code == MB_R_DEVICE_INFO) // devide identification header
{
modbus_uart_buff[ind++] = modbus_msg->DevId.MEI_Type;
modbus_uart_buff[ind++] = modbus_msg->DevId.ReadDevId;
modbus_uart_buff[ind++] = modbus_msg->DevId.Conformity;
modbus_uart_buff[ind++] = modbus_msg->DevId.MoreFollows;
modbus_uart_buff[ind++] = modbus_msg->DevId.NextObjId;
modbus_uart_buff[ind++] = modbus_msg->DevId.NumbOfObj;
if (modbus_msg->ByteCnt > DATA_SIZE*2) // if ByteCnt less than DATA_SIZE
{
return RS_COLLECT_MSG_ERR;
}
//---------------DATA----------------
//-----------[data bytes]------------
uint8_t *tmp_data_addr = (uint8_t *)modbus_msg->DATA;
for(int i = 0; i < modbus_msg->ByteCnt; i++) // filling buffer with data
{ // set data
modbus_uart_buff[ind++] = *tmp_data_addr;
tmp_data_addr++;
}
}
else // modbus data header
{
// set size of received data
if (modbus_msg->ByteCnt <= DATA_SIZE*2) // if ByteCnt less than DATA_SIZE
modbus_uart_buff[ind++] = modbus_msg->ByteCnt;
else // otherwise return data_size err
{
return RS_COLLECT_MSG_ERR;
}
//---------------DATA----------------
//-----------[data bytes]------------
uint16_t *tmp_data_addr = (uint16_t *)modbus_msg->DATA;
for(int i = 0; i < modbus_msg->ByteCnt; i++) // filling buffer with data
{ // set data
if (i%2 == 0) // HI byte
modbus_uart_buff[ind++] = (*tmp_data_addr)>>8;
else // LO byte
{
modbus_uart_buff[ind++] = *tmp_data_addr;
tmp_data_addr++;
}
}
}
}
else // if some error occur
{ // send expection code
modbus_uart_buff[ind++] = modbus_msg->Except_Code;
}
}
//---------------CRC----------------
//---------[last 16 bytes]----------
// calc crc of received data
uint16_t CRC_VALUE = crc16(modbus_uart_buff, ind);
// write crc to message structure and modbus-uart buffer
modbus_msg->MB_CRC = CRC_VALUE;
modbus_uart_buff[ind++] = CRC_VALUE;
modbus_uart_buff[ind++] = CRC_VALUE >> 8;
hmodbus->RS_Message_Size = ind;
return RS_OK; // returns ok
}
/**
* @brief Parse message from buffer to process it.
* @param hRS - указатель на хендлер RS.
* @param RS_msg - указатель на структуру сообщения.
* @param msg_uart_buff - указатель на буффер UART.
* @return RS_RES - статус о результате заполнения структуры.
* @details Заполнение структуры сообщения из буффера UART.
*/
RS_StatusTypeDef RS_Parse_Message(RS_HandleTypeDef *hmodbus, RS_MsgTypeDef *modbus_msg, uint8_t *modbus_uart_buff)
{
uint32_t check_empty_buff;
int ind = 0; // ind for modbus-uart buffer
//-----INFO ABOUT DATA/MESSAGE-------
//-----------[first bits]------------
// get ID of message/user
modbus_msg->MbAddr = modbus_uart_buff[ind++];
if(modbus_msg->MbAddr != hmodbus->ID)
return RS_SKIP;
// get func code
modbus_msg->Func_Code = modbus_uart_buff[ind++];
if(modbus_msg->Func_Code == MB_R_DEVICE_INFO) // if it device identification request
{
modbus_msg->DevId.MEI_Type = modbus_uart_buff[ind++];
modbus_msg->DevId.ReadDevId = modbus_uart_buff[ind++];
modbus_msg->DevId.NextObjId = modbus_uart_buff[ind++];
modbus_msg->ByteCnt = 0;
}
else // if its classic modbus request
{
// get address from CMD
modbus_msg->Addr = modbus_uart_buff[ind++] << 8;
modbus_msg->Addr |= modbus_uart_buff[ind++];
// get address from CMD
modbus_msg->Qnt = modbus_uart_buff[ind++] << 8;
modbus_msg->Qnt |= modbus_uart_buff[ind++];
}
if(hmodbus->f.RX_Half == 0) // if all message received
{
//---------------DATA----------------
// (optional)
if (modbus_msg->ByteCnt != 0)
{
ind++; // increment ind for data_size byte
//check that data size is correct
if (modbus_msg->ByteCnt > DATA_SIZE*2)
{
modbus_msg->Func_Code += ERR_VALUES_START;
return RS_PARSE_MSG_ERR;
}
uint16_t *tmp_data_addr = (uint16_t *)modbus_msg->DATA;
for(int i = 0; i < modbus_msg->ByteCnt; i++) // /2 because we transmit 8 bits, not 16 bits
{ // set data
if (i%2 == 0)
*tmp_data_addr = ((uint16_t)modbus_uart_buff[ind++] << 8);
else
{
*tmp_data_addr |= modbus_uart_buff[ind++];
tmp_data_addr++;
}
}
}
//---------------CRC----------------
//----------[last 16 bits]----------
// calc crc of received data
uint16_t CRC_VALUE = crc16(modbus_uart_buff, ind);
// get crc of received data
modbus_msg->MB_CRC = modbus_uart_buff[ind++];
modbus_msg->MB_CRC |= modbus_uart_buff[ind++] << 8;
// compare crc
if (modbus_msg->MB_CRC != CRC_VALUE)
{
modbus_msg->Func_Code += ERR_VALUES_START;
}
// hmodbus->MB_RESPONSE = MB_CRC_ERR; // set func code - error about wrong crc
// check is buffer empty
check_empty_buff = 0;
for(int i=0; i<ind;i++)
check_empty_buff += modbus_uart_buff[i];
// if(check_empty_buff == 0)
// hmodbus->MB_RESPONSE = MB_EMPTY_MSG; //
}
return RS_OK;
}
/**
* @brief Define size of RX Message that need to be received.
* @param hRS - указатель на хендлер RS.
* @param rx_data_size - указатель на переменную для записи кол-ва байт для принятия.
* @return RS_RES - статус о корректности рассчета кол-ва байт для принятия.
* @details Определение сколько байтов надо принять по протоколу.
*/
RS_StatusTypeDef RS_Define_Size_of_RX_Message(RS_HandleTypeDef *hmodbus, uint32_t *rx_data_size)
{
RS_StatusTypeDef MB_RES = 0;
MB_RES = RS_Parse_Message(hmodbus, hmodbus->pMessagePtr, hmodbus->pBufferPtr);
if(MB_RES == RS_SKIP) // if message not for us
return MB_RES; // return
if ((hmodbus->pMessagePtr->Func_Code & ~ERR_VALUES_START) < 0x0F)
{
hmodbus->pMessagePtr->ByteCnt = 0;
*rx_data_size = 1;
}
else
{
hmodbus->pMessagePtr->ByteCnt = hmodbus->pBufferPtr[RX_FIRST_PART_SIZE-1]; // get numb of data in command
// +1 because that defines is size, not ind.
*rx_data_size = hmodbus->pMessagePtr->ByteCnt + 2;
}
if(hmodbus->pMessagePtr->Func_Code == MB_R_DEVICE_INFO)
{
*rx_data_size = 0;
}
hmodbus->RS_Message_Size = RX_FIRST_PART_SIZE + *rx_data_size; // size of whole message
return RS_OK;
}
//-----------------------------FOR USER------------------------------
//-------------------------------------------------------------------
void MB_DevoceInentificationInit(void)
{
MB_INFO.VendorName.name = MODBUS_VENDOR_NAME;
MB_INFO.ProductCode.name = MODBUS_PRODUCT_CODE;
MB_INFO.Revision.name = MODBUS_REVISION;
MB_INFO.VendorUrl.name = MODBUS_VENDOR_URL;
MB_INFO.ProductName.name = MODBUS_PRODUCT_NAME;
MB_INFO.ModelName.name = MODBUS_MODEL_NAME;
MB_INFO.UserApplicationName.name = MODBUS_USER_APPLICATION_NAME;
MB_INFO.VendorName.length = sizeof(MODBUS_VENDOR_NAME);
MB_INFO.ProductCode.length = sizeof(MODBUS_PRODUCT_CODE);
MB_INFO.Revision.length = sizeof(MODBUS_REVISION);
MB_INFO.VendorUrl.length = sizeof(MODBUS_VENDOR_URL);
MB_INFO.ProductName.length = sizeof(MODBUS_PRODUCT_NAME);
MB_INFO.ModelName.length = sizeof(MODBUS_MODEL_NAME);
MB_INFO.UserApplicationName.length = sizeof(MODBUS_USER_APPLICATION_NAME);
}

View File

@@ -0,0 +1,57 @@
#include "../include/modbus_data.h"
uint8_t mb_data_validate(uint8_t function, uint16_t quantity, uint16_t bytes, size_t capacity)
{
size_t needed;
switch (function) {
case 1: case 15:
if (!quantity || quantity > (function == 1 ? 2000U : 1968U)) return 3;
needed = ((size_t)quantity+15U)/16U;
if (function == 15 && bytes != (quantity+7U)/8U) return 3;
break;
case 3: case 4: case 16:
if (!quantity || quantity > (function == 16 ? 123U : 125U)) return 3;
needed = quantity;
if (function == 16 && bytes != quantity*2U) return 3;
break;
case 5: return (quantity == 0 || quantity == 0xff00U) ? 0 : 3;
case 6: return 0;
default: return 1;
}
return needed <= capacity ? 0 : 3;
}
uint8_t mb_data_transfer(uint8_t function, uint16_t quantity, uint16_t bytes,
uint16_t *bank, uint16_t offset, uint16_t *data, size_t capacity, uint16_t *response_bytes)
{
uint16_t i;
uint8_t status = mb_data_validate(function, quantity, bytes, capacity);
if (status) return status;
if (bank == NULL || data == NULL || response_bytes == NULL || offset > 15U) return 3;
if (function == 1) {
for (i = 0; i < (quantity+15U)/16U; ++i) data[i] = 0;
}
if (function == 1 || function == 15) {
for (i = 0; i < quantity; ++i) {
uint16_t position = (uint16_t)(i+offset);
uint16_t mask = (uint16_t)(1U << (position%16U));
uint16_t wire_mask = (uint16_t)(1U << ((i%16U)^8U));
if (function == 1) {
if (bank[position/16U] & mask) data[i/16U] |= wire_mask;
} else if (data[i/16U] & wire_mask) bank[position/16U] |= mask;
else bank[position/16U] &= (uint16_t)~mask;
}
if (function == 1) *response_bytes = (uint16_t)((quantity+7U)/8U);
} else if (function == 5) {
if (quantity) *bank |= (uint16_t)(1U << offset);
else *bank &= (uint16_t)~(1U << offset);
} else if (function == 6) *bank = quantity;
else {
for (i = 0; i < quantity; ++i) {
if (function == 16) bank[i] = data[i];
else data[i] = bank[i];
}
if (function != 16) *response_bytes = (uint16_t)(quantity*2U);
}
return 0;
}

View File

@@ -0,0 +1,29 @@
#include "modbus_data.h"
#include <assert.h>
#include <string.h>
int main(void)
{
uint16_t bank[4]={0xa5a5,0x5a5a,0x1234,0xbeef}, original[4], data[130], out;
unsigned offset, quantity, bit;
assert(mb_data_transfer(3,4,0,bank,0,data,130,&out)==0 && out==8 && data[3]==0xbeef);
assert(mb_data_transfer(16,4,7,bank,0,data,130,&out)==3);
assert(mb_data_validate(1,0,0,130)==3);
assert(mb_data_validate(3,126,0,130)==3);
assert(mb_data_validate(16,124,248,130)==3);
assert(mb_data_validate(15,17,3,1)==3);
assert(mb_data_validate(5,1,0,0)==3);
assert(mb_data_validate(2,1,0,130)==1);
for(offset=0;offset<16;++offset) for(quantity=1;quantity<=32;++quantity) {
bank[0]=0xa5a5; bank[1]=0x5a5a; bank[2]=0x1234; bank[3]=0xbeef;
memcpy(original,bank,sizeof(bank));
memset(data,0xcc,sizeof(data));
assert(mb_data_transfer(1,(uint16_t)quantity,0,bank,(uint16_t)offset,data,130,&out)==0);
for(bit=0;bit<quantity;++bit)
assert(((data[bit/16]>>((bit%16)^8))&1)==((bank[(bit+offset)/16]>>((bit+offset)%16))&1));
for(bit=0;bit<quantity;++bit) bank[(bit+offset)/16]^=(uint16_t)(1U<<((bit+offset)%16));
assert(mb_data_transfer(15,(uint16_t)quantity,out,bank,(uint16_t)offset,data,130,&out)==0);
assert(memcmp(bank,original,sizeof(bank))==0);
assert(data[(quantity+15)/16]==0xcccc); /* no write past response */
}
return 0;
}

View File

@@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.13)
project(sd_file_browser C)
add_library(sd_file_browser STATIC Src/sd_file_browser.c)
target_include_directories(sd_file_browser PUBLIC Inc)
set_target_properties(sd_file_browser PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED YES)
enable_testing()
add_executable(test_sd_file_browser Tests/test_sd_file_browser.c)
target_link_libraries(test_sd_file_browser PRIVATE sd_file_browser)
add_test(NAME test_sd_file_browser COMMAND test_sd_file_browser)

Some files were not shown because too many files have changed in this diff Show More