Руководства · 14 мин чтения

Arduino и Python по последовательному порту: pyserial, CSV и графики в реальном времени

Связь Arduino и Python по последовательному порту с pyserial: установка, поиск порта, запись данных датчика в CSV, отправка команд и графики в реальном времени с matplotlib.

Связь Arduino и Python по последовательному порту означает, что программа на Python, запущенная на компьютере, обменивается данными с Arduino через USB-кабель. Arduino измеряет показания датчика и пишет результат в последовательный порт; Python читает эти строки с помощью библиотеки pyserial, сохраняет их в CSV-файл, строит график или принимает решение и отправляет команду обратно в Arduino. Так точное управление железом на Arduino соединяется с мощью Python в обработке данных, графиках, ИИ и автоматизации. Дополнительный модуль тоже не нужен: хватает того же USB-кабеля, через который загружаются скетчи.

В этом руководстве пошагово разберём, как установить pyserial, автоматически найти порт Arduino, записывать данные датчика с метками времени в CSV-файл, отправлять команды для светодиода и ШИМ из Python в Arduino и рисовать график в реальном времени с matplotlib. Затем поговорим о выборе формата данных и конца строки, о двух самых запутанных проблемах (ошибка занятого порта и перезагрузка платы при открытии порта) и о том, чем отличается та же задача на Raspberry Pi Pico с MicroPython. Примеры написаны для Arduino Uno; изменения для ESP32 и Pico описаны отдельно.

Плата Arduino Uno R3 и разъём USB
Arduino Uno: разъём USB служит и для загрузки кода, и для связи с Python по последовательному порту; пока идут данные, мигают светодиоды TX и RX.

Как Arduino общается с Python?

USB-UART-преобразователь на Arduino Uno (на оригинальных платах это ATmega16U2, на клонах обычно CH340) виден компьютеру как виртуальный последовательный порт: в Windows это COM3, COM4 и так далее; в macOS — /dev/cu.usbmodem… или /dev/cu.usbserial-…; в Linux — /dev/ttyACM0 или /dev/ttyUSB0. Каждый символ, который Arduino пишет через Serial.print(), уходит через этот порт в компьютер в виде байтов; на стороне Python pyserial открывает порт как файл, читает из него и пишет в него. Важное различие: Arduino Uno не выполняет код на Python. На плате работает код Arduino на C++, на компьютере работает Python, и общаются они только по последовательной линии. Чтобы обе стороны говорили на одном языке, эти настройки должны совпадать:

НастройкаСторона ArduinoСторона PythonВнимание
ПортСоздаётся автоматически при подключении по USBserial.Serial("COM3", ...)Имя порта зависит от операционной системы
Скорость (бод)Serial.begin(115200)serial.Serial(port, 115200)Если скорости разные, приходят мусорные символы
Формат кадра8 бит данных, без чётности, 1 стоповый бит (8N1)У pyserial по умолчанию тоже 8N1Менять обычно не нужно
Конец строкиSerial.println() добавляет \r\nreadline() читает, пока не встретит \nНе забудь добавить \n при отправке из Python
ТаймаутSerial.setTimeout(), по умолчанию 1000 мсtimeout=1 (секунды)При timeout=None readline() ждёт бесконечно

Установка pyserial (Windows, macOS, Linux)

  1. Убедись, что установлен Python 3. В Windows на первом экране установщика с официального сайта Python отметь галочку Add python.exe to PATH; в macOS и Linux Python 3 обычно уже есть в системе.
  2. Открой терминал (в Windows — командную строку или PowerShell) и введи python -m pip install pyserial. Если Windows не узнаёт команду python, попробуй py -m pip install pyserial; в macOS и Linux команда может выглядеть как python3 -m pip install pyserial.
  3. Проверь установку: команда python -c "import serial; print(serial.__version__)" должна вывести номер версии, например 3.5.
  4. Для раздела с графиком в реальном времени нужна ещё matplotlib: python -m pip install matplotlib.
  5. В Linux добавь своего пользователя в группу dialout, чтобы можно было открыть порт: sudo usermod -a -G dialout $USER, затем выйди из системы и войди снова. Если в Raspberry Pi OS или свежих версиях Ubuntu pip выдаёт ошибку externally-managed-environment, используй sudo apt install python3-serial или создай виртуальное окружение командой python3 -m venv и запускай pip внутри него.
В PyPI есть другой пакет с именем serial, который не имеет никакого отношения к последовательным портам. Если ввести pip install serial, код пройдёт строку import serial, но потом упадёт с ошибкой AttributeError: module 'serial' has no attribute 'Serial' (в модуле нет атрибута Serial). Правильный пакет называется pyserial; удали неправильный командой pip uninstall serial и установи pyserial заново. Та же ошибка появляется, если назвать свой файл serial.py: тогда Python импортирует твой файл вместо библиотеки.

На каком порту Arduino? Ищем порт с помощью Python

Проще всего посмотреть в меню Инструменты › Порт (Tools › Port) в Arduino IDE, но Python и сам умеет выводить список портов. Введи в терминале python -m serial.tools.list_ports -v, и для каждого порта появятся имя, описание и аппаратный идентификатор (VID:PID). Если непонятно, какая строка относится к Arduino, отключи плату, запусти команду ещё раз и запомни порт, который исчез. В macOS одна и та же плата видна и как /dev/tty…, и как /dev/cu…; в Python используй тот, что начинается с cu.

Модуль USB-TTL-преобразователя на CH340
Микросхема CH340: на большинстве клонов Arduino именно она служит мостом между USB и последовательным портом; её идентификатор производителя (VID) — 1A86.
USB-микросхемаГде встречаетсяVID:PIDИмя в WindowsИмя порта в Linux
ATmega16U2Оригинальные Uno R3, Mega 25602341:0043 (Uno)Arduino Uno (COMx)/dev/ttyACM0
CH340Клоны Uno, Nano и Mega1A86:7523USB-SERIAL CH340 (COMx)/dev/ttyUSB0
CP2102ESP32 DevKit, NodeMCU10C4:EA60Silicon Labs CP210x USB to UART Bridge (COMx)/dev/ttyUSB0
FT232RСтарые Nano, FTDI-кабели0403:6001USB Serial Port (COMx)/dev/ttyUSB0
RP2040 (встроенный USB)Raspberry Pi Pico с MicroPython2E8A:0005Последовательное устройство USB (COMx)/dev/ttyACM0

Функция ниже выводит список всех портов и, если узнаёт плату по VID, возвращает имя её порта. Сохрани код как port_bul.py, и в других скриптах можно будет написать from port_bul import port_bul и PORT = port_bul() вместо того, чтобы каждый раз менять имя порта вручную.

Python
1import serial.tools.list_ports
2
3# Bilinen USB yongalarının üretici kimlikleri (VID) / Vendor IDs of common USB chips
4BILINEN = {
5    0x2341: "Arduino (orijinal / genuine)",
6    0x2A03: "Arduino (orijinal / genuine)",
7    0x1A86: "CH340 (klon / clone)",
8    0x10C4: "CP210x (ESP32, NodeMCU)",
9    0x0403: "FTDI",
10    0x2E8A: "Raspberry Pi Pico",
11}
12
13
14def port_bul():
15    portlar = sorted(serial.tools.list_ports.comports())
16    for p in portlar:
17        print(p.device, "|", p.description, "|", p.hwid)   # tüm portları listele / list every port
18    for p in portlar:
19        if p.vid in BILINEN:
20            print("Bulundu / found:", p.device, "->", BILINEN[p.vid])
21            return p.device
22    return None
23
24
25if __name__ == "__main__":
26    if port_bul() is None:
27        print("Kart yok: kabloyu ve surucuyu kontrol et / no board: check cable and driver")

Отправляем данные из Arduino: первый скетч

В примерах используется потенциометр, подключённый к пину A0: средний вывод идёт на A0, один крайний на 5V, другой на GND. Вместо потенциометра можно подключить LDR с резистором 10 kΩ по схеме делителя напряжения (её удобно рассчитать в утилите делителя напряжения). Скетч делает измерение каждые 200 мс и отправляет результат одной строкой в формате время,значение:

C++ (Arduino IDE)
1// A0 değerini "millis,deger" CSV satırı olarak gönderir / Sends the A0 reading as a "millis,value" CSV line
2const int SENSOR_PIN = A0;            // potansiyometrenin orta ucu / potentiometer wiper
3const unsigned long ARALIK = 200;     // gönderim aralığı, ms / send interval, ms
4unsigned long sonGonderim = 0;
5
6void setup() {
7  Serial.begin(115200);               // Python'daki hızla aynı olmalı / must match the baud rate in Python
8}
9
10void loop() {
11  if (millis() - sonGonderim >= ARALIK) {   // delay() olmadan zamanlama / timing without delay()
12    sonGonderim = millis();
13    int deger = analogRead(SENSOR_PIN);     // 0-1023
14    Serial.print(sonGonderim);              // zaman damgası / timestamp
15    Serial.print(',');                      // ayırıcı / separator
16    Serial.println(deger);                  // sonuna \r\n ekler / appends \r\n
17  }
18}

Если использовать millis() вместо delay(), потом будет проще добавить в тот же цикл чтение команд из Python; подробности в статье millis и delay. После загрузки открой монитор порта на скорости 115200 бод, и побегут строки вроде 2573,512. Это только проверка: перед запуском скрипта на Python обязательно закрой монитор порта, иначе порт будет занят. Если монитор порта пока в новинку, поможет руководство по монитору порта Arduino.

Читаем данные Arduino в Python и сохраняем в CSV

Python
1import csv
2import time
3from datetime import datetime
4
5import serial
6
7PORT = "COM3"     # macOS: /dev/cu.usbmodem14101, Linux: /dev/ttyACM0
8BAUD = 115200     # Arduino'daki Serial.begin ile aynı / same as Serial.begin on the Arduino
9
10with serial.Serial(PORT, BAUD, timeout=1) as ser, \
11        open("olcumler.csv", "w", newline="", encoding="utf-8") as dosya:
12    time.sleep(2)               # Uno port açılınca resetlenir / the Uno resets when the port opens
13    ser.reset_input_buffer()    # yarım kalmış eski veriyi at / drop stale, partial data
14    yazici = csv.writer(dosya)
15    yazici.writerow(["tarih", "zaman_ms", "deger", "volt"])   # başlık satırı / header row
16    print("Kayit basladi, durdurmak icin Ctrl+C / logging, press Ctrl+C to stop")
17    try:
18        while True:
19            satir = ser.readline().decode("utf-8", errors="ignore").strip()
20            parcalar = satir.split(",")
21            if len(parcalar) != 2:
22                continue                    # boş ya da beklenmeyen satır / empty or unexpected line
23            try:
24                zaman_ms, deger = int(parcalar[0]), int(parcalar[1])
25            except ValueError:
26                continue                    # bozuk satır / corrupted line
27            volt = deger * 5.0 / 1023       # Uno ADC: 0-1023 -> 0-5 V
28            yazici.writerow([datetime.now().isoformat(timespec="seconds"),
29                             zaman_ms, deger, round(volt, 3)])
30            dosya.flush()                   # satırı hemen diske yaz / write the row to disk now
31            print(zaman_ms, deger, f"{volt:.2f} V")
32    except KeyboardInterrupt:
33        print("Durduruldu / stopped")

Этот скрипт открывает порт с timeout=1: readline() ждёт конца строки не больше 1 секунды, если ничего не пришло, возвращает пустой объект bytes, и цикл никогда не зависает. Входящие данные имеют тип bytes; decode() превращает их в текст, strip() убирает \r\n в конце, а split(",") делит строку на столбцы. time.sleep(2) и reset_input_buffer() после открытия порта выбрасывают обрывки строк, которые приходят, пока Uno перезагружается; почему так происходит, разберём ниже. Открытие файла с newline="" не даёт появляться пустым строкам в Windows, а flush() сохраняет уже записанные измерения на диске, даже если программа внезапно закроется. Чтобы остановить запись, нажми Ctrl+C в терминале.

Если CSV-файл открывается в Excel одним столбцом, дело в региональных настройках: в русской и многих европейских локалях Excel ждёт точку с запятой как разделитель списка и запятую как десятичный разделитель. Используй csv.writer(dosya, delimiter=";") и записывай напряжение через str(round(volt, 3)).replace(".", ",") или импортируй файл в Excel через Данные › Из текстового/CSV-файла (Data › From Text/CSV) и выбери разделитель вручную. Другие способы передать данные в Excel описаны в статье передача данных Arduino в Excel.

Отправляем команды из Python в Arduino

Связь работает в обе стороны. Скетч Arduino ниже ждёт команды, приходящие построчно: LED ON и LED OFF включают и выключают встроенный светодиод, PWM 128 задаёт яркость светодиода на пине 9, а READ отправляет обратно значение с A0. На каждую команду приходит короткий ответ, так что Python знает, что команда действительно выполнена. Подходящий резистор для светодиода на пине 9 можно подобрать в калькуляторе резистора для светодиода; как работает ШИМ, объясняет статья Что такое ШИМ?

C++ (Arduino IDE)
1// Python'dan gelen satır komutlarını işler / Handles line-based commands from Python
2const int LED_PIN = LED_BUILTIN;      // Uno'da pin 13 / pin 13 on the Uno
3const int PWM_PIN = 9;                // 220 ohm dirençli LED / LED with a 220 ohm resistor
4
5void setup() {
6  pinMode(LED_PIN, OUTPUT);
7  pinMode(PWM_PIN, OUTPUT);
8  Serial.begin(115200);
9  Serial.setTimeout(50);              // readStringUntil en fazla 50 ms bekler / waits at most 50 ms
10  Serial.println("HAZIR");            // Python bu satırı bekler / Python waits for this line
11}
12
13void loop() {
14  if (Serial.available() > 0) {
15    String komut = Serial.readStringUntil('\n');   // satır sonuna kadar oku / read up to the newline
16    komut.trim();                                  // \r ve boşlukları sil / strip \r and spaces
17
18    if (komut == "LED ON") {
19      digitalWrite(LED_PIN, HIGH);
20      Serial.println("OK LED ON");
21    } else if (komut == "LED OFF") {
22      digitalWrite(LED_PIN, LOW);
23      Serial.println("OK LED OFF");
24    } else if (komut.startsWith("PWM ")) {
25      int deger = komut.substring(4).toInt();     // "PWM 128" -> 128
26      deger = constrain(deger, 0, 255);           // 0-255 aralığında tut / keep it within 0-255
27      analogWrite(PWM_PIN, deger);
28      Serial.print("OK PWM ");
29      Serial.println(deger);
30    } else if (komut == "READ") {
31      Serial.println(analogRead(A0));              // A0 ölçümünü gönder / send the A0 reading
32    } else if (komut.length() > 0) {
33      Serial.print("ERR ");                        // bilinmeyen komut / unknown command
34      Serial.println(komut);
35    }
36  }
37}
Python
1import time
2
3import serial
4
5
6def komut_gonder(ser, komut):
7    ser.write((komut + "\n").encode("ascii"))       # satır sonu şart / the newline is required
8    return ser.readline().decode("ascii", errors="ignore").strip()   # cevabı bekle / wait for the reply
9
10
11with serial.Serial("COM3", 115200, timeout=2) as ser:
12    bitis = time.time() + 3                         # en fazla 3 sn bekle / wait at most 3 s
13    while time.time() < bitis:
14        if ser.readline().strip() == b"HAZIR":      # Arduino açıldı / the Arduino is up
15            break
16    print(komut_gonder(ser, "LED ON"))              # OK LED ON
17    time.sleep(1)
18    print(komut_gonder(ser, "PWM 128"))             # OK PWM 128
19    print("A0 =", komut_gonder(ser, "READ"))        # A0 = 512
20    print(komut_gonder(ser, "LED OFF"))             # OK LED OFF

