回顾
复习上一节内容。
通过在多任务调度中每10ms调用keyMain函数检测16位的按键标志keyF(单击),keyDF(双击标志),keyLF(长按)得到按键物理按键码(跟接线有关),getKeyNum函数通过16位按键标志位得到按键位置码,将按键位置码通过表格KeyMap映射为虚拟按键码。将按键的虚拟按键码与时间戳,按键事件类型包装成事件写入到消息队列中。
UINT8 const KeyMap[16] =
{
'1', '2', '3', VK_ENTER,
'4', '5', '6', VK_CONTROL,
'7', '8', '9', VK_SHIFT,
'*', '0', '#', VK_ALT
};
组合键
组合键实现的功能是Ctrl+‘0’产生字符‘A’,Ctrl+‘1’产生字符‘B’,以此类推。
实现步骤
1,在key.h中添加四个宏定义,四个控制按键的标志位置
//四个控制按键的位置码
#define KEY_ENTER 0x08
#define KEY_CONTROL 0x80
#define KEY_SHIFT 0x0800
#define KEY_ALT 0x8000
2,在led.c中添加函数并声明
//这里使用单个静态数码管进行测试
void combinationKey(void *pMsg)//<= KEY_S
{
KEY_S *p = (KEY_S *)pMsg;
MSG_S msg;
if(msgQueueNum(&p->msgQueue))//如果消息队列有数据
{
getMsgQueue(&p->msgQueue,&msg);//获取消息
if(msg.event == KEY_CHAR_EVENT)//
{
if(p->keyDown & KEY_CONTROL)//ctrl按下,并且字符是0-9
{
if(msg.code >= '0' && msg.code <= '9')
{
led_write(LedChars[msg.code - '0' + 10]);// 前十个字符是‘0’-‘9’
}
}
else
{
if(msg.code >= '0' && msg.code <= '9')
{
led_write(LedChars[msg.code - '0']);
}
}
}
}
}
3,将combinationKey函数放到任务列表中,20ms调用一次
4,在KerMain中启用keyScan,keyDScan,keyLScab函数扫描按键。
5,开启时间片任务task_exec();
按键转义
分别是使用Ctrl,Shift,Alt三个键配合左侧12个按键产生其他字符。映射表如下
UINT8 const ConbCtrl[16] =
{
'a', 'b', 'c', VK_ENTER,
'd', 'e', 'f', VK_CONTROL,
'g', 'h', 'i', VK_SHIFT,
'j', 'k', 'l', VK_ALT
};
UINT8 const ConbShift[16] =
{
'm', 'n', 'o', VK_ENTER,
'p', 'q', 'r', VK_CONTROL,
's', 't', 'u', VK_SHIFT,
'v', 'w', 'x', VK_ALT
};
UINT8 const ConbAlt[16] =
{
'y', 'x', ',', VK_ENTER,
'.', '?', '!', VK_CONTROL,
'+', '-', VK_UP, VK_SHIFT,
VK_DOWN, VK_LEFT, VK_RIGHT, VK_ALT
};
消息生成函数
void creatKeyMsg(KEY_S *p,KEY_TYPE *pKeyF,UINT8 event)
{
MSG_S msg;
UINT8 num;
if(*pKeyF && (isMsgQueueFull(&p->msgQueue) == FALSE))//这里是每10ms产生一次消息,并不是使用while将所有标志位全部转换为消息
{
num = getKeyNum(pKeyF);
msg.code = KeyMap[num];//ASCII 码
msg.time = TimeStamp;
if(event == KEY_D_EVENT || event == KEY_L_EVENT)//双击长按不做转义
{
msg.event = event;
}
else
{
if(num != 0x07 && num != 0x0b && num != 0x0f && num != 0x03)//排除Enter,Ctrl,Shift,Alt;
{
switch(p->keyDown & 0x8880)//这里检测的是持续按下状态,Ctrl,Shift,Alt三个中是否有键按下
{
case KEY_CONTROL:
{
msg.code = ConbCtrl[num];
break;
}
case KEY_SHIFT:
{
msg.code = ConbShift[num];
break;
}
case KEY_ALT:
{
msg.code = ConbAlt[num];
break;
}
default:
{
break;
}
}
if(msg.code >= 32 && msg.code <=126)
{
msg.event = KEY_CHAR_EVENT;
}
else
{
msg.event = KEY_DOWN_EVENT;
}
}
}
saveMsgQueue(&p->msgQueue,&msg);
*pKeyF &= ~(1 << num);//这句原来在getKeyNum,清零标志位。
}
}
生产者消费者模型的三种模式
路由模式
生产者产生各种不同类型的消息(这里指四种按键消息类型)
消费者有多个,不同的消费者各自只需要某一种类型的消息,这时就需要将不同类型的消息路由到不同的队列中,简单说就是将产生的消息进行分类发送到不同的队列
实现步骤:
1, key.c创建两个消息队列。
2,在按键结构体中增加路由表数组,因为按键事件有四种类型,这里数组的容量为4。
typedef struct
{
KEY_TYPE keyS2;
KEY_TYPE keyS1;
KEY_TYPE keyFall;
KEY_TYPE keyF;
KEY_TYPE keyDown;
#if(D_CLICK_EN)
KEY_TYPE keyDF;
UINT8 keyDCnt[KEY_NUM];
UINT8 keyDTime;
#endif
KEY_TYPE keyLF;
UINT8 keyLCnt[KEY_NUM];
UINT8 keyLTime;
PFUN_KEY_READ pf;
//===============消息队列开始==============================
MSG_QUEUE_S msgQueue;
//===============消息队列结束==============================
//===============路由模式开始==============================
MSG_QUEUE_S * route[4];
//===============路由模式结束==============================
}KEY_S;
3,初始化路由表,使其指向对应的队列。
void initKey(KEY_S *p,
#if(D_CLICK_EN)
UINT8 dTime,
#endif
UINT8 lTime,PFUN_KEY_READ pf)
{
UINT8 i;
p->keyDown = 0x00;
p->keyF = 0x00;
p->keyFall = 0x00;
p->keyS1 = ~0x00;
p->keyS2 = ~0x00;
#if(D_CLICK_EN)
p->keyDF = 0x00;
p->keyDTime = dTime;
#endif
p->keyLF = 0x00;
p->keyLTime = lTime;
for(i=0;i<KEY_NUM;i++)
{
#if(D_CLICK_EN)
p->keyDCnt[i] = ~0x00;
#endif
p->keyLCnt[i] = ~0x00;
}
p->pf = pf;
//====================消息队列初始化===================================
//initMsgQueue(&p->msgQueue);
//====================消息队列初始化结束===============================
//====================路由模式初始化===================================
initMsgQueue(&Queue0);
initMsgQueue(&Queue1);
p->route[0]=&Queue0;
p->route[1]=&Queue0;
p->route[2]=&Queue1;
p->route[3]=&Queue1;
//====================路由模式初始化结束================================
}
4,产生消息并路由
void createRouteMsg(KEY_S * p,KEY_TYPE *pKeyF,UINT8 event){
MSG_S msg;
UINT8 num;
MSG_QUEUE_S *pQ;
if(*pKeyF){//一次产生一个消息
num=getKeyNum(pKeyF);
msg.code=KeyMap[num];//ASCII 码
msg.time=TimeStamp;
if(event== KEY_D_EVENT||event==KEY_L_EVENT){
msg.event=event;
}else{
if(msg.code>=32 && msg.code<= 126){//可视字符
msg.event=KEY_CHAR_EVENT;
}else{//控制字符
msg.event=KEY_DOWN_EVENT;
}
}
pQ=p->route[msg.event];
if(isMsgQueueFull(pQ)==FALSE){
saveMsgQueue(pQ,&msg);
*pKeyF&=~(1<<num);//清零按键标志位
}
}
}
5,将createRouteMsg函数放到kMain函数中测试。
void keyMain(void *pMsg)
{
KEY_S *p = pMsg;
//keyScan(p,p->pf());
#if(D_CLICK_EN)
//keyDScan(p);
#endif
//keyLScan(p);
//keyF 按键单击标志位
if(p->keyF){//按键单击事件
createRouteMsg(p,&p->keyF,KEY_CHAR_EVENT);
}
if(p->keyDF){//按键双击事件
createRouteMsg(p,&p->keyDF,KEY_D_EVENT);
}
if(p->keyLF){//按键长按事件
createRouteMsg(p,&p->keyLF,KEY_L_EVENT);
}
}

可以看到Queue0中有三个消息,两个字符消息分别为’2’,‘3’,还有一个控制消息Enter.
Queue1中有6个消息, 分别是字符消息123的长按和短按。

订阅模式
多个消费者需要生产者的所有消息。
生产者创建一个订阅数组,存放消费者订阅的消息队列的指针
如果说路由模式是将不同的消息类型存入到不同的队列中,那么订阅模式就是将所有的消息给所有队列都发一遍。
有个问题,不同的消费者消费消息的速度不同可能会产生消费慢的队列存不进消息的情况,这里有几种解决办法:
1,加大消息队列的大小
2,提高消费速度,即放在更短的时间片内
3,使用链表实现队列,动态增长。
实现步骤
0,msgQueue.h添加一个宏定义,作为订阅数组的尾标识
#define MSG_NULL (MSG_QUEUE_S *)0
1,在按键结构中添加订阅数组
typedef struct
{
KEY_TYPE keyS2;
KEY_TYPE keyS1;
KEY_TYPE keyFall;
KEY_TYPE keyF;
KEY_TYPE keyDown;
#if(D_CLICK_EN)
KEY_TYPE keyDF;
UINT8 keyDCnt[KEY_NUM];
UINT8 keyDTime;
#endif
KEY_TYPE keyLF;
UINT8 keyLCnt[KEY_NUM];
UINT8 keyLTime;
PFUN_KEY_READ pf;
//===============消息队列开始==============================
MSG_QUEUE_S msgQueue;
//===============消息队列结束==============================
//===============路由模式开始==============================
MSG_QUEUE_S * route[4];
//===============路由模式结束==============================
//===============订阅模式开始==============================
MSG_QUEUE_S * Subscribe[4];
//===============订阅模式结束==============================
}KEY_S;
2,initKey中初始化订阅数组
//====================订阅模式初始化===================================
initMsgQueue(&Queue0);
initMsgQueue(&Queue1);
p->Subscribe[0]=&Queue0;
p->Subscribe[1]=&Queue1;
p->Subscribe[2]=MSG_NULL;
p->Subscribe[3]=MSG_NULL;
//====================订阅模式初始化结束================================
3,生成消息并存入所有队列
void creatSubscribeMsg(KEY_S * p,KEY_TYPE *pKeyF,UINT8 event){
MSG_S msg;
UINT8 num,i,len;
MSG_QUEUE_S *pQ;
if(*pKeyF){//一次产生一个消息
num=getKeyNum(pKeyF);
msg.code=KeyMap[num];//ASCII 码
msg.time=TimeStamp;
if(event== KEY_D_EVENT||event==KEY_L_EVENT){
msg.event=event;
}else{
if(msg.code>=32 && msg.code<= 126){//可视字符
msg.event=KEY_CHAR_EVENT;
}else{//控制字符
msg.event=KEY_DOWN_EVENT;
}
}
len=sizeof(p->Subscribe)/sizeof(MSG_QUEUE_S*);
for(i=0;i<len;i++){//或者使用while(p->Subscribe[i]!=NULL)pQ=p->Subscribe[i];if(pQ!=NULL&&isMsgQueueFull(pQ)==FALSE){saveMsgQueue(pQ,&msg);}}*pKeyF&=~(1<<num);//清零按键标志位}}
4,测试
void keyMain(void *pMsg)
{
KEY_S *p = pMsg;
//keyScan(p,p->pf());
#if(D_CLICK_EN)
//keyDScan(p);
#endif
//keyLScan(p);
//keyF 按键单击标志位
if(p->keyF){//按键单击事件
creatSubscribeMsg(p,&p->keyF,KEY_CHAR_EVENT);
}
if(p->keyDF){//按键双击事件
creatSubscribeMsg(p,&p->keyDF,KEY_D_EVENT);
}
if(p->keyLF){//按键长按事件
creatSubscribeMsg(p,&p->keyLF,KEY_L_EVENT);
}
}


可以看到Queue0,Queue1中的消息是一样的。
主题模式
路由模式是将需要类型的消息存入到队列,订阅模式则是一股脑全部存进去,主题模式是一种折中的方案选择性存储。
在队列中加入一个标识符合集,如果消息类型与标识符合集中的类型相同则消息存入队列,其实就是队列设置了入队条件,选择性的存入消息,不在那么无脑全存。
比如上图中第一个队列 需要字符消息和双击消息,则这两种消息都会存入到队列1中。队列2需要字符消息和长按消息。
消费消息的速度不同可能会产生存不进消息的情况还是同样的处理方式。
1,在按键结构中定义主题栏
//===============主题模式开始==============================
MSG_QUEUE_S * subject[4];
//===============主题模式结束==============================
2,在队列结构中创建主题的标识符集 msgQueue.h
typedef struct{
MSG_S msg[MSG_NUM];
UINT8 num;
UINT8 save;
UINT8 get;
UINT8 id[2];
}MSG_QUEUE_S;
3,增加一个队列Queue2,并初始化。
//====================主题模式初始化===================================
initMsgQueue(&Queue0);//队列0需要字符消息和双击消息
Queue0.id[0]=KEY_CHAR_EVENT;
Queue0.id[1]=KEY_D_EVENT;
initMsgQueue(&Queue1);//队列1需要字符消息和长按消息
Queue1.id[0]=KEY_CHAR_EVENT;
Queue1.id[1]=KEY_L_EVENT;
initMsgQueue(&Queue2);//队列2只需要控制消息
Queue2.id[0]=KEY_DOWN_EVENT;
Queue2.id[1]=0xff;
p->subject[0]=&Queue0;
p->subject[1]=&Queue1;
p->subject[2]=&Queue2;
p->subject[3]=MSG_NULL;
//====================主题模式初始化结束================================
4,生成消息并入队
void creatSubjectMsg(KEY_S * p,KEY_TYPE *pKeyF,UINT8 event){
MSG_S msg;
UINT8 num,i,len,j;
MSG_QUEUE_S *pQ;
if(*pKeyF){//一次产生一个消息
num=getKeyNum(pKeyF);
msg.code=KeyMap[num];//ASCII 码
msg.time=TimeStamp;
if(event== KEY_D_EVENT||event==KEY_L_EVENT){
msg.event=event;
}else{
if(msg.code>=32 && msg.code<= 126){//可视字符
msg.event=KEY_CHAR_EVENT;
}else{//控制字符
msg.event=KEY_DOWN_EVENT;
}
}
len=sizeof(p->subject)/sizeof(MSG_QUEUE_S*);
for(i=0;i<len;i++){
pQ=p->subject[i];
if(pQ!=NULL&&isMsgQueueFull(pQ)==FALSE){
for(j=0;j<2;j++){
if(msg.event==p->subject[i]->id[j]){//匹配标识符
saveMsgQueue(pQ,&msg);
break;//匹配到了就可以跳出
}
}
}
}
*pKeyF&=~(1<<num);//清零按键标志位
}
}
5,测试
void keyMain(void *pMsg)
{
KEY_S *p = pMsg;
//keyScan(p,p->pf());
#if(D_CLICK_EN)
//keyDScan(p);
#endif
//keyLScan(p);
//keyF 按键单击标志位
if(p->keyF){//按键单击事件
creatSubjectMsg(p,&p->keyF,KEY_CHAR_EVENT);
}
if(p->keyDF){//按键双击事件
creatSubjectMsg(p,&p->keyDF,KEY_D_EVENT);
}
if(p->keyLF){//按键长按事件
creatSubjectMsg(p,&p->keyLF,KEY_L_EVENT);
}
}

Queue0两个字符类型加三个双击共5个
Queue1两个字符类型加三个长按类型
Queue2只有一个控制类型 没有问题。