Audio double buffering on K66

cancel
Showing results for 
Search instead for 
Did you mean: 

Audio double buffering on K66

1,765 Views
jeffgregorio
Contributor I

Hello! I'm trying to implement a block-based audio processor using double buffering on the input and output and I'm getting some unexpected discontinuities in my output signal. I'm using the Arduino IDE Teensy 3.6, which uses the K66 processor, but I have no debugger capability. The configuration of the peripherals is as follows:

  • ADC0 is hardware triggered by PIT0, which runs at the audio sample rate
  • ADC0 conversion completions trigger transfers on DMA0 from ADC0_RA to dma0_dest_buffer, which is split into halves, input_buffer and proc_buffer
    • DMA0 interrupts on major and half major loop, whereupon the pointers to input_buffer and proc_buffer are swapped
  •  DMA0 is chained to DMA1, which transfers samples to DAC0_DAT0L from dma1_src_buffer, which is split into halves, render_buffer and output_buffer
    • DMA1 interrupts on major and half major loop, whereupon the pointers to render_buffer and output_buffer are swapped, and the contents of proc_buffer are copied into render_buffer after scaling the 16-bit ADC readings to 12 bits for the DAC

The discontinuities are shown in the attached image. Seems that it periodically re-plays samples that have already been sent to the output. The really strange thing is that the spacing of the discontinuities scales with the buffer length, but it's not at the buffer boundaries, as I would expect if I had messed up the double-buffer pointer swaps.

For buffer length 4 (83us) , they are spaced at ~650us, or every 8 buffers

For buffer length 8 (167us), they are spaced at ~2.6ms, or every 16 buffers

For buffer length 16 (333us), they are spaced at ~11ms, or every 32 buffers

For buffer length 32 (667us), they are spaced at ~48ms, or every 64 buffers

For buffer length 64 (1.3ms), they are spaced at ~170ms, or every 128 buffers

Beyond that, the discontinuities are spread so far apart that I can no longer see them on the scope at that time scale. I've been stuck on this problem for a few days, unable to figure out what could be causing that strange pattern.

Lowering the sample rate has no effect. I have also tried filling render_buffer with samples synthesized directly from a wavetable oscillator in dma_ch1_isr(), and I get no discontinuities. I also observe no discontinuities when I print samples from proc_buffer to Arduino's serial plotter from dma_ch0_isr(), so I believe the problem lies somewhere in the DMA transfers or double buffer handling. 

Any help is greatly appreciated. Code shown below:

#define BAUD_RATE (230400)
#define SAMPLE_RATE (48000.0f)
#define PIT_PERIOD ((uint32_t)(F_BUS / SAMPLE_RATE)-1)
#define BUFFER_LEN (16)
#define ADC_RES (16)
#define DAC_RES (12)

static volatile uint16_t dma0_dest_buffer[2*BUFFER_LEN];
static volatile uint16_t * input_buffer = &dma0_dest_buffer[0];
static volatile uint16_t * proc_buffer = &dma0_dest_buffer[BUFFER_LEN];
static volatile uint16_t dma1_src_buffer[2*BUFFER_LEN];
static volatile uint16_t * render_buffer = &dma1_src_buffer[BUFFER_LEN];
static volatile uint16_t * output_buffer = &dma1_src_buffer[0];

// See table 39.1.3.1 for pin mapping
int ch = 5; // ADC0 channel 5 => AD4b, input signal ADC0_SE4b
// (Mapped to pin A0 on Teensy 3.6)

void setup() {


Serial.begin(BAUD_RATE);

// Initialize DMA1 source buffer
for (int i = 0; i < 2*BUFFER_LEN; i++) {


dma0_dest_buffer[i] = (1 << ADC_RES) / 2;
dma1_src_buffer[i] = (1 << DAC_RES) / 2;

}

init_adc0();
init_dac0();
init_dma();
init_pit0();

}

void init_adc0() {

// (13.2.16) System clock gating control register 6
SIM_SCGC6 |= SIM_SCGC6_ADC0; // Enable ADC0 clock

// (39.4.2) ADC Configuration Register 1
ADC0_CFG1 |= ADC_CFG1_ADICLK(0); // Input clock = Bus clock
ADC0_CFG1 |= ADC_CFG1_MODE(3); // Single-ended, 16-bit
ADC0_CFG1 &= ~ADC_CFG1_ADLSMP; // Short sample time
ADC0_CFG1 |= ADC_CFG1_ADIV(1); // Input clock / 2
ADC0_CFG1 &= ~ADC_CFG1_ADLPC; // Normal power configuration
// ADC Clock = F_BUS / 2 = 30MHz

// (39.4.3) ADC Configuration Register 2
ADC0_CFG2 |= ADC_CFG2_ADLSTS(2); // 6 extra ADCK cycles, 10 total sample time
ADC0_CFG2 &= ~ADC_CFG2_ADHSC; // Normal speed conversion
ADC0_CFG2 |= ADC_CFG2_MUXSEL; // ADxxB channels are selected (See table 39.1.3.1)

// (39.4.6) Status and Control Register 2
ADC0_SC2 |= ADC_SC2_REFSEL(0); // Voltage reference = Vcc
ADC0_SC2 |= ADC_SC2_DMAEN; // Enable DMA
ADC0_SC2 &= ~ADC_SC2_ACREN; // Disable comp. function range
ADC0_SC2 &= ~ADC_SC2_ACFGT; // Disable comp. function greater than
ADC0_SC2 &= ~ADC_SC2_ACFE; // Disable comp. function
ADC0_SC2 &= ~ADC_SC2_ADTRG; // Software trigger (for calibration)

// (39.4.6) Status and Control Register 3
ADC0_SC3 |= ADC_SC3_AVGS(0); // Hardware average 4 samples
ADC0_SC3 |= ADC_SC3_AVGE; // Enable hardware averaging
ADC0_SC3 &= ~ADC_SC3_ADCO; // Single conversion

// Run calibration
adc_calibrate();

// (39.4.6) Status and Control Register 2
ADC0_SC2 |= ADC_SC2_ADTRG; // Enable hardware trigger

// (13.2.7) System options register 7
SIM_SOPT7 |= SIM_SOPT7_ADC0ALTTRGEN; // Alt trigger select
SIM_SOPT7 |= SIM_SOPT7_ADC0TRGSEL(4); // PIT trigger 0 select

// (39.4.1) ADC Status and Control Registers 1 (ADCx_SC1n)
uint32_t sc1a_config;
sc1a_config |= ADC_SC1_ADCH(ch); // Channel selection
sc1a_config &= ~ADC_SC1_DIFF; // Single-ended mode
sc1a_config &= ~ADC_SC1_AIEN; // Disable interrupts
ADC0_SC1A = sc1a_config;

}

void adc_calibrate() {

uint16_t sum;

ADC0_SC3 |= ADC_SC3_CAL; // Initiate automatic calibration sequence

while (ADC0_SC3 & ADC_SC3_CAL) {/* wait for it... */}

if (ADC0_SC3 & ADC_SC3_CALF)
   Serial.println("Calibration Failed");
else
   Serial.println("Calibration Successful");

// Plus-side gain calibration
sum = (ADC0_CLP0 + ADC0_CLP1 + ADC0_CLP2 + ADC0_CLP3 + ADC0_CLP4 + ADC0_CLPS) >> 1;
sum |= (1 << 15);
ADC0_PG = sum;

// Minus-side gain calibration
sum = (ADC0_CLMS + ADC0_CLM4 + ADC0_CLM3 + ADC0_CLM2 + ADC0_CLM1 + ADC0_CLM0) >> 1;
sum |= (1 << 15);
ADC0_MG = sum;

}

void init_dac0() {


// (13.2.12) System clock gating control register 2
SIM_SCGC2 |= SIM_SCGC2_DAC0; // Enable DAC0 clock

// (41.5.4) DAC Control Register 0
DAC0_C0 |= DAC_C0_DACEN; // Enable DAC
DAC0_C0 |= DAC_C0_DACRFS; // Select DACREF_2 (3.3V)

}

void init_pit0() {


SIM_SCGC6 |= SIM_SCGC6_PIT; // (13.2.16) Send system clock to PIT
PIT_MCR = 0x00; // (46.4.1) Turn on PIT
PIT_LDVAL0 = PIT_PERIOD; // (46.4.4) Set timer 0 period
PIT_TCTRL0 |= PIT_TCTRL_TEN; // (46.4.6) Enable timer 0

}

 

void init_dma() {


// (13.2.16-17) System clock gating control registers 6 and 7
SIM_SCGC6 |= SIM_SCGC6_DMAMUX; // Enable DMAMUX clock
SIM_SCGC7 |= SIM_SCGC7_DMA; // Enable DMA clock

// (24.3.1) Control Register
DMA_CR |= DMA_CR_EMLM; // Enable minor loop mapping

init_dma0();
init_dma1();

}

 

void init_dma0() {


/* === Source === */
// (24.3.18) TCD Source Address
DMA_TCD0_SADDR = (volatile uint16_t *)&ADC0_RA;

// (24.3.20) TCD Transfer Attributes
DMA_TCD0_ATTR = 0x0000;
DMA_TCD0_ATTR |= DMA_TCD_ATTR_SSIZE(1); // Source data transfer size (16-bit)

// (24.3.19) TCD Signed Source Address Offset
DMA_TCD0_SOFF = 0; // Don't advance source pointer after minor loop

// (24.3.23) TCD Signed Minor Loop Offset (Minor Loop Enabled and Offset Enabled)
DMA_TCD0_NBYTES_MLOFFYES = sizeof(uint16_t); // 2 bytes per minor loop
DMA_TCD0_NBYTES_MLOFFYES &= ~DMA_TCD_NBYTES_SMLOE; // Source minor loop offset disable (src)
DMA_TCD0_NBYTES_MLOFFYES |= DMA_TCD_NBYTES_DMLOE; // Source minor loop offset enable (dest)

// TCD Current Minor Loop Link, Major Loop Count
DMA_TCD0_BITER_ELINKYES = sizeof(dma0_dest_buffer) / sizeof(uint16_t); // (24.3.32) Begining count
DMA_TCD0_BITER_ELINKYES |= DMA_TCD_BITER_ELINKYES_ELINK;
DMA_TCD0_BITER_ELINKYES |= DMA_TCD_BITER_ELINKYES_LINKCH(1);
DMA_TCD0_CITER_ELINKYES = sizeof(dma0_dest_buffer) / sizeof(uint16_t); // (24.3.28) Current count
DMA_TCD0_CITER_ELINKYES |= DMA_TCD_CITER_ELINKYES_ELINK;
DMA_TCD0_CITER_ELINKYES |= DMA_TCD_CITER_ELINKYES_LINKCH(1);

// (24.3.24) TCD Last Source Address Adjustment
DMA_TCD0_SLAST = 0; // Don't advance source pointer after major loop

/* === Destination === */
// (21.3.24) TCD Destination Address
DMA_TCD0_DADDR = (volatile uint16_t *)&dma0_dest_buffer;

// (24.3.20) TCD Transfer Attributes
DMA_TCD0_ATTR |= DMA_TCD_ATTR_DSIZE(1); // Destination data transfer size (16-bit)

// (24.3.26) TCD Signed Destination Address Offset
DMA_TCD0_DOFF = sizeof(uint16_t); // Advance dest pointer by 2 bytes per value

// (24.3.29) TCD Last Destination Address Adjustment/Scatter Gather Address
DMA_TCD0_DLASTSGA = -sizeof(dma0_dest_buffer); // Return to &dma_dest_buffer[0] after major loop

/* === Interrupts === */
// (24.3.30) TCD Control and Status
DMA_TCD0_CSR |= DMA_TCD_CSR_INTMAJOR; // Interrupt at major loop (CITER == 0)
DMA_TCD0_CSR |= DMA_TCD_CSR_INTHALF; // Also interrupt at half major loop (CITER == BITER/2)

// (24.3.8) Set Enable Request Register
DMA_SERQ |= DMA_SERQ_SERQ(0); // Enable channel 0 DMA requests

NVIC_ENABLE_IRQ(IRQ_DMA_CH0); // Enable interrupt request line for DMA channel 0

/* === Triggering === */
// (23.4.1) Channel Configuration Register
DMAMUX0_CHCFG0 = DMAMUX_SOURCE_ADC0; // Trigger after ADC0 transfers
DMAMUX0_CHCFG0 |= DMAMUX_ENABLE; // DMA channel enable

}

void init_dma1() {

 

/* === Source === */
// (24.3.18) TCD Source Address
DMA_TCD1_SADDR = (volatile uint16_t *)&dma1_src_buffer;

// (24.3.20) TCD Transfer Attributes
DMA_TCD1_ATTR = 0x0000;
DMA_TCD1_ATTR |= DMA_TCD_ATTR_SSIZE(1); // Source data transfer size (16-bit)

// (24.3.19) TCD Signed Source Address Offset
DMA_TCD1_SOFF = sizeof(uint16_t); // Advance source pointer by 2 bytes per value

// (24.3.23) TCD Signed Minor Loop Offset (Minor Loop Enabled and Offset Enabled)
DMA_TCD1_NBYTES_MLOFFYES = sizeof(uint16_t); // 2 bytes per minor loop
DMA_TCD1_NBYTES_MLOFFYES |= DMA_TCD_NBYTES_SMLOE; // Src. minor loop offset enable
DMA_TCD0_NBYTES_MLOFFYES &= ~DMA_TCD_NBYTES_DMLOE; // Dest. minor loop offset disable

// TCD Current Minor Loop Link, Major Loop Count
DMA_TCD1_BITER_ELINKNO = sizeof(dma1_src_buffer) / sizeof(uint16_t); // (24.3.32) Begining count
DMA_TCD1_CITER_ELINKNO = sizeof(dma1_src_buffer) / sizeof(uint16_t); // (24.3.28) Current count

// (24.3.24) TCD Last Source Address Adjustment
DMA_TCD1_SLAST = -sizeof(dma1_src_buffer); // Return to &dma1_src_buffer[0] after major loop

/* === Destination === */

// (21.3.24) TCD Destination Address
DMA_TCD1_DADDR = (volatile uint16_t *)&DAC0_DAT0L;

// (24.3.20) TCD Transfer Attributes
DMA_TCD1_ATTR |= DMA_TCD_ATTR_DSIZE(1); // Destination data transfer size (16-bit)

// (24.3.26) TCD Signed Destination Address Offset
DMA_TCD1_DOFF = 0;

// (24.3.29) TCD Last Destination Address Adjustment/Scatter Gather Address
DMA_TCD1_DLASTSGA = 0;

/* === Interrupts === */
// (24.3.30) TCD Control and Status
DMA_TCD1_CSR |= DMA_TCD_CSR_INTMAJOR; // Interrupt at major loop (CITER == 0)
DMA_TCD1_CSR |= DMA_TCD_CSR_INTHALF; // Also interrupt at half major loop (CITER == BITER/2)

// (24.3.8) Set Enable Request Register
DMA_SERQ |= DMA_SERQ_SERQ(1); // Enable channel 1 DMA requests

NVIC_ENABLE_IRQ(IRQ_DMA_CH1); // Enable interrupt request line for DMA channel 1

}

void loop() {}

void dma_ch0_isr(void) {

// Swap pointers to input and proc buffers
static volatile uint16_t * tmp;
tmp = input_buffer;
input_buffer = proc_buffer;
proc_buffer = tmp;

DMA_CINT = DMA_CINT_CINT(0); // (24.3.12) Clear interrupt request register

}

void dma_ch1_isr(void) {

// Swap pointers to output and render buffers
static volatile uint16_t * tmp;
tmp = output_buffer;
output_buffer = render_buffer;
render_buffer = tmp;

// Copy samples to render_buffer

for (int i = 0; i < BUFFER_LEN; i++) {


render_buffer[i] = proc_buffer[i] >> (ADC_RES - DAC_RES);

}

DMA_CINT = DMA_CINT_CINT(1); // (24.3.12) Clear interrupt request register

}

Labels (1)
0 Kudos
9 Replies

599 Views
xiangjun_rong
NXP TechSupport
NXP TechSupport

Hi, Jeff,

I have checked the register setting, I think it is okay.

But maybe I am not clear about your code, it seems that ping-pong buffer is NOT used by DMA, because you do not reconfigure DMA in DMA ISR. In the case, I suggest you disable INTHALF interrupt, just use the INTMAJOR interrupt. For the two DMA channel, just use one channel interrupt for example just use DMA1 interrupt, in the ISR, just scale the ADC sample buffer and copy the sample buffer array  to DAC buffer array. What is the result?

BR

Xiangjun Rong

0 Kudos

599 Views
egoodii
Senior Contributor III

I'm not sure what Rong is trying to say --- using ONE contiguous buffer with TWO interrupts as the DMA engine re-cycles is a GREAT way to implement 'ping' and 'pong' memory areas without needing to reconfigure ANY DMA registers 'on the fly'.  There is no logical reason to expect running the DMA in a 'more complicated way' (such as the suggested continuous modifications, OR with a chained-pair of control descriptors) will affect your timing issue.

0 Kudos

599 Views
jeffgregorio
Contributor I

Yes, I was a bit confused as well, but I think Rong is suggesting that I use 1 DMA channel to transfer ADC results, and using that channel's interrupt to copy those results into the DAC's hardware buffer. That's not an ideal solution for me, as I would have to make the incoming buffer's length match the length of the DAC buffer, and I'd like to be able to vary the buffer length for different latencies. 

However, I do think there may be something to using chained-pair control descriptors for the DMA, and here's the reason:

I discovered that my discontinuities may be resulting from timing drift between the interrupts on the two DMA channels. I'm now measuring the timing in microseconds between dma_ch0_isr() and dma_ch1_isr(), and printing it out in dma_ch1_isr(). They start out synchronized (called between 0 and 1 microseconds of each other), then drift, then re-synchronize. This occurs at the exact period of the discontinuities.

The sequence for BUFFER_LEN = 16 is

{0, 21, 21, 41, 42, 63, 62, 83, 84, 104, 104, 125, 125, 146, 146, 166, 167, 188, 187, 208, 209, 229, 229, 250, 250, 271, 271, 291, 292, 313, 312}

I have no idea what's causing this, but using chained TCDs might help. Going to try this as soon as I can. 

0 Kudos

599 Views
egoodii
Senior Contributor III

Sorry, with the title I was off into the DMA process, but simply 'assumed' that you had inherent rate-lock between the input and the output, based on some 'common clock source' and 'calculable dividers' that would insure coordinated in/out buffer use - but rate-slip is certainly the more obvious issue!

I see you have the ADC triggered by PIT0, running 48KHz, and presumably an ADC COCO comes 'soon thereafter' at the same 48KHz, triggering each DMA0 (in).  BUT I don't see what triggers DMA1 to pace-out samples -- I expected to see PIT1 set up for this, and DMAMUX0_CHCFG1 = DMAMUX_TRIG | DMAMUX_ENABLE; --- OR swap DMA0 and DMA1 usage throughout and double-use the same PIT0 (or, I suppose, vice-versa:  Leave DMA 'as is' and use PIT1 throughout including SIM_SOPT7_ADC0TRGSEL(5)).

I will note that you call out a 30MHz ADC clock, while the K66-180 is limited to 12MHz in 16-bit mode.  On a related note, you should know that if you just run the ADC in 12-bit mode, the right-justified result-register will already be in the right position for DAC0_DAT0, AND in that mode the conversion clock can be up to 24MHz.  And anything over 8MHz is 'high speed' (ADC_CFG2_ADHSC).

0 Kudos

599 Views
jeffgregorio
Contributor I

So before I had the DMA channels linked so DMA0 transfers would initiate DMA1 transfers, so the common clock source was PIT0. I suppose now that this doesn't guarantee synchronization, but I'm still not sure why.

I was unaware that a PIT channel could be used to trigger multiple peripherals, but I tried both of your suggestions. I tried:

1) PIT0 triggers ADC0. ADC0 conversions trigger DMA1, which moves the ADC result to the input buffer. PIT0 also triggers DMA0, which moves samples from the buffer to DAC0.

2) PIT0 triggers ADC0 and ADC0 triggers DMA0. PIT1 triggers DMA1, which moves samples from the buffer to the DAC.

In both cases I'm getting some unexpected behavior, namely that the DMA channel handing output runs twice as often as the DMA channel handling input, so now it's outputting each buffer twice, but at least the rate-slip looks like it might be gone. 

In the case where I'm using two PIT channels, the PIT itself is running at twice the rate (tested by enabling PIT interrupts), even though their LDVALs are the same. I tried a hack, setting the LDVAL of the PIT channel used for output to twice the LDVAL of the other, but I still get rate-slip, albeit much less often than before. 

Thank you for highlighting the issue with my ADC settings, which I've corrected. However the problem with the DMA transfers persists. 

I've gone over the code so many times over a week, and I'm stumped. Someone in another thread (https://community.nxp.com/thread/102320 ) mentioned a "bug in the silicon" related to PIT-triggered DMA transfers, so I'm about ready to abandon this configuration, even though it seems like the most straightforward way to implement double-buffering.

I'm also having DMA channels completely crap out on me after uploading code (their interrupts don't fire). It seems DMA0 always works, without exception, but I've had to make alternate versions that use DMA1, 2, and 3 for the second channel. Sometimes 0 and 3 work, and 1 and 2 don't. Sometimes 0 and 2 work, and 1 and 3 don't. In one case, 0, 1, and 2 worked while 3 didn't.

Is it possible there is something wrong with the Teensy bootloader, or would this more likely be a hardware defect? 

0 Kudos

599 Views
mjbcswitzerland
Specialist V

Jeff

The K66 has a PDB which is intended for ADC/DAC timing, rather than the more limited PIT. See chapter 3 in http://www.utasker.com/docs/uTasker/uTaskerADC.pdf for a reference to do more or less what you are doing.

I have attached binary files for the Teensy 3.6 (hex file actually since the Teensy can't load binary files) and the FRDM-K66F which sample ADC1_DM0 at 8kHz and transfer the input value to DAC0 output (both 12 bits) using PDB and DMA. It doesn't use any interrupts but can optionally trigger these at half and full buffer intervals in case the content needs to be manipulated. This reference causes a 1s delay between input and output (allowing adequate time for manipulation) but this is just the way that it is configured as reference.

I tested that the FRDM-K66F works correctly but, since I don't have a TEENSY-3.6, I couldn't test on the HW but the simulation was OK and I have had confirmation from Teensy 3.6 users that the generated files do (usually) run on it. I put FS USB CDC on the Teens 3.6's USB with a command line menu (in the I/O menu one can view registers with the "md" command so you can also check register content of the DMA and PDB as reference in case you can copy something).

As mentioned before you could get the code to do this from the uTasker open source project on GIT hub (you just need to copy some code and not use the complete environment).

Note that I only changed one line in the reference code for the Teensy 3.6
adc_setup.int_adc_bit = ADC_DM1_SINGLE;
to
adc_setup.int_adc_bit = ADC_DM0_SINGLE;
since the K66 doesn't have an ADC1_DM1 input (as various other K parts do).
Otherwise I took the reference project and enabled TEST_AD_DA in ADC_Timer.h (ensuring SUPPORT_PDB, SUPPORT_DAC and SUPPORT_ADC were also enabled for the respective drivers) and build. On the HW the operation was immediately successful.

If you have limited time it makes sense in using more power-ful solutions that you presently use - did you receive your FRDM-K66F board yet?

Regards

Mark

0 Kudos

599 Views
mjbcswitzerland
Specialist V

Jeff

If DMA interrupts don't fire it will usually be due to a DMA error taking place, which aborts the transfer and so it never completes - and this no interrupt.

You will need to observe the DMA status and error registers since they will inform you of the channel that fails and the general cause (eg. source or destination).

Regards

Mark

0 Kudos

599 Views
mjbcswitzerland
Specialist V

Hi Jeff

There is a double-buffered DMA based reference for your processor in the Open Source uTasker project on GIT hub. It will also run the code (with interrupt and DMA emulation) in Visual Studio so that you can analyse the behavior and correct anything needed in your source. It will also record to SD card and there is a USB-Audio reference using PIT0 to double-buffered USB.

Documents:
- http://www.utasker.com/docs/uTasker/uTaskerADC.pdf
- http://www.utasker.com/docs/uTasker/uTaskerUSB_Audio.pdf
Code: http://www.utasker.com/kinetis.html
References for K66
- http://www.utasker.com/kinetis/TWR-K65F180M.html
- http://www.utasker.com/kinetis/FRDM-K66F.html
- http://www.utasker.com/kinetis/TEENSY_3.6.html (also for direct operation on Teensy 3.6)


I would get a FRDM-K66F (it is not much more expensive than a Teensy 3.6 but has much more HW to play with and an inbuilt debugger). This will allow you to work efficiently and then you can put the confirmed code back into your Arduino environment to run on the Teensy. Developing on a Teensy directly is really only for people who like spending days solving quite simple problems that FRDM-K66F users will be able to work out quite simply - with simulation as well you may be able to increase your development efficiency by at least a factor of 10.

Regards

Mark

0 Kudos

599 Views
jeffgregorio
Contributor I

Thanks, Mark. I've taken your suggestion and ordered a FRDM-K66F, but I'm not sure how soon I'll get to learning a new development environment since this is a personal project and I'm squeezing it in around work.

For now I'm still troubleshooting in the Arduino IDE, and I've narrowed the source of the problem down a bit further, as detailed in my reply to Earl Goodrich below. 

0 Kudos