项目介绍
本项目我通过使用硬禾学堂提供的RP2040 Game Kit完成基于RP2040的电子沙漏任务,该项目作品通过部分工具完成了两个LED 灯板和RP2040 Game Kit的固定,以c/c++编程的方式,实现在RP2040 Game Kit上的按键和屏幕完成电子沙漏的一个周期的计时,在点下开始按钮后,通过上边的姿态传感器来感知沙漏方向的变化,并开始沙漏操作。该项目使用到了两块8*8 LED灯板,一根数据线,多根杜邦线,以及一个RP2040 Game Kit扳机。
软件实现流程图
主要硬件介绍
本次所使用的的设备rp2040游戏机是基于树莓派RP2040的嵌入式系统学习平台,通过USB Type-C连接器供电
RP2040作为主控,具有双核Arm Cortex M0+内核和264KB内存,可通过MicroPython或C/C++编程,性能强大
机身集成了丰富的硬件资源,如板上四向摇杆 + 2个轻触按键 + 一个三轴姿态传感器MMA7660,一块240*240像素的lcd屏幕
两块LED灯板
主要软件介绍
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=0x02;
Sensor_Write_Byte(SensorAddr,0x08,®);
reg=0x01;
Sensor_Write_Byte(SensorAddr,0x07,®);
}
这个函数则是加速度传感器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的使用,故对本次的活动项目产生了极为浓厚的兴趣。但在编程的过程中,也意识到了自己的不足。首先是在电子方向上仍然存在着很多还未了解的专业知识,其次是代码的编写略显累赘,期望在下次的活动中有所改善。