MCU进阶篇(二)——Linux搭建STM32基础工程

前言:
   本篇在 Linux 下从零搭建一个 STM32F103ZET6 的裸机工程:用 arm-none-eabi-gcc 编译,用 CMake 管理工程,最后通过 GPIO 点亮 M144Z-M3 开发板上的 LED0 和 LED1。工程按 core、drv、strtup、lnk、cmake 分层组织,先不引入 RTOS,先做一个纯裸机工程。固件的下载和调试不在本文范围内,可以参考《OpenOCD 基础篇(二)——烧录和调试》。

1 目标与硬件

1.1 最终目标

  完成这篇文章后,应该得到这样一个工程:

  • 在 Linux 终端里一条命令完成编译,生成 .elf、.hex、.bin。
  • 工程按 core、drv、strtup、lnk、cmake 分层组织,结构清晰,便于后续扩展。
  • 板载 LED0 每 500 ms 翻转一次,LED1 每 1000 ms 翻转一次。

  本文不依赖 Keil、IAR 或 STM32CubeIDE,编译和链接全部在 Linux 命令行里完成。第一次先把最小工程跑通,后面再往里面加 UART、定时器、中断和 FreeRTOS。

1.2 开发板与芯片

  本文使用正点原子 M144Z-M3 最小系统板 STM32F103 版:

项目 说明
MCU STM32F103ZET6,Cortex-M3,512 KB Flash,64 KB SRAM,最高 72 MHz
外部高速晶振 8 MHz HSE
外部低速晶振 32.768 kHz LSE
LED0 PB5,红色,低电平点亮
LED1 PE5,绿色,低电平点亮
USB 串口 CH340,连接 USART1 的 PA9/PA10

  如果你用的是其他 STM32F103 板子,只需要把 LED 引脚、HSE 频率、Flash/SRAM 大小和启动文件换成自己的,后面的步骤不变。

1.3 参考资料

  本文需要两类资料:

  • STM32CubeF1 固件包:包含 CMSIS、HAL 库、启动文件和官方模板工程。可以在 ST 官网的 STM32CubeF1 页面 下载。
  • M144Z-M3 开发板资料:随开发板提供,包含使用指南和硬件参考手册。本文用到《M144Z-M3 最小系统板使用指南》第 10 章“跑马灯实验”和《M144Z-M3 最小系统板硬件参考手册》第 2.1.14 节 LED 原理图。

  寄存器细节可以参考 ST 官方的 RM0008 参考手册。

  如果你用的不是 M144Z-M3,只需要把 LED 引脚、HSE 频率、Flash/SRAM 大小和启动文件换成自己板子的参数,后文会指出需要修改的位置。如果你没有开发板资料,本文前面的表格已经给出了用到的 LED 引脚;换板子时以自己板子的原理图为准。

  本文的工程目录和 CMake 脚本都按下面的步骤从零创建。

2 Linux 环境准备

2.1 安装工具链

  以 Ubuntu/Debian 为例,安装交叉编译器和构建工具:

1
2
3
sudo apt update
sudo apt install -y build-essential cmake ninja-build git unzip
sudo apt install -y gcc-arm-none-eabi binutils-arm-none-eabi libnewlib-arm-none-eabi

  安装完成后确认版本:

1
2
3
4
arm-none-eabi-gcc --version
arm-none-eabi-objcopy --version
cmake --version
ninja --version

  不同发行版的包名略有差异。如果 gcc-arm-none-eabi 版本较旧也没关系,Cortex-M3 的编译不受影响。

2.2 解压官方固件包

  从 ST 官网下载 STM32CubeF1 后,把压缩包放到 ~/work/stm32f103-src 并解压。下面假设压缩包名为 stm32cubef1.zip,解压后的目录为 STM32Cube_FW_F1_V1.8.0;如果你下载的版本或文件名不同,按实际名称修改:

1
2
3
4
mkdir -p ~/work/stm32f103-src
cd ~/work/stm32f103-src
unzip -q stm32cubef1.zip
export CUBEF1=~/work/stm32f103-src/STM32Cube_FW_F1_V1.8.0

  如果你已经解压过,直接把 CUBEF1 指向解压后的根目录即可,例如:

1
export CUBEF1=~/work/stm32f103-src/STM32Cube_FW_F1_V1.8.0

  export 只在当前终端有效,换终端后需要重新执行,或者把它写进 ~/.bashrc / ~/.zshrc。

  确认关键文件存在:

1
2
ls $CUBEF1/Drivers/STM32F1xx_HAL_Driver/Src
ls $CUBEF1/Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/gcc/startup_stm32f103xe.s

3 工程目录设计

3.1 目录结构

  本文把工程放在 ~/work/stm32f103-basic;如果你的工作目录不同,后面命令里的路径相应替换。工程结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
stm32f103-basic/
├── CMakeLists.txt
├── cmake/
│ └── gcc-arm-none-eabi.cmake
├── core/
│ ├── CMakeLists.txt
│ ├── inc/
│ │ ├── main.h
│ │ ├── stm32f1xx_hal_conf.h
│ │ └── stm32f1xx_it.h
│ └── src/
│ ├── main.c
│ ├── stm32f1xx_hal_msp.c
│ ├── stm32f1xx_it.c
│ ├── system_stm32f1xx.c
│ └── syscalls.c
├── drv/
│ ├── CMakeLists.txt
│ └── stm32/
│ └── f1xx/
│ ├── CMSIS/
│ │ ├── Include/
│ │ └── Device/ST/STM32F1xx/Include/
│ └── STM32F1xx_HAL_Driver/
│ ├── Inc/
│ └── Src/
├── strtup/
│ ├── CMakeLists.txt
│ └── stm32/
│ └── f103xe/
│ └── strtup.s
├── lnk/
│ └── stm32/
│ └── f103xx/
│ └── STM32F103XX_FLASH.ld
└── build/

3.2 各目录的职责

  工程目录的职责划分如下:

目录 作用 是否修改
cmake/ 交叉编译工具链配置 自己维护
core/ 应用代码、时钟、中断、HAL 配置、系统文件 主要修改区域
drv/ ST 官方 CMSIS 和 HAL 驱动 不修改,整体替换升级
strtup/ 启动汇编和中断向量表 一般不改
lnk/ 链接脚本,定义 Flash/SRAM 和段布局 换芯片时修改
build/ CMake 构建输出 不提交到 Git

  本文先直接调用 HAL,不引入额外的封装;等 LED 跑通、工程骨架稳定后,再按需扩展。

3.3 创建目录

  执行:

1
2
3
4
5
6
7
8
mkdir -p ~/work/stm32f103-basic
cd ~/work/stm32f103-basic
mkdir -p cmake \
core/inc core/src \
drv/stm32/f1xx/CMSIS \
drv/stm32/f1xx/CMSIS/Device/ST/STM32F1xx \
strtup/stm32/f103xe \
lnk/stm32/f103xx

4 从官方固件包复制文件

4.1 复制 CMSIS 和 HAL

  先把 ST 官方驱动整体复制到 drv。这部分代码不要修改,后续升级 CubeF1 时直接整体替换:

1
2
3
4
5
6
7
8
9
10
11
12
13
cd ~/work/stm32f103-basic

# CMSIS 内核头文件。
cp -r "$CUBEF1/Drivers/CMSIS/Include" \
drv/stm32/f1xx/CMSIS/

# STM32F1 设备头文件和 system 头文件。
cp -r "$CUBEF1/Drivers/CMSIS/Device/ST/STM32F1xx/Include" \
drv/stm32/f1xx/CMSIS/Device/ST/STM32F1xx/

# HAL 驱动,包含 Inc、Src 和 Inc/Legacy。
cp -r "$CUBEF1/Drivers/STM32F1xx_HAL_Driver" \
drv/stm32/f1xx/

  复制完成后,drv/stm32/f1xx 下应该有:

1
2
3
4
5
6
7
drv/stm32/f1xx/
├── CMSIS/
│ ├── Include/
│ └── Device/ST/STM32F1xx/Include/
└── STM32F1xx_HAL_Driver/
├── Inc/
└── Src/

4.2 复制启动文件、系统文件和模板

  启动文件必须和芯片容量匹配。STM32F103ZET6 是大容量产品,选 startup_stm32f103xe.s;如果选成 startup_stm32f103xb.s,中断向量表会不完整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
cd ~/work/stm32f103-basic

# 启动文件:GCC 版本,F103xE 大容量。
cp "$CUBEF1/Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/gcc/startup_stm32f103xe.s" \
strtup/stm32/f103xe/strtup.s

# system_stm32f1xx.c:SystemInit、SystemCoreClockUpdate。
cp "$CUBEF1/Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/system_stm32f1xx.c" \
core/src/

# 中断服务程序和 HAL MSP 模板。
cp "$CUBEF1/Projects/STM32F103RB-Nucleo/Templates/Src/stm32f1xx_it.c" \
core/src/
cp "$CUBEF1/Projects/STM32F103RB-Nucleo/Templates/Src/stm32f1xx_hal_msp.c" \
core/src/
cp "$CUBEF1/Projects/STM32F103RB-Nucleo/Templates/Inc/stm32f1xx_it.h" \
core/inc/

# HAL 配置模板,复制后重命名为 stm32f1xx_hal_conf.h。
cp "$CUBEF1/Drivers/STM32F1xx_HAL_Driver/Inc/stm32f1xx_hal_conf_template.h" \
core/inc/stm32f1xx_hal_conf.h

# newlib 系统调用桩,方便以后使用 printf。
cp "$CUBEF1/Projects/STM3210C_EVAL/Examples/UART/UART_Printf/syscalls.c" \
core/src/

  注意这里没有复制 Nucleo 模板的 main.h。那个文件包含 stm32f1xx_nucleo.h,会依赖 Nucleo 板级 BSP。本文自己写一个最小的 main.h,只包含 HAL 头文件。

  不同版本的 CubeF1 目录结构可能略有差异。如果上面的某个路径不存在,可以用 find 按文件名定位,例如:

1
2
find "$CUBEF1" -name syscalls.c
find "$CUBEF1" -name stm32f1xx_hal_conf_template.h

  链接脚本需要按 STM32F103ZET6 的 512 KB Flash / 64 KB RAM 配置。最方便的做法是用 STM32CubeMX 新建一个(或使用你已有的)STM32F103ZETx 工程,只配置时钟和 GPIO 即可。生成代码后找到链接脚本:

1
2
3
# 把 CUBEMX_PROJ 换成你的 CubeMX 工程目录。
export CUBEMX_PROJ=~/work/STM32F103ZE-empty
find "$CUBEMX_PROJ" -name "*FLASH.ld"

  find 可能会列出多个文件,选名字里带 FLASH 的那个。把找到的文件复制到本工程(下面以 STM32F103XX_FLASH.ld 为例,文件名不同时替换):

1
cp "$CUBEMX_PROJ/STM32F103XX_FLASH.ld" lnk/stm32/f103xx/

  复制后检查文件里的 MEMORY 段,确认 FLASH 的 LENGTH = 512K、RAM 的 LENGTH = 64K。如果手头没有现成的 F103ZE 工程,用 CubeMX 新建一个只配置时钟和 GPIO 的工程,生成后复制它的链接脚本即可,不需要把 CubeMX 工程的其他文件带进来。

  复制完成后检查一下:

1
find core drv strtup lnk -maxdepth 4 -type f | sort | head -40

5 编写 CMake 工程

5.1 工具链文件

  创建 cmake/gcc-arm-none-eabi.cmake:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)

set(TOOLCHAIN_PREFIX arm-none-eabi-)
set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}gcc)
set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER})
set(CMAKE_OBJCOPY ${TOOLCHAIN_PREFIX}objcopy)
set(CMAKE_SIZE ${TOOLCHAIN_PREFIX}size)

set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
set(CMAKE_EXECUTABLE_SUFFIX ".elf")

set(TARGET_FLAGS "-mcpu=cortex-m3 -mthumb")

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${TARGET_FLAGS} -Wall -fdata-sections -ffunction-sections")
set(CMAKE_ASM_FLAGS "${CMAKE_C_FLAGS} -x assembler-with-cpp")
set(CMAKE_C_FLAGS_DEBUG "-O0 -g3")
set(CMAKE_C_FLAGS_RELEASE "-Os -g0")

set(CMAKE_EXE_LINKER_FLAGS "${TARGET_FLAGS}")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -T ${CMAKE_CURRENT_LIST_DIR}/../lnk/stm32/f103xx/STM32F103XX_FLASH.ld")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --specs=nano.specs --specs=nosys.specs")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-Map=firmware.map -Wl,--gc-sections -Wl,--print-memory-usage")

  几个关键点:

  • -mcpu=cortex-m3 -mthumb 必须和芯片内核一致。
  • -T 指向链接脚本,路径用 CMAKE_CURRENT_LIST_DIR 相对工具链文件计算,避免绝对路径。
  • --specs=nano.specs 使用 newlib-nano,减小代码体积。
  • --specs=nosys.specs 提供最基础的系统调用桩。
  • -Wl,--gc-sections 配合 -ffunction-sections -fdata-sections 删除未使用的函数和数据。
  • -Wl,--print-memory-usage 在链接结束时打印 Flash/RAM 占用。

5.2 根 CMakeLists.txt

  创建根目录 CMakeLists.txt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
cmake_minimum_required(VERSION 3.16)

if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif()

set(CMAKE_TOOLCHAIN_FILE ${CMAKE_CURRENT_SOURCE_DIR}/cmake/gcc-arm-none-eabi.cmake)
set(CMAKE_PROJECT_NAME stm32f103-basic)

project(${CMAKE_PROJECT_NAME} C ASM)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)

add_executable(${PROJECT_NAME})

add_subdirectory(core)
add_subdirectory(drv)
add_subdirectory(strtup)

target_compile_definitions(${PROJECT_NAME} PRIVATE
STM32F103xE
USE_HAL_DRIVER
HSE_VALUE=8000000
)

target_link_libraries(${PROJECT_NAME} PRIVATE
m
c
)

add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O ihex
$<TARGET_FILE:${PROJECT_NAME}>
$<TARGET_FILE_DIR:${PROJECT_NAME}>/${CMAKE_PROJECT_NAME}.hex
COMMAND ${CMAKE_OBJCOPY} -O binary
$<TARGET_FILE:${PROJECT_NAME}>
$<TARGET_FILE_DIR:${PROJECT_NAME}>/${CMAKE_PROJECT_NAME}.bin
COMMENT "Generating HEX and BIN"
)

add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${PROJECT_NAME}>
COMMENT "Firmware size"
)

  STM32F103xE 是 HAL/CMSIS 的器件宏,必须和 startup_stm32f103xe.s 对应。USE_HAL_DRIVER 告诉 CMSIS 头文件包含 HAL 配置。HSE_VALUE=8000000 和板载 8 MHz 晶振一致。

5.3 子目录 CMakeLists

  core/CMakeLists.txt:

1
2
3
4
5
file(GLOB CORE_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/*.c")
target_sources(${PROJECT_NAME} PRIVATE ${CORE_SOURCES})
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/inc
)

  drv/CMakeLists.txt:

1
2
3
4
5
6
7
8
9
10
11
12
file(GLOB HAL_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/stm32/f1xx/STM32F1xx_HAL_Driver/Src/*.c"
)

target_sources(${PROJECT_NAME} PRIVATE ${HAL_SOURCES})

target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/stm32/f1xx/CMSIS/Include
${CMAKE_CURRENT_SOURCE_DIR}/stm32/f1xx/CMSIS/Device/ST/STM32F1xx/Include
${CMAKE_CURRENT_SOURCE_DIR}/stm32/f1xx/STM32F1xx_HAL_Driver/Inc
${CMAKE_CURRENT_SOURCE_DIR}/stm32/f1xx/STM32F1xx_HAL_Driver/Inc/Legacy
)

  strtup/CMakeLists.txt:

1
2
3
target_sources(${PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/stm32/f103xe/strtup.s
)

  这里用 file(GLOB) 把 HAL 的所有 .c 文件都加入编译。没有启用的 HAL 模块会编译成空文件,链接时再被 --gc-sections 删掉,第一次搭建工程时比较省事。后续如果在意编译速度,可以改成只列实际使用的源文件。

6 配置 HAL

6.1 启用需要的模块

  打开 core/inc/stm32f1xx_hal_conf.h,只保留本文需要的模块:

1
2
3
4
5
6
#define HAL_MODULE_ENABLED
#define HAL_CORTEX_MODULE_ENABLED
#define HAL_FLASH_MODULE_ENABLED
#define HAL_GPIO_MODULE_ENABLED
#define HAL_PWR_MODULE_ENABLED
#define HAL_RCC_MODULE_ENABLED

  其他模块(ADC、CAN、DMA、I2C、SPI、TIM、UART 等)先注释掉。本文只做 GPIO 点灯,用不到它们;后面加 UART 或定时器时再打开对应模块。

  确认下面几个值与板子一致:

1
2
3
4
#define HSE_VALUE              8000000U
#define HSI_VALUE 8000000U
#define TICK_INT_PRIORITY 15U
#define USE_RTOS 0U

  HSE_VALUE 也可以由 CMake 的 HSE_VALUE=8000000 编译宏提供,两处保持一致即可。

6.2 HAL_MspInit

  core/src/stm32f1xx_hal_msp.c 里的 HAL_MspInit() 可以保持为空:

1
2
3
void HAL_MspInit(void)
{
}

  如果需要提前打开 AFIO/PWR 时钟,可以写成:

1
2
3
4
5
void HAL_MspInit(void)
{
__HAL_RCC_AFIO_CLK_ENABLE();
__HAL_RCC_PWR_CLK_ENABLE();
}

6.3 SysTick 中断

  core/src/stm32f1xx_it.c 里的 SysTick_Handler() 负责给 HAL 提供 1 ms 时基:

1
2
3
4
void SysTick_Handler(void)
{
HAL_IncTick();
}

  HAL_Init() 会配置 SysTick 为 1 ms 中断,HAL_RCC_ClockConfig() 在时钟切换后会按新的 HCLK 重新配置 SysTick。因此 HAL_Delay(500) 可以直接使用。如果后面要把 SysTick 交给 FreeRTOS,就需要重新处理这个中断,那是另一篇文章的内容。

6.4 system 和 syscalls

  system_stm32f1xx.c 由启动文件调用,负责 SystemInit() 和 SystemCoreClockUpdate(),本文不需要修改。syscalls.c 提供 newlib 的 _write、_read、_sbrk 等桩函数,当前点灯用不到,但后面加 printf 时可以直接在里面重定向到 UART。

  本文复制的 CubeF1 syscalls.c 已经包含 _sbrk,一般不需要额外的 sysmem.c。如果你换用了其它精简版 syscalls.c,链接时报 undefined reference to _sbrk,再从 CubeMX 工程复制一个 sysmem.c 到 core/src/ 即可。

7 编写 main.c

7.1 最小 main.h

  创建 core/inc/main.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef __MAIN_H
#define __MAIN_H

#ifdef __cplusplus
extern "C" {
#endif

#include "stm32f1xx_hal.h"

void Error_Handler(void);

#ifdef __cplusplus
}
#endif

#endif /* __MAIN_H */

7.2 时钟和 LED

  创建 core/src/main.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include "main.h"

static void SystemClock_Config(void);
static void LED_Init(void);

int main(void)
{
uint32_t tick = 0;

HAL_Init();
SystemClock_Config();
LED_Init();

while (1)
{
/* LED0 每 500 ms 翻转一次。 */
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);

/* LED1 每 1000 ms 翻转一次。 */
if ((tick % 2u) == 0u)
{
HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_5);
}

tick++;
HAL_Delay(500);
}
}

