ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32外设DeInit()函数详解:为何必须与Init成对使用

STM32外设DeInit()函数详解:为何必须与Init成对使用 简介一份讲解STM32中DeInit()函数作用与必要性的PDF资料面向嵌入式开发者和高校单片机学习者。文档围绕“为什么每个STM32模块都提供DeInit()”这一常见疑惑先厘清Init()负责配置工作模式、波特率、中断等并启动模块而DeInit()则负责恢复初始状态、清除配置、关闭电源并释放资源二者并非功能重复随后归纳了初始化一致性、功耗与处理器资源管理、调试器下载时序、异常复位后的安全状态恢复、可重用性以及多任务模块复用等六个典型场景特别解释了在main()函数开始时先执行DeInit()以关闭上次残留外设让调试器能够有充足时间完成初始化和下载操作。资源为单篇PDF文档共1个文件、体积约27KB篇幅紧凑、逻辑清晰。目前已有2966人浏览学习适合在阅读固件库代码、设计外设上下电流程或排查重复初始化问题时快速查阅。1. 为什么几乎所有STM32外设库都会留一个DeInit()函数很多刚接触STM32的朋友第一次打开标准外设库或者HAL库的时候都会有这样一个疑问既然有了Init()函数为什么还要一个DeInit()我见过不少人在项目里从头到尾只调Init从不碰DeInit代码也跑得好好的。直到某天遇到一个“莫名其妙”的bug查了半天才发现是外设没有彻底复位导致的。说个我自己的经历。之前做一个多传感器采集板板上同时挂了三个UART外设程序运行一段时间后需要切换串口的引脚映射。当时图省事直接改了GPIO配置想着重新Init一次就可以了。结果切换之后串口数据偶尔会出现乱码而且不是每次都复现排查了整整两天。最后翻手册才发现问题出在USART外设内部的状态寄存器还保留着上一次的配置说白了就是外设没有被“干净地”关掉就急着把它当另一个设备来用。这就是DeInit()存在的意义它负责把外设“打回原形”让外设寄存器恢复到复位时的默认状态避免旧配置对新功能产生干扰。听起来很简单但真正用好它背后涉及芯片复位机制、外设时钟管理、引脚复用配置、HAL库的分层设计等一系列知识点。这篇文章我想把这些东西一次说清楚尤其是那些文档里不会明说、但实际开发中特别容易踩的坑。2. DeInit()函数存在的根本原因2.1 芯片级复位与外设级复位的区别先明确一个容易被忽略的概念复位有层次之分。STM32上电或者按复位键属于系统级复位它会复位整个芯片所有外设寄存器全部回到复位值。但系统级复位的方式比较粗暴而且会打断正在运行的程序。更多时候我们希望只复位某一个外设而不是把整个芯片都重启一遍。这个时候就需要DeInit()这种外设级的“定点复位”。另外还有一类场景程序里用NVIC_SystemReset()做了软复位但软复位之后如果代码是从main函数重新开始跑的那外设寄存器其实已经被复位了和上电没什么两样。但如果你是通过跳转的方式回到某个初始化函数而不是真正触发系统复位那外设里的残留配置就会一直存在。这两种情况的差别恰恰是很多人搞不清楚DeInit到底有没有必要的原因。2.2 从寄存器层面看DeInit到底干了什么以标准外设库为例随便打开一个XXX_DeInit()函数比如GPIO_DeInit()你会发现它的核心操作就是把GPIOx的寄存器值写回复位值然后重新使能GPIO外设时钟。写回复位值这一步很关键它会把GPIO配置寄存器CRL、CRH恢复到默认状态ODR清零也就是把所有引脚恢复成“未初始化”的浮空输入状态。再比如USART_DeInit()除了把USART的SR、DR、BRR、CR1、CR2、CR3等寄存器全部清零之外还会操作RCC_APB2PeriphResetCmd()或者RCC_APB1PeriphResetCmd()先让外设进入复位状态再退出复位状态。这一步在标准库里叫“外设时钟复位”它的作用是让外设内部的逻辑电路彻底复位属于比较深层次的复位操作。HAL库的做法类似但稍有不同。HAL_XXX_DeInit()第一步同样会触发外设的硬件复位通过操作RCC里的外设复位寄存器让外设先进入复位态再解除复位。这样外设内部的移位寄存器、状态机、FIFO指针等硬件逻辑都会回到初始状态。第二步会调用一个回调函数HAL_XXX_MspDeInit()把跟这个外设相关的底层资源引脚、时钟、中断、DMA全部释放掉。所以DeInit的本质就是把外设从“已配置、工作中”的状态拉回到“未初始化”的状态。它不仅仅是关掉外设而是彻底清理外设留下的所有“痕迹”。3. 标准库和HAL库中DeInit的实现差异3.1 标准外设库一切以寄存器为中心早期使用标准外设库开发DeInit的逻辑很直白。以GPIO为例void GPIO_DeInit(GPIO_TypeDef* GPIOx) { // 先恢复寄存器默认值 GPIOx-CRL 0x44444444; // 所有引脚设为浮空输入 GPIOx-CRH 0x44444444; GPIOx-ODR 0x0; // 再对GPIO外设时钟做一次复位 if(GPIOx GPIOA) { RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOA, DISABLE); } // GPIOB、GPIOC等同理 }看到没先写寄存器再操作RCC复位。顺序不能反因为如果先复位时钟再写寄存器寄存器写入的值也保不住。这种实现方式的好处是简单、可控、依赖少坏处是每个外设的DeInit都是定制的代码重复度比较高而且开发者必须清楚每个寄存器的复位值是多少。3.2 HAL库DeInit被拆成了两层HAL库的DeInit设计思路完全不同。它把清理工作拆成了两层一层是HAL层自己负责的“外设核心复位”也就是触发外设时钟复位让外设硬件回到初始状态另一层是用户层负责的MSPMCU Support Package初始化回调专门处理引脚、时钟、中断、DMA这些与具体硬件板卡相关的资源。HAL_StatusTypeDef HAL_UART_DeInit(UART_HandleTypeDef *huart) { // 检查句柄是否合法 if (huart NULL) return HAL_ERROR; // 第一步关闭UART外设 __HAL_UART_DISABLE(huart); // 第二步触发外设时钟复位 UART_FORCE_RESET(huart); // 设置RCC复位寄存器 UART_RELEASE_RESET(huart); // 清除RCC复位寄存器 // 第三步调用MSP回调释放引脚/中断/DMA HAL_UART_MspDeInit(huart); // 第四步将句柄状态改为复位状态 huart-gState HAL_UART_STATE_RESET; huart-RxState HAL_UART_STATE_RESET; return HAL_OK; }再往下用户需要自己实现HAL_UART_MspDeInit()在这里把GPIO引脚改回模拟输入或者浮空输入关闭UART对应的中断释放DMA通道甚至停掉DMA的时钟void HAL_UART_MspDeInit(UART_HandleTypeDef* huart) { GPIO_InitTypeDef GPIO_InitStruct {0}; if(huart-Instance USART1) { // 关闭USART1中断 HAL_NVIC_DisableIRQ(USART1_IRQn); // 释放引脚恢复默认状态 HAL_GPIO_DeInit(GPIOA, GPIO_PIN_9 | GPIO_PIN_10); // 关闭DMA如果用了DMA HAL_DMA_DeInit(huart-hdmatx); HAL_DMA_DeInit(huart-hdmarx); } }这种分层设计的好处是职责清晰。HAL层负责“外设本身”用户层负责“外设和外部世界的连接”。用CubeMX生成代码的时候它会自动帮你生成对仗的MspInit和MspDeInit这也就是为什么很多人用CubeMX生成工程后从没手动写过DeInit代码但函数却一直在工程里存在的原因。3.3 Init和DeInit必须成对出现这里要特别强调一个原则Init和DeInit最好成对出现。很多开发者习惯在初始化时只关注Init把MspInit里做的事情当作“理所当然”却忽略了这些资源是需要释放的。如果你在初始化时开启了某个引脚的复用功能在DeInit时却没有把它恢复下次再用这个引脚做普通GPIO输出就会因为复用功能残留导致电平拉不动、波形异常等问题。实际的工程里正确的写法往往是这样的模式// 使用外设A之前 MX_USART1_UART_Init(); // ...使用串口... // 需要切换到外设B时 HAL_UART_DeInit(huart1); // 彻底清理串口 MX_TIM2_Init(); // 再初始化定时器 // ...使用定时器...4. DeInit真正派上用场的5个常见场景4.1 引脚复用切换这是DeInit最典型的应用场景。STM32的大多数引脚都是复用引脚同一个引脚可以映射到USART、TIM、I2C、SPI等多个外设。比如PA9既能做USART1_TX也能做TIM1_CH2。如果你想把PA9从“串口发送”切换成“定时器PWM输出”只调HAL_GPIO_Init()重新配置引脚是不够的。因为USART1外设还在运行它仍然占用着这个引脚的输出控制权。正确做法是先调用HAL_UART_DeInit()停掉串口再调用HAL_TIM_PWM_Init()初始化定时器。中间这一层DeInit不只是清理GPIO更重要的是让USART外设停止对该引脚的占用。这类问题在需要动态切换功能的产品里特别常见比如同一个调试口既想当串口下载用又想当普通IO控制LED用。没有DeInit的话大概率会碰到“引脚切换不彻底”的诡异问题。4.2 软复位之后的外设状态确认用IWDG独立看门狗或者调用NVIC_SystemReset()做软复位之后有些开发者会担心外设是不是真的回到了初始状态。事实上系统级复位会把所有外设寄存器复位到默认值理论上不需要再手动DeInit。但有一种情况例外——如果复位之后你没有重新执行完整的初始化流程而是希望通过“恢复现场”的方式快速恢复业务那就必须确认每一个外设的当前状态。有些外设的复位值并不等于“关闭”状态而是“默认配置”状态。比如某些定时器的预分频器复位值不为零如果你不重新Init就直接使用时序就会出错。稳妥的做法是在重新初始化外设之前先DeInit一次。这样做虽然多花几个时钟周期但换来的是一次彻底干净的重置。尤其是在产品需要支持“远程复位升级”这类功能时宁可多调用一次DeInit也不要去赌外设复位之后的状态。4.3 低功耗模式唤醒后的重新初始化STM32进入Stop模式或者Standby模式之后外设状态的处理方式和时机差别很大。Standby模式下所有外设都会掉电唤醒后一切都要重新初始化。这种情况你反而不需要DeInit因为外设本来就完全复位了。但Stop模式就不一样了。Stop模式下外设的时钟会被停止但寄存器里的配置还在唤醒后外设会从停止前的状态继续运行。如果你在进入Stop模式之前特意关闭了某个外设唤醒之后又希望它能以一套全新参数工作那先DeInit再Init就是最稳妥的方式。否则你可能遇到“外设看起来没反应”的情况——其实就是因为外设寄存器里的状态和你期望的不一致。低功耗项目里还有一个常见操作是在进入低功耗之前把用不到的引脚全部配置成模拟输入或者高阻态来降低漏电。唤醒之后再恢复功能。这个过程中DeInit特别是GPIO_DeInit几乎是必须的因为它能快速把引脚恢复成默认低功耗状态。4.4 通信外设的错误恢复通信外设和普通外设最大的不同在于它有自己的状态机和总线协议。比如I2C总线一旦从设备拉低SCL或者SDA导致总线锁死光重新初始化I2C主机没有用因为总线已经被占住了。这个时候的常规做法是先DeInit掉I2C外设把SCL和SDA引脚释放出来然后手动翻转SCL时钟把总线上的从设备状态机“冲”出来最后再重新Init I2C。这个过程中DeInit的意义不仅仅是复位外设更是把引脚控制权交还给GPIO让你能用软件方式去“救”总线。还有UART通信中如果接收端检测到持续的错误帧比如波特率不匹配导致的一连串乱码外设内部的错误标志位会堆积。虽然可以通过软件清除标志位但更保险的方式是DeInit之后重新Init让外设内部的移位寄存器和状态机彻底重置。这样能避免一些隐藏很深的边缘情况。4.5 模块动态重新配置很多STM32应用不是固定功能的而是需要运行时动态改变外设的工作模式。比如同一个SPI外设一段时间在驱动Flash存储芯片另一段时间在驱动LCD屏幕。虽然两者共用SPI协议但速率、模式、DMA通道都可能不同。如果直接在旧配置的基础上修改新的Init参数有可能出现配置残留。知道了SPI1之前开启了硬件NSS而新设备并不需要这个引脚如果不用DeInit硬件NSS的控制逻辑还在新设备的通信就可能会异常。这类场景的通用做法是切换设备之前先DeInit掉当前外设释放所有引脚和DMA资源然后再按新设备的配置走一遍Init流程。动态配置和静态初始化的本质区别就在这里静态初始化只需要管好自己动态配置还得处理好“上一任”外设留下的烂摊子。5. 不调用DeInit会带来哪些后果每次在技术群里看到有人问“为什么我的代码跑着跑着就不对了”我第一反应就是问他有没有用DeInit。很多人觉得DeInit是可有可无的东西但实际出问题的时候往往就是因为它。第一种后果是外设状态残留。最典型的例子是DMA。DMA传输完成之后DMA的传输计数器、中断标志位、数据流配置都还留在寄存器里。如果你直接修改DMA配置而不先DeInit下一次传输可能会把上一次的数据长度或者地址偏移一起算进去导致数据传输错位。这类bug的诡异之处在于它不是每次必现而是和上一次传输的结果强相关非常难排查。第二种后果是引脚状态不受控。比如你初始化了USART的TX引脚它是推挽复用输出高电平为默认空闲状态。如果你不DeInit就直接把这个引脚改成普通GPIO输出并且期望它输出低电平你会发现引脚可能始终是高电平怎么拉都拉不下来。原因就是USART外设还在控制这个引脚。很多硬件工程师反映“GPIO输出不听话”排查到最后往往都是外设复用的锅。第三种后果是中断悬空。有些外设的中断标志位在硬件复位之前是无法清除的或者需要特定操作顺序才能清除。如果你没有DeInit外设又恰好挂在一个使能的中断通道上那就会进入“中断触发-标志位未清-再次触发”的死循环程序卡死在中断服务函数里出不来。这类问题在调试时最折磨人因为从代码逻辑上看中断服务函数的写法完全正确问题其实出在初始化阶段。我整理了一个简单的对比表方便你判断什么情况下可以不调用DeInit使用场景是否建议DeInit原因系统上电后第一次Init不需要外设本来就是复位状态同一个外设重新Init视情况只有参数变化时可以省略但先DeInit更保险不同外设共用引脚必须释放引脚控制权通信外设故障恢复强烈建议清空状态机和错误标志Stop模式唤醒后视情况如果期望全新配置建议先DeInitStandby模式唤醒后不需要外设已经完全复位6. DeInit使用的实战建议与常见误区6.1 DeInit不是万能的这是最容易被误解的地方。DeInit只能复位外设本身但如果你在初始化时手动配置了EXTI外部中断或者用了某个GPIO的外部中断功能这些并不一定属于外设DeInit的管辖范围。HAL库的MspDeInit通常只会处理和外设直接绑定的中断和DMAEXTI相关配置还是需要你自己手动清理。另外DeInit不会帮你清理软件层面的状态。比如你自己定义的一个结构体变量记录了外设的工作模式和数据长度DeInit之后这个变量并不会自动归零。所以在实际代码里DeInit往往需要配合软件标志位的重置一起使用。6.2 DeInit和Init的时序问题有一个细节容易被忽略DeInit之后外设时钟处于“未使能”状态。在HAL库中HAL_XXX_Init()的第一步通常会自动使能外设时钟所以DeInit之后直接调Init没有问题。但在标准外设库中有些外设的Init函数并不会自动开时钟需要手动调用RCC_APBxPeriphClockCmd()来使能。所以如果你从标准库迁移到HAL库千万不要想当然地以为“我DeInit之后原来外设的时钟还开着”。HAL库的DeInit会通过MspDeInit释放时钟源标准库则不会至少在旧版本标准库中GPIO_DeInit会重新使能APB2时钟但其他外设不一定。最靠谱的做法是DeInit之后严格按初始化流程重新走一遍不要做任何“隐式假设”。6.3 一个通用的DeInit操作模板实际工程中我建议把DeInit和Init封装成一对对称接口避免在业务代码里到处散落DeInit调用。以HAL库的UART为例void APP_UART_Enable(void) { MX_USART1_UART_Init(); // 初始化串口 } void APP_UART_Disable(void) { HAL_UART_DeInit(huart1); // 复位串口外设 // 额外释放软件资源 uart1_rx_len 0; uart1_rx_state IDLE; }这样做的好处是业务代码里只需要调用APP_UART_Enable和APP_UART_Disable不需要关心底层细节。以后如果要换用其他串口只需要改这两个函数内部实现业务代码完全不用动。这也是我前几年做代码重构时总结的经验宁可在封装层多花一点心思也不要在业务代码里堆细节。6.4 利用调试工具验证DeInit是否生效最后分享一个排查技巧。如果你不确定DeInit是否真正生效可以在调试模式下通过寄存器窗口直接观察外设的关键寄存器值。以USART为例DeInit之后USART的CR1寄存器应该为0x0000SR寄存器应该为0x00C0复位值TC和TXE位为1。如果你的CR1寄存器不为零说明DeInit没有完全执行或者中途又被其他代码修改了。如果觉得手动看寄存器太麻烦也可以写一个简单的自检函数在DeInit之后读取几个关键寄存器做断言判断void ASSERT_UART_RESET(USART_TypeDef* USARTx) { if (USARTx-CR1 ! 0x0000) { // 打印错误或者进入错误处理 Error_Handler(); } }这是我踩过几次坑之后养成的习惯。用下来的感受是这种“防御式”的自检代码在开发阶段确实会多花一点时间但相比你后期耗费两天去定位一个莫名其妙的外设问题这点成本的投入非常划算。7. 写在最后的一点体会回到文章开头的问题Stm32为什么需要模块的DeInit()函数说白了嵌入式开发本质上就是在和“状态”打交道。初始化是在创建设备状态DeInit是在销毁状态。很多人只关注前者却忽略了后者同样重要。一个健壮的系统不应该只考虑“怎么跑起来”更要想清楚“怎么停下来”“怎么切换”“怎么恢复”。我个人在项目里已经形成了一种习惯凡是写了Init的地方如果这个外设可能在运行时被关闭或重新配置就一定会配套写DeInit。这样做初期确实会多写几行代码但从长期维护的角度看它帮你规避的bug数量远超这几行代码的成本。希望这篇分享能帮你在以后的项目里少踩几个外设残留配置的坑。如果你在实践过程中还有其他DeInit相关的奇奇怪怪的问题欢迎一起交流。本文还有配套的精品资源点击获取
返回列表