Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: PlatformIO CI

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
environment:
- seeed-xiao-nrf54l15
- seeed-xiao-nrf54lm20a

steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: |
~/.cache/pip
~/.platformio/.cache
key: ${{ runner.os }}-pio-${{ matrix.environment }}
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install PlatformIO Core
run: pip install --upgrade platformio

- name: Build firmware
run: |
platformio run --environment ${{ matrix.environment }}

- name: Upload firmware artifact
uses: actions/upload-artifact@v4
with:
name: firmware-${{ matrix.environment }}
path: |
.pio/build/${{ matrix.environment }}/firmware.hex
.pio/build/${{ matrix.environment }}/zephyr/zephyr.hex
if-no-files-found: error
5 changes: 4 additions & 1 deletion platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ build_flags =
-DOD_APP_VERSION=0x0100
-DOPENDISPLAY_BUILD_ID=\"nrf54\"
-DOPENDISPLAY_ZLIB_USE_HEAP_WINDOW=0
-D__ARM_FEATURE_DSP=1

[env:seeed-xiao-nrf54l15]
platform = https://github.com/Seeed-Studio/platform-seeedboards.git
Expand All @@ -30,3 +29,7 @@ board = seeed-xiao-nrf54lm20a
build_flags =
${env.build_flags}
-DNRF54_BOARD_LM20
; CMSIS 6 (this env's framework-zephyr) gates DSP wrappers on __ARM_FEATURE_DSP,
; but g++ never declares the __sxtb16/__sxtab16 ACLE builtins they call, so every
; C++ TU including a Zephyr header fails. Nothing here uses DSP intrinsics.
-U__ARM_FEATURE_DSP
3 changes: 2 additions & 1 deletion src/boot_screen.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "boot_screen.h"
#include "opendisplay_ble.h"
#include "opendisplay_battery.h"
#include "opendisplay_config_parser.h"
#include "opendisplay_display_color.h"
#include "opendisplay_structs.h"
Expand Down Expand Up @@ -76,7 +77,7 @@ static int boot_get_plane(uint8_t color_scheme)

static float boot_read_battery_voltage(void)
{
return -999.0f;
return opendisplay_battery_read_voltage_volts();
}

static float boot_read_chip_temperature(void)
Expand Down
211 changes: 211 additions & 0 deletions src/opendisplay_battery.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
#include "opendisplay_battery.h"
#include "opendisplay_ble.h"
#include "opendisplay_sensor_bq27220.h"
#include "opendisplay_structs.h"
#include "nrf54_gpio.h"

#include <stdio.h>
#include <zephyr/devicetree.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/adc.h>

/*
* SAADC battery read.
*
* The 8 SAADC channels are defined in the board devicetree (channel@0..7 ->
* NRF_SAADC_AIN0..AIN7, gain 1/4, internal reference, 12-bit, 8x oversample).
* app.overlay exposes them through /zephyr,user io-channels so we get one
* ADC_DT_SPEC per channel and can select at runtime by index. On nRF54L the
* analog inputs are on P1.00..P1.07 -> AIN0..AIN7, so only pins on port 1 with
* pin 0..7 are analog-capable; anything else is rejected with a log.
*
* Transfer-function parity with the reference:
* nRF52840 Arduino analogRead() is 10-bit with AR_DEFAULT (0.6V internal ref,
* 1/6 gain => 3.6V full scale), so raw10 = pin_volts / 3.6 * 1024. Existing
* device configs calibrated voltage_scaling_factor for
* volts = raw10 * factor / 100000.
* Here the SAADC yields pin millivolts (adc_raw_to_millivolts_dt), which we
* convert to the same 10-bit count space before applying the formula:
* raw10 = pin_mv * 1024 / 3600
* volts = raw10 * factor / 100000
* so unchanged configs produce the same voltage.
*/

#define OD_BATTERY_TTL_MS 30000u
#define OD_BATTERY_SAMPLES 10

#if DT_NODE_EXISTS(DT_PATH(zephyr_user)) && DT_NODE_HAS_PROP(DT_PATH(zephyr_user), io_channels)
#define OD_ADC_AVAILABLE 1
static const struct adc_dt_spec s_adc_specs[] = {
ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 0),
ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 1),
ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 2),
ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 3),
ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 4),
ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 5),
ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 6),
ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 7),
};
#else
#define OD_ADC_AVAILABLE 0
#endif

