硬件介绍:STM32F072的多功能掌中仪器, 主控芯片为STM32F072CB,是主流ARM Cortex-M0 USB系列MCU,具有128 KB Flash、48 MHz CPU、USB、CAN和CEC功能。开发板上集成了240*240 LCD显示,2个按键和一个拨轮开关,可以使用DFU方式烧写程序。但是DFU方式烧写程序,每次需要断电重启,很不方便。我从电路板上将SWD的接口引出了,并且将串口的TX也一并引出,加上5v电源(图片中是接的3.3v,后改为接5v),一共引出了5根线。方便编程和调试。
任务选择:这里我选择的是任务3 制作双通道可调直流电压。
从电路图可以看出,开发板有两路电压输出,对应DC1,DC2。这两路电压输出是由TL974输出的。TL974我的理解就是一个放大器,如DC2,输入电压由2脚输入,放大器将输入电压与3脚的电压作比较,然后放大输出到1脚(DC2)。3脚电压恒定在1.5v,TL974的4脚和11脚接入正负5v电压。当2脚输入电压等于3脚的1.5v时,1脚输出为0v。2脚与单片机的IO相连接,单片机可以通过IO输出指定电压,范围0v~3.3v。当2脚输入电压高于或低于3脚的1.5v时,1脚输出的电压由2脚与1.5v电压差决定,理论上可以从-5v~+5v,但实际会小于最大值。任务要求是输出-4v~+4v。2脚的输入的电压由单片机的pwm值来控制,pwm本质上是占空比可以改变的方波,经过电阻电容的低通滤波转换为指定的电压值。在输出端,输出的电压经过电阻分压,重新送回单片机的IO,这样单片机就可以通过ADC获得输出的电压值了,可以做闭环处理。不过只能测量输出部分的正电压部分。
实现经历:以前接触过STM32F103的芯片,这次开发板是STM32F072,感觉上应该是差不多。但是等动手去做,才发现自己错了。STM32F072是M0的内核,和M3的内核相差还是挺大的。本想着STM32F103的程序拿来能直接用,结果基本都不能运行。
首先是开发工具,在STM32F103时,使用的是keil,有大量的教程,而且有帮忙建设好了的模板,做过的事情基本就是修修改改。这次的开发板,连基本的工程搭建搞不定,从网上下载了很多个工程模板,有能用的,有不能用的。才发现自己对基础的时钟概念都没理顺。静下心来,从头学习。下载了STM32CUBEMX工具,用图形的方式慢慢地摸索前行。
先配置时钟,开发板没有用晶振,使用使用内部时钟源,最高到48Mhz,所以配置到48Mhz,后边SPI和pwm都需要用到这个时钟频率。
GPIO配置,开发板一共有2个按键,一个拨轮。拨轮有左中右三个按键。定义中间按键负责处理DC1、DC2控制的切换,左右负责调解电压值。两个按键定义为DC1和DC2是否联动。
开启串口,一步一步地摸索,总是出错,有了串口加成,可以清晰地看见是否达到自己的预期。
DC1、DC2对应PB14、PB15两个IO,在系统中是属于普通定时器,对应TIM15的1、2通道。这里有两个参数,Prescaler和Counter Period。一个决定分频数,一个决定周期。两个合起来就决定了PWM的频率,这里是48M/(12*1000)=4000Hz。测试过2000Hz的效果也一样,最后是一个低通滤波器,高频信号都会被过滤掉。
SPI用来驱动ST7789屏幕。以前使用spi协议,最少用过3线的,一个时钟线,一个MOSI,一个MISO。按着这个思路折腾了两天,始终是驱动不了屏幕,一度以为屏幕故障了,找了个中景园的驱动程序测试,屏幕正常,可是程序是模拟SPI的,速度太慢,在讨论组里求助,要来了别人的屏幕驱动程序才明白,这个屏幕仅仅需要写入数据即可,所以只用了MOSI,MISO没有用,是作为res功能脚的。终于按这个思路驱动了屏幕,偷了个懒,直接调用中景园的屏幕绘图函数。但是不知道为啥,感觉还是有点慢,应该是SPI配置还是没配好。DMA还不会用,还需要再学习一下DMA的配置,应该还能再提速一些的。这里对stm32clubmx还有个小疑问:软件默认的SPI1的管脚是PA4、PA7,可以在图形界面中修改为PB3、PB5,可是在生成的代码中没有找到对应的管脚重映射的部分,不明白这个管脚是如何控制功能映射的。
ADC的配置,开发板的ADC是12bit精度的逐级逼近式ADC。有两种读取模式,一种是间断模式,每次需要先使用HAL_ADC_Start(),需要使用HAL_ADC_PollForConversion()等待转换完成,HAL_ADC_GetState()获取ADC转换状态(若返回值为HAL_OK说明转换完成),转换完成后使用HAL_ADC_GetValue()读取ADC原始值,读取完成后,使用HAL_ADC_Stop()停止转换,如需再次获取ADC数据,需重复执行上述步骤。一种是连续模式,只需要使用一次HAL_ADC_Start(),开启转换,ADC会马不停蹄的电压转换成数字量,用户只需要调用HAL_ADC_GetValue(),读取ADC原始值。这里使用的是间断模式,但是不明白为啥,ADC得到的结果波动还是挺大的,为了处理波动带了的误差,采取每次收集10次ADC数据,然后取均值作为读取的电压值。
void readADC(uint16_t adc_dc[]){
uint8_t i=0,flag=1,readtimes=0;
uint16_t val0=0,val1=0;
adc_dc[0]=0;
adc_dc[1]=0;
for(i=0;i<10;i++){
flag=1;
HAL_ADC_Start(&hadc);//启动ADC
HAL_ADC_PollForConversion(&hadc,0x30);//表示等待转换完成,第二个参数表示超时时间,单位ms.
//HAL_ADC_GetState(&hadc1)为换取ADC状态,HAL_ADC_STATE_REG_EOC表示转换完成标志位,转换数据可用。
if(HAL_IS_BIT_SET(HAL_ADC_GetState(&hadc),HAL_ADC_STATE_REG_EOC)){//就是判断转换完成标志位是否设置,HAL_ADC_STATE_REG_EOC表示转换完成标志位,转换数据可用
//读取ADC转换数据,数据为12位。查看数据手册可知,寄存器为16位存储转换数据,数据右对齐,则转换的数据范围为0~2^12-1,即0~4095.
val0=HAL_ADC_GetValue(&hadc);
}else{
flag=0;
}
HAL_ADC_Start(&hadc);//启动ADC
HAL_ADC_PollForConversion(&hadc,0x30);//表示等待转换完成,第二个参数表示超时时间,单位ms.
//HAL_ADC_GetState(&hadc1)为换取ADC状态,HAL_ADC_STATE_REG_EOC表示转换完成标志位,转换数据可用。
if(HAL_IS_BIT_SET(HAL_ADC_GetState(&hadc),HAL_ADC_STATE_REG_EOC)){//就是判断转换完成标志位是否设置,HAL_ADC_STATE_REG_EOC表示转换完成标志位,转换数据可用
//读取ADC转换数据,数据为12位。查看数据手册可知,寄存器为16位存储转换数据,数据右对齐,则转换的数据范围为0~2^12-1,即0~4095.
val1=HAL_ADC_GetValue(&hadc);
}else{
flag=0;
}
if(flag==1){ //本次暑假采集有效 记录数据
adc_dc[0]+=val0;
adc_dc[1]+=val1;
readtimes++;
}
HAL_ADC_Stop(&hadc);
}
//printf("%d\t%d\t%d\n",readtimes,adc_dc[0],adc_dc[1]);
//取平均值
adc_dc[0]=adc_dc[0]/readtimes;
adc_dc[1]=adc_dc[1]/readtimes;
有了stm32cubema的帮助,很快建立的项目工程。然后测试PWM与输出电压的关系。因为pwm的Counter Period设置为999,所以pwm的取值范围是0~999。使用万用表协助测量,在pwm值为530时,输出为0v。预计是500,略有偏差。最高能输出到4.87v。DC输出电压+4v~-4v对应的PWM取值范围为【220,840】,在取值范围内呈现良好的线性关系。
因此,当输出为正值时,使用ADC的测量值作为输出的电压值,当输出为负值时,使用pwm映射出来的电压值作为输出电压值。在两路电压输出不联动时,每路输出给定一个PWM值;当联动时,PWM值也是互相做映射,同步修改。实际测量中,输出电压还是挺准的,就是在输出0时,ADC测量,还是比较小的读数,大约在0.2v左右。
屏幕分为两个部分,左边为DC1的控制,右边为DC2的控制。使用一个小红点,标注出当前按键控制的是哪个输出。使用中断来接收按键的操作。这里有使用系统的Tick时钟来进行消抖,但是从实际测试情况来看,效果一般。还是会有抖动情况发生。当左边按键按下时,DC1、DC2被锁定联动,调节时两边电压会同时变化。按中间的键,解除锁定状态,两个电压输出独立。每次锁定、解锁操作时都会将电压归零,防止意外发生。
//按键处理
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
if(HAL_GetTick()-lasttick<=15){ //15ms为界限
return ;
}else{
lasttick=HAL_GetTick();
}
if(GPIO_Pin == KEY_1_Pin){/* KEY 1 左键 联动*/
if(lockdc==0){
lockdc=1;
pwm_dc[0]=PWMZERO; //电压归零
pwm_dc[1]=PWMZERO;
}
}
if(GPIO_Pin == KEY_2_Pin){/* KEY 1 右键 独立*/
if(lockdc==1){
lockdc=0;
pwm_dc[0]=PWMZERO; //电压归零
pwm_dc[1]=PWMZERO;
}
}
if(GPIO_Pin == KEY_R_Pin){/* KEY L */
if(lockdc==0){ //独立模式
pwm_dc[chose]+=PWMSTEP;
}else{ //联动模式
pwm_dc[chose]+=PWMSTEP;
pwm_dc[(chose+1)%2]=PWMZERO+PWMZERO-pwm_dc[chose];
}
if(pwm_dc[0]>PWMMAX) pwm_dc[0]=PWMMAX;
if(pwm_dc[0]<PWMMIN) pwm_dc[0]=PWMMIN;
if(pwm_dc[1]>PWMMAX) pwm_dc[1]=PWMMAX;
if(pwm_dc[1]<PWMMIN) pwm_dc[1]=PWMMIN;
}
if(GPIO_Pin == KEY_O_Pin){/* KEY O 拨轮*/
chose=(chose+1)%2;
}
if(GPIO_Pin == KEY_L_Pin){/* KEY R */
if(lockdc==0){ //独立模式
pwm_dc[chose]-=PWMSTEP;
}else{ //联动模式
pwm_dc[chose]-=PWMSTEP;
pwm_dc[(chose+1)%2]=PWMZERO+PWMZERO-pwm_dc[chose];
}
if(pwm_dc[0]>PWMMAX) pwm_dc[0]=PWMMAX;
if(pwm_dc[0]<PWMMIN) pwm_dc[0]=PWMMIN;
if(pwm_dc[1]>PWMMAX) pwm_dc[1]=PWMMAX;
if(pwm_dc[1]<PWMMIN) pwm_dc[1]=PWMMIN;
}
}
心得体会:一直想拥有一台示波器,这次活动算是满足了自己的心愿。期待硬禾学堂即将推出的固件。之前看直播课,说是有做塑料外壳,不知道如何能够或得到,现在的3D打印的外壳有点卡不紧了。期待其他大神的作品,自己的屏幕刷新始终不够快,还待学习。