8 Commits

239 changed files with 24922 additions and 3174 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/

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

@@ -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

@@ -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)

View File

@@ -0,0 +1,82 @@
#ifndef SD_FILE_BROWSER_H
#define SD_FILE_BROWSER_H
#include <stdint.h>
/* Ограничения одинаковы для portable core, Modbus mailbox и GUI. */
#define SD_FILE_BROWSER_MAX_PATH 96U
#define SD_FILE_BROWSER_MAX_NAME 48U
#define SD_FILE_BROWSER_MAX_PAGE_ITEMS 3U
#define SD_FILE_BROWSER_MAX_CHUNK_BYTES 64U
typedef enum {
/* Успех означает полностью сформированную страницу, а не только open. */
SD_FILE_BROWSER_OK = 0,
/* BUSY оставляет retry политике приложения и ничего не пишет на носитель. */
SD_FILE_BROWSER_BUSY,
SD_FILE_BROWSER_NOT_READY,
SD_FILE_BROWSER_IO_ERROR,
SD_FILE_BROWSER_INVALID_PATH,
SD_FILE_BROWSER_INVALID_CURSOR,
SD_FILE_BROWSER_NAME_TOO_LONG,
SD_FILE_BROWSER_FILE_CHANGED,
SD_FILE_BROWSER_NOT_LOG_FILE
} SdFileBrowserStatus;
typedef struct {
/* Имя всегда завершается нулём и не содержит родительского пути. */
char name[SD_FILE_BROWSER_MAX_NAME + 1U];
uint32_t size;
uint16_t modified_date;
uint16_t modified_time;
uint8_t is_directory;
} SdFileBrowserEntry;
typedef struct {
/* Ответ имеет фиксированную RAM-ёмкость и никогда не выделяет heap. */
SdFileBrowserEntry items[SD_FILE_BROWSER_MAX_PAGE_ITEMS];
uint16_t next_offset;
uint8_t count;
uint8_t has_more;
} SdFileBrowserPage;
typedef struct {
/* Metadata повторяется в каждом chunk и защищает bridge от склейки разных версий файла. */
uint32_t total_size;
uint32_t offset;
uint16_t modified_date;
uint16_t modified_time;
uint8_t length;
uint8_t end_of_file;
uint8_t data[SD_FILE_BROWSER_MAX_CHUNK_BYTES];
} SdFileBrowserChunk;
typedef SdFileBrowserStatus (*SdFileBrowserListPageFn)(
void *context, const char *path, uint16_t offset, uint8_t limit,
SdFileBrowserPage *page);
typedef struct {
/* Context принадлежит платформе; portable core его не освобождает. */
void *context;
SdFileBrowserListPageFn list_page;
} SdFileBrowser;
/* Принимается только логический путь от корня карты: "/" или "/dir/file".
* Проверка не исправляет вход: неканонические строки отклоняются fail-closed. */
SdFileBrowserStatus SdFileBrowser_ValidatePath(const char *path);
/* Разрешает только legacy TEMPLOG.MD или суточный temperature_*.md внутри
* штатного корня niceOne/mounth; служебные JSON/прошивки сюда не попадают. */
SdFileBrowserStatus SdFileBrowser_ValidateLogFilePath(const char *path);
/* Разрешает только сырой образ .bin/.fw внутри каталога niceOne/firmware.
* Каталог отделён от журналов, чтобы прошивка и логи не смешивались. */
SdFileBrowserStatus SdFileBrowser_ValidateFirmwarePath(const char *path);
/* Вызов делегирует порту ровно одну ограниченную страницу каталога.
* Output предварительно очищается, поэтому ошибка не публикует старые имена. */
SdFileBrowserStatus SdFileBrowser_ListPage(
const SdFileBrowser *browser, const char *path, uint16_t offset,
uint8_t limit, SdFileBrowserPage *page);
#endif /* SD_FILE_BROWSER_H */

View File

