雷竞技注册
项目

如何用Atmel SAM4S xplainpro创建游戏系统

2016年4月1日经过罗伯特·凯

本文提供了与代码开发,硬件配置和OLED接口相关的其他详细信息。

本文提供了与代码开发,硬件配置和OLED接口相关的其他详细信息。

必需的硬件/软件

上一篇文章

a(非常)简单的显示驱动程序

在上一篇文章结束时,我们已成功创建了一个配置和初始化ATSAM4SD32C微控制器的新项目,然后在OLED显示屏上重复显示简单的文本消息。也许这个功能对你来说足够令人兴奋,但我不太满意。因此,让我们继续开发我们的高度原始视频游戏,该游戏包括移动方形目标和激光束,当您按下按钮2时发射。如果激光束击中目标,则会获胜。如果激光束未命中目标,请继续拍摄,直到您获胜。

我们需要的第一件事是我们非常简单的视频游戏的非常简单的显示驱动程序。这将为我们提供一种方便的方法来设置和清除OLED屏幕内的任何位置。此外,我们希望在对应于屏幕的当前状态的微控制器中保持存储器。这允许我们检查像素的状态而不从OLED控制器读取数据;这对我们的特定硬件配置尤其重要,因为我们通过SPI与OLED模块进行了交谈,OLED控制器的SPI接口(与其并行接口不同)不支持读取操作。

页面和列

首先,我们需要了解OLED像素是如何排列的。上的下面两个图SSD1306数据表

所以我们这里有8“页”的垂直像素和128列的水平像素。每页由8个垂直像素组成,因此总分辨率是64个垂直像素乘以128个水平像素。然而,显示器的物理尺寸是32个垂直像素× 128个水平像素;因此,OLED控制器有足够的像素存储8个垂直页面,但一次只能显示4个页面。

我们通过SPI编写字节来修改OLED显示。该字节控制与当前所选页面和列地址对应的8个垂直像素。第二图表指示页面中的像素与字节中最低有效位内的比特之间的关系对应于页面中的最高像素,并且最有效位对应于最低像素。

单个像素vs.所有像素

可以只修改一页中的一列;可以通过设置列和页面地址,然后写入单个数据字节来实现这一点。ASF函数是ssd1306_set_page_address()和ssd1306_set_column_address(),然后是ssd1306_write_data()。不过,我更喜欢修改显示缓冲区中的像素,然后更新整个屏幕。我发现这种方法更加通用和直观。它可能看起来相当低效,但真的不是太糟糕。其中一个原因是OLED的SPI接口支持相当高的时钟速率。下面的范围跟踪显示了SPI时钟信号,它当前被配置为运行在5 MHz (OLED控制器支持的最大频率是10 MHz)。

所以它只需要1.6μs写一个字节。您认为我们可以在大约(1.6μs)×(128列)×(4页)×(4页)中更新整个屏幕≈0.8ms,但它实际上需要长时间,因为在连续字节之间存在大约10μs的死区时间。This dead time is intentionally incorporated into the ssd1306_write_data() function in order to accommodate the SSD1306’s latency, though a comment in the function indicates that the dead time can be as low as 3 µs, so I’m not sure why Atmel chose 10 µs. You can try reducing the delay in ssd1306_write_data() if you want to speed things up. Anyways, with the current delay it should take about (11.6 µs) × (128 columns) × (4 pages) ≈ 5.9 ms to update the entire screen, and the scope confirms that this is indeed the case:

因此,我们可以看到更新整个屏幕所需要的时间并不过多,特别是当你考虑到即使是30帧每秒,我们在屏幕更新之间的间隔也是33毫秒时。

驱动程序代码

我使用以下二维数组作为显示缓冲区:

//预处理器定义#define oled_height_bytes 32/8 #define oled_width_pixels 128 //全局变量无符号char oled_pixels [oled_height_bytes] [oled_width_pixels];

这两个功能在显示缓冲区中修改单个像素:

/ *此函数设置由行和列参数标识的一个OLED像素。请记住,OLED阵列行被寻址为字节(8个像素),而传递给此函数的行参数是像素地址。* /静态内联void set_oled_pixel(未签名的char行,无符号char列){if(行<8){OLED_PIXELS [0] [列] | =(0x01 <<行);}如果(行<16){oled_pixels [1] [列] | =(0x01 <<(Row  -  8));}如果(行<24){oled_pixels [2] [列] | =(0x01 <<(Row  -  16));}如果(行<32){oled_pixels [3] [列] | =(0x01 <<(Row  -  24));} / *此函数清除由行和列参数标识的一个OLED像素。请记住,OLED数组行被寻址为字节(8个像素),而传递给此函数的行参数是像素地址。* /静态内联void clear_oled_pixel(无符号char行,无符号char列){if(行<8){OLED_PIXELS [0] [列]&=〜(0x01 <<行);}如果(行<16){oled_pixels [1] [列]&=〜(0x01 <<(Row  -  8)); } else if (Row < 24) { OLED_Pixels[2][Column] &= ~(0x01 << (Row - 16)); } else if (Row < 32) { OLED_Pixels[3][Column] &= ~(0x01 << (Row - 24)); } }

请注意,我将这些称为“内联”功能,以便它们将更快地执行。

下面是我用来清除显示缓冲区中所有像素的函数:

静态void clear_oled_array(void){无符号char x,y;for(x = 0; x 

请注意,这三个功能实际上不会更新OLED屏幕;它们仅修改缓冲区。通过将显示缓冲区的内容复制到OLED控制器中的内存来更新屏幕:

静态void update_oled_display(void){无符号char行,列;ssd1306_set_page_address(0);ssd1306_set_column_address(0);for(行= 0;行

该函数将显示缓冲区中的所有字节通过SPI写入OLED控制器,按照如下模式:page 0, column 0;第0页,第1列;。;第0页,第127列;第1页,第0列;。;第1页,第127列;第2页,第0列; . . . ; page 2, column 127; page 3, column 0; . . . ; page 3, column 127. Keep in mind that these bytes are written one after another, without any intervening memory-address information. Thus, we need to make sure that the OLED controller interprets this data correctly. To do this, we configure it for “horizontal addressing mode”:

在这种模式下操作时,OLED控制器将自动更新其地址指针,以与我们发送显示缓冲区内容的方式一致。

一个移动的目标

目标是一个4像素-4像素平方,从左到右反复移动,如下:

在向右移动一个像素的目标之间有30毫秒的延迟。该范围捕获显示发送到OLED控制器的数据包;正如我们在上面所看到的,数据包(即用于更新整个屏幕的数据字节)的宽度约为6毫秒,而现在我们在数据包之间有30毫秒的间隔。

以下代码摘录为您提供了整个Main()函数。移动目标功能在循环时处于无限状态。请参阅重要细节的评论。

int main(void){//时钟配置和初始化sysclk_init();/ *禁用看门狗定时器和配置/初始化连接到包含在SAM4S Xplated开发平台中的各种组件的端口引脚,例如NAND闪存,OLED接口,LED,SW0 PushButton。* / Board_init();//初始化SPI和OLED控制器SSD1306_INIT();/ *这两个语句为“水平寻址模式”配置OLED模块。* / ssd1306_write_command(ssd1306_cmd_set_memory_addressing_mode);SSD1306_WRITE_COMMAND(0x00);configure_pushbutton2();/ *此功能清除阵列中的所有像素;它不会更新OLED显示。* / clear_oled_array();update_oled_display();/ *这些变量按住与4像素-4像素方形目标的左上像素相对应的行和列地址。 The row address is set to 14 because we want the target to be in the middle of the display, and the column address is set to 0 so that the target starts from the left-hand edge of the screen.*/ TargetRow = 14; TargetColumn = 0; while (1) { //form the 4-pixel-by-4-pixel square target Set_OLED_Pixel(TargetRow, TargetColumn); Set_OLED_Pixel(TargetRow, TargetColumn+1); Set_OLED_Pixel(TargetRow, TargetColumn+2); Set_OLED_Pixel(TargetRow, TargetColumn+3); Set_OLED_Pixel(TargetRow+1, TargetColumn); Set_OLED_Pixel(TargetRow+1, TargetColumn+1); Set_OLED_Pixel(TargetRow+1, TargetColumn+2); Set_OLED_Pixel(TargetRow+1, TargetColumn+3); Set_OLED_Pixel(TargetRow+2, TargetColumn); Set_OLED_Pixel(TargetRow+2, TargetColumn+1); Set_OLED_Pixel(TargetRow+2, TargetColumn+2); Set_OLED_Pixel(TargetRow+2, TargetColumn+3); Set_OLED_Pixel(TargetRow+3, TargetColumn); Set_OLED_Pixel(TargetRow+3, TargetColumn+1); Set_OLED_Pixel(TargetRow+3, TargetColumn+2); Set_OLED_Pixel(TargetRow+3, TargetColumn+3); Update_OLED_Display(); delay_ms(30); //clear the previous target Clear_OLED_Pixel(TargetRow, TargetColumn); Clear_OLED_Pixel(TargetRow, TargetColumn+1); Clear_OLED_Pixel(TargetRow, TargetColumn+2); Clear_OLED_Pixel(TargetRow, TargetColumn+3); Clear_OLED_Pixel(TargetRow+1, TargetColumn); Clear_OLED_Pixel(TargetRow+1, TargetColumn+1); Clear_OLED_Pixel(TargetRow+1, TargetColumn+2); Clear_OLED_Pixel(TargetRow+1, TargetColumn+3); Clear_OLED_Pixel(TargetRow+2, TargetColumn); Clear_OLED_Pixel(TargetRow+2, TargetColumn+1); Clear_OLED_Pixel(TargetRow+2, TargetColumn+2); Clear_OLED_Pixel(TargetRow+2, TargetColumn+3); Clear_OLED_Pixel(TargetRow+3, TargetColumn); Clear_OLED_Pixel(TargetRow+3, TargetColumn+1); Clear_OLED_Pixel(TargetRow+3, TargetColumn+2); Clear_OLED_Pixel(TargetRow+3, TargetColumn+3); /*Move the target one pixel to the right. If the right edge of the target has reached the last display column, return the target to the left-hand edge of the screen.*/ TargetColumn++; if ( (TargetColumn+3) == OLED_WIDTH_PIXELS) { TargetColumn = 0; } } }

发射激光

你按2号按钮发射激光。我们需要启用连接到按钮2的端口引脚,并配置它的中断(按钮功能是中断驱动的)。

静态Void Configure_PushButton2(void){/ *使能PIOC的时钟,这是并联输入/输出控制器之一。按钮2信号连接到PIOC中包含的引脚。* / pmc_enable_periph_clk(id_pioc);/ *为连接到按钮2的引脚配置集成的去抖功能。* / pio_set_debounce_filter(pioc,pin_pushbutton_2_mask,10);/ *这为按钮2中断分配了一个中断处理函数。第三个参数由在配置中断的过程中使用的属性组成。在这种情况下,属性参数指示PIN的内部上拉有效,即PIN的去抖过滤器处于活动状态,并且中断在下降沿触发。* / pio_handler_set(pioc,id_pioc,pin_pustton_2_mask,pin_pustton_2_attr,pushbutton2_interrupthandler);//在NVIC中启用PIoC中断(嵌套的矢量中断控制器)nvic_enableirq((irqn_type)id_pioc);// 15是最低优先级,0是最高优先级pio_handler_set_priority(pioc,(irqn_type)id_pioc,0);/ *启用按钮2中断源。注意,有必要“启用中断”和“启用NVIC中的中断”。* / PIO_ENABLE_INTERRUPT(PIOC,PIN_PUSHBUTTON_2_MASK); }

激光显示为垂直线,垂直线从屏幕的底部中心快速延伸(即,行= 31,列= 63)。如果激光击中目标,则程序产生“碎片效果”,即,方形消失,并被向外扩展的四个像素(来自广场的每个角落)取代。我意识到这一点不满足质量守恒,考虑到原始目标总共16个像素;我们只假设其他12个像素被蒸发成无形的小颗粒。

这里是按钮2中断处理程序,其中包括所有的激光功能。同样,注释本质上是文章本身信息的延续,所以不要忘记它们。

静态void按钮2_interrupthandler(UInt32_t id,uint32_t mask){/ *确认中断的源是按钮2.这似乎不必要,但它包含在Atmel的示例代码中。* / if((id == id_pioc)&&(mask == pin_pushbutton_2_mask)){/ *禁用按钮2中断,以防止由开关反弹引起的虚假中断。我探测了按钮信号,看起来像信号在信号的低电平状态结束时发生虚假转换,所以一旦我们进入中断处理程序,就会禁用中断,有效地抑制了虚假的中断请求。由于某种原因,我无法通过PIN的集成去抖过滤器完全解决此问题。* / pio_disable_interrupt(pioc,pin_pushbutton_2_mask);//这些下一个8组语句创建了激光束Set_oled_pixel(31,63);set_oled_pixel(30,63);set_oled_pixel(29,63);set_oled_pixel(28,63);update_oled_display();delay_ms(10); Set_OLED_Pixel(27, 63); Set_OLED_Pixel(26, 63); Set_OLED_Pixel(25, 63); Set_OLED_Pixel(24, 63); Update_OLED_Display(); delay_ms(10); Set_OLED_Pixel(23, 63); Set_OLED_Pixel(22, 63); Set_OLED_Pixel(21, 63); Set_OLED_Pixel(20, 63); Update_OLED_Display(); delay_ms(10); Set_OLED_Pixel(19, 63); Set_OLED_Pixel(18, 63); Set_OLED_Pixel(17, 63); Set_OLED_Pixel(16, 63); Update_OLED_Display(); delay_ms(10); Set_OLED_Pixel(15, 63); Set_OLED_Pixel(14, 63); Set_OLED_Pixel(13, 63); Set_OLED_Pixel(12, 63); Update_OLED_Display(); delay_ms(10); Set_OLED_Pixel(11, 63); Set_OLED_Pixel(10, 63); Set_OLED_Pixel(9, 63); Set_OLED_Pixel(8, 63); Update_OLED_Display(); delay_ms(10); Set_OLED_Pixel(7, 63); Set_OLED_Pixel(6, 63); Set_OLED_Pixel(5, 63); Set_OLED_Pixel(4, 63); Update_OLED_Display(); delay_ms(10); Set_OLED_Pixel(3, 63); Set_OLED_Pixel(2, 63); Set_OLED_Pixel(1, 63); Set_OLED_Pixel(0, 63); Update_OLED_Display(); delay_ms(10); /*Does the target currently include the laser-beam column? If so, clear the OLED array to remove the target and the laser, then create the debris effect. After that, clear the OLED screen and return the target to its starting position.*/ if (TargetColumn >= 60 && TargetColumn <= 63) { Clear_OLED_Array(); Set_OLED_Pixel(TargetRow-1, TargetColumn-1); Set_OLED_Pixel(TargetRow-1, TargetColumn+4); Set_OLED_Pixel(TargetRow+4, TargetColumn-1); Set_OLED_Pixel(TargetRow+4, TargetColumn+4); Update_OLED_Display(); delay_ms(200); Clear_OLED_Pixel(TargetRow-1, TargetColumn-1); Clear_OLED_Pixel(TargetRow-1, TargetColumn+4); Clear_OLED_Pixel(TargetRow+4, TargetColumn-1); Clear_OLED_Pixel(TargetRow+4, TargetColumn+4); Set_OLED_Pixel(TargetRow-2, TargetColumn-2); Set_OLED_Pixel(TargetRow-2, TargetColumn+5); Set_OLED_Pixel(TargetRow+5, TargetColumn-2); Set_OLED_Pixel(TargetRow+5, TargetColumn+5); Update_OLED_Display(); delay_ms(200); Clear_OLED_Pixel(TargetRow-2, TargetColumn-2); Clear_OLED_Pixel(TargetRow-2, TargetColumn+5); Clear_OLED_Pixel(TargetRow+5, TargetColumn-2); Clear_OLED_Pixel(TargetRow+5, TargetColumn+5); Set_OLED_Pixel(TargetRow-3, TargetColumn-3); Set_OLED_Pixel(TargetRow-3, TargetColumn+6); Set_OLED_Pixel(TargetRow+6, TargetColumn-3); Set_OLED_Pixel(TargetRow+6, TargetColumn+6); Update_OLED_Display(); delay_ms(200); Clear_OLED_Pixel(TargetRow-3, TargetColumn-3); Clear_OLED_Pixel(TargetRow-3, TargetColumn+6); Clear_OLED_Pixel(TargetRow+6, TargetColumn-3); Clear_OLED_Pixel(TargetRow+6, TargetColumn+6); Set_OLED_Pixel(TargetRow-4, TargetColumn-4); Set_OLED_Pixel(TargetRow-4, TargetColumn+7); Set_OLED_Pixel(TargetRow+7, TargetColumn-4); Set_OLED_Pixel(TargetRow+7, TargetColumn+7); Update_OLED_Display(); delay_ms(200); Clear_OLED_Pixel(TargetRow-4, TargetColumn-4); Clear_OLED_Pixel(TargetRow-4, TargetColumn+7); Clear_OLED_Pixel(TargetRow+7, TargetColumn-4); Clear_OLED_Pixel(TargetRow+7, TargetColumn+7); Set_OLED_Pixel(TargetRow-5, TargetColumn-5); Set_OLED_Pixel(TargetRow-5, TargetColumn+8); Set_OLED_Pixel(TargetRow+8, TargetColumn-5); Set_OLED_Pixel(TargetRow+8, TargetColumn+8); Update_OLED_Display(); delay_ms(200); Clear_OLED_Pixel(TargetRow-5, TargetColumn-5); Clear_OLED_Pixel(TargetRow-5, TargetColumn+8); Clear_OLED_Pixel(TargetRow+8, TargetColumn-5); Clear_OLED_Pixel(TargetRow+8, TargetColumn+8); Set_OLED_Pixel(TargetRow-6, TargetColumn-6); Set_OLED_Pixel(TargetRow-6, TargetColumn+9); Set_OLED_Pixel(TargetRow+9, TargetColumn-6); Set_OLED_Pixel(TargetRow+9, TargetColumn+9); Update_OLED_Display(); delay_ms(200); Clear_OLED_Pixel(TargetRow-6, TargetColumn-6); Clear_OLED_Pixel(TargetRow-6, TargetColumn+9); Clear_OLED_Pixel(TargetRow+9, TargetColumn-6); Clear_OLED_Pixel(TargetRow+9, TargetColumn+9); Set_OLED_Pixel(TargetRow-7, TargetColumn-7); Set_OLED_Pixel(TargetRow-7, TargetColumn+10); Set_OLED_Pixel(TargetRow+10, TargetColumn-7); Set_OLED_Pixel(TargetRow+10, TargetColumn+10); Update_OLED_Display(); delay_ms(500); Clear_OLED_Array(); Update_OLED_Display(); delay_ms(1000); TargetColumn = 0; } /*If the laser beam missed the target, clear the laser beam pixels. Nothing else needs to be done; the target will continue moving when execution returns to the infinite loop in main().*/ else { OLED_Pixels[0][63] = 0; OLED_Pixels[1][63] = 0; OLED_Pixels[2][63] = 0; OLED_Pixels[3][63] = 0; } //reenable the pushbutton 2 interrupt pio_enable_interrupt(PIOC, PIN_PUSHBUTTON_2_MASK); } }

结果

您可以使用以下链接下载所有源和项目文件。

OLEDTargetPractice_Part2.zip

这里是行动中的代码。它让你想知道为什么人们购买视频游戏,当它非常容易让他们自己!

为自己提供这个项目!BOM。

1条评论