Самая частая ошибка на стороне Python — забыть \n в конце команды: если readStringUntil('\n') не находит конец строки, Arduino ждёт время, заданное через Serial.setTimeout(), и только потом обрабатывает команду. В этом скетче это время 50 мс; со значением по умолчанию 1000 мс каждая команда опаздывала бы на секунду. write() принимает только байты, поэтому текст нужно преобразовать через encode() или поставить перед ним b (b"LED ON\n"). Сначала скрипт до 3 секунд ждёт строку HAZIR (готово): Uno перезагружается при открытии порта, поэтому команда, отправленная сразу, попадает в загрузчик и теряется. Если после каждой команды читать ответ, то есть работать по схеме «запрос — ответ», обе стороны остаются синхронными, а буферы не переполняются; приёмный буфер Uno — всего 64 байта.

Формат данных и конец строки: что выбрать?

Последовательная линия даёт только поток байтов; где заканчивается одно измерение и начинается следующее, решать тебе. Самый надёжный способ — писать каждое измерение в отдельную строку и заканчивать каждую строку символом \n. Какой формат использовать внутри строки, зависит от размера проекта:

ФорматПример строкиПлюсыМинусы
Одно значение512Самый простой формат; плоттер Arduino IDE (Serial Plotter) рисует его сразуПри нескольких значениях непонятно, где какое
CSV2573,512,23.5Коротко; Excel и pandas читают его напрямуюПорядок столбцов должен совпадать на обеих сторонах
Ключ:значениеtemp:23.5,hum:41Читается легко; плоттер показывает подписиДлиннее, разбирать чуть сложнее
JSON{"t":23.5,"h":41}В Python разбирается одной строкой через json.loads()Самый длинный формат; нагружает скромное ОЗУ Uno
Двоичный2 байта: 0x02 0x00Самый быстрый и короткий вариантЧеловеку не прочитать; нужны модуль struct и стартовый байт

Концы строк — ещё один частый источник путаницы. Serial.println() добавляет в конец каждой строки два символа: возврат каретки (\r, CR) и перевод строки (\n, LF). readline() в Python ждёт только \n, поэтому в конце строки остаётся \r; strip() убирает оба. В списке концов строки монитора порта Arduino IDE есть варианты No Line Ending («Нет конца строки»), New Line («Новая строка»), Carriage Return («Возврат каретки») и Both NL & CR («NL и CR»); когда скетч Arduino проверяется в мониторе порта до перехода на Python, выбирай New Line, чтобы отправлялся тот же символ, что и из Python. Для дробных чисел указывай количество знаков, как в Serial.print(temperature, 1); на Uno sprintf не поддерживает формат с плавающей точкой (%f), поэтому попытка вывести дробное число через sprintf даёт вопросительный знак вместо числа.

Скорость (бод)Байт в секунду (8N1)Строк по 12 байт в секундуКогда использовать
960096080Медленные датчики, длинные кабели, старые примеры скетчей
576005 760480Измерения средней частоты
11520011 520960Графики в реальном времени, ESP32 и большинство новых проектов

По последовательной линии каждый байт передаётся как 10 бит вместе со стартовым и стоповым битами, поэтому 115200 бод — это примерно 11 520 байт в секунду. Для 12-байтной строки из 25730,1023 и конца строки (\r\n) это до 960 строк в секунду. Если скорость на двух сторонах разная, по линии идут мусорные символы, а Python выдаёт UnicodeDecodeError. От 5 до 20 измерений в секунду более чем достаточно и для человеческого глаза, и для большинства датчиков; если отправлять данные намного чаще, чем нужно, график тормозит, а файл раздувается.

Рисуем график в реальном времени с matplotlib

Python
1from collections import deque
2
3import matplotlib.pyplot as plt
4import serial
5from matplotlib.animation import FuncAnimation
6
7ser = serial.Serial("COM3", 115200, timeout=0.1)
8N = 200                                          # ekrandaki ölçüm sayısı / readings on screen
9veri = deque([0] * N, maxlen=N)                  # eskiler kendiliğinden düşer / old values drop out
10
11fig, ax = plt.subplots()
12cizgi, = ax.plot(range(N), list(veri))
13ax.set_ylim(0, 1023)                             # Uno ADC aralığı / Uno ADC range
14ax.set_xlabel("Ornek / sample")
15ax.set_ylabel("A0")
16
17
18def guncelle(_):
19    while ser.in_waiting:                        # biriken bütün satırları oku / read every queued line
20        parca = ser.readline().decode("utf-8", errors="ignore").strip().split(",")
21        if len(parca) == 2 and parca[1].isdigit():
22            veri.append(int(parca[1]))
23    cizgi.set_ydata(list(veri))
24    return (cizgi,)
25
26
27ani = FuncAnimation(fig, guncelle, interval=50, blit=True, cache_frame_data=False)   # ani silinmemeli / keep ani
28plt.show()
29ser.close()

График построен на deque, которая хранит последние 200 измерений: каждое новое значение добавляется справа, а самое старое автоматически выпадает слева. FuncAnimation вызывает функцию guncelle (обновить) каждые 50 мс. Цикл while ser.in_waiting внутри функции важен: сейчас Arduino отправляет 5 строк в секунду, но если поднять частоту, чтение одной строки за кадр приведёт к тому, что график начнёт отставать от реального времени; этот цикл читает все накопившиеся строки и держит график актуальным. Не удаляй переменную ani: если на объект анимации не ссылается ни одна переменная, сборщик мусора Python его удалит, и окно застынет. Для ESP32 используй set_ylim(0, 4095), для Pico — set_ylim(0, 65535). Если нужно просто быстро посмотреть, справится и «Плоттер по последовательному соединению» (Serial Plotter) в Arduino IDE; преимущество Python в том, что график можно сохранить, объединить несколько источников и одновременно обрабатывать данные.

Ошибка занятого порта и проблема автосброса

Последовательный порт может одновременно открыть только одна программа. Если во время работы скрипта на Python открыты монитор порта или плоттер Arduino IDE, порт держит другой скрипт на Python или в фоне работает программа, которая сканирует порты (например, программы для 3D-принтеров), то в Windows появится ошибка could not open port 'COM3': PermissionError(13, 'Access is denied.', None, 5) (в русской Windows — «Отказано в доступе»), а в macOS и Linux — Resource busy или Device or resource busy. Это работает и в обратную сторону: пока скрипт на Python держит порт открытым, Arduino IDE не может загрузить код. Поэтому в скриптах используй конструкцию with serial.Serial(...) as ser:; порт закроется автоматически, когда блок закончится или произойдёт ошибка. Возьми за привычку останавливать скрипт через Ctrl+C перед загрузкой.

Вторая проблема коварнее: на Arduino Uno, Nano и Mega линия DTR USB-микросхемы подключена к пину RESET процессора через конденсатор 100 nF. Эта схема нужна, чтобы IDE могла автоматически перезагрузить плату при загрузке кода, но каждый раз, когда pyserial открывает порт, DTR тоже меняется, и плата сбрасывается. Сначала плата секунду-другую ждёт в загрузчике, а потом запускает скетч с начала. Команды, отправленные за это время, теряются, а первые строки, которые Arduino печатает в setup(), можно пропустить. Решение — подождать time.sleep(2) после открытия порта или, что надёжнее, дождаться, пока Arduino отправит строку вроде HAZIR; пример с командами выше делает именно так. Открывай порт один раз в начале программы и держи открытым до конца: цикл, который открывает и закрывает порт для каждого измерения, будет сбрасывать плату снова и снова.

Если нужно полностью избежать сброса, есть два способа. Программный: создай закрытый порт через ser = serial.Serial(), задай ser.port и ser.baudrate, напиши ser.dtr = False и только потом вызови ser.open(). В Windows это обычно работает; в Linux и macOS операционная система всё равно может дёрнуть DTR при открытии порта. Аппаратный: поставь конденсатор 10 µF между пинами RESET и GND на Uno, плюсовой ножкой к RESET. Платы со встроенным USB, такие как Leonardo, Micro и Raspberry Pi Pico, не перезагружаются при открытии порта; на них строки, которые код печатает до подключения Python, могут потеряться, поэтому важные сообщения надёжнее отправлять в ответ на пришедшую команду.

Пока установлен конденсатор 10 µF, Arduino IDE не может загрузить код, потому что загрузка опирается на ту же схему автосброса; перед загрузкой конденсатор нужно убрать. Если Python выдаёт ошибку device reports readiness to read but returned no data (устройство готово к чтению, но данных нет), значит, плата потеряла соединение или порт одновременно используют две программы. Если плата сама сбрасывается во время работы, причина обычно в нехватке питания; запитывай сервоприводы и моторы от отдельного источника, а не от 5V с USB.

Связь ESP32 и Python по последовательному порту

Весь код на Python из этой статьи работает и с ESP32; на стороне Arduino нужно поменять лишь несколько вещей. Для аналогового входа используй один из пинов ADC1 (например, GPIO 34). АЦП у ESP32 12-битный и переводит диапазон 0–3,3 V в 0–4095, поэтому напряжение в Python считай как deger * 3.3 / 4095. При загрузке ESP32 печатает собственные стартовые сообщения на скорости 115200 бод, например rst:0x1 (POWERON_RESET),boot:0x13; скрипты выше пропускают строки, которые нельзя превратить в числа, так что эти сообщения не мешают. В зависимости от схемы автосброса на плате и операционной системы ESP32 тоже может перезагружаться при открытии порта, поэтому ожидание и строка HAZIR пригодятся и здесь. USB-микросхема обычно CP2102 или CH340. Если хочется обойтись без кабеля, ESP32 может отправлять те же данные по Wi-Fi; смотри руководства ESP32 и WebSocket и ESP32 и Google Таблицы. Если это первая ESP32, сначала прочитай статью Что такое ESP32?

Raspberry Pi Pico и MicroPython: в чём разница?

Отладочная плата Raspberry Pi Pico
Raspberry Pi Pico: благодаря встроенному USB-контроллеру RP2040 отдельный USB-UART-преобразователь ей не нужен; с установленным MicroPython она видна компьютеру прямо как последовательный порт.

Ещё один способ соединить Python и железо — запускать Python прямо на плате. Raspberry Pi Pico можно программировать на MicroPython: чтение датчиков, управление светодиодами и отсчёт времени выполняются внутри Pico с синтаксисом Python. Однако MicroPython — это урезанная версия настольного Python; matplotlib, pandas и OpenCV на Pico не работают. Поэтому для записи данных, построения графиков или обработки с помощью ИИ Pico тоже подключают к Python на компьютере. Код на pyserial из этой статьи работает и с Pico; нужно лишь адаптировать имя порта, диапазон значений и способ чтения ответов на команды, как описано ниже.

ХарактеристикаArduino + Python на ПКPico + MicroPython (автономно)Pico + Python на ПК
Где работает Python?Только на компьютере; на плате работает C++Внутри PicoНа обеих сторонах: MicroPython на Pico, Python на компьютере
Нужен ли компьютер?Да, данные обрабатываются на компьютереНет, работает от USB-питания или батареиДа
Доступные библиотекиpyserial, matplotlib, pandas, numpy и OpenCVОграниченный набор модулей, например machine и timeВсе библиотеки компьютера
Скорость (бод)Важна, должна совпадать на обеих сторонахПоследовательный порт не используетсяНе важна, данные идут со скоростью USB
Сброс при открытии портаДа, на Uno, Nano и MegaНетНет
Когда выбиратьArduino уже есть под рукой и нужны готовые библиотекиПростые автономные устройстваХочется один язык (Python) на обеих сторонах

Код на MicroPython ниже — это версия примера для Arduino под Pico: он каждые 200 мс читает потенциометр на GP26, отправляет результат в том же формате время,значение и принимает те же команды для светодиода. Здесь аналог Serial из Arduino — это print() и sys.stdin; благодаря select.poll() цикл измерений не останавливается, пока ждёт команды.

MicroPython
1# Pico'ya main.py adıyla kaydet / save it on the Pico as main.py
2import sys
3import select
4import time
5from machine import ADC, Pin
6
7led = Pin("LED", Pin.OUT)          # Pico ve Pico W dahili LED / onboard LED on Pico and Pico W
8pot = ADC(26)                      # GP26 = ADC0, potansiyometre orta ucu / potentiometer wiper
9okuyucu = select.poll()
10okuyucu.register(sys.stdin, select.POLLIN)   # USB'den veri gelince haber ver / notify when USB data arrives
11
12son = time.ticks_ms()
13while True:
14    if okuyucu.poll(0):                       # bekleyen komut var mı? / is a command waiting?
15        komut = sys.stdin.readline().strip()
16        if komut == "LED ON":
17            led.on()
18            print("OK LED ON")
19        elif komut == "LED OFF":
20            led.off()
21            print("OK LED OFF")
22    if time.ticks_diff(time.ticks_ms(), son) >= 200:   # 200 ms'de bir ölçüm / one reading every 200 ms
23        son = time.ticks_ms()
24        print("{},{}".format(son, pot.read_u16()))     # zaman,değer (0-65535) / time,value (0-65535)

Сохрани код на Pico в Thonny под именем main.py, затем закрой Thonny, отключи USB-кабель Pico и подключи снова: Thonny сама использует последовательный порт Pico, поэтому, пока она подключена, твой скрипт на Python не сможет открыть порт, а при подключении она ещё и останавливает запущенную программу; после перезагрузки main.py запускается автоматически. На USB-порту Pico скорость в бодах не важна: какое бы значение ни стояло в Python, данные идут со скоростью USB. read_u16() возвращает значения от 0 до 65535, поэтому напряжение считай как deger * 3.3 / 65535. Поскольку Pico отправляет строки измерений без остановки, readline() в скрипте с командами выше может вернуть измерение вместо ответа; с Pico продолжай читать, пока не придёт строка, начинающаяся с OK. В этом примере нет строки HAZIR и команд PWM и READ; при необходимости добавь их в код для Pico. Осторожно с отправкой двоичных данных с компьютера: байт 0x03 в MicroPython означает Ctrl+C и останавливает программу; это можно отключить через micropython.kbd_intr(-1). Если Pico пока в новинку, хорошее начало — статьи Что такое Raspberry Pi Pico? и Основы MicroPython.

Идеи проектов с Arduino и Python

  • Регистратор температуры и влажности: раз в минуту записывай в CSV показания DHT11 и построй график микроклимата в комнате за неделю в Excel или с pandas.
  • Управление лампой с компьютера: управляй модулем реле командами ON и OFF из Python и добавь к этому таймер или голосовые команды.
  • График расстояния в реальном времени: выводи расстояние, измеренное HC-SR04, на график с matplotlib. Если поворачивать датчик сервоприводом, тем же способом можно нарисовать и экран радара.
  • Экран состояния ПК: пусть Python читает загрузку процессора и памяти через библиотеку psutil и отправляет её в Arduino, а Arduino показывает данные на OLED-дисплее.
  • Камера, следящая за лицом: OpenCV находит положение лица на изображении, Python переводит его в угол и отправляет, а Arduino поворачивает камеру сервоприводом.
  • Своя клавиатура для горячих клавиш: пусть кнопки на Arduino при нажатии отправляют строку, а Python читает её и открывает программу или выполняет сочетание клавиш на компьютере.

Частые ошибки и как их исправить

Ошибка или симптомПричинаРешение
ModuleNotFoundError: No module named 'serial' (модуль не найден)pyserial не установлен для интерпретатора Python, который запускает скрипт; VS Code или Thonny могут использовать другой PythonВыполни python -m pip install pyserial тем же интерпретатором и выбери нужный интерпретатор в редакторе
AttributeError: module 'serial' has no attribute 'Serial' (нет атрибута Serial)Установлен не тот пакет serial, или твой файл называется serial.pypip uninstall serial, затем pip install pyserial; переименуй свой файл
Порт занят — could not open port 'COM3': PermissionError(13, 'Отказано в доступе.', None, 5)Порт открыт в другой программе: мониторе порта, плоттере или втором скриптеЗакрой другую программу; используй в скрипте конструкцию with
Порт не найден — could not open port 'COM5': FileNotFoundError(2, 'Не удается найти указанный файл.', None, 2)Неверное имя порта или плата не подключенаНайди правильное имя командой python -m serial.tools.list_ports -v
[Errno 13] Permission denied: '/dev/ttyACM0'У твоего пользователя Linux нет прав на доступ к последовательному портуsudo usermod -a -G dialout $USER, затем выйди из системы и войди снова
Мусорные символы или UnicodeDecodeErrorРазная скорость в бодах или помехи при запускеИспользуй одинаковую скорость на обеих сторонах; добавь decode(errors="ignore") и reset_input_buffer()
readline() всегда возвращает b''Arduino не отправляет конец строки (print вместо println), порт неверный или плата ещё загружаетсяИспользуй println, проверь порт и скорость, увеличь timeout
ValueError: invalid literal for int() with base 10 (строку нельзя превратить в число)В число пытались превратить неполную, пустую или неожиданную строкуОберни преобразование в try/except и проверяй количество столбцов
Первые команды игнорируютсяUno перезагрузилась при открытии порта, и команда ушла в загрузчикДождись строки HAZIR или подожди 2 секунды после открытия порта
График отстаёт от реального времениЗа кадр читается только одна строка, и данные копятся в буфереЧитай все строки циклом in_waiting и отправляй данные реже
Код не загружается, пока работает скриптPython держит портОстанови скрипт через Ctrl+C, затем загружай
Когда что-то идёт не так, сначала убери Python из цепочки и проверь скетч Arduino в мониторе порта на той же скорости. Если там строки выглядят нормально, проблема на стороне Python (имя порта, скорость, конец строки или разбор); если и там каша, сначала исправь скетч Arduino. Если код вообще не загружается, пройди шаги из руководства Ошибка загрузки Arduino. Если интересна аппаратная сторона последовательной связи, то есть TX, RX и UART, загляни в статью Последовательный интерфейс UART.

Связать Arduino и Python через последовательный порт — самый быстрый способ превратить простой проект с датчиком в систему, которая записывает данные, рисует графики и управляется с компьютера. Сначала прочитай одно значение и выведи его на экран, потом сохрани в CSV, а команды и графики добавь в последнюю очередь; на каждом шаге проверяй, что скорость в бодах и конец строки совпадают на обеих сторонах. В приложении BlueGrays есть пошаговые проекты со схемами подключения и кодом: измерение освещённости с LDR, температуры и влажности с DHT11, расстояния с HC-SR04, а также последовательная связь между двумя Arduino. С ними можно быстро собрать источник данных, которые потом будет читать Python.

Частые вопросы

Можно ли программировать Arduino на Python?

Внутри Arduino Uno, Nano или Mega Python не работает; эти платы программируются в Arduino IDE на языке Arduino, основанном на C++. Python работает на компьютере и общается с платой по последовательному порту через pyserial; ещё можно загрузить пример StandardFirmata и управлять пинами прямо из Python с помощью библиотеки вроде pyFirmata2. Если нужно, чтобы Python работал на самой плате, выбирай Raspberry Pi Pico или ESP32 с поддержкой MicroPython.

Как установить pyserial?

Выполни в терминале python -m pip install pyserial и напиши в коде import serial. Пакет называется pyserial, а импортируемый модуль — serial; команда pip install serial ставит совершенно другой пакет и приводит к ошибке AttributeError. В Linux ещё нужно добавить своего пользователя в группу dialout.

