Как создать игровую систему с atmel sam4s xplained pro

Как создать игровую систему с atmel sam4s xplained pro
Как создать игровую систему с atmel sam4s xplained pro
Anonim

Как создать игровую систему с помощью Atmel SAM4S Xplained Pro

В этой статье приводятся дополнительные сведения о разработке кода, конфигурации оборудования и интерфейсе OLED.

Требуемое оборудование / программное обеспечение

  • Стартовый комплект SAM4S Xplained Pro или комплект для оценки SAM4S Xplained Pro плюс плата расширения OLED1 Xplained Pro
  • Студия Atmel

Предыдущая статья

Разработка Intro Project с помощью Atmel SAM4S Xplained Pro

Простой экранный драйвер A (Very)

В конце предыдущей статьи мы успешно создали новый проект, который настраивает и инициализирует микроконтроллер ATSAM4SD32C, а затем повторно отображает простое текстовое сообщение на OLED-дисплее. Возможно, эта функциональность достаточно захватывает для вас, но я не совсем удовлетворен. Итак, давайте продолжим разработку нашей очень примитивной видеоигры, состоящей из движущейся квадратной цели и лазерного луча, который срабатывает при нажатии кнопки 2. Если лазерный луч попадает в цель, вы выигрываете. Если лазерный луч пропускает цель, вы стремитесь, пока не выиграете.

Первое, что нам нужно - очень простой драйвер для нашей очень простой видеоигры. Это даст нам удобный способ установки и очистки пикселей в любом месте экрана OLED. Кроме того, мы хотим сохранить память в микроконтроллере, которая соответствует текущему состоянию экрана. Это позволяет нам проверять состояние пикселей без чтения данных с контроллера OLED; это особенно важно для нашей конкретной конфигурации оборудования, потому что мы говорим с модулем OLED через SPI, а интерфейс SPI контроллера OLED (в отличие от его параллельного интерфейса) не поддерживает операции чтения.

Страницы и столбцы

Сначала нам нужно понять, как расположены OLED-пиксели. Взгляните на следующие две диаграммы из таблицы SSD1306.

Image
Image
Image
Image

Итак, у нас есть 8 "страниц" вертикальных пикселей и 128 столбцов горизонтальных пикселей. Каждая страница состоит из 8 вертикальных пикселей, так что общее разрешение составляет 64 вертикальных пикселя на 128 горизонтальных пикселей. Однако физический размер дисплея составляет 32 вертикальных пикселя на 128 горизонтальных пикселей; таким образом, OLED-контроллер имеет достаточную память пикселей для 8 вертикальных страниц, но только 4 страницы могут отображаться за один раз.

Мы модифицируем OLED-дисплей, записывая байт через SPI. Этот байт управляет 8 вертикальными пикселями, соответствующими текущему выбранному адресу страницы и столбца. Вторая диаграмма показывает взаимосвязь между пикселями на странице и битами в записанном байте - младший значащий бит в байте соответствует наивысшему пикселю на странице, а самый старший бит соответствует самому низкому пикселю.

Индивидуальные пиксели против всех пикселей

Можно изменить только один столбец одной страницы; вы делаете это, устанавливая адрес столбца и страницы, а затем записывая один байт данных. Функции ASF для этого: ssd1306_set_page_address () и ssd1306_set_column_address (), за которыми следует ssd1306_write_data (). Я предпочитаю, однако, изменять пиксели в буфере дисплея, а затем обновлять весь экран. Я нахожу этот подход более универсальным и интуитивным. Это может показаться довольно неэффективным, но это действительно не так уж плохо. Одна из причин этого заключается в том, что интерфейс SPI OLED поддерживает довольно высокие тактовые частоты. Следующая трассировка области показывает синхронизирующий сигнал SPI, который в настоящее время настроен на работу на частоте 5 МГц (максимальный уровень поддержки, поддерживаемый OLED-контроллером, составляет 10 МГц).

Image
Image

Поэтому для записи одного байта требуется всего 1, 6 мкс. Вы могли бы подумать, что мы могли бы обновить весь экран примерно на 1, 6 мкс × (128 столбцов) × (4 страницы) ≈ 0, 8 мс, но на самом деле это занимает гораздо больше времени, чем это, потому что существует около 10 мкс мертвого времени между последовательными байтами, Это мертвое время намеренно включается в функцию ssd1306_write_data (), чтобы удовлетворить задержку SSD1306, хотя комментарий в функции указывает, что мертвое время может быть как 3 мкс, поэтому я не уверен, почему Atmel выбрал 10 мкс, Вы можете попытаться уменьшить задержку в ssd1306_write_data (), если хотите ускорить процесс. В любом случае, с текущей задержкой, для обновления всего экрана требуется около (11, 6 мкс) × (128 столбцов) × (4 страницы) ≈ 5, 9 мс, и область подтверждает, что это действительно так:

Image
Image

Таким образом, мы можем видеть, что время, необходимое для обновления всего экрана, отнюдь не является чрезмерным, особенно если вы считаете, что даже при 30 кадрах в секунду у нас будет 33 мс между обновлениями экрана.

Код драйвера

Я использую следующий двумерный массив в качестве буфера отображения:


//Preprocessor definitions #define OLED_HEIGHT_BYTES 32/8 #define OLED_WIDTH_PIXELS 128 //Global variables unsigned char OLED_Pixels(OLED_HEIGHT_BYTES)(OLED_WIDTH_PIXELS);

Эти две функции изменяют один пиксель в буфере дисплея:


/*This function sets one OLED pixel identified by the Row and Column parameters. Keep in mind that the OLED array rows are addressed as bytes (of 8 pixels), whereas the Row parameter passed to this function is a pixel address.*/ static inline void Set_OLED_Pixel(unsigned char Row, unsigned char Column) { if (Row < 8) OLED_Pixels(0)(Column) else if (Row < 16) = (0x01 << (Row - 8)); else if (Row < 24) OLED_Pixels(2)(Column) else if (Row < 32) OLED_Pixels(3)(Column) } /*This function clears one OLED pixel identified by the Row and Column parameters. Keep in mind that the OLED array rows are addressed as bytes (of 8 pixels), whereas the Row parameter passed to this function is a pixel address.*/ static inline void Clear_OLED_Pixel(unsigned char Row, unsigned char Column) { if (Row < 8) { OLED_Pixels(0)(Column) &= ~(0x01 << Row); } else if (Row < 16) { OLED_Pixels(1)(Column) &= ~(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)); } }

Обратите внимание, что я объявлял их «встроенными» функциями, чтобы они выполнялись быстрее.

Вот функция, которую я использую для очистки всех пикселей в буфере дисплея:


static void Clear_OLED_Array(void) { unsigned char x, y; for (x = 0; x < OLED_HEIGHT_BYTES; x+) { for (y = 0; y < OLED_WIDTH_PIXELS; y+) { OLED_Pixels(x)(y) = 0; } } }

Обратите внимание, что эти три функции фактически не обновляют экран OLED; они просто модифицируют буфер. Экран обновляется путем копирования содержимого буфера дисплея в память контроллера OLED:


static void Update_OLED_Display(void) { unsigned char Row, Column; ssd1306_set_page_address(0); ssd1306_set_column_address(0); for (Row = 0; Row < OLED_HEIGHT_BYTES; Row+) { for (Column = 0; Column < OLED_WIDTH_PIXELS; Column+) { ssd1306_write_data(OLED_Pixels(Row)(Column)); } } }

Эта функция записывает все байты из буфера дисплея в контроллер OLED через SPI в соответствии со следующим шаблоном: страница 0, столбец 0; стр. 0, столбец 1;,,,; стр. 0, столбец 127; стр. 1, столбец 0;,,,; стр. 1, столбец 127; стр. 2, столбец 0;,,,; стр. 2, столбец 127; стр. 3, столбец 0;,,,; стр. 3, столбец 127. Имейте в виду, что эти байты записываются один за другим без какой-либо промежуточной информации о памяти. Таким образом, мы должны убедиться, что контроллер OLED правильно интерпретирует эти данные. Для этого мы настраиваем его для режима горизонтальной адресации:

Image
Image

При работе в этом режиме контроллер OLED автоматически обновляет свой адресный указатель таким образом, который согласуется с тем, как мы отправляем содержимое буфера отображения.

Движущаяся цель

Целью является квадрат размером 4 пикселя на 4 пикселя, который многократно перемещается слева направо, следующим образом:

Существует задержка 30 мс между движениями с одним пикселем вправо. Этот захват области видимости показывает пакеты данных, передаваемые контроллеру OLED; как мы видели выше, пакеты (т. е. байты данных для обновления всего экрана) имеют ширину около 6 мс, и теперь у нас есть 30 мс между пакетами.

Image
Image

Следующий фрагмент кода дает вам всю функцию main (). Функциональность движущейся цели находится в бесконечном цикле while. Обратитесь к комментариям за важной информацией.


int main (void) { //clock configuration and initialization sysclk_init(); /*Disable the watchdog timer and configure/initialize port pins connected to various components incorporated into the SAM4S Xplained development platform, eg, the NAND flash, the OLED interface, the LEDs, the SW0 pushbutton.*/ board_init(); //initialize SPI and the OLED controller ssd1306_init(); /*These two statements configure the OLED module for "horizontal addressing mode."*/ ssd1306_write_command(SSD1306_CMD_SET_MEMORY_ADDRESSING_MODE); ssd1306_write_command(0x00); Configure_Pushbutton2(); /*This function clears all the pixels in the array; it does not update the OLED display.*/ Clear_OLED_Array(); Update_OLED_Display(); /*These variables hold the row and column address corresponding to the top-left pixel of the 4-pixel-by-4-pixel square target. 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, и настроить его прерывание (функциональность кнопки управляется прерыванием).


static void Configure_Pushbutton2(void) { /*Enable the clock for PIOC, which is one of the parallel input/output controllers. The pushbutton 2 signal is connected to a pin included in PIOC.*/ pmc_enable_periph_clk(ID_PIOC); /*Configure the integrated debounce functionality for the pin connected to pushbutton 2.*/ pio_set_debounce_filter(PIOC, PIN_PUSHBUTTON_2_MASK, 10); /*This assigns an interrupt handler function for the pushbutton 2 interrupt. The third parameter consists of attributes used in the process of configuring the interrupt. In this case the attributes parameter indicates that the pin's internal pull-up is active, that the pin's debounce filter is active, and that the interrupt fires on the falling edge.*/ pio_handler_set(PIOC, ID_PIOC, PIN_PUSHBUTTON_2_MASK, PIN_PUSHBUTTON_2_ATTR, PushButton2_InterruptHandler); //enable PIOC interrupts in the NVIC (nested vectored interrupt controller) NVIC_EnableIRQ((IRQn_Type) ID_PIOC); //15 is the lowest priority, 0 is the highest priority pio_handler_set_priority(PIOC, (IRQn_Type) ID_PIOC, 0); /*Enable the pushbutton 2 interrupt source. Note that it is necessary to both "enable the interrupt" and "enable the interrupt in the NVIC."*/ pio_enable_interrupt(PIOC, PIN_PUSHBUTTON_2_MASK); }

Лазер показан как вертикальная линия, которая быстро распространяется вверх от нижнего центра экрана (т. Е. Строка = 31, столбец = 63). Если лазер попадает в цель, программа генерирует «эффект мусора», т. Е. Квадрат исчезает и заменяется четырьмя пикселями, которые расширяются наружу (по одному от каждого угла квадрата). Я понимаю, что это не удовлетворяет сохранению массы, учитывая, что исходная цель имела в общей сложности 16 пикселей; мы предположим, что остальные 12 пикселей испарялись в невидимо мелкие частицы.

Здесь находится обработчик прерываний 2, который включает в себя все функции лазера. Опять же, комментарии по сути являются продолжением информации в самой статье, поэтому не забывайте о них.


static void PushButton2_InterruptHandler(uint32_t id, uint32_t mask) { /*Confirm that the source of the interrupt is pushbutton 2. This seems unnecessary to me, but it is included in Atmel's example code.*/ if ((id == ID_PIOC) & (mask == PIN_PUSHBUTTON_2_MASK)) { /*Disable the pushbutton 2 interrupt to prevent spurious interrupts caused by switch bounce. I probed the pushbutton signal, and it looks like the spurious transitions occur at the end of the signal's active-low state, so disabling the interrupt as soon as we enter the interrupt handler effectively suppresses the spurious interrupt requests. For some reason I was not able to completely resolve this problem by means of the pin's integrated debounce filter.*/ pio_disable_interrupt(PIOC, PIN_PUSHBUTTON_2_MASK); //these next 8 groups of statements create the laser beam 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); } }

Результаты

Вы можете использовать следующую ссылку для загрузки всех исходных файлов и файлов проекта.

Файлы источника и проекта

И вот код в действии. Это заставляет вас задаться вопросом, почему люди покупают видеоигры, когда их так легко сделать сами!

Попробуйте этот проект сами! Получить спецификацию.