这是一份详细的 STM32F103C8T6 标准库(SPL)向 HAL 库移植的指南,涵盖 GPIO、USART、I2C、SPI、ADC、TIM 等外设。
这将包括:
- 创建 HAL 工程:使用 STM32CubeMX 生成基础工程。
- 外设初始化对比:分析 SPL 与 HAL 在 GPIO、USART、I2C、SPI、ADC、TIM 等模块上的差异,并提供等效的 HAL API 代码。
- 中断处理适配:将 SPL 的 NVIC 方式调整为 HAL 的中断回调机制。
- 寄存器操作调整:适配 SPL 直接寄存器访问代码到 HAL 结构体 API。
- 测试与调试:推荐的调试方法及常见错误排查。
STM32F103C8T6 的标准外设库(SPL)与 STM32Cube HAL 库在使用方式上存在较大差异。本文将详细介绍如何将基于 SPL 的工程迁移到 HAL 库,包括 HAL 工程创建、各外设迁移方法、中断处理适配、寄存器操作调整,以及测试与调试技巧。
1. HAL 工程创建
迁移的第一步是建立一个 HAL 库工程。使用 STM32CubeMX 是最简便的方法:
- 选择芯片并初始化项目:打开 STM32CubeMX,使用 MCU Selector 选择 STM32F103C8T6,然后创建新项目 (〖STM32CubeMX〗配置STM32F103C8T6_stm32f103c8t6 cubemx-优快云博客)。CubeMX 会显示芯片引脚图,您可以在图形界面上配置引脚功能。
- 配置时钟和基础设置:在 Clock Configuration 中设置系统时钟源和频率(例如使用外部8MHz晶振并启用PLL将系统时钟配置为72MHz)。在 Configuration 界面下选择 RCC,配置 High Speed Clock 为 HSE 等,根据需求启用调试接口(Debug)等系统设置 (〖STM32CubeMX〗配置STM32F103C8T6_stm32f103c8t6 cubemx-优快云博客)。
- 启用所需外设:在 Pinout & Configuration 中启用 GPIO、USART、I2C、SPI、ADC、TIM 等外设,并为它们分配引脚和参数。CubeMX 提供外设的模式配置选项(如将 USART1 设置为异步模式,波特率115200;开启 ADC1 等),以及 NVIC中断优先级配置选项 (〖STM32CubeMX〗配置STM32F103C8T6_stm32f103c8t6 cubemx-优快云博客)。
- 生成工程代码:在 Project Manager 中选择开发环境(如 Keil MDK5、STM32CubeIDE 等)并命名工程,然后点击 “GENERATE CODE” 生成工程源码。CubeMX 将自动生成包含 HAL 库驱动的项目框架,包括
main.c
、外设初始化函数(如MX_GPIO_Init()
等)和 HAL 库所需的启动文件。
生成的工程中,main.c
通常包含如下初始化代码框架:
HAL_Init(); // 初始化HAL库
SystemClock_Config(); // 配置系统时钟(CubeMX生成)
MX_GPIO_Init(); // 初始化GPIO(CubeMX生成)
MX_USART1_UART_Init(); // 初始化USART1等(根据启用的外设)
...
其中 HAL_Init()
会初始化 HAL 库、设置 SysTick 定时器(1ms 中断用于 HAL_Delay)和中断优先级分组;SystemClock_Config()
配置时钟树;MX_<Periph>_Init()
则是 CubeMX 为每个外设生成的初始化函数。您可以在这些函数中看到 HAL 库的调用实例,为后续迁移提供参考。
注意:如果不使用 CubeMX,也可以手动创建 HAL 工程。在这种情况下,需要包含 HAL 库的头文件和源文件,调用 HAL_Init()
和系统时钟配置函数,并确保启用了 USE_HAL_DRIVER
宏以及正确的启动文件和中断向量表。
2. 各外设的迁移方法
下面分别介绍 GPIO、USART、I2C、SPI、ADC、TIM 等常用外设从 SPL 迁移到 HAL 的方法。我们将通过对比分析初始化、数据读写和模式配置的差异,并提供SPL 与 HAL 的代码示例,便于理解迁移后的等效实现。
2.1 GPIO
初始化对比:在 SPL 中,使用 GPIO_InitTypeDef
配置GPIO引脚,典型流程包括打开时钟、设置引脚模式和速率,然后调用 GPIO_Init()
。在 HAL 中,亦使用 GPIO_InitTypeDef
但结构字段名称有所不同,同样需要先使能时钟,然后调用 HAL_GPIO_Init()
。主要区别包括:HAL 库使用 Pin
字段替代 SPL 的 GPIO_Pin
,使用 Mode
/Pull
字段替代 SPL 的 GPIO_Mode
(输入上拉下拉在 HAL 中通过 Pull 配置),以及使用统一的速度枚举(如 GPIO_SPEED_FREQ_HIGH
)。例如,将一个GPIO引脚配置为推挽输出:
- SPL 实现: 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE)
,配置GPIO_InitTypeDef
后调用GPIO_Init(GPIOA, &GPIO_InitStruct)
。 - HAL 实现: 调用宏
__HAL_RCC_GPIOA_CLK_ENABLE()
开启时钟,配置GPIO_InitStruct
后调用HAL_GPIO_Init(GPIOA, &GPIO_InitStruct)
。
引脚读写:SPL 提供 GPIO_SetBits()/GPIO_ResetBits()
设置输出,GPIO_ReadInputDataBit()
读取输入。在 HAL 中,使用 HAL_GPIO_WritePin(GPIOx, Pin, GPIO_PIN_SET/RESET)
设置引脚电平,使用 HAL_GPIO_TogglePin()
切换电平,使用 HAL_GPIO_ReadPin()
读取引脚状态。例如,将 PA5 设置为高电平输出:
// **SPL 示例**:将 PA5 配置为推挽输出并拉高
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启GPIOA时钟
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_SetBits(GPIOA, GPIO_Pin_5); // 将PA5置1 (输出高电平)
// **HAL 示例**:将 PA5 配置为推挽输出并拉高
__HAL_RCC_GPIOA_CLK_ENABLE(); // 开启GPIOA时钟
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // 将PA5置1 (输出高电平)
如上代码所示,SPL 的 GPIO_SetBits(GPIOA, GPIO_Pin_8)
在 HAL 中对应为 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET)
(GPIO_SetBits 函数报错 )。迁移时应将所有直接使用 SPL GPIO 函数的地方替换为 HAL GPIO 接口。类似地,输入电平读取由 GPIO_ReadInputDataBit
替换为 HAL_GPIO_ReadPin
。
模式配置:HAL 库将上拉/下拉作为单独的配置项(Pull),例如将输入模式分为无上下拉(GPIO_NOPULL
)、上拉(GPIO_PULLUP
)、下拉(GPIO_PULLDOWN
),而 SPL 则通过不同的GPIO_Mode枚举来表示输入浮空、上拉等。迁移时需要注意对应关系:
GPIO_Mode_IN_FLOATING
(浮空输入) → HAL: Mode 设为GPIO_MODE_INPUT
,Pull 设为GPIO_NOPULL
。GPIO_Mode_IPU
(上拉输入) → HAL:GPIO_MODE_INPUT
+GPIO_PULLUP
。GPIO_Mode_Out_PP
(推挽输出) → HAL:GPIO_MODE_OUTPUT_PP
,Pull 通常NOPULL
。
2.2 USART (串口)
USART/UART 在 HAL 中使用 USART(或 UART)_HandleTypeDef 结构体管理,初始化流程与 SPL 有明显不同。下面通过 USART1 的初始化对比说明:
- SPL 初始化流程:启用时钟 -> 配置 GPIO 引脚模式 -> 配置 USART_InitTypeDef (波特率、数据位、停止位等) -> 调用
USART_Init()
-> 使能串口和中断。 (STM32 stdperiph vs HAL library example) (STM32 stdperiph vs HAL library example) - HAL 初始化流程:启用时钟 -> 配置 GPIO 引脚(包括复用功能 AF)-> 配置 UART_HandleTypeDef 的 Init 参数 -> 调用
HAL_UART_Init()
-> 使能中断。 (STM32 stdperiph vs HAL library example)
下面是 USART1 配置为115200 8-N-1并打开接收中断的代码对比:
// **SPL USART1 初始化示例**
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1 | RCC_APB2Periph_AFIO, ENABLE);
// 配置PA9为USART1_TX (复用推挽输出),PA10为USART1_RX (浮空输入)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置USART1参数: 波特率115200, 8位数据,1位停止,无奇偶校验
USART_InitTypeDef USART_InitStructure;
USART_StructInit(&USART_InitStructure); // 使用默认值初始化结构体(可选)
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE); // 使能USART1
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 使能接收中断
// 配置USART1中断优先级并使能NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_ClearPendingIRQ(USART1_IRQn);
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// **HAL USART1 初始化示例**
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_USART1_CLK_ENABLE();
// 配置PA9为USART1_TX,PA10为USART1_RX,并设置复用功能(AF7对应USART1)
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置USART1参数: 波特率115200, 8位数据,1位停止,无校验,无硬件流控
UART_HandleTypeDef huart1;
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&huart1); // 初始化USART1
// 使能接收中断并配置NVIC
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE); // 使能USART1接收中断请求
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
两段代码实现了等价的功能:SPL 版本显式配置了GPIO和USART寄存器并调用中断初始化函数,而 HAL 版本通过句柄和 HAL 库调用完成初始化。需要注意以下迁移要点:
- GPIO 复用配置:在 HAL 中需要为 USART 引脚指定 Alternate 功能,例如
GPIO_InitStruct.Alternate = GPIO_AF7_USART1
(对于F1系列,UART1的AF编号也按照AF7处理)。在 SPL 中,对应的操作是使能 AFIO 时钟并(若使用重映射引脚)调用GPIO_PinRemapConfig
。例如若将USART1重映射到PB6/PB7,则在 SPL 中需调用GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE)
,而在 HAL 中对应地需要调用__HAL_AFIO_REMAP_USART1_ENABLE()
宏启用重映射(仅F1系列需要考虑 AFIO 重映射)。 - USART 初始化结构差异:SPL 使用
USART_InitTypeDef
,HAL 使用UART_InitTypeDef
(其定义包含在 UART_HandleTypeDef 的 Init 成员中)。字段名称略有不同,但含义相同,如 WordLength、BaudRate 等直接对应。迁移时将 SPL 结构体赋值转换为 HAL 结构体赋值即可。 - 中断配置:SPL 用
USART_ITConfig(USARTx, USART_IT_RXNE, ENABLE)
来使能中断,HAL 则通常在初始化后调用宏__HAL_UART_ENABLE_IT(&huart, UART_IT_RXNE)
来使能中断请求标志位。此外,NVIC 配置在 HAL 中使用HAL_NVIC_SetPriority
和HAL_NVIC_EnableIRQ
实现,取代了 SPL 的 NVIC_InitTypeDef (STM32 stdperiph vs HAL library example)。CubeMX 生成的代码通常会在MX_USART1_UART_Init()
中通过HAL_NVIC_SetPriority/EnableIRQ
自动配置好中断。 - 数据收发:SPL 常用
USART_SendData
/USART_ReceiveData
或直接查询标志位USART_GetFlagStatus
来发送接收数据。HAL 提供更高级的函数,如HAL_UART_Transmit(&huart1, buf, len, timeout)
和HAL_UART_Receive(&huart1, buf, len, timeout)
实现轮询收发;以及HAL_UART_Transmit_IT
/HAL_UART_Receive_IT
实现中断收发(内部会开启中断并在中断服务中调用回调)。迁移时,若原代码使用查询或中断方式收发,需要选择 HAL 提供的相应模式函数。例如,将循环调用USART_SendData
改为一次性的HAL_UART_Transmit
,或者将原先在中断中读取USART_ReceiveData
的方式改为使用 HAL 的接收回调(见下文中断处理部分)。
2.3 I2C
初始化对比:SPL 下使用 I2C_InitTypeDef
配置 I2C 主从模式、时钟速度等,然后调用 I2C_Init()
。HAL 下使用 I2C_HandleTypeDef
,需设置 hi2c.Init.ClockSpeed
(如100kHz)、地址模式(7位或10位)、自身地址等,然后调用 HAL_I2C_Init()
。例如,将 I2C1 配置为主机,100kHz时钟,7位地址模式:
- SPL:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); I2C_InitTypeDef I2C_InitStruct; I2C_InitStruct.I2C_ClockSpeed = 100000; I2C_InitStruct.I2C_Mode = I2C_Mode_I2C; I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2; I2C_InitStruct.I2C_OwnAddress1 = 0x00; I2C_InitStruct.I2C_Ack = I2C_Ack_Enable; I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_Init(I2C1, &I2C_InitStruct); I2C_Cmd(I2C1, ENABLE);
- HAL:
可以看到配置项基本对应,只是 HAL 使用__HAL_RCC_I2C1_CLK_ENABLE(); hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 100000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0x00; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 = 0; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; HAL_I2C_Init(&hi2c1);
AddressingMode
来区分7/10位地址等。使能I2C在 HAL 初始化中已完成,因此无需额外的I2C_Cmd
调用。
数据传输:这是 I2C 迁移中差异最大的部分。SPL 通常需要逐步控制 I2C 状态机:发送START信号、发送地址、发送数据、等待事件完成、发送STOP信号。例如,通过查询事件标志寄存器(I2C_CheckEvent)或标志位(I2C_GetFlagStatus)实现。这种代码往往较长且易错。而 HAL 提供了一系列封装好的 API 实现完整的数据收发流程:
- 发送数据:使用
HAL_I2C_Master_Transmit()
实现主机发送;HAL_I2C_Slave_Transmit()
实现从机发送。调用时提供总线地址、数据缓冲区和长度,HAL 会完成从起始信号到停止信号的整个过程。 - 接收数据:类似地,使用
HAL_I2C_Master_Receive()
或HAL_I2C_Slave_Receive()
。 - 中断/DMA模式:HAL 也提供了
..._IT
和..._DMA
后缀的非阻塞接口,在配置好NVIC或DMA后可使用这些函数实现异步收发。
实例:假设主机向从机地址0x50发送一个字节数据 0xAB
:
-
SPL 实现 (轮询):
I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); // 等待Master模式选定 I2C_Send7bitAddress(I2C1, 0x50, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); // 等待地址发送并被ACK I2C_SendData(I2C1, 0xAB); while(!I2C_CheckEvent(I2C1, I2C_EVENT_TXE)); // 等待数据寄存器空(数据已发送) I2C_GenerateSTOP(I2C1, ENABLE);
SPL 代码需要逐个步骤检查事件确保总线状态正确。
-
HAL 实现 (阻塞):
uint8_t data = 0xAB; HAL_I2C_Master_Transmit(&hi2c1, 0x50<<1, &data, 1, HAL_MAX_DELAY);
HAL 的实现仅需一行函数调用,其中地址参数
0x50<<1
左移一位以包含读/写位(CubeMX生成的代码会定义#define I2C_ADDRESS 0x50<<1
这样的宏)。该函数内部完成了上述所有步骤,并在指定的超时时间内阻塞等待传输完成。
迁移I2C代码时,应充分利用 HAL 的高级API简化实现,将原来繁琐的状态机流程替换为 HAL 提供的接口。此外,需要注意 HAL 默认会处理ACK/NACK,如果通信错误会返回 HAL_ERROR
状态,可通过函数返回值进行判断和错误处理。
2.4 SPI
SPI 在 SPL 和 HAL 中的配置思路类似,都需要设置通信模式(主/从)、时钟极性相位(CPOL/CPHA)、数据帧长度等。主要区别在于函数命名和数据传输方法:
-
初始化:SPL 用
SPI_InitTypeDef
和SPI_Init()
,HAL 用SPI_HandleTypeDef
和HAL_SPI_Init()
。例如,将 SPI1 配置为主模式、8位数据、模式0、波特率预分频为FPCLK/16:- SPL:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); SPI_InitTypeDef SPI_InitStruct; SPI_InitStruct.SPI_Mode = SPI_Mode_Master; SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b; SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low; SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge; SPI_InitStruct.SPI_NSS = SPI_NSS_Soft; SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16; SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStruct); SPI_Cmd(SPI1, ENABLE);
- HAL:
区别主要在宏名称上,HAL 的枚举更直观(如 SPI_MODE_MASTER 等),迁移时对应替换即可。__HAL_RCC_SPI1_CLK_ENABLE(); hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi1);
- SPL:
-
数据传输:SPL 常用
SPI_I2S_SendData()
和SPI_I2S_ReceiveData()
,通常需要等待状态标志。如发送一个字节:SPI_I2S_SendData(SPI1, byte); while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); // 等待发送完成
接收则等待 RXNE 标志。HAL 提供的接口有:
- 阻塞:
HAL_SPI_Transmit()
,HAL_SPI_Receive()
,或HAL_SPI_TransmitReceive()
(全双工收发)。 - 中断:
HAL_SPI_Transmit_IT()
等,需配合回调。 - DMA:
HAL_SPI_Transmit_DMA()
等。
例如发送并接收1字节数据(全双工交换):
uint8_t tx=0x5A, rx=0; HAL_SPI_TransmitReceive(&hspi1, &tx, &rx, 1, HAL_MAX_DELAY);
该调用会发送
tx
数据并同时读取收到的字节到rx
。 - 阻塞:
迁移注意:SPI 的 HAL 初始化会自动处理一些配置,比如配置寄存器 CR1/CR2 并使能 SPI。如果您在 SPL 中手动操作了 NSS 管脚(软件管理模式),在 HAL 中确保 NSS = SPI_NSS_SOFT
并自行通过GPIO控制片选。数据传输部分,如果以前使用中断需转换为 HAL 的中断模式,并在回调函数中处理传输完成逻辑。通常,HAL_SPI 的中断回调为 HAL_SPI_TxCpltCallback
/RxCpltCallback
等,可用来指示传输结束。
2.5 ADC
ADC 部分迁移需注意初始化配置以及触发/读取方式的变化。STM32F103 的 ADC 为12位逐次逼近式,SPL 和 HAL 都提供相关接口,但 HAL 结构稍有不同:
-
初始化:在 SPL 中,需配置 ADC 时钟预分频、模式、扫描转换等,然后逐个配置通道顺序和采样时间,最后使能 ADC 并启动校准/转换。典型步骤:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE)
并调用RCC_ADCCLKConfig(RCC_PCLK2_Div6)
设置ADC时钟分频(F1系列特有)。- 配置
ADC_InitTypeDef
:ADC_InitTypeDef ADC_InitStruct; ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; ADC_InitStruct.ADC_ScanConvMode = ENABLE; // 扫描模式(多通道) ADC_InitStruct.ADC_ContinuousConvMode = DISABLE; // 连续转换模式关闭(单次转换) ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 软件触发 ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStruct.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStruct);
- 配置通道:
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
设置序列1的通道0及采样时间。 - 校准并启动:F1需要执行校准:
ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE);
HAL 中,使用
ADC_HandleTypeDef
和ADC_ChannelConfTypeDef
:- 使能时钟:
__HAL_RCC_ADC1_CLK_ENABLE()
。 - 配置句柄并初始化:
hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE; // 扫描模式 hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; HAL_ADC_Init(&hadc1);
提示: HAL 的 ADC初始化结构中没有显式的时钟配置项,对应的 ADC时钟预分频在 HAL 内部通过
RCC_ADCPRE
设置(CubeMX 根据您在时钟树中配置的ADC预分频自动生成)。 - 配置通道:使用
ADC_ChannelConfTypeDef
设置通道编号和采样时间,然后调用HAL_ADC_ConfigChannel()
,例如:ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig);
- 校准与启动:对于支持校准的ADC(如F1系列),HAL 提供
HAL_ADCEx_Calibration_Start()
函数,可在HAL_ADC_Init()
之后调用来校准ADC。 ([PDF] 6 HAL ADC Generic Driver)然后使用HAL_ADC_Start(&hadc1)
开始转换。
-
数据读取:SPL 中读取ADC值通常如下:
ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 软件触发开始转换 while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // 等待转换完成 uint16_t val = ADC_GetConversionValue(ADC1); // 读取结果
HAL 等效的轮询方式是:
HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY); // 轮询等待转换完成 uint16_t val = HAL_ADC_GetValue(&hadc1);
如果使用中断模式,HAL 提供
HAL_ADC_Start_IT()
,并在转换完成时调用回调HAL_ADC_ConvCpltCallback()
。DMA 模式下,则使用HAL_ADC_Start_DMA()
。
迁移注意:多数情况下,将 SPL 中对 ADC 的配置和读写按上述步骤替换即可。需注意 HAL 的通道配置必须在每次改变通道时调用(对于多通道扫描,CubeMX 会自动配置 Rank 顺序)。如果原 SPL 代码使用了注入通道、模拟看门狗等高级特性,HAL 也有对应的 API(例如 HAL 的 injected group 配置函数),可查阅 STM32F1 HAL 驱动手册进一步迁移。一般简单的单通道或多通道采集,通过 HAL_ADC_Init + HAL_ADC_ConfigChannel + HAL_ADC_Start/PollForConversion 即可实现。
2.6 TIM 定时器
STM32F1 定时器包括通用定时器TIM2-TIM5等,SPL 与 HAL 在定时器初始化上的思路类似,但 HAL 抽象出 TIM_HandleTypeDef 并提供了启动/停止API。
-
初始化:SPL 使用
TIM_TimeBaseInitTypeDef
配置基本参数(预分频、计数周期等)并调用TIM_TimeBaseInit()
,然后根据用途配置输出比较、输入捕获等,以及使能中断或DMA。HAL 将这些配置汇总在 TIM_HandleTypeDef 的 Init 中,并根据定时器用途调用不同初始化函数:- 基本定时器/通用定时器做周期中断:使用
HAL_TIM_Base_Init()
。 - 输出比较/PWM:使用
HAL_TIM_PWM_Init()
并结合HAL_TIM_PWM_ConfigChannel()
配置通道。 - 输入捕获:使用
HAL_TIM_IC_Init()
等等。
例如,将 TIM3 配置为每隔100ms产生中断(假设72MHz时钟,下设预分频=7200-1,周期=1000-1,可得到 0.1s):
- SPL:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct; TIM_TimeBaseStruct.TIM_Prescaler = 7200 - 1; // 7200分频 -> 10kHz TIM_TimeBaseStruct.TIM_Period = 1000 - 1; // 1000个计数 -> 0.1s TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStruct); TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); // 使能更新中断 TIM_Cmd(TIM3, ENABLE); // 开始计数
- HAL:
HAL 初始化后,通过__HAL_RCC_TIM3_CLK_ENABLE(); TIM_HandleTypeDef htim3; htim3.Instance = TIM3; htim3.Init.Prescaler = 7200 - 1; htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 1000 - 1; htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(&htim3); HAL_NVIC_SetPriority(TIM3_IRQn, 1, 0); HAL_NVIC_EnableIRQ(TIM3_IRQn); HAL_TIM_Base_Start_IT(&htim3); // 开始计数并启用中断
HAL_TIM_Base_Start_IT()
一步替代了 SPL 中的 TIM_Cmd 和 TIM_ITConfig,两者效果等同:启动定时器并打开更新中断。
- 基本定时器/通用定时器做周期中断:使用
-
中断与回调:SPL 中定时器更新中断在
TIMx_IRQHandler
中处理,例如:void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // 用户代码: 定时器触发事件处理 } }
在 HAL 中,库提供
HAL_TIM_IRQHandler()
函数处理中断标志,并通过回调通知用户:void TIM3_IRQHandler(void) { HAL_TIM_IRQHandler(&htim3); }
然后用户需实现回调函数:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim->Instance == TIM3) { // 用户代码: 定时器触发事件处理 } }
HAL 在检测到更新中断后会调用
HAL_TIM_PeriodElapsedCallback
(对于输出比较中断则调用HAL_TIM_OC_DelayElapsedCallback
)。因此迁移时,需要将原先在中断处理函数内直接执行的用户逻辑移动到 HAL 的回调函数中,并确保在对应的 IRQHandler 中调用了 HAL 的 IRQ处理函数。 -
PWM 输出:如果TIM用于PWM,在 SPL 中需使用
TIM_OCInitTypeDef
配置输出比较通道、占空比等,然后TIM_OCxInit()
,并TIM_Cmd
开始。在 HAL 中,对应流程是:HAL_TIM_PWM_Init()
初始化TIM基本参数。HAL_TIM_PWM_ConfigChannel()
配置通道(TIM_OC_InitTypeDef
)参数如脉宽值Pulse
。HAL_TIM_PWM_Start()
或HAL_TIM_PWM_Start_IT()
启动输出,后者带中断(CCR更新触发)。 迁移时,将 SPL 的 OCInit配置转换为 HAL 的 OC配置即可。
总之,TIM 外设迁移时要注意启动/停止函数的变化以及中断回调机制的不同。在HAL中,大多数情况下利用 HAL_TIM_Base_Start_IT
、HAL_TIM_PWM_Start
等即可启动相应模式且包含中断配置,而 SPL 则需要分别调用启用中断和启动计数。下一节将更详细讨论 HAL 的中断处理机制。
3. 中断处理适配
在 SPL 中,设置和处理中断通常包括以下步骤:配置 NVIC 优先级 (NVIC_InitTypeDef
)、使能外设中断(如 XXX_ITConfig
)、实现中断服务函数(ISR) 来读取清标志位并执行用户代码。
在 HAL 框架下,中断处理有所不同,主要体现在NVIC配置接口和中断回调机制上:
-
NVIC 配置:HAL 不再提供类似 NVIC_InitTypeDef 的结构,而是通过函数直接配置。常用的是:
HAL_NVIC_SetPriority(IRQn, preemptPriority, subPriority)
设置中断优先级。HAL_NVIC_EnableIRQ(IRQn)
使能中断线。
例如,将 USART1 中断优先级设为0并使能:
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0); HAL_NVIC_EnableIRQ(USART1_IRQn);
这相当于 SPL 中设置 NVIC_IRQChannel 为 USART1_IRQn,抢占优先级和子优先级为0并使能的配置 (STM32 stdperiph vs HAL library example)。迁移时,可直接用 HAL 提供的函数替换原 NVIC_Init 调用。注意:HAL_Init() 默认将系统中断优先级分组设置为4(4位抢占优先级,无子优先级),这与STM32F1默认相符,一般无需修改。如果需要不同分组,可用
HAL_NVIC_SetPriorityGrouping()
进行调整。 -
中断使能与标志:在 SPL 中,通常调用例如
USART_ITConfig(..., ENABLE)
或EXTI_Init()
等来使能外设的中断源。HAL 中,大多数情况下初始化函数已经帮我们配置好了中断源,但也有需要手动设置的。例如 UART,需要调用__HAL_UART_ENABLE_IT(&huart, UART_IT_RXNE)
来打开接收中断 (STM32 stdperiph vs HAL library example);EXTI 外部中断则在 GPIO 初始化时通过HAL_GPIO_Init
的配置已经注册中断线,但仍需用HAL_NVIC_EnableIRQ(EXTIx_IRQn)
使能。在迁移过程中,要对应检查每个中断源的使能:定时器可使用HAL_TIM_Base_Start_IT
代替TIM_ITConfig
;ADC用HAL_ADC_Start_IT
;DMA有HAL_DMA_Start_IT
等。 -
ISR 与回调:HAL 库引入了中断回调机制,即在中断发生时,不直接在ISR中处理具体事务,而是调用HAL库的IRQHandler函数,由该函数内部清标志并调用用户提供的回调函数。这样做的好处是将中断处理标准化,用户只需关注回调实现而无需手动清理标志。迁移要点如下:
-
调用HAL IRQHandler:对于每个启用中断的外设,都有对应的
HAL_xxx_IRQHandler()
需要在实际ISR中被调用。例如:- USART:在
USART1_IRQHandler
中调用HAL_UART_IRQHandler(&huart1)
。 - 定时器:在
TIM3_IRQHandler
中调用HAL_TIM_IRQHandler(&htim3)
。 - ADC:在
ADC1_2_IRQHandler
中调用HAL_ADC_IRQHandler(&hadc1)
。 - GPIO外部中断:在
EXTI15_10_IRQHandler
等中调用HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_X)
对应引脚。 CubeMX 会自动生成这些 ISR 框架代码。在手工移植时,需要在启动文件的中断函数里加入上述调用。切记:如果缺少这一步,中断发生时 HAL 无法得知,不会调用回调函数。
- USART:在
-
实现回调函数:HAL 库在驱动中定义了若干弱引用的回调函数,如
HAL_UART_TxCpltCallback
/RxCpltCallback
,HAL_TIM_PeriodElapsedCallback
,HAL_ADC_ConvCpltCallback
等。这些函数默认为空实现,用户可在应用代码中自行实现(函数名需完全一致)。当对应中断发生且处理完毕后,HAL 内部就会调用相应的回调。例如 UART 接收完成时调用HAL_UART_RxCpltCallback
。迁移时,将原先ISR中执行的用户逻辑移至这些回调中:- 串口接收:原 SPL 中断函数里读取
USART_ReceiveData
的代码,可以放入HAL_UART_RxCpltCallback
中处理接收到的数据(HAL 已经将数据存入用户提供的缓冲区)。 - 定时器更新:原来
TIMx_IRQHandler
中用户代码,迁移为在HAL_TIM_PeriodElapsedCallback
判断htim->Instance
后执行。 - ADC转换完成:在
HAL_ADC_ConvCpltCallback
中处理新数据等。
- 串口接收:原 SPL 中断函数里读取
-
启动中断模式:HAL 的中断回调通常在使用
HAL_xxx_Start_IT()
或HAL_xxx_Receive_IT()
之后才会被触发。比如要使用 UART 接收中断,需要先调用HAL_UART_Receive_IT(&huart1, buf, len)
提供接收缓冲区和长度;使用 ADC EOC中断,需要调用HAL_ADC_Start_IT(&hadc1)
;使用定时器更新中断,则通过HAL_TIM_Base_Start_IT()
。确保这些函数已被调用,否则即使中断配置好了,回调也不会执行(因为 HAL 未进入中断工作状态)。
-
示例:以下展示将一个简单的串口接收中断处理从 SPL 迁移到 HAL 的对比:
- SPL 中断服务:
// 假设接收缓冲区 rx_buf void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { uint8_t data = USART_ReceiveData(USART1); rx_buf[index++] = data; // 简单存缓冲 USART_ClearITPendingBit(USART1, USART_IT_RXNE); } }
- HAL 回调处理:
其中 HAL 库会自动逐字节接收UART数据直到达到指定长度,然后调用回调函数通知用户处理。用户无需手动清除中断标志,HAL_UART_IRQHandler 内部已处理。// 在适当地方启动接收(比如初始化后) HAL_UART_Receive_IT(&huart1, rx_buf, RX_BUF_SIZE); ... void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { // 此时 rx_buf 已经填满RX_BUF_SIZE字节或者达到指定条件 // 在这里处理接收完成事件,如通知主循环或处理数据 // 若需连续接收,再次调用 HAL_UART_Receive_IT 以继续下一轮接收 } }
迁移中断时,务必逐一检查每个中断源:是否已用 HAL 函数正确配置 NVIC,是否调用了 HAL_IRQHandler,以及是否实现了对应回调并启动了中断模式。确保这些环节完善后,中断功能才能在 HAL 下正常工作。
4. 寄存器操作调整
在标准库SPL开发中,开发者有时直接访问硬件寄存器(通过CMSIS定义的寄存器结构体)或使用 SPL 提供的寄存器级操作函数。而在 HAL 库中,建议主要使用 HAL 提供的API和句柄来操作外设。如果直接修改寄存器,可能绕过HAL状态管理,需谨慎。迁移过程中应做如下调整:
- 时钟使能:SPL 中通过
RCC_APB2PeriphClockCmd
等函数或直接设置 RCC->APB2ENR 位来开启外设时钟。HAL 提供了一系列宏,例如__HAL_RCC_GPIOA_CLK_ENABLE()
、__HAL_RCC_USART1_CLK_ENABLE()
来完成相同操作 (STM32 stdperiph vs HAL library example)。迁移时,将 RCC 时钟相关的函数替换为 HAL 宏即可。这些宏本质上也是设置寄存器位,但命名更直观统一。 - GPIO 寄存器:如果原代码通过直接操作 GPIOx->ODR/IDR 等寄存器控制引脚,则应改用 HAL 函数。例如,将直接写 ODR 的操作替换为
HAL_GPIO_WritePin
,直接读 IDR 的操作替换为HAL_GPIO_ReadPin
。如前文所述,GPIO_SetBits/ResetBits
在 HAL 中已由HAL_GPIO_WritePin
取代 (GPIO_SetBits 函数报错 )。使用 HAL API 可以确保代码可移植性,并兼顾 HAL 对引脚状态的抽象管理。 - 标志位与事件:SPL 通常提供
XXX_GetFlagStatus
和XXX_ClearFlag
来读写状态寄存器标志。HAL 对应提供了宏或函数。例如:- USART 状态标志
USART_FLAG_TXE
在 HAL 可用宏__HAL_UART_GET_FLAG(&huart, UART_FLAG_TXE)
获取, (STM32 stdperiph vs HAL library example) (STM32 stdperiph vs HAL library example)。 - 清除标志在 HAL 使用
__HAL_UART_CLEAR_FLAG()
等宏(针对某些标志也会在 HAL_IRQHandler 中自动清除)。 - EXTI 线的中断挂起标志清除,HAL 提供
__HAL_GPIO_EXTI_CLEAR_IT(pin)
。 迁移时,可用 HAL 宏操作对应的寄存器标志,或尽可能让 HAL 内部完成(例如大多数中断标志 HAL_IRQHandler 会处理)。
- USART 状态标志
- 直接寄存器配置:一些在 SPL 中没有提供封装的操作,用户可能直接用寄存器宏设置。例如改变 AFIO 寄存器做引脚重映射、调试接口配置等。HAL 提供部分封装,如
__HAL_AFIO_REMAP_SWJ_DISABLE()
可用于关闭JTAG (STM32CUBEMX keeps generating _HAL_AFIO_REMAP_SWJ...)。对于没有直接HAL函数的操作,可以继续使用寄存器宏,但要确保与 HAL 初始化不冲突。例如在 HAL 初始化之后修改某外设控制寄存器,需明确知道不会影响 HAL 对该外设状态的追踪。一般推荐优先使用 HAL 扩展层提供的 LL (Low-Layer) 接口来操作底层寄存器,以保证兼容性。
总而言之,迁移过程中应避免混用 SPL 函数和 HAL 函数,因为它们可能在寄存器配置上相互影响 (GPIO_SetBits 函数报错 )。最佳实践是选择HAL则完全使用HAL,将先前直接寄存器访问替换为等价的 HAL API 或宏。这不仅减少出错概率,也提高代码可维护性。如果确有HAL未覆盖的特殊寄存器操作,可在 HAL 库基础上使用寄存器宏,但要在注释中注明,并在升级HAL库时验证兼容性。
5. 测试与调试
完成移植后,需要对系统功能进行充分的测试与调试,以确保移植后的代码功能正确、性能稳定。以下是一些推荐的方法和常见问题的解决方案:
-
分模块测试:不要一次性修改完所有外设代码再调试,建议逐个外设迁移和测试。例如先移植并测试GPIO输出(点亮LED),确认GPIO正常后,再移植USART通信,通过串口打印调试信息验证USART工作,然后依次是SPI、I2C、ADC、TIM等。逐步验证每个外设能独立运行后,再测试它们之间的交互。
-
使用调试接口:STM32F103C8T6支持SWD调试。可在CubeMX中启用Debug选项(保持PA13/PA14为调试引脚),使用调试器单步运行程序。利用断点和观察变量来确认HAL句柄的状态,例如查看
huart1.State
是否变为HAL_UART_STATE_READY
,或在执行 HAL 调用后检查返回值是否为 HAL_OK。 -
检查 HAL 返回状态:HAL 库的大多数函数会返回
HAL_StatusTypeDef
(HAL_OK, HAL_ERROR 等)。移植后要检查这些返回值。例如HAL_I2C_Master_Transmit
返回 HAL_OK 才表示传输成功;如果返回 HAL_ERROR,可以调用HAL_I2C_GetError()
获取错误码找出失败原因(如仲裁丢失、NACK等)。 -
HAL 库断言调试:如果启用了 HAL 库的参数检查(需要在 stm32f1xx_hal_conf.h 中定义
USE_FULL_ASSERT
),当调用HAL函数参数不正确时会触发assert_failed
。确保移植过程中正确配置了 HAL_InitStruct 的各字段,避免断言错误。例如 HAL_UART_Init 若未正确设置 huart.Instance,可能导致断言失败。利用断言信息可以迅速定位配置遗漏。 -
常见错误及解决:
- 时钟未使能: 移植后某外设不工作,首先检查对应的
__HAL_RCC_XXX_CLK_ENABLE()
是否被调用。HAL 初始化大多不会自动打开时钟,需用户确保。漏掉时钟会导致寄存器写入无效,HAL_Init返回错误。 - 中断未触发: 若某中断回调不执行,可能是NVIC未配置或IRQHandler未调用HAL处理函数。检查
HAL_NVIC_EnableIRQ
是否调用,以及中断向量函数中是否调用了HAL_XXX_IRQHandler
。另外,确保使用了HAL_XXX_Start_IT
函数启动了中断模式。 - 句柄作用域问题: HAL 外设句柄如
UART_HandleTypeDef huart1
通常需要是全局或静态的。若误用局部变量,函数返回后句柄失效会导致后续操作出错。CubeMX生成的句柄都是全局的。在移植时也应仿照这一点。 - 配置不当: 例如 I2C 的地址模式、OWN地址等配置错误,会导致通信失败。ADC 的连续转换、DMA设置不当也可能无法正常获取数据。遇到问题时对比 CubeMX 生成的配置或参考 HAL 库例程调整配置。
- 混用库函数: 切记不要同时调用 SPL 和 HAL 的函数。同一工程应统一使用 HAL 接口,否则可能出现难以预料的错误 (GPIO_SetBits 函数报错 )。如果某些功能HAL没有直接支持,可以考虑使用 HAL 的 LL 底层库(与 HAL 配套且与寄存器一一对应)来替代,而不是引入旧的 SPL 调用。
- 时钟未使能: 移植后某外设不工作,首先检查对应的
-
打印调试信息: 利用已移植的 USART,实现一个简单的调试日志输出(printf重定向或HAL_UART_Transmit),可打印关键变量或状态帮助调试其他外设。例如在I2C读写后打印返回的错误码。
-
逻辑分析/示波器: 在调试通信外设(I2C/SPI/UART)或定时器时,使用示波器或逻辑分析仪观测信号(如波形、时序)是很有帮助的。比如,通过观察I2C总线上的启动信号和数据帧,确认HAL_I2C函数是否按照预期发送了数据。
-
参考示例和文档: ST官方的HAL库例程和应用笔记是重要的参考。 ([PDF] STM32 standard peripheral library to STM32Cube low-layer migration)例如 STM32CubeF1 包含了一些示例工程,可对比其中初始化和回调的用法。遇到问题可以查阅STM32社区论坛或参考他人经验。
完成逐项调试后,建议对照迁移前的SPL工程功能列表,逐一验证HAL工程实现的功能是否全部正常。经过充分测试和优化,移植工作即告完成。
通过本文的指南,我们比较全面地介绍了STM32F103C8T6从标准库(SPL)迁移到HAL库的关键步骤和注意事项。从工程创建、外设初始化到中断和寄存器操作,再到测试调试,按照这些方法进行移植可以最大程度减少问题。迁移过程中保持耐心、仔细对照旧代码逻辑,充分利用HAL提供的便捷接口,相信您可以顺利完成代码移植并享受HAL库带来的开发便利。