Files
ewsdr/IIOBindings.pas
ew8bakandClaude Opus 5 25fbf10212 feat(cw): программный кейер — телеграф на Pluto/LibreSDR
У openHPSDR точки и тире формирует кейер в FPGA, и это остаётся путём по
умолчанию: тайминги в железе, джиттер PC не влияет вовсе. У AD936x такого
кейера нет вовсе, поэтому телеграфа на Pluto не было в принципе — голосовой
TXA в CW не запускается, а несущую давать некому.

CWKeyer.pas: TCWLocalKeyer рисует манипулированную несущую прямо в поток IQ,
мимо WDSP (эталон pihpsdr cw_shape_buffer: в WDSP режим TXA_CWL — ветка SSB,
да и PostGen не умеет огибающей). Длительности живут В СЭМПЛАХ, а не в
миллисекундах: планировщик ОС влияет только на темп выдачи блоков, а его
сглаживает FIFO бэкенда. Умеет текст, иамбик A/B и прямой ключ; TCWKeyPort
читает манипулятор с модемных линий COM/USB-serial (DTR/RTS питают ключ,
CTS/DSR/DCD/RI — лепестки). Оффлайн-прогон: PARIS занимает ровно 43 точки,
все посылки и паузы 1/3/7 dit с точностью до сэмпла.

Сессия = одна передача, а не одна посылка: PTT, реле T/R, антенна, OC и
аттенюатор Pluto поднимаются один раз и держатся до конца hang — внутри
манипуляция чисто цифровая. Предзаполнение FIFO задано железом: TX-поток
Pluto наполняет буфер целиком (16384 пары ≈ 28 мс на 576k) и добивает
нулями, чего не хватило. Сайдтон эту латентность не наследует — кольцо
огибающей листается, если отставание больше 20 мс.

Сдвиг несущей: у zero-IF ноль бейсбенда совпадает с частотой гетеродина, и
там же его утечка — манипуляция на нуле дала бы backwave ровно на рабочей
частоте. Поэтому несущая идёт на сдвиге, а гетеродин уезжает навстречу через
новую единую дверь TXTuneFreqHz (все пуши TX-частоты переведены на неё).
Знак (CW_TX_BB_SIGN) замерен на эфире, а не выведен.

Развилка источника в UI — один список Keyer: firmware / software (PC) / off;
хранится прежней парой флагов, и на железе без кейера выбор «прошивка»
вырождается в программный, поэтому Pluto работает из коробки. Новое поле
caps HasCWKeyer (раньше это неявно жило в HasHWMic).

Попутно: у AD9361 calib_mode стоял manual_tx_quad, то есть драйвер не
переигрывал калибровку TX quad (она нулит утечку гетеродина) на сменах
TX LO — выставляем auto при подключении.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:38:54 +03:00

