零知IDE——STM32F407VET6与ADS1115模数转换器实现多通道数据采集显示系统
✔零知IDE 是一个真正属于国人自己的开源软件平台,在开发效率上超越了Arduino平台并且更加容易上手,大大降低了开发难度。零知开源在软件方面提供了完整的学习教程和丰富示例代码,让不懂程序的工程师也能非常轻而易举的搭建电路来创作产品,测试产品。快来动手试试吧!
✔访问零知实验室,获取更多实战项目和教程资源吧!
www.lingzhilab.com
目录
一、硬件连接部分
1.1 硬件清单
1.2 接线方案
1.3 具体接线图
1.4 连接实物图
二、代码讲解部分
2.1 数据结构与定义
2.2 核心数据采集
2.3 实时数据显示
2.4 结果排序和显示
2.5 完整代码
三、项目结果演示
3.1 操作流程
3.2 数据记录
3.3 视频演示
四、ADS1115工作原理详解
4.1 内部架构与功能
4.2 I2C通信协议
4.3 寄存器映射
五、常见问题解答
Q1: ADS1115读取数据不稳定怎么办?
Q2: 显示屏出现闪烁如何解决?
Q3: 如何提高ADC采集精度?
(1)项目概述
本项目基于STM32F407VET6微控制器和ADS1115 16位高精度模数转换器,设计了一个多功能数据采集与显示系统。系统通过STM32F407VET6控制ADS1115采集四路模拟信号,并将采集到的数据实时显示在240×240的ST7789彩色LCD显示屏上。项目展示了高精度ADC数据采集、实时数据显示、多通道信号处理等关键技术。
(2)项目难点及解决方案
问题描述:多任务实时处理
解决方案:优化程序结构,采用状态机模式管理不同显示界面
一、硬件连接部分
1.1 硬件清单
组件 | 型号 | 数量 | 备注 |
---|---|---|---|
主控板 | STM32F407VET6 | 1 | 零知增强板 |
ADC模块 | ADS1115 | 1 | 16位精度 |
显示屏 | ST7789 | 1 | 240×240分辨率 |
电位器 | 10kΩ | 1 | 模拟信号输入 |
连接线 | 杜邦线 | 若干 | 信号连接 |
1.2 接线方案
STM32F407引脚 | ADS1115引脚 | ST7789引脚 | 功能描述 |
---|---|---|---|
3.3V | VDD | VCC | 电源正极 |
GND | GND | GND | 电源地 |
52 | SCL | SCL | I2C时钟 |
51 | SDA | SDA | I2C数据 |
/ | A0 | / | 模拟输入0 |
/ | A1 | / | 模拟输入1 |
/ | A2 | / | 模拟输入2 |
/ | A3 | / | 模拟输入3 |
4 | / | DC | 数据/命令 |
2 | / | RST | 复位 |
53 | / | CS | 片选 |
注意:本项目ADS1117模数转换器的引脚连接如下,A0和A2连接到3.3V电源、A1连接电位器调节ADC值、A3接到零知增强板任一模拟引脚观察悬空引脚模拟值变化
1.3 具体接线图
1.4 连接实物图
二、代码讲解部分
2.1 数据结构与定义
// 定义ST7789显示屏引脚
#define TFT_CS 53
#define TFT_RST 2
#define TFT_DC 4// 创建ADS1115和ST7789对象
Adafruit_ADS1115 ads;
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);// 游戏相关变量
struct Car {int16_t x; // 赛车x坐标int16_t y; // 赛车y坐标uint16_t color; // 赛车颜色int16_t lastADC; // 上一次ADC值int16_t speed; // 当前速度bool finished; // 是否完成比赛uint32_t finishTime; // 完成时间
};Car cars[4]; // 四辆赛车
bool gameStarted = false;
bool gameFinished = false;
uint32_t startTime = 0;
const int16_t FINISH_LINE = 200; // 终点线位置// A1和A3通道数据显示变量
int16_t lastA1ADC = -1;
float lastA1Voltage = -1;
int16_t lastA3ADC = -1;
float lastA3Voltage = -1;// 颜色定义
#define CAR1_COLOR ST77XX_RED
#define CAR2_COLOR ST77XX_GREEN
#define CAR3_COLOR ST77XX_BLUE
#define CAR4_COLOR ST77XX_YELLOW
#define TRACK_COLOR 0x7453
#define BACKGROUND_COLOR ST77XX_BLACK
#define DATA_BG_COLOR 0x1082
#define DATA_TEXT_COLOR ST77XX_WHITE
ST7789显示屏引脚定义到零知增强板的硬件SPI接口、Car结构体封装了每个通道的显示属性和状态、每个通道对应特定颜色
2.2 核心数据采集
void updateGame() {bool allFinished = true;float ret;// 读取A1和A3通道数据并更新显示updateChannelDataDisplay();for (int i = 0; i < 4; i++) {if (!cars[i].finished) {// 读取ADC值并转换为速度int16_t adc = ads.readADC_SingleEnded(i);cars[i].speed = map(adc, 0, 26000, 1, 10);// 更新赛车位置cars[i].x += cars[i].speed;// 检查是否到达终点if (cars[i].x >= FINISH_LINE) {cars[i].finished = true;cars[i].finishTime = millis() - startTime;cars[i].x = FINISH_LINE;} else {allFinished = false;}updateCarDisplay(i);// 串口调试信息(仅显示A1通道)if (i == 1) {Serial.print("ADC");Serial.print(i);Serial.print(": ");Serial.print(adc);Serial.print(" | Voltage:");ret = adc * 0.0001875; Serial.print(ret); Serial.print(" | Speed: ");Serial.print(cars[i].speed);Serial.print(" | Position: ");Serial.println(cars[i].x);}}}if (allFinished && !gameFinished) {gameFinished = true;showResults();}
}
电压计算公式采用:adc * 0.0001875基于6.144V量程的分辨率
2.3 实时数据显示
void updateChannelDataDisplay() {// 读取A1和A3通道数据int16_t a1ADC = ads.readADC_SingleEnded(1);float a1Voltage = a1ADC * 0.0001875;int16_t a3ADC = ads.readADC_SingleEnded(3);float a3Voltage = a3ADC * 0.0001875;// 只在新数据与旧数据不同时更新显示,避免闪烁if (a1ADC != lastA1ADC) {displayChannelData(1, a1ADC, a1Voltage, 210);lastA1ADC = a1ADC;lastA1Voltage = a1Voltage;}if (a3ADC != lastA3ADC) {displayChannelData(3, a3ADC, a3Voltage, 225);lastA3ADC = a3ADC;lastA3Voltage = a3Voltage;}
}
仅当ADC值变化时更新显示,避免不必要的屏幕刷新
2.4 结果排序和显示
void showResults() {// ... 显示标题等代码 ...// 确定名次int rankings[4] = {0, 1, 2, 3};// 冒泡排序按完成时间排序for (int i = 0; i < 3; i++) {for (int j = 0; j < 3 - i; j++) {if (cars[rankings[j]].finishTime > cars[rankings[j + 1]].finishTime) {int temp = rankings[j];rankings[j] = rankings[j + 1];rankings[j + 1] = temp;}}}// 显示排序结果int yPos = 60;for (int i = 0; i < 4; i++) {int carIndex = rankings[i];tft.setCursor(30, yPos);tft.print("Channel ");tft.print(carIndex);tft.print(": ");tft.fillRect(90, yPos, 10, 10, cars[carIndex].color);tft.setCursor(105, yPos);tft.print("Time: ");tft.print(cars[carIndex].finishTime / 1000.0, 2);tft.print("s");yPos += 30;}
}
相邻元素比较交换,最大元素"冒泡"到末尾。时间复杂度为O(n²),适合小数据量排序
2.5 完整代码
// 文件名:STM32F407_ADS1115_DataDisplay.ino#include <Wire.h>
#include <Adafruit_ADS1X15.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>// 定义ST7789显示屏引脚
#define TFT_CS 53
#define TFT_RST 2
#define TFT_DC 4// 创建ADS1115和ST7789对象
Adafruit_ADS1115 ads;
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);// 游戏相关变量
struct Car {int16_t x; // 赛车x坐标int16_t y; // 赛车y坐标uint16_t color; // 赛车颜色int16_t lastADC; // 上一次ADC值int16_t speed; // 当前速度bool finished; // 是否完成比赛uint32_t finishTime; // 完成时间
};Car cars[4]; // 四辆赛车
bool gameStarted = false;
bool gameFinished = false;
uint32_t startTime = 0;
const int16_t FINISH_LINE = 200; // 终点线位置// A1和A3通道数据显示变量
int16_t lastA1ADC = -1;
float lastA1Voltage = -1;
int16_t lastA3ADC = -1;
float lastA3Voltage = -1;// 颜色定义
#define CAR1_COLOR ST77XX_RED
#define CAR2_COLOR ST77XX_GREEN
#define CAR3_COLOR ST77XX_BLUE
#define CAR4_COLOR ST77XX_YELLOW
#define TRACK_COLOR 0x7453
#define BACKGROUND_COLOR ST77XX_BLACK
#define DATA_BG_COLOR 0x1082
#define DATA_TEXT_COLOR ST77XX_WHITEvoid setup(void) {Serial.begin(115200);Serial.println("STM32F407 ADS1115数据采集系统启动!");// 初始化ADS1115Wire.begin();if (!ads.begin()) {Serial.println("ADS1115初始化失败!");while (1);}ads.setGain(GAIN_TWOTHIRDS);// 初始化ST7789显示屏tft.init(240, 240);tft.setRotation(1); // 重要:设置屏幕方向tft.fillScreen(BACKGROUND_COLOR);// 初始化赛车initializeCars();// 显示开始界面showStartScreen();
}void loop(void) {if (!gameStarted) {// 等待开始信号(任意通道电压超过1V)if (checkStartCondition()) {startGame();}return;}if (gameFinished) {// 游戏结束,显示结果if (checkRestartCondition()) {resetGame();}return;}// 主游戏循环updateGame();delay(50); // 控制游戏速度
}void initializeCars() {// 初始化四辆赛车的属性int16_t startY[] = {40, 80, 120, 160};uint16_t colors[] = {CAR1_COLOR, CAR2_COLOR, CAR3_COLOR, CAR4_COLOR};for (int i = 0; i < 4; i++) {cars[i].x = 20;cars[i].y = startY[i];cars[i].color = colors[i];cars[i].lastADC = 0;cars[i].speed = 0;cars[i].finished = false;cars[i].finishTime = 0;}
}bool checkStartCondition() {// 检查任意通道电压是否超过1V作为开始信号for (int i = 0; i < 4; i++) {int16_t adc = ads.readADC_SingleEnded(i);float voltage = adc * 0.0001875;if (voltage > 1.0) {return true;}}return false;
}bool checkRestartCondition() {// 检查所有通道电压是否都低于0.5V作为重新开始信号for (int i = 0; i < 4; i++) {int16_t adc = ads.readADC_SingleEnded(i);float voltage = adc * 0.0001875;if (voltage > 0.5) {return false;}}return true;
}void startGame() {gameStarted = true;startTime = millis();drawGameScreen();
}void resetGame() {gameStarted = false;gameFinished = false;initializeCars();tft.fillScreen(BACKGROUND_COLOR);showStartScreen();
}void updateGame() {bool allFinished = true;float ret;// 读取A1和A3通道数据并更新显示updateChannelDataDisplay();for (int i = 0; i < 4; i++) {if (!cars[i].finished) {// 读取ADC值并转换为速度int16_t adc = ads.readADC_SingleEnded(i);cars[i].speed = map(adc, 0, 26000, 1, 10); // 映射ADC值到速度// 更新赛车位置cars[i].x += cars[i].speed;// 检查是否到达终点if (cars[i].x >= FINISH_LINE) {cars[i].finished = true;cars[i].finishTime = millis() - startTime;cars[i].x = FINISH_LINE; // 确保不会超出终点} else {allFinished = false;}// 更新显示updateCarDisplay(i);// 在串口显示调试信息if (i == 1) { // 显示A1通道的信息Serial.print("ADC");Serial.print(i);Serial.print(": ");Serial.print(adc);Serial.print(" | Voltage:");ret = adc * 0.0001875; Serial.print(ret); Serial.print(" | Speed: ");Serial.print(cars[i].speed);Serial.print(" | Position: ");Serial.println(cars[i].x);}}}// 检查游戏是否结束if (allFinished && !gameFinished) {gameFinished = true;showResults();}
}void updateChannelDataDisplay() {// 读取A1和A3通道数据int16_t a1ADC = ads.readADC_SingleEnded(1);float a1Voltage = a1ADC * 0.0001875;int16_t a3ADC = ads.readADC_SingleEnded(3);float a3Voltage = a3ADC * 0.0001875;// 只在新数据与旧数据不同时更新显示,避免闪烁if (a1ADC != lastA1ADC) {displayChannelData(1, a1ADC, a1Voltage, 210);lastA1ADC = a1ADC;lastA1Voltage = a1Voltage;}if (a3ADC != lastA3ADC) {displayChannelData(3, a3ADC, a3Voltage, 225);lastA3ADC = a3ADC;lastA3Voltage = a3Voltage;}
}void displayChannelData(int channel, int16_t adc, float voltage, int yPos) {// 设置文本颜色和大小tft.setTextColor(DATA_TEXT_COLOR);tft.setTextSize(1);// 清除旧数据区域(局部刷新)tft.fillRect(55, yPos, 180, 10, DATA_BG_COLOR);// 显示通道数据tft.setCursor(5, yPos);tft.print("A");tft.print(channel);tft.print(": ADC= ");tft.print(adc);tft.print(" | V=");tft.print(voltage, 3);tft.print("V");
}void updateCarDisplay(int carIndex) {// 清除旧的赛车位置(绘制背景色矩形)tft.fillRect(cars[carIndex].x - cars[carIndex].speed - 2, cars[carIndex].y - 8, cars[carIndex].speed + 4, 16, TRACK_COLOR);// 绘制新位置的赛车drawCar(carIndex);
}void drawCar(int carIndex) {// 绘制赛车(简单的矩形表示)tft.fillRoundRect(cars[carIndex].x, cars[carIndex].y - 6, 12, 12, 3, cars[carIndex].color);tft.fillRoundRect(cars[carIndex].x + 10, cars[carIndex].y - 4, 6, 8, 2, ST77XX_WHITE);
}void showStartScreen() {tft.setTextSize(3);tft.setTextColor(ST77XX_GREEN);tft.setCursor(20, 50);tft.println("Data Display");tft.setTextColor(ST77XX_WHITE);tft.setTextSize(1);tft.setCursor(20, 80);tft.println("Connect sensors to ADS1115:");tft.setCursor(40, 100);tft.println("Channel 0 - Red indicator");tft.setCursor(40, 115);tft.println("Channel 1 - Potentiometer");tft.setCursor(40, 130);tft.println("Channel 2 - Blue indicator");tft.setCursor(40, 145);tft.println("Channel 3 - Analog input");tft.setCursor(30, 170);tft.println("Adjust any channel voltage");tft.setCursor(50, 185);tft.println("to start data display!");delay(500);
}void drawGameScreen() {tft.fillScreen(BACKGROUND_COLOR);// 绘制赛道背景tft.fillRect(0, 30, 240, 160, TRACK_COLOR);// 绘制起点线for (int y = 30; y < 190; y += 10) {tft.drawFastVLine(20, y, 5, ST77XX_WHITE);}// 绘制终点线for (int y = 30; y < 190; y += 10) {tft.drawFastVLine(FINISH_LINE, y, 5, ST77XX_WHITE);}// 绘制数据背景区域tft.fillRect(0, 200, 240, 40, DATA_BG_COLOR);// 绘制数据标题tft.setTextColor(DATA_TEXT_COLOR);tft.setTextSize(1);// 初始化A1和A3数据显示tft.setCursor(5, 210);tft.print("A1: ADC= --- =---V");tft.setCursor(5, 225);tft.print("A3: ADC= --- =---V");// 绘制赛车初始位置for (int i = 0; i < 4; i++) {drawCar(i);}// 显示游戏状态tft.setTextColor(ST77XX_WHITE);tft.setTextSize(1);tft.setCursor(5, 5);tft.print("Data Acquisition Active...");
}void showResults() {tft.fillScreen(BACKGROUND_COLOR);tft.setTextColor(ST77XX_WHITE);tft.fillRect(0, 0, 240, 40, ST77XX_RED);tft.setTextSize(2);tft.setCursor(40, 20);tft.println("Test Complete");tft.setTextSize(1);// 确定名次int rankings[4] = {0, 1, 2, 3};// 简单的冒泡排序按完成时间排序for (int i = 0; i < 3; i++) {for (int j = 0; j < 3 - i; j++) {if (cars[rankings[j]].finishTime > cars[rankings[j + 1]].finishTime) {int temp = rankings[j];rankings[j] = rankings[j + 1];rankings[j + 1] = temp;}}}// 显示测试结果int yPos = 60;for (int i = 0; i < 4; i++) {int carIndex = rankings[i];tft.setCursor(30, yPos);tft.print("Channel ");tft.print(carIndex);tft.print(": ");// 显示通道颜色方块tft.fillRect(90, yPos, 10, 10, cars[carIndex].color);tft.setCursor(105, yPos);tft.print("Time: ");tft.print(cars[carIndex].finishTime / 1000.0, 2);tft.print("s");yPos += 30;}// 显示最终数据tft.setCursor(30, 180);tft.print("Final A1 Voltage: ");tft.print(lastA1Voltage, 3);tft.print("V");tft.setCursor(30, 195);tft.print("Final A3 Voltage: ");tft.print(lastA3Voltage, 3);tft.print("V");tft.setTextSize(1); tft.setCursor(40, 215);tft.println("Reset all channels to restart");
}// 辅助函数:绘制带边框的矩形
void drawBorderedRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color, uint16_t borderColor) {tft.fillRect(x, y, w, h, borderColor);tft.fillRect(x + 1, y + 1, w - 2, h - 2, color);
}
冒泡排序算法原理:
冒泡排序是一种简单的排序算法,它重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成
算法步骤分解:
①外层循环(i循环):控制排序的轮数
对于4个元素,需要3轮比较(n-1轮)、i < 3 因为4个元素需要3轮比较
②内层循环(j循环):进行相邻元素的比较和交换
j < 3 - i 每完成一轮,最大的元素就会"冒泡"到末尾,因此后续比较次数减少
③比较和交换:
cars[rankings[j]].finishTime > cars[rankings[j + 1]].finishTime
如果前一个元素的完成时间大于后一个元素,则交换它们在排名数组中的位置
三、项目结果演示
3.1 操作流程
(1)系统上电,将A0和A2连接到GND(后接3.3V)、A1连接5V电位器、A3连接零知增强板A0引脚
(2)旋转电位器(大于1V时)系统启动,观察模拟值和电压值变化
将ADC值map映射到物体的移动速度,通过小车移动速度的快慢直观的感受到A1通道模拟值变化
底部显示旋转电位器的A1通道和零知增强板A0模拟引脚的A3通道具体ADC值和GAIN_TWOTHIRDS分辨率的电压值
(3)同时打开串口监视器观察通道1的ADC值和波形
3.2 数据记录
时间点 | A0电压 | A1电压 | A2电压 | A3电压 | 备注 |
---|---|---|---|---|---|
初始 | 0V | 0V | 0V | 0V | 系统启动 |
测试1 | 3.3V | 2.5V | 3.3V | 0.4V | 正常采集 |
测试2 | 3.3V | 4.8V | 3.3V | 0.4V | 电位器调节 |
对模拟值的大小进行冒泡排序,观察ADC值的变化过程
3.3 视频演示
ADS1115模数转换器实现多通道数据采集
系统启动和数据采集显示、电位器调节效果以及不同ADC值大小变化过程
四、ADS1115工作原理详解
ADS1115 具有 一个输入多路复用器 (MUX),可实现双路差分输入或 四路单端输入测量。兼容 I 2C 的 16 位低功耗精密模数转换器 (ADC)
4.1 内部架构与功能
(1)Δ-Σ调制器
以远高于奈奎斯特频率的速率采样、将量化噪声推向高频区域、将模拟信号转换为位流
(2)Multiplexer 复用器
ADS1115包含一个输入多路器,四个单电压输入或者两个差分输入。AIN0和AIN1可以与AIN3进行差分测量,多路复用器由配置寄存器中的位MUX [2:0]配置。
测量单端信号时,ADC的负输入通过多路器内的开关内部连接到GND
(3)噪声表现
采样速率不宜过高,由表格看出当采样速率大于128SPS时,分辨率在不同量程下均有所下降
(4)满量程范围
由配置寄存器中的 PGA [2:0] 位配置,可设置为 ±6.144 V、±4.096 V、±2.048 V、±1.024 V、±0.512 V、±0.256 V
4.2 I2C通信协议
(1)I2C 地址选择
ADS1115有一个地址引脚 ADDR,用于配置器件的 I2C 地址。该引脚可连接至 GND、VDD、SDA 或 SCL,通过一个引脚即可选择四种不同的地址
(2)向寄存器写入数据
要访问 ADS111x 中的特定寄存器,主机必须首先向地址指针寄存器中的寄存器地址指针位 P [1:0] 写入适当的值。
地址指针寄存器是在从机地址字节、低电平的读 / 写位以及从机成功应答之后直接写入,从机进行应答,主机发出停止条件或重复起始条件
(3)从寄存器读取数据
从 ADS111x 读取数据时,先前写入位 P [1:0] 的值决定了要读取的寄存器。要更改读取的寄存器,必须向 P [1:0] 写入新值
(4)数据格式
以二进制补码格式提供 16 位数据。正满量程(+FS)输入产生的输出代码为 7FFFh,负满量程(–FS)输入产生的输出代码为 8000h
对于超过满量程的信号,输出会钳位在这些代码上
电压计算原理
ADS1115的输出代码与输入电压的关系为:电压 = (ADC读数 × 满量程电压) / (2¹⁵ - 1)
在GAIN_TWOTHIRDS模式下:满量程电压 = 6.144V、分辨率 = 6.144V / 32767 ≈ 0.1875mV
4.3 寄存器映射
ADS1115有四个寄存器,可通过I2C接口和地址指针寄存器访问。转换寄存器存上次转换结果,配置寄存器用于更改工作模式和查询设备状态,另两个寄存器(Lo_thresh和Hi_thresh)则设定比较器功能的阈值。
(1)地址指针寄存器
地址指针寄存器的2-7位保留全部置0,最低两位写入地址指针访问对应寄存器
(2)转换寄存器(P [1:0] = 0h)
16 位转换寄存器以二进制补码格式存储最后一次转换的结果。上电后,转换寄存器被清零,并且在第一次转换完成前一直保持为 0
(3)配置寄存器(P [1:0] = 1h)
16位配置寄存器用于控制工作模式、输入选择、数据速率、满量程范围和比较器模式
eg:设置DR[2:0]位为100,控制数据速率默认的128SPS
五、常见问题解答
Q1: ADS1115读取数据不稳定怎么办?
A: 检查电源稳定性,添加滤波电容,确保I2C上拉电阻正确连接。
Q2: 显示屏出现闪烁如何解决?
A: 使用局部刷新技术,避免全屏刷新,优化刷新频率。
Q3: 如何提高ADC采集精度?
A: 使用外部基准电压,降低采样速率,添加硬件滤波。
项目资源整合:
ADS1115库文件:Adafruit_ADS1X15
ADS1115数据手册:ADS111x datasheet