ARM编译器深度解析:从Keil到VSCode的STM32开发之

前言

做了这么多年嵌入式开发,Keil MDK一直是我的主力工具。但随着项目规模越来越大,Keil的一些局限性也逐渐暴露出来:编辑器功能相对简陋、代码提示不够智能、多文件跳转效率低、界面不够现代化等等。最让人头疼的是,Keil的授权费用对个人开发者来说确实是笔不小的开支。

经过一段时间的摸索,我现在已经完全转向VSCode + ARM-GCC的开发方式,效率提升明显。这篇文章会详细记录整个迁移过程,希望能帮到有同样需求的朋友。

一、ARM编译器家族概述

1.1 ARMCC(ARM Compiler)

ARMCC是ARM官方推出的编译器,也就是Keil MDK默认使用的编译器。目前主要有两个版本:

ARMCC v5(基于ARM编译器5)

  • 这是传统的ARMCC编译器
  • 代码生成质量高,特别是对ARM架构的优化非常出色
  • 但是,ARM公司已经宣布ARMCC v5进入维护模式,不再添加新特性
  • 编译速度相对较慢

ARMCC v6(基于LLVM)

  • ARM公司新一代编译器,基于LLVM/Clang架构
  • 支持C++14/C++17等新标准
  • 编译速度有明显提升
  • 代码生成质量接近v5

ARMCC的优势在于:

  • 对ARM指令集的优化非常深入
  • 生成的代码通常比较紧凑,执行效率高
  • 与ARM的调试工具集成度高

但问题也很明显:

  • 商业软件,需要授权费用
  • 只能在Keil或DS-5等ARM官方IDE中使用
  • 开源生态相对封闭

1.2 ARM-GCC(GNU ARM Embedded Toolchain)

ARM-GCC是基于GCC的ARM交叉编译工具链,由ARM官方维护并免费提供。这是一个完全开源的解决方案。

主要特点:

  • 完全免费,无需任何授权
  • 跨平台支持Windows/Linux/macOS
  • 社区活跃,文档丰富
  • 支持所有主流ARM Cortex内核
  • 可以配合任意IDE或文本编辑器使用

工具链组成:

复制代码
arm-none-eabi-gcc      # C编译器
arm-none-eabi-g++      # C++编译器
arm-none-eabi-as       # 汇编器
arm-none-eabi-ld       # 链接器
arm-none-eabi-objcopy  # 目标文件转换工具
arm-none-eabi-objdump  # 反汇编工具
arm-none-eabi-size     # 代码大小分析工具
arm-none-eabi-gdb      # 调试器

1.3 编译器对比实测

我曾经用同一个STM32F4项目分别用ARMCC和ARM-GCC编译,结果如下:

编译器 代码大小(KB) 编译时间(s) RAM使用(KB)
ARMCC v5 42.3 8.7 18.5
ARM-GCC 10.3 44.1 6.2 18.7

可以看出,ARMCC在代码体积上略有优势,但ARM-GCC的编译速度更快。对于大多数应用场景,这点差异基本可以忽略。

二、为什么选择VSCode + ARM-GCC方案

经过实际使用,我总结了以下几个核心优势:

2.1 开发体验的飞跃

VSCode的代码编辑体验远超Keil:

  • 智能代码补全,支持基于上下文的提示
  • 强大的多光标编辑功能
  • Git集成,可视化diff和merge
  • 丰富的插件生态
  • 可定制的主题和快捷键

2.2 成本优势

Keil MDK的专业版授权费用在几千到上万元不等,而ARM-GCC + VSCode完全免费。对于个人开发者、学生或小团队来说,这是个巨大优势。

2.3 跨平台开发

VSCode + ARM-GCC可以在Windows、Linux、macOS上无缝切换。我的台式机是Windows,笔记本是Ubuntu,配置文件同步后可以在两台机器上交替开发,非常方便。

2.4 自动化和CI/CD

