文章目录

  • 前言
  • 一、数据包格式(江科大规定)
    • 1.HEX数据包
    • 2.文本数据包
    • 3.两者对比
  • 二、数据包收发流程
    • 1.HEX数据包接收(只演示固定包长)
    • 2.文本数据包接收(只演示可变包长)
  • 三、实操案例
    • 1.串口收发HEX数据包
    • 2.串口收发文本数据包(发直接用SendString,代码主要写接收)
  • 总结

声明:学习笔记根据b站江科大自化协stm32入门教程编辑,仅供学习交流使用!

前言

本次学习有两个实操代码,第一个是串口收发HEX数据包,第二个是串口收发文本数据包


一、数据包格式(江科大规定)

数据包的作用是把一个个单独的数据打包起来,方便我们进行多字节的数据通信。之前学习的串口代码,发送一个或接收一个字节都没问题。但在实际应用中需要把多个字节打包为一个整体进行发送。比如有一个陀螺仪传感器需要用串口发送数据到STM32,比如X轴一个字节、Y轴一个字节、Z轴一个字节总共3个数据需要连续不断地发送,当按照XYZXYZXYZ…进行连续发送时会出现一个问题,接收方不知道哪个对应X、哪个对应Y,哪个对应Z,因为接收方可能会从任意位置开始接收,会出现数据错位的现象,这时需要一种方式对数据进行分割为一个个数据包,这样接收方可以方便识别第1个为X、第2个位Y、第3个为Z。
分割打包的方法可以自己发挥设计,只要逻辑合理即可比如可以设计在XYZXYZ…数据流中,数据包第1个数据也就是X数据包,它的最高位置1,其余数据包最高位都置0,当接收到数据后判断下最高位,如果是1就是X数据,然后紧跟着的两个数据就是Y和Z,这种分割方法就是把每个数据的最高位当作标志位来进行分割,实际例子比如UTF8的编码方法和这个类似(不过它更高级些)。
本节的数据包分割方法并不是这种,这种方式破坏了原有数据使用起来比较复杂,串口数据包通常使用的是额外添加包头包尾的方式


1.HEX数据包

包头包尾方法如下江科大列举了2种数据包格式:
具体格式可根据需求自己规定,也可能是买了个模块别的开发者规定的
1、固定包长,含包头包尾。即每个数据包的长度都固定不变,数据包前面的是包头,后面的是包围。

这里规定了一批数据有4个字节,在这4个字节首尾加上包头包围,比如规定0xFF为包头,0xFE为包围(类似一个标志位作用)。
2、可变包长,含包头包尾。每个数据包的长度可以是不一样的。

研究问题:
①包头包尾和数据载荷重复的问题。比如规定0xFF为包头,0xFE为包围,那如果传输的数据本身就是就是FF或FE呢?这里确实会引起误判,解决方法有:第一种,限制载荷数据的范围,在发送的时候对数据进行限幅,比如XYZ3个数据变化范围只可以0-100。第二种,如果无法避免数据与包头包尾重复,就尽量使用固定长度的数据包(固定包长),这样由于载荷数据固定只要通过包头包尾对齐了数据,就可严格知道哪个应该是包头包尾哪个应该是数据(只在特定步长处if判断是否为包头包尾)。第三种,增加包头包尾的数量,并且让它尽量呈现出载荷数据出现不了的状态,比如使用FF、FE作为包头,FD、FC作为包尾。
②包头包尾并不是全部都需要,比如可以只要包头FF不要包尾,包头+4个字节,收够4个字节后置标志位,一个数据包接收完成,只不过这样载荷和包头重复的问题会更严重些。
③固定包长和可变包长的选择。对于HEX数据包,如果载荷会出现和包头包尾重复的情况,最好选择固定包长,无重复情况可选择可变包长。
④各种数据转换为字节流。这里的数据包都是一个字节一个字节组成的,如果想发送16位整型数据、32位整型数据、float、double、甚至结构体都没问题,因为它们内部其实都是由一个字节一个字节组成的。只需要用一个uint8_t的指针指向它,把它们当作一个字节数组发送就行(可见指针学习)


2.文本数据包

在HEX数据包里,数据都是以原始的字节数据本身呈现,在文本数据包里,每个字节经过了一层编码和译码,最终表现出来的就是文本格式。所以实际上每个文本字符背后都是一个字节的HEX数据:
1、固定包长,含包头包尾

2、可变包长,含包头包尾

由于数据译码成为字符形式,所以存在大量字符可作为包头包尾,可有效避免数据与包头包尾重复的问题。这里以@作为包头,\r和\n作为包尾,当我们接收到载荷数据之后得到就是一个字符串,在软件中再对字符串进行操作和判断,就可实现各种指令控制功能,且字符串数据包表达的意义很明显,可发送到串口助手在电脑显示打印,所以常以\n换行符作为包尾,这样打印是就可一行一行显示。


3.两者对比

HEX数据包优点是传输最直接、解析数据简单,比较适合一些模块发送原始数据,如一些使用串口通信的陀螺仪、温湿传感器;缺点是灵活性不足、载荷数据易于包头包尾重复。
文本数据包优点是直观易理解、灵活,比较适合一些文本指令进行人机交互的场合,如蓝牙模块常使用的AT指令、CNC和3D打印机常用的G代码都是文本数据包格式;缺点是解析效率低,比如发送一个100,HEX就直接是100一个字节,文本数据包要3个字节的字符‘1’、‘0’、‘0’,收到后还要把字符转换为数据才能得到100。


二、数据包收发流程

数据包的发送过程很简单,如HEX数据包发送,先定义一个数组,填充数据,然后用上一节写过的USART_SendArray函数;文本数据包同理,写一个字符串,调用上一节写的USART_SendString函数。之所以简单是因为发送过程完全自主可控,想发什么就发什么,上一节串口也可体会到发送比接收简单多了。
下面重点介绍下接收(HEX只演示固定包长,文本只演示可变包长)

1.HEX数据包接收(只演示固定包长)

根据之前代码,每收到一个字节程序都会进一遍中断,在中断函数里我们可以拿到这一个字节,但拿到之后就要退出中断了,所以每拿到一个数据都是一个独立的过程。而对于数据包来说,它具有前后关联性,对于包头、数据、包尾这三种状态我们需要不同的处理逻辑,所以在程序中,我们需要设计一个能记住不同状态的机制,在不同的状态执行不同的操作,同时还要进行状态的合理转移,这种程序思维叫做“状态机”。
要想设计好的“状态机”程序,画一个以下的状态转移图很有必要:

对于上面的一个固定包长的HEX数据包,我们定义三个状态:等待包头、接收数据、等待包尾,每个状态都需要一个变量来标志一下,比如上面依次用S=0、S=1、S=2(有点类似置标志位,只不过标志位只有0/1,而状态机是多标志位状态的一种方式)。


执行流程是
①最开始S=0。收到一个数据,进中断,根据S=0进入第一个状态的程序,判断数据是不是包头FF,如果是FF则代表收到包头,之后置S=1退出中断,结束。这样下次再进中断,根据S=1就可以进行接收数据的程序了。如果在第一个状态收到的不是FF,就说明数据包未对齐,这时应该等待数据包包头的出现,S仍是0,下次进中断仍是执行判断包头的逻辑,直到出现FF才可进入下一个状态。
②到接收数据的状态后,收到数据就把它存在数组中,再用一个变量记录接收了多少数据,没到4个就一直是这个接收数据状态,收够了就置S=2,进入下一个状态。
②最后等待包尾,判断数据是否为FE,是的话就置S=0回到最初状态,开始下一轮回。也有可能不是FE,比如数据于包头重复,导致前面包头位置判断错误,就可能导致包尾不是FE,这时就可进入重复等待包尾的状态,直到接收到真正包尾。


状态机”是一种普遍编程思路,一般最好配合状态转移图思路更加清晰。比如做个菜单,按什么键切换什么菜单,执行什么程序;还有一些芯片内部逻辑,芯片什么时候进入待机状态什么时候进入工作状态,都会用到状态机。


2.文本数据包接收(只演示可变包长)


流程类似,只不过这里因为演示的是可变包长接受数据的状态(S=1)在进行数据接收的逻辑时,还要兼具等待包尾的功能:收到一个数据判断是否为\r,如果不是\r则正常接收数据;如果是\r则不接收数据,同时跳到下一个状态(S=2),等待包尾\n。因为这里设置了两个包尾\r、\n,所以需要第三个状态(S=2),如果只有一个包尾\r,那么在S=1状态中逻辑判断出现包尾\r,后就可直接回到初始状态。


三、实操案例

接线图与上次串口的基本相同,只不过HEX数据包接线图里PB1口接了个按键,用于控制;收发文本的接线图,在PA1口接了LED用于指示。按键和LED的附加功能可自己实现,下面的代码只写核心部分

1.串口收发HEX数据包


代码是在9-2串口发送+接收(只可一个字节)基础上改进而成:

//Serial.c
//在9-2串口发送+接收(只可一个字节)基础上改进#include "stm32f10x.h"
//先定义两个缓存区的数组,4个字节(只存储发送或接收的载荷数据)
//在头文件里声明外部可调用,使它们可在main.c里使用赋值
uint8_t Serial_RxPacket[4];
uint8_t Serial_TxPacket[4];uint8_t Serial_RxFlag;void Serial_Init(void){RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP ;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 ;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_Tx |USART_Mode_Rx;USART_InitStructure.USART_Parity= USART_Parity_No;USART_InitStructure.USART_StopBits= USART_StopBits_1;USART_InitStructure.USART_WordLength= USART_WordLength_8b;USART_Init(USART1,&USART_InitStructure);USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel= USART1_IRQn;NVIC_InitStructure.NVIC_IRQChannelCmd= ENABLE;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority= 1;NVIC_InitStructure.NVIC_IRQChannelSubPriority= 1;NVIC_Init(&NVIC_InitStructure);USART_Cmd(USART1,ENABLE);
}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]);}
}//函数功能:发送。调用后,TxPacket数组里的4个数据,就会自动加上包头包尾发送出去
void Serial_SendPacket(void){Serial_SendByte(0xFF);Serial_SendArray(Serial_TxPacket,4);Serial_SendByte(0xFE);
}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;//状态变量S(接收)//这个静态变量类似于全局变量,函数进入后只会初始化一次0,函数退出后数据仍然有效//与全局变量不同的是,静态变量只能在本函数使用static uint8_t pRxPacket = 0;//指示接收到哪一个字节(载荷数据)if (USART_GetFlagStatus(USART1,USART_IT_RXNE)==SET){uint8_t RxData = USART_ReceiveData(USART1);//取出接收到的字节(一次一个)if (RxState == 0){if (RxData == 0xFF){//检测到包头RxState = 1;//进入下一个状态pRxPacket = 0;//在进入S=1前,提前清0}}else if (RxState == 1){Serial_RxPacket[pRxPacket] = RxData;//传给接收数组pRxPacket++;if (pRxPacket >=4){//接收够4个字节的载荷数据RxState = 2;//进入下一个状态}}else if (RxState == 2){if (RxData == 0xFE){//检测到包尾RxState = 0;//S清0开始下一轮回Serial_RxFlag = 1;//一个数据包接收完毕,置一个标志位}}//别用3个if,防止上一个if执行一半时出现多分枝同时成立,执行故障。比如if(RxState=0)执行到置S为1时...//用else if可保证每次进来只能选择一个分支执行,也可用switch实现USART_ClearITPendingBit(USART1,USART_IT_RXNE);}
}
//Serial.h
#ifndef __SERIAL_H
#define __SERIAL_Hextern uint8_t Serial_RxPacket[];//数组声明时数量可以不要
extern uint8_t Serial_TxPacket[];void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array,uint16_t Length);
uint8_t Serial_GetRxFlag(void);
void Serial_SendPacket(void);
#endif
//main.c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"int main(void){OLED_Init();Serial_Init();Serial_TxPacket[0]=0x11;//赋值要发送的Serial_TxPacket[1]=0x22;Serial_TxPacket[2]=0x33;Serial_TxPacket[3]=0x44;while(1){if (Serial_GetRxFlag() == 1){//如果接收到外部数据包,则显示OLED_ShowHexNum(1,1,Serial_RxPacket[0],2);OLED_ShowHexNum(2,1,Serial_RxPacket[1],2);OLED_ShowHexNum(3,1,Serial_RxPacket[2],2);OLED_ShowHexNum(4,1,Serial_RxPacket[3],2);//程序问题:Serial_RxPacket是一个同时被写入又同时被读出的数组,//在中断函数里依次把接收的字节写入它,在main.c里由依次读出它显示,//这会导致数据包之间会混在一起,比如读出速度太慢,读到一半数组就刷新了//解决方法:在接收部分加入判断,在数据包读取完成后再写入下一轮//很多情况不需要考虑此问题,这种HEX数据包多用于传输各种传感器的每个独立数据://比如陀螺仪的X、Y、Z轴数据,温湿度数据等,它们相邻数据包之间的数据具有连续性即使混在一起也没关系}}
}

2.串口收发文本数据包(发直接用SendString,代码主要写接收)

//Serial.c
#include "stm32f10x.h"  char Serial_RxPacket[100];//数量100给大点,防止溢出,这要求单条指令不可超过100个字符uint8_t Serial_RxFlag;void Serial_Init(void){RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP ;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 ;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_Tx |USART_Mode_Rx;USART_InitStructure.USART_Parity= USART_Parity_No;USART_InitStructure.USART_StopBits= USART_StopBits_1;USART_InitStructure.USART_WordLength= USART_WordLength_8b;USART_Init(USART1,&USART_InitStructure);USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel= USART1_IRQn;NVIC_InitStructure.NVIC_IRQChannelCmd= ENABLE;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority= 1;NVIC_InitStructure.NVIC_IRQChannelSubPriority= 1;NVIC_Init(&NVIC_InitStructure);USART_Cmd(USART1,ENABLE);
}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_SendNumer(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+0x30);}}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_GetFlagStatus(USART1,USART_IT_RXNE)==SET){uint8_t RxData = USART_ReceiveData(USART1);if (RxState == 0){if (RxData == '@'){RxState = 1;pRxPacket = 0;}}else if (RxState == 1){if (RxData == '\r'){//因为可变包长,接受前先判断包尾RxState = 2;}Serial_RxPacket[pRxPacket] = RxData;pRxPacket++;}else if (RxState == 2){if (RxData == '\n'){RxState = 0;Serial_RxPacket[pRxPacket] = '\0';//接收到之后还要在字符数组的最后加上结束标志位'\0',方便后续对字符串进行处理Serial_RxFlag = 1;}}USART_ClearITPendingBit(USART1,USART_IT_RXNE);}
}
//main.c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"int main(void){OLED_Init();OLED_ShowString(1,1,"RxPacket");Serial_Init();while(1){if (Serial_GetRxFlag() == 1){OLED_ShowString(2,1,"              ");//清屏OLED_ShowString(2,1,Serial_RxPacket);}}
}

总结

做任何事情,都要有一股坚忍不拔的毅力,只要坚持,挺过风雨,终会看见彩虹;只要坚持,走过黑暗,总会拥抱黎明;只要坚持,战胜失败,总能赢得成功!
今天的学习分享到此就结束了,我们下次再见!!
往期精彩:
STM32定时器输入捕获(IC)
STM32定时器输出比较(PWM波)
STM32定时中断
STM32外部中断
STM32GPIO精讲

【STM32学习】——USART串口数据包HEX/文本数据包收发流程串口收发HEX/文本数据包实操相关推荐

  1. 网络数据包收发流程(三):e1000网卡和DMA

    早就想整理网络数据包收发流程了,一直太懒没动笔.今天下决心写了 一.硬件环境 intel82546:PHY与MAC集成在一起的PCI网卡芯片,很强大 bcm5461:   PHY芯片,与之对应的MAC ...

  2. STM32学习——USART收发数据

    简介 1.串口通讯的双方若采用不同的电平标准,则需要利用电平转换芯片进行转换. 2.调试程序时可以把一些调试信息"打印"在电脑端的串口调试助手上. 3.硬件原理以后有空再研究,应该 ...

  3. 大数据之-Hadoop之HDFS_读数据流程_原理篇---大数据之hadoop工作笔记0069

    然后我们再来看下,这个客户端去从hadoop的hdfs上面读取数据的一个过程. 1.首先我们先看一下hadoop是怎么来存数据的. 2.首先对于namenode节点来说,我们说他存了元数据,比如他这里 ...

  4. 网络数据包收发流程(四):协议栈之packet_type

    进入函数netif_receive_skb()后,skb正式开始协议栈之旅. 先上图,协议栈大致过程如下所示: 跟OSI七层模型不同,linux根据包结构对网络进行分层. 比如,arp头和ip头都是紧 ...

  5. STM32学习心得十九:电容触摸按键实验及相关代码解读

    记录一下,方便以后翻阅~ 主要内容 1) 电容触摸按键原理: 2)部分实验代码解读. 实验内容 手触摸按键后,LED1灯翻转. 硬件原理图 上图,TPAD与STM_ADC用跳线帽相连,即TPAD与PA ...

  6. STM32学习心得二十一:实时时钟RTC和备份寄存器BKP特征、原理及相关实验代码解读

    记录一下,方便以后翻阅~ 主要内容 1) RTC特征与原理: 2) BKP备份寄存器特征与原理: 3) RTC常用寄存器+库函数介绍: 4) 相关实验代码解读. 实验内容: 因为没有买LCD屏,所以计 ...

  7. STM32学习心得十七:窗口看门狗(WWDG)实验及旧知识点复习

    记录一下,方便以后翻阅~ 主要内容: 1) 窗口看门狗概述: 2) 常用寄存器和库函数配置: 3) 窗口看门狗实验. 窗口看门狗实验内容: 为了对之前的知识进行总结复习,本人在教学案例的基础上又&qu ...

  8. java udp包_基于UDP协议的数据包收发程序(代码+报告)Java

    [实例简介] 设计要求: 1)按照UDP协议数据包发送方式实现用户端之间的通信. 2)统计包的发送和接收数,计算数据包的丢失数. 3)设计美观易用的图形界面. [实例截图] [核心代码] 基于UDP协 ...

  9. IKEv2协议协商流程: (IKE-SA-INIT 交换)第二包

    IKEv2协议协商流程: (IKE-SA-INIT 交换)第二包 文章目录 IKEv2协议协商流程: (IKE-SA-INIT 交换)第二包 1. IKEv2 协商总体框架 2. 第二包流程图 3. ...

最新文章

  1. PostgreSql安装(win 2003 下)
  2. Java 多线程:InheritableThreadLocal 实现原理
  3. 使用Jenkins来发布和代理.NetCore项目
  4. vue element ui下拉菜单和不是table列表全选功能问题解决方案
  5. 分布式数据库中间件概念
  6. android studio moudel,Android Studio将module变为library
  7. MFC 教程【10_内存分配方式和调试机制 】
  8. Takeown 实现解析
  9. MyBatis的常见面试题
  10. 音视频实时交互/语音通话/即时通话/连麦,EasyRTC即时通讯系统全方位服务
  11. 【算法java版09】:利用java实现对二进制数进行AMI编码
  12. 个人游戏开发者是如何盈利
  13. 远程桌面连接计算机是什么,远程桌面连接是什么意思?
  14. u8零售服务器端口号修改,用友U8服务器修改数据库端口
  15. stm32F103R6之BKP(备份寄存器)
  16. 学习【菜鸟教程】【C++ 类 对象】【内联函数】(例子简单,评论难懂)
  17. 联发科MT6580_datasheet/规格书资料分享
  18. 使用scrapy爬虫框架爬取慕课网全部课程信息
  19. PCB设计相关经验分享【From EDN China】
  20. CAN Bus-Off详解

热门文章

  1. 水调歌头·重上井冈山
  2. 该如何选择LoRaWAN终端入的网方式
  3. 【蜜拓蜜热闻】哈美华董事长一行莅临蜜拓蜜集团参观指导
  4. ABAP中的subroutine和function module
  5. 求助帖|用conda安装软件时报错CondaHTTPError: HTTP 403 FORBIDDEN for url
  6. 声网下一代视频引擎架构探索与实践
  7. 网优任我行手机版 v3.3.2
  8. 2021年数学建模国赛C题问题三详细思路和代码
  9. 高阶低通无源滤波器的设计
  10. Educational Codeforces Round 114 (Rated for Div. 2) 个人题解