STM32实战:基于STM32F103的简易示波器(ADC+DMA+LCD)

文章目录

前言

很多刚入门STM32的同学,都想做一个能自己采集电压波形、在LCD屏幕上实时显示的简易示波器项目。既能吃透ADC模数转换DMA直接内存访问LCD屏幕驱动三大核心知识点,又能做出看得见、摸得着的实物效果。

本文基于STM32F103C8T6最小系统板,采用ADC1通道采集模拟电压,配合DMA自动搬运ADC采样数据,无需CPU逐次读取,解放内核资源;再通过1.8寸/2.4寸TFT LCD屏幕实时绘制电压波形、标注电压刻度、时间刻度,零基础跟着一步步接线、配置工程、复制代码、烧录,即可直接跑通成品简易示波器。

一、项目整体原理与硬件架构

1.1 实现原理

  1. ADC模块 :将外部输入的03.3V模拟电压,转换成04095的12位数字量;
  2. DMA模块:配置ADC为连续转换模式,DMA自动把每一次ADC采样值搬运到自定义数组,CPU无需干预;
  3. 数据处理:定时读取DMA缓存数组,做电压换算、波形坐标映射;
  4. 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 新建工程基础步骤

  1. 打开Keil MDK5,新建Project,命名为STM32F103_Scope_ADC_DMA_LCD
  2. 选择芯片型号 STM32F103C8
  3. 导入标准库核心文件:stm32f10x_gpio.cstm32f10x_adc.cstm32f10x_dma.cstm32f10x_rcc.cmisc.c
  4. 新建分组:USERADCDMALCDSYSTEM,方便文件管理;
  5. 配置编译优化等级为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 添加文件到工程分组

  1. lcd.hlcd.c 加入LCD分组;
  2. adc_dma.hadc_dma.c 加入ADC分组;
  3. main.c 加入USER分组;
  4. 把标准库stm32f10x_adc.cstm32f10x_dma.cstm32f10x_gpio.cstm32f10x_rcc.cmisc.c 加入LIB分组。

4.2 Keil编译配置

  1. 点击魔法棒 -> Target 选择晶振8MHz;
  2. Output 勾选 Create HEX File;
  3. C/C++ 加入标准库头文件路径;
  4. 点击编译,0错误0警告即可。

4.3 烧录与实物调试

  1. 按照前文接线表,严格接好LCD、PA0电位器;
  2. ST-Link连接开发板,下载程序;
  3. 上电后LCD自动显示黑色背景、白色坐标轴;
  4. 旋转电位器,屏幕绿色波形会跟随电压上下波动,实现简易示波器效果。

五、核心知识点讲解(小白必懂)

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像素;
  • 每次刷新只清空波形区域,保留坐标轴不重绘,刷新更流畅。

六、常见问题排查

  1. LCD不亮:检查SPI接线、复位引脚、屏幕供电,确认引脚定义和代码一致;
  2. 无波形变化:检查PA0是否接电位器,ADC时钟是否开启,DMA是否使能;
  3. 波形乱跳:增大ADC采样时间,电源增加104电容滤波;
  4. 编译报错:缺少标准库文件、头文件路径未添加,对照本文文件补齐即可。

七、项目拓展方向

  1. 增加按键切换电压量程(03.3V、05V分压);
  2. 添加频率计算,自动测量输入信号频率;
  3. 改成双通道ADC,实现双通道示波器;
  4. 加入串口上传波形数据到电脑上位机。
相关推荐
小灰灰搞电子2 小时前
rt-thread UART串口使用详解
单片机·嵌入式硬件·串口
洲洲不是州州2 小时前
单片机onenet云平台的万能APP
单片机·onenet·app·嵌入式·云平台
钿驰科技2 小时前
无刷电机的驱动原理及驱动电路解析
单片机·嵌入式硬件
木木_王3 小时前
嵌入式学习 | STM32裸板驱动开发(Day01)入门学习笔记(超详细完整版|点灯实验 + 库函数代码 + 原理全解)
linux·驱动开发·笔记·stm32·学习
小锋学长生活大爆炸3 小时前
【教程】树莓派驱动 0.96 寸 SSD1315 OLED 屏幕完整指南
单片机·嵌入式硬件·嵌入式·教程·树莓派·oled·屏幕
ye150127774554 小时前
12V-24V升110V升压转换WT3207
单片机·嵌入式硬件·其他·硬件工程
yong99904 小时前
基于 STM32 的数字控制实现双向 DC-DC 电源
stm32·单片机·嵌入式硬件
12.=0.5 小时前
【stm32_9】RTOS的概念、种类对比,FressRTOS的概述、FressRTOS的源码结构、FressRTOS的源码移植
stm32·单片机·嵌入式硬件
Yeats_Liao5 小时前
智能感知低功耗设计:MCU上的AI异常检测与能效优化
人工智能·单片机·物联网·neo4j