Для задач робототехники, когда хочется применять вычисления на Python, использовать компьютерное зрение, ROS возникает необходимость быстрого и надежного обмена данными с микроконтроллером, который уже рулит всевозможными моторами, сервоприводами и датчиками.
Первое, о чем пришлось позаботиться - это согласование логических уровней двух устройств. Arduino работает на 5V, Raspberry на 3.3V. Для этого используется устройство LogicLevelConverter на 4 канала.
Порты для подключения на устройствах строго определены
Arduino Uno(Nano):
13 - SCK - тактовые импульсы для работы протокола SPI
12 - MISO (Master Input Slave Output) - передача данных от ведомого устройства (Arduino) к ведущему (Raspberry)
11 - MOSI (Master Output Slave Input) - передача данных от ведущего устройства (Raspberry) к ведомому (Arduino)
10 - CS или SS (Chip Select или Slave Select) - выбор устройства для работы. Raspberry может работать с 2 устройствами SPI сразу, и этот порт используется для указания, с каким идет обмен данными
Arduino Mega:
52 - SCK - тактовые импульсы для работы протокола SPI
50 - MISO (Master Input Slave Output) - передача данных от ведомого устройства (Arduino) к ведущему (Raspberry)
51 - MOSI (Master Output Slave Input) - передача данных от ведущего устройства (Raspberry) к ведомому (Arduino)
53 - CS или SS (Chip Select или Slave Select) - выбор устройства для работы. Raspberry может работать с 2 устройствами SPI сразу, и этот порт используется для указания, с каким идет обмен данными
Raspberry PI:
23 - SCK - тактовые импульсы для работы протокола SPI
21 - MISO (Master Input Slave Output) - передача данных от ведомого устройства (Arduino) к ведущему (Raspberry)
19 - MOSI (Master Output Slave Input) - передача данных от ведущего устройства (Raspberry) к ведомому (Arduino)
24 - CS или SS (Chip Select или Slave Select) - выбор устройства для работы. Raspberry может работать с 2 устройствами SPI сразу, и этот порт используется для указания, с каким идет обмен данными
Также к Logic level converter подключается рабочее напряжение каждого устройства и земля
Теперь к коду:
На Raspberry Pi необходимо включить SPI:
sudo raspi-config
-
Interfacing options - SPI
-
Включаем SPI
Далее устанавливаем библиотеку spidev
pip3 install spidev
И используем заготовку кода для передачи данных
import spidev
import time
from camer2 import getCherry
def list_int_to_bytes(input_list):
# Split list int values to list ready for transfer by SPI
# every value from -32768 to 32767
# will be replaced two values from -255 to 255
# Original values must be collected by Arduino after transmission
output_list = []
for int_data in input_list:
output_list.append(int_data >> 8)
output_list.append(int_data & 255)
return output_list
def spi_send(txData):
# Send and recieve 40 bytes
N = 40
spi = spidev.SpiDev()
spi.open(0, 0)
spi.max_speed_hz = 1000000
txData = list_int_to_bytes(txData)
txData = txData+[0]*(N-len(txData))
rxData = []
_ = spi.xfer2([240]) # 240 - b11110000 - start byte
for i in range(40):
rxData.append(spi.xfer2([txData[i]])[0])
spi.close()
return rxData
recieved_data = spi_send([1,2,3,4,5,6])
Функция spi_send принимает на вход список до 20 значений от -32768 до 32767, которые разбиваются в 40 байт и передаются в Arduino. В ответ функция возвращает 40 байт, полученных из Arduino
Код для Arduino:
#include <SPI.h>
#define DATA_SIZE 40
byte data[DATA_SIZE];//массив, в которые получаем исходные данные
int int_data[DATA_SIZE / 2];//массив в котором будут значения, полученные от Raspberry
byte sendData[DATA_SIZE];//массив, значения которого будут переданы на Raspberry
volatile byte counter = 0;
volatile byte in_byte = 0;
volatile byte spiTranferEnd = 0;
volatile byte spiTranferStarted = 0;
void fillSendData() {//заполняем массив числами, чтобы проверить корректность передачи
for (byte i = 1; i < 40; i++) {
sendData[i] = i;
}
}
void setup() {
Serial.begin(9600);
pinMode(MISO, OUTPUT);
SPCR |= _BV(SPE);//переводим SPI в режим Slave
SPI.attachInterrupt();//включаем прерывания по SPI
fillSendData();
}
ISR (SPI_STC_vect)//обработка прерывания, получение и передача данных
{
in_byte = SPDR;
if (in_byte == 240 and !spiTranferStarted) {
spiTranferStarted = 1;
counter = 0;
SPDR = sendData[counter];
}
if (spiTranferStarted and counter > 0) {
data[counter - 1] = in_byte;
SPDR = sendData[counter];
}
counter++;
if (counter == DATA_SIZE) {
SPDR = sendData[counter - 1];
counter = 0;
spiTranferStarted = 0;
spiTranferEnd = 1;
}
}
void joinRecievedBytes() {//функция, которая собирает 40 байт в 20 значений, которые передавались
for (int i = 0; i < DATA_SIZE; i += 2) {
int_data[i / 2] = data[i] << 8 | data[i + 1];
}
spiTranferEnd = 0;
}
void printSpiData() {//вывод в монитор порта полученных значений
for (int i = 0; i < DATA_SIZE / 2; i++) {
Serial.print(int_data[i]);
Serial.print(" ");
}
Serial.println();
}
void loop () {
if (spiTranferEnd) {//если эта переменная стала равна true, значит мы получили все 40 байт
joinRecievedBytes();//собираем из 40 байт 20 значений
// Тут можно написать действия с массивом int_data
// if (int_data[0]==1) {
// что-то делаем
//}
printSpiData();//выводим данные в монитор порта. Только для тестов!
//ПОТОМ ОТКЛЮЧИТЬ, Т.К. ЗАМЕДЛЯЕТ РАБОТУ ПРОГРАММЫ
}
}
Такие дела! Успехов!
Комментарии (19)
PKav
01.01.2023 22:30SPI так себе выбор. А что если слейву срочно захочется что-то сказать? Например, сообщить об ошибке, чтобы вся система максимально оперативно остановилась? Тут по всем параметрам лучше подойдет CAN, а если без дополнительного оборудования - UART.
AquariusStar
01.01.2023 22:39+2Обычно вешают уже дополнительную линию в качестве прерывания с общей подтяжкой. Просто надо смотреть, какие скорости передачи нужны.
Stepan_Burmistrov Автор
02.01.2023 06:53+4Слейв в данной конфигурации может опрашиваться постоянно и сообщать все, что угодно. Если в spi_send передать пустой список, то мы не передавая команд видим все, что хочет сказать ардуино.
Uart мне показался менее стабильным.
CAN-идея хорошая но штатно не поддерживается не распберри, не ардуино. Я-преподаватель и мне хочется делать достаточно простые в реализации решения.
PKav
02.01.2023 14:33+1Тогда уж I2C. Те же 2 провода, но не придется тянуть CS до каждого слейва. Плюс детекция отключения слейва от шины, когда он ACK в ответ на адрес не пришлет.
Stepan_Burmistrov Автор
02.01.2023 18:10I2C, как вариант.
Я пока не разбирался, как arduino в режиме слейв работает. Но идея существует, да.
Nahrenako
02.01.2023 02:17+1А как насчет трехпроводного SPI с двунаправленной шиной данных?
Есть идеи как это реализовать на Rasperry PI? Гугл говорит что задача не тривиальная.
Stepan_Burmistrov Автор
02.01.2023 06:58+2Если обмен должен инициировать именно слейв, как сказал AquariusStar, в комментарии выше, можно линию с прерыванием сделать. Других идей пока не приходило
firehacker
02.01.2023 18:58Ну как же написать статью на тему электроники, транзисторов, пинов, уровней напряжения, и не вставить сюда упоминание Python-а... Вселенная же схлопнется...
sami777
03.01.2023 12:43Вот эта платка для согласования уровней идея так себе. Может с импульсами в сотни килогерц она еще и будет работать, но выше уже не годится. Нужны пуш-пульные схемы.
Stepan_Burmistrov Автор
03.01.2023 17:50Интересно, у меня тоже были сомнения, но на скорости 1МГц прекрасно работает несколько таких сборок. Пока все стабильно
Aquahawk
Вопрос не по софту, а по аппаратаной части, как вы крепите такие платы на основание?
Stepan_Burmistrov Автор
Ардуино и logicLevelConverter можно распаять на зеленой макетке, если "по-быстрому". Расберри имеет штатные крепления. Платы между собой коннекторами DuPont
Aquahawk
Штатные крепления отверстия это хорошо. И допустим ардуину распаяли, а дальше, воо есть платы с отверстиями, как их крепить в корпус? Дело в том что крепление по типу материнских плат на стойки очень непросто выполнить, ибо точность сверления под стойки должна быть ну очень высокой, ручным инструментом нормально не делается.
ToSHiC
Да ну не придумывайте, нету ничего такого сложного. Тёмным маркером рисуете, потом поверх процарапываете разметку, где сверлить (можно циркулем, можно штангенциркулем), керном керните центры отверстий и дальше 3мм сверлом сверлите. Керном пользоваться обязательно, в этом секрет успеха.
Aquahawk
Мне нужно закрепить несколько десятков плат, это пару сотен отверстий, слишком много усилий на такой метод, уже пробовал.
ToSHiC
Платы одинаковые? Если да - то сделайте кондуктор хоть из фанеры и сверлите в удовольствие, повторяемость неплохая будет.
Aquahawk
Нескольких разных типов. Да кондукторы вариант, скорее всего так и буду делать, но ещё рассматриваю вариант крепления в мягкое основание саморезами через дистанционные втулки. Вопрос технологичности производства. Странно что очень много информации как собрать схемы на соплях, как развести платы, а как доводить это до конечного устройства информации в разы меньше.
Stepan_Burmistrov Автор
У меня есть лазерный резак по фанере 3д-принтер. Варианты:
Корпус из фанеры и крепление винтами(возможно пластик) через проставки
Корпус из пластика на принтере. Крепление саморезами с плоской шляпкой
Корпус из пластика и вплавление резьбовых вставок из латуни
Корпус из металла, тогда шаблон из фанеры и по нему сверлить, как уже выше предлагали
Aquahawk
Да всё это включено в перечень рассматриваемых вариантов. Мне не нравится применение горючих материалов, и найти достаточно большие листы толстого негорючего пластика тоже сложно. Но спасибо, как минимум я понимаю что не только мне в голову пришли такие варианты и, похоже, каких-то очевидных варианов я не пропустил.