From 906e390786bf4060a33a95739673de99adde634e Mon Sep 17 00:00:00 2001 From: eleccoder <9162301+eleccoder@users.noreply.github.com> Date: Sun, 1 Aug 2021 17:18:36 +0200 Subject: [PATCH] Split into lib + example application --- CMakeLists.txt | 62 +++++++++++++++++------- README.md | 23 +++++---- include/aprs_pico.h | 44 +++++++++++++++++ src/aprs_pico.c | 105 ++++++++++++++-------------------------- src/aprs_pico_example.c | 48 ++++++++++++++++++ 5 files changed, 186 insertions(+), 96 deletions(-) create mode 100644 include/aprs_pico.h create mode 100644 src/aprs_pico_example.c diff --git a/CMakeLists.txt b/CMakeLists.txt index e238af9..06bfaf9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ set(CMAKE_CXX_STANDARD 17) pico_sdk_init() -# Setup of eleccoder's 'ax25-aprs-lib' +# (1) Setup of eleccoder's 'ax25-aprs-lib' include(FetchContent) FetchContent_Declare(ax25_aprs_lib @@ -24,30 +24,56 @@ FetchContent_MakeAvailable(ax25_aprs_lib) add_library(ax25_aprs_lib::ax25beacon ALIAS ax25beacon) -# Setup of the application +# (2) Configure the library build -set(EXE_NAME aprs_pico) +set(TARGET_LIB_NAME aprs_pico) -add_executable(${EXE_NAME} - src/aprs_pico.c +add_library(${TARGET_LIB_NAME} + src/aprs_pico.c) + +set_target_properties(${TARGET_LIB_NAME} + PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" ) - -target_include_directories(${EXE_NAME} PRIVATE + +target_include_directories(${TARGET_LIB_NAME} PUBLIC include ) -#target_compile_options(${EXE_NAME} PRIVATE "-Wall") +# Enable on demand +#target_compile_options(${TARGET_LIB_NAME} PRIVATE "-Wundef;-Wall") -# Set the console interface -pico_enable_stdio_usb(aprs_pico 1) # USB -# pico_enable_stdio_uart(aprs_pico 1) # UART - -# create map/bin/hex file etc. -pico_add_extra_outputs(${EXE_NAME}) - -target_link_libraries(${EXE_NAME} - pico_stdlib - pico_audio_pwm +# This properly resolves the include dirs of the dependent libraries +target_link_libraries(${TARGET_LIB_NAME} ax25_aprs_lib::ax25beacon + pico_audio_pwm + pico_stdlib +) + + +# (3) Configure the example application build + +set(TARGET_EXAMPLE_EXE_NAME aprs_pico_example) + +add_executable(${TARGET_EXAMPLE_EXE_NAME} + src/aprs_pico_example.c +) + +target_include_directories(${TARGET_EXAMPLE_EXE_NAME} PRIVATE + include +) + +# Set the console interface +pico_enable_stdio_usb(${TARGET_EXAMPLE_EXE_NAME} 1) # USB +#pico_enable_stdio_uart(${TARGET_EXAMPLE_EXE_NAME} 1) # UART + +# create map/bin/hex file etc. +pico_add_extra_outputs(${TARGET_EXAMPLE_EXE_NAME}) + +target_link_libraries(${TARGET_EXAMPLE_EXE_NAME} + ${TARGET_LIB_NAME} + ax25_aprs_lib::ax25beacon + pico_audio_pwm + pico_stdlib ) diff --git a/README.md b/README.md index 9afa3fe..ddeda20 100644 --- a/README.md +++ b/README.md @@ -6,21 +6,25 @@ An analog line-out audio signal will be produced at GPIO-pin 'GP0'. You can obse Basically, this is the data/signal flow: ``` -APRS (text msg + geo-coordinates + meta-data) -> AX.25 -> PCM -> PWM -> Low-Pass filtering -> AFSK signal +APRS (text msg + geo-coordinates + meta-data) -> AX.25 -> PCM -> PWM -> Band-Pass filtering -> AFSK audio signal ``` +Both a static library `libaprs_pico.a` and an example application will be generated by the build. + ## Preliminaries Your host platform is assumed to be LINUX. If you have already installed the Pico-SDK, set the `PICO_SDK_PATH` environment variable accordingly to avoid installing the SDK twice. + ## Hardware We just need a simple band-pass filter to extract the AFSK-signal from the PWM signal: ![band-pass filter](https://github.com/eleccoder/raspi-pico-aprs-tnc/blob/main/doc/schematic/band_pass_filter.png) -## Build the application + +## Build the library and the example application ``` (cd into the cloned dir) @@ -28,28 +32,27 @@ cmake -S . -B build cmake --build build ``` -## Run the application +`build/lib/libaprs_pico.a` and `build/aprs_pico_example.xxx` will be created. + +## Run the example application ``` cd build -(flash 'aprs_pico.uf2' or 'aprs_pico.elf' to the Pico board as usual) +(flash 'aprs_pico_example.uf2' or 'aprs_pico_example.elf' to the Pico board as usual) ``` The GPIO-pin 'GP0' is the line-out for the analog AFSK-signal. You can observe it by using a scope, listen to it by using an audio amp, or connect it to any RF transceiver to send it on the air (ham radio license required). ![AFSK scope screenshot](https://github.com/eleccoder/raspi-pico-aprs-tnc/blob/main/doc/img/afsk_scope.png "Scope screenshot of an AFSK output signal") -## Modify the application - -To send an APRS message of your choice, you have to modify the *main()* function in `src/aprs_pico.c`. ## TODO (Aug 2021) -- [x] Thorough evaluation +- [x] Thorough evaluation, in general - [x] Send the APRS message on the console (USB or UART) rather than hard-coding -- [x] Code documentation - [x] Show how to connect to a Baofeng HT -- [x] PTT control for HTs +- [x] PTT control for RF tranceivers + ## Acknowledgements diff --git a/include/aprs_pico.h b/include/aprs_pico.h new file mode 100644 index 0000000..a6169b1 --- /dev/null +++ b/include/aprs_pico.h @@ -0,0 +1,44 @@ +/* +* Project 'raspi-pico-aprs-tnc' +* Copyright (C) 2021 Thomas Glau, DL3TG +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. + +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. + +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +#ifndef APRS_PICO_H +#define APRS_PICO_H + +#include +#include + +// WARNING: ATTOW, the pico audio PWM lib worked only @ 22050 Hz sampling frequency and 48 MHz system clock +// This is documented here: https://github.com/raspberrypi/pico-extras +#define APRS_PICO__PICO_EXTRA_AUDIO_PWM_LIB_FIXED_SAMPLE_FREQ_IN_HZ (22050) + + +void sendAPRS(const char* call_sign_src, + const char* call_sign_dst, + const char* aprs_path_1, + const char* aprs_path_2, + const char* aprs_message, + const double latitude_in_deg, + const double longitude_in_deg, + const double altitude_in_m, + const uint8_t volume, + const bool is_loop); + + +void send1kHz(unsigned int sample_freq_in_hz, uint8_t volume); + +#endif // APRS_PICO_H diff --git a/src/aprs_pico.c b/src/aprs_pico.c index 63c9f54..c6d9abd 100644 --- a/src/aprs_pico.c +++ b/src/aprs_pico.c @@ -16,7 +16,8 @@ * along with this program. If not, see . */ -#include +#include + #include #include @@ -26,21 +27,16 @@ #include -// WARNING: ATTOW, the pico audio PWM lib worked only @ 22050 Hz sampling frequency and 48 MHz system clock -// This is documented here: https://github.com/raspberrypi/pico-extras -#define PICO_EXTRA_AUDIO_PWM_LIB_FIXED_SAMPLE_FREQ_IN_HZ (22050) -#define SINE_WAVE_TEST (0) // For test & debug - - typedef struct AudioCallBackUserData { - uint aprs_sample_freq_in_hz; - bool is_loop; - uint8_t volume; + unsigned int aprs_sample_freq_in_hz; + bool is_loop; + uint8_t volume; + } AudioCallBackUserData_t; -audio_buffer_pool_t* init_audio(uint sample_freq_in_hz, uint16_t audio_buffer_format) +static audio_buffer_pool_t* init_audio(unsigned int sample_freq_in_hz, uint16_t audio_buffer_format) { const int NUM_AUDIO_BUFFERS = 3; const int SAMPLES_PER_BUFFER = 256; @@ -68,12 +64,12 @@ audio_buffer_pool_t* init_audio(uint sample_freq_in_hz, uint16_t audio_buffer_fo } -static void init(uint sample_freq_in_hz) +static void init(unsigned int sample_freq_in_hz) { // WARNING: ATTOW, the pico audio PWM lib worked only @ 22050 Hz sampling frequency and 48 MHz system clock // This is documented here: https://github.com/raspberrypi/pico-extras - if (sample_freq_in_hz == PICO_EXTRA_AUDIO_PWM_LIB_FIXED_SAMPLE_FREQ_IN_HZ) + if (sample_freq_in_hz == APRS_PICO__PICO_EXTRA_AUDIO_PWM_LIB_FIXED_SAMPLE_FREQ_IN_HZ) { // This is the safe case, see the comment above set_sys_clock_48mhz(); @@ -83,7 +79,7 @@ static void init(uint sample_freq_in_hz) // Compensate a non-'PICO_EXTRA_AUDIO_PWM_LIB_FIXED_SAMPLE_FREQ_IN_HZ' sampling frequency // by a adapting the system clock accordingly - float sys_clock_in_mhz = 48.0f * (float)sample_freq_in_hz / (float)PICO_EXTRA_AUDIO_PWM_LIB_FIXED_SAMPLE_FREQ_IN_HZ; + float sys_clock_in_mhz = 48.0f * (float)sample_freq_in_hz / (float)APRS_PICO__PICO_EXTRA_AUDIO_PWM_LIB_FIXED_SAMPLE_FREQ_IN_HZ; // Round to full Mhz to increase the chance that 'set_sys_clock_khz()' can exactly realize this frequency sys_clock_in_mhz = round(sys_clock_in_mhz); @@ -96,9 +92,9 @@ static void init(uint sample_freq_in_hz) static void fill_audio_buffer(audio_buffer_pool_t* audio_pool, const int16_t* pcm_data, - uint num_samples, uint8_t volume, bool is_loop_forever) + unsigned int num_samples, uint8_t volume, bool is_loop_forever) { - uint pos = 0u; + unsigned int pos = 0u; bool is_keep_going = true; while (is_keep_going) @@ -106,7 +102,7 @@ static void fill_audio_buffer(audio_buffer_pool_t* audio_pool, const int16_t* pc audio_buffer_t* buffer = take_audio_buffer(audio_pool, true); int16_t* samples = (int16_t*)buffer->buffer->bytes; - for (uint i = 0u; i < buffer->max_sample_count; i++) + for (unsigned int i = 0u; i < buffer->max_sample_count; i++) { samples[i] = (volume * pcm_data[pos]) >> 8u; pos++; @@ -131,13 +127,23 @@ static void fill_audio_buffer(audio_buffer_pool_t* audio_pool, const int16_t* pc } +static void sendAPRS_audioCallback(void* callback_user_data, int16_t* pcm_data, size_t num_samples) +{ + const AudioCallBackUserData_t user_data = *((const AudioCallBackUserData_t*)callback_user_data); + + audio_buffer_pool_t* audio_pool = init_audio(user_data.aprs_sample_freq_in_hz, AUDIO_BUFFER_FORMAT_PCM_S16); + + fill_audio_buffer(audio_pool, pcm_data, num_samples, user_data.volume, user_data.is_loop); +} + + // Test tone: 1 kHz sine wave -static void send1kHz(uint sample_freq_in_hz, uint8_t volume) +void send1kHz( unsigned int sample_freq_in_hz, uint8_t volume) { init(sample_freq_in_hz); - const uint tone_freq_in_hz = 1000u; - const uint num_samples = sample_freq_in_hz / tone_freq_in_hz; + const unsigned int TONE_FREQ_IN_HZ = 1000u; + const unsigned int num_samples = sample_freq_in_hz / TONE_FREQ_IN_HZ; int16_t* sine_wave_table = malloc(num_samples * sizeof(int16_t)); @@ -146,7 +152,7 @@ static void send1kHz(uint sample_freq_in_hz, uint8_t volume) panic("Out of memory: malloc() failed.\n"); } - for (uint i = 0u; i < num_samples; i++) + for (unsigned int i = 0u; i < num_samples; i++) { sine_wave_table[i] = (int16_t)(32767.0f * sinf(2.0f * (float)M_PI * (float)i / (float)num_samples)); } @@ -159,26 +165,16 @@ static void send1kHz(uint sample_freq_in_hz, uint8_t volume) } -static void sendAPRS_audioCallback(void* callback_user_data, int16_t* pcm_data, size_t num_samples) -{ - const AudioCallBackUserData_t user_data = *((const AudioCallBackUserData_t*)callback_user_data); - - audio_buffer_pool_t* audio_pool = init_audio(user_data.aprs_sample_freq_in_hz, AUDIO_BUFFER_FORMAT_PCM_S16); - - fill_audio_buffer(audio_pool, pcm_data, num_samples, user_data.volume, user_data.is_loop); -} - - -static void sendAPRS(const char* call_sign_src, - const char* call_sign_dst, - const char* aprs_path_1, - const char* aprs_path_2, - const char* aprs_message, - const double latitude_in_deg, - const double longitude_in_deg, - const double altitude_in_m, - const uint8_t volume, - const bool is_loop) +void sendAPRS(const char* call_sign_src, + const char* call_sign_dst, + const char* aprs_path_1, + const char* aprs_path_2, + const char* aprs_message, + const double latitude_in_deg, + const double longitude_in_deg, + const double altitude_in_m, + const uint8_t volume, + const bool is_loop) { static AudioCallBackUserData_t callback_user_data; @@ -200,30 +196,3 @@ static void sendAPRS(const char* call_sign_src, aprs_message, '/', 'O'); } - - -int main() -{ -#if (SINE_WAVE_TEST == 1) - - const uint8_t VOLUME = 128u; - send1kHz(PICO_EXTRA_AUDIO_PWM_LIB_FIXED_SAMPLE_FREQ_IN_HZ, VOLUME); - -#else // !SINE_WAVE_TEST - - // Send an APRS test message - sendAPRS("SRC", // Src call sign - "DST", // Dst call sign - "PATH1", - "PATH2", - "Test message", - 10.0, // Lat in deg - 20.0, // Long in deg - 100.0, // Alt in m - 128u, // Volume (0 ... 255) - false); // Loop forever - -#endif // SINE_WAVE_TEST, !SINE_WAVE_TEST - - return 0; -} diff --git a/src/aprs_pico_example.c b/src/aprs_pico_example.c new file mode 100644 index 0000000..7999549 --- /dev/null +++ b/src/aprs_pico_example.c @@ -0,0 +1,48 @@ +/* +* Project 'raspi-pico-aprs-tnc' +* Copyright (C) 2021 Thomas Glau, DL3TG +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. + +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. + +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +#include + +#define SINE_WAVE_TEST (0) // For test & debug + + +int main() +{ +#if (SINE_WAVE_TEST == 1) + + const uint8_t VOLUME = 128u; + send1kHz(APRS_PICO__PICO_EXTRA_AUDIO_PWM_LIB_FIXED_SAMPLE_FREQ_IN_HZ, VOLUME); + +#else // !SINE_WAVE_TEST + + // Send an APRS test message + sendAPRS("SRC", // Src call sign + "DST", // Dst call sign + "PATH1", + "PATH2", + "Test message", + 10.0, // Lat in deg + 20.0, // Long in deg + 100.0, // Alt in m + 128u, // Volume (0 ... 255) + false); // Loop forever + +#endif // SINE_WAVE_TEST, !SINE_WAVE_TEST + + return 0; +}