Подключение OLED дисплея ssd1306 к STM32 (SPI+DMA)

от автора

В данной статье будет описан процесс подключение oled дисплея с контроллером ssd1306 разрешением 128×64 к микроконтроллеру 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_BR3  void spi1Init() {     return; } 

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

 RCC->APB2ENR|=RCC_APB2ENR_SPI1EN | RCC_APB2ENR_IOPAEN;//Включить тактирование SPI1 и GPIOA RCC->AHBENR|=RCC_AHBENR_DMA1EN;//Включить тактирование DMA GPIOA->CRL|= GPIO_CRL_MODE5 | GPIO_CRL_MODE7;//PA4,PA5,PA7 в режим выходов 50MHz GPIOA->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);//Делитель частоты на 2 SPI1->CR1|=SPI_CR1_SSM;//Программный NSS SPI1->CR1|=SPI_CR1_SSI;//NSS - high SPI1->CR2|=SPI_CR2_TXDMAEN;//Разрешить запросы DMA SPI1->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);//Выключить DMA 	DMA1_Channel3->CPAR=(uint32_t)(&SPI1->DR);//Занесем в DMA адрес регистра данных SPI1 	DMA1_Channel3->CMAR=(uint32_t)&displayBuff;//Адрес данных 	DMA1_Channel3->CNDTR=sizeof(displayBuff);//Размер данных 	DMA1->IFCR&=~(DMA_IFCR_CGIF3); 	CS_RES;//Выбор устройства на шине 	DMA1_Channel3->CCR|=DMA_CCR1_CIRC;//Циклический режим DMA 	DMA1_Channel3->CCR|=DMA_CCR1_EN;//Включить DMA }  void ssd1306StopDispayUPD() { 	CS_SET;//Дезактивация устройства на шине 	DMA1_Channel3->CCR&=~(DMA_CCR1_EN);//Выключить DMA 	DMA1_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 off ssd1306SendCommand(0xD5); //Set Memory Addressing Mode ssd1306SendCommand(0x80); //00,Horizontal Addressing Mode;01,Vertical ssd1306SendCommand(0xA8); //Set Page Start Address for Page Addressing ssd1306SendCommand(0x3F); //Set COM Output Scan Direction ssd1306SendCommand(0xD3); //set low column address ssd1306SendCommand(0x00); //set high column address ssd1306SendCommand(0x40); //set start line address ssd1306SendCommand(0x8D); //set contrast control register ssd1306SendCommand(0x14); ssd1306SendCommand(0x20); //set segment re-map 0 to 127 ssd1306SendCommand(0x00); //set normal display ssd1306SendCommand(0xA1); //set multiplex ratio(1 to 64) ssd1306SendCommand(0xC8); // ssd1306SendCommand(0xDA); //0xa4,Output follows RAM ssd1306SendCommand(0x12); //set display offset ssd1306SendCommand(0x81); //not offset ssd1306SendCommand(0x8F); //set display clock divide ratio/oscillator frequency ssd1306SendCommand(0xD9); //set divide ratio ssd1306SendCommand(0xF1); //set pre-charge period ssd1306SendCommand(0xDB);  ssd1306SendCommand(0x40); //set com pins hardware configuration ssd1306SendCommand(0xA4); ssd1306SendCommand(0xA6); //set vcomh ssd1306SendCommand(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 PLL	 	uint16_t timeDelay; 	RCC->CR|=RCC_CR_HSEON;//Enable HSE 	for(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 x9 	RCC->CFGR|=RCC_CFGR_PLLSRC_HSE;//PLL sourse:HSE 	RCC->CR|=RCC_CR_PLLON;//Enable PLL 	for(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=2 	RCC->CFGR|=RCC_CFGR_SW_PLL;//Switch to PLL 	while((RCC->CFGR&RCC_CFGR_SWS)!=(0x02<<2)){} 	RCC->CR&=~RCC_CR_HSION;//Disable HSI 	return 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 Гц.

Ссылки

ссылка на оригинал статьи https://habr.com/ru/post/514382/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *