• STM32使用SPI驱动WS2812灯带


    由来

    最近有使用ws2812实现大规模灯带的需求,所以研究了一下如何驱动一排排的灯带。

    目前网上有开源的WS2812驱动,它是用Arduino实现的,这些实现都使用arduino的io口模拟ws2812的通信时序,因此具有固有的耗时的缺点。WS2812的数据手册描述如下。

    When the refresh rate is 30fps, low speed model cascade number are not less than 512 points, high speed mode not less than1024 points.

    即在高速模式下,30Fps的帧率可以最多连接1024个LED。

    arduino的驱动一方面依靠模拟通信时序,另一方面arduino的单片机性能本来就比较低,所以较难应对高帧率的刷新要求。所以这里决定使用STM32的SPI进行驱动开发。

    驱动原理

    WS2812灯的驱动时序以800K的速度为例,其采用单线通信的设计,通信协议为非归零编码,每个LED需要24个bit的数据,数据依次经过串联的LED时,第一个LED截取数据开头的24bit,并将剩下的数据流传给下一个LED,以此类推。 那么从这种形式上看,是非常类似SPI的通信时序的,也就是说可以直接使用STM32的SPI外设,只使用MOSI引脚,只要将合适的数据内容丢给SPI,那么SPI就可以输出合适的WS2812通信时序。

    结合STM32的DMA功能,就可以将驱动灯带的功能与CPU隔开,可以达到非常高的效率,即:CPU计算一帧数据到缓存 --> 使用DMA将缓存内容发给SPI --> 驱动灯带。

    因此这里的重点就在于如何让SPI模拟WS2812通信时序,WS2812的通信时序如下:

    image-20210531221246754

    可知,0code1code 由一段时间内的高电平时间来区分。因此每个bitcode需要用多个SPIbit来表示,我设计了两种表示格式,如下图,第一种为使用5个SPIbit表示一个bitcode,第二种使用8个SPIbit表示一个bitcode。

    image-20210531223040274

    为了保证最终通许速率为800k,每一个bitcode持续时间为1.25us,因此,5SPIbit表示法需要4M的SPI速率,8SPIbit表示法需要6.4M的SPI速率。但最后经过实际测试,800k的速度通信时,灯带会存在随机漂移,导致乱码。最后使用8SPIbit表示法,SPI速率在8M,即WS2812通信速度为1M比较合适,不会造成乱码,通信很稳定。

    同样地,最后经过实际测试,发现SPI驱动的灯带存在一个bit的偏移,使用逻辑分析仪测量信号发现是SPI默认电平为1导致的,因为WS2812的通信协议中,默认不发信号的电平应当为0。找了半天也没有发现让SPI MOSI信号默认电平为0的配置,所以可以考虑在发送的缓存中填充长度大于50us的0数据,表示复位信号。

    有了表示法,即可编写相应的程序进行驱动

    程序编写

    相关宏定义和结构体,注意下面的两个结构体都是屏幕的原始数据,最终转换出的WS2812码流需要单独申请一块内存,不需要结构体。

    struct frame_buf {
    	struct led_pixel color;			// 整个屏幕使用统一颜色
    	uint8_t pixel_brightness[LED_NUM]; 	// 每个像素亮度
    };
    
    union ws2812_pixel{					// 单个像素的格式
    	struct {
    		uint8_t g;
    		uint8_t r;
    		uint8_t b;
    	}color;
    	uint8_t data[3];
    };
    
    #define FIVEBIT_0CODE 	0x18
    #define FIVEBIT_1CODE	0x1c
    #define EIGHTBIT_0CODE	0xc0
    #define EIGHTBIT_1CODE	0xf8
    

    转换源码:

    /**
     * 转换成ws2812缓存
     * 	有两种转换模式,
     * 		一种是5个SPI bit 表示一个ws2812bit,要求SPI发送速率为4Mhz,ws2812信号频率为800k
     * 		一种是8个SPI bit 表示一个ws2812bit,要求SPI发送速率为8Mhz,ws2812信号频率为1M
     * 	经实测,还是8bit/1M 的模式比较准确,灯带不会误识别造成乱码,
     * 	因此函数的第四个参数 推荐使用 EIGHTBIT
     */
    int convert2ws2812(struct frame_buf* fbuf, uint8_t *ws_buf, uint16_t buf_len, enum spi_format format){
    
    	union ws2812_pixel pcolor;
    	uint8_t *subpixel = NULL;
    
    	if (format == FIVEBIT){
    		ws_buf[0] = 0;
    		for (uint16_t pos = 0; pos < LED_NUM; pos++) {
    			// 处理当前像素点颜色
    			pcolor.color.r = ((uint16_t)fbuf->color.r * fbuf->pixel_brightness[pos]) >> 8;
    			pcolor.color.g = ((uint16_t)fbuf->color.g * fbuf->pixel_brightness[pos]) >> 8;
    			pcolor.color.b = ((uint16_t)fbuf->color.b * fbuf->pixel_brightness[pos]) >> 8;
    			// 转换每个颜色通道
    			memset(ws_buf + pos * 15, 0, 15);
    			for(uint16_t i = 0; i < 3; i++) {
    				subpixel = ws_buf + pos * 15 + i * 5 + 0;
    				subpixel[0] |= ((pcolor.data[i] & 0x80) ? FIVEBIT_1CODE : FIVEBIT_0CODE) << 3;
    				subpixel[0] |= ((pcolor.data[i] & 0x40) ? FIVEBIT_1CODE : FIVEBIT_0CODE) >> 2;
    				subpixel[1] |= ((pcolor.data[i] & 0x40) ? FIVEBIT_1CODE : FIVEBIT_0CODE) << 6;
    				subpixel[1] |= ((pcolor.data[i] & 0x20) ? FIVEBIT_1CODE : FIVEBIT_0CODE) << 1;
    				subpixel[1] |= ((pcolor.data[i] & 0x10) ? FIVEBIT_1CODE : FIVEBIT_0CODE) >> 4;
    				subpixel[2] |= ((pcolor.data[i] & 0x10) ? FIVEBIT_1CODE : FIVEBIT_0CODE) << 4;
    				subpixel[2] |= ((pcolor.data[i] & 0x08) ? FIVEBIT_1CODE : FIVEBIT_0CODE) >> 1;
    				subpixel[3] |= ((pcolor.data[i] & 0x08) ? FIVEBIT_1CODE : FIVEBIT_0CODE) << 7;
    				subpixel[3] |= ((pcolor.data[i] & 0x04) ? FIVEBIT_1CODE : FIVEBIT_0CODE) << 2;
    				subpixel[3] |= ((pcolor.data[i] & 0x02) ? FIVEBIT_1CODE : FIVEBIT_0CODE) >> 3;
    				subpixel[4] |= ((pcolor.data[i] & 0x02) ? FIVEBIT_1CODE : FIVEBIT_0CODE) << 5;
    				subpixel[4] |= ((pcolor.data[i] & 0x01) ? FIVEBIT_1CODE : FIVEBIT_0CODE) >> 0;
    			}
    		}
    		
    	} else if (format == EIGHTBIT){
    
    		ws_buf[0] = 0;
    		for (uint16_t pos = 0; pos < LED_NUM; pos++) {
    			// 处理当前像素点颜色
    			pcolor.color.r = fbuf->color.r * fbuf->pixel_brightness[pos] / UINT8_MAX;
    			pcolor.color.g = fbuf->color.g * fbuf->pixel_brightness[pos] / UINT8_MAX;
    			pcolor.color.b = fbuf->color.b * fbuf->pixel_brightness[pos] / UINT8_MAX;
    			// 转换每个颜色通道
    			memset(ws_buf + pos * 24, 0, 24);
    			for(uint16_t i = 0; i < 3; i++) {
    				subpixel = ws_buf + pos * 24 + i * 8 + 0;
    				subpixel[0] |= ((pcolor.data[i] & 0x80) ? EIGHTBIT_1CODE : EIGHTBIT_0CODE);
    				subpixel[1] |= ((pcolor.data[i] & 0x40) ? EIGHTBIT_1CODE : EIGHTBIT_0CODE);
    				subpixel[2] |= ((pcolor.data[i] & 0x20) ? EIGHTBIT_1CODE : EIGHTBIT_0CODE);
    				subpixel[3] |= ((pcolor.data[i] & 0x10) ? EIGHTBIT_1CODE : EIGHTBIT_0CODE);
    				subpixel[4] |= ((pcolor.data[i] & 0x08) ? EIGHTBIT_1CODE : EIGHTBIT_0CODE);
    				subpixel[5] |= ((pcolor.data[i] & 0x04) ? EIGHTBIT_1CODE : EIGHTBIT_0CODE);
    				subpixel[6] |= ((pcolor.data[i] & 0x02) ? EIGHTBIT_1CODE : EIGHTBIT_0CODE);
    				subpixel[7] |= ((pcolor.data[i] & 0x01) ? EIGHTBIT_1CODE : EIGHTBIT_0CODE);
    			}
    		}
    	} else return -1;
    	return 0;
    }
    

    程序使用

    #define WS2812_RESET_HEAD 		100		// 100us
    
    main() {
        uint8_t *lsp, *ws_buf;	// 这里申请两个指针
        struct frame_buf fbuf; 	// 屏幕数据
    	uint16_t wsbuflen = 24*LED_NUM + 0; // 采用8SPIbit表示法,每一个LED用24*8bit也就是24byte表示
    
    
    	lsp = malloc(LED_SCREEN_PAYLOAD_LEN);			// 申请屏幕数据缓存
    	ws_buf = malloc(wsbuflen + WS2812_RESET_HEAD);	// WS2812码流缓存,其中有100us长度的0数据
    	memset(ws_buf, 0, WS2812_RESET_HEAD);			// 把前面的一段填充为0
    	
        while(1){
            /* 首先通过一个函数根据传入的参数填充好fbuf,该函数对于本文内容不重要,就不展示源码了 */
            if(HAL_OK == frame_create(lsp, lsp_recv_count, slave_id, &fbuf, LED_BAR_POLAR_UP))
                /* 根据fbuf的内容,以及8SPIbit表示法,填充ws_buf,当然要偏移掉前面的0数据段 */
    			convert2ws2812(&fbuf, ws_buf + WS2812_RESET_HEAD, wsbuflen, EIGHTBIT);
            /* 最后用DMA把ws_buf中的码流发送出去,完成一帧的显示 */
    		HAL_SPI_Transmit_DMA(&hspi1, ws_buf, wsbuflen);
            
            // 下面这一行的延时可以换成别的内容,因为使用DMA+SPI时,数据发送时不占用CPU时间的。
            HAL_Delay(20);
        }
    }
    
  • 相关阅读:
    JDBC第一部分
    java mysql学习第三部分
    java mysql 第六部分
    java mysql学习第五部分
    java mysql学习第二部分
    java mysql学习第一部分
    元注解
    改良之前写的模拟栈代码
    java中如何自定义异常
    java中的语法规则
  • 原文地址:https://www.cnblogs.com/Gentleaves/p/14833810.html
Copyright © 2020-2023  润新知