Alright, couple of general questions on the code you posted if you do not mind.
1) The classes "decoder" and "edgeprovider" header (.h) files seem to only contain the definition of the class, and the actual functions and code are contained in the .cpp file with the same name. The header file for "RingBuf" has the definition at the top and all the functions immediately below it. There is no .cpp file for the RingBuf class. Is there a reason for this? Is this just preference of the programmer or is there a functional difference in the behavior of the two ways of defining a class and its operations?
Usually you want to separate the actual code (the definition) from the declaration in the header. There are various reasons for that.
- If you have all the code in the header it needs to be compiled fore each and every compilation unit which includes the header. In large projects (and old times) this would generate a significant compilation time penalty. For the tiny Arduino stuff and the fast computers we have now compilation time is usually not a big concern anymore.
- For commercial projects you don't want to distribute your source code. The only thing users of a libraries need are the headers with the declarations. The actual code can then be distributed in a compiled format (object files *.o)
- Encapsulation. Generally you try to hide away as much information about your implementation as possible and provide only a small interface to the users of your code. (google for "code against interfaces not implemenations" if you want to learn about this technique). So, better not expose your code in the header.
- However, for templated code like the one in RingBuf.h having the code in the header is mandatory.
But, again, given that this is all for hobby and it is quicker to write, you see code in headers more often these days
macardoso said:
2) edgeprovider.h sets up the timer capture using this line: "static constexpr IMXRT_TMR_CH_t* ch = &IMXRT_TMR1.CH[2]; // TMR1 channel 2 -> input pin 11," Where do you look to figure out these (what I am assuming is inline assembly) commands? I could not find IMXRT_TMR1.CH in the 3400 page processor reference manual. This same question applies to all the commands in void EdgeProvider::init()
No, this is not inline assembly but perfectly valid c++ code. I didn't want to hard code the address of the second channel of the TMR1 module. If want to use another timer module/channel you only need to adjust this line. It defines a pointer to an object of type IMXRT_TMR_CH_t which is defined in imxrt.h (see here:
https://github.dev/PaulStoffregen/c...dbca8032763fe97e2a99e7e/teensy4/imxrt.h#L7884) Information on the TMR timers is found in chapter 54 of the IMRT manual. imxrt.h defines the vast majority of the symbols you find in the manual.
macardoso said:
3) Is using "lockedPop()" which uses NoInterrupts() going to interfere with capture of edges, hardware quadrature decoding, or serial on one of the serial ports? I'd like/need to avoid situations where these events are missed.
Yes, but without locking you may run into issues when interrupting the pop code by some push code. In the posts above it is discussed that circular buffers should have no issues with this, but the implementation used here does. The current code from my github repo replaced the fully blown RingBuf.h buffer by a very simple implementation which doesn't need to disable interrupts during poping. I was able to reduce interrupt time to about 40-80ns (can't measure more accurate)
macardoso said:
4) In edgeprovider.cpp, I understand how you are using the variable "edge" to hold the delta clock cycles in the Low Word and some other data in the High Word. What data is returned by the expression ch->SCTRL & TMR_SCTRL_INPUT for the data in the high word? Where is this documented? Is this just 1=Rising edge, 0=Falling edge?
You can look that up in chapt. 54.6.8 in the manual. It says that the INPUT bit is bit 8 of the SCTRL register. Imxrt.h defines this in
line 8184
macardoso said:
5) My understanding of the implementation of this ring buffer is that the interrupt routine triggers for each capture flag and pushes that data into a buffer (member of class EdgeProvider). The line "using EdgeBuffer = RingBuf<uint32_t, 65000>;" makes sense to me as this assigns EdgeBuffer as a member of class RingBuf (RingBuf is using template meta programming to allow me to store uint32_t or really any other datatype in there).
The line "using EdgeBuffer = RingBuf<uint32_t, 65000>;" is just a typedef to not always have to write RingBuf<uint32_t, 65000>. I.e., it simply defines the short name EdgeBuffer.
Somehow the line "static EdgeBuffer buffer;" changes the name of EdgeBuffer to "buffer" and that is used throughout EdgeProvider.
EdgeBuffer (or fully written RingBuf<uint32_t, 65000>) is the type (like int, float etc) and buffer is the name of the variable. Same as you have in float x = 3.0; float is the type and x is the name of the variable.
macardoso said:
So EdgeProvider is filling up EdgeProvider.Buffer (which is technically EdgeBuffer of type RingBuf). Occasionally, Decoder::tick() must be called which takes empties the EdgeProvider.Buffer and sample by sample passes it to Decoder::decode(). Decoder::decode() is where the magic happens. The samples are measured to determine if they represent a high or a low bit (or a start or stop or junk data). The bits are placed in an intermediate buffer (bits.buffer) and collected until the buffer has filled with a full packet. Once this happens, the bits buffer is pushed to a resultBuffer (also member of class RingBuf). This holds the decoded data until I am ready to do something with it (accomplished by Decoder::read (), which empties the Result buffer to wherever I want to put it.
Is this pretty correct? Took me much longer than I care to admit to understand how all of this works. I do OOP in PLC ladder logic, so I'm familiar with the concepts, but I am completely unfamiliar with the syntax in C++.
That's perfectly correct.
macardoso said:
6) Since decoder needs to run often, should I just do all the compare statements in terms of clockcycles, rather than converting to microseconds with floating point multiplication? Or is the penalty in processor time minimal due to the FPU on this processor?
The critical timing is the edgeProvider, this needs to be as fast as possible since it shouldn't miss edges. The decoding is not so critical since it can work async on the stored edges.
macardoso said:
7) What does void yield() do? I tried googling it, but I mostly find confused people on Arduino forums or ESP8266 stuff. I'm assuming it runs when a blocking function (like delay()) is called?
yield is called whenever teensyduino is looping. I.e. it is called once per loop and e.g. while delay or other long running code is spinning. -> it is usually called more often than you have calls from the main loop. It also allows to use e.g. delay in the main loop without having to worry about not calling tick fast enough.
macardoso said:
Thanks so much! I think I now know enough to be dangerous to modify this to work for my application.
Spoiler alarm: If you want you can have a look at the new code in the gitHub repo. I improved the edge detection code to be much faster and changed the decoder to handle TS5643 data fields. The receiver should run out of the box and display the encoder counts (it also parses the various flags if you are interested). I observed that from time to time some high priority interrupt delays the edge detection for about 1µs so that you'll get reading errors. Most of those will be caught but since I didn't implement the CRC some of those might pass. I wasn't able to find the actual interrupt source but the errror rate increases when you print something. So, probably related to the USB system...
Next thing I want to try is the DMA path...