RCC: reset and clock control 复位和时钟控制

配置系统时钟,开启/关闭各个外设的时钟电源。

一般首要操作就是通过RCC去打开相应外设的时钟,比如:

1
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);

通过RCC打开APB2总线外设的GPIOC的时钟,使他开启(ENABLE)。

GPIO_InitTypeDef 结构体

对GPIO进行设置/初始化之前,要先定义一个结构体,这个模板里规定了:如果你想配置 GPIO,你必须提供 模式速度引脚号 这三样东西。

1
2
3
4
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_PP;
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_13;
GPIO_InitStryct.GPIO_Speed=GPIO_Speed_50MHz;

然后再去对GPIO进行配置

1
GPIO_InitStryct.GPIO_Speed=GPIO_Speed_50MHz;

GPIO_SetBits/GPIO_ResetBits

1
2
3
4
GPIO_SetBits(GPIO_TypeDef* GPIOX , GPIO_Pin);
// 将某个GPIO引脚输出为高电平。
GPIO_ResetBits(GPIO_TypeDef* GPIOX , GPIO_Pin);
// 将某个GPIO引脚输出为低电平。

GPIO_WriteBits

1
2
3
GPIO_WriteBits(GPIO_TypeDef* GPIOX , GPIO_Pin , BitVal);
// 其中BitVal是枚举类型可以设置为Bit_Reset(低电平)=0和Bit_Set(高电平)=1。
// 将某个GPIO引脚输出为高/低电平。

同时初始化多个GPIO要用或(|)而不是与(&)

1
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_0|GPIO_Pin_1 | GPIO_Pin_2;

因为:

1
2
3
4
5
  0000 0000 0000 0001  (Pin_0)
| 0000 0000 0000 0010 (Pin_1)
| 0000 0000 0000 0100 (Pin_2)
-----------------------
= 0000 0000 0000 0111 (结果是 0x0007)

这是计算过程,如果是与的话,就是:

1
2
3
4
5
  0000 0000 0000 0001  (Pin_0)
& 0000 0000 0000 0010 (Pin_1)
& 0000 0000 0000 0100 (Pin_2)
-----------------------
= 0000 0000 0000 0000 (结果是 0x0000)

因为这三个数的 1 都在不同的位置上,没有任何重叠,所以按位与的结果是 全 0,相当于啥也没干。

推挽输出/开漏输出驱动GPIO

推挽输出:

1
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;

这使得GPIO输出高低电平都有驱动能力。

开漏输出:

1
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_OD;

这使得GPIO输出高电平时没有驱动能力,

输出低电平时才有驱动能力。

上拉输入

1
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IPU;

上拉输入模式,默认保持高电平。

接一个开关按钮,一端接GND,一端接GPIO端口。

输入寄存器默认高电平,也就是默认1(因为是上拉输入模式),如果通过GPIO读取函数读到寄存器的某一位为0,意味着按钮接通了,按钮的一端GND会导致GPIO端口被输入强下拉信号,也就是低电平,因此当检测到输入寄存器的电平为0时,说明按钮被按下。

1
2
3
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0){
//说明按键触发。
}

按键封装函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void key_Init(){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50Hz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
}

uint8_t Key_GetNum(void){
uint8_t KeyNum=0;
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0){
Delay(30);
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0){}
Delay(30);
KeyNum=1;
}
return KeyNum;
}

GPIO初始化固定模版

1
2
3
4
5
6
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_Init(GPIOB, &GPIO_InitStructure);

GPIO读取函数

GPIO作为输入引脚

1
2
GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_15)
// 读取GPIO作为输入引脚,外设给他的电平值,此函数输出为1或0,为uint8_t类型,只有0或者1。

示例应用(eg:通过读取光敏电阻输入的高低电平来控制蜂鸣器)

1
2
3
4
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_15)==1) {
GPIO_ResetBits(GPIOB, GPIO_Pin_12);
}
//通过读取光敏电阻输入的高低电平来控制蜂鸣器。

GPIO作为输出引脚

1
2
GPIO_ReadOutputDataBit(GPIOB,GPIO_Pin_2)
//读取GPIO作为输出引脚,此时的电平值,相当于,我在前面给他设好了电平值,我现在读取一下,把这个uint8_t类型的1/0的高低电平值用来进行其他操作。

