mirror of
https://github.com/Show-maket/IR-protocol.git
synced 2026-09-21 20:39:35 +00:00
Compare commits
110 Commits
archive/de
...
STM32
| Author | SHA1 | Date | |
|---|---|---|---|
| 5759658d42 | |||
| 1b408b0de6 | |||
| baac9fbf46 | |||
| 5b220dadd8 | |||
| b375aa169e | |||
| 883c0b00cf | |||
| d103d2a3ae | |||
| 86956bcf99 | |||
| 5b9f73ec7c | |||
| 8f45f6e214 | |||
| 6c97d33c7c | |||
| 628c050702 | |||
| a589416cfc | |||
| e25feb6824 | |||
| 96ffb91b97 | |||
| 08ea71eb5e | |||
| 5bcaf4092a | |||
| 873277e59f | |||
| f93f4101c3 | |||
| bf14b3d7ad | |||
| a8d32bbcef | |||
| fc3510c425 | |||
| e5ed029cfd | |||
| 55935b8b92 | |||
| 7176fe5321 | |||
| b1d7016147 | |||
| 9935ac192a | |||
| 8361828c44 | |||
| 38d93edd1b | |||
| 2f5b57680b | |||
| 31ac7a3625 | |||
| b4f644c258 | |||
| a75fd0c564 | |||
| 4061a4ed10 | |||
| 01a34ed3f7 | |||
| d788358ac8 | |||
| 8daff9c46a | |||
| 8631f23b53 | |||
| ad1e16cfda | |||
| 57f79b35c7 | |||
| f8daa68381 | |||
| ddb8a9e143 | |||
| c29fe2cf7c | |||
| 7651f07e0a | |||
| e7d7c0e1c1 | |||
| af3e012aac | |||
| fc93ea41db | |||
| 0620d98e35 | |||
| 4caed06218 | |||
| fc1a3bacef | |||
| 8a0d7f8dba | |||
| d1c84ba18a | |||
| e9c568aed2 | |||
| 7bf71d1d52 | |||
| 38f3ecac3a | |||
| dec8467280 | |||
| bc9563fbb5 | |||
| 021e1e290d | |||
| 89d14919c9 | |||
| 403b8e6850 | |||
| d0c3138c52 | |||
| 2d839d3ff8 | |||
| 6ba8fdffe4 | |||
| aa0b478229 | |||
| 277985f79a | |||
| 444b84c313 | |||
| 2db1ef7805 | |||
| 1353ab6f75 | |||
| d1cb167aaf | |||
| 30ad816c2a | |||
| ecfb3b5f98 | |||
| 70a22463ef | |||
| 71f58a4992 | |||
| b6b9d2c820 | |||
| 98a21f5e56 | |||
| 591727546e | |||
| 79bb804bb4 | |||
| 0471b8cc89 | |||
| 90c41cfe2b | |||
| 1ecc33e9c4 | |||
| 7ef8158a00 | |||
| 37522f974f | |||
| 341ff3a470 | |||
| e6dbdcee74 | |||
| cf5a6641f4 | |||
| da152c65ee | |||
| 8f77c60cba | |||
| 6375c4eed5 | |||
| fd51a4935c | |||
| aa862d8f2c | |||
| 7c9529d42f | |||
| d4dd0e95fd | |||
| 04af094f4b | |||
| 2f4ac3ddf8 | |||
| 784365181e | |||
| c4000d6b75 | |||
| c66d47e464 | |||
| 3057e78aeb | |||
| a958c1d3b2 | |||
| 2147bf0788 | |||
| e37d4d79f1 | |||
| 373cd43b73 | |||
| 96f0ac623e | |||
| d46640b145 | |||
| e752d0fb50 | |||
| 5a5142e0aa | |||
| e951111c53 | |||
| 06d27f2590 | |||
| fc02c79135 | |||
| d068a576f7 |
10
.gitignore
vendored
10
.gitignore
vendored
@ -1,5 +1,13 @@
|
|||||||
.vscode/*
|
.vscode/*
|
||||||
bin/*
|
bin/*
|
||||||
!.vscode/arduino.json
|
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
log/*
|
log/*
|
||||||
|
/.vscode
|
||||||
|
*.zip
|
||||||
|
**/__pycache__
|
||||||
|
Analyzer/raw/dll/*.dll
|
||||||
|
Analyzer/raw/dll/*.so
|
||||||
|
Analyzer/raw/dll/*.dylib
|
||||||
|
/Analyzer/raw/IR_Fox/.github
|
||||||
|
**/.build
|
||||||
|
graphify-out/*
|
||||||
|
|||||||
7
.vscode/arduino.json
vendored
7
.vscode/arduino.json
vendored
@ -1,8 +1,5 @@
|
|||||||
{
|
{
|
||||||
"configuration": "pnum=BLUEPILL_F103C8,upload_method=swdMethod,xserial=none,usb=CDCgen,xusb=FS,opt=osstd,dbg=none,rtlib=nano",
|
"board": "STMicroelectronics:stm32:GenG4",
|
||||||
"board": "STMicroelectronics:stm32:GenF1",
|
|
||||||
"port": "COM17",
|
"port": "COM17",
|
||||||
"output": "bin",
|
"prebuild": "if exist bin rd /s /q bin"
|
||||||
"prebuild": "if exist bin rd /s /q bin",
|
|
||||||
"sketch": "IR-protocol.ino"
|
|
||||||
}
|
}
|
||||||
52
Analyzer/raw/IR_Fox/.github/workflows/build.yml
vendored
Normal file
52
Analyzer/raw/IR_Fox/.github/workflows/build.yml
vendored
Normal file
@ -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/IR_Fox/build -S ${{github.workspace}}/Analyzer/raw/IR_Fox -A x64
|
||||||
|
cmake --build ${{github.workspace}}/Analyzer/raw/IR_Fox/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/IR_Fox/build -S ${{github.workspace}}/Analyzer/raw/IR_Fox -DCMAKE_BUILD_TYPE=Release
|
||||||
|
cmake --build ${{github.workspace}}/Analyzer/raw/IR_Fox/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/IR_Fox/build -S ${{github.workspace}}/Analyzer/raw/IR_Fox -DCMAKE_BUILD_TYPE=Release
|
||||||
|
cmake --build ${{github.workspace}}/Analyzer/raw/IR_Fox/build
|
||||||
|
env:
|
||||||
|
CC: gcc-10
|
||||||
|
CXX: g++-10
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: linux
|
||||||
|
path: ${{github.workspace}}/Analyzer/raw/dll/*.so
|
||||||
2
Analyzer/raw/IR_Fox/.gitignore
vendored
Normal file
2
Analyzer/raw/IR_Fox/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/build
|
||||||
|
.DS_Store
|
||||||
26
Analyzer/raw/IR_Fox/CMakeLists.txt
Normal file
26
Analyzer/raw/IR_Fox/CMakeLists.txt
Normal file
@ -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})
|
||||||
49
Analyzer/raw/IR_Fox/CMakePresets.json
Normal file
49
Analyzer/raw/IR_Fox/CMakePresets.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
21
Analyzer/raw/IR_Fox/build_msvc.bat
Normal file
21
Analyzer/raw/IR_Fox/build_msvc.bat
Normal file
@ -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%
|
||||||
66
Analyzer/raw/IR_Fox/cmake/ExternalAnalyzerSDK.cmake
Normal file
66
Analyzer/raw/IR_Fox/cmake/ExternalAnalyzerSDK.cmake
Normal file
@ -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()
|
||||||
39
Analyzer/raw/IR_Fox/configure_msvc.bat
Normal file
39
Analyzer/raw/IR_Fox/configure_msvc.bat
Normal file
@ -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
|
||||||
280
Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp
Normal file
280
Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
#include "IrFoxAnalyzer.h"
|
||||||
|
#include "IrFoxAnalyzerSettings.h"
|
||||||
|
#include "IrFoxDecoder.h"
|
||||||
|
#include <AnalyzerChannelData.h>
|
||||||
|
#include <AnalyzerResults.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<U64>(1ULL, static_cast<U64>((static_cast<double>(irfox::kMinFilteredPulseUs) * 1e-6) * static_cast<double>(fs) + 0.5));
|
||||||
|
struct RawEdge
|
||||||
|
{
|
||||||
|
U64 sample;
|
||||||
|
bool rising;
|
||||||
|
};
|
||||||
|
std::vector<RawEdge> 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<std::ptrdiff_t>(i),
|
||||||
|
pending.begin() + static_cast<std::ptrdiff_t>(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<S64>(e.start_sample);
|
||||||
|
frame.mEndingSampleInclusive = static_cast<S64>(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<S64>(p.start_sample);
|
||||||
|
frame.mEndingSampleInclusive = static_cast<S64>(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<S64>(p.pack_size));
|
||||||
|
fv2.AddInteger("err_low", static_cast<S64>(p.err_low));
|
||||||
|
fv2.AddInteger("err_high", static_cast<S64>(p.err_high));
|
||||||
|
fv2.AddInteger("err_other", static_cast<S64>(p.err_other));
|
||||||
|
fv2.AddByteArray("data", p.data_bytes, p.pack_size);
|
||||||
|
mResults->AddFrameV2(fv2, p.crc_ok ? "packet_ok" : "packet_bad", static_cast<U64>(p.start_sample),
|
||||||
|
static_cast<U64>(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;
|
||||||
|
}
|
||||||
49
Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.h
Normal file
49
Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.h
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#ifndef IRFOX_ANALYZER_H
|
||||||
|
#define IRFOX_ANALYZER_H
|
||||||
|
|
||||||
|
#include <Analyzer.h>
|
||||||
|
#include "IrFoxAnalyzerSettings.h"
|
||||||
|
#include "IrFoxAnalyzerResults.h"
|
||||||
|
#include "IrFoxSimulationDataGenerator.h"
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
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<IrFoxAnalyzerResults> mResults;
|
||||||
|
AnalyzerChannelData* mIr;
|
||||||
|
|
||||||
|
IrFoxSimulationDataGenerator mSimulationDataGenerator;
|
||||||
|
bool mSimulationInitilized;
|
||||||
|
|
||||||
|
std::unordered_map<U64, std::string> m_packet_hex_by_frame;
|
||||||
|
std::unordered_map<U64, std::string> 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
|
||||||
175
Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp
Normal file
175
Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
#include "IrFoxAnalyzerResults.h"
|
||||||
|
#include <AnalyzerHelpers.h>
|
||||||
|
#include <AnalyzerResults.h>
|
||||||
|
#include "IrFoxAnalyzer.h"
|
||||||
|
#include "IrFoxAnalyzerSettings.h"
|
||||||
|
#include "IrFoxDecoder.h"
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
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<unsigned>(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<U32>((frame.mData2 >> 16) & 0xFFull);
|
||||||
|
err_h = static_cast<U32>((frame.mData2 >> 24) & 0xFFull);
|
||||||
|
err_o = static_cast<U32>((frame.mData2 >> 32) & 0xFFull);
|
||||||
|
}
|
||||||
|
|
||||||
|
file_stream << time_str << "," << typ << "," << frame.mData1 << "," << bit_idx << "," << err_l << "," << err_h
|
||||||
|
<< "," << err_o << "," << static_cast<unsigned>(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<unsigned>(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;
|
||||||
|
}
|
||||||
27
Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.h
Normal file
27
Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.h
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#ifndef IRFOX_ANALYZER_RESULTS
|
||||||
|
#define IRFOX_ANALYZER_RESULTS
|
||||||
|
|
||||||
|
#include <AnalyzerResults.h>
|
||||||
|
|
||||||
|
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
|
||||||
62
Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.cpp
Normal file
62
Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.cpp
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
#include "IrFoxAnalyzerSettings.h"
|
||||||
|
#include <AnalyzerHelpers.h>
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
24
Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.h
Normal file
24
Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.h
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#ifndef IRFOX_ANALYZER_SETTINGS
|
||||||
|
#define IRFOX_ANALYZER_SETTINGS
|
||||||
|
|
||||||
|
#include <AnalyzerSettings.h>
|
||||||
|
#include <AnalyzerTypes.h>
|
||||||
|
|
||||||
|
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
|
||||||
593
Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp
Normal file
593
Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp
Normal file
@ -0,0 +1,593 @@
|
|||||||
|
#include "IrFoxDecoder.h"
|
||||||
|
#include <AnalyzerResults.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
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<uint16_t>(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<uint8_t>((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<uint16_t>(static_cast<uint16_t>(crc8(data_buffer, 0, len, irfox::kPoly1) << 8) & 0xFF00u);
|
||||||
|
crc_out = static_cast<uint16_t>(crc_out | (crc8(data_buffer, 0, static_cast<uint8_t>(len + 1), irfox::kPoly2) & 0xFFu));
|
||||||
|
|
||||||
|
const bool ok = (data_buffer[len] == static_cast<uint8_t>((crc_out >> 8) & 0xFF)) &&
|
||||||
|
(data_buffer[len + 1] == static_cast<uint8_t>(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<int64_t>(cell_start_s);
|
||||||
|
e.end_sample = static_cast<int64_t>(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<uint8_t>(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<int64_t>(cell_start_s), static_cast<int64_t>(cell_end_s), IRF_FT_DATA_BIT,
|
||||||
|
bit ? 1u : 0u, static_cast<uint64_t>(i_data_buffer - 1), fl, pack_trace_invert_fix, 0, 0, 0};
|
||||||
|
fill_err_snapshot(e);
|
||||||
|
e.bubble_text[0] = static_cast<char>(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<bool>(last_data_bit))
|
||||||
|
{
|
||||||
|
buf_bit_pos++;
|
||||||
|
i_sync_bit++;
|
||||||
|
if (on_bit && emit_mode == IrFoxEmitBitMode::WithBubble)
|
||||||
|
{
|
||||||
|
IrFoxEmitBit e{static_cast<int64_t>(cell_start_s), static_cast<int64_t>(cell_end_s), IRF_FT_SYNC_BIT,
|
||||||
|
bit ? 1u : 0u, static_cast<uint64_t>(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<int64_t>(cell_start_s), static_cast<int64_t>(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<int64_t>(cell_start_s), static_cast<int64_t>(cell_end_s), IRF_FT_SYNC_BIT,
|
||||||
|
bit ? 1u : 0u, static_cast<uint64_t>(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<uint16_t>(data_buffer[0] & 0x1Fu);
|
||||||
|
|
||||||
|
if (pack_size && (i_data_buffer == 8))
|
||||||
|
msg_type_receive = static_cast<uint8_t>((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<uint8_t>(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<int64_t>(cell_start_s);
|
||||||
|
pkt.end_sample = static_cast<int64_t>(cell_end_s);
|
||||||
|
pkt.crc_ok = crc_ok;
|
||||||
|
pkt.pack_size = static_cast<uint8_t>(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<uint32_t>(delta_rp);
|
||||||
|
const uint32_t cand_ht = static_cast<uint32_t>(t_us - prev_fall_us);
|
||||||
|
const uint32_t cand_lt = static_cast<uint32_t>(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<double>(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<int8_t>(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<double>(rise_period_us);
|
||||||
|
const uint64_t half_s = static_cast<uint64_t>(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<int64_t>(preamble_bubble_start_sample_);
|
||||||
|
int64_t pe_end = static_cast<int64_t>(preamble_bubble_end_sample);
|
||||||
|
if (preamble_bubble_end_sample == 0 || pe_end < pe_start)
|
||||||
|
pe_end = static_cast<int64_t>(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<uint16_t>(high_time_us > 0xFFFF ? 0xFFFF : high_time_us),
|
||||||
|
static_cast<uint16_t>(rise_sync_time_us));
|
||||||
|
uint16_t lc = ceil_div_u16(static_cast<uint16_t>(low_time_us > 0xFFFF ? 0xFFFF : low_time_us),
|
||||||
|
static_cast<uint16_t>(rise_sync_time_us));
|
||||||
|
uint16_t ac = ceil_div_u16(static_cast<uint16_t>(rise_period_us > 0xFFFF ? 0xFFFF : rise_period_us),
|
||||||
|
static_cast<uint16_t>(rise_sync_time_us));
|
||||||
|
high_count = static_cast<int8_t>(hc > 127 ? 127 : hc);
|
||||||
|
low_count = static_cast<int8_t>(lc > 127 ? 127 : lc);
|
||||||
|
all_count = static_cast<int8_t>(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<int8_t>(all_count - high_count);
|
||||||
|
err_low_signal = static_cast<uint8_t>(err_low_signal + static_cast<uint8_t>(low_count));
|
||||||
|
}
|
||||||
|
else if (low_count < high_count)
|
||||||
|
{
|
||||||
|
high_count = static_cast<int8_t>(all_count - low_count);
|
||||||
|
err_high_signal = static_cast<uint8_t>(err_high_signal + static_cast<uint8_t>(high_count));
|
||||||
|
}
|
||||||
|
else if (low_count == high_count)
|
||||||
|
{
|
||||||
|
invert_err = true;
|
||||||
|
err_other = static_cast<uint8_t>(err_other + static_cast<uint8_t>(all_count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (low_count < high_count)
|
||||||
|
err_high_signal = static_cast<uint8_t>(err_high_signal + static_cast<uint8_t>(high_count));
|
||||||
|
else
|
||||||
|
err_low_signal = static_cast<uint8_t>(err_low_signal + static_cast<uint8_t>(low_count));
|
||||||
|
|
||||||
|
const bool burst_is_data_start = is_data;
|
||||||
|
const uint64_t merge_bit_index =
|
||||||
|
burst_is_data_start ? static_cast<uint64_t>(i_data_buffer) : static_cast<uint64_t>(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<char>(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<int64_t>(cell_start_s);
|
||||||
|
e.end_sample = static_cast<int64_t>(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;
|
||||||
|
}
|
||||||
135
Analyzer/raw/IR_Fox/src/IrFoxDecoder.h
Normal file
135
Analyzer/raw/IR_Fox/src/IrFoxDecoder.h
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IrFoxProtocolConstants.h"
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
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<void(const IrFoxEmitBit&)>;
|
||||||
|
using IrFoxOnPacket = std::function<void(const IrFoxEmitPacket&)>;
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
71
Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h
Normal file
71
Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
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<uint8_t>(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<double>(rise_sync_us)))
|
||||||
|
return;
|
||||||
|
const double nudged = edge_us - static_cast<double>(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
|
||||||
67
Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.cpp
Normal file
67
Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.cpp
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
#include "IrFoxSimulationDataGenerator.h"
|
||||||
|
#include "IrFoxAnalyzerSettings.h"
|
||||||
|
#include <AnalyzerHelpers.h>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
27
Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.h
Normal file
27
Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.h
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#ifndef IRFOX_SIMULATION_DATA_GENERATOR
|
||||||
|
#define IRFOX_SIMULATION_DATA_GENERATOR
|
||||||
|
|
||||||
|
#include <SimulationChannelDescriptor.h>
|
||||||
|
|
||||||
|
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
|
||||||
52
Analyzer/raw/PulseLengthStat/.github/workflows/build.yml
vendored
Normal file
52
Analyzer/raw/PulseLengthStat/.github/workflows/build.yml
vendored
Normal file
@ -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
|
||||||
3
Analyzer/raw/PulseLengthStat/.gitignore
vendored
Normal file
3
Analyzer/raw/PulseLengthStat/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/build
|
||||||
|
/build-nmake
|
||||||
|
.DS_Store
|
||||||
24
Analyzer/raw/PulseLengthStat/CMakeLists.txt
Normal file
24
Analyzer/raw/PulseLengthStat/CMakeLists.txt
Normal file
@ -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})
|
||||||
49
Analyzer/raw/PulseLengthStat/CMakePresets.json
Normal file
49
Analyzer/raw/PulseLengthStat/CMakePresets.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
21
Analyzer/raw/PulseLengthStat/build_msvc.bat
Normal file
21
Analyzer/raw/PulseLengthStat/build_msvc.bat
Normal file
@ -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%
|
||||||
66
Analyzer/raw/PulseLengthStat/cmake/ExternalAnalyzerSDK.cmake
Normal file
66
Analyzer/raw/PulseLengthStat/cmake/ExternalAnalyzerSDK.cmake
Normal file
@ -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()
|
||||||
39
Analyzer/raw/PulseLengthStat/configure_msvc.bat
Normal file
39
Analyzer/raw/PulseLengthStat/configure_msvc.bat
Normal file
@ -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
|
||||||
110
Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.cpp
Normal file
110
Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.cpp
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
#include "PulseLengthStatAnalyzer.h"
|
||||||
|
#include "PulseLengthStatAnalyzerSettings.h"
|
||||||
|
#include <AnalyzerChannelData.h>
|
||||||
|
|
||||||
|
// 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<S64>(segment_start);
|
||||||
|
frame.mEndingSampleInclusive = static_cast<S64>(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;
|
||||||
|
}
|
||||||
39
Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.h
Normal file
39
Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.h
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#ifndef PULSELENGTHSTAT_ANALYZER_H
|
||||||
|
#define PULSELENGTHSTAT_ANALYZER_H
|
||||||
|
|
||||||
|
#include <Analyzer.h>
|
||||||
|
#include "PulseLengthStatAnalyzerSettings.h"
|
||||||
|
#include "PulseLengthStatAnalyzerResults.h"
|
||||||
|
#include "PulseLengthStatSimulationDataGenerator.h"
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
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<PulseLengthStatAnalyzerResults> 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
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
#include "PulseLengthStatAnalyzerResults.h"
|
||||||
|
#include <AnalyzerHelpers.h>
|
||||||
|
#include "PulseLengthStatAnalyzer.h"
|
||||||
|
#include "PulseLengthStatAnalyzerSettings.h"
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
#ifndef PULSELENGTHSTAT_ANALYZER_RESULTS
|
||||||
|
#define PULSELENGTHSTAT_ANALYZER_RESULTS
|
||||||
|
|
||||||
|
#include <AnalyzerResults.h>
|
||||||
|
|
||||||
|
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
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
#include "PulseLengthStatAnalyzerSettings.h"
|
||||||
|
#include <AnalyzerHelpers.h>
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
#ifndef PULSELENGTHSTAT_ANALYZER_SETTINGS
|
||||||
|
#define PULSELENGTHSTAT_ANALYZER_SETTINGS
|
||||||
|
|
||||||
|
#include <AnalyzerSettings.h>
|
||||||
|
#include <AnalyzerTypes.h>
|
||||||
|
|
||||||
|
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
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
#include "PulseLengthStatSimulationDataGenerator.h"
|
||||||
|
#include "PulseLengthStatAnalyzerSettings.h"
|
||||||
|
#include <AnalyzerHelpers.h>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
#ifndef PULSELENGTHSTAT_SIMULATION_DATA_GENERATOR
|
||||||
|
#define PULSELENGTHSTAT_SIMULATION_DATA_GENERATOR
|
||||||
|
|
||||||
|
#include <SimulationChannelDescriptor.h>
|
||||||
|
|
||||||
|
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
|
||||||
BIN
Analyzer/raw/Session 0.sal
Normal file
BIN
Analyzer/raw/Session 0.sal
Normal file
Binary file not shown.
BIN
Analyzer/raw/Session 2.sal
Normal file
BIN
Analyzer/raw/Session 2.sal
Normal file
Binary file not shown.
BIN
Analyzer/raw/Session 3.sal
Normal file
BIN
Analyzer/raw/Session 3.sal
Normal file
Binary file not shown.
BIN
Analyzer/raw/Session Sync anchor.sal
Normal file
BIN
Analyzer/raw/Session Sync anchor.sal
Normal file
Binary file not shown.
25175
Analyzer/raw/car.txt
Normal file
25175
Analyzer/raw/car.txt
Normal file
File diff suppressed because it is too large
Load Diff
0
Analyzer/raw/dll/.gitkeep
Normal file
0
Analyzer/raw/dll/.gitkeep
Normal file
18243
Analyzer/raw/point.txt
Normal file
18243
Analyzer/raw/point.txt
Normal file
File diff suppressed because it is too large
Load Diff
32791
Analyzer/raw/rx_car.txt
Normal file
32791
Analyzer/raw/rx_car.txt
Normal file
File diff suppressed because it is too large
Load Diff
BIN
Analyzer/raw/Неорректная преамбула.sal
Normal file
BIN
Analyzer/raw/Неорректная преамбула.sal
Normal file
Binary file not shown.
565
IR-protocol.ino
565
IR-protocol.ino
@ -4,25 +4,24 @@
|
|||||||
#include "MemoryCheck.h"
|
#include "MemoryCheck.h"
|
||||||
/////////////// Pinout ///////////////
|
/////////////// Pinout ///////////////
|
||||||
|
|
||||||
#define dec0_PIN PB15
|
#define dec0_PIN PIN_KT1_IN
|
||||||
#define dec1_PIN PB1
|
#define dec1_PIN PIN_KT2_IN
|
||||||
#define dec2_PIN PB2
|
#define dec2_PIN PIN_KT3_IN
|
||||||
#define dec3_PIN PB3
|
#define dec3_PIN PIN_KT4_IN
|
||||||
#define dec4_PIN PB4
|
#define dec4_PIN PIN_KT5_IN
|
||||||
#define dec5_PIN PB5
|
#define dec5_PIN PIN_KT6_IN
|
||||||
#define dec6_PIN PB6
|
#define dec6_PIN PIN_KT7_IN
|
||||||
#define dec7_PIN PB7
|
#define dec7_PIN PIN_KT8_IN
|
||||||
#define dec8_PIN PB8
|
// #define dec8_PIN PB8
|
||||||
#define dec9_PIN PB9
|
// #define dec9_PIN PB9
|
||||||
#define dec10_PIN PB10
|
// #define dec10_PIN PB10
|
||||||
#define dec11_PIN PB11
|
// #define dec11_PIN PB11
|
||||||
#define dec12_PIN PB12
|
// #define dec12_PIN PB12
|
||||||
#define dec13_PIN PB13
|
// #define dec13_PIN PB13
|
||||||
#define dec14_PIN PB14
|
// #define dec14_PIN PB14
|
||||||
#define dec15_PIN PB15
|
// #define dec15_PIN PB15
|
||||||
|
|
||||||
// #define LoopOut PA7
|
|
||||||
|
|
||||||
|
#define LoopOut PC13
|
||||||
|
|
||||||
//////////////// Ini /////////////////
|
//////////////// Ini /////////////////
|
||||||
|
|
||||||
@ -30,72 +29,544 @@
|
|||||||
#define SERIAL_SPEED 115200
|
#define SERIAL_SPEED 115200
|
||||||
|
|
||||||
//////////////// Var /////////////////
|
//////////////// Var /////////////////
|
||||||
|
// IR_Encoder encForward(PA5, 42 /* , &decBackward */);
|
||||||
|
|
||||||
IR_Decoder dec0(dec1_PIN, 0);
|
IR_Encoder enc0(PIN_KT8_OUT, 42 /* , &decBackward */);
|
||||||
// IR_Decoder dec1(dec2_PIN, 1);
|
// IR_Encoder enc1(PA1, 127 /* , &decBackward */);
|
||||||
|
// IR_Encoder enc2(PA2, 137 /* , &decBackward */);
|
||||||
|
// IR_Encoder enc3(PA3, 777 /* , &decBackward */);
|
||||||
|
// IR_Encoder enc10(PA4, 555 /* , &decBackward */);
|
||||||
|
// IR_Encoder enc11(PC14, 127 /* , &decBackward */);
|
||||||
|
// IR_Encoder enc12(PC13, 137 /* , &decBackward */);
|
||||||
|
// IR_Encoder enc13(PA12, 777 /* , &decBackward */);
|
||||||
|
|
||||||
|
// IR_Encoder encTree(325, A2);
|
||||||
|
|
||||||
//////////////////////// Функции прерываний ////////////////////////
|
//////////////////////// Функции прерываний ////////////////////////
|
||||||
|
|
||||||
//------------------------------------------------------------------
|
void EncoderISR()
|
||||||
// #define dec_ISR(n) void dec_##n##_ISR() { dec##n.isr(); }
|
{
|
||||||
// #define decPinMode(n) pinMode(dec##n##_PIN, INPUT_PULLUP);
|
IR_Encoder::isr();
|
||||||
// #define decAttach(n) attachInterrupt(dec##n##_PIN, dec_##n##_ISR, CHANGE);
|
|
||||||
// #define decSetup(n) decPinMode(n); decAttach(n);
|
|
||||||
// #define decTick(n) dec##n.tick();
|
|
||||||
// #define decStat(n) rx_flag |= statusSimple(dec##n);
|
|
||||||
|
|
||||||
// dec_ISR(0);
|
|
||||||
// dec_ISR(1);
|
|
||||||
|
|
||||||
void dec_0_ISR() {
|
|
||||||
dec0.isr();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------
|
||||||
|
|
||||||
|
IR_Decoder dec0(dec0_PIN, 0);
|
||||||
|
void dec_0_ISR() { dec0.isr(); }
|
||||||
|
|
||||||
|
IR_Decoder dec1(dec1_PIN, 1);
|
||||||
|
void dec_1_ISR() { dec1.isr(); }
|
||||||
|
|
||||||
|
IR_Decoder dec2(dec2_PIN, 2);
|
||||||
|
void dec_2_ISR() { dec2.isr(); }
|
||||||
|
|
||||||
|
IR_Decoder dec3(dec3_PIN, 3);
|
||||||
|
void dec_3_ISR() { dec3.isr(); }
|
||||||
|
|
||||||
|
IR_Decoder dec4(dec4_PIN, 4);
|
||||||
|
void dec_4_ISR() { dec4.isr(); }
|
||||||
|
|
||||||
|
IR_Decoder dec5(dec5_PIN, 5);
|
||||||
|
void dec_5_ISR() { dec5.isr(); }
|
||||||
|
|
||||||
|
IR_Decoder dec6(dec6_PIN, 6);
|
||||||
|
void dec_6_ISR() { dec6.isr(); }
|
||||||
|
|
||||||
|
IR_Decoder dec7(dec7_PIN, 7);
|
||||||
|
void dec_7_ISR() { dec7.isr(); }
|
||||||
|
|
||||||
|
// IR_Decoder dec8(dec8_PIN, 8);
|
||||||
|
// void dec_8_ISR() { dec8.isr(); }
|
||||||
|
|
||||||
|
// IR_Decoder dec9(dec9_PIN, 9);
|
||||||
|
// void dec_9_ISR() { dec9.isr(); }
|
||||||
|
|
||||||
|
// IR_Decoder dec10(dec10_PIN, 10);
|
||||||
|
// void dec_10_ISR() { dec10.isr(); }
|
||||||
|
|
||||||
|
// IR_Decoder dec11(dec11_PIN, 11);
|
||||||
|
// void dec_11_ISR() { dec11.isr(); }
|
||||||
|
|
||||||
|
// IR_Decoder dec12(dec12_PIN, 12);
|
||||||
|
// void dec_12_ISR() { dec12.isr(); }
|
||||||
|
|
||||||
|
// IR_Decoder dec13(dec13_PIN, 13);
|
||||||
|
// void dec_13_ISR() { dec13.isr(); }
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
uint8_t data0[] = {};
|
||||||
|
uint8_t data1[] = {42};
|
||||||
|
uint8_t data2[] = {42, 127};
|
||||||
|
uint8_t data3[] = {42, 127, 137};
|
||||||
|
uint8_t data4[] = {42, 127, 137, 255};
|
||||||
|
|
||||||
Timer t1(50, millis, []()
|
uint32_t loopTimer;
|
||||||
{ digitalToggle(LED_BUILTIN); });
|
uint8_t sig = 0;
|
||||||
|
uint16_t targetAddr = IR_Broadcast;
|
||||||
|
|
||||||
|
Timer t1(500, millis, []()
|
||||||
|
{
|
||||||
|
// Serial.println( digitalPinToBitMask(enc0.getPin()), BIN);
|
||||||
|
// enc0.sendData(IR_Broadcast, data4, sizeof(data4));
|
||||||
|
// enc1.sendData(IR_Broadcast, data3, sizeof(data3));
|
||||||
|
// enc2.sendData(IR_Broadcast, data2, sizeof(data2));
|
||||||
|
// enc3.sendData(IR_Broadcast, data1, sizeof(data1));
|
||||||
|
// enc10.sendData(IR_Broadcast, data4, sizeof(data4));
|
||||||
|
// enc11.sendData(IR_Broadcast, data3, sizeof(data3));
|
||||||
|
// enc12.sendData(IR_Broadcast, data2, sizeof(data2));
|
||||||
|
// enc13.sendData(IR_Broadcast, data1, sizeof(data1));
|
||||||
|
|
||||||
|
// Serial.println(sig);
|
||||||
|
|
||||||
|
// switch (sig)
|
||||||
|
// {
|
||||||
|
// case 0:
|
||||||
|
// encForward.sendData(targetAddr);
|
||||||
|
// break;
|
||||||
|
// case 1:
|
||||||
|
// encForward.sendData(targetAddr, data1, sizeof(data1));
|
||||||
|
// break;
|
||||||
|
// case 2:
|
||||||
|
// encForward.sendData(targetAddr, data2, sizeof(data2));
|
||||||
|
// break;
|
||||||
|
// case 3:
|
||||||
|
// encForward.sendData(targetAddr, data3, sizeof(data3));
|
||||||
|
// break;
|
||||||
|
// case 4:
|
||||||
|
// encForward.sendData(targetAddr, data4, sizeof(data4));
|
||||||
|
// break;
|
||||||
|
|
||||||
|
// case 10:
|
||||||
|
// encForward.sendData(targetAddr, data0, sizeof(data0), true);
|
||||||
|
// break;
|
||||||
|
// case 11:
|
||||||
|
// encForward.sendData(targetAddr, data1, sizeof(data1), true);
|
||||||
|
// break;
|
||||||
|
// case 12:
|
||||||
|
// encForward.sendData(targetAddr, data2, sizeof(data2), true);
|
||||||
|
// break;
|
||||||
|
// case 13:
|
||||||
|
// encForward.sendData(targetAddr, data3, sizeof(data3), true);
|
||||||
|
// break;
|
||||||
|
// case 14:
|
||||||
|
// encForward.sendData(targetAddr, data4, sizeof(data4), true);
|
||||||
|
// break;
|
||||||
|
|
||||||
|
// case 20:
|
||||||
|
// encForward.sendBack();
|
||||||
|
// break;
|
||||||
|
// case 21:
|
||||||
|
// encForward.sendBack(data1, sizeof(data1));
|
||||||
|
// break;
|
||||||
|
// case 22:
|
||||||
|
// encForward.sendBack(data2, sizeof(data2));
|
||||||
|
// break;
|
||||||
|
// case 23:
|
||||||
|
// encForward.sendBack(data3, sizeof(data3));
|
||||||
|
// break;
|
||||||
|
// case 24:
|
||||||
|
// encForward.sendBack(data4, sizeof(data4));
|
||||||
|
// break;
|
||||||
|
|
||||||
|
// case 30:
|
||||||
|
// encForward.sendBackTo(targetAddr);
|
||||||
|
// break;
|
||||||
|
// case 31:
|
||||||
|
// encForward.sendBackTo(targetAddr, data1, sizeof(data1));
|
||||||
|
// break;
|
||||||
|
// case 32:
|
||||||
|
// encForward.sendBackTo(targetAddr, data2, sizeof(data2));
|
||||||
|
// break;
|
||||||
|
// case 33:
|
||||||
|
// encForward.sendBackTo(targetAddr, data3, sizeof(data3));
|
||||||
|
// break;
|
||||||
|
// case 34:
|
||||||
|
// encForward.sendBackTo(targetAddr, data4, sizeof(data4));
|
||||||
|
// break;
|
||||||
|
|
||||||
|
// case 41:
|
||||||
|
// encForward.sendRequest(targetAddr);
|
||||||
|
// break;
|
||||||
|
// case 42:
|
||||||
|
// encForward.sendAccept(targetAddr);
|
||||||
|
// break;
|
||||||
|
|
||||||
|
// default:
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// encBackward.sendData(IR_Broadcast, data2);
|
||||||
|
// encTree.sendData(IR_Broadcast, rawData3);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timer t2(50, millis, []()
|
||||||
|
// { digitalToggle(LED_BUILTIN); });
|
||||||
|
|
||||||
|
Timer signalDetectTimer;
|
||||||
/////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
HardwareTimer IR_Timer(TIM3);
|
||||||
|
|
||||||
void setup()
|
void setup()
|
||||||
{
|
{
|
||||||
|
IR_Timer.setOverflow((uint32_t)carrierFrec * (uint32_t)IR_Encoder::carrierMultiply(), HERTZ_FORMAT);
|
||||||
|
IR_Timer.attachInterrupt(1, EncoderISR);
|
||||||
|
NVIC_SetPriority(IRQn_Type::TIM3_IRQn, 0);
|
||||||
|
IR_Timer.resume();
|
||||||
|
|
||||||
Serial.begin(SERIAL_SPEED);
|
Serial.begin(SERIAL_SPEED);
|
||||||
Serial.println(F(INFO));
|
Serial.println(F(INFO));
|
||||||
|
|
||||||
// pinMode(LoopOut, OUTPUT);
|
pinMode(LoopOut, OUTPUT);
|
||||||
pinMode(LED_BUILTIN, OUTPUT);
|
|
||||||
|
|
||||||
pinMode(dec0_PIN, INPUT_PULLUP);
|
pinMode(dec0_PIN, INPUT_PULLUP);
|
||||||
|
pinMode(dec1_PIN, INPUT_PULLUP);
|
||||||
|
pinMode(dec2_PIN, INPUT_PULLUP);
|
||||||
|
pinMode(dec3_PIN, INPUT_PULLUP);
|
||||||
|
pinMode(dec4_PIN, INPUT_PULLUP);
|
||||||
|
pinMode(dec5_PIN, INPUT_PULLUP);
|
||||||
|
pinMode(dec6_PIN, INPUT_PULLUP);
|
||||||
|
pinMode(dec7_PIN, INPUT_PULLUP);
|
||||||
|
// pinMode(dec8_PIN, INPUT_PULLUP);
|
||||||
|
// pinMode(dec9_PIN, INPUT_PULLUP);
|
||||||
|
// pinMode(dec10_PIN, INPUT_PULLUP);
|
||||||
|
// pinMode(dec11_PIN, INPUT_PULLUP);
|
||||||
|
// pinMode(dec12_PIN, INPUT_PULLUP);
|
||||||
|
// pinMode(dec13_PIN, INPUT_PULLUP);
|
||||||
|
|
||||||
attachInterrupt(dec0_PIN, dec_0_ISR, CHANGE);
|
attachInterrupt(dec0_PIN, dec_0_ISR, CHANGE);
|
||||||
|
attachInterrupt(dec1_PIN, dec_1_ISR, CHANGE);
|
||||||
|
attachInterrupt(dec2_PIN, dec_2_ISR, CHANGE);
|
||||||
|
attachInterrupt(dec3_PIN, dec_3_ISR, CHANGE);
|
||||||
|
attachInterrupt(dec4_PIN, dec_4_ISR, CHANGE);
|
||||||
|
attachInterrupt(dec5_PIN, dec_5_ISR, CHANGE);
|
||||||
|
attachInterrupt(dec6_PIN, dec_6_ISR, CHANGE);
|
||||||
|
attachInterrupt(dec7_PIN, dec_7_ISR, CHANGE);
|
||||||
|
// attachInterrupt(dec8_PIN, dec_8_ISR, CHANGE);
|
||||||
|
// attachInterrupt(dec9_PIN, dec_9_ISR, CHANGE);
|
||||||
|
// attachInterrupt(dec10_PIN, dec_10_ISR, CHANGE);
|
||||||
|
// attachInterrupt(dec11_PIN, dec_11_ISR, CHANGE);
|
||||||
|
// attachInterrupt(dec12_PIN, dec_12_ISR, CHANGE);
|
||||||
|
// attachInterrupt(dec13_PIN, dec_13_ISR, CHANGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop()
|
void loop()
|
||||||
{
|
{
|
||||||
// digitalToggle(LoopOut);
|
digitalToggle(LoopOut);
|
||||||
Timer::tick();
|
Timer::tick();
|
||||||
|
IR_Decoder::tick();
|
||||||
|
|
||||||
dec0.tick();
|
bool rx_flag = false;
|
||||||
|
rx_flag |= status(dec0);
|
||||||
|
rx_flag |= status(dec1);
|
||||||
|
rx_flag |= status(dec2);
|
||||||
|
rx_flag |= status(dec3);
|
||||||
|
rx_flag |= status(dec4);
|
||||||
|
rx_flag |= status(dec5);
|
||||||
|
rx_flag |= status(dec6);
|
||||||
|
rx_flag |= status(dec7);
|
||||||
|
// rx_flag |= status(dec8);
|
||||||
|
// rx_flag |= status(dec9);
|
||||||
|
// rx_flag |= status(dec10);
|
||||||
|
// rx_flag |= status(dec11);
|
||||||
|
// rx_flag |= status(dec12);
|
||||||
|
// rx_flag |= status(dec13);
|
||||||
|
|
||||||
bool rx_flag;
|
if (Serial.available())
|
||||||
rx_flag |= statusSimple(dec0);
|
{
|
||||||
|
uint8_t in = Serial.parseInt();
|
||||||
if(rx_flag){
|
switch (in)
|
||||||
Serial.print("\n\n\n\n");
|
{
|
||||||
|
case 100:
|
||||||
|
targetAddr = IR_Broadcast;
|
||||||
|
break;
|
||||||
|
case 101:
|
||||||
|
targetAddr = 555;
|
||||||
|
break;
|
||||||
|
case 102:
|
||||||
|
targetAddr = 777;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
sig = in;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
bool statusSimple(IR_Decoder &dec){
|
Timer statusSimpleDelay;
|
||||||
|
bool statusSimple(IR_Decoder &dec)
|
||||||
|
{
|
||||||
bool ret;
|
bool ret;
|
||||||
if (ret = dec.gotData.available())
|
if (ret = dec.gotData.available())
|
||||||
{
|
{
|
||||||
Serial.print("DEC "); Serial.println(dec.id);
|
Serial.print("DEC: ");
|
||||||
|
Serial.print(dec.getId());
|
||||||
|
Serial.print(" err: ");
|
||||||
|
Serial.print(dec.gotData.getErrorCount());
|
||||||
|
Serial.print("\n");
|
||||||
|
statusSimpleDelay.delay(100, millis, []()
|
||||||
|
{ Serial.print("\n\n\n\n"); });
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void detectSignal()
|
||||||
|
{
|
||||||
|
// digitalWrite(SignalDetectLed, HIGH);
|
||||||
|
// signalDetectTimer.delay(50, millis, []()
|
||||||
|
// { digitalWrite(SignalDetectLed, LOW); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// test
|
||||||
|
bool status(IR_Decoder &dec)
|
||||||
|
{
|
||||||
|
if (dec.gotData.available())
|
||||||
|
{
|
||||||
|
detectSignal();
|
||||||
|
// Serial.println(micros());
|
||||||
|
String str;
|
||||||
|
if (/* dec.gotData.getDataPrt()[1] */ 1)
|
||||||
|
{
|
||||||
|
str += ("Data on pin ");
|
||||||
|
str += (dec.getPin());
|
||||||
|
str += "\n";
|
||||||
|
|
||||||
|
uint8_t msg = dec.gotData.getMsgRAW();
|
||||||
|
str += (" MSG: ");
|
||||||
|
for (size_t i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
if (i == 3)
|
||||||
|
str += " ";
|
||||||
|
str += (msg >> (7 - i)) & 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
str += "\n";
|
||||||
|
|
||||||
|
str += (" DATA SIZE: ");
|
||||||
|
str += (dec.gotData.getDataSize());
|
||||||
|
str += "\n";
|
||||||
|
str += (" ADDRESS FROM: ");
|
||||||
|
str += (dec.gotData.getAddrFrom());
|
||||||
|
str += "\n";
|
||||||
|
str += (" ADDRESS TO: ");
|
||||||
|
str += (dec.gotData.getAddrTo());
|
||||||
|
str += "\n";
|
||||||
|
// str += (" CRC PACK: "); str += (dec.gotData.getCrcIN()); str += "\n";
|
||||||
|
// str += (" CRC CALC: "); str += (dec.gotData.getCrcCALC()); str += "\n";
|
||||||
|
str += "\n";
|
||||||
|
|
||||||
|
for (size_t i = 0; i < min(uint8_t(10), dec.gotData.getDataSize()); i++)
|
||||||
|
{
|
||||||
|
switch (i)
|
||||||
|
{
|
||||||
|
// case 0:
|
||||||
|
// str += (" ADDR: ");
|
||||||
|
// break;
|
||||||
|
// case 1:
|
||||||
|
// str += (" CMD: ");
|
||||||
|
// break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
str += (" Data[");
|
||||||
|
str += (i);
|
||||||
|
str += ("]: ");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
str += (dec.gotData.getDataPrt()[i]);
|
||||||
|
str += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
str += ("\n*******ErrAll: ");
|
||||||
|
str += (dec.gotData.getErrorCount());
|
||||||
|
str += "\n";
|
||||||
|
str += ("**ErrDistance: ");
|
||||||
|
str += ((int)(dec.gotData.getErrorHighSignal() - dec.gotData.getErrorLowSignal()));
|
||||||
|
str += "\n";
|
||||||
|
|
||||||
|
str += "\n";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
str += ("SELF");
|
||||||
|
str += "\n";
|
||||||
|
str += "\n";
|
||||||
|
}
|
||||||
|
// obj->resetAvailable();
|
||||||
|
Serial.write(str.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dec.gotBackData.available())
|
||||||
|
{
|
||||||
|
detectSignal();
|
||||||
|
String str;
|
||||||
|
if (/* dec.gotData.getDataPrt()[1] */ 1)
|
||||||
|
{
|
||||||
|
str += ("BackData on pin ");
|
||||||
|
str += (dec.getPin());
|
||||||
|
str += "\n";
|
||||||
|
|
||||||
|
uint8_t msg = dec.gotBackData.getMsgRAW();
|
||||||
|
str += (" MSG: ");
|
||||||
|
for (size_t i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
if (i == 3)
|
||||||
|
str += " ";
|
||||||
|
str += (msg >> (7 - i)) & 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
str += "\n";
|
||||||
|
|
||||||
|
str += (" DATA SIZE: ");
|
||||||
|
str += (dec.gotBackData.getDataSize());
|
||||||
|
str += "\n";
|
||||||
|
str += (" ADDRESS FROM: ");
|
||||||
|
str += (dec.gotBackData.getAddrFrom());
|
||||||
|
str += "\n";
|
||||||
|
// str += (" ADDRESS TO: "); str += (dec.gotBackData.getAddrTo()); str += "\n";
|
||||||
|
// str += (" CRC PACK: "); str += (dec.gotBackData.getCrcIN()); str += "\n";
|
||||||
|
// str += (" CRC CALC: "); str += (dec.gotBackData.getCrcCALC()); str += "\n";
|
||||||
|
str += "\n";
|
||||||
|
|
||||||
|
for (size_t i = 0; i < min(uint8_t(10), dec.gotBackData.getDataSize()); i++)
|
||||||
|
{
|
||||||
|
switch (i)
|
||||||
|
{
|
||||||
|
// case 0:
|
||||||
|
// str += (" ADDR: ");
|
||||||
|
// break;
|
||||||
|
// case 1:
|
||||||
|
// str += (" CMD: ");
|
||||||
|
// break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
str += (" Data[");
|
||||||
|
str += (i);
|
||||||
|
str += ("]: ");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
str += (dec.gotBackData.getDataPrt()[i]);
|
||||||
|
str += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
str += ("\n*******ErrAll: ");
|
||||||
|
str += (dec.gotBackData.getErrorCount());
|
||||||
|
str += "\n";
|
||||||
|
str += ("**ErrDistance: ");
|
||||||
|
str += ((int)(dec.gotBackData.getErrorHighSignal() - dec.gotBackData.getErrorLowSignal()));
|
||||||
|
str += "\n";
|
||||||
|
|
||||||
|
str += "\n";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
str += ("SELF");
|
||||||
|
str += "\n";
|
||||||
|
str += "\n";
|
||||||
|
}
|
||||||
|
// obj->resetAvailable();
|
||||||
|
Serial.write(str.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dec.gotAccept.available())
|
||||||
|
{
|
||||||
|
detectSignal();
|
||||||
|
String str;
|
||||||
|
if (/* dec.gotData.getDataPrt()[1] */ 1)
|
||||||
|
{
|
||||||
|
str += ("Accept on pin ");
|
||||||
|
str += (dec.getPin());
|
||||||
|
str += "\n";
|
||||||
|
|
||||||
|
uint8_t msg = dec.gotAccept.getMsgRAW();
|
||||||
|
str += (" MSG: ");
|
||||||
|
for (size_t i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
if (i == 3)
|
||||||
|
str += " ";
|
||||||
|
str += (msg >> (7 - i)) & 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
str += "\n";
|
||||||
|
|
||||||
|
// str += (" DATA SIZE: "); str += (dec.gotAccept.getDataSize()); str += "\n";
|
||||||
|
str += (" ADDRESS FROM: ");
|
||||||
|
str += (dec.gotAccept.getAddrFrom());
|
||||||
|
str += "\n";
|
||||||
|
// str += (" ADDRESS TO: "); str += (dec.gotAccept.getAddrTo()); str += "\n";
|
||||||
|
// str += (" CRC PACK: "); str += (dec.gotAccept.getCrcIN()); str += "\n";
|
||||||
|
// str += (" CRC CALC: "); str += (dec.gotAccept.getCrcCALC()); str += "\n";
|
||||||
|
str += "\n";
|
||||||
|
|
||||||
|
str += (" Data: ");
|
||||||
|
str += (dec.gotAccept.getCustomByte());
|
||||||
|
|
||||||
|
str += ("\n\n*******ErrAll: ");
|
||||||
|
str += (dec.gotAccept.getErrorCount());
|
||||||
|
str += "\n";
|
||||||
|
str += ("**ErrDistance: ");
|
||||||
|
str += ((int)(dec.gotAccept.getErrorHighSignal() - dec.gotAccept.getErrorLowSignal()));
|
||||||
|
str += "\n";
|
||||||
|
|
||||||
|
str += "\n";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
str += ("SELF");
|
||||||
|
str += "\n";
|
||||||
|
str += "\n";
|
||||||
|
}
|
||||||
|
// obj->resetAvailable();
|
||||||
|
Serial.write(str.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dec.gotRequest.available())
|
||||||
|
{
|
||||||
|
detectSignal();
|
||||||
|
String str;
|
||||||
|
if (/* dec.gotData.getDataPrt()[1] */ 1)
|
||||||
|
{
|
||||||
|
str += ("Request on pin ");
|
||||||
|
str += (dec.getPin());
|
||||||
|
str += "\n";
|
||||||
|
|
||||||
|
uint8_t msg = dec.gotRequest.getMsgRAW();
|
||||||
|
str += (" MSG: ");
|
||||||
|
for (size_t i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
if (i == 3)
|
||||||
|
str += " ";
|
||||||
|
str += (msg >> (7 - i)) & 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
str += "\n";
|
||||||
|
|
||||||
|
// str += (" DATA SIZE: "); str += (dec.gotRequest.getDataSize()); str += "\n";
|
||||||
|
str += (" ADDRESS FROM: ");
|
||||||
|
str += (dec.gotRequest.getAddrFrom());
|
||||||
|
str += "\n";
|
||||||
|
str += (" ADDRESS TO: ");
|
||||||
|
str += (dec.gotRequest.getAddrTo());
|
||||||
|
str += "\n";
|
||||||
|
// str += (" CRC PACK: "); str += (dec.gotRequest.getCrcIN()); str += "\n";
|
||||||
|
// str += (" CRC CALC: "); str += (dec.gotRequest.getCrcCALC()); str += "\n";
|
||||||
|
str += "\n";
|
||||||
|
|
||||||
|
str += ("\n*******ErrAll: ");
|
||||||
|
str += (dec.gotRequest.getErrorCount());
|
||||||
|
str += "\n";
|
||||||
|
str += ("**ErrDistance: ");
|
||||||
|
str += ((int)(dec.gotRequest.getErrorHighSignal() - dec.gotRequest.getErrorLowSignal()));
|
||||||
|
str += "\n";
|
||||||
|
|
||||||
|
str += "\n";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
str += ("SELF");
|
||||||
|
str += "\n";
|
||||||
|
str += "\n";
|
||||||
|
}
|
||||||
|
// obj->resetAvailable();
|
||||||
|
Serial.write(str.c_str());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
141
IR_DMA_ISR_signal_analysis.md
Normal file
141
IR_DMA_ISR_signal_analysis.md
Normal file
@ -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` пересчитывайте скриптом.*
|
||||||
178
IR_Decoder.cpp
Normal file
178
IR_Decoder.cpp
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
#include "IR_Decoder.h"
|
||||||
|
|
||||||
|
#if defined(ARDUINO_ARCH_STM32) && !defined(HAL_EXTI_MODULE_DISABLED)
|
||||||
|
#include "Arduino.h"
|
||||||
|
/* NVIC_SetPriority — CMSIS, как в IR_Encoder::begin и Car.ino (без HAL-заголовка yyxx). */
|
||||||
|
|
||||||
|
/** NVIC для линии EXTI пина (как в Arduino STM32 SrcWrapper interrupt.cpp). */
|
||||||
|
static IRQn_Type ir_decoder_exti_irqn_for_pin(uint8_t arduino_pin)
|
||||||
|
{
|
||||||
|
#if defined(STM32C0xx) || defined(STM32F0xx) || defined(STM32G0xx) || defined(STM32L0xx)
|
||||||
|
(void)arduino_pin;
|
||||||
|
return (IRQn_Type)(-1);
|
||||||
|
#else
|
||||||
|
const PinName p = digitalPinToPinName(arduino_pin);
|
||||||
|
if (p == NC) {
|
||||||
|
return (IRQn_Type)(-1);
|
||||||
|
}
|
||||||
|
const uint16_t pinmask = STM_GPIO_PIN(p);
|
||||||
|
uint8_t id = 0U;
|
||||||
|
uint16_t pm = pinmask;
|
||||||
|
while (pm != 0x0001U) {
|
||||||
|
pm = (uint16_t)(pm >> 1U);
|
||||||
|
id++;
|
||||||
|
}
|
||||||
|
#if defined(STM32H5xx) || defined(STM32MP1xx) || defined(STM32L5xx) || defined(STM32U5xx) || defined(STM32WBAxx)
|
||||||
|
static const IRQn_Type exti_irqnb[16] = {
|
||||||
|
EXTI0_IRQn, EXTI1_IRQn, EXTI2_IRQn, EXTI3_IRQn, EXTI4_IRQn, EXTI5_IRQn, EXTI6_IRQn,
|
||||||
|
EXTI7_IRQn, EXTI8_IRQn, EXTI9_IRQn, EXTI10_IRQn, EXTI11_IRQn,
|
||||||
|
EXTI12_IRQn, EXTI13_IRQn, EXTI14_IRQn, EXTI15_IRQn};
|
||||||
|
#else
|
||||||
|
static const IRQn_Type exti_irqnb[16] = {
|
||||||
|
EXTI0_IRQn, EXTI1_IRQn, EXTI2_IRQn, EXTI3_IRQn, EXTI4_IRQn,
|
||||||
|
EXTI9_5_IRQn, EXTI9_5_IRQn, EXTI9_5_IRQn, EXTI9_5_IRQn, EXTI9_5_IRQn,
|
||||||
|
EXTI15_10_IRQn, EXTI15_10_IRQn, EXTI15_10_IRQn, EXTI15_10_IRQn,
|
||||||
|
EXTI15_10_IRQn, EXTI15_10_IRQn};
|
||||||
|
#endif
|
||||||
|
if (id < 16U) {
|
||||||
|
return exti_irqnb[id];
|
||||||
|
}
|
||||||
|
return (IRQn_Type)(-1);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ir_decoder_apply_rx_exti_nvic(uint8_t arduino_pin, uint32_t preempt)
|
||||||
|
{
|
||||||
|
const IRQn_Type irqn = ir_decoder_exti_irqn_for_pin(arduino_pin);
|
||||||
|
if ((int)irqn < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#if !defined(STM32C0xx) && !defined(STM32F0xx) && !defined(STM32G0xx) && !defined(STM32L0xx)
|
||||||
|
NVIC_SetPriority(irqn, preempt);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void IR_Decoder::setReceiveExtiPreemptPriority(uint32_t preempt)
|
||||||
|
{
|
||||||
|
rxExtiPreemptConfigured_ = true;
|
||||||
|
rxExtiPreemptValue_ = preempt;
|
||||||
|
if (extiEnabled_) {
|
||||||
|
ir_decoder_apply_rx_exti_nvic(pin, preempt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif /* ARDUINO_ARCH_STM32 && !HAL_EXTI_MODULE_DISABLED */
|
||||||
|
|
||||||
|
std::list<IR_Decoder *> &IR_Decoder::get_dec_list() // определение функции
|
||||||
|
{
|
||||||
|
static std::list<IR_Decoder *> dec_list; // статическая локальная переменная
|
||||||
|
return dec_list; // возвращается ссылка на переменную
|
||||||
|
}
|
||||||
|
|
||||||
|
// IR_Decoder::IR_Decoder() {};
|
||||||
|
IR_Decoder::IR_Decoder(const uint8_t pin, uint16_t addr, IR_Encoder *encPair, bool enableOnConstruct)
|
||||||
|
: IR_DecoderRaw(pin, addr, encPair)
|
||||||
|
{
|
||||||
|
get_dec_list().push_back(this);
|
||||||
|
if (enableOnConstruct) {
|
||||||
|
enable();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void IR_Decoder::enable()
|
||||||
|
{
|
||||||
|
auto &dec_list = get_dec_list();
|
||||||
|
if (std::find(dec_list.begin(), dec_list.end(), this) == dec_list.end())
|
||||||
|
{
|
||||||
|
dec_list.push_back(this);
|
||||||
|
}
|
||||||
|
pinMode(pin, INPUT_PULLUP);
|
||||||
|
attachInterrupt(pin, (*this)(), CHANGE);
|
||||||
|
extiEnabled_ = true;
|
||||||
|
#if defined(ARDUINO_ARCH_STM32) && !defined(HAL_EXTI_MODULE_DISABLED)
|
||||||
|
if (rxExtiPreemptConfigured_) {
|
||||||
|
ir_decoder_apply_rx_exti_nvic(pin, rxExtiPreemptValue_);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void IR_Decoder::disable()
|
||||||
|
{
|
||||||
|
extiEnabled_ = false;
|
||||||
|
detachInterrupt(pin);
|
||||||
|
pinMode(pin, INPUT);
|
||||||
|
auto &dec_list = get_dec_list();
|
||||||
|
auto it = std::find(dec_list.begin(), dec_list.end(), this);
|
||||||
|
if (it != dec_list.end())
|
||||||
|
{
|
||||||
|
dec_list.erase(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::function<void()> IR_Decoder::operator()()
|
||||||
|
{
|
||||||
|
return std::bind(&IR_Decoder::isr, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
IR_Decoder::~IR_Decoder()
|
||||||
|
{
|
||||||
|
IR_Decoder::get_dec_list().remove(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IR_Decoder::tick()
|
||||||
|
{
|
||||||
|
for (const auto &element : IR_Decoder::get_dec_list())
|
||||||
|
{
|
||||||
|
element->_tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void IR_Decoder::_tick()
|
||||||
|
{
|
||||||
|
IR_DecoderRaw::tick();
|
||||||
|
|
||||||
|
if (availableRaw())
|
||||||
|
{
|
||||||
|
isWaitingAcceptSend = false;
|
||||||
|
switch (packInfo.buffer[0] >> 5 & IR_MASK_MSG_TYPE)
|
||||||
|
{
|
||||||
|
case IR_MSG_DATA_ACCEPT:
|
||||||
|
case IR_MSG_DATA_NOACCEPT:
|
||||||
|
gotData.set(&packInfo, id);
|
||||||
|
break;
|
||||||
|
case IR_MSG_BACK:
|
||||||
|
case IR_MSG_BACK_TO:
|
||||||
|
gotBackData.set(&packInfo, id);
|
||||||
|
break;
|
||||||
|
case IR_MSG_REQUEST:
|
||||||
|
gotRequest.set(&packInfo, id);
|
||||||
|
break;
|
||||||
|
case IR_MSG_ACCEPT:
|
||||||
|
gotAccept.set(&packInfo, id);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (gotData.isAvailable && (gotData.getMsgType() == IR_MSG_DATA_ACCEPT))
|
||||||
|
{
|
||||||
|
acceptSendTimer = millis();
|
||||||
|
addrAcceptSendTo = gotData.getAddrFrom();
|
||||||
|
acceptCustomByte = crc8(gotData.getDataPrt(), 0, gotData.getDataSize(), poly1);
|
||||||
|
if (addrAcceptSendTo && addrAcceptSendTo < IR_Broadcast)
|
||||||
|
isWaitingAcceptSend = true;
|
||||||
|
}
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
if (encoder != nullptr) encoder->sendAccept(addrAcceptSendTo, acceptCustomByte); // B2: у Car decoder без парного энкодера (nullptr) → был HardFault
|
||||||
|
isWaitingAcceptSend = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IR_Decoder::isReceive(uint8_t type) {
|
||||||
|
return (msgTypeReceive & 0b11111000) && ((msgTypeReceive & IR_MASK_MSG_TYPE) == type);
|
||||||
|
}
|
||||||
96
IR_Decoder.h
96
IR_Decoder.h
@ -5,12 +5,21 @@
|
|||||||
|
|
||||||
class IR_Decoder : public IR_DecoderRaw
|
class IR_Decoder : public IR_DecoderRaw
|
||||||
{
|
{
|
||||||
uint32_t acceptSendTimer;
|
private:
|
||||||
bool isWaitingAcceptSend;
|
// static std::list<IR_Decoder *> dec_list;
|
||||||
uint16_t addrAcceptSendTo;
|
static std::list<IR_Decoder*>& get_dec_list();
|
||||||
|
void _tick();
|
||||||
|
|
||||||
uint16_t acceptDelay = 75;
|
uint32_t acceptSendTimer = 0;
|
||||||
uint8_t acceptCustomByte;
|
bool isWaitingAcceptSend = false; // B4: без инициализации мусор мог дёрнуть sendAccept при старте
|
||||||
|
uint16_t addrAcceptSendTo = 0;
|
||||||
|
|
||||||
|
uint16_t acceptDelay = IR_ResponseDelay;
|
||||||
|
uint8_t acceptCustomByte = 0;
|
||||||
|
|
||||||
|
bool extiEnabled_ = false;
|
||||||
|
bool rxExtiPreemptConfigured_ = false;
|
||||||
|
uint32_t rxExtiPreemptValue_ = 0;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
PacketTypes::Data gotData;
|
PacketTypes::Data gotData;
|
||||||
@ -19,59 +28,44 @@ public:
|
|||||||
PacketTypes::Request gotRequest;
|
PacketTypes::Request gotRequest;
|
||||||
PacketTypes::BasePack gotRaw;
|
PacketTypes::BasePack gotRaw;
|
||||||
|
|
||||||
IR_Decoder(const uint8_t isrPin, uint16_t addr, IR_Encoder *encPair = nullptr) : IR_DecoderRaw(isrPin, addr, encPair) {}
|
// IR_Decoder();
|
||||||
|
/** @param enableOnConstruct true — вызвать enable() из конструктора; false — отложенный enable() (NVIC и т.д.), tick — tickThis() / tick(). */
|
||||||
|
IR_Decoder(const uint8_t pin, uint16_t addr = 0, IR_Encoder *encPair = nullptr, bool enableOnConstruct = true);
|
||||||
|
|
||||||
void tick()
|
std::function<void()> operator()();
|
||||||
{
|
|
||||||
IR_DecoderRaw::tick();
|
/**
|
||||||
if (availableRaw())
|
* Arduino STM32: после attachInterrupt ядро выставляет свой приоритет EXTI.
|
||||||
{
|
* Если вызывали setReceiveExtiPreemptPriority(), здесь он применяется поверх (обычно нужен выше срочности, чем DMA ИК-TX).
|
||||||
#ifdef IRDEBUG_INFO
|
* На других платформах поведение без изменений.
|
||||||
Serial.println("PARSING RAW DATA");
|
*/
|
||||||
|
void enable();
|
||||||
|
void disable();
|
||||||
|
|
||||||
|
#if defined(ARDUINO_ARCH_STM32) && !defined(HAL_EXTI_MODULE_DISABLED)
|
||||||
|
/**
|
||||||
|
* Задать preempt-приоритет NVIC для EXTI линии этого пина (тот же смысл, что второй аргумент CMSIS NVIC_SetPriority).
|
||||||
|
* Вызывайте до или после enable(); при активном приёме применяется сразу.
|
||||||
|
* При использовании DMA на передачу ИК preempt приёма должен быть меньше, чем у DMA TX (выше срочность прерывания).
|
||||||
|
*/
|
||||||
|
void setReceiveExtiPreemptPriority(uint32_t preempt);
|
||||||
#endif
|
#endif
|
||||||
isWaitingAcceptSend = false;
|
|
||||||
switch (packInfo.buffer[0] >> 5 & IR_MASK_MSG_TYPE)
|
|
||||||
{
|
|
||||||
case IR_MSG_DATA_ACCEPT:
|
|
||||||
case IR_MSG_DATA_NOACCEPT:
|
|
||||||
gotData.set(&packInfo, id);
|
|
||||||
break;
|
|
||||||
case IR_MSG_BACK:
|
|
||||||
case IR_MSG_BACK_TO:
|
|
||||||
gotBackData.set(&packInfo, id);
|
|
||||||
break;
|
|
||||||
case IR_MSG_REQUEST:
|
|
||||||
gotRequest.set(&packInfo, id);
|
|
||||||
break;
|
|
||||||
case IR_MSG_ACCEPT:
|
|
||||||
gotAccept.set(&packInfo, id);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
bool isReceive(uint8_t type);
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (gotData.isAvailable && (gotData.getMsgType() == IR_MSG_DATA_ACCEPT))
|
|
||||||
{
|
|
||||||
acceptSendTimer = millis();
|
|
||||||
addrAcceptSendTo = gotData.getAddrFrom();
|
|
||||||
acceptCustomByte = crc8(gotData.getDataPrt(), 0, gotData.getDataSize(), poly1);
|
|
||||||
if (addrAcceptSendTo && addrAcceptSendTo < IR_Broadcast)
|
|
||||||
isWaitingAcceptSend = true;
|
|
||||||
}
|
|
||||||
gotRaw.set(&packInfo, id);
|
|
||||||
}
|
|
||||||
if (isWaitingAcceptSend && millis() - acceptSendTimer > 75)
|
|
||||||
{
|
|
||||||
encoder->sendAccept(addrAcceptSendTo, acceptCustomByte);
|
|
||||||
isWaitingAcceptSend = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setAcceptDelay(uint16_t acceptDelay)
|
~IR_Decoder();
|
||||||
|
|
||||||
|
/** Обойти все экземпляры из внутреннего списка и вызвать tick у каждого. */
|
||||||
|
static void tick();
|
||||||
|
|
||||||
|
/** Tick только этого декодера (без обхода списка). Не комбинируйте с static tick() для того же экземпляра. */
|
||||||
|
void tickThis() { _tick(); }
|
||||||
|
|
||||||
|
inline void setAcceptDelay(uint16_t acceptDelay)
|
||||||
{
|
{
|
||||||
this->acceptDelay = acceptDelay;
|
this->acceptDelay = acceptDelay;
|
||||||
}
|
}
|
||||||
uint16_t getAcceptDelay()
|
inline uint16_t getAcceptDelay()
|
||||||
{
|
{
|
||||||
return this->acceptDelay;
|
return this->acceptDelay;
|
||||||
}
|
}
|
||||||
|
|||||||
1610
IR_DecoderRaw.cpp
1610
IR_DecoderRaw.cpp
File diff suppressed because it is too large
Load Diff
265
IR_DecoderRaw.h
265
IR_DecoderRaw.h
@ -2,20 +2,20 @@
|
|||||||
#include "IR_config.h"
|
#include "IR_config.h"
|
||||||
#include "RingBuffer.h"
|
#include "RingBuffer.h"
|
||||||
|
|
||||||
// #define IRDEBUG
|
class Print;
|
||||||
|
|
||||||
|
#define IRDEBUG
|
||||||
|
|
||||||
#ifdef IRDEBUG
|
#ifdef IRDEBUG
|
||||||
#define wrHigh PA1 // Запись HIGH инициирована // green
|
#define wrHigh 255 // Запись HIGH инициирована // green
|
||||||
#define wrLow PA0 // Запись LOW инициирована // blue
|
#define wrLow 255 // Запись LOW инициирована // blue
|
||||||
#define writeOp PA5 // Операция записи, 1 пульс для 0 и 2 для 1 // orange
|
#define writeOp 255 // Операция записи, 1 пульс для 0 и 2 для 1 // orange
|
||||||
// Исправленные ошибки // purle
|
// Исправленные ошибки // purle
|
||||||
// 1 пульс: fix
|
// 1 пульс: fix
|
||||||
#define errOut PA4
|
#define errOut 255
|
||||||
#define up PA3
|
#define up 255
|
||||||
#define down PA2
|
#define down 255
|
||||||
#endif
|
#endif
|
||||||
#define up PA3
|
|
||||||
#define down PA2
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
@ -25,6 +25,7 @@
|
|||||||
#define riseTimeMin (riseTime - riseTolerance)
|
#define riseTimeMin (riseTime - riseTolerance)
|
||||||
#define aroundRise(t) (riseTimeMin < t && t < riseTimeMax)
|
#define aroundRise(t) (riseTimeMin < t && t < riseTimeMax)
|
||||||
#define IR_timeout (riseTimeMax * (8 + syncBits + 1)) // us // таймаут в 8 data + 3 sync + 1
|
#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_Encoder;
|
||||||
class IR_DecoderRaw : virtual public IR_FOX
|
class IR_DecoderRaw : virtual public IR_FOX
|
||||||
@ -33,52 +34,112 @@ class IR_DecoderRaw : virtual public IR_FOX
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
PackInfo packInfo;
|
PackInfo packInfo;
|
||||||
|
uint8_t msgTypeReceive = 0;
|
||||||
IR_Encoder *encoder; // Указатель на парный передатчик
|
IR_Encoder *encoder; // Указатель на парный передатчик
|
||||||
bool availableRaw()
|
bool availableRaw();
|
||||||
{
|
|
||||||
if (isAvailable)
|
|
||||||
{
|
|
||||||
isAvailable = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
const uint8_t isrPin; // Пин прерывания
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
/// @brief Конструктор
|
/// @brief Конструктор
|
||||||
/// @param isrPin Номер вывода прерывания/данных от приёмника (2 или 3 для atmega 328p)
|
/// @param pin Номер вывода прерывания/данных от приёмника (2 или 3 для atmega 328p)
|
||||||
/// @param addr Адрес приёмника
|
/// @param addr Адрес приёмника
|
||||||
/// @param encPair Указатель на передатчик, работающий в паре
|
/// @param encPair Указатель на передатчик, работающий в паре
|
||||||
IR_DecoderRaw(const uint8_t isrPin, uint16_t addr, IR_Encoder *encPair = nullptr);
|
IR_DecoderRaw(const uint8_t pin, uint16_t addr, IR_Encoder *encPair = nullptr);
|
||||||
|
|
||||||
void isr(); // Функция прерывания
|
void isr(); // Функция прерывания
|
||||||
void tick(); // Обработка приёмника, необходима для работы
|
void tick(); // Обработка приёмника, необходима для работы
|
||||||
void tickOld();
|
|
||||||
|
|
||||||
bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился
|
inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился
|
||||||
bool isSubOverflow()
|
bool isSubOverflow();
|
||||||
{
|
volatile inline bool isReciving() { return isRecive; }; // Возвращает true, если происходит приём пакета
|
||||||
|
// Активность линии по СОСТОЯНИЮ (не по хардкод-длительности): кадр залочен ИЛИ формируется
|
||||||
|
// ВАЛИДНАЯ преамбула (>=1 совпавший по периоду фронт — отличает реальный кадр от одиночного
|
||||||
|
// шумового фронта, который лишь заводит Candidate, но не набирает goodPeriods). Для гейта заднего:
|
||||||
|
// «не стрелять, пока на линии идёт/формируется кадр (напр. ответ точки)». Аддитивно, const.
|
||||||
|
inline bool rxLineActive() const {
|
||||||
|
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)irMaxPackSize; }
|
||||||
|
|
||||||
// noInterrupts();
|
// ---- Наблюдаемость приёма по СОСТОЯНИЮ: лок / ожидаемый конец / факт завершения с причиной ----
|
||||||
volatile bool ret = isSubBufferOverflow;
|
enum class RxEndReason : uint8_t { None = 0, Ok, Crc, Timeout, Abort };
|
||||||
// interrupts();
|
struct RxEndInfo {
|
||||||
return ret;
|
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 = неизвестен)
|
||||||
};
|
};
|
||||||
bool isReciving() { return isBufferOverflow; }; // Возвращает true, если происходит приём пакета
|
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; }
|
||||||
|
void pulseFilterResetStats();
|
||||||
|
|
||||||
|
#if defined(IR_EDGE_TRACE)
|
||||||
|
void edgeTraceClear();
|
||||||
|
bool edgeTraceOverflow() const { return edgeTrace_overflow; }
|
||||||
|
uint16_t edgeTracePendingCount() const;
|
||||||
|
/** При непустом кольце: перевод строки + @IRF1v1: + hex; в tick() сброс на Serial автоматически. См. ref/IR_EDGE_TRACE_FORMAT.md */
|
||||||
|
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; }
|
||||||
|
const uint8_t* getRejectBuffer() const { return rejectBuffer; }
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
private:
|
private:
|
||||||
|
enum class RxBriefReason : uint8_t
|
||||||
|
{
|
||||||
|
MuteBegin = 1,
|
||||||
|
MuteEnd = 2,
|
||||||
|
RawOverflow = 3,
|
||||||
|
FilterOverflow = 4,
|
||||||
|
HoldOverflow = 5,
|
||||||
|
Glitch = 6,
|
||||||
|
Timing = 7,
|
||||||
|
Preamble = 8,
|
||||||
|
Sync = 9,
|
||||||
|
BufferOverflow = 10,
|
||||||
|
Timeout = 11,
|
||||||
|
Crc = 12,
|
||||||
|
Ok = 13
|
||||||
|
};
|
||||||
|
|
||||||
|
bool isRejectAvailable = false;
|
||||||
|
uint8_t rejectPackSize = 0;
|
||||||
|
uint8_t rejectBuffer[dataByteSizeMax]{};
|
||||||
|
|
||||||
ErrorsStruct errors;
|
ErrorsStruct errors;
|
||||||
bool isAvailable = false;
|
bool isAvailable = false;
|
||||||
uint16_t packSize;
|
uint16_t packSize = 0;
|
||||||
uint16_t crcValue;
|
uint16_t crcValue = 0;
|
||||||
volatile uint16_t isPairSending = 0; // Флаг передачи парного передатчика
|
volatile uint16_t isPairSending = 0; // Число активных TX, временно глушащих этот RX.
|
||||||
volatile bool isRecive = false; // Флаг приёма
|
volatile bool isRecive = false; // Флаг приёма
|
||||||
volatile bool isPreamb = false; // флаг начальной последовости
|
volatile bool isPreamb = false; // флаг начальной последовости
|
||||||
volatile bool isSubBufferOverflow = false;
|
volatile bool isSubBufferOverflow = false;
|
||||||
@ -87,6 +148,8 @@ private:
|
|||||||
|
|
||||||
uint16_t riseSyncTime = bitTime; // Подстраиваемое время бита в мкс
|
uint16_t riseSyncTime = bitTime; // Подстраиваемое время бита в мкс
|
||||||
|
|
||||||
|
volatile uint32_t lastEdgeTime = 0; // время последнего фронта
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
volatile uint32_t currentSubBufferIndex; // Счетчик текущей позиции во вспомогательном буфере фронтов/спадов
|
volatile uint32_t currentSubBufferIndex; // Счетчик текущей позиции во вспомогательном буфере фронтов/спадов
|
||||||
|
|
||||||
@ -101,6 +164,60 @@ private:
|
|||||||
// volatile FrontStorage subBuffer[subBufferSize]; // вспомогательный буфер для хранения необработанных фронтов/спадов
|
// volatile FrontStorage subBuffer[subBufferSize]; // вспомогательный буфер для хранения необработанных фронтов/спадов
|
||||||
|
|
||||||
RingBuffer<FrontStorage, subBufferSize> subBuffer;
|
RingBuffer<FrontStorage, subBufferSize> subBuffer;
|
||||||
|
IR_Encoder *pairMuteEncoders[IR_PAIR_MUTE_MAX_ENCODERS]{};
|
||||||
|
uint8_t pairMuteEncoderCount = 0;
|
||||||
|
static constexpr uint8_t kPulseFilterHoldCap = 6;
|
||||||
|
FrontStorage pulseFilterHoldEdges[kPulseFilterHoldCap]{};
|
||||||
|
uint8_t pulseFilterHoldCount = 0;
|
||||||
|
bool pulseFilterLastRawValid = false;
|
||||||
|
uint32_t pulseFilterLastRawTime = 0;
|
||||||
|
uint32_t pulseFilterDropHoldOverflow = 0;
|
||||||
|
uint32_t pulseFilterDropGlitchPairs = 0;
|
||||||
|
static constexpr uint8_t kPreambleLockNeed = (uint8_t)IR_PREAMBLE_LOCK_RISE_PERIODS;
|
||||||
|
enum class PreambleState : uint8_t
|
||||||
|
{
|
||||||
|
Idle = 0,
|
||||||
|
Candidate = 1,
|
||||||
|
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 releasePreambleGuard(uint32_t tUs); // после abort: новый кандидат преамбулы без ожидания длинной тишины
|
||||||
|
void expirePreambleCandidate(); // кандидат без фронтов дольше таймаута → Idle
|
||||||
|
uint8_t preambleGoodPeriods = 0;
|
||||||
|
uint16_t preambleMeanPeriod = 0;
|
||||||
|
uint32_t preambleCandidateLastEdgeTime = 0;
|
||||||
|
uint32_t preambleCandidateFirstRiseTime = 0;
|
||||||
|
bool preambleCandidateFirstRiseValid = false;
|
||||||
|
|
||||||
|
#if defined(IR_EDGE_TRACE)
|
||||||
|
struct IrEdgeTraceRec
|
||||||
|
{
|
||||||
|
uint32_t t_us;
|
||||||
|
uint8_t level;
|
||||||
|
uint8_t flags;
|
||||||
|
};
|
||||||
|
void edgeTracePush(uint32_t t_us, uint8_t level, uint8_t flags);
|
||||||
|
IrEdgeTraceRec edgeTrace_buf[IR_EDGE_TRACE_CAPACITY]{};
|
||||||
|
volatile uint16_t edgeTrace_w = 0;
|
||||||
|
volatile uint16_t edgeTrace_r = 0;
|
||||||
|
volatile bool edgeTrace_overflow = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if IR_RX_BRIEF_LOG
|
||||||
|
volatile bool rxBriefMuteBeginPending = false;
|
||||||
|
volatile uint32_t rxBriefMuteBeginUs = 0;
|
||||||
|
volatile bool rxBriefMuteEndPending = false;
|
||||||
|
volatile uint32_t rxBriefMuteEndUs = 0;
|
||||||
|
volatile uint16_t rxBriefMuteEndCount = 0;
|
||||||
|
volatile uint16_t rxBriefMuteBlockedEdges = 0;
|
||||||
|
volatile uint16_t rxBriefRawOverflowDrops = 0;
|
||||||
|
volatile uint32_t rxBriefRawOverflowLastUs = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
uint8_t dataBuffer[dataByteSizeMax]{0}; // Буффер данных
|
uint8_t dataBuffer[dataByteSizeMax]{0}; // Буффер данных
|
||||||
@ -110,7 +227,6 @@ private:
|
|||||||
volatile uint32_t highTime;
|
volatile uint32_t highTime;
|
||||||
volatile uint32_t lowTime;
|
volatile uint32_t lowTime;
|
||||||
|
|
||||||
uint32_t oldTime;
|
|
||||||
uint16_t wrongCounter;
|
uint16_t wrongCounter;
|
||||||
|
|
||||||
int8_t highCount;
|
int8_t highCount;
|
||||||
@ -122,7 +238,25 @@ private:
|
|||||||
int16_t bufBitPos = 0; // Позиция для записи бита в буффер
|
int16_t bufBitPos = 0; // Позиция для записи бита в буффер
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void listenStart(); // @brief Слушатель для работы isReciving()
|
bool isReciveRaw = false;
|
||||||
|
void listenStart();
|
||||||
|
void checkTimeout(); //
|
||||||
|
/** В очередях/hold фильтра ещё есть фронты — не оценивать таймаут по micros()-lastEdgeTime (ложный TIMEOUT). */
|
||||||
|
bool rxTimeoutPipelineBusy() const;
|
||||||
|
void pulseFilterPushRaw(const FrontStorage &e);
|
||||||
|
bool pulseFilterTryTakeConfirmed(FrontStorage &out, uint32_t logTime = 0);
|
||||||
|
bool pulseFilterTryFlushOne(uint32_t nowUs, FrontStorage &out);
|
||||||
|
void pulseFilterShiftLeft(uint8_t n);
|
||||||
|
void pulseFilterReset();
|
||||||
|
void processDecodedFront(const FrontStorage ¤tFront);
|
||||||
|
static uint32_t absDiffU32(uint32_t a, uint32_t b);
|
||||||
|
bool registerPairMuteEncoder(IR_Encoder *enc);
|
||||||
|
void refreshPairMuteState();
|
||||||
|
uint32_t preambleJitterTolUs(uint32_t baselineUs) const;
|
||||||
|
bool preambleRisePeriodCoarseOk(uint32_t periodUs) const;
|
||||||
|
void preambleResetToIdle();
|
||||||
|
void preambleStartCandidate(const FrontStorage &front);
|
||||||
|
bool preambleProcessEdge(const FrontStorage &front);
|
||||||
|
|
||||||
/// @brief Проверка CRC. Проверяет len байт со значением crc, пришедшим в пакете
|
/// @brief Проверка CRC. Проверяет len байт со значением crc, пришедшим в пакете
|
||||||
/// @param len Длина в байтах проверяемых данных
|
/// @param len Длина в байтах проверяемых данных
|
||||||
@ -132,14 +266,14 @@ private:
|
|||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
bool isData = true; // Флаг относится ли бит к данным, или битам синхронизации
|
bool isData = true; // Флаг относится ли бит к данным, или битам синхронизации
|
||||||
uint16_t i_dataBuffer; // Счётчик буфера данных
|
uint16_t i_dataBuffer = 0; // Счётчик буфера данных
|
||||||
uint8_t nextControlBit = bitPerByte; // Метка для смены флага isData
|
uint16_t nextControlBit = bitPerByte; // Метка для смены флага isData; uint16_t нужен для длинных кадров (>24 байт total)
|
||||||
uint8_t i_syncBit; // Счётчик битов синхронизации
|
uint8_t i_syncBit = 0; // Счётчик битов синхронизации
|
||||||
uint8_t err_syncBit; // Счётчик ошибок синхронизации
|
uint8_t err_syncBit = 0; // Счётчик ошибок синхронизации
|
||||||
|
|
||||||
/// @brief Запиь бита в буффер, а так же проверка битов синхранизации и их фильтрация
|
/// @brief Запиь бита в буффер, а так же проверка битов синхранизации и их фильтрация
|
||||||
/// @param Бит данных
|
/// @param packTraceInvertFix если true — в IRDEBUG_SERIAL_PACK бит в трассе пишется как `0`/`1` (исправление по фронтам)
|
||||||
void writeToBuffer(bool);
|
void writeToBuffer(bool bit, bool packTraceInvertFix = false);
|
||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
void firstRX(); /// @brief Установка и сброс начальных значений и флагов в готовность к приёму данных
|
void firstRX(); /// @brief Установка и сброс начальных значений и флагов в готовность к приёму данных
|
||||||
@ -150,8 +284,47 @@ private:
|
|||||||
/// @return Результат
|
/// @return Результат
|
||||||
uint16_t ceil_div(uint16_t val, uint16_t divider);
|
uint16_t ceil_div(uint16_t val, uint16_t divider);
|
||||||
|
|
||||||
#if true //def IRDEBUG
|
// 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 rxBriefNoteMuteBlockedIsr(uint32_t tUs);
|
||||||
|
void rxBriefNoteRawOverflowIsr(uint32_t tUs);
|
||||||
|
void rxBriefFlushDeferredIsrLogs();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef IRDEBUG
|
||||||
|
uint32_t wrCounter;
|
||||||
inline void errPulse(uint8_t pin, uint8_t count);
|
inline void errPulse(uint8_t pin, uint8_t count);
|
||||||
inline void infoPulse(uint8_t pin, uint8_t count);
|
inline void infoPulse(uint8_t pin, uint8_t count);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(IRDEBUG_SERIAL_PACK)
|
||||||
|
static constexpr uint16_t kPackTraceBufCap =
|
||||||
|
uint16_t(dataByteSizeMax) * (uint16_t(bitPerByte) + uint16_t(syncBits)) + 48u;
|
||||||
|
|
||||||
|
void packTraceResetFrame();
|
||||||
|
void packTracePushBit(bool bit);
|
||||||
|
void packTracePushChar(char c);
|
||||||
|
/** Помечает в packTraceBitBuf бит (после BRUTEFORCE_CHECK) обёрткой `0`/`1` по финальному значению в dataBuffer. */
|
||||||
|
void packTraceWrapDataBitInBackticks(uint16_t byteIndex, uint8_t bitInByte);
|
||||||
|
/** IR hex: все байты dataBuffer[0 .. byteCount-1] в hex. */
|
||||||
|
void packTraceEmitHex(uint8_t byteCount) const;
|
||||||
|
/** IR raw: биты и синхра; тройной пробел между блоками msg/from/to/data/CRC; первый байт 3+пробел+5. endWithNewline — перевод строки после сырой строки. */
|
||||||
|
void packTraceEmitRawBitsLine(bool endWithNewline = true) const;
|
||||||
|
void packTraceEmitErrorFlash(const __FlashStringHelper *msg);
|
||||||
|
void packTraceEmitEndOk(uint8_t packSize);
|
||||||
|
void packTraceEmitEndBadCrc(uint8_t packSize);
|
||||||
|
void packTraceOnTimeoutOrAbort(bool fromListenStart);
|
||||||
|
void packTraceForceEndSyncPhase();
|
||||||
|
bool packTraceSoftReject() const;
|
||||||
|
|
||||||
|
bool packTraceOpen = false;
|
||||||
|
bool packTraceHadWrongSync = false;
|
||||||
|
char packTraceBitBuf[kPackTraceBufCap]{};
|
||||||
|
uint16_t packTraceLen = 0;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
1220
IR_Encoder.cpp
1220
IR_Encoder.cpp
File diff suppressed because it is too large
Load Diff
261
IR_Encoder.h
261
IR_Encoder.h
@ -1,75 +1,189 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "IR_config.h"
|
#include "IR_config.h"
|
||||||
|
#include "IrTxGateTypes.h"
|
||||||
|
|
||||||
// TODO: Отложенная передача после завершения приема
|
// TODO: Отложенная передача после завершения приема
|
||||||
|
|
||||||
|
enum class IR_SendStatus : uint8_t {
|
||||||
|
Success = 0,
|
||||||
|
PayloadTooLarge,
|
||||||
|
EncoderBusy,
|
||||||
|
BufferTooLarge,
|
||||||
|
ExternalBackendBusy,
|
||||||
|
ExternalStartFailed,
|
||||||
|
ExternalNoStream,
|
||||||
|
ExternalInvalidConfig,
|
||||||
|
BuildGateRunsFailed,
|
||||||
|
ScaleGateRunsFailed,
|
||||||
|
DmaStartFailed,
|
||||||
|
EncoderPinUnavailable,
|
||||||
|
BufferedStorageInvalid,
|
||||||
|
};
|
||||||
|
|
||||||
|
const char* irSendStatusToString(IR_SendStatus status);
|
||||||
|
|
||||||
|
// Структура для возврата результата отправки
|
||||||
|
struct IR_SendResult {
|
||||||
|
bool success; // Флаг успешности отправки
|
||||||
|
uint32_t sendTimeMs; // Время отправки пакета в миллисекундах
|
||||||
|
IR_SendStatus status; // Детализированный статус старта передачи
|
||||||
|
|
||||||
|
IR_SendResult(bool success = false,
|
||||||
|
uint32_t sendTimeMs = 0,
|
||||||
|
IR_SendStatus status = IR_SendStatus::ExternalStartFailed)
|
||||||
|
: success(success), sendTimeMs(sendTimeMs), status(status) {}
|
||||||
|
};
|
||||||
|
|
||||||
class IR_DecoderRaw;
|
class IR_DecoderRaw;
|
||||||
class IR_Encoder : IR_FOX
|
class IrTxIsrBufferedStorageBase;
|
||||||
|
class IR_Encoder : public IR_FOX
|
||||||
{
|
{
|
||||||
friend IR_DecoderRaw;
|
friend IR_DecoderRaw;
|
||||||
|
static IR_Encoder *head;
|
||||||
|
static IR_Encoder *last;
|
||||||
|
IR_Encoder *next;
|
||||||
public:
|
public:
|
||||||
private:
|
static HardwareTimer* IR_Timer;
|
||||||
uint16_t id; /// @brief Адрес передатчика
|
|
||||||
|
|
||||||
|
using IR_TxGateRun = IrTxGateRun;
|
||||||
|
enum class TxIsrMode : uint8_t {
|
||||||
|
Legacy = 0,
|
||||||
|
Buffered = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
using ExternalTxBusyFn = bool (*)(void *ctx);
|
||||||
|
using ExternalTxStartFn = IR_SendStatus (*)(void *ctx, IR_Encoder *enc, const uint8_t *packet, uint8_t len);
|
||||||
|
private:
|
||||||
|
// uint16_t id; /// @brief Адрес передатчика
|
||||||
public:
|
public:
|
||||||
/// @brief Класс передатчика
|
/// @brief Класс передатчика
|
||||||
/// @param addr Адрес передатчика
|
/// @param addr Адрес передатчика
|
||||||
/// @param pin Вывод передатчика
|
/// @param pin Вывод передатчика
|
||||||
/// @param tune Подстройка несущей частоты
|
/// @param decPair Если задан, конструктор регистрирует этот один приёмник как blind-decoder
|
||||||
/// @param decPair Приёмник, для которого отключается приём в момент передачи передатчиком
|
/// (аналог setBlindDecoders() для одного RX).
|
||||||
IR_Encoder(uint16_t addr, IR_DecoderRaw *decPair = nullptr);
|
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);
|
||||||
|
/**
|
||||||
|
* Глобальный знаменатель: частота таймера TX = carrierFrec × multiply (слотов на период несущей).
|
||||||
|
* По умолчанию multiply=2 (как бывшие carrierFrec×2). Задавать до begin/beginClockOnly либо после
|
||||||
|
* изменения вызвать retuneCarrierClock() (не менять multiply во время активной передачи).
|
||||||
|
*/
|
||||||
|
static void setCarrierMultiply(uint16_t multiply);
|
||||||
|
static uint16_t carrierMultiply();
|
||||||
|
/** Повторно применить carrierFrec×multiply к IR_Timer (pause + setOverflow), ISR не перенавешивает. */
|
||||||
|
static void retuneCarrierClock();
|
||||||
|
|
||||||
// static void timerSetup()
|
/** Максимальный числитель мощности: ⌊multiply/2⌋ (100% в setPowerPercent). */
|
||||||
// {
|
static uint16_t maxPowerNumerator();
|
||||||
// // // TIMER2 Ini
|
|
||||||
// // uint8_t oldSREG = SREG; // Save global interupts settings
|
|
||||||
// // cli();
|
|
||||||
// // // DDRB |= (1 << PORTB3); //OC2A (17)
|
|
||||||
// // TCCR2A = 0;
|
|
||||||
// // TCCR2B = 0;
|
|
||||||
|
|
||||||
// // // TCCR2A |= (1 << COM2A0); //Переключение состояния
|
/** Числитель N: при открытой огибающей N из multiply тиков HIGH за период несущей. Clamped к maxPowerNumerator(). */
|
||||||
|
void setPowerNumerator(uint16_t n);
|
||||||
|
uint16_t powerNumerator() const;
|
||||||
|
/** p∈[0,100] → ближайший допустимый числитель; 100% даёт N = maxPowerNumerator(). */
|
||||||
|
void setPowerPercent(uint8_t p);
|
||||||
|
|
||||||
// // TCCR2A |= (1 << WGM21); // Clear Timer On Compare (Сброс по совпадению)
|
/** Legacy helper: lenTicks в тактах 2×Fc → физические тики (carrierFrec×multiply). Может разбить сегменты. */
|
||||||
// // TCCR2B |= (1 << CS20); // Предделитель 1
|
static bool scaleGateRunsToPhysical(IR_TxGateRun* runs, size_t* ioCount, size_t maxRuns, uint16_t multiply);
|
||||||
// // TIMSK2 |= (1 << OCIE2A); // Прерывание по совпадению
|
|
||||||
|
|
||||||
// // OCR2A = /* 465 */ ((F_CPU / (38000 * 2)) - 2); // 38кГц
|
/** Configure timer frequency for TX clock (carrierFrec × multiply) without attaching ISR. */
|
||||||
|
static void beginClockOnly(HardwareTimer *timer);
|
||||||
|
static HardwareTimer* get_IR_Timer();
|
||||||
|
/** Call from main loop/tick: if ISR requested carrier stop, pause timer here (not in ISR). */
|
||||||
|
static void tick();
|
||||||
|
|
||||||
// // SREG = oldSREG; // Return interrupt settings
|
/**
|
||||||
|
* Режим внутреннего TX без DMA: false — BSRR + кольцо (direct physical gate-runs builder);
|
||||||
|
* true — FSM «налету» + скважность несущей как у буферного пути (подшаги multiply/2 на шаг FSM).
|
||||||
|
* По умолчанию включён legacy=true для обратной совместимости. Вызов меняет default и обновляет
|
||||||
|
* все зарегистрированные encoder-объекты. Buffered ISR реально используется только если у encoder
|
||||||
|
* привязан storage через attachBufferedIsrStorage()/enableBufferedIsr().
|
||||||
|
* Выставить до begin/rawSend. Игнорируется при externalTxStartFn.
|
||||||
|
*/
|
||||||
|
static void setTxIsrLegacyMode(bool legacy);
|
||||||
|
static bool txIsrLegacyMode();
|
||||||
|
void attachBufferedIsrStorage(IrTxIsrBufferedStorageBase& storage);
|
||||||
|
void detachBufferedIsrStorage();
|
||||||
|
bool hasBufferedIsrStorage() const;
|
||||||
|
void enableBufferedIsr(IrTxIsrBufferedStorageBase& storage);
|
||||||
|
void disableBufferedIsr();
|
||||||
|
TxIsrMode txIsrMode() const;
|
||||||
|
|
||||||
|
/** Optional: register external TX backend (e.g. DMA driver). */
|
||||||
|
static void setExternalTxBackend(ExternalTxStartFn startFn, ExternalTxBusyFn busyFn, void *ctx);
|
||||||
|
|
||||||
// }
|
/** Called by external TX backend on actual end of transmission. */
|
||||||
// static void timerOFFSetup()
|
void externalFinishSend();
|
||||||
// {
|
|
||||||
// TIMSK2 &= ~(1 << OCIE2A); // Прерывание по совпадению выкл
|
/** Build RLE runs of carrier gate for a packet in logical 2×Fc ticks (no HW access). */
|
||||||
// }
|
static size_t buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns);
|
||||||
|
/** Build RLE runs directly in physical carrierFrec×multiply ticks (DMA/buffered ISR path). */
|
||||||
|
static size_t buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns, uint16_t multiply);
|
||||||
|
|
||||||
|
void enable();
|
||||||
|
void disable();
|
||||||
|
|
||||||
void setBlindDecoders(IR_DecoderRaw *decoders[], uint8_t count);
|
void setBlindDecoders(IR_DecoderRaw *decoders[], uint8_t count);
|
||||||
void rawSend(uint8_t *ptr, uint8_t len);
|
template <size_t N>
|
||||||
|
void setBlindDecoders(IR_DecoderRaw *(&decoders)[N])
|
||||||
|
{
|
||||||
|
static_assert(N <= IR_PAIR_MUTE_MAX_ENCODERS,
|
||||||
|
"IR_Encoder::setBlindDecoders: array size exceeds IR_PAIR_MUTE_MAX_ENCODERS");
|
||||||
|
setBlindDecoders(decoders, static_cast<uint8_t>(N));
|
||||||
|
}
|
||||||
|
IR_SendStatus rawSend(uint8_t *ptr, uint8_t len);
|
||||||
|
|
||||||
void sendData(uint16_t addrTo, uint8_t dataByte, bool needAccept = false);
|
IR_SendResult sendData(uint16_t addrTo, uint8_t dataByte, bool needAccept = false);
|
||||||
void sendData(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false);
|
IR_SendResult sendData(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false);
|
||||||
|
IR_SendResult sendDataFULL(uint16_t addrFrom, uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false);
|
||||||
|
|
||||||
void sendAccept(uint16_t addrTo, uint8_t customByte = 0);
|
|
||||||
void sendRequest(uint16_t addrTo);
|
|
||||||
|
|
||||||
void sendBack(uint8_t data);
|
IR_SendResult sendAccept(uint16_t addrTo, uint8_t customByte = 0);
|
||||||
void sendBack(uint8_t *data = nullptr, uint8_t len = 0);
|
IR_SendResult sendRequest(uint16_t addrTo);
|
||||||
void sendBackTo(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0);
|
|
||||||
|
IR_SendResult sendBack(uint8_t data);
|
||||||
|
IR_SendResult sendBack(uint8_t *data = nullptr, uint8_t len = 0);
|
||||||
|
IR_SendResult sendBackTo(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0);
|
||||||
|
|
||||||
|
// Функция для тестирования времени отправки без фактической отправки
|
||||||
|
uint32_t testSendTime(uint16_t addrTo, uint8_t dataByte, bool needAccept = false) const;
|
||||||
|
uint32_t testSendTime(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false) const;
|
||||||
|
uint32_t testSendTimeFULL(uint16_t addrFrom, uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false) const;
|
||||||
|
uint32_t testSendAccept(uint16_t addrTo, uint8_t customByte = 0) const;
|
||||||
|
uint32_t testSendRequest(uint16_t addrTo) const;
|
||||||
|
uint32_t testSendBack(uint8_t data) const;
|
||||||
|
uint32_t testSendBack(uint8_t *data = nullptr, uint8_t len = 0) const;
|
||||||
|
uint32_t testSendBackTo(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0) const;
|
||||||
|
|
||||||
|
inline bool isBusy() const { return isSending;}
|
||||||
|
|
||||||
void isr();
|
|
||||||
|
|
||||||
~IR_Encoder();
|
~IR_Encoder();
|
||||||
volatile bool ir_out_virtual;
|
volatile bool ir_out_virtual;
|
||||||
|
|
||||||
|
void _isr();
|
||||||
private:
|
private:
|
||||||
void _sendBack(bool isAdressed, uint16_t addrTo, uint8_t *data, uint8_t len);
|
static volatile bool carrierStopPending;
|
||||||
|
static bool txIsrLegacyMode_;
|
||||||
|
static uint16_t s_carrierMultiply;
|
||||||
|
static void carrierResume();
|
||||||
|
static void carrierPauseIfIdle();
|
||||||
|
|
||||||
void setDecoder_isSending();
|
static ExternalTxStartFn externalTxStartFn;
|
||||||
|
static ExternalTxBusyFn externalTxBusyFn;
|
||||||
|
static void *externalTxCtx;
|
||||||
|
IR_SendResult _sendBack(bool isAdressed, uint16_t addrTo, uint8_t *data, uint8_t len);
|
||||||
|
|
||||||
|
void refreshBlindDecoderMuteState();
|
||||||
|
void registerWithBlindDecoders();
|
||||||
void sendByte(uint8_t byte, bool *prev, bool LOW_FIRST);
|
void sendByte(uint8_t byte, bool *prev, bool LOW_FIRST);
|
||||||
void addSync(bool *prev, bool *next);
|
void addSync(bool *prev, bool *next);
|
||||||
|
uint32_t calculateSendTime(uint8_t packSize) const;
|
||||||
|
uint32_t testSendBack(bool isAdressed, uint16_t addrTo, uint8_t *data, uint8_t len) const;
|
||||||
void send_HIGH(bool = 1);
|
void send_HIGH(bool = 1);
|
||||||
void send_LOW();
|
void send_LOW();
|
||||||
void send_EMPTY(uint8_t count);
|
void send_EMPTY(uint8_t count);
|
||||||
@ -82,34 +196,73 @@ private:
|
|||||||
sync = 3
|
sync = 3
|
||||||
};
|
};
|
||||||
|
|
||||||
IR_DecoderRaw *decPair;
|
struct TxFsmState
|
||||||
IR_DecoderRaw **blindDecoders;
|
{
|
||||||
uint8_t decodersCount;
|
uint8_t sendLen = 0;
|
||||||
|
uint8_t toggleCounter = 0;
|
||||||
|
uint8_t dataBitCounter = 0;
|
||||||
|
uint8_t dataByteCounter = 0;
|
||||||
|
uint8_t preambFrontCounter = 0;
|
||||||
|
uint8_t dataSequenceCounter = 0;
|
||||||
|
uint8_t syncSequenceCounter = 0;
|
||||||
|
bool syncLastBit = false;
|
||||||
|
bool state = LOW;
|
||||||
|
uint8_t *currentBitSequence = nullptr;
|
||||||
|
SignalPart signal = noSignal;
|
||||||
|
};
|
||||||
|
|
||||||
uint8_t sendLen;
|
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 <typename Emit>
|
||||||
|
static bool txWalkRuns(TxFsmState &st, const uint8_t *sendBufferLocal, Emit emit);
|
||||||
|
void loadTxFsmFromMembers(TxFsmState &st) const;
|
||||||
|
void storeTxFsmToMembers(const TxFsmState &st);
|
||||||
|
bool shouldUseBufferedIsr() const;
|
||||||
|
|
||||||
|
/** Снимок на старт TX (буферный и legacy путь). */
|
||||||
|
uint16_t txPowerSnap_ = 1;
|
||||||
|
uint16_t txMultiplySnap_ = 2;
|
||||||
|
|
||||||
|
/** Legacy: физических тиков на один логический шаг FSM = multiply/2. */
|
||||||
|
uint16_t legacyPhysPerLogical_ = 1;
|
||||||
|
uint16_t legacyPhysCounter_ = 0;
|
||||||
|
uint16_t legacySlotInPeriod_ = 0;
|
||||||
|
|
||||||
|
volatile uint16_t powerNumerator_ = 1;
|
||||||
|
IrTxIsrBufferedStorageBase* txBufferedCtx_ = nullptr;
|
||||||
|
IrTxIsrBufferedStorageBase* txActiveBufferedCtx_ = nullptr;
|
||||||
|
TxIsrMode txIsrMode_ = TxIsrMode::Legacy;
|
||||||
|
bool txUseBufferedIsr_ = false;
|
||||||
|
|
||||||
|
IR_DecoderRaw *decPair = nullptr;
|
||||||
|
IR_DecoderRaw *singleBlindDecoder = nullptr;
|
||||||
|
IR_DecoderRaw **blindDecoders = nullptr;
|
||||||
|
uint8_t decodersCount = 0;
|
||||||
|
|
||||||
|
uint8_t sendLen = 0;
|
||||||
uint8_t sendBuffer[dataByteSizeMax]{0}; /// @brief Буффер данных для отправки
|
uint8_t sendBuffer[dataByteSizeMax]{0}; /// @brief Буффер данных для отправки
|
||||||
|
|
||||||
volatile bool isSending;
|
volatile bool isSending = false;
|
||||||
volatile bool state; /// @brief Текущий уровень генерации
|
volatile bool state = LOW; /// @brief Текущий уровень генерации
|
||||||
|
|
||||||
volatile uint8_t dataByteCounter;
|
volatile uint8_t dataByteCounter = 0;
|
||||||
|
|
||||||
volatile uint8_t toggleCounter; /// @brief Счётчик переключений
|
volatile uint8_t toggleCounter = 0; /// @brief Счётчик переключений
|
||||||
volatile uint8_t dataBitCounter;
|
volatile uint8_t dataBitCounter = 0;
|
||||||
|
|
||||||
volatile uint8_t preambFrontCounter;
|
volatile uint8_t preambFrontCounter = 0;
|
||||||
volatile uint8_t dataSequenceCounter;
|
volatile uint8_t dataSequenceCounter = 0;
|
||||||
volatile uint8_t syncSequenceCounter;
|
volatile uint8_t syncSequenceCounter = 0;
|
||||||
volatile bool syncLastBit;
|
volatile bool syncLastBit = false;
|
||||||
|
|
||||||
struct BitSequence
|
struct BitSequence
|
||||||
{
|
{
|
||||||
uint8_t low;
|
uint8_t low;
|
||||||
uint8_t high;
|
uint8_t high;
|
||||||
};
|
};
|
||||||
static uint8_t *bitHigh;
|
static uint8_t bitHigh[2];
|
||||||
static uint8_t *bitLow;
|
static uint8_t bitLow[2];
|
||||||
uint8_t *currentBitSequence = bitLow;
|
uint8_t *currentBitSequence = bitLow;
|
||||||
volatile SignalPart signal;
|
volatile SignalPart signal = noSignal;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
33
IR_config.cpp
Normal file
33
IR_config.cpp
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#include "IR_config.h"
|
||||||
|
|
||||||
|
void IR_FOX::setPin(uint8_t pin){
|
||||||
|
this->pin = pin;
|
||||||
|
port = digitalPinToPort(pin);
|
||||||
|
mask = digitalPinToBitMask(pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IR_FOX::checkAddressRuleApply(uint16_t address, uint16_t id, bool &flag)
|
||||||
|
{
|
||||||
|
flag = false;
|
||||||
|
flag |= id == 0;
|
||||||
|
flag |= address == id;
|
||||||
|
flag |= address >= IR_Broadcast;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t IR_FOX::crc8(uint8_t *data, uint8_t start, uint8_t end, uint8_t poly)
|
||||||
|
{ // TODO: сделать возможность межбайтовой проверки
|
||||||
|
uint8_t crc = 0xff;
|
||||||
|
size_t i, j;
|
||||||
|
for (i = start; i < end; i++)
|
||||||
|
{
|
||||||
|
crc ^= data[i];
|
||||||
|
for (j = 0; j < 8; j++)
|
||||||
|
{
|
||||||
|
if ((crc & 0x80) != 0)
|
||||||
|
crc = (uint8_t)((crc << 1) ^ poly);
|
||||||
|
else
|
||||||
|
crc <<= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
};
|
||||||
260
IR_config.h
260
IR_config.h
@ -1,25 +1,87 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
#include <list>
|
||||||
|
// #define IRDEBUG_INFO
|
||||||
|
|
||||||
#define IRDEBUG_INFO
|
/** Число потоков 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. */
|
||||||
|
constexpr size_t kIsrTxMaxGateRuns = 512U;
|
||||||
|
static_assert((kIsrTxBsrrWordCount & 1U) == 0U, "kIsrTxBsrrWordCount must be even");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пошаговый разбор кадра на Serial (по умолчанию выключено). Пульсы IRDEBUG на пинах не меняют.
|
||||||
|
// #define IRDEBUG_SERIAL_PACK
|
||||||
|
// Не обрывать приём сразу при накопленной sync-ошибке — «дописывать» до таймаута (только вместе с IRDEBUG_SERIAL_PACK).
|
||||||
|
// #define IRDEBUG_SERIAL_SOFT_REJECT
|
||||||
|
|
||||||
|
// Краткий лог причин, почему физический сигнал не дошёл до распознанного пакета.
|
||||||
|
// Формат и коды: ref/IR_RX_BRIEF_LOG.md
|
||||||
|
#ifndef IR_RX_BRIEF_LOG
|
||||||
|
#define IR_RX_BRIEF_LOG 0
|
||||||
|
#endif
|
||||||
|
// 1: печатать только отклонённые/ошибочные события; успехи и шумовые PREAMB скрыть.
|
||||||
|
#ifndef IR_RX_BRIEF_LOG_REJECT_ONLY
|
||||||
|
#define IR_RX_BRIEF_LOG_REJECT_ONLY 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Журнал фронтов ИК в ISR; сброс строк @IRF1v1: в IR_DecoderRaw::tick(). См. ref/IR_EDGE_TRACE_FORMAT.md
|
||||||
|
// Расход RAM ≈ IR_EDGE_TRACE_CAPACITY * 6 байт на декодер. Выключить — закомментировать:
|
||||||
|
// #define IR_EDGE_TRACE
|
||||||
|
#if defined(IR_EDGE_TRACE)
|
||||||
|
#ifndef IR_EDGE_TRACE_CAPACITY
|
||||||
|
#define IR_EDGE_TRACE_CAPACITY 512u
|
||||||
|
#endif
|
||||||
|
/** Запись в edgeTrace: фронт не передан в decode (isPairSending). */
|
||||||
|
#define IR_EDGE_TRACE_F_SKIP_DECODE 0x01u
|
||||||
|
#endif
|
||||||
|
// Пример в скетче: void irPackTracePrintOkCommand(const uint8_t* b, uint8_t n) { Serial.print(F("CarCmd::...")); }
|
||||||
|
#if defined(IRDEBUG_SERIAL_PACK)
|
||||||
|
/** Слабая реализация в IR_DecoderRaw.cpp; в скетче определите свою для вывода вроде CarCmd::... */
|
||||||
|
void irPackTracePrintOkCommand(const uint8_t *buf, uint8_t packSize);
|
||||||
|
#endif
|
||||||
/*//////////////////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
Для работы в паре положить декодер в энкодер
|
Для работы в паре положить декодер в энкодер
|
||||||
|
|
||||||
*/// Адресация с 1 до 65 499
|
*/
|
||||||
#define IR_Broadcast 65000 // 65 500 ~ 65 535 - широковещательные пакеты (всем), возможно разделить на 35 типов
|
// Адресация с 1 до 65 499
|
||||||
|
#define IR_Broadcast 65000 // 65 500 ~ 65 535 - широковещательные пакеты (всем)
|
||||||
/*
|
/*
|
||||||
|
*Адресное пространство:
|
||||||
Адрес 0 запрещен и зарезервирован под NULL, либо тесты
|
Адрес 0 запрещен и зарезервирован под NULL, либо тесты
|
||||||
IR_MSG_ACCEPT с адреса 0 воспринимается всеми устройствами
|
IR_MSG_ACCEPT с адреса 0 воспринимается всеми устройствами
|
||||||
|
*/
|
||||||
|
//**** Контрольные точки ******
|
||||||
|
#define IR_MAX_ADDR_CPU 63999
|
||||||
|
#define IR_MIN_ADDR_CPU 32000
|
||||||
|
|
||||||
|
// //***** Группы машинок ********
|
||||||
|
// #define IR_MAX_CAR_GROUP 31999
|
||||||
|
// #define IR_MIN_CAR_GROUP 30000
|
||||||
|
|
||||||
Адресное пространство:
|
// //********** FREE *************
|
||||||
|
// #define IR_MAX_FREE 31999
|
||||||
Излучатели контрольных точек: 1000 ~ 1999
|
// #define IR_MIN_FREE 2000
|
||||||
Излучатели без обратной связиЖ 2000 ~ 2999
|
|
||||||
Излучатели светофоров: 3000 ~ 3999
|
|
||||||
|
|
||||||
|
//********* Машинки ***********
|
||||||
|
#define IR_MAX_CAR 31999
|
||||||
|
#define IR_MIN_CAR 1
|
||||||
|
|
||||||
|
//***** Пульты управления *****
|
||||||
|
#define IR_MAX_CONTROLLER 64999
|
||||||
|
#define IR_MIN_CONTROLLER 64000
|
||||||
|
/*
|
||||||
|
|
||||||
/```````````````````````````````````````````````` data pack `````````````````````````````````````````````\
|
/```````````````````````````````````````````````` data pack `````````````````````````````````````````````\
|
||||||
|
|
||||||
@ -34,18 +96,19 @@ IR_MSG_ACCEPT с адреса 0 воспринимается всеми устр
|
|||||||
\____________________________________________________________________________________________________/
|
\____________________________________________________________________________________________________/
|
||||||
|
|
||||||
msg type:
|
msg type:
|
||||||
// __________
|
// __________
|
||||||
// | 01234567 |
|
// | 01234567 |
|
||||||
// ----------
|
// ----------
|
||||||
// | xxx..... | = тип сообщения
|
// | xxx..... | = тип сообщения (биты 7..5)
|
||||||
// | ...xxxxx | = длина (максимум 31 бита)
|
// | ...xxxxx | = полная длина кадра в байтах (5 бит, 0..31, IR_MASK_MSG_INFO), не «31 бит» и не отдельный лимит «24 байта»
|
||||||
// ---------- */
|
// Полезная нагрузка в data pack: до bytePerPack байт (см. #define bytePerPack).
|
||||||
|
// ---------- */
|
||||||
#define IR_MSG_BACK 0U // | 000...... | = Задний сигнал машинки
|
#define IR_MSG_BACK 0U // | 000...... | = Задний сигнал машинки
|
||||||
#define IR_MSG_ACCEPT 1U // | 001..... | = подтверждение
|
#define IR_MSG_ACCEPT 1U // | 001..... | = подтверждение
|
||||||
#define IR_MSG_REQUEST 2U // | 010..... | = запрос
|
#define IR_MSG_REQUEST 2U // | 010..... | = запрос
|
||||||
#define IR_MSG_ 3U // | 011..... | = ??
|
// #define IR_MSG_ 3U // | 011..... | = ??
|
||||||
#define IR_MSG_BACK_TO 4U // | 100..... | = Задний сигнал машинки c адресацией
|
#define IR_MSG_BACK_TO 4U // | 100..... | = Задний сигнал машинки c адресацией
|
||||||
#define IR_MSG_ 5U // | 101..... | = ??
|
// #define IR_MSG_ 5U // | 101..... | = ??
|
||||||
#define IR_MSG_DATA_NOACCEPT 6U // | 110..... | = данные, не требующие подтверждения
|
#define IR_MSG_DATA_NOACCEPT 6U // | 110..... | = данные, не требующие подтверждения
|
||||||
#define IR_MSG_DATA_ACCEPT 7U // | 111..... | = данные требующие подтверждения
|
#define IR_MSG_DATA_ACCEPT 7U // | 111..... | = данные требующие подтверждения
|
||||||
; /* // ----------
|
; /* // ----------
|
||||||
@ -67,12 +130,13 @@ customByte - контрольная сумма принятых данных п
|
|||||||
|
|
||||||
|
|
||||||
/`````````````````````` Задний сигнал машинки без адресации ``````````````````````\
|
/`````````````````````` Задний сигнал машинки без адресации ``````````````````````\
|
||||||
|
// Первый байт: (IR_MSG_BACK<<5) | (packSize & IR_MASK_MSG_INFO) — как у data pack (тип + длина 0..31).
|
||||||
|
|
||||||
{``````````} [````````````````````````] [````````````````````````] [``````````````]
|
{``````````} [````````````````````````] [````````````````````````] [``````````````]
|
||||||
{ msg type } [ addr_from uint16_t ] [====== data bytes ======] [ CRC Bytes ]
|
{ msg type } [ addr_from uint16_t ] [====== data bytes ======] [ CRC Bytes ]
|
||||||
{..........} [........................] [........................] [..............]
|
{..........} [........................] [........................] [..............]
|
||||||
|
|
||||||
{ 0000xxxx } [addr_from_H][addr_from_L] [data_H][data_n..][data_L] [ crc1 ][ crc2 ]
|
{ xxx..|..xxxxx } [addr_from_H][addr_from_L] [data_H][data_n..][data_L] [ crc1 ][ crc2 ]
|
||||||
| 0 1 2 3 | |
|
| 0 1 2 3 | |
|
||||||
\_____________________________________________________________________/ |
|
\_____________________________________________________________________/ |
|
||||||
| |
|
| |
|
||||||
@ -81,12 +145,13 @@ customByte - контрольная сумма принятых данных п
|
|||||||
|
|
||||||
|
|
||||||
/```````````````````````````````````` Задний сигнал машинки с адресацией ````````````````````````````````````\
|
/```````````````````````````````````` Задний сигнал машинки с адресацией ````````````````````````````````````\
|
||||||
|
// Первый байт: (IR_MSG_BACK_TO<<5) | (packSize & IR_MASK_MSG_INFO) — IR_MSG_BACK_TO в битах 7..5, длина 0..31.
|
||||||
|
|
||||||
{``````````} [````````````````````````] [````````````````````````] [````````````````````````] [``````````````]
|
{``````````} [````````````````````````] [````````````````````````] [````````````````````````] [``````````````]
|
||||||
{ msg type } [ addr_from uint16_t ] [ addr_to uint16_t ] [====== data bytes ======] [ CRC Bytes ]
|
{ msg type } [ addr_from uint16_t ] [ addr_to uint16_t ] [====== data bytes ======] [ CRC Bytes ]
|
||||||
{..........} [........................] [........................] [........................] [..............]
|
{..........} [........................] [........................] [........................] [..............]
|
||||||
|
|
||||||
{ 0001xxxx } [addr_from_H][addr_from_L] [addr_from_H][addr_from_L] [data_H][data_n..][data_L] [ crc1 ][ crc2 ]
|
{ xxx..|..xxxxx } [addr_from_H][addr_from_L] [addr_to_H][addr_to_L] [data_H][data_n..][data_L] [ crc1 ][ crc2 ]
|
||||||
| 0 1 2 3 4 5 | |
|
| 0 1 2 3 4 5 | |
|
||||||
\________________________________________________________________________________________________/ |
|
\________________________________________________________________________________________________/ |
|
||||||
| |
|
| |
|
||||||
@ -101,20 +166,80 @@ customByte - контрольная сумма принятых данных п
|
|||||||
/////////////////////////////////////////////////////////////////////////////////////*/
|
/////////////////////////////////////////////////////////////////////////////////////*/
|
||||||
typedef uint16_t crc_t;
|
typedef uint16_t crc_t;
|
||||||
|
|
||||||
#define BRUTEFORCE_CHECK // Перепроверяет пакет на 1 битные ошибки //TODO: зависает
|
// #define BRUTEFORCE_CHECK // Перепроверяет пакет на 1 битные ошибки //TODO: зависает
|
||||||
#define bytePerPack 16 // колличество байтов в пакете
|
#define bytePerPack (31) // колличество байтов в пакете
|
||||||
#ifndef freeFrec
|
#ifndef freeFrec
|
||||||
#define freeFrec false
|
#define freeFrec false
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef subBufferSize
|
#ifndef subBufferSize
|
||||||
#define subBufferSize 5 //Буфер для складирования фронтов, пока их не обработают (передатчик)
|
#define subBufferSize 250 // Буфер для складирования фронтов, пока их не обработают (передатчик)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Максимальное число передатчиков, способных временно заглушить один декодер. */
|
||||||
|
#ifndef IR_PAIR_MUTE_MAX_ENCODERS
|
||||||
|
#define IR_PAIR_MUTE_MAX_ENCODERS 8U
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Минимальная длительность удержания уровня (мкс): короче — импульс/пара фронтов выкидывается до tick()
|
||||||
|
* (иголки на плато, дребезг). 0 — фильтр выключен, фронты идут в декодер как с ISR. */
|
||||||
|
#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
|
||||||
|
#endif
|
||||||
|
/** Если новых фронтов нет, через minPulse*mult держатель принудительно сбрасывается в декодер. */
|
||||||
|
#ifndef IR_INPUT_FILTER_TIMEOUT_MULT
|
||||||
|
#define IR_INPUT_FILTER_TIMEOUT_MULT 5U
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Синхронно с IrFoxProtocolConstants.h / IrFoxDecoder (плагин Saleae). */
|
||||||
|
#ifndef IR_SHORT_LOW_GLITCH_REJECT
|
||||||
|
#define IR_SHORT_LOW_GLITCH_REJECT 1
|
||||||
|
#endif
|
||||||
|
#ifndef IR_GLITCH_REJECT_PHASE_NUDGE
|
||||||
|
#define IR_GLITCH_REJECT_PHASE_NUDGE 1
|
||||||
|
#endif
|
||||||
|
#ifndef IR_MICRO_GAP_RISE_REJECT
|
||||||
|
#define IR_MICRO_GAP_RISE_REJECT 1
|
||||||
|
#endif
|
||||||
|
/** Лок преамбулы: сколько одинаковых подряд периодов подъёма нужно для старта кадра. */
|
||||||
|
#ifndef IR_PREAMBLE_LOCK_RISE_PERIODS
|
||||||
|
#define IR_PREAMBLE_LOCK_RISE_PERIODS 2U
|
||||||
|
#endif
|
||||||
|
/** Допуск одинаковости периода преамбулы (проценты) + минимальная абсолютная полка в мкс. */
|
||||||
|
#ifndef IR_PREAMBLE_JITTER_PCT
|
||||||
|
#define IR_PREAMBLE_JITTER_PCT 18U
|
||||||
|
#endif
|
||||||
|
#ifndef IR_PREAMBLE_JITTER_US_MIN
|
||||||
|
#define IR_PREAMBLE_JITTER_US_MIN 80U
|
||||||
|
#endif
|
||||||
|
/** Грубое окно валидности периода преамбулы RISE->RISE (в процентах от bitTime).
|
||||||
|
* Для текущего протокола преамбула заметно длиннее обычного битового периода. */
|
||||||
|
#ifndef IR_PREAMBLE_PERIOD_MIN_FACTOR_PCT
|
||||||
|
#define IR_PREAMBLE_PERIOD_MIN_FACTOR_PCT 220U
|
||||||
|
#endif
|
||||||
|
#ifndef IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT
|
||||||
|
#define IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT 340U
|
||||||
|
#endif
|
||||||
|
/** Таймаут окна кандидата преамбулы: IR_timeout * mult. Кандидат без фронтов дольше таймаута байта
|
||||||
|
преамбулой быть не может; при 3× линия считалась занятой (rxLineActive) ещё 45 мс после последнего
|
||||||
|
паразитного фронта (напр. засветка своим дальномером) и откладывала передачу. */
|
||||||
|
#ifndef IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT
|
||||||
|
#define IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT 1U
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define preambPulse 3
|
#define preambPulse 3
|
||||||
|
|
||||||
#define disablePairDec false // Отключать парный приёмник, возможны баги, используйте setBlindDecoders()
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
#define bitPerByte 8U // Колличество бит в байте
|
#define bitPerByte 8U // Колличество бит в байте
|
||||||
@ -140,9 +265,43 @@ typedef uint16_t crc_t;
|
|||||||
#define bitTakts (bitActiveTakts + bitPauseTakts) // Общая длительность бита в тактах
|
#define bitTakts (bitActiveTakts + bitPauseTakts) // Общая длительность бита в тактах
|
||||||
#define bitTime (bitTakts * carrierPeriod) // Общая длительность бита
|
#define bitTime (bitTakts * carrierPeriod) // Общая длительность бита
|
||||||
#define tolerance 300U
|
#define tolerance 300U
|
||||||
class IR_FOX {
|
|
||||||
|
// ---- Длительности и размеры кадра ФОРМУЛАМИ из 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);
|
||||||
|
/// Таймаут байта декодера (как 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).
|
||||||
|
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);
|
||||||
|
constexpr uint16_t test_low = ((bitPauseTakts / 2 + bitActiveTakts) * 2 - 0) + ((bitPauseTakts)-0);
|
||||||
|
|
||||||
|
class IR_FOX
|
||||||
|
{
|
||||||
public:
|
public:
|
||||||
struct PackOffsets {
|
struct PackOffsets
|
||||||
|
{
|
||||||
uint8_t msgOffset;
|
uint8_t msgOffset;
|
||||||
uint8_t addrFromOffset;
|
uint8_t addrFromOffset;
|
||||||
uint8_t addrToOffset;
|
uint8_t addrToOffset;
|
||||||
@ -150,21 +309,23 @@ public:
|
|||||||
uint8_t crcOffset;
|
uint8_t crcOffset;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ErrorsStruct {
|
struct ErrorsStruct
|
||||||
|
{
|
||||||
uint8_t lowSignal = 0;
|
uint8_t lowSignal = 0;
|
||||||
uint8_t highSignal = 0;
|
uint8_t highSignal = 0;
|
||||||
uint8_t other = 0;
|
uint8_t other = 0;
|
||||||
|
|
||||||
void reset() {
|
void reset()
|
||||||
|
{
|
||||||
lowSignal = 0;
|
lowSignal = 0;
|
||||||
highSignal = 0;
|
highSignal = 0;
|
||||||
other = 0;
|
other = 0;
|
||||||
}
|
}
|
||||||
uint16_t all() { return lowSignal + highSignal + other; }
|
uint16_t all() { return lowSignal + highSignal + other; }
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PackInfo {
|
struct PackInfo
|
||||||
|
{
|
||||||
uint8_t *buffer = nullptr;
|
uint8_t *buffer = nullptr;
|
||||||
uint8_t packSize = 0;
|
uint8_t packSize = 0;
|
||||||
uint16_t crc = 0;
|
uint16_t crc = 0;
|
||||||
@ -172,32 +333,19 @@ public:
|
|||||||
uint16_t rTime = 0;
|
uint16_t rTime = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
static void checkAddressRuleApply(uint16_t address, uint16_t id, bool& flag) {
|
inline uint16_t getId() const { return id; }
|
||||||
flag = false;
|
inline void setId(uint16_t id) { this->id = id; }
|
||||||
flag |= id == 0;
|
static void checkAddressRuleApply(uint16_t address, uint16_t id, bool &flag);
|
||||||
flag |= address == id;
|
void setPin(uint8_t pin);
|
||||||
flag |= address >= IR_Broadcast;
|
inline uint8_t getPin() { return pin; };
|
||||||
}
|
inline GPIO_TypeDef *getPort() const { return port; }
|
||||||
|
inline uint16_t getPinMask() const { return mask; }
|
||||||
|
|
||||||
uint16_t getId() { return id; }
|
|
||||||
void setId(uint16_t id) { this->id = id; }
|
|
||||||
|
|
||||||
uint16_t id;
|
|
||||||
protected:
|
protected:
|
||||||
|
uint16_t id;
|
||||||
|
uint8_t pin;
|
||||||
|
GPIO_TypeDef *port;
|
||||||
|
uint16_t mask;
|
||||||
ErrorsStruct errors;
|
ErrorsStruct errors;
|
||||||
uint8_t crc8(uint8_t* data, uint8_t start, uint8_t end, uint8_t poly) { //TODO: сделать возможность межбайтовой проверки
|
uint8_t crc8(uint8_t *data, uint8_t start, uint8_t end, uint8_t poly);
|
||||||
uint8_t crc = 0xff;
|
|
||||||
size_t i, j;
|
|
||||||
for (i = start; i < end; i++) {
|
|
||||||
crc ^= data[i];
|
|
||||||
for (j = 0; j < 8; j++) {
|
|
||||||
if ((crc & 0x80) != 0)
|
|
||||||
crc = (uint8_t)((crc << 1) ^ poly);
|
|
||||||
else
|
|
||||||
crc <<= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return crc;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
431
IrDmaTxStm32.h
Normal file
431
IrDmaTxStm32.h
Normal file
@ -0,0 +1,431 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IR_Encoder.h"
|
||||||
|
#include "IrTxBsrrWave.h"
|
||||||
|
|
||||||
|
#if defined(ARDUINO_ARCH_STM32) && defined(STM32G4xx)
|
||||||
|
|
||||||
|
#if defined(_MSC_VER)
|
||||||
|
#define IRPROTO_DMA_PRAGMA_MESSAGE(text) __pragma(message(text))
|
||||||
|
#else
|
||||||
|
#define IRPROTO_DMA_PRAGMA_MESSAGE(text) _Pragma(#text)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
IRPROTO_DMA_PRAGMA_MESSAGE(message("[IR-protocol] TX path available: built-in DMA"))
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <HardwareTimer.h>
|
||||||
|
|
||||||
|
#if defined(__GNUC__)
|
||||||
|
#define IR_DMA_TX_HOT __attribute__((always_inline)) inline
|
||||||
|
#else
|
||||||
|
#define IR_DMA_TX_HOT inline
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "stm32g4xx_hal.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* STM32G4: ИК TX через DMA в GPIO BSRR, такт от TIM UPDATE (carrierFrec × IR_Encoder::carrierMultiply()).
|
||||||
|
*
|
||||||
|
* Число слотов потоков — параметр шаблона (без макросов), например IrDmaTxStm32<2>.
|
||||||
|
* По умолчанию: IrDmaTxStm32<> ≡ irproto::kDefaultDmaTxMaxStreams (см. IR_config.h).
|
||||||
|
*
|
||||||
|
* Контракт: ref/IR_DMA_TX_backend.md
|
||||||
|
*/
|
||||||
|
template<size_t MaxStreams = irproto::kDefaultDmaTxMaxStreams>
|
||||||
|
class IrDmaTxStm32 {
|
||||||
|
static_assert(MaxStreams >= 1U, "IrDmaTxStm32: MaxStreams >= 1");
|
||||||
|
|
||||||
|
public:
|
||||||
|
struct StreamCfg {
|
||||||
|
DMA_Channel_TypeDef* instance = nullptr;
|
||||||
|
IRQn_Type irq = IRQn_Type(0);
|
||||||
|
uint32_t dmamuxRequest = 0;
|
||||||
|
IR_Encoder* enc = nullptr;
|
||||||
|
|
||||||
|
uint32_t* dmaWords = nullptr;
|
||||||
|
uint16_t dmaWordCount = 0;
|
||||||
|
|
||||||
|
IrTxGateRun* gateRuns = nullptr;
|
||||||
|
size_t maxGateRuns = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Config {
|
||||||
|
HardwareTimer* timer = nullptr;
|
||||||
|
uint8_t streamCount = 0;
|
||||||
|
StreamCfg streams[MaxStreams];
|
||||||
|
};
|
||||||
|
|
||||||
|
bool begin(const Config& cfg) {
|
||||||
|
cfg_ = cfg;
|
||||||
|
streamCount_ = cfg.streamCount;
|
||||||
|
|
||||||
|
if (cfg_.timer == nullptr || streamCount_ == 0) return false;
|
||||||
|
if (streamCount_ > MaxStreams) return false;
|
||||||
|
|
||||||
|
htim_ = cfg_.timer->getHandle();
|
||||||
|
if (htim_ == nullptr) return false;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < streamCount_; i++) {
|
||||||
|
const StreamCfg& sc = cfg_.streams[i];
|
||||||
|
if (sc.enc == nullptr || sc.instance == nullptr) return false;
|
||||||
|
if (sc.dmaWords == nullptr || sc.dmaWordCount < 2U) return false;
|
||||||
|
if ((sc.dmaWordCount & 1U) != 0U) return false;
|
||||||
|
if (sc.gateRuns == nullptr || sc.maxGateRuns == 0U) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
__HAL_RCC_DMA1_CLK_ENABLE();
|
||||||
|
__HAL_RCC_DMAMUX1_CLK_ENABLE();
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < streamCount_; i++) {
|
||||||
|
const StreamCfg& sc = cfg_.streams[i];
|
||||||
|
if (!initStream(streams_[i], sc)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static IrDmaTxStm32<MaxStreams>* instance() {
|
||||||
|
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 {
|
||||||
|
for (uint8_t i = 0; i < streamCount_; i++) {
|
||||||
|
if (streams_[i].active) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
IR_SendStatus start(IR_Encoder* enc, const uint8_t* packet, uint8_t len) {
|
||||||
|
if (enc == nullptr) return IR_SendStatus::ExternalNoStream;
|
||||||
|
for (uint8_t i = 0; i < streamCount_; i++) {
|
||||||
|
if (streams_[i].enc == enc) {
|
||||||
|
return startStream(streams_[i], packet, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return IR_SendStatus::ExternalNoStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
void irqForStream(size_t streamIndex) {
|
||||||
|
if (streamIndex >= streamCount_) return;
|
||||||
|
HAL_DMA_IRQHandler(&streams_[streamIndex].hdma);
|
||||||
|
}
|
||||||
|
|
||||||
|
DMA_HandleTypeDef* dmaHandle(size_t streamIndex) {
|
||||||
|
if (streamIndex >= streamCount_) return nullptr;
|
||||||
|
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_; }
|
||||||
|
/** Сколько раз DMA-передача завершилась по Transfer-Error (наблюдаемость/логи). */
|
||||||
|
uint32_t errorCount() const { return errors_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct TxStream {
|
||||||
|
DMA_HandleTypeDef hdma{};
|
||||||
|
DMA_Channel_TypeDef* dmaInstance = nullptr;
|
||||||
|
IRQn_Type dmaIrq = IRQn_Type(0);
|
||||||
|
uint32_t dmamuxRequest = 0;
|
||||||
|
|
||||||
|
IR_Encoder* enc = nullptr;
|
||||||
|
GPIO_TypeDef* port = nullptr;
|
||||||
|
uint16_t mask = 0;
|
||||||
|
uint32_t setWord = 0;
|
||||||
|
uint32_t resetWord = 0;
|
||||||
|
|
||||||
|
uint32_t* dmaBuf = nullptr;
|
||||||
|
uint16_t bufLen = 0;
|
||||||
|
uint16_t halfLen = 0;
|
||||||
|
|
||||||
|
IrTxGateRun* runs = nullptr;
|
||||||
|
size_t maxRuns = 0;
|
||||||
|
|
||||||
|
size_t runCount = 0;
|
||||||
|
IrTxBsrrWave wave{};
|
||||||
|
|
||||||
|
uint32_t totalTicks = 0;
|
||||||
|
volatile uint32_t ticksOutput = 0;
|
||||||
|
|
||||||
|
// Fix D (watchdog): прогресс ticksOutput против стенных часов (контекст потока).
|
||||||
|
uint32_t lastTicks = 0;
|
||||||
|
uint32_t lastProgressMs = 0;
|
||||||
|
|
||||||
|
volatile bool active = false; // Fix: делится поток<->ISR (busy()/poll()/колбэки/спин applyCarrierMultiply) — запрет кэширования компилятором
|
||||||
|
|
||||||
|
void resetWave() {
|
||||||
|
wave.configure(setWord, resetWord, nullptr, 0, 2, 1);
|
||||||
|
ticksOutput = 0;
|
||||||
|
totalTicks = 0;
|
||||||
|
runCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
IR_DMA_TX_HOT void fill(uint32_t* dst, uint16_t count) {
|
||||||
|
wave.fill(dst, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
static IrDmaTxStm32<MaxStreams>* s_instance;
|
||||||
|
|
||||||
|
Config cfg_{};
|
||||||
|
TIM_HandleTypeDef* htim_ = nullptr;
|
||||||
|
|
||||||
|
TxStream streams_[MaxStreams]{};
|
||||||
|
uint8_t streamCount_ = 0;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix C: TIM держим free-running. Включаем ИДЕМПОТЕНТНО при старте потока и НИКОГДА не
|
||||||
|
// останавливаем по разделяемому счётчику (счётчик убран → гонка поток↔ISR устранена).
|
||||||
|
// Счётчик TIM НЕ сбрасываем: сброс глитчил бы уже активный поток; для ИК ≤1 период несущей
|
||||||
|
// фазового сдвига пренебрежим. В простое (ни один DMA-канал не заармлен) update-события TIM
|
||||||
|
// генерят DMA-запросы «в никуда» — безвредно, в GPIO ничего не пишется, светодиод не дёргается.
|
||||||
|
void ensureTimerRunning() {
|
||||||
|
if (htim_ == nullptr) return;
|
||||||
|
__HAL_TIM_ENABLE_DMA(htim_, TIM_DMA_UPDATE);
|
||||||
|
if ((htim_->Instance->CR1 & TIM_CR1_CEN) == 0U) {
|
||||||
|
__HAL_TIM_CLEAR_FLAG(htim_, TIM_FLAG_UPDATE);
|
||||||
|
HAL_TIM_Base_Start(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) {
|
||||||
|
if (s_instance == nullptr || hdma == nullptr) return nullptr;
|
||||||
|
for (uint8_t i = 0; i < s_instance->streamCount_; i++) {
|
||||||
|
if (hdma == &s_instance->streams_[i].hdma) {
|
||||||
|
return &s_instance->streams_[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dmaHalfCpltCb(DMA_HandleTypeDef* hdma) {
|
||||||
|
auto* s = streamFromDma(hdma);
|
||||||
|
if (s == nullptr || !s->active) return;
|
||||||
|
s->onHalf();
|
||||||
|
if (s_instance != nullptr && s->ticksOutput >= s->totalTicks) {
|
||||||
|
s_instance->stopStream(*s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dmaCpltCb(DMA_HandleTypeDef* hdma) {
|
||||||
|
auto* s = streamFromDma(hdma);
|
||||||
|
if (s == nullptr || !s->active) return;
|
||||||
|
s->onComplete();
|
||||||
|
if (s_instance != nullptr && s->ticksOutput >= s->totalTicks) {
|
||||||
|
s_instance->stopStream(*s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dmaErrorCb(DMA_HandleTypeDef* hdma) {
|
||||||
|
auto* s = streamFromDma(hdma);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool initStream(TxStream& s, const StreamCfg& chCfg) {
|
||||||
|
s.enc = chCfg.enc;
|
||||||
|
s.dmaInstance = chCfg.instance;
|
||||||
|
s.dmaIrq = chCfg.irq;
|
||||||
|
s.dmamuxRequest = chCfg.dmamuxRequest;
|
||||||
|
|
||||||
|
s.dmaBuf = chCfg.dmaWords;
|
||||||
|
s.bufLen = chCfg.dmaWordCount;
|
||||||
|
s.halfLen = (uint16_t)(chCfg.dmaWordCount / 2U);
|
||||||
|
s.runs = chCfg.gateRuns;
|
||||||
|
s.maxRuns = chCfg.maxGateRuns;
|
||||||
|
|
||||||
|
s.port = (s.enc != nullptr) ? s.enc->getPort() : nullptr;
|
||||||
|
s.mask = (s.enc != nullptr) ? s.enc->getPinMask() : 0;
|
||||||
|
s.setWord = (uint32_t)s.mask;
|
||||||
|
s.resetWord = ((uint32_t)s.mask) << 16;
|
||||||
|
|
||||||
|
s.resetWave();
|
||||||
|
|
||||||
|
s.hdma.Instance = s.dmaInstance;
|
||||||
|
s.hdma.Init.Request = s.dmamuxRequest;
|
||||||
|
s.hdma.Init.Direction = DMA_MEMORY_TO_PERIPH;
|
||||||
|
s.hdma.Init.PeriphInc = DMA_PINC_DISABLE;
|
||||||
|
s.hdma.Init.MemInc = DMA_MINC_ENABLE;
|
||||||
|
s.hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
|
||||||
|
s.hdma.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
|
||||||
|
s.hdma.Init.Mode = DMA_CIRCULAR;
|
||||||
|
s.hdma.Init.Priority = DMA_PRIORITY_HIGH;
|
||||||
|
|
||||||
|
HAL_DMA_DeInit(&s.hdma);
|
||||||
|
if (HAL_DMA_Init(&s.hdma) != HAL_OK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
s.hdma.XferHalfCpltCallback = dmaHalfCpltCb;
|
||||||
|
s.hdma.XferCpltCallback = dmaCpltCb;
|
||||||
|
s.hdma.XferErrorCallback = dmaErrorCb;
|
||||||
|
s.hdma.XferAbortCallback = nullptr;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
IR_SendStatus startStream(TxStream& s, const uint8_t* packet, uint8_t len) {
|
||||||
|
if (s.enc == nullptr || s.port == nullptr || s.mask == 0) return IR_SendStatus::ExternalInvalidConfig;
|
||||||
|
if (s.active) return IR_SendStatus::EncoderBusy;
|
||||||
|
if (s.dmaBuf == nullptr || s.bufLen < 2 || s.halfLen == 0) return IR_SendStatus::ExternalInvalidConfig;
|
||||||
|
if (s.runs == nullptr || s.maxRuns == 0) return IR_SendStatus::ExternalInvalidConfig;
|
||||||
|
|
||||||
|
s.resetWave();
|
||||||
|
|
||||||
|
const uint16_t mult = IR_Encoder::carrierMultiply();
|
||||||
|
s.runCount = IR_Encoder::buildPhysicalGateRuns(packet, len, s.runs, s.maxRuns, mult);
|
||||||
|
if (s.runCount == 0) return IR_SendStatus::BuildGateRunsFailed;
|
||||||
|
|
||||||
|
uint32_t total = 0;
|
||||||
|
for (size_t i = 0; i < s.runCount; i++) total += s.runs[i].lenTicks;
|
||||||
|
s.totalTicks = total;
|
||||||
|
|
||||||
|
uint16_t pwr = mult / 2U;
|
||||||
|
if (s.enc != nullptr) {
|
||||||
|
const uint16_t want = s.enc->powerNumerator();
|
||||||
|
const uint16_t cap = IR_Encoder::maxPowerNumerator();
|
||||||
|
pwr = (want > cap) ? cap : want;
|
||||||
|
}
|
||||||
|
|
||||||
|
s.wave.configure(s.setWord, s.resetWord, s.runs, s.runCount, mult, pwr);
|
||||||
|
|
||||||
|
s.fill(&s.dmaBuf[0], s.bufLen);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
__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;
|
||||||
|
return IR_SendStatus::DmaStartFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureTimerRunning();
|
||||||
|
return IR_SendStatus::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
void stopStream(TxStream& s) {
|
||||||
|
if (!s.active) return;
|
||||||
|
|
||||||
|
s.active = false;
|
||||||
|
HAL_DMA_Abort_IT(&s.hdma);
|
||||||
|
|
||||||
|
if (s.port != nullptr) {
|
||||||
|
s.port->BSRR = s.resetWord;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.enc != nullptr) {
|
||||||
|
s.enc->externalFinishSend();
|
||||||
|
}
|
||||||
|
// Fix C: TIM НЕ останавливаем — он free-running, без разделяемого счётчика.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template<size_t MaxStreams>
|
||||||
|
IrDmaTxStm32<MaxStreams>* IrDmaTxStm32<MaxStreams>::s_instance = nullptr;
|
||||||
|
|
||||||
|
inline void IrDmaTxStm32_onDmaHandle(DMA_HandleTypeDef* hdma) {
|
||||||
|
HAL_DMA_IRQHandler(hdma);
|
||||||
|
}
|
||||||
|
|
||||||
|
#elif defined(ARDUINO_ARCH_STM32)
|
||||||
|
|
||||||
|
#error "IrDmaTxStm32: добавьте ветку HAL для вашей серии STM32 (сейчас только STM32G4xx)."
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
template<size_t MaxStreams = irproto::kDefaultDmaTxMaxStreams>
|
||||||
|
class IrDmaTxStm32 {};
|
||||||
|
|
||||||
|
#endif
|
||||||
137
IrTxBsrrWave.h
Normal file
137
IrTxBsrrWave.h
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IrTxGateTypes.h"
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#if defined(__GNUC__)
|
||||||
|
#define IR_TX_BSRR_WAVE_HOT __attribute__((always_inline)) inline
|
||||||
|
#else
|
||||||
|
#define IR_TX_BSRR_WAVE_HOT inline
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Генерация потока 32-бит слов для GPIO BSRR из RLE-сегментов IrTxGateRun.
|
||||||
|
* За один период несущей — multiply физических тиков; при gate — powerN из них HIGH (N ≤ multiply/2).
|
||||||
|
*/
|
||||||
|
class IrTxBsrrWave {
|
||||||
|
public:
|
||||||
|
void configure(uint32_t setW, uint32_t resetW, IrTxGateRun* r, size_t n, uint16_t multiply, uint16_t powerN) {
|
||||||
|
setWord = setW;
|
||||||
|
resetWord = resetW;
|
||||||
|
runs = r;
|
||||||
|
runCount = n;
|
||||||
|
multiply_ = multiply < 2 ? 2 : multiply;
|
||||||
|
const uint16_t cap = static_cast<uint16_t>(multiply_ / 2U);
|
||||||
|
powerN_ = (powerN > cap) ? cap : powerN;
|
||||||
|
resetWave();
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetWave() {
|
||||||
|
runIndex_ = 0;
|
||||||
|
slotInPeriod_ = 0;
|
||||||
|
ticksLeftInRun_ = 0;
|
||||||
|
if (runCount > 0U && runs != nullptr) {
|
||||||
|
ticksLeftInRun_ = runs[0].lenTicks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IR_TX_BSRR_WAVE_HOT uint32_t nextWord() {
|
||||||
|
if (runIndex_ >= runCount) {
|
||||||
|
return resetWord;
|
||||||
|
}
|
||||||
|
const bool gate = runs[runIndex_].gate;
|
||||||
|
uint32_t out;
|
||||||
|
if (!gate) {
|
||||||
|
slotInPeriod_ = 0;
|
||||||
|
out = resetWord;
|
||||||
|
} else {
|
||||||
|
out = (slotInPeriod_ < powerN_) ? setWord : resetWord;
|
||||||
|
slotInPeriod_++;
|
||||||
|
if (slotInPeriod_ >= multiply_) {
|
||||||
|
slotInPeriod_ = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ticksLeftInRun_ > 0) {
|
||||||
|
ticksLeftInRun_--;
|
||||||
|
}
|
||||||
|
if (ticksLeftInRun_ == 0) {
|
||||||
|
runIndex_++;
|
||||||
|
if (runIndex_ < runCount) {
|
||||||
|
ticksLeftInRun_ = runs[runIndex_].lenTicks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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:
|
||||||
|
uint32_t setWord = 0;
|
||||||
|
uint32_t resetWord = 0;
|
||||||
|
IrTxGateRun* runs = nullptr;
|
||||||
|
size_t runCount = 0;
|
||||||
|
uint16_t multiply_ = 2;
|
||||||
|
uint16_t powerN_ = 1;
|
||||||
|
size_t runIndex_ = 0;
|
||||||
|
uint16_t ticksLeftInRun_ = 0;
|
||||||
|
uint16_t slotInPeriod_ = 0;
|
||||||
|
};
|
||||||
13
IrTxGateTypes.h
Normal file
13
IrTxGateTypes.h
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Один RLE-сегмент огибающей несущей.
|
||||||
|
* В legacy buildGateRuns: lenTicks в тактах логической шкалы 2×carrierFrec.
|
||||||
|
* В современном DMA/buffered ISR пути buildPhysicalGateRuns строит lenTicks сразу в физических тиках carrierFrec×multiply.
|
||||||
|
*/
|
||||||
|
struct IrTxGateRun {
|
||||||
|
uint16_t lenTicks;
|
||||||
|
bool gate;
|
||||||
|
};
|
||||||
64
IrTxIsrBufferedStorage.h
Normal file
64
IrTxIsrBufferedStorage.h
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IR_config.h"
|
||||||
|
#include "IrTxBsrrWave.h"
|
||||||
|
|
||||||
|
class IrTxIsrBufferedStorageBase {
|
||||||
|
public:
|
||||||
|
IrTxGateRun* gateRuns = nullptr;
|
||||||
|
size_t maxGateRuns = 0;
|
||||||
|
uint32_t* bsrrWords = nullptr;
|
||||||
|
uint16_t wordCount = 0;
|
||||||
|
|
||||||
|
IrTxBsrrWave wave{};
|
||||||
|
uint16_t readIdx = 0;
|
||||||
|
uint16_t halfLen = 0;
|
||||||
|
uint32_t totalTicks = 0;
|
||||||
|
uint32_t ticksSent = 0;
|
||||||
|
|
||||||
|
bool isValid() const {
|
||||||
|
return gateRuns != nullptr &&
|
||||||
|
maxGateRuns != 0U &&
|
||||||
|
bsrrWords != nullptr &&
|
||||||
|
wordCount >= 2U &&
|
||||||
|
(wordCount & 1U) == 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetRuntimeState() {
|
||||||
|
readIdx = 0;
|
||||||
|
halfLen = static_cast<uint16_t>(wordCount / 2U);
|
||||||
|
totalTicks = 0;
|
||||||
|
ticksSent = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class IrTxIsrBufferedStorageView : public IrTxIsrBufferedStorageBase {
|
||||||
|
public:
|
||||||
|
IrTxIsrBufferedStorageView(IrTxGateRun* runs, size_t runCount, uint32_t* words, uint16_t wordsCount) {
|
||||||
|
gateRuns = runs;
|
||||||
|
maxGateRuns = runCount;
|
||||||
|
bsrrWords = words;
|
||||||
|
wordCount = wordsCount;
|
||||||
|
resetRuntimeState();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template<size_t MaxGateRuns = irproto::kIsrTxMaxGateRuns, uint16_t WordCount = irproto::kIsrTxBsrrWordCount>
|
||||||
|
class IrTxIsrBufferedStorage : public IrTxIsrBufferedStorageBase {
|
||||||
|
static_assert(MaxGateRuns > 0U, "IrTxIsrBufferedStorage: MaxGateRuns > 0");
|
||||||
|
static_assert(WordCount >= 2U, "IrTxIsrBufferedStorage: WordCount >= 2");
|
||||||
|
static_assert((WordCount & 1U) == 0U, "IrTxIsrBufferedStorage: WordCount must be even");
|
||||||
|
|
||||||
|
public:
|
||||||
|
IrTxIsrBufferedStorage() {
|
||||||
|
gateRuns = gateRunsStorage_;
|
||||||
|
maxGateRuns = MaxGateRuns;
|
||||||
|
bsrrWords = bsrrWordsStorage_;
|
||||||
|
wordCount = WordCount;
|
||||||
|
resetRuntimeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
IrTxGateRun gateRunsStorage_[MaxGateRuns]{};
|
||||||
|
uint32_t bsrrWordsStorage_[WordCount]{};
|
||||||
|
};
|
||||||
180
PacketTypes.cpp
Normal file
180
PacketTypes.cpp
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
#include "PacketTypes.h"
|
||||||
|
|
||||||
|
namespace PacketTypes
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
isRawAvailable = true;
|
||||||
|
#ifdef IRDEBUG_INFO
|
||||||
|
Serial.print(" OK ");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
isRawAvailable = true;
|
||||||
|
#ifdef IRDEBUG_INFO
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
if (isAvailable)
|
||||||
|
{
|
||||||
|
isAvailable = false;
|
||||||
|
isRawAvailable = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool BasePack::availableRaw()
|
||||||
|
{
|
||||||
|
if (isRawAvailable)
|
||||||
|
{
|
||||||
|
isRawAvailable = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Data::checkAddress()
|
||||||
|
{
|
||||||
|
bool ret;
|
||||||
|
IR_FOX::checkAddressRuleApply(getAddrTo(), this->id, ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DataBack::checkAddress()
|
||||||
|
{
|
||||||
|
bool ret;
|
||||||
|
if (getMsgType() == IR_MSG_BACK_TO)
|
||||||
|
{
|
||||||
|
DataOffset = 5;
|
||||||
|
IR_FOX::checkAddressRuleApply((packInfo->buffer[addressToOffset] << 8) | packInfo->buffer[addressToOffset + 1], this->id, ret);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DataOffset = 3;
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Accept::checkAddress() { return true; }
|
||||||
|
|
||||||
|
bool Request::checkAddress()
|
||||||
|
{
|
||||||
|
bool ret;
|
||||||
|
IR_FOX::checkAddressRuleApply(getAddrTo(), this->id, ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
335
PacketTypes.h
335
PacketTypes.h
@ -4,103 +4,56 @@
|
|||||||
class IR_Decoder;
|
class IR_Decoder;
|
||||||
namespace PacketTypes
|
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
|
class BasePack
|
||||||
{
|
{
|
||||||
friend IR_Decoder;
|
friend IR_Decoder;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool isAvailable;
|
bool isAvailable = false;
|
||||||
bool isRawAvailable;
|
bool isRawAvailable = false;
|
||||||
bool isNeedAccept;
|
bool isNeedAccept = false;
|
||||||
|
|
||||||
uint8_t msgOffset;
|
uint8_t msgOffset = 0;
|
||||||
uint8_t addressFromOffset;
|
uint8_t addressFromOffset = 0;
|
||||||
uint8_t addressToOffset;
|
uint8_t addressToOffset = 0;
|
||||||
uint8_t DataOffset;
|
uint8_t DataOffset = 0;
|
||||||
|
|
||||||
IR_FOX::PackInfo *packInfo;
|
IR_FOX::PackInfo *packInfo = nullptr;
|
||||||
uint16_t id;
|
uint16_t id = 0;
|
||||||
|
|
||||||
virtual bool checkAddress() { return true; };
|
virtual bool checkAddress();
|
||||||
void set(IR_FOX::PackInfo *packInfo, uint16_t id)
|
bool set(IR_FOX::PackInfo *packInfo, uint16_t id, bool requireTypedSize = true);
|
||||||
{
|
|
||||||
this->packInfo = packInfo;
|
|
||||||
this->id = id;
|
|
||||||
|
|
||||||
if (checkAddress())
|
static uint16_t _getAddrFrom(BasePack *obj);
|
||||||
{
|
static uint16_t _getAddrTo(BasePack *obj);
|
||||||
isAvailable = true;
|
static uint8_t _getDataSize(BasePack *obj);
|
||||||
isRawAvailable = true;
|
static uint8_t *_getDataPrt(BasePack *obj);
|
||||||
#ifdef IRDEBUG_INFO
|
static uint8_t _getDataRawSize(BasePack *obj);
|
||||||
Serial.print(" OK ");
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
isRawAvailable = true;
|
|
||||||
#ifdef IRDEBUG_INFO
|
|
||||||
Serial.print(" NOT-OK ");
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint16_t _getAddrFrom(BasePack *obj)
|
|
||||||
{
|
|
||||||
return (obj->packInfo->buffer[obj->addressFromOffset] << 8) | obj->packInfo->buffer[obj->addressFromOffset + 1];
|
|
||||||
};
|
|
||||||
static uint16_t _getAddrTo(BasePack *obj)
|
|
||||||
{
|
|
||||||
return (obj->packInfo->buffer[obj->addressToOffset] << 8) | obj->packInfo->buffer[obj->addressToOffset + 1];
|
|
||||||
};
|
|
||||||
|
|
||||||
static uint8_t _getDataSize(BasePack *obj)
|
|
||||||
{
|
|
||||||
return obj->packInfo->packSize - crcBytes - obj->DataOffset;
|
|
||||||
};
|
|
||||||
static uint8_t *_getDataPrt(BasePack *obj)
|
|
||||||
{
|
|
||||||
return obj->packInfo->buffer + obj->DataOffset;
|
|
||||||
};
|
|
||||||
static uint8_t _getDataRawSize(BasePack *obj)
|
|
||||||
{
|
|
||||||
return obj->packInfo->packSize;
|
|
||||||
};
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
bool available()
|
bool available();
|
||||||
{
|
bool availableRaw();
|
||||||
if (isAvailable)
|
|
||||||
{
|
inline uint8_t getMsgInfo() { return packInfo->buffer[0] & IR_MASK_MSG_INFO; };
|
||||||
isAvailable = false;
|
inline uint8_t getMsgType() { return (packInfo->buffer[0] >> 5) & IR_MASK_MSG_TYPE; };
|
||||||
isRawAvailable = false;
|
inline uint8_t getMsgRAW() { return packInfo->buffer[0]; };
|
||||||
return true;
|
inline uint16_t getErrorCount() { return packInfo->err.all(); };
|
||||||
}
|
inline uint8_t getErrorLowSignal() { return packInfo->err.lowSignal; };
|
||||||
else
|
inline uint8_t getErrorHighSignal() { return packInfo->err.highSignal; };
|
||||||
{
|
inline uint8_t getErrorOther() { return packInfo->err.other; };
|
||||||
return false;
|
inline uint16_t getTunerTime() { return packInfo->rTime; };
|
||||||
}
|
inline uint8_t *getDataRawPtr() { return packInfo->buffer; };
|
||||||
};
|
/** Полный размер кадра в байтах (как packInfo.packSize); доступен для gotRaw (BasePack). */
|
||||||
bool availableRaw()
|
inline uint8_t getDataRawSize() { return _getDataRawSize(this); };
|
||||||
{
|
|
||||||
if (isRawAvailable)
|
|
||||||
{
|
|
||||||
isRawAvailable = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
uint8_t getMsgInfo() { return packInfo->buffer[0] & IR_MASK_MSG_INFO; };
|
|
||||||
uint8_t getMsgType() { return (packInfo->buffer[0] >> 5) & IR_MASK_MSG_TYPE; };
|
|
||||||
uint8_t getMsgRAW() { return packInfo->buffer[0]; };
|
|
||||||
uint16_t getErrorCount() { return packInfo->err.all(); };
|
|
||||||
uint8_t getErrorLowSignal() { return packInfo->err.lowSignal; };
|
|
||||||
uint8_t getErrorHighSignal() { return packInfo->err.highSignal; };
|
|
||||||
uint8_t getErrorOther() { return packInfo->err.other; };
|
|
||||||
uint16_t getTunerTime() { return packInfo->rTime; };
|
|
||||||
uint8_t *getDataRawPtr() { return packInfo->buffer; };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class Data : public BasePack
|
class Data : public BasePack
|
||||||
@ -114,20 +67,14 @@ namespace PacketTypes
|
|||||||
DataOffset = 5;
|
DataOffset = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t getAddrFrom() { return _getAddrFrom(this); };
|
inline uint16_t getAddrFrom() { return _getAddrFrom(this); };
|
||||||
uint16_t getAddrTo() { return _getAddrTo(this); };
|
inline uint16_t getAddrTo() { return _getAddrTo(this); };
|
||||||
|
|
||||||
uint8_t getDataSize() { return _getDataSize(this); };
|
inline uint8_t getDataSize() { return _getDataSize(this); };
|
||||||
uint8_t *getDataPrt() { return _getDataPrt(this); };
|
inline uint8_t *getDataPrt() { return _getDataPrt(this); };
|
||||||
uint8_t getDataRawSize() { return _getDataRawSize(this); };
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool checkAddress() override
|
bool checkAddress() override;
|
||||||
{
|
|
||||||
bool ret;
|
|
||||||
IR_FOX::checkAddressRuleApply(getAddrTo(), this->id, ret);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class DataBack : public BasePack
|
class DataBack : public BasePack
|
||||||
@ -141,29 +88,14 @@ namespace PacketTypes
|
|||||||
DataOffset = 3;
|
DataOffset = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t getAddrFrom() { return _getAddrFrom(this); };
|
inline uint16_t getAddrFrom() { return _getAddrFrom(this); };
|
||||||
uint16_t getAddrTo() { return _getAddrTo(this); };
|
inline uint16_t getAddrTo() { return _getAddrTo(this); };
|
||||||
|
|
||||||
uint8_t getDataSize() { return _getDataSize(this); };
|
inline uint8_t getDataSize() { return _getDataSize(this); };
|
||||||
uint8_t *getDataPrt() { return _getDataPrt(this); };
|
inline uint8_t *getDataPrt() { return _getDataPrt(this); };
|
||||||
uint8_t getDataRawSize() { return _getDataRawSize(this); };
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool checkAddress() override
|
bool checkAddress() override;
|
||||||
{
|
|
||||||
bool ret;
|
|
||||||
if (getMsgType() == IR_MSG_BACK_TO)
|
|
||||||
{
|
|
||||||
DataOffset = 5;
|
|
||||||
IR_FOX::checkAddressRuleApply((packInfo->buffer[addressToOffset] << 8) | packInfo->buffer[addressToOffset + 1], this->id, ret);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DataOffset = 3;
|
|
||||||
ret = true;
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class Accept : public BasePack
|
class Accept : public BasePack
|
||||||
@ -176,11 +108,11 @@ namespace PacketTypes
|
|||||||
DataOffset = 3;
|
DataOffset = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t getAddrFrom() { return _getAddrFrom(this); };
|
inline uint16_t getAddrFrom() { return _getAddrFrom(this); };
|
||||||
uint8_t getCustomByte() { return packInfo->buffer[DataOffset]; };
|
inline uint8_t getCustomByte() { return packInfo->buffer[DataOffset]; };
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool checkAddress() override { return true; }
|
bool checkAddress() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Request : public BasePack
|
class Request : public BasePack
|
||||||
@ -194,168 +126,11 @@ namespace PacketTypes
|
|||||||
DataOffset = 3;
|
DataOffset = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t getAddrFrom() { return _getAddrFrom(this); };
|
inline uint16_t getAddrFrom() { return _getAddrFrom(this); };
|
||||||
uint16_t getAddrTo() { return _getAddrTo(this); };
|
inline uint16_t getAddrTo() { return _getAddrTo(this); };
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool checkAddress() override
|
bool checkAddress() override;
|
||||||
{
|
|
||||||
bool ret;
|
|
||||||
IR_FOX::checkAddressRuleApply(getAddrTo(), this->id, ret);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// class IOffsets {
|
|
||||||
// protected:
|
|
||||||
// uint8_t msgOffset;
|
|
||||||
// uint8_t addressFromOffset;
|
|
||||||
// uint8_t addressToOffset;
|
|
||||||
// uint8_t DataOffset;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// class IPackInfo {
|
|
||||||
// public:
|
|
||||||
// IR_FOX::PackInfo* packInfo;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// class IBaseEmptyPack : virtual public IOffsets, virtual public IPackInfo {
|
|
||||||
// };
|
|
||||||
|
|
||||||
// class IR_Decoder;
|
|
||||||
// class IEmptyPack : virtual protected IBaseEmptyPack, virtual public IR_FOX {
|
|
||||||
// friend IR_Decoder;
|
|
||||||
// bool isAvailable;
|
|
||||||
// bool isRawAvailable;
|
|
||||||
// bool isNeedAccept;
|
|
||||||
|
|
||||||
// protected:
|
|
||||||
// uint16_t id;
|
|
||||||
|
|
||||||
// virtual bool checkAddress() {};
|
|
||||||
|
|
||||||
// virtual void set(IR_FOX::PackInfo* packInfo, uint16_t id, bool isNeedAccept = false) {
|
|
||||||
// IBaseEmptyPack::IPackInfo::packInfo = packInfo;
|
|
||||||
// this->id = id;
|
|
||||||
// this->isNeedAccept = isNeedAccept;
|
|
||||||
|
|
||||||
// if (isAvailable = checkAddress()) {
|
|
||||||
// isAvailable = true;
|
|
||||||
// isRawAvailable = true;
|
|
||||||
// Serial.print(" OK ");
|
|
||||||
// } else {
|
|
||||||
// isRawAvailable = true;
|
|
||||||
// Serial.print(" NOT-OK ");
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public:
|
|
||||||
// virtual bool available() { if (isAvailable) { isAvailable = false; isRawAvailable = false; return true; } else { return false; } };
|
|
||||||
// virtual bool availableRaw() { if (isRawAvailable) { isRawAvailable = false; return true; } else { return false; } };
|
|
||||||
// virtual uint8_t getMsgInfo() { return packInfo->buffer[0] & IR_MASK_MSG_INFO; };
|
|
||||||
// virtual uint8_t getMsgType() { return (packInfo->buffer[0] >> 5) & IR_MASK_MSG_TYPE; };
|
|
||||||
// virtual uint8_t getMsgRAW() { return packInfo->buffer[0]; };
|
|
||||||
// virtual uint16_t getErrorCount() { return packInfo->err.all(); };
|
|
||||||
// virtual uint8_t getErrorLowSignal() { return packInfo->err.lowSignal; };
|
|
||||||
// virtual uint8_t getErrorHighSignal() { return packInfo->err.highSignal; };
|
|
||||||
// virtual uint8_t getErrorOther() { return packInfo->err.other; };
|
|
||||||
// virtual uint16_t getTunerTime() { return packInfo->rTime; };
|
|
||||||
// };
|
|
||||||
|
|
||||||
// class IHasAddresFrom : virtual protected IBaseEmptyPack {
|
|
||||||
// public:
|
|
||||||
// virtual uint16_t getAddrFrom() { return (packInfo->buffer[addressFromOffset] << 8) | packInfo->buffer[addressFromOffset + 1]; };
|
|
||||||
// };
|
|
||||||
|
|
||||||
// class IHasAddresTo : virtual protected IBaseEmptyPack {
|
|
||||||
// public:
|
|
||||||
// virtual uint16_t getAddrTo() { return (packInfo->buffer[addressToOffset] << 8) | packInfo->buffer[addressToOffset + 1]; };
|
|
||||||
// };
|
|
||||||
|
|
||||||
// class IHasAddresData : virtual protected IBaseEmptyPack {
|
|
||||||
// public:
|
|
||||||
// virtual uint8_t getDataSize() { return packInfo->packSize - crcBytes - DataOffset; };
|
|
||||||
// virtual uint8_t* getDataPrt() { return packInfo->buffer + DataOffset; };
|
|
||||||
// virtual uint8_t getDataRawSize() { return packInfo->packSize; };
|
|
||||||
// virtual uint8_t* getDataRawPtr() { return packInfo->buffer; };
|
|
||||||
// };
|
|
||||||
|
|
||||||
// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// class Data :
|
|
||||||
// virtual public IEmptyPack,
|
|
||||||
// virtual public IHasAddresFrom,
|
|
||||||
// virtual public IHasAddresTo,
|
|
||||||
// virtual public IHasAddresData {
|
|
||||||
// public:
|
|
||||||
// Data() {
|
|
||||||
// msgOffset = 0;
|
|
||||||
// addressFromOffset = 1;
|
|
||||||
// addressToOffset = 3;
|
|
||||||
// DataOffset = 5;
|
|
||||||
// }
|
|
||||||
// protected:
|
|
||||||
// bool checkAddress() override {
|
|
||||||
// bool ret;
|
|
||||||
// checkAddressRuleApply(getAddrTo(), this->id, ret);
|
|
||||||
// return ret;
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// class DataBack :
|
|
||||||
// virtual public IEmptyPack,
|
|
||||||
// virtual public IHasAddresFrom,
|
|
||||||
// virtual public IHasAddresData {
|
|
||||||
// public:
|
|
||||||
// DataBack() {
|
|
||||||
// msgOffset = 0;
|
|
||||||
// addressFromOffset = 1;
|
|
||||||
// addressToOffset = 3;
|
|
||||||
// DataOffset = 3;
|
|
||||||
// }
|
|
||||||
// protected:
|
|
||||||
// bool checkAddress() override {
|
|
||||||
// bool ret;
|
|
||||||
// if (getMsgType() == IR_MSG_BACK_TO) {
|
|
||||||
// DataOffset = 5;
|
|
||||||
// checkAddressRuleApply((packInfo->buffer[addressToOffset] << 8) | packInfo->buffer[addressToOffset + 1], this->id, ret);
|
|
||||||
// } else {
|
|
||||||
// DataOffset = 3;
|
|
||||||
// ret = true;
|
|
||||||
// }
|
|
||||||
// return ret;
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// class Request :
|
|
||||||
// virtual public IEmptyPack,
|
|
||||||
// virtual public IHasAddresFrom,
|
|
||||||
// virtual public IHasAddresTo {
|
|
||||||
// public:
|
|
||||||
// Request() {
|
|
||||||
// msgOffset = 0;
|
|
||||||
// addressFromOffset = 1;
|
|
||||||
// addressToOffset = 3;
|
|
||||||
// DataOffset = 3;
|
|
||||||
// }
|
|
||||||
// protected:
|
|
||||||
// bool checkAddress() override {
|
|
||||||
// bool ret;
|
|
||||||
// checkAddressRuleApply(getAddrTo(), this->id, ret);
|
|
||||||
// return ret;
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// class Accept :
|
|
||||||
// virtual public IEmptyPack,
|
|
||||||
// virtual public IHasAddresFrom {
|
|
||||||
// public:
|
|
||||||
// Accept() {
|
|
||||||
// msgOffset = 0;
|
|
||||||
// addressFromOffset = 1;
|
|
||||||
// DataOffset = 1;
|
|
||||||
// }
|
|
||||||
// protected:
|
|
||||||
// };
|
|
||||||
|
|||||||
20
RingBuffer.h
20
RingBuffer.h
@ -13,13 +13,16 @@ public:
|
|||||||
return start == end;
|
return start == end;
|
||||||
}
|
}
|
||||||
|
|
||||||
void push(T element) {
|
bool push(T element) {
|
||||||
|
bool pushed = false;
|
||||||
noInterrupts();
|
noInterrupts();
|
||||||
if (!isFull()) {
|
if (!isFull()) {
|
||||||
data[end] = element;
|
data[end] = element;
|
||||||
end = (end + 1) % BufferSize;
|
end = (end + 1) % BufferSize;
|
||||||
|
pushed = true;
|
||||||
}
|
}
|
||||||
interrupts();
|
interrupts();
|
||||||
|
return pushed;
|
||||||
}
|
}
|
||||||
|
|
||||||
T* pop() {
|
T* pop() {
|
||||||
@ -33,6 +36,21 @@ public:
|
|||||||
return value;
|
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:
|
private:
|
||||||
T data[BufferSize];
|
T data[BufferSize];
|
||||||
unsigned int start, end;
|
unsigned int start, end;
|
||||||
|
|||||||
142
docs_analysis/IR_protocol_analysis.md
Normal file
142
docs_analysis/IR_protocol_analysis.md
Normal file
@ -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 и <broadcast) → через `acceptDelay` → `sendAccept` по nullptr → HardFault. Детерминированно, если кто-то шлёт машинке DATA_ACCEPT; плюс шумом (1/65536 на ложный кадр нужного типа).
|
||||||
|
- **HIGH — OOB-запись** `dataBuffer[38]` (off-by-one) при `packSize==0` (`IR_DecoderRaw.cpp:753` — `>` вместо `>=`): при `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<FrontStorage,250>` (`.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]`, отсев пар <minUs как глитч.
|
||||||
|
- **D. FSM преамбулы** (`preambleProcessEdge :1599-1723`): Idle→Candidate→Locked, нужно 2 согласованных периода rise→rise в окне 220-340% bitTime после «тишины» >`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`.
|
||||||
72
docs_analysis/Signal_physics_analysis.md
Normal file
72
docs_analysis/Signal_physics_analysis.md
Normal file
@ -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.*`.
|
||||||
36
docs_analysis/crc_combined_sim.py
Normal file
36
docs_analysis/crc_combined_sim.py
Normal file
@ -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}% необнаружено")
|
||||||
33
docs_analysis/crc_shift_findings.md
Normal file
33
docs_analysis/crc_shift_findings.md
Normal file
@ -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× хуже текущего). Как отдельный компромисс — обсуждать вместе с обратной совместимостью.
|
||||||
|
|
||||||
|
(Обратную совместимость версий протокола обсудим отдельно — по запросу Даши.)
|
||||||
103
docs_analysis/crc_shift_sim.py
Normal file
103
docs_analysis/crc_shift_sim.py
Normal file
@ -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} бит подряд")
|
||||||
97
docs_analysis/crc_shift_visualize.py
Normal file
97
docs_analysis/crc_shift_visualize.py
Normal file
@ -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<len(labels) else ''
|
||||||
|
lab+=(L[:8]).center(8)+ (' s ' if i<len(frame_bytes)-1 else '')
|
||||||
|
line+=piece
|
||||||
|
return line, lab
|
||||||
|
|
||||||
|
def show_frame(title, frame_bytes, labels):
|
||||||
|
print(f"\n### {title}")
|
||||||
|
print(" байты (hex):", ' '.join(f"{b:02x}" for b in frame_bytes))
|
||||||
|
print(" поля :", ' '.join((labels[i] if i<len(labels) else '?')[:2].ljust(2) for i in range(len(frame_bytes))),
|
||||||
|
" (по 1 байту)")
|
||||||
|
line,lab=onair(frame_bytes,labels)
|
||||||
|
print(" эфир (data 8 бит + [sync 3]) и подписи:")
|
||||||
|
print(" "+line)
|
||||||
|
print(" "+lab)
|
||||||
|
|
||||||
|
def slip_delete_databits(data, p):
|
||||||
|
b=[x for by in data for x in bits(by)]
|
||||||
|
b=b[:p]+b[p+1:]+[0]
|
||||||
|
return [int(''.join(map(str,b[i:i+8])),2) for i in range(0,len(b),8)]
|
||||||
|
|
||||||
|
def show_shift(title, data, p):
|
||||||
|
print(f"\n### {title}")
|
||||||
|
shifted=slip_delete_databits(data,p)
|
||||||
|
b0=[x for by in data for x in bits(by)]
|
||||||
|
b1=[x for by in shifted for x in bits(by)]
|
||||||
|
# выравненные строки битов по байтам
|
||||||
|
def grp(bl): return ' '.join(bstr(bl[i:i+8]) for i in range(0,len(bl),8))
|
||||||
|
print(" ДО :", ' '.join(f"{x:02x}" for x in data), " crc1(poly1)=%02x"%crc8(data,POLY1),
|
||||||
|
" crc2(poly2 over data+crc1)=%02x"%crc8(data+[crc8(data,POLY1)],POLY2))
|
||||||
|
print(" ПОСЛЕ :", ' '.join(f"{x:02x}" for x in shifted), " crc1(poly1)=%02x"%crc8(shifted,POLY1),
|
||||||
|
" crc2=%02x"%crc8(shifted+[crc8(shifted,POLY1)],POLY2))
|
||||||
|
print(" биты ДО :", grp(b0))
|
||||||
|
# маркер позиции удалённого бита p
|
||||||
|
mk=[' ']*len(b0);
|
||||||
|
pos = p + p//8 # +пробелы между байтами
|
||||||
|
mk_line=''
|
||||||
|
# построим маркерную строку под "биты ДО"
|
||||||
|
s=grp(b0)
|
||||||
|
# индекс символа для бита p:
|
||||||
|
char_idx = p + (p//8) # каждый байт(8) + 1 пробел
|
||||||
|
mk_line=' '*char_idx+'^'
|
||||||
|
print(" ", mk_line, f"<- бит #{p} удалён, всё правее сдвинуто влево на 1")
|
||||||
|
print(" биты ПОСЛЕ:", grp(b1))
|
||||||
|
# различия побитно
|
||||||
|
diff=''.join('X' if (i<len(b1) and b0[i]!=b1[i]) else '.' for i in range(len(b0)))
|
||||||
|
print(" дельта :", ' '.join(diff[i:i+8] for i in range(0,len(diff),8)), " (X=бит изменился)")
|
||||||
|
same1 = crc8(data,POLY1)==crc8(shifted,POLY1)
|
||||||
|
c2o=crc8(data+[crc8(data,POLY1)],POLY2); c2s=crc8(shifted+[crc8(shifted,POLY1)],POLY2)
|
||||||
|
print(f" ВЕРДИКТ: single(poly1) {'ПРОШЁЛ (не заметил сдвиг!)' if same1 else 'поймал'};"
|
||||||
|
f" double(+poly2) {'НЕ поймал' if (same1 and c2o==c2s) else 'ПОЙМАЛ'} (crc2 {c2o:02x}{'=='if c2o==c2s else '≠'}{c2s:02x})")
|
||||||
|
|
||||||
|
print("="*70)
|
||||||
|
print("ЧАСТЬ 1. Реалистичный кадр на эфире (структура + синхробиты)")
|
||||||
|
print("="*70)
|
||||||
|
# BACK-кадр: msgType=0(BACK), packSize=5 → [hdr][addrFrom_hi][addrFrom_lo][crc1][crc2]
|
||||||
|
# hdr = (0<<5)|5 = 0x05 ; addrFrom = 300 = 0x012C
|
||||||
|
data_back=[0x05, 0x01, 0x2C]
|
||||||
|
c1=crc8(data_back,POLY1); c2=crc8(data_back+[c1],POLY2)
|
||||||
|
frame_back=data_back+[c1,c2]
|
||||||
|
show_frame("BACK, addrFrom=300, packSize=5", frame_back,
|
||||||
|
["HDR","AfHi","AfLo","CRC1","CRC2"])
|
||||||
|
print(" (HDR = 000 00101: msgType=0(BACK), len=5=весь пакет; sync между байтами = [¬last,last,¬next])")
|
||||||
|
|
||||||
|
print("\n"+"="*70)
|
||||||
|
print("ЧАСТЬ 2. Сдвиги, где ОДИНОЧНЫЙ CRC8 слеп (из симуляции), в битах")
|
||||||
|
print("="*70)
|
||||||
|
show_shift("Пример A: data 26 0b 05, слип бита #10", [0x26,0x0b,0x05], 10)
|
||||||
|
show_shift("Пример B: data 3b 7c 43, слип бита #14 (окно в середине)", [0x3b,0x7c,0x43], 14)
|
||||||
|
show_shift("Пример C: data cc 4f b8 f6 33, слип бита #11", [0xcc,0x4f,0xb8,0xf6,0x33], 11)
|
||||||
174
ir_protocol_gate_runs_sim.py
Normal file
174
ir_protocol_gate_runs_sim.py
Normal file
@ -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())
|
||||||
41082
ref/DMA_no_send__extRX.txt
Normal file
41082
ref/DMA_no_send__extRX.txt
Normal file
File diff suppressed because it is too large
Load Diff
14922
ref/DMA_self_frontlog.txt
Normal file
14922
ref/DMA_self_frontlog.txt
Normal file
File diff suppressed because it is too large
Load Diff
66
ref/IR_DMA_TX_backend.md
Normal file
66
ref/IR_DMA_TX_backend.md
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# Контракт бэкенда DMA-TX ИК (`IrDmaTxStm32`)
|
||||||
|
|
||||||
|
См. также: [IR_TX_MODES.md](IR_TX_MODES.md) — общая схема выбора `legacy ISR`, `buffered ISR` и `external backend`.
|
||||||
|
|
||||||
|
Платформа: **STM32G4**, Arduino STM32. Передача: **DMA memory → GPIO BSRR**, запрос от **TIM UPDATE** (частота `carrierFrec×2` из `IR_Encoder::beginClockOnly`).
|
||||||
|
|
||||||
|
### Число потоков (шаблон)
|
||||||
|
|
||||||
|
Класс: **`IrDmaTxStm32<MaxStreams>`**. Число слотов в `Config::streams[]` и внутреннем массиве задаётся **в коде**, без `-D` и без макроса до инклюда:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
constexpr size_t kStreams = 2;
|
||||||
|
static IrDmaTxStm32<kStreams> dma;
|
||||||
|
IrDmaTxStm32<kStreams>::Config cfg;
|
||||||
|
// IRQ: IrDmaTxStm32<kStreams>::instance()
|
||||||
|
```
|
||||||
|
|
||||||
|
По умолчанию: **`IrDmaTxStm32<>`** эквивалентно **`IrDmaTxStm32<irproto::kDefaultDmaTxMaxStreams>`** (`IR_config.h`, обычно 4). Реализация в заголовке (отдельного `.cpp` нет).
|
||||||
|
|
||||||
|
## Роль библиотеки
|
||||||
|
|
||||||
|
- Разбор пакета в RLE-пробеги: `IR_Encoder::buildGateRuns`.
|
||||||
|
- Генерация слов для BSRR (несущая/тишина по тикам), предзаполнение буфера и дозаполнение по прерываниям half/complete.
|
||||||
|
- Настройка канала DMA, DMAMUX, кольцевой режим, старт/стоп DMA и таймера, колбэки HAL.
|
||||||
|
- В `begin()` только `HAL_NVIC_EnableIRQ` для каналов DMA (без `SetPriority`).
|
||||||
|
|
||||||
|
## Роль прошивки (клиента)
|
||||||
|
|
||||||
|
### Буферы и размеры
|
||||||
|
|
||||||
|
На **каждый** поток в `StreamCfg` клиент передаёт:
|
||||||
|
|
||||||
|
| Поле | Смысл |
|
||||||
|
|------|--------|
|
||||||
|
| `dmaWords` | Указатель на массив `uint32_t` — слова для записи в BSRR. |
|
||||||
|
| `dmaWordCount` | Число **слов** (32-bit), **чётное**, ≥ 2. Половина — один «полубуфер» для HT/TC IRQ. |
|
||||||
|
| `gateRuns` | Массив `IR_Encoder::IR_TxGateRun` для выхода `buildGateRuns`. |
|
||||||
|
| `maxGateRuns` | Длина этого массива. Должен быть достаточен для самого длинного кадра. |
|
||||||
|
|
||||||
|
Память и выравнивание — ответственность клиента; типичные порядки: 4096 слов DMA, 1024 ранов (как в машинке).
|
||||||
|
|
||||||
|
### Таймер и DMA
|
||||||
|
|
||||||
|
- `HardwareTimer` / тот же TIM, что и `beginClockOnly`, без конкурирующего `attachInterrupt` на UPDATE.
|
||||||
|
- `instance`, `irq`, `dmamuxRequest` (например `DMA_REQUEST_TIM17_UP`) — из схемы платы; оба потока на одном TIM обычно используют **один** `TIMx_UP` в DMAMUX.
|
||||||
|
|
||||||
|
### Приоритеты NVIC
|
||||||
|
|
||||||
|
Не задаются в библиотеке. После `begin()` клиент выставляет preempt/sub для `DMA1_ChannelN` (и согласует с приёмом EXTI и др.), например общей функцией вроде `Car_applyInterruptPriorities()`.
|
||||||
|
|
||||||
|
### Прерывания DMA
|
||||||
|
|
||||||
|
Библиотека **не** объявляет `DMA1_ChannelN_IRQHandler`. В одном `.cpp` прошивки — единственное определение на канал, внутри:
|
||||||
|
|
||||||
|
`IrDmaTxStm32<N>::instance()->irqForStream(i)` (тот же **N**, что у объекта бэкенда) или `IrDmaTxStm32_onDmaHandle(hdma)`.
|
||||||
|
|
||||||
|
## Контракт `IR_Encoder::setExternalTxBackend`
|
||||||
|
|
||||||
|
Подключение: `setExternalTxBackend(startFn, busyFn, ctx)`.
|
||||||
|
|
||||||
|
- **`startFn(ctx, enc, packet, len)`** — должен вызвать `IrDmaTxStm32<N>::start(enc, packet, len)` (или обёртку). Возвращает успех старта DMA.
|
||||||
|
- **`busyFn(ctx)`** — пока возвращает «занято», новая отправка не стартует. У `IrDmaTxStm32<N>::busy()`: **true**, если **все** настроенные потоки в передаче (для двух передатчиков — оба активны); иначе можно запустить второй канал.
|
||||||
|
|
||||||
|
## Сбой `begin()`
|
||||||
|
|
||||||
|
При ошибке `HAL_DMA_Init` и т.п. `begin()` возвращает `false`, `instance()` не используется для IRQ до успешного `begin()`.
|
||||||
87
ref/IR_EDGE_TRACE_FORMAT.md
Normal file
87
ref/IR_EDGE_TRACE_FORMAT.md
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# Формат журнала фронтов ИК (`IR_EDGE_TRACE`)
|
||||||
|
|
||||||
|
Включается в `IR_config.h`: раскомментируйте `#define IR_EDGE_TRACE`.
|
||||||
|
Размер кольца задаётся `IR_EDGE_TRACE_CAPACITY` (по умолчанию 512 записей).
|
||||||
|
Память: примерно `IR_EDGE_TRACE_CAPACITY × 6` байт на экземпляр `IR_DecoderRaw`.
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
В ISR на каждый аппаратный фронт на линии приёмника пишется запись: абсолютное время `micros()`, уровень линии после фронта, флаги. Это **отдельное** кольцо от `subBuffer`: при переполнении `subBuffer` фронты в журнале всё равно сохраняются, пока не заполнится это кольцо.
|
||||||
|
|
||||||
|
При **передаче ИК по DMA** на STM32 важно, чтобы **прерывание приёма (EXTI)** имело **более высокий приоритет NVIC**, чем DMA канала передачи — иначе метки времени и сам поток фронтов в логе искажаются. См. **[`IR_DMA_ISR_signal_analysis.md`](../IR_DMA_ISR_signal_analysis.md)** (раздел 2.1).
|
||||||
|
|
||||||
|
## Вывод в Serial
|
||||||
|
|
||||||
|
При включённом `IR_EDGE_TRACE` протокол **сам** сбрасывает накопленные фронты в конце каждого `IR_DecoderRaw::tick()` (и на ветке «subBuffer пуст»): в цикле вызывается `edgeTraceFlushChunk(Serial, 48)`, пока в кольце есть записи.
|
||||||
|
|
||||||
|
Вручную тот же смысл: `edgeTraceFlushChunk(Print &out, maxRec)` печатает **ровно одну строку** (если есть что выгрузить; при пустом кольце выход без печати):
|
||||||
|
|
||||||
|
1. Символ перевода строки `\n` (отделяет блок от предыдущего вывода).
|
||||||
|
2. Литеральный префикс **`@IRF1v1:`** (удобно grep-ать; не используйте этот текст в других `Serial.print`, чтобы строки не сливались).
|
||||||
|
3. **Нижний регистр hex** без пробелов: полезная нагрузка бинарного блока ниже.
|
||||||
|
4. `\n` в конце.
|
||||||
|
|
||||||
|
Другой текст (например `IR raw:` из `IRDEBUG_SERIAL_PACK`) не содержит `@IRF1v1:`, поэтому визуально и по парсеру блоки разделимы.
|
||||||
|
|
||||||
|
Если авто-сброс в `tick()` отключён или нужен другой `Print`, вызывайте `edgeTraceFlushChunk` в цикле, пока возвращаемое значение > 0.
|
||||||
|
|
||||||
|
## Бинарная полезная нагрузка (до кодирования в hex)
|
||||||
|
|
||||||
|
Все многобайтовые целые — **little-endian**, порядок байт от младшего к старшему.
|
||||||
|
|
||||||
|
| Смещение | Размер | Поле |
|
||||||
|
|----------|--------|------|
|
||||||
|
| 0 | 1 | **meta** |
|
||||||
|
| 1 | 2 | **count** — число записей в этой строке (`uint16_t`) |
|
||||||
|
| 3 | `count × 6` | Массив записей |
|
||||||
|
|
||||||
|
### meta (байт)
|
||||||
|
|
||||||
|
| Бит | Значение |
|
||||||
|
|-----|----------|
|
||||||
|
| 0 | **overflow**: кольцо хотя бы раз переполнилось с момента последнего `edgeTraceClear()`; новые фронты терялись, пока не освободилось место. Сбрасывается только `edgeTraceClear()`. |
|
||||||
|
| 1 | **truncated**: после этой выгрузки в буфере ещё есть записи (chunk урезан лимитом `maxRec` или внутренним максимумом 64). |
|
||||||
|
|
||||||
|
Биты 2–7 зарезервированы (0).
|
||||||
|
|
||||||
|
### Одна запись (6 байт)
|
||||||
|
|
||||||
|
| Смещение в записи | Размер | Поле |
|
||||||
|
|-------------------|--------|------|
|
||||||
|
| 0 | 4 | **t_us** — значение `micros()` на момент фронта (`uint32_t`). При переполнении `micros()` (~70 мин) разницы между соседними записями всё ещё корректны, если обрабатывать как unsigned. |
|
||||||
|
| 4 | 1 | **level** — уровень входа приёмника после фронта: `0` = LOW, `1` = HIGH (как `port->IDR & mask` в ISR). |
|
||||||
|
| 5 | 1 | **flags** |
|
||||||
|
| | | бит 0 **SKIP_DECODE** (`IR_EDGE_TRACE_F_SKIP_DECODE`): фронт записан, но в `subBuffer` **не** попал, потому что был активен `isPairSending` (пара передаёт). Алгоритм декодирования этот фронт не видит. |
|
||||||
|
|
||||||
|
## Минимальный разбор (Python 3)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import binascii, re
|
||||||
|
|
||||||
|
def parse_irf1_line(line: str):
|
||||||
|
m = re.search(r"@IRF1v1:([0-9a-f]+)\s*$", line.strip())
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
raw = binascii.unhexlify(m.group(1))
|
||||||
|
meta, cnt_lo, cnt_hi = raw[0], raw[1], raw[2]
|
||||||
|
count = cnt_lo | (cnt_hi << 8)
|
||||||
|
recs = []
|
||||||
|
p = 3
|
||||||
|
for _ in range(count):
|
||||||
|
t = raw[p] | (raw[p+1]<<8) | (raw[p+2]<<16) | (raw[p+3]<<24)
|
||||||
|
level, flags = raw[p+4], raw[p+5]
|
||||||
|
recs.append((t, level, flags))
|
||||||
|
p += 6
|
||||||
|
return {
|
||||||
|
"overflow": bool(meta & 1),
|
||||||
|
"truncated": bool(meta & 2),
|
||||||
|
"count": count,
|
||||||
|
"records": recs,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Рекомендации по съёму
|
||||||
|
|
||||||
|
- При очень плотном потоке фронтов кольцо всё же может переполниться до следующего `tick()` — увеличьте `IR_EDGE_TRACE_CAPACITY` или уменьшите нагрузку на ISR.
|
||||||
|
- Для «чистого» лога отключите или сильно урежьте `IRDEBUG_SERIAL_PACK`, иначе объём Serial будет очень большим.
|
||||||
|
- Для полного сброса состояния перед тестом: `edgeTraceClear()`.
|
||||||
62
ref/IR_RX_BRIEF_LOG.md
Normal file
62
ref/IR_RX_BRIEF_LOG.md
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# IR RX Brief Log
|
||||||
|
|
||||||
|
Краткий лог включается через:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#define IR_RX_BRIEF_LOG 1
|
||||||
|
#define IR_RX_BRIEF_LOG_REJECT_ONLY 1 // только отклонённые/ошибочные события
|
||||||
|
```
|
||||||
|
|
||||||
|
Лог печатается короткими строками вида:
|
||||||
|
|
||||||
|
```text
|
||||||
|
IRRX t=1234567 rsn=CRC len=25 err=3
|
||||||
|
IRRX t=1234000 rsn=MUTE_BEGIN
|
||||||
|
IRRX t=1234988 rsn=MUTE_END cnt=42
|
||||||
|
```
|
||||||
|
|
||||||
|
Где:
|
||||||
|
|
||||||
|
- `t` — uptime МК в `micros()`
|
||||||
|
- `rsn` — краткий код причины
|
||||||
|
- остальные поля зависят от причины
|
||||||
|
|
||||||
|
## Коды `rsn`
|
||||||
|
|
||||||
|
| Код | Смысл | Типичные поля |
|
||||||
|
|-----|-------|---------------|
|
||||||
|
| `MUTE_BEGIN` | Началось окно mute: RX временно игнорирует вход, пока активен связанный TX | - |
|
||||||
|
| `MUTE_END` | Окно mute завершилось; `cnt` показывает число заблокированных фронтов за всё окно | `cnt` |
|
||||||
|
| `QRAW` | Потеря фронтов из-за переполнения сырой очереди `subBuffer` | `cnt` |
|
||||||
|
| `QFLT` | Потеря фронтов из-за переполнения очереди после входного фильтра | `total` |
|
||||||
|
| `HOLD` | Переполнен holdback фильтра до выпуска фронтов | `total` |
|
||||||
|
| `GLITCH` | Фронт/пара фронтов отброшены как глитч | `total` |
|
||||||
|
| `TIME` | Плохой тайминг фронтов/битов, кадр не может нормально разбираться | `rp`, `hp` |
|
||||||
|
| `PREAMB` | Кандидат преамбулы не залочился или был перезапущен | `good`, `per` |
|
||||||
|
| `SYNC` | Ошибка sync-бита привела к reject кадра | `err` |
|
||||||
|
| `BUF` | Переполнен битовый буфер кадра | `bits` |
|
||||||
|
| `TIMEOUT` | Кадр оборвался по таймауту до завершения; после записи в лог вызываются `isReciveRaw=false` и `firstRX()` (полный сброс декодера) | `bits`, `exp` |
|
||||||
|
| `CRC` | Кадр дошёл до конца по длине, но CRC не сошёлся | `len`, `err` |
|
||||||
|
| `OK` | Кадр успешно распознан | `len`, `err` |
|
||||||
|
|
||||||
|
## Поля
|
||||||
|
|
||||||
|
- `cnt` — число событий/фронтов, накопленных за окно или пакетную группу
|
||||||
|
- `total` — накопленный счётчик отбраковок данного типа
|
||||||
|
- `rp` — `risePeriod`
|
||||||
|
- `hp` — `highTime`
|
||||||
|
- `good` — число подряд подходящих периодов преамбулы перед срывом
|
||||||
|
- `per` — период преамбулы/кандидата
|
||||||
|
- `err` — суммарные ошибки `lowSignal + highSignal + other` либо счётчик sync-ошибок
|
||||||
|
- `bits` — сколько data-бит успело накопиться
|
||||||
|
- `exp` — ожидаемая длина кадра из первого байта, если уже известна
|
||||||
|
- `len` — полная длина кадра в байтах
|
||||||
|
|
||||||
|
## Когда смотреть подробный debug
|
||||||
|
|
||||||
|
- `listenStart` / `checkTimeout` — в конце обработки фронта (`END:`) и во ветке «нет фронта» в `tick()`; не в начале до `pop`, иначе после таймаута `lastEdgeTime` расходится с метками ISR из очереди → ложные `TIMEOUT` (`bits=0`).
|
||||||
|
- Пока в `subBuffer` или в hold фильтра есть необработанные фронты, таймаут по `micros() - lastEdgeTime` **не оценивается** (`rxTimeoutPipelineBusy`): иначе при хвосте очереди «тихая пауза» считается слишком длинной и снова ложный `TIMEOUT`.
|
||||||
|
- Если нужен полный поток битов и sync: включать `IRDEBUG_SERIAL_PACK`
|
||||||
|
- Если нужно понять, какие именно фронты пришли в ISR: включать `IR_EDGE_TRACE`
|
||||||
|
- `IR_RX_BRIEF_LOG` нужен как короткий always-on-ish индикатор сути проблемы, без длинного дампа
|
||||||
|
- `IR_RX_BRIEF_LOG_REJECT_ONLY=1` скрывает `OK` и `PREAMB`, оставляя только отклонения/ошибки
|
||||||
146
ref/IR_TX_MODES.md
Normal file
146
ref/IR_TX_MODES.md
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
# Режимы IR TX в библиотеке
|
||||||
|
|
||||||
|
Этот документ описывает, как в библиотеке выбирается путь передачи IR и как его правильно использовать в проектах Arduino STM32.
|
||||||
|
|
||||||
|
## Кратко
|
||||||
|
|
||||||
|
У библиотеки есть три варианта TX:
|
||||||
|
|
||||||
|
- `legacy ISR` — внутренний ISR-путь без внешнего backend. Это путь по умолчанию для обратной совместимости.
|
||||||
|
- `buffered ISR` — внутренний ISR-путь с предварительной подготовкой BSRR-слов и кольцевым буфером.
|
||||||
|
- `external backend` — передача делегируется проекту через `IR_Encoder::setExternalTxBackend(...)`, например в DMA backend.
|
||||||
|
|
||||||
|
Порядок выбора такой:
|
||||||
|
|
||||||
|
1. Если зарегистрирован `external backend`, используется он.
|
||||||
|
2. Иначе используется внутренний ISR библиотеки.
|
||||||
|
3. Для внутреннего ISR:
|
||||||
|
- по умолчанию включён `legacy ISR`
|
||||||
|
- `buffered ISR` включается явно: нужно привязать storage к encoder и переключить режим
|
||||||
|
|
||||||
|
## 1. Legacy ISR
|
||||||
|
|
||||||
|
Это режим по умолчанию. Старые проекты могут ничего не менять:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static HardwareTimer timer(TIM11);
|
||||||
|
static IR_Encoder enc(PA9, 42, &dec);
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
IR_Encoder::begin(&timer, 1, TIM11_IRQn, 0);
|
||||||
|
enc.enable();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Если проект не регистрирует внешний backend и не переключает режим явно, библиотека работает в `legacy ISR`.
|
||||||
|
|
||||||
|
Для явного выбора можно написать:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
IR_Encoder::setTxIsrLegacyMode(true);
|
||||||
|
IR_Encoder::begin(&timer, 1, TIM11_IRQn, 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Buffered ISR
|
||||||
|
|
||||||
|
Этот режим использует внутренний буферный ISR-путь библиотеки. Он включается только явно:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <IrTxIsrBufferedStorage.h>
|
||||||
|
|
||||||
|
static IrTxIsrBufferedStorage<> txStorage;
|
||||||
|
|
||||||
|
enc.enableBufferedIsr(txStorage);
|
||||||
|
IR_Encoder::begin(&timer, 1, TIM11_IRQn, 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
Нижнеуровневый вариант API — отдельно привязать storage через `attachBufferedIsrStorage(...)`, но в обычном проекте удобнее использовать `enableBufferedIsr(...)`.
|
||||||
|
|
||||||
|
Смысл режима:
|
||||||
|
|
||||||
|
- пакет сначала превращается в `gate runs`
|
||||||
|
- затем в поток слов `GPIO->BSRR`
|
||||||
|
- ISR выдаёт готовые слова из кольцевого буфера
|
||||||
|
|
||||||
|
### Важно про RAM
|
||||||
|
|
||||||
|
В текущей реализации память под буферный ISR вынесена из `IR_Encoder` в отдельный storage-объект.
|
||||||
|
|
||||||
|
То есть:
|
||||||
|
|
||||||
|
- `legacy ISR` не тянет buffered-буферы в RAM самого `IR_Encoder`
|
||||||
|
- память под `gate runs` и `BSRR words` появляется только там, где проект сам создал `IrTxIsrBufferedStorage<>`
|
||||||
|
|
||||||
|
Это важно для STM32 с небольшим объёмом RAM, например для `STM32F401`.
|
||||||
|
|
||||||
|
## 3. External backend
|
||||||
|
|
||||||
|
Если проект хочет полностью взять TX на себя, библиотека позволяет зарегистрировать внешний backend:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static bool txBusy(void* ctx);
|
||||||
|
static bool txStart(void* ctx, IR_Encoder* enc, const uint8_t* packet, uint8_t len);
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
IR_Encoder::beginClockOnly(&timer);
|
||||||
|
IR_Encoder::setExternalTxBackend(txStart, txBusy, nullptr);
|
||||||
|
enc.enable();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
После вызова `setExternalTxBackend(...)` библиотека больше не использует свои внутренние ISR-пути для фактической передачи.
|
||||||
|
|
||||||
|
В этом режиме:
|
||||||
|
|
||||||
|
- `setTxIsrLegacyMode(true/false)` игнорируется
|
||||||
|
- завершение передачи должен сигнализировать сам backend через `enc->externalFinishSend()`
|
||||||
|
|
||||||
|
Подробности по встроенному DMA backend для `STM32G4xx`: см. [IR_DMA_TX_backend.md](IR_DMA_TX_backend.md).
|
||||||
|
|
||||||
|
## Когда какой режим использовать
|
||||||
|
|
||||||
|
### Старый проект, который ничего не настраивает
|
||||||
|
|
||||||
|
Использовать как есть:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
IR_Encoder::begin(...);
|
||||||
|
```
|
||||||
|
|
||||||
|
Итог: `legacy ISR`
|
||||||
|
|
||||||
|
### Нужен новый внутренний буферный ISR
|
||||||
|
|
||||||
|
Включить явно:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <IrTxIsrBufferedStorage.h>
|
||||||
|
|
||||||
|
static IrTxIsrBufferedStorage<> txStorage;
|
||||||
|
|
||||||
|
enc.enableBufferedIsr(txStorage);
|
||||||
|
IR_Encoder::begin(...);
|
||||||
|
```
|
||||||
|
|
||||||
|
Итог: `buffered ISR`
|
||||||
|
|
||||||
|
### Нужен проектный DMA или другой свой транспорт
|
||||||
|
|
||||||
|
Подключить внешний backend:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
IR_Encoder::beginClockOnly(...);
|
||||||
|
IR_Encoder::setExternalTxBackend(...);
|
||||||
|
```
|
||||||
|
|
||||||
|
Итог: `external backend`
|
||||||
|
|
||||||
|
## Рекомендация для совместимости
|
||||||
|
|
||||||
|
Для старых проектов безопаснее не вызывать `setTxIsrLegacyMode(false)`, если нет явной причины переходить на buffered ISR.
|
||||||
|
|
||||||
|
Если задача — сохранить старое поведение без неожиданного роста нагрузки на TX-логику, оставляйте default `legacy ISR` или задавайте его явно:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
IR_Encoder::setTxIsrLegacyMode(true);
|
||||||
|
```
|
||||||
289
ref/ISR_self.txt
Normal file
289
ref/ISR_self.txt
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
IR sendTimer()
|
||||||
|
IR Laser();
|
||||||
|
IR SKIP LASER
|
||||||
|
|
||||||
|
IR sendTimer()
|
||||||
|
IR irSend()
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: => ERROR: TIMEOUT, rx_data_size = 10, but only 10 bytes received
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
|
|
||||||
|
IR raw: 110 01010 101 00000000 101 00000000 100 11111101 010 11101000 101 01011000 101 00000000 101 00000000 100 11011101 010 11011000 => OK: SendInfo_v1_1, Empty_Command, Empty_Command
|
||||||
|
IR hex: CA 00 00 FD E8 58 00 00 DD D8
|
||||||
17170
ref/ISR_self_frontlog.txt
Normal file
17170
ref/ISR_self_frontlog.txt
Normal file
File diff suppressed because it is too large
Load Diff
13744
ref/OtherCar_DMA_send__NoSelfTx.txt
Normal file
13744
ref/OtherCar_DMA_send__NoSelfTx.txt
Normal file
File diff suppressed because it is too large
Load Diff
13
ref/point response.txt
Normal file
13
ref/point response.txt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
IR raw: 110 01001 011 01111101 011 00100110 101 00000000 100 00000000 101 01110110 101 00100111 010 10011100 101 01101100 => OK: Laser_ON, LED_Left_BlinkTest
|
||||||
|
IR hex: C9 7D 26 00 00 76 27 9C 6C
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
IR raw: 110 01001 011 01111101 011 00100110 101 00000000 100 00000000 101 01110110 101 00100111 010 10011100 101 01101100 => OK: Laser_ON, LED_Left_BlinkTest
|
||||||
|
|
||||||
|
|
||||||
152
test_examples/longData/longData.ino
Normal file
152
test_examples/longData/longData.ino
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* Тест длинной полезной нагрузки IR (0x5E + строка, как ответ версии в Car).
|
||||||
|
* Передача через DMA + TIM17 — как в машинке (Car/src/IR/IR.cpp, BoardTest/IR_DMA).
|
||||||
|
* Авто-режим: чередование total длины кадра 24/25 байт.
|
||||||
|
*/
|
||||||
|
#include "IR_Decoder.h"
|
||||||
|
#include "IR_Encoder.h"
|
||||||
|
#include "TimerStatic.h"
|
||||||
|
|
||||||
|
/** 1 — TX через DMA (STM32G4). */
|
||||||
|
#ifndef LONGDATA_USE_DMA
|
||||||
|
#define LONGDATA_USE_DMA 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if LONGDATA_USE_DMA
|
||||||
|
#include <IrDmaTxStm32.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static constexpr uint16_t kIrDeviceAddr = 0;
|
||||||
|
static constexpr uint8_t kCmdVersion = 0x5E;
|
||||||
|
static constexpr uint32_t kSerialBaud = 115200;
|
||||||
|
static constexpr uint32_t kSendPeriodMs = 500;
|
||||||
|
static constexpr uint8_t kMaxPayload = bytePerPack;
|
||||||
|
static constexpr uint8_t kMaxParamBytes = kMaxPayload - 1;
|
||||||
|
|
||||||
|
static IR_Encoder enc(PIN_IR_ENC_FORWARD, kIrDeviceAddr, nullptr);
|
||||||
|
static HardwareTimer irTimer(TIM17);
|
||||||
|
|
||||||
|
#if LONGDATA_USE_DMA
|
||||||
|
namespace {
|
||||||
|
constexpr size_t kIrDmaStreams = 1;
|
||||||
|
constexpr uint16_t kIrDmaTxWordCount = 4096U;
|
||||||
|
constexpr size_t kIrDmaTxMaxGateRuns = 1024U;
|
||||||
|
static uint32_t s_irDmaWords[kIrDmaTxWordCount];
|
||||||
|
static IR_Encoder::IR_TxGateRun s_irGateRuns[kIrDmaTxMaxGateRuns];
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
static IrDmaTxStm32<kIrDmaStreams> dmaBackend;
|
||||||
|
static bool txBusy(void * /*ctx*/) { return dmaBackend.busy(); }
|
||||||
|
static bool txStart(void * /*ctx*/, IR_Encoder *e, const uint8_t *packet, uint8_t len) {
|
||||||
|
return dmaBackend.start(e, packet, len);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static char s_paramAscii[kMaxParamBytes + 1];
|
||||||
|
static uint8_t s_irPayload[kMaxPayload];
|
||||||
|
static uint8_t s_irPayloadLen = 0;
|
||||||
|
static uint32_t s_lastSendMs = 0;
|
||||||
|
static bool s_sendLongerFrame = false;
|
||||||
|
|
||||||
|
// 24 байта total: msg(1)+addr(2)+addr(2)+data(17)+crc(2), где data=0x5E + 16 ASCII.
|
||||||
|
static const char kPayload16[] = "Car_v4.3.9_[12MH";
|
||||||
|
// 25 байт total: как выше, но data=0x5E + 17 ASCII.
|
||||||
|
static const char kPayload17[] = "Car_v4.3.9_[12MHz]_G491";
|
||||||
|
|
||||||
|
static void rebuildIrPayload() {
|
||||||
|
s_irPayload[0] = kCmdVersion;
|
||||||
|
size_t n = 0;
|
||||||
|
while (n < sizeof(s_paramAscii) && s_paramAscii[n]) {
|
||||||
|
++n;
|
||||||
|
}
|
||||||
|
const size_t copyLen = (n > kMaxParamBytes) ? kMaxParamBytes : n;
|
||||||
|
memcpy(s_irPayload + 1, s_paramAscii, copyLen);
|
||||||
|
s_irPayloadLen = static_cast<uint8_t>(1 + copyLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setAlternatingPayload() {
|
||||||
|
const char* src = s_sendLongerFrame ? kPayload17 : kPayload16;
|
||||||
|
strncpy(s_paramAscii, src, kMaxParamBytes);
|
||||||
|
s_paramAscii[kMaxParamBytes] = '\0';
|
||||||
|
s_sendLongerFrame = !s_sendLongerFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sendVersionPacket() {
|
||||||
|
rebuildIrPayload();
|
||||||
|
const IR_SendResult r = enc.sendData(IR_Broadcast, s_irPayload, s_irPayloadLen);
|
||||||
|
Serial.print(F("TX 0x5E + "));
|
||||||
|
Serial.print((unsigned)(s_irPayloadLen - 1));
|
||||||
|
Serial.print(F(" B, ok="));
|
||||||
|
Serial.print(r.success ? F("1") : F("0"));
|
||||||
|
Serial.print(F(", t="));
|
||||||
|
Serial.print(r.sendTimeMs);
|
||||||
|
Serial.println(F(" ms"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(kSerialBaud);
|
||||||
|
|
||||||
|
strncpy(s_paramAscii, kPayload16, kMaxParamBytes);
|
||||||
|
s_paramAscii[kMaxParamBytes] = '\0';
|
||||||
|
rebuildIrPayload();
|
||||||
|
|
||||||
|
#if LONGDATA_USE_DMA
|
||||||
|
// IR_Encoder::setCarrierMultiply(N); // до beginClockOnly; после смены — retuneCarrierClock()
|
||||||
|
IR_Encoder::beginClockOnly(&irTimer);
|
||||||
|
|
||||||
|
IrDmaTxStm32<kIrDmaStreams>::Config cfg;
|
||||||
|
cfg.timer = &irTimer;
|
||||||
|
cfg.streamCount = kIrDmaStreams;
|
||||||
|
cfg.streams[0].instance = DMA1_Channel1;
|
||||||
|
cfg.streams[0].irq = DMA1_Channel1_IRQn;
|
||||||
|
cfg.streams[0].dmamuxRequest = DMA_REQUEST_TIM17_UP;
|
||||||
|
cfg.streams[0].enc = &enc;
|
||||||
|
cfg.streams[0].dmaWords = s_irDmaWords;
|
||||||
|
cfg.streams[0].dmaWordCount = kIrDmaTxWordCount;
|
||||||
|
cfg.streams[0].gateRuns = s_irGateRuns;
|
||||||
|
cfg.streams[0].maxGateRuns = kIrDmaTxMaxGateRuns;
|
||||||
|
|
||||||
|
if (!dmaBackend.begin(cfg)) {
|
||||||
|
Serial.println(F("[IR_DMA] init FAILED"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IR_Encoder::setExternalTxBackend(txStart, txBusy, nullptr);
|
||||||
|
#elif LONGDATA_LEGACY_ISR
|
||||||
|
IR_Encoder::begin(&irTimer, 1, TIM17_IRQn, 0);
|
||||||
|
#else
|
||||||
|
IR_Encoder::begin(&irTimer, 1, TIM17_IRQn, 0);
|
||||||
|
#endif
|
||||||
|
enc.enable();
|
||||||
|
|
||||||
|
#if LONGDATA_USE_DMA
|
||||||
|
Serial.println(F("longData: DMA TX alternating 24/25 bytes"));
|
||||||
|
#elif LONGDATA_LEGACY_ISR
|
||||||
|
Serial.println(F("longData: legacy ISR TX (pre-unified FSM) 24/25 bytes"));
|
||||||
|
#else
|
||||||
|
Serial.println(F("longData: ISR TX (unified FSM) alternating 24/25 bytes"));
|
||||||
|
#endif
|
||||||
|
Serial.print(F("Auto-period ms = "));
|
||||||
|
Serial.println((unsigned long)kSendPeriodMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
if (now - s_lastSendMs >= kSendPeriodMs) {
|
||||||
|
s_lastSendMs = now;
|
||||||
|
setAlternatingPayload();
|
||||||
|
sendVersionPacket();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if LONGDATA_USE_DMA && defined(STM32G4xx)
|
||||||
|
extern "C" void DMA1_Channel1_IRQHandler(void) {
|
||||||
|
if (auto *p = IrDmaTxStm32<kIrDmaStreams>::instance()) {
|
||||||
|
p->irqForStream(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
100
tools/analyze_pkt33_car_point.py
Normal file
100
tools/analyze_pkt33_car_point.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Сравнение трасс decode по carraw3 vs pointraw_3 для кадра pkt[33] (car: CRC fail)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_TOOLS = Path(__file__).resolve().parent
|
||||||
|
if str(_TOOLS) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_TOOLS))
|
||||||
|
|
||||||
|
from ir_decoder_raw_sim import (
|
||||||
|
BIT_TIME_US,
|
||||||
|
RISE_MAX,
|
||||||
|
RISE_MIN,
|
||||||
|
SimState,
|
||||||
|
first_rx,
|
||||||
|
parse_raw_csv,
|
||||||
|
segments_to_edges,
|
||||||
|
tick,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_traced(path: Path) -> tuple[SimState, list[dict]]:
|
||||||
|
rows = parse_raw_csv(path)
|
||||||
|
assert rows is not None
|
||||||
|
edges = segments_to_edges(rows)
|
||||||
|
st = SimState()
|
||||||
|
tr: list[dict] = []
|
||||||
|
for t_us, rising in edges:
|
||||||
|
tick(st, t_us, rising, BIT_TIME_US, trace_rise=tr)
|
||||||
|
return st, tr
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
root = Path(__file__).resolve().parents[1] / "Analyzer" / "raw"
|
||||||
|
car_p = root / "carraw3.txt"
|
||||||
|
pt_p = root / "pointraw_3.txt"
|
||||||
|
st_c, tr_c = run_traced(car_p)
|
||||||
|
st_p, tr_p = run_traced(pt_p)
|
||||||
|
|
||||||
|
bad_i = next(i for i, x in enumerate(st_c.packets) if not x[0])
|
||||||
|
print(f"carraw3: bad packet index {bad_i}, bytes {st_c.packets[bad_i][2].hex()}")
|
||||||
|
print(f"carraw3 packets={len(st_c.packets)} ok={sum(1 for x in st_c.packets if x[0])}")
|
||||||
|
print(f"pointraw_3 packets={len(st_p.packets)} ok={sum(1 for x in st_p.packets if x[0])}")
|
||||||
|
print(f"rise window us: min={RISE_MIN} max={RISE_MAX} bit={BIT_TIME_US}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Пока кадр №bad_i собирается, в списке уже bad_i завершённых пакетов → len(packets)==bad_i.
|
||||||
|
build_i = bad_i
|
||||||
|
|
||||||
|
def frame_trace(tr: list[dict]) -> list[dict]:
|
||||||
|
return [r for r in tr if r["n_pkt"] == build_i]
|
||||||
|
|
||||||
|
fc = frame_trace(tr_c)
|
||||||
|
fp = frame_trace(tr_p)
|
||||||
|
print(
|
||||||
|
f"decode rises while building packet[{bad_i}] (trace n_pkt=={build_i}): "
|
||||||
|
f"car {len(fc)} vs point {len(fp)} lines"
|
||||||
|
)
|
||||||
|
print("--- car (first 40 decode steps of this frame) ---")
|
||||||
|
for i, r in enumerate(fc[:40]):
|
||||||
|
br = r["branch"]
|
||||||
|
ex = ""
|
||||||
|
if br == "ceil":
|
||||||
|
ex = f" hc={r['hc']} lc={r['lc']} ac={r['ac']}"
|
||||||
|
print(
|
||||||
|
f" {i:2} t={r['t_us'] / 1e6:.6f}s rp={r['rp']} ht={r['ht']} lt={r['lt']} "
|
||||||
|
f"{br} bits_out={r['bits_out']} i_buf={r['i_buf']}{ex}"
|
||||||
|
)
|
||||||
|
print("--- point (first 40) ---")
|
||||||
|
for i, r in enumerate(fp[:40]):
|
||||||
|
br = r["branch"]
|
||||||
|
ex = ""
|
||||||
|
if br == "ceil":
|
||||||
|
ex = f" hc={r['hc']} lc={r['lc']} ac={r['ac']}"
|
||||||
|
print(
|
||||||
|
f" {i:2} t={r['t_us'] / 1e6:.6f}s rp={r['rp']} ht={r['ht']} lt={r['lt']} "
|
||||||
|
f"{br} bits_out={r['bits_out']} i_buf={r['i_buf']}{ex}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print()
|
||||||
|
# Первое расхождение по (rp, ht, lt, branch, bits_out)
|
||||||
|
for i, (a, b) in enumerate(zip(fc, fp)):
|
||||||
|
ka = (a["rp"], a["ht"], a["lt"], a["branch"], a["bits_out"])
|
||||||
|
kb = (b["rp"], b["ht"], b["lt"], b["branch"], b["bits_out"])
|
||||||
|
if ka != kb:
|
||||||
|
print(f"First decode diff at step {i} within frame:")
|
||||||
|
print(f" car {a}")
|
||||||
|
print(f" point {b}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if len(fc) != len(fp):
|
||||||
|
print(f"Same tuples for min(len)={min(len(fc), len(fp))}; length car={len(fc)} point={len(fp)}")
|
||||||
|
else:
|
||||||
|
print("Identical decode trace for full frame (unexpected if CRC differs)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
517
tools/ir_decoder_raw_sim.py
Normal file
517
tools/ir_decoder_raw_sim.py
Normal file
@ -0,0 +1,517 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Симуляция IR_DecoderRaw::tick() по CSV уровней (как Analyzer/raw/*_raw.txt).
|
||||||
|
Сверка с IR_DecoderRaw.cpp / IR_config.h — без Arduino, только логика декодера.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# IR_config.h
|
||||||
|
BIT_ACTIVE_TAKTS = 25
|
||||||
|
BIT_PAUSE_TAKTS = 12
|
||||||
|
CARRIER_HZ = 38000
|
||||||
|
CARRIER_PERIOD_US = 1_000_000 / CARRIER_HZ
|
||||||
|
BIT_TIME_US = int((BIT_ACTIVE_TAKTS + BIT_PAUSE_TAKTS) * CARRIER_PERIOD_US)
|
||||||
|
TOLERANCE_US = 300
|
||||||
|
SYNC_BITS = 3
|
||||||
|
BIT_PER_BYTE = 8
|
||||||
|
MSG_BYTES = 1
|
||||||
|
CRC_BYTES = 2
|
||||||
|
POLY1 = 0x31
|
||||||
|
POLY2 = 0x8C
|
||||||
|
DATA_BYTE_SIZE_MAX = 1 + 2 + 2 + 31 + 2
|
||||||
|
IR_MASK_MSG_INFO = 0x1F
|
||||||
|
PREAMB_FRONTS = 6 # preambPulse*2
|
||||||
|
|
||||||
|
RISE_MIN = BIT_TIME_US - TOLERANCE_US
|
||||||
|
RISE_MAX = BIT_TIME_US + TOLERANCE_US
|
||||||
|
IR_TIMEOUT = RISE_MAX * (8 + SYNC_BITS + 1)
|
||||||
|
# IR_config.h: IR_SHORT_LOW_GLITCH_REJECT (по умолчанию 1)
|
||||||
|
SHORT_LOW_GLITCH_REJECT = True
|
||||||
|
GLITCH_REJECT_PHASE_NUDGE = True
|
||||||
|
MICRO_GAP_RISE_REJECT = True
|
||||||
|
IN_MARK_DOUBLE_FALL_IGNORE = False # как IR_config по умолчанию; True — меньше err_other, на разбор битов не влияет
|
||||||
|
# IR_RISE_INCLUSIVE_AROUND, IR_RISE_GRAY_SINGLE_BIT_FALLBACK
|
||||||
|
RISE_INCLUSIVE_AROUND = True
|
||||||
|
RISE_GRAY_SINGLE_BIT_FALLBACK = False # как IR_config по умолчанию; True — только для экспериментов
|
||||||
|
|
||||||
|
|
||||||
|
def around_rise_period(rise_period: int, rise_sync: int) -> bool:
|
||||||
|
lo = max(0, rise_sync - TOLERANCE_US)
|
||||||
|
hi = rise_sync + TOLERANCE_US
|
||||||
|
if RISE_INCLUSIVE_AROUND:
|
||||||
|
return lo <= rise_period <= hi
|
||||||
|
return lo < rise_period < hi
|
||||||
|
|
||||||
|
|
||||||
|
def rise_gray_single_bit_fallback(rise_period: int, rise_sync: int) -> bool:
|
||||||
|
hi = rise_sync + TOLERANCE_US
|
||||||
|
return rise_period > hi and rise_period <= 2 * rise_sync
|
||||||
|
|
||||||
|
|
||||||
|
def glitch_phase_nudge(edge_us: float, rise_sync: int, prev_rise: float) -> float:
|
||||||
|
if not GLITCH_REJECT_PHASE_NUDGE:
|
||||||
|
return prev_rise
|
||||||
|
if edge_us <= rise_sync:
|
||||||
|
return prev_rise
|
||||||
|
nudged = edge_us - rise_sync
|
||||||
|
if nudged > prev_rise and nudged < edge_us:
|
||||||
|
return nudged
|
||||||
|
return prev_rise
|
||||||
|
|
||||||
|
|
||||||
|
def ceil_div(val: int, div: int) -> int:
|
||||||
|
ret = val // div
|
||||||
|
if ((val << 4) // div - (ret << 4)) >= 8:
|
||||||
|
ret += 1
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def crc8(data: bytes, start: int, end: int, poly: int) -> int:
|
||||||
|
crc = 0xFF
|
||||||
|
for i in range(start, end):
|
||||||
|
crc ^= data[i]
|
||||||
|
for _ in range(8):
|
||||||
|
if crc & 0x80:
|
||||||
|
crc = ((crc << 1) ^ poly) & 0xFF
|
||||||
|
else:
|
||||||
|
crc = (crc << 1) & 0xFF
|
||||||
|
return crc
|
||||||
|
|
||||||
|
|
||||||
|
def crc_check(buf: bytearray, pack_size: int) -> bool:
|
||||||
|
ln = pack_size - CRC_BYTES
|
||||||
|
c1 = crc8(bytes(buf), 0, ln, POLY1)
|
||||||
|
c2 = crc8(bytes(buf), 0, ln + 1, POLY2)
|
||||||
|
crc = ((c1 << 8) & 0xFF00) | (c2 & 0xFF)
|
||||||
|
return buf[ln] == ((crc >> 8) & 0xFF) and buf[ln + 1] == (crc & 0xFF)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_raw_csv(path: Path) -> list[tuple[float, str, float]] | None:
|
||||||
|
"""None — файл не в формате уровней (например экспорт битов декодера)."""
|
||||||
|
rows = []
|
||||||
|
with path.open(encoding="utf-8", errors="replace") as f:
|
||||||
|
header = f.readline()
|
||||||
|
h = header.lower()
|
||||||
|
if "duration" not in h and "level" not in h:
|
||||||
|
if "bit_idx" in h or (len(header.split(",")) >= 3 and header.split(",")[1].strip() == "Type"):
|
||||||
|
return None
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split(",")
|
||||||
|
if len(parts) < 4:
|
||||||
|
continue
|
||||||
|
t_s = float(parts[0])
|
||||||
|
level = parts[1].strip()
|
||||||
|
if level.upper() not in ("HIGH", "LOW"):
|
||||||
|
return None
|
||||||
|
dur_us = float(parts[3].replace(" us", "").strip())
|
||||||
|
rows.append((t_s, level, dur_us))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def segments_to_edges(rows: list[tuple[float, str, float]]) -> list[tuple[float, bool]]:
|
||||||
|
"""Фронты: (t_us, rising)."""
|
||||||
|
edges: list[tuple[float, bool]] = []
|
||||||
|
prev = None
|
||||||
|
for t_s, level, _dur in rows:
|
||||||
|
high = level.upper() == "HIGH"
|
||||||
|
if prev is not None:
|
||||||
|
rising = high
|
||||||
|
edges.append((t_s * 1e6, rising))
|
||||||
|
prev = high
|
||||||
|
return edges
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SimState:
|
||||||
|
prev_rise: float = 0.0
|
||||||
|
prev_fall: float = 0.0
|
||||||
|
rise_period: int = 0
|
||||||
|
high_time: int = 0
|
||||||
|
low_time: int = 0
|
||||||
|
last_edge: float = 0.0
|
||||||
|
preamb_front_counter: int = 0
|
||||||
|
is_preamb: bool = False
|
||||||
|
is_recive_raw: bool = False
|
||||||
|
is_recive: bool = False
|
||||||
|
is_wrong_pack: bool = False
|
||||||
|
is_buffer_overflow: bool = False
|
||||||
|
buf_bit_pos: int = 0
|
||||||
|
is_data: bool = True
|
||||||
|
i_data_buffer: int = 0
|
||||||
|
next_control_bit: int = BIT_PER_BYTE
|
||||||
|
i_sync_bit: int = 0
|
||||||
|
err_sync_bit: int = 0
|
||||||
|
pack_size: int = 0
|
||||||
|
data_buffer: bytearray = field(default_factory=lambda: bytearray(DATA_BYTE_SIZE_MAX))
|
||||||
|
err_low: int = 0
|
||||||
|
err_high: int = 0
|
||||||
|
err_other: int = 0
|
||||||
|
high_count: int = 0
|
||||||
|
low_count: int = 0
|
||||||
|
all_count: int = 0
|
||||||
|
packets: list[tuple[bool, int, bytes]] = field(default_factory=list) # crc_ok, pack_size, raw bytes
|
||||||
|
bits_log: list[tuple[float, str, int]] = field(default_factory=list) # t_us, kind, bit
|
||||||
|
|
||||||
|
|
||||||
|
def first_rx(st: SimState) -> None:
|
||||||
|
"""Аналог IR_DecoderRaw::firstRX: сброс буфера битов, преамбулы, счётчиков ошибок по битам.
|
||||||
|
В прошивке isRecive / isReciveRaw сбрасывают вызывающие пути (listenStart, checkTimeout, конец кадра);
|
||||||
|
затем firstRX() обнуляет буфер и preambleResetToIdle()."""
|
||||||
|
st.is_preamb = True
|
||||||
|
st.is_wrong_pack = False
|
||||||
|
st.is_buffer_overflow = False
|
||||||
|
st.buf_bit_pos = 0
|
||||||
|
st.is_data = True
|
||||||
|
st.i_data_buffer = 0
|
||||||
|
st.next_control_bit = BIT_PER_BYTE
|
||||||
|
st.i_sync_bit = 0
|
||||||
|
st.err_sync_bit = 0
|
||||||
|
st.pack_size = 0
|
||||||
|
st.data_buffer = bytearray(DATA_BYTE_SIZE_MAX)
|
||||||
|
|
||||||
|
|
||||||
|
def tick(
|
||||||
|
st: SimState,
|
||||||
|
t_us: float,
|
||||||
|
rising: bool,
|
||||||
|
rise_sync_time: int,
|
||||||
|
*,
|
||||||
|
trace_rise: list[dict] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Один вызов tick — один фронт (как IR_DecoderRaw после pop)."""
|
||||||
|
rise_min = max(0, rise_sync_time - TOLERANCE_US)
|
||||||
|
rise_max = rise_sync_time + TOLERANCE_US
|
||||||
|
irmax = IR_TIMEOUT # упрощ.: без подстройки riseSyncTime в timeout
|
||||||
|
|
||||||
|
# listenStart: как IR_DecoderRaw — пауза по lastEdgeTime (между обработанными фронтами), не по prevRise.
|
||||||
|
if st.last_edge > 0 and st.is_recive_raw and (t_us - st.last_edge) > irmax * 2:
|
||||||
|
st.is_recive_raw = False
|
||||||
|
first_rx(st)
|
||||||
|
|
||||||
|
# checkTimeout: как IR_DecoderRaw после фикса — isReciveRaw=0 и firstRX(), иначе залипание FSM.
|
||||||
|
if st.last_edge > 0 and st.is_recive and (t_us - st.last_edge) > irmax * 2:
|
||||||
|
st.is_recive = False
|
||||||
|
st.is_recive_raw = False
|
||||||
|
first_rx(st)
|
||||||
|
# Не подставлять last_edge = t_us здесь: как IR_DecoderRaw после фикса.
|
||||||
|
|
||||||
|
st.last_edge = t_us
|
||||||
|
skip_rest = False
|
||||||
|
|
||||||
|
if rising:
|
||||||
|
cand_rp = int(t_us - st.prev_rise)
|
||||||
|
cand_ht = int(t_us - st.prev_fall)
|
||||||
|
cand_lt = int(st.prev_fall - st.prev_rise)
|
||||||
|
if SHORT_LOW_GLITCH_REJECT:
|
||||||
|
short_low_glitch = (
|
||||||
|
st.is_recive
|
||||||
|
and not st.is_preamb
|
||||||
|
and cand_ht < (rise_min // 8)
|
||||||
|
and cand_lt >= rise_min
|
||||||
|
and cand_rp >= rise_min
|
||||||
|
and cand_rp <= IR_TIMEOUT
|
||||||
|
)
|
||||||
|
if short_low_glitch:
|
||||||
|
st.err_other += 1
|
||||||
|
if GLITCH_REJECT_PHASE_NUDGE:
|
||||||
|
st.prev_rise = glitch_phase_nudge(t_us, rise_sync_time, st.prev_rise)
|
||||||
|
skip_rest = True
|
||||||
|
if not skip_rest and MICRO_GAP_RISE_REJECT:
|
||||||
|
lt_ok = (cand_lt >= rise_min) or (cand_lt >= (rise_min // 4) and cand_lt < rise_min)
|
||||||
|
micro_gap = (
|
||||||
|
st.is_recive
|
||||||
|
and not st.is_preamb
|
||||||
|
and cand_ht < (rise_min // 8)
|
||||||
|
and lt_ok
|
||||||
|
and cand_rp >= (rise_min // 4)
|
||||||
|
and cand_rp < rise_min
|
||||||
|
and cand_rp <= IR_TIMEOUT
|
||||||
|
)
|
||||||
|
if micro_gap:
|
||||||
|
st.err_other += 1
|
||||||
|
if GLITCH_REJECT_PHASE_NUDGE:
|
||||||
|
st.prev_rise = glitch_phase_nudge(t_us, rise_sync_time, st.prev_rise)
|
||||||
|
skip_rest = True
|
||||||
|
if not skip_rest and cand_rp <= rise_max / 4 and not st.high_count and not st.low_count:
|
||||||
|
st.err_other += 1
|
||||||
|
skip_rest = True
|
||||||
|
if not skip_rest:
|
||||||
|
# Как IR_DecoderRaw::tick: до обновления prev_rise, иначе (t_us - prev_rise) == 0 на подъёме.
|
||||||
|
if cand_rp > irmax * 2 and not st.is_recive_raw:
|
||||||
|
first_rx(st)
|
||||||
|
st.preamb_front_counter = PREAMB_FRONTS - 1
|
||||||
|
st.is_preamb = True
|
||||||
|
st.is_recive = True
|
||||||
|
st.is_recive_raw = True
|
||||||
|
st.is_wrong_pack = False
|
||||||
|
st.rise_period = cand_rp
|
||||||
|
st.high_time = cand_ht
|
||||||
|
st.low_time = cand_lt
|
||||||
|
st.prev_rise = t_us
|
||||||
|
else:
|
||||||
|
if t_us - st.prev_fall > rise_min / 4:
|
||||||
|
st.prev_fall = t_us
|
||||||
|
else:
|
||||||
|
skip_err = False
|
||||||
|
if IN_MARK_DOUBLE_FALL_IGNORE:
|
||||||
|
hi_since_rise = t_us - st.prev_rise
|
||||||
|
mark_end_min = (rise_min * BIT_ACTIVE_TAKTS) // (
|
||||||
|
BIT_ACTIVE_TAKTS + BIT_PAUSE_TAKTS
|
||||||
|
)
|
||||||
|
skip_err = (
|
||||||
|
st.is_recive
|
||||||
|
and not st.is_preamb
|
||||||
|
and hi_since_rise < mark_end_min
|
||||||
|
)
|
||||||
|
if not skip_err:
|
||||||
|
st.err_other += 1
|
||||||
|
|
||||||
|
if skip_rest:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Старт нового кадра после длинной паузы (в прошивке буфер обнуляется через available/таймаут;
|
||||||
|
# для мульти-пакета в симуляции явно first_rx, иначе i_data_buffer залипает).
|
||||||
|
if t_us > st.prev_rise and (t_us - st.prev_rise) > irmax * 2 and not st.is_recive_raw:
|
||||||
|
first_rx(st)
|
||||||
|
st.preamb_front_counter = PREAMB_FRONTS - 1
|
||||||
|
st.is_preamb = True
|
||||||
|
st.is_recive = True
|
||||||
|
st.is_recive_raw = True
|
||||||
|
st.is_wrong_pack = False
|
||||||
|
|
||||||
|
if st.preamb_front_counter:
|
||||||
|
if rising and st.rise_period < irmax:
|
||||||
|
if st.rise_period < rise_min // 2:
|
||||||
|
st.preamb_front_counter += 2
|
||||||
|
st.err_other += 1
|
||||||
|
st.preamb_front_counter -= 1
|
||||||
|
else:
|
||||||
|
if st.is_preamb:
|
||||||
|
st.is_preamb = False
|
||||||
|
st.prev_rise += st.rise_period / 2.0
|
||||||
|
return
|
||||||
|
|
||||||
|
if st.is_preamb:
|
||||||
|
return
|
||||||
|
|
||||||
|
if st.rise_period > irmax or st.is_buffer_overflow or st.rise_period < rise_min or st.is_wrong_pack:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not rising:
|
||||||
|
return
|
||||||
|
|
||||||
|
st.high_count = st.low_count = st.all_count = 0
|
||||||
|
invert_err = False
|
||||||
|
|
||||||
|
def write_to_buffer(bit: bool, invert_fix: bool) -> None:
|
||||||
|
if st.i_data_buffer > DATA_BYTE_SIZE_MAX * 8:
|
||||||
|
st.is_buffer_overflow = True
|
||||||
|
if st.is_buffer_overflow or st.is_preamb or st.is_wrong_pack:
|
||||||
|
st.is_recive = False
|
||||||
|
st.is_recive_raw = False
|
||||||
|
first_rx(st)
|
||||||
|
return
|
||||||
|
if st.buf_bit_pos == st.next_control_bit:
|
||||||
|
st.next_control_bit += SYNC_BITS if st.is_data else BIT_PER_BYTE
|
||||||
|
st.is_data = not st.is_data
|
||||||
|
st.i_sync_bit = 0
|
||||||
|
st.err_sync_bit = 0
|
||||||
|
if st.is_data:
|
||||||
|
bi = st.i_data_buffer
|
||||||
|
st.data_buffer[bi // 8] |= (1 if bit else 0) << (7 - (bi % 8))
|
||||||
|
st.i_data_buffer += 1
|
||||||
|
st.buf_bit_pos += 1
|
||||||
|
st.bits_log.append((t_us, "D", 1 if bit else 0))
|
||||||
|
else:
|
||||||
|
if st.i_sync_bit == 0:
|
||||||
|
last_b = (st.data_buffer[((st.i_data_buffer - 1) // 8)] >> (7 - ((st.i_data_buffer - 1) % 8))) & 1
|
||||||
|
if bit != bool(last_b):
|
||||||
|
st.buf_bit_pos += 1
|
||||||
|
st.i_sync_bit += 1
|
||||||
|
st.bits_log.append((t_us, "S", 1 if bit else 0))
|
||||||
|
else:
|
||||||
|
st.i_sync_bit = 0
|
||||||
|
st.err_other += 1
|
||||||
|
st.err_sync_bit += 1
|
||||||
|
if st.err_sync_bit >= SYNC_BITS:
|
||||||
|
st.is_wrong_pack = True
|
||||||
|
else:
|
||||||
|
st.buf_bit_pos += 1
|
||||||
|
st.i_sync_bit += 1
|
||||||
|
st.bits_log.append((t_us, "S", 1 if bit else 0))
|
||||||
|
st.is_wrong_pack = st.err_sync_bit >= SYNC_BITS
|
||||||
|
if st.is_data and not st.is_wrong_pack:
|
||||||
|
if st.i_data_buffer == 8 * MSG_BYTES:
|
||||||
|
st.pack_size = st.data_buffer[0] & IR_MASK_MSG_INFO
|
||||||
|
if st.pack_size and st.i_data_buffer == st.pack_size * BIT_PER_BYTE:
|
||||||
|
ok = crc_check(st.data_buffer, st.pack_size)
|
||||||
|
st.packets.append((ok, st.pack_size, bytes(st.data_buffer[: st.pack_size])))
|
||||||
|
st.is_recive = False
|
||||||
|
st.is_recive_raw = False
|
||||||
|
# Как в IR_DecoderRaw: буфер не чистят на успешном CRC; сброс по listenStart/checkTimeout/firstRX
|
||||||
|
|
||||||
|
if around_rise_period(st.rise_period, rise_sync_time):
|
||||||
|
if st.high_time > st.low_time:
|
||||||
|
write_to_buffer(True, False)
|
||||||
|
else:
|
||||||
|
write_to_buffer(False, False)
|
||||||
|
elif RISE_GRAY_SINGLE_BIT_FALLBACK and rise_gray_single_bit_fallback(st.rise_period, rise_sync_time):
|
||||||
|
st.err_other += 1
|
||||||
|
if st.high_time > st.low_time:
|
||||||
|
write_to_buffer(True, False)
|
||||||
|
else:
|
||||||
|
write_to_buffer(False, False)
|
||||||
|
else:
|
||||||
|
hc = ceil_div(min(st.high_time, 0xFFFF), rise_sync_time)
|
||||||
|
lc = ceil_div(min(st.low_time, 0xFFFF), rise_sync_time)
|
||||||
|
ac = ceil_div(min(st.rise_period, 0xFFFF), rise_sync_time)
|
||||||
|
st.high_count = min(hc, 127)
|
||||||
|
st.low_count = min(lc, 127)
|
||||||
|
st.all_count = min(ac, 127)
|
||||||
|
if st.high_count == 0 and st.high_time > rise_sync_time // 3:
|
||||||
|
st.high_count += 1
|
||||||
|
st.err_other += 1
|
||||||
|
if st.low_count + st.high_count > st.all_count:
|
||||||
|
if st.low_count > st.high_count:
|
||||||
|
st.low_count = st.all_count - st.high_count
|
||||||
|
st.err_low += st.low_count
|
||||||
|
elif st.low_count < st.high_count:
|
||||||
|
st.high_count = st.all_count - st.low_count
|
||||||
|
st.err_high += st.high_count
|
||||||
|
elif st.low_count == st.high_count:
|
||||||
|
invert_err = True
|
||||||
|
st.err_other += st.all_count
|
||||||
|
if st.low_count < st.high_count:
|
||||||
|
st.err_high += st.high_count
|
||||||
|
else:
|
||||||
|
st.err_low += st.low_count
|
||||||
|
# Как IR_DecoderRaw.cpp / IrFoxDecoder: не более 8 LOW и 8 HIGH за один подъём (i < n && 8 - i).
|
||||||
|
i = 0
|
||||||
|
while i < st.low_count and (8 - i):
|
||||||
|
if i == st.low_count - 1 and invert_err:
|
||||||
|
invert_err = False
|
||||||
|
write_to_buffer(True, True)
|
||||||
|
else:
|
||||||
|
write_to_buffer(False, False)
|
||||||
|
i += 1
|
||||||
|
i = 0
|
||||||
|
while i < st.high_count and (8 - i):
|
||||||
|
if i == st.high_count - 1 and invert_err:
|
||||||
|
invert_err = False
|
||||||
|
write_to_buffer(False, True)
|
||||||
|
else:
|
||||||
|
write_to_buffer(True, False)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if trace_rise is not None and rising and not skip_rest:
|
||||||
|
# После decode: залогировать только реальные записи битов (не преамбула, не ранний return выше)
|
||||||
|
if (
|
||||||
|
not st.is_preamb
|
||||||
|
and st.rise_period <= irmax
|
||||||
|
and not st.is_buffer_overflow
|
||||||
|
and st.rise_period >= rise_min
|
||||||
|
):
|
||||||
|
rec: dict = {
|
||||||
|
"t_us": t_us,
|
||||||
|
"rp": st.rise_period,
|
||||||
|
"ht": st.high_time,
|
||||||
|
"lt": st.low_time,
|
||||||
|
"i_buf": st.i_data_buffer,
|
||||||
|
"n_pkt": len(st.packets),
|
||||||
|
}
|
||||||
|
if around_rise_period(st.rise_period, rise_sync_time):
|
||||||
|
rec["branch"] = "around"
|
||||||
|
rec["bits_out"] = 1
|
||||||
|
elif RISE_GRAY_SINGLE_BIT_FALLBACK and rise_gray_single_bit_fallback(
|
||||||
|
st.rise_period, rise_sync_time
|
||||||
|
):
|
||||||
|
rec["branch"] = "gray"
|
||||||
|
rec["bits_out"] = 1
|
||||||
|
else:
|
||||||
|
rec["branch"] = "ceil"
|
||||||
|
rec["hc"] = st.high_count
|
||||||
|
rec["lc"] = st.low_count
|
||||||
|
rec["ac"] = st.all_count
|
||||||
|
rec["bits_out"] = st.low_count + st.high_count
|
||||||
|
trace_rise.append(rec)
|
||||||
|
|
||||||
|
|
||||||
|
def run_file(path: Path, max_packets: int = 0) -> SimState | None:
|
||||||
|
"""max_packets=0 — обработать весь файл (для регрессии по всем пакетам)."""
|
||||||
|
rows = parse_raw_csv(path)
|
||||||
|
if rows is None:
|
||||||
|
return None
|
||||||
|
edges = segments_to_edges(rows)
|
||||||
|
st = SimState()
|
||||||
|
rise_sync = BIT_TIME_US
|
||||||
|
for t_us, rising in edges:
|
||||||
|
tick(st, t_us, rising, rise_sync)
|
||||||
|
if max_packets and len(st.packets) >= max_packets:
|
||||||
|
break
|
||||||
|
return st
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
root = Path(__file__).resolve().parents[1]
|
||||||
|
default_set = [
|
||||||
|
("CAR", root / "Analyzer" / "raw" / "car_raw.txt"),
|
||||||
|
("POINT", root / "Analyzer" / "raw" / "point_raw.txt"),
|
||||||
|
("CARRAW3", root / "Analyzer" / "raw" / "carraw3.txt"),
|
||||||
|
("POINTRA3", root / "Analyzer" / "raw" / "pointraw_3.txt"),
|
||||||
|
]
|
||||||
|
files = default_set
|
||||||
|
if len(sys.argv) >= 2:
|
||||||
|
files = [(Path(p).name, Path(p)) for p in sys.argv[1:]]
|
||||||
|
|
||||||
|
for label, p in files:
|
||||||
|
if not p.exists():
|
||||||
|
print(f"skip {label}: {p} not found")
|
||||||
|
continue
|
||||||
|
st = run_file(p, max_packets=0)
|
||||||
|
print(f"=== {label} {p.name} ===")
|
||||||
|
if st is None:
|
||||||
|
print(
|
||||||
|
" (skip: decoder trace CSV Type/bit_idx, or missing Level+Duration; need Saleae level like car_raw.txt)"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
n_ok = sum(1 for x in st.packets if x[0])
|
||||||
|
n_bad = len(st.packets) - n_ok
|
||||||
|
print(
|
||||||
|
f" packets={len(st.packets)} crc_ok={n_ok} crc_bad={n_bad} "
|
||||||
|
f"err_other={st.err_other} err_low={st.err_low} err_high={st.err_high}"
|
||||||
|
)
|
||||||
|
for i, (ok, psz, raw) in enumerate(st.packets[:12]):
|
||||||
|
hx = raw.hex(" ")
|
||||||
|
print(f" pkt[{i}] crc_ok={ok} len={psz} {hx}")
|
||||||
|
if len(st.packets) > 12:
|
||||||
|
print(f" ... ({len(st.packets) - 12} more packets)")
|
||||||
|
print(f" first 24 bits: {st.bits_log[:24]}")
|
||||||
|
|
||||||
|
car = root / "Analyzer" / "raw" / "car_raw.txt"
|
||||||
|
point = root / "Analyzer" / "raw" / "point_raw.txt"
|
||||||
|
if car.exists() and point.exists():
|
||||||
|
sc = run_file(car, max_packets=1)
|
||||||
|
sp = run_file(point, max_packets=1)
|
||||||
|
if sc is None or sp is None:
|
||||||
|
return
|
||||||
|
print("\n=== First packet bit diff (D/S sequence) CAR vs POINT ===")
|
||||||
|
for i, (a, b) in enumerate(zip(sc.bits_log, sp.bits_log)):
|
||||||
|
if a != b:
|
||||||
|
print(f" idx {i}: CAR {a} vs POINT {b}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if len(sc.bits_log) != len(sp.bits_log):
|
||||||
|
print(f" len CAR={len(sc.bits_log)} POINT={len(sp.bits_log)}")
|
||||||
|
else:
|
||||||
|
print(" identical bit logs for min(len)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
726
tools/ir_decoder_sim.py
Normal file
726
tools/ir_decoder_sim.py
Normal file
@ -0,0 +1,726 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Офлайн-симуляция IR_DecoderRaw::tick + writeToBuffer по журналу @IRF1v1:
|
||||||
|
См. ref/IR_EDGE_TRACE_FORMAT.md
|
||||||
|
|
||||||
|
При FRAME_END: сводка (чистые биты / burst / отброшенные фронты), список точечных
|
||||||
|
исправлений (преамбула, пропуск такта, поджатие low/high, sync), при crc_ok=False —
|
||||||
|
разбор несовпадения CRC; в первой строке пакет как hex= и bin= (8 бит на байт MSB-first,
|
||||||
|
байты через пробел); отдельная строка «синхро» — счётчик ошибок 1-го бита тройки (как
|
||||||
|
err_syncBit в прошивке) и принятые тройки sync-битов между байтами (2-й и 3-й в прошивке
|
||||||
|
не сверяются с шаблоном, только пишутся в поток — их значения для наглядности сдвига).
|
||||||
|
WRONG_PACK_SYNC — отдельное событие с причиной и тем же блоком «синхро».
|
||||||
|
|
||||||
|
Флаг -v/--verbose: дополнительно каждая строка для «чистого» бита (aroundRise) и
|
||||||
|
отброшенных фронтов; без флага эти события только в счётчиках сводки.
|
||||||
|
|
||||||
|
Не моделирует IRDEBUG_SERIAL_SOFT_REJECT (жёсткий Wrong sync).
|
||||||
|
Таймауты: как IR_DecoderRaw::tick — listenStart и checkTimeout в начале каждого тика (не только при пустых
|
||||||
|
очередях), пауза > IR_timeout*2 по lastEdgeTime; при checkTimeout: isReciveRaw=0, firstRX(), lastEdgeTime=now.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import binascii
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
# --- IR_config.h (как в прошивке, целочисленно) ---
|
||||||
|
CARRIER_FREC = 38000
|
||||||
|
CARRIER_PERIOD = 1000000 // CARRIER_FREC
|
||||||
|
BIT_ACTIVE_TAKTS = 25
|
||||||
|
BIT_PAUSE_TAKTS = 12
|
||||||
|
BIT_TAKTS = BIT_ACTIVE_TAKTS + BIT_PAUSE_TAKTS
|
||||||
|
BIT_TIME = BIT_TAKTS * CARRIER_PERIOD
|
||||||
|
TOLERANCE = 300
|
||||||
|
SYNCBITS = 3
|
||||||
|
BIT_PER_BYTE = 8
|
||||||
|
MSGBYTES = 1
|
||||||
|
CRC_BYTES = 2
|
||||||
|
POLY1 = 0x31
|
||||||
|
POLY2 = 0x8C
|
||||||
|
IR_MASK_MSG_INFO = 0x1F
|
||||||
|
PREAMB_PULSE = 3
|
||||||
|
PREAMB_FRONTS = PREAMB_PULSE * 2
|
||||||
|
BYTE_PER_PACK = 31
|
||||||
|
DATA_BYTE_SIZE_MAX = MSGBYTES + 2 + 2 + BYTE_PER_PACK + CRC_BYTES
|
||||||
|
FREE_FREC = False
|
||||||
|
SKIP_DECODE_FLAG = 0x01
|
||||||
|
|
||||||
|
# Myagkie tsveta dlya terminala (256-color), ne iarkie default 31/32
|
||||||
|
_ANSI_GREEN = "\033[38;5;107m" # priglushennyj zelenyj (ne iarkij 32)
|
||||||
|
_ANSI_RED = "\033[38;5;174m" # pylno-rozovyj / myagkij krasnyj (ne iarkij 31)
|
||||||
|
_ANSI_RESET = "\033[0m"
|
||||||
|
# Zolotistyj / birjuzovyj + zhirnyj dlya hex i bin paketa v stroke FRAME_END
|
||||||
|
_HEX_PACKET_FG = "\033[1;38;5;222m"
|
||||||
|
_BIN_PACKET_FG = "\033[1;38;5;109m"
|
||||||
|
|
||||||
|
|
||||||
|
def _bytes_bin_msb(data: bytes) -> str:
|
||||||
|
"""8 bit na bajt (MSB pervyj, kak v writeToBuffer), bajty cherez probel."""
|
||||||
|
return " ".join(f"{b:08b}" for b in data)
|
||||||
|
|
||||||
|
|
||||||
|
def _use_terminal_color() -> bool:
|
||||||
|
if os.environ.get("NO_COLOR"):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return sys.stdout.isatty()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _colorize_block(text: str, ok: bool, enabled: bool) -> str:
|
||||||
|
if not enabled:
|
||||||
|
return text
|
||||||
|
code = _ANSI_GREEN if ok else _ANSI_RED
|
||||||
|
return f"{code}{text}{_ANSI_RESET}"
|
||||||
|
|
||||||
|
|
||||||
|
def _highlight_hex_bin_in_frame_line(line: str, ok: bool, enabled: bool) -> str:
|
||||||
|
"""Odna stroka s FRAME_END: vydeljaet hex= i bin=."""
|
||||||
|
if not enabled or "FRAME_END" not in line:
|
||||||
|
return line
|
||||||
|
parent = _ANSI_GREEN if ok else _ANSI_RED
|
||||||
|
after = "\033[22m" + parent
|
||||||
|
spans: list[tuple[int, int, str]] = []
|
||||||
|
mhx = re.search(r"hex=([0-9a-fA-F]+)", line)
|
||||||
|
if mhx:
|
||||||
|
spans.append((mhx.start(1), mhx.end(1), _HEX_PACKET_FG))
|
||||||
|
mbn = re.search(r"bin=([01 ]+)$", line)
|
||||||
|
if mbn:
|
||||||
|
spans.append((mbn.start(1), mbn.end(1), _BIN_PACKET_FG))
|
||||||
|
if not spans:
|
||||||
|
return line
|
||||||
|
spans.sort(key=lambda t: t[0])
|
||||||
|
out: list[str] = []
|
||||||
|
last = 0
|
||||||
|
for s, e, col in spans:
|
||||||
|
out.append(line[last:s])
|
||||||
|
out.append(col + line[s:e] + after)
|
||||||
|
last = e
|
||||||
|
out.append(line[last:])
|
||||||
|
return "".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def _highlight_frame_end_payloads(ev: str, ok: bool, enabled: bool) -> str:
|
||||||
|
"""Vo vseh strokah FRAME_END vydeljaet hex= i bin= (v tom chisle povtor svodki v konce)."""
|
||||||
|
if not enabled:
|
||||||
|
return ev
|
||||||
|
return "\n".join(_highlight_hex_bin_in_frame_line(L, ok, enabled) for L in ev.split("\n"))
|
||||||
|
|
||||||
|
|
||||||
|
def _packet_event_tone(ev: str) -> str | None:
|
||||||
|
"""'ok' | 'bad' | None — dlya pokrashki celogo bloka sobytija."""
|
||||||
|
head = ev.split("\n", 1)[0]
|
||||||
|
if "FRAME_END" in head:
|
||||||
|
if "crc_ok=True" in head:
|
||||||
|
return "ok"
|
||||||
|
if "crc_ok=False" in head:
|
||||||
|
return "bad"
|
||||||
|
if "WRONG_PACK_SYNC" in head:
|
||||||
|
return "bad"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def rise_time_max(rise_sync: int) -> int:
|
||||||
|
return rise_sync + TOLERANCE
|
||||||
|
|
||||||
|
|
||||||
|
def rise_time_min(rise_sync: int) -> int:
|
||||||
|
return rise_sync - TOLERANCE
|
||||||
|
|
||||||
|
|
||||||
|
def ir_timeout_us(rise_sync: int) -> int:
|
||||||
|
return rise_time_max(rise_sync) * (8 + SYNCBITS + 1)
|
||||||
|
|
||||||
|
|
||||||
|
def around_rise(t: int, rise_sync: int) -> bool:
|
||||||
|
return rise_time_min(rise_sync) < t < rise_time_max(rise_sync)
|
||||||
|
|
||||||
|
|
||||||
|
def ceil_div(val: int, divider: int) -> int:
|
||||||
|
ret = val // divider
|
||||||
|
if (val << 4) // divider - (ret << 4) >= 8:
|
||||||
|
ret += 1
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def crc8(data: bytes, start: int, end: int, poly: int) -> int:
|
||||||
|
crc = 0xFF
|
||||||
|
for i in range(start, end):
|
||||||
|
crc ^= data[i]
|
||||||
|
for _ in range(8):
|
||||||
|
if crc & 0x80:
|
||||||
|
crc = ((crc << 1) ^ poly) & 0xFF
|
||||||
|
else:
|
||||||
|
crc = (crc << 1) & 0xFF
|
||||||
|
return crc
|
||||||
|
|
||||||
|
|
||||||
|
def crc_compute_bytes(data: bytearray, pack_size: int) -> Tuple[int, int, int]:
|
||||||
|
"""Ожидаемые байты CRC: (crc1_hi, crc2_lo, len_payload) или (-1,-1,-1) при неверном pack_size."""
|
||||||
|
if pack_size < 1 + CRC_BYTES or pack_size > DATA_BYTE_SIZE_MAX:
|
||||||
|
return (-1, -1, -1)
|
||||||
|
ln = pack_size - CRC_BYTES
|
||||||
|
c1 = crc8(bytes(data), 0, ln, POLY1)
|
||||||
|
c2 = crc8(bytes(data), 0, ln + 1, POLY2)
|
||||||
|
return (c1 & 0xFF, c2 & 0xFF, ln)
|
||||||
|
|
||||||
|
|
||||||
|
def crc_check(data: bytearray, pack_size: int) -> bool:
|
||||||
|
ln = pack_size - CRC_BYTES
|
||||||
|
c1 = crc8(bytes(data), 0, ln, POLY1)
|
||||||
|
c2 = crc8(bytes(data), 0, ln + 1, POLY2)
|
||||||
|
crc = ((c1 << 8) & ~0xFF) | (c2 & 0xFF)
|
||||||
|
return data[ln] == ((crc >> 8) & 0xFF) and data[ln + 1] == (crc & 0xFF)
|
||||||
|
|
||||||
|
|
||||||
|
def crc_failure_lines(data: bytearray, pack_size: int) -> List[str]:
|
||||||
|
"""Подробности при несовпадении CRC."""
|
||||||
|
out: List[str] = []
|
||||||
|
exp_hi, exp_lo, ln = crc_compute_bytes(data, pack_size)
|
||||||
|
if ln < 0:
|
||||||
|
out.append(f" некорректный pack_size={pack_size} (ожидается 1..{DATA_BYTE_SIZE_MAX})")
|
||||||
|
return out
|
||||||
|
got_hi = data[pack_size - 2]
|
||||||
|
got_lo = data[pack_size - 1]
|
||||||
|
hdr = data[0]
|
||||||
|
msg_t = (hdr >> 5) & 0x07
|
||||||
|
out.append(
|
||||||
|
f" байт[0]=0x{hdr:02x} тип_сообщения={msg_t} заявл._длина_кадра={hdr & IR_MASK_MSG_INFO} байт"
|
||||||
|
)
|
||||||
|
out.append(
|
||||||
|
f" правило CRC: crc8(data[0..{ln - 1}], poly1=0x{POLY1:02x}) -> байт[{ln}]; "
|
||||||
|
f"crc8(data[0..{ln}], poly2=0x{POLY2:02x}) -> байт[{ln + 1}]"
|
||||||
|
)
|
||||||
|
out.append(f" ожидалось: {exp_hi:02x} {exp_lo:02x} принято в кадре: {got_hi:02x} {got_lo:02x}")
|
||||||
|
if got_hi != exp_hi:
|
||||||
|
out.append(
|
||||||
|
f" первый байт CRC не совпал - искажены данные [0..{ln - 1}] и/или этот байт CRC"
|
||||||
|
)
|
||||||
|
if got_lo != exp_lo:
|
||||||
|
out.append(
|
||||||
|
" второй байт CRC не совпал - в poly2 входит уже первый байт CRC; типично сдвиг битовой границы"
|
||||||
|
)
|
||||||
|
pl = data[:ln].hex()
|
||||||
|
out.append(f" полезная нагрузка без CRC ({ln} байт): {pl}")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EdgeRec:
|
||||||
|
t_us: int
|
||||||
|
level: int # 0/1 после фронта
|
||||||
|
flags: int
|
||||||
|
|
||||||
|
|
||||||
|
def parse_irf1_lines(text: str) -> List[EdgeRec]:
|
||||||
|
out: List[EdgeRec] = []
|
||||||
|
pat = re.compile(r"@IRF1v1:([0-9a-fA-F]+)\s*")
|
||||||
|
for m in pat.finditer(text):
|
||||||
|
hx = m.group(1)
|
||||||
|
if len(hx) % 2:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
raw = binascii.unhexlify(hx)
|
||||||
|
except binascii.Error:
|
||||||
|
continue
|
||||||
|
if len(raw) < 3:
|
||||||
|
continue
|
||||||
|
meta = raw[0]
|
||||||
|
count = raw[1] | (raw[2] << 8)
|
||||||
|
need = 3 + count * 6
|
||||||
|
if len(raw) < need or count > 2000:
|
||||||
|
continue
|
||||||
|
p = 3
|
||||||
|
for _ in range(count):
|
||||||
|
t = raw[p] | (raw[p + 1] << 8) | (raw[p + 2] << 16) | (raw[p + 3] << 24)
|
||||||
|
lvl = raw[p + 4]
|
||||||
|
flg = raw[p + 5]
|
||||||
|
out.append(EdgeRec(t, lvl & 1, flg))
|
||||||
|
p += 6
|
||||||
|
if meta & 1:
|
||||||
|
pass # overflow — запись могла обрезаться
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DecoderSim:
|
||||||
|
prev_rise: int = 0
|
||||||
|
prev_fall: int = 0
|
||||||
|
rise_period: int = 0
|
||||||
|
high_time: int = 0
|
||||||
|
low_time: int = 0
|
||||||
|
last_edge_time: int = 0
|
||||||
|
preamb_front_counter: int = 0
|
||||||
|
is_preamb: bool = False
|
||||||
|
is_recive: bool = False
|
||||||
|
is_recive_raw: bool = False
|
||||||
|
is_wrong_pack: bool = False
|
||||||
|
is_buffer_overflow: bool = False
|
||||||
|
rise_sync_time: int = BIT_TIME
|
||||||
|
high_count: int = 0
|
||||||
|
low_count: int = 0
|
||||||
|
all_count: int = 0
|
||||||
|
i_data_buffer: int = 0
|
||||||
|
buf_bit_pos: int = 0
|
||||||
|
next_control_bit: int = BIT_PER_BYTE
|
||||||
|
is_data: bool = True
|
||||||
|
i_sync_bit: int = 0
|
||||||
|
err_sync_bit: int = 0
|
||||||
|
data_buffer: bytearray = field(default_factory=lambda: bytearray(DATA_BYTE_SIZE_MAX))
|
||||||
|
pack_size: int = 0
|
||||||
|
errors_other: int = 0
|
||||||
|
events: List[str] = field(default_factory=list)
|
||||||
|
verbose: bool = False
|
||||||
|
"""Подстроеки/исправления как в IR_DecoderRaw::tick (за текущий пакет)."""
|
||||||
|
packet_fixes: List[str] = field(default_factory=list)
|
||||||
|
_fatal_sync_event_sent: bool = False
|
||||||
|
stat_clean_bits: int = 0
|
||||||
|
stat_burst_edges: int = 0
|
||||||
|
stat_debounce_rise: int = 0
|
||||||
|
stat_debounce_fall: int = 0
|
||||||
|
# Mezhdu bajtami: trojki sync-bitov (kak v writeToBuffer); oshibka schetaetsja tolko za 1-j bit
|
||||||
|
stat_sync_first_error: int = 0
|
||||||
|
sync_groups: List[str] = field(default_factory=list)
|
||||||
|
sync_bits_current_group: List[int] = field(default_factory=list)
|
||||||
|
|
||||||
|
def _fix(self, msg: str) -> None:
|
||||||
|
self.packet_fixes.append(msg)
|
||||||
|
|
||||||
|
def _clear_packet_state(self) -> None:
|
||||||
|
self.packet_fixes.clear()
|
||||||
|
self._fatal_sync_event_sent = False
|
||||||
|
self.stat_clean_bits = 0
|
||||||
|
self.stat_burst_edges = 0
|
||||||
|
self.stat_debounce_rise = 0
|
||||||
|
self.stat_debounce_fall = 0
|
||||||
|
self.stat_sync_first_error = 0
|
||||||
|
self.sync_groups.clear()
|
||||||
|
self.sync_bits_current_group.clear()
|
||||||
|
|
||||||
|
def _sync_bit_consumed(self, bit_val: int) -> None:
|
||||||
|
"""Odin prinjatyj sync-bit (bufBitPos++ v vetke sync v proshivke)."""
|
||||||
|
self.sync_bits_current_group.append(bit_val & 1)
|
||||||
|
if len(self.sync_bits_current_group) == SYNCBITS:
|
||||||
|
self.sync_groups.append("".join(str(b) for b in self.sync_bits_current_group))
|
||||||
|
self.sync_bits_current_group.clear()
|
||||||
|
|
||||||
|
def _sync_summary_lines(self, *, with_firmware_note: bool = False) -> List[str]:
|
||||||
|
"""Stroki svodki po sinhrobitam dlya FRAME_END i WRONG_PACK_SYNC."""
|
||||||
|
sg = "/".join(self.sync_groups) if self.sync_groups else "—"
|
||||||
|
lines = [
|
||||||
|
f" синхро: ошибок_1-го_бита(как_в_IR_DecoderRaw)={self.stat_sync_first_error}; "
|
||||||
|
f"полных_троек={len(self.sync_groups)}; биты_троек={sg}"
|
||||||
|
]
|
||||||
|
if self.sync_bits_current_group:
|
||||||
|
tail = "".join(str(b) for b in self.sync_bits_current_group)
|
||||||
|
lines.append(f" синхро: незавершённая_тройка (уже приняты биты): {tail}")
|
||||||
|
if with_firmware_note:
|
||||||
|
lines.append(
|
||||||
|
" синхро: в прошивке при ошибке считается только случай «1-й бит тройки совпал с последним "
|
||||||
|
"data-битом» (errors.other++, err_syncBit); 2-й и 3-й sync-биты не сравниваются с эталоном."
|
||||||
|
)
|
||||||
|
return lines
|
||||||
|
|
||||||
|
def _emit_wrong_sync_fatal(self, t: int) -> None:
|
||||||
|
lines = [
|
||||||
|
f"t={t} WRONG_PACK_SYNC (аналог ERROR: Wrong sync bit в прошивке, err_sync_bit>={SYNCBITS})"
|
||||||
|
]
|
||||||
|
if self.packet_fixes:
|
||||||
|
lines.append(" подстройки и исправления до ошибки:")
|
||||||
|
for fx in self.packet_fixes:
|
||||||
|
lines.append(f" - {fx}")
|
||||||
|
lines.extend(self._sync_summary_lines(with_firmware_note=True))
|
||||||
|
lines.append(
|
||||||
|
" причина фатала: повторы неверного 1-го sync-бита накапливают err_syncBit до порога syncBits."
|
||||||
|
)
|
||||||
|
self.events.append("\n".join(lines))
|
||||||
|
|
||||||
|
def first_rx(self) -> None:
|
||||||
|
self.is_buffer_overflow = False
|
||||||
|
self.pack_size = 0
|
||||||
|
self.buf_bit_pos = 0
|
||||||
|
self.is_data = True
|
||||||
|
self.i_data_buffer = 0
|
||||||
|
self.next_control_bit = BIT_PER_BYTE
|
||||||
|
self.i_sync_bit = 0
|
||||||
|
self.err_sync_bit = 0
|
||||||
|
self.is_wrong_pack = False
|
||||||
|
self.data_buffer[:] = bytes(DATA_BYTE_SIZE_MAX)
|
||||||
|
self.rise_sync_time = BIT_TIME
|
||||||
|
self.stat_sync_first_error = 0
|
||||||
|
self.sync_groups.clear()
|
||||||
|
self.sync_bits_current_group.clear()
|
||||||
|
|
||||||
|
def listen_start(self, now: int) -> None:
|
||||||
|
to = ir_timeout_us(self.rise_sync_time)
|
||||||
|
if self.is_recive_raw and self.last_edge_time > 0 and (now - self.last_edge_time) > to * 2:
|
||||||
|
self.events.append(f"t={now} listenStart abort raw (gap since last edge, как IR_DecoderRaw)")
|
||||||
|
self.is_recive_raw = False
|
||||||
|
self._clear_packet_state()
|
||||||
|
self.first_rx()
|
||||||
|
|
||||||
|
def check_timeout(self, now: int) -> None:
|
||||||
|
if not self.is_recive:
|
||||||
|
return
|
||||||
|
to = ir_timeout_us(self.rise_sync_time)
|
||||||
|
if now - self.last_edge_time > to * 2:
|
||||||
|
self.events.append(f"t={now} checkTimeout -> isReciveRaw=0, firstRX() (как IR_DecoderRaw)")
|
||||||
|
self.is_recive = False
|
||||||
|
self.is_recive_raw = False
|
||||||
|
self._clear_packet_state()
|
||||||
|
self.first_rx()
|
||||||
|
# Не last_edge_time = now: в прошивке убрано — расхождение с метками фронтов из очереди.
|
||||||
|
|
||||||
|
def write_to_buffer(self, bit_val: int) -> None:
|
||||||
|
if self.i_data_buffer > DATA_BYTE_SIZE_MAX * 8:
|
||||||
|
self.is_buffer_overflow = True
|
||||||
|
self._fix("переполнение буфера битов (writeToBuffer: i_dataBuffer > dataByteSizeMax*8)")
|
||||||
|
if self.is_buffer_overflow or self.is_preamb or self.is_wrong_pack:
|
||||||
|
self.is_recive = False
|
||||||
|
self.is_recive_raw = False
|
||||||
|
self.first_rx()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.buf_bit_pos == self.next_control_bit:
|
||||||
|
self.next_control_bit += SYNCBITS if self.is_data else BIT_PER_BYTE
|
||||||
|
self.is_data = not self.is_data
|
||||||
|
self.i_sync_bit = 0
|
||||||
|
self.err_sync_bit = 0
|
||||||
|
|
||||||
|
if self.is_data:
|
||||||
|
bi = self.i_data_buffer // 8
|
||||||
|
self.data_buffer[bi] |= (bit_val & 1) << (7 - (self.i_data_buffer % 8))
|
||||||
|
self.i_data_buffer += 1
|
||||||
|
self.buf_bit_pos += 1
|
||||||
|
else:
|
||||||
|
if self.i_sync_bit == 0:
|
||||||
|
prev_bit = (
|
||||||
|
self.data_buffer[(self.i_data_buffer - 1) // 8]
|
||||||
|
>> (7 - (self.i_data_buffer - 1) % 8)
|
||||||
|
) & 1
|
||||||
|
if bit_val != prev_bit:
|
||||||
|
self.buf_bit_pos += 1
|
||||||
|
self.i_sync_bit += 1
|
||||||
|
self._sync_bit_consumed(bit_val)
|
||||||
|
else:
|
||||||
|
self.i_sync_bit = 0
|
||||||
|
self.errors_other += 1
|
||||||
|
self.err_sync_bit += 1
|
||||||
|
self.stat_sync_first_error += 1
|
||||||
|
self._fix(
|
||||||
|
f"sync: 1-й sync-бит совпал с последним data-битом (data={prev_bit}); "
|
||||||
|
f"err_sync_bit={self.err_sync_bit}/{SYNCBITS} (как в прошивке)"
|
||||||
|
)
|
||||||
|
if self.err_sync_bit >= SYNCBITS:
|
||||||
|
self.is_wrong_pack = True
|
||||||
|
if not self._fatal_sync_event_sent:
|
||||||
|
self._fatal_sync_event_sent = True
|
||||||
|
self._emit_wrong_sync_fatal(self.last_edge_time)
|
||||||
|
else:
|
||||||
|
self.buf_bit_pos += 1
|
||||||
|
self.i_sync_bit += 1
|
||||||
|
self._sync_bit_consumed(bit_val)
|
||||||
|
self.is_wrong_pack = self.err_sync_bit >= SYNCBITS
|
||||||
|
|
||||||
|
if self.is_data and not self.is_wrong_pack:
|
||||||
|
if self.i_data_buffer == 8 * MSGBYTES:
|
||||||
|
self.pack_size = self.data_buffer[0] & IR_MASK_MSG_INFO
|
||||||
|
if self.pack_size and self.i_data_buffer == self.pack_size * BIT_PER_BYTE:
|
||||||
|
ok = crc_check(self.data_buffer, self.pack_size)
|
||||||
|
raw = self.data_buffer[: self.pack_size]
|
||||||
|
hx = raw.hex()
|
||||||
|
bstr = _bytes_bin_msb(raw)
|
||||||
|
frame_line = (
|
||||||
|
f"t={self.last_edge_time} FRAME_END pack={self.pack_size} crc_ok={ok} hex={hx} bin={bstr}"
|
||||||
|
)
|
||||||
|
sync_lines = self._sync_summary_lines()
|
||||||
|
tick_summary = (
|
||||||
|
f" сводка тактов: чистых_битов_aroundRise={self.stat_clean_bits}, "
|
||||||
|
f"фронтов_с_burst-коррекцией={self.stat_burst_edges}, "
|
||||||
|
f"отброшенных_фронтов_up={self.stat_debounce_rise}, down={self.stat_debounce_fall}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _frame_summary_block() -> List[str]:
|
||||||
|
return [frame_line, *sync_lines, tick_summary]
|
||||||
|
|
||||||
|
lines: List[str] = []
|
||||||
|
lines.extend(_frame_summary_block())
|
||||||
|
if self.packet_fixes:
|
||||||
|
if self.verbose:
|
||||||
|
lines.append(
|
||||||
|
" подстройки и исправления за пакет, подробный режим (-v) (аналог IR_DecoderRaw::tick):"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lines.append(
|
||||||
|
" подстройки и исправления за пакет (преамбула, пропуск такта, burst, sync; "
|
||||||
|
"без строк по каждому «чистому» биту — включите -v):"
|
||||||
|
)
|
||||||
|
for fx in self.packet_fixes:
|
||||||
|
lines.append(f" - {fx}")
|
||||||
|
else:
|
||||||
|
lines.append(
|
||||||
|
" дополнительных исправлений нет (см. сводку; для строк по каждому биту: -v/--verbose)"
|
||||||
|
)
|
||||||
|
if not ok:
|
||||||
|
lines.append(" неуспешный пакет — причина:")
|
||||||
|
lines.extend(crc_failure_lines(self.data_buffer, self.pack_size))
|
||||||
|
lines.append(" --- сводка пакета (конец записи) ---")
|
||||||
|
lines.extend(_frame_summary_block())
|
||||||
|
self.events.append("\n".join(lines))
|
||||||
|
self.is_recive = False
|
||||||
|
self.is_recive_raw = False
|
||||||
|
self._clear_packet_state()
|
||||||
|
self.first_rx()
|
||||||
|
|
||||||
|
def tick_edge(self, t: int, level: int) -> None:
|
||||||
|
"""Один фронт: level = состояние линии ПОСЛЕ фронта (как dir в C++)."""
|
||||||
|
self.listen_start(t)
|
||||||
|
to = ir_timeout_us(self.rise_sync_time)
|
||||||
|
if self.is_recive and self.last_edge_time > 0 and (t - self.last_edge_time) > to * 2:
|
||||||
|
self.check_timeout(t)
|
||||||
|
self.last_edge_time = t
|
||||||
|
rising = level == 1
|
||||||
|
|
||||||
|
if rising:
|
||||||
|
cond = (t - self.prev_rise > rise_time_max(self.rise_sync_time) // 4) or self.high_count or self.low_count
|
||||||
|
if cond:
|
||||||
|
self.rise_period = t - self.prev_rise
|
||||||
|
self.high_time = t - self.prev_fall
|
||||||
|
self.low_time = self.prev_fall - self.prev_rise
|
||||||
|
self.prev_rise = t
|
||||||
|
else:
|
||||||
|
self.errors_other += 1
|
||||||
|
self.stat_debounce_rise += 1
|
||||||
|
if self.verbose:
|
||||||
|
self._fix(
|
||||||
|
f"t={t} отброшен фронт ↑: слишком короткий интервал до предыдущего ↑ "
|
||||||
|
f"(<= riseTimeMax/4 при hc=lc=0), errors.other++"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if t - self.prev_fall > rise_time_min(self.rise_sync_time) // 4:
|
||||||
|
self.prev_fall = t
|
||||||
|
else:
|
||||||
|
self.errors_other += 1
|
||||||
|
self.stat_debounce_fall += 1
|
||||||
|
if self.verbose:
|
||||||
|
self._fix(
|
||||||
|
f"t={t} отброшен фронт ↓: слишком короткий интервал до предыдущего ↓ (<= riseTimeMin/4), errors.other++"
|
||||||
|
)
|
||||||
|
|
||||||
|
rt = self.rise_sync_time
|
||||||
|
to = ir_timeout_us(rt)
|
||||||
|
if t > self.prev_rise and (t - self.prev_rise) > to * 2 and not self.is_recive_raw:
|
||||||
|
self.preamb_front_counter = PREAMB_FRONTS - 1
|
||||||
|
self.is_preamb = True
|
||||||
|
self.is_recive = True
|
||||||
|
self.is_recive_raw = True
|
||||||
|
self.is_wrong_pack = False
|
||||||
|
self._clear_packet_state()
|
||||||
|
self.events.append(f"t={t} PACKET_START (long idle)")
|
||||||
|
|
||||||
|
if self.preamb_front_counter:
|
||||||
|
if rising and self.rise_period < to:
|
||||||
|
if self.rise_period < rise_time_min(rt) // 2:
|
||||||
|
self.preamb_front_counter += 2
|
||||||
|
self.errors_other += 1
|
||||||
|
self._fix(
|
||||||
|
f"преамбула: «рваная единица» risePeriod={self.rise_period} us < riseTimeMin/2 "
|
||||||
|
f"({rise_time_min(rt) // 2} us) -> preambFrontCounter += 2, errors.other++"
|
||||||
|
)
|
||||||
|
elif FREE_FREC:
|
||||||
|
old = self.rise_sync_time
|
||||||
|
self.rise_sync_time = (self.rise_sync_time + self.rise_period // 2) // 2
|
||||||
|
self._fix(
|
||||||
|
f"преамбула: подстройка riseSyncTime {old}->{self.rise_sync_time} us (freeFrec)"
|
||||||
|
)
|
||||||
|
self.preamb_front_counter -= 1
|
||||||
|
else:
|
||||||
|
if self.is_preamb:
|
||||||
|
self.is_preamb = False
|
||||||
|
half = self.rise_period // 2
|
||||||
|
self.prev_rise += half
|
||||||
|
self._fix(
|
||||||
|
f"после преамбулы: prev_rise += risePeriod/2 (+{half} us) - фазовая привязка к центру бита"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.is_preamb:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.rise_period > to or self.is_buffer_overflow or self.rise_period < rise_time_min(rt) or self.is_wrong_pack:
|
||||||
|
if self.is_recive and rising and (self.rise_period > to or self.rise_period < rise_time_min(rt)):
|
||||||
|
reason = (
|
||||||
|
f"risePeriod={self.rise_period} us: "
|
||||||
|
+ (f"> IR_timeout={to} us " if self.rise_period > to else "")
|
||||||
|
+ (f"< riseTimeMin={rise_time_min(rt)} us " if self.rise_period < rise_time_min(rt) else "")
|
||||||
|
)
|
||||||
|
self._fix(f"t={t} пропуск такта (goto END): {reason.strip()}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not rising:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.high_count = 0
|
||||||
|
self.low_count = 0
|
||||||
|
self.all_count = 0
|
||||||
|
invert_err = False
|
||||||
|
rt = self.rise_sync_time
|
||||||
|
|
||||||
|
if around_rise(self.rise_period, rt):
|
||||||
|
self.stat_clean_bits += 1
|
||||||
|
bit = 1 if self.high_time > self.low_time else 0
|
||||||
|
if self.verbose:
|
||||||
|
self._fix(
|
||||||
|
f"t={t} «чистый» бит: aroundRise (risePeriod={self.rise_period} us в [{rise_time_min(rt)}..{rise_time_max(rt)}]), "
|
||||||
|
f"highTime={self.high_time} lowTime={self.low_time} us -> bit {bit}"
|
||||||
|
)
|
||||||
|
self.write_to_buffer(bit)
|
||||||
|
else:
|
||||||
|
self.stat_burst_edges += 1
|
||||||
|
self.high_count = ceil_div(self.high_time, rt)
|
||||||
|
self.low_count = ceil_div(self.low_time, rt)
|
||||||
|
self.all_count = ceil_div(self.rise_period, rt)
|
||||||
|
self._fix(
|
||||||
|
f"t={t} пропуск такта / растяжение: risePeriod={self.rise_period} us вне aroundRise "
|
||||||
|
f"[{rise_time_min(rt)}..{rise_time_max(rt)}]; "
|
||||||
|
f"ceil_div: highTime/{rt}->{self.high_count}, lowTime/{rt}->{self.low_count}, risePeriod/{rt}->{self.all_count}"
|
||||||
|
)
|
||||||
|
if self.high_count == 0 and self.high_time > rt // 3:
|
||||||
|
self.high_count += 1
|
||||||
|
self.errors_other += 1
|
||||||
|
self._fix(
|
||||||
|
f"доп. коррекция: highCount был 0 при highTime={self.high_time} > riseTime/3 ({rt // 3}) -> highCount++"
|
||||||
|
)
|
||||||
|
if self.low_count + self.high_count > self.all_count:
|
||||||
|
lo, hi, ac = self.low_count, self.high_count, self.all_count
|
||||||
|
if self.low_count > self.high_count:
|
||||||
|
self.low_count = self.all_count - self.high_count
|
||||||
|
self._fix(
|
||||||
|
f"поджатие: low+high>{ac} и low>high -> lowCount {lo}->{self.low_count} (лишние нули)"
|
||||||
|
)
|
||||||
|
elif self.low_count < self.high_count:
|
||||||
|
self.high_count = self.all_count - self.low_count
|
||||||
|
self._fix(
|
||||||
|
f"поджатие: low+high>{ac} и low<high -> highCount {hi}->{self.high_count} (лишние единицы)"
|
||||||
|
)
|
||||||
|
elif self.low_count == self.high_count:
|
||||||
|
invert_err = True
|
||||||
|
self.errors_other += self.all_count
|
||||||
|
self._fix(
|
||||||
|
f"поджатие: low==high при low+high>{ac} -> invertErr (последний из low-цикла пишется как 1)"
|
||||||
|
)
|
||||||
|
i = 0
|
||||||
|
while i < self.low_count and (8 - i):
|
||||||
|
if i == self.low_count - 1 and invert_err:
|
||||||
|
invert_err = False
|
||||||
|
self.write_to_buffer(1)
|
||||||
|
else:
|
||||||
|
self.write_to_buffer(0)
|
||||||
|
i += 1
|
||||||
|
i = 0
|
||||||
|
while i < self.high_count and (8 - i):
|
||||||
|
if i == self.high_count - 1 and invert_err:
|
||||||
|
invert_err = False
|
||||||
|
self.write_to_buffer(0)
|
||||||
|
else:
|
||||||
|
self.write_to_buffer(1)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
|
||||||
|
def timing_stats(edges: List[EdgeRec]) -> None:
|
||||||
|
dts: List[int] = []
|
||||||
|
for i in range(1, len(edges)):
|
||||||
|
d = edges[i].t_us - edges[i - 1].t_us
|
||||||
|
if 0 <= d < 1_000_000:
|
||||||
|
dts.append(d)
|
||||||
|
if not dts:
|
||||||
|
print("Нет интервалов для статистики.")
|
||||||
|
return
|
||||||
|
dts.sort()
|
||||||
|
def pct(p: float) -> int:
|
||||||
|
return dts[int(len(dts) * p)]
|
||||||
|
print("--- Inter-edge deltas in log (us) ---")
|
||||||
|
print(f" N={len(dts)} min={dts[0]} p50={pct(0.5)} p90={pct(0.9)} max={dts[-1]}")
|
||||||
|
print(
|
||||||
|
f" bitTime(ref)~{BIT_TIME} us aroundRise window ({rise_time_min(BIT_TIME)}..{rise_time_max(BIT_TIME)}) us"
|
||||||
|
)
|
||||||
|
# грубые корзины
|
||||||
|
buckets = [0, 0, 0, 0, 0]
|
||||||
|
for d in dts:
|
||||||
|
if d < 200:
|
||||||
|
buckets[0] += 1
|
||||||
|
elif d < 600:
|
||||||
|
buckets[1] += 1
|
||||||
|
elif d < 1200:
|
||||||
|
buckets[2] += 1
|
||||||
|
elif d < 3000:
|
||||||
|
buckets[3] += 1
|
||||||
|
else:
|
||||||
|
buckets[4] += 1
|
||||||
|
print(f" корзины [0-200) [200-600) [600-1200) [1200-3000) [3000+): {buckets}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser(description="Симуляция IR decode по @IRF1v1 логу")
|
||||||
|
ap.add_argument("logfile", nargs="?", default=None, help="Текстовый лог с @IRF1v1:")
|
||||||
|
ap.add_argument("--include-skipped", action="store_true", help="Подмешивать фронты с SKIP_DECODE (обычно нет)")
|
||||||
|
ap.add_argument("--max-events", type=int, default=80, help="Макс. событий FRAME_START/END в отчёте")
|
||||||
|
ap.add_argument(
|
||||||
|
"--no-color",
|
||||||
|
action="store_true",
|
||||||
|
help="Bez ANSI-tsvetov (ili zadajte NO_COLOR v okruzhenii)",
|
||||||
|
)
|
||||||
|
ap.add_argument(
|
||||||
|
"-v",
|
||||||
|
"--verbose",
|
||||||
|
action="store_true",
|
||||||
|
help="Podrobnyj vyvod: kazhdyj chistyj bit (aroundRise), otbrosy frontov; inache tolko svodka",
|
||||||
|
)
|
||||||
|
args = ap.parse_args()
|
||||||
|
if not args.logfile:
|
||||||
|
print("Укажите путь к логу, например: python tools/ir_decoder_sim.py ref/ISR_self_frontlog.txt")
|
||||||
|
return 1
|
||||||
|
text = open(args.logfile, "r", encoding="utf-8", errors="replace").read()
|
||||||
|
raw_edges = parse_irf1_lines(text)
|
||||||
|
edges = [e for e in raw_edges if args.include_skipped or not (e.flags & SKIP_DECODE_FLAG)]
|
||||||
|
edges.sort(key=lambda e: (e.t_us, id(e)))
|
||||||
|
print(f"Записей фронтов (после фильтра SKIP): {len(edges)} (всего распарсено: {len(raw_edges)})")
|
||||||
|
timing_stats(edges)
|
||||||
|
|
||||||
|
dec = DecoderSim(verbose=args.verbose)
|
||||||
|
for e in edges:
|
||||||
|
dec.tick_edge(e.t_us, e.level)
|
||||||
|
|
||||||
|
print("--- События декодера (первые N), пакеты разделены пустой строкой ---")
|
||||||
|
slice_ev = dec.events[: args.max_events]
|
||||||
|
first_packet = True
|
||||||
|
use_color = _use_terminal_color() and not args.no_color
|
||||||
|
for ev in slice_ev:
|
||||||
|
head = ev.split("\n", 1)[0]
|
||||||
|
if "PACKET_START" in head:
|
||||||
|
if not first_packet:
|
||||||
|
print()
|
||||||
|
first_packet = False
|
||||||
|
tone = _packet_event_tone(ev)
|
||||||
|
if tone is not None:
|
||||||
|
ok = tone == "ok"
|
||||||
|
ev_out = _highlight_frame_end_payloads(ev, ok=ok, enabled=use_color)
|
||||||
|
print(_colorize_block(ev_out, ok=ok, enabled=use_color))
|
||||||
|
else:
|
||||||
|
print(ev)
|
||||||
|
if len(dec.events) > args.max_events:
|
||||||
|
print(f"... всего событий: {len(dec.events)}")
|
||||||
|
print(f"errors_other={dec.errors_other} wrong_pack_end={dec.is_wrong_pack} recive={dec.is_recive}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user