一个可以用来提示的磁吸方块灯
前言
此次的磁吸小灯相当于基于之前学习的内容的融合巩固,所做的一个基于CH549单片机的小制作。这个磁吸小灯用到了外部中断,OLED屏幕(SSD1306)和WS2812B灯带的相关知识。
新手打基础可以看论坛上的《以 CH549 为例的 51 教程》,想要详细了解WS2812B灯带可以关注我的上一篇帖子《基于CH549单片机用旋转电位器对WS2812B灯带的控制学习》。
接线
OLED引脚接线表:
SSD1036 | CH549 | 功能 |
CS | P1.4 | CS片选 |
RST | P3.5 | LED复位 |
DC | P2.7 | 数据/命令控制 |
D0 | P1.7 | SCLK时钟信号 |
D1 | P1.5 | MOSI数据 |
WS2812B和开关
单元 | CH549 |
WS2812B | P2.2 |
开关 | P3.2 |
具体程序 main.c
#include <CH549_sdcc.h> //ch549的头文件,其中定义了单片机的一些特殊功能寄存器
#include <CH549_OLED.h> //其中有驱动屏幕使用的函数
#include <CH549_BMP.h> //用于显示图片的头文件
#include <CH549_DEBUG.h> //CH549官方提供库的头文件,定义了一些关于主频,延时,串口设置,看门口,赋值设置等基础函数
#include <CH549_SPI.h> //CH549官方提供库的头文件,定义了一些关于SPI初始化,传输数据等函数
/********************************************************************
* TIPS:
* oled引脚接线表
* CS P1_4 //CS片选
* RST P3_5 //LED复位
* DC P2_7 //数据/命令控制
* D0 P1_7 //SCLK时钟信号
* D1 P1_5 //MOSI数据
*********************************************************************/
#define WS2812 P2_2 //设定p2.2口作为灯带的输入口
#define key1 P3_2 //将单片机的P3.2端口定义为key1
#define _nop() __asm NOP __endasm //将nop指令定义为宏
//1码,高电平850ns 低电平400ns 误差正负150ns
#define RGB_1() do{WS2812 = 1;\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
WS2812 = 0;}while(0) //此处加do while循环可以将宏定义的部分可以被识别为语句,方便纠错
//0码,高电平400ns 低电平850ns 误差正负150ns
#define RGB_0() do{WS2812 = 1;\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
WS2812 = 0;\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
_nop();_nop();_nop();_nop();_nop();}while(0)
#define numLEDs 11 //灯的个数
enum roll_frame
{
FRAME_5,
FRAME_64,
FRAME_128,
FRAME_256,
FRAME_3,
FRAME_4,
FRAME_25,
FRAME_2,
};
enum page_num
{
PAGE0,
PAGE1,
PAGE2,
PAGE3,
PAGE4,
PAGE5,
PAGE6,
PAGE7,
};
//发送24位数据
void Send_2811_24bits(unsigned char G8,unsigned char R8,unsigned char B8)
{
char n = 0;
//发送G8位
for(n=0;n<8;n++)
{
if(G8&0x80)
{
RGB_1();
}
else
{
RGB_0();
}
G8<<=1;
}
//发送R8位
for(n=0;n<8;n++)
{
if(R8&0x80)
{
RGB_1();
}
else
{
RGB_0();
}
R8<<=1;
}
//发送B8位
for(n=0;n<8;n++)
{
if(B8&0x80)
{
RGB_1();
}
else
{
RGB_0();
}
B8<<=1;
}
}
//复位码
void RGB_Rst()
{
WS2812 = 0;
mDelayuS( 50 );
}
//直接将颜色数据赋予灯带
void Set_Light(unsigned char x,unsigned char y,unsigned char z)
{
unsigned char i;
for ( i = 0; i < numLEDs; i++)
{
Send_2811_24bits( x , y , z );//发送显示
}
}
/******************************************************************************************************
* @brief OLED水平滚动显示
* @param start_page : PAGE0~PAGE7
* @param end_page : PAGE0~PAGE7 (end_page必须大于start_page)
* @param frame : 0~7
* @param dir : 0 右滚 1 左滚
* @retval 无
*******************************************************************************************************/
void oled_hor_scroll(enum page_num start_page, enum page_num end_page, enum roll_frame frame, UINT8 dir)
{
OLED_WR_Byte(0x2E, OLED_CMD); /* 停止滚动 调用后,RAM数据需要重新写入 */
OLED_WR_Byte(dir ? 0x26 : 0x27, OLED_CMD); /* 0,向右滚动 1,向左移动 */
OLED_WR_Byte(0x00, OLED_CMD); /* 发送空字节 设置为0x00即可 */
OLED_WR_Byte(start_page & 0x07, OLED_CMD); /* 起始页地址 */
OLED_WR_Byte(frame & 0x07, OLED_CMD); /* 设置滚动步长的时间间隔, 帧为单位 */
OLED_WR_Byte(end_page & 0x07, OLED_CMD); /* 终止页地址 */
OLED_WR_Byte(0x00, OLED_CMD); /* 发送空字节 设置为0x00即可 */
OLED_WR_Byte(0xFF, OLED_CMD); /* 发送空字节 设置为0xFF即可 */
OLED_WR_Byte(0x2F, OLED_CMD); /* 启动滚动 禁止对RAM访问,改变水平滚动配置参数 */
}
/********************************************************************
* 函 数 名 : Int0Init()
* 函数功能 : 设置外部中断0
* 输 入 : 无
* 输 出 : 无
*********************************************************************/
void Int0Init()
{
//设置INT0
IT0=1; //跳变沿触发方式(下降沿)
EX0=1; //打开INT0的中断允许。
EA=1; //打开总中断
}
volatile UINT8 F; //水平滚动指令需要无限循环,给与屏幕赋值会干扰水平滚动,设定F,仅在第一次给屏幕赋值,便置0跳过赋值指令
volatile UINT8 led = 0; //定义灯的四种情况
/********************************************************************
* 函 数 名 : main
* 函数功能 : 主函数
* 输 入 : 无
* 输 出 : 无
*********************************************************************/
void main(void)
{
CfgFsys(); //CH549时钟选择配置
mDelaymS(20);
Int0Init(); //对外部中断0(P3.2)初始化
SPIMasterModeSet(3); //SPI主机模式设置,模式3
SPI_CK_SET(12); //设置spi sclk 时钟信号为12分频
OLED_Init(); //初始化OLED
OLED_Clear(); //将oled屏幕上内容清除
setFontSize(16); //设置文字大小
F = 1;
/* 主循环 */
while (1)
{
switch (led)
{
case 0:if(F == 1) {F=0;OLED_Clear();OLED_ShowString(0,2,"RESTING");} //接电后初始为熄灭状态,且OLED上显示为RESTING
Set_Light( 0X00 , 0X00 , 0X00 );mDelaymS(30);break;
case 1:if(F == 1) {F=0;OLED_Clear();OLED_ShowString(0,2,"BACK OFF");} //按第一下按钮为红灯,且OLED上显示为BACK OFF
Set_Light( 0X00 , 0Xff , 0X00 );mDelaymS(30);break;
case 2:if(F == 1) {F=0;OLED_Clear();OLED_ShowString(0,2,"KNOCK KNOCK");} //按第二下按钮为黄灯,且OLED上显示为KNOCK KNOCK
Set_Light( 0Xff , 0Xff , 0X00 );mDelaymS(30);break;
case 3:if(F == 1) {F=0;OLED_Clear();OLED_ShowString(0,2,"FREEDOM!!");} //按第三下按钮为绿灯,且OLED上显示为FREEDOM!!
Set_Light( 0Xff , 0X00 , 0X00 );mDelaymS(30);break;
default:led = 0;if(F == 1) {F=0;OLED_Clear();OLED_ShowString(0,2,"RESTING");} //按第四下按钮为灯熄灭,且OLED上显示为RESTING,相当于回到循环起点重新开始
Set_Light( 0X00 , 0X00 , 0X00 );mDelaymS(30);break;
}
oled_hor_scroll(PAGE0, PAGE7, FRAME_5, 0); //OLED上的句子水平滚动
delay_ms(500);
}
}
/*******************************************************************
* 函 数 名 : Int0() __interrupt 0
* 函数功能 : 外部中断0的中断函数
* 输 入 : 无
* 输 出 : 无
* TIPS:1.Int0 可以修改成任意函数名
* 2.举例:外部中断1 void Int1() __interrupt 2
* 3.外部中断0 中断号0
* 定时器0中断 中断号1
* 外部中断1 中断号2
* 定时器1中断 中断号3
*******************************************************************/
void Int0() __interrupt 0 //外部中断0的中断函数
{
delay_ms(120); //延时消抖
IE0=0;
if(key1==0)
{
led++; //LED计数加1
F = 1; //按下按钮便重置给屏幕赋值的指令
}
}
整个代码分为四个部分
其中<CH549_sdcc.h>、<CH549_DEBUG.h>、<CH549_BMP.h>、<CH549_OLED.h>、<CH549_SPI.h>均为CH549官方提供库的头文件;
第一部分:WS2812B控制部分
#define WS2812 P2_2 //设定p2.2口作为灯带的输入口
#define _nop() __asm NOP __endasm //将nop指令定义为宏
//1码,高电平850ns 低电平400ns 误差正负150ns
#define RGB_1() do{WS2812 = 1;\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
WS2812 = 0;}while(0) //此处加do while循环可以将宏定义的部分可以被识别为语句,方便纠错
//0码,高电平400ns 低电平850ns 误差正负150ns
#define RGB_0() do{WS2812 = 1;\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
WS2812 = 0;\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
_nop();_nop();_nop();_nop();_nop();}while(0)
#define numLEDs 11 //灯的个数
//发送24位数据
void Send_2811_24bits(unsigned char G8,unsigned char R8,unsigned char B8)
{
char n = 0;
//发送G8位
for(n=0;n<8;n++)
{
if(G8&0x80)
{
RGB_1();
}
else
{
RGB_0();
}
G8<<=1;
}
//发送R8位
for(n=0;n<8;n++)
{
if(R8&0x80)
{
RGB_1();
}
else
{
RGB_0();
}
R8<<=1;
}
//发送B8位
for(n=0;n<8;n++)
{
if(B8&0x80)
{
RGB_1();
}
else
{
RGB_0();
}
B8<<=1;
}
}
//复位码
void RGB_Rst()
{
WS2812 = 0;
mDelayuS( 50 );
}
//直接将颜色数据赋予灯带
void Set_Light(unsigned char x,unsigned char y,unsigned char z)
{
unsigned char i;
for ( i = 0; i < numLEDs; i++)
{
Send_2811_24bits( x , y , z );//发送显示
}
}
该部分作为WS2812B控制部分,基本上完全套用上一篇文章的WS2812B控制部分,略微根据实际情况调节0码和1码的时序。
第二部分:OLED屏幕控制部分
enum roll_frame
{
FRAME_5,
FRAME_64,
FRAME_128,
FRAME_256,
FRAME_3,
FRAME_4,
FRAME_25,
FRAME_2,
};
enum page_num
{
PAGE0,
PAGE1,
PAGE2,
PAGE3,
PAGE4,
PAGE5,
PAGE6,
PAGE7,
};
void oled_hor_scroll(enum page_num start_page, enum page_num end_page, enum roll_frame frame, UINT8 dir)
{
OLED_WR_Byte(0x2E, OLED_CMD); /* 停止滚动 调用后,RAM数据需要重新写入 */
OLED_WR_Byte(dir ? 0x26 : 0x27, OLED_CMD); /* 0,向右滚动 1,向左移动 */
OLED_WR_Byte(0x00, OLED_CMD); /* 发送空字节 设置为0x00即可 */
OLED_WR_Byte(start_page & 0x07, OLED_CMD); /* 起始页地址 */
OLED_WR_Byte(frame & 0x07, OLED_CMD); /* 设置滚动步长的时间间隔, 帧为单位 */
OLED_WR_Byte(end_page & 0x07, OLED_CMD); /* 终止页地址 */
OLED_WR_Byte(0x00, OLED_CMD); /* 发送空字节 设置为0x00即可 */
OLED_WR_Byte(0xFF, OLED_CMD); /* 发送空字节 设置为0xFF即可 */
OLED_WR_Byte(0x2F, OLED_CMD); /* 启动滚动 禁止对RAM访问,改变水平滚动配置参数 */
}
这个OLED屏幕的元件名为SSD1306,该屏幕自带驱动程序来操作字幕的水平滚动,可以不依靠软件进行水平滚动。看了下手册中对水平滚动设置的描述可以发现该命令由连续的字节组成,用于设置水平滚动参数和决定滚动的起始页、终止页和滚动速度。并且在使用该命令时,必须先发送2Eh命令停止滚动。否则,GRAM内容有可能被破坏。SSD1306水平滚动设计为128列滚动。

大家可以从手册第9章命名看到COMMANDTABLE。跳转到该页可以发现
第三部分:按钮实现的外部中断
#define key1 P3_2 //将单片机的P3.2端口定义为key1
void Int0Init()
{
//设置INT0
IT0=1; //跳变沿触发方式(下降沿)
EX0=1; //打开INT0的中断允许。
EA=1; //打开总中断
}
volatile UINT8 F; //水平滚动指令需要无限循环,给与屏幕赋值会干扰水平滚动,设定F,仅在第一次给屏幕赋值,便置0跳过赋值指令
volatile UINT8 led = 0; //定义灯的四种情况
void Int0() __interrupt 0 //外部中断0的中断函数
{
delay_ms(120); //延时消抖
IE0=0;
if(key1==0)
{
led++; //LED计数加1
F = 1; //按下按钮便重置给屏幕赋值的指令
}
}
相当于每次按钮按下,将led的计数+1,同时将数据赋予的信号重置。
对于之前所说的51教程其中有相对不完善的地方,即该按钮模块的引脚相对严格,即如图所示

他的底部两端有两个银色贴片,直的接输入,弯的接地。
第四部分:主程序
void main(void)
{
CfgFsys(); //CH549时钟选择配置
mDelaymS(20);
Int0Init(); //对外部中断0(P3.2)初始化
SPIMasterModeSet(3); //SPI主机模式设置,模式3
SPI_CK_SET(12); //设置spi sclk 时钟信号为12分频
OLED_Init(); //初始化OLED
OLED_Clear(); //将oled屏幕上内容清除
setFontSize(16); //设置文字大小
F = 1;
/* 主循环 */
while (1)
{
switch (led)
{
case 0:if(F == 1) {F=0;OLED_Clear();OLED_ShowString(0,2,"RESTING");} //接电后初始为熄灭状态,且OLED上显示为RESTING
Set_Light( 0X00 , 0X00 , 0X00 );mDelaymS(30);break;
case 1:if(F == 1) {F=0;OLED_Clear();OLED_ShowString(0,2,"BACK OFF");} //按第一下按钮为红灯,且OLED上显示为BACK OFF
Set_Light( 0X00 , 0Xff , 0X00 );mDelaymS(30);break;
case 2:if(F == 1) {F=0;OLED_Clear();OLED_ShowString(0,2,"KNOCK KNOCK");} //按第二下按钮为黄灯,且OLED上显示为KNOCK KNOCK
Set_Light( 0Xff , 0Xff , 0X00 );mDelaymS(30);break;
case 3:if(F == 1) {F=0;OLED_Clear();OLED_ShowString(0,2,"FREEDOM!!");} //按第三下按钮为绿灯,且OLED上显示为FREEDOM!!
Set_Light( 0Xff , 0X00 , 0X00 );mDelaymS(30);break;
default:led = 0;if(F == 1) {F=0;OLED_Clear();OLED_ShowString(0,2,"RESTING");} //按第四下按钮为灯熄灭,且OLED上显示为RESTING,相当于回到循环起点重新开始
Set_Light( 0X00 , 0X00 , 0X00 );mDelaymS(30);break;
}
oled_hor_scroll(PAGE0, PAGE7, FRAME_5, 0); //OLED上的句子水平滚动
delay_ms(500);
}
}
- 先初始化时钟、外部中断0、操控OLED屏幕的SPI和OLED屏幕;
- 然后把OLED屏幕上的内容清除、预先设置文字大小,并且把赋值信号置1进入循环;
- 根据当前led的数值分情况判断,进入相应的情况之后判断赋值信号F是否为,如果是1就清除当前屏幕中的字幕再重新赋予当前情况的字幕,同时将赋值信号置0使得之后的无限循环不再赋值打断字幕的水平滚动,之后再对应字幕灯带亮相应的颜色,三种颜色为一周期,第四下重置led回归0。
成品展示图





具体演示可以看B站verimake上传的视频:传送门
这次的文件我也放到了gitee仓库上,文件名区别于上一篇的采用:SDCC-LIGHT-OLED
欢迎大家参考!!