前提
在开发STM32过程中,芯片提供的串口引脚一般是不会发生变化的,所以为了方便移植,借助HAL提供的注册回调函数自定义,这边重新进行简要的封装
此工程开发是以Clion为开发的IDE,用keil只需将对应的文件进行移植即可.
文章末尾附带gitee工程地址
工程创建(以STM32F103C8T6为例)
1.参考Stm32开发环境从0搭建(Clion作为开发软件)
2.开启自定义注册串口硬件注册回调(方便后面工程移植,也可以使用HAL默认的硬件回调)
3. 打开工程并测试
添加自定义的项目路径(方便后期移植)
1.创建目录框架
2. 编写CMakeLists文件t和CMakeLists模板文件(此步骤是将自定义目录框架包含到项目中)
3. 创建同一的头文件去管理
统一串口宏定义
头文件 (bsp_serial_define.h)
#ifndef STM32_VET6_BSP_SERIAL_DEFINE_H
#define STM32_VET6_BSP_SERIAL_DEFINE_H
#include "sys_driver_include.h"
// 串口只支持异步
/***********************************************************串口1相关宏定义*******************************************************/
#define USE_COM1_ENABLE (1)
#define USE_COM1_IRQ_ENABLE (0) // 串口1中断使能
#define USE_COM1_DMA_RX_ENABLE (0) // 串口1 DMA RX 使能
#define COM1_TX_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()
#define COM1_TX_PORT GPIOA
#define COM1_TX_PIN GPIO_PIN_9
#define COM1_RX_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()
#define COM1_RX_PORT GPIOA
#define COM1_RX_PIN GPIO_PIN_10
#define COM1_IRQ_Priority 5 // 抢占优先级
#define COM1_IRQ_SubPriority 0 // 响应优先级
#define COM1_DMA_RX_CHANNEL DMA1_Channel5
#define COM1_DMA_CLK_ENABLE() __HAL_RCC_DMA1_CLK_ENABLE()
#define COM1_DMA_IRQ DMA1_Channel5_IRQn
#define COM1_IRQ_HANDLE DMA1_Channel5_IRQHandler
extern UART_HandleTypeDef com1_handle;
/***********************************************************串口2相关宏定义*******************************************************/
#define USE_COM2_ENABLE (0)
#define USE_COM2_IRQ_ENABLE (0) // 串口2中断使能
#define USE_COM2_DMA_RX_ENABLE (0) // 串口1 DMA RX 使能
/**@Details 引脚定义*/
#define COM2_TX_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()
#define COM2_RX_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()
#define COM2_TX_PIN GPIO_PIN_2
#define COM2_RX_PIN GPIO_PIN_3
#define COM2_TX_PORT GPIOA
#define COM2_RX_PORT GPIOA
#define COM2_IRQ_Priority 5 // 抢占优先级
#define COM2_IRQ_SubPriority 0 // 响应优先级
#define COM2_DMA_RX_CHANNEL DMA1_Channel6
#define COM2_DMA_CLK_ENABLE() __HAL_RCC_DMA1_CLK_ENABLE()
#define COM2_DMA_IRQ DMA1_Channel6_IRQn
#define COM2_IRQ_HANDLE DMA1_Channel6_IRQHandler
extern UART_HandleTypeDef com2_handle;
/***********************************************************串口3相关宏定义*******************************************************/
#define USE_COM3_ENABLE (0)
#define USE_COM3_IRQ_ENABLE (1) // 串口3中断使能
#define USE_COM3_DMA_RX_ENABLE (1) // 串口3 DMA RX 使能
#define COM3_TX_GPIO_CLK_ENABLE() __HAL_RCC_GPIOD_CLK_ENABLE()
#define COM3_TX_PORT GPIOD
#define COM3_TX_PIN GPIO_PIN_8
#define COM3_RX_GPIO_CLK_ENABLE() __HAL_RCC_GPIOD_CLK_ENABLE()
#define COM3_RX_PORT GPIOD
#define COM3_RX_PIN GPIO_PIN_9
#define COM3_IRQ_Priority 5 // 抢占优先级
#define COM3_IRQ_SubPriority 0 // 响应优先级
#define COM3_DMA_CLK_ENABLE() __HAL_RCC_DMA1_CLK_ENABLE()
#define COM3_DMA_RX_CHANNEL DMA1_Channel3
#define COM3_DMA_IRQ DMA1_Channel3_IRQn
#define COM3_IRQ_HANDLE DMA1_Channel3_IRQHandler
extern UART_HandleTypeDef com3_handle;
/***********************************************************串口4相关宏定义*******************************************************/
#define USE_COM4_ENABLE (0)
#define USE_COM4_IRQ_ENABLE (1) // 串口4中断使能
#define USE_COM4_DMA_RX_ENABLE (1) // 串口4 DMA RX 使能
#define COM4_TX_GPIO_CLK_ENABLE() __HAL_RCC_GPIOC_CLK_ENABLE()
#define COM4_RX_GPIO_CLK_ENABLE() __HAL_RCC_GPIOC_CLK_ENABLE()
#define COM4_TX_PORT GPIOC
#define COM4_RX_PORT GPIOC
#define COM4_TX_PIN GPIO_PIN_10
#define COM4_RX_PIN GPIO_PIN_11
#define COM4_IRQ_Priority 5 // 抢占优先级
#define COM4_IRQ_SubPriority 0 // 响应优先级
#define COM4_DMA_CLK_ENABLE() __HAL_RCC_DMA2_CLK_ENABLE()
#define COM4_DMA_RX_CHANNEL DMA2_Channel3
#define COM4_DMA_IRQ DMA2_Channel3_IRQn
#define COM4_DMA_IRQ_HANDLE DMA2_Channel3_IRQHandler
extern UART_HandleTypeDef com4_handle;
#endif //STM32_VET6_BSP_SERIAL_DEFINE_H
串口核心文件
头文件(bsp_serial.h)
#ifndef STM32_VET6_BSP_SERIAL_H
#define STM32_VET6_BSP_SERIAL_H
#include "bsp_serial_define.h"
#include "bsp_serial_com1.h"
void Com_Init_01(USART_TypeDef *uart, uint32_t baud);
void Com_Init(UART_HandleTypeDef *comHandle, UART_InitTypeDef *config);
void Com_RegisterCallback(UART_HandleTypeDef *comHandle);
void Com_Dma_Init(DMA_HandleTypeDef *dmaHandle, DMA_InitTypeDef *config);
void UART_MspInit(UART_HandleTypeDef *comHandle);
void UART_MspDeInit(UART_HandleTypeDef *comHandle);
void UART_RxEventCallback(UART_HandleTypeDef *com_handle, uint16_t Pos);
#endif //STM32_VET6_BSP_SERIAL_H
源文件(bsp_serial.c)
#include "bsp_serial.h"
static UART_InitTypeDef init_cnf = {
.OverSampling = UART_OVERSAMPLING_16,
.Mode = UART_MODE_TX_RX,
.HwFlowCtl = UART_HWCONTROL_NONE,
.Parity = UART_PARITY_NONE,
.StopBits = UART_STOPBITS_1,
.WordLength = UART_WORDLENGTH_8B,
.BaudRate = 9600
};
static DMA_InitTypeDef default_dma_config = {
.Direction = DMA_PERIPH_TO_MEMORY,
.PeriphInc = DMA_PINC_DISABLE,
.MemInc = DMA_MINC_ENABLE,
.PeriphDataAlignment = DMA_PDATAALIGN_BYTE,
.MemDataAlignment =DMA_MDATAALIGN_BYTE,
.Mode = DMA_NORMAL,
.Priority = DMA_PRIORITY_LOW
};
/**
* 通用串口初始化
* @param uart 串口
* @param baud
*/
void Com_Init_01(USART_TypeDef *uart, uint32_t baud) {
init_cnf.BaudRate = baud;
if (uart == NULL) {
}
#if USE_COM1_ENABLE
else if (uart == USART1) {
Com_Init(&com1_handle, &init_cnf);
}
#endif
#if USE_COM2_ENABLE
else if (uart == USART2) {
Sw_Com_Init(&com2_handle, &init_cnf);
}
#endif
#if USE_COM3_ENABLE
else if (uart == USART3) {
Sw_Com_Init(&com3_handle, &init_cnf);
}
#endif
#if USE_COM4_ENABLE
else if (uart == UART4) {
Sw_Com_Init(&com4_handle, &init_cnf);
}
#endif
}
/**
* @brief 通用串口初始化
* @param comHandle
* @param config
*/
void Com_Init(UART_HandleTypeDef *comHandle, UART_InitTypeDef *config) {
#if USE_HAL_UART_REGISTER_CALLBACKS
comHandle->MspDeInitCallback = UART_MspDeInit;
comHandle->MspInitCallback = UART_MspInit;
#endif
UART_InitTypeDef *ptr = NULL;
if (config != NULL) {
ptr = config;
} else {
ptr = &init_cnf;
}
memcpy(&comHandle->Init, ptr, sizeof(UART_InitTypeDef));
if (HAL_UART_Init(comHandle) != HAL_OK) {
common_error_handle(__FILE__, __LINE__);
}
}
/**
* @brief 注册接收事件回调
* @param comHandle
*/
void Com_RegisterCallback(UART_HandleTypeDef *comHandle) {
#if USE_HAL_UART_REGISTER_CALLBACKS
HAL_UART_RegisterRxEventCallback(comHandle, UART_RxEventCallback);
#endif
}
void UART_MspInit(UART_HandleTypeDef *comHandle) {
#if USE_COM1_ENABLE
if (comHandle->Instance == USART1) {
Com1_MspInit();
}
#endif
#if USE_COM2_ENABLE
if (comHandle->Instance == USART2) {
Com2_MspInit();
}
#endif
#if USE_COM3_ENABLE
if (comHandle->Instance == USART3) {
Com3_MspInit();
}
#endif
#if USE_COM4_ENABLE
if (comHandle->Instance == UART4) {
Com4_MspInit();
}
#endif
}
void UART_MspDeInit(UART_HandleTypeDef *comHandle) {
#if USE_COM1_ENABLE
if (comHandle->Instance == USART1) {
Com1_MspDeInit();
}
#endif
#if USE_COM2_ENABLE
if (comHandle->Instance == USART2) {
Com2_MspDeInit();
}
#endif
#if USE_COM3_ENABLE
if (comHandle->Instance == USART3) {
Com3_MspDeInit();
}
#endif
#if USE_COM4_ENABLE
if (comHandle->Instance == UART4) {
Com4_MspDeInit();
}
#endif
}
/**
* @brief 串口接收事件
* @param com_handle
* @param Pos
*/
void UART_RxEventCallback(UART_HandleTypeDef *com_handle, uint16_t Pos) {
if (com_handle == NULL) {
common_error_handle(__FILE__, __LINE__);
}
#if (USE_COM1_ENABLE == 1)
else if (com_handle->Instance == USART1) {
Com1_RxEvent(Pos);
}
#endif
#if (USE_COM2_ENABLE == 1)
else if (comHandle->Instance == USART2) {
Com2_RxEvent(Pos);
}
#endif
#if (USE_COM3_ENABLE == 1)
else if (comHandle->Instance == USART3) {
Com3_RxEvent(Pos);
}
#endif
#if (USE_COM4_ENABLE == 1)
else if (comHandle->Instance == UART4) {
Com4_RxEvent(Pos);
}
#endif
}
/**
* 错误回调
* @param comHandle 串口句柄
*/
void HAL_UART_ErrorCallback(UART_HandleTypeDef *comHandle) {
switch (comHandle->ErrorCode) {
case HAL_UART_ERROR_PE:
__HAL_UART_CLEAR_FLAG(comHandle, UART_FLAG_PE);
break;
case HAL_UART_ERROR_NE:
__HAL_UART_CLEAR_FLAG(comHandle, UART_FLAG_NE);
break;
case HAL_UART_ERROR_FE:
__HAL_UART_CLEAR_FLAG(comHandle, UART_FLAG_FE);
break;
case HAL_UART_ERROR_ORE:
__HAL_UART_CLEAR_FLAG(comHandle, UART_FLAG_ORE);
break;
}
if (comHandle == NULL) {
common_error_handle(__FILE__, __LINE__);
}
#if (USE_COM1_ENABLE == 1)
else if (comHandle->Instance == USART1) {
Com1_ErrEvent();
}
#endif
#if (USE_COM2_ENABLE == 1)
else if (comHandle->Instance == USART2) {
Com2_ErrEvent();
}
#endif
#if (USE_COM3_ENABLE == 1)
else if (comHandle->Instance == USART3) {
Com3_ErrEvent();
}
#endif
#if (USE_COM4_ENABLE == 1)
else if (comHandle->Instance == UART4) {
Com4_ErrEvent();
}
#endif
}
/**
* @brief 统一DMA接口初始化
* @param dmaHandle
* @param config
*/
void Com_Dma_Init(DMA_HandleTypeDef *dmaHandle, DMA_InitTypeDef *config) {
DMA_InitTypeDef *ptr;
if (config != NULL) {
ptr = config;
} else {
ptr = &default_dma_config;
}
memcpy(&dmaHandle->Init, ptr, sizeof(DMA_InitTypeDef));
}
串口1
头文件(bsp_serial_com1.h)
#ifndef STM32_VET6_BSP_SERIAL_COM1_H
#define STM32_VET6_BSP_SERIAL_COM1_H
#include "bsp_serial_define.h"
#if USE_COM1_ENABLE
void Com1_DMA_Init(uint32_t PreemptPriority, uint32_t SubPriority, DMA_InitTypeDef *config);
void Com1_Init(UART_InitTypeDef *config);
void Com1_MspInit(void);
void Com1_MspDeInit(void);
void Com1_DMA_Rx_MspInit(void);
void Com1_DMA_Rx_MspDeInit(void);
void Com1_RxEvent(uint16_t pos);
void Com1_ErrEvent(void);
#endif
#endif //STM32_VET6_BSP_SERIAL_COM1_H
基础源文件(bsp_serial_com1.c)
#include "bsp_serial.h"
UART_HandleTypeDef com1_handle = {.Instance=USART1};
/**
* @brief Com1初始化
* @param config
*/
void Com1_Init(UART_InitTypeDef *config) {
Com_Init(&com1_handle, config);
// 使用自定义的中断回调
Com_RegisterCallback(&com1_handle);
}
void Com1_MspInit(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* USART1 clock enable */
__HAL_RCC_USART1_CLK_ENABLE();
COM1_TX_GPIO_CLK_ENABLE();
COM1_RX_GPIO_CLK_ENABLE();
/**USART1 GPIO Configuration
PA9 ------> USART1_TX
PA10 ------> USART1_RX
*/
GPIO_InitStruct.Pin = COM1_TX_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(COM1_TX_PORT, &GPIO_InitStruct);
GPIO_InitStruct.Pin = COM1_RX_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(COM1_RX_PORT, &GPIO_InitStruct);
#if USE_COM1_DMA_RX_ENABLE
Com1_DMA_Rx_MspInit();
#endif
}
void Com1_MspDeInit(void) {
/* Peripheral clock disable */
__HAL_RCC_USART1_CLK_DISABLE();
HAL_GPIO_DeInit(COM1_TX_PORT, COM1_TX_PIN);
HAL_GPIO_DeInit(COM1_RX_PORT, COM1_RX_PIN);
#if USE_COM1_DMA_RX_ENABLE
Com1_DMA_Rx_MspDeInit();
#endif
}
void USART1_IRQHandler(void) {
HAL_UART_IRQHandler(&com1_handle);
}
扩展串口接收dma(bsp_serial_dma_com1.c)
#include "bsp_serial.h"
DMA_HandleTypeDef com1_dma_rx_handle={
.Instance=COM1_DMA_RX_CHANNEL,
};
void Com1_DMA_Init(uint32_t PreemptPriority, uint32_t SubPriority, DMA_InitTypeDef *config) {
/* DMA controller clock enable */
COM1_DMA_CLK_ENABLE();
/* DMA interrupt init */
HAL_NVIC_SetPriority(COM1_DMA_IRQ, PreemptPriority, SubPriority);
HAL_NVIC_EnableIRQ(COM1_DMA_IRQ);
// com1初始化
Com_Dma_Init(&com1_dma_rx_handle, config);
}
void Com1_DMA_Rx_MspInit(void) {
if (HAL_DMA_Init(&com1_dma_rx_handle) != HAL_OK) {
common_error_handle(__FILE__, __LINE__);
}
__HAL_LINKDMA(&com1_handle, hdmarx, com1_dma_rx_handle);
#if USE_COM1_IRQ_ENABLE
HAL_NVIC_SetPriority(USART1_IRQn, COM1_IRQ_Priority, COM1_IRQ_SubPriority);
HAL_NVIC_EnableIRQ(USART1_IRQn);
#endif
}
void Com1_DMA_Rx_MspDeInit(void) {
/* DMA DeInit */
HAL_DMA_DeInit(&com1_dma_rx_handle);
/* interrupt Deinit */
HAL_NVIC_DisableIRQ(COM1_DMA_IRQ);
}
void COM1_IRQ_HANDLE(void) {
/* USER CODE BEGIN DMA1_Channel5_IRQn 0 */
HAL_DMA_IRQHandler(&com1_dma_rx_handle);
}
/**
* @brief 参考 @ref HAL_UARTEx_RxEventCallback
* @param pos
*/
__weak void Com1_RxEvent(uint16_t pos) {
UNUSED(pos);
}
/**
* @brief 错误事件
*/
__weak void Com1_ErrEvent(void) {
}
示例
开启串口中断和DMA接收中断
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2023 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include <stdbool.h>
#include "main.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"
#include "bsp_serial.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
static uint8_t buffer[256];
// 接收数据标志位
static volatile bool rec_data_flag = false;
// 接收数据长度
static volatile uint16_t rec_len = 0;
/**
* @brief 错误事件
*/
void Com1_ErrEvent(void) {
rec_len = 0;
rec_data_flag = false;
// 重新开启DMA接收
HAL_UARTEx_ReceiveToIdle_DMA(&com1_handle, buffer, 256);
}
void Com1_RxEvent(uint16_t pos) {
rec_len = pos;
rec_data_flag = true;
}
static uint16_t com_rec(void *retBuf) {
if (rec_data_flag) {
rec_data_flag = false;
memcpy(retBuf, buffer, rec_len);
HAL_UARTEx_ReceiveToIdle_DMA(&com1_handle, buffer, 256);
return rec_len;
}
return 0;
}
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void) {
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
#if 0
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
#endif
Com1_DMA_Init(5, 0, NULL);
Com_Init_01(USART1, 9600);
// 开启串口屏接收数据
HAL_UARTEx_ReceiveToIdle_DMA(&com1_handle, buffer, 256);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
uint8_t read_buf[256];
while (1) {
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
uint16_t data_len = com_rec(read_buf);
if (data_len > 0) {
// todo 解析数据 read_buf
}
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) {
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void) {
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1) {
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
项目gitee
https://gitee.com/scl_arm/serial_proj.git
————————————————
版权声明:本文为CSDN博主「詩不诉卿」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44742767/article/details/130444774
|
|