☰
STM32寄存器编程实战:GPIO与RCC底层配置详解
2026/10/19 1:38:50 网站建设 项目流程

很多从库函数入门的朋友,点灯、串口都能跑,可一旦调试器停在某个外设寄存器上,看着那一串十六进制数,还是会头皮发麻。这不是你学得不好,而是库函数把“往地址里写值”这件事藏得太深了。STM32寄存器这个系列,上一章我们用最小工程把工程骨架和位操作基础过了一遍,这一章就专门挑最常用的 GPIO 和 RCC 下手,把参考手册里那些英文位域翻译成人话,看完你就能直接照着写代码,不看库源码也能知道芯片到底在干什么。

这一章不是教你把每个寄存器都背下来,而是教你怎么看寄存器手册、怎么把一个“点亮LED”“读取按键”的需求拆成寄存器的动作。适合那些已经会HAL库、但总感觉隔着一层纱的人,也适合准备从零裸机开发的初学者。我会把底层机制、实操代码、常见翻车点放在一起,争取一章下来,你自己就能对着手册改点别的引脚。

1. 为什么放着现成的库不用,偏要自己摆弄寄存器

先说个扎心的事实:HAL库也好、标准库也好,抽丝剥茧到最后,它们的工作依然是往寄存器地址里写数。库函数做的是把“配置模式”翻译成“MODER 的某两位是 01”,把“输出高电平”翻译成“BSRR 的某一位写 1”。你天天用库,其实一直在间接操作寄存器。

那为什么还要故意绕开库,自己写?我自己的体会有三个。

第一个原因是排查问题。程序跑飞、引脚无输出、串口乱码,用HAL库的时候你只能一层层追进源码里看哪一步没执行,效率很低。如果心里有寄存器层面的地图,拿到报错信息就能直接对应到某个外设的某个寄存器,问题范围立刻缩小。

第二个原因是效率极限。某些场景对启动时间、内存占用非常敏感,比如需要在上电后极短时间输出一个波形,或者把初始化代码压到几百字节。库函数哪怕做了内联,仍然可能多出你想不到的状态判断和延迟。寄存器方式可以做到“看到就是执行到”。

第三个原因是面试和笔试。这个话题老套但真实,很多技术面试都会问“GPIO 输出模式有哪几种”“开漏和推挽的区别是什么”“BSRR 为什么比 ODR 赋值安全”。不亲手写几遍寄存器,这些问题答起来永远像背课文。

我整理一个最直观的对比,点一个 LED,HAL 的方式大致是:

__HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef init = {0}; init.Pin = GPIO_PIN_5; init.Mode = GPIO_MODE_OUTPUT_PP; init.Pull = GPIO_NOPULL; init.Speed = GPIO_SPEED_FREQ_MEDIUM; HAL_GPIO_Init(GPIOA, &init); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);

寄存器的方式是:

RCC->AHB1ENR |= (1U << 0); // 开 GPIOA 时钟 GPIOA->MODER &= ~(3U << (5 * 2)); // 清空 PA5 模式位 GPIOA->MODER |= (1U << (5 * 2)); // PA5 设为输出 GPIOA->BSRR = (1U << 5); // PA5 输出高电平

如果你能看懂后面这四行,并且知道每一行为什么这么写,那本章的目的就达到了。库的优势是跨芯片复用,而寄存器的优势是直白、可控、不藏逻辑。掌握寄存器不是要和库对立,而是在库失效的时候你还有自己的底牌。

2. 地址、指针和结构体:寄存器读写是怎么发生的

在写代码之前,必须把“寄存器到底在哪里”“我为什么可以用 GPIOA->MODER 这种写法”这两件事搞清楚。很多人卡在寄存器的学习的门槛,根本不是不会位运算,而是不知道符号和硬件地址是怎么绑定的。

2.1 外设地址与寄存器偏移

32 位 MCU 的寻址空间是 4GB,各个外设被映射到其中一段固定地址。以 F4 系列为例,GPIO 外设的起点在 0x40020000 附近,RCC 时钟模块的起点在 0x40023800 附近。每个外设内部又有一串连续的寄存器,相互之间间隔 0x04,也就是一个 32 位字的长度。

