maxfg: log relaxations

log a subset of fg registers when the relaxation state changes.

Bug: 317827237
Change-Id: I4fa6b849bbf60ed725db4d92c8682d292e670b8c
Signed-off-by: AleX Pelosi <[email protected]>
(cherry picked from commit a1cbcf3ab9f4b2dedb4abe72b74b414a684a9ea1)
diff --git a/Documentation/ABI/testing/sysfs-class-power b/Documentation/ABI/testing/sysfs-class-power
new file mode 100644
index 0000000..04d0edd
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-class-power
@@ -0,0 +1,9 @@
+What:		/sys/class/power_supply/maxfg/fg_learning_events
+Date:		Feb 2024
+Contact:	[email protected]
+Description:
+		Report a snapshot of fuel gauge registers captured during
+                each learning event. Write 0 to clear the events.
+
+		Access: Read/Write
+		Valid values: Represented as string
\ No newline at end of file
diff --git a/Makefile b/Makefile
index efd0f3b..ac52f71 100644
--- a/Makefile
+++ b/Makefile
@@ -101,6 +101,7 @@
 obj-$(CONFIG_MAX1720X_BATTERY)  += max1720x-battery.o
 max1720x-battery-objs += max1720x_battery.o
 max1720x-battery-objs += max1720x_outliers.o
+max1720x-battery-objs += maxfg_logging.o
 max1720x-battery-objs += max_m5.o
 
 # OVP
diff --git a/google_bms.c b/google_bms.c
index 3e78080..fc2f171 100644
--- a/google_bms.c
+++ b/google_bms.c
@@ -717,6 +717,30 @@
 }
 EXPORT_SYMBOL_GPL(gbms_logbuffer_prlog);
 
+void gbms_logbuffer_devlog(struct logbuffer *log, struct device *dev, int level,
+			  int debug_no_logbuffer, int debug_printk_prlog,
+			  const char *f, ...)
+{
+	struct va_format vaf;
+	va_list args;
+
+	va_start(args, f);
+
+	vaf.fmt = f;
+	vaf.va = &args;
+
+	if (!debug_no_logbuffer)
+		logbuffer_vlog(log, f, args);
+
+	if (level <= debug_printk_prlog)
+		dev_printk_emit(level, dev, "%s %s: %pV",
+				dev_driver_string(dev),
+				dev_name(dev), &vaf);
+
+	va_end(args);
+}
+EXPORT_SYMBOL_GPL(gbms_logbuffer_devlog);
+
 bool chg_state_is_disconnected(const union gbms_charger_state *chg_state)
 {
 	return ((chg_state->f.flags & GBMS_CS_FLAG_BUCK_EN) == 0) &&
diff --git a/google_bms.h b/google_bms.h
index 83fc9ad..fec3346 100644
--- a/google_bms.h
+++ b/google_bms.h
@@ -456,6 +456,10 @@
 void gbms_logbuffer_prlog(struct logbuffer *log, int level, int debug_no_logbuffer,
 			  int debug_printk_prlog, const char *f, ...);
 
+void gbms_logbuffer_devlog(struct logbuffer *log, struct device *dev, int level,
+			   int debug_no_logbuffer, int debug_printk_prlog,
+			   const char *f, ...);
+
 /* debug/print */
 const char *gbms_chg_type_s(int chg_type);
 const char *gbms_chg_status_s(int chg_status);
diff --git a/max1720x_battery.c b/max1720x_battery.c
index d27896e..8e99bc7 100644
--- a/max1720x_battery.c
+++ b/max1720x_battery.c
@@ -35,6 +35,7 @@
 #include "gbms_power_supply.h"
 #include "google_bms.h"
 #include "max1720x_battery.h"
+#include "maxfg_logging.h"
 
 #include <linux/debugfs.h>
 
@@ -278,6 +279,9 @@
 
 	/* Current Offset */
 	bool current_offset_done;
+
+	/* buffer for recording learning history */
+	struct maxfg_capture_buf cb_lh;
 };
 
 #define MAX1720_EMPTY_VOLTAGE(profile, temp, cycle) \
@@ -308,41 +312,7 @@
 /* TODO: split between NV and Volatile? */
 
 
