Article Directory
====>>> 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:
2. Generate project
2.1. Create project selection master
2.2. System configuration
Configure clock source
Configure the debug mode (you can check it if you need ST-Link download and debugging)
Configure the clock tree (you can directly enter 72 in HCLK, and then press Enter to configure automatically)
2.3. Configuration project directory
If checked, c files and header files will be generated separately
2.4. Peripherals used in configuration
PA8:LED0。
3. Query mode
3.1. Configure the serial port
3.2. Generate code
3.3. Writing code
write main.c
code
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
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
configuration interrupt
4.2. Generate code
4.3. Operating principle and code analysis
Same process as analyzing interrupts . Open the project, stm32f1xx_it.h
you 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);
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.
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)
Then look at this callback function. Still in this file, the definition is as follows.
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
- Reset the development board, the computer receives start...
- 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;
- 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
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)
Send channel (set to normal mode)
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).
5.2. Generate code
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.
5.4. Effect verification
Compile, burn, and view the results.
Effect: Use the serial port debugging assistant
- After initialization, the serial port assistant receives the string running... every second
- 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).
6. Serial port redirection
In order to send data conveniently, we use printf
a function to send data, but using this function needs to be configured first.
6.1. Configuration principle
printf
The 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.
The printf function calls a lower-level I/O function: fputc
to print characters one by one. fputc is also defined in the header file <stdio.h>.
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
Step 2: Add redirection code in the generated usart.c. put it here at the end
/* 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
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.
// 需要调用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
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.
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.
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. . .
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
- The serial port receives the data, and the DMA continuously transfers the data to the memory.
- After a frame of data is sent, the serial port is temporarily idle, and the serial port idle interrupt is triggered.
- 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.
- 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.
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.