Compare commits

..

2 Commits

Author SHA1 Message Date
d4rks1d33 99ac826a49 Added script to easily add new tables to modulation hopping (depending on which specific modulation you want to use)
Build Dev Firmware / build (push) Successful in 17m30s
2026-06-13 17:12:39 -03:00
d4rks1d33 a3698f93a9 Improvements in modulation hopping. Thanks Zero-Mega
Build Dev Firmware / build (push) Successful in 16m28s
2026-06-13 16:56:02 -03:00
9 changed files with 352 additions and 34 deletions
+34 -32
View File
@@ -1,9 +1,11 @@
#include "subghz_txrx_i.h" // IWYU pragma: keep
#include <math.h>
#include <furi_hal_subghz.h>
#include <lib/subghz/protocols/protocol_items.h>
#include <applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h>
#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
#include "../../../../lib/subghz/devices/subghz_preset_delta.h"
#include <lib/subghz/blocks/custom_btn.h>
#define TAG "SubGhzTxRx"
@@ -498,6 +500,36 @@ void subghz_txrx_hopper_pause(SubGhzTxRx* instance) {
}
}
// Identify the hop index (0=AM650, 1=FM476, 2=FM95) from the name.
// Must match the order defined in subghz_preset_delta.h
static int subghz_hop_index_from_name(const char* name) {
if(strcmp(name, "AM650") == 0) return 0;
if(strcmp(name, "FM476") == 0) return 1;
if(strcmp(name, "FM95") == 0) return 2;
return -1; // is not part of the fast hopping set
}
// Applies the target preset using delta-patch (without SRES) when possible,
// or falls back to the original full reload in any other case.
static void subghz_txrx_apply_preset_fast(
SubGhzTxRx* instance,
const char* old_preset_name,
const char* preset_name) {
int from_idx = subghz_hop_index_from_name(old_preset_name);
int to_idx = subghz_hop_index_from_name(preset_name);
if(instance->radio_device_type == SubGhzRadioDeviceTypeInternal && from_idx >= 0 &&
to_idx >= 0 && from_idx != to_idx) {
// Fast path: delta-patch without SRES or full reload (only internal CC1101)
const PresetDeltaEntry* e = &preset_delta_table[from_idx][to_idx];
furi_hal_subghz_apply_preset_delta(e->delta, e->delta_len, e->needs_scal, e->pa_table);
} else {
// Fallback: original behavior (full reload)
subghz_devices_load_preset(
instance->radio_device, FuriHalSubGhzPresetCustom, instance->preset->data);
}
}
void subghz_txrx_preset_hopper_update(SubGhzTxRx* instance, float stay_threshold) {
furi_assert(instance);
@@ -550,22 +582,7 @@ void subghz_txrx_preset_hopper_update(SubGhzTxRx* instance, float stay_threshold
subghz_txrx_set_preset_internal(
instance, instance->preset->frequency, actual_preset_idx, 0);
bool old_is_am = (strstr(old_preset_name, "AM") != NULL);
bool new_is_am = (strstr(preset_name, "AM") != NULL);
bool modulation_changed = (old_is_am != new_is_am);
if(modulation_changed) {
subghz_devices_reset(instance->radio_device);
subghz_devices_load_preset(
instance->radio_device,
FuriHalSubGhzPresetCustom,
instance->preset->data);
} else {
subghz_devices_load_preset(
instance->radio_device,
FuriHalSubGhzPresetCustom,
instance->preset->data);
}
subghz_txrx_apply_preset_fast(instance, old_preset_name, preset_name);
subghz_txrx_rx(instance, instance->preset->frequency);
}
@@ -588,22 +605,7 @@ void subghz_txrx_preset_hopper_update(SubGhzTxRx* instance, float stay_threshold
subghz_txrx_set_preset_internal(
instance, instance->preset->frequency, instance->preset_hopper_idx, 0);
bool old_is_am = (strstr(old_preset_name, "AM") != NULL);
bool new_is_am = (strstr(preset_name, "AM") != NULL);
bool modulation_changed = (old_is_am != new_is_am);
if(modulation_changed) {
subghz_devices_reset(instance->radio_device);
subghz_devices_load_preset(
instance->radio_device,
FuriHalSubGhzPresetCustom,
instance->preset->data);
} else {
subghz_devices_load_preset(
instance->radio_device,
FuriHalSubGhzPresetCustom,
instance->preset->data);
}
subghz_txrx_apply_preset_fast(instance, old_preset_name, preset_name);
subghz_txrx_rx(instance, instance->preset->frequency);
}
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""
Generates delta {reg,val} tables between the 3 hopper presets (AM650, FM476, FM95) for fast hopping without SRES.
Uses CC1101 power-on-reset (POR) values for registers that a preset doesn't explicitly set, since the current flow performs SRES before loading each preset (which resets those registers to their default values).
Sources:
- AM650 = subghz_device_cc1101_preset_ook_650khz_async_regs (cc1101_configs.c)
- FM476 = subghz_device_cc1101_preset_2fsk_dev47_6khz_async_regs (cc1101_configs.c)
- FM95 = Custom_preset_data in setting_user
"""
# POR values (CC1101 datasheet Table 23) for records that
# appear in at least one of the 3 presets but not all.
POR_DEFAULTS = {
0x02: 0x3F, # IOCFG0
0x03: 0x07, # FIFOTHR
0x07: 0x04, # PKTCTRL1
0x08: 0x45, # PKTCTRL0
0x0B: 0x0F, # FSCTRL1
0x10: 0x8C, # MDMCFG4
0x11: 0x22, # MDMCFG3
0x12: 0x02, # MDMCFG2
0x13: 0x22, # MDMCFG1
0x14: 0xF8, # MDMCFG0
0x15: 0x47, # DEVIATN
0x18: 0x18, # MCSM0
0x19: 0x14, # FOCCFG
0x1B: 0x03, # AGCCTRL2
0x1C: 0x40, # AGCCTRL1
0x1D: 0x91, # AGCCTRL0
0x20: 0xFB, # WORCTRL
0x21: 0x56, # FREND1
0x22: 0x10, # FREND0
}
# Registers that, if changed, force SCAL (synthesizer recalibration)
FREQ_SYNTH_REGS = {0x0B, 0x0C, 0x0D, 0x0E, 0x0F} # FSCTRL1/0, FREQ2/1/0
# Explicit definitions (only what each preset actually writes)
AM650 = {
0x02: 0x0D, 0x03: 0x07, 0x08: 0x32, 0x0B: 0x06,
0x10: 0x17, 0x11: 0x32, 0x12: 0x30, 0x13: 0x00, 0x14: 0x00,
0x18: 0x18, 0x19: 0x18, 0x1B: 0x07, 0x1C: 0x00, 0x1D: 0x91,
0x20: 0xFB, 0x21: 0xB6, 0x22: 0x11,
}
AM650_PA = [0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
FM476 = {
0x02: 0x0D, 0x07: 0x04, 0x08: 0x32, 0x0B: 0x06,
0x10: 0x67, 0x11: 0x83, 0x12: 0x04, 0x13: 0x02, 0x14: 0x00, 0x15: 0x47,
0x18: 0x18, 0x19: 0x16, 0x1B: 0x07, 0x1C: 0x00, 0x1D: 0x91,
0x20: 0xFB, 0x21: 0x56, 0x22: 0x10,
}
FM476_PA = [0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
FM95 = {
0x02: 0x0D, 0x07: 0x04, 0x08: 0x32, 0x0B: 0x06,
0x10: 0x67, 0x11: 0x83, 0x12: 0x04, 0x13: 0x02, 0x15: 0x24,
0x18: 0x18, 0x19: 0x16, 0x1B: 0x07, 0x1C: 0x00, 0x1D: 0x91,
0x20: 0xFB, 0x21: 0x56, 0x22: 0x10,
}
FM95_PA = [0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
PRESETS = {
"AM650": (AM650, AM650_PA),
"FM476": (FM476, FM476_PA),
"FM95": (FM95, FM95_PA),
}
# Union of all records played by at least one preset
ALL_REGS = sorted(set().union(*[set(r.keys()) for r, _ in PRESETS.values()]))
def effective(regs):
"""Applies POR defaults to ALL_REGS records not explicitly set."""
out = {}
for reg in ALL_REGS:
if reg in regs:
out[reg] = regs[reg]
elif reg in POR_DEFAULTS:
out[reg] = POR_DEFAULTS[reg]
else:
raise ValueError(f"Reg 0x{reg:02X} without known default POR")
return out
def gen_delta(name_a, name_b):
eff_a = effective(PRESETS[name_a][0])
eff_b = effective(PRESETS[name_b][0])
pa_a, pa_b = PRESETS[name_a][1], PRESETS[name_b][1]
delta = []
needs_scal = False
for reg in ALL_REGS:
if eff_a[reg] != eff_b[reg]:
delta.append((reg, eff_b[reg]))
if reg in FREQ_SYNTH_REGS:
needs_scal = True
pa_changed = (pa_a != pa_b)
return delta, needs_scal, (pa_b if pa_changed else None)
def main():
names = list(PRESETS.keys())
print("// === Auto-generated by gen_preset_delta.py ===")
print("// subghz_preset_delta.c\n")
print('#include <stdint.h>')
print('#include <stddef.h>')
print('#include "subghz_preset_delta.h"\n')
entries = {}
for a in names:
for b in names:
if a == b:
continue
delta, needs_scal, pa = gen_delta(a, b)
sym = f"preset_delta_{a}_to_{b}"
print(f"// {a} -> {b}: {len(delta)} records change"
f"{' (+SCAL)' if needs_scal else ''}"
f"{' (+PA)' if pa else ''}")
print(f"static const uint8_t {sym}[] = {{")
for reg, val in delta:
print(f" 0x{reg:02X}, 0x{val:02X},")
print("};")
if pa:
pa_sym = f"{sym}_pa"
print(f"static const uint8_t {pa_sym}[8] = {{ " +
", ".join(f"0x{x:02X}" for x in pa) + " };")
else:
pa_sym = "NULL"
print()
entries[(a, b)] = (sym, len(delta) * 2, needs_scal, pa_sym)
# Lookup table
print("// Order: HOP_AM650=0, HOP_FM476=1, HOP_FM95=2")
print("const PresetDeltaEntry preset_delta_table[HOP_PRESET_COUNT][HOP_PRESET_COUNT] = {")
for a in names:
print(" {")
for b in names:
if a == b:
print(" {0}, // self, unused")
continue
sym, dlen, scal, pa_sym = entries[(a, b)]
print(f" {{ .delta = {sym}, .delta_len = {dlen}, "
f".needs_scal = {'true' if scal else 'false'}, "
f".pa_table = {pa_sym} }}, // {a} -> {b}")
print(" },")
print("};")
if __name__ == "__main__":
main()
+79
View File
@@ -0,0 +1,79 @@
#include <stdint.h>
#include <stddef.h>
#include "subghz_preset_delta.h"
static const uint8_t preset_delta_AM650_to_FM476[] = {
0x10, 0x67,
0x11, 0x83,
0x12, 0x04,
0x13, 0x02,
0x19, 0x16,
0x21, 0x56,
0x22, 0x10,
};
static const uint8_t preset_delta_AM650_to_FM476_pa[8] = { 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
static const uint8_t preset_delta_AM650_to_FM95[] = {
0x10, 0x67,
0x11, 0x83,
0x12, 0x04,
0x13, 0x02,
0x14, 0xF8,
0x15, 0x24,
0x19, 0x16,
0x21, 0x56,
0x22, 0x10,
};
static const uint8_t preset_delta_AM650_to_FM95_pa[8] = { 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
static const uint8_t preset_delta_FM476_to_AM650[] = {
0x10, 0x17,
0x11, 0x32,
0x12, 0x30,
0x13, 0x00,
0x19, 0x18,
0x21, 0xB6,
0x22, 0x11,
};
static const uint8_t preset_delta_FM476_to_AM650_pa[8] = { 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
static const uint8_t preset_delta_FM476_to_FM95[] = {
0x14, 0xF8,
0x15, 0x24,
};
static const uint8_t preset_delta_FM95_to_AM650[] = {
0x10, 0x17,
0x11, 0x32,
0x12, 0x30,
0x13, 0x00,
0x14, 0x00,
0x15, 0x47,
0x19, 0x18,
0x21, 0xB6,
0x22, 0x11,
};
static const uint8_t preset_delta_FM95_to_AM650_pa[8] = { 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
static const uint8_t preset_delta_FM95_to_FM476[] = {
0x14, 0x00,
0x15, 0x47,
};
const PresetDeltaEntry preset_delta_table[HOP_PRESET_COUNT][HOP_PRESET_COUNT] = {
{
{0},
{ .delta = preset_delta_AM650_to_FM476, .delta_len = 14, .needs_scal = false, .pa_table = preset_delta_AM650_to_FM476_pa },
{ .delta = preset_delta_AM650_to_FM95, .delta_len = 18, .needs_scal = false, .pa_table = preset_delta_AM650_to_FM95_pa },
},
{
{ .delta = preset_delta_FM476_to_AM650, .delta_len = 14, .needs_scal = false, .pa_table = preset_delta_FM476_to_AM650_pa },
{0},
{ .delta = preset_delta_FM476_to_FM95, .delta_len = 4, .needs_scal = false, .pa_table = NULL },
},
{
{ .delta = preset_delta_FM95_to_AM650, .delta_len = 18, .needs_scal = false, .pa_table = preset_delta_FM95_to_AM650_pa },
{ .delta = preset_delta_FM95_to_FM476, .delta_len = 4, .needs_scal = false, .pa_table = NULL },
{0},
},
};
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
typedef struct {
const uint8_t* delta;
size_t delta_len;
bool needs_scal;
const uint8_t* pa_table;
} PresetDeltaEntry;
typedef enum {
HOP_AM650 = 0,
HOP_FM476 = 1,
HOP_FM95 = 2,
HOP_PRESET_COUNT,
} SubghzHopPreset;
extern const PresetDeltaEntry preset_delta_table[HOP_PRESET_COUNT][HOP_PRESET_COUNT];
+1
View File
@@ -56,6 +56,7 @@ class FlipperApplication:
name: Optional[str] = ""
entry_point: Optional[str] = None
flags: List[str] = field(default_factory=lambda: ["Default"])
cflags: List[str] = field(default_factory=list)
cdefines: List[str] = field(default_factory=list)
requires: List[str] = field(default_factory=list)
conflicts: List[str] = field(default_factory=list)
+1
View File
@@ -61,6 +61,7 @@ class AppBuilder:
("FAP_VERSION", f'\\"{".".join(map(str, self.app.fap_version))}\\"'),
*self.app.cdefines,
],
CCFLAGS=self.app.cflags,
)
self.app_env.VariantDir(self.app_work_dir, self.app._appdir, duplicate=False)
+3 -2
View File
@@ -1,5 +1,5 @@
entry,status,name,type,params
Version,+,89.1,,
Version,+,89.2,,
Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,,
Header,+,applications/services/applications.h,,
Header,+,applications/services/bt/bt_service/bt.h,,
@@ -1783,6 +1783,7 @@ Function,-,furi_hal_spi_config_init,void,
Function,-,furi_hal_spi_config_init_early,void,
Function,-,furi_hal_spi_dma_init,void,
Function,+,furi_hal_spi_release,void,const FuriHalSpiBusHandle*
Function,+,furi_hal_subghz_apply_preset_delta,void,"const uint8_t*, size_t, _Bool, const uint8_t*"
Function,-,furi_hal_subghz_dump_state,void,
Function,+,furi_hal_subghz_flush_rx,void,
Function,+,furi_hal_subghz_flush_tx,void,
@@ -3462,8 +3463,8 @@ Function,+,subghz_block_generic_global_reset,void,void*
Function,+,subghz_block_generic_serialize,SubGhzProtocolStatus,"SubGhzBlockGeneric*, FlipperFormat*, SubGhzRadioPreset*"
Function,+,subghz_custom_btn_get,uint8_t,
Function,-,subghz_custom_btn_get_long,_Bool,
Function,+,subghz_custom_btn_get_original,uint8_t,
Function,+,subghz_custom_btn_get_max_pages,uint8_t,
Function,+,subghz_custom_btn_get_original,uint8_t,
Function,+,subghz_custom_btn_get_page,uint8_t,
Function,+,subghz_custom_btn_has_pages,_Bool,
Function,+,subghz_custom_btn_is_allowed,_Bool,
1 entry status name type params
2 Version + 89.1 89.2
3 Header + applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h
4 Header + applications/services/applications.h
5 Header + applications/services/bt/bt_service/bt.h
1783 Function - furi_hal_spi_config_init_early void
1784 Function - furi_hal_spi_dma_init void
1785 Function + furi_hal_spi_release void const FuriHalSpiBusHandle*
1786 Function + furi_hal_subghz_apply_preset_delta void const uint8_t*, size_t, _Bool, const uint8_t*
1787 Function - furi_hal_subghz_dump_state void
1788 Function + furi_hal_subghz_flush_rx void
1789 Function + furi_hal_subghz_flush_tx void
3463 Function + subghz_block_generic_serialize SubGhzProtocolStatus SubGhzBlockGeneric*, FlipperFormat*, SubGhzRadioPreset*
3464 Function + subghz_custom_btn_get uint8_t
3465 Function - subghz_custom_btn_get_long _Bool
Function + subghz_custom_btn_get_original uint8_t
3466 Function + subghz_custom_btn_get_max_pages uint8_t
3467 Function + subghz_custom_btn_get_original uint8_t
3468 Function + subghz_custom_btn_get_page uint8_t
3469 Function + subghz_custom_btn_has_pages _Bool
3470 Function + subghz_custom_btn_is_allowed _Bool
+53
View File
@@ -218,6 +218,59 @@ void furi_hal_subghz_load_custom_preset(const uint8_t* preset_data) {
}
}
/**
* @brief Applies only to the registers that change between the current preset
* and the destination preset, without SRES or a full reload.
*
* Requires the chip state to be RX/TX or IDLE before calling;
* the function itself forces IDLE (without SRES) before writing.
*
* @param delta array of {reg, val} pairs (exact length given by delta_len)
* @param delta_len number of bytes in delta (multiple of 2)
* @param needs_scal if true, forces SCAL after writing to the delta
* @param pa_table 8 bytes of new PA table, or NULL if not changing
*/
void furi_hal_subghz_apply_preset_delta(
const uint8_t* delta,
size_t delta_len,
bool needs_scal,
const uint8_t* pa_table) {
furi_check(delta);
furi_hal_spi_acquire(&furi_hal_spi_bus_handle_subghz);
// Ensure IDLE (not SRES!) before touching records
cc1101_switch_to_idle(&furi_hal_spi_bus_handle_subghz);
furi_check(cc1101_wait_status_state(&furi_hal_spi_bus_handle_subghz, CC1101StateIDLE, 10000));
// Write only the records that change
for(size_t i = 0; i + 1 < delta_len; i += 2) {
cc1101_write_reg(&furi_hal_spi_bus_handle_subghz, delta[i], delta[i + 1]);
}
if(needs_scal) {
cc1101_calibrate(&furi_hal_spi_bus_handle_subghz);
furi_check(
cc1101_wait_status_state(&furi_hal_spi_bus_handle_subghz, CC1101StateIDLE, 10000));
}
furi_hal_spi_release(&furi_hal_spi_bus_handle_subghz);
if(pa_table) {
furi_hal_subghz_load_patable(pa_table);
}
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) {
FURI_LOG_D(
TAG,
"Applied preset delta: %u regs, scal=%d, pa=%d",
(unsigned)(delta_len / 2),
needs_scal,
pa_table != NULL);
}
}
void furi_hal_subghz_load_registers(const uint8_t* data) {
furi_check(data);
+6
View File
@@ -66,6 +66,12 @@ void furi_hal_subghz_dump_state(void);
*/
void furi_hal_subghz_load_custom_preset(const uint8_t* preset_data);
void furi_hal_subghz_apply_preset_delta(
const uint8_t* delta,
size_t delta_len,
bool needs_scal,
const uint8_t* pa_table);
/** Load registers
*
* @param data Registers data