② ESP8266 开发学习笔记_By_GYC 【ESP8266 驱动 ws2812 三原色灯(spi方式 稳定灯光)】

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/gengyuchao/article/details/93239317

目录

 

② ESP8266 开发学习笔记_By_GYC 【ESP8266 驱动 ws2812 三原色灯(spi方式 稳定灯光)】

一、驱动ws2812遇到的问题

二、可能的方案

三、具体实现

四、测试程序

五、还没结束


② ESP8266 开发学习笔记_By_GYC 【ESP8266 驱动 ws2812 三原色灯(spi方式 稳定灯光)】

        本章介绍ESP8266 IDF 框架下 如何使用 骚操作 的使用SPI总线,发送更高精度的脉冲信号,ws2812作为控制芯片三色灯的使用方法,实现三原色显示灯带。在研究过程中,发现ESP8266的引脚响应速度有些慢,输出2.5us才能够翻转一次,而ws2812的控制电平分辨率要求在百纳秒级,所以需要其他方法来输出控制信号才能保证灯光稳定。本次选用SPI信号输出口,使灯光达到了稳定。

 

一、驱动ws2812遇到的问题
 

        在淘宝上偶然看见有只需要一个引脚就能高速的控制三原色全彩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,发送一字节正好是1.25us,恰好符合ws2812一个位的时间,给ws2812发送逻辑0即可以通过SPI总线发送11000000b来实现,发送逻辑1即可以通过SPI总线发送11111100b来实现。通过这种方式驱动的灯光稳定可靠。能够保证灯光不会出现闪烁或者某个灯珠颜色跳变的情况。本次要介绍的ws2812驱动就是使用这种控制方式来实现的。

 

三、具体实现

首先根据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<PIXEL_MAX;i++)
  {							  
    WS2812BSend_24bit(rBuffer[i],gBuffer[i],bBuffer[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呀。

 

 

 

展开阅读全文

没有更多推荐了,返回首页