.NET Core en reinos lejanos: hardware, radios y señales

El objetivo de este artículo es brindarles un punto de partida para trabajar con hardware y, en general, con librerías externas desde .NET Core. Encontrarán varios enlaces a documentación y a otros recursos complementarios.

Primero el contexto, que es lo más complejo:

En un Radio Software, Software Defined Radio o SDR, los componentes de procesamiento de señales se implementan en software, aprovechando los procesadores de propósito general, en lugar de utilizar hardware dedicado. Son esenciales para la Radio Cognitiva.

La bladeRF es un SDR fabricado por Nuand, que está disponible como hardware libre. Me gané una durante la GNU Radio Conference de 2012 y desde entonces he participado en varios proyectos que la incluyen. Además de la bladeRF, usaremos un transverter XB-200, que básicamente amplía el rango de frecuencias de la bladeRF.

Abajo la bladeRF y arriba el XB-200

GNU Radio es un toolkit que tiene bloques de procesamiento para desarrollar aplicaciones de Radio Software. Los bloques se "crean" con C++ y se "conectan" con Python. El bloque que se comunica con la bladeRF, utiliza la librería libbladeRF.

En este artículo usaremos la librería libbladeRF desde .NET Core, en un Mac.

El hardware:

Los drivers de la bladeRF incluyen la utilidad bladeRF-cli y varias librerías. Lo primero es conectar el dispositivo, cargar la FPGA, comprobar que esté operando correctamente y activar el XB-200. Los detalles de los comandos no son importantes.

bladeRF con el XB-200 instalado
Podemos comprobar el estado de la tarjeta usando bladeRF-cli

El proceso:

Debemos ubicar la librería y referenciarla en nuestra aplicación por consola de .NET Core. Una opción rápida es copiarla desde /opt/local/lib/libbladeRF.dylib al directorio del proyecto, configurando el .csproj. La ubicación depende de la plataforma (Windows/Linux/Mac) y hay opciones que nos permiten buscar en distintos directorios o usar variables de entorno según el caso.

<ItemGroup>
<Content Include="/opt/local/lib/libbladeRF.dylib">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

El primer paso es buscar los dispositivos conectados, para esto debemos usar el método bladerf_open de libbladeRF. Como se ve en la documentación, el método recibe como parámetros un device que se pasa como referencia y que, si todo sale bien, contendrá el handle del dispositivo, y un device_identifier que dejaremos en null para activar la detección automática; y retorna un código de estado.

Para llamar ese método desde C#, usamos InteropServices y creamos un conector, binding o fachada con la librería.

using System.Runtime.InteropServices;

[DllImport(dllName: "libbladeRF", EntryPoint="bladerf_open")]
public static extern int BladeRFOpen(out IntPtr handle, string id);

De aquí quiero resaltar 2 cosas: la propiedad EntryPoint, que especifica el nombre del método que queremos llamar. Y el tipo de dato IntPtr, que representa un apuntador y es una de las mejores herramientas que tenemos para interactuar con librerías C++. Podemos llamar el método así:

public static IntPtr GetDeviceHandle()
{
IntPtr handle;
BladeRFOpen(out handle, null);
return handle;
}

Luego debemos establecer la tasa de muestreo usando el método bladerf_set_sample_rate de libbladeRF. El método recibe el handle, el canal (transmisión o recepción), el valor y una referencia que tendrá el estado después del proceso; y retorna un código de estado.

[DllImport(dllName: "libbladeRF", EntryPoint="bladerf_set_sample_rate")]
public static extern int BladeRFSetSampleRate(IntPtr handle, int channel, int value, out int current);

public static bool SetSampleRate(IntPtr handle, int channel, int sampleRate)
{
int current;
BladeRFSetSampleRate(handle, channel, sampleRate, out current);
return current == sampleRate;
}

Debemos hacer lo mismo con el método  bladerf_set_frequency, que recibe como parámetros el handle, el canal y el valor; y retorna el código de estado. A diferencia de bladerf_set_sample_rate, este no retorna el valor final de la variable.

[DllImport(dllName: "libbladeRF", EntryPoint="bladerf_set_frequency")]
public static extern int BladeRFSetFrequency(IntPtr handle, int channel, int value);

public static bool SetCenterFreq(IntPtr handle, int channel, int centerFreq)
{
int result = BladeRFSetFrequency(handle, channel, centerFreq);
return result == 0;
}

Otros métodos sirven para habilitar el XB-200 y para desactivar sus filtros físicos. El primer proceso es importante porque queremos sintonizar 107.5MHz pero la bladeRF va de 300MHz a 3.8GHz. El transverter agrega de 60kHz a 300MHz. El segundo es por simplicidad del ejercicio.

[DllImport(dllName: "libbladeRF", EntryPoint="bladerf_expansion_attach")]
public static extern int BladeRFExpansionAttach(IntPtr handle, int xb);

[DllImport(dllName: "libbladeRF", EntryPoint="bladerf_xb200_set_filterbank")]
public static extern int BladeRFXB200SetFilterbank(IntPtr handle, int channel, int filter);

public static void AttachXB200(IntPtr handle)
{
BladeRFExpansionAttach(handle, 2);
BladeRFXB200SetFilterbank(handle, 0, 3);
}

Para terminar, unimos las partes en el Main.

static void Main(string[] args)
{
var handle = GetDeviceHandle();
Console.WriteLine("Device handle: " + handle);

if (handle == IntPtr.Zero)
{
Console.WriteLine("No device detected");
return;
}

AttachXB200(handle);

var sampleRate = 5_000_000;
var srResult = SetSampleRate(handle, 0, (int)sampleRate);
Console.WriteLine("Set sample rate " + sampleRate + ": " + srResult);

var centerFreq = 107_500_000;
var cfResult = SetCenterFreq(handle, 0, (int)centerFreq);
Console.WriteLine("Set center frequency " + centerFreq + ": " + cfResult);
}

Luego de compilar y correr el programa, se puede ver el resultado en la consola.

Resultado del programa
A través de la librería podemos obtener muestras para después procesarlas y generar gráficos como los de Gqrx, que es un referente de la comunidad, o para crear otras aplicaciones relacionadas con SDR y procesamiento digital de señales.

Este es un reino dominado por C++ y Python, pero C# puede tener un protagonismo interesante gracias a .NET Core. En mi opinión, cada vez más lenguajes fuertemente tipados van a ganar espacio en análisis y procesamiento de datos, conforme mejore el rendimientos y el soporte multiplataforma.

Espero que este artículo les ayude con proyectos que requieren interacción con hardware y con librerías externas. Lo más complejo es interpretar el API externo para evitar errores graves, pero afortunadamente los entornos de ejecución están cada vez más aislados y eso nos evita muchas pantallas azules.

En otra ocasión les mostraré herramientas de visualización usando el mismo hardware. Pueden consultar el código de este ejemplo en GitHub.

Este es un artículo para el primer calendario de adviento de C# en español.
Los invito a revisar los otros artículos de la iniciativa.

Oops!:

Son inevitables cuando trabajamos con hardware y librerías externas...

No verás alertas ni problemas de compilación, sólo programas rotos

Comentarios

Entradas más populares de este blog

Ampliar el sistema de archivos de una máquina virtual Ubuntu

C#: Registrar las dependencias de un módulo usando extensiones

Adviento C# 2020: Diario de ASP.NET