GD32E230 FMC保存后出现奇怪的ADC混乱及处理方法

    最近在使用兆易创新的GD32E230C8T6的FMC时(即Flash读写),出现一个非常奇怪的异常现像。本身这个MCU的Flash还算比较大,64K的空间,而程序代码空间使用后还有剩余,就想把配置信息保存到Flash中,这样可以省掉一个外部存储的费用,毕竟大部分的配置其实并不多,可能就只有个几十字节,如果额外添置一个外置Flash,真的是有些没必要。

    但在使用中出现一些奇怪的问题,就是项目中使用到了ADC,在使用Flash写入后大概率会出现ADC采样混乱,就是采样不到正确的数值。就非常奇怪,又无耐,总不能非要添加一个外置吧!

    现像说完了,下面说下我所使用的软硬件情况:

系统    Win11
软件    Keil5.36
MCU    GD32E230C8T6 (淘宝老五家的二手拆机)

    大家看到了,我使用的是二手的MCU,以下是实物图片:

    当时买当然为图便宜,因为当时淘了几十块,所以暂时是够用了,也就没有买新的必要,所以我以上所描述的问题,不知道新的芯片是不是存在。

    本文中会涉及到以下几个内容:

1、使用DMA持续采样ADC
2、内置Flash读取及写入

    哪么,我们就先来实现以上功能,我们将使用DMA来持续采样ADC,这样速度很快,而且不占用MCU时间,非常的省时省力。我这里需要采样三路ADC信息,分别是输入电压、输出电压和温度信息,使用的引脚对应信息如下:

PA1 --- 输入电压 --- 通道 ADC_CH1
PA2 --- 输出电压 --- 通道 ADC_CH2
PA3 --- 温度信息 --- 通道 ADC_CH3

为了保障采样精度、降低波动,我做了16倍采样后取平均值,所以我们申明了两个参数,一个是用于DMA采样后的数据存储,一个是存储取平均值后的值:

// ADC原始数据缓存区 4通道16倍采样
__IO uint16_t ADCRawValue[16][3];
// ADC实际值
__IO uint16_t ADCValue[3];

ADC 初始化完整代码如下:

// ADC初始化例程,使用DMA转换

#include "bsp_adc.h"

// ADC原始数据缓存区 4通道16倍采样
__IO uint16_t ADCRawValue[16][3];
// ADC实际值
__IO uint16_t ADCValue[3];


// DMA进行ADC转换配置 4通道采样
void adc_config(void)
{
	// 配置GPIO
	rcu_periph_clock_enable(RCU_GPIOA);
	
	gpio_mode_set(GPIOA, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, VBAT_PIN | VBUS_PIN | TEMP_PIN );
	
	
	// 配置DMA中断
	nvic_irq_enable(DMA_Channel0_IRQn, 1);
	
	// 配置DMA
	rcu_periph_clock_enable(RCU_DMA);
	dma_deinit(DMA_CH0);
	
	dma_parameter_struct dma_data_parameter;
	dma_data_parameter.periph_addr  = (uint32_t)(&ADC_RDATA);
	dma_data_parameter.periph_inc   = DMA_PERIPH_INCREASE_DISABLE;
	dma_data_parameter.memory_addr  = (uint32_t)(&ADCRawValue);
	dma_data_parameter.memory_inc   = DMA_MEMORY_INCREASE_ENABLE;
	dma_data_parameter.periph_width = DMA_PERIPHERAL_WIDTH_16BIT;
	dma_data_parameter.memory_width = DMA_MEMORY_WIDTH_16BIT;  
	dma_data_parameter.direction    = DMA_PERIPHERAL_TO_MEMORY;
	dma_data_parameter.number       = 48U;
	dma_data_parameter.priority     = DMA_PRIORITY_HIGH;
	dma_init(DMA_CH0, &dma_data_parameter);
	// 开启连续采样
	dma_circulation_enable(DMA_CH0);
  
	dma_interrupt_enable(DMA_CH0,DMA_INT_FTF);
	/* enable DMA channel */
	dma_channel_enable(DMA_CH0);
	
	
	// 配置ADC
	rcu_periph_clock_enable(RCU_ADC);
	/* config ADC clock */
	rcu_adc_clock_config(RCU_ADCCK_APB2_DIV6);
	
	adc_deinit();
	// 开启连续转换
	adc_special_function_config(ADC_CONTINUOUS_MODE, ENABLE);		
	/* ADC scan function enable */
	adc_special_function_config(ADC_SCAN_MODE, ENABLE);
	// 数据对齐模式,低位对齐
	adc_data_alignment_config(ADC_DATAALIGN_RIGHT);		
	// 设置采样通道数
	adc_channel_length_config(ADC_REGULAR_CHANNEL, 3U);
	// 设置采样分辨率 12Bit
	adc_resolution_config(ADC_RESOLUTION_12B);

	// 配置采样周期
	adc_regular_channel_config(0U, ADC_CHANNEL_1, ADC_SAMPLETIME_239POINT5);
	adc_regular_channel_config(1U, ADC_CHANNEL_2, ADC_SAMPLETIME_239POINT5);
	adc_regular_channel_config(2U, ADC_CHANNEL_3, ADC_SAMPLETIME_239POINT5);
	
	// 触发源配置
	adc_external_trigger_source_config(ADC_REGULAR_CHANNEL, ADC_EXTTRIG_REGULAR_NONE); 
	adc_external_trigger_config(ADC_REGULAR_CHANNEL, ENABLE);

	
	/* enable ADC interface */
	adc_enable();
	delay_1ms(1U);
	// 开启自检准
	adc_calibration_enable();

	// 开启DMA
	adc_dma_mode_enable();
	// 启用软件触发
	adc_software_trigger_enable(ADC_REGULAR_CHANNEL);
	
}

uint32_t lastSwitchTime=0;
void DMA_Channel0_IRQHandler(void)
{
	if(dma_interrupt_flag_get(DMA_CH0,DMA_INT_FLAG_FTF) != RESET)
	{
		uint32_t raw[3]={0};
		for(uint8_t i=0;i<16;i++)
		{
			raw[0] += ADCRawValue[i][0];
			raw[1] += ADCRawValue[i][1];
			raw[2] += ADCRawValue[i][2];
		}

		for(uint8_t i=0;i<3;i++)
		{
			ADCValue[i] = raw[i]>>4;
		}
		
		// 清除转换完成中断标志
		dma_interrupt_flag_clear(DMA_CH0,DMA_INT_FLAG_FTF);
	}
}

    为了方便测试,我们还要打开串口,这样可以实时打印信息,串口初始化部分就不详细说了,后面我会附上完整项目代码,可以自行查看。

  在main.c 中打印信息:


#include "main.h"
#include "bsp_usart.h"
#include "bsp_adc.h"
#include "bsp_key.h"

ConfigStrc config;

int main(void)
{
	// 滴嗒定时器配置
	systick_config();
	// 串口初始化
	BSP_USART_Init(115200);
	// ADC 配置
	adc_config();

	printf("Init Success\n");
	
	
	while(1)
	{
		// 每隔500ms打印一次信息
		delay_1ms(500);
		printf("ADC1=%d ADC2=%d ADC3=%d\n",ADCValue[0],ADCValue[1],ADCValue[2]);
	}
}

   我的打印信息如下:

   目前这些信息都是比较正常的,为了测试方便,我们再添加一个按键触发来实现配置的保存,这里使用的是中断方式来实现的。

按键初始化代码

// 使用外部中断方式进行按键触发

#include "bsp_key.h"

uint32_t key_press_time = 0;

// 按键初始化
void Key_Init(void)
{
	// 开启时钟
	rcu_periph_clock_enable(BTN_RCU);
	/* enable the CFGCMP clock */
	rcu_periph_clock_enable(RCU_CFGCMP);
	
	// EC11引脚初始化
	gpio_mode_set(BTN_PORT,GPIO_MODE_INPUT,GPIO_PUPD_NONE,BTN_PIN );
	gpio_output_options_set(BTN_PORT,GPIO_OTYPE_PP,GPIO_OSPEED_50MHZ,BTN_PIN );
	
	
	// 开启中断
	nvic_irq_enable(EXTI0_1_IRQn, 0U);

	// 配置中断线
	syscfg_exti_line_config(BTN_EXIT_PORT, BTN_LINE);

	// 配置按键触发模式
	exti_init(BTN_EXIT, EXTI_INTERRUPT, EXTI_TRIG_BOTH);
	
}


void EXTI0_1_IRQHandler(void)
{
	if(RESET != exti_interrupt_flag_get(BTN_EXIT)) {
		exti_interrupt_flag_clear(BTN_EXIT);
		if(realConsumeTime(key_press_time) <= 50)return;
		
		if(gpio_input_bit_get(BTN_PORT,BTN_PIN) != RESET)
		{
			printf("按键按下了,保存配置\n");
			// 松开按键时保存配置到Flash中
			saveConfig();
			
			key_press_time = CounterTime;
		}else{
			key_press_time = CounterTime;
		}
	}
	
}

毫秒计数器初始化代码:

   使用毫秒定时器来防止按键连续触发,所以还要再开启一个基础定时器来做毫秒计数。 

// 使用 TIM5基础定时器来做毫秒计数器

#include "counter_timer.h"

uint32_t CounterTime = 0;


// 使用基本定时器用于毫秒计数器
void Counter_Timer_Init(void)
{
	rcu_periph_clock_enable(COUNTER_RCU);
	
	timer_deinit(COUNTER_TIMER);
	
	// 定时器初始化
	timer_parameter_struct timer_struct;
	timer_struct_para_init(&timer_struct);
	
	// APB1 最高主频为72MHz, 所以这里预分频设置为72-1,相当于为1us,然后将计数设置为1000-1,即可得到1ms定时
	timer_struct.prescaler = 71;
	timer_struct.alignedmode = TIMER_COUNTER_EDGE;
	timer_struct.counterdirection = TIMER_COUNTER_UP;
	timer_struct.clockdivision = TIMER_CKDIV_DIV1;
	timer_struct.period = 999;
	timer_struct.repetitioncounter = 0;
	
	timer_init(COUNTER_TIMER,&timer_struct);
	
  nvic_irq_enable(TIMER5_IRQn, 0);
	
	// 配置中断
	timer_interrupt_enable(COUNTER_TIMER,TIMER_INT_UP);
	timer_enable(COUNTER_TIMER);
	timer_interrupt_flag_clear(COUNTER_TIMER,TIMER_INT_FLAG_UP);
	
}

uint32_t realConsumeTime(uint32_t lastTime)
{
	if(CounterTime>=lastTime){return CounterTime-lastTime;}
	else {return 0xFFFF-lastTime+CounterTime;}
}

void TIMER5_IRQHandler(void)
{
	if(timer_interrupt_flag_get(COUNTER_TIMER,TIMER_INT_FLAG_UP) == SET ){
		timer_interrupt_flag_clear(COUNTER_TIMER,TIMER_INT_FLAG_UP);
		CounterTime++;
		
	}
}

FMC初始化代码:

    GD32E230每页的大小是1K,这样我们就使用最后一页来存储配置数据,我们定义一下页的大小和起始位置:

// 页大小 1K
#define FMC_PAGE_SIZE		0x400
// 读写开始位置
#define FMC_START_ADDR	0x8010000 - FMC_PAGE_SIZE
#include "bsp_fmc.h"

// 页擦除
void fmc_erase_pages(void)
{
	uint32_t erase_counter;

	/* unlock the flash program/erase controller */
	fmc_unlock();

	/* clear all pending flags */
	fmc_flag_clear(FMC_FLAG_END | FMC_FLAG_WPERR | FMC_FLAG_PGERR);
	
	/* erase the flash pages */
	for(erase_counter = 0U; erase_counter < FMC_PAGE_SIZE; erase_counter++){
			fmc_page_erase(FMC_START_ADDR + (FMC_PAGE_SIZE * erase_counter));
			fmc_flag_clear(FMC_FLAG_END | FMC_FLAG_WPERR | FMC_FLAG_PGERR);
	}

	/* lock the main FMC after the erase operation */
	fmc_lock();
}
// 读flash数据
uint8_t fmc_read_buffer(uint16_t offset,uint8_t *buffer,uint16_t len)
{
	uint32_t rdAddr = FMC_START_ADDR + offset;

	for( uint16_t dataIndex = 0; dataIndex < len; dataIndex++ )
	{
		buffer[dataIndex] = *( __IO uint8_t* ) rdAddr;
		rdAddr += 1;//地址累加
	}
	return 0;
}
// 写flash数据
uint8_t fmc_write_buffer(uint16_t offset,uint8_t *buffer,uint16_t len)
{
	// 因为FMC是需要按4字节写入,所以这里需要做个转换,将uint_t8转换成uint_t32
	// 计算转换成uint32后长度
	uint16_t size = ceil(len/4.0f);
	uint32_t *data = malloc(size*4);
	memset(data,0,size*4);
	
	// uint8_t*转uint32_t*
	for(uint16_t i=0;i<size;i++)
	{
		data[i] = buffer[i*4+0] + (buffer[i*4+1]<<8) + (buffer[i*4+2]<<16) + (buffer[i*4+3]<<24);
	}
	
	fmc_state_enum FLASHStatus;
	uint16_t i;    
	uint32_t AddressTemp = 0;

	/*写入数据*/
	AddressTemp = FMC_START_ADDR + offset;
	/* 解锁 */
	fmc_unlock();
	
	/* clear all pending flags */
	fmc_flag_clear(FMC_FLAG_END | FMC_FLAG_WPERR | FMC_FLAG_PGERR);
	/* erase the flash pages */
	fmc_page_erase(FMC_START_ADDR);
	
	/* 操作FMC前先清空STAT 状态寄存器,非常必要*/
	fmc_flag_clear(FMC_FLAG_END | FMC_FLAG_WPERR | FMC_FLAG_PGERR);
	// 开始写入
	for (i=0; i<size; i++)
	{
		FLASHStatus = fmc_word_program(AddressTemp, data[i]);
		AddressTemp += 4U;
		fmc_flag_clear(FMC_FLAG_END | FMC_FLAG_WPERR | FMC_FLAG_PGERR);
	}
	/* 上锁 */
	fmc_lock();
	
	free(data);
	
	return 0;

}

// 加载配置
void loadConfig(void)
{
	uint16_t hLen = sizeof(ConfigStrc);
	uint8_t buffer[sizeof(ConfigStrc)+1]={0};
	fmc_read_buffer(0,buffer,hLen);
	
	ConfigStrc cfg;
	memcpy(&cfg,buffer,hLen);
	
	if(strcmp(VERSION,cfg.version) != 0)
	{
		// 初始化配置参数
		memcpy(config.version,VERSION,4);
		// 默认输出电压为 单位:mV 默认为5V
		config.volt = 5000;
		
		saveConfig();
		printf("首次加载初始化配置\n");
	}else{
		printf("读取的配置信息: version=%s volt=%d  \n",config.version,config.volt);
		
		memcpy(&config,&cfg,hLen);
	}
	// 开关默认为关
	config.work_switch = 0;
	
}

// 保存配置数据
void saveConfig(void)
{
	
	uint8_t buffer[sizeof(ConfigStrc)+1]={0};
	memcpy(buffer,&config,sizeof(ConfigStrc));
	// 先擦除,再写入
	fmc_erase_pages();
	fmc_write_buffer(0,buffer,sizeof(ConfigStrc));
	
	printf("保存配置\n");
		
}

现在我们把相关的初始化代码都放到main函数中


#include "main.h"
#include "bsp_usart.h"
#include "bsp_adc.h"
#include "bsp_key.h"

ConfigStrc config;

int main(void)
{
	// 滴嗒定时器配置
	systick_config();
	// 毫秒计数器初始化
	Counter_Timer_Init();
	// 串口初始化
	BSP_USART_Init(115200);
	// ADC 配置
	adc_config();
	// 初始化按键
	Key_Init();
	

	printf("System initialization completed.\n");
	
	
	while(1)
	{
		// 每隔500ms打印一次信息
		delay_1ms(500);
		printf("ADC1=%d ADC2=%d ADC3=%d\n",ADCValue[0],ADCValue[1],ADCValue[2]);
	}
}

好的,完成了,正常进入时打印数据是正常的,当我们按下按键后,会保存数据,之后读取的ADC值就变了,我们来看下打印信息:

   图中的已经用红字标识出了保存之前和保存之后的数据,此时环境未发生任何变化,但发现ADC读取到的值全变了。发生了什么情况?整个人都蒙了,从代码中我们也可以看出,全部都是使用官方提供的标准库操作的,难道写Flash会改变ADC的寄存器值???

    具体原因就不明白了,既然出现了问题,哪我们就来解决问题就是了。最后打印的结果看上去好像有点熟悉的味道,嗯,看起来好像是顺序变了,似乎是后移了一位,既然是保存后才出现的问题,我们就简单点来处理吧。就是在添加一个保存标志,检查到这个标志后,我们重新初始化一下ADC不就完事了嘛,至于这个问题产生的原因,我是没搞明白,不好意思了。

以下实现的代码,仅取需要修改部分的了:

//bsp_fmc.c

// FMC保存状态
uint8_t fmc_save_status = 0;



// 保存配置数据
void saveConfig(void)
{
	
	// 开启DMA
	adc_dma_mode_disable();
	
	uint8_t buffer[sizeof(ConfigStrc)+1]={0};
	memcpy(buffer,&config,sizeof(ConfigStrc));
	// 先擦除,再写入
	fmc_erase_pages();
	fmc_write_buffer(0,buffer,sizeof(ConfigStrc));
	
	printf("保存配置\n");
    // 保存后将标志位置 1
	fmc_save_status = 1;
}
// bsp_adc.c


void DMA_Channel0_IRQHandler(void)
{
	if(dma_interrupt_flag_get(DMA_CH0,DMA_INT_FLAG_FTF) != RESET)
	{
		uint32_t raw[3]={0};
		for(uint8_t i=0;i<16;i++)
		{
			raw[0] += ADCRawValue[i][0];
			raw[1] += ADCRawValue[i][1];
			raw[2] += ADCRawValue[i][2];
		}

		for(uint8_t i=0;i<3;i++)
		{
			ADCValue[i] = raw[i]>>4;
		}
		// 保存数据后在DMA中断里将adc重新初始化
		if(fmc_save_status)
		{
			fmc_save_status = 0;
			adc_config();
		}

		dma_interrupt_flag_clear(DMA_CH0,DMA_INT_FLAG_FTF);
	}
}

    问题是解决了,不过也多出一段采样的空白时间,如果对ADC采样要求非常严的项目可能会有问题,但我的这个要求没这么高,所以也算是投机取巧了吧! 

结束语:

    虽然没搞明白为什么出现这个问题,但还是可以解决问题,文末也附上完整示例工程,如果有积分还请下载CSDN上的,我也很缺积分,感谢大家了。好了,希望对本文对大家有用。

下载地址: 

https://download.csdn.net/download/weixin_57604547/87789563https://download.csdn.net/download/weixin_57604547/87789563

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值