【Ai-WB2中级篇】SPI与WS2812B驱动

[复制链接]
查看1196 | 回复3 | 2024-9-6 10:49:40 | 显示全部楼层 |阅读模式
SPI与WS2812B驱动
串行外设接口(Serial Peripheral|nterface Bus,SPl)是一种用于短程通信的同步串行通信接口规范,设备之间使用全双工模式通信,是一个主机和一个或多个从机的主从模式。 SPI使用4根线完成全双工的通信,这4根信号线分别是:CS(片选)、SCLK(时钟)、MOSI(主机输出从机输入)、MISO(主机输入从机输出)。
本文将详细介绍如何使用Ai-WB2的SPI模块驱动 WS2812B
一:Ai-WB2的SPI介绍
BL602的SPI具有如下特性:
      · 既可作为SPI主设备,也可作为SPI从设备
      · 主从设备都支持4种工作模式(CPOL,CPHA)
      · 主从设备都支持112/3/4字节传输模式
      · 发送和接收通道各有深度为32个字节的 FIFOQ
      · 自适应的FIFO深度变化特性,适配高性能的应用场景

            · 当Frame为32Bits时,FIFO的深度为8
            · 当Frame为24Bits时,FIFO的深度为8
            · 当Frame为16Bits时,FIFO的深度为160
            · 当Frame为8Bits时,FIFO的深度为32

      · 可调整每个Frame的字节传输顺序
      · 可配置每个字节内的MSB/LSB优先传输
      · 灵活的时钟配置,最高可支持80M时钟
      · 接收忽略功能,可以忽略对每个Frame指定位置数据的接收
      · 支持从设备模式下的超时机制
      · 支持DMA传输模式

SPI的时序控制
依照不同的时钟极性和相位设定,SPI时钟共有四种模式,可以通过寄存器spi_confg的cr_spi_sck_pol(CPOL)和cr_spi_sclk_ph(CPHA)进行设置,CPOL用来决定SCK时钟信号空闲时的电平,CPOL=0(cr_spi_sck_pol=0)则空闲电平为低电平,CPOL=1(cr_spi_sck_pol=1)则空闲电平为高电平。CPHA用来决定采样时刻,CPHA=0(cr_spi_sclk_ph=1)则在每个周期的第一个时钟沿采样,CPHA=1(cr_spi_sck_ph=0)则在每个周期的第二个时钟沿采样。通过设置寄存器spi_prd_0和spi_prd_1,还可以调整时钟的开始和结束电平持续时间、每个周期内相位0/1的时间以及每帧数据之间的间隔。四种模式下的具体设置如下图所示:
1.png
其中各数字含义如下:
      · 1是起始条件的长度,通过寄存器spi_prd_0中的cr_spi_prd_s进行配置。
      · 2是停止条件的长度,通过寄存器spi_prd_0中的cr_spi_prd_p进行配置。
      · 3是相位0的长度,通过寄存器spi_prd_0中的cr_spi_prd_d_ph_0进行配置。
      · 4是相位1的长度,通过寄存器spi_prd_0中的cr_spi_prd_d_ph_1进行配置。
      · 5是每帧数据之间的间隔,通过寄存器spi_prd_1中的cr_spi_prd_i进行配置。
主设备连续传输模式
开启该模式后,在发送完当前数据帧后 TX FIFO 里还存在待发送数据时,CS信号不会被拉高。
主从设备数据收发
通过寄存器 spi_confg 中的 cr_spi_fame_size 可以设置数据收发时的 famesize(816/24/32-bit),主从设备应保持相同的 framesize。如果主设备和从设备约定以 32bits的 framesize 进行通信,在某一帧数据中,主设备的 clk 由于异常不满足 32 个周期时,会出现下列现象:
      · 主设备当前发送的这帧数据不会进入从设备的 RX FIFO 中,而是被丢弃,从设备当前发送的数据也不会进入主设备的 RX FIFO 中·。
      · 从设备会认为当前数据帧已经发送结束,等下次主设备 cIk 正常,继续发送下一帧数据。
