1 Commits

119 changed files with 3633 additions and 10181 deletions

View File

@@ -1,9 +0,0 @@
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})

File diff suppressed because one or more lines are too long

View File

@@ -1,16 +0,0 @@
# 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`.

View File

@@ -1,1151 +0,0 @@
/*
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;
}

View File

@@ -1,29 +0,0 @@
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

View File

@@ -1,287 +0,0 @@
/*
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

View File

@@ -1,234 +0,0 @@
/*
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

@@ -1,49 +0,0 @@
/*
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);

View File

@@ -1,101 +0,0 @@
/*
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;

View File

@@ -1,37 +0,0 @@
/*
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

@@ -11,6 +11,7 @@ set(SETPROTOCOL_V2_SOURCES
src/set_protocol.c
src/set_can.c
src/set_firmware.c
src/set_boot.c
src/set_telemetry.c
src/set_plot.c
src/set_trends.c
@@ -36,6 +37,12 @@ set(SETPROTOCOL_LEGACY_SOURCES
)
set(SETPROTOCOL_SOURCES
src/set_regmap.c
src/set_emu_ump.c
src/set_emu_tms.c
src/set_emu_igbt.c
src/set_emu_server.c
src/set_wavegen.c
${SETPROTOCOL_V2_SOURCES}
${SETPROTOCOL_LEGACY_SOURCES}
)
@@ -81,6 +88,14 @@ endif()
option(SETP_BUILD_TESTS "Build host tests" ON)
if(SETP_BUILD_TESTS)
enable_testing()
add_executable(test_emulator_services tests/test_emulator_services.c ports/example-emulator/service.c)
target_link_libraries(test_emulator_services PRIVATE setprotocol_static)
add_test(NAME emulator_services COMMAND test_emulator_services)
if(MSVC)
target_compile_options(test_emulator_services PRIVATE /UNDEBUG)
else()
target_compile_options(test_emulator_services PRIVATE -UNDEBUG)
endif()
add_executable(test_altera_logic tests/test_altera_logic.c)
target_link_libraries(test_altera_logic PRIVATE setprotocol_static)
add_test(NAME altera_logic COMMAND test_altera_logic)

View File

@@ -1,5 +1,8 @@
# SETProtocol
Универсальные сервисы эмуляторов: [адресное пространство, API и порт новой платы](docs/EMULATOR_SERVICES.md).
`set_regmap` объединяет УМП, 2812, IGBT и DAC за общим API для RTU, CAN и других оболочек.
Единое переносимое протокольное ядро SET для `SETGUI`, Android GUI, устройств
и сервисных утилит. В одном C99-проекте собраны:

View File

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

View File

@@ -0,0 +1,126 @@
# Генератор произвольного сигнала WG v1
Общее C99-ядро строит кривые по точкам, преобразует напряжения в коды ЦАП и
принимает таблицы через RTU поверх USB CDC/COM. Оно не зависит от Qt, HAL или ОС.
Память предоставляет вызывающий; скрытого heap и глобального состояния нет.
```text
SETGUI: точки -> Python/ctypes -> set_signal.c -> график / CSV / C
-> set_wavegen.c -> USB CDC
MCU: USB stream -> set_wave_rtu -> set_wave_port -> TIM6/DMA/DAC
```
| Файл | Зависимости и назначение |
|---|---|
| `include/set_signal.h`, `src/set_signal.c` | C99/math: МНК 1…5, linear, PCHIP, natural cubic spline, 12-bit DAC |
| `include/set_wavegen.h`, `src/set_wavegen.c` | C99 + `set_crc.c`: транзакционная загрузка и wire codec |
| `ports/stm32f407-wavegen` | CMSIS F407: PA4/DAC1, TIM6, DMA1 Stream5 Channel7 |
| `ports/stm32g474-wavegen` | STM32G4 HAL: готовые DAC/TIM/DMA handles из CubeMX |
| `python/set_devices/signal_reconstruction.py`, `waveform.py` | Модели, JSON/экспорт, ctypes; алгоритмов на Python нет |
| `python/set_devices/wavegen_protocol.py`, `qt_ports/wavegen.py` | ctypes-кодек и Qt COM worker с проверкой чтением обратно |
## Интерполяция и таблица
`set_signal_reconstruct` принимает раздельные X/Y и workspace `14*N+128`
элементов double. Сортировка, усреднение одинаковых времён, нормализация,
МНК через QR и интерполяция выполняются в C. Максимум 100000 входных/10000
выходных точек. Переданный workspace может повторно использоваться.
Большие расчёты предназначены для хоста; на MCU можно строить малые таблицы
или принимать готовую таблицу без затрат на интерполяцию.
`endpoint=1` включает последний X (анализ логов); `endpoint=0` строит один
период `[0,T)` без дублированного отсчёта стыка. Частота Fs и период T задают
целое `N=Fs*T`; для WG максимум N=4096, Fs=1…50000 Гц, один канал, циклический
выход. При разных значениях первой и последней точки на стыке будет скачок.
Напряжения 0…Vref округляются к ближайшему коду 0…4095; выход за диапазон
отклоняется целиком. Это относится и к выбросам сплайна. Vref должен
соответствовать фактическому VDDA/VREF+ платы; это не программируемое питание.
Пример хоста (DLL передаётся через `SETPROTOCOL_LIBRARY`):
```python
from set_devices.waveform import generate, c_header
points = [(0, 0), (10, 3.3), (20, 0)]
wave = generate(points, sample_rate=10000, vref=3.3, method="linear")
assert len(wave.codes) == 200
with open("waveform.h", "w", encoding="utf-8") as output:
output.write(c_header(wave))
```
## Контракт порта
`start(context, samples, count, sample_rate)` возвращает 0 при успехе;
`stop(context)` синхронно прекращает DMA и устанавливает нулевой выход.
Память таблицы принадлежит `set_wave_state`, её нельзя менять до stop.
Вызывайте RTU и обслуживание состояния из одного основного потока, не из IRQ.
IRQ USB помещает байты в очередь; USB-пакеты не являются границами RTU.
```c
static uint16_t samples[SET_WAVE_MAX]; /* DMA-accessible SRAM, not CCM */
static set_wave_state wave;
static set_wave_f407 hardware = {72000000};
void application_init(void) {
set_wave_port port = set_wave_f407_port(&hardware);
set_wave_init(&wave, samples, SET_WAVE_MAX, &port);
}
size_t on_rtu(const uint8_t *frame, size_t n, uint8_t reply[256]) {
return set_wave_rtu(&wave, 16, frame, n, reply, 256);
}
```
Для F407 DAC_CH1 — PA4, TIM6 TRGO_UPDATE, DMA1 Stream5/Channel7. Порт использует
только нижние 16 бит DAC_CR и не меняет канал 2. Таблица лежит в SRAM1/2
`0x20000000…0x2001FFFF`, не в CCM. Перед первым отсчётом есть один такт
предзагрузки последнего значения предыдущего периода. Для гарантированной
целой частоты порт принимает только делители входной частоты TIM6; при
72 МГц подходят, например, 100, 1000, 5000, 10000, 20000 Гц.
Частоту таймера передавайте с учётом удвоения при делителе APB1 > 1.
Ошибку DMA/underrun проверяйте через `set_wave_f407_fault()` и останавливайте
состояние. На F407-проекте этот вызов включён в главный цикл.
Для G474 настройте CubeMX: DAC1 channel1 (PA4), output buffer enabled,
TIM6 TRGO_UPDATE, DMA memory-to-peripheral, circular, halfword/halfword,
memory increment, запрос `DMA_REQUEST_DAC1_CHANNEL1`; подключите HAL IRQ
для выбранного канала DMA и DAC. Передайте handles и timer_hz в
`set_wave_g474`. Обработчики ошибок DMA/DAC в приложении должны вызвать stop
и сбросить `wave.running/ready`. Порт проверен компиляцией с CubeG4 1.6.1;
конкретная плата G474 здесь не прошита.
## USB / Modbus RTU
Адрес по умолчанию 16, FC03/FC06, CRC16 Modbus. Максимальный запрос — 8 байт,
ответ — 21 байт; существующий потоковый USB RTU parser F407 подходит без
изменения формата. Все поля строит/проверяет C-кодек, GUI не пакует байты.
| Регистр | Значение |
|---|---|
| `0x1300`, FC03, 8 слов | `0x5747`, версия 1, running, Fs, count, received, ready, capacity |
| `0x1302`, FC06 | 0: stop и нулевой выход; 1: start только после commit |
| `0x1303`, FC06 | Частота 1…50000; сбрасывает ready |
| `0x1304`, FC06 | Начать загрузку, число отсчётов 2…4096; сброс received/ready |
| `0xA000+i`, FC06 | Последовательная загрузка отсчёта 0…4095; повтор того же значения допустим |
| `0xA000+i`, FC03, 1 слово | Чтение загруженного отсчёта для проверки |
| `0x1306`, FC06 | `0xA55A`: commit, только если все отсчёты загружены |
Во время running разрешены чтение и stop; запись/перезагрузка дают исключение
6. Неполная таблица не запускается. Повреждённый CRC и чужой адрес не меняют
состояние. При reset ready/running=0, таблица не сохраняется во Flash.
Профиль и регистры генератора не являются регистратором УМП или протоколом 2812.
Хост выполняет probe → stop → Fs → count → samples → readback каждого
отсчёта → commit → status. Пуск — отдельная пользовательская команда.
После потери USB уже запущенный DAC продолжает автономное воспроизведение;
при таймауте состояние неизвестно до повторного status/stop. Одновременно
используйте одного клиента, не открывайте этот же COM во вкладке подключения.
## Потребители и проверки
- SETGUI: «Логи и графики → Генератор», JSON, CSV и C header.
- `home/407vet6_emul_TMS_Periph`: USB CDC и UART, штатный адрес настройки STM,
отдельный PA4 DAC выход; HAL/CMSIS и настройки платы остаются в проекте.
- G474: переносимый HAL-порт; board init остаётся в целевой прошивке.
- `tests/test_signal_wave.c`: C-векторы интерполяции, DAC, CRC, неполной
загрузки, readback и блокировки running. Общие байты проверены Python
ctypes и фактическим USB stream parser эмулятора.
Аппаратная проверка амплитуды, периода и формы осциллографом не выполнена.

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
#ifndef SET_EMU_TMS_H
#define SET_EMU_TMS_H
#include "set_regmap.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SET_TMS_WORDS 80u
typedef struct {
uint8_t controls[6], running;
uint16_t commands[10], divider;
int32_t frequency, target;
} set_tms_emu;
PCAN_ABI_API void set_tms_init(set_tms_emu *);
PCAN_ABI_API void set_tms_tick(set_tms_emu *);
PCAN_ABI_API unsigned set_tms_read(void *,uint16_t,uint16_t,uint16_t *);
PCAN_ABI_API unsigned set_tms_write(void *,uint16_t,uint16_t);
PCAN_ABI_API size_t set_tms_rtu(set_tms_emu *,unsigned device,const uint8_t *,
size_t,uint8_t *,size_t);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,25 @@
#ifndef SET_EMU_UMP_H
#define SET_EMU_UMP_H
#include "set_regmap.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SET_UMP_WORDS 40u
typedef struct {
uint16_t (*banks[2])[SET_UMP_WORDS];
uint16_t capacity,can_mode;
uint8_t enabled;
uint16_t state,used,next,bank,archive_bank,archive_count,archive_next,period,divider,cursor,source,generation,post,dropped,prev_command,prev_input,prev_output;
uint32_t elapsed,sequence,scenario;
} set_ump_emu;
/* Two distinct caller-owned arrays, capacity*40 words EACH. No heap/MCU layout. */
PCAN_ABI_API unsigned set_ump_init(set_ump_emu *,uint16_t *,uint16_t *,uint16_t capacity,
uint16_t can_mode,uint8_t enabled);
PCAN_ABI_API void set_ump_tick(set_ump_emu *); /* one millisecond */
PCAN_ABI_API unsigned set_ump_read(void *,uint16_t,uint16_t,uint16_t *);
PCAN_ABI_API unsigned set_ump_indexed_read(void *,uint16_t,uint16_t,uint16_t *);
PCAN_ABI_API unsigned set_ump_write(void *,uint16_t,uint16_t);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,48 @@
/* Transport-independent 16-bit word address space. No heap, clocks or board headers. */
#ifndef SET_REGMAP_H
#define SET_REGMAP_H
#include "pcan_abi.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SET_REG_MAX_WORDS 124u
#define SET_REG_READ 1u
#define SET_REG_WRITE 2u
enum { SET_REG_OK=0, SET_REG_FUNCTION=1, SET_REG_ADDRESS=2,
SET_REG_VALUE=3, SET_REG_FAILURE=4, SET_REG_BUSY=6 };
typedef unsigned (*set_reg_read_fn)(void *, uint16_t, uint16_t, uint16_t *);
typedef unsigned (*set_reg_write_fn)(void *, uint16_t, uint16_t);
typedef struct {
uint16_t base, words, kind, version, instance, flags;
void *context;
set_reg_read_fn read;
set_reg_write_fn write;
} set_reg_region;
typedef struct {
const set_reg_region *regions;
size_t count;
} set_regmap;
/* Directory at 0: 8-word header, then 8 words per region. Reject overlaps,
* wraparound, invalid callbacks and overlap with the directory itself.
* Regions and contexts must outlive the map and remain unchanged. */
PCAN_ABI_API unsigned set_regmap_init(set_regmap *, const set_reg_region *, size_t);
/* One region per request. Preflight all bounds/capacity before calling a module.
* Writes return absolute address/value echo. Reads must be side-effect free.
* A module validates the entire write before mutation. Caller serializes access. */
PCAN_ABI_API unsigned set_regmap_request(const set_regmap *, unsigned function,
uint16_t address, uint16_t value, uint16_t *out, size_t capacity, size_t *count);
/* Complete Modbus RTU FC03/06 frame; no broadcast. Capacity >=253 bytes.
* CRC/address/capacity validation precedes dispatch. */
PCAN_ABI_API size_t set_regmap_rtu(const set_regmap *, unsigned device,
const uint8_t *, size_t, uint8_t *, size_t);
/* Legacy UMP CAN envelope, usable by any region: BE address/value/1F00/token/fn.
* Returns padded EVEN words, last padding word=0. Caller retains original
* requested count; token/error accompany fragments. Output capacity >=124. */
PCAN_ABI_API size_t set_regmap_can(const set_regmap *, const uint8_t *, size_t,
uint16_t *, size_t, unsigned *token, unsigned *error);
PCAN_ABI_API size_t set_regmap_can_fragment(const uint16_t *, size_t count,
size_t index, unsigned token, unsigned error, uint8_t *, size_t capacity);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,59 @@
/** @file set_wavegen.h Transactional table upload and cyclic DAC playback. */
#ifndef SET_WAVEGEN_H
#define SET_WAVEGEN_H
#include "pcan_abi.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SET_WAVE_MAX 4096u
#define SET_WAVE_MAX_RATE 1000000u
#define SET_WAVE_BLOCK_MAX 120u
#define SET_WAVE_BASE 0x1300u
#define SET_WAVE_DATA 0xA000u
typedef struct {
void *context;
/* Return 0 on success. Samples remain owned by state until stop returns.
* start must arrange periodic output, typically DAC + timer + DMA.
* stop must quiesce DMA before returning and set output to zero. */
int (*start)(void *, const uint16_t *, size_t, uint32_t);
void (*stop)(void *);
} set_wave_port;
typedef struct {
uint16_t *samples;
size_t capacity;
uint16_t count, received, ready, running, rate_high;
uint32_t rate;
set_wave_port port;
} set_wave_state;
void set_wave_init(set_wave_state *, uint16_t *, size_t, const set_wave_port *);
/* Atomic sample-block transfer. values=NULL builds a read; otherwise a write.
* At most 120 words: request <=249 bytes, response <=245 bytes. */
PCAN_ABI_API size_t set_wave_block_request(unsigned device,unsigned index,
const uint16_t *values,size_t count,uint8_t *out,size_t capacity);
PCAN_ABI_API int set_wave_block_response(const uint8_t *request,size_t request_size,
const uint8_t *reply,size_t reply_size,uint16_t *words,size_t capacity);
size_t set_wave_block_rtu(set_wave_state *,unsigned device,const uint8_t *,size_t,
uint8_t *,size_t);
/* Relative register callbacks for set_regmap; status=8 words, samples=4096.
* These are the same state/validation used by the legacy RTU wrapper. */
PCAN_ABI_API unsigned set_wave_read(void *,uint16_t,uint16_t,uint16_t *);
PCAN_ABI_API unsigned set_wave_write(void *,uint16_t,uint16_t);
PCAN_ABI_API unsigned set_wave_samples_read(void *,uint16_t,uint16_t,uint16_t *);
PCAN_ABI_API unsigned set_wave_samples_write(void *,uint16_t,uint16_t);
/* Caller serializes calls in the main loop. Returns complete reply size,
* 0 for unrelated/invalid frames. CRC verified before all state changes. */
size_t set_wave_rtu(set_wave_state *, unsigned device, const uint8_t *, size_t,
uint8_t *, size_t);
/* Thin host transport ABI. Operations: 0=status, 1=stop, 2=sample rate,
* 3=begin upload(count), 4=sample(index,value), 5=commit, 6=start.
* 7=read a sample(index) for verification. Return 8 or 0. */
PCAN_ABI_API size_t set_wave_request(unsigned device,unsigned operation,unsigned index,
unsigned value,uint8_t *out,size_t capacity);
/* Return 0=need more bytes, positive=word count, negative=invalid/exception.
* Validates exact echo/read shape, CRC, address and signature on status. */
PCAN_ABI_API int set_wave_response(const uint8_t *request,const uint8_t *reply,
size_t size,uint16_t *words,size_t capacity);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -4,6 +4,8 @@
*/
#ifndef SETPROTOCOL_H
#define SETPROTOCOL_H
#include "set_regmap.h"
#include "set_emu_server.h"
#include "balsam_can.h"
#include "periph28335.h"

View File

@@ -0,0 +1,55 @@
/* UMP logger v2 (PM35), independent of PM67 and SET protocol v2.
* Octets use 16-bit storage: C28x has CHAR_BIT=16 and no uint8_t.
* All lengths below count wire octets or register words, never sizeof bytes.
*/
#ifndef UMP_PROTOCOL_H
#define UMP_PROTOCOL_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#define UMP_BASE 0x1000U
#define UMP_DATA 0x1010U
#define UMP_LIVE 0x1100U
#define UMP_MARKER 0x1F00U
#define UMP_MAX_WORDS 124U
#define UMP_RTU_CAPACITY 253U
typedef uint16_t ump_octet;
/* Called under caller's snapshot lock. write returns 1 on success, 0 otherwise. */
typedef struct {
void *context;
uint16_t (*read)(void *context, uint16_t address);
int (*write)(void *context, uint16_t address, uint16_t value);
} ump_backend;
/* Returns Modbus exception 0/1/2/3. CAN has stricter whole-block reads. */
unsigned ump_validate(unsigned function, uint16_t address, uint16_t value, int can);
/* reply capacity is >=124 words. Errors and writes return address/value echo. */
unsigned ump_prepare(const ump_backend *backend, unsigned function,
uint16_t address, uint16_t value, int can, uint16_t *reply, unsigned *error);
uint16_t ump_crc(const ump_octet *data, size_t count);
/* Mode 1..16; kind 0=request, 1=normal telemetry, 2=logger reply; invalid ->0. */
uint32_t ump_can_id(unsigned mode, unsigned kind);
/* Returns 0 for non-service frames, malformed size, or zero token. */
unsigned ump_can_request(const ump_backend *backend, const ump_octet *request,
size_t size, uint16_t *reply, unsigned *token, unsigned *error);
/* count must be an even number 2..124; index counts pairs. Returns 8 or 0. */
unsigned ump_can_fragment(const uint16_t *words, unsigned count, unsigned index,
unsigned token, unsigned error, ump_octet *reply);
/* Complete RTU frame only, after UART framing. Ignores bad CRC/foreign address.
* Caller provides >=253 octets; no writes occur on insufficient capacity.
* Device address 1..247; no broadcasts or legacy padding in this extension.
*/
size_t ump_rtu(const ump_backend *backend, unsigned device,
const ump_octet *request, size_t size, ump_octet *reply, size_t capacity);
/* Response for an already CRC-checked/framed request (legacy UART receiver). */
size_t ump_rtu_response(const ump_backend *backend, unsigned device, unsigned function,
uint16_t address, uint16_t value, ump_octet *reply, size_t capacity);
/* Serialize a captured reply after releasing the snapshot lock. Returns 0 on
* invalid count/capacity; error replies need no words. */
size_t ump_rtu_encode(unsigned device, unsigned function, unsigned error,
const uint16_t *words, unsigned count, ump_octet *reply, size_t capacity);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,41 @@
/* Minimal board-independent composition. This compiles without any MCU headers.
* Adapt these entry points to the selected board's network/USB/UART/CAN port.
* Run every call in one task (or protect the entire call with the same lock). */
#include "set_emu_igbt.h"
static set_igbt_emu drivers[2];
static set_reg_region regions[2];
static set_regmap map;
unsigned example_service_init(void)
{
unsigned i;
for(i=0;i<2;++i) {
set_reg_region *r=&regions[i];
set_igbt_init(&drivers[i]);
r->base=(uint16_t)(0x300+i*0x100);r->words=SET_IGBT_WORDS;
r->kind=4;r->version=2;r->instance=(uint16_t)i;r->flags=3;
r->context=&drivers[i];r->read=set_igbt_read;r->write=set_igbt_write;
}
return set_regmap_init(&map,regions,2);
}
void example_service_tick(uint8_t inputs0,uint8_t inputs1,uint8_t outputs[2])
{
set_igbt_tick(&drivers[0],inputs0);set_igbt_tick(&drivers[1],inputs1);
outputs[0]=drivers[0].rx;outputs[1]=drivers[1].rx;
}
unsigned example_custom_protocol(unsigned function,uint16_t address,uint16_t value,
uint16_t *words,size_t capacity,size_t *count)
{
/* Caller has already decoded/authenticated its own transport envelope. */
return set_regmap_request(&map,function,address,value,words,capacity,count);
}
size_t example_uart_or_usb(const uint8_t *frame,size_t length,uint8_t *reply,size_t capacity)
{
return set_regmap_rtu(&map,16,frame,length,reply,capacity);
}
size_t example_can(const uint8_t frame[8],uint16_t *snapshot,size_t capacity,
unsigned *token,unsigned *error)
{
/* Select the CAN request ID outside the model; send the returned snapshot
* using set_regmap_can_fragment and the board's chosen response CAN ID. */
return set_regmap_can(&map,frame,8,snapshot,capacity,token,error);
}

View File

@@ -0,0 +1,54 @@
/* Shared SET USB boot dispatcher. The MCU wrapper supplies BOOT_* macros. */
#include "usb_setp_stream.h"
#include <string.h>
static setp_boot boot;
static bool reset_pending;
static uint32_t reset_at;
typedef struct { uint8_t *out; uint16_t size; uint32_t now; } reply_context;
void boot_protocol_init(void) { setp_boot_init(&boot,&BOOT_OPS,0,BOOT_APP_BASE,BOOT_APP_SIZE); }
void boot_protocol_poll(uint32_t now) {
if(reset_pending && (int32_t)(now-reset_at)>=0) BOOT_RESET();
}
static void dispatch(const setp_frame_t *r,void *user) {
reply_context *c=user;
uint8_t body[128]; uint16_t status=SETP_STATUS_OK; size_t n=0;
setp_frame_t response={0};
if((r->destination!=0 && r->destination!=BOOT_NODE) ||
(r->flags&(SETP_FLAG_RESPONSE|SETP_FLAG_EVENT))) return;
if(r->flags&(SETP_FLAG_ERROR|SETP_FLAG_MORE)) status=SETP_STATUS_INVALID_ARGUMENT;
else if(r->message_type>=SETP_MSG_FW_BEGIN && r->message_type<=SETP_MSG_FW_ACTIVATE) {
status=setp_boot_command(&boot,r->message_type,r->payload,r->payload_length);
if(!status && r->message_type==SETP_MSG_FW_STATUS)
n=setp_fw_status_encode(&boot.status,body+2,sizeof(body)-2);
if(!status && r->message_type==SETP_MSG_FW_ACTIVATE) {
reset_pending=true; reset_at=c->now+500;
}
} else if(r->payload_length) status=SETP_STATUS_INVALID_LENGTH;
else switch(r->message_type) {
case SETP_MSG_PING: setp_put_u32(body+2,c->now); n=4; break;
case SETP_MSG_DEVICE_INFO: {
static const uint8_t model[]=BOOT_MODEL;
const setp_device_info_t info={1,0,0,0x10000,3,0,sizeof(model)-1,model};
n=setp_device_info_encode(&info,body+2,sizeof(body)-2); break;
}
case SETP_MSG_CAPABILITIES: {
const setp_capabilities_t caps={1,SETP_MAX_PAYLOAD,
SETP_IFACE_MASK(SETP_IFACE_USB_CDC),SETP_FEATURE_FIRMWARE,0,0,0,0};
n=setp_capabilities_encode(&caps,body+2,sizeof(body)-2); break;
}
default: status=SETP_STATUS_UNSUPPORTED; break;
}
setp_put_u16(body,status); if(status) n=0;
response.flags=SETP_FLAG_RESPONSE|(status ? SETP_FLAG_ERROR : 0);
response.message_type=r->message_type; response.sequence=r->sequence;
response.source=BOOT_NODE; response.destination=r->source;
response.payload=body; response.payload_length=(uint16_t)(n+2);
c->size=(uint16_t)setp_frame_encode(&response,c->out,SETP_FRAME_MAX);
}
void usb_setp_reset(usb_setp_stream *s) { memset(s,0,sizeof(*s)); }
uint16_t usb_setp_feed(usb_setp_stream *s,uint8_t byte,uint32_t now,uint8_t *out) {
reply_context c={out,0,now};
if(now-s->last_byte>1000) setp_parser_reset(&s->parser);
s->last_byte=now; (void)setp_parser_feed(&s->parser,&byte,1,dispatch,&c);
return c.size;
}

View File

@@ -0,0 +1,13 @@
# Порт STM32F407 / эмулятор периферии
Добавьте `src/ump_protocol.c` и `ump_stm32f407.c` в сборку.
Порт получает backend с read/write и переводит 8-битные буферы STM32 в
октеты ядра. Приём UART, CRC framing, CAN ID/DLC/RTR-фильтры и очередь
передачи находятся в проекте; ядро проверяет CRC полного RTU кадра.
Вызывать из главного цикла. В эмуляторе SysTick лишь накапливает тики,
поэтому read/write и генератор не выполняются одновременно.
Ответ RTU требует 253 байта; CAN snapshot — 124 uint16_t. Не передавайте
неполный snapshot в fragment, count должен быть фактическим числом слов.
Подключено в `home/407vet6_emul_TMS_Periph/Src/shared_ump.c`.
[Контракт, пример и проверки](../../docs/UMP_PROTOCOL.md).

View File

@@ -0,0 +1,30 @@
#include "ump_stm32f407.h"
size_t ump_stm32f407_rtu(const ump_backend *b, unsigned device,
const uint8_t *request, size_t length, uint8_t *reply, size_t capacity)
{
ump_octet r[8],out[UMP_RTU_CAPACITY];
size_t i,n;
if (!request || !reply || length!=8 || capacity<UMP_RTU_CAPACITY) return 0;
for (i=0;i<8;i++) r[i]=request[i];
n=ump_rtu(b,device,r,8,out,UMP_RTU_CAPACITY);
for (i=0;i<n;i++) reply[i]=(uint8_t)out[i];
return n;
}
unsigned ump_stm32f407_can(const ump_backend *b, const uint8_t *request,
size_t length, uint16_t *words, unsigned *token, unsigned *error)
{
ump_octet r[8];
unsigned i;
if (!request || length!=8) return 0;
for (i=0;i<8;i++) r[i]=request[i];
return ump_can_request(b,r,8,words,token,error);
}
unsigned ump_stm32f407_fragment(const uint16_t *words, unsigned count,
unsigned index, unsigned token, unsigned error, uint8_t reply[8])
{
ump_octet out[8];
unsigned i;
if (!reply || !ump_can_fragment(words,count,index,token,error,out)) return 0;
for (i=0;i<8;i++) reply[i]=(uint8_t)out[i];
return 8;
}

View File

@@ -0,0 +1,11 @@
#ifndef UMP_STM32F407_H
#define UMP_STM32F407_H
#include "../../include/ump_protocol.h"
/* Application owns framing, ID filtering, snapshot lock, and tx buffers. */
size_t ump_stm32f407_rtu(const ump_backend *backend, unsigned device,
const uint8_t *request, size_t length, uint8_t *reply, size_t capacity);
unsigned ump_stm32f407_can(const ump_backend *backend, const uint8_t *request,
size_t length, uint16_t *words, unsigned *token, unsigned *error);
unsigned ump_stm32f407_fragment(const uint16_t *words, unsigned count,
unsigned index, unsigned token, unsigned error, uint8_t reply[8]);
#endif

View File

@@ -0,0 +1,68 @@
# STM32F407VET6 USB boot port
SET v2 single-slot updater: `include/set_boot.h`, `src/set_boot.c` and the existing
`set_firmware` codec. Byte transport is supplied by the application, independently
of the Flash callbacks. The emulator project supplies the ST USB CDC stream.
## Integration in another F407 project
1. Copy `boot_config.template.h` to the project's include directory as
`boot_config.h`: set crystal frequency, SET node and product model. Match
the crystal frequency with HAL `HSE_VALUE`.
2. Compile `src/set_protocol.c`, `src/set_firmware.c`, `src/set_boot.c` and this
port's `boot_main.c`, `boot_flash.c`, `boot_request.c`, `boot_usb_stream.c`.
3. Provide STM32F4 CMSIS/HAL, startup, the ST USB device CDC middleware and the
board USB transport (`usb_cdc.h`, `usb_setp_stream.h` and their implementation).
The transport calls `usb_setp_feed`/`usb_setp_reset` from this port instead of
an application command dispatcher. Enable SET v2 for the bootloader build.
4. Link boot code below `0x0800C000`; link the application at `0x08010000`.
Add `boot_request.c` to the application and periodically call
`f407_reset_poll`; its update command calls `f407_request_boot`.
The emulator's `Bootloader/Src/*.c` files are include-only build wrappers. Neither
the portable updater nor this port depends on the emulator models or its
`board_config.h`. The USB transport and HAL configuration remain board supplied.
Flash layout (512 KiB device): boot sectors 0..2 at `0x08000000` (48 KiB),
commit record in sector 3 at `0x0800C000`, application sectors 4..7 at
`0x08010000` (448 KiB). All addresses and sector operations are in this port.
The sector programming/jump approach is adapted from climate's
`climate_control_f407vet6_f4/Bootloader/bootloader_f407.c`.
BEGIN requires slot 0, base `0x08010000`, size 8..458752, block size 1..256,
ERASE_SLOT flag only, zero SHA/key/signature. DATA must be sequential; its CRC,
length and bounds are checked. END requires complete receipt and matching size
and CRC. The port verifies the actual Flash CRC and vector table before writing
the commit marker last. SHA, signatures, automatic activation and resume are
not supported and are rejected explicitly. CRC is an integrity check.
STATUS returns the standard 16-byte SET firmware status. ACTIVATE accepts an
empty payload after END; the transport adapter defers reset to let the USB
reply finish. ABORT clears the RAM session; it does not restore erased firmware.
On reset, SP and reset PC are checked like in climate. USB metadata is not
required, so Keil/SWD installs and alternating SWD/USB updates work even with
absent or stale USB metadata. Preserve the bootloader using sector erase rather
than full-chip erase. Reflash the updated bootloader once to adopt this behavior.
USB END/ACTIVATE still require complete receipt and CRC verification.
Like climate, the reset vector check cannot detect incomplete images once valid
vectors are written: after an interrupted update and reset such an image may run.
To recover an unresponsive app, erase sector 4 at `0x08010000` via SWD, reset, and
upload the full image again. Erasing metadata sector 3 no longer forces bootloader
entry. An interrupted update requires a fresh BEGIN and full retransmission.
`test_boot_image.c` is a host-only regression test; do not add it to firmware targets.
`boot_request.c` is shared by the application and bootloader. Product command
`0x1003` (empty payload) in the application sets RTC backup register 0 and
schedules reset after 500 ms. The bootloader consumes that flag before deciding
whether to jump. Reserve BKP0R for this port. The jump must run early after reset,
before enabling peripheral interrupts, USB or DMA. Both images must set VTOR
to their own link address in SystemInit.
Board: PA11 D-/PA12 D+, existing self-powered OTG_FS CDC port, 25 MHz HSE;
no VBUS host drive, PA9/PA10 are not used. Flash erase uses x32 at nominal 3.3 V,
programming uses bytes to support unaligned protocol blocks. Hardware acceptance
must cover disconnect/power loss during erase, programming and commit.
The SET USB command dispatcher is shared with G474 in
`../stm32-usb-boot/boot_usb_protocol.inc`; keep that directory when importing this port.

View File

@@ -0,0 +1,9 @@
#ifndef F407_BOOT_CONFIG_H
#define F407_BOOT_CONFIG_H
/* Copy to the board include directory as boot_config.h.
* HSE must be an integer MHz frequency; PLL input is 1 MHz.
* The Flash layout in boot_port.h targets the 512 KiB STM32F407VE. */
#define F407_BOOT_HSE_HZ 25000000UL
#define F407_BOOT_NODE 16U
#define F407_BOOT_MODEL "STM32F407-USB-BOOT"
#endif

