【外设移植】WS2812B彩灯+M61开发板+显示器氛围灯

[复制链接]
查看409 | 回复8 | 2024-3-25 01:28:21 | 显示全部楼层 |阅读模式

本帖最后由 jkernet 于 2024-4-6 02:54 编辑

本帖最后由 jkernet 于 2024-4-6 02:48 编辑

本帖最后由 jkernet 于 2024-4-6 02:48 编辑

本帖最后由 jkernet 于 2024-4-6 02:42 编辑

本帖最后由 jkernet 于 2024-4-6 02:35 编辑

本帖最后由 jkernet 于 2024-4-5 23:01 编辑

本帖最后由 jkernet 于 2024-4-5 22:22 编辑

本帖最后由 jkernet 于 2024-4-5 22:21 编辑

本帖最后由 jkernet 于 2024-4-5 22:01 编辑

本帖最后由 jkernet 于 2024-4-5 21:41 编辑

本帖最后由 jkernet 于 2024-4-5 12:55 编辑

本帖最后由 jkernet 于 2024-4-1 23:22 编辑

本帖最后由 jkernet 于 2024-3-31 23:12 编辑

本帖最后由 jkernet 于 2024-3-26 21:52 编辑

本帖最后由 jkernet 于 2024-3-25 22:42 编辑

本帖最后由 jkernet 于 2024-3-25 09:26 编辑

前言

之前B站看到大佬的关于显示器氛围灯的视频,觉得很漂亮,趁着这次活动机会,我也来搞一个!

WS2812B彩灯

JLC搜索WS2812B,可以看到有各种型号

image.png

image.png

就比如这颗"XL-5050RGBC-WS2812B"型号的灯珠,XL是厂商,5050是灯珠尺寸毫米,RGBC是可以发出光的颜色和色温.具体可以自行搜索.

image.png

普通的RGB灯珠有6个引脚,灯珠越多占用的引脚数就越多,就算用矩阵也是一样,而且控制繁琐,WS2812B彩灯就是普通的RGB灯珠中加了一颗控制芯片,使用简单的串行控制协议,只需要3个引脚(电源,地,信号)就能控制很多的灯珠.

image.png

如灯带,灯板,就是通过DO(输出)连接DIN(输入)头尾相连串起来的,特别是灯带要仔细分辨,很容易就头晕搞不清楚了,只要记住板子的信号输出引脚一定是连接灯带的DIN(输入)就OK了.

image.png

那么想要灯珠发出什么颜色的光,我们该怎么做呢,提到颜色,肯定首先想到的就是RGB,这是一种最常用的表示颜色的数据格式,Red(红)Green(绿)Blue(蓝)三种颜色按顺序依次用1个字节来表示,取值范围是0-255,值越大亮度越高(颜色深),混合发出不同的颜色,1个字节8位,3个字节就是24位,所以也被称为24位色,比如(255,0,0)就是绿色,我们可以把它展开成24位二进制数据就是"11111111 00000000 00000000",但是它只能表示纯色,没有透明色,如今很多地方都使用到了透明色,于是在RGB的基础上增加了1个字节A(Alpha)来表示透明度,就变成了RGBA,通过上图可以看出WS2812B需要的24位颜色数据,但它不是按照RGB的顺序,而且特立独行的使用了GRB的顺序,这里我一开始也是比较疑惑的,于是问了问通义千问,它是这样回答我的

image.png

虽然没有正面回答我的问题,但是我觉得它说的都有那么点意思,我们继续,不过是调换了一下绿色和红色的位置,RGB绿色(255,0,0)变成了GRB(0,255,0),展开成24位二进制数据就是"00000000 11111111 00000000".

image.png

我们知道了灯珠需要的数据格式,那控制器也就是M61开发板该如何向WS2812B传递数据呢?设置开发板与WS2812B的DIO引脚(输入)相连的GPIO引脚为输出模式,输出高低电平,如上图0码对应二进制数据中的0,1码对应二进制数据中的1,看右边的波形图,一高一低代表一个数据位,0码高电平持续时间短一些,低电平持续时间长一些,1码高电平持续时间长一些,低电平持续时间短一些,上图下面的表格给出了相应的持续时长,其实表格中的数据也不用特别在意,表格下的注解才是真正的干货,我们需要注意的有5点: 1.高电平的持续时间,决定是0码还是1码 2.高低电平1个周期时间尽量保持在1.25us左右 3.低电平时间不要超过30us,否则可能被认为是复位 4.低电平复位时间最好留有余地,比如90us 5.尽可能在保证持续时间留有余地的情况下减少持续时间,可以提高速率

