当前位置: 首页 > article >正文

基于51单片机和WS2812B彩色灯带的流水灯

目录

  • 系列文章目录
  • 前言
  • 一、效果展示
  • 二、原理分析
  • 三、各模块代码
  • 四、主函数
  • 总结

系列文章目录


前言

用彩色灯带按自己想法DIY一条流水灯,谁不喜欢呢?

所用单片机:STC15W204S
(也可以用其他1T单片机,例如,STC8G系列、STC32G系列)

用到的外设:WS2812B彩色灯带(60个灯珠)

效果查看/操作演示:B站搜索“甘腾胜”或“gantengsheng”查看。
源代码下载:B站对应视频的简介有工程文件下载链接。

一、效果展示

在这里插入图片描述

二、原理分析

1、用什么MCU
如果只是驱动一条灯带,并且灯珠的数量不多(少于或等于77个)的话,用STC15W204S是比较合适的。因为只需用到一个引脚,用引脚多的单片机会比较浪费,STC15W204S总共有8个引脚,除了供电的VCC和GND,就只有P30~P33和P54~P55共6个IO口引脚。并且STC15W204S是1T单片机,适合驱动灯带。

我买的板子PCB板上印错了,P54、P55印成了P34、P35。

在这里插入图片描述

2、显示缓存
每个灯珠需要写入24Bit(3个字节)控制显示的颜色,共有Quantity个灯珠的话,要用3*Quantity个字节作为WS2812B彩色灯带的显示缓存。

必须要用到显示缓存,不能只对其中一个灯珠的WS2812B芯片写入数据,因为灯珠内的WS2812B芯片接收的数据是靠上一个灯珠内的WS2812B芯片传递过来的。

更显灯带显示的时候要通过数据线写入3*Quantity个字节。

3、WS2812B芯片
WS2812B芯片要求我们要按G(绿)、R(红)、B(蓝)的顺序发送数据,并且每个字节要高位先发。具体的时序要求可以看一下其他博主的介绍。我们可以用空操作 nop(); 来延时,分频系数为1,频率为24.000MHz的话,一个 nop(); 执行的时长就是1/24us。

为了方便向灯珠中的WS2812B芯片写入数据,缓存数组中的数据每3个字节为一组,每组的3个字节按G、R、B的顺序存放,这样的话,需要更新显示时,就按照索引的顺序,将3*Quantity个字节发送给灯带即可。修改缓存的时候每一组也是需要按照G、R、B的顺序赋值。

第一个灯珠接收了3个字节的数据后,会将后面的数据直接传给第二个灯珠,以此类推。

4、不同灯珠数量的灯带
已经考虑到了这种情况,所以代码中作了相应的处理,修改宏定义中Quantity对应的数值即可,经过测试,如果用STC15W204S的话,1~77个灯都是可以驱动的。

5、亮度的调整
G、R、B的值越大,就表示该种颜色越亮,所以想要调整亮度的话,修改G、R、B的值就行了。

6、函数的设计
可以根据自己的需求设计对应的函数。我觉得以下函数是必须的,其他的可以自由发挥。

①写一个字节的函数
②显示缓存数据清零的函数
③更改一个灯珠对应的三个字节的缓存的函数
④更新显示的函数(将缓存数组的数据全部发送给灯带)

三、各模块代码

四、主函数

main.c

/*
by甘腾胜@20250131
效果查看/操作演示:可以在B站搜索“甘腾胜”或“gantengsheng”查看
开发环境:Keil C51
单片机:STC15W204S
分频系数及频率:1T@24.000MHz
外设:WS2812B彩色灯带(60个灯珠)
注意:
(1)驱动WS2812B彩色点阵屏最好用1T的单片机,想用传统的12T单片机,则需要用较高频率的晶振,并且要使能6T(双倍速)模式
(2)如果使用STC15W204S这款单片机,并且灯珠数量小于等于77的话,根据实际灯珠数量修改宏定义中Quantity对应的数值即可
*/

#include <STC15F2K60S2.H>	//包含寄存器定义的头文件
#include <INTRINS.H>	//需要用空操作 _nop_(); 来延时

//Quantity:灯珠的数量,如果用STC15W204S,则范围是:1~77(data不能超过256个字节,实测超过77就不能正常显示了)
//想控制更加多的灯珠,需要更换为有较多片外RAM的单片机,一个灯珠需要3个字节的RAM缓存
//并且把缓存数组的idata改成xdata
#define Quantity 60