static void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};

__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();

/* LED 低电平点亮,先输出高电平让它们熄灭。 */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_5, GPIO_PIN_SET);

GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;

HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);
}

static void SystemClock_Config(void)
{
RCC_OscInitTypeDef osc = {0};
RCC_ClkInitTypeDef clk = {0};

/* HSE 8 MHz,PLL x9 -> SYSCLK 72 MHz。 */
osc.OscillatorType = RCC_OSCILLATORTYPE_HSE;
osc.HSEState = RCC_HSE_ON;
osc.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
osc.PLL.PLLState = RCC_PLL_ON;
osc.PLL.PLLSource = RCC_PLLSOURCE_HSE;
osc.PLL.PLLMUL = RCC_PLL_MUL9;

if (HAL_RCC_OscConfig(&osc) != HAL_OK)
{
Error_Handler();
}

/* AHB 72 MHz,APB2 72 MHz,APB1 36 MHz。 */
clk.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK |
RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
clk.AHBCLKDivider = RCC_SYSCLK_DIV1;
clk.APB1CLKDivider = RCC_HCLK_DIV2;
clk.APB2CLKDivider = RCC_HCLK_DIV1;

if (HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}

void Error_Handler(void)
{
__disable_irq();
while (1)
{
}
}

  这段代码的关键点:

  • 板载 HSE 是 8 MHz,PLL 倍频 9 得到 72 MHz。
  • 72 MHz 时 Flash 需要 2 个等待周期,所以用 FLASH_LATENCY_2。
  • APB1 的最大频率是 36 MHz,所以要 2 分频;APB2 可以到 72 MHz,不分频。
  • LED0 在 PB5,LED1 在 PE5,都是低电平点亮;初始化时先写高电平熄灭。
  • 循环每 500 ms 执行一次:LED0 每次都翻转,所以亮 500 ms、灭 500 ms;LED1 每两次循环翻转一次,所以亮 1 s、灭 1 s。

  如果希望两个 LED 的闪烁节奏不同,也可以各自用一个计数变量控制翻转次数,但第一次验证不建议把逻辑写复杂。

8 编译工程

8.1 配置和构建

  在工程根目录执行:

1
2
3
cd ~/work/stm32f103-basic
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j

  如果系统没有 Ninja,去掉 -G Ninja,CMake 会使用默认的 Unix Makefiles:

1
2
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j

  构建成功后,build/ 下会生成:

1
2
3
4
5
build/stm32f103-basic.elf
build/stm32f103-basic.hex
build/stm32f103-basic.bin
build/firmware.map
build/compile_commands.json

  其中 .elf 是带符号的完整映像,.hex 和 .bin 由它转换而来;.map 用于查看符号和内存布局,compile_commands.json 可以给 clangd 提供代码补全。

8.2 检查固件

  用 file 和 size 确认文件类型和占用:

1
2
file build/stm32f103-basic.elf
arm-none-eabi-size build/stm32f103-basic.elf

  预期 file 输出类似:

1
build/stm32f103-basic.elf: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, not stripped

  size 会输出 text、data、bss 三列。text + data 是占用 Flash 的大小,data + bss 是占用 SRAM 的大小。第一次点灯工程通常只有几 KB,离 512 KB Flash 和 64 KB RAM 还很远。

8.3 常见编译问题

现象 原因 处理
arm-none-eabi-gcc: command not found 工具链未安装或不在 PATH 重新安装并检查 which arm-none-eabi-gcc
stm32f1xx_hal_conf.h: No such file core/inc 没加入头文件路径 检查 core/CMakeLists.txt
stm32f1xx.h: No such file CMSIS 设备头文件路径不对 检查 drv/CMakeLists.txt
undefined reference to _sbrk 使用了 malloc/printf,但 syscalls.c 不含 _sbrk 换用本文的 CubeF1 syscalls.c,或补一个 sysmem.c
undefined reference to _write 缺少 syscalls.c 把 syscalls.c 加入 core/src/
region RAM overflowed 链接脚本的 RAM 大小或栈/堆设置不对 检查链接脚本和 _Min_Heap_Size、_Min_Stack_Size
启动文件报语法错误 选错了启动文件或工具链 F103ZE 使用 startup_stm32f103xe.s 的 GCC 版本
cannot find -lnosys 工具链不完整 安装 libnewlib-arm-none-eabi 或检查工具链

  如果修改了 CMakeLists 或新增了源文件,重新执行一次 cmake -S . -B build,让 file(GLOB) 重新收集文件。

9 常见问题

9.1 编译和链接

现象 原因 处理
undefined reference to HAL_GPIO_Init HAL_GPIO_MODULE_ENABLED 没打开,或 HAL Src 没加入编译 检查 stm32f1xx_hal_conf.h 和 drv/CMakeLists.txt
undefined reference to HAL_InitTick HAL_CORTEX_MODULE_ENABLED 没打开 打开 CORTEX 模块
FLASH_LATENCY_2 未定义 HAL_FLASH_MODULE_ENABLED 没打开 打开 FLASH 模块
multiple definition of SysTick_Handler 启动文件、stm32f1xx_it.c 或其它文件重复定义 只保留 stm32f1xx_it.c 中的实现
multiple definition of _write syscalls.c 和应用层自定义的 _write 重复 只保留一个;要重定向输出时直接改 syscalls.c 里的 _write
undefined reference to SystemInit system_stm32f1xx.c 没加入编译 检查 core/src 和 core/CMakeLists.txt
undefined reference to g_pfnVectors 启动文件没加入编译 检查 strtup/CMakeLists.txt
.elf 生成但没有 .hex/.bin objcopy 路径不对 检查工具链文件里的 CMAKE_OBJCOPY

9.2 运行现象

现象 排查方向
两个 LED 都不亮 检查 PB5/PE5 是否配置为输出、GPIOB/GPIOE 时钟是否使能、程序是否卡在 Error_Handler()
LED 常亮或常灭 LED 是低电平点亮,确认初始化时写的是 GPIO_PIN_SET,循环里用的是 TogglePin
LED 闪烁频率明显不对 检查 HSE 是否为 8 MHz、PLL 是否为 x9、HSE_VALUE 是否为 8000000、Flash 等待周期是否为 2
程序一上电就 HardFault 检查启动文件、链接脚本、时钟配置、栈大小和中断向量表
HAL_Delay() 卡死 SysTick_Handler() 没有调用 HAL_IncTick(),或中断被关闭

10 小结

  本文从官方 STM32CubeF1 固件包出发,在 Linux 下搭建了一个最小 STM32F103ZET6 工程:

  1. 安装 arm-none-eabi-gcc 和 CMake。
  2. 从 CubeF1 复制 CMSIS、HAL、启动文件、系统文件、中断模板和 HAL 配置。
  3. 按 core/drv/strtup/lnk/cmake 分层组织工程。
  4. 编写 CMake 工具链和构建脚本,用 GCC 编译生成 .elf/.hex/.bin。
  5. 配置 HSE 8 MHz + PLL x9 = 72 MHz,初始化 PB5/PE5,点亮板载 LED。

  本文只覆盖编译和链接,固件的下载和调试可以参考《OpenOCD 基础篇(二)——烧录和调试》。这条编译链路打通后,后面的 UART、定时器、中断、DMA、FreeRTOS 都可以在这个骨架上逐步添加。