基于Makefile的构建系统可以很容易地集成到CI/CD流程中。我现在用Jenkins自动构建固件,每次提交代码后自动编译并生成hex文件,大大提高了效率。

三、开发环境搭建详解

3.1 安装ARM-GCC工具链

Windows平台:

  1. 访问ARM官网下载页面:

    https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm/downloads

  2. 下载最新版本(我用的是10.3-2021.10版本),安装过程中注意勾选"Add path to environment variable"

  3. 验证安装:

bash 复制代码
arm-none-eabi-gcc --version

输出类似:

复制代码
arm-none-eabi-gcc (GNU Arm Embedded Toolchain 10.3-2021.10) 10.3.1 20210824 (release)
Copyright (C) 2020 Free Software Foundation, Inc.

Linux平台:

Ubuntu可以直接用apt安装:

bash 复制代码
sudo apt-get update
sudo apt-get install gcc-arm-none-eabi binutils-arm-none-eabi

或者下载官方编译好的版本:

bash 复制代码
cd ~/Downloads
wget https://developer.arm.com/-/media/Files/downloads/gnu-rm/10.3-2021.10/gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2
tar -xjf gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2
sudo mv gcc-arm-none-eabi-10.3-2021.10 /opt/
echo 'export PATH=$PATH:/opt/gcc-arm-none-eabi-10.3-2021.10/bin' >> ~/.bashrc
source ~/.bashrc

3.2 安装Make工具

Windows:

推荐使用MinGW或直接下载GNU Make for Windows:

bash 复制代码
# 使用chocolatey安装
choco install make

# 或下载安装包
# http://gnuwin32.sourceforge.net/packages/make.htm

Linux:

bash 复制代码
sudo apt-get install build-essential

3.3 安装OpenOCD调试工具

OpenOCD是一个开源的片上调试工具,支持几乎所有主流的调试器(ST-Link、J-Link等)。

Windows:

下载预编译版本:

https://gnutoolchains.com/arm-eabi/openocd/

解压后添加到系统路径。

Linux:

bash 复制代码
sudo apt-get install openocd

验证:

bash 复制代码
openocd --version

3.4 配置VSCode

安装以下插件:

  1. C/C++ (Microsoft) - 必装
  2. Cortex-Debug - ARM调试必备
  3. Makefile Tools - Makefile支持
  4. LinkerScript - 链接脚本语法高亮
  5. Arm Assembly - 汇编语言支持

我的个人推荐还包括:

  • GitLens - 增强的Git功能
  • Better Comments - 彩色注释
  • Bracket Pair Colorizer 2 - 括号配对

四、从零搭建STM32工程

这里以STM32F103C8T6为例,详细演示整个工程的搭建过程。

4.1 工程目录结构

复制代码
STM32F103_Project/
├── Core/
│   ├── Inc/                    # 头文件
│   │   ├── main.h
│   │   ├── stm32f1xx_hal_conf.h
│   │   └── stm32f1xx_it.h
│   ├── Src/                    # 源文件
│   │   ├── main.c
│   │   ├── stm32f1xx_hal_msp.c
│   │   ├── stm32f1xx_it.c
│   │   └── system_stm32f1xx.c
│   └── Startup/
│       └── startup_stm32f103xb.s
├── Drivers/
│   ├── STM32F1xx_HAL_Driver/   # HAL库
│   └── CMSIS/                  # CMSIS库
├── Middlewares/                # 中间件(可选)
├── Build/                      # 编译输出目录
├── .vscode/                    # VSCode配置
│   ├── c_cpp_properties.json
│   ├── launch.json
│   ├── settings.json
│   └── tasks.json
├── STM32F103C8Tx_FLASH.ld     # 链接脚本
└── Makefile

4.2 获取HAL库和CMSIS

可以从ST官网下载STM32CubeMX,然后生成一个基础工程,把HAL库和CMSIS文件夹复制出来。或者直接从GitHub克隆:

bash 复制代码
git clone https://github.com/STMicroelectronics/STM32CubeF1.git

4.3 编写Makefile

这是整个工程的核心,一个完整的Makefile示例:

makefile 复制代码
######################################
# target
######################################
TARGET = STM32F103_Demo

######################################
# building variables
######################################
# debug build?
DEBUG = 1
# optimization
OPT = -Og

#######################################
# paths
#######################################
# Build path
BUILD_DIR = Build

######################################
# source
######################################
# C sources
C_SOURCES =  \
Core/Src/main.c \
Core/Src/stm32f1xx_it.c \
Core/Src/stm32f1xx_hal_msp.c \
Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_gpio_ex.c \
Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_tim.c \
Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_tim_ex.c \
Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal.c \
Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_rcc.c \
Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_rcc_ex.c \
Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_gpio.c \
Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_dma.c \
Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_cortex.c \
Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_pwr.c \
Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_flash.c \
Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_flash_ex.c \
Core/Src/system_stm32f1xx.c

# ASM sources
ASM_SOURCES =  \
Core/Startup/startup_stm32f103xb.s

#######################################
# binaries
#######################################
PREFIX = arm-none-eabi-
# 如果工具链不在系统路径中,可以指定完整路径
# BINPATH = /opt/gcc-arm-none-eabi-10.3-2021.10/bin
ifdef BINPATH
CC = $(BINPATH)/$(PREFIX)gcc
AS = $(BINPATH)/$(PREFIX)gcc -x assembler-with-cpp
CP = $(BINPATH)/$(PREFIX)objcopy
SZ = $(BINPATH)/$(PREFIX)size
else
CC = $(PREFIX)gcc
AS = $(PREFIX)gcc -x assembler-with-cpp
CP = $(PREFIX)objcopy
SZ = $(PREFIX)size
endif
HEX = $(CP) -O ihex
BIN = $(CP) -O binary -S
 
#######################################
# CFLAGS
#######################################
# cpu
CPU = -mcpu=cortex-m3

# fpu
# NONE for Cortex-M0/M0+/M3

# float-abi

# mcu
MCU = $(CPU) -mthumb $(FPU) $(FLOAT-ABI)

# macros for gcc
# AS defines
AS_DEFS = 

# C defines
C_DEFS =  \
-DUSE_HAL_DRIVER \
-DSTM32F103xB

# AS includes
AS_INCLUDES = 

# C includes
C_INCLUDES =  \
-ICore/Inc \
-IDrivers/STM32F1xx_HAL_Driver/Inc \
-IDrivers/STM32F1xx_HAL_Driver/Inc/Legacy \
-IDrivers/CMSIS/Device/ST/STM32F1xx/Include \
-IDrivers/CMSIS/Include

# compile gcc flags
ASFLAGS = $(MCU) $(AS_DEFS) $(AS_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections

CFLAGS = $(MCU) $(C_DEFS) $(C_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections

ifeq ($(DEBUG), 1)
CFLAGS += -g -gdwarf-2
endif

# Generate dependency information
CFLAGS += -MMD -MP -MF"$(@:%.o=%.d)"

#######################################
# LDFLAGS
#######################################
# link script
LDSCRIPT = STM32F103C8Tx_FLASH.ld

# libraries
LIBS = -lc -lm -lnosys 
LIBDIR = 
LDFLAGS = $(MCU) -specs=nano.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=$(BUILD_DIR)/$(TARGET).map,--cref -Wl,--gc-sections

# default action: build all
all: $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).hex $(BUILD_DIR)/$(TARGET).bin

#######################################
# build the application
#######################################
# list of objects
OBJECTS = $(addprefix $(BUILD_DIR)/,$(notdir $(C_SOURCES:.c=.o)))
vpath %.c $(sort $(dir $(C_SOURCES)))
# list of ASM program objects
OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(ASM_SOURCES:.s=.o)))
vpath %.s $(sort $(dir $(ASM_SOURCES)))