@@ -0,0 +1,30 @@
# SD File Browser
Небольшое переносимое read-only ядро для постраничного просмотра каталогов.
Оно не зависит от STM32 HAL, FatFs, Modbus или GUI. Платформа передаёт callback
`list_page`; ядро проверяет путь, предел страницы и очищает ответ перед вызовом.
Логический путь всегда начинается с `/`. Запрещены `..`, `.`, пустые части,
обратная косая черта, двоеточие, управляющие символы, путь от ОС и компоненты
длиннее 48 ASCII-байт. Максимум — три элемента на страницу и 95 байт пути.
FatFs-порт сканирует позднюю страницу пошагово: один вызов service выполняет не
более шестнадцати `readdir`. Поэтому любой `uint16` offset доступен без длинного
блокирующего вызова main loop. Между страницами `DIR` закрыт. Если логгер или
backup получает работу во время сканирования, приложение отменяет viewer,
освобождает `DIR` и возвращает `logger busy`; writer всегда имеет приоритет.
Для переноса реализуйте `SdFileBrowserListPageFn`, обеспечьте read-only открытие
каталога, конечные тайм-ауты носителя и закройте cursor при извлечении карты.
Порт также предоставляет `SdFileBrowserFatFs_ReadLogChunk`: он разрешает только
legacy `/TEMPLOG.MD` и `temperature_*.md` внутри `/niceOne/mounth`, читает не
более 64 байт и закрывает `FIL` до возврата. Первый ответ публикует размер и FAT
date/time; каждый следующий запрос обязан повторить их. Замена, обрезание или
изменение файла возвращает `SD_FILE_BROWSER_FILE_CHANGED`. В API нет функций
записи, удаления, rename, выполнения Markdown или чтения служебных каталогов.
## Shared source
Canonical source: `templates/c/sd-file-browser`. 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,177 @@
#include "sd_file_browser.h"
#include <stddef.h>
#include <string.h>
static uint8_t has_temperature_name(const char *name)
{
const size_t length = strlen(name);
const char prefix[] = "temperature_";
return (uint8_t)((length > (sizeof(prefix) - 1U + 3U)) &&
(strncmp(name, prefix, sizeof(prefix) - 1U) == 0) &&
(strcmp(name + length - 3U, ".md") == 0));
}
/* Сравнение расширения без учёта регистра для короткого суффикса ASCII. */
static uint8_t has_suffix_ci(const char *name, const char *suffix)
{
const size_t name_length = strlen(name);
const size_t suffix_length = strlen(suffix);
size_t index;
if (name_length <= suffix_length) {
return 0U;
}
for (index = 0U; index < suffix_length; ++index) {
char left = name[name_length - suffix_length + index];
char right = suffix[index];
if ((left >= 'A') && (left <= 'Z')) {
left = (char)(left - 'A' + 'a');
}
if ((right >= 'A') && (right <= 'Z')) {
right = (char)(right - 'A' + 'a');
}
if (left != right) {
return 0U;
}
}
return 1U;
}
/* Сырой образ прошивки: .bin или .fw. Intel HEX здесь не принимается — его
* адреса лежат внутри файла и требуют разбора, недоступного на этом этапе. */
static uint8_t has_firmware_name(const char *name)
{
return (uint8_t)((has_suffix_ci(name, ".bin") != 0U) ||
(has_suffix_ci(name, ".fw") != 0U));
}
/* Разрешены печатные ASCII-имена без разделителей ОС и управляющих байтов. */
static uint8_t path_character_is_safe(char character)
{
unsigned char value = (unsigned char)character;
return (uint8_t)((value >= 0x20U) && (value <= 0x7EU) &&
(character != '\\') && (character != ':'));
}
SdFileBrowserStatus SdFileBrowser_ValidatePath(const char *path)
{
size_t length;
size_t component_start;
size_t index;
/* strlen вызывается только после явной проверки внешнего указателя. */
if (path == NULL) {
return SD_FILE_BROWSER_INVALID_PATH;
}
length = strlen(path);
if ((length == 0U) || (length >= SD_FILE_BROWSER_MAX_PATH) ||
(path[0] != '/')) {
/* Абсолютный путь накопителя и усечённый путь fail-closed отклоняются. */
return SD_FILE_BROWSER_INVALID_PATH;
}
if (length == 1U) {
return SD_FILE_BROWSER_OK;
}
/* Компоненты анализируются за один проход, включая завершающий ноль. */
component_start = 1U;
for (index = 1U; index <= length; ++index) {
char character = path[index];
if ((character == '/') || (character == '\0')) {
size_t component_length = index - component_start;
if ((component_length == 0U) ||
(component_length > SD_FILE_BROWSER_MAX_NAME) ||
((component_length == 1U) &&
(path[component_start] == '.')) ||
((component_length == 2U) &&
(path[component_start] == '.') &&
(path[component_start + 1U] == '.'))) {
/* Пустые, точечные и слишком длинные компоненты не нормализуются. */
return SD_FILE_BROWSER_INVALID_PATH;
}
component_start = index + 1U;
} else if (path_character_is_safe(character) == 0U) {
/* Кодировка текущего FatFs-порта ASCII; байты UTF-8 не угадываются. */
return SD_FILE_BROWSER_INVALID_PATH;
}
}
return SD_FILE_BROWSER_OK;
}
SdFileBrowserStatus SdFileBrowser_ValidateLogFilePath(const char *path)
{
const char root[] = "/niceOne/mounth/";
const char *relative;
const char *separator;
const char *name;
SdFileBrowserStatus status = SdFileBrowser_ValidatePath(path);
if (status != SD_FILE_BROWSER_OK) {
return status;
}
/* TEMPLOG.MD сохранён только для старых карт, где журнал лежал в корне. */
if (strcmp(path, "/TEMPLOG.MD") == 0) {
return SD_FILE_BROWSER_OK;
}
if (strncmp(path, root, sizeof(root) - 1U) != 0) {
return SD_FILE_BROWSER_NOT_LOG_FILE;
}
relative = path + sizeof(root) - 1U;
separator = strchr(relative, '/');
/* Штатное дерево содержит ровно месяц и имя; обход дополнительных уровней запрещён. */
if ((separator == NULL) || (separator == relative) ||
(strchr(separator + 1U, '/') != NULL)) {
return SD_FILE_BROWSER_NOT_LOG_FILE;
}
name = strrchr(path, '/');
if ((name == NULL) || (has_temperature_name(name + 1U) == 0U)) {
return SD_FILE_BROWSER_NOT_LOG_FILE;
}
return SD_FILE_BROWSER_OK;
}
SdFileBrowserStatus SdFileBrowser_ValidateFirmwarePath(const char *path)
{
const char root[] = "/niceOne/firmware/";
const char *name;
SdFileBrowserStatus status = SdFileBrowser_ValidatePath(path);
if (status != SD_FILE_BROWSER_OK) {
return status;
}
/* Образы лежат ровно в одном каталоге; вложенные уровни не обходятся. */
if (strncmp(path, root, sizeof(root) - 1U) != 0) {
return SD_FILE_BROWSER_NOT_LOG_FILE;
}
name = path + sizeof(root) - 1U;
if ((name[0] == '\0') || (strchr(name, '/') != NULL)) {
return SD_FILE_BROWSER_NOT_LOG_FILE;
}
if (has_firmware_name(name) == 0U) {
return SD_FILE_BROWSER_NOT_LOG_FILE;
}
return SD_FILE_BROWSER_OK;
}
SdFileBrowserStatus SdFileBrowser_ListPage(
const SdFileBrowser *browser, const char *path, uint16_t offset,
uint8_t limit, SdFileBrowserPage *page)
{
SdFileBrowserStatus status;
/* limit проверяется до очистки, чтобы invalid call не трогал чужую память. */
if ((browser == NULL) || (browser->list_page == NULL) || (page == NULL) ||
(limit == 0U) || (limit > SD_FILE_BROWSER_MAX_PAGE_ITEMS)) {
return SD_FILE_BROWSER_INVALID_CURSOR;
}
status = SdFileBrowser_ValidatePath(path);
if (status != SD_FILE_BROWSER_OK) {
return status;
}
/* Нулевой tail исключает утечку содержимого предыдущего Modbus-ответа. */
memset(page, 0, sizeof(*page));
/* Portable слой не знает FatFs/HAL и передаёт только проверенный запрос. */
return browser->list_page(browser->context, path, offset, limit, page);
}

