Compare commits

..

4 Commits

108 changed files with 13797 additions and 32 deletions

View File

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

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

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

66
c/candle/LICENSE Normal file

File diff suppressed because one or more lines are too long

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

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

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

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

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

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

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

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

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

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

View File

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

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

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

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

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

View File

@@ -14,4 +14,9 @@ if(DS18B20_DS2480_BUILD_TESTS)
add_executable(test_ds18b20_ds2480 tests/test_ds18b20_ds2480.c) add_executable(test_ds18b20_ds2480 tests/test_ds18b20_ds2480.c)
target_link_libraries(test_ds18b20_ds2480 PRIVATE ds18b20_ds2480) target_link_libraries(test_ds18b20_ds2480 PRIVATE ds18b20_ds2480)
add_test(NAME ds18b20_ds2480 COMMAND test_ds18b20_ds2480) add_test(NAME ds18b20_ds2480 COMMAND test_ds18b20_ds2480)
add_executable(test_ds2480_stm32f4
ports/stm32f4/ds2480_stm32f4_hal.c ports/stm32f4/tests/test_port.c)
target_include_directories(test_ds2480_stm32f4 PRIVATE
. ports/stm32f4 ports/stm32f4/tests)
add_test(NAME ds2480_stm32f4 COMMAND test_ds2480_stm32f4)
endif() endif()

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ extern "C" {
typedef enum { typedef enum {
DS2480_OK = 0, DS2480_DONE, DS2480_NO_PRESENCE, DS2480_SHORT, DS2480_OK = 0, DS2480_DONE, DS2480_NO_PRESENCE, DS2480_SHORT,
DS2480_IO, DS2480_PROTOCOL, DS2480_CRC, DS2480_ARGUMENT, DS2480_IO, DS2480_PROTOCOL, DS2480_CRC, DS2480_ARGUMENT,
DS2480_NOT_READY, DS2480_DATA DS2480_NOT_READY, DS2480_DATA, DS2480_BUSY
} ds2480_status; } ds2480_status;
/** Blocking UART callbacks: 0 = success, nonzero = error/timeout. /** Blocking UART callbacks: 0 = success, nonzero = error/timeout.
@@ -33,6 +33,8 @@ typedef struct {
ds2480_port port; ds2480_port port;
uint32_t timeout_ms; uint32_t timeout_ms;
uint8_t ready; uint8_t ready;
uint8_t power_active;
uint8_t data_mode;
} ds2480; } ds2480;
/** Independent ROM search cursor. Zero-initialize before each enumeration. */ /** Independent ROM search cursor. Zero-initialize before each enumeration. */
@@ -50,12 +52,20 @@ ds2480_status ds2480_init(ds2480 *bus, const ds2480_port *port, uint32_t timeout
ds2480_status ds2480_reset(ds2480 *bus); ds2480_status ds2480_reset(ds2480 *bus);
/** Exchange one slot (write 1 to read); received must be non-NULL. */ /** Exchange one slot (write 1 to read); received must be non-NULL. */
ds2480_status ds2480_bit(ds2480 *bus, uint8_t bit, uint8_t *received); ds2480_status ds2480_bit(ds2480 *bus, uint8_t bit, uint8_t *received);
/** Exchange a byte, LSB first, in command mode (eight UART transactions). */ /** Exchange a byte in Data Mode; escapes E3 and preserves one reply per byte. */
ds2480_status ds2480_byte(ds2480 *bus, uint8_t value, uint8_t *received); ds2480_status ds2480_byte(ds2480 *bus, uint8_t value, uint8_t *received);
/** Write a byte and start strong pullup immediately after its last slot. /** Write a byte and start strong pullup immediately after its last slot.
* Blocks for hold_ms (1..1000), then terminates the pulse and consumes reply. * Blocks for hold_ms (1..1000), then terminates the pulse and consumes reply.
*/ */
ds2480_status ds2480_power_byte(ds2480 *bus, uint8_t value, uint32_t hold_ms); ds2480_status ds2480_power_byte(ds2480 *bus, uint8_t value, uint32_t hold_ms);
/** Start indefinite strong pullup after the final bit; returns immediately
* after UART exchange. Until power_end, other bus operations return BUSY.
* Application MUST call power_end after the sensor's required hold time,
* or init to cancel/recover. No internal timer/interrupt releases the pulse.
*/
ds2480_status ds2480_power_begin(ds2480 *bus, uint8_t value);
/** Release strong pullup, consume its reply. Idempotent when ready/idle. */
ds2480_status ds2480_power_end(ds2480 *bus);
/** CRC8 Dallas/Maxim. data must address size bytes (NULL allowed for size=0). */ /** CRC8 Dallas/Maxim. data must address size bytes (NULL allowed for size=0). */
uint8_t ds2480_crc8(const uint8_t *data, uint32_t size); uint8_t ds2480_crc8(const uint8_t *data, uint32_t size);
/** Search ALL families. OK yields ROM with valid CRC; DONE ends enumeration. /** Search ALL families. OK yields ROM with valid CRC; DONE ends enumeration.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,93 @@
# Altera Logic: общее ядро и порты
Потоковый SETCAN/GAS-профиль и общие порты описаны отдельно:
[Altera Logic ONLINE](../../doc/setcan/ALTERA_LOGIC_STREAM.md).
Ниже — совместимый прежний буферный протокол UART `A5 CMD`.
Клиент UART-протокола FPGA-анализатора Cyclone IV: 16 каналов, 4096 выборок.
Работает с существующим RTL `altera_loganalisator`, версия протокола 1.
Ядро C99 не зависит от Qt, ОС, системных часов и динамической памяти.
Слои: SETGUI → Python ctypes → C99 → Python Qt-порт → COM / USBUART.
GUI передаёт числовые настройки и получает готовый массив; формат пакетов,
XOR, endian, последовательность команд, парсер и состояния тайм-аутов находятся в C.
| Файл | Назначение и зависимости |
|---|---|
| `include/altera_logic.h` | Публичный ABI, макрос экспорта из `pcan_abi.h` |
| `src/altera_logic.c` | Кодек и автомат обмена; только стандартная библиотека C |
| `tests/test_altera_logic.c` | Эталонные пакеты RTL, полный захват, ошибки |
| `../../python/altera_logic/native.py` | ctypes-модели, загрузка DLL, экспорт CSV; stdlib |
| `../../python/altera_logic/qt_port.py` | Асинхронный COM-порт; PySide6 QtSerialPort |
## Сборка
Из корня `templates`:
```powershell
python c/set-protocol/tools/build_host.py --output python/altera_logic/native/setprotocol.dll
```
На Linux выходной файл — `python/altera_logic/native/libsetprotocol.so`.
Также поддерживается основная CMake-сборка `c/set-protocol`; путь к результату
задаётся через `ALTERA_LOGIC_LIBRARY`. Библиотека без символов `la_*`
не поддерживается: тихого Python-fallback нет.
## Контракт порта
Выделить выровненную память размером `la_context_size()`; все обращения к
одному контексту выполняются в одном потоке.
- `la_init(ctx)` — начать с INFO.
- `la_next(ctx, out, capacity)` — получить следующий запрос, либо 0, если отправлять нечего.
- `la_feed(ctx, bytes, size)` — передать принятые байты, включая частичные ответы.
- `la_tick(ctx, elapsed_ms)` — сообщить прошедшее время; тайм-аут ответа 1000 мс.
- `la_start(ctx, divider, mask, value, edge_mask, edge_value)` — настройка и новый захват.
- `la_get(ctx, field)` — состояние и прогресс; `la_samples` — завершённая запись.
- `la_fail(ctx, LA_IO_ERROR)` — сообщить ошибку транспорта.
COM: 921600 бод, 8N1, без управления потоком. Одновременно ожидается только
один ответ. После ARM STATUS опрашивается каждые 25 мс; ожидание самого
триггера не ограничено. После done читаются 64 блока по 64 выборки.
Ошибки закрывают сеанс без автоматического повтора ARM. Неверный размер,
код команды или XOR прекращает сеанс. При рассинхронизации запроса на FPGA
может потребоваться аппаратный RESET_N; переподключение не гарантирует её сброс.
Клиент намеренно принимает только профиль 16 × 4096 версии 1.
Поддержка внешней SDRAM и расширенных адресов сюда пока не входит.
Прерывание в GUI закрывает порт ПК: STOP в протоколе FPGA отсутствует.
## Быстрый старт без платы
Добавить `templates/python` в PYTHONPATH:
```python
from altera_logic import NativeAnalyzer
client = NativeAnalyzer()
client.reset(demo=True)
client.start(divider=49)
capture = client.capture()
print(len(capture.samples)) # 4096
print(capture.sample_rate) # 1_000_000
print(capture.trigger_index) # 2048
capture.save_csv("demo.csv")
```
Демо создаёт синтетическую запись в C; не моделирует заданные условия триггера.
CSV содержит явный признак demo, время относительно триггера и состояния D0…D15.
## Потребители и проверка
Используется вкладкой SETGUI «Altera Logic». Для Android/JNI новый потребитель
может использовать тот же ABI; интерфейс Android в этой задаче не изменяется.
Существующий ABI SETProtocol не изменён, добавлены только символы `la_*`.
```powershell
$env:PYTHONPATH = "$PWD/python"
python -m unittest discover -s python/tests -p test_altera_logic.py
```
CMake включает `test_altera_logic` и CTest `altera_logic`.
На плате требуется отдельно проверить разводку пинов, RAM-модуль и прошивку;
программные проверки не заменяют аппаратную проверку.

View File

@@ -6,6 +6,8 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
# SET protocol v2: управление, телеметрия, CAN segmentation и firmware flow. # SET protocol v2: управление, телеметрия, CAN segmentation и firmware flow.
set(SETPROTOCOL_V2_SOURCES set(SETPROTOCOL_V2_SOURCES
src/altera_logic.c
src/altera_stream.c
src/set_protocol.c src/set_protocol.c
src/set_can.c src/set_can.c
src/set_firmware.c src/set_firmware.c
@@ -79,6 +81,12 @@ endif()
option(SETP_BUILD_TESTS "Build host tests" ON) option(SETP_BUILD_TESTS "Build host tests" ON)
if(SETP_BUILD_TESTS) if(SETP_BUILD_TESTS)
enable_testing() enable_testing()
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)
add_executable(test_altera_stream tests/test_altera_stream.c)
target_link_libraries(test_altera_stream PRIVATE setprotocol_static)
add_test(NAME altera_stream COMMAND test_altera_stream)
add_executable(test_plot tests/test_plot.c) add_executable(test_plot tests/test_plot.c)
target_link_libraries(test_plot PRIVATE setprotocol_static) target_link_libraries(test_plot PRIVATE setprotocol_static)

View File

@@ -0,0 +1,35 @@
/** Altera logic analyzer UART client. C99, caller-owned memory, no OS/heap.
* One request at a time; no automatic retries (ARM is not idempotent).
* Initialize aligned storage of la_context_size() bytes, then call la_next,
* la_feed and la_tick from one thread. tick receives elapsed milliseconds.
*/
#ifndef ALTERA_LOGIC_H
#define ALTERA_LOGIC_H
#include "pcan_abi.h"
#ifdef __cplusplus
extern "C" {
#endif
enum { LA_CONNECTING, LA_READY, LA_CAPTURING, LA_DONE, LA_ERROR };
enum { LA_OK, LA_BAD_FRAME, LA_CHECKSUM, LA_DEVICE_ERROR, LA_TIMEOUT,
LA_UNSUPPORTED, LA_ARGUMENT, LA_IO_ERROR };
enum { LA_STATE, LA_ERROR_CODE, LA_CHANNELS, LA_DEPTH, LA_CLOCK_HZ,
LA_FLAGS, LA_TRIGGER_INDEX, LA_READ_COUNT, LA_DIVIDER };
PCAN_ABI_API size_t la_context_size(void);
PCAN_ABI_API void la_init(void *ctx);
/** Return 0 on success. Configuration is immutable throughout this capture. */
PCAN_ABI_API int la_start(void *ctx, uint32_t divider, uint32_t mask,
uint32_t value, uint32_t edge_mask, uint32_t edge_value);
/** Returns 6 when a request is ready, otherwise 0. Capacity must be >=6. */
PCAN_ABI_API size_t la_next(void *ctx, uint8_t *out, size_t capacity);
PCAN_ABI_API void la_feed(void *ctx, const uint8_t *data, size_t size);
PCAN_ABI_API void la_tick(void *ctx, uint32_t elapsed_ms);
PCAN_ABI_API void la_fail(void *ctx, uint32_t code);
PCAN_ABI_API uint32_t la_get(const void *ctx, uint32_t field);
PCAN_ABI_API size_t la_samples(const void *ctx, uint16_t *out, size_t capacity);
/** Offline fixture, never communicates with hardware or claims a real trigger. */
PCAN_ABI_API void la_demo_init(void *ctx);
PCAN_ABI_API int la_demo_capture(void *ctx, uint32_t divider);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,46 @@
/** SETCAN/GAS Altera Logic online stream, shared by CAN and UART.
* Caller-owned context; one serialized caller. No heap, OS or Qt dependencies.
*/
#ifndef ALTERA_STREAM_H
#define ALTERA_STREAM_H
#include "pcan_abi.h"
#include "pcan_frame.h"
#ifdef __cplusplus
extern "C" {
#endif
#define LAS_DEVICE_TYPE 6U
#define LAS_DEVICE_ID 14U
#define LAS_GAS_META 0xFE00U
#define LAS_GAS_DATA 0xFF00U
#define LAS_CAPACITY 8192U
#define LAS_TAG 0x4C01U
enum { LAS_COUNT, LAS_PERIOD_NS, LAS_SESSION, LAS_RECEIVED, LAS_MISSING,
LAS_DUPLICATES, LAS_INVALID, LAS_IGNORED, LAS_CRC_ERRORS, LAS_HAS_META };
PCAN_ABI_API size_t las_context_size(void);
PCAN_ABI_API int las_init(void *ctx, uint32_t device_id);
PCAN_ABI_API void las_can(void *ctx, uint32_t id, const uint8_t *data, size_t size,
uint32_t extended, uint32_t remote);
PCAN_ABI_API void las_uart(void *ctx, const uint8_t *data, size_t size);
PCAN_ABI_API uint32_t las_get(const void *ctx, uint32_t field);
/** Chronological bounded history. breaks[i]=1 marks a discontinuity before i. */
PCAN_ABI_API size_t las_snapshot(const void *ctx, uint64_t *indices, uint16_t *samples,
uint8_t *breaks, size_t capacity);
/** Sparse step trace for rendering; capacity >= 2*LAS_COUNT, move=pen-up. */
PCAN_ABI_API size_t las_trace(const void *ctx, uint32_t channel, uint64_t *indices,
uint8_t *levels, uint8_t *moves, size_t capacity);
PCAN_ABI_API uint32_t las_device_type(void);
PCAN_ABI_API uint32_t las_device_id(void);
PCAN_ABI_API const char *las_device_name(void);
PCAN_ABI_API void las_demo_step(void *ctx, uint32_t count);
/** Device-side packet builders; return zero for invalid arguments.
* uart=0: 8-byte CAN data + *id; uart=1: complete AA55 transport + *id.
* Same builders are used by firmware ports, tests and the demo producer.
*/
PCAN_ABI_API size_t las_metadata(uint32_t device, uint32_t session, uint32_t period_ns,
uint32_t uart, uint32_t *id, uint8_t *out, size_t capacity);
PCAN_ABI_API size_t las_data(uint32_t device, uint32_t session, uint32_t index,
uint32_t sample, uint32_t uart, uint32_t *id, uint8_t *out, size_t capacity);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,142 @@
#include "altera_logic.h"
#include <string.h>
typedef struct {
uint32_t state, error, clock_hz, elapsed, delay;
uint16_t config[5], channels, depth, trigger, count, offset;
uint16_t samples[4096];
uint8_t command, pending, flags, requested, rx[133];
size_t used;
} la_context;
static uint8_t checksum(const uint8_t *p, size_t n) {
uint8_t x = 0; size_t i;
for (i = 0; i < n; ++i) x ^= p[i];
return x;
}
static uint16_t u16(const uint8_t *p) {
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}
size_t la_context_size(void) { return sizeof(la_context); }
void la_init(void *ctx) {
la_context *s = (la_context *)ctx;
memset(s, 0, sizeof(*s)); s->command = 1; s->state = LA_CONNECTING;
}
void la_fail(void *ctx, uint32_t code) {
la_context *s = (la_context *)ctx;
s->state = LA_ERROR; s->error = code; s->pending = 0; s->command = 0;
}
int la_start(void *ctx, uint32_t div, uint32_t mask, uint32_t val,
uint32_t emask, uint32_t eval) {
la_context *s = (la_context *)ctx;
if ((s->state != LA_READY && s->state != LA_DONE) ||
div > 65535 || mask > 65535 || val > 65535 ||
emask > 65535 || eval > 65535 || ((val ^ eval) & mask & emask))
return LA_ARGUMENT;
s->config[0]=(uint16_t)div; s->config[1]=(uint16_t)mask;
s->config[2]=(uint16_t)val; s->config[3]=(uint16_t)emask;
s->config[4]=(uint16_t)eval;
s->state=LA_CAPTURING; s->error=0; s->flags=0; s->count=0;
s->offset=0; s->command=2; s->delay=0;
return LA_OK;
}
size_t la_next(void *ctx, uint8_t *out, size_t capacity) {
la_context *s = (la_context *)ctx; uint16_t arg = 0;
if (!out || capacity < 6 || !s->command || s->pending || s->delay) return 0;
if (s->command >= 2 && s->command <= 6) arg=s->config[s->command-2];
if (s->command == 9) {
arg=s->offset;
s->requested=(uint8_t)((s->depth-s->offset > 64) ? 64 : s->depth-s->offset);
}
out[0]=0xa5; out[1]=s->command; out[2]=(uint8_t)arg;
out[3]=(uint8_t)(arg >> 8); out[4]=(s->command == 9) ? s->requested : 0;
out[5]=checksum(out,5);
s->pending=1; s->elapsed=0; s->used=0;
return 6;
}
static void complete(la_context *s) {
size_t i;
if (checksum(s->rx,s->used)) { la_fail(s,LA_CHECKSUM); return; }
if (s->rx[2]) { la_fail(s,LA_DEVICE_ERROR); return; }
s->pending=0;
if (s->command == 1) {
s->channels=s->rx[3]; s->depth=u16(s->rx+4);
s->clock_hz=(uint32_t)s->rx[6]*1000000U;
if (s->channels != 16 || s->depth != 4096 || !s->clock_hz || s->rx[7]!=1) {
la_fail(s,LA_UNSUPPORTED); return;
}
s->state=LA_READY; s->command=0;
} else if (s->command < 7) ++s->command;
else if (s->command == 7) { s->command=8; s->delay=25; }
else if (s->command == 8) {
s->flags=s->rx[3]; s->trigger=u16(s->rx+4);
if (s->flags & 4) {
if (s->trigger >= s->depth || (s->flags & 1)) { la_fail(s,LA_BAD_FRAME); return; }
s->command=9;
} else s->delay=25;
} else {
for(i=0; i<s->requested; ++i) s->samples[s->offset+i]=u16(s->rx+4+i*2);
s->offset=(uint16_t)(s->offset+s->requested); s->count=s->offset;
if(s->offset == s->depth) { s->state=LA_DONE; s->command=0; }
}
}
void la_feed(void *ctx, const uint8_t *data, size_t size) {
la_context *s = (la_context *)ctx; size_t i, expected;
if (!data) return;
for(i=0; i<size; ++i) {
if(!s->pending || s->used >= sizeof(s->rx)) { la_fail(s,LA_BAD_FRAME); return; }
s->rx[s->used++]=data[i];
if(s->rx[0]!=0x5a || (s->used>=2 && s->rx[1]!=(s->command|0x80))) {
la_fail(s,LA_BAD_FRAME); return;
}
if(s->used<3) continue;
expected=4;
if(!s->rx[2]) {
if(s->command==1) expected=9;
else if(s->command==8) expected=7;
else if(s->command==9) {
if(s->used<4) continue;
if(s->rx[3]!=s->requested) { la_fail(s,LA_BAD_FRAME); return; }
expected=5U+2U*s->requested;
}
}
if(s->used==expected) complete(s);
}
}
void la_tick(void *ctx, uint32_t ms) {
la_context *s=(la_context *)ctx;
if(s->pending) {
if(ms >= 1000U-s->elapsed) la_fail(s,LA_TIMEOUT);
else s->elapsed+=ms;
}
s->delay=(ms >= s->delay) ? 0 : s->delay-ms;
}
uint32_t la_get(const void *ctx, uint32_t field) {
const la_context *s=(const la_context *)ctx;
switch(field) {
case LA_STATE:return s->state; case LA_ERROR_CODE:return s->error;
case LA_CHANNELS:return s->channels; case LA_DEPTH:return s->depth;
case LA_CLOCK_HZ:return s->clock_hz; case LA_FLAGS:return s->flags;
case LA_TRIGGER_INDEX:return s->trigger; case LA_READ_COUNT:return s->count;
case LA_DIVIDER:return s->config[0]; default:return 0;
}
}
size_t la_samples(const void *ctx, uint16_t *out, size_t capacity) {
const la_context *s=(const la_context *)ctx;
if(s->state!=LA_DONE || !out || capacity<s->count) return 0;
memcpy(out,s->samples,s->count*sizeof(uint16_t)); return s->count;
}
void la_demo_init(void *ctx) {
la_context *s=(la_context *)ctx; la_init(s);
s->channels=16; s->depth=4096; s->clock_hz=50000000;
s->state=LA_READY; s->command=0;
}
int la_demo_capture(void *ctx, uint32_t divider) {
la_context *s=(la_context *)ctx; size_t i, bit;
int rc=la_start(s,divider,0,0,0,0); if(rc) return rc;
for(i=0;i<4096;++i) {
uint16_t v=0;
for(bit=0;bit<16;++bit) if(((i/(4U+bit*7U))&1U)!=0) v|=(uint16_t)(1U<<bit);
s->samples[i]=v;
}
s->trigger=2048; s->flags=6; s->count=4096; s->command=0; s->state=LA_DONE;
return LA_OK;
}

View File

@@ -0,0 +1,158 @@
#include "altera_stream.h"
#include "pcan_id.h"
#include <string.h>
typedef struct {
pcan_parser_t parser;
uint64_t indices[LAS_CAPACITY], next_index;
uint16_t samples[LAS_CAPACITY];
uint8_t breaks[LAS_CAPACITY];
uint32_t head, count, period, session, received, missing, duplicates, invalid, ignored;
uint8_t device, metadata, started;
} las_context;
static uint16_t r16(const uint8_t *p) { return (uint16_t)(p[0]|((uint16_t)p[1]<<8)); }
static uint32_t r32(const uint8_t *p) { return (uint32_t)r16(p)|((uint32_t)r16(p+2)<<16); }
static void w16(uint8_t *p,uint16_t v) { p[0]=(uint8_t)v;p[1]=(uint8_t)(v>>8); }
static void w32(uint8_t *p,uint32_t v) { w16(p,(uint16_t)v);w16(p+2,(uint16_t)(v>>16)); }
uint32_t las_device_type(void) { return LAS_DEVICE_TYPE; }
uint32_t las_device_id(void) { return LAS_DEVICE_ID; }
const char *las_device_name(void) { return "Altera Logic"; }
size_t las_context_size(void) { return sizeof(las_context); }
int las_init(void *ctx,uint32_t device) {
las_context *s=(las_context *)ctx;
if(!ctx || device>15) return 0;
memset(s,0,sizeof(*s));s->device=(uint8_t)device;pcan_parser_init(&s->parser);return 1;
}
static void append(las_context *s,uint64_t index,uint16_t value,uint8_t gap) {
s->indices[s->head]=index;s->samples[s->head]=value;s->breaks[s->head]=gap;
s->head=(s->head+1U)%LAS_CAPACITY;
if(s->count<LAS_CAPACITY) ++s->count;
}
void las_can(void *ctx,uint32_t raw,const uint8_t *p,size_t n,uint32_t extended,uint32_t remote) {
las_context *s=(las_context *)ctx;pcan_id_t id;uint32_t index,delta,period,session;
if(!extended || remote || raw>PCAN_ID_MASK) { ++s->ignored;return; }
pcan_id_unpack(raw,&id);
if(id.device_type!=LAS_DEVICE_TYPE || id.device_id!=s->device ||
id.route!=PCAN_ROUTE_FROM_DEVICE || id.msg_type!=PCAN_MSG_GAS ||
(id.msg_body!=LAS_GAS_META && id.msg_body!=LAS_GAS_DATA)) { ++s->ignored;return; }
if(n!=8 || !p) { ++s->invalid;return; }
if(id.msg_body==LAS_GAS_META) {
period=r32(p+4);session=r16(p+2);
if(r16(p)!=LAS_TAG || period==0) { ++s->invalid;return; }
if(s->metadata && session==s->session) {
if(period!=s->period) ++s->invalid;
return;
}
if(s->metadata && (uint16_t)(session-s->session)>=0x8000U) {
++s->duplicates;return;
}
s->metadata=1;s->session=session;s->period=period;
s->count=0;s->head=0;s->started=0;s->next_index=0;
return;
}
if(!s->metadata) { ++s->ignored;return; }
if(r16(p)!=s->session) { ++s->ignored;return; }
index=r32(p+4);
delta=0;
if(s->started) {
delta=index-(uint32_t)s->next_index;
if(delta>=0x80000000U) { ++s->duplicates;return; }
s->missing+=delta;
} else s->next_index=index;
s->next_index+=delta;
append(s,s->next_index,r16(p+2),(uint8_t)(!s->started || delta!=0));
s->next_index+=1;s->started=1;++s->received;
}
static void frame_callback(const pcan_frame_t *frame,void *user) {
if(frame->flags & (PCAN_FLAG_DIR|PCAN_FLAG_ERR)) {
++((las_context *)user)->ignored;return;
}
las_can(user,frame->id,frame->data,frame->dlc,
frame->flags&PCAN_FLAG_IDE,frame->flags&PCAN_FLAG_RTR);
}
void las_uart(void *ctx,const uint8_t *data,size_t size) {
las_context *s=(las_context *)ctx;
if(data) pcan_parser_feed(&s->parser,data,size,frame_callback,s);
}
uint32_t las_get(const void *ctx,uint32_t field) {
const las_context *s=(const las_context *)ctx;
switch(field) {
case LAS_COUNT:return s->count;case LAS_PERIOD_NS:return s->period;
case LAS_SESSION:return s->session;case LAS_RECEIVED:return s->received;
case LAS_MISSING:return s->missing;case LAS_DUPLICATES:return s->duplicates;
case LAS_INVALID:return s->invalid;case LAS_IGNORED:return s->ignored;
case LAS_CRC_ERRORS:return s->parser.stats.crc_errors;
case LAS_HAS_META:return s->metadata;default:return 0;
}
}
size_t las_snapshot(const void *ctx,uint64_t *indices,uint16_t *samples,uint8_t *breaks,size_t cap) {
const las_context *s=(const las_context *)ctx;uint32_t i,pos;
if(!indices || !samples || !breaks || cap<s->count) return 0;
pos=(s->head+LAS_CAPACITY-s->count)%LAS_CAPACITY;
for(i=0;i<s->count;++i) {
indices[i]=s->indices[pos];samples[i]=s->samples[pos];breaks[i]=s->breaks[pos];
pos=(pos+1U)%LAS_CAPACITY;
}
return s->count;
}
size_t las_trace(const void *ctx,uint32_t channel,uint64_t *indices,uint8_t *levels,
uint8_t *moves,size_t cap) {
const las_context *s=(const las_context *)ctx;uint32_t i,pos;
uint64_t previous_x=0;uint8_t previous_y=0;size_t n=0;
if(channel>=16 || !indices || !levels || !moves || cap<2U*s->count) return 0;
pos=(s->head+LAS_CAPACITY-s->count)%LAS_CAPACITY;
for(i=0;i<s->count;++i) {
uint64_t x=s->indices[pos];uint8_t y=(uint8_t)((s->samples[pos]>>channel)&1U);
if(!i) { indices[n]=x;levels[n]=y;moves[n++]=1; }
else if(s->breaks[pos]) {
indices[n]=previous_x;levels[n]=previous_y;moves[n++]=0;
indices[n]=x;levels[n]=y;moves[n++]=1;
} else if(y!=previous_y) {
indices[n]=x;levels[n]=previous_y;moves[n++]=0;
indices[n]=x;levels[n]=y;moves[n++]=0;
}
previous_x=x;previous_y=y;pos=(pos+1U)%LAS_CAPACITY;
}
if(s->count>1) { indices[n]=previous_x;levels[n]=previous_y;moves[n++]=0; }
return n;
}
static size_t packet(uint32_t device,uint16_t body,const uint8_t *data,uint32_t uart,
uint32_t *raw,uint8_t *out,size_t cap) {
pcan_id_t id;pcan_frame_t frame;
if(device>15 || !raw || !out || cap<(uart?PCAN_FRAME_MAX:8U)) return 0;
memset(&id,0,sizeof(id));id.device_type=LAS_DEVICE_TYPE;id.device_id=(uint8_t)device;
id.priority=PCAN_PRIORITY_STANDARD;id.route=PCAN_ROUTE_FROM_DEVICE;
id.msg_type=PCAN_MSG_GAS;id.msg_body=body;
memset(&frame,0,sizeof(frame));frame.id=pcan_id_pack(&id);
frame.flags=PCAN_FLAG_IDE;frame.dlc=8;memcpy(frame.data,data,8);*raw=frame.id;
if(uart) return pcan_frame_encode(&frame,out,cap);
memcpy(out,data,8);return 8;
}
size_t las_metadata(uint32_t device,uint32_t session,uint32_t period,uint32_t uart,
uint32_t *id,uint8_t *out,size_t cap) {
uint8_t data[8];if(session>65535 || !period) return 0;
w16(data,LAS_TAG);w16(data+2,(uint16_t)session);w32(data+4,period);
return packet(device,LAS_GAS_META,data,uart,id,out,cap);
}
size_t las_data(uint32_t device,uint32_t session,uint32_t index,uint32_t sample,uint32_t uart,
uint32_t *id,uint8_t *out,size_t cap) {
uint8_t data[8];if(session>65535 || sample>65535) return 0;
w16(data,(uint16_t)session);w16(data+2,(uint16_t)sample);w32(data+4,index);
return packet(device,LAS_GAS_DATA,data,uart,id,out,cap);
}
void las_demo_step(void *ctx,uint32_t count) {
las_context *s=(las_context *)ctx;uint32_t i,bit,idx,raw;uint8_t packet_data[PCAN_FRAME_MAX];size_t n;
if(!s->metadata) {
n=las_metadata(s->device,1,1000000,1,&raw,packet_data,sizeof(packet_data));
las_uart(s,packet_data,n);
}
if(count>4096) count=4096;
for(i=0;i<count;++i) {
uint16_t a=0;idx=(uint32_t)s->next_index;
for(bit=0;bit<16;++bit) {
if((idx/(5U+bit*7U))&1U) a|=(uint16_t)(1U<<bit);
}
n=las_data(s->device,s->session,idx,a,1,&raw,packet_data,sizeof(packet_data));
las_uart(s,packet_data,n);
}
}

View File

@@ -0,0 +1,72 @@
#include "altera_logic.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
static void response(void *s, uint8_t *data, size_t n) {
size_t i; uint8_t sum=0;
for(i=0;i<n-1;++i) sum^=data[i]; data[n-1]=sum;
/* Serial reads can split at every byte. */
for(i=0;i<n;++i) la_feed(s,data+i,1);
}
static void connect_ok(void *s) {
uint8_t p[6], info[]={0x5a,0x81,0,16,0,16,50,1,0xe8};
const uint8_t request[]={0xa5,1,0,0,0,0xa4};
la_init(s); assert(la_next(s,p,5)==0);
assert(la_next(s,p,6)==6 && !memcmp(p,request,6));
assert(la_next(s,p,6)==0);
response(s,info,sizeof(info)); assert(la_get(s,LA_STATE)==LA_READY);
}
int main(void) {
void *s=calloc(1,la_context_size()); uint8_t p[6], ack[4], status[7], block[133];
uint16_t samples[4096]; unsigned int cmd, offset, i;
assert(s); connect_ok(s);
assert(la_start(s,65536,0,0,0,0)==LA_ARGUMENT);
assert(la_start(s,49,1,0,1,1)==LA_ARGUMENT);
assert(la_start(s,49,0,0,1,1)==0);
for(cmd=2;cmd<=7;++cmd) {
assert(la_next(s,p,6)==6 && p[1]==cmd);
if(cmd==2) { const uint8_t v[]={0xa5,2,49,0,0,0x96}; assert(!memcmp(p,v,6)); }
ack[0]=0x5a; ack[1]=(uint8_t)(cmd|0x80); ack[2]=0; response(s,ack,4);
}
assert(la_next(s,p,6)==0); la_tick(s,25);
assert(la_next(s,p,6)==6 && p[1]==8);
{ uint8_t waiting[]={0x5a,0x88,0,1,0,8,0}; response(s,waiting,7); }
la_tick(s,60000); /* Waiting for a physical trigger is not a UART timeout. */
assert(la_get(s,LA_STATE)==LA_CAPTURING);
assert(la_next(s,p,6)==6 && p[1]==8);
status[0]=0x5a; status[1]=0x88; status[2]=0; status[3]=6;
status[4]=0;status[5]=8;response(s,status,7);
for(offset=0;offset<4096;offset+=64) {
assert(la_next(s,p,6)==6 && p[1]==9 && p[4]==64);
assert((unsigned int)(p[2]|(p[3]<<8))==offset);
block[0]=0x5a;block[1]=0x89;block[2]=0;block[3]=64;
for(i=0;i<64;++i) { block[4+2*i]=(uint8_t)(offset+i);block[5+2*i]=(uint8_t)((offset+i)>>8); }
response(s,block,133);
}
assert(la_get(s,LA_STATE)==LA_DONE);
assert(la_samples(s,samples,4095)==0);
assert(la_samples(s,samples,4096)==4096);
for(i=0;i<4096;++i) assert(samples[i]==i);
assert(la_get(s,LA_TRIGGER_INDEX)==2048);
/* Corrupt XOR, wrong command, device error, timeout and extra data. */
la_init(s);la_next(s,p,6);
{ uint8_t bad[]={0x5a,0x81,0,16,0,16,50,1,0};la_feed(s,bad,9); }
assert(la_get(s,LA_ERROR_CODE)==LA_CHECKSUM);
la_init(s);la_next(s,p,6);
{ uint8_t bad[]={0x5a,0x88};la_feed(s,bad,2); }
assert(la_get(s,LA_ERROR_CODE)==LA_BAD_FRAME);
la_init(s);la_next(s,p,6);
{ uint8_t bad[]={0x5a,0x81,1,0};response(s,bad,4); }
assert(la_get(s,LA_ERROR_CODE)==LA_DEVICE_ERROR);
la_init(s);la_next(s,p,6);la_tick(s,999);
assert(la_get(s,LA_STATE)==LA_CONNECTING);la_tick(s,1);
assert(la_get(s,LA_ERROR_CODE)==LA_TIMEOUT && la_next(s,p,6)==0);
connect_ok(s);la_feed(s,p,1);assert(la_get(s,LA_ERROR_CODE)==LA_BAD_FRAME);
la_init(s);la_next(s,p,6);
{ uint8_t bad[]={0x5a,0x81,0,8,0,16,50,1,0};response(s,bad,9); }
assert(la_get(s,LA_ERROR_CODE)==LA_UNSUPPORTED);
la_demo_init(s);assert(la_demo_capture(s,49)==0);
assert(la_get(s,LA_STATE)==LA_DONE && la_samples(s,samples,4096)==4096);
free(s);puts("Altera Logic C tests passed");return 0;
}

View File

@@ -0,0 +1,62 @@
#include "altera_stream.h"
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
static void meta(void *s,uint32_t session,int uart) {
uint8_t data[32];uint32_t id;size_t n,i;
n=las_metadata(LAS_DEVICE_ID,session,1000000,(uint32_t)uart,&id,data,sizeof(data));assert(n);
assert(id==0x1EE3FE00U);
if(uart) for(i=0;i<n;++i) las_uart(s,data+i,1);
else las_can(s,id,data,n,1,0);
}
static void sample(void *s,uint32_t session,uint32_t index,int uart) {
uint8_t data[32];uint32_t id;size_t n;
n=las_data(LAS_DEVICE_ID,session,index,index&65535U,(uint32_t)uart,&id,data,sizeof(data));assert(n);
assert(id==0x1EE3FF00U);
if(uart) las_uart(s,data,n);else las_can(s,id,data,n,1,0);
}
int main(void) {
void *can=malloc(las_context_size()),*uart=malloc(las_context_size());
uint64_t *indices=malloc(LAS_CAPACITY*sizeof(uint64_t));
uint16_t *samples=malloc(LAS_CAPACITY*sizeof(uint16_t));
uint8_t *breaks=malloc(LAS_CAPACITY);uint8_t packet[32];uint32_t id;size_t n,i;
assert(can&&uart&&indices&&samples&&breaks);
assert(las_init(can,14)&&las_init(uart,14));assert(!las_init(can,16));
sample(can,1,0,0);assert(las_get(can,LAS_COUNT)==0);
meta(can,1,0);meta(uart,1,1);
/* Independent golden SETCAN bytes. */
n=las_data(14,1,0x12345678U,0x8001,0,&id,packet,sizeof(packet));
{ const uint8_t v[]={1,0,1,0x80,0x78,0x56,0x34,0x12};assert(n==8&&!memcmp(v,packet,8)); }
for(i=0;i<9000;++i) { sample(can,1,(uint32_t)i,0);sample(uart,1,(uint32_t)i,1); }
assert(las_get(can,LAS_COUNT)==8192&&las_get(uart,LAS_COUNT)==8192);
assert(las_get(can,LAS_RECEIVED)==9000&&las_get(uart,LAS_RECEIVED)==9000);
assert(las_snapshot(can,indices,samples,breaks,LAS_CAPACITY)==8192);
assert(indices[0]==808&&indices[8191]==8999&&samples[8191]==8999);
assert(las_snapshot(uart,indices,samples,breaks,LAS_CAPACITY)==8192);
assert(indices[0]==808&&indices[8191]==8999&&samples[8191]==8999);
sample(can,1,8999,1);assert(las_get(can,LAS_DUPLICATES)==1); /* second transport duplicate */
sample(can,1,9003,0);assert(las_get(can,LAS_MISSING)==3);
las_snapshot(can,indices,samples,breaks,LAS_CAPACITY);assert(breaks[8191]);
/* Repeated metadata must not erase rolling history. */
meta(can,1,1);assert(las_get(can,LAS_COUNT)==8192);
meta(can,2,0);assert(las_get(can,LAS_COUNT)==0);
sample(can,1,9004,1);assert(las_get(can,LAS_COUNT)==0);
meta(can,1,1);assert(las_get(can,LAS_SESSION)==2);
sample(can,2,0xffffffffU,0);sample(can,2,0,1);
las_snapshot(can,indices,samples,breaks,LAS_CAPACITY);
assert(indices[0]==0xffffffffULL&&indices[1]==0x100000000ULL&&!breaks[1]);
/* Reject wrong address, standard ID, RTR and short payload. */
n=las_data(14,2,1,0,0,&id,packet,sizeof(packet));assert(n==8);
las_can(can,id,packet,8,0,0);las_can(can,id,packet,8,1,1);
las_can(can,id^(1U<<20),packet,8,1,0);las_can(can,id,packet,7,1,0);
assert(las_get(can,LAS_COUNT)==2&&las_get(can,LAS_INVALID)==1);
n=las_data(14,2,1,1,1,&id,packet,sizeof(packet));packet[n-1]^=0x80;
las_uart(can,packet,n);assert(las_get(can,LAS_CRC_ERRORS)==1);
sample(can,2,1,1);assert(las_get(can,LAS_COUNT)==3); /* recovery after CRC */
assert(!las_metadata(14,65536,1000,0,&id,packet,sizeof(packet)));
assert(!las_metadata(14,1,0,0,&id,packet,sizeof(packet)));
assert(!las_data(14,1,0,65536,0,&id,packet,sizeof(packet)));
free(can);free(uart);free(indices);free(samples);free(breaks);
puts("Altera SETCAN/CAN/UART stream tests passed");return 0;
}

View File

@@ -20,6 +20,8 @@ INCLUDE = ROOT / "include"
JNI_INCLUDES: list[Path] = [] JNI_INCLUDES: list[Path] = []
SOURCES = [ SOURCES = [
ROOT / "src" / name for name in ( ROOT / "src" / name for name in (
"altera_logic.c",
"altera_stream.c",
"set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c", "set_plot.c", "set_trends.c", "set_spectrum.c", "set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c", "set_plot.c", "set_trends.c", "set_spectrum.c",
"balsam_can.c", "set_crc.c", "periph28335.c", "tms2812.c", "gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c", "balsam_can.c", "set_crc.c", "periph28335.c", "tms2812.c", "gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c",
"pcan_frame.c", "pcan_id.c", "pcan_link.c", "pcan_ring.c", "pcan_frame.c", "pcan_id.c", "pcan_link.c", "pcan_ring.c",

View File

@@ -0,0 +1,189 @@
# Altera Logic — ONLINE через SETCAN / GAS / UART
Версия профиля: **1**. Реализация, порты и тесты принадлежат `templates`.
Это профиль существующего classic SETCAN/ProtoCAN с 29-битным ID;
он не подменяется кадром SETP v2 и не использует старые команды `A5 CMD` FPGA.
## Адрес и носители
| Поле | Значение |
|---|---|
| Priority | 1, стандартный |
| Route | 1, от устройства |
| DeviceType | `0x6` |
| DeviceID | `0xE` |
| Device Name | `Altera Logic` |
| MsgType | `0x3`, GAS |
| CAN | Extended ID, data frame, DLC=8 |
CAN доставляет ID + 8 байт непосредственно. UART доставляет ровно тот же ID
и те же данные в существующем контейнере `pcan_frame`:
```text
AA 55 | LEN | SEQ | FLAGS | CAN_ID u32 LE | DATA | CRC16 u16 LE
```
Для этого профиля LEN=14, FLAGS=1 (IDE), DIR/RTR/ERR сброшены,
итоговая длина 19 байтов. CRC-16/CCITT-FALSE считается существующим кодеком
по LEN…DATA. SEQ — транспортный 8-битный счётчик; порядок выборок определяется
полем SampleIndex, поэтому независимые CAN/UART потоки дают одинаковый результат.
Предоставленные простые C-билдеры устанавливают SEQ=0; устройство может вести
его самостоятельно, используя `pcan_frame_encode`.
Настройки прямого UART-порта GUI: **921600, 8N1, без flow control**.
## GAS: метаданные потока
**CAN ID `0x1EE3FE00`, MsgBody/GAS address `0xFE00`, DLC=8.**
| Регистры GAS | Байты DATA | Поле |
|---|---|---|
| FE00 | 0…1 | Tag/version = `0x4C01` |
| FE01 | 2…3 | SessionID u16 |
| FE02…FE03 | 4…7 | SamplePeriodNs u32, строго больше 0 |
Порядок байтов little-endian. Число каналов профиля фиксировано: 16.
Частота выборок = `1e9 / SamplePeriodNs`.
Пример сессии 1 с частотой 1000 выборок/с:
```text
ID: 1EE3FE00
DATA: 01 4C 01 00 40 42 0F 00
```
Источник отправляет метаданные перед первой выборкой и повторяет примерно
раз в секунду, чтобы подключившийся GUI мог присоединиться к потоку.
Повтор с теми же SessionID/периодом историю не очищает.
При перезапуске потока или изменении периода источник увеличивает SessionID
по модулю 65536. Новый SessionID очищает историю; задержанные метаданные
предыдущей сессии игнорируются. Разница менее 32768 считается движением вперёд.
Если после аппаратного сброса устройство начало счётчик с меньшего значения,
нужно остановить и снова включить приём в GUI (либо сохранять счётчик сессий
на устройстве). Без полученных метаданных отсчёты не отображаются.
## GAS: одна выборка всех 16 каналов
**CAN ID `0x1EE3FF00`, MsgBody/GAS address `0xFF00`, DLC=8.**
| Регистры GAS | Байты DATA | Поле |
|---|---|---|
| FF00 | 0…1 | SessionID u16, должен совпадать с метаданными |
| FF01 | 2…3 | Sample u16: бит n = уровень канала Dn |
| FF02…FF03 | 4…7 | SampleIndex u32 |
Пример: сессия 1, выборка 0, D0 и D15 равны 1:
```text
ID: 1EE3FF00
DATA: 01 00 01 80 00 00 00 00
```
SampleIndex увеличивается на каждой аппаратной выборке, в том числе если
пакет не удалось передать. GUI замечает скачок индекса, учитывает потерянные
выборки и рисует разрыв, не соединяя его выдуманными состояниями.
Время получается из индекса и периода устройства, а не из времени прихода UART.
Переполнение индекса u32 разворачивается в u64 на приёмнике. Однозначное
восстановление порядка требует разрыва меньше 2^31 выборок. При более долгом
перерыве следует начать новую сессию. При подключении после полного оборота
индекса GUI не знает число оборотов до первой принятой выборки.
Одновременный CAN+UART приём объединяет один поток: одинаковые или запоздалые
индексы в одной сессии не добавляются повторно. Их число показывается как
«дубли/старые». Поздние данные старой сессии игнорируются по SessionID.
Если два источника прислали разные значения с одинаковыми SessionID/индексом,
остаётся первый принятый пакет: это не механизм голосования или сверки каналов.
## Нагрузка и история
Профиль передаёт одну 16-битную выборку на CAN-кадр. Это осознанный обмен
пропускной способности на независимую проверку сессии, потерь и дублей.
Начальный профиль для устройства — 1000 выборок/с.
На UART при 921600 бод и 19 байтах × 10 бит на пакет теоретический потолок
составляет около 4850 выборок/с, без учёта метаданных и пауз. Для непрерывной
передачи 50 Мвыб/с этот транспорт не подходит. Высокоскоростной буферный
захват остаётся отдельным режимом.
Общая C-история ограничена 8192 последними принятыми выборками. Старые данные
вытесняются; полный длительный архив в этом режиме не ведётся. GUI обновляет
экран не чаще 20 раз в секунду. Пауза фиксирует показанное окно, но приём
продолжается. CSV сохраняет текущее окно с индексами, временем и признаками разрывов.
## API для прошивки / порта
### Маркеры в ONLINE GUI
Колесо внутри графика прокручивает его по X (влево/вправо), Ctrl + колесо —
по Y (вверх/вниз), не меняя масштаб. Когда вся ось помещается в окно,
прокрутка этой оси не требуется. После ручной прокрутки X автоматический
переход к концу потока отключается до сброса масштаба.
Qt-порт этой обработки: `python/altera_logic/plot_scroll.py`.
Масштабирование внутри графика: зажать ЛКМ и двигать вправо/влево для X;
Ctrl + ЛКМ и движение вверх/вниз — для Y. Начальная точка остаётся на месте
в видимой области. Масштаб X: 1…16, Y: 1…8. «Сброс масштаба» возвращает
обе оси к 1. ЛКМ за линию маркера перемещает маркер; Ctrl отдаёт приоритет
масштабу Y. Обычный клик устанавливает выбранный маркер, если он включён.
При увеличении Y растягиваются дорожки цифровых каналов, не изменяются
измеренные значения. Прокрутка позволяет просматривать увеличенную область.
Две пары X1/X2 и X3/X4 включаются независимо и показывают время каждого
маркера, знаковый интервал Δt = X2 X1 (либо X4 X3) и f = 1/|Δt|.
При совпадении маркеров частота отображается как «—». Это обратный интервал,
а не автоматическое определение частоты сигнала.
Две пары Y1/Y2 и Y3/Y4 включаются независимо, показывают оба положения и
знаковую ΔY в процентах высоты графика (это не измерение напряжения).
Панель измерений построена по примеру AndroidGUI: отдельная строка для каждой
пары и ползунок выбранного маркера. dB для цифровых дорожек не вычисляются.
Разности вычисляет общий C-код `set_plot.c` через `protocan.plot.PlotMath`,
тот же код используется Android-портом.
Линии можно перетаскивать мышью; список «Установить кликом» выбирает маркер
для установки в любой точке. Режим «Курсор» возвращает обычный просмотр выборок.
Снятие галочки скрывает линии, сохраняя их позиции. «Маркеры в окно» возвращает
их в текущую область. Для измерения неподвижного фрагмента включите паузу.
X сохраняет положение во времени при обновлении истории и может уйти за окно;
новая сессия устанавливает начальные позиции. Визуальная модель находится
в `python/altera_logic/markers.py`, обработка мыши и рисование — в SETGUI.
`c/set-protocol/include/altera_stream.h` экспортирует:
- `las_metadata` — общий билдер метаданных для CAN либо UART;
- `las_data` — общий билдер выборки для CAN либо UART;
- `las_can` / `las_uart` — два входа одного декодера и истории;
- `las_snapshot` — хронологический снимок с индексами и разрывами;
- `las_get` — диагностика: потери, дубли, неправильные кадры, CRC.
Пример формирования одной выборки для CAN, без HAL-зависимостей:
```c
#include "altera_stream.h"
uint8_t bytes[32];
uint32_t id;
size_t size;
/* SessionID=1, период=1 мс. CAN-драйверу передаются id и size байт. */
size = las_metadata(LAS_DEVICE_ID, 1, 1000000, 0, &id, bytes, sizeof(bytes));
/* board_can_send_extended(id, bytes, size); */
size = las_data(LAS_DEVICE_ID, 1, 0, 0x8001, 0, &id, bytes, sizeof(bytes));
/* board_can_send_extended(id, bytes, size); */
/* Для UART последний флаг носителя заменить с 0 на 1:
билдер вернёт полный пакет AA55, который передаётся в UART без изменений. */
```
Драйвер платы должен передавать каждый пакет полностью и сохранять порядок,
периодически публиковать метаданные и увеличивать SampleIndex даже при потере
пакета в своей очереди. CAN-контроллер и трансивер остаются аппаратным портом.
## Размещение реализации и границы
- `templates/c/set-protocol/src/altera_stream.c`: протокол и кольцевая история.
- `templates/python/altera_logic/stream.py`: тонкий ctypes-порт и CSV-модель.
- `templates/python/altera_logic/stream_port.py`: Qt UART/CAN-приём и жизненный цикл.
- SETGUI: только визуальная вкладка и подключение к существующему CAN-источнику.
Текущий FPGA RTL с протоколом `A5 CMD` не становится SETCAN-источником от
обновления GUI. Нужен передатчик этого профиля в прошивке/RTL либо внешний
мост, который получает реальные выборки и публикует их в описанном формате.
В этой версии реализованы GUI-приёмник, общие C-билдеры для устройства и
демопроизводитель; FPGA RTL и физический CAN-контроллер не изменены.

View File

@@ -0,0 +1,21 @@
# Реестр адресов SETCAN
Дополнения к исходной таблице `Протокол CAN и ОАП.xlsx` фиксируются здесь.
Числовая раскладка CAN ID не меняется: DeviceType — 3 бита, DeviceID — 4 бита.
Device Name — отображаемое имя адреса, а не дополнительное поле CAN ID.
| DeviceType | DeviceID | Device Name | Назначение |
|---|---|---|---|
| `0x0` | существующие | Верхний уровень | Существующие адреса исходной таблицы |
| `0x6` | `0xE` | **Altera Logic** | Онлайн-анализатор 16 цифровых каналов |
| `0x7` | `0xD` | configurator | Существующий конфигуратор |
| `0x7` | `0xF` | KONOR / SETTINGS | Существующий профиль SETCAN и привязка датчиков |
Для Altera Logic выбран предпоследний тип `0x6` и предпоследний экземпляр `0xE`:
эта пара не была назначена в проверенных локальных таблицах и исходниках.
Тип `0x7` уже используется. Наличие другого физического узла с тем же адресом
на конкретной шине нужно исключить при вводе в эксплуатацию.
Числовые константы устройства: `c/set-protocol/include/altera_stream.h`.
Отображаемые имена: `python/protocan/protocan.py`.
Контракт потока: [ALTERA_LOGIC_STREAM.md](ALTERA_LOGIC_STREAM.md).

View File

@@ -32,3 +32,7 @@
Для пересборки HTML запустите `doc/setcan/build-html.bat` из корня Для пересборки HTML запустите `doc/setcan/build-html.bat` из корня
репозитория `templates`. репозитория `templates`.
# Расширение Altera Logic
- [Реестр устройств SETCAN](DEVICE_REGISTRY.md).
- [ONLINE-поток Altera Logic через GAS и UART](ALTERA_LOGIC_STREAM.md).

47
python/README.md Normal file
View File

@@ -0,0 +1,47 @@
# Общие Python-библиотеки SET
Исходники для всех приложений редактируются в репозитории `templates`.
Потребитель подключает конкретный коммит как Git submodule либо устанавливает
пакет из `templates/python`: `pip install -e ./python`.
| Пакет | Назначение |
|---|---|
| `logic_analyzer.files` | CSV/SAL/DSL, цифровые каналы и массивы фронтов |
| `logic_analyzer.analysis` | UART/CAN, измерения, интервалы и события |
| `logic_analyzer.decoders` | SET UART/CAN, PM35, 1SP0635/1SD536F2 |
| `logic_analyzer.saleae` | Настройки, обнаружение и захват Saleae Logic 2 |
| `altera_logic` | C FFI Altera, SETCAN stream, модели и Qt-порты |
| `set_devices` | Кодеки, каталоги, прошивки, EEPROM/DS18B20, TMS, UMP, CAN485, спектр, демо-модели |
| `set_devices.qt_ports` | UART/mock, SLCAN, Candle/WinUSB, STM/TMS boot, STM settings, UMP CAN |
| `protocan`, `setprotocol` | Существующие обёртки общего C99-ядра и SET v2 |
Импорт `logic_analyzer` / `set_devices` не загружает Qt и не зависит от GUI.
Qt подключается только при импорте конкретного `qt_ports` (PySide6 или PySide2).
Saleae SDK подключается при обращении к устройству: `pip install -e './python[saleae]'`.
Qt-виджеты, пользовательские настройки и управление сессией находятся в приложении.
Пути к нативным библиотекам задаёт потребитель перед импортом:
`SETPROTOCOL_LIBRARY`, при необходимости `ALTERA_LOGIC_LIBRARY`, `CANDLE_LIBRARY`.
Порты не ищут DLL в каталогах SETGUI. C99-ядро собирается из `c/set-protocol`,
Candle/WinUSB — из `c/candle`. Библиотеки не выполняют поиск соседних репозиториев.
Пример без GUI:
```python
from logic_analyzer.files import read_capture
from logic_analyzer.analysis import analyze_capture
capture = read_capture('digital.csv')
result = analyze_capture(capture, dict(mode='UART', channel=0, baudrate=115200))
for event in result.events:
print(event.start, event.text)
```
Тесты логанализаторов без Qt и Saleae:
`python -m unittest discover -s python/tests -p 'test_logic_analyzer_*.py'`.
Добавьте `templates/python` в `PYTHONPATH` либо установите пакет.
`test_shared_library_boundary.py` проверяет отсутствие зависимости от приложения.
Декодеры перенесены из SETGUI; исходное происхождение:
`DSLogic_Logic_2/dslogic_script/dsview_decoders/{gate_driver_timing,set_uart,set_can,pm35_uart}`.
После переноса единственное место сопровождения этих ядер — `templates/python/logic_analyzer`.

1
python/altera_logic/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
native/

View File

@@ -0,0 +1,4 @@
"""Portable Altera analyzer client. Wire protocol and sequencing live in C99."""
from .native import Capture, NativeAnalyzer
__all__ = ["Capture", "NativeAnalyzer"]

View File

@@ -0,0 +1,89 @@
"""Presentation state for waveform rulers; no transport or wire processing."""
from dataclasses import dataclass
def drag_zoom_factor(delta_pixels):
"""Visual gesture mapping: 200 pixels doubles the selected axis."""
return 2 ** (max(-1000., min(1000., delta_pixels)) / 200.)
@dataclass
class Marker:
name: str
axis: str
value: float
color: str
visible: bool = False
class WaveformMarkers:
def __init__(self):
self.items = [Marker('X1', 'x', .2, '#ffce63'),
Marker('X2', 'x', .4, '#ffce63'),
Marker('X3', 'x', .6, '#e794ff'),
Marker('X4', 'x', .8, '#e794ff'),
Marker('Y1', 'y', .2, '#72e7ff'),
Marker('Y2', 'y', .4, '#72e7ff'),
Marker('Y3', 'y', .6, '#a9df79'),
Marker('Y4', 'y', .8, '#a9df79')]
self._math = None
self.session = None
self.bounds = None
def window(self, session, start, end):
if session != self.session or self.bounds is None:
for marker, fraction in zip(self.items[:4], (.2, .4, .6, .8)):
marker.value = start + (end-start)*fraction
self.session, self.bounds = session, (start, end)
def reset(self):
if self.bounds:
start, end = self.bounds
for marker, fraction in zip(self.items[:4], (.2, .4, .6, .8)):
marker.value = start + (end-start)*fraction
for marker, fraction in zip(self.items[4:], (.2, .4, .6, .8)):
marker.value = fraction
def measurements(self, period_ns):
"""Use the same native delta operation as Android's plotDelta."""
if self._math is None:
from protocan.plot import PlotMath
from .native import NativeAnalyzer
self._math = PlotMath(NativeAnalyzer().lib)
result = []
for index in range(0, 8, 2):
a, b = self.items[index:index+2]
if not (a.visible and b.visible):
continue
if a.axis == 'x':
if self.bounds is None:
continue
factor = period_ns / 1e9
av, bv = a.value*factor, b.value*factor
delta = self._math.delta(a.value, b.value, factor)
frequency = 1/abs(delta) if delta else None
else:
av, bv = 100*(1-a.value), 100*(1-b.value)
delta = self._math.delta(av, bv)
frequency = None
result.append((a, b, av, bv, delta, frequency))
return result
def place(self, index, fraction):
marker = self.items[index]
fraction = min(1., max(0., fraction))
if marker.axis == 'x':
if self.bounds is None:
return
start, end = self.bounds
marker.value = start + (end-start)*fraction
else:
marker.value = fraction
def fraction(self, marker):
if marker.axis == 'y':
return marker.value
if self.bounds is None:
return 0.
start, end = self.bounds
return (marker.value-start)/max(1, end-start)

View File

@@ -0,0 +1,131 @@
"""ctypes binding only: no Python packet codec or protocol fallback."""
from __future__ import annotations
import csv
import ctypes as C
import os
import sys
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class Capture:
samples: tuple[int, ...]
trigger_index: int
sample_rate: float
demo: bool = False
def save_csv(self, path):
with open(path, "w", newline="", encoding="utf-8") as stream:
writer = csv.writer(stream)
writer.writerow(["index", "time_s", "sample_hex", "demo"] +
["D%d" % n for n in range(16)])
for i, sample in enumerate(self.samples):
writer.writerow([i, (i-self.trigger_index)/self.sample_rate,
"%04X" % sample, int(self.demo)] +
[(sample >> n) & 1 for n in range(16)])
class NativeAnalyzer:
CONNECTING, READY, CAPTURING, DONE, ERROR = range(5)
ERROR_TEXT = {
1: "Неверный формат ответа FPGA",
2: "Ошибка контрольной суммы ответа",
3: "FPGA отклонила команду",
4: "Тайм-аут UART. При потере синхронизации нужен RESET_N и повторная настройка",
5: "Неподдерживаемая конфигурация FPGA: ожидается 16 каналов, 4096 выборок, версия 1",
6: "Недопустимые или противоречивые настройки триггера",
7: "Ошибка последовательного порта",
}
def __init__(self, library=None):
if library is None:
explicit = os.environ.get("ALTERA_LOGIC_LIBRARY") or os.environ.get("SETPROTOCOL_LIBRARY")
name = "setprotocol.dll" if sys.platform == "win32" else "libsetprotocol.so"
bundled = Path(getattr(sys, "_MEIPASS", "")) / "native" / name
local = Path(__file__).resolve().parent / "native" / name
path = Path(explicit) if explicit else (bundled if bundled.is_file() else local)
try:
library = C.CDLL(str(path))
except OSError as exc:
raise RuntimeError("Не загружено C-ядро Altera Logic. Соберите templates/"
"c/set-protocol/tools/build_host.py; " + str(exc)) from exc
self.lib = library
signatures = {
"la_context_size": ([], C.c_size_t),
"la_init": ([C.c_void_p], None),
"la_start": ([C.c_void_p] + [C.c_uint32]*5, C.c_int),
"la_next": ([C.c_void_p, C.c_void_p, C.c_size_t], C.c_size_t),
"la_feed": ([C.c_void_p, C.c_void_p, C.c_size_t], None),
"la_tick": ([C.c_void_p, C.c_uint32], None),
"la_fail": ([C.c_void_p, C.c_uint32], None),
"la_get": ([C.c_void_p, C.c_uint32], C.c_uint32),
"la_samples": ([C.c_void_p, C.c_void_p, C.c_size_t], C.c_size_t),
"la_demo_init": ([C.c_void_p], None),
"la_demo_capture": ([C.c_void_p, C.c_uint32], C.c_int),
}
try:
for name, (args, result) in signatures.items():
fn = getattr(self.lib, name)
fn.argtypes, fn.restype = args, result
except AttributeError as exc:
raise RuntimeError("C-ядро устарело: пересоберите templates с Altera Logic") from exc
# Explicitly aligned caller-owned storage; C never allocates memory.
self.ctx = (C.c_uint64 * ((self.lib.la_context_size()+7)//8))()
self.demo = False
self.reset()
def reset(self, demo=False):
self.demo = demo
(self.lib.la_demo_init if demo else self.lib.la_init)(self.ctx)
def get(self, field):
return int(self.lib.la_get(self.ctx, field))
@property
def state(self):
return self.get(0)
@property
def error(self):
return self.ERROR_TEXT.get(self.get(1), "Ошибка обмена")
@property
def progress(self):
return self.get(7)
@property
def flags(self):
return self.get(5)
def start(self, divider, mask=0, value=0, edge_mask=0, edge_value=0):
values = (divider, mask, value, edge_mask, edge_value)
if any(not isinstance(v, int) or not 0 <= v <= 65535 for v in values):
raise ValueError(self.ERROR_TEXT[6])
result = (self.lib.la_demo_capture(self.ctx, divider) if self.demo else
self.lib.la_start(self.ctx, *values))
if result:
raise ValueError(self.ERROR_TEXT.get(result, "Захват уже выполняется"))
def next_request(self):
out = (C.c_ubyte*6)()
size = self.lib.la_next(self.ctx, out, len(out))
return bytes(out[:size])
def feed(self, data):
self.lib.la_feed(self.ctx, data, len(data))
def tick(self, elapsed_ms):
self.lib.la_tick(self.ctx, max(0, min(int(elapsed_ms), 0xffffffff)))
def fail_io(self):
self.lib.la_fail(self.ctx, 7)
def capture(self):
out = (C.c_uint16*4096)()
count = self.lib.la_samples(self.ctx, out, len(out))
if not count:
raise RuntimeError("Запись ещё не завершена")
return Capture(tuple(out[:count]), self.get(6),
self.get(4)/(self.get(8)+1), self.demo)

View File

@@ -0,0 +1,27 @@
"""Qt presentation port: wheel pans X; Ctrl+wheel pans Y, never zooms."""
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QScrollArea
class PlotScrollArea(QScrollArea):
user_scrolled = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self._remainder = {'x': 0., 'y': 0.}
def wheelEvent(self, event):
axis = 'y' if event.modifiers() & Qt.KeyboardModifier.ControlModifier else 'x'
bar = self.verticalScrollBar() if axis == 'y' else self.horizontalScrollBar()
pixels, angle = event.pixelDelta(), event.angleDelta()
if not pixels.isNull():
delta = pixels.y() if pixels.y() else pixels.x()
else:
delta = (angle.y() if angle.y() else angle.x()) / 120. * 60.
self._remainder[axis] -= delta
movement = int(self._remainder[axis])
self._remainder[axis] -= movement
if delta:
self.user_scrolled.emit(axis)
bar.setValue(bar.value()+movement)
event.accept()

View File

@@ -0,0 +1,114 @@
"""Qt serial/lifecycle port. Protocol decisions are exclusively in C99."""
from __future__ import annotations
from set_devices.qt_ports.qt_compat import QObject, QTimer, QElapsedTimer, Signal
from set_devices.qt_ports.qt_compat import QSerialPort, QSerialPortInfo
from .native import NativeAnalyzer
class AnalyzerPort(QObject):
changed = Signal()
completed = Signal(object)
error = Signal(str)
log = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.core = None
self.active = False
self._last_done = False
self.serial = QSerialPort(self)
self.serial.readyRead.connect(self._read)
self.serial.errorOccurred.connect(self._serial_error)
self.timer = QTimer(self)
self.timer.setInterval(10)
self.timer.timeout.connect(self._tick)
self.clock = QElapsedTimer()
@staticmethod
def ports():
return [(p.portName(), p.description()) for p in QSerialPortInfo.availablePorts()]
def open(self, name, demo=False):
self.close()
try:
self.core = NativeAnalyzer()
self.core.reset(demo)
except RuntimeError as exc:
self.error.emit(str(exc))
return
if not demo:
self.serial.setPortName(name)
self.serial.setBaudRate(921600)
self.serial.setDataBits(QSerialPort.DataBits.Data8)
self.serial.setParity(QSerialPort.Parity.NoParity)
self.serial.setStopBits(QSerialPort.StopBits.OneStop)
self.serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl)
if not self.serial.open(QSerialPort.OpenModeFlag.ReadWrite):
self.error.emit(self.serial.errorString())
return
self.active = True
self._last_done = False
self.clock.start()
self.timer.start()
self.log.emit("ДЕМО: синтетические данные, триггер не моделируется" if demo
else "%s · 921600 8N1" % name)
self._pump()
def close(self):
self.active = False
self.timer.stop()
self.serial.close()
self.changed.emit()
def start(self, *settings):
if not self.active or not self.core:
return
try:
self.core.start(*settings)
except ValueError as exc:
self.error.emit(str(exc))
return
self._last_done = False
self._pump()
def _tick(self):
if not self.active:
return
self.core.tick(self.clock.restart())
self._pump()
def _read(self):
data = bytes(self.serial.readAll())
if self.active and data:
self.log.emit("RX " + data.hex(" ").upper())
self.core.feed(data)
self._pump()
def _pump(self):
if not self.active:
return
if self.core.state == NativeAnalyzer.ERROR:
message = self.core.error
self.close()
self.error.emit(message)
return
packet = self.core.next_request()
if packet:
self.log.emit("TX " + packet.hex(" ").upper())
if self.serial.write(packet) != len(packet):
self.core.fail_io()
self._pump()
return
self.clock.restart()
if self.core.state == NativeAnalyzer.DONE and not self._last_done:
self._last_done = True
self.completed.emit(self.core.capture())
self.changed.emit()
def _serial_error(self, code):
if self.active and code != QSerialPort.SerialPortError.NoError:
message = self.serial.errorString()
self.core.fail_io()
self.close()
self.error.emit(message)

View File

@@ -0,0 +1,108 @@
"""SETCAN streaming FFI and immutable display/export models. No wire codec."""
from __future__ import annotations
import csv
import ctypes as C
from dataclasses import dataclass
from .native import NativeAnalyzer
@dataclass(frozen=True)
class StreamSnapshot:
indices: tuple[int, ...]
samples: tuple[int, ...]
breaks: tuple[int, ...]
period_ns: int
session: int
demo: bool = False
traces: tuple = ()
def save_csv(self, path):
with open(path, "w", newline="", encoding="utf-8") as file:
writer = csv.writer(file)
writer.writerow(["session", "sample_index", "time_s", "gap_before", "sample_hex", "demo"] +
["D%d" % n for n in range(16)])
for index, sample, gap in zip(self.indices, self.samples, self.breaks):
writer.writerow([self.session, index, index*self.period_ns/1e9, gap,
"%04X" % sample, int(self.demo)] +
[(sample >> bit)&1 for bit in range(16)])
class NativeStream:
def __init__(self, device_id=None):
self._owner = NativeAnalyzer()
self.lib = self._owner.lib
signatures = {
"las_context_size": ([], C.c_size_t),
"las_init": ([C.c_void_p, C.c_uint32], C.c_int),
"las_can": ([C.c_void_p, C.c_uint32, C.c_void_p, C.c_size_t, C.c_uint32, C.c_uint32], None),
"las_uart": ([C.c_void_p, C.c_void_p, C.c_size_t], None),
"las_get": ([C.c_void_p, C.c_uint32], C.c_uint32),
"las_snapshot": ([C.c_void_p, C.c_void_p, C.c_void_p, C.c_void_p, C.c_size_t], C.c_size_t),
"las_trace": ([C.c_void_p, C.c_uint32, C.c_void_p, C.c_void_p, C.c_void_p, C.c_size_t], C.c_size_t),
"las_device_type": ([], C.c_uint32),
"las_device_id": ([], C.c_uint32),
"las_device_name": ([], C.c_char_p),
"las_demo_step": ([C.c_void_p, C.c_uint32], None),
"las_metadata": ([C.c_uint32]*4+[C.POINTER(C.c_uint32), C.c_void_p, C.c_size_t], C.c_size_t),
"las_data": ([C.c_uint32]*5+[C.POINTER(C.c_uint32), C.c_void_p, C.c_size_t], C.c_size_t),
}
try:
for name, (args, result) in signatures.items():
fn = getattr(self.lib, name)
fn.argtypes, fn.restype = args, result
except AttributeError as exc:
raise RuntimeError("Обновите DLL templates: отсутствует SETCAN Altera stream") from exc
self.device_type = int(self.lib.las_device_type())
self.device_id = int(self.lib.las_device_id()) if device_id is None else device_id
self.device_name = self.lib.las_device_name().decode("utf-8")
self.ctx = (C.c_uint64*((self.lib.las_context_size()+7)//8))()
if not 0 <= self.device_id <= 15 or not self.lib.las_init(self.ctx, self.device_id):
raise ValueError("DeviceID должен быть 0…15")
@property
def stats(self):
fields = ("count", "period_ns", "session", "received", "missing", "duplicates",
"invalid", "ignored", "crc_errors", "has_meta")
return {name: int(self.lib.las_get(self.ctx, i)) for i, name in enumerate(fields)}
def feed_can(self, identifier, data, extended=True, remote=False):
if not 0 <= identifier <= 0xffffffff:
return
self.lib.las_can(self.ctx, identifier, data, len(data), int(extended), int(remote))
def feed_uart(self, data):
self.lib.las_uart(self.ctx, data, len(data))
def snapshot(self, demo=False):
stats = self.stats
count = stats["count"]
indices, samples, breaks = (C.c_uint64*count)(), (C.c_uint16*count)(), (C.c_ubyte*count)()
n = self.lib.las_snapshot(self.ctx, indices, samples, breaks, count)
traces = []
xs, ys, moves = (C.c_uint64*(2*count))(), (C.c_ubyte*(2*count))(), (C.c_ubyte*(2*count))()
for channel in range(16):
size = self.lib.las_trace(self.ctx, channel, xs, ys, moves, 2*count)
traces.append(tuple(zip(xs[:size], ys[:size], moves[:size])))
return StreamSnapshot(tuple(indices[:n]), tuple(samples[:n]), tuple(breaks[:n]),
stats["period_ns"], stats["session"], demo, tuple(traces))
def demo_step(self, count=50):
self.lib.las_demo_step(self.ctx, count)
def metadata_packet(self, session, period_ns, uart=False):
if not 0 <= session <= 65535 or not 1 <= period_ns <= 0xffffffff:
raise ValueError("Invalid stream metadata")
return self._packet(self.lib.las_metadata, session, period_ns, int(uart))
def data_packet(self, session, index, sample, uart=False):
if not 0 <= index <= 0xffffffff or any(not 0 <= v <= 65535 for v in (session, sample)):
raise ValueError("Invalid stream samples")
return self._packet(self.lib.las_data, session, index, sample, int(uart))
def _packet(self, fn, *args):
identifier = C.c_uint32()
out = (C.c_ubyte*32)()
n = fn(self.device_id, *args, C.byref(identifier), out, len(out))
if not n:
raise ValueError("Invalid SETCAN packet arguments")
return identifier.value, bytes(out[:n])

View File

@@ -0,0 +1,103 @@
"""Qt transport/lifecycle port for the C SETCAN stream receiver.
CAN ingress accepts canonical RX events from an existing bus connection.
UART uses the same SETCAN frames inside the shared AA55/CRC16 transport.
Reception and rendering clocks are separate; no packet-rate repainting.
"""
from __future__ import annotations
from set_devices.qt_ports.qt_compat import QObject, QTimer, QElapsedTimer, Signal
from set_devices.qt_ports.qt_compat import QSerialPort, QSerialPortInfo
from .stream import NativeStream
class StreamPort(QObject):
updated = Signal(object, object)
changed = Signal(bool)
error = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.core = None
self.active = False
self.mode = "demo"
self.paused = False
self.serial = QSerialPort(self)
self.serial.readyRead.connect(self._read)
self.serial.errorOccurred.connect(self._error)
self.timer = QTimer(self)
self.timer.setInterval(50)
self.timer.timeout.connect(self._tick)
self._previous = None
self._last_data = QElapsedTimer()
self._last_received = None
@staticmethod
def ports():
return [(p.portName(), p.description()) for p in QSerialPortInfo.availablePorts()]
def open(self, mode, port_name="", device_id=None):
self.close()
if mode not in ("demo", "can", "uart", "both"):
self.error.emit("Неизвестный транспорт")
return
try:
self.core = NativeStream(device_id)
except (RuntimeError, ValueError) as exc:
self.error.emit(str(exc))
return
self.mode = mode
if mode in ("uart", "both"):
self.serial.setPortName(port_name)
self.serial.setBaudRate(921600)
self.serial.setDataBits(QSerialPort.DataBits.Data8)
self.serial.setParity(QSerialPort.Parity.NoParity)
self.serial.setStopBits(QSerialPort.StopBits.OneStop)
self.serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl)
if not self.serial.open(QSerialPort.OpenModeFlag.ReadWrite):
self.error.emit(self.serial.errorString())
return
self.active = True
self.paused = False
self._previous = None
self._last_received = None
self._last_data.start()
self.timer.start()
self.changed.emit(True)
def close(self):
self.active = False
self.timer.stop()
self.serial.close()
self.changed.emit(False)
def receive_event(self, event):
if (self.active and self.mode in ("can", "both") and event.get("kind") == "can"
and event.get("direction") == "RX"):
self.core.feed_can(event["identifier"], event["data"],
event.get("extended", False), event.get("remote", False))
def _read(self):
data = bytes(self.serial.readAll())
if self.active and self.mode in ("uart", "both"):
self.core.feed_uart(data)
def _tick(self):
if not self.active:
return
if self.mode == "demo":
self.core.demo_step()
stats = self.core.stats
signature = (stats["received"], stats["session"])
if signature != self._last_received:
self._last_received = signature
self._last_data.restart()
stats["stale"] = self._last_data.elapsed() > max(2000, stats["period_ns"]*3/1e6)
if not self.paused and stats != self._previous:
self._previous = stats
self.updated.emit(self.core.snapshot(self.mode == "demo"), stats)
def _error(self, code):
if self.active and code != QSerialPort.SerialPortError.NoError:
message = self.serial.errorString()
self.close()
self.error.emit(message)

View File

@@ -0,0 +1 @@
"""Reusable device libraries from setcorp/templates."""

View File

@@ -0,0 +1,358 @@
"""Offline analysis of timestamped edges; no resampling or hardware access."""
from __future__ import annotations
from bisect import bisect_left, bisect_right
from dataclasses import dataclass
import heapq
import math
from .files import ImportCancelled
from .decoders.gate_timing import TimingChecker
from .decoders.set_uart import StreamParser
from .decoders.pm35_uart import PM35Parser
from .decoders.set_can import legacy, Reassembler
@dataclass
class AnalysisEvent:
start: float
end: float
kind: str
text: str
details: str = ''
@dataclass
class AnalysisResult:
events: list
truncated: bool
start: float
end: float
def pulse_measurements(channel, time, start, end):
"""Measure a complete adjacent high/low cycle, without scanning the record."""
edges = channel.edges
index = bisect_right(edges, time)
result = {'level': channel.initial ^ (index & 1)}
if 0 < index < len(edges):
left, right = edges[index - 1], edges[index]
if start <= left < right <= end:
result['width'] = right - left
result['high' if result['level'] else 'low'] = right - left
# Prefer the next complete interval, otherwise the preceding one.
other = None
if index + 1 < len(edges) and edges[index + 1] <= end:
other = edges[index + 1] - right
elif index >= 2 and edges[index - 2] >= start:
other = left - edges[index - 2]
if other is not None:
result['low' if result['level'] else 'high'] = other
result['period'] = result['high'] + result['low']
result['frequency'] = 1 / result['period']
result['duty'] = 100 * result['high'] / result['period']
return result
def interval_edges(channel, a, b):
"""Count rising/falling edges in (min(a,b), max(a,b)]."""
left, right = sorted((a, b))
lo, hi = bisect_right(channel.edges, left), bisect_right(channel.edges, right)
count = hi - lo
first_rising = channel.initial ^ (lo & 1) == 0
rising = count // 2 + int(bool(count & 1) and first_rising)
return rising, count - rising
def _cancel(cancel):
if cancel():
raise ImportCancelled()
def uart_frames(channel, start, end, baud, parity='none', stops=1, inverted=False, cancel=lambda: False):
"""Yield (start, end, byte, error), sampling 8-bit UART at bit centres."""
bit = 1.0 / baud
bits = 9 + (parity != 'none') + stops
edges = channel.edges
index = bisect_left(edges, start)
while index < len(edges) and edges[index] < end:
_cancel(cancel)
time = edges[index]
level = channel.initial ^ ((index + 1) & 1) ^ inverted
if level:
index += 1
continue
finish = time + bits * bit
if finish > end + bit * 1e-6:
yield time, end, None, 'Неполный UART-байт в конце диапазона'
break
read = lambda pos: channel.level_at(time + pos * bit) ^ inverted
if read(0.5):
index += 1 # A glitch shorter than half a start bit.
continue
values = [read(1.5 + i) for i in range(8)]
value = sum(v << i for i, v in enumerate(values))
error = None
if parity != 'none' and read(9.5) != ((sum(values) + (parity == 'odd')) & 1):
error = 'Ошибка чётности UART'
stop_start = 9 + (parity != 'none')
if any(read(stop_start + i + 0.5) != 1 for i in range(stops)):
error = 'Ошибка стопового бита UART / BREAK'
yield time, finish, value, error
index = bisect_left(edges, finish - bit * 1e-6, index + 1)
def _number(bits):
value = 0
for bit in bits:
value = (value << 1) | bit
return value
def can_crc(bits):
crc = 0
for bit in bits:
feedback = ((crc >> 14) & 1) ^ bit
crc = (crc << 1) & 0x7fff
if feedback:
crc ^= 0x4599
return crc
class _CanBits:
def __init__(self, channel, time, end, bitrate, inverted, sample_point):
self.channel, self.time, self.end = channel, time, end
self.bit, self.inverted = 1 / bitrate, inverted
self.sample_point = sample_point
self.position, self.previous, self.run = 0, None, 0
self.bits = []
self.last_sample = time
def raw(self):
# Resynchronise on recessive->dominant edges near the bit boundary.
boundary = self.time + self.position * self.bit
if self.position:
lo = bisect_right(self.channel.edges, max(self.last_sample, boundary - self.bit * .2))
hi = bisect_right(self.channel.edges, boundary + self.bit * .2, lo)
for index in range(lo, hi):
level = self.channel.initial ^ ((index + 1) & 1) ^ self.inverted
if level == 0:
self.time += self.channel.edges[index] - boundary
boundary = self.channel.edges[index]
break
sample = boundary + self.sample_point * self.bit
if sample >= self.end:
raise EOFError('Неполный CAN-кадр в конце диапазона')
self.last_sample = sample
self.position += 1
return self.channel.level_at(sample) ^ self.inverted
def stuffed(self):
if self.run == 5:
value = self.raw()
if value == self.previous:
raise ValueError('CAN: ошибка bit stuffing / error frame')
self.previous, self.run = value, 1
value = self.raw()
self.run = self.run + 1 if value == self.previous else 1
self.previous = value
self.bits.append(value)
return value
def take(self, count):
return _number([self.stuffed() for _ in range(count)])
def can_frames(channel, start, end, bitrate, inverted=False, sample_point=.7, cancel=lambda: False):
"""Classic CAN, strict stuffing/CRC/form validation before application decode."""
edges = channel.edges
index = bisect_left(edges, start)
bit = 1 / bitrate
while index < len(edges) and edges[index] < end:
_cancel(cancel)
time = edges[index]
level = channel.initial ^ ((index + 1) & 1) ^ inverted
previous = edges[index - 1] if index else start
# SOF follows at least three recessive intermission bits.
if level or time - previous < bit * 2.9:
index += 1
continue
reader = _CanBits(channel, time, end, bitrate, inverted, sample_point)
frame, error = None, None
try:
if reader.take(1):
raise ValueError('CAN: неверный SOF')
ident = reader.take(11)
rtr, extended = reader.take(1), reader.take(1)
if extended:
if rtr != 1:
raise ValueError('CAN: неверный SRR')
ident = (ident << 18) | reader.take(18)
rtr = reader.take(1)
if reader.take(2):
raise ValueError('CAN FD / reserved bits не поддерживаются')
elif reader.take(1):
raise ValueError('CAN FD / reserved bit не поддерживается')
dlc = reader.take(4)
if dlc > 8:
raise ValueError('CAN: поддерживается classic DLC 0…8')
data = bytes(reader.take(8) for _ in range(0 if rtr else dlc))
expected = can_crc(reader.bits)
received = reader.take(15)
if reader.run == 5:
if reader.raw() == reader.previous:
raise ValueError('CAN: неверный последний stuff bit')
if reader.raw() != 1:
raise ValueError('CAN: неверный CRC delimiter')
ack = reader.raw() == 0
if reader.raw() != 1 or any(reader.raw() != 1 for _ in range(7)):
raise ValueError('CAN: неверный ACK delimiter / EOF')
if received != expected:
raise ValueError('CAN CRC15: получено %04X, ожидается %04X' % (received, expected))
frame = dict(ident=ident, extended=bool(extended), remote=bool(rtr),
data=data, dlc=dlc, ack=ack)
except (ValueError, EOFError) as exc:
error = str(exc)
finish = min(end, reader.time + reader.position * bit)
yield time, finish, frame, error
index = bisect_left(edges, finish - bit * 1e-6, index + 1)
def analyze_capture(capture, options, start=None, end=None, progress=lambda n: None, cancel=lambda: False):
start = capture.start if start is None else max(capture.start, start)
end = capture.end if end is None else min(capture.end, end)
if end <= start:
raise ValueError('Выберите непустой интервал анализа.')
mode = options['mode']
channel = capture.channels[options.get('channel', 0)]
events = []
last_progress = -1
limit = options.get('max_events', 50000)
if limit <= 0:
raise ValueError('Лимит результатов должен быть положительным.')
def emit(a, b, kind, text, details=''):
if len(events) >= limit:
raise OverflowError()
events.append(AnalysisEvent(a, b, kind, text, details))
def report(time):
nonlocal last_progress
_cancel(cancel)
value = min(99, int((time - start) / (end - start) * 100))
if value != last_progress:
progress(value)
last_progress = value
def parsed(items):
for a, b, frame, error in items:
emit(a, b, 'error' if error else 'frame', error or frame['summary'],
'' if not frame else bytes(frame.get('raw', frame.get('payload', b''))).hex(' '))
if frame and frame.get('protocol') == 'ProtoCAN bridge' and frame['flags'] & 1 and not frame['flags'] & 10:
try:
application = legacy(frame['can_id'], frame['payload'])
emit(a, b, 'frame', application['summary'])
except ValueError as exc:
emit(a, b, 'error', str(exc))
truncated = False
try:
if mode in ('1SP0635', '1SD536F2'):
other = options.get('status_channel', 1)
if other == options.get('channel', 0):
raise ValueError('Vin и Vstat должны быть разными каналами.')
status = capture.channels[other]
checker = TimingChecker(1e9, mode, options.get('tolerance_ns', 100),
not options.get('vin_low', False), not options.get('status_low', False))
# The pure checker works in integer ticks. Offset before rounding
# to preserve ns precision for CSV timestamps far from zero.
tick = lambda t: round((t - start) * 1e9)
def timing(items):
for item in items:
text = item['text']
if item['kind'] == 'fault':
text += ' · аварийная обратная связь; превышение тока не подтверждено'
emit(start + item['start'] / 1e9, start + item['end'] / 1e9, item['kind'], text)
def transitions(ch, which):
first = bisect_right(ch.edges, start)
for i in range(first, bisect_right(ch.edges, end)):
yield ch.edges[i], which, ch.initial ^ ((i + 1) & 1)
if bool(status.level_at(start)) == checker.vstat_active_high:
emit(start, start, 'orphan', 'Vstat активен в начале диапазона: начало импульса не записано')
for time, which, level in heapq.merge(transitions(channel, 0), transitions(status, 1)):
report(time)
timing(checker.expire(tick(time)))
timing(checker.on_control_edge(tick(time), level) if which == 0 else checker.on_status_edge(tick(time), level))
timing(checker.expire(tick(end)))
if checker.status_start is not None:
emit(start + checker.status_start / 1e9, end, 'incomplete', 'Vstat: импульс не завершён в диапазоне')
for pending in checker.pending:
emit(start + pending['sample'] / 1e9, end, 'incomplete', 'Vin: диапазон закончился до тайм-аута ACK')
elif mode in ('UART', 'SET UART', 'PM35 UART'):
baud = options.get('baudrate', 115200)
parity, stops = options.get('parity', 'none'), options.get('stops', 1)
if not math.isfinite(baud) or baud <= 0 or parity not in ('none', 'even', 'odd') or stops not in (1, 2):
raise ValueError('Некорректные настройки UART.')
parser = (StreamParser(options.get('protocol', 'auto')) if mode == 'SET UART' else
PM35Parser(options.get('role', 'response')) if mode == 'PM35 UART' else None)
gap = (3.5 * (9 + (parity != 'none') + stops) / baud if mode == 'PM35 UART'
else options.get('gap_ms', 100) / 1000)
last = None
for a, b, value, error in uart_frames(channel, start, end, baud, parity, stops, options.get('inverted', False), cancel):
report(a)
if parser and last is not None and gap > 0 and a - last >= gap:
parsed(parser.flush())
last = b
if error:
if parser:
parsed(parser.flush())
emit(a, b, 'error', error)
elif parser:
parsed(parser.feed(value, a, b))
else:
emit(a, b, 'byte', 'UART 0x%02X' % value, chr(value) if 32 <= value < 127 else '')
if parser:
parsed(parser.flush())
elif mode in ('CAN', 'SET CAN'):
bitrate = options.get('baudrate', 1000000)
sample_point = options.get('sample_point', 70) / 100
if not math.isfinite(bitrate) or bitrate <= 0 or not .1 <= sample_point <= .95:
raise ValueError('Некорректный битрейт / точка выборки CAN.')
reassembler = Reassembler()
for a, b, frame, error in can_frames(channel, start, end, bitrate, options.get('inverted', False), sample_point, cancel):
report(a)
if error:
emit(a, b, 'error', error)
# Never bridge an invalid/missing physical frame.
for pending in reassembler.pending.values():
emit(pending['start'], b, 'incomplete', 'SET CAN: сборка прервана ошибкой шины')
reassembler.pending.clear()
continue
ident, data = frame['ident'], frame['data']
emit(a, b, 'can', 'CAN %s ID=%08X DLC=%d %s %s' % (
'EXT' if frame['extended'] else 'STD', ident, frame['dlc'],
'RTR' if frame['remote'] else 'DATA', 'ACK' if frame['ack'] else 'NACK'), data.hex(' '))
if mode == 'SET CAN' and frame['extended'] and not frame['remote']:
protocol = options.get('protocol', 'protocan')
if protocol == 'set-v2':
parsed(reassembler.feed(ident, data, a, b, b * 1000))
else:
try:
result = legacy(ident, data, protocol)
if result:
emit(a, b, 'frame', result['summary'], data.hex(' '))
except ValueError as exc:
emit(a, b, 'error', str(exc))
for pending in reassembler.pending.values():
emit(pending['start'], end, 'incomplete', 'Неполная сборка SET CAN в конце диапазона')
else:
raise ValueError('Неизвестный анализатор: ' + mode)
except OverflowError:
truncated = True
_cancel(cancel)
progress(100)
events.sort(key=lambda event: (event.start, event.end))
return AnalysisResult(events, truncated, start, end)

View File

@@ -0,0 +1 @@
"""Reusable device libraries from setcorp/templates."""

View File

@@ -0,0 +1,181 @@
"""Pure timing checker used by the DSView decoder and unit tests.
All time values in profiles are nanoseconds. The ACK delay in the vendor
data sheets is a typical value only, so the delay window is intentionally an
engineering warning threshold. ACK pulse width has specified limits and is
therefore checked as PASS/FAIL.
"""
from collections import deque
PROFILES = {
'1SP0635': {
'ack_delay_typ_ns': 250.0,
'ack_width_min_ns': 400.0,
'ack_width_typ_ns': 700.0,
'ack_width_max_ns': 1050.0,
'fault_threshold_ns': 1500.0,
},
'1SD536F2': {
'ack_delay_typ_ns': 380.0,
'ack_width_min_ns': 600.0,
'ack_width_typ_ns': 900.0,
'ack_width_max_ns': 1800.0,
# The application manual calls >1.5 us a fault, while individual
# data sheets allow ACK pulses up to 1.8 us. A correlated pulse in
# the specified ACK range wins; otherwise the longer limit is used.
'fault_threshold_ns': 1800.0,
},
}
def format_ns(value_ns):
if value_ns >= 1000000.0:
return '%.3f ms' % (value_ns / 1000000.0)
if value_ns >= 1000.0:
return '%.3f us' % (value_ns / 1000.0)
return '%.1f ns' % value_ns
class TimingChecker(object):
"""Match Vin edges to Vstat pulses and return annotation dictionaries."""
def __init__(self, samplerate, profile='1SP0635', delay_tolerance_ns=100.0,
vin_active_high=True, vstat_active_high=True,
custom=None):
if not samplerate:
raise ValueError('samplerate is required')
if profile == 'custom':
if not custom:
raise ValueError('custom profile values are required')
self.spec = dict(custom)
else:
self.spec = dict(PROFILES[profile])
self.profile = profile
self.samplerate = float(samplerate)
self.delay_tolerance_ns = float(delay_tolerance_ns)
self.vin_active_high = bool(vin_active_high)
self.vstat_active_high = bool(vstat_active_high)
self.pending = deque()
self.status_start = None
self.status_control = None
# Long enough to avoid calling a merely late ACK "missing", while
# still producing a useful annotation during a capture.
self.ack_timeout_ns = max(
self.spec['ack_delay_typ_ns'] + 3.0 * self.delay_tolerance_ns,
self.spec['ack_delay_typ_ns'] + self.spec['ack_width_max_ns'])
def samples_to_ns(self, samples):
return float(samples) * 1000000000.0 / self.samplerate
def ns_to_samples(self, value_ns):
return max(1, int(round(float(value_ns) * self.samplerate / 1000000000.0)))
def next_deadline(self):
if not self.pending:
return None
return self.pending[0]['sample'] + self.ns_to_samples(self.ack_timeout_ns)
def on_control_edge(self, sample, level):
state_on = bool(level) == self.vin_active_high
item = {
'sample': int(sample),
'level': int(level),
'edge': 'ON' if state_on else 'OFF',
}
self.pending.append(item)
return [{
'kind': 'control', 'start': int(sample), 'end': int(sample),
'text': 'Vin %s' % item['edge'], 'short': item['edge'],
}]
def expire(self, sample):
events = []
timeout_samples = self.ns_to_samples(self.ack_timeout_ns)
while self.pending and int(sample) >= self.pending[0]['sample'] + timeout_samples:
item = self.pending.popleft()
end = item['sample'] + timeout_samples
events.append({
'kind': 'missing', 'start': item['sample'], 'end': end,
'text': 'FAIL: no Vstat ACK after Vin %s (timeout %s)' %
(item['edge'], format_ns(self.ack_timeout_ns)),
'short': 'NO ACK',
})
return events
def on_status_edge(self, sample, level):
sample = int(sample)
is_active = bool(level) == self.vstat_active_high
events = self.expire(sample)
if is_active:
# Ignore a second active edge caused by an inconsistent trace.
if self.status_start is not None:
return events
self.status_start = sample
self.status_control = self.pending.popleft() if self.pending else None
if self.status_control is not None:
delay_ns = self.samples_to_ns(sample - self.status_control['sample'])
typ_ns = self.spec['ack_delay_typ_ns']
delta_ns = delay_ns - typ_ns
in_window = abs(delta_ns) <= self.delay_tolerance_ns
events.append({
'kind': 'delay_ok' if in_window else 'delay_warn',
'start': self.status_control['sample'], 'end': sample,
'delay_ns': delay_ns,
'text': '%s: ACK delay %s (typ %s, delta %+0.1f ns)' %
('PASS' if in_window else 'WARN', format_ns(delay_ns),
format_ns(typ_ns), delta_ns),
'short': '%s %s' % ('OK' if in_window else 'WARN',
format_ns(delay_ns)),
})
return events
if self.status_start is None:
events.append({
'kind': 'orphan', 'start': sample, 'end': sample,
'text': 'Unexpected inactive Vstat edge', 'short': 'Vstat?',
})
return events
start = self.status_start
control = self.status_control
width_ns = self.samples_to_ns(sample - start)
self.status_start = None
self.status_control = None
lo = self.spec['ack_width_min_ns']
hi = self.spec['ack_width_max_ns']
if control is not None and lo <= width_ns <= hi:
events.append({
'kind': 'width_ok', 'start': start, 'end': sample,
'width_ns': width_ns,
'text': 'PASS: ACK width %s (limit %s...%s)' %
(format_ns(width_ns), format_ns(lo), format_ns(hi)),
'short': 'ACK %s' % format_ns(width_ns),
})
elif width_ns > self.spec['fault_threshold_ns']:
events.append({
'kind': 'fault', 'start': start, 'end': sample,
'width_ns': width_ns,
'text': 'FAULT: Vstat active for %s' % format_ns(width_ns),
'short': 'FAULT %s' % format_ns(width_ns),
})
elif control is None:
events.append({
'kind': 'orphan', 'start': start, 'end': sample,
'width_ns': width_ns,
'text': 'Unexpected Vstat pulse %s (no Vin edge)' % format_ns(width_ns),
'short': 'ORPHAN %s' % format_ns(width_ns),
})
else:
events.append({
'kind': 'width_fail', 'start': start, 'end': sample,
'width_ns': width_ns,
'text': 'FAIL: ACK width %s outside %s...%s' %
(format_ns(width_ns), format_ns(lo), format_ns(hi)),
'short': 'BAD ACK %s' % format_ns(width_ns),
})
return events

View File

@@ -0,0 +1,95 @@
"""PM35 MODBUS 03/06, including the legacy 128-word/zero-byte-count reply."""
from .set_uart import crc16, number
def parse_pm35(raw, role):
raw = bytes(raw)
if len(raw) < 5:
raise ValueError('Incomplete PM35 frame')
if crc16(raw[:-2], True) != number(raw[-2:]):
raise ValueError('PM35 CRC mismatch')
address, function = raw[:2]
result = dict(protocol='PM35', controller=address, function=function, raw=raw)
if function in (0x83, 0x86):
if len(raw) != 5 or role == 'request':
raise ValueError('Invalid exception response')
result['summary'] = 'PM35 node=%d exception fn=%02X code=%d' % (address, function, raw[2])
elif function == 6 or function == 3 and role == 'request':
if len(raw) != 8:
raise ValueError('PM35 request/06 must contain 8 bytes')
register, value = number(raw[2:4], 'big'), number(raw[4:6], 'big')
if register >= 128 or function == 3 and not 1 <= value <= 128 - register:
raise ValueError('PM35 register range outside 0..127')
result.update(register=register, value=value)
result['summary'] = 'PM35 node=%d %s R%d %s=%d' % (address,
'READ' if function == 3 else 'WRITE/echo', register,
'count' if function == 3 else 'value', value)
if function == 6 and register == 127:
result['summary'] += ' command_bits=%04X' % value
elif function == 3:
count = raw[2] or 256
if count % 2 or len(raw) != count + 5:
raise ValueError('Invalid PM35 response byte count')
result['values'] = [number(raw[i:i+2], 'big') for i in range(3, len(raw)-2, 2)]
result['summary'] = 'PM35 node=%d READ response %d words: %s' % (address,
len(result['values']), ' '.join('%04X' % v for v in result['values']))
else:
raise ValueError('Unsupported PM35 function')
return result
class PM35Parser:
def __init__(self, role):
self.role = role
self.buffer = []
def flush(self):
if not self.buffer:
return []
ss, es = self.buffer[0][1], self.buffer[-1][2]
self.buffer = []
return [(ss, es, None, 'Incomplete PM35 frame')]
def feed(self, value, ss, es):
self.buffer.append((value, ss, es))
events = []
while len(self.buffer) >= 3:
raw = bytes(item[0] for item in self.buffer)
fn = raw[1]
if fn not in (3, 6, 0x83, 0x86):
del self.buffer[0]
continue
role = self.role
total = 5 if fn & 128 else 8
if role == 'auto' and fn == 3:
candidates = [(8, 'request'), ((raw[2] or 256) + 5, 'response')]
found = None
for length, candidate_role in candidates:
if len(raw) >= length:
try:
found = (length, candidate_role, parse_pm35(raw[:length], candidate_role))
break
except ValueError:
pass
if found:
length, candidate_role, result = found
events.append((self.buffer[0][1], self.buffer[length-1][2], result, None))
del self.buffer[:length]
continue
if len(raw) < max(length for length, _ in candidates):
break
events.append((self.buffer[0][1], self.buffer[-1][2], None, 'Invalid PM35 frame/CRC'))
del self.buffer[0]
continue
if fn == 3 and role == 'response':
total = (raw[2] or 256) + 5
if len(raw) < total:
break
result, error = None, None
try:
result = parse_pm35(raw[:total], role)
except ValueError as exc:
error = str(exc)
events.append((self.buffer[0][1], self.buffer[total-1][2], result, error))
del self.buffer[:1 if error else total]
return events

View File

@@ -0,0 +1,124 @@
"""ProtoCAN, Balsam and SET v2 classic CAN binding."""
from .set_uart import number, parse_frame
TYPES = {0: 'BROADCAST', 1: 'DISCRETE', 2: 'ANALOG', 3: 'GAS',
4: 'MODBUS_COIL', 5: 'MODBUS_DISCRETE', 6: 'MODBUS_HOLDING',
7: 'MODBUS_INPUT', 8: 'ERROR', 9: 'BOOT_CONTROL', 10: 'BOOT_DATA_A',
11: 'BOOT_DATA_B', 12: 'BOOT_STATUS', 13: 'BOOT_DISCOVERY', 15: 'PULSE'}
BOOT = dict(enumerate(('IDENTIFY ENTER_BOOT BEGIN_IMAGE BEGIN_COMPAT ERASE VERIFY '
'COMMIT CONFIRM REBOOT ABORT QUERY_PROGRESS').split(), 1))
def legacy(ident, data, profile='protocan'):
if not 0 <= ident <= 0x1fffffff or len(data) > 8:
raise ValueError('Invalid classic CAN frame')
relative = ident - 0xba0000
if profile == 'balsam':
if not (0 <= relative < 13 or 16 <= relative < 29):
return None
if len(data) != 8:
raise ValueError('Balsam requires DLC=8')
header = number(data[:2], 'big')
address, mask = header & 0x1fff, header >> 13
registers = [(address + i, number(data[2+i*2:4+i*2], 'big'))
for i in range(3) if mask & (4 >> i)]
return dict(protocol='Balsam', registers=registers, summary=
'Balsam node=%d %s mask=%X %s' % ((relative & 15) + 1,
'data' if relative >= 16 else 'command', mask,
' '.join('R%04X=%04X' % pair for pair in registers)))
msg, body = (ident >> 16) & 15, ident & 0xffff
result = dict(protocol='ProtoCAN', priority=(ident >> 28) & 1,
route=(ident >> 27) & 1, device_type=(ident >> 24) & 7,
device_id=(ident >> 20) & 15, message_type=msg, body=body)
text = 'ProtoCAN %s dev=%d:%d route=%d priority=%d body=%04X' % (
TYPES.get(msg, 'TYPE_%X' % msg), result['device_type'], result['device_id'],
result['route'], result['priority'], body)
if msg == 3:
if len(data) % 2 or body + len(data)//2 > 65536:
raise ValueError('Invalid GAS register payload/range')
result['registers'] = [(body + i//2, number(data[i:i+2])) for i in range(0, len(data), 2)]
text += ' ' + ('READ' if not data else ' '.join('R%04X=%04X' % pair for pair in result['registers']))
elif 4 <= msg <= 7:
text += ' address=%03X count=%d' % (body >> 4, body & 15)
elif msg == 8:
text += ' info=%02X code=%02X' % (body >> 8, body & 255)
elif msg in (9, 12):
command = body & 255
text += ' session=%d command=%s' % (body >> 8, BOOT.get(command, '%02X' % command))
if msg == 12:
if len(data) != 8:
raise ValueError('BOOT_STATUS requires DLC=8')
text += ' status=%d slot=%d next_block=%d CRC32=%08X' % (data[0], data[1], number(data[2:4]), number(data[4:8]))
elif command in (3, 4):
if len(data) != 8:
raise ValueError('BOOT metadata requires DLC=8')
if command == 3:
text += ' image_size=%d CRC32=%08X' % (number(data[:4]), number(data[4:]))
else:
text += ' product=%d hw=%d..%d version=%08X' % (number(data[:2]), data[2], data[3], number(data[4:]))
elif command in BOOT and data:
raise ValueError('BOOT control requires DLC=0')
elif msg in (10, 11):
if len(data) != 8:
raise ValueError('BOOT_DATA requires DLC=8')
text += ' block=%d offset=0x%X' % (body, body * 8)
elif msg in (0, 1, 2):
shift = 4 if msg == 0 else 12
text += ' type=%X value=%X' % (body >> shift, body & ((1 << shift) - 1))
result['summary'] = text
return result
class Reassembler:
def __init__(self):
self.pending = {}
def feed(self, ident, data, ss, es, now_ms):
events = []
for key, state in list(self.pending.items()):
if now_ms - state['time'] >= 500:
events.append((state['start'], es, None, 'SET CAN timeout ID=%08X' % key))
del self.pending[key]
if ident >> 24 != 0x12:
return events
try:
if not 1 <= len(data) <= 8:
raise ValueError('Invalid SET CAN DLC')
pci = data[0] & 0xf0
if pci == 0x30:
if len(data) != 3 or data[0] & 15 > 2 or data[2] > 127:
raise ValueError('Invalid FLOW_CONTROL')
events.append((ss, es, dict(protocol='SET CAN', summary='FLOW_CONTROL status=%d block=%d st_min=%d ms' % (data[0] & 15, data[1], data[2])), None))
elif pci == 0x10:
if data[0] != 0x10 or len(data) != 8 or not 18 <= number(data[1:3]) <= 530:
raise ValueError('Invalid FIRST segment')
old = self.pending.pop(ident, None)
if old:
events.append((old['start'], es, None, 'FIRST replaced incomplete packet'))
if len(self.pending) >= 64:
raise ValueError('Too many concurrent SET CAN packets (64)')
self.pending[ident] = dict(start=ss, time=now_ms, total=number(data[1:3]),
data=bytearray(data[3:]), sequence=1)
events.append((ss, es, dict(protocol='SET CAN', summary='FIRST length=%d' % number(data[1:3])), None))
elif pci == 0x20:
state = self.pending.get(ident)
if not state or len(data) < 2 or data[0] & 15 != state['sequence']:
raise ValueError('Unexpected CONSECUTIVE sequence')
state['data'].extend(data[1:])
state['time'], state['sequence'] = now_ms, (state['sequence'] + 1) & 15
if len(state['data']) > state['total']:
raise ValueError('SET CAN packet exceeds advertised length')
if len(state['data']) == state['total']:
packet = parse_frame(state['data'])
if packet['protocol'] != 'SET v2' or packet['source'] != (ident >> 8) & 255 or packet['destination'] != (ident >> 16) & 255 or bool(packet['flags'] & 32) != bool(ident & 128):
raise ValueError('CAN ID and SET header disagree')
events.append((state['start'], es, packet, None))
del self.pending[ident]
else:
events.append((ss, es, dict(protocol='SET CAN', summary='CONSECUTIVE SN=%d' % (data[0] & 15)), None))
else:
raise ValueError('Unknown SET CAN PCI')
except ValueError as exc:
state = self.pending.pop(ident, None)
events.append((state['start'] if state else ss, es, None, str(exc)))
return events

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