目录
② ESP8266 开发学习笔记_By_GYC 【ESP8266 驱动 ws2812 三原色灯(spi方式 稳定灯光)】
一、驱动ws2812遇到的问题
二、可能的方案
三、具体实现
四、测试程序
五、还没结束
本章介绍ESP8266 IDF 框架下 如何使用 骚操作 的使用SPI总线,发送更高精度的脉冲信号,ws2812作为控制芯片三色灯的使用方法,实现三原色显示灯带。在研究过程中,发现ESP8266的引脚响应速度有些慢,输出2.5us才能够翻转一次,而ws2812的控制电平分辨率要求在百纳秒级,所以需要其他方法来输出控制信号才能保证灯光稳定。本次选用SPI信号输出口,使灯光达到了稳定。
在淘宝上偶然看见有只需要一个引脚就能高速的控制三原色全彩LED灯,这让我很感兴趣,就买下来回来尝试,结果到手当天就遇到了很严重的问题,根据手册的说明写了一下简单的驱动程序,灯亮是能亮但是只有一个颜色,没有办法像网上说的那样能够自由的调节颜色,搞得我很是崩溃,还以为自己的编程水平出了问题,明明代码逻辑已经没有什么问题了,却还是不能正常显示,我就喊朋友来帮忙驱动一下,他用stm32的开发板,十几分钟就从网上扣下源码给我驱动了,代码逻辑和我的相差无几。
那么确定不是代码的问题了,就要找找其他的问题,比如我正在研究的单片机ESP8266。放到示波器上显示GPIO引脚的输出电平可以发现,引脚的实际输出速度并不像程序设计的那样,实际操作时ESP8266的管脚每2.5us(0.4MHz)才能够进行一次有效的翻转,而ws2812的控制电平要求精度在百ns级别,普通的GPIO管脚并不能达到这样的速度,而stm32的引脚翻转速度远大于ESP8266的,其I/O口驱动电路的响应速度有2M、10M、50M可选,轻松就能达到百纳秒的精度。所以stm32能够轻松的驱动ws2812而ESP8266只能通过骚操作来实现。
1、特殊GPIO
一般如stm32主频比较高的单片机,可以直接通过驱动GPIO引脚,控制引脚的翻转,实现对ws2812的控制。虽然ESP8266的GPIO翻转速度无法达到期望的速度,但是根据网上其他人的分享,发现ESP8266的GPIO0的翻转速度和响应速度都比片上其他的GPIO快,可以作为ws2812的驱动引脚。经过测试,发现配合寄存器操作的GPIO0确实能够驱动ws2812、并且能够显示色彩进行调节。不过这种方法稳定性较低,不知道是我使用的芯片问题还是普遍存在,用GPIO0驱动的ws2812灯圈(8个)不稳定,偶尔就会一个灯珠颜色错误。这让我很是难受。
2、使用pwm驱动
PWM,周期设置为3MHz,发送0就把占空比设置为33%,发送1就把占空比设置为66%。也是一种很有创意的驱动方式。可惜的是ESP8266的PWM功能是通过定时器用GPIO翻转模拟的,它的PWM 周期范围是:1000us (1KHz) ~ 10000us (100Hz),达不到要求。
3、使用SPI方案(本次使用)
可以注意到,将SPI的时钟调整为8MHz,发送一字节是1us,一个比特是0.125us,给ws2812发送逻辑0即可以通过SPI总线发送11000000b来实现(0.25us高电平,0.75us低电平),发送逻辑1即可以通过SPI总线发送11111100b来实现(0.75us高电平,0.25低电平)。通过这种方式驱动的灯光稳定可靠。能够保证灯光不会出现闪烁或者某个灯珠颜色跳变的情况。本次要介绍的ws2812驱动就是使用这种控制方式来实现的。
TODO: 低电平时间0.25us是硬件规定的低电平最小时间,如果能增大一点会更稳定,可以把控制引脚高低的数据改成数据流,比如
TL:1110000000b (0.375us[0.4-0.025] 高电平,0.875us[0.85+0.025] 低电平) 9位 共1.125us
TH:1111110000b (0.75us [0.8-0.05] 高电平,0.5us [0.45+0.05] 低电平)9位 共1.125us
高低电平的数据组合组成一长串的 spi 数据,可以使控制更加稳定。
首先根据ESP8266 的资源信息确认需要用到的引脚。
根据上图所示,ESP8266在nodemcu上的SPI引脚是D5-D8、我们可以通过初始化控制禁用CS和MISO使能,只使用MOSI作为WS2812的输出引脚。设置SPI的时钟频率(SPI clock frequency)为8MHz,使一个字节周期为1.25us。
spi引脚初始化函数如下。
void ws2812_spi_mode_init(void) //must use the ESP8266 GPIO13 as the hspi pin to drive WS2812B RGB LED!!!
{
uint8_t x = 0;
ESP_LOGI("WS2812", "ws2812 init gpio");
ESP_LOGI("WS2812", "init hspi");
spi_config_t spi_config;
// Load default interface parameters
// CS_EN:1, MISO_EN:1, MOSI_EN:1, BYTE_TX_ORDER:1, BYTE_TX_ORDER:1, BIT_RX_ORDER:0, BIT_TX_ORDER:0, CPHA:0, CPOL:0
spi_config.interface.val = SPI_DEFAULT_INTERFACE;
// Load default interrupt enable
// TRANS_DONE: true, WRITE_STATUS: false, READ_STATUS: false, WRITE_BUFFER: false, READ_BUFFER: false
spi_config.intr_enable.val = SPI_MASTER_DEFAULT_INTR_ENABLE;
// Cancel hardware cs
spi_config.interface.cs_en = 0;
// MISO pin is used for DC
spi_config.interface.miso_en = 0;
// CPOL: 1, CPHA: 1
spi_config.interface.cpol = 1;
spi_config.interface.cpha = 1;
// Set SPI to master mode
// 8266 Only support half-duplex
spi_config.mode = SPI_MASTER_MODE;
// Set the SPI clock frequency division factor
spi_config.clk_div = SPI_8MHz_DIV;
// Register SPI event callback function
spi_config.event_cb = spi_event_callback;
spi_init(HSPI_HOST, &spi_config);
ESP_LOGI("WS2812", "init over");
}
需要注意的是,这里虽然没有用到,但是你需要设置spi的事件回调函数,即使他是空的
static void IRAM_ATTR spi_event_callback(int event, void *arg)
{
switch (event) {
case SPI_INIT_EVENT: {
}
break;
case SPI_TRANS_START_EVENT: {
}
break;
case SPI_TRANS_DONE_EVENT: {
}
break;
case SPI_DEINIT_EVENT: {
}
break;
}
}
准备工作做好之后,我们就要编写数据发送函数了。网上其他的例程里面常常把数据发送函数分为位发送、字节发送、像素点发送三层,层层调用,这种逻辑非常的便于阅读。但是在引脚响应速度并不那么快的单片机上,这种结构并不能保证时序的稳定性,因此,此处我直接略去了前两个过程,直接提供了一个像素数据发送的函数。避免函数切换、SPI重新启动引起的时序不稳定问题。
这个函数在结构上还有待优化,待我闲下来的时候再重构一下,先提供一个能够使用的版本,也希望有高手能够分享这个程序的简化版本。
void WS2812BSend_24bit(uint8_t R, uint8_t G, uint8_t B)
{
uint32_t GRB=G<<16|R<<8|B;
uint8_t data_buf[24];
uint8_t *p_data=data_buf;
//能用 等待优化!
uint8_t mask = 0x80;
uint8_t byte = G;
while (mask)
{
if( byte & mask ) {*p_data = 0xFC;/*11111100b;*/} else {*p_data = 0XC0;/*11000000b;*/}
mask >>= 1;
p_data++;
}
mask = 0x80;
byte = R;
while (mask)
{
if( byte & mask ) {*p_data = 0xFC;/*11111100b;*/} else {*p_data = 0XC0;/*11000000b;*/}
mask >>= 1;
p_data++;
}
mask = 0x80;
byte = B;
while (mask)
{
if( byte & mask ) {*p_data = 0xFC;/*11111100b;*/} else {*p_data = 0XC0;/*11000000b;*/}
mask >>= 1;
p_data++;
}
uint8_t* p_8_data;
for(int i=0;i<6;i++)
{
p_8_data=(data_buf+(i*4));
uint8_t temp;
for(int j=0;j<2;j++)
{
temp=p_8_data[j];
p_8_data[j]=p_8_data[3-j];
p_8_data[3-j] = temp;
}
}
uint32_t *spi_buf=(uint32_t*)data_buf;
spi_trans_t trans = {0};
trans.mosi = spi_buf;
trans.bits.mosi = 24*8;
//ETS_INTR_LOCK();
spi_trans(HSPI_HOST, trans);
//ETS_INTR_UNLOCK();
}
比较麻烦的是,我这里每次传输了192(24*8)bit,由于这是32位的单片机,他是以32bit为单位进行传输的,而且每次都是从低位开始传输。由于ESP8266是小端字节序(与我们的阅读习惯不一致),所以在设置传输的时候需要将数据反一下,保证数据输出的顺序是我们想要的顺序。
以unsigned int value = 0x12345678为例,分别看看在两种字节序下其存储情况
内存地址 | 小端模式存放内容 | 大端模式存放内容 |
0x4000 | 0x78 | 0x12 |
0x4001 | 0x56 | 0x34 |
0x4002 | 0x34 | 0x56 |
0x4003 | 0x12 | 0x78 |
有了如上函数,我们就可以轻松的点亮ws2812三原色灯珠了。
ws2812具体的协议可以参考技术规格书(https://wenku.baidu.com/view/25f176db482fb4daa48d4ba1.html?rec_flag=default&sxts=1561280682919),
使用到的主要内容如下:
需要注意的是 要保证电源稳定,因为电源问题我遇到了 意外的灯光闪烁、多个灯一起点亮时产生颜色偏差 的问题,更换了供电线和使用5V给模块供电之后,颜色显示完全稳定和正常了。保证硬件良好是软件调试好软件的关键。
#define PIXEL_MAX 4 //the total numbers of LEDs you are used in your project
uint8_t rBuffer[PIXEL_MAX]={0,0,255,255};
uint8_t gBuffer[PIXEL_MAX]={0,255,0,255};
uint8_t bBuffer[PIXEL_MAX]={255,0,0,255};
void WS2812_Test(void)
{
//初始化 HSPI 作为数据输出引脚
ws2812_spi_mode_init();
//刷新显示4个LED灯
for(int i=0;i
调用我github上写好的库函数进行测试:
void app_main(void)
{
printf("SDK version:%s\n", esp_get_idf_version());
printf("WS2812 Demo\n");
WS2812_Init(); //初始化
rainbowCycle(10); //彩虹环
}
效果
目前这个项目还未完全完成,还存在一些优化空间,希望大家能够多多和我交流,写出更好的程序。O(∩_∩)O哈哈~
在此特别感谢“半颗心脏”大佬对我项目的关注,互相学习啦。
我的源文件和头文件已经上传至我的github上(https://github.com/gengyuchao),欢迎大家关注我的博客和github呀。