View File

@@ -0,0 +1,88 @@
/* Flash geometry and jump sequence derived from climate's F407 bootloader.
* Sector 3 is a durable commit record; sectors 4..7 hold the application.
* Reset checks vectors like climate; USB END always verifies Flash. */
#include "boot_port.h"
#include "boot_image.h"
#include "stm32f4xx_hal.h"
#include <string.h>
#define META 0x0800C000UL
#define COMMIT 0x53455442UL
#define ERRORS (FLASH_SR_PGSERR|FLASH_SR_PGPERR|FLASH_SR_PGAERR|FLASH_SR_WRPERR|FLASH_SR_SOP)
static void unlock(void) {
if(FLASH->CR & FLASH_CR_LOCK) { FLASH->KEYR=0x45670123; FLASH->KEYR=0xCDEF89AB; }
FLASH->SR=ERRORS;
}
static bool finish(void) {
while(FLASH->SR & FLASH_SR_BSY) {}
return !(FLASH->SR & ERRORS);
}
static void cache_reset(void) {
FLASH->ACR &= ~(FLASH_ACR_ICEN|FLASH_ACR_DCEN);
FLASH->ACR |= FLASH_ACR_ICRST|FLASH_ACR_DCRST;
FLASH->ACR &= ~(FLASH_ACR_ICRST|FLASH_ACR_DCRST);
FLASH->ACR |= FLASH_ACR_ICEN|FLASH_ACR_DCEN;
__DSB(); __ISB();
}
static bool erase_sector(unsigned sector) {
bool ok; unlock();
FLASH->CR=(FLASH->CR & ~(FLASH_CR_SNB|FLASH_CR_PSIZE)) |
FLASH_CR_SER | (2UL<<FLASH_CR_PSIZE_Pos) | (sector<<FLASH_CR_SNB_Pos);
FLASH->CR|=FLASH_CR_STRT; ok=finish();
FLASH->CR &= ~FLASH_CR_SER; FLASH->CR |= FLASH_CR_LOCK;
cache_reset(); return ok;
}
static bool program(uint32_t address, const uint8_t *p, uint32_t n) {
bool ok=true; unlock();
/* Byte programming supports arbitrary SET block boundaries and final tails. */
FLASH->CR=(FLASH->CR & ~FLASH_CR_PSIZE)|FLASH_CR_PG;
while(n--) {
*(volatile uint8_t *)address=*p;
if(!finish()) { ok=false; break; }
++address; ++p;
}
FLASH->CR &= ~FLASH_CR_PG; FLASH->CR |= FLASH_CR_LOCK;
cache_reset(); return ok;
}
static bool invalidate(void *u) { (void)u; return erase_sector(3); }
static bool erase(void *u, uint32_t size) {
unsigned s; (void)u;
if(size<8 || size>F407_APP_SIZE) return false;
for(s=4;s<=7;++s) {
uint32_t start=s==4 ? F407_APP_BASE : 0x08020000UL+(s-5)*0x20000UL;
if(start>=F407_APP_BASE+size) break;
if(!erase_sector(s)) return false;
}
return true;
}
static bool write(void *u,uint32_t off,const uint8_t *p,uint16_t n) {
(void)u;
if(!n || off>F407_APP_SIZE || n>F407_APP_SIZE-off) return false;
return program(F407_APP_BASE+off,p,n) && !memcmp((const void *)(F407_APP_BASE+off),p,n);
}
static bool verify(void *u,uint32_t size,uint32_t crc) {
(void)u;
return f407_vectors_valid((const uint8_t *)F407_APP_BASE,size) &&
setp_crc32((const uint8_t *)F407_APP_BASE,size)==crc;
}
static bool commit(void *u,uint32_t size,uint32_t crc) {
uint32_t record[4]={size,crc,~size,COMMIT}; (void)u;
return program(META,(const uint8_t *)record,sizeof(record)) &&
!memcmp((const void *)META,record,sizeof(record));
}
bool f407_app_valid(void) {
/* Like climate: direct Keil/SWD writes need no USB completion record. */
return f407_vectors_valid((const uint8_t *)F407_APP_BASE,F407_APP_SIZE);
}
static void activate(void *u) { (void)u; /* main waits for USB response before reset */ }
const setp_boot_ops f407_boot_ops={invalidate,erase,write,verify,commit,activate};
__attribute__((naked,noreturn)) static void enter(uint32_t sp,uint32_t pc) {
__asm volatile("msr msp, r0\nmovs r2, #0\nmsr control, r2\ndsb\nisb\ncpsie i\nbx r1\n");
}
void f407_jump_app(void) {
unsigned i; uint32_t sp=*(const uint32_t *)F407_APP_BASE;
uint32_t pc=*(const uint32_t *)(F407_APP_BASE+4);
__disable_irq(); SysTick->CTRL=0;
for(i=0;i<8;++i) { NVIC->ICER[i]=0xffffffff; NVIC->ICPR[i]=0xffffffff; }
SCB->ICSR=SCB_ICSR_PENDSTCLR_Msk|SCB_ICSR_PENDSVCLR_Msk;
SCB->VTOR=F407_APP_BASE; enter(sp,pc);
}

View File

@@ -0,0 +1,19 @@
#ifndef F407_BOOT_IMAGE_H
#define F407_BOOT_IMAGE_H
#include "boot_port.h"
#include <string.h>
static inline bool f407_vectors_valid(const uint8_t *image, uint32_t size)
{
uint32_t sp, pc;
if (size < 8 || size > F407_APP_SIZE) return false;
memcpy(&sp, image, 4);
memcpy(&pc, image + 4, 4);
return !(sp & 7) &&
((sp > 0x20000000UL && sp <= 0x20020000UL) ||
(sp > 0x10000000UL && sp <= 0x10010000UL)) &&
(pc & 1) && (pc & ~1UL) >= F407_APP_BASE &&
(pc & ~1UL) < F407_APP_BASE + size;
}
#endif

View File

@@ -0,0 +1,63 @@
#include "stm32f4xx_hal.h"
#include "boot_config.h"
#include "usb_cdc.h"
#include "boot_port.h"
#if F407_BOOT_HSE_HZ != HSE_VALUE
#error boot_config.h and HAL HSE_VALUE must describe the same crystal
#endif
#if (F407_BOOT_HSE_HZ % 1000000UL) || F407_BOOT_HSE_HZ < 2000000UL || F407_BOOT_HSE_HZ > 50000000UL
#error HSE must provide a valid integer PLLM divider (2..50 MHz)
#endif
uint32_t SystemCoreClock=16000000;
const uint8_t AHBPrescTable[16]={0,0,0,0,0,0,0,0,1,2,3,4,6,7,8,9};
const uint8_t APBPrescTable[8]={0,0,0,0,1,2,3,4};
static uint8_t usb_clock_ready;
static volatile uint32_t clock_fault;
void SystemInit(void) { SCB->CPACR|=0xFu<<20; SCB->VTOR=0x08000000; }
void SysTick_Handler(void) { HAL_IncTick(); }
static void board_fatal(uint32_t code) { clock_fault=code; for(;;) {} }
static void peripheral_clock(void)
{
RCC_OscInitTypeDef osc={0};
RCC_ClkInitTypeDef clk={0};
/* Разрешаем доступ к PWR и выбираем диапазон питания ядра перед повышением частоты. */
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
osc.OscillatorType=RCC_OSCILLATORTYPE_HSE;
osc.HSEState=RCC_HSE_ON;
/* Готовность HSE проверяется HAL с ограниченным временем ожидания, а не бесконечным циклом. */
if (HAL_RCC_OscConfig(&osc)==HAL_OK) usb_clock_ready=1;
else {
clock_fault=1;
osc.HSEState=RCC_HSE_OFF;
if (HAL_RCC_OscConfig(&osc)!=HAL_OK) board_fatal(3);
}
/* CPU 72 MHz, APB1 36 MHz, USB 48 MHz; USB requires the HSE crystal. */
osc.OscillatorType=RCC_OSCILLATORTYPE_HSI;
osc.HSIState=RCC_HSI_ON;
osc.HSICalibrationValue=RCC_HSICALIBRATION_DEFAULT;
osc.PLL.PLLState=RCC_PLL_ON;
osc.PLL.PLLSource=usb_clock_ready ? RCC_PLLSOURCE_HSE : RCC_PLLSOURCE_HSI;
osc.PLL.PLLM=usb_clock_ready ? F407_BOOT_HSE_HZ/1000000UL : 16;
osc.PLL.PLLN=144; osc.PLL.PLLP=RCC_PLLP_DIV2; osc.PLL.PLLQ=3;
if (HAL_RCC_OscConfig(&osc)!=HAL_OK) board_fatal(3);
clk.ClockType=RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
clk.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;
clk.AHBCLKDivider=RCC_SYSCLK_DIV1;
clk.APB1CLKDivider=RCC_HCLK_DIV2; clk.APB2CLKDivider=RCC_HCLK_DIV1;
/* HAL задаёт два такта ожидания Flash и перестраивает SysTick для новой частоты CPU. */
if (HAL_RCC_ClockConfig(&clk,FLASH_LATENCY_2)!=HAL_OK) board_fatal(3);
}
void boot_protocol_init(void);
void boot_protocol_poll(uint32_t now);
int main(void) {
/* Jump from reset, before initializing USB, DMA, clocks or interrupts. */
bool requested=f407_boot_requested();
if(!requested && f407_app_valid()) f407_jump_app();
if(HAL_Init()!=HAL_OK) board_fatal(3);
peripheral_clock(); boot_protocol_init();
if(usb_clock_ready) usb_cdc_init();
for(;;) { uint32_t now=HAL_GetTick(); usb_cdc_poll(now); boot_protocol_poll(now); }
}

View File

@@ -0,0 +1,13 @@
#ifndef F407_BOOT_PORT_H
#define F407_BOOT_PORT_H
#include "set_boot.h"
#define F407_APP_BASE 0x08010000UL
#define F407_APP_SIZE 0x00070000UL
#define F407_BOOT_REQUEST 0x1003U
extern const setp_boot_ops f407_boot_ops;
bool f407_app_valid(void);
bool f407_boot_requested(void);
void f407_request_boot(uint32_t now);
void f407_reset_poll(uint32_t now);
void f407_jump_app(void);
#endif

View File

@@ -0,0 +1,26 @@
#include "boot_port.h"
#include "stm32f4xx_hal.h"
#define REQUEST_MAGIC 0x42555342UL
static uint32_t reset_at;
static bool reset_pending;
static void backup_access(void)
{
__HAL_RCC_PWR_CLK_ENABLE();
PWR->CR |= PWR_CR_DBP;
__HAL_RCC_RTC_ENABLE();
}
bool f407_boot_requested(void)
{
bool requested;
backup_access(); requested=RTC->BKP0R==REQUEST_MAGIC;
RTC->BKP0R=0; return requested;
}
void f407_request_boot(uint32_t now)
{
backup_access(); RTC->BKP0R=REQUEST_MAGIC;
reset_at=now+500; reset_pending=true;
}
void f407_reset_poll(uint32_t now)
{
if(reset_pending && (int32_t)(now-reset_at)>=0) NVIC_SystemReset();
}

View File

@@ -0,0 +1,10 @@
#include "stm32f4xx_hal.h"
#include "boot_port.h"
#include "boot_config.h"
#define BOOT_OPS f407_boot_ops
#define BOOT_APP_BASE F407_APP_BASE
#define BOOT_APP_SIZE F407_APP_SIZE
#define BOOT_NODE F407_BOOT_NODE
#define BOOT_MODEL F407_BOOT_MODEL
#define BOOT_RESET() NVIC_SystemReset()
#include "../stm32-usb-boot/boot_usb_protocol.inc"

View File

@@ -0,0 +1,34 @@
#include "boot_image.h"
#include <assert.h>
#include <stdio.h>
int main(void)
{
uint32_t words[32] = {0x20020000, F407_APP_BASE + 9};
uint8_t *image = (uint8_t *)words;
/* A Keil image needs no metadata. Replacing its body must still boot. */
assert(f407_vectors_valid(image, F407_APP_SIZE));
words[10] ^= 1;
assert(f407_vectors_valid(image, F407_APP_SIZE));
words[0] = words[1] = 0xffffffff;
assert(!f407_vectors_valid(image, F407_APP_SIZE));
words[0] = 0x20020000; words[1] = 0x08000101;
assert(!f407_vectors_valid(image, F407_APP_SIZE));
words[1] = F407_APP_BASE + 8;
assert(!f407_vectors_valid(image, F407_APP_SIZE));
words[1] = F407_APP_BASE + 9; words[0] = 0x20020001;
assert(!f407_vectors_valid(image, F407_APP_SIZE));
words[0] = 0x10010000;
assert(f407_vectors_valid(image, F407_APP_SIZE));
assert(!f407_vectors_valid(image, 8));
assert(!f407_vectors_valid(image, 7));
assert(!f407_vectors_valid(image, F407_APP_SIZE + 1));
words[1] = F407_APP_BASE + F407_APP_SIZE + 1;
assert(!f407_vectors_valid(image, F407_APP_SIZE));
words[1] = F407_APP_BASE + 9;
/* Like climate, vectors alone cannot establish image completeness. */
memset(image + 8, 0xff, sizeof(words) - 8);
assert(f407_vectors_valid(image, F407_APP_SIZE));
puts("F407 boot: Keil image replacement and vector checks passed");
return 0;
}

View File

