想法来源:笔者之前逛某宝时候,发现有个贴在电脑屏幕四周的灯带,并且灯带会随着电脑屏幕颜色变化而变化,当时就在心中就埋下了DIY的想法,直到后来帮老师做东西,得到了一些边角料,于是下决心干它!
软件
下位机
本次采用WS2812灯带,所以首先要将其驱动点亮。
于是查阅WS2812的数据手册,其驱动时序如下图。
网上有很多驱动方法,有SPI、PWM、定时器等,但是本次笔者采用最简单的IO翻转来驱动它,上面提到其他方法请读者自行搜索查阅。
#ifndef __WS2812_H
#define __WS2812_H
#include "sys.h"
#define LED_Nums 60
#define RGB_LED_HIGH PAout(8)=1
#define RGB_LED_LOW PAout(8)=0
void RGB_LED_Init(void);
void RGB_LED_Write0(void);
void RGB_LED_Write1(void);
void RGB_LED_Reset(void);
void RGB_LED_Write_Byte(uint8_t byte);
void RGB_LED_Write_24Bits(uint8_t red, uint8_t green, uint8_t blue);
void RGB_LED_Show(uint8_t dat[]);
#endif
```c
#include "WS2812.h"
#include "delay.h"
void RGB_LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_8);
}
void RGB_LED_Write0(void)//WS2812写0码
{
RGB_LED_HIGH;
__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
__nop();__nop();
RGB_LED_LOW;
__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();
}
void RGB_LED_Write1(void)//WS2812写1码
{
RGB_LED_HIGH;
__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();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
__nop();__nop();__nop();__nop();__nop();__nop();__nop();
RGB_LED_LOW;
__nop();__nop();
}
void RGB_LED_Reset(void)//WS2812复位操作
{
RGB_LED_LOW;
delay_us(80);
}
void RGB_LED_Write_Byte(uint8_t byte)//WS2812写1字节
{
uint8_t i;
for(i=0;i<8;i++)
{
if(byte&0x80)
{
RGB_LED_Write1();
}
else
{
RGB_LED_Write0();
}
byte <<= 1;
}
}
void RGB_LED_Write_24Bits(uint8_t red, uint8_t green, uint8_t blue)//WS2812写1个像素点
{
RGB_LED_Write_Byte(green);
RGB_LED_Write_Byte(red);
RGB_LED_Write_Byte(blue);
}
void RGB_LED_Show(uint8_t dat[])//WS2812写多个个像素点
{
int16_t i;
RGB_LED_Reset();
for(i=0; i<LED_Nums; i++)
{
RGB_LED_Write_24Bits(dat[3*i], dat[3*i+1], dat[3*i+2]);
}
RGB_LED_HIGH;
}
在源文件中,为了提高STM32的IO翻转速度,采用了位带输出,然后再使用_nop();来调整延时,使其满足WS2812驱动信号的要求。有条件的可以使用逻辑分析仪或示波器来调整延时。这个笔者当时调整了有一会时间,需要点耐心。
本次的电脑屏幕氛围灯,单片机采用串口与PC通信。
接下来是串口接收程序。
void USART1_IRQHandler(void)//串口1中断服务程序
{
static uint8_t k=0,i=0,rebuf[3*LED_Nums+2]={ 0};
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
rebuf[k++] = USART_ReceiveData(USART1);//读取接收到的数据
if(!(rebuf[0]==0xaa))//如果帧头错误,清缓存
{
k = 0;
rebuf[0]=0;
printf("err!!!");
}
if(k == 3*LED_Nums + 2)//数据接收完毕
{
if(rebuf[3*LED_Nums + 1] == 0x55)//判断帧尾
{
for(i = 1; i<3*LED_Nums + 1; i++)
{
LED_data[i-1] = rebuf[i];
show_flag = 1;
}
}
k=0;//清缓存
}
}
}
串口程序,是随便找个串口中断接收的例程修改的,只需修改串口中断服务函数即可。可以看到,本次笔者自定义了一个简单的通信协议,其为0xaa,R,G,B,R,G,B…,0x55。
主程序也很简单,如下。
while(1)
{
if(show_flag==1)
{
RGB_LED_Show(LED_data);
show_flag=0;
printf("show ok!");
}
}
自此,下位机的程序编写完毕。接下来,我们只需要让电脑通过串口向单片机发送相应像素点的颜色值即可。
上位机
本次上位机采用C#开发,这次也算我自学C#编程的一个练习项目。笔者主要是想学成后,可以自己做电脑与单片机的基础通信,其他东西都是需要用时,参考大佬的程序,一点一点修改成自己想要的样子。
上位机主要是串口通信部分和屏幕颜色获取部分。
颜色获取程序如下。
int len = 60; //设置像素点
Byte lightness = Convert.ToByte(textBox1.Text); //获取亮度设置
int x = 0, y = 200; //截图起始点
Bitmap RGB_Led = new Bitmap(1920, 1);
Graphics g = Graphics.FromImage(RGB_Led);
Byte[] fram_data = new Byte[3];
Byte[] GRB_data_last = new Byte[3 * len];
Byte[] GRB_data_curr = new Byte[3 * len];
Byte RGB_r = 0, RGB_g = 0, RGB_b = 0;
fram_data[0] = 0xAA;
fram_data[1] = lightness;
fram_data[2] = 0x55;
//g.CopyFromScreen(new Point(x, y), new Point(0, 0), new Size(1, 120));
g.CopyFromScreen(x, y, 0, 0, new Size(1920, 1));//起始坐标想x,y,取一个1920*1的屏幕图像
for (int i = 0; i < len; i++)
{
Color color_start = RGB_Led.GetPixel(16 * 2 * i, 0); //隔每16点取颜色
RGB_r = color_start.R;
RGB_g = color_start.G;
RGB_b = color_start.B;
GRB_data_last[3 * i] = RGB_r;
GRB_data_last[3 * i + 1] = RGB_g;
GRB_data_last[3 * i + 2] = RGB_b;
}
serialPort1.Write(fram_data, 0, 1);
//serialPort1.Write(fram_data, 1, 1);
serialPort1.Write(GRB_data_curr, 0, 3 * len);
serialPort1.Write(fram_data, 2, 1);
程序的思路很简单,就是定时获取屏幕固定像素点的颜色的RGB分量,然后按之前设计通信协议发送给单片机。具体的也不细讲了,上位机还有很多没有完善的地方,比如现在实现获取一个像素点,更好的是获取一块区域的像素点的颜色平均值,另外为了灯条显示效果更加顺滑,对获取到颜色数据也需要一些算法来处理。
不过,网上专门的取色软件Prismatik。它是我做完这个练习项目后才看到的。我也下载该软件测试过,发现其有一点不稳定,串口数据经常中断。不过,看其他人配合Arduino做出来效果还不错,不知道是我哪里出现了问题。
上位机运行结果如下。
硬件
硬件部分,我打了一块小驱动板,顺便也是为了验证自己的一些想法,方便以后设计电路的时候参考。
一是,USB转串口芯片采用CH340E,它不需要外部晶振,体积也很小。
二是,串口和SWD下载接口共用Type-C接口,通过拨码开关进行选择。
整体演示效果
总结一下,最后的效果并不是很理想,程序算法还需要优化,有机会我也再尝试Prismatik软件,因为它的效果确实可人。