[TOC]

任务(1):串⼝中断接收回显

知识学习(技术点介绍——>通信协议/双工模式)

通信的目的/协议

image-20241021105006393

  • 在stm32内部,pwm输出,定时器计数等功能,都是在单片机内部的输出寄存器,数据寄存器实现的

  • 在stm32外部,想要使用其他的外挂的芯片,就要与stm32进行通信

  • 协议:双方约定的用于通信的规则

  • USART TX: Transmit Exchange 数据发送 RX: Receive Exchange 数据接收

  • I2C SCL: 时钟 SDA:数据

  • SPI SCLK:时钟 MOSI:主机输出数据脚 MISO: 主机输入数据脚 CS:片选,用于指定通信对象

  • CAN CAN-H CAN_L:两个差分数据脚,用两个引脚表示一个差分的数据

  • USB DP DM,或者脚D+ D-, 也是一对差分数据脚

双工模式的区别:

  • 全双工:是指通信双方能够同时进行双向通信,一般来说都有两根通信线
  • 半双工:同一时刻 只能进行单方向的通信 (同一时间下只能进行单向传输,但是不同的时间传输的方向可以不同)
  • 单工: 无论什么时刻,都只能由一个设备传到另一个设备,不能反着来 (比如把串口的rx引脚去掉,那串口就退化成单工了)

时钟特性

  • 如果有单独的时钟信号线,就是同步的,没有就是异步的
  • 异步相比于同步,只是说两个设备之间时钟的信息(采样频率)不能传递罢了,所以在开始前设置好两者约定的采样频率,并添加帧头帧尾等,进行采样位置的对齐

电平特性

  • 单端:单端电平所谓的高低电平,都是相对于gnd的,因此单端通信的双方必须共地,因此,前三个通信协议严格来说引脚一栏还要加上GND引脚
  • 差分:差分信号是通过两个信号的电压差来进行通信的,因此不需要接地(主要是说can,因为usb里面有些东西也是单端信号,还是得接地)

设备

  • 点对点:两个设备之间点对点,直接传输就行了
  • 多设备:支持不止有两个设备之间的通信,可以挂载多个设备,在通信前要进行寻址操作,来确定要进行通信的对象

技术点介绍——>串口通信

硬件电路接法:
image-20241021112612894

电平标准(使用的主要都是ttl电平)

image-20241021112838627
串口参数和时序

image-20241021113332364

  • 注意,当串口空闲的时候,始终是高电平,起始位和终止位的作用就是:

    起始位——产生一个下降沿,告诉设备要开始发数据了

    终止位——产生一个上升沿,标志着这个字节传输完成,同时将电平拉回高电平

  • 数据位——低位先行的含义

    假设要发送一个0x0F,第一步,首先把他转化成二进制 00001111 ,然后从低位开始传入,即传入顺序为11110000,产生的波形就是

    image-20241021113946893

    当然,接收的一方也是低位先行,产生的波形就是00001111,还是回到了0 x0F

  • 校验位:

    image-20241021114145525

校验分为奇校验和偶校验,奇校验就是自动补全最后一位,使数据位所有位置上的1为奇数个,偶校验是偶数个

USTAR

image-20241021130136230

  • 最常使用配置:收发器:异步 波特率:9600/115200 数据位长度:8 停止位长度:1 无校验

  • 硬件流控制的意思,就是在rx和tx中还有一根线用于通信,这条线的的信号发射端是接收端设备,默认高电平

    当接收端设备准备好接收的时候,就置低电平,发送端就开始发信号,这样能避免接如果收端性能比较低,还没准备好就接受一堆数据从而出错 ,但是硬件控制流一般也不用

  • usatr资源: usatr1在apb2总线上,usatr2,usatr3在apb1总线上

image-20241021181937363

USTAR基本结构:

image-20241021184406936

串口通信中的hex模式和文本模式

hex模式:以原始数据的形式展示

文本模式:以原始数据编码后的形式显示

示意图:

image-20241022001324298

技术点介绍:串⼝ RXNE 中断(如何配置,如何使⽤)

配置exne中断:

配置中断,无非就是把当前蕴含某个可以当做中断事件的外设连接通路到nvic

比如之前的tim中断,就是使用tim2_it_config函数连接到nvic通路以配置中断

所以类似的,配置中断的第一步也是,配置usart1到nvic的通路

1
2
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);//这句usatr1的itconfig配置了usatr1到nvic的通路

下面就是常规的配置nvic

  • 先分组,默认一般使用group2
  • 创建一个nvic初始化结构体,结构体包含channel,cmd,响应优先级和抢占优先级
  • channel(通道):就具体通道而定,配置谁的nvic,就用谁的nvic到自身的通道,比如对于tim2来说,通道就是tim2_irqn,这里对于usart1来说,通道就是usart1_irqn
  • cmd:就是个开关,enable即可,两个优先级视情况而定
  • 最后初始化nvic结构体,并开启usart开关

关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
//下面配置接收中断
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);//这句usatr1的itconfig配置了usatr1到nvic的通路

//下面配置nvic
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//先分组
NVIC_InitTypeDef NVIC_InitSturcture;
NVIC_InitSturcture.NVIC_IRQChannel = USART1_IRQn; //配置nvic的irqn通道
NVIC_InitSturcture.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitSturcture.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitSturcture.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitSturcture);

USART_Cmd(USART1,ENABLE);//打开开关

这样,nvic就会自动调用中断函数,当中断函数里我们给出的条件为真时,就能只想我们想要的操作

使用exne中断:

中断执行时最核心的代码就是中断执行函数:USART1_IRQHandler

因此我们配置一个判断条件,这里想用exne是否为真来进行判断,所以条件应设为

if(USART_GetITStatus(USART1,USART_IT_RXNE)== SET)

如果这个条件为真,那么说明计算机向单片机发送了数据,我们此时的任务是接受这个数据,并把它回显到计算机上,那在中断函数中,我们可以设置一个标志位来标志单片机接收到了数据,再设置一个临时变量用于存储接收到的数据,在主函数中,如果标志位为真,那就把数据拿出来并发送到计算机上

在serial.c中,设置两个临时变量

1
2
uint8_t Serial_RxData;
uint8_t Serial_RxFlag;

两个封装函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
uint8_t Serial_GetRxFlag(void)
{
if(Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0 ;
}

uint8_t Serial_GetRxData(void)
{
return Serial_RxData;
}

并按照我们的设想配置中断函数

1
2
3
4
5
6
7
8
9
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1,USART_IT_RXNE)== SET)
{
Serial_RxData = USART_ReceiveData(USART1);
Serial_RxFlag = 1;
USART_ClearITPendingBit(USART1,USART_IT_RXNE);//清除标志位
}
}

这样,我们就只需要在主函数中不断检查flag是否为1,如果为1,设置一个临时变量,并将它赋值为getrxdata函数的返回值,然后将这个返回值发回计算机,即可完成

主函数内关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uint8_t RxData;
int main(void)
{
OLED_Init();
Serial_Init();
Serial_Printf("num is %d",10);
while(1)
{
if(Serial_GetRxFlag() == 1)//判断flag是否为1
{
RxData = Serial_GetRxData();//是的话,获取信息
Serial_Printf("%c",RxData);//回显
OLED_ShowHexNum(1,1,RxData,2);
}
}
}

serial.c总代码:

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#include "stm32f10x.h"                  // Device header
#include <stdarg.h>
#include <stdio.h>

uint8_t Serial_RxData;
uint8_t Serial_RxFlag;


void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//USATR1在APB2总线上
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //tx设置为复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //rx设置为上拉输入(输入不存在复用不复用的,因为输出只能有一个,但是输入可以有多个)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);


USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600; //设置波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //设置没有硬件流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx|USART_Mode_Tx; //设置模式为rx和tx都用
USART_InitStructure.USART_Parity = USART_Parity_No; //设置不进行奇偶校验
USART_InitStructure.USART_StopBits = USART_StopBits_1;//设置终止位长度位1
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//设置数据长度为8位

USART_Init(USART1,&USART_InitStructure);//初始化



//下面配置接收中断
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);//这句usatr1的itconfig配置了usatr1到nvic的通路

//下面配置nvic
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//先分组
NVIC_InitTypeDef NVIC_InitSturcture;
NVIC_InitSturcture.NVIC_IRQChannel = USART1_IRQn; //配置nvic的irqn通道
NVIC_InitSturcture.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitSturcture.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitSturcture.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitSturcture);

USART_Cmd(USART1,ENABLE);//打开开关

}