示例应用(eg:摁钮控制LED灯,翻转他的电平,让他亮或灭)

1
2
3
4
5
6
7
8
9
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_15)==0) {
Delay(50);
while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_15)==0){}
if(GPIO_ReadOutputDataBit(GPIOB,GPIO_Pin_2)==1)
GPIO_ResetBits(GPIOB,GPIO_Pin_2);
else{
GPIO_SetBits(GPIOB,GPIO_Pin_2);
}
}

中断配置

GPIO——AFIO——EXTI——NVIC

模块化编程,单开一个文件,简历库函数和中断函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void countsenser_init() {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入,默认高电平。
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; //A3引脚
GPIO_StructInit(&GPIO_InitStructure);
//GPIO初始化
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
//AFIO初始化
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource3);//A3作为外部中断。
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line3;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;//上升沿触发中断。
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
//EXTI初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置优先级。
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
//NVIC初始化


void EXTI3_IRQHandler() {
if (EXTI_GetITStatus(EXTI_Line3) != RESET) //检查标志位是不是被触发,标志位函数默认为0,触发为1。
{
//自由发挥
}
EXTI_ClearITPendingBit(EXTI_Line3); //!!必须写上这个函数,每一次中断结束后都要清除标志位。
}

定时器配置中断

按照上图顺序从左到右依次配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
void Timer_Init(void) {
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
//使能RCC内部时钟TIM2
TIM_InternalClockConfig(TIM2);
//TIM2设置内部时钟模式,这行可以不写,因为默认就是内部时钟模式


TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //这里用不到,默认DIV1。
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数模式:向上计数。
TIM_TimeBaseStructure.TIM_Period = 10000-1; //计数10000就触发中断。
TIM_TimeBaseStructure.TIM_Prescaler = 7200-1;
//72MHz/7200=72,000,000Hz/7200=10000,把时钟频率从72MHz降到10000Hz,也就是每秒跳动10000次。
//TIM_Period和TIM_Prescaler组合,实现了1s触发中断一次。
TIM_TimeBaseStructure.TIM_RepetitionCounter=0; //重复计数器,高级定时器才需要,一会把他注释掉看看能不能用。
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
//配置时基单元
//--------------------------------------------------------------------------//


//下面是使能中断(IT)部分,不用中断就不用设置
TIM_ClearFlag(TIM2, TIM_IT_Update);
//STM32系统默认配置后后会触发一次中断,所以我们要在使能TIM2之前先清除标志位。
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //定时器2使能中断,向上计数。
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
//配置通道,这里和GPIO那边的通道做区分,GPIO那边是EXTI9_5_IRQn这种。
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
//NVIC配置
//--------------------------------------------------------------------------//


TIM_Cmd(TIM2, ENABLE);
//开启TIM2,前面都是配置默认使能等等,这里是给他明确指令,开始干活。
}

//下面是中断函数
void TIM2_IRQHandler(void) {
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
//xxxxxxxxxxxxxxx
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}

上方是使用内部时钟模式

下方是使用ETR外部触发输入来计数。

也就是把:

TIM_InternalClockConfig(TIM2);

换成:

TIM_ETRClockMode2Config();

具体如下:

1
TIM_ETRClockMode2Config(TIM2,TIM_ExtTRGPSC_OFF,TIM_ExtTRGPolarity_NonInverted,0x03);

四个参数:

TIM2:选择定时器2

TIM_ExtTRGPSC_OFF:不分频,外部触发一次,计数一次。

TIM_ExtTRGPolarity_NonInverted:上升沿或高电平触发。

0x03:信号滤波,一般设0x03就行。

1
2
TIM_TimeBaseStructure.TIM_Period = 10-1;
TIM_TimeBaseStructure.TIM_Prescaler = 1-1;

TIM_Period = 10-1,意味着计数器记到10就触发中断。

TIM_Prescaler = 1-1,意味着外部触发信号来一次,计数器就记一次。而上一个代码用内部时钟计数时,为啥要写7200-1,是因为内部时钟触发信号相当于一秒来72,000,000次,所以你必须先分频7200次,相当于每秒来10000次,而同时TIM_Period设置为10000-1,即计数器加到10000才触发一次中断,这样就实现了一秒触发一次中断。

PWM实现呼吸灯

PWM主要靠定时器,不需要中断,所以不用配置NVIC,EXTI等等,需要配置定时器和GPIO。

PWM初始化函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
void PWM_Init() {		
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
//开启TIM2时钟


TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Period = 100-1; //ARR
TIM_TimeBaseStructure.TIM_Prescaler = 720-1; //PSC
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
//时基单元初始化


TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);
//先把TIM_OCInitStructure结构体中的参数都初始化默认,防止出错,下面把需要修改的参数单独修改
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
//一般用PWM就默认这个模式就可以了:TIM_OCMode_PWM1
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
//设置极性,一般用High
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
//使能,不必多说
TIM_OCInitStructure.TIM_Pulse= 0; //CCR 由于上面ARR与PSC的赋值,这里的CCR在0-100代表PWM的占空比
//设置CCR比较值
TIM_OC1Init(TIM2,&TIM_OCInitStructure);
TIM_OC2Init(TIM2,&TIM_OCInitStructure);
TIM_OC3Init(TIM2,&TIM_OCInitStructure);
TIM_OC4Init(TIM2,&TIM_OCInitStructure);
//输出比较单元初始化,看你的GPIO对应的是哪个通道。


RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//初始化GPIO口

TIM_Cmd(TIM2, ENABLE);//最后一步,开启TIM2
}

void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare);
}
//把写入CCR的值封装成一个可读性高的函数。

void PWM_SetPSC(uint16_t PSC) {
TIM_PrescalerConfig(TIM2,PSC,TIM_PSCReloadMode_Immediate);
}
//把写入PSC的值封装成一个可读性高的函数。

到这里实现了LED灯以百分之20的功率输出。但我们要实现呼吸灯的话,还要实时调整CCR的值,让他在0-100之间均匀的变化。

有一个库函数可以设置/写入CCR的值,TIM_SetCompare4(TIM2,X);,这个函数可以实时指定输出比较单元的CCR。

1
2
3
4
5
TIM_SetCompare1(TIM2,X);
TIM_SetCompare2(TIM2,X);
TIM_SetCompare3(TIM2,X);
TIM_SetCompare4(TIM2,X);
//写入/设置TIM2不同通道的CCR的值。

因此可以把TIM_SetCompare4(TIM2,X);放到主程序的for循环中,这样就可以实时增加/减少PWM的占空比,从而实现呼吸灯。

具体如下:

1
2
3
4
5
6
7
8
9
10
11
void main(){
for(i=0;i<100;i++)
{
TIM_SetCompare4(TIM2,i);
}
for(j=0;j<100;j++)
{
TIM_SetCompare4(TIM2,100-i);
}
//占空比从0-100,再从100-0;
}

引脚重映射(暂时了解)

针对TIM2的引脚重映射

!注重映射的配置寄存器位于 AFIO 模块内:重映射的配置信息(比如把 TIM2_CH1 从 PA0 重映射到 PA15)是存储在 AFIO 模块内部的寄存器(如 AFIO_MAPR)中的。

1
2
3
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //先开启AFIO引脚。
GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2,ENABLE);
//使用图中第二栏,部分重映射,将TIM2_CH1_ETR功能从PA0重映射到PA15,同时TIM2_CH2功能从PA1重映射到PB3,剩下两个通道不变。

PWM控制直流电机转速(不包括方向)

主要是C语言逻辑这一块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uint8_t i;
uint8_t j=30;
while (1)
{
i=Key_GetNum(); //封装的按键函数,用i获取。
if (i==1) {
j-=10;
GPIO_SetBits(GPIOA, GPIO_Pin_4);
GPIO_ResetBits(GPIOA, GPIO_Pin_5);//配置两个GPIO来确定方向。
if (j==0) {
j=30;
}
PWM_SetCompare3(j);//输出RCC的值,这里直接等于PWM的占空比。
}
OLED_ShowSignedNum(1, 7, j, 3);
}

PWMI模版

输入捕获部分,任何从外部输入的信号想要进入定时器,都需要配置TIM_ICInit

先配置GPIO,然后时基单元(TIM),然后输入捕获单元(IC)初始化(包括选择通道,滤波器,边沿检测,极性选择,分频器),然后触发源选择,从模式选择,启动定时器。

计数值存在CCR1中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void IC_Init(void) {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//GPIO配置

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
//因为TIM2用来输出波形,所以TIM3用来接收波形
TIM_InternalClockConfig(TIM3);
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Prescaler = 72-1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Period = 65536-1;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
//时基单元配置

TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICStructInit(&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICFilter=0x03;
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);
//输入捕获单元

TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
//触发源选择
TIM_SelectSlaveMode(TIM3,TIM_SlaveMode_Reset);
//从模式选择
TIM_Cmd(TIM3, ENABLE);
//启动定时器
}
uint16_t IC_GetFreq(){
return 1000000/TIM_GetCapture1(TIM3);
}
//读取CCR的值结合公式封装成获取频率的函数。

有一个函数可以实时读取CCR的值,就是TIM_GetCapture1(TIMX);结果会返回一个uint16_t的整型常量。

1
2
3
4
5
	TIM_GetCapture1(TIM3);
TIM_GetCapture2(TIM3);
TIM_GetCapture3(TIM3);
TIM_GetCapture4(TIM3);
//读取定时器3不同通道的CCR的值,并输出/返回一个整型常量。

PWMI双通道

前面GPIO,TIM基本不变,主要是输入捕获部分要多设置一个通道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
TIM_ICInitTypeDef  TIM_ICInitStructure;
TIM_ICStructInit(&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICFilter=0x03;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
//通道一。
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
TIM_ICInitStructure.TIM_ICFilter=0x03;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Falling;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_IndirectTI;
//通道二选择触发信号为交叉通道输入。
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInit(TIM3, &TIM_ICInitStructure);//每个通道又要初始化设置一下。
//通道二。
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
//TI1FP1,触发源选择了通道一,而非通道二。
//从模式选择
TIM_SelectSlaveMode(TIM3,TIM_SlaveMode_Reset);
//启动定时器
TIM_Cmd(TIM3, ENABLE);

获取频率和占空比的函数封装

1
2
3
4
5
6
7
uint16_t IC_GetFreq() {
return 1000000/TIM_GetCapture1(TIM3);
//TIM_GetCapture1(TIM3)是读取CCR的值
}
uint16_t IC_GetDuty() {
return TIM_GetCapture2(TIM3)*100/TIM_GetCapture1(TIM3);
}

双通道测频率和占空比原理

对于上图用来捕获频率和PWM占空比。输入捕获部分设置了两路通道,一路上升沿触发,一路下降沿,而触发源则特定选择了通道一,来触发从模式Reset(计数清零)。所以每次上升沿来了,计数都会清零。但清零前,STM32硬件电路会把计数器CNT的值存到CCR1中,这是硬件电路自己完成的,不需要软件操作。这样我们就通过CNT的计数,知道了一个周期的时间。而通道二下降沿来临,计数器CNT的值会计入到CCR2中。这样就通过CCR2的计数知道了高电平占总周期的比例,即占空比。

定时器的编码器接口及其库函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
void Encoder_Interface_Init(void) {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;
GPIO_Init(GPIOA, &GPIO_InitStructure);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
//GPIO配置

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Prescaler = 1-1; //不分频PSC
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;//没用
//因为目前定时器的计数模式被编码器接口托管,内部本身不计数。
TIM_TimeBaseStructure.TIM_Period = 65536-1;//自动重装值, 满量程计数ARR
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
//定时器配置

TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICStructInit(&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICFilter=0x0A;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICFilter=0x0A;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
//输入捕获部分配置,要配置两路

TIM_EncoderInterfaceConfig(TIM3,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);
////编码器接口配置

TIM_Cmd(TIM3, ENABLE);
//开启定时器
}


int16_t GetCounter(void) {
int16_t Temp =0;
Temp=TIM_GetCounter(TIM3); //TIM_GetCounter()是获取CNT的值的库函数
TIM_SetCounter(TIM3,0); //TIM_SetCounter()是写入CNT的值的库函数
return Temp;
}
//在主函数中用OLED显示,并Delay_ms(1000),及得到旋转编码器的速度。

//---------------------用中断实现----------------------------//
//首先在Timer函数中已经配置好了TIM2,内部时钟计数,且1s钟触发一次中断//
//在主函数中的main函数下面写入中断函数//
void TIM2_IRQHandler() {
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
counter=TIM_GetCounter(TIM3);
TIM_SetCounter(TIM3,0);
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}


ADC

DMA工作的三个条件:

  1. 传输计数器不为0。
  2. DMA使能。
  3. 触发源(软件触发/硬件触发)有信号。

单通道,非连续转换,非扫描模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void AD_Init(){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;//AIN是ADC专属模式,模拟输入
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
//GPIO配置,A1引脚被初始化为模拟输入引脚

RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
//开启ADC1的时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//对ADC进行分频,72/6=12MHz
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_28Cycles5 );
//点菜菜单:选择规则组,在ADC中16个序列中的序列1写入通道1,采样时间是28.5个ADC时钟周期。其中一个ADC时钟周期的时间为1/12,000,000秒,那么28.5个周期时间约为2.375微秒(us)
ADC_InitTypeDef ADC_InitStruct;
ADC_InitStruct.ADC_Mode=ADC_Mode_Independent;//设置 ADC 为独立模式
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;//数据右对齐
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
//不使用外部触发转换,也就是软件触发,使用库函数ADC_SoftwareStartConvCmd(ADC1, ENABLE);来执行转换开始
ADC_InitStruct.ADC_ScanConvMode = DISABLE;//关闭扫描模式(即非扫描模式)。
ADC_InitStruct.ADC_ContinuousConvMode = DISABLE;//ENABLE:连续转换模式,DISENABLE:单次转换模式
ADC_InitStruct.ADC_NbrOfChannel=1;//通道数目,目前是一个通道
ADC_Init(ADC1, &ADC_InitStruct);
ADC_Cmd(ADC1, ENABLE);//开启ADC1,等待工作,等待触发源

//校准ADC,固定操作//
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1)==SET);
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1)==SET);
}

uint16_t AD_GetValue(void){
ADC_SoftwareStartConvCmd(ADC1,ENABLE);//软件触发转换指令
while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)==RESET){}
///死循环等待,直到 ADC 转换完成标志位(EOC, End Of Conversion)被置位
return ADC_GetConversionValue(ADC1);
//在 STM32 中,当你调用这个函数读取数据时,硬件会自动把刚才的 EOC(转换完成)标志位清零,为下一次转换做好准备。
}

单通道,连续转换,非扫描模式

把上述代码AD_Init函数中的结构体配置中ADC_ContinuousConvModeDISABLE修改为ENABLE

并且自始至终只需要一次软件触发开启AD转换,因此直接把这个指令塞进初始化函数里即可。

1
2
ADC_InitStruct.ADC_ContinuousConvMode = ENABLE;
ADC_SoftwareStartConvCmd(ADC1,ENABLE);//软件触发转换指令,加在最后。

而下面的uint16_t AD_GetValue(void)函数只需要return ADC_GetConversionValue(ADC1);即可,不需要判断EOC标志位是否被置位,因为全程连续转换,并不是单次转换。

1
2
3
4
uint16_t AD_GetValue(void){
return ADC_GetConversionValue(ADC1);
//在 STM32 中,调用这个函数读取数据。
}

单通道模式实现多通道模式,非连续转换,非扫描模式(不用硬件电路,而是用代码实现)

AD_Init函数中配置通道的函数:ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_28Cycles5 );把第二个参数ADC_Channel_1变成一个变量,然后在循环中不断输入不同的通道,因为每次转换都在几微秒内完成,因此在人眼来看,就是4个通道同时进行。

具体实现,先把ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_28Cycles5 );拿出来,然后封装一个需要输入形参的函数,然后把形参输入到第二个变量位置,即可实现。

1
2
3
4
5
6
7
uint16_t AD_GetValue(uint8_t ADC_Channel) {
ADC_RegularChannelConfig(ADC1, ADC_Channel, 1, ADC_SampleTime_28Cycles5 );
ADC_SoftwareStartConvCmd(ADC1, ENABLE);//软件触发转换
while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)==RESET){}
//死循环等待,直到 ADC 转换完成标志位(EOC, End Of Conversion)被置位。
return ADC_GetConversionValue(ADC1);
}

然后在主函数的循环中,写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
while (1)
{
AD1=AD_GetValue(ADC_Channel_1);
AD2=AD_GetValue(ADC_Channel_2);
AD3=AD_GetValue(ADC_Channel_3);
AD4=AD_GetValue(ADC_Channel_4);
OLED_ShowString(1,1,"AD1:");
OLED_ShowString(2,1,"AD2:");
OLED_ShowString(3,1,"AD3:");
OLED_ShowString(4,1,"AD4:");
OLED_ShowNum(1,5,AD1,5);
OLED_ShowNum(2,5,AD2,5);
OLED_ShowNum(3,5,AD3,5);
OLED_ShowNum(4,5,AD4,5);
}

表示变量地址

在stm32中,所有的地址都是16进制的,也就是0x……。

1
2
3
4
5
6
7
8
9
10
11
12
13
const uint8_t a=66;
uint8_t b=77;
int main(void)
{
OLED_Init();
while (1)
{
OLED_ShowNum(1,1,a,3);
OLED_ShowHexNum(2,1,(uint32_t)&a,8);//地址都是16进制的,所以要用ShowHexNum。
OLED_ShowNum(3,1,b,3);
OLED_ShowHexNum(4,1,(uint32_t)&b,8);//地址都是16进制的,所以要用ShowHexNum。
}
}

现象如下:

由上图结合代码可知,在变量a前加入const,那么a就被存储到了Flash中,成为了常量。而b作为uint8_t的变量,则被存储到了SRAM中。

所以如果有一个很大的表或者数据库,一般不需要修改,那就在前面加上const,让他存放在FLash中,而非SRAM中。

ADC1->DR“去那个地址把数据拿回来”

&ADC1->DR 才是“那个地址本身”

DMA转运数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
uint8_t Transfer_Size;
void My_DMAInit(uint32_t AddrA , uint32_t AddrB , uint8_t Size) {
Transfer_Size=Size; //为下面的函数设置传输计数器
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //DMA是AHB总线上的
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA; //外设地址,源端
DMA_InitStructure.DMA_PeripheralDataSize= DMA_PeripheralDataSize_Byte; //每次转运数据大小,这里是一个字节
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable; //地址是否自增
DMA_InitStructure.DMA_MemoryBaseAddr = AddrB; //内存地址,目的地
DMA_InitStructure.DMA_MemoryDataSize = DMA_PeripheralDataSize_Byte; //每次转运数据大小,这里是一个字节,与源端统一
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //地址是否自增
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //外设作为数据源,还有一个参数是DMA_DIR_PeripheralDST,这个是外设作为目的地
DMA_InitStructure.DMA_BufferSize = Size; //传输计数器,每次转运一次,这个数就减一,减到0就停止转运/转运完成
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //模式,是否循环,这里的Normal是不循环,如果是DMA_Mode_Circular那就是循环
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; //M2M是软件转运,这里是开启软件转运
DMA_InitStructure.DMA_Priority = DMA_Priority_High; //设置优先级,这里就一个DMA,所以随便设置
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //M2M,存储器到存储器
DMA_Cmd(DMA1_Channel1, DISABLE); //先不使能,在下面的函数里使能
}

void DMA_Transfer(void) {
DMA_Cmd(DMA1_Channel1, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1, Transfer_Size); //自己设置传输计数器。
DMA_Cmd(DMA1_Channel1, ENABLE);
while (DMA_GetFlagStatus(DMA1_FLAG_TC1==RESET)); //检测转运标志位,判断是否转运完成
DMA_ClearFlag(DMA1_FLAG_TC1); //转运完成了,记得手动清除标志位
}

ADC扫描模式+DMA

1

串口USART通信

配置流程图如下:

TX设置复用输出,RX设置输入。图片中间部分全部通过结构体配置。

1

拆分数字的思路

12345/10000%10=1

12345/1000%10=2

12345/100%10=3

12345/10%10=4

12345/1%10=5

想取出某个数的第x位,就把这个数除以(10的(x-1)次方),再对10取余。