Как узнать, на каком COM-порту Arduino?

Посмотри в меню «Инструменты › Порт» (Tools › Port) в Arduino IDE или выполни в терминале python -m serial.tools.list_ports -v. В Windows порт также виден в «Диспетчере устройств» в разделе «Порты (COM и LPT)». Если есть сомнения, отключи плату и выведи список портов снова; исчезнувший порт и есть порт Arduino.

Как отправить данные из Python в Arduino?

Открой порт через serial.Serial и отправляй байты, например ser.write(b"LED ON\n"); не забудь преобразовать текст через encode() и добавить конец строки. На стороне Arduino проверяй наличие входящих данных через Serial.available() и читай строку через Serial.readStringUntil('\n'). Uno перезагружается при открытии порта, поэтому перед первой командой подожди около 2 секунд.

Можно ли одновременно пользоваться монитором порта Arduino и Python?

Нет, последовательный порт одновременно может открыть только одна программа; вторая получит ошибку Access is denied («Отказано в доступе») или Resource busy. Зато входящие данные можно в Python одновременно обрабатывать и выводить на экран через print. Если нужны два отдельных канала, подключи к Arduino второй USB-TTL-преобразователь и создай второй последовательный порт через SoftwareSerial.

Почему Arduino перезагружается, когда Python открывает порт?

На Uno, Nano и Mega линия DTR USB-микросхемы подключена к пину RESET через конденсатор, поэтому плата перезагружается при каждом открытии порта; эта схема нужна, чтобы IDE могла загружать код. Подожди 2 секунды после открытия порта или дождись от Arduino строки о готовности. Чтобы полностью избежать сброса, можно поставить конденсатор 10 µF между RESET и GND, но перед загрузкой его нужно убрать.

Все проекты
Измерение освещённости с LDR
#11Новичок

Измерение освещённости с LDR

Хочешь увидеть яркость своей комнаты в виде числа? LDR — маленькая деталь, реагирующая на свет, — измеряет освещённость вокруг, а Arduino каждые полсекунды выводит значение на экран компьютера. Закрой

4.6(81)
139
Измерение расстояния HC-SR04 (вывод в порт)
#27Новичок

Измерение расстояния HC-SR04 (вывод в порт)

Датчик HC-SR04 работает совсем как летучая мышь, которая находит дорогу в темноте: он посылает звук, которого ты не слышишь, слушает эхо и измеряет, насколько далеко предмет перед ним. Arduino каждые

4.6(46)
71
Температура и влажность с DHT11
#55Средний

Температура и влажность с DHT11

Насколько тепло в твоей комнате и влажный ли воздух или сухой? Датчик DHT11 измеряет температуру и влажность, а Arduino выводит значения на двухстрочный LCD-экран и обновляет их каждые пару секунд. Ес

4.7(47)
73
Измерение освещённости модулем LDR на ESP32
#113Новичок

Измерение освещённости модулем LDR на ESP32

Наблюдение в мониторе порта в реальном времени за сырым значением ADC модуля LDR и относительным процентом освещённости, вычисленным по опорным значениям темноты и света. Этот процент — не измерение в люксах. Учит логике калибровки модуля. Идеально выполнить его перед проектом 10, потому что он нужен для определения порога.

4.6(36)
55
Односторонняя связь между двумя Arduino
#57Средний
Premium

Односторонняя связь между двумя Arduino

Ты заставляешь две Arduino разговаривать по одному проводу: нажми кнопку на одной, и на другой загорится светодиод, нажми снова — погаснет. Плата-передатчик сообщает о нажатии кнопки коротким сообщени

4.4(37)
50
Двусторонняя связь между двумя Arduino
#58Средний
Premium

Двусторонняя связь между двумя Arduino

Две Arduino соединены проводами и разговаривают друг с другом: нажми кнопку на одной, и на другой загорится свет, нажми снова — погаснет. На обеих платах одна и та же программа, поэтому каждая и отпра

4.5(20)
26
Управление светодиодом с телефона по Bluetooth
#34Новичок
Premium

Управление светодиодом с телефона по Bluetooth

Небольшая репетиция того, как включать и выключать свет в комнате с телефона. Bluetooth-модуль передаёт в Arduino буквы, отправленные с телефона; одна буква включает красный светодиод, другая — зелёны

4.8(68)
89
Регистратор температуры на ESP32 (DHT22 + DS1307 RTC + microSD + OLED)
#144Продвинутый
Premium

Регистратор температуры на ESP32 (DHT22 + DS1307 RTC + microSD + OLED)

Собери на ESP32 регистратор температуры с отметками времени! Каждую минуту читай температуру и влажность с DHT22, бери реальное время из DS1307 RTC и записывай всё на карту microSD в формате CSV. OLED показывает текущую температуру (крупным шрифтом), влажность в процентах, время и счётчик записей. Файл /temp_log.csv на SD-карте открывается в Excel, Google Sheets или Python pandas — идеально для теплиц, комнат, лабораторий и хранения продуктов.

4.7(7)
12