基于RP2040游戏机的综合应用-电子沙漏实现
本片文章分享我在硬禾学堂的2022年暑假在家一起练活动,基于RP2040游戏机的电子沙漏项目的流程及效果展示
标签
嵌入式
2022年暑假在家一起练
基于RP2040游戏机的综合应用
反正都一样
更新2022-09-09
567

FjDfq9G1D043UfRKYeJqO98et9AFFvO6NLrEB86j8RpMqE6CBOhqfjaV

项目介绍

本项目我通过使用硬禾学堂提供的RP2040 Game Kit完成基于RP2040的电子沙漏任务,该项目作品通过部分工具完成了两个LED 灯板和RP2040 Game Kit的固定,以c/c++编程的方式,实现在RP2040 Game Kit上的按键和屏幕完成电子沙漏的一个周期的计时,在点下开始按钮后,通过上边的姿态传感器来感知沙漏方向的变化,并开始沙漏操作。该项目使用到了两块8*8 LED灯板,一根数据线,多根杜邦线,以及一个RP2040 Game Kit扳机。

软件实现流程图

                                     Fs-InW3OsjD2u-2Gh9PfiF0Hpwcr

主要硬件介绍

本次所使用的的设备rp2040游戏机是基于树莓派RP2040的嵌入式系统学习平台,通过USB Type-C连接器供电

RP2040作为主控,具有双核Arm Cortex M0+内核和264KB内存,可通过MicroPython或C/C++编程,性能强大

机身集成了丰富的硬件资源,如板上四向摇杆 + 2个轻触按键 + 一个三轴姿态传感器MMA7660,一块240*240像素的lcd屏幕

两块LED灯板

FpTLWH480kB5tipJm1BmSB38UoPh

主要软件介绍

rp2040游戏机有两种开发方式

1、Micropython

2、最基本的c/c++开发方式,但相较于前一种,需要自己搭建开发环境,比较麻烦,不适合新手使用

我所使用的就是第二种开发方式,至于具体的开发环境该如何搭建就不做详细介绍了,大家感兴趣,可以自行百度

代码实现

由于没有使用Micropython的方式编程,所以板子上的屏幕,加速度传感器的驱动都需自己编写

void LCD_Init(void)
{
	lcd_gpio_init();
	
	nur_lcd_reset();
	
	LCD_WR_REG(0x11); 
	sleep_ms(120);
	LCD_WR_REG(0x36); 
	if(USE_HORIZONTAL==0)LCD_WR_DATA8(0x00);
	else if(USE_HORIZONTAL==1)LCD_WR_DATA8(0xC0);
	else if(USE_HORIZONTAL==2)LCD_WR_DATA8(0x70);
	else LCD_WR_DATA8(0xA0);

	LCD_WR_REG(0x3A);
	LCD_WR_DATA8(0x05);

	LCD_WR_REG(0xB2);
	LCD_WR_DATA8(0x0C);
	LCD_WR_DATA8(0x0C);
	LCD_WR_DATA8(0x00);
	LCD_WR_DATA8(0x33);
	LCD_WR_DATA8(0x33); 

	LCD_WR_REG(0xB7); 
	LCD_WR_DATA8(0x35);  

	LCD_WR_REG(0xBB);
	LCD_WR_DATA8(0x19);

	LCD_WR_REG(0xC0);
	LCD_WR_DATA8(0x2C);

	LCD_WR_REG(0xC2);
	LCD_WR_DATA8(0x01);

	LCD_WR_REG(0xC3);
	LCD_WR_DATA8(0x12);   

	LCD_WR_REG(0xC4);
	LCD_WR_DATA8(0x20);  

	LCD_WR_REG(0xC6); 
	LCD_WR_DATA8(0x0F);    

	LCD_WR_REG(0xD0); 
	LCD_WR_DATA8(0xA4);
	LCD_WR_DATA8(0xA1);

	LCD_WR_REG(0xE0);
	LCD_WR_DATA8(0xD0);
	LCD_WR_DATA8(0x04);
	LCD_WR_DATA8(0x0D);
	LCD_WR_DATA8(0x11);
	LCD_WR_DATA8(0x13);
	LCD_WR_DATA8(0x2B);
	LCD_WR_DATA8(0x3F);
	LCD_WR_DATA8(0x54);
	LCD_WR_DATA8(0x4C);
	LCD_WR_DATA8(0x18);
	LCD_WR_DATA8(0x0D);
	LCD_WR_DATA8(0x0B);
	LCD_WR_DATA8(0x1F);
	LCD_WR_DATA8(0x23);

	LCD_WR_REG(0xE1);
	LCD_WR_DATA8(0xD0);
	LCD_WR_DATA8(0x04);
	LCD_WR_DATA8(0x0C);
	LCD_WR_DATA8(0x11);
	LCD_WR_DATA8(0x13);
	LCD_WR_DATA8(0x2C);
	LCD_WR_DATA8(0x3F);
	LCD_WR_DATA8(0x44);
	LCD_WR_DATA8(0x51);
	LCD_WR_DATA8(0x2F);
	LCD_WR_DATA8(0x1F);
	LCD_WR_DATA8(0x1F);
	LCD_WR_DATA8(0x20);
	LCD_WR_DATA8(0x23);

	LCD_WR_REG(0x21); 
	LCD_Fill(0,0,LCD_W,LCD_H,BLACK);
	//LCD_ShowChinese(70,		110,"请稍等",	WHITE,	BLACK,32,0);
	// LCD_ShowIntNum(0,100,a,3,WHITE,BLACK,16);
	// LCD_ShowFloatNum(0,50,b,3,WHITE,BLACK,32);
	LCD_WR_REG(0x29);
    //LCD_DrawRectangle(20,20,40,40,YELLOW);
    printf("屏幕初始化\n");
}

这个函数为st7789驱动芯片的屏幕初始化函数,使用的是软件模拟spi的方式进行通信。先将其复位,重置内部寄存器,并设置合适的屏幕电源设置,显示的像素位数,屏幕的旋转,最后打开屏幕显示。

void MMA7660_Init(void){

    uint8_t reg;

    Analog_IIC_Pin_Init();

    reg=0x00;

    Sensor_Write_Byte(SensorAddr,0x07,&reg);

    reg=0x02;

    Sensor_Write_Byte(SensorAddr,0x08,&reg);

    reg=0x01;

    Sensor_Write_Byte(SensorAddr,0x07,&reg);

}

这个函数则是加速度传感器MMA7660的初始化函数,使用的是软件模拟I2C的方式进行通信。先是对加速度传感器的模式进行设置,并设量程范围设置为2g。

    gpio_init(DIN);
    gpio_set_dir(DIN, GPIO_OUT);
    gpio_init(SRCLK);
    gpio_set_dir(SRCLK, GPIO_OUT);
    gpio_init(RCLK);
    gpio_set_dir(RCLK, GPIO_OUT);
    DIN_0;
    SRCLK_0;
    RCLK_0;
    
    LCD_Init();
    MMA7660_Init();
    // sleep_ms(7000);
    gpio_pull_up(5);
    gpio_set_irq_enabled_with_callback(5, GPIO_IRQ_EDGE_FALL, true, &gpio_callback);

    gpio_pull_up(6);
    gpio_set_irq_enabled_with_callback(6, GPIO_IRQ_EDGE_FALL, true, &gpio_callback);

    LCD_ShowString(0,0,	"Time Set:",	YELLOW,	BLACK,	32,0);
    LCD_ShowIntNum(50,100,timing_1,1,RED,BLACK,32);
	LCD_ShowIntNum(150,100,timing_2,1,YELLOW,BLACK,32);
	LCD_ShowString(150,200,	"Start",	YELLOW,	BLACK,	32,0);
	flash();

在main函数中,先是对初始化函数的调用,并将屏幕设置成提示用户进行输入计时时间的样式。同时两个按键进行下降沿中断的中断函数绑定。这两个按键分别设置为计时数值的加操作和目标框的移动工作。

    while (true) {
		unsigned long currentMillis = to_ms_since_boot(get_absolute_time());
		if(hourglass_start==0){
			if(currentMillis>next_getahr_time){
				getAcceleration(&ax, &ay, &az);
				if(ax>0.8){//倒放了
					if(button_lock==1){
						hourglass_start=1;
						sleep_ms(1000);
					}
				}
				next_getahr_time=currentMillis+100;
			}
		}else if(hourglass_start==1){
			fill(timing_1*10+timing_2);
			next_update_time=currentMillis+100;
			next_drop_time=currentMillis+1000;
			hourglass_start=2;
		}else if(hourglass_start==2){
			if(currentMillis>next_update_time){
				update();
				next_update_time=currentMillis+100;
			}
			if(currentMillis>next_drop_time){
				fanzhuan();
				next_drop_time=currentMillis+1000;
			}
			flash();
		}
    }

上面就是电子沙漏的具体实现流程。可以看出我用了一个currentMillis变量记录下了当前程序执行过的毫秒数,并将其作为基准用于在下面的沙漏效果变换。同时前期的控制是用hourglass_start进行判断的,在用户输入希望的计时时间按下确定按钮之前hourglass_start的值为-1,在循环一直执行flash函数。在按下确定按钮后,会每隔100ms查询一次加速度传感器的值getAcceleration(&ax, &ay, &az),并通过判断x轴的加速度值判断是否对沙漏进行了翻转。

