文章目录
- 前言
- 一、项目整体原理与硬件架构
-
- [1.1 实现原理](#1.1 实现原理)
- [1.2 硬件器件清单](#1.2 硬件器件清单)
- [1.3 硬件接线定义](#1.3 硬件接线定义)
-
- [STM32F103 与 LCD 接线](#STM32F103 与 LCD 接线)
- ADC模拟输入接线
- [1.4 项目工作流程Mermaid流程图](#1.4 项目工作流程Mermaid流程图)
- 二、开发环境与工程准备
-
- [2.1 开发环境](#2.1 开发环境)
- [2.2 新建工程基础步骤](#2.2 新建工程基础步骤)
- 三、工程文件创建与代码实现
-
- [3.1 新建文件:lcd.h](#3.1 新建文件:lcd.h)
- [3.2 新建文件:lcd.c](#3.2 新建文件:lcd.c)
- [3.3 新建文件:adc_dma.h](#3.3 新建文件:adc_dma.h)
- [3.4 新建文件:adc_dma.c](#3.4 新建文件:adc_dma.c)
- [3.5 新建文件:main.c](#3.5 新建文件:main.c)
- 四、工程配置与编译烧录步骤
-
- [4.1 添加文件到工程分组](#4.1 添加文件到工程分组)
- [4.2 Keil编译配置](#4.2 Keil编译配置)
- [4.3 烧录与实物调试](#4.3 烧录与实物调试)
- 五、核心知识点讲解(小白必懂)
-
- [5.1 ADC部分关键点](#5.1 ADC部分关键点)
- [5.2 DMA部分关键点](#5.2 DMA部分关键点)
- [5.3 LCD波形绘制逻辑](#5.3 LCD波形绘制逻辑)
- 六、常见问题排查
- 七、项目拓展方向
前言
很多刚入门STM32的同学,都想做一个能自己采集电压波形、在LCD屏幕上实时显示的简易示波器项目。既能吃透ADC模数转换 、DMA直接内存访问 、LCD屏幕驱动三大核心知识点,又能做出看得见、摸得着的实物效果。
本文基于STM32F103C8T6最小系统板,采用ADC1通道采集模拟电压,配合DMA自动搬运ADC采样数据,无需CPU逐次读取,解放内核资源;再通过1.8寸/2.4寸TFT LCD屏幕实时绘制电压波形、标注电压刻度、时间刻度,零基础跟着一步步接线、配置工程、复制代码、烧录,即可直接跑通成品简易示波器。
一、项目整体原理与硬件架构
1.1 实现原理
- ADC模块 :将外部输入的03.3V模拟电压,转换成04095的12位数字量;
- DMA模块:配置ADC为连续转换模式,DMA自动把每一次ADC采样值搬运到自定义数组,CPU无需干预;
- 数据处理:定时读取DMA缓存数组,做电压换算、波形坐标映射;
- LCD显示:清空波形区域、绘制坐标轴、绘制实时电压波形、显示当前电压数值、量程标注。
1.2 硬件器件清单
- STM32F103C8T6 最小系统板 1块
- 1.8寸 SPI TFT LCD 屏幕(分辨率128*160)1块
- 杜邦线若干
- 电位器1个(用来输入可变模拟电压,调试波形)
- 5V/3.3V电源
1.3 硬件接线定义
STM32F103 与 LCD 接线
| STM32引脚 | LCD引脚 | 功能 |
|---|---|---|
| PA5 | SCK | 串行时钟 |
| PA7 | MOSI | 串行数据 |
| PA6 | RES | 复位 |
| PB0 | DC | 数据/命令选择 |
| PB1 | CS | 片选 |
| 3.3V | VCC | 供电 |
| GND | GND | 地线 |
ADC模拟输入接线
- ADC1_IN0 对应 PA0 引脚
- 电位器中间引脚接PA0,两端分别接3.3V和GND,用来输出0~3.3V可变电压作为示波器输入信号。
1.4 项目工作流程Mermaid流程图
系统初始化
GPIO初始化
LCD屏幕初始化
ADC1初始化 12位分辨率
DMA1初始化 绑定ADC1
开启ADC连续转换
DMA自动搬运采样数据到缓存数组
读取数组数据 换算实际电压
坐标映射 适配LCD屏幕分辨率
绘制坐标轴+网格刻度
实时绘制电压波形曲线
刷新LCD 循环采集显示
二、开发环境与工程准备
2.1 开发环境
- 编译软件:Keil MDK5
- 芯片库:STM32F103 标准库 V3.5
- 仿真器:ST-Link / J-Link
- 烧录方式:串口烧录 / ST-Link 在线烧录
2.2 新建工程基础步骤
- 打开Keil MDK5,新建Project,命名为
STM32F103_Scope_ADC_DMA_LCD; - 选择芯片型号
STM32F103C8; - 导入标准库核心文件:
stm32f10x_gpio.c、stm32f10x_adc.c、stm32f10x_dma.c、stm32f10x_rcc.c、misc.c; - 新建分组:
USER、ADC、DMA、LCD、SYSTEM,方便文件管理; - 配置编译优化等级为O1,开启HEX文件生成。
三、工程文件创建与代码实现
下面按模块逐个创建文件,每个文件给出完整可直接复制的代码,小白只需新建对应文件名,粘贴代码即可。
3.1 新建文件:lcd.h
c
#ifndef __LCD_H
#define __LCD_H
#include "stm32f10x.h"
// LCD 引脚定义
#define LCD_SCK_PIN GPIO_Pin_5
#define LCD_SCK_PORT GPIOA
#define LCD_MOSI_PIN GPIO_Pin_7
#define LCD_MOSI_PORT GPIOA
#define LCD_RES_PIN GPIO_Pin_6
#define LCD_RES_PORT GPIOA
#define LCD_DC_PIN GPIO_Pin_0
#define LCD_DC_PORT GPIOB
#define LCD_CS_PIN GPIO_Pin_1
#define LCD_CS_PORT GPIOB
#define LCD_Width 128
#define LCD_Height 160
// 函数声明
void LCD_Init(void);
void LCD_Clear(uint16_t color);
void LCD_DrawPoint(uint16_t x,uint16_t y,uint16_t color);
void LCD_DrawLine(uint16_t x1,uint16_t y1,uint16_t x2,uint16_t y2,uint16_t color);
void LCD_ShowString(uint16_t x,uint16_t y,char *str);
void LCD_ShowNum(uint16_t x,uint16_t y,uint16_t num,uint8_t len);
void LCD_DrawAxis(void);
#endif
3.2 新建文件:lcd.c
c
#include "lcd.h"
#define WHITE 0xFFFF
#define BLACK 0x0000
#define BLUE 0x001F
#define RED 0xF800
#define GREEN 0x07E0
// 引脚高低电平操作
#define LCD_SCK_L GPIO_ResetBits(LCD_SCK_PORT,LCD_SCK_PIN)
#define LCD_SCK_H GPIO_SetBits(LCD_SCK_PORT,LCD_SCK_PIN)
#define LCD_MOSI_L GPIO_ResetBits(LCD_MOSI_PORT,LCD_MOSI_PIN)
#define LCD_MOSI_H GPIO_SetBits(LCD_MOSI_PORT,LCD_MOSI_PIN)
#define LCD_RES_L GPIO_ResetBits(LCD_RES_PORT,LCD_RES_PIN)
#define LCD_RES_H GPIO_SetBits(LCD_RES_PORT,LCD_RES_PIN)
#define LCD_DC_L GPIO_ResetBits(LCD_DC_PORT,LCD_DC_PIN)
#define LCD_DC_H GPIO_SetBits(LCD_DC_PORT,LCD_DC_PIN)
#define LCD_CS_L GPIO_ResetBits(LCD_CS_PORT,LCD_CS_PIN)
#define LCD_CS_H GPIO_SetBits(LCD_CS_PORT,LCD_CS_PIN)
// 延时函数
void LCD_Delay(uint32_t t)
{
while(t--);
}
// SPI 写一个字节
void LCD_WriteByte(uint8_t dat)
{
uint8_t i;
LCD_CS_L;
for(i=0;i<8;i++)
{
LCD_SCK_L;
if(dat & 0x80)
LCD_MOSI_H;
else
LCD_MOSI_L;
dat <<= 1;
LCD_SCK_H;
}
LCD_CS_H;
}
// 写命令
void LCD_WriteCmd(uint8_t cmd)
{
LCD_DC_L;
LCD_WriteByte(cmd);
}
// 写数据
void LCD_WriteData(uint8_t dat)
{
LCD_DC_H;
LCD_WriteByte(dat);
}
// 设置窗口地址
void LCD_SetWindows(uint16_t x1,uint16_t y1,uint16_t x2,uint16_t y2)
{
LCD_WriteCmd(0x2A);
LCD_WriteData(x1>>8);
LCD_WriteData(x1&0xff);
LCD_WriteData(x2>>8);
LCD_WriteData(x2&0xff);
LCD_WriteCmd(0x2B);
LCD_WriteData(y1>>8);
LCD_WriteData(y1&0xff);
LCD_WriteData(y2>>8);
LCD_WriteData(y2&0xff);
LCD_WriteCmd(0x2C);
}
// LCD 初始化
void LCD_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB,ENABLE);
// PA5 PA6 PA7 初始化
GPIO_InitStruct.GPIO_Pin = LCD_SCK_PIN|LCD_MOSI_PIN|LCD_RES_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(LCD_SCK_PORT,&GPIO_InitStruct);
// PB0 PB1 初始化
GPIO_InitStruct.GPIO_Pin = LCD_DC_PIN|LCD_CS_PIN;
GPIO_Init(LCD_DC_PORT,&GPIO_InitStruct);
LCD_RES_H;
LCD_Delay(1000);
LCD_RES_L;
LCD_Delay(1000);
LCD_RES_H;
LCD_Delay(1000);
// 1.8寸LCD 初始化指令
LCD_WriteCmd(0x11);
LCD_Delay(120000);
LCD_WriteCmd(0x36);
LCD_WriteData(0x00);
LCD_WriteCmd(0x3A);
LCD_WriteData(0x55);
LCD_WriteCmd(0xB2);
LCD_WriteData(0x0C);
LCD_WriteData(0x0C);
LCD_WriteData(0x00);
LCD_WriteData(0x33);
LCD_WriteData(0x33);
LCD_WriteCmd(0xB7);
LCD_WriteData(0x35);
LCD_WriteCmd(0xC0);
LCD_WriteData(0x2C);
LCD_WriteCmd(0xC1);
LCD_WriteData(0x01);
LCD_WriteCmd(0xC2);
LCD_WriteData(0x01);
LCD_WriteCmd(0xC3);
LCD_WriteData(0x10);
LCD_WriteCmd(0xC4);
LCD_WriteData(0x20);
LCD_WriteCmd(0xC5);
LCD_WriteData(0x07);
LCD_WriteCmd(0xE0);
LCD_WriteData(0xD0);
LCD_WriteData(0x00);
LCD_WriteData(0x06);
LCD_WriteData(0x0F);
LCD_WriteData(0x17);
LCD_WriteData(0x1D);
LCD_WriteData(0x34);
LCD_WriteData(0x44);
LCD_WriteData(0x4C);
LCD_WriteData(0x53);
LCD_WriteData(0x2B);
LCD_WriteData(0x28);
LCD_WriteData(0x15);
LCD_WriteData(0x16);
LCD_WriteData(0x19);
LCD_WriteCmd(0xE1);
LCD_WriteData(0xD0);
LCD_WriteData(0x00);
LCD_WriteData(0x06);
LCD_WriteData(0x0F);
LCD_WriteData(0x17);
LCD_WriteData(0x1D);
LCD_WriteData(0x34);
LCD_WriteData(0x44);
LCD_WriteData(0x4C);
LCD_WriteData(0x53);
LCD_WriteData(0x2B);
LCD_WriteData(0x28);
LCD_WriteData(0x15);
LCD_WriteData(0x16);
LCD_WriteData(0x19);
LCD_WriteCmd(0x29);
LCD_Clear(BLACK);
}
// 清屏
void LCD_Clear(uint16_t color)
{
uint16_t i,j;
LCD_SetWindows(0,0,LCD_Width-1,LCD_Height-1);
for(i=0;i<LCD_Width;i++)
{
for(j=0;j<LCD_Height;j++)
{
LCD_WriteData(color>>8);
LCD_WriteData(color&0xff);
}
}
}
// 画点
void LCD_DrawPoint(uint16_t x,uint16_t y,uint16_t color)
{
LCD_SetWindows(x,y,x,y);
LCD_WriteData(color>>8);
LCD_WriteData(color&0xff);
}
// 画线
void LCD_DrawLine(uint16_t x1,uint16_t y1,uint16_t x2,uint16_t y2,uint16_t color)
{
uint16_t i;
int16_t dx,dy,sx,sy,err,e2;
dx = x2 - x1;
dy = y2 - y1;
if(dx>0) sx=1; else sx=-1;
if(dy>0) sy=1; else sy=-1;
dx = abs(dx);
dy = abs(dy);
err = dx - dy;
while(1)
{
LCD_DrawPoint(x1,y1,color);
if(x1==x2 && y1==y2) break;
e2 = 2*err;
if(e2 > -dy) {err -= dy; x1 += sx;}
if(e2 < dx) {err += dx; y1 += sy;}
}
}
// 绘制坐标轴
void LCD_DrawAxis(void)
{
// X轴 水平中线
LCD_DrawLine(10,80,120,80,WHITE);
// Y轴 垂直起点
LCD_DrawLine(10,20,10,140,WHITE);
// 网格刻度
uint16_t x;
for(x=10;x<=120;x+=10)
{
LCD_DrawLine(x,78,x,82,WHITE);
}
uint16_t y;
for(y=20;y<=140;y+=20)
{
LCD_DrawLine(8,y,12,y,WHITE);
}
}
3.3 新建文件:adc_dma.h
c
#ifndef __ADC_DMA_H
#define __ADC_DMA_H
#include "stm32f10x.h"
#define ADC_BUF_LEN 128
extern uint16_t adc_buf[ADC_BUF_LEN];
void ADC_DMA_Init(void);
#endif
3.4 新建文件:adc_dma.c
c
#include "adc_dma.h"
uint16_t adc_buf[ADC_BUF_LEN];
// ADC+DMA 初始化
void ADC_DMA_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
ADC_InitTypeDef ADC_InitStruct;
DMA_InitTypeDef DMA_InitStruct;
// 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_ADC1,ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
// PA0 模拟输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA,&GPIO_InitStruct);
// ADC 时钟分频 6分频
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
// ADC1 基础配置
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;
ADC_InitStruct.ADC_ScanConvMode = DISABLE;
ADC_InitStruct.ADC_ContinuousConvMode = ENABLE;
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStruct.ADC_NbrOfChannel = 1;
ADC_Init(ADC1,&ADC_InitStruct);
// 配置ADC通道0 规则组
ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_239Cycles5);
// DMA1 通道1配置 对应ADC1
DMA_DeInit(DMA1_Channel1);
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)adc_buf;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStruct.DMA_BufferSize = ADC_BUF_LEN;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;
DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel1,&DMA_InitStruct);
// 使能DMA
DMA_Cmd(DMA1_Channel1,ENABLE);
// ADC DMA 使能
ADC_DMACmd(ADC1,ENABLE);
// 开启ADC1
ADC_Cmd(ADC1,ENABLE);
// 校准ADC
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
// 启动ADC转换
ADC_SoftwareStartConvCmd(ADC1,ENABLE);
}
3.5 新建文件:main.c
c
#include "stm32f10x.h"
#include "lcd.h"
#include "adc_dma.h"
// 简单延时
void Delay_ms(uint32_t ms)
{
uint32_t i,j;
for(i=ms;i>0;i--)
for(j=110;j>0;j--);
}
// ADC原始值转实际电压 0~3.3V
float ADC_Get_Voltage(uint16_t adc_val)
{
return adc_val * 3.3f / 4095.0f;
}
int main(void)
{
uint16_t i;
uint16_t y_pos;
float vol;
// 初始化
LCD_Init();
ADC_DMA_Init();
// 绘制坐标轴
LCD_DrawAxis();
while(1)
{
// 遍历采样数组 绘制波形
for(i=0;i<ADC_BUF_LEN;i++)
{
// 电压映射到LCD Y坐标 20~140
vol = ADC_Get_Voltage(adc_buf[i]);
// 3.3V对应20 0V对应140 线性映射
y_pos = 140 - (uint16_t)(vol / 3.3f * 120.0f);
if(y_pos < 20) y_pos = 20;
if(y_pos > 140) y_pos = 140;
// 绘制波形点
LCD_DrawPoint(10+i,y_pos,GREEN);
}
Delay_ms(50);
// 清除波形区域 保留坐标轴
LCD_SetWindows(11,21,120,139);
for(i=0;i<110;i++)
{
for(uint16_t j=0;j<118;j++)
{
LCD_WriteData(0x00);
LCD_WriteData(0x00);
}
}
}
}
四、工程配置与编译烧录步骤
4.1 添加文件到工程分组
- 将
lcd.h、lcd.c加入LCD分组; - 将
adc_dma.h、adc_dma.c加入ADC分组; main.c加入USER分组;- 把标准库
stm32f10x_adc.c、stm32f10x_dma.c、stm32f10x_gpio.c、stm32f10x_rcc.c、misc.c加入LIB分组。
4.2 Keil编译配置
- 点击魔法棒 -> Target 选择晶振8MHz;
- Output 勾选 Create HEX File;
- C/C++ 加入标准库头文件路径;
- 点击编译,0错误0警告即可。
4.3 烧录与实物调试
- 按照前文接线表,严格接好LCD、PA0电位器;
- ST-Link连接开发板,下载程序;
- 上电后LCD自动显示黑色背景、白色坐标轴;
- 旋转电位器,屏幕绿色波形会跟随电压上下波动,实现简易示波器效果。
五、核心知识点讲解(小白必懂)
5.1 ADC部分关键点
- STM32F103 ADC为12位,分辨率04095,对应03.3V;
- 配置为连续转换模式,不用每次手动触发;
- 采样时间设置为239.5周期,兼顾精度和速度。
5.2 DMA部分关键点
- DMA1通道1固定绑定ADC1外设;
- 配置为循环模式,自动反复填充缓存数组;
- 内存地址自增,外设地址不增量,自动搬运每一次ADC结果。
5.3 LCD波形绘制逻辑
- X轴对应采样点序号,128个点铺满横轴;
- Y轴将03.3V电压线性映射到LCD屏幕20140像素;
- 每次刷新只清空波形区域,保留坐标轴不重绘,刷新更流畅。
六、常见问题排查
- LCD不亮:检查SPI接线、复位引脚、屏幕供电,确认引脚定义和代码一致;
- 无波形变化:检查PA0是否接电位器,ADC时钟是否开启,DMA是否使能;
- 波形乱跳:增大ADC采样时间,电源增加104电容滤波;
- 编译报错:缺少标准库文件、头文件路径未添加,对照本文文件补齐即可。
七、项目拓展方向
- 增加按键切换电压量程(03.3V、05V分压);
- 添加频率计算,自动测量输入信号频率;
- 改成双通道ADC,实现双通道示波器;
- 加入串口上传波形数据到电脑上位机。