Skip to content

Commit 15590cc

Browse files
Health Monitor - WIP
1 parent 86a89bb commit 15590cc

16 files changed

Lines changed: 618 additions & 0 deletions

soc/nordic/nrf54l/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ zephyr_library_sources(
77
)
88
zephyr_include_directories(.)
99

10+
add_subdirectory_ifdef(CONFIG_SOC_HEALTH_MONITORING health_monitoring_nrfx)
11+
1012
dt_nodelabel(kmu_push_area_node NODELABEL nrf_kmu_reserved_push_area)
1113

1214
# We need a buffer in memory in a static location which can be used by

soc/nordic/nrf54l/Kconfig

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,12 @@ config SOC_NRF54L_ANOMALY_56_WORKAROUND
107107
help
108108
This option enables configuration workaround 56 for nRF54L Series SoCs.
109109

110+
config VBAT_MONITOR_ENABLE
111+
bool "Enable VBAT monitoring"
112+
select SOC_HEALTH_MONITORING
113+
114+
config VINTERNAL_MONITOR_ENABLE
115+
bool "Enable VInternal monitoring"
116+
select SOC_HEALTH_MONITORING
117+
110118
endif # SOC_SERIES_NRF54L

soc/nordic/nrf54l/Kconfig.defconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
if SOC_SERIES_NRF54L
77

88
rsource "Kconfig.defconfig.nrf54l*"
9+
rsource "health_monitoring_nrfx/Kconfig"
910

1011
if ARM
1112

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright (c) 2026 Nordic Semiconductor ASA
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
cmake_minimum_required(VERSION 3.20.0)
5+
6+
zephyr_library_sources(health_monitoring.c)
7+
zephyr_include_directories(.)
8+
9+
# if(CONFIG_VBAT_MONITOR_ENABLE)
10+
set(EXTRA_DTC_OVERLAY_FILE "vbatt_monitor.overlay")
11+
# endif()
12+
13+
if(CONFIG_VINTERNAL_MONITOR_ENABLE)
14+
set(EXTRA_DTC_OVERLAY_FILE "v_internal_monitor.overlay")
15+
endif()
16+
17+
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Nordic Semiconductor nRF71 MCU line
2+
3+
# Copyright (c) 2025 Nordic Semiconductor ASA
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
config VBAT_MONITOR_ENABLE
7+
bool "Enable VBAT monitoring"
8+
select SOC_HEALTH_MONITORING
9+
10+
config VINTERNAL_MONITOR_ENABLE
11+
bool "Enable VInternal monitoring"
12+
select SOC_HEALTH_MONITORING
13+
14+
config SOC_HEALTH_MONITORING
15+
bool "Enable health monitoring system"
16+
default n
17+
select ADC
18+
select SOC_LATE_INIT_HOOK
19+
# Debug
20+
select SHELL
21+
select KERNEL_SHELL
22+
help
23+
Enable periodic VBAT voltage monitoring using SAADC.
24+
When enabled, the application will periodically measure
25+
the battery voltage and trigger a callback if it falls
26+
below the configured threshold.
27+
28+
29+
if SOC_HEALTH_MONITORING
30+
31+
config VBAT_MONITOR_INTERVAL_MS
32+
int "VBAT measurement interval (milliseconds)"
33+
default 2000
34+
range 100 3600000
35+
help
36+
Interval between VBAT measurements in milliseconds.
37+
Minimum: 100 ms
38+
Maximum: 3600000 ms (1 hour)
39+
40+
config VBAT_MONITOR_LOWER_THRESHOLD_MV
41+
int "VBAT low threshold (millivolts)"
42+
default 250
43+
range 200 5000
44+
help
45+
Battery voltage threshold in millivolts. When the measured
46+
voltage falls below this value, the low battery callback
47+
will be triggered.
48+
Typical range: 2000-5000 mV
49+
50+
config HEALTH_MONITOR_LOG_LEVEL
51+
int "Health monitor log level"
52+
default 3
53+
range 0 4
54+
help
55+
Log level for health monitor module:
56+
0 = OFF
57+
1 = ERROR
58+
2 = WARN
59+
3 = INFO
60+
4 = DEBUG
61+
62+
endif # VBAT_MONITOR_ENABLE
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* ADC measurement sample with work queue.
3+
*
4+
* - Performs an ADC measurement in a work queue item scheduled every 5 seconds.
5+
* - Updates a global variable with the latest ADC value (raw and millivolts).
6+
* - If the ADC value (mV) is below a configured threshold, triggers a
7+
* low-threshold callback (e.g. for alert/interrupt handling).
8+
* - After each measurement, schedules the same work again to repeat.
9+
*/
10+
11+
#include <zephyr/device.h>
12+
#include <zephyr/kernel.h>
13+
#include <zephyr/devicetree.h>
14+
#include <zephyr/drivers/adc.h>
15+
#include <zephyr/sys/printk.h>
16+
17+
#if !DT_NODE_EXISTS(DT_PATH(zephyr_user)) || \
18+
!DT_NODE_HAS_PROP(DT_PATH(zephyr_user), io_channels)
19+
#error "No suitable devicetree overlay specified"
20+
#endif
21+
22+
#define DT_SPEC_AND_COMMA(node_id, prop, idx) \
23+
ADC_DT_SPEC_GET_BY_IDX(node_id, idx),
24+
25+
/* Data of ADC io-channels specified in devicetree. */
26+
static const struct adc_dt_spec adc_channels[] = {
27+
DT_FOREACH_PROP_ELEM(DT_PATH(zephyr_user), io_channels,
28+
DT_SPEC_AND_COMMA)
29+
};
30+
31+
/* Delay between measurements */
32+
#define MEASUREMENT_INTERVAL_SEC 5
33+
34+
/* Threshold in millivolts: trigger callback when ADC (mV) is below this */
35+
#define ADC_LOW_THRESHOLD_MV 1500
36+
37+
/** Global variable: latest ADC value in millivolts (updated by work item). */
38+
volatile int32_t g_adc_value_mv;
39+
40+
/** Global variable: latest raw ADC value (updated by work item). */
41+
volatile int32_t g_adc_value_raw;
42+
43+
/** Set when ADC value is below threshold (can be used like an interrupt flag). */
44+
volatile bool g_adc_below_threshold;
45+
46+
/** Optional: count how many times low threshold was triggered. */
47+
volatile uint32_t g_adc_low_trigger_count;
48+
49+
static int32_t adc_buffer;
50+
static struct adc_sequence sequence = {
51+
.buffer = &adc_buffer,
52+
.buffer_size = sizeof(adc_buffer),
53+
#if CONFIG_SAMPLE_ADC_CALIBRATE_REQUIRED
54+
.calibrate = true,
55+
#endif
56+
};
57+
58+
static void adc_work_handler(struct k_work *work);
59+
60+
K_WORK_DELAYABLE_DEFINE(adc_work, adc_work_handler);
61+
62+
/**
63+
* Called when ADC value is below threshold (treat as software "interrupt" handler).
64+
* Can be extended to set a GPIO, wake a thread, or raise a poll signal.
65+
*/
66+
static void adc_low_threshold_trigger(void)
67+
{
68+
g_adc_below_threshold = true;
69+
g_adc_low_trigger_count++;
70+
printk("ADC below threshold: %d mV (raw %d)\n",
71+
(int)g_adc_value_mv, (int)g_adc_value_raw);
72+
}
73+
74+
static int adc_setup()
75+
{
76+
int err = 0;
77+
/* Configure channels individually prior to sampling. */
78+
for (size_t i = 0U; i < ARRAY_SIZE(adc_channels); i++) {
79+
printk("Is ready %s \n", adc_channels[i].dev->name);
80+
if (!adc_is_ready_dt(&adc_channels[i])) {
81+
printk("ADC controller device %s not ready\n", adc_channels[i].dev->name);
82+
return 0;
83+
}
84+
printk("Configure %s \n", adc_channels[i].dev->name);
85+
err = adc_channel_setup_dt(&adc_channels[i]);
86+
if (err < 0) {
87+
printk("Could not setup channel #%d (%d)\n", i, err);
88+
return 0;
89+
}
90+
}
91+
return 0;
92+
}
93+
94+
static void adc_work_handler(struct k_work *work)
95+
{
96+
int err;
97+
98+
(void)work;
99+
100+
for (size_t i = 0U; i < ARRAY_SIZE(adc_channels); i++) {
101+
int32_t val_mv;
102+
103+
printk("- %s, channel %d: ",
104+
adc_channels[i].dev->name,
105+
adc_channels[i].channel_id);
106+
107+
(void)adc_sequence_init_dt(&adc_channels[i], &sequence);
108+
109+
err = adc_read_dt(&adc_channels[i], &sequence);
110+
if (err < 0) {
111+
printk("Could not read (%d)\n", err);
112+
continue;
113+
}
114+
115+
val_mv = adc_buffer;
116+
117+
printk("%"PRId32, val_mv);
118+
err = adc_raw_to_millivolts_dt(&adc_channels[i],
119+
&val_mv);
120+
/* conversion to mV may not be supported, skip if not */
121+
if (err < 0) {
122+
printk(" (value in mV not available)\n");
123+
} else {
124+
printk(" = %"PRId32" mV\n", val_mv);
125+
}
126+
}
127+
k_work_schedule(&adc_work, K_MSEC(CONFIG_VBAT_MONITOR_INTERVAL_MS));
128+
129+
// /* Update global variable (raw) */
130+
// g_adc_value_raw = adc_buffer;
131+
132+
// /* Convert to millivolts and update global */
133+
// ret = adc_raw_to_millivolts_dt(&adc_channel, &adc_buffer);
134+
// if (ret == 0) {
135+
// g_adc_value_mv = adc_buffer;
136+
// } else {
137+
// g_adc_value_mv = 0;
138+
// }
139+
140+
// /* If below threshold, trigger "interrupt" (callback) */
141+
// if (g_adc_value_mv < ADC_LOW_THRESHOLD_MV) {
142+
// adc_low_threshold_trigger();
143+
// } else {
144+
// g_adc_below_threshold = false;
145+
// }
146+
147+
// printk("ADC: %d mV (raw %d)%s\n",
148+
// (int)g_adc_value_mv, (int)g_adc_value_raw,
149+
// g_adc_below_threshold ? " [LOW]" : "");
150+
151+
// reschedule:
152+
// /* Schedule this work again in 5 seconds to repeat the process */
153+
// k_work_schedule(&adc_work, K_SECONDS(MEASUREMENT_INTERVAL_SEC));
154+
}
155+
156+
157+
158+
int health_monitoring_setup(void)
159+
{
160+
161+
printk("ADC work queue sample: measure every %d s, low threshold %d mV\n",
162+
MEASUREMENT_INTERVAL_SEC, ADC_LOW_THRESHOLD_MV);
163+
164+
adc_setup();
165+
166+
/* Start the cycle: first run in 5 seconds */
167+
k_work_schedule(&adc_work, K_MSEC(CONFIG_VBAT_MONITOR_INTERVAL_MS));
168+
169+
// for (;;) {
170+
// k_sleep(K_SECONDS(10));
171+
// /* Optional: print global state from main loop */
172+
// printk("Global ADC: %d mV, low_trigger_count %u\n",
173+
// (int)g_adc_value_mv, (unsigned)g_adc_low_trigger_count);
174+
// }
175+
176+
return 0;
177+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Copyright (c) 2024 Nordic Semiconductor ASA
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
#ifndef _HEALTH_MONITORING_H_
8+
#define _HEALTH_MONITORING_H_
9+
10+
int health_monitoring_setup(void);
11+
12+
#endif /* _HEALTH_MONITORING_H_ */
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/ {
2+
zephyr,user {
3+
io-channels = <&adc 2>;
4+
};
5+
};
6+
7+
&adc {
8+
#address-cells = <1>;
9+
#size-cells = <0>;
10+
11+
channel@2 {
12+
reg = <2>;
13+
zephyr,gain = "ADC_GAIN_1";
14+
zephyr,reference = "ADC_REF_INTERNAL";
15+
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
16+
zephyr,input-positive = <NRF_SAADC_VDD>;
17+
zephyr,resolution = <12>;
18+
zephyr,oversampling = <8>;
19+
};
20+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/ {
2+
zephyr,user {
3+
io-channels = <&adc 1>;
4+
};
5+
};
6+
7+
&adc {
8+
#address-cells = <1>;
9+
#size-cells = <0>;
10+
11+
channel@1 {
12+
reg = <1>;
13+
zephyr,gain = "ADC_GAIN_1"; //NRF_SAADC_GAIN1_4
14+
zephyr,reference = "ADC_REF_INTERNAL";
15+
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
16+
zephyr,input-positive = <NRF_SAADC_AIN2>; /* P1.06 */
17+
zephyr,input-negative = <NRF_SAADC_GND>;
18+
zephyr,resolution = <12>;
19+
zephyr,oversampling = <8>;
20+
};
21+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright (c) 2026 Nordic Semiconductor ASA
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
cmake_minimum_required(VERSION 3.20.0)
5+
6+
zephyr_library_sources(health_monitoring.c
7+
battery_monitor.c)
8+
9+
zephyr_include_directories(.)
10+
11+
if (CONFIG_ADC) # TODO Currently this does not cause an error when enabled from Kconfig
12+
message(FATAL_ERROR
13+
"Health Monitoring feautre can not be used in conjunction with other ADC measurements,
14+
see sample XXXX to implement an alternative, disabling this feature may have
15+
adverse effects on performance over temperature")
16+
endif()

0 commit comments

Comments
 (0)