接收忽略功能
通过设置忽略的开始位和结束位,SPI会将接收的每帧数据中的对应数据段丢弃。如下图所示:
2.png
通过配置寄存器spi_config中的cr_spi_rxd_ignr_en开启忽略功能。通过配置寄存器spi_xd_ignr中的cr_spi_rxd_ignr_s设置忽略功能的起始位。通过配置寄存器 spi_rxd ignr 中的 cr_spi_rxd_ignr_p设置忽略功能的结束位。
如上图所示,如果忽略的开始位设为 0,结東位设为7则Dummy Bvte会被收到,结束位设为15则Dummy Bvte会被丢弃。
SPI的滤波功能
通过使能该功能和设置门限值,SPI会将小于或等于门限值宽度的数据过滤。假设SPltop clock为 160MHz,门限值设为4,则宽度在(4/160MHz=25ns)以下的数据都会被过滤掉。该功能由寄存器 spi_confg 中的 cr_spi_deg_en 进行使能,通过配置 cr_spi_deg_cnt可以设置门限值。 滤波过程如下图所示,假设 cr_spi_deg_cnt 的值设置为4,input为初始数据,output为滤波后的数据。
滤波逻辑过程:
      · tgl为input和output的异或结果。
      · deg_cnt从0开始计数,计数条件为tgl为高电平,并且reached为低电平。
      · 若deg_cnt计数值达到cr_urx_deg_cnt设置的值时,reached为高电平。
      · 当reached为高电平时,将input输出到output。
      · 注释:deg_cnt自加的条件:tgl为高电平且reached为低电平,其余情况下deg_cnt会被清0。

3.png
可调整宁节传输顺序
该功能仅限于调整每一帧数据内不同的 bvte 间的优先传输顺序。通过配置寄存器 spi_confg 中的 cr_spi_bvte_inv 位进行设置。0 表示优先发送低字节,1表示优先发送高字节。 以 frame size 等于 24 bits 传输数据为例,数据格式为:Data[23:0]=0x123456。当设置优先发送低字节,传输的顺序为:0x56(第1个字节:低字节);0x34(第2个字节:中间字节);0x12(第3个字节:高字节);当设置优先发送高字节,传输的顺序为:0x12(第3个字节:高字节);0x34(第2个字节:中间字节);0x56(第1个字节:低字节);
字节传输顺序调整功能可以和 MSB/LSB 传输配置功能配合使用。
可配置每个字节的 MSB/LSB 优先传输
该功能仅限于设置每个 byte 中的8个bits 间的优先传输顺序,通过配置寄存器 spi_confg 中的 cr_spi_bit_inv 位进行设置。0 表示 MSBFirst,1 表示 LSB-First,同样以 frame size 等于24 bits 传输数据为例,数据格式为:Datal23:0]=0x123456
当设置为 MSB-First 传输时,传输的顺序为:01010110(二进制,第1个字节:0x56):00110100(二进制,第2个字节:0x34):00010010(二进制,第3个字节:0x12);
当设置为 LSB-First 传输时,传输的顺序为:01101010(二进制,第1个字节:0x56);00101100(二进制,第2个字节:0x34):01001000(二进制,第3个字节:0x12)。
从模式超时机制
通过寄存器 spi_sto_value 可以设定超时门限值,当 SP|处于从模式且检测到CS被拉低时,会开始计时,如果超过了该超时门限所对应的时间仍未收到时钟信号时,会触发超时中断。
I/O 传输模式
CPU 可以响应来自 FIFO 的中断来执行 FIFO 填充和清空操作。每个 FIFO 都有一个可编程的 FIFO 触发阈值来触发中断。当寄存器spi_fito_confg_1 中的rx_ffo_cnt大于 以rx_ffo_th触发阈值时,将产生 RX请求中断,通知 CPU 读取 RX FIFO 中的数据。 当寄存器spi_fio_confg_1中的tx_fifo_cnt大于tx_ffo_th 时,将产生 TX请求中断,通知 CPU 向 TXFIFO 填充数据。 可以通过査询 FIFO 状态寄存器来确定 FIFO 中的采样值以及 FIFO 的状态。 需要确保正确的 RX FIFO 触发阈值和 TX FIFO 触发阈值,以防止 FIFO overfiow 或underflow。
DMA 传输模式
SPI支持 DMA传输模式。使用该模式需要分别设置 TX和 RXFIFO 的阈值,将寄存器 spi_fifo_config_0中的 spi_dma_tx_en置 1,则开启 DMA发送模式。 将寄存器 spi_fifo_config_0 中的spi_dma_rx_en 置 1,则开启 DMA 接收模式。 当该模式启用后,SPI 会对TX/RX FIEO 进行检査。一旦寄存器 spi_ffo_config_1中的 tx_fifo_cnt/rx_fifo_cnt大于tx_fifo_th/rx_fifo_th,将会发起 DMA请求,DMA会按照设定将数据搬移至 TX FIFO 中或从 RX FIFO 中移出。
SPI中断
SPI 有着丰富的中断控制,包括以下几种中断模式:
· SPI传输结束中断
      · 在主模式下,SPI传输结束中断会在每帧数据传输结束时触发。
      · 在从模式下,SPI传输结束中断会在 CS 信号被拉高时触发。

· TX FIFO 请求中断
      · TX FIFO 请求中断会在其 FIFO 可用计数值大于设定的阈值时触发,当条件不满足时该中断标志会自动清除。
· RX FIFO 请求中断
      · RX FIFO 请求中断会在其 FIFO 可用计数值大于设定的阈值时触发,当条件不满足时该中断标志会自动清除。
· 从模式传输超时中断
      · 从模式传输超时中断会在从模式下检测到CS拉低之后,超过超时门限值对应的时间后仍未收到时钟信号时触发。
· 从模式 TX 过载中断
      · 从模式 TX 过载中断会在从模式下 TX 没有准备好数据传输而时钟信号却已经到来时触发。
· TX/RX FIFO 溢出中断
      · 如果 TX/RX FIFO 发生了上溢或者下溢,会触发 TX/RX FIFO 溢出中断,当 FIFO 清除寄存器 spi_fifo_config_0 中的tx_fifo_clr/rx_fifo_clr 被置1时,对应的 FIFO 会被清空,同时溢出中断标志会自动清除。
可通过寄存器 SPI_INT_STS 查询各中断状态和对相应的位写 1 清除中断。
二:SPI驱动API介绍
BL602 SPI的HOSAL层的驱动AP!在文件 components/platform/hosal/include/hosa1_spi.h 中定义。常用的AP!如下:
· int hosal_spi_init(hosal_spi_dev_t *spi):SPl初始化。参数说明如下:
      · spi:SPl设备实例。其定义如下:
  1.   typedef struct {
  2.       uint8_t port;               /**< spi 端口 */
  3.       hosal_spi_config_t  config; /**< spi 配置 */
  4.       hosal_spi_irq_t cb;         /**< spi 中断回调函数 */
  5.       void *p_arg;                /**< 中断回调函数参数 */
  6.       void *priv;                 /**< 用户自定义数据 */
  7.   } hosal_spi_dev_t;
复制代码
回调函数定义如下:
  1. typedef void (*hosal_spi_irq_t)(void *parg);
复制代码
      · 返回值:成功时,返回0;否则返回非零。
· int hosal_spi_ send(hosal_spi _dev_t *spi, const uint8_t*data, uint16_t size, uint32_t timeout):SPI发送数据。参数说明如下:
      · spi: SPl设备
      · data:需要发送的数据
      · size:发送数据长度
      · timeout:通信时长。以毫秒为单位
      · 返回值:成功时,返回0;否则返回非零。

· int hosal_spi _recv(hosal_spi_dev _t *spi, uint8_t*data, uint16_t size, uint32_t timeout):SPI接收数据。参数说明如下:
      · spi: SPl设备
      · data:接收数据缓存
      · size:接收数据长度
      · timeout:通信时长。以毫秒为单位
      · 返回值:成功时,返回0;否则返回非零。

