N.B., Cannot use 32-bit programmable interrupt timer (PIT) to trigger periodic DMA due to hardware bug.

See here.

The solution shown below uses the 16-bit programmable delay block (PDB).

Disadvantages to using PDB:

  • Lower resolution counter compared to PIT (16-bit vs 32-bit).
    • In practice, this limits the maximum timer period to about 1.5 seconds.
  • There is only one PDB. Using it for ADC means it cannot be used for another task. Note that there are four different programmable interrupt timers.

Advantages to using PDB:

  • It works!

In [ ]:
import pandas as pd


def get_pdb_divide_params(frequency, F_BUS=int(48e6)):
    mult_factor = np.array([1, 10, 20, 40])
    prescaler = np.arange(8)

    clock_divide = (pd.DataFrame([[i, m, p, m * (1 << p)]
                                  for i, m in enumerate(mult_factor)
                                  for p in prescaler],
                                 columns=['mult_', 'mult_factor',
                                          'prescaler', 'combined'])
                    .drop_duplicates(subset=['combined'])
                    .sort_values('combined', ascending=True))
    clock_divide['clock_mod'] = (F_BUS / frequency
                                 / clock_divide.combined).astype(int)
    return clock_divide.loc[clock_divide.clock_mod <= 0xffff]

Overview

Use linked DMA channels to perform "scan" across multiple ADC input channels.

After each scan, use DMA scatter chain to write the converted ADC values to a separate output array for each ADC channel. The length of the output array to allocate for each ADC channel is determined by the sample_count in the example below.

See diagram below.

Channel configuration

  • DMA channel $i$ copies conesecutive SC1A configurations to the ADC SC1A register. Each SC1A configuration selects an analog input channel.
    • Channel $i$ is initially triggered by software trigger (i.e., DMA_SSRT = i), starting the ADC conversion for the first ADC channel configuration.
    • Loading of subsequent ADC channel configurations is triggered through minor loop linking of DMA channel $ii$ to DMA channel $i$.
  • DMA channel $ii$ is triggered by ADC conversion complete (i.e., COCO), and copies the output result of the ADC to consecutive locations in the result array.
    • Channel $ii$ has minor loop link set to channel $i$, which triggers the loading of the next channel SC1A configuration to be loaded immediately after the current ADC result has been copied to the result array.
  • After $n$ triggers of channel $i$, the result array contains $n$ ADC results, one result per channel in the SC1A table.
    • N.B., Only the trigger for the first ADC channel is an explicit software trigger. All remaining triggers occur through minor-loop DMA channel linking from channel $ii$ to channel $i$.
  • After each scan through all ADC channels is complete, the ADC readings are scattered using the selected "scatter" DMA channel through a major-loop link between DMA channel $ii$ and the "scatter" channel.

Device

Connect to device


In [ ]:
import arduino_helpers.hardware.teensy as teensy
from arduino_rpc.protobuf import resolve_field_values
from teensy_minimal_rpc import SerialProxy
import teensy_minimal_rpc.DMA as DMA
import teensy_minimal_rpc.ADC as ADC
import teensy_minimal_rpc.SIM as SIM
import teensy_minimal_rpc.PIT as PIT


# Disconnect from existing proxy (if available)
try:
    del proxy
except NameError:
    pass

proxy = SerialProxy()
proxy.pin_mode(teensy.LED_BUILTIN, 1)

In [ ]:
from IPython.display import display

proxy.update_sim_SCGC6(SIM.R_SCGC6(PDB=True))
sim_scgc6 = SIM.R_SCGC6.FromString(proxy.read_sim_SCGC6().tostring())
display(resolve_field_values(sim_scgc6)[['full_name', 'value']].T)

Test periodic ADC scan using PDB


In [ ]:
dma_channel_scatter = 0
dma_channel_i = 1
dma_channel_ii = 2

In [ ]:
import numpy as np


PDB0_IDLY = 0x4003600C  # Interrupt Delay Register
PDB0_SC = 0x40036000  # Status and Control Register
PDB0_MOD = 0x40036004  # Modulus Register

PDB_SC_PDBEIE = 0x00020000  # Sequence Error Interrupt Enable
PDB_SC_SWTRIG = 0x00010000  # Software Trigger
PDB_SC_DMAEN = 0x00008000  # DMA Enable
PDB_SC_PDBEN = 0x00000080  # PDB Enable
PDB_SC_PDBIF = 0x00000040  # PDB Interrupt Flag
PDB_SC_PDBIE = 0x00000020  # PDB Interrupt Enable.
PDB_SC_CONT = 0x00000002  # Continuous Mode Enable
PDB_SC_LDOK = 0x00000001  # Load OK