@@ -0,0 +1,53 @@
#include "set_wave_f407.h"
#include "stm32f4xx.h"
static void stop(void *context) {
unsigned limit=100000; (void)context;
RCC->AHB1ENR|=RCC_AHB1ENR_GPIOAEN|RCC_AHB1ENR_DMA1EN;
RCC->APB1ENR|=RCC_APB1ENR_DACEN|RCC_APB1ENR_TIM6EN;
(void)RCC->APB1ENR;
TIM6->CR1=0;
DAC->CR&=~(DAC_CR_DMAEN1|DAC_CR_TEN1);
DMA1_Stream5->CR&=~DMA_SxCR_EN;
while((DMA1_Stream5->CR&DMA_SxCR_EN)&&--limit) {}
/* Leave other DAC channel untouched. Channel 1 outputs zero on stop. */
DAC->DHR12R1=0;
DMA1->HIFCR=0xF40u;
DAC->SR=DAC_SR_DMAUDR1;
}
static int start(void *context,const uint16_t *samples,size_t count,uint32_t rate) {
set_wave_f407 *c=(set_wave_f407 *)context;
uint32_t ticks,psc,arr; uintptr_t address=(uintptr_t)samples;
if(!c||!rate||rate>SET_WAVE_MAX_RATE||count<2||count>SET_WAVE_MAX||c->timer_hz%rate
||address<0x20000000u||address+count*2>0x20020000u) return 1;
ticks=c->timer_hz/rate;
for(psc=1;psc<=65536;++psc) if(ticks%psc==0&&ticks/psc<=65536) break;
if(psc>65536||ticks<psc) return 1;
arr=ticks/psc;
stop(context);
if(DMA1_Stream5->CR&DMA_SxCR_EN) return 1;
GPIOA->MODER|=3u<<8;
GPIOA->PUPDR&=~(3u<<8);
TIM6->PSC=psc-1;TIM6->ARR=arr-1;TIM6->CNT=0;
TIM6->CR2=0;TIM6->EGR=TIM_EGR_UG;TIM6->SR=0;
TIM6->CR2=TIM_CR2_MMS_1; /* update -> TRGO */
DMA1_Stream5->PAR=(uint32_t)&DAC->DHR12R1;
DMA1_Stream5->M0AR=(uint32_t)samples;DMA1_Stream5->NDTR=(uint32_t)count;
DMA1_Stream5->FCR=0;
DMA1_Stream5->CR=(7u<<25)|DMA_SxCR_DIR_0|DMA_SxCR_MINC|DMA_SxCR_CIRC
|DMA_SxCR_PSIZE_0|DMA_SxCR_MSIZE_0|DMA_SxCR_PL_1;
/* Prime the previous period's last sample; DMA supplies the next value
* after each trigger. Cyclic order is last,0,1,... with one priming tick. */
DAC->CR&=~0xFFFFu;
DAC->DHR12R1=samples[count-1];
DAC->CR|=DAC_CR_EN1|DAC_CR_TEN1|DAC_CR_DMAEN1; /* TSEL1=0: TIM6 */
DMA1_Stream5->CR|=DMA_SxCR_EN;
TIM6->CR1=TIM_CR1_CEN;
return 0;
}
set_wave_port set_wave_f407_port(set_wave_f407 *context) {
set_wave_port port={context,start,stop};return port;
}
int set_wave_f407_fault(void) {
return (DMA1->HISR&(DMA_HISR_TEIF5|DMA_HISR_DMEIF5|DMA_HISR_FEIF5))
||(DAC->SR&DAC_SR_DMAUDR1);
}

View File

@@ -0,0 +1,12 @@
#ifndef SET_WAVE_F407_H
#define SET_WAVE_F407_H
#include "../../include/set_wavegen.h"
/* Owns PA4, DAC channel 1, TIM6 and DMA1 stream 5/channel 7.
* timer_hz is TIM6 input clock (twice APB1 when APB1 prescaler !=1).
* Rate: 1..SET_WAVE_MAX_RATE (1 MS/s), exact divisors of timer_hz only.
* Context and sample buffer must outlive playback; buffer must be in SRAM. */
typedef struct { uint32_t timer_hz; } set_wave_f407;
set_wave_port set_wave_f407_port(set_wave_f407 *context);
/* Nonzero on DMA error or DAC underrun; caller stops state on failure. */
int set_wave_f407_fault(void);
#endif

View File

@@ -0,0 +1,34 @@
# STM32G474RB emulator peripheral port
Board adapters for the common UMP/TMS/IGBT server and waveform generator.
All peripheral source files have detailed Russian comments matching the F407 port.
The shared algorithms, codecs and register map stay in `../../src`.
- `board.c`: application VTOR 0x08010000; HSI16/PLL 160 MHz, APB1 80 MHz;
HSI48/CRS USB; board GPIO and 1 ms model ticks.
- `transport.c`: USART2 PA2/PA3 RTU, optional RS485 DE PB15;
FDCAN1 PB8/PB9, Classic CAN, extended IDs, bounded polling and TX timeout.
- `wave_hal.c`: PA4 DAC1_CH1, TIM6, DMA1_Channel1 with DMAMUX DAC1 request;
2..4096 samples in SRAM1/2, exact timer division, maximum 1 MS/s.
- `main.c`: service ordering and deferred reboot into the G474 USB bootloader.
IGBT input bits 0..7 use PB0/PB1/PB2/PB10/PB11/PB12/PB13/PB14;
output bits use PC0..PC7. SWD, SWO and USB pins are preserved.
Clock, addresses and RS485 mode come from the board-supplied `board_config.h`.
The default internal clock does not guarantee CAN or waveform accuracy over
all temperature conditions; use a verified 24 MHz HSE source when required.
The consumer supplies `board.h`, `transport.h`, `wave_hal.h`, `emulator.h`,
G4 HAL configuration and the common model composition. The reference project
is `Nucleo_G474RB` in the emulator repository. Compile `EMU_CAPACITY=400`:
each 32000-byte archive bank fits the MCU's 32 KiB CCM/96 KiB SRAM layout.
DMA samples stay in SRAM1/2. No F407 peripheral implementation is compiled.
Link at 0x08010000 with a 64 KiB Flash limit; install the separate G474RB
USB bootloader first. Require 128 KiB silicon, DBANK=0, BFB2=0 and no bank swap.
The firmware rejects mismatched geometry before peripheral initialization.
Reuse `../stm32g474-usb-boot/boot_usb_port.c` and `boot_request.c` with the
shared USB CDC transport. Device model: STM32G474RB-EMU-EXPERIMENT.
Host tests validate the shared software; peripheral timing and electrical
behavior have not yet been validated on a physical board.

View File

@@ -0,0 +1,109 @@
/**
* Аппаратный порт STM32G474RB: векторы приложения, тактирование и GPIO.
* Алгоритмы UMP/TMS/IGBT находятся в общем C-ядре templates; здесь только пины.
*/
#include "board.h"
#include "board_config.h"
#include "transport.h"
#include "emulator.h"
#include "igbt_emulator.h"
#include "stm32g4xx_hal.h"
volatile uint32_t clock_fault;
uint32_t SystemCoreClock=16000000u;
const uint8_t AHBPrescTable[16]={0,0,0,0,0,0,0,0,1,2,3,4,6,7,8,9};
const uint8_t APBPrescTable[8]={0,0,0,0,1,2,3,4};
static uint8_t usb_clock_ready;
/* Восемь входов IGBT: PB0,PB1,PB2,PB10..PB14. PB3/SWO и PB8/PB9 CAN свободны. */
static const uint16_t input_pins[8]={GPIO_PIN_0,GPIO_PIN_1,GPIO_PIN_2,
GPIO_PIN_10,GPIO_PIN_11,GPIO_PIN_12,GPIO_PIN_13,GPIO_PIN_14};
/* До C runtime разрешены только регистровые операции; глобальные данные ещё не готовы. */
void SystemInit(void) { SCB->CPACR|=0xFu<<20; SCB->VTOR=0x08010000u; }
void SystemCoreClockUpdate(void) {
RCC_ClkInitTypeDef config; uint32_t latency;
HAL_RCC_GetClockConfig(&config,&latency);
SystemCoreClock=HAL_RCC_GetSysClockFreq() >> AHBPrescTable[config.AHBCLKDivider >> RCC_CFGR_HPRE_Pos];
}
/* ISR только считает миллисекунды: протоколы и модели обслуживаются главным циклом. */
void SysTick_Handler(void) { HAL_IncTick(); }
void board_fatal(uint32_t code) { clock_fault=code; for (;;) {} }
static void peripheral_clock(void)
{
RCC_OscInitTypeDef osc={0}; RCC_ClkInitTypeDef clk={0};
RCC_PeriphCLKInitTypeDef peripheral={0}; RCC_CRSInitTypeDef crs={0};
__HAL_RCC_PWR_CLK_ENABLE();
/* Для 160 МГц нужен Range1 Boost; Flash 4 wait states по таблице ST. */
if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1_BOOST)!=HAL_OK) board_fatal(3);
osc.OscillatorType=RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSI48;
osc.HSIState=RCC_HSI_ON; osc.HSICalibrationValue=RCC_HSICALIBRATION_DEFAULT;
osc.HSI48State=RCC_HSI48_ON;
osc.PLL.PLLState=RCC_PLL_ON; osc.PLL.PLLSource=RCC_PLLSOURCE_HSI;
/* HSI16 / 4 * 80 / 2 = 160 МГц; PLLQ также 160 МГц, FDCAN берёт PCLK1. */
osc.PLL.PLLM=RCC_PLLM_DIV4;
#if EMU_USE_HSE
#if EMU_HSE_HZ != 24000000u
#error This board profile supports 24 MHz HSE or internal HSI16
#endif
osc.OscillatorType|=RCC_OSCILLATORTYPE_HSE;
osc.HSEState=EMU_HSE_BYPASS ? RCC_HSE_BYPASS : RCC_HSE_ON;
osc.PLL.PLLSource=RCC_PLLSOURCE_HSE; osc.PLL.PLLM=RCC_PLLM_DIV6;
#endif
osc.PLL.PLLN=80; osc.PLL.PLLP=RCC_PLLP_DIV2;
osc.PLL.PLLQ=RCC_PLLQ_DIV2; osc.PLL.PLLR=RCC_PLLR_DIV2;
if (HAL_RCC_OscConfig(&osc)!=HAL_OK) board_fatal(3);
clk.ClockType=RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
clk.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK; clk.AHBCLKDivider=RCC_SYSCLK_DIV1;
clk.APB1CLKDivider=RCC_HCLK_DIV2; clk.APB2CLKDivider=RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&clk,FLASH_LATENCY_4)!=HAL_OK) board_fatal(3);
/* USB не зависит от PLL/HSE. USART2 и FDCAN получают 80 МГц PCLK1. */
peripheral.PeriphClockSelection=RCC_PERIPHCLK_USB|RCC_PERIPHCLK_FDCAN|RCC_PERIPHCLK_USART2;
peripheral.UsbClockSelection=RCC_USBCLKSOURCE_HSI48;
peripheral.FdcanClockSelection=RCC_FDCANCLKSOURCE_PCLK1;
peripheral.Usart2ClockSelection=RCC_USART2CLKSOURCE_PCLK1;
if (HAL_RCCEx_PeriphCLKConfig(&peripheral)!=HAL_OK) board_fatal(3);
__HAL_RCC_CRS_CLK_ENABLE();
crs.Prescaler=RCC_CRS_SYNC_DIV1; crs.Source=RCC_CRS_SYNC_SOURCE_USB;
crs.Polarity=RCC_CRS_SYNC_POLARITY_RISING;
crs.ReloadValue=__HAL_RCC_CRS_RELOADVALUE_CALCULATE(48000000u,1000u);
crs.ErrorLimitValue=RCC_CRS_ERRORLIMIT_DEFAULT;
crs.HSI48CalibrationValue=RCC_CRS_HSI48CALIBRATION_DEFAULT;
HAL_RCCEx_CRSConfig(&crs); usb_clock_ready=1;
}
void board_init(void)
{
GPIO_InitTypeDef gpio={0};
/* Этот образ связан с RB-загрузчиком: 128 КБ Flash, непрерывная карта DBANK=0. */
__HAL_RCC_SYSCFG_CLK_ENABLE();
if (*(const uint16_t *)FLASHSIZE_BASE!=128u ||
(FLASH->OPTR&(FLASH_OPTR_DBANK|FLASH_OPTR_BFB2)) ||
(SYSCFG->MEMRMP&SYSCFG_MEMRMP_FB_MODE)) board_fatal(5);
peripheral_clock();
__HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_USART2_CLK_ENABLE(); __HAL_RCC_FDCAN_CLK_ENABLE();
/* ST-LINK VCP физически подключён к PA2/PA3; выбираем USART2 AF7, формат 8N1. */
gpio.Pin=GPIO_PIN_2|GPIO_PIN_3; gpio.Mode=GPIO_MODE_AF_PP;
gpio.Speed=GPIO_SPEED_FREQ_VERY_HIGH; gpio.Pull=GPIO_PULLUP;
gpio.Alternate=GPIO_AF7_USART2; HAL_GPIO_Init(GPIOA,&gpio);
gpio.Pin=GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14;
gpio.Mode=GPIO_MODE_INPUT; gpio.Pull=GPIO_PULLDOWN; HAL_GPIO_Init(GPIOB,&gpio);
/* PC0..PC7 — восемь ответов IGBT. Устанавливаем нули до включения выходов. */
gpio.Pin=0xff; gpio.Mode=GPIO_MODE_OUTPUT_PP; gpio.Pull=GPIO_NOPULL;
HAL_GPIO_WritePin(GPIOC,0xff,GPIO_PIN_RESET); HAL_GPIO_Init(GPIOC,&gpio);
#if EMU_RS485_DE
gpio.Pin=GPIO_PIN_15; HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_RESET);
HAL_GPIO_Init(GPIOB,&gpio);
#endif
/* FDCAN1 Classic CAN: PB8 RX, PB9 TX. PA11/PA12 остаются для USB FS. */
gpio.Pin=GPIO_PIN_8|GPIO_PIN_9; gpio.Mode=GPIO_MODE_AF_PP;
gpio.Pull=GPIO_PULLUP; gpio.Alternate=GPIO_AF9_FDCAN1; HAL_GPIO_Init(GPIOB,&gpio);
transport_init();
}
uint8_t board_usb_ready(void) { return usb_clock_ready; }
/* Один шаг 1 мс. Бит n соответствует элементу input_pins[n], выход — PCn. */
void board_tick(void)
{
uint8_t inputs=0,outputs; unsigned i;
for(i=0;i<8;++i) if(HAL_GPIO_ReadPin(GPIOB,input_pins[i])==GPIO_PIN_SET) inputs|=(uint8_t)(1u<<i);
igbt_emu_set_tx(inputs); emu_tick(); outputs=igbt_emu_get_rx();
HAL_GPIO_WritePin(GPIOC,(uint16_t)(~outputs&0xffu),GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOC,outputs,GPIO_PIN_SET);
}