系统架构

了解完WS2812B彩灯,脑子里大概就有了初步的架构,WS2812B彩灯能根据电脑桌面的颜色改变,那么一定是有上位机持续采集桌面的颜色,然后发送给开发板,开发板再控制WS2812彩灯发出相应颜色的光.如下图:

image.png

硬件连接

ws2812b灯带 1米 18颗灯珠

IMG_20240331_20261_c2.jpg

ai-m61-32su开发板

IMG_20240331_202627_c.jpg

注意灯带头尾一定要辨别清楚,这里标注方式有点绕,好几次没点亮就是因为这里接反了

image.png

手绘接线图

image.png

驱动WS2812B(需要逻辑分析仪的帮助)

重点!!!把WS2812B驱动起来是一切功能的前提,也是本文的目标!驱动方式有很多种,如SPI,PWM,GPIO...我这里选择了最原始的方式GPIO翻转(高低电平切换)不去占用有限的外设资源,其实驱动代码并不复杂,主要是在于时序控制,驱动的方式根据上面手册知道,WS2812B需要的最小延时是在0.2us左右,经过逻辑分析仪测试,M61的GPIO翻转速度至少能达到42ns(24M逻辑分析仪的能力极限),但是SDK只提供了ms和us级别的延时函数且不支持小数,所以我们需要自己用"nop"空指令来实现ns级别的延时方法

时钟周期: 时钟周期也称为振荡周期,定义为时钟频率的倒数。时钟周期是计算机中最基本的、最小的时间单位。在一个时钟周期内,CPU仅完成一个"微操作"。时钟周期是一个时间的量。时钟周期表示了SDRAM所能运行的最高频率。更小的时钟周期就意味着更高的工作频率。

机器周期: 机器周期也称为CPU周期。在计算机中,为了便于管理,常把一条指令的执行过程划分为若干个阶段(如取指、译码、执行等),每一阶段完成一个基本操作。完成一个基本操作所需要的时间称为机器周期。一般情况下,一个机器周期由若干个时钟周期组成。

指令周期: CPU取出一条指令并执行所需要的时间叫做一个指令周期。由于各条指令的操作功能不同,因此指令周期也是不相同的。

M61开发板的主频是320MHz,所以时钟周期=1s/320Mhz=1s/320000000Hz=0.000000003125s=0.000003125ms=0.003125us=3.125ns,通常情况一个"nop"是一个单周期(机器周期)指令,但是一个机器周期要花多少个时钟周期呢?我去翻了翻BL616的芯片手册,没找到(太多了,小白也不知道关键字),实际的IO翻转速度以及"nop"指令所占用的时间,只能自己用逻辑分析仪测试了(分析仪最好直连被测IO),我们先测试一下在不加任何延时的情况下是什么样的.

#include "board.h"
#include "bflb_gpio.h"

#define TEST_PIN 18

int main(void)
{
  board_init();
  struct bflb_device_s *gpio;
  gpio = bflb_device_get_by_name("gpio");
  bflb_gpio_init(gpio, TEST_PIN, GPIO_OUTPUT | GPIO_PULLDOWN);
  bflb_gpio_reset(gpio, TEST_PIN);
  while (1)
  {
    bflb_gpio_set(gpio, TEST_PIN);
    bflb_gpio_reset(gpio, TEST_PIN);
  }
}

image.png

这是捕获到的波形图,代码写的是高电平切换到低电平,主要关注高电平的持续时间,有跳变,但大多数是42ns,这个值越小,代表IO翻转的速度越快,但是这款24M逻辑分析仪最小采集间隔是42ns,所以我们尝试加入"nop"进行延时,要在C代码里面写汇编,需要特殊的修饰符,"nop"指令的完整代码:__ASM __volatile("nop");

#include "board.h"
#include "bflb_gpio.h"

#define TEST_PIN 18

int main(void)
{
  board_init();
  struct bflb_device_s *gpio;
  gpio = bflb_device_get_by_name("gpio");
  bflb_gpio_init(gpio, TEST_PIN, GPIO_OUTPUT | GPIO_PULLDOWN);
  bflb_gpio_reset(gpio, TEST_PIN);
  while (1)
  {
    bflb_gpio_set(gpio, TEST_PIN);
    // 执行"nop"指令,__ASM表示汇编 __volatile防止编译器优化
    __ASM __volatile("nop");
    bflb_gpio_reset(gpio, TEST_PIN);
  }
}

image.png

加上一条"nop"指令进行延时后,发现高电平持续时间还是42ns,没什么变化,这说明测试到的42ns确实是逻辑分析仪的极限,实际的IO翻转速度还要更快!这是肯定的,毕竟320Mhz的主频,时钟周期是3.125ns,暂且就当"nop"指令只占用一个时钟周期,要在24M逻辑分析仪下能测到变化就让持续时间提高1倍,42ns*2=84ns,84ns/3.125=26.88,四舍五入需要执行27次"nop".

#include "board.h"
#include "bflb_gpio.h"

#define TEST_PIN 18

int main(void)
{
  board_init();
  struct bflb_device_s *gpio;
  gpio = bflb_device_get_by_name("gpio");
  bflb_gpio_init(gpio, TEST_PIN, GPIO_OUTPUT | GPIO_PULLDOWN);
  bflb_gpio_reset(gpio, TEST_PIN);
  while (1)
  {
    bflb_gpio_set(gpio, TEST_PIN);
    // 执行"NOP"指令,__ASM表示汇编 __volatile防止编译器优化
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    __ASM __volatile("nop");
    bflb_gpio_reset(gpio, TEST_PIN);
  }
}

.........这里我是真的无语了,写了一下午的内容,手贱点了一下超链接,页面跳转后什么都没了!!!要把代码回退,然后抓波形工程确实太大了只好省略了!抱歉.大致内容就是

1.再增加延时,(第二次的高电平持续时间-第一次高电平持续时间)/(第二次的延时次数-第一次延时次数)得出1次"nop"的时间大概为3.074ns,跟之前算出来的时钟周期3.125ns差不多,考虑到误差,完全可以认为1次"nop"时间就是3.125ns,1个时钟周期.

2.写一个动态延时方法,以100ns为单位,完善WS2812B驱动,发现波形异常,前两次波形持续时间过长达到了3us左右,最后排查问题发现可能是从FLASH载入执行代码耗时过长导致的,需要加修饰符 ATTR_TCM_SECTION,将需要保证运行时间的方法代码放入到高速缓存中,完整库代码.

mini_ws2812b.h

#ifndef MINI_WS2812B_H
#include "bflb_core.h"
void ws2812b_delay100ns(uint32_t time);
void ws2812b_flush();
bool ws2812b_isReady();
void ws2812b_deinit();
void ws2812b_init(uint8_t pin, uint32_t num, uint8_t type);
void ws2812b_setColor(uint32_t index, uint8_t *color);
void ws2812b_setColors(uint32_t start, uint32_t end, uint8_t *color);
void ws2812b_setOff(uint32_t index);
void ws2812b_setOffAll();
void ws2812b_setColorAll(uint8_t *color);
void ws2812b_reset();
#endif

mini_ws2812b.c

#include "board.h"
#include "bflb_gpio.h"
#include "hardware/gpio_reg.h"
#include "bflb_clock.h"
#include "mini_ws2812b.h"

// 灯珠类型
// 3色
#define WS2812B_TYPE_GRB 3
// 4色
#define WS2812B_TYPE_GRBW 4

// 100ns需要的指令周期数
uint16_t ws2812b_delay_ns = 0;

// 灯珠颜色数据
static uint8_t *ws2812b_addr = NULL;
// 灯珠颜色数据大小
static uint32_t ws2812b_size = 0;
// 灯珠数量
static uint32_t ws2812b_num = 0;
// 灯珠类型
static uint8_t ws2812b_type = WS2812B_TYPE_GRB;
// 数据引脚
static uint8_t ws2812b_pin = 0;

// GPIO控制
static struct bflb_device_s *ws2812b_gpio;
static uint32_t ws2812b_pin_addr = 0;
static uint32_t ws2812b_pin_high = 0;
static uint32_t ws2812b_pin_low = 0;

// 纳秒级延时 单位:100ns
// ATTR_TCM_SECTION 将此方法代码放入高速缓存中,提高运行效率,保证延时精度.
void __WEAK ATTR_TCM_SECTION ws2812b_delay100ns(uint32_t time)
{
    // time *= ws2812b_delay_ns;
    // // 减去额外的4条指令
    // time -= 4;
    // while (time)
    // {
    //     __ASM __volatile("nop");
    //     time--;
    // }
    __ASM __volatile(
        "lhu a5,ws2812b_delay_ns\n\t"
        "mul a0,a5,a0\n\t"
        "addi a0,a0,-4\n\t"
        "beqz a0,wsdlend\n\t"
        "wsdlloop:"
        "nop\n\t"
        "addi a0,a0,-1\n\t"
        "bnez a0,wsdlloop\n\t"
        "wsdlend:"
        "ret\n\t");
}

// 发送数据
// ATTR_TCM_SECTION 将此方法代码放入高速缓存中,提高运行效率,保证时序准确.
void ATTR_TCM_SECTION ws2812b_flush()
{
    uint8_t *addr;
    uint8_t value;
    // 遍历所有灯珠
    for (uint32_t i = 0; i < ws2812b_num; i++)
    {
        // 计算i灯珠颜色数据地址
        addr = ws2812b_addr + i * ws2812b_type;
        // 遍历每个颜色值
        for (uint8_t j = 0; j < ws2812b_type; j++)
        {
            // 取出颜色值 1字节
            value = *(addr + j);
            // 遍历每个位
            for (uint8_t k = 0; k < 8; k++)
            {
                // 0x80 二进制就是1000 0000,把value跟它进行位于操作就是判断最高位是否为1
                if (value & 0x80)
                {
                    // 1码
                    // 输出0.6us低电平
                    putreg32(ws2812b_pin_addr, ws2812b_pin_high);
                    ws2812b_delay100ns(6);
                    // 输出0.3us低电平
                    putreg32(ws2812b_pin_addr, ws2812b_pin_low);
                    ws2812b_delay100ns(3);
                }
                else
                {
                    // 0码
                    // 输出0.2us高电平
                    putreg32(ws2812b_pin_addr, ws2812b_pin_high);
                    ws2812b_delay100ns(2);
                    // 输出0.7us低电平
                    putreg32(ws2812b_pin_addr, ws2812b_pin_low);
                    ws2812b_delay100ns(7);
                }
                // 小端 高->低
                // value左移1位 比如1111 1111左移1位变成1111 1110,就是把整体往左移动,丢掉1位高位,低位补1位0
                value <<= 1;
            }
        }
    }
}
// #pragma GCC pop_options

// 初始化
void ws2812b_init(uint8_t pin, uint32_t num, uint8_t type)
{
    ws2812b_deinit();
    // 根据主频计算100ns需要的指令周期数
    ws2812b_delay_ns = 100.0 / (1000000000.0 / bflb_clk_get_system_clock(BFLB_SYSTEM_CPU_CLK)) / 3.0;
    ws2812b_pin = pin;
    ws2812b_num = num;
    ws2812b_type = type;
    ws2812b_size = num * type;
    // 是否考虑对齐问题?
    ws2812b_addr = malloc(ws2812b_size);
    memset(ws2812b_addr, 0, ws2812b_size);
    // 初始化控制IO
    ws2812b_gpio = bflb_device_get_by_name("gpio");
    bflb_gpio_init(ws2812b_gpio, ws2812b_pin, GPIO_OUTPUT | GPIO_PULLDOWN | GPIO_SMT_DIS | GPIO_DRV_0);
    ws2812b_pin_addr = 1 << (ws2812b_pin & 0x1f);
    // bflb_gpio_set
    ws2812b_pin_high = ws2812b_gpio->reg_base + GLB_GPIO_CFG138_OFFSET + ((ws2812b_pin >> 5) << 2);
    // bflb_gpio_reset
    ws2812b_pin_low = ws2812b_gpio->reg_base + GLB_GPIO_CFG140_OFFSET + ((ws2812b_pin >> 5) << 2);
    // 复位
    ws2812b_reset();
}

// 反初始化
void ws2812b_deinit()
{
    if (!ws2812b_isReady())
    {
        return;
    }
    bflb_gpio_deinit(ws2812b_gpio, ws2812b_pin);
    free(ws2812b_addr);
    ws2812b_addr = NULL;
}

// 是否初始化
bool ws2812b_isReady()
{
    return ws2812b_addr != NULL;
}

// 设置单个灯珠颜色
// index 灯珠索引 0开始
// color 灯珠颜色
void ws2812b_setColor(uint32_t index, uint8_t *color)
{
    memcpy(ws2812b_addr + index * ws2812b_type, color, ws2812b_type);
}

// 设置连续多个灯珠颜色
// start 开始灯珠索引
// end 结束灯珠索引
// color 灯珠颜色
void ws2812b_setColors(uint32_t start, uint32_t end, uint8_t *color)
{
    for (uint32_t i = start; i <= end; i++)
    {
        ws2812b_setColor(i, color);
    }
}

// 设置所有灯珠颜色
void ws2812b_setColorAll(uint8_t *color)
{
    ws2812b_setColors(0, ws2812b_num - 1, color);
}

// 关闭单个灯珠
// index 灯珠索引 0开始
void ws2812b_setOff(uint32_t index)
{
    memset(ws2812b_addr + index * ws2812b_type, 0, ws2812b_type);
}

// 关闭所有灯珠
void ws2812b_setOffAll()
{
    memset(ws2812b_addr, 0, ws2812b_size);
}

// 复位(只是复位发送状态,并不会清除灯珠现有状态)
void ws2812b_reset()
{
    putreg32(ws2812b_pin_addr, ws2812b_pin_low);
    ws2812b_delay100ns(1000);
}

// 示例
void ws2812b_test()
{
    // 初始化ws2812b 数据引脚:18 灯珠数量:1 灯珠类型:GRB
    ws2812b_init(18, 1, WS2812B_TYPE_GRB);
    // 颜色变量,蓝色
    uint8_t color[3] = {0, 0, 255};
    // 设置第一个灯珠为蓝色
    ws2812b_setColor(0, color);
    // 发送数据,点亮灯珠
    ws2812b_flush();
}

image.png

实测数据在误差范围内了.

IMG_20240405_220725_c.jpg

成功点亮一颗蓝色的小星星.

通信方式

M61-32S是一块主打WIFI/蓝牙的开发板,首先想到的就是用socket,但这种方式要建立通信就得有一个配网流程,有点繁琐了,仔细琢磨了一下使用场景,设备在电脑上使用,大多采用主机USB供电,直接用USB通信不就行了,研究了一顿USB设备通信协议后,发现坑了,M61开发板只有一个USB口且连接的是CH340 USB转串口芯片,无法切换到USB功能...(外接太麻烦略),那么就只能将就一下使用串口通信吧,注意这里与SDK本身的控制台功能有冲突,需要先屏蔽掉"board_init"函数中的"console_init"初始化函数

通信协议

数据用大写的16进制文本,字符取值范围就在"0123456789ABCDEF"也就是"48,49,50,51,52,53,54,55,56,57,65,66,67,68,69,70"

控制标记用十进制,0-47,71-255都可以使用,完全足够了.

配置指令:

配置开始标记+灯珠数量+控制引脚+灯珠类型+配置结束标记 1 00000003 12 03 2

数据指令:

数据开始标记+[索引+ 绿色+红色+蓝色...]+数据结束标记 10 00000000 00 00 FF 11

上位机软件

易语言编写,使用方法:

image.png

image.png

image.png

image.png

效果展示

https://www.bilibili.com/video/BV1ef421o7sy/?share_source=copy_web&vd_source=50f6572b9419853aac1662ba1b5f868f

注意

灯珠超过8颗要考虑独立供电,否则可能出现异常!

回复

使用道具 举报

jkernet | 2024-4-6 02:39:03 | 显示全部楼层
MonitorRGB.zip (6.61 KB, 下载次数: 9)
回复 支持 反对

使用道具 举报

1055173307 | 2024-4-7 14:43:05 | 显示全部楼层
woc佬!这个上位机自己写的吗,怎么实现的获取屏幕的显示内容哇
回复 支持 反对

使用道具 举报

jkernet | 2024-4-7 16:20:15 | 显示全部楼层
1055173307 发表于 2024-4-7 14:43
woc佬!这个上位机自己写的吗,怎么实现的获取屏幕的显示内容哇

是的,简单!调用win api "GetPixel"取屏幕像素点颜色
https://learn.microsoft.com/zh-c ... /nf-wingdi-getpixel
回复 支持 反对

使用道具 举报

1055173307 | 2024-4-7 16:29:30 | 显示全部楼层
jkernet 发表于 2024-4-7 16:20
是的,简单!调用win api "GetPixel"取屏幕像素点颜色
https://learn.microsoft.com/zh-cn/windows/win32/a ...

学到了学到了,谢谢佬!
回复 支持 反对

使用道具 举报

iiv | 2024-4-7 18:05:46 | 显示全部楼层
谢谢佬的分享
回复 支持 反对

使用道具 举报

King6688 | 2024-4-7 19:32:35 | 显示全部楼层
半夜还在发帖,这真的太卷了
回复 支持 反对

使用道具 举报

wukong50 | 2024-4-14 14:41:12 | 显示全部楼层
你好,老哥,这翻译是用什么翻译的
回复 支持 反对

使用道具 举报

jkernet | 2024-4-15 09:44:49 | 显示全部楼层
wukong50 发表于 2024-4-14 14:41
你好,老哥,这翻译是用什么翻译的

什么翻译?
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则