Compare commits

..

6 commits

Author SHA1 Message Date
Thomas Kolb a520b3232d sdr/sx: implement timed transmission
Some checks failed
/ build-hamnet70 (push) Failing after 16s
/ build-doc (push) Successful in 20s
/ deploy-doc (push) Has been skipped
2024-10-13 18:43:17 +01:00
Thomas Kolb 5aa9eeb18b test_sx: test stream activation just before TX 2024-10-13 18:40:52 +01:00
Thomas Kolb e4a6e6b300 Add direct test for sxceiver TX operation
Some checks failed
/ build-hamnet70 (push) Failing after 17s
/ build-doc (push) Successful in 22s
/ deploy-doc (push) Has been skipped
The test generates a continuous carrier signal, offset by 20 kHz from
the center frequency. The generated carrier must be clean when
received by another device.
2024-10-06 14:38:24 +01:00
Thomas Kolb 79d1178150 sdr: use the logger module 2024-10-04 20:49:11 +01:00
Thomas Kolb ec8770f399 sxceiver: make it run without crashing
Some checks failed
/ build-hamnet70 (push) Failing after 13s
/ build-doc (push) Successful in 16s
/ deploy-doc (push) Has been skipped
Had to fix multiple segmentation faults due to required by-reference
outputs that were set to NULL pointers.
2024-10-01 21:10:14 +01:00
Thomas Kolb eb03e6a661 Add basic sxceiver sdr module
All checks were successful
/ build-hamnet70 (push) Successful in 26s
/ build-doc (push) Successful in 17s
/ deploy-doc (push) Has been skipped
2024-10-01 20:51:39 +02:00
8 changed files with 603 additions and 161 deletions

View file

@ -3,7 +3,7 @@ jobs:
build-doc: build-doc:
runs-on: docker runs-on: docker
container: container:
image: git.tkolb.de/amateurfunk/hamnet70/asciidoctor:1.7 image: git.tkolb.de/amateurfunk/hamnet70/asciidoctor:1.6
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- run: cd doc && make - run: cd doc && make
@ -16,7 +16,7 @@ jobs:
runs-on: docker runs-on: docker
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
container: container:
image: git.tkolb.de/amateurfunk/hamnet70/asciidoctor:1.7 image: git.tkolb.de/amateurfunk/hamnet70/asciidoctor:1.6
steps: steps:
- run: mkdir ~/.ssh && echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_ed25519 && chmod 0600 ~/.ssh/id_ed25519 && echo "${{ secrets.SSH_KNOWN_HOST }}" > ~/.ssh/known_hosts - run: mkdir ~/.ssh && echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_ed25519 && chmod 0600 ~/.ssh/id_ed25519 && echo "${{ secrets.SSH_KNOWN_HOST }}" > ~/.ssh/known_hosts
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3

View file