手册里通常画成表格:偏移地址、寄存器名称、重置值、位描述。比如 GPIOA 这个外设,从基地址开始:

寄存器名偏移作用
MODER0x00引脚模式选择(输入/输出/复用/模拟)
OTYPER0x04输出类型(推挽/开漏)
OSPEEDR0x08输出速度
PUPDR0x0C上下拉配置
IDR0x10输入数据(只读)
ODR0x14输出数据
BSRR0x18置位/复位寄存器
LCKR0x1C配置锁定
AFRL / AFRH0x20 / 0x24复用功能选择

注意:不同系列之间这些地址差别很大。F1 系列 GPIO 只有 CRL、CRH、IDR、ODR 这些寄存器,没有 MODER、OTYPER。所以哪怕你看得懂 F4 的代码,换成 F1 也要重新查手册。我这章用 F4 风格的寄存器做例子,具体芯片永远以对应参考手册为准。

2.2 CMSIS 头文件里的结构体魔法

你可能会好奇,为什么写GPIOA->MODER,编译器就知道把它翻译成对 0x40020000 这个地址的读写?秘密藏在头文件里。

CMSIS 把每个外设定义成一个结构体,比如:

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; volatile uint32_t BSRR; volatile uint32_t LCKR; volatile uint32_t AFRL; volatile uint32_t AFRH; } GPIO_TypeDef;

然后又定义了基地址指针:

#define GPIOA ((GPIO_TypeDef *)0x40020000U)

所以GPIOA->MODER就等价于*(volatile uint32_t *)(0x40020000U + 0x00U),也就是向基地址偏移 0 的地方写入一个 32 位数。这个结构体成员的摆放顺序和偏移完全对应手册,如果中间少定义了一个成员,后面所有寄存器的地址就全错了。

这里的volatile非常关键,它告诉编译器:这个地址的值可能被硬件随时改变,每次读都重新去访问,不要自作聪明优化。你见过while (GPIOA->IDR & (1U<<0))死循环退不出来的例子,很多情况就是某个变量被误加了编译器优化,但寄存器访问本身因为有 volatile,通常不会出现这种问题。

2.3 位运算的数学基础

GPIO 的每个寄存器控制 16 个引脚,一个引脚分到的位宽可能不止 1 位,比如 MODER 每个引脚占 2 位,说明一个引脚能表达 4 种状态。你想要配置第 n 个引脚,就得操作(n * 2)到(n * 2 + 1)这两 bit。

清位的标准写法是“与上取反”:

GPIOA->MODER &= ~(3U << (n * 2));

置位的标准写法是“或上”:

GPIOA->MODER |= (1U << (n * 2));

为什么不能直接写GPIOA->MODER = 1U?因为一个寄存器同时控制着 16 个引脚,直接整个赋值会把其他引脚的配置全部冲掉。这也是我反复强调的:寄存器操作是“读-改-写”,除非你确定整个寄存器的值,否则不要直接赋值。这个习惯后面会帮你少踩很多坑。

3. 点亮一颗 LED 背后要配置哪些寄存器

现在我们从零开始,让 PA5 输出高电平。整个过程分为两步:先给 GPIOA 模块通电,再设置引脚模式并输出电平。这两步缺一不可,也是新人最容易卡住的地方。

3.1 RCC 时钟使能:忘记它等于办公室没开电闸

STM32 为了省电,大多数外设的时钟默认是关闭的。你直接写 GPIOA 的寄存器,模块根本没通电,数据根本进不去,读出来永远是复位值。这就好比你到了办公室,按开关想让电灯亮,但整层楼的电闸没合上,按多少次都没用。

RCC 模块里有一个 AHB1ENR 寄存器,bit0 控制 GPIOA 的时钟。置 1 使能:

RCC->AHB1ENR |= (1U << 0);

如果是 GPIOB,就是 bit1;GPIOC 是 bit2,以此类推。程序刚复位后 AHB1ENR 默认值通常为 0,所以这一位必须要自己置上去。

这里提一个调试技巧:如果你怀疑时钟没开,不要在代码里猜,直接在调试器的“外设寄存器”窗口里展开 RCC 模块,盯着 AHB1ENR 的值,单步执行到这一行代码,看这一位是否从 0 变成 1。能这么直观地验证,就说明你对地址映射的理解是对的。

