STM32应用开发——使用PWM+DMA驱动WS2812

news2024/10/6 8:22:40

STM32应用开发——使用PWM+DMA驱动WS2812

目录

  • STM32应用开发——使用PWM+DMA驱动WS2812
    • 前言
    • 1 硬件介绍
      • 1.1 WS2812介绍
        • 1.1.1 芯片简介
        • 1.1.2 引脚描述
        • 1.1.3 工作原理
        • 1.1.4 时序
        • 1.1.5 传输协议
      • 1.2 电路设计
    • 2 软件编程
      • 2.1 软件原理
      • 2.2 测试代码
        • 2.2.1 底层驱动
        • 2.2.2 灯效应用
      • 2.3 运行测试
        • 2.3.1 时序测试
        • 2.3.2 实际效果
    • 结束语

前言

串行灯带的应用十分广泛,其中以WS2812最为经典,这种灯带一般都是通过单总线的方式来驱动,也就是由一根数据线按照特定的时序输出,继而驱动灯带。这种方式在硬件和软件上都非常简单,但是如果软件用GPIO模拟时序的话比较占用主线程的资源,因此,如果能用硬件外设(比如PWM、SPI、串口)来模拟出这个时序,就能节省MCU的资源。
本文以PWM+DMA为例介绍如何驱动WS2812。

1 硬件介绍

1.1 WS2812介绍

1.1.1 芯片简介

WS2812是一款智能控制LED光源,其外观采用最新的MOLDING封装技术、控制电路和RGB芯片集成在2020组件的封装中。其内部包括智能数字端口数据锁存和信号整形放大驱动电路。还包括精密内部振荡器和电压可编程恒流控制部分,有效保证像素点光源的颜色。

1.1.2 引脚描述
引脚名称描述
DO数据输出控制数据输出到下一个芯片
GND电源负极
DI数据输入控制数据输入
VDD电源电源正极
1.1.3 工作原理

通过级联法把每个灯的DI和DO引脚首尾相连,数据可以从第一个IC开始,不断的传输到后面每一个IC,从而实现整条灯带的控制。
在这里插入图片描述

1.1.4 时序

WS2812通过不同的时序来表示0码1码复位码,如下图所示:
在这里插入图片描述
其中各信号的电平如下图所示:
在这里插入图片描述
注:不同型号的芯片在时序上会有点差异,具体以芯片数据手册为准。

1.1.5 传输协议

传输过程如下图所示:
在这里插入图片描述

每一个灯珠的RGB数据排列如下:
在这里插入图片描述

1.2 电路设计

WS2812的控制方法很简单,每个灯珠首尾相接进行级联即可,如下图所示:
在这里插入图片描述
其中,第一个灯珠的DI引脚接入到MCU的一个GPIO上面。

我这里使用STM32F103来作为主控MCU,引脚接线如下:

MCU引脚灯带引脚描述
PA0DI由MCU发送控制信号输入到灯带

2 软件编程

2.1 软件原理

通过DMA可以精确控制PWM输出的每一个方波,然后通过调整占空比,就可以输出0码1码复位码,从而实现灯珠的驱动。
举个例子:按照上面的手册的时序要求,每一个逻辑电平周期在1.25us左右,也就是800kHz,那么PWM输出的频率就可以设置为800kHz。此时改变PWM的占空比,就可以区分编码“0”和编码“1”,比如编码“0”的高电平脉宽和低电平脉宽分别为0.4us和0.85us,那么对应的PWM占空比就是32%和68%,然后通过DMNA连续传输RGB数据就可以实现灯带的颜色和亮度控制。

测试电平时序如下:

逻辑电平脉宽PWM占空比
逻辑0高电平0.40±0.15us32%
逻辑0低电平0.85±0.15us68%
逻辑1高电平0.85±0.15us68%
逻辑1低电平0.40±0.15us32%
复位低电平1.25±0.60us0%

2.2 测试代码

根据上述原理,编写测试代码。

2.2.1 底层驱动

ws2812_driver.h :

#ifndef __WS2812_DRIVER_H
#define __WS2812_DRIVER_H

#include "stm32f10x.h"
#include "stm32f10x_conf.h"

#define TIM2_CCR1_Address 0x40000034  // stm32 tim2 base address offset 0x34

#define LED_NUM     8    // LED的数量
#define RGB_BIT     24   // 每个灯有24bit的RGB数据,依次按G R B排列

#define RESET_WORD  5    // 在传输RGB数据前保持一段低电平
#define DUMMY_WORD  5    // 在传输RGB数据后保持一段低电平

#define TIMING_0    29   // T0H(32%) = 1.25us * (29 / 90) = 0.40us, T0L(68%) = 1.25 - 0.40 = 0.85us 
#define TIMING_1    61   // T1H(68%) = 1.25us * (61 / 90) = 0.85us, T1L(32%) = 1.25 - 0.85 = 0.40us 

void led_display(uint8_t (*led_buf)[3], uint8_t led_num);
void ws2812_init(void);

#endif

ws2812_driver.c :

#include "ws2812_driver.h"
#include "string.h"

uint16_t pwm_dma_buf[RESET_WORD + RGB_BIT * LED_NUM + DUMMY_WORD];

void pwm_init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
    TIM_OCInitTypeDef  TIM_OCInitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
    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);
    GPIO_ResetBits(GPIOA, GPIO_Pin_0);

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    TIM_TimeBaseStructure.TIM_Period = 90 - 1;     // 72MHz / 90 = 800kHz
    TIM_TimeBaseStructure.TIM_Prescaler = 0;
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

    /* PWM2 Mode configuration: Channel1 */
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = 50;
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Reset;
    TIM_OC1Init(TIM2, &TIM_OCInitStructure);

    TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);

	// TIM_ARRPreloadConfig(TIM2, ENABLE);

    /* TIM2 enable counter */
    TIM_Cmd(TIM2, ENABLE);
}

void pwm_dma_init(void)
{
    /* configure DMA */
    DMA_InitTypeDef DMA_InitStructure;//定义DMA初始化结构体

    /* DMA clock enable */
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);	//使能DMA时钟(用于SPI的数据传输)

    memset((uint8_t*)&pwm_dma_buf, 0, sizeof(pwm_dma_buf));

    /* DMA1 Channel5 Config for PWM2 by TIM2_CH1*/
    DMA_DeInit(DMA1_Channel5);
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)TIM2_CCR1_Address;	// physical address of Timer 3 CCR1
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&pwm_dma_buf;		// this is the buffer memory 
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;						// data shifted from memory to peripheral
    DMA_InitStructure.DMA_BufferSize = sizeof(pwm_dma_buf)/2;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;					// automatically increase buffer index
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;							// stop DMA feed after buffer size is reached
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel5, &DMA_InitStructure);

    /* TIM2 DMA Request enable */
    TIM_DMACmd(TIM2, TIM_DMA_CC1, ENABLE);
    TIM_DMACmd(TIM2, TIM_DMA_Update, ENABLE);
}

void pwm_dma_send(void)
{
    DMA_SetCurrDataCounter(DMA1_Channel5, sizeof(pwm_dma_buf)/2); 	// load number of bytes to be transferred
    DMA_Cmd(DMA1_Channel5, ENABLE); 			// enable DMA channel 5
    TIM_Cmd(TIM2, ENABLE); 						// enable Timer 2
    while(!DMA_GetFlagStatus(DMA1_FLAG_TC5)) ; 	// wait until transfer complete
    DMA_Cmd(DMA1_Channel5, DISABLE); 			// disable DMA channel 5
    DMA_ClearFlag(DMA1_FLAG_TC5); 				// clear DMA1 Channel 5 transfer complete flag
    TIM_Cmd(TIM2, DISABLE); 	// disable Timer 2
}

void led_display(uint8_t (*led_buf)[3], uint8_t led_num)
{
	uint8_t i, j;

	// led_buf -> pwm_dma_buf
	for(i = 0; i < led_num; i++)
	{// N led
		for(j = 0; j < 8; j++)
		{// 1 color -> 8bit
			// g
			pwm_dma_buf[RESET_WORD+RGB_BIT*i+j] = ((led_buf[i][1] << j) & 0x80) ? TIMING_1 : TIMING_0;
			// r
			pwm_dma_buf[RESET_WORD+RGB_BIT*i+j+8] = ((led_buf[i][0] << j) & 0x80) ? TIMING_1 : TIMING_0;
			// b
			pwm_dma_buf[RESET_WORD+RGB_BIT*i+j+16] = ((led_buf[i][2] << j) & 0x80) ? TIMING_1 : TIMING_0;
		}
	}
	// pwm start
	pwm_dma_send();
}

void ws2812_init(void)
{
    pwm_init();
    pwm_dma_init();
}
2.2.2 灯效应用

ws2812_app.h :

#ifndef __WS2812_APP_H
#define __WS2812_APP_H

#include "stm32f10x.h"
#include "stm32f10x_conf.h"
#include "ws2812_driver.h"

typedef enum 
{
	LED_MODE_OFF,
	LED_MODE_ALL_ON,	
	LED_MODE_BREATHE,	
	LED_MODE_GRADIENT,
	LED_MODE_FLOW,	
}led_mode_t;

typedef struct
{
    led_mode_t mode;  
    uint8_t g;                
	uint8_t r;              
	uint8_t b;              
	uint8_t brightness;  
}led_t;

void led_init(void);
void led_handle(void);

#endif

ws2812_app.c :

#include "ws2812_app.h"

led_t led;
uint8_t rgb_buf[LED_NUM][3];

void led_init(void)
{
    ws2812_init();

	led.mode = LED_MODE_ALL_ON;
	led.r = 0x00;
	led.g = 0xE0;
	led.b = 0x80;
}

void led_handle(void)
{
	uint8_t i;
    switch (led.mode)
	{
	case LED_MODE_OFF:
		for (i = 0; i < LED_NUM; i++)
		{
			rgb_buf[i][0] = 0;  // r
			rgb_buf[i][1] = 0;  // g
			rgb_buf[i][2] = 0;  // b
		}
		break;
	case LED_MODE_ALL_ON:
		for (i = 0; i < LED_NUM; i++)
		{
			rgb_buf[i][0] = led.r;  // r
			rgb_buf[i][1] = led.g;  // g
			rgb_buf[i][2] = led.b;  // b
		}
		break;
	// ......可以自己加入更多的灯效
	default:
		break;
	}
	
	led_display(rgb_buf, LED_NUM);
}

main.c :

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "ws2812_app.h"

int main(void)
{
    SystemInit();
    delay_init();
    led_init();
    while(1)
    {
        led_handle();
        delay_ms(5);
    }
}

2.3 运行测试

2.3.1 时序测试

使用逻辑分析仪抓取信号,得到的结果如下:

  1. 8个LED连续写入RGB值:
    在这里插入图片描述

  2. 编码1高电平(T1H)850ns:
    在这里插入图片描述

  3. 编码1低电平(T1L)400ns:
    在这里插入图片描述

  4. 编码1周期1.25us:
    在这里插入图片描述

  5. 编码0高电平(T0H)400ns:
    在这里插入图片描述

  6. 编码0高电平(T0H)850ns:
    在这里插入图片描述

  7. 编码0周期1.25us:
    在这里插入图片描述

结论:实际输出的波形和理论一致。

2.3.2 实际效果

用在线颜色选取器把代码设置的颜色值输入进去,得到该颜色,然后和实际灯带点亮的颜色比对。

  1. 颜色拾取器显示如下:
    在这里插入图片描述
  2. 实际灯带颜色如下:
    在这里插入图片描述

结论:灯带实际显示的颜色准确无误。

