STM32CubeMX series 04 - serial port (query, interrupt, DMA, variable length reception, redirection)

====>>> Article summary (with code summary) <<<====

1. Hardware used

Punctual atom Mini board, the main control STM32F103RCT6.

Peripherals used: LED, serial port 1 (PA9, PA10). Schematic:
insert image description here

2. Generate project

2.1. Create project selection master

insert image description here

2.2. System configuration

Configure clock source
insert image description here

Configure the debug mode (you can check it if you need ST-Link download and debugging)
insert image description here

Configure the clock tree (you can directly enter 72 in HCLK, and then press Enter to configure automatically)
insert image description here

2.3. Configuration project directory

insert image description here

If checked, c files and header files will be generated separately
insert image description here

2.4. Peripherals used in configuration

PA8:LED0。
insert image description here

3. Query mode

3.1. Configure the serial port

insert image description here

3.2. Generate code

insert image description here

3.3. Writing code

write main.ccode

int main(void)
{
    
    
  /* USER CODE BEGIN 1 */
	char light_on[11] = "Light on \r\n";
	char light_off[12] = "Light off \r\n";
	char rec_buf[1] = {
    
    0};
  /* USER CODE END 1 */

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* Configure the system clock */
  SystemClock_Config();

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();

  /* USER CODE BEGIN WHILE */
  while (1)
  {
    
    
  	  // 串口1、接收数据的数组、接收的长度、等待时间
	  if(HAL_OK == HAL_UART_Receive(&huart1, (uint8_t *)rec_buf, 1, 0xFFFF))
	  {
    
    
			if(rec_buf[0] == 'A')
			{
    
    
				// 串口1、发送的数据、发送的长度(单位:字节)、超时时间
				HAL_UART_Transmit(&huart1, (uint8_t *)light_on, 11, 0xFFFF);
				HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);
			}
			else
			{
    
    
				// 串口1、发送的数据、发送的长度(单位:字节)、超时时间
				HAL_UART_Transmit(&huart1, (uint8_t *)light_off, 12, 0xFFFF);
				HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET);
			}
	  }
	  
	  
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

3.4. Effect verification

Compile, burn, and view the results.
Effect: Use the serial port debugging assistant
insert image description here
to send a character A to the computer, and the microcontroller will send Light on to the computer after receiving it, and the LED will be on at the same time.
The computer sends a character B (other characters), and the microcontroller sends Light off to the computer after receiving it, and the LED turns off at the same time.

4. Interrupt mode

4.1. Configure the serial port

insert image description here

configuration interrupt
insert image description here

4.2. Generate code

insert image description here

4.3. Operating principle and code analysis

Same process as analyzing interrupts . Open the project, stm32f1xx_it.hyou can see the function in USART1_IRQHandler, obviously, this is the serial port interrupt processing function, and the function is called in the interrupt processing functionHAL_UART_IRQHandler(&huart1);

insert image description here

Then go and see HAL_UART_IRQHandler(&huart1);what the functions do. Many situations have been considered in HAL_UART_IRQHandler(&huart1);the function. It can be seen from the comments that if there is no error, the function will be called UART_Receive_IT(huart);and then returned.
insert image description here

Therefore, let's look at the function UART_Receive_IT(huart);. There are many situations in this function. Let's look directly at the end. There is a function at the end. As HAL_UART_RxCpltCallback(huart);can be seen from the comments, this is a callback function for receiving completion. (After learning the configuration, be sure to read Section 7.1, about the trigger conditions of this function)
insert image description here

Then look at this callback function. Still in this file, the definition is as follows.
insert image description here

It can be seen that this function is a weak function, and the user can define the function again. That is, we can redefine this function and write our interrupt handling logic in the function.

The comment is also very clear:
when a callback is required, this function should not be modified, HAL_UART_RxCpltCallback can be implemented in the user file

This paragraph can be placed anywhere. For large projects, you can create a file and put it in; here it is directly placed in main.c.

/* USER CODE BEGIN PV */
char light_on[11] = "Light on \r\n";
char light_off[12] = "Light off \r\n";
char rec_buf[1] = {
    
    0};
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    
    
	// 判断是不是串口1 中断
	if(huart->Instance == USART1)
	{
    
    
		if(rec_buf[0] == 'A')
		{
    
    
			// 串口1、发送的数据、发送的长度(单位:字节)、超时时间
			HAL_UART_Transmit(&huart1, (uint8_t *)light_on, 11, 0xFFFF);
			HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);
		}
		else
		{
    
    
			// 串口1、发送的数据、发送的长度(单位:字节)、超时时间
			HAL_UART_Transmit(&huart1, (uint8_t *)light_off, 12, 0xFFFF);
			HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET);
		}
		// 重新使能接收中断
		HAL_UART_Receive_IT(&huart1, (uint8_t *)rec_buf, 1);
	}
}
/* USER CODE END PFP */

int main(void)
{
    
    
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* Configure the system clock */
  SystemClock_Config();

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
	// 开启串口接收中断
	HAL_UART_Receive_IT(&huart1, (uint8_t *)rec_buf, 1);
	// 发送开始提示信息
	HAL_UART_Transmit(&huart1, (uint8_t *)"start...\r\n", 10, 0xFFFF);
  while (1)
  {
    
    
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

4.4. Effect verification

Compile, burn, and view the results.

Effect: Use the serial port debugging assistant
insert image description here

  1. Reset the development board, the computer receives start...
  2. The computer sends the character A, and the microcontroller sends Light on to the computer after receiving it, and the LED lights up at the same time;
  3. The computer sends the character B (other characters are also acceptable), and the MCU sends Light off to the computer after receiving it, and the LED turns off at the same time.

5. DMA mode

5.1. Configure the serial port

insert image description here

Configure DMA

DMA ( Direct Memory Access) direct memory access, which can transfer data between memory and peripherals (or from memory to memory) without using the CPU.
STM32 has up to 2 DMA controllers (DMA2 only exists in high-capacity products), STM32F103RCT6 has two DMA controllers, DMA1 and DMA2, and DMA1 has 7 channels. DMA2 has 5 channels.

Receive channel (set to receive in a loop)insert image description here

Send channel (set to normal mode)
insert image description here

DMA Request:

  • USART1_TX: Indicates that the DMA channel is used when sending data.
  • USART1_RX: Indicates that the DMA channel is used when receiving data.

Mode:

  • Normal mode: Indicates that the CPU initiates a DMA transfer request, and the data is transferred through the DMA channel, and an interrupt is triggered after the transfer is completed;
  • Circular mode means that the data will be transmitted cyclically, and then restart from the beginning after the transmission is completed.

Increment Address: Check the memory here, and uncheck the peripherals, because the address of the peripheral (here is the serial port) is fixed, you only need to transfer the starting address of the data, and the addresses can be continuously accumulated.

Check the interrupt (the DMA interrupt priority setting is lower than the serial port interrupt, but it can run even if it is not low).
insert image description here

5.2. Generate code

insert image description here

5.3. Writing code

/* USER CODE BEGIN PV */
char buf[13] = "running... \r\n";
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
    
    
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* Configure the system clock */
  SystemClock_Config();

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();

  /* USER CODE BEGIN WHILE */
	
	  // 开启DMA接收数据通道,将串口数据存到buf数组。
	  // 这里设置的循环模式,会不断接收串口数据到内存。
	  HAL_UART_Receive_DMA(&huart1, (uint8_t *)buf, 13);
	
	// 串口其他 DMA 相关函数
//	HAL_UART_DMAResume(&huart1); 恢复串口DMA
//	HAL_UART_DMAPause(&huart1) 暂停串口DMA
//	HAL_UART_DMAStop(&huart1); 结束串口DMA

  while (1)
  {
    
    
	  // 开启DMA发送传输通道
	  // 这里设置的普通模式,发送一次就关闭了。延迟一段时间后,重新打开发送。
	  HAL_UART_Transmit_DMA(&huart1, (uint8_t *)buf, 13);
	  HAL_Delay(1000);

    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

There is a bug here.

  • MX_DMA_Init();
  • MX_USART1_UART_Init();

For these two initializations, the DMA must be in the front and the USART must be in the back, otherwise the compilation will pass, but the operation will be stuck.
If the DMA and serial ports are set up from the beginning, there is generally no problem. However, if only the serial port is set at the beginning, and then DMA is added after the code is generated, it is easy for the DMA to be behind.
In addition: You can set the sequence by setting.
insert image description here

5.4. Effect verification

Compile, burn, and view the results.

Effect: Use the serial port debugging assistant
insert image description here

  1. After initialization, the serial port assistant receives the string running... every second
  2. Use the serial port assistant to send any character (such as 1234567890ab) to the development board, the character will be stored in the buf array, and then the serial port assistant will continue to receive the arbitrary string sent (if the length of the sent string is less than 13 characters, it will only be replaced The first few characters, the last few are still the original).
    insert image description here

6. Serial port redirection

In order to send data conveniently, we use printfa function to send data, but using this function needs to be configured first.

6.1. Configuration principle

printfThe function is defined in <stdio.h>the header file, and the printf function prints to std::out (standard output) according to the format given by the format string.
insert image description here
insert image description here
The printf function calls a lower-level I/O function: fputcto print characters one by one. fputc is also defined in the header file <stdio.h>.
insert image description here
insert image description here
Therefore, if we want to use printf to print data to the serial port, we only need to redefine the fputc function, and send the data through the serial port in the fputc function, which is called: fputc redirection or printf redirection .

6.2. Implementation

The configuration is still the above three configurations, whatever.

6.2.1. Using the MicroLIB library

MicroLib is a highly optimized library for the standard C library, which is used by MDK by default. In contrast, MicroLIB has less code and takes up less resources.

Step 1: Use MicroLib to redirect printf in MDK
insert image description here
Step 2: Add redirection code in the generated usart.c. put it here at the end
insert image description here

/* USER CODE BEGIN 1 */
/**********************printf重定向****************************/
// 需要调用stdio.h文件
#include <stdio.h>
int fputc(int ch, FILE *f)
{
    
      
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
	return ch;
}
/* USER CODE END 1 */

main.c

  while (1)
  {
    
    
      printf("Hello, %s \r\n", "world");		// 字符串
      printf("Test int: i = %d \r\n", 100);		// 整数
      printf("Test float: i = %f \r\n", 1.234);	// 浮点数
      printf("Test hex: i = 0x%2x \r\n",100);	// 16进制
	  printf("中文测试 \r\n");					// 中文
	  HAL_Delay(2000);
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
  }

Effect verification
Compile, burn, and view the results.

Effect: Use the serial port debugging assistant
insert image description here
to send data every two seconds.

6.2.2. Without using the MicroLIB library

Do not check Use MicroLIB, directly add the following code in the usart.c file.
insert image description here

// 需要调用stdio.h文件
#include <stdio.h>
//取消ARM的半主机工作模式
#pragma import(__use_no_semihosting)//标准库需要的支持函数                 
struct __FILE 
{
    
     
	int handle; 
}; 
FILE __stdout;       
void _sys_exit(int x) //定义_sys_exit()以避免使用半主机模式
{
    
     
	x = x;
} 

int fputc(int ch, FILE *f)
{
    
      
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
	return ch;
}

main.c

  while (1)
  {
    
    
      printf("Hello, %s \r\n", "world");		// 字符串
      printf("Test int: i = %d \r\n", 100);		// 整数
      printf("Test float: i = %f \r\n", 1.234);	// 浮点数
      printf("Test hex: i = 0x%2x \r\n",100);	// 16进制
	  printf("中文测试 \r\n");					// 中文
	  HAL_Delay(2000);
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
  }

Effect verification
Same as above.
Every two seconds, send data.
PS: When using printf in other files, there will be a warning function “printf” declared implicitly , which does not affect the use. If you don't want to see the warning, you can add the #include <stdio.h> header file to the called file .

7. Advanced - receiving variable length data

7.1. Supplementary explanation of serial port interrupt function and callback function ⭐⭐⭐

Every time the serial port interrupt function
receives a byte, it will enter the serial port interrupt function once. If a data frame sends multiple bytes, it will enter multiple interrupt processing functions continuously.

void USART1_IRQHandler(void)For example: send "0123456789" to the MCU through the serial port assistant (do not check the send new line), then the MCU will enter the interrupt processing function 10 times continuously .

The receiving completion callback function
will be executed every time the receiving completion callback function is completed.

In the previous interrupt chapter, it is said that the reception completion callback function will be executed after the reception is completed void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart), but it is not specified when the reception is completed.

You can see the following line of code. Obviously, here is to receive 10 bytes and put it into the rec_buf array. Then, receiving enough 10 bytes means that the reception is complete.

// 开启串口接收中断
HAL_UART_Receive_IT(&huart1, (uint8_t *)rec_buf, 10);

What if I only send 5 bytes? Then only 5 interrupts will be entered, and the receiving completion callback function will not be executed. When 5 bytes are sent again, the receiving completion callback function will be executed when the fifth interrupt is executed.
And if, send 8 characters? Then it will only enter 8 interrupts, and will not execute the receiving completion callback function. When sending 8 bytes again, it will execute 2 interrupts and execute the callback function, and then execute the interrupt function again and it will be stuck...

Example
The serial port configuration is the same as that in the interrupt chapter: open the serial port and check the interrupt.
main.c

char rec_buf[10] = {
    
    0};
volatile uint8_t num = 0;	 	 // 记录进入中断的次数
volatile uint8_t numback = 0;	 // 记录进入回调的次数

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    
    
	// 判断是不是串口1 中断
	if(huart->Instance == USART1)
	{
    
    
		printf("已经进入了 %d 次中断函数了 \r\n", num);
		printf("第 %d 次进入回调 \r\n", ++numback);
		// 重新开启接收
		HAL_UART_Receive_IT(&huart1, (uint8_t *)rec_buf, 10);
	}
}

int main(void)
{
    
    
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();

	// 开启串口接收中断	
	HAL_UART_Receive_IT(&huart1, (uint8_t *)rec_buf, 10);
  while (1)
  {
    
    
		printf("已经进入了 %d 次中断函数了 \r\n", num);
		HAL_Delay(1000);
  }
}

stm32f1xx_it.c

extern volatile uint8_t num;

void USART1_IRQHandler(void)
{
    
    
	num++; // 记录进入中断的次数
  HAL_UART_IRQHandler(&huart1);
}

Send "1234567890" 10 characters (10 bytes), you can see that it will enter 10 interrupts, and at the last interrupt, just enough 10 bytes are received, and the callback function is executed.
insert image description here
When sending "12345" 5 characters (5 bytes), 5 interrupts are entered, but the reception is not completed. If 5 characters are sent again, 5 interrupts will be entered again, and then received at the 5th interrupt Done, the callback function is executed.
insert image description here
And if you send "12345678" 8 characters (8 bytes), 8 interrupts are entered, but the reception is not completed. If you send 8 characters again, the second interrupt will execute the callback function and enter the interrupt again. After that, clicking send is useless. . .
insert image description here
In short, the previous three schemes can only handle data with a relatively fixed data length.

7.1. DMA+idle interrupt

Let me talk about the interrupt of the serial port first. Mainly, two serial ports receive interrupts and serial port idle interrupts .

  • Serial receiving interrupt (UART_IT_RXNE): A frame of data is a meaningful string consisting of one or more bytes, such as a string sent in the previous case. In the process of sending a frame of data, every time a byte is received, it will enter the serial port interrupt function once. If the conditions for receiving completion are met, the callback function for sending completion will also be executed in the interrupt function.
  • Serial port idle interrupt (UART_IT_IDLE): A string (a frame of data) sent above may contain multiple bytes, and it will continue to enter the receiving interrupt function, so the time between sending two bytes is very short, and the serial port cannot be considered as idle . But after the entire string (one frame of data) is sent, the serial port does not send data again within the time that one byte can be sent. At this time, an idle interrupt will occur.

Summarize:

receive interrupt idle interrupt
processing function USARTx_IRQHandler() USARTx_IRQHandler()
Callback HAL_UART_RxCpltCallback() The HAL library does not provide
bits in the USART status register UART_IT_RXNE UART_IT_IDLE
Triggering conditions Receive a byte of data to trigger once After receiving one frame of data, another byte time has elapsed and no data has been received

In the above program, when the DMA serial port reception starts, the DMA channel will continuously transfer the sent data to the memory, that is to say, the receiving program is running all the time, so how to judge whether the serial port reception is completed and close the DMA channel in time? How to know the length of the received data in order to receive variable length? The answer is to use the serial port idle interrupt.

Solution

  1. The serial port receives the data, and the DMA continuously transfers the data to the memory.
  2. After a frame of data is sent, the serial port is temporarily idle, and the serial port idle interrupt is triggered.
  3. In the interrupt service function, judge whether an idle interrupt occurs according to the flag bit of the idle interrupt, and can calculate how many bytes of data have just been received.
  4. Store the received data, clear the flag bit, and start receiving the next frame.

Code (configuration is the configuration of DMA mode)
main.c

/* USER CODE BEGIN PV */
// 自己定义两个变量
uint8_t rx_buffer[100];   			// 接收数据的数组
volatile uint8_t rx_len = 0; 		// 接收数据的长度
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
    
    
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* Configure the system clock */
  SystemClock_Config();

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();

  /* USER CODE BEGIN WHILE */
	  //开启空闲中断
	  __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
	  // 开启DMA接收数据通道,将串口数据存到buf数组。
	  HAL_UART_Receive_DMA(&huart1, (uint8_t *)rx_buffer, 100);
  while (1)
  {
    
    
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

stm32f1xx_it.c

/* USER CODE BEGIN PV */

// 定义在main.c中,在此处声明
extern uint8_t rx_buffer[100];   			// 接收数据的数组
extern volatile uint8_t rx_len; 			// 接收数据的长度

/* USER CODE END PV */

/**
  * @brief This function handles USART1 global interrupt.
  */
void USART1_IRQHandler(void)
{
    
    
  /* USER CODE BEGIN USART1_IRQn 0 */

  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */

	// 获取IDLE状态
	uint8_t tmp_flag = __HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE); 
	if((tmp_flag != RESET))	// 判断接收是否结束
	{
    
     
		__HAL_UART_CLEAR_IDLEFLAG(&huart1);	// 清除空闲中断标志
		
		HAL_UART_DMAStop(&huart1); 			// 停止DMA通道
		// 查询DMA剩余传输数据个数
		uint8_t temp = __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
		//	最大接收长度 - 剩余长度 = 已发送长度	
		rx_len = 100 - temp;
		/* === 在下面写自己的处理逻辑(注意这里是中断,尽量简短) === */ 
		
		// 发送接收到的数据
		HAL_UART_Transmit_DMA(&huart1, rx_buffer, rx_len);
		
		/* === 在上面写自己的处理逻辑(注意这里是中断,尽量简短) === */ 
		// 重新开启DMA
		HAL_UART_Receive_DMA(&huart1,rx_buffer,100);
	}

  /* USER CODE END USART1_IRQn 1 */
}

Effect verification
No matter how long the data is sent to the development board, it will be sent back again.
insert image description here

7.2. Interrupt + ring buffer

The code is a bit long, the code link is on the home page, and the code has detailed comments and instructions for use.

Guess you like

Origin blog.csdn.net/weixin_46253745/article/details/127807452
Recommended