Version overlay app ABI and capabilities

This commit is contained in:
Armel FAUVEAU committed 2026-08-31 20:51:09 +02:00
1 parent 902e19ba8d
commit c8da5f99cc
17 files changed
+130 -64

No files matched your search

+2 -2
View File
@@ -26,6 +26,7 @@
#include "k5viewer.h" #include "k5viewer.h"
#endif #endif
#include "settings.h"
#include "ui/status.h" #include "ui/status.h"
// Signal window mapped onto the RSSI bar, in dBm. // Signal window mapped onto the RSSI bar, in dBm.
@@ -104,7 +105,6 @@
#define FOXHUNT_BEACON_TX_MIN 5 // shortest TX window (s) #define FOXHUNT_BEACON_TX_MIN 5 // shortest TX window (s)
#define FOXHUNT_BEACON_TX_MAX 60 // longest TX window (s) #define FOXHUNT_BEACON_TX_MAX 60 // longest TX window (s)
#define FOXHUNT_BEACON_TX_STEP 5 // TX adjust step (s) #define FOXHUNT_BEACON_TX_STEP 5 // TX adjust step (s)
#define FOXHUNT_CALLSIGN_ADDR 0x00A0C8u // boot message line 1 in SPI flash
#define FOXHUNT_CALLSIGN_MAX 12 // maximum boot-message characters used by the beacon #define FOXHUNT_CALLSIGN_MAX 12 // maximum boot-message characters used by the beacon
// Fox identifier (3 key). MOE..MO5 are the five standard IARU ARDF foxes ("MO" + 1..5 // Fox identifier (3 key). MOE..MO5 are the five standard IARU ARDF foxes ("MO" + 1..5
@@ -1366,7 +1366,7 @@ static void FOXHUNT_LoadCallsign(void)
// Read the callsign from the boot message (line 1), sanitised to what Morse can // Read the callsign from the boot message (line 1), sanitised to what Morse can
// send; empty/erased leaves foxCall empty (the CALL id then falls back to bare MOE). // send; empty/erased leaves foxCall empty (the CALL id then falls back to bare MOE).
PY25Q16_ReadBuffer(FOXHUNT_CALLSIGN_ADDR, call, FOXHUNT_CALLSIGN_MAX); PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR, call, FOXHUNT_CALLSIGN_MAX);
call[FOXHUNT_CALLSIGN_MAX] = '\0'; call[FOXHUNT_CALLSIGN_MAX] = '\0';
for (uint8_t i = 0; i < FOXHUNT_CALLSIGN_MAX; i++) { for (uint8_t i = 0; i < FOXHUNT_CALLSIGN_MAX; i++) {
char c = call[i]; char c = call[i];
+25 -17
View File
@@ -25,9 +25,11 @@
* *
* void app_main(const app_api_t *api); // entry, at blob offset 0 * void app_main(const app_api_t *api); // entry, at blob offset 0
* *
* Both the firmware loader and the app include THIS header, so the struct layout * Both the firmware loader and the app include THIS header. Within one ABI
* can never disagree. Bump APP_ABI_VERSION on any incompatible change; the loader * major, app_api_t is append-only: existing fields may never move, disappear or
* refuses a blob whose header abi_version does not match. * change signature. Append a service and bump APP_API_LEVEL; only apps using
* that service need the new level. A breaking layout change bumps
* APP_ABI_MAJOR and resets APP_API_LEVEL to 1.
*/ */
#ifndef APPS_APP_API_H #ifndef APPS_APP_API_H
@@ -36,11 +38,10 @@
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
/* The table below is a single versioned layout. Any change to app_api_t - a /* First unpublished/public baseline: all services currently present below are
* reorder, a removal, or an append - MUST bump this. Keep in sync with the * ABI major 1, API level 1. */
* value read by pack_app.py, which #define APP_ABI_MAJOR 1u
* stamps the blob the loader checks against. */ #define APP_API_LEVEL 1u
#define APP_ABI_VERSION 8u
/* KEY codes mirrored from driver/keyboard.h (enum KEY_Code_e). Kept in sync by /* KEY codes mirrored from driver/keyboard.h (enum KEY_Code_e). Kept in sync by
* value so the app stays independent of the firmware headers. */ * value so the app stays independent of the firmware headers. */
@@ -143,7 +144,7 @@ typedef struct {
uint8_t band; uint8_t band;
uint8_t scanlist; uint8_t scanlist;
uint8_t compander; uint8_t compander;
char name[16]; char name[16]; /* fixed-width wire field; NUL termination is not required */
} app_beam_channel_t; } app_beam_channel_t;
_Static_assert(sizeof(app_beam_channel_t) == 44u, _Static_assert(sizeof(app_beam_channel_t) == 44u,
@@ -155,8 +156,19 @@ enum {
APP_BEAM_RX_ERROR = 2, APP_BEAM_RX_ERROR = 2,
}; };
/* CRITICAL PERSISTENCE RULE
*
* The app executes from the RAM buffer also used as PY25Q16's sector cache.
* Therefore no API callback may erase or write external flash while app_main()
* is running: a read-modify-write would overwrite the executing app. Services
* such as cfg_save, fm_commit and beam_save must only stage resident RAM state;
* APP_LaunchOverlay commits it after app_main() returns. */
typedef struct app_api { typedef struct app_api {
uint8_t abi_version; /* == APP_ABI_VERSION */ /* Fixed four-byte prefix; services remain naturally pointer-aligned. */
uint8_t abi_major; /* == APP_ABI_MAJOR */
uint8_t api_level; /* == APP_API_LEVEL */
uint16_t api_size; /* sizeof(app_api_t), for optional probing */
app_fb_t fb; /* -> gFrameBuffer */ app_fb_t fb; /* -> gFrameBuffer */
@@ -182,10 +194,6 @@ typedef struct app_api {
void (*play_tone)(uint16_t tone, uint16_t ms); /* full BK4819 tone burst + AF path */ void (*play_tone)(uint16_t tone, uint16_t ms); /* full BK4819 tone burst + AF path */
void (*led)(bool on); /* green GPIO indicator */ void (*led)(bool on); /* green GPIO indicator */
/* ==== appended when the ABI moved 1 -> 2; now an integral part of ABI 2.
* A firmware and an app that agree on abi_version agree on this whole layout,
* so these must never be reached through a table that does not include them. == */
/* ---- extra text drawing ---- */ /* ---- extra text drawing ---- */
void (*print_normal)(const char *s, uint8_t start, uint8_t end, uint8_t line); /* UI_PrintStringSmallNormal */ void (*print_normal)(const char *s, uint8_t start, uint8_t end, uint8_t line); /* UI_PrintStringSmallNormal */
void (*print_inverse)(const char *s, uint8_t x, uint8_t line, void (*print_inverse)(const char *s, uint8_t x, uint8_t line,
@@ -240,14 +248,14 @@ typedef struct app_api {
void (*fm_state)(app_fm_state_t *s, bool write);/* read/write the resident gEeprom.FM_* */ void (*fm_state)(app_fm_state_t *s, bool write);/* read/write the resident gEeprom.FM_* */
void (*fm_commit)(void); /* deferred SETTINGS_SaveFM (config + channels) */ void (*fm_commit)(void); /* deferred SETTINGS_SaveFM (config + channels) */
/* ---- navigation (ABI 3) ---- /* ---- navigation (API level 1 baseline) ----
* Convert a raw APP_KEY_UP/DOWN into a semantic value direction: * Convert a raw APP_KEY_UP/DOWN into a semantic value direction:
* UV-K5 UP/DOWN -> +1/-1 * UV-K5 UP/DOWN -> +1/-1
* UV-K1 LEFT/RIGHT -> -1/+1 * UV-K1 LEFT/RIGHT -> -1/+1
* Returns 0 for any other key. Keep get_key() raw for spatial controls. */ * Returns 0 for any other key. Keep get_key() raw for spatial controls. */
int8_t (*nav_dir)(uint8_t key); int8_t (*nav_dir)(uint8_t key);
/* ---- triple VFO (ABI 4) ---- /* ---- triple VFO (API level 1 baseline) ----
* A and B are the live Main Display VFOs. C is a resident temporary VFO * A and B are the live Main Display VFOs. C is a resident temporary VFO
* loaded from c_channel (or the first valid memory after B when invalid). * loaded from c_channel (or the first valid memory after B when invalid).
* tick is called every 20 ms by the app and returns APP_TRIVFO_* state. */ * tick is called every 20 ms by the app and returns APP_TRIVFO_* state. */
@@ -259,7 +267,7 @@ typedef struct app_api {
uint8_t (*trivfo_tick)(void); uint8_t (*trivfo_tick)(void);
uint8_t (*trivfo_ptt)(bool pressed); /* physical PTT edge; resident applies SetPTT */ uint8_t (*trivfo_ptt)(bool pressed); /* physical PTT edge; resident applies SetPTT */
/* ---- BEAM channel transfer (ABI 8) ---- */ /* ---- BEAM channel transfer (API level 1 baseline) ---- */
void (*beam_prepare)(void); /* tune the fixed narrow-band FSK channel */ void (*beam_prepare)(void); /* tune the fixed narrow-band FSK channel */
void (*beam_leave)(void); /* defensively stop FSK before app return */ void (*beam_leave)(void); /* defensively stop FSK before app return */
void (*beam_get)(app_beam_channel_t *channel); /* export selected VFO */ void (*beam_get)(app_beam_channel_t *channel); /* export selected VFO */
+18 -12
View File
@@ -170,35 +170,41 @@ static void app_format_size(char out[6], uint32_t bytes)
out[5] = '\0'; out[5] = '\0';
} }
/* Human-readable reason for an APP_LaunchOverlay / APP_ValidateSlot failure. */ /* Human-readable action for an APP_LaunchOverlay / APP_ValidateSlot failure. */
static const char *app_err_text(uint8_t rc) static const char *app_err_text(const app_header_t *header, uint8_t rc)
{ {
switch (rc) switch (rc)
{ {
case APP_ERR_SLOT: return "BAD SLOT"; case APP_ERR_SLOT: return "BAD SLOT";
case APP_ERR_MAGIC: return "NO APP"; case APP_ERR_MAGIC: return "NO APP";
case APP_ERR_ABI: return "ABI MISMATCH"; case APP_ERR_ABI:
case APP_ERR_NOT_COMMITTED: return "INCOMPLETE"; /* api_min == 0 also identifies pre-reset development blobs whose
case APP_ERR_SIZE: return "BAD SIZE"; * former uint16_t ABI value occupies these two bytes. */
case APP_ERR_CRC: return "CRC ERROR"; if (header->api_min == 0u || header->abi_major < APP_ABI_MAJOR)
case APP_ERR_VMA: return "VMA MISMATCH"; return "UPDATE APP";
return "UPDATE FIRMWARE";
case APP_ERR_NOT_COMMITTED: return "REINSTALL APP";
case APP_ERR_SIZE: return "UPDATE APP";
case APP_ERR_CRC: return "REINSTALL APP";
case APP_ERR_VMA: return "UPDATE APP";
case APP_ERR_AUTH: return "AUTH"; case APP_ERR_AUTH: return "AUTH";
case APP_ERR_CAP: return "NOT SUPPORTED";
default: return "ERROR"; default: return "ERROR";
} }
} }
/* A launch failed: name the app and the reason, then wait for a key. Without this /* A launch failed: name the app and the action to take, then wait for a key. Without this
* an incompatible app would silently "do nothing" when selected. */ * an incompatible app would silently "do nothing" when selected. */
static void app_show_error(const char *name, uint8_t rc) static void app_show_error(const app_header_t *header, uint8_t rc)
{ {
char nm[19]; char nm[19];
app_copy(nm, sizeof(nm), name, APP_NAME_LEN); app_copy(nm, sizeof(nm), header->name, APP_NAME_LEN);
UI_DisplayClear(); UI_DisplayClear();
UI_StatusClear(); UI_StatusClear();
GUI_DisplaySmallestInverse("APP ERROR", 46, 0, true, true, 82); GUI_DisplaySmallestInverse("APP ERROR", 46, 0, true, true, 82);
UI_PrintStringSmallNormal(nm, 2, 0, 2); /* which app */ UI_PrintStringSmallNormal(nm, 2, 0, 2); /* which app */
UI_PrintStringSmallNormal(app_err_text(rc), 2, 0, 4); /* why */ UI_PrintStringSmallNormal(app_err_text(header, rc), 2, 0, 4); /* action */
UI_PrintStringSmallNormal("Press any key", 2, 0, 6); UI_PrintStringSmallNormal("Press any key", 2, 0, 6);
ST7565_BlitStatusLine(); ST7565_BlitStatusLine();
ST7565_BlitFullScreen(); ST7565_BlitFullScreen();
@@ -316,7 +322,7 @@ void APP_MenuOpen(void)
const uint8_t rc = APP_LaunchOverlay(sel); /* runs until the app exits */ const uint8_t rc = APP_LaunchOverlay(sel); /* runs until the app exits */
if (rc != APP_OK) if (rc != APP_OK)
app_show_error(hdr[sel].name, rc); /* no longer silent */ app_show_error(&hdr[sel], rc); /* no longer silent */
app_wait_release(); app_wait_release();
break; break;
} }
+19 -7
View File
@@ -51,6 +51,13 @@
#include "misc.h" /* dBmCorrTable */ #include "misc.h" /* dBmCorrTable */
_Static_assert(sizeof(app_header_t) == 64, "app_header_t must be 64 bytes"); _Static_assert(sizeof(app_header_t) == 64, "app_header_t must be 64 bytes");
_Static_assert(sizeof(app_api_t) <= UINT16_MAX, "app_api_t size field overflow");
#ifdef ENABLE_FMRADIO
#define APP_AVAILABLE_CAPS APP_CAP_FM
#else
#define APP_AVAILABLE_CAPS 0u
#endif
/* ---- ABI wrappers: the few resident calls that are not a direct signature match ---- */ /* ---- ABI wrappers: the few resident calls that are not a direct signature match ---- */
static bool app_allow_screen_saver; static bool app_allow_screen_saver;
@@ -129,7 +136,7 @@ static void app_play_tone(uint16_t tone, uint16_t ms)
AUDIO_AudioPathOff(); AUDIO_AudioPathOff();
} }
/* ---- ABI 4: resident triple-VFO engine --------------------------------- /* ---- API level 1: resident triple-VFO engine ----------------------------
* The overlay owns the UI and key timing, while this resident engine owns all * The overlay owns the UI and key timing, while this resident engine owns all
* radio details. Keeping VFO_Info_t and BK4819 sequencing on this side makes * radio details. Keeping VFO_Info_t and BK4819 sequencing on this side makes
* the app independent of feature-dependent firmware layouts. */ * the app independent of feature-dependent firmware layouts. */
@@ -525,7 +532,7 @@ static uint8_t app_trivfo_ptt(bool pressed)
return 0; return 0;
} }
/* ---- ABI 8: BEAM radio/channel bridge ------------------------------------ /* ---- API level 1: BEAM radio/channel bridge -------------------------------
* The modal app owns the packet format, CRC, UI and state machine. Resident * The modal app owns the packet format, CRC, UI and state machine. Resident
* code only translates the stable ABI channel structure and performs the FSK * code only translates the stable ABI channel structure and performs the FSK
* operations which depend on VFO_Info_t and the BK4819 driver. */ * operations which depend on VFO_Info_t and the BK4819 driver. */
@@ -719,7 +726,7 @@ static void app_beam_draw(const char *status)
UI_PrintStringSmallBold(status, 2, LCD_WIDTH - 1u, line); UI_PrintStringSmallBold(status, 2, LCD_WIDTH - 1u, line);
} }
/* ---- v2 radio wrappers ---- */ /* ---- radio wrappers ---- */
static int16_t app_rssi_dbm(void) { return BK4819_GetRSSI_dBm() + dBmCorrTable[gRxVfo->Band]; } static int16_t app_rssi_dbm(void) { return BK4819_GetRSSI_dBm() + dBmCorrTable[gRxVfo->Band]; }
static uint16_t app_bk_read(uint8_t r) { return BK4819_ReadRegister((BK4819_REGISTER_t)r); } static uint16_t app_bk_read(uint8_t r) { return BK4819_ReadRegister((BK4819_REGISTER_t)r); }
static void app_bk_write(uint8_t r, uint16_t v) { BK4819_WriteRegister((BK4819_REGISTER_t)r, v); } static void app_bk_write(uint8_t r, uint16_t v) { BK4819_WriteRegister((BK4819_REGISTER_t)r, v); }
@@ -787,7 +794,7 @@ static uint32_t app_tx_freq(void) { return gTxVfo->pTX->Frequency; }
static void app_boot_callsign(char *buf, uint8_t len) static void app_boot_callsign(char *buf, uint8_t len)
{ {
char raw[12]; uint8_t n = 0; char raw[12]; uint8_t n = 0;
PY25Q16_ReadBuffer(0x00A0C8u, raw, sizeof(raw)); /* boot message line 1 */ PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR, raw, sizeof(raw));
for (uint8_t i = 0; i < sizeof(raw) && (uint8_t)(n + 1) < len; i++) { for (uint8_t i = 0; i < sizeof(raw) && (uint8_t)(n + 1) < len; i++) {
char c = raw[i]; char c = raw[i];
if (c == '\0' || (uint8_t)c == 0xFFu) break; if (c == '\0' || (uint8_t)c == 0xFFu) break;
@@ -849,8 +856,10 @@ uint8_t APP_ValidateSlot(uint8_t slot, app_header_t *out_header)
if (h.magic != APP_MAGIC) return APP_ERR_MAGIC; if (h.magic != APP_MAGIC) return APP_ERR_MAGIC;
if (h.hdr_version != APP_HDR_VERSION) return APP_ERR_MAGIC; if (h.hdr_version != APP_HDR_VERSION) return APP_ERR_MAGIC;
if (h.abi_version != APP_ABI_VERSION) return APP_ERR_ABI; if (h.abi_major != APP_ABI_MAJOR || h.api_min == 0u ||
h.api_min > APP_API_LEVEL) return APP_ERR_ABI;
if (!(h.flags & APP_FLAG_COMMITTED)) return APP_ERR_NOT_COMMITTED; if (!(h.flags & APP_FLAG_COMMITTED)) return APP_ERR_NOT_COMMITTED;
if (h.required_caps & ~APP_AVAILABLE_CAPS) return APP_ERR_CAP;
if (h.code_size < 2u || h.code_size > APP_OVERLAY_MAX || if (h.code_size < 2u || h.code_size > APP_OVERLAY_MAX ||
(uint32_t)h.entry_off > h.code_size - 2u || /* leave room for a 2-byte Thumb insn */ (uint32_t)h.entry_off > h.code_size - 2u || /* leave room for a 2-byte Thumb insn */
(h.entry_off & 1u) != 0u) /* entry must be Thumb-aligned (even) */ (h.entry_off & 1u) != 0u) /* entry must be Thumb-aligned (even) */
@@ -932,9 +941,12 @@ uint8_t APP_SlotInfo(uint8_t slot, app_header_t *out_header)
/* All services are immutable. Keeping the table in flash avoids rebuilding a /* All services are immutable. Keeping the table in flash avoids rebuilding a
* roughly quarter-kilobyte automatic object on every launch and removes that * roughly quarter-kilobyte automatic object on every launch and removes that
* object from the launcher's stack frame. */ * object from the launcher's stack frame. Callbacks must also obey the ABI's
* no-external-flash-write rule while entry() is running. */
static const app_api_t app_api = { static const app_api_t app_api = {
.abi_version = APP_ABI_VERSION, .abi_major = APP_ABI_MAJOR,
.api_level = APP_API_LEVEL,
.api_size = sizeof(app_api_t),
.fb = gFrameBuffer, .fb = gFrameBuffer,
.display_clear = UI_DisplayClear, .display_clear = UI_DisplayClear,
.status_clear = UI_StatusClear, .status_clear = UI_StatusClear,
+10 -3
View File
@@ -78,10 +78,15 @@
#define APP_SHORTCUT_BEACON 0x04u #define APP_SHORTCUT_BEACON 0x04u
#define APP_SHORTCUT_BEAM 0x08u #define APP_SHORTCUT_BEAM 0x08u
/* Optional resident facilities an app may require. Requirements live in the
* previously reserved header bytes, so app_header_t remains 64 bytes. */
#define APP_CAP_FM 0x00000001u
typedef struct __attribute__((packed)) { typedef struct __attribute__((packed)) {
uint32_t magic; /* APP_MAGIC */ uint32_t magic; /* APP_MAGIC */
uint16_t hdr_version; /* APP_HDR_VERSION */ uint16_t hdr_version; /* APP_HDR_VERSION */
uint16_t abi_version; /* ABI the app was built against */ uint8_t abi_major; /* required ABI family */
uint8_t api_min; /* minimum append-only API level */
uint32_t code_size; /* bytes of code, <= APP_OVERLAY_MAX */ uint32_t code_size; /* bytes of code, <= APP_OVERLAY_MAX */
uint32_t code_crc32; /* CRC-32 (zlib) over code_size bytes */ uint32_t code_crc32; /* CRC-32 (zlib) over code_size bytes */
uint16_t entry_off; /* entry offset within the code (0) */ uint16_t entry_off; /* entry offset within the code (0) */
@@ -89,19 +94,21 @@ typedef struct __attribute__((packed)) {
char name[APP_NAME_LEN]; /* human-readable, NUL-terminated */ char name[APP_NAME_LEN]; /* human-readable, NUL-terminated */
char version[APP_VERSION_LEN];/* app version string */ char version[APP_VERSION_LEN];/* app version string */
uint32_t link_vma; /* RAM VMA the code was linked at */ uint32_t link_vma; /* RAM VMA the code was linked at */
uint8_t reserved[8]; /* pad to 64 bytes */ uint32_t required_caps; /* APP_CAP_* required by this app */
uint8_t reserved[4]; /* pad to 64 bytes */
} app_header_t; } app_header_t;
enum { enum {
APP_OK = 0, APP_OK = 0,
APP_ERR_SLOT, /* slot index out of range */ APP_ERR_SLOT, /* slot index out of range */
APP_ERR_MAGIC, /* no/invalid header */ APP_ERR_MAGIC, /* no/invalid header */
APP_ERR_ABI, /* ABI version mismatch */ APP_ERR_ABI, /* ABI family/API level mismatch */
APP_ERR_NOT_COMMITTED, /* image not marked complete */ APP_ERR_NOT_COMMITTED, /* image not marked complete */
APP_ERR_SIZE, /* code_size out of range */ APP_ERR_SIZE, /* code_size out of range */
APP_ERR_CRC, /* code CRC-32 mismatch */ APP_ERR_CRC, /* code CRC-32 mismatch */
APP_ERR_VMA, /* overlay buffer not at the link VMA */ APP_ERR_VMA, /* overlay buffer not at the link VMA */
APP_ERR_AUTH, /* host write refused: timestamp mismatch */ APP_ERR_AUTH, /* host write refused: timestamp mismatch */
APP_ERR_CAP, /* required firmware capability missing */
}; };
/* Read + validate a slot header (no CRC of the code). */ /* Read + validate a slot header (no CRC of the code). */
+2 -1
View File
@@ -9,6 +9,7 @@ set -euo pipefail
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
APP_NAME="Beacon" # <-- the only per-app line APP_NAME="Beacon" # <-- the only per-app line
APP_VER="1.0" APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
@@ -29,7 +30,7 @@ trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --vma "${APP_VMA}" \ --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \
--shortcut beacon >/dev/null --shortcut beacon >/dev/null
trap - ERR trap - ERR
+2 -1
View File
@@ -4,6 +4,7 @@ set -euo pipefail
APP="$(basename "$PWD")" APP="$(basename "$PWD")"
APP_NAME="BEAM" APP_NAME="BEAM"
APP_VER="1.0" APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} APP_VMA=${APP_VMA:-0x20000280}
OUT="${APP_NAME// /}" OUT="${APP_NAME// /}"
@@ -24,7 +25,7 @@ trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --vma "${APP_VMA}" \ --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \
--shortcut beam >/dev/null --shortcut beam >/dev/null
trap - ERR trap - ERR
+2 -1
View File
@@ -9,6 +9,7 @@ set -euo pipefail
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
APP_NAME="Breakout" # <-- the only per-app line APP_NAME="Breakout" # <-- the only per-app line
APP_VER="1.0" APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
@@ -29,7 +30,7 @@ trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --vma "${APP_VMA}" >/dev/null --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" >/dev/null
trap - ERR trap - ERR
BYTES=$(wc -c < "${APP}.bin") BYTES=$(wc -c < "${APP}.bin")
+2 -1
View File
@@ -9,6 +9,7 @@ set -euo pipefail
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
APP_NAME="Cube3D" # <-- the only per-app line APP_NAME="Cube3D" # <-- the only per-app line
APP_VER="1.0" APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
@@ -29,7 +30,7 @@ trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --vma "${APP_VMA}" --screensaver >/dev/null --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver >/dev/null
trap - ERR trap - ERR
BYTES=$(wc -c < "${APP}.bin") BYTES=$(wc -c < "${APP}.bin")
+3 -2
View File
@@ -9,6 +9,7 @@ set -euo pipefail
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
APP_NAME="Broadcast FM" # <-- the only per-app line APP_NAME="Broadcast FM" # <-- the only per-app line
APP_VER="1.0" APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
@@ -29,8 +30,8 @@ trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --vma "${APP_VMA}" \ --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \
--shortcut fm --screensaver >/dev/null --shortcut fm --require fm --screensaver >/dev/null
trap - ERR trap - ERR
BYTES=$(wc -c < "${APP}.bin") BYTES=$(wc -c < "${APP}.bin")
+2 -1
View File
@@ -9,6 +9,7 @@ set -euo pipefail
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
APP_NAME="FoxHunt" # <-- the only per-app line APP_NAME="FoxHunt" # <-- the only per-app line
APP_VER="1.0" APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
@@ -29,7 +30,7 @@ trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --vma "${APP_VMA}" \ --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \
--shortcut foxhunt >/dev/null --shortcut foxhunt >/dev/null
trap - ERR trap - ERR
+25 -7
View File
@@ -1,13 +1,14 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Pack an overlay-app raw binary into a .app blob: 64-byte header + code. # Pack an overlay-app raw binary into a .app blob: 64-byte header + code.
# Header layout mirrors app_overlay.h : app_header_t (little-endian, packed). # Header layout mirrors app_overlay.h : app_header_t (little-endian, packed).
# The format/version constants (magic, header + ABI version, overlay budget) are # The format/version constants (magic, header + ABI/API levels, overlay budget) are
# read straight from the C headers the firmware itself compiles, so there is a # read straight from the C headers the firmware itself compiles, so there is a
# SINGLE source of truth - the packer can never silently drift from the loader. # SINGLE source of truth - the packer can never silently drift from the loader.
# CRC-32 is zlib/PKZIP (init 0xFFFFFFFF, poly 0xEDB88320, final XOR) to match # CRC-32 is zlib/PKZIP (init 0xFFFFFFFF, poly 0xEDB88320, final XOR) to match
# the firmware's mb_ext_image_crc32 / mb_crc32_bytes. # the firmware's mb_ext_image_crc32 / mb_crc32_bytes.
# #
# ./pack_app.py breakout/breakout.bin breakout/breakout.app --name Breakout --ver 1.0 # ./pack_app.py breakout/breakout.bin breakout/breakout.app --name Breakout \
# --ver 1.0 --vma 0x20000280 --api-min 1
import argparse, os, re, struct, zlib, sys import argparse, os, re, struct, zlib, sys
HERE = os.path.dirname(os.path.abspath(__file__)) HERE = os.path.dirname(os.path.abspath(__file__))
@@ -28,7 +29,8 @@ def cdefine(header: str, name: str) -> int:
# Single source of truth: the C headers the firmware also compiles. # Single source of truth: the C headers the firmware also compiles.
MAGIC = cdefine("app_overlay.h", "APP_MAGIC").to_bytes(4, "little") # 0x31504146 -> b"FAP1" MAGIC = cdefine("app_overlay.h", "APP_MAGIC").to_bytes(4, "little") # 0x31504146 -> b"FAP1"
HDR_VERSION = cdefine("app_overlay.h", "APP_HDR_VERSION") HDR_VERSION = cdefine("app_overlay.h", "APP_HDR_VERSION")
ABI_VERSION = cdefine("app_api.h", "APP_ABI_VERSION") ABI_MAJOR = cdefine("app_api.h", "APP_ABI_MAJOR")
API_LEVEL = cdefine("app_api.h", "APP_API_LEVEL")
OVERLAY_MAX = cdefine("app_overlay.h", "APP_OVERLAY_MAX") # 4 KiB overlay budget OVERLAY_MAX = cdefine("app_overlay.h", "APP_OVERLAY_MAX") # 4 KiB overlay budget
FLAG_COMMITTED = cdefine("app_overlay.h", "APP_FLAG_COMMITTED") FLAG_COMMITTED = cdefine("app_overlay.h", "APP_FLAG_COMMITTED")
FLAG_SCREEN_SAVER = cdefine("app_overlay.h", "APP_FLAG_SCREEN_SAVER") FLAG_SCREEN_SAVER = cdefine("app_overlay.h", "APP_FLAG_SCREEN_SAVER")
@@ -40,6 +42,9 @@ SHORTCUTS = {
"beacon": cdefine("app_overlay.h", "APP_SHORTCUT_BEACON"), "beacon": cdefine("app_overlay.h", "APP_SHORTCUT_BEACON"),
"beam": cdefine("app_overlay.h", "APP_SHORTCUT_BEAM"), "beam": cdefine("app_overlay.h", "APP_SHORTCUT_BEAM"),
} }
CAPABILITIES = {
"fm": cdefine("app_overlay.h", "APP_CAP_FM"),
}
def field(s: str, n: int) -> bytes: def field(s: str, n: int) -> bytes:
b = s.encode("ascii", "strict")[: n - 1] b = s.encode("ascii", "strict")[: n - 1]
@@ -52,14 +57,21 @@ def main():
ap.add_argument("--name", default="app") ap.add_argument("--name", default="app")
ap.add_argument("--ver", default="1.0") ap.add_argument("--ver", default="1.0")
ap.add_argument("--entry", type=lambda x: int(x, 0), default=0) ap.add_argument("--entry", type=lambda x: int(x, 0), default=0)
ap.add_argument("--api-min", type=int, required=True,
help="minimum append-only firmware API level required by this app")
ap.add_argument("--vma", type=lambda x: int(x, 0), required=True, ap.add_argument("--vma", type=lambda x: int(x, 0), required=True,
help="RAM VMA the app was linked at (must match the firmware overlay)") help="RAM VMA the app was linked at (must match the firmware overlay)")
ap.add_argument("--screensaver", action="store_true", ap.add_argument("--screensaver", action="store_true",
help="allow the resident BLTime screen saver while this app is idle") help="allow the resident BLTime screen saver while this app is idle")
ap.add_argument("--shortcut", choices=SHORTCUTS, default="none", ap.add_argument("--shortcut", choices=SHORTCUTS, default="none",
help="resident quick action advertised by this app") help="resident quick action advertised by this app")
ap.add_argument("--require", action="append", choices=CAPABILITIES, default=[],
help="resident capability required by this app (repeatable)")
a = ap.parse_args() a = ap.parse_args()
if not 1 <= a.api_min <= API_LEVEL:
sys.exit(f"--api-min must be between 1 and current API level {API_LEVEL}")
code = open(a.infile, "rb").read() code = open(a.infile, "rb").read()
if len(code) == 0: if len(code) == 0:
sys.exit("empty input") sys.exit("empty input")
@@ -70,11 +82,15 @@ def main():
flags = (FLAG_COMMITTED | flags = (FLAG_COMMITTED |
(FLAG_SCREEN_SAVER if a.screensaver else 0) | (FLAG_SCREEN_SAVER if a.screensaver else 0) |
(SHORTCUTS[a.shortcut] << FLAG_SHORTCUT_SHIFT)) (SHORTCUTS[a.shortcut] << FLAG_SHORTCUT_SHIFT))
required_caps = 0
for capability in a.require:
required_caps |= CAPABILITIES[capability]
header = struct.pack( header = struct.pack(
"<4sHHIIHH16s16sI8s", "<4sHBBIIHH16s16sII4s",
MAGIC, HDR_VERSION, ABI_VERSION, MAGIC, HDR_VERSION, ABI_MAJOR, a.api_min,
len(code), crc, a.entry, flags, len(code), crc, a.entry, flags,
field(a.name, 16), field(a.ver, 16), a.vma, b"\x00" * 8, field(a.name, 16), field(a.ver, 16), a.vma,
required_caps, b"\x00" * 4,
) )
assert len(header) == 64, len(header) assert len(header) == 64, len(header)
@@ -82,7 +98,9 @@ def main():
f.write(header) f.write(header)
f.write(code) f.write(code)
print(f"{a.outfile}: name={a.name!r} ver={a.ver!r} vma=0x{a.vma:08x} " print(f"{a.outfile}: name={a.name!r} ver={a.ver!r} "
f"abi={ABI_MAJOR} api>={a.api_min} caps=0x{required_caps:08x} "
f"vma=0x{a.vma:08x} "
f"code={len(code)} B crc32=0x{crc:08x} -> blob {64 + len(code)} B") f"code={len(code)} B crc32=0x{crc:08x} -> blob {64 + len(code)} B")
if __name__ == "__main__": if __name__ == "__main__":
+2 -1
View File
@@ -9,6 +9,7 @@ set -euo pipefail
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
APP_NAME="Plasma" # <-- the only per-app line APP_NAME="Plasma" # <-- the only per-app line
APP_VER="1.0" APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
@@ -29,7 +30,7 @@ trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --vma "${APP_VMA}" --screensaver >/dev/null --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver >/dev/null
trap - ERR trap - ERR
BYTES=$(wc -c < "${APP}.bin") BYTES=$(wc -c < "${APP}.bin")
+2 -1
View File
@@ -3,6 +3,7 @@ set -euo pipefail
APP="$(basename "$PWD")" APP="$(basename "$PWD")"
APP_NAME="Triple VFO" APP_NAME="Triple VFO"
APP_VER="1.0" APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} APP_VMA=${APP_VMA:-0x20000280}
OUT="${APP_NAME// /}" OUT="${APP_NAME// /}"
CC=/opt/toolchain/bin/arm-none-eabi-gcc CC=/opt/toolchain/bin/arm-none-eabi-gcc
@@ -13,7 +14,7 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} -Wl,-
rm -f ./*.app ./*.elf ./*.bin rm -f ./*.app ./*.elf ./*.bin
"$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
"$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
python3 ../pack_app.py "${APP}.bin" "${OUT}.app" --name "$APP_NAME" --ver "$APP_VER" --vma "${APP_VMA}" --screensaver >/dev/null python3 ../pack_app.py "${APP}.bin" "${OUT}.app" --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver >/dev/null
BYTES=$(wc -c < "${APP}.bin") BYTES=$(wc -c < "${APP}.bin")
test "$BYTES" -le 4096 test "$BYTES" -le 4096
printf ' ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app\n' \ printf ' ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app\n' \
+8 -5
View File
@@ -90,8 +90,10 @@ void SETTINGS_InitEEPROM(void)
// 4. Reset logo lines (clear to null for strlen() == 0) // 4. Reset logo lines (clear to null for strlen() == 0)
char logoLines[32]; /* The two boot-message lines are contiguous in external flash. */
PY25Q16_ReadBuffer(0x00A0C8, logoLines, sizeof(logoLines)); char bootMessageLines[32];
PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR,
bootMessageLines, sizeof(bootMessageLines));
bool needsWrite = false; bool needsWrite = false;
@@ -99,12 +101,12 @@ void SETTINGS_InitEEPROM(void)
int offset = line * 16; int offset = line * 16;
for (int i = 0; i < 16; i++) { for (int i = 0; i < 16; i++) {
char c = logoLines[offset + i]; char c = bootMessageLines[offset + i];
if (c == 0) { if (c == 0) {
break; break;
} }
if (c < 0x20 || c > 0x7E) { if (c < 0x20 || c > 0x7E) {
memset(logoLines + offset, 0, 16); memset(bootMessageLines + offset, 0, 16);
needsWrite = true; needsWrite = true;
break; break;
} }
@@ -112,7 +114,8 @@ void SETTINGS_InitEEPROM(void)
} }
if (needsWrite) { if (needsWrite) {
PY25Q16_WriteBuffer(0x00A0C8, logoLines, sizeof(logoLines), false); PY25Q16_WriteBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR,
bootMessageLines, sizeof(bootMessageLines), false);
} }
// 5. Reset dBmCorrTable // 5. Reset dBmCorrTable
+4
View File
@@ -25,6 +25,10 @@
#include "radio.h" #include "radio.h"
#include <driver/backlight.h> #include <driver/backlight.h>
/* Shared PY25Q16 locations for the two configurable boot-message lines. */
#define SETTINGS_BOOT_MESSAGE_LINE1_ADDR 0x00A0C8u
#define SETTINGS_BOOT_MESSAGE_LINE2_ADDR 0x00A0D8u
enum POWER_OnDisplayMode_t { enum POWER_OnDisplayMode_t {
#ifdef ENABLE_FEAT_F4HWN #ifdef ENABLE_FEAT_F4HWN
POWER_ON_DISPLAY_MODE_ALL, POWER_ON_DISPLAY_MODE_ALL,
+2 -2
View File
@@ -261,10 +261,10 @@ void UI_DisplayWelcome(void)
char WelcomeString3[32]; char WelcomeString3[32];
// 0x0EB0 // 0x0EB0
PY25Q16_ReadBuffer(0x00A0C8, WelcomeString0, 16); PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR, WelcomeString0, 16);
WelcomeString0[16] = '\0'; WelcomeString0[16] = '\0';
// 0x0EC0 // 0x0EC0
PY25Q16_ReadBuffer(0x00A0D8, WelcomeString1, 16); PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE2_ADDR, WelcomeString1, 16);
WelcomeString1[16] = '\0'; WelcomeString1[16] = '\0';
sprintf(WelcomeString2, "%u.%02uV %u%%", sprintf(WelcomeString2, "%u.%02uV %u%%",