按键引脚工作逻辑说明.md 28 KB

新版模拟分机STM32 MCU按键引脚工作逻辑说明文档

1. 引脚配置概述

本系统使用多个GPIO引脚连接不同功能的按键,主要包括护理按键、增援按键、取消按键、呼叫按键、手柄呼叫、手柄换药和手柄拔针等功能。

1.1 按键引脚定义

mapp_gpio.h文件中,定义了按键输入引脚:

// 文件: source/user/mapp_gpio.h
#define KEY_PA7_EMEGENCY_DET       GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_7)  // 紧急按钮
#define KEY_PA4_HANDLE_DET         GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_4)  // 手柄检测
#define KEY_PA5_CHANGE_DET         GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_5)  // 换药按键
#define KEY_PB3_NURSE_DET          GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_3)  // 护理按键
#define KEY_PB5_CALL_DET           GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_5)  // 呼叫按键
#define KEY_PA15_REINFORCE_DET     GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_15) // 增援按键
#define KEY_PB4_CANCEL_DET         GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_4)  // 取消按键

1.2 语音功能引脚定义

语音输出相关的引脚定义:

// 文件: source/user/mapp_gpio.h
#define VOICE_PB15_ON     GPIO_SetBits(GPIOB, GPIO_Pin_15)     // 语音芯片数据线
#define VOICE_PB15_OFF    GPIO_ResetBits(GPIOB, GPIO_Pin_15)   
#define VOICE_PF7_ON      GPIO_SetBits(GPIOF, GPIO_Pin_7)      // 语音通道控制
#define VOICE_PF7_OFF     GPIO_ResetBits(GPIOF, GPIO_Pin_7)
#define VOICESW_H         GPIO_SetBits(GPIOA, GPIO_Pin_0)      // 语音通道
#define VOICESW_L         GPIO_ResetBits(GPIOA, GPIO_Pin_0)

2. 按键状态扫描流程

2.1 按键信号采集

mapp_key.c文件的KeyTask函数中,每35ms执行一次按键扫描,采集各个按键的状态:

// 文件: source/user/mapp_key.c
void KeyTask(void)
{
    static U08  last_KeyVal[KEY_COUNT] = {0};
    static U08  trascnt1[KEY_COUNT]    = {0};
    static U08  trascnt2[KEY_COUNT]    = {0};
    U08         index      = 0;
    U08         u8KeyVal[KEY_COUNT] = {0};
  
    u8KeyVal[0] = !KEY_PB5_CALL_DET;          // 呼叫按键
    u8KeyVal[1] = !gtArm.PA4_Value;           // 手柄检测
    u8KeyVal[2] = KEY_PA7_EMEGENCY_DET;       // 紧急按钮
    u8KeyVal[3] = !KEY_PA15_REINFORCE_DET;    // 增援按键
    u8KeyVal[4] = !KEY_PB3_NURSE_DET;         // 护理按键
    u8KeyVal[5] = !KEY_PA5_CHANGE_DET;        // 换药按键
    u8KeyVal[6] = !KEY_PB4_CANCEL_DET;        // 取消按键
    
    // 按键状态处理...
}

2.2 按键去抖处理与按键状态位存储

在按键信号采集后,系统会进行按键去抖处理,只有连续多次采样状态一致才认为是有效按键。同时,按键状态会被存储在gtArm.KeyState变量中。

2.2.1 gtArm.KeyState结构和位映射

gtArm.KeyState是一个8位无符号整数(U08)变量,每一位对应一个按键的状态:

二进制值 十六进制值 对应按键 对应KEY变量
位0 0x01 0x01 呼叫按键(KEY_PB5_CALL_DET) KEY1
位1 0x02 0x02 手柄呼叫(KEY_PA4_HANDLE_DET) KEY0
位2 0x04 0x04 紧急按钮(KEY_PA7_EMEGENCY_DET) KEY3
位3 0x08 0x08 增援按键(KEY_PA15_REINFORCE_DET) KEY5
位4 0x10 0x10 护理按键(KEY_PB3_NURSE_DET) KEY4
位5 0x20 0x20 换药按键(KEY_PA5_CHANGE_DET) KEY2
位6 0x40 0x40 取消按键(KEY_PB4_CANCEL_DET) KEY3
位7 0x80 0x80 未使用

当对应位的值为1时,表示该按键被按下;值为0时,表示按键未被按下。

注意:位2(0x04)和位6(0x40)都映射到同一个KEY3变量,这可能会导致功能混淆。

2.2.2 按键状态位操作详解

对于每个按键,都进行如下处理:

// 文件: source/user/mapp_key.c
for (index = 0; index < KEY_COUNT; index++) {
    if (u8KeyVal[index] == 0) {   // 没有按下
        gtArm.KeyState &= ~(1 << index);  // 清除对应位,表示按键未按下
        trascnt1[index] = 0;
        trascnt2[index]++;
        s_keytime[index] = 0;
        if (trascnt2[index] > SCAN_ACK_COUNT) {  // 防抖处理
            trascnt2[index] = 0;
            if (last_KeyVal[index] != u8KeyVal[index]) {
                // 处理按键释放事件
                OSQPost(AmiMsgQueue, ARM_MSG_KEY, TO_WORD(index+5, KEY_RELEASED));
                last_KeyVal[index] = u8KeyVal[index];
            }
        }
    } else {  // 按键按下
        gtArm.KeyState |= (1 << index);  // 设置对应位为1,表示按键按下
        trascnt1[index]++;
        trascnt2[index] = 0;
        
        if (trascnt1[index] > SCAN_ACK_COUNT) {  // 防抖处理
            trascnt1[index] = 0;
            if (s_keytime[index] > OS_TIMER_1S5) {  // 长按判断
                if (last_KeyVal[index] != KEY_CONTINUE) {
                    last_KeyVal[index] = KEY_CONTINUE;
                }
            } else if (last_KeyVal[index] != u8KeyVal[index]) {
                // 处理按键按下事件
                OSQPost(AmiMsgQueue, ARM_MSG_KEY, TO_WORD(index+5, KEY_PRESSED));
                last_KeyVal[index] = u8KeyVal[index];
            }
        }
    }
}

2.2.3 位操作详细说明

  1. 按键未按下时的位操作c // 文件: source/user/mapp_key.c gtArm.KeyState &= ~(1 << index); 这行代码执行以下操作:
    • (1 << index) 创建一个只有第index位为1的掩码
    • ~(1 << index) 对掩码取反,创建一个除第index位外都为1的掩码
    • gtArm.KeyState &= ~(1 << index) 使用按位与操作,保留gtArm.KeyState中除了第index位之外的所有位,而将第index位清零

例如,当index=3时(增援按键),操作过程为:

  • (1 << 3) = 0x08
  • ~(1 << 3) = 0xF7 (二进制: 11110111)
  • gtArm.KeyState &= 0xF7 将gtArm.KeyState的第3位清零,其他位保持不变
  1. 按键按下时的位操作c // 文件: source/user/mapp_key.c gtArm.KeyState |= (1 << index); 这行代码执行以下操作:
    • (1 << index) 创建一个只有第index位为1的掩码
    • gtArm.KeyState |= (1 << index) 使用按位或操作,将gtArm.KeyState的第index位设置为1,其他位保持不变

例如,当index=4时(护理按键),操作过程为:

  • (1 << 4) = 0x10 (二进制: 00010000)
  • gtArm.KeyState |= 0x10 将gtArm.KeyState的第4位设置为1,其他位保持不变

2.2.4 按键防抖机制

为了防止按键抖动,系统使用了计数器:

  1. 按键按下防抖: 使用trascnt1[index]计数器,只有当连续多次(SCAN_ACK_COUNT次)检测到按键按下时,才认为按键真正按下,并触发相应的事件。

  2. 按键释放防抖: 使用trascnt2[index]计数器,只有当连续多次(SCAN_ACK_COUNT次)检测到按键释放时,才认为按键真正释放,并触发相应的事件。

这种防抖机制确保了按键信号的稳定性,避免了由于机械抖动导致的误触发。

2.2.5 按键状态与事件触发

当按键状态变化且通过防抖处理后,系统会触发相应的事件:

  1. 按键按下事件

    // 文件: source/user/mapp_key.c
    OSQPost(AmiMsgQueue, ARM_MSG_KEY, TO_WORD(index+5, KEY_PRESSED));
    
  2. 按键释放事件

    // 文件: source/user/mapp_key.c
    OSQPost(AmiMsgQueue, ARM_MSG_KEY, TO_WORD(index+5, KEY_RELEASED));
    

这些事件会被发送到消息队列AmiMsgQueue中,后续由事件处理系统进行处理。

3. 按键消息处理流程

3.1 消息队列处理

按键事件通过消息队列AmiMsgQueue传递给消息处理函数。在AmiTxTask函数中会调用相应的处理函数:

// 文件: source/user/mapp_arm.c
void AmiTxTask(void)
{
    T_MESSAGE *pmsg;
    pmsg = OSQGet(AmiMsgQueue);
    if (pmsg->id < NumOfAMIMsg) {
        arryAmiTxFuncTbl[pmsg->id](pmsg->prm);
    }
}

3.2 按键消息到UART发送

按键消息经过处理后,通过s_frmAmiTxKey函数将按键代码转换为UART数据:

// 文件: source/user/mapp_arm.c
static void s_frmAmiTxKey(U16 prm)
{
    U08 *buf = (U08*)NULL;
    buf = &s_u8AmiTxBuf[0];
    *buf++ = '$';   
    // 添加地址信息...
    *buf++ = 'K';    
    *buf++ = 'E';   
    *buf++ = 'Y';   
    
    // 根据不同按键设置对应代码
    if (HI_BYTE(prm) == 8) {
        *buf++ = 0x30 + 0x0b;
    } else if (HI_BYTE(prm) == 9) {
        *buf++ = 0x30 + 0x0c;
    } else if (HI_BYTE(prm) == 0xa) {
        *buf++ = 0x30 + 0x0d;
    } else if (HI_BYTE(prm) == 0xb) {
        *buf++ = 0x30 + 0x0e;
    } else {
        *buf++ = 0x30 + HI_BYTE(prm);
    }
    
    *buf++ = ',';   
    gtArm.startworkflag = 1;
    *buf++ = 0X30 + LO_BYTE(prm);
    *buf++ = '#';
    Uart2PutChar(s_u8AmiTxBuf, 9);
}

3.3 按键消息到UART的完整流程

按键信号从物理按键被按下到最终通过UART发送出去,需要经过多个阶段的处理和转换。以下是这个完整流程的详细解释:

3.3.1 按键信号采集与状态存储

  1. 物理按键状态读取: 在mapp_key.cKeyTask函数中,系统通过GPIO读取引脚状态:

    // 文件: source/user/mapp_key.c
    u8KeyVal[0] = !KEY_PB5_CALL_DET;          // 呼叫按键
    u8KeyVal[1] = !gtArm.PA4_Value;           // 手柄检测
    u8KeyVal[2] = KEY_PA7_EMEGENCY_DET;       // 紧急按钮
    u8KeyVal[3] = !KEY_PA15_REINFORCE_DET;    // 增援按键
    u8KeyVal[4] = !KEY_PB3_NURSE_DET;         // 护理按键
    u8KeyVal[5] = !KEY_PA5_CHANGE_DET;        // 换药按键
    u8KeyVal[6] = !KEY_PB4_CANCEL_DET;        // 取消按键
    
  2. 按键状态存储: 读取的状态经过防抖处理后,存储在gtArm.KeyState变量中:

    // 文件: source/user/mapp_key.c
    if (u8KeyVal[index] == 0) {  
       gtArm.KeyState &= ~(1 << index);  // 清除对应位
    } else {
       gtArm.KeyState |= (1 << index);   // 设置对应位
    }
    

3.3.2 按键消息投递到消息队列

  1. 状态变化检测: 当按键状态发生变化且通过防抖处理后,系统会生成按键事件:

    // 文件: source/user/mapp_key.c
    // 按键按下事件
    OSQPost(AmiMsgQueue, ARM_MSG_KEY, TO_WORD(index+5, KEY_PRESSED));
    
  2. 消息队列结构: OSQPost函数投递消息到消息队列中,消息包含三个部分:

    • 消息队列:AmiMsgQueue
    • 消息类型:ARM_MSG_KEY(表示这是按键消息)
    • 消息参数:TO_WORD(index+5, KEY_PRESSED)(高字节是按键索引+5,低字节是KEY_PRESSED表示按下状态)
   // TO_WORD宏定义(通常在头文件中)
   #define TO_WORD(h, l)    (((U16)(h) << 8) | (l))

