From b1d7016147435c4c91aceccbe5a7038e8bbdbc5e Mon Sep 17 00:00:00 2001 From: DashyFox Date: Tue, 2 Jun 2026 11:19:32 +0300 Subject: [PATCH 01/24] analyzer --- .gitignore | 7 + Analyzer/raw/IR_Fox/.gitignore | 2 + Analyzer/raw/IR_Fox/CMakeLists.txt | 26 + Analyzer/raw/IR_Fox/CMakePresets.json | 49 ++ Analyzer/raw/IR_Fox/build_msvc.bat | 21 + .../IR_Fox/cmake/ExternalAnalyzerSDK.cmake | 66 ++ Analyzer/raw/IR_Fox/configure_msvc.bat | 39 ++ Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp | 280 +++++++++ Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.h | 49 ++ .../raw/IR_Fox/src/IrFoxAnalyzerResults.cpp | 175 ++++++ .../raw/IR_Fox/src/IrFoxAnalyzerResults.h | 27 + .../raw/IR_Fox/src/IrFoxAnalyzerSettings.cpp | 62 ++ .../raw/IR_Fox/src/IrFoxAnalyzerSettings.h | 24 + Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp | 593 ++++++++++++++++++ Analyzer/raw/IR_Fox/src/IrFoxDecoder.h | 135 ++++ .../raw/IR_Fox/src/IrFoxProtocolConstants.h | 71 +++ .../src/IrFoxSimulationDataGenerator.cpp | 67 ++ .../IR_Fox/src/IrFoxSimulationDataGenerator.h | 27 + .../.github/workflows/build.yml | 52 ++ Analyzer/raw/PulseLengthStat/.gitignore | 3 + Analyzer/raw/PulseLengthStat/CMakeLists.txt | 24 + .../raw/PulseLengthStat/CMakePresets.json | 49 ++ Analyzer/raw/PulseLengthStat/build_msvc.bat | 21 + .../cmake/ExternalAnalyzerSDK.cmake | 66 ++ .../raw/PulseLengthStat/configure_msvc.bat | 39 ++ .../src/PulseLengthStatAnalyzer.cpp | 110 ++++ .../src/PulseLengthStatAnalyzer.h | 39 ++ .../src/PulseLengthStatAnalyzerResults.cpp | 107 ++++ .../src/PulseLengthStatAnalyzerResults.h | 27 + .../src/PulseLengthStatAnalyzerSettings.cpp | 62 ++ .../src/PulseLengthStatAnalyzerSettings.h | 24 + ...PulseLengthStatSimulationDataGenerator.cpp | 65 ++ .../PulseLengthStatSimulationDataGenerator.h | 27 + Analyzer/raw/dll/.gitkeep | 0 IR_DMA_ISR_signal_analysis.md | 141 +++++ ir_protocol_gate_runs_sim.py | 174 +++++ 36 files changed, 2750 insertions(+) create mode 100644 Analyzer/raw/IR_Fox/.gitignore create mode 100644 Analyzer/raw/IR_Fox/CMakeLists.txt create mode 100644 Analyzer/raw/IR_Fox/CMakePresets.json create mode 100644 Analyzer/raw/IR_Fox/build_msvc.bat create mode 100644 Analyzer/raw/IR_Fox/cmake/ExternalAnalyzerSDK.cmake create mode 100644 Analyzer/raw/IR_Fox/configure_msvc.bat create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.h create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.h create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.cpp create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.h create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxDecoder.h create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.cpp create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.h create mode 100644 Analyzer/raw/PulseLengthStat/.github/workflows/build.yml create mode 100644 Analyzer/raw/PulseLengthStat/.gitignore create mode 100644 Analyzer/raw/PulseLengthStat/CMakeLists.txt create mode 100644 Analyzer/raw/PulseLengthStat/CMakePresets.json create mode 100644 Analyzer/raw/PulseLengthStat/build_msvc.bat create mode 100644 Analyzer/raw/PulseLengthStat/cmake/ExternalAnalyzerSDK.cmake create mode 100644 Analyzer/raw/PulseLengthStat/configure_msvc.bat create mode 100644 Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.cpp create mode 100644 Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.h create mode 100644 Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerResults.cpp create mode 100644 Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerResults.h create mode 100644 Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerSettings.cpp create mode 100644 Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerSettings.h create mode 100644 Analyzer/raw/PulseLengthStat/src/PulseLengthStatSimulationDataGenerator.cpp create mode 100644 Analyzer/raw/PulseLengthStat/src/PulseLengthStatSimulationDataGenerator.h create mode 100644 Analyzer/raw/dll/.gitkeep create mode 100644 IR_DMA_ISR_signal_analysis.md create mode 100644 ir_protocol_gate_runs_sim.py diff --git a/.gitignore b/.gitignore index 9403dff..9663919 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,10 @@ bin/* !.vscode/launch.json log/* /.vscode +*.zip +**/__pycache__ +Analyzer/raw/dll/*.dll +Analyzer/raw/dll/*.so +Analyzer/raw/dll/*.dylib +/Analyzer/raw/IR_Fox/.github +**/.build diff --git a/Analyzer/raw/IR_Fox/.gitignore b/Analyzer/raw/IR_Fox/.gitignore new file mode 100644 index 0000000..7981c18 --- /dev/null +++ b/Analyzer/raw/IR_Fox/.gitignore @@ -0,0 +1,2 @@ +/build +.DS_Store diff --git a/Analyzer/raw/IR_Fox/CMakeLists.txt b/Analyzer/raw/IR_Fox/CMakeLists.txt new file mode 100644 index 0000000..6a7f575 --- /dev/null +++ b/Analyzer/raw/IR_Fox/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) + +project(IrFoxAnalyzer) + +add_definitions(-DLOGIC2) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) + +include(ExternalAnalyzerSDK) + +set(SOURCES + src/IrFoxAnalyzer.cpp + src/IrFoxAnalyzer.h + src/IrFoxDecoder.cpp + src/IrFoxDecoder.h + src/IrFoxAnalyzerResults.cpp + src/IrFoxAnalyzerResults.h + src/IrFoxAnalyzerSettings.cpp + src/IrFoxAnalyzerSettings.h + src/IrFoxSimulationDataGenerator.cpp + src/IrFoxSimulationDataGenerator.h +) + +add_analyzer_plugin(${PROJECT_NAME} SOURCES ${SOURCES}) diff --git a/Analyzer/raw/IR_Fox/CMakePresets.json b/Analyzer/raw/IR_Fox/CMakePresets.json new file mode 100644 index 0000000..3636fb2 --- /dev/null +++ b/Analyzer/raw/IR_Fox/CMakePresets.json @@ -0,0 +1,49 @@ +{ + "version": 3, + "cmakeMinimumRequired": { + "major": 3, + "minor": 19, + "patch": 0 + }, + "configurePresets": [ + { + "name": "win-vs2022-x64", + "displayName": "Visual Studio 2022 (x64)", + "generator": "Visual Studio 17 2022", + "architecture": "x64", + "binaryDir": "${sourceDir}/build" + }, + { + "name": "win-vs2019-x64", + "displayName": "Visual Studio 2019 (x64)", + "generator": "Visual Studio 16 2019", + "architecture": "x64", + "binaryDir": "${sourceDir}/build" + }, + { + "name": "win-nmake-release", + "displayName": "NMake Release (только из «x64 Native Tools Command Prompt for VS»)", + "generator": "NMake Makefiles", + "binaryDir": "${sourceDir}/build-nmake", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + } + ], + "buildPresets": [ + { + "name": "win-release", + "configurePreset": "win-vs2022-x64", + "configuration": "Release" + }, + { + "name": "win-release-vs2019", + "configurePreset": "win-vs2019-x64", + "configuration": "Release" + }, + { + "name": "nmake-release", + "configurePreset": "win-nmake-release" + } + ] +} diff --git a/Analyzer/raw/IR_Fox/build_msvc.bat b/Analyzer/raw/IR_Fox/build_msvc.bat new file mode 100644 index 0000000..9bad4a4 --- /dev/null +++ b/Analyzer/raw/IR_Fox/build_msvc.bat @@ -0,0 +1,21 @@ +@echo off +setlocal EnableDelayedExpansion +cd /d "%~dp0" + +if not exist build\CMakeCache.txt ( + echo Run configure_msvc.bat first. + exit /b 1 +) + +set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" +for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSINSTALL=%%i" +if not defined VSINSTALL ( + echo MSVC not found. Add C++ workload in Visual Studio Installer. + exit /b 1 +) +call "!VSINSTALL!\Common7\Tools\VsDevCmd.bat" -arch=x64 -host_arch=x64 +if errorlevel 1 exit /b 1 + +cmake --build build +pause +exit /b %ERRORLEVEL% diff --git a/Analyzer/raw/IR_Fox/cmake/ExternalAnalyzerSDK.cmake b/Analyzer/raw/IR_Fox/cmake/ExternalAnalyzerSDK.cmake new file mode 100644 index 0000000..10ca7ba --- /dev/null +++ b/Analyzer/raw/IR_Fox/cmake/ExternalAnalyzerSDK.cmake @@ -0,0 +1,66 @@ +include(FetchContent) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED YES) + +if(NOT CMAKE_RUNTIME_OUTPUT_DIRECTORY OR NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin/) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin/) +endif() + +if(NOT TARGET Saleae::AnalyzerSDK) + FetchContent_Declare( + analyzersdk + GIT_REPOSITORY https://github.com/saleae/AnalyzerSDK.git + GIT_TAG master + GIT_SHALLOW True + GIT_PROGRESS True + ) + + FetchContent_GetProperties(analyzersdk) + + if(NOT analyzersdk_POPULATED) + FetchContent_Populate(analyzersdk) + include(${analyzersdk_SOURCE_DIR}/AnalyzerSDKConfig.cmake) + + if(APPLE OR WIN32) + get_target_property(analyzersdk_lib_location Saleae::AnalyzerSDK IMPORTED_LOCATION) + if(CMAKE_LIBRARY_OUTPUT_DIRECTORY) + file(COPY ${analyzersdk_lib_location} DESTINATION ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}) + else() + message(WARNING "Please define CMAKE_RUNTIME_OUTPUT_DIRECTORY and CMAKE_LIBRARY_OUTPUT_DIRECTORY if you want unit tests to locate ${analyzersdk_lib_location}") + endif() + endif() + endif() +endif() + +# Shared folder for all Saleae LLA plugins in this repo: Analyzer/raw/dll +set(ANALYZER_DLL_OUT_DIR "${CMAKE_SOURCE_DIR}/../dll") +get_filename_component(ANALYZER_DLL_OUT_DIR "${ANALYZER_DLL_OUT_DIR}" ABSOLUTE) +file(MAKE_DIRECTORY "${ANALYZER_DLL_OUT_DIR}") + +function(add_analyzer_plugin TARGET) + set(options) + set(single_value_args) + set(multi_value_args SOURCES) + cmake_parse_arguments(_p "${options}" "${single_value_args}" "${multi_value_args}" ${ARGN}) + + add_library(${TARGET} MODULE ${_p_SOURCES}) + target_link_libraries(${TARGET} PRIVATE Saleae::AnalyzerSDK) + + set(ANALYZER_DESTINATION "Analyzers") + install(TARGETS ${TARGET} RUNTIME DESTINATION ${ANALYZER_DESTINATION} + LIBRARY DESTINATION ${ANALYZER_DESTINATION}) + + set_target_properties(${TARGET} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${ANALYZER_DLL_OUT_DIR}" + LIBRARY_OUTPUT_DIRECTORY "${ANALYZER_DLL_OUT_DIR}") + if(CMAKE_CONFIGURATION_TYPES) + foreach(CFG ${CMAKE_CONFIGURATION_TYPES}) + string(TOUPPER ${CFG} CFG_UPPER) + set_target_properties(${TARGET} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY_${CFG_UPPER} "${ANALYZER_DLL_OUT_DIR}" + LIBRARY_OUTPUT_DIRECTORY_${CFG_UPPER} "${ANALYZER_DLL_OUT_DIR}") + endforeach() + endif() +endfunction() diff --git a/Analyzer/raw/IR_Fox/configure_msvc.bat b/Analyzer/raw/IR_Fox/configure_msvc.bat new file mode 100644 index 0000000..d3ed219 --- /dev/null +++ b/Analyzer/raw/IR_Fox/configure_msvc.bat @@ -0,0 +1,39 @@ +@echo off +setlocal EnableDelayedExpansion +cd /d "%~dp0" + +echo === IrFoxAnalyzer: configure with MSVC === +echo. + +set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" +if not exist "%VSWHERE%" ( + echo [ERROR] vswhere not found. Install one of: + echo - Visual Studio 2022 with workload "Desktop development with C++" + echo - Build Tools for Visual Studio 2022: https://visualstudio.microsoft.com/visual-cpp-build-tools/ + echo ^(select "Desktop development with C++" / MSVC, Windows SDK^) + exit /b 1 +) + +for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSINSTALL=%%i" +if not defined VSINSTALL ( + echo [ERROR] MSVC toolset not found. Add "Desktop development with C++" in Visual Studio Installer. + exit /b 1 +) + +echo Found: !VSINSTALL! +call "!VSINSTALL!\Common7\Tools\VsDevCmd.bat" -arch=x64 -host_arch=x64 +if errorlevel 1 exit /b 1 + +if exist build rmdir /s /q build +if exist build-nmake rmdir /s /q build-nmake +mkdir build +cd build + +cmake .. -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release +if errorlevel 1 exit /b 1 + +echo. +echo Configure OK. Build: build_msvc.bat ^(or from same VS env: cd build ^& cmake --build .^) +echo Output DLL: ..\dll\ ^(all analyzers share this folder^) +pause +exit /b 0 diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp new file mode 100644 index 0000000..e4cc182 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp @@ -0,0 +1,280 @@ +#include "IrFoxAnalyzer.h" +#include "IrFoxAnalyzerSettings.h" +#include "IrFoxDecoder.h" +#include +#include +#include +#include +#include +#include +#include + +IrFoxAnalyzer::IrFoxAnalyzer() + : Analyzer2(), + mSettings(), + mSimulationInitilized(false) +{ + SetAnalyzerSettings(&mSettings); + UseFrameV2(); +} + +IrFoxAnalyzer::~IrFoxAnalyzer() +{ + KillThread(); +} + +void IrFoxAnalyzer::SetupResults() +{ + m_packet_hex_by_frame.clear(); + mResults.reset(new IrFoxAnalyzerResults(this, &mSettings)); + SetAnalyzerResults(mResults.get()); + mResults->AddChannelBubblesWillAppearOn(mSettings.mInputChannel); +} + +static void append_hex(std::string& s, const uint8_t* p, size_t n, size_t max_bytes = 64) +{ + static const char* hd = "0123456789abcdef"; + const size_t m = n < max_bytes ? n : max_bytes; + for (size_t i = 0; i < m; i++) + { + s.push_back(hd[p[i] >> 4]); + s.push_back(hd[p[i] & 0xFu]); + if (i + 1 < m) + s.push_back(' '); + } + if (n > max_bytes) + s += "..."; +} + +const char* IrFoxAnalyzer::PacketHexForFrame(U64 frame_id) +{ + auto it = m_packet_hex_by_frame.find(frame_id); + if (it == m_packet_hex_by_frame.end()) + return ""; + m_hex_scratch = it->second; + return m_hex_scratch.c_str(); +} + +const char* IrFoxAnalyzer::BubbleTextForFrame(U64 frame_id) const +{ + auto it = m_bubble_text_by_frame.find(frame_id); + if (it == m_bubble_text_by_frame.end()) + return ""; + m_bubble_scratch = it->second; + return m_bubble_scratch.c_str(); +} + +void IrFoxAnalyzer::WorkerThread() +{ + mIr = GetAnalyzerChannelData(mSettings.mInputChannel); + m_packet_hex_by_frame.clear(); + m_bubble_text_by_frame.clear(); + + const U32 fs = GetSampleRate(); + IrFoxDecoder decoder; + decoder.reset(); + + /** Потоковый фильтр: убирает импульсы короче kMinFilteredPulseUs (иголки/дребезг в сэмплах). */ + const U64 min_seg_samples = + std::max(1ULL, static_cast((static_cast(irfox::kMinFilteredPulseUs) * 1e-6) * static_cast(fs) + 0.5)); + struct RawEdge + { + U64 sample; + bool rising; + }; + std::vector pending; + U64 last_dec_edge_sample = 0; + bool last_dec_edge_valid = false; + + auto collapse_short_pairs = [&]() { + for (size_t i = 0; i + 1 < pending.size();) + { + if (pending[i + 1].sample - pending[i].sample < min_seg_samples) + { + pending.erase(pending.begin() + static_cast(i), + pending.begin() + static_cast(i + 2)); + if (i > 0) + --i; + } + else + ++i; + } + }; + + auto strip_vs_last_decoder = [&]() { + for (;;) + { + collapse_short_pairs(); + if (!last_dec_edge_valid || pending.empty()) + return; + if (pending[0].sample - last_dec_edge_sample >= min_seg_samples) + return; + pending.erase(pending.begin()); + } + }; + + U32 frames_since_commit = 0; + const U32 kCommitBatch = 256; + + IrFoxOnBit on_bit = [&](const IrFoxEmitBit& e) { + Frame frame; + frame.mStartingSampleInclusive = static_cast(e.start_sample); + frame.mEndingSampleInclusive = static_cast(e.end_sample); + frame.mType = e.frame_type; + frame.mData1 = e.bit_value; + frame.mData2 = e.bit_index | (U64(e.err_low) << 16) | (U64(e.err_high) << 24) | (U64(e.err_other) << 32); + frame.mFlags = e.mflags; + // В SDK только ERROR/WARNING меняют цвет бабла; sync выделяем янтарным (как warning), данные — обычные. + if (e.frame_type == IRF_FT_SYNC_BIT) + frame.mFlags |= DISPLAY_AS_WARNING_FLAG; + + const U64 fid = mResults->AddFrame(frame); + if (e.bubble_text[0] != '\0') + m_bubble_text_by_frame[fid] = e.bubble_text; + if (++frames_since_commit >= kCommitBatch) + { + mResults->CommitResults(); + frames_since_commit = 0; + } + }; + + IrFoxOnPacket on_pkt = [&](const IrFoxEmitPacket& p) { + Frame frame; + frame.mStartingSampleInclusive = static_cast(p.start_sample); + frame.mEndingSampleInclusive = static_cast(p.end_sample); + frame.mType = p.crc_ok ? IRF_FT_PACKET_OK : IRF_FT_PACKET_CRC_FAIL; + frame.mData1 = p.pack_size; + frame.mData2 = (U64(p.err_low) << 0) | (U64(p.err_high) << 8) | (U64(p.err_other) << 16); + if (!p.crc_ok) + frame.mFlags |= DISPLAY_AS_ERROR_FLAG; + + const U64 fid = mResults->AddFrame(frame); + + std::string hx; + append_hex(hx, p.data_bytes, p.pack_size); + m_packet_hex_by_frame[fid] = std::move(hx); + + FrameV2 fv2; + fv2.AddBoolean("crc_ok", p.crc_ok); + fv2.AddInteger("len", static_cast(p.pack_size)); + fv2.AddInteger("err_low", static_cast(p.err_low)); + fv2.AddInteger("err_high", static_cast(p.err_high)); + fv2.AddInteger("err_other", static_cast(p.err_other)); + fv2.AddByteArray("data", p.data_bytes, p.pack_size); + mResults->AddFrameV2(fv2, p.crc_ok ? "packet_ok" : "packet_bad", static_cast(p.start_sample), + static_cast(p.end_sample)); + + if (++frames_since_commit >= kCommitBatch) + { + mResults->CommitResults(); + frames_since_commit = 0; + } + }; + + auto emit_confirmed_edges = [&]() { + for (;;) + { + collapse_short_pairs(); + strip_vs_last_decoder(); + if (pending.size() < 2) + return; + if (pending[1].sample - pending[0].sample < min_seg_samples) + continue; + decoder.processEdge(pending[0].sample, pending[0].rising, fs, on_bit, on_pkt); + last_dec_edge_sample = pending[0].sample; + last_dec_edge_valid = true; + pending.erase(pending.begin()); + } + }; + + auto flush_pending_tail = [&]() { + collapse_short_pairs(); + strip_vs_last_decoder(); + while (pending.size() >= 2 && pending[1].sample - pending[0].sample >= min_seg_samples) + { + decoder.processEdge(pending[0].sample, pending[0].rising, fs, on_bit, on_pkt); + last_dec_edge_sample = pending[0].sample; + last_dec_edge_valid = true; + pending.erase(pending.begin()); + collapse_short_pairs(); + strip_vs_last_decoder(); + } + if (pending.size() == 1) + { + decoder.processEdge(pending[0].sample, pending[0].rising, fs, on_bit, on_pkt); + last_dec_edge_sample = pending[0].sample; + last_dec_edge_valid = true; + pending.clear(); + } + }; + + for (;;) + { + CheckIfThreadShouldExit(); + + const U64 segment_start = mIr->GetSampleNumber(); + const BitState level = mIr->GetBitState(); + + mIr->AdvanceToNextEdge(); + + const U64 edge_sample = mIr->GetSampleNumber(); + if (edge_sample == segment_start) + break; + + const BitState new_level = mIr->GetBitState(); + const bool rising = (new_level == BIT_HIGH); + + pending.push_back(RawEdge{edge_sample, rising}); + emit_confirmed_edges(); + ReportProgress(edge_sample); + } + + flush_pending_tail(); + decoder.flushEnd(mIr->GetSampleNumber(), fs, on_bit, on_pkt); + + if (frames_since_commit != 0) + mResults->CommitResults(); +} + +bool IrFoxAnalyzer::NeedsRerun() +{ + return false; +} + +U32 IrFoxAnalyzer::GenerateSimulationData(U64 minimum_sample_index, U32 device_sample_rate, + SimulationChannelDescriptor** simulation_channels) +{ + if (mSimulationInitilized == false) + { + mSimulationDataGenerator.Initialize(GetSimulationSampleRate(), &mSettings); + mSimulationInitilized = true; + } + + return mSimulationDataGenerator.GenerateSimulationData(minimum_sample_index, device_sample_rate, + simulation_channels); +} + +U32 IrFoxAnalyzer::GetMinimumSampleRateHz() +{ + return 200000; +} + +const char* IrFoxAnalyzer::GetAnalyzerName() const +{ + return "IR Fox"; +} + +const char* GetAnalyzerName() +{ + return "IR Fox"; +} + +Analyzer* CreateAnalyzer() +{ + return new IrFoxAnalyzer(); +} + +void DestroyAnalyzer(Analyzer* analyzer) +{ + delete analyzer; +} diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.h b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.h new file mode 100644 index 0000000..dbf3167 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.h @@ -0,0 +1,49 @@ +#ifndef IRFOX_ANALYZER_H +#define IRFOX_ANALYZER_H + +#include +#include "IrFoxAnalyzerSettings.h" +#include "IrFoxAnalyzerResults.h" +#include "IrFoxSimulationDataGenerator.h" +#include +#include +#include + +class ANALYZER_EXPORT IrFoxAnalyzer : public Analyzer2 +{ +public: + IrFoxAnalyzer(); + virtual ~IrFoxAnalyzer(); + + virtual void SetupResults(); + virtual void WorkerThread(); + + virtual U32 GenerateSimulationData(U64 newest_sample_requested, U32 sample_rate, + SimulationChannelDescriptor** simulation_channels); + virtual U32 GetMinimumSampleRateHz(); + + virtual const char* GetAnalyzerName() const; + virtual bool NeedsRerun(); + + const char* PacketHexForFrame(U64 frame_id); + const char* BubbleTextForFrame(U64 frame_id) const; + +protected: + IrFoxAnalyzerSettings mSettings; + std::unique_ptr mResults; + AnalyzerChannelData* mIr; + + IrFoxSimulationDataGenerator mSimulationDataGenerator; + bool mSimulationInitilized; + + std::unordered_map m_packet_hex_by_frame; + std::unordered_map m_bubble_text_by_frame; + mutable std::string m_hex_scratch; + mutable std::string m_bubble_scratch; +}; + +extern "C" ANALYZER_EXPORT const char* __cdecl GetAnalyzerName(); +extern "C" ANALYZER_EXPORT Analyzer* __cdecl CreateAnalyzer(); +extern "C" ANALYZER_EXPORT void __cdecl DestroyAnalyzer(Analyzer* analyzer); + +#endif diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp new file mode 100644 index 0000000..0499232 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp @@ -0,0 +1,175 @@ +#include "IrFoxAnalyzerResults.h" +#include +#include +#include "IrFoxAnalyzer.h" +#include "IrFoxAnalyzerSettings.h" +#include "IrFoxDecoder.h" +#include +#include + +IrFoxAnalyzerResults::IrFoxAnalyzerResults(IrFoxAnalyzer* analyzer, IrFoxAnalyzerSettings* settings) + : AnalyzerResults(), + mSettings(settings), + mAnalyzer(analyzer) +{ +} + +IrFoxAnalyzerResults::~IrFoxAnalyzerResults() +{ +} + +void IrFoxAnalyzerResults::GenerateBubbleText(U64 frame_index, Channel& channel, DisplayBase display_base) +{ + (void)display_base; + (void)channel; + ClearResultStrings(); + Frame frame = GetFrame(frame_index); + + char line[256]; + + switch (frame.mType) + { + case IRF_FT_DATA_BIT: + case IRF_FT_SYNC_BIT: + case IRF_FT_PREAMBLE: + case IRF_FT_OVERFLOW: + case IRF_FT_ABORT: + { + const char* bt = mAnalyzer->BubbleTextForFrame(frame_index); + if (bt && bt[0]) + AddResultString(bt); + else if (frame.mType == IRF_FT_DATA_BIT) + AddResultString(frame.mData1 ? "1" : "0"); + else if (frame.mType == IRF_FT_SYNC_BIT) + { + snprintf(line, sizeof line, "sync: %s", frame.mData1 ? "1" : "0"); + AddResultString(line); + } + else if (frame.mType == IRF_FT_OVERFLOW) + AddResultString("OVF"); + else if (frame.mType == IRF_FT_ABORT) + AddResultString("SYNC!"); + else + AddResultString("PRE"); + break; + } + + case IRF_FT_PACKET_OK: + case IRF_FT_PACKET_CRC_FAIL: + { + snprintf(line, sizeof line, "%s %lluB", frame.mType == IRF_FT_PACKET_OK ? "OK" : "CRC", + (unsigned long long)frame.mData1); + AddResultString(line); + const char* hx = mAnalyzer->PacketHexForFrame(frame_index); + if (hx && hx[0]) + AddResultString(hx); + break; + } + + default: + snprintf(line, sizeof line, "? type=%u", static_cast(frame.mType)); + AddResultString(line); + break; + } +} + +void IrFoxAnalyzerResults::GenerateExportFile(const char* file, DisplayBase display_base, U32 export_type_user_id) +{ + (void)export_type_user_id; + (void)display_base; + std::ofstream file_stream(file, std::ios::out); + + const U64 trigger_sample = mAnalyzer->GetTriggerSample(); + const U32 sample_rate = mAnalyzer->GetSampleRate(); + + file_stream << "Time[s],Type,Data1,bit_idx,err_low,err_high,err_other,Flags,Hex" << std::endl; + + const U64 num_frames = GetNumFrames(); + for (U32 i = 0; i < num_frames; i++) + { + Frame frame = GetFrame(i); + + char time_str[128]; + AnalyzerHelpers::GetTimeString(frame.mStartingSampleInclusive, trigger_sample, sample_rate, time_str, 128); + + const char* typ = "?"; + switch (frame.mType) + { + case IRF_FT_DATA_BIT: + typ = "D"; + break; + case IRF_FT_SYNC_BIT: + typ = "S"; + break; + case IRF_FT_PACKET_OK: + typ = "OK"; + break; + case IRF_FT_PACKET_CRC_FAIL: + typ = "CRC"; + break; + case IRF_FT_OVERFLOW: + typ = "OVF"; + break; + case IRF_FT_ABORT: + typ = "ABORT"; + break; + case IRF_FT_PREAMBLE: + typ = "PRE"; + break; + default: + break; + } + + const char* hx = mAnalyzer->PacketHexForFrame(i); + if (!hx) + hx = ""; + + U64 bit_idx = 0; + U32 err_l = 0, err_h = 0, err_o = 0; + if (frame.mType == IRF_FT_DATA_BIT || frame.mType == IRF_FT_SYNC_BIT || + frame.mType == IRF_FT_OVERFLOW || frame.mType == IRF_FT_ABORT) + { + bit_idx = frame.mData2 & 0xFFFFull; + err_l = static_cast((frame.mData2 >> 16) & 0xFFull); + err_h = static_cast((frame.mData2 >> 24) & 0xFFull); + err_o = static_cast((frame.mData2 >> 32) & 0xFFull); + } + + file_stream << time_str << "," << typ << "," << frame.mData1 << "," << bit_idx << "," << err_l << "," << err_h + << "," << err_o << "," << static_cast(frame.mFlags) << "," << hx << std::endl; + + if (UpdateExportProgressAndCheckForCancel(i, num_frames) == true) + { + file_stream.close(); + return; + } + } + + file_stream.close(); +} + +void IrFoxAnalyzerResults::GenerateFrameTabularText(U64 frame_index, DisplayBase display_base) +{ +#ifdef SUPPORTS_PROTOCOL_SEARCH + (void)display_base; + Frame frame = GetFrame(frame_index); + ClearTabularText(); + char buf[64]; + snprintf(buf, sizeof buf, "t%u", static_cast(frame.mType)); + AddTabularText(buf); + snprintf(buf, sizeof buf, "%llu", (unsigned long long)frame.mData1); + AddTabularText(buf); +#endif +} + +void IrFoxAnalyzerResults::GeneratePacketTabularText(U64 packet_id, DisplayBase display_base) +{ + (void)packet_id; + (void)display_base; +} + +void IrFoxAnalyzerResults::GenerateTransactionTabularText(U64 transaction_id, DisplayBase display_base) +{ + (void)transaction_id; + (void)display_base; +} diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.h b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.h new file mode 100644 index 0000000..4007fcc --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.h @@ -0,0 +1,27 @@ +#ifndef IRFOX_ANALYZER_RESULTS +#define IRFOX_ANALYZER_RESULTS + +#include + +class IrFoxAnalyzer; +class IrFoxAnalyzerSettings; + +class IrFoxAnalyzerResults : public AnalyzerResults +{ +public: + IrFoxAnalyzerResults(IrFoxAnalyzer* analyzer, IrFoxAnalyzerSettings* settings); + virtual ~IrFoxAnalyzerResults(); + + virtual void GenerateBubbleText(U64 frame_index, Channel& channel, DisplayBase display_base); + virtual void GenerateExportFile(const char* file, DisplayBase display_base, U32 export_type_user_id); + + virtual void GenerateFrameTabularText(U64 frame_index, DisplayBase display_base); + virtual void GeneratePacketTabularText(U64 packet_id, DisplayBase display_base); + virtual void GenerateTransactionTabularText(U64 transaction_id, DisplayBase display_base); + +protected: + IrFoxAnalyzerSettings* mSettings; + IrFoxAnalyzer* mAnalyzer; +}; + +#endif diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.cpp b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.cpp new file mode 100644 index 0000000..c7b0163 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.cpp @@ -0,0 +1,62 @@ +#include "IrFoxAnalyzerSettings.h" +#include + +IrFoxAnalyzerSettings::IrFoxAnalyzerSettings() + : mInputChannel(UNDEFINED_CHANNEL), + mInputChannelInterface() +{ + mInputChannelInterface.SetTitleAndTooltip( + "IR", + "Demodulated IR receiver output (e.g. TSOP: idle HIGH, active LOW)"); + mInputChannelInterface.SetChannel(mInputChannel); + + AddInterface(&mInputChannelInterface); + + AddExportOption(0, "Export as text/csv file"); + AddExportExtension(0, "text", "txt"); + AddExportExtension(0, "csv", "csv"); + + ClearChannels(); + AddChannel(mInputChannel, "IR", false); +} + +IrFoxAnalyzerSettings::~IrFoxAnalyzerSettings() +{ +} + +bool IrFoxAnalyzerSettings::SetSettingsFromInterfaces() +{ + mInputChannel = mInputChannelInterface.GetChannel(); + + ClearChannels(); + AddChannel(mInputChannel, "IR Fox", true); + + return true; +} + +void IrFoxAnalyzerSettings::UpdateInterfacesFromSettings() +{ + mInputChannelInterface.SetChannel(mInputChannel); +} + +void IrFoxAnalyzerSettings::LoadSettings(const char* settings) +{ + SimpleArchive text_archive; + text_archive.SetString(settings); + + text_archive >> mInputChannel; + + ClearChannels(); + AddChannel(mInputChannel, "IR Fox", true); + + UpdateInterfacesFromSettings(); +} + +const char* IrFoxAnalyzerSettings::SaveSettings() +{ + SimpleArchive text_archive; + + text_archive << mInputChannel; + + return SetReturnString(text_archive.GetString()); +} diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.h b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.h new file mode 100644 index 0000000..694239a --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.h @@ -0,0 +1,24 @@ +#ifndef IRFOX_ANALYZER_SETTINGS +#define IRFOX_ANALYZER_SETTINGS + +#include +#include + +class IrFoxAnalyzerSettings : public AnalyzerSettings +{ +public: + IrFoxAnalyzerSettings(); + virtual ~IrFoxAnalyzerSettings(); + + virtual bool SetSettingsFromInterfaces(); + void UpdateInterfacesFromSettings(); + virtual void LoadSettings(const char* settings); + virtual const char* SaveSettings(); + + Channel mInputChannel; + +protected: + AnalyzerSettingInterfaceChannel mInputChannelInterface; +}; + +#endif diff --git a/Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp b/Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp new file mode 100644 index 0000000..de25571 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp @@ -0,0 +1,593 @@ +#include "IrFoxDecoder.h" +#include +#include +#include +#include +#include + +void IrFoxDecoder::reset() +{ + *this = IrFoxDecoder{}; + rise_sync_time_us = irfox::kBitTimeUs; + next_control_bit = irfox::kBitPerByte; + have_last_processed = false; + last_processed_edge_us = 0; +} + +uint16_t IrFoxDecoder::ceil_div_u16(uint16_t val, uint16_t divider) +{ + if (divider == 0) + return 0; + int ret = val / divider; + if ((val << 4) / divider - (ret << 4) >= 8) + ret++; + return static_cast(ret); +} + +uint8_t IrFoxDecoder::crc8(const uint8_t* data, uint8_t start, uint8_t end, uint8_t poly) +{ + uint8_t crc = 0xff; + for (size_t i = start; i < end; i++) + { + crc ^= data[i]; + for (size_t j = 0; j < 8; j++) + { + if ((crc & 0x80) != 0) + crc = static_cast((crc << 1) ^ poly); + else + crc <<= 1; + } + } + return crc; +} + +bool IrFoxDecoder::crc_check(uint8_t len, uint16_t& crc_out) +{ + crc_out = 0; + crc_out = static_cast(static_cast(crc8(data_buffer, 0, len, irfox::kPoly1) << 8) & 0xFF00u); + crc_out = static_cast(crc_out | (crc8(data_buffer, 0, static_cast(len + 1), irfox::kPoly2) & 0xFFu)); + + const bool ok = (data_buffer[len] == static_cast((crc_out >> 8) & 0xFF)) && + (data_buffer[len + 1] == static_cast(crc_out & 0xFF)); + return ok; +} + +void IrFoxDecoder::first_rx() +{ + err_low_signal = err_high_signal = err_other = 0; + pack_size = 0; + is_buffer_overflow = false; + is_available = false; + buf_bit_pos = 0; + is_data = true; + i_data_buffer = 0; + next_control_bit = irfox::kBitPerByte; + i_sync_bit = 0; + err_sync_bit = 0; + is_wrong_pack = false; + is_preamb = true; + is_recive = false; + is_recive_raw = false; + msg_type_receive = 0; + rise_sync_time_us = irfox::kBitTimeUs; + std::memset(data_buffer, 0, sizeof data_buffer); + preamble_bubble_start_valid_ = false; + trim_first_data_bit_cell_ = false; +} + +void IrFoxDecoder::listen_start(double t_us) +{ + const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us); + // Как IR_DecoderRaw::listenStart: пауза по lastEdgeTime, не по prevRise. + if (is_recive_raw && last_edge_time_us > 0.0 && (t_us - last_edge_time_us) > irmax * 2.0) + { + is_recive_raw = false; + first_rx(); + } +} + +void IrFoxDecoder::check_timeout(double t_us) +{ + if (!is_recive) + return; + const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us); + if (t_us - last_edge_time_us > irmax * 2.0) + { + // Как IR_DecoderRaw::checkTimeout после фикса: полный сброс, иначе залипание FSM. + is_recive = false; + msg_type_receive = 0; + is_recive_raw = false; + first_rx(); + // Не last_edge_time_us = t_us: как IR_DecoderRaw — не расходить с «хвостом» фронтов. + } +} + +void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_t cell_start_s, uint64_t cell_end_s, + const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt, IrFoxEmitBitMode emit_mode) +{ + if (i_data_buffer > irfox::kDataByteSizeMax * 8u) + { + if (!is_buffer_overflow && on_bit) + { + IrFoxEmitBit e{}; + e.start_sample = static_cast(cell_start_s); + e.end_sample = static_cast(cell_end_s); + e.frame_type = IRF_FT_OVERFLOW; + e.mflags = DISPLAY_AS_ERROR_FLAG; + fill_err_snapshot(e); + std::strncpy(e.bubble_text, "OVF", sizeof e.bubble_text); + e.bubble_text[sizeof e.bubble_text - 1] = '\0'; + on_bit(e); + } + is_buffer_overflow = true; + } + + if (is_buffer_overflow || is_preamb || is_wrong_pack) + { + // Как IR_DecoderRaw::writeToBuffer: полный first_rx() вместо только сброса флагов приёма. + first_rx(); + return; + } + + if (buf_bit_pos == next_control_bit) + { + next_control_bit = next_control_bit + (is_data ? irfox::kSyncBits : irfox::kBitPerByte); + is_data = !is_data; + i_sync_bit = 0; + err_sync_bit = 0; + } + + if (is_data) + { + const bool was_first_data_bit = (i_data_buffer == 0); + data_buffer[i_data_buffer / 8] |= static_cast(bit ? 1 : 0) << (7 - (i_data_buffer % 8)); + i_data_buffer++; + buf_bit_pos++; + + uint8_t fl = 0; + if (pack_trace_invert_fix) + fl |= DISPLAY_AS_WARNING_FLAG; + if (on_bit && emit_mode == IrFoxEmitBitMode::WithBubble) + { + IrFoxEmitBit e{static_cast(cell_start_s), static_cast(cell_end_s), IRF_FT_DATA_BIT, + bit ? 1u : 0u, static_cast(i_data_buffer - 1), fl, pack_trace_invert_fix, 0, 0, 0}; + fill_err_snapshot(e); + e.bubble_text[0] = static_cast(bit ? '1' : '0'); + e.bubble_text[1] = '\0'; + on_bit(e); + } + if (was_first_data_bit && trim_first_data_bit_cell_) + trim_first_data_bit_cell_ = false; + } + else + { + if (i_sync_bit == 0) + { + const bool last_data_bit = + (data_buffer[((i_data_buffer - 1) / 8)] >> (7 - ((i_data_buffer - 1) % 8))) & 1; + if (bit != static_cast(last_data_bit)) + { + buf_bit_pos++; + i_sync_bit++; + if (on_bit && emit_mode == IrFoxEmitBitMode::WithBubble) + { + IrFoxEmitBit e{static_cast(cell_start_s), static_cast(cell_end_s), IRF_FT_SYNC_BIT, + bit ? 1u : 0u, static_cast(buf_bit_pos), 0, false, 0, 0, 0}; + fill_err_snapshot(e); + std::snprintf(e.bubble_text, sizeof e.bubble_text, "s: %c", bit ? '1' : '0'); + on_bit(e); + } + } + else + { + i_sync_bit = 0; + err_other++; + err_sync_bit++; + const bool fatal_sync = (err_sync_bit >= irfox::kSyncBits); + if (fatal_sync) + is_wrong_pack = true; + if (on_bit && fatal_sync) + { + IrFoxEmitBit e{static_cast(cell_start_s), static_cast(cell_end_s), IRF_FT_ABORT, + 0, 0, DISPLAY_AS_ERROR_FLAG, false, 0, 0, 0}; + fill_err_snapshot(e); + std::strncpy(e.bubble_text, "SYNC!", sizeof e.bubble_text); + e.bubble_text[sizeof e.bubble_text - 1] = '\0'; + on_bit(e); + } + } + } + else + { + buf_bit_pos++; + i_sync_bit++; + if (on_bit && emit_mode == IrFoxEmitBitMode::WithBubble) + { + IrFoxEmitBit e{static_cast(cell_start_s), static_cast(cell_end_s), IRF_FT_SYNC_BIT, + bit ? 1u : 0u, static_cast(buf_bit_pos), 0, false, 0, 0, 0}; + fill_err_snapshot(e); + std::snprintf(e.bubble_text, sizeof e.bubble_text, "s: %c", bit ? '1' : '0'); + on_bit(e); + } + } + is_wrong_pack = (err_sync_bit >= irfox::kSyncBits); + } + + if (!is_available && is_data && !is_wrong_pack) + { + if (i_data_buffer == 8 * irfox::kMsgBytes) + pack_size = static_cast(data_buffer[0] & 0x1Fu); + + if (pack_size && (i_data_buffer == 8)) + msg_type_receive = static_cast((data_buffer[0] >> 5) | 0xF8u); + + if (pack_size && (i_data_buffer == pack_size * irfox::kBitPerByte)) + { + uint16_t crc_computed = 0; + const bool crc_ok = crc_check(static_cast(pack_size - irfox::kCrcBytes), crc_computed); + crc_value = crc_computed; + is_recive = false; + is_recive_raw = false; + msg_type_receive = 0; + is_available = crc_ok; + + IrFoxEmitPacket pkt{}; + pkt.start_sample = static_cast(cell_start_s); + pkt.end_sample = static_cast(cell_end_s); + pkt.crc_ok = crc_ok; + pkt.pack_size = static_cast(pack_size); + pkt.err_low = err_low_signal; + pkt.err_high = err_high_signal; + pkt.err_other = err_other; + if (pack_size > 0 && pack_size <= irfox::kDataByteSizeMax) + std::memcpy(pkt.data_bytes, data_buffer, pack_size); + if (on_pkt) + on_pkt(pkt); + } + } +} + +void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const IrFoxOnBit& on_bit, + const IrFoxOnPacket& on_pkt) +{ + const double t_us = sample_to_us(sample, fs); + const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us); + uint32_t rise_min_us = rise_sync_time_us > irfox::kToleranceUs ? rise_sync_time_us - irfox::kToleranceUs : 0U; + + listen_start(t_us); + + // Как IR_DecoderRaw: пауза между фронтами по lastEdgeTime при активном приёме кадра. + if (last_edge_time_us > 0.0 && (t_us - last_edge_time_us) > irmax * 2.0 && is_recive) + check_timeout(t_us); + + last_edge_time_us = t_us; + last_edge_sample = sample; + + const uint32_t rise_max_us = rise_sync_time_us + irfox::kToleranceUs; + + /** Визуализация: начало PRE с ближайшего спада в пределах ~3 битовых периодов (ИК-метка). */ + auto new_bubble_preamble_start = [&](uint64_t edge_s, bool is_rising) -> uint64_t { + if (!is_rising) + return edge_s; + if (edge_s > prev_fall_sample) + { + const double span_us = double(edge_s - prev_fall_sample) * 1e6 / double(fs); + const double max_us = double(rise_max_us) * 3.0; + if (span_us <= max_us) + return prev_fall_sample; + } + return edge_s; + }; + + if (rising) + { + const double delta_rp = t_us - prev_rise_us; + const uint32_t cand_rp = static_cast(delta_rp); + const uint32_t cand_ht = static_cast(t_us - prev_fall_us); + const uint32_t cand_lt = static_cast(prev_fall_us - prev_rise_us); + +#if IRFOX_SHORT_LOW_GLITCH_REJECT + const bool short_low_glitch = + is_recive && !is_preamb && cand_ht < (rise_min_us / 8U) && cand_lt >= rise_min_us && + cand_rp >= rise_min_us && cand_rp <= irmax; + if (short_low_glitch) + { + err_other++; + irfox::irfoxGlitchPhaseNudgeUs(t_us, rise_sync_time_us, prev_rise_us); + last_processed_edge_us = t_us; + have_last_processed = true; + return; + } +#endif +#if IRFOX_MICRO_GAP_RISE_REJECT + const bool micro_gap_cand_lt_ok = + (cand_lt >= rise_min_us) || (cand_lt >= (rise_min_us / 4U) && cand_lt < rise_min_us); + const bool micro_gap_rise = is_recive && !is_preamb && cand_ht < (rise_min_us / 8U) && micro_gap_cand_lt_ok && + cand_rp >= (rise_min_us / 4U) && cand_rp < rise_min_us && cand_rp <= irmax; + if (micro_gap_rise) + { + err_other++; + irfox::irfoxGlitchPhaseNudgeUs(t_us, rise_sync_time_us, prev_rise_us); + last_processed_edge_us = t_us; + have_last_processed = true; + return; + } +#endif + if (cand_rp <= rise_max_us / 4U && !high_count && !low_count) + { + err_other++; + last_processed_edge_us = t_us; + have_last_processed = true; + return; + } + + // Визуализация PRE: длинная пауза, первый подъём — якорь от спада метки (декод как STM32DMA). + if (cand_rp > irmax * 2U && !is_recive_raw) + { + preamble_bubble_start_sample_ = new_bubble_preamble_start(sample, true); + preamble_bubble_start_valid_ = true; + } + + const bool accept_rise_timing = + (delta_rp > static_cast(rise_max_us) / 4.0) || high_count != 0 || low_count != 0; + if (accept_rise_timing) + { + rise_period_anchor_sample_ = prev_rise_sample; + rise_period_us = cand_rp; + high_time_us = cand_ht; + low_time_us = cand_lt; + prev_rise_us = t_us; + prev_rise_sample = sample; + } + else + { + err_other++; + } + } + else + { + if (t_us - prev_fall_us > rise_min_us / 4.0) + { + prev_fall_us = t_us; + prev_fall_sample = sample; + } + else + { + err_other++; + } + } + + // Как IR_DecoderRaw::tick: после длинной паузы старт сырого приёма (без отдельного firstRX — флаги ниже). + if (t_us > prev_rise_us && (t_us - prev_rise_us) > irmax * 2.0 && !is_recive_raw) + { + preamb_front_counter = static_cast(irfox::kPreambFronts - 1); + is_preamb = true; + is_recive = true; + is_recive_raw = true; + is_wrong_pack = false; + if (!preamble_bubble_start_valid_) + { + preamble_bubble_start_sample_ = new_bubble_preamble_start(sample, rising); + preamble_bubble_start_valid_ = true; + } + } + + if (preamb_front_counter) + { + if (rising && rise_period_us < irmax) + { + if (rise_period_us < rise_min_us / 2U) + { + preamb_front_counter += 2; + err_other++; + } + } + preamb_front_counter--; + } + else + { + if (is_preamb) + { + is_preamb = false; + // IR_DecoderRaw: prevRise += risePeriod / 2 — фаза как в прошивке. + // Бабл PRE: до текущего фронта (sample−1), чтобы охватить все kPreambPulse периодов (3 импульса), + // а не только до предыдущего подъёма (~2 периода). + const uint64_t preamble_bubble_end_sample = sample > 0 ? sample - 1 : sample; + prev_rise_us += rise_period_us / 2.0; + { + const double half_us = 0.5 * static_cast(rise_period_us); + const uint64_t half_s = static_cast(std::llround(half_us * double(fs) / 1e6)); + prev_rise_sample += half_s; + } + trim_first_data_bit_cell_ = true; + if (on_bit && preamble_bubble_start_valid_) + { + int64_t pe_start = static_cast(preamble_bubble_start_sample_); + int64_t pe_end = static_cast(preamble_bubble_end_sample); + if (preamble_bubble_end_sample == 0 || pe_end < pe_start) + pe_end = static_cast(sample > 0 ? sample - 1 : sample); + IrFoxEmitBit pe{}; + pe.start_sample = pe_start; + pe.end_sample = pe_end; + pe.frame_type = IRF_FT_PREAMBLE; + fill_err_snapshot(pe); + std::strncpy(pe.bubble_text, "PRE", sizeof pe.bubble_text); + pe.bubble_text[sizeof pe.bubble_text - 1] = '\0'; + on_bit(pe); + } + preamble_bubble_start_valid_ = false; + last_processed_edge_us = t_us; + have_last_processed = true; + return; + } + } + + if (is_preamb) + { + last_processed_edge_us = t_us; + have_last_processed = true; + return; + } + + if (rise_period_us > irmax || is_buffer_overflow || rise_period_us < rise_min_us || is_wrong_pack) + { + last_processed_edge_us = t_us; + have_last_processed = true; + return; + } + + if (rising) + { + high_count = low_count = all_count = 0; + bool invert_err = false; + + uint64_t cell_start_s = rise_period_anchor_sample_; + const uint64_t cell_end_s = sample > 0 ? sample - 1 : sample; + // После prev_rise += half period якорь может оказаться близко к текущему подъёму; + // max(anchor, prev_fall) > cell_end даёт пустой интервал — бабл первого бита пропадает. + if (trim_first_data_bit_cell_ && is_data && i_data_buffer == 0) + { + const uint64_t trimmed = std::max(cell_start_s, prev_fall_sample); + if (trimmed <= cell_end_s) + cell_start_s = trimmed; + } + + if (irfox::aroundRisePeriod(rise_period_us, rise_sync_time_us)) + { + if (high_time_us > low_time_us) + write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::WithBubble); + else + write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::WithBubble); + } + else + { + uint16_t hc = ceil_div_u16(static_cast(high_time_us > 0xFFFF ? 0xFFFF : high_time_us), + static_cast(rise_sync_time_us)); + uint16_t lc = ceil_div_u16(static_cast(low_time_us > 0xFFFF ? 0xFFFF : low_time_us), + static_cast(rise_sync_time_us)); + uint16_t ac = ceil_div_u16(static_cast(rise_period_us > 0xFFFF ? 0xFFFF : rise_period_us), + static_cast(rise_sync_time_us)); + high_count = static_cast(hc > 127 ? 127 : hc); + low_count = static_cast(lc > 127 ? 127 : lc); + all_count = static_cast(ac > 127 ? 127 : ac); + + if (high_count == 0 && high_time_us > rise_sync_time_us / 3U) + { + high_count++; + err_other++; + } + + if (low_count + high_count > all_count) + { + if (low_count > high_count) + { + low_count = static_cast(all_count - high_count); + err_low_signal = static_cast(err_low_signal + static_cast(low_count)); + } + else if (low_count < high_count) + { + high_count = static_cast(all_count - low_count); + err_high_signal = static_cast(err_high_signal + static_cast(high_count)); + } + else if (low_count == high_count) + { + invert_err = true; + err_other = static_cast(err_other + static_cast(all_count)); + } + } + + if (low_count < high_count) + err_high_signal = static_cast(err_high_signal + static_cast(high_count)); + else + err_low_signal = static_cast(err_low_signal + static_cast(low_count)); + + const bool burst_is_data_start = is_data; + const uint64_t merge_bit_index = + burst_is_data_start ? static_cast(i_data_buffer) : static_cast(buf_bit_pos + 1); + char s_bits[20]{}; + char d_bits[20]{}; + size_t s_n = 0; + size_t d_n = 0; + int first_merge_bit = -1; + bool merge_warn = false; + auto append_merge = [&](bool as_data, bool bitv) { + if (first_merge_bit < 0) + first_merge_bit = bitv ? 1 : 0; + char* buf = as_data ? d_bits : s_bits; + size_t& n = as_data ? d_n : s_n; + if (n + 1 < sizeof s_bits) + buf[n++] = static_cast(bitv ? '1' : '0'); + }; + auto emit_merge_if_needed = [&]() { + if ((s_n == 0 && d_n == 0) || !on_bit) + return; + IrFoxEmitBit e{}; + e.start_sample = static_cast(cell_start_s); + e.end_sample = static_cast(cell_end_s); + e.frame_type = (d_n > 0) ? IRF_FT_DATA_BIT : IRF_FT_SYNC_BIT; + e.bit_value = (first_merge_bit > 0) ? 1u : 0u; + e.bit_index = merge_bit_index; + e.mflags = merge_warn ? DISPLAY_AS_WARNING_FLAG : 0; + e.invert_fix = merge_warn; + fill_err_snapshot(e); + s_bits[s_n] = '\0'; + d_bits[d_n] = '\0'; + if (s_n && d_n) + std::snprintf(e.bubble_text, sizeof e.bubble_text, "s: %s d: %s", s_bits, d_bits); + else if (s_n) + std::snprintf(e.bubble_text, sizeof e.bubble_text, "s: %s", s_bits); + else + std::memcpy(e.bubble_text, d_bits, d_n + 1); + on_bit(e); + }; + + for (int8_t i = 0; i < low_count && 8 - i; i++) + { + const bool row_is_data = is_data; + if (i == low_count - 1 && invert_err) + { + invert_err = false; + write_to_buffer(true, true, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet); + merge_warn = true; + append_merge(row_is_data, true); + } + else + { + write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet); + append_merge(row_is_data, false); + } + } + + for (int8_t i = 0; i < high_count && 8 - i; i++) + { + const bool row_is_data = is_data; + if (i == high_count - 1 && invert_err) + { + invert_err = false; + write_to_buffer(false, true, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet); + merge_warn = true; + append_merge(row_is_data, false); + } + else + { + write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet); + append_merge(row_is_data, true); + } + } + + emit_merge_if_needed(); + } + } + + last_processed_edge_us = t_us; + have_last_processed = true; +} + +void IrFoxDecoder::flushEnd(uint64_t last_sample, uint32_t fs, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt) +{ + const double t_us = sample_to_us(last_sample, fs); + listen_start(t_us); + check_timeout(t_us); + (void)on_bit; + (void)on_pkt; +} diff --git a/Analyzer/raw/IR_Fox/src/IrFoxDecoder.h b/Analyzer/raw/IR_Fox/src/IrFoxDecoder.h new file mode 100644 index 0000000..c36d6c1 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxDecoder.h @@ -0,0 +1,135 @@ +#pragma once + +#include "IrFoxProtocolConstants.h" +#include +#include + +enum IrFoxFrameType : uint8_t +{ + IRF_FT_DATA_BIT = 1, + IRF_FT_SYNC_BIT = 2, + IRF_FT_PACKET_OK = 3, + IRF_FT_PACKET_CRC_FAIL = 4, + IRF_FT_OVERFLOW = 5, + IRF_FT_ABORT = 6, + IRF_FT_PREAMBLE = 7, +}; + +/** WithBubble — вызвать on_bit; Quiet — только обновить состояние (для пакета битов с одного фронта). */ +enum class IrFoxEmitBitMode : uint8_t +{ + WithBubble, + Quiet, +}; + +struct IrFoxEmitBit +{ + int64_t start_sample; + int64_t end_sample; + uint8_t frame_type; + uint64_t bit_value; + uint64_t bit_index; + uint8_t mflags; + bool invert_fix; + uint8_t err_low; + uint8_t err_high; + uint8_t err_other; + /** Подпись бабла: "0", "1" или склейка "001"; пусто — смотреть bit_value. */ + char bubble_text[32]{}; +}; + +struct IrFoxEmitPacket +{ + int64_t start_sample; + int64_t end_sample; + bool crc_ok; + uint8_t pack_size; + uint8_t err_low; + uint8_t err_high; + uint8_t err_other; + uint8_t data_bytes[irfox::kDataByteSizeMax]; +}; + +using IrFoxOnBit = std::function; +using IrFoxOnPacket = std::function; + +class IrFoxDecoder +{ +public: + void reset(); + void processEdge(uint64_t sample, bool rising, uint32_t sample_rate_hz, const IrFoxOnBit& on_bit, + const IrFoxOnPacket& on_pkt); + void flushEnd(uint64_t last_sample, uint32_t sample_rate_hz, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt); + +private: + static uint16_t ceil_div_u16(uint16_t val, uint16_t divider); + static uint8_t crc8(const uint8_t* data, uint8_t start, uint8_t end, uint8_t poly); + bool crc_check(uint8_t len, uint16_t& crc_out); + + void first_rx(); + void listen_start(double t_us); + void check_timeout(double t_us); + void write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_t cell_start_s, uint64_t cell_end_s, + const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt, + IrFoxEmitBitMode emit_mode = IrFoxEmitBitMode::WithBubble); + + double sample_to_us(uint64_t sample, uint32_t fs) const { return double(sample) * 1e6 / double(fs); } + + // --- state (mirror IR_DecoderRaw) --- + uint8_t data_buffer[irfox::kDataByteSizeMax]{}; + uint8_t err_low_signal = 0; + uint8_t err_high_signal = 0; + uint8_t err_other = 0; + + bool is_available = false; + uint16_t pack_size = 0; + uint16_t crc_value = 0; + bool is_recive = false; + bool is_recive_raw = false; + bool is_preamb = false; + bool is_buffer_overflow = false; + bool is_wrong_pack = false; + + uint32_t rise_sync_time_us = irfox::kBitTimeUs; + + double prev_rise_us = 0; + double prev_fall_us = 0; + uint64_t prev_rise_sample = 0; + uint64_t prev_fall_sample = 0; + /** Сэмпл предыдущего нарастающего фронта до обновления на текущем тике (граница ячейки бита, см. IR_DecoderRaw::tick). */ + uint64_t rise_period_anchor_sample_ = 0; + uint64_t preamble_bubble_start_sample_ = 0; + bool preamble_bubble_start_valid_ = false; + bool trim_first_data_bit_cell_ = false; + + double last_edge_time_us = 0; + uint64_t last_edge_sample = 0; + double last_processed_edge_us = 0; + bool have_last_processed = false; + + void fill_err_snapshot(IrFoxEmitBit& e) const + { + e.err_low = err_low_signal; + e.err_high = err_high_signal; + e.err_other = err_other; + } + + uint32_t rise_period_us = 0; + uint32_t high_time_us = 0; + uint32_t low_time_us = 0; + + int8_t high_count = 0; + int8_t low_count = 0; + int8_t all_count = 0; + + uint16_t wrong_counter = 0; + int8_t preamb_front_counter = 0; + int16_t buf_bit_pos = 0; + bool is_data = true; + uint16_t i_data_buffer = 0; + uint16_t next_control_bit = irfox::kBitPerByte; + uint8_t i_sync_bit = 0; + uint8_t err_sync_bit = 0; + uint16_t error_counter = 0; + uint8_t msg_type_receive = 0; +}; diff --git a/Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h b/Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h new file mode 100644 index 0000000..63ff42b --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h @@ -0,0 +1,71 @@ +#pragma once + +#include + +namespace irfox { + +constexpr uint32_t kCarrierPeriodUs = 1000000U / 38000U; +constexpr uint32_t kBitActiveTakts = 25U; +constexpr uint32_t kBitPauseTakts = 12U; +constexpr uint32_t kBitTakts = kBitActiveTakts + kBitPauseTakts; +constexpr uint32_t kBitTimeUs = kBitTakts * kCarrierPeriodUs; +constexpr uint32_t kToleranceUs = 300U; + +/** Мин. длительность плато (мкс) для потокового анти-глитча в анализаторе; согласовано с IR_INPUT_MIN_PULSE_US. */ +constexpr uint32_t kMinFilteredPulseUs = 10U; + +constexpr uint8_t kBitPerByte = 8U; +constexpr uint8_t kMsgBytes = 1; +constexpr uint8_t kAddrBytes = 2; +constexpr uint8_t kCrcBytes = 2; +constexpr uint8_t kPoly1 = 0x31; +constexpr uint8_t kPoly2 = 0x8C; +constexpr uint8_t kSyncBits = 3U; +constexpr uint8_t kBytePerPack = 31; +constexpr uint8_t kDataByteSizeMax = + static_cast(kMsgBytes + kAddrBytes + kAddrBytes + kBytePerPack + kCrcBytes); + +constexpr uint8_t kPreambPulse = 3; +constexpr uint8_t kPreambFronts = kPreambPulse * 2U; + +/** Отброс ложного подъёма после микро-LOW в паузе; зеркало IR_config.h (прошивка). */ +#ifndef IRFOX_SHORT_LOW_GLITCH_REJECT +#define IRFOX_SHORT_LOW_GLITCH_REJECT 1 +#endif +#ifndef IRFOX_GLITCH_REJECT_PHASE_NUDGE +#define IRFOX_GLITCH_REJECT_PHASE_NUDGE 1 +#endif +#ifndef IRFOX_MICRO_GAP_RISE_REJECT +#define IRFOX_MICRO_GAP_RISE_REJECT 1 +#endif + +inline uint32_t irTimeoutUs(uint32_t riseSyncTimeUs) +{ + const uint32_t riseMax = riseSyncTimeUs + kToleranceUs; + return riseMax * (8U + kSyncBits + 1U); +} + +/** Как IR_DecoderRaw.h: aroundRise(t) → riseTimeMin < t && t < riseTimeMax (ветка STM32DMA). */ +inline bool aroundRisePeriod(uint32_t periodUs, uint32_t riseSyncTimeUs) +{ + const uint32_t lo = riseSyncTimeUs > kToleranceUs ? riseSyncTimeUs - kToleranceUs : 0U; + const uint32_t hi = riseSyncTimeUs + kToleranceUs; + return lo < periodUs && periodUs < hi; +} + +inline void irfoxGlitchPhaseNudgeUs(double edge_us, uint32_t rise_sync_us, double& prev_rise_us) +{ +#if IRFOX_GLITCH_REJECT_PHASE_NUDGE + if (!(edge_us > static_cast(rise_sync_us))) + return; + const double nudged = edge_us - static_cast(rise_sync_us); + if (nudged > prev_rise_us && nudged < edge_us) + prev_rise_us = nudged; +#else + (void)edge_us; + (void)rise_sync_us; + (void)prev_rise_us; +#endif +} + +} // namespace irfox diff --git a/Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.cpp b/Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.cpp new file mode 100644 index 0000000..3aae375 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.cpp @@ -0,0 +1,67 @@ +#include "IrFoxSimulationDataGenerator.h" +#include "IrFoxAnalyzerSettings.h" +#include + +IrFoxSimulationDataGenerator::IrFoxSimulationDataGenerator() +{ +} + +IrFoxSimulationDataGenerator::~IrFoxSimulationDataGenerator() +{ +} + +void IrFoxSimulationDataGenerator::Initialize(U32 simulation_sample_rate, IrFoxAnalyzerSettings* settings) +{ + mSimulationSampleRateHz = simulation_sample_rate; + mSettings = settings; + + mIrSimulationData.SetChannel(mSettings->mInputChannel); + mIrSimulationData.SetSampleRate(simulation_sample_rate); + // Как у «покоя» на выходе TSOP: линия подтянута вверх + mIrSimulationData.SetInitialBitState(BIT_HIGH); +} + +void IrFoxSimulationDataGenerator::EmitIdle(U32 samples) +{ + mIrSimulationData.Advance(samples); +} + +void IrFoxSimulationDataGenerator::EmitLow(U32 samples) +{ + mIrSimulationData.TransitionIfNeeded(BIT_LOW); + mIrSimulationData.Advance(samples); +} + +void IrFoxSimulationDataGenerator::EmitHigh(U32 samples) +{ + mIrSimulationData.TransitionIfNeeded(BIT_HIGH); + mIrSimulationData.Advance(samples); +} + +U32 IrFoxSimulationDataGenerator::GenerateSimulationData(U64 largest_sample_requested, U32 sample_rate, + SimulationChannelDescriptor** simulation_channel) +{ + const U64 adjusted_largest_sample_requested = + AnalyzerHelpers::AdjustSimulationTargetSample(largest_sample_requested, sample_rate, mSimulationSampleRateHz); + + // Упрощённый «пакет»: несколько импульсов вниз (активный уровень приёмника). + while (mIrSimulationData.GetCurrentSampleNumber() < adjusted_largest_sample_requested) + { + const U32 us_to_samples = mSimulationSampleRateHz / 1000000; + if (us_to_samples == 0) + break; + + EmitIdle(500 * us_to_samples); + EmitLow(4500 * us_to_samples); + EmitHigh(4500 * us_to_samples); + EmitLow(4500 * us_to_samples); + EmitHigh(4500 * us_to_samples); + EmitLow(560 * us_to_samples); + EmitHigh(560 * us_to_samples); + EmitLow(560 * us_to_samples); + EmitHigh(20000 * us_to_samples); + } + + *simulation_channel = &mIrSimulationData; + return 1; +} diff --git a/Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.h b/Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.h new file mode 100644 index 0000000..00fd2c3 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.h @@ -0,0 +1,27 @@ +#ifndef IRFOX_SIMULATION_DATA_GENERATOR +#define IRFOX_SIMULATION_DATA_GENERATOR + +#include + +class IrFoxAnalyzerSettings; + +class IrFoxSimulationDataGenerator +{ +public: + IrFoxSimulationDataGenerator(); + ~IrFoxSimulationDataGenerator(); + + void Initialize(U32 simulation_sample_rate, IrFoxAnalyzerSettings* settings); + U32 GenerateSimulationData(U64 newest_sample_requested, U32 sample_rate, SimulationChannelDescriptor** simulation_channel); + +protected: + IrFoxAnalyzerSettings* mSettings; + U32 mSimulationSampleRateHz; + SimulationChannelDescriptor mIrSimulationData; + + void EmitIdle(U32 samples); + void EmitLow(U32 samples); + void EmitHigh(U32 samples); +}; + +#endif diff --git a/Analyzer/raw/PulseLengthStat/.github/workflows/build.yml b/Analyzer/raw/PulseLengthStat/.github/workflows/build.yml new file mode 100644 index 0000000..90a4b51 --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/.github/workflows/build.yml @@ -0,0 +1,52 @@ +name: Build + +on: + push: + branches: [master, main] + tags: + - "*" + pull_request: + branches: [master, main] + +jobs: + windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Build + run: | + cmake -B ${{github.workspace}}/Analyzer/raw/PulseLengthStat/build -S ${{github.workspace}}/Analyzer/raw/PulseLengthStat -A x64 + cmake --build ${{github.workspace}}/Analyzer/raw/PulseLengthStat/build --config Release + - uses: actions/upload-artifact@v4 + with: + name: windows + path: ${{github.workspace}}/Analyzer/raw/dll/*.dll + + macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Build + run: | + cmake -B ${{github.workspace}}/Analyzer/raw/PulseLengthStat/build -S ${{github.workspace}}/Analyzer/raw/PulseLengthStat -DCMAKE_BUILD_TYPE=Release + cmake --build ${{github.workspace}}/Analyzer/raw/PulseLengthStat/build + - uses: actions/upload-artifact@v4 + with: + name: macos + path: ${{github.workspace}}/Analyzer/raw/dll/*.so + + linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build + run: | + cmake -B ${{github.workspace}}/Analyzer/raw/PulseLengthStat/build -S ${{github.workspace}}/Analyzer/raw/PulseLengthStat -DCMAKE_BUILD_TYPE=Release + cmake --build ${{github.workspace}}/Analyzer/raw/PulseLengthStat/build + env: + CC: gcc-10 + CXX: g++-10 + - uses: actions/upload-artifact@v4 + with: + name: linux + path: ${{github.workspace}}/Analyzer/raw/dll/*.so diff --git a/Analyzer/raw/PulseLengthStat/.gitignore b/Analyzer/raw/PulseLengthStat/.gitignore new file mode 100644 index 0000000..01ee8ea --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/.gitignore @@ -0,0 +1,3 @@ +/build +/build-nmake +.DS_Store diff --git a/Analyzer/raw/PulseLengthStat/CMakeLists.txt b/Analyzer/raw/PulseLengthStat/CMakeLists.txt new file mode 100644 index 0000000..e7756de --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.13) + +project(PulseLengthStatAnalyzer) + +add_definitions(-DLOGIC2) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) + +include(ExternalAnalyzerSDK) + +set(SOURCES + src/PulseLengthStatAnalyzer.cpp + src/PulseLengthStatAnalyzer.h + src/PulseLengthStatAnalyzerResults.cpp + src/PulseLengthStatAnalyzerResults.h + src/PulseLengthStatAnalyzerSettings.cpp + src/PulseLengthStatAnalyzerSettings.h + src/PulseLengthStatSimulationDataGenerator.cpp + src/PulseLengthStatSimulationDataGenerator.h +) + +add_analyzer_plugin(${PROJECT_NAME} SOURCES ${SOURCES}) diff --git a/Analyzer/raw/PulseLengthStat/CMakePresets.json b/Analyzer/raw/PulseLengthStat/CMakePresets.json new file mode 100644 index 0000000..d0ed249 --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/CMakePresets.json @@ -0,0 +1,49 @@ +{ + "version": 3, + "cmakeMinimumRequired": { + "major": 3, + "minor": 19, + "patch": 0 + }, + "configurePresets": [ + { + "name": "win-vs2022-x64", + "displayName": "Visual Studio 2022 (x64)", + "generator": "Visual Studio 17 2022", + "architecture": "x64", + "binaryDir": "${sourceDir}/build" + }, + { + "name": "win-vs2019-x64", + "displayName": "Visual Studio 2019 (x64)", + "generator": "Visual Studio 16 2019", + "architecture": "x64", + "binaryDir": "${sourceDir}/build" + }, + { + "name": "win-nmake-release", + "displayName": "NMake Release (x64 Native Tools Command Prompt)", + "generator": "NMake Makefiles", + "binaryDir": "${sourceDir}/build-nmake", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + } + ], + "buildPresets": [ + { + "name": "win-release", + "configurePreset": "win-vs2022-x64", + "configuration": "Release" + }, + { + "name": "win-release-vs2019", + "configurePreset": "win-vs2019-x64", + "configuration": "Release" + }, + { + "name": "nmake-release", + "configurePreset": "win-nmake-release" + } + ] +} diff --git a/Analyzer/raw/PulseLengthStat/build_msvc.bat b/Analyzer/raw/PulseLengthStat/build_msvc.bat new file mode 100644 index 0000000..9bad4a4 --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/build_msvc.bat @@ -0,0 +1,21 @@ +@echo off +setlocal EnableDelayedExpansion +cd /d "%~dp0" + +if not exist build\CMakeCache.txt ( + echo Run configure_msvc.bat first. + exit /b 1 +) + +set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" +for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSINSTALL=%%i" +if not defined VSINSTALL ( + echo MSVC not found. Add C++ workload in Visual Studio Installer. + exit /b 1 +) +call "!VSINSTALL!\Common7\Tools\VsDevCmd.bat" -arch=x64 -host_arch=x64 +if errorlevel 1 exit /b 1 + +cmake --build build +pause +exit /b %ERRORLEVEL% diff --git a/Analyzer/raw/PulseLengthStat/cmake/ExternalAnalyzerSDK.cmake b/Analyzer/raw/PulseLengthStat/cmake/ExternalAnalyzerSDK.cmake new file mode 100644 index 0000000..10ca7ba --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/cmake/ExternalAnalyzerSDK.cmake @@ -0,0 +1,66 @@ +include(FetchContent) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED YES) + +if(NOT CMAKE_RUNTIME_OUTPUT_DIRECTORY OR NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin/) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin/) +endif() + +if(NOT TARGET Saleae::AnalyzerSDK) + FetchContent_Declare( + analyzersdk + GIT_REPOSITORY https://github.com/saleae/AnalyzerSDK.git + GIT_TAG master + GIT_SHALLOW True + GIT_PROGRESS True + ) + + FetchContent_GetProperties(analyzersdk) + + if(NOT analyzersdk_POPULATED) + FetchContent_Populate(analyzersdk) + include(${analyzersdk_SOURCE_DIR}/AnalyzerSDKConfig.cmake) + + if(APPLE OR WIN32) + get_target_property(analyzersdk_lib_location Saleae::AnalyzerSDK IMPORTED_LOCATION) + if(CMAKE_LIBRARY_OUTPUT_DIRECTORY) + file(COPY ${analyzersdk_lib_location} DESTINATION ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}) + else() + message(WARNING "Please define CMAKE_RUNTIME_OUTPUT_DIRECTORY and CMAKE_LIBRARY_OUTPUT_DIRECTORY if you want unit tests to locate ${analyzersdk_lib_location}") + endif() + endif() + endif() +endif() + +# Shared folder for all Saleae LLA plugins in this repo: Analyzer/raw/dll +set(ANALYZER_DLL_OUT_DIR "${CMAKE_SOURCE_DIR}/../dll") +get_filename_component(ANALYZER_DLL_OUT_DIR "${ANALYZER_DLL_OUT_DIR}" ABSOLUTE) +file(MAKE_DIRECTORY "${ANALYZER_DLL_OUT_DIR}") + +function(add_analyzer_plugin TARGET) + set(options) + set(single_value_args) + set(multi_value_args SOURCES) + cmake_parse_arguments(_p "${options}" "${single_value_args}" "${multi_value_args}" ${ARGN}) + + add_library(${TARGET} MODULE ${_p_SOURCES}) + target_link_libraries(${TARGET} PRIVATE Saleae::AnalyzerSDK) + + set(ANALYZER_DESTINATION "Analyzers") + install(TARGETS ${TARGET} RUNTIME DESTINATION ${ANALYZER_DESTINATION} + LIBRARY DESTINATION ${ANALYZER_DESTINATION}) + + set_target_properties(${TARGET} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${ANALYZER_DLL_OUT_DIR}" + LIBRARY_OUTPUT_DIRECTORY "${ANALYZER_DLL_OUT_DIR}") + if(CMAKE_CONFIGURATION_TYPES) + foreach(CFG ${CMAKE_CONFIGURATION_TYPES}) + string(TOUPPER ${CFG} CFG_UPPER) + set_target_properties(${TARGET} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY_${CFG_UPPER} "${ANALYZER_DLL_OUT_DIR}" + LIBRARY_OUTPUT_DIRECTORY_${CFG_UPPER} "${ANALYZER_DLL_OUT_DIR}") + endforeach() + endif() +endfunction() diff --git a/Analyzer/raw/PulseLengthStat/configure_msvc.bat b/Analyzer/raw/PulseLengthStat/configure_msvc.bat new file mode 100644 index 0000000..04288dc --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/configure_msvc.bat @@ -0,0 +1,39 @@ +@echo off +setlocal EnableDelayedExpansion +cd /d "%~dp0" + +echo === PulseLengthStatAnalyzer: configure with MSVC === +echo. + +set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" +if not exist "%VSWHERE%" ( + echo [ERROR] vswhere not found. Install one of: + echo - Visual Studio 2022 with workload "Desktop development with C++" + echo - Build Tools for Visual Studio 2022: https://visualstudio.microsoft.com/visual-cpp-build-tools/ + echo ^(select "Desktop development with C++" / MSVC, Windows SDK^) + exit /b 1 +) + +for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSINSTALL=%%i" +if not defined VSINSTALL ( + echo [ERROR] MSVC toolset not found. Add "Desktop development with C++" in Visual Studio Installer. + exit /b 1 +) + +echo Found: !VSINSTALL! +call "!VSINSTALL!\Common7\Tools\VsDevCmd.bat" -arch=x64 -host_arch=x64 +if errorlevel 1 exit /b 1 + +if exist build rmdir /s /q build +if exist build-nmake rmdir /s /q build-nmake +mkdir build +cd build + +cmake .. -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release +if errorlevel 1 exit /b 1 + +echo. +echo Configure OK. Build: build_msvc.bat ^(or from same VS env: cd build ^& cmake --build .^) +echo Output DLL: ..\dll\ ^(all analyzers share this folder^) +pause +exit /b 0 diff --git a/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.cpp b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.cpp new file mode 100644 index 0000000..84d42dc --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.cpp @@ -0,0 +1,110 @@ +#include "PulseLengthStatAnalyzer.h" +#include "PulseLengthStatAnalyzerSettings.h" +#include + +// One frame per stable level between edges. mData1 = duration in samples; mFlags: 1 = HIGH, 0 = LOW. + +PulseLengthStatAnalyzer::PulseLengthStatAnalyzer() + : Analyzer2(), + mSettings(), + mSimulationInitilized(false) +{ + SetAnalyzerSettings(&mSettings); +} + +PulseLengthStatAnalyzer::~PulseLengthStatAnalyzer() +{ + KillThread(); +} + +void PulseLengthStatAnalyzer::SetupResults() +{ + mResults.reset(new PulseLengthStatAnalyzerResults(this, &mSettings)); + SetAnalyzerResults(mResults.get()); + mResults->AddChannelBubblesWillAppearOn(mSettings.mInputChannel); +} + +void PulseLengthStatAnalyzer::WorkerThread() +{ + mChannelData = GetAnalyzerChannelData(mSettings.mInputChannel); + + U32 frames_since_commit = 0; + const U32 kCommitBatch = 256; + + for (;;) + { + CheckIfThreadShouldExit(); + + const U64 segment_start = mChannelData->GetSampleNumber(); + const BitState level = mChannelData->GetBitState(); + + mChannelData->AdvanceToNextEdge(); + + const U64 edge_sample = mChannelData->GetSampleNumber(); + if (edge_sample == segment_start) + break; + + const U64 duration_samples = edge_sample - segment_start; + const U64 end_inclusive = edge_sample > segment_start ? edge_sample - 1 : segment_start; + + Frame frame; + frame.mData1 = duration_samples; + frame.mFlags = (level == BIT_HIGH) ? 1 : 0; + frame.mStartingSampleInclusive = static_cast(segment_start); + frame.mEndingSampleInclusive = static_cast(end_inclusive); + + mResults->AddFrame(frame); + if (++frames_since_commit >= kCommitBatch) + { + mResults->CommitResults(); + frames_since_commit = 0; + } + ReportProgress(edge_sample); + } + + if (frames_since_commit != 0) + mResults->CommitResults(); +} + +bool PulseLengthStatAnalyzer::NeedsRerun() +{ + return false; +} + +U32 PulseLengthStatAnalyzer::GenerateSimulationData(U64 minimum_sample_index, U32 device_sample_rate, + SimulationChannelDescriptor** simulation_channels) +{ + if (mSimulationInitilized == false) + { + mSimulationDataGenerator.Initialize(GetSimulationSampleRate(), &mSettings); + mSimulationInitilized = true; + } + + return mSimulationDataGenerator.GenerateSimulationData(minimum_sample_index, device_sample_rate, + simulation_channels); +} + +U32 PulseLengthStatAnalyzer::GetMinimumSampleRateHz() +{ + return 200000; +} + +const char* PulseLengthStatAnalyzer::GetAnalyzerName() const +{ + return "Pulse Length Stat"; +} + +const char* GetAnalyzerName() +{ + return "Pulse Length Stat"; +} + +Analyzer* CreateAnalyzer() +{ + return new PulseLengthStatAnalyzer(); +} + +void DestroyAnalyzer(Analyzer* analyzer) +{ + delete analyzer; +} diff --git a/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.h b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.h new file mode 100644 index 0000000..29c5714 --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.h @@ -0,0 +1,39 @@ +#ifndef PULSELENGTHSTAT_ANALYZER_H +#define PULSELENGTHSTAT_ANALYZER_H + +#include +#include "PulseLengthStatAnalyzerSettings.h" +#include "PulseLengthStatAnalyzerResults.h" +#include "PulseLengthStatSimulationDataGenerator.h" +#include + +class ANALYZER_EXPORT PulseLengthStatAnalyzer : public Analyzer2 +{ +public: + PulseLengthStatAnalyzer(); + virtual ~PulseLengthStatAnalyzer(); + + virtual void SetupResults(); + virtual void WorkerThread(); + + virtual U32 GenerateSimulationData(U64 newest_sample_requested, U32 sample_rate, + SimulationChannelDescriptor** simulation_channels); + virtual U32 GetMinimumSampleRateHz(); + + virtual const char* GetAnalyzerName() const; + virtual bool NeedsRerun(); + +protected: + PulseLengthStatAnalyzerSettings mSettings; + std::unique_ptr mResults; + AnalyzerChannelData* mChannelData; + + PulseLengthStatSimulationDataGenerator mSimulationDataGenerator; + bool mSimulationInitilized; +}; + +extern "C" ANALYZER_EXPORT const char* __cdecl GetAnalyzerName(); +extern "C" ANALYZER_EXPORT Analyzer* __cdecl CreateAnalyzer(); +extern "C" ANALYZER_EXPORT void __cdecl DestroyAnalyzer(Analyzer* analyzer); + +#endif diff --git a/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerResults.cpp b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerResults.cpp new file mode 100644 index 0000000..40076c7 --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerResults.cpp @@ -0,0 +1,107 @@ +#include "PulseLengthStatAnalyzerResults.h" +#include +#include "PulseLengthStatAnalyzer.h" +#include "PulseLengthStatAnalyzerSettings.h" +#include +#include + +PulseLengthStatAnalyzerResults::PulseLengthStatAnalyzerResults(PulseLengthStatAnalyzer* analyzer, + PulseLengthStatAnalyzerSettings* settings) + : AnalyzerResults(), + mSettings(settings), + mAnalyzer(analyzer) +{ +} + +PulseLengthStatAnalyzerResults::~PulseLengthStatAnalyzerResults() +{ +} + +static void FormatDurationUs(U64 duration_samples, U32 sample_rate_hz, char* out, size_t out_sz) +{ + if (sample_rate_hz == 0) + { + snprintf(out, out_sz, "? us"); + return; + } + const double us = double(duration_samples) * 1e6 / double(sample_rate_hz); + snprintf(out, out_sz, "%.2f us", us); +} + +void PulseLengthStatAnalyzerResults::GenerateBubbleText(U64 frame_index, Channel& channel, DisplayBase display_base) +{ + (void)display_base; + ClearResultStrings(); + Frame frame = GetFrame(frame_index); + + const U32 fs = mAnalyzer->GetSampleRate(); + char dur[64]; + FormatDurationUs(frame.mData1, fs, dur, sizeof dur); + + const char* lev = (frame.mFlags != 0) ? "HIGH" : "LOW"; + char line[160]; + snprintf(line, sizeof line, "%s %s", lev, dur); + AddResultString(line); +} + +void PulseLengthStatAnalyzerResults::GenerateExportFile(const char* file, DisplayBase display_base, U32 export_type_user_id) +{ + (void)export_type_user_id; + (void)display_base; + std::ofstream file_stream(file, std::ios::out); + + const U64 trigger_sample = mAnalyzer->GetTriggerSample(); + const U32 sample_rate = mAnalyzer->GetSampleRate(); + + file_stream << "Time [s],Level,Duration_samples,Duration_us" << std::endl; + + const U64 num_frames = GetNumFrames(); + for (U32 i = 0; i < num_frames; i++) + { + Frame frame = GetFrame(i); + + char time_str[128]; + AnalyzerHelpers::GetTimeString(frame.mStartingSampleInclusive, trigger_sample, sample_rate, time_str, 128); + + char dur_us[64]; + FormatDurationUs(frame.mData1, sample_rate, dur_us, sizeof dur_us); + + file_stream << time_str << "," << ((frame.mFlags != 0) ? "HIGH" : "LOW") << "," << frame.mData1 << "," + << dur_us << std::endl; + + if (UpdateExportProgressAndCheckForCancel(i, num_frames) == true) + { + file_stream.close(); + return; + } + } + + file_stream.close(); +} + +void PulseLengthStatAnalyzerResults::GenerateFrameTabularText(U64 frame_index, DisplayBase display_base) +{ +#ifdef SUPPORTS_PROTOCOL_SEARCH + (void)display_base; + Frame frame = GetFrame(frame_index); + ClearTabularText(); + + const U32 fs = mAnalyzer->GetSampleRate(); + char dur[64]; + FormatDurationUs(frame.mData1, fs, dur, sizeof dur); + AddTabularText((frame.mFlags != 0) ? "H" : "L"); + AddTabularText(dur); +#endif +} + +void PulseLengthStatAnalyzerResults::GeneratePacketTabularText(U64 packet_id, DisplayBase display_base) +{ + (void)packet_id; + (void)display_base; +} + +void PulseLengthStatAnalyzerResults::GenerateTransactionTabularText(U64 transaction_id, DisplayBase display_base) +{ + (void)transaction_id; + (void)display_base; +} diff --git a/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerResults.h b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerResults.h new file mode 100644 index 0000000..8a1d355 --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerResults.h @@ -0,0 +1,27 @@ +#ifndef PULSELENGTHSTAT_ANALYZER_RESULTS +#define PULSELENGTHSTAT_ANALYZER_RESULTS + +#include + +class PulseLengthStatAnalyzer; +class PulseLengthStatAnalyzerSettings; + +class PulseLengthStatAnalyzerResults : public AnalyzerResults +{ +public: + PulseLengthStatAnalyzerResults(PulseLengthStatAnalyzer* analyzer, PulseLengthStatAnalyzerSettings* settings); + virtual ~PulseLengthStatAnalyzerResults(); + + virtual void GenerateBubbleText(U64 frame_index, Channel& channel, DisplayBase display_base); + virtual void GenerateExportFile(const char* file, DisplayBase display_base, U32 export_type_user_id); + + virtual void GenerateFrameTabularText(U64 frame_index, DisplayBase display_base); + virtual void GeneratePacketTabularText(U64 packet_id, DisplayBase display_base); + virtual void GenerateTransactionTabularText(U64 transaction_id, DisplayBase display_base); + +protected: + PulseLengthStatAnalyzerSettings* mSettings; + PulseLengthStatAnalyzer* mAnalyzer; +}; + +#endif diff --git a/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerSettings.cpp b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerSettings.cpp new file mode 100644 index 0000000..ec939b9 --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerSettings.cpp @@ -0,0 +1,62 @@ +#include "PulseLengthStatAnalyzerSettings.h" +#include + +PulseLengthStatAnalyzerSettings::PulseLengthStatAnalyzerSettings() + : mInputChannel(UNDEFINED_CHANNEL), + mInputChannelInterface() +{ + mInputChannelInterface.SetTitleAndTooltip( + "Input", + "Digital channel: one frame per stable level between edges (duration in samples / us)."); + mInputChannelInterface.SetChannel(mInputChannel); + + AddInterface(&mInputChannelInterface); + + AddExportOption(0, "Export as text/csv file"); + AddExportExtension(0, "text", "txt"); + AddExportExtension(0, "csv", "csv"); + + ClearChannels(); + AddChannel(mInputChannel, "Input", false); +} + +PulseLengthStatAnalyzerSettings::~PulseLengthStatAnalyzerSettings() +{ +} + +bool PulseLengthStatAnalyzerSettings::SetSettingsFromInterfaces() +{ + mInputChannel = mInputChannelInterface.GetChannel(); + + ClearChannels(); + AddChannel(mInputChannel, "Pulse Length Stat", true); + + return true; +} + +void PulseLengthStatAnalyzerSettings::UpdateInterfacesFromSettings() +{ + mInputChannelInterface.SetChannel(mInputChannel); +} + +void PulseLengthStatAnalyzerSettings::LoadSettings(const char* settings) +{ + SimpleArchive text_archive; + text_archive.SetString(settings); + + text_archive >> mInputChannel; + + ClearChannels(); + AddChannel(mInputChannel, "Pulse Length Stat", true); + + UpdateInterfacesFromSettings(); +} + +const char* PulseLengthStatAnalyzerSettings::SaveSettings() +{ + SimpleArchive text_archive; + + text_archive << mInputChannel; + + return SetReturnString(text_archive.GetString()); +} diff --git a/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerSettings.h b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerSettings.h new file mode 100644 index 0000000..132a65c --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerSettings.h @@ -0,0 +1,24 @@ +#ifndef PULSELENGTHSTAT_ANALYZER_SETTINGS +#define PULSELENGTHSTAT_ANALYZER_SETTINGS + +#include +#include + +class PulseLengthStatAnalyzerSettings : public AnalyzerSettings +{ +public: + PulseLengthStatAnalyzerSettings(); + virtual ~PulseLengthStatAnalyzerSettings(); + + virtual bool SetSettingsFromInterfaces(); + void UpdateInterfacesFromSettings(); + virtual void LoadSettings(const char* settings); + virtual const char* SaveSettings(); + + Channel mInputChannel; + +protected: + AnalyzerSettingInterfaceChannel mInputChannelInterface; +}; + +#endif diff --git a/Analyzer/raw/PulseLengthStat/src/PulseLengthStatSimulationDataGenerator.cpp b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatSimulationDataGenerator.cpp new file mode 100644 index 0000000..3cffbe2 --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatSimulationDataGenerator.cpp @@ -0,0 +1,65 @@ +#include "PulseLengthStatSimulationDataGenerator.h" +#include "PulseLengthStatAnalyzerSettings.h" +#include + +PulseLengthStatSimulationDataGenerator::PulseLengthStatSimulationDataGenerator() +{ +} + +PulseLengthStatSimulationDataGenerator::~PulseLengthStatSimulationDataGenerator() +{ +} + +void PulseLengthStatSimulationDataGenerator::Initialize(U32 simulation_sample_rate, PulseLengthStatAnalyzerSettings* settings) +{ + mSimulationSampleRateHz = simulation_sample_rate; + mSettings = settings; + + mSimChannel.SetChannel(mSettings->mInputChannel); + mSimChannel.SetSampleRate(simulation_sample_rate); + mSimChannel.SetInitialBitState(BIT_HIGH); +} + +void PulseLengthStatSimulationDataGenerator::EmitIdle(U32 samples) +{ + mSimChannel.Advance(samples); +} + +void PulseLengthStatSimulationDataGenerator::EmitLow(U32 samples) +{ + mSimChannel.TransitionIfNeeded(BIT_LOW); + mSimChannel.Advance(samples); +} + +void PulseLengthStatSimulationDataGenerator::EmitHigh(U32 samples) +{ + mSimChannel.TransitionIfNeeded(BIT_HIGH); + mSimChannel.Advance(samples); +} + +U32 PulseLengthStatSimulationDataGenerator::GenerateSimulationData(U64 largest_sample_requested, U32 sample_rate, + SimulationChannelDescriptor** simulation_channel) +{ + const U64 adjusted_largest_sample_requested = + AnalyzerHelpers::AdjustSimulationTargetSample(largest_sample_requested, sample_rate, mSimulationSampleRateHz); + + while (mSimChannel.GetCurrentSampleNumber() < adjusted_largest_sample_requested) + { + const U32 us_to_samples = mSimulationSampleRateHz / 1000000; + if (us_to_samples == 0) + break; + + EmitIdle(500 * us_to_samples); + EmitLow(4500 * us_to_samples); + EmitHigh(4500 * us_to_samples); + EmitLow(4500 * us_to_samples); + EmitHigh(4500 * us_to_samples); + EmitLow(560 * us_to_samples); + EmitHigh(560 * us_to_samples); + EmitLow(560 * us_to_samples); + EmitHigh(20000 * us_to_samples); + } + + *simulation_channel = &mSimChannel; + return 1; +} diff --git a/Analyzer/raw/PulseLengthStat/src/PulseLengthStatSimulationDataGenerator.h b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatSimulationDataGenerator.h new file mode 100644 index 0000000..0aa5139 --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatSimulationDataGenerator.h @@ -0,0 +1,27 @@ +#ifndef PULSELENGTHSTAT_SIMULATION_DATA_GENERATOR +#define PULSELENGTHSTAT_SIMULATION_DATA_GENERATOR + +#include + +class PulseLengthStatAnalyzerSettings; + +class PulseLengthStatSimulationDataGenerator +{ +public: + PulseLengthStatSimulationDataGenerator(); + ~PulseLengthStatSimulationDataGenerator(); + + void Initialize(U32 simulation_sample_rate, PulseLengthStatAnalyzerSettings* settings); + U32 GenerateSimulationData(U64 newest_sample_requested, U32 sample_rate, SimulationChannelDescriptor** simulation_channel); + +protected: + PulseLengthStatAnalyzerSettings* mSettings; + U32 mSimulationSampleRateHz; + SimulationChannelDescriptor mSimChannel; + + void EmitIdle(U32 samples); + void EmitLow(U32 samples); + void EmitHigh(U32 samples); +}; + +#endif diff --git a/Analyzer/raw/dll/.gitkeep b/Analyzer/raw/dll/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/IR_DMA_ISR_signal_analysis.md b/IR_DMA_ISR_signal_analysis.md new file mode 100644 index 0000000..8812651 --- /dev/null +++ b/IR_DMA_ISR_signal_analysis.md @@ -0,0 +1,141 @@ +# IR DMA vs ISR: анализ согласованности сигнала и ответа версии + +Связка с остальным пультом (модули, настройки): **[`ARCHITECTURE.md`](ARCHITECTURE.md)**. + +Документ фиксирует наблюдения по переходу машинки (проект Car) на **DMA-передачу** ИК через `IR_Encoder::setExternalTxBackend` и `IrDmaBackend`, сравнение со **старым путём** (таймер + **`_isr()`**), ручную проверку CRC по логу пульта и роль **`buildGateRuns`** в библиотеке **IR-protocol**. + +--- + +## 1. Контекст + +- До введения DMA передача шла через **`IR_Encoder::begin(..., IR_Encoder::isr)`**: на каждый тик таймера (`carrierFrec * 2`) вызывается **`_isr()`**, формируются преамбула, данные, синхробиты. +- После коммита с **IR_DMA** (`Car`, `IR.cpp`): **`beginClockOnly`**, **`setExternalTxBackend`**, фактическая модуляция — **`IrDmaBackend::start`** → **`IR_Encoder::buildGateRuns`** + DMA в **BSRR**. +- Ответ версии — один из самых **длинных** кадров (до **31 байта** полного кадра по заголовку). Короткие пакеты (эхо `Version_Query`, 8 байт) в логе остаются **FrameOK**; длинный ответ версии даёт **CRC fail** / `Frame reject`. + +--- + +## 2. Два пути: ISR и DMA + +| Этап | Старый ISR | DMA | +|------|------------|-----| +| Байты пакета + CRC | `sendDataFULL` → `sendBuffer` | То же; в `buildGateRuns` — `memcpy` в локальный буфер размером `dataByteSizeMax` | +| Развёртка в импульсы | **`_isr()`**: счётчик `toggleCounter`, ветки preamb / data / sync | **`buildGateRuns`**: RLE-сегменты `(gate, lenTicks)` → **`nextWord()`** по тикам таймера | +| Останов передачи | `signal == noSignal`, `isSending = false` | `ticksOutput >= totalTicks`, `sum(runs[i].lenTicks)` | + +Идея `buildGateRuns`: **эмулировать** шаги FSM, которые в ISR выполняются при **`toggleCounter == 0`** (см. комментарий в `IR_Encoder.cpp` рядом с внутренним `while`). + +### 2.1. Приоритеты NVIC: приём ИК выше, чем DMA передачи (STM32) + +Пока активна **внешняя** передача по DMA (`IrDmaBackend` и т.п.), таймер крутит поток запросов к DMA — срабатывают **`DMA1_Channelx_IRQn`** (половина/конец буфера и т.д.). Если их приоритет **выше**, чем у **EXTI** линии пина приёмника, обработка фронтов на входе ИК **откладывается** → растёт джиттер `micros()` и страдает заполнение `subBuffer` / журнал `@IRF1v1`, хотя алгоритм `tick`/`writeToBuffer` не менялся. + +**Требование:** числовой приоритет **приёма (EXTI)** должен быть **выше приоритета DMA передачи** (в терминах Cortex-M / STM32 HAL: **меньше** значение preempt priority у EXTI, чем у канала DMA ИК). + +**В репозитории:** + +- **`IR_Decoder`**: библиотека **не** задаёт приоритет EXTI по умолчанию. На Arduino STM32 пользователь вызывает **`setReceiveExtiPreemptPriority(preempt)`** (до или после `enable()`); после `attachInterrupt` применяется поверх приоритета ядра. Семейства с укороченной картой EXTI (C0/F0/G0/L0) — без изменения NVIC из этой функции. +- **DMA ИК-TX** (например **`Car/src/IR/IrDmaBackend.cpp`**): preempt задаётся в прошивке носителя (**`CarIrq::kIrTxDmaPreempt`** и т.д.) и должен быть **больше** (ниже срочность), чем у приёма. + +Свой проект: пользователь обязан согласовать приоритеты; **ни один** канал DMA ИК-TX не должен вытеснять EXTI приёма (меньший preempt у DMA = ошибка). + +--- + +## 3. Ключевое наблюдение: `runLenTicks = toggleCounter + 1` + +В **`IR_Encoder::buildGateRuns`** на каждой итерации внешнего цикла: + +```cpp +const uint16_t runLenTicks = (uint16_t)toggleCounterLocal + 1U; +``` + +В **`_isr()`** при стартовом **`toggleCounter == N`** выполняется **ровно N** раз ветка `if (toggleCounter) { toggleCounter--; }` подряд, пока счётчик не станет **0**; **следующий** тик попадает в `else` и делает один шаг `switch (signal)`. + +Между двумя такими визитами в `else` проходит **N тиков таймера**, не **N+1**. + +В `buildGateRuns` для того же начального `toggleCounterLocal` в run записывается **`N + 1` тик**. Это даёт **систематическое удлинение каждого сегмента на 1 тик** относительно модели «счётчик убывает N раз до нуля». + +**Следствие:** + +- `totalTicks = Σ lenTicks` в **`IrDmaBackend::startStream`** **больше**, чем число тиков, которое дал бы чистый ISR при том же пакете. +- Число внешних итераций `buildGateRuns` (шагов FSM) совпадает с числом таких сегментов; приближённо: + `totalTicks ≈ totalTicks_ISR + (число_внешних_шагов)`. + +Короткий кадр: ошибка может «теряться» в допусках приёмника. Длинный (версия) — **накопление** ошибки по времени → сдвиг границ битов → **неверные байты**, в том числе **CRC**. + +--- + +## 4. Ручная проверка CRC по логу (пульт) + +Алгоритм: **`IR_FOX::crc8`** (`IR_config.cpp`), два байта как в **`sendDataFULL`**: + +- первый байт CRC = `crc8(data, 0, packSize - 2, poly1)`; +- второй = `crc8(data, 0, packSize - 1, poly2)` (в расчёт второго входит уже первый байт CRC). + +Пример **31-байтного** кадра из лога `Frame reject`: + +- Тело **0…28** (29 байт). +- Байты **29…30** — CRC на проводе. + +Для фиксированного дампа байтов **0…28** корректная пара CRC по формуле библиотеки — **`6E 54`**, в логе на проводе — **`96 62`** → **не совпадает**; приёмник обоснованно отклоняет кадр. + +Это **не** объясняется разницей AVR vs STM32: счёт идёт по массиву `uint8_t` побайтно. + +Эхо **8 байт** `C8 FA 2A FD E8 5D AA B4`: пересчёт даёт **`AA B4`** — совпадает с последними байтами кадра → для этого пакета цепочка **байт → CRC** согласована. + +--- + +## 5. Скрипт симуляции + +В репозитории: **`docs/scripts/ir_protocol_gate_runs_sim.py`**. + +Запуск: + +```bash +python docs/scripts/ir_protocol_gate_runs_sim.py +``` + +Скрипт: + +1. Считает **CRC** для примеров пакетов (8 байт эха и 31 байт из reject). +2. Воспроизводит логику **`buildGateRuns`** (с дополнением буфера до `dataByteSizeMax`, как в C++). +3. Печатает **`totalTicks`**, число **внешних шагов** FSM и связь **`totalTicks - outer_steps`** как оценку «тиков в модели ISR без +1 на каждый шаг». + +Пример вывода (значения могут слегка отличаться при смене констант в `IR_config.h`): + +- `preambToggle = 97` +- для 8-байт пакета: сотни шагов FSM, `totalTicks` порядка тысяч тиков +- для 31-байт: больше шагов и `totalTicks` (~25k+ тиков для текущих констант) + +--- + +## 6. Связь с проектами + +- **Car** (`Executer.cpp`): ответ версии через **`IR_Module::getENC().sendData(...)`** — тот же **`sendDataFULL`**, затем **`rawSend`** → DMA. +- **ControlPointUnion** (`CustomCmd.h`, слоты): запрос версии через **`sendResp`** с **`version_query`** — задержка **`IR_ResponseDelay`**, затем **`sendData`** на адрес машинки. +- **ControlPointUnion** (`Plan_B.ino`): разбор **`version_response`** из **`gotData` / `gotBackData`** только после **успешного CRC** в декодере. + +--- + +## 7. Выводы + +1. **Байты в RAM** на передаче формируются корректно библиотекой; проблема «после DMA» укладывается в **расхождение тайминговой развёртки** (`buildGateRuns` + DMA) со **старой** развёрткой ISR, а не в «другой CRC на машинке» при неизменённой библиотеке. +2. **Подозрение №1:** `runLenTicks = toggleCounter + 1` в **`buildGateRuns`** не совпадает с числом тиков ISR между шагами FSM (**`N`** vs **`N+1`**). Требуется сверка с эталонной трассой ISR или логическим анализатором. +3. **Проверка на будущее:** сравнить побитово выходы ISR и DMA на **одном** буфере (8 и 31 байт); при необходимости поправить формулу длины run в **`IR-protocol`** и пересобрать Car и пульт. +4. При **DMA-режиме передачи** на STM32 соблюдать **приоритеты NVIC** (раздел **2.1**): приём EXTI **выше**, чем DMA ИК-TX. + +--- + +## 8. Ссылки на файлы + +| Файл | Назначение | +|------|------------| +| `Documents/Arduino/libraries/IR-protocol/IR_Encoder.cpp` | `buildGateRuns`, `_isr`, `rawSend` | +| `Documents/Arduino/libraries/IR-protocol/IR_Decoder.cpp` | `setReceiveExtiPreemptPriority` / `enable`: опциональный `NVIC_SetPriority` для EXTI (Arduino STM32) | +| `Documents/Arduino/libraries/IR-protocol/IR_config.cpp` | `crc8` | +| `Car/src/IR/IR.cpp` | `setExternalTxBackend`, `txStart` | +| `Car/src/IR/IrDmaBackend.cpp` | `startStream`, `totalTicks`, `nextWord`, NVIC DMA из `CarIrq` | +| `Car/src/IR/IR.cpp` | `setReceiveExtiPreemptPriority` + `enable` декодера | +| `ControlPointUnion/Plan_B/TestPoints/CustomCmd.h` | `sendResp` / `version_query` для тестовых слотов | + +--- + +*Документ составлен по обсуждению в чате; при смене версии IR-protocol числа констант и `totalTicks` пересчитывайте скриптом.* diff --git a/ir_protocol_gate_runs_sim.py b/ir_protocol_gate_runs_sim.py new file mode 100644 index 0000000..ae72860 --- /dev/null +++ b/ir_protocol_gate_runs_sim.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +""" +Симуляция логики IR_Encoder::buildGateRuns (IR-protocol) и утилиты CRC8. +Запуск из корня репозитория: python docs/scripts/ir_protocol_gate_runs_sim.py +Или: python ir_protocol_gate_runs_sim.py из каталога scripts/ +""" + +from __future__ import annotations + +import sys + +# --- IR_config.h (фрагмент) --- +bitPauseTakts = 12 +bitActiveTakts = 25 +preambPulse = 3 +syncBits = 3 +bitPerByte = 8 +preambToggle = ((bitPauseTakts * 2 + bitActiveTakts) * 2 - 1) +bitHigh = [(bitPauseTakts) * 2 - 1, (bitActiveTakts) * 2 - 1] +bitLow = [(bitPauseTakts // 2 + bitActiveTakts) * 2 - 1, (bitPauseTakts) - 1] + +preamb, data, sync, noSignal = 0, 1, 2, 3 +HIGH = True + + +def crc8(data: bytes, start: int, end: int, poly: int) -> int: + """Как IR_FOX::crc8 в IR_config.cpp: [start, end).""" + crc = 0xFF + for i in range(start, end): + crc ^= data[i] + for _ in range(8): + if (crc & 0x80) != 0: + crc = ((crc << 1) ^ poly) & 0xFF + else: + crc = (crc << 1) & 0xFF + return crc + + +def crc_pair_over_wire(packet: bytes) -> tuple[int, int]: + """Два байта CRC как в IR_Encoder::sendDataFULL (poly1 старший, poly2 младший).""" + ps = len(packet) + if ps < 2: + return 0, 0 + b1 = crc8(packet, 0, ps - 2, 0x31) & 0xFF + b2 = crc8(packet, 0, ps - 1, 0x8C) & 0xFF + return b1, b2 + + +# Как dataByteSizeMax в IR_config.h (msg+addr+addr+bytePerPack+crc) +DATA_BYTE_SIZE_MAX = 1 + 2 + 2 + 31 + 2 + + +def build_gate_runs(packet: bytes): + """ + Повторяет IR_Encoder::buildGateRuns: список (gate: bool, lenTicks: int), сумма lenTicks = totalTicks DMA. + Буфер дополняется нулями до dataByteSizeMax, как sendBufferLocal[dataByteSizeMax] в C++. + """ + send_len = len(packet) + send_buf = bytearray(packet) + bytes(max(0, DATA_BYTE_SIZE_MAX - len(packet))) + + toggle = preambToggle + data_bit = bitPerByte - 1 + data_byte = 0 + preamb_front = preambPulse * 2 - 1 + data_seq = bitPerByte * 2 + sync_seq = syncBits * 2 + sync_last = False + sig = preamb + state = HIGH + cur_seq = bitHigh + + runs: list[tuple[bool, int]] = [] + outer_steps = 0 + + while True: + outer_steps += 1 + gate = state + run_len = toggle + 1 # как в C++: (uint16_t)toggleCounterLocal + 1U + + if runs and runs[-1][0] == gate: + g, ln = runs[-1] + runs[-1] = (g, ln + run_len) + else: + runs.append((gate, run_len)) + + while True: + if sig == noSignal: + return runs, outer_steps + + if sig == preamb: + if preamb_front: + preamb_front -= 1 + toggle = preambToggle + break + sig = data + state = not False + continue + + if sig == data: + if data_seq: + if not (data_seq & 1): + cur_seq = bitHigh if ((send_buf[data_byte] >> data_bit) & 1) else bitLow + data_bit -= 1 + toggle = cur_seq[not state] + data_seq -= 1 + break + sync_last = send_buf[data_byte] & 1 + data_byte += 1 + data_bit = bitPerByte - 1 + data_seq = bitPerByte * 2 + sig = sync + continue + + if sig == sync: + if sync_seq: + if not (sync_seq & 1): + if sync_seq == 2: + cur_seq = bitLow if (send_buf[data_byte] & 0x80) else bitHigh + else: + cur_seq = bitLow if sync_last else bitHigh + sync_last = not sync_last + toggle = cur_seq[not state] + sync_seq -= 1 + break + sig = data + sync_seq = syncBits * 2 + if data_byte >= send_len: + sig = noSignal + continue + + return [], 0 + + state = not state + + +def main() -> int: + print("IR-protocol: preambToggle =", preambToggle) + print() + + # Пример из лога: 8-байтный эхо-пакет Version_Query (CRC OK на приёме) + echo = bytes.fromhex("C8 FA 2A FD E8 5D AA B4") + c1, c2 = crc_pair_over_wire(echo) + print("8 байт (эхо): CRC вычисленный:", f"{c1:02X}", f"{c2:02X}", "| на проводе:", f"{echo[6]:02X}", f"{echo[7]:02X}") + + runs8, steps8 = build_gate_runs(echo) + total8 = sum(r[1] for r in runs8) + print(" buildGateRuns: внешних шагов FSM =", steps8, ", totalTicks =", total8, ", число run-сегментов =", len(runs8)) + print() + + # 31 байт из лога Frame reject (пример) + reject = bytes.fromhex( + "DF 00 00 FA 2A 5E 43 61 72 5F 76 34 2E 33 2E 38 5F 5B 31 32 4D 68 7A 5D 6B ED 1D 9A 53 96 62" + ) + if len(reject) == 31: + c1, c2 = crc_pair_over_wire(reject) + print("31 байт (reject): CRC по телу 0..28 должен быть:", f"{c1:02X}", f"{c2:02X}", "| байты [29:31]:", f"{reject[29]:02X}", f"{reject[30]:02X}") + print(" Совпадение с формулой:", c1 == reject[29] and c2 == reject[30]) + + runs31, steps31 = build_gate_runs(reject) + total31 = sum(r[1] for r in runs31) + print(" buildGateRuns: внешних шагов =", steps31, ", totalTicks =", total31, ", run-сегментов =", len(runs31)) + print() + + # Связь totalTicks с моделью «N тиков на сегмент до шага FSM» + # total_build = sum(toggle_i + 1); если бы было sum(toggle_i), разница = steps + theoretical_isr_ticks = total31 - steps31 + print("Для 31-байт пакета: totalTicks (buildGateRuns) =", total31) + print(" Если каждый внешний шаг даёт +1 к длине сегмента относительно ISR (runLen = toggle+1 vs toggle),") + print(" оценка «ISR-тиков» как totalTicks - outer_steps =", theoretical_isr_ticks) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 7176fe53213a76d68782afb63a0282d8fa71bcbe Mon Sep 17 00:00:00 2001 From: DashyFox Date: Fri, 19 Jun 2026 15:26:52 +0300 Subject: [PATCH 02/24] =?UTF-8?q?fix(ir-dma):=20eliminate=20isSending=20la?= =?UTF-8?q?tch=20=E2=80=94=20free-running=20TIM,=20completion=20watchdog,?= =?UTF-8?q?=20busy()=20OR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C: remove racy activeCount_ + shared-timer gating; TIM17 free-running, per-stream DMA arming only (HAL_DMA_Start_IT/Abort_IT). Sets active before arming; never stops TIM on a shared counter. D: poll() watchdog detects ticksOutput stall (>=stallTimeoutMs_, default 100ms) and forceStop()s the stuck stream under a per-channel NVIC mask; recoveryCount()/setStallTimeoutMs() accessors. dmaErrorCb now checks !active for symmetric single-stop. E: busy() now returns true if ANY stream active (was AND) — honest contract for irAnyTxBusy()/carrier retune; does not serialize the send path. Bug: IR TX rarely latched (LED frozen on/off) because stopStream was reachable only from the DMA HT/CPLT IRQ. Hardware bring-up (logic analyzer, recoveryCount telemetry) pending. Co-Authored-By: Claude Opus 4.8 (1M context) --- IrDmaTxStm32.h | 102 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 77 insertions(+), 25 deletions(-) diff --git a/IrDmaTxStm32.h b/IrDmaTxStm32.h index 8b01eb3..56436a0 100644 --- a/IrDmaTxStm32.h +++ b/IrDmaTxStm32.h @@ -85,7 +85,6 @@ public: } s_instance = this; - activeCount_ = 0; for (uint8_t i = 0; i < streamCount_; i++) { HAL_NVIC_EnableIRQ(streams_[i].dmaIrq); @@ -98,12 +97,19 @@ public: return s_instance; } + // Fix E (контракт busy): "занят" = АКТИВЕН ХОТЯ БЫ ОДИН поток (OR). + // Раньше было AND (все потоки сразу) — почти всегда false, не отражало занятости и было + // избыточно (per-stream s.active в startStream + per-encoder isSending уже не дают повторный + // старт того же потока). OR нужен, чтобы irAnyTxBusy()/applyCarrierMultiply реально дожидались + // простоя ВСЕХ эмиттеров перед ретюном несущей. + // ВНИМАНИЕ: это НЕ сериализует независимые front/rear — путь отправки гейтится per-stream; + // внешний busy()-гейт в IR_Encoder::rawSend намеренно нейтрализован в прошивке (txBusy()=false). + // Чтобы ВКЛЮЧИТЬ сериализацию front/rear — вернуть txBusy()=busy() в Car/src/IR/IR.cpp. bool busy() const { - if (streamCount_ == 0) return false; for (uint8_t i = 0; i < streamCount_; i++) { - if (!streams_[i].active) return false; + if (streams_[i].active) return true; } - return true; + return false; } IR_SendStatus start(IR_Encoder* enc, const uint8_t* packet, uint8_t len) { @@ -126,6 +132,35 @@ public: return &streams_[streamIndex].hdma; } + // Fix D: не-IRQ страховка завершения. Вызывать из главного цикла (IR_Module::tick()). + // Штатно isSending сбрасывает только DMA HT/CPLT IRQ; если он не сработает (пропуск/слипание + // флагов, Transfer-Error, последствие любой гонки) — поток «залипает» навсегда. Здесь ловим + // застой прогресса ticksOutput по стенным часам и форсим stopStream как аварийное + // восстановление. Это recovery (не prevention): фикс C убирает саму гонку, а poll() страхует + // структурный SPOF завершения и делает редкий баг наблюдаемым (recoveryCount()). + void poll() { + const uint32_t now = millis(); + for (uint8_t i = 0; i < streamCount_; i++) { + TxStream& s = streams_[i]; + if (!s.active) continue; // atomic: bool + const uint32_t t = s.ticksOutput; // atomic: выровненный u32 на Cortex-M + if (t != s.lastTicks) { + s.lastTicks = t; + s.lastProgressMs = now; + continue; + } + if ((uint32_t)(now - s.lastProgressMs) >= stallTimeoutMs_) { + forceStop(s); + } + } + } + + /** Порог застоя для watchdog (мс). По умолчанию 100; должен быть заметно больше времени + вывода одной половины DMA-буфера на самой медленной несущей. 0 игнорируется. */ + void setStallTimeoutMs(uint32_t ms) { if (ms != 0U) stallTimeoutMs_ = ms; } + /** Сколько раз watchdog аварийно восстановил залипший поток (наблюдаемость/логи). */ + uint32_t recoveryCount() const { return recoveries_; } + private: struct TxStream { DMA_HandleTypeDef hdma{}; @@ -152,6 +187,10 @@ private: uint32_t totalTicks = 0; volatile uint32_t ticksOutput = 0; + // Fix D (watchdog): прогресс ticksOutput против стенных часов (контекст потока). + uint32_t lastTicks = 0; + uint32_t lastProgressMs = 0; + bool active = false; void resetWave() { @@ -186,29 +225,38 @@ private: TxStream streams_[MaxStreams]{}; uint8_t streamCount_ = 0; - volatile uint8_t activeCount_ = 0; + // Fix C: разделяемый activeCount_ удалён — он и был источником гонки поток↔ISR. + uint32_t stallTimeoutMs_ = 100; // Fix D + uint32_t recoveries_ = 0; // Fix D (только контекст потока) static uint32_t u32ptr(const volatile void* p) { return (uint32_t)(uintptr_t)p; } - void startTimerIfNeeded() { + // Fix C: TIM держим free-running. Включаем ИДЕМПОТЕНТНО при старте потока и НИКОГДА не + // останавливаем по разделяемому счётчику (счётчик убран → гонка поток↔ISR устранена). + // Счётчик TIM НЕ сбрасываем: сброс глитчил бы уже активный поток; для ИК ≤1 период несущей + // фазового сдвига пренебрежим. В простое (ни один DMA-канал не заармлен) update-события TIM + // генерят DMA-запросы «в никуда» — безвредно, в GPIO ничего не пишется, светодиод не дёргается. + void ensureTimerRunning() { if (htim_ == nullptr) return; - if (activeCount_ != 1) return; - - __HAL_TIM_DISABLE_DMA(htim_, TIM_DMA_UPDATE); - __HAL_TIM_CLEAR_FLAG(htim_, TIM_FLAG_UPDATE); - __HAL_TIM_SET_COUNTER(htim_, 0); __HAL_TIM_ENABLE_DMA(htim_, TIM_DMA_UPDATE); - HAL_TIM_Base_Start(htim_); + if ((htim_->Instance->CR1 & TIM_CR1_CEN) == 0U) { + __HAL_TIM_CLEAR_FLAG(htim_, TIM_FLAG_UPDATE); + HAL_TIM_Base_Start(htim_); + } } - void stopTimerIfIdle() { - if (htim_ == nullptr) return; - if (activeCount_ != 0) return; - - __HAL_TIM_DISABLE_DMA(htim_, TIM_DMA_UPDATE); - HAL_TIM_Base_Stop(htim_); + // Fix D: аварийный стоп залипшего потока из контекста потока (не ISR). Маскируем ИМЕННО IRQ + // этого DMA-канала, чтобы штатный HT/CPLT/Error-обработчик не вытеснил нас посреди stopStream + // (иначе двойной abort/externalFinishSend). Другие потоки и прочие IRQ не трогаем. + void forceStop(TxStream& s) { + HAL_NVIC_DisableIRQ(s.dmaIrq); + if (s.active) { + stopStream(s); + recoveries_++; + } + HAL_NVIC_EnableIRQ(s.dmaIrq); } static TxStream* streamFromDma(DMA_HandleTypeDef* hdma) { @@ -241,7 +289,7 @@ private: static void dmaErrorCb(DMA_HandleTypeDef* hdma) { auto* s = streamFromDma(hdma); - if (s == nullptr) return; + if (s == nullptr || !s->active) return; // симметрично HT/CPLT: одиночный stop, без двойного finish s->onError(); if (s_instance != nullptr) { s_instance->stopStream(*s); @@ -319,14 +367,20 @@ private: s.port->BSRR = s.resetWord; + // Fix C/D: помечаем active и инициализируем watchdog ДО арминга DMA — иначе ранний + // HT/CPLT IRQ при active==false выйдет из callback'а и пропустит refill → недосчёт + // ticksOutput → латч. TIM уже (или станет) free-running, старт по счётчику не нужен. + s.lastTicks = 0; + s.lastProgressMs = millis(); + s.active = true; + const uint32_t dst = u32ptr(&s.port->BSRR); if (HAL_DMA_Start_IT(&s.hdma, (uint32_t)(uintptr_t)s.dmaBuf, dst, s.bufLen) != HAL_OK) { + s.active = false; return IR_SendStatus::DmaStartFailed; } - s.active = true; - activeCount_++; - startTimerIfNeeded(); + ensureTimerRunning(); return IR_SendStatus::Success; } @@ -343,9 +397,7 @@ private: if (s.enc != nullptr) { s.enc->externalFinishSend(); } - - if (activeCount_ > 0) activeCount_--; - stopTimerIfIdle(); + // Fix C: TIM НЕ останавливаем — он free-running, без разделяемого счётчика. } }; From 55935b8b92f7b6293867f5293a6b15b5d3501eef Mon Sep 17 00:00:00 2001 From: DashyFox Date: Tue, 30 Jun 2026 12:25:36 +0300 Subject: [PATCH 03/24] fix(ir-dma): volatile active + errorCount() observability; live poll() in longData example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IrDmaTxStm32.h: TxStream::active -> volatile (делится поток<->ISR: busy()/poll()/колбэки/спин applyCarrierMultiply) - IrDmaTxStm32.h: + volatile errors_ и errorCount(), инкремент в dmaErrorCb (наблюдаемость Transfer-Error) - test_examples/longData: dmaBackend.poll() в loop, иначе watchdog завершения DMA мёртв в эталонном примере Co-Authored-By: Claude Opus 4.8 (1M context) --- IrDmaTxStm32.h | 6 +++++- test_examples/longData/longData.ino | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/IrDmaTxStm32.h b/IrDmaTxStm32.h index 56436a0..7fefd2e 100644 --- a/IrDmaTxStm32.h +++ b/IrDmaTxStm32.h @@ -160,6 +160,8 @@ public: void setStallTimeoutMs(uint32_t ms) { if (ms != 0U) stallTimeoutMs_ = ms; } /** Сколько раз watchdog аварийно восстановил залипший поток (наблюдаемость/логи). */ uint32_t recoveryCount() const { return recoveries_; } + /** Сколько раз DMA-передача завершилась по Transfer-Error (наблюдаемость/логи). */ + uint32_t errorCount() const { return errors_; } private: struct TxStream { @@ -191,7 +193,7 @@ private: uint32_t lastTicks = 0; uint32_t lastProgressMs = 0; - bool active = false; + volatile bool active = false; // Fix: делится поток<->ISR (busy()/poll()/колбэки/спин applyCarrierMultiply) — запрет кэширования компилятором void resetWave() { wave.configure(setWord, resetWord, nullptr, 0, 2, 1); @@ -228,6 +230,7 @@ private: // Fix C: разделяемый activeCount_ удалён — он и был источником гонки поток↔ISR. uint32_t stallTimeoutMs_ = 100; // Fix D uint32_t recoveries_ = 0; // Fix D (только контекст потока) + volatile uint32_t errors_ = 0; // Fix #5: счётчик Transfer-Error (пишется в ISR dmaErrorCb, читается из потока) static uint32_t u32ptr(const volatile void* p) { return (uint32_t)(uintptr_t)p; @@ -292,6 +295,7 @@ private: if (s == nullptr || !s->active) return; // симметрично HT/CPLT: одиночный stop, без двойного finish s->onError(); if (s_instance != nullptr) { + s_instance->errors_++; // Fix #5: наблюдаемость аварийных завершений по Transfer-Error s_instance->stopStream(*s); } } diff --git a/test_examples/longData/longData.ino b/test_examples/longData/longData.ino index d958800..9aca9e5 100644 --- a/test_examples/longData/longData.ino +++ b/test_examples/longData/longData.ino @@ -132,6 +132,7 @@ void setup() { void loop() { #if LONGDATA_USE_DMA IR_Encoder::tick(); + dmaBackend.poll(); // Fix D: watchdog завершения DMA-передачи (как в Car/src/IR/IR.cpp) #endif const uint32_t now = millis(); From e5ed029cfddc0c6a1ff54e59f213fb78a55aaaa7 Mon Sep 17 00:00:00 2001 From: DashyFox Date: Tue, 30 Jun 2026 14:12:28 +0300 Subject: [PATCH 04/24] fix(ir-dma): __DSB() barriers before DMA arm and after circular refill (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Перестраховка корректности (на M4F без D-кэша обычно не обязательно): завершить запись dmaBuf/BSRR до HAL_DMA_Start_IT и refill полубуфера до прохода циркулярного DMA. Co-Authored-By: Claude Opus 4.8 (1M context) --- IrDmaTxStm32.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/IrDmaTxStm32.h b/IrDmaTxStm32.h index 7fefd2e..2e71fe2 100644 --- a/IrDmaTxStm32.h +++ b/IrDmaTxStm32.h @@ -209,11 +209,13 @@ private: void onHalf() { ticksOutput += halfLen; fill(&dmaBuf[0], halfLen); + __DSB(); // Fix #8: refill первой половины виден DMA до следующего прохода кольца } void onComplete() { ticksOutput += halfLen; fill(&dmaBuf[halfLen], halfLen); + __DSB(); // Fix #8: refill второй половины виден DMA до следующего прохода кольца } void onError() {} @@ -378,6 +380,7 @@ private: s.lastProgressMs = millis(); s.active = true; + __DSB(); // Fix #8: завершить запись dmaBuf (fill) и BSRR до арминга DMA const uint32_t dst = u32ptr(&s.port->BSRR); if (HAL_DMA_Start_IT(&s.hdma, (uint32_t)(uintptr_t)s.dmaBuf, dst, s.bufLen) != HAL_OK) { s.active = false; From fc3510c425c1b1ccb31c16b44df6e76f0145e53e Mon Sep 17 00:00:00 2001 From: DashyFox Date: Wed, 1 Jul 2026 10:34:12 +0300 Subject: [PATCH 05/24] fix(rx): guard null encoder in auto-accept (B2), init accept fields (B4), fix dataBuffer OOB off-by-one (B3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B2 (HIGH): IR_Decoder::_tick — encoder->sendAccept() без null-check; у Car decoder создан с encoder==nullptr → HardFault при приёме IR_MSG_DATA_ACCEPT. Добавлен guard. B4 (MEDIUM): acceptSendTimer/isWaitingAcceptSend/addrAcceptSendTo/acceptCustomByte не инициализировались → мусор мог спонтанно дёрнуть sendAccept. Дефолты в .h. B3 (HIGH): writeToBuffer — '>' вместо '>=' → при i_dataBuffer==dataByteSizeMax*8 (304) запись dataBuffer[38] за границей 38-байтового массива (packSize==0 runaway). Валидные кадры закрываются при <=248, не затронуты. Co-Authored-By: Claude Opus 4.8 (1M context) --- IR_Decoder.cpp | 2 +- IR_Decoder.h | 8 ++++---- IR_DecoderRaw.cpp | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/IR_Decoder.cpp b/IR_Decoder.cpp index 96191e9..9998b03 100644 --- a/IR_Decoder.cpp +++ b/IR_Decoder.cpp @@ -166,7 +166,7 @@ void IR_Decoder::_tick() } if (isWaitingAcceptSend && millis() - acceptSendTimer > acceptDelay) { - encoder->sendAccept(addrAcceptSendTo, acceptCustomByte); + if (encoder != nullptr) encoder->sendAccept(addrAcceptSendTo, acceptCustomByte); // B2: у Car decoder без парного энкодера (nullptr) → был HardFault isWaitingAcceptSend = false; } } diff --git a/IR_Decoder.h b/IR_Decoder.h index 0c2e238..4b6fd99 100644 --- a/IR_Decoder.h +++ b/IR_Decoder.h @@ -10,12 +10,12 @@ private: static std::list& get_dec_list(); void _tick(); - uint32_t acceptSendTimer; - bool isWaitingAcceptSend; - uint16_t addrAcceptSendTo; + uint32_t acceptSendTimer = 0; + bool isWaitingAcceptSend = false; // B4: без инициализации мусор мог дёрнуть sendAccept при старте + uint16_t addrAcceptSendTo = 0; uint16_t acceptDelay = IR_ResponseDelay; - uint8_t acceptCustomByte; + uint8_t acceptCustomByte = 0; bool extiEnabled_ = false; bool rxExtiPreemptConfigured_ = false; diff --git a/IR_DecoderRaw.cpp b/IR_DecoderRaw.cpp index cdbf6b7..2671bfc 100644 --- a/IR_DecoderRaw.cpp +++ b/IR_DecoderRaw.cpp @@ -750,8 +750,8 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) #if !defined(IRDEBUG_SERIAL_PACK) (void)packTraceInvertFix; #endif - if (i_dataBuffer > dataByteSizeMax * 8) - { // проверка переполнения + if (i_dataBuffer >= dataByteSizeMax * 8) + { // проверка переполнения (>=: иначе при i_dataBuffer==dataByteSizeMax*8 запись dataBuffer[38] за границей массива — B3) isBufferOverflow = true; #if IR_RX_BRIEF_LOG rxBriefLog(RxBriefReason::BufferOverflow, i_dataBuffer, 0, micros()); From a8d32bbcef1b2fa94893c030df416b2b96ee12c6 Mon Sep 17 00:00:00 2001 From: DashyFox Date: Wed, 1 Jul 2026 12:13:27 +0300 Subject: [PATCH 06/24] fix(rx): guard sub-minimum packSize before crcCheck (B1 OOB read) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit packSize==1 давало crcCheck(1-crcBytes) → uint8 len=255 → crc8 читает dataBuffer[0..256] при массиве 38 (OOB-чтение ~217 байт). Триггер — лок преамбулы + первый байт с младшими 5 битами=1 (~1/32 ложных локов). Двойная защита: (1) ранний reject packSize 1..2 (< msg+crc) как битого; (2) конец кадра требует packSize>crcBytes, поэтому crcCheck(packSize-crcBytes) не уходит в underflow. Валидные кадры (packSize>=5) и будущие компактные (>=3) не затронуты. Co-Authored-By: Claude Opus 4.8 (1M context) --- IR_DecoderRaw.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/IR_DecoderRaw.cpp b/IR_DecoderRaw.cpp index 2671bfc..2be0582 100644 --- a/IR_DecoderRaw.cpp +++ b/IR_DecoderRaw.cpp @@ -862,6 +862,11 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) if (i_dataBuffer == 8 * msgBytes) { // Ппервый байт packSize = dataBuffer[0] & IR_MASK_MSG_INFO; + // B1: под-минимальная длина (1..2) физически не несёт CRC (min кадр = msg+crc = 3 байта) → шум/битьё. + // Без отсева packSize==1 даёт crcCheck(1-2) → len=255 → OOB-чтение dataBuffer[0..256] (массив 38). + // packSize>=3 (в т.ч. будущие компактные кадры) обрабатываются как обычно. + if (packSize != 0 && packSize < msgBytes + crcBytes) + isWrongPack = true; } // Тип приёма (для isReceive): выставляем сразу после первого байта, ДО проверки «Конец». @@ -872,8 +877,8 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) msgTypeReceive = (dataBuffer[0] >> 5) | 0b11111000; } - if (packSize && (i_dataBuffer == packSize * bitPerByte)) - { // Конец + if (packSize > crcBytes && (i_dataBuffer == packSize * bitPerByte)) + { // Конец (B1: packSize>crcBytes → crcCheck(packSize-crcBytes) не уходит в underflow/OOB) packInfo.buffer = dataBuffer; packInfo.crc = crcValue; packInfo.err = errors; From bf14b3d7ad768cdff9c1c35aa6e200a7dfc68338 Mon Sep 17 00:00:00 2001 From: DashyFox Date: Wed, 1 Jul 2026 13:38:51 +0300 Subject: [PATCH 07/24] fix(rx/tx): B5 pop-by-value, B6 ISR micros mask, B7 crc parens, B10 div guard, send length validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B5 (MEDIUM): RingBuffer::pop(T&) копирует под одной критсекцией; tick() перешёл на неё → нет торн-рида (внутренний interrupts() в T* pop снимал внешнюю защиту до чтения *ptr). T* pop() оставлен (не используется). B6 (MEDIUM): убрана лишняя noInterrupts/interrupts вокруг micros() в EXTI-ISR (снимала PRIMASK посреди ISR). std::function-диспетчеризация attachInterrupt — структурна, не трогаю. B7 (LOW): скобки в crcCheck (== & 0xFF по приоритету). B10 (LOW): guard деления на 0 в ceil_div (актуально только при freeFrec — НЕ включаю). send: sendDataFULL — валидация полного packSize<=31 (было len>bytePerPack=31, packSize=7+len оборачивался → тихая потеря Data payload 25..31). B8 (isSubBufferOverflow) — уже volatile, потеря флага безвредна (диагностика), не трогаю. B9 (prevRise==0 при micros()==0) — уже обработан веткой в preambleProcessEdge, намеренно. Co-Authored-By: Claude Opus 4.8 (1M context) --- IR_DecoderRaw.cpp | 19 +++++-------------- IR_Encoder.cpp | 5 ++++- RingBuffer.h | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/IR_DecoderRaw.cpp b/IR_DecoderRaw.cpp index 2be0582..66482f2 100644 --- a/IR_DecoderRaw.cpp +++ b/IR_DecoderRaw.cpp @@ -276,10 +276,8 @@ void IR_DecoderRaw::isr() // Интервалы между соседними фронтами считаются как (uint32_t)(t1 - t0) — корректно при // паузе < ~35 мин между фронтами; условие «тишина > longSilence» в preambleProcessEdge // переписано без front.time > prevRise (оно ломается при wrap). - uint32_t t; - noInterrupts(); - t = micros(); - interrupts(); + // B6: micros() в ISR не требует маскировки; прежний interrupts() посреди ISR снимал PRIMASK. + const uint32_t t = micros(); FrontStorage edge; edge.dir = port->IDR & mask; @@ -403,16 +401,8 @@ void IR_DecoderRaw::tick() // с метками ISR из очереди → ложные TIMEOUT (bits=0) каждый пакет. FrontStorage rawFront; - bool hasRawFront = false; bool processedFront = false; - noInterrupts(); - FrontStorage *rawPtr = subBuffer.pop(); - if (rawPtr != nullptr) - { - rawFront = *rawPtr; - hasRawFront = true; - } - interrupts(); + const bool hasRawFront = subBuffer.pop(rawFront); // B5: копия под одной критсекцией (без торн-рида) if (IR_INPUT_MIN_PULSE_US > 0U) { @@ -961,7 +951,7 @@ bool IR_DecoderRaw::crcCheck(uint8_t len, crc_t &crc) crc = (crc8(dataBuffer, 0, len, poly1) << 8) & ~((crc_t)0xFF); crc |= crc8(dataBuffer, 0, len + 1, poly2) & (crc_t)0xFF; - if (dataBuffer[len] == (crc >> 8) & 0xFF && + if (dataBuffer[len] == ((crc >> 8) & 0xFF) && // B7: скобки — иначе (== & 0xFF) по приоритету dataBuffer[len + 1] == (crc & 0xFF)) { crcOK = true; @@ -984,6 +974,7 @@ bool IR_DecoderRaw::availableReject() uint16_t IR_DecoderRaw::ceil_div(uint16_t val, uint16_t divider) { + if (divider == 0) return 0; // B10: защита от деления на 0 (актуально при freeFrec — сейчас выкл, не включаем) int ret = val / divider; if ((val << 4) / divider - (ret << 4) >= 8) ret++; diff --git a/IR_Encoder.cpp b/IR_Encoder.cpp index 872e47e..e92b565 100644 --- a/IR_Encoder.cpp +++ b/IR_Encoder.cpp @@ -688,7 +688,10 @@ IR_SendResult IR_Encoder::sendData(uint16_t addrTo, uint8_t *data, uint8_t len, IR_SendResult IR_Encoder::sendDataFULL(uint16_t addrFrom, uint16_t addrTo, uint8_t *data, uint8_t len, bool needAccept) { - if (len > bytePerPack) + // 5-битное поле длины => ВЕСЬ кадр ≤31 байт (для Data payload ≤24). Было `len > bytePerPack(31)` — + // неверно: packSize=7+len оборачивался в заголовке (packSize & 0x1F) при len 25..31 → кадр молча + // терялся, а send возвращал успех. Проверяем полный packSize в широком типе (uint8_t 7+len мог переполниться). + if (((uint16_t)msgBytes + addrBytes + addrBytes + len + crcBytes) > IR_MASK_MSG_INFO) { Serial.println("IR Pack to big"); return IR_SendResult(false, 0, IR_SendStatus::PayloadTooLarge); diff --git a/RingBuffer.h b/RingBuffer.h index 7bd7818..2ed56c5 100644 --- a/RingBuffer.h +++ b/RingBuffer.h @@ -36,6 +36,21 @@ public: return value; } + // B5: безопасный pop — копирует элемент под ОДНОЙ критсекцией и отдаёт по значению. + // (T* pop() отдаёт указатель во внутренний слот; его внутренний interrupts() снимает внешнюю + // защиту вызывающего ДО чтения *ptr → торн-рид, если кольцо переполнится в этом окне.) + bool pop(T &out) { + bool popped = false; + noInterrupts(); + if (!isEmpty()) { + out = data[start]; + start = (start + 1) % BufferSize; + popped = true; + } + interrupts(); + return popped; + } + private: T data[BufferSize]; unsigned int start, end; From f93f4101c398b3443643e88b4c0b1d96eed979fe Mon Sep 17 00:00:00 2001 From: DashyFox Date: Wed, 1 Jul 2026 15:20:27 +0300 Subject: [PATCH 08/24] fix(tx): correct multi-byte BACK frames (packSize used min(1,len)) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _sendBack и testSendBack считали packSize с min(uint8_t(1),len) → в кадр входил только 1 байт данных, остальные обрезались (и затирались CRC). Многобайтовый back (speed + customBackData/sendOrderedBack) слался битым. Теперь packSize = ...+len+crcBytes, плюс валидация полного packSize<=31 (IR_MASK_MSG_INFO). Для len=1 поведение не меняется. Co-Authored-By: Claude Opus 4.8 (1M context) --- IR_Encoder.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/IR_Encoder.cpp b/IR_Encoder.cpp index e92b565..c7c96c6 100644 --- a/IR_Encoder.cpp +++ b/IR_Encoder.cpp @@ -834,14 +834,17 @@ IR_SendResult IR_Encoder::sendBackTo(uint16_t addrTo, uint8_t *data, uint8_t len IR_SendResult IR_Encoder::_sendBack(bool isAdressed, uint16_t addrTo, uint8_t *data, uint8_t len) { - if (len > bytePerPack) + // Длина = ВЕСЬ кадр в 5 битах (≤31). Проверяем полный packSize. Было `len>bytePerPack` + `min(1,len)`: + // многобайтовый back (speed + customBackData) слался ОБРЕЗАННЫМ — packSize считал лишь 1 байт данных, + // остальные не влезали в кадр и затирались CRC. Теперь учитываем полный len. + if (((uint16_t)msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + len + crcBytes) > IR_MASK_MSG_INFO) { return IR_SendResult(false, 0, IR_SendStatus::PayloadTooLarge); } memset(sendBuffer, 0x00, dataByteSizeMax); uint8_t dataStart = msgBytes + addrBytes + (isAdressed ? addrBytes : 0); - uint8_t packSize = msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + min(uint8_t(1), len) + crcBytes; + uint8_t packSize = msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + len + crcBytes; uint8_t msgType = ((isAdressed ? IR_MSG_BACK_TO : IR_MSG_BACK) << 5) | ((packSize) & IR_MASK_MSG_INFO); @@ -1254,12 +1257,12 @@ uint32_t IR_Encoder::testSendBackTo(uint16_t addrTo, uint8_t *data, uint8_t len) uint32_t IR_Encoder::testSendBack(bool isAdressed, uint16_t addrTo, uint8_t *data, uint8_t len) const { - if (len > bytePerPack) + if (((uint16_t)msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + len + crcBytes) > IR_MASK_MSG_INFO) { return 0; // Возвращаем 0 для недопустимого размера } - - uint8_t packSize = msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + min(uint8_t(1), len) + crcBytes; + + uint8_t packSize = msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + len + crcBytes; return calculateSendTime(packSize); } From 873277e59f7324662b4b4737e4d54b2a24dc8d2a Mon Sep 17 00:00:00 2001 From: DashyFox Date: Wed, 1 Jul 2026 15:33:49 +0300 Subject: [PATCH 09/24] docs --- .gitignore | 1 + docs_analysis/IR_protocol_analysis.md | 142 +++++++++++++++++++++++ docs_analysis/Signal_physics_analysis.md | 72 ++++++++++++ docs_analysis/crc_combined_sim.py | 36 ++++++ docs_analysis/crc_shift_findings.md | 33 ++++++ docs_analysis/crc_shift_sim.py | 103 ++++++++++++++++ docs_analysis/crc_shift_visualize.py | 97 ++++++++++++++++ 7 files changed, 484 insertions(+) create mode 100644 docs_analysis/IR_protocol_analysis.md create mode 100644 docs_analysis/Signal_physics_analysis.md create mode 100644 docs_analysis/crc_combined_sim.py create mode 100644 docs_analysis/crc_shift_findings.md create mode 100644 docs_analysis/crc_shift_sim.py create mode 100644 docs_analysis/crc_shift_visualize.py diff --git a/.gitignore b/.gitignore index 9663919..ae3844c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ Analyzer/raw/dll/*.so Analyzer/raw/dll/*.dylib /Analyzer/raw/IR_Fox/.github **/.build +graphify-out/* diff --git a/docs_analysis/IR_protocol_analysis.md b/docs_analysis/IR_protocol_analysis.md new file mode 100644 index 0000000..350705d --- /dev/null +++ b/docs_analysis/IR_protocol_analysis.md @@ -0,0 +1,142 @@ +# IR-protocol — глубокий анализ (RX-производительность, обработка ошибок, протокол, мины) + +> Дата: 2026-07-01. Только чтение исходников. Цель: после переноса TX на DMA приём (RX) стал узким местом — разобрать RX-конвейер, обработку ошибок сигнала, дизайн протокола и найти баги/мины. Тайминги: `carrierFrec=38000`, `bitTakts=37` → `carrierPeriod=26µs`, `bitTime≈962µs` (комментарий про 1100 в `IR_DecoderRaw.h:107` устарел), `riseTime=962`, окно ±300µs, `IR_timeout≈15144µs`, `IR_ResponseDelay≈42мс`. Числа длительностей — оценки (замеров в коде нет). + +## 0. Резюме (главное) +1. **Найдены реальные баги, триггеримые ШУМОМ (не только валидным трафиком):** + - **CRITICAL — OOB-чтение** в `crcCheck` при `packSize==1` (`IR_DecoderRaw.cpp:887`): `packSize - crcBytes` = `1-2` → `uint8_t len=255` → `crc8` читает `dataBuffer[0..254]` при массиве 38 → OOB ~217 байт + `dataBuffer[255/256]`. Мусорный CRC (редко — ложно-валидный кадр) или HardFault у границы SRAM. Нижняя граница `packSize` нигде не проверяется. + - **HIGH — реальный крэш на Car: null-deref** `encoder->sendAccept()` без проверки (`IR_Decoder.cpp:169`). У Car декодер создан с `encoder==nullptr` (`Car/src/IR/IR.cpp:31`). Приходит валидный кадр `IR_MSG_DATA_ACCEPT`, адресованный машинке (addrTo==id, addrFrom≠0 и ` вместо `>=`): при `packSize==0` конец кадра не наступает, `i_dataBuffer` растёт до 304, запись `dataBuffer[304/8=38]` затирает соседнее поле `prevRise`. + - **MEDIUM — неинициализированные** `isWaitingAcceptSend`/`acceptSendTimer`/… (`IR_Decoder.cpp:72-79`): мусор при старте → возможен спонтанный `sendAccept` (и крэш по null-deref) без приёма чего-либо. +2. **RX-bottleneck — НЕ ISR и НЕ тяжёлый декод в прерывании** (декода в ISR нет, ISR лёгкий ~2-4µs). Узкое место структурное: **`tick()` вынимает РОВНО один сырой фронт за вызов** → скорость слива кольца привязана к частоте `loop()`; при медленном loop 250-элементный буфер (~120мс запаса) переполняется и рвёт кадры. Плюс битовый CRC ×2 на конец кадра и маленький hold-фильтр. +3. **Коррекции ошибок по сути нет** — detect-and-drop: 2×CRC8 (~16 бит), FEC/повтор отсутствует, однобитный bruteforce выключен («зависает»). Нет seq/дедупа/ретраев; ACK задуман, но отправитель его игнорирует (мёртвый механизм). 5-битная длина молча обрезает payload >24 байт (send возвращает Success, кадр теряется). + +--- + +## 1. RX-конвейер и производительность + +### Стадии (кто где исполняется) +- **A. EXTI-фронт → ISR** (`IR_DecoderRaw::isr`, `.cpp:274-308`; `attachInterrupt(pin, CHANGE)` в `IR_Decoder::enable :88-89`). Приёмник демодулирующий (TSOP), EXTI по фронтам огибающей. В ISR: `micros()` (в `noInterrupts/interrupts`), `dir = port->IDR&mask` (пост-фактум чтение уровня — при коротком импульсе возможна неверная dir), mute-гейт (`isPairSending` → фронт только в edgeTrace и return), `subBuffer.push(edge)`. +- **B. Сырое кольцо** `RingBuffer` (`.h:124`, `subBufferSize=250`, `IR_config.h:169`), ~8 байт/фронт ≈ 2КБ/декодер. `push/pop` под своей критсекцией. **`pop()` — один элемент за вызов.** +- **C. Фильтр импульсов** (`tick :417-444`) — по умолчанию **выключен** (`IR_INPUT_MIN_PULSE_US=0`), сырой фронт идёт прямо в декод. Включённый: hold-массив `[6]`, отсев пар `IR_timeout*2`. +- **E. Декод бита** (`processDecodedFront :466-746`): инлайн-отсев глитчей, окна таймингов, реконструкция пропущенных бит через `ceil_div`. +- **F. Сборка/sync/CRC** (`writeToBuffer :748-949`): биты в `dataBuffer[38]`, sync 3 бита/байт, длина из первого байта, на конце — `crcCheck`. +- **G. Диспетчеризация** (`IR_Decoder::_tick :130-172`): разбор msgType → gotData/gotBack/gotAccept/gotRequest/gotRaw; авто-ACK для DATA_ACCEPT. + +**Весь декод (C–G) — в `tick()`/loop, не в ISR.** Это осознанная разгрузка прерывания. + +### Узкое место +- ISR лёгкий (~2-4µs, оценка), даже при 5-6к фронтов/с ~1.5-2% CPU. Не он предел. +- **Структурный предел: 1 сырой pop/tick** (`.cpp:409-415`) → RX-throughput = частота `loop()`. Полный кадр ≈ 836 фронтов ≈ 836 tick'ов. Медленный loop (Serial-отладка, моторы, дисплей) < частоты фронтов → кольцо копится → overflow → рваные кадры → CRC/timeout reject. +- Буфер 250 = ~120мс запаса (250/2080 фр/с). Хватает от коротких стопоров, не от устойчивого отставания. +- Финальный CRC — битовый `crc8` дважды по всей длине (~5-10µs, оценка). `BRUTEFORCE_CHECK` (выкл, «зависает») при включении — O(len²·64), отдельная мина. + +### Переполнение/backpressure +- `subBuffer` полон → `push`=false, `isSubBufferOverflow=true` (транзиентный, сбрасывается в tick), счётчик `rxBriefRawOverflowDrops` (u16, насыщение), лог `QRAW`. +- hold-фильтр(6) переполнение → `pulseFilterDropHoldOverflow++` (стойкий), лог `HOLD`. +- Битовый буфер кадра → `isBufferOverflow`, лог `BUF`, сброс. +- `QFLT`/`pulseFilterDroppedByFilteredOverflow()` — **мёртвый код**, всегда 0. +- **Стойкого счётчика потерянных ПАКЕТОВ нет** — потери видны косвенно (CRC/TIMEOUT/SYNC в RX-brief-логе), последний битый кадр — в `rejectBuffer`. + +### RX во время своего TX (mute) +- `isPairSending` (число busy-энкодеров, пересчёт `refreshPairMuteState` под критсекцией) → в ISR фронт отбрасывается на входе (`:293-299`). Глушится **любой** чужой кадр на всё время своей передачи (`isSending`). Для DMA-TX снятие mute зависит от `externalFinishSend()` — задержка удлиняет «глухоту» на хвост. +- NVIC: библиотека приоритеты почти не трогает; RX-EXTI — дефолт ядра, поднять `setReceiveExtiPreemptPriority` (`IR_Decoder.cpp:44-62`). **Нужно: preempt RX-EXTI срочнее DMA-TX**, иначе чужой DMA/IRQ сдвигает метку фронта → искажение `risePeriod` (окно всего ±300µs). + +### Идеи ускорения RX (trade-off) +1. **Таймер input-capture + DMA вместо софт-EXTI** — аппаратный штамп фронтов, убирает per-edge ISR и джиттер метки. Дорого (канал таймера, wrap, восстановление dir), но чистейшие тайминги. +2. **Батч-pop сырых фронтов за tick** (с лимитом на итерацию) — снимает привязку к частоте loop. Главный дешёвый выигрыш. +3. **subBuffer 250→512/1024** — больше запаса (RAM: 1024≈8КБ). +4. **Табличный CRC** вместо битового ×2 (~8× быстрее, +~512Б flash). +5. **Поднять preempt RX-EXTI выше DMA-TX** (механизм есть). +6. **Дешёвая метка** DWT->CYCCNT вместо `micros()`+критсекции в ISR. + +--- + +## 2. Обработка ошибок сигнала и протокол + +### CRC — это НЕ CRC16-CCITT, а два сцепленных CRC8 +`crc8()` (`IR_config.cpp:17-33`) битовый MSB-first, init=0xFF. Полиномы `poly1=0x31`, `poly2=0x8C` (=битовое зеркало 0x31, но алгоритм тот же → фактически два разных CRC8). Сборка (`crcCheck :951-970`): `crc = crc8(0..len,poly1)<<8 | crc8(0..len+1,poly2)`. Асимметрия: байт2 покрывает данные+байт1, байт1 — только данные. Суммарно ~16 бит, вероятность пропуска ~1/65536, но гарантированной хэмминг-дистанции реального CRC16 нет. Мелочь `:959`: `dataBuffer[len] == (crc>>8) & 0xFF` парсится как `(==)&0xFF` — работает случайно (хрупко при смене `crc_t`). + +### Фильтры/устойчивость +- Pulse-filter по длительности — **по умолчанию выкл** (`IR_INPUT_MIN_PULSE_US=0`). +- Инлайн-отсев глитчей в декодере — вкл: `IR_SHORT_LOW_GLITCH_REJECT`, `IR_MICRO_GAP_RISE_REJECT` (`:484-516`), «подтяжка фазы». +- Преамбула устойчива к случайному шуму (2 согласованных периода в узком окне). Sync 3 бита/байт — ловит расстройку сетки (до 3 ошибок → reject), но это не коррекция. + +### Коррекции нет +- FEC отсутствует. Есть эвристическая реконструкция бит по времени (`:636-741`) — угадывание, финально валидируется CRC. +- `BRUTEFORCE_CHECK` (однобитная коррекция перебором) — **выключен, `//TODO: зависает`** (`IR_config.h:162`). При 16-битном контроле ещё и рискует ложной коррекцией. +- `rejectBuffer` — не коррекция, а «detect-and-expose» битого кадра приложению. + +### Подтверждения (Accept) — механизм есть, но мёртвый +Приёмник на `IR_MSG_DATA_ACCEPT` планирует `sendAccept(from, crc8(данные))` через `acceptDelay` (`IR_Decoder::_tick :157-171`). **Отправитель Accept игнорирует** — логика ожидания/ретрая закомментирована (`IR_Encoder.cpp:736-741`, `rawSend:906 «TODO»`). Ретраев/дедупа/seq нет. Гарантий доставки нет. + +### Форматы кадров +Первый байт `(msgType<<5)|(packSize&0x1F)`, `packSize` — полный размер кадра, длина 5 бит (0..31). Адреса big-endian. + +| Тип | Класс | Поля | data offset | CRC | +|---|---|---|---|---| +| BACK 0 | DataBack | msg, addrFrom(1-2) | 3 | 2 | +| ACCEPT 1 | Accept | msg, addrFrom, customByte(3) | — | 2 | +| REQUEST 2 | Request | msg, addrFrom, addrTo | — | 2 | +| BACK_TO 4 | DataBack | msg, addrFrom, addrTo | 5 | 2 | +| DATA_NOACCEPT 6 | Data | msg, addrFrom, addrTo | 5 | 2 | +| DATA_ACCEPT 7 | Data (нужен Accept) | как 6 | 5 | 2 | + +Адресация (`IR_config`): Broadcast=65000+; id==0 = promiscuous (принимает всё); машинки 1..31999, КТ 32000..63999, пульты 64000..64999. + +### Что теряется/портится молча +1. **Переполнение 5-битной длины (серьёзно):** накладные Data = 7 байт → payload ≤24, но send разрешает `len` до `bytePerPack=31` (`IR_Encoder.cpp:691`). При len 25..31 `packSize` пишется как `&0x1F` (оборот) → декодер читает короткий кадр → CRC не сойдётся → пакет теряется, **а send вернул Success**. (Комментарий `config:96` сам предупреждает про «31 vs 24».) +2. **Нет seq** → дубликаты/пропуски незаметны (любой будущий ретрай = повтор действия). +3. **Коллизии не детектируются** (half-duplex без CSMA/CD; `isPairSending` глушит только свой RX). TODO «отложить TX после приёма» (`IR_Encoder.cpp:5`) не реализован. +4. Тайминговая реконструкция может собрать «правдоподобный» буфер, редко проходящий CRC на неверных данных (~1/65536). +5. Accept-customByte — 1 байт CRC8 (1/256 ложного подтверждения), но т.к. игнорируется — без эффекта. + +--- + +## 3. Мины/баги (severity, подтверждено по коду) + +| # | Severity | Файл:line | Суть | Триггер | +|---|----------|-----------|------|---------| +| B1 | **CRITICAL** | `IR_DecoderRaw.cpp:887,951-970` | `crcCheck(packSize-crcBytes)` при `packSize==1` → len=255 → OOB-чтение `dataBuffer[0..256]` (массив 38) | шум: первый байт с младш. 5 бит =1 (0x01,0x21,…) после лока преамбулы | +| B2 | **HIGH** | `IR_Decoder.cpp:167-169` | `encoder->sendAccept()` без null-check; у Car `encoder==nullptr` (`IR.cpp:31`) → HardFault | валидный DATA_ACCEPT машинке (детерминированно) или шум (1/65536) | +| B3 | **HIGH** | `IR_DecoderRaw.cpp:753` | off-by-one `>` вместо `>=` → OOB-запись `dataBuffer[38]` (затирает `prevRise`) при `packSize==0` | шум: первый байт с младш. 5 бит =0, ~38 «дата»-байт подряд | +| B4 | **MEDIUM** | `IR_Decoder.cpp:72-79` (конструктор) | `isWaitingAcceptSend`/`acceptSendTimer`/`addrAcceptSendTo`/`acceptCustomByte` не инициализированы → спонтанный `sendAccept` (+ B2 крэш) | мусор в RAM при старте | +| B5 | **MEDIUM** | `RingBuffer.h:28-37` + `IR_DecoderRaw.cpp:408-415` | вложенные критсекции не вкладываются (`interrupts()` в `pop()` снимает внешний `noInterrupts()` из `tick()`) → торн-рид `*rawPtr`, слот уже свободен для ISR | заполненное кольцо в узком окне | +| B6 | MEDIUM | `IR_Decoder.cpp:112-115`, `IR_DecoderRaw.cpp:280-282` | джиттер метки фронта: `std::function` indirect-call + лишний `noInterrupts/interrupts` вокруг `micros()` в ISR → разброс `risePeriod` | нагрузка/параллельный DMA | +| B7 | LOW | `IR_DecoderRaw.cpp:959` | приоритет операторов в сравнении CRC — работает случайно | смена `crc_t` | +| B8 | LOW | `IR_DecoderRaw.cpp:448` | `isSubBufferOverflow=false` без критсекции параллельно ISR `=true` → потеря флага (диагностика) | — | +| B9 | LOW | `IR_DecoderRaw.cpp:1612` | sentinel `prevRise==0` при `micros()==0` (раз в ~71.6 мин) → разовый глитч преамбулы | редко | +| B10 | LOW (спит) | `IR_DecoderRaw.cpp:980-986`, `:1676` | `ceil_div` /`riseTime`; при `freeFrec==true` `riseSyncTime` может уйти <300 → `riseTimeMin` overflow / деление на 0 | только при `freeFrec` (по умолч. выкл) | + +**Опровергнуто (REFUTED):** wrap `micros()` в таймаутах (wrap-safe, `(uint32_t)(a-b)`); переполнение subBuffer за границы (push проверяет isFull); `packSize>dataByteSizeMax` (5 бит → max 31 < 38, опасна только НИЖНЯЯ граница); overflow `highCount/lowCount` (гейт по IR_timeout); аллокации в ISR (bind SBO на attachInterrupt, в ISR только indirect-call); реентерабельность одной EXTI (M4 не вытесняет сам себя). + +**Требует железной проверки:** джиттер метки фронта под параллельным DMA-TX; NVIC preempt RX>TX; воспроизведение B1/B3 (подать первый байт 0x01 / 0x00 под отладчиком); RAM-бюджет subBuffer (2КБ/декодер) на F4-пульте с несколькими декодерами. + +--- + +## 4. Приоритетный список доработок + +### Сначала — баги (кандидаты на фикс, все в общей библиотеке → blast radius на все проекты) +1. **B1 CRITICAL:** валидировать `packSize >= crcBytes` (и разумную нижнюю границу) ДО `crcCheck`; отбрасывать кадр иначе. Однострочная защита от OOB. +2. **B2 HIGH:** `if (encoder) encoder->sendAccept(...)` (null-check). Спасает Car от HardFault. +3. **B3 HIGH:** `>=` вместо `>` в границе `writeToBuffer:753` (+ обрабатывать `packSize==0` как невалидный). +4. **B4 MEDIUM:** инициализировать accept-поля в конструкторе (`isWaitingAcceptSend=false` и т.д.). +5. **B5 MEDIUM:** `pop()` отдавать по значению (out-параметр) под одной критсекцией, не указателем во внутренний слот. + +### Затем — производительность RX (эффект/сложность) +- Батч-pop за tick (высокий эффект / низкая сложность) → снять привязку к частоте loop. +- Табличный CRC; больше subBuffer; preempt RX>TX; (крупно) input-capture+DMA. + +### Протокол (эффект/сложность) +1. **Валидация полной длины в send** (высокий/низкий): проверять `packSize<=31` (Data ≤24, BACK ≤26), возвращать `PayloadTooLarge` — убирает тихую потерю. +2. **Seq + дедуп** (высокий/средний): 1 байт seq, дедуп по (addrFrom, seq). Предпосылка для любого ретрая. +3. **Реальный ACK+ретрай на отправителе** (высокий/средне-высокий): механизм генерации Accept уже есть, замкнуть петлю (нужен seq). +4. **Listen-before-talk** (средний/низкий): отложить TX пока `isReciving()` — TODO уже помечен. +5. **Заголовочный CRC/дублирование длины** (средний/низкий): битая длина не должна ломать границу кадра. +6. Кадровый повтор как лёгкий FEC (в паре с seq); CRC16-CCITT вместо 2×CRC8 (маргинально, ломает совместимость); фрагментация >31 байт с ACK (если реально нужны большие payload); починить/включить bruteforce (сомнительно). + +--- + +## Ключевые файлы +`IR-protocol/`: `IR_DecoderRaw.{h,cpp}` (ISR, декод, CRC, reject, преамбула, mute), `IR_Decoder.{h,cpp}` (диспетчеризация, Accept-петля, EXTI/NVIC), `IR_Encoder.{h,cpp}` (сборка кадра, CRC, send, isSending), `PacketTypes.{h,cpp}` (форматы, адресация), `IR_config.{h,cpp}` (тайминги, framing, crc8), `RingBuffer.h`, `IrTxIsrBufferedStorage.h`. diff --git a/docs_analysis/Signal_physics_analysis.md b/docs_analysis/Signal_physics_analysis.md new file mode 100644 index 0000000..921286e --- /dev/null +++ b/docs_analysis/Signal_physics_analysis.md @@ -0,0 +1,72 @@ +# Анализ физики сигнала: TX-форма против RX-коррекций + +## 1. Резюме + +Система в основном **не борется сама с собой**: и TX, и RX выведены из одних констант `IR_config.h`, форма на передаче при `multiply=2` (дефолт, чётный) — **бит-точная, без округлений**, а подавляющая часть RX-эвристик ловит деформации, которые реально вносит ИК-канал через демодулятор (TSOP-АРУ, отражения, фон, расхождение кварцев). Различение 0/1 идёт по скважности (`highTime>lowTime`, IR_DecoderRaw.cpp:620-633) внутри периода и **не зависит** от широкого окна периода — это архитектурно здоровое решение. + +Но есть **три очага самопорождённых ошибок**, которые RX молча компенсирует: + +1. **Главный (чистый баг, не канал):** RX строит всю временную базу на усечённой константе `carrierPeriod = 1e6/38000 = 26` (bitTime=962 µs), тогда как TX физически излучает на реальной частоте таймера — `26.316 µs`, bitTime≈973.7 µs. Систематический **односторонний сдвиг ~11.7 µs/бит (1.2%)**, который целиком съедается допуском `tolerance=300`. Это TX/модель кривит — RX терпит. +2. **Конфиг-зависимый:** ISR-путь вносит программный джиттер фронтов (латентность/вытеснение, IR_Encoder.cpp:1037-1042), которого нет у DMA-пути. Часть glitch/reconstruction-логики RX страхует именно этот самопорождённый джиттер. +3. **Спровоцированный дизайном модуляции:** протокол использует **длинные непрерывные MARK** (преамбула 49 периодов ≈1290 µs, бит '1' = 25 периодов ≈658 µs). Длинные непрерывные burst'ы — классический триггер АРУ TSOP-демодулятора (просадка огибающей в середине долгого mark) → «рваная единица», которую чинит реконструкция E/G. Деформацию вносит канал (TSOP), но провоцирует её выбор формы на TX. + +Остальное — стартовая фаза (≤26 µs), дрейф периода, дребезг фронтов — это то, что **обязан** обрабатывать любой ИК-приёмник; здесь самолечения нет. + +## 2. Карта механизмов RX + +| Механизм RX (file:line) | Какую деформацию ловит/чинит | Детект/Коррекция | Источник деформации | Вердикт | +|---|---|---|---|---| +| **A.** pulse-filter, минимум ширины (DecoderRaw 1493-1555) | иголка/дребезг компаратора, пара «↑↓» короче минимума | детект | реальный канал (front-end, дребезг) | оправданно (но дефолт `MIN_PULSE_US=0` — выключен) | +| **B.** short-LOW glitch reject + phase nudge (484-499, 8-16) | короткий ложный ↑ после микро-провала огибающей | детект + коррекция фазы | реальный канал (TSOP-АРУ, просадка) | оправданно | +| **C.** micro-gap rise reject + nudge (500-516) | распад фронта на два близких | детект + коррекция фазы | реальный канал; **частично** ISR-джиттер | в основном оправданно | +| **D.** отсев короткого стартового периода / спада (517-524, 566-573) | сжатый ↑→↑ без контекста, дребезг спада | детект | реальный канал (шум/дребезг) | оправданно | +| **E.** сбор рваной единицы (526-557) | длинный HIGH-бит, распавшийся из-за пропадания несущей | подготовка к коррекции | канал (TSOP-АРУ), **спровоцировано длинными MARK на TX** | оправданно, но триггер — дизайн формы | +| **F.** окно валидности периода (h:24-27; 583-592) | грубо растянутый/сжатый период | детект/брак | реальный канал (потеря кадра) | оправданно | +| **G.** реконструкция ceil_div, invertErr, «лишний ноль/единица» (617-741, 980-986) | пропущенные ↑-фронты, N слипшихся бит, дроблёный/короткий бит | **коррекция (ядро)** | канал (дропаут несущей); косвенно ISR-джиттер | оправданно под канал; часть работы — следствие TX-дизайна | +| **H.** восстановление сетки по sync (804-853) | сдвиг сетки data↔sync из-за лишнего/недостающего бита | детект срыва + локальная коррекция на 1 бит | канал (потеря бита); маркер `{!L,L,!M}` от TX детерминирован | оправданно (детект реальной потери) | +| **I.** преамбула Idle→Candidate→Locked, EMA периода, якорь фазы, freeFrec (1557-1723) | дрейф периода, разброс кварцев, старт-фаза, чужая частота | коррекция базы/фазы | **смешанный:** реальный (кварцы/дрейф) + самопорождённый (старт-фаза TIM ≤26 µs, сдвиг 1.2% из carrierPeriod=26) | необходимо всегда; но частично адаптируется к TX-багу | + +## 3. Где Даша сама порождает ошибку формы и потом её чинит + +**(а) Усечённый `carrierPeriod=26` — самое чистое «самолечение».** +Физическая генерация НЕ использует эту константу: `setOverflow(carrierFrec*multiply, HERTZ_FORMAT)` (IR_Encoder.cpp:411) даёт реальные ~26.316 µs. А вся сетка RX (`riseSyncTime=962`, `riseTimeMin/Max`, `aroundRise`, `IR_timeout`) выведена из `carrierPeriod=26`. Результат: TX-биты систематически **на 1.2% длиннее**, чем ждёт RX. Сдвиг **односторонний** (всегда в плюс), поэтому запас `aroundRise` со стороны `riseTimeMax=1262` меньше, чем со стороны `riseTimeMin=662`. На бит это 11.7 µs, на преамбулу — ~15 µs (1274 vs 1289.5 µs). Это не канальная ошибка — это несогласованность двух представлений одной частоты внутри одного кода. **RX-допуск `tolerance=300` существует в т.ч. чтобы это не всплыло.** Исправляется тривиально (общий реальный период), после чего часть запаса окна освобождается под настоящий канал. + +**(б) ISR-путь как источник джиттера, который чинит glitch-логика.** +DMA-путь детерминирован (аппаратная пересылка BSRR по TIM_UP, IrDmaTxStm32.h:246-253). ISR-путь плавает по времени из-за латентности прерывания и обхода списка энкодеров (IR_Encoder.cpp:1037-1042). Плавающие фронты — ровно то, что подхватывают B/C (nudge) и частично G. То есть при работе через ISR приёмник страхует **собственный** TX-джиттер, а не канал. Лечение: всегда использовать DMA-путь (или признать ISR как деградированный режим). + +**(в) Длинные непрерывные MARK провоцируют TSOP-АРУ → «рваная единица».** +Кодирование pulse-width с MARK до 25 периодов и преамбулой 49 периодов непрерывной несущей — это осознанный выбор формы на TX. TSOP-демодуляторы имеют АРУ и штатно «проседают» на длинных непрерывных burst'ах, порождая ложные провалы огибающей в середине mark. Реконструкция E + G (ceil_div, `highCount==0 && highTime>riseTime/3 → highCount++`, 641-648) существует во многом чтобы это дособрать. Деформацию физически вносит канал (TSOP), но её вероятность задаёт форма на TX. Это не баг, но это «дизайн, требующий костыля на приёме». + +**(г) Что НЕ является самолечением (важно не перепутать):** +- Стартовая фаза кадра ≤1 период несущей (free-running TIM, IrDmaTxStm32.h:241-253) — компенсируется якорем фазы преамбулы (I, `prevRise=front.time+mean/2`, 1709). Но приёмник **в принципе** не знает абсолютную фазу передатчика и обязан выводить её из преамбулы — это необходимая функция, а не борьба с собой. +- Маркер sync `{!L, L, !M}` детерминирован (IR_Encoder.cpp:305-331), буфер зануляется — H ловит **реальную** потерю/вставку бита в канале, не TX-артефакт. +- Квантование фронтов сеткой 13.16 µs (DMA/BSRR) — тонет в допуске TSOP (десятки µs) и погоды не делает. + +## 4. Широкие ли допуски и что они маскируют + +`aroundRise` = 662..1262 µs при бите 962 → **±31%**. По логике подсчёта бит это **не** широко и путаницы 1-бит/2-бита не создаёт: 2 бита ≈1924 µs, порог `ceil_div` на 2 бита ≈1443 µs; между концом окна одного бита (1262) и порогом двух (1443) есть зазор ~180 µs — перекрытия нет. Значение бита определяется по duty, а не по длине периода, поэтому широкое окно значения не портит. + +Куда реально уходит ±300 µs: +- **Легитимно:** асимметрия leading/trailing фронтов TSOP-демодулятора (типично десятки–сотни µs), разброс кварцев RX/TX, дрожание огибающей, задержка АРУ. Для настоящего ИК-канала запас в этом порядке нужен. +- **Маскирует баг:** самопорождённый односторонний сдвиг 1.2% (пункт 3а) полностью прячется в этом окне. Именно поэтому баг `carrierPeriod=26` не проявляется как отказ — допуск его глотает. Это и есть «система не замечает, что борется сама с собой». + +Слабое место (согласен с RX-разбором): при `freeFrec=false` база `riseTime` жёстко=962 и **не** подстраивается по ходу кадра (медиана — «TODO», IR_DecoderRaw.h:22). Односторонний TX-сдвиг + систематическое смещение периода передатчика съедают запас именно со стороны `riseTimeMax`; на краю окна шум легче перекидывает интервал в ветку реконструкции G, накручивая `errors.*`. Также джиттер-полки преамбулы (18%, но не менее 80 µs) **жёстче** рабочего окна декодера (31%) — залочиться труднее, чем потом удержать биты; это перекос порогов, а не форма. + +## 5. Рекомендации + +**Чинить на TX (чтобы соответствующие RX-эвристики стали не нужны):** + +1. **Согласовать период несущей между TX и RX.** Убрать усечение `carrierPeriod=26` из временной модели RX: выводить `bitTime`/`riseSyncTime` из той же реальной частоты, что даёт `setOverflow(carrierFrec*multiply,...)` (дробно, напр. фиксированная точка). Устраняет односторонний сдвиг 1.2% и освобождает ~12 µs/бит запаса окна. *Trade-off:* дробная арифметика на MCU; но можно один раз посчитать реальный период при инициализации. +2. **Всегда использовать DMA-путь; ISR — только как явный деградированный fallback.** Снимает программный джиттер фронтов и часть нагрузки с B/C/G. *Trade-off:* требует свободного канала DMA+таймера; на платах без ресурса — остаётся ISR с его джиттером. +3. **Пересмотреть длину непрерывных MARK/преамбулы под конкретный TSOP.** Если приёмник — демодулирующий TSOP с АРУ, длинные burst'ы (658/1290 µs) провоцируют «рваную единицу». Сократить максимальный непрерывный mark или разбить преамбулу на более частые фронты в пределах спецификации TSOP по burst-length. *Trade-off:* меняет протокол/энергетику кадра, требует ревизии окон преамбулы RX; выигрыш — можно ослабить E/G. +4. **(Опционально) фазовое выравнивание старта кадра** (сброс счётчика TIM при старте потока). Уберёт стартовый сдвиг ≤26 µs. *Trade-off:* минимальный выигрыш (преамбула всё равно якорит фазу) — низкий приоритет. + +**Что из «коррекций» оставить под настоящий канал (даже после идеального TX):** + +- **G (ceil_div реконструкция) и E (сбор рваной единицы)** — оставить: дропаут несущей и просадка АРУ на реальной дистанции/отражениях никуда не денутся. Это ядро устойчивости к каналу. +- **B/C (glitch reject + phase nudge)** и **A (pulse-filter)** — оставить: дребезг/иголки front-end реальны; A стоит **включить** (`IR_INPUT_MIN_PULSE_US>0`) на STM32, где сейчас фронты идут в декодер напрямую (tick 440-444). +- **H (sync-grid recovery)** — оставить: детектирует реальную потерю/вставку бита; маркер детерминирован, ложных срабатываний от TX не будет. +- **I (преамбула: EMA периода, якорь фазы)** — оставить: адаптация под кварцы/дрейф и вывод фазы обязательны для любого RX. Рекомендуется **выровнять пороги**: сделать джиттер-полку преамбулы не жёстче рабочего окна декодера, иначе лок труднее удержания. +- **Кандидаты на сужение после фикса TX:** окно `aroundRise` можно сузить в сторону `riseTimeMax` (убрать заложенный под 1.2% односторонний запас) и реализовать бегущую медиану периода (IR_DecoderRaw.h:22 TODO) — тогда допуск начнёт отражать реальный канал, а не прятать несогласованность констант. + +**Итог-вердикт:** коррекции RX в массе **оправданны под реальный ИК-канал**, но система содержит одно явное самолечение (усечённый `carrierPeriod` → 1.2% сдвиг, спрятанный в `tolerance`) и два условных (ISR-джиттер; длинные MARK, провоцирующие TSOP-АРУ). Широкий допуск ±31% сам по себе безопасен для подсчёта бит, но именно он маскирует баг с частотой — то есть система действительно частично борется сама с собой, просто настолько тихо, что это не видно в `errors.*`. \ No newline at end of file diff --git a/docs_analysis/crc_combined_sim.py b/docs_analysis/crc_combined_sim.py new file mode 100644 index 0000000..d715abc --- /dev/null +++ b/docs_analysis/crc_combined_sim.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Точное сравнение CRC-схем на бит-слипе. crc8 как в IR_config.cpp. +# ВАЖНО: CRC в протоколе считается над БАЙТАМИ данных (sync НЕ входит — подтверждено кодом), +# поэтому здесь sync в CRC-математику не включён (это корректно для оценки самой CRC). +# Второй ниббл — СО СЦЕПКОЙ: poly2 над (data + crc1_байт), как в оригинале crcCheck. +# Сквозной анализ с sync-фильтром требует модели FSM декодера (resync/‘исправление лишнего нуля’) — см. заметку. +import random +random.seed(3) +POLY1=0x31; POLY2=0x8C +def crc8(d,poly): + c=0xFF + for b in d: + c^=b + for _ in range(8): c=((c<<1)^poly)&0xFF if c&0x80 else (c<<1)&0xFF + return c +def fold4(x): return (x^(x>>4))&0x0F +def single(d): return (crc8(d,POLY1),) +def double(d): c1=crc8(d,POLY1); return (c1, crc8(d+[c1],POLY2)) # текущая 2 байта, сцепка +def combo_indep(d): return ((fold4(crc8(d,POLY1))<<4)|fold4(crc8(d,POLY2)),) # 1 байт, БЕЗ сцепки +def combo_chain(d): c1=crc8(d,POLY1); return ((fold4(c1)<<4)|fold4(crc8(d+[c1],POLY2)),) # 1 байт, СО сцепкой (как ориг.) +def bitsof(d): return [x for b in d for x in [(b>>(7-j))&1 for j in range(8)]] +def bytesof(b): return [int(''.join(map(str,b[i:i+8])),2) for i in range(0,len(b),8)] +def slip(d,p): b=bitsof(d); return bytesof(b[:p]+b[p+1:]+[0]) +S=[("single 1б",single),("double 2б (текущая, сцепка)",double), + ("combo 1б БЕЗ сцепки",combo_indep),("combo 1б СО сцепкой (как ориг.)",combo_chain)] +for L in (4,6,8): + und={n:0 for n,_ in S}; ch=0 + for _ in range(300000): + d=[random.randint(0,255) for _ in range(L)]; p=random.randint(L*2,L*6) + c=slip(d,p) + if c==d: continue + ch+=1 + for n,f in S: + if f(c)==f(d): und[n]+=1 + print(f"данные {L}б, {ch} слипов:") + for n,_ in S: print(f" {n:34s}: {und[n]/ch*100:.4f}% необнаружено") diff --git a/docs_analysis/crc_shift_findings.md b/docs_analysis/crc_shift_findings.md new file mode 100644 index 0000000..667318d --- /dev/null +++ b/docs_analysis/crc_shift_findings.md @@ -0,0 +1,33 @@ +# CRC при сдвиге битов — симуляция (воспроизведение и оценка сжатия) + +> Скрипт: `crc_shift_sim.py` (рядом). `crc8` точно как в `IR_config.cpp` (MSB-first, init=0xFF, без final-xor). `poly1=0x31`, `poly2=0x8C` (битовое зеркало 0x31). Модель ошибки: сдвиг битов в СЕРЕДИНЕ данных (бит-слип / циклический сдвиг), длина кадра сохраняется; «чек» сверяется как `check(искажённые данные) == check(оригинал)`. 200000 попыток на модель, данные 3/5/8 байт. + +## 1. Твой исторический баг ВОСПРОИЗВЕДён +Одиночный CRC8 (один полином) реально пропускает сдвиг в середине пакета. Примеры (данные → сдвиг → искажённые): +- `26 0b 05` → `26 16 0a`: `crc1=0x78` совпал → **single ПРОШЁЛ**; двойная схема: `crc2 0xd8≠0x5c` → **поймала**. +- `cc4fb8f633` → `cc5f71ec66`: `crc1=0x20` совпал → single прошёл; double поймала (`crc2 0xfc≠0x90`). +- `b983315b416440c3` → `b983315e82c88186`: `crc1=0x3a` совпал → single прошёл; double поймала. + +## 2. Частота пропуска (доля НЕобнаруженных сдвигов) +| Схема | slip-delete | rotate-all | rotate-mid-window | +|---|---|---|---| +| **single** (1×CRC8, 8 бит) | 0.29–0.41 % | ~0.78 % | 0.55–0.78 % | +| **double** (2×CRC8, 16 бит) — текущая | **0.007–0.015 %** | **0.025–0.03 %** | **0.008–0.026 %** | +| nibble-fold (4+4 бит) | 0.76–0.80 % | ~1.58 % | 1.18–1.55 % | +| nibble-trunc (4+4 бит) | 1.37–1.57 % | ~1.53 % | 1.22–1.57 % | + +Вывод: **double примерно в 20–40× надёжнее single** и ловит практически все сдвиги, которые single пропускает. Это подтверждает, зачем добавляли второй полином (0x8C = зеркало 0x31 ловит «зеркальные» сдвиговые ошибки, слепые для 0x31). + +## 3. Идея «склеить 2 байта в 1» (полбайта на полином) — ХУЖЕ, чем есть +- **nibble-fold** (свернуть каждый CRC8 в 4 бита xor'ом) и **nibble-trunc** (взять по 4 бита) дают **0.6–1.6 % пропусков — это ХУЖЕ даже одиночного полного CRC8** и в ~50–150× хуже текущей двойной схемы. +- Burst-ошибки (подряд искажённые биты — типичный IR-сбой: бит-слипы/всплески). Гарантия обнаружения = ширине контроля: + - single 8 бит и double — надёжны на коротких burst (≥8 бит), эмпирически в выборке не пропускали и длиннее; + - **nibble-fold** — гарантия ~4 бита (эмпирически до 7); + - **nibble-trunc** — **пропускает даже ОДИНОЧНЫЙ бит** (усечение теряет старший ниббл) → так делать нельзя. + +## 4. Рекомендация +- **CRC — не то место, где стоит экономить байт.** Сжатие контроля до 1 байта повышает пропуск сдвигов/burst в ~50–150× (с ~0.01 % до ~1 %) — прямо в том классе ошибок, ради которого второй полином и вводили. +- Если байт очень нужен — забирать его **не из CRC**, а из уже выявленного резерва: свободные `msgType` (3,5), «пустая» зона длины (компактные кадры), или переупаковка адресов/полей. 16-битный контроль (2×CRC8) сохранить. +- Если всё же сжимать CRC до 1 байта — только **fold (xor нибблов)**, никогда не truncate; и принять ~1 % пропуска на сдвигах (в ~50–100× хуже текущего). Как отдельный компромисс — обсуждать вместе с обратной совместимостью. + +(Обратную совместимость версий протокола обсудим отдельно — по запросу Даши.) diff --git a/docs_analysis/crc_shift_sim.py b/docs_analysis/crc_shift_sim.py new file mode 100644 index 0000000..1343bda --- /dev/null +++ b/docs_analysis/crc_shift_sim.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Симуляция CRC-схем IR-protocol на "сдвиговых" ошибках. +# crc8 воспроизводит IR_config.cpp: MSB-first, init=0xFF, без final-xor. +import random +random.seed(1) +POLY1 = 0x31 +POLY2 = 0x8C # битовое зеркало 0x31 + +def crc8(data, poly): + crc = 0xFF + for b in data: + crc ^= b + for _ in range(8): + crc = ((crc << 1) ^ poly) & 0xFF if (crc & 0x80) else (crc << 1) & 0xFF + return crc + +def fold4(x): # свернуть 8-бит CRC в 4 бита (xor старший^младший ниббл) + return (x ^ (x >> 4)) & 0x0F + +# --- схемы контроля: считают "чек" от данных и сверяют --- +def single_chk(d): return (crc8(d, POLY1),) # 8 бит, 1 полином +def double_chk(d): c1 = crc8(d, POLY1); return (c1, crc8(d + [c1], POLY2)) # текущая: 16 бит +def nib_fold_chk(d):return ((fold4(crc8(d, POLY1)) << 4) | fold4(crc8(d, POLY2)),) # предложенная: 4+4 бит +def nib_trunc_chk(d):return (((crc8(d, POLY1) & 0x0F) << 4) | (crc8(d, POLY2) & 0x0F),) + +def bits_of(data): + return [(b >> (7 - j)) & 1 for b in data for j in range(8)] +def bytes_of(bits): + return [sum(bits[i + j] << (7 - j) for j in range(8)) for i in range(0, len(bits), 8)] + +# --- модели "сдвига" в середине пакета (над битами данных, длина сохраняется) --- +def slip_delete(data, p): # выкинуть бит p, сдвинуть хвост влево, добить 0 в конец + b = bits_of(data); b = b[:p] + b[p+1:] + [0]; return bytes_of(b) +def rotate_all_right(data, _): # циклический сдвиг ВСЕХ бит вправо на 1 + b = bits_of(data); b = [b[-1]] + b[:-1]; return bytes_of(b) +def rotate_mid_window(data, p): # циклический сдвиг окна [p, p+16) на 1 (локальный сдвиг в середине) + b = bits_of(data); q = min(p+16, len(b)) + w = b[p:q]; w = [w[-1]] + w[:-1]; b = b[:p] + w + b[q:]; return bytes_of(b) + +MODELS = [("slip-delete (бит-слип)", slip_delete), + ("rotate-all (сдвиг всего на 1)", rotate_all_right), + ("rotate-mid-window (сдвиг окна в середине)", rotate_mid_window)] +SCHEMES = [("single (1×CRC8, 8бит)", single_chk), + ("double (2×CRC8, 16бит) — текущая", double_chk), + ("nibble-fold (4+4бит)", nib_fold_chk), + ("nibble-trunc (4+4бит)", nib_trunc_chk)] + +def run(datalen, trials): + print(f"\n===== данные {datalen} байт, {trials} попыток на модель =====") + for mname, model in MODELS: + # для каждой схемы: сколько ошибок НЕ обнаружено (чек совпал, а данные изменились) + undetected = {s[0]: 0 for s in SCHEMES} + changed = 0 + first_single_miss = None + for _ in range(trials): + d = [random.randint(0, 255) for _ in range(datalen)] + p = random.randint(datalen*8//4, datalen*8*3//4) # позиция в середине + c = model(d, p) + if c == d: + continue + changed += 1 + for sname, chk in SCHEMES: + if chk(c) == chk(d): + undetected[sname] += 1 + if sname.startswith("single") and first_single_miss is None: + first_single_miss = (d[:], c[:], p) + print(f" модель: {mname} (изменённых кадров: {changed})") + for sname, _ in SCHEMES: + u = undetected[sname] + rate = u / changed if changed else 0 + print(f" {sname:38s}: пропущено {u:6d} ({rate*100:.4f}%)") + if first_single_miss: + d, c, p = first_single_miss + print(f" ПРИМЕР пропуска single (poly1): data={bytes(d).hex()} -> shift@{p} -> {bytes(c).hex()}") + print(f" single: crc1(orig)={crc8(d,POLY1):02x} == crc1(shift)={crc8(c,POLY1):02x} -> ПРОШЁЛ") + print(f" double: crc2(orig)={double_chk(d)[1]:02x} vs crc2(shift)={double_chk(c)[1]:02x} -> {'ПРОШЁЛ (не поймал!)' if double_chk(d)==double_chk(c) else 'ПОЙМАЛ'}") + +for L in (3, 5, 8): + run(L, 200000) + +# Гарантированное покрытие burst-ошибок (подряд идущие искажённые биты) — ключ для IR (бит-слипы/всплески) +print("\n===== Гарантия обнаружения BURST-ошибок (100% всех burst данной длины?) =====") +def burst_guaranteed(chk, datalen, blen): + # перебор всех burst длины blen по всем позициям на нескольких случайных базах; ищем ХОТЬ ОДИН пропуск + for _ in range(300): + d = [random.randint(0,255) for _ in range(datalen)] + base = bits_of(d) + for start in range(len(base)-blen+1): + b2 = base[:] + for k in range(blen): + b2[start+k] ^= 1 + c = bytes_of(b2) + if c != d and chk(c) == chk(d): + return False # найден необнаруженный burst + return True +for sname, chk in SCHEMES: + maxb = 0 + for blen in range(1, 25): + if burst_guaranteed(chk, 5, blen): + maxb = blen + else: + break + print(f" {sname:38s}: 100%% ловит burst до {maxb} бит подряд") diff --git a/docs_analysis/crc_shift_visualize.py b/docs_analysis/crc_shift_visualize.py new file mode 100644 index 0000000..72a03a1 --- /dev/null +++ b/docs_analysis/crc_shift_visualize.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# Визуализация кадров и сдвигов битов (с синхробитами и подписями). +# crc8 как в IR_config.cpp. Синхро (syncBits=3) логически = [~last, last, ~next] (addSync, send_HIGH=1). +POLY1=0x31; POLY2=0x8C +def crc8(data,poly): + c=0xFF + for b in data: + c^=b + for _ in range(8): + c=((c<<1)^poly)&0xFF if c&0x80 else (c<<1)&0xFF + return c +def bits(b): return [(b>>(7-j))&1 for j in range(8)] +def bstr(bl): return ''.join(str(x) for x in bl) + +def onair(frame_bytes, labels): + """строка вида: dddddddd sss dddddddd sss ... + строка подписей под байтами""" + rows=[] + line=''; lab='' + for i,by in enumerate(frame_bytes): + db=bstr(bits(by)) + # синхрогруппа ПОСЛЕ байта (между байтами), кроме последнего + if i < len(frame_bytes)-1: + last=bits(by)[7]; nxt=bits(frame_bytes[i+1])[0] + sy=f"{1-last}{last}{1-nxt}" + piece=db+' ['+sy+'] ' + else: + piece=db + # подпись байта, центрируем под 8 бит + L=labels[i] if i Date: Tue, 28 Jul 2026 18:22:44 +0300 Subject: [PATCH 10/24] =?UTF-8?q?perf(rx):=20=D0=B1=D0=B0=D1=82=D1=87-?= =?UTF-8?q?=D0=B2=D1=8B=D0=B5=D0=BC=D0=BA=D0=B0=20=D1=84=D1=80=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=20=D0=B2=20tick=20(IR=5FRX=5FTICK=5FBATCH=3D?= =?UTF-8?q?16)=20=E2=80=94=20=D0=BF=D1=80=D0=B8=D1=91=D0=BC=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B7=D0=B2=D1=8F=D0=B7=D0=B0=D0=BD=20=D0=BE=D1=82=20=D1=87?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D0=BE=D1=82=D1=8B=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Было: 1 сырой фронт за tick() -> RX-пропускная способность привязана к loop; медленный loop (телеметрия/дисплей/КУ-печать) переполнял ISR-буфер 250 фронтов (~120мс эфира) и кадры гибли молча (вероятная составляющая 'КУ отвечает не на каждый пакет'). Стало: до IR_RX_TICK_BATCH фронтов за tick; idle-семантика сохранена (flush пульс-фильтра только при пустом буфере; listenStart/ checkTimeout как раньше). Переопределяемо дефайном до include. Компил-чек: LaserTestCheck (G491) собирается. Co-Authored-By: Claude Opus 4.8 (1M context) --- IR_DecoderRaw.cpp | 37 +++++++++++++++++++++++-------------- IR_config.h | 7 +++++++ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/IR_DecoderRaw.cpp b/IR_DecoderRaw.cpp index 66482f2..a230806 100644 --- a/IR_DecoderRaw.cpp +++ b/IR_DecoderRaw.cpp @@ -402,11 +402,15 @@ void IR_DecoderRaw::tick() FrontStorage rawFront; bool processedFront = false; - const bool hasRawFront = subBuffer.pop(rawFront); // B5: копия под одной критсекцией (без торн-рида) - - if (IR_INPUT_MIN_PULSE_US > 0U) + // Батч-выемка (IR_RX_TICK_BATCH): раньше 1 фронт/tick — приём был привязан + // к частоте loop, буфер 250 фронтов (~120мс эфира) переполнялся на медленном + // loop и кадры гибли молча. Семантика idle-пути сохранена: flush + // пульс-фильтра — только когда буфер пуст (как прежняя else-ветка). + uint8_t batchBudget = IR_RX_TICK_BATCH; + bool hasRawFront = subBuffer.pop(rawFront); // B5: копия под одной критсекцией (без торн-рида) + while (hasRawFront) { - if (hasRawFront) + if (IR_INPUT_MIN_PULSE_US > 0U) { pulseFilterPushRaw(rawFront); FrontStorage confirmedFront; @@ -418,19 +422,24 @@ void IR_DecoderRaw::tick() } else { - const uint32_t nowUs = micros(); - FrontStorage confirmedFront; - while (pulseFilterTryFlushOne(nowUs, confirmedFront)) - { - processDecodedFront(confirmedFront); - processedFront = true; - } + processDecodedFront(rawFront); + processedFront = true; } + if (--batchBudget == 0U) + { + break; + } + hasRawFront = subBuffer.pop(rawFront); } - else if (hasRawFront) + if (!hasRawFront && IR_INPUT_MIN_PULSE_US > 0U) { - processDecodedFront(rawFront); - processedFront = true; + const uint32_t nowUs = micros(); + FrontStorage confirmedFront; + while (pulseFilterTryFlushOne(nowUs, confirmedFront)) + { + processDecodedFront(confirmedFront); + processedFront = true; + } } if (!processedFront) diff --git a/IR_config.h b/IR_config.h index d585d9e..afdfff5 100644 --- a/IR_config.h +++ b/IR_config.h @@ -179,6 +179,13 @@ typedef uint16_t crc_t; #ifndef IR_INPUT_MIN_PULSE_US #define IR_INPUT_MIN_PULSE_US 0 #endif +/** Сколько сырых фронтов забирать из ISR-буфера за один tick() (батч-выемка). + * 1 фронт/tick привязывал RX-пропускную способность к частоте loop: медленный + * loop (телеметрия/дисплей) переполнял буфер 250 фронтов (~120мс эфира) и + * кадры терялись молча. Батч развязывает приём от темпа loop. */ +#ifndef IR_RX_TICK_BATCH +#define IR_RX_TICK_BATCH 16U +#endif /** Сколько подтверждённых фронтов держать перед выпуском в декодер (потоковая задержка). */ #ifndef IR_INPUT_FILTER_HOLDBACK_EDGES #define IR_INPUT_FILTER_HOLDBACK_EDGES 3U From 08ea71eb5e1a5fda8b800e21fde31a65e6629893 Mon Sep 17 00:00:00 2001 From: DashyFox Date: Tue, 28 Jul 2026 23:05:50 +0300 Subject: [PATCH 11/24] =?UTF-8?q?feat(rx):=20always-on=20=D1=81=D1=87?= =?UTF-8?q?=D1=91=D1=82=D1=87=D0=B8=D0=BA=D0=B8=20=D0=BF=D1=80=D0=B8=D1=87?= =?UTF-8?q?=D0=B8=D0=BD=20RX-=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9=20?= =?UTF-8?q?(rxReasonCnt)=20+=20RXSTAT-=D1=81=D0=B2=D0=BE=D0=B4=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rxBriefLog теперь безусловен: ВСЕГДА инкрементирует rxReasonCnt[reason] (наблюдаемость по контракту живучести — работает в проде без печати), печать события — только при IR_RX_BRIEF_LOG. 14 точек вызова развёрнуты из-под #if (Glitch/Timing/Preamble/Sync/BufOverflow/Timeout/Crc/Ok + pulse-filter пути); ISR-агрегатные MuteBegin/End/RawOverflow остаются только при логе (их флаш живёт в brief-механике). Публичное API: rxReasonCounters() / rxReasonCountersClear() / printRxReasonStats(Print&) -> 'RXSTAT,GLITCH=..,TIME=..,...,OK=..'. Компил-чек: LaserTestCheck (G4, флаг выкл) и Plan_B (F4, флаг вкл) — чисто. Co-Authored-By: Claude Opus 4.8 (1M context) --- IR_DecoderRaw.cpp | 56 +++++++++++++++++++++++------------------------ IR_DecoderRaw.h | 14 +++++++++++- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/IR_DecoderRaw.cpp b/IR_DecoderRaw.cpp index a230806..325d52a 100644 --- a/IR_DecoderRaw.cpp +++ b/IR_DecoderRaw.cpp @@ -145,9 +145,19 @@ const __FlashStringHelper *IR_DecoderRaw::rxBriefReasonTag(RxBriefReason reason) default: return F("UNK"); } } +#endif // IR_RX_BRIEF_LOG (rxBriefReasonTag) +// Безусловна: always-on счёт причин RX-событий (наблюдаемость по контракту +// живучести — счётчики живут и в проде без печати); печать — только под флагом. void IR_DecoderRaw::rxBriefLog(RxBriefReason reason, uint16_t a, uint16_t b, uint32_t tUs) { + const uint8_t ri = (uint8_t)reason; + if (ri < 14U) + rxReasonCnt[ri]++; +#if !IR_RX_BRIEF_LOG + (void)a; (void)b; (void)tUs; + return; +#else #if IR_RX_BRIEF_LOG_REJECT_ONLY if (reason == RxBriefReason::Ok || reason == RxBriefReason::Preamble) return; @@ -220,8 +230,26 @@ void IR_DecoderRaw::rxBriefLog(RxBriefReason reason, uint16_t a, uint16_t b, uin break; } Serial.println(); +#endif // IR_RX_BRIEF_LOG (печать) } +void IR_DecoderRaw::printRxReasonStats(Print &out) const +{ + static const char *const kTags[14] = {"?", "MUTEB", "MUTEE", "QRAW", "QFLT", "HOLD", + "GLITCH", "TIME", "PREAMB", "SYNC", "BUF", + "TIMEOUT", "CRC", "OK"}; + out.print(F("RXSTAT")); + for (uint8_t i = 1; i < 14U; i++) + { + out.print(','); + out.print(kTags[i]); + out.print('='); + out.print(rxReasonCnt[i]); + } + out.println(); +} + +#if IR_RX_BRIEF_LOG void IR_DecoderRaw::rxBriefNoteMuteBlockedIsr(uint32_t tUs) { (void)tUs; @@ -373,10 +401,8 @@ inline void IR_DecoderRaw::checkTimeout() #if defined(IRDEBUG_SERIAL_PACK) packTraceOnTimeoutOrAbort(false); #endif -#if IR_RX_BRIEF_LOG const uint16_t expected = (i_dataBuffer >= 8U) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0U; rxBriefLog(RxBriefReason::Timeout, i_dataBuffer, expected, micros()); -#endif isRecive = false; // приём завершён msgTypeReceive = 0; // Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся @@ -487,9 +513,7 @@ void IR_DecoderRaw::processDecodedFront(const FrontStorage ¤tFront) if (short_low_glitch) { errors.other++; -#if IR_RX_BRIEF_LOG rxBriefLog(RxBriefReason::Glitch, 1, 0, currentFront.time); -#endif #if IR_GLITCH_REJECT_PHASE_NUDGE irGlitchPhaseNudge(currentFront.time, riseSyncTime, prevRise); #endif @@ -504,9 +528,7 @@ void IR_DecoderRaw::processDecodedFront(const FrontStorage ¤tFront) if (micro_gap_rise) { errors.other++; -#if IR_RX_BRIEF_LOG rxBriefLog(RxBriefReason::Glitch, 1, 0, currentFront.time); -#endif #if IR_GLITCH_REJECT_PHASE_NUDGE irGlitchPhaseNudge(currentFront.time, riseSyncTime, prevRise); #endif @@ -516,9 +538,7 @@ void IR_DecoderRaw::processDecodedFront(const FrontStorage ¤tFront) if (candRp <= riseTimeMax / 4U && !highCount && !lowCount) { errors.other++; -#if IR_RX_BRIEF_LOG rxBriefLog(RxBriefReason::Timing, irClampU16(candRp), 0, currentFront.time); -#endif return; } @@ -582,11 +602,9 @@ void IR_DecoderRaw::processDecodedFront(const FrontStorage ¤tFront) if (risePeriod > IR_timeout || isBufferOverflow || risePeriod < riseTimeMin || isWrongPack) // ~Мы в пределах таймаута и буффер не переполнен и fix дроблёных единиц { -#if IR_RX_BRIEF_LOG if (!isBufferOverflow && !isWrongPack) rxBriefLog(RxBriefReason::Timing, irClampU16((uint32_t)risePeriod), irClampU16((uint32_t)highTime), currentFront.time); -#endif return; } @@ -752,9 +770,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) if (i_dataBuffer >= dataByteSizeMax * 8) { // проверка переполнения (>=: иначе при i_dataBuffer==dataByteSizeMax*8 запись dataBuffer[38] за границей массива — B3) isBufferOverflow = true; -#if IR_RX_BRIEF_LOG rxBriefLog(RxBriefReason::BufferOverflow, i_dataBuffer, 0, micros()); -#endif #if defined(IRDEBUG_SERIAL_PACK) if (packTraceOpen) packTraceEmitErrorFlash(F("ERROR: buffer overflow")); @@ -831,9 +847,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) #endif { isWrongPack = true; -#if IR_RX_BRIEF_LOG rxBriefLog(RxBriefReason::Sync, err_syncBit, 0, micros()); -#endif #if defined(IRDEBUG_SERIAL_PACK) packTraceEmitErrorFlash(F("ERROR: Wrong sync bit")); #endif @@ -935,12 +949,10 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) #endif const uint16_t errSum = uint16_t(errors.lowSignal) + uint16_t(errors.highSignal) + uint16_t(errors.other); -#if IR_RX_BRIEF_LOG if (isAvailable) rxBriefLog(RxBriefReason::Ok, packSize, errSum, micros()); else rxBriefLog(RxBriefReason::Crc, packSize, errSum, micros()); -#endif if (!isAvailable && packSize > 0 && packSize <= dataByteSizeMax) { memcpy(rejectBuffer, dataBuffer, packSize); rejectPackSize = static_cast(packSize); @@ -1486,9 +1498,7 @@ void IR_DecoderRaw::pulseFilterPushRaw(const FrontStorage &e) if (pulseFilterHoldCount >= kPulseFilterHoldCap) { pulseFilterDropHoldOverflow++; -#if IR_RX_BRIEF_LOG rxBriefLog(RxBriefReason::HoldOverflow, irClampU16(pulseFilterDropHoldOverflow), 0, e.time); -#endif pulseFilterShiftLeft(1); } @@ -1513,9 +1523,7 @@ bool IR_DecoderRaw::pulseFilterTryTakeConfirmed(FrontStorage &out, uint32_t logT if (dt < minUs) { pulseFilterDropGlitchPairs++; -#if IR_RX_BRIEF_LOG rxBriefLog(RxBriefReason::Glitch, irClampU16(pulseFilterDropGlitchPairs), 0, logTime); -#endif pulseFilterShiftLeft(2); continue; } @@ -1545,9 +1553,7 @@ bool IR_DecoderRaw::pulseFilterTryFlushOne(uint32_t nowUs, FrontStorage &out) if (dt < IR_INPUT_MIN_PULSE_US) { pulseFilterDropGlitchPairs++; -#if IR_RX_BRIEF_LOG rxBriefLog(RxBriefReason::Glitch, irClampU16(pulseFilterDropGlitchPairs), 0, nowUs); -#endif pulseFilterShiftLeft(2); continue; } @@ -1623,9 +1629,7 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front) { if ((uint32_t)(front.time - preambleCandidateLastEdgeTime) > candTimeout) { -#if IR_RX_BRIEF_LOG rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, 0, front.time); -#endif preambleStartCandidate(front); } @@ -1646,9 +1650,7 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front) { preambleGoodPeriods = 0; preambleMeanPeriod = 0; -#if IR_RX_BRIEF_LOG rxBriefLog(RxBriefReason::Preamble, 0, irClampU16(period), front.time); -#endif return true; } @@ -1669,9 +1671,7 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front) } else { -#if IR_RX_BRIEF_LOG rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, irClampU16(period), front.time); -#endif preambleGoodPeriods = 1; preambleMeanPeriod = (uint16_t)period; } diff --git a/IR_DecoderRaw.h b/IR_DecoderRaw.h index 6cc3dee..a574934 100644 --- a/IR_DecoderRaw.h +++ b/IR_DecoderRaw.h @@ -65,6 +65,14 @@ public: uint16_t edgeTraceFlushChunk(Print &out, uint16_t maxRec = 48); #endif + /// Always-on счётчики RX-событий по причинам (см. RxBriefReason: 6=Glitch, + /// 7=Timing, 8=Preamble, 9=Sync, 10=BufOverflow, 11=Timeout, 12=Crc, 13=Ok). + /// MuteBegin/End и RawOverflow(1..3) тикают только при IR_RX_BRIEF_LOG (ISR-агрегат). + const uint16_t *rxReasonCounters() const { return rxReasonCnt; } + void rxReasonCountersClear() { for (uint8_t i = 0; i < 14; i++) rxReasonCnt[i] = 0; } + /// Однострочная сводка: "RXSTAT,GLITCH=..,TIME=..,PREAMB=..,SYNC=..,BUF=..,TIMEOUT=..,CRC=..,OK=.." + void printRxReasonStats(Print &out) const; + /// Кадр собран по длине из заголовка, но CRC не сошёлся — один раз можно прочитать копию сырых байтов. bool availableReject(); uint8_t getRejectSize() const { return rejectPackSize; } @@ -235,9 +243,13 @@ bool isReciveRaw = false; /// @return Результат uint16_t ceil_div(uint16_t val, uint16_t divider); + // rxBriefLog безусловен: ВСЕГДА инкрементирует rxReasonCnt[reason] + // (always-on наблюдаемость по контракту живучести), печать события — + // только при IR_RX_BRIEF_LOG. Вызовы в местах отказов тоже безусловны. + void rxBriefLog(RxBriefReason reason, uint16_t a = 0, uint16_t b = 0, uint32_t tUs = 0); + uint16_t rxReasonCnt[14] = {}; // индекс = (uint8_t)RxBriefReason, 1..13 #if IR_RX_BRIEF_LOG static const __FlashStringHelper *rxBriefReasonTag(RxBriefReason reason); - void rxBriefLog(RxBriefReason reason, uint16_t a = 0, uint16_t b = 0, uint32_t tUs = 0); void rxBriefNoteMuteBlockedIsr(uint32_t tUs); void rxBriefNoteRawOverflowIsr(uint32_t tUs); void rxBriefFlushDeferredIsrLogs(); From 96ffb91b971302ba8edc080a1ef704967587cefd Mon Sep 17 00:00:00 2001 From: DashyFox Date: Tue, 11 Aug 2026 18:33:07 +0300 Subject: [PATCH 12/24] =?UTF-8?q?fix(tx):=20=D0=BF=D1=80=D0=B8=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D1=82=D0=B5=D1=82=20IRQ=20DMA-TX=20=D1=8F=D0=B2=D0=BD?= =?UTF-8?q?=D0=BE=20=D0=BD=D0=B8=D0=B6=D0=B5=20SysTick/EXTI=20=E2=80=94=20?= =?UTF-8?q?millis()=20=D0=B7=D0=B0=D0=BC=D0=B5=D0=B4=D0=BB=D1=8F=D0=BB?= =?UTF-8?q?=D1=81=D1=8F=20=D0=BF=D0=BE=D0=B4=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B4=D0=B0=D1=87=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HAL_NVIC_EnableIRQ без SetPriority оставлял DMA-каналы TX на приоритете 0 (уровень SysTick на G4). Длинный refill кольца BSRR (полубуфер 2048 слов = ~3.3мс при HCLK 12МГц) не вытеснялся, тики SysTick слипались: millis() замедлялся в 1.6x (один излучатель) / 2.2x (два) на всё время передачи. На машинке это ~37% вранья часов в каждый ИК-кадр: PID dt, планировщик, тайминги протокола. Мерено на стенде TofSickHunt 2026-08-11 (Car raw/idle_cur_vs_coup*): прогоны «60с» шли 85-95 реальных секунд. Фикс: irproto::kDmaTxIrqPriority=8 — ниже EXTI приёмника (6), выше дна; SysTick честный. ISR-TX путь (F401/КУ) не подвержен: его refill на TIM-приоритете 14. Верификация дилатации на стенде — раунд 4 (прерван, перезапустить). Co-Authored-By: Claude Fable 5 --- IR_config.h | 7 +++++++ IrDmaTxStm32.h | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/IR_config.h b/IR_config.h index afdfff5..31f439e 100644 --- a/IR_config.h +++ b/IR_config.h @@ -6,6 +6,13 @@ /** Число потоков DMA-TX задаётся шаблоном: IrDmaTxStm32<2>, см. IrDmaTxStm32.h и irproto::kDefaultDmaTxMaxStreams. */ namespace irproto { constexpr size_t kDefaultDmaTxMaxStreams = 4U; +/** NVIC-приоритет IRQ DMA-каналов TX (refill кольца BSRR-слов). Refill длинный (полубуфер + * 2048 слов = миллисекунды на небыстрых SYSCLK), а его дедлайн мягкий (~9 мс при 38кГц×6), + * поэтому он обязан сидеть НИЖЕ SysTick (0) и EXTI приёмника (6): с приоритетом по умолчанию + * (0, HAL_NVIC_EnableIRQ без SetPriority) он не вытеснялся SysTick'ом, тики слипались и + * millis() замедлялся в 1.6×/2.2× (один/два потока) на всё время передачи — мерено на стенде + * TofSickHunt 2026-08-11. Меньше число = выше приоритет; 8 = ниже EXTI, выше самого дна. */ +constexpr uint32_t kDmaTxIrqPriority = 8U; /** Кольцевой буфер BSRR-слов для ISR-TX (как у DMA: два полублока). Чётное число. */ constexpr uint16_t kIsrTxBsrrWordCount = 256U; /** Максимум RLE-сегментов для buildGateRuns при ISR-TX. */ diff --git a/IrDmaTxStm32.h b/IrDmaTxStm32.h index 2e71fe2..3ef0549 100644 --- a/IrDmaTxStm32.h +++ b/IrDmaTxStm32.h @@ -87,6 +87,10 @@ public: s_instance = this; for (uint8_t i = 0; i < streamCount_; i++) { + // Приоритет ЯВНО и ДО включения: без SetPriority канал остаётся на 0 (уровень + // SysTick), длинный refill не вытесняется и глотает тики millis. Обоснование + // и измерения — у irproto::kDmaTxIrqPriority (IR_config.h). + HAL_NVIC_SetPriority(streams_[i].dmaIrq, irproto::kDmaTxIrqPriority, 0); HAL_NVIC_EnableIRQ(streams_[i].dmaIrq); } From e25feb6824d726c040e36c1c09ad5226c32cdba6 Mon Sep 17 00:00:00 2001 From: DashyFox Date: Fri, 28 Aug 2026 14:30:44 +0300 Subject: [PATCH 13/24] reconstruct: restore brave-tape3 packet types stage --- PacketTypes.cpp | 95 +++++++++++++++++++++++++++++++++++++++++++------ PacketTypes.h | 29 +++++++++------ 2 files changed, 103 insertions(+), 21 deletions(-) diff --git a/PacketTypes.cpp b/PacketTypes.cpp index d1c9dc4..db7fd5c 100644 --- a/PacketTypes.cpp +++ b/PacketTypes.cpp @@ -2,12 +2,49 @@ namespace PacketTypes { - bool BasePack::checkAddress() { return true; }; - void BasePack::set(IR_FOX::PackInfo *packInfo, uint16_t id) + uint8_t minimumPacketSize(uint8_t msgType) { + switch (msgType) + { + case IR_MSG_DATA_ACCEPT: + case IR_MSG_DATA_NOACCEPT: + case IR_MSG_BACK_TO: + case IR_MSG_REQUEST: + return uint8_t(msgBytes + addrBytes + addrBytes + crcBytes); + case IR_MSG_BACK: + return uint8_t(msgBytes + addrBytes + crcBytes); + case IR_MSG_ACCEPT: + return uint8_t(msgBytes + addrBytes + 1U + crcBytes); + default: + return 0; + } + } + + bool isTypedPacketSizeValid(uint8_t msgType, uint8_t packSize) + { + const uint8_t minimum = minimumPacketSize(msgType); + return minimum != 0 && packSize >= minimum; + } + + bool BasePack::checkAddress() { return true; } + bool BasePack::set(IR_FOX::PackInfo *packInfo, uint16_t id, bool requireTypedSize) + { + isAvailable = false; + isRawAvailable = false; this->packInfo = packInfo; this->id = id; + if (packInfo == nullptr || packInfo->buffer == nullptr) + { + return false; + } + + const uint8_t msgType = (packInfo->buffer[msgOffset] >> 5) & IR_MASK_MSG_TYPE; + if (requireTypedSize && !isTypedPacketSizeValid(msgType, packInfo->packSize)) + { + return false; + } + if (checkAddress()) { isAvailable = true; @@ -23,29 +60,65 @@ namespace PacketTypes Serial.print(" NOT-OK "); #endif } + return isAvailable; } uint16_t BasePack::_getAddrFrom(BasePack *obj) { + if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr || + obj->packInfo->packSize < crcBytes || + uint16_t(obj->addressFromOffset) + 1U >= uint16_t(obj->packInfo->packSize - crcBytes)) + { + return 0; + } return (obj->packInfo->buffer[obj->addressFromOffset] << 8) | obj->packInfo->buffer[obj->addressFromOffset + 1]; - }; + } uint16_t BasePack::_getAddrTo(BasePack *obj) { + if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr || + obj->packInfo->packSize < crcBytes || + uint16_t(obj->addressToOffset) + 1U >= uint16_t(obj->packInfo->packSize - crcBytes)) + { + return 0; + } return (obj->packInfo->buffer[obj->addressToOffset] << 8) | obj->packInfo->buffer[obj->addressToOffset + 1]; - }; + } uint8_t BasePack::_getDataSize(BasePack *obj) { - return obj->packInfo->packSize - crcBytes - obj->DataOffset; - }; + if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr) + { + return 0; + } + const uint16_t frameOverhead = uint16_t(crcBytes) + uint16_t(obj->DataOffset); + if (uint16_t(obj->packInfo->packSize) <= frameOverhead) + { + return 0; + } + return uint8_t(uint16_t(obj->packInfo->packSize) - frameOverhead); + } uint8_t *BasePack::_getDataPrt(BasePack *obj) { + if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr || + obj->packInfo->packSize < crcBytes) + { + return nullptr; + } + const uint16_t dataEnd = uint16_t(obj->packInfo->packSize) - uint16_t(crcBytes); + if (uint16_t(obj->DataOffset) > dataEnd) + { + return nullptr; + } return obj->packInfo->buffer + obj->DataOffset; - }; + } uint8_t BasePack::_getDataRawSize(BasePack *obj) { + if (obj == nullptr || obj->packInfo == nullptr) + { + return 0; + } return obj->packInfo->packSize; - }; + } bool BasePack::available() { @@ -59,7 +132,7 @@ namespace PacketTypes { return false; } - }; + } bool BasePack::availableRaw() { if (isRawAvailable) @@ -71,7 +144,7 @@ namespace PacketTypes { return false; } - }; + } bool Data::checkAddress() { @@ -104,4 +177,4 @@ namespace PacketTypes IR_FOX::checkAddressRuleApply(getAddrTo(), this->id, ret); return ret; } -} \ No newline at end of file +} diff --git a/PacketTypes.h b/PacketTypes.h index 37e18a8..734913a 100644 --- a/PacketTypes.h +++ b/PacketTypes.h @@ -4,25 +4,34 @@ class IR_Decoder; namespace PacketTypes { + /** + * Minimum complete frame size (header, addresses/data required by the type, + * and CRC). Unknown/reserved message types return 0. + */ + uint8_t minimumPacketSize(uint8_t msgType); + + /** True only for a known typed packet whose complete frame is long enough. */ + bool isTypedPacketSizeValid(uint8_t msgType, uint8_t packSize); + class BasePack { friend IR_Decoder; protected: - bool isAvailable; - bool isRawAvailable; - bool isNeedAccept; + bool isAvailable = false; + bool isRawAvailable = false; + bool isNeedAccept = false; - uint8_t msgOffset; - uint8_t addressFromOffset; - uint8_t addressToOffset; - uint8_t DataOffset; + uint8_t msgOffset = 0; + uint8_t addressFromOffset = 0; + uint8_t addressToOffset = 0; + uint8_t DataOffset = 0; - IR_FOX::PackInfo *packInfo; - uint16_t id; + IR_FOX::PackInfo *packInfo = nullptr; + uint16_t id = 0; virtual bool checkAddress(); - void set(IR_FOX::PackInfo *packInfo, uint16_t id); + bool set(IR_FOX::PackInfo *packInfo, uint16_t id, bool requireTypedSize = true); static uint16_t _getAddrFrom(BasePack *obj); static uint16_t _getAddrTo(BasePack *obj); From a589416cfccd7f760284eda46edbb1401f8d5a8b Mon Sep 17 00:00:00 2001 From: DashyFox Date: Fri, 28 Aug 2026 14:35:19 +0300 Subject: [PATCH 14/24] reconstruct: restore brave-tape3 decoder stage --- IR_Decoder.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/IR_Decoder.cpp b/IR_Decoder.cpp index 9998b03..a8b1809 100644 --- a/IR_Decoder.cpp +++ b/IR_Decoder.cpp @@ -162,7 +162,9 @@ void IR_Decoder::_tick() if (addrAcceptSendTo && addrAcceptSendTo < IR_Broadcast) isWaitingAcceptSend = true; } - gotRaw.set(&packInfo, id); + // Raw keeps the decoder's common minimum-size contract. Known packet + // layouts are validated by their typed BasePack::set calls above. + gotRaw.set(&packInfo, id, false); } if (isWaitingAcceptSend && millis() - acceptSendTimer > acceptDelay) { From 628c050702663f1efb69de6e23b5809964d8c224 Mon Sep 17 00:00:00 2001 From: DashyFox Date: Fri, 4 Sep 2026 15:09:27 +0300 Subject: [PATCH 15/24] =?UTF-8?q?feat(rx):=20rxLineActive()=20=E2=80=94=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=D0=B7=D0=BD=D0=B0=D0=BA=20=C2=AB=D0=B8=D0=B4?= =?UTF-8?q?=D1=91=D1=82=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=BD=D1=8B=D0=B9?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=D1=91=D0=BC=C2=BB=20=D0=BF=D0=BE=20=D0=A1?= =?UTF-8?q?=D0=9E=D0=A1=D0=A2=D0=9E=D0=AF=D0=9D=D0=98=D0=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Декодер уже знает состояние приёма (isReciving=локнут; preambleState Idle/ Candidate/Locked). rxLineActive() = локнут ИЛИ формируется ВАЛИДНАЯ преамбула (>=1 совпавший по периоду фронт — отличает реальный кадр от одиночного шумового фронта, который лишь заводит Candidate, но не набирает goodPeriods). Для гейта заднего у машинки: не стрелять, пока на линии идёт/формируется ответ точки — решение по состоянию, а не по хардкод-длительности (Даша 04.09). Аддитивно, const. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01J3Ca6J2JJ1NrptqQjKs8BK --- IR_DecoderRaw.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/IR_DecoderRaw.h b/IR_DecoderRaw.h index a574934..7408c8d 100644 --- a/IR_DecoderRaw.h +++ b/IR_DecoderRaw.h @@ -52,6 +52,14 @@ public: inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился bool isSubOverflow(); volatile inline bool isReciving() { return isRecive; }; // Возвращает true, если происходит приём пакета + // Активность линии по СОСТОЯНИЮ (не по хардкод-длительности): кадр залочен ИЛИ формируется + // ВАЛИДНАЯ преамбула (>=1 совпавший по периоду фронт — отличает реальный кадр от одиночного + // шумового фронта, который лишь заводит Candidate, но не набирает goodPeriods). Для гейта заднего: + // «не стрелять, пока на линии идёт/формируется кадр (напр. ответ точки)». Аддитивно, const. + inline bool rxLineActive() const { + return isRecive || + (preambleState == PreambleState::Candidate && preambleGoodPeriods >= 1U); + } uint32_t pulseFilterDroppedByFilteredOverflow() const { return 0; } uint32_t pulseFilterDroppedByHoldOverflow() const { return pulseFilterDropHoldOverflow; } uint32_t pulseFilterDroppedGlitchPairs() const { return pulseFilterDropGlitchPairs; } From 6c97d33c7c3439a6727de93ed11aa42bacf5bd3c Mon Sep 17 00:00:00 2001 From: DashyFox Date: Fri, 4 Sep 2026 15:51:59 +0300 Subject: [PATCH 16/24] =?UTF-8?q?feat(rx):=20rxDeclaredPackSize/rxMaxPackS?= =?UTF-8?q?ize=20+=20packAirtimeMs=20=E2=80=94=20=D0=B1=D1=8E=D0=B4=D0=B6?= =?UTF-8?q?=D0=B5=D1=82=20=D1=83=D0=B4=D0=B5=D1=80=D0=B6=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BE=20=D1=80=D0=B0=D0=B7=D0=BC=D0=B5=D1=80?= =?UTF-8?q?=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Для гейта заднего у машинки (Даша 04.09): держать задний по СОСТОЯНИЮ приёма, но не дольше airtime кадра — а если объявленный в 1-м байте размер МЕНЬШЕ протокольного максимума, то ровно до его конца (усиление надёжности). rxDeclaredPackSize() = packSize из 1-го байта, если валиден (не битый) и идёт приём, иначе 0 (потребитель берёт rxMaxPackSize() — до CRC значение недоверенное). packAirtimeMs() — публичная обёртка над calculateSendTime (чистая функция протокольных констант, годна и для RX). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01J3Ca6J2JJ1NrptqQjKs8BK --- IR_DecoderRaw.h | 8 ++++++++ IR_Encoder.h | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/IR_DecoderRaw.h b/IR_DecoderRaw.h index 7408c8d..19b7955 100644 --- a/IR_DecoderRaw.h +++ b/IR_DecoderRaw.h @@ -60,6 +60,14 @@ public: return isRecive || (preambleState == PreambleState::Candidate && preambleGoodPeriods >= 1U); } + // Объявленная длина ПРИНИМАЕМОГО кадра (байт) из ПЕРВОГО байта, если он уже принят и валиден; + // иначе 0 (ещё не знаем / битый). До CRC это НЕДОВЕРЕННОЕ значение — потребитель, получив 0 + // или чрезмерное, обязан брать rxMaxPackSize() (безопасно держать задний до конца макс.кадра). + inline uint16_t rxDeclaredPackSize() const { + return (isRecive && packSize && !isWrongPack) ? packSize : 0; + } + // Протокольный МАКСИМУМ длины кадра (байт) — верхняя граница бюджета удержания заднего. + static constexpr uint16_t rxMaxPackSize() { return (uint16_t)dataByteSizeMax; } uint32_t pulseFilterDroppedByFilteredOverflow() const { return 0; } uint32_t pulseFilterDroppedByHoldOverflow() const { return pulseFilterDropHoldOverflow; } uint32_t pulseFilterDroppedGlitchPairs() const { return pulseFilterDropGlitchPairs; } diff --git a/IR_Encoder.h b/IR_Encoder.h index bb21d4b..af097cf 100644 --- a/IR_Encoder.h +++ b/IR_Encoder.h @@ -62,6 +62,10 @@ public: /// @param decPair Если задан, конструктор регистрирует этот один приёмник как blind-decoder /// (аналог setBlindDecoders() для одного RX). IR_Encoder(uint8_t pin, uint16_t addr = 0, IR_DecoderRaw *decPair = nullptr, bool autoHandle = true); + /// Публичная оценка airtime кадра (мс) по его полной длине в байтах (packSize). Чистая функция + /// протокольных констант — подходит и для приёма (напр. бюджет удержания заднего по объявленному + /// в 1-м байте размеру принимаемого ответа). БЕЗ +30% компенсации занижения sync — добавляет потребитель. + uint32_t packAirtimeMs(uint8_t packSize) const { return calculateSendTime(packSize); } static void isr(); static void begin(HardwareTimer* timer, uint8_t channel, IRQn_Type IRQn, uint8_t priority, void(*isrCallback)() = nullptr); /** From 8f45f6e2144c184d531103d17804902b9e3027b5 Mon Sep 17 00:00:00 2001 From: DashyFox Date: Mon, 7 Sep 2026 12:22:22 +0300 Subject: [PATCH 17/24] =?UTF-8?q?feat(rx):=20=D0=BD=D0=B0=D0=B1=D0=BB?= =?UTF-8?q?=D1=8E=D0=B4=D0=B0=D0=B5=D0=BC=D0=BE=D1=81=D1=82=D1=8C=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D1=91=D0=BC=D0=B0=20=D0=BF=D0=BE=20=D1=81=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D1=8E=20+=20airtime=20?= =?UTF-8?q?=D0=BA=D0=B0=D0=B4=D1=80=D0=B0=20=D1=84=D0=BE=D1=80=D0=BC=D1=83?= =?UTF-8?q?=D0=BB=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IR_config.h: irFrameAirtimeUs/irFrameDecodeEndUs/irLockToDecodeEndUs/irLockLatencyUs, irMaxPackSize (31), irDataPackSize/irBackPackSize — все из констант FSM передатчика (преамбула 6×98 тактов, байт 11 бит × 74 такта, такт = полпериода несущей). - IR_Encoder::calculateSendTime по той же формуле (раньше синхробиты считались один раз на кадр → занижение 21-26%, потребители компенсировали +30%). - IR_DecoderRaw: rxLockSeq/rxLockTimeUs (лок преамбулы), rxMsgType, rxExpectedEndUs (по объявленной длине), rxLastEnd {seq, reason Ok/Crc/Timeout/Abort, msgType, packSize, tUs, expectedEndUs}. Терминалы: конец кадра, таймаут тишины, немедленный abort при sync-ошибке / длине <3 (в т.ч. 0) / переполнении — битый кадр больше не держит isReciving до конца чужой передачи + 30 мс. - Порядок в tick: checkTimeout до listenStart (TIMEOUT-лог с реальной длиной), истечение кандидата преамбулы без фронтов, ложный PREAMB-инкремент на старте кандидата убран. - rxMaxPackSize() = протокольные 31 (было 38 = размер буфера). Совместимость: только добавления; существующие сигнатуры не тронуты. Собрано для G4 (Car) и F4 (КУ). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq --- IR_DecoderRaw.cpp | 68 ++++++++++++++++++++++++++++++++++++++++++----- IR_DecoderRaw.h | 26 +++++++++++++++++- IR_Encoder.cpp | 22 ++------------- IR_config.h | 23 ++++++++++++++++ 4 files changed, 111 insertions(+), 28 deletions(-) diff --git a/IR_DecoderRaw.cpp b/IR_DecoderRaw.cpp index 325d52a..7359dc8 100644 --- a/IR_DecoderRaw.cpp +++ b/IR_DecoderRaw.cpp @@ -403,6 +403,7 @@ inline void IR_DecoderRaw::checkTimeout() #endif const uint16_t expected = (i_dataBuffer >= 8U) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0U; rxBriefLog(RxBriefReason::Timeout, i_dataBuffer, expected, micros()); + noteRxEnd(RxEndReason::Timeout, micros()); isRecive = false; // приём завершён msgTypeReceive = 0; // Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся @@ -417,6 +418,49 @@ inline void IR_DecoderRaw::checkTimeout() } // ==================================================================== +void IR_DecoderRaw::noteRxEnd(RxEndReason reason, uint32_t tUs) +{ + rxEnd.seq++; + rxEnd.reason = reason; + rxEnd.msgType = (i_dataBuffer >= 8U * msgBytes) ? (uint8_t)((dataBuffer[0] >> 5) & IR_MASK_MSG_TYPE) : 0xFF; + rxEnd.packSize = (uint8_t)packSize; + rxEnd.tUs = tUs; + rxEnd.expectedEndUs = (packSize >= msgBytes + crcBytes) + ? rxLockTimeUsVal + irLockToDecodeEndUs((uint8_t)packSize) + irTicksToUs((uint32_t)syncBits * irBitTicks) + : 0U; +} + +void IR_DecoderRaw::abortFrame(uint32_t tUs) +{ +#if defined(IRDEBUG_SERIAL_PACK) + packTraceOnTimeoutOrAbort(false); +#endif + noteRxEnd(RxEndReason::Abort, tUs); + isRecive = false; + isReciveRaw = false; + msgTypeReceive = 0; + firstRX(); +} + +void IR_DecoderRaw::expirePreambleCandidate() +{ + if (preambleState != PreambleState::Candidate || rxTimeoutPipelineBusy()) + return; + if ((micros() - preambleCandidateLastEdgeTime) > IR_timeout * (uint32_t)IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT) + { + if (preambleGoodPeriods) + rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, 0, micros()); + preambleResetToIdle(); + } +} + +uint32_t IR_DecoderRaw::rxExpectedEndUs() const +{ + if (!isRecive || preambleState != PreambleState::Locked || isWrongPack || packSize < msgBytes + crcBytes) + return 0; + return rxLockTimeUsVal + irLockToDecodeEndUs((uint8_t)packSize); +} + void IR_DecoderRaw::tick() { #if IR_RX_BRIEF_LOG @@ -471,15 +515,16 @@ void IR_DecoderRaw::tick() if (!processedFront) { isSubBufferOverflow = false; - listenStart(); checkTimeout(); + listenStart(); + expirePreambleCandidate(); #if defined(IR_EDGE_TRACE) while (edgeTraceFlushChunk(Serial, 48) > 0) {} #endif return; } // Если данных нет - ничего не делаем - listenStart(); checkTimeout(); + listenStart(); #if IR_RX_BRIEF_LOG rxBriefFlushDeferredIsrLogs(); #endif @@ -780,10 +825,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) { // Как checkTimeout/listenStart: firstRX() сбрасывает буфер битов, преамбулу и // pulseFilterReset() — при IR_INPUT_MIN_PULSE_US > 0 иначе остаётся «хвост» в hold/filtered. - isRecive = false; - isReciveRaw = false; - msgTypeReceive = 0; - firstRX(); + abortFrame(micros()); return; } @@ -851,6 +893,8 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) #if defined(IRDEBUG_SERIAL_PACK) packTraceEmitErrorFlash(F("ERROR: Wrong sync bit")); #endif + abortFrame(micros()); // битый кадр не удерживает приёмник до таймаута + return; } } } @@ -878,8 +922,12 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) // B1: под-минимальная длина (1..2) физически не несёт CRC (min кадр = msg+crc = 3 байта) → шум/битьё. // Без отсева packSize==1 даёт crcCheck(1-2) → len=255 → OOB-чтение dataBuffer[0..256] (массив 38). // packSize>=3 (в т.ч. будущие компактные кадры) обрабатываются как обычно. - if (packSize != 0 && packSize < msgBytes + crcBytes) + if (packSize < msgBytes + crcBytes) // 0..2: кадр физически не несёт CRC — шум/битьё + { isWrongPack = true; + abortFrame(micros()); + return; + } } // Тип приёма (для isReceive): выставляем сразу после первого байта, ДО проверки «Конец». @@ -903,6 +951,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) preambleResetToIdle(); msgTypeReceive = 0; isAvailable = crcCheck(packSize - crcBytes, crcValue); + noteRxEnd(isAvailable ? RxEndReason::Ok : RxEndReason::Crc, micros()); #ifdef BRUTEFORCE_CHECK { @@ -1622,7 +1671,10 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front) if (!isReciveRaw && front.dir && ((prevRise == 0U && front.time > longSilence) || (prevRise != 0U && (uint32_t)(front.time - prevRise) > longSilence))) + { preambleStartCandidate(front); + return true; + } } if (preambleState == PreambleState::Candidate) @@ -1703,6 +1755,8 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front) isRecive = true; isReciveRaw = true; risePeriod = preambleMeanPeriod; + rxLockSeqCnt++; + rxLockTimeUsVal = front.time; #if defined(IRDEBUG_SERIAL_PACK) packTraceResetFrame(); packTraceOpen = true; diff --git a/IR_DecoderRaw.h b/IR_DecoderRaw.h index 19b7955..4b12363 100644 --- a/IR_DecoderRaw.h +++ b/IR_DecoderRaw.h @@ -67,7 +67,25 @@ public: return (isRecive && packSize && !isWrongPack) ? packSize : 0; } // Протокольный МАКСИМУМ длины кадра (байт) — верхняя граница бюджета удержания заднего. - static constexpr uint16_t rxMaxPackSize() { return (uint16_t)dataByteSizeMax; } + static constexpr uint16_t rxMaxPackSize() { return (uint16_t)irMaxPackSize; } + + // ---- Наблюдаемость приёма по СОСТОЯНИЮ: лок / ожидаемый конец / факт завершения с причиной ---- + enum class RxEndReason : uint8_t { None = 0, Ok, Crc, Timeout, Abort }; + struct RxEndInfo { + uint16_t seq = 0; // номер завершения (растёт на каждом терминале) + RxEndReason reason = RxEndReason::None; + uint8_t msgType = 0xFF; // 0xFF = первый байт не был принят + uint8_t packSize = 0; // объявленная длина (0 = неизвестна) + uint32_t tUs = 0; // micros() терминала + uint32_t expectedEndUs = 0; // расчётный конец кадра в эфире (0 = неизвестен) + }; + uint16_t rxLockSeq() const { return rxLockSeqCnt; } // ++ в момент лока преамбулы + uint32_t rxLockTimeUs() const { return rxLockTimeUsVal; } // метка фронта лока (ISR-время) + /// Тип принимаемого кадра (3 бита) после первого байта; 0xFF пока неизвестен / приём не идёт. + uint8_t rxMsgType() const { return (isRecive && packSize) ? (uint8_t)((dataBuffer[0] >> 5) & IR_MASK_MSG_TYPE) : 0xFF; } + const RxEndInfo &rxLastEnd() const { return rxEnd; } + /// Расчётный момент последнего бита данных текущего кадра (по объявленной длине); 0 = не Locked / длина неизвестна. + uint32_t rxExpectedEndUs() const; uint32_t pulseFilterDroppedByFilteredOverflow() const { return 0; } uint32_t pulseFilterDroppedByHoldOverflow() const { return pulseFilterDropHoldOverflow; } uint32_t pulseFilterDroppedGlitchPairs() const { return pulseFilterDropGlitchPairs; } @@ -163,6 +181,12 @@ private: Locked = 2 }; PreambleState preambleState = PreambleState::Idle; + uint16_t rxLockSeqCnt = 0; + uint32_t rxLockTimeUsVal = 0; + RxEndInfo rxEnd; + void noteRxEnd(RxEndReason reason, uint32_t tUs); // терминал: фиксирует тип/длину/расчётный конец + void abortFrame(uint32_t tUs); // немедленный сброс битого кадра (sync/длина/overflow) + void expirePreambleCandidate(); // кандидат без фронтов дольше таймаута → Idle uint8_t preambleGoodPeriods = 0; uint16_t preambleMeanPeriod = 0; uint32_t preambleCandidateLastEdgeTime = 0; diff --git a/IR_Encoder.cpp b/IR_Encoder.cpp index c7c96c6..8663ccb 100644 --- a/IR_Encoder.cpp +++ b/IR_Encoder.cpp @@ -1183,26 +1183,8 @@ uint8_t IR_Encoder::bitLow[2] = { uint32_t IR_Encoder::calculateSendTime(uint8_t packSize) const { - // Расчет времени отправки пакета в миллисекундах - - // Время преамбулы: preambPulse * 2 фронта * bitTakts тактов - uint32_t preambTime = preambPulse * 2 * bitTakts; - - // Время данных: количество бит * bitTakts тактов - uint32_t dataTime = packSize * 8 * bitTakts; - - // Время синхронизации: syncBits * 2 фронта * bitTakts тактов - uint32_t syncTime = syncBits * 2 * bitTakts; - - // Общее время в тактах - uint32_t totalTakts = preambTime + dataTime + syncTime; - - // Конвертируем в миллисекунды - // carrierPeriod - период несущей в микросекундах - // totalTakts * carrierPeriod / 1000 = время в миллисекундах - uint32_t sendTimeMs = (totalTakts * carrierPeriod) / 1000; - - return sendTimeMs; + // Полное время кадра в эфире по формуле FSM (преамбула + байты с синхробитами), округление вверх до мс. + return (irFrameAirtimeUs(packSize) + 999U) / 1000U; } // Функции для тестирования времени отправки без фактической отправки diff --git a/IR_config.h b/IR_config.h index 31f439e..223a209 100644 --- a/IR_config.h +++ b/IR_config.h @@ -264,6 +264,29 @@ typedef uint16_t crc_t; #define bitTime (bitTakts * carrierPeriod) // Общая длительность бита #define tolerance 300U +// ---- Длительности и размеры кадра ФОРМУЛАМИ из FSM передатчика (IR_Encoder::txAdvanceBoundary) ---- +// Логический такт TX = полпериода несущей (toggleCounter считает полупериоды). Преамбула = 6 ран по +// (preambToggle+1) тактов; лок декодера — на 3-м RISE (конец 5-й раны); байт = (8 данных + 3 sync) бит по 74 такта. +constexpr uint32_t irTxTickNs = 1000000000UL / (carrierFrec * 2U); +constexpr uint32_t irPreambleTicks = (uint32_t)preambPulse * 2U * ((uint32_t)preambToggle + 1U); +constexpr uint32_t irLockTicks = ((uint32_t)preambPulse * 2U - 1U) * ((uint32_t)preambToggle + 1U); +constexpr uint32_t irBitTicks = (uint32_t)bitTakts * 2U; +constexpr uint32_t irByteTicks = ((uint32_t)bitPerByte + (uint32_t)syncBits) * irBitTicks; +constexpr uint32_t irTicksToUs(uint32_t ticks) { return (uint32_t)(((uint64_t)ticks * irTxTickNs + 500U) / 1000U); } +/// Полное время кадра в эфире (от первой несущей до последнего sync-бита), мкс. +constexpr uint32_t irFrameAirtimeUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks + (uint32_t)packSize * irByteTicks); } +/// От старта кадра до последнего БИТА ДАННЫХ (момент, когда декодер отдаёт кадр), мкс. +constexpr uint32_t irFrameDecodeEndUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks + (uint32_t)packSize * irByteTicks - (uint32_t)syncBits * irBitTicks); } +/// От лока декодера (3-й RISE преамбулы) до последнего бита данных, мкс. +constexpr uint32_t irLockToDecodeEndUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks - irLockTicks + (uint32_t)packSize * irByteTicks - (uint32_t)syncBits * irBitTicks); } +/// Латентность лока: от первой несущей чужого кадра до лока декодера, мкс. +constexpr uint32_t irLockLatencyUs = irTicksToUs(irLockTicks); +/// Протокольный максимум длины кадра (5-битное поле длины). +constexpr uint8_t irMaxPackSize = IR_MASK_MSG_INFO; +/// Размер кадра по полезной нагрузке: DATA (from+to) и BACK (только from). +constexpr uint8_t irDataPackSize(uint8_t payload) { return (uint8_t)(msgBytes + addrBytes * 2 + payload + crcBytes); } +constexpr uint8_t irBackPackSize(uint8_t payload) { return (uint8_t)(msgBytes + addrBytes + payload + crcBytes); } + constexpr uint16_t test_all_Time = bitTime; constexpr uint16_t test_all_Takts = bitTakts * 2; constexpr uint16_t test_hi = ((bitPauseTakts) * 2 - 0) + ((bitActiveTakts) * 2 - 0); From 5b9f73ec7cfe8f1c89a27501fe1a7265243b61aa Mon Sep 17 00:00:00 2001 From: DashyFox Date: Mon, 7 Sep 2026 16:30:44 +0300 Subject: [PATCH 18/24] =?UTF-8?q?feat(config):=20irRxByteTimeoutUs/irRxAbo?= =?UTF-8?q?rtSilenceUs=20=E2=80=94=20=D1=82=D0=B0=D0=B9=D0=BC=D0=B0=D1=83?= =?UTF-8?q?=D1=82=20=D0=B1=D0=B0=D0=B9=D1=82=D0=B0=20=D0=B8=20=D1=82=D0=B8?= =?UTF-8?q?=D1=88=D0=B8=D0=BD=D0=B0=20=D0=BE=D0=B1=D1=80=D1=8B=D0=B2=D0=B0?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=D1=91=D0=BC=D0=B0=20=D1=84=D0=BE=D1=80?= =?UTF-8?q?=D0=BC=D1=83=D0=BB=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Константы использует Car (IrCycle/Executer) вместо макроса IR_timeout, который завязан на член класса декодера. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq --- IR_config.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/IR_config.h b/IR_config.h index 223a209..f0ab3f1 100644 --- a/IR_config.h +++ b/IR_config.h @@ -281,6 +281,9 @@ constexpr uint32_t irFrameDecodeEndUs(uint8_t packSize) { return irTicksToUs(irP constexpr uint32_t irLockToDecodeEndUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks - irLockTicks + (uint32_t)packSize * irByteTicks - (uint32_t)syncBits * irBitTicks); } /// Латентность лока: от первой несущей чужого кадра до лока декодера, мкс. constexpr uint32_t irLockLatencyUs = irTicksToUs(irLockTicks); +/// Таймаут байта декодера (как IR_timeout при номинальном bitTime) и тишина, по которой декодер обрывает приём. +constexpr uint32_t irRxByteTimeoutUs = ((uint32_t)bitTime + tolerance) * ((uint32_t)bitPerByte + syncBits + 1U); +constexpr uint32_t irRxAbortSilenceUs = 2U * irRxByteTimeoutUs; /// Протокольный максимум длины кадра (5-битное поле длины). constexpr uint8_t irMaxPackSize = IR_MASK_MSG_INFO; /// Размер кадра по полезной нагрузке: DATA (from+to) и BACK (только from). From 86956bcf997e127aeb6d26091a87e5c784f5766e Mon Sep 17 00:00:00 2001 From: DashyFox Date: Mon, 7 Sep 2026 17:07:22 +0300 Subject: [PATCH 19/24] =?UTF-8?q?fix(rx):=20=D1=82=D0=B0=D0=B9=D0=BC=D0=B0?= =?UTF-8?q?=D1=83=D1=82=20=D0=BA=D0=B0=D0=BD=D0=B4=D0=B8=D0=B4=D0=B0=D1=82?= =?UTF-8?q?=D0=B0=20=D0=BF=D1=80=D0=B5=D0=B0=D0=BC=D0=B1=D1=83=D0=BB=D1=8B?= =?UTF-8?q?=203=C3=97=E2=86=921=C3=97=20IR=5Ftimeout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Кандидат без фронтов дольше таймаута байта преамбулой быть не может. При 3× (45 мс) rxLineActive() держал линию «занятой» после паразитных фронтов (засветка своим дальномером) и откладывал пинг машинки на ~30 мс сверх заданного гэпа. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq --- IR_config.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/IR_config.h b/IR_config.h index f0ab3f1..6997da5 100644 --- a/IR_config.h +++ b/IR_config.h @@ -231,9 +231,11 @@ typedef uint16_t crc_t; #ifndef IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT #define IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT 340U #endif -/** Таймаут окна кандидата преамбулы: IR_timeout * mult. */ +/** Таймаут окна кандидата преамбулы: IR_timeout * mult. Кандидат без фронтов дольше таймаута байта + преамбулой быть не может; при 3× линия считалась занятой (rxLineActive) ещё 45 мс после последнего + паразитного фронта (напр. засветка своим дальномером) и откладывала передачу. */ #ifndef IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT -#define IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT 3U +#define IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT 1U #endif #define preambPulse 3 From d103d2a3ae5c7197bd9a6da3b54cf83bf2b863e7 Mon Sep 17 00:00:00 2001 From: DashyFox Date: Mon, 7 Sep 2026 17:33:20 +0300 Subject: [PATCH 20/24] =?UTF-8?q?perf(tx):=20=D1=82=D0=B0=D0=B1=D0=BB?= =?UTF-8?q?=D0=B8=D1=86=D0=B0=20=D1=80=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=BA?= =?UTF-8?q?=D0=B0=D0=B4=D1=80=D0=B0=20=D1=81=D1=82=D1=80=D0=BE=D0=B8=D1=82?= =?UTF-8?q?=D1=81=D1=8F=20=D0=BF=D0=BE=20=D0=B3=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=86=D0=B0=D0=BC=20=D1=80=D0=B0=D0=BD=D0=BE=D0=B2,=20=D0=B0?= =?UTF-8?q?=20=D0=BD=D0=B5=20=D0=BF=D0=BE=20=D1=82=D0=B0=D0=BA=D1=82=D0=B0?= =?UTF-8?q?=D0=BC=20=D0=BD=D0=B5=D1=81=D1=83=D1=89=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildGateRuns/buildPhysicalGateRuns крутили автомат передачи по одному такту (пинг 10 байт = 8728 тактов) — на 12 МГц это ~30 мс между вызовом send и стартом DMA, на каждой передаче (задний ~20 мс). Обход по границам (txWalkRuns: ран = toggleCounter+1 тактов уровня state) даёт ту же последовательность за число шагов = число ранов (225). Эквивалентность проверена на хосте: 12480 случаев, len 1..38, множитель 2..8, логические и физические раны совпадают бит-в-бит (len=39 — чтение за буфером в sync-фазе автомата у обеих версий, на эфире ≤31). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq --- IR_Encoder.cpp | 93 ++++++++++++++++++++++++++++---------------------- IR_Encoder.h | 2 ++ 2 files changed, 54 insertions(+), 41 deletions(-) diff --git a/IR_Encoder.cpp b/IR_Encoder.cpp index 8663ccb..62ec179 100644 --- a/IR_Encoder.cpp +++ b/IR_Encoder.cpp @@ -352,6 +352,29 @@ bool IR_Encoder::txEmitTick(TxFsmState &st, const uint8_t *sendBufferLocal, bool return txAdvanceAfterOutput(st, sendBufferLocal); } +// Обход кадра по границам ранов: между границами автомат выдаёт st.toggleCounter+1 тактов +// уровня st.state (txAdvanceAfterOutput считает toggleCounter до нуля, затем txAdvanceBoundary +// открывает следующий ран). Даёт ту же последовательность тактов, что потиковый обход, но за +// число шагов = число ранов (пинг: ~230 вместо ~8700 тактов — на 12 МГц это ~30 мс перед стартом DMA). +template +bool IR_Encoder::txWalkRuns(TxFsmState &st, const uint8_t *sendBufferLocal, Emit emit) +{ + for (;;) + { + const bool gate = st.state; + const uint32_t lenTicks = (uint32_t)st.toggleCounter + 1U; + if (!emit(gate, lenTicks)) + { + return false; + } + st.toggleCounter = 0; + if (!txAdvanceBoundary(st, sendBufferLocal)) + { + return true; + } + } +} + void IR_Encoder::loadTxFsmFromMembers(TxFsmState &st) const { st.sendLen = sendLen; @@ -476,28 +499,27 @@ size_t IR_Encoder::buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRu st.currentBitSequence = bitHigh; size_t runCount = 0; - bool isActive = true; - while (isActive) - { - bool gate = false; - isActive = txEmitTick(st, sendBufferLocal, gate); - + const bool ok = txWalkRuns(st, sendBufferLocal, [&](bool gate, uint32_t lenTicks) -> bool { if (runCount > 0 && outRuns[runCount - 1].gate == gate) { - outRuns[runCount - 1].lenTicks = (uint16_t)(outRuns[runCount - 1].lenTicks + 1U); - } - else - { - if (runCount >= maxRuns) + const uint32_t merged = (uint32_t)outRuns[runCount - 1].lenTicks + lenTicks; + if (merged > 65535U) { - return 0; + return false; } - outRuns[runCount].gate = gate; - outRuns[runCount].lenTicks = 1U; - runCount++; + outRuns[runCount - 1].lenTicks = (uint16_t)merged; + return true; } - } - return runCount; + if (runCount >= maxRuns || lenTicks > 65535U) + { + return false; + } + outRuns[runCount].gate = gate; + outRuns[runCount].lenTicks = (uint16_t)lenTicks; + runCount++; + return true; + }); + return ok ? runCount : 0; } size_t IR_Encoder::buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns, uint16_t multiply) @@ -564,40 +586,29 @@ size_t IR_Encoder::buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_ bool currentGate = false; uint32_t currentLogicalLen = 0; bool havePendingRun = false; - bool isActive = true; - while (isActive) - { - bool gate = false; - isActive = txEmitTick(st, sendBufferLocal, gate); - - if (!havePendingRun) + const bool ok = txWalkRuns(st, sendBufferLocal, [&](bool gate, uint32_t lenTicks) -> bool { + if (havePendingRun && currentGate == gate) { - currentGate = gate; - currentLogicalLen = 1U; - havePendingRun = true; - continue; + currentLogicalLen += lenTicks; + return true; } - - if (currentGate == gate) + if (havePendingRun && !appendPhysicalRun(currentGate, currentLogicalLen, runCount)) { - currentLogicalLen++; - continue; + return false; } - - if (!appendPhysicalRun(currentGate, currentLogicalLen, runCount)) - { - return 0; - } - currentGate = gate; - currentLogicalLen = 1U; + currentLogicalLen = lenTicks; + havePendingRun = true; + return true; + }); + if (!ok) + { + return 0; } - if (havePendingRun && !appendPhysicalRun(currentGate, currentLogicalLen, runCount)) { return 0; } - return runCount; } diff --git a/IR_Encoder.h b/IR_Encoder.h index af097cf..df55d86 100644 --- a/IR_Encoder.h +++ b/IR_Encoder.h @@ -214,6 +214,8 @@ private: static bool txAdvanceBoundary(TxFsmState &st, const uint8_t *sendBufferLocal); static bool txAdvanceAfterOutput(TxFsmState &st, const uint8_t *sendBufferLocal); static bool txEmitTick(TxFsmState &st, const uint8_t *sendBufferLocal, bool &gateOut); + template + static bool txWalkRuns(TxFsmState &st, const uint8_t *sendBufferLocal, Emit emit); void loadTxFsmFromMembers(TxFsmState &st) const; void storeTxFsmToMembers(const TxFsmState &st); bool shouldUseBufferedIsr() const; From 883c0b00cfaf349966a35175d7d7fe5c4fe3fc8f Mon Sep 17 00:00:00 2001 From: DashyFox Date: Mon, 7 Sep 2026 17:56:23 +0300 Subject: [PATCH 21/24] =?UTF-8?q?perf(tx):=20=D0=B7=D0=B0=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20DMA-=D0=B1=D1=83=D1=84?= =?UTF-8?q?=D0=B5=D1=80=D0=B0=20=D0=BF=D0=BE=20=D1=80=D0=B0=D0=BD=D0=B0?= =?UTF-8?q?=D0=BC=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=81=D0=BB=D0=BE=D0=B2=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=B0=D0=B2?= =?UTF-8?q?=D1=82=D0=BE=D0=BC=D0=B0=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IrTxBsrrWave::fill: пауза (gate=0) — один цикл записи resetWord, несущая — копия шаблона периода (powerN слов setWord + остальные resetWord); состояние переносится через границы порций как у nextWord(). На 12 МГц предзаполнение 4096 слов перед стартом DMA и ISR-дозаполнение половин (2048 слов каждые ~8.9 мс кадра — было ~88% CPU) укорачиваются в разы: старт несущей после вызова send 15.7 → ~5 мс (с построением ранов). Эквивалентность проверена на хосте: 17856 случаев (len 1..31, multiply 2..8, powerN 1..multiply/2, порции 2048/случайные/по 1 слову), 0 расхождений на 719 млн слов. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq --- IrTxBsrrWave.h | 56 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/IrTxBsrrWave.h b/IrTxBsrrWave.h index 31115ff..f5178f4 100644 --- a/IrTxBsrrWave.h +++ b/IrTxBsrrWave.h @@ -65,13 +65,63 @@ public: return out; } + // Заполнение по ранам, а не по словам: тот же поток слов, что даёт nextWord() (состояние + // runIndex_/ticksLeftInRun_/slotInPeriod_ переносится через границы порций), но пауза (gate=0) + // пишется одним циклом записи, а несущая — копией готового шаблона периода. На 12 МГц это + // ~1 мс на 4096 слов вместо ~16 (пословный автомат) — и в предзаполнении перед стартом DMA, + // и в ISR-дозаполнении половин буфера во время передачи. IR_TX_BSRR_WAVE_HOT void fill(uint32_t* dst, uint16_t count) { if (dst == nullptr || count == 0) { return; } - do { - *dst++ = nextWord(); - } while (--count != 0); + while (count != 0) { + if (runIndex_ >= runCount) { + do { *dst++ = resetWord; } while (--count != 0); + return; + } + const bool gate = runs[runIndex_].gate; + uint16_t n = ticksLeftInRun_; // слов до конца текущего рана + if (n == 0) n = 1; // ран нулевой длины: nextWord() выдаёт одно слово и переходит дальше + if (n > count) n = count; + if (!gate) { + slotInPeriod_ = 0; + uint16_t k = n; + do { *dst++ = resetWord; } while (--k != 0); + } else { + uint16_t k = n; + // добить текущий период до слота 0 (если ран начался посреди периода на границе порции) + while (k != 0 && slotInPeriod_ != 0) { + *dst++ = (slotInPeriod_ < powerN_) ? setWord : resetWord; + if (++slotInPeriod_ >= multiply_) slotInPeriod_ = 0; + k--; + } + // целые периоды: powerN_ слов setWord, остальные resetWord + while (k >= multiply_) { + uint16_t i = 0; + for (; i < powerN_; ++i) *dst++ = setWord; + for (; i < multiply_; ++i) *dst++ = resetWord; + k = (uint16_t)(k - multiply_); + } + // хвост неполного периода + while (k != 0) { + *dst++ = (slotInPeriod_ < powerN_) ? setWord : resetWord; + if (++slotInPeriod_ >= multiply_) slotInPeriod_ = 0; + k--; + } + } + count = (uint16_t)(count - n); + if (ticksLeftInRun_ > n) { + ticksLeftInRun_ = (uint16_t)(ticksLeftInRun_ - n); + } else { + ticksLeftInRun_ = 0; + } + if (ticksLeftInRun_ == 0) { + runIndex_++; + if (runIndex_ < runCount) { + ticksLeftInRun_ = runs[runIndex_].lenTicks; + } + } + } } private: From b375aa169ebf13920b04e6f8a57e3c8b924b1ab7 Mon Sep 17 00:00:00 2001 From: DashyFox Date: Tue, 8 Sep 2026 14:09:38 +0300 Subject: [PATCH 22/24] =?UTF-8?q?refactor(config):=20IR=5FResponseDelay=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=B5=D1=81=D1=91=D0=BD=20=D0=B2?= =?UTF-8?q?=20IR=5Fconfig.h?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зависит только от конфигурационных констант (bitTime, tolerance, syncBits) — это таймаут байта (irRxByteTimeoutUs) × 2.7735 = 42 мс; значение не изменилось. Потребителям достаточно IR_config.h. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq --- IR_DecoderRaw.h | 1 - IR_config.h | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/IR_DecoderRaw.h b/IR_DecoderRaw.h index 4b12363..4c2d98e 100644 --- a/IR_DecoderRaw.h +++ b/IR_DecoderRaw.h @@ -25,7 +25,6 @@ class Print; #define riseTimeMin (riseTime - riseTolerance) #define aroundRise(t) (riseTimeMin < t && t < riseTimeMax) #define IR_timeout (riseTimeMax * (8 + syncBits + 1)) // us // таймаут в 8 data + 3 sync + 1 -constexpr uint16_t IR_ResponseDelay = ((uint16_t)(((bitTime+riseTolerance) * (8 + syncBits + 1))*2.7735))/1000; class IR_Encoder; class IR_DecoderRaw : virtual public IR_FOX diff --git a/IR_config.h b/IR_config.h index 6997da5..7fca919 100644 --- a/IR_config.h +++ b/IR_config.h @@ -286,6 +286,9 @@ constexpr uint32_t irLockLatencyUs = irTicksToUs(irLockTicks); /// Таймаут байта декодера (как IR_timeout при номинальном bitTime) и тишина, по которой декодер обрывает приём. constexpr uint32_t irRxByteTimeoutUs = ((uint32_t)bitTime + tolerance) * ((uint32_t)bitPerByte + syncBits + 1U); constexpr uint32_t irRxAbortSilenceUs = 2U * irRxByteTimeoutUs; +/// Оборот ответа (КТ отвечает на пинг через это время; машинка — задний после принятого кадра): таймаут байта × +/// 2.7735 = 42 мс. Коэффициент исторический, подобран под 42; зависит только от bitTime/tolerance/syncBits. +constexpr uint16_t IR_ResponseDelay = (uint16_t)((irRxByteTimeoutUs * 2.7735) / 1000); /// Протокольный максимум длины кадра (5-битное поле длины). constexpr uint8_t irMaxPackSize = IR_MASK_MSG_INFO; /// Размер кадра по полезной нагрузке: DATA (from+to) и BACK (только from). From 5b220dadd881c5626763b6dec5996edbf602ad40 Mon Sep 17 00:00:00 2001 From: DashyFox Date: Tue, 8 Sep 2026 14:13:14 +0300 Subject: [PATCH 23/24] =?UTF-8?q?Revert=20"refactor(config):=20IR=5FRespon?= =?UTF-8?q?seDelay=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=B5=D1=81=D1=91?= =?UTF-8?q?=D0=BD=20=D0=B2=20IR=5Fconfig.h"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit b375aa169ebf13920b04e6f8a57e3c8b924b1ab7. --- IR_DecoderRaw.h | 1 + IR_config.h | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/IR_DecoderRaw.h b/IR_DecoderRaw.h index 4c2d98e..4b12363 100644 --- a/IR_DecoderRaw.h +++ b/IR_DecoderRaw.h @@ -25,6 +25,7 @@ class Print; #define riseTimeMin (riseTime - riseTolerance) #define aroundRise(t) (riseTimeMin < t && t < riseTimeMax) #define IR_timeout (riseTimeMax * (8 + syncBits + 1)) // us // таймаут в 8 data + 3 sync + 1 +constexpr uint16_t IR_ResponseDelay = ((uint16_t)(((bitTime+riseTolerance) * (8 + syncBits + 1))*2.7735))/1000; class IR_Encoder; class IR_DecoderRaw : virtual public IR_FOX diff --git a/IR_config.h b/IR_config.h index 7fca919..6997da5 100644 --- a/IR_config.h +++ b/IR_config.h @@ -286,9 +286,6 @@ constexpr uint32_t irLockLatencyUs = irTicksToUs(irLockTicks); /// Таймаут байта декодера (как IR_timeout при номинальном bitTime) и тишина, по которой декодер обрывает приём. constexpr uint32_t irRxByteTimeoutUs = ((uint32_t)bitTime + tolerance) * ((uint32_t)bitPerByte + syncBits + 1U); constexpr uint32_t irRxAbortSilenceUs = 2U * irRxByteTimeoutUs; -/// Оборот ответа (КТ отвечает на пинг через это время; машинка — задний после принятого кадра): таймаут байта × -/// 2.7735 = 42 мс. Коэффициент исторический, подобран под 42; зависит только от bitTime/tolerance/syncBits. -constexpr uint16_t IR_ResponseDelay = (uint16_t)((irRxByteTimeoutUs * 2.7735) / 1000); /// Протокольный максимум длины кадра (5-битное поле длины). constexpr uint8_t irMaxPackSize = IR_MASK_MSG_INFO; /// Размер кадра по полезной нагрузке: DATA (from+to) и BACK (только from). From baac9fbf46433f63acd7e1a5f6aebd38e533c163 Mon Sep 17 00:00:00 2001 From: DashyFox Date: Wed, 9 Sep 2026 16:09:55 +0300 Subject: [PATCH 24/24] =?UTF-8?q?fix(rx):=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5?= =?UTF-8?q?=20=D0=BE=D0=B1=D1=80=D1=8B=D0=B2=D0=B0=20=D0=BA=D0=B0=D0=B4?= =?UTF-8?q?=D1=80=D0=B0=20=D0=BD=D0=B5=20=D1=82=D1=80=D0=B5=D0=B1=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D1=8C=2030=20=D0=BC=D1=81=20=D1=82=D0=B8?= =?UTF-8?q?=D1=88=D0=B8=D0=BD=D1=8B=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=20?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D1=8B=D0=BC=20=D0=BA=D0=B0=D0=BD=D0=B4=D0=B8?= =?UTF-8?q?=D0=B4=D0=B0=D1=82=D0=BE=D0=BC=20=D0=BF=D1=80=D0=B5=D0=B0=D0=BC?= =?UTF-8?q?=D0=B1=D1=83=D0=BB=D1=8B=20(releasePreambleGuard)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit prevRise (последний декодированный фронт) не сбрасывался при abortFrame, а отброшенные гвардом фронты его не двигают — валидный кадр, начавшийся через <30 мс после обрыва мусора, проглатывался целиком без счётчика. Стенд 09.09: КУ теряла пинг машинки после обрывков чужого заднего и всплеска её дальномера за 24 мс до пинга (V-серия 75–83 %, после правки 100 %). Хост-тест (настоящий IR_DecoderRaw.cpp + шим Arduino): после abort кадр через 5…28 мс — терялся, теперь принимается; чистый конец кадра не тронут (мин. пауза 26.25 мс), хвост оборванного кадра ложных захватов не даёт, 200/200 валидных среди мусора, стресс 166→176/200. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq --- IR_DecoderRaw.cpp | 13 +++++++++++++ IR_DecoderRaw.h | 1 + 2 files changed, 14 insertions(+) diff --git a/IR_DecoderRaw.cpp b/IR_DecoderRaw.cpp index 7359dc8..5892546 100644 --- a/IR_DecoderRaw.cpp +++ b/IR_DecoderRaw.cpp @@ -440,6 +440,19 @@ void IR_DecoderRaw::abortFrame(uint32_t tUs) isReciveRaw = false; msgTypeReceive = 0; firstRX(); + releasePreambleGuard(tUs); +} + +// После обрыва кадра «длинная тишина» (IR_timeout × 2 ≈ 30 мс) перед новым кандидатом преамбулы не требуется. +// prevRise — последний ДЕКОДИРОВАННЫЙ фронт; после abort он свежий, а фронты, отброшенные гвардом, его не двигают, +// поэтому валидный кадр, начавшийся через <30 мс после обрыва мусора, проглатывался целиком без счётчика +// (стенд 09.09: КУ теряла пинг машинки после обрывков чужого заднего и всплеска её дальномера за 24 мс до пинга). +// Ложных захватов это не добавляет: хвост оборванного кадра (период фронтов 962 мкс, синхробиты ~1100) не проходит +// грубый фильтр периода преамбулы (2116…3270 мкс), а настоящая преамбула перезапускает кандидата по паузе > IR_timeout. +// После чистого конца кадра гвард остаётся: там он отсекает хвост синхробитов. +void IR_DecoderRaw::releasePreambleGuard(uint32_t tUs) +{ + prevRise = tUs - IR_timeout * 2U - 1U; // «тишина уже была»: (front.time - prevRise) > longSilence для следующего фронта } void IR_DecoderRaw::expirePreambleCandidate() diff --git a/IR_DecoderRaw.h b/IR_DecoderRaw.h index 4b12363..b3309e8 100644 --- a/IR_DecoderRaw.h +++ b/IR_DecoderRaw.h @@ -186,6 +186,7 @@ private: RxEndInfo rxEnd; void noteRxEnd(RxEndReason reason, uint32_t tUs); // терминал: фиксирует тип/длину/расчётный конец void abortFrame(uint32_t tUs); // немедленный сброс битого кадра (sync/длина/overflow) + void releasePreambleGuard(uint32_t tUs); // после abort: новый кандидат преамбулы без ожидания длинной тишины void expirePreambleCandidate(); // кандидат без фронтов дольше таймаута → Idle uint8_t preambleGoodPeriods = 0; uint16_t preambleMeanPeriod = 0;