271 lines
12 KiB
ObjectPascal
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit IIOBindings;
{
Минимальный биндинг к libiio (классический API 0.x; в системе 0.26) с
ДИНАМИЧЕСКОЙ загрузкой. Если libiio нет — IIOLoad возвращает False, и
Pluto-бэкенд просто не участвует в discovery (приложение работает с HPSDR).
Загружаем libiio.so / libiio.so.0 (Linux), iio.dll (Windows),
libiio.dylib (macOS). Имена символов одинаковые на всех платформах.
Покрыты функции для: scan (discovery), context create/destroy, поиск
device/channel, чтение/запись атрибутов, буферы RX/TX. Этого набора хватает
на фазы 14 (discovery → RX → управление → TX).
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
dynlibs, ctypes, SysUtils;
type
// Непрозрачные указатели libiio
Piio_scan_context = Pointer;
Piio_context_info = Pointer;
PPiio_context_info = ^Piio_context_info; // struct iio_context_info **
Piio_context = Pointer;
Piio_device = Pointer;
Piio_channel = Pointer;
Piio_buffer = Pointer;
var
// ---- Scan / discovery ----
iio_create_scan_context: function(backend: PAnsiChar; flags: cuint): Piio_scan_context; cdecl;
iio_scan_context_destroy: procedure(ctx: Piio_scan_context); cdecl;
iio_scan_context_get_info_list: function(ctx: Piio_scan_context;
var info: PPiio_context_info): ptrint; cdecl;
iio_context_info_list_free: procedure(info: PPiio_context_info); cdecl;
iio_context_info_get_description: function(info: Piio_context_info): PAnsiChar; cdecl;
iio_context_info_get_uri: function(info: Piio_context_info): PAnsiChar; cdecl;
// ---- Context ----
iio_create_context_from_uri: function(uri: PAnsiChar): Piio_context; cdecl;
iio_context_destroy: procedure(ctx: Piio_context); cdecl;
iio_context_get_name: function(ctx: Piio_context): PAnsiChar; cdecl;
iio_context_find_device: function(ctx: Piio_context; name: PAnsiChar): Piio_device; cdecl;
iio_context_get_attr_value: function(ctx: Piio_context; name: PAnsiChar): PAnsiChar; cdecl;
// ---- Device / channel ----
// output — C '_Bool' (1 байт). Передаём явный байт 0/1 (ByteBool/True давал
// некорректное значение → output-каналы не находились).
iio_device_find_channel: function(dev: Piio_device; name: PAnsiChar;
output: cuchar): Piio_channel; cdecl;
iio_device_set_kernel_buffers_count: function(dev: Piio_device; nb: cuint): cint; cdecl;
iio_channel_enable: procedure(chn: Piio_channel); cdecl;
iio_channel_disable: procedure(chn: Piio_channel); cdecl;
// ---- Атрибуты ----
iio_channel_attr_read_longlong: function(chn: Piio_channel; attr: PAnsiChar;
out val: clonglong): cint; cdecl;
iio_channel_attr_read_double: function(chn: Piio_channel; attr: PAnsiChar;
out val: cdouble): cint; cdecl;
iio_channel_attr_write_longlong: function(chn: Piio_channel; attr: PAnsiChar;
val: clonglong): cint; cdecl;
iio_channel_attr_write: function(chn: Piio_channel; attr: PAnsiChar;
src: PAnsiChar): ptrint; cdecl;
iio_channel_attr_write_bool: function(chn: Piio_channel; attr: PAnsiChar;
val: ByteBool): cint; cdecl;
iio_channel_attr_write_double: function(chn: Piio_channel; attr: PAnsiChar;
val: cdouble): cint; cdecl;
// ---- Атрибуты устройства (не канальные) ----
// ad9361-phy: 'xo_correction' — фактическая частота опорного XO в Гц, какой её
// считает драйвер. Правка = частотная калибровка всего чипа (RX и TX LO).
iio_device_attr_read_longlong: function(dev: Piio_device; attr: PAnsiChar;
out val: clonglong): cint; cdecl;
iio_device_attr_write_longlong: function(dev: Piio_device; attr: PAnsiChar;
val: clonglong): cint; cdecl;
// 'calib_mode' — режим калибровок AD936x (строка): 'auto' = драйвер сам
// переигрывает TX quad (утечка LO + квадратура) на каждой смене TX LO.
iio_device_attr_write: function(dev: Piio_device; attr: PAnsiChar;
src: PAnsiChar): ptrint; cdecl;
// ---- Debug-атрибуты устройства (debugfs) ----
// Нужны для выбора антенного разъёма у AD936x в режиме 1R1T:
// 'adi,1rx-1tx-mode-use-rx-num'/'-tx-num' (какой физический канал кормит тракт)
// + 'initialize' (драйвер заново прогоняет ad9361_setup из pdata).
iio_device_debug_attr_read: function(dev: Piio_device; attr: PAnsiChar;
dst: PAnsiChar; len: csize_t): ptrint; cdecl;
iio_device_debug_attr_write: function(dev: Piio_device; attr: PAnsiChar;
src: PAnsiChar): ptrint; cdecl;
iio_device_debug_attr_write_longlong: function(dev: Piio_device; attr: PAnsiChar;
val: clonglong): cint; cdecl;
// Прямой доступ к регистрам (debugfs direct_reg_access) — быстрое переключение
// антенного мультиплексора AD9361 (биты RX1/RX2 и TX1/TX2 регистров 0x3/0x2).
iio_device_reg_read: function(dev: Piio_device; address: cuint32;
out value: cuint32): cint; cdecl;
iio_device_reg_write: function(dev: Piio_device; address: cuint32;
value: cuint32): cint; cdecl;
// ---- Буферы (RX refill / TX push) ----
iio_device_create_buffer: function(dev: Piio_device; samples_count: csize_t;
cyclic: ByteBool): Piio_buffer; cdecl;
iio_buffer_destroy: procedure(buf: Piio_buffer); cdecl;
iio_buffer_cancel: procedure(buf: Piio_buffer); cdecl; // разблокирует refill/push
iio_buffer_refill: function(buf: Piio_buffer): ptrint; cdecl;
iio_buffer_push: function(buf: Piio_buffer): ptrint; cdecl;
iio_buffer_first: function(buf: Piio_buffer; chn: Piio_channel): Pointer; cdecl;
iio_buffer_step: function(buf: Piio_buffer): ptrint; cdecl;
iio_buffer_end: function(buf: Piio_buffer): Pointer; cdecl;
// ---- Утилиты ----
iio_strerror: procedure(err: cint; dst: PAnsiChar; len: csize_t); cdecl;
// ---- libad9361 (опционально) ----
// ad9361_set_bb_rate: настраивает BBPLL + ЗАГРУЖАЕТ FIR-таблицу под частоту.
// Без него прямая запись sampling_frequency на холодную даёт мусор (битый FIR).
// nil если libad9361 не загружена.
ad9361_set_bb_rate: function(dev: Piio_device; rate: culong): cint; cdecl;
ad9361_set_trx_fir_enable: function(dev: Piio_device; enable: cint): cint; cdecl;
// Загружает libiio (один раз). True — готово к использованию. Идемпотентно.
function IIOLoad: Boolean;
// True, если libiio загружена и символы разрешены.
function IIOLoaded: Boolean;
// Расшифровка кода ошибки libiio (err — отрицательный errno).
function IIOStrError(err: cint): string;
implementation
var
GLib: TLibHandle = NilHandle;
GLibAD: TLibHandle = NilHandle; // libad9361 (опционально)
GLoaded: Boolean = False;
const
{$IFDEF WINDOWS}
IIO_LIBS: array[0..1] of string = ('libiio.dll', 'iio.dll');
AD_LIBS: array[0..1] of string = ('libad9361.dll', 'ad9361.dll');
{$ELSE}
{$IFDEF DARWIN}
IIO_LIBS: array[0..1] of string = ('libiio.dylib', 'libiio.1.dylib');
AD_LIBS: array[0..1] of string = ('libad9361.dylib', 'libad9361.0.dylib');
{$ELSE}
IIO_LIBS: array[0..1] of string = ('libiio.so', 'libiio.so.0');
AD_LIBS: array[0..1] of string = ('libad9361.so', 'libad9361.so.0');
{$ENDIF}
{$ENDIF}
function Sym(const Name: string): Pointer;
begin
Result := GetProcedureAddress(GLib, Name);
if Result = nil then
raise Exception.CreateFmt('libiio: symbol "%s" not found', [Name]);
end;
function IIOLoaded: Boolean;
begin
Result := GLoaded;
end;
function IIOLoad: Boolean;
var
I: Integer;
begin
if GLoaded then Exit(True);
if GLib = NilHandle then
for I := 0 to High(IIO_LIBS) do
begin
GLib := LoadLibrary(IIO_LIBS[I]);
if GLib <> NilHandle then Break;
end;
if GLib = NilHandle then Exit(False);
try
iio_create_scan_context := Sym('iio_create_scan_context');
iio_scan_context_destroy := Sym('iio_scan_context_destroy');
iio_scan_context_get_info_list := Sym('iio_scan_context_get_info_list');
iio_context_info_list_free := Sym('iio_context_info_list_free');
iio_context_info_get_description := Sym('iio_context_info_get_description');
iio_context_info_get_uri := Sym('iio_context_info_get_uri');
iio_create_context_from_uri := Sym('iio_create_context_from_uri');
iio_context_destroy := Sym('iio_context_destroy');
iio_context_get_name := Sym('iio_context_get_name');
iio_context_find_device := Sym('iio_context_find_device');
iio_context_get_attr_value := Sym('iio_context_get_attr_value');
iio_device_find_channel := Sym('iio_device_find_channel');
iio_device_set_kernel_buffers_count := Sym('iio_device_set_kernel_buffers_count');
iio_channel_enable := Sym('iio_channel_enable');
iio_channel_disable := Sym('iio_channel_disable');
iio_channel_attr_read_longlong := Sym('iio_channel_attr_read_longlong');
iio_channel_attr_read_double := Sym('iio_channel_attr_read_double');
iio_channel_attr_write_longlong := Sym('iio_channel_attr_write_longlong');
iio_channel_attr_write := Sym('iio_channel_attr_write');
iio_channel_attr_write_bool := Sym('iio_channel_attr_write_bool');
iio_channel_attr_write_double := Sym('iio_channel_attr_write_double');
iio_device_attr_read_longlong := Sym('iio_device_attr_read_longlong');
iio_device_attr_write_longlong := Sym('iio_device_attr_write_longlong');
iio_device_attr_write := Sym('iio_device_attr_write');
iio_device_debug_attr_read := Sym('iio_device_debug_attr_read');
iio_device_debug_attr_write := Sym('iio_device_debug_attr_write');
iio_device_debug_attr_write_longlong := Sym('iio_device_debug_attr_write_longlong');
iio_device_reg_read := Sym('iio_device_reg_read');
iio_device_reg_write := Sym('iio_device_reg_write');
iio_device_create_buffer := Sym('iio_device_create_buffer');
iio_buffer_destroy := Sym('iio_buffer_destroy');
iio_buffer_cancel := Sym('iio_buffer_cancel');
iio_buffer_refill := Sym('iio_buffer_refill');
iio_buffer_push := Sym('iio_buffer_push');
iio_buffer_first := Sym('iio_buffer_first');
iio_buffer_step := Sym('iio_buffer_step');
iio_buffer_end := Sym('iio_buffer_end');
iio_strerror := Sym('iio_strerror');
// libad9361 — опционально (ad9361_set_bb_rate грузит FIR). Отсутствие не
// фатально: ApplySampleRate откатится на прямую запись sampling_frequency.
if GLibAD = NilHandle then
for I := 0 to High(AD_LIBS) do
begin
GLibAD := LoadLibrary(AD_LIBS[I]);
if GLibAD <> NilHandle then Break;
end;
if GLibAD <> NilHandle then
begin
ad9361_set_bb_rate := GetProcedureAddress(GLibAD, 'ad9361_set_bb_rate');
ad9361_set_trx_fir_enable := GetProcedureAddress(GLibAD, 'ad9361_set_trx_fir_enable');
end;
GLoaded := True;
except
on E: Exception do
begin
UnloadLibrary(GLib);
GLib := NilHandle;
GLoaded := False;
end;
end;
Result := GLoaded;
end;
function IIOStrError(err: cint): string;
var
Buf: array[0..255] of AnsiChar;
begin
if GLoaded and Assigned(iio_strerror) then
begin
FillChar(Buf, SizeOf(Buf), 0);
iio_strerror(Abs(err), @Buf[0], SizeOf(Buf));
Result := string(PAnsiChar(@Buf[0]));
end
else
Result := Format('iio error %d', [err]);
end;
finalization
if GLibAD <> NilHandle then
begin
UnloadLibrary(GLibAD);
GLibAD := NilHandle;
end;
if GLib <> NilHandle then
begin
UnloadLibrary(GLib);
GLib := NilHandle;
GLoaded := False;
end;
end.