【TOLIN】第六章|STM32移植WS2812FX库(上)
作者:Tkwer
公众号:Tkwer望远镜
WS2812B彩灯介绍
在上个推送中我们使用PWM+DMA驱动WS2812B。并且成功移植了Adafruit_NeoPixel库。最近在github上关注了WS2812FX库,这个库的实现函数要比Adafruit_NeoPixel库丰富的多。
很可惜,这个库是基于Arduino和ESP8266、ESP32的,是用C++写的,移植到我们的TOIN核心板上,需要很大的工作量。不过有这个想法,肯定不止我一个!所以:
Thank lamik !这个是他基于STM32F103写的,我们只需要配置底层代码,即配置SPI+DMA,就能在我们的TOIN核心板跑啦。由于WS2812B发送一位有时钟要求,所以我们先回顾一下它的时序图吧。
数据线低电平保持时间大于50us时,为复位信号。复位后,每个LED读取“DIN”线上开始的24bit(绿:红:蓝为8:8:8)数据到驱动芯片内部缓存。除了开始的24bit数据,后面的数据都通过“DOUT”脚传递到下一个LED,即每经过一个像素点的传输,信号减少24bit。内部缓存数据在下一个复位脉冲后被写入PWM控制器。一个bit为1.25us±0.15us,一个LED有3*8bits=24bits,传输完大概需要24*1.25us=30us。(注:在spi中,我们用spi一个字节(8位)来表示这里的一"bit",0 code :11000000 1 code :11111000)
所以在ws2812_spi.c文件中做以下定义了:
1// ___
2// | |_____| 11000000 low level
3
4// _____
5// | |___| 11111000 high level
6#define zero 192U
7#define one 248U
使用STM32CubeMX生成工程模板
本例程我们需要用到SPI和DMA,所以我们在CuBeMX中添加配置一下。
首先选中SPI--->Mode 选择Transmit only master--->配置 Parameter Settings
注意这里的圈出来的时钟范围一定要满足WS2812时钟要求,如果不满足我们需要进行时钟树配置,SPI1是挂在APB2 Peripheral clocks(MHz)时钟上的.(ps:1/1.25us/8 = 6.4MHz,输入误差公式,时钟范围可以是6.4±0.66MHz。6.4MHz/8=800khz,用SPI一个字节传输代表LED的一个“bit”)
DMA配置,一定是Memory to Peripheral 且Mode是circular。作者是通过circular 这个模式节省很多内存资源,稍后再介绍。
或许有的同学在第一步就有这个困惑,为啥我配置的SPI1口在的是PB3,PB5,而自己配置图显示的是PA5,PA7,这个不是很大问题。cubeMX默认是SPI1映射到PA5,PA7,但是我们可以通过选pin脚让它映射到PB3,PB5。
以上就算配置好需要的SPI驱动了,下面我来回答几个问题:
- 为什么要用DMA?
WS2812灯用作一些动态刷新的时候需要传输大量数据,如果不使用DMA,可能我们在用中断的时候破坏了传输数据。这个库也确切用到了SysTick计时器的中断。
- 为什么DMA Mode是Circular 而不是Normal?
从内存方面思考,每个LED消耗24字节,100颗就消耗2.4KB内存(这很不利于我们做Bad apple!),但是我们利用DMA的HAL_SPI_TxHalfCpltCallback回调函数,可以先传送一半存储区的内容,一半用于装载数据,注意装载数据所需时间应该是小于传送出去的时间的。circular就可以实现两个半缓存区交替。缓存区大小不受灯数量影响!
从时间层面,通过DMA发送一半缓冲区(24字节)所需的时间为31 µs。 接下来的24个字节的数据准备仅花费MCU 7 µs。一个LED可以为CPU节省26 µs的时间,这可以做其他事情。如果有100个LED,则为2.6毫秒,而有1000个LED,则CPU需要26毫秒的时间。
数据缓冲区将缩短为48个字节,即它将适合两个二极管的数据。它将很好地包装。操作图如下:
- 加载2 * 24字节的复位信号并开始循环DMA传输。
- 半传输触发器-将另外24个字节加载到前半缓冲区中。
- 完全传输的触发器-第一个LED的数据到缓冲器的后半部分。
- 半传输触发-第二个(偶数)LED的数据传输到缓冲器的前半部分。
- 完全传输的触发器-第三个(奇数)二LED的数据到缓冲器的后半部分。
- 重复4和5,直到所有LED均已发送。
- 完成。
2{
3 CurrentLed = 0;
4 ResetSignal = 0;
5
6 for(uint8_t i = 0; i < 48; i++)
7 buffer[i] = 0x00;
8
9 HAL_SPI_Transmit_DMA(hspi_ws2812b, buffer, 48); // Additional 3 for reset signal
10 while(HAL_DMA_STATE_READY != HAL_DMA_GetState(hspi_ws2812b->hdmatx));
11}
12
13void HAL_SPI_TxHalfCpltCallback(SPI_HandleTypeDef *hspi)
14{
15 if(hspi == hspi_ws2812b)
16 {
17 if(!ResetSignal)
18 {
19 for(uint8_t k = 0; k < 24; k++) // To 72 impulses of reset
20 {
21 buffer[k] = 0x00;
22 }
23 ResetSignal = 1; // End reset signal
24 }
25 else // LEDs Odd 1,3,5,7...
26 {
27 if(CurrentLed > WS2812B_LEDS)
28 {
29 HAL_SPI_DMAStop(hspi_ws2812b);
30 }
31 else
32 {
33 uint8_t j = 0;
34 //GREEN
35 for(int8_t k=7; k>=0; k--)
36 {
37 if((ws2812b_array[CurrentLed].green & (1<<k)) == 0)
38 buffer[j] = zero;
39 else
40 buffer[j] = one;
41 j++;
42 }
43
44 //RED
45 for(int8_t k=7; k>=0; k--)
46 {
47 if((ws2812b_array[CurrentLed].red & (1<<k)) == 0)
48 buffer[j] = zero;
49 else
50 buffer[j] = one;
51 j++;
52 }
53
54 //BLUE
55 for(int8_t k=7; k>=0; k--)
56 {
57 if((ws2812b_array[CurrentLed].blue & (1<<k)) == 0)
58 buffer[j] = zero;
59 else
60 buffer[j] = one;
61 j++;
62 }
63 CurrentLed++;
64 }
65 }
66 }
67}
68
69void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
70{
71 if(hspi == hspi_ws2812b)
72 {
73 if(CurrentLed > WS2812B_LEDS)
74 {
75 HAL_SPI_DMAStop(hspi_ws2812b);
76 }
77 else
78 {
79 // Even LEDs 0,2,0
80 uint8_t j = 24;
81 //GREEN
82 for(int8_t k=7; k>=0; k--)
83 {
84 if((ws2812b_array[CurrentLed].green & (1<<k)) == 0)
85 buffer[j] = zero;
86 else
87 buffer[j] = one;
88 j++;
89 }
90
91 //RED
92 for(int8_t k=7; k>=0; k--)
93 {
94 if((ws2812b_array[CurrentLed].red & (1<<k)) == 0)
95 buffer[j] = zero;
96 else
97 buffer[j] = one;
98 j++;
99 }
100
101 //BLUE
102 for(int8_t k=7; k>=0; k--)
103 {
104 if((ws2812b_array[CurrentLed].blue & (1<<k)) == 0)
105 buffer[j] = zero;
106 else
107 buffer[j] = one;
108 j++;
109 }
110 CurrentLed++;
111 }
112 }
113}
传送门回顾:【TOLIN】第四章|驱动WS2812B彩灯