#创作灵感:在实际的工业生产生活中,很多嵌入式编程的流程之中,几乎都是不允许CPU在当前的状态阻塞太长时间,一般不会超过1ms。以此建立的时间片轮询的算法,成为了工程师必备的本领,不同人的实现方式可能不一样,但是实现逻辑是差不多的。
下面我们以一个实际的单总线半双工通信的发送情况来进行描述,希望能帮助你更好的理解时间片。
工程要求:全程无阻塞,按键KEY1按下(无需松手)的时候,单总线COM发送完整的一帧。COM的常态为低电平,需要发送数据的时候,需要提前拉高电平至少2ms,发送数据的时候,每一位数据的电平持续至少30us.
帧格式包括:帧头(0x55)、数据包长度(1字节)、数据、校验和 四部分
解决逻辑:时间片的本质在于将所谓的时间切片,规划即将到来的每个时间片要做的事情。
我的实际方案是:定时器中断切片,每一次中断都将全局变量timetap加1,以此计数时间片。
实际的时候还需要将不同任务的计数timetap区分开(改为不同的变量),原因在后续。
每个任务即将开始的时候,标志位flag=0,此时清空时间片计数timetap,而后flag=1,进入下一个阶段,等待计数完成。下一阶段对flag=1进行判断,确认是计时阶段,然后对timetap的值进行判断,是否符合指定的要求,不符合的话不进入下一个阶段,保留flag的值。
这里我们优先展示计时器0的中断函数
void interrupt ISR(void)
{
if (T0IF) //中断溢出标志位
{
T0IF = 0;
TMR0 = 125; //计时器重载 每次溢出时间为16us
KEY1_GAP++;
KEY2_GAP++;
Frame->timetap++; //4个计时单位
if (KEY1_GAP >= 200)
{
KEY1_GAP = 0;
GAP_400ms++; //400ms 的计时单位
}
if (KEY2_GAP >= 200) //回收机制 也可以放在主循环里,中断里也可
KEY2_GAP = 0;
if (GAP_400ms >= 10) //注意回收时间不要比任务需要的定时还短,那任务的定时就出不来了
GAP_400ms = 0;
if (Frame->timetap >= 200)
Frame->timetap = 0;
}
}
按键的状态可以分为:初次按下、前沿抖动、彻底按下、松手、后沿抖动、彻底松手。
实际抖动可以通过电容的设置来降低抖动,电路图可以是如下情况:
有了电容并不是不抖动,只是按键的抖动时间非常短,可以达到几us级别。
#define Wait 0 //两段宏定义 用以促进代码理解
#define Tap_to_Toggle 1
if (KEY1 == 0 && KEY1CheckFlag == Wait) //第一次KEY1按下,需要判断当前阶段
{
KEY1_GAP = 0; //两个不同的计时单元 一个是400ms的
GAP_400ms = 0;
KEY1CheckFlag = Tap_to_Toggle;
}
if (KEY1CheckFlag == Tap_to_Toggle)
{
if (KEY1_GAP >= 14 && KEY1_GAP <= 15) //大概的计时时间是14到15个GAP之间
{
if (KEY1 == 0) //如果这个时候按键还是按下的状态 那么就是真的彻底按下
{
mode = Single;
Data_Build(mode); //建立数据包 完善帧的内容
Frame->state = send; //Frame 结构体指针 调整状态state为发送状态
KEY1_GAP = KEY1_GAP + 2; //小技巧 因为在14-15之间可能会出现多次KEY1=0的
} //情况,而我们实际只需要判断一次即可,这样做可以跳
} //出当前GAP的判断区间
if (GAP_400ms >= 2) // //标志位的返还周期,周期太短的话,出现第二次按键按下
{
KEY1CheckFlag = Wait;
}
}
不堵塞的理念就是当按键开始计时的时候,程序不会停留在此等待计时的完成,而是不停的通过while访问来确认计时是否完成,所以我们这里建立了KEY1CheckFlag,以便再次来访问的时候,不知道是处于哪个阶段,可能会导致计时单元KEY1_GAP又重新清0;
跟前面差不多,先拉高电平,而后依次发送数据,这里我们也是分阶段的。
先制作一帧的结构体,主要包括帧的各种发送信息和发送数据。
typedef struct
{
enum
{
wait,
send,
receive,
}state;
unsigned char sendflag; //发送阶段
unsigned char timetap; //用于计时的时间单元
unsigned char leadcode; //前导码
unsigned char datalength; //数据长度
unsigned char data; //数据
unsigned char checksum; //校验和
} Datatype;
Datatype DataBuff;
Datatype *Frame= &DataBuff;
分阶段发送
void Communte_Send()
{
if (Frame->sendflag == waitfor)
{
Frame->timetap = 0;
Frame->sendflag = HighLevel;
}
if (Frame->sendflag == HighLevel)
{
COM = 1; // 拉高电平
if (Frame->timetap >= highlevellimit) // 等待拉高时间2ms
{
Frame->timetap = 0;
Frame->sendflag = LeadCode; //拉高时间结束 进入发送前导码0x55
}
}
// 拉高电平之后,准备开始发送数据
if (Frame->sendflag == LeadCode) // 发送帧头
{
Data_Send(SendData_Head, 1, LengthCode);
}
if (Frame->sendflag == LengthCode) // 发送数据长度
{
Data_Send(DataLength, 1, DataCode);
}
if (Frame->sendflag == DataCode) // 发送数据包
{
Data_Send(Frame->data, 1, CheckCode);
}
if (Frame->sendflag == CheckCode) // 发送校验码
{
Data_Send(Frame->checksum, 1, LowLevel);
}
if (Frame->sendflag == LowLevel)
{
Frame->sendflag = waitfor; // 等待下一次发送
Frame->state = wait; // 状态调整为等待
COM = 0; // 拉低电平 结束发送
//Data_Clear(); // 清空所有发送内容
}
}
这里还有一个函数的设计十分关键,便是这个按位发送的函数Data_Send()
void Data_Send(unsigned char data, unsigned char leng, unsigned char nexttime)
{
static unsigned char n = 11; //静态局部变量 放置再次进入被初始化
if (n == 11) //这里实际发送的数据只有8位 所以没有用到参数leng,待后续使用
{
Frame->timetap = 0; //开始计时
n=0;
}
if (n < 8)
{
COM = (data >> n) & 0x01;
}
if (Frame->timetap >= 2) //每次计时完毕才给n++
{
n++;
Frame->timetap = 0;
}
if (n == 8) //发送完8位数据
{
Frame->sendflag = nexttime;
n = 11;
}
}
设计此类函数的时候要十分注意此函数会跳出去执行其他事件,很容易因为某个标志位的修改导致程序不再进入这个判断过程中。