STM32 HAL/LL

技术栈
嵌入式 SDK
stm32hallow-levelcortex-m意法半导体CubeMX

概览

STM32 HAL/LL 技术栈概览

STM32 HAL(Hardware Abstraction Layer)与 LL(Low-Layer)是意法半导体为 STM32 全系列 MCU 提供的官方固件库。HAL 抽象级别最高,提供可移植 API;LL 贴近寄存器,性能更优。配合 STM32CubeMX 图形化配置工具,构成完整的 STM32 开发生态。

解决什么问题

  • 外设复杂性:STM32 单颗芯片集成数十种外设(定时器、ADC、DMA、CAN、USB 等),HAL 统一 API 降低认知负荷
  • 代码可移植性:同一 HAL API 可在 F0 到 H7 系列间无缝迁移
  • 快速初始化:CubeMX 图形化配置时钟树、引脚、外设,自动生成初始化代码
  • 双轨性能:HAL 易用性强,LL 提供寄存器级性能(适合中断密集场景)

关键特性

  • HAL 驱动层:完整外设抽象,包含超时、错误处理机制
  • LL 低层驱动:轻量级内联函数,单周期操作,适合 RTOS ISR
  • CubeMX 代码生成:图形化外设/时钟/功耗配置,一键生成 Keil/IAR/GCC 工程
  • CMSIS-RTOS 支持:无缝对接 FreeRTOS、ThreadX 等主流 RTOS
  • 广泛器件覆盖:全系列 1000+ 型号,持续更新支持新芯片

安装

STM32 HAL/LL 开发环境安装指南

1. 环境准备

项目 要求
操作系统 Windows 10/11 / macOS 11+ / Ubuntu 20.04+
IDE STM32CubeIDE(推荐)/ Keil MDK / IAR EWARM / VSCode + GCC
调试器 ST-Link/v2-v3 / J-Link / CMSIS-DAP
硬件 STM32 开发板(Nucleo / Discovery / 自制)

软件依赖(非 CubeIDE 路径):

  • arm-none-eabi-gcc 交叉编译链
  • openocdstlink-tools
  • makeCMake ≥ 3.16

2. 安装步骤

方式一:STM32CubeIDE(推荐 — 一站式)

  1. 访问 https://www.st.com/en/development-tools/stm32cubeide.html
  2. 注册 ST 账号(免费),下载对应系统安装包
  3. 安装过程会自动携带:
    • CubeMX 代码生成器
    • arm-none-eabi-gcc 工具链
    • ST-Link GDB Server
    • Eclipse 定制 IDE
  4. 首次启动后自动下载对应芯片的 HAL 固件包

方式二:CubeMX + VSCode(轻量方案)

# 1. 安装交叉工具链
## Ubuntu
sudo apt install gcc-arm-none-eabi binutils-arm-none-eabi

## macOS
brew install --cask gcc-arm-embedded

## Windows: 下载 https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain

# 2. 安装 STM32CubeMX(代码生成)
# 下载:https://www.st.com/en/development-tools/stm32cubemx.html

# 3. 安装 OpenOCD 或 stlink-tools
sudo apt install openocd stlink-tools   # Linux
brew install openocd stlink             # macOS

# 4. VSCode 安装插件
# - Cortex-Debug
# - C/C++ (Microsoft)
# - ARM Assembly

方式三:PlatformIO 选项

# platformio.ini
[env:nucleo_f401re]
platform = ststm32
board = nucleo_f401re
framework = stm32cube
# 自动拉取 HAL 依赖

3. 常见安装问题

问题 解决方案
CubeIDE 启动慢 / 卡死 给工作区分配短路径,关闭 Windows Defender 实时扫描
GCC 版本不兼容 ST 推荐使用 CubeIDE 自带工具链,版本 10.3-2021.10 前后
ST-Link 固件过旧 用 STM32CubeProgrammer 升级 ST-Link 固件
CubeMX 代码生成乱码 Windows 下设置区域语言 UTF-8 支持
调试时无法连接目标 检查 BOOT0 引脚是否拉低;按住 Reset + 连接 GDB 再释放
固件包下载失败 从 ST 官网手动下载 zip 后离线导入

示例

