行业资讯
STM32 GPIO八种工作模式详解:从点灯到按键状态机实战
1. 从点灯开始为什么GPIO是STM32的“第一课”如果你刚拿到一块STM32开发板或者从51单片机、Arduino转过来第一个要跑的程序十有八九是点灯。这几乎成了嵌入式开发界的“Hello World”。但很多人可能没细想为什么偏偏是点灯为什么不是串口打印或者直接操作一个复杂的定时器原因很简单点灯这个动作完美地串联了从硬件连接到软件控制、从寄存器理解到库函数使用的整个链条而这一切的核心就是GPIO。GPIO全称General Purpose Input/Output即通用输入输出口。你可以把它想象成STM32芯片伸向外部世界的“手脚”。这些“手脚”非常灵活既能输出高/低电平去驱动LED、继电器也能读取外部按键、传感器的状态。我刚开始学的时候觉得GPIO配置不就是设置一下方向、速度嘛有什么难的后来在实际项目中因为GPIO配置不当导致系统功耗异常、按键误触发、驱动能力不足等问题接踵而至才真正明白“基础不牢地动山摇”的道理。今天我就结合最经典的LED和按键KEY控制把STM32的GPIO从头到尾、从理论到实践、从标准库到HAL库掰开揉碎了讲清楚。这不仅是点亮一个灯、读取一个键更是理解STM32如何与外界对话的起点。2. GPIO的八种工作模式不只是输入和输出很多教程会告诉你GPIO有输入和输出两种模式。这么说对但也不全对。在STM32的世界里GPIO的模式被细分为八种理解这八种模式的差异是避免后续各种诡异问题的关键。这八种模式可以归为三大类输入类、输出类和复用功能类。2.1 输入类模式如何“听”清外面的世界输入模式的核心任务是准确、稳定地读取外部引脚的电平状态。STM32提供了四种输入模式它们的区别主要在于内部电路结构。1. 浮空输入 (Input floating)这是最“原始”的模式。芯片内部既不上拉也不下拉引脚完全悬空。此时引脚的电平完全由外部电路决定。如果你外接了一个按键按键另一端接地并且没有外接上拉电阻那么当按键松开时引脚就处于一种不确定的“浮空”状态读取的电平可能是0也可能是1甚至会受到电磁干扰而跳动。所以浮空输入通常用于外部已经提供了确定驱动能力的场景比如接另一个MCU的输出或者接有明确上/下拉电阻的电路。2. 上拉输入 (Input pull-up)芯片内部通过一个电阻通常几十K欧姆连接到电源VDD。当外部没有信号驱动时引脚会被内部电阻拉至高电平逻辑1。这非常适合连接一个接地式的按键按键未按下时引脚被内部上拉为高电平按键按下时引脚被直接拉到地变为低电平。这样就省去了外部上拉电阻。3. 下拉输入 (Input pull-down)与上拉输入相反内部电阻连接到地VSS。外部无驱动时引脚被拉至低电平逻辑0。这种模式适合接一个接电源式的按键或传感器。4. 模拟输入 (Analog input)这是为ADC模数转换器或某些模拟比较器准备的。在此模式下GPIO的数字输入部分被完全断开信号直接进入内部的模拟外设。如果你要用引脚来测量电压值比如接电位器、光敏电阻分压就必须配置为模拟输入模式否则数字电路的施密特触发器会“扭曲”模拟信号导致测量不准。注意很多人在使用ADC时读数跳动大第一个要检查的就是GPIO模式是否配置成了模拟输入。配置成普通的输入模式是常见错误。2.2 输出类模式如何“说”得清晰有力输出模式的任务是向外部电路输出一个确定的电平信号。STM32的输出模式有两种区别在于驱动能力和速度。1. 开漏输出 (Output open-drain)你可以把开漏输出想象成一个只有“下拉开关”的电路。当MCU想输出低电平0时内部MOS管导通把引脚拉到地当想输出高电平1时内部MOS管关闭引脚既不接VDD也不接VSS处于高阻态。此时引脚的电平由外部电路决定。开漏输出有两个非常重要的特性线与功能多个开漏输出的引脚可以直接连在一起共同通过一个上拉电阻接到VDD。只要有一个输出低总线就是低只有所有都输出高阻即逻辑1总线才是高。这在I2C等总线协议中至关重要。电平转换由于高电平靠外部上拉产生你可以把上拉电阻接到一个比MCU电压更高的电源上比如MCU是3.3V上拉到5V从而实现不同电压器件间的通信需注意耐压。2. 推挽输出 (Output push-pull)这是最常用、驱动能力最强的输出模式。它内部有一对MOS管一个负责“推”上拉至VDD一个负责“挽”或“拉”下拉至VSS。当输出1时上管导通下管关闭引脚直接连接到VDD当输出0时上管关闭下管导通引脚直接连接到VSS。推挽输出可以提供连续的电流source current和吸收电流sink current能直接驱动LED、蜂鸣器等小功率器件。2.3 复用功能模式当GPIO成为外设的“代言人”STM32的许多强大功能如串口、SPI、I2C、定时器PWM都需要通过特定的引脚来实现。这些引脚在用于这些功能时就需要配置为复用模式。1. 复用开漏输出 (Alternate function open-drain)当该引脚被分配给像I2C这样的外设时外设模块会控制引脚的输出但输出结构采用开漏形式。你需要根据I2C总线要求在外部接上拉电阻。2. 复用推挽输出 (Alternate function push-pull)当引脚被分配给像USART_TX、SPI_MOSI或定时器PWM输出这类外设时通常配置为此模式。外设模块产生信号并通过推挽结构强力输出确保信号质量。模式选择速查表应用场景推荐GPIO模式关键原因与注意事项读取外接上拉电阻的按键按键接地输入浮空 或 输入上拉外部已有上拉用浮空即可用内部上拉可省电阻但两者电阻并联会改变上拉强度。读取外接下拉电阻的按键按键接电源输入浮空 或 输入下拉同上根据外部电路选择。读取无外接电阻的按键按键接地输入上拉必须使用内部上拉否则按键松开时引脚浮空电平不确定。驱动LED阴极接GPIO阳极接VCC推挽输出输出低电平时点亮LEDMCU吸收电流推挽模式下拉能力强。驱动LED阳极接GPIO阴极接GND推挽输出输出高电平时点亮LEDMCU提供电流推挽模式上拉能力强。I2C的SDA/SCL引脚复用开漏输出实现“线与”功能和电平兼容必须在外部接上拉电阻通常4.7K。USART的TX引脚复用推挽输出确保串口信号有足够的驱动能力和边沿速度。USART的RX引脚浮空输入 或 上拉输入通常配置为浮空输入即可如果线路干扰大可考虑弱上拉。用作ADC采集的引脚模拟输入必须配置为此模式否则数字电路干扰会导致采样值不准、跳动。输出PWM波如控制舵机、电机复用推挽输出确保PWM信号有良好的上升/下降沿驱动能力强。3. 硬件连接与电路设计别让软件为硬件背锅在写代码之前硬件电路的设计是基础。一个合理的电路能让软件工作事半功倍而一个有缺陷的电路会让调试过程痛苦不堪。我们分别看看LED和按键的经典电路。3.1 LED驱动电路限流电阻怎么算驱动LED最常用的两种接法低电平有效和高电平有效。低电平有效阴极控制LED阳极接VCC3.3V阴极通过一个限流电阻R接到GPIO引脚。当GPIO输出低电平0时电流从VCC流经LED和电阻到GPIO再到地LED点亮。此时MCU的GPIO处于“吸电流”状态。优点很多MCU的灌电流sink current能力比拉电流source current强这种方式更可靠。且逻辑直观输出0亮1灭。限流电阻计算R (VCC - Vf) / If。Vf是LED正向压降通常红色约1.8V-2.2V绿色/蓝色约3.0V-3.4VIf是你想要的工作电流通常3-20mA查看LED手册。例如VCC3.3V Vf2.0V If10mA 则 R (3.3-2.0)/0.01 130欧姆。取标称值120或150欧姆。高电平有效阳极控制LED阳极通过限流电阻R接到GPIO引脚阴极接地。当GPIO输出高电平1时电流从GPIO流经电阻和LED到地LED点亮。此时MCU的GPIO处于“供电流”状态。电阻计算R (Voh - Vf) / If。Voh是GPIO输出高电平时的电压非常接近VCC如3.2V。计算方式同上。实操心得我习惯用低电平有效接法。一是因为STM32的I/O口吸电流能力通常略强查看数据手册的I/O特性章节二是在多LED系统中如果需要共阳连接控制起来更方便。务必接上限流电阻直接连接会因电流过大烧毁LED或损坏GPIO口。3.2 按键电路与消抖软件硬件如何配合按键的硬件电路核心是上拉或下拉以确保在按键未按下时有一个确定的电平。经典电路按键接地GPIO配置为上拉输入模式。按键一端接GPIO另一端接地。未按下时内部上拉电阻将电平拉高读为1按下时引脚直接接地读为0。是否需要外部电阻如果GPIO配置为内部上拉则可以不接外部电阻。但有些情况下为了更强的抗干扰能力或特定的上拉强度也会并一个外部电阻如10K。注意内部上拉电阻通常约40K和外部电阻是并联关系。按键最大的敌人是抖动。机械触点在闭合或断开的瞬间会因为弹性产生一系列快速的、不稳定的通断这个过程可能持续5-20ms。如果不处理一次按键会被误判为多次。消抖策略硬件消抖利用RC电路的积分效应或施密特触发器的迟滞特性来滤除毛刺。例如在按键两端并联一个0.1uF的电容。优点是节省CPU时间缺点是增加成本和PCB面积对于多个按键不经济。软件消抖最常用检测到按键状态变化后延时10-50ms具体时间需实测再次读取引脚状态如果状态一致则确认按键有效。这是最通用、成本最低的方法。// 一个简单的软件消抖函数示例阻塞式仅用于说明原理 uint8_t KEY_Scan(void) { if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) 0) { // 检测到低电平按下 HAL_Delay(20); // 延时20ms避开抖动期 if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) 0) { // 再次确认 while (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) 0); // 等待按键释放可优化 return 1; // 返回按键有效标志 } } return 0; }在实际项目中我绝不会在主循环里用HAL_Delay这样的阻塞延时来做按键扫描。更优的做法是使用状态机结合系统滴答定时器进行非阻塞式扫描这在后续讲到定时器时再深入。4. 软件驱动从寄存器到HAL库的进化理解了硬件和模式我们来看软件。STM32的GPIO驱动经历了从直接操作寄存器到标准外设库SPL再到硬件抽象层库HAL的演变。我们通过点亮一个LED的例子感受一下这三种方式的区别。4.1 寄存器操作最底层最直接这是最原始的方式直接读写内存映射的寄存器。你需要查阅芯片的参考手册找到GPIO相关寄存器的地址和位定义。GPIOx_CRL/CRH 端口配置寄存器低/高用于设置引脚模式输入/输出/复用/模拟和输出速度。GPIOx_IDR 端口输入数据寄存器读取引脚电平。GPIOx_ODR 端口输出数据寄存器写入数据控制输出电平。GPIOx_BSRR 端口位设置/清除寄存器原子操作常用于单位置1或清0比操作ODR更安全避免读-改-写风险。// 假设LED接在PC13使用寄存器方式点亮低电平点亮 // 1. 使能GPIOC时钟在RCC_APB2ENR寄存器中 *(volatile uint32_t*)(0x40021018) | (1 4); // 置位第4位使能IOPCEN // 2. 配置PC13为推挽输出最大速度2MHz在GPIOC_CRH寄存器中 // CRH控制高8位引脚8-15PC13是第13位属于CRH的[5:4]和[7:6]位域。 // 先清空对应位域再设置为CNF00通用推挽输出MODE01输出模式最大速度2MHz *(volatile uint32_t*)(0x40011004) ~(0xF 20); // 清空PC13的配置位位20-23 *(volatile uint32_t*)(0x40011004) | (0x01 20); // MODE[1:0] 01 // 3. 输出低电平点亮LED使用BSRR寄存器低16位用于置位高16位用于复位 *(volatile uint32_t*)(0x40011010) (1 (13 16)); // 置位BSRR的Bit29(1316)即清除ODR13输出低电平这种方式效率最高代码量最小但可读性极差容易出错且严重依赖特定芯片移植性为零。现在除了在极端资源受限或对性能有苛刻要求的场合基本不再直接使用。4.2 标准外设库SPL经典的平衡点ST官方早期提供的库用结构体和宏定义封装了寄存器操作大大提高了可读性和可移植性。// 使用SPL库操作PC13 LED // 1. 时钟使能 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); // 2. 初始化结构体配置 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_2MHz; // 速度2MHz GPIO_Init(GPIOC, GPIO_InitStructure); // 3. 控制电平 GPIO_ResetBits(GPIOC, GPIO_Pin_13); // 输出低点亮 // GPIO_SetBits(GPIOC, GPIO_Pin_13); // 输出高熄灭 // GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET); // 另一种写法SPL库在效率和易用性之间取得了很好的平衡一度是STM32开发的主流。但ST后来停止了更新转向了HAL/LL库。4.3 硬件抽象层库HAL与LL库现代与高效的选择HAL库是ST目前主推的库旨在提供跨STM32系列的高度抽象接口配合STM32CubeMX工具可以快速生成初始化代码。// 使用HAL库操作PC13 LED假设已用CubeMX生成初始化代码 // 1. 时钟和引脚初始化代码通常在生成的 MX_GPIO_Init() 函数中由CubeMX完成。 // 2. 控制电平 HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); // 输出低点亮 // HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); // 输出高熄灭 // 3. 翻转引脚电平非常实用的函数 HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); // 4. 读取按键假设按键接PA0上拉输入 if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET) { // 按键按下低电平 }HAL库的优点是抽象程度高移植方便生态工具完善CubeMX, CubeIDE。缺点是代码体积相对庞大执行效率稍低。为此ST还提供了LL库Low-Layer它是对寄存器的轻量级封装保留了类似寄存器的效率又提供了更好的可读性。在CubeMX中你可以选择“HALLL”的组合在需要性能的关键部分使用LL库。我的选择建议对于初学者和新项目强烈推荐从HAL库STM32CubeMX开始。它能让你快速搭建项目框架避免在底层硬件配置上耗费过多时间。当你对芯片和外设非常熟悉后如果项目对代码大小或执行效率有严格要求可以再考虑混合使用HAL和LL甚至在某些地方直接优化。5. 实战用状态机实现按键长按、短按与连击掌握了基础的点灯和读键我们来解决一个更实际的问题如何区分按键的短按、长按甚至连击这就需要引入状态机的思想。阻塞式的延时扫描会卡住整个程序而状态机则可以在系统定时器的节拍下非阻塞地运行。我们设计一个简单的按键状态机假设按键接在PA0低电平有效按下为0。5.1 定义按键状态与变量// key.h typedef enum { KEY_STATE_RELEASED, // 按键释放状态 KEY_STATE_DEBOUNCE, // 消抖确认状态 KEY_STATE_PRESSED, // 按下稳定状态 KEY_STATE_LONG_PRESS, // 长按状态 KEY_STATE_REPEAT // 连击状态如果需要 } KeyState_t; typedef enum { KEY_EVENT_NONE 0, KEY_EVENT_SHORT_PRESS, // 短按事件 KEY_EVENT_LONG_PRESS, // 长按事件 KEY_EVENT_REPEAT_PRESS // 连击事件 } KeyEvent_t; // key.c static KeyState_t s_key_state KEY_STATE_RELEASED; static uint32_t s_key_press_duration 0; // 记录按下持续时间以系统滴答数为单位 static uint32_t s_key_last_scan_tick 0; // 上次扫描时间 #define KEY_SCAN_INTERVAL_MS 10 // 按键扫描间隔10ms #define DEBOUNCE_TIME_MS 20 // 消抖时间20ms #define LONG_PRESS_TIME_MS 1000 // 长按判定时间1000ms #define REPEAT_INTERVAL_MS 200 // 连击间隔200ms如果支持连击5.2 状态机扫描函数这个函数需要被周期性地调用例如放在1ms的系统滴答定时器中断里或者主循环中配合HAL_GetTick()进行非阻塞调用。// key.c KeyEvent_t KEY_Scan_StateMachine(void) { uint32_t current_tick HAL_GetTick(); static uint8_t key_repeat_count 0; // 连击计数 KeyEvent_t event KEY_EVENT_NONE; // 非阻塞时间控制每10ms扫描一次 if (current_tick - s_key_last_scan_tick KEY_SCAN_INTERVAL_MS) { return KEY_EVENT_NONE; } s_key_last_scan_tick current_tick; uint8_t key_current_level HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin); // 读取当前电平 switch (s_key_state) { case KEY_STATE_RELEASED: if (key_current_level 0) { // 检测到低电平按下 s_key_state KEY_STATE_DEBOUNCE; s_key_press_duration 0; // 重置按下计时 } break; case KEY_STATE_DEBOUNCE: s_key_press_duration KEY_SCAN_INTERVAL_MS; if (s_key_press_duration DEBOUNCE_TIME_MS) { // 消抖时间到确认按键状态 if (key_current_level 0) { s_key_state KEY_STATE_PRESSED; // 确认按下 // 这里可以触发一个“按下”事件如果需要 } else { s_key_state KEY_STATE_RELEASED; // 是抖动回到释放状态 } s_key_press_duration 0; // 重置计时用于后续长按计时 } break; case KEY_STATE_PRESSED: if (key_current_level 1) { // 按键释放了 // 释放时按下时长小于长按时间判定为短按 if (s_key_press_duration LONG_PRESS_TIME_MS) { event KEY_EVENT_SHORT_PRESS; key_repeat_count; // 短按计数用于连击判断 } s_key_state KEY_STATE_RELEASED; s_key_press_duration 0; } else { // 按键仍处于按下状态 s_key_press_duration KEY_SCAN_INTERVAL_MS; if (s_key_press_duration LONG_PRESS_TIME_MS) { s_key_state KEY_STATE_LONG_PRESS; event KEY_EVENT_LONG_PRESS; // 进入长按状态时触发一次长按事件 // 也可以选择在长按状态下持续触发事件如音量持续增加 } } break; case KEY_STATE_LONG_PRESS: if (key_current_level 1) { // 长按后释放 s_key_state KEY_STATE_RELEASED; s_key_press_duration 0; key_repeat_count 0; // 长按后重置连击计数 } else { // 可以在这里实现长按保持期间的动作例如每200ms触发一次 // if ((s_key_press_duration % 200) 0) { /* 触发连续动作 */ } s_key_press_duration KEY_SCAN_INTERVAL_MS; } break; // 可以扩展连击状态KEY_STATE_REPEAT判断两次短按的时间间隔 default: s_key_state KEY_STATE_RELEASED; break; } // 简单的连击判断在释放状态判断两次短按间隔 // 这部分逻辑可以更复杂单独作为一个状态或放在主循环中判断 // if (event KEY_EVENT_SHORT_PRESS) { // static uint32_t last_short_press_tick 0; // if (current_tick - last_short_press_tick REPEAT_INTERVAL_MS) { // event KEY_EVENT_REPEAT_PRESS; // } // last_short_press_tick current_tick; // } return event; // 返回触发的事件 }5.3 在主循环中应用// main.c int main(void) { // HAL初始化... // 外设初始化... KeyEvent_t key_event; while (1) { key_event KEY_Scan_StateMachine(); switch (key_event) { case KEY_EVENT_SHORT_PRESS: HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); // 短按翻转LED printf(Short Press Detected.\r\n); break; case KEY_EVENT_LONG_PRESS: // 长按执行其他功能比如进入配置模式 printf(Long Press Detected.\r\n); break; case KEY_EVENT_REPEAT_PRESS: printf(Double Click Detected.\r\n); break; default: break; } // 其他任务... } }这个状态机框架清晰地将按键的物理动作电平变化转换成了逻辑事件短按、长按并且整个扫描过程是非阻塞的不会影响系统其他任务的执行。你可以根据需要扩展状态比如加入双击、三击的判断或者让长按期间重复触发动作。6. 进阶话题与避坑指南当你掌握了基本的GPIO控制后在实际项目中还会遇到一些更深入的问题。这里分享几个我踩过的坑和对应的解决方案。6.1 GPIO速度配置不是越快越好在输出模式配置中有一个“GPIO_Speed”选项在HAL库中是GPIO_InitStruct.Speed通常有2MHz, 25MHz, 50MHz等档位。这个速度指的是GPIO输出驱动电路的压摆率即电平翻转的速度。速度越高边沿越陡峭信号高频成分越丰富。什么时候用高速用于高频信号线如SPI、SDIO、USB、高速USART、外部存储器接口等。高速翻转可以减少信号上升/下降时间保证信号完整性。什么时候用低速用于普通的LED、按键、继电器控制等低频场合。低速模式可以显著减少高频噪声辐射和电源噪声降低对系统EMC电磁兼容性的影响有时还能降低一点点功耗。如果你发现系统ADC采样时噪声很大或者电路容易受干扰检查一下那些不必要的高速GPIO把它们降速可能会有奇效。6.2 引脚复用与重映射资源冲突的救星STM32的很多外设如USART、定时器的引脚是固定的但有时这些固定引脚可能被PCB上的其他器件占用了。这时就需要用到重映射功能。例如STM32F103的USART1默认在PA9(TX)/PA10(RX)但可以通过配置AFIO_MAPR寄存器重映射到PB6/PB7。 使用CubeMX可以非常直观地配置重映射。在芯片引脚图上如果某个外设的引脚旁边有一个“菱形”或“复用”标志点击它通常可以看到可用的重映射选项。在代码层面HAL库会自动生成正确的初始化代码。你需要做的就是仔细阅读数据手册的“Alternate function”章节确认你想要的重映射组合是否被芯片支持。6.3 读取ODR与IDR的差异理解“回读”有时你会遇到一个奇怪的现象你用代码设置某个GPIO输出高电平用万用表量也是高电平但用HAL_GPIO_ReadPin读回来的却是0。这可能是因为你读的是输入数据寄存器IDR而不是输出数据寄存器ODR。IDR反映的是引脚上实际的电平。这个电平可能来自外部电路如果你配置为开漏输出且外部没有上拉即使ODR写了1引脚实际也是浮空的IDR读回来的可能就是不确定的值可能是0。ODR是你希望输出的电平。 如果你需要确认输出状态应该去读ODR寄存器HAL库没有直接函数可能需要用GPIOx-ODR。更常见的做法是输出模式下去读IDR来判断实际引脚电平通常是为了检测外部短路或过载等情况。6.4 驱动能力与灌电流点亮多个LED的考量数据手册里会给出GPIO口的最大拉电流和灌电流如±25mA。但要注意这是单个引脚的极限值并且所有引脚的总电流还有全局限制如VDD/VSS总电流不超过150mA。当你需要驱动多个LED或者一个较大电流的器件时并联LED需谨慎不要直接将多个LED并联后用一个GPIO驱动。即使LED参数一致微小的差异也会导致电流分配不均有的很亮有的很暗甚至烧毁。应该每个LED单独串联限流电阻再用GPIO分别控制。使用晶体管或MOS管扩流当驱动电流超过GPIO能力时如驱动继电器、电机、大功率LED灯带务必使用三极管或MOS管作为开关。GPIO仅提供控制信号微安级电流由外部电源通过晶体管提供大电流。这是硬件设计的基本原则。检查电源大电流负载会导致电源电压瞬间跌落可能引起MCU复位。必要时在负载电源端加大电容储能。GPIO是STM32与物理世界交互的桥梁看似简单却蕴含着从硬件设计到软件架构的诸多细节。从正确选择工作模式到设计稳定的外围电路再到编写高效、可靠的驱动代码每一步都需要仔细考量。希望这篇长文能帮你打下坚实的基础在后续学习更复杂的定时器、中断、通信协议时能够更加得心应手。记住点灯不是终点而是你探索STM32广阔世界的第一步。
郑州网站建设
网页设计
企业官网