写这个文章是用来学习的,记录一下我的学习过程。希望我能一直坚持下去,我只是一个小白,只是想好好学习,我知道这会很难,但我还是想去做!
本文写于:2025.04.04
前言
本次笔记是用来记录我的学习过程,同时把我需要的困难和思考记下来,有助于我的学习,同时也作为一种习惯,可以督促我学习,是一个激励自己的过程,让我们开始32单片机的学习之路。
欢迎大家给我提意见,能给我的嵌入式之旅提供方向和路线,现在作为小白,我就先学习32单片机了,就跟着B站上的江协科技开始学习了.
在这里会记录下江协科技32单片机开发板的配套视频教程所作的实验和学习笔记内容,因为我之前有一个开发板,我大概率会用我的板子模仿着来做.让我们一起加油!
另外为了增强我的学习效果:每次笔记把我不知道或者问题在后面提出来,再下一篇开头作为解答!
开发板说明
本人采用的是慧净的开发板,因为这个板子是我N年前就买的板子,索性就拿来用了。另外我也购买了江科大的学习套间。
原理图如下
1、开发板原理图
2、STM32F103C6和51对比
3、STM32F103C6核心板
视频中的都用这个开发板来实现,如果有资源就利用起来。另外也计划实现江协科技的套件。
下图是实物图
引用
【STM32入门教程-2023版 细致讲解 中文字幕】
还参考了下图中的书籍:
STM32库开发实战指南:基于STM32F103(第2版)
数据手册
解答和科普
一、定时器中断代码
定时器不涉及外部硬件,所以放在了System文件夹下,
第一步:RCC开启时钟,这个基本上每个代码都是第一步,不用想
定时器的基准时钟和整个外设的工作时钟就都会同时打开了。
第二步:选择时基单元的时钟源,对于定时中断,我们就选择内部时钟源
第三步:配置时基单元:包括这里的预分频器、自动重装定时器、技术模式等等
第四步:配置输出中断控制,允许更新中断输出到NVIC
第五步:配置NVIC,在NVIC中打开定时器中断的通道,并分配一个优先级
第六步:运行控制了,
整个模块配置完成后,我们还需要是能一下计数器,要不然计数器不会运行的.当定时器使能后,计数器就会开始计数了,当计数器更新时,触发中断,
最后在写一个定时器中断函数。这样这个中断函数每隔一段时间就能自动执行一次了。
标记一下
因为在初始化结构体里有很多关键的参数,比如自动重装值和预分频值等,这些参数可能会在初始化之后还需呀更改,如果为了更改某个参数还要再调用一次初始化函数,那太麻烦了,所有这有一些单独的函数,可以方便地更改这些关键参数,比如
TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode);
单独写预分频值的:第一个为那个定时器,第二个为值,第三个为模式,写入的值在更新事件后才有效
void TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);
改变计数器的技术模式
void TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState);
自动重装器预装功能配置:有预装还是物预装
void TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);
给计数器写入一个值:如果你想手动写一个计数值,就可以用这个函数;
void TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);
给自动重装值写入一个值:手动给一个自动重装值,用这个函数;
uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);
获取当前计数器计数值:如果你想看当前计数器计到哪里了,调用这个函数,返回值就是当前计数器的值;
uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);
获取当前预分频器值:如果想看预分频器值,就调用这个函数;
FlagStatus TIM_GetFlagStatus(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);
void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);
这四个和之前的一样:获取标志位和清除标志位的。
初始化TIM2,第一步开启时钟
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
}
选择时基单元的时钟
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
TIM_InternalClockConfig(TIM2); //好多人不写,默认的是内部时钟
}
配置时基单元
TIM_TimeBaseInitStructure.TIM_ClockDivision=;
这个时钟分频的参数在之前的原理一直没出现过,那它是用来干啥的,
关键点来了,这个采样频率f从哪来,手册写的是它可以由内部时钟直接而来,也可以是由内部时钟加一个时钟分频而来,那分频多少,就由我们这个参数ClockDivision决定的;可加这个参数和时基单元关系不大,在这里我们随便配一个。
TIM_TimeBaseInitStructure.TIM_Period=;
TIM_TimeBaseInitStructure.TIM_Prescaler=;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=;
TIM_Period:周期,就是ARR自动重装器的值;
TIM_Prescaler: PSC就是预分频器的值;
TIM_RepetitionCounter:就是重复计数器的值;只有高级定时器才有。给0;
不过这里面并没有CNT计数器的参数,如果我们之后需要的话,需要用SetCounter和GetCounter这两个函数来操作计数器;
定时频率=72M/(PSC+1)(ARR+1),定时1s,也就是定时频率1Hz,那么PSC给一个7200,ARR给一个10000,然后两个参数都再减一个1,这样就完成了。因为预分频器和计数器都有一个数的偏差,所以这里要减个1,然后注意PSC和ARR的取值都在0 ~ 65535,不要超范围了。
你可以预分频器给少点,自动重装给多点,这样就是一个较高的频率计比较多的数(说明预分频后的时间短),也可以预分频器给多点,自动重装给少点,这样就是一个较低的频率计比较少的数(说明预分频后的时间长);预分频对72M进行7200分频,得到的就是10k的计数频率,在10K的频率下,计10000个数,那不就是1s的时间吗?
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1; //滤波的分频关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up ; //向上计数
TIM_TimeBaseInitStructure.TIM_Period=7200-1; //ARR
TIM_TimeBaseInitStructure.TIM_Prescaler=10000-1; //PSC
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
使能更新中断
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); //更新中断到NVIC的通路
配置NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel=TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStructure);
使能定时器
TIM_Cmd(TIM2,ENABLE);
定时器初始化
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,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=7200-1; //ARR
TIM_TimeBaseInitStructure.TIM_Prescaler=10000-1; //PSC
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); //更新中断到NVIC的通路
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel=TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM2,ENABLE);
}
接下来写中断函数
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update)== SET)
{
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
我们现在想让显示1s进行计数,uint16_t Num;然后这个Num要在中断函数里执行++;但是中断函数是在Timer模块里的,如果直接在主程序写Num++,那Num就是跨越不同.C文件的变量了,会报错;Num++:在主函数中会跨文件,所以我们有这几种方法:
1、 如果你想跨文件使用变量,那可以在使用变量的那个文件上面,用external unit6_t Num,这样就行了,使用extern声明;就是告诉编译器,我现在有Num这个变量,他在别的文件定义了,至于在哪里,你自己帮我找好,最终在main。c找到,那编译器就知道了,他就会把这个external声明的变量,当作main.C变量的一个引用,注意这个过程没有新定义变量,他操作的还是main.c里的这个Num,其实我们头文件里面的函数声明,也是用extern 实现的。在这个函数的前面,是有一个extern的,只不过这个extern可以省略,所以我们一般不写。
当我们用extern声明了主函数的Num变量时,就可以在这里直接使用主函数的Num变量了,
2、 把定时器中断函数移动到主函数中去。这样变量就这一个.c文件里了,就不用再使用extern了;
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "Timer.h"
#include "OLED.h"
uint16_t Num;
int main(void)
{
OLED_Init();
Timer_Init();
OLED_ShowString(1,2,"Hello STM32 MCU");
OLED_ShowString(2,1,"Num:");
while(1)
{
OLED_ShowNum(1,5,Num,5);
}
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update)== SET)
{
Num++;
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
初始化后看到直接进入中断1次,为什么:
有个缓存寄存器,这时为了我们写的值只有在更新事件时,才会真正起作用,所以这里为了让值立刻起作用,就在这最后,手动生成了一个更新事件,这样预分频器的值就有效了,但同时,它的副作用就是,更新事件和更新中断是同时发生的,,更新中断会置更新中断标志位,当我们之后一但初始化
完了,更新中断就会立刻进入,这就是我们刚一上电,就立刻进中断的原因,解决方案也很简单,在TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure)的后面,开启中断的前面,再手动调用一下,TIM_ClearFlag(TIM2,TIM_FLAG_Update);手动把更新标志位清除一下,就能避免刚初始化完就进中断的问题了。
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2,TIM_FLAG_Update); //手动把更新标志位清除一下,就能避免刚初始化完就进中断的问题了
ARR和PSC
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "Timer.h"
#include "OLED.h"
uint16_t Num;
int main(void)
{
OLED_Init();
Timer_Init();
OLED_ShowString(1,2,"Hello STM32 MCU");
OLED_ShowString(2,1,"Num:");
while(1)
{
OLED_ShowNum(2,5,Num,5);
OLED_ShowNum(3,5,TIM_GetCounter (TIM2),5); //查看CNT计数器值的变化
}
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update)== SET)
{
Num++;
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
代码
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "Timer.h"
#include "OLED.h"
uint16_t Num;
int main(void)
{
OLED_Init();
Timer_Init();
OLED_ShowString(1,2,"Hello STM32 MCU");
OLED_ShowString(2,1,"Num:");
while(1)
{
OLED_ShowNum(2,5,Num,5);
}
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update)== SET)
{
Num++;
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
Timer.Ch
#include "stm32f10x.h" // Device header
extern uint16_t Num;
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,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=7200-1; //ARR
TIM_TimeBaseInitStructure.TIM_Prescaler=10000-1; //PSC
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2,TIM_FLAG_Update); //手动把更新标志位清除一下,就能避免刚初始化完就进中断的问题了
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); //更新中断到NVIC的通路
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel=TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM2,ENABLE);
}
/*
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update)== SET)
{
Num++;
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
*/
#ifndef __TIMER_H
#define __TIMER_H
void Timer_Init(void);
#endif
实验现象
定时器中断Num++
二、外部时钟
TIM_ETRClockMode2Config();
通过ETR引脚的外部时钟模式2配置,
TIM_ETRClockMode2Config(TIM2,TIM_ExtTRGPSC_OFF,TIM_ExtTRGPolarity_NonInverted,0x00);
引脚要用到GOIO,在这之前还要配置GPIO,
给了上啦输入,也是可以的。浮空输入时外部功率很小,内部的上拉电阻可能影响到这个输入信号,这时候就可以用浮空输入,防止影响外部输入的电平,
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "Timer.h"
#include "OLED.h"
uint16_t Num;
int main(void)
{
OLED_Init();
Timer_Init();
OLED_ShowString(1,2,"Hello STM32 MCU");
OLED_ShowString(2,1,"Num:");
OLED_ShowString(3,1,"CNT:");
while(1)
{
OLED_ShowNum(2,5,Num,5);
OLED_ShowNum(3,5,Timer_GetCounter(),5);
}
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update)== SET)
{
Num++;
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
main.C
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "Timer.h"
#include "OLED.h"
uint16_t Num;
int main(void)
{
OLED_Init();
Timer_Init();
OLED_ShowString(1,2,"Hello STM32 MCU");
OLED_ShowString(2,1,"Num:");
OLED_ShowString(3,1,"CNT:");
while(1)
{
OLED_ShowNum(2,5,Num,5);
OLED_ShowNum(3,5,Timer_GetCounter(),5);
}
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update)== SET)
{
Num++;
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
Timer.CH
#include "stm32f10x.h" // Device header
extern uint16_t Num;
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//TIM_InternalClockConfig(TIM2); //好多人不写,默认的是内部时钟
TIM_ETRClockMode2Config(TIM2,TIM_ExtTRGPSC_OFF,TIM_ExtTRGPolarity_NonInverted,0xaa);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1; //滤波的分频关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up ; //向上计数
TIM_TimeBaseInitStructure.TIM_Period=10-1; //ARR
TIM_TimeBaseInitStructure.TIM_Prescaler=1-1; //PSC
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2,TIM_FLAG_Update); //手动把更新标志位清除一下,就能避免刚初始化完就进中断的问题了
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); //更新中断到NVIC的通路
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel=TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM2,ENABLE);
}
uint16_t Timer_GetCounter(void)
{
return TIM_GetCounter (TIM2);
}
/*
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update)== SET)
{
Num++;
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
*/
#ifndef __TIMER_H
#define __TIMER_H
void Timer_Init(void);
uint16_t Timer_GetCounter(void);
#endif
实验现象
这时上面是Num,下面是CNT计数器的值,我们用挡光片档一下,CNT加1,因为现在时基单元没有预分频,所以每次遮挡CNT都会加1如果有预分频了就是遮挡几次,才能加1次,然后加到9后,自动清零,同时申请中断,Num++;这里不准需要加滤波。
外部时钟源完成计数
问题
1、复杂凌乱
总结
本节课主要学了了如何配置定时器,首先是定时器中断,其中还多了对滤波器的设置,外部毛刺确实可以设置,主要是按照上一节课的理论也是打通这个线路,第一步:RCC开启时钟,这个基本上每个代码都是第一步,不用想定时器的基准时钟和整个外设的工作时钟就都会同时打开了。第二步:选择时基单元的时钟源,对于定时中断,我们就选择内部时钟源第三步:配置时基单元:包括这里的预分频器、自动重装定时器、技术模式等等;第四步:配置输出中断控制,允许更新中断输出到NVIC;第五步:配置NVIC,在NVIC中打开定时器中断的通道,并分配一个优先级;第六步:运行控制了。整个模块配置完成后,我们还需要是能一下计数器,要不然计数器不会运行的.当定时器使能后,计数器就会开始计数了,当计数器更新时,触发中断,最后在写一个定时器中断函数。这样这个中断函数每隔一段时间就能自动执行一次了。然后是选择外部时钟作为时钟源的设置。