الإتصال
سنقوم بتوصيل الشاشة بالمتحكم الدقيق عبر واجهة SPI1 على النحو التالي:
- VDD-> + 3.3 فولت
- GND-> الأرض
- SCK -> PA5
- SDA -> PA7 (MOSI)
- RES-> PA1
- CS-> PA2
- DS-> PA3

يحدث نقل البيانات على الحافة الصاعدة لإشارة التزامن بمعدل 1 بايت لكل رتل. يتم استخدام خطوط SCK و SDA لنقل البيانات عبر واجهة SPI ، RES - يعيد تشغيل وحدة التحكم في العرض عند مستوى منطقي منخفض ، CS مسؤول عن اختيار جهاز على ناقل SPI بمستوى منطقي منخفض ، ويحدد DS نوع البيانات (الأمر - 1 / البيانات - 0) التي يتم إرسالها عرض. نظرًا لأنه لا يمكن قراءة أي شيء من الشاشة ، فلن نستخدم إخراج MISO.
تنظيم ذاكرة وحدة تحكم العرض
قبل عرض أي شيء على الشاشة ، تحتاج إلى فهم كيفية تنظيم الذاكرة في وحدة التحكم ssd1306.
تبلغ مساحة ذاكرة الرسومات (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.
لتسهيل كتابة التعليمات البرمجية ، سنعلن عن بعض الثوابت وننشئ دالة لتهيئة 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 على الوضع الرئيسي وتردد 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). تكون عملية تبادل البيانات كما يلي:
- في انتظار الإفراج عن SPI
- CS = 0
- إرسال البيانات
- 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);
لنرسم على الشاشة في شبكة بزيادات قدرها 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);
}
}
تعمل الوظائف بشكل صحيح ، تتم كتابة المخزن المؤقت باستمرار في ذاكرة وحدة التحكم في العرض ، مما يسمح باستخدام نظام الإحداثيات الديكارتية عند عرض العناصر الأولية الرسومية.
عرض معدل التحديث
نظرًا لأنه يتم إرسال المخزن المؤقت دوريًا إلى ذاكرة العرض ، فسيكون ذلك كافيًا لمعرفة الوقت الذي يستغرقه DMA لإكمال نقل البيانات لتقدير معدل تحديث العرض. لتصحيح الأخطاء في الوقت الفعلي ، سنستخدم مكتبة Keil's EventRecorder.
من أجل معرفة لحظة انتهاء نقل البيانات ، سنقوم بتهيئة مقاطعة DMA في نهاية عملية النقل.
DMA1_Channel3->CCR|=DMA_CCR1_TCIE;//
DMA1->IFCR&=~DMA_IFCR_CTCIF3;//
NVIC_EnableIRQ(DMA1_Channel3_IRQn);//
سنتعقب الفاصل الزمني باستخدام وظائف EventStart و EventStop.
نحصل على 0.00400881-0.00377114 = 0.00012767 ثانية ، وهو ما يتوافق مع معدل تحديث يبلغ 4.2 كيلو هرتز. في الواقع ، التردد ليس مرتفعًا جدًا ، ويرجع ذلك إلى عدم دقة طريقة القياس ، ولكن من الواضح أنه أكثر من 60 هرتز القياسي.