结束语

关于stm32如何使用PWM+DMA驱动WS2812的讲解就到这里,如果还有什么问题,欢迎在评论区留言。

源码下载链接

如果这篇文章能够帮到你,就…你懂的。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1564834.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

联想 Y9000P 连接网线速度慢 的 问题解决

参考帖子&#xff1a;求助&#xff0c;拯救者Y9000P 2022 i73060版本 有线网非常慢 无线网正常【笔记本吧】_百度贴吧 问题原因&#xff1a; 网卡驱动版本不对。不能用Win11版&#xff0c;要用Win10版。 问题解决&#xff1a; 1、卸载原驱动 2、下载Win10 驱动 并安装 下载…

探寻大数据思想的主要贡献者与核心内容

引言&#xff1a; 在当今数字化时代&#xff0c;大数据已成为企业和科学研究的关键要素。其背后的思想和概念不仅引领了数据处理和分析的革新&#xff0c;也推动了人类对于信息时代的理解与认知。 大数据思想的起源&#xff1a; 在信息爆炸的时代背景下&#xff0c;大数据思…

QT5-qmediaplayer播放视频及进度条控制实例

qmediaplayer是QT5的播放视频的一个模块。它在很多时候还是要基于第三方的解码器。这里以Ubuntu系统为例&#xff0c;记录其用法及进度条qslider的控制。 首先&#xff0c;制作一个简单的界面文件mainwindow.ui&#xff1a; 然后&#xff0c;下载一个mp4或其他格式视频&#x…

Java设计模式:代理模式的静态和动态之分(八)

码到三十五 &#xff1a; 个人主页 心中有诗画&#xff0c;指尖舞代码&#xff0c;目光览世界&#xff0c;步履越千山&#xff0c;人间尽值得 ! 在软件设计中&#xff0c;代理模式是一种常用的设计模式&#xff0c;它为我们提供了一种方式来控制对原始对象的访问。在Java中&a…

微信开发工具——进行网页授权

微信开发工具——进行网页授权 微信公众平台设置 1.在首页创建好自己的订阅号 网站&#xff1a;https://mp.weixin.qq.com/ 点击立即注册,在选择订阅号&#xff08;个人创建使用&#xff09; 之后按流程填写后&#xff0c;点击设置与开发-------->基本配置&#xff0c;这…

【苍穹外卖】SkyApplication类启动报错

报的这个错 The PoM for com.sky:sky-common:jar:1.0-SNAPSHoT is missing, no dependency information available Maven里重新install一下就好

DOTS:Burst

目录 一&#xff1a;简介 1.1 Getting started 1.2 C# language support 1.2.1 HPC# overview 1.2.1.1 Exception expressions 1.2.1.2 Foreach and While 1.2.1.3 Unsupported C# features in HPC# 1.2.2 Static read-only fields and static constructor support 1.…

Linux 使用管理线程、多线程、分离线程

目录 一、使用线程 1、pthread_create创建线程 2、pthread_join等待线程 主线程获取新线程退出结果 获取新线程退出返回的数组 3、线程异常导致进程终止 4、pthread_exit 5、pthread_cancel 6、主线程可以取消新线程&#xff0c;新线程可以取消主线程吗 二、如何管理线…

open Gauss 数据库-03 openGauss数据库维护管理指导手册

发文章是为了证明自己真的掌握了一个知识&#xff0c;同时给他人带来帮助&#xff0c;如有问题&#xff0c;欢迎指正&#xff0c;祝大家万事胜意&#xff01; 目录 前言 openGauss数据库维护管理 1 操作系统参数检查 1.1 实验介绍 1.2 场景设置及操作步骤 2 openGauss 运…

【三】EMQX 手动创建集群

EMQX 手动创建集群 简介 因为项目中使用到了emqx中间件&#xff0c;所以近期对中间件进行了进一步的研究&#xff0c;每次选用中间件我都会考虑可用性方案&#xff0c;如下是本地实践的记录。 一、部署 1、创建一个 Docker 网络&#xff0c;用于节点间通信。处于同一网络下的…

Golang Channel底层实现原理

1、本文讨论Channel的底层实现原理 首先&#xff0c;我们看Channel的结构体 简要介绍管道结构体中&#xff0c;几个关键字段 在Golang中&#xff0c;管道是分为有缓冲区的管道和无缓冲区的管道。 这里简单提一下&#xff0c;缓冲区大小为1的管道和无缓冲区的管道的区别&…

基于Scala开发Spark ML的ALS推荐模型实战

推荐系统&#xff0c;广泛应用到电商&#xff0c;营销行业。本文通过Scala&#xff0c;开发Spark ML的ALS算法训练推荐模型&#xff0c;用于电影评分预测推荐。 算法简介 ALS算法是Spark ML中实现协同过滤的矩阵分解方法。 ALS&#xff0c;即交替最小二乘法&#xff08;Alte…

练习 16 Web [极客大挑战 2019]LoveSQL

extractvalue(1,concat(‘~’, (‘your sql’) ) )报错注入&#xff0c;注意爆破字段的时候表名有可能是table_name不是table_schema 有登录输入框 常规尝试一下 常规的万能密码&#xff0c;返回了一个“admin的密码”&#xff1a; Hello admin&#xff01; Your password is…

Java获取IP地址以及MAC地址(附Demo)

目录 前言1. IP及MAC2. 特定适配器 前言 需要获取客户端的IP地址以及MAC地址 import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader;public class test {public static void main(String[] args) {try {// 执行命令Process process…

基于springboot实现房屋租赁管理系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现房屋租赁系统演示 摘要 房屋是人类生活栖息的重要场所&#xff0c;随着城市中的流动人口的增多&#xff0c;人们对房屋租赁需求越来越高&#xff0c;为满足用户查询房屋、预约看房、房屋租赁的需求&#xff0c;特开发了本基于Spring Boot的房屋租赁系统。 …

蓝桥杯第八届c++大学B组详解

目录 1.购物单 2.等差素数列 3.承压计算 4.方格分割 5.日期问题 6.包子凑数 7.全球变暖 8.k倍区间 1.购物单 题目解析&#xff1a;就是将折扣字符串转化为数字&#xff0c;进行相加求和。 #include<iostream> #include<string> #include<cmath> usin…

【Python项目】基于django的【医用耗材网上申领系统】

医院信息化是社会发展的一个重要标志&#xff0c;它涉及到医院的各个方面&#xff0c;包括人员和物资&#xff0c;因此受到社会各界的广泛关注。近年来&#xff0c;随着医疗耗材数量的不断增加&#xff0c;如何有效管理这些耗材已经成为管理人员、医生以及社会各方面共同面临的…

【Web】记录Polar靶场<困难>难度题一遍过

目录 上传 PHP是世界上最好的语言 非常好绕的命令执行 这又是一个上传 网站被黑 flask_pin veryphp 毒鸡汤 upload tutu Unserialize_Escape 自由的文件上传系统​​​​​​​ ezjava 苦海 你想逃也逃不掉 safe_include CB链 phar PHP_Deserializatio…

Web CSS笔记3

一、边框弧度 使用它你就可以制作盒子边框圆角 border-radius&#xff1a;1个值四个圆角值相同2个值 第一个值为左上角与右下角&#xff0c;第二个值为右上角与左下角3个值第一个值为左上角, 第二个值为右上角和左下角&#xff0c;第三个值为右下角4个值 左上角&#xff0c;右…

Mac下Docker Desktop starting的解决方法

记录下自己在新增了一个新的容器后&#xff0c;Disk Size过大导致启动Docker Desktop会一直卡在Docker Desktop starting&#xff0c;并且重启无效的解决方法。该方法无需重新卸载&#xff0c;并且能保留原有的镜像和容器。 一、确认问题 首先确认Docker.raw大小以确认是否和笔…