3.2 MODER、OTYPER、OSPEEDR、PUPDR 逐个配置

时钟打开以后,GPIOA 模块才真正活过来。接下来要设置 PA5 的模式。先清两个人的位再置位,为的是防止复位默认值或者之前的配置干扰:

GPIOA->MODER &= ~(3U << (5 * 2)); // 清空 PA5 这两位 GPIOA->MODER |= (1U << (5 * 2)); // 置为 01,输出模式

MODER 的编码是:00 输入、01 输出、10 复用、11 模拟。所以两位二进制写成01,对,就是往较低的 bit 写 1,较高的 bit 写 0。如果你见过某些人用|= 1而不是&= ~3; |= 1,那多半是在复位后恰好寄存器为 0 才有用,一旦初始化过就会出问题。

OTYPER 决定引脚是推挽还是开漏。LED 点灯用推挽就够了,写 0 即可:

GPIOA->OTYPER &= ~(1U << 5);

推挽模式的含义是:引脚既能主动输出高电平,也能主动拉低。你可以理解为引脚内部有两个开关,一个负责输出高,一个负责拉低,总有一个是闭合的。所以 LED 无论接高电平点亮还是低电平点亮,推挽都能驱动。开漏模式则只有一个拉低的开关,高电平基本靠外部上拉电阻,这点我们放到后面复用功能章节再展开。

OSPEEDR 控制输出压摆率。不是越快越好,越快电磁干扰越明显。点 LED 这种低频应用,用最低档甚至都没问题。这里设置成中速:

GPIOA->OSPEEDR &= ~(3U << (5 * 2)); GPIOA->OSPEEDR |= (1U << (5 * 2)); // 01,中等速度

对 GPIO 来说,速度指的是电平翻转的速度上限。如果你的引脚接了高速通信线,速度太低会削波;如果只是点灯,速度太高反而会在长走线上引起振铃。选型的逻辑是“够用就行”,这也是我一开始不建议所有人无脑开启高速档的原因。

PUPDR 配置上下拉。输出模式下我们不需要上下拉,清 0 即可:

GPIOA->PUPDR &= ~(3U << (5 * 2));

到这里,PA5 已经被配置成一个推挽输出、中速、无上下拉的引脚。代码长但每一步都对应手册里一个位域定义,没有一步是多余的。

3.3 BSRR 才是输出高低电平的正确姿势

引脚配置好了,接下来就是让电平翻转。最直观的写法是操作 ODR:

GPIOA->ODR |= (1U << 5); // 拉高 GPIOA->ODR &= ~(1U << 5); // 拉低

这种写法能工作,但推荐的做法是用 BSRR。BSRR 是一个很特别的寄存器,低 16 位写 1 对应“置位”,高 16 位写 1 对应“复位”,写 0 不产生任何影响。因为它本身是硬件完成的单次写操作,所以是原子性的:

GPIOA->BSRR = (1U << 5); // PA5 输出高电平 GPIOA->BSRR = (1U << (5 + 16)); // PA5 输出低电平

为什么说原子性很重要?因为ODR |=的本质是三步:读出 ODR、按位或、写回 ODR。假如执行过程中来了一个中断,中断里也改了这个寄存器的某一位,中断返回后再把旧值写回去,就会把中断里改的那一位覆盖掉。BSRR 不存在这个问题,你只写一次,硬件帮你完成置位或复位。这也是为什么真正的驱动代码里几乎都是用 BSRR/BRR 输出电平。

LED 接法也要注意。假设 LED 正极接 3.3V,负极通过 330Ω 电阻接 PA5,那 LED 的压降约 1.8V 到 2V,PA5 输出低电平时 LED 点亮,输出高电平时熄灭。这时候代码就是:

GPIOA->BSRR = (1U << (5 + 16)); // PA5 拉低,点亮 LED

很多教程默认“置位就是亮”,这其实是假设 LED 正极接引脚、负极通过电阻接地。实际操作时要先确认原理图,别让代码和硬件打架。

4. 按键输入、上下拉配置与读改写陷阱

点灯是输出方向,读按键是输入方向。这两个方向其实是同一套 GPIO 底层机制在服务,但输入方向有一个特别容易被忽略的坑:引脚悬空。

