2021-11-14

ws2812 程序设计与应用(1)DMA 控制 PWM 占空比原理及实现(STM32)

传感器与功能模块 专栏收录该内容
10 篇文章 5 订阅

本文开发环境:

  • MCU型号:STM32F1038C8T6
  • IDE环境: MDK 5.27
  • 代码生成工具:STM32CubeMx 5.6.1
  • HAL库版本:STM32Cube_FW_F1_V1.8.0

本文内容:

  • STM32 使用 DMA+PWM 方式驱动 ws2812(x4)

附件:

  • MDK5 示例工程
  • WS2812 中英文数据手册

一、WS2812 简介

WS2812 内部集成了处理芯片和3颗不同颜色的led灯(红,绿,蓝),通过单总线协议分别控制三个灯的亮度强弱,达到全彩的效果,每一个灯需要 8 bits(1 byte) 的数据,所以一颗 ws2812 共需要24 bits(3 bytes) 的数据。

时序

在这里插入图片描述
ws2812 采用 PWM方式来编码,即每个PWM的周期固定为1.25us(800k),占空比为 1/3 时为 0 码,占空比为 2/31 码。另外,ws2812 复位信号为一个 不低于50us的低电平:
在这里插入图片描述

传输

ws2812 的特点是可以多个灯珠串联起来,这样就可以通过一个总线控制多个灯珠:
在这里插入图片描述
ws2812 可以将第一个24字节的时序留下,余下的往下一位传递:
在这里插入图片描述
可以结合波逻辑分析仪捕获的波形来理解:
在这里插入图片描述
上图是4个led的驱动时序,首先是一个100us的低电平复位来ws2812,接着是第一个led灯的数据,它有24个周期,分为3个部分,每一个部分是8个周期分别对应不同颜色的灯,可以看到,第一个部分都是宽占空比,所以全是1,第二个部分都是窄占空比,所以全是0,第三个部分和第一个部分同。所以第一颗led灯,是绿色和蓝色一起亮,视觉效果为青色,其它同理。

时序的数据结构是,高位到低位
在这里插入图片描述
程序实现之前,需要了解清楚时序,更多请参考附件中相关手册。

二、ws2812 驱动的几种方式

本文只讨论 DMA+PWM+TIM 的方式来实现,其他的开发方式还有以下几种:

  • 使用 nop 指令延时,直接翻转IO口产生时序,这种方式最为简单易用,只需要控制nop延时的时间,就可以从产生0和1码,它需要占用系统资源。
  • 使用 SPI 数据传输产生时序
    SPI 只需要控制在合适的波特率,在传输不同数据的时候,可以产生符合要求的0和1码,这种方式需要等同于使用了一个SPI设备
  • 使用 DMA+Timer 产生时序
    这种方式需要使用一个定时器,其中一个通道固定产生一个周期1.25us的PWM,占空比2/3,接着需要另一个通道,在周期的1/3处搬运数据到IO口,若为1,PWM不变,若为0,PWM则为0码,这种方式有更大的局限性,由于DMA只能搬运至少一个字节,所以每次会同时改变8个IO口的高低电平,或许使用位带操作可以解决这问题
  • 使用 DMA+Timer+PWM 产生时序
    本文讨论的实现方案,这种方案有2种驱动的方式,一种是直接建立一个大的数组,存放所有灯珠的数据,然后启动DMA传输,第二种是建立2个灯组数据大小的数组,当DMA传输一个灯珠数据时,改变另一个灯组数据,通过不断改变数组的方式,节约内存,相比较而言,第一种方式较为直观,第二种方式则可以解决灯珠较多的情况,本文讨论第一种的原理和程序的实现。

三、DMA+PWM+TIM 驱动 ws2812

定时器 TIM 用以产生一个固定周期的PWM,DMA用以改变PWM 的占空比:
在这里插入图片描述
如图,DMA通过不断的搬运数据到定时器调节占空比的CCR寄存器,实现ws2812时序的产生,在STM32中,通过配置外设可实现:定时器每产生一次溢出事件(即计数完成),就请求一次DMA搬运一个数据(长度:字节/半字/字可选),所以用户只需要将数据排列在数组里,就可以产生所需要的时序。

四、STM32CubeMx 配置

基础配置

基础配置是每一个工程都必须的,包括系统时钟,指定延时函数定时器,打开调试口等:

  • 选择芯片:STM32F103C8T6
    请添加图片描述
  • 配置系统时钟:设置为外部晶振
    请添加图片描述
  • 配置系统时钟为最高的72M:请添加图片描述
  • 打开 SW 调试口:用以ST-LINK下载和调试程序请添加图片描述

定时器配置

  • 定时器配置:配置定时器1,不分频,72M计数(89+1)个,时间为1.25us。
    请添加图片描述

PWM 配置

  • PWM配置:打开定时器1通道1,作为ws2812时序产生IO口
    请添加图片描述

DMA 配置

  • 配置DMA:连接DMA和定时器1,方向为内存到外设,内存地址需要增长,外设不增长在这里插入图片描述

生成工程

  • 生成工程:选择工程路径,库使用的库等请添加图片描述

五、硬件连接

由 CubeMx 可知,所配置的定时器1通道1在PA8上,所以将ws2812的信号线连接到PA8上,注意ws2812的供电充足,并与STM32共地。

六、程序设计

流程

程序的初始化一般CuMx自动为程序员添加代码,所以驱动ws2812只需要以下三步:

  1. 初始化灯珠的数组
  2. 开启DMA传输
  3. 传输完毕关闭DMA传输

程序

... ...
// DMA 传输完成回调函数
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
    HAL_TIM_PWM_Stop_DMA(&htim1,TIM_CHANNEL_1);
}

//DMA 需要传输的数据
uint16_t pulse[176] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
59,59,59,59,59,59,59,59,
29,29,29,29,29,29,29,29,
59,59,59,59,59,59,59,59,

29,29,29,29,29,29,29,29,
59,59,59,59,59,59,59,59,
29,29,29,29,29,29,29,29,

29,29,29,29,29,29,29,29,
29,29,29,29,29,29,29,29,
59,59,59,59,59,59,59,59,

29,29,29,29,29,29,29,29,
29,29,29,29,29,29,29,29,
29,29,29,29,29,29,29,29,};

//测试函数
void ws2182_test(void)
{
HAL_TIM_PWM_Start_DMA(&htim1,TIM_CHANNEL_1,(uint32_t *)pulse,(176));
}

  • 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

第3行:HAL_TIM_PWM_PulseFinishedCallback() 是一个回调函数,当DMA传输完成以后,就会调用这个函数,由于本文DMA传输模式选择为Circular,所以DMA需要手动关闭,否则DMA会不断的搬运数据。

第9行:这是一个176字节的数组,注意到这不是一个小的数组,所以尽量避免在函数内被申请,请在全局变量区域申请。数组的开始是80个0,DMA 没一个定时器周期就搬运一个0到定时器CCR中,定时器将产生一个1.25us的全低电平,80个为100us,这个100us的低电平作为ws2812的复位信号。
接着,数据在逻辑上被分为4个部分,对应的是4个灯珠的颜色数据,其中,每一个部分又分为3行,每一行,代表该灯珠某一颜色的数据。
在定时器中,由于配置为计90个数一个周期,所以计数到30拉低电平(占空比 1/3)就产生了一个0码,若计数到60个拉低电平(占空比 2/3)就产生了1码,定时器计数个数为其值+1,所以这里数值29,59将会对应产生0码1码

第28行:这是一个示例函数,它使用HAL_TIM_PWM_Start_DMA(),开启本次的DMA传输,运行该函数以后,定时器将被打开,DMA也开始搬运数据。

验证

int main(void)
{
  /* USER CODE BEGIN 1 */

/* 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_DMA_Init();
MX_TIM1_Init();
/ USER CODE BEGIN 2 */

/* USER CODE END 2 */

/* Infinite loop /
/ USER CODE BEGIN WHILE /
ws2182_test(); //调用测试函数
while (1)
{
/ USER CODE END WHILE */

<span class="token comment">/* USER CODE BEGIN 3 */</span>

}
/* USER CODE END 3 */
}

  • 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

