目前,诸如 Arduino 和 Feather 等高级开发平台已经提供了出色的支持,可以通过易于使用的库和普遍使用的示例代码与 NeoPixel LED、灯带、矩阵等相连接。然而,更高级的平台(例如 STM32 开发板)通常缺乏相同水平的支持。因此,希望将NeoPixels整合到项目中的开发人员需要全面了解NeoPixel通信协议以及如何克服它所带来的挑战。
NeoPixels
Adafruit 推出的极受欢迎的可寻址全彩LED灯“NeoPixels”系列分为RGB和RGBW两个种类。尽管二者都将红、绿和蓝色LED与驱动器芯片相集成,但RGBW组件还集成了第四个纯白色的LED。可以使用类似的单线串行接口来控制这两种类型的NeoPixel,其时间值和数据结构仅存在微小的差异。
WS2812
RGB NeoPixels实际上是WS2812智能控制LED,包括数据信号输入引脚(DIN)和数据信号输出引脚(DOUT)。这允许多个LED级联并且只用一个数据线进行控制。链中的第一个LED负责处理从MCU接收到的前三个字节数据,然后将后续的数据简单地转发给DOUT引脚,该引脚可以连接到另一个LED的DIN引脚。LED将以此方式继续向下传递数据,直到它们接收到复位信号为止(即,DIN线在一段时间内持续保持低电平状态)。传输的字节按照图1所示的协议进行组织。第一个字节(G7-G0)表示绿色LED的8位PWM强度,其中0x00是完全关闭,0xFF是完全打开。类似地,第二个字节(R7-R0)用于控制红色LED的强度,第三个字节(B7-B0)用于控制蓝色LED的强度。
这些24位数据都是通过改变方波的脉冲宽度来进行编码的,如图2所示。请注意,无论发送代码0还是代码1,方波的周期仍保持在1.25μs。对于WS2812,使数据线保持低电平至少50μs即可生成复位信号。另请注意,图2中显示的计时值具有±0.15μs的公差。
图 2 : WS2812 LED的0和1位的计时图。
SK6812
一种截然不同的组件,NeoPixels的RGBW种类实际上是SK6812智能控制LED,采用与WS2812 LED相同的运作原理。然而,由于它们包含第四个LED,因此实施了图3所示的4字节数据协议。与图1相比,唯一的区别在于数据的串联字节(W7-W0),该字节指定了白色LED的8位PWM强度。
图4展示了SK6812控制信号的时间值,同样与WS2812略有差别(不过仍在±0.15μs的公差范围内)。请注意,这两种代码的方波周期均保持不变,都为1.2μs。此外,SK6812的复位信号长度为80μs ,而非50μs。
图 4 : SK6812 LED的0位和1位的计时图。
步骤
由于NeoPixel的控制信号对计时要求非常严格,因此除非使用汇编语言,否则无法通过简单的比特带宽方法产生此信号。虽然还有许多其他方法可以利用各种MCU外设、外部硬件或其组合来生成该信号,但其中最直接的方法是配置MCU定时器来生成PWM输出信号。这是因为,如上一部分中所述,NeoPixel控制信号只是一种固定频率的PWM信号,采用不同的占空比表示0位和1位。为了以与传输协议相同的速率高效地在这两个占空比之间进行切换,还必须配置DMA流来管理更新。尽管这种方法可能是内存效率最低的方式,但它易于理解、CPU高效并且易于实施(得益于STM32Cube环境)。
以下应用程式利用STM32CubeIDE(版本1.8.0)、NUCLEO-F401RE开发板和RGBW 5x8 NeoPixel Shield实现上述的方法。不过,这些步骤可以轻松地推广到任何STM32 MCU/板和NeoPixel产品上。假定我们已经创建了一个STM32CubeIDE项目。如需使用其他IDE,你可以改为使用独立的STM32CubeMX代码配置器工具,将项目导出到所需的开发平台上。
1.配置PWM
a. 先打开STM32CubeMX配置.ioc
文件(如果还未打开的话)。随后,STM32CubeIDE将切换到*器件配置工具(*Device Configuration Tool ) 视图,供你配置MCU。
b. 将定时器通道备用功能分配给选定的GPIO引脚,以与NeoPixel进行连接。所选定时器通道应该能够生成PWM输出。图5显示了我的项目中的相关部分,我选择了引脚PB10,并将它分配给定时器2、通道3(TIM2_CH3)功能。
图 5 : 将连接到DIN的GPIO引脚配置为定时器通道。
c. 从左侧的组件列表中选择上一步中确定的定时器外设,以打开模式和配置(*Mode and Configuration ) 面板。在模式(*Mode ) 面板中,选择“内部时钟”作为时钟源,并从适当的定时器通道的下拉列表中选择“PWM生成CHx”。在图6中,定时器2、通道3已设为“PWM生成CH3”模式,因为我在上一步中选择了TIM2_CH3备用功能。请注意,在完成此步骤后,关联的GPIO引脚应在引脚排列视图中从黄色变为绿色。
d. 在定时器的*配置(*Configuration ) 面板中,验证“预分频器”和“脉冲”值是否都设置为0。计数器周期,即自动重载寄存器(ARR),需要进行设置以得到所需的PWM周期(如果使用RGB WS2812 LED,则为1.25μs; 如果使用RGBW SK6812 LED,则为1.2μs)。这将取决于定时器外设输入的速率。只需将所需的PWM周期除以时钟周期,并减去1即可得到此值(减去1是因为定数器从0开始)。就我的器件而言,该公式得出的ARR值为99.8,我将其四舍五入为100(图6)。请参见下文,了解有关计算理想ARR值的详细说明。
图 6 : 将所选定时器通道配置为PWM输出。
计算ARR值
假设定时器“预分频器”值设为0,可以很容易的计算出ARR值
具体来说,ARR值等于PWM信号周期除以定时器外设的时钟信号周期。我们知道,根据使用的NeoPixel类型不同,TPWM可以是1.25μs或1.2μs(例如本例中,TPWM=1.2μs)。要确定Ttimer,你需要查阅器件的规格书,确定定时器外设连接到哪个总线。规格书可以在ST的网站上找到或STM32CubeIDE会随附提供:选择帮助>目标器件文档和资源( Help > Target Device Docs and Resources ) 。然后,在MCU 选项卡下选择规格书,如图7所示。
在我使用的MCU(STM32F401RE)规格书中,器件框图中显示我的定时器(TIM2)已连接到APB1(见图8)。
图9介绍了:通过切换到STM32CubeIDE中的*时钟配置(*Clock Configuration ) 选项卡,我们可以发现TIM2的时钟频率为84MHz (T_{timer} = \frac{1}{f_{timer}} = \frac{1}{84\text{ MHz}})
因此,
为了使PWM周期尽可能接近NeoPixel控制信号的周期,我们四舍五入至最接近的整数并得到 ARR = 100. 。
2.配置DMA
a. 从组件列表中选择DMA外设。
b. 在配置(Configuration) 面板的DMA1 选项卡下,点击添加 ( Add ) 按钮。在下拉菜单中,选择你的定时器/通道组合。在我的项目中,我选择了“TIM2_CH3/UP”。
c. 针对该新的DMA请求,将方向改为“内存到外设”。
d. 同时,将优先级改为“非常高”。
e. 验证默认的DMA请求设置是否与图10中显示的相匹配。
f. 保存.ioc
文件,以生成项目代码。
3.编写代码
在main.c
文件中,按从上到下的顺序编写,本部分展示了一个简单的示例应用,用于测试NeoPixel LED的全彩能力。此处提供了两个版本的main()
函数,一个用于RGB WS2818 LED,另一个用于RGBW SK6812 LED。
a. 在main.c
文件的私有typedef部分,你可以创建一个新的数据类型,以便轻松访问单个LED颜色值以及整个NeoPixel数据结构(如图1和图3所示)。列表1提供了RGB和RGBW NeoPixel组件的typedef。此代码应粘贴在/* USER CODE BEGIN PTD */
和/* USER CODE END PTD */
注释之间。
列表 1 : 为RGB WS2812和RGBW SK6812 LED自定义数据类型
typedef union
{
struct
{
uint8_t b;
uint8_t r;
uint8_t g;
} color;
uint32_t data;
} PixelRGB_t;
typedef union
{
struct
{
uint8_t w;
uint8_t b;
uint8_t r;
uint8_t g;
} color;
uint32_t data;
} PixelRGBW_t;
b. 更改“脉冲”寄存器(也称为CCRx)的值,这样可以改变PWM波形的占空比。因此,我们必须计算适当的CCRx值,以实现使用的NeoPixels所需的代码0和代码1方波(无论是在图2还是图4中所示的那些)。对于RGB WS2812 LED,这些值计算如下:
对于RGBW SK6812 LED,其计算过程稍有不同。
当然,这些计算出的值应该四舍五入到最接近的整数。在 main.c
文件的私有定义部分,为每个值创建一个#define
指令(请参见以下图11中的示例)。
c. 除了CCRx值之外,还应在私有定义部分中定义控制的NeoPixel LED数量和DMA缓冲区大小。如图11所示,只需将LED的数量乘以相应的NeoPixel数据结构中的位数即可(回想图1和图3)。还必须分配一个额外的缓冲区元素,因为最后一个CCRx值应为零(复位信号)。
d. 将列表2中提供的DMA完成回调函数添加到/* USER CODE BEGIN 0 /和/ USER CODE END 0*/之间的私有用户代码部分。务必将 TIM_CHANNEL_x
更改为步骤1c中配置的通道。
列表 2 : HAL_TIM_PWM_PulseFinishedCallback()
函数的实施
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
HAL_TIM_PWM_Stop_DMA(htim, TIM_CHANNEL_x);
}
e. 最后,必须将应用代码添加到main()
函数中。列表3提供了一个使用WS2812 LED的示例main()
函数,而列表4提供了使用SK6812 LED的类似示例main()
函数。请注意,HAL_TIM_PWM_Start_DMA()
函数的TIM_CHANNEL_x
参数必须再次进行修改,以匹配步骤1c中配置的通道。
列表 3 : RGB WS2812 LED的示例main()
函数
int main(void)
{
/* USER CODE BEGIN 1 */
PixelRGB_t pixel[NUM_PIXELS] = {0};
uint32_t dmaBuffer[DMA_BUFF_SIZE] = {0};
uint32_t *pBuff;
int i, j, k;
uint16_t stepSize;
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART2_UART_Init();
MX_DMA_Init();
MX_TIM2_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
k = 0;
stepSize = 4;
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
for (i = (NUM_PIXELS - 1); i > 0; i--)
{
pixel[i].data = pixel[i-1].data;
}
if (k < 255)
{
pixel[0].color.g = 254 - k; //[254, 0]
pixel[0].color.r = k + 1; //[1, 255]
pixel[0].color.b = 0;
}
else if (k < 510)
{
pixel[0].color.g = 0;
pixel[0].color.r = 509 - k; //[254, 0]
pixel[0].color.b = k - 254; //[1, 255]
j++;
}
else if (k < 765)
{
pixel[0].color.g = k - 509; //[1, 255];
pixel[0].color.r = 0;
pixel[0].color.b = 764 - k; //[254, 0]
}
k = (k + stepSize) % 765;
// not so bright
pixel[0].color.g >>= 2;
pixel[0].color.r >>= 2;
pixel[0].color.b >>= 2;
pBuff = dmaBuffer;
for (i = 0; i < NUM_PIXELS; i++)
{
for (j = 23; j >= 0; j--)
{
if ((pixel[i].data >> j) & 0x01)
{
*pBuff = NEOPIXEL_ONE;
}
else
{
*pBuff = NEOPIXEL_ZERO;
}
pBuff++;
}
}
dmaBuffer[DMA_BUFF_SIZE - 1] = 0; // last element must be 0!
HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_x, dmaBuffer, DMA_BUFF_SIZE);
HAL_Delay(10);
}
/* USER CODE END 3 */
}
列表 4 : RGBW SK6812 LED的示例main()
函数
int main(void)
{
/* USER CODE BEGIN 1 */
PixelRGBW_t pixel[NUM_PIXELS] = {0};
uint32_t dmaBuffer[DMA_BUFF_SIZE] = {0};
uint32_t *pBuff;
int i, j, k;
uint16_t stepSize;
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART2_UART_Init();
MX_DMA_Init();
MX_TIM2_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
k = 0;
stepSize = 4;
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
for (i = (NUM_PIXELS - 1); i > 0; i--)
{
pixel[i].data = pixel[i-1].data;
}
if (k < 255)
{
pixel[0].color.g = 254 - k; //[254, 0]
pixel[0].color.r = k + 1; //[1, 255]
pixel[0].color.b = 0;
pixel[0].color.w = 0;
}
else if (k < 510)
{
pixel[0].color.g = 0;
pixel[0].color.r = 509 - k; //[254, 0]
pixel[0].color.b = k - 254; //[1, 255]
pixel[0].color.w = 0;
j++;
}
else if (k < 765)
{
pixel[0].color.g = 0;
pixel[0].color.r = 0;
pixel[0].color.b = 764 - k; //[254, 0]
pixel[0].color.w = k - 509; //[1, 255]
}
else if (k < 1020)
{
pixel[0].color.g = k - 764; //[1, 255]
pixel[0].color.r = 0;
pixel[0].color.b = 0;
pixel[0].color.w = 1019 - k; //[254, 0]
}
k = (k + stepSize) % 1020;
// 50% brightness
pixel[0].color.g >>= 2;
pixel[0].color.r >>= 2;
pixel[0].color.b >>= 2;
pixel[0].color.w >>= 2;
pBuff = dmaBuffer;
for (i = 0; i < NUM_PIXELS; i++)
{
for (j = 31; j >= 0; j--)
{
if ((pixel[i].data >> j) & 0x01)
{
*pBuff = NEOPIXEL_ONE;
}
else
{
*pBuff = NEOPIXEL_ZERO;
}
pBuff++;
}
}
dmaBuffer[DMA_BUFF_SIZE - 1] = 0; // last element must be 0!
HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_x, dmaBuffer, DMA_BUFF_SIZE);
HAL_Delay(10);
}
/* USER CODE END 3 */
}
该项目现在应该能够成功构建,并支持你在器件上运行代码了。
结论
使用逻辑分析仪捕获了上面提供的RGB和RGBW配置生成的控制信号。分别如图12和图13中所示。请注意,它们与图2和图4中指定的预期输出相匹配。