【130.1 “异或”的校验。】
通信的校验常用有两种,一种是“累加和”,另一种是“异或”。“异或”算法的详细介绍请看前面章节的第32节。
上一节讲的“累加和”,放在数据串的最后一个字节,是前面所有字节的累加之和(不包括自己本身的字节),累加的结果高于一个字节的那部分自动溢出丢掉,只保留低8位的一个字节的数据。本节讲的“异或”,也是放在数据串的最后一个字节,是前面所有字节的异或结果(不包括自己本身的字节)。本节在上一节的基础上,只更改以下这段校验算法的代码即可。
上一节的“累加和”算法如下:
Gu8ReceZZ=Gu8ReceBuffer\[Gu32ReceDataLength-1\]; //提取“累加和”
Su8RecZZ=0;
for(i=0;i<(Gu32ReceDataLength-1);i++)
{
Su8RecZZ=Su8RecZZ+Gu8ReceBuffer\[i\]; //计算“累加和”
}
if(Su8RecZZ==Gu8ReceZZ) //验证“累加和”,“计算的”与“接收的”是否一致
{
//此处省去若干代码
}
本节的“异或”算法如下:
Gu8ReceZZ=Gu8ReceBuffer\[Gu32ReceDataLength-1\]; //提取接收到的“异或”
Su8RecZZ=Gu8ReceBuffer\[0\]; //提取数据串第“i=0”个数据作为异或的原始数据
for(i=1;i<(Gu32ReceDataLength-1);i++) //注意,这里是从第“i=1”个数据开始
{
Su8RecZZ=Su8RecZZ^Gu8ReceBuffer\[i\]; //计算“异或”
}
if(Su8RecZZ==Gu8ReceZZ) //验证“异或”,“计算的”与“接收的”是否一致
{
//此处省去若干代码
}
【130.2 通信协议。】
数据头(EB):占1个字节,作为“起始字节”,起到“接头暗号”的作用,平时用来过滤无关的数据。
数据类型(01):占用1个字节。数据类型是用来定义这串数据的用途。
数据长度(00 00 00 0B):占4个字节。用来告诉通信的对方,这串数据一共有多少个字节。
其它数据(03 E8):此数据根据不同的“数据类型”可以用来做不同的用途,根据具体的项目而定。
动态密匙(00 01):这两个字节代表一个unsigned int类型的数据,数据范围是从0到65535,但是考虑到数据更加安全可靠,一般丢弃了首尾的0(十六进制的00 00)与65535(十六进制的FF FF),只保留从1到65534的变化。大部分的通信模型都是主机对从机的“一问一应答”模式,也就是,主机每发送一条指令给从机,从机才返回一条消息作为应答。如果主机发送了信息后,在规定的时间内,没有收到从机的应答指令,主机就继续发送信息给从机,但是此时,从机本来应该应答主机当前指令的,可能因为某种情况导致反馈的信息发生了延时,导致此时应答的数据是主机的上一条指令,从而造成“一问一应答”的数据帧发送了错位,这种情况加上“动态密匙”就能使问题得到有效的解决。主机每发送一条信息,信息里都携带了2个字节的“动态密匙”,从机每收到主机的一条信息,在应答此信息时都把收到的“动态密匙”原封不动的反馈给主机,主机再查看发送的“动态密匙”与接收到的“动态密匙”是否一致,以此来判断应答数据是否有效。“动态密匙”像流水号一样,每发送一次指令后都累加1,不断发生变化,从1到65534,依次循环。这是数据校验的一种方式。
异或(0B)。“异或”放在数据串的最后一个字节,是前面所有字节的异或结果(不包括自己本身的字节)。比如:本例子中,数据串是:EB 01 00 00 00 0B 03 E8 00 01 0B。其中最后一个字节0B就是“异或”字节,前面所有字节相“异或”等于十六进制的0B。验证“异或”的方法,可以借用电脑“附件”自带的“计算器”软件来实现,打开“计算器”软件后,在“查看”的下拉菜单里,选择“程序员”,然后选择“十六进制”,该计算器软件的异或运算按键是“Xor”。不管是主机还是从机,每接收到一串数据后,都要自己计算一次“异或”,把自己计算得到的“异或”与接收到的最后一个字节的“异或”进行对比,来判断接收到的数据是否发生了丢失或者错误。
【130.3 程序例程。】
**![](https://img.kancloud.cn/89/70/8970513a066fe0726b2997dcb0329ce0_194x190.png)**
上图130.3.1 有源蜂鸣器电路
![](https://img.kancloud.cn/57/01/57010762abae4157797b57319514eff1_468x181.png)
上图130.3.2 232串口电路
程序功能如下:
(5) 单片机模拟从机,上位机的串口助手模拟主机。在上位机的串口助手里,发送一串数据,控制蜂鸣器发出不同长度的声音。
(6) 本节因为还没有讲到数据发送的内容,因此应答“动态密匙”那部分的代码暂时不写,只写验证“异或”那部分的代码。
(7) 波特率9600,校验位NONE(无),数据位8,停止位1。
(8) 十六进制的数据格式:EB 01 00 00 00 0B XX XX YY YY ZZ 。其中:
EB是数据头。
01是代表数据类型。
00 00 00 0B代表数据长度是11个(十进制)。
XX XX代表一个unsigned int的数据,此数据的大小决定了蜂鸣器发出声音的长度。
YY YY代表一个unsigned int的动态密匙,每收发一条指令,此数据累加一次1,范围从1到65534。
ZZ 代表前面所有字节的异或结果。
比如:
让蜂鸣器鸣叫1000毫秒,密匙为00 01,发送十六进制的:EB 01 00 00 00 0B 03 E8 00 01 0B
让蜂鸣器鸣叫100毫秒, 密匙为00 02,发送十六进制的:EB 01 00 00 00 0B 00 64 00 02 87
\#include "REG52.H"
\#define RECE\_TIME\_OUT 2000 //通信过程中字节之间的超时时间2000ms
\#define REC\_BUFFER\_SIZE 20 //接收数据的缓存数组的长度
void usart(void); //串口接收的中断函数
void T0\_time(); //定时器的中断函数
void UsartTask(void); //串口接收的任务函数,放在主函数内
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;
void BeepOpen(void);
void BeepClose(void);
void VoiceScan(void);
sbit P3\_4=P3^4;
volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;
unsigned char Gu8ReceBuffer\[REC\_BUFFER\_SIZE\]; //开辟一片接收数据的缓存
unsigned long Gu32ReceCnt=0; //接收缓存数组的下标
unsigned char Gu8ReceStep=0; //接收中断函数里的步骤变量
unsigned char Gu8ReceFeedDog=1; //“喂狗”的操作变量。
unsigned char Gu8ReceType=0; //接收的数据类型
unsigned int Gu16ReceYY=0; //接收的动态密匙
unsigned char Gu8ReceZZ=0; //接收的异或
unsigned long Gu32ReceDataLength=0; //接收的数据长度
unsigned char Gu8FinishFlag=0; //是否已接收完成一串数据的标志
unsigned long \*pu32Data; //用于数据转换的指针
volatile unsigned char vGu8ReceTimeOutFlag=0;//通信过程中字节之间的超时定时器的开关
volatile unsigned int vGu16ReceTimeOutCnt=0; //通信过程中字节之间的超时定时器,“喂狗”的对象
void main()
{
SystemInitial();
Delay(10000);
PeripheralInitial();
while(1)
{
UsartTask(); //串口接收的任务函数
}
}
void usart(void) interrupt 4 //串口接发的中断函数,中断号为4
{
if(1==RI) //接收完一个字节后引起的中断
{
RI = 0; //及时清零,避免一直无缘无故的进入中断。
/\* 注释一:
\* 以下Gu8FinishFlag变量的用途。
\* 此变量一箭双雕,0代表正处于接收数据的状态,1代表已经接收完毕并且及时通知主函数中的处理函数
\* UsartTask()去处理新接收到的一串数据。除此之外,还起到一种“自锁自保护”的功能,在新数据还
\* 没有被主函数处理完毕的时候,禁止接收其它新的数据,避免新数据覆盖了尚未处理的数据。
\*/
if(0==Gu8FinishFlag) //1代表已经完成接收了一串新数据,并且禁止接收其它新的数据
{
/\* 注释二:
\* 以下Gu8ReceFeedDog变量的用途。
\* 此变量是用来检测并且识别通信过程中相邻的字节之间是否存在超时的情况。
\* 如果大家听说过单片机中的“看门狗”这个概念,那么每接收到一个数据此变量就“置1”一次,它的
\* 作用就是起到及时“喂狗”的作用。每接收到一个数据此变量就“置1”一次,在主函数里,相关
\* 的定时器就会被重新赋值,只要这个定时器能不断及时的被补充新的“能量”新的值,那么这个定时器
\* 就永远不会变成0,只要不变成0就不会超时。如果两个字节之间通信时间超过了固定的长度,就意味
\* 着此定时器变成了0,这时就需要把中断函数里的接收步骤Gu8Step及时切换到“接头暗号”的步骤。
\*/
Gu8ReceFeedDog=1; //每接收到一个字节的数据,此标志就置1及时更新定时器的值。
switch(Gu8ReceStep)
{
case 0: //接头暗号的步骤。判断数据头的步骤。
Gu8ReceBuffer\[0\]=SBUF; //直接读取刚接收完的一个字节的数据。
if(0xeb==Gu8ReceBuffer\[0\]) //等于数据头0xeb,接头暗号吻合。
{
Gu32ReceCnt=1; //接收缓存的下标
Gu8ReceStep=1; //切换到下一个步骤,接收其它有效的数据
}
break;
case 1: //数据类型和长度
Gu8ReceBuffer\[Gu32ReceCnt\]=SBUF; //直接读取刚接收完的一个字节的数据。
Gu32ReceCnt++; //每接收一个字节,数组下标都自加1,为接收下一个数据做准备
if(Gu32ReceCnt>=6) //前6个数据。接收完了“数据类型”和“数据长度”。
{
Gu8ReceType=Gu8ReceBuffer\[1\]; //提取“数据类型”
//以下的数据转换,在第62节讲解过的指针法
pu32Data=(unsigned long \*)&Gu8ReceBuffer\[2\]; //数据转换
Gu32ReceDataLength=\*pu32Data; //提取“数据长度”
if(Gu32ReceCnt>=Gu32ReceDataLength) //靠“数据长度”来判断是否完成
{
Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
Gu8ReceStep=0; //及时切换回接头暗号的步骤
}
else //如果还没结束,继续切换到下一个步骤,接收“其它数据”
{
Gu8ReceStep=2; //切换到下一个步骤
}
}
break;
case 2: //其它数据
Gu8ReceBuffer\[Gu32ReceCnt\]=SBUF; //直接读取刚接收完的一个字节的数据。
Gu32ReceCnt++; //每接收一个字节,数组下标都自加1,为接收下一个数据做准备
//靠“数据长度”来判断是否完成。也不允许超过数组的最大缓存的长度
if(Gu32ReceCnt>=Gu32ReceDataLength||Gu32ReceCnt>=REC\_BUFFER\_SIZE)
{
Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
Gu8ReceStep=0; //及时切换回接头暗号的步骤
}
break;
}
}
}
else //发送数据引起的中断
{
TI = 0; //及时清除发送中断的标志,避免一直无缘无故的进入中断。
//以下可以添加一个全局变量的标志位的相关代码,通知主函数已经发送完一个字节的数据了。
}
}
void UsartTask(void) //串口接收的任务函数,放在主函数内
{
static unsigned int \*pSu16Data; //数据转换的指针
static unsigned int Su16Data; //转换后的数据
static unsigned int i;
static unsigned char Su8RecZZ=0; //计算的“异或”
if(1==Gu8ReceFeedDog) //每被“喂一次狗”,就及时更新一次“超时检测的定时器”的初值
{
Gu8ReceFeedDog=0;
vGu8ReceTimeOutFlag=0;
vGu16ReceTimeOutCnt=RECE\_TIME\_OUT;//更新一次“超时检测的定时器”的初值
vGu8ReceTimeOutFlag=1;
}
else if(Gu8ReceStep>0&&0==vGu16ReceTimeOutCnt) //超时,并且步骤不在接头暗号的步骤
{
Gu8ReceStep=0; //串口接收数据的中断函数及时切换回接头暗号的步骤
}
if(1==Gu8FinishFlag) //1代表已经接收完毕一串新的数据,需要马上去处理
{
switch(Gu8ReceType) //接收到的数据类型
{
case 0x01: //驱动蜂鸣器
//以下的数据转换,在第62节讲解过的指针法
pSu16Data=(unsigned int \*)&Gu8ReceBuffer\[Gu32ReceDataLength-3\]; //数据转换
Gu16ReceYY=\*pSu16Data; //提取“动态密匙”。本例子中暂时不做返回应答的处理
Gu8ReceZZ=Gu8ReceBuffer\[Gu32ReceDataLength-1\]; //提取接收到的“异或”
Su8RecZZ=Gu8ReceBuffer\[0\]; //提取数据串第“i=0”个数据作为异或的原始数据
for(i=1;i<(Gu32ReceDataLength-1);i++) //注意,这里是从第“i=1”个数据开始
{
Su8RecZZ=Su8RecZZ^Gu8ReceBuffer\[i\]; //计算“异或”
}
if(Su8RecZZ==Gu8ReceZZ) //验证“异或”,“计算的”与“接收的”是否一致
{
pSu16Data=(unsigned int \*)&Gu8ReceBuffer\[6\]; //数据转换。
Su16Data=\*pSu16Data; //提取“蜂鸣器声音的长度”
vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=Su16Data; //让蜂鸣器鸣叫
vGu8BeepTimerFlag=1;
}
break;
}
Gu8FinishFlag=0; //上面处理完数据再清零标志,为下一次接收新的数据做准备
}
}
void T0\_time() interrupt 1
{
VoiceScan();
if(1==vGu8ReceTimeOutFlag&&vGu16ReceTimeOutCnt>0) //通信过程中字节之间的超时定时器
{
vGu16ReceTimeOutCnt--;
}
TH0=0xfc;
TL0=0x66;
}
void SystemInitial(void)
{
unsigned char u8\_TMOD\_Temp=0;
//以下是定时器0的中断的配置
TMOD=0x01;
TH0=0xfc;
TL0=0x66;
EA=1;
ET0=1;
TR0=1;
//以下是串口接收中断的配置
//串口的波特率与内置的定时器1直接相关,因此配置此定时器1就等效于配置波特率。
u8\_TMOD\_Temp=0x20; //即将把定时器1设置为:工作方式2,初值自动重装的8位定时器。
TMOD=TMOD&0x0f; //此寄存器低4位是跟定时器0相关,高4位是跟定时器1相关。先清零定时器1。
TMOD=TMOD|u8\_TMOD\_Temp; //把高4位的定时器1填入0x2,低4位的定时器0保持不变。
TH1=256-(11059200L/12/32/9600); //波特率为9600。11059200代表晶振11.0592MHz,
TL1=256-(11059200L/12/32/9600); //L代表long的长类型数据。根据芯片手册提供的计算公式。
TR1=1; //开启定时器1
SM0=0;
SM1=1; //SM0与SM1的设置:选择10位异步通信,波特率根据定时器1可变
REN=1; //允许串口接收数据
//为了保证串口中断接收的数据不丢失,必须设置IP = 0x10,相当于把串口中断设置为最高优先级,
//这个时候,串口中断可以打断任何其他的中断服务函数实现嵌套,
IP =0x10; //把串口中断设置为最高优先级,必须的。
ES=1; //允许串口中断
EA=1; //允许总中断
}
void Delay(unsigned long u32DelayTime)
{
for(;u32DelayTime>0;u32DelayTime--);
}
void PeripheralInitial(void)
{
}
void BeepOpen(void)
{
P3\_4=0;
}
void BeepClose(void)
{
P3\_4=1;
}
void VoiceScan(void)
{
static unsigned char Su8Lock=0;
if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
{
if(0==Su8Lock)
{
Su8Lock=1;
BeepOpen();
}
else
{
vGu16BeepTimerCnt--;
if(0==vGu16BeepTimerCnt)
{
Su8Lock=0;
BeepClose();
}
}
}
}
- 首页
- 第一节:我的价值观
- 第二节:初学者的疑惑
- 第三节:单片机最重要的一个特性
- 第四节:平台软件和编译器软件的简介
- 第五节:用Keil2软件关闭,新建,打开一个工程的操作流程
- 第六节:把.c源代码编译成.hex机器码的操作流程
- 第七节:本节预留
- 第八节:把.hex机器码程序烧录到单片机的操作流程
- 第九节:本节预留
- 第十节:程序从哪里开始,要到哪里去?
- 第十一节:一个在单片机上练习C语言的模板程序
- 第十二节:变量的定义和赋值
- 【TODO】第十三节:赋值语句的覆盖性
- 【TODO】第十四节:二进制与字节单位,以及常用三种变量的取值范围
- 【TODO】第十五节:二进制与十六进制
- 【TODO】第十六节:十进制与十六进制
- 【TODO】第十七节:加法运算的5种常用组合
- 【TODO】第十八节:连加、自加、自加简写、自加1
- 【TODO】第十九节:加法运算的溢出
- 【TODO】第二十节:隐藏中间变量为何物?
- 【TODO】第二十一节:减法运算的5种常用组合。
- 【TODO】第二十二节:连减、自减、自减简写、自减1
- 【TODO】第二十三节:减法溢出与假想借位
- 【TODO】第二十四节:借用unsigned long类型的中间变量可以减少溢出现象
- 【TODO】第二十五节:乘法运算中的5种常用组合
- 【TODO】第二十六节:连乘、自乘、自乘简写,溢出
- 【TODO】第二十七节:整除求商
- 【TODO】第二十八节:整除求余
- 【TODO】第二十九节:“先余后商”和“先商后余”提取数据某位,哪家强?
- 【TODO】第三十节:逻辑运算符的“与”运算
- 【TODO】第三十一节:逻辑运算符的“或”运算
- 【TODO】第三十二节:逻辑运算符的“异或”运算
- 【TODO】第三十三节:逻辑运算符的“按位取反”和“非”运算
- 【TODO】第三十四节:移位运算的左移
- 【TODO】第三十五节:移位运算的右移
- 【TODO】第三十六节:括号的强制功能---改变运算优先级
- 【TODO】第三十七节:单字节变量赋值给多字节变量的疑惑
- 【TODO】第三十八节:第二种解决“运算过程中意外溢出”的便捷方法
- 【TODO】第三十九节:if判断语句以及常量变量的真假判断
- 【TODO】第四十节:关系符的等于“==”和不等于“!=”
- 【TODO】第四十一节:关系符的大于“>”和大于等于“>=”
- 【TODO】第四十二节:关系符的小于“<”和小于等于“<=”
- 【TODO】第四十三节:关系符中的关系符:与“&&”,或“||”
- 【TODO】第四十四节:小括号改变判断优先级
- 【TODO】第四十五节: 组合判断if...else if...else
- 【TODO】第四十六节: 一维数组
- 【TODO】第四十七节: 二维数组
- 【TODO】第四十八节: while循环语句
- 【TODO】第四十九节: 循环语句do while和for
- 【TODO】第五十节: 循环体内的continue和break语句
- 【TODO】第五十一节: for和while的循环嵌套
- 【TODO】第五十二节: 支撑程序框架的switch语句
- 【TODO】第五十三节: 使用函数的三要素和执行顺序
- 【TODO】第五十四节: 从全局变量和局部变量中感悟“栈”为何物
- 【TODO】第五十五节: 函数的作用和四种常见书写类型
- 【TODO】第五十六节: return在函数中的作用以及四个容易被忽略的功能
- 【TODO】第五十七节: static的重要作用
- 【TODO】第五十八节: const(./book/或code)在定义数据时的作用
- 【TODO】第五十九节: 全局“一键替换”功能的#define
- 【TODO】第六十节: 指针在变量(./book/或常量)中的基础知识
- 【TODO】第六十一节: 指针的中转站作用,地址自加法,地址偏移法
- 【TODO】第六十二节: 指针,大小端,化整为零,化零为整
- 【TODO】第六十三节: 指针“化整为零”和“化零为整”的“灵活”应用
- 【TODO】第六十四节: 指针让函数具备了多个相当于return的输出口
- 【TODO】第六十五节: 指针作为数组在函数中的入口作用
- 【TODO】第六十六节: 指针作为数组在函数中的出口作用
- 【TODO】第六十七节: 指针作为数组在函数中既“入口”又“出口”的作用
- 【TODO】第六十八节: 为函数接口指针“定向”的const关键词
- 【TODO】第六十九节: 宏函数sizeof(./book/)
- 【TODO】第七十节: “万能数组”的结构体
- 【TODO】第七十一节: 结构体的内存和赋值
- 【TODO】第七十二节: 结构体的指针
- 【TODO】第七十三节: 结构体数据的传输存储和还原
- 【TODO】第七十四节: 结构体指针在函数接口处的频繁应用
- 【TODO】第七十五节: 指针的名义(例:一维指针操作二维数组)
- 【TODO】第七十六节: 二维数组的指针
- 【TODO】第七十七节: 指针唯一的“单向输出”通道return
- 【TODO】第七十八节: typedef和#define和enum
- 【TODO】第七十九节: 各种变量常量的命名规范
- 【TODO】第八十节: 单片机IO口驱动LED
- 【TODO】第八十一节: 时间和速度的起源(指令周期和晶振频率)
- 【TODO】第八十二节: Delay“阻塞”延时控制LED闪烁
- 【TODO】第八十三节: 累计主循环的“非阻塞”延时控制LED闪烁
- 【TODO】第八十四节: 中断与中断函数
- 【TODO】第八十五节: 定时中断的寄存器配置
- 【TODO】第八十六节: 定时中断的“非阻塞”延时控制LED闪烁
- 【TODO】第八十七节: 一个定时中断产生N个软件定时器
- 【TODO】第八十八节: 两大核心框架理论(四区一线,switch外加定时中断)
- 【TODO】第八十九节: 跑马灯的三种境界
- 【TODO】第九十节: 多任务并行处理两路跑马灯
- 【TODO】第九十一节: 蜂鸣器的“非阻塞”驱动
- 【TODO】第九十二节: 独立按键的四大要素(自锁,消抖,非阻塞,清零式滤波)
- 【TODO】第九十三节: 独立按键鼠标式的单击与双击
- 【TODO】第九十四节: 两个独立按键构成的组合按键
- 【TODO】第九十五节: 两个独立按键的“电脑键盘式”组合按键
- 【TODO】第九十六节: 独立按键“一键两用”的短按与长按
- 【TODO】第九十七节: 独立按键按住不松手的连续均匀触发
- 【TODO】第九十八节: 独立按键按住不松手的“先加速后匀速”的触发
- 【TODO】第九十九节: “行列扫描式”矩阵按键的单个触发(原始版)
- 【TODO】第一百节: “行列扫描式”矩阵按键的单个触发(优化版)
- 【TODO】第一百零一节: 矩阵按键鼠标式的单击与双击
- 【TODO】第一百零二节: 两个“任意行输入”矩阵按键的“有序”组合触发
- 【TODO】第一百零三节: 两个“任意行输入”矩阵按键的“无序”组合触发
- 【TODO】第一百零四节: 矩阵按键“一键两用”的短按与长按
- 【TODO】第一百零五节: 矩阵按键按住不松手的连续均匀触发
- 【TODO】第一百零六节: 矩阵按键按住不松手的“先加速后匀速”触发
- 【TODO】第一百零七节: 开关感应器的识别与软件滤波
- 【TODO】第一百零八节: 按键控制跑马灯的启动和暂停和停止
- 【TODO】第一百零九节: 按键控制跑马灯的方向
- 【TODO】第一百一十节: 按键控制跑马灯的速度
- 第一百一十一节: 工业自动化设备的开关信号的运动控制
- 【TODO】第一百一十二节: 数码管显示的基础知识
- 【TODO】第一百一十三节: 动态扫描的数码管显示数字
- 【TODO】第一百一十四节: 动态扫描的数码管显示小数点
- 【TODO】第一百一十五节: 按键控制数码管的秒表
- 【TODO】第一百一十六节: 按键控制数码管的倒计时
- 【TODO】第一百一十七节: 按键切换数码管窗口来设置参数
- 【TODO】第一百一十八节: 按键让某位数码管闪烁跳动来设置参数
- 【TODO】第一百一十九节: 一个完整的人机界面的程序框架的脉络
- 【TODO】第一百二十节: 按键切换窗口切换局部来设置参数
- 【TODO】第一百二十一节: 可调参数的数码管倒计时
- 【TODO】第一百二十二节: 利用定时中断做的“时分秒”数显时钟
- 【TODO】第一百二十三节: 一种能省去一个lock自锁变量的按键驱动程序
- 【TODO】第一百二十四节: 数显仪表盘显示“速度、方向、计数器”的跑马灯
- 【TODO】第一百二十五节: “双线”的肢体接触通信
- 【TODO】第一百二十六节: “单线”的肢体接触通信
- 【TODO】第一百二十七节: 单片机串口接收数据的机制
- 【TODO】第一百二十八节: 接收“固定协议”的串口程序框架
- 【TODO】第一百二十九节: 接收带“动态密匙”与“累加和”校验数据的串口程序框架
- 【TODO】第一百三十节: 接收带“动态密匙”与“异或”校验数据的串口程序框架
- 【TODO】第一百三十一节: 灵活切换各种不同大小“接收内存”的串口程序框架
- 【TODO】第一百三十二节:“转发、透传、多种协议并存”的双缓存串口程序框架
- 【TODO】第一百三十三节:常用的三种串口发送函数
- 【TODO】第一百三十四节:“应用层半双工”双机串口通讯的程序框架