$(BUILD_DIR)/%.o: %.c Makefile | $(BUILD_DIR) 
	$(CC) -c $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(notdir $(<:.c=.lst)) $< -o $@

$(BUILD_DIR)/%.o: %.s Makefile | $(BUILD_DIR)
	$(AS) -c $(CFLAGS) $< -o $@

$(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile
	$(CC) $(OBJECTS) $(LDFLAGS) -o $@
	$(SZ) $@

$(BUILD_DIR)/%.hex: $(BUILD_DIR)/%.elf | $(BUILD_DIR)
	$(HEX) $< $@
	
$(BUILD_DIR)/%.bin: $(BUILD_DIR)/%.elf | $(BUILD_DIR)
	$(BIN) $< $@	
	
$(BUILD_DIR):
	mkdir $@		

#######################################
# clean up
#######################################
clean:
	-rm -fR $(BUILD_DIR)
  
#######################################
# dependencies
#######################################
-include $(wildcard $(BUILD_DIR)/*.d)

# *** EOF ***

这个Makefile的几个关键点:

  1. 模块化设计: 源文件、编译选项、链接选项分离,便于维护
  2. 依赖关系生成 : 使用-MMD -MP自动生成依赖文件,修改头文件后会自动重新编译相关源文件
  3. 优化选项 : -Og在调试时既保证优化又不影响调试体验
  4. 代码裁剪 : -ffunction-sections -fdata-sections配合--gc-sections可以去除未使用的代码,减小固件大小

4.4 链接脚本详解

链接脚本(Linker Script)定义了程序在内存中的布局。STM32F103C8T6的Flash是64KB,RAM是20KB,对应的链接脚本:

ld 复制代码
/* Entry Point */
ENTRY(Reset_Handler)

/* Highest address of the user mode stack */
_estack = 0x20005000;    /* end of RAM */
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x200;      /* required amount of heap  */
_Min_Stack_Size = 0x400; /* required amount of stack */

/* Specify the memory areas */
MEMORY
{
RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 20K
FLASH (rx)      : ORIGIN = 0x8000000, LENGTH = 64K
}

/* Define output sections */
SECTIONS
{
  /* The startup code goes first into FLASH */
  .isr_vector :
  {
    . = ALIGN(4);
    KEEP(*(.isr_vector)) /* Startup code */
    . = ALIGN(4);
  } >FLASH

  /* The program code and other data goes into FLASH */
  .text :
  {
    . = ALIGN(4);
    *(.text)           /* .text sections (code) */
    *(.text*)          /* .text* sections (code) */
    *(.glue_7)         /* glue arm to thumb code */
    *(.glue_7t)        /* glue thumb to arm code */
    *(.eh_frame)

    KEEP (*(.init))
    KEEP (*(.fini))

    . = ALIGN(4);
    _etext = .;        /* define a global symbols at end of code */
  } >FLASH

  /* Constant data goes into FLASH */
  .rodata :
  {
    . = ALIGN(4);
    *(.rodata)         /* .rodata sections (constants, strings, etc.) */
    *(.rodata*)        /* .rodata* sections (constants, strings, etc.) */
    . = ALIGN(4);
  } >FLASH

  .ARM.extab   : { *(.ARM.extab* .gnu.linkonce.armextab.*) } >FLASH
  .ARM : {
    __exidx_start = .;
    *(.ARM.exidx*)
    __exidx_end = .;
  } >FLASH

  .preinit_array     :
  {
    PROVIDE_HIDDEN (__preinit_array_start = .);
    KEEP (*(.preinit_array*))
    PROVIDE_HIDDEN (__preinit_array_end = .);
  } >FLASH
  
  .init_array :
  {
    PROVIDE_HIDDEN (__init_array_start = .);
    KEEP (*(SORT(.init_array.*)))
    KEEP (*(.init_array*))
    PROVIDE_HIDDEN (__init_array_end = .);
  } >FLASH
  
  .fini_array :
  {
    PROVIDE_HIDDEN (__fini_array_start = .);
    KEEP (*(SORT(.fini_array.*)))
    KEEP (*(.fini_array*))
    PROVIDE_HIDDEN (__fini_array_end = .);
  } >FLASH

  /* used by the startup to initialize data */
  _sidata = LOADADDR(.data);

  /* Initialized data sections goes into RAM, load LMA copy after code */
  .data : 
  {
    . = ALIGN(4);
    _sdata = .;        /* create a global symbol at data start */
    *(.data)           /* .data sections */
    *(.data*)          /* .data* sections */

    . = ALIGN(4);
    _edata = .;        /* define a global symbol at data end */
  } >RAM AT> FLASH

  /* Uninitialized data section */
  . = ALIGN(4);
  .bss :
  {
    _sbss = .;         /* define a global symbol at bss start */
    __bss_start__ = _sbss;
    *(.bss)
    *(.bss*)
    *(COMMON)

    . = ALIGN(4);
    _ebss = .;         /* define a global symbol at bss end */
    __bss_end__ = _ebss;
  } >RAM

  /* User_heap_stack section, used to check that there is enough RAM left */
  ._user_heap_stack :
  {
    . = ALIGN(8);
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
  } >RAM

  /* Remove information from the standard libraries */
  /DISCARD/ :
  {
    libc.a ( * )
    libm.a ( * )
    libgcc.a ( * )
  }

  .ARM.attributes 0 : { *(.ARM.attributes) }
}

关键点说明:

  1. 内存区域定义: STM32F103C8的Flash起始地址0x08000000,RAM起始地址0x20000000
  2. 段的布局 :
    • .isr_vector: 中断向量表,必须放在Flash开始位置
    • .text: 代码段
    • .rodata: 只读数据(常量、字符串等)
    • .data: 初始化的全局变量(需要从Flash复制到RAM)
    • .bss: 未初始化的全局变量
  3. 栈和堆 : _estack定义栈顶,_Min_Heap_Size_Min_Stack_Size定义最小堆栈大小

4.5 VSCode配置文件

c_cpp_properties.json

这个文件配置IntelliSense,让代码提示和跳转正常工作:

json 复制代码
{
    "configurations": [
        {
            "name": "STM32",
            "includePath": [
                "${workspaceFolder}/**",
                "${workspaceFolder}/Core/Inc",
                "${workspaceFolder}/Drivers/STM32F1xx_HAL_Driver/Inc",
                "${workspaceFolder}/Drivers/STM32F1xx_HAL_Driver/Inc/Legacy",
                "${workspaceFolder}/Drivers/CMSIS/Device/ST/STM32F1xx/Include",
                "${workspaceFolder}/Drivers/CMSIS/Include"
            ],
            "defines": [
                "USE_HAL_DRIVER",
                "STM32F103xB"
            ],
            "compilerPath": "C:/Program Files (x86)/GNU Arm Embedded Toolchain/10 2021.10/bin/arm-none-eabi-gcc.exe",
            "cStandard": "c11",
            "cppStandard": "c++17",
            "intelliSenseMode": "gcc-arm",
            "compilerArgs": [
                "-mcpu=cortex-m3",
                "-mthumb",
                "-specs=nano.specs"
            ]
        }
    ],
    "version": 4
}

tasks.json

定义编译任务:

json 复制代码
{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Build STM32",
            "type": "shell",
            "command": "make",
            "args": [
                "-j8"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "problemMatcher": [
                "$gcc"
            ],
            "presentation": {
                "reveal": "always",
                "panel": "new"
            }
        },
        {
            "label": "Clean",
            "type": "shell",
            "command": "make",
            "args": [
                "clean"
            ],
            "problemMatcher": []
        },
        {
            "label": "Flash",
            "type": "shell",
            "command": "openocd",
            "args": [
                "-f", "interface/stlink.cfg",
                "-f", "target/stm32f1x.cfg",
                "-c", "program Build/STM32F103_Demo.elf verify reset exit"
            ],
            "dependsOn": "Build STM32",
            "problemMatcher": []
        }
    ]
}

launch.json

配置调试:

json 复制代码
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Cortex Debug",
            "cwd": "${workspaceFolder}",
            "executable": "./Build/STM32F103_Demo.elf",
            "request": "launch",
            "type": "cortex-debug",
            "runToEntryPoint": "main",
            "servertype": "openocd",
            "device": "STM32F103C8",
            "configFiles": [
                "interface/stlink.cfg",
                "target/stm32f1x.cfg"
            ],
            "svdFile": "${workspaceFolder}/STM32F103.svd",
            "preLaunchTask": "Build STM32"
        }
    ]
}

