文章目录
其实是在整理手上的元器件的时候,想用一个示波器,但是我没有,因此就想到自己简单做一个,能满足自己的简单需求即可。
经过测试 10Khz 及以下的波形都可以正常显示,可以先去文章末尾看效果。更高的频率还没试过。缺点就是:只能测量正电压,0-3.3。一般测量开发板调试电机时输出的PWM波是够用了。
1. 硬件准备
- 正点原子STM32F103 Mini开发板,主控 STM32F103RCT6.
- 配套的 2.8英寸 LCD屏幕。
其他开发板也行,只要能驱动这个屏幕就行。
普中的开发板的话,我主页也有同样 LCD屏的驱动教程,完全可以直接替代移植。
2. 软件规划
示波器监测部分
- 定时器定时触发ADC;
- ADC监测电压变化;
- DMA传输数据ADC数据。
显示部分:
- LCD屏进行显示;
3. 硬件配置
3.1. 创建工程
选择 STM32F103RCT6主控;
配置调试接口
配置外部时钟
配置时钟树
工程设置
3.2. LCD配置
懒的话,可以直接把 软件模拟8080接口驱动LCD的工程拿过来(主页置顶文章有)。跳过前面和本节的步骤。
因为LCD占用的引脚比较多,而且在开发板上都是定死的,而ADC之类的,都有的选则,所以先配置这个。
这里使用的是 正点原子STM32F103 Mini开发板,采用软件模拟8080通信协议进行驱动。
LCD的配置已经说过了,这里就不再重复了。看往期文章的 软件模拟8080并口驱动LCD屏部分。
配置完成之后的脚印分布如图。
然后添加 LCD 驱动文件。先测试LCD是否可以正常驱动。同理,看上面说的那篇文章。
懒的话,可以直接把 软件模拟8080接口驱动LCD的工程拿过来(主页置顶文章有)。跳过前面和本节的步骤。
在确保 LCD 可以运行的情况下,然后开始配置剩下的部分。
3.3. 定时器配置
因为需要保证 ADC每次采用的时间间隔相同,不能再while(1)中进行循环采样。
在《STM32中文参考手册》中可以看到如下内容。
也就是说,可以利用一个定时器触发ADC采样。
因此,进行如下配置。
定时器都是挂在APB1和APB2上,时钟频率都一样,都是72MHZ。
这里 分频系数为 36-1
,分频完之后的频率是72Mhz / 36 = 2Mhz
,计数值为100
,也就是每秒产生20000
次中断。
也就是每秒触发ADC采用20000次。换句话说,ADC的采样频率是 20000HZ。
3.4. ADC配置
任选一个ADC通道即可。
- 设置数据对齐方式,一般都直接选右对齐即可。
- 扫描、循环都不用设置。
- 设置为定时器3外部事件触发。
- 设置采用周期。
- 配置DMA,从外设到内存。
DMA是需要循环转移数据的。
配置完ADC之后,ADC的时钟也需要再改一下,配置ADC之前,这个地方是灰色的。现在就可以改了,配置完不超过14Mhz就可以了。
3.5. 按键配置
调整示波器的显示,正经的示波器上是个旋钮,这里用按键代替,一个开发板齐活。
开发板上有三个按钮。WK_UP、KEY0、KEY1. 原理图如下, 把这三个按钮都配置为中断。
- WK_UP:上升沿中断、下拉;
- KEY0:下降沿中断、上拉;
- KEY1:下降沿中断、上拉;
需要注意的就是 按键中断 和 DMA中断的优先级(跟DMA相关的中断优先级总是要调试很久。。。不明白,心累,改天补补课研究研究)。
3.6. 指示灯配置
最后再配置个指示灯,用于给个提示,正在获取数据 刷新波形之前让 LED 亮,波形刷新之后让他灭,等待下次获取的时候再亮。。。
开发板上原理图有两个,这里随便用一个,选 LED0 吧。
设置为推挽输出,其他的不重要。
到这里所有的配置就没了,重新生成工程即可。
要注意,因为重新生成了代码,前面LCD的源文件和头文件如果没了,需要重新添加源文件和头文件路径。
4. 软件代码
4.1. GUI 代码
先规划一下 LCD 显示界面,把LCD的驱动代码包装一下,方便绘制。
先看一下最终效果。网格效果应该不会再改了,还有一些静态的不需要刷新的文字,放在了函数void setBackGroundText(void);
里面,到最后根据布局再回来更改。
在icode
文件夹中再创建一个GUI
文件夹,在GUI
文件夹中创建gui.c
和gui.h
文件。并将源文件和头文件在keil中添加到工程。
gui.h
#ifndef __GUI_H
#define __GUI_H
/*
author:Haozi
Author URI:https://blog.csdn.net/weixin_46253745
Describe:对 LCD 的驱动文件进行了包装,方便绘制GUI
*/
#include "stdint.h" // uint16_t 定义
void drawLineWithColor(uint16_t startX, uint16_t startY, uint16_t endX, uint16_t endY, uint16_t color);
void drawStringWithColor(uint16_t startX, uint16_t startY, uint16_t width, uint8_t *p, uint16_t color);
void setBackGroundColor(void);
void drawNetwork(void);
void setBackGroundText(void);
#endif
gui.c
#include "gui.h"
#include "stdint.h" // uint16_t 定义
#include "lcd.h"
/*
author:Haozi
Author URI:https://blog.csdn.net/weixin_46253745
Describe:对 LCD 的驱动文件进行了包装,方便绘制GUI
*/
/* ===================================================== */
// 描述:画线函数
// 参数:
// 起始和结束的x y坐标。
// color:线的颜色
// 返回值:
/* ===================================================== */
void drawLineWithColor(uint16_t startX, uint16_t startY, uint16_t endX, uint16_t endY, uint16_t color)
{
POINT_COLOR = color;
LCD_DrawLine(startX, startY, endX, endY);
}
/* ===================================================== */
// 描述:显示字符串函数
// 参数:
// startX、startY:起始的x y坐标。
// width: 区域宽度。
// p: 字符串地址。
// color: 线的颜色
// 返回值:
/* ===================================================== */
void drawStringWithColor(uint16_t startX, uint16_t startY, uint16_t width, uint8_t *p, uint16_t color)
{
POINT_COLOR = color;
// 字符区域大小 和 字体大小 直接定死了
LCD_ShowString(startX, startY, width, 16, 16, p);
}
/* ===================================================== */
// 描述:设置LCD背景。设置显示方向为横向;背景颜色为黑色。
// 参数:
// 返回值:
/* ===================================================== */
void setBackGroundColor(void)
{
LCD_Display_Dir(1); // 设置LCD显示方向为横向
LCD_Clear(BLACK); // 清空LCD,用黑色覆盖
BACK_COLOR = BLACK; // 背景颜色
POINT_COLOR = YELLOW; // 线的颜色
}
/* ===================================================== */
// 描述:显示字符串函数
// 参数:
// startX、startY:起始的x y坐标。
// width: 区域宽度。
// p: 字符串地址。
// color: 线的颜色
// 返回值:
/* ===================================================== */
void drawNetwork(void)
{
uint16_t y = 0;
uint16_t x = 0;
for(x = 20; x < lcddev.width; x += 20)
{
for(y = 20; y < (lcddev.height - 20); y += 5)
{
LCD_Fast_DrawPoint(x, y, 0XAAAA);
}
}
for(y = 20; y < (lcddev.height - 20); y += 20)
{
for(x = 0 ; x < lcddev.width ; x += 5)
{
LCD_Fast_DrawPoint(x, y, 0xAAAA);
}
}
POINT_COLOR = 0X534c;
drawLineWithColor(0, lcddev.height / 2, lcddev.width, lcddev.height / 2, POINT_COLOR);
drawLineWithColor(lcddev.width / 2, 20, lcddev.width / 2, (lcddev.height - 20), POINT_COLOR);
LCD_DrawRectangle(0, 20, lcddev.width, (lcddev.height - 20)); // 矩形
}
/* ===================================================== */
// 描述:设置背景上静态的文字
// 参数:
// 返回值:
/* ===================================================== */
void setBackGroundText(void)
{
// 待定,最后根据布局再看吧。
}
然后在主函数中调用即可看到前面的效果。
// main函数上面
#include "lcd.h"
#include "gui.h"
// main函数中
// 1. 初始化LCD
LCD_Init();
// 2. 初始化 LCD 背景设置
setBackGroundColor(); // 设置背景颜色
drawNetwork(); // 绘制背景网络
setBackGroundText(); // 设置背景静态文字
4.2. 电压采集及绘制
这节是重点
这里设计ADC的采集方法并显示在上面的网格中。
先说思路,方便理解代码:
可以看到,按照这个思路,其实ADC并不是在一直连续的显示电压的变化,而是每隔一段时间截取一段,显示之后,再次截取,避免LCD的刷新速度影响。
在icode
文件夹中再创建一个OSC
文件夹,在OSC
文件夹中创建osc.c
和osc.h
文件。并将源文件和头文件在keil中添加到工程。
这里的代码有点长,放几个关键的说一下,全部代码主页置顶文章有。
初始化代码相对简单。
/* ===================================================== */
// 描述:示波器初始化
// 参数:
// 返回值:
/* ===================================================== */
void OSC_Init(void)
{
// 初始化 示波器 背景设置
setBackGroundColor(); // 设置背景颜色
setBackGroundText(); // 设置背景静态文字
// 1. adc校准
HAL_ADCEx_Calibration_Start(&hadc1);
// 2. 开启DMA传输
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&originalAdc, numOfCollect);
// 3. 关掉传输一半中断
__HAL_DMA_DISABLE_IT((&hadc1)->DMA_Handle, DMA_IT_HT);
// 4. 设置ADC采样频率。并开始计时采样
setAdcFrequency(numF);
HAL_TIM_Base_Start(&htim3);
}
DMA中断代码,这段其实时关键,中间的代码有点多,又分成了几个函数。
/* ===================================================== */
// 描述:DMA传输回调函数,用于绘制获取的ADC波形并输出提示信息。
// 参数:
// 返回值:
// 注意:在绘制期间,会关闭定时器,停止触发ADC。
// 可以看出,这里的逻辑是,
// 1. 打开定时器计时,定时器开始计数,可以触发中断;
// 2. 定时器触发中断,ADC获取连续的一段电压值;(在这里频率是可以设置的)
// 3. 关闭定时器计数,ADC停止转换;
// 4. 绘制刚刚获取的ADC波形,绘制完成之后,再次打开定时器。
//
// 简单来说,其实电压值并不是在连续转换。而是转换一段 停一会 再次转换一段。
/* ===================================================== */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
// 失能定时器3
HAL_TIM_Base_MspDeInit(&htim3);
if(oscState == 1)
{
// HAL_GPIO_TogglePin(LED0_GPIO_Port, LED0_Pin);
drawStringWithColor(240, 1, 120, "Running ", YELLOW);
/*
显示所有需要的内容
1. 绘制背景网格
2. 计算被测波形的频率;
3. 将波形显示在屏幕上;
4. 更新波形信息
*/
// 执行过程中 让LED亮
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
drawNetwork();
updateWaveFrequency();
OSC_ShowWave();
OSC_ShowInfo();
// 刷新完成之后 灭掉LED
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
}
else
{
drawStringWithColor(240, 1, 120, "Stopping", RED);
}
// 使能定时器3
HAL_TIM_Base_MspInit(&htim3);
}
中间四个函数的逻辑,就是上面流程图的逻辑,这里就不放了,可以直接去下载工程。全部代码主页置顶文章有
4.3. 主函数
在主函数中其实非常简单,初始化即可,剩下的都在 回调函数和中断中完成。
#include "lcd,h"
#include "osc,h"
// 1. 初始化LCD
LCD_Init();
// 2. 初始化示波器(核心代码)
OSC_Init();
// 3. 设置运行标志位
oscState = 1;
4.4. 测试工程(可有可无)
另找一个开发板,用定时器生成PWM波,进行测试。
我这里用另一个开发板的三个定时器,分别生成 100hz、1Khz、10Khz的PWM波。
在主函数中,打开定时器,随便设置个占空比,然后监测一下试试。
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_2);
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 4000);
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 8000);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 300);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 500);
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 20);
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_2, 60);
5. 使用说明及测试
把两个开发板的 GND 连起来。
把输出信号的开发板引脚,连到示波器开发板的PA1引脚。
按键功能:
- 长按 WK_UP 键,暂停刷新 / 重新启动;
- 短按 WK_UP 键,切换模式,被选中的模式会有一个方格,选中中间的偏移的话,中间的两根线会变成白色;
- KEY0、KEY1,在不同模式下,增加或减少。
下面信息含义:
- max:这个屏幕中最大的电压值;
- min:这个屏幕中最大的电压值;
- dif:这个屏幕中最大与最小电压的差;
- Hz:被测信号的频率。
上面信息的含义:
- 1V / 2V / 4V 显示的电压范围;
- 5ms:表示一个小方格(红色点组成的方格)对应的实际时常;
- [—|—]符号:波形左右偏移量。
- Running / Stoping:示波器状态。
LED 闪烁:亮表示正在测量及显示;灭表示已经在LCD上刷新了。
视频效果:https://www.bilibili.com/video/BV1AP411M75M/
1000HZ测试效果
10000HZ测试效果
正弦波测试效果
三角波测试效果
代码在主页置顶文章,或者点个关注点个赞,私聊我直接给你发也行。
2023年2月22日记。
刚刚调试电机PWM波,手贱想看看量12V电压会有什么情况,就碰了一下,只一瞬间。。。芯片烧了。。。
-100。
请勿尝试高电压!!!