Русский
Русский
English
Статистика
Реклама

Из песочницы Подключение OLED дисплея ssd1306 к STM32 (SPIDMA)

В данной статье будет описан процесс подключение oled дисплея с контроллером ssd1306 разрешением 128x64 к микроконтроллеру stm32f103C8T6 по интерфейсу SPI. Также мне хотелось добиться максимальной скорости обновления дисплея, поэтому целесообразно использовать DMA, а программирование микроконтроллера производить с помощью библиотеки CMSIS.

Подключение


Подключать дисплей к микроконтроллеру будем по интерфейсу SPI1 по следующей схеме:

  • VDD-> +3.3В
  • GND-> Земля
  • SCK -> PA5
  • SDA -> PA7(MOSI)
  • RES-> PA1
  • CS-> PA2
  • DS-> PA3

imageimage

Передача данных происходит по возрастающему фронту сигнала синхронизации по 1 байту за кадр. Линии SCK и SDA служат для передачи данных по интерфейсу SPI, RES перезагружает контроллер дисплея при низком логическом уровне, CS отвечает за выбор устройства на шине SPI при низком логическом уровне, DS определяет тип данных (команда 1/данные 0) которые передаются дисплею. Так как с дисплея ничего считать нельзя, вывод MISO использовать не будем.

Организация памяти контроллера дисплея


Перед тем, как выводить что-либо на экран, необходимо разобраться как в контроллере ssd1306 организована память.

image
image

Вся графическая память (GDDRAM) представляет собой область 128*64=8192 бит=1 Кбайт. Область разбита на 8 страниц, которые представлены в виде в виде совокупности из 128-ми 8-ми битных сегментов. Адресация памяти происходит по номеру страницы и номеру сегмента соответственно.

При таком методе адресации есть очень неприятная особенность невозможность записать в память 1 бит информации, так как запись происходит по сегменту (по 8 бит). А так как для корректного отображения единичного пикселя на экране, необходимо знать состояние остальных пикселей в сегменте, целесообразно создать в памяти микроконтроллера буфер размером 1 Кбайт и циклически загружать его в память дисплея (тут и пригодится DMA), соответственно, производя его полное обновление. При использовании такого метода возможно пересчитать положение каждого бита в памяти на классические координаты x,y. Тогда для вывода на экран точки с координатами x и y воспользуемся следующим способом:

displayBuff[x+(y/8)*SSD1306_WIDTH]|=(1<<(y%8));

А для того, чтобы стереть точку

displayBuff[x+(y/8)*SSD1306_WIDTH]&=~(1<<(y%8));


Настройка SPI


Как говорилось выше, подключать дисплей будем к SPI1 микроконтроллера STM32F103C8.

image

Для удобства написания кода объявим некоторые константы и создадим функцию для инициализации SPI.

#define SSD1306_WIDTH 128#define SSD1306_HEIGHT 64#define BUFFER_SIZE 1024//Макросы для активации устройства на шине, сброса экрана и выбора команды/данных#define CS_SET GPIOA->BSRR|=GPIO_BSRR_BS2#define CS_RES GPIOA->BSRR|=GPIO_BSRR_BR2#define RESET_SET GPIOA->BSRR|=GPIO_BSRR_BS1#define RESET_RES GPIOA->BSRR|=GPIO_BSRR_BR1#define DATA GPIOA->BSRR|=GPIO_BSRR_BS3#define COMMAND GPIOA->BSRR|=GPIO_BSRR_BR3void spi1Init(){    return;}

Включим тактирование и произведем настройку выходов GPIO, как показано в таблице выше.

RCC->APB2ENR|=RCC_APB2ENR_SPI1EN | RCC_APB2ENR_IOPAEN;//Включить тактирование SPI1 и GPIOARCC->AHBENR|=RCC_AHBENR_DMA1EN;//Включить тактирование DMAGPIOA->CRL|= GPIO_CRL_MODE5 | GPIO_CRL_MODE7;//PA4,PA5,PA7 в режим выходов 50MHzGPIOA->CRL&= ~(GPIO_CRL_CNF5 | GPIO_CRL_CNF7);GPIOA->CRL|=  GPIO_CRL_CNF5_1 | GPIO_CRL_CNF7_1;//PA5,PA7 - выход с альтернативной функцией push-pull, PA4 - выход push-pull

Далее произведем настройку SPI в режим master и частотой 18 Мгц.

SPI1->CR1|=SPI_CR1_MSTR;//Режим ведущегоSPI1->CR1|= (0x00 & SPI_CR1_BR);//Делитель частоты на 2SPI1->CR1|=SPI_CR1_SSM;//Программный NSSSPI1->CR1|=SPI_CR1_SSI;//NSS - highSPI1->CR2|=SPI_CR2_TXDMAEN;//Разрешить запросы DMASPI1->CR1|=SPI_CR1_SPE;//включить SPI1

Настроим DMA.

DMA1_Channel3->CCR|=DMA_CCR1_PSIZE_0;//Размер периферии 1байтDMA1_Channel3->CCR|=DMA_CCR1_DIR;//Режим DMA из памяти в перифериюDMA1_Channel3->CCR|=DMA_CCR1_MINC;//Включить инкремент памятиDMA1_Channel3->CCR|=DMA_CCR1_PL;//Высокий приоритет DMA

Далее напишем функцию отправки данных по SPI (пока без DMA). Процесс обмена данными заключается в следующем:

  1. Ожидаем, пока SPI освободится
  2. CS=0
  3. Отправка данных
  4. CS=1

void spiTransmit(uint8_t data){CS_RES;SPI1->DR = data;while((SPI1->SR & SPI_SR_BSY)){};CS_SET;}

Также напишем функцию непосредственно отправки команды экрану (Переключение линии DC производим только при передаче команды, а затем возвращаем ее в состояние данные, так как команды передавать будем не так часто и в производительности не потеряем).

void ssd1306SendCommand(uint8_t command){COMMAND;spiTransmit(command);DATA;}

Далее займемся функциями для работы непосредственно с DMA, для этого объявим буфер в памяти микроконтроллера и создадим функции для начала и остановки циклической отправки этого буфера в память экрана.

static uint8_t displayBuff[BUFFER_SIZE];//Буфер экранаvoid ssd1306RunDisplayUPD(){DATA;DMA1_Channel3->CCR&=~(DMA_CCR1_EN);//Выключить DMADMA1_Channel3->CPAR=(uint32_t)(&SPI1->DR);//Занесем в DMA адрес регистра данных SPI1DMA1_Channel3->CMAR=(uint32_t)&displayBuff;//Адрес данныхDMA1_Channel3->CNDTR=sizeof(displayBuff);//Размер данныхDMA1->IFCR&=~(DMA_IFCR_CGIF3);CS_RES;//Выбор устройства на шинеDMA1_Channel3->CCR|=DMA_CCR1_CIRC;//Циклический режим DMADMA1_Channel3->CCR|=DMA_CCR1_EN;//Включить DMA}void ssd1306StopDispayUPD(){CS_SET;//Дезактивация устройства на шинеDMA1_Channel3->CCR&=~(DMA_CCR1_EN);//Выключить DMADMA1_Channel3->CCR&=~DMA_CCR1_CIRC;//Выключить циклический режим}

Инициализация экрана и вывод данных


Теперь создадим функцию для инициализации самого экрана.

void ssd1306Init(){}

Для начала настроим CS, RESET и линию DC, а также произведем сброс контроллера дисплея.

uint16_t i;GPIOA->CRL|= GPIO_CRL_MODE2 |GPIO_CRL_MODE1 | GPIO_CRL_MODE3;GPIOA->CRL&= ~(GPIO_CRL_CNF1 | GPIO_CRL_CNF2 | GPIO_CRL_CNF3);//PA1,PA2,PA3 в режим выхода//Сброс экрана и очистка буфераRESET_RES;for(i=0;i<BUFFER_SIZE;i++){displayBuff[i]=0;}RESET_SET;CS_SET;//Выбор устройства на шине

Далее отправим последовательность команд для инициализации (Более подробно о них можно узнать в документации на контроллер ssd1306).

ssd1306SendCommand(0xAE); //display offssd1306SendCommand(0xD5); //Set Memory Addressing Modessd1306SendCommand(0x80); //00,Horizontal Addressing Mode;01,Verticalssd1306SendCommand(0xA8); //Set Page Start Address for Page Addressingssd1306SendCommand(0x3F); //Set COM Output Scan Directionssd1306SendCommand(0xD3); //set low column addressssd1306SendCommand(0x00); //set high column addressssd1306SendCommand(0x40); //set start line addressssd1306SendCommand(0x8D); //set contrast control registerssd1306SendCommand(0x14);ssd1306SendCommand(0x20); //set segment re-map 0 to 127ssd1306SendCommand(0x00); //set normal displayssd1306SendCommand(0xA1); //set multiplex ratio(1 to 64)ssd1306SendCommand(0xC8); //ssd1306SendCommand(0xDA); //0xa4,Output follows RAMssd1306SendCommand(0x12); //set display offsetssd1306SendCommand(0x81); //not offsetssd1306SendCommand(0x8F); //set display clock divide ratio/oscillator frequencyssd1306SendCommand(0xD9); //set divide ratiossd1306SendCommand(0xF1); //set pre-charge periodssd1306SendCommand(0xDB); ssd1306SendCommand(0x40); //set com pins hardware configurationssd1306SendCommand(0xA4);ssd1306SendCommand(0xA6); //set vcomhssd1306SendCommand(0xAF); //0x20,0.77xVcc

Создадим функции для заполнения всего экрана выбранным цветом и отображения одного пикселя.

typedef enum COLOR{BLACK,WHITE}COLOR;void ssd1306DrawPixel(uint16_t x, uint16_t y,COLOR color){if(x<SSD1306_WIDTH && y <SSD1306_HEIGHT && x>=0 && y>=0){if(color==WHITE){displayBuff[x+(y/8)*SSD1306_WIDTH]|=(1<<(y%8));}else if(color==BLACK){displayBuff[x+(y/8)*SSD1306_WIDTH]&=~(1<<(y%8));}}}void ssd1306FillDisplay(COLOR color){uint16_t i;for(i=0;i<SSD1306_HEIGHT*SSD1306_WIDTH;i++){if(color==WHITE)displayBuff[i]=0xFF;else if(color==BLACK)displayBuff[i]=0;}}

Далее в теле основной программы инициализируем SPI и дисплей.

RccClockInit();spi1Init();ssd1306Init();

Функция RccClockInit() предназначена для настройки тактирования микроконтроллера.

RccClockInit код
int RccClockInit(){//Enable HSE//Setting PLL//Enable PLL//Setting count wait cycles of FLASH//Setting AHB1,AHB2 prescaler//Switch to PLLuint16_t timeDelay;RCC->CR|=RCC_CR_HSEON;//Enable HSEfor(timeDelay=0;;timeDelay++){if(RCC->CR&RCC_CR_HSERDY) break;if(timeDelay>0x1000){RCC->CR&=~RCC_CR_HSEON;return 1;}}RCC->CFGR|=RCC_CFGR_PLLMULL9;//PLL x9RCC->CFGR|=RCC_CFGR_PLLSRC_HSE;//PLL sourse:HSERCC->CR|=RCC_CR_PLLON;//Enable PLLfor(timeDelay=0;;timeDelay++){if(RCC->CR&RCC_CR_PLLRDY) break;if(timeDelay>0x1000){RCC->CR&=~RCC_CR_HSEON;RCC->CR&=~RCC_CR_PLLON;return 2;}}FLASH->ACR|=FLASH_ACR_LATENCY_2;RCC->CFGR|=RCC_CFGR_PPRE1_DIV2;//APB1 prescaler=2RCC->CFGR|=RCC_CFGR_SW_PLL;//Switch to PLLwhile((RCC->CFGR&RCC_CFGR_SWS)!=(0x02<<2)){}RCC->CR&=~RCC_CR_HSION;//Disable HSIreturn 0;}


Зальем весь дисплей белым цветом и посмотрим результат.

ssd1306RunDisplayUPD();ssd1306FillDisplay(WHITE);

image

Нарисуем на экране в сетку шагом в 10 пикселей.

for(i=0;i<SSD1306_WIDTH;i++){for(j=0;j<SSD1306_HEIGHT;j++){if(j%10==0 || i%10==0)ssd1306DrawPixel(i,j,WHITE);}}

image

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

Частота обновления дисплея


Так как буфер отправляется в память дисплея циклически, для приблизительного определения частоты обновления дисплея достаточно будет узнать время, за которое DMA осуществляет полную передачу данных. Для отладки в реальном времени воспользуемся библиотекой EventRecorder из Keil.

Для того, чтобы узнать момент окончания передачи данных, настроим прерывание DMA на окончание передачи.

DMA1_Channel3->CCR|=DMA_CCR1_TCIE;//Прерывание по завершении передачиDMA1->IFCR&=~DMA_IFCR_CTCIF3;//Сбрасываем флаг прерыванияNVIC_EnableIRQ(DMA1_Channel3_IRQn);//Включить прерывание

Промежуток времени будем отслеживать с помощью функций EventStart и EventStop.

image

Получаем 0.00400881-0.00377114=0.00012767 сек, что соответствует частоте обновления 4.2 Кгц. На самом деле частота не такая большая, что связано с неточностью способа измерения, но явно больше стандартных 60 Гц.

Ссылки


Источник: habr.com
К списку статей
Опубликовано: 09.08.2020 00:17:30
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Программирование микроконтроллеров

Stm32

Diy

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru