(题外话)为什么选择寄存器来实现,对于初学者而言我非常建议从寄存器配置开始,主要是因为搞单片机本来就是一项接近于底层硬件的工作,不要嫌麻烦。了解硬件外设工作原理和配置过程会对以后的调试有很大帮助。更容易理解库函数开发。
1.硬件资源描述
主控 | STM32F103RC |
---|---|
通讯方式 | 硬件SPI1 +DMA1(DMA可选) |
屏幕 | 0.96寸蓝色OLED屏幕 |
下面是屏幕图片
2.OLED屏幕驱动方法说明
屏幕的话可以在那啥宝上买到大概10块钱,现在应该还没涨价吧。至于买IIC协议的还是SPI协议的就看单片机使用习惯了,个人还是喜欢SPI的,相比IIC讲SPI传输速度更快点。至于NSS是接地还是硬件控制,看个人习惯,我选择的是硬件控制。
屏幕与单片机接线如下:
注意:VDD和GND千万不要搞反了不然10块钱白给(我就烧坏了一个)😂
2.1:怎么和屏幕通讯并配置单片机的通讯
- 下面这张图是OLED屏幕驱动芯片手册提供的SPI时序CS# :通讯时一定要保持低电平。
D/C# :就是命令模式选择信号低电平时SDIN(D1)的数据就是命令控制字节。
SCLK :空闲电平高低无关,从这条时钟线可以看出屏幕是在上升沿锁存数据,下降沿允许数据变化。(这点很重要!)
SDIN(D1) :8为数据高位在前。
(第三行的时序就是最后两行的组合)
- 单片机通讯配置
这张图来自STM32F1参考手册
stm32f1可以将SPI配置成4种通讯时序。对比我们的屏幕,只能选择上升沿采样的两种方式。
如下表:
序号 | 控制位 | 描述 |
---|---|---|
1 | CPHA=1 CPOL=1 | 空闲时高电平,第二个边沿采样(上升沿) |
2 | CPHA=0 CPOL=0 | 空闲时低电平,第一个边沿采样(上升沿) |
下面的例程使用的是上表中序号2。
stm32SPI1寄存器配置如下:
void SPI1_Init(void)
{u16 spitest= 0;RCC->APB2ENR |= 1<<2; //IO端口A时钟开启RCC->APB2ENR |= 1<<0; //辅助功能IO时钟开启RCC->APB2ENR |= 1<<12; //SPI1时钟开启GPIOA->CRL &= 0x0F00FFFF; //PA4配置清除(NSS) PA5配置清除(SCK) PA7配置清除(MOSI)GPIOA->CRL |= 0xB0BB0000; //PA4复用功能推挽输出模式,输出模式,最大速度10MHz PA5 & PA7复用功能推挽输出模式,输出模式,最大速度50MHzspitest |= 1<<2; //配置为主设备spitest |= 1<<9; //软件从设备管理spitest |= 1<<8; //NSS输出spitest |= 1<<15; //选择“单线双向”模式spitest |= 1<<14; //输出使能(只发模式)spitest &=~(1<<1); //空闲状态时,SCK保持低电平spitest &=~(1<<0); //数据采样从第一个时钟边沿开始spitest &=~(1<<11); //使用8位数据帧格式进行发送/接收spitest &= 0xFFFFFFC7; //波特率36Mbs= 4.5M/Sspitest &=~(1<<7); //先发送MSBSPI1->CR1 = spitest;SPI1->CR2 |= 1<<1; //TXDMAEN:发送缓冲区DMA使能SPI1->CR1 |=1<<6; //打开SPI设备
}
2.2:DMA
stm32的DMA可是32的一大特色,优点在于它不需要CPU干预,只要DMA被触发就能直接对数据进行搬运。所以使用它可以节省CPU的工作时间用来处理其他任务,在一些大项目中这点尤为突出。详细的配置介绍请参考STM32F1参考手册,这块我就不多言了。直接上代码。
(不想使用DMA的话可以将主函数注释掉的部分取消注释,并在屏幕初始化函数子函数内倒数第二行取消调用DMA函数)
inline static void DMA_OLED_Init(void)
{RCC->AHBENR |= 1<<0; //DMA1时钟开启DMA1_Channel3->CPAR = (u32)(&(SPI1->DR));//CPAR:外设数据寄存器的基地址DMA1_Channel3->CMAR = (u32)(OLED_SRAM); //CMAR:存储器的基地址DMA1_Channel3->CCR |= 0<<14; //MEM2MEM:非存储器到存储器模式DMA1_Channel3->CCR |= 0<<6; //MINC:不执行外设地址增量操作DMA1_Channel3->CCR |= 1<<7; //MINC:存储器地址增量模式DMA1_Channel3->CCR |= 1<<5; //CIRC:循环模式DMA1_Channel3->CCR |= 1<<4; //DIR :从存储器读DMA1_Channel3->CNDTR= 0x400; //1024个字节DMA1_Channel3->CCR |= 1<<0; //EN :通道开启}
2.3:OLED屏幕的初始化及设置
相关的控制命令在屏幕手册中都有,下面要注意的一点就是DC控制要注意时间问题。DC是由GPIO控制的,所以反转速度很快,数据发送相比DC要慢得多。倒数第四行的 Wait_us(10); 就是在调试过程中遇到的问题,从代码上看数据已经发送完成了然后再改变DC命令选择,貌似没问题。但是屏幕一直没反应,用逻辑分析仪抓取时序图后发现该问题,下面附图。例程代码已纠正放心复制。
图中时序自上而下分别是SCK,MISO,DC。明显看出数据还未发送结束DC已经改变。
分析原因:子函数OLED_SendCmd(unsigned char)是判断发送区是否为空,然后条件式的装入。而硬件SPI则需等待上一个数据发送完成才会再处理刚送进来的数据,同样也需要时间。
代码部分如下:
void OLED_Init(void) //初始化函数
{#define OLED_DC PA1#define OLED_RES PA3#define OLED_NSS PA4#define OLED_D0 PA5#define OLED_D1 PA7GPIOA->CRL &= 0xFFFF0F0F; //rs,dcGPIOA->CRL |= 0x00003030; //OLED_RES = 0; //低电平复位Wait_us(100);OLED_RES = 1; //复位结束OLED_DC = 0; //命令模式Wait_us(100);OLED_SendCmd(0xAE);//关闭显示OLED_SendCmd(0xD5);//设置时钟分频因子,震荡频率OLED_SendCmd(0xF0);//[3:0],分频因子;[7:4],震荡频率OLED_SendCmd(0x81);//设置对比度OLED_SendCmd(0x7F);//128OLED_SendCmd(0x8D);//设置电荷泵开关OLED_SendCmd(0x14);//开OLED_SendCmd(0x20);//设置模式OLED_SendCmd(0x00);//设置为水平地址模式OLED_SendCmd(0xD3);//行偏移命令OLED_SendCmd(0x2A);//校正参数OLED_SendCmd(0x21);//设置列地址的起始和结束的位置OLED_SendCmd(0x00);//0OLED_SendCmd(0x7F);//127 OLED_SendCmd(0x22);//设置页地址的起始和结束的位置OLED_SendCmd(0x00);//0OLED_SendCmd(0x07);//7OLED_SendCmd(0xC8);//0xc9上下反置 0xc8正常OLED_SendCmd(0xA1);//0xa0左右反置 0xa1正常OLED_SendCmd(0xA4);//全局显示开启;0xa4正常,0xa5无视命令点亮全屏OLED_SendCmd(0xA6);//设置显示方式;A7,反相显示;A6,正常显示 OLED_SendCmd(0xAF);//开启显示OLED_SendCmd(0x56);Wait_us(10);//OLED_DC = 1; //显示数据模式DMA_OLED_Init();Wait_us(10);//}
OLED_SendCmd(0xA4);//全局显示开启;0xa4正常,0xa5无视命令点亮全屏
这行代码在程序调试的时候可以将参数改为0xa5判断通讯是否正常。
这个是命令发送函数
OLED屏幕刷新一帧需要128*8个字节,每个字节的每一位控制屏幕的一个像素点(位的0或1表示亮灭),刚好是128 *64个位。
static void OLED_SendCmd(unsigned char ctrl_data)
{unsigned char t=200;while(! (SPI1->SR & 1<<1) ) //SPI1->SR & 1<<1=1:发送缓冲为空。{t--;if(t<=0)break;}SPI1->DR = ctrl_data;
}void OLED_Write(unsigned char ASII,unsigned char ye,unsigned char lie)
{char i;for(i=0;i<6;i++){OLED_SRAM[ye][lie+i]=F6X8[(ASII-32)*6+i];}
}
这个是ASII字库,网上能找到太多了。
const unsigned char F6X8[] =
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00 , // sp0x00, 0x00, 0x00, 0x2f, 0x00, 0x00 , // !0x00, 0x00, 0x07, 0x00, 0x07, 0x00 , // "0x00, 0x14, 0x7f, 0x14, 0x7f, 0x14 , // #0x00, 0x24, 0x2a, 0x7f, 0x2a, 0x12 , // $0x00, 0x62, 0x64, 0x08, 0x13, 0x23 , // %0x00, 0x36, 0x49, 0x55, 0x22, 0x50 , // &0x00, 0x00, 0x05, 0x03, 0x00, 0x00 , // '0x00, 0x00, 0x1c, 0x22, 0x41, 0x00 , // (0x00, 0x00, 0x41, 0x22, 0x1c, 0x00 , // )0x00, 0x14, 0x08, 0x3E, 0x08, 0x14 , // *0x00, 0x08, 0x08, 0x3E, 0x08, 0x08 , // +0x00, 0x00, 0x00, 0xA0, 0x60, 0x00 , // ,0x00, 0x08, 0x08, 0x08, 0x08, 0x08 , // -0x00, 0x00, 0x60, 0x60, 0x00, 0x00 , // .0x00, 0x20, 0x10, 0x08, 0x04, 0x02 , // /0x00, 0x3E, 0x51, 0x49, 0x45, 0x3E , // 0 //30[16]480x00, 0x00, 0x42, 0x7F, 0x40, 0x00 , // 10x00, 0x42, 0x61, 0x51, 0x49, 0x46 , // 20x00, 0x21, 0x41, 0x45, 0x4B, 0x31 , // 30x00, 0x18, 0x14, 0x12, 0x7F, 0x10 , // 40x00, 0x27, 0x45, 0x45, 0x45, 0x39 , // 50x00, 0x3C, 0x4A, 0x49, 0x49, 0x30 , // 60x00, 0x01, 0x71, 0x09, 0x05, 0x03 , // 70x00, 0x36, 0x49, 0x49, 0x49, 0x36 , // 80x00, 0x06, 0x49, 0x49, 0x29, 0x1E , // 90x00, 0x00, 0x36, 0x36, 0x00, 0x00 , // :0x00, 0x00, 0x56, 0x36, 0x00, 0x00 , // ;0x00, 0x08, 0x14, 0x22, 0x41, 0x00 , // <0x00, 0x14, 0x14, 0x14, 0x14, 0x14 , // =0x00, 0x00, 0x41, 0x22, 0x14, 0x08 , // >0x00, 0x02, 0x01, 0x51, 0x09, 0x06 , // ?0x00, 0x32, 0x49, 0x59, 0x51, 0x3E , // @0x00, 0x7C, 0x12, 0x11, 0x12, 0x7C , // A0x00, 0x7F, 0x49, 0x49, 0x49, 0x36 , // B0x00, 0x3E, 0x41, 0x41, 0x41, 0x22 , // C0x00, 0x7F, 0x41, 0x41, 0x22, 0x1C , // D0x00, 0x7F, 0x49, 0x49, 0x49, 0x41 , // E0x00, 0x7F, 0x09, 0x09, 0x09, 0x01 , // F0x00, 0x3E, 0x41, 0x49, 0x49, 0x7A , // G0x00, 0x7F, 0x08, 0x08, 0x08, 0x7F , // H0x00, 0x00, 0x41, 0x7F, 0x41, 0x00 , // I0x00, 0x20, 0x40, 0x41, 0x3F, 0x01 , // J0x00, 0x7F, 0x08, 0x14, 0x22, 0x41 , // K0x00, 0x7F, 0x40, 0x40, 0x40, 0x40 , // L0x00, 0x7F, 0x02, 0x0C, 0x02, 0x7F , // M0x00, 0x7F, 0x04, 0x08, 0x10, 0x7F , // N0x00, 0x3E, 0x41, 0x41, 0x41, 0x3E , // O0x00, 0x7F, 0x09, 0x09, 0x09, 0x06 , // P0x00, 0x3E, 0x41, 0x51, 0x21, 0x5E , // Q0x00, 0x7F, 0x09, 0x19, 0x29, 0x46 , // R0x00, 0x46, 0x49, 0x49, 0x49, 0x31 , // S0x00, 0x01, 0x01, 0x7F, 0x01, 0x01 , // T0x00, 0x3F, 0x40, 0x40, 0x40, 0x3F , // U0x00, 0x1F, 0x20, 0x40, 0x20, 0x1F , // V0x00, 0x3F, 0x40, 0x38, 0x40, 0x3F , // W0x00, 0x63, 0x14, 0x08, 0x14, 0x63 , // X0x00, 0x07, 0x08, 0x70, 0x08, 0x07 , // Y0x00, 0x61, 0x51, 0x49, 0x45, 0x43 , // Z0x00, 0x00, 0x7F, 0x41, 0x41, 0x00 , // [0x00, 0x55, 0x2A, 0x55, 0x2A, 0x55 , // 550x00, 0x00, 0x41, 0x41, 0x7F, 0x00 , // ]0x00, 0x04, 0x02, 0x01, 0x02, 0x04 , // ^0x00, 0x40, 0x40, 0x40, 0x40, 0x40 , // _0x00, 0x00, 0x01, 0x02, 0x04, 0x00 , // '0x00, 0x20, 0x54, 0x54, 0x54, 0x78 , // a0x00, 0x7F, 0x48, 0x44, 0x44, 0x38 , // b0x00, 0x38, 0x44, 0x44, 0x44, 0x20 , // c0x00, 0x38, 0x44, 0x44, 0x48, 0x7F , // d0x00, 0x38, 0x54, 0x54, 0x54, 0x18 , // e0x00, 0x08, 0x7E, 0x09, 0x01, 0x02 , // f0x00, 0x18, 0xA4, 0xA4, 0xA4, 0x7C , // g0x00, 0x7F, 0x08, 0x04, 0x04, 0x78 , // h0x00, 0x00, 0x44, 0x7D, 0x40, 0x00 , // i0x00, 0x40, 0x80, 0x84, 0x7D, 0x00 , // j0x00, 0x7F, 0x10, 0x28, 0x44, 0x00 , // k0x00, 0x00, 0x41, 0x7F, 0x40, 0x00 , // l0x00, 0x7C, 0x04, 0x18, 0x04, 0x78 , // m0x00, 0x7C, 0x08, 0x04, 0x04, 0x78 , // n0x00, 0x38, 0x44, 0x44, 0x44, 0x38 , // o0x00, 0xFC, 0x24, 0x24, 0x24, 0x18 , // p0x00, 0x18, 0x24, 0x24, 0x18, 0xFC , // q0x00, 0x7C, 0x08, 0x04, 0x04, 0x08 , // r0x00, 0x48, 0x54, 0x54, 0x54, 0x20 , // s0x00, 0x04, 0x3F, 0x44, 0x40, 0x20 , // t0x00, 0x3C, 0x40, 0x40, 0x20, 0x7C , // u0x00, 0x1C, 0x20, 0x40, 0x20, 0x1C , // v0x00, 0x3C, 0x40, 0x30, 0x40, 0x3C , // w0x00, 0x44, 0x28, 0x10, 0x28, 0x44 , // x0x00, 0x1C, 0xA0, 0xA0, 0xA0, 0x7C , // y0x00, 0x44, 0x64, 0x54, 0x4C, 0x44 , // z0x14, 0x14, 0x14, 0x14, 0x14, 0x14 // horiz lines
};
这个是用到的延时初始化函数和ms级延时函数
inline void Wait_Init(void)
{SysTick->CTRL &= (unsigned int)(~(1<<0)); //关闭SysTick定时器SysTick->CTRL &= (unsigned int)(~(1<<2)); //9MHzSysTick->CTRL &= (unsigned int)(~(1<<1)); //不产生下溢中断
}
void Wait_ms(unsigned int t)
{
#define ms_t 9000SysTick->LOAD = ms_t; //1ms定时SysTick->VAL = 0; //当前值清零SysTick->CTRL |= 1<<0; //打开SysTick定时器while(t){if(SysTick->CTRL & 1<<16) //判断下溢{t--;}}
}
下面是主函数
注意:前面用到的子函数请做好声明
unsigned char OLED_SRAM[8][128]; //图像储存
extern const unsigned char F6X8[];int main()
{Wait_Init();//这里的延时函数我用的是SYSTICKSPI1_Init();OLED_Init();OLED_Write('L',0,0);OLED_Write('i',0,6);OLED_Write('a',0,12);OLED_Write('n',0,18);OLED_Write('g',0,24);while(1){
// for(j=0;j<8;j++)
// {
// for(i=0;i<128;i++)
// OLED_Write(OLED_SRAM[j][i]);
// }}
}
结果
;
;
;
;
如果有问题欢迎指正和讨论