介绍

炫彩灯珠内置 IC,可以显示 256×256×256256\times256\times256 种颜色,仅凭一根信号线即可实现多种多样的效果

硬件电路

根据 SK6812 的电气参数和手册上的典型应用电路来设计其硬件电路

参数 符号 范围 单位
电压 VDD +3.7~+5.5 V
逻辑输入电压 VI -0.5~VDD+0.5 V
工作温度 Topt -40~+85
储存温度 Tstg -40~+85
ESD耐压(设备模式) VESD 200 V
ESD耐压(人体模式) VESD 2K V

1750583530550.png

控制方式

SK6812 的数据传输方式比起一般的外设来说,有点特别,每个位都是一个码元,然后通过控制码元的高低电平时间来控制该位的高低

1750590740631.png

时序表名称 min typic max 单位
T 1.20 - - us
T0H 0.2 0.3 0.4 us
T0L 0.8 - - us
T1H 0.6 0.67 1.0 us
T1L 0.2 - - us
Trst 80\geq80 - - us

协议中每个码元都必须有低电平,每个码元起始为高电平,高电平时间宽度决定了该码元是 0 码还是 1 码,每个码元周期最小为 1.2us,具体的传输方式如下图所示

1750587905523.png

GPIO 翻转

GPIO 可以通过计算系统主频来计算出单条指令的时间,从而实现纳秒级硬延时。例如对于 AT32F403,设置单片机运行主频为 240MHz,所以它单条指令所需要的时间为 1240M=4.167ns\frac{1}{240M}=4.167ns 。根据上述分析,设置码元时间为 1.2us1.2us ,则 0 码的高电平持续时间为 0.3us0.3us ,低电平持续时间为 0.9us0.9us ,而 1 码的高电平持续时间为 0.6us0.6us ,低电平持续时间为 0.6us0.6us

一般可以利用 __nop() 指令,单条 __nop() 指令所需要的时间是 4.167ns4.167ns ,所以可以利用 7272 条指令来模拟延时 0.3us0.3us ,对应的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// clang-format off
uint8_t color_data_list[4][] = {
0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
0x00, 0x00, 0x00
};
// clang-format on

void delay_30us() {
__nop();
...
__nop();
// 72 个 __nop();
}

void sk_init() {
wk_dma_channel_config(DMA1_CHANNEL1, (uint32_t) &SPI2->dt, (uint32_t) color_data_list, 96);
}

static void set_color(uint8_t r, uint8_t g, uint8_t b) {
color_data_list[0][0] = color_data_list[1][0] = color_data_list[2][0] = color_data_list[3][0] = g;
color_data_list[0][1] = color_data_list[1][1] = color_data_list[2][1] = color_data_list[3][1] = r;
color_data_list[0][2] = color_data_list[1][2] = color_data_list[2][2] = color_data_list[3][2] = b;
}

static void set_high_level() {
gpio_bits_set(GPIOB, GPIO_PINS_13);
delay_30us();
delay_30us();
gpio_bits_reset(GPIOB, GPIO_PINS_13);
delay_30us();
delay_30us();
}

static void set_low_level() {
gpio_bits_set(GPIOB, GPIO_PINS_13);
delay_30us();
gpio_bits_reset(GPIOB, GPIO_PINS_13);
delay_30us();
delay_30us();
delay_30us();
}

static void send_color() {
for(int i = 0; i < 12; ++i)
for(int j = 7; j >= 0; --j) {
if(color_data_list[i] & (0x01 << j)) set_high_level();
else set_low_level();
}
}

PWM+DMA

根据上述中每个码元的时间为 1.2us1.2us ,因此可以设置 PWM 的周期为 1.25us1.25us 即频率为 800KHz800KHz,通知通过控制重装载值即可控制高低电平的时间,从而实现码元输出。由于 AT32F403 时钟的主频为 240MHz240MHz ,因此可以设置预分频值为 2 ,设置周期值为 99 即可。由于 PWM 的输出,当计数值小于比较值时,输出低电平,由于码元需要是高电平在前,所以设置 PWM 计数方式为向下计数

设置 0 码元和 1 码元的高低电平占比

  • 0 码元: 0.3us0.3us 高电平, 0.95us0.95us 低电平,设置比较值为 76
  • 1 码元: 0.65us0.65us 高电平, 0.6us0.6us 低电平,设置比较值为 50

另外打开该通道的 DMA,传输方向设置为从内存到外设,代码实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#define HIGH_LEVEL 76
#define LOW_LEVEL 50

// clang-format off
uint8_t color_data_list[4][24] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// clang-format on

void sk_init() {
wk_dma_channel_config(DMA1_CHANNEL2, (uint32_t) &TMR3->c1dt, (uint32_t) color_data_list, 96);
}

static void set_color(uint8_t r, uint8_t g, uint8_t b) {
uint32_t grb = (g << 16) | (r << 8) | b;
uint32_t mask = 0x01 << 23;
uint8_t i;
for (i = 0; i < 24; ++i) color_data_list[0][i] = color_data_list[1][i] = color_data_list[2][i] = color_data_list[3][i] = ((grb & (mask >> i)) ? HIGH_LEVEL : LOW_LEVEL);
}

static void send_color() {
dma_channel_enable(DMA1_CHANNEL2, TRUE);

while (dma_flag_get(DMA1_FDT2_FLAG) == RESET);
dma_flag_clear(DMA1_FDT2_FLAG);

dma_channel_enable(DMA1_CHANNEL2, FALSE);
}

SPI+DMA

由上述所述,可知每个码元的持续时间大概在 1.2us1.2\sim\infty us 之间, 为了传输的效率,此处要选择尽量小的周期。使用 SPI 控制时,可以通过发送 8 bits 数据来控制,可以利用单个 8 bit 数据所形成的波形来分别表示 0 码和 1 码

  • 0 码:前 2 个比特为高电平,后 6 个比特为低电平,因此发送的数据应当是 0b11000000
  • 1 码:前 5 个比特为高电平,后 3 个比特为低电平,因此发送的数据为 0b11111000

假设 SPI 的频率为 xMHzx MHz (也就是发送一个比特位的频率),则可以调整 xx 的大小,使得 SPI 能在 1.2us1.2us\sim\infty 之间发送一个字节,计算可得 x6.67MHzx\leq6.67MHz ,所以在设置 SPI 时就不能设置的主频过高,而且需要注意设置 SPI 的帧位个数为 8 位,且高位先传输(MSB First)。对应的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#define HIGH_LEVEL 0xf8
#define LOW_LEVEL 0xc0
// 上述的写作 16 进制是由于 KEIL 的 C 编译器不支持直接写二进制 0b11111000 0b11000000,也可以写作 Bin(0b11111000) Bin(0b11000000)

// clang-format off
uint8_t color_data_list[4][] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// clang-format on

void sk_init() {
wk_dma_channel_config(DMA1_CHANNEL1, (uint32_t) &SPI2->dt, (uint32_t) color_data_list, 96);
}

static void set_color(uint8_t r, uint8_t g, uint8_t b) {
uint32_t grb = (g << 16) | (r << 8) | b;
uint32_t mask = 0x01 << 23;
uint8_t i;
for (i = 0; i < 24; ++i) color_data_list[0][i] = color_data_list[1][i] = color_data_list[2][i] = color_data_list[3][i] = (grb & (mask >> i)) ? HIGH_LEVEL : LOW_LEVEL;
}

static void send_color() {
spi_i2s_dma_transmitter_enable(SPI2, TRUE);
dma_channel_enable(DMA1_CHANNEL1, TRUE);

while (dma_flag_get(DMA1_FDT1_FLAG) == RESET);
dma_flag_clear(DMA1_FDT1_FLAG);

dma_channel_enable(DMA1_CHANNEL1, FALSE);
spi_i2s_dma_transmitter_enable(SPI2, FALSE);
}

进阶控制方式——呼吸灯

SK6812 可以通过控制 RGB 三色值来控制彩灯的颜色,当然还可以通过控制颜色空间 HSV,再转换为 RGB 来控制颜色空间

RGB 转 HSV

max=max(r,g,b)min=min(r,g,b)h={0max=min60×gbmaxmin+0max=rgb60×gbmaxmin+360max=rg<b60×brmaxmin+120max=g60×rgmaxmin+240max=bs={0max=0maxminmaxv=maxmax=\max(r,g,b)\\min=\min(r,g,b)\\h=\left\{\begin{aligned}&0^\circ&&max=min\\&60^\circ\times\frac{g-b}{max-min}+0^\circ&&max=r\quad g\geq b\\&60^\circ\times\frac{g-b}{max-min}+360^\circ&&max=r\quad g< b\\&60^\circ\times\frac{b-r}{max-min}+120^\circ&&max=g\\&60^\circ\times\frac{r-g}{max-min}+240^\circ&&max=b\end{aligned}\right.\\s=\left\{\begin{aligned}&0^\circ&&max=0\\&\frac{max-min}{max}\end{aligned}\right.\\v=max

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
static void rgb_to_hsv(float* h, float* s, float* v, uint8_t r, uint8_t g, uint8_t b) {
uint8_t max = (r > g) ? (r > b ? r : b) : (g > b ? g : b);
uint8_t min = (r < g) ? (r < b ? r : b) : (g < b ? g : b);
float tmp = max - min;
*v = max;
if (max == 0) *s = 0;
else *s = tmp / max;
if (max == min) *h = 0;
else if (max == r && g >= b) *h = 60.f * (g - b) / tmp;
else if (max == r && g < b) *h = 60.f * (g - b) / tmp + 360;
else if (max == g) *h = 60.f * (b - r) / tmp + 120;
else if (max == b) *h = 60.f * (r - g) / tmp + 240;
}

HSV 转 RGB

c=v×sx=c×(1mod(h60,2)1)m=vc(r,g,b)={(c,x,0)0h<60(x,c,0)60h<120(0,c,0)120h<180(0,x,c)180h<240(x,0,c)240h<300(c,0,x)300h<360(r,g,b)=((r+m)×255),(g+m)×255),(b+m)×255))c=v\times s\\x=c\times(1-\vert\mod(\frac{h}{60^\circ},2)-1\vert)\\m=v-c\\(r^\prime,g^\prime,b^\prime)=\left\{\begin{aligned}&(c,x,0)&&0^\circ\leq h<60^\circ\\&(x,c,0)&&60^\circ\leq h<120^\circ\\&(0,c,0)&&120^\circ\leq h<180^\circ\\&(0,x,c)&&180^\circ\leq h<240^\circ\\&(x,0,c)&&240^\circ\leq h<300^\circ\\&(c,0,x)&&300^\circ\leq h<360^\circ\end{aligned}\right.\\(r,g,b)=((r^\prime+m)\times255),(g^\prime+m)\times255),(b^\prime+m)\times255))

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static void hsv_to_rgb(float h, float s, float v, uint8_t* r, uint8_t* g, uint8_t* b) {
float c, tmp, x, m, r_, g_, b_;
c = v * s;
tmp = h / 60 - 1;
x = c * (1 - (int) tmp % 2);
m = v - c;
if (h < 60) {
r_ = c;
g_ = x;
b_ = 0;
} else if (h < 120) {
r_ = x;
g_ = c;
b_ = 0;
} else if (h < 180) {
r_ = 0;
g_ = c;
b_ = x;
} else if (h < 240) {
r_ = 0;
g_ = x;
b_ = c;
} else if (h < 300) {
r_ = x;
g_ = 0;
b_ = c;
} else {
r_ = c;
g_ = 0;
b_ = x;
}
*r = (r_ + m) * 255;
*g = (g_ + m) * 255;
*b = (b_ + m) * 255;
}

一个示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void func() {
float h, s, v;
uint8_t r, g, b;
h = 0;
for (;;) {
h = fmod(h + 2, 360);
v = 0;
while (v < 1) {
v += 0.01f;
hsv_to_rgb(h, s, v, &r, &g, &b);
set_color(r, g, b);
send_color();
vTaskDelay(1);
}
while (v > 0) {
v -= 0.01f;
hsv_to_rgb(h, s, v, &r, &g, &b);
set_color(r, g, b);
send_color();
vTaskDelay(1);
}
}
}