STM32F103C8T6 标准库(SPL)迁移到 HAL 库指南

这是一份详细的 STM32F103C8T6 标准库(SPL)向 HAL 库移植的指南,涵盖 GPIO、USART、I2C、SPI、ADC、TIM 等外设。

这将包括:

  1. 创建 HAL 工程:使用 STM32CubeMX 生成基础工程。
  2. 外设初始化对比:分析 SPL 与 HAL 在 GPIO、USART、I2C、SPI、ADC、TIM 等模块上的差异,并提供等效的 HAL API 代码。
  3. 中断处理适配:将 SPL 的 NVIC 方式调整为 HAL 的中断回调机制。
  4. 寄存器操作调整:适配 SPL 直接寄存器访问代码到 HAL 结构体 API。
  5. 测试与调试:推荐的调试方法及常见错误排查。

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 的初始化对比说明:

下面是 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_SetPriorityHAL_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_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);
    
    可以看到配置项基本对应,只是 HAL 使用 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_InitTypeDefSPI_Init(),HAL 用 SPI_HandleTypeDefHAL_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_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);
      
      区别主要在宏名称上,HAL 的枚举更直观(如 SPI_MODE_MASTER 等),迁移时对应替换即可。
  • 数据传输: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 并启动校准/转换。典型步骤:

    1. RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE) 并调用 RCC_ADCCLKConfig(RCC_PCLK2_Div6) 设置ADC时钟分频(F1系列特有)。
    2. 配置 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);
      
    3. 配置通道:ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); 设置序列1的通道0及采样时间。
    4. 校准并启动: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_HandleTypeDefADC_ChannelConfTypeDef

    1. 使能时钟:__HAL_RCC_ADC1_CLK_ENABLE()
    2. 配置句柄并初始化:
      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预分频自动生成)。

    3. 配置通道:使用 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);
      
    4. 校准与启动:对于支持校准的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_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 初始化后,通过 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 中,对应流程是:

    1. HAL_TIM_PWM_Init() 初始化TIM基本参数。
    2. HAL_TIM_PWM_ConfigChannel() 配置通道(TIM_OC_InitTypeDef)参数如脉宽值 Pulse
    3. HAL_TIM_PWM_Start()HAL_TIM_PWM_Start_IT() 启动输出,后者带中断(CCR更新触发)。 迁移时,将 SPL 的 OCInit配置转换为 HAL 的 OC配置即可。

总之,TIM 外设迁移时要注意启动/停止函数的变化以及中断回调机制的不同。在HAL中,大多数情况下利用 HAL_TIM_Base_Start_ITHAL_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_ITConfigADCHAL_ADC_Start_ITDMAHAL_DMA_Start_IT 等。

  • ISR 与回调:HAL 库引入了中断回调机制,即在中断发生时,不直接在ISR中处理具体事务,而是调用HAL库的IRQHandler函数,由该函数内部清标志并调用用户提供的回调函数。这样做的好处是将中断处理标准化,用户只需关注回调实现而无需手动清理标志。迁移要点如下:

    1. 调用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 无法得知,不会调用回调函数。
    2. 实现回调函数:HAL 库在驱动中定义了若干弱引用的回调函数,如 HAL_UART_TxCpltCallback/RxCpltCallbackHAL_TIM_PeriodElapsedCallbackHAL_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 中处理新数据等。
    3. 启动中断模式: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_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 库会自动逐字节接收UART数据直到达到指定长度,然后调用回调函数通知用户处理。用户无需手动清除中断标志,HAL_UART_IRQHandler 内部已处理。

迁移中断时,务必逐一检查每个中断源:是否已用 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_GetFlagStatusXXX_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 会处理)。
  • 直接寄存器配置:一些在 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库带来的开发便利。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值