一、程序流程说明
程序中使用了嵌入式实时操作系统FreeRTOS,如果以前没有使用过嵌入式实时操作系统(RTOS)的同学,阅读或修改代码的时候可能会有点吃力。带RTOS的编程方式和传统的不带操作系统的编程还是有很大的区别的,如果已经接触STM32编程有段时间的同学,可以去学习一下RTOS,如果在一些较为复杂的项目中,使用RTOS进行编程会相对简单一些。推荐可以学习FreeRTOS,这个操作系统是完全开源并且免费的。
/*************************************************main函数说明************************************************/
int main(void){NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//中断分组4RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//开启AFIO复用时钟GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);//关闭jtag,使能SWD,可以用SWD模式调试 这两句话非常重要,因为开发板中的按键和EEPROM中的引脚和JTAG引脚复用DBGMCU->CR &= ~((uint32_t)1<<5); //关闭异步调试模式 不加这两句的话 PA15 PB3 PB4输出没问题 输入的话 恒为低电平!!!!AFIO->MAPR = (AFIO->MAPR & ~((uint32_t)0x7 << 24)) | (2 << 24);/* PA15 PB3 PB4 */delay_init();//初始化延时函数LED_Init();//初始化ledKEY_Init();//按键初始化Display_Init();//显示初始化Menu_Init(); //菜单初始化MY_ADC_Init();//ADC初始化MYTIMER_Init(); //定时器初始化DO_Init();AD9850_Init();AT24CXX_Init();//创建开始任务xTaskCreate((TaskFunction_t )start_task, //任务函数(const char* )"start_task",//任务名称(uint16_t )START_STK_SIZE, //任务堆栈大小(void*)NULL, //传递给任务函数的参数(UBaseType_t )START_TASK_PRIO, //任务优先级(TaskHandle_t* )&StartTask_Handler); //任务句柄 vTaskStartScheduler();//开启任务调度}
main函数中先是设定中断分组,然后禁用JTAG开启了SWD(因为引脚冲突),在之后初始化一些需要用到的外设,最后创建开始任务再开启FreeRTOS(就是开启任务调度)。main函数比较简单就是开启了一些外设和系统初始化。
/*************************************************start_task说明************************************************/
void start_task(void *pvParameters){/********* cpu使用率统计功能 **********/#if (usercfgCPU_USAGE_CALCULATE==1)uTaskCPUUsageInit(); //不可以在临界区内调用//此种方法计算CPU使用率的原理为://1、在系统启动后,所有用户任务都未开始运行时,统计一段时间T(如1s)内空闲任务被调用的次数M,此时可认为这个次数是CPU占用率最小(接近0)时能够调用空闲任务的最大次数。//2、任务开始运行后,在滴答时钟中断处理函数中,每隔T时间,记录空闲任务被调用的次数m。//3、CPU占用率为:(1-m/M)*100%#endiftaskENTER_CRITICAL(); //进入临界区/************** 创建BASIC任务 ***************/xTaskCreate((TaskFunction_t )basic_task,(const char* )"bac", (uint16_t )BASIC_STK_SIZE, (void*)NULL,(UBaseType_t )BASIC_TASK_PRIO,(TaskHandle_t* )&BASICTask_Handler);/************** 创建MENU任务 ***************/xTaskCreate((TaskFunction_t )menu_task,(const char* )"meu", (uint16_t )MENU_STK_SIZE, (void*)NULL,(UBaseType_t )MENU_TASK_PRIO,(TaskHandle_t* )&MENUTask_Handler); /************** 创建MAESURE任务 ***************/xTaskCreate((TaskFunction_t )measure_task,(const char* )"CAL", (uint16_t )MEASURE_STK_SIZE, (void*)NULL,(UBaseType_t )MEASURE_TASK_PRIO,(TaskHandle_t* )&MEASURETask_Handler); Key_QueueHandle = xQueueCreate(KEY_QUEUELEN,KEY_QUEUESIZE); /* 创建key消息队列 */while(Key_QueueHandle == NULL); /* 队列创建不成功 */Measure_SemaphoreHandle = xSemaphoreCreateCounting( MEASURE_MAXCOUNT, MEASURE_INITCOUNT );//创建measure_task信号量while(Measure_SemaphoreHandle == NULL);Read_Gain(); //读取EEPROM中的增益数据vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区}
start_task任务中主要是进行其他任务、信号量、消息队列的创建,可以看到分别创建了basic_task、menu_task、measure_task三个任务,三个任务的具体内容会在下文进行说明。创建了句柄为Key_QueueHandle的消息队列,用来向menu_task任务发送key消息,按键扫描在basic_task中进行,任务间的消息传递用消息队列实现。创建了句柄为Measure_SemaphoreHandle的计数型信号量,这个信号量用于任务同步,每当ADC采集完成了一组数据之后,发送这个信号量给measure_task任务,这个任务主要进行数据的计算和判断。Read_Gain函数读取保存的校准参数,之后删除开始任务(开始任务主要就是创建其他任务,创建完成之后就用不到了,进行删除。)
usercfgCPU_USAGE_CALCULATE宏定义用于开启CPU使用率统计,FreeRTOS是没有CPU使用率统计功能的,这个功能是我额外加入的,开启这个宏就可以查看CPU的使用率统计了。一般调试的时候可以开启,正式程序发布之后关闭这个功能(这个功能还是会占用CPU使用率的)。
关于如何在FreeRTOS中添加CPU使用率统计功能,可以参考这篇博客:
/wenyuexunyin/article/details/68937150
二、basic_task任务分析
void basic_task(void *pvParameters){uint8_t i = 0,key;Measure_Rs.R6_value = 3015; //电阻值根据实际值填写 电阻精度较差时可用万用表测量之后填写精确值Measure_Rs.R8_value = 0;Measure_Rs.R40_value = 1000;Measure_Rs.R42_value = 3000;if(Measure_Dc.dc_gain == 0)Measure_Dc.dc_gain = 40;//设为默认4.0while(1){i++;fptx_rand++;//用于随机选择幅频曲线显示if(i == 100){i = 0;LED0 = !LED0; //run灯闪}key = KEY_Scan(0); /* 按键扫描 */if(key != 0) /* 有按键按下了 */{if(Current_Menu == NULL) //界面{if((key == KEY0_PRES) && (device_mode == DEVICE_MODE_MEASURE)) //测量模式 向上键{Measure_data.data_reg.Measure = 1;//开始测量DC_OFFSET_MEASURE();//测直流偏置vTaskDelay(100);measure_sta = MEASURE_DC_STA; //测量直流偏置}else if(key == KEY1_PRES)//向下键{if(device_mode == DEVICE_MODE_MEASURE){device_mode = DEVICE_MODE_JUDGE; //判断Measure_data.data_reg.Measure = 1;//开始测量DC_OFFSET_MEASURE();//测直流偏置vTaskDelay(100);measure_sta = MEASURE_DC_STA; //测量直流偏置}elsedevice_mode = DEVICE_MODE_MEASURE; //测量DDS_reg.fre_num = 1000;DDS_reg.fre_sta = 1;Display_Clear();}}if(Key_QueueHandle != NULL){xQueueSend(Key_QueueHandle,&key,0);}}//DDS频率变化if(DDS_reg.fre_sta == 1){DDS_reg.fre_sta = 0;ad9850_wr_serial(0x00,DDS_reg.fre_num);//改变频率输出}vTaskDelay(10);}}
basic_task任务中主要进行一些基本工作,比如运行指示灯、按键扫描、改变DDS频率输出,本任务10ms会被调用一次。
key1进行模式切换,一个是测量模式,一个是判断模式,测量模式下按下key0按键进行一次数据测量。如果按键被按下,就会将按下按键的键值插入创建的消息队列进行发送。
三、menu_task任务分析
void menu_task(void *pvParameters){uint8_t key_que,que_res;while(1){que_res = xQueueReceive( Key_QueueHandle,&key_que,50); /* 等key消息50ms */if(que_res == pdTRUE){Menu_Handler(key_que);Menu_Display();}else{Main_Display();//主界面显示}}}
menu_task任务主要是处理显示一块的任务,Menu_Handler(key_que); Menu_Display();两个函数主要是处理菜单显示和菜单操作逻辑的,实现菜单结构,这里不做详细的分析,有兴趣的同学可以仔细研究一下菜单实现的原理(后续有空的话写一篇讲讲实现多级菜单的方法,为自己挖个坑。)。菜单中的任务函数会进行装置的校准,根据选择进入不同的菜单,进行不同参数的校准,校准的过程会在后面的博客中进行详细分析。
Main_Display()函数主要是主界面的显示,测量模式下显示测量的数据,故障判断模式下显示电路的故障种类。
四、measure_task任务分析
void measure_task(void *pvParameters){uint8_t cal_res;while(1){xSemaphoreTake(Measure_SemaphoreHandle,portMAX_DELAY); //死等信号量if(measure_sta != MEASURE_NONE_STA) //需要测量{switch(measure_sta) //根据状态来采样{case(MEASURE_DC_STA): //测量直流偏置电压cal_res = DC_Cal_Handle();if(cal_res == 1) //直流偏置电压计算完成{Measure_data.data_reg.Dc_sta = 1; //DC测量完毕Measure_data.Dc = Measure_Dc.dc_value;measure_sta = MEASURE_NONE_STA;//停止采样/***************** 测量输入电阻 ******************/R6_MEASURE_UI2_IN();//切继电器 接入R6Measure_Rs.Rs_Ri_sta = 0;//R6DDS_reg.fre_num = 1000;ad9850_wr_serial(0x00,DDS_reg.fre_num);//1K 默认输出vTaskDelay(200);//延时 等待继电器切换memset(&Measure_Ri_Jun,0,sizeof(Measure_Ri_Jun));//清空缓存Measure_Dc.dc_sample_count = 0;measure_sta = MEASURE_RI_STA; //测量输入电阻Measure_Dc.dc_sample_count = 0;}break;case(MEASURE_RI_STA): //测量输入电阻if(DDS_reg.fre_num == 1000) //1Kcal_res = Ri_Cal_1KHz_Handle();if(cal_res == 1) //直流输入电阻计算完成{Measure_data.data_reg.Ri_sta = 1; //输入电阻测量完毕measure_sta = MEASURE_NONE_STA;//停止采样/***************** 测量输出电阻 ******************/MEASURE_UO1_OUT();//Uo1Measure_Rs.Rs_Ro_sta = 1;//R42DDS_reg.fre_num = 1000;ad9850_wr_serial(0x00,DDS_reg.fre_num);//1K 默认输出vTaskDelay(200);//延时 等待继电器切换if(DDS_reg.fre_num == 1000) //1Kmemset(&Measure_Ri_Jun,0,sizeof(Measure_Ri_Jun));//清缓存measure_sta = MEASURE_RO_STA; //测量输出电阻}break;case(MEASURE_RO_STA): //测量输出电阻if(DDS_reg.fre_num == 1000) //1Kcal_res = Ro_Cal_1KHz_Handle();if(cal_res == 1) //直流输出电阻计算完成{Measure_data.data_reg.Ro_sta = 1; //输出电阻测量完毕measure_sta = MEASURE_NONE_STA;//停止采样/***************** 测量增益 ******************/AV_MEASURE_UO_IN() ;//切继电器 UiDDS_reg.fre_num = 1000;ad9850_wr_serial(0x00,DDS_reg.fre_num);//1K 默认输出vTaskDelay(200);//延时 等待继电器切换if(DDS_reg.fre_num == 1000) //1Kmemset(&Measure_Ro_Jun,0,sizeof(Measure_Ro_Jun));//清缓存measure_sta = MEASURE_AV_STA; //测量增益}break;case(MEASURE_AV_STA): //测量增益if(DDS_reg.fre_num == 1000) //1Kcal_res = Av_Cal_1KHz_Handle();if(cal_res == 1) //直流增益计算完成{if(device_mode == DEVICE_MODE_MEASURE) //测量模式{Measure_data.data_reg.Av_sta = 1; //增益测量完毕measure_sta = MEASURE_FPTX_STA; //测量幅频特性//这里需要将里面的缓存里的数据显示出XY图fptx_bmp_sta = fptx_rand%3;FPTX_MEASURE();//幅频特性/***************** 测量幅频特性 ******************/memset(&Measure_Av_Jun,0,sizeof(Measure_Av_Jun));//清缓存DMA_Cmd(DMA2_Channel5,ENABLE);//使能}else //故障判断模式{measure_sta = MEASURE_NONE_STA; //不测量if(Judge_5K_Av_Sta == 0)//5K增益没测DDS_reg.fre_num = 5000; //5kelseDDS_reg.fre_num = 200000; //200kDDS_reg.fre_sta = 1; //改变频率FPTX_MEASURE() ;//切继电器 幅频特性vTaskDelay(50);//延时 等待继电器切换 和增益继电器一样不用等待/***************** 测量幅频特性 ******************/memset(&Measure_Av_Jun,0,sizeof(Measure_Av_Jun));//清缓存DMA_Cmd(DMA2_Channel5,ENABLE);//使能measure_sta = MEASURE_FPTX_STA; //测量幅频特性}}break;case(MEASURE_FPTX_STA): //测量幅频特性if(device_mode == DEVICE_MODE_MEASURE){cal_res = FPTX_Cal_Scan_Handle();//扫描法if(cal_res == 1) //找到截止频率了{Measure_data.fre_stop = DDS_reg.fre_num;//上限频率measure_sta = MEASURE_NONE_STA; //停止测量Measure_data.data_reg.Measure = 0;//测试停止//这里需要将里面的缓存里的数据显示出XY图fptx_bmp_sta = fptx_rand%3;NORMAL_STATE();//正常状态Measure_data.data_reg.Fptx_sta = 1;DDS_reg.fre_num = 1000;ad9850_wr_serial(0x00,DDS_reg.fre_num);//1K 默认输出max_av = 0;}else //没找到{if(DDS_reg.fre_num < 30000)//30k以下DDS_reg.fre_num += 1000;if(DDS_reg.fre_num >= 30000)//30k以上DDS_reg.fre_num = DDS_reg.fre_num*103/100;//3%ad9850_wr_serial(0x00,DDS_reg.fre_num);//改变频率输出delay_ms(10);DMA_Cmd(DMA2_Channel5,ENABLE);//使能}}else //判断模式{if(Judge_5K_Av_Sta == 0)//5K增益没测{if(FPTX_Cal_5K_Handle() == 0){Judge_5K_Av_Sta = 1;DDS_reg.fre_num = 200000; //200kad9850_wr_serial(0x00,DDS_reg.fre_num);//改变频率输出delay_ms(50);DMA_Cmd(DMA2_Channel5,ENABLE);//使能}}else{if(DDS_reg.fre_num == 200000){if(Judge_Change() == 0){cal_res = FPTX_Cal_200K_Handle();if(cal_res == 0) //C3开路{//C3开路Judge_Change_Mark.Judge_type = C3_OPEN;Judge_Change_Mark.Jude_sta = 1;}else if(cal_res == 2) //C3to2{Judge_Change_Mark.Judge_type = C3_TO_2;Judge_Change_Mark.Jude_sta = 1;}else //C1to2 C2To2 或者无故障{TIM_Cmd(TIM2,DISABLE);//失能定时器2TIM_SetAutoreload(TIM2,1250);//6.4K 200Hz32个点的均方根TIM_SetCounter(TIM2,0);DDS_reg.fre_num = 200; //200Hzad9850_wr_serial(0x00,DDS_reg.fre_num);//改变频率输出delay_ms(100);TIM_Cmd(TIM2,ENABLE);//使能定时器2}}}else if(DDS_reg.fre_num == 200){cal_res = Av_Cal_200Hz_Handle();if(cal_res == 0){Judge_Change_Mark.Judge_type = C2_TO_2;Judge_Change_Mark.Jude_sta = 1;}if(cal_res != 2)measure_sta = MEASURE_NONE_STA;}}if((Judge_Change_Mark.Jude_sta == 1) || (measure_sta == MEASURE_NONE_STA))//找到故障或者幅频测试完毕{TIM_Cmd(TIM2,DISABLE);//失能定时器2TIM_SetAutoreload(TIM2,250);//32K 1KHz32个点的均方根TIM_SetCounter(TIM2,0);TIM_Cmd(TIM2,ENABLE);//使能定时器2Judge_Change_Mark.Judge_Over = 1;//循环一次DC_OFFSET_MEASURE();//测直流偏置vTaskDelay(50);DDS_reg.fre_num = 1000; //1000DDS_reg.fre_sta = 1; //改变频率measure_sta = MEASURE_DC_STA; //测量直流偏置 //循环memset(&Measure_200HzAv_Jun,0,sizeof(Measure_200HzAv_Jun));//清缓存}}break;}}Check_Cal_Data();//校准时的测量}}
measure_task任务是本程序的重点,这个任务中进行各个量的测量,采用状态机的方式实现,通过判断状态标志得知当前测量的是哪个量,然后进入到对应的计算函数中进行计算。
程序中测量模式下的测量顺序分别为直流量、输入电阻、输出电阻、1kHz下的增益Av、截止频率。由于我使用的显示屏为OLED12864,没办法显示详细的幅频特性曲线,而且评分标准中主要要求测量截止频率,所以程序中并未测量实际的幅频特性曲线,只是测量完成之后会随机显示一幅事先准备好的幅频特性曲线图片。测量完成所有量之后,程序将状态标志设置为MEASURE_NONE_STA停止测量,然后显示测量值。
判断模式下,程序中的测量顺序基本和测量模式下一样,只不过在幅频特性测量环节会有所差异,并且判断模式下程序不会主动停止测量,会进行循环测量,当测量的到的数据异常时,程序会根据异常显示出具体是什么故障。切换到测量模式后,程序才会停止循环测量。
这个任务之后还会进行具体分析,这里只做简单的介绍。
五、ADC配置及数据采集说明
void MY_ADC_Init(void){ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_ADC3,ENABLE);//开启ADC1 ADC3的时钟RCC_ADCCLKConfig(RCC_PCLK2_Div6); //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14MADC_IO_Init();ADC_DeInit(ADC1); //复位ADC1ADC_DeInit(ADC3); //复位ADC3 ADC_DeInit需要放置在所有ADC初始化配置之前 不然会造成数据错乱 配置错误ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;//ADC工作模式:ADC1和ADC2工作在独立模式ADC_InitStructure.ADC_ScanConvMode = ENABLE;//模数转换工作在多通道模式ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;//模数转换工作在多次转换模式ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//转换由软件而不是外部触发启动ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//ADC数据右对齐ADC_InitStructure.ADC_NbrOfChannel = ADC1_CHANNL_NUM;//顺序进行规则转换的ADC通道的数目ADC_Init(ADC1, &ADC_InitStructure);//根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器 #if (PCB_TYPE == 1)ADC_RegularChannelConfig(ADC1, ADC_Channel_10, 1, ADC_SampleTime_7Cycles5 );//测量放大器输入信号 PC0->UI_ADCADC_RegularChannelConfig(ADC1, ADC_Channel_11, 2, ADC_SampleTime_7Cycles5 );//测量放大器输出直流 PC1->UO_DCADC_RegularChannelConfig(ADC1, ADC_Channel_12, 3, ADC_SampleTime_7Cycles5 );//测量放大器输出信号 PC2->UO_1ADC_RegularChannelConfig(ADC1, ADC_Channel_13, 4, ADC_SampleTime_7Cycles5 );//测量放大器输出信号 PC3->UO_2 均方根#endif#if (PCB_TYPE == 2)#endif#if (PCB_TYPE == 3)#endif/******************** ADC1 *****************/ADC_DMACmd(ADC1,ENABLE);//开启ADC1的DMAADC_Cmd(ADC1, ENABLE);//使能指定ADC1ADC_ResetCalibration(ADC1);//使能复位校准 while(ADC_GetResetCalibrationStatus(ADC1));//等待复位校准结束ADC_StartCalibration(ADC1); //开启AD校准while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束ADC_SoftwareStartConvCmd(ADC1,ENABLE);//软件开启ADC1转换ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;//ADC工作模式:独立模式ADC_InitStructure.ADC_ScanConvMode = DISABLE ;//模数转换工作在单通道模式ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;//模数转换工作在多次转换模式ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//转换由软件而不是外部触发启动ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//ADC数据右对齐ADC_InitStructure.ADC_NbrOfChannel = 1;//顺序进行规则转换的ADC通道的数目ADC_Init(ADC3, &ADC_InitStructure);//根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器 ADC_RegularChannelConfig(ADC3, ADC_Channel_0, 1, ADC_SampleTime_1Cycles5 );//测量放大器输出信号 PA0->UO_2 扫描/******************** ADC3 *****************/ADC_DMACmd(ADC3,ENABLE);//开启ADC3的DMAADC_Cmd(ADC3, ENABLE);//使能指定ADC3ADC_ResetCalibration(ADC3);//使能复位校准 while(ADC_GetResetCalibrationStatus(ADC3));//等待复位校准结束ADC_StartCalibration(ADC3); //开启AD校准while(ADC_GetCalibrationStatus(ADC3)); //等待校准结束ADC_SoftwareStartConvCmd(ADC3,ENABLE);//软件开启ADC3转换ADC_DMA_Init();}
程序中一共需要测量5路信号,其中ADC1测量4路信号,ADC3测量1路信号,两个ADC都工作在独立模式下。
ADC1配置为多通道、多次转换、数据右对齐、软件触发转换,调用ADC_RegularChannelConfig函数配置4个通道。然后开启ADC1的DMA,配置为循环模式,16位数据,传输量为4,不开启中断,这样配置后缓存数组的的4个值依次就是ADC_RegularChannelConfig函数配置的通道顺序,所对应的AD转换值最新值。使用TIM2中断进行数据采样,中断中只需要读取缓存数组中的对应元素值即可,TIM2工作在32k的频率下,1k的信号可以采集32个点做均方根计算。——定时器触发ADC采样
ADC3配置为单通道、多次转换、数据右对齐、软件触发转换,调用ADC_RegularChannelConfig函数配置1个通道,ADC3_Channel_0上的信号和ADC1_Channel_12的信号是相同的,都是Uo_1信号,ADC1_Channel_12只在信号频率不超过1k时进行均方根计算时使用。ADC3_Channel_0通道则是进行扫描法,专门用来测量截止频率的,信号频率大于1k时使用。由于ADC3_Channel_0完成单次转换需要(12.5+1.5)/12M=1.167us,所以要保证1kHz下的信号可以完成一个周期的测量,DMA的传输数据量必须大于(1000/1.167=857)。最后ADC3的DMA配置为单次模式,传输量设置为2000,开启传输完成中断。
200k频率下周期为5us,5us和76us\frac{7}{6}us67us的最小公倍数为35us,也就是转换30次即可得到一组最值,所以2000次的转换中可以得到多组波形信号的最值,将最值做差即可得到信号的峰峰值。——高频信号测量原理
这篇博客对程序流程等进行了整体分析,之后的博客会对程序进行更为具体细致的分析,主要是测量、判断和校准部分的分析。
PS:程序源码和硬件电路图都在之前发的这个系列博文第一篇中,大家可以在那篇博文的最下面,点击网盘链接进行下载。