Интерфейс SPI.
SPI — последовательный синхронный полнодуплексный интерфейс передачи данных.
Разберёмся с каждым термином по отдельности.
Последовательный: данные передаются по одной линии последовательно бит за битом.
Синхронный: передача данных синхронизируется с помощью тактового сигнала, что позволяет передавать данные быстрее и надежнее.
Полнодуплексный: устройство может одновременно передавать и получать данные.
Топология «Мастер-слейв»: один мастер (Master) управляет одним или несколькими ведомыми устройствами (Slave).
Соединение устройств происходит следующим образом.
MOSI — Master Output / Slave Input. Выход ведущего / вход ведомого. Служит для передачи данных от ведущего устройства к ведомому.
MISO – Master Input / Slave Output. Вход ведущего / выход ведомого. Служит для передачи данных от ведомого устройства к ведущему.
SCK — Serial Clock. сигнал, используемый для генерации тактовой частоты синхронизации передачи данных
SS — Slave Select. Служит для выбора ведомого устройства, путем установки в низкий логический уровень. Если устройств несколько то в один момент времени может быть активно только одно устройство.
SS1,SS2 и т.д если ведомых устройств больше 1
Стоит отметить, что если мы только читаем данные из ведомого устройства, то нам нужны только провода SCK и MISO, а если только пишем в ведомое то SCK и MOSI.
Передача данных может осуществляться пакетами либо по 8 либо по 16 бит.
Возможна аппаратная и программная реализация протокола. Мы разберёмся с аппаратной, т.к. она проще и приводит к меньшему количеству ошибок. При аппаратной реализации SPI мы передаём данные в специальный регистр, а микроконтроллер сам устанавливает нужные уровни сигнала для передачи данных.
Ещё одним моментом является то что slave не может стучаться к мастеру. Чтобы получить данные от slave, master должен отправить запрос к нему.
Режимы работы
SPI поддерживает 4 режима работы, которые задаются комбинацией двух параметров:
CPOL (Clock Polarity) — полярность такта. Определяет, какой уровень сигнала на линии SCLK в состоянии покоя по умолчанию 0 или 1.
CPHA (Clock Phase) — фаза выборки.Определяет, по какому фронту (первому или второму) данные считаются вне зависимости от того возрастающий ли фронт или спадающий.
Основные функции
Реализация интерфейса SPI возможна классическим способом, через регистры или HAL функции, либо с помощью DMA.
Когда мы реализуем SPI первым способом, без DMA, то возможно два режима работы.
Блокирующий режим (Polling) — Процессор сам постоянно проверяет флаги в SPI (или ждёт внутри HAL-функции), пока операция передачи/приёма не завершится. Соответственно в это время процессор занят и не может делать ничего другого, он занят ожиданием.
Режим с прерываниями (Interrupts) — Процессор запускает передачу и продолжает выполнять другие задачи. Когда передача завершится аппаратный модуль SPI генерирует прерывание, и в вашем коде вызывается специальная функция-обработчик (коллбек).
При реализации с помощью DMA, процессор только запускает функцию DMA и дальше работает, выполняя другие функции. DMA в это время работает параллельно. При завершении передачи вызывается колбек функция. Удобно применять на больших объемах данных.
DMA может генерировать несколько типов прерываний.
- Transfer Complete (TC) — передача завершена. Вызывается HAL_SPI_RxCpltCallback()
- Half Transfer Complete (HT) — половина буфера передана. Вызывается HAL_SPI_TxHalfCpltCallback() либо половина буфера получена HAL_SPI_RxHalfCpltCallback()
- Transfer Error (TE) — ошибка передачи. Вызовет HAL_SPI_ErrorCallback()
- Abort Complete (ABT) — завершение по принудительной остановке.Может быть полезно, если нужно срочно остановить передачу.
Основные функции (HAL, без DMA)
Передача данных (блокирующая)
HAL_SPI_Transmit(&hspi1, uint8_t *pData, uint16_t Size, uint32_t Timeout);
Приём данных (блокирующая)
HAL_SPI_Receive(&hspi1, uint8_t *pData, uint16_t Size, uint32_t Timeout);
Передача и приём одновременно (полный дуплекс)
HAL_SPI_TransmitReceive(&hspi1, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout);
Те же функции, но в режиме прерываний (IRQ)
HAL_SPI_Transmit_IT(…)
HAL_SPI_Receive_IT(…)
HAL_SPI_TransmitReceive_IT(…)
Основные функции (HAL, с DMA)
Передача данных через DMA
HAL_SPI_Transmit_DMA(&hspi1, uint8_t *pData, uint16_t Size);
Приём данных через DMA
HAL_SPI_Receive_DMA(&hspi1, uint8_t *pData, uint16_t Size);
Полный дуплекс через DMA
HAL_SPI_TransmitReceive_DMA(&hspi1, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size);
Модуль датчика атмосферного давления и температуры BMP280.
Подключим модуль датчика давления и температуры BMP280 к STM32F4 Discovery с STM32F407VGT6. Считаем данные с него по SPI и выведем на TFT Display ST7735 который соединён тоже по SPI. Как подсоединить дисплей есть в этой статье ( ссылка ) повторяться не будем.Сам датчик выглядит так.Т.к. проект достаточно простой, то будем использовать обычные блокирующие функции получения и передачи.
Настройка в CubeIDE.
Будем использовать SPI2 нашего микроконтроллера, т.к. SPI1 занят под наш дисплей. Настройки ставим следующим образом.
У нас подсвечиваются нужные пины для соединения.
Как видно PC2-MISO PC3-MOSI PB10-SCK. Ещё нам необходим один пин для Chip Select. Пусть будет PC1, его выставляем в Output.
Схема подключения получилась вот такая.
Код программы.
Для чистоты кода создадим отдельный файл для работы с нашим датчиком. Назовём его BMP280.c и поместим в папку Src. соответственно заголовочный файл BMP280.h в папку Inc.
Подключим их в нашем файле main.c
/* USER CODE BEGIN Includes */ #include "BMP280.h" /* USER CODE END Includes */
Сначала нам необходимо проверить что наш датчик подключен и отвечает корректно. Для этого есть специальный регистр Chip ID.У BMP280 этот регистр находится по адресу 0xD0. Значение в этом регистре всегда 0x58(для BMP280). Если приходит что то другое, значит надо искать ошибку.
Создадим функцию чтения регистра.
static uint8_t BMP280_ReadReg(uint8_t reg) { uint8_t tx = reg | 0x80; uint8_t rx = 0; BMP_CS_L(); HAL_SPI_Transmit(&hspi2, &tx, 1, HAL_MAX_DELAY); HAL_SPI_Receive(&hspi2, &rx, 1, HAL_MAX_DELAY); BMP_CS_H(); return rx; }
Установка нашего CS в высокий и низкий уровень
static inline void BMP_CS_L(void) { HAL_GPIO_WritePin(GPIOC, GPIO_PIN_1, GPIO_PIN_RESET); }static inline void BMP_CS_H(void) { HAL_GPIO_WritePin(GPIOC, GPIO_PIN_1, GPIO_PIN_SET);}
Вызываем эту функцию.
uint8_t chip_id = BMP_ReadReg(0xD0);
Далее создадим функцию записи в регистр.
static void BMP280_WriteReg(uint8_t reg, uint8_t val) { uint8_t tx[2] = { reg & 0x7F, val }; BMP_CS_L(); HAL_SPI_Transmit(&hspi2, tx, 2, HAL_MAX_DELAY); BMP_CS_H(); }
В чипе BMP280 прошиты коэффициенты калибровки, которые производитель записывает при тестировании датчика.Когда мы читаем сырые данные температуры и давления, они сами по себе ничего не значат. Чтобы перевести их в градусы и Паскали, нужно подставить их в формулы, которые используют калибровочные коэффициенты. Для этого создадим функцию получения калибровочных параметров.
static void BMP280_ReadCalibrationData(void) { dig_T1 = BMP280_ReadReg(0x88) | (BMP280_ReadReg(0x89) << 8); dig_T2 = BMP280_ReadReg(0x8A) | (BMP280_ReadReg(0x8B) << 8); dig_T3 = BMP280_ReadReg(0x8C) | (BMP280_ReadReg(0x8D) << 8); dig_P1 = BMP280_ReadReg(0x8E) | (BMP280_ReadReg(0x8F) << 8); dig_P2 = BMP280_ReadReg(0x90) | (BMP280_ReadReg(0x91) << 8); dig_P3 = BMP280_ReadReg(0x92) | (BMP280_ReadReg(0x93) << 8); dig_P4 = BMP280_ReadReg(0x94) | (BMP280_ReadReg(0x95) << 8); dig_P5 = BMP280_ReadReg(0x96) | (BMP280_ReadReg(0x97) << 8); dig_P6 = BMP280_ReadReg(0x98) | (BMP280_ReadReg(0x99) << 8); dig_P7 = BMP280_ReadReg(0x9A) | (BMP280_ReadReg(0x9B) << 8); dig_P8 = BMP280_ReadReg(0x9C) | (BMP280_ReadReg(0x9D) << 8); dig_P9 = BMP280_ReadReg(0x9E) | (BMP280_ReadReg(0x9F) << 8); }
Настроим сам датчик. Для этого записываем в регистр датчика 0xF4 значение 0x27, чтобы включить нормальные измерения температуры и давления в непрерывном режиме, а в регистр 0xF5 значение 0xA0, чтобы задать интервал между измерениями примерно 1 секунду, отключить цифровой фильтр и использовать стандартный SPI; эти настройки обеспечивают простую и стабильную работу датчика. Всё это вместе с калибровочными параметрами поместим в функцию инициализации.
void BMP280_Init(void) { BMP280_WriteReg(0xF4, 0x27); BMP280_WriteReg(0xF5, 0xA0); BMP280_ReadCalibrationData(); }
И вызовем её в main.c
BMP280_Init</span><span style="font-weight: 400;">();
Напишем функцию компенсации. Она нужна чтобы верно рассчитывать значения температуры и давления.Данные взяты из даташита.
</pre> static int32_t BMP280_CompensateTemp(int32_t adc_T) { int32_t var1 = ((((adc_T >> 3) - ((int32_t)dig_T1 << 1))) * ((int32_t)dig_T2)) >> 11; int32_t var2 = (((((adc_T >> 4) - ((int32_t)dig_T1)) * ((adc_T >> 4) - ((int32_t)dig_T1))) >> 12) * ((int32_t)dig_T3)) >> 14; t_fine = var1 + var2; return (t_fine * 5 + 128) >> 8; // °C * 100 } static float BMP280_CompensatePressure(int32_t adc_P) { int64_t var1, var2, p; var1 = ((int64_t)t_fine) - 128000; var2 = var1 * var1 * (int64_t)dig_P6; var2 = var2 + ((var1 * (int64_t)dig_P5) << 17); var2 = var2 + (((int64_t)dig_P4) << 35); var1 = ((var1 * var1 * (int64_t)dig_P3) >> 8) + ((var1 * (int64_t)dig_P2) << 12); var1 = (((((int64_t)1) << 47) + var1)) * ((int64_t)dig_P1) >> 33; if (var1 == 0) return 0; p = 1048576 - adc_P; p = (((p << 31) - var2) * 3125) / var1; var1 = (((int64_t)dig_P9) * (p >> 13) * (p >> 13)) >> 25; var2 = (((int64_t)dig_P8) * p) >> 19; p = ((p + var1 + var2) >> 8) + (((int64_t)dig_P7) << 4); return (float)p / 25600.0f; } <pre>
Теперь нам остается только вызывать функции измерения температуры и давления и вывести значения на дисплей.
while (1) { HAL_Delay(1000); float temp = BMP280_ReadTemperature(); // °C float pres = BMP280_ReadPressure(); // hPa sendNumber((int32_t)temp); // вывод давления на дисплей /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ }
Мы разобрали работу интерфейса SPI и на практике закрепили знания, подключив датчик BMP280. В процессе научились читать калибровочные данные, настраивать рабочий режим через регистры, а также использовать формулы из даташита для получения температуры и давления.