目录
-
第一部分:核心概念
- 1 ARM 架构简介
- 2 ARM 汇编与 C 的关系
- 3 关键概念:处理器模式、工作模式、异常
- 4 关键概念:寄存器
- 5 关键概念:内存映射
-
第二部分:ARM C 语言编程核心
- 1
volatile关键字:嵌入式编程的灵魂 - 2
static关键字:防止重入和作用域控制 - 3 内联汇编:直接与硬件对话
- 4 内存对齐
- 5 结构体与位域
- 1
-
第三部分:实践与工具链
- 1 开发环境搭建
- 2 启动代码
- 3 编写第一个 ARM C 程序:点灯
- 4 中断服务程序 的编写
- 5 与外设通信:GPIO、UART 示例
-
第四部分:进阶主题
- 1 C 语言与 MMU/MPU (内存管理单元/保护单元)
- 2 RTOS (实时操作系统) 下的 ARM C 编程
- 3 性能优化技巧
第一部分:核心概念
1 ARM 架构简介
- 什么是 ARM? ARM (Advanced RISC Machines) 是一种精简指令集计算机 架构,它的特点是指令简单、执行速度快、功耗低。
- RISC vs CISC:
- CISC (复杂指令集):如 x86,指令数量多,功能强大,但执行周期长,一条指令可能完成多个操作。
- RISC (精简指令集):如 ARM,指令数量少,格式统一,大多在一个时钟周期内完成,复杂操作通过多条简单指令组合实现。
- 为什么 ARM 主导嵌入式领域? 因为它的高能效比,非常适合对功耗敏感的设备,如手机、平板、物联网设备等。
2 ARM 汇编与 C 的关系
在 ARM 平台上,C 语言是应用层开发的主力,但底层离不开汇编。
- C 编译器的作用:将 C 代码翻译成 ARM 汇编指令,再汇编成机器码。
- 汇编的作用:
- 启动代码:配置系统时钟、初始化内存、设置堆栈指针,最后跳转到 C 语言的
main函数,这是芯片上电后最先执行的代码。 - 中断处理:当硬件中断发生时,CPU 会跳转到一段预设的汇编代码,保存现场,再调用 C 语言编写的 ISR。
- 访问特殊功能寄存器:直接读写控制硬件的寄存器。
- 优化关键代码:对性能要求极高的代码段,可以用内联汇编或纯汇编实现。
- 启动代码:配置系统时钟、初始化内存、设置堆栈指针,最后跳转到 C 语言的
3 关键概念:处理器模式、工作模式、异常
ARM 处理器有多种工作模式,这决定了它能执行的操作和访问的资源。
- 特权模式:可以执行所有指令,访问所有内存地址。
- 管理模式:正常的程序执行模式,也是
main函数的运行模式。 - 中断模式:响应普通中断时进入。
- 快速中断模式:响应快速中断(如 DMA 传输完成)时进入。
- 管理模式、中止模式、未定义模式、系统模式(也是一种特权模式,但不会通过异常进入)。
- 管理模式:正常的程序执行模式,也是
- 非特权模式:用户模式,应用程序通常在此模式下运行,权限受限,不能直接访问硬件。
- 异常:是导致处理器改变正常执行流程的事件。
- 复位:芯片上电或复位。
- 中断:来自外部的请求。
- 异常向量表:一个内存地址表,存储了每种异常对应的处理函数入口地址。
4 关键概念:寄存器
ARM 处理器有 37 个 32 位寄存器,但在任何时刻,程序员只能看到其中的一部分,这取决于处理器当前所处的模式。
- 通用寄存器:
R0-R7:在所有模式下都是同一个物理寄存器。R8-R12:在特权模式和非特权模式下是同一个,但在异常模式下,会映射到另一组物理寄存器(称为银行寄存器),用于保存上下文。
- 专用寄存器:
R13(Stack Pointer, SP):堆栈指针,每个模式都有自己独立的 SP,用于管理该模式的堆栈。R14(Link Register, LR):链接寄存器,用于保存函数调用的返回地址。R15(Program Counter, PC):程序计数器,指向当前正在执行的指令地址。
- 程序状态寄存器:
- CPSR (Current Program Status Register):当前程序状态寄存器,存储当前处理器的状态和模式。
- SPSR (Saved Program Status Register):备份的程序状态寄存器,用于在异常发生时保存 CPSR。
5 关键概念:内存映射
ARM 芯片将整个 4GB 的地址空间(32位系统)划分为不同的功能区域,这就是内存映射。
- 代码段:存放程序指令。
- 数据段:存放全局变量、静态变量。
- 堆:用于动态内存分配(
malloc/free)。 - 栈:用于局部变量、函数参数、返回地址。
- 外设寄存器区:将 CPU 的总线地址映射到具体的硬件寄存器上,CPU 像读写内存一样读写这些地址,就能控制外设(如 GPIO、UART、Timer 等),某个 GPIO 端口的数据寄存器可能位于
0x4001 0000地址。
第二部分:ARM C 语言编程核心
1 volatile 关键字:嵌入式编程的灵魂
这是最重要的知识点!
-
问题:编译器为了优化,会假设程序中的变量不会在它“看不见”的地方被改变,它会缓存变量值到寄存器中,而不是每次都从内存读取。
-
场景:当一个变量被硬件(如外设寄存器、中断服务程序)修改时,编译器并不知道,C 代码中有一个循环依赖这个变量,编译器可能会错误地使用寄存器中的旧值,导致死循环或逻辑错误。
-
解决方案:使用
volatile关键字。- 作用:告诉编译器:“这个变量可能会被我不知道的方式改变,所以每次使用它时,都必须从内存中重新读取,不要做任何优化(如缓存到寄存器)。”
-
典型用法:
volatile uint32_t * const GPIO_DATA = (uint32_t *)0x40010000; // 假设这是GPIO数据寄存器地址 while(1) { if (*GPIO_DATA & 0x01) { // 必须每次都从内存读取,不能优化 // ... } }
2 static 关键字:防止重入和作用域控制
在嵌入式系统中,static 有两个重要作用:
- 限定作用域:使变量或函数只在当前文件内可见,避免全局命名冲突。
- 延长生命周期:使局部变量在函数调用之间保持其值,存储在全局数据区而非栈上,这在需要“记住状态”的函数中非常有用,也常用于防止重入问题。
- 重入问题:如果一个函数可以被中断服务程序调用,或者可以被多线程调用,并且它使用了静态局部变量,就会导致数据错乱,因为 ISR 和主程序可能同时修改这个静态变量。
3 内联汇编:直接与硬件对话
当 C 代码无法完成某些操作时(如精确的时序控制、特殊的指令),就需要使用内联汇编。
- 语法:
__asm__ ("assembly code" : output_operands : input_operands : clobber_list); - 示例:禁用中断(ARM Cortex-M 系列)
void disable_irq(void) { __asm__ ("cpsid i"); // 执行 CPSID I 指令,关闭可屏蔽中断 }
4 内存对齐
- 问题:处理器从内存读取数据时,通常以字(4字节)、半字(2字节)为单位,如果数据没有对齐(一个
int类型变量从奇数地址开始),处理器可能需要执行两次内存访问,效率降低,甚至在某些架构上会引发硬件异常。 - 解决方案:
- 编译器选项:使用编译器选项(如
-falign-functions)让编译器自动对齐结构体和函数。 - 手动对齐:使用编译器提供的伪指令或关键字(如
__attribute__((aligned(8))))。 - 访问对齐数据:确保指针指向正确的地址。
- 编译器选项:使用编译器选项(如
5 结构体与位域
-
结构体:用于组织相关的寄存器,用一个结构体来表示一个 GPIO 端口的所有寄存器。
typedef struct { volatile uint32_t MODER; // 模式寄存器 volatile uint32_t OTYPER; // 输出类型寄存器 volatile uint32_t OSPEEDR; // 输出速度寄存器 volatile uint32_t PUPDR; // 上拉/下拉寄存器 volatile uint32_t IDR; // 输入数据寄存器 volatile uint32_t ODR; // 输出数据寄存器 // ... 其他寄存器 } GPIO_TypeDef; // 定义一个指向GPIOA的指针 #define GPIOA ((GPIO_TypeDef *) 0x40020000)这样访问
GPIOA->ODR就非常直观。 -
位域:当一个寄存器的每一位都有独立含义时,使用位域可以方便地操作。
typedef struct { uint32_t pin0 : 1; // 1位 uint32_t pin1 : 1; // 1位 uint32_t reserved : 30; // 保留位 } GPIO_ODR_Bits; volatile GPIO_ODR_Bits * const GPIO_ODR_Ptr = (GPIO_ODR_Bits *) 0x40020014; // 设置 pin1 为 1 GPIO_ODR_Ptr->pin1 = 1;
第三部分:实践与工具链
1 开发环境搭建
一个典型的 ARM 开发环境包括:
- IDE:Keil MDK, IAR, SEGGER Embedded Studio, 或者开源的 VS Code + PlatformIO。
- 编译器:ARM Compiler (ARMCC/ARMCLANG), GCC (ARM-none-eabi-gcc)。
- 调试器/烧录器:J-Link, ST-Link, U-Link。
- 硬件:开发板(如 STM32 Nucleo, LPCXpresso)。
2 启动代码
这是芯片上电后运行的第一段代码,通常用汇编或 C 写,它的任务是:
- 设置堆栈指针:为 C 运行环境准备栈。
- 初始化数据段:将
.data段(已初始化的全局/静态变量)从 Flash 复制到 RAM。 - 清零 BSS 段:将
.bss段(未初始化的全局/静态变量)清零。 - 调用
SystemInit()(可选):配置系统时钟。 - 跳转到
main():启动 C 程序。
3 编写第一个 ARM C 程序:点灯
假设我们使用 STM32 和 HAL 库(一个硬件抽象库)。
-
配置引脚:通过 STM32CubeMX 工具配置一个引脚为输出模式。
-
生成代码:生成包含初始化代码的工程。
-
编写 C 代码:
// main.c #include "main.h" #include "stm32f4xx_hal.h" // 包含芯片相关的头文件 int main(void) { // 1. 系统时钟和外设初始化(由 HAL 库完成) HAL_Init(); SystemClock_Config(); // 2. 初始化 GPIO(由 HAL 库完成) MX_GPIO_Init(); // 3. 主循环 while (1) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 翻转 PA5 引脚电平(通常连接板载LED) HAL_Delay(500); // 延时 500ms } }这里的
HAL_GPIO_TogglePin实际上是通过一个结构体指针访问 GPIO 端口的 ODR 寄存器,然后执行一次异或操作来翻转某一位。
4 中断服务程序 的编写
-
配置中断:使用 CubeMX 或手动配置某个外设(如定时器)使其能产生中断,并设置中断优先级。
-
编写 ISR:ISR 函数名通常有固定格式,由编译器或库定义。
// stm32f4xx_it.c (中断文件) void TIM2_IRQHandler(void) { // 1. 检查中断标志位 if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET) { // 2. 清除中断标志位(非常重要!否则会一直进入中断) __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE); // 3. 执行中断服务逻辑 HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); } }- 注意:ISR 应该尽可能短小精悍,复杂操作应该通过“事件标志”或队列通知主循环去处理。
volatile:所有在 ISR 和主循环之间共享的全局变量,都必须声明为volatile。
5 与外设通信:GPIO、UART 示例
-
GPIO:如上所述,通过操作
ODR(输出) 或IDR(输入) 寄存器来控制或读取引脚状态。 -
UART (串口):
// 初始化 UART (通常由 CubeMX 完成) UART_HandleTypeDef huart1; MX_USART1_UART_Init(); // 发送字符串 char *msg = "Hello from ARM!\r\n"; HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY); // 接收字符串 (使用中断或DMA方式) uint8_t rx_buffer[64]; HAL_UART_Receive_IT(&huart1, rx_buffer, 64); // 启动中断接收
第四部分:进阶主题
1 C 语言与 MMU/MPU
- MMU (Memory Management Unit):在更复杂的 ARM 处理器(如 Cortex-A 系列,运行 Linux/Android)中,MMU 负责虚拟内存管理、地址映射和内存保护。
- MPU (Memory Protection Unit):在 Cortex-M 系列中,MPU 提供了简化的内存保护功能,它可以将内存区域划分为不同的“区域”,并为每个区域设置访问权限(读、写、执行)和缓存属性,在 C 语言中,你通过配置 MPU 寄存器来定义这些区域,从而防止程序错误地修改关键代码或数据。
2 RTOS (实时操作系统) 下的 ARM C 编程
在 RTOS(如 FreeRTOS, uC/OS)下,编程模型发生了变化:
- 任务:C 代码被组织成多个独立的任务。
- 同步与通信:任务间需要通过信号量、互斥锁、消息队列等进行通信,以避免资源竞争。
- 栈管理:每个任务都有自己的栈,需要合理配置栈大小。
- 中断:ISR 在 RTOS 中通常只做最基本的事(如“给信号量”),然后尽快退出,具体的处理交给任务。
3 性能优化技巧
- 使用寄存器变量:
register关键字(现代编译器通常能自动优化,但有时手动指定仍有帮助)。 - 循环展开:减少循环次数,增加每次循环内的操作。
- 查表法:用预计算的查找表替代复杂的实时计算。
- 算法优化:选择时间复杂度更低的算法。
- 使用 DSP 指令:ARM Cortex-M4/M7 等处理器带有 DSP 扩展,可以使用 SIMD 指令进行并行数据处理。
ARM C 语言程序设计是一个结合了C 语言基础、计算机体系结构和硬件知识的综合性领域。
- 基础:掌握 C 语言,特别是
volatile、static、指针和结构体。 - 桥梁:理解 ARM 的核心概念(模式、寄存器、内存映射),知道 C 代码如何被编译成机器码,以及汇编如何与 C 交互。
- 实践:熟练使用开发工具链,学会阅读芯片手册和外设数据手册,能够编写驱动程序和中断服务程序。
- 进阶:了解操作系统、内存保护和性能优化,为构建复杂系统打下基础。
从点亮一个 LED 开始,逐步尝试更复杂的外设,最终你就能成为一名合格的嵌入式 ARM 开发者。
