这些实例将围绕 ARM Cortex-M 系列(如 STM32, NXP LPC, TI Tiva 等)展开,因为这是目前最主流的 32 位 ARM 微控制器架构,实例会使用 Keil MDK 作为开发环境,并基于 STM32F103C8T6 这款经典的“蓝丸”开发板进行讲解。

(图片来源网络,侵删)
实例 1:最基础的点灯程序 - GPIO 输出
这是嵌入式世界的 "Hello, World!",目标是控制一个 LED 灯的亮灭。
硬件准备
- 开发板: STM32F103C8T6 (通常板载一个 LED 连接到 PC13 引脚)
- 工具: Keil MDK (或其他 IDE 如 STM32CubeIDE)
理解 STM32 的 GPIO
与简单的 51 单片机不同,STM32 的 GPIO 功能非常强大,需要通过配置多个寄存器来设置其工作模式、速度、上下拉等。
对于 STM32F103,我们需要操作以下几个关键的寄存器来配置 PC13 引脚:
- RCC_APB2ENR (时钟使能寄存器): 必须先给 GPIOC 端口使能时钟,否则无法操作其寄存器。
- GPIOC_CRH (端口配置高寄存器): PC13 是一个高引脚号,所以配置在 CRH 寄存器中,我们需要将其设置为推挽输出模式。
- GPIOC_ODR (端口输出数据寄存器): 写 1 使引脚输出高电平(熄灭 LED),写 0 使引脚输出低电平(点亮 LED,假设是低电平点亮)。
- GPIOC_BSRR (端口位设置/复位寄存器): 这是一个更方便的寄存器,可以原子地设置或复位某个引脚,避免在多线程中出现问题。
C 语言代码实现
#include "stm32f10x.h" // 包含 STM32F10x 的头文件
// 简单延时函数
void simple_delay(volatile uint32_t count) {
while(count--);
}
int main(void) {
// 1. 使能 GPIOC 的时钟
// RCC_APB2ENR 寄存器的第 4 位 (IOPCEN) 控制 GPIOC 的时钟
RCC->APB2ENR |= (1 << 4); // 使用位带操作或直接赋值,这里用按位或确保不修改其他位
// 2. 配置 PC13 为推挽输出模式
// GPIOC_CRH 的第 20-21 位 (CNF13) 设置为 00 (推挽)
// GPIOC_CRH 的第 16-19 位 (MODE13) 设置为 01 (输出模式,最大速度 10MHz)
// 原始值: 0x44444444
// 对 PC13 的配置: 位 [21:20] = 00, 位 [19:16] = 0001
// CRH 寄存器需要设置为: 0x44444444 | (0x0 << 20) | (0x1 << 16)
// 简化操作:先清零,再设置
GPIOC->CRH &= ~(0x0F << 16); // 清除 PC13 的原有配置 (4位)
GPIOC->CRH |= (0x01 << 16); // 设置为推挽输出,最大速度 10MHz
// 3. 主循环
while (1) {
// 点亮 LED (PC13 输出低电平)
// 方法一:直接操作 ODR 寄存器
// GPIOC->ODR &= ~(1 << 13); // 将第 13 位置 0
// 方法二:使用 BSRR 寄存器(推荐,更安全)
GPIOC->BSRR = (1 << 13); // BSRR 的低 16 位用于复位,写 1 复位对应引脚
simple_delay(500000); // 延时
// 熄灭 LED (PC13 输出高电平)
// GPIOC->ODR |= (1 << 13); // 将第 13 位置 1
// 使用 BSRR 寄存器
GPIOC->BSRR = (1 << (13 + 16)); // BSRR 的高 16 位用于置位,写 1 置位对应引脚
simple_delay(500000); // 延时
}
}
代码解析
#include "stm32f10x.h": 包含了 STM32F10x 所有寄存器定义的内存映射文件,这是直接操作寄存器的基础。RCC->APB2ENR |= (1 << 4);: 这是 至关重要 的一步,ARM Cortex-M 采用的是总线架构,外设(如 GPIO)必须先被“时钟使能”,才能被 CPU 访问和控制。->结构体指针访问方式是操作 SFR(特殊功能寄存器)的标准方法。GPIOC->CRH &= ...; GPIOC->CRH |= ...;: 这里对 CRH 寄存器进行了“读-改-写”操作,精确地配置了 PC13 的引脚功能。GPIOC->BSRR = ...;: 这是 Cortex-M GPIO 的一个特色寄存器,它允许你原子地(在单个总线周期内)设置或复位引脚,避免了在复杂系统中因竞争条件导致的问题,低 16 位用于复位(输出低),高 16 位用于置位(输出高)。
实例 2:按键检测 - GPIO 输入
本实例将读取一个按键的状态,当按键按下时,点亮 LED。

(图片来源网络,侵删)
硬件准备
- 开发板: STM32F103C8T6
- 按键: 一个按键,一端接 GND,另一端接一个 GPIO 引脚(PA0)。
理解 GPIO 输入
为了读取按键输入,我们需要:
- 时钟使能: 同样需要使能 GPIOA 的时钟。
- 模式配置: 将 PA0 配置为输入模式。
- 浮空输入: 按键不接上下拉电阻,状态不确定。
- 上拉输入: 默认高电平,按键按下后变为低电平。
- 下拉输入: 默认低电平,按键按下后变为高电平。
- 模拟输入: 用于 ADC。
- 读取状态: 通过读取
GPIOA->IDR(端口输入数据寄存器) 来获取引脚状态。
C 语言代码实现
#include "stm32f10x.h"
void simple_delay(volatile uint32_t count) {
while(count--);
}
int main(void) {
// 1. 使能 GPIOA 和 GPIOC 的时钟
RCC->APB2ENR |= (1 << 2); // 使能 GPIOA
RCC->APB2ENR |= (1 << 4); // 使能 GPIOC
// 2. 配置 PA0 为上拉输入
// GPIOA_CRL 的位 [1:0] (MODE0) = 00 (输入)
// GPIOA_CRL 的位 [3:2] (CNF0) = 10 (上拉/下拉)
// 同时设置 ODR0 位为 1,选择上拉
GPIOA->CRL &= ~(0x0F << 0); // 清除 PA0 的原有配置
GPIOA->CRL |= (0x08 << 0); // 设置为上拉/下拉输入模式
GPIOA->ODR |= (1 << 0); // 选择上拉
// 3. 配置 PC13 为推挽输出 (同实例1)
GPIOC->CRH &= ~(0x0F << 16);
GPIOC->CRH |= (0x01 << 16);
// 4. 主循环
while (1) {
// 读取 PA0 的状态
// IDR 寄存器,位 0 对应 PA0
if ((GPIOA->IDR & (1 << 0)) == 0) {
// 如果为 0,说明按键按下(低电平)
// 点亮 LED
GPIOC->BSRR = (1 << 13); // 复位 PC13
} else {
// 否则,按键未按下(高电平)
// 熄灭 LED
GPIOC->BSRR = (1 << (13 + 16)); // 置位 PC13
}
// 简单的消抖延时
simple_delay(10000);
}
}
代码解析
GPIOA->CRL |= (0x08 << 0);: 将 PA0 配置为上拉/下拉输入模式。GPIOA->ODR |= (1 << 0);: 在配置上拉/下拉模式后,必须通过ODR寄存器来选择是上拉还是下拉。ODR对应位写 1 为上拉,写 0 为下拉。if ((GPIOA->IDR & (1 << 0)) == 0): 通过按位与操作&来检测 IDR 的第 0 位是否为 0。== 0是为了确保整个表达式的结果是 0(假)或非 0(真)。
实例 3:使用标准外设库 - 控制串口
直接操作寄存器虽然高效,但代码复杂且不易移植。STM32 标准外设库 提供了封装好的函数,让代码更清晰、更易于维护。
本实例将通过串口发送 "Hello, ARM!\r\n" 字符串。
硬件准备
- 开发板: STM32F103C8T6
- USB 转串口模块: 连接 PA9 (TX) 和 PA10 (RX) 到电脑。
库文件准备
在 Keil MDK 工程中,你需要添加以下库文件和文件夹:

(图片来源网络,侵删)
STM32F10x_StdPeriph_Driver文件夹CMSIS文件夹- 并在
Options for Target->C/C++->Include Paths中添加这些路径。
C 语言代码实现
#include "stm32f10x.h"
#include "stdio.h" // 为了使用 printf
// 重定向 printf 到串口
int fputc(int ch, FILE *f) {
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // 等待发送寄存器为空
USART_SendData(USART1, (uint8_t)ch);
return ch;
}
void USART1_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 2. 配置 USART1 TX (PA9) 和 RX (PA10) 引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用功能推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. USART1 参数配置
USART_InitStructure.USART_BaudRate = 115200; // 波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 8位数据位
USART_InitStructure.USART_StopBits = USART_StopBits_1; // 1位停止位
USART_InitStructure.USART_Parity = USART_Parity_No; // 无校验
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件流控
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 收发模式
USART_Init(USART1, &USART_InitStructure);
// 4. 使能 USART1
USART_Cmd(USART1, ENABLE);
}
int main(void) {
USART1_Init();
printf("Hello, ARM!\r\n");
printf("This is a message from STM32 using Standard Peripheral Library.\r\n");
while (1) {
// 主循环可以执行其他任务
}
}
代码解析
#include "stm32f10x.h": 包含了所有库函数的声明。GPIO_InitTypeDef和USART_InitTypeDef: 这是 SPL 的核心,使用结构体来封装寄存器配置,你只需要填充结构体成员,然后调用初始化函数,库会自动完成寄存器的设置。RCC_APB2PeriphClockCmd(...): 一个函数调用就完成了多个外设的时钟使能,比手动操作寄存器方便得多。USART_Init(): 填充USART_InitTypeDef结构体后,调用此函数完成串口的所有底层配置。printf重定向: 通过重写fputc函数,我们可以使用标准 C 库的printf函数,让串口打印调试信息变得非常简单。USART_GetFlagStatus和USART_SendData都是库提供的函数。
总结与进阶
| 实例 | 核心概念 | 关键技术点 | 适用场景 |
|---|---|---|---|
| 点灯 | GPIO 输出 | 寄存器操作、时钟使能、推挽输出 | 理解底层硬件,最简单的控制 |
| 按键 | GPIO 输入 | 寄存器操作、上拉/下拉输入、消抖 | 读取外部信号,人机交互基础 |
| 串口 | 通信、SPL | 标准外设库、printf重定向、中断(可扩展) | 调试、数据传输、与PC通信 |
进阶方向:
- 中断编程: 学习使用 NVIC (嵌套向量中断控制器) 来处理按键中断、串口接收中断等,提高系统的实时性和效率。
- 定时器: 学习使用 SysTick (系统滴答定时器) 或通用定时器 来实现精确的延时、PWM 输出(控制舵机、LED 亮度)、输入捕获(测量频率)等。
- DMA (直接内存访问): 学习使用 DMA 来搬运数据,例如让 DMA 将串口接收到的数据直接存入内存,解放 CPU。
- RTOS (实时操作系统): 学习使用 FreeRTOS、RT-Thread 等轻量级操作系统,管理多个任务,实现更复杂的并发逻辑。
这些实例为你打下了坚实的基础,从直接操作寄存器理解硬件本质,到使用标准库提高开发效率,是 ARM C 语言学习的必经之路,希望这些实例能帮助你更好地入门!