-static const struct max17x0x_reg * max17x0x_find_by_index(struct max17x0x_regtags *tags,
-							  int index)
-{
-	if (index < 0 || !tags || index >= tags->max)
-		return NULL;
 
-	return &tags->map[index];
-}
-
-static const struct max17x0x_reg * max17x0x_find_by_tag(struct max17x0x_regmap *map,
-							enum max17x0x_reg_tags tag)
-{
-	return max17x0x_find_by_index(&map->regtags, tag);
-}
-
-static inline int max17x0x_reg_read(struct max17x0x_regmap *map,
-				    enum max17x0x_reg_tags tag,
-				    u16 *val)
-{
-	const struct max17x0x_reg *reg;
-	unsigned int tmp;
-	int rtn;
-
-	reg = max17x0x_find_by_tag(map, tag);
-	if (!reg)
-		return -EINVAL;
-
-	rtn = regmap_read(map->regmap, reg->reg, &tmp);
-	if (rtn)
-		pr_err("Failed to read %x\n", reg->reg);
-	else
-		*val = tmp;
-
-	return rtn;
-}
 
 /* ------------------------------------------------------------------------- */
 
@@ -1142,6 +1112,36 @@
 
 static const DEVICE_ATTR_RW(rc_switch_enable);
 
+
+static ssize_t fg_learning_events_show(struct device *dev,
+				       struct device_attribute *attr, char *buf)
+{
+	struct power_supply *psy = container_of(dev, struct power_supply, dev);
+	struct max1720x_chip *chip = power_supply_get_drvdata(psy);
+
+	return maxfg_show_captured_buffer(&chip->cb_lh, buf, PAGE_SIZE);
+}
+
+static ssize_t fg_learning_events_store(struct device *dev,
+					      struct device_attribute *attr,
+					      const char *buf, size_t count)
+{
+	struct power_supply *psy = container_of(dev, struct power_supply, dev);
+	struct max1720x_chip *chip = power_supply_get_drvdata(psy);
+	int value, ret;
+
+	ret = kstrtoint(buf, 0, &value);
+	if (ret < 0)
+		return ret;
+
+	if (value == 0)
+		maxfg_clear_capture_buf(&chip->cb_lh);
+
+	return count;
+}
+
+static DEVICE_ATTR_RW(fg_learning_events);
+
 /* lsb 1/256, race with max1720x_model_work()  */
 static int max1720x_get_capacity_raw(struct max1720x_chip *chip, u16 *data)
 {
@@ -2272,6 +2272,47 @@
 	return ret;
 }
 
+static int max1720x_monitor_log_learning(struct max1720x_chip *chip, bool force)
+{
+	const bool seed = !chip->cb_lh.latest_entry;
+	bool log_it;
+	char *buf;
+	int ret;
+
+	/* do nothing if no changes on dpacc/dqacc or relaxation */
+	log_it = force || seed ||
+	         maxfg_ce_relaxed(&chip->regmap,  MAX_M5_FSTAT_RELDT | MAX_M5_FSTAT_RELDT2,
+				(u16 *)chip->cb_lh.latest_entry);
+	if (!log_it)
+		return 0;
+
+	ret = maxfg_capture_registers(&chip->cb_lh);
+	if (ret < 0) {
+		dev_dbg(chip->dev, "cannot read learning parameters (%d)\n", ret);
+		return ret;
+	}
+
+	/* no need to log at boot */
+	if (seed)
+		return 0;
+
+	buf = kmalloc(PAGE_SIZE, GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+
+	ret = maxfg_capture_to_cstr(&chip->cb_lh.config,
+				    (u16 *)chip->cb_lh.latest_entry,
+				    buf, PAGE_SIZE);
+	if (ret > 0)
+		gbms_logbuffer_devlog(chip->monitor_log, chip->dev,
+				      LOGLEVEL_INFO, 0, LOGLEVEL_INFO,
+				      "learn %s", buf);
+
+	kfree(buf);
+
+	return 0;
+}
+
 static int max1720x_get_property(struct power_supply *psy,
 				 enum power_supply_property psp,
 				 union power_supply_propval *val)
@@ -2309,6 +2350,9 @@
 		if (err == POWER_SUPPLY_STATUS_FULL)
 			batt_ce_start(&chip->cap_estimate,
 				      chip->cap_estimate.cap_tsettle);
+		/* check for relaxation event and log it */
+		max1720x_monitor_log_learning(chip, false);
+
 		/* return data ok */
 		err = 0;
 		break;
@@ -2940,6 +2984,7 @@
 	return storm;
 }
 
