HPM6360使用ADC内置DMA实现最高4K深度周期采样
参考资料
开发环境
硬件环境
HPM6300EVK(主芯片HPM6360)
软件环境
Ubuntu 22.04(虚拟机环境),hpm_sdk_v1.3,nds-gcc-toolchain(unknown工具链不支持DSP函数)
使用VSCode进行编程
原理解析
HPM6360的ADC官方例程给得很玄学,只有CPU中断的周期采样和没有任何周期性可言的序列采样。本例给出等周期序列采样的实现过程。
首先,等周期采样需要进行定时。在HPM6360中,ADC依赖于中断触发采样序列,定时依赖于PWM。而在PWM与ADC之间还需要通过TRGM进行中转。在ADC整个序列采样完成之后,ADC的内置DMA会完成本次采集的数据转移工作。在DMA把整个缓冲区填满之后,会调用CPU中断对结果进行处理。
信号触发流程示意图如下:
因此,本例需要配置的也就是这四个主要部件。
详细配置
宏定义
// adc/inc/main.h
#define APP_ADC16_SAMPLE_RATE_KHZ 1000
#define APP_ADC16_SEQ_DMA_BUFF_LEN_IN_4BYTES 4096
#define APP_ADC16_CH_NUM 1
ADC
先端上代码:
// adc/src/init.c
/* initialize an ADC instance */
adc16_config_t cfg;
adc16_get_default_config(&cfg);
cfg.res = adc16_res_16_bits;
cfg.conv_mode = adc16_conv_mode_sequence;
cfg.adc_clk_div = adc16_clock_divider_4;
cfg.sel_sync_ahb = (clk_adc_src_ahb0 == clock_get_source(BOARD_APP_ADC16_CLK_NAME)) ? true : false;
cfg.adc_ahb_en = true;
stat = adc16_init(BOARD_APP_ADC16_BASE, &cfg);
if (stat != status_success)
{
printf("%s initialization failed, cause: %s\n", BOARD_APP_ADC16_NAME, err_print_error_string(stat));
while (1)
;
}
intc_m_enable_irq_with_priority(BOARD_APP_ADC16_IRQn, 1);
printf("Initialized %s.\n", BOARD_APP_ADC16_NAME);
这里就没什么好说的,主要就是把ADC配置成序列转换模式,并且开启ADC的IRQ中断。这一部分代码抄自官方ADC16的例程。
序列
// adc/src/init.c
/* Set a sequence config */
adc16_seq_config_t seq_cfg;
seq_cfg.seq_len = APP_ADC16_CH_NUM;
seq_cfg.restart_en = false;
seq_cfg.cont_en = true;
seq_cfg.hw_trig_en = true;
seq_cfg.sw_trig_en = false;
for (int i = 0; i < seq_cfg.seq_len; i++)
{
seq_cfg.queue[i].seq_int_en = false;
seq_cfg.queue[i].ch = seq_adc_channel[i];
}
seq_cfg.queue[seq_cfg.seq_len - 1].seq_int_en = true;
这里主要就是进行ADC采样序列的配置。测试时只设置了一个通道,那其实seq_cfg.cont_en
的值关系不大。但是如果要进行多通道的ADC等采样的话,seq_cfg.cont_en
的值必须设置为true
。
对于seq_cfg.restart_en
和seq_cfg.cont_en
的设置,数据手册中有详细的说明:
这个叙述在HPM6360数据手册的第895页,详细程度在HPM的官方手册里面也属于是相当详尽了。如果是多通道等周期采样,则需要将seq_cfg.cont_en
设置为true
,seq_cfg.restart_en
设置位true
。如果seq_cfg.restart_en
设置位false
,则ADC在收到一次触发信号之后直接以最该采样率完成整个缓冲区的转换,这样就使得ADC的采样变得不可控了。
PWM
同样地,放上代码好说话:
// adc/src/init.c
/* Trigger source initialization */
pwm_cmp_config_t pwm_cmp_cfg;
pwm_output_channel_t pwm_output_ch_cfg;
pwm_set_reload(BOARD_APP_ADC16_HW_TRIG_SRC, 0, clock_get_frequency(clock_ahb) / 1000 / APP_ADC16_SAMPLE_RATE_KHZ - 1);
memset(&pwm_cmp_cfg, 0x00, sizeof(pwm_cmp_config_t));
pwm_cmp_cfg.enable_ex_cmp = false;
pwm_cmp_cfg.mode = pwm_cmp_mode_output_compare;
pwm_cmp_cfg.update_trigger = pwm_shadow_register_update_on_shlk;
pwm_cmp_cfg.cmp = 1;
pwm_config_cmp(BOARD_APP_ADC16_HW_TRIG_SRC, 8, &pwm_cmp_cfg);
pwm_issue_shadow_register_lock_event(BOARD_APP_ADC16_HW_TRIG_SRC);
pwm_output_ch_cfg.cmp_start_index = 8; /* start channel */
pwm_output_ch_cfg.cmp_end_index = 8; /* end channel */
pwm_output_ch_cfg.invert_output = false;
pwm_config_output_channel(BOARD_APP_ADC16_HW_TRIG_SRC, 8, &pwm_output_ch_cfg);
首先需要注意的是,pwm_set_reload
函数用于设定PWM的重载值,也就是相当于设置了PWM一个周期的时间,也就是相当于在设定ADC的等周期采样率。PWM的基频是AHB总线的频率,这里用clock_get_frequency
函数获取AHB的时钟频率之后再设置PWM的重载值。这个设置的公式很简单,就不多说了。此处配置也是照抄官方例程的,改了一下重载值而已。
另一个要注意的是,pwm_config_output_channel函数将PWM的输出通道设为8,因为TRGM中PWM通道8才能到达ADC。具体参考数据手册:
明显,PWM通道号小于8的通道没法进入TRGM。
TRGM
代码如下:
// adc/src/init.c
/* Trigger mux initialization */
trgm_output_t trgm_output_cfg;
trgm_output_cfg.invert = false;
trgm_output_cfg.type = trgm_output_same_as_input;
trgm_output_cfg.input = BOARD_APP_ADC16_HW_TRGM_IN;
trgm_output_config(BOARD_APP_ADC16_HW_TRGM, BOARD_APP_ADC16_HW_TRGM_OUT_SEQ, &trgm_output_cfg);
宏定义BOARD_APP_ADC16_HW_TRGM_OUT_SEQ
指向的是TRGM_TRGOCFG_ADC0_STRGI
(在board.h内定义)。BOARD_APP_ADC16_HW_TRGM_IN
指向的是HPM_TRGM0_INPUT_SRC_PWM0_CH8REF
。也就是说,PWM的通道8进入了TRGM,然后TRGM把这个通道给到ADC作为中断源。
DMA
代码先上:
// adc/src/init.c
/* Initialize a sequence */
adc16_dma_config_t dma_cfg;
adc16_set_seq_config(BOARD_APP_ADC16_BASE, &seq_cfg);
dma_cfg.start_addr = (uint32_t *)core_local_mem_to_sys_address(BOARD_RUNNING_CORE, (uint32_t)seq_buff);
dma_cfg.buff_len_in_4bytes = APP_ADC16_SEQ_DMA_BUFF_LEN_IN_4BYTES;
dma_cfg.stop_en = true;
dma_cfg.stop_pos = APP_ADC16_SEQ_DMA_BUFF_LEN_IN_4BYTES - 1;
adc16_init_seq_dma(BOARD_APP_ADC16_BASE, &dma_cfg);
大部分都是抄官方例程的,但是stop_en
改为了true,因为只需要进行一次APP_ADC16_SEQ_DMA_BUFF_LEN_IN_4BYTES
深度的等周期采样。DMA在把seq_buff
缓冲区填满之后就停下并且触发DMA Abort中断。这个中断将会在稍后被设置。
触发与中断
采样触发
采样的触发函数在定时器的中断函数内,可以设置每隔几秒钟触发一次:
// adc/src/interrupts.c
void timer_callback_func(void)
{
board_led_toggle();
pwm_start_counter(BOARD_APP_ADC16_HW_TRIG_SRC);
return;
}
没错,就很粗暴,只需要让PWM开始计数就行了。
中断处理
首先是中断的设置:
// adc/src/init.c
/* Enable sequence complete interrupt */
adc16_enable_interrupts(BOARD_APP_ADC16_BASE, adc16_event_seq_dma_abort);
这里设置的是DMA Abort中断,也就是DMA在填满缓冲区(点题:最大4096,也就是4K深度)之后会触发ADC的中断。以下是中断的处理函数:
// adc/src/interrupts.c
void isr_adc16(void)
{
pwm_stop_counter(BOARD_APP_ADC16_HW_TRIG_SRC);
BOARD_APP_ADC16_BASE->SEQ_DMA_CFG |= (1 << 13);
BOARD_APP_ADC16_BASE->SEQ_DMA_CFG &= ~(1 << 13);
uint32_t status = adc16_get_status_flags(BOARD_APP_ADC16_BASE);
adc16_clear_status_flags(BOARD_APP_ADC16_BASE, status);
adc16_seq_dma_data_t *dma_data = (adc16_seq_dma_data_t *)seq_buff;
float *array = (float *)sdram;
for (int i = 0; i < APP_ADC16_SEQ_DMA_BUFF_LEN_IN_4BYTES; ++i)
{
array[i] = dma_data[i].result;
}
const uint32_t conv_core_length = 256;
float conv_core[conv_core_length];
for (int i = 0; i < conv_core_length; ++i)
{
conv_core[i] = GET_MIN_VALUE(i, conv_core_length - 1);
}
float *conv_result = &(array[APP_ADC16_SEQ_DMA_BUFF_LEN_IN_4BYTES]);
misc_start_time();
hpm_dsp_conv_f32(conv_core, conv_core_length, array, APP_ADC16_SEQ_DMA_BUFF_LEN_IN_4BYTES, conv_result);
printf("Convolution time: %d us\n", misc_get_end_time() / 200);
uint32_t buffer_butwidth = 12;
printf("Start algorithm: %d\n", buffer_butwidth);
uint32_t max_index = 0;
misc_start_time();
hpm_dsp_rifft_f32(array, buffer_butwidth);
for (int i = 0; i < 5; ++i)
{
array[i] = 0;
}
hpm_dsp_max_f32(array, APP_ADC16_SEQ_DMA_BUFF_LEN_IN_4BYTES, &max_index);
printf("FFT time: %d us\n", misc_get_end_time() / 200);
printf("Max Frequency: %d, %f KHz\n", max_index, 1.0 * max_index * APP_ADC16_SAMPLE_RATE_KHZ / APP_ADC16_SEQ_DMA_BUFF_LEN_IN_4BYTES);
// for (int i = 0; i < APP_ADC16_SEQ_DMA_BUFF_LEN_IN_4BYTES - 1; i++)
// {
// printf("%d, %04d\n", i, array[i]);
// }
return;
}
SDK_DECLARE_EXT_ISR_M(BOARD_APP_ADC16_IRQn, isr_adc16)
在这个处理函数中,先将PWM停下,然后初始化ADC内置DMA、清除中断标志。这样就完成了下一次采样的准备。但是做完这些工作后,下一次采样肯定没有这么快到达,因此可以先对ADC采集到的数据进行处理。
ADC的DMA移动数据的时候是有一定的格式的,这个格式在结构体adc16_seq_dma_data_t
中被定义。也就是说,其实seq_buff
的每一个数据项都以这种格式存在。首先在for循环中把数据取出来形成一个纯数据缓冲区。这里还做了一下数据格式转换,把16位无符号整数转换成了32位浮点数,以备后续的卷积以及FFT处理。
卷积和FFT就不多说了,但是速度很惊人:
4096长度的数据跟256长度的数据进行卷积,只用了12毫秒;4096点的基2FFT只用二3毫秒多一点。这个速度也是很优秀了,硬件加速就是快。
在这个性能下,芯片的温度也还是比较好看的:
核心温度才44.6℃,估计结温不超过60℃。
ADC性能测试
因为春节期间还在家,没得示波器和信号源,这一部分留到回学校再测。
其他问题
使用XDMA和HDMA进行ADC等周期采样的DMA是否可行?
目前按照数据手册来看好像不是很行,因为DMAMUX没有ADC中断的输入可选,也就是ADC的中断不能作为触发源进入DMA。也就是说,在CPU不参与的情况下是没法实现ADC一个序列完成之后通过中断触发DMA进行数据搬移的。
这样设计的原因有可能是这个片子的定位就是在电机控制那种实时控制类场景,而不是进行等周期采样后进行数据分析。
采样率最高能到多少?
现在我设置的采样率时1MSPS,数据手册的纸面数据最高是2MSPS。但是按照HPM一贯的超频潜力来看,估计不止这个数。但是这么高的采样率精准度是否能够保证,则要在之后有专业仪器的测试中才能确定。