STM32 HAL 进阶:DMA + ADC 多通道连续采集

目标

使用 STM32F407 的 ADC1 + DMA 实现 4 通道连续采样(无需 CPU 参与传输),配合 TIM2 触发实现精确采样率。

硬件配置(CubeMX)

  • MCU:STM32F407VGT6
  • ADC1:4 通道(PA0=CH0, PA1=CH1, PA2=CH2, PA3=CH3)
  • DMA2 Stream0:外设到内存,循环模式,半字(16bit)
  • TIM2:PWM 输出触发 ADC(10kHz 采样率)
  • USART2:115200bps,DMA 发送

完整代码

/* adc_dma.c — ADC + DMA 多通道连续采样 */

#include "main.h"

/* ── 常量 ── */
#define ADC_CHANNELS    4
#define ADC_BUFFER_SIZE (ADC_CHANNELS * 256)  /* 每通道 256 个样本 */
#define SAMPLE_RATE_HZ  10000

/* ── DMA 缓冲区 ── */
static __attribute__((aligned(4))) uint16_t adc_buffer[ADC_BUFFER_SIZE];
static volatile uint8_t adc_half_complete = 0;
static volatile uint8_t adc_full_complete  = 0;

/* ── 处理后的数据(mV) ── */
static float channel_voltage[ADC_CHANNELS];

/* ══════════════════════════════════════
 * 回调:DMA 半传输完成
 * ══════════════════════════════════════ */
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) {
    if (hadc == &hadc1) {
        adc_half_complete = 1;
    }
}

/* ══════════════════════════════════════
 * 回调:DMA 全传输完成
 * ══════════════════════════════════════ */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
    if (hadc == &hadc1) {
        adc_full_complete = 1;
    }
}

/* ══════════════════════════════════════
 * 处理 ADC 数据(在 main 中调用)
 * ══════════════════════════════════════ */
static void process_adc_data(uint16_t *buf, uint32_t len) {
    float sum[ADC_CHANNELS] = {0};

    /* 对每通道数据累加 */
    uint32_t count_per_channel = len / ADC_CHANNELS;
    for (uint32_t ch = 0; ch < ADC_CHANNELS; ch++) {
        for (uint32_t i = 0; i < count_per_channel; i++) {
            sum[ch] += buf[i * ADC_CHANNELS + ch];
        }
        /* 平均 + 转换为 mV(12bit ADC,Vref=3.3V) */
        float avg_raw = sum[ch] / count_per_channel;
        channel_voltage[ch] = (avg_raw * 3300.0f) / 4095.0f;
    }
}

/* ══════════════════════════════════════
 * 打印采样结果
 * ══════════════════════════════════════ */
static void print_results(void) {
    char msg[256];
    int len = snprintf(msg, sizeof(msg),
        "CH0=%.1fmV | CH1=%.1fmV | CH2=%.1fmV | CH3=%.1fmV | Fs=%dHz\r\n",
        channel_voltage[0], channel_voltage[1],
        channel_voltage[2], channel_voltage[3],
        SAMPLE_RATE_HZ);

    /* DMA 发送到串口(非阻塞) */
    HAL_UART_Transmit_DMA(&huart2, (uint8_t *)msg, len);
}

/* ══════════════════════════════════════
 * ADC 初始化(CubeMX 生成的基础上补充)
 * ══════════════════════════════════════ */
static void adc_dma_init(void) {
    /* 校准 ADC */
    HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED);

    /* 启动 ADC DMA 连续模式 */
    HAL_ADC_Start_DMA(&hadc1,
                      (uint32_t *)adc_buffer,
                      ADC_BUFFER_SIZE);

    /* 启动 TIM2 PWM 触发 ADC */
    HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);

    printf("ADC + DMA 初始化完成 | %d 通道 @ %d Hz | 缓冲区 %d 样本\r\n",
           ADC_CHANNELS, SAMPLE_RATE_HZ, ADC_BUFFER_SIZE);
}

/* ══════════════════════════════════════
 * 主函数
 * ══════════════════════════════════════ */