4.6 实战示例:LED闪烁程序

main.c

c 复制代码
#include "main.h"

/* Private variables */
TIM_HandleTypeDef htim2;

/* Private function prototypes */
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_TIM2_Init(void);

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_TIM2_Init();

    /* Start timer */
    HAL_TIM_Base_Start_IT(&htim2);

    /* Infinite loop */
    while (1)
    {
        // 主循环可以做其他事情
        HAL_Delay(1000);
    }
}

/**
  * @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();
    }
}

/**
  * @brief TIM2 Initialization Function
  * @param None
  * @retval None
  */
static void MX_TIM2_Init(void)
{
    TIM_ClockConfigTypeDef sClockSourceConfig = {0};
    TIM_MasterConfigTypeDef sMasterConfig = {0};

    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 7200 - 1;  // 72MHz / 7200 = 10kHz
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 5000 - 1;     // 10kHz / 5000 = 2Hz (500ms)
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
    if (HAL_TIM_Base_Init(&htim2) != HAL_OK)
    {
        Error_Handler();
    }
    sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
    if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK)
    {
        Error_Handler();
    }
    sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
    sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
    if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK)
    {
        Error_Handler();
    }
}

/**
  * @brief GPIO Initialization Function
  * @param None
  * @retval None
  */
static void MX_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    /* GPIO Ports Clock Enable */
    __HAL_RCC_GPIOC_CLK_ENABLE();
    __HAL_RCC_GPIOD_CLK_ENABLE();

    /* Configure GPIO pin Output Level */
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);

    /* Configure GPIO pin : PC13 (LED) */
    GPIO_InitStruct.Pin = GPIO_PIN_13;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
}

