#申请原创# @21小跑堂 前言: 各位论坛老板们新年快乐,果然还是二姨智囊团厉害,刚过完年就出来整活,直接手撕我们的工作内容,真不知道这主意谁想出来的,但是各位大佬写帖子注意不要触犯保密协议哈,这里预祝活动红红火火,看大家都没有动手,小弟先抛个砖。。。
其实过完年回来本人准备请假休息两天的,因为春节档的高速路况大家都是心知肚明的,返程开了十几个小时的车,真的累坏了。于是准备请假歇一下,但是被老板无情驳回,因为这周有客户过来商讨新的产品的测试数据分析和下一步优化工作,我年前就是做测试,为别人提供数据,所以放假回来第一天必须返岗整理数据,为后面的会议的顺利进行做准备,期间遇到了几个问题,这里向大家分享一下。
我的主控是GD32E230C8T6,64K的FLASH,8K的SRAM。这里我累赘的提一下MCU的型号和FLASH和SRAM的容量,这个也是经常会遇到的问题。会在后文进行详细描述。我们是一个同步的项目,一般在MCU读取传感器数据时只需要发送指令便可获得,但是为了得到同步的数据,需要MCU不停地为传感器提供时钟,这种问题其实很简单,由定时器的PWM模式直接输出给传感器即可,但是传感器内部不可能一直去提取数据,所以就需要MCU提供信号给传感器去触发传感器的数据采集,采集完成后再通过通信协议将数据返回给MCU。其实整个过程都不是很麻烦,麻烦在我们的周期特别短,只有几十微秒。要在极短的时间内完成一系列的操作还是很麻烦的。
问题一:如何输出传感器需要的波形数据。
传感器需要的波形如下:
上面两个波形是MCU需要输出给传感器的波形,第三个是传感器回复的波形,1号波形的下降沿离2号波形的上升沿必须大于10ns。
分析:
中间的固定脉宽的方波可由PWM直接输出,且PWM启动后无需再做任何处理,问题的关键在于控制第一个波形,该波形的下降沿要可控,在控制低电平的时长等于二号方波的固定数量(该数量大约是100个左右)。受其他因素影响,整个过程不能占用太多时间,因为在做完该步骤后还需要进行SPI和USART的通信。所以就要求2号方波的脉宽要尽可能短,才能使整个过程的时间缩短。而1号和2号波形的联动,最好采用定时器的级联--主从定时器。而3号波形是传感器反馈的,由MCU接收,做一个外部中断就好。
解决方法:
主定时器采用TIMER14的PWM模式,预分频值(prescaler)为4-1,周期(Period)为2-1,使用PWM1模式,通道输出比较值(Pulse)为1。
定时器的时钟频率为systemcoreclock/prescaler = 72MHz/4=18MHz
那么单脉冲宽度大约为
single pulse value = (TIMER14_Period - TIMER14_Pulse) / TIMER14 counter clock
= (2 - 1) / 18MHz = 55 ns.
假设1波形低电平期间需要100个2号脉冲,那么也只需要5.5us,还是非常快的。这样就可以给其他通信留有足够的空间。
代码实现如下:
timer_oc_parameter_struct timer_ocinitpara;
timer_parameter_struct timer_initpara;
/* enable the peripherals clock */
rcu_periph_clock_enable(RCU_TIMER0);
rcu_periph_clock_enable(RCU_TIMER14);
rcu_periph_clock_enable(RCU_TIMER2);
/* TIMER14 configuration */
timer_deinit(TIMER14);
/* initialize TIMER init parameter struct */
timer_struct_para_init(&timer_initpara);
timer_initpara.prescaler = 3;
timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
timer_initpara.counterdirection = TIMER_COUNTER_UP;
timer_initpara.period = 1;
timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
timer_initpara.repetitioncounter = 0;
timer_init(TIMER14, &timer_initpara);
/* initialize TIMER channel output parameter struct */
timer_channel_output_struct_para_init(&timer_ocinitpara);
/* configure TIMER channel output function */
timer_ocinitpara.outputstate = TIMER_CCX_ENABLE;
timer_ocinitpara.outputnstate = TIMER_CCXN_DISABLE;
timer_ocinitpara.ocpolarity = TIMER_OC_POLARITY_HIGH;
timer_ocinitpara.ocnpolarity = TIMER_OCN_POLARITY_HIGH;
timer_ocinitpara.ocidlestate = TIMER_OC_IDLE_STATE_LOW;
timer_ocinitpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;
timer_channel_output_config(TIMER14, TIMER_CH_0, &timer_ocinitpara);
/* configure TIMER channel output pulse value */
timer_channel_output_pulse_value_config(TIMER14, TIMER_CH_0, 1);
/* CH0 configuration in PWM0 mode */
timer_channel_output_mode_config(TIMER14, TIMER_CH_0, TIMER_OC_MODE_PWM1);
/* configure TIMER channel output shadow function */
timer_channel_output_shadow_config(TIMER14, TIMER_CH_0, TIMER_OC_SHADOW_DISABLE);
/* auto-reload preload enable */
timer_auto_reload_shadow_enable(TIMER14);
/* select the master slave mode */
timer_master_slave_mode_config(TIMER14, TIMER_MASTER_SLAVE_MODE_ENABLE);
/* TIMER14 update event is used as trigger output */
timer_master_output_trigger_source_select(TIMER14, TIMER_TRI_OUT_SRC_UPDATE);
/* TIMER14 output enable */
timer_primary_output_config(TIMER14, ENABLE);
/* TIMER counter enable */
timer_enable(TIMER14);
在确定完主定时器后需要选择从定时器,以及确定从定时器的模式。此时插一个题外话,不知是否有朋友想到不用主从定时器,1号波形就用IO口输出高低电平来处理,通过将TIMER14的输出口与任一闲置IO短接,通过外部中断来识别经过了多少个脉冲,到达指定数量后关闭中断即可。其实这个方法是用于这个波形,但是不是用于这个项目,因为我的单脉冲只有55ns,如下图:
MCU跳转中断会耽误时间,同时使用IO的输出功能翻转IO电平更是耽误时间,翻转一个IO的时间2号脉冲都跑过去很多了,达不到目的,此方案我做过测试,不可用,但是我这里没有测试数据了,这里不详细介绍。
关于从定时器的选择我最先考虑的是从定时器采用输入捕获模式,将IO口拉低,数到对应数量的波形进入中断,再将IO拉高。这就带来了和上述同样的问题,而且无法控制在拉低IO口时的下降沿距离下一个2号波形的上升沿之间严格大于10ns。
经过多次试验挑选,最终确定从定时器同样采用PWM方式,而从定时器的时钟是由主定时器提供的,我们只要定好周期和通道输出脉冲值就可以做到在主定时器的固定数量脉冲实现翻转,而该IO的翻转由定时器硬件实现,速度非常快。
举例说明:我们将从定时器的预分频值设为0,不进行时钟分频,将周期设置为160,周期设为80,这样就得到80个主定时器的脉冲从定时器进行翻转。然后在3号波形的下降沿产生中断,在中断中将从定时器失能,这样就只会产生一次翻转。由于中断的延时性,可实现在从定时器的输出变为高电平后才被失能。
从定时器代码实现如下:
void timer2_config(void)
{
timer_oc_parameter_struct timer_ocinitpara;
timer_parameter_struct timer_initpara;
/* TIMER2 configuration */
timer_deinit(TIMER2);
/* initialize TIMER init parameter struct */
timer_struct_para_init(&timer_initpara);
timer_initpara.prescaler = 0;
timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
timer_initpara.counterdirection = TIMER_COUNTER_UP;
timer_initpara.period = 159;
timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
timer_initpara.repetitioncounter = 0;
timer_init(TIMER2, &timer_initpara);
/* initialize TIMER channel output parameter struct */
timer_channel_output_struct_para_init(&timer_ocinitpara);
/* configure TIMER channel output function */
timer_ocinitpara.outputstate = TIMER_CCX_ENABLE;
timer_ocinitpara.outputnstate = TIMER_CCXN_DISABLE;
timer_ocinitpara.ocpolarity = TIMER_OC_POLARITY_LOW;
timer_ocinitpara.ocnpolarity = TIMER_OCN_POLARITY_HIGH;
timer_ocinitpara.ocidlestate = TIMER_OC_IDLE_STATE_HIGH;
timer_ocinitpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;
timer_channel_output_config(TIMER2, TIMER_CH_0, &timer_ocinitpara);
/* configure TIMER channel output pulse value */
timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_0, 80);
/* CH0 configuration in PWM mode 0 */
timer_channel_output_mode_config(TIMER2, TIMER_CH_0, TIMER_OC_MODE_PWM0);
/* configure TIMER channel output shadow function */
timer_channel_output_shadow_config(TIMER2, TIMER_CH_0, TIMER_OC_SHADOW_DISABLE);
/* auto-reload preload enable */
timer_auto_reload_shadow_enable(TIMER2);
/* slave mode selection: external clock mode 0 */
timer_slave_mode_select(TIMER2, TIMER_SLAVE_MODE_EXTERNAL0);
/* select TIMER input trigger source: internal trigger 2(ITI2) */
timer_input_trigger_source_select(TIMER2, TIMER_SMCFG_TRGSEL_ITI2);
/* select the master slave mode */
timer_master_slave_mode_config(TIMER2, TIMER_MASTER_SLAVE_MODE_ENABLE);
/* TIMER2 update event is used as trigger output */
timer_master_output_trigger_source_select(TIMER2, TIMER_TRI_OUT_SRC_UPDATE);
timer_enable(TIMER2);
}
中断函数如下:
void EXTI0_1_IRQHandler(void)
{
if(SET == exti_interrupt_flag_get(EXTI_0)){
EXTI_PD = (uint32_t)EXTI_0;
EXTI_INTEN &= ~(uint32_t)EXTI_0;/*失能外部中断*/
TXFLAG = 1;
timer_disable(TIMER2);
}
}
实现效果如下:
至此该问题得到解决,在做完该步骤之后使用SPI读取4个字节传感器数据也可以正确读取。然后会议所需要的数据已经完成。但是会议中提到在此基础上要增加SPI的读取数据量,将4字节扩充到12个字节,于是再次出现问题。
(此处只介绍自己用的方法,关于定时器的更多用法,留待后续出一篇帖子介绍)
问题二:如何在有限的时间内读取到完整的SPI数据,并通过USART将数据传送出去。
问题分析:先说一下之前的步奏,USART的波特率是2500000,接收1个字节,返回11个字节,可以得出仅仅是返回这11个字节的数据就需要35us左右,再加上接收数据,SPI,PWM等等,我的时间是肯定不够的,所以我采用的是在启动USART的DMA后再去做数据的采集,这样会造成这次USART发送的数据是上一次采集到的值,不过在当前这一点不影响,我们需要的是连续的数据,对单个数据的采集时间可不做要求。
之前SPI只需要读取4个字节,也就是32个CLK,现在增加到12个字节,88个CLK,看起来增加了几倍,但是相对于速度快的SPI来说其实压力不大,但是我在实际操作过程中发现当数据收发量增加后,之前的代码变得无效了,SCN总线会提前拉高,导致数据收发不完整,代码及现象如下所示:
void DMA_SPI_Write(void)
{
GPIO_BC (GPIOA) = (uint32_t)GPIO_PIN_4;
SPI_CTL1(SPI0) |= (uint32_t)SPI_CTL1_DMATEN; /*SPI DMA发送使能*/
SPI_CTL1(SPI0) |= (uint32_t)SPI_CTL1_DMAREN;/*SPI DMA接收使能*/
DMA_CHCTL(DMA_CH2) &= ~DMA_CHXCTL_CHEN; /*失能DMA通道2*/
DMA_CHCNT(DMA_CH2) = ARRAYSIZE ; /*传输长度*/
DMA_CHCTL(DMA_CH2) |= DMA_CHXCTL_CHEN; /*使能DMA通道2*/
dma_channel_disable(DMA_CH1); /*失能DMA通道2*///此处不能使用寄存器,会造成失能失败
DMA_CHCNT(DMA_CH1) = ARRAYSIZE ; /*传输长度*/
dma_channel_enable(DMA_CH1); /*使能DMA通道2*/
while(RESET == dma_flag_get(DMA_CH2,DMA_FLAG_FTF));
while(RESET == dma_flag_get(DMA_CH1,DMA_FLAG_FTF));
// while(RESET == dma_flag_get(DMA_CH2,DMA_FLAG_FTF));
GPIO_BOP(GPIOA) = (uint32_t)GPIO_PIN_4;
}
因为我需要速度较高的环境,所以部分代码采用寄存器方式,在启动DMA之后等待DMA传输通道完整标志复位,但是会变成如下情况:
个人猜测是DMA将数据搬运到SPI的数据缓冲区,完成后标志位复位,但是由于SPI的传输速度不及DMA的搬运速度,导致虽然DMA通道完成,但是SPI的数据传输并未完成。
于是我将检测改为SPI发送完成标志位:
此时情况变成偶然现象,大部分都是没问题的,但是偶尔会有出错。
解决方案:
最后经过查阅资料和多次测试,采用如下方式进行通信检测。测试无任何问题:
void SPI_DMA_WriteReadByte(void)
{
GPIO_BC (GPIOA) = (uint32_t)GPIO_PIN_4;
SPI_CTL1(SPI0) |= (uint32_t)SPI_CTL1_DMATEN; /*SPI DMA发送使能*/
SPI_CTL1(SPI0) |= (uint32_t)SPI_CTL1_DMAREN;/*SPI DMA接收使能*/
DMA_CHCTL(DMA_CH2) &= ~DMA_CHXCTL_CHEN; /*失能DMA通道2*/
DMA_CHCNT(DMA_CH2) = ARRAYSIZE ; /*传输长度*/
DMA_CHCTL(DMA_CH2) |= DMA_CHXCTL_CHEN; /*使能DMA通道2*/
while(RESET == spi_i2s_flag_get(SPI0, SPI_FLAG_TBE));
dma_channel_disable(DMA_CH1); /*失能DMA通道2*///此处不能使用寄存器,会造成失能失败
DMA_CHCNT(DMA_CH1) = ARRAYSIZE ; /*传输长度*/
dma_channel_enable(DMA_CH1); /*使能DMA通道2*/
while(RESET == spi_i2s_flag_get(SPI0, SPI_FLAG_TBE));
GPIO_BOP(GPIOA) = (uint32_t)GPIO_PIN_4;
}
至此,SPI的问题也已解决,但是由于需要的数据量大,为了省事,我决定在工程中定义5个数组变量,每个变量的大小是1000,用来储存数据,然后在debug模式下导出即可,结果工程直接报错,原因是变量太大,超出MCU的RAM空间了。而且后来得知需要3000左右的数据量,这个方法直接废弃。于是只能将数据从串口输出到串口助手,从串口助手复制出去然后用EXCEL处理了。虽然从debug 模式下无法成功,这里也介绍一下如何导出debug时的数组数据。
问题三:KeiL 调试时保存watchwindow的参数变量到文件
1.假设需要导出的数组为uint8_t spi0_receive_array[12]
2.在工程目录下创建一个.ini文件,文件内容为:
FUNC void displayvalues(void) {
int idx;
exec("log > MyValues.log");
for (idx = 0; idx < 12; idx++) {
printf ("%d\n",spi0_receive_array[idx]);
}
exec("log off");
}
在for循环中,idx<12为你需要导出的数据量,将spi0_receive_array换成自己的数组名。
3.进入debug,运行程序,完成数组赋值,暂停debug.
4.进入function editor
选择之前创建的文件:
此时进入编辑框,可在此修改代码:
点击Compile
在命令行输入displayvalues(),回车
如果一切正常,可在工程下出现名为MyValues.log的文件。
2022开年第一帖完成,预祝所有大佬事业顺利,21ic越来越强,另外,希望大家一起努力,多多写原创,把跑堂的钱包干光,把论坛做强,砖落地,等玉响。
|
真有才