本次课程采用单片机型号为STM32F103C8T6。
课程链接:江科大自化协 STM32入门教程
往期笔记链接:
STM32学习笔记(一)丨建立工程丨GPIO 通用输入输出
STM32学习笔记(二)丨STM32程序调试丨OLED的使用
STM32学习笔记(三)丨中断系统丨EXTI外部中断
STM32学习笔记(四)丨TIM定时器及其应用(定时中断、内外时钟源选择)
STM32学习笔记(五)丨TIM定时器及其应用(输出比较丨PWM驱动呼吸灯、舵机、直流电机)
如果上一篇笔记的内容为史诗级副本,本篇文章的内容我愿称之为传说级副本(三)。
输入捕获,即Input Capture,英文缩写为IC。输入捕获模式下,当通道输入引脚出现指定电平跳变(可以定义为上升沿、下降沿)时,当前CNT的值将被锁存到CCR中(这就是“捕获”的含义),可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数。在这里,“脉冲间隔、电平持续时间”和“频率、占空比”是互相对应的关系。每个高级定时器和通用定时器都拥有4各输入捕获通道,且二者没有区别。
输入捕获模块可以配置为PWMI(PWM输入)模式和主从触发模式。PWMI模式是专门用来同时测量PWM波形的频率和占空比的。主从触发模式可以实现对频率或占空比的硬件的全自动测量,不占用软件资源,可以极大地减轻CPU的压力。
在定时器中,输入捕获和输出比较共用同一个引脚和同一个CCR,故在使用时,对同一个TIM定时器而言,输入捕获和输出比较功能只能使用一个,不能同时使用。
接下来分析输入捕获通道的工作原理。输入信号首先进入输入滤波器和边沿检测器。这个滤波器和定时器的外部时钟模式2的输入滤波原理类似,它可以避免一些高频的毛刺信号造成误触发。在博主的文章STM32学习笔记(四)丨TIM定时器及其应用(定时中断、内外时钟源选择)中有较为详细的说明:
在定时器的外部信号输入引脚一般都有一个滤波器来消除信号的抖动干扰,它的工作原理是:在一个固定的时钟频率 f f f下进行采样,如果连续 N N N个采样点都是相同的电平,就代表输入信号稳定了,就将采样值输出到下一级电路;如果 N N N个采样点不全都相同,就说明信号有抖动,这时保持上一次的输出,或直接输出低电平。 这样就能保证输出信号在一定程度上的滤波。这里的采样频率 f f f和采样点数 N N N都是滤波器的参数,频率越低,采样点数越多,滤波效果就越好,不过相应的信号延迟就越大。
在实际应用时,通过改变TIMx_CCMR1寄存器的IC1F位就可以配置不同的滤波频率。
边沿检测部分和外部中断类似,可以选择高电平触发,或者低电平触发。当触发指定的电平时,边沿检测电路就会触发后续的电路执行动作。
在CH1,CH2,CH3之间跨接了一个异或门,它实际上是为三相无刷电机服务的。无刷电机拥有三个霍尔传感器检测转子的位置,可以根据转子的位置进行换相,通过将异或门的三个输入端与三个霍尔传感器相连接,此时的定时器就称为无刷电机的接口定时器,取驱动换相电路工作。这部分内容暂时不涉及,仅作了解即可。
CH3、CH4的输入滤波和边沿检测电路的结构与CH1、CH2类似。图中用一个方框代表了输入滤波和边沿检测,但实际上这里有两套相同的电路结构。CH1通道的一个输入滤波和边沿检测接到TI1FP1(Timer Input 1 Filter Polarity 1,可连接到IC1),另一个输入滤波和边沿检测接到TI1FP2(Timer Input 1 Filter Polarity 2,可连接到IC2);同样,CH2通道的一个输入滤波和边沿检测接到TI2FP1(可连接到IC1),另一个输入滤波和边沿检测接到TI1FP2(可连接到IC2)。这样“交叉连接”的目的主要有以下两点:
此外,TRC信号也可以作为捕获信号的输入。这个设计同样是为无刷电机的驱动设计的。了解即可,这里暂时不涉及。
信号进入捕获单元后,经过一个预分频器就可以控制CCR对CNT进行捕获操作了。捕获信号同时会触发一个事件,这个事件会在状态寄存器置标志位,同时也可以触发中断。
输入捕获通道1的详细框图如下所示:
CCR对CNT进行捕获之后,需要对CNT进行一次清0操作,这样每次捕获得到的值才是测周法(下文有讲解)两个上升沿(下降沿)之间的时间间隔。这个清0操作,就需要用到主从触发模式来自动完成。由输入捕获通道1的详细框图可得:经过滤波和极性选择的TI1FP1信号和经过滤波的边沿信号TI1F_ED都可以通向从模式控制器,之后便可以通过硬件电路自动完成CNT的清0操作。
主从触发模式,即主模式、从模式和触发源选择三个功能的简称。主模式可以将定时器内部的信号映射到TRGO引脚,用于触发其他外设的操作;从模式可以接收其他外设或自身外设的一些信号,用于触发自己的一些操作(定时器的运行);触发源选择,即选择从模式的触发信号源功能,也可以认为它是从模式的一部分。
在从模式下,可以通过触发源选择功能选择一个信号产生TRGI信号,之后去触发从模式。关于主从模式的详细说明可以参见手册:
下图是输出捕获模式测频率的基本结构图。下图清晰地展示了输入捕获模式测量频率的过程,同时也是编程的逻辑基础。在这里我们只使用了一个通道,所以它只能测量频率。
首先,配置时基单元,启动寄存器,则CNT就会在时钟驱动下不断自增。我们使用CNT来计数,间接实现计时的功能。经过预分频后的时钟频率,就是测周法的标准频率 f c f_c fc。之后,GPIO输入一个待测的方波信号,经过TI1FP1为上升沿触发,之后数据选择器选择直连通道,分频器选择不分频。当TI1FP1出现上升沿之后,CNT的值就会被CCR1捕获;同时触发源选择模块选择TI1FP1为触发信号,从模式选择复位操作。则TI1FP1信号完成两个操作:捕获CNT的值到CCR1中、清零CNT。当然这里存在先后顺序,先进行捕获后进行清0;或者存在非阻塞的同时转移。当电路不断工作是,CCR1中的值始终是最新一个周期的计数值,即测周法的计次数 N N N。
这里需要注意以下两点:
- CNT的计数值是有上限的。由于ARR最大为65535,故CNT最大也只能计65535个数。如果信号频率太低,CNT的计数值可能会溢出。
- 从模式的触发源选择中有TI1FP1和TI2FP2,但是没有TI3和TI4的信号。所以如果要使用从模式自从清零CNT,就必须使用CH1或CH2作为输入。对于CH3和CH4,就只能开启捕获中断,在中断中手动清0了(程序会处于频繁中断的状态,比较占用软件资源)。
通过将一个引脚的信号映射到两个输入捕获单元,将TI1FP2配置为下降沿触发,就可以同时测量PWM波形的频率和占空比了,如下图所示,占空比:
D u t y = C C R 2 C C R 1 × 100 % Duty = \frac {CCR_2} {CCR_1} \times 100 \% Duty=CCR1CCR2×100%
对于测频率而言,STM32只能测量数字信号(高电平3.3V,低电平0V)。如果要测量模拟信号(例如一个正弦波)的频率,还需要在测量之前使信号通过一个信号预处理电路。可以使用继承运放搭接一个比较器,也可以使信号通过一个施密特触发器,将模拟信号转换为数字信号(保证二者频率相同),之后再输入给STM32测量信号频率。如果需要测量的信号电压较高,则还需要考虑隔离的问题,使用隔离放大器、电压互感器等元件,隔离高压端和低压端,保证电路的安全。
测频法的测试方法是:在闸门时间 T T T 内,对上升沿(也可以是下降沿)计次,得到 N N N,则测量频率:
= _=\frac fx=TN
例如,可以定义闸门时间闸门时间 T = 1 s T=1s T=1s ,则在一秒中得到的上升沿的个数(完整的一个周期的信号个数)就是频率。对应之前的代码,可以使用对射式红外传感器计次的代码(详见STM32学习笔记(三)丨中断系统丨EXTI外部中断),将待测信号接入一个外部中断,在外部中断中计次数不断自增;再使用定时器定时1s产生中断,在中断中将计数值保存并清0。这部分比较简单,这里不再演示。
测周法的测试方法是:两个上升沿内,以标准频率 f c f_c fc 计次,得到 N N N ,则测量频率:
= _ = \frac {_} fx=Nfc
测周法的基本思想是:周期的倒数就是频率。如果我们能用定时器测量出一个周期的时间(相邻上升沿或相邻下降沿的间隔时间),取倒数即得到测量频率。输入捕获模块采用测周法进行测量。
根据上图可以清晰地看出,测频法适用于测量高频信号,测周法适用于测量低频信号。
对于测频法,当被测频率过低,会导致 N N N 很小,此时的误差非常大;对于测周法,当被测频率过高( f x f_x fx 和 f c f_c fc 非常接近),在 f x f_x fx 的一个周期内只能得到一个很小的 N N N ,此时的误差也非常大。
由于测量原理的差异,一般而言,测频法的结果更新频率会比较慢,但是数值较为稳定;测周法的结果更新频率较快,数据跳变也比较灵敏。从原理上看,测频法自带一个均值滤波的功能,如果在闸门时间 T T T 内被测频率有变化,测频法得到的实际是这一段闸门时间内的平均频率;而测周法只测量一个周期,故其结果会受噪声的影响,波动会比较大。所以,对于测频法和测周法的一个共同点是: N N N 越大,误差就越小。在两种方法中,计次都可能会产生正负1误差。在测频法的一个闸门时间内,并不是每一个被测信号的周期都是完整的;测周法的标准计数信号的信号也不一定是被测信号的整数倍,所以它也不一定是每一个都完整的。对于上述的两种情况,都会出现多计一个数或者少计一个数的情况,所以会产生正负1误差。
如何在不同情况下正确选择测频法和测周法呢?我们有以下一个参数来考量:中界频率,测频法和测周法误差相等的频率点。由于两种方法的误差都与 N N N 的正负1误差有关,所以当两种方法计次的N相同时,两种方法的误差也就相同。消去两种方法公式中的 N N N,可得:
f m = f x = f c T f_m=f_x=\sqrt {\frac {f_c} T} fm=fx=Tfc
式中, T T T 是测频法的闸门时间, f c f_c fc 是测频法的标准频率。
// 输入捕获IC初始化函数
// 输入捕获有四个通道,但是只有一个初始化函数,通道选择在结构体成员变量中,这一点与输出比较不同
void TIM_ICInit(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
// 给IC初始化函数中的结构体参数赋一个默认值
void TIM_ICStructInit(TIM_ICInitTypeDef* TIM_ICInitStruct);
// 快速配置PWMI模式(自动将另一个通道配置为相反的模式)
void TIM_PWMIConfig(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
// 单独写入时基单元的PSC,第三个参数与预装载有关
void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode);
// 单独配置四个通道的预分频器,结构体中也可以配置,效果相同
void TIM_SetIC1Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetIC2Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetIC3Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetIC4Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
// 分别读取四个通道的CCR的值(与输出比较中的TIM_SetComparex函数对应)
uint16_t TIM_GetCapture1(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture2(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture3(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture4(TIM_TypeDef* TIMx);
// 选择输入(从模式)触发源TRGI
void TIM_SelectInputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
// 选择输入(主模式)触发源TRGO
void TIM_SelectOutputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_TRGOSource);
// 选择从模式需要执行的操作
void TIM_SelectSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_SlaveMode);
为了测量外部信号的频率和占空比,首先要产生一个信号以供测量。博主手头也没有信号发生器,在这里与课程中演示的操作相同,先使用PWM模块在PA0端口输出一个频率和占空比都可调的波形,之后在PA6端口测量该波形的频率和占空比。
接线图和程序源码如下所示:
PWM.c
#include "stm32f10x.h" // Device header
/**
* @brief PWM输出初始化函数
* @param 无
* @retval 无
*/
void PWM_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
TIM_InternalClockConfig(TIM2);
// 时基单元初始化
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; // ARR,确定后即确定了分辨率,这里分辨率为1%
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1; // PSC(调节ARR和PSC都可以改变频率,而ARR同时影响占空比,故这里通过调节PSC来调节PWM波形的频率)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
// GPIO初始化,输出端口为PA0
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// OC初始化
TIM_OCInitTypeDef TIM_OCInitStruct;
TIM_OCStructInit(&TIM_OCInitStruct); // 结构体成员赋初始值
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; // OC输出极性(有效电平为高电平)
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; // OC输出使能
TIM_OCInitStruct.TIM_Pulse = 0; // CCR
TIM_OC1Init(TIM2, &TIM_OCInitStruct);
TIM_Cmd(TIM2, ENABLE);
}
/**
* @brief 更改比较/捕获寄存器的值CCR(当 ARR + 1 == 100 时 CCR 即为占空比)
* @param Compare 无符号16位整型数,注意:它只能是正数
* @retval 无
*/
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare);
}
/**
* @brief 更改与分频器的值
* @param Prescaler 写入的新预分频器的值
* @retval 无
*/
void PWM_SetPrescaler(uint16_t Prescaler)
{
TIM_PrescalerConfig(TIM2, Prescaler, TIM_PSCReloadMode_Immediate); // 单独设置PSC(立刻生效)
}
InputCapture.c
#include "stm32f10x.h" // Device header
/**
* @brief 输入捕获初始化函数
* @param 无
* @retval 无
*/
void IC_Init(void)
{
// 1. 开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 使用TIM2输出PWM波形,TIM3进行输入捕获
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 2. GPIO初始化
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. 配置时基单元
TIM_InternalClockConfig(TIM3); // 选择内部时钟
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; // ARR,该值应该设置的尽量大,防止计数溢出
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; // PSC,它的值决定了测周法的标准频率fc,它的值要根据测量信号的频率范围来调整,这里fc为1MHz
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
// 4. 配置输入捕获单元
TIM_ICInitTypeDef TIM_ICInitStruct;
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1; // IC通道
TIM_ICInitStruct.TIM_ICFilter = 0xF; // 滤波属性(滤波检测频率应远高于被测频率)
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising; // 边沿检测
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1; // 触发信号分频器
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI; // 配置数据选择器(这里选择直连通道)
TIM_ICInit(TIM3, &TIM_ICInitStruct);
// 5. 选择从模式的触发源
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
// 6. 选择从模式触发后执行的操作
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
// 7. 开启定时器
TIM_Cmd(TIM3, ENABLE);
}
/**
* @brief 获取测得的频率
* @param 无
* @retval 测得的频率
*/
uint32_t IC_GetFreq(void)
{
// 测周法标准频率为1MHz
return 1000000 / TIM_GetCapture1(TIM3);
// return 1000000 / (TIM_GetCapture1(TIM3) + 1) // 面向结果编程否则逼死强迫症(?)
}
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
#include "InputCapture.h"
int main()
{
OLED_Init();
IC_Init();
PWM_Init();
PWM_SetPrescaler(720 - 1); // Freq = 72M / (PSC + 1) / (ARR + 1),这里ARR + 1 == 100
PWM_SetCompare1(50); // Duty = CCR / (ARR + 1)
OLED_ShowString(1, 1, "Freq:00000Hz");
while(1)
{
OLED_ShowNum(1, 6, IC_GetFreq(), 5);
}
}
PWM.c
与3.2中完全相同InputCapture.c
#include "stm32f10x.h" // Device header
/**
* @brief 输出捕获PWMI模式初始化函数
* @param 无
* @retval 无
*/
void IC_Init(void)
{
// 1. 开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 使用TIM2输出PWM波形,TIM3进行输入捕获
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 2. GPIO初始化
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入模式,防止信号干扰,且单片机为强下拉,弱上拉
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. 配置时基单元
TIM_InternalClockConfig(TIM3); // 选择内部时钟
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; // ARR,该值应该设置的尽量大,防止计数溢出
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; // PSC,它的值决定了测周法的标准频率fc,它的值要根据测量信号的频率范围来调整,这里fc为1MHz
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
// 4. 配置输入捕获单元
// 通道1测量频率
TIM_ICInitTypeDef TIM_ICInitStruct;
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1; // IC通道,通道1
TIM_ICInitStruct.TIM_ICFilter = 0xF;
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising; // 边沿检测,上升沿触发
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI; // 配置数据选择器,直连通道
TIM_ICInit(TIM3, &TIM_ICInitStruct);
// 通道2测量占空比
TIM_PWMIConfig(TIM3, &TIM_ICInitStruct); // 在该函数中会快捷地将另一个通道初始化为“相反”的配置,这个函数执行的操作和下面的操作是等价的
// TIM_ICInitStruct.TIM_Channel = TIM_Channel_2; // IC通道,通道2
// TIM_ICInitStruct.TIM_ICFilter = 0xF;
// TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Falling; // 边沿检测,下降沿触发
// TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;
// TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_IndirectTI; // 配置数据选择器,交叉通道
// TIM_ICInit(TIM3, &TIM_ICInitStruct);
// 5. 选择从模式的触发源
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
// 6. 选择从模式触发后执行的操作
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
// 7. 开启定时器
TIM_Cmd(TIM3, ENABLE);
}
/**
* @brief 获取测得的频率
* @param 无
* @retval 测得的频率
*/
uint32_t IC_GetFreq(void)
{
// 测周法标准频率为1MHz
return 1000000 / TIM_GetCapture1(TIM3);
// return 1000000 / (TIM_GetCapture1(TIM3) + 1); // 面向结果编程否则逼死强迫症(?)
}
/**
* @brief 获取测得的占空比
* @param 无
* @retval 测得的占空比(百分之)
*/
uint32_t IC_GetDuty(void)
{
return TIM_GetCapture2(TIM3) * 100 / TIM_GetCapture1(TIM3);
// return (TIM_GetCapture2(TIM3) + 1) * 100 / (TIM_GetCapture1(TIM3) + 1);
}
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
#include "InputCapture.h"
int main()
{
OLED_Init();
IC_Init();
PWM_Init();
PWM_SetPrescaler(720 - 1); // Freq = 72M / (PSC + 1) / (ARR + 1),这里 ARR + 1 == 100
PWM_SetCompare1(80); // Duty = CCR / (ARR + 1)
OLED_ShowString(1, 1, "Freq:00000Hz");
OLED_ShowString(2, 1, "Duty:00%");
while(1)
{
OLED_ShowNum(1, 6, IC_GetFreq(), 5);
OLED_ShowNum(2, 6, IC_GetDuty(), 2);
}
}
原创笔记,码字不易,欢迎点赞,收藏~ 如有谬误敬请在评论区不吝告知,感激不尽!博主将持续更新有关嵌入式开发、机器学习方面的学习笔记~