前段时间我用一个国产MCU实现了雷蛇键盘的效果,按键支持十键无冲,RGB灯支持单控任意一个灯任意一种颜色,但是这个过程还是比较曲折的,原本以为键盘功能是最难搞的,低功耗处理是最简单的,没想到前面这么顺利,最后才翻车了,所以特意出一期记录一下我踩过的坑。
1 USB远程睡眠唤醒要注意的几个点
1、配置描述符(Configuration Descriptor)要打开远程唤醒(Remote Wakeup)功能。
Configuration Descriptor
Offset | Field | Size | Value | Description | remark |
---|---|---|---|---|---|
0 | bLength | 1 | Number | 以字节为单位的描述符大小 | bLength以字节为单位的描述符大小(0x09) |
1 | bDescriptorType | 1 | Constant | 配置描述符类型 | 一般为CONFIGURATION (0x02) |
2 | wTotalLength | 2 | Number | 配置返回的数据总长度 | 包括该配置返回的所有描述符(配置、接口、端点、和专用的类型或者专用的厂商描述符)的总长度 |
3 | bNumInterfaces | 1 | Number | 配置支持的接口数量 | 最小值为0x01 |
4 | bConfigurationValue | 1 | Number | Get Configuration 和Set Configuration请求的配置值 | 必须为0x01或者更高值。取值为0的Set Configuration请求会使设备进入未配置状态(Not Configured state) |
5 | iConfiguration | 1 | Index | 字符串描述符索引 | 若没有字符串描述符,这个字段的值为0 |
6 | bmAttributes | 1 | Bitmap | 配置特性 | Bit7: USB1.0协议中表示总线供电(Bus Powered),设置bit7=1表示由总线供电(Bus Powered),其他协议该位保留(Reserved),必须设置为1 Bit6: 自供电(Self-powered),bits6=1时,设备自供电(Self-powered) Bit5: 远程唤醒(Remote Wakeup),bit5=1时,设备支持远程唤醒 Bit4…0: 未使用,保留,必须为0 |
7 | bMaxPower | 1 | mA | 设备从总线获取的最大功耗 | 当设备完全运行时,特定配置的USB设备从总线取得的最大功耗 |
bmAttributes属性的Bit5要置1,这样才能打开远程唤醒(Remote Wakeup)功能,另外PC端也要在相应的USB设备上打开“允许此设备唤醒计算机(O)”
。
Configuration Descriptor 部分配置参考示例:
0x09, //bLength(9); 配置描述符
0x02, //bDescriptorType(Configuration);
0x29,0x00, //wTotalLength(41);
0x01, //bNumInterfaces(1);
0x01, //bConfigurationValue(1);
0x00, //iConfiguration(0);
0xA0, //bmAttributes(BUSPower); //支持远程唤醒
0x32, //MaxPower(100mA);
2、USB的时钟频率不能低于48MHz且必须是48的倍数。
时钟频率要设对,否则会导致通讯异常。这个点原本我是知道的,但没想到的是MCU在进入休眠之后自动切换到了内部时钟,而且在唤醒之后没有切换回来,因此导致唤醒之后USB通讯异常。
所以,在上电初始化的时候以及休眠唤醒之后都需要配置好系统时钟。
3、USB Device唤醒PC需要发送唤醒序列。
PC在进入睡眠之后会主动发送SetDeviceFeature
,设备端收到以后进入挂起状态(SUSPend)
并且USB进入低功耗模式,如果设备需要唤醒PC的话则需要发送唤醒序列
,先使用RESUME_INTERNAL
唤醒设备本身,然后进入远程唤醒状态RESUME_START
,远程唤醒的操作就是把USB控制寄存器的第4位置1,然后等待10ms把USB控制寄存器的第4位置为0,最后进入RESUME_OFF状态
,设备的一次远程唤醒请求完成。
注:USB总线由SUSPend状态
切换回CONFIGURED状态
实际上是由Host决定的,Device只能发送唤醒序列,然后等Host返回ClearFeature
之后才能真正的唤醒USB,回到正常的CONFIGURED状态
。
RESUME函数参考示例:
/*******************************************************************************
* @fn Resume
*
* @brief This is the state machine handling resume operations and
* timing sequence. The control is based on the Resume structure
* variables and on the ESOF interrupt calling this subroutine
* without changing machine state.
*
* @param a state machine value (RESUME_STATE)
* RESUME_ESOF doesn't change ResumeS.eState allowing
* decrementing of the ESOF counter in different states.
*
* @return None.
*/
void Resume(RESUME_STATE eResumeSetVal)
{
uint16_t wCNTR;
if (eResumeSetVal != RESUME_ESOF)
{
ResumeS.eState = eResumeSetVal;
}
switch (ResumeS.eState)
{
case RESUME_EXTERNAL:
if (remotewakeupon ==0)
{
Resume_Init();
ResumeS.eState = RESUME_OFF;
}
else
{
ResumeS.eState = RESUME_ON;
}
break;
case RESUME_INTERNAL:
Resume_Init();
ResumeS.eState = RESUME_START;
remotewakeupon = 1;
break;
case RESUME_LATER:
ResumeS.bESOFcnt = 2;
ResumeS.eState = RESUME_WAIT;
break;
case RESUME_WAIT:
ResumeS.bESOFcnt--;
if (ResumeS.bESOFcnt == 0)
ResumeS.eState = RESUME_START;
break;
case RESUME_START:
wCNTR = _GetCNTR();
wCNTR |= CNTR_RESUME;
_SetCNTR(wCNTR);
ResumeS.eState = RESUME_ON;
ResumeS.bESOFcnt = 10;
break;
case RESUME_ON:
ResumeS.bESOFcnt--;
if (ResumeS.bESOFcnt == 0)
{
wCNTR = _GetCNTR();
wCNTR &= (~CNTR_RESUME);
_SetCNTR(wCNTR);
ResumeS.eState = RESUME_OFF;
remotewakeupon = 0;
}
break;
case RESUME_OFF:
case RESUME_ESOF:
default:
ResumeS.eState = RESUME_OFF;
break;
}
}
2 MCU唤醒之后引起USB异常的几个点
我在调试好键盘功能之后就开始着手做MCU的休眠,但是在调试的过程中发现了一些新的问题。
注:我用的MCU是ch32v203,这个MCU是一款国产IC,应该是参考了stm32设计的,无论是硬件还是软件都极其相似,因此,如果改用stm32或者其他stm32的替代方案可能也会有类似的问题。
1、不能在USB中断服务函数里面让MCU进入休眠。
收到PC端传过来的休眠信号之后,会进USB中断,然后进入挂起状态(SUSPend),我测试的时候图方便直接在中断里面让MCU进入了停机模式,结果MCU唤不醒了,可能是因为唤醒之后要从睡眠那行代码继续往后跑,但是因为睡眠是在中断服务函数里面的,唤醒之后进不了这个中断了,也就没法继续往下跑了。
2、睡眠之前要失能窗口看门狗。
这个问题有点莫名其妙,窗口看门狗是挂在APB1时钟上面的,MCU进入休眠的时候会关闭APB1时钟,所以看门狗是不会影响休眠和唤醒的,实际上也是MCU休眠和唤醒的功能也是正常的,休眠之前USB和看门狗也是正常的,但是如果休眠时不先关闭看门狗时钟,唤醒之后就会出现USB通讯异常的情况,我一时间也没搞懂是什么原因,唯一有关联的是USB和看门狗都是挂在APB1下面的,有大神可以解答一下我的疑惑吗?
3、MCU休眠只能选择WFE,选择WFI的话USB无法唤醒MCU。
普通外部中断唤醒(EXTI0-15)不管用WFI还是WFE都是可以正常使用的,USB中断(EXTI18)在MCU休眠之前也是可以正常使用,但是一旦MCU通过WFI进入休眠之后,就无法通过USB中断唤醒了,这个时候哪怕通过其他外部中断唤醒了MCU,USB也还是无法恢复正常通讯。
如果是用WFE则没有这个问题,这就很奇怪了,中断配置我也检查过很多次了,并没有发现什么问题,最后没办法就只能用WFE了。
结束语
关于USB远程睡眠唤醒的坑就讲到这,这里其实只是列举了一部分,因为这只是总结我遇到的新坑,有些以前踩过的坑这里就没写了,我也是第一次做USB的低功耗,没想到会遇到这么多奇怪的问题。虽然最后问题都解决了,但是有些疑惑还是没想明白,有大神知道的话还望不吝赐教!