View File

@@ -0,0 +1,39 @@
/**
* Точка входа прошивки. Здесь только порядок запуска и совместное обслуживание модулей.
* Настройки оборудования находятся в board.c, обмен — в transport.c, генератор — в wave_hal.c.
* Все модели и разбор команд выполняются в главном цикле; ISR обслуживают HAL и передачу байтов.
*/
#include "board.h"
#include "transport.h"
#include "emulator.h"
#include "usb_cdc.h"
#include "wave_output.h"
#include "stm32g4xx_hal.h"
#if defined(EMU_USB_BOOT_ENABLE)
#include "boot_port.h"
#endif
int main(void)
{
uint32_t serviced;
/* HAL запускает миллисекундный SysTick до настройки PLL: его счётчик нужен таймаутам HAL. */
if (HAL_Init()!=HAL_OK) board_fatal(3);
/* Сначала создаём модели и значения по умолчанию, затем настраиваем плату с этими параметрами. */
emu_init(); board_init();
/* Начинаем модельное время после настройки платы, не отрабатывая задержки её инициализации. */
serviced=HAL_GetTick();
/* USB использует отдельный HSI48 с CRS: внешний кварц для USB не нужен. */
if (board_usb_ready()) usb_cdc_init();
for (;;) {
uint32_t now=HAL_GetTick();
/* Один пропущенный миллисекундный шаг за проход. Между шагами всегда обслуживаем связь,
* чтобы догоняющая обработка модели не вытесняла UART/CAN/USB. */
if (serviced!=now) { ++serviced; board_tick(); }
transport_uart_poll(now); transport_can_poll(now);
usb_cdc_poll(now); wave_output_poll();
#if defined(EMU_USB_BOOT_ENABLE)
g474_reset_poll(now);
#endif
}
}

View File

@@ -0,0 +1,163 @@
/**
* USART2/VCP и FDCAN1 в режиме Classic CAN. Формат запросов совпадает с F407:
* UART RTU, CAN extended ID 0xBA0000+mode-1; ответы 0xBA0020+mode-1.
* Здесь нет второго кодека: все байты обрабатывают emu_rtu/emu_can из templates.
*/
#include "transport.h"
#include "board.h"
#include "board_config.h"
#include "emulator.h"
#include "stm32g4xx_hal.h"
static UART_HandleTypeDef uart;
static FDCAN_HandleTypeDef can;
static volatile uint8_t transmitting;
static uint8_t can_ready;
volatile uint32_t uart_errors,uart_overflows,can_timeouts,can_overflows;
void transport_init(void)
{
FDCAN_FilterTypeDef filter={0};
uart.Instance=USART2; uart.Init.BaudRate=emu_uart_baud();
uart.Init.WordLength=UART_WORDLENGTH_8B; uart.Init.StopBits=UART_STOPBITS_1;
uart.Init.Parity=UART_PARITY_NONE; uart.Init.Mode=UART_MODE_TX_RX;
uart.Init.HwFlowCtl=UART_HWCONTROL_NONE; uart.Init.OverSampling=UART_OVERSAMPLING_16;
uart.Init.ClockPrescaler=UART_PRESCALER_DIV1;
if(HAL_UART_Init(&uart)!=HAL_OK) board_fatal(4);
HAL_NVIC_SetPriority(USART2_IRQn,6,0); HAL_NVIC_EnableIRQ(USART2_IRQn);
can.Instance=FDCAN1; can.Init.ClockDivider=FDCAN_CLOCK_DIV1;
can.Init.FrameFormat=FDCAN_FRAME_CLASSIC; can.Init.Mode=FDCAN_MODE_NORMAL;
can.Init.AutoRetransmission=ENABLE; can.Init.TransmitPause=DISABLE;
can.Init.ProtocolException=DISABLE;
/* PCLK1=80 МГц, 16 квантов/бит, prescaler=20 для 250 кбит/с.
* Sample point=(1+13)/16=87,5%; CAN FD/BRS на проводе не используется. */
if(HAL_RCC_GetPCLK1Freq()%(EMU_CAN_BITRATE*16u)) board_fatal(2);
can.Init.NominalPrescaler=HAL_RCC_GetPCLK1Freq()/(EMU_CAN_BITRATE*16u);
can.Init.NominalSyncJumpWidth=1; can.Init.NominalTimeSeg1=13; can.Init.NominalTimeSeg2=2;
can.Init.DataPrescaler=1; can.Init.DataSyncJumpWidth=1;
can.Init.DataTimeSeg1=1; can.Init.DataTimeSeg2=1;
can.Init.StdFiltersNbr=0; can.Init.ExtFiltersNbr=1;
can.Init.TxFifoQueueMode=FDCAN_TX_FIFO_OPERATION;
if(HAL_FDCAN_Init(&can)!=HAL_OK) board_fatal(2);
filter.IdType=FDCAN_EXTENDED_ID; filter.FilterIndex=0;
filter.FilterType=FDCAN_FILTER_MASK; filter.FilterConfig=FDCAN_FILTER_TO_RXFIFO0;
filter.FilterID1=0xBA0000u+EMU_CAN_MODE-1; filter.FilterID2=0x1fffffffu;
/* Отдельно запрещаем все стандартные, посторонние и remote-кадры. */
if(HAL_FDCAN_ConfigFilter(&can,&filter)!=HAL_OK ||
HAL_FDCAN_ConfigGlobalFilter(&can,FDCAN_REJECT,FDCAN_REJECT,
FDCAN_REJECT_REMOTE,FDCAN_REJECT_REMOTE)!=HAL_OK) board_fatal(2);
can_ready=(HAL_FDCAN_Start(&can)==HAL_OK);
if(!can_ready) clock_fault=2;
}
/* Опрос ограничен одним RX и одним TX за проход. Статический снимок ответа
* сохраняется до завершения всех фрагментов; новый запрос во время ответа не принимается. */
void transport_can_poll(uint32_t now)
{
static uint16_t words[124],count,fragment;
static uint8_t tag,error,pending;
static uint32_t sent_at,mailbox;
FDCAN_RxHeaderTypeDef rx; FDCAN_TxHeaderTypeDef tx={0};
uint8_t data[64]; /* HAL может скопировать до 64 байт чужого FD-кадра до проверки формата. */
if(!can_ready) return;
/* FDCAN не делает bxCAN AutoBusOff. Снятие INIT запускает аппаратное ожидание
* 129 последовательностей recessive перед восстановлением шины. */
if((can.Instance->PSR&FDCAN_PSR_BO) && (can.Instance->CCCR&FDCAN_CCCR_INIT)) {
CLEAR_BIT(can.Instance->CCCR,FDCAN_CCCR_INIT);
count=0;
}
if(pending) {
if(!HAL_FDCAN_IsTxBufferMessagePending(&can,mailbox)) {
if(pending==1 && !(can.Instance->TXBTO&mailbox)) {count=0;++can_timeouts;}
pending=0;
} else if(pending==1 && (uint32_t)(now-sent_at)>=250u) {
(void)HAL_FDCAN_AbortTxRequest(&can,mailbox);
count=0;pending=2;++can_timeouts;
}
}
if(__HAL_FDCAN_GET_FLAG(&can,FDCAN_FLAG_RX_FIFO0_MESSAGE_LOST)) {
__HAL_FDCAN_CLEAR_FLAG(&can,FDCAN_FLAG_RX_FIFO0_MESSAGE_LOST);++can_overflows;
}
if(HAL_FDCAN_GetRxFifoFillLevel(&can,FDCAN_RX_FIFO0) &&
HAL_FDCAN_GetRxMessage(&can,FDCAN_RX_FIFO0,&rx,data)==HAL_OK) {
if(rx.DataLength==FDCAN_DLC_BYTES_8 && rx.IdType==FDCAN_EXTENDED_ID &&
rx.RxFrameType==FDCAN_DATA_FRAME && rx.FDFormat==FDCAN_CLASSIC_CAN &&
rx.Identifier==0xBA0000u+EMU_CAN_MODE-1 && !count && !pending) {
count=emu_can(data,words,&tag,&error);fragment=0;
}
}
if(count && !pending && HAL_FDCAN_GetTxFifoFreeLevel(&can)) {
emu_can_fragment(words,fragment,tag,error,data);
tx.Identifier=0xBA0020u+EMU_CAN_MODE-1;tx.IdType=FDCAN_EXTENDED_ID;
tx.TxFrameType=FDCAN_DATA_FRAME;tx.DataLength=FDCAN_DLC_BYTES_8;
tx.ErrorStateIndicator=FDCAN_ESI_ACTIVE;tx.BitRateSwitch=FDCAN_BRS_OFF;
tx.FDFormat=FDCAN_CLASSIC_CAN;tx.TxEventFifoControl=FDCAN_NO_TX_EVENTS;
if(HAL_FDCAN_AddMessageToTxFifoQ(&can,&tx,data)==HAL_OK) {
mailbox=HAL_FDCAN_GetLatestTxFifoQRequestBuffer(&can);
pending=1;sent_at=now;
if(++fragment>=count/2)count=0;
} else {count=0;++can_timeouts;}
}
}
/* HAL обслуживает TXE/TC и вызывает callback завершения; прикладной протокол в ISR не выполняется. */
void USART2_IRQHandler(void) { HAL_UART_IRQHandler(&uart); }
/* Вызывается HAL после TC: последний стоп-бит уже вышел на линию, можно отключить передатчик RS-485.
* Также используется локально для возврата в приём, если запуск передачи завершился ошибкой. */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *h)
{
if (h!=&uart) return;
#if EMU_RS485_DE
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_RESET);
#endif
transmitting=0;
}
/* Принимает доступный байт без ожидания, накапливает кадр и определяет конец по паузе RTU.
* rx/tx имеют по 256 байт; invalid запрещает выполнение всего повреждённого кадра.
* now и last_rx измеряются в миллисекундах; переданный HAL буфер tx живёт до завершения ISR. */
void transport_uart_poll(uint32_t now)
{
static uint8_t rx[256], tx[256], invalid;
static uint16_t received;
static uint32_t last_rx;
uint32_t baud=uart.Init.BaudRate;
/* Изменение скорости применяется после окончания ответа на старой скорости.
* Незавершённый входной кадр сбрасывается, чтобы не объединить байты разных скоростей. */
if (!transmitting && baud!=emu_uart_baud()) {
uart.Init.BaudRate=emu_uart_baud();
if (HAL_UART_Init(&uart)!=HAL_OK) board_fatal(4);
received=0; invalid=0; return;
}
if (__HAL_UART_GET_FLAG(&uart,UART_FLAG_ORE) || __HAL_UART_GET_FLAG(&uart,UART_FLAG_FE) ||
__HAL_UART_GET_FLAG(&uart,UART_FLAG_NE) || __HAL_UART_GET_FLAG(&uart,UART_FLAG_PE)) {
/* На G4 ошибки сбрасываются записью ICR. RQR удаляет ошибочный байт;
* чтение SR/DR, применяемое на F407, здесь не используется. */
__HAL_UART_CLEAR_FLAG(&uart,UART_CLEAR_PEF|UART_CLEAR_FEF|UART_CLEAR_NEF|UART_CLEAR_OREF);
__HAL_UART_SEND_REQ(&uart,UART_RXDATA_FLUSH_REQUEST);
invalid=1; ++uart_errors; last_rx=now;
} else if (__HAL_UART_GET_FLAG(&uart,UART_FLAG_RXNE)) {
uint8_t b;
/* RXNE уже установлен; timeout=0 не допускает ожидания следующего байта.
* Во время передачи входные байты вычитываются и отбрасываются, чтобы не принять собственное эхо. */
if (HAL_UART_Receive(&uart,&b,1,0)==HAL_OK && !transmitting) {
if (received<sizeof(rx)) rx[received++]=b;
else { invalid=1; ++uart_overflows; }
last_rx=now;
}
}
/* At >19200 baud RTU t3.5 is 1.75 ms; 3 ticks guarantees >=2 ms. */
/* Для 8N1 ниже порога считаем 3,5 символа по 10 бит с округлением и запасом одного тика.
* Выше 19200 бод используем 3 тика: с учётом фазы SysTick фактическая пауза не меньше 2 мс. */
if (received && !transmitting && (uint32_t)(now-last_rx)>=
(baud>19200u ? 3u : (35000u+baud-1)/baud+1u)) {
uint16_t length=invalid ? 0 : emu_rtu(rx,received,tx);
received=0; invalid=0;
if (length) {
transmitting=1;
#if EMU_RS485_DE
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_SET);
#endif
/* Передача асинхронная: poll возвращается сразу. tx не перезаписывается, пока transmitting != 0. */
if (HAL_UART_Transmit_IT(&uart,tx,length)!=HAL_OK) {
++uart_errors; HAL_UART_TxCpltCallback(&uart);
}
}
}
}

View File

@@ -0,0 +1,116 @@
/**
* Аппаратный исполнитель генератора: таблица uint16_t в SRAM -> DMA1 -> DAC1/PA4.
* TIM6 задаёт частоту выборок аппаратными событиями TRGO; CPU не отправляет каждый отсчёт.
* Модуль владеет только DAC channel 1, TIM6 и DMA1 channel 1 и DMAMUX request DAC1_CHANNEL1.
*/
/* HAL backend owned by this board; the shared protocol remains hardware independent. */
#include "wave_hal.h"
#include "stm32g4xx_hal.h"
static DAC_HandleTypeDef dac;
static DMA_HandleTypeDef dma;
static TIM_HandleTypeDef timer;
static uint8_t initialized;
/* Callback остановки set_wave_port. context не используется: имеется один статический комплект HAL handles.
* Сначала останавливаем источник триггеров, затем DMA/DAC. После первого запуска держим PA4 на нулевом коде;
* до инициализации функция ничего не делает. Второй канал DAC не перенастраивается. */
static void stop(void *context)
{
(void)context;
if (!initialized) return;
(void)HAL_TIM_Base_Stop(&timer);
(void)HAL_DAC_Stop_DMA(&dac,DAC_CHANNEL_1);
/* Keep channel 1 driving zero after stop; channel 2 is untouched. */
{
DAC_ChannelConfTypeDef config={0};
/* G4 при AHB=160 МГц требует HFSEL; выход направляем на внешний PA4. */
config.DAC_HighFrequency=DAC_HIGH_FREQUENCY_INTERFACE_MODE_AUTOMATIC;
config.DAC_ConnectOnChipPeripheral=DAC_CHIPCONNECT_EXTERNAL;
config.DAC_UserTrimming=DAC_TRIMMING_FACTORY;
config.DAC_Trigger=DAC_TRIGGER_NONE; config.DAC_OutputBuffer=DAC_OUTPUTBUFFER_ENABLE;
(void)HAL_DAC_ConfigChannel(&dac,&config,DAC_CHANNEL_1);
(void)HAL_DAC_SetValue(&dac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,0);
(void)HAL_DAC_Start(&dac,DAC_CHANNEL_1);
}
}
/* Callback запуска: samples — таблица кодов DAC, count — число отсчётов, rate — выборок/с.
* Таблица должна оставаться действительной и неизменной до stop(). Возвращает 0 при успехе, 1 при ошибке.
* Проверки формы сигнала выполняет общий генератор; здесь проверяется аппаратная выполнимость. */
static int start(void *context,const uint16_t *samples,size_t count,uint32_t rate)
{
GPIO_InitTypeDef gpio={0};
DAC_ChannelConfTypeDef config={0};
/* G4 при AHB=160 МГц требует HFSEL; выход направляем на внешний PA4. */
config.DAC_HighFrequency=DAC_HIGH_FREQUENCY_INTERFACE_MODE_AUTOMATIC;
config.DAC_ConnectOnChipPeripheral=DAC_CHIPCONNECT_EXTERNAL;
config.DAC_UserTrimming=DAC_TRIMMING_FACTORY;
TIM_MasterConfigTypeDef master={0};
/* При делителе APB1=2 таймер получает удвоенную частоту PCLK1, то есть 160 МГц.
* Эта формула соответствует дереву тактирования из board.c. */
uint32_t ticks,psc,clock=HAL_RCC_GetPCLK1Freq()*2u;
uintptr_t address=(uintptr_t)samples;
(void)context;
/* Разрешены 2..SET_WAVE_MAX отсчётов, частота до 1 МГц и только точное деление частоты таймера.
* Для DMA используем SRAM1/2: буфер должен лежать в 0x20000000..0x20017FFF; CCM оставляем архиву. */
if (!rate || rate>SET_WAVE_MAX_RATE || count<2 || count>SET_WAVE_MAX || clock%rate ||
address<0x20000000u || address+count*2>0x20018000u) return 1;
ticks=clock/rate;
/* Подбираем целые (PSC+1) и (ARR+1), оба не больше 65536.
* Частота выборок = timer_clock / ((PSC+1)*(ARR+1)); в регистры HAL передаёт значения минус один. */
for (psc=1;psc<=65536;++psc) if (ticks%psc==0 && ticks/psc<=65536) break;
if (psc>65536 || ticks<psc) return 1;
stop(context);
__HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_DMA1_CLK_ENABLE();
__HAL_RCC_DMAMUX1_CLK_ENABLE(); __HAL_RCC_DAC1_CLK_ENABLE(); __HAL_RCC_TIM6_CLK_ENABLE();
gpio.Pin=GPIO_PIN_4; gpio.Mode=GPIO_MODE_ANALOG; gpio.Pull=GPIO_NOPULL;
HAL_GPIO_Init(GPIOA,&gpio);
timer.Instance=TIM6; timer.Init.Prescaler=psc-1; timer.Init.Period=ticks/psc-1;
timer.Init.CounterMode=TIM_COUNTERMODE_UP; timer.Init.AutoReloadPreload=TIM_AUTORELOAD_PRELOAD_DISABLE;
/* Remove TRGO before Base_Init generates its prescaler update event. */
master.MasterOutputTrigger=TIM_TRGO_RESET;
master.MasterSlaveMode=TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&timer,&master)!=HAL_OK ||
HAL_TIM_Base_Init(&timer)!=HAL_OK) return 1;
__HAL_TIM_SET_COUNTER(&timer,0); __HAL_TIM_CLEAR_FLAG(&timer,TIM_FLAG_UPDATE);
master.MasterOutputTrigger=TIM_TRGO_UPDATE;
if (HAL_TIMEx_MasterConfigSynchronization(&timer,&master)!=HAL_OK) return 1;
/* DMA пишет полуслова в фиксированный регистр DAC, последовательно читая таблицу.
* Circular автоматически возвращает адрес к началу; высокий приоритет уменьшает риск underrun. */
dma.Instance=DMA1_Channel1; dma.Init.Request=DMA_REQUEST_DAC1_CHANNEL1;
dma.Init.Direction=DMA_MEMORY_TO_PERIPH; dma.Init.PeriphInc=DMA_PINC_DISABLE;
dma.Init.MemInc=DMA_MINC_ENABLE; dma.Init.PeriphDataAlignment=DMA_PDATAALIGN_HALFWORD;
dma.Init.MemDataAlignment=DMA_MDATAALIGN_HALFWORD; dma.Init.Mode=DMA_CIRCULAR;
dma.Init.Priority=DMA_PRIORITY_HIGH;
if (HAL_DMA_Init(&dma)!=HAL_OK) return 1;
/* Связываем DAC с DMA handle: HAL использует эту связь при запуске и остановке передачи. */
dac.Instance=DAC1; __HAL_LINKDMA(&dac,DMA_Handle1,dma);
if (HAL_DAC_Init(&dac)!=HAL_OK) return 1;
initialized=1;
config.DAC_Trigger=DAC_TRIGGER_T6_TRGO; config.DAC_OutputBuffer=DAC_OUTPUTBUFFER_ENABLE;
if (HAL_DAC_ConfigChannel(&dac,&config,DAC_CHANNEL_1)!=HAL_OK) goto fail;
__HAL_DAC_CLEAR_FLAG(&dac,DAC_FLAG_DMAUDR1);
/* DMA fills the next sample after each trigger, preserving cyclic phase. */
/* Первым триггером выводится заранее записанный последний отсчёт; далее идут 0,1,... из DMA.
* Таймер запускается последним, когда DAC и DMA уже готовы. Приведение к uint32_t* требуется API HAL,
* но размер реальной передачи задаётся DMA как HALFWORD (16 бит). */
if (HAL_DAC_SetValue(&dac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,samples[count-1])!=HAL_OK ||
HAL_DAC_Start_DMA(&dac,DAC_CHANNEL_1,(uint32_t *)samples,(uint32_t)count,DAC_ALIGN_12B_R)!=HAL_OK ||
HAL_TIM_Base_Start(&timer)!=HAL_OK) goto fail;
return 0;
/* Если запуск после инициализации DAC не завершился, возвращаем выход к нулю и сообщаем ошибку. */
fail:
stop(context); return 1;
}
/* Возвращает таблицу callback-функций для общего генератора; само оборудование ещё не запускается. */
set_wave_port wave_hal_port(void)
{
set_wave_port port={0,start,stop}; return port;
}
/* Опрос ошибки канала DMA и underrun DAC. У G4 нет F4 stream FIFO/direct-mode flags.
* NVIC-прерывания DMA/DAC здесь не включены: флаги проверяет основной цикл, остановку выполняет wave_output_poll(). */
int wave_hal_fault(void)
{
return initialized && (__HAL_DMA_GET_FLAG(&dma,DMA_FLAG_TE1) ||
__HAL_DAC_GET_FLAG(&dac,DAC_FLAG_DMAUDR1));
}

View File

@@ -0,0 +1,47 @@
# STM32G474xB USB boot port
SET v2 USB CDC bootloader for STM32G474 with 128 KiB Flash. Uses HSI16 for
the CPU and HSI48 with CRS USB SOF synchronization for USB; no HSE required.
Dedicated USB pins: PA11/PA12. The board must supply VDDUSB correctly.
Compile the shared `src/set_protocol.c`, `src/set_firmware.c`, `src/set_boot.c`,
this port's `boot_main.c`, `boot_flash.c`, `boot_request.c`, `boot_usb_stream.c`
and `boot_usb_port.c`. Supply G4 CMSIS/HAL/startup, ST USB Device CDC and the
board's CDC byte transport (`usb_cdc.h`, `usb_setp_stream.h`).
Copy `boot_config.template.h` as `boot_config.h`. The wrapper uses the common
`../stm32-usb-boot/boot_usb_protocol.inc`; retain that directory when importing.
For the shared emulator CDC transport, set `EMU_USB_SET_V2=1`,
`USB_CDC_IRQn=USB_LP_IRQn` and optionally `USB_CDC_PRODUCT_NAME`.
Provide `usbd_conf.h` using `stm32g4xx_hal.h`. The low-level port allocates
non-overlapping PMA buffers for EP0, CDC IN/OUT and notifications.
Link boot code below `0x0800F000`, reserve `0x0800F000..0x0800FFFF` for metadata,
and link the application at `0x08010000` with VTOR at that address.
Application capacity is 64 KiB, ending at `0x08020000` (exclusive).
Flash size must be 128 KiB and DBANK must be zero (single bank, 4 KiB pages).
BFB2 and FB_MODE bank swapping must be disabled. Unsupported geometry is
rejected before erase. On xB in dual-bank mode the upper 64 KiB starts at
`0x08040000`, leaving a hole at the application's link address. Therefore this
profile deliberately rejects dual-bank mode. Set DBANK=0 via SWD before the
initial installation, then program both bootloader and application afresh.
The port does not modify Option Bytes.
HAL doubleword programming plus an eight-byte staging buffer handles arbitrary
SET block sizes without programming ECC words twice. Only the final word is
padded with FF; CRC covers the declared image size. END checks written Flash
and vectors, commits metadata last; ACTIVATE defers reset by 500 ms for the reply.
The reset policy matches F407: valid vectors alone allow SWD-installed images.
Metadata is not required at reset, so an incomplete image with valid vectors can
also start after an interrupted update. Erase the first application page through
SWD to recover. No rollback, resume or signature authentication is implemented.
Application integration: compile `boot_request.c`, reserve `TAMP->BKP0R`, call
`g474_request_boot(now)` for an update request and `g474_reset_poll(now)` from
the main loop. Reset consumes and clears the backup-register request.
Host test `test_boot_port.c` covers the 64 KiB slot boundaries, geometry rejection, vector validation,
every block size 1..256, all final-word tails, sequential offsets and failures.
Compile it with this directory and `../../include` on the include path.
Hardware USB/Flash/power-loss tests remain required on the target board.

View File

@@ -0,0 +1,5 @@
#ifndef G474_BOOT_CONFIG_H
#define G474_BOOT_CONFIG_H
#define G474_BOOT_NODE 16U
#define G474_BOOT_MODEL "STM32G474RB-USB-BOOT"
#endif

View File

@@ -0,0 +1,109 @@
#include "boot_port.h"
#include "boot_image.h"
#include "boot_writer.h"
#include "stm32g4xx_hal.h"
#include <string.h>
#define COMMIT 0x53455442UL
static g474_writer writer;
static bool geometry_valid(void)
{
__HAL_RCC_SYSCFG_CLK_ENABLE();
return g474_geometry_supported(*(const uint16_t *)FLASHSIZE_BASE,
(FLASH->OPTR & FLASH_OPTR_DBANK) != 0,
(FLASH->OPTR & FLASH_OPTR_BFB2) != 0 ||
(SYSCFG->MEMRMP & SYSCFG_MEMRMP_FB_MODE) != 0);
}
static bool erase_page(uint32_t address)
{
FLASH_EraseInitTypeDef config = {0};
uint32_t bank, page, error;
if (!g474_flash_page(address, (FLASH->OPTR & FLASH_OPTR_DBANK) != 0,
&bank, &page)) return false;
config.TypeErase = FLASH_TYPEERASE_PAGES;
config.Banks = bank == 2U ? FLASH_BANK_2 : FLASH_BANK_1;
config.Page = page; config.NbPages = 1;
return HAL_FLASHEx_Erase(&config, &error) == HAL_OK;
}
static bool invalidate(void *user)
{
bool ok; (void)user;
if (!geometry_valid() || HAL_FLASH_Unlock() != HAL_OK) return false;
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS);
ok = erase_page(G474_META_BASE);
(void)HAL_FLASH_Lock(); return ok;
}
static bool erase(void *user, uint32_t size)
{
uint32_t address, step;
bool ok = true; (void)user;
if (size < 8 || size > G474_APP_SIZE || !geometry_valid()) return false;
g474_writer_init(&writer, size);
if (HAL_FLASH_Unlock() != HAL_OK) return false;
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS);
step = (FLASH->OPTR & FLASH_OPTR_DBANK) ? 2048U : 4096U;
for (address = G474_APP_BASE; address < G474_APP_BASE + size; address += step) {
if (!erase_page(address)) { ok = false; break; }
}
(void)HAL_FLASH_Lock(); return ok;
}
static bool program_word(void *user, uint32_t offset, uint64_t word)
{
uint64_t actual; (void)user;
if ((offset & 7U) || offset > G474_APP_SIZE - 8U) return false;
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, G474_APP_BASE + offset,
word) != HAL_OK) return false;
memcpy(&actual, (const void *)(G474_APP_BASE + offset), 8);
return actual == word;
}
static bool write(void *user, uint32_t offset, const uint8_t *data, uint16_t length)
{
bool ok; (void)user;
if (HAL_FLASH_Unlock() != HAL_OK) return false;
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS);
ok = g474_writer_write(&writer, offset, data, length, program_word, 0);
(void)HAL_FLASH_Lock(); return ok;
}
static bool verify(void *user, uint32_t size, uint32_t crc)
{
(void)user;
return !writer.failed && !writer.used && writer.received == size &&
writer.size == size &&
g474_vectors_valid((const uint8_t *)G474_APP_BASE, size) &&
setp_crc32((const uint8_t *)G474_APP_BASE, size) == crc;
}
static bool commit(void *user, uint32_t size, uint32_t crc)
{
uint32_t record[4] = {size, crc, ~size, COMMIT};
uint64_t word; unsigned i; bool ok = true; (void)user;
if (HAL_FLASH_Unlock() != HAL_OK) return false;
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS);
for (i = 0; i < sizeof(record); i += 8) {
memcpy(&word, (const uint8_t *)record + i, 8);
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, G474_META_BASE + i,
word) != HAL_OK) { ok = false; break; }
}
(void)HAL_FLASH_Lock();
return ok && !memcmp((const void *)G474_META_BASE, record, sizeof(record));
}
bool g474_app_valid(void)
{
/* Same reset policy as F407: SWD images need no USB commit record. */
return geometry_valid() &&
g474_vectors_valid((const uint8_t *)G474_APP_BASE, G474_APP_SIZE);
}
static void activate(void *user) { (void)user; }
const setp_boot_ops g474_boot_ops = {invalidate, erase, write, verify, commit, activate};
__attribute__((naked,noreturn)) static void enter(uint32_t sp, uint32_t pc)
{
__asm volatile("msr msp, r0\nmovs r2, #0\nmsr control, r2\ndsb\nisb\ncpsie i\nbx r1\n");
}
void g474_jump_app(void)
{
unsigned i;
uint32_t sp = *(const uint32_t *)G474_APP_BASE;
uint32_t pc = *(const uint32_t *)(G474_APP_BASE + 4);
__disable_irq(); SysTick->CTRL = 0;
for (i = 0; i < 8; ++i) { NVIC->ICER[i] = 0xffffffff; NVIC->ICPR[i] = 0xffffffff; }
SCB->ICSR = SCB_ICSR_PENDSTCLR_Msk | SCB_ICSR_PENDSVCLR_Msk;
SCB->VTOR = G474_APP_BASE; enter(sp, pc);
}

View File

@@ -0,0 +1,34 @@
#ifndef G474_BOOT_IMAGE_H
#define G474_BOOT_IMAGE_H
#include "boot_port.h"
#include <string.h>
/* SRAM1+SRAM2: 96 KB; CCM: 32 KB, also aliased after SRAM2. */
static inline bool g474_vectors_valid(const uint8_t *image, uint32_t size)
{
uint32_t sp, pc;
if (size < 8 || size > G474_APP_SIZE) return false;
memcpy(&sp, image, 4); memcpy(&pc, image + 4, 4);
return !(sp & 7U) &&
((sp > 0x20000000UL && sp <= 0x20020000UL) ||
(sp > 0x10000000UL && sp <= 0x10008000UL)) &&
(pc & 1U) && (pc & ~1UL) >= G474_APP_BASE &&
(pc & ~1UL) < G474_APP_BASE + size;
}
/* A contiguous xB slot at 0x08010000 exists only with DBANK=0.
* In dual mode the upper 64 KB is at 0x08040000, not at this slot address. */
static inline bool g474_geometry_supported(uint16_t flash_kb, bool dual, bool swapped)
{
return flash_kb == 128U && !dual && !swapped;
}
static inline bool g474_flash_page(uint32_t address, bool dual,
uint32_t *bank, uint32_t *page)
{
uint32_t offset;
if (dual || address < G474_META_BASE || address >= G474_APP_BASE + G474_APP_SIZE)
return false;
offset = address - 0x08000000UL;
*bank = 1U;
*page = offset / 4096U;
return true;
}
#endif

View File

@@ -0,0 +1,39 @@
#include "stm32g4xx_hal.h"
#include "boot_port.h"
#include "usb_cdc.h"
static volatile uint32_t clock_fault;
void SysTick_Handler(void) { HAL_IncTick(); }
static void fatal(uint32_t code) { clock_fault = code; for (;;) {} }
static void usb_clock_init(void)
{
RCC_OscInitTypeDef osc = {0};
RCC_PeriphCLKInitTypeDef peripheral = {0};
RCC_CRSInitTypeDef crs = {0};
/* CPU stays on reset HSI16. USB uses HSI48 disciplined by USB SOF. */
osc.OscillatorType = RCC_OSCILLATORTYPE_HSI48;
osc.HSI48State = RCC_HSI48_ON;
if (HAL_RCC_OscConfig(&osc) != HAL_OK) fatal(2);
peripheral.PeriphClockSelection = RCC_PERIPHCLK_USB;
peripheral.UsbClockSelection = RCC_USBCLKSOURCE_HSI48;
if (HAL_RCCEx_PeriphCLKConfig(&peripheral) != HAL_OK) fatal(3);
__HAL_RCC_CRS_CLK_ENABLE();
crs.Prescaler = RCC_CRS_SYNC_DIV1;
crs.Source = RCC_CRS_SYNC_SOURCE_USB;
crs.Polarity = RCC_CRS_SYNC_POLARITY_RISING;
crs.ReloadValue = __HAL_RCC_CRS_RELOADVALUE_CALCULATE(48000000U, 1000U);
crs.ErrorLimitValue = RCC_CRS_ERRORLIMIT_DEFAULT;
crs.HSI48CalibrationValue = RCC_CRS_HSI48CALIBRATION_DEFAULT;
HAL_RCCEx_CRSConfig(&crs);
}
void boot_protocol_init(void);
void boot_protocol_poll(uint32_t now);
int main(void)
{
bool requested;
SCB->VTOR = FLASH_BASE;
requested = g474_boot_requested();
if (!requested && g474_app_valid()) g474_jump_app();
if (HAL_Init() != HAL_OK) fatal(1);
usb_clock_init(); boot_protocol_init(); usb_cdc_init();
for (;;) { uint32_t now = HAL_GetTick(); usb_cdc_poll(now); boot_protocol_poll(now); }
}

View File

@@ -0,0 +1,15 @@
#ifndef G474_BOOT_PORT_H
#define G474_BOOT_PORT_H
#include "set_boot.h"
/* STM32G474xB (128 KB), single-bank mode, normal bank mapping. */
#define G474_APP_BASE 0x08010000UL
#define G474_APP_SIZE 0x00010000UL
#define G474_META_BASE 0x0800F000UL
#define G474_BOOT_REQUEST 0x1003U
extern const setp_boot_ops g474_boot_ops;
bool g474_app_valid(void);
bool g474_boot_requested(void);
void g474_request_boot(uint32_t now);
void g474_reset_poll(uint32_t now);
void g474_jump_app(void);
#endif

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