验证程序可以在mian()初始化完所有硬件之后,调用ws8212_test()函数,之后程序进入while(1)循环等待。

程序运行正常,将可以看到一个红灯,一个蓝灯,一个青灯,最后一个关闭:
(尽量了 = =|||)

函数封装

上述程序实现了灯效的控制,但是其数组不够灵活,所以需要封装一个函数,这个函数可以通过用户传入的RGB参数,来自动填充数组:

#define ONE_PULSE        (59)                           //1 码计数个数
#define ZERO_PULSE       (29)                           //0 码计数个数
#define RESET_PULSE      (80)                           //80 复位电平个数(不能低于40)
#define LED_NUMS         (4)                            //led 个数
#define LED_DATA_LEN     (24)                           //led 长度,单个需要24个字节
#define WS2812_DATA_LEN  (LED_NUMS*LED_DATA_LEN)        //ws2812灯条需要的数组长度
... ...
uint16_t static RGB_buffur[RESET_PULSE + WS2812_DATA_LEN] = { 0 };
... ...
void ws2812_set_RGB(uint8_t R, uint8_t G, uint8_t B, uint16_t num)
{
    //指针偏移:需要跳过复位信号的N个0
    uint16_t* p = (RGB_buffur + RESET_PULSE) + (num * LED_DATA_LEN);
<span class="token keyword">for</span> <span class="token punctuation">(</span>uint16_t i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>i <span class="token operator">&lt;</span> <span class="token number">8</span><span class="token punctuation">;</span>i<span class="token operator">++</span><span class="token punctuation">)</span>
<span class="token punctuation">{<!-- --></span>
    <span class="token comment">//填充数组</span>
    p<span class="token punctuation">[</span>i<span class="token punctuation">]</span>      <span class="token operator">=</span> <span class="token punctuation">(</span>G <span class="token operator">&lt;&lt;</span> i<span class="token punctuation">)</span> <span class="token operator">&amp;</span> <span class="token punctuation">(</span><span class="token number">0x80</span><span class="token punctuation">)</span><span class="token operator">?</span>ONE_PULSE<span class="token punctuation">:</span>ZERO_PULSE<span class="token punctuation">;</span>
    p<span class="token punctuation">[</span>i <span class="token operator">+</span> <span class="token number">8</span><span class="token punctuation">]</span>  <span class="token operator">=</span> <span class="token punctuation">(</span>R <span class="token operator">&lt;&lt;</span> i<span class="token punctuation">)</span> <span class="token operator">&amp;</span> <span class="token punctuation">(</span><span class="token number">0x80</span><span class="token punctuation">)</span><span class="token operator">?</span>ONE_PULSE<span class="token punctuation">:</span>ZERO_PULSE<span class="token punctuation">;</span>
    p<span class="token punctuation">[</span>i <span class="token operator">+</span> <span class="token number">16</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">(</span>B <span class="token operator">&lt;&lt;</span> i<span class="token punctuation">)</span> <span class="token operator">&amp;</span> <span class="token punctuation">(</span><span class="token number">0x80</span><span class="token punctuation">)</span><span class="token operator">?</span>ONE_PULSE<span class="token punctuation">:</span>ZERO_PULSE<span class="token punctuation">;</span>

/*
}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

其中 p[i] = (G << i) & (0x80)?ONE_PULSE:ZERO_PULSE;是一个三元操作符,其等效以下程序:

        if ((R << i) & (0x80))
        {
            p[i] = ONE_PULSE;
        }
        else
        {
            p[i] = ZERO_PULSE;
        }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

测试

void ws2812_example(void)
{
    //#1.填充数组
    ws2812_set_RGB(0x22, 0x00, 0x00, 0);
    ws2812_set_RGB(0x00, 0x22, 0x00, 1);
    ws2812_set_RGB(0x00, 0x00, 0x22, 2);
    ws2812_set_RGB(0x22, 0x22, 0x22, 3);
    //#2.传输数据
    HAL_TIM_PWM_Start_DMA(&htim1,TIM_CHANNEL_1,(uint32_t *)RGB_buffur,(176));  
    //#3.延时:使效果可以被观察
    HAL_Delay(500);
<span class="token function">ws2812_set_RGB</span><span class="token punctuation">(</span><span class="token number">0x22</span><span class="token punctuation">,</span> <span class="token number">0x00</span><span class="token punctuation">,</span> <span class="token number">0x00</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">ws2812_set_RGB</span><span class="token punctuation">(</span><span class="token number">0x00</span><span class="token punctuation">,</span> <span class="token number">0x22</span><span class="token punctuation">,</span> <span class="token number">0x00</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">ws2812_set_RGB</span><span class="token punctuation">(</span><span class="token number">0x00</span><span class="token punctuation">,</span> <span class="token number">0x00</span><span class="token punctuation">,</span> <span class="token number">0x22</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">ws2812_set_RGB</span><span class="token punctuation">(</span><span class="token number">0x22</span><span class="token punctuation">,</span> <span class="token number">0x22</span><span class="token punctuation">,</span> <span class="token number">0x22</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token function">HAL_TIM_PWM_Start_DMA</span><span class="token punctuation">(</span><span class="token operator">&amp;</span>htim1<span class="token punctuation">,</span>TIM_CHANNEL_1<span class="token punctuation">,</span><span class="token punctuation">(</span>uint32_t <span class="token operator">*</span><span class="token punctuation">)</span>RGB_buffur<span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token number">176</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 

<span class="token function">HAL_Delay</span><span class="token punctuation">(</span><span class="token number">500</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

}

int main(void)
{
... ...
HAL_Init();
... ...
while (1)
{
... ...
ws2812_example();
.... ...
}
/* USER CODE END 3 */
}

  • 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

ws2812_example()函数设置了灯的颜色情况,并通过延时来控制灯的闪烁:
在这里插入图片描述

附件

MDK5工程及中英文手册
提取码:c4r8

</article>
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这个数据可以使用Python进行解析和处理。可以按照以下步骤进行: 1. 将数据分割成每个数据项。 ``` data_items = data.split(',') ``` 2. 对于每个数据项,将其按#分割成四个小项,并存储到一个列表中。 ``` data_list = [] for item in data_items: item_list = item.split('#') data_list.append(item_list) ``` 3. 对于每个小项,进行相应的类型转换。 ``` for item in data_list: item[0] = int(item[0]) # 题号转换为整数 item[1] = datetime.datetime.strptime(item[1], '%Y-%m-%d %H:%M:%S') # 时间转换为datetime类型 if item[2] != '': # 操作类型转换为整数 item[2] = int(item[2]) item[3] = str(item[3]) # 科目转换为字符串类型 ``` 4. 可以按照需要对数据进行进一步处理,如按照题号、时间等进行排序、筛选等操作。 完整的Python代码如下: ``` import datetime data = '''1#2021-05-18 14:31:55##初级会计实务,2#2021-05-18 14:31:57#12#初级会计实务,2#2021-05-18 14:32:08##初级会计实务,3#2021-05-18 14:32:09#12#初级会计实务,4#2021-05-18 14:32:34#12#初级会计实务,4#2021-05-18 14:32:45##初级会计实务,5#2021-05-18 14:32:46#12#初级会计实务,5#2021-05-18 14:32:57##初级会计实务,6#2021-05-18 14:33:00#12#初级会计实务,7#2021-05-18 14:33:44#12#初级会计实务,7#2021-05-18 14:34:42##初级会计实务,8#2021-05-18 14:34:43#12''' # 将数据分割成每个数据项 data_items = data.split(',') # 对于每个数据项,将其按#分割成四个小项,并存储到一个列表中 data_list = [] for item in data_items: item_list = item.split('#') data_list.append(item_list) # 对于每个小项,进行相应的类型转换 for item in data_list: item[0] = int(item[0]) # 题号转换为整数 item[1] = datetime.datetime.strptime(item[1], '%Y-%m-%d %H:%M:%S') # 时间转换为datetime类型 if item[2] != '': # 操作类型转换为整数 item[2] = int(item[2]) item[3] = str(item[3]) # 科目转换为字符串类型 print(data_list) ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值