39 Commits

Author SHA1 Message Date
c7efd1cb65 fix(rx): reject undersized typed frames safely 2026-08-28 14:54:06 +03:00
36f234739a feat(protocol): derive wire size and airtime at compile time 2026-08-28 14:53:55 +03:00
96ffb91b97 fix(tx): приоритет IRQ DMA-TX явно ниже SysTick/EXTI — millis() замедлялся под передачей
HAL_NVIC_EnableIRQ без SetPriority оставлял DMA-каналы TX на приоритете 0 (уровень
SysTick на G4). Длинный refill кольца BSRR (полубуфер 2048 слов = ~3.3мс при HCLK
12МГц) не вытеснялся, тики SysTick слипались: millis() замедлялся в 1.6x (один
излучатель) / 2.2x (два) на всё время передачи. На машинке это ~37% вранья часов
в каждый ИК-кадр: PID dt, планировщик, тайминги протокола.

Мерено на стенде TofSickHunt 2026-08-11 (Car raw/idle_cur_vs_coup*): прогоны «60с»
шли 85-95 реальных секунд. Фикс: irproto::kDmaTxIrqPriority=8 — ниже EXTI приёмника
(6), выше дна; SysTick честный. ISR-TX путь (F401/КУ) не подвержен: его refill на
TIM-приоритете 14. Верификация дилатации на стенде — раунд 4 (прерван, перезапустить).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:33:07 +03:00
08ea71eb5e feat(rx): always-on счётчики причин RX-событий (rxReasonCnt) + RXSTAT-сводка
rxBriefLog теперь безусловен: ВСЕГДА инкрементирует rxReasonCnt[reason]
(наблюдаемость по контракту живучести — работает в проде без печати),
печать события — только при IR_RX_BRIEF_LOG. 14 точек вызова развёрнуты
из-под #if (Glitch/Timing/Preamble/Sync/BufOverflow/Timeout/Crc/Ok +
pulse-filter пути); ISR-агрегатные MuteBegin/End/RawOverflow остаются
только при логе (их флаш живёт в brief-механике). Публичное API:
rxReasonCounters() / rxReasonCountersClear() / printRxReasonStats(Print&)
-> 'RXSTAT,GLITCH=..,TIME=..,...,OK=..'.
Компил-чек: LaserTestCheck (G4, флаг выкл) и Plan_B (F4, флаг вкл) — чисто.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 23:05:50 +03:00
5bcaf4092a perf(rx): батч-выемка фронтов в tick (IR_RX_TICK_BATCH=16) — приём развязан от частоты loop
Было: 1 сырой фронт за tick() -> RX-пропускная способность привязана к loop;
медленный loop (телеметрия/дисплей/КУ-печать) переполнял ISR-буфер 250 фронтов
(~120мс эфира) и кадры гибли молча (вероятная составляющая 'КУ отвечает не на
каждый пакет'). Стало: до IR_RX_TICK_BATCH фронтов за tick; idle-семантика
сохранена (flush пульс-фильтра только при пустом буфере; listenStart/
checkTimeout как раньше). Переопределяемо дефайном до include.
Компил-чек: LaserTestCheck (G491) собирается.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 18:22:44 +03:00
873277e59f docs 2026-07-01 15:33:49 +03:00
f93f4101c3 fix(tx): correct multi-byte BACK frames (packSize used min(1,len))
_sendBack и testSendBack считали packSize с min(uint8_t(1),len) → в кадр входил только 1 байт данных, остальные обрезались (и затирались CRC). Многобайтовый back (speed + customBackData/sendOrderedBack) слался битым. Теперь packSize = ...+len+crcBytes, плюс валидация полного packSize<=31 (IR_MASK_MSG_INFO). Для len=1 поведение не меняется.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:20:27 +03:00
bf14b3d7ad fix(rx/tx): B5 pop-by-value, B6 ISR micros mask, B7 crc parens, B10 div guard, send length validation
B5 (MEDIUM): RingBuffer::pop(T&) копирует под одной критсекцией; tick() перешёл на неё → нет торн-рида (внутренний interrupts() в T* pop снимал внешнюю защиту до чтения *ptr). T* pop() оставлен (не используется).
B6 (MEDIUM): убрана лишняя noInterrupts/interrupts вокруг micros() в EXTI-ISR (снимала PRIMASK посреди ISR). std::function-диспетчеризация attachInterrupt — структурна, не трогаю.
B7 (LOW): скобки в crcCheck (== & 0xFF по приоритету).
B10 (LOW): guard деления на 0 в ceil_div (актуально только при freeFrec — НЕ включаю).
send: sendDataFULL — валидация полного packSize<=31 (было len>bytePerPack=31, packSize=7+len оборачивался → тихая потеря Data payload 25..31).
B8 (isSubBufferOverflow) — уже volatile, потеря флага безвредна (диагностика), не трогаю. B9 (prevRise==0 при micros()==0) — уже обработан веткой в preambleProcessEdge, намеренно.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:38:51 +03:00
a8d32bbcef fix(rx): guard sub-minimum packSize before crcCheck (B1 OOB read)
packSize==1 давало crcCheck(1-crcBytes) → uint8 len=255 → crc8 читает dataBuffer[0..256] при массиве 38 (OOB-чтение ~217 байт). Триггер — лок преамбулы + первый байт с младшими 5 битами=1 (~1/32 ложных локов). Двойная защита: (1) ранний reject packSize 1..2 (< msg+crc) как битого; (2) конец кадра требует packSize>crcBytes, поэтому crcCheck(packSize-crcBytes) не уходит в underflow. Валидные кадры (packSize>=5) и будущие компактные (>=3) не затронуты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 12:13:27 +03:00
fc3510c425 fix(rx): guard null encoder in auto-accept (B2), init accept fields (B4), fix dataBuffer OOB off-by-one (B3)
B2 (HIGH): IR_Decoder::_tick — encoder->sendAccept() без null-check; у Car decoder создан с encoder==nullptr → HardFault при приёме IR_MSG_DATA_ACCEPT. Добавлен guard.
B4 (MEDIUM): acceptSendTimer/isWaitingAcceptSend/addrAcceptSendTo/acceptCustomByte не инициализировались → мусор мог спонтанно дёрнуть sendAccept. Дефолты в .h.
B3 (HIGH): writeToBuffer — '>' вместо '>=' → при i_dataBuffer==dataByteSizeMax*8 (304) запись dataBuffer[38] за границей 38-байтового массива (packSize==0 runaway). Валидные кадры закрываются при <=248, не затронуты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:34:12 +03:00
e5ed029cfd fix(ir-dma): __DSB() barriers before DMA arm and after circular refill (#8)
Перестраховка корректности (на M4F без D-кэша обычно не обязательно): завершить запись dmaBuf/BSRR до HAL_DMA_Start_IT и refill полубуфера до прохода циркулярного DMA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:12:28 +03:00
55935b8b92 fix(ir-dma): volatile active + errorCount() observability; live poll() in longData example
- IrDmaTxStm32.h: TxStream::active -> volatile (делится поток<->ISR: busy()/poll()/колбэки/спин applyCarrierMultiply)
- IrDmaTxStm32.h: + volatile errors_ и errorCount(), инкремент в dmaErrorCb (наблюдаемость Transfer-Error)
- test_examples/longData: dmaBackend.poll() в loop, иначе watchdog завершения DMA мёртв в эталонном примере

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 12:25:36 +03:00
7176fe5321 fix(ir-dma): eliminate isSending latch — free-running TIM, completion watchdog, busy() OR
C: remove racy activeCount_ + shared-timer gating; TIM17 free-running, per-stream DMA arming only (HAL_DMA_Start_IT/Abort_IT). Sets active before arming; never stops TIM on a shared counter.

D: poll() watchdog detects ticksOutput stall (>=stallTimeoutMs_, default 100ms) and forceStop()s the stuck stream under a per-channel NVIC mask; recoveryCount()/setStallTimeoutMs() accessors. dmaErrorCb now checks !active for symmetric single-stop.

E: busy() now returns true if ANY stream active (was AND) — honest contract for irAnyTxBusy()/carrier retune; does not serialize the send path.

Bug: IR TX rarely latched (LED frozen on/off) because stopStream was reachable only from the DMA HT/CPLT IRQ. Hardware bring-up (logic analyzer, recoveryCount telemetry) pending.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:26:52 +03:00
9935ac192a Merge pull request #11 from Show-maket/fix-DMA-and-Analyzer
Fix dma and analyzer
2026-04-21 07:05:29 +00:00
8361828c44 tx error tracking and inline glitch filter 2026-04-21 10:00:46 +03:00
38d93edd1b upd 2026-04-20 17:47:33 +03:00
2f5b57680b Add documentation for IR TX modes in the library 2026-04-20 14:49:38 +03:00
31ac7a3625 Refactor IR decoder and encoder for improved pulse filtering and ISR handling. Removed unused filtered sub-buffer, updated pulse filter methods, and added support for buffered ISR storage in the encoder. Enhanced documentation for clarity on DMA TX backend and ISR modes. 2026-04-20 14:48:45 +03:00
b4f644c258 Align main with fix-DMA-and-Analyzer (encoder API, sketch, .gitignore)
Restore IR_SendResult + sendDataFULL, fix IR-protocol.ino, match .gitignore to analyzer branch.

Made-with: Cursor
2026-04-17 15:07:54 +03:00
a75fd0c564 Merge branch 'main' of https://github.com/Show-maket/IR-protocol 2026-04-17 14:31:32 +03:00
4061a4ed10 Merge pull request #10 from Show-maket/fix-DMA-and-Analyzer
Fix dma and analyzer
2026-04-17 11:29:56 +00:00
01a34ed3f7 Merge pull request #9 from Show-maket/opti-dma-enc-power
add brightness controll
2026-04-17 06:47:16 +00:00
d788358ac8 add brightness controll 2026-04-17 09:45:32 +03:00
8daff9c46a fix timer overflow? 2026-04-15 11:00:48 +03:00
8631f23b53 fix overflow and mute 2026-04-14 09:36:27 +03:00
ad1e16cfda common fsm 2026-04-13 14:45:38 +03:00
57f79b35c7 fix longPacket sync_bit 2026-04-13 14:44:18 +03:00
f8daa68381 analyzer debug 2026-04-13 10:29:39 +03:00
ddb8a9e143 Filters and stable 2026-04-08 16:11:10 +03:00
c29fe2cf7c Analyzer 2026-04-07 15:08:02 +03:00
7651f07e0a Analyzer plug 2026-04-07 13:25:55 +03:00
fc93ea41db Merge pull request #8 from Show-maket/STM32
Stm32
2026-03-11 17:06:20 +03:00
0620d98e35 Merge branch 'main' into STM32 2026-03-11 17:06:06 +03:00
4caed06218 Merge pull request #7 from Show-maket/STM32DMA
upd
2026-03-11 16:59:08 +03:00
37522f974f upd 2024-12-25 16:56:18 +03:00
6375c4eed5 for debug test 2024-09-04 10:12:39 +03:00
c4000d6b75 test 2024-08-14 17:43:12 +03:00
fc02c79135 upd 2024-04-22 14:56:12 +03:00
d068a576f7 Merge pull request #3 from Show-maket/dev
v2.0
2024-03-18 16:24:38 +03:00
76 changed files with 82819 additions and 553 deletions

6
.gitignore vendored
View File

@ -5,3 +5,9 @@ log/*
/.vscode /.vscode
*.zip *.zip
**/__pycache__ **/__pycache__
Analyzer/raw/dll/*.dll
Analyzer/raw/dll/*.so
Analyzer/raw/dll/*.dylib
/Analyzer/raw/IR_Fox/.github
**/.build
graphify-out/*

View File

@ -1,5 +1,5 @@
{ {
"board": "STMicroelectronics:stm32:GenF4", "board": "STMicroelectronics:stm32:GenG4",
"port": "COM17", "port": "COM17",
"prebuild": "if exist bin rd /s /q bin" "prebuild": "if exist bin rd /s /q bin"
} }

View 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
View File

@ -0,0 +1,2 @@
/build
.DS_Store

View 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})

View 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"
}
]
}

View 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%

View 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()

View 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

View 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;
}

View 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

View 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;
}

View 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

View 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());
}

View 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

View 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: до текущего фронта (sample1), чтобы охватить все 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;
}

View 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;
};

View 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

View 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;
}

View 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

View 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

View File

@ -0,0 +1,3 @@
/build
/build-nmake
.DS_Store

View 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})

View 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"
}
]
}

View 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%

View 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()

View 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

View 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;
}

View 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

View File

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

View File

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

View File

@ -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());
}

View File

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

View File

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

View File

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

Binary file not shown.

BIN
Analyzer/raw/Session 2.sal Normal file

Binary file not shown.

BIN
Analyzer/raw/Session 3.sal Normal file

Binary file not shown.

Binary file not shown.

25175
Analyzer/raw/car.txt Normal file

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

View File

@ -207,7 +207,7 @@ HardwareTimer IR_Timer(TIM3);
void setup() void setup()
{ {
IR_Timer.setOverflow(carrierFrec * 2, HERTZ_FORMAT); IR_Timer.setOverflow((uint32_t)carrierFrec * (uint32_t)IR_Encoder::carrierMultiply(), HERTZ_FORMAT);
IR_Timer.attachInterrupt(1, EncoderISR); IR_Timer.attachInterrupt(1, EncoderISR);
NVIC_SetPriority(IRQn_Type::TIM3_IRQn, 0); NVIC_SetPriority(IRQn_Type::TIM3_IRQn, 0);
IR_Timer.resume(); IR_Timer.resume();

View File

@ -166,7 +166,7 @@ void IR_Decoder::_tick()
} }
if (isWaitingAcceptSend && millis() - acceptSendTimer > acceptDelay) if (isWaitingAcceptSend && millis() - acceptSendTimer > acceptDelay)
{ {
encoder->sendAccept(addrAcceptSendTo, acceptCustomByte); if (encoder != nullptr) encoder->sendAccept(addrAcceptSendTo, acceptCustomByte); // B2: у Car decoder без парного энкодера (nullptr) → был HardFault
isWaitingAcceptSend = false; isWaitingAcceptSend = false;
} }
} }

View File

@ -10,12 +10,12 @@ private:
static std::list<IR_Decoder*>& get_dec_list(); static std::list<IR_Decoder*>& get_dec_list();
void _tick(); void _tick();
uint32_t acceptSendTimer; uint32_t acceptSendTimer = 0;
bool isWaitingAcceptSend; bool isWaitingAcceptSend = false; // B4: без инициализации мусор мог дёрнуть sendAccept при старте
uint16_t addrAcceptSendTo; uint16_t addrAcceptSendTo = 0;
uint16_t acceptDelay = IR_ResponseDelay; uint16_t acceptDelay = IR_ResponseDelay;
uint8_t acceptCustomByte; uint8_t acceptCustomByte = 0;
bool extiEnabled_ = false; bool extiEnabled_ = false;
bool rxExtiPreemptConfigured_ = false; bool rxExtiPreemptConfigured_ = false;

View File

@ -3,6 +3,34 @@
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#if IR_GLITCH_REJECT_PHASE_NUDGE
/** Подтяжка опоры фазы после отброса шипа (как irfoxGlitchPhaseNudgeUs в плагине). */
static inline void irGlitchPhaseNudge(uint32_t edgeTime, uint16_t riseSync, volatile uint32_t &prevRise)
{
if (edgeTime > riseSync)
{
const uint32_t nudged = edgeTime - riseSync;
if (nudged > prevRise && nudged < edgeTime)
prevRise = nudged;
}
}
#endif
static inline uint8_t irPulseFilterHoldbackEdges()
{
uint8_t hb = (uint8_t)IR_INPUT_FILTER_HOLDBACK_EDGES;
if (hb < 2U)
hb = 2U;
if (hb > 4U)
hb = 4U;
return hb;
}
static inline uint16_t irClampU16(uint32_t v)
{
return (v > 0xFFFFU) ? 0xFFFFU : (uint16_t)v;
}
IR_DecoderRaw::IR_DecoderRaw(const uint8_t pin, uint16_t addr, IR_Encoder *encPair) : encoder(encPair) IR_DecoderRaw::IR_DecoderRaw(const uint8_t pin, uint16_t addr, IR_Encoder *encPair) : encoder(encPair)
{ {
setPin(pin); setPin(pin);
@ -44,23 +72,244 @@ bool IR_DecoderRaw::availableRaw()
} }
}; };
//////////////////////////////////// isr /////////////////////////////////////////// void IR_DecoderRaw::pulseFilterResetStats()
volatile uint32_t time_; {
pulseFilterDropHoldOverflow = 0;
pulseFilterDropGlitchPairs = 0;
}
bool IR_DecoderRaw::registerPairMuteEncoder(IR_Encoder *enc)
{
if (enc == nullptr)
return false;
for (uint8_t i = 0; i < pairMuteEncoderCount; ++i)
{
if (pairMuteEncoders[i] == enc)
return true;
}
if (pairMuteEncoderCount >= IR_PAIR_MUTE_MAX_ENCODERS)
return false;
pairMuteEncoders[pairMuteEncoderCount++] = enc;
return true;
}
void IR_DecoderRaw::refreshPairMuteState()
{
uint16_t active = 0;
for (uint8_t i = 0; i < pairMuteEncoderCount; ++i)
{
IR_Encoder *enc = pairMuteEncoders[i];
if (enc != nullptr && enc->isBusy())
++active;
}
const uint32_t nowUs = micros();
noInterrupts();
const bool wasActive = (isPairSending != 0);
isPairSending = active;
#if IR_RX_BRIEF_LOG
if (!wasActive && active != 0)
{
rxBriefMuteBlockedEdges = 0;
rxBriefMuteBeginPending = true;
rxBriefMuteBeginUs = nowUs;
}
else if (wasActive && active == 0)
{
rxBriefMuteEndPending = true;
rxBriefMuteEndUs = nowUs;
rxBriefMuteEndCount = rxBriefMuteBlockedEdges;
rxBriefMuteBlockedEdges = 0;
}
#endif
interrupts();
}
#if IR_RX_BRIEF_LOG
const __FlashStringHelper *IR_DecoderRaw::rxBriefReasonTag(RxBriefReason reason)
{
switch (reason)
{
case RxBriefReason::MuteBegin: return F("MUTE_BEGIN");
case RxBriefReason::MuteEnd: return F("MUTE_END");
case RxBriefReason::RawOverflow: return F("QRAW");
case RxBriefReason::FilterOverflow: return F("QFLT");
case RxBriefReason::HoldOverflow: return F("HOLD");
case RxBriefReason::Glitch: return F("GLITCH");
case RxBriefReason::Timing: return F("TIME");
case RxBriefReason::Preamble: return F("PREAMB");
case RxBriefReason::Sync: return F("SYNC");
case RxBriefReason::BufferOverflow: return F("BUF");
case RxBriefReason::Timeout: return F("TIMEOUT");
case RxBriefReason::Crc: return F("CRC");
case RxBriefReason::Ok: return F("OK");
default: return F("UNK");
}
}
#endif // IR_RX_BRIEF_LOG (rxBriefReasonTag)
// Безусловна: always-on счёт причин RX-событий (наблюдаемость по контракту
// живучести — счётчики живут и в проде без печати); печать — только под флагом.
void IR_DecoderRaw::rxBriefLog(RxBriefReason reason, uint16_t a, uint16_t b, uint32_t tUs)
{
const uint8_t ri = (uint8_t)reason;
if (ri < 14U)
rxReasonCnt[ri]++;
#if !IR_RX_BRIEF_LOG
(void)a; (void)b; (void)tUs;
return;
#else
#if IR_RX_BRIEF_LOG_REJECT_ONLY
if (reason == RxBriefReason::Ok || reason == RxBriefReason::Preamble)
return;
#endif
if (tUs == 0U)
tUs = micros();
Serial.print(F("IRRX t="));
Serial.print((unsigned long)tUs);
Serial.print(F(" rsn="));
Serial.print(rxBriefReasonTag(reason));
switch (reason)
{
case RxBriefReason::MuteBegin:
break;
case RxBriefReason::MuteEnd:
case RxBriefReason::RawOverflow:
Serial.print(F(" cnt="));
Serial.print(a);
break;
case RxBriefReason::FilterOverflow:
case RxBriefReason::HoldOverflow:
case RxBriefReason::Glitch:
Serial.print(F(" total="));
Serial.print(a);
break;
case RxBriefReason::Timing:
Serial.print(F(" rp="));
Serial.print(a);
if (b)
{
Serial.print(F(" hp="));
Serial.print(b);
}
break;
case RxBriefReason::Preamble:
Serial.print(F(" good="));
Serial.print(a);
if (b)
{
Serial.print(F(" per="));
Serial.print(b);
}
break;
case RxBriefReason::Sync:
Serial.print(F(" err="));
Serial.print(a);
break;
case RxBriefReason::BufferOverflow:
Serial.print(F(" bits="));
Serial.print(a);
break;
case RxBriefReason::Timeout:
Serial.print(F(" bits="));
Serial.print(a);
if (b)
{
Serial.print(F(" exp="));
Serial.print(b);
}
break;
case RxBriefReason::Crc:
case RxBriefReason::Ok:
Serial.print(F(" len="));
Serial.print(a);
if (b)
{
Serial.print(F(" err="));
Serial.print(b);
}
break;
}
Serial.println();
#endif // IR_RX_BRIEF_LOG (печать)
}
void IR_DecoderRaw::printRxReasonStats(Print &out) const
{
static const char *const kTags[14] = {"?", "MUTEB", "MUTEE", "QRAW", "QFLT", "HOLD",
"GLITCH", "TIME", "PREAMB", "SYNC", "BUF",
"TIMEOUT", "CRC", "OK"};
out.print(F("RXSTAT"));
for (uint8_t i = 1; i < 14U; i++)
{
out.print(',');
out.print(kTags[i]);
out.print('=');
out.print(rxReasonCnt[i]);
}
out.println();
}
#if IR_RX_BRIEF_LOG
void IR_DecoderRaw::rxBriefNoteMuteBlockedIsr(uint32_t tUs)
{
(void)tUs;
if (rxBriefMuteBlockedEdges != UINT16_MAX)
++rxBriefMuteBlockedEdges;
}
void IR_DecoderRaw::rxBriefNoteRawOverflowIsr(uint32_t tUs)
{
if (rxBriefRawOverflowDrops != UINT16_MAX)
++rxBriefRawOverflowDrops;
rxBriefRawOverflowLastUs = tUs;
}
void IR_DecoderRaw::rxBriefFlushDeferredIsrLogs()
{
bool muteBeginPending = false;
uint32_t muteBeginUs = 0;
bool muteEndPending = false;
uint32_t muteEndUs = 0;
uint16_t muteEndCnt = 0;
uint16_t rawCnt = 0;
uint32_t rawLastUs = 0;
noInterrupts();
muteBeginPending = rxBriefMuteBeginPending;
muteBeginUs = rxBriefMuteBeginUs;
rxBriefMuteBeginPending = false;
rxBriefMuteBeginUs = 0;
muteEndPending = rxBriefMuteEndPending;
muteEndUs = rxBriefMuteEndUs;
muteEndCnt = rxBriefMuteEndCount;
rxBriefMuteEndPending = false;
rxBriefMuteEndUs = 0;
rxBriefMuteEndCount = 0;
rawCnt = rxBriefRawOverflowDrops;
rawLastUs = rxBriefRawOverflowLastUs;
rxBriefRawOverflowDrops = 0;
rxBriefRawOverflowLastUs = 0;
interrupts();
if (muteBeginPending)
rxBriefLog(RxBriefReason::MuteBegin, 0, 0, muteBeginUs);
if (muteEndPending)
rxBriefLog(RxBriefReason::MuteEnd, muteEndCnt, 0, muteEndUs);
if (rawCnt)
rxBriefLog(RxBriefReason::RawOverflow, rawCnt, 0, rawLastUs);
}
#endif
//////////////////////////////////// isr ///////////////////////////////////////////
void IR_DecoderRaw::isr() void IR_DecoderRaw::isr()
{ {
noInterrupts(); // Интервалы между соседними фронтами считаются как (uint32_t)(t1 - t0) — корректно при
time_ = micros(); // паузе < ~35 мин между фронтами; условие «тишина > longSilence» в preambleProcessEdge
interrupts(); // переписано без front.time > prevRise (оно ломается при wrap).
if (time_ < oldTime) // B6: micros() в ISR не требует маскировки; прежний interrupts() посреди ISR снимал PRIMASK.
{ const uint32_t t = micros();
time_ += 1000;
}
oldTime = time_;
FrontStorage edge; FrontStorage edge;
edge.dir = port->IDR & mask; edge.dir = port->IDR & mask;
edge.time = time_; edge.time = t;
#if defined(IR_EDGE_TRACE) #if defined(IR_EDGE_TRACE)
edgeTracePush(edge.time, edge.dir ? 1u : 0u, edgeTracePush(edge.time, edge.dir ? 1u : 0u,
@ -69,10 +318,19 @@ void IR_DecoderRaw::isr()
if (isPairSending) if (isPairSending)
{ {
#if IR_RX_BRIEF_LOG
rxBriefNoteMuteBlockedIsr(edge.time);
#endif
return; return;
} }
subBuffer.push(edge); if (!subBuffer.push(edge))
{
isSubBufferOverflow = true;
#if IR_RX_BRIEF_LOG
rxBriefNoteRawOverflowIsr(edge.time);
#endif
}
} }
//////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////
@ -93,6 +351,7 @@ void IR_DecoderRaw::firstRX()
i_dataBuffer = 0; i_dataBuffer = 0;
nextControlBit = bitPerByte; nextControlBit = bitPerByte;
i_syncBit = 0; i_syncBit = 0;
err_syncBit = 0;
isWrongPack = false; isWrongPack = false;
isPreamb = true; isPreamb = true;
@ -101,11 +360,25 @@ void IR_DecoderRaw::firstRX()
wrCounter = 0; wrCounter = 0;
#endif #endif
memset(dataBuffer, 0x00, dataByteSizeMax); memset(dataBuffer, 0x00, dataByteSizeMax);
pulseFilterReset();
preambleResetToIdle();
}
bool IR_DecoderRaw::rxTimeoutPipelineBusy() const
{
if (pulseFilterHoldCount != 0U)
return true;
noInterrupts();
const bool busy = !subBuffer.isEmpty();
interrupts();
return busy;
} }
void IR_DecoderRaw::listenStart() void IR_DecoderRaw::listenStart()
{ {
if (isReciveRaw && ((micros() - prevRise) > IR_timeout * 2)) if (rxTimeoutPipelineBusy())
return;
if (isReciveRaw && ((micros() - lastEdgeTime) > IR_timeout * 2U))
{ {
#if defined(IRDEBUG_SERIAL_PACK) #if defined(IRDEBUG_SERIAL_PACK)
packTraceOnTimeoutOrAbort(true); packTraceOnTimeoutOrAbort(true);
@ -120,65 +393,160 @@ void IR_DecoderRaw::listenStart()
inline void IR_DecoderRaw::checkTimeout() inline void IR_DecoderRaw::checkTimeout()
{ {
if (!isRecive) return; // уже не принимаем нечего проверять if (!isRecive) return; // уже не принимаем нечего проверять
if (rxTimeoutPipelineBusy())
return;
if (micros() - lastEdgeTime > IR_timeout * 2U) if (micros() - lastEdgeTime > IR_timeout * 2U)
{ {
#if defined(IRDEBUG_SERIAL_PACK) #if defined(IRDEBUG_SERIAL_PACK)
packTraceOnTimeoutOrAbort(false); packTraceOnTimeoutOrAbort(false);
#endif #endif
const uint16_t expected = (i_dataBuffer >= 8U) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0U;
rxBriefLog(RxBriefReason::Timeout, i_dataBuffer, expected, micros());
isRecive = false; // приём завершён isRecive = false; // приём завершён
msgTypeReceive = 0; msgTypeReceive = 0;
// firstRX(); // подготовка к новому пакету // Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся
lastEdgeTime = micros(); // защита от повторного срабатывания // с «сырым» флагом приёма / Locked и не может заново поймать преамбулу до очень
// длинной тишины (см. preambleProcessEdge Idle и listenStart).
isReciveRaw = false;
firstRX();
// Не подставлять lastEdgeTime = micros(): при хвосте в subBuffer следующий фронт
// имеет edge.time < micros() — откат lastEdgeTime даёт ложный TIMEOUT на следующем tick().
// Повторного checkTimeout при тишине нет: isRecive уже false.
} }
} }
// ==================================================================== // ====================================================================
void IR_DecoderRaw::tick() void IR_DecoderRaw::tick()
{ {
// FrontStorage *currentFrontPtr; #if IR_RX_BRIEF_LOG
// noInterrupts(); rxBriefFlushDeferredIsrLogs();
// currentFrontPtr = subBuffer.pop(); #endif
// interrupts(); // listenStart/checkTimeout: в конце tick (END) и при отсутствии фронта (ниже).
// Не в начале до pop: иначе после checkTimeout lastEdgeTime vs micros() расходятся
// с метками ISR из очереди → ложные TIMEOUT (bits=0) каждый пакет.
FrontStorage currentFront; FrontStorage rawFront;
noInterrupts(); bool processedFront = false;
listenStart(); // Батч-выемка (IR_RX_TICK_BATCH): раньше 1 фронт/tick — приём был привязан
FrontStorage *currentFrontPtr; // к частоте loop, буфер 250 фронтов (~120мс эфира) переполнялся на медленном
currentFrontPtr = subBuffer.pop(); // loop и кадры гибли молча. Семантика idle-пути сохранена: flush
if (currentFrontPtr == nullptr) // пульс-фильтра — только когда буфер пуст (как прежняя else-ветка).
uint8_t batchBudget = IR_RX_TICK_BATCH;
bool hasRawFront = subBuffer.pop(rawFront); // B5: копия под одной критсекцией (без торн-рида)
while (hasRawFront)
{
if (IR_INPUT_MIN_PULSE_US > 0U)
{
pulseFilterPushRaw(rawFront);
FrontStorage confirmedFront;
while (pulseFilterTryTakeConfirmed(confirmedFront))
{
processDecodedFront(confirmedFront);
processedFront = true;
}
}
else
{
processDecodedFront(rawFront);
processedFront = true;
}
if (--batchBudget == 0U)
{
break;
}
hasRawFront = subBuffer.pop(rawFront);
}
if (!hasRawFront && IR_INPUT_MIN_PULSE_US > 0U)
{
const uint32_t nowUs = micros();
FrontStorage confirmedFront;
while (pulseFilterTryFlushOne(nowUs, confirmedFront))
{
processDecodedFront(confirmedFront);
processedFront = true;
}
}
if (!processedFront)
{ {
isSubBufferOverflow = false; isSubBufferOverflow = false;
checkTimeout(); // <--- новое место проверки listenStart();
interrupts(); checkTimeout();
#if defined(IR_EDGE_TRACE) #if defined(IR_EDGE_TRACE)
while (edgeTraceFlushChunk(Serial, 48) > 0) {} while (edgeTraceFlushChunk(Serial, 48) > 0) {}
#endif #endif
return; return;
} // Если данных нет - ничего не делаем } // Если данных нет - ничего не делаем
currentFront = *currentFrontPtr; listenStart();
interrupts(); checkTimeout();
#if IR_RX_BRIEF_LOG
rxBriefFlushDeferredIsrLogs();
#endif
#if defined(IR_EDGE_TRACE)
while (edgeTraceFlushChunk(Serial, 48) > 0) {}
#endif
}
// ---------- буфер пуст: фронтов нет, проверяем тайм-аут ---------- void IR_DecoderRaw::processDecodedFront(const FrontStorage &currentFront)
// if (currentFrontPtr == nullptr) {
// { if (preambleProcessEdge(currentFront))
// isSubBufferOverflow = false; {
// return; lastEdgeTime = currentFront.time;
// } return;
}
// // ---------- есть фронт: продолжаем обработку ----------
// FrontStorage currentFront = *currentFrontPtr;
lastEdgeTime = currentFront.time; // запоминаем любой фронт lastEdgeTime = currentFront.time; // запоминаем любой фронт
//////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (currentFront.dir) if (currentFront.dir)
{ // Если __/``` ↑ { // Если __/``` ↑
if (currentFront.time - prevRise > riseTimeMax / 4 || highCount || lowCount) const uint32_t candRp = currentFront.time - prevRise;
const uint32_t candHt = currentFront.time - prevFall;
const uint32_t candLt = prevFall - prevRise;
#if IR_SHORT_LOW_GLITCH_REJECT
const bool short_low_glitch =
isRecive && !isPreamb && candHt < (riseTimeMin / 8U) && candLt >= riseTimeMin &&
candRp >= riseTimeMin && candRp <= IR_timeout;
if (short_low_glitch)
{
errors.other++;
rxBriefLog(RxBriefReason::Glitch, 1, 0, currentFront.time);
#if IR_GLITCH_REJECT_PHASE_NUDGE
irGlitchPhaseNudge(currentFront.time, riseSyncTime, prevRise);
#endif
return;
}
#endif
#if IR_MICRO_GAP_RISE_REJECT
const bool micro_gap_cand_lt_ok =
(candLt >= riseTimeMin) || (candLt >= (riseTimeMin / 4U) && candLt < riseTimeMin);
const bool micro_gap_rise = isRecive && !isPreamb && candHt < (riseTimeMin / 8U) && micro_gap_cand_lt_ok &&
candRp >= (riseTimeMin / 4U) && candRp < riseTimeMin && candRp <= IR_timeout;
if (micro_gap_rise)
{
errors.other++;
rxBriefLog(RxBriefReason::Glitch, 1, 0, currentFront.time);
#if IR_GLITCH_REJECT_PHASE_NUDGE
irGlitchPhaseNudge(currentFront.time, riseSyncTime, prevRise);
#endif
return;
}
#endif
if (candRp <= riseTimeMax / 4U && !highCount && !lowCount)
{
errors.other++;
rxBriefLog(RxBriefReason::Timing, irClampU16(candRp), 0, currentFront.time);
return;
}
if (candRp > riseTimeMax / 4 || highCount || lowCount)
{ // комплексный фикс рваной единицы { // комплексный фикс рваной единицы
risePeriod = currentFront.time - prevRise; risePeriod = candRp;
highTime = currentFront.time - prevFall; highTime = candHt;
lowTime = prevFall - prevRise; lowTime = candLt;
prevRise = currentFront.time; prevRise = currentFront.time;
if ( if (
@ -224,90 +592,20 @@ void IR_DecoderRaw::tick()
} }
} }
#ifdef IRDEBUG #ifdef IRDEBUG
// goto END; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // return; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#endif #endif
//---------------------------------------------------------------------------------- //----------------------------------------------------------------------------------
#ifdef IRDEBUG #ifdef IRDEBUG
digitalWrite(errOut, currentFront.dir); digitalWrite(errOut, currentFront.dir);
#endif #endif
if (currentFront.time > prevRise && currentFront.time - prevRise > IR_timeout * 2 && !isReciveRaw)
{ // первый
#ifdef IRDEBUG
errPulse(up, 50);
errPulse(down, 50);
errPulse(up, 150);
errPulse(down, 150);
#endif
preambFrontCounter = preambFronts - 1U;
isPreamb = true;
isRecive = true;
isReciveRaw = true;
isWrongPack = false;
#if defined(IRDEBUG_SERIAL_PACK)
packTraceResetFrame();
packTraceOpen = true;
#endif
}
//-------------------------------------------------------------------------------------------------------
if (preambFrontCounter)
{ // в преамбуле
#ifdef IRDEBUG
// Serial.print("risePeriod: ");
// Serial.println(risePeriod);
#endif
if (currentFront.dir && risePeriod < IR_timeout)
{ // __/``` ↑ и мы в внутри пакета
if (risePeriod < riseTimeMin / 2)
{ // fix рваной единицы
preambFrontCounter += 2;
errors.other++;
#ifdef IRDEBUG
errPulse(down, 350);
#endif
}
else
{
if (freeFrec)
{
riseSyncTime = (riseSyncTime + risePeriod / 2) / 2;
} // tuner
}
}
else
{ /* riseSyncTime = bitTime; */
} // сброс тюнера
preambFrontCounter--;
// Serial.print("preambFrontCounter: "); Serial.println(preambFrontCounter);
}
else
{
if (isPreamb)
{ // первый фронт после
// gotTune.set(riseSyncTime);
isPreamb = false;
#ifdef IRDEBUG
errPulse(up, 50);
errPulse(down, 50);
#endif
prevRise += risePeriod / 2;
// prevRise = currentFront.time + riseTime;
goto END;
}
}
if (isPreamb)
{
goto END;
}
if (risePeriod > IR_timeout || isBufferOverflow || risePeriod < riseTimeMin || isWrongPack) if (risePeriod > IR_timeout || isBufferOverflow || risePeriod < riseTimeMin || isWrongPack)
// ~Мы в пределах таймаута и буффер не переполнен и fix дроблёных единиц // ~Мы в пределах таймаута и буффер не переполнен и fix дроблёных единиц
{ {
goto END; if (!isBufferOverflow && !isWrongPack)
rxBriefLog(RxBriefReason::Timing, irClampU16((uint32_t)risePeriod),
irClampU16((uint32_t)highTime), currentFront.time);
return;
} }
// определить направление фронта // определить направление фронта
@ -462,12 +760,6 @@ void IR_DecoderRaw::tick()
else else
{ // Если ```\__ ↓ { // Если ```\__ ↓
} }
////////////////////////////////////////////////////////////////////////////////////////////////////////////
END:;
#if defined(IR_EDGE_TRACE)
while (edgeTraceFlushChunk(Serial, 48) > 0) {}
#endif
} }
void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
@ -475,9 +767,10 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
#if !defined(IRDEBUG_SERIAL_PACK) #if !defined(IRDEBUG_SERIAL_PACK)
(void)packTraceInvertFix; (void)packTraceInvertFix;
#endif #endif
if (i_dataBuffer > dataByteSizeMax * 8) if (i_dataBuffer >= dataByteSizeMax * 8)
{ // проверка переполнения { // проверка переполнения (>=: иначе при i_dataBuffer==dataByteSizeMax*8 запись dataBuffer[38] за границей массива — B3)
isBufferOverflow = true; isBufferOverflow = true;
rxBriefLog(RxBriefReason::BufferOverflow, i_dataBuffer, 0, micros());
#if defined(IRDEBUG_SERIAL_PACK) #if defined(IRDEBUG_SERIAL_PACK)
if (packTraceOpen) if (packTraceOpen)
packTraceEmitErrorFlash(F("ERROR: buffer overflow")); packTraceEmitErrorFlash(F("ERROR: buffer overflow"));
@ -485,9 +778,12 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
} }
if (isBufferOverflow || isPreamb || isWrongPack) if (isBufferOverflow || isPreamb || isWrongPack)
{ {
// Как checkTimeout/listenStart: firstRX() сбрасывает буфер битов, преамбулу и
// pulseFilterReset() — при IR_INPUT_MIN_PULSE_US > 0 иначе остаётся «хвост» в hold/filtered.
isRecive = false; isRecive = false;
isReciveRaw = false; isReciveRaw = false;
msgTypeReceive = 0; msgTypeReceive = 0;
firstRX();
return; return;
} }
@ -551,6 +847,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
#endif #endif
{ {
isWrongPack = true; isWrongPack = true;
rxBriefLog(RxBriefReason::Sync, err_syncBit, 0, micros());
#if defined(IRDEBUG_SERIAL_PACK) #if defined(IRDEBUG_SERIAL_PACK)
packTraceEmitErrorFlash(F("ERROR: Wrong sync bit")); packTraceEmitErrorFlash(F("ERROR: Wrong sync bit"));
#endif #endif
@ -578,6 +875,11 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
if (i_dataBuffer == 8 * msgBytes) if (i_dataBuffer == 8 * msgBytes)
{ // Ппервый байт { // Ппервый байт
packSize = dataBuffer[0] & IR_MASK_MSG_INFO; packSize = dataBuffer[0] & IR_MASK_MSG_INFO;
// B1: под-минимальная длина (1..2) физически не несёт CRC (min кадр = msg+crc = 3 байта) → шум/битьё.
// Без отсева packSize==1 даёт crcCheck(1-2) → len=255 → OOB-чтение dataBuffer[0..256] (массив 38).
// packSize>=3 (в т.ч. будущие компактные кадры) обрабатываются как обычно.
if (packSize != 0 && packSize < msgBytes + crcBytes)
isWrongPack = true;
} }
// Тип приёма (для isReceive): выставляем сразу после первого байта, ДО проверки «Конец». // Тип приёма (для isReceive): выставляем сразу после первого байта, ДО проверки «Конец».
@ -588,8 +890,8 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
msgTypeReceive = (dataBuffer[0] >> 5) | 0b11111000; msgTypeReceive = (dataBuffer[0] >> 5) | 0b11111000;
} }
if (packSize && (i_dataBuffer == packSize * bitPerByte)) if (packSize > crcBytes && (i_dataBuffer == packSize * bitPerByte))
{ // Конец { // Конец (B1: packSize>crcBytes → crcCheck(packSize-crcBytes) не уходит в underflow/OOB)
packInfo.buffer = dataBuffer; packInfo.buffer = dataBuffer;
packInfo.crc = crcValue; packInfo.crc = crcValue;
packInfo.err = errors; packInfo.err = errors;
@ -598,6 +900,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
isRecive = false; isRecive = false;
isReciveRaw = false; isReciveRaw = false;
preambleResetToIdle();
msgTypeReceive = 0; msgTypeReceive = 0;
isAvailable = crcCheck(packSize - crcBytes, crcValue); isAvailable = crcCheck(packSize - crcBytes, crcValue);
@ -644,6 +947,12 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
else else
packTraceEmitEndBadCrc(static_cast<uint8_t>(packSize)); packTraceEmitEndBadCrc(static_cast<uint8_t>(packSize));
#endif #endif
const uint16_t errSum =
uint16_t(errors.lowSignal) + uint16_t(errors.highSignal) + uint16_t(errors.other);
if (isAvailable)
rxBriefLog(RxBriefReason::Ok, packSize, errSum, micros());
else
rxBriefLog(RxBriefReason::Crc, packSize, errSum, micros());
if (!isAvailable && packSize > 0 && packSize <= dataByteSizeMax) { if (!isAvailable && packSize > 0 && packSize <= dataByteSizeMax) {
memcpy(rejectBuffer, dataBuffer, packSize); memcpy(rejectBuffer, dataBuffer, packSize);
rejectPackSize = static_cast<uint8_t>(packSize); rejectPackSize = static_cast<uint8_t>(packSize);
@ -663,7 +972,7 @@ bool IR_DecoderRaw::crcCheck(uint8_t len, crc_t &crc)
crc = (crc8(dataBuffer, 0, len, poly1) << 8) & ~((crc_t)0xFF); crc = (crc8(dataBuffer, 0, len, poly1) << 8) & ~((crc_t)0xFF);
crc |= crc8(dataBuffer, 0, len + 1, poly2) & (crc_t)0xFF; crc |= crc8(dataBuffer, 0, len + 1, poly2) & (crc_t)0xFF;
if (dataBuffer[len] == (crc >> 8) & 0xFF && if (dataBuffer[len] == ((crc >> 8) & 0xFF) && // B7: скобки — иначе (== & 0xFF) по приоритету
dataBuffer[len + 1] == (crc & 0xFF)) dataBuffer[len + 1] == (crc & 0xFF))
{ {
crcOK = true; crcOK = true;
@ -686,6 +995,7 @@ bool IR_DecoderRaw::availableReject()
uint16_t IR_DecoderRaw::ceil_div(uint16_t val, uint16_t divider) uint16_t IR_DecoderRaw::ceil_div(uint16_t val, uint16_t divider)
{ {
if (divider == 0) return 0; // B10: защита от деления на 0 (актуально при freeFrec — сейчас выкл, не включаем)
int ret = val / divider; int ret = val / divider;
if ((val << 4) / divider - (ret << 4) >= 8) if ((val << 4) / divider - (ret << 4) >= 8)
ret++; ret++;
@ -1147,6 +1457,276 @@ __attribute__((weak)) void irPackTracePrintOkCommand(const uint8_t *buf, uint8_t
#endif // IRDEBUG_SERIAL_PACK #endif // IRDEBUG_SERIAL_PACK
uint32_t IR_DecoderRaw::absDiffU32(uint32_t a, uint32_t b)
{
return (a > b) ? (a - b) : (b - a);
}
void IR_DecoderRaw::pulseFilterShiftLeft(uint8_t n)
{
if (n == 0 || pulseFilterHoldCount == 0)
return;
if (n >= pulseFilterHoldCount)
{
pulseFilterHoldCount = 0;
return;
}
const uint8_t newCount = static_cast<uint8_t>(pulseFilterHoldCount - n);
for (uint8_t i = 0; i < newCount; ++i)
pulseFilterHoldEdges[i] = pulseFilterHoldEdges[static_cast<uint8_t>(i + n)];
pulseFilterHoldCount = newCount;
}
void IR_DecoderRaw::pulseFilterReset()
{
pulseFilterHoldCount = 0;
pulseFilterLastRawValid = false;
pulseFilterLastRawTime = 0;
}
void IR_DecoderRaw::pulseFilterPushRaw(const FrontStorage &e)
{
const uint32_t minUs = IR_INPUT_MIN_PULSE_US;
if (minUs == 0U)
{
return;
}
pulseFilterLastRawTime = e.time;
pulseFilterLastRawValid = true;
if (pulseFilterHoldCount >= kPulseFilterHoldCap)
{
pulseFilterDropHoldOverflow++;
rxBriefLog(RxBriefReason::HoldOverflow, irClampU16(pulseFilterDropHoldOverflow), 0, e.time);
pulseFilterShiftLeft(1);
}
pulseFilterHoldEdges[pulseFilterHoldCount++] = e;
}
bool IR_DecoderRaw::pulseFilterTryTakeConfirmed(FrontStorage &out, uint32_t logTime)
{
const uint32_t minUs = IR_INPUT_MIN_PULSE_US;
if (minUs == 0U)
return false;
const uint8_t holdback = irPulseFilterHoldbackEdges();
if (logTime == 0U)
logTime = pulseFilterLastRawTime;
for (;;)
{
if (pulseFilterHoldCount < 2)
return false;
const uint32_t dt = pulseFilterHoldEdges[1].time - pulseFilterHoldEdges[0].time;
if (dt < minUs)
{
pulseFilterDropGlitchPairs++;
rxBriefLog(RxBriefReason::Glitch, irClampU16(pulseFilterDropGlitchPairs), 0, logTime);
pulseFilterShiftLeft(2);
continue;
}
if (pulseFilterHoldCount <= holdback)
return false;
out = pulseFilterHoldEdges[0];
pulseFilterShiftLeft(1);
return true;
}
}
bool IR_DecoderRaw::pulseFilterTryFlushOne(uint32_t nowUs, FrontStorage &out)
{
if (IR_INPUT_MIN_PULSE_US == 0U || !pulseFilterLastRawValid || pulseFilterHoldCount == 0)
return false;
const uint32_t waitUs = IR_INPUT_MIN_PULSE_US * (uint32_t)IR_INPUT_FILTER_TIMEOUT_MULT;
if ((uint32_t)(nowUs - pulseFilterLastRawTime) < waitUs)
return false;
while (pulseFilterHoldCount > 0)
{
if (pulseFilterHoldCount >= 2)
{
const uint32_t dt = pulseFilterHoldEdges[1].time - pulseFilterHoldEdges[0].time;
if (dt < IR_INPUT_MIN_PULSE_US)
{
pulseFilterDropGlitchPairs++;
rxBriefLog(RxBriefReason::Glitch, irClampU16(pulseFilterDropGlitchPairs), 0, nowUs);
pulseFilterShiftLeft(2);
continue;
}
}
out = pulseFilterHoldEdges[0];
pulseFilterShiftLeft(1);
return true;
}
return false;
}
uint32_t IR_DecoderRaw::preambleJitterTolUs(uint32_t baselineUs) const
{
const uint32_t pct = (baselineUs * (uint32_t)IR_PREAMBLE_JITTER_PCT) / 100U;
return (pct > (uint32_t)IR_PREAMBLE_JITTER_US_MIN) ? pct : (uint32_t)IR_PREAMBLE_JITTER_US_MIN;
}
bool IR_DecoderRaw::preambleRisePeriodCoarseOk(uint32_t periodUs) const
{
const uint32_t base = (uint32_t)bitTime;
const uint32_t minP = (base * (uint32_t)IR_PREAMBLE_PERIOD_MIN_FACTOR_PCT) / 100U;
const uint32_t maxP = (base * (uint32_t)IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT) / 100U;
return periodUs >= minP && periodUs <= maxP;
}
void IR_DecoderRaw::preambleResetToIdle()
{
preambleState = PreambleState::Idle;
preambleGoodPeriods = 0;
preambleMeanPeriod = 0;
preambleCandidateLastEdgeTime = 0;
preambleCandidateFirstRiseTime = 0;
preambleCandidateFirstRiseValid = false;
preambFrontCounter = 0;
isPreamb = false;
isWrongPack = false;
isBufferOverflow = false;
}
void IR_DecoderRaw::preambleStartCandidate(const FrontStorage &front)
{
preambleState = PreambleState::Candidate;
preambleGoodPeriods = 0;
preambleMeanPeriod = 0;
preambleCandidateLastEdgeTime = front.time;
preambleCandidateFirstRiseTime = front.time;
preambleCandidateFirstRiseValid = front.dir;
preambFrontCounter = 0;
isPreamb = true;
isRecive = false;
isReciveRaw = false;
}
bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
{
const uint32_t longSilence = IR_timeout * 2U;
const uint32_t candTimeout = IR_timeout * (uint32_t)IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT;
if (preambleState == PreambleState::Idle)
{
if (isReciveRaw)
return false;
// Не использовать front.time > prevRise: после переполнения micros (~2^32 µs) новое t
// меньше prevRise в unsigned-смысле — преамбула никогда не стартует («залипание» RX).
// prevRise == 0: как раньше — «длинная тишина» только по абсолютному front.time > longSilence.
if (!isReciveRaw && front.dir &&
((prevRise == 0U && front.time > longSilence) ||
(prevRise != 0U && (uint32_t)(front.time - prevRise) > longSilence)))
preambleStartCandidate(front);
}
if (preambleState == PreambleState::Candidate)
{
if ((uint32_t)(front.time - preambleCandidateLastEdgeTime) > candTimeout)
{
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, 0, front.time);
preambleStartCandidate(front);
}
preambleCandidateLastEdgeTime = front.time;
if (!front.dir)
return true;
if (!preambleCandidateFirstRiseValid)
{
preambleCandidateFirstRiseValid = true;
preambleCandidateFirstRiseTime = front.time;
return true;
}
const uint32_t period = front.time - preambleCandidateFirstRiseTime;
preambleCandidateFirstRiseTime = front.time;
if (!preambleRisePeriodCoarseOk(period))
{
preambleGoodPeriods = 0;
preambleMeanPeriod = 0;
rxBriefLog(RxBriefReason::Preamble, 0, irClampU16(period), front.time);
return true;
}
if (preambleGoodPeriods == 0)
{
preambleGoodPeriods = 1;
preambleMeanPeriod = (uint16_t)period;
}
else
{
const uint32_t tol = preambleJitterTolUs(preambleMeanPeriod);
if (absDiffU32(period, preambleMeanPeriod) <= tol)
{
if (preambleGoodPeriods < 255U)
++preambleGoodPeriods;
preambleMeanPeriod =
(uint16_t)(((uint32_t)preambleMeanPeriod * 3U + period) / 4U);
}
else
{
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, irClampU16(period), front.time);
preambleGoodPeriods = 1;
preambleMeanPeriod = (uint16_t)period;
}
}
if (freeFrec)
riseSyncTime = (riseSyncTime + period / 2U) / 2U;
if (preambleGoodPeriods >= kPreambleLockNeed)
{
// Новый кадр обязан стартовать с чистого state, иначе возможны CRC/sync срывы
// при lock без промежуточного listenStart()->firstRX().
errors.reset();
packSize = 0;
isBufferOverflow = false;
isAvailable = false;
bufBitPos = 0;
isData = true;
i_dataBuffer = 0;
nextControlBit = bitPerByte;
i_syncBit = 0;
err_syncBit = 0;
isWrongPack = false;
msgTypeReceive = 0;
memset(dataBuffer, 0x00, dataByteSizeMax);
preambleState = PreambleState::Locked;
isPreamb = false;
isRecive = true;
isReciveRaw = true;
risePeriod = preambleMeanPeriod;
#if defined(IRDEBUG_SERIAL_PACK)
packTraceResetFrame();
packTraceOpen = true;
#endif
#ifdef IRDEBUG
errPulse(up, 50);
errPulse(down, 50);
#endif
prevRise = front.time + preambleMeanPeriod / 2U;
return true;
}
return true;
}
if (preambleState == PreambleState::Locked)
{
if (!isReciveRaw)
preambleResetToIdle();
return false;
}
return !isReciveRaw;
}
// IRDEBUG FUNC // IRDEBUG FUNC
#ifdef IRDEBUG #ifdef IRDEBUG
inline void IR_DecoderRaw::errPulse(uint8_t pin, uint8_t count) inline void IR_DecoderRaw::errPulse(uint8_t pin, uint8_t count)

View File

@ -25,7 +25,7 @@ class Print;
#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; constexpr uint16_t IR_ResponseDelay = irproto::kMandatoryInterPacketQuietMs;
class IR_Encoder; class IR_Encoder;
class IR_DecoderRaw : virtual public IR_FOX class IR_DecoderRaw : virtual public IR_FOX
@ -52,6 +52,10 @@ public:
inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился
bool isSubOverflow(); bool isSubOverflow();
volatile inline bool isReciving() { return isRecive; }; // Возвращает true, если происходит приём пакета volatile inline bool isReciving() { return isRecive; }; // Возвращает true, если происходит приём пакета
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) #if defined(IR_EDGE_TRACE)
void edgeTraceClear(); void edgeTraceClear();
@ -61,6 +65,14 @@ public:
uint16_t edgeTraceFlushChunk(Print &out, uint16_t maxRec = 48); uint16_t edgeTraceFlushChunk(Print &out, uint16_t maxRec = 48);
#endif #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 не сошёлся — один раз можно прочитать копию сырых байтов. /// Кадр собран по длине из заголовка, но CRC не сошёлся — один раз можно прочитать копию сырых байтов.
bool availableReject(); bool availableReject();
uint8_t getRejectSize() const { return rejectPackSize; } uint8_t getRejectSize() const { return rejectPackSize; }
@ -68,15 +80,32 @@ public:
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
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; bool isRejectAvailable = false;
uint8_t rejectPackSize = 0; uint8_t rejectPackSize = 0;
uint8_t rejectBuffer[dataByteSizeMax]{}; 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;
@ -101,6 +130,28 @@ 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;
uint8_t preambleGoodPeriods = 0;
uint16_t preambleMeanPeriod = 0;
uint32_t preambleCandidateLastEdgeTime = 0;
uint32_t preambleCandidateFirstRiseTime = 0;
bool preambleCandidateFirstRiseValid = false;
#if defined(IR_EDGE_TRACE) #if defined(IR_EDGE_TRACE)
struct IrEdgeTraceRec struct IrEdgeTraceRec
@ -116,6 +167,17 @@ private:
volatile bool edgeTrace_overflow = false; volatile bool edgeTrace_overflow = false;
#endif #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}; // Буффер данных
volatile uint32_t prevRise, prevPrevRise, prevFall, prevPrevFall; // Время предыдущих фронтов/спадов volatile uint32_t prevRise, prevPrevRise, prevFall, prevPrevFall; // Время предыдущих фронтов/спадов
@ -124,7 +186,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;
@ -136,9 +197,25 @@ private:
int16_t bufBitPos = 0; // Позиция для записи бита в буффер int16_t bufBitPos = 0; // Позиция для записи бита в буффер
private: private:
bool isReciveRaw; bool isReciveRaw = false;
void listenStart(); void listenStart();
void checkTimeout(); // 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 &currentFront);
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 Длина в байтах проверяемых данных
@ -148,10 +225,10 @@ bool isReciveRaw;
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
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 packTraceInvertFix если true — в IRDEBUG_SERIAL_PACK бит в трассе пишется как `0`/`1` (исправление по фронтам) /// @param packTraceInvertFix если true — в IRDEBUG_SERIAL_PACK бит в трассе пишется как `0`/`1` (исправление по фронтам)
@ -166,6 +243,18 @@ bool isReciveRaw;
/// @return Результат /// @return Результат
uint16_t ceil_div(uint16_t val, uint16_t divider); uint16_t ceil_div(uint16_t val, uint16_t divider);
// rxBriefLog безусловен: ВСЕГДА инкрементирует rxReasonCnt[reason]
// (always-on наблюдаемость по контракту живучести), печать события —
// только при IR_RX_BRIEF_LOG. Вызовы в местах отказов тоже безусловны.
void rxBriefLog(RxBriefReason reason, uint16_t a = 0, uint16_t b = 0, uint32_t tUs = 0);
uint16_t rxReasonCnt[14] = {}; // индекс = (uint8_t)RxBriefReason, 1..13
#if IR_RX_BRIEF_LOG
static const __FlashStringHelper *rxBriefReasonTag(RxBriefReason reason);
void rxBriefNoteMuteBlockedIsr(uint32_t tUs);
void rxBriefNoteRawOverflowIsr(uint32_t tUs);
void rxBriefFlushDeferredIsrLogs();
#endif
#ifdef IRDEBUG #ifdef IRDEBUG
uint32_t wrCounter; uint32_t wrCounter;
inline void errPulse(uint8_t pin, uint8_t count); inline void errPulse(uint8_t pin, uint8_t count);

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,41 @@
#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 { struct IR_SendResult {
bool success; // Флаг успешности отправки bool success; // Флаг успешности отправки
uint32_t sendTimeMs; // Время отправки пакета в миллисекундах uint32_t sendTimeMs; // Время отправки пакета в миллисекундах
IR_SendStatus status; // Детализированный статус старта передачи
IR_SendResult(bool success = false, uint32_t sendTimeMs = 0)
: success(success), sendTimeMs(sendTimeMs) {} 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 IrTxIsrBufferedStorageBase;
class IR_Encoder : public IR_FOX class IR_Encoder : public IR_FOX
{ {
friend IR_DecoderRaw; friend IR_DecoderRaw;
@ -22,43 +45,93 @@ class IR_Encoder : public IR_FOX
public: public:
static HardwareTimer* IR_Timer; static HardwareTimer* IR_Timer;
struct IR_TxGateRun { using IR_TxGateRun = IrTxGateRun;
uint16_t lenTicks; // number of timer ticks at carrierFrec*2 enum class TxIsrMode : uint8_t {
bool gate; // true: carrier enabled (output toggles), false: silent (output forced low) Legacy = 0,
Buffered = 1
}; };
using ExternalTxBusyFn = bool (*)(void *ctx); using ExternalTxBusyFn = bool (*)(void *ctx);
using ExternalTxStartFn = bool (*)(void *ctx, IR_Encoder *enc, const uint8_t *packet, uint8_t len); using ExternalTxStartFn = IR_SendStatus (*)(void *ctx, IR_Encoder *enc, const uint8_t *packet, uint8_t len);
private: private:
// uint16_t id; /// @brief Адрес передатчика // uint16_t id; /// @brief Адрес передатчика
public: public:
/// @brief Класс передатчика /// @brief Класс передатчика
/// @param addr Адрес передатчика /// @param addr Адрес передатчика
/// @param pin Вывод передатчика /// @param pin Вывод передатчика
/// @param decPair Приёмник, для которого отключается приём в момент передачи передатчиком /// @param decPair Если задан, конструктор регистрирует этот один приёмник как blind-decoder
/// (аналог setBlindDecoders() для одного RX).
IR_Encoder(uint8_t pin, uint16_t addr = 0, IR_DecoderRaw *decPair = nullptr, bool autoHandle = true); IR_Encoder(uint8_t pin, uint16_t addr = 0, IR_DecoderRaw *decPair = nullptr, bool autoHandle = true);
static void isr(); static void isr();
static void begin(HardwareTimer* timer, uint8_t channel, IRQn_Type IRQn, uint8_t priority, void(*isrCallback)() = nullptr); static void begin(HardwareTimer* timer, uint8_t channel, IRQn_Type IRQn, uint8_t priority, void(*isrCallback)() = nullptr);
/** Configure timer frequency for TX clock (carrierFrec*2) without attaching ISR. */ /**
* Глобальный знаменатель: частота таймера 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();
/** Максимальный числитель мощности: ⌊multiply/2⌋ (100% в setPowerPercent). */
static uint16_t maxPowerNumerator();
/** Числитель 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);
/** Legacy helper: lenTicks в тактах 2×Fc → физические тики (carrierFrec×multiply). Может разбить сегменты. */
static bool scaleGateRunsToPhysical(IR_TxGateRun* runs, size_t* ioCount, size_t maxRuns, uint16_t multiply);
/** Configure timer frequency for TX clock (carrierFrec × multiply) without attaching ISR. */
static void beginClockOnly(HardwareTimer *timer); static void beginClockOnly(HardwareTimer *timer);
static HardwareTimer* get_IR_Timer(); static HardwareTimer* get_IR_Timer();
/** Call from main loop/tick: if ISR requested carrier stop, pause timer here (not in ISR). */ /** Call from main loop/tick: if ISR requested carrier stop, pause timer here (not in ISR). */
static void tick(); static void tick();
/**
* Режим внутреннего 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). */ /** Optional: register external TX backend (e.g. DMA driver). */
static void setExternalTxBackend(ExternalTxStartFn startFn, ExternalTxBusyFn busyFn, void *ctx); static void setExternalTxBackend(ExternalTxStartFn startFn, ExternalTxBusyFn busyFn, void *ctx);
/** Called by external TX backend on actual end of transmission. */ /** Called by external TX backend on actual end of transmission. */
void externalFinishSend(); void externalFinishSend();
/** Build RLE runs of carrier gate for a packet (no HW access). */ /** 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); 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 enable();
void disable(); 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);
IR_SendResult sendData(uint16_t addrTo, uint8_t dataByte, bool needAccept = false); IR_SendResult sendData(uint16_t addrTo, uint8_t dataByte, bool needAccept = false);
IR_SendResult 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);
@ -91,6 +164,8 @@ public:
void _isr(); void _isr();
private: private:
static volatile bool carrierStopPending; static volatile bool carrierStopPending;
static bool txIsrLegacyMode_;
static uint16_t s_carrierMultiply;
static void carrierResume(); static void carrierResume();
static void carrierPauseIfIdle(); static void carrierPauseIfIdle();
@ -99,7 +174,8 @@ private:
static void *externalTxCtx; static void *externalTxCtx;
IR_SendResult _sendBack(bool isAdressed, uint16_t addrTo, uint8_t *data, uint8_t len); IR_SendResult _sendBack(bool isAdressed, uint16_t addrTo, uint8_t *data, uint8_t len);
void setDecoder_isSending(); 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 calculateSendTime(uint8_t packSize) const;
@ -116,25 +192,63 @@ 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);
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
{ {
@ -144,5 +258,5 @@ private:
static uint8_t bitHigh[2]; static uint8_t bitHigh[2];
static uint8_t bitLow[2]; static uint8_t bitLow[2];
uint8_t *currentBitSequence = bitLow; uint8_t *currentBitSequence = bitLow;
volatile SignalPart signal; volatile SignalPart signal = noSignal;
}; };

View File

@ -6,6 +6,18 @@
/** Число потоков DMA-TX задаётся шаблоном: IrDmaTxStm32<2>, см. IrDmaTxStm32.h и irproto::kDefaultDmaTxMaxStreams. */ /** Число потоков DMA-TX задаётся шаблоном: IrDmaTxStm32<2>, см. IrDmaTxStm32.h и irproto::kDefaultDmaTxMaxStreams. */
namespace irproto { namespace irproto {
constexpr size_t kDefaultDmaTxMaxStreams = 4U; 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 на пинах не меняют. // Пошаговый разбор кадра на Serial (по умолчанию выключено). Пульсы IRDEBUG на пинах не меняют.
@ -13,6 +25,16 @@ constexpr size_t kDefaultDmaTxMaxStreams = 4U;
// Не обрывать приём сразу при накопленной sync-ошибке — «дописывать» до таймаута (только вместе с IRDEBUG_SERIAL_PACK). // Не обрывать приём сразу при накопленной sync-ошибке — «дописывать» до таймаута (только вместе с IRDEBUG_SERIAL_PACK).
// #define IRDEBUG_SERIAL_SOFT_REJECT // #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 // Журнал фронтов ИК в ISR; сброс строк @IRF1v1: в IR_DecoderRaw::tick(). См. ref/IR_EDGE_TRACE_FORMAT.md
// Расход RAM ≈ IR_EDGE_TRACE_CAPACITY * 6 байт на декодер. Выключить — закомментировать: // Расход RAM ≈ IR_EDGE_TRACE_CAPACITY * 6 байт на декодер. Выключить — закомментировать:
// #define IR_EDGE_TRACE // #define IR_EDGE_TRACE
@ -154,9 +176,67 @@ typedef uint16_t crc_t;
#define subBufferSize 250 // Буфер для складирования фронтов, пока их не обработают (передатчик) #define subBufferSize 250 // Буфер для складирования фронтов, пока их не обработают (передатчик)
#endif #endif
#define preambPulse 3 /** Максимальное число передатчиков, способных временно заглушить один декодер. */
#ifndef IR_PAIR_MUTE_MAX_ENCODERS
#define IR_PAIR_MUTE_MAX_ENCODERS 8U
#endif
#define disablePairDec false // Отключать парный приёмник, возможны баги, используйте setBlindDecoders() /** Минимальная длительность удержания уровня (мкс): короче — импульс/пара фронтов выкидывается до 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. */
#ifndef IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT
#define IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT 3U
#endif
#define preambPulse 3
///////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////
@ -184,6 +264,164 @@ typedef uint16_t crc_t;
#define bitTime (bitTakts * carrierPeriod) // Общая длительность бита #define bitTime (bitTakts * carrierPeriod) // Общая длительность бита
#define tolerance 300U #define tolerance 300U
namespace irproto
{
/** Maximum complete frame length representable by the five header bits. */
constexpr uint8_t kMaxWireFrameBytes = static_cast<uint8_t>(IR_MASK_MSG_INFO);
constexpr uint8_t kDataFrameOverheadBytes = msgBytes + addrBytes + addrBytes + crcBytes;
constexpr uint8_t kBackFrameOverheadBytes = msgBytes + addrBytes + crcBytes;
constexpr uint8_t kBackToFrameOverheadBytes = msgBytes + addrBytes + addrBytes + crcBytes;
constexpr uint8_t kAcceptFrameBytes = msgBytes + addrBytes + 1U + crcBytes;
constexpr uint8_t kRequestFrameBytes = msgBytes + addrBytes + addrBytes + crcBytes;
constexpr uint8_t kMaxDataPayloadBytes = kMaxWireFrameBytes - kDataFrameOverheadBytes;
constexpr uint8_t kMaxBackPayloadBytes = kMaxWireFrameBytes - kBackFrameOverheadBytes;
constexpr uint8_t kMaxBackToPayloadBytes = kMaxWireFrameBytes - kBackToFrameOverheadBytes;
/** Complete DATA frame size, or zero when payloadBytes cannot fit on wire. */
constexpr uint8_t dataWireBytes(uint8_t payloadBytes)
{
return payloadBytes <= kMaxDataPayloadBytes
? static_cast<uint8_t>(kDataFrameOverheadBytes + payloadBytes)
: 0U;
}
/** Complete non-addressed BACK frame size, or zero when it cannot fit. */
constexpr uint8_t backWireBytes(uint8_t payloadBytes)
{
return payloadBytes <= kMaxBackPayloadBytes
? static_cast<uint8_t>(kBackFrameOverheadBytes + payloadBytes)
: 0U;
}
/** Complete addressed BACK_TO frame size, or zero when it cannot fit. */
constexpr uint8_t backToWireBytes(uint8_t payloadBytes)
{
return payloadBytes <= kMaxBackToPayloadBytes
? static_cast<uint8_t>(kBackToFrameOverheadBytes + payloadBytes)
: 0U;
}
/** Minimum complete frame size for a known message type; zero means reserved/unknown. */
constexpr uint8_t minimumWireBytes(uint8_t msgType)
{
return (msgType == IR_MSG_DATA_ACCEPT || msgType == IR_MSG_DATA_NOACCEPT)
? kDataFrameOverheadBytes
: msgType == IR_MSG_BACK
? kBackFrameOverheadBytes
: (msgType == IR_MSG_BACK_TO || msgType == IR_MSG_REQUEST)
? kRequestFrameBytes
: msgType == IR_MSG_ACCEPT
? kAcceptFrameBytes
: 0U;
}
constexpr bool isTypedWireSizeValid(uint8_t msgType, uint8_t wireBytes)
{
return minimumWireBytes(msgType) != 0U &&
wireBytes >= minimumWireBytes(msgType) &&
wireBytes <= kMaxWireFrameBytes;
}
/*
* TX FSM timing contract.
*
* The FSM runs on 2*carrierFrec. The preamble contains preambPulse*2
* constant runs; each run is preambToggle+1 ticks. Every data bit and every
* per-byte sync bit occupies bitTakts*2 ticks, independently of its value.
*/
constexpr uint32_t kTxLogicalClockHz = static_cast<uint32_t>(carrierFrec) * 2U;
constexpr uint32_t kPreambleLogicalTicks =
static_cast<uint32_t>(preambPulse * 2U) * static_cast<uint32_t>(preambToggle + 1U);
constexpr uint32_t kEncodedBitLogicalTicks = static_cast<uint32_t>(bitTakts * 2U);
constexpr uint32_t kWireByteLogicalTicks =
static_cast<uint32_t>(bitPerByte + syncBits) * kEncodedBitLogicalTicks;
constexpr uint32_t wireLogicalTicks(uint8_t wireBytes)
{
return wireBytes != 0U && wireBytes <= kMaxWireFrameBytes
? kPreambleLogicalTicks + static_cast<uint32_t>(wireBytes) * kWireByteLogicalTicks
: 0U;
}
constexpr uint32_t logicalTicksToUsCeil(uint32_t logicalTicks)
{
return logicalTicks == 0U
? 0U
: static_cast<uint32_t>(
(static_cast<uint64_t>(logicalTicks) * 1000000ULL +
static_cast<uint64_t>(kTxLogicalClockHz) - 1ULL) /
static_cast<uint64_t>(kTxLogicalClockHz));
}
constexpr uint32_t preambleAirtimeUsCeil()
{
return logicalTicksToUsCeil(kPreambleLogicalTicks);
}
/** Complete nominal on-air duration, rounded up to a whole microsecond. */
constexpr uint32_t wireAirtimeUsCeil(uint8_t wireBytes)
{
return logicalTicksToUsCeil(wireLogicalTicks(wireBytes));
}
constexpr uint32_t wireAirtimeMsCeil(uint8_t wireBytes)
{
return wireAirtimeUsCeil(wireBytes) == 0U
? 0U
: (wireAirtimeUsCeil(wireBytes) + 999U) / 1000U;
}
/* Preserve the deployed library turn-around policy, but expose it by name. */
constexpr uint16_t kMandatoryInterPacketQuietMs =
static_cast<uint16_t>(
static_cast<uint16_t>(
(static_cast<uint32_t>(bitTime + tolerance) *
static_cast<uint32_t>(bitPerByte + syncBits + 1U)) *
2.7735) /
1000U);
constexpr uint32_t kMandatoryInterPacketQuietUs =
static_cast<uint32_t>(kMandatoryInterPacketQuietMs) * 1000U;
constexpr uint16_t kDefaultTimingGuardPermille = 1150U;
constexpr uint32_t addTimingGuardUs(uint32_t durationUs,
uint16_t marginPermille = kDefaultTimingGuardPermille)
{
return marginPermille == 0U
? 0U
: static_cast<uint32_t>(
(static_cast<uint64_t>(durationUs) * marginPermille + 999ULL) / 1000ULL);
}
/** Deadline for seeing enough preamble to know that a response has started. */
constexpr uint32_t responseStartGuardUs(
uint16_t marginPermille = kDefaultTimingGuardPermille)
{
return addTimingGuardUs(kMandatoryInterPacketQuietUs + preambleAirtimeUsCeil(),
marginPermille);
}
/** Conservative deadline for receiving a complete response of maxWireBytes. */
constexpr uint32_t responseFrameGuardUs(
uint8_t maxWireBytes,
uint16_t marginPermille = kDefaultTimingGuardPermille)
{
return wireAirtimeUsCeil(maxWireBytes) == 0U
? 0U
: addTimingGuardUs(kMandatoryInterPacketQuietUs +
wireAirtimeUsCeil(maxWireBytes),
marginPermille);
}
static_assert(kMaxDataPayloadBytes == 24U, "DATA payload contract changed");
static_assert(kMaxBackPayloadBytes == 26U, "BACK payload contract changed");
static_assert(kPreambleLogicalTicks == 588U, "preamble timing contract changed");
static_assert(kWireByteLogicalTicks == 814U, "wire-byte timing contract changed");
static_assert(kMandatoryInterPacketQuietMs == 42U, "inter-packet quiet policy changed");
}
constexpr uint16_t test_all_Time = bitTime; constexpr uint16_t test_all_Time = bitTime;
constexpr uint16_t test_all_Takts = bitTakts * 2; constexpr uint16_t test_all_Takts = bitTakts * 2;
constexpr uint16_t test_hi = ((bitPauseTakts) * 2 - 0) + ((bitActiveTakts) * 2 - 0); constexpr uint16_t test_hi = ((bitPauseTakts) * 2 - 0) + ((bitActiveTakts) * 2 - 0);

View File

@ -1,9 +1,18 @@
#pragma once #pragma once
#include "IR_Encoder.h" #include "IR_Encoder.h"
#include "IrTxBsrrWave.h"
#if defined(ARDUINO_ARCH_STM32) && defined(STM32G4xx) #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 <Arduino.h>
#include <HardwareTimer.h> #include <HardwareTimer.h>
@ -16,7 +25,7 @@
#include "stm32g4xx_hal.h" #include "stm32g4xx_hal.h"
/** /**
* STM32G4: ИК TX через DMA в GPIO BSRR, такт от TIM UPDATE (carrierFrec×2). * STM32G4: ИК TX через DMA в GPIO BSRR, такт от TIM UPDATE (carrierFrec × IR_Encoder::carrierMultiply()).
* *
* Число слотов потоков — параметр шаблона (без макросов), например IrDmaTxStm32<2>. * Число слотов потоков — параметр шаблона (без макросов), например IrDmaTxStm32<2>.
* По умолчанию: IrDmaTxStm32<> ≡ irproto::kDefaultDmaTxMaxStreams (см. IR_config.h). * По умолчанию: IrDmaTxStm32<> ≡ irproto::kDefaultDmaTxMaxStreams (см. IR_config.h).
@ -37,7 +46,7 @@ public:
uint32_t* dmaWords = nullptr; uint32_t* dmaWords = nullptr;
uint16_t dmaWordCount = 0; uint16_t dmaWordCount = 0;
IR_Encoder::IR_TxGateRun* gateRuns = nullptr; IrTxGateRun* gateRuns = nullptr;
size_t maxGateRuns = 0; size_t maxGateRuns = 0;
}; };
@ -76,9 +85,12 @@ public:
} }
s_instance = this; s_instance = this;
activeCount_ = 0;
for (uint8_t i = 0; i < streamCount_; i++) { 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); HAL_NVIC_EnableIRQ(streams_[i].dmaIrq);
} }
@ -89,22 +101,29 @@ public:
return s_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 { bool busy() const {
if (streamCount_ == 0) return false;
for (uint8_t i = 0; i < streamCount_; i++) { for (uint8_t i = 0; i < streamCount_; i++) {
if (!streams_[i].active) return false; if (streams_[i].active) return true;
} }
return true; return false;
} }
bool start(IR_Encoder* enc, const uint8_t* packet, uint8_t len) { IR_SendStatus start(IR_Encoder* enc, const uint8_t* packet, uint8_t len) {
if (enc == nullptr) return false; if (enc == nullptr) return IR_SendStatus::ExternalNoStream;
for (uint8_t i = 0; i < streamCount_; i++) { for (uint8_t i = 0; i < streamCount_; i++) {
if (streams_[i].enc == enc) { if (streams_[i].enc == enc) {
return startStream(streams_[i], packet, len); return startStream(streams_[i], packet, len);
} }
} }
return false; return IR_SendStatus::ExternalNoStream;
} }
void irqForStream(size_t streamIndex) { void irqForStream(size_t streamIndex) {
@ -117,6 +136,37 @@ public:
return &streams_[streamIndex].hdma; 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: private:
struct TxStream { struct TxStream {
DMA_HandleTypeDef hdma{}; DMA_HandleTypeDef hdma{};
@ -134,69 +184,42 @@ private:
uint16_t bufLen = 0; uint16_t bufLen = 0;
uint16_t halfLen = 0; uint16_t halfLen = 0;
IR_Encoder::IR_TxGateRun* runs = nullptr; IrTxGateRun* runs = nullptr;
size_t maxRuns = 0; size_t maxRuns = 0;
size_t runCount = 0; size_t runCount = 0;
size_t runIndex = 0; IrTxBsrrWave wave{};
uint16_t ticksLeftInRun = 0;
bool carrierPhase = false;
uint32_t totalTicks = 0; uint32_t totalTicks = 0;
volatile uint32_t ticksOutput = 0; volatile uint32_t ticksOutput = 0;
bool active = false; // Fix D (watchdog): прогресс ticksOutput против стенных часов (контекст потока).
uint32_t lastTicks = 0;
uint32_t lastProgressMs = 0;
volatile bool active = false; // Fix: делится поток<->ISR (busy()/poll()/колбэки/спин applyCarrierMultiply) — запрет кэширования компилятором
void resetWave() { void resetWave() {
runIndex = 0; wave.configure(setWord, resetWord, nullptr, 0, 2, 1);
carrierPhase = false;
ticksOutput = 0; ticksOutput = 0;
totalTicks = 0; totalTicks = 0;
runCount = 0; runCount = 0;
ticksLeftInRun = 0;
}
IR_DMA_TX_HOT uint32_t nextWord() {
if (runIndex >= runCount) {
return resetWord;
}
const bool gate = runs[runIndex].gate;
if (!gate) {
carrierPhase = false;
} else {
carrierPhase = !carrierPhase;
}
const uint32_t out = (gate && carrierPhase) ? setWord : resetWord;
if (ticksLeftInRun > 0) {
ticksLeftInRun--;
}
if (ticksLeftInRun == 0) {
runIndex++;
if (runIndex < runCount) {
ticksLeftInRun = runs[runIndex].lenTicks;
}
}
return out;
} }
IR_DMA_TX_HOT void fill(uint32_t* dst, uint16_t count) { IR_DMA_TX_HOT void fill(uint32_t* dst, uint16_t count) {
if (dst == nullptr || count == 0) { wave.fill(dst, count);
return;
}
do {
*dst++ = nextWord();
} while (--count != 0);
} }
void onHalf() { void onHalf() {
ticksOutput += halfLen; ticksOutput += halfLen;
fill(&dmaBuf[0], halfLen); fill(&dmaBuf[0], halfLen);
__DSB(); // Fix #8: refill первой половины виден DMA до следующего прохода кольца
} }
void onComplete() { void onComplete() {
ticksOutput += halfLen; ticksOutput += halfLen;
fill(&dmaBuf[halfLen], halfLen); fill(&dmaBuf[halfLen], halfLen);
__DSB(); // Fix #8: refill второй половины виден DMA до следующего прохода кольца
} }
void onError() {} void onError() {}
@ -210,29 +233,39 @@ private:
TxStream streams_[MaxStreams]{}; TxStream streams_[MaxStreams]{};
uint8_t streamCount_ = 0; uint8_t streamCount_ = 0;
volatile uint8_t activeCount_ = 0; // Fix C: разделяемый activeCount_ удалён — он и был источником гонки поток↔ISR.
uint32_t stallTimeoutMs_ = 100; // Fix D
uint32_t recoveries_ = 0; // Fix D (только контекст потока)
volatile uint32_t errors_ = 0; // Fix #5: счётчик Transfer-Error (пишется в ISR dmaErrorCb, читается из потока)
static uint32_t u32ptr(const volatile void* p) { static uint32_t u32ptr(const volatile void* p) {
return (uint32_t)(uintptr_t)p; return (uint32_t)(uintptr_t)p;
} }
void startTimerIfNeeded() { // Fix C: TIM держим free-running. Включаем ИДЕМПОТЕНТНО при старте потока и НИКОГДА не
// останавливаем по разделяемому счётчику (счётчик убран → гонка поток↔ISR устранена).
// Счётчик TIM НЕ сбрасываем: сброс глитчил бы уже активный поток; для ИК ≤1 период несущей
// фазового сдвига пренебрежим. В простое (ни один DMA-канал не заармлен) update-события TIM
// генерят DMA-запросы «в никуда» — безвредно, в GPIO ничего не пишется, светодиод не дёргается.
void ensureTimerRunning() {
if (htim_ == nullptr) return; if (htim_ == nullptr) return;
if (activeCount_ != 1) return;
__HAL_TIM_DISABLE_DMA(htim_, TIM_DMA_UPDATE);
__HAL_TIM_CLEAR_FLAG(htim_, TIM_FLAG_UPDATE);
__HAL_TIM_SET_COUNTER(htim_, 0);
__HAL_TIM_ENABLE_DMA(htim_, TIM_DMA_UPDATE); __HAL_TIM_ENABLE_DMA(htim_, TIM_DMA_UPDATE);
HAL_TIM_Base_Start(htim_); if ((htim_->Instance->CR1 & TIM_CR1_CEN) == 0U) {
__HAL_TIM_CLEAR_FLAG(htim_, TIM_FLAG_UPDATE);
HAL_TIM_Base_Start(htim_);
}
} }
void stopTimerIfIdle() { // Fix D: аварийный стоп залипшего потока из контекста потока (не ISR). Маскируем ИМЕННО IRQ
if (htim_ == nullptr) return; // этого DMA-канала, чтобы штатный HT/CPLT/Error-обработчик не вытеснил нас посреди stopStream
if (activeCount_ != 0) return; // (иначе двойной abort/externalFinishSend). Другие потоки и прочие IRQ не трогаем.
void forceStop(TxStream& s) {
__HAL_TIM_DISABLE_DMA(htim_, TIM_DMA_UPDATE); HAL_NVIC_DisableIRQ(s.dmaIrq);
HAL_TIM_Base_Stop(htim_); if (s.active) {
stopStream(s);
recoveries_++;
}
HAL_NVIC_EnableIRQ(s.dmaIrq);
} }
static TxStream* streamFromDma(DMA_HandleTypeDef* hdma) { static TxStream* streamFromDma(DMA_HandleTypeDef* hdma) {
@ -265,9 +298,10 @@ private:
static void dmaErrorCb(DMA_HandleTypeDef* hdma) { static void dmaErrorCb(DMA_HandleTypeDef* hdma) {
auto* s = streamFromDma(hdma); auto* s = streamFromDma(hdma);
if (s == nullptr) return; if (s == nullptr || !s->active) return; // симметрично HT/CPLT: одиночный stop, без двойного finish
s->onError(); s->onError();
if (s_instance != nullptr) { if (s_instance != nullptr) {
s_instance->errors_++; // Fix #5: наблюдаемость аварийных завершений по Transfer-Error
s_instance->stopStream(*s); s_instance->stopStream(*s);
} }
} }
@ -314,38 +348,51 @@ private:
return true; return true;
} }
bool startStream(TxStream& s, const uint8_t* packet, uint8_t len) { IR_SendStatus startStream(TxStream& s, const uint8_t* packet, uint8_t len) {
if (s.enc == nullptr || s.port == nullptr || s.mask == 0) return false; if (s.enc == nullptr || s.port == nullptr || s.mask == 0) return IR_SendStatus::ExternalInvalidConfig;
if (s.active) return false; if (s.active) return IR_SendStatus::EncoderBusy;
if (s.dmaBuf == nullptr || s.bufLen < 2 || s.halfLen == 0) return false; if (s.dmaBuf == nullptr || s.bufLen < 2 || s.halfLen == 0) return IR_SendStatus::ExternalInvalidConfig;
if (s.runs == nullptr || s.maxRuns == 0) return false; if (s.runs == nullptr || s.maxRuns == 0) return IR_SendStatus::ExternalInvalidConfig;
s.resetWave(); s.resetWave();
s.runCount = IR_Encoder::buildGateRuns(packet, len, s.runs, s.maxRuns); const uint16_t mult = IR_Encoder::carrierMultiply();
if (s.runCount == 0) return false; s.runCount = IR_Encoder::buildPhysicalGateRuns(packet, len, s.runs, s.maxRuns, mult);
if (s.runCount == 0) return IR_SendStatus::BuildGateRunsFailed;
uint32_t total = 0; uint32_t total = 0;
for (size_t i = 0; i < s.runCount; i++) total += s.runs[i].lenTicks; for (size_t i = 0; i < s.runCount; i++) total += s.runs[i].lenTicks;
s.totalTicks = total; s.totalTicks = total;
s.runIndex = 0; uint16_t pwr = mult / 2U;
s.ticksLeftInRun = s.runs[0].lenTicks; if (s.enc != nullptr) {
s.carrierPhase = false; 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.fill(&s.dmaBuf[0], s.bufLen);
s.port->BSRR = s.resetWord; 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); 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) { if (HAL_DMA_Start_IT(&s.hdma, (uint32_t)(uintptr_t)s.dmaBuf, dst, s.bufLen) != HAL_OK) {
return false; s.active = false;
return IR_SendStatus::DmaStartFailed;
} }
s.active = true; ensureTimerRunning();
activeCount_++; return IR_SendStatus::Success;
startTimerIfNeeded();
return true;
} }
void stopStream(TxStream& s) { void stopStream(TxStream& s) {
@ -361,9 +408,7 @@ private:
if (s.enc != nullptr) { if (s.enc != nullptr) {
s.enc->externalFinishSend(); s.enc->externalFinishSend();
} }
// Fix C: TIM НЕ останавливаем — он free-running, без разделяемого счётчика.
if (activeCount_ > 0) activeCount_--;
stopTimerIfIdle();
} }
}; };

87
IrTxBsrrWave.h Normal file
View File

@ -0,0 +1,87 @@
#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;
}
IR_TX_BSRR_WAVE_HOT void fill(uint32_t* dst, uint16_t count) {
if (dst == nullptr || count == 0) {
return;
}
do {
*dst++ = nextWord();
} while (--count != 0);
}
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
View 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
View 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]{};
};

View File

@ -2,12 +2,30 @@
namespace PacketTypes namespace PacketTypes
{ {
bool BasePack::checkPacketLayout() const
{
if (packInfo == nullptr || packInfo->buffer == nullptr ||
packInfo->packSize < msgBytes + crcBytes ||
packInfo->packSize > irproto::kMaxWireFrameBytes)
{
return false;
}
return (packInfo->buffer[msgOffset] & IR_MASK_MSG_INFO) == packInfo->packSize;
}
bool BasePack::checkAddress() { return true; }; bool BasePack::checkAddress() { return true; };
void BasePack::set(IR_FOX::PackInfo *packInfo, uint16_t id) void BasePack::set(IR_FOX::PackInfo *packInfo, uint16_t id)
{ {
isAvailable = false;
isRawAvailable = false;
this->packInfo = packInfo; this->packInfo = packInfo;
this->id = id; this->id = id;
if (!checkPacketLayout())
{
return;
}
if (checkAddress()) if (checkAddress())
{ {
isAvailable = true; isAvailable = true;
@ -27,24 +45,58 @@ namespace PacketTypes
uint16_t BasePack::_getAddrFrom(BasePack *obj) uint16_t BasePack::_getAddrFrom(BasePack *obj)
{ {
if (obj == nullptr || !obj->checkPacketLayout() ||
obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
obj->packInfo->packSize < crcBytes ||
static_cast<uint16_t>(obj->addressFromOffset) + 1U >=
static_cast<uint16_t>(obj->packInfo->packSize - crcBytes))
{
return 0U;
}
return (obj->packInfo->buffer[obj->addressFromOffset] << 8) | obj->packInfo->buffer[obj->addressFromOffset + 1]; return (obj->packInfo->buffer[obj->addressFromOffset] << 8) | obj->packInfo->buffer[obj->addressFromOffset + 1];
}; };
uint16_t BasePack::_getAddrTo(BasePack *obj) uint16_t BasePack::_getAddrTo(BasePack *obj)
{ {
if (obj == nullptr || !obj->checkPacketLayout() ||
obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
obj->packInfo->packSize < crcBytes ||
static_cast<uint16_t>(obj->addressToOffset) + 1U >=
static_cast<uint16_t>(obj->packInfo->packSize - crcBytes))
{
return 0U;
}
return (obj->packInfo->buffer[obj->addressToOffset] << 8) | obj->packInfo->buffer[obj->addressToOffset + 1]; return (obj->packInfo->buffer[obj->addressToOffset] << 8) | obj->packInfo->buffer[obj->addressToOffset + 1];
}; };
uint8_t BasePack::_getDataSize(BasePack *obj) uint8_t BasePack::_getDataSize(BasePack *obj)
{ {
return obj->packInfo->packSize - crcBytes - obj->DataOffset; if (obj == nullptr || !obj->checkPacketLayout() ||
obj->packInfo == nullptr || obj->packInfo->buffer == nullptr)
{
return 0U;
}
const uint16_t overhead = static_cast<uint16_t>(obj->DataOffset) + crcBytes;
return static_cast<uint16_t>(obj->packInfo->packSize) > overhead
? static_cast<uint8_t>(static_cast<uint16_t>(obj->packInfo->packSize) - overhead)
: 0U;
}; };
uint8_t *BasePack::_getDataPrt(BasePack *obj) uint8_t *BasePack::_getDataPrt(BasePack *obj)
{ {
if (obj == nullptr || !obj->checkPacketLayout() ||
obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
obj->packInfo->packSize < crcBytes ||
static_cast<uint16_t>(obj->DataOffset) >
static_cast<uint16_t>(obj->packInfo->packSize - crcBytes))
{
return nullptr;
}
return obj->packInfo->buffer + obj->DataOffset; return obj->packInfo->buffer + obj->DataOffset;
}; };
uint8_t BasePack::_getDataRawSize(BasePack *obj) uint8_t BasePack::_getDataRawSize(BasePack *obj)
{ {
return obj->packInfo->packSize; return obj != nullptr && obj->checkPacketLayout() && obj->packInfo != nullptr
? obj->packInfo->packSize
: 0U;
}; };
bool BasePack::available() bool BasePack::available()
@ -73,6 +125,17 @@ namespace PacketTypes
} }
}; };
bool Data::checkPacketLayout() const
{
if (!BasePack::checkPacketLayout())
{
return false;
}
const uint8_t msgType = (packInfo->buffer[msgOffset] >> 5) & IR_MASK_MSG_TYPE;
return (msgType == IR_MSG_DATA_ACCEPT || msgType == IR_MSG_DATA_NOACCEPT) &&
irproto::isTypedWireSizeValid(msgType, packInfo->packSize);
}
bool Data::checkAddress() bool Data::checkAddress()
{ {
bool ret; bool ret;
@ -80,6 +143,17 @@ namespace PacketTypes
return ret; return ret;
} }
bool DataBack::checkPacketLayout() const
{
if (!BasePack::checkPacketLayout())
{
return false;
}
const uint8_t msgType = (packInfo->buffer[msgOffset] >> 5) & IR_MASK_MSG_TYPE;
return (msgType == IR_MSG_BACK || msgType == IR_MSG_BACK_TO) &&
irproto::isTypedWireSizeValid(msgType, packInfo->packSize);
}
bool DataBack::checkAddress() bool DataBack::checkAddress()
{ {
bool ret; bool ret;
@ -96,12 +170,34 @@ namespace PacketTypes
return ret; return ret;
} }
bool Accept::checkPacketLayout() const
{
if (!BasePack::checkPacketLayout())
{
return false;
}
const uint8_t msgType = (packInfo->buffer[msgOffset] >> 5) & IR_MASK_MSG_TYPE;
return msgType == IR_MSG_ACCEPT &&
irproto::isTypedWireSizeValid(msgType, packInfo->packSize);
}
bool Accept::checkAddress() { return true; } bool Accept::checkAddress() { return true; }
bool Request::checkPacketLayout() const
{
if (!BasePack::checkPacketLayout())
{
return false;
}
const uint8_t msgType = (packInfo->buffer[msgOffset] >> 5) & IR_MASK_MSG_TYPE;
return msgType == IR_MSG_REQUEST &&
irproto::isTypedWireSizeValid(msgType, packInfo->packSize);
}
bool Request::checkAddress() bool Request::checkAddress()
{ {
bool ret; bool ret;
IR_FOX::checkAddressRuleApply(getAddrTo(), this->id, ret); IR_FOX::checkAddressRuleApply(getAddrTo(), this->id, ret);
return ret; return ret;
} }
} }

View File

@ -9,18 +9,19 @@ namespace PacketTypes
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 checkPacketLayout() const;
virtual bool checkAddress(); virtual bool checkAddress();
void set(IR_FOX::PackInfo *packInfo, uint16_t id); void set(IR_FOX::PackInfo *packInfo, uint16_t id);
@ -34,9 +35,9 @@ namespace PacketTypes
bool available(); bool available();
bool availableRaw(); bool availableRaw();
inline uint8_t getMsgInfo() { return packInfo->buffer[0] & IR_MASK_MSG_INFO; }; inline uint8_t getMsgInfo() { return packInfo != nullptr && packInfo->buffer != nullptr ? packInfo->buffer[0] & IR_MASK_MSG_INFO : 0U; };
inline uint8_t getMsgType() { return (packInfo->buffer[0] >> 5) & IR_MASK_MSG_TYPE; }; inline uint8_t getMsgType() { return packInfo != nullptr && packInfo->buffer != nullptr ? (packInfo->buffer[0] >> 5) & IR_MASK_MSG_TYPE : 0U; };
inline uint8_t getMsgRAW() { return packInfo->buffer[0]; }; inline uint8_t getMsgRAW() { return packInfo != nullptr && packInfo->buffer != nullptr ? packInfo->buffer[0] : 0U; };
inline uint16_t getErrorCount() { return packInfo->err.all(); }; inline uint16_t getErrorCount() { return packInfo->err.all(); };
inline uint8_t getErrorLowSignal() { return packInfo->err.lowSignal; }; inline uint8_t getErrorLowSignal() { return packInfo->err.lowSignal; };
inline uint8_t getErrorHighSignal() { return packInfo->err.highSignal; }; inline uint8_t getErrorHighSignal() { return packInfo->err.highSignal; };
@ -65,6 +66,7 @@ namespace PacketTypes
inline uint8_t *getDataPrt() { return _getDataPrt(this); }; inline uint8_t *getDataPrt() { return _getDataPrt(this); };
private: private:
bool checkPacketLayout() const override;
bool checkAddress() override; bool checkAddress() override;
}; };
@ -86,6 +88,7 @@ namespace PacketTypes
inline uint8_t *getDataPrt() { return _getDataPrt(this); }; inline uint8_t *getDataPrt() { return _getDataPrt(this); };
private: private:
bool checkPacketLayout() const override;
bool checkAddress() override; bool checkAddress() override;
}; };
@ -103,6 +106,7 @@ namespace PacketTypes
inline uint8_t getCustomByte() { return packInfo->buffer[DataOffset]; }; inline uint8_t getCustomByte() { return packInfo->buffer[DataOffset]; };
private: private:
bool checkPacketLayout() const override;
bool checkAddress() override; bool checkAddress() override;
}; };
@ -121,6 +125,7 @@ namespace PacketTypes
inline uint16_t getAddrTo() { return _getAddrTo(this); }; inline uint16_t getAddrTo() { return _getAddrTo(this); };
private: private:
bool checkPacketLayout() const override;
bool checkAddress() override; bool checkAddress() override;
}; };

View File

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

View 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`): IdleCandidateLocked, нужно 2 согласованных периода riserise в окне 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.
**Весь декод (CG) — в `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: 10248КБ).
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`.

View 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.*`.

View 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}% необнаружено")

View 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.290.41 % | ~0.78 % | 0.550.78 % |
| **double** (2×CRC8, 16 бит) — текущая | **0.0070.015 %** | **0.0250.03 %** | **0.0080.026 %** |
| nibble-fold (4+4 бит) | 0.760.80 % | ~1.58 % | 1.181.55 % |
| nibble-trunc (4+4 бит) | 1.371.57 % | ~1.53 % | 1.221.57 % |
Вывод: **double примерно в 2040× надёжнее single** и ловит практически все сдвиги, которые single пропускает. Это подтверждает, зачем добавляли второй полином (0x8C = зеркало 0x31 ловит «зеркальные» сдвиговые ошибки, слепые для 0x31).
## 3. Идея «склеить 2 байта в 1» (полбайта на полином) — ХУЖЕ, чем есть
- **nibble-fold** (свернуть каждый CRC8 в 4 бита xor'ом) и **nibble-trunc** (взять по 4 бита) дают **0.61.6 % пропусков — это ХУЖЕ даже одиночного полного CRC8** и в ~50150× хуже текущей двойной схемы.
- Burst-ошибки (подряд искажённые биты — типичный IR-сбой: бит-слипы/всплески). Гарантия обнаружения = ширине контроля:
- single 8 бит и double — надёжны на коротких burst (≥8 бит), эмпирически в выборке не пропускали и длиннее;
- **nibble-fold** — гарантия ~4 бита (эмпирически до 7);
- **nibble-trunc** — **пропускает даже ОДИНОЧНЫЙ бит** (усечение теряет старший ниббл) → так делать нельзя.
## 4. Рекомендация
- **CRC — не то место, где стоит экономить байт.** Сжатие контроля до 1 байта повышает пропуск сдвигов/burst в ~50150× (с ~0.01 % до ~1 %) — прямо в том классе ошибок, ради которого второй полином и вводили.
- Если байт очень нужен — забирать его **не из CRC**, а из уже выявленного резерва: свободные `msgType` (3,5), «пустая» зона длины (компактные кадры), или переупаковка адресов/полей. 16-битный контроль (2×CRC8) сохранить.
- Если всё же сжимать CRC до 1 байта — только **fold (xor нибблов)**, никогда не truncate; и принять ~1 % пропуска на сдвигах (в ~50100× хуже текущего). Как отдельный компромисс — обсуждать вместе с обратной совместимостью.
(Обратную совместимость версий протокола обсудим отдельно — по запросу Даши.)

View 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} бит подряд")

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

View File

@ -1,5 +1,7 @@
# Контракт бэкенда DMA-TX ИК (`IrDmaTxStm32`) # Контракт бэкенда 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`). Платформа: **STM32G4**, Arduino STM32. Передача: **DMA memory → GPIO BSRR**, запрос от **TIM UPDATE** (частота `carrierFrec×2` из `IR_Encoder::beginClockOnly`).
### Число потоков (шаблон) ### Число потоков (шаблон)

62
ref/IR_RX_BRIEF_LOG.md Normal file
View 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
View 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);
```

View 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

View File

@ -0,0 +1,50 @@
#pragma once
#include <cstddef>
#include <cstdint>
struct GPIO_TypeDef
{
uint32_t BSRR = 0U;
uint32_t IDR = 0U;
};
using IRQn_Type = int;
enum TimerFormat_t : uint8_t { TICK_FORMAT = 0, MICROSEC_FORMAT, HERTZ_FORMAT };
constexpr uint8_t LOW = 0U;
constexpr uint8_t HIGH = 1U;
constexpr uint8_t INPUT = 0U;
constexpr uint8_t OUTPUT = 1U;
class HardwareTimer
{
public:
void pause() {}
void resume() {}
void setOverflow(uint32_t value, TimerFormat_t = TICK_FORMAT) { overflow_ = value; }
uint32_t getOverflow(TimerFormat_t = TICK_FORMAT) { return overflow_; }
uint32_t getPrescaleFactor() { return 1U; }
uint32_t getTimerClkFreq() { return 12000000U; }
void attachInterrupt(uint8_t, void (*)()) {}
private:
uint32_t overflow_ = 1U;
};
inline GPIO_TypeDef *digitalPinToPort(uint8_t) { return nullptr; }
inline uint16_t digitalPinToBitMask(uint8_t) { return 0U; }
inline void pinMode(uint8_t, uint8_t) {}
inline void digitalWrite(uint8_t, uint8_t) {}
inline void NVIC_SetPriority(IRQn_Type, uint8_t) {}
inline void noInterrupts() {}
inline void interrupts() {}
struct ArduinoSerialStub
{
template <typename T> void print(const T &) {}
template <typename T> void println(const T &) {}
void println() {}
};
inline ArduinoSerialStub Serial;

38
tests/run_host_tests.ps1 Normal file
View File

@ -0,0 +1,38 @@
$ErrorActionPreference = 'Stop'
$repo = Split-Path -Parent $PSScriptRoot
$build = Join-Path $PSScriptRoot '.build'
New-Item -ItemType Directory -Force -Path $build | Out-Null
$compiler = if (Test-Path -LiteralPath 'C:\MinGW\bin\g++.exe') {
'C:\MinGW\bin\g++.exe'
} else {
(Get-Command g++ -ErrorAction Stop).Source
}
$common = @(
'-std=c++17', '-Wall', '-Wextra', '-Werror',
'-Wno-unused-parameter', '-Wno-ignored-qualifiers', '-Wno-sign-compare',
'-I', (Join-Path $PSScriptRoot 'arduino_stubs'),
'-I', $repo
)
& $compiler @common `
(Join-Path $PSScriptRoot 'test_timing_contract.cpp') `
(Join-Path $repo 'IR_Encoder.cpp') `
(Join-Path $repo 'IR_config.cpp') `
'-o' (Join-Path $build 'test_timing_contract.exe')
if ($LASTEXITCODE -ne 0) { throw 'timing test build failed' }
& (Join-Path $build 'test_timing_contract.exe')
if ($LASTEXITCODE -ne 0) { throw 'timing test failed' }
& $compiler @common `
(Join-Path $PSScriptRoot 'test_packet_types.cpp') `
(Join-Path $repo 'PacketTypes.cpp') `
(Join-Path $repo 'IR_config.cpp') `
'-o' (Join-Path $build 'test_packet_types.exe')
if ($LASTEXITCODE -ne 0) { throw 'packet test build failed' }
& (Join-Path $build 'test_packet_types.exe')
if ($LASTEXITCODE -ne 0) { throw 'packet test failed' }

145
tests/test_packet_types.cpp Normal file
View File

@ -0,0 +1,145 @@
#include "PacketTypes.h"
#include <array>
#include <cassert>
#include <cstdint>
#include <iostream>
namespace
{
template <typename Packet>
class ExposedPacket : public Packet
{
public:
void attach(IR_FOX::PackInfo *info, uint16_t id = 0U)
{
this->set(info, id);
}
};
IR_FOX::PackInfo makeFrame(uint8_t *buffer, uint8_t msgType, uint8_t wireBytes)
{
buffer[0] = static_cast<uint8_t>((msgType << 5) | (wireBytes & IR_MASK_MSG_INFO));
IR_FOX::PackInfo result;
result.buffer = buffer;
result.packSize = wireBytes;
return result;
}
template <typename Packet>
void verifyMinimum(uint8_t msgType, uint8_t minimum)
{
std::array<uint8_t, irproto::kMaxWireFrameBytes> buffer{};
ExposedPacket<Packet> packet;
IR_FOX::PackInfo shortFrame = makeFrame(buffer.data(), msgType, minimum - 1U);
packet.attach(&shortFrame);
assert(!packet.available());
assert(!packet.availableRaw());
IR_FOX::PackInfo minimumFrame = makeFrame(buffer.data(), msgType, minimum);
packet.attach(&minimumFrame);
assert(packet.available());
}
void verifyTypedMinimums()
{
verifyMinimum<PacketTypes::Data>(IR_MSG_DATA_ACCEPT, 7U);
verifyMinimum<PacketTypes::Data>(IR_MSG_DATA_NOACCEPT, 7U);
verifyMinimum<PacketTypes::DataBack>(IR_MSG_BACK, 5U);
verifyMinimum<PacketTypes::DataBack>(IR_MSG_BACK_TO, 7U);
verifyMinimum<PacketTypes::Accept>(IR_MSG_ACCEPT, 6U);
verifyMinimum<PacketTypes::Request>(IR_MSG_REQUEST, 7U);
}
void verifyDataAccessCannotUnderflow()
{
std::array<uint8_t, irproto::kMaxWireFrameBytes> buffer{};
ExposedPacket<PacketTypes::Data> data;
for (uint8_t wireBytes = 0U; wireBytes < irproto::kDataFrameOverheadBytes; ++wireBytes)
{
IR_FOX::PackInfo malformed = makeFrame(buffer.data(), IR_MSG_DATA_ACCEPT, wireBytes);
data.attach(&malformed);
assert(!data.available());
assert(data.getDataSize() == 0U);
assert(data.getDataPrt() == nullptr);
assert(data.getAddrTo() == 0U);
}
IR_FOX::PackInfo empty = makeFrame(buffer.data(), IR_MSG_DATA_ACCEPT, 7U);
data.attach(&empty);
assert(data.available());
assert(data.getDataSize() == 0U);
assert(data.getDataPrt() == buffer.data() + 5U);
IR_FOX::PackInfo oneByte = makeFrame(buffer.data(), IR_MSG_DATA_ACCEPT, 8U);
data.attach(&oneByte);
assert(data.available());
assert(data.getDataSize() == 1U);
assert(data.getDataPrt() == buffer.data() + 5U);
}
void verifyBackLayouts()
{
std::array<uint8_t, irproto::kMaxWireFrameBytes> buffer{};
ExposedPacket<PacketTypes::DataBack> back;
IR_FOX::PackInfo shortBroadcast = makeFrame(buffer.data(), IR_MSG_BACK, 4U);
back.attach(&shortBroadcast);
assert(!back.available());
assert(back.getDataSize() == 0U);
assert(back.getDataPrt() == nullptr);
IR_FOX::PackInfo broadcast = makeFrame(buffer.data(), IR_MSG_BACK, 5U);
back.attach(&broadcast);
assert(back.available());
assert(back.getDataSize() == 0U);
assert(back.getDataPrt() == buffer.data() + 3U);
IR_FOX::PackInfo shortAddressed = makeFrame(buffer.data(), IR_MSG_BACK_TO, 6U);
back.attach(&shortAddressed);
assert(!back.available());
assert(back.getDataSize() == 0U);
IR_FOX::PackInfo addressed = makeFrame(buffer.data(), IR_MSG_BACK_TO, 7U);
back.attach(&addressed);
assert(back.available());
assert(back.getDataSize() == 0U);
assert(back.getDataPrt() == buffer.data() + 5U);
}
void verifyRawAndHeaderContracts()
{
std::array<uint8_t, irproto::kMaxWireFrameBytes> buffer{};
ExposedPacket<PacketTypes::BasePack> raw;
// Raw diagnostics remain able to observe a CRC-sized frame even when its
// declared type-specific layout is too short.
IR_FOX::PackInfo shortTyped = makeFrame(buffer.data(), IR_MSG_DATA_ACCEPT, 3U);
raw.attach(&shortTyped);
assert(raw.availableRaw());
IR_FOX::PackInfo inconsistent = makeFrame(buffer.data(), IR_MSG_BACK, 5U);
inconsistent.packSize = 6U;
raw.attach(&inconsistent);
assert(!raw.available());
assert(!raw.availableRaw());
IR_FOX::PackInfo nullFrame;
nullFrame.packSize = 31U;
raw.attach(&nullFrame);
assert(!raw.available());
assert(!raw.availableRaw());
}
}
int main()
{
verifyTypedMinimums();
verifyDataAccessCannotUnderflow();
verifyBackLayouts();
verifyRawAndHeaderContracts();
std::cout << "IR packet boundary tests: OK\n";
return 0;
}

View File

@ -0,0 +1,85 @@
#include "IR_Encoder.h"
#include "IR_DecoderRaw.h"
#include <array>
#include <cassert>
#include <cstdint>
#include <iostream>
// Link seams: these paths are not exercised by the pure host timing test.
bool IR_DecoderRaw::registerPairMuteEncoder(IR_Encoder *) { return true; }
void IR_DecoderRaw::refreshPairMuteState() {}
void IR_Encoder::send_HIGH(bool) {}
void IR_Encoder::send_LOW() {}
void IR_Encoder::send_EMPTY(uint8_t) {}
namespace
{
static_assert(irproto::dataWireBytes(0U) == 7U, "empty DATA wire size changed");
static_assert(irproto::dataWireBytes(3U) == 10U, "DATA wire size changed");
static_assert(irproto::dataWireBytes(24U) == 31U, "maximum DATA wire size changed");
static_assert(irproto::dataWireBytes(25U) == 0U, "oversized DATA must be rejected");
static_assert(irproto::backWireBytes(1U) == 6U, "BACK wire size changed");
static_assert(irproto::backWireBytes(26U) == 31U, "maximum BACK wire size changed");
static_assert(irproto::backToWireBytes(24U) == 31U, "maximum BACK_TO wire size changed");
static_assert(irproto::wireLogicalTicks(6U) == 5472U, "6-byte tick count changed");
static_assert(irproto::wireLogicalTicks(10U) == 8728U, "10-byte tick count changed");
static_assert(irproto::wireLogicalTicks(31U) == 25822U, "31-byte tick count changed");
static_assert(irproto::preambleAirtimeUsCeil() == 7737U, "preamble airtime changed");
static_assert(irproto::wireAirtimeUsCeil(6U) == 72000U, "6-byte airtime changed");
static_assert(irproto::wireAirtimeUsCeil(10U) == 114843U, "10-byte airtime changed");
static_assert(irproto::wireAirtimeUsCeil(31U) == 339764U, "31-byte airtime changed");
static_assert(irproto::responseStartGuardUs() == 57198U, "response-start guard changed");
static_assert(irproto::responseFrameGuardUs(6U) == 131100U, "response-frame guard changed");
uint32_t sumLogicalTicks(const IrTxGateRun *runs, size_t count)
{
uint32_t total = 0U;
for (size_t i = 0U; i < count; ++i)
total += runs[i].lenTicks;
return total;
}
void verifyFormulaAgainstTxFsm()
{
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
std::array<IrTxGateRun, 1024U> runs{};
for (uint8_t wireBytes = 1U; wireBytes <= irproto::kMaxWireFrameBytes; ++wireBytes)
{
for (uint8_t pattern = 0U; pattern < 4U; ++pattern)
{
for (uint8_t i = 0U; i < wireBytes; ++i)
{
frame[i] = pattern == 0U ? 0x00U
: pattern == 1U ? 0xFFU
: pattern == 2U ? static_cast<uint8_t>((i & 1U) ? 0x55U : 0xAAU)
: static_cast<uint8_t>(i * 73U + 19U);
}
const size_t count = IR_Encoder::buildGateRuns(
frame.data(), wireBytes, runs.data(), runs.size());
assert(count != 0U);
assert(sumLogicalTicks(runs.data(), count) == irproto::wireLogicalTicks(wireBytes));
}
}
}
void verifyPublicSendTimeResults()
{
IR_Encoder encoder(1U, 42U, nullptr, false);
uint8_t payload[26]{};
assert(encoder.testSendAccept(1U) == 72U); // six-byte wire frame
assert(encoder.testSendTime(1U, payload, 3U) == 115U); // ten-byte wire frame
assert(encoder.testSendBack(payload, 26U) == 340U); // 31-byte wire frame
}
}
int main()
{
verifyFormulaAgainstTxFsm();
verifyPublicSendTimeResults();
std::cout << "IR timing contract tests: OK\n";
return 0;
}

View 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
View 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()

View File

@ -16,7 +16,8 @@ WRONG_PACK_SYNC — отдельное событие с причиной и т
отброшенных фронтов; без флага эти события только в счётчиках сводки. отброшенных фронтов; без флага эти события только в счётчиках сводки.
Не моделирует IRDEBUG_SERIAL_SOFT_REJECT (жёсткий Wrong sync). Не моделирует IRDEBUG_SERIAL_SOFT_REJECT (жёсткий Wrong sync).
Таймауты: между фронтами, если gap > IR_timeout*2 и isRecive — checkTimeout. Таймауты: как IR_DecoderRaw::tick — listenStart и checkTimeout в начале каждого тика (не только при пустых
очередях), пауза > IR_timeout*2 по lastEdgeTime; при checkTimeout: isReciveRaw=0, firstRX(), lastEdgeTime=now.
""" """
from __future__ import annotations from __future__ import annotations
@ -360,8 +361,8 @@ class DecoderSim:
def listen_start(self, now: int) -> None: def listen_start(self, now: int) -> None:
to = ir_timeout_us(self.rise_sync_time) to = ir_timeout_us(self.rise_sync_time)
if self.is_recive_raw and (now - self.prev_rise) > to * 2: 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 from prev_rise)") self.events.append(f"t={now} listenStart abort raw (gap since last edge, как IR_DecoderRaw)")
self.is_recive_raw = False self.is_recive_raw = False
self._clear_packet_state() self._clear_packet_state()
self.first_rx() self.first_rx()
@ -371,9 +372,12 @@ class DecoderSim:
return return
to = ir_timeout_us(self.rise_sync_time) to = ir_timeout_us(self.rise_sync_time)
if now - self.last_edge_time > to * 2: if now - self.last_edge_time > to * 2:
self.events.append(f"t={now} checkTimeout (gap since last edge)") self.events.append(f"t={now} checkTimeout -> isReciveRaw=0, firstRX() (как IR_DecoderRaw)")
self.is_recive = False self.is_recive = False
self.last_edge_time = now 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: def write_to_buffer(self, bit_val: int) -> None:
if self.i_data_buffer > DATA_BYTE_SIZE_MAX * 8: if self.i_data_buffer > DATA_BYTE_SIZE_MAX * 8:
@ -382,6 +386,7 @@ class DecoderSim:
if self.is_buffer_overflow or self.is_preamb or self.is_wrong_pack: if self.is_buffer_overflow or self.is_preamb or self.is_wrong_pack:
self.is_recive = False self.is_recive = False
self.is_recive_raw = False self.is_recive_raw = False
self.first_rx()
return return
if self.buf_bit_pos == self.next_control_bit: if self.buf_bit_pos == self.next_control_bit:
@ -478,6 +483,9 @@ class DecoderSim:
def tick_edge(self, t: int, level: int) -> None: def tick_edge(self, t: int, level: int) -> None:
"""Один фронт: level = состояние линии ПОСЛЕ фронта (как dir в C++).""" """Один фронт: level = состояние линии ПОСЛЕ фронта (как dir в C++)."""
self.listen_start(t) 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 self.last_edge_time = t
rising = level == 1 rising = level == 1
@ -689,9 +697,6 @@ def main() -> int:
dec = DecoderSim(verbose=args.verbose) dec = DecoderSim(verbose=args.verbose)
for e in edges: for e in edges:
to_us = ir_timeout_us(dec.rise_sync_time)
if dec.is_recive and dec.last_edge_time > 0 and (e.t_us - dec.last_edge_time) > to_us * 2:
dec.check_timeout(e.t_us)
dec.tick_edge(e.t_us, e.level) dec.tick_edge(e.t_us, e.level)
print("--- События декодера (первые N), пакеты разделены пустой строкой ---") print("--- События декодера (первые N), пакеты разделены пустой строкой ---")