ARTICLE DETAIL

资讯详情

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

GD32F103 PWM输出实战:从原理到代码,彻底搞懂定时器PWM

GD32F103 PWM输出实战:从原理到代码,彻底搞懂定时器PWM 先交代一下这个系列写到第六篇了。前面几篇我们聊了环境搭建、GPIO点灯、串口打印、中断和定时器这次终于轮到PWM了。老实说PWM这东西是单片机入门之后最实用、最能出效果的一个外设它既是“软件控制硬件”的典型代表又是电机驱动、灯光调光、舵机控制、音频输出、电源管理这些实际项目都绕不开的核心能力。这篇我会用GD32F103系列为例从PWM的原理讲起再到定时器资源的分配、寄存器级和库函数级的代码实现最后附上几个可以直接抄的实战源码工程零基础的朋友按着步骤来大概率一次点亮。1. PWM波到底是啥先别急着写代码把物理概念搞明白1.1 从电风扇和调光灯说起你家的调光台灯如果拆开看很多用的就是PWM。它的本质就是把一个方波的“高电平时间”和“低电平时间”做比例分配。比如一个固定频率的方波周期是1ms高电平占0.5ms低电平占0.5ms那这个波形的占空比就是50%。如果高电平占0.8ms低电平占0.2ms占空比就是80%。PWM的全称是Pulse Width Modulation脉冲宽度调制。所谓“调制”调的就是“高电平持续的时间宽度”。它最妙的地方在于很多负载LED灯、直流电机、继电器、加热丝对高频开关并不敏感它们感知到的是“平均电压”。占空比越高平均电压越高灯就越亮、电机转得越快。于是我们用一串数字就能精确控制模拟的物理量根本不需要什么DAC。1.2 频率、占空比、分辨率三个参数必须吃透频率PWM波每秒钟重复多少个周期单位是Hz。频率决定了控制的“细腻度”。LED调光用1kHz以上就没闪烁感了舵机控制要求50Hz电机驱动的频率通常在10kHz~20kHz之间以避开人耳噪声。占空比高电平时间占整个周期的百分比。0%~100%对应0到满幅输出。分辨率占空比能精细到多少档。GD32的定时器是16位的计数范围0~65535所以理论上占空比分辨率可以做到1/65535实际用起来十几位足够。这里要特别强调一点PWM只是“控制信号”它本身不带功率。比如你要驱动一个大功率电机单片机的GPIO输出PWM信号这个信号只是接到MOSFET或者电机驱动芯片的使能脚、输入脚上真正的功率通路由外部的开关管完成。初学者最容易踩的坑就是把电机直接接到单片机引脚上那必烧。1.3 GD32的PWM是怎么“自动”输出的很多人一听PWM就紧张觉得要在中断里翻转电平。实际上完全不用。GD32的定时器内部有一组比较寄存器专门干这件事。你设定好定时器的计数上限ARR自动重装载值再设定比较值CCRcapture/compare register。定时器从0开始往上数计数到CCR的时候输出引脚自动翻转为低电平或者高电平取决于模式计数到ARR的时候计数器清零引脚自动翻回高电平。整个过程中CPU根本不参与真正的硬件级自动化。你只要在需要改变亮度、转速的时候改一下CCR的值就好。这个机制就是PWM的“硬件核心”计数不停比较器自动触发翻转输出端持续出波CPU可以去干别的。理解了这个后面所有代码都好写。2. GD32定时器资源盘点哪个定时器能输出PWM通道怎么选2.1 先看芯片本身有什么我们用的是GD32F103C8T6这颗比较常见的芯片它内部有多个定时器。按功能分大致三类高级定时器TIMER0带有互补PWM输出和刹车功能专门用来驱动三相电机、无刷电机这类需要“死区时间”保护的场景。通用定时器TIMER1、TIMER2、TIMER3不同型号编号略有差异F103系列是TIMER0~TIMER3比较齐全还有TIMER4/5/6这种基础定时器都能输出PWM。基础定时器某些不带输出比较通道只能做定时PWM用不上。每个定时器又带多个通道比如TIMER0有4个通道CH0~CH3外加CH0N、CH1N这样的互补通道TIMER1/2/3各有4个通道。通道就是物理引脚上的映射关系你得查数据手册看引脚号。我这里做了一张常用PWM通道引脚对照表F103C8T6封装定时器通道引脚备注TIMER0CH0PA8高级定时器支持互补TIMER0CH1PA9高级定时器TIMER0CH2PA10高级定时器TIMER0CH3PA11高级定时器TIMER1CH0PA0通用定时器TIMER1CH1PA1通用定时器TIMER1CH2PA2通用定时器TIMER1CH3PA3通用定时器TIMER2CH0PA6通用定时器TIMER2CH1PA7通用定时器TIMER2CH2PB0通用定时器多路复用TIMER2CH3PB1通用定时器多路复用TIMER3CH0PB6通用定时器TIMER3CH1PB7通用定时器TIMER3CH2PB8通用定时器TIMER3CH3PB9通用定时器注意不同厂家的数据手册引脚标注可能有出入而且很多引脚是多功能复用的比如PA9还能做USART1_TX、USB的VBUS等。所以选完定时器和通道之后一定要确认这个引脚没有被别的功能占用否则会出现“信号出不来”“电平不对”这类怪问题。2.2 选定时器的三个基本原则第一个原则能用通用定时器就别用高级定时器。高级定时器的初始化逻辑更复杂多出来的死区、刹车、互补输出这些配置新手没用上反而容易出错。除非你在做电机驱动确实需要互补PWM和刹车保护再上TIMER0。第二个原则通道对应的引脚要避开你已经在用的串口、I2C、SPI。实操中最常见的情况是用PA9做PWM输出结果串口1的TX也在这两边同时初始化引脚复用冲突输出乱套。第三个原则如果有多路PWM需求尽量用同一个定时器的不同通道。因为同一个定时器的所有通道共用同一个计数周期ARR相同同步性天然好初始化代码也能省很多重复劳动。2.3 频率怎么定一个公式解决所有疑问PWM的输出频率由三个参数决定定时器时钟频率CK_APBx、预分频系数PSC、自动重装载值ARR。公式是PWM频率 定时器时钟频率 / (PSC 1) / (ARR 1)。举个具体例子GD32F103的外部晶振通常是8MHz系统时钟跑到72MHzAPB1定时器时钟一般是72MHz。如果我想要一个20kHz的PWM适合电机驱动先定PSC 71即72分频那么计数器的计数脉冲频率就是 72MHz / 72 1MHz。接着要得到20kHz就需要 ARR 1 1MHz / 20kHz 50所以 ARR 49。这样算出来的占空比调节粒度是 1/50也就是每一档变化2%对于电机调速够用了对灯光调光稍显粗糙。如果你想精细调光可以把PSC调小、ARR调大比如 PSC 7、ARR 899这样频率是 72MHz / 8 / 900 10kHz分辨率是1/900调光就很顺滑。占空比对应的比较值计算公式也很简单CCR (ARR 1) × 目标占空比 / 100。比如上面ARR899想输出50%占空比CCR就是 900×50/100 450。记住这两个公式你就能自由地按需求反算出所有参数。别死记硬背库函数里的参数怎么来的原理通了改频率、改占空比就是做算术题。3. 工程实操零基础也能跑通的GD32 PWM输出代码3.1 工程模板准备前面系列文章里我们已经在Keil MDK里基于GD32标准外设库Firmware Library建好了工程模板也验证过能正常点灯和串口打印。这里我直接沿用那个模板只新增PWM相关的配置。建议先确认你手上的开发板是GD32F103C8T6还是别的型号。不同型号的启动文件startup文件和设备头文件可能不同如果是GD32F103CBT6这类管脚和中断向量基本一致可以通用如果是GD32E103或者GD32F303那寄存器地址、外设时钟都有差异代码不能硬搬要看对应的用户手册。打开工程后需要确认标准外设库的头文件路径已经包含常用的有GD32F10x.h芯片寄存器定义system_gd32f10x.c系统时钟配置gd32f10x_timer.h / gd32f10x_timer.c定时器外设驱动函数gd32f10x_gpio.h / gd32f10x_gpio.cGPIO控制gd32f10x_rcu.h / gd32f10x_rcu.c复位时钟单元管理这些文件在GD32官方固件库的Firmware目录里都有模板不用自己写。3.2 初始化流程拆解先开时钟再配引脚最后配定时器PWM初始化的顺序是有讲究的按这个流水线走大部分问题都不会出第一步打开GPIO时钟和定时器时钟。注意GD32的RCU模块里外设时钟是分别控制的尤其要注意定时器挂在哪个总线上。TIMER1挂在APB1上TIMER0挂在APB2上。挂错了总线开了时钟也白搭。第二步配置GPIO引脚为复用推挽输出。这里有个非常关键的细节不能配成普通推挽输出。因为PWM波是定时器外设驱动的引脚必须处于复用功能模式AFIO否则输出不生效。GD32的库函数里用gpio_init()后还要调用gpio_pin_remap_config()处理重映射如果需要的话。第三步配置定时器的时基单元预分频、计数模式、周期。GD32的定时器支持向上计数、向下计数、中央对齐计数PWM常用的是向上计数。第四步配置输出比较模式选择PWM模式1还是模式2。PWM模式1下计数器值小于CCR时输出有效电平通常是高大于CCR时输出无效电平模式2则相反。驱动LED、电机用模式1最常见。第五步使能输出通道和定时器主输出。这里有个隐蔽的“坑”高级定时器TIMER0除了要调用timer_channel_output_enable()之外还要调用timer_primary_output_config(TIMER0, ENABLE)把主输出使能打开否则通道上永远没波。我之前遇到过一次低级错误查了半天。3.3 完整示例代码TIMER1通道0输出1kHz、50%占空比PWM下面我贴一段完整的代码配合详细注释。需求场景是从PA0引脚输出一个1kHz、占空比50%的PWM波用LED观察亮度用示波器/逻辑分析仪观察波形。当前内容为测试代码实际项目请根据自身情况调整参数并注意用电安全。#include gd32f10x.h #include gd32f10x_gpio.h #include gd32f10x_timer.h #include gd32f10x_rcu.h void pwm_gpio_config(void) { /* 开启GPIOA和AFIO时钟 */ rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_AF); /* 配置PA0为复用推挽输出最大速度50MHz */ gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0); } void pwm_timer_config(void) { timer_oc_parameter_struct timer_ocintpara; timer_parameter_struct timer_initpara; /* 开启TIMER1时钟挂载在APB1总线上 */ rcu_periph_clock_enable(RCU_TIMER1); /* 定时器时基配置 */ timer_deinit(TIMER1); timer_initpara.prescaler 71; /* 72MHz / (711) 1MHz计数频率 */ timer_initpara.alignedmode TIMER_COUNTER_EDGE; timer_initpara.counterdirection TIMER_COUNTER_UP; timer_initpara.period 999; /* 1MHz / (9991) 1kHz PWM频率 */ timer_initpara.clockdivision TIMER_CKDIV_DIV1; timer_initpara.repetitioncounter 0; timer_init(TIMER1, timer_initpara); /* PWM输出比较配置 */ timer_ocintpara.outputstate TIMER_CCX_ENABLE; timer_ocintpara.ocpolarity TIMER_OC_POLARITY_HIGH; timer_ocintpara.ocidlestate TIMER_OC_IDLE_STATE_LOW; /* 占空比 CCR / (ARR1) 500 / 1000 50% */ timer_channel_output_config(TIMER1, TIMER_CH_0, timer_ocintpara); timer_channel_output_pulse_value_config(TIMER1, TIMER_CH_0, 500); timer_channel_output_mode_config(TIMER1, TIMER_CH_0, TIMER_OC_MODE_PWM0); timer_channel_output_shadow_config(TIMER1, TIMER_CH_0, TIMER_OC_SHADOW_DISABLE); /* 自动重装载影子寄存器使能 */ timer_auto_reload_shadow_enable(TIMER1); /* 开启定时器 */ timer_enable(TIMER1); } int main(void) { /* 系统时钟初始化默认为72MHz */ system_clock_config(); pwm_gpio_config(); pwm_timer_config(); while (1) { /* 主循环里什么都不用做波形由硬件自动输出 */ } }当前内容为测试代码实际项目请根据自身情况调整参数并注意用电安全。3.4 关键函数逐行解读别看完了还是不会改参数代码贴完了但我估计很多人直接复制下来改一改就完事这不行。你不能只会抄得知道每一行的含义否则当频率不对、占空比不对的时候你根本不知道从哪下手。首先看prescaler 71。前面我讲了公式72MHz除以72等于1MHz这1MHz是计数器的时钟。为什么是71而不是72因为预分频器的值是从0开始计数的实际分频系数是prescaler 1。这个规律和ARR同理所有N分频都是写N-1切记。再看period 999。在1MHz的计数频率下计数器从0数到999然后清零重来每1000个计数脉冲输出一个完整周期也就是1ms一个周期换算成频率就是1kHz。然后是关键的timer_channel_output_pulse_value_config(TIMER1, TIMER_CH_0, 500)。这一行把比较值CCR设为500。计数器从0往上数当计数值到达500时PWM通道输出翻转。由于计数上限是999即ARR所以高电平持续500个计数周期低电平持续499个计数周期近似50%输出波形就是1kHz、50%占空比。最后说timer_channel_output_mode_config里的TIMER_OC_MODE_PWM0。这个PWM0就是PWM模式1的意思即“向上计数时计数值小于比较值时输出有效高电平”。GD32库函数里还有PWM1、PWM2等模式分别对应不同的输出极性逻辑。新手阶段用PWM0然后通过调节CCR来控制占空比这是最顺的思路。4. 让PWM“动”起来呼吸灯、舵机控制两个实例源码4.1 实例一呼吸灯感受PWM的魅力固定占空比的PWM只是静态输出真正好玩的是动态控制。呼吸灯就是一个特别直观的例子LED从灭到亮再从亮到灭循环往复就像人在呼吸一样。核心逻辑就是每隔一小段时间把CCR值递增或递减。思路如下ARR999不变PWM频率1kHz。用一个变量duty从0逐渐加到999过了峰值再从999逐渐减到0如此循环。每步延时1ms~5ms肉眼就看到平滑渐变。这个渐变速度可以通过延时长短调节。当前内容为测试代码实际项目请根据自身情况调整参数并注意用电安全。volatile uint16_t duty 0; volatile uint8_t dir 1; /* 1递增0递减 */ while (1) { if (dir) { duty; if (duty 999) { dir 0; } } else { duty--; if (duty 0) { dir 1; } } timer_channel_output_pulse_value_config(TIMER1, TIMER_CH_0, duty); delay_1ms(3); }注意一个问题如果你用的是LED直接接在PA0引脚上电流不要太大建议串一个220欧姆~1k欧姆的限流电阻。GD32的GPIO驱动能力有限直接驱动高功率LED或者灯带是不现实的需要外部驱动电路。4.2 实例二SG90舵机控制把PWM用到舵机角度上呼吸灯只是演示舵机控制才是PWM实战中非常有代表性的场景。SG90这种小型舵机控制信号就是50Hz的PWM波也就是周期20ms。占空比的高电平时间决定了舵机转到的角度大致对应关系是0.5ms高电平0度1.0ms高电平45度左右1.5ms高电平90度左右2.0ms高电平135度左右2.5ms高电平180度左右所以控制舵机的核心就是产生50Hz的PWM然后让高电平脉宽在0.5ms~2.5ms之间变化。因为GD32的定时器是16位的哪怕频率很低精度也完全够用。参数怎么算定时器时钟仍然是1MHzPSC71PWM频率要50Hz所以period (1MHz / 50Hz) - 1 19999。那么0.5ms高电平对应的比较值就是 0.5ms × 1MHz 5001.5ms对应15002.5ms对应2500。这算法是不是很简单代码只需要改period为19999然后动态改pulse值即可。当前内容为测试代码实际项目请根据自身情况调整参数并注意用电安全。/* 假设TIMER1_CH_0已初始化period 1999950Hz */ while (1) { /* 转动到0度 */ timer_channel_output_pulse_value_config(TIMER1, TIMER_CH_0, 500); delay_ms(1000); /* 转动到90度 */ timer_channel_output_pulse_value_config(TIMER1, TIMER_CH_0, 1500); delay_ms(1000); /* 转动到180度 */ timer_channel_output_pulse_value_config(TIMER1, TIMER_CH_0, 2500); delay_ms(1000); }注意我给SG90供电时通常不直接用单片机3.3V引脚去推而是外部5V供电舵机信号线再串一个1k电阻到PA0。这样既保证信号电平兼容又避免舵机启动瞬间大电流拉低单片机供电电压导致复位。4.3 电机调速的注意点PWM不是“想接就接”电机调速的原理和舵机类似也是通过占空比控制平均电压。但很多新手在这里翻车我单独拎出来说电机是大功率感性负载启动电流很大而且关断瞬间会产生反向电动势电压可能达到几十伏甚至上百伏。单片机的引脚直接驱动必烧无疑。正确的做法是用一个N沟道MOSFET或者电机驱动芯片如L298N、TB6612、DRV8833来做功率级PWM信号只接到芯片的IN脚或使能脚。如果驱动的是小功率直流电机比如5V玩具电机用一颗AO3400这种低压MOS管就能搞定栅极接PWM输出源极接地漏极接电机负极电机正极接5V电源再在电机两端反并联一个续流二极管1N5819之类吸收反向电动势。PWM频率建议设在10kHz~20kHz低于这个范围可能会有电机啸叫高了我实测MOS管开关损耗会明显增加。5. GD32使用PWM时的高频问题和解决方案5.1 通道没有波形输出先查主输出使能这个问题发生率极高尤其是第一次用高级定时器的时候。GD32的高级定时器TIMER0相比通用定时器多了一个主输出使能位如果没打开定时器内部一切正常但输出引脚被禁止驱动。表现为代码跑起来了寄存器也能读到配置值示波器上就是一条平线。解决方法是调用一行函数timer_primary_output_config(TIMER0, ENABLE);如果你用的是TIMER1/2/3这些通用定时器就不需要这个函数。5.2 引脚复用不对PWM波形出不来或者和别的功能抢引脚PA0、PA1、PA2、PA3这几个引脚非常“忙”它们除了做TIMER1的CH0~CH3之外还有可能就是ADC输入引脚、比较器输入或者USART的某些重映射功能。如果初始化顺序里后面的代码把GPIO重新配置成普通输入模式那么PWM输出立刻就没了。做法是整个工程里对同一个引脚的gpio_init操作要全局梳理一遍避免重复初始化。如果确实需要动态切换功能一定要在切换前把定时器输出通道关闭切换后重新初始化GPIO和定时器。我在实际项目里就踩过在一个板子上PA1既接了一个按键输入又想把TIMER1_CH1的PWM引出来两个功能硬抢同一个引脚结果按键按下去PWM波形就乱跳。建议优先选择专用引脚少用复用引脚如果不得不复用加跳线或外部分离电路千万别软件硬刚。5.3 PWM频率和分辨率配对不合理出现闪烁或噪声如果你把ARR设成很小比如ARR9那占空比只能10档一档一档跳LED调光会明显看到阶梯感如果PSC设得很大ARR也很大频率太低可能出现LED肉眼可见的闪烁。经验值LED调光PWM频率至少1kHz最好5kHz以上舵机必须50Hz左右因为舵机内部电路按它的标准信号设计电机驱动10kHz~20kHz。实在记不住也没关系用公式反推先定频率F再定PSC让计数时钟 F × (ARR1)把ARR取在几百到几千之间基本就是合理区间。不建议让ARR超过2^16-1因为定时器是16位的超过65535会自动溢出波形完全乱掉。5.4 占空比改了没反应小心影子寄存器GD32的定时器很多寄存器带有“影子寄存器”功能。你写入的比较值CCR可能不会立刻生效而在某个事件比如更新事件到来时才真正加载到比较器。这个设计是为了避免多通道PWM输出不同步时出现毛刺。库函数里有个函数timer_channel_output_shadow_config()刚才代码里我传的是TIMER_OC_SHADOW_DISABLE意思就是关闭影子功能写进去立刻生效。如果你设置成ENABLE那么不开启预装载、不触发更新事件的情况下改了CCR也不会更新输出。同理ARR也有影子机制timer_auto_reload_shadow_enable()之后修改period要等到更新事件才生效。新手阶段建议全关影子或者使能后每次改完主动触发一次更新事件免得出现“改了参数但没反应”的灵异现象。5.5 想在中断里动态调PWM怎么处理才好有些场景需要在PWM周期中断里实时调整占空比比如无刷电机FOC、电子负载、某些波形合成。做法是打开定时器更新中断或比较捕获中断在中断服务函数里更新pulse值。一个常见的错误是在中断里做过多计算导致中断响应时间过长影响PWM波形的稳定性甚至丢失更新事件。我的建议是中断服务函数里只做“设置新比较值”这一个动作具体的占空比算法放到主循环或者专门的任务里计算好用全局变量传递。如果你用到多路PWM同步输出建议使用DMA来更新多路比较值把CPU从中断中解放出来。GD32的定时器DMA请求是支持按通道单独配置的这在后续做无刷驱动时是常用手法。5.6 推挽输出和开漏输出别搞混了GPIO模式里AF_PP是复用推挽输出AF_OD是复用开漏输出。PWM输出要选复用推挽GPIO_MODE_AF_PP因为推挽输出既能灌电流也能拉电流输出高低电平的驱动能力都强。开漏输出需要额外上拉电阻且高电平驱动能力弱用在I2C这种线或场景合适用在PWM驱动上就很容易出现高电平偏弱、波形变形的问题。判断你有没有配错的方法也简单看波形。如果示波器上高电平幅度明显低于3.3V或者波形上升沿很“钝”大概率是开漏模式加上拉电阻阻值太大导致的。6. 进阶思路从单路PWM到多路PWM再到“PWMDMA”6.1 多路PWM怎么配最省事实际项目很少只用一路PWM。比如四轴飞行器要4路电机云台要2路舵机RGB灯要3路PWM。如果你用通用定时器每个定时器有4个通道一个TIMER1全用满就能同时输出4路不同占空比的PWM。初始化的时候前面章节里的时基配置PSC、period只需要做一次然后对每个通道分别调用timer_channel_output_config和timer_channel_output_pulse_value_config即可。之后主循环里更新哪个通道就调哪个比较值函数。多路输出有一个好处所有通道相位同步性非常好因为它们共用一个计数器。这对电机正弦波驱动来说很重要相位偏一点电机的力矩特性就不对了。6.2 DMA出击批量更新多路PWM的比较值如果你想做的不是简单的固定占空比而是要动态生成一组连续变化的波形例如产生一个正弦波表预先算好256个点的占空比值存在一个数组里然后通过DMA循环搬运到比较值寄存器CPU全程不参与就能在PWM引脚上得到一个近似的正弦波。这个思路在很多领域都有应用逆变器、电子负载、音频播放PWM DAC都能这么干。GD32的定时器DMA功能可以通过配置DMAR寄存器实现使用库函数时也有对应的timer_dma_enable等接口。对于刚入门的读者这个可以先记下方法后面项目需要时再深入。6.3 软件PWM值不值得学遇到定时器资源不够用的情况很多人会考虑软件PWM——用普通GPIO加定时器中断在中断里翻转引脚来模拟PWM。这个方案不是不能用但有几个硬伤占空比精度和PWM频率互相压制频率高了CPU负载就爆表。多路输出时中断里要处理多路逻辑时序抖动大波形质量差。如果主循环里有阻塞延时或者UART发送波形会出现明显毛刺。所以我个人建议软件PWM只适合“临时点个灯”“应急验证”这样的场景。真正做产品老老实实换一颗定时器资源更多、或者带专用PWM外设的芯片比在软件层死磕要靠谱得多。7. 写在最后的几条经验我调试PWM这几年最深的感受是PWM本身不难难的是周围那圈“隐性知识”。比如不同定时器之间的差异、引脚复用的冲突、影子寄存器的生效时机、电机驱动电路的续流问题任何一个环节没注意都可能让一个看起来简单的功能卡住半天。如果你照着这篇的步骤做最后PA0输出还是没有波形我建议按这个顺序排查第一确认时钟树里TIMER1时钟开没开挂的总线对不对第二确认GPIO是不是配成了复用推挽而不是普通推挽或者开漏第三确认比较值设置合理不要小于0也不要大于period第四通用定时器确认没有动主输出使能这种高级功能第五用示波器量引脚别只看代码。最后再多说一句GD32的库函数和STM32的标准外设库长得非常像如果你之前学过STM32迁移成本极低。反过来如果你是从零开始直接学GD32那直接上手官方固件库也是个不错的选择。国产芯片现在的资料虽然不如ST那么多但官方文档和例程都在持续完善多啃一啃数据手册收获会很大。这篇就到这儿下面是完整的示例源码需要的小伙伴自取。有问题可以在评论里交流我看到会回复。# 源码文件清单以仓库为准 . ├── README.md ├── pwm_breathing_led.c # 呼吸灯示例 ├── pwm_servo_control.c # SG90舵机控制示例 ├── pwm_motor_speed.c # 直流电机调速示例需外部MOS驱动 └── gd32_pwm_common.c # PWM初始化公共函数
返回列表