/* Copyright 2012 The Chromium OS Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ /* LM4-specific ADC module for Chrome EC */ #include "adc.h" #include "atomic.h" #include "clock.h" #include "console.h" #include "common.h" #include "gpio.h" #include "hooks.h" #include "registers.h" #include "task.h" #include "timer.h" #include "util.h" /* Maximum time we allow for an ADC conversion */ #define ADC_TIMEOUT_US SECOND static volatile task_id_t task_waiting_on_ss[LM4_ADC_SEQ_COUNT]; static void configure_gpio(void) { int i, port, mask; /* Use analog function for AIN */ for (i = 0; i < ADC_CH_COUNT; ++i) { if (adc_channels[i].gpio_mask) { mask = adc_channels[i].gpio_mask; port = adc_channels[i].gpio_port; LM4_GPIO_DEN(port) &= ~mask; LM4_GPIO_AMSEL(port) |= mask; } } } /** * Flush an ADC sequencer and initiate a read. * * @param seq Sequencer to read * @return Raw ADC value. */ static int flush_and_read(enum lm4_adc_sequencer seq) { /* * This is currently simple because we can dedicate a sequencer to each * ADC channel. If we have enough channels that's no longer possible, * this code will need to become more complex. For example, we could: * * 1) Read them all using a timer interrupt, and then return the most * recent value? This is lowest-latency for the caller, but won't * return accurate data if read frequently. * * 2) Reserve SS3 for reading a single value, and configure it on each * read? Needs mutex if we could have multiple callers; doesn't matter * if just used for debugging. * * 3) Both? */ volatile uint32_t scratch __attribute__((unused)); int event; /* Empty the FIFO of any previous results */ while (!(LM4_ADC_SSFSTAT(seq) & 0x100)) scratch = LM4_ADC_SSFIFO(seq); /* * This assumes we don't have multiple tasks accessing the same * sequencer. Add mutex lock if needed. */ task_waiting_on_ss[seq] = task_get_current(); /* Clear the interrupt status */ LM4_ADC_ADCISC |= 0x01 << seq; /* Enable interrupt */ LM4_ADC_ADCIM |= 0x01 << seq; /* Initiate sample sequence */ LM4_ADC_ADCPSSI |= 0x01 << seq; /* Wait for interrupt */ event = task_wait_event_mask(TASK_EVENT_ADC_DONE, ADC_TIMEOUT_US); /* Disable interrupt */ LM4_ADC_ADCIM &= ~(0x01 << seq); task_waiting_on_ss[seq] = TASK_ID_INVALID; if (!(event & TASK_EVENT_ADC_DONE)) return ADC_READ_ERROR; /* Read the FIFO and convert to temperature */ return LM4_ADC_SSFIFO(seq); } /** * Configure an ADC sequencer to be dedicated for an ADC input. * * @param seq Sequencer to configure * @param ain_id ADC input to use * @param ssctl Value for sampler sequencer control register * */ static void adc_configure(const struct adc_t *adc) { const enum lm4_adc_sequencer seq = adc->sequencer; /* Configure sample sequencer */ LM4_ADC_ADCACTSS &= ~(0x01 << seq); /* Trigger sequencer by processor request */ LM4_ADC_ADCEMUX = (LM4_ADC_ADCEMUX & ~(0xf << (seq * 4))) | 0x00; /* Sample internal temp sensor */ if (adc->channel == LM4_AIN_NONE) { LM4_ADC_SSMUX(seq) = 0x00; LM4_ADC_SSEMUX(seq) = 0x00; } else { LM4_ADC_SSMUX(seq) = adc->channel & 0xf; LM4_ADC_SSEMUX(seq) = adc->channel >> 4; } LM4_ADC_SSCTL(seq) = adc->flag; /* Enable sample sequencer */ LM4_ADC_ADCACTSS |= 0x01 << seq; } int adc_read_channel(enum adc_channel ch) { const struct adc_t *adc = adc_channels + ch; static uint32_t ch_busy_mask; static struct mutex adc_clock; int rv; /* * TODO(crbug.com/314121): Generalize ADC reads such that any task can * trigger a read of any channel. */ /* * Enable ADC clock and set a bit in ch_busy_mask to signify that this * channel is busy. Note, this function may be called from multiple * tasks, but each channel may be read by only one task. If assert * fails, then it means multiple tasks are trying to read same channel. */ mutex_lock(&adc_clock); ASSERT(!(ch_busy_mask & (1UL << ch))); clock_enable_peripheral(CGC_OFFSET_ADC, 0x1, CGC_MODE_RUN | CGC_MODE_SLEEP); ch_busy_mask |= (1UL << ch); mutex_unlock(&adc_clock); rv = flush_and_read(adc->sequencer); /* * If no ADC channels are busy, then disable ADC clock to conserve * power. */ mutex_lock(&adc_clock); ch_busy_mask &= ~(1UL << ch); if (!ch_busy_mask) clock_disable_peripheral(CGC_OFFSET_ADC, 0x1, CGC_MODE_RUN | CGC_MODE_SLEEP); mutex_unlock(&adc_clock); if (rv == ADC_READ_ERROR) return ADC_READ_ERROR; return rv * adc->factor_mul / adc->factor_div + adc->shift; } /*****************************************************************************/ /* Interrupt handlers */ /** * Handle an interrupt on the specified sample sequencer. */ static void handle_interrupt(int ss) { int id = task_waiting_on_ss[ss]; /* Clear the interrupt status */ LM4_ADC_ADCISC = (0x1 << ss); /* Wake up the task which was waiting on the interrupt, if any */ if (id != TASK_ID_INVALID) task_set_event(id, TASK_EVENT_ADC_DONE); } void ss0_interrupt(void) { handle_interrupt(0); } void ss1_interrupt(void) { handle_interrupt(1); } void ss2_interrupt(void) { handle_interrupt(2); } void ss3_interrupt(void) { handle_interrupt(3); } DECLARE_IRQ(LM4_IRQ_ADC0_SS0, ss0_interrupt, 2); DECLARE_IRQ(LM4_IRQ_ADC0_SS1, ss1_interrupt, 2); DECLARE_IRQ(LM4_IRQ_ADC0_SS2, ss2_interrupt, 2); DECLARE_IRQ(LM4_IRQ_ADC0_SS3, ss3_interrupt, 2); /*****************************************************************************/ /* Console commands */ #ifdef CONFIG_CMD_ECTEMP static int command_ectemp(int argc, char **argv) { int t = adc_read_channel(ADC_CH_EC_TEMP); ccprintf("EC temperature is %d K = %d C\n", t, K_TO_C(t)); return EC_SUCCESS; } DECLARE_CONSOLE_COMMAND(ectemp, command_ectemp, NULL, "Print EC temperature"); #endif /*****************************************************************************/ /* Initialization */ static void adc_init(void) { int i; /* Configure GPIOs */ configure_gpio(); /* * Temporarily enable the PLL when turning on the clock to the ADC * module, to work around chip errata (10.4). No need to notify * other modules; the PLL isn't enabled long enough to matter. */ clock_enable_pll(1, 0); /* Enable ADC0 module in run and sleep modes. */ clock_enable_peripheral(CGC_OFFSET_ADC, 0x1, CGC_MODE_RUN | CGC_MODE_SLEEP); /* * Use external voltage references (VREFA+, VREFA-) instead of * VDDA and GNDA. */ LM4_ADC_ADCCTL = 0x01; /* Use internal oscillator */ LM4_ADC_ADCCC = 0x1; /* Disable the PLL now that the ADC is using the internal oscillator */ clock_enable_pll(0, 0); /* No tasks waiting yet */ for (i = 0; i < LM4_ADC_SEQ_COUNT; i++) task_waiting_on_ss[i] = TASK_ID_INVALID; /* Enable IRQs */ task_enable_irq(LM4_IRQ_ADC0_SS0); task_enable_irq(LM4_IRQ_ADC0_SS1); task_enable_irq(LM4_IRQ_ADC0_SS2); task_enable_irq(LM4_IRQ_ADC0_SS3); /* 2**6 = 64x oversampling */ LM4_ADC_ADCSAC = 6; /* Initialize ADC sequencer */ for (i = 0; i < ADC_CH_COUNT; ++i) adc_configure(adc_channels + i); /* Disable ADC0 module until it is needed to conserve power. */ clock_disable_peripheral(CGC_OFFSET_ADC, 0x1, CGC_MODE_RUN | CGC_MODE_SLEEP); } DECLARE_HOOK(HOOK_INIT, adc_init, HOOK_PRIO_INIT_ADC);