Данная заметка посвящена решению несложной задачи: измерить и вывести на экран ПК пару значений постоянного напряжения. В качестве измерителя используется готовое изделие: плата 16-канального 12-разрядного АЦП с USB интерфейсом на базе микроконтроллера STM32.
Аппаратная часть
На Алиэкспресс изделие называется "Модуль АЦП STM32 UART 16 каналов 12 бит". Как это часто бывает, его описание на русском языке сгенерировано кривым переводчиком. Ниже привожу фото платы и немного полезной информации из описания, а также размеры, которых в описании нет.

Подключение: USB-C, UART.
Диапазон измеряемых напряжений: от 0 до 3.3 В.
Шестнадцать 12-битных каналов.
STM32F103R8T6 в качестве основного управляющего чипа и встроенный чип CH340T.
Результаты выборки обновляются каждые 500 мс.
Размеры 65 х 33 х 15 мм.
При USB подключении к ПК (Linux) команда lsusb выдаёт следующую информацию:
ID 1a86:7523 QinHeng Electronics CH340 serial converter
При USB подключении к ПК (Windows) через незначительное время система сама находит и устанавливает рабочий драйвер последовательного порта.
Тестируем
Создадим нехитрый код (python) чтобы понять, как работает устройство:
#!/usr/bin/env python3
"""
pip install serial # this are required python libraries
sudo usermod -a -G dialout $USER # USER access to serial port in unix
"""
import serial
from serial.tools import list_ports
ioPorts = [d for d in list_ports.comports() if "1A86:7523" in d.hwid]
if ioPorts:
try:
with serial.Serial(ioPorts[-1].device, baudrate=115200, timeout=0.5) as com:
rcv = com.read_until(expected = "\r\n\r")
print(rcv.decode("utf-8"))
except Exception as e:
print(e)
else:
print("no device")
Запускаем, получаем следующий вывод:
CH0:2048 1.652V
CH1:2056 1.657V
CH2:2055 1.655V
CH3:2058 1.658V
CH4:2060 1.658V
CH5:2061 1.660V
CH6:2062 1.660V
CH7:2059 1.660V
CH8:2057 1.658V
CH9:2056 1.657V
CH10:2058 1.658V
CH11:2056 1.657V
CH12:2055 1.656V
CH13:2056 1.658V
CH14:2057 1.656V
CH15:2058 1.657V
Раз в полсекунды устройство передаёт в последовательный порт 16 строк, каждая из которых содержит имя канала, код отсчёта АЦП () и результат измерения в вольтах (
), причем напряжение в вольтах
/ 0xFFF, где
= 3.3 В — напряжение питания микропроцессора. Видно, что «висящие в воздухе» измерительные входы показывают половину напряжения питания, то есть имеют внутреннее соединение с питанием и нулем платы через одинаковое сопротивление
. Это сопротивление можно определить, подсоединив вход к нулю платы (GND) через известное сопротивление
. Измеряя получившееся напряжение
, используя формулу
, получаем
.
Прибор
Для изготовления прибора использовался корпус размерами 80х50х20 мм. В торцах корпуса профрезерован паз для USB-C разъема, установлены два BNC коннектора и светодиодный индикатор питания. Два BNC коннектора подсоединины одновременно к 1 - 8 и 9 - 16 входам АЦП соответственно, ибо устройство проектировалось для работы в одно - двух канальном режиме. Поскольку входы коммутируются к АЦП программно в момент измерения, такое подключение не меняет внутреннее сопротивление входов, зато итоговое значение будет получено путём усреднения по восьми измерениям.



Приложение (Python, pyQt)
Осталось написать приложение с графическим интерфейсом. Ниже я просто привожу получившийся код, ибо он несложный и достаточно прямолинейный. Будут вопросы - могу ответить. Скриншот результата его работы приведен на обложке данной заметки. Проверено, что приложение надёжно работает в Linux и в Windows.
import argparse, serial
from statistics import mean
from time import sleep
from serial.tools import list_ports
from PyQt6 import QtCore, QtGui
from PyQt6.QtWidgets import (QApplication, QMainWindow,
QWidget, QLabel,
QHBoxLayout, QVBoxLayout,
QGroupBox, QLCDNumber)
class LCDpanel:
def __init__(self, color = "white", units = "мВ", digits=4):
units = QLabel(units)
units.setStyleSheet(f"QLabel {{color: {color}; font-size: 40pt;}}")
self.LCD = QLCDNumber()
self.LCD.setStyleSheet(f"QLCDNumber{{color: {color}; background-color:black;}}")
self.LCD.setDigitCount(digits)
self.LCD.setSegmentStyle(QLCDNumber.SegmentStyle.Flat)
self.BOX = QGroupBox()
self.BOX.setFixedWidth(320)
self.BOX.setFixedHeight(160)
self.BOX.setStyleSheet("QGroupBox {background-color: black;}")
layout = QHBoxLayout()
layout.addWidget(self.LCD, digits)
layout.addWidget(units, 1)
self.BOX.setLayout(layout)
class ADC:
def __init__(self, port):
self.ninput, self.handle = 0, port
if port is not None:
rcv = self.Measure()
if rcv[0]:
self.ninput = len(rcv[1])
else:
self.handle = rcv[1]
def Measure(self):
for attempt in range(5):
try:
with serial.Serial(self.handle, baudrate=115200, timeout=0.5) as com:
rcv = com.read_until(expected = "\r\n\r")
except Exception as e:
return False, e
s = rcv.decode("utf-8").split("\r\n")[:-2]
if len(s) in (10,16):
data = []
try:
for el in s:
data.append(int(el.split("\t")[0][-4:]))
return True, data
except:
pass
sleep(0.1)
print(f"Attempt {attempt}: {s}")
return False, "Unexpected data format"
class Application(QMainWindow, ADC):
CLRS = ("#FFF000", "#A0FFF0")
GAIN = 3300/0xFFF # ADC code to millivolts
TGUI = 100 # update time, milliseconds
def __init__(self, p=None, n=2, w=None):
super().__init__(port = p)
self.nlcd, self.nave = n, self.ninput//2
self.Timer = QtCore.QTimer()
self.Timer.timeout.connect(self.Update)
self.setWindowTitle("Милливольтметр")
self.area = QWidget()
self.setCentralWidget(self.area)
self.PANELS = []
layout = QVBoxLayout()
if p is None:
layout.addWidget(QLabel(w))
elif self.ninput == 0:
text = f"Проблема с портом {p}:\n {self.handle}".replace(":", "\n")
layout.addWidget(QLabel(text))
else:
for i in range(self.nlcd):
self.PANELS.append(LCDpanel(color=self.CLRS[i]))
layout.addWidget(self.PANELS[i].BOX)
self.Timer.start(self.TGUI)
self.area.setLayout(layout)
self.setFixedSize(340, 160*self.nlcd + 20)
self.icon = QtGui.QIcon("volt.ico")
self.setWindowIcon(self.icon)
def Update(self):
OK, DATA = self.Measure()
if OK:
for i in range(self.nlcd):
V = int(mean(DATA[i*self.nave:(i+1)*self.nave])*self.GAIN)
self.PANELS[i].LCD.display(V)
else:
print(DATA)
self.Timer.start(self.TGUI)
def closeEvent(self, action=None):
self.close()
def Parse_Args(ioPort=None):
""" command line arguments parser """
parser = argparse.ArgumentParser(prog = "volt",
add_help=False,
epilog ="Спасибо что пользуетесь %(prog)s!")
parser.add_argument('-h', '--help',
action='help',
default=argparse.SUPPRESS,
help='справка по запуску программы.')
select = parser.add_argument_group("Параметры конфигурации")
select.add_argument("-p", "--port",
metavar="port",
default = ioPort,
help = "имя последовательного порта в вашей системе,")
select.add_argument("-n", "--nlcd",
type=int,
metavar="nlcd",
default = 2,
help = "количество вольтметров - 1 или 2.")
return parser.parse_args(), parser.format_help()
def main():
port, nlcd = None, 2
ioPorts = [d for d in list_ports.comports() if "1A86:7523" in d.hwid]
if len(ioPorts) == 0:
warn = """ В вашей системе не найдено USB устройств типа VID:PID=1A86:7523. \n
Убедитесь что АЦП подключено и установите подходящий драйвер! """
else:
args, warn = Parse_Args(ioPort = ioPorts[-1].device)
port, nlcd = args.port, args.nlcd
try:
app = QApplication([])
window = Application(p=port, n=nlcd, w=warn)
window.show()
app.exec()
except:
window.closeEvent()
if __name__ == "__main__":
main()
Заключение
Вроде бы это всё, что я хотел сказать. Может быть, приведённая информация кому‑то окажется полезной. Я (пока) не владею программированием микроконтроллеров, в часности на базе STM32, и не уверен что у меня будет время для изучения этого вопроса. Очевидно, что содержащаяся в описанном устройстве микропрограмма ограничивает гибкость его применения, в тоже время делая работу с ним предельно простой для неспециалистов вроде меня. И всё же, может кто напишет, насколько проста или непроста реализация обмена через эмулятор последовательного интерфейса, наподобие описанного выше.
GambitOZ
Штука интересная, но лично я бы не стал подключать "на серьезных щах" эту или похожую плату к компьютеру. Т.к. есть большой шанс в случае кз спалить порт USB. Да и если измеряемый сигнал будет "шумным" весь этот "мусор" по питанию пойдет в компьютер или телефон. И проверено - ни к чему хорошему это не приведет. Через изолятор USB да возможно но он будет стоить как две эти платы.
slog2
Если надо развязать гальванически от компа, то проще и дешевле через блютус это сделать. Тут еще АЦП входы проца наружу торчат без всякой защиты.
pvvv
И городить отдельно питание. Китайский же изолятор на ADUM3160 вроде не особо дороже этого "bluepill" стоит.