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. 加入串口上传波形数据到电脑上位机。
相关推荐
蓝斯4971 天前
从.到实现一个Windows上的虚拟hid键盘设备
windows·单片机·计算机外设
熙芯XiChip1 天前
硬实时与软实时RTOS核心区别
嵌入式硬件
伟大的前程1 天前
unsigned int 定义的变量 在 电脑的 64 位系统中 和 32 位 cortex-m0 和 8 位的 51 单片机
单片机·嵌入式硬件
小渔村的拉线工1 天前
1.HPM6E80 解析芯片整体工作原理和存储架构
单片机·嵌入式硬件·mcu·架构·hpm6e80
熙芯XiChip1 天前
CAN与CANopen的本质区别
单片机
lilihewo1 天前
Altium Designer 封装库的创建(分享pcb 3D封装库)
嵌入式硬件·51单片机
神罚天下ET1 天前
典型arm位单片机启动流程(从上电到main.c)
c语言·arm开发·单片机
碧海银沙音频科技研究院1 天前
蓝牙耳机使用WSDF5361锂保芯片进入船运模式实现方法
嵌入式硬件·算法
Zevalin爱灰灰1 天前
模拟信号链电路——基于TL431的电压基准电路
单片机·嵌入式硬件
csdn杰哥1 天前
瑞萨单片机开发教程[二十七] DTC 外部中断触发传输实验
单片机·嵌入式硬件·mongodb·瑞萨·ra6m5