#define Duration1 15	//呼吸灯延时的时长,更改数值可以改变呼吸灯的快慢,数值越大,呼吸灯越慢
#define Duration2 25	//渐变灯延时的时长,更改数值可以改变渐变灯的快慢,数值越大,渐变灯越慢
#define Duration3 30	//流星流水灯延时的时长,更改数值可以改变移动的速度,数值越大,速度越慢

//引脚定义
//有6个IO口,P30~P33和P54、P55
//我买的板子PCB板上印错了,P54、P55印成了P34、P35
sbit WS2812B_Din=P5^4;

//用3*Quantity个字节作为WS2812B彩色灯带的显示缓存,共有Quantity个灯珠,每个灯珠需要写入24Bit(3个字节)控制显示的颜色
//WS2812B芯片要求按G、R、B的顺序发送数据,并且每个字节要高位先发
//缓存数组中每三个字节为一组,每一组分别对应一个灯珠的G(绿)、R(红)、B(蓝)三原色
//第一组对应数据传输方向的第一个灯珠,第二组对应第二个灯珠,以此类推
//STC15W204S只有256K的片内RAM空间,没有片外RAM空间,只能用idata修饰,不能用xdata修饰,否则灯带不能正常显示
//STC15W204S最多只能定义七十多个灯珠的缓存,想要控制更加多的灯,需要用有较多片外RAM空间的单片机,并且要把idata改成xdata
//想更改灯带的显示,先更改此显示缓存中的数据,再通过函数WS2812B_UpdateDisplay将显示缓存的数据写入每个灯珠的WS2812B芯片内
unsigned char idata WS2812B_Buffer[3*Quantity];

//用来实现流水灯退出的效果
unsigned char code Table0[]={
0,0,0,	//无显示
};

//通过查表的方法显示流水灯,三个为一组,一组对应一个灯珠的R、G、B的值
unsigned char code Table1[]={	//流星流水灯
255,0,0,191,0,0,127,0,0,95,0,0,63,0,0,47,0,0,31,0,0,23,0,0,	//红
15,0,0,11,0,0,7,0,0,5,0,0,3,0,0,2,0,0,1,0,0,0,0,0,

0,255,0,0,191,0,0,127,0,0,95,0,0,63,0,0,47,0,0,31,0,0,23,0,	//绿
0,15,0,0,11,0,0,7,0,0,5,0,0,3,0,0,2,0,0,1,0,0,0,0,

0,0,255,0,0,191,0,0,127,0,0,95,0,0,63,0,0,47,0,0,31,0,0,23,	//蓝
0,0,15,0,0,11,0,0,7,0,0,5,0,0,3,0,0,2,0,0,1,0,0,0,

255,255,0,191,191,0,127,127,0,95,95,0,63,63,0,47,47,0,31,31,0,23,23,0,	//黄
15,15,0,11,11,0,7,7,0,5,5,0,3,3,0,2,2,0,1,1,0,0,0,0,

255,0,255,191,0,191,127,0,127,95,0,95,63,0,63,47,0,47,31,0,31,23,0,23,	//紫
15,0,15,11,0,11,7,0,7,5,0,5,3,0,3,2,0,2,1,0,1,0,0,0,

0,255,255,0,191,191,0,127,127,0,95,95,0,63,63,0,47,47,0,31,31,0,23,23,	//青
0,15,15,0,11,11,0,7,7,0,5,5,0,3,3,0,2,2,0,1,1,0,0,0,

255,255,255,191,191,191,127,127,127,95,95,95,63,63,63,47,47,47,31,31,31,23,23,23,	//白
15,15,15,11,11,11,7,7,7,5,5,5,3,3,3,2,2,2,1,1,1,0,0,0,
};

unsigned char code Table2[]={	//顺向逆向流星流水灯叠加
255,191,127,95,63,47,31,23,15,11,7,5,3,2,1,0,
};

/**
  * @brief  延时函数,1T@24.000MHz调用可延时约xms毫秒
  * @param  xms 要延时的时间,范围:0~65535
  * @retval 无
  */
void Delay(unsigned int xms)
{
	unsigned char i,j;
	while(xms--)
	{
		i=24;
		j=85;
		do
		{
			while(--j);
		}while(--i);
	}
}

/**
  * @brief  WS2812B彩带私有延时函数,1T@24.000MHz调用可延时约100us
  * @param  无
  * @retval 无
  */
void WS2812B_Delay100us(void)
{
	unsigned char i,j;
	i=3;
	j=82;
	do
	{
		while(--j);
	}while(--i);
}

/**
* @brief  WS2812B彩色灯带清空显示缓存(需要调用函数WS2812B_UpdateDisplay才能更新灯带的显示)
  * @param  无
  * @retval 无
  */
void WS2812B_Clear(void)
{
	unsigned int i;	//如果用unsigned char而不用unsigned int的话,Quantity大于85之后,此函数就会陷入死循环
					//用unsigned int定义是为了方便拓展显示更多的灯珠
	for(i=0;i<3*Quantity;i++){WS2812B_Buffer[i]=0;}
}

/**
  * @brief  WS2812B彩色灯带按灯带数据传输方向移动显示数组Array中的数据(需要调用函数WS2812B_UpdateDisplay才能更新灯带的显示)
  * @param  *Array Array为传递过来的指针(即内存地址),数组名就是数组的首地址
  * @param  Offset 偏移量,范围:0~Array数组数据总数/3-1
			Array中的数据三个为一组,移动后第一个灯珠显示第Offset组的数据(第0组为数组Array中的前三个数据)
  * @retval 无
  */
void WS2812B_MoveInOrder(unsigned char *Array,unsigned int Offset)
	{	//Offset用unsigned int定义是为了方便拓展显示更长的流水灯
	unsigned int i;
	
	//缓存数组中的数据按数组索引增大的方向移动3个字节
	for(i=0;i<Quantity-1;i++)
	{
		WS2812B_Buffer[3*(Quantity-i)-3]=WS2812B_Buffer[3*(Quantity-i)-6];
		WS2812B_Buffer[3*(Quantity-i)-2]=WS2812B_Buffer[3*(Quantity-i)-5];
		WS2812B_Buffer[3*(Quantity-i)-1]=WS2812B_Buffer[3*(Quantity-i)-4];
	}
	
	//移动后,对第一个灯珠对应的三个字节进行赋值,从而实现流水灯的效果
	Array+=Offset*3;	//指针方面的知识不清楚的先去了解一下
	WS2812B_Buffer[1]=*Array;	//WS2812B_Buffer中按G、R、B存放,
	WS2812B_Buffer[0]=*(Array+1);	//Array数组中按R、G、B存放,
	WS2812B_Buffer[2]=*(Array+2);	//赋值的时候需要调一下顺序
}

/**
  * @brief  WS2812B彩色灯带按灯带数据传输方向的反方向移动显示数组Array中的数据(需要调用函数WS2812B_UpdateDisplay才能更新灯带的显示)
  * @param  *Array Array为传递过来的指针(即内存地址),数组名就是数组的首地址
  * @param  Offset 偏移量,范围:0~Array数组数据总数/3-1
			Array中的数据三个为一组,移动后最后一个灯珠显示第Offset组的数据(第0组为数组Array中的前三个数据)
  * @retval 无
  */
void WS2812B_MoveInReverseOrder(unsigned char *Array,unsigned int Offset)
{
	unsigned int i;
	
	//缓存数组中的数据按数组索引减小的方向移动3个字节
	for(i=0;i<Quantity-1;i++)
	{
		WS2812B_Buffer[3*i  ]=WS2812B_Buffer[3*i  +3];
		WS2812B_Buffer[3*i+1]=WS2812B_Buffer[3*i+1+3];
		WS2812B_Buffer[3*i+2]=WS2812B_Buffer[3*i+2+3];
	}
	
	//移动后,对最后一个灯珠对应的三个字节进行赋值,从而实现流水灯的效果
	Array+=Offset*3;
	WS2812B_Buffer[3*Quantity-2]=*Array;	//WS2812B_Buffer中按G、R、B存放,
	WS2812B_Buffer[3*Quantity-3]=*(Array+1);	//Array数组中按R、G、B存放,
	WS2812B_Buffer[3*Quantity-1]=*(Array+2);	//赋值的时候需要调一下顺序
}

/**
  * @brief  WS2812B彩色灯带设置一个灯珠的缓存(需要调用函数WS2812B_UpdateDisplay才能更新灯带的显示)
  * @param  Position 要设置的位置,范围:0~Quantity-1,对应第1个到第Quantity个灯珠
  * @param  R 红(Red),范围:0~255
  * @param  G 绿(Green),范围:0~255
  * @param  B 蓝(Blue),范围:0~255
  * @retval 无
  */
void WS2812B_SetBuffer(unsigned int Position,unsigned char R,unsigned char G,unsigned char B)
{
	//缓存数组中,每三个为一组,每一组按G、R、B的顺序存放,
	//这样方便数据的写入,通过数据线按数组索引顺序直接写入缓存数组的3*Quantity个字节的数据就行了
	WS2812B_Buffer[3*Position  ]=G;
	WS2812B_Buffer[3*Position+1]=R;
	WS2812B_Buffer[3*Position+2]=B;
}

/**
  * @brief  WS2812B彩色灯带写入一个字节
  * @brief  频率要求:1T@24.000MHz,如果要换其他频率,则需要调整“_nop_();”的数量
  * @param  Byte 要写入的字节
  * @retval 无
  */
void WS2812B_WriteByte(unsigned char Byte)
{
	unsigned char i;
	
//	EA=0;	//关闭总中断(如果用到中断的话)(时序要求严格,不能被打断),并要求中断函数执行的时间不能太长
			//时间太长,相当于发送了重置信号

	for(i=0;i<8;i++)
	{
		if(Byte&(0x80>>i))	//写1(高位先发)
		{
			WS2812B_Din=1;	//根据高电平的时长确定发送的是1还是0,跟DS18B20类似
			//用空操作进行延时,单片机使用不同的频率,就需要不一样“_nop_();”的数量
			_nop_();_nop_();_nop_();_nop_();_nop_();
			_nop_();_nop_();_nop_();_nop_();_nop_();
			WS2812B_Din=0;	//经测试,数据线拉低后不用加延时
		}
		else	//写0
		{
			WS2812B_Din=1;
			_nop_();_nop_();_nop_();_nop_();_nop_();
			WS2812B_Din=0;	//经测试,数据线拉低后不用加延时
		}
	}
	
//	EA=1;	//开启总中断

}

/**
  * @brief  WS2812B彩色灯带更新显示,将显示缓存数组WS2812B_Buffer的数据写入到灯珠的WS2812B芯片内
  * @param  无
  * @retval 无
  */
void WS2812B_UpdateDisplay(void)
{
	unsigned int i;
	
	for(i=0;i<3*Quantity;i++){WS2812B_WriteByte(WS2812B_Buffer[i]);}	//连续写入显示缓存的3*Quantity个字节
	
	WS2812B_Delay100us();	//Reset(重置)信号
}

void main()
{
	unsigned int i,j,k;
	unsigned char Sum;	//用来控制顺向和逆向流星流水灯的叠加显示
						//Sum表示顺向或逆向流星流水灯出现的个数总和-1
	P3M1=0;P3M0=0;	//将P3口设置为上拉模式
	P5M1=0;P5M0=0;	//将P5口设置为上拉模式
	while(1)
	{
		//呼吸灯(红、绿、蓝、黄、紫、青、白)
		for(k=0;k<7;k++)	//总共要显示七种颜色的呼吸灯
		{
			for(i=0;i<256;i=i+3)	//变亮
			{
				for(j=0;j<Quantity;j++)
				{
					switch(k)
					{
						case 0:WS2812B_SetBuffer(j,i,0,0);break;	//红
						case 1:WS2812B_SetBuffer(j,0,i,0);break;	//绿
						case 2:WS2812B_SetBuffer(j,0,0,i);break;	//蓝
						case 3:WS2812B_SetBuffer(j,i,i,0);break;	//黄
						case 4:WS2812B_SetBuffer(j,i,0,i);break;	//紫
						case 5:WS2812B_SetBuffer(j,0,i,i);break;	//青
						case 6:WS2812B_SetBuffer(j,i,i,i);break;	//白
						default:break;
					}
				}
				WS2812B_UpdateDisplay();	//更新显示
				Delay(Duration1);	//延时“Duration1”ms
			}
			for(i=0;i<256;i=i+3)	//变暗
			{
				for(j=0;j<Quantity;j++)
				{
					switch(k)
					{
						case 0:WS2812B_SetBuffer(j,255-i,0,0);break;	//红
						case 1:WS2812B_SetBuffer(j,0,255-i,0);break;	//绿
						case 2:WS2812B_SetBuffer(j,0,0,255-i);break;	//蓝
						case 3:WS2812B_SetBuffer(j,255-i,255-i,0);break;	//黄
						case 4:WS2812B_SetBuffer(j,255-i,0,255-i);break;	//紫
						case 5:WS2812B_SetBuffer(j,0,255-i,255-i);break;	//青
						case 6:WS2812B_SetBuffer(j,255-i,255-i,255-i);break;	//白
						default:break;
					}
				}
				WS2812B_UpdateDisplay();
				Delay(Duration1);	//更改延时时间可以改变呼吸灯的快慢
			}
		}

		//红绿蓝三原色渐变
		for(i=0;i<256;i=i+3)	//红变亮,至全亮
		{
			for(j=0;j<Quantity;j++)
			{
				WS2812B_SetBuffer(j,i,0,0);
			}
			WS2812B_UpdateDisplay();	//更新显示
			Delay(Duration1);
		}
		for(k=0;k<2;k++)	//循环2次
		{
			for(i=0;i<256;i=i+3)	//红->绿渐变
			{
				for(j=0;j<Quantity;j++)
				{
					WS2812B_SetBuffer(j,255-i,i,0);	//红变暗,至全灭,绿变亮,至全亮
				}
				WS2812B_UpdateDisplay();	//更新显示
				Delay(Duration2);
			}
			for(i=0;i<256;i=i+3)	//绿->蓝渐变
			{
				for(j=0;j<Quantity;j++)
				{
					WS2812B_SetBuffer(j,0,255-i,i);	//绿变暗,至全灭,蓝变亮,至全亮
				}
				WS2812B_UpdateDisplay();	//更新显示
				Delay(Duration2);
			}
			for(i=0;i<256;i=i+3)	//蓝->红渐变
			{
				for(j=0;j<Quantity;j++)
				{
					WS2812B_SetBuffer(j,i,0,255-i);	//蓝变暗,至全灭,红变亮,至全亮
				}
				WS2812B_UpdateDisplay();	//更新显示
				Delay(Duration2);
			}
		}
		for(i=0;i<256;i=i+3)	//红变暗,至全灭
		{
			for(j=0;j<Quantity;j++)
			{
				WS2812B_SetBuffer(j,255-i,0,0);
			}
			WS2812B_UpdateDisplay();	//更新显示
			Delay(Duration1);
		}
	
		//流星流水灯(红、绿、蓝、黄、紫、青、白)
		for(j=0;j<2;j++)	//正向,循环2次
		{
			for(i=0;i<16*7;i++)	//每种颜色流星灯的长度是16个灯珠,总共有7种颜色
			{
				WS2812B_MoveInOrder(Table1,i);
				WS2812B_UpdateDisplay();
				Delay(Duration3);	//更改延时时间可以改变流星灯的速度
			}
		}
		for(i=0;i<Quantity;i++)	//退出
		{
			WS2812B_MoveInOrder(Table0,0);
			WS2812B_UpdateDisplay();
			Delay(Duration3);
		}
		for(j=0;j<2;j++)	//逆向,循环2次
		{
			for(i=0;i<16*7;i++)
			{
				WS2812B_MoveInReverseOrder(Table1,i);
				WS2812B_UpdateDisplay();
				Delay(Duration3);
			}
		}
		for(i=0;i<Quantity;i++)	//退出
		{
			WS2812B_MoveInReverseOrder(Table0,0);
			WS2812B_UpdateDisplay();
			Delay(Duration3);
		}

		//三原色流星流水灯顺向逆向叠加
		//相遇的时候会有颜色叠加,看不清楚可以增长延时观察
		for(k=0;k<2;k++)	//循环2次
		{
			Sum=0;
			for(j=0;j<Quantity+16;j++)	//顺红逆绿
			{
				WS2812B_Clear();	//清空缓存
				for(i=0;i<=Sum;i++)
				{
					if(j<16)	//流星灯进入过程
					{
						WS2812B_Buffer[3*(Sum-i)+1]=Table2[i];	//红
						WS2812B_Buffer[3*(Quantity-(Sum-i))-3]=Table2[i];	//绿
					}
					else if(j-i<Quantity)	//完全进入及退出
					{
						WS2812B_Buffer[3*(j-i)+1]=Table2[i];	//红
						WS2812B_Buffer[3*(Quantity-(j-i))-3]=Table2[i];	//绿
					}
				}
				WS2812B_UpdateDisplay();
				Sum++;	//每经过一个Delay延时,流星灯进入的数量+1
				if(Sum>15){Sum=15;}	//流星灯长度为1~16,对应sum的0~15,完全进入后保持15不变
									//Table2数组中16个数据255,191,127,95,63,47,31,23,15,11,7,5,3,2,1,0,对应的灯珠的亮度
				Delay(Duration3);
			}
			Sum=0;
			for(j=0;j<Quantity+16;j++)	//顺蓝逆红
			{
				WS2812B_Clear();	//清空缓存
				for(i=0;i<=Sum;i++)
				{
					if(j<16)	//流星灯进入过程
					{
						WS2812B_Buffer[3*(Sum-i)+2]=Table2[i];	//蓝
						WS2812B_Buffer[3*(Quantity-(Sum-i))-2]=Table2[i];	//红
					}
					else if(j-i<Quantity)	//完全进入及退出
					{
						WS2812B_Buffer[3*(j-i)+2]=Table2[i];	//蓝
						WS2812B_Buffer[3*(Quantity-(j-i))-2]=Table2[i];	//红
					}
				}
				WS2812B_UpdateDisplay();
				Sum++;
				if(Sum>15){Sum=15;}
				Delay(Duration3);
			}
			Sum=0;
			for(j=0;j<Quantity+16;j++)	//顺绿逆蓝
			{
				WS2812B_Clear();
				for(i=0;i<=Sum;i++)
				{
					if(j<16)
					{
						WS2812B_Buffer[3*(Sum-i)]=Table2[i];	//绿
						WS2812B_Buffer[3*(Quantity-(Sum-i))-1]=Table2[i];	//蓝
					}
					else if(j-i<Quantity)
					{
						WS2812B_Buffer[3*(j-i)]=Table2[i];	//绿
						WS2812B_Buffer[3*(Quantity-(j-i))-1]=Table2[i];	//蓝
					}
				}
				WS2812B_UpdateDisplay();
				Sum++;
				if(Sum>15){Sum=15;}
				Delay(Duration3);
			}
		}

	}
}

总结

感觉WS2812B芯片对时序的要求并没有像手册说的那么严格。


http://www.kler.cn/a/527345.html

相关文章:

  • Golang :用Redis构建高效灵活的应用程序
  • 基于51单片机和WS2812B彩色灯带的流水灯
  • 深度学习的应用
  • P1044 [NOIP2003 普及组] 栈 C语言
  • Transformer+vit原理分析
  • DRF开发避坑指南01
  • Baklib助力企业实现高效灵活的基于云的内容中台转型
  • 基于springboot+vue的母婴护理知识共享系统
  • 【愚公系列】《循序渐进Vue.js 3.x前端开发实践》039-使用JavaScript的方式实现动画效果
  • 10.4 LangChain核心架构揭秘:模块化设计如何重塑大模型应用开发?
  • SpringBoot AOP 和 事务
  • AI应用部署——streamlit
  • 基于Rectified Flow FLUX的图像编辑方法 RF-Solver
  • 17.2 图形绘制5
  • Streamlit入门
  • 04树 + 堆 + 优先队列 + 图(D1_树(D2_二叉树(BT)(D2_刷题练习)))
  • “星门计划对AI未来的意义——以及谁将掌控它”
  • Ethflow Round 1 (Codeforces Round 1001, Div. 1 + Div. 2)(A,B,C,E1)
  • hot100(4)
  • 对比DeepSeek、ChatGPT和Kimi的学术写作关键词提取能力
  • Baklib推动企业知识管理创新与效率提升的全面探讨
  • 计算机网络 性能指标相关
  • Python——基本数据类型——字符串类型
  • 代码随想录刷题day20|(哈希表篇)15.三数之和
  • 机器学习6-全连接神经网络2
  • 基于改进的强跟踪技术的扩展Consider Kalman滤波算法在无人机导航系统中的应用研究