/**
  * @brief  Period elapsed callback in non blocking mode
  * @note   This function is called  when TIM2 interrupt took place, inside
  * HAL_TIM_IRQHandler(). It makes a direct call to HAL_IncTick() to increment
  * a global variable "uwTick" used as application time base.
  * @param  htim : TIM handle
  * @retval None
  */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
    }
}

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
    __disable_irq();
    while (1)
    {
    }
}

#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 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) */
}
#endif /* USE_FULL_ASSERT */

stm32f1xx_it.c

中断服务函数:

c 复制代码
#include "main.h"
#include "stm32f1xx_it.h"

extern TIM_HandleTypeDef htim2;

/**
  * @brief This function handles Non maskable interrupt.
  */
void NMI_Handler(void)
{
}

/**
  * @brief This function handles Hard fault interrupt.
  */
void HardFault_Handler(void)
{
    while (1)
    {
    }
}

/**
  * @brief This function handles Memory management fault.
  */
void MemManage_Handler(void)
{
    while (1)
    {
    }
}

/**
  * @brief This function handles Prefetch fault, memory access fault.
  */
void BusFault_Handler(void)
{
    while (1)
    {
    }
}

/**
  * @brief This function handles Undefined instruction or illegal state.
  */
void UsageFault_Handler(void)
{
    while (1)
    {
    }
}

/**
  * @brief This function handles System service call via SWI instruction.
  */
void SVC_Handler(void)
{
}

/**
  * @brief This function handles Debug monitor.
  */
void DebugMon_Handler(void)
{
}

/**
  * @brief This function handles Pendable request for system service.
  */
void PendSV_Handler(void)
{
}

/**
  * @brief This function handles System tick timer.
  */
void SysTick_Handler(void)
{
    HAL_IncTick();
}

/**
  * @brief This function handles TIM2 global interrupt.
  */
void TIM2_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&htim2);
}

五、编译和下载

5.1 编译项目

在VSCode中按Ctrl+Shift+B或在终端执行:

bash 复制代码
make -j8

编译成功后会看到:

复制代码
arm-none-eabi-gcc Build/main.o Build/stm32f1xx_it.o Build/stm32f1xx_hal_msp.o ... -o Build/STM32F103_Demo.elf
arm-none-eabi-size Build/STM32F103_Demo.elf
   text    data     bss     dec     hex filename
  12456     108    1640   14204    377c Build/STM32F103_Demo.elf
arm-none-eabi-objcopy -O ihex Build/STM32F103_Demo.elf Build/STM32F103_Demo.hex
arm-none-eabi-objcopy -O binary -S Build/STM32F103_Demo.elf Build/STM32F103_Demo.bin

5.2 下载程序

使用ST-Link下载:

bash 复制代码
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c "program Build/STM32F103_Demo.elf verify reset exit"

或者配置好tasks.json后,直接运行Flash任务。

5.3 在线调试

F5启动调试,Cortex-Debug插件会自动:

  1. 编译项目
  2. 启动OpenOCD
  3. 连接目标板
  4. 下载程序
  5. 运行到main函数

调试界面可以:

  • 查看寄存器值(通过SVD文件解析)
  • 查看外设状态
  • 设置断点
  • 单步执行
  • 查看变量
  • 查看内存

六、高级技巧

6.1 多配置编译

在实际项目中,经常需要编译不同的配置版本(Debug/Release)。可以这样改造Makefile:

makefile 复制代码
# 在命令行指定配置
# make BUILD=Debug
# make BUILD=Release

BUILD ?= Debug

ifeq ($(BUILD), Debug)
    OPT = -Og
    C_DEFS += -DDEBUG
    CFLAGS += -g -gdwarf-2
else
    OPT = -O2
    LDFLAGS += -s  # strip symbols
endif

6.2 自动生成编译数据库

为了让clangd等工具更好地工作,可以生成compile_commands.json:

bash 复制代码
# 安装bear工具
sudo apt-get install bear

# 生成编译数据库
bear -- make clean all

6.3 代码大小优化

几个实用的优化技巧:

  1. 启用LTO(Link Time Optimization):
makefile 复制代码
CFLAGS += -flto
LDFLAGS += -flto
  1. 使用newlib-nano:
makefile 复制代码
LDFLAGS += -specs=nano.specs
  1. 优化浮点运算:
makefile 复制代码
# 如果确实不需要完整的printf浮点支持
LDFLAGS += -u _printf_float
  1. 分析代码大小:
bash 复制代码
arm-none-eabi-nm --size-sort -S Build/STM32F103_Demo.elf

6.4 集成FreeRTOS

添加FreeRTOS源文件到Makefile:

makefile 复制代码
C_SOURCES += \
Middlewares/Third_Party/FreeRTOS/Source/croutine.c \
Middlewares/Third_Party/FreeRTOS/Source/event_groups.c \
Middlewares/Third_Party/FreeRTOS/Source/list.c \
Middlewares/Third_Party/FreeRTOS/Source/queue.c \
Middlewares/Third_Party/FreeRTOS/Source/stream_buffer.c \
Middlewares/Third_Party/FreeRTOS/Source/tasks.c \
Middlewares/Third_Party/FreeRTOS/Source/timers.c \
Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM3/port.c \
Middlewares/Third_Party/FreeRTOS/Source/portable/MemMang/heap_4.c

