本帖最后由 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,可以看到有各种型号
就比如这颗"XL-5050RGBC-WS2812B"型号的灯珠,XL是厂商,5050是灯珠尺寸毫米,RGBC是可以发出光的颜色和色温.具体可以自行搜索.
普通的RGB灯珠有6个引脚,灯珠越多占用的引脚数就越多,就算用矩阵也是一样,而且控制繁琐,WS2812B彩灯就是普通的RGB灯珠中加了一颗控制芯片,使用简单的串行控制协议,只需要3个引脚(电源,地,信号)就能控制很多的灯珠.
如灯带,灯板,就是通过DO(输出)连接DIN(输入)头尾相连串起来的,特别是灯带要仔细分辨,很容易就头晕搞不清楚了,只要记住板子的信号输出引脚一定是连接灯带的DIN(输入)就OK了.
那么想要灯珠发出什么颜色的光,我们该怎么做呢,提到颜色,肯定首先想到的就是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的顺序,这里我一开始也是比较疑惑的,于是问了问通义千问,它是这样回答我的
虽然没有正面回答我的问题,但是我觉得它说的都有那么点意思,我们继续,不过是调换了一下绿色和红色的位置,RGB绿色(255,0,0)变成了GRB(0,255,0),展开成24位二进制数据就是"00000000 11111111 00000000".
我们知道了灯珠需要的数据格式,那控制器也就是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彩灯发出相应颜色的光.如下图:
硬件连接
ws2812b灯带 1米 18颗灯珠
ai-m61-32su开发板
注意灯带头尾一定要辨别清楚,这里标注方式有点绕,好几次没点亮就是因为这里接反了
手绘接线图
驱动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);
}
}
这是捕获到的波形图,代码写的是高电平切换到低电平,主要关注高电平的持续时间,有跳变,但大多数是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);
}
}
加上一条"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();
}
实测数据在误差范围内了.
成功点亮一颗蓝色的小星星.
通信方式
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
上位机软件
易语言编写,使用方法:
效果展示
https://www.bilibili.com/video/BV1ef421o7sy/?share_source=copy_web&vd_source=50f6572b9419853aac1662ba1b5f868f
注意
灯珠超过8颗要考虑独立供电,否则可能出现异常!