View File

@@ -0,0 +1,67 @@
#include "sd_file_browser.h"
#include <stdio.h>
#include <string.h>
static unsigned checks;
static unsigned failures;
static unsigned calls;
#define CHECK(condition) do { ++checks; if (!(condition)) ++failures; } while (0)
/* Mock подтверждает, что core передаёт только проверенный bounded запрос. */
static SdFileBrowserStatus mock_list(
void *context, const char *path, uint16_t offset, uint8_t limit,
SdFileBrowserPage *page)
{
(void)context;
++calls;
CHECK(strcmp(path, "/niceOne") == 0);
CHECK(offset == 3U);
CHECK(limit == 2U);
memcpy(page->items[0].name, "July", sizeof("July"));
page->items[0].is_directory = 1U;
page->count = 1U;
page->next_offset = 4U;
return SD_FILE_BROWSER_OK;
}
int main(void)
{
static const char *const invalid[] = {
"", "niceOne", "/../secret", "/./logs", "/a//b", "/a\\b",
"/a:b", "/trailing/"
};
SdFileBrowser browser = { NULL, mock_list };
SdFileBrowserPage page;
unsigned index;
CHECK(SdFileBrowser_ValidatePath("/") == SD_FILE_BROWSER_OK);
CHECK(SdFileBrowser_ValidateLogFilePath("/TEMPLOG.MD") == SD_FILE_BROWSER_OK);
CHECK(SdFileBrowser_ValidateLogFilePath(
"/niceOne/mounth/2026-07_July/temperature_2026-07-18_10-00-00.md") ==
SD_FILE_BROWSER_OK);
CHECK(SdFileBrowser_ValidateLogFilePath("/niceOne/settings/private.json") ==
SD_FILE_BROWSER_NOT_LOG_FILE);
CHECK(SdFileBrowser_ValidateLogFilePath("/niceOne/mounth/fw.bin") ==
SD_FILE_BROWSER_NOT_LOG_FILE);
CHECK(SdFileBrowser_ValidateLogFilePath("/niceOne/mounth/temperature_bad.md") ==
SD_FILE_BROWSER_NOT_LOG_FILE);
CHECK(SdFileBrowser_ValidatePath("/niceOne/2026-07_July") ==
SD_FILE_BROWSER_OK);
for (index = 0U; index < sizeof(invalid) / sizeof(invalid[0]); ++index) {
CHECK(SdFileBrowser_ValidatePath(invalid[index]) ==
SD_FILE_BROWSER_INVALID_PATH);
}
memset(&page, 0xA5, sizeof(page));
CHECK(SdFileBrowser_ListPage(&browser, "/niceOne", 3U, 2U, &page) ==
SD_FILE_BROWSER_OK);
CHECK(calls == 1U);
CHECK(page.count == 1U);
CHECK(strcmp(page.items[0].name, "July") == 0);
CHECK(SdFileBrowser_ListPage(&browser, "/niceOne", 0U, 4U, &page) ==
SD_FILE_BROWSER_INVALID_CURSOR);
CHECK(calls == 1U);
(void)printf("SD browser core: %u checks, %u failures\n", checks, failures);
return failures == 0U ? 0 : 1;
}

View File

@@ -6,16 +6,18 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
# SET protocol v2: управление, телеметрия, CAN segmentation и firmware flow.
set(SETPROTOCOL_V2_SOURCES
src/firmware_image.c
src/altera_logic.c
src/altera_stream.c
src/set_protocol.c
src/set_can.c
src/set_firmware.c
src/set_boot.c
src/set_telemetry.c
src/set_plot.c
src/set_trends.c
src/set_spectrum.c
src/set_signal.c
src/set_wavegen.c
)
# Совместимые ProtoCAN/SETGUI v1 форматы переходного периода.
@@ -37,12 +39,6 @@ set(SETPROTOCOL_LEGACY_SOURCES
)
set(SETPROTOCOL_SOURCES
src/set_regmap.c
src/set_emu_ump.c
src/set_emu_tms.c
src/set_emu_igbt.c
src/set_emu_server.c
src/set_wavegen.c
${SETPROTOCOL_V2_SOURCES}
${SETPROTOCOL_LEGACY_SOURCES}
)
@@ -88,14 +84,12 @@ endif()
option(SETP_BUILD_TESTS "Build host tests" ON)
if(SETP_BUILD_TESTS)
enable_testing()
add_executable(test_emulator_services tests/test_emulator_services.c ports/example-emulator/service.c)
target_link_libraries(test_emulator_services PRIVATE setprotocol_static)
add_test(NAME emulator_services COMMAND test_emulator_services)
if(MSVC)
target_compile_options(test_emulator_services PRIVATE /UNDEBUG)
else()
target_compile_options(test_emulator_services PRIVATE -UNDEBUG)
endif()
add_executable(test_signal_wave tests/test_signal_wave.c)
target_link_libraries(test_signal_wave PRIVATE setprotocol_static)
add_test(NAME signal_wave COMMAND test_signal_wave)
add_executable(test_firmware_image tests/test_firmware_image.c)
target_link_libraries(test_firmware_image PRIVATE setprotocol_static)
add_test(NAME firmware_image COMMAND test_firmware_image)
add_executable(test_altera_logic tests/test_altera_logic.c)
target_link_libraries(test_altera_logic PRIVATE setprotocol_static)
add_test(NAME altera_logic COMMAND test_altera_logic)

View File

@@ -1,7 +1,7 @@
# SETProtocol
Универсальные сервисы эмуляторов: [адресное пространство, API и порт новой платы](docs/EMULATOR_SERVICES.md).
`set_regmap` объединяет УМП, 2812, IGBT и DAC за общим API для RTU, CAN и других оболочек.
Генератор произвольного сигнала, интерполяция, таблицы ЦАП и порты
STM32F407/G474: [SIGNAL_GENERATOR.md](docs/SIGNAL_GENERATOR.md).
Единое переносимое протокольное ядро SET для `SETGUI`, Android GUI, устройств
и сервисных утилит. В одном C99-проекте собраны:
@@ -97,3 +97,9 @@ FFI-клиенты подключают `setprotocol_abi.h`. Имена функ
- Изменение wire contract требует новой версии и тестовых векторов.
Полная интерактивная документация: [`../../doc/setprotocol.html`](../../doc/setprotocol.html).
Обработка кривых: МНК, линейная интерполяция, PCHIP, кубический сплайн,
общие модели и Qt-панель для разных графиков:
[PLOT_PROCESSING.md](docs/PLOT_PROCESSING.md).
Генератор сигнала, USB и порты ЦАП F407/G474:
[SIGNAL_GENERATOR.md](docs/SIGNAL_GENERATOR.md).

View File

@@ -1,228 +0,0 @@
# Универсальное адресное пространство эмуляторов
Общий код находится в `templates/c/set-protocol`. Прошивка платы задаёт память,
GPIO и аппаратные callbacks; протокол и модели не включают STM32/HAL/USB/ОС,
не выделяют heap и не читают системное время. Все вызовы одного экземпляра
сериализуются главным циклом или блокировкой вызывающего.
## Слои и точка расширения
- `set_regmap.h/.c`: таблица непересекающихся регионов и общий вызов
`set_regmap_request(map, function, address, value, out, capacity, &count)`.
Function 3 читает `value` слов, function 6 пишет одно слово.
- `set_emu_ump`, `set_emu_tms`, `set_emu_igbt`, `set_wavegen`:
независимые экземпляры моделей. Callbacks получают **относительный offset**,
а не внешний адрес. Один модуль можно разместить несколько раз по разным адресам.
- `set_emu_server`: готовая композиция для текущего стенда, режим УМП/2812,
подготовка настроек с применением через 500 тиков.
- `set_regmap_rtu` и `set_regmap_can`: готовые транспортные кодеки над одной картой.
`set_emu_server_rtu/can` дополнительно сохраняют legacy поведение стенда.
USB CDC, UART, TCP или иной поток только доставляет полный RTU-кадр.
Другой протокол разбирает собственную оболочку и вызывает `set_regmap_request`.
Вызов с неизвестной функцией возвращает 1; неизвестный адрес, RO-запись,
чтение через границу региона — 2; неверное значение или размер буфера — 3;
ошибка аппаратного порта — 4; занятый модуль — 6. При ошибке `count=0`.
Длина успешного чтения равна запрошенной, запись возвращает адрес/значение.
Максимум — 124 слова. RTU-обёртка требует буфер 253 байта до выполнения команды;
CAN — 124 слова. Недостаточный буфер не может выполнить запись без ответа.
При ошибке содержимое выходного буфера не используется.
`set_regmap_init` проверяет границы 16-битного пространства, пересечения,
права и callbacks. Таблица и состояния принадлежат вызывающему и должны жить
дольше карты. После регистрации таблица неизменна; для изменения карты
инициализируйте её заново в момент, когда нет обслуживаемых запросов.
## Каталог и стандартная карта
Каталог доступен на адресе 0 через любой транспорт.
| Слово заголовка | Значение |
|---|---|
| 0 | 0x5345, сигнатура SE |
| 1 | версия каталога 1 |
| 2 | количество регионов |
| 3 | начало дескрипторов: 8 |
| 4 | длина дескриптора: 8 |
| 5 | максимум слов в одном чтении: 124 |
| 6, 7 | резерв: 0 |
Дескриптор: `kind, version, instance, base, words, flags, 0, 0`.
Flags: bit0 чтение, bit1 запись. Права описывают возможности региона;
отдельные регистры внутри могут быть только для чтения. Пропуски внутри
региона возвращают ошибку, если модуль их не поддерживает.
| Kind | Стандартный адрес | Размер | Модуль |
|---|---|---|---|
| 1 | 0x1000 | 0x17C | УМП: статус, окно, онлайн |
| 2 | 0x1200 | 36 | управление, конфигурация и indexed capability |
| 3 | 0x1300 | 8 | генератор DAC |
| 4 | 0x1400 | 112 | IGBT, версия 3 |
| 5 | 0x1500 | 80 | 2812 через регистры |
| 6 | 0x2000 | capacity × 40 | архив/банк УМП |
| 7 | 0xA000 | wave capacity | таблица DAC |
Эта карта — готовая композиция, а не требование к новым платам.
`set_regmap` позволяет выбирать адреса, подмножество и несколько экземпляров
модуля; каталог сообщает фактическую карту. Настройка UART и выбор УМП/2812
принадлежат только готовому `set_emu_server`. Максимальная ёмкость УМП
в этой карте — 819 записей на банк; текущая STM32 использует 800.
В режиме 2812 прямое обращение к регистрам УМП возвращает busy, legacy
RTU/CAN к этим диапазонам игнорируются для совместимости. Остальные сервисы
и каталог доступны. Регистры управления 2812 принимают запись только в режиме 2812.
Чтение его состояния не запускает модель.
## IGBT
Предыдущий локальный диапазон IGBT 0x1300 конфликтовал с DAC. Его alias
сохранить нельзя: он обращается к генератору. Используйте каталог kind=4 или
новый стандартный адрес 0x1400.
| Offset | Назначение |
|---|---|
| 0, 1 | 0x4947, версия 3 |
| 2 | enable 0/1; выключение сразу очищает состояние RX и ожидаемые ACK |
| 3 | чтение последнего TX; запись подготовленной TX-маски |
| 4 | RX-маска |
| 5 | резерв 0 |
| 6 | FAULT-маска 0…255 |
| 7 | задержка ACK 0…1000 мс |
| 8 | ширина ACK 1…255 мс |
| 9 | счётчик фронтов modulo 65536; запись 0 сбрасывает |
| 10 | число каналов 8 |
| 11 | один шаг модели = 1 мс |
| 12 | источник TX: 0 переданные GPIO, 1 подготовленная маска |
| 13 | подготовленная TX-маска, чтение/запись |
| 14 | запись маски запуска одиночного TX; чтение 0 |
| 15 | длина записи канала: 12 слов |
Версия 3 сохраняет первые 14 регистров. Общие delay/width при записи
устанавливают значения для всех каналов; при чтении показывают последнее
общее задание, а не поканальные значения. Полное состояние: FC03 0x1400 / 112 слов.
Запуск TX требует enabled=1, source=1, разрешённого TX канала и низкого TX;
повторный запуск высокого TX отвергается кодом 6. Длительность отсчитывает
сама модель, не клиент COM. Ручная запись TX отменяет все текущие TX-импульсы.
Выключение эмуляции отменяет импульсы и сбрасывает их TX; статическое задание
TX сохраняется. PB остаются входами: тестовый TX — внутренняя подстановка,
физическая обратная связь RX выводится на PC0…PC7.
Поканальные регистры: `0x1410 + 12 * channel`, channel=0…7.
| Offset канала | Значение |
|---|---|
| 0 | flags: bit0 разрешение TX, bit1 разрешение RX, bit2 ACK active-low |
| 1 | RX mode: 0 ACK, 1 физический 0, 2 физическая 1, 3 пропуск ACK |
| 2, 3 | задержка ACK ON 0…1000 мс, ширина 1…1000 мс |
| 4, 5 | задержка ACK OFF 0…1000 мс, ширина 1…1000 мс |
| 6 | одиночный TX: 1…60000 мс |
| 7 | маска фронтов для ACK: bit0 ON, bit1 OFF |
| 8 | ширина FAULT 1…60000 мс; 0 — удержание до снятия FAULT |
| 9 | RO счётчик фронтов канала, modulo 65536 |
| 10, 11 | RO оставшаяся задержка / длительность ACK в мс |
Изменение flags/mode отменяет ожидаемый и активный ACK канала.
Отключённый RX и выключенная модель всегда дают физический 0.
Инверсия применяется к автоматическому ответу и idle при пропуске ACK;
ручные уровни 0/1 не инвертируются. Сброс общего счётчика сбрасывает и канальные.
FAULT моделирует длинный/удерживаемый ответ после фронта; это не модель Vce,
Vge, DESAT, внутренней защиты или блокировки конкретного силового драйвера.
`set_igbt_tick(state, pins)` получает маску входов от порта платы.
Фронт на тике T запускает ACK на T+delay. Повторный фронт до начала ACK
перезапускает задержку; это определённая модельная обработка слишком частых команд.
Каждый из восьми каналов независим. FAULT удерживает RX после его активации;
снятие FAULT освобождает только ранее аварийные каналы и сохраняет обычные ACK.
Чтобы TX через регистры не перезаписывался GPIO, выберите source=1.
**Текущая IGBT-модель имеет шаг 1 мс.** Она проверяет логическую последовательность
Vin/Vstat, но не воспроизводит ACK 250/380 нс и ширины профилей 1SP/1SD в DSLogic.
Такую аппаратную точность нельзя получить переносом модели или вызовом из SysTick:
для неё требуется отдельный быстрый аппаратный порт и соответствующая модель времени.
## 2812
Состояние едино для legacy CMD_STD и адресного API:
| Offset | Назначение |
|---|---|
| 0, 1 | 0x544D, версия 1 |
| 2, 3, 4 | running, частота и задание (int16 ×100 Гц) |
| 8…11 | четыре подготовленных analog int16 |
| 12…17 | шесть подготовленных управляющих байт (значение 0…255) |
| 18 | записать 1: атомарно применить подготовленную команду |
| 32…45 | 28 байт дискретов; младший байт слова — первый байт |
| 46…79 | 34 аналоговых int16 |
| остальные | чтение 0 |
Модель остаётся упрощённой BALZAM_7: Go, выбор remote, Setspeed,
разгон 10 Гц/с и синтетическая телеметрия. Legacy CMD_STD имеет отдельный
адрес 2812, запрос 18 и ответ 101 байт, с проверкой CRC до изменения состояния.
Буфер ответа менее 101 байта запрещает выполнение команды.
## УМП и DAC
УМП сохраняет прежние записи по 40 слов, банк/архив, фронтовой триггер,
постинтервал 1000 мс и indexed-чтение без изменения курсора. Два разных
буфера по `capacity*40` слов предоставляет плата. Размещение SRAM/CCM
не является частью библиотеки.
DAC использует прежний `set_wave_port` с `start/stop`, транзакционную загрузку,
commit, чтение таблицы и запрет изменений при работе. Регистровые callbacks
и прежний `set_wave_rtu` вызывают одну реализацию. До повторного init вызывающий
останавливает аппаратный вывод; библиотека не угадывает текущее состояние DMA.
## Обёртки и новая плата
Пример: [ports/example-emulator/service.c](../ports/example-emulator/service.c).
Он регистрирует два экземпляра IGBT и показывает прямой, RTU и CAN вызов.
Порт новой платы должен:
1. Создать состояния и таблицу регионов, вызвать init и проверить результат.
2. Предоставить буферы и аппаратный DAC-порт, если нужен генератор.
3. Передавать реальные тики и входы; читать результат модели и обновлять GPIO.
4. Доставлять запросы через выбранную оболочку в одну карту, сериализуя доступ.
CAN-envelope совместим с UMP: BE address/value/0x1F00/token/function.
Идентификаторы CAN и маршрутизацию задаёт порт. Ответ делится по два слова,
нечётное чтение дополняется нулём до чётного числа; клиент отбрасывает последнее
слово по длине исходного запроса. Токен 1…255; один незавершённый ответ на соединение.
Проверенная копия слов хранится до отправки всех фрагментов.
`set_emu_stream` — только сборщик известных запросов FC03/FC06 и CMD_STD,
по одному экземпляру на поток. Он принимает монотонные uint32 миллисекунды,
сохраняет кадр между USB-пакетами, сбрасывает незавершённый после паузы >1000 мс
и восстанавливается после плохого CRC. Это не универсальный Modbus-парсер
FC16/FC23: для другого формата нужна другая тонкая оболочка.
## Сборка и проверки
C99-ядро подключается через `setprotocol_static`/DLL, определения публичного
API экспортируются через PCAN_ABI_API. Включить `set_regmap.h` или
`set_emu_server.h`. Для иной архитектуры проверить наличие uint8/16/32_t;
существующий 16-битный C28x octet-порт UMP остаётся отдельным, этот новый
байтовый API пока не проверен TI-компилятором.
`test_emulator_services.c` проверяет каталог, пересечения, границы и короткие
буферы, два экземпляра, переназначение адресов, RTU/CAN/stream, совместное
состояние 2812 и DAC, длинные задержки IGBT и освобождение FAULT.
Существующие тесты прошивки дополнительно проверяют архив УМП, USB-фрагментацию,
смену режима/адресов и совместимость с декодерами SETGUI.
## Выбор единственного эмулятора
В SETGUI: «Эмулятор → Настройки STM → Тип эмулятора», затем
«Применить в STM и проверить» и «Открыть управление эмулятором».
Конфигурация версии 3: режим 0 — УМП, 1 — 2812, 2 — IGBT, 3 — ЦАП.
Регистр 0x1219 подготавливает режим; 0x1218 = 0xA55A применяет его через
500 мс после подтверждения. Статус FC03 0x1210 / 10 слов содержит версию 3,
действующий режим в слове 8 и подготовленный в слове 9.
При смене режима останавливаются TIM6/DMA/DAC, ЦАП устанавливается в ноль,
IGBT отключается и его выходы обнуляются, 2812 сбрасывается, УМП останавливается.
Новый режим не запускается автоматически. Для IGBT нужно явно включить
эмуляцию; для ЦАП заново загрузить таблицу и дать команду пуска.
Повторное применение того же режима не прерывает текущую работу.
Записи невыбранным IGBT/ЦАП/2812 отклоняются с кодом 6; чтение статуса
и явные команды остановки IGBT/ЦАП доступны. Настройки связи доступны всегда.
После сброса выбран УМП, IGBT выключен, ЦАП не запущен.

