✅作者简介:热爱科研的嵌入式开发者,修心和技术同步精进
❤欢迎关注我的知乎:对error视而不见
代码获取、问题探讨及文章转载可私信。
☁ 愿你的生命中有够多的云翳,来造就一个美丽的黄昏。
🍎获取更多嵌入式资料可点击链接进群领取,谢谢支持!👇
一、引言
WS2812是一种集控制电路与发光电路于一体的智能外控LED光源,内置信号整形电路,能保证波形畸变不会累加。STM32是意法半导体推出的一系列32位微控制器,具备强大的处理能力和丰富的外设资源。利用STM32的PWM(脉冲宽度调制)和DMA(直接内存访问)功能可以高效地驱动WS2812,实现多彩灯光效果。本文将详细介绍如何使用STM32的PWM和DMA功能来驱动WS2812,并给出相应的代码示例。
二、WS2812工作原理
2.1 信号传输
WS2812采用单总线通信协议,通过一根数据线接收数据。数据以串行方式传输,每个LED灯珠都能接收并转发数据,实现级联控制。
2.2 数据编码
WS2812的数据编码采用脉冲宽度调制,一个比特的数据由不同宽度的脉冲表示。逻辑“0”和逻辑“1”的脉冲宽度不同,通过精确控制脉冲宽度来传输数据。例如,逻辑“0”通常用一个较窄的脉冲表示,逻辑“1”用一个较宽的脉冲表示。
2.3 数据帧格式
每个WS2812灯珠需要24位数据来控制其RGB颜色,依次为绿(G)、红(R)、蓝(B)各8位。多个灯珠级联时,数据依次传输。
三、STM32的PWM和DMA功能
3.1 PWM功能
PWM是一种对模拟信号电平进行数字编码的方法,通过调节脉冲的占空比来控制输出信号的平均电压。STM32的定时器可以产生PWM信号,通过配置定时器的周期和占空比,可以精确控制PWM信号的频率和脉冲宽度。
3.2 DMA功能
DMA允许数据在不经过CPU干预的情况下,直接在内存和外设之间传输。使用DMA可以提高数据传输效率,减轻CPU的负担。在驱动WS2812时,可以使用DMA将存储在内存中的数据直接传输到定时器的比较寄存器,从而实现PWM信号的自动更新。
四、硬件连接
将WS2812的数据线连接到STM32的一个PWM输出引脚,例如PA0(对应定时器TIM2的通道1)。同时,确保WS2812的电源和地连接正确。
五、代码实现
5.1 定时器和DMA初始化
#include "stm32f10x.h"
#define LED_NUM 10 // WS2812灯珠数量
#define LED_DATA_LEN (LED_NUM * 24) // 每个灯珠24位数据
uint16_t led_data[LED_DATA_LEN]; // 存储LED数据
// 定时器和DMA初始化
void TIM2_PWM_DMA_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
DMA_InitTypeDef DMA_InitStructure;
// 使能GPIOA、TIM2和DMA1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 配置PA0为复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置定时器TIM2
TIM_TimeBaseStructure.TIM_Period = 89; // 定时器周期,根据WS2812的时钟要求设置
TIM_TimeBaseStructure.TIM_Prescaler = 0;
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// 配置PWM模式
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
// 配置DMA
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&TIM2->CCR1;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)led_data;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = LED_DATA_LEN;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel2, &DMA_InitStructure);
// 使能DMA请求
TIM_DMACmd(TIM2, TIM_DMA_CC1, ENABLE);
// 使能定时器和DMA
TIM_Cmd(TIM2, ENABLE);
DMA_Cmd(DMA1_Channel2, ENABLE);
}
5.2 数据编码函数
// 编码逻辑0和逻辑1的脉冲宽度
#define T0H 26 // 逻辑0的高电平时间
#define T1H 56 // 逻辑1的高电平时间
// 编码一个字节的数据
void encode_byte(uint8_t byte, uint16_t *data) {
for (int i = 7; i >= 0; i--) {
if ((byte >> i) & 0x01) {
*data++ = T1H;
} else {
*data++ = T0H;
}
}
}
// 编码一个灯珠的RGB数据
void encode_led(uint8_t r, uint8_t g, uint8_t b, uint16_t *data) {
encode_byte(g, data);
data += 8;
encode_byte(r, data);
data += 8;
encode_byte(b, data);
}
// 设置所有灯珠的颜色
void set_all_leds(uint8_t r, uint8_t g, uint8_t b) {
for (int i = 0; i < LED_NUM; i++) {
encode_led(r, g, b, &led_data[i * 24]);
}
}
5.3 主函数
int main(void) {
TIM2_PWM_DMA_Init();
while (1) {
// 设置所有灯珠为红色
set_all_leds(255, 0, 0);
// 启动DMA传输
DMA_Cmd(DMA1_Channel2, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel2, LED_DATA_LEN);
DMA_Cmd(DMA1_Channel2, ENABLE);
// 延时一段时间
for (volatile int i = 0; i < 1000000; i++);
// 设置所有灯珠为绿色
set_all_leds(0, 255, 0);
// 启动DMA传输
DMA_Cmd(DMA1_Channel2, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel2, LED_DATA_LEN);
DMA_Cmd(DMA1_Channel2, ENABLE);
// 延时一段时间
for (volatile int i = 0; i < 1000000; i++);
// 设置所有灯珠为蓝色
set_all_leds(0, 0, 255);
// 启动DMA传输
DMA_Cmd(DMA1_Channel2, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel2, LED_DATA_LEN);
DMA_Cmd(DMA1_Channel2, ENABLE);
// 延时一段时间
for (volatile int i = 0; i < 1000000; i++);
}
}
六、代码解释
6.1 定时器和DMA初始化
在TIM2_PWM_DMA_Init
函数中,首先使能了GPIOA、TIM2和DMA1的时钟。然后配置PA0为复用推挽输出,用于输出PWM信号。接着配置定时器TIM2的周期和PWM模式,以及DMA的相关参数,包括外设地址、内存地址、数据传输方向等。最后使能定时器和DMA。
6.2 数据编码函数
encode_byte
函数用于编码一个字节的数据,将每个比特转换为逻辑0或逻辑1的脉冲宽度。encode_led
函数用于编码一个灯珠的RGB数据,依次编码绿、红、蓝三个字节。set_all_leds
函数用于设置所有灯珠的颜色。
6.3 主函数
在主函数中,首先调用TIM2_PWM_DMA_Init
函数进行初始化。然后进入一个无限循环,依次设置所有灯珠为红、绿、蓝三种颜色,并通过DMA将数据传输到定时器的比较寄存器,实现PWM信号的更新。每次设置颜色后,延时一段时间,以便观察灯光效果。
七、总结
通过使用STM32的PWM和DMA功能,可以高效地驱动WS2812灯珠,实现多彩灯光效果。本文提供的代码示例可以作为一个基础,开发者可以根据实际需求进行扩展,例如实现渐变效果、流水效果等。同时,需要注意定时器的周期和PWM的脉冲宽度设置,以确保与WS2812的通信协议兼容。