· int hosal_spi_send_recv(hosal_spi_dev_t*spi, uint8_t *tx_data, uint8_t*rx_data, uint16_t size, uint32_t timeout):发送并接收数据。参数说明如下:
      · spi:SPl设备
      · tx_data:发送数据
      · rx_data:接收数据
      · size:数据长度
      · timeout:通信时长。以毫秒为单位
      · 返回值:成功时,返回0;否则返回非零。

· int hosal_ spi_irq_callback_set(hosal_spi_dev_t*spi, hosal_ spi_irg_t pfn, void *p_arg):设置SPI中断回调函数。参数说明如下:
      · spi:SPl设备
      · pfn:回调函数
      · p_arg:回调函数参数
      · 返回值:成功时,返回0;否则返回非零。

· int hosal_spi_set_cs(uint8_t pin,uint8_t value):设置片选引脚电平。参数说明如下:
      · pin:CS引脚
      · value:电平值。1表示高电平:0表示低电平
      · 返回值:成功时,返回0;否则返回非零。
· int hosal_spi_finalize(hosal_spi_dev_t*spi):销毁SPI设备实例,并释放相关资源。参数说明如下:
      · spi:SPl设备实例
      · 返回值:成功时,返回0;否则返回非零。

三:WS2812B介绍
WS2812采用单线通信的设计,通信协议为非归零编码,每个LED需要24个bit的数据,数据依次经过串联的LED时,第一个LED截取数据开头的24bit,并将剩下的数据流传给下一个LED,以此类推。数据线上的位由高脉冲编码,然后是低脉冲。时序如下:
4.png
WS2812支持高速数据传输并且其数据传输时序与SPI的通信时序类似,因此可以使用BL的SPI外设模拟WS2812的通信时席。
四:WS2812B的SPI驱动实现
1) WS2812B驱动定义。创建一个ws2812_spi.h文件,并添加如下内容:
  1. #ifndef __WS2812B_SPI_H__
  2. #define __WS2812B_SPI_H__
  3. #ifdef __cplusplus
  4. extern "C" {
  5. #endif

  6. #include <stdbool.h>
  7. #include "hosal_spi.h"

  8. #define WS2812B_USE_DMA 1

  9. /**
  10. * @brief RGB颜色定义
  11. */
  12. typedef struct {
  13.         uint8_t r;
  14.         uint8_t g;
  15.         uint8_t b;
  16. } color_t;

  17. typedef struct {
  18.         uint16_t id; // 编号
  19.         uint16_t led_counts; // LED数量
  20.         uint8_t *color_datas; // 24位颜色数据
  21.         uint16_t color_data_size; // 颜色数据长度
  22.         color_t *led_colors; // LED颜色
  23.         bool inited; // 是否初始化,1表示已经初始化,0则表示未初始化
  24. } ws2812b_t;

  25. /**
  26. * @brief 初始化
  27. * @param ws2812b WS2812对象
  28. */
  29. void ws2812b_init(ws2812b_t *ws2812b);

  30. /***
  31. * @brief 释放WS2812占用内存
  32. */
  33. void ws2812b_release(ws2812b_t* ws2812b);

  34. /**
  35. * @brief 设置指定位置WS2812B灯珠颜色
  36. * @param ws2812b WS2812对象
  37. * @param index WS2812灯珠位置
  38. * @param color 颜色
  39. */
  40. void ws2812b_set_color(ws2812b_t *ws2812b, uint16_t index, color_t color);

  41. /**
  42. * @brief 打开WS2812B显示颜色
  43. * @param ws2812b WS2812B对象
  44. */
  45. void ws2812b_show(ws2812b_t *ws2812b);
  46.          
  47.          
  48. #ifdef __cplusplus
  49. }
  50. #endif

  51. #endif //__WS2812B_SPI_H__
