一、前言:
本开源工程描述含有:
【一、前言】
【二、工程描述(器件,功能,参考资料)】
【三、原理图与PCB】
【四、元件与焊接】
【五、代码详解】
【六、改进方向】
二、工程描述:
1、主要器件:
GD32E230C8T6最小系统板、底板和1.8寸 TFT彩屏三部分。其余元件参见后文BOM
2、最终功能:
测量简单信号的波形、电压和频率。
2-1、屏幕波形和参数显示功能
(1)屏幕显示波形【注意波形为倒相显示】
(2)输出信号参数显示(特指PWM方波)
输出状态【关闭/打开】
输出频率【1kHz,2kHz,4kHz】
占空比
(3)输入信号参数显示
输入幅值【范围:-1.6V~5V】(因为实际上1/50衰减最好别用)
输入频率【理论可测范围:1kHz~10kHz】
2-2、人机交互:调整波形显示和参数功能
(1)【按键KEY1】(左侧按键)
每按一次以5%的步进增加方波占空比,至100%则又变回0%
(2)【按键KEY2】(中间按键)
控制PWM方波的输出与停止,按一次开启,再按一次关闭
(3)【按键KEY3】(右侧按键)
切换PWM方波的输出频率,按动则依次在1Hz,2Hz,4Hz循环切换
(4)【编码器】(右下角)
按压则波形固定;
顺(正)转编码器,波形放大;
逆(反)转编码器,波形缩小;
3、其他
电源:5V 直流电源
电源接口:type-c
探头类型:BNC转鳄鱼夹
4.参考资料(嘉立创官方):
(1)详细开源文档:(后文说“开源文档”都指的这个)
https://www.yuque.com/wldz/jlceda/dso
(2)B站免费录播课:
搜索UP主:立创EDA,查看24年初一系列视频即得,与开源文档教程相辅相成
(3)官方参考代码(各部分代码,以及最终代码)
https://gitee.com/chen11232/GD32E230-Oscilloscope
【附件处有原版官方最终代码工程,以及一个我加了很多注释的版本的代码工程可以直接下载取用】
二、原理图与PCB板:
电路上的分析在之前总结过,这里主要对代码进行解析:
四、元件:
因为是嘉立创的活动结束之后才开始做的,我买元件时需要自己对着元件表输入商城的供应商编号,不过这样也不错,至少保险一点。这里主要要留意一下几个东西需要自己另行购买:
(1)1.8寸TFT显示屏。
这个在立创商城应该是没有的,画原理图时搜的供应商编号C9900080251并不能直接在立创商城下单,我当时自动对应的相似产品买回来是显示屏对应的八脚排母……
直接在某宝搜索关键字“TFT 1.8 128*160”即得。
(2)BNC转鳄鱼夹探头。
这个接头不在BOM单里,很容易忘记,要记得提前买
(3)芯片底座和排母
这个是最容易忽略的部分,视频里也没怎么提到,很容易跟着焊着焊着就突然发现没买,就又要等两三天,真的是很折磨。不过买过一次应该就能用很多很多次了。排母要买LCD屏幕用的8引脚排母和最小系统板用的10引脚排母
以下为板子实物图
五、代码详解:
主要用到的外设是定时器TIMER、模数转换器ADC、串口通信USART、SPI通信。
这里用一个示波器参数结构体把主要的参数集合到一起:
void Init_Oscilloscope(volatile struct Oscilloscope *value)
{
(*value).showbit =0; //清除显示标志位
(*value).sampletime =ADC_SAMPLETIME_239POINT5; //adc采样周期
(*value).keyValue =0; //清楚按键值
(*value).ouptputbit =0; //输出标志位
(*value).gatherFreq =0; //采集频率
(*value).outputFreq =1000; //输出频率
(*value).pwmOut =500; //PWM引脚输出的PWM占空比
(*value).timerPeriod=1000; //PWM输出定时器周期
(*value).vpp =0.0f; //峰峰值
}
另外还有 voltage Value[300] //ADC采集电压值
我们先从信号发出的PWM模块开始:
#define ADC_VALUE_NUM 300U
uint16_t adc_value[ADC_VALUE_NUM];
/*
* 函数内容:得到ADC值
* 函数参数:value–数组下标
* 返回值: 无
*/
uint16_t Get_ADC_Value(uint16_t value)
{
uint16_t returnValue=0;
if(value>ADC_VALUE_NUM)
{
value=0;
}
returnValue=adc_value[value];
adc_value[value]=0;
return returnValue;
}
DMA的设置:
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)(&adc_value); //内存地址
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 = ADC_VALUE_NUM; //数量
dma_data_parameter.priority = DMA_PRIORITY_HIGH; //高优先级
dma_init(DMA_CH0, &dma_data_parameter); //DMA通道0初始化
ADC在接收转换完之后,数据就能够被DMA转移到adc_value所在的地址,之后就可以直接拿出来显示了,所用到的函数就是这个Get_ADC_Value了。
extern volatile struct Oscilloscope oscilloscope;
void DMA_Channel0_IRQHandler(void)
{
if(dma_interrupt_flag_get(DMA_CH0, DMA_INT_FLAG_FTF)){
oscilloscope.showbit=1;
dma_channel_disable(DMA_CH0);
//清除中断标志位
dma_interrupt_flag_clear(DMA_CH0, DMA_INT_FLAG_G);
}
}
DMA的一次搬运完成后就可以开始显示了,这时候我们可以在DMA一次搬运完成后产生的中断中对示波器参数结构体中的showbit置一,表示我的ADC转换完的数据已经搬运到内存里了,可以进行显示了。
有了需要显示的数据,接下来就是要显示的方法和显示的界面弄好。
这里我们就要参考这个1.8寸TFT屏幕的厂商提中景园提供的代码了。因为这个屏幕使用的是SPI通信,所以我们要先把SPI通信相关的引脚先设置好。其中的SCL和SDA引脚我们分别定义在PA5口和PA7口,因为这个屏幕只需要接收主机传来的数据,不需要向主机传输数据,所以只有MOSI口没有MISO口,就只需要两个SPI通信相关的引脚就够了。设置为复用推完输出模式即可。剩余的三个引脚:
//#define TFT_RES PB5
//#define TFT_CS PB7
//#define TFT_BLK PB8
我们直接使用GPIO口来操作,将其设置为不加上下拉的推挽输出就可以了。其余的初始化配置参考厂商提供的代码即可。
接下来我们就能够利用厂商封装好的函数,来设计示波器的显示界面了。
显示界面我们可以分为显示波形、静态UI和动态数据
首先来解决显示波形,这里的原理就是一开始先只画一个点,下一次画第二个点的时候顺带把当前点和前一个点之间连线。
这里我们规定波形显示的区域是长0~100像素点,宽30~80像素点。注意提前刷新下一列像素点为背景色。
接下来是静态UI的设置函数:
void TFT_StaticUI(void)
{
uint16_t i=0,j=0;
char showData[32]={0};
TFT_ShowChinese(10,0,(uint8_t *)”简易示波器”,BLACK,GREEN,16,0);
sprintf(showData,” PWM “);//将字符串“PWM”放入showData字符串数组中
TFT_ShowString(110,0,(uint8_t *)showData,BLACK,YELLOW,16,0);
memset(showData,0,32);
TFT_ShowChinese(110,20,(uint8_t *)”输出状态”,WHITE,PURPLE,12,0);
sprintf(showData,” “);
TFT_ShowString(110,36,(uint8_t *)showData,BLACK,YELLOW,16,0);
memset(showData,0,32);
TFT_ShowChinese(110,56,(uint8_t *)”输出频率”,WHITE,PURPLE,12,0);
sprintf(showData,” “);
TFT_ShowString(110,72,(uint8_t *)showData,BLACK,YELLOW,16,0);
memset(showData,0,32);
sprintf(showData,” “);
TFT_ShowString(110,92,(uint8_t *)showData,WHITE,PURPLE,12,0);
memset(showData,0,32);
TFT_ShowChinese(118,92,(uint8_t *)”占空比”,WHITE,PURPLE,12,0);
sprintf(showData,” “);
TFT_ShowString(110,106,(uint8_t *)showData,BLACK,YELLOW,16,0);
memset(showData,0,32);
TFT_ShowChinese(5,92,(uint8_t *)”输入幅值”,WHITE,PURPLE,12,0);
TFT_ShowChinese(55,92,(uint8_t *)”输入频率”,WHITE,PURPLE,12,0);
for(i=0;i<=128;i=i+2)
{
TFT_DrawPoint(106,i,YELLOW);//中间分割线
}
for(i=0;i<100;i++)
{
TFT_DrawPoint(i,81,GREEN);//波形显示区域的底部参考线
}
for(i=30;i<=80;i++)
{
TFT_DrawPoint(0,i,GREEN);
}
for(i=0;i<10;i++)
{
TFT_DrawPoint((i*10)+2,82,GREEN);
TFT_DrawPoint((i*10)+3,82,GREEN);//波形底部的标尺
}
for(i=0;i<10;i++)
{
TFT_DrawPoint((i*10)+2,83,GREEN);
TFT_DrawPoint((i*10)+3,83,GREEN);//波形底部的标尺
}
}
这些区域都是可以自己自行规划的,如果要改变界面UI,直接在这里改就行了。
之后是动态数据的显示
/*
* 函数内容: 显示字符串
* 函数参数: uint16_t vpp–峰峰值
* uint16_t freq-频率
* float DoBias–直流偏执信号
* 返回值: 无
*/
void TFT_ShowUI(volatile const struct Oscilloscope *value)
{
char showData[32]={0};
sprintf(showData,”%1.2fV “,(*value).vpp);
TFT_ShowString(5,106,(uint8_t *)showData,BLACK,GREEN,16,0);//从示波器参数结构体中获取的数据,显示输入幅值在左下角
memset(showData,0,32);
if((*value).gatherFreq>=1000)//如果大于1KHz,就以KHz的单位来显示,避免占据过多范围
{
sprintf(showData,”%2.0fKHz”,(*value).gatherFreq/1000.0f);
TFT_ShowString(55,106,(uint8_t *)showData,BLACK,GREEN,16,0);
memset(showData,0,32);
}
else
{
sprintf(showData,”%3.0fHz “,(*value).gatherFreq);//以Hz的单位来显示
TFT_ShowString(55,106,(uint8_t *)showData,BLACK,GREEN,16,0);
memset(showData,0,32);
}
if((*value).ouptputbit == 1)
{
TFT_ShowChinese(118,36,(uint8_t *)”打开”,BLACK,YELLOW,16,0);//这个outputbit会在按键模块那里接受控制
}
else
{
TFT_ShowChinese(118,36,(uint8_t *)”关闭”,BLACK,YELLOW,16,0);
}
if((*value).outputFreq>=1000)
{
sprintf(showData,”%3dKHz”,(*value).outputFreq/1000);//输出模块是在PWM模块那里获取的数据
TFT_ShowString(110,72,(uint8_t *)showData,BLACK,YELLOW,16,0);
memset(showData,0,32);
}
else
{
sprintf(showData,” %3dHz”,(*value).outputFreq);
TFT_ShowString(110,72,(uint8_t *)showData,BLACK,YELLOW,16,0);
memset(showData,0,32);
}
sprintf(showData,”%3.1f%% “,(((*value).pwmOut)/((*value).timerPeriod+0.0f))*100);//计算占空比
TFT_ShowString(110,106,(uint8_t *)showData,BLACK,YELLOW,16,0);
memset(showData,0,32);
}
到这里显示的模块就完成了,就是还有一些比较底层的代码,我们直接参考中景园提供的代码就OK了。
接下来是人机交互的按键模块。
总共是有三个轻触按键和一个旋转编码器
三个轻触按键对应的是GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15
旋转编码器总共有三个引脚要接GPIO口,A相、B相、D相,分别对应是GPIO_PIN_4 | GPIO_PIN_3 | GPIO_PIN_9
其中三个轻触按键和B相D相的引脚可以一起配置,配置为上拉输入模式就好了,同时要记得中断线使能、配置中断线、初始化中断线配置为中断模式上升沿触发
另外的选择A相作为基准信号,要单独配置是因为要给它配置CMP比较器,用于比较两个输入信号的大小,并根据比较结果输出相应的信号,其实让B相来配置CMP也可以,不过方向会反过来。
顺时针旋转时,A相产生上升沿,并且B相会在短时间内保持低电平;逆时针旋转时,A相产生上升沿,并且B相会在短时间内保持高电平,以此我们就能够来区分正转和反转了。
中断触发函数中:
{
delay_1ms(5);
if(gpio_input_bit_get(GPIOB,GPIO_PIN_9)==RESET)
{
oscilloscope.keyValue=KEYD;
}
exti_interrupt_flag_clear(EXTI_9);
}
{
uint8_t i=0,j=0;
float tempValue=0;
switch((*value).keyValue)
{
case KEY1://KEY1按钮,每按一次以5%的步进增加方波占空比,至100%则又变回0%
(*value).pwmOut=((*value).timerPeriod*0.05f)+(*value).pwmOut;
if((*value).pwmOut > (*value).timerPeriod)
{
(*value).pwmOut = 0;
}
Set_Output_PWMComparex((*value).pwmOut);
break;
case KEY3://切换PWM方波的输出频率,按动则依次在1Hz,2Hz,4Hz循环切换
tempValue=(*value).pwmOut/((*value).timerPeriod+0.0f);
(*value).timerPeriod = (*value).timerPeriod/2.0f;
if((*value).timerPeriod < 250)
{
(*value).timerPeriod = 1000;
}
(*value).outputFreq=1000000/(*value).timerPeriod;
(*value).pwmOut=(*value).timerPeriod*tempValue;
Set_Output_PWMComparex((*value).pwmOut);
Set_Output_Freq((*value).timerPeriod-1);
tempValue=0;
break;
case KEY2://控制PWM方波的输出与停止,按一次开启,再按一次关闭
if((*value).ouptputbit == 0)
{
(*value).ouptputbit=1;
timer_enable(TIMER14);
}
else
{
(*value).ouptputbit=0;
timer_disable(TIMER14);
}
break;
case KEYA//:顺(正)转编码器,波形变宽
switch((*value).sampletime)
{
case ADC_SAMPLETIME_239POINT5:
(*value).sampletime=ADC_SAMPLETIME_71POINT5;
break;
case ADC_SAMPLETIME_71POINT5:
(*value).sampletime=ADC_SAMPLETIME_55POINT5;
break;
case ADC_SAMPLETIME_55POINT5:
(*value).sampletime=ADC_SAMPLETIME_41POINT5;
break;
case ADC_SAMPLETIME_41POINT5:
(*value).sampletime=ADC_SAMPLETIME_28POINT5;
break;
case ADC_SAMPLETIME_28POINT5:
(*value).sampletime=ADC_SAMPLETIME_28POINT5;
break;
default:
(*value).sampletime=ADC_SAMPLETIME_239POINT5;
break;
}
//ADC常规通道配置–PA3,顺序组0,通道3,采样时间
adc_regular_channel_config(0, ADC_CHANNEL_3, (*value).sampletime);
break;
case KEYB://逆(反)转编码器,波形变窄
switch((*value).sampletime)
{
case ADC_SAMPLETIME_239POINT5:
(*value).sampletime=ADC_SAMPLETIME_239POINT5;
break;
case ADC_SAMPLETIME_71POINT5:
(*value).sampletime=ADC_SAMPLETIME_239POINT5;
break;
case ADC_SAMPLETIME_55POINT5:
(*value).sampletime=ADC_SAMPLETIME_71POINT5;
break;
case ADC_SAMPLETIME_41POINT5:
(*value).sampletime=ADC_SAMPLETIME_55POINT5;
break;
case ADC_SAMPLETIME_28POINT5:
(*value).sampletime=ADC_SAMPLETIME_41POINT5;
break;
default:
(*value).sampletime=ADC_SAMPLETIME_239POINT5;
break;
}
//ADC常规通道配置–PA3,顺序组0,通道3,采样时间
adc_regular_channel_config(0, ADC_CHANNEL_3, (*value).sampletime);
break;
case KEYD:
break;
default:
break;
}
(*value).keyValue=0;
//参数显示UI
TFT_ShowUI(value);
}
Key_Handle(&oscilloscope);
//(ADC分辨率) / (参考电压) = (ADC获得值) / (实际电压)
//换算时进行反相伸缩,将图形控制在以y=30为中心,范围不超过18.75~41.25的区域内显示
六、改进方向
(1)示波器由原来的单通道变为双通道乃至多通道
(2)添加信号发生器功能,此时除了最小系统板外还需要外接的DAC模块
电路改进
(1)输入信号衰减电路改进:
×1/50的衰减对本项目不实用,因为对于5V~25V的电压,如果使用×1档则会超出限额,使用×1/50档则衰减到0.1~0.5V从而易受干扰从而不精确。可以调整诸如1/3,即对应电路(模拟前端处理电路->电压衰减电路)的三个分压电阻阻值要自己调。(但是三个电阻的阻值加起来需要超过1MΩ)
(2)比较器测频电路的改进:
可以通过调整电阻阻值来让滞回比较器的阈值电压U+更高,U-更低,从而让测频效果更好(但电阻阻值调整后依然要满足一定的约束关系)
器件选择改进
直插原件变贴片元件。
*直插元件便于新手焊接,但是占地空间大;
*贴片元件占地空间小,但是焊接需要用到的不只是电烙铁
改变探头:
原来是BNC转鳄鱼夹探头,可以换成专业示波器的无源探头,这样可以利用探头自带的×10档对信号有一个初步的衰减,而不是仅靠衰减电路来衰减了