Каскад из четырёх 74HC595 из урока про сдвиговый регистр жил у меня на shiftOut и был счастлив — пока я не прикинул анимацию на тысяче обновлений в секунду. Куб тогда не собрался, но арифметика вышла обидная: каждый байт выкатывался примерно за сто микросекунд, кадр из восьми байтов — за восемьсот, а весь бюджет кадра при тысяче кадров — те же тысяча микросекунд. Восемьдесят процентов процессора уходило на то, чтобы дёргать ногой вверх-вниз, и на кнопки с датчиками сил не оставалось. Внутри ATmega328P всё это время простаивал аппаратный блок SPI, делающий ту же работу в сотню раз быстрее, — его надо было просто включить.
Слово «шина» здесь не для красоты. В отличие от shiftOut, где каждый чип тянет свою пару пинов, SPI — общая дорога: одни и те же MOSI, MISO и SCK обслуживают и регистр, и матрицу, и SD-карту, а кого слушать, решает персональный CS. Поэтому урок про четыре линии — это ещё и урок про то, как потом подключить пятый и шестой чип, не найдя ни одного свободного пина.
Здесь и включим: четыре линии — MOSI, MISO, SCK и CS, их аппаратные пины на Uno, SPI.begin(), beginTransaction() с настройкой скорости и порядка битов, transfer() с его полнодуплексной натурой, когда байт уезжает и приезжает одновременно. Плюс честное сравнение с I2C и UART — какая шина для какой работы. О границах договоримся сразу: SPI-периферию симулятор не эмулирует, поэтому в sim-блоке логику сдвига покажут светодиод и Serial-лог, а настоящий скетч с SPI.h — в коде ниже, для живой платы.
Четыре линии: кто говорит, кто слушает
Названия линий — честная стенография. MOSI — Master Out, Slave In: мастер, то есть наша плата, говорит, слейв слушает. MISO — зеркально: говорит слейв, слушает мастер. SCK — такт: импульсы генерирует всегда мастер, слейвы права тикать не имеют. CS (он же SS, Chip Select) — персональный провод каждого чипа: опустили в LOW — выбранный чип слушает шину, подняли — отвернулся и ни на что не реагирует.
На Uno эти четыре линии сидят на фиксированных пинах: MOSI — D11, MISO — D12, SCK — D13, SS — D10. Это не договорённость библиотеки, а периферия самого ATmega328P: перенести их на другие ноги нельзя, как нельзя вынести контакты USB-порта на заднюю стенку корпуса. Отсюда первое правило планировки макетки: под SPI места держат заранее, потому что пересадить шину поздно.
Полнодуплексность — то, что отличает SPI от знакомого по I2C полудуплекса. transfer() отправляет и принимает один и тот же кусок времени: пока по MOSI уезжают восемь бит, по MISO параллельно приезжают восемь чужих, и SPI.transfer(0x55) возвращает то, что слейв выдавил в ответ. Для чипов только на запись вроде 74HC595 линия MISO вообще не подключается — вернувшийся мусор просто выбрасывается. А для читающих чипов это главный канал: SD-карта отдаёт сектор именно так — вы в это же время шлёте ей нули-заполнители, чтобы дать такт. У I2C же одна линия SDA работает то на отправку, то на приём, и шина в каждый момент бежит либо туда, либо обратно.
| Линия | Пин на Uno | Кто ведёт | Роль в кадре |
|---|---|---|---|
| MOSI | D11 | мастер | данные от платы к чипу: бит за битом под такт |
| MISO | D12 | слейв | данные обратно: чип отвечает, пока мастер шлёт |
| SCK | D13 | мастер | такт: один импульс — один бит в обе стороны |
| CS / SS | D10 | мастер | выбор чипа: LOW — слушает, HIGH — отвязался |
begin, beginTransaction, transfer: минимальный скетч
Три вызова — и шина жива. SPI.begin() включает периферию и сажает пины на аппаратные функции. SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)) описывает кадр: 8 000 000 герц такта — половина от кварца 16 МГц, больше на Uno физически не выжать; MSBFIRST — старший бит уезжает первым; SPI_MODE0 — самая ходовая комбинация полярности и фазы такта, по ней живут и 595, и MAX7219. Дальше пружина любого кадра: CS вниз, SPI.transfer(байт), CS вверх. endTransaction() снимает настройки — между транзакциями другие библиотеки могут пересадить шину под себя, и я пользуюсь этим, когда на одной шине соседствуют быстрая SD-карта и какой-нибудь чип на 250 килогерцах.
Любое число в SPISettings легально, но ядро округляет его вниз к ближайшему делителю кварца: набор фиксированный — 8, 4, 2 и 1 МГц, затем 500, 250 и 125 кГц. Восьмёрка попадает точно, а вот 6 000 000 молча уедет на четыре: не пугайтесь, если чип работает вдвое медленнее задуманного. Для пары светодиодных чипов вся эта математика безразлична — берите миллион и забудьте; для SD-карты или Ethernet-модуля разница между делителями уже не косметика.
// Аппаратные линии SPI на Uno не переносятся: MOSI D11, MISO D12,// SCK D13, SS D10. CS держим как обычный пин — руками.#include <SPI.h> const int PIN_CS = 10; void setup() { pinMode(PIN_CS, OUTPUT); digitalWrite(PIN_CS, HIGH); // в покое чип не выбран SPI.begin(); Serial.begin(9600);} void loop() { SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); digitalWrite(PIN_CS, LOW); // опустили CS - чип слушает byte otvet = SPI.transfer(0b10110010); digitalWrite(PIN_CS, HIGH); // кадр завершён SPI.endTransaction(); Serial.print("otvet="); Serial.println(otvet, BIN); // что слейв вернул по MISO delay(500);}Грабля, на которую я потратил вечер: пин D10 должен быть OUTPUT и в покое держать HIGH. Если CS окажется входом и поймает на земле ноль, аппаратура ATmega решит, что её назначили слейвом: MOSI самопроизвольно станет входом, и мастер замолчит при идеальном скетче. Симптом узнаётся мгновенно — шина мертва целиком, а перезагрузка ничего не меняет. Вторая история — провода: на такте 8 МГц макетные перемычки длиннее полуметра уже ловят звон, и биты доезжают битыми. Если схема расползлась — снижайте частоту: SPISettings(1000000, ...) для болтающихся проводов, 250 000 для совсем длинных линий. Чипы медленного такта не обижаются — скорость не гордость, а инструмент.
Теперь честное соседство. I2C выигрывает проводами: две линии тянут десятки чипов, у каждого адрес, и для дешёвых датчиков это идеал — но платит тактом. 400 килогерц быстрого режима против наших восьми миллионов — в двадцать раз медленнее, плюс подтяжки к питанию обязательны, о чём я подробно писал в уроке про два провода. UART — это основной порт, который вы видите в мониторе, и второй UART на SoftwareSerial: два провода без такта, обе стороны заранее договорились о бодах. Зато участников ровно двое — третий на линию не сядет. SPI берёт скоростью и глупостью чипа: никаких адресов и подтяжек, слейв может вообще не иметь кварца, — но каждому новому чипу нужен свой CS-провод от мастера. Мой расклад по умолчанию простой: датчики — I2C, разговор с компьютером — UART, всё, что шустрит и обновляется, — SPI.
| SPI | I2C | UART | |
|---|---|---|---|
| Провода | 4: MOSI, MISO, SCK и CS на каждый чип | 2: SDA и SCL | 2: TX и RX |
| Скорость на Uno | до 8 МГц | 100 и 400 кГц | обычно 9600-115200 бод |
| Адресация | нет: выбирает пин CS | адрес 7 бит | нет: только пара |
| Типовая работа | матрицы, SD-карты, регистры | датчики и часы | плата и компьютер |
Справедливости ради: shiftOut не мусор. На статичной индикации — пара чипов, светодиоды мигают раз в секунду — сотня микросекунд на байт ничего не стоит, а пины остаются свободными. Мой водораздел простой: статика — shiftOut, всё, что мультиплексируется, анимируется или делит шину с быстрым соседом, — SPI. А если захочется выжать из платы всё и без аппаратной периферии, прямой доступ к портам из урока про PORTB переписывает кадр быстрее transfer — ценой кода, который страшно показать непосвящённым. Замерять и то и другое лучше числом, а не ощущением: приёмы отсчёта времени разобраны в уроке про micros().
И ещё одна деталь, которую я понял только на замерах: скорость шины — это про свободный процессор, а не про глаза. Светодиод не различит кадр за две микросекунды и кадр за сто — различит ваш loop(), который в первом случае успевает опросить кнопку, прочитать датчик и пересчитать анимацию, а во втором стоит у стены и ждёт.
- Четыре линии: MOSI D11 (плата говорит), MISO D12 (слейв отвечает), SCK D13 (такт всегда от мастера), CS D10 (персональный выбор чипа) — пины аппаратные и не переносятся
- SPI.transfer полнодуплексный: отправляет байт и возвращает пришедший за одно и то же время; у чипов только на запись MISO не подключается
- Скелет кадра: SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)), CS вниз, transfer, CS вверх, endTransaction
- SPI против I2C: 8 МГц против 400 кГц и отсутствие адресов против двух проводов; против UART — такт в проводе и больше двух участников
- Главные грабли: CS в INPUT валит шину в слейв-режим, длинные провода на 8 МГц ломают биты — лечится OUTPUT на D10 и снижением частоты
- 1. Сколько линий нужно каждому новому чипу на шине SPI?
- 2. Что вернёт вызов SPI.transfer(0b10110010)?
- 3. Где на Uno сидят линии SPI и можно ли их перенести на другие пины?
- 4. Каким именно способом мастер выбирает, кому адресован кадр?
- 5. Почему линии I2C тянут резисторами к питанию, а линии SPI — нет?
Частые вопросы по теме урока
- Сколько линий нужно каждому новому чипу на шине SPI?
- MOSI, MISO и SCK общие для всей шины, а CS индивидуален: мастер опускает в LOW его и только его — так выбирается адресат. Адресов в кадре нет. четыре MOSI, MISO и SCK общие, а CS — отдельный провод на каждый чип
- Что вернёт вызов SPI.transfer(0b10110010)?
- SPI полнодуплексный: пока восемь бит уезжают по MOSI, восемь бит приезжают по MISO. Для чипов только на запись возвращённое значение выбрасывают. байт, который слейв выдавил по MISO в это же время
- Где на Uno сидят линии SPI и можно ли их перенести на другие пины?
- Аппаратный блок SPI ATmega328P разведён на конкретные ноги, и библиотека лишь включает их. В отличие от shiftOut, пересадить шину на другие пины нельзя. MOSI D11, MISO D12, SCK D13, SS D10; перенести нельзя — это периферия чипа
- Каким именно способом мастер выбирает, кому адресован кадр?
- Протоколы адресации у SPI нет: чип слушает, пока его персональный CS в LOW. Поэтому каждому новому чипу нужен свой провод выбора от мастера. пином CS: опустили его в LOW — чип слушает шину
- Почему линии I2C тянут резисторами к питанию, а линии SPI — нет?
- I2C — открытый сток: линии отпускают, подтяжки поднимают. У SPI выходы толкающие, уровень держит активный драйвер, поэтому подтяжки не нужны. у SPI линии толкаются активными выходами: мастер или слейв держат уровень сами