+
 static irqreturn_t max1720x_fg_irq_thread_fn(int irq, void *obj)
 {
 	struct max1720x_chip *chip = (struct max1720x_chip *)obj;
@@ -3104,6 +3149,7 @@
 		if (storm) {
 			pr_debug("Force power_supply_change in storm\n");
 		} else {
+			max1720x_monitor_log_learning(chip, false);
 			max1720x_monitor_log_data(chip, false);
 			if (chip->gauge_type == MAX_M5_GAUGE_TYPE)
 				max_m5_check_recal_state(chip->model_data,
@@ -3830,6 +3876,17 @@
 DEFINE_SIMPLE_ATTRIBUTE(debug_fake_battery_fops, NULL,
 			debug_fake_battery_set, "%llu\n");
 
+
+static int max1720x_log_learn_set(void *data, u64 val)
+{
+	struct max1720x_chip *chip = data;
+
+       max1720x_monitor_log_learning(chip, true);
+       return 0;
+}
+DEFINE_SIMPLE_ATTRIBUTE(debug_log_learn_fops, NULL, max1720x_log_learn_set, "%llu\n");
+
+
 static void max17x0x_reglog_dump(struct max17x0x_reglog *regs,
 				 size_t size,
 				 char *buff)
@@ -4231,6 +4288,7 @@
 
 static const DEVICE_ATTR_RW(act_impedance);
 
+
 static int max17x0x_init_sysfs(struct max1720x_chip *chip)
 {
 	struct dentry *de;
@@ -4252,6 +4310,7 @@
 	debugfs_create_file("fake_battery", 0400, de, chip, &debug_fake_battery_fops);
 	debugfs_create_file("batt_id", 0600, de, chip, &debug_batt_id_fops);
 	debugfs_create_file("force_psy_update", 0600, de, chip, &debug_force_psy_update_fops);
+	debugfs_create_file("log_learn", 0400, de, chip, &debug_log_learn_fops);
 
 	if (chip->regmap.reglog)
 		debugfs_create_file("regmap_writes", 0440, de,
@@ -6164,6 +6223,26 @@
 	return chip ? chip->model_data : NULL;
 }
 
+
+static int max1720x_init_fg_capture(struct max1720x_chip *chip)
+{
+	struct device *dev = &chip->psy->dev;
+	int ret;
+
+	/* Logging FG Learning */
+	maxfg_init_fg_learn_capture_config(&chip->cb_lh.config,
+					   &chip->regmap, &chip->regmap);
+	ret = maxfg_alloc_capture_buf(&chip->cb_lh, MAX_FG_LEARN_PARAM_MAX_HIST);
+	if (ret < 0)
+		return -ENOMEM;
+
+	ret = device_create_file(dev, &dev_attr_fg_learning_events);
+	if (ret)
+		dev_err(dev, "Failed to create fg_learning_params attribute\n");
+
+	return ret;
+}
+
 static int max1720x_probe(struct i2c_client *client,
 			  const struct i2c_device_id *id)
 {
@@ -6338,6 +6417,7 @@
 	chip->reg_prop_capacity_raw = (reg) ? reg->reg : MAX1720X_REPSOC;
 
 	max17x0x_init_sysfs(chip);
+	max1720x_init_fg_capture(chip);
 
 	INIT_DELAYED_WORK(&chip->cap_estimate.settle_timer,
 			  batt_ce_capacityfiltered_work);
@@ -6381,6 +6461,8 @@
 	if (chip->secondary)
 		i2c_unregister_device(chip->secondary);
 
+
+	maxfg_free_capture_buf(&chip->cb_lh);
 	wakeup_source_unregister(chip->get_prop_ws);
 
 	return 0;
diff --git a/max1720x_battery.h b/max1720x_battery.h
index 93e8f3f..403b992 100644
--- a/max1720x_battery.h
+++ b/max1720x_battery.h
@@ -67,6 +67,17 @@
 	MAX17X0X_TAG_BCEA,
 	MAX17X0X_TAG_rset,
 	MAX17X0X_TAG_BRES,
+
+	MAXFG_TAG_fcnom,
+	MAXFG_TAG_dpacc,
+	MAXFG_TAG_dqacc,
+	MAXFG_TAG_fcrep,
+	MAXFG_TAG_repsoc,
+	MAXFG_TAG_msoc,
+	MAXFG_TAG_learn,
+	MAXFG_TAG_rcomp0,
+	MAXFG_TAG_tempco,
+	MAXFG_TAG_fstat,
 };
 
 enum max17x0x_reg_types {
@@ -139,6 +150,41 @@
 	struct max17x0x_reglog *reglog;
 };
 
+static inline const struct max17x0x_reg *max17x0x_find_by_index(struct max17x0x_regtags *tags,
+							  int index)
+{
+	if (index < 0 || !tags || index >= tags->max)
+		return NULL;
+
+	return &tags->map[index];
+}
+
+static inline const struct max17x0x_reg *max17x0x_find_by_tag(struct max17x0x_regmap *map,
+								enum max17x0x_reg_tags tag)
+{
+	return max17x0x_find_by_index(&map->regtags, tag);
+}
+
+static inline int max17x0x_reg_read(struct max17x0x_regmap *map,
+				    enum max17x0x_reg_tags tag,
+				    u16 *val)
+{
+	const struct max17x0x_reg *reg;
+	unsigned int tmp;
+	int rtn;
+
+	reg = max17x0x_find_by_tag(map, tag);
+	if (!reg)
+		return -EINVAL;
+
+	rtn = regmap_read(map->regmap, reg->reg, &tmp);
+	if (rtn == 0)
+		*val = tmp;
+
+	return rtn;
+}
+
+
 int max1720x_get_capacity(struct i2c_client *client, int *iic_raw);
 int max1720x_get_voltage_now(struct i2c_client *client, int *iic_raw);
 int max17x0x_sw_reset(struct i2c_client *client);
diff --git a/max_m5.c b/max_m5.c
index 35db63f..7cdc61a 100644
--- a/max_m5.c
+++ b/max_m5.c
@@ -1594,6 +1594,17 @@
 	[MAX17X0X_TAG_curr] = { ATOM_INIT_REG16(MAX_M5_CURRENT)},
 	[MAX17X0X_TAG_mcap] = { ATOM_INIT_REG16(MAX_M5_MIXCAP)},
 	[MAX17X0X_TAG_vfsoc] = { ATOM_INIT_REG16(MAX_M5_VFSOC)},
+
+	[MAXFG_TAG_tempco] = { ATOM_INIT_REG16(MAX_M5_TEMPCO)},
+	[MAXFG_TAG_rcomp0] = { ATOM_INIT_REG16(MAX_M5_RCOMP0)},
+	[MAXFG_TAG_fcnom] = { ATOM_INIT_REG16(MAX_M5_FULLCAPNOM)},
+	[MAXFG_TAG_fcrep] = { ATOM_INIT_REG16(MAX_M5_FULLCAPREP)},
+	[MAXFG_TAG_repsoc] = { ATOM_INIT_REG16(MAX_M5_REPSOC)},
+	[MAXFG_TAG_msoc] = { ATOM_INIT_REG16(MAX_M5_MIXSOC)},
+	[MAXFG_TAG_learn] = { ATOM_INIT_REG16(MAX_M5_LEARNCFG)},
+	[MAXFG_TAG_fstat] = { ATOM_INIT_REG16(MAX_M5_FSTAT)},
+	[MAXFG_TAG_dqacc] = { ATOM_INIT_REG16(MAX_M5_DQACC)},
+	[MAXFG_TAG_dpacc] = { ATOM_INIT_REG16(MAX_M5_DPACC)},
 };
 
 int max_m5_regmap_init(struct max17x0x_regmap *regmap, struct i2c_client *clnt)
diff --git a/maxfg_logging.c b/maxfg_logging.c
new file mode 100644
index 0000000..d4acc0a
--- /dev/null
+++ b/maxfg_logging.c
@@ -0,0 +1,277 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (C) 2024 Google LLC
+ */
+
+#include <linux/err.h>
+#include <linux/regmap.h>
+#include <linux/slab.h>
+#include "google_bms.h"
+#include "max1720x_battery.h"
+#include "maxfg_logging.h"
+
+/* learning parameters */
+#define MAX_FG_LEARNING_CONFIG_NORMAL_REGS 8
+#define MAX_FG_LEARNING_CONFIG_DEBUG_REGS 2
+
+/* see maxfg_ce_relaxed() */
+static const enum max17x0x_reg_tags fg_learning_param[] = {
+	/* from normal regmap */
+	MAXFG_TAG_fcnom,
+	MAXFG_TAG_dpacc,
+	MAXFG_TAG_dqacc,
+	MAXFG_TAG_fcrep,
+	MAXFG_TAG_repsoc,
+	MAXFG_TAG_msoc,
+	MAX17X0X_TAG_vfsoc,
+	MAXFG_TAG_fstat,
+
+	/* from debug_regmap */
+	MAXFG_TAG_rcomp0,
+	MAXFG_TAG_tempco,
+};
+
+/* this could be static */
+void maxfg_init_fg_learn_capture_config(struct maxfg_capture_config *config,
+					struct max17x0x_regmap *regmap,
+					struct max17x0x_regmap *debug_regmap)
+{
+	strscpy(config->name,  "FG Learning Events", sizeof(config->name));
+	config->normal.tag = &fg_learning_param[0];
+	config->normal.reg_cnt = MAX_FG_LEARNING_CONFIG_NORMAL_REGS;
+	config->normal.regmap = regmap;
+
+	config->debug.tag = &fg_learning_param[MAX_FG_LEARNING_CONFIG_NORMAL_REGS];
+	config->debug.reg_cnt = MAX_FG_LEARNING_CONFIG_DEBUG_REGS;
+	config->debug.regmap = debug_regmap;
+
+	config->data_size = ARRAY_SIZE(fg_learning_param) * sizeof(u16);
+}
+
+
+int maxfg_alloc_capture_buf(struct maxfg_capture_buf *buf, int slots)
+{
+	if ((slots & (slots - 1)) || !buf || !buf->config.data_size || !slots)
+		return -EINVAL;
+
+	buf->slots = 0;
+	buf->latest_entry = NULL;
+
+	buf->cb.buf = kzalloc(buf->config.data_size * slots, GFP_KERNEL);
+	if (!buf->cb.buf)
+		return -ENOMEM;
+
+	buf->cb.head = 0;
+	buf->cb.tail = 0;
+	buf->slots = slots;
+
+	mutex_init(&buf->cb_wr_lock);
+	mutex_init(&buf->cb_rd_lock);
+
+	return 0;
+}
+
+void maxfg_clear_capture_buf(struct maxfg_capture_buf *buf)
+{
+	if (!buf)
+		return;
+
+	mutex_lock(&buf->cb_wr_lock);
+	mutex_lock(&buf->cb_rd_lock);
+
+	buf->latest_entry = NULL;
+	buf->cb.head = 0;
+	buf->cb.tail = 0;
+
+	mutex_unlock(&buf->cb_rd_lock);
+	mutex_unlock(&buf->cb_wr_lock);
+}
+
+void maxfg_free_capture_buf(struct maxfg_capture_buf *buf)
+{
+	if (!buf)
+		return;
+
+	if (buf->cb.buf && buf->slots > 0)
+		kfree(buf->cb.buf);
+
+	mutex_destroy(&buf->cb_wr_lock);
+	mutex_destroy(&buf->cb_rd_lock);
+
+	buf->cb.buf = NULL;
+	buf->slots = 0;
+}
+
+static inline int maxfg_read_registers(struct maxfg_capture_regs *regs, u16 *buffer)
+{
+	int ret, idx;
+
+	for (idx = 0; idx < regs->reg_cnt; idx++) {
+		ret = max17x0x_reg_read(regs->regmap, regs->tag[idx], &buffer[idx]);
+		if (ret < 0) {
+			pr_err("failed to reg_tag(%u) %d\n", regs->tag[idx], ret);
+			return ret;
+		}
+	}
+
+	return 0;
+}
+
+int maxfg_capture_registers(struct maxfg_capture_buf *buf)
+{
+	struct maxfg_capture_config *config = &buf->config;
+	const int data_size = config->data_size;
+	void *latest_entry;
+	int head, tail, ret;
+	u16 *reg_val;
+
+	mutex_lock(&buf->cb_wr_lock);
+
+	head = buf->cb.head;
+	tail = READ_ONCE(buf->cb.tail);
+
+	/* if buffer is full, drop the last entry */
+	if (CIRC_SPACE(head, tail, buf->slots) == 0) {
+		mutex_lock(&buf->cb_rd_lock);
+		WRITE_ONCE(buf->cb.tail, (tail + 1) & (buf->slots - 1));
+		mutex_unlock(&buf->cb_rd_lock);
+	}
+
+	reg_val = (u16 *)&buf->cb.buf[head * data_size];
+	latest_entry = reg_val;
+
+	ret = maxfg_read_registers(&config->normal, reg_val);
+	if (ret < 0)
+		goto exit_done;
+
+	reg_val += config->normal.reg_cnt;
+
+	ret = maxfg_read_registers(&config->debug, reg_val);
+	if (ret < 0)
+		goto exit_done;
+
+	smp_wmb();
+	WRITE_ONCE(buf->cb.head, (head + 1) & (buf->slots - 1));
+
+	buf->latest_entry = latest_entry;
+
+exit_done:
+	mutex_unlock(&buf->cb_wr_lock);
+	return ret;
+}
+
+int maxfg_capture_to_cstr(struct maxfg_capture_config *config, u16 *reg_val,
+			  char *str_buf, int buf_len)
+{
+	const struct max17x0x_reg *fg_reg;
+	int reg_idx;
+	int len = 0;
+
+	for (reg_idx = 0; reg_idx < config->normal.reg_cnt && len < buf_len; reg_idx++) {
+		fg_reg = max17x0x_find_by_tag(config->normal.regmap,
+					      config->normal.tag[reg_idx]);
+		if (!fg_reg)
+			return len;
+
+		len += scnprintf(&str_buf[len], buf_len - len, "%02X:%04X ",
+				 fg_reg->reg, reg_val[reg_idx]);
+	}
+
+	reg_val += config->normal.reg_cnt;
+
+	for (reg_idx = 0; reg_idx < config->debug.reg_cnt && len < buf_len; reg_idx++) {
+		fg_reg = max17x0x_find_by_tag(config->debug.regmap,
+					      config->debug.tag[reg_idx]);
+		if (!fg_reg)
+			return len;
+
+		len += scnprintf(&str_buf[len], buf_len - len, "%02X:%04X ",
+				 fg_reg->reg, reg_val[reg_idx]);
+	}
+
+	return len;
+}
+
+int maxfg_show_captured_buffer(struct maxfg_capture_buf *buf,
+			       char *str_buf, int buf_len)
+{
+	struct maxfg_capture_config *config = &buf->config;
+	const int data_size = config->data_size;
+	int head, tail, count, to_end, idx, rt;
+	u16 *reg_val;
+
+	if (!buf)
+		return -EINVAL;
+
+	mutex_lock(&buf->cb_rd_lock);
+
+	head = READ_ONCE(buf->cb.head);
+	tail = buf->cb.tail;
+
+	count = CIRC_CNT(head, tail, buf->slots);
+	rt = scnprintf(&str_buf[0], buf_len, "%s (%d):\n", config->name, count);
+
+	if (count == 0)
+		goto maxfg_show_captured_buffer_exit;
+
+	to_end = CIRC_CNT_TO_END(head, tail, buf->slots);
+
+	for (idx = 0; idx < to_end && rt < buf_len; idx++) {
+		reg_val = (u16 *)&buf->cb.buf[(tail + idx) * data_size];
+		rt += maxfg_capture_to_cstr(config, reg_val, &str_buf[rt],
+					    buf_len - rt);
+		rt += scnprintf(&str_buf[rt], buf_len - rt, "\n");
+	}
+
+	count -= idx;
+
+	for (idx = 0; idx < count && rt < buf_len; idx++) {
+		reg_val = (u16 *)&buf->cb.buf[idx * data_size];
+		rt += maxfg_capture_to_cstr(config, reg_val, &str_buf[rt],
+					    buf_len - rt);
+		rt += scnprintf(&str_buf[rt], buf_len - rt, "\n");
+	}
+
+maxfg_show_captured_buffer_exit:
+	mutex_unlock(&buf->cb_rd_lock);
+	return rt;
+}
+
+/*
+ * data in prev_val follows the order of fg_learning_param[]
+ *  prev_val[0]: fcnom
+ *  prev_val[1]: dpacc
+ *  prev_val[2]: dqacc
+ *  prev_val[7]: fstat
+ */
+bool maxfg_ce_relaxed(struct max17x0x_regmap *regmap, const u16 relax_mask,
+                      const u16 *prev_val)
+{
+	u16 fstat, fcnom, dpacc, dqacc;
+	int ret;
+
+	ret = max17x0x_reg_read(regmap, MAXFG_TAG_fstat, &fstat);
+	if (ret < 0)
+		return false;
+
+	ret = max17x0x_reg_read(regmap, MAXFG_TAG_fcnom, &fcnom);
+	if (ret < 0)
+		return false;
+
+	ret = max17x0x_reg_read(regmap, MAXFG_TAG_dpacc, &dpacc);
+	if (ret < 0)
+		return false;
+
+	ret = max17x0x_reg_read(regmap, MAXFG_TAG_dqacc, &dqacc);
+	if (ret < 0)
+		return false;
+
+	/*
+	 * log when relaxed state changes, when fcnom, dpacc, dqacc change
+	 * TODO: log only when dpacc, dqacc or fcnom change and simply
+	 * count the relaxation event otherwise.
+	 */
+	return (fstat & relax_mask) != (prev_val[7] & relax_mask) ||
+		dpacc != prev_val[1] || dqacc != prev_val[2] ||
+		fcnom != prev_val[0];
+}
diff --git a/maxfg_logging.h b/maxfg_logging.h
new file mode 100644
index 0000000..ac1e63b
--- /dev/null
+++ b/maxfg_logging.h
@@ -0,0 +1,57 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (C) 2024 Google LLC
+ */
+
+#ifndef MAXFG_LOGGING_H_
+#define MAXFG_LOGGING_H_
+
+#include <linux/circ_buf.h>
+
+#define MAX_FG_LEARN_PARAM_MAX_HIST 32
+#define MAX_FG_CAPTURE_CONFIG_NAME_MAX 32
+
+struct maxfg_capture_regs {
+	struct max17x0x_regmap *regmap;
+	const enum max17x0x_reg_tags *tag;
+	int reg_cnt;
+};
+
+/* a configuration can simply be a list of tags, a regmap and a name */
+struct maxfg_capture_config {
+	char name[MAX_FG_CAPTURE_CONFIG_NAME_MAX];
+	struct maxfg_capture_regs normal;
+	struct maxfg_capture_regs debug;
+	int data_size;
+};
+
+/* only one configuration now */
+struct maxfg_capture_buf {
+	struct maxfg_capture_config config;
+
+	int slots;
+	struct circ_buf cb;
+	void *latest_entry;
+	struct mutex cb_wr_lock;
+	struct mutex cb_rd_lock;
+};
+
+void maxfg_init_fg_learn_capture_config(struct maxfg_capture_config *config,
+					struct max17x0x_regmap *regmap,
+					struct max17x0x_regmap *debug_regmap);
+
+int maxfg_alloc_capture_buf(struct maxfg_capture_buf *buf, int slots);
+void maxfg_clear_capture_buf(struct maxfg_capture_buf *buf);
+void maxfg_free_capture_buf(struct maxfg_capture_buf *buf);
+
+int maxfg_capture_registers(struct maxfg_capture_buf *buf);
+
+int maxfg_show_captured_buffer(struct maxfg_capture_buf *buf,
+			       char *str_buf, int buf_len);
+int maxfg_capture_to_cstr(struct maxfg_capture_config *config, u16 *reg_val,
+			  char *str_buf, int buf_len);
+
+bool maxfg_ce_relaxed(struct max17x0x_regmap *regmap, const u16 relax_mask,
+		      const u16 *prev_val);
+
+#endif
\ No newline at end of file