4.1 输入模式要怎么配:MODER 清 0 + PUPDR 决定电位

读取按键之前,先把引脚配置成输入模式。PA0 为例:

GPIOA->MODER &= ~(3U << (0 * 2)); // 00,输入模式 GPIOA->PUPDR &= ~(3U << (0 * 2)); GPIOA->PUPDR |= (1U << (0 * 2)); // 01,上拉

输入模式本身不需要速度寄存器,所以 OSPEEDR 不用管。关键在 PUPDR,也就是上下拉电阻。如果不配置成上拉或下拉,引脚就处于高阻输入状态,外界没有显著驱动时电平会漂浮不定,读出来的值可能一会儿 0 一会儿 1。这个道理跟音频线没插紧时的电流声很像,周围电磁噪声会直接耦合进来。

一般按键的典型接法是:按键一端接 GND,另一端接 PA0,同时 PA0 内部上拉。按键按下时,引脚被外部直接短路到 GND,读到的电平是 0;松开时,内部上拉电阻把引脚拉回高电平,读到 1。配置代码里的PUPDR |= (1U << 0)就是给引脚“接了一个到 VDD 的内部电阻”。

读取引脚用 IDR:

if ((GPIOA->IDR & (1U << 0)) == 0) { // 按键按下 }

注意 IDR 是只读寄存器,你往里写值没有任何意义。有些人会习惯性地把 ODR 和 IDR 混着看,其实输出状态下你读 IDR 也能反映引脚实际电平,但输入状态下 ODR 和引脚电平没有关系。搞清这个区别,很多“明明输出了但读不到”的困惑能少一半。

4.2 按键抖动与软件去抖

机械按键按下和松开的瞬间,触点会连续弹跳几次,持续几毫秒到十几毫秒。你会发现IDR & (1U<<0)的值在那一刻不是稳定的 0 或 1,而是 0、1、0、1 快速变化。如果不去抖,一次按键可能被程序识别成好几次按下,就会出现按一次灯闪了好几次的现象。

最简单的去抖就是延时后再读一次,这种方法在低实时性场景里足够实用:

uint8_t read_button_debounced(void) { if ((GPIOA->IDR & (1U << 0)) == 0) { delay_ms(10); if ((GPIOA->IDR & (1U << 0)) == 0) { return 1; // 确认按下 } } return 0; }

第一次读到低电平先不着急认为是按下,等到 10 毫秒之后再读一次,如果还是低电平,才判定为一次有效的按下。10 毫秒的选择不是拍脑袋,而是因为常见轻触按键的抖动时间一般在 5 到 10 毫秒,延时超过这个窗口就有很大概率避开弹跳区。你要是舍得程序里多占一点延时时间,也可以改成 20 毫秒,稳定性更好。

4.3 读改写陷阱:中断里千万别用 ODR 做位操作

前面提到过ODR |=是读-改-写三步,在中断场景下会出现非常隐蔽的丢位问题。举个实际例子:主循环里用GPIOA->ODR ^= (1U << 5)翻转 LED,按键中断里用GPIOA->ODR |= (1U << 6)点另一个灯。看似两人各改各的位,但硬件时序可能是这样的:

  1. 主循环读出 ODR,数值等于旧状态。
  2. 按键中断触发,修改 bit6 后写回 ODR。
  3. 主循环把自己修改完的整块数值写回去,把 bit6 的修改覆盖了。

结果就是,中断里明明执行了置位,灯却没亮。这个问题用调试器很难一眼看出来,因为“那一瞬间”的时序可遇不可求。

正确做法是能不用读改写就不用读改写。输出电平的位操作全部交给 BSRR 的高 16 位和低 16 位:

GPIOA->BSRR = (1U << 5); // 置位,不会被覆盖 GPIOA->BSRR = (1U << (5 + 16)); // 复位,不会被覆盖

如果你的代码里需要修改同一个寄存器的多个位,那就用局部变量先算好最终值,一次写回。这个习惯比任何神奇的代码技巧都可靠。

5. 复用功能、锁定寄存器与那些容易翻车的寄存器细节

“复用功能”是 GPIO 里另一个高频使用但理解成本偏高的概念。当一个引脚不是当普通 IO 用,而是接到串口、SPI、定时器输出等外设内部电路上,就需要配置 MODER 为 10 复用模式,然后在 AFRL/AFRH 里选择具体是哪个复用功能编号。

5.1 AFRL 和 AFRH:一个引脚能接一到多种外设

每个引脚有一个 4 位的复用号,取值范围 0 到 15。引脚号 0 到 7 配置在 AFRL,引脚号 8 到 15 配置在 AFRH。以 F4 的 USART1 为例,它经常用 PA9 做 TX、PA10 做 RX,这两个引脚的复用号都是 AF7。

配置代码大概这样:

GPIOA->MODER &= ~(3U << (9 * 2)); GPIOA->MODER |= (2U << (9 * 2)); // 10,复用模式 GPIOA->AFRH &= ~(0xFU << ((9 - 8) * 4)); GPIOA->AFRH |= (7U << ((9 - 8) * 4)); // 引脚9在AFRH,复用号7

不同芯片的复用映射表可能不一样,同一引脚在不同型号上可能是 AF5 也可能是 AF7。这块没有任何口诀可以绕过,只能打开参考手册的“Alternate function mapping”表格去查。很多人在这里把串口配置得“看起来没问题”但信号就是不出来,多半不是 MODER 错了,而是 AF 编号选错了。

5.2 OTYPER 开漏输出的真实用途

开漏输出是 GPIO 方向里容易被误用的选项。开漏模式下,引脚内部没有主动输出高电平的能力,只能把引脚拉低或者释放。释放后引脚电平取决于外部电路,最常见的是接一个上拉电阻到 VDD。

什么时候必须用开漏?典型场景是 I2C 总线。I2C 协议要求多个设备都能拉低总线,所以每个设备都必须是开漏输出,不能有任何一个设备主动输出高电平去“抢”线路。电平转换电路也常用开漏,上拉电阻接到目标电压,这样两块不同电压域的芯片就能互相通信。

如果你在自己的电路里把普通 LED 输出配置成开漏,又没加外部上拉电阻,那 LED 会怎么表现取决于外部负载,大概率是“只能灭不能亮”。所以看到引脚输出不了高电平,先检查 OTYPER 是不是 0,再检查 PCB 上有没有外部上拉。

5.3 LCKR 锁定寄存器:防篡改也要防自己

LCKR 可以做引脚配置锁定。它的锁定机制比较特别,不是简单的写 1 就能锁定,而是要用代码执行一个序列:先写“引脚位 + bit16 置 1”,再写“引脚位 + bit16 清 0”,再写“引脚位 + bit16 置 1”,最后读回 bit16,如果是 1,说明锁定成功。

GPIOA->LCKR = (1U << 0) | (1U << 16); GPIOA->LCKR = (1U << 0); GPIOA->LCKR = (1U << 0) | (1U << 16); if (GPIOA->LCKR & (1U << 16)) { // 已锁定,复位之前不能再改 PA0 配置 }

为什么要搞这么复杂?是为了防止程序跑飞的时候不小心把引脚配置改了。锁定以后,除非系统复位,否则你对 MODER、OTYPER、PUPDR 等寄存器的写操作都会被忽略。这功能在防拆、固件保护场景有用,但对于自己的开发板,我建议你尽量别用。否则调试时想临时改个引脚模式,却发现怎么改都不生效,需要整板复位才能解开,非常影响心情。

5.4 关于 5V 容忍和模拟模式的两个提醒

某些引脚是 5V 容忍的,意思是当引脚配置成输入或者开漏输出且外部接 5V 上拉时,引脚不会被损坏。但如果你把引脚配置成模拟模式或者推挽输出直接驱动到 5V,那就不一定安全了。很多人以为“标注 5V 容忍就能随便接 5V”,这是误解。5V 容忍的前提是引脚不在推挽输出模式,并且参考手册里明确标注了 FT 标志。

另外,当你把引脚配成模拟模式(MODER 为 11)时,引脚内部的大多数数字输入缓冲器会被关掉,串行调试相关的功能也可能受影响。有些工程师为了省电把 JTAG/SWD 的引脚配成普通 GPIO,结果下次烧录怎么也连不上调试器。碰到这种情况别慌,按住复位键的同时尝试连接调试器,或者在代码里保留调试引脚功能,别图一时省事。

6. 实战排障:LED 不亮与引脚状态异常该按什么顺序查

遇到“我代码明明写了,为什么芯片不理我”的情况,最忌讳的是随机试错。下面这套排查链路是我自己调试 GPIO 时固定走的路,几乎覆盖了 90% 的初级问题。

首先是 LED 不亮。从芯片往外,按逻辑链一步一步查:

排查步骤检查点常见原因
1RCC 时钟是否使能AHB1ENR 对应位是否为 1
2引脚模式MODER 对应位是否为 01 输出
3输出状态BSRR 写过的高/低电平是否符合预期
4电路连接LED 极性是否接反、限流电阻是否过大或开路

具体操作是,用调试器全速运行几毫秒后暂停,打开外设寄存器窗口,展开 GPIOA,看 MODER、OTYPER、PUPDR 和 ODR 的真实值。如果你看到 MODER 对应位是 00,说明引脚还是输入模式;看到 ODR 对应位是 1,但 LED 不亮,问题大概率在硬件;看到 ODR 是 0,但只要 BSRR 置位过,就说明输出数据本身根本没写进去,再回头查时钟。

其次是引脚输出波形不对,比如示波器看到波形上升沿超调、振铃明显。先别怀疑芯片坏了,检查 OSPEEDR。信号线很长、走线阻抗不匹配时,非常高的输出速度会产生明显的过冲。把速度档位调到最低或者中速,往往波形立刻好看很多。

再有一种场景是引脚无论如何都写不进值,读出来一直是复位默认。这时候除了检查时钟,还要看一眼 LCKR 是不是已经锁定。锁定状态下寄存器写入是静默失败的,也就是写操作不报错,但值不变。如果你之前跑过程序里带锁定的逻辑,就要通过复位整个芯片来解锁。

还有一种情况是按键输入乱跳,或者外部信号正常但读到的值不对。优先检查 PUPDR。上拉/下拉配置与外部电路不匹配,比如外部已经接了下拉电阻,内部又配置了上拉,两个电阻分压后的电平可能刚好在阈值附近漂移。处理方式很简单:外部电路有明确电位,内部上下拉就配成无;外部悬空,内部上下拉才发挥作用。

最后分享一个调试器的小技巧:很多 IDE 支持读写断点(Watchpoint),你可以把它设置在 GPIOA 的 BSRR 或者 ODR 上。当程序里任何一处代码往这个寄存器写值时,调试器都会停下来。这会让你极其惊讶地发现,某些你以为“只读不写”的库函数,背后其实偷偷动了不少寄存器。

7. 本章练习:把寄存器操作变成肌肉记忆

光看不练永远形成不了手感。我在第二张结尾留三道题,难度从低到高,建议你亲手写一遍,尽量不要参考任何库代码。

第一题:把 PA5 的 MODER、OTYPER、OSPEEDR、PUPDR 全部手动清成复位默认值,然后重新配置成输出模式。目的是让你习惯“清位-置位”的完整动作,同时体会复位默认值到底是什么状态。

第二题:配置 PA5 和 PA6,让两个 LED 交替点亮,要求一个用 BSRR 高 16 位熄灭,另一个用 BSRR 低 16 位点亮,中间加延时。这道题练的是对 BSRR 双向写入的理解。

第三题:在 PA1 接一个按键,按键另一端接 GND,配置 PA1 为带上拉输入。按下按键时,翻转 PA5 输出。注意把去抖函数写进去,观察没有去抖和有去抖的区别。这道题会强迫你同时用上输入、输出、上下拉和延时四个知识点。

做完这三道题,再回去看 HAL 库的 GPIO 初始化代码,你会有一种“终于看懂了套娃里面最里层那个娃娃”的通透感。

最后说一句个人的体会:寄存器手册不是拿来背的,是拿来查的。做了这么多年嵌入式,我每次换一个芯片型号,打开芯片手册时依然要重新查内存映射和寄存器偏移表。真正值钱的不是把某个寄存器地址记住,而是知道该去哪里查、怎么读位域、怎么对照复位值判断异常状态。养成“改一个寄存器,就去调试器里看一眼真实值”的习惯,后面写定时器、写串口、写 DMA,都是同一套动作。这一章的内容就到这里,下一章该聊聊中断和定时器了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询