我们会从一个真实的鼠标设计案例开始,来看看一个真实的光电鼠标是什么样子的,在文章的最后我会将鼠标项目打包上传到资源部分。
这个方案主要使用了adns2620芯片,这个芯片是光电传感器的集成芯片,方案中也有PCB图以及对应的程序代码,PCB结构之类的和本系列无关,就不去一一赘述了,我们主要看代码部分。
fireware代码如下:
/* 自动捕获鼠标坐标 */
static void getMouseMovement(void)
{
reportBuffer.dx = -read(DELTA_X_REG);
reportBuffer.dy = read(DELTA_Y_REG);
}
/* ------------------------------------------------------------------------- */
usbMsgLen_t usbFunctionSetup(uchar data[8])
{
usbRequest_t *rq = (void *)data;
// 以下请求从未使用过。但由于它们是规范,我们在这个例子中实现了它们。
if((rq->bmRequestType & USBRQ_TYPE_MASK) == USBRQ_TYPE_CLASS)
{
// 类请求类型
DBG1(0x50, &rq->bRequest, 1);
// 调试输出: 打印请求
if(rq->bRequest == USBRQ_HID_GET_REPORT)
{
// wValue:ReportType(高字节),ReportID(低字节)
usbMsgPtr = (void *)&reportBuffer;
return sizeof(reportBuffer);
}
else if(rq->bRequest == USBRQ_HID_GET_IDLE)
{
usbMsgPtr = &idleRate;
return 1;
}
else if(rq->bRequest == USBRQ_HID_SET_IDLE)
{
idleRate = rq->wValue.bytes[1];
}
}
else
{
// 未实现特定于供应商的请求 未实现请求,就不向主机返回任何数据
}
return 0;
}
/* ------------------------------------------------------------------------- */
/* ------------------- ADNS2610 Functions ---------------------------------- */
//Function Description: 从ADNS2610读取指定地址的字节
//Inputs: ADNS2610要写入的寄存器地址
//Outputs: None
//Return: 从ADNS2610读取的字节
//usage: value = read(CONFIGURATION_REG);
char read(char address)
{
char value=0;
sbi(ADNS_DIR, ADNS_SDIO); // 确保SDIO引脚设置为输出。
sbi(ADNS_PORT, ADNS_SCL); // 确保时钟引脚拉高。
address &= 0x7F; // 请确保地址字节的最高位为“0”以指示读取。
int address_bit=0;
// 发送地址到 ADNS2610
for(address_bit=7; address_bit >=0; address_bit--)
{
cbi(ADNS_PORT, ADNS_SCL); // 拉低时钟引脚
sbi(ADNS_DIR, ADNS_SDIO); // 确保SDIO引脚设置为输出
// 如果当前位为1,则设置SDIO引脚。否则,清除SDIO引脚
if(address & (1<<address_bit))
{
sbi(ADNS_PORT, ADNS_SDIO);
}
else
{
cbi(ADNS_PORT, ADNS_SDIO);
}
_delay_us(1);
sbi(ADNS_PORT, ADNS_SCL);
_delay_us(1);
}
// 允许ADNS2610有额外的时间转换SDIO引脚(根据数据表)
_delay_us(120);
// 将SDIO作为微控制器上的输入
cbi(ADNS_DIR, ADNS_SDIO); // 确保将SDIO引脚设置为输入。
sbi(ADNS_PORT, ADNS_SDIO); // SDIO引脚的上拉。
int value_bit=0;
// 将值按字节发送到ADNS2610
for(value_bit=7; value_bit >= 0; value_bit--)
{
cbi(ADNS_PORT, ADNS_SCL); // 拉低 clock 引脚
_delay_us(1); // 允许ADNS2610配置SDIO引脚
sbi(ADNS_PORT, ADNS_SCL); // 上拉 clock 引脚
_delay_us(1);
// 如果SDIO引脚为高电平,则在value变量中设置当前位。如果为低,则保留默认值位(0)。
//if(sdio.read())value|=(1<<value_bit);
if((ADNS_PIN & (1<<ADNS_SDIO)) == (1<<ADNS_SDIO))value|=(1<<value_bit);
}
return value;
}
// Function Description: 将值写入ADNS2619上的指定地址
// Inputs: 字符地址-ADNS2610要写入的寄存器地址
// Outputs: None
// Return: None
// usage: write(CONFIGURATION_REGISTER, config_setting);
void write(char address, char value)
{
sbi(ADNS_DIR, ADNS_SDIO); // 确保 SDIO 引脚设置为输出。
sbi(ADNS_PORT, ADNS_SCL); // 确认 clock 引脚为高.
address |= 0x80; // 请确保地址字节的最高位为“1”以指示写入.
int address_bit=0;
//Send the Address to the ADNS2610
for(address_bit=7; address_bit >=0; address_bit--)
{
cbi(ADNS_PORT, ADNS_SCL); // 拉低 clock 引脚
// 提供一个小延迟(仅用于第一次迭代,以确保ADNS2610放弃
_delay_us(1);
// 如果我们在“读取”命令之后执行此写入,则控制SDIO。
// 如果当前位为1,则设置SDIO引脚。如果没有,清除SDIO引脚
if(address & (1<<address_bit))sbi(ADNS_PORT, ADNS_SDIO);
else cbi(ADNS_PORT, ADNS_SDIO);
_delay_us(1);
sbi(ADNS_PORT, ADNS_SCL);
_delay_us(1);
}
int value_bit=0;
//Send the Value byte to the ADNS2610
for(value_bit=7; value_bit >= 0; value_bit--)
{
cbi(ADNS_PORT, ADNS_SCL); // 拉低 clock 引脚
// 如果当前位为1,则设置SDIO引脚。否则,清除SDIO引脚
if(value & (1<<value_bit))
sbi(ADNS_PORT, ADNS_SDIO);
else
cbi(ADNS_PORT, ADNS_SDIO);
_delay_us(1);
sbi(ADNS_PORT, ADNS_SCL);
_delay_us(1);
}
}
void sync(void)
{
sbi(ADNS_PORT, ADNS_SCL);
_delay_ms(1);
cbi(ADNS_PORT, ADNS_SCL);
_delay_ms(1);
sbi(ADNS_PORT, ADNS_SCL);
_delay_ms(100);
}
//Function Description: 初始化ADNS2610的I/O。假设已经定义了sck和sdio的实例.
//Inputs: None
//Outputs: None
//Return: None.
//usage: setup();
void setupAdns(void)
{
sbi(ADNS_DIR, ADNS_SCL); // 将时钟设置为输出
sbi(ADNS_DIR, ADNS_SDIO); // 将SDIO设置为输出
sbi(ADNS_PORT, ADNS_SCL); // 设置 Clock 引脚为高
sbi(ADNS_PORT, ADNS_SDIO); // 设置 SDIO 引脚为高
}
/* ------------------------------------------------------------------------------------------- */
int __attribute__((noreturn)) main(void)
{
uchar i;
wdt_enable(WDTO_1S);
// 即使不用看门狗,也要把它关掉。在较新的设备上,
// 看门狗的状态(开/关,周期)!
// RESET状态:所有端口位均为无上拉输入。
// 这就是我们需要D+和D-的方式。所以我们不需要
// 额外的硬件初始化。
odDebugInit();
DBG1(0x00, 0, 0); /* debug output: main starts */
setupAdns();
_delay_ms(100);
sync();
usbInit();
usbDeviceDisconnect(); /* 强制重新枚举,在禁用中断时执行此操作! */
i = 0;
while(--i)
{
/* USB断开连接 > 250 ms */
wdt_reset();
_delay_ms(1);
}
usbDeviceConnect();
sei();
DBG1(0x01, 0, 0); /* debug output: main loop starts */
for(;;){ /* 主循环 */
DBG1(0x02, 0, 0); /* debug output: main loop iterates */
wdt_reset();
usbPoll();
if(usbInterruptIsReady())
{
// 在每次轮询中断端点后调用
//advanceCircleByFixedAngle();
getMouseMovement();
DBG1(0x03, 0, 0); /* debug output: interrupt report prepared */
usbSetInterrupt((void *)&reportBuffer, sizeof(reportBuffer));
}
}
}
/* ------------------------------------------------------------------------- */
这部分主要是对一些芯片上的引脚的操作。
可以看一下Auduino库:
// 从ADNS2620传感器读取寄存器。将结果返回给调用函数。
// Example: value = mouse.read(CONFIGURATION_REG);
char ADNS2620::read(char address)
{
char value=0;
pinMode(_sda, OUTPUT); // 确保SDIO引脚设置为输出。
digitalWrite(_scl, HIGH); // 确保时钟引脚为高。
address &= 0x7F; // 请确保地址字节的最高位为“0”以指示读取。
//Send the Address to the ADNS2620
for(int address_bit=7; address_bit >=0; address_bit--)
{
digitalWrite(_scl, LOW);
pinMode(_sda, OUTPUT);
if(address & (1<<address_bit))
{
digitalWrite(_sda, HIGH);
}
else
{
digitalWrite(_sda, LOW);
}
delayMicroseconds(10);
digitalWrite(_scl, HIGH);
delayMicroseconds(10);
}
delayMicroseconds(120);
pinMode(_sda, INPUT);
digitalWrite(_sda, HIGH);
//Send the Value byte to the ADNS2620
for(int value_bit=7; value_bit >= 0; value_bit--)
{
digitalWrite(_scl, LOW);
delayMicroseconds(10);
digitalWrite(_scl, HIGH);
delayMicroseconds(10);
if(digitalRead(_sda))value |= (1<<value_bit);
}
return value;
}
我们对比FW程序和Auduino库,会发现几乎一致,实际上就是这么修改的,在读写的时候,会根据引脚的状态来设置value。
不过我估计很多人看了这个还是一头雾水,所以下一个文档会从最上层的开发方式来再做一个鼠标键盘输入器,读者可以体会两者之间的差距。