From fda9cef76a3dd9cf48288f254d3d0e638ad6550a Mon Sep 17 00:00:00 2001 From: Mikael Nousiainen Date: Wed, 9 Aug 2023 18:28:00 +0300 Subject: [PATCH] Implement simple GPS power saving. Read leap seconds from GPS data when available. Improve documentation. --- README.md | 70 ++++++++++++++++++++---------- src/codecs/horus/horus_packet_v1.c | 2 + src/codecs/horus/horus_packet_v2.c | 2 + src/config.h | 15 ++++++- src/drivers/ubxg6010/ubxg6010.c | 29 ++++++++++++- src/drivers/ubxg6010/ubxg6010.h | 2 + src/gps.h | 9 ++++ src/radio.c | 2 +- src/telemetry.c | 24 ++++++++-- src/telemetry.h | 2 + 10 files changed, 129 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 13e2229..1698751 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # RS41ng - Amateur radio firmware for Vaisala RS41 radiosonde -**NOTE:** This firmware is a work in progress and some features might not work as expected yet! +**NOTE:** While this firmware has been tested with great success on a number of high-altitude balloon +flights, it is still a work in progress and some features might not work as expected yet! +In particular, the time sync (scheduling) features and use of an external Si5351 as a transmitter need more testing. This is a custom, amateur radio-oriented firmware for [Vaisala RS41 radiosondes](https://www.vaisala.com/en/products/instruments-sensors-and-other-measurement-devices/soundings-products/rs41). Some code is based on an earlier RS41 firmware project called [RS41HUP](https://github.com/df8oe/RS41HUP), @@ -133,6 +135,15 @@ Sensor driver code contributions are welcome! transmit frequencies and transmission mode parameters in `config.h` 2. Set up transmitted message templates in `config.c`, depending on the modes you use +### Power consumption and power-saving features + +Power consumption notes (at 3V supply voltage) by Mark VK5QI: + +- GPS in default (max performance) mode, transmitting with Si4032 @ 13 dBm = ~150 mA +- GPS in default (max performance) mode, not transmitting = 70-90 mA +- GPS in power-saving mode, transmitting with Si4032 @ 13 dBm = ~120 mA +- GPS in power-saving mode, not transmitting = 30-50 mA, depending on GPS state. + ### Time sync settings The time sync feature is a simple way to activate the transmissions every N seconds, delayed by the `TIME_SYNC_OFFSET_SECONDS` setting. @@ -242,6 +253,39 @@ Payload 3: ## Building the firmware +The easiest and the recommended method to build the firmware is using Docker. + +If you have a Linux environment -- Windows Subsystem for Linux (WSL) or macOS might work too -- and +you feel adventurous, you can try to build using the Linux-based instructions. + +### Building the firmware with Docker + +Using Docker to build the firmware is usually the easiest option, because it provides a stable Fedora Linux-based +build environment on any platform. It should work on Windows and Mac operating systems too. + +The Docker environment can also help address issues with the build process. + +1. Install Docker if not already installed +2. Set the current directory to the RS41ng source directory +3. Build the RS41ng compiler Docker image using the following command. It is necessary to build the Docker image only once. + ``` + docker build -t rs41ng_compiler . + ``` +4. Build the firmware using the following command. If you need to rebuild the firmware, simply run the command again. + On Linux/macOS, run: + ``` + docker run --rm -it -v $(pwd):/usr/local/src/RS41ng rs41ng_compiler + ``` + On Windows, run: + ``` + docker run --rm -it -v %cd%:/usr/local/src/RS41ng rs41ng_compiler + ``` +5. The firmware will be stored in file `build/src/RS41ng.elf` + +Now you can flash the firmware using instructions below (skip the build instructions for Linux). + +### Building the firmware in a Linux environment + Software requirements: * [GNU GCC toolchain](https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-a/downloads/9-2-2019-12) @@ -254,7 +298,7 @@ On a Red Hat/Fedora Linux installation, the following packages can be installed: dnf install arm-none-eabi-gcc-cs arm-none-eabi-gcc-cs-c++ arm-none-eabi-binutils-cs arm-none-eabi-newlib cmake openocd ``` -### Steps to build the firmware +#### Steps to build the firmware on Linux 1. Install the required software dependencies listed above 2. Build the firmware using the following commands @@ -266,25 +310,6 @@ dnf install arm-none-eabi-gcc-cs arm-none-eabi-gcc-cs-c++ arm-none-eabi-binutils ``` 3. The firmware will be stored in file `build/src/RS41ng.elf` -### Building the firmware with Docker - -Using Docker to build the firmware is usually the easiest option, because it provides a stable Fedora Linux-based -build environment on any platform. It should work on Windows and Mac operating systems too. - -The Docker environment can also help address issues with the build process, including the `strlcpy()` errors observed on certain Linux distributions. - -1. Install Docker if not already installed -2. Set the current directory to the RS41ng source directory -3. Build the RS41ng compiler Docker image using the following command. It is necessary to build the Docker image only once. - ``` - docker build -t rs41ng_compiler . - ``` -4. Build the firmware using the following command. If you need to rebuild the firmware, simply run the command again. - ``` - docker run --rm -it -v $(pwd):/usr/local/src/RS41ng rs41ng_compiler - ``` -5. The firmware will be stored in file `build/src/RS41ng.elf` - ## Flashing the firmware Hardware requirements: @@ -326,7 +351,8 @@ ______________________| |______________________ ### Steps to flash the firmware -1. If your ST-LINK v2 programmer is capable of providing power (as some third-party clones are), remove the batteries from the sonde. Otherwise, leave the battiers in and power on the sonde. +1. If your ST-LINK v2 programmer is capable of providing power (as some third-party clones are), + remove the batteries from the sonde. Otherwise, leave the batteries in and power on the sonde. 2. Connect an ST-LINK v2 programmer dongle to the sonde via the following pins: * SWDIO -> Pin 9 (SWDIO) * SWCLK -> Pin 8 (SWCLK) diff --git a/src/codecs/horus/horus_packet_v1.c b/src/codecs/horus/horus_packet_v1.c index 20c4098..661196e 100644 --- a/src/codecs/horus/horus_packet_v1.c +++ b/src/codecs/horus/horus_packet_v1.c @@ -48,6 +48,8 @@ size_t horus_packet_v1_create(uint8_t *payload, size_t length, telemetry_data *d horus_packet.Sats += 100; } else if (gps_data->power_safe_mode_state == POWER_SAFE_MODE_STATE_POWER_OPTIMIZED_TRACKING) { horus_packet.Sats += 200; + } else if (gps_data->power_safe_mode_state == POWER_SAFE_MODE_STATE_INACTIVE) { + horus_packet.Sats += 300; } horus_packet.Checksum = (uint16_t) calculate_crc16_checksum((char *) &horus_packet, sizeof(horus_packet) - 2); diff --git a/src/codecs/horus/horus_packet_v2.c b/src/codecs/horus/horus_packet_v2.c index b267be2..9988d69 100644 --- a/src/codecs/horus/horus_packet_v2.c +++ b/src/codecs/horus/horus_packet_v2.c @@ -43,6 +43,8 @@ size_t horus_packet_v2_create(uint8_t *payload, size_t length, telemetry_data *d horus_packet.Sats += 100; } else if (gps_data->power_safe_mode_state == POWER_SAFE_MODE_STATE_POWER_OPTIMIZED_TRACKING) { horus_packet.Sats += 200; + } else if (gps_data->power_safe_mode_state == POWER_SAFE_MODE_STATE_INACTIVE) { + horus_packet.Sats += 300; } memset(horus_packet.CustomData, 0, sizeof(horus_packet.CustomData)); diff --git a/src/config.h b/src/config.h index b2fb44a..0056158 100644 --- a/src/config.h +++ b/src/config.h @@ -2,7 +2,8 @@ #define __CONFIG_H // Enable semihosting to receive debug logs during development -// NOTE: Semihosting has to be disabled when the RS41 radiosonde is not connected to the STM32 programmer dongle, otherwise the firmware will not run. +// See the README for details on how to set up debugging and debug logs with GDB +// NOTE: Semihosting has to be disabled when the RS41 radiosonde is not connected to an STM32 programmer dongle, otherwise the firmware will not run. //#define SEMIHOSTING_ENABLE //#define LOGGING_ENABLE @@ -58,11 +59,22 @@ #define RADIO_TIME_SYNC_THRESHOLD_MS 2000 // Number of leap seconds to add to the raw GPS time reported by the GPS chip (see https://timetoolsltd.com/gps/what-is-gps-time/ for more info) +// This value is used by default, but if the received GPS data contains indication about leap seconds, that one is used instead. #define GPS_TIME_LEAP_SECONDS 18 // Enable this setting to require 3D fix (altitude required, enable for airborne use), otherwise 2D fix is enough #define GPS_REQUIRE_3D_FIX true +// Enable power-saving features of the GPS chip to save power. +// This option should be safe to enable, as it enters a selective power saving mode. +// If the GPS chip loses fix, it will enter a higher power state automatically. +// Note that power saving mode is only enabled after the GPS chip has acquired good GPS fix for the first time. +// It is not necessary to use power saving on short flights (e.g. less than 6 hours). +// Based on measurements Mark VK5QI, enabling this reduces power consumption by about 30-40 mA (~50%) to around 30-50 mA, +// where the consumption is 70-90 mA when power saving is not enabled and any radio transmitters are idle. +// See the README for details about power consumption. +#define GPS_POWER_SAVING_ENABLE false + // Enable NMEA output from GPS via external serial port. This disables use of I²C bus (Si5351 and sensors) because the pins are shared. #define GPS_NMEA_OUTPUT_VIA_SERIAL_PORT_ENABLE false @@ -105,6 +117,7 @@ // 0 = -1dBm, 1 = 2dBm, 2 = 5dBm (~3 mW), 3 = 8dBm (~6 mW), 4 = 11dBm (~12 mW), 5 = 14dBm (25 mW), 6 = 17dBm (50 mW), 7 = 20dBm (100 mW) // This defaults to 5 (14 dBm, 25 mW), which is a good setting for Horus 4FSK transmissions and it saves power. // For APRS usage, you might want to use maximum power setting of 7 (20 dBm, 100 mW). Note that this setting reduces battery life. +// See the README for details about power consumption. #define RADIO_SI4032_TX_POWER 5 // Which modes to transmit using the built-in Si4032 transmitter chip diff --git a/src/drivers/ubxg6010/ubxg6010.c b/src/drivers/ubxg6010/ubxg6010.c index 585fc17..dca001e 100644 --- a/src/drivers/ubxg6010/ubxg6010.c +++ b/src/drivers/ubxg6010/ubxg6010.c @@ -391,7 +391,7 @@ uBloxPacket msgcfgrxm = { }, .data.cfgrxm = { .reserved1=8, // Always set to 8 - .lpMode=0 // Low power mode: Eco mode -- TODO: set back to Eco mode + .lpMode=0 // Enable max performance mode at start-up time to acquire GPS fix quickly } }; @@ -489,6 +489,28 @@ uBloxPacket msgcfgnmea = { } }; +bool ubxg6010_enable_power_save_mode() +{ + bool success; + uBloxPacket packet; + + // Copy the packet header + memcpy(&packet, &msgcfgrxm, sizeof(uBloxPacket)); + + // The default power-save settings should be OK (1 second cyclic) + // Enable power-saving mode + packet.data.cfgrxm.lpMode = 1; + + log_info("GPS: Entering power-saving mode\n"); + ubxg6010_send_packet(&packet); + success = ubxg6010_wait_for_ack(); + if (!success) { + log_error("GPS: Entering power-saving mode failed\n") + } + + return success; +} + bool ubxg6010_init() { bool success; @@ -714,6 +736,11 @@ static void ubxg6010_handle_packet(uBloxPacket *pkt) ubxg6010_current_gps_data.time_of_week_millis = pkt->data.navtimegps.iTOW; ubxg6010_current_gps_data.week = pkt->data.navtimegps.week; + if (pkt->data.navtimegps.valid & 0x04) { + // Flag set if leap seconds are valid + ubxg6010_current_gps_data.leap_seconds = pkt->data.navtimegps.leapS; + } + ubxg6010_current_gps_data.updated = true; } else if (pkt->header.messageClass == 0x01 && pkt->header.messageId == 0x21) { ubxg6010_current_gps_data.year = pkt->data.navtimeutc.year; diff --git a/src/drivers/ubxg6010/ubxg6010.h b/src/drivers/ubxg6010/ubxg6010.h index 02302bc..d1a94e6 100644 --- a/src/drivers/ubxg6010/ubxg6010.h +++ b/src/drivers/ubxg6010/ubxg6010.h @@ -7,6 +7,8 @@ bool ubxg6010_init(); +bool ubxg6010_enable_power_save_mode(); + void ubxg6010_request_gpstime(); bool ubxg6010_get_current_gps_data(gps_data *data); diff --git a/src/gps.h b/src/gps.h index 31775a7..26b6c92 100644 --- a/src/gps.h +++ b/src/gps.h @@ -5,11 +5,19 @@ #include #include "config.h" +// Acquisition state: The receiver actively searches for and acquires signals. Maximum power consumption. #define POWER_SAFE_MODE_STATE_ACQUISITION 0 +// Tracking state: The receiver continuously tracks and downloads data. Less power consumption than in Acquisition state. #define POWER_SAFE_MODE_STATE_TRACKING 1 +// POT state: The receiver repeatedly loops through a sequence of tracking (TRK), calculating the position fix +// (Calc), and entering an idle period (Idle). No new signals are acquired and no data is downloaded. Much less +// power consumption than in Tracking state. #define POWER_SAFE_MODE_STATE_POWER_OPTIMIZED_TRACKING 2 +// Inactive state: Most parts of the receiver are switched off. #define POWER_SAFE_MODE_STATE_INACTIVE 3 +#define GPS_IS_POWER_SAVING_ACTIVE(gps_data) (gps_data.power_safe_mode_state != POWER_SAFE_MODE_STATE_ACQUISITION) + #define GPS_FIX_NO_FIX 0 #define GPS_FIX_DEAD_RECKONING_ONLY 1 #define GPS_FIX_2D 2 @@ -34,6 +42,7 @@ typedef struct _gps_data { uint8_t seconds; uint8_t minutes; uint8_t hours; + int8_t leap_seconds; int32_t latitude_degrees_1000000; int32_t longitude_degrees_1000000; diff --git a/src/radio.c b/src/radio.c index 8ab09b6..e047689 100644 --- a/src/radio.c +++ b/src/radio.c @@ -712,7 +712,7 @@ bool radio_handle_time_sync() return false; } - uint32_t time_millis = gps.time_of_week_millis - (GPS_TIME_LEAP_SECONDS * 1000); + uint32_t time_millis = gps.time_of_week_millis - (gps_time_leap_seconds * 1000); if (time_millis == radio_previous_time_sync_scheduled) { // The GPS chip has not provided an updated time yet for some reason diff --git a/src/telemetry.c b/src/telemetry.c index fa382d0..aae73c0 100644 --- a/src/telemetry.c +++ b/src/telemetry.c @@ -9,6 +9,11 @@ #include "config.h" #include "log.h" +// Initialize leap seconds with a known good value +int8_t gps_time_leap_seconds = GPS_TIME_LEAP_SECONDS; + +static bool gps_power_saving_enabled = false; + void telemetry_collect(telemetry_data *data) { log_info("Collecting telemetry...\n"); @@ -31,9 +36,22 @@ void telemetry_collect(telemetry_data *data) ubxg6010_get_current_gps_data(&data->gps); - // Zero out position data if we don't have a valid GPS fix. - // This is done to avoid transmitting invalid position information. - if (!GPS_HAS_FIX(data->gps)) { + if (GPS_HAS_FIX(data->gps)) { + // If we have a good fix, we can enter power-saving mode + if ((data->gps.satellites_visible >= 6) && !gps_power_saving_enabled) { + #ifdef GPS_POWER_SAVING_ENABLE + ubxg6010_enable_power_save_mode(); + gps_power_saving_enabled = true; + #endif + } + + // If we get the number of leap seconds from GPS data, use it + if (data->gps.leap_seconds > 0) { + gps_time_leap_seconds = data->gps.leap_seconds; + } + } else { + // Zero out position data if we don't have a valid GPS fix. + // This is done to avoid transmitting invalid position information. data->gps.latitude_degrees_1000000 = 0; data->gps.longitude_degrees_1000000 = 0; data->gps.altitude_mm = 0; diff --git a/src/telemetry.h b/src/telemetry.h index 6e1a00a..f7c6f10 100644 --- a/src/telemetry.h +++ b/src/telemetry.h @@ -25,4 +25,6 @@ typedef struct _telemetry_data { void telemetry_collect(telemetry_data *data); +extern int8_t gps_time_leap_seconds; + #endif