news 2026/1/21 3:01:38

从零实现:基于ARM Compiler 5.06的LED闪烁程序

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零实现:基于ARM Compiler 5.06的LED闪烁程序

从零开始:用ARM Compiler 5.06点亮第一颗LED

你有没有过这样的经历?手握一块STM32开发板,装好了Keil,建了工程,写完代码一点编译——程序下载进去,LED却纹丝不动。查了一遍又一遍,代码逻辑没问题,引脚也没接错,可就是不亮。

别急,这几乎是每个嵌入式开发者都会踩的坑。而解决这些问题的过程,恰恰是理解底层系统如何真正“跑起来”的关键。

今天,我们就以最经典的LED闪烁程序为切入点,带你完整走一遍基于ARM Compiler 5.06的裸机开发全流程。不依赖HAL库、不调用复杂API,只用最原始的方式操作寄存器,让你看清每一步背后发生了什么。


为什么选 ARM Compiler 5.06?

尽管 Arm 已经推出了基于 LLVM 架构的新一代ARM Compiler 6(armclang),但在很多企业项目和教学场景中,ARM Compiler 5.06(armcc)依然是主力工具链。

原因很简单:

  • 它与Keil MDK-ARM 深度集成,界面友好,调试流畅;
  • 对旧项目的兼容性极佳,尤其是那些运行多年的工业控制器;
  • 编译行为稳定,优化策略成熟,在特定性能点上仍有优势;
  • 大量经典教材、课程、参考设计都基于它构建。

更重要的是,AC5 的编译流程更直观地暴露了底层机制—— 启动文件怎么加载?内存怎么分布?数据段如何初始化?这些在 AC6 或 GCC 中可能被自动隐藏的细节,在 AC5 下必须手动配置清楚,反而更适合学习。

所以,哪怕你是为未来准备,掌握 AC5 依然是打牢基础的必经之路。


硬件平台与目标功能

我们选用最常见的STM32F103C8T6芯片(即“蓝丸”开发板的核心MCU),实现以下功能:

控制连接在PA5 引脚上的LED,以约1秒间隔持续闪烁。

这是一个最小可行系统(Minimal Working System),但它涵盖了嵌入式开发的所有核心环节:

  • 寄存器级外设控制
  • 时钟使能管理
  • 堆栈与启动流程
  • 内存布局定义
  • 编译链接全过程

接下来,我们将从零开始,一步步构建这个工程。


第一步:编写主程序 —— 直接操作GPIO

#include "stm32f10x.h" #define LED_PIN 5 #define RCC_APB2ENR_IOPA_EN (1 << 2) #define GPIOA_MODER_OUTPUT (1 << (LED_PIN * 2)) void delay(volatile uint32_t count) { while (count--) { __nop(); } } int main(void) { // 1. 使能GPIOA时钟 RCC->APB2ENR |= RCC_APB2ENR_IOPA_EN; // 2. 配置PA5为通用推挽输出模式(最大10MHz) GPIOA->CRL &= ~(0xF << (4 * LED_PIN)); // 清除原有配置 GPIOA->CRL |= (1 << (4 * LED_PIN)); // MODE=01, CNF=00 → 推挽输出 // 3. 主循环:翻转LED状态 while (1) { GPIOA->BSRR = (1 << LED_PIN); // 置位(点亮) delay(1000000); GPIOA->BRR = (1 << LED_PIN); // 清零(熄灭) delay(1000000); } }

关键点解析

✅ 为什么要先开时钟?

STM32 的所有外设默认都是断电状态。即使你写了GPIOA->CRL,如果 RCC 没有开启 GPIOA 的时钟,这些写操作会被忽略!

这就是为什么第一行必须是:

RCC->APB2ENR |= RCC_APB2ENR_IOPA_EN;

否则,后续任何对 GPIOA 寄存器的操作都将无效。

✅ 为什么用 CRL 而不是 MODER?

注意!这里是STM32F1系列,它的 GPIO 配置寄存器和 F4/F7/H7 不同。F1 使用的是CRLCRH来分别配置低8位和高8位引脚,而不是统一的MODER

所以 PA5 属于低8位,我们要改的是GPIOA->CRL

✅ BSRR 与 BRR:原子操作的秘密

直接对ODR进行读-改-写存在竞态风险。而 STM32 提供了两个专用寄存器:

  • BSRR:写1到某位,对应引脚输出高;
  • BRR:写1到某位,对应引脚输出低;

两者都是单向触发,无需读取当前状态,保证了操作的原子性。

比如:

GPIOA->BSRR = (1 << 5); // 只让第5位变高,其他不变 GPIOA->BRR = (1 << 5); // 只让第5位变低

GPIOA->ODR ^= (1<<5)更安全,尤其在中断环境中。

✅ volatile 关键字的重要性

延时函数中的参数用了volatile

void delay(volatile uint32_t count)

这是为了防止编译器将空循环整个优化掉。如果没有volatile,当开启-O2优化时,编译器会发现这个循环“什么都不做”,直接删掉,导致延时不生效。


第二步:不可或缺的拼图 —— 启动文件

你以为写了main()函数就能运行?错了。

MCU 上电后,第一条执行的指令并不是main,而是从复位向量开始的汇编代码 —— 即启动文件(startup file)

典型的启动文件名为:startup_stm32f103xb.s

它做了几件至关重要的事:

  1. 定义中断向量表
  2. 初始化栈指针(SP)
  3. 跳转到 Reset_Handler
  4. 调用 SystemInit()(可选)
  5. 最终跳转至 __main,进入 C 运行时环境

其中最关键的一环是:.data段复制 和.bss段清零

全局变量和静态变量需要放在 RAM 中,但 Flash 是只读的。所以链接器会把初始值存在 Flash 的.data段,然后在启动时由一段小代码将其拷贝到 RAM 中。未初始化的变量(如static int buf[100];)则属于.bss段,需清零。

如果缺少这段初始化代码,你的全局变量就会是随机值,程序行为不可预测。

而在 AC5 中,这个工作是由编译器提供的__main入口完成的。只要你在 scatter 文件中正确描述内存结构,armlink就会自动生成对应的初始化代码。


第三步:掌控内存布局 —— Scatter 加载文件详解

Scatter 文件(.sct)决定了程序各部分在芯片内存中的位置。对于 STM32F103CBT6(128KB Flash + 20KB RAM),典型的配置如下:

LR_IROM1 0x08000000 0x00020000 { ; Load Region: Flash, 128KB ER_IROM1 0x08000000 0x00020000 { ; Exec Region: Code *.o (RESET, +First) ; 向量表必须放最前面 *(InRoot$$Sections) .ANY (+RO) ; 所有只读段(代码、常量) } RW_IRAM1 0x20000000 0x00005000 { ; Read-Write Region: RAM, 20KB .ANY (+RW +ZI) ; 可读写段和未初始化段 } }

关键说明

部分作用
LR_IROM1加载域,表示程序烧录到 Flash 的哪个区域
ER_IROM1执行域,程序运行时代码所在地址
*.o (RESET, +First)确保包含 RESET 标签的目标文件(通常是启动文件)放在最前面,即复位向量位于 0x08000000
.ANY (+RO)收集所有只读内容(代码、字符串常量等)
.ANY (+RW +ZI)包含已初始化全局变量(.data)和未初始化变量(.bss)

⚠️ 如果你忘记把 RESET 段放在首位,或者 RAM 区域大小设置错误,轻则程序无法启动,重则 HardFault。


工程搭建实战:Keil µVision 中的关键配置

假设你在 Keil MDK 中新建一个工程,以下是必须检查的几个关键点:

1. 设置正确的设备型号

Project → Manage → Components, Environment, Books
→ Device:STM32F103C8T6

这一步会影响:
- 默认包含的启动文件
- 外设寄存器定义
- 内存布局建议

2. 添加必要的源文件

  • main.c
  • system_stm32f10x.c(提供 SystemInit 函数)
  • startup_stm32f103xb.s(Keil 通常自动添加)

3. 包含头文件路径

Options → C/C++ → Include Paths:

.\Inc .\CMSIS

确保能正确找到stm32f10x.hcore_cm3.h

4. 选择 ARM Compiler 5

Options → Target → Toolchain:
-Use default compiler version 5

如果你电脑上同时安装了 AC6,请务必确认这里没有误选。

5. 启用调试信息 & 关闭过度优化

Options → C/C++:
- ✔ Debug Information
- Optimization:-O0(调试阶段禁用优化)
- ✔ Browse Information(便于查看符号)

6. 使用自定义 Scatter 文件

Options → Linker:
- ✔ Use Memory Layout from Target Dialog
- 或者 ❌ Uncheck 上述选项 → 输入.sct文件路径


常见问题排查清单

现象可能原因解决方法
LED完全不亮未开启GPIO时钟检查RCC->APB2ENR是否置位
程序卡死或跳不到main启动文件未正确加载查看 map 文件,确认 Reset_Handler 是第一个入口
全局变量非零初始值.data 未复制确保 scatter 文件包含 RW 段,且 __main 被调用
编译报错 “undefined symbol”头文件路径缺失添加 include paths 并重新 build
延时不准确甚至消失循环被优化掉给变量加volatile,使用 -O0
下载失败Flash算法未匹配在 Flash → Configure Flash Tools 中选择对应算法

背后的工具链:armcc 如何一步步构建程序?

ARM Compiler 5.06 实际上是一套工具集合,它们协同完成整个构建过程:

工具作用
armcc.c文件编译成汇编代码
armasm汇编.s文件生成目标文件.o
armlink链接所有.o文件,依据.sct分配地址,生成.axf
fromelf.axf提取.bin.hex用于烧录

你可以通过 Keil 的 Build Output 窗口看到类似命令行:

armcc --cpu=Cortex-M3 -O0 -g ... main.c armasm --cpu=Cortex-M3 startup_stm32f103xb.s armlink --scatter project.sct main.o startup.o -o output.axf fromelf --bin -o output.bin output.axf

正是这些工具的精密协作,才让高级语言最终变成能在硬件上奔跑的机器码。


总结:不只是点亮LED

看似简单的 LED 闪烁程序,实则串联起了嵌入式开发的完整知识链条:

  • 硬件层:GPIO、时钟、电源
  • 软件层:寄存器操作、C语言编程
  • 系统层:启动流程、内存管理、链接脚本
  • 工具链:编译、链接、烧录、调试

当你真正搞懂为什么 LED 必须“先开时钟再配置”,为什么.bss要清零,为什么向量表要放最前面……你就已经跨过了入门门槛,进入了真正的嵌入式世界。


下一步可以探索的方向

掌握了这套基础框架后,你可以尝试:

  • 用定时器替代 delay() 实现精准延时
  • 添加按键中断检测
  • 移植 FreeRTOS 实现多任务调度
  • 切换到 ARM Compiler 6 对比差异
  • 使用 GCC for ARM Embedded 构建相同工程

你会发现,无论工具如何变化,底层原理始终相通。


如果你正在学习嵌入式开发,不妨亲手试一次:从头创建一个 Keil 工程,不用任何库,只靠 CMSIS 头文件和启动代码,写出属于你自己的第一个裸机程序。

当那颗小小的 LED 第一次按你的意志闪烁起来时,你会明白——这不是一个结束,而是一个开始。

💬 你在实现过程中遇到过哪些奇怪的问题?欢迎留言分享你的“踩坑”经历。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/1/20 16:59:00

神经网络(从感知机到神经网络)

从感知机到神经网络 神经网络和上一章介绍的感知机有很多共同点。这里&#xff0c;我们主要以两者 的差异为中心&#xff0c;来介绍神经网络的结构。 神经网络的例子 用图来表示神经网络的话&#xff0c;如图3-1 所示。我们把最左边的一列称为 输入层&#xff0c;最右边的一列称…

作者头像 李华
网站建设 2025/12/31 8:53:57

10分钟快速上手:用Docker搭建Obsidian知识管理环境终极指南

10分钟快速上手&#xff1a;用Docker搭建Obsidian知识管理环境终极指南 【免费下载链接】awesome-obsidian &#x1f576;️ Awesome stuff for Obsidian 项目地址: https://gitcode.com/gh_mirrors/aw/awesome-obsidian 想要快速搭建个人知识管理平台吗&#xff1f;Obs…

作者头像 李华
网站建设 2026/1/20 9:02:19

Pyenv与Miniconda对比:哪个更适合管理Python3.11和PyTorch?

Pyenv与Miniconda对比&#xff1a;哪个更适合管理Python3.11和PyTorch&#xff1f; 在深度学习项目日益复杂的今天&#xff0c;一个常见的场景是&#xff1a;你在本地用 Python 3.11 跑通了 PyTorch 模型&#xff0c;结果换到服务器上却因为 CUDA 版本不兼容、Python 编译选项…

作者头像 李华
网站建设 2026/1/9 14:44:13

SSH远程访问TensorFlow 2.9深度学习镜像的操作步骤

SSH远程访问TensorFlow 2.9深度学习镜像的操作实践 在AI研发日益工程化的今天&#xff0c;一个常见的痛点浮出水面&#xff1a;我们能在Jupyter Notebook里轻松跑通模型&#xff0c;却总在训练到第100个epoch时因为网络波动断开连接&#xff0c;任务戛然而止。更不用说团队协作…

作者头像 李华
网站建设 2026/1/2 19:01:35

SSH远程开发指南:连接云端TensorFlow深度学习环境

SSH远程开发指南&#xff1a;连接云端TensorFlow深度学习环境 在现代AI研发中&#xff0c;一个常见的场景是&#xff1a;你手头只有一台轻薄笔记本&#xff0c;却需要训练一个包含上亿参数的深度学习模型。本地算力捉襟见肘&#xff0c;而云服务器上的GPU资源空闲待命——如何…

作者头像 李华
网站建设 2025/12/31 8:51:45

学术自动化新纪元:AI论文评审工具的终极指南

学术自动化新纪元&#xff1a;AI论文评审工具的终极指南 【免费下载链接】paper-reviewer Generate a comprehensive review from an arXiv paper, then turn it into a blog post. This project powers the website below for the HuggingFaces Daily Papers (https://hugging…

作者头像 李华