mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
У 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>
271 lines
12 KiB
ObjectPascal
271 lines
12 KiB
ObjectPascal
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. Этого набора хватает
|
||
на фазы 1–4 (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.
|