1
完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
扫一扫,分享给好友
|
|
相关推荐
1个回答
|
|
前言:用正点原子的mini开发板,设计制作简易示波器和简易函数发生器,需要运用的知识是 ADC+DAC+DMA+通用定时器+外部中断。
一、项目整体思路和实现的功能 这个项目是基于正点原子stm32 mini开发板设计的,使用芯片为STM32F103RCT6,相关配置步骤和基础知识,可以在正点原子论坛找到。 (一)、简易示波器思路和功能 利用stm32的ADC功能,在一定时间内采集IO口电压,将采集到的数值保存在数组中,经过数据处理后,显示在LCD上。 能实现正电压下,0~3.3v电压的显示,以及最高10KHZ的频率显示(10K以上显示将不清晰)。能通过两个按键实现对ADC采样周期的转换,分为us级和ms级。 (二)、简易函数发生器思路和功能 利用stm32强大的DAC和DMA功能,以定时器2触发DAC转换,以DMA传送需要转换的数值,以达到目标波形的输出。 能实现正弦波,三角波,方波,锯齿波,甚至模拟噪声波等多种波形的输出,可以调节输出波形的幅值和频率。 二、程序设计和部分原理解释 (一)、外围按键设计 这部分主要涉及改变ADC采样周期,由于整个程序有延时,必须采用中断的方式读取键值并改变采样周期标志位,这样才能达到按一次改变一次的效果。但是任然存在延时,按了按键以后需要等到下一个循环周期才能改变LCD显示。 这部分包括按键初始化,键值读取,外部中断配置。主要展示外部中断配置。 void EXTIX_Init(void) { EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//打开AFIO时钟 KEY_INT(); //初始化按键 //配置外部中断 GPIO_EXTILineConfig(GPIO_PortSourceGPIOC,GPIO_PinSource5);//key0 PC5引脚 EXTI_InitStructure.EXTI_Line=EXTI_Line5; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource15);//key1 PA15引脚 EXTI_InitStructure.EXTI_Line=EXTI_Line15; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0);//wk_up PA0引脚 EXTI_InitStructure.EXTI_Line=EXTI_Line0; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;//抢占优先级2 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01; //子优先级1 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init( &NVIC_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init( &NVIC_InitStructure); } 对于在按键的中断函数中如何实现让采样周期变长或缩短,我这里是通过改变标志位,在主函数中判断标志位的值来改变的。一是可以改变时间单位,即ms或者us,二是可以改变数值,我设置了六个数值,分别为20,40,60,80,100,120。大家也可以根据自己的需要更改。 (二)、ADC初始化和数值获取 初始化ADC1的PC0口作为ADC的输入端口。采用6分频,即12M时钟作为ADC的时钟,选取ADC最小采用时间是71.5个时钟周期,外加12.5个固定的转换时钟周期,如此计算可得ADC最小采样周期为1/72M*90=7.5us,为达到比较好的效果,人为设置成最小20us。 void adc_init(void) { GPIO_InitTypeDef GPIO_InitStruct; ADC_InitTypeDef ADC_InitStruct; //开启ADC1和相应IO口时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC|RCC_APB2Periph_ADC1,ENABLE); //ADC时钟由主时钟六分频 RCC_ADCCLKConfig(RCC_PCLK2_Div6); //PC0初始化 GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AIN; GPIO_InitStruct.GPIO_Pin=GPIO_Pin_0; GPIO_Init(GPIOC, &GPIO_InitStruct); ADC_DeInit(ADC1); //ADC1初始化 ADC_InitStruct.ADC_ContinuousConvMode=DISABLE; ADC_InitStruct.ADC_DataAlign=ADC_DataAlign_Right; ADC_InitStruct.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None; ADC_InitStruct.ADC_Mode=ADC_Mode_Independent; ADC_InitStruct.ADC_NbrOfChannel=1; ADC_InitStruct.ADC_ScanConvMode=DISABLE; ADC_Init(ADC1,&ADC_InitStruct); //使能ADC1 ADC_Cmd(ADC1,ENABLE); //使能复位校准 ADC_ResetCalibration(ADC1); //等待复位校准结束 while(ADC_GetResetCalibrationStatus(ADC1)); //使能ADC校准 ADC_StartCalibration(ADC1); //等待校准完成 while(ADC_GetCalibrationStatus(ADC1)); } 接下来是ADC值获取,换取之后将其转换成电压值,需要强调的是,转换时间遵循以下,T转换=采样时间(可以设置的ADC时钟周期)+12.5个ADC时钟周期,为了方便主函数处理,电压值转换成以3300为峰值的四位数。 u16 adc_get(void) { u16 value=0; //转换周期为7us ADC_RegularChannelConfig(ADC1,ADC_Channel_10,1,ADC_SampleTime_71Cycles5); ADC_SoftwareStartConvCmd(ADC1,ENABLE); while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC )); value=ADC_GetConversionValue(ADC1); value=(int)value*3.3*1000/4096; return value; } (三)、ADC数据处理部分 这一步是将ADC采集的值转换成便于LCD显示的值后存储 while(i<160) { value=adc_get(); //由于返回的ADC的值是四位整数,且显示电压部分像素点共120个,对应每个点 //0.0275v,故将采集到的电压值除以27以便求出每个电压对应的像素点个数 value=(int)value/27; if(us_ms==1) delay_ms(time_get); else delay_us(time_get-16); i++; } i=0; (四)、LCD显示设置 LCD使用的是正点原子的2.4*2.8的屏幕,所以直接采用正点原子提供的库函数。对于LCD的初始化和相关函数使用,可以参考正点原子相关文档。这里展示如何在LCD上描绘波形。我采用的是采集160个ADC值,由于是横屏显示,有320个像素点,所以每隔一个点描绘一个ADC值,把描绘的点连线后就是波形。LCD描点和连线的函数可以采用正点原子官方函数,例如连线的函数是LCD_DrawLine(X1,Y1,X2,Y2)函数中的参数为:X1为X轴起点,X2为X轴终点,Y1为Y轴起点,Y2为Y轴终点。 while(i<159) { POINT_COLOR=RED; LCD_DrawLine(i*2,120-value,(i+1)*2,120-value[i+1]); delay_ms(5); i++; } i=0; (五)、DAC初始化 使用的Mini开发板的RCT6芯片有两个DAC,即DAC通道1,对应PA4口, DAC通道2,对应PA5口。需要注意的是DAC自带的输出缓存功能,如果使能该功能,虽然带负载能力更强,但是输出无法到0。同时DAC需要用到定时器触发,并且开启DMA使能。 void dac_init(void) { GPIO_InitTypeDef GPIO_InitStruct; DAC_InitTypeDef DAC_InitStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC,ENABLE); //GPIO_Mode也可以设置成模拟输入 GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5; GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStruct); //结构体成员初始化一定要有,不然会出错。 DAC_StructInit(&DAC_InitStruct); DAC_InitStruct.DAC_OutputBuffer=DAC_OutputBuffer_Disable; DAC_InitStruct.DAC_Trigger=DAC_Trigger_T2_TRGO;//定时器2触发 DAC_InitStruct.DAC_WaveGeneration=DAC_WaveGeneration_None; DAC_Init(DAC_Channel_1, &DAC_InitStruct); DAC_Cmd(DAC_Channel_1,ENABLE); //开启DMA DAC_DMACmd(DAC_Channel_1,ENABLE); } (六)、定时器初始化 此函数需要传入波形输出频率,由于在定时器初始化函数中赋值给TIM_TimeBaseInitStruct.TIM_Period成员的实际上是定时器重装载值,所以需要将传入的参数进行转换再赋给这个成员。具体转换见程序: void timer_init(u32 f) { TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; //将传入的参数转换成定时器重装载值 f=(u16)(72000000*2/sizeof(dac_out)/f); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); TIM_TimeBaseStructInit(&TIM_TimeBaseInitStruct); TIM_TimeBaseInitStruct.TIM_ClockDivision=TIM_CKD_DIV1;//不预分频 72M TIM_TimeBaseInitStruct.TIM_Period=f; TIM_TimeBaseInitStruct.TIM_Prescaler=0x00; //不时钟分割 TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStruct); //更新事件触发 TIM_SelectOutputTrigger(TIM2,TIM_TRGOSource_Update); } (七)、DMA初始化 DMA主要功能是将存储在内存或外设的数据,不经过CPU直接传送给目标寄存器或者外设,能节省CPU分配,也能加快程序运行效率。这里是将计算好的DAC值直接传送给DAC寄存器。DMA采取内存递增,循环模式。当TIM2产生更新事件时,DAC将最近存放在寄存器DAC_DHRX中的数据传送至寄存器DAC_DORX中,从而产生电压,同时DAC使能了DMA,当产生一个电压后就会触发DMA,从而得到下一个电压值。对于DMA循环功能,如果下一个内存超出指定最大位置时就会回到开始位置。关于外设地址,可以在stm32f10x.h文件中查找。内存地址就是存放电压值的数组名。 void dma_init(void) { DMA_InitTypeDef DMA_InitStruct; //开启时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2,ENABLE); //初始化结构体成员 DMA_StructInit( &DMA_InitStruct); DMA_InitStruct.DMA_BufferSize=much; //much在主函数中定义 是数组成员个数 DMA_InitStruct.DMA_DIR=DMA_DIR_PeripheralDST; //由内存到外设 DMA_InitStruct.DMA_M2M=DMA_M2M_Disable; //内存到内存关闭 DMA_InitStruct.DMA_MemoryDataSize=DMA_MemoryDataSize_HalfWord; DMA_InitStruct.DMA_MemoryInc=DMA_MemoryInc_Enable; //内存地址递增 DMA_InitStruct.DMA_Mode=DMA_Mode_Circular;//循环模式 DMA_InitStruct.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord; DMA_InitStruct.DMA_PeripheralInc=DMA_PeripheralInc_Disable;//外设地址不递增 DMA_InitStruct.DMA_Priority=DMA_Priority_VeryHigh;//等级非常高 DMA_InitStruct.DMA_MemoryBaseAddr=(uint32_t)dac_out;//内存地址 DMA_InitStruct.DMA_PeripheralBaseAddr=DAC_DHR12R2; //DAC地址 在主函数中定义 DMA_Init(DMA2_Channel3, &DMA_InitStruct); DMA_Cmd(DMA2_Channel3,ENABLE); } (八)、波形表数值的产生 本来是将波形产生的函数设计在MDK程序中的,可是在实际运行中波形输出不好,估计是受限于32对浮点数的计算能力,所以我在VC6.0中设计了一个程序,以计算输出不同波形下不同样点个数的波形数值表。由于简易示波器无法显示负电压,所以需要有个基础电压,这里我设置成1.6v,当然如果想要修改对应峰值,改1.6就行。这点很容易理解。各个波形的32——256位的波形表我在文件中都有分享,下面是程序: #include #include #define much 32 //波形数组里的成员个数 #define much_float 32.0000 //便于计算,分母必须是浮点型 输出才准确 int value[much]; int i; for(i=0;i //锯齿波产生函数 if(i<=(much/4-1)) value=(1.6+1.6/(much_float/4)*i)*4095/3.3; if(i>(much/4-1)&&i<=(much/4-1+much/2)) value=(3.2/(much_float/2)*(i-(much/4)))*4095/3.3; if(i>(much/4-1+much/2)&&i //三角波函数 /*if(i<=(much/4-1)) value=(1.6+1.6/(much_float/4)*i)*4095/3.3; if(i>(much/4-1)&&i<=(much/4-1+much/2)) value=(3.2-3.2/(much_float/2)*(i-(much/4)))*4095/3.3; if(i>(much/4-1+much/2)&&i 正弦波函数 //value=((1.6*sin(i/32.00*2*3.14)+1.6)*4095/3.3); for(i=0;i printf("%d,",value); } } 三、注意事项 (一)、关于示波器部分 1、LCD显示的总时间是:160*Tadc转换时间,所以波形周期可以根据查看显示的波形周期数和LCD显示的时间得到。如果想要改采集的点数,修改相应的数组大小和LCD显示程序即可。 2、对转换时间误差的个人理解:由于单片机执行程序时会耗时,ADC转换时间又是us级别,以20us为实际转换周期,除去初始化时设置的7.5us的转换时间,本来需要延时12.5us,由于 程序执行时间的消耗,实际延时不能是12.5us,这样会使转换周期与设计有较大偏差,经过测试,延时时间应是(20-16)us,如果需要设置其他转换周期,将20改掉即可。 3、由于ADC最大读取值是3.3v,当采集的峰值大于该值时,为了方便,我直接采用分压电路,将信号分压后再采入。 4、配置DAC功能时,GPIO引脚需要设置为模拟输入,为了避免寄生的干扰和额外的功耗。 5、在配置DMA功能时,需要极其注意外设地址是否正确。查找外设的基地址在头文件stm32f10x.h内,例如需要查找DAC的外设基地址,找到DAC,进入DAC的结构体,就可以查找到了: (二)、关于函数发生器部分 1、注意函数发生器的频率不能超过20k。 2、当波形表数组大小越大时所能展现的波形越细致。 四、效果展示 PWM波形 正弦波形(1K) |
|
|
|
只有小组成员才能发言,加入小组>>
调试STM32H750的FMC总线读写PSRAM遇到的问题求解?
1767 浏览 1 评论
X-NUCLEO-IHM08M1板文档中输出电流为15Arms,15Arms是怎么得出来的呢?
1619 浏览 1 评论
1069 浏览 2 评论
STM32F030F4 HSI时钟温度测试过不去是怎么回事?
724 浏览 2 评论
ST25R3916能否对ISO15693的标签芯片进行分区域写密码?
1673 浏览 2 评论
1935浏览 9评论
STM32仿真器是选择ST-LINK还是选择J-LINK?各有什么优势啊?
727浏览 4评论
STM32F0_TIM2输出pwm2后OLED变暗或者系统重启是怎么回事?
567浏览 3评论
592浏览 3评论
stm32cubemx生成mdk-arm v4项目文件无法打开是什么原因导致的?
551浏览 3评论
小黑屋| 手机版| Archiver| 德赢Vwin官网 ( 湘ICP备2023018690号 )
GMT+8, 2024-12-21 21:42 , Processed in 0.707600 second(s), Total 76, Slave 60 queries .
Powered by 德赢Vwin官网 网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
德赢Vwin官网 观察
版权所有 © 湖南华秋数字科技有限公司
德赢Vwin官网 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号