若是发生了翻转,则会真正开始电子沙漏的显示效果

电子沙漏的实现难点

uint8_t status[16]={0}程序中声明了这个全局变量,用户记录两个led灯板的每一行的每一个led灯的亮灭情况。

void Hc595OneRow(uint8_t R, uint8_t C)
{
        uint16_t cnt;
        SRCLK_0;
        RCLK_0;
        for(cnt=0;cnt<16;cnt++)
        {
               if (cnt<=7)
               {
                       if(R & 0x80)
                               DIN_1;
                       else
                              DIN_0;
                       R<<=1;
               }else{
                       if(C & 0x80)
                               DIN_1;
                       else
                               DIN_0;
                       C<<=1;
               }
               SRCLK_1;
               SRCLK_0;
        }     
}
void flash(){
        uint8_t row=0x01;
        for(uint8_t i=0;i<8;i++){
               Hc595OneRow(row,status[15-i]);
               Hc595OneRow(row,status[7-i]);
               RCLK_1;
               RCLK_0;       
               row<<=1;
               sleep_ms(2);
        }
}

因为led灯板上使用的是74H595芯片,该芯片并没有锁存功能,及某一瞬间其最多只能控制一行led灯的亮灭情况,所以在程序运行的过程中,除了沙粒每隔100ms变换的瞬间,其余的时刻则是一直在执行上面的flash函数,不断的输入数据给led灯板,使灯板不断的刷新,再根据人类的视觉残留效果,使我们看到led灯板呈现出整个屏幕一起显示的效果。

void fanzhuan(){
        if ((getled(0, 7, 0) && !getled(1, 0, 7)) || (!getled(0, 7, 0) && getled(1, 0, 7))) {
               // for (byte d=0; d<8; d++) { lc.invertXY(0, 0, 7); delay(50); }
               setled(0, 7, 0, 0);
               setled(1, 0, 7, 1);
        }
}

在电子沙漏的显示效果中,某一时刻,只有一个沙粒在移动,那么如何实现在一个沙粒落到底部后,上方的灯板落下第二个沙粒到下方的灯板中呢,就是用上面这个函数实现,setled(0, 7, 0, 0);这函数将上方板子最下面的led灯珠状态转为0,即灭。而setled(1, 0, 7, 1);该函数则是将下方板子的最上面的led灯珠状态转为1,即亮。这样就完成了同一时间里只有一粒沙粒在移动了,至于之间的时间间隔为多少,取决于主循环里的next_drop_time=currentMillis+1000;,这里是每隔1秒执行一次。

void update(){
	uint8_t n = 8;
	uint8_t somethingMoved = 0;
	uint8_t x,y;
	uint8_t direction;
	for (uint8_t slice = 0; slice < 2*n-1; ++slice) {
	  direction = (to_ms_since_boot(get_absolute_time())%2 == 1); // randomize if we scan from left to right or from right to left, so the grain doesn't always fall the same direction
		uint8_t z = slice<n ? 0 : slice-n + 1;
		for (uint8_t j = z; j <= slice-z; ++j) {
      y = direction ? (7-j) : (7-(slice-j));
      x = direction ? (slice-j) : j;
			//printf("x:%d,y:%d\r\n",x,y);
			if(move_led(0, x, y)) {
				somethingMoved = 1;
			};
			if (move_led(1, x, y)) {
				somethingMoved = 1;
			}
		}
	}
}

那么一粒沙粒又是如何呈现出一点点的移动到底部的动画效果的呢,通过调用上面的update函数,在该函数中我们对整个status数组进行了判断,判断的依据为当前led灯珠的下面是否有空位,即是否是没有灯珠亮着,若是没有,代表着当前的灯珠可以往下移动,也就是进行了碰撞检测。注意这里只是对status数组进行了更新,具体执行灯亮灭的还是要靠外面的flash函数执行。那么这个update函数又是多久执行一次呢,在主循环中,我们根据next_update_time变量,在next_update_time=currentMillis+100;操作中得知将会每隔100ms执行一次,也就是说,一粒沙粒会每隔100ms的时间对其当前的位置进行移动,从而产生了移动的动画效果。

主要的收获和遇到的困难

通过本次的实践练习,我对树莓派RP2040和C语言编程的了解更加的深入,编程能力得到一定程度的提升,尤其是对原理图的使用有了更深刻的理解。在平时的学习生活中曾接触过树莓派4B的使用,故对本次的活动项目产生了极为浓厚的兴趣。但在编程的过程中,也意识到了自己的不足。首先是在电子方向上仍然存在着很多还未了解的专业知识,其次是代码的编写略显累赘,期望在下次的活动中有所改善。

 

附件下载
rp2040.zip
用c/c++编程的的,电子沙漏程序
团队介绍
个人
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号