3.3.3 消息队列处理与函数调用

  1. 消息获取: 在mapp_arm.cAmiTxTask函数中,系统从消息队列中获取消息:

    // 文件: source/user/mapp_arm.c
    T_MESSAGE *pmsg;
    pmsg = OSQGet(AmiMsgQueue);
    
  2. 处理函数映射: 根据消息ID调用对应的处理函数:

    // 文件: source/user/mapp_arm.c
    if (pmsg->id < NumOfAMIMsg) {
       arryAmiTxFuncTbl[pmsg->id](pmsg->prm);
    }
    

其中arryAmiTxFuncTbl是一个函数指针数组,定义了不同消息ID对应的处理函数:

   // 文件: source/user/mapp_arm.c
   static void (*const arryAmiTxFuncTbl[NumOfAMIMsg])(U16 prm) = {
       s_frmAmiTxEvent,   // ARM_MSG_EVENT
       s_frmAmiTxKey,     // ARM_MSG_KEY
       s_frmAmiTxReply,   // ARM_MSG_REPLY
       // 其他处理函数...
   };

当接收到ARM_MSG_KEY类型的消息时,系统会调用s_frmAmiTxKey函数。

3.3.4 按键代码到UART数据转换

s_frmAmiTxKey函数将按键代码转换为UART协议格式的数据:

// 文件: source/user/mapp_arm.c
static void s_frmAmiTxKey(U16 prm)
{
    U08 *buf = (U08*)NULL;
    buf = &s_u8AmiTxBuf[0];
    
    // 1. 帧头
    *buf++ = '$';   
    
    // 2. 添加地址信息(从配置读取)
    *buf++ = s_u8RoomId[0];
    *buf++ = s_u8RoomId[1];
    
    // 3. 添加命令类型 - "KEY"表示按键命令
    *buf++ = 'K';    
    *buf++ = 'E';   
    *buf++ = 'Y';   
    
    // 4. 按键代码转换
    // 根据按键索引设置对应代码
    if (HI_BYTE(prm) == 8) {
        *buf++ = 0x30 + 0x0b;  // ASCII码转换
    } else if (HI_BYTE(prm) == 9) {
        *buf++ = 0x30 + 0x0c;
    } else if (HI_BYTE(prm) == 0xa) {
        *buf++ = 0x30 + 0x0d;
    } else if (HI_BYTE(prm) == 0xb) {
        *buf++ = 0x30 + 0x0e;
    } else {
        *buf++ = 0x30 + HI_BYTE(prm);  // 其他按键,直接加0x30转ASCII
    }
    
    // 5. 分隔符
    *buf++ = ',';   
    
    // 6. 按键状态(按下/释放)
    gtArm.startworkflag = 1;  // 标记系统开始工作
    *buf++ = 0X30 + LO_BYTE(prm);  // 状态码转ASCII:0=释放,1=按下
    
    // 7. 帧尾
    *buf++ = '#';
    
    // 8. 发送数据到UART
    Uart2PutChar(s_u8AmiTxBuf, 9);  // 发送9字节数据
}

3.3.5 UART数据发送

Uart2PutChar函数将按键消息按照特定协议格式发送到UART接口:

// 文件: source/user/mapp_arm.c 或 source/user/uart.c
void Uart2PutChar(U08 *pu8data, U16 len)
{
    U16 i;
    for(i=0; i<len; i++) {
        // 等待UART发送缓冲区空
        while(USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET);
        // 发送一个字节
        USART_SendData(USART2, *pu8data++);
    }
}

3.3.6 协议格式说明

最终发送的UART数据遵循以下格式:

$RRKEY<k>,<s>#