uint8_t Serial_GetRxFlag(void)
{
if(Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0 ;
}

uint8_t Serial_GetRxData(void)
{
return Serial_RxData;
}

void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1,USART_IT_RXNE)== SET)
{
Serial_RxData = USART_ReceiveData(USART1);
Serial_RxFlag = 1;
USART_ClearITPendingBit(USART1,USART_IT_RXNE);//清除标志位
}
}
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1,Byte);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
}

void Serial_SendArray(uint8_t *Array,uint16_t length)
{
uint16_t i;
for(i =0;i<length;i++)
{
Serial_SendByte(Array[i]);
}
}
void Serial_SendString(char* String)
{
uint8_t i;
for(i=0;String[i]!=0;i++)
{
Serial_SendByte(String[i]);
}
}

uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y --)
{
Result *= X;
}
return Result;
}

void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
}
}


int fputc(int ch, FILE *f)
{
Serial_SendByte(ch); //将printf的底层重定向到自己的发送字节函数
return ch;
}

void Serial_Printf(char *format, ...)//多参数
{
char String[100];
va_list arg;
va_start(arg, format);
vsprintf(String, format, arg);
va_end(arg);
Serial_SendString(String);
}




视频演示:

(2)任务二:串口控制LED亮灭

技术点介绍 :串⼝ RXNE 中断(如何利⽤读取到的数据)

使用数据包获取输入的数据:

image-20241023161552791

1. 什么是数据包?

在串口通信中,数据包是用来传输有结构的数据的一种“包装”。它就像寄快递一样,把我们要传输的信息(比如控制命令或传感器数据)装进一个“包裹”里,再通过串口把这个包裹从上位机(比如电脑)发到单片机(STM32)。数据包是为了让双方清楚地知道,传过来的这些数据是什么意思,如何处理,如何检查有没有出错。

2. 数据包的基本组成

一个标准的数据包一般包括以下部分:

  1. 起始符:告诉接收端,“数据要开始了!”。这是一个特殊的字符或字节
  2. 数据字段:这里才是实际有用的信息,可能是传感器的数据、控制命令或者状态反馈。数据字段可以是一个字节或多个字节。
  3. 结束符:告诉接收端,“数据传完了!”。常用的结束符也是一个特定字符,
  4. 有时候,结束符前面还可以加个校验符

例子:假设你想通过串口传递两个字节的数据,我们可以构造一个简单的数据包:

1
起始符@| 数据1| 数据2| 结束符 $

在这个例子中:

  • @ 是起始符
  • 数据1 和 数据2 是实际要传输的数据
  • $ 是结束符
3. 为什么要使用数据包?有什么好处?
  • 数据有序传输,避免混乱,串口通信是逐个字节地传输数据。如果没有数据包的概念,单片机无法知道接收到的某些字节到底是什么——是开始的命令还是中途传过来的数据?使用数据包后,起始符和结束符让STM32清楚地知道数据从哪里开始、哪里结束,避免了数据混乱。
4. 使用数据包的步骤

(1) 构造数据包并发送

  • 上位机(比如电脑)根据预定好的格式,构造一个数据包。假如你想控制一个灯的亮灭,可以构造一个数据包,里面包括:设备ID(代表灯)、控制命令(开或关),以及校验位来保证数据没有出错。

(2) 接收数据包

  • STM32 会通过串口接收数据。在接收到数据时,它首先要找到起始符,确定这是一个新的数据包,然后开始接收后面的数据字段。如果有校验位,还能通过他来检查数据是否正确。

(4) 执行相应操作

  • 解析完成后,STM32可以根据数据字段中的命令,执行对应的操作,比如打开或关闭灯,读取传感器数据等。

画状态机进行程序设计

在设计程序的时候,可以利用状态机来画出程序的流程,来理清逻辑,如下便是文本数据包接收的状态机示意图:

image-20241023162516302

rxne中断的实现:

核心:

同任务一,中断执行时最核心的代码依然是是中断执行函数:USART1_IRQHandler,我们所需要的中断后引发的功能要在这里面配置

方法:

要想让stm32通过接收 ledoff 和ledon 来进行相应的操作,无非是把任务1接收数字的地方改成接收字符串数据包即可,接收数据包的程序按照状态机的画法设计,接收到数据以后,与LEDOFF LEDON这两个字符串进行比对,如果是同一个字符串,那就执行相应操作,对不上号的话就返回error

总结一下步骤:
  1. 使能usart1时钟,gpio时钟,配置好gpio和usatr1的初始化结构体
  2. 使用usatr的itconfig函数打通rxne到nvic的通路
  3. 配置好nvic,设置好中断参数
  4. 编写USART1_IRQHandler函数,把中断函数执行步骤写好

代码实现:

库函数:

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include "stm32f10x.h"                  // Device header
#include <stdarg.h>
#include <stdio.h>

char Serial_RxPacket[100]; //数据包数组
uint8_t Serial_RxFlag; //数据包接受标志位


void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//USATR1在APB2总线上
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //tx设置为复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //rx设置为上拉输入(输入不存在复用不复用的,因为输出只能有一个,但是输入可以有多个)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);


USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600; //设置波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //设置没有硬件流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx|USART_Mode_Tx; //设置模式为rx和tx都用
USART_InitStructure.USART_Parity = USART_Parity_No; //设置不进行奇偶校验
USART_InitStructure.USART_StopBits = USART_StopBits_1;//设置终止位长度位1
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//设置数据长度为8位

USART_Init(USART1,&USART_InitStructure);//初始化



//下面配置接收中断
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);//这句usatr1的itconfig配置了usatr1到nvic的通路

//下面配置nvic
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//先分组
NVIC_InitTypeDef NVIC_InitSturcture;
NVIC_InitSturcture.NVIC_IRQChannel = USART1_IRQn; //配置nvic的irqn通道
NVIC_InitSturcture.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitSturcture.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitSturcture.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitSturcture);

USART_Cmd(USART1,ENABLE);//打开开关

}

uint8_t Serial_GetRxFlag(void)
{
if(Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0 ;
}


void USART1_IRQHandler(void)
{
static uint8_t RxState = 0; //当前状态机状态的静态变量,出函数之后不会销毁,下次用的时候保持上次用最后的值
static uint8_t pRxPacket = 0; //当前接收数据位置的静态变量
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //是否是USART1的接收触发中断
{
uint8_t RxData = USART_ReceiveData(USART1); //读取数据寄存器,存放在接收的数据变量

//根据状态机画的图编写下面的程序:

//rxstate为0,接收数据包包头
if (RxState == 0)
{
if (RxData == '@' && Serial_RxFlag == 0) //数据是包头,并且上一个数据包已处理完了
{
RxState = 1; //下面接受数据
pRxPacket = 0; //数据包的位置归零,开始挨个往里塞
}
}
//restate为1,接收数据包数据,同时判断是否接收到了第一个包尾
else if (RxState == 1)
{
if (RxData == '\r') //如果收到第一个包尾
{
RxState = 2; //置下一个状态
}
else //接收到了正常的数据
{
Serial_RxPacket[pRxPacket] = RxData; //数据存入数据包数组的对应的位置
pRxPacket ++; //标记下一次该存放的位置
}
}
//rxstate为2,接收数据包第二个包尾
else if (RxState == 2)
{
if (RxData == '\n') //如果收到第二个包尾
{
RxState = 0; //状态归0
Serial_RxPacket[pRxPacket] = '\0'; //将收到的字符数据包添加一个字符串结束标志(不然不会自带)
Serial_RxFlag = 1; //rxflag为1,成功接收一个数据包
}
}

USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除标志位
}
}



主函数;

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
#include "stm32f10x.h"      // Device header
#include "LED.h"
#include "DELAY.h"
#include "Serial.h"
#include "Oled.h"
#include "LED.h"
#include "string.h"
uint8_t RxData;
int main(void)
{
OLED_Init();
LED_Init(1);
Serial_Init();
Serial_Printf("num is %d",10);
OLED_ShowString(1, 1, "TxPacket");
OLED_ShowString(3, 1, "RxPacket");
while (1)
{
if (Serial_RxFlag == 1) //如果接收到数据包
{
OLED_ShowString(4, 1, " "); //OLED清除原有的显示
OLED_ShowString(4, 1, Serial_RxPacket); //显示接收到的数据包

//将收到的数据包进行字符串比对,执行对应操作
if (strcmp(Serial_RxPacket, "LEDON") == 0)
{
LED_ON(1);
Serial_SendString("LEDONOK\r\n");
OLED_ShowString(2, 1, " ");
OLED_ShowString(2, 1, "LEDONOK");
}
else if (strcmp(Serial_RxPacket, "LEDOFF") == 0)
{
LED_OFF(1);
Serial_SendString("LEDOFFOK\r\n");
OLED_ShowString(2, 1, " ");
OLED_ShowString(2, 1, "LEDOFFOK");
}
else //输入不是我们指定的格式
{
Serial_SendString("ERROR\r\n");
OLED_ShowString(2, 1, " ");
OLED_ShowString(2, 1, "ERROR");
}

Serial_RxFlag = 0; //将接收数据包标志位清零,否则无法接收后续数据包
}
}
}

实现视频:

任务(3):通过串⼝调整LED闪烁频率

分析:

相比于任务二,无非是改变一下中断事件,这个事件就是改变一个float变量而已

这个float变量范围取值0-3,精度0.1

当然,涉及到时间就要配置一下tim计时器来结合使用

我们搬出之前秒计时器的代码:

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
#include "stm32f10x.h"                  // Device header
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);//使能时钟

TIM_InternalClockConfig(TIM2);

TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1; //ָ设置时钟分频(1分频)
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数
TIM_TimeBaseInitStructure.TIM_Period=10000-1;
TIM_TimeBaseInitStructure.TIM_Prescaler=7200-1; //72mzh / 7200 = 10k ,72mhz / 7200 = 10k
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;//重复计数(高级计时器有,现在不用)
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);

TIM_ClearFlag(TIM2,TIM_FLAG_Update); //清除TIM2的更新中断标志位,确保定时器开始时没有残留的中断标志

TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);//开启更新中断到nvic通路

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
//省略
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM2,ENABLE);

}

对于周期的设定,核心代码就是这两个:

1
2
TIM_TimeBaseInitStructure.TIM_Period=10000-1;
TIM_TimeBaseInitStructure.TIM_Prescaler=7200-1; //72mzh / 7200 = 10k ,72mhz / 7200 = 10k

上面一行是arr(自动重装计数值),下面一行是把72mhz分成7200份

如果说上面是10000的话,那周期就是1秒,如果把上面改成5k,那周期就是0.5秒

因此,我们可以通过只修改arr来实现改变周期

修改arr的函数:

1
void TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);

动态修改 TIM_Period:

1
2
3
4
5
// 修改自动重装载值
TIM_SetAutoreload(TIM2, new_arr);

// 触发更新事件,让新的ARR值立即生效
TIM_GenerateEvent(TIM2, TIM_EventSource_Update);

串口中断,tim中断:

串口中断:

源于上位机的信息输入,中断后引起事件——tim2计时器周期修改

tim中断:

源于自增计数器达到预设值,中断后引起事件——开关led

二者的配合:

将这个程序的执行过程提炼出来,就是:

启用tim2计时器,启用usart1

——>把tim2的自动重装计数事件连接到NVIC,把usart1的rxne事件连接到NVIC

——>配置tim2的中断事件为亮灯灭灯,rxne的中断事件配置为 更改tim2计数器的周期

——>配置usart1的串口输入参数,配置好它的接收数据包格式

——>在主函数中整合计算,分拣数据,实现功能

serial.c:

代码实现:

库函数

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include "stm32f10x.h"                  // Device header
#include <stdarg.h>
#include <stdio.h>

char Serial_RxPacket[100]; //数据包数组
uint8_t Serial_RxFlag; //数据包接受标志位


void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//USATR1在APB2总线上
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //tx设置为复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //rx设置为上拉输入(输入不存在复用不复用的,因为输出只能有一个,但是输入可以有多个)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);


USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600; //设置波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //设置没有硬件流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx|USART_Mode_Tx; //设置模式为rx和tx都用
USART_InitStructure.USART_Parity = USART_Parity_No; //设置不进行奇偶校验
USART_InitStructure.USART_StopBits = USART_StopBits_1;//设置终止位长度位1
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//设置数据长度为8位

USART_Init(USART1,&USART_InitStructure);//初始化



//下面配置接收中断
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);//这句usatr1的itconfig配置了usatr1到nvic的通路

//下面配置nvic
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//先分组
NVIC_InitTypeDef NVIC_InitSturcture;
NVIC_InitSturcture.NVIC_IRQChannel = USART1_IRQn; //配置nvic的irqn通道
NVIC_InitSturcture.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitSturcture.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitSturcture.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitSturcture);

USART_Cmd(USART1,ENABLE);//打开开关

}

