本帖最后由 sjtitr 于 2014-2-20 00:51 编辑
依然,首先回顾构想贴:
晒设计方案+马里奥大叔--听我指挥
本辑内容,核心内容是讲述如何使用鸡腿,以及少量的I2C的使用。到此为止,算是实现了NES模拟器的基本目标。不能玩的游戏机那还算不上游戏机。能控制了,才有点游戏机的模样。
但是截至本辑结束,这个模拟器也没有发出声音……事实上,声音一部分,已经在上一辑里通过吊丝听音乐的方式大概讲解了,模拟器这部分就不再讨论有关声音的问题啦。
之所以如此热衷于马里奥大叔系列,不仅因为作为一个忠诚的吊丝,要随时把技术和玩捏在一起,更是因为,大叔他肩负着我童年的美好回忆。
废话少说,节目开始。
我们接着NES模拟器继续,有了前面的基础,模拟器已经可以正常运转,只是还没有接受用户输入信息。
这里,我们使用wii的左手手柄来作为输入设备,我们通常称呼这个家伙为鸡腿。
鸡腿连接我们的板子,需要接四根线,两根电源,两根I2C通信线。
在F4上,恰好触摸屏也是I2C通信的,所以我们可以借用这部分内容。在示例工程中,我们选择Touch_Panel工程进行初步的调试。
I2C,这个就不多说了,尤其是STM32的I2C,更是坑人不浅呢。不过在这里我仍然坚持使用硬件I2C。
参考关于触摸屏接口的I2C,我们来实现鸡腿的I2C通信。
首先要了解如何利用鸡腿来工作。
到网络上找资料,于是找到如下的内容,请重点观赏后者
http://wiibrew.org/wiki/Wiimote/Extension_Controllers
http://wiibrew.org/wiki/Wiimote/Extension_Controllers/Nunchuck
为了懒得啃E文的童鞋,我来总结一下吧。
1.鸡腿的8位Slave地址(包括读写标志位),是0xA4。
2.鸡腿需要初始化,方法是先向0xF0写0x55,然后再向0xFB写0x00。
3.读取鸡腿的信息,是从0x00处读取6个字节,这6个字节,我们现在只需要前两个和最后一个。前两个分别表示x轴和y轴,最后一个的低两位表示鸡腿上的两个按键状态。
然后把鸡腿连在板子上,接线规则是,-+dc四根线分别接在GND、3V、PC9、PA8。
于是可以开始写代码啦。
初始化:
- static void delay(volatile uint32_t nCount)
- {
- volatile uint32_t index = 0;
- for(index = nCount; index != 0; index--)
- {
- }
- }
- uint8_t IOE_Chuck_Set(uint8_t reg, uint8_t val)
- {
- I2C_GenerateSTART(IOE_I2C, ENABLE);
- /* Test on EV5 and clear it */
- IOE_TimeOut = TIMEOUT_MAX;
- while (!I2C_GetFlagStatus(IOE_I2C, I2C_FLAG_SB))
- {
- if (IOE_TimeOut-- == 0) return(IOE_FAILURE);
- }
- /* Transmit the slave address and enable writing operation */
- I2C_Send7bitAddress(IOE_I2C, NUNCHUCK_ADDR, I2C_Direction_Transmitter);
-
- /* Test on EV6 and clear it */
- IOE_TimeOut = TIMEOUT_MAX;
- while (!I2C_GetFlagStatus(IOE_I2C, I2C_FLAG_ADDR))
- {
- if (IOE_TimeOut-- == 0) return(IOE_FAILURE);
- }
-
- /* Read status register 2 to clear ADDR flag */
- IOE_I2C->SR2;
-
- /* Test on EV8_1 and clear it */
- IOE_TimeOut = TIMEOUT_MAX;
- while (!I2C_GetFlagStatus(IOE_I2C, I2C_FLAG_TXE))
- {
- if (IOE_TimeOut-- == 0) return(IOE_FAILURE);
- }
-
- /* Transmit the first address for r/w operations */
- I2C_SendData(IOE_I2C, reg);
-
- /* Test on EV8 and clear it */
- IOE_TimeOut = TIMEOUT_MAX;
- while (!I2C_GetFlagStatus(IOE_I2C, I2C_FLAG_TXE))
- {
- if (IOE_TimeOut-- == 0) return(IOE_FAILURE);
- }
-
- /* Prepare the register value to be sent */
- I2C_SendData(IOE_I2C, val);
-
- /* Test on EV8_2 and clear it */
- IOE_TimeOut = TIMEOUT_MAX;
- while ((!I2C_GetFlagStatus(IOE_I2C, I2C_FLAG_TXE)) || (!I2C_GetFlagStatus(IOE_I2C, I2C_FLAG_BTF)))
- {
- if (IOE_TimeOut-- == 0) return(IOE_FAILURE);
- }
-
- /* End the configuration sequence */
- I2C_GenerateSTOP(IOE_I2C, ENABLE);
-
- /* All configuration done */
- return IOE_OK;
- }
-
- void IOE_Chuck_Init(void)
- {
- delay(50000);
- ioe_err = IOE_Chuck_Set(0xf0, 0x55);
- delay(50000);
- ioe_err = IOE_Chuck_Set(0xfb, 0x00);
- delay(50000);
- }
复制代码
以上代码,我自己写的比较少,基本都是找示例扒过来的。注意到里面有许多的delay,这个东西不是鸡腿控制说明里面提的,而是我根据实验,发现我手里的破鸡腿,需要“慢”一点来访问,才能正常工作,最后加上的延时操作,否则,可能初始化就会不成功,进而从鸡腿得到的数据都是0xFF。事实上,我还有另外一个鸡腿,不需要这样那样的延时,也可以工作得很好,所以我觉得是破鸡腿的问题,但是作为分享,我把这些延时也拿出来,提醒各位控制鸡腿的时候可能需要“慢”一点哦。
接下来就是读取鸡腿数据啦:
- uint8_t IOE_Chuck_Seek(uint8_t reg)
- {
- I2C_GenerateSTART(IOE_I2C, ENABLE);
- /* Test on EV5 and clear it */
- IOE_TimeOut = TIMEOUT_MAX;
- while (!I2C_GetFlagStatus(IOE_I2C, I2C_FLAG_SB))
- {
- if (IOE_TimeOut-- == 0) return(IOE_FAILURE);
- }
- /* Transmit the slave address and enable writing operation */
- I2C_Send7bitAddress(IOE_I2C, NUNCHUCK_ADDR, I2C_Direction_Transmitter);
-
- /* Test on EV6 and clear it */
- IOE_TimeOut = TIMEOUT_MAX;
- while (!I2C_GetFlagStatus(IOE_I2C, I2C_FLAG_ADDR))
- {
- if (IOE_TimeOut-- == 0) return(IOE_FAILURE);
- }
-
- /* Read status register 2 to clear ADDR flag */
- IOE_I2C->SR2;
-
- /* Test on EV8_1 and clear it */
- IOE_TimeOut = TIMEOUT_MAX;
- while (!I2C_GetFlagStatus(IOE_I2C, I2C_FLAG_TXE))
- {
- if (IOE_TimeOut-- == 0) return(IOE_FAILURE);
- }
-
- /* Transmit the first address for r/w operations */
- I2C_SendData(IOE_I2C, reg);
-
- /* Test on EV8 and clear it */
- IOE_TimeOut = TIMEOUT_MAX;
- while (!I2C_GetFlagStatus(IOE_I2C, I2C_FLAG_TXE))
- {
- if (IOE_TimeOut-- == 0) return(IOE_FAILURE);
- }
-
- /* End the configuration sequence */
- I2C_GenerateSTOP(IOE_I2C, ENABLE);
-
- /* All configuration done */
- return IOE_OK;
- }
- uint8_t IOE_Chuck_Read(uint8_t *buf)
- {
- /* Send START condition a second time */
- I2C_GenerateSTART(IOE_I2C, ENABLE);
-
- /* Test on EV5 and clear it */
- IOE_TimeOut = TIMEOUT_MAX;
- while (!I2C_GetFlagStatus(IOE_I2C, I2C_FLAG_SB))
- {
- if (IOE_TimeOut-- == 0) return(IOE_FAILURE);
- }
-
- /* Send IO Expander address for read */
- I2C_Send7bitAddress(IOE_I2C, NUNCHUCK_ADDR, I2C_Direction_Receiver);
-
- /* Test on EV6 and clear it */
- IOE_TimeOut = TIMEOUT_MAX;
- while (!I2C_GetFlagStatus(IOE_I2C, I2C_FLAG_ADDR))
- {
- if (IOE_TimeOut-- == 0) return(IOE_FAILURE);
- }
- /* Disable Acknowledgement and set Pos bit */
- I2C_AcknowledgeConfig(IOE_I2C, DISABLE);
- I2C_NACKPositionConfig(IOE_I2C, I2C_NACKPosition_Next);
-
- /* Read status register 2 to clear ADDR flag */
- IOE_I2C->SR2;
- /* Test on EV7 and clear it */
- IOE_TimeOut = TIMEOUT_MAX;
- while (!I2C_GetFlagStatus(IOE_I2C, I2C_FLAG_BTF))
- {
- if (IOE_TimeOut-- == 0) return(IOE_FAILURE);
- }
-
- /* Send STOP Condition */
- I2C_GenerateSTOP(IOE_I2C, ENABLE);
-
- /* Read the first byte from the IO Expander */
- buf[0] = I2C_ReceiveData(IOE_I2C);
-
- /* Read the second byte from the IO Expander */
- buf[1] = I2C_ReceiveData(IOE_I2C);
-
- /* Enable Acknowledgement and reset POS bit to be ready for another reception */
- I2C_AcknowledgeConfig(IOE_I2C, ENABLE);
- I2C_NACKPositionConfig(IOE_I2C, I2C_NACKPosition_Current);
-
- /* All read done */
- return IOE_OK;
- }
复制代码- ……
- ioe_err = IOE_Chuck_Seek(0);
- delay(50000);
- ioe_err = IOE_Chuck_Read(chuck_buf);
- delay(1000);
- ioe_err = IOE_Chuck_Read(&chuck_buf[2]);
- delay(1000);
- ioe_err = IOE_Chuck_Read(&chuck_buf[4]);
- delay(1000);
复制代码
有童鞋可能会问,不是说从0读取6个字节么……是啊,可是,实践是检验真理的唯一标准。我发现结合STM32的I2C,还有我的破鸡腿,直接连续读6个字节真的是噩梦。恰示例读取触摸屏是每次2字节,于是我也就模仿着,然后自己每次去读两个字节,定好地址,连续读3次,反正都差不多嘛,最后我的代码也奏效了,哦呵呵
补充一点,如果你读到鸡腿的返回值都是0xFF,说明鸡腿初始化不正确哦,就需要在初始化部分做文章了,切记。
以上代码,在触摸屏的工程上,测试通过了,然后移到NES工程里。
为了能“慢”一点访问鸡腿,我不得不在每一帧的4条不同扫描线里去做读鸡腿的动作,先定好地址,之后3次每次都两个字节……搞出这样的处理,郁闷啊。
所幸的是,InfoNES已经预留了这样的位置,InfoNES_Wait函数,就是为了在高速环境里拖慢模拟器运行速度到真实速度,在每个扫描线都会调用一次,如果你想降低模拟器运行速度,就再这里写一些耽误时间的操作,例如Sleep。我们在这里,判断当前扫描线,在适当的时机进行动作。
- /* Wait */
- void InfoNES_Wait()
- {
- switch(PPU_Scanline)
- {
- // case 9:
- // ioe_err = IOE_Chuck_Seek(0);
- // delay(50000);
- // ioe_err = IOE_Chuck_Read(chuck_buf);
- // delay(1000);
- // ioe_err = IOE_Chuck_Read(&chuck_buf[2]);
- // delay(1000);
- // ioe_err = IOE_Chuck_Read(&chuck_buf[4]);
- // delay(1000);
- // break;
- case 10:
- ioe_err = IOE_Chuck_Seek(0);
- break;
- case 240:
- ioe_err = IOE_Chuck_Read(chuck_buf);
- break;
- case 245:
- ioe_err = IOE_Chuck_Read(&chuck_buf[2]);
- break;
- case 250:
- ioe_err = IOE_Chuck_Read(&chuck_buf[4]);
- break;
- default:
- break;
- }
- }
复制代码
最后,在定期调用的InfoNES_PadState函数中,分析chuck_buf的内容,并填充pad键状态。
- /* Get a joypad state */
- void InfoNES_PadState( DWORD *pdwPad1, DWORD *pdwPad2, DWORD *pdwSystem )
- {
- {
- *pdwPad1 = 0;
- if(chuck_buf[0]<0x60)
- {
- *pdwPad1 |= PAD_JOY_LEFT;
- }
- else if(chuck_buf[0]>0x90)
- {
- *pdwPad1 |= PAD_JOY_RIGHT;
- }
- else
- {
- }
- if(chuck_buf[1]<0x60)
- {
- *pdwPad1 |= PAD_JOY_DOWN;
- }
- else if(chuck_buf[1]>0x90)
- {
- *pdwPad1 |= PAD_JOY_UP;
- }
- else
- {
- }
-
- if(chuck_buf[5]&0x01)
- {
- }
- else
- {
- *pdwPad1 |= PAD_JOY_A;
- }
- if(chuck_buf[5]&0x02)
- {
- }
- else
- {
- *pdwPad1 |= PAD_JOY_B;
- }
- if(STM_EVAL_PBGetState(BUTTON_USER) == Bit_RESET)
- {
- }
- else
- {
- *pdwPad1 |= PAD_JOY_START;
- }
- }
- }
复制代码
需要注意到一点,鸡腿上只有两个按键,要想玩游戏,除了方向需要3个按键,所以用板子上的User按键顶个包。
当然,我把I2C和鸡腿的初始化放在读取ROM的后面了。
download!运行!
翻滚吧,大叔。
其实,稍有风吹草动,I2C就挂了,然后大叔就不受控制了。I2C的稳定性,暂时不在讨论范围内,追求稳定的童鞋,去用软件模拟吧。
仍然,最后把可以运行的工程,分享给大家。不过也隐隐的觉得,有可能直接烧写我的原版,也不能100%驱动你的鸡腿……桑心!
Nes.zip
(2.36 MB, 下载次数: 34)
(7z压缩,体积更小)
本辑完。