View File

@@ -0,0 +1,146 @@
# Общая обработка графиков и сигналов
Аппроксимация, интерполяция и восстановление кривой по редким отсчётам имеют
один расчётный API. Источник может быть временным графиком, журналом, спектром
или таблицей ручного генератора. Выбор устройства и отрисовка остаются в порте.
| Метод | Назначение | Ограничения |
|---|---|---|
| `polynomial` | Аппроксимация полиномом МНК, степени 1–5; сглаживание шумных отсчётов | Нужно не меньше `degree + 1` различных X; степень задаёт пользователь |
| `linear` | Линейная интерполяция | Минимум два различных X; изломы в узлах |
| `pchip` | Интерполяция с сохранением формы | Минимум два различных X; подходит для фронтов и монотонных участков |
| `spline` | Восстановление естественным кубическим сплайном | Минимум два различных X; возможны выбросы между узлами |
Все методы строят сетку только между крайними выбранными отсчётами.
Восстановление по малому числу точек — оценка, зависящая от метода; утраченные
высокочастотные детали не определяются однозначно. СКО считается на исходных
измерениях, поэтому нулевая СКО интерполяции не означает нулевую ошибку между ними.
Одинаковые X усредняются, нечисловые и бесконечные значения отклоняются.
## Слои и зависимости
```text
График / журнал → Snapshot → Request → set_signal_reconstruct (C99) → Curve
Ручные точки генератора → waveform.generate → то же C99-ядро → таблица ЦАП
Qt-панель → worker → Curve → отдельный слой графика / CSV
МК / Android / другой GUI → C ABI → свой порт отображения или вывода
```
| Файл | Роль / зависимости |
|---|---|
| `include/set_signal.h`, `src/set_signal.c` | Все четыре численных метода и квантование ЦАП; C99, без Qt, heap и файловой системы |
| `python/set_devices/signal_reconstruction.py` | Тонкий вызов C через ctypes, без второго алгоритма |
| `python/set_devices/plot_processing.py` | Неизменяемые модели, выбор канала и диапазона, явные единицы, CSV; без Qt |
| `python/set_devices/qt_ports/plot_processing.py` | Общая панель, фоновый расчёт, проверка актуальности, слой отрисовки; PySide6 или PySide2 |
| `python/set_devices/waveform.py` | Периодическая сетка генератора и DAC12 через то же ядро |
Перед вызовом расчёта потребитель задаёт `SETPROTOCOL_LIBRARY` либо устанавливает
собранную библиотеку в штатный каталог `protocan/native`. Никакие соседние
репозитории или каталоги приложений автоматически не подключаются.
## Контракт графика
- `Series(key, label, points, visible, discrete, y_unit)` копирует пары X/Y.
Ключи уникальны внутри снимка. Расчёт доступен только видимым аналоговым каналам.
- `Axis(label, unit, encoding)` явно задаёт область X. `numeric` сохраняет числа,
`unix_ms` сохраняет UTC ISO timestamp. Большое число само по себе не является датой.
- `Snapshot(series, axis, source, x_range, blocked_reason)` описывает снимок источника.
`x_range=(left, right)` включает точки на обеих границах; `None` означает весь снимок.
`source` должен различать файлы/источники, если переключение между ними требует
сброса результата даже при совпадающих числах. `blocked_reason` запрещает расчёт.
- `prepare(snapshot, key, method, output_count, degree)` создаёт сравнимый запрос.
`process(request)` возвращает `Curve` с точками, числом измерений/уникальных X и СКО.
- `write_csv(curve, stream)` сохраняет отдельный результат, подпись метода и единицы.
Исходные данные не изменяются. При визуальном множителе Y адаптер передаёт
отображаемые значения и указывает множитель в `y_unit`, например `В (×2)`.
Нативное ядро допускает до 100000 входных и 10000 выходных точек. UI задаёт
2–10000 точек результата. После обрезки по X должно остаться достаточно узлов
для выбранного метода. Расчёт не добавляет искусственные узлы на границах окна.
## Пример без GUI
```python
from set_devices.plot_processing import Axis, Series, Snapshot, prepare, process, write_csv
samples = [(0, 0), (10, 2), (20, 1), (30, 0)]
snapshot = Snapshot((Series("voltage", "Напряжение", samples, y_unit="В"),),
Axis("Время", "мс"), source="bench-1")
request = prepare(snapshot, "voltage", method="pchip", output_count=301)
curve = process(request)
with open("calculated.csv", "w", encoding="utf-8-sig", newline="") as stream:
write_csv(curve, stream)
```
Рабочий CLI-пример для всех методов:
`python python/examples/plot_processing.py --method spline --output curve.csv`.
Добавьте `templates/python` в `PYTHONPATH` или установите пакет из этого каталога.
## Подключение нового Qt-графика
`PlotProcessingAttachment(parent, snapshot, repaint)` принимает два callback:
```python
def snapshot() -> Snapshot: ... # текущие отображаемые данные и видимые границы X
def repaint() -> None: ... # обычно QWidget.update
```
1. Создайте attachment и добавьте его `button` в панель графика.
2. После изменения данных, каналов, единиц или окна вызывайте `source_changed()`.
Пока панель не открывали, снимки не строятся. Уведомления одного прохода
event loop объединяются; устаревшая линия сразу скрывается.
3. После исходных кривых вызывайте `attachment.paint(painter, analog_rect, project)`.
`project(x, y, rect) -> QPointF` использует ту же проекцию, что исходные данные.
4. Вызов `open()` открывает немодальное окно с выбором всех четырёх методов,
числа точек, степени полинома, расчётом, удалением и экспортом.
```python
self.processing = PlotProcessingAttachment(self, self.processing_snapshot, self.update)
self.toolbar.layout().addWidget(self.processing.button)
# после обновления источника/масштаба:
self.processing.source_changed()
# внутри paintEvent, после исходных линий:
self.processing.paint(painter, self.analog_rect(), self.project)
```
Для встроенной панели используйте `SignalProcessingPanel.set_snapshot(snapshot)`
и сигнал `changed` для перерисовки. Передавайте `panel.curve` слою через
`set_external_curve(curve, x_offset=0)`. В этом варианте владелец обновляет снимок
и слой вместе. `x_offset` используется только при рисовании, например при сдвиге
epoch на графике наносекундного масштаба; исходные X и CSV сохраняются.
Панель сравнивает снимок выбранного канала, единицы, источник, диапазон и параметры.
При изменении результат убирается, запоздалый ответ worker игнорируется.
Для обработки живого потока остановите его обновление. Y-масштаб не меняет запрос,
если X и данные остались прежними. Кривая обрезается текущей областью Y; при выбросе
сплайна можно увеличить диапазон Y. Панель не меняет историю, autoscale, курсоры,
FFT и цифровые дорожки графика.
## Порты и расширение
SETGUI использует компонент в «Логах и графиках», SignalPlot (CAN, температуры,
УМП), TrendPlot и SpectrumPlot. В спектре методы обрабатывают зависимость уровня
от частоты в Гц; восстановление временного сигнала из амплитудного спектра этим
не выполняется. Временная панель «Логов» блокируется при включённом FFT.
Генератор использует те же методы через `waveform.generate`; для циклической
таблицы последний отсчёт периода не дублируется.
Для нового графика достаточно адаптера `Snapshot` и существующей панели/слоя.
Для другого GUI используйте модель без Qt или непосредственно C ABI.
Для MCU вызывайте `set_signal_reconstruct` с буфером `14 * count + 128` double,
выделенным вызывающей стороной; рабочая память должна соответствовать RAM платы.
Контракт вывода ЦАП и готовые порты F407/G474 описаны в [SIGNAL_GENERATOR.md](SIGNAL_GENERATOR.md).
Для нового численного метода сначала расширьте C API, C-тесты и соответствие
`METHODS` в ctypes-порте; затем добавьте эталон в `test_plot_processing.py`.
Алгоритмы в обработчиках отдельных графиков не дублируются.
Проверки библиотеки:
```text
python -m unittest discover -s python/tests -p test_plot_processing*.py
python -m unittest discover -s python/tests -p test_shared_library_boundary.py
```
Для тестов Qt нужен PySide6/PySide2; для headless-прогона задайте
`QT_QPA_PLATFORM=offscreen`. Численные тесты `test_plot_processing.py` Qt не требуют.

