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是起始条件的长度,通过寄存器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会将接收的每帧数据中的对应数据段丢弃。如下图所示:
通过配置寄存器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。
可调整宁节传输顺序
该功能仅限于调整每一帧数据内不同的 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设备实例。其定义如下:
- typedef struct {
- uint8_t port; /**< spi 端口 */
- hosal_spi_config_t config; /**< spi 配置 */
- hosal_spi_irq_t cb; /**< spi 中断回调函数 */
- void *p_arg; /**< 中断回调函数参数 */
- void *priv; /**< 用户自定义数据 */
- } hosal_spi_dev_t;
复制代码 回调函数定义如下:
- 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,以此类推。数据线上的位由高脉冲编码,然后是低脉冲。时序如下:
WS2812支持高速数据传输并且其数据传输时序与SPI的通信时序类似,因此可以使用BL的SPI外设模拟WS2812的通信时席。
四:WS2812B的SPI驱动实现
1) WS2812B驱动定义。创建一个ws2812_spi.h文件,并添加如下内容:
- #ifndef __WS2812B_SPI_H__
- #define __WS2812B_SPI_H__
- #ifdef __cplusplus
- extern "C" {
- #endif
- #include <stdbool.h>
- #include "hosal_spi.h"
- #define WS2812B_USE_DMA 1
- /**
- * @brief RGB颜色定义
- */
- typedef struct {
- uint8_t r;
- uint8_t g;
- uint8_t b;
- } color_t;
- typedef struct {
- uint16_t id; // 编号
- uint16_t led_counts; // LED数量
- uint8_t *color_datas; // 24位颜色数据
- uint16_t color_data_size; // 颜色数据长度
- color_t *led_colors; // LED颜色
- bool inited; // 是否初始化,1表示已经初始化,0则表示未初始化
- } ws2812b_t;
- /**
- * @brief 初始化
- * @param ws2812b WS2812对象
- */
- void ws2812b_init(ws2812b_t *ws2812b);
- /***
- * @brief 释放WS2812占用内存
- */
- void ws2812b_release(ws2812b_t* ws2812b);
- /**
- * @brief 设置指定位置WS2812B灯珠颜色
- * @param ws2812b WS2812对象
- * @param index WS2812灯珠位置
- * @param color 颜色
- */
- void ws2812b_set_color(ws2812b_t *ws2812b, uint16_t index, color_t color);
- /**
- * @brief 打开WS2812B显示颜色
- * @param ws2812b WS2812B对象
- */
- void ws2812b_show(ws2812b_t *ws2812b);
-
-
- #ifdef __cplusplus
- }
- #endif
- #endif //__WS2812B_SPI_H__
复制代码 2) WS2812驱动实现。创建一个ws2812_spi.c,并添加内容如下:
- #include "ws2812_spi.h"
- #include "blog.h"
- #include <stdlib.h>
- #include <stdio.h>
- // SPI数据为0的时序
- #define SPI_NEO0 ((uint8_t) 0b11000000)
- // SPI数据为1的时序
- #define SPI_NEO1 ((uint8_t) 0b11111100)
- #define BITS_PER_LED_COLOR (sizeof(color_t) * 8)
-
- #if (WS2812B_USE_DMA == 1)
- ws2812b_t * __g_ws2812b__ = NULL;
- #endif
- hosal_spi_dev_t spi;
- void spi_master_cb(void *arg)
- {
-
- blog_info("master send complete\r\n");
- }
- static bool spi_init(void){
- /* spi port set */
- spi.port = 0;
- /* spi master mode */
- spi.config.mode = HOSAL_SPI_MODE_MASTER;
- #if (WS2812B_USE_DMA == 1)
- /* 1: enable dma, 0: disable dma */
-
- spi.config.dma_enable = 1;
- #else
- spi.config.dma_enable = 0;
- #endif
- /* 0: phase 0, polarity low
- * 1: phase 1, polarity low
- * 2: phase 0, polarity high
- * 3: phase 0, polarity high
- */
- spi.config.polar_phase= 0;
- /* 0 ~ 40M */
- spi.config.freq= 8000000;
- spi.config.pin_clk = 3;
- /* hardware cs now is pin 2 */
- spi.config.pin_mosi= 4;
- spi.config.pin_miso= 5;
- /* init spi device */
- hosal_spi_init(&spi);
- /* register trans complete callback */
- hosal_spi_irq_callback_set(&spi, spi_master_cb, (void*)&spi);
- return true;
- }
- void ws2812b_init(ws2812b_t *ws2812b) {
- if(ws2812b == NULL){
- return;
- }
-
- #if (WS2812B_USE_DMA == 1)
- __g_ws2812b__ = ws2812b;
- #else
- ws2812b->inited = spi_init();
- #endif
-
- // 分配24位颜色数据内存
- ws2812b->color_datas = (uint8_t*) malloc(
- ws2812b->led_counts * BITS_PER_LED_COLOR + 32);
- // 分配LED颜色数据内存
- ws2812b->led_colors = (color_t*) malloc(
- sizeof(color_t) * ws2812b->led_counts);
- if (ws2812b->color_datas && ws2812b->led_colors) {
- ws2812b->inited = true;
- ws2812b->color_data_size = ws2812b->led_counts * BITS_PER_LED_COLOR
- + 32;
- } else {
- ws2812b->inited = false;
- }
-
- }
- void ws2812b_release(ws2812b_t* ws2812b){
- if(!ws2812b->inited){
- return;
- }
- // 释放内存
- free(ws2812b->led_colors);
- ws2812b->led_colors = NULL;
- free(ws2812b->color_datas);
- ws2812b->color_datas = NULL;
-
- ws2812b->led_counts = 0;
- ws2812b->inited = false;
- }
- // 生成24位颜色数据序列
- void __build_led_color_data(ws2812b_t *ws2812b) {
- for (int i = 0; i < ws2812b->led_counts; i++) {
- uint8_t m = 0b10000000;
- for (int b = 0; b < 8; b++) {
- ws2812b->color_datas[BITS_PER_LED_COLOR * i + b] =
- ws2812b->led_colors[i].g & m ? SPI_NEO1 : SPI_NEO0;
- m >>= 1u;
- }
- m = 0b10000000;
- for (int b = 0; b < 8; b++) {
- ws2812b->color_datas[BITS_PER_LED_COLOR * i + b + 8] =
- ws2812b->led_colors[i].r & m ? SPI_NEO1 : SPI_NEO0;
- m >>= 1u;
- }
- m = 0b10000000;
- for (int b = 0; b < 8; b++) {
- ws2812b->color_datas[BITS_PER_LED_COLOR * i + b + 16] =
- ws2812b->led_colors[i].b & m ? SPI_NEO1 : SPI_NEO0;
- m >>= 1u;
- }
- }
- }
- void ws2812b_set_color(ws2812b_t *ws2812b, uint16_t index, color_t color) {
- if (index >= ws2812b->led_counts && ws2812b->inited) {
- return;
- }
- ws2812b->led_colors[index] = color;
- }
- void ws2812b_show(ws2812b_t *ws2812b) {
- if (!ws2812b->inited) {
- return;
- }
- __build_led_color_data(ws2812b);
- #if (WS2812B_USE_DMA == 0)
- // hosal_spi_set_cs(14, 0);
- hosal_spi_send(&spi, ws2812b->color_datas,ws2812b->color_data_size,1000);
- //hosal_spi_set_cs(14, 1);
- #else
- #endif
- }
复制代码 3) 驱动程序测试。创建 main.c文件,并添加如下内容:
- #include <stdio.h>
- #include <string.h>
- #include <FreeRTOS.h>
- #include <task.h>
- #include <stdio.h>
- #include <stdbool.h>
- #include <blog.h>
- #include "hosal_spi.h"
- #include "ws2812_spi.h"
- ws2812b_t ws2812b = {
- .id = 1,
- .inited = false,
- .led_counts = 9,
- };
- color_t RED = { 255, 0, 0 };
- color_t GREEN = { 0, 255, 0 };
- color_t BLUE = { 0, 0, 255 };
- void ws2812b_task(void* params){
- printf("ws2812b task start...\r\n");
- while(true){
- for(size_t i = 0;i < ws2812b.led_counts;i++){
- ws2812b_set_color(&ws2812b, i, RED);
- }
- ws2812b_show(&ws2812b);
- vTaskDelay(500);
- for(size_t i = 0;i < ws2812b.led_counts;i++){
- ws2812b_set_color(&ws2812b, i, GREEN);
- }
- ws2812b_show(&ws2812b);
- vTaskDelay(500);
- for(size_t i = 0;i < ws2812b.led_counts;i++){
- ws2812b_set_color(&ws2812b, i, BLUE);
- }
- ws2812b_show(&ws2812b);
- vTaskDelay(500);
- }
- }
- void main(void) {
- printf("start to init spi master...\r\n");
- ws2812b_init(&ws2812b);
- if(ws2812b.inited){
- xTaskCreate(ws2812b_task, "ws2812b_task", 1024, NULL, 15, NULL);
- }
-
-
- }
复制代码 |
-
|