int main(void) {
    HAL_Init();
    SystemClock_Config();       /* CubeMX */
    MX_GPIO_Init();             /* CubeMX */
    MX_DMA_Init();              /* CubeMX */
    MX_ADC1_Init();             /* CubeMX */
    MX_TIM2_Init();             /* CubeMX — TIM2 CH1 → ADC Trigger */
    MX_USART2_UART_Init();      /* CubeMX */

    adc_dma_init();

    while (1) {
        /* 处理半缓冲区(无缝处理,不丢数据) */
        if (adc_half_complete) {
            adc_half_complete = 0;
            /* 处理前半部分 */
            process_adc_data(adc_buffer, ADC_BUFFER_SIZE / 2);
            print_results();
        }

        if (adc_full_complete) {
            adc_full_complete = 0;
            /* 处理后半部分 */
            process_adc_data(
                adc_buffer + ADC_BUFFER_SIZE / 2,
                ADC_BUFFER_SIZE / 2);
            print_results();
        }

        /* 用户按键等其他任务... */
    }
}

/* ══════════════════════════════════════
 * 错误处理
 * ══════════════════════════════════════ */
void HAL_ADC_ErrorCallback(ADC_HandleTypeDef *hadc) {
    if (hadc == &hadc1) {
        uint32_t err = HAL_ADC_GetError(hadc);
        printf("ADC 错误: 0x%08lx\r\n", err);

        /* 重新启动 */
        HAL_ADC_Stop_DMA(&hadc1);
        HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buffer, ADC_BUFFER_SIZE);
    }
}

关键 CubeMX 设置

外设 配置
ADC1 Scan Conversion Mode: Enabled, Continuous Conversion: Disabled, DMA Continuous Requests: Enabled, Number of Conversions: 4
ADC1 Rank 1-4 Channel 0-3, Sampling Time: 480 Cycles
TIM2 Period: 8399 (84MHz / 8400 = 10kHz), CH1: PWM Mode 1, Pulse: 4200
ADC Trigger Timer 2 Capture Compare 1 event
DMA2 Stream0 Direction: Peripheral to Memory, Circular, Half Word, Priority: High

预期行为

  • CPU 零参与 ADC 数据搬运(全部 DMA 自动完成)
  • 串口以约 20Hz 频率输出 4 通道的电压平均值
  • 双缓冲机制:处理前半时 DMA 继续填充后半,零数据丢失

关键点

  • 双缓冲(Half/Full callback)是 DMA 连续采集的关键模式——处理 & 采集并行
  • TIM 触发 ADC 确保精确等间隔采样(采样抖动 < 1 个时钟周期)
  • HAL_ADC_Start_DMA 启动后无需 CPU 干预,直到回调触发
  • __attribute__((aligned(4))) 确保缓冲区对齐满足 DMA 要求

STM32 HAL:GPIO 流水灯与按键控制

目标

基于 STM32CubeMX 生成的 HAL 工程,实现 3 个 LED 流水灯和按键切换流水方向。

硬件配置(CubeMX)

  • MCU:STM32F407VGT6(Nucleo-F407ZG,其他 F4/F1/H7 类似)
  • LED1-3:PD12、PD13、PD14(Nucleo 板载 LED 为 LD4=PD12, LD5=PD13, LD6=PD14)
  • 按键:PC13(Nucleo 蓝色用户按键),上拉输入,按下为低电平
  • 时钟:HSE 8MHz → PLL 168MHz(F407 最大)

完整代码

/* main.c — 流水灯 + 按键方向控制,非阻塞实现 */

#include "main.h"

/* 外设句柄(CubeMX 自动生成) */
extern TIM_HandleTypeDef htim2;  /* 用于非阻塞定时 */

/* LED 配置 */
#define LED_COUNT      3
const uint16_t LED_PINS[LED_COUNT] = {
    GPIO_PIN_12,  // PD12 - LED1 (绿)
    GPIO_PIN_13,  // PD13 - LED2 (橙)
    GPIO_PIN_14   // PD14 - LED3 (红)
};
GPIO_TypeDef *const LED_PORT = GPIOD;

/* 按键配置 */
#define KEY_PORT      GPIOC
#define KEY_PIN       GPIO_PIN_13

/* 全局状态 */
typedef enum { FORWARD, REVERSE } Direction;
static Direction dir = FORWARD;
static uint8_t active_led = 0;
static uint32_t last_toggle_tick = 0;
static uint32_t last_key_check = 0;

