一、所选任务介绍
本项目为任务7-自命题任务,以NS800RT5039芯片为主控核心设计了一款圆屏开发板——脉冲星开发板,主要使用场景目标为智能手表。
二、项目描述
本项目设计了一款以NS800RT5039芯片为主控核心的开发板——脉冲星开发板,板载接口和外设包括:1.28寸15pin圆形彩屏、SDIO SD卡、音频功放IC NS4150B、MIC、8MB PSRAM APS6404L、USB转TTL CH340、电池充电IC ME4054。主要功能包括:通过MIC采集环境音频信号并展示频谱、从SD卡读取音频并播放、查看SD卡中的图片、小说阅读、切换背景、调整屏幕背光亮度等。
三、主要芯片选型
3.1 主控芯片NS800RT5039
微控制器NS800RT5039为纳芯微电子研发的中端算力实时控制MCU,采用运行在200~260MHz的Cortex M7内核,片上 FLASH(带ECC) 512KB, 片上SRAM 388KB。且片内自带一个自研的eMath浮点数学运算加速核,可以方便且快速的进行各种浮点三角函数运算及FFT、卷积、矩阵运算,本项目就使用eMath实现了IIR滤波器和512点的FFT。
3.2 音频功放芯片NS4150B
NS4150B 是一款超低 EMI、无需滤波器 3W 单声道 D 类音频功率放大器。NS4150B 采用先进的技术,在全带宽范围内极大地降低了 EMI 干扰,最大限度地减少对其他部件的影响。NS4150B 内置过流保护、过热保护及欠压保护功能,有效地保护芯片在异常工作状况下不被损坏。并且利用扩频技术充分优化全新电路设计,高达90%的效率更加适合于便携式音频产品。NS4150B 无需滤波器的 PWM 调制结构及增益内置方式减少了外部元件、PCB 面积和系统成本。
3.3 外扩RAM芯片APS6404L
外扩RAM芯片APS6404L-3SQR-SN是一款64Mb的QSPI PSRAM芯片。该芯片以SOP8封装形式呈现,适用于广泛的工业温度范围。其供电电压为3.3V,兼容SPI/QPI及SDR通信模式,时钟速率最高为144MHz。其内存架构设置为8M x 8bits,寻址范围涵盖A[22:0],页面配置为1024字节。该芯片具备自我管理刷新功能,无需系统主机干预即可完成DRAM刷新。
3.4 电池充电芯片ME4054BN
ME4054B-N 是一款完整的单节锂离子电池用恒定电流/恒定电压线性充电芯片。其中 ThinSOT 封装与较少的外部元器件数目使得 ME4054B-N 成为便携式应用的理想选择。而且 ME4054B-N 是专为在 USB 电源规范内工作而设计的。由于采用内部 MOSFET 构架,所以不需要外部检测电阻器和隔离二极管。热反馈可对充电电流进行调节以便在大功率操作或高环境温度条件下对芯片温度加以限制。充电电压固定为 4.2V,而充电电流可通过一个电阻器进行外部设置。当充电电流在达到最终浮充电压之后降至设定值的 1/10 时,ME4054B-N 将自动终止充电循环。当输入电压(交流适配器或 USB 电源)被拿掉时,ME4054B-N 自动进入一个低电流状态,将电池漏电流降至 2μA 以下,可将 ME4054B-N 置于停机模式,从而将供电电流 降至 55μA。ME4054B-N 的其他特点包括充电电流监控器、欠压闭锁、自动再充电和一个用于指示充电结束和输入电压接入的状态引脚。
四、设计思路
4.1 方案框图

方案框图如上图所示,系统方案以微控制器NS800RT5039为主控核心,通过SPI和IIC驱动LCD彩屏和触摸屏,通过SDIO驱动SD卡,通过ADC采集音频信号和电池电压信息,通过EQEP接口和GPIO驱动带按键的编码器作为用户控制输入,通过QSPI外扩一个8MB的PSRAM,通过DAC+音频功放播放音乐,使用UART+CH340实现串口日志打印。
4.2 主要设计思路
4.2.1 用户界面显示与交互
用户界面显示与交互采用LVGL实现,LVGL(Light and Versatile Graphics Library)是一个轻量级、多功能、灵活且可移植的开源图形用户界面(GUI)库,适用于各种嵌入式平台,如智能手机、智能手表和汽车仪表盘等。实际输入设备可以用触摸屏或者带按键的编码器注册,按键用于在各个界面之间切换,编码器用在同一个界面内切换不同组件的焦点。
4.2.2 实时音频频谱展示
首先采用ADC采集原始音频,由于ADC的局限性,原始的音频数据会有一个比较大的直流分量,直接FFT的话会导致频谱上的0频谱非常高而其他的都很小。所以需要采集到的音频数据先过一个高通滤波器去除直流分量,这里我采用MATLAB设计了一个截止频率1500Hz的高通滤波器得到滤波器参数,再使用芯片内的eMath实现这个IIR滤波器,当采集点数足够时再使用eMath模块进行FFT获得频谱,最后将频谱输送到lvgl进行展示。
4.2.3 音乐播放器
首先从SD卡中读取音频文件,提取音频数据然后写入到外扩的PSRAM中,播放音乐时就可以直接读取PSRAM中的数据,再使用定时器中断将音频数据输送到DAC中进行音乐播放
五、原理图与PCB设计
5.1 原理图
本项目原理图设计如下图所示,需要注意的是PSRAM型号得用APS6404L-3SQR这个带3的,不带3的为1.8V逻辑电平,带3的为3.3V逻辑电平。

5.2 PCB
本项目PCB设计如下图所示

六、软件调试与关键代码
6.1 软件流程图
软件流程图如下图所示,主要是在初始化UI之前得先将SD卡中的素材写入到PSRAM中,初始化UI的时候会用到,写入PSRAM的速度很快,时间主要消耗在SD卡素材的读取上。

6.2 软件调试与关键代码说明
6.2.1 PSRAM与SD卡
本项目的SD卡调试的时间最久,一是应为官方SDK中没有SDIO的例子,只能自己按照寄存机手册研究,而是按照文档研究了好多天用示波器挨个引脚检测波形才发现SDIO的CLK引脚没有任何输出,即便初始化为普通GPIO也没有输出,最后只能用旁边的IO短接过来,再使用IO模拟SPI来驱动SD卡,好在也是成功驱动了,前后花费了一周左右,除了速度稍慢一些其他都没什么问题。本项目的外扩PSRAM调试可以参考官方SDK中的QPIFLASH例子以及芯片文档,前后花了3天左右成功驱动。以下是调试过程中的两张记录图片,一是刚焊好主控芯片和USB串口用来测试芯片是否焊好,二是调试SD卡时一直有问题就飞了4根线来用示波器或者逻辑分析仪来抓波形。


PSRAM的主要驱动代码如下
//PSRAM写入
void psram_ap6404_write(uint32_t addr_start, uint8_t* pdata, uint32_t len)
{
QSPI_selectSpiProtocol(QSPI, QUAD_SPI_PROTOCOL);
QSPI_setDirectCommMode(QSPI);
QSPI_writeQuadModeD0Byte(0x38);
QSPI_setCommPort(QSPI, (addr_start>>16)&0xFF);
QSPI_setCommPort(QSPI, (addr_start>>8)&0xFF);
QSPI_setCommPort(QSPI, (addr_start)&0xFF);
for (uint32_t i = 0; i < len; i++)
{
QSPI->SFMCOM.WORDVAL = pdata[i];
}
QSPI_setDirectCommMode(QSPI);
QSPI_selectSpiProtocol(QSPI, EXTENDED_SPI_PROTOCOL);
QSPI_setRomAccessMode(QSPI);
}
//PSRAM读取,用不上,直接使用地址映射读取即可
void psram_ap6404_read(uint32_t addr_start, uint8_t* pdata, uint32_t len)
{
uint8_t cmd_and_addr[7] = {0};
QSPI_selectSpiProtocol(QSPI, QUAD_SPI_PROTOCOL);
QSPI_setDirectCommMode(QSPI);
QSPI_writeQuadModeD0Byte(0xeb);
QSPI_setCommPort(QSPI, (addr_start>>16)&0xFF);
QSPI_setCommPort(QSPI, (addr_start>>8)&0xFF);
QSPI_setCommPort(QSPI, (addr_start)&0xFF);
QSPI_setCommPort(QSPI, 0x00);
QSPI_setCommPort(QSPI, 0xff);
QSPI_setCommPort(QSPI, 0xff);
/* Read data from QSPI. */
for (uint32_t i = 0; i < len; i++)
{
pdata[i] = QSPI_getCommPort(QSPI);
}
QSPI_setDirectCommMode(QSPI);
QSPI_selectSpiProtocol(QSPI, EXTENDED_SPI_PROTOCOL);
QSPI_setRomAccessMode(QSPI);
}
SD卡的主要驱动代码如下
static
BYTE send_cmd ( /* Returns command response (bit7==1:Send failed)*/
BYTE cmd, /* Command byte */
DWORD arg /* Argument */
)
{
BYTE n, d, buf[6];
if (cmd & 0x80) { /* ACMD<n> is the command sequense of CMD55-CMD<n> */
cmd &= 0x7F;
n = send_cmd(CMD55, 0);
if (n > 1) return n;
}
/* Select the card and wait for ready except to stop multiple block read */
if (cmd != CMD12) {
deselect();
if (!sd_select()) return 0xFF;
}
/* Send a command packet */
buf[0] = 0x40 | cmd; /* Start + Command index */
buf[1] = (BYTE)(arg >> 24); /* Argument[31..24] */
buf[2] = (BYTE)(arg >> 16); /* Argument[23..16] */
buf[3] = (BYTE)(arg >> 8); /* Argument[15..8] */
buf[4] = (BYTE)arg; /* Argument[7..0] */
n = 0x01; /* Dummy CRC + Stop */
if (cmd == CMD0) n = 0x95; /* (valid CRC for CMD0(0)) */
if (cmd == CMD8) n = 0x87; /* (valid CRC for CMD8(0x1AA)) */
buf[5] = n;
spi_write(buf, 6);
/* Receive command response */
if (cmd == CMD12) spi_read(&d, 1); /* Skip a stuff byte when stop reading */
n = 30; /* Wait for a valid response in timeout of 10 attempts */
do
{
spi_read(&d, 1);
}
while ((d & 0x80) && --n);
return d; /* Return with the response value */
}
/*-----------------------------------------------------------------------*/
/* Get Drive Status */
/*-----------------------------------------------------------------------*/
DSTATUS disk_status (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
if(pdrv == SD_CARD)
return Stat;
return STA_NOINIT;
}
/*-----------------------------------------------------------------------*/
/* Inidialize a Drive */
/*-----------------------------------------------------------------------*/
DSTATUS disk_initialize (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
BYTE n, ty, cmd, buf[4];
UINT tmr;
DSTATUS s;
if (pdrv != SD_CARD)
{
printf("sd not support\n");
return RES_NOTRDY;
}
flag_low_speed = 1;
dly_us(10000); /* 10ms */
SD_CS_SET();
for (n = 10; n; n--) spi_read(buf, 1); /* Apply 80 dummy clocks and the card gets ready to receive command */
ty = 0;
int x = send_cmd(CMD0, 0);
if (x == 1)
{ /* Enter Idle state */
if (send_cmd(CMD8, 0x1AA) == 1)
{ /* SDv2? */
spi_read(buf, 4); /* Get trailing return value of R7 resp */
if (buf[2] == 0x01 && buf[3] == 0xAA)
{ /* The card can work at vdd range of 2.7-3.6V */
for (tmr = 1000; tmr; tmr--)
{ /* Wait for leaving idle state (ACMD41 with HCS bit) */
if (send_cmd(ACMD41, 1UL << 30) == 0) break;
dly_us(1000);
}
if (tmr && send_cmd(CMD58, 0) == 0)
{ /* Check CCS bit in the OCR */
spi_read(buf, 4);
ty = (buf[0] & 0x40) ? CT_SDC2 | CT_BLOCK : CT_SDC2; /* SDv2+ */
}
}
}
else
{ /* SDv1 or MMCv3 */
if (send_cmd(ACMD41, 0) <= 1)
{
ty = CT_SDC2; cmd = ACMD41; /* SDv1 */
}
else
{
ty = CT_MMC3; cmd = CMD1; /* MMCv3 */
}
for (tmr = 1000; tmr; tmr--)
{ /* Wait for leaving idle state */
if (send_cmd(cmd, 0) == 0) break;
dly_us(1000);
}
if (!tmr || send_cmd(CMD16, 512) != 0) /* Set R/W block length to 512 */
ty = 0;
}
}
flag_low_speed = 0;
CardType = ty;
s = ty ? 0 : STA_NOINIT;
Stat = s;
printf("CardType = %d, x=%d\n",CardType,x);
SD_CS_SET();
return s;
}
/*-----------------------------------------------------------------------*/
/* Read Sector(s) */
/*-----------------------------------------------------------------------*/
DRESULT disk_read (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
BYTE *buff, /* Data buffer to store read data */
LBA_t sector, /* Start sector in LBA */
UINT count /* Number of sectors to read */
)
{
BYTE cmd;
DWORD sect = (DWORD)sector;
if (disk_status(pdrv) & STA_NOINIT) return RES_NOTRDY;
if (!(CardType & CT_BLOCK)) sect *= 512; /* Convert LBA to byte address if needed */
cmd = count > 1 ? CMD18 : CMD17; /* READ_MULTIPLE_BLOCK : READ_SINGLE_BLOCK */
if (send_cmd(cmd, sect) == 0) {
do {
if (!rcvr_datablock(buff, 512)) break;
buff += 512;
} while (--count);
if (cmd == CMD18) send_cmd(CMD12, 0); /* STOP_TRANSMISSION */
}
SD_CS_SET();
return count ? RES_ERROR : RES_OK;
}
/*-----------------------------------------------------------------------*/
/* Write Sector(s) */
/*-----------------------------------------------------------------------*/
DRESULT disk_write (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
const BYTE *buff, /* Data to be written */
LBA_t sector, /* Start sector in LBA */
UINT count /* Number of sectors to write */
)
{
DWORD sect = (DWORD)sector;
if (disk_status(pdrv) & STA_NOINIT) return RES_NOTRDY;
if (!(CardType & CT_BLOCK)) sect *= 512; /* Convert LBA to byte address if needed */
if (count == 1) { /* Single block write */
if ((send_cmd(CMD24, sect) == 0) /* WRITE_BLOCK */
&& xmit_datablock(buff, 0xFE))
count = 0;
}
else { /* Multiple block write */
if (CardType & CT_SDC) send_cmd(ACMD23, count);
if (send_cmd(CMD25, sect) == 0) { /* WRITE_MULTIPLE_BLOCK */
do {
if (!xmit_datablock(buff, 0xFC)) break;
buff += 512;
} while (--count);
if (!xmit_datablock(0, 0xFD)) /* STOP_TRAN token */
count = 1;
}
}
SD_CS_SET();
return count ? RES_ERROR : RES_OK;
}
6.2.2 LCD驱动
本项目的LCD驱动芯片为gc9a01,SPI接口,我在调试时也遇到一个奇怪的问题,SPI数据一直发送不出去导致LCD初始化一直卡住,最终是使用模拟SPI才驱动成功。LCD驱动的主要代码如下所示
/******************************************************************************
函数说明:LCD串行数据写入函数
入口数据:dat 要写入的串行数据
返回值: 无
******************************************************************************/
void LCD_Writ_Bus(uint8_t dat)
{
uint8_t i;
LCD_CS_Clr();
for(i=0;i<8;i++)
{
LCD_SCLK_Clr();
if(dat&0x80)
{
LCD_MOSI_Set();
}
else
{
LCD_MOSI_Clr();
}
__NOP();
LCD_SCLK_Set();
dat<<=1;
}
LCD_CS_Set();
}
/******************************************************************************
函数说明:LCD写入数据
入口数据:dat 写入的数据
返回值: 无
******************************************************************************/
void LCD_WR_DATA8(uint8_t dat)
{
LCD_Writ_Bus(dat);
}
/******************************************************************************
函数说明:LCD写入数据
入口数据:dat 写入的数据
返回值: 无
******************************************************************************/
void LCD_WR_DATA(uint16_t dat)
{
LCD_Writ_Bus(dat>>8);
LCD_Writ_Bus(dat);
}
/******************************************************************************
函数说明:LCD写入命令
入口数据:dat 写入的命令
返回值: 无
******************************************************************************/
void LCD_WR_REG(uint8_t dat)
{
LCD_DC_CLEAR();//写命令
LCD_Writ_Bus(dat);
LCD_DC_SET();//写数据
}
/******************************************************************************
函数说明:设置起始和结束地址
入口数据:x1,x2 设置列的起始和结束地址
y1,y2 设置行的起始和结束地址
返回值: 无
******************************************************************************/
void LCD_Address_Set(uint16_t x1,uint16_t y1,uint16_t x2,uint16_t y2)
{
LCD_WR_REG(0x2a);//列地址设置
LCD_WR_DATA(x1);
LCD_WR_DATA(x2);
LCD_WR_REG(0x2b);//行地址设置
LCD_WR_DATA(y1);
LCD_WR_DATA(y2);
LCD_WR_REG(0x2c);//储存器写
}
void LCD_Init(void)
{
LCD_BLK_SET();//打开背光
Delay_ms(100);
LCD_WR_REG(0xEF);
LCD_WR_REG(0xEB);
LCD_WR_DATA8(0x14);
LCD_WR_REG(0xFE);
// printf("SPI_getTxFifoEmptyUnitStatus = %d\n",SPI_getTxFifoEmptyUnitStatus(SPI1));
LCD_WR_REG(0xEF);
// printf("SPI_getTxFifoEmptyUnitStatus = %d\n",SPI_getTxFifoEmptyUnitStatus(SPI1));
LCD_WR_REG(0xEB);
// printf("SPI_getTxFifoEmptyUnitStatus = %d\n",SPI_getTxFifoEmptyUnitStatus(SPI1));
LCD_WR_DATA8(0x14);
LCD_WR_REG(0x84);
LCD_WR_DATA8(0x40);
LCD_WR_REG(0x85);
LCD_WR_DATA8(0xFF);
LCD_WR_REG(0x86);
LCD_WR_DATA8(0xFF);
LCD_WR_REG(0x87);
LCD_WR_DATA8(0xFF);
LCD_WR_REG(0x88);
LCD_WR_DATA8(0x0A);
LCD_WR_REG(0x89);
LCD_WR_DATA8(0x21);
LCD_WR_REG(0x8A);
LCD_WR_DATA8(0x00);
LCD_WR_REG(0x8B);
LCD_WR_DATA8(0x80);
LCD_WR_REG(0x8C);
LCD_WR_DATA8(0x01);
LCD_WR_REG(0x8D);
LCD_WR_DATA8(0x01);
LCD_WR_REG(0x8E);
LCD_WR_DATA8(0xFF);
LCD_WR_REG(0x8F);
LCD_WR_DATA8(0xFF);
LCD_WR_REG(0xB6);
LCD_WR_DATA8(0x00);
LCD_WR_DATA8(0x20);
LCD_WR_REG(0x36);
if(USE_HORIZONTAL==0)LCD_WR_DATA8(0x08);
else if(USE_HORIZONTAL==1)LCD_WR_DATA8(0xC8);
else if(USE_HORIZONTAL==2)LCD_WR_DATA8(0x68);
else LCD_WR_DATA8(0xA8);
LCD_WR_REG(0x3A);
LCD_WR_DATA8(0x05);
LCD_WR_REG(0x90);
LCD_WR_DATA8(0x08);
LCD_WR_DATA8(0x08);
LCD_WR_DATA8(0x08);
LCD_WR_DATA8(0x08);
LCD_WR_REG(0xBD);
LCD_WR_DATA8(0x06);
LCD_WR_REG(0xBC);
LCD_WR_DATA8(0x00);
LCD_WR_REG(0xFF);
LCD_WR_DATA8(0x60);
LCD_WR_DATA8(0x01);
LCD_WR_DATA8(0x04);
LCD_WR_REG(0xC3);
LCD_WR_DATA8(0x13);
LCD_WR_REG(0xC4);
LCD_WR_DATA8(0x13);
LCD_WR_REG(0xC9);
LCD_WR_DATA8(0x22);
LCD_WR_REG(0xBE);
LCD_WR_DATA8(0x11);
LCD_WR_REG(0xE1);
LCD_WR_DATA8(0x10);
LCD_WR_DATA8(0x0E);
LCD_WR_REG(0xDF);
LCD_WR_DATA8(0x21);
LCD_WR_DATA8(0x0c);
LCD_WR_DATA8(0x02);
LCD_WR_REG(0xF0);
LCD_WR_DATA8(0x45);
LCD_WR_DATA8(0x09);
LCD_WR_DATA8(0x08);
LCD_WR_DATA8(0x08);
LCD_WR_DATA8(0x26);
LCD_WR_DATA8(0x2A);
LCD_WR_REG(0xF1);
LCD_WR_DATA8(0x43);
LCD_WR_DATA8(0x70);
LCD_WR_DATA8(0x72);
LCD_WR_DATA8(0x36);
LCD_WR_DATA8(0x37);
LCD_WR_DATA8(0x6F);
LCD_WR_REG(0xF2);
LCD_WR_DATA8(0x45);
LCD_WR_DATA8(0x09);
LCD_WR_DATA8(0x08);
LCD_WR_DATA8(0x08);
LCD_WR_DATA8(0x26);
LCD_WR_DATA8(0x2A);
LCD_WR_REG(0xF3);
LCD_WR_DATA8(0x43);
LCD_WR_DATA8(0x70);
LCD_WR_DATA8(0x72);
LCD_WR_DATA8(0x36);
LCD_WR_DATA8(0x37);
LCD_WR_DATA8(0x6F);
LCD_WR_REG(0xED);
LCD_WR_DATA8(0x1B);
LCD_WR_DATA8(0x0B);
LCD_WR_REG(0xAE);
LCD_WR_DATA8(0x77);
LCD_WR_REG(0xCD);
LCD_WR_DATA8(0x63);
LCD_WR_REG(0x70);
LCD_WR_DATA8(0x07);
LCD_WR_DATA8(0x07);
LCD_WR_DATA8(0x04);
LCD_WR_DATA8(0x0E);
LCD_WR_DATA8(0x0F);
LCD_WR_DATA8(0x09);
LCD_WR_DATA8(0x07);
LCD_WR_DATA8(0x08);
LCD_WR_DATA8(0x03);
LCD_WR_REG(0xE8);
LCD_WR_DATA8(0x34);
LCD_WR_REG(0x62);
LCD_WR_DATA8(0x18);
LCD_WR_DATA8(0x0D);
LCD_WR_DATA8(0x71);
LCD_WR_DATA8(0xED);
LCD_WR_DATA8(0x70);
LCD_WR_DATA8(0x70);
LCD_WR_DATA8(0x18);
LCD_WR_DATA8(0x0F);
LCD_WR_DATA8(0x71);
LCD_WR_DATA8(0xEF);
LCD_WR_DATA8(0x70);
LCD_WR_DATA8(0x70);
LCD_WR_REG(0x63);
LCD_WR_DATA8(0x18);
LCD_WR_DATA8(0x11);
LCD_WR_DATA8(0x71);
LCD_WR_DATA8(0xF1);
LCD_WR_DATA8(0x70);
LCD_WR_DATA8(0x70);
LCD_WR_DATA8(0x18);
LCD_WR_DATA8(0x13);
LCD_WR_DATA8(0x71);
LCD_WR_DATA8(0xF3);
LCD_WR_DATA8(0x70);
LCD_WR_DATA8(0x70);
LCD_WR_REG(0x64);
LCD_WR_DATA8(0x28);
LCD_WR_DATA8(0x29);
LCD_WR_DATA8(0xF1);
LCD_WR_DATA8(0x01);
LCD_WR_DATA8(0xF1);
LCD_WR_DATA8(0x00);
LCD_WR_DATA8(0x07);
LCD_WR_REG(0x66);
LCD_WR_DATA8(0x3C);
LCD_WR_DATA8(0x00);
LCD_WR_DATA8(0xCD);
LCD_WR_DATA8(0x67);
LCD_WR_DATA8(0x45);
LCD_WR_DATA8(0x45);
LCD_WR_DATA8(0x10);
LCD_WR_DATA8(0x00);
LCD_WR_DATA8(0x00);
LCD_WR_DATA8(0x00);
LCD_WR_REG(0x67);
LCD_WR_DATA8(0x00);
LCD_WR_DATA8(0x3C);
LCD_WR_DATA8(0x00);
LCD_WR_DATA8(0x00);
LCD_WR_DATA8(0x00);
LCD_WR_DATA8(0x01);
LCD_WR_DATA8(0x54);
LCD_WR_DATA8(0x10);
LCD_WR_DATA8(0x32);
LCD_WR_DATA8(0x98);
LCD_WR_REG(0x74);
LCD_WR_DATA8(0x10);
LCD_WR_DATA8(0x85);
LCD_WR_DATA8(0x80);
LCD_WR_DATA8(0x00);
LCD_WR_DATA8(0x00);
LCD_WR_DATA8(0x4E);
LCD_WR_DATA8(0x00);
LCD_WR_REG(0x98);
LCD_WR_DATA8(0x3e);
LCD_WR_DATA8(0x07);
LCD_WR_REG(0x35);
LCD_WR_REG(0x21);
LCD_WR_REG(0x11);
Delay_ms(120);
LCD_WR_REG(0x29);
Delay_ms(20);
}
6.2.3 实时环境音频频谱
想要展示实时音频频谱,首先得采集环境音频数据,这一步直接使用ADC采集MIC信号即可,只是由于ADC的局限性,采集到的音频数据会有一个很大的直流分量,如果直接使用ADC数据进行FFT计算会导致0频率的数据很大,这样显示在频谱上会显得其他频率都很小,所以需要设计一个高通数字滤波器将这个直流分量滤除。本项目是使用到MATLAB设计了一个截止频率1500Hz的高通二阶IIR滤波器,再配合主控芯片上的eMath模块实现这个滤波器。采集到足够的数据(512点)时进行FFT计算,主控芯片上的eMath也有FFT计算功能,但我在调试时发现使用eMath进行FFT计算只能计算一次,无法连续计算,最终是使用自己实现的FFT计算。以下是调试过程中的记录图片,一是MATLAB的滤波器设计界面,二是将ADC采集到的音频数据滤波后上传到匿名上位机进行展示和分析的界面。


音频频谱计算相关主要代码如下
//to匿名上位机
void adc_send(uint16_t adc)
{
uint8_t sumcheck = 0;
uint8_t addcheck = 0;
uint8_t tx[20]={0};
tx[0] = 0xaa;
tx[1] = 0xff;
tx[2] = 0xf1;
tx[3] = 2;
tx[4] = adc;
tx[5] = adc>>8;
for(uint8_t i=0;i<6;i++)
{
sumcheck += tx[i];
addcheck += sumcheck;
}
tx[6] = sumcheck;
tx[7] = addcheck;
for (uint16_t i = 0; i < 8; i++)
{
while (!UART_getStatusFlag(UART1, UART_TX_DATA_REG_EMPTY));
UART_writeChar(UART1, tx[i]);
}
}
/*
* 离散时间 IIR 滤波器(实数)
* ----------------
* 滤波器结构 : 直接 II 型,二阶节
* 节数 : 1
* 稳定 : 是
* 线性相位 : 否
*/
int16_t filter_iir(int16_t input)
{
static float32_t input_list[16]={0};
static uint8_t n = 0;
float32_t iir_result[16] = {0};
if(n<16)
{
input_list[n] = input;
n++;
return 0;
}
else
{
for(uint8_t i=0;i<15;i++)input_list[i] = input_list[i+1];
input_list[15] = input;
}
EMATH_BiquadState state = {
.compreg = 0,
.param =
{
.vn1 = 0.0f,
.vn = 0.0f,
.a1 = -0.747789178258f,
.a2 = 0.27221493792f,
.b0 = 1.f,
.b1 = -2.f,
.b2 = 1.f,
}
};
EMATH_restoreBiquadState(0, &state);
EMATH_df2BiquadIirF32(0, input_list, iir_result, 16);
return iir_result[15];
}
void mic_result_hander(void)
{
int16_t adc = 0;
/* Add the latest result to the buffer */
adc = ADC_readResult(ADCBRESULT, ADC_SOC_NUMBER0);
if(flag_full==0)
{
adc = filter_iir(adc);
adc_result[index] = adc;
index++;
// adc_send(adc);
}
// printf("adc indedx:%d,result:%d",index,adc_result[index]);
if(index>=512){
index = 0;
flag_full = 1;
}
/* Clear the interrupt flag */
ADC_clearInterruptStatus(ADCB, ADC_INT_NUMBER1);
/* Check if overflow has occurred */
if(true == ADC_getInterruptOverflowStatus(ADCB, ADC_INT_NUMBER1))
{
ADC_clearInterruptOverflowStatus(ADCB, ADC_INT_NUMBER1);
ADC_clearInterruptStatus(ADCB, ADC_INT_NUMBER1);
}
}
void mic_data_check(void)
{
if(flag_full==1)
{
//按照官方示例使用EMATH计算只有一次结果,可能需要清除某个寄存器才能连续计算
// EMATH_setFormat(EMATH_CP_FFT, EMATH_16Bit, 5, 0);
// EMATH_transformRFFT(adc_result, 512, fft_result);
// EMATH_waitDone();
// EMATH_setFormat(EMATH_CP_EMATH, EMATH_16Bit, 0, 0);
//自行实现fft
fft_perform(adc_result, fft_result, &fft_st);
// printf("FFT RESULT:[");
// for(uint16_t i=0;i<257;i++)printf("%d,",fft_result[i]);
// printf("]\n");
mic_data_update(fft_result);
flag_full = 0;
}
}
/*
* Initialisation routine - sets up tables and space to work in.
* Returns a pointer to internal state, to be used when performing calls.
* On error, returns NULL.
* The pointer should be freed when it is finished with, by fft_close().
*/
void visual_fft_init(fft_state *p_state)
{
unsigned int i;
for(i = 0; i < FFT_BUFFER_SIZE; i++)
{
p_state->bitReverse[i] = reverseBits(i);
}
for(i = 0; i < FFT_BUFFER_SIZE / 2; i++)
{
float j = 2 * PI * i / FFT_BUFFER_SIZE;
p_state->costable[i] = cos(j);
p_state->sintable[i] = sin(j);
}
}
/*
* Do all the steps of the FFT, taking as input sound data (as described in
* sound.h) and returning the intensities of each frequency as floats in the
* range 0 to ((FFT_BUFFER_SIZE / 2) * 32768) ^ 2
*
* The input array is assumed to have FFT_BUFFER_SIZE elements,
* and the output array is assumed to have (FFT_BUFFER_SIZE / 2 + 1) elements.
* state is a (non-NULL) pointer returned by visual_fft_init.
*/
void fft_perform(const sound_sample *input, uint16_t *output, fft_state *state)
{
float output_f[512] = {0};
/* Convert data from sound format to be ready for FFT */
fft_prepare(input, state->real, state->imag, state->bitReverse );
/* Do the actual FFT */
fft_calculate(state->real, state->imag, state->costable, state->sintable);
/* Convert the FFT output into intensities */
fft_output(state->real, state->imag, output_f);
for(uint16_t i=0;i<257;i++)output[i] = output_f[i]/4;
}
/*****************************************************************************
* These functions are called from the other ones
*****************************************************************************/
/*
* Prepare data to perform an FFT on
*/
static void fft_prepare( const sound_sample *input, float * re, float * im, const unsigned int *bitReverse )
{
unsigned int i;
float *p_real = re;
float *p_imag = im;
/* Get input, in reverse bit order */
for(i = 0; i < FFT_BUFFER_SIZE; i++)
{
*p_real++ = input[bitReverse[i]];
*p_imag++ = 0;
}
}
/*
* Take result of an FFT and calculate the intensities of each frequency
* Note: only produces half as many data points as the input had.
*/
static void fft_output(const float * re, const float * im, float *output)
{
float *p_output = output;
const float *p_real = re;
const float *p_imag = im;
float *p_end = output + FFT_BUFFER_SIZE / 2;
while(p_output <= p_end)
{
*p_output = sqrtf((*p_real * *p_real) + (*p_imag * *p_imag));
p_output++; p_real++; p_imag++;
}
/* Do divisions to keep the constant and highest frequency terms in scale
* with the other terms. */
*output /= 4;
*p_end /= 4;
}
/*
* Actually perform the FFT
*/
static void fft_calculate(float * re, float * im, const float *costable, const float *sintable )
{
unsigned int i, j, k;
unsigned int exchanges;
float fact_real, fact_imag;
float tmp_real, tmp_imag;
unsigned int factfact;
/* Set up some variables to reduce calculation in the loops */
exchanges = 1;
factfact = FFT_BUFFER_SIZE / 2;
/* Loop through the divide and conquer steps */
for(i = FFT_BUFFER_SIZE_LOG; i != 0; i--) {
/* In this step, we have 2 ^ (i - 1) exchange groups, each with
* 2 ^ (FFT_BUFFER_SIZE_LOG - i) exchanges
*/
/* Loop through the exchanges in a group */
for(j = 0; j != exchanges; j++) {
/* Work out factor for this exchange
* factor ^ (exchanges) = -1
* So, real = cos(j * PI / exchanges),
* imag = sin(j * PI / exchanges)
*/
fact_real = costable[j * factfact];
fact_imag = sintable[j * factfact];
/* Loop through all the exchange groups */
for(k = j; k < FFT_BUFFER_SIZE; k += exchanges << 1) {
int k1 = k + exchanges;
tmp_real = fact_real * re[k1] - fact_imag * im[k1];
tmp_imag = fact_real * im[k1] + fact_imag * re[k1];
re[k1] = re[k] - tmp_real;
im[k1] = im[k] - tmp_imag;
re[k] += tmp_real;
im[k] += tmp_imag;
}
}
exchanges <<= 1;
factfact >>= 1;
}
}
static int reverseBits(unsigned int initial)
{
unsigned int reversed = 0, loop;
for(loop = 0; loop < FFT_BUFFER_SIZE_LOG; loop++) {
reversed <<= 1;
reversed += (initial & 1);
initial >>= 1;
}
return reversed;
}
6.2.4 音乐播放
本项目的音乐播放分两步,开始播放时首先从SD卡中读取处理好的音频数据,并写入到PSRAM中,再开启定时器,以相应频率相PSRAM中的音频数据输送到DAC输出,再经过音频功放芯片进行放大。虽然调试的时候喇叭并没有出声,但使用示波器看了下DAC输出波形,是正常音频波形,可能是硬件上有点问题。以下是使用示波器查看DAC输出的波形

音频播放相关主要代码如下
//音频文件信息,这里先使用固定信息,正常应使用readdir读取音频文件
static char* music_file_info[] =
{
"/music/yinyue_1.wav",
"/music/yinyue_2.wav",
"/music/yinyue_3.wav",
"/music/yinyue_4.wav",
"/music/yinyue_5.wav",
"/music/yinyue_6.wav",
"/music/jiewang_1.wav",
"/music/jiewang_2.wav",
"/music/jiewang_3.wav",
"/music/jiewang_4.wav",
"/music/jiewang_5.wav",
"/music/jiewang_6.wav",
"/music/jiewang_7.wav",
"/music/jiewang_8.wav",
"/music/jiewang_9.wav",
"/music/jiewang_10.wav",
"/music/jiewang_11.wav",
"/music/wuting_1.wav",
"/music/wuting_2.wav",
"/music/wuting_3.wav",
"/music/wuting_4.wav",
"/music/wuting_5.wav",
"/music/wuting_6.wav",
"/music/wuting_7.wav",
"/music/wuting_8.wav",
"/music/wuting_9.wav"
};
char* get_music_name(void){return music_file_info[music_index];}
void read_wav_to_psram(char* wav_path)
{
FRESULT fr;
FIL fil;
uint8_t data[WRITE_LEN_ONCE] = {0};
uint16_t len = 0;
uint32_t read_len = 0;
uint32_t wrote_len = 0;
uint32_t file_size = 0;
fr = f_open(&fil, wav_path, FA_READ);
if(fr != FR_OK)
{
printf("open file read %s failed!! ret = %d !\n",wav_path, fr);
return;
}
wav_point_count = 0;
file_size = f_size(&fil);
printf("read file %s size = %d bytes\n",wav_path, file_size);
fr = f_lseek(&fil,44);
file_size -= 44;
while(wrote_len<file_size)
{
if(file_size - wrote_len > WRITE_LEN_ONCE)len = WRITE_LEN_ONCE;
else len = file_size - wrote_len;
wav_point_count += len/2;
fr = f_read(&fil,data,len,&read_len);
if(fr != FR_OK)
{
printf("read file failed!! ret = %d !\n",fr);
f_close(&fil);
return;
}
psram_ap6404_write(0x700000 + wrote_len, data, read_len);
wrote_len += read_len;
printf("wrote len:%d\n",wrote_len);
}
fr = f_close(&fil);
}
void music_play(void)
{
uint16_t dac_value = 0;
if (TIM_getInterruptSource(TIM1, TIM_IT_UPDATE) & TIM_getFlags(TIM1, TIM_FLAG_UPDATE))
{
TIM_clearFlags(TIM1, TIM_FLAG_UPDATE);
dac_value = *(uint8_t*)(0x80700000+wav_index*2) + *(uint8_t*)(0x80700001+wav_index*2) * 256;
wav_index++;
if(wav_index>=wav_point_count)
{
wav_index = 0;
TIM_disableCounter(TIM1);
}
DAC_setShadowValue(DAC1, dac_value);
// printf("dac value:%d",dac_value);
}
__DSB();
}
void music_play_start(void)
{
if(flag_music_load==0)
{
read_wav_to_psram(music_file_info[music_index]);
flag_music_load=1;
}
flag_music_playing = 1;
TIM_enableCounter(TIM1);
}
void music_play_pause(void)
{
flag_music_playing = 0;
TIM_disableCounter(TIM1);
}
void music_play_prev(void)
{
if(music_index>0)
{
TIM_disableCounter(TIM1);
music_index--;
wav_index = 0;
if(flag_music_playing==1)
{
read_wav_to_psram(music_file_info[music_index]);
flag_music_load=1;
}
else
{
flag_music_load=0;
}
TIM_enableCounter(TIM1);
}
}
void music_play_next(void)
{
if(music_index<25)
{
TIM_disableCounter(TIM1);
music_index++;
wav_index = 0;
if(flag_music_playing==1)
{
read_wav_to_psram(music_file_info[music_index]);
flag_music_load=1;
}
else
{
flag_music_load=0;
}
TIM_enableCounter(TIM1);
}
}
七、实物演示及说明
7.1 菜单界面
菜单界面如下图所示,可以上滑或者下滑编码器来切换选择APP

7.2 实时频谱界面
实时频谱界面如下图所示,再有人说话时可以明显看到相应频谱变化

7.3 音乐播放界面
音乐播放界面如下图所示,可以通过上滑或者下滑编码器来切换不同按键的焦点,当按下编码器时当前焦点的按键将会按下,并执行相应逻辑,包括上/下一曲、播放/暂停、是否禁音等四个按键

7.4 图片查看界面
图片查看界面如下图所示,可以上滑或者下滑编码器来切换当前图片

7.5 文本查看界面
文本查看界面如下图所示,可以通过上滑或者下滑编码器来切换不同按键的焦点,当按下编码器时当前焦点的按键将会按下,并执行相应逻辑,包括上/下滑动页面、左/右翻页等四个按键

7.6 设置界面
设置界面如下图所示,可以切换系统背景图片、修改屏幕亮度、查看当前电池电压等功能

八、总结
8.1遇到的难点及解决方法
本项目在开发过程中遇到的难点主要有以下两点:一是官方SDK中没有SDIO的相关例子,没有参开调试起来不好定位问题;二是使用eMath模块实现FFT时发现只有第一次有结果,之后再计算获取到的结果一直全0。解决方法:一是使用逻辑分析仪和示波器查看了各个引脚的波形发现有个引脚坏了,最终是飞线使用模拟SPI驱动的SD卡;二则是直接使用C代码了实现FFT。
8.2心得体会
通过本次活动,我又学习到了一种单片机的开发与使用方法,同时也是第一次设计外扩RAM、音频采集和播放相关的电路,又积累了一份软件开发经验和电路设计的经验。