一、介绍
二、新建工程
2.1添加文件
新建工程需要
Start文件中(一共六个):
启动文件地址:C:\Users\亲爱的玩家\Desktop\STM32F10x_StdPeriph_Lib_V3.5.0
其他描述文件地址:C:\Users\亲爱的玩家\Desktop\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries
library库文件:
uesr文件:
最终如图。红色为必要文件,蓝色是封装库
2.2必要步骤
点击魔法棒,点击C/C++。第一个框填写一模一样,第二个添加路径(头文件路径)
烧录器类型
点击setting,选择port
点击flashdownload,烧录之后自动运行
配置完成
2.3硬件连接
如图形成闭合回路
2.4.keilkill
如下工具可以删除编译数据,减少内存
三、实战案例(1)
3.1LED闪烁、LED流水灯、蜂鸣器
3.1.1
接线图
代码
3.1.2
接线图
代码
红框为GPIOA的二进制开口
3.1.3
接线
代码
3.2按键控制、光敏传感器控制蜂鸣器
3.2.1按键控制
接线图
代码(主函数和封装)
建立封装函数,一定建立 .C 和 .H 两个
3.2.1.1 LED
3.2.1.2 Key
3.2.1.3 Buzzer
3.2.1.4 Light Sensor
3.2.2光敏传感器控制蜂鸣器
3.3OLED
3.3.1硬件
3.3.2软件
OLED封装函数需要自己添加
3.4对射式红外传感器、旋转编码传感器
外部中断结构
3.4.1对射式红外
3.4.1.2接线、程序
封装函数
主函数
3.4.1.2调试、是否进入中断
点击进入调试模式
先在58行打断点,然后点2,全速运行
然后给传感器遮光,进入中断,如图,断点进入if语句
3.4.2旋转编码
3.4.2.1接线
3.4.2.2原理
机械式 EC11(最常用),内部结构:
- 公共端 C(GND)
- A 相触点 → 接 PB0
- B 相触点 → 接 PB1
- 内部有两个弹片,旋转时依次接通 / 断开 GND。
静止时(你设了上拉输入):
- PB0(A)=高电平 1
- PB1(B)=高电平 1
旋转时:
- 弹片碰到 GND → 引脚变低(0)
- 离开 → 弹回高(1)
旋转编码器不管正转、反转,两个中断(EXTI0 + EXTI1)都会触发!都会触发!都会触发!
- 顺时针转一格:EXTI0 触发 → EXTI1 触发
- 逆时针转一格:EXTI1 触发 → EXTI0 触发
两个中断都会进!
只是先后顺序不一样!
3.4.2.3封装函数
函数运行原理:
1、旋转编码器→AB都产生下降沿→触发中断
- 圆盘转动
- A 弹片先碰到导电区 → PB0 变低(下降沿)
- 再转一点点
- B 弹片也碰到导电区 → PB1 变低(下降沿)
2、if语句判断旋转方向(根据GPIO0或GPIO1的高低电平)→进行count加减
#include "stm32f10x.h" // Device header uint16_t Encoder_Count; void Encoder_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU ; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 ; //GPIOB 0&1 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB,&GPIO_InitStruct); GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0); GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1); EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; EXTI_Init(&EXTI_InitStructure); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; NVIC_Init(&NVIC_InitStructure); } uint16_t Encoder_Get(void) { int16_t Temp; Temp = Encoder_Count; Encoder_Count = 0; return Temp; //return Encoder_Count; } //只要编码器旋转,EXTI0、EXTI1 都会进入。 void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) == SET) { if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) // 判断方向 { Encoder_Count --; } EXTI_ClearITPendingBit(EXTI_Line0); } } void EXTI1_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line1) == SET) // { if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0) // 判断方向 { Encoder_Count ++; } EXTI_ClearITPendingBit(EXTI_Line1); } }这里面temp ,每转一次,temp变成1,主程序中,把每个1都加起来
3.4.2.4主函数
OLED显示count数
3.4.2.5 tips
uint16_t Encoder_Get(void) { int16_t Temp; // 定义临时变量 Temp = Encoder_Count; // 把当前计数值存起来 Encoder_Count = 0; // 清零,准备下一次计数 return Temp; // 返回刚刚存的值 }- 你刚想读取
Encoder_Count - 突然中断触发了
- 中断里把
Encoder_Count++ - 你原来的值就变了!
这叫 **“数据被打断”**,会导致数字乱跳。
************************旋钮按下功能(按下num清零)*****************************
第一步:接线
编码器模块上方两个引脚(VCC、GND)是按键:
- VCC → 接单片机 PA2(随便选一个 GPIO,我选 PA2 举例)
- GND → 接单片机 GND
第二步:代码
只加了按键初始化 + 按键检测 + 清零功能其他完全不动!
1. 新增头文件 & 全局变量
#include "stm32f10x.h" #include "OLED.h" #include "Encoder.h" // 新增:按键引脚定义 #define KEY_Pin GPIO_Pin_2 #define KEY_GPIO_Port GPIOA2. 按键初始化函数(加到 Encoder_Init 里面)
void Encoder_Init(void) { // 原来的编码器初始化代码…… // …… // --------------------- 新增:按键初始化 --------------------- GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = KEY_Pin; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(KEY_GPIO_Port, &GPIO_InitStructure); }3. 主函数 while (1) 里加按键检测
int main(void) { OLED_Init(); Encoder_Init(); OLED_ShowString(1, 1, "Num:"); int16_t Num = 0; // 你的计数 while(1) { // --------------- 旋转计数(你原来的代码) --------------- Num += Encoder_Get(); // ------------------- 新增:按键按下清零 ------------------- if(GPIO_ReadInputDataBit(KEY_GPIO_Port, KEY_Pin) == 0) { Delay_ms(20); // 消抖 while(GPIO_ReadInputDataBit(KEY_GPIO_Port, KEY_Pin) == 0); Delay_ms(20); Num = 0; // 清零! } // 显示 OLED_ShowSignedNum(1,5,Num,5); } }3.5 定时器中断、定时器外部时钟
定时器类型
3.5.1定时器中断
函数及其对应硬件
封装函数
1.RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 开启内部时钟 TIM2
2.TIM_InternalClockConfig(TIM2); //选择内部时钟(系统默认内部时钟)
//配置时基单元
3.TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1; // PSC 预分频器
- 作用:把 72MHz 时钟放慢 7200 倍
- 结果:CNT 计数器 每秒跳 10000 下
4.TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1; // ARR 自动重装器
- 作用:CNT 数到 10000 就归零,触发中断
- 结果:每秒刚好满一次 → 1 秒中断一次
5.TIM_GetCounter(TIM2) // → CNT(硬件) 能显示CNT计数器
6.TIM_Cmd(TIM2, ENABLE); //运行控制 启动定时器
7.TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //定时器2 更新中断
当CNT到达10000,执行此行代码,进行中断
中断控制函数模板(库函数自带,可以直接全局调用)
主函数
3.5.2定时器外部时钟
接线
封装,该为外部时钟控制,对射红外对PA0口进行输出
改一下计时时间
为什么【外部红外计数】PSC 必须 = 0?
因为:外部时钟不是 72MHz,是【你遮挡红外的动作】!
如果 PSC = 1(不分频是 0,分频 1 就是 2 分频)
来 1 个脉冲 → 不算
来 2 个脉冲 → CNT 才 +1
你遮挡 2 次,只记 1 次 → 数据少一半!错了!
如果 PSC = 9(10 分频)
来 10 个脉冲
CNT 才 +1
你挡 10 次才算 1 次 → 完全不能用
3.6 PWM驱动LED呼吸灯、PWM驱动舵机、PWM驱动直流电机
原理结构图
CK_PSC默认72MHz
3.6.1 LED呼吸灯
接线
封装初始化
波形图
安捷伦DSO-X 2012A 使用说明在另一个文章
频率1KHz,占空比为50%的波形
常规端口
TIM2_CH1 默认对应 GPIO PA0 引脚(当需要用头一个引脚的不同默认功能,可以重定义)
一定要打开定时器PWM输出功能(把定时器中断功能关闭,要不然CNT到达设置的数,就会进入中断,然后卡死)
端口映射
TIM2_CH1默认PA0,映射到PA15,但PA15默认JTDI调试端口,需要特殊设置
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); //将PA15、PB3、PB4变成普通GPIO口
1.开启AFIO时钟
2.映射配置
3.选择修改对应GPIO口
主函数
for循环控制CCR,从而改变占空比大小
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare);//此函数只改变CCR,并不直接改变占空比
}
根据调节CCR,灯的占空比从1到100到1
3.6.2 舵机
硬件
舵机要求频率50Hz、高电平时间是0.5ms~2.5ms(周期2ms)
封装
PWM封装
只改了通道,上一个LED用的OC1,这个用的OC2
舵机封装
主函数
底层逻辑,每次按键,都会改变CCR的值
3.6.3 直流电机
硬件
A0 为PWM输出端 A4 A5为电机极性设置端 电机驱动H桥需要外接5V供电
设置特定频率,消除人耳电机噪音
设置电机驱动模块的in 1 in 2 设置极性(电机正反转)
控制电机速度
3.7 输入捕获模式测频率、PWMI模式测占空比
原理
定时器输入和输出共用4个通道,原理都是对CCR的改变。
输出PWM波,就是不断改变CCR
输入检测,就是不断检测波形的高低电平,从而改变CCR来检测
32只能测高低电平,不能测正弦波(需要搭建外设)
测频法需要计次数目高一些。防止N太小,有误差
测周法要求计次数目低一些
测周法数学推理:
N / fc = T (测得的一个周期)1 / T 为测得频率。
fc 自己设置 72MHz / PSC (1MHz 标准频率,每 1us+1,72MHz是 1/72us +1 )
通过计算中介频率,来判断目标频率(目标频率可以根据经验,从而选择测频还是测周)是高频还是低频,从而选择测频法还是测周法
3.7.1输入捕获模式测频率 测周
原理
IC函数对应
原理:从第一个上升沿开始计数,到第二个上升沿结束,并且把CNT赋值到CCR,然后就能计算频率(具体计算步骤在下)N计次就是CNT 的数值,他等于CCR 的值。
信号发生器
代码解读
1.GPIO配置
// GPIOA配置 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 ; // GPIOA 6 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct);配置输入捕获单元1
2.滤波器 →边沿检测→ 数据选择器(黄色块)→ 分频器
// 配置PWM IC 输入捕获单元 TIM_ICInitStructure.TIM_Channel = TIM_Channel_1 ; //选择通道 TIM_ICInitStructure.TIM_ICFilter = 0xF; //滤波器 TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //边缘检测 极性选择 TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; // 数据选择器 TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //分频器 TIM_ICInit(TIM3, &TIM_ICInitStructure);3.从模式配置
配置TRGI触发源为 TI1FP1
//触发源选择 TRGI的触发源为TI1FP1 TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);从模式为Reset
//选择从模式 TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);关于计算问题
//这是定时器最大能数到多少 希望CNT 能数很大,不要中途溢出 //最多能测量 65535 微秒的周期,足够测量 0.065 秒 以内的信号,也就是 频率 > 15Hz 都能测 TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //ARR 自动重装器 //把 72MHz 时钟 → 变成 1MHz CNT 每 1 微秒 + 1 TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;//PSC 预分频器 // uint32_t IC_GetFreq(void) { return 1000000 / TIM_GetCapture1(TIM3); //计算 }ARR设置最大值,是防止计数溢出重置
PSC分频是因为时钟72MHz是 1/72us +1,有可能一个周期没测完,就达到ARR最大值重置了
分频成1MHz是 1us +1,基本不会超过ARR。最后利用公式1000000 / TIM_GetCapture1(TIM3);
也就是fc / N 。N计次就是CNT 的数值,他等于CCR 的值。
N / fc = T (测得的一个周期)1 / T 为测得频率。
fc 自己设置 72MHz / PSC (1MHz 标准频率,每 1us+1,72MHz是 1/72us +1 )
3.7.2 PWMI模式测占空比
原理
就是多加了一个TI1FP2,来负责获取高电平的计数,上升沿TI1FP1,TI1FP2都开始计数。当出现下降沿,TI1FP2结束计数,但不给CNT赋0,等到出现第二个上升沿TI1FP1给Reset赋值0。
主要看TI1FP2配置
3.8 编码器接口测速
利用定时器对编码器计数,可以有效节约资源,避免3.4.2小节,利用中断进行计数。
原理
代码用了TIM_EncoderMode_TI12四倍频模式:
EC11 旋转编码器是有「阻尼卡位」的,一格内部藏了 4 个信号边沿;
✅ 每触发 1 个边沿 → CNT ±1
✅ 4 个边沿凑齐 → 才是完整 1 格 → CNT ±4
函数
也就是说我的编码器就是外部时钟,转一下CNT就上升。如果是内部时钟72MHz,1秒跳72M次。时钟跳的次数和时钟频率有关
之前写过秒级定时器Timer.h,把获取速度的函数用定时器中断(硬件)获取,这样减轻CPU(主函数)压力,保证OLED能实时显示speed。