/* ── 初始化 ── */
void led_init(void) {
    for (int i = 0; i < LED_COUNT; i++) {
        HAL_GPIO_WritePin(LED_PORT, LED_PINS[i], GPIO_PIN_RESET);
    }
    HAL_TIM_Base_Start(&htim2);
}

/* ── 按键扫描(消抖 + 检测下降沿) ── */
uint8_t key_pressed(void) {
    static uint8_t last_state = 1;
    uint8_t now = HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN);

    if (last_state == 1 && now == 0) {    // 下降沿
        HAL_Delay(20);                     // 消抖
        if (HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) == 0) {
            last_state = now;
            return 1;  // 有效按下
        }
    }
    last_state = now;
    return 0;
}

/* ── 流水灯更新 ── */
void led_shift(void) {
    // 先全部关闭
    for (int i = 0; i < LED_COUNT; i++) {
        HAL_GPIO_WritePin(LED_PORT, LED_PINS[i], GPIO_PIN_RESET);
    }
    // 点亮当前 LED
    HAL_GPIO_WritePin(LED_PORT, LED_PINS[active_led], GPIO_PIN_SET);

    // 根据方向移动
    if (dir == FORWARD) {
        active_led = (active_led + 1) % LED_COUNT;
    } else {
        active_led = (active_led == 0) ? (LED_COUNT - 1) : (active_led - 1);
    }
}

/* ── 主循环 ── */
int main(void) {
    HAL_Init();
    SystemClock_Config();        // CubeMX 生成
    MX_GPIO_Init();              // CubeMX 生成
    MX_TIM2_Init();              // CubeMX 生成,TIM2 用于基准计时

    led_init();

    uint32_t now;

    while (1) {
        now = HAL_GetTick();

        // 流水灯定时 (200ms)
        if (now - last_toggle_tick >= 200) {
            last_toggle_tick = now;
            led_shift();
        }

        // 按键检测 (每 10ms)
        if (now - last_key_check >= 10) {
            last_key_check = now;
            if (key_pressed()) {
                dir = (dir == FORWARD) ? REVERSE : FORWARD;

                // LED 快速闪烁指示方向切换
                for (int i = 0; i < 2; i++) {
                    HAL_GPIO_WritePin(LED_PORT, LED_PINS[1], GPIO_PIN_SET);
                    HAL_Delay(50);
                    HAL_GPIO_WritePin(LED_PORT, LED_PINS[1], GPIO_PIN_RESET);
                    HAL_Delay(50);
                }
            }
        }
    }
}

运行步骤

  1. STM32CubeMX 打开 .ioc → 引脚配置 PD12-14 为 Output,PC13 为 Input(上拉)
  2. TIM2 使能(用作 HAL_GetTick() 的时基),生成代码
  3. 将上述 main.c 替换 CubeMX 生成的 main 函数体
  4. 编译下载:arm-none-eabi-gcc 或 CubeIDE → Run
  5. 观察 LED 流水效果,按蓝色用户按键切换方向

预期行为

  • 3 个 LED 以 200ms 间隔依次点亮形成流水效果
  • 按一次按键,方向反转(逆流水)
  • 切换时中间 LED 快闪两次确认

关键点

  • HAL_GetTick() 由 SysTick 中断驱动(默认 1ms),用作非阻塞定时基准
  • 按键消抖用硬件延时 20ms + 二次确认
  • 避免在中断或主循环中使用 HAL_Delay 做长延时(阻塞型)

教程

STM32 HAL 开发实战

本章目标

掌握 STM32CubeMX + HAL 的开发流程,理解时钟树配置与外设初始化,完成定时器 PWM 和 ADC 采集。


1. STM32 家族概览

系列 内核 频率 定位
F0 Cortex-M0 48MHz 低成本替代 8-bit
F1 Cortex-M3 72MHz 经典入门
F4 Cortex-M4 168-180MHz DSP + FPU
F7 Cortex-M7 216MHz 高性能 + Cache
H7 Cortex-M7+M4 480MHz 双核旗舰
G0/G4 Cortex-M0+/M4 64/170MHz 新一代主流
L0/L4 Cortex-M0+/M4 32/80MHz 超低功耗

2. CubeMX 配置流程

步骤

  1. 新建工程 → 选择 MCU 型号(如 STM32F407VGT6)
  2. Pinout → 配置引脚功能:
    • RCC:HSE = Crystal/Ceramic Resonator
    • SYS:Debug = Serial Wire
  3. Clock Configuration → 配置时钟树:
    • HSE 8MHz → PLL /8 ×336 /2 = 168MHz (SYSCLK)
    • APB1 = 42MHz, APB2 = 84MHz
  4. 外设配置(见下方各节)
  5. Project Manager → Toolchain = Makefile(或 CubeIDE)
  6. Generate Code

3. GPIO 最佳实践

// 读取按键(带软件消抖)
uint8_t read_button(void) {
    if (HAL_GPIO_ReadPin(BTN_GPIO_Port, BTN_Pin) == GPIO_PIN_RESET) {
        HAL_Delay(20);  // 消抖
        if (HAL_GPIO_ReadPin(BTN_GPIO_Port, BTN_Pin) == GPIO_PIN_RESET) {
            while (HAL_GPIO_ReadPin(BTN_GPIO_Port, BTN_Pin) == GPIO_PIN_RESET);
            return 1;  // 按键释放后才返回
        }
    }
    return 0;
}

// 翻转 LED
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);

// 原子写入(位带操作,F1 系列不支持)
LED_GPIO_Port->BSRR = LED_Pin;       // 置位
LED_GPIO_Port->BSRR = LED_Pin << 16; // 复位

4. 定时器 PWM

CubeMX 配置

  • TIM3 → Clock Source = Internal Clock
  • Channel 1 → PWM Generation CH1
  • Prescaler = 84-1 (84MHz / 84 = 1MHz)
  • Counter Period = 1000-1 (1MHz / 1000 = 1kHz PWM)
// 初始化
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);

// 设置占空比(0-999)
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 500);  // 50%

// 呼吸灯效果
for (int duty = 0; duty <= 999; duty++) {
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, duty);
    HAL_Delay(1);
}

5. ADC 连续采样

CubeMX 配置

  • ADC1 → IN0 (PA0), Scan Conversion = Enabled
  • DMA2 Stream0 → Circular, Half Word
#define ADC_BUF_SIZE 100
static uint16_t adc_buf[ADC_BUF_SIZE];

void adc_init(void) {
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_BUF_SIZE);
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
    // DMA 完成一帧,处理数据
    float avg = 0;
    for (int i = 0; i < ADC_BUF_SIZE; i++) {
        avg += adc_buf[i];
    }
    avg /= ADC_BUF_SIZE;
    float voltage = avg * 3.3f / 4095.0f;
    printf("ADC Average: %.3f V\n", voltage);
}

6. HAL 超时机制

// 所有阻塞型 HAL API 都带超时参数
if (HAL_I2C_Master_Transmit(&hi2c1, addr, data, len, 100) != HAL_OK) {
    // 100ms 超时后返回 HAL_TIMEOUT
    handle_i2c_error();
}

非阻塞 API(_IT_DMA 后缀)不使用超时,依赖回调。


思考题

  1. STM32 的 HSI vs HSE vs PLL 分别用于什么场景?
  2. 为什么 ADC 推荐用 DMA 而非中断方式?
  3. HAL 库和 LL 库各适合什么样的开发场景?

参考资料

  1. [1] STMicroelectronics. STM32CubeF4 HAL Driver Description. 2024. https://www.st.com/en/embedded-software/stm32cubef4.html
  2. [2] STMicroelectronics. STM32F407 Reference Manual (RM0090). 2021. https://www.st.com/resource/en/reference_manual/rm0090-stm32f405415-stm32f407417-stm32f427437-and-stm32f429439-advanced-armbased-32bit-mcus-stmicroelectronics.pdf
  3. [3] Carmine Noviello. Mastering STM32. 2022.
  4. [4] STMicroelectronics. STM32 Cortex-M4 Programming Manual (PM0214). 2021. https://www.st.com/resource/en/programming_manual/pm0214-stm32-cortexm4-mcus-and-mpus-programming-manual-stmicroelectronics.pdf