0
\$\begingroup\$

I have a rotary encoder AEAT-9922, which is connected using SSI2 mode. The read format is defined on the 16th page of the datasheet.

Encoder is connected to the STM32F373 microcontroller, which runs FreeRTOS with task where the encoder value is read and stored for different parts of the program.

The code for the task is as follows:

void StartAbsoluteEncTask(void *argument)
{
  /* USER CODE BEGIN StartAbsoluteEncTask */
    /* Infinite loop */
    for (;;) {
        osDelay(1);
        HAL_SPI_Receive(&hspi3, (uint8_t *) encRxBuf, 3, 1);
        volatile uint32_t receivedValue = ((encRxBuf[0] << 16) | (encRxBuf[1] << 8) | encRxBuf[2]) >> 2;
        encoderState.isReady = (receivedValue & 0b1000) >> 3;
        encoderState.isMHI   = (receivedValue & 0b100 ) >> 2;
        encoderState.isMLO   = (receivedValue & 0b10  ) >> 1;
        uint8_t parity = receivedValue & 1;

        if (encoderState.isReady == 0) continue; // if not ready - skip
        if (encoderState.isMHI   == 1) continue; // if MHI error - skip
        if (encoderState.isMLO   == 1) continue; // if MLO error - skip
        if (__builtin_parity(encoderState.absolutePos) == parity) continue; // if parity doesn't match - skip

        encoderState.absolutePos = (receivedValue >> 4) & 0x3FFFF;
    }
  /* USER CODE END StartAbsoluteEncTask */
}

So when the encoder values is read, we check for errors MHI and MLO and, if one of them exists - we drop this sample. If everything is all right, we accept the sample and store it into the encoderState.absolutePos variable and move on.

But, here is the question: I've used STM32CubeMonitor to view the value of the encoder and it has these strange peaks downwards, which I don't really know where they are coming from: graph with peaks

All errors are filtered out and parity is checked - where can these peaks come from?

SPI config is as follows:

STM32CubeMX SPI Config

\$\endgroup\$
12
  • 2
    \$\begingroup\$ I would not filter out ANY errors until you get to the bottom of why you're getting any measurable number of errors (if you are). You may be hiding a huge issue. \$\endgroup\$ Sep 10, 2023 at 21:07
  • 1
    \$\begingroup\$ Quick comment, consider (receivedValue >> 3) & 1 (shift then mask) rather than mask then shift. This is a cleaner way to get bits into a boolean variable since you're always ANDint with 1. \$\endgroup\$ Sep 10, 2023 at 22:50
  • 1
    \$\begingroup\$ You're going to want to get the raw SPI data, possibly with timestamps then analyze that directly. Ideally include timestamps. There may be a pattern to the errors. Either the monitoring tool is having glitches or the encoder is spitting out glitchy values. It would be good to rule out the first. Log in 3 byte chunks to a buffer and then stop and look at a few KB of data to see what's going on. Also, what's the kernel tick rate? \$\endgroup\$ Sep 10, 2023 at 23:00
  • 1
    \$\begingroup\$ @Andrii I don't know anything about FreeRTOS, how your task is scheduled, or if pre-emption is operating and can pre-empt your task. I also don't know anything about HAL_SPI_Receive, except that it is some library routine. Questions: Is your task able to be pre-empted based upon an operating system timer event? Is HAL_SPI_Receive a routine that must complete something before it returns? (Looks like it to me.) If so, does all this mean that HAL_SPI_Receive itself can be pre-empted and later rescheduled for run by FreeRTOS? I'm worried about the entire design, FYI. \$\endgroup\$ Sep 11, 2023 at 0:07
  • 1
    \$\begingroup\$ @Andrii You didn't address my question about pre-emption. \$\endgroup\$ Sep 11, 2023 at 0:39

1 Answer 1

2
\$\begingroup\$

You've fortunately been able to validate the issues I was concerned about -- pre-emption. So we both know the source of the problem, now. No question.

There were two huge elephants in the room that I saw.

  1. Your code written like this:

    encoderState.isReady = (receivedValue & 0b1000) >> 3;
    encoderState.isMHI   = (receivedValue & 0b100 ) >> 2;
    encoderState.isMLO   = (receivedValue & 0b10  ) >> 1;
    
  2. And your blind use of a library routine.

In the first case, ask yourself "What happens if the operating system's heart-beat timer fires between the first and second line or perhaps, worse still, in the middle of some of the assembly language statements that are processing the semantics of the first line? Will other code that uses this structure and returned to running by the O/S be able to make sense of it, if it is only partially updated or otherwise in the middle of getting things written out?"

The same questions could also be applied to the library routine, which may be in the middle of operating the hardware peripheral. For example, there may be some critical timing involved there. Or? Who knows?

Pre-emption isn't something to take lightly. (Neither is the ignorant use of floating-point. See this paper for some thoughts there.)

Most people will have had some experience using workstation level operating systems. But these have professional drivers written for them for the hardware that they operate. Experience with tasks in C#, F#, or VB.NET ill-prepare programmers for what takes place when having to deal with hardware peripherals in the face of pre-emption. Though if they work a lot in the area of multi-threaded apartments under Windows, for example, they will have learned much of what's needed to be known. (How to handle multiple CPUs that run literally at the same exact time and can access the exact same memory location effectively at the same moment, for example.) But without that experience, a lot of this just flies right over their heads.

I personally think that the better way to learn is to dive in and write a multi-threaded O/S. I'd say to first start with a simple co-operative system that uses a switch call to allow a thread to release control at a convenient place. It also means you can use library code without worrying as the only way a cooperative mode operating system can switch tasks is when one of them asks for it, which is always after any library code has been completed. This is very easy because all you need is a structure to hold certain register values that need to be preserved using a very short assembly routine to do it. But it forces you to learn about the language's specifications for registers that must be preserved. (The so-called preserve registers, which won't be all of them but just some, as a function call in C for example expects that some registers will scratched by making the call.) Once you understand this, and I've taught it to programmers who were able to implement their own in less than a day's time, then you can move on to consider pre-emption details. (Of course, to teach it in less than a day the programmer must already be experienced with C and assembly and mixing the two.)

For learning pre-emption, I'd recommend Doug Comer's 'red book' on XINU. This is his first edition and it includes several important ideas that he later removed from following editions to make room for other topics. For example, he introduces the idea of memory marking and this is something important to gather up. (Gone from later editions, as he expected that anyone reading them would download the source and find the code handling memory marking and read it on their own.) Another important idea to get is the delta queue.

Writing a pre-emptive O/S requires more time. But it is still doable in a week's time if you keep the ideas simple enough and don't go crazy. A semaphore queue, a sleep queue, and the ready queue, for example. But this will sensitize you to writing libraries that can be pre-empted and to ideas related to the so-called upper and lower half code used for hardware drivers and their events and servicing needs.

There's nothing like putting in a week or two on the topic making something that actually works to deepen these things into your bones. Also, get in the practice of looking at the generated machine code/assembly code that your compiler produces. Not all the time, obviously. But every so often. Do it enough so that if you are looking at some section of C you can predict what will be generated with some accuracy. Test yourself from time to time. And learn what volatile does and does not do for you, as it applies to local registers and local stack stores as well as static lifetime structures of various incarnations, and heap space. Sometimes, it also helps to understand how the linker works and how you can alter its behavior, when needed. But that's more 'down the road, a bit.' But you should keep the idea in view and work on it when you get time. You should also understand the basic program model used by compilers and linkers. This includes execute-only, read-and-execute, initialized read-write, uninitialized read-write, heap, stack; and the additional sections required by Harvard vs von Neumann.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.