其中:

  • $ - 帧起始标记
  • RR - 房间/设备地址(两个字节)
  • KEY - 命令类型(表示按键消息)
  • <k> - 按键代码(ASCII码表示的十六进制数)
  • , - 分隔符
  • <s> - 按键状态(1=按下,0=释放)
  • # - 帧结束标记

例如,当护理按键(KEY4)被按下时,可能发送的数据为:

$01KEY9,1#

这表示设备地址01的护理按键被按下。

3.3.7 总结

按键消息从物理按下到UART发送的完整流程为:

  1. GPIO引脚状态读取和防抖
  2. 按键状态存储在gtArm.KeyState变量
  3. 状态变化时生成消息并投递到消息队列
  4. 消息队列系统调用对应的处理函数
  5. 处理函数将按键代码转换为特定协议格式
  6. 通过UART发送出去

这个流程确保了按键信号能够被可靠地处理并转换为标准化的通信协议,供其他设备(如主控系统)接收和处理。

3.3.8 消息ID(pmsg->id)的设置过程

在代码片段中,我们看到pmsg->id用于确定调用哪个处理函数:

// 文件: source/user/mapp_arm.c
pmsg = OSQGet(ArmMsgQueue);  // 注意这里是ArmMsgQueue而非之前提到的AmiMsgQueue
if (pmsg->id < NumOfArmMsg) {
    arryArmTxFuncTbl[pmsg->id](pmsg->prm);  // 使用消息ID索引处理函数
}

那么这个pmsg->id是如何设置的呢?这涉及到消息队列的核心机制:

  1. 消息结构定义: 系统定义了一个消息结构体,通常如下:

    // 文件: source/user/mapp_os.h 或其他头文件
    typedef struct {
       U08 id;    // 消息ID
       U16 prm;   // 消息参数
    } T_MESSAGE;
    
  2. 消息队列的创建: 系统在初始化阶段创建消息队列:

    // 文件: source/user/mapp_arm.c 或初始化文件
    ArmMsgQueue = OSQCreate(sizeof(arryMsg) / sizeof(T_MESSAGE));
    
  3. 消息ID的分配: 系统通常用枚举定义各类消息ID:

    // 文件: source/user/mapp_arm.h 或其他头文件
    enum {
       ARM_MSG_EVENT = 0,  // 事件消息
       ARM_MSG_KEY,        // 按键消息
       ARM_MSG_REPLY,      // 回复消息
       // 其他消息类型...
       NumOfArmMsg         // 消息类型总数
    };
    
  4. 消息投递时ID的设置: 在OSQPost函数中,第二个参数就是消息ID:

    // 文件: source/user/mapp_key.c
    OSQPost(ArmMsgQueue, ARM_MSG_KEY, TO_WORD(index+5, KEY_PRESSED));
    //       消息队列    ^消息ID     消息参数
    
  5. OSQPost函数实现: 这个函数将消息ID和参数封装到消息结构体中:

    // 文件: source/user/mapp_os.c 或RTOS源码
    U08 OSQPost(U08 qid, U08 id, U16 prm)
    {
       if (qid >= MAX_QUEUE || s_QueueSize[qid] >= MAX_QUEUE_SIZE) {
           return FALSE;
       }
           
       // 设置消息ID和参数
       s_arryMsg[qid][s_QueueSize[qid]].id = id;   // 这里设置了pmsg->id
       s_arryMsg[qid][s_QueueSize[qid]].prm = prm;
           
       s_QueueSize[qid]++;
       return TRUE;
    }
    
  6. OSQGet函数实现: 当任务调用OSQGet获取消息时,会返回包含id和prm的消息结构体:

    // 文件: source/user/mapp_os.c 或RTOS源码
    T_MESSAGE* OSQGet(U08 qid)
    {
       static T_MESSAGE msg;
           
       if (qid >= MAX_QUEUE || s_QueueSize[qid] == 0) {
           return NULL;
       }
           
       // 获取队列第一个消息
       msg = s_arryMsg[qid][0];
           
       // 队列前移
       s_QueueSize[qid]--;
       for (U08 i = 0; i < s_QueueSize[qid]; i++) {
           s_arryMsg[qid][i] = s_arryMsg[qid][i+1];
       }
           
       return &msg;  // 返回消息结构体,其中包含id和prm
    }
    