/* Map a compact nRF54 pin byte to a SAADC channel index (== AIN index).
* Returns -1 for non-analog pins. */
static int battery_pin_to_ain(uint8_t pin_cfg)
{
uint8_t port;
uint8_t pin;

if (!nrf54_pin_decode(pin_cfg, &port, &pin)) {
return -1;
}
if (port != 1u || pin > 7u) {
return -1;
}
return (int)pin;
}

#if OD_ADC_AVAILABLE
/* Read one channel and return the pin voltage in millivolts, or -1 on error. */
static int32_t saadc_read_mv(const struct adc_dt_spec *spec)
{
int16_t sample = 0;
struct adc_sequence seq = {
.buffer = &sample,
.buffer_size = sizeof(sample),
};

if (adc_sequence_init_dt(spec, &seq) != 0) {
return -1;
}
if (adc_read(spec->dev, &seq) != 0) {
return -1;
}
int32_t mv = sample;
/* Single-ended readings can dip slightly negative near 0. */
if (mv < 0) {
mv = 0;
}
if (adc_raw_to_millivolts_dt(spec, &mv) != 0) {
return -1;
}
return mv;
}
#endif

/* Uncached SAADC path. Returns volts, or -1.0f if unavailable. */
static float battery_read_saadc_volts(void)
{
const struct GlobalConfig *cfg = opendisplay_get_global_config();

if (cfg == NULL) {
return -1.0f;
}
uint8_t sense_pin = cfg->power_option.battery_sense_pin;
uint8_t enable_pin = cfg->power_option.battery_sense_enable_pin;
uint16_t scaling = cfg->power_option.voltage_scaling_factor;

if (sense_pin == 0xFFu) {
return -1.0f;
}
int ain = battery_pin_to_ain(sense_pin);
if (ain < 0) {
printf("[OD] battery: sense pin 0x%02X is not SAADC-capable "
"(need P1.00-P1.07)\r\n", sense_pin);
return -1.0f;
}
#if !OD_ADC_AVAILABLE
printf("[OD] battery: ADC not available in this build\r\n");
return -1.0f;
#else
const struct adc_dt_spec *spec = &s_adc_specs[ain];

if (!adc_is_ready_dt(spec)) {
printf("[OD] battery: ADC device not ready\r\n");
return -1.0f;
}
if (adc_channel_setup_dt(spec) != 0) {
printf("[OD] battery: adc_channel_setup failed (AIN%d)\r\n", ain);
return -1.0f;
}

/* Enable the sense divider (reference drives it HIGH; battery_sense_flags
* ENABLE_INVERTED is not honored, matching readBatteryVoltageUncached). */
if (enable_pin != 0xFFu) {
nrf54_gpio_configure_output(enable_pin, true);
k_msleep(10);
}

int64_t mv_sum = 0;
int good = 0;
for (int i = 0; i < OD_BATTERY_SAMPLES; i++) {
int32_t mv = saadc_read_mv(spec);
if (mv >= 0) {
mv_sum += mv;
good++;
}
k_msleep(2);
}

if (enable_pin != 0xFFu) {
nrf54_gpio_write(enable_pin, false);
nrf54_gpio_park(enable_pin);
}

if (good == 0) {
return -1.0f;
}
int32_t avg_mv = (int32_t)(mv_sum / good);
/* Convert to the reference's 10-bit / 3.6V full-scale count space. */
int32_t raw10 = (avg_mv * 1024) / 3600;
if (scaling > 0u) {
return (float)((double)raw10 * (double)scaling / 100000.0);
}
return -1.0f;
#endif
}

/* Uncached: BQ27220 fuel gauge first, then SAADC. */
static float battery_read_uncached(void)
{
if (opendisplay_sensor_bq27220_is_configured()) {
float gauge_v = opendisplay_sensor_bq27220_voltage_volts();
if (gauge_v >= 0.0f) {
return gauge_v;
}
}
return battery_read_saadc_volts();
}

float opendisplay_battery_read_voltage_volts(void)
{
static uint32_t last_read_ms;
static float cached = -1.0f;
static bool have_reading;

uint32_t now = k_uptime_get_32();

if (have_reading && (now - last_read_ms) < OD_BATTERY_TTL_MS) {
return cached;
}
cached = battery_read_uncached();
last_read_ms = now;
have_reading = true;
return cached;
}

uint16_t opendisplay_battery_get_10mv(void)
{
float volts = opendisplay_battery_read_voltage_volts();

if (volts < 0.0f) {
return 0u;
}
uint16_t mv = (uint16_t)(volts * 1000.0f);
uint16_t v10 = (uint16_t)(mv / 10u);
if (v10 > 511u) {
v10 = 511u;
}
return v10;
}
27 changes: 27 additions & 0 deletions src/opendisplay_battery.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#ifndef OPENDISPLAY_BATTERY_H
#define OPENDISPLAY_BATTERY_H

#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

/*
* Battery voltage measurement. Source priority mirrors the nRF52840 reference
* readBatteryVoltageUncached(): a configured BQ27220 fuel gauge wins, otherwise
* the SAADC on the configured battery_sense_pin. Results are cached for 30 s.
*/

/* Cached battery voltage in volts, or -1.0f if not configured / unavailable. */
float opendisplay_battery_read_voltage_volts(void);

/* Cached battery voltage in 10 mV units, clamped to 0..511 (9-bit MSD field);
* 0 when unavailable. */
uint16_t opendisplay_battery_get_10mv(void);

#ifdef __cplusplus
}
#endif

#endif
15 changes: 14 additions & 1 deletion src/opendisplay_ble.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
#include "opendisplay_button.h"
#include "opendisplay_led.h"
#include "opendisplay_pipe.h"
#include "opendisplay_battery.h"
#include "opendisplay_sensor_sht40.h"
#include "opendisplay_sensor_bq27220.h"
#include "board_nrf54.h"

#include <stdio.h>
Expand Down Expand Up @@ -128,12 +131,19 @@ static void read_chip_temperature_once(void)

static void update_msd_payload(void)
{
uint16_t battery_voltage_10mv = 0;
uint16_t battery_voltage_10mv;
int16_t temp_encoded;
uint8_t temperature_byte;
uint8_t battery_voltage_low_byte;
uint8_t status_byte;

/* Mirror the reference updatemsdata() ordering: refresh the sensor
* dynamic slots, then the battery source (BQ27220-preferred, else SAADC),
* before packing the frame. All three are TTL-cached (30 s). */
opendisplay_sensor_sht40_poll();
opendisplay_sensor_bq27220_poll();
battery_voltage_10mv = opendisplay_battery_get_10mv();

temp_encoded = (int16_t)((s_chip_temperature + 40.0f) * 2.0f);
if (temp_encoded < 0) {
temp_encoded = 0;
Expand Down Expand Up @@ -454,6 +464,9 @@ void opendisplay_ble_init(void)

read_chip_temperature_once();

opendisplay_sensor_bq27220_init();
opendisplay_sensor_sht40_init();

opendisplay_led_init();
opendisplay_display_boot_apply();

Expand Down
1 change: 1 addition & 0 deletions src/opendisplay_ble.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ void opendisplay_ble_restart_advertising(void);
uint16_t opendisplay_ble_get_app_version(void);
void opendisplay_ble_copy_msd_bytes(uint8_t out[16]);
void opendisplay_ble_update_msd(bool refresh_advertising);
void opendisplay_ble_set_dynamic_byte(uint8_t index, uint8_t value);
float opendisplay_ble_get_chip_temperature(void);

bool opendisplay_ble_pipe_notify(const uint8_t *data, uint16_t len);
Expand Down
Loading