复制代码
2) WS2812驱动实现。创建一个ws2812_spi.c,并添加内容如下:
  1. #include "ws2812_spi.h"
  2. #include "blog.h"
  3. #include <stdlib.h>
  4. #include <stdio.h>

  5. // SPI数据为0的时序
  6. #define SPI_NEO0 ((uint8_t) 0b11000000)
  7. // SPI数据为1的时序
  8. #define SPI_NEO1 ((uint8_t) 0b11111100)

  9. #define BITS_PER_LED_COLOR (sizeof(color_t) * 8)
  10.         
  11. #if (WS2812B_USE_DMA == 1)
  12. ws2812b_t * __g_ws2812b__ = NULL;
  13. #endif

  14. hosal_spi_dev_t spi;

  15. void spi_master_cb(void *arg)
  16. {
  17.    
  18.     blog_info("master send complete\r\n");
  19. }

  20. static bool spi_init(void){
  21.     /* spi port set */
  22.     spi.port = 0;
  23.     /* spi master mode */
  24.     spi.config.mode  = HOSAL_SPI_MODE_MASTER;  
  25. #if (WS2812B_USE_DMA == 1)
  26.     /* 1: enable dma, 0: disable dma */
  27.      
  28.     spi.config.dma_enable = 1;
  29. #else
  30.     spi.config.dma_enable = 0;
  31. #endif
  32.      /* 0: phase 0, polarity low                                          
  33.       * 1: phase 1, polarity low                                          
  34.       * 2: phase 0, polarity high                                          
  35.       * 3: phase 0, polarity high
  36.       */
  37.     spi.config.polar_phase= 0;               
  38.     /* 0 ~ 40M */
  39.     spi.config.freq= 8000000;
  40.     spi.config.pin_clk = 3;
  41.     /* hardware cs now is pin 2 */
  42.     spi.config.pin_mosi= 4;
  43.     spi.config.pin_miso= 5;
  44.     /* init spi device */
  45.     hosal_spi_init(&spi);

  46.     /* register trans complete callback */
  47.     hosal_spi_irq_callback_set(&spi, spi_master_cb, (void*)&spi);
  48.     return true;
  49. }

  50. void ws2812b_init(ws2812b_t *ws2812b) {
  51.         if(ws2812b == NULL){
  52.                 return;
  53.         }
  54.         
  55. #if (WS2812B_USE_DMA == 1)
  56.     __g_ws2812b__ = ws2812b;
  57. #else
  58.     ws2812b->inited = spi_init();
  59. #endif
  60.         
  61.         // 分配24位颜色数据内存
  62.         ws2812b->color_datas = (uint8_t*) malloc(
  63.                         ws2812b->led_counts * BITS_PER_LED_COLOR + 32);
  64.      // 分配LED颜色数据内存
  65.         ws2812b->led_colors = (color_t*) malloc(
  66.                         sizeof(color_t) * ws2812b->led_counts);
  67.         if (ws2812b->color_datas && ws2812b->led_colors) {
  68.                 ws2812b->inited = true;
  69.                 ws2812b->color_data_size = ws2812b->led_counts * BITS_PER_LED_COLOR
  70.                                 + 32;
  71.         } else {
  72.                 ws2812b->inited = false;
  73.         }
  74.         
  75. }

  76. void ws2812b_release(ws2812b_t* ws2812b){
  77.         if(!ws2812b->inited){
  78.                 return;
  79.         }
  80.     // 释放内存
  81.         free(ws2812b->led_colors);
  82.         ws2812b->led_colors = NULL;
  83.         free(ws2812b->color_datas);
  84.         ws2812b->color_datas = NULL;
  85.         
  86.         ws2812b->led_counts = 0;
  87.         ws2812b->inited = false;
  88. }

  89. // 生成24位颜色数据序列
  90. void __build_led_color_data(ws2812b_t *ws2812b) {
  91.         for (int i = 0; i < ws2812b->led_counts; i++) {

  92.                 uint8_t m = 0b10000000;
  93.                 for (int b = 0; b < 8; b++) {
  94.                         ws2812b->color_datas[BITS_PER_LED_COLOR * i + b] =
  95.                                         ws2812b->led_colors[i].g & m ? SPI_NEO1 : SPI_NEO0;
  96.                         m >>= 1u;
  97.                 }

  98.                 m = 0b10000000;
  99.                 for (int b = 0; b < 8; b++) {
  100.                         ws2812b->color_datas[BITS_PER_LED_COLOR * i + b + 8] =
  101.                                         ws2812b->led_colors[i].r & m ? SPI_NEO1 : SPI_NEO0;
  102.                         m >>= 1u;
  103.                 }

  104.                 m = 0b10000000;
  105.                 for (int b = 0; b < 8; b++) {
  106.                         ws2812b->color_datas[BITS_PER_LED_COLOR * i + b + 16] =
  107.                                         ws2812b->led_colors[i].b & m ? SPI_NEO1 : SPI_NEO0;
  108.                         m >>= 1u;
  109.                 }
  110.         }
  111. }

  112. void ws2812b_set_color(ws2812b_t *ws2812b, uint16_t index, color_t color) {
  113.         if (index >= ws2812b->led_counts && ws2812b->inited) {
  114.                 return;
  115.         }
  116.         ws2812b->led_colors[index] = color;

  117. }

  118. void ws2812b_show(ws2812b_t *ws2812b) {
  119.         if (!ws2812b->inited) {
  120.                 return;
  121.         }
  122.         __build_led_color_data(ws2812b);
  123. #if (WS2812B_USE_DMA == 0)
  124.    // hosal_spi_set_cs(14, 0);
  125.         hosal_spi_send(&spi, ws2812b->color_datas,ws2812b->color_data_size,1000);
  126.     //hosal_spi_set_cs(14, 1);
  127. #else

  128. #endif
  129. }
复制代码
3) 驱动程序测试。创建 main.c文件,并添加如下内容:
  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <FreeRTOS.h>
  4. #include <task.h>
  5. #include <stdio.h>
  6. #include <stdbool.h>
  7. #include <blog.h>
  8. #include "hosal_spi.h"
  9. #include "ws2812_spi.h"

  10. ws2812b_t ws2812b = {
  11.         .id = 1,
  12.         .inited = false,
  13.         .led_counts = 9,
  14. };

  15. color_t RED = { 255, 0, 0 };
  16. color_t GREEN = { 0, 255, 0 };
  17. color_t BLUE = { 0, 0, 255 };

  18. void ws2812b_task(void* params){
  19.     printf("ws2812b task start...\r\n");
  20.     while(true){
  21.         for(size_t i = 0;i < ws2812b.led_counts;i++){
  22.                         ws2812b_set_color(&ws2812b, i, RED);
  23.                 }
  24.                 ws2812b_show(&ws2812b);

  25.                 vTaskDelay(500);

  26.                 for(size_t i = 0;i < ws2812b.led_counts;i++){
  27.                         ws2812b_set_color(&ws2812b, i, GREEN);
  28.                 }
  29.                 ws2812b_show(&ws2812b);

  30.                 vTaskDelay(500);

  31.                 for(size_t i = 0;i < ws2812b.led_counts;i++){
  32.                         ws2812b_set_color(&ws2812b, i, BLUE);
  33.                 }
  34.                 ws2812b_show(&ws2812b);

  35.                 vTaskDelay(500);
  36.     }
  37. }

  38. void main(void) {
  39.      printf("start to init spi master...\r\n");
  40.     ws2812b_init(&ws2812b);
  41.     if(ws2812b.inited){
  42.         xTaskCreate(ws2812b_task, "ws2812b_task", 1024, NULL, 15, NULL);
  43.     }
  44.    
  45.    
  46. }
复制代码
3.png

本帖被以下淘专辑推荐:

用心做好保姆工作
回复

使用道具 举报

iiv | 2024-9-7 13:40:10 | 显示全部楼层
园长好厉害
回复 支持 反对

使用道具 举报

djy876 | 2024-9-10 15:44:28 | 显示全部楼层
学习打卡
回复

使用道具 举报

King6688 | 2024-9-11 19:32:05 | 显示全部楼层
支持
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则