1. 为什么第二章才是寄存器学习真正的分水岭
很多人学STM32寄存器,第一章看完GPIO的配置,感觉还挺简单——不就是往几个地址写0和1嘛,时钟一开,模式一配,灯就亮了。但到了第二章,事情开始变得不对劲了。中断、时钟树、NVIC、优先级分组、外部中断的边沿检测,这些东西一股脑涌上来,很多人就是在这个阶段放弃寄存器操作,转头去抱库函数的大腿。
我特别理解这种感受。第一章的GPIO像是学骑自行车,摔两下总能学会;第二章的中断和时钟配置,更像是第一次上高速——规则多、细节密、一个参数配错,程序要么不响应,要么直接跑飞。但恰恰是这一章的内容,决定了你到底是在"用STM32"还是在"理解STM32"。
这篇内容就是围绕STM32寄存器操作的第二阶段展开,重点讲清楚三件事:时钟树到底怎么配、中断系统从寄存器层面怎么跑通、NVIC优先级分组为什么容易踩坑。适合已经看过第一章、能独立配置GPIO的读者,也适合那些用库函数用久了、想回头补底层原理的人。我不会只给你一堆寄存器地址让你抄,而是把每个配置背后的"为什么"讲透,让你下次遇到新芯片也能自己推出来。
提示:本文所有寄存器描述基于Cortex-M3/M4内核的通用架构,不同型号的STM32在具体地址和位定义上会有差异,实际使用时请对照对应型号的参考手册。文中提到的地址和位域仅作为讲解示例。
2. 时钟树:所有外设的"总闸"到底怎么合
2.1 时钟不是"开了就行",理解树形结构才能配得对
第一章里我们配置GPIO时,第一步就是开时钟,比如RCC->APB2ENR |= (1 << 2)打开PA口的时钟。当时你可能没多想,照着抄就完了。但到了第二章,如果你还是这个态度,迟早要出问题。
STM32的时钟系统是一棵树,不是一排开关。树根是时钟源,树干是各级分频和倍频,树枝才是各个外设的总线。你得先搞清楚自己用的外设挂在哪根树枝上,才知道该操作哪个寄存器。
以常见的STM32F103为例,时钟源主要有四个:
- HSI:内部高速时钟,8MHz,精度一般,但上电就能用,不需要外部器件
- HSE:外部高速时钟,通常接8MHz晶振,精度高,是大多数项目的首选
- LSI:内部低速时钟,约40kHz,主要给独立看门狗用
- LSE:外部低速时钟,32.768kHz,给RTC用
系统时钟SYSCLK的来源可以是HSI、HSE或者PLL倍频后的输出。PLL的存在是为了把8MHz的HSE倍频到72MHz甚至更高,让芯片跑得更快。这里有个关键点:PLL的输入可以来自HSI/2或者HSE,但HSE更稳定,所以工业项目基本都用HSE。
配置时钟的顺序不能乱,我见过太多人上来就写RCC->CFGR,结果程序卡在等待标志位的地方。正确的流程是这样的:
- 打开HSE,等待HSERDY标志位置1
- 配置Flash预取和等待周期(这一步很多人漏掉,导致高频下取指失败)
- 配置PLL的倍频系数和时钟源
- 打开PLL,等待PLLRDY标志位置1
- 切换SYSCLK到PLL输出
- 配置AHB、APB1、APB2的分频系数
每一步都要等标志位,不能跳过。因为时钟切换是硬件行为,需要时间稳定,你不等它,后面的配置就是在错误的时钟下执行的。
2.2 Flash等待周期:那个被90%的人忽略的配置
说到Flash等待周期,这是第二章里最容易被忽略、但后果最严重的一个配置。STM32的Flash读取速度跟不上CPU主频,当SYSCLK超过一定频率时,必须插入等待周期,否则取指会出错,程序直接跑飞。
具体规则是这样的:STM32F103在24MHz以下不需要等待,24到48MHz需要1个等待周期,48到72MHz需要2个等待周期。这个配置在FLASH->ACR寄存器里,低三位是LATENCY。
// 假设目标主频72MHz FLASH->ACR |= (1 << 4); // 使能预取缓冲 FLASH->ACR |= (1 << 3); // 使能指令缓存 FLASH->ACR |= 0x02; // 2个等待周期为什么预取和缓存也要开?因为Flash的读取是按行来的,预取缓冲可以提前把下一行指令取回来,指令缓存则把常用的指令存起来。开了这两个,即使有等待周期,实际性能损失也会小很多。我实测过,72MHz下不开预取和缓存,跑循环密集的代码,性能大概只有开了之后的六成。
注意:等待周期的配置必须在切换SYSCLK之前完成。如果你先切到72MHz再配Flash,中间那段时间CPU就是在错误的时序下取指,程序可能直接HardFault。
2.3 APB1和APB2的分频:不是随便设的
APB1和APB2是两条外设总线,APB2的速度上限比APB1高。在STM32F103上,APB2最高72MHz,APB1最高36MHz。所以当SYSCLK是72MHz时,APB2不分频,APB1要2分频。
这个分频系数在RCC->CFGR的PPRE1和PPRE2位域里配置。PPRE1管APB1,PPRE2管APB2。分频系数从1到16,用三位表示。
为什么要关心这个?因为定时器的时钟不是直接来自APB总线。如果APB的分频系数是1,定时器时钟等于APB时钟;如果分频系数大于1,定时器时钟是APB时钟的2倍。这个规则很多人不知道,导致算出来的定时器频率总是差一倍。
举个例子:SYSCLK=72MHz,APB1分频系数为2,所以APB1时钟是36MHz。但挂在APB1上的定时器(比如TIM2)的时钟是72MHz,不是36MHz。你在算定时器预分频值的时候,要用72MHz去算,不是36MHz。
| 总线 | 最高频率 | 分频系数 | 定时器时钟 |
|---|---|---|---|
| AHB | 72MHz | 1 | - |
| APB1 | 36MHz | 2 | 72MHz |
| APB2 | 72MHz | 1 | 72MHz |
这个表格建议你贴在显示器旁边,配定时器的时候看一眼,能省下大量调试时间。
2.4 外设时钟使能:别忘了那一步
时钟配好了,外设还不一定能用,因为每个外设还有自己的时钟开关。这些开关在RCC->APB1ENR、RCC->APB2ENR、RCC->AHBENR这几个寄存器里。
我个人的习惯是,在初始化任何一个外设之前,先写一行开时钟的代码,然后再去配外设的寄存器。顺序反了的话,外设寄存器写不进去,你还在那儿纳闷为什么配置没生效。
// 开GPIOA时钟 RCC->APB2ENR |= (1 << 2); // 开USART1时钟 RCC->APB2ENR |= (1 << 14); // 开TIM2时钟 RCC->APB1ENR |= (1 << 0);还有一点,开时钟之后最好加几个NOP或者短暂延时,让时钟稳定下来再操作外设寄存器。虽然大多数情况下不延时也没问题,但在低功耗唤醒或者时钟切换的场景下,这个延时能避免一些玄学问题。
3. 中断系统:从寄存器层面把NVIC跑通
3.1 Cortex-M内核的中断和外部中断是两回事
很多人学中断的时候会混淆两个概念:内核异常和外部中断。Cortex-M内核自己有一套异常处理机制,包括复位、NMI、HardFault、SVC、PendSV、SysTick这些,它们的中断号是固定的,从-14到-1(负数表示内核异常)。而外部中断,比如GPIO的EXTI、定时器中断、串口中断,中断号从0开始往上排。
这两套东西最终都汇入NVIC(嵌套向量中断控制器)统一管理。NVIC是内核的一部分,不是外设,所以它的寄存器地址在所有Cortex-M芯片上都是一样的。
NVIC的寄存器主要有这几组:
- ISER:中断使能寄存器,写1使能对应中断
- ICER:中断清除使能寄存器,写1禁用对应中断
- ISPR:中断挂起寄存器,写1手动触发中断
- ICPR:中断清除挂起寄存器
- IABR:中断活跃寄存器,只读
- IPR:中断优先级寄存器,每个中断占8位
这些寄存器都是按位操作的,32位寄存器管理32个中断。STM32F103有60个可屏蔽中断,所以ISER有两位,ISER[0]管中断0到31,ISER[1]管中断32到59。
// 使能EXTI0中断(中断号6) NVIC->ISER[0] |= (1 << 6); // 禁用EXTI0中断 NVIC->ICER[0] |= (1 << 6);注意,ISER和ICER是写1生效,写0无效。你不能用&= ~的方式去清除ISER的位,那样没用。要禁用中断,必须往ICER对应的位写1。
3.2 优先级分组:那个让无数人栽跟头的配置
NVIC的优先级寄存器IPR每个中断占8位,但STM32只用了高4位,低4位保留。这4位又分成两组:抢占优先级和子优先级。分组方式由内核的SCB->AIRCR寄存器的PRIGROUP位域决定。
五种分组方式:
| 分组 | 抢占优先级位数 | 子优先级位数 | 抢占级别数 | 子级别数 |
|---|---|---|---|---|
| 0 | 0 | 4 | 1 | 16 |
| 1 | 1 | 3 | 2 | 8 |
| 2 | 2 | 2 | 4 | 4 |
| 3 | 3 | 1 | 8 | 2 |
| 4 | 4 | 0 | 16 | 1 |
抢占优先级决定能不能嵌套。高抢占优先级的中断可以打断低抢占优先级的中断。子优先级只在多个中断同时挂起时决定谁先执行,不能嵌套。
我踩过的最大的坑是:整个系统只能设置一次优先级分组。你可以在main函数开头调一次NVIC_SetPriorityGrouping(2),之后就不要再改了。如果不同模块各自设置分组,后面的会覆盖前面的,导致优先级行为完全乱套。
还有一个坑:优先级数值越小,优先级越高。这跟直觉相反,很多人第一次配的时候会搞反。抢占优先级0比抢占优先级1高,子优先级0比子优先级1高。
// 设置优先级分组为2(2位抢占,2位子优先级) SCB->AIRCR = (0x5FA << 16) | (2 << 8); // 设置EXTI0中断的优先级:抢占1,子优先级2 // 优先级值要左移到高4位 NVIC->IP[6] = (1 << 6) | (2 << 4);注意AIRCR寄存器的写入需要先写0x5FA到高16位,这是解锁键,不写的话写入无效。这个细节在参考手册里有,但很多人看漏了。
3.3 EXTI外部中断:从引脚到中断服务函数的完整链路
外部中断的配置是第二章里最综合的一个练习,它把GPIO、AFIO、EXTI、NVIC四个模块串在一起。完整链路是这样的:
- 配置GPIO为输入模式
- 开AFIO时钟,配置引脚映射到EXTI线
- 配置EXTI的触发边沿和屏蔽位
- 配置NVIC使能和优先级
- 写中断服务函数
AFIO在这里的作用是"接线员"。STM32的EXTI线只有16条(EXTI0到EXTI15),但GPIO引脚有几十个。AFIO的AFIO->EXTICR寄存器负责把某个引脚的信号接到对应的EXTI线上。比如你想用PA0做外部中断,就要把EXTICR1的低4位设成0;想用PB0,就设成1。
// 开AFIO时钟 RCC->APB2ENR |= (1 << 0); // PA0映射到EXTI0 AFIO->EXTICR[0] &= ~0x0F; // 清除低4位 AFIO->EXTICR[0] |= 0x00; // 选择PA // 配置EXTI0:上升沿触发,不屏蔽 EXTI->RTSR |= (1 << 0); // 上升沿触发 EXTI->IMR |= (1 << 0); // 不屏蔽 // 使能NVIC中的EXTI0中断 NVIC->ISER[0] |= (1 << 6);中断服务函数的名称必须和启动文件里的向量表一致。STM32F103的EXTI0中断服务函数叫EXTI0_IRQHandler。你写错一个字母,中断就进不去,而且编译器不会报错,因为链接器找不到这个符号时会用默认的弱定义。
在中断服务函数里,必须清除中断挂起位,否则中断会反复触发。EXTI的挂起位在EXTI->PR寄存器里,写1清除。
void EXTI0_IRQHandler(void) { if (EXTI->PR & (1 << 0)) { EXTI->PR |= (1 << 0); // 清除挂起位 // 你的处理逻辑 } }提示:清除挂起位之前先判断一下,虽然大多数情况下直接写也没问题,但在中断密集的场景下,判断一下能避免误清其他标志。
3.4 中断服务函数里的那些"潜规则"
中断服务函数不是普通函数,它有几个必须遵守的规则,违反了轻则功能异常,重则系统崩溃。
第一,执行时间要短。中断里不要做延时、不要等标志位、不要跑循环。中断执行时间太长,会阻塞其他中断,导致系统响应变慢。如果确实有耗时操作,在中断里设个标志位,让主循环去处理。
第二,不要调用可能阻塞的函数。比如printf,它内部可能有等待发送完成的逻辑,在中断里调用会导致死锁。串口发送数据用中断方式时,在中断里只做数据搬运,不做格式化输出。
第三,共享变量要加volatile。中断里修改的变量,主循环里读取的,必须加volatile关键字,告诉编译器这个变量可能被意外修改,不要优化它。我见过太多因为没加volatile导致主循环读不到中断更新的案例。
第四,中断服务函数没有返回值,参数是void。这个虽然简单,但初学者容易写成普通函数的形式。
第五,优先级配置要在使能中断之前完成。先配优先级,再开NVIC使能。顺序反了的话,中断可能在优先级还没设好的时候就触发了。
4. 定时器中断:寄存器配置的完整实战
4.1 定时器时钟的计算:那个容易算错的地方
定时器中断是第二章里最实用的内容之一,但定时器时钟的计算是很多人的噩梦。前面说过,APB1上的定时器时钟是APB1时钟的2倍(当APB1分频系数大于1时)。所以TIM2的时钟是72MHz,不是36MHz。
定时器的溢出频率由三个参数决定:时钟频率、预分频值(PSC)、自动重装载值(ARR)。
公式是这样的:
溢出频率 = 时钟频率 / ((PSC + 1) * (ARR + 1))注意PSC和ARR都要加1,因为它们是16位寄存器,实际分频系数是寄存器值加1。PSC=0时不分频,PSC=1时2分频,以此类推。
假设我要1kHz的溢出频率,时钟是72MHz:
1000 = 72000000 / ((PSC + 1) * (ARR + 1)) (PSC + 1) * (ARR + 1) = 72000我可以选PSC=71,ARR=999,这样(PSC+1)=72,(ARR+1)=1000,乘积72000,正好。
TIM2->PSC = 71; // 预分频 TIM2->ARR = 999; // 自动重装载 TIM2->CR1 |= (1 << 0); // 使能计数器这里有个细节:ARR是缓冲寄存器,你写入的值不会立即生效,要等到下一次更新事件才加载到影子寄存器。如果你在定时器运行中修改ARR,新的值会在当前周期结束后生效。这个机制是为了防止修改过程中产生错误的溢出。
4.2 更新中断的使能和清除
定时器溢出时会产生更新事件,如果要触发中断,需要使能更新中断:
TIM2->DIER |= (1 << 0); // 使能更新中断 NVIC->ISER[0] |= (1 << 28); // TIM2中断号是28在中断服务函数里,清除更新中断标志位:
void TIM2_IRQHandler(void) { if (TIM2->SR & (1 << 0)) { TIM2->SR &= ~(1 << 0); // 清除更新中断标志 // 你的处理逻辑 } }注意这里和EXTI不同,TIM的SR寄存器是写0清除,不是写1清除。这个差异很容易搞混,我建议你配完一个外设的中断后,专门查一下它的标志位清除方式。
4.3 预分频和自动重装载的缓冲机制
TIM的PSC和ARR都有缓冲寄存器(影子寄存器)。你写入PSC的值会立即生效,但ARR的值要等到更新事件才生效。这个差异在动态调整定时周期时很重要。
如果你想在运行中改变定时周期,直接写ARR,新的周期会在当前周期结束后生效。但如果你写PSC,它会立即改变分频系数,可能导致当前周期的计时出错。所以动态调整时优先改ARR,不要改PSC。
还有一个细节:ARR的预装载使能位。TIMx->CR1的ARPE位控制ARR是否使用缓冲。如果ARPE=0,写入ARR立即生效;如果ARPE=1,写入ARR在更新事件时生效。默认是0,但很多库函数会把它设成1。
TIM2->CR1 |= (1 << 7); // 使能ARR预装载使能预装载的好处是,你可以在任何时候安全地修改ARR,不会产生中间状态的错误溢出。代价是修改后要等一个周期才生效。
5. 那些手册上不会写的调试经验
5.1 中断进不去?按这个顺序排查
中断进不去是第二章最高频的问题。我总结了一个排查顺序,按这个顺序走,基本能定位到问题:
第一步,查时钟。外设时钟开了吗?AFIO时钟开了吗?时钟没开,外设寄存器写不进去,中断自然不触发。
第二步,查NVIC使能。NVIC->ISER对应的位写了吗?中断号算对了吗?不同型号的STM32中断号可能不一样,要查对应的头文件或参考手册。
第三步,查外设的中断使能。EXTI的IMR位、TIM的DIER位,这些是外设级别的中断开关,不开的话信号到不了NVIC。
第四步,查标志位。中断触发后标志位会置1,如果标志位没置1,说明触发条件没满足。比如EXTI的边沿配置错了,或者GPIO模式配错了。
第五步,查中断服务函数名。名字必须和启动文件里的向量表完全一致。我建议直接从启动文件里复制函数名,不要手打。
第六步,查优先级分组。如果系统里有多处设置优先级分组,后面的会覆盖前面的。确保只在一处设置。
5.2 硬件调试的局限性
用调试器单步跟踪中断代码时,你会发现程序经常不进中断,或者进了中断但行为异常。这不是代码问题,是调试器的问题。
调试器在单步执行时,会暂停内核,中断无法响应。而且调试器本身可能占用一些资源,影响中断的时序。所以调试中断代码时,不要单步跟踪,用断点加变量观察的方式。在中断服务函数里设断点,全速运行,看能不能命中。命中了再逐步缩小范围。
还有一个经验:在中断服务函数开头翻转一个GPIO,用示波器或者逻辑分析仪看波形。这是最直观的调试方式,能看出中断有没有触发、触发频率对不对、执行时间有多长。
5.3 优先级配置的实战建议
优先级分组怎么选?我的建议是:如果系统不复杂,直接用分组2(2位抢占,2位子优先级)。这样有4级抢占优先级,4级子优先级,足够大多数项目用。
如果系统里有严格实时性要求的中断,比如电机控制、高速采样,用分组4(4位抢占,0位子优先级),16级抢占优先级,可以精细控制嵌套关系。
如果系统里中断很少,用分组0也行,但那样就没有抢占嵌套了,所有中断都是平级的。
配置优先级的时候,记住一个原则:越紧急的中断,抢占优先级数值越小。比如故障保护中断设0,通信中断设1,普通定时器中断设2。
注意:不要把所有中断都设成最高优先级。那样等于没有优先级,因为大家都一样,嵌套关系就乱了。
5.4 从寄存器到库函数的思维转换
学完第二章的寄存器操作,你再回头看库函数,会有一种"原来如此"的感觉。比如NVIC_Init()这个函数,它内部就是在操作ISER和IPR寄存器。NVIC_InitTypeDef结构体里的NVIC_IRQChannelPreemptionPriority和NVIC_IRQChannelSubPriority,对应的就是IPR寄存器里的抢占和子优先级位。
我建议你在学完寄存器之后,花点时间把常用库函数的源码翻一遍,看看它到底操作了哪些寄存器。这个过程能帮你把零散的知识串起来,也能让你在库函数出问题的时候,知道从哪里下手排查。
比如GPIO_Init()函数,它内部会先判断引脚号,然后操作CRL或CRH寄存器。如果你知道CRL和CRH的位定义,就能理解为什么配置一个引脚要写那么多代码。这种理解,是单纯会用库函数的人永远体会不到的。
6. 把第二章的知识串起来:一个完整的按键中断案例
6.1 需求拆解与方案设计
假设我们要做一个按键控制LED的功能:按一下按键,LED翻转一次。按键接在PA0,LED接在PC13。用外部中断实现,按键按下时触发中断,在中断里翻转LED。
这个需求涉及的知识点:GPIO输入输出配置、AFIO引脚映射、EXTI边沿检测、NVIC优先级配置、中断服务函数编写。基本把第二章的核心内容都覆盖了。
方案设计上有几个选择需要说明:
按键触发方式选下降沿还是上升沿?这取决于硬件电路。如果按键按下时引脚接地,松开时通过上拉电阻接高电平,那么按下瞬间是下降沿,应该选下降沿触发。如果按键按下时接高电平,就选上升沿。我一般用下降沿触发,配合内部上拉,这样按键松开时引脚是高电平,按下时拉低,产生下降沿。
要不要做消抖?机械按键有抖动,一次按下可能产生多个边沿。硬件消抖加电容,软件消抖在中断里延时或者用定时器。但在中断里延时是大忌,所以我建议用定时器做消抖,或者简单点,在中断里设个标志,主循环里延时处理。
优先级怎么设?按键中断不紧急,抢占优先级设低一点,比如2或3。如果系统里有其他中断,确保按键中断不会阻塞它们。
6.2 寄存器配置的完整代码
// 1. 开时钟 RCC->APB2ENR |= (1 << 2); // GPIOA RCC->APB2ENR |= (1 << 4); // GPIOC RCC->APB2ENR |= (1 << 0); // AFIO // 2. 配置PA0为输入,上拉 GPIOA->CRL &= ~0x0F; // 清除PA0配置 GPIOA->CRL |= 0x08; // 输入模式,上拉/下拉 GPIOA->ODR |= (1 << 0); // 上拉 // 3. 配置PC13为推挽输出 GPIOC->CRH &= ~(0x0F << 20); // 清除PC13配置 GPIOC->CRH |= (0x02 << 20); // 推挽输出,2MHz // 4. PA0映射到EXTI0 AFIO->EXTICR[0] &= ~0x0F; AFIO->EXTICR[0] |= 0x00; // 5. 配置EXTI0下降沿触发 EXTI->FTSR |= (1 << 0); // 下降沿触发 EXTI->IMR |= (1 << 0); // 不屏蔽 // 6. 配置NVIC SCB->AIRCR = (0x5FA << 16) | (2 << 8); // 分组2 NVIC->IP[6] = (2 << 6); // 抢占2,子优先级0 NVIC->ISER[0] |= (1 << 6); // 使能EXTI0中断 // 7. 中断服务函数 void EXTI0_IRQHandler(void) { if (EXTI->PR & (1 << 0)) { EXTI->PR |= (1 << 0); // 清除挂起位 GPIOC->ODR ^= (1 << 13); // 翻转LED } }这段代码里,每一步都有它的道理。开AFIO时钟是因为要配置EXTI映射,不开的话EXTICR写不进去。PA0配上拉是因为按键另一端接地,上拉保证松开时是高电平。EXTI选下降沿是因为按下瞬间产生下降沿。NVIC优先级设2是因为按键不紧急。
6.3 实测中遇到的意外情况
我第一次跑这个代码的时候,按键按下去LED没反应。排查过程是这样的:
先查时钟,确认GPIOA、GPIOC、AFIO的时钟都开了。然后查GPIO配置,PA0的CRL寄存器值是对的。再查EXTI,FTSR和IMR都设了。NVIC的ISER也设了。中断服务函数名也对着启动文件检查了,没问题。
最后发现问题出在优先级分组上。我在另一个文件里调用了库函数的NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4),把分组改成了4。分组4是4位抢占、0位子优先级,我写的NVIC->IP[6] = (2 << 6)在分组4下,抢占优先级是2,子优先级位被忽略了。这本身没问题,但分组被改了之后,我后面设置的IP值可能被覆盖了。
解决方法是把优先级分组统一在一处设置,其他文件不再修改。这个坑让我明白了一个道理:优先级分组是全局配置,只能设一次。
还有一个意外是按键抖动。按下一次,LED翻转了好几次。这是机械按键的抖动导致的,一次按下产生了多个下降沿。后来我在中断里加了一个简单的软件消抖:检测到中断后,延时一小段时间再读引脚状态,确认还是低电平才处理。但延时不能放在中断里,我改成在中断里设标志,主循环里延时处理。
6.4 这个案例还能怎么扩展
这个按键中断的框架可以扩展到很多场景。比如把按键换成红外接收头,就变成了红外遥控解码。换成旋转编码器,就变成了位置检测。换成霍尔传感器,就变成了转速测量。
扩展的时候,核心思路是一样的:配置GPIO、映射EXTI、配NVIC、写中断服务函数。变化的只是触发边沿和中断里的处理逻辑。你把第二章的这些寄存器操作练熟了,后面换什么传感器都是换汤不换药。
我个人的经验是,每学一个新外设,先用寄存器写一遍,再用库函数写一遍。寄存器写一遍让你理解底层,库函数写一遍让你提高效率。两遍下来,这个外设你就真正掌握了。