Chapter 01 · ARM 嵌入式 C 开发

嵌入式 C 入门

从冯诺依曼架构到 ARM Cortex-M,理解嵌入式系统的本质;搭建 STM32 开发环境,点亮第一颗 LED。

课程进度10%

关键概念

嵌入式系统 (Embedded System)
嵌入在更大产品内部、执行特定功能的计算机系统。与通用计算机不同,嵌入式系统往往资源受限(RAM 以 KB 计)、实时性要求高、需要直接控制硬件。STM32 就是一颗典型的嵌入式微控制器。
MCU(微控制器)vs MPU(微处理器)vs FPGA
MCU 把 CPU + RAM + Flash + 外设全部集成在一颗芯片上,便宜省电,适合控制类任务(STM32、Arduino)。MPU 只有 CPU 核,需要外接 DDR/Flash,运算能力强,跑 Linux(树莓派)。FPGA 是可编程逻辑器件,用硬件描述语言配置电路,极高并行度。
冯诺依曼架构 vs 哈佛架构
冯诺依曼:程序指令和数据共用同一条总线和存储空间,结构简单但存在"冯诺依曼瓶颈"。哈佛:指令总线和数据总线分离,可同时取指与读数据,性能更高。ARM Cortex-M 采用改进哈佛架构——内部是哈佛,对外统一地址空间。
ARM Cortex-M 系列
ARM 设计的嵌入式处理器内核家族。M0/M0+ 极低功耗入门级;M3 有除法指令、Thumb-2 指令集;M4 增加 DSP 指令和单精度 FPU(STM32F4 用此核);M7 双精度 FPU,性能更强(STM32H7);M33/M55 加入 TrustZone 安全扩展。
存储器映射 (Memory Map)
Cortex-M 使用统一的 32 位地址空间(4 GB),将 Flash、RAM、外设寄存器都映射到不同地址段。STM32F4 的 Flash 从 0x0800_0000 开始,RAM 在 0x2000_0000,外设寄存器在 0x4000_0000。访问外设就是读写这些特定地址。
寄存器 (Register)
固定在芯片内部、有特定地址的 32 位存储单元。每个 bit 或 bit 段控制硬件的某个具体行为——例如 GPIOA 的 ODR 寄存器第 5 位置 1,PA5 引脚就输出高电平。寄存器是软件与硬件之间最直接的接口。
HAL 库(硬件抽象层)
ST 官方提供的面向对象风格 C 库,封装了所有外设的寄存器操作,提供 HAL_GPIO_WritePin()HAL_UART_Transmit() 等函数。优点是代码可移植性强,缺点是有额外开销、调试难度略高。学习时建议先理解底层寄存器,再用 HAL 提高效率。
STM32CubeMX
ST 提供的图形化初始化代码生成工具。通过图形界面配置时钟树、外设引脚、中断优先级,自动生成初始化 C 代码。与 STM32CubeIDE 或 Keil 集成,大大减少手写配置代码的工作量。

嵌入式系统架构图解

理解 STM32F4 的内部结构,是正确操作外设的基础。下图展示了 Cortex-M4 核心与片上外设之间的连接关系:

STM32F407 内部架构(简化) ═══════════════════════════════════════════════════════════════ ┌─────────────────────────────────────────────────────┐ │ ARM Cortex-M4 Core │ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │ │ CPU │ │ FPU(SP) │ │ NVIC(中断控制) │ │ │ │(168MHz) │ │ │ │ 240个中断向量 │ │ │ └────┬─────┘ └──────────┘ └──────────────────┘ │ └───────┼─────────────────────────────────────────────┘ │ ICode/DCode Bus System Bus ════════╪═══════════════════════════════╪══════════ │ │ ┌───────▼──────┐ ┌─────────▼──────────┐ │ Flash │ │ SRAM │ │ 1MB │ │ 192KB │ │ 0x0800_0000 │ │ 0x2000_0000 │ └──────────────┘ └────────────────────┘ │ AHB Bus (最高速总线) ═══════════════════════════════════════════════ │ │ │ ┌───────▼───┐ ┌────────▼────┐ ┌──────▼──────┐ │ DMA1/2 │ │ GPIO A-K │ │ USB OTG │ └───────────┘ └─────────────┘ └─────────────┘ │ APB1/APB2 桥(降速总线) ═══════════════════════════════════════════════ APB1(42MHz): UART2-5、I2C1-3、SPI2-3、TIM2-14 APB2(84MHz): UART1/6、SPI1、TIM1/8、ADC1-3
为什么外设有"总线速度"限制?

Cortex-M4 核心可以跑到 168 MHz,但片上外设(UART、I2C 等)的内部逻辑无法工作在如此高的频率下。因此 STM32 设计了 AHB、APB1、APB2 多级总线,每级通过分频器降速。操作外设前必须先开启对应总线的时钟,否则寄存器读写无效——这是初学者最常见的 Bug 之一。

冯诺依曼 vs 哈佛架构对比

冯诺依曼架构

  • 指令和数据共享同一总线
  • 同一时刻只能取指读数据
  • 结构简单,成本低
  • 存在"冯诺依曼瓶颈"
  • 代表:x86 PC、早期单片机

哈佛架构

  • 指令总线和数据总线分离
  • 可同时取指读写数据
  • 指令存储只读,更安全
  • 性能更高,流水线更深
  • 代表:ARM Cortex-M(改进型)

ARM Cortex-M 系列对比

内核位宽FPUDSP代表芯片应用场景
Cortex-M032-bitSTM32F0超低成本控制
Cortex-M332-bit有限STM32F1/F2通用控制
Cortex-M432-bit单精度完整STM32F4电机控制、音频
Cortex-M732-bit双精度完整STM32H7高性能图像处理
Cortex-M3332-bit可选完整STM32U5IoT 安全

开发环境搭建

STM32 开发有两大主流工具链,选择其一即可:

方案一:STM32CubeIDE(推荐新手)

ST 官方免费 IDE,基于 Eclipse,内置 GCC 编译器、调试器,集成 STM32CubeMX 代码生成。一站式解决方案,无需额外配置。

# 下载地址(官网免费)
https://www.st.com/en/development-tools/stm32cubeide.html

# 安装后首次使用:
# 1. File → New → STM32 Project
# 2. 搜索 STM32F407VG(或你的芯片型号)
# 3. 选择 Targeted Language: C
# 4. STM32CubeMX 界面自动打开,配置引脚
# 5. Project → Generate Code
# 6. 编写用户代码,Build → Debug

方案二:Keil MDK-ARM(行业标准)

Arm 官方 IDE,行业使用最广泛,ARM 编译器优化效果更好,有完善的代码分析工具。社区版免费,专业版需购买授权。

# 下载 Keil MDK
https://www.keil.arm.com/mdk-community/

# 安装 STM32F4 支持包
# 打开 Keil → Pack Installer
# 搜索 STM32F4xx → Install

# 烧录工具:ST-Link(官方调试器)
# 连接:SWDIO、SWCLK、GND、3.3V(4根线)

STM32F407 存储器映射(关键地址)

Cortex-M4 统一地址空间(32位 = 4GB) 地址范围 区域 说明 ───────────────────────────────────────────────────── 0xFFFF_FFFF ┐ │ 厂商外设区 保留/芯片私有 0xE000_00000xE000_E000 ┐ │ 内核私有 NVIC、SysTick、SCB 等 0xE000_0000 ┘ (调试器访问) 0x5000_0000 ┐ │ AHB3 FSMC(外部存储器控制) 0x4000_00000x4002_0000 ┐ │ AHB1 GPIO A-K、DMA1/2、RCC时钟控制 0x4001_00000x4001_0000 ┐ │ APB2 UART1/6、SPI1、TIM1/8、ADC 0x4000_00000x4000_0000 ┐ │ APB1 UART2-5、I2C、SPI2/3、基本定时器 0x4000_00000x2001_C000 ┐ │ CCM RAM 64KB 核心耦合内存(仅CPU可访问) 0x1000_00000x2002_0000 ┐ │ SRAM 128KB 主内存 (0x2000_0000 起) 0x2000_00000x0807_FFFF ┐ │ Flash 1MB 程序存储(从此处启动) 0x0800_0000

第一个程序:点亮 LED

STM32F407 Discovery 开发板的 LD4(绿灯)连接在 PD12 引脚上。我们用两种方式实现点亮:直接操作寄存器,和使用 HAL 库。

寄存器方式(最底层)

要点亮 PD12,需要:① 开启 GPIOD 时钟;② 设置 PD12 为推挽输出;③ 向 ODR 写 1。

/* main.c — 寄存器直接操作,无需 HAL 库 */
#include "stm32f4xx.h"  /* CMSIS 头文件,定义所有寄存器地址 */

int main(void) {

  /* ① 开启 GPIOD 的 AHB1 总线时钟
     RCC->AHB1ENR 地址 0x4002_3830
     bit3 = GPIODEN,置1表示使能 */
  RCC->AHB1ENR |= (1 << 3);   /* 等价于 |= RCC_AHB1ENR_GPIODEN */

  /* ② 设置 PD12 为推挽输出模式
     MODER 每个引脚占 2 位,PD12 对应 bit25:24
     00=输入, 01=通用输出, 10=复用, 11=模拟
     先清零 bit25:24,再置 01 */
  GPIOD->MODER &= ~(0x3 << (12 * 2));  /* 清零 */
  GPIOD->MODER |=  (0x1 << (12 * 2));  /* 置输出模式 */

  /* ③ 设置为推挽输出(OTYPER bit12 = 0)
     0=推挽(Push-Pull), 1=开漏(Open-Drain) */
  GPIOD->OTYPER &= ~(1 << 12);   /* 推挽输出 */

  /* ④ 设置速度(OSPEEDR bit25:24 = 00,低速) */
  GPIOD->OSPEEDR &= ~(0x3 << (12 * 2));

  /* ⑤ 点亮 LED:向 ODR(输出数据寄存器)的 bit12 置 1 */
  GPIOD->ODR |= (1 << 12);

  while (1) { }  /* 保持运行 */
}

GPIOD_MODER 寄存器(地址 0x40020C00)— 模式配置

位[31:26]位[25:24]位[23:22]...位[1:0]
MODER15 MODER12 ← 01 MODER11 ... MODER0
每引脚 2 bit:00=输入 01=通用输出 10=复用功能 11=模拟模式

HAL 库方式(推荐日常开发)

HAL 库将寄存器细节封装起来,代码更易读,但内部做了同样的事情。

/* STM32CubeMX 生成的 main.c 框架 */
#include "main.h"
#include "stm32f4xx_hal.h"

int main(void) {
  HAL_Init();              /* 初始化 HAL 库、SysTick 1ms 中断 */
  SystemClock_Config();    /* 配置时钟树(CubeMX 自动生成)*/
  MX_GPIO_Init();          /* 初始化 GPIO(CubeMX 自动生成)*/

  while (1) {
    /* 点亮 PD12(LED4)*/
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_SET);
    HAL_Delay(500);    /* 延时 500ms */

    /* 熄灭 */
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_RESET);
    HAL_Delay(500);

    /* 或者直接翻转(Toggle)*/
    /* HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12); */
  }
}

/* MX_GPIO_Init 函数内容(CubeMX 自动生成)*/
static void MX_GPIO_Init(void) {
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* 开启 GPIOD 时钟 */
  __HAL_RCC_GPIOD_CLK_ENABLE();

  /* 初始状态:引脚低电平(LED 灭)*/
  HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_RESET);

  /* 配置 PD12 为推挽输出,无上下拉,低速 */
  GPIO_InitStruct.Pin   = GPIO_PIN_12;
  GPIO_InitStruct.Mode  = GPIO_MODE_OUTPUT_PP;  /* Push-Pull */
  GPIO_InitStruct.Pull  = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
}
硬件连接原理

LED 阳极(长脚)→ 限流电阻(330Ω)→ MCU 引脚。当 MCU 引脚输出高电平(3.3V),电流从 MCU 流向 GND,LED 点亮。STM32F407 Discovery 板上的 LED 是共阴极接法,输出高电平亮、低电平灭。如果你的开发板 LED 共阳极,逻辑相反。

新手常见错误

忘记开启 GPIO 时钟!在 STM32 中,所有外设默认关闭时钟(节省功耗)。操作任何外设前必须先执行 __HAL_RCC_GPIOx_CLK_ENABLE() 或直接操作 RCC 的 ENR 寄存器。若忘记开时钟,对寄存器的任何写入都会被忽略,引脚没有任何反应。

编译与烧录流程

从源码到运行:完整工具链 ┌──────────────┐ │ main.c │ 用户编写的 C 源码 │ stm32f4xx.h │ CMSIS 头文件(寄存器定义) └──────┬───────┘ │ GCC-ARM / ARMCC 编译 ┌──────▼───────┐ │ main.o │ 目标文件(机器码) │ startup.o │ 启动文件(汇编写的复位处理) └──────┬───────┘ │ 链接器(ld)+ 链接脚本(.ld) ┌──────▼───────┐ │ firmware.elf│ 带调试信息的可执行文件 │ firmware.bin│ 纯二进制,用于烧录 └──────┬───────┘ │ ST-Link / J-Link 下载 ┌──────▼───────┐ │ STM32 Flash │ 0x0800_0000 起写入 firmware.bin └──────────────┘ │ 复位 → 执行 CPU 从 0x0800_0000 取指,跳转 Reset_Handler → main()