(题外话)为什么选择寄存器来实现,对于初学者而言我非常建议从寄存器配置开始,主要是因为搞单片机本来就是一项接近于底层硬件的工作,不要嫌麻烦。了解硬件外设工作原理和配置过程会对以后的调试有很大帮助。更容易理解库函数开发。
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复用功能推挽输出模式,输出模式,最大速度50MHz
spitest |= 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/S
spitest &=~(1<<7); //先发送MSB
SPI1->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 PA7
GPIOA->CRL &= 0xFFFF0F0F; //rs,dc
GPIOA->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);//128
OLED_SendCmd(0x8D);//设置电荷泵开关
OLED_SendCmd(0x14);//开
OLED_SendCmd(0x20);//设置模式
OLED_SendCmd(0x00);//设置为水平地址模式
OLED_SendCmd(0xD3);//行偏移命令
OLED_SendCmd(0x2A);//校正参数
OLED_SendCmd(0x21);//设置列地址的起始和结束的位置
OLED_SendCmd(0x00);//0
OLED_SendCmd(0x7F);//127
OLED_SendCmd(0x22);//设置页地址的起始和结束的位置
OLED_SendCmd(0x00);//0
OLED_SendCmd(0x07);//7
OLED_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 , // sp
0x00, 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]48
0x00, 0x00, 0x42, 0x7F, 0x40, 0x00 , // 1
0x00, 0x42, 0x61, 0x51, 0x49, 0x46 , // 2
0x00, 0x21, 0x41, 0x45, 0x4B, 0x31 , // 3
0x00, 0x18, 0x14, 0x12, 0x7F, 0x10 , // 4
0x00, 0x27, 0x45, 0x45, 0x45, 0x39 , // 5
0x00, 0x3C, 0x4A, 0x49, 0x49, 0x30 , // 6
0x00, 0x01, 0x71, 0x09, 0x05, 0x03 , // 7
0x00, 0x36, 0x49, 0x49, 0x49, 0x36 , // 8
0x00, 0x06, 0x49, 0x49, 0x29, 0x1E , // 9
0x00, 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 , // A
0x00, 0x7F, 0x49, 0x49, 0x49, 0x36 , // B
0x00, 0x3E, 0x41, 0x41, 0x41, 0x22 , // C
0x00, 0x7F, 0x41, 0x41, 0x22, 0x1C , // D
0x00, 0x7F, 0x49, 0x49, 0x49, 0x41 , // E
0x00, 0x7F, 0x09, 0x09, 0x09, 0x01 , // F
0x00, 0x3E, 0x41, 0x49, 0x49, 0x7A , // G
0x00, 0x7F, 0x08, 0x08, 0x08, 0x7F , // H
0x00, 0x00, 0x41, 0x7F, 0x41, 0x00 , // I
0x00, 0x20, 0x40, 0x41, 0x3F, 0x01 , // J
0x00, 0x7F, 0x08, 0x14, 0x22, 0x41 , // K
0x00, 0x7F, 0x40, 0x40, 0x40, 0x40 , // L
0x00, 0x7F, 0x02, 0x0C, 0x02, 0x7F , // M
0x00, 0x7F, 0x04, 0x08, 0x10, 0x7F , // N
0x00, 0x3E, 0x41, 0x41, 0x41, 0x3E , // O
0x00, 0x7F, 0x09, 0x09, 0x09, 0x06 , // P
0x00, 0x3E, 0x41, 0x51, 0x21, 0x5E , // Q
0x00, 0x7F, 0x09, 0x19, 0x29, 0x46 , // R
0x00, 0x46, 0x49, 0x49, 0x49, 0x31 , // S
0x00, 0x01, 0x01, 0x7F, 0x01, 0x01 , // T
0x00, 0x3F, 0x40, 0x40, 0x40, 0x3F , // U
0x00, 0x1F, 0x20, 0x40, 0x20, 0x1F , // V
0x00, 0x3F, 0x40, 0x38, 0x40, 0x3F , // W
0x00, 0x63, 0x14, 0x08, 0x14, 0x63 , // X
0x00, 0x07, 0x08, 0x70, 0x08, 0x07 , // Y
0x00, 0x61, 0x51, 0x49, 0x45, 0x43 , // Z
0x00, 0x00, 0x7F, 0x41, 0x41, 0x00 , // [
0x00, 0x55, 0x2A, 0x55, 0x2A, 0x55 , // 55
0x00, 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 , // a
0x00, 0x7F, 0x48, 0x44, 0x44, 0x38 , // b
0x00, 0x38, 0x44, 0x44, 0x44, 0x20 , // c
0x00, 0x38, 0x44, 0x44, 0x48, 0x7F , // d
0x00, 0x38, 0x54, 0x54, 0x54, 0x18 , // e
0x00, 0x08, 0x7E, 0x09, 0x01, 0x02 , // f
0x00, 0x18, 0xA4, 0xA4, 0xA4, 0x7C , // g
0x00, 0x7F, 0x08, 0x04, 0x04, 0x78 , // h
0x00, 0x00, 0x44, 0x7D, 0x40, 0x00 , // i
0x00, 0x40, 0x80, 0x84, 0x7D, 0x00 , // j
0x00, 0x7F, 0x10, 0x28, 0x44, 0x00 , // k
0x00, 0x00, 0x41, 0x7F, 0x40, 0x00 , // l
0x00, 0x7C, 0x04, 0x18, 0x04, 0x78 , // m
0x00, 0x7C, 0x08, 0x04, 0x04, 0x78 , // n
0x00, 0x38, 0x44, 0x44, 0x44, 0x38 , // o
0x00, 0xFC, 0x24, 0x24, 0x24, 0x18 , // p
0x00, 0x18, 0x24, 0x24, 0x18, 0xFC , // q
0x00, 0x7C, 0x08, 0x04, 0x04, 0x08 , // r
0x00, 0x48, 0x54, 0x54, 0x54, 0x20 , // s
0x00, 0x04, 0x3F, 0x44, 0x40, 0x20 , // t
0x00, 0x3C, 0x40, 0x40, 0x20, 0x7C , // u
0x00, 0x1C, 0x20, 0x40, 0x20, 0x1C , // v
0x00, 0x3C, 0x40, 0x30, 0x40, 0x3C , // w
0x00, 0x44, 0x28, 0x10, 0x28, 0x44 , // x
0x00, 0x1C, 0xA0, 0xA0, 0xA0, 0x7C , // y
0x00, 0x44, 0x64, 0x54, 0x4C, 0x44 , // z
0x14, 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)); //9MHz
SysTick->CTRL &= (unsigned int)(~(1<<1)); //不产生下溢中断
}
void Wait_ms(unsigned int t)
{
#define ms_t 9000
SysTick->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();//这里的延时函数我用的是SYSTICK
SPI1_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]);
// }
}
}
结果
;
;
;
;
如果有问题欢迎指正和讨论