第一章:万用表项目概述

做嵌入式这么多年,我一直觉得万用表是个特别有意思的项目。它不复杂,但五脏俱全——ADC采集、量程切换、数据处理、显示刷新,每个环节都能讲出不少门道。今天咱们就从零开始,把这个项目拆开揉碎了讲。

1.1 项目背景:为什么选万用表?

说实话,我刚开始带团队时,新人上手总喜欢做流水灯或者温湿度计。这些项目太简单了,学不到真正的状态机思维。万用表不一样——它需要处理多个测量模式、自动量程切换、数据滤波,这些恰好是状态机设计的绝佳案例。

我记得有一次,一个学员拿着他做的万用表原型给我看。功能都能跑,但切换量程时屏幕会闪一下,偶尔还会跳出一个错误值。我一看代码,好家伙,所有逻辑都写在了一个大while循环里,用一堆if-else硬撑。这就是典型的「能跑但不好用」——说白了,缺的就是状态机设计。

核心价值:通过万用表项目,你将掌握嵌入式固件中80%的常见设计模式——状态机、环形缓冲区、量程自适应、数据滤波。这些技能放到任何项目里都通用。

1.2 功能需求:我们要做什么?

先列一下功能清单。嗯,这里要注意,需求不是越多越好,关键是「够用且可扩展」。

功能模块 具体需求 优先级
直流电压测量 0~30V,分辨率1mV,自动量程 P0(必须)
电阻测量 0~10MΩ,分辨率0.1Ω,自动量程 P0
通断测试 电阻<50Ω时蜂鸣器响,响应<100ms P0
二极管测试 正向压降0~2V,分辨率1mV P1(重要)
数据保持 按HOLD键冻结当前读数 P1
自动关机 15分钟无操作自动断电 P2(锦上添花)

你可能会问:「为什么通断测试要100ms响应?」我在项目中遇到过类似问题——当时客户反馈蜂鸣器总是慢半拍,排查后发现是ADC采样周期太长。后来我改用比较器硬件实现,响应直接降到10ms以内。这个坑,咱们后面讲硬件选型时会细说。

1.3 硬件选型:STM32 + ADS1115

选型这事儿,我个人的习惯是「够用就好,留有余量」。咱们这个项目,核心器件就三个:主控、ADC、显示。

主控:STM32F103C8T6

为什么选它?说白了,这颗芯片已经被市场验证了十年,资料多、坑少、价格便宜。你想想看,淘宝上10块钱就能买到一块最小系统板,还要什么自行车?

  • 内核:Cortex-M3,72MHz主频,做万用表绰绰有余
  • 存储:64KB Flash + 20KB RAM,够放状态机和数据缓冲区
  • 外设:I2C、SPI、UART、定时器,该有的都有

我的建议:如果你手头有STM32G0系列,也可以替代。G0的功耗更低,适合电池供电的便携设备。不过教程里我还是用F103,因为大家最熟悉。

ADC:ADS1115

ADS1115是一颗16位精度的I2C接口ADC。我最早用STM32内置的12位ADC做过一版,结果发现分辨率根本不够——测量1mV级别的电压变化时,跳变太明显了。后来换成ADS1115,问题迎刃而解。

参数 ADS1115 STM32内置ADC
分辨率 16位(实际有效位约15位) 12位
采样率 最高860SPS 最高1MSPS
输入范围 ±6.144V(可编程增益) 0~3.3V
接口 I2C 直接连接

这里有个关键点:ADS1115的输入范围可以通过PGA(可编程增益放大器)调整。比如测量小信号时,设置PGA为±0.256V,分辨率能达到7.8μV——这个精度,做热电偶测温都够了。

避坑指南:我曾经在ADS1115的I2C地址上栽过跟头。它的地址引脚ADDR接GND时是0x48,接VDD时是0x49,接SCL时是0x4A,接SDA时是0x4B。如果你发现读不到数据,先检查这个引脚的电平。

其他关键器件

  • 显示:0.96寸OLED(SSD1306),I2C接口,128x64分辨率
  • 输入:旋转编码器(带按键)+ 3个独立按键(HOLD、RANGE、MODE)
  • 电源:3.7V锂电池 + TP4056充电模块 + ME6206稳压到3.3V

1.4 开发环境搭建

环境搭建这事儿,说简单也简单,说麻烦也麻烦。我见过太多新手卡在第一步——Keil装不上、驱动打不了、下载器不识别。咱们一步步来。

工具链清单

  1. IDE:Keil MDK-ARM V5.38(我用的是这个版本,稳定)
  2. 编译器:ARMCC V6(AC6)——比AC5编译快30%,代码密度也更好
  3. 调试器:ST-Link V2(淘宝15块钱那种就行)
  4. 串口工具:Putty 或 SSCOM(调试时打印日志用)

工程模板创建步骤

我个人习惯用STM32CubeMX生成初始化代码,然后手动添加应用层。这样既省事,又能保证底层配置不出错。

// 工程目录结构(我的习惯)
project/
├── Core/           // 主程序
│   ├── Inc/        // 头文件
│   └── Src/        // 源文件
├── Drivers/        // 外设驱动
│   ├── BSP/        // 板级支持包(OLED、编码器、蜂鸣器)
│   └── HAL_Driver/ // ST官方HAL库
├── Middlewares/     // 中间件(ADS1115驱动、数据滤波算法)
└── User/           // 用户代码(状态机、应用逻辑)

小技巧:在Keil里把优化等级设为-O2,但记得把ADS1115的I2C读写函数单独设为-O0。为什么?因为优化器有时会把I2C的时序搞乱,导致通信失败。这个坑我踩过两次,后来学乖了。

验证环境是否搭好

写一个最简单的点灯程序,让LED以1Hz频率闪烁。如果灯亮了,说明编译、下载、运行都没问题。然后写一个I2C扫描程序,看看能不能读到ADS1115的0x48地址。能读到,恭喜你,环境搭好了。

// I2C扫描示例代码
void I2C_Scan(void)
{
    for (uint8_t addr = 0x01; addr < 0x7F; addr++)
    {
        if (HAL_I2C_IsDeviceReady(&hi2c1, addr << 1, 1, 10) == HAL_OK)
        {
            printf("Found device at 0x%02X\r\n", addr);
        }
    }
}

嗯,到这里,项目背景、需求、硬件选型、环境搭建就都讲完了。下一章咱们开始设计状态机——这才是万用表固件的灵魂所在。