def PDB_SC_TRGSEL(n): return (((n) & 15) << 8)  # Trigger Input Source Select
def PDB_SC_PRESCALER(n): return (((n) & 7) << 12)  # Prescaler Divider Select
def PDB_SC_MULT(n): return (((n) & 3) << 2)  # Multiplication Factor
def PDB_SC_LDMOD(n): return (((n) & 3) << 18)  # Load Mode Select


# PDB0_IDLY = 1; // the pdb interrupt happens when IDLY is equal to CNT+1
proxy.mem_cpy_host_to_device(PDB0_IDLY, np.uint32(1).tostring())

#             software trigger    enable PDB     continuous
PDB_CONFIG = (PDB_SC_TRGSEL(15) | PDB_SC_PDBEN | PDB_SC_CONT |  PDB_SC_LDMOD(0))

clock_divide = get_pdb_divide_params(1).iloc[0]
PDB0_SC_ = (PDB_CONFIG | PDB_SC_PRESCALER(clock_divide.prescaler) |
            PDB_SC_MULT(clock_divide.mult_) | 
            PDB_SC_DMAEN | PDB_SC_LDOK)  # load all new values
proxy.mem_cpy_host_to_device(PDB0_SC, np.uint32(PDB0_SC_).tostring())

# PDB0_MOD = (uint16_t)(mod-1);
proxy.mem_cpy_host_to_device(PDB0_MOD, np.uint32(clock_divide.clock_mod).tostring())

Configure ADC sample rate, etc.


In [ ]:
# Set ADC parameters
proxy.setAveraging(4, teensy.ADC_0)
proxy.setResolution(10, teensy.ADC_0)
proxy.setConversionSpeed(teensy.ADC_MED_SPEED, teensy.ADC_0)
proxy.setSamplingSpeed(teensy.ADC_MED_SPEED, teensy.ADC_0)
proxy.update_adc_registers(
    teensy.ADC_0,
    ADC.Registers(CFG2=ADC.R_CFG2(MUXSEL=ADC.R_CFG2.B)))

Pseudo-code to set DMA channel $i$ to be triggered by ADC0 conversion complete.

DMAMUX0_CFGi[SOURCE] = DMAMUX_SOURCE_ADC0  // Route ADC0 as DMA channel source.
DMAMUX0_CFGi[TRIG] = 0  // Disable periodic trigger.
DMAMUX0_CFGi[ENBL] = 1  // Enable the DMAMUX configuration for channel.

DMA_ERQ[i] = 1  // DMA request input signals and this enable request flag
                // must be asserted before a channel’s hardware service
                // request is accepted (21.3.3/394).
DMA_SERQ = i  // Can use memory mapped convenience register to set instead.

Set DMA mux source for channel 0 to ADC0


In [ ]:
DMAMUX_SOURCE_ADC0 = 40  # from `kinetis.h`
DMAMUX_SOURCE_ADC1 = 41  # from `kinetis.h`

#    DMAMUX0_CFGi[SOURCE] = DMAMUX_SOURCE_ADC0  // Route ADC0 as DMA channel source.
#    DMAMUX0_CFGi[TRIG] = 0  // Disable periodic trigger.
#    DMAMUX0_CFGi[ENBL] = 1  // Enable the DMAMUX configuration for channel.
proxy.update_dma_mux_chcfg(dma_channel_ii,
                           DMA.MUX_CHCFG(SOURCE=DMAMUX_SOURCE_ADC0,
                                         TRIG=False,
                                         ENBL=True))
proxy.enableDMA(teensy.ADC_0)

In [ ]:
proxy.DMA_registers().loc['']

Analog channel list

  • List of channels to sample.
  • Map channels from Teensy references (e.g., A0, A1, etc.) to the Kinetis analog pin numbers using the adc.CHANNEL_TO_SC1A_ADC0 mapping.

In [ ]:
import re

import numpy as np
import pandas as pd
import arduino_helpers.hardware.teensy.adc as adc

# The number of samples to record for each ADC channel.
sample_count = 32

teensy_analog_channels = ['A0', 'A1', 'A0', 'A3', 'A0']
sc1a_pins = pd.Series(dict([(v, adc.CHANNEL_TO_SC1A_ADC0[getattr(teensy, v)])
                            for v in dir(teensy) if re.search(r'^A\d+', v)]))
channel_sc1as = np.array(sc1a_pins[teensy_analog_channels].tolist(), dtype='uint32')

Allocate and initialize device arrays

  • SD1A register configuration for each ADC channel in the channel_sc1as list.
    • Copy channel_sc1as list to device.
  • ADC result array
    • Initialize to zero.

In [ ]:
proxy.free_all()

N = np.dtype('uint16').itemsize * channel_sc1as.size

# Allocate source array
adc_result_addr = proxy.mem_alloc(N)

# Fill result array with zeros
proxy.mem_fill_uint8(adc_result_addr, 0, N)

# Copy channel SC1A configurations to device memory
adc_sda1s_addr = proxy.mem_aligned_alloc_and_set(4, channel_sc1as.view('uint8'))

# Allocate source array
samples_addr = proxy.mem_alloc(sample_count * N)

tcds_addr = proxy.mem_aligned_alloc(32, sample_count * 32)
hw_tcds_addr = 0x40009000
tcd_addrs = [tcds_addr + 32 * i for i in xrange(sample_count)]
hw_tcd_addrs = [hw_tcds_addr + 32 * i for i in xrange(sample_count)]

# Fill result array with zeros
proxy.mem_fill_uint8(samples_addr, 0, sample_count * N)

# Create Transfer Control Descriptor configuration for first chunk, encoded
# as a Protocol Buffer message.
tcd0_msg = DMA.TCD(CITER_ELINKNO=DMA.R_TCD_ITER_ELINKNO(ITER=1),
                   BITER_ELINKNO=DMA.R_TCD_ITER_ELINKNO(ITER=1),
                   ATTR=DMA.R_TCD_ATTR(SSIZE=DMA.R_TCD_ATTR._16_BIT,
                                       DSIZE=DMA.R_TCD_ATTR._16_BIT),
                   NBYTES_MLNO=channel_sc1as.size * 2,
                   SADDR=int(adc_result_addr),
                   SOFF=2,
                   SLAST=-channel_sc1as.size * 2,
                   DADDR=int(samples_addr),
                   DOFF=2 * sample_count,
                   DLASTSGA=int(tcd_addrs[1]),
                   CSR=DMA.R_TCD_CSR(START=0, DONE=False, ESG=True))

# Convert Protocol Buffer encoded TCD to bytes structure.
tcd0 = proxy.tcd_msg_to_struct(tcd0_msg)

# Create binary TCD struct for each TCD protobuf message and copy to device
# memory.
for i in xrange(sample_count):
    tcd_i = tcd0.copy()
    tcd_i['SADDR'] = adc_result_addr
    tcd_i['DADDR'] = samples_addr + 2 * i
    tcd_i['DLASTSGA'] = tcd_addrs[(i + 1) % len(tcd_addrs)]
    tcd_i['CSR'] |= (1 << 4)
    if i == (sample_count - 1):  # Last sample, so trigger major loop interrupt
        print 'Enable major loop interrupt for sample %d' % i
        tcd_i['CSR'] |= (1 << 1)  # Set `INTMAJOR` (21.3.29/426)
    proxy.mem_cpy_host_to_device(tcd_addrs[i], tcd_i.tostring())

# Load initial TCD in scatter chain to DMA channel chosen to handle scattering.
proxy.mem_cpy_host_to_device(hw_tcd_addrs[dma_channel_scatter],
                             tcd0.tostring())
proxy.attach_dma_interrupt(dma_channel_scatter)

print 'ADC results:', proxy.mem_cpy_device_to_host(adc_result_addr, N).view('uint16')
print 'Analog pins:', proxy.mem_cpy_device_to_host(adc_sda1s_addr, len(channel_sc1as) *
                                                   channel_sc1as.dtype.itemsize).view('uint32')

Configure DMA channel $i$


In [ ]:
ADC0_SC1A = 0x4003B000  # ADC status and control registers 1

sda1_tcd_msg = DMA.TCD(CITER_ELINKNO=DMA.R_TCD_ITER_ELINKNO(ELINK=False, ITER=channel_sc1as.size),
                       BITER_ELINKNO=DMA.R_TCD_ITER_ELINKNO(ELINK=False, ITER=channel_sc1as.size),
                       ATTR=DMA.R_TCD_ATTR(SSIZE=DMA.R_TCD_ATTR._32_BIT,
                                           DSIZE=DMA.R_TCD_ATTR._32_BIT),
                       NBYTES_MLNO=4,
                       SADDR=int(adc_sda1s_addr),
                       SOFF=4,
                       SLAST=-channel_sc1as.size * 4,
                       DADDR=int(ADC0_SC1A),
                       DOFF=0,
                       DLASTSGA=0,
                       CSR=DMA.R_TCD_CSR(START=0, DONE=False))

proxy.update_dma_TCD(dma_channel_i, sda1_tcd_msg)

Configure DMA channel $ii$


In [ ]:
ADC0_RA = 0x4003B010  # ADC data result register
ADC0_RB = 0x4003B014  # ADC data result register


tcd_msg = DMA.TCD(CITER_ELINKYES=DMA.R_TCD_ITER_ELINKYES(ELINK=True, LINKCH=1, ITER=channel_sc1as.size),
                  BITER_ELINKYES=DMA.R_TCD_ITER_ELINKYES(ELINK=True, LINKCH=1, ITER=channel_sc1as.size),
                  ATTR=DMA.R_TCD_ATTR(SSIZE=DMA.R_TCD_ATTR._16_BIT,
                                      DSIZE=DMA.R_TCD_ATTR._16_BIT),
                  NBYTES_MLNO=2,
                  SADDR=ADC0_RA,
                  SOFF=0,
                  SLAST=0,
                  DADDR=int(adc_result_addr),
                  DOFF=2,
                  DLASTSGA=-channel_sc1as.size * 2,
                  CSR=DMA.R_TCD_CSR(START=0, DONE=False,
                                    MAJORELINK=True,
                                    MAJORLINKCH=dma_channel_scatter))

proxy.update_dma_TCD(dma_channel_ii, tcd_msg)

# DMA request input signals and this enable request flag
# must be asserted before a channel’s hardware service
# request is accepted (21.3.3/394).
#    DMA_SERQ = i
proxy.update_dma_registers(DMA.Registers(SERQ=dma_channel_ii))

Trigger sample scan across selected ADC channels


In [ ]:
# Clear output array to zero.
proxy.mem_fill_uint8(adc_result_addr, 0, N)
proxy.mem_fill_uint8(samples_addr, 0, sample_count * N)

In [ ]:
# Software trigger channel $i$ to copy *first* SC1A configuration, which
# starts ADC conversion for the first channel.
#
# Conversions for subsequent ADC channels are triggered through minor-loop
# linking from DMA channel $ii$ to DMA channel $i$ (*not* through explicit
# software trigger).
print 'ADC results:'
for i in xrange(sample_count):
    proxy.update_dma_registers(DMA.Registers(SSRT=dma_channel_i))

    # Display converted ADC values (one value per channel in `channel_sd1as` list).
    print '  Iteration %s:' % i, proxy.mem_cpy_device_to_host(adc_result_addr, N).view('uint16')

print ''

Set DMA channel $i$ to be triggered by PDB (when PDB enabled)


In [ ]:
proxy.update_dma_mux_chcfg(dma_channel_i,
                           DMA.MUX_CHCFG(SOURCE=48,
                                         TRIG=False,
                                         ENBL=True))
proxy.update_dma_registers(DMA.Registers(SERQ=dma_channel_i))

In [ ]:
%matplotlib inline

In [ ]:
PDB0_SC_ = 0
proxy.mem_cpy_host_to_device(PDB0_SC, np.uint32(PDB0_SC_).tostring())

In [ ]:
proxy.mem_cpy_device_to_host(PDB0_SC, 4)

Seems to work at 100kHz.....


In [ ]:
# Set sampling frequency
f_sample = 150e3

# Determine timing parameters to meet specified sampling frequency.
clock_divide = get_pdb_divide_params(f_sample).iloc[0]

# Configure Programmable Delay Block (PDB) register state for sampling frequency.
PDB0_SC_ = (PDB_CONFIG | PDB_SC_PRESCALER(clock_divide.prescaler) |
            PDB_SC_MULT(clock_divide.mult_) | 
            PDB_SC_DMAEN | PDB_SC_LDOK)  # load all new values
proxy.mem_cpy_host_to_device(PDB0_SC, np.uint32(PDB0_SC_).tostring())
PDB0_SC_ = (PDB_CONFIG | PDB_SC_PRESCALER(clock_divide.prescaler) |
            PDB_SC_DMAEN | PDB_SC_MULT(clock_divide.mult_) |
            PDB_SC_SWTRIG)  # start the counter!
# Copy configured PDB register state to device hardware register.
proxy.mem_cpy_host_to_device(PDB0_SC, np.uint32(PDB0_SC_).tostring())

# **N.B.,** Timer will be stopped by the scatter DMA channel major loop interrupt
# handler after `sample_count` samples have been collected.

In [ ]:
print 'Samples by channel:'
device_dst_data = proxy.mem_cpy_device_to_host(samples_addr, sample_count * N)
df_adc_results = pd.DataFrame(device_dst_data.view('uint16').reshape(-1, sample_count).T,
                              columns=teensy_analog_channels)
df_adc_results.plot(ylim=(-5, 1030))
# df_adc_results

In [ ]:
proxy.last_dma_channel_done()

In [ ]:
proxy.DMA_registers().loc['']

In [ ]: