一:项目介绍
我使用了纳芯微NS800RT5039评估板进行整体的项目开发,项目分为两个部分,第一部分是我的基础任务部分,完成串口输出和调用ePWM外设,在对应输出端口,输出一对互补PWM波形,波形频率2MHz,占空比50%的任务;第二部分是完成音乐律动器的制作,使用emath外设,把ADC采集到的数据进行快速傅里叶变换得到FFT数据,设计算法实现类似重力和刚体反弹的效果,在整体的感官上还是不错的。

屏幕上方是音乐当前的响度,反应当前音乐的声音大小。
二:项目设计思路
第一部分:
1.使用串口输出“Hello, NOVOSENSE Wedesign project.”字符串,需要使用到串口与板子上的DAPlink进行串口数据的提交与接收,当配置好串口和printf重定向以后即可进行串口数据的输出,这部分只需要配置好GPIO的复用关系,开启时钟和启动串口即可配置其进行串口的输出。

2.使用epwm输出一对互补的PWM波形,这个需要配置好在内部计数值匹配到数值以后的动作即可进行输出,需要配置好GPIO的复用关系,配置好GPIO和epwm的时钟,设置epwm的具体参数,类似于定时器,但是在内部的OC通道数据寄存器和CNT计数器的比对上有着更多的可调控的配置,方便了我们更加精细的配置PWM波形的输出。

第二部分:
使用ADC采集外部麦克风进行FFT制作的音乐律动器,律动器整体使用的FFT解析的数据,进行线性量化和限幅操作,从而达到在整个屏幕上显示,在处理特效上,使用了柱形图顶部的一个小球进行抛体运动增强视觉效果,在采集的ADC数据,使用ADC轮询模式进行ADC数据的采集,一次性采集到256组数据,然后进行限幅和整体去除偏置电压最后进行FFT变化,fft输出为复数,需要进行实数处理,然后进行限幅即可输出到屏幕上,在处理小球的加速度上,采取了固定间隔,路程的单次变化进行小球的路径显示,这样即可做到了小球的加速度处理,这里我们设置“地面”为硬化地面,当小球落在这个上面会损失20%的速度进行反弹,然后慢慢停下来,符合现实生活里的物理现象,使用OLED屏幕进行显示,由于其帧率高,显示效果好等特点被采用,具体的设计思路在下面的详细解读里会介绍到。

三:使用的硬件
1.纳芯微NS800RT5039评估板
2.OLED屏幕(1.4寸)
3.麦克风模块
4.手持示波器

四:硬件介绍:
NS800RT5039 评估板:其核心是 NS800RT5039 MCU,它采用单颗主频为 260MHz 的 Arm Cortex-M7 内核,支持分支预测、DSP 指令集和 FPU。配备 256KB SRAM、32KB 高速缓存(I-Cache, D-Cache)和最高 256KB 的超大紧耦合内存(ITCM, DTCM),还集成了支持浮点运算的硬件数学加速核(eMath),可大幅提升数字信号处理运算能力。该 MCU 集成了丰富的外设,评估板也相应提供了这些外设的接口。包括 16 对互补 / 32 路独立 PWM 输出(其中 16 路为高精度 HRPWM)、3 个 12 位 ADC(采样速率高达 5MSPS)、2 个 12 位 DAC(采样速率 1MSPS)、7 对高速模拟比较器 CMPSS、3 路可编程差分输入运放(PGA)等,可满足各种高精度控制应用需求。
OLED屏幕:OLED屏幕由于其高亮度和方便驱动,低功耗等优势被广泛的使用,在这里使用其进行频谱图的显示,正是其高对比度才让使用其进行显示。
麦克风模块:对外部的声音信号生产对应的电压信号,这个电压的变化会被ADC采集到,这样就可以进行FFT的运算了。
手持示波器:可以进行对EPWM输出的波形进行采样然后显示出来,方便我们查看PWM的输出效果和定位一些问题。
五:项目的详细解读:
- 串口输出“Hello, NOVOSENSE Wedesign project.”字符串
输出字符串需要使用到上面的串口外设,这里查看原理图,得知串口1接在板载的DAPlink的串口端,这里我们使用的串口一进行数据的发送,配置外设的第一步需要我们进行时钟的使能,这一步官方已经帮我们做好了,他开启了全部外设的时钟,导致这个芯片的功耗高得离谱,芯片也会发热,这里我们需要去除除时钟控制单元,GPIO,串口之外的所有的时钟开启代码,以此减少能耗。
首先进行GPIO的设定,在操作寄存器进行写入数据的时候,需要我们首先对寄存器进行解锁,这款芯片较其他的单片机而言,需要在操作寄存器的时候进行寄存器的解锁,这可以起到防止数据误操作的效果,在车载环境尤为重要。
使用Device_unlockPeriphReg();函数进行全局寄存器的解锁,我们解开以后不需要进行上锁操作,毕竟我们没有极端环境会轻易改变寄存器的数值,所以在这里只解锁不上锁了。
接下来进行GPIO的配置,首先配置其AF模式,复用引脚到串口,然后配置其的输出模式和输入的采样率,配置完GPIO以后就可以进行串口的配置了,首先我们配置好串口的波特率,停止位,校验位,最后开启串口发送就可以使用串口进行数据的发送了。
代码如下:
void Uart_Init_Config(void)
{
GPIO_setPinConfig(GPIO_29_SCIA_TX);
GPIO_setPinConfig(GPIO_28_SCIA_RX);
GPIO_setAnalogMode(GPIO_28,GPIO_ANALOG_DISABLED);
GPIO_setAnalogMode(GPIO_29,GPIO_ANALOG_DISABLED);
GPIO_setPadConfig(GPIO_28,GPIO_PIN_TYPE_STD);
GPIO_setPadConfig(GPIO_29,GPIO_PIN_TYPE_PULLUP);
GPIO_setQualificationMode(GPIO_29,GPIO_QUAL_SYNC);
GPIO_setQualificationPeriod(GPIO_29,GPIO_SMP_SYSCLK_DIV_1);
GPIO_setDriveLevel(GPIO_28,GPIO_DRV_MAX);
GPIO_writePin(GPIO_28,1);
GPIO_setDirectionMode(GPIO_29,GPIO_DIR_MODE_IN);
GPIO_setDirectionMode(GPIO_28,GPIO_DIR_MODE_OUT);
UART_enableModule(UART1);
UART_resetModule(UART1);
UART_setBaud(UART1,115200);
UART_setStopBitCount(UART1,UART_ONE_STOP_BIT);
UART_setMSB(UART1,false);
UART_enableTxFifo(UART1);
UART_resetTxFifo(UART1);
UART_setTxFifoWatermark(UART1,UART_FIFO_TX6);
UART_setParityMode(UART1,UART_PAR_NONE);
UART_enableTxModule(UART1);
UART_enableRxModule(UART1);
}
初始化串口以后就可以进行串口的重定向,这里只需要把c库里面的fputc进行重写即可:
int fputc (int ch, FILE *f)
{
(void)f;
while (UART1->STAT.BIT.TDRE == 0)
{
;
}
UART1->DATA.WORDVAL = ch;
return ch;
}
这样包含头文件stdio以后即可通过printf打印字符串:
int main(void)
{
Device_init();
Device_unlockPeriphReg();
GPIO_Config();
Uart_Init_Config();
printf("Hello, NOVOSENSE Wedesign project.\r\n");
while(1)
{
}
}
串口打印如下:

这就实现了串口打印字符串的操作,基础任务1就这样完成了。
- 调用ePWM外设,实现在对应输出端口,输出一对互补PWM波形,波形频率2MHz,占空比50%
这里使用的是NS800RT5039内部的高级外设EPWM进行波形的输出操作,对这个外设进行操作,只需要把他看作定时器的PWM输出,但是在PWM指定那里有着更多的选项,可以进行详细精细的配置。
首先我们需要配置GPIO,配置GPIO的复用和具体配置,设置成EPWM通道1的A和B输出,成为互补通道,然后配置个GPIO的模拟输入关闭,设置为复用推挽输出(这里只有推挽输出,看来复用功能是笼统的包含),设置GPIO的速度和输出方向,最后设置一下采样频率即可初始化GPIO,接下来即可初始化EPWM的相关功能了。
代码如下:
// /* gpio config */
//
GPIO_setPinConfig(GPIO_30_EPWM1_A);
GPIO_setAnalogMode(GPIO_30,GPIO_ANALOG_DISABLED);
GPIO_setPadConfig(GPIO_30,GPIO_PIN_TYPE_STD);
GPIO_setDriveLevel(GPIO_30,GPIO_DRV_MAX);
GPIO_setDirectionMode(GPIO_30,GPIO_DIR_MODE_OUT);
GPIO_setQualificationMode(GPIO_30,GPIO_QUAL_SYNC);
GPIO_setPinConfig(GPIO_31_EPWM1_B);
GPIO_setAnalogMode(GPIO_31,GPIO_ANALOG_DISABLED);
GPIO_setPadConfig(GPIO_31,GPIO_PIN_TYPE_STD);
GPIO_setDriveLevel(GPIO_31,GPIO_DRV_MAX);
GPIO_setDirectionMode(GPIO_31,GPIO_DIR_MODE_OUT);
GPIO_setQualificationMode(GPIO_31,GPIO_QUAL_SYNC);
初始化ePWM,需要像配置定时器一样,首先配置时钟(这里选择内部的时钟),然后设置定时周期,分频和CNT,计数方向,这里不启动相位偏移和输出反向,设置输出比较的比较值,设置当CNT归零以后,把比较值加载到影子寄存器里面,最后就是设置一些详细的参数了,先看PWM1的通道A,当CNT的数值归零,通道A是逻辑0,当达到周期值以后还是0,当大于比较值以后输出高,这就完成了在关键的事件发生时,输出的动作,配置还是比较详细的,我们这里只使用CMPA来操作,不使用CMPB,所以当达到CMPB的时候,无论是比CMPB高还是低都不管,所以不改变逻辑输出。同理,当CNT是0时,输出通道B输出的是高,达到周期值也是输出高,当比CMPA高时输出低,不管CMPB的任何变化(按照默认的也是可以的,这里单独配置一下就是起到突出强调的做用)无论是CNT大于还是小于CMPB,都不产生任何的变化,可见在同样的事件发生,通道A和B的动作始终是相反的,这就构造了互补PWM输出的效果。
在最后的上升沿延迟计数值(CNT)加载到影子寄存器和下降沿延迟计数值(CNT)加载到影子寄存器均设置为当CNT达到0时加载,紧接着关闭这个功能(上升沿延迟计数值(CNT)加载到影子寄存器和下降沿延迟计数值(CNT)加载到影子寄存器),可能就是需要关闭的,这里设置一下表示一下。
如此就设置好了EPWM的互补通道输出PWM的功能,由于时钟已经在系统初始化的时候全部开启了,不需要我们单独设置,通过配置这戏参数,ePWM就可以正常的运行了。
代码如下:
EPWM_setClockPrescaler(EPWM1, EPWM_CLOCK_DIVIDER_1, EPWM_HSCLOCK_DIVIDER_1);
EPWM_setTimeBasePeriod(EPWM1, 130 - 1);
EPWM_setTimeBaseCounter(EPWM1, 0);
EPWM_setTimeBaseCounterMode(EPWM1, EPWM_COUNTER_MODE_UP);
EPWM_disablePhaseShiftLoad(EPWM1);
EPWM_setPhaseShift(EPWM1, 0);
EPWM_setCounterCompareValue(EPWM1, EPWM_COUNTER_COMPARE_A, (130-1) / 2 -1);
EPWM_setCounterCompareShadowLoadMode(EPWM1, EPWM_COUNTER_COMPARE_A, EPWM_COMP_LOAD_ON_CNTR_ZERO);
EPWM_setActionQualifierAction(EPWM1, EPWM_AQ_OUTPUT_A, EPWM_AQ_OUTPUT_LOW, EPWM_AQ_OUTPUT_ON_TIMEBASE_ZERO);
EPWM_setActionQualifierAction(EPWM1, EPWM_AQ_OUTPUT_A, EPWM_AQ_OUTPUT_LOW, EPWM_AQ_OUTPUT_ON_TIMEBASE_PERIOD);
EPWM_setActionQualifierAction(EPWM1, EPWM_AQ_OUTPUT_A, EPWM_AQ_OUTPUT_HIGH, EPWM_AQ_OUTPUT_ON_TIMEBASE_UP_CMPA);
EPWM_setActionQualifierAction(EPWM1, EPWM_AQ_OUTPUT_A, EPWM_AQ_OUTPUT_NO_CHANGE, EPWM_AQ_OUTPUT_ON_TIMEBASE_UP_CMPB);
EPWM_setActionQualifierAction(EPWM1, EPWM_AQ_OUTPUT_A, EPWM_AQ_OUTPUT_NO_CHANGE, EPWM_AQ_OUTPUT_ON_TIMEBASE_DOWN_CMPB);
EPWM_setActionQualifierAction(EPWM1, EPWM_AQ_OUTPUT_B, EPWM_AQ_OUTPUT_HIGH, EPWM_AQ_OUTPUT_ON_TIMEBASE_ZERO);
EPWM_setActionQualifierAction(EPWM1, EPWM_AQ_OUTPUT_B, EPWM_AQ_OUTPUT_HIGH, EPWM_AQ_OUTPUT_ON_TIMEBASE_PERIOD);
EPWM_setActionQualifierAction(EPWM1, EPWM_AQ_OUTPUT_B, EPWM_AQ_OUTPUT_LOW, EPWM_AQ_OUTPUT_ON_TIMEBASE_UP_CMPA);
EPWM_setActionQualifierAction(EPWM1, EPWM_AQ_OUTPUT_B, EPWM_AQ_OUTPUT_NO_CHANGE, EPWM_AQ_OUTPUT_ON_TIMEBASE_UP_CMPB);
EPWM_setActionQualifierAction(EPWM1, EPWM_AQ_OUTPUT_B, EPWM_AQ_OUTPUT_NO_CHANGE, EPWM_AQ_OUTPUT_ON_TIMEBASE_DOWN_CMPB);
EPWM_setRisingEdgeDelayCountShadowLoadMode(EPWM1, EPWM_RED_LOAD_ON_CNTR_ZERO);
EPWM_disableRisingEdgeDelayCountShadowLoadMode(EPWM1);
EPWM_setFallingEdgeDelayCountShadowLoadMode(EPWM1, EPWM_FED_LOAD_ON_CNTR_ZERO);
EPWM_disableFallingEdgeDelayCountShadowLoadMode(EPWM1);
这里我们单独看一下EPWM两个通道的输出:
通道A:

通道B:

既然是互补通道,这样肯定都是看不出互补的效果,但是既然是互补,肯定是一个为高的时候一个为低,如果把示波器的正接在通道A,负接在通道B,那么会呈现频率,占空比不变,但是会出现负电压的情况,如下图可见,出现了负电压,证明是互补的一对通道生成的2Mhz,占空比50%的互补PWM波。

至此,互补PWM生成的例子就完成了,第二部分到此结束。
- 额外任务:使用NSSinePad-NS800RT5039开发板,并使用硬件EMATH功能和麦克风,配合LED屏或者LCD屏,完成一个音乐律动器。
音乐律动器的制作看起来很复杂,但是理清顺序其实就是麦克风通过ADC采集电压数据,传输给eMATH外设进行快速傅里叶变换,然后从复数域转换成实数域以后,量化以后即可得到初步的频谱图,接着设计一些算法让显示更加的灵动,就得到了我们的音乐律动器。
首先我们需要使用到ADC,这里我们不需要配置GPIO,ADC有着专门的通道介入到内部的ADC转换单元,包括DAC和模拟比较器。ADC这里是需要使用到内部一个叫SOC的转换单元,这个转换单元有20个,一共三个ADC转换器,每个里面都有20个SOC,转换单元一个只负责转换一个ADC通道,内部的转换是通过一个硬件状态机实现的,具体的实现和使用方法在数据手册里提到的很详细,这里就不再赘述了,我们这里介绍如何使用ADC进行数据采样。
首先我们设置一下采样的基准电压,可惜的是ADC这里只有1.7V和2.5V的内部基准电压,没有3.3v,需要通过外部的基准电压进行校准,好在板子上引出了VREF的引脚,连接到3.3v即可把3..3v作为校准电压(最大值),通过设置外部基准进行设置,然后设置ADC内部时钟的分频,设置当ADC一个SOC转换结束以后进行中断标志的置位,设置完这些基础参数以后,就可以开启ADCA的转换单元,这里开启以后还需要延时1秒,目的是ADC刚开始的时候采样不精准,内部的校准电容还没有达到最佳工作状态,延时1秒让ADC的系统稳定下来以后再进行采样,那数据就会精准很多。
在关闭“burst”模式(突发模式允许单个触发器一次遍历一个或多个循环 SOC)和设置SOC的优先级为无高优先级以后,即可具体设置SOC的工作状态:
设置软件触发SOC单元,使用SOC0的通道0,设置SOC转换完成就触发中断,开启中断,清除标志位和开启连续采样即可让ADC正常的工作,这样就可以进行ADC的采样了。
void ADC_Soc_0_Channal1_Init_Config(void)
{
ADC_setVREF(ADCA, ADC_REFERENCE_EXTERNAL, ADC_REFERENCE_3_3V);
/* Configures the analog-to-digital converter module prescaler. */
ADC_setPrescaler(ADCA, ADC_CLK_DIV_4);
/* Sets the timing of the end-of-conversion pulse */
ADC_setInterruptPulsePosMode(ADCA, ADC_PULSE_END_OF_CONV);
/* Powers up the analog-to-digital converter core. */
ADC_enableConverter(ADCA);
/* Delay for 1ms to allow ADC time to power up */
Delay_us(1000);
/* SOC Configuration: Setup ADC EPWM channel and trigger settings */
/* Disables SOC burst mode. */
ADC_disableBurstMode(ADCA);
/* Sets the priority mode of the SOCs. */
ADC_setSOCPriority(ADCA, ADC_PRI_ALL_ROUND_ROBIN);
/* Start of Conversion 0 Configuration */
/* Configures a start-of-conversion (SOC) in the ADC and its interrupt SOC trigger.
SOC number : 0
Trigger : ADC_TRIGGER_SW_ONLY
Channel : ADC_CH_ADCIN0
Sample Window : 8 SYSCLK cycles
Interrupt Trigger : ADC_INT_SOC_TRIGGER_NONE */
ADC_setupSOC(ADCA, ADC_SOC_NUMBER0, ADC_TRIGGER_SW_ONLY, ADC_CH_ADCIN0, 8U);
ADC_setInterruptSOCTrigger(ADCA, ADC_SOC_NUMBER0, ADC_INT_SOC_TRIGGER_NONE);
/* Start of Conversion 1 Configuration */
/* Configures a start-of-conversion (SOC) in the ADC and its interrupt SOC trigger.
SOC number : 1
Trigger : ADC_TRIGGER_SW_ONLY
Channel : ADC_CH_ADCIN1
Sample Window : 8 SYSCLK cycles
Interrupt Trigger : ADC_INT_SOC_TRIGGER_NONE */
ADC_setInterruptSource(ADCA, ADC_INT_NUMBER1, ADC_SOC_NUMBER0);
ADC_clearInterruptStatus(ADCA, ADC_INT_NUMBER1);
ADC_enableContinuousMode(ADCA, ADC_INT_NUMBER1);
ADC_enableInterrupt(ADCA, ADC_INT_NUMBER1);
}
使用ADC进行采样的函数如下,把采集到的数据存放在一个全局数组内部,当然,麦克风携带着2v左右的分量这样会导致ADC的FFT直流分量很大,所以需要减去分量,然后采样原理就是等待中断标志位被置位,清除以后读取即可实现这个功能,循环256次采集256组ADC数据,然后进行最大最小值搜索,这目的是实现获取在这个256数据里,采集到的音频数据的极差,通过这个即可知道响度,这样就可以得到一个可以动的音量大小随着音乐实时动作的效果,这样就是实现声音最大响度的获取操作。
void ADC_Test(void)
{
for(int i =0;i <256 ; i++)
{
/* Wait for ADCA to complete, then acknowledge flag */
ADC_forceMultipleSOC(ADCA, (ADC_FORCE_SOC0));
while(ADC_getInterruptStatus(ADCA, ADC_INT_NUMBER1) == false){}
ADC_clearInterruptStatus(ADCA, ADC_INT_NUMBER1);
uint32_t tem = ADC_readResult(ADCARESULT, ADC_SOC_NUMBER0);
Rand_Buffer[i] = (tem - 2780);
ADC_Scan.ADC_Current = Rand_Buffer[i];
if(ADC_Scan.ADC_Current < ADC_Scan.ADC_Min)
{
ADC_Scan.ADC_Min = (ADC_Scan.ADC_Current);
}
else if(ADC_Scan.ADC_Current > ADC_Scan.ADC_Max)
{
ADC_Scan.ADC_Max = (ADC_Scan.ADC_Current);
}
Delay_us(6);
/* Store results */
//printf("ADC:%d\r\n",ADC_readResult(ADCARESULT, ADC_SOC_NUMBER0));
}
}
下一步就可以把获取的ADC数据进行FFT操作了,首先需要开启emath的时钟,让emath进行运作,然后调用FFT的库函数,选择输入数据的格式是16位的,采集到的数据就是short类型的,所以选择16位的FFT,8指的是你的FFT的数据量,我采用的是256个数据就是2的8次方,所以通过设置这个参数可以进行全局的一个限幅操作,最后一个不用,填0.
然后就可以把数据通过Rand_Buffer传输到FFT进行运算了,一共256个参数,输出的FFT数据输出到output数组内部,这样就实现了FFT的一个运作,这个函数的调用还是比较简单的,最后等待EMATH运算结束,即可开始对得到的FFT从复数域转到实数域的操作。
EMATH_setFormat(EMATH_CP_FFT, EMATH_16Bit, 8, 0);
EMATH_transformRFFT(Rand_Buffer, 256, output);
EMATH_waitDone();
从复数域转到实数域可以通过对复数进行实部虚部平方和再开根号即可得到,这个很简单,通过一个循环即可实现:
void ADC_Data_Deal(void)
{
for(uint16_t i =0;i<256;i++)
{
uint16_t Check = (uint16_t)EMATH_sqrtF32((float)output[i*2] * output[i*2]+(float)output[i*2+1] * output[i*2+1]); // Ð鲿ƽ·½
output[i] = Check >> 7;
}
FFT_Test(output + 1);
}
最后就可进行FFT再OLED上的绘制,绘制采用的是通过一个缓冲区,每次绘制128*8大小的像素,通过从底部到顶部每层开始绘制,通过对每一个FFT的数据进行逐个判断是否再当前的行来进行绘制,如果不是则当前行就是0xff全满,不是就按照当前行需要显示的剩余个数进行绘制,最终会得到一个频谱,由于ADC采集和运算需要时间,间隔的ADC采样必然会导致频谱泄露,这里没有处理,因为泄露的不算太严重。这里如何绘制其实蛮好理解的,就是判断FFT是否是在当前行,如果是就减去下面的部分,剩余的必然在0~8之间,然后就可以把余数绘制出来啦,就实现了频谱的绘制。
然后就是响度的绘制了,响度的绘制就是获取ADC的极差,然后量化一下,把数据放在第8行进行显示,第八行的显示只需要获得在哪里进行点亮,直到哪里结束,通过循环即可进行绘制响度条,绘制出当前音乐的响度。
void FFT_Test(int16_t Data_Buffer[128])
{
uint8_t i =0;
uint8_t height = 0;
ADC_Scan.ADC_Diff = ADC_Scan.ADC_Max - (ADC_Scan.ADC_Min);
uint16_t Diff = abs((ADC_Scan.ADC_Diff - 140) / 10);
ADC_Scan.ADC_Current = 0;
ADC_Scan.ADC_Diff = 0;
ADC_Scan.ADC_Max = 0;
ADC_Scan.ADC_Min = 0;
Last_Data_Deal(Data_Buffer);
for(int i = 0;i < 7;i++)
{
for(int j =0;j < 128 ; j++)
{
if(Data_Buffer[j] >=8)
{
OLED_Display_Buffer[j] = 0xff;
Data_Buffer[j]-=8;
continue;
}
if(Data_Buffer[j]<8 && Data_Buffer[j]>0)
{
uint8_t Add = 0x00;
for(int a = 0; a < Data_Buffer[j];a ++)
{
Add <<= 1;
Add |= 0x01;
}
OLED_Display_Buffer[j] = Add;
Data_Buffer[j] = 0;
}
if(Last_Data[j] - 8 * i > 0 && Last_Data[j] - 8 *i <8)
{
OLED_Display_Buffer[j] |= 0x01 << ((Last_Data[j] - 8 * i) - 1);
}
}
OLED_Show_Whole_Screen(i,OLED_Display_Buffer);
memset((char *)OLED_Display_Buffer,0,128);
}
for(int j =0;j < 128 ; j++)
{
if(j > 63 - Diff && j < 64)
{
OLED_Display_Buffer[j] |= 0xC0;
}
if(j < 64 + Diff && j > 63)
{
OLED_Display_Buffer[j] |= 0xC0;
}
if(Data_Buffer[j] >=8)
{
OLED_Display_Buffer[j] |= 0x0f;
Data_Buffer[j]-=8;
continue;
}
if(Data_Buffer[j]<8 && Data_Buffer[j]>0)
{
uint8_t Add = 0x00;
if(Data_Buffer[j]>4)
{
Data_Buffer[j] = 4;
}
for(int a = 0; a < Data_Buffer[j];a ++)
{
Add <<= 1;
Add |= 0x01;
}
OLED_Display_Buffer[j] |= Add;
Data_Buffer[j] = 0;
}
if(Last_Data[j] - 8 * 7 > 0 && Last_Data[j] - 8 * 7 <8)
{
OLED_Display_Buffer[j] |= 0x01 << ((Last_Data[j] - 8 * 7) - 1);
}
}
OLED_Show_Whole_Screen(7,OLED_Display_Buffer);
memset((char *)OLED_Display_Buffer,0,128);
Last_Data_Dec(Last_Data);
//Time_Base++;
}
按理来说到这一步已经完成了频谱的绘制,只要把ADC采样连起来,就看也绘制动态的频谱图了,但是这里比较是音乐频谱,看起来灵动一点,这里我们才用了模拟重力的小球起跳操作,小球是频谱的最高点,最高点会以条从上一刻到这一刻的速度得到初始速度,然后做垂直上抛运动落到条上结束,并且我设立了地面是硬质,小球落在地面会反弹,模拟真实物理条件下小球落地反弹的运动,符合我们的认知。
这里我们需要获取条相对于上一刻的速度,如果小球还没有落到条上,那么就不需要管,只需要在小球落在条上或者下面的时候才更新状态,这里的速度是小球相对于条运动的速度,这样就可以模拟小球的初始速度了。
接下来就是计算小球的在加速度下的运动路程了,我不想计算事件,并且时间间隔一定,所以可以计算每一刻的位移,公式就是:Last_DATA[i] = Last_DATA[i] + ((float)Speed[i]*t -(float)((2.0f*Diff_Time[i] -1.0f)/2.0f) * G * t*t ); 即可获取到每一时刻的唯一,更新一下然后再显示函数里进行显示即可实现这个操作,在小球落地以后,会减少20%的速度,这样会模拟出比较真实的小球落地慢慢衰减高度的效果。
void Last_Data_Deal(int16_t Data_Buffer[128])
{
for(uint8_t i=0;i<128;i++)
{
if(Data_Buffer[i] > Last_Data[i])
{
Speed[i] = (Data_Buffer[i] - Last_Data[i]) * 0.75 ;
Last_Data[i] = Data_Buffer[i];
Diff_Time[i] = 1;
}
}
}
void Last_Data_Dec(int16_t Last_DATA[128])
{
for(uint8_t i=0;i < 128;i ++)
{
Last_DATA[i] = Last_DATA[i] + ((float)Speed[i]*t -(float)((2.0f*Diff_Time[i] -1.0f)/2.0f) * G * t*t );
Diff_Time[i] ++ ;
if(Last_DATA[i] < 0)
{
Last_DATA[i] = 0;
Speed[i] -= Speed[i] * 0.2;
Diff_Time[i] = 1;
}
}
}
通过这样,就可以制作出频谱图的小球运动的效果,实现在重力加速度下的美观绘制,这就是我实现的音乐律动器的具体思路。
最后main函数如下:
int main(void)
{
Device_init();
Device_unlockPeriphReg();
GPIO_Config();
Uart_Init_Config();
printf("Hello, NOVOSENSE Wedesign project.\r\n");
OLED_Init();
ADC_Soc_0_Channal1_Init_Config();
// ÉèÖÃËæ»úÊýÖÖ×Ó
//OLED_ShowString(1,1,"zxyyl");
while(1)
{
GPIO_clearPin(GPIO_15);
// Delay_ms(1000UL);
// GPIO_setPin(GPIO_20);
// GPIO_setPin(GPIOA,GPIO_PIN_15);
// Delay_ms(1000UL);
// srand(SysTick->VAL);
ADC_Test();
//Get_Sin_Data();
EMATH_setFormat(EMATH_CP_FFT, EMATH_16Bit, 8, 0);
EMATH_transformRFFT(Rand_Buffer, 256, output);
EMATH_waitDone();
//Rand_Data_Get();
//FFT_Test(output);
//Delay_ms(50);
ADC_Data_Deal();
}
}
效果如下:
这里使用了《Country road》进行演示,由于麦克风的灵敏度不高,需要把手机扬声器放在麦克风上会有好的响应,所以
需要放在上面才可以。


可见上面的白色长条是响度的显示,下面的就是频谱图和小球的弹射效果。
至此,音乐律动器的制作就结束了,可以参考这个视频查看具体的频谱显示效果,由于OLED的刷新和摄像头的刷新差不多,会出现斜着的移动黑条,实际的显示和上面一样,帧率还不错,看着很流畅。
六:总结
遇到的问题:
- 在开发这个芯片的时候,是使用的板级包进行开发,这个里面开启了全部的外设,导致整体的功率(整个板子的功率达到了1瓦,芯片也略微发烫,虽然厂家的操作开启了全部的时钟,这样我们就不必担心时钟开启的问题,这也导致了无关的功率的增加,可以改进一下。
- 在使用芯片进行SOC采集ADC的时候使用的是中断标志位的方法,本来想配置为连续采样的模式,可是配置以后只能采集一次就停止了,并没有按照预期的一样进行连续的采样,这个问题没有得到解决,没办法还是采用读取标志位然后读取数据,手动开启下一次转换实现了,不简洁,但是无论怎么配置都无法启动其连续采样模式。
- 在一开始进行开发的时候,不知道寄存器需要解锁才可以进行配置,导致一开始的配置没起到任何的效果,后来参考手册和例程得知,这个芯片在操作寄存器的时候需要先对寄存器进行解锁,然后配置完以后再锁上,放在寄存器被错误的更改,这一点需要注意,再很多芯片上没有这一步操作,这个算是一个比较安全的配置方案。
心得体会:
通过这次活动,我理解了如何基于NS800RT5039进行开发其EPWM和emath两个外设,还有操作GPIO和串口,再开发的过程里,学习到了很多的知识点,包括ADC的SOC硬件状态机和指针式SOC转换方法,EPWM的详细转换单元和时基单元的配置,已经输出比较寄存器的具体事件的详细配置方法,emath的256的fft计算,方便了我们的使用和计算FFT,通过制作了音乐律动器,实现了缓冲区转换和重力加速度的计算和使用,丰富了我开发经验和算法的搭建方法,还学到了很多的开发方法和一些外设相对于其他单片机外设的异同,拓展了我的视野。
最后,感谢硬禾学堂推出的《WeDesign7》活动,此次活动带给我许多宝贵实践经验和机会,让我们下期活动再见!