最近在使用兆易创新的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上的,我也很缺积分,感谢大家了。好了,希望对本文对大家有用。