因此,pmsg->id是在消息投递(OSQPost)时设置的,用于标识消息类型,接收方根据这个ID选择相应的处理函数。在按键处理流程中,当按键状态变化并通过防抖处理后,系统会投递一个ID为ARM_MSG_KEY的消息,然后在消息处理任务中根据这个ID调用按键处理函数s_frmArmTxKey。

4. 按键处理与语音控制逻辑

4.1 按键状态再处理

mapp_bus.c文件的GPIO_KEY_Tick函数中,对gtArm.KeyState进行再次处理,将按键状态转换为功能状态:

// 文件: source/user/mapp_bus.c
void GPIO_KEY_Tick(void)
{
    static unsigned char KeyoldState = 0;
    
    // 复位按键状态
    if (KEY0 == 2) KEY0 = 0;
    if (KEY1 == 2) KEY1 = 0;
    // ...
    
    // 检测按键状态变化
    if ((KeyoldState & 0x01) != (gtArm.KeyState & 0x01)) {
        if (gtArm.KeyState & 0x01) {
            KEY1 = 1;
        } else {
            KEY1 = 2;
        }
    }
    // 其他按键检测...
    
    // 特殊组合按键检测
    if ((KEY0 == 1) && (KEY2 == 1)) {
        KEY0 = 0;
        KEY2 = 0;
        KEY6 = 1;
        key6laycnt = 200;  // 拔针释放两个按键不同步,需要延时
    } else {
        KEY6 = 0;
        if (key6laycnt) {
            key6laycnt--;
            KEY0 = 0;
            KEY2 = 0;
        }
    }
    
    KeyoldState = gtArm.KeyState;
}

这里存在一个潜在问题:按键状态处理可能存在冲突:

// 文件: source/user/mapp_bus.c
// 第一次检测 - 位掩码0x04
if ((KeyoldState & 0x04) != (gtArm.KeyState & 0x04)) { ... }

// 第二次检测 - 位掩码0x40
if ((KeyoldState & 0x40) != (gtArm.KeyState & 0x40)) { ... }

这可能导致按键状态混乱,特别是当0x40位发生变化时。

4.2 功能逻辑处理

按键状态转换后,在BUSTask函数中根据按键状态执行相应的功能逻辑:

// 文件: source/user/mapp_bus.c
void BUSTask(void)
{
    // 处理护理按键状态计时
    if (nursestate) {  // 护理按键延时用
        ii++;
    } else {
        ii = 0;
    }
    
    // 语音命令处理
    sentvoicecmd(VoiceCmdApp);
    
    // 按键功能处理
    if (KEY_OLD[2] != KEY2) {    // 换药
        if (KEY2 == 1) {
            brm.DaddressDate = (uint16_t)((Addrp[0] << 8) | Addrp[1]);
            brm.DataType = 0x4;
            BUS_SendData(brm);
            VoiceCmdApp = CMD_CHANGE;
        }
        KEY_OLD[2] = KEY2;
    } else if (KEY_OLD[3] != KEY3) {  // 取消 - 原增援按键改为取消
        if (KEY3 == 1) {
            brm.DaddressDate = (uint16_t)((Addrp[0] << 8) | Addrp[1]);
            brm.DataType = 0x05;
            BUS_SendData(brm);
            VoiceCmdApp = CMD_CALLESC;
        }
        KEY_OLD[3] = KEY3;
    } else if (KEY_OLD[4] != KEY4) {    // 护理 - 原护理按键位置不变
        // 5秒后再按,要发退出
        if (ii > 500) { // 5秒
            nursestate = 0;
            if (KEY4 == 1) {
                brm.DaddressDate = (uint16_t)((Addrp[0] << 8) | Addrp[1]);
                brm.DataType = 0x03;
                BUS_SendData(brm);
                VoiceCmdApp = CMD_NURSEOUT;
            }
        } else {
            ii = 0;
            nursestate = 1;
            if (KEY4 == 1) {
                brm.DaddressDate = (uint16_t)((Addrp[0] << 8) | Addrp[1]);
                brm.DataType = 0x03;
                BUS_SendData(brm);
                VoiceCmdApp = CMD_NURSEIN;
            }
        }
        KEY_OLD[4] = KEY4;
    } else if (KEY_OLD[5] != KEY5) {      // 增援 - 原取消按键改为增援
        if (KEY5 == 1) {
            brm.DaddressDate = (uint16_t)((Addrp[0] << 8) | Addrp[1]);
            brm.DataType = 0x02;
            BUS_SendData(brm);
            VoiceCmdApp = CMD_REINFORCE;
        }
        KEY_OLD[5] = KEY5;
    }
    // 其他按键处理...
}

