STM32 | Cube | HAL library | UART receive interrupt application

Foreword

If you can see this, it is assumed that you are not a novice, but someone who has a certain foundation in C language. At some point in your work or study career, you suddenly have to come into contact with the HAL library of STM32, and you have been exposed to it before. It was developed using the standard library.
I haven’t written a blog for a long time. It seems like a year has passed since the last one was officially released. Although I wrote some in between, it was always a draft and I didn’t want to publish it, because that period was considered as formal learning of 32BitMCU. The method of learning 8BitMCU before was obvious. It is no longer applicable, so there is no way to write out the functions of 32BitMCU as much as possible (mainly because there are so many people using it, and I am afraid of being scolded if it is not written well).
Why did you suddenly want to write recently? Recently, due to work requirements, I have to start contacting STM32 again and develop using the HAL library. Although I am mentally prepared for this to be a slightly troublesome process, it is a big change in development thinking, but after contacting it, I still feel that there are many situations
I was still a little dizzy just looking at the program, so I decided to go online and see how others used it. After looking around, I still had to sigh. Many people wrote about the same content. At most, they just changed the names of their variables and processes. They are all the same, and some people even don’t know who copied the code from whom, so they make it exactly the same and mark it as original. However, the content of some blogs is still a bit good, haha, but not much, not at all.
And I feel like I haven’t written the key points I want to see, so I’ll write it myself

Interrupt mechanism of HAL library

It’s really super troublesome, my first impression, and with multiple layers of function nesting, just jumping takes a lot of time. But thinking about it later, normal development is unlikely to be interrupted frequently. In addition, it is 72M. At the main frequency, the overhead of these jumps can be barely accepted?
Take the UART receiving interrupt as an example. The program of the HAL library alone is a nest of various functions

  1. void USART1_IRQHandler(void);
  2. HAL_UART_IRQHandler( & amp;huart1);
  3. UART_Receive_IT(huart);
  4. HAL_UART_RxCpltCallback(huart);

It feels good to list it out, haha In fact, there is no need to clarify this sequence or all the programs involved. My suggestion is to look at the corresponding branch for which interrupt is used, and the first three mentioned above Functions basically do not need to be changed, unless there are very special needs, mainly because it is useless if you change them. If Cube is reconfigured, the changes you made will not be retained
The branch of the entire process that receives interrupts is what I wrote above. Generally, if it needs to be changed, it is the fourth one. The callback function is a special mechanism for interrupts in the HAL library. If I remember correctly, the standard library should not have such a thing.
The program itself reserves a function modified with the keyword __weak. The specific principle has not been studied in detail, but it can be roughly assumed that if there is no function with the same name elsewhere in the program, this virtual function will be used. The program, if any, uses functions without the __weak keyword modification.
I’m going too far. The general meaning is that you need to copy the function name of this callback function yourself and then rewrite it in a place where you can easily modify it. It seems that you have never seen it written directly in the official reserved function.

Different modules use the same callback function

The idea is somewhat similar to the interrupt branch of the standard library. If a serial port interrupt was entered in the standard library before, how do you know which behavior triggered the interrupt? Therefore, it is generally judged in the interrupt whether it is a receiving interrupt, a sending completion interrupt, or a receiving idle interrupt, etc.
But HAL can be regarded as helping us write the branch judgments for different triggering interrupts. However, because of the reserved callback function mechanism, as long as the receiving interrupt is triggered by the UART module, it is the same callback function whether it is UART1 or UART2, so before entering When calling the callback function, we need to judge whether UART1 or UART2 is triggered. If we do not make a judgment, it will lead to an obvious problem. The operations of UART1 and UART2 after receiving data are completely different. We write them in a callback function without making a judgment. If you want to differentiate, you can’t write it.
I’m a little surprised that some tutorials don’t mention this. Are they so careless?
The method of differentiation is also very simple. I tried it briefly and found that both of the following two methods are acceptable.

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{<!-- -->
    if(huart->Instance == USART1)
    {<!-- -->
    
    }
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{<!-- -->
    if(huart == ( & amp;huart1))
    {<!-- -->
    
    }
}

Receive idle interrupt

The standard library has reserved relevant flag bits for receiving idle interrupts to judge, but I don’t know why the HAL library doesn’t seem to have it yet. I saw others saying that it was added to the F4 program, but it is not yet available for F1.
I searched for other people’s operations to add this interrupt branch. One blogger imitated the style of the HAL library and added the idle interrupt branch. Is there really a lot of content? But the problem is still there. Add these programs unless you no longer use Cube in the future. configuration, otherwise it would still be cleared, so I didn’t look at it carefully.
Therefore, it is a bit contradictory. In the development of Cube + HAL library, unless the HAL library is officially upgraded and everyone’s feedback is added, it seems inappropriate for me to add some low-level things personally.
The focus of this blog is not to discuss the issue of receiving idle interrupts. I am more concerned about the functions encountered in actual development.
The receiving idle interrupt is used in the situation where the data is not equal in length and there is no fixed format of the end data, but I haven’t encountered it yet. If there is such a thing, you may have to build a timer module to calculate it. time.
If the data is not of equal length, a \r\\
will usually be added at the end of the data to mark it, otherwise it will be really difficult to process.
Another situation is that the data length of each communication is fixed.
Let’s write a code to verify these two situations.

Unequal length character reception

Initial configuration reception interrupt

First, enable the serial port reception interrupt in the main function user initialization area. The Cube configuration program does not include this sentence

HAL_UART_Receive_IT( & amp;huart1, (uint8_t ) & amp;uart_data_byte, 1); / Enable serial port reception interrupt */

Analysis of receive interrupt configuration function

You can still focus on understanding this function. I will move the source code here.
In fact, it can also be understood as turning on the receive interrupt, but because the HAL library has added special functions, it is not just a function of turning on the interrupt.

HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{<!-- -->
  /* Check that a Rx process is not already ongoing */
  if (huart->RxState == HAL_UART_STATE_READY)
  {<!-- -->
    if ((pData == NULL) || (Size == 0U))
    {<!-- -->
      return HAL_ERROR;
    }

    /* Process Locked */
    __HAL_LOCK(huart);

    huart->pRxBuffPtr = pData;
    huart->RxXferSize = Size;
    huart->RxXferCount = Size;

    huart->ErrorCode = HAL_UART_ERROR_NONE;
    huart->RxState = HAL_UART_STATE_BUSY_RX;

    /* Process Unlocked */
    __HAL_UNLOCK(huart);

    /* Enable the UART Parity Error Interrupt */
    __HAL_UART_ENABLE_IT(huart, UART_IT_PE);

    /* Enable the UART Error Interrupt: (Frame error, noise error, overrun error) */
    __HAL_UART_ENABLE_IT(huart, UART_IT_ERR);

    /* Enable the UART Data Register not empty Interrupt */
    __HAL_UART_ENABLE_IT(huart, UART_IT_RXNE);

    return HAL_OK;
  }
  else
  {<!-- -->
    return HAL_BUSY;
  }
}

The second parameter of this function that receives interrupt configuration is a pointer. It does not mean a variable or an array, it is a pointer. The essence of a pointer is an address. The actual parameter of the function is a variable uart_data_byte, use & to get the address and pass it into the function.
huart->pRxBuffPtr = pData;This sentence means that the huart->pRxBuffPtr address points to the address of the variable I just passed in, huart->pRxBuffPtrWhat is this? This will be analyzed later.
It’s a bit convoluted, but there’s really no way to explain it all at once when it comes to pointers
The third parameter of the function configures the two huart->RxXferSize = Size; and huart->RxXferCount = Size;, which will be analyzed in detail later.

Receive interrupt function

The following is the source code for receiving interrupts, but it is not a callback function. The callback function is called by this function.

static HAL_StatusTypeDef UART_Receive_IT(UART_HandleTypeDef *huart)
{<!-- -->
  uint16_t *tmp;

  /* Check that a Rx process is ongoing */
  if (huart->RxState == HAL_UART_STATE_BUSY_RX)
  {<!-- -->
    if (huart->Init.WordLength == UART_WORDLENGTH_9B)
    {<!-- -->
      tmp = (uint16_t *) huart->pRxBuffPtr;
      if (huart->Init.Parity == UART_PARITY_NONE)
      {<!-- -->
        *tmp = (uint16_t)(huart->Instance->DR & amp; (uint16_t)0x01FF);
        huart->pRxBuffPtr + = 2U;
      }
      else
      {<!-- -->
        *tmp = (uint16_t)(huart->Instance->DR & amp; (uint16_t)0x00FF);
        huart->pRxBuffPtr + = 1U;
      }
    }
    else
    {<!-- -->
      if (huart->Init.Parity == UART_PARITY_NONE)
      {<!-- -->
        *huart->pRxBuffPtr + + = (uint8_t)(huart->Instance->DR & amp; (uint8_t)0x00FF);
      }
      else
      {<!-- -->
        *huart->pRxBuffPtr + + = (uint8_t)(huart->Instance->DR & amp; (uint8_t)0x007F);
      }
    }

    if (--huart->RxXferCount == 0U)
    {<!-- -->
      /* Disable the UART Data Register not empty Interrupt */
      __HAL_UART_DISABLE_IT(huart, UART_IT_RXNE);

      /* Disable the UART Parity Error Interrupt */
      __HAL_UART_DISABLE_IT(huart, UART_IT_PE);

      /* Disable the UART Error Interrupt: (Frame error, noise error, overrun error) */
      __HAL_UART_DISABLE_IT(huart, UART_IT_ERR);

      /* Rx process is completed, restore huart->RxState to Ready */
      huart->RxState = HAL_UART_STATE_READY;

#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
      /*Call registered Rx complete callback*/
      huart->RxCpltCallback(huart);
#else
      /*Call legacy weak Rx complete callback*/
      HAL_UART_RxCpltCallback(huart);
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */

      return HAL_OK;
    }
    return HAL_OK;
  }
  else
  {<!-- -->
    return HAL_BUSY;
  }
}

Generally, the configuration of the serial port is 8-bit data and no parity. Every time a character is received, the program will go to line 26, which is this sentence:

*huart->pRxBuffPtr + + = (uint8_t)(huart->Instance->DR & amp; (uint8_t)0x00FF);

The right side of the equal sign can be understood as, I read the data directly from the data register of the UART module. The left side of the equal sign is an address, and then I put the read data on this address. What is this address? When I configured the receive interrupt before, I made it point to the address of a variable. That variable is a global variable I defined myself to store the data read through the serial port.
How to assign a value to an address? To use this * operator, combined with a pointer means to assign a value to this variable.
Judging from the results, it is to put the data received by the interrupt into a variable defined by myself. This complex process involves a lot of pointer knowledge. Why use pointers?

Principle overview

There are several reasons. Let me talk about the most basic point first. A knowledge point that you will definitely encounter in college is to use pointers to exchange values for two variables. Even if you can understand it, you can write this kind of program yourself, but when you encounter In actual development, it is probably very easy to be confused. The core point is that when you use a function to pass a variable in, essentially when you enter the function, an invisible temporary variable will be generated and the value of the parameter will be assigned to this temporary variable. , no matter how you operate this variable, it will not affect the variable of the parameter itself, because it is the temporary variable that is operated.
But if the address of this variable is passed, I can use the * operator to assign a value to the variable at the specified address to complete a value transfer operation.
Hmm I don’t know how to express it better. I feel that if you understand the pointer, you can understand it at a glance. If you don’t understand it, you won’t understand it even if you read it several times. I won’t write so many things, it will be over if the secret is revealed.
The second reason is, do you still remember the third parameter of that function, which is how many characters are received, and then what happens. If it is not 1, it is a case of multi-character reception, which will be analyzed later.

Code verification

Let’s first look at the simple test code for receiving a single character. The following callback function needs to be written by yourself.
Only this function needs to be changed. The other functions just take out some fragments to read separately and do not need to be changed.

uint8_t uart_rx_index = 0;
uint8_t uart_data_byte;

#define UART_RX_BUF_LEN 100
uint8_t uart_rx_buffer[UART_RX_BUF_LEN] = {<!-- -->0};

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{<!-- -->
        if(huart == ( & amp;huart1))
        {<!-- -->
                if(uart_rx_index < UART_RX_BUF_LEN)
                {<!-- -->
                        /* Character splicing */
                        uart_rx_buffer[uart_rx_index + + ] = uart_data_byte;
                }
                else /* Clear directly if it exceeds the maximum limit */
                {<!-- -->
                        uart_rx_index = 0;

                        memset(uart_rx_buffer, 0x00, UART_RX_BUF_LEN);
                }

                /* If a line feed and carriage return are received, it proves that one frame of data is completed */
                if((uart_rx_buffer[uart_rx_index - 2] == '\r') & amp; & amp; (uart_rx_buffer[uart_rx_index - 1] == '\\
'))
                {<!-- -->
                        printf("Rx Receive: %s\r\\
", uart_rx_buffer);

                        uart_rx_index = 0; /* Clear pointer */

                        memset(uart_rx_buffer, 0x00, UART_RX_BUF_LEN);
                }

                HAL_UART_Receive_IT( & amp;huart1, (uint8_t *) & amp;uart_data_byte, 1); //Re-enable the receive interrupt
        }
}

Note: In the actual development process, it is not recommended to use the printf function in interrupts. This thing can be understood to take more time than imagined.

The general meaning of the whole process is that I configure the callback function to be entered every time a character is received. In fact, it has something to do with the setting of the variable below. Because the configured value is 1, it seems that the callback function will be called every time data is received. The actual situation should be that the callback function will be called once when the count variable decrements to 0. (The same is the HAL library source code above, with some things deleted for easier reading)

 huart->RxXferSize = Size;
    huart->RxXferCount = Size;
if (--huart->RxXferCount == 0U)
{<!-- -->
    HAL_UART_RxCpltCallback(huart);
}

Splice all the received characters into an array. If a carriage return \r and a line feed \\
are received, print it out and start receiving again. There is no additional processing for overflow here, and an error message can be printed.
There is another question. The function UART_Receive_IT mentioned above has the operation of turning off the reception interrupt, so every time a character is received, it will enter the following branch and then execute the callback function, so it must be Re-enable the interrupt in the callback function.

if (--huart->RxXferCount == 0U)
{<!-- -->
  /* Disable the UART Data Register not empty Interrupt */
  __HAL_UART_DISABLE_IT(huart, UART_IT_RXNE);

  /* Disable the UART Parity Error Interrupt */
  __HAL_UART_DISABLE_IT(huart, UART_IT_PE);

  /* Disable the UART Error Interrupt: (Frame error, noise error, overrun error) */
  __HAL_UART_DISABLE_IT(huart, UART_IT_ERR);

  /* Rx process is completed, restore huart->RxState to Ready */
  huart->RxState = HAL_UART_STATE_READY;

#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
  /*Call registered Rx complete callback*/
  huart->RxCpltCallback(huart);
#else
  /*Call legacy weak Rx complete callback*/
  HAL_UART_RxCpltCallback(huart);
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */

  return HAL_OK;
}

Verification results

Fixed character reception

Assume that I want to receive 5 characters each time. The data structure is as follows:
Data header + Data + Checksum + Carriage Enter + Line feed

Interrupt configuration function

HAL_UART_Receive_IT( & amp;huart1, (uint8_t *) & amp;uart_rx_buffer, 5);

The second parameter changes from the address of a previous variable to the address of an array. The length is temporarily fixed at 100, and then the number of received bytes is 5. It can be understood that a callback function cannot be triggered until 5 bytes are received.
Assume that if there is a problem with the data sender, after sending 4 bytes at a time, it will return to normal and send 5 bytes each time. However, if there is no additional processing mechanism on the receiving side, it will definitely fail. I tested it myself. It also provides a simple protection mechanism.

Code verification

#define UART_RX_DATA_LEN 3
uint8_t sensor_data_buf[UART_RX_DATA_LEN + 2] = {<!-- -->0};

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{<!-- -->
        if(huart->Instance == USART1)
        {<!-- -->
                /* Being able to come in proves that 5byte data has been received */

                /* This cache is used directly without conversion */
                if(uart_rx_buffer[UART_RX_DATA_LEN] == '\r' & amp; & amp; uart_rx_buffer[UART_RX_DATA_LEN + 1] == '\\
')
                {<!-- -->
                        /* Simple checksum, equal to the sum of data [0][1], taking the lower 8 bits */
                        if(((uart_rx_buffer[0] + uart_rx_buffer[1]) & amp; 0xFF) == uart_rx_buffer[2])
                        {<!-- -->
                                printf("Sensor Data:");
                                for(uint8_t i = 0; i < UART_RX_DATA_LEN; i + + )
                                {<!-- -->
                                        sensor_data_buf[i] = uart_rx_buffer[i];
                                        if(i == UART_RX_DATA_LEN - 1)
                                                printf("%d\r\\
", sensor_data_buf[i]);
                                        else
                                                printf("%d-", sensor_data_buf[i]);
                                }
                        }
                }

                __HAL_UART_FLUSH_DRREGISTER( & amp;huart1); /* Clear the serial port data register to prevent excess data from affecting the next reception */
                
                HAL_UART_Receive_IT( & amp;huart1, (uint8_t *) & amp;uart_rx_buffer, 5); /* Re-enable a receive interrupt */
        }
}

As mentioned before, one of the functions of configuring the number of bytes received is to enter the callback function only after receiving the specified number of characters.
There will also be detailed tests later to see what the impact will be if less or more data is sent.
The logic of this callback function is that after receiving 5 bytes, it is judged whether the final carriage return and line feed are, and if so, it is further judged whether the received data conforms to the simple verification logic. If not, it proves that the data is out. No other processing will be done to solve the problem, and the next data will be reconfigured, interrupted, etc.
If the data meets the conditions, assign the received data to the place you need. Here we assume a set of sensor_data_buf variables. If the data is normal, it will be received and printed.

Serial port assistant configuration


Check HEX to send, and then fill in the data to be sent in hexadecimal form, leaving a space between each byte of data.

Verification results

[Picture]
5byte data can only be printed if there is a carriage return and line feed and the checksum is correct.
[Picture]
5byte data, but the checksum is wrong and it is not printed.
[Picture]
6byte data is sent, but the program configured to receive 5byte data meets the conditions, and a program to clear the data register is added, so it can print normally.
[Picture]
If the data is less than 5 bytes, it cannot be printed normally.

Develop programs using pointers

One of the things I mentioned earlier seems to have not been finished yet. I happened to be feeling a little emotional while writing this diary.
During the C language course in college, the teacher directly said that pointers are the most difficult knowledge point. In fact, he was right, but it scared everyone. At the same time, in the following few years, he did not do any specific work. During the study process of the project, I read various textbooks to learn about pointers. The one that impressed me most was the book “The Art of Pointers” that I had read before. I also learned it a bit vaguely because I really didn’t know when I would use it. This kind of thing.
In my second year at work, I suddenly thought of starting some projects. Then, without any reference to third-party open source projects, I began to integrate some commonly used driver frameworks, such as LCD and OLED. In this process, you will often encounter a problem, such as:

  1. How to pass an array as a parameter to a function?
  2. What is the relationship between one-dimensional arrays and two-dimensional arrays, and how can they be converted?
  3. If the return value of a function is fixed in two states: 0/1, but I also want to return a counting variable, how should I deal with it?

Most functions in the novice stage can be implemented without using pointers, and you can choose to use some stupid methods to implement them. Let’s take the first point as an example. If I don’t know how to pass in an array as a parameter, then I won’t pass the parameter and operate the array directly in the function. This is actually no problem, but what if there is more than one array that needs to be operated? The way to deal with this method is to configure a function for each array that needs to be processed.
If the array processing operations inside the function are all the same, it would be very inappropriate. The more arrays that need to be processed, the more Flash will be wasted. But if you can understand the relationship between arrays and pointers, it will be easier to solve this problem. Pass the address of the array, which is the pointer, to the function. Inside the function, you can increment the address through a for loop, and then read each address. Or assignment operation, to complete the processing of the array, you can use a function to complete the processing of multiple arrays of the same type.
As for the third point, the description may not be perfect. The specific situation is that the return value is several fixed state values. I need to pass this value back after calculation through the function, but the return value of the function can only be one. , write a simple code to help understand.
In a situation like the one below, if I want to pass in the variable Data and pass it out after being processed by a function, it will not be possible.

#define STATUS_OK 1
#define STATUS_ERROR 2

uint8_t Data;

uint8_t ReadData(uint8_t data)
{<!-- -->
        data >>= 1;
        if(data == 0)
        {<!-- -->
                return STATUS_ERROR;
        }
        else
        {<!-- -->
                return STATUS_OK;
        }
}

int main(void)
{<!-- -->
        uint8_t tmp_status = 0, tmp_data = 0;

        Data = 0x01;

        tmp_status = ReadData(Data);
        
        /* Data is passed in, but nothing actually changes */

        return 1;
}

As mentioned earlier, no matter how much the variable passed in is manipulated, after the function is executed, the local variable that was manipulated is unregistered, and the variable passed in will still not change. But if you pass the address of the variable in like the HAL library and assign a value to this address in the function, you can achieve your goal.

int main(void)
{<!-- -->
        uint8_t tmp_status = 0;

        Data = 0x01;

        tmp_status = ReadData( & amp;Data);

        if(tmp_status == STATUS_OK)
        {<!-- -->

        }
        else if(tmp_status == STATUS_ERROR)
        {<!-- -->

        }

        return 1;
}

Haha, if there is still a problem, the probability is that since the return value needs to be judged, it proves that not all situations must be able to operate on that variable, so how should it be handled? Is it a matter of adding a local variable

int main(void)
{<!-- -->
        uint8_t tmp_status = 0, tmp_data = 0;

        Data = 0x01;

        tmp_data = Data;

        tmp_status = ReadData( & amp;tmp_data);

        if(tmp_status == STATUS_OK)
        {<!-- -->
               Data = tmp_data;
        }
        else if(tmp_status == STATUS_ERROR)
        {<!-- -->

        }

        return 1;
}

That’s probably what it looks like