[PATCH] iio: adc: aspeed: Reserve battery sensing channel for on-demand use

Billy Tsai
2026-03-13T10:02:26Z

For controllers with battery sensing capability (AST2600/AST2700), the last
channel uses a different circuit design optimized for battery voltage
measurement. This channel should not be enabled by default along with other
channels to avoid potential interference and power efficiency issues.

Changes made:
- Introduce aspeed_adc_get_active_channels() to return the number of
channels that should be enabled by default
- For battery sensing capable controllers, exclude the last channel
from the default channel enable mask
- Enable the battery sensing channel only when explicitly accessed
via read_raw()
- Replace hardcoded channel numbers with ASPEED_ADC_BATTERY_CHANNEL macro
- Add helper functions for cleaner channel management

This ensures optimal power efficiency for normal ADC operations while
maintaining full functionality when battery sensing is needed.

Signed-off-by: Billy Tsai <billy_tsai@aspeedtech.com>
---
drivers/iio/adc/aspeed_adc.c | 88 ++++++++++++++++++++++++++++++++++++++------
1 file changed, 76 insertions(+), 12 deletions(-)

diff --git a/drivers/iio/adc/aspeed_adc.c b/drivers/iio/adc/aspeed_adc.c
index 4be44c524b4d..376a024341e3 100644
--- a/drivers/iio/adc/aspeed_adc.c
+++ b/drivers/iio/adc/aspeed_adc.c
@@ -73,6 +73,31 @@
#define ASPEED_ADC_CTRL_CHANNEL GENMASK(31, 16)
#define ASPEED_ADC_CTRL_CHANNEL_ENABLE(ch) FIELD_PREP(ASPEED_ADC_CTRL_CHANNEL, BIT(ch))

+/*
+ * Enable multiple consecutive channels starting from channel 0.
+ * This creates a bitmask for channels 0 to (num_channels - 1).
+ * For example: num_channels=3 creates mask 0x0007 (channels 0,1,2)
+ */
+static inline u32 aspeed_adc_channels_mask(unsigned int num_channels)
+{
+ if (num_channels == 0)
+ return 0;
+ if (num_channels >= 16)
+ return GENMASK(15, 0);
+ return GENMASK(num_channels - 1, 0);
+}
+
+/*
+ * Helper function to enable multiple channels in the control register
+ */
+static inline u32 aspeed_adc_enable_channels(unsigned int num_channels)
+{
+ return FIELD_PREP(ASPEED_ADC_CTRL_CHANNEL, aspeed_adc_channels_mask(num_channels));
+}
+
+/* Battery sensing is typically on the last channel */
+#define ASPEED_ADC_BATTERY_CHANNEL 7
+
#define ASPEED_ADC_INIT_POLLING_TIME 500
#define ASPEED_ADC_INIT_TIMEOUT 500000
/*
@@ -121,6 +146,18 @@ struct aspeed_adc_data {
struct adc_gain battery_mode_gain;
};

+static inline unsigned int aspeed_adc_get_active_channels(const struct aspeed_adc_data *data)
+{
+ /*
+ * For controllers with battery sensing capability, the last channel
+ * is reserved for battery sensing and should not be included in
+ * normal channel operations.
+ */
+ if (data->model_data->bat_sense_sup)
+ return data->model_data->num_channels - 1;
+ return data->model_data->num_channels;
+}
+
#define ASPEED_CHAN(_idx, _data_reg_addr) { \
.type = IIO_VOLTAGE, \
.indexed = 1, \
@@ -281,13 +318,35 @@ static int aspeed_adc_read_raw(struct iio_dev *indio_dev,
int *val, int *val2, long mask)
{
struct aspeed_adc_data *data = iio_priv(indio_dev);
- u32 adc_engine_control_reg_val;
+ u32 adc_engine_control_reg_val = readl(data->base + ASPEED_REG_ENGINE_CONTROL);

switch (mask) {
case IIO_CHAN_INFO_RAW:
- if (data->battery_sensing && chan->channel == 7) {
- adc_engine_control_reg_val =
- readl(data->base + ASPEED_REG_ENGINE_CONTROL);
+ /*
+ * For battery sensing capable controllers, we need to enable
+ * the specific channel before reading. This is required because
+ * the battery channel may not be enabled by default.
+ */
+ if (data->model_data->bat_sense_sup &&
+ chan->channel == ASPEED_ADC_BATTERY_CHANNEL) {
+ u32 ctrl_reg = adc_engine_control_reg_val & ~ASPEED_ADC_CTRL_CHANNEL;
+
+ ctrl_reg |= ASPEED_ADC_CTRL_CHANNEL_ENABLE(chan->channel);
+ writel(ctrl_reg, data->base + ASPEED_REG_ENGINE_CONTROL);
+ /*
+ * After enable a new channel need to wait some time for adc stable
+ * Experiment result is 1ms.
+ */
+ mdelay(1);
+ }
+
+ /*
+ * Battery sensing mode requires special configuration:
+ * - Set channel 7 to battery mode
+ * - Enable battery sensing functionality
+ * - Apply voltage divider compensation
+ */
+ if (data->battery_sensing && chan->channel == ASPEED_ADC_BATTERY_CHANNEL) {
writel(adc_engine_control_reg_val |
FIELD_PREP(ASPEED_ADC_CH7_MODE,
ASPEED_ADC_CH7_BAT) |
@@ -301,15 +360,15 @@ static int aspeed_adc_read_raw(struct iio_dev *indio_dev,
*val = readw(data->base + chan->address);
*val = (*val * data->battery_mode_gain.mult) /
data->battery_mode_gain.div;
- /* Restore control register value */
- writel(adc_engine_control_reg_val,
- data->base + ASPEED_REG_ENGINE_CONTROL);
} else
*val = readw(data->base + chan->address);
+ /* Restore control register value */
+ writel(adc_engine_control_reg_val,
+ data->base + ASPEED_REG_ENGINE_CONTROL);
return IIO_VAL_INT;

case IIO_CHAN_INFO_OFFSET:
- if (data->battery_sensing && chan->channel == 7)
+ if (data->battery_sensing && chan->channel == ASPEED_ADC_BATTERY_CHANNEL)
*val = (data->cv * data->battery_mode_gain.mult) /
data->battery_mode_gain.div;
else
@@ -607,10 +666,15 @@ static int aspeed_adc_probe(struct platform_device *pdev)
}

aspeed_adc_compensation(indio_dev);
- /* Start all channels in normal mode. */
- adc_engine_control_reg_val =
- readl(data->base + ASPEED_REG_ENGINE_CONTROL);
- adc_engine_control_reg_val |= ASPEED_ADC_CTRL_CHANNEL;
+ adc_engine_control_reg_val = readl(data->base + ASPEED_REG_ENGINE_CONTROL);
+ /*
+ * Enable channels for normal operation. For battery sensing capable
+ * controllers, the battery channel is handled separately and is not
+ * included in the normal channel enable mask.
+ */
+ adc_engine_control_reg_val |=
+ aspeed_adc_enable_channels(aspeed_adc_get_active_channels(data));
+
writel(adc_engine_control_reg_val,
data->base + ASPEED_REG_ENGINE_CONTROL);

---
base-commit: 6de23f81a5e08be8fbf5e8d7e9febc72a5b5f27f
change-id: 20260313-adc-479b0ab09bae

Best regards,
--
Billy Tsai <billy_tsai@aspeedtech.com>