1.项目介绍
本文展示的是一个基于MSP430的鼠标以及键盘模拟器。
MSP430通过测量摇杆发送出来的PWM信号的脉宽和频率依赖来获取摇杆的上下左右情况,MSP430依据这个结果在屏幕上对鼠标进行相应的偏移以此来获取模拟鼠标移动光标的效果。
MSP430通过读取2个独立按键发送出来的电压值,依据不同的采样电压来判断出不同的按键被按下,当S1被按下时,MSP430向PC发送鼠标左键被按下的消息;当S1抬起时,MSP430向PC发送鼠标左键被释放消息。当S2被按下时,MSP430向PC发送键盘按下消息,按下按键为:eetree.cn
2.硬件链接
由于本次所有的连接都是通过配套的转接板进行展示,不需要额外连线,因此此处展示的是硬件链接示意图。
MSP430链接摇杆电路以及独立按键电路,以此读取摇杆以及按键状态。通过USB线缆链接PC机,枚举成HID设备,用于模拟鼠标以及键盘。
如下所示为独立按键以及旋钮对应的硬件电路,此处使用电阻分压网络构成,最终输出的是一个电压值,不同的按键被按下,输出的电压值也各不相同,MSP430通过读取输出管脚的电压值以此来判断那个按键被按下,设计很精妙,只是看不太懂.....
如下为摇杆部分的电路图,正常情况下,摇杆应该需要四个AD管脚进行采样以此来获取到对应的摇杆状态,但是这里使用运放一通操作,然后输出只需要一个管脚来展示摇杆的状态,通过输出PWM的占空比、频率来进行区分。只能说溜溜溜,奈何看不懂咋实现的....
3.代码说明
3.1.鼠标相关定义
如下代码为鼠标消息发送的相关结构体定义,button表示鼠标按键状态,bit0为1时表示左键被按下,为0表示左键被松开;x、y表示光标偏移坐标,通过调整这个偏移坐标就可以使用鼠标的移动。
typedef struct {
uint8_t buttons; /* bit1 */
int8_t dX;
int8_t dY;
int8_t dZ;
} MOUSE_REPORT;
MOUSE_REPORT mouseReport = { 1, 10, 10, 0 }; // HID report, to be sent to the PC
3.2.USB初始化
如下代码进行USB初始化,并通过配置数组来枚举不同的设备,此处代码来自与官方键盘例程,直接照抄即可。
PMM_setVCore(PMM_CORE_LEVEL_2);
USBHAL_initPorts(); // Config GPIOS for low-power (output low)
USBHAL_initClocks(8000000); // Config clocks. MCLK=SMCLK=FLL=8MHz; ACLK=REFO=32kHz
USBHAL_initButtons(); // Init the two buttons
Keyboard_init(); // Init keyboard report
USB_setup(TRUE, TRUE); // Init USB & events; if a host is present, connect
3.3.按键相关
如下为按键相关代码,整体就是一个key_init函数用于进行按键需要的相关外设的初始化。这里主要初始化两个部分:
- ADC按键:使用ADC12_A的通道0进行初始化
- 定时器:创建一个100us为一个周期的定时器,用于记录系统启动时间。
对于按键状态获取操作在key_init中,分为两个部分
- 通过读取PWM输入管脚的电平变化,并通过定时器的计数值来记录对应的时间戳,最后通过时间戳计算出PWM的频率以及占空比,并通过占空比和频率来区分摇杆在X、Y方向上的偏移量。
- 通过读取ADC管脚的电压,来判断当前那些按键被按下。
按键的存储使用的是一个8bit的无符号整形数,每一位都代表对应按键的状态,为1表示按键被按下,为0表示按键被抬起。
enum {
KEY_NONE = 0,
KEY_S1 = 0x1 << 0,
KEY_S2 = 0x1 << 1,
KEY_X1 = 0x1 << 2,
KEY_X2 = 0x1 << 3,
KEY_Y1 = 0x1 << 4,
KEY_Y2 = 0x1 << 5
};
/* ض */
typedef struct {
uint8_t buttons; /* bit1 */
int8_t dX;
int8_t dY;
int8_t dZ;
} MOUSE_REPORT;
MOUSE_REPORT mouseReport = { 1, 10, 10, 0 }; // HID report, to be sent to the PC
/*********** Application specific globals **********************/
volatile uint8_t button1Pressed = FALSE;
volatile uint8_t button2Pressed = FALSE;
volatile uint8_t keySendComplete = TRUE;
uint8_t button1Buf[128] = "eetree.cn";
uint8_t button1StringLength;
/* ADC ֵ */
static volatile uint16_t key_value = 0;
/* ϵͳ ʱ 䣬 λ us */
static volatile uint64_t uptime_us = 0;
static void key_init(void)
{
/* Լ λ ʼ */
//P6.0 ADC option select
GPIO_setAsPeripheralModuleFunctionInputPin(
GPIO_PORT_P6,
GPIO_PIN0
);
GPIO_setAsOutputPin(
GPIO_PORT_P1,
GPIO_PIN0
);
//Initialize the ADC12_A Module
/*
* Base address of ADC12_A Module
* Use internal ADC12_A bit as sample/hold signal to start conversion
* USE MODOSC 5MHZ Digital Oscillator as clock source
* Use default clock divider of 1
*/
ADC12_A_init(ADC12_A_BASE,
ADC12_A_SAMPLEHOLDSOURCE_SC,
ADC12_A_CLOCKSOURCE_ADC12OSC,
ADC12_A_CLOCKDIVIDER_1);
ADC12_A_enable(ADC12_A_BASE);
/*
* Base address of ADC12_A Module
* For memory buffers 0-7 sample/hold for 64 clock cycles
* For memory buffers 8-15 sample/hold for 4 clock cycles (default)
* Disable Multiple Sampling
*/
ADC12_A_setupSamplingTimer(ADC12_A_BASE,
ADC12_A_CYCLEHOLD_64_CYCLES,
ADC12_A_CYCLEHOLD_4_CYCLES,
ADC12_A_MULTIPLESAMPLESDISABLE);
//Configure Memory Buffer
/*
* Base address of the ADC12_A Module
* Configure memory buffer 0
* Map input A0 to memory buffer 0
* Vref+ = AVcc
* Vr- = AVss
* Memory buffer 0 is not the end of a sequence
*/
ADC12_A_configureMemoryParam param = {0};
param.memoryBufferControlIndex = ADC12_A_MEMORY_0;
param.inputSourceSelect = ADC12_A_INPUT_A0;
param.positiveRefVoltageSourceSelect = ADC12_A_VREFPOS_AVCC;
param.negativeRefVoltageSourceSelect = ADC12_A_VREFNEG_AVSS;
param.endOfSequence = ADC12_A_NOTENDOFSEQUENCE;
ADC12_A_configureMemory(ADC12_A_BASE ,¶m);
//Enable memory buffer 0 interrupt
ADC12_A_clearInterrupt(ADC12_A_BASE,
ADC12IFG0);
ADC12_A_enableInterrupt(ADC12_A_BASE,
ADC12IE0);
/* ҡ ˳ ʼ */
/* ʹ CCR0 ж */
TA0CCTL0 = CCIE;
/* Ŀ ֵ, 100us */
TA0CCR0 = 100;
/* ѡ SMCLK ģʽ TAR */
TA0CTL = TASSEL__SMCLK + MC_1 + TACLR;
/* P1.2 */
GPIO_setAsInputPinWithPullDownResistor(
GPIO_PORT_P1,
GPIO_PIN2
);
return;
}
uint16_t freq;
uint16_t duty;
static uint8_t key_get(void)
{
uint64_t time_begin;
uint64_t time_end;
uint64_t time_turn;
uint8_t key;
/* ״ֵ̬ ʼ */
key = KEY_NONE;
/* ȴ ڿ ʼ */
if (GPIO_getInputPinValue(GPIO_PORT_P1, GPIO_PIN2) == GPIO_INPUT_PIN_HIGH)
{
/* ȴ ߵ ƽ */
while (GPIO_getInputPinValue(GPIO_PORT_P1, GPIO_PIN2) == GPIO_INPUT_PIN_HIGH);
/* ȴ ͵ ƽ */
while (GPIO_getInputPinValue(GPIO_PORT_P1, GPIO_PIN2) == GPIO_INPUT_PIN_LOW);
}
else
{
/* ȴ ͵ ƽ */
while (GPIO_getInputPinValue(GPIO_PORT_P1, GPIO_PIN2) == GPIO_INPUT_PIN_LOW);
}
time_begin = uptime_us;
/* ȴ ߵ ƽ */
while (GPIO_getInputPinValue(GPIO_PORT_P1, GPIO_PIN2) == GPIO_INPUT_PIN_HIGH);
time_turn = uptime_us;
/* ȴ ͵ ƽ */
while (GPIO_getInputPinValue(GPIO_PORT_P1, GPIO_PIN2) == GPIO_INPUT_PIN_LOW);
time_end = uptime_us;
freq = 1000000.0 / (time_end - time_begin);
duty = 100.0 * (time_turn - time_begin) / (time_end - time_begin);
/*
* freq duty
* ֹ 18 57
* X- 14 57
* X+ 27 58
* Y- 18 81
* Y+ 18 34
*/
if (freq <= 15)
{
key |= KEY_X1;
}
else if (23 <= freq)
{
key |= KEY_X2;
}
if (duty <= 48)
{
key |= KEY_Y2;
}
else if (66 <= duty)
{
key |= KEY_Y1;
}
/* ʼ ADC ֵ */
key_value = 0;
//Enable/Start sampling and conversion
/*
* Base address of ADC12_A Module
* Start the conversion into memory buffer 0
* Use the single-channel, single-conversion mode
*/
ADC12_A_startConversion(ADC12_A_BASE,
ADC12_A_MEMORY_0,
ADC12_A_SINGLECHANNEL);
/* ȴ ת */
while (0 == key_value);
/*
* ̬ 3930
* S1 2922
* S2 1930
* S1+S2 931
*/
if (key_value <= 1000)
{
key |= KEY_S1;
key |= KEY_S2;
}
else if (key_value <= 2000)
{
key |= KEY_S2;
}
else if (key_value <= 3000)
{
key |= KEY_S1;
}
return key;
}
3.4.逻辑实现
串联各个模块的主要逻辑如下所示,当读取到按键值中有任意按键被按下时,进入后续处理。
当S1被按下时,传递给PC的鼠标消息中的button变量被设置为1,表示按键被按下;S1抬起时,传递消息的button变量变为0,表示左键被释放。
当S2被按下时,传递PC键盘按下消息为:eetree.cn。并且该按键会判断上一次按下的值,避免持续按下导致功能被持续触发。
当摇杆X、Y方向的偏移达到阈值之后,表示X1、X2、Y1、Y2被按下,此时会将传递给鼠标消息的中X、Y变量进行赋值用于标识鼠标移动,以此来实现光标跟随摇杆的效果。
while (1)
{
switch(USB_getConnectionState())
{
// This case is executed while your device is enumerated on the
// USB host
case ST_ENUM_ACTIVE:
key_now = key_get();
if (key_now != KEY_NONE)
{
mouseReport.buttons = 0;
mouseReport.dX = 0;
mouseReport.dY = 0;
if (key_now & KEY_S1)
{
if (key_now & KEY_S1)
{
mouseReport.buttons = 1;
}
else
{
mouseReport.buttons = 0;
}
}
if ((key_now & KEY_S2) != (key_old & KEY_S2))
{
if (key_now & KEY_S2)
{
for (i=0; i<strlen((const char *)button1Buf); i++)
{
Keyboard_press(button1Buf[i]);
while(!keySendComplete);
keySendComplete = FALSE;
Keyboard_release(button1Buf[i]);
while(!keySendComplete);
keySendComplete = FALSE;
}
}
}
if (key_now & KEY_X1)
{
mouseReport.dX = -10;
}
if (key_now & KEY_X2)
{
mouseReport.dX = 10;
}
if (key_now & KEY_Y1)
{
mouseReport.dY = 10;
}
if (key_now & KEY_Y2)
{
mouseReport.dY = -10;
}
if (mouseReport.buttons != 0 || mouseReport.dX != 0 || mouseReport.dY != 0)
{
USBHID_sendReport((void *)&mouseReport, HID1_INTFNUM);
}
}
/* ± İ Ϣ */
key_old = key_now;
// Enter LPM0 w/interrupt, until a keypress occurs
//__bis_SR_register(LPM0_bits + GIE);
/************* HID keyboard portion ************************/
if (button1Pressed){
USBHID_sendReport((void *)&mouseReport, HID1_INTFNUM);
button1StringLength = strlen((const char *)button1Buf);
if (button2Pressed) {
Keyboard_press(KEY_LEFT_SHIFT);
while(!keySendComplete);
keySendComplete = FALSE;
}
for (i=0; i<button1StringLength; i++) {
Keyboard_press(button1Buf[i]);
while(!keySendComplete);
keySendComplete = FALSE;
Keyboard_release(button1Buf[i]);
while(!keySendComplete);
keySendComplete = FALSE;
}
Keyboard_release(KEY_LEFT_SHIFT);
while(!keySendComplete);
keySendComplete = FALSE;
button1Pressed = FALSE;
button2Pressed = FALSE;
}
break;
// These cases are executed while your device is disconnected from
// the host (meaning, not enumerated); enumerated but suspended
// by the host, or connected to a powered hub without a USB host
// present.
case ST_PHYS_DISCONNECTED:
case ST_ENUM_SUSPENDED:
case ST_PHYS_CONNECTED_NOENUM_SUSP:
__bis_SR_register(LPM3_bits + GIE);
_NOP();
break;
// The default is executed for the momentary state
// ST_ENUM_IN_PROGRESS. Usually, this state only last a few
// seconds. Be sure not to enter LPM3 in this state; USB
// communication is taking place here, and therefore the mode must
// be LPM0 or active-CPU.
case ST_ENUM_IN_PROGRESS:
default:;
}
} //while(1)
4.功能展示
4.1.鼠标移动
如下面两张图中所示,为移动摇杆,光标在屏幕上进行跟随移动的效果,因为是一个动态过程,图片不太好展示,可以看视频演示更为清晰。
4.2.鼠标左键
如下图所示,为鼠标左键按下+鼠标移动共同作用,展示出来的效果就是记事本中的字符被选中高亮。由于是动态过程,图片不太好展示,清晰展示请看视频演示。
4.3.键盘输入
如下图所示,模拟的式键盘输入,当S2按键被按下时,MSP430会向PC机发送按键被按下消息,按下按键为:eetree.cn,S2每次被按下就会触发一次输入。
5.心得
本次使用的TI推出的具备USB接口的16位单片机,就体验而言功能比较强大,对应的CCS、USB工具使用简单,例程也较为丰富。但是性能有一些若,而且如果想要深入开发,需要去详细了解寄存器的定义,开发速度较慢。
板卡对应的扩展板,设计很巧妙,不论是多个按键通过模拟电压输出,还是摇杆通过PWM输出信号都很厉害,第一次见。只是可惜知识有限,不知道具体是咋实现的,比较可惜。
作为一个资深白嫖,这一次白嫖失败了,还是时间管理有问题,以前的难度都比较简单,能够快速冲一把。这回被FPGA给背刺了,血亏....
下回一定要提前搞、提前搞、提前搞,对着这个FPGA开发板立誓,哼。