I’ve been trying to add frequency channels to PteroDAQ, so that it can plot the frequency of an input on a digital channel. So far, I’ve only implemented this for the Teensy LC, which uses a KL26 ARM processor. Because I want to be able to mix frequency channels with regular analog or digital channels, I’m using the method that counts rising edges in a fixed time period (the sampling period set for PteroDAQ), rather than timing how long there is between edges. The chosen edge-counting method is more appropriate for high-frequency signals than for low-frequency ones, but by using long-term averaging, one can measure fairly low frequencies also.
In order to count adequately high frequencies, I need to run a hardware clock that does not use interrupts to do individual counts, but which can keep counting why the processor does useful stuff. One choice (made, for example in the Teensy FreqCount library) is to use the low-power timer LPTMR. But only one pin on the Teensy LC can be used with this timer—pin 13, which is also the LED output pin. I wanted something more versatile, that could be used with any digital pin and that could have multiple frequencies being counted at once.
The closest I’ve been able to come so far is to use the DMA channels as counters (a suggestion my son made to me, based on ideas he saw in a Freescale application note). I can use any pin on Port A, C, or D to control a DMA channel, and there are 4 DMA channels, but each port only provides one signal to the DMA, so I can have 3 independent frequency measurements (one for any pin from Port A, one for any pin from Port C, one for any pin from Port D). If I use the same trick on the Teensy 3.1, which is a K20 processor, I can have 5 independent measurements, one each from Ports A, B, C, D, and E. On the FRDM-KL25Z board, I only get 2 measurements, one each from Ports A and D.
To use the DMA channel as a counter, I had to set it up with a large byte count (the max is 0xFFFFF, or 1,048,575) and have the DMA do a no-op 1-byte transfer from a fixed source to a fixed destination on each external signal. On each read of the frequency, I read the remaining count and subtracted from 0xFFFFF to see how many external signals had happened, and put that count in the queue for sending to the host computer like any analog read.
It took me a long time to get the code working on the Teensy LC (about 2 days). The first problem was just understanding all the pieces of the DMA system:
- Using the SIM_SCGC6 and SIM_SCGC7 registers to turn on clocking to the DMAMUX and DMA
- Using DMAMUX0_CHCFGn to route signals from the ports to the DMA channels
- Setting up the PORTx_PCRn register to send DMA signals on rising edges
- Setting up the DMA_SAR, DMA_DAR, DMA_DSR_BCR, and DMA_DCR registers for the DMA operation itself.
Another big chunk of time was spent rearranging some of the python software, so that the frequency channels could be selected in the GUI and the communications protocol to the microcontroller board updated to handle frequency channels.
But the biggest chunk of time was spent trying to debug a race condition. No matter what I did, running a high-frequency signal with a high sampling rate resulted in things failing in unpredictable ways after an unpredictable delay (the counts all coming out 0, the communication packets getting incorrect checksums, or just freezing). I had to power cycle the Teensy LC after each failure to try again.
I tried all sorts of debugging tricks (no SWD debugging interface on the Teensy LC), including sending contents of the various DMA registers instead of the count, to see which things were going wrong. Unfortunately, almost everything seemed ok until it failed, and then nothing came out. I was convinced it was a race condition—but where?
I tried various ways of turning the counting on and off, based on different suggestions in different Freescale manuals (using the ERQ bit in DMA_DCR or using the ENABLE bit in DMAMUX). I even tried various combinations of the turning on and turning off in different orders. It was clear to me that the problem was in reading or resetting the DMA_DSR_BCR register, but there were several places where a race might be occurring and no help in any of the reference manuals.
By trial and error, I finally figured out that if I used the DMAMUX enable bit to turn off the counting, and put a NOP after it before looking at the DMA_DSR_BCR register, the race condition went away. The crucial problem seems to be that looking at DMA_DSR_BCR at the same time that hardware is trying to change it results in unpredictable behavior—not just unpredictable values for the read, but seriously wedging the DMA so that a hardware reset is needed. The extra NOP makes sure that any pending DMA operations are completed before accessing the DMA_DSR_BCR register. This warning does not seem to be present in any of the KL26 documentation—I’m not sure the engineers who designed the DMA were aware of it. Either that or the engineers failed to communicate the importance of this constraint to the tech writers who did a sloppy cut-and-paste job in writing the KL26 DMA documentation (for example, there are references to a non-existent “TCDn”, by which they mean the 4 registers DMA_SARn, DMA_DARn, DMA_DSR_BCRn, and DMA_DCRn).
Running the frequency channel with a square-wave input from the FG085 function generator lead to some more discoveries:
- The samples taken by PteroDAQ have a fairly large amount of jitter at high sampling frequencies. At 10kHz sampling and a 500kHz input, the number of observed edges varied from about 47 to 53, representing a ±6µs variation on the nominal 100µs. My son suggested that the jitter was caused by the USB communications—if the timer interrupt occurred while the USB interrupt was in progress, the sample would be delayed up to about 6µs, and this could happen about every millisecond with heavy communication. Sure enough, the long periods were followed immediately by short periods, and some clusters about 1msec apart occurred (though not ever millisecond). At low sampling frequencies, there is less USB communication, so a lower probability of getting the 6µs delay, which also represents a much small fraction of the period, so I did not see jitter at lower sampling frequencies.
- One of the clocks is out of spec. When the FG085 was putting out a nominal 500kHz signal, the PteroDAQ system measured it at 499958.5 Hz, which is –83ppm. The two crystals should each be about ±20ppm, so an 83ppm discrepancy is larger than expected. With a nominal 5kHZ signal, the PteroDAQ system measured it at 4999.524 Hz (–95ppm). I’ll have to take the function generator and PteroDAQ board into the circuits lab and use the frequency counters and function generators there to see what the actual accuracies are. For anything that we do in the applied electronics class, 100ppm is good enough, but it would be nice to know how far off my instruments really are.
- The FG085 glitches (turning off the output for a while) when changing frequency. I plotted the frequency as I stepped the frequency down in 100Hz steps from 5kHz to 2kHz:
The next step for me is to implement frequency channels for the FRDM-KL25Z board (pretty much identical to the KL26 on the Teensy LC, except for having to use the MBED syntax for the register setting rather than the one Teensyduino uses) and the Teensy 3.1 board (the K20 DMA is much more complicated, so it will probably take me a while to figure out and set up—especially if it also has undocumented race conditions). Once I get those working, I’ll have to figure out how to do the GUI so that people can quickly figure out that which combinations of frequency channels are legal. I’ll probably want to do that with separators in a menu, greying out sections that already have frequency channels in use.
Once all that is done (later this week??), I’ll push it up to the PteroDAQ repository.