@ -10,7 +10,7 @@ Thomas Kolb DL5TKL
== Introduction == Introduction
https://git.tkolb.de/amateurfunk/hamnet70[Hamnet70] intends to provide a network system in the 70 cm amateur radio band that can transfer Internet Protocol (IP) packets at a high speed. Hamnet70 intends to provide a network system in the 70 cm amateur radio band that can transfer Internet Protocol (IP) packets at a high speed.
The protocols defined here are inspired by the Packet Radio network and Bluetooth Low Energy. The protocols defined here are inspired by the Packet Radio network and Bluetooth Low Energy.
The protocols are designed primarily for centralized infrastructure as they were common in the Packet Radio network. The protocols are designed primarily for centralized infrastructure as they were common in the Packet Radio network.
@ -238,49 +238,6 @@ Due to the long possible bursts, this system can achieve high throughput if ther
=== Frame Definitions === Frame Definitions
==== Data Frame
Data Frames carry all higher-layer data in a connection.
Each Data Frame transfers a single layer-3 packet.
The layer 2 header of Data Frames is filled as follows:
- Message Type: `000` (Data Frame)
- TX Request: `1` if this is the last packet in the burst, `0` otherwise
- Source Address: the transmitters HAM-64 address
- Destination Address: the target stations HAM-64 address
- TX sequence number: as required by Go-Back-N
- RX sequence number: as required by Go-Back-N
To identify how the encoded packet should be handled, the layer 3 protocol is encoded in the first byte of the layer 2 payload.
The full layer 2 payload therefore is composed as follows:
- Layer 3 protocol ID (1 Byte)
- Layer 3 packet data (variable length)
So far, the following protocols are defined and supported:
[cols="2,1,1", options="header"]
.Layer 3 protocol identifiers. EtherType is given as reference.
|===
| Protocol | Hamnet70 ID | EtherType
| IPv6
| `0x00`
| `0x86DD`
| IPv4
| `0x10`
| `0x0800`
| _undefined/auto_
| `0xFF`
| -
3+|_All other values are reserved._
|===
==== Empty Frame ==== Empty Frame
The Empty Frame does not contain any data and therefore only consists of the header and the CRC. The Empty Frame does not contain any data and therefore only consists of the header and the CRC.
@ -448,104 +405,15 @@ The following Configuration Types are defined:
If the client receives an unknown Configuration Type the corresponding block shall be skipped. If the client receives an unknown Configuration Type the corresponding block shall be skipped.
The remaining blocks shall be parsed as usual. The remaining blocks shall be parsed as usual.
===== Connection Reset === Ideas
A Connection Reset is set by the digipeater if it receives unexpected packets from stations that are not connected. To be defined:
The layer 2 header is filled as follows: - connection establishment procedure (request, response)
- how do clients get an IP(v6) address? -> should be derived from the call sign
- Message Type: `001` (Connection Management) - when are new clients allowed to connect? -> base station calls for any new stations in regular intervals
- TX Request: `1` if this is the last packet in the burst, `0` otherwise - handling of packets from unknown clients that are not connection requests
- Source Address: the digipeaters HAM-64 address - signal quality handling
- Destination Address: the new clients HAM-64 address
- TX sequence number: reserved, always 0
- RX sequence number: reserved, always 0
The message contains exactly 1 data byte: `0x03` to indicate that this is a Connection Reset packet.
A Connection Request is sent in a regular burst and can be in any position.
This message means that the digipeater does not have any information about a connection with the addressed client.
Therefore, when a client receives a Connection Reset, it shall drop its complete connection state and start a new connection procedure if desired.
===== Disconnect Request
A Disconnect Request is set by the digipeater when it wants to orderly shut down a client connection.
It is the last packet that the digipeater sends in a connection.
The layer 2 header is filled as follows:
- Message Type: `001` (Connection Management)
- TX Request: `1` if this is the last packet in the burst, `0` otherwise
- Source Address: the digipeaters HAM-64 address
- Destination Address: the clients HAM-64 address
- TX sequence number: as required by Go-Back-N
- RX sequence number: as required by Go-Back-N
The message contains exactly 1 data byte: `0x04` to indicate that this is a Disconnect Request packet.
A Disconnect Request is sent in a regular burst and can be in any position.
The sequence numbers are sent and handled the same way as in regular connection packets.
When a client receives a Disconnect Request, it may transmit the remainder of its current queue, but must not queue new data packets.
After the last packet is transmitted, the client shall send a Disconnect packet, which confirms the end of the connection.
The digipeater must keep the connection state in memory until either the Disconnect packet is received from the client or the connection times out.
===== Disconnect
A Disconnect packet is sent by the client to terminate the connection.
It is the last packet in a connection and is not confirmed by the digipeater.
The layer 2 header is filled as follows:
- Message Type: `001` (Connection Management)
- TX Request: `0`
- Source Address: the clients HAM-64 address
- Destination Address: the digipeaters HAM-64 address
- TX sequence number: as required by Go-Back-N
- RX sequence number: as required by Go-Back-N
The message contains exactly 1 data byte: `0x05` to indicate that this is a Disconnect packet.
A Disconnect is sent always as the final packet of a burst.
The sequence numbers are sent and handled the same way as in regular connection packets.
When the digipeater receives a disconnect packet in the regular packet flow (i.e. no previous packets are lost), it will immediately drop the connection state and not call this client again.
Therefore, if the client wants to ensure that all previous packets are transmitted, it must wait until the digipeater confirms that by sending the corresponding RX sequence number before sending the Disconnect packet.
==== Connectionless Frame
Connectionless Frames are used to transfer packets between unconnected notes.
They can be used to implement custom protocols (similar to APRS, which is implemented on top of AX.25).
The layer 2 header of Connectionless Frames is filled as follows:
- Message Type: `100` (Connectionless Frame)
- TX Request: `1` if this is the last packet in the burst, `0` otherwise
- Source Address: the transmitters HAM-64 address
- Destination Address: the target stations HAM-64 address
- TX sequence number: user-defined
- RX sequence number: user-defined
The sequence numbers can be used in any way that is useful for the custom protocol.
It is required that the first byte of each Connectionless Frame identify the protocol being used.
Protocol numbers are centrally assigned and are listed below.
Some protocol numbers are reserved for experimentation and development and can be self-assigned temporarily.
[cols="1,3", options="header"]
.Connectionless Frame protocol IDs
|===
|Protocol IDs
|Description
|`0x00 .. 0xF7`
|_reserved_
|`0xF8 .. 0xFF`
|Available for experimentation and developmentfootnote:[If you are developing a new protocol, you can freely pick a number from this range. Please check which temporary IDs are already used around your location and pick a free one. When your protocol reaches a sufficiently stable state, please request an official ID assignment].
|===
=== Message Sequence Charts === Message Sequence Charts

View file

@ -2,7 +2,7 @@ cmake_minimum_required (VERSION 3.20)
project (hamnet70 VERSION 0.1 LANGUAGES C) project (hamnet70 VERSION 0.1 LANGUAGES C)
set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD 99)
set(CMAKE_C_FLAGS "-Wall -pedantic -Wextra -DDEBUG_LIQUID") set(CMAKE_C_FLAGS "-Wall -pedantic -Wextra -DDEBUG_LIQUID -fsanitize=address")
include_directories(src) include_directories(src)
@ -55,8 +55,8 @@ set(sources
src/layer2/ham64.h src/layer2/ham64.h
src/layer2/connection.c src/layer2/connection.c
src/layer2/connection.h src/layer2/connection.h
src/sdr/sdr.c src/sdr/sdr_sxceiver.c
src/sdr/sdr.h src/sdr/sdr_sxceiver.h
) )
include_directories( include_directories(
@ -75,7 +75,7 @@ target_link_libraries(
rt rt
fftw3f fftw3f
fec fec
hackrf SoapySDR
) )
add_subdirectory(test) add_subdirectory(test)

View file

@ -35,7 +35,7 @@
#include "layer2/tundev.h" #include "layer2/tundev.h"
#include "sdr/sdr.h" #include "sdr/sdr_sxceiver.h"
#include "config.h" #include "config.h"
@ -50,6 +50,8 @@
static int m_tunfd = -1; static int m_tunfd = -1;
static bool m_running = true; static bool m_running = true;
static double m_tstart = 0.0;
static double next_tx_switch_time = 0.0; static double next_tx_switch_time = 0.0;
static rx_stats_t m_rx_stats; static rx_stats_t m_rx_stats;
@ -149,14 +151,8 @@ void cb_rx(rx_evt_t evt, const struct layer1_rx_s *rx, uint8_t *packet_data, siz
static result_t transmit(sdr_ctx_t *sdr, const float complex *samples, size_t len) static result_t transmit(sdr_ctx_t *sdr, const float complex *samples, size_t len)
{ {
size_t to_transmit_rf = len * SDR_OVERSAMPLING; int64_t tx_time_ns = (int64_t)((get_hires_time() - m_tstart + 0.1) * 1e9);
float complex *rf_samples = malloc(sizeof(*rf_samples) * to_transmit_rf); result_t result = sdr_transmit(sdr, samples, len, tx_time_ns, 100000);
RESULT_CHECK(sdr_baseband_to_rf(sdr, samples, len, rf_samples, &to_transmit_rf));
result_t result = sdr_transmit(sdr, rf_samples, to_transmit_rf, 100000);
free(rf_samples);
fprintf(stderr, "t"); fprintf(stderr, "t");
return result; return result;
@ -230,10 +226,11 @@ int main(int argc, char **argv)
// start in TX mode to work around SoapyHackRF not setting the correct frequency. // start in TX mode to work around SoapyHackRF not setting the correct frequency.
RESULT_CHECK(sdr_start_tx(&sdr, 1)); RESULT_CHECK(sdr_start_tx(&sdr, 1));
m_tstart = get_hires_time();
unsigned rx_retries = 0; unsigned rx_retries = 0;
double old = get_hires_time(); double old = m_tstart;
size_t total_samples = 0; size_t total_samples = 0;
double next_stats_print_time = old + 0.5; double next_stats_print_time = old + 0.5;
@ -256,7 +253,7 @@ int main(int argc, char **argv)
// there is a packet to be (re)transmitted. // there is a packet to be (re)transmitted.
// check free buffer space (50 ms required corresponding to 5000 baseband symbols) // check free buffer space (50 ms required corresponding to 5000 baseband symbols)
size_t buffer_free_space_samples = sdr_get_tx_buffer_free_space(&sdr); size_t buffer_free_space_samples = 1000000; // FIXME! sdr_get_tx_buffer_free_space(&sdr);
LOG(LVL_DEBUG, "TX buffer free: %zu", buffer_free_space_samples); LOG(LVL_DEBUG, "TX buffer free: %zu", buffer_free_space_samples);
@ -318,7 +315,6 @@ int main(int argc, char **argv)
on_air = true; on_air = true;
} else if(on_air) { // TX on, but no more bursts to send } else if(on_air) { // TX on, but no more bursts to send
LOG(LVL_INFO, "TX -> RX"); LOG(LVL_INFO, "TX -> RX");
RESULT_CHECK(sdr_flush_tx_buffer(&sdr));
RESULT_CHECK(layer1_rx_reset(&rx)); RESULT_CHECK(layer1_rx_reset(&rx));
RESULT_CHECK(sdr_stop_tx(&sdr)); RESULT_CHECK(sdr_stop_tx(&sdr));
@ -343,7 +339,7 @@ int main(int argc, char **argv)
size_t n_rf_samples = CHUNKSIZE_RF; size_t n_rf_samples = CHUNKSIZE_RF;
size_t n_bb_samples = CHUNKSIZE_BB; size_t n_bb_samples = CHUNKSIZE_BB;
if(sdr_receive(&sdr, rf_samples, &n_rf_samples, 100000, SDR_OVERSAMPLING) != OK) { if(sdr_receive(&sdr, bb_samples, &n_bb_samples, 100000) != OK) {
rx_retries++; rx_retries++;
LOG(LVL_INFO, "sdr_receive() failed %d times.", rx_retries); LOG(LVL_INFO, "sdr_receive() failed %d times.", rx_retries);
if(rx_retries >= 3) { if(rx_retries >= 3) {
@ -382,8 +378,6 @@ int main(int argc, char **argv)
fprintf(stderr, "r"); fprintf(stderr, "r");
RESULT_CHECK(sdr_rf_to_baseband(&sdr, rf_samples, n_rf_samples, bb_samples, &n_bb_samples));
RESULT_CHECK(layer1_rx_process(&rx, bb_samples, n_bb_samples)); RESULT_CHECK(layer1_rx_process(&rx, bb_samples, n_bb_samples));
} else { } else {
rx_retries = 0; rx_retries = 0;

330
impl/src/sdr/sdr_sxceiver.c Normal file
View file

@ -0,0 +1,330 @@
#include <SoapySDR/Device.h>
#include <SoapySDR/Formats.h>
#include <SoapySDR/Logger.h>
#include <liquid/liquid.h>
#include <stdio.h> //printf
#include <stdlib.h> //free
#include <complex.h>
#include <math.h>
#include <string.h>
#include "logger.h"
#include "config.h"
#include "utils.h"
#include "results.h"
#include "sdr_sxceiver.h"
void soapy_log_handler(const SoapySDRLogLevel logLevel, const char *message)
{
int level;
switch(logLevel) {
case SOAPY_SDR_CRITICAL:
case SOAPY_SDR_FATAL:
level = LVL_FATAL;
break;
case SOAPY_SDR_ERROR:
level = LVL_ERR;
break;
case SOAPY_SDR_WARNING:
level = LVL_WARN;
break;
case SOAPY_SDR_INFO:
level = LVL_INFO;
break;
case SOAPY_SDR_DEBUG:
level = LVL_DEBUG;
break;
default:
level = LVL_DUMP;
break;
}
LOG(level, "soapy [%d]: %s", logLevel, message);
}
static void close_streams(sdr_ctx_t *ctx)
{
if(ctx->rx_stream) {
SoapySDRDevice_deactivateStream(ctx->sdr, ctx->rx_stream, 0, 0);
SoapySDRDevice_closeStream(ctx->sdr, ctx->rx_stream);
}
if(ctx->tx_stream) {
SoapySDRDevice_deactivateStream(ctx->sdr, ctx->tx_stream, 0, 0);
SoapySDRDevice_closeStream(ctx->sdr, ctx->tx_stream);
}
}
result_t sdr_init(sdr_ctx_t *ctx)
{
size_t length;
ctx->sdr = NULL;
ctx->tx_stream = NULL;
ctx->rx_stream = NULL;
ctx->tx_nco = nco_crcf_create(LIQUID_NCO);
ctx->rx_nco = nco_crcf_create(LIQUID_NCO);
nco_crcf_set_frequency(ctx->tx_nco, 2 * M_PI * SDR_TX_IF_SHIFT / SDR_TX_SAMPLING_RATE);
nco_crcf_set_frequency(ctx->rx_nco, 2 * M_PI * SDR_RX_IF_SHIFT / SDR_RX_SAMPLING_RATE);
// set up logging
SoapySDR_registerLogHandler(soapy_log_handler);
//SoapySDR_setLogLevel(SOAPY_SDR_DEBUG);
//enumerate devices
SoapySDRKwargs *results = SoapySDRDevice_enumerate(NULL, &length);
for (size_t i = 0; i < length; i++)
{
LOG(LVL_INFO, "Found device #%d:", (int)i);
for (size_t j = 0; j < results[i].size; j++)
{
LOG(LVL_INFO, "- %s=%s", results[i].keys[j], results[i].vals[j]);
}
LOG(LVL_ERR, "");
}
SoapySDRKwargsList_clear(results, length);
//create device instance
//args can be user defined or from the enumeration result
SoapySDRKwargs args;
memset(&args, 0, sizeof(args));
SoapySDRKwargs_set(&args, "driver", "sx");
ctx->sdr = SoapySDRDevice_make(&args);
SoapySDRKwargs_clear(&args);
if (ctx->sdr == NULL)
{
LOG(LVL_ERR, "SoapySDRDevice_make fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
//query device info
SoapySDRRange *ranges;
char** names = SoapySDRDevice_listAntennas(ctx->sdr, SOAPY_SDR_RX, 0, &length);
LOG(LVL_INFO, "Rx antennas: ");
for (size_t i = 0; i < length; i++) LOG(LVL_INFO, "%s, ", names[i]);
LOG(LVL_INFO, "");
SoapySDRStrings_clear(&names, length);
names = SoapySDRDevice_listGains(ctx->sdr, SOAPY_SDR_RX, 0, &length);
LOG(LVL_INFO, "Rx gains: ");
for (size_t i = 0; i < length; i++) {
SoapySDRRange range = SoapySDRDevice_getGainRange(ctx->sdr, SOAPY_SDR_RX, i);
LOG(LVL_INFO, "%s [%.3lf .. %.3lf / %.3lf], ", names[i], range.minimum, range.maximum, range.step);
}
LOG(LVL_INFO, "");
SoapySDRStrings_clear(&names, length);
ranges = SoapySDRDevice_getFrequencyRange(ctx->sdr, SOAPY_SDR_RX, 0, &length);
LOG(LVL_INFO, "Rx freq ranges: ");
for (size_t i = 0; i < length; i++) LOG(LVL_INFO, "[%g Hz -> %g Hz], ", ranges[i].minimum, ranges[i].maximum);
LOG(LVL_INFO, "");
free(ranges);
names = SoapySDRDevice_listAntennas(ctx->sdr, SOAPY_SDR_TX, 0, &length);
LOG(LVL_INFO, "Tx antennas: ");
for (size_t i = 0; i < length; i++) LOG(LVL_INFO, "%s, ", names[i]);
LOG(LVL_INFO, "");
SoapySDRStrings_clear(&names, length);
names = SoapySDRDevice_listGains(ctx->sdr, SOAPY_SDR_TX, 0, &length);
LOG(LVL_INFO, "Tx gains: ");
for (size_t i = 0; i < length; i++) {
SoapySDRRange range = SoapySDRDevice_getGainRange(ctx->sdr, SOAPY_SDR_TX, i);
LOG(LVL_INFO, "%s [%.3lf .. %.3lf / %.3lf], ", names[i], range.minimum, range.maximum, range.step);
}
LOG(LVL_INFO, "");
SoapySDRStrings_clear(&names, length);
ranges = SoapySDRDevice_getFrequencyRange(ctx->sdr, SOAPY_SDR_TX, 0, &length);
LOG(LVL_INFO, "Tx freq ranges: ");
for (size_t i = 0; i < length; i++) LOG(LVL_INFO, "[%g Hz -> %g Hz], ", ranges[i].minimum, ranges[i].maximum);
LOG(LVL_INFO, "");
free(ranges);
//setup streams
ctx->rx_stream = SoapySDRDevice_setupStream(ctx->sdr, SOAPY_SDR_RX, SOAPY_SDR_CF32, NULL, 0, NULL);
if(ctx->rx_stream == NULL) {
LOG(LVL_ERR, "setupStream fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
ctx->tx_stream = SoapySDRDevice_setupStream(ctx->sdr, SOAPY_SDR_TX, SOAPY_SDR_CF32, NULL, 0, NULL);
if(ctx->tx_stream == NULL) {
LOG(LVL_ERR, "setupStream fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
//apply settings
if (SoapySDRDevice_setSampleRate(ctx->sdr, SOAPY_SDR_RX, 0, SDR_RX_SAMPLING_RATE) != 0) {
LOG(LVL_ERR, "setSampleRate fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
if (SoapySDRDevice_setFrequency(ctx->sdr, SOAPY_SDR_RX, 0, SDR_RX_FREQ - SDR_RX_IF_SHIFT, NULL) != 0) {
LOG(LVL_ERR, "setFrequency fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
if (SoapySDRDevice_setSampleRate(ctx->sdr, SOAPY_SDR_TX, 0, SDR_TX_SAMPLING_RATE) != 0) {
LOG(LVL_ERR, "setSampleRate fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
if (SoapySDRDevice_setFrequency(ctx->sdr, SOAPY_SDR_TX, 0, SDR_TX_FREQ, NULL) != 0) {
LOG(LVL_ERR, "setFrequency fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
#if 0
// PA always on
if (SoapySDRDevice_writeSetting(ctx->sdr, "PA", "ON") != 0) {
LOG(LVL_ERR, "writeSetting fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
#endif
// set up and start RX
// set gains
if(SoapySDRDevice_setGainElement(ctx->sdr, SOAPY_SDR_RX, 0, "PGA", SDR_GAIN_RX_PGA) != 0) {
LOG(LVL_ERR, "setGainElement fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
if(SoapySDRDevice_setGainElement(ctx->sdr, SOAPY_SDR_RX, 0, "LNA", SDR_GAIN_RX_LNA) != 0) {
LOG(LVL_ERR, "setGainElement fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
if(SoapySDRDevice_activateStream(ctx->sdr, ctx->rx_stream, 0, 0, 0) != 0) {
LOG(LVL_ERR, "activateStream fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
// set up and start TX
// set gain
/*if(SoapySDRDevice_setGainElement(ctx->sdr, SOAPY_SDR_TX, 0, "DAC", SDR_GAIN_TX_DAC) != 0) {
LOG(LVL_ERR, "setGainElement fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
if(SoapySDRDevice_setGainElement(ctx->sdr, SOAPY_SDR_TX, 0, "MIXER", SDR_GAIN_TX_MIXER) != 0) {
LOG(LVL_ERR, "setGainElement fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}*/
if(SoapySDRDevice_setGain(ctx->sdr, SOAPY_SDR_TX, 0, SDR_GAIN_TX) != 0) {
LOG(LVL_ERR, "setGain fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
if(SoapySDRDevice_activateStream(ctx->sdr, ctx->tx_stream, 0, 0, 0) != 0) {
LOG(LVL_ERR, "activateStream fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
return OK;
}
result_t sdr_destroy(sdr_ctx_t *ctx)
{
close_streams(ctx);
nco_crcf_destroy(ctx->rx_nco);
nco_crcf_destroy(ctx->tx_nco);
if(ctx->sdr) {
SoapySDRDevice_unmake(ctx->sdr);
}
return OK;
}
result_t sdr_start_rx(sdr_ctx_t *ctx)
{
return OK;
}
result_t sdr_start_tx(sdr_ctx_t *ctx, size_t burst_size)
{
return OK;
}
result_t sdr_stop_rx(sdr_ctx_t *ctx)
{
return OK;
}
result_t sdr_stop_tx(sdr_ctx_t *ctx)
{
return OK;
}
result_t sdr_transmit(sdr_ctx_t *ctx, const float complex *samples, size_t nsamples, int64_t time_ns, long timeout_us)
{
if(ctx->tx_stream == NULL) {
return ERR_INVALID_STATE;
}
int flags = 0;
if(time_ns != 0) {
flags |= SOAPY_SDR_HAS_TIME;
}
void *buffs[] = {(void*)(samples)};
int ret = SoapySDRDevice_writeStream(ctx->sdr, ctx->tx_stream, (const void* const*)buffs, nsamples, &flags, time_ns, timeout_us);
if(ret <= 0) {
LOG(LVL_ERR, "writeStream fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
} else if(ret != nsamples) {
LOG(LVL_WARN, "writeStream() did not transmit all samples: %i/%zu", ret, nsamples);
}
return OK;
}
result_t sdr_receive(sdr_ctx_t *ctx, float complex *samples, size_t *nsamples, long timeout_us)
{
if(ctx->rx_stream == NULL) {
return ERR_INVALID_STATE;
}
void *buffs[] = {(void*)samples};
long long timeNs;
int flags;
int ret = SoapySDRDevice_readStream(ctx->sdr, ctx->rx_stream, (void* const*)buffs, *nsamples, &flags, &timeNs, timeout_us);
if(ret <= 0) {
LOG(LVL_ERR, "readStream fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
*nsamples = ret;
return OK;
}

View file

@ -0,0 +1,34 @@
#ifndef SDR_SDR_H
#define SDR_SDR_H
#include <complex.h>
#include <SoapySDR/Device.h>
#include <liquid/liquid.h>
#include "results.h"
typedef struct {
SoapySDRDevice *sdr;
SoapySDRStream *rx_stream;
SoapySDRStream *tx_stream;
nco_crcf tx_nco;
nco_crcf rx_nco;
} sdr_ctx_t;
result_t sdr_init(sdr_ctx_t *ctx);
result_t sdr_destroy(sdr_ctx_t *ctx);
result_t sdr_start_rx(sdr_ctx_t *ctx);
result_t sdr_start_tx(sdr_ctx_t *ctx, size_t burst_size);
result_t sdr_stop_rx(sdr_ctx_t *ctx);
result_t sdr_stop_tx(sdr_ctx_t *ctx);
result_t sdr_transmit(sdr_ctx_t *ctx, const float complex *samples, size_t nsamples, int64_t time_ns, long timeout_us);
result_t sdr_receive(sdr_ctx_t *ctx, float complex *samples, size_t *nsamples, long timeout_us);
#endif // SDR_SDR_H

View file

@ -222,3 +222,24 @@ target_link_libraries(
test_interleaver test_interleaver
m m
) )
#------------------------------------
add_executable(
test_sx
../src/utils.c
../src/utils.h
../src/logger.c
../src/logger.h
test_sx.c
)
target_link_libraries(
test_sx
liquid
m
rt
fftw3f
fec
SoapySDR
)

195
impl/test/test_sx.c Normal file
View file

@ -0,0 +1,195 @@
#include <SoapySDR/Device.h>
#include <SoapySDR/Formats.h>
#include <SoapySDR/Logger.h>
#include <string.h>
#include <math.h>
#include "logger.h"
#include "config.h"
#include "results.h"
#include "utils.h"
void soapy_log_handler(const SoapySDRLogLevel logLevel, const char *message)
{
int level;
switch(logLevel) {
case SOAPY_SDR_CRITICAL:
case SOAPY_SDR_FATAL:
level = LVL_FATAL;
break;
case SOAPY_SDR_ERROR:
level = LVL_ERR;
break;
case SOAPY_SDR_WARNING:
level = LVL_WARN;
break;
case SOAPY_SDR_INFO:
level = LVL_INFO;
break;
case SOAPY_SDR_DEBUG:
level = LVL_DEBUG;
break;
default:
level = LVL_DUMP;
break;
}
LOG(level, "soapy [%d]: %s", logLevel, message);
}
typedef struct {
SoapySDRDevice *sdr;
SoapySDRStream *rx_stream;
SoapySDRStream *tx_stream;
} sdr_ctx_t;
int main(void)
{
sdr_ctx_t ctx;
logger_init();
// set up logging
SoapySDR_registerLogHandler(soapy_log_handler);
SoapySDR_setLogLevel(SOAPY_SDR_DEBUG);
SoapySDRKwargs args;
memset(&args, 0, sizeof(args));
SoapySDRKwargs_set(&args, "driver", "sx");
ctx.sdr = SoapySDRDevice_make(&args);
SoapySDRKwargs_clear(&args);
if (ctx.sdr == NULL)
{
LOG(LVL_ERR, "SoapySDRDevice_make fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
//setup streams
ctx.rx_stream = SoapySDRDevice_setupStream(ctx.sdr, SOAPY_SDR_RX, SOAPY_SDR_CF32, NULL, 0, NULL);
if(ctx.rx_stream == NULL) {
LOG(LVL_ERR, "setupStream fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
ctx.tx_stream = SoapySDRDevice_setupStream(ctx.sdr, SOAPY_SDR_TX, SOAPY_SDR_CF32, NULL, 0, NULL);
if(ctx.tx_stream == NULL) {
LOG(LVL_ERR, "setupStream fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
//apply settings
if (SoapySDRDevice_setSampleRate(ctx.sdr, SOAPY_SDR_RX, 0, SDR_RX_SAMPLING_RATE) != 0) {
LOG(LVL_ERR, "setSampleRate fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
if (SoapySDRDevice_setFrequency(ctx.sdr, SOAPY_SDR_RX, 0, SDR_RX_FREQ - SDR_RX_IF_SHIFT, NULL) != 0) {
LOG(LVL_ERR, "setFrequency fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
if (SoapySDRDevice_setSampleRate(ctx.sdr, SOAPY_SDR_TX, 0, SDR_TX_SAMPLING_RATE) != 0) {
LOG(LVL_ERR, "setSampleRate fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
if (SoapySDRDevice_setFrequency(ctx.sdr, SOAPY_SDR_TX, 0, SDR_TX_FREQ, NULL) != 0) {
LOG(LVL_ERR, "setFrequency fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
if(SoapySDRDevice_setGain(ctx.sdr, SOAPY_SDR_TX, 0, SDR_GAIN_TX) != 0) {
LOG(LVL_ERR, "setGain fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
#define BUFSIZE 300000
#if 0
if(SoapySDRDevice_activateStream(ctx.sdr, ctx.tx_stream, 0, 0, 0) != 0) {
LOG(LVL_ERR, "activateStream fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
#endif
float complex samples[BUFSIZE];
float twopi = 6.283185307f;
float offset_hz = 20e3;
float fm_osc_freq_hz = 1.0f;
float fm_osc_dev_hz = 5e3;
float fm_dphi = twopi * fm_osc_freq_hz / SDR_TX_SAMPLING_RATE;
float fm_phi = 0.0f;
float phi = 0.0f;
int64_t timeNs = 0;
while(true) {
for(size_t i = 0; i < BUFSIZE; i++) {
fm_phi += fm_dphi;
if(fm_phi > twopi) {
fm_phi -= twopi;
}
float fm_dev_hz = fm_osc_dev_hz * sinf(fm_phi);
float dphi = twopi * (offset_hz + fm_dev_hz) / SDR_TX_SAMPLING_RATE;
phi += dphi;
if(phi > twopi) {
phi -= twopi;
}
samples[i] = cexpf(I * phi);
}
timeNs += 1000000000LL + 1000000000LL * BUFSIZE / SDR_TX_SAMPLING_RATE;
if(SoapySDRDevice_activateStream(ctx.sdr, ctx.tx_stream, 0, 0, 0) != 0) {
LOG(LVL_ERR, "activateStream fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
double tstart = get_hires_time();
int flags = 0;
void *buffs[] = {(void*)samples};
size_t timeout_us = 100000;
int ret = SoapySDRDevice_writeStream(ctx.sdr, ctx.tx_stream, (const void* const*)buffs, BUFSIZE, &flags, timeNs, timeout_us);
if(ret <= 0) {
LOG(LVL_ERR, "writeStream fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
double tend_write = get_hires_time();
if(SoapySDRDevice_deactivateStream(ctx.sdr, ctx.tx_stream, 0, 0) != 0) {
LOG(LVL_ERR, "deactivateStream fail: %s", SoapySDRDevice_lastError());
return ERR_SOAPY;
}
double tend = get_hires_time();
LOG(LVL_INFO, "write duration: %.3f ms", (tend_write - tstart) * 1000);
LOG(LVL_INFO, "total duration: %.3f ms", (tend - tstart) * 1000);
fsleep(1);
}
return OK;
}