Conexión
Conectaremos la pantalla al microcontrolador a través de la interfaz SPI1 de la siguiente manera:
- VDD-> + 3,3 V
- GND-> Tierra
- SCK -> PA5
- SDA -> PA7 (MOSI)
- RES-> PA1
- CS-> PA2
- DS-> PA3
La transmisión de datos se produce en el flanco ascendente de la señal de sincronización a 1 byte por trama. Las líneas SCK y SDA se utilizan para transferir datos a través de la interfaz SPI, RES: reinicia el controlador de pantalla a un nivel lógico bajo, CS es responsable de seleccionar un dispositivo en el bus SPI a un nivel lógico bajo, DS determina el tipo de datos (comando - 1 / datos - 0) que se transmiten monitor. Dado que no se puede leer nada en la pantalla, no usaremos la salida MISO.
Organización de la memoria del controlador de pantalla
Antes de mostrar algo en la pantalla, debe comprender cómo está organizada la memoria en el controlador ssd1306.
Toda la memoria de gráficos (GDDRAM) es un área de 128 * 64 = 8192 bits = 1KB. El área está dividida en 8 páginas, que se presentan como una colección de 128 segmentos de 8 bits. La memoria se direcciona por número de página y número de segmento, respectivamente.
Con este método de direccionamiento, existe una característica muy desagradable: la imposibilidad de escribir 1 bit de información en la memoria, ya que la grabación ocurre en un segmento (8 bits cada uno). Y dado que para la correcta visualización de un solo píxel en la pantalla, es necesario conocer el estado de los píxeles restantes en el segmento, es recomendable crear un búfer de 1 KB en la memoria del microcontrolador y cargarlo cíclicamente en la memoria de visualización (aquí es donde DMA es útil), respectivamente, haciendo su actualización completa. Usando este método, es posible recalcular la posición de cada bit en la memoria a las coordenadas clásicas x, y. Luego, para mostrar un punto con coordenadas xey, usaremos el siguiente método:
displayBuff[x+(y/8)*SSD1306_WIDTH]|=(1<<(y%8));
Y para borrar el punto
displayBuff[x+(y/8)*SSD1306_WIDTH]&=~(1<<(y%8));
Configuración de SPI
Como se mencionó anteriormente, conectaremos la pantalla a SPI1 del microcontrolador STM32F103C8.
Para la conveniencia de escribir código, declararemos algunas constantes y crearemos una función para inicializar el 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;
}
Encienda el reloj y configure las salidas GPIO, como se muestra en la tabla anterior.
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
A continuación, configuremos SPI en modo maestro y una frecuencia de 18 MHz.
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
Configuremos 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
A continuación, escribiremos una función para enviar datos a través de SPI (hasta ahora sin DMA). El proceso de intercambio de datos es el siguiente:
- Esperando que se publique SPI
- CS = 0
- Enviando datos
- CS = 1
void spiTransmit(uint8_t data)
{
CS_RES;
SPI1->DR = data;
while((SPI1->SR & SPI_SR_BSY))
{};
CS_SET;
}
También escribiremos una función para enviar directamente un comando a la pantalla (Cambiamos la línea DC solo cuando transmitimos un comando, y luego lo devolvemos al estado de "datos", ya que no transmitiremos comandos con tanta frecuencia y no perderemos rendimiento).
void ssd1306SendCommand(uint8_t command)
{
COMMAND;
spiTransmit(command);
DATA;
}
A continuación, nos ocuparemos de las funciones para trabajar directamente con DMA, para ello declararemos un búfer en la memoria del microcontrolador y crearemos funciones para iniciar y detener el envío cíclico de este búfer a la memoria de pantalla.
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;//
}
Inicialización de pantalla y salida de datos
Ahora creemos una función para inicializar la propia pantalla.
void ssd1306Init()
{
}
Primero, configuremos CS, RESET y DC line, y también reiniciemos el controlador de pantalla.
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;//
A continuación, enviaremos una secuencia de comandos para la inicialización (puede obtener más información sobre ellos en la documentación del controlador 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
Creemos funciones para llenar toda la pantalla con el color seleccionado y mostrar un píxel.
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;
}
}
A continuación, en el cuerpo del programa principal, inicializamos el SPI y la pantalla.
RccClockInit();
spi1Init();
ssd1306Init();
La función RccClockInit () está destinada a configurar el reloj del microcontrolador.
Código 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;
}
Llene toda la pantalla con blanco y vea el resultado.
ssd1306RunDisplayUPD();
ssd1306FillDisplay(WHITE);
Dibujemos en la pantalla en una cuadrícula en incrementos de 10 píxeles.
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);
}
}
Las funciones funcionan correctamente, el búfer se escribe continuamente en la memoria del controlador de pantalla, lo que permite utilizar el sistema de coordenadas cartesianas al mostrar primitivas gráficas.
Mostrar frecuencia de actualización
Dado que el búfer se envía cíclicamente a la memoria de la pantalla, será suficiente saber el tiempo que tarda el DMA en completar la transferencia de datos para estimar la frecuencia de actualización de la pantalla. Para la depuración en tiempo real, usaremos la biblioteca EventRecorder de Keil.
Para conocer el momento del final de la transferencia de datos, configuramos la interrupción DMA para finalizar la transferencia.
DMA1_Channel3->CCR|=DMA_CCR1_TCIE;//
DMA1->IFCR&=~DMA_IFCR_CTCIF3;//
NVIC_EnableIRQ(DMA1_Channel3_IRQn);//
Realizaremos un seguimiento del intervalo de tiempo utilizando las funciones EventStart y EventStop.
Obtenemos 0.00400881-0.00377114 = 0.00012767 seg, que corresponde a una frecuencia de actualización de 4.2 KHz. De hecho, la frecuencia no es tan alta, lo que se debe a la inexactitud del método de medición, pero claramente más que el estándar de 60 Hz.