公司动态
SPI+DMA驱动WS2812
01学习背景与目标我在学习单片机的SPI通信的时候最先接触的是HAL库所以大部分的外设都是通过CUBEMAX进行直接配置会使用SPI但在我用国产芯片进行开发的时候更多的是标准库所以在进行初始化的时候需要我手动配置我需要明白我的驱动需要怎样的SPI方式什么样的速度等我们在掌握某一个知识点的时候一定要明白它的底层逻辑不能只知道表面所以我将以SPIDMAWS2812B这一套驱动的使用来彻底理解SPI。02SPI 基础概念首先我们得明白SPI是什么我一般喜欢根据缩写联想到英文全称这样我看到就会知道中文全称明白它大概是什么。SPI全称Serial Peripheral Interface 串行外设接口1. 串行串行之外的就是并行我们可以想象一个字节8bit通过串行进行传输也就是利用一根传输线1bit进行传输而并行就是通过八根假设传输线进行传输一次性传8bit速度快但是由于线多所以干扰大布线复杂。2. 外设除MCU内核之外的用来扩展单片机功能的硬件模块所以它是单片机自带的硬件模块。3. 接口两个设备之间交换信息的规则和通道。通信接口如上图所示在使用SPI的时候我们需要用到SCLK、MOSI、MISO、CS四根线来进行驱动这是标准的SPI信号传输但是由于WS2812B只需要DATA线就可以所以本质是利用SPI产生一个固定时间宽度的单线脉冲协议。03SPI DMA WS2812B 实战void SPI1_UserInit(void) { // 声明相关结构体 SPI_InitTypeDef SPI_InitStructure; DMA_InitTypeDef DMA_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; // 外设时钟使能 RCC_AHBPeriphClk_Enable(RCC_AHB_PERIPH_GPIOA | RCC_AHB_PERIPH_DMA, ENABLE); SPIx_APBClkENx(SPIx_CLK, ENABLE); SPIx_MOSI_AF(); // 复用相关GPIO GPIO_InitStructure.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStructure.Speed GPIO_SPEED_HIGH; GPIO_InitStructure.Pins SPIx_MOSI_GPIO_PIN; GPIO_Init(SPIx_MOSI_GPIO_PORT, GPIO_InitStructure); // 初始化SPI DMA TX DMA_InitStructure.DMA_Mode DMA_MODE_BLOCK; // DMA_MODE_BLOCK; DMA_InitStructure.DMA_TransferWidth DMA_TRANSFER_WIDTH_8BIT; DMA_InitStructure.DMA_SrcInc DMA_SrcAddress_Increase; DMA_InitStructure.DMA_DstInc DMA_DstAddress_Fix; DMA_InitStructure.TrigMode DMA_HardTrig; // DMA_SWTrig; DMA_InitStructure.HardTrigSource SPIx_DMA_TxTrigSource; DMA_InitStructure.DMA_TransferCnt SPIxTxBufSize; DMA_InitStructure.DMA_SrcAddress (uint32_t) SPIxTxBuf; DMA_InitStructure.DMA_DstAddress (uint32_t) SPIx-DR; DMA_Init(SPIx_TX_DMACHANNEL, DMA_InitStructure); DMA_Cmd(SPIx_TX_DMACHANNEL, DISABLE); // 配置SPI 4Mbps SPI_InitStructure.SPI_Direction SPI_Direction_1Line_TxOnly; // SPI_InitStructure.SPI_Mode SPI_Mode_Master; // 主机模式 SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; // 帧数据长度为8bit SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; // 时钟空闲电平为低 SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; // 第一个边沿采样 SPI_InitStructure.SPI_NSS SPI_NSS_Soft; // 片选信号由SSI寄存器控制 SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_8; // 波特率为PCLK的8分频 - 4MHz SPI_InitStructure.SPI_FirstBit SPI_FirstBit_LSB; // SPI_InitStructure.SPI_Speed SPI_Speed_High; // 高速SPI SPI_Init(SPIx, SPI_InitStructure); SPI_Cmd(SPIx, ENABLE); SPI_DMACmd(SPIx, SPI_DMAReq_Tx, ENABLE); }1. 芯片型号CW32F0302. 驱动WS2812B3. 只使用了MOSI4. SPIx_MOSI_GPIO_PINPA75. 利用DMA进行传输03.1通信方向SPI_Direction#define SPI_Direction_2Lines_FullDuplex ((uint16_t)0x0000) // 双向全双工 #define SPI_Direction_1Line_TxOnly ((uint16_t)0x4000) // 单工单发 #define SPI_Direction_1Line_RxOnly ((uint16_t)0x8000) // 单工单收 #define SPI_Direction_1Lines_HalfDuplex ((uint16_t)0xC000) // 单线半双工这里是配置数据通信模式这里我们选择的是单工单发。03.2主从模式SPI_Mode#define SPI_Mode_Master ((uint16_t)0x0004) #define SPI_Mode_Slave ((uint16_t)0x0000)这里配置的是SPI主从模式我们只需要配置主机模式。03.3数据帧长度SPI_DataSize#define SPI_DataSize_4b ((uint16_t)0x0C00) #define SPI_DataSize_5b ((uint16_t)0x1000) #define SPI_DataSize_6b ((uint16_t)0x1400) #define SPI_DataSize_7b ((uint16_t)0x1800) #define SPI_DataSize_8b ((uint16_t)0x1C00) #define SPI_DataSize_9b ((uint16_t)0x2000) #define SPI_DataSize_10b ((uint16_t)0x2400) #define SPI_DataSize_11b ((uint16_t)0x2800) #define SPI_DataSize_12b ((uint16_t)0x2C00) #define SPI_DataSize_13b ((uint16_t)0x3000) #define SPI_DataSize_14b ((uint16_t)0x3400) #define SPI_DataSize_15b ((uint16_t)0x3800) #define SPI_DataSize_16b ((uint16_t)0x3C00)WS2812通信数据结构这里配置的是单帧数据宽度我们选择8bit这里需要根据通信协议的需要进行选择。SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; // 时钟空闲电平为低#define SPI_CPOL_Low ((uint16_t)0x0000) #define SPI_CPOL_High ((uint16_t)0x0002)它决定的是SCLK时钟线在空闲时保持什么电平我们选择低电平。SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; // 第一个边沿采样#define SPI_CPHA_1Edge ((uint16_t)0x0000) #define SPI_CPHA_2Edge ((uint16_t)0x0001)这里配置的是什么时候开始采样需要结合SPI_CPOL一起来看也就是时钟空闲为低电平在第一个上升沿采样第一个下降沿移位。SPI_InitStructure.SPI_NSS SPI_NSS_Soft; // 片选信号由SSI寄存器控制#define SPI_NSS_Soft ((uint16_t)0x0200) #define SPI_NSS_Hard ((uint16_t)0x0000)这里配置的是软件控制片选WS2812B严格来说不算SPI通信设备因此作用不大。SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_8; // 波特率为PCLK的8分频 - 4MHz#define SPI_BaudRatePrescaler_2 ((uint16_t)0x0000) #define SPI_BaudRatePrescaler_4 ((uint16_t)0x0008) #define SPI_BaudRatePrescaler_8 ((uint16_t)0x0010) #define SPI_BaudRatePrescaler_16 ((uint16_t)0x0018) #define SPI_BaudRatePrescaler_32 ((uint16_t)0x0020) #define SPI_BaudRatePrescaler_64 ((uint16_t)0x0028) #define SPI_BaudRatePrescaler_128 ((uint16_t)0x0030)SCLK每秒产生600万个周期T ≈ 0.166usSPI_DataSize SPI_DataSize_8b所以发送一个字节需要8个SCLK周期。8×0.166us 1.328us由于WS2812的传输要求一个0或者1的传输时间大概在1.25us所以刚好符合要求。SPI_InitStructure.SPI_FirstBit SPI_FirstBit_LSB; //#define SPI_FirstBit_MSB ((uint16_t)0x0000) #define SPI_FirstBit_LSB ((uint16_t)0x0080)数据帧高低位顺序选择这里选择的是低位在前根据WS2812的数据结构来的。SPI_InitStructure.SPI_Speed SPI_Speed_High; // 高速SPI这里决定的是GPIO输出高低的速度快慢。也就是边沿变化是否陡峭。为什么使用DMA进行传输因为驱动WS2812需要数据一直传输如果不使用DMA进行传输会占用大量的CPU资源不利于程序的运行使用DMA将数组内的数据直接搬运到SPI的数据寄存器这样不需要占用CPU资源能够高效快速的进行数据的传输。WS2812驱动void LedSpi24byte(unsigned char r, unsigned char g, unsigned char b, unsigned char * buf, int idx) { buf buf idx * 24 LED_SPI_RESET_TIME; unsigned int dat (g 16) | (r 8) | b; for (unsigned char i 0; i 24; i) { if (dat BIT(23 - i)) { *(buf i) 0x3E; } else { *(buf i) 0x0C; } } } void LedSpiSendBuf(unsigned short led_num) { LedSpiErrCheck(); if (!led_num) { return; } if (led_num LED_SPI_NUM_MAX) { led_num LED_SPI_NUM_MAX; } SPIx_TX_DMACHANNEL-CNT bv16 | SPIxTxBufSize; // 重置CNT计数 SPIx_TX_DMACHANNEL-SRCADDR (uint32_t) SPIxTxBuf; // 重置源地址 DMA_Cmd(SPIx_TX_DMACHANNEL, ENABLE); // 发送完成后硬件自动清零该位再次发送需要重新置1使能DMA } void LedSpiProcess(void) { for (U16 i 0; i LED_SPI_NUM_MAX; i) { LedSpi24byte(LED_BOND[i].Red, LED_BOND[i].Green, LED_BOND[i].Blue, SPIxTxBuf, i); } LedSpiSendBuf(LED_SPI_NUM_MAX); }首先来看void LedSpi24byte(unsigned char r, unsigned char g, unsigned char b, unsigned char * buf, int idx)它是将整个灯带运行一次效果所需要传输的数据写入SPIxTxBuf内我们根据其数据格式来看每种颜色用8bit组成我们需要传多少的24bit由idx决定再加上重置时间。然后将数据写进SPIxTxBuf一次需要传输如下这么多的bit。buf buf idx * 24 LED_SPI_RESET_TIME; //LED_SPI_RESET_TIME:100unsigned int dat (g 16) | (r 8) | b;假设R 255 G 255 B 255 整个过程就是G:1111 1111 变成 1111 1111 0000 0000 0000 0000R:1111 1111 变成 1111 1111 0000 0000B:1111 1111变成 1111 11111或起来就是 1111 1111 1111 1111 1111 1111下一步for (unsigned char i 0; i 24; i) {if (dat BIT(23 - i)) {* (buf i) 0x3E; //0011 1110}else {* (buf i) 0x0C; //0000 1100}}对0-23bit分别进行判断那么SPIxTxBuf数组就会有0-2324个0X7C为什么会是0x3E和0x0C?0x3E0011 1110 由于SPI是LSB那么低位在前0111 11000x0C: 0000 1100 由于SPI是LSB那么低位在前0011 00001码:那么在其第一个上升沿开始采集也就是0 111 1100高电平时间5* 0.166us 0.83us 低电平时间2 * 0.16us 0.33us0码那么在其第一个上升沿开始采集也就是00 11 0000高电平时间2* 0.166us 0.33us 低电平时间5 * 0.16us 0.83us所以是比较符合时序要求的所以这样就将255的数值映射到G7-G0或者R7-R0B7-B0来控制他的一个亮度