概述:
万利LPC54110开发板板载LPC54114高性能双核MCU,板载数字麦克风芯片,TF卡以及丰富的外设接口。LPC54114内部带有数字麦克风子系统DMIC,能够直接实现PDM、I2S等数字音频方面的应用。本项目基于万利LPC54110开发板,通过DMIC子系统实现音频信号16ksps的拾取,并利用该信号进行FFT变换,将8kHz内频谱信息显示在8×8的WS2812面板上,实现声音氛围灯。限于时间和作者水平,该项目仅仅呈现出了该芯片的一小部分功能。
系统原理:
代码介绍: WS2812驱动
WS2812不同于其它显示驱动芯片,其仅采用一个管脚,通过高低电平持续时间长短来区分一个code(0或1)。Code0时,高电平≈400ns,低电平≈800ns;Code1时,高电平≈800ns,低电平≈400ns;RESET时,低电平大于50μs。
结合其数据特征,可以直接使用SPI中MOSI管脚,无数据时,MOSI一直输出低电平,等效于WS2812的RESET信号,需要指出的是,该RESET信号并不会清除显示,只是让各个WS2812重新进入数据采集状态。而在发送数据时,则一次性发送全部显示数据。由于SPI在时钟输出前MOSI会提前两个时钟周期上线,因此,SPI发送数据的第一个字节为0x00,显示数据从第二个字节开始填充。
考虑使用3个bit数据来表示一个code,WS2812为GRB24bit颜色模式,因此,一个WS2812的颜色需要24codes = 72bits=9bytes的数据。利用共用体数据结构,按照大端在前规则,设置如下数据结构。
union WS2812_pixelData //Stand for one pixel data for WS2812
{
unsigned int box; //4 byte
struct
{
unsigned int pixelBit0:3;
unsigned int pixelBit1:3;
unsigned int pixelBit2:3;
unsigned int pixelBit3:3;
unsigned int pixelBit4:3;
unsigned int pixelBit5:3;
unsigned int pixelBit6:3;
unsigned int pixelBit7:3;
}pixelByteX; //Share space of box, 4 byte, but 24bit used
unsigned char byteX[3]; //used for GRB
}WSpData[3];
根据数据结构开始进行底层显存masterTxData填充,发送的第一个字节为全0数据,后续字节按照应用层显存WS_masterTxDataX结合位段操作填充WS2812_CODE1(0x06,0b‘110)或者WS2812_CODE0(0x04,0b‘100)。
masterTxData[0] = 0x00;
for(i = 0;i < (TRANSFER_SIZE / 9);i++)
{
//Green pixel byte
G_pixel = ((*(WS_masterTxDataX + i * 3 + 2)) > WS_brightness) ? (WS_brightness) : (*(WS_masterTxDataX + i * 3 + 2));
WSpData[2].pixelByteX.pixelBit7 = ((0x01 << 7) & (G_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[2].pixelByteX.pixelBit6 = ((0x01 << 6) & (G_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[2].pixelByteX.pixelBit5 = ((0x01 << 5) & (G_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[2].pixelByteX.pixelBit4 = ((0x01 << 4) & (G_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[2].pixelByteX.pixelBit3 = ((0x01 << 3) & (G_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[2].pixelByteX.pixelBit2 = ((0x01 << 2) & (G_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[2].pixelByteX.pixelBit1 = ((0x01 << 1) & (G_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[2].pixelByteX.pixelBit0 = ((0x01 << 0) & (G_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
//Red pixel byte
R_pixel = ((*(WS_masterTxDataX + i * 3 + 1)) > WS_brightness) ? (WS_brightness) : (*(WS_masterTxDataX + i * 3 + 1));
WSpData[1].pixelByteX.pixelBit7 = ((0x01 << 7) & (R_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[1].pixelByteX.pixelBit6 = ((0x01 << 6) & (R_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[1].pixelByteX.pixelBit5 = ((0x01 << 5) & (R_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[1].pixelByteX.pixelBit4 = ((0x01 << 4) & (R_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[1].pixelByteX.pixelBit3 = ((0x01 << 3) & (R_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[1].pixelByteX.pixelBit2 = ((0x01 << 2) & (R_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[1].pixelByteX.pixelBit1 = ((0x01 << 1) & (R_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[1].pixelByteX.pixelBit0 = ((0x01 << 0) & (R_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
//Blue pixel byte
B_pixel = ((*(WS_masterTxDataX + i * 3 + 0)) > WS_brightness) ? (WS_brightness) : (*(WS_masterTxDataX + i * 3 + 0));
WSpData[0].pixelByteX.pixelBit7 = ((0x01 << 7) & (B_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[0].pixelByteX.pixelBit6 = ((0x01 << 6) & (B_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[0].pixelByteX.pixelBit5 = ((0x01 << 5) & (B_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[0].pixelByteX.pixelBit4 = ((0x01 << 4) & (B_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[0].pixelByteX.pixelBit3 = ((0x01 << 3) & (B_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[0].pixelByteX.pixelBit2 = ((0x01 << 2) & (B_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[0].pixelByteX.pixelBit1 = ((0x01 << 1) & (B_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
WSpData[0].pixelByteX.pixelBit0 = ((0x01 << 0) & (B_pixel)) ? (WS2812_CODE1) : (WS2812_CODE0);
masterTxData[9 * i + 1] = WSpData[2].byteX[2];
masterTxData[9 * i + 2] = WSpData[2].byteX[1];
masterTxData[9 * i + 3] = WSpData[2].byteX[0];
masterTxData[9 * i + 4] = WSpData[1].byteX[2];
masterTxData[9 * i + 5] = WSpData[1].byteX[1];
masterTxData[9 * i + 6] = WSpData[1].byteX[0];
masterTxData[9 * i + 7] = WSpData[0].byteX[2];
masterTxData[9 * i + 8] = WSpData[0].byteX[1];
masterTxData[9 * i + 9] = WSpData[0].byteX[0];
}
/* Set up handle for spi master */
SPI_MasterTransferCreateHandleDMA(WS2812_SPI_MASTER, &masterHandle, SPI_MasterUserCallback, NULL, &masterTxHandle,
&masterRxHandle);
/* Start master transfer */
masterXfer.txData = (uint8_t *)&masterTxData;
masterXfer.rxData = (uint8_t *)&masterRxData;
masterXfer.dataSize = TRANSFER_SIZE * sizeof(masterTxData[0]);
masterXfer.configFlags = kSPI_FrameAssert;
if (kStatus_Success != SPI_MasterTransferDMA(WS2812_SPI_MASTER, &masterHandle, &masterXfer))
{
PRINTF("There is an error when start SPI_MasterTransferDMA \r\n ");
}
/* Wait until transfer completed */
while (!isTransferCompleted)
{
}
SysTick_DelayTicks(4); //ms
DMIC子系统
DMIC子系统时LPC54114的一个特色功能,能够实现PDM、PCM、I2S数据接口之间的数据直通,大幅降低了数据驱动开发的工作量。其初始化代码以及传输启动代码如下所示。
void DMICx_Config(void)
{
dmic_channel_config_t dmic_channel_cfg;
//////////DMIC Init//////////
dmic_channel_cfg.divhfclk = kDMIC_PdmDiv1;
dmic_channel_cfg.osr = 25U;
dmic_channel_cfg.gainshft = 3U;
dmic_channel_cfg.preac2coef = kDMIC_CompValueZero;
dmic_channel_cfg.preac4coef = kDMIC_CompValueZero;
dmic_channel_cfg.dc_cut_level = kDMIC_DcCut155;
dmic_channel_cfg.post_dc_gain_reduce = 1;
dmic_channel_cfg.saturate16bit = 1U;
dmic_channel_cfg.sample_rate = kDMIC_PhyFullSpeed;
#if defined(FSL_FEATURE_DMIC_CHANNEL_HAS_SIGNEXTEND) && (FSL_FEATURE_DMIC_CHANNEL_HAS_SIGNEXTEND)
dmic_channel_cfg.enableSignExtend = true;
#endif
DMIC_Init(DMIC0);
#if !(defined(FSL_FEATURE_DMIC_HAS_NO_IOCFG) && FSL_FEATURE_DMIC_HAS_NO_IOCFG)
DMIC_SetIOCFG(DMIC0, kDMIC_PdmDual);
#endif
DMIC_Use2fs(DMIC0, true);
DMIC_SetOperationMode(DMIC0, kDMIC_OperationModeDma);
DMIC_ConfigChannel(DMIC0, kDMIC_Channel0, kDMIC_Left, &dmic_channel_cfg);
DMIC_FifoChannel(DMIC0, kDMIC_Channel0, FIFO_DEPTH, true, true);
DMIC_EnableChannelDma(DMIC0, APP_DMIC_CHANNEL, true);
DMIC_ConfigChannel(DMIC0, APP_DMIC_CHANNEL, kDMIC_Left, &dmic_channel_cfg);
DMIC_FifoChannel(DMIC0, APP_DMIC_CHANNEL, FIFO_DEPTH, true, true);
DMIC_EnableChannnel(DMIC0, APP_DMIC_CHANNEL_ENABLE);
PRINTF("Configure DMA\r\n");
// DMA_Init(DMA0);
DMA_EnableChannel(DMA0, APP_DMAREQ_CHANNEL);
DMA_SetChannelPriority(DMA0, DMAREQ_DMIC0, kDMA_ChannelPriority1);
/* Request dma channels from DMA manager. */
DMA_CreateHandle(&g_dmicRxDmaHandle, DMA0, APP_DMAREQ_CHANNEL);
/* Create DMIC DMA handle. */
DMIC_TransferCreateHandleDMA(DMIC0, &g_dmicDmaHandle, DMICx_UserCallback, NULL, &g_dmicRxDmaHandle);
}
void DMICx_start(void)
{
receiveXfer.dataSize = 2 * BUFFER_LENGTH;
receiveXfer.data = (uint16_t *)g_rxBuffer;//DmicState.Buffer;//(uint16_t *)testbuffer;//
DMIC_TransferReceiveDMA(DMIC0, &g_dmicDmaHandle, &receiveXfer, kDMIC_Channel0);
DMIC_EnableChannnel(DMIC0, DMIC_CHANEN_EN_CH0(1));
}
需要注意的是,由于DMIC和SPI均使用了DMA数据传输功能,因此,在初始化和操作时需要注意不要对DMA0重复初始化,另外,SPI传输完成后不能调用DMA_Deinit(WS2812_DMA),这样会关闭所有的DMA传输导致程序卡死。
管脚配置与时钟配置
该项目利用MCUXpresso IDE进行开发,该工具类似与STMCubeIDE,能够进行以GUI形式配置GPIO、时钟、外设等。配置时钟如下图:新建BOARD_BootClockPLL150M功能组,并配置相应的时钟。
快速傅里叶变换(FFT)
该部分代码直接搬运Arduino中的FFT例程,为了实现比较短的采样间隔,使用800kHz时钟频率,并降低采样点数为128点,虽然,频率精度变低,但是一个完整显示和处理周期时间相比于256点由45ms降低至26ms,能够较快响应外部声音变化。
在FFT部分,对计算得到的频域功率谱进行分段求和、归一,得到WS2812显示面板每一列像素数量,之后在主程序中调用WS2812_ShowLineDMA(Red,Blue4);函数进行显示。
小结:
氛围灯一直时我想做的项目之一,然而,因为各种各样的借口一拖再拖。这次,借着硬禾学堂给的机会,一鼓作气,实现了声音氛围灯的雏形,为后续继续开展相关创作奠定基础,树立信心。本次项目的感受:
- MCU领域日新月异,有各种各样强大功能的MCU陆续问世。身处此洪流之中,虽有官方例程加持,但核心还是ARM、RISC-V等内核原理和典型外设(DMA等)的工作原理、调试方法。
- 学会如何使用别人的轮子。别人的“轮子”,特别是开源平台上的,代码规范,水平高,学习他们的代码要强于自己一点一点写,做出来的功能也要更丰富。
- 为了扩展产品的应用,各个厂商制作了丰富的教程、例程、IDE等,加快了产品投放市场的速度。那么,嵌入式MCU的共性技术时什么呢?