View File

@@ -1,4 +1,24 @@
# Генератор произвольного сигнала WG v1
# Генератор произвольного сигнала WG v1/v2
## Расширение карты v2: F407 до 1 000 000 отсчётов/с
Версия карты в статусе — 2, `set_wave_state.rate` — uint32_t.
Для записи частоты сначала остановить выход, записать старшие 16 бит в
`0x1308`, затем младшие 16 бит в `0x1303`. Вторая запись проверяет диапазон
1…1 000 000 и применяет частоту; обе записи снимают ready. Чтение `0x1308`
возвращает старшие биты применённой частоты, а слово 3 статуса — младшие.
Host ABI: операция 9 — запись старшего слова, 8 — его чтение, 2 — запись
младшего слова (включая ноль). Остальные команды не меняются.
Это версия карты регистров, а не транспорт: SETGUI использует SET v2
через EmulatorSerialPort, с резервным RTU для старой прошивки. Новый клиент
понимает статус карт v1/v2; для v1 предел остаётся 50 000. Старый клиент
отклоняет незнакомую версию 2 до записи. Код F407 и GUI поддерживают 1 МГц;
порт G474 пока сохраняет прежний аппаратный лимит 50 кГц.
Ниже описание исходной карты v1; пределы и регистры частоты заменены этим
расширением для v2. На высоких частотах аналоговое установление зависит
от величины скачка и нагрузки; аппаратная проверка осциллографом обязательна
для оценки точности конкретного сигнала.
Общее C99-ядро строит кривые по точкам, преобразует напряжения в коды ЦАП и
принимает таблицы через RTU поверх USB CDC/COM. Оно не зависит от Qt, HAL или ОС.

View File

@@ -0,0 +1,24 @@
#ifndef SET_FIRMWARE_IMAGE_H
#define SET_FIRMWARE_IMAGE_H
#include "pcan_abi.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Caller-owned workspace; the parser never allocates memory or accesses files. */
typedef struct { uint32_t address; uint8_t value; } firmware_hex_cell_t;
enum { FW_HEX_OK, FW_HEX_FORMAT, FW_HEX_CHECKSUM, FW_HEX_RECORD,
FW_HEX_EOF, FW_HEX_OVERLAP, FW_HEX_RANGE, FW_HEX_CAPACITY,
FW_HEX_PREFIX, FW_HEX_DIGITS, FW_HEX_LENGTH };
/* Strict HEX: reject any overlap and any non-whitespace after EOF. On failure
* count is zero. Cells are sorted by address only on successful completion. */
PCAN_ABI_API int firmware_hex_parse(const char *text, size_t length,
firmware_hex_cell_t *cells, size_t capacity, size_t max_records,
size_t *count, size_t *error_line);
/* Return one contiguous segment starting at offset in a successfully parsed
* workspace; the caller uses its address and copies its values into its model. */
PCAN_ABI_API size_t firmware_hex_segment_size(const firmware_hex_cell_t *cells,
size_t count, size_t offset);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -1,26 +0,0 @@
#ifndef SET_BOOT_H
#define SET_BOOT_H
#include "set_firmware.h"
/* Transport-independent, single-slot updater. invalidate MUST become durable
* before erase; commit is called only after read-back verification. */
typedef struct {
bool (*invalidate)(void *user);
bool (*erase)(void *user, uint32_t size);
bool (*write)(void *user, uint32_t offset, const uint8_t *data, uint16_t size);
bool (*verify)(void *user, uint32_t size, uint32_t crc);
bool (*commit)(void *user, uint32_t size, uint32_t crc);
void (*activate)(void *user);
} setp_boot_ops;
typedef struct {
setp_boot_ops ops;
void *user;
uint32_t base, capacity, crc;
uint16_t block_size;
setp_fw_status_t status;
} setp_boot;
void setp_boot_init(setp_boot *b, const setp_boot_ops *ops, void *user,
uint32_t base, uint32_t capacity);
/* Returns SET status; FW_STATUS body is encoded by the caller. */
uint16_t setp_boot_command(setp_boot *b, uint16_t type, const uint8_t *p, uint16_t n);
#endif

View File

@@ -1,32 +0,0 @@
#ifndef SET_EMU_IGBT_H
#define SET_EMU_IGBT_H
#include "set_regmap.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SET_IGBT_CHANNELS 8u
#define SET_IGBT_WORDS 112u
#define SET_IGBT_CHANNEL_WORDS 12u
typedef struct {
uint16_t flags, mode, on_delay, on_width, off_delay, off_width;
uint16_t tx_width, edges, fault_width, count;
} set_igbt_channel;
typedef struct {
uint8_t enabled, tx, previous_tx, rx, fault, injected_tx, input_source;
uint16_t pending[SET_IGBT_CHANNELS], active[SET_IGBT_CHANNELS];
uint16_t delay_ms, width_ms, event_count;
uint8_t logical_rx;
uint16_t pulse[SET_IGBT_CHANNELS], response_width[SET_IGBT_CHANNELS];
set_igbt_channel channel[SET_IGBT_CHANNELS];
} set_igbt_emu;
PCAN_ABI_API void set_igbt_init(set_igbt_emu *);
/* One model tick = 1 ms, no system clock. This coarse logical model does NOT
* reproduce the sub-microsecond timing of 1SP/1SD DSLogic hardware profiles.
* input_source=0 takes the supplied pins, 1 uses register-injected TX. */
PCAN_ABI_API void set_igbt_tick(set_igbt_emu *, uint8_t pins);
PCAN_ABI_API unsigned set_igbt_read(void *, uint16_t offset, uint16_t count, uint16_t *);
PCAN_ABI_API unsigned set_igbt_write(void *, uint16_t offset, uint16_t value);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -1,48 +0,0 @@
/* Reference service composition for emulator boards; use set_regmap directly
* for a different map, subset, or multiple instances of any module. */
#ifndef SET_EMU_SERVER_H
#define SET_EMU_SERVER_H
#include "set_emu_ump.h"
#include "set_emu_tms.h"
#include "set_emu_igbt.h"
#include "set_wavegen.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SET_EMU_CONTROL_BASE 0x1200u
#define SET_EMU_IGBT_BASE 0x1400u
#define SET_EMU_TMS_BASE 0x1500u
#define SET_EMU_INDEXED_BASE 0x2000u
enum { SET_EMU_KIND_UMP=1,SET_EMU_KIND_CONTROL=2,SET_EMU_KIND_WAVE=3,
SET_EMU_KIND_IGBT=4,SET_EMU_KIND_TMS=5,SET_EMU_KIND_UMP_DATA=6,
SET_EMU_KIND_WAVE_DATA=7 };
typedef struct {
set_regmap map;
set_reg_region regions[7];
set_ump_emu ump;
set_tms_emu tms;
set_igbt_emu igbt;
set_wave_state *wave;
/* config[3]: 0=UMP, 1=TMS2812, 2=IGBT, 3=DAC. Configuration version 3. */
uint16_t config[4],staged[4],apply_delay;
} set_emu_server;
/* bank_capacity in records, each bank has capacity*40 words.
* Wave may be NULL. UART rate index 0..4. No pins, heap or global instances.
* Init is for a quiescent instance: caller stops hardware before reinitializing. */
PCAN_ABI_API unsigned set_emu_server_init(set_emu_server *,uint16_t *bank0,
uint16_t *bank1,uint16_t bank_capacity,set_wave_state *,unsigned address,
unsigned tms_address,unsigned baud_index,unsigned can_mode,unsigned enabled);
PCAN_ABI_API void set_emu_server_tick(set_emu_server *,uint8_t igbt_pins);
PCAN_ABI_API uint32_t set_emu_server_baud(const set_emu_server *);
PCAN_ABI_API size_t set_emu_server_rtu(set_emu_server *,const uint8_t *,size_t,uint8_t *,size_t);
PCAN_ABI_API size_t set_emu_server_can(set_emu_server *,const uint8_t *,size_t,
uint16_t *,size_t,unsigned *,unsigned *);
/* Stream adapter for FC03/06 (8 octets) and TMS CMD_STD (18 octets). One
* context per connection. Transport passes monotonic milliseconds. */
typedef struct { uint8_t data[18],used;uint32_t last_byte; } set_emu_stream;
PCAN_ABI_API size_t set_emu_stream_feed(set_emu_stream *,set_emu_server *,
uint8_t byte,uint32_t now_ms,uint8_t *reply,size_t capacity);
#ifdef __cplusplus
}
#endif
#endif

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