C_INCLUDES += \
-IMiddlewares/Third_Party/FreeRTOS/Source/include \
-IMiddlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM3

七、常见问题排查

7.1 编译错误

问题 : undefined reference to '__libc_init_array'

解决 : 检查启动文件是否正确添加,确保链接脚本中包含.preinit_array.init_array

问题 : section .data will not fit in region RAM

解决: 全局变量太多导致RAM溢出,检查是否有大数组定义,考虑使用动态分配或将部分数据放到Flash

7.2 下载问题

问题: OpenOCD连接失败

解决:

bash 复制代码
# 检查ST-Link驱动
# Windows需要安装官方驱动或用Zadig安装WinUSB驱动
# Linux需要添加udev规则

# /etc/udev/rules.d/49-stlinkv2.rules
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="3748", MODE:="0666"

7.3 调试问题

问题: 断点无法命中

解决 : 检查优化等级,-O2以上优化可能导致代码被优化掉。调试时使用-Og

问题 : 变量显示<optimized out>

解决 : 降低优化等级或使用volatile关键字

八、性能对比

我用一个实际的STM32F407项目测试了Keil和VSCode两种开发方式:

对比项 Keil MDK VSCode + ARM-GCC
全量编译时间 18.3秒 12.7秒
增量编译时间 2.1秒 1.6秒
代码跳转响应 0.5秒 0.1秒
代码提示延迟 明显 几乎无感知
内存占用 450MB 280MB
固件大小 156KB 162KB

九、总结

经过几个月的实践,我已经完全习惯了VSCode + ARM-GCC的开发方式。虽然初期搭建环境需要花点时间,但后续的开发效率提升是显而易见的。

这套方案特别适合:

  • 个人开发者和学生(零成本)
  • 需要跨平台开发的团队
  • 习惯使用Git的开发者
  • 需要集成CI/CD的项目
  • 追求现代化开发体验的工程师

不适合的场景:

  • 公司已经购买了Keil授权
  • 团队成员不熟悉命令行工具
  • 项目严重依赖Keil特有功能

最后分享一个小经验:刚开始迁移时不要一次性把所有项目都转过来,可以先用一个小项目练手,熟悉整个流程后再逐步迁移大项目。遇到问题多查文档,ARM-GCC和OpenOCD的社区都很活跃,基本上遇到的问题都能找到解决方案。

文中的完整工程文件我已经上传到GitHub,有需要的朋友可以直接clone下来参考。有问题欢迎在评论区交流!


参考资料:

相关推荐
fengfuyao9852 小时前
STM32 控制 SG90 舵机指南
stm32·单片机·嵌入式硬件
学不懂飞行器2 小时前
【电赛保姆级教程】电赛视觉怎么选?怎么调?从OpenMV到边缘计算硬核避坑指南(附高鲁棒通信源码)
人工智能·stm32·边缘计算·电赛·视觉
kkoral3 小时前
Vue3 图片标框功能实现方案
前端·vue.js·vscode·typescript
星夜夏空994 小时前
STM32单片机学习(29) —— SPI引脚和外设初始化
stm32·单片机·学习
不断学习加努力4 小时前
ubuntu22.04的vscode上部署claude的教程
ide·vscode·编辑器
三品吉他手会点灯4 小时前
STM32F103 学习笔记-23-常用存储器原理与分类
笔记·stm32·单片机·嵌入式硬件·学习
陌上花开缓缓归以4 小时前
基于 W25N01KV 的 MTD/BBT/BMT/UBI 框架与坏块导致系统挂死问题剖析
arm开发
dotRed4 小时前
VSCode + CubeMX + Makefile 构建STM32工程:分层架构与双调试配置
ide·vscode·stm32
m0_377108144 小时前
stm32-TIM
stm32·单片机·嵌入式硬件
相醉为友4 小时前
001 VSCode图形化提交也弹出GPG密码框
ide·vscode·编辑器