资讯详情

资讯详情

STM32F103C8点灯工程实战:从GPIO配置到时钟与PWM的嵌入式入门

简介针对STM32F103C8微控制器的一款LED点阵屏驱动演示工程采用HUB08接口连接32x64双色点阵屏以静态刷新方式显示图像适合嵌入式初学者或LED显示应用开发者参考。工程代码涵盖系统时钟与GPIO初始化、显示缓冲区管理、HUB08协议数据传输等核心环节目录按Bsp、App、Libraries等模块划分便于学习与二次开发。压缩包共74个文件主体为31个h头文件和30个c源文件另有8个汇编启动文件、Keil工程配置uvprojx/uvoptx及辅助脚本包体仅273KB结构轻量完整。已有2011人学习下载对理解STM32的GPIO控制、定时器配置、中断处理以及点阵屏静态驱动方案有直接帮助也可在此基础上扩展为动态扫描或图文显示项目。1. 为什么拿到 STM32F103C8 的第一步是点 LED从压缩包到第一个可复现实例拿到一块 STM32F103C8 最小系统板绝大多数人的第一个动作不是翻数据手册而是先找一个能跑的 LED 示例把它点亮。这不是偷懒是嵌入式开发里最省时间的验证方式MCU 能启动、时钟能跑、编译链通、下载链路正常一盏灯就能全部证明。这个压缩包里装的正是这样一个基础工程——基于标准外设库的 GPIO 输出 Demo目标芯片是 72MHz 主频的 STM32F103C8T6。里面把引脚配置、时钟使能、延时逻辑都拆成了可以直接修改的代码块。新手照着一路点灯能走通全流程已入门的开发者可以跳过基础解释直接看电路匹配和引脚复用这两处最容易翻车的细节。2. 拆开 Demo 包先搞懂工程结构再谈烧录2.1 目录里各文件夹的真实分工解压这个 rar 之后第一眼看到的是若干个按功能命名的文件夹。许多第一次接触 STM32 工程的人会被这些目录吓到其实它们的分工非常固定。常见的标准外设库工程会包含 CORE、LIB、SYSTEM、HARDWARE、USER、OBJ 这几个部分其中 USER 是入口CORE 放启动文件和内核相关头文件LIB 是官方外设库HARDWARE 是你自己写的外设驱动OBJ 是编译输出目录。先看一眼 USER 目录里的主工程文件Keil MDK 工程文件的后缀是 .uvprojx。双击它之前建议先确认 MDK 的版本和芯片支持包。这个 Demo 的工程文件是用 MDK 5 创建的MDK 4 打开会提示版本过高强行打开也编译不过。另一个容易忽略的文件是 stm32f10x_conf.h它控制着外设库中哪些模块被编译。这个头文件里有一大堆#include stm32f10x_xxx.h如果你的工程用不到某个外设注释掉对应 include 可以减少编译时间但这个 Demo 里点亮 LED 只需要 GPIO 和 RCC所以保留这两个即可。CORE 目录里的 startup_stm32f10x_hd.s 是启动文件这里要特别说明为什么选 hd 而不是 ld 或 md。STM32F103C8T6 虽然只有 64KB Flash但它的 Flash 容量落在高密度区间启动文件要根据芯片具体型号去选选错的话程序会跑飞而且这种问题用仿真器都不太好查。LIB 目录放的是 stm32f10x.c 和 stm32f10x.h这是原厂提供的标准外设库源码它把寄存器操作封装成函数和结构体方便上层直接调用。HARDWARE 目录里通常放 led.c 和 led.h这是这个 Demo 的核心驱动。SYSTEM 目录一般放延时、串口这类通用模块这里面的 delay.c 值得单独看后面我专门讲延时。这个工程的编译输出默认指向 OBJ 目录。打开工程文件后按 F7 编译如果一切正常OBJ 里会出现 .axf、.hex 和 .map 三个关键文件。.hex 是烧录文件.map 是内存分布图。很多人只关心 .hex 是否生成成功忽略了 .map。实际上看 .map 能快速确认 Flash 占用、RAM 占用、哪个函数体积最大尤其在板载 Flash 只有 64KB 的芯片上养成看 .map 的习惯比瞎调代码更有效。2.2 把工程导入 MDK 并完成首次编译打开工程前先做环境准备。安装 MDK 5 之后还需要装对应芯片型号的 Device Family Pack否则打开 .uvprojx 会弹窗提示找不到设备。这个 Pack 可以直接在 MDK 的 Pack Installer 里找到选择 STM32F1 系列下载即可。装完 Pack 再打开工程文件在 Options for Target 的 Device 标签页里确认当前芯片是 STM32F103C8否则编译时连接的启动文件和链接脚本可能不匹配。导入工程后先不要急着写代码按 F7 做一次全量编译。如果编译器报错优先看头文件路径有没有配置。标准外设库工程常见的头文件路径包括 CORE、LIB、SYSTEM、HARDWARE、USER 这五个目录只要少配置一个编译就会报出stm32f10x.h: No such file or directory。这个 Demo 在压缩包里已经写好了相对路径所以解压后一般不会遇到这个问题但如果你把整个工程移动到了中文路径下或者路径层级变了就要重新配置 include path。第一次编译通常会有几个 warning这个 Demo 里最常出现的是warning: #1-D: last line of file ends without a newline不影响功能。真正要在意的是编译结束后 Output 窗口显示0 Error(s), 0 Warning(s)还是0 Error(s), N Warning(s)。0 Error 只代表语法和链接通过不代表硬件一定正常但它至少证明工具链是通的。还有一点编译成功后如果你在 OBJ 目录里找不到 .hex 文件打开 Options for Target 的 Output 标签页确认 Create HEX File 被勾选。这个选项默认是不勾的很多新手卡在“编译成功却烧不了”这一步其实就是 hex 没有被生成。工程能编译通过之后下一步是连接硬件。这里有一个接线的关键点Demo 默认的 LED 引脚是 PC13这是很多最小系统板板上 LED 的位置。如果你的板子 LED 接在 PA1 或者其他引脚上直接烧录会出现灯不亮但程序其实已经跑起来了。遇到这种情况不要急着改电路先看原理图确认 LED 接在哪个 GPIO 上再去 led.c 里改宏定义。这个 Demo 把引脚定义都集中在头文件里改一处就能全局生效比在代码里到处搜 GPIO 宏要省事得多。中间还有一个容易被忽略的环节电源。USB 供电在只带一块板子的时候完全够用但如果板上又接了传感器又接了 OLEDUSB 口的 5V 转 3.3V 就可能压不住表现是下载偶尔失败灯亮度偏低。遇到这种问题不要先去怀疑代码先量一下 3.3V 引脚的电平是否稳定。点灯 Demo 的功耗确实很小但它验证的恰恰是整个链路是否健康。3. 点灯不是置位引脚那么简单时钟、GPIO 与延时的三层关系3.1 时钟树里找 GPIO 外设时钟点灯代码里第一句不是操作 GPIO而是打开 GPIO 外设的时钟。这是 STM32 与 51 单片机最大的区别之一STM32 的外设默认是不上电的想让某个外设工作必须先使能它的时钟。很多人点灯失败就是因为忘了开 RCC 时钟导致后续所有寄存器操作都写进了空气。在这个 Demo 里开时钟的代码一般是这样的#include stm32f10x.h void LED_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; // 使能 GPIOC 端口时钟注意时钟挂在 APB2 总线上 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; // 翻转速度 50MHz GPIO_Init(GPIOC, GPIO_InitStructure); // 初始化 PC13 }这段代码的逻辑分三层。先说明时钟层RCC_APB2PeriphClockCmd的第一个参数要指定外设GPIOC 挂在 APB2 上而不是 APB1。APB1 上的外设最大支持 36MHzAPB2 支持 72MHzGPIO 属于高速外设所以挂在 APB2。第二个参数是 ENABLE意思是打开时钟。如果这里写错成RCC_APB1Periph_GPIOC编译会直接报错因为标准外设库的枚举定义里就没有这种组合这是好事可以在编译阶段拦住一部分错误。接下来是模式层。GPIO_InitStructure是一个结构体先给成员赋值再传给GPIO_Init。GPIO_Mode_Out_PP是推挽输出可以输出高电平和低电平驱动能力较强适合直接驱动 LED。还有一个容易被搞混的模式是GPIO_Mode_Out_OD开漏输出开漏模式只能主动拉低不能主动拉高如果用它去点灯LED 会出现“微亮”或者完全不亮的情况。GPIO_Speed_50MHz指的是 IO 翻转速率不是输出频率对点灯来说 2MHz 都足够但为了后续扩展 PWM 或串口统一选 50MHz 更省事。最后一层是执行层。GPIO_Init会把这个结构体里的配置全部写入 GPIOC 的相关寄存器。注意这个结构体是值传递的所以每次配置一个新的引脚组合前都要重新给结构体成员赋值一次。有人图省事新建了一个引脚初始化函数直接把上次的配置复制过来结果发现新引脚不工作一查发现是结构体里GPIO_Pin还是旧值。3.2 GPIO 模式选择与输出控制的两种写法GPIO 配置完成之后控制 LED 亮灭有三种常见写法这个 Demo 里用的是查表式的宏定义把控制逻辑从硬件层面隔离开了。第一种写法是直接操作 ODR 寄存器#define LED_ON() GPIOC-BSRR GPIO_Pin_13 // 置高电平亮 #define LED_OFF() GPIOC-BRR GPIO_Pin_13 // 置低电平灭 #define LED_TOGGLE() GPIOC-ODR ^ GPIO_Pin_13 // 翻转电平BSRR 和 BRR 这两个寄存器的设计很聪明。BSRR 的全称是 Bit Set/Reset Register高 16 位用于复位引脚低 16 位用于置位引脚。往 BSRR 的对应写 1引脚输出高电平写 0 不影响。BRR 专门用于清低电平。硬件上这两个寄存器还支持原子操作不用先读后写比操作 ODR 更安全。很多人习惯直接对 ODR 做异或来翻转电平这在逻辑上是正确的但在多线程或者中断环境下可能出问题如果一个外部中断刚好在你的读和写之间把引脚电平改了你的异或结果就会覆盖掉中断里的修改。点灯场景不会碰到这种竞争但养成用 BSRR/BRR 的习惯没有坏处。第二种写法是标准外设库提供的GPIO_WriteBit函数GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET); // 亮 GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET); // 灭这个函数内部也是操作 BSRR/BRR只是封装成了函数调用。它的可读性比寄存器写法更好但每次调用都有函数压栈出栈的开销。在点灯这种毫秒级操作里完全感觉不到差异但如果放到中断服务函数里高频调用建议直接用寄存器宏定义省掉几拍 CPU 周期。第三种写法是用库函数直接修改 ODRGPIOC-ODR (GPIOC-ODR ~GPIO_Pin_13) | GPIO_Pin_13;这种写法有一个隐患它不是原子操作且语义不直观。我在实际项目中见过有人把这一行复制到多个分支里有的分支把 ~GPIO_Pin_13漏写了导致原本应该清掉的引脚变成只置位不清位。排查这种问题时最浪费时间的地方在于所有分支看起来都“对”但行为就是不符合预期。代码里能用 BSRR/BRR 表达清楚意图的就不要用 ODR 和位运算混搭。LED 初始化函数里还有一点值得观察Demo 在 GPIO 配置之后会立刻把 LED 置为关闭状态。这个动作不是多余的。有些板子的引脚在上电瞬间是默认低电平如果 LED 是低电平点亮那种接法上电瞬间会闪一下。先关后用的逻辑避免了这个现象也让你在调试时能确定当前电平是代码主动控制的而不是上电默认值。3.3 延时函数for 循环不能定时的原因点灯 Demo 里必然会有一个闪烁效果这就牵出另一个核心问题延时怎么实现。新手最容易想到的是 for 循环空转比如写一个delay(5000000)然后盲调到肉眼看着舒服为止。这个做法在工作频率固定的板子上可行但有两个问题一是不同优化等级下循环次数会变二是代码可移植性几乎为零。这个 Demo 的延时模块用的是 SysTick 定时器这是 ARM Cortex-M 内核自带的一个 24 位倒计时定时器。用 SysTick 做延时的核心逻辑如下static uint8_t fac_us 72; // 1ms 需要的 SysTick 时钟数 static uint16_t fac_ms 1000; // 1ms 需要的循环次数 void delay_init(void) { SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); // 此时 SysTick 计数频率 72MHz / 8 9MHz fac_us 9; // 1us 需要 9 个计数 fac_ms (uint16_t)fac_us * 1000; } void delay_us(uint32_t nus) { uint32_t temp; SysTick-LOAD nus * fac_us - 1; // 设置计数值 SysTick-VAL 0; // 清空当前计数 SysTick-CTRL SysTick_CTRL_ENABLE_Msk; // 启动定时器 do { temp SysTick-CTRL; } while ((temp SysTick_CTRL_COUNTFLAG_Msk) 0); // 等待计数归零 SysTick-CTRL 0; // 关闭定时器 }这段代码里最关键的是SysTick_CLKSourceConfig。RCC 配置成 HCLK 的八分频也就是 9MHz这意味着 SysTick 计数器每递减一次耗时 1/9 微秒。LOAD寄存器设置的就是你要延时的长度比如延时 1msLOAD 就写 8999。CTRL寄存器最末尾的 COUNTFLAG 位在计数器从 1 减到 0 时被硬件置 1软件轮询这个标志位就能知道延时是否结束。每次延时结束必须把 CTRL 清零关闭定时器否则下一个延时时 COUNFLAG 会是残留状态导致延时时间减半。这个问题非常隐蔽我见过有人把SysTick-CTRL 0注释掉之后整个工程的延时全部变成原来一半而且不是固定减半是越复杂的代码越明显查起来像玄学。实际使用中不要在每个 LED 翻转循环里自己调用SysTick-LOAD寄存器。标准做法是把上面这段封装成delay_us和delay_ms主程序里只调用函数。SysTick 的 LOAD 寄存器是 24 位的最大计数值 16777215以 9MHz 计数频率算单次最大延时约 1.86 秒。所以delay_ms(2000)这类超长延时函数内部会做循环拆分。这个细节在这个 Demo 里已经处理好了你不需要改但如果自己写延时模块长度上限是你必须考虑的第一件事。还有一个容易被忽略的配置SysTick 的异常优先级。Cortex-M 内核里 SysTick 中断可以设置为最低优先级不影响其他中断响应。这个 Demo 用的是轮询等待方式不依赖 SysTick 中断所以在延时过程中 CPU 一直在忙等任何中断都可以打断等待。点灯场景没有问题但如果之后你需要用同一个工程做按键扫描或者串口接收长时间忙等会带来按键响应迟滞的体验到那时候就值得切换到中断计时的方案了。4. 常见问题排查点灯工程的四个高频坑4.1 现象下载成功但灯完全不亮这个坑的概率最高。下载器报告烧录完成复位后 LED 没有任何动静。排查的第一步是确认 GPIO 配置有没有生效。常见原因是RCC_APB2PeriphClockCmd里时钟使能写错了外设比如芯片上的 LED 实际接在 GPIOB代码里却开了 GPIOC 的时钟。编译不会报错因为参数枚举是合法的CPU 也执行了初始化只是初始化了一个空端口。第二个原因是推挽模式和开漏模式选错。开漏输出模式在没有外部上拉电阻的情况下引脚只能拉低不能主动拉高。如果 LED 是高电平点亮电路上又恰好没有上拉电阻灯就会一直灭着。这不是硬件坏了只是 GPIO 模式定义和电路不匹配。解决方法是先查原理图确认 LED 是低电平点亮还是高电平点亮再回来改GPIO_Mode_Out_PP或GPIO_Mode_Out_OD。第三个原因是厂商封装和最小系统板引脚不标称一致。很多开发板的 LED 标识旁边印的丝印是 D2、D3 之类的编号不直接写 PC13。你以为是代码问题结果用杜邦线一测才发现引脚根本没有连到 LED。这类问题最有效的排查手段是看原理图文件而不是反复重新编译代码。再有一种情况灯确实不亮但用示波器量引脚有电平翻转。这说明 LED 本身或限流电阻有问题。常见做法是先量一下 LED 两端的电压如果是 0V 且芯片引脚有高电平故障就在外接电路上如果引脚就是 0V问题回到代码。4.2 现象下载失败提示连接不到目标设备点灯工程本身没有难度真正卡住新手的是第一次下载。J-Link 下载器接好之后MDK 提示Cannot connect to target这基本是三种原因之一。第一种是 BOOT0 引脚状态不对。STM32F103C8 的 BOOT0 拉高时会从系统存储器启动这个模式常用于串口下载但 J-Link 调试下载时要求 BOOT0 处于低电平。如果你的板子是出厂默认状态BOOT0 跳线帽接在高电平侧J-Link 会连不上。解决方法是把跳线帽拨到低电平一侧重新上电再试。第二种是连接线序出错。J-Link 和开发板之间的 SWD 接口只需要四根线SWDIO、SWCLK、GND、3.3V。实际接线时经常会把 SWDIO 和 SWCLK 接反因为板子上这两个丝印可能被其他元件挡住。接反之后 MDK 的报告并不直接说“线序错了”而是提示找不到目标设备非常容易误导人。我一般会先把四根线按颜色固定然后验证一遍再上电。第三种是供电不足。用 USB 线单独给开发板供电时如果板上同时接了 3.3V 的传感器模块、SD 卡槽、OLEDUSB 口的 5V 转 3.3V 芯片可能已经濒临极限。J-Link 本身也提供 3.3V 输出但两个电源同时供电时电流分配并不平均甚至可能出现电平打架。遇到连接失败先拔掉所有外设只留 J-Link 和开发板最小系统再试一次。多数情况下问题就消失了。4.3 现象烧录成功但复位后程序不自动运行这个坑比前面两个更隐蔽。下载完成后程序可能已经开始运行但只是你没有看到现象也可能真的没跑。区分方法是按一下开发板上的复位键如果按键后 LED 有反应说明程序在运行但下载初始时没有自动复位。MDK 的烧录配置里有 Reset and Run 选项通常位于 Flash Download 页面里勾选之后烧录完会自动复位执行。如果不勾烧录完成后芯片还停在调试暂停状态看起来就像程序没烧进去。如果复位键按下后 LED 仍然没有反应检查启动文件是否选对。startup_stm32f10x_hd.s 与 ld/md 版本的核心差异是中断向量表长度和堆栈设置用错文件可能导致程序启动即 HardFault。判断方法是用调试器单步如果复位后 PC 指针直接跳到了 HardFault_Handler基本就是启动文件或链接脚本匹配出了问题。另一种可能更直接中断向量表里的首个值也就是初始堆栈指针是从 Flash 的 0x00000000 地址加载的。如果工程链接时把只读数据放到了向量表之前整个启动行为就会变得不可预测。BOOT0 跳到系统存储器模式也会导致程序不自动运行。在这种模式下芯片上电后不执行 Flash 里的用户程序而是执行出厂固化的一段引导代码。解决方法是把 BOOT0 拉低再复位。很多开发板出厂前 BOOT0 跳线就是高电平因为生产调试用的串口下载需要这个状态。拿到板子第一件事就是检查跳线帽这是点灯工程最常见的隐性前提。开机不自动运行还有一种情况是被看门狗咬了。如果工程初始化里意外使能了独立看门狗代码在喂狗逻辑之前卡住了芯片就会周期性复位现象是灯闪一下就灭间隔周期恒定。点灯 Demo 一般不含看门狗但如果你是从别的工程拷贝过来的初始代码就可能有遗留的看门狗初始化逻辑。4.4 现象LED 亮度偏低或间接性闪烁抖动灯亮了但亮度比别人同款板子明显暗一截这类问题看起来不像故障往往是最浪费时间调试的。先明确概念STM32 的 GPIO 推挽输出高电平不是稳压源它本质上是一个 MOS 管导通到 3.3V引脚能提供的电流一般几毫安到二十毫安不等。如果 LED 的限流电阻取值过大比如 1kΩ 以上亮度就会明显偏低。这个 Demo 里默认限流电阻通常取 330Ω 到 1kΩ你自己焊板子时一定要先算电流。如果 LED 亮度偏高并伴随微闪检查是不是 GPIO 直接驱动了多个负载。曾经见过一个工程同一个引脚除了接 LED还飞线接了一个继电器模块。GPIO 灌电流超限后引脚输出电压被拉低LED 亮度波动继电器也可能误动作。用万用表量引脚电压发现只有 2.4V 而不是 3.3V这说明引脚过载了要加驱动管或换用高电平有效逻辑。还有一类亮度异常是软件导致的高速翻转 LED利用占空比控制亮度。如果delay_ms的实现不精确或者中断里电灯逻辑和高频任务抢占了 CPU人眼看到的“呼吸”效果就会变成抖动感。这种问题在点灯 Demo 里不常见但只要你开始把延时从循环改成 SysTick 抢占或者改到中断里就要重新审视延时函数的实现细节。5. 从闪灯到呼吸灯用定时器 PWM 把同一个引脚玩出新变化5.1 为什么普通延时做不出平滑呼吸效果LED 闪烁是二值的呼吸灯需要连续变化。如果用delay_ms不断改延时时间和电平状态确实能实现一个缓慢的“亮→灭→亮”循环但你会在渐变过程中看到明显的台阶感。原因是人眼对亮度的感知近似对数关系而普通延时的占空比变化是线性的在线性变化下暗区变化太慢亮区变化又太快。PWM 方式控制亮度时占空比递增本身也要做映射常见做法是用正弦或指数曲线调整占空比而不是线性加一。这个 Demo 的后续扩展例程里就是靠定时器 PWM TIM_SetCompare来做的。5.2 TIM2 输出 PWM 的最小配置把同一个 LED 引脚从 GPIO 普通输出切换成 PWM 输出配置上要改三处开启 TIM2 时钟、把引脚复用为定时器通道、初始化定时器周期和占空比。这里只贴最核心的一段void TIM2_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA1 复用为 TIM2_CH2 GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 899; // 计数周期 900 TIM_TimeBaseStructure.TIM_Prescaler 0; // 预分频 0即 72MHz 计数 TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 450; // 初始占空比 50% TIM_OC1Init(TIM2, TIM_OCInitStructure); // 第 2 通道注意函数名对应 CH2 TIM_Cmd(TIM2, ENABLE); }这段代码的关键点在于 APB1 和 APB2 的时钟来源不同。TIM2 挂在 APB1 上最高 36MHz但示例里用 RCC_PCLK1 给定时器时钟经过倍频后仍是 72MHz 计数频率所以Prescaler 0时计数频率就是 72MHzPeriod 899意味着 PWM 频率约 80kHz。呼吸灯用这么高的频率没有意义常见做法是把Prescaler调到 71让计数频率降到 1MHz再配合 900 的 Period 得到约 1kHz 的 PWM 频率肉眼看不到闪烁同时电流声音也降到可接受范围。占空比渐变就在主循环里调用TIM_SetCompare2每次加 1 并延时几毫秒。从点灯 Demo 走到这一步你已经把 GPIO、时钟树、定时器、PWM 四个基础模块串起来了。我自己的习惯是每新建一个工程都把「外设时钟 → GPIO 复用 → 定时器通道」这三件事事先画成一张表再写代码而不是等灯不亮了再回头猜。这套流程看起来多花十分钟但能省下半天的调试时间。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →