嵌入式 C 入门
从冯诺依曼架构到 ARM Cortex-M,理解嵌入式系统的本质;搭建 STM32 开发环境,点亮第一颗 LED。
从冯诺依曼架构到 ARM Cortex-M,理解嵌入式系统的本质;搭建 STM32 开发环境,点亮第一颗 LED。
0x0800_0000 开始,RAM 在 0x2000_0000,外设寄存器在 0x4000_0000。访问外设就是读写这些特定地址。HAL_GPIO_WritePin()、HAL_UART_Transmit() 等函数。优点是代码可移植性强,缺点是有额外开销、调试难度略高。学习时建议先理解底层寄存器,再用 HAL 提高效率。理解 STM32F4 的内部结构,是正确操作外设的基础。下图展示了 Cortex-M4 核心与片上外设之间的连接关系:
Cortex-M4 核心可以跑到 168 MHz,但片上外设(UART、I2C 等)的内部逻辑无法工作在如此高的频率下。因此 STM32 设计了 AHB、APB1、APB2 多级总线,每级通过分频器降速。操作外设前必须先开启对应总线的时钟,否则寄存器读写无效——这是初学者最常见的 Bug 之一。
| 内核 | 位宽 | FPU | DSP | 代表芯片 | 应用场景 |
|---|---|---|---|---|---|
| Cortex-M0 | 32-bit | 无 | 无 | STM32F0 | 超低成本控制 |
| Cortex-M3 | 32-bit | 无 | 有限 | STM32F1/F2 | 通用控制 |
| Cortex-M4 | 32-bit | 单精度 | 完整 | STM32F4 | 电机控制、音频 |
| Cortex-M7 | 32-bit | 双精度 | 完整 | STM32H7 | 高性能图像处理 |
| Cortex-M33 | 32-bit | 可选 | 完整 | STM32U5 | IoT 安全 |
STM32 开发有两大主流工具链,选择其一即可:
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
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 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) { } /* 保持运行 */ }
| 位[31:26] | 位[25:24] | 位[23:22] | ... | 位[1:0] |
|---|---|---|---|---|
| MODER15 | MODER12 ← 01 | MODER11 | ... | MODER0 |
| 每引脚 2 bit:00=输入 01=通用输出 10=复用功能 11=模拟模式 | ||||
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 寄存器。若忘记开时钟,对寄存器的任何写入都会被忽略,引脚没有任何反应。