4.3 语音命令处理

语音命令由sentvoicecmd函数处理:

// 文件: source/user/mapp_bus.c
void sentvoicecmd(unsigned char prm)
{
    static unsigned char bcnt = 0;
    static unsigned char VoiceCmdOld = 0;
    static unsigned short s5muitecnt = 0;   // 5秒后可以继续发语音

    if (BUS_SendInex) {  // 总线数据先发
        return;
    }
    
    if (prm == CMD_NULL) {
        VoiceCmdOld = prm;
        s5muitecnt = 0;
        VOICE_PF7_ON;  // 恢复呼叫通话
        return;
    }
    
    s5muitecnt++;
    if (s5muitecnt > 400) {  // 5秒后关闭
        VOICE_PF7_ON;  // 恢复呼叫通话
        VoiceCmdApp = CMD_NULL;
        VoiceCmdOld = CMD_NULL;
        bcnt = 10;
        return;
    }
    
    if (VoiceCmdOld != prm) {    
        // 语音芯片播报声音通道打开
        VOICE_PF7_OFF;
        VoiceCmdAppAck = VoiceCmdApp;
        
        s5muitecnt = 0;
        VoiceCmd = 0xfe;
        VoiceCmdFlag = 1;
        bcnt = 10;
        VoiceCmdOld = prm;
        TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE);
    } 
    
    // 发送语音指令
    if (bcnt == 0) {
        VoiceCmdFlag = 1;
        VoiceCmd = VoiceCmdOld;
        TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE);
        bcnt = 100;
    } else if (VoiceCmd == 0xfe) {
        bcnt--;
    }
}

4.4 语音芯片控制

语音芯片通过PWM波形控制,在TIM6定时器中断中实现:

// 文件: source/user/mapp_bus.c
void TIM6_WTV3808S_TASK(void) 
{
    static unsigned char voiceflag = 0;
    switch (voiceflag) {
        case 0: 
            if (VoiceCmdFlag) {
                voiceflag = 1;
                TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE);
            }
            break;
        case 1: 
            VOICE_PB15_OFF;
            VoiceCmdFlag++;
            if (VoiceCmdFlag > 55) { // 5ms
                voiceflag++;
                VoiceCmdFlag = 150;
            }
            break;
        case 2:
            VOICE_PB15_ON;
            // 根据VoiceCmd的位生成PWM波形
            // ...
            break;
        // 更多状态...
    }
}

4.4 BUSTask函数的实现逻辑解析

BUSTask函数是整个按键处理系统的最上层逻辑实现,每10ms被调用一次,负责处理按键功能和语音命令:

// 文件: source/user/mapp_bus.c
void BUSTask(void)
{
    // 1. 语音命令处理
    sentvoicecmd(VoiceCmdApp);
    
    // 2. 护理按键状态计时
    if (nursestate) { ii++; } else { ii = 0; }
    
    // 3. 调用底层功能处理函数
    GPIO_MIC_SW(Micswitch);
    GPIO_LED_Tick();
    BUS_Rev_Tick();
    GPIO_KEY_Tick();  // 处理按键状态
    
    // 4. 语音通道控制
    // 根据Voiceswitch和VoiceCmdApp控制VOICESW_H/L
    
    // 5. 按键功能处理逻辑
    if (KEY_OLD[2] != KEY2) {  // 换药
        if (KEY2 == 1) {
            // 发送数据包
            VoiceCmdApp = CMD_CHANGE;
        }
        KEY_OLD[2] = KEY2;
    } 
    else if (KEY_OLD[3] != KEY3) {  // 取消
        if (KEY3 == 1) {
            // 发送数据包
            VoiceCmdApp = CMD_CALLESC;
        }
        KEY_OLD[3] = KEY3;
    } 
    else if (KEY_OLD[4] != KEY4) {  // 护理
        if (ii > 500) {  // 5秒后再按 = 退出
            // 发送退出命令
            VoiceCmdApp = CMD_NURSEOUT;
        } else {  // 进入
            // 发送进入命令
            VoiceCmdApp = CMD_NURSEIN;
        }
        KEY_OLD[4] = KEY4;
    }
    // 类似处理其他按键...
}

4.4.1 主要功能

  1. 语音命令处理:调用sentvoicecmd函数处理语音命令
  2. 按键状态管理:调用GPIO_KEY_Tick更新按键状态
  3. 功能逻辑处理:检测按键状态变化,触发对应功能
  4. 通信数据发送:构造数据包发送到总线

4.4.2 按键处理模式

函数采用"状态变化检测"模式处理按键:

if (KEY_OLD[#] != KEY#) {  // 检测按键状态变化
    if (KEY# == 1) {       // 按键按下时执行
        // 构造数据包并发送
        VoiceCmdApp = CMD_XXX;  // 设置语音命令
    }
    KEY_OLD[#] = KEY#;  // 更新旧状态
}

4.4.3 特殊处理

  1. 护理按键:通过nursestate和计数器ii实现长按检测

    • 短按:触发"护理进入"(CMD_NURSEIN)
    • 5秒后再按:触发"护理退出"(CMD_NURSEOUT)
  2. 语音通道控制:根据状态控制语音通道开关

    • 有语音命令或Voiceswitch=1时打开通道
    • 无语音命令且Voiceswitch=0时关闭通道

4.4.4 按键功能映射

按键 数据类型 语音命令 功能
KEY2 PA5 换药 CMD_CHANGE
KEY3 PB4 取消 CMD_CALLESC
KEY4 PB3 护理 CMD_NURSEIN(按下) / CMD_NURSEOUT(5秒后再按)
KEY5 PA15 增援 CMD_REINFORCE
KEY6 组合 拔针 CMD_NEEDLES

4.4.5 代码问题

  1. 重复代码:按键处理逻辑高度重复
  2. 条件嵌套:多层if-else结构复杂
  3. 硬编码:大量使用数字常量
  4. 职责过多:单函数承担太多功能

BUSTask是整个按键系统的业务核心,将底层按键状态转换为实际功能操作和语音命令,完成了从物理信号到用户功能的最终映射。

5. 按键与语音关系映射

各按键触发的语音命令总结如下:

按键 引脚 功能 语音命令
KEY2 PA5 换药 CMD_CHANGE
KEY3 PB4 取消 CMD_CALLESC
KEY4 PB3 护理 CMD_NURSEIN(按下) / CMD_NURSEOUT(5秒后再按)
KEY5 PA15 增援 CMD_REINFORCE
KEY6 组合 拔针 CMD_NEEDLES

6. 潜在问题分析

  1. 多重触发问题

按键触发可能存在逻辑冲突问题:

   // 文件: source/user/mapp_bus.c
   if ((KeyoldState & 0x04) != (gtArm.KeyState & 0x04)) { ... }
   if ((KeyoldState & 0x40) != (gtArm.KeyState & 0x40)) { ... }

这可能导致KEY3被错误地更新,特别是当两个位同时或先后变化时。

  1. 护理状态计时问题

护理按键(KEY4)按下后会设置nursestate=1并开始累加计数器ii。如果此时由于0x40位变化错误触发了KEY3,且ii>500,系统可能会误认为这是护理功能的长按退出,从而触发CMD_NURSEOUT语音。

  1. 组合按键处理冲突

当组合按键KEY0和KEY2同时按下触发拔针功能时,会将两个按键状态清零,这可能与其他状态检测逻辑产生冲突。

7. 总结

本文档详细分析了按键引脚从配置到语音处理的完整工作流程,包括按键信号采集、按键去抖、消息处理、功能逻辑处理和语音命令处理等环节。通过对代码的分析,发现了可能导致按键功能混淆的潜在问题,主要是由于按键状态检测逻辑中存在重复检测和状态混淆的情况。