[PICO][Adv]汇编语言

Raspberry Pi Pico 2 汇编语言

Raspberry Pi Pico 2 通常使用 C/C++(基于 Pico SDK)或 Arduino 语言(基于 Arduino-Pico 核心)进行编程。但有时我们需要更直接地控制硬件,或对性能关键代码进行极致优化,这时汇编语言就派上了用场。汇编语言是一种低级编程语言,它直接与硬件交互,提供了对微控制器(如 Pico 2 的 RP2350 芯片)的精确控制。通过学习汇编语言,你可以更好地理解底层硬件的工作原理,并优化程序的性能。

本文将介绍 RP2350(ARM Cortex-M33 架构)的汇编语言基础,包括寄存器、指令集、语法,并通过 Arduino 风格(内联汇编)和 C/C++ 风格(独立汇编文件)展示实际案例。


1. 为什么使用汇编语言?

优势 说明
性能优化 汇编语言可以直接操作寄存器,减少函数调用和编译器生成的多余指令,榨干芯片性能。
精确控制 你可以精确控制硬件的每一个细节,例如设置 CPU 状态、中断控制、时序循环。
学习底层 通过汇编语言,你可以深入理解 ARM Cortex-M 体系结构、内存映射、指令流水线等。
特殊指令 某些功能(如 DMBDSB 内存屏障、WFE 低功耗指令)只能通过内联汇编或汇编文件实现。

提示:汇编语言会增加代码复杂性和维护成本。建议仅在性能瓶颈或需要特殊指令的关键代码段中使用。


2. 基本概念:ARM Cortex-M33 汇编

RP2350 使用的是 ARMv8-M Mainline 指令集(兼容 Thumb-2)。与 AVR 的 8 位 RISC 不同,ARM Cortex-M 是 32 位架构,但指令通常为 16 位(Thumb)或 32 位(Thumb-2),以提高代码密度。

2.1 寄存器

ARM Cortex-M33 有 16 个通用寄存器(R0-R12,SP,LR,PC),以及特殊寄存器(PSR、PRIMASK、CONTROL 等)。

寄存器 别名 描述
R0-R3 参数传递和返回值,函数调用时临时使用
R4-R11 通用寄存器,调用子程序时需保存(如果使用)
R12 IP 内部过程调用临时寄存器
R13 SP 堆栈指针(PSP 或 MSP)
R14 LR 链接寄存器,存储返回地址
R15 PC 程序计数器

注意:在编写内联汇编时,编译器可能会自动保存/恢复 R4-R11,但需遵守 AAPCS 调用规范。

2.2 常用指令集(部分)

指令 示例 描述
MOV MOV R0, #10 将立即数或寄存器值移动到目标寄存器
ADD ADD R1, R2, R3 R1 = R2 + R3
SUB SUB R0, #1 R0 = R0 - 1
LDR LDR R0, [R1] 从内存地址 R1 加载字到 R0
STR STR R0, [R1] 将 R0 存储到内存地址 R1
CMP CMP R0, #0 比较 R0 和 0,更新条件标志
B B label 无条件跳转到 label
BEQ BEQ label 如果相等(Z=1)则跳转
BL BL func 带链接的跳转(调用子程序)
BX BX LR 返回(跳转到 LR 地址)
NOP NOP 无操作,常用于延时

提示:ARM 汇编通常一行一条指令,注释以 @; 开头。

2.3 汇编语法(GNU Assembler)

Pico SDK 使用 GNU 工具链,汇编语法遵循 GNU Assembler(GAS)。

  • 标签:以 : 结尾,例如 loop:
  • 指令:前面加点,如 .section, .word, .thumb_func
  • 立即数:前面加 #,例如 MOV R0, #42
  • 注释@// 到行尾。

3. 代码示例

3.1 Arduino 风格:内联汇编

在 Arduino-Pico 核心中,可以使用 __asm__ 关键字嵌入汇编语句。以下示例演示了如何使用内联汇编直接操作 GPIO 寄存器来翻转板载 LED(GP25)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 使用内联汇编闪烁板载 LED(GP25)
void setup() {
// 使能 GPIO 25 的输出功能(通过 C 函数)
gpio_init(25);
gpio_set_dir(25, GPIO_OUT);
}

void loop() {
// 使用内联汇编设置 GPIO 输出高电平
__asm__ volatile (
"mov r0, #1\n" // 将值 1 放入 R0
"ldr r1, =0xd0000000\n" // GPIO 输出寄存器基址(实际地址请参考 SDK)
"str r0, [r1, #(0x14)]" // 存储到 GPIO_OUT 寄存器(偏移 0x14)
: : : "r0", "r1" // 告诉编译器 R0、R1 被修改
);

delay(500);

// 设置低电平
__asm__ volatile (
"mov r0, #0\n"
"ldr r1, =0xd0000000\n"
"str r0, [r1, #(0x14)]"
: : : "r0", "r1"
);

delay(500);
}

注意:直接操作硬件寄存器地址需要知道确切的映射(Pico SDK 提供了宏,如 sio_hw->gpio_out)。更安全的方式是使用 SDK 函数,内联汇编仅用于演示。实际项目中推荐如下方式:

更实用的内联汇编示例:读取 CPU 周期计数器

Cortex-M33 提供了一个 DWT(Data Watchpoint and Trace)单元,可以读取 CPU 周期计数器。这通常需要汇编指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 读取 CPU 周期计数的函数(内联汇编)
uint32_t get_cpu_cycles() {
uint32_t cycles;
__asm__ volatile (
"ldr r0, =0xE0001004\n" // DWT_CYCCNT 地址
"ldr %0, [r0]\n"
: "=r"(cycles)
:
: "r0"
);
return cycles;
}

void setup() {
Serial.begin(115200);
// 启用 DWT 周期计数器(需要先写 DEMCR 和 DWT_CTRL)
// ...(省略初始化代码)
}

void loop() {
uint32_t start = get_cpu_cycles();
delayMicroseconds(100);
uint32_t end = get_cpu_cycles();
Serial.print("Cycles used: ");
Serial.println(end - start);
delay(1000);
}

3.2 C/C++ 风格:独立汇编文件

当汇编代码较多时,可以创建独立的 .S 文件,并在 C 代码中声明外部函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// blink.S - 汇编函数,翻转 LED(GP25)
.syntax unified
.thumb
.thumb_func
.global blink_asm

blink_asm:
// 参数:无,直接操作 GPIO
// 加载 GPIO 基址到 R0
ldr r0, =0xd0000000
// 读取当前 GPIO_OUT 值
ldr r1, [r0, #0x14]
// 翻转第 25 位(即 1<<25)
eor r1, r1, #(1<<25)
// 写回
str r1, [r0, #0x14]
bx lr

步骤 2:在 C 代码中声明和使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "pico/stdlib.h"

// 声明汇编函数
extern void blink_asm(void);

int main() {
stdio_init_all();
gpio_init(25);
gpio_set_dir(25, GPIO_OUT);

while (true) {
blink_asm(); // 调用汇编函数翻转 LED
sleep_ms(500);
}
return 0;
}

步骤 3:修改 CMakeLists.txt

1
2
3
4
add_executable(blink_asm
main.c
blink.S
)

说明.thumb_func 告诉链接器这是 Thumb 代码;.global 导出符号。


4. 实际应用案例

4.1 精确延时函数(不使用 SysTick)

用汇编实现一个微秒级阻塞延时,比循环调用 SDK 函数更可控。

汇编延时函数 delay_us.S

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.syntax unified
.thumb
.thumb_func
.global delay_us

// 参数:R0 = 延时微秒数(假设 CPU 频率 150MHz)
delay_us:
// 粗略估算:每个循环约 2 个周期(实际需根据流水线调整)
// 为简化,此处实现一个简单递减循环
ldr r1, =150 @ 每微秒需要的循环次数(需校准)
mul r0, r0, r1 @ 总循环次数
1:
subs r0, #1
bne 1b
bx lr

注意:实际 CPU 频率可能不同,且循环周期受流水线和内存访问影响,需要校准。更精确的方法是使用 DWT 周期计数器。

4.2 临界区与内存屏障

在操作共享资源或关闭中断时,需要内存屏障指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 禁用全局中断(ARM 汇编)
uint32_t disable_irq(void) {
uint32_t primask;
__asm__ volatile (
"mrs %0, primask\n"
"cpsid i\n"
: "=r"(primask)
);
return primask;
}

void restore_irq(uint32_t primask) {
if (primask == 0) {
__asm__ volatile ("cpsie i");
}
}

4.3 低功耗模式

使用 WFI(Wait For Interrupt)指令让 CPU 进入休眠,等待外部中断唤醒。

1
2
3
void sleep_until_interrupt(void) {
__asm__ volatile ("wfi");
}

5. 汇编与 C 混合编程的注意事项

要点 说明
寄存器保存 如果修改 R4-R11,需要先压栈保存,返回前恢复。R0-R3 和 R12 可以随意修改。
堆栈对齐 ARM Cortex-M 要求堆栈 8 字节对齐,调用 C 函数前确保对齐。
Thumb 状态 使用 .thumb.thumb_func 确保代码在 Thumb 状态执行。
符号命名 C 函数名在汇编中通常加下划线(GCC 不加),但 Pico SDK 使用 _ 前缀?实际上 GNU 工具链不会自动添加,直接使用相同名称即可。
调试 可以使用 -g 编译选项,GDB 支持混合汇编/源码调试。

6. 汇编语言的优缺点

优点 缺点
极致性能,可手写循环展开、SIMD(有限) 编写效率低,易出错
直接访问特殊功能寄存器(如 BASEPRI、DWT) 可移植性差,更换芯片需重写
精确控制指令时序 维护成本高,难以阅读

7. 总结

Raspberry Pi Pico 2 的汇编语言虽然复杂,但它提供了对硬件的精确控制,是优化性能和理解底层硬件的有力工具。通过学习汇编语言,你可以:

  • 编写比 C 更高效的代码(例如实现 memcpy、CRC 计算)
  • 使用 C 语言无法直接访问的特殊指令(内存屏障、低功耗指令)
  • 深入理解 ARM Cortex-M33 体系结构

在实际项目中,建议优先使用 C 语言,仅在性能瓶颈或需要特殊指令的关键代码段中使用内联汇编或独立汇编文件。


8. 附加资源与练习

通过不断练习,你将逐步掌握 Pico 2 汇编语言,并能够在必要时进行底层优化。祝你编程愉快!


[PICO][Adv]汇编语言
https://ka5fxt.cn/2026/03/30/PICO-Adv-汇编语言/
发布于
2026年3月30日
许可协议