I am a little embarrassed to ask this because I am certain it is in the reference manual for our CPU (K65) but I wonder if anyone in the community has done this.
Is it possible to:
1. Software configure the A/D peripheral (16 bit samples, averaging, sample time, etc)
2. Software configure the DMA peripheral (memory buffers, 30 A/D channel numbers, etc)
3. Software configure a timer (PIT, PDB or other) to trigger A/D conversions at a predetermined rate.
And then let these hardware peripherals perform the A/D trigger, read, store, mux cycle without software involvement?
Anyone have any experience at this?
Thanks Derek!
Hi Mark
Here is one method (from the uTasker project).
#define ADC_CHANNELS 4
static unsigned long ulCalibrate = ADC_CALIBRATE;
static unsigned short usADCbuffer = {0}; // output buffer for ADC samples
static const unsigned long ADC_Mux_values[ADC_CHANNELS] = {(ADC_CFG2_MUXSEL_B | ADC_SAMPLE_LONG_PLUS_12 | ADC_SAMPLE_AVERAGING_32),
(ADC_CFG2_MUXSEL_A | ADC_SAMPLE_LONG_PLUS_12 | ADC_SAMPLE_AVERAGING_32),
(ADC_CFG2_MUXSEL_A | ADC_SAMPLE_LONG_PLUS_12 | ADC_SAMPLE_AVERAGING_32),
(ADC_CFG2_MUXSEL_B | ADC_SAMPLE_LONG_PLUS_12 | ADC_SAMPLE_AVERAGING_32)
};
static const unsigned long ADC_inputs[ADC_CHANNELS] = {(ADC_D1_DIFF | ADC_SC1A_DIFF),
(ADC_SE9_SINGLE),
(ADC_SE14_SINGLE),
(ADC_DP3_SINGLE)
};
void fnStartADC(void)
{
ADC_SETUP adc_setup; // interrupt configuration parameters
PWM_INTERRUPT_SETUP pwm_setup;
adc_setup.int_type = ADC_INTERRUPT; // identifier when configuring
adc_setup.ucDmaChannel = 0;
adc_setup.dma_int_priority = 3; // priority of DMA interrupt the user wants to set
adc_setup.usDmaTriggerSource = DMAMUX0_CHCFG_SOURCE_ADC0; // trigger DMA on ADC conversion ready
adc_setup.pga_gain = PGA_GAIN_OFF; // PGA gain can be specified for certain inputs
adc_setup.int_priority = PRIORITY_ADC; // ADC interrupt priority
adc_setup.int_adc_controller = 0; // ADC controller 0
adc_setup.int_handler = 0;
adc_setup.int_adc_int_type = (ADC_DISABLE_INTS);
adc_setup.int_adc_offset = 0; // no offset
adc_setup.int_adc_bit = ADC_D1_DIFF; // ADC DM3 single-ended
adc_setup.dma_int_handler = _int_scan_ready; // no user DMA interrupt call-back
adc_setup.ptrADC_Buffer = usADCbuffer; // ADC sample buffer to be used
adc_setup.ulADC_buffer_length = sizeof(usADCbuffer); // physical length of the buffer
adc_setup.int_adc_mode = (ulCalibrate | ADC_FULL_BUFFER_DMA | ADC_SELECT_INPUTS_A | ADC_CLOCK_BUS_DIV_2 | ADC_CLOCK_DIVIDE_8 | ADC_SAMPLE_ACTIVATE_LONG | ADC_CONFIGURE_ADC | ADC_REFERENCE_VREF | ADC_CONFIGURE_CHANNEL | ADC_SINGLE_ENDED_INPUT | ADC_SINGLE_SHOT_MODE | ADC_12_BIT_MODE | ADC_ALTERNATIVE_HW_TRIGGER); // hardware triggering (DMA to buffer with interrupt on buffer completion)
adc_setup.int_adc_sample = (ADC_SAMPLE_LONG_PLUS_12 | ADC_SAMPLE_AVERAGING_8); // additional sampling clocks and hardware averaging
adc_setup.hw_trigger = ADC_HW_TRIGGER_TPM1; // trigger on TPM overflow
adc_setup.int_adc_result = 0; // no result is requested
adc_setup.int_adc_speed = (unsigned char)(ADC_SAMPLING_SPEED(5000000)); // 5MHz sampling (must be between 100kHz and 5MHz)
fnConfigureInterrupt((void *)&adc_setup); // configure ADC
ulCalibrate = 0;
pwm_setup.int_type = PWM_INTERRUPT;
pwm_setup.pwm_mode = (PWM_SYS_CLK | PWM_PRESCALER_16 | PWM_NO_OUTPUT | PWM_DMA_CHANNEL_ENABLE | PWM_FULL_BUFFER_DMA_AUTO_REPEAT); // clock PWM timer from the system clock with /16 pre-scaler
pwm_setup.ucDmaChannel = 1;
pwm_setup.usDmaDestination = ADD_ADC0_CFG2;
pwm_setup.usDmaTriggerSource = DMAMUX0_CHCFG_SOURCE_FTM1_C0; // when TPM1 channel 0 fires it triggers the transfer of the next ADC CFG2 setting
pwm_setup.dma_int_handler = 0;
pwm_setup.ptrPWM_Buffer = ADC_Mux_values;
pwm_setup.ulPWM_buffer_length = sizeof(ADC_Mux_values);
pwm_setup.pwm_frequency = PWM_FREQUENCY(1000, 16); // generate 1000Hz time base
pwm_setup.int_handler = 0; // no user interrupt call-back on PWM cycle
pwm_setup.pwm_reference = (_TPM_TIMER_1 | 0); // timer module 1, channel 0
pwm_setup.pwm_value = _PWM_PERCENT(80, pwm_setup.pwm_frequency); // 80% PWM (high/low)
fnConfigureInterrupt((void *)&pwm_setup); // enter configuration
pwm_setup.ucDmaChannel = 2;
pwm_setup.usDmaDestination = ADD_ADC0_SC1A;
pwm_setup.usDmaTriggerSource = DMAMUX0_CHCFG_SOURCE_FTM1_C1; // when TPM1 channel 1 fires it triggers the transfer of the next ADC SC1A setting
pwm_setup.dma_int_handler = 0;
pwm_setup.ptrPWM_Buffer = ADC_inputs;
pwm_setup.ulPWM_buffer_length = sizeof(ADC_inputs);
pwm_setup.pwm_reference = (_TPM_TIMER_1 | 1); // timer module 1, channel 1
pwm_setup.pwm_value = _PWM_PERCENT(90, pwm_setup.pwm_frequency); // 90% PWM (high/low)
fnConfigureInterrupt((void *)&pwm_setup); // enter configuration
}
// Interrupt call back when all samples are ready
//
static void _int_scan_ready(void)
{
}
It used TPM1 to trigger ADC conversions (HW triggers) and sets up two DMA triggers on the TPM0's channels 0 and 1 which 'fire' at 80% and 90% of the sampling cycle.
There are 3 DMA channels involved (the DMA setup is integrated in the ADC and PWM APIs which support DMA register to circular buffer transfers and buffer to register transfers respectively).
The first DMA channel copies the ADC0RA value to the buffer at each ADC conversion completion (after a scan set it will reset to the start of the buffer and the interrupt _int_scan_ready is called so that the collected scan samples can be processed).
The second DMA channel (called after the 80% of the sample cycle (assumed after ADC has completed) copies the input channel (A or B) setting for the next input to be sampled (if all are on inputs A or B this is not needed). [ADC0_CFG2 needs changing if the input changes between A and B - See appendix D of http://www.utasker.com/docs/uTasker/uTaskerADC.pdf for a description of the HW input muxing in the ADC.]
The third DMA channel (called after 90% of the sample cycle) copies the input (the ADC input itself) setting for the next input to be sampled. [ADC0_SC1A is the ADC channel setting].
The ref code uses just 4 inputs but it can be extended to a larger number. The same can be performed for ADC1 in parallel with a different set (ADC1 would need to use TPM2 for trigger). The scan output buffer can be larger (eg. collects 10 sets of scans before completing, and a half-scan interrupt can be used (half-DMA ready) so that a ping-pong processing can be performed (further samples collected whilst first half are processed).
This is just one possible configuration since there are various trigger and DMA possibilities to do about the same thing.
Since sometimes two ADC registers need to be set up it would further be possible to chain DMA channel to do it. The two TPM channel triggers is however simpler to configure. It is possible to trigger the two for a single DMA trigger but it is not advisable since, according to the manuals, this could lead to undefined behavior - although practical tests seems to be fine.
Regards
Mark
P.S. Don't forget that the ADCs need to be calibrated once on first use after reset.
P.P.S. The sample buffer has interleaved samples.
uTasker developer and supporter (+5'000 hours experience on +60 Kinetis derivatives in +80 product developments)
Kinetis: http://www.utasker.com/kinetis.html
Thanks Mark