资讯动态

STM32寄存器级开发实战:GPIO与外部中断EXTI深度解析

发布时间:2026/10/19 1:09:12 来源:尧图企业网站定制
1. 为什么还要啃寄存器手册1.1 从一次调不通的串口说起前阵子帮一个刚入行的朋友看代码他用的是一块STM32F103C8T6的最小系统板想点亮一个LED并且通过串口打印一句“hello”。代码是用标准外设库写的编译没报错下载进去LED也闪了但串口助手那边死活收不到数据。他折腾了一整天换了三根USB转串口线重装了两次驱动甚至怀疑是芯片坏了。我让他把工程发过来打开一看问题出在串口初始化的部分。他调用了库函数USART_Init()参数填得也没错波特率9600、8位数据、无校验、1位停止位。但问题是他忘了使能USART的时钟也忘了配置GPIO的复用推挽输出模式。库函数封装得再好底层还是要去操作RCC_APB2ENR和GPIOB_CRH这些寄存器。他以为调了库函数就万事大吉结果底层寄存器没配对外设根本没工作。这件事让我想起自己刚学STM32的时候也是被各种库函数惯坏了直到有一次遇到一个库函数不支持的功能才被迫去翻参考手册一个位一个位地看寄存器定义。从那以后我养成了一个习惯不管用不用库每配置一个外设都要把对应的寄存器过一遍。这个过程一开始很痛苦但熬过第三章之后你会发现后面越来越顺。1.2 寄存器手册到底该怎么读很多人拿到STM32的参考手册看到几百页的寄存器描述就头大。其实读寄存器手册是有方法的不是从第一页翻到最后一页。我的经验是分三步走第一步先看外设的功能框图。比如你要配置GPIO就先翻到GPIO那一章看它的框图搞清楚信号从哪进、从哪出有哪些控制信号。这一步不需要记任何寄存器名字只需要理解数据流。第二步看寄存器映射表。每个外设章节后面都会有一个寄存器映射表列出了这个外设所有寄存器的地址偏移和复位值。你不需要背下来但要知道有哪些寄存器各自负责什么功能。第三步针对你要用的功能找到对应的寄存器仔细看每一位的定义。比如你要配置PA8为推挽输出那就去看GPIOA_CRH寄存器的第0到第3位这四位叫MODE8和CNF8分别控制输出模式和输出速度。这个顺序很重要先宏观再微观先理解功能再抠位定义。很多人一上来就盯着位定义看看完就忘了这个寄存器在整个外设里是干嘛的效率很低。提示读寄存器手册的时候手边一定要放一份芯片的Datasheet因为参考手册讲的是外设功能而引脚复用、电气特性这些信息在Datasheet里。两份文档对照着看才不会漏掉关键信息。1.3 第三章在整个学习路径中的位置如果把STM32的学习比作盖房子第一章是认识砖头水泥芯片架构、存储器映射第二章是学会看图纸时钟树、复位和电源管理第三章就是真正开始砌墙——GPIO和外部中断。这一章之所以关键是因为它是你第一次直接操控硬件引脚第一次感受到“我写的代码真的改变了物理世界”。GPIO是STM32最基础也最常用的外设没有之一。你点的每一颗LED、按的每一个按键、驱动的每一块屏幕底层都是GPIO在干活。而外部中断则是从“轮询”到“事件驱动”的思维跃迁学会了EXTI你才能理解后面定时器中断、串口中断、DMA中断的运作方式。这一章的内容看起来简单但坑特别多。比如GPIO的模式配置有8种组合什么时候用推挽输出、什么时候用开漏输出、什么时候用上拉输入、什么时候用浮空输入这些选择背后都有电路原理支撑。再比如外部中断的触发方式上升沿、下降沿、双边沿选错了就会出现“按一次键进两次中断”的诡异现象。我见过太多人在这一章栽跟头然后回头去补模电数电的知识。其实不用那么麻烦你只需要把寄存器手册里关于GPIO和EXTI的部分吃透再动手做几个实验自然就理解了。下面我就按照自己的学习路径把这一章的核心内容拆开来讲。2. GPIO寄存器深度拆解2.1 GPIO的8种工作模式及其电路原理STM32的GPIO有8种工作模式这个数字听起来很多但其实是由两个配置位组合出来的。每个引脚在CRL或CRH寄存器里占4个位高两位是CNF低两位是MODE。MODE决定输出速度或输入模式CNF决定具体的电路结构。先看输入模式。当MODE为00时引脚处于输入模式此时CNF的取值决定输入类型CNF00模拟输入。这个模式把引脚直接连到ADC或DAC不经过施密特触发器也不经过上下拉电阻。如果你要用ADC采集电压就必须配成这个模式。CNF01浮空输入。引脚通过施密特触发器连到输入数据寄存器但没有上下拉电阻。这个模式适合外部已经有上下拉电阻的场景比如I2C总线。CNF10上拉输入。内部上拉电阻约40kΩ使能引脚默认是高电平。按键一端接引脚、另一端接GND时用这个模式。CNF11下拉输入。内部下拉电阻使能引脚默认是低电平。按键一端接引脚、另一端接VCC时用这个模式。再看输出模式。当MODE不为00时引脚处于输出模式MODE的值决定最大输出速度MODE01最大10MHz。适合低速信号比如LED、蜂鸣器。MODE10最大2MHz。适合中速信号比如普通串口。MODE11最大50MHz。适合高速信号比如SPI、SDIO。CNF在输出模式下决定输出类型CNF00通用推挽输出。可以输出高电平和低电平驱动能力强是最常用的输出模式。CNF01通用开漏输出。只能输出低电平高电平需要外部上拉电阻。这个模式适合I2C总线和电平转换。CNF10复用推挽输出。引脚控制权交给片上外设比如USART的TX引脚。CNF11复用开漏输出。同样交给外设但输出结构是开漏比如I2C的SCL和SDA。这里有个容易混淆的点通用输出和复用输出的区别。通用输出是你直接写ODR寄存器来控制引脚复用输出是外设自动控制引脚你写ODR没用。很多人配置串口的时候忘了把TX引脚设成复用推挽结果外设工作了但引脚没输出就是这个原因。注意开漏输出模式下如果你不接外部上拉电阻引脚只能输出低电平高电平会处于高阻态。我见过有人用开漏输出驱动LED结果LED一直亮着不灭就是因为没有上拉电阻引脚无法拉高。2.2 CRL和CRH寄存器的位操作技巧GPIOx_CRL控制引脚0到7GPIOx_CRH控制引脚8到15。每个引脚占4个位所以一个32位寄存器刚好管8个引脚。这个设计很规整但操作起来需要一点位运算的技巧。假设你要把PA8配置成推挽输出、最大50MHz。PA8对应CRH的第0到第3位。推挽输出的CNF0050MHz的MODE11所以这4位应该是0011也就是0x3。但你不能直接写GPIOA-CRH 0x3因为这样会把其他引脚PA9到PA15的配置全部清零。正确的做法是先清零这4位再写入新值GPIOA-CRH ~(0xF 0); // 清零PA8的4个配置位 GPIOA-CRH | (0x3 0); // 设置PA8为推挽输出、50MHz如果你要配置PA9就左移4位GPIOA-CRH ~(0xF 4); // 清零PA9的4个配置位 GPIOA-CRH | (0x3 4); // 设置PA9为推挽输出、50MHz这个“先清零再置位”的模式在寄存器操作里非常常见一定要养成习惯。直接赋值会破坏其他位的状态导致一些莫名其妙的问题。对于CRL寄存器操作方式完全一样只是控制的引脚不同。比如配置PA0为浮空输入CNF01、MODE00所以4位是0100也就是0x4GPIOA-CRL ~(0xF 0); // 清零PA0的4个配置位 GPIOA-CRL | (0x4 0); // 设置PA0为浮空输入这里有个小技巧如果你要配置多个引脚可以先把所有要清零的位一次性清零再一次性置位。比如同时配置PA8和PA9GPIOA-CRH ~((0xF 0) | (0xF 4)); // 清零PA8和PA9 GPIOA-CRH | ((0x3 0) | (0x3 4)); // 都设为推挽输出、50MHz这样写出来的代码更简洁执行效率也更高。不过要注意运算符的优先级和|的优先级低于所以括号不能省。2.3 IDR和ODR寄存器的读写时机IDR是输入数据寄存器ODR是输出数据寄存器。这两个寄存器都是32位的但STM32F103只用了低16位对应16个引脚。高16位保留读出来是0。读IDR的时候你拿到的是引脚在当前时刻的电平状态。但要注意IDR的值是经过施密特触发器整形后的数字信号不是模拟电压。如果你配的是模拟输入模式IDR的值没有意义因为施密特触发器被旁路了。写ODR的时候你设置的是引脚的输出电平。但前提是引脚被配置成了通用输出模式。如果引脚是复用输出模式你写ODR不会影响引脚状态因为引脚控制权在外设手里。这里有一个很实用的技巧ODR寄存器支持位带操作可以单独写某一位而不影响其他位。比如你要翻转PA5的电平可以这样写GPIOA-ODR ^ (1 5); // 翻转PA5或者用位带别名区#define PA5_OUT (*((volatile uint32_t *)0x42210194)) PA5_OUT !PA5_OUT; // 翻转PA5位带操作的好处是原子性不会被中断打断。如果你用^的方式在“读-改-写”的过程中如果来了中断而中断里也修改了ODR就可能出现竞争。位带操作直接写别名区硬件保证原子性更安全。不过位带操作的地址计算比较麻烦需要查表或者用宏定义。对于初学者来说先用^的方式也没问题等遇到竞争问题了再换位带。提示读IDR之前建议先读一次ODR然后再读IDR。这是参考手册里推荐的做法目的是清除读取过程中的毛刺。虽然实际项目中很少遇到这个问题但养成这个习惯没坏处。2.4 BSRR和BRR寄存器的原子操作优势BSRR是位设置/清除寄存器BRR是位清除寄存器。这两个寄存器是为了解决ODR读-改-写竞争问题而设计的。BSRR的高16位用于清除位低16位用于设置位。写1有效写0无效。比如你要把PA5置高同时把PA6拉低GPIOA-BSRR (1 5) | (1 (6 16));这一条语句就完成了两个操作而且是原子性的不会被中断打断。如果用ODR你需要GPIOA-ODR | (1 5); // 置高PA5 GPIOA-ODR ~(1 6); // 拉低PA6这两条语句之间可能被中断打断如果中断里也操作了PA5或PA6结果就不确定了。BRR寄存器只有低16位有效写1清除对应的位。比如拉低PA6GPIOA-BRR (1 6);BRR的功能其实是BSRR高16位功能的子集有了BSRR之后BRR就有点冗余了。但在一些老代码里还能看到BRR知道它的作用就行。在实际项目中我建议统一用BSRR来操作输出引脚不管是置高还是拉低一条语句搞定既简洁又安全。特别是驱动LCD或者LED点阵这种需要频繁翻转引脚的应用BSRR的优势非常明显。3. 外部中断EXTI配置全流程3.1 EXTI的架构与信号流向EXTI是外部中断/事件控制器它管理着STM32上所有的外部中断线。STM32F103有19条中断线其中EXTI0到EXTI15对应GPIO引脚EXTI16对应PVD输出EXTI17对应RTC闹钟EXTI18对应USB唤醒。每条中断线都可以独立配置触发方式上升沿触发、下降沿触发、双边沿触发。触发后可以产生中断CPU执行中断服务函数或者事件触发其他外设比如ADC转换。EXTI的架构可以分成三个部分边沿检测电路、中断屏蔽寄存器和挂起寄存器。边沿检测电路监测输入信号的变化当检测到指定的边沿时会置位挂起寄存器的对应位。如果中断屏蔽寄存器没有屏蔽这条线就会向NVIC发出中断请求。这里有一个关键点EXTI的输入信号来自GPIO但GPIO和EXTI之间的连接不是固定的。PA0、PB0、PC0都连接到EXTI0但同一时刻只能有一个引脚连接到EXTI0。这个选择是通过AFIO_EXTICR寄存器配置的。很多人配置外部中断的时候忘了配置AFIO的时钟也忘了设置EXTICR寄存器结果中断死活不进。我当年第一次用外部中断的时候就是卡在这个地方查了半天才发现AFIO时钟没开。3.2 AFIO_EXTICR寄存器的引脚映射AFIO_EXTICR有4个寄存器分别是EXTICR1到EXTICR4每个管4条中断线。EXTICR1管EXTI0到EXTI3EXTICR2管EXTI4到EXTI7以此类推。每个中断线占4个位取值0到4分别对应PA、PB、PC、PD、PE。假设你要把PA0配置为EXTI0的来源RCC-APB2ENR | RCC_APB2ENR_AFIOEN; // 使能AFIO时钟 AFIO-EXTICR[0] ~(0xF 0); // 清零EXTI0的配置位 AFIO-EXTICR[0] | (0x0 0); // 选择PA0如果你要选PB0就把最后一句改成(0x1 0)。选PC0就是(0x2 0)以此类推。这里有个容易忽略的点AFIO_EXTICR的索引是从0开始的EXTICR[0]对应EXTICR1EXTICR[1]对应EXTICR2。用数组下标的方式写代码更简洁也不容易搞错。注意配置AFIO_EXTICR之前必须先使能AFIO时钟。AFIO时钟在RCC_APB2ENR的第0位。忘了开这个时钟写EXTICR不会有任何效果而且不会报错非常隐蔽。3.3 中断屏蔽与挂起寄存器的配合EXTI_IMR是中断屏蔽寄存器写1允许中断写0屏蔽中断。EXTI_EMR是事件屏蔽寄存器功能类似但产生的是事件而不是中断。EXTI_RTSR和EXTI_FTSR分别控制上升沿和下降沿触发。配置一条中断线的完整流程是使能AFIO时钟。配置GPIO为输入模式上拉、下拉或浮空。配置AFIO_EXTICR选择引脚来源。配置EXTI_RTSR或EXTI_FTSR设置触发边沿。配置EXTI_IMR使能中断。配置NVIC使能对应的中断通道。EXTI_PR是挂起寄存器当检测到指定边沿时硬件会自动置位对应的位。在中断服务函数里你必须手动清除这个位否则中断会一直触发。清除的方法是写1不是写0EXTI-PR | (1 0); // 清除EXTI0的挂起位写1清除是STM32的常见设计目的是避免读-改-写操作影响其他位。如果你写0什么都不会发生。我见过有人在中断服务函数里忘了清挂起位结果程序一进中断就出不来了CPU一直在执行中断服务函数。这种问题用调试器一看EXTI_PR的值就能发现但如果不熟悉寄存器可能会卡很久。3.4 NVIC优先级分组与中断嵌套NVIC是嵌套向量中断控制器负责管理所有中断的优先级和嵌套。STM32F103的NVIC支持16个优先级但实际只用了高4位所以优先级范围是0到15数值越小优先级越高。优先级分成抢占优先级和响应优先级两组分组方式由SCB-AIRCR寄存器的PRIGROUP位决定。常用的分组方式是NVIC_PriorityGroup_2即2位抢占优先级、2位响应优先级。抢占优先级决定中断能否嵌套。如果两个中断的抢占优先级相同高响应优先级的中断不能打断低响应优先级的中断只能等它执行完再执行。如果抢占优先级不同高抢占优先级的中断可以打断低抢占优先级的中断。配置NVIC的代码通常用库函数NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure);但如果你要直接操作寄存器需要写NVIC-ISER使能中断写NVIC-IP设置优先级。ISER有3个寄存器每个管32个中断EXTI0_IRQn的值是6所以它在ISER[0]的第6位。直接操作NVIC寄存器比较繁琐而且容易出错所以大部分情况下用库函数就够了。但你要知道库函数底层做了什么这样遇到问题的时候才能排查。4. 实战按键控制LED的完整实现4.1 硬件电路分析与引脚分配假设我们有一块STM32F103C8T6最小系统板上面有一个用户LED接在PA5一个用户按键接在PA0。LED是低电平点亮按键按下时PA0接地松开时PA0通过外部上拉电阻接VCC。等等这里有个问题如果PA0外部已经有上拉电阻那我们在STM32内部就不需要再开上拉。但如果外部没有上拉电阻就必须开内部上拉否则引脚浮空电平不确定。我一般会先看原理图确认外部有没有上下拉电阻。如果没有就在代码里开内部上拉或下拉。对于按键接地的场景开内部上拉配置为上升沿触发按键松开时触发或下降沿触发按键按下时触发。这里我选择下降沿触发因为按键按下时PA0从高变低产生下降沿。LED接在PA5配置为推挽输出、最大50MHz。引脚分配如下引脚功能模式触发方式PA0按键输入上拉输入下降沿PA5LED输出推挽输出不适用4.2 寄存器级代码实现与注释下面是不用任何库函数纯寄存器操作的完整代码#include stm32f103xb.h void LED_Init(void) { // 使能GPIOA时钟 RCC-APB2ENR | RCC_APB2ENR_IOPAEN; // 配置PA5为推挽输出、最大50MHz GPIOA-CRL ~(0xF 20); // 清零PA5的配置位 GPIOA-CRL | (0x3 20); // 推挽输出、50MHz // 初始状态熄灭LEDPA5置高 GPIOA-BSRR (1 5); } void KEY_Init(void) { // 使能GPIOA和AFIO时钟 RCC-APB2ENR | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN; // 配置PA0为上拉输入 GPIOA-CRL ~(0xF 0); // 清零PA0的配置位 GPIOA-CRL | (0x8 0); // 上拉输入 // 选择PA0作为EXTI0的来源 AFIO-EXTICR[0] ~(0xF 0); AFIO-EXTICR[0] | (0x0 0); // 配置EXTI0为下降沿触发 EXTI-FTSR | (1 0); // 使能下降沿触发 EXTI-RTSR ~(1 0); // 禁止上升沿触发 // 使能EXTI0中断 EXTI-IMR | (1 0); // 配置NVIC NVIC_SetPriority(EXTI0_IRQn, 1); NVIC_EnableIRQ(EXTI0_IRQn); } void EXTI0_IRQHandler(void) { // 检查是否是EXTI0触发 if (EXTI-PR (1 0)) { // 清除挂起位 EXTI-PR | (1 0); // 翻转LED GPIOA-ODR ^ (1 5); } } int main(void) { LED_Init(); KEY_Init(); while (1) { // 主循环空转所有逻辑在中断里处理 } }这段代码有几个关键点需要解释第一RCC-APB2ENR的IOPAEN位是第2位AFIOEN位是第0位。用位定义宏比直接写数字更清晰。第二GPIOA-CRL的第20到23位控制PA5。0x3表示MODE1150MHz、CNF00推挽输出。第三GPIOA-CRL的第0到3位控制PA0。0x8表示MODE00输入、CNF10上拉输入。第四AFIO-EXTICR[0]的第0到3位选择EXTI0的来源。0x0表示PA0。第五EXTI-FTSR的第0位使能下降沿触发EXTI-IMR的第0位使能中断。第六中断服务函数里必须先清挂起位再执行其他操作。清挂起位用|而不是因为写1清除。4.3 中断服务函数的编写规范中断服务函数有几个必须遵守的规范第一函数名必须和启动文件里的向量表一致。EXTI0_IRQHandler这个名字是固定的写成EXTI0_Handler就不会被调用。第二函数不能有参数和返回值。中断是硬件触发的没有调用者所以不能有参数。返回值也没有意义因为硬件不关心。第三函数要尽量短。中断服务函数执行期间同级和低优先级的中断会被阻塞。如果函数执行时间太长会影响系统的实时性。我一般建议中断服务函数里只做标志位设置具体处理放到主循环里。第四必须清除中断标志。对于EXTI就是清EXTI-PR的对应位。对于定时器就是清TIMx-SR的对应位。忘了清标志中断会反复触发。第五共享变量要加volatile。如果中断里修改了一个变量主循环里也要读这个变量那这个变量必须声明为volatile否则编译器可能优化掉主循环里的读取操作。提示如果你在中断服务函数里调用了库函数要确认这个库函数是“可重入”的。有些库函数内部用了静态变量在中断里调用可能会破坏主循环的状态。最安全的做法是中断里只操作寄存器不调用任何库函数。4.4 按键消抖的寄存器级实现机械按键在按下和松开的瞬间会产生抖动通常持续5到20毫秒。如果不处理按一次键可能会触发多次中断。消抖有两种方式硬件消抖和软件消抖。硬件消抖是在按键两端并联一个0.1uF的电容利用电容的充放电特性滤除抖动。这种方式简单可靠但会增加物料成本。软件消抖是在中断里启动一个定时器延时10毫秒后再读取引脚状态。如果状态和触发时一致就认为是有效按键。这种方式不需要额外硬件但会占用一个定时器。用寄存器实现软件消抖的思路是在EXTI中断里关闭EXTI中断启动一个10毫秒的定时器。定时器中断里读取PA0的状态如果还是低电平就确认按键按下翻转LED然后重新使能EXTI中断。void EXTI0_IRQHandler(void) { if (EXTI-PR (1 0)) { EXTI-PR | (1 0); // 清挂起位 EXTI-IMR ~(1 0); // 关闭EXTI0中断 // 启动定时器210ms后触发 TIM2-CNT 0; TIM2-ARR 7200 - 1; // 72MHz / 10000 7200 TIM2-CR1 | TIM_CR1_CEN; // 使能定时器 } } void TIM2_IRQHandler(void) { if (TIM2-SR TIM_SR_UIF) { TIM2-SR ~TIM_SR_UIF; // 清更新中断标志 TIM2-CR1 ~TIM_CR1_CEN; // 关闭定时器 // 再次读取PA0状态 if (!(GPIOA-IDR (1 0))) { GPIOA-ODR ^ (1 5); // 翻转LED } EXTI-IMR | (1 0); // 重新使能EXTI0中断 } }这段代码里TIM2-ARR的值是7200-1因为系统时钟是72MHz定时器预分频器设为0所以计数频率是72MHz。要定时10毫秒需要计数72000次。但ARR是16位的最大65535所以需要设置预分频器。这里为了简化假设预分频器已经设为9计数频率是7.2MHz10毫秒需要72000次还是超了。实际项目中需要根据时钟树仔细计算。消抖时间的选择也有讲究。太短了消抖不彻底太长了响应迟钝。我一般用10到20毫秒实测下来大部分按键都能稳定工作。如果按键质量比较差可以适当延长到30毫秒。5. 常见问题与排查技巧实录5.1 中断不触发的排查思路中断不触发是最常见的问题排查起来有一套固定的流程第一步确认时钟使能。GPIO时钟、AFIO时钟、EXTI时钟EXTI不需要单独使能时钟它挂在APB2上但AFIO需要。用调试器查看RCC-APB2ENR的值确认对应位是1。第二步确认GPIO模式。输入模式配错比如配成了输出模式引脚电平不会随外部变化自然不会有边沿。用调试器查看GPIOx-CRL或CRH的值。第三步确认AFIO映射。AFIO-EXTICR的值决定了哪条中断线连到哪个引脚。如果配错了比如把PA0配成了PB0那PA0的边沿不会触发EXTI0。第四步确认触发边沿。EXTI-RTSR和FTSR的值决定了触发方式。如果按键按下是下降沿但你配的是上升沿那就不会触发。第五步确认中断屏蔽。EXTI-IMR的对应位必须是1。如果忘了使能边沿检测电路会置位EXTI-PR但不会向NVIC发中断请求。第六步确认NVIC使能。NVIC-ISER的对应位必须是1。如果忘了使能EXTI发了中断请求但NVIC不会转发给CPU。第七步确认中断服务函数名。必须和启动文件里的向量表一致。如果名字写错了链接器不会报错但中断触发时会跳转到默认的无限循环。这七步走下来99%的中断不触发问题都能找到原因。我一般会从第一步开始用调试器逐个检查寄存器的值而不是凭感觉猜。5.2 按键触发两次中断的原因分析按键按一次触发两次中断通常有三个原因原因一抖动。机械按键在按下瞬间会产生多个边沿如果没消抖就会触发多次中断。解决方法是在中断里加延时或者用定时器消抖。原因二边沿配置错误。如果同时使能了上升沿和下降沿触发按键按下和松开都会触发中断。解决方法是在EXTI-RTSR和FTSR里只使能一个边沿。原因三清挂起位的时机不对。如果在中断服务函数里先执行了耗时操作再清挂起位那在耗时操作期间如果又来了一个边沿挂起位会再次被置位导致中断再次触发。解决方法是进中断后立刻清挂起位。我遇到过一次比较诡异的情况按键触发两次中断但抖动和边沿配置都没问题。后来用示波器看波形发现按键松开时有一个很慢的下降沿从高电平到低电平用了将近1毫秒。这个慢边沿被EXTI检测到了所以触发了一次中断。解决方法是在按键两端并联一个电容加快边沿速度。5.3 GPIO输出无反应的检查清单GPIO配置成输出模式后引脚没有反应检查清单如下检查项可能问题解决方法时钟GPIO时钟未使能设置RCC-APB2ENR对应位模式配成了输入模式检查CRL/CRH的MODE位类型配成了开漏输出但无上拉改用推挽输出或加上拉电阻复用配成了复用输出改用通用输出引脚引脚被其他外设占用检查是否有重映射硬件LED极性接反检查原理图焊接引脚虚焊用万用表测通断这张表我一般贴在工位上遇到问题就逐项排查。大部分情况下是时钟没使能或者模式配错了。还有一个容易忽略的点如果引脚配成了复用输出但外设没有使能引脚会保持最后的状态。比如你把PA9配成了复用推挽USART1_TX但USART1没有使能PA9会输出高电平。这时候你写ODR没用因为引脚控制权在USART1手里。5.4 寄存器操作中的位运算陷阱寄存器操作离不开位运算但位运算有几个常见的陷阱陷阱一运算符优先级。和|的优先级低于和但高于。所以GPIOA-CRL | 0x3 20是合法的但GPIOA-CRL | 0x3 20 0xF就会先算再算结果不对。保险的做法是加括号。陷阱二符号扩展。如果操作数是char或short左移时可能会符号扩展。比如char c 0x80; c 1的结果是0x00而不是0x100。解决方法是用unsigned类型或者强制转换。陷阱三读-改-写竞争。GPIOA-ODR | (1 5)这条语句包含读、改、写三个操作。如果在读和写之间来了中断而中断里也修改了ODR那中断的修改会被覆盖。解决方法是关中断或者用BSRR。陷阱四写1清除。STM32的很多寄存器是写1清除比如EXTI-PR、TIMx-SR。如果你习惯性地用~来清除反而清不掉。一定要看参考手册里的说明确认是写1清除还是写0清除。陷阱五保留位。有些寄存器的某些位是保留的读出来是0写进去必须写0。如果你用|操作可能会把保留位写成1导致不可预期的行为。保险的做法是先读出来清零要改的位再写入。这些陷阱我在实际项目中都踩过有些坑花了好几个小时才找到原因。现在写寄存器代码的时候我会特别小心每一条位运算语句都要在脑子里过一遍。5.5 从寄存器到库函数的思维转换学会了寄存器操作之后再用库函数会有一种“原来如此”的感觉。比如GPIO_Init()函数底层就是在操作CRL和CRH。GPIO_ResetBits()底层就是写BRR或BSRR。但库函数不是万能的。有些功能库函数没有封装比如位带操作、某些寄存器的特殊位。这时候就需要直接操作寄存器。我的建议是常用功能用库函数提高开发效率特殊功能用寄存器保证灵活性。还有一个经验调试的时候尽量看寄存器。库函数封装了太多细节出问题的时候看库函数源码很费劲。直接看寄存器一眼就能看出哪里配错了。比如串口不发送数据看USART-CR1的TE位是不是1看GPIOx-CRH的CNF是不是复用推挽几秒钟就能定位问题。从寄存器到库函数再从库函数回到寄存器这是一个螺旋上升的过程。刚开始学寄存器很痛苦但熬过这一章后面学定时器、串口、DMA都会轻松很多。因为你知道底层是怎么工作的库函数只是帮你省了几行代码而已。最后分享一个我个人的小习惯每配置一个新外设我都会在代码注释里写上对应的寄存器地址和位定义。比如// RCC-APB2ENR bit2: IOPAEN, 使能GPIOA时钟 RCC-APB2ENR | (1 2); // GPIOA-CRL bits 20-23: PA5配置 // MODE511 (50MHz), CNF500 (推挽输出) GPIOA-CRL | (0x3 20);这样过一段时间回头看代码不用翻手册就能想起当时的配置思路。特别是接手别人的项目时这种注释能省很多时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