uint8_t Serial_GetRxFlag(void)
{
if(Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0 ;
}


void USART1_IRQHandler(void)
{
static uint8_t RxState = 0; //当前状态机状态的静态变量,出函数之后不会销毁,下次用的时候保持上次用最后的值
static uint8_t pRxPacket = 0; //当前接收数据位置的静态变量
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //是否是USART1的接收触发中断
{
uint8_t RxData = USART_ReceiveData(USART1); //读取数据寄存器,存放在接收的数据变量

//根据状态机画的图编写下面的程序:

//rxstate为0,接收数据包包头
if (RxState == 0)
{
if (RxData == '@' && Serial_RxFlag == 0) //数据是包头,并且上一个数据包已处理完了
{
RxState = 1; //下面接受数据
pRxPacket = 0; //数据包的位置归零,开始挨个往里塞
}
}
//restate为1,接收数据包数据,同时判断是否接收到了第一个包尾
else if (RxState == 1)
{
if (RxData == '\r') //如果收到第一个包尾
{
RxState = 2; //置下一个状态
}
else //接收到了正常的数据
{
Serial_RxPacket[pRxPacket] = RxData; //数据存入数据包数组的对应的位置
pRxPacket ++; //标记下一次该存放的位置
}
}
//rxstate为2,接收数据包第二个包尾
else if (RxState == 2)
{
if (RxData == '\n') //如果收到第二个包尾
{
RxState = 0; //状态归0
Serial_RxPacket[pRxPacket] = '\0'; //将收到的字符数据包添加一个字符串结束标志(不然不会自带)
Serial_RxFlag = 1; //rxflag为1,成功接收一个数据包
}
}

USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除标志位
}
}



主函数:

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
65
66
67
68
69
70
71
72
73
74
#include "stm32f10x.h"      // Device header
#include "LED.h"
#include "DELAY.h"
#include "Serial.h"
#include "Oled.h"
#include "LED.h"
#include "string.h"
#include "Timer.h"
uint8_t RxData;
uint8_t ledstate = 0;
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update) == SET)
{
if(ledstate == 0)
{
ledstate = 1;
LED_ON(1);
}
else
{
ledstate = 0;
LED_OFF(1);
}
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}

int main(void)
{
OLED_Init();
LED_Init(1);
Serial_Init();
Timer_Init();


while (1)
{
if (Serial_RxFlag == 1) //如果接收到数据包
{
OLED_ShowString(1,1,"num:");
OLED_ShowString(1,6,Serial_RxPacket);

float tempint = Serial_RxPacket[0]-48;
float tempflo = Serial_RxPacket[2]-48;
int arraylength = strlen(Serial_RxPacket);
if(((Serial_RxPacket[1]!='.' && Serial_RxPacket[1]=='0')&&(arraylength>1))||arraylength>3)
{
OLED_ShowString(3,1,"error input");
}
else
{
int arrtemp = 10000*(tempint + tempflo*0.1);

if(arrtemp>0&&arrtemp<=30000)
{ OLED_ShowNum(2,1,arrtemp,8);
OLED_ShowString(3,1,"valid input");
// 修改自动重装载值
TIM_SetAutoreload(TIM2, arrtemp-1);

// 触发更新事件,让新的ARR值立即生效
TIM_GenerateEvent(TIM2, TIM_EventSource_Update);
}
else
{
OLED_ShowString(3,1,"error input");
}
}

Serial_RxFlag = 0; //将接收数据包标志位清零,否则无法接收后续数据包
}
}
}

视频效果:

任务(4) Debug 功能和使⽤

debug功能简记

1. 进入 Debug 模式

  1. 编译项目
    • 在菜单栏中,点击 “Project” -> **”Build Target”**(或直接点击工具栏中的小锤子图标)。
  2. 进入 Debug 模式
    • 点击工具栏中的 “Debug” 按钮(一个带有小虫子的图标),或从菜单中选择 “Debug” -> **”Start/Stop Debug Session”**。

2. 复位 MCU

  • 复位

    在 Debug 界面上,找到工具栏中的 “Reset” 按钮image-20241023222235399,点击该按钮即可复位

3. 调试运行模式

在调试模式下,有以下按钮在上方工具栏:

  • 全速运行(F5)

    image-20241023222252338这个按钮将使程序一直处于运行状态,或者直接运行到设置的断点处。

  • 单步执行

    image-20241023222301550点每点一次按钮,程序运行一步,遇到函数会进入函数执行

  • 逐行调试

    image-20241023222336782每点一次按钮,程序运行一行,遇到函数跳过函数执行

  • 跳出调试

    image-20241023222437618在代码中找到你想运行到的行,右键点击该行,然后选择 **”Run to Cursor”**。

  • 运行到光标处

    image-20241023222526194直接运行到光标处

  • 前进后退

    image-20241023222957219返回上一步调试,进行下一步调试

4. 断点 (Breakpoint)

  • 设置断点

    image-20241023222555566设置当前光标处是断点

  • 失能断点

    image-20241023222641250失能当前光标处的断点 image-20241023222703855失能所有断点 image-20241023222739694删除所有断点

debug用法示例

设置断点

  • 行断点:在源代码窗口,选择需要暂停的代码行,双击行号或右键选择 “Insert/Remove Breakpoint”
  • 条件断点:右键断点,选择 **”Edit Breakpoint”**,设置条件(例如变量的值或特定的表达式)

逐步执行代码

  • 单步执行(Step Over):点击 F10,跳过函数调用但执行完整的单条语句。
  • 单步跳入(Step Into):点击 F11,进入当前行的函数体内,逐行查看函数的内部执行流程。
  • 跳出函数(Step Out):点击 Shift + F11,继续执行至函数结束并返回。
  • 运行到光标处(Run to Cursor):右键选择 “Run to Cursor”,代码将从当前执行位置直接运行到你选择的行。

观察变量和寄存器

  • 观察变量

    Watch 窗口(右键变量,选择 Add to Watch Window),可以查看变量值的动态变化。

    通过“Memory”窗口,可以直接查看并修改特定内存地址的内容。

  • 观察寄存器

    “Registers”窗口 中查看 CPU 寄存器的状态,跟踪数据处理和寄存器值的变化。

观察和修改内存内容

  • Memory 窗口

    通过 “View” > “Memory Windows” > “Memory 1/2” 来打开内存观察窗口。

    输入内存地址,可以实时查看数据。窗口支持查看 ASCIIHex 格式的数据。

  • 实时修改

    右键内存窗口中的值,选择“Modify Memory”可实时更改特定内存位置的值,以测试程序对不同数据的响应。

使用调试信息窗口

  • **调用堆栈 (Call Stack + Locals)**:显示函数调用堆栈,跟踪代码执行路径。适用于调试递归或多层函数嵌套的问题。

  • 硬件外设窗口 (Peripherals)

    在调试过程中查看和配置硬件外设(如 GPIO、串口、定时器)的状态。

    打开 “View” > “System Viewer” 查看不同外设的寄存器和状态。

Advance_project任务

  • 进入debug模式,单步执行至LED_Flashes_Init后,两个灯全部亮起,为正常现象,
  • GPIO_EXIT_Init函数为按键控制外部中断,和bug展示的现象无关,故跳过即可
  • 进入到最有可能有问题的函数:TIM_Flashes_Init

image-20241025183756295

  • 进入到TIM_Flashes_Init函数中,可以发现,这个函数仅调用了一个函数指针:handler

    除此之外,剩下的全是常规的tim计时器初始化以及nvic配置,尚未发现错误

​ 此时,handler指针所指向的函数现已被列为重大嫌疑函数😡

image-20241025184110670
  • 追根溯源handler:

回退到上一步,发现handler指针指向了嫌疑人image-20241025184323222

tick_handler函数依存于Flashes_TIM_IRQHandler(中断处理)函数,后者执行,前者就执行,因此我们找到后者,直接设置断点并转到,或者选中中断处理函数使用ctrl+F10,直接执行到他那里,会发现程序卡在了

image-20241025185037967

看名字也知道,这个函数的作用是清空time,而不是time自减

当然为了严谨,我们还是验证一下:

转到调用它的函数的定义:

image-20241025184456370

简单计算验证一下,这个函数自己的逻辑是没有问题的

那问题就出在他调用的别的函数中

  • 函数一:get_tick()
  • 函数二:clare_tick()

get_tick函数的逻辑是,获取当前自增计数达到阈值产生的中断次数

clare_tick函数的逻辑是,定期清除中断次数防止int存不下了

因此get函数的实现就应该是return中断次数,经检查没问题

clare函数的实现就应该是把中断次数置0,就是他的问题

把 time– 改成 time = 0;bug已修复

演示视频: