На данном уроке мы попытаемся соединить между собой два контроллера STM32 между собой.
Подобные уроки у нас уже были, когда мы соединяли два контроллера STM32F1 между собой по шине SPI, но делали мы это с использованием библиотек HAL и LL. Кто не смотрел данные уроки, то обязательно посмотрите. Это урок 153 и урок 154.
Теперь же мы попытаемся для достижения данной цели использовать CMSIS.
Я думаю, цель соединения по шине SPI между собой двух контроллеров, в объяснении не нуждается. Конечно же, самая основная цель — научиться настраивать контроллер для работы с шиной SPI в режиме SLAVE, что не всем удаётся. Хотя, вышеназванные уроки, в принципе, показали обратное, что ничего в этом страшного-то и нет. Видимо, сказывается наш опыт.
Схема урока будет та же самая, что и в уроках 153 и 154, только, опять же, соединю я всё между собой без использования макетных плат.
Для начала подсоединим к нашим платам программаторы и светодиодные индикаторы, которые подключены к шинам SPI2 (ножки PB12-PB14).
Вот это плата для устройства SLAVE
А вот это устройство MASTER
Вроде разницы особой нет, но она есть. Если вы увеличите картинку, то обязательно заметите, что на плате для устройства MASTER у меня впаяны другие гребёнки (двухсторонние). В чём их удобство вы увидите, когда я к ним подключу логически анализатор. Гребёнки такие мне приобрести готовые не удалось, но в одном из магазинов мне попались вот такие межплатные штрырьевые линейки
С данных линеек я аккуратненько снял верхние соединительные пластмассовые муфты
И получились двухсторонние гребёнки, которые я длинной стороной поместил в плату и запаял со стороны данных штырей. Получилось вот так
Соединим платы между собой по шинам SPI1 (PA4-PA7). Здесь уже используется и MISO. Ничего не перекрещивается — всё один в один, не как с USART. Также не забываем про общий провод
К тем же ножкам устройства MATER подключим логический анализатор, но с другой стороны платы
Вот он, смысл двухсторонних гребёнок. Нам теперь не надо использовать разветвляющих проводов, которые постоянно путаются между собой, а также макетных плат.
Подключим наш логический анализатор к ПК, а также подключим устройство MASTER и начнём писать под него проект.
Конечно же, за основу мы возьмём проект прошлого урока под названием CMSIS_LED7219 и назовём его CMSIS_SPI_MASTER.
Первым делом нам нужно будет переключить наш драйвер на SPI2.
Откроем наш проект в Keil и в файле main.c сделаем дубликат нашей функции SPI_Init и назовём после этого одну из функций-дубликатов SPI1_Init, а другую SPI2_Init.
Соответственно, в функции main() исправим вызов функции
SPI2_Init();
Перейдём теперь в тело этой функции и исправим там ножки в инициализации GPIO
//SPI2 GPIO
MODIFY_REG(GPIOB->CRH, GPIO_CRH_CNF13_0 | GPIO_CRH_CNF14_1 | GPIO_CRH_CNF15_0 | GPIO_CRH_MODE14,
GPIO_CRH_CNF13_1 | GPIO_CRH_CNF14_0 | GPIO_CRH_CNF15_1 | GPIO_CRH_MODE13 | GPIO_CRH_MODE15);
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPBEN);
Далее в данной функции APB2 меняем везде на APB1, так как SPI2 сидит на данной шине, SPI1 меняем на SPI2, а также у нас немного изменятся значения переключателей в предделителе, так как данная шина тактируется другой частотой. Поэтому я дам текст нашей функции целиком
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
static void SPI2_Init(void) { //SPI2 GPIO SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPBEN); MODIFY_REG(GPIOB->CRH, GPIO_CRH_CNF13_0 | GPIO_CRH_CNF14_1 | GPIO_CRH_CNF15_0 | GPIO_CRH_MODE14, GPIO_CRH_CNF13_1 | GPIO_CRH_CNF14_0 | GPIO_CRH_CNF15_1 | GPIO_CRH_MODE13 | GPIO_CRH_MODE15); //RCC peripheral clock enabling SET_BIT(RCC->APB1ENR, RCC_APB1ENR_SPI2EN); //Delay after an RCC peripheral clock enabling tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_SPI2EN); //8 bit 0:0 // MODIFY_REG(SPI2->CR1, SPI_CR1_BIDIMODE | SPI_CR1_BIDIOE | SPI_CR1_CRCEN | SPI_CR1_CRCNEXT | \ // SPI_CR1_DFF | SPI_CR1_RXONLY | SPI_CR1_LSBFIRST | \ // SPI_CR1_BR_2 | SPI_CR1_CPOL | SPI_CR1_CPHA , \ // SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_BR_1 | SPI_CR1_BR_0 | SPI_CR1_MSTR); //16 bit 0:0 // MODIFY_REG(SPI2->CR1, SPI_CR1_BIDIMODE | SPI_CR1_BIDIOE | SPI_CR1_CRCEN | SPI_CR1_CRCNEXT | \ // SPI_CR1_RXONLY | SPI_CR1_LSBFIRST | \ // SPI_CR1_BR_2 | SPI_CR1_CPOL | SPI_CR1_CPHA , \ // SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_BR_1 | SPI_CR1_DFF | SPI_CR1_BR_0 | SPI_CR1_MSTR); //16 bit 1:1 MODIFY_REG(SPI2->CR1, SPI_CR1_BIDIMODE | SPI_CR1_BIDIOE | SPI_CR1_CRCEN | SPI_CR1_CRCNEXT | \ SPI_CR1_RXONLY | SPI_CR1_LSBFIRST | SPI_CR1_BR_2, SPI_CR1_BR_0 | \ SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_BR_1 | SPI_CR1_DFF | \ SPI_CR1_MSTR | SPI_CR1_CPOL | SPI_CR1_CPHA); CLEAR_BIT(SPI2->CR2, 0x00FF); } |
Также нам надо будет в функции GPIO_Init инициализировать ножку CS для SPI2, это будет у нас PB12.
Также даю функцию целиком, так как лучше включение тактирования портов объединить в один вызов макроса
1 2 3 4 5 6 |
static void GPIO_Init(void) { SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN); MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF4_1 | GPIO_CRL_CNF4_0 | GPIO_CRL_MODE4_1, GPIO_CRL_MODE4_0); MODIFY_REG(GPIOB->CRH, GPIO_CRH_CNF12_1 | GPIO_CRH_CNF12_0 | GPIO_CRH_MODE12_1, GPIO_CRH_MODE12_0); } |
Далее переименуем макросы для ножки CS, которая у нас будет управлять выбором на шине SPI1 и добавим ещё два макроса для другой ножки CS, которая будет управлять уже выбором на шине SPI2, а также аналогично поступим и с макросами включения модулей SPI1 и SPI2
#define CS1_RESET() SET_BIT(GPIOA->ODR,GPIO_ODR_ODR4)
#define CS1_SET() CLEAR_BIT(GPIOA->ODR,GPIO_ODR_ODR4)
#define CS2_RESET() SET_BIT(GPIOB->ODR,GPIO_ODR_ODR12)
#define CS2_SET() CLEAR_BIT(GPIOB->ODR,GPIO_ODR_ODR12)
#define SPI1_ENABLE() SET_BIT(SPI1->CR1, SPI_CR1_SPE);
#define SPI2_ENABLE() SET_BIT(SPI2->CR1, SPI_CR1_SPE);
В main() также исправим вызовы наших макросов
CS2_RESET();
CS2_SET();
SPI2_ENABLE();
Но и это ещё не всё, не забываем про наш библиотечный файл max7219.c, в котором также надо исправить ножку CS в макросах
#define cs_set() CLEAR_BIT(GPIOB->ODR,GPIO_ODR_ODR12)
#define cs_reset() SET_BIT(GPIOB->ODR,GPIO_ODR_ODR12)
В функции Send_7219 также изменим модуль на SPI2, включая также и закомментированный участок кода, так как, мало ли, когда-то придётся использовать 8-битный режим
/*
while(!(READ_BIT(SPI2->SR, SPI_SR_TXE) == (SPI_SR_TXE))) {}
SPI2->DR = (uint16_t)rg;
while(!(READ_BIT(SPI2->SR, SPI_SR_RXNE) == (SPI_SR_RXNE))) {}
(void) SPI2->DR;
while(!(READ_BIT(SPI2->SR, SPI_SR_TXE) == (SPI_SR_TXE))) {}
SPI2->DR = (uint16_t)dt;
while(!(READ_BIT(SPI2->SR, SPI_SR_RXNE) == (SPI_SR_RXNE))) {}
(void) SPI2->DR;
*/
//16 bit
while(!(READ_BIT(SPI2->SR, SPI_SR_TXE) == (SPI_SR_TXE))) {}
SPI2->DR = (uint16_t)rg << 8 | dt;
while(!(READ_BIT(SPI2->SR, SPI_SR_RXNE) == (SPI_SR_RXNE))) {}
(void) SPI2->DR;
Вот теперь можно собрать проект и проверить работу индикатора.
Если всё работает, переходим в main.c и немного поправим функцию инициализации первого модуля SPI1_Init.
Вот эту строчку поставим в начало, так как сначала на ножку надо включать тактирование
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);
Здесь, в принципе, всё.
Перейдём в main() и вызовем данную функцию
1 2 |
GPIO_Init(); SPI1_Init(); |
Поднимем пока ножку выбора
1 2 |
delay_ms(100); CS1_RESET(); |
Вот это можно убрать
CS2_SET();
Включим модуль SPI1
1 2 |
CS2_RESET(); SPI1_ENABLE(); |
Добавим ещё одну локальную переменную
uint16_t i, r;
Из бесконечного цикла удалим весь код и добавим вот этот
1 2 3 4 5 6 7 8 9 10 11 12 13 |
while(1) { i++; if(i>9999) i=0; CS1_SET(); while(!(READ_BIT(SPI1->SR, SPI_SR_TXE) == (SPI_SR_TXE))) {} WRITE_REG(SPI1->DR, i); while(!(READ_BIT(SPI1->SR, SPI_SR_RXNE) == (SPI_SR_RXNE))) {} r = READ_REG(SPI1->DR); CS1_RESET(); NumberR_7219(i); NumberL_7219(r); delay_ms(100); |
Здесь мы передаём инкрементирующееся значение ведомому устройству, дожидаемся от него также 16-разрядной величины и показываем на индикаторе оба эти числа.
Соберём код, прошьём контроллер
Пока у нас будут только передаваемые числа, так как ведомый у нас подключен по шине, но кода там пока никакого нет.
Поэтому подключим теперь ведущее устройство к независимому источнику питания, а программатор устройства ведомого — к ПК.
Из проекта для устройства MASTER создадим проект для ведомого устройства с именем CMSIS_SPI_SLAVE.
Откроем наш проект в Keil и поправим в файле main.c функцию инициализации SPI1.
Исправим инициализацию ножек, так как теперь у нас ножки CS, SCK, и MOSI, наоборот, будут работать на вход, а MISO — на выход
MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF4_1 | GPIO_CRL_CNF5_1 | GPIO_CRL_CNF6_0 | GPIO_CRL_CNF7_0 | GPIO_CRL_MODE7,
GPIO_CRL_CNF4_0 | GPIO_CRL_CNF5_0 | GPIO_CRL_CNF6_1 | GPIO_CRL_CNF7_1 | \
GPIO_CRL_MODE4 | GPIO_CRL_MODE5 | GPIO_CRL_MODE6);
В настройке регистра CR1 у нас также будут изменения, так как теперь устройство у нас будет работать в режиме ведомого.
Внесём изменения для всех режимов, включая и закомментированные
//8 bit 0:0
// MODIFY_REG(SPI1->CR1, SPI_CR1_BIDIMODE | SPI_CR1_BIDIOE | SPI_CR1_CRCEN | SPI_CR1_CRCNEXT | \
// SPI_CR1_DFF | SPI_CR1_RXONLY | SPI_CR1_LSBFIRST | \
// SPI_CR1_BR_2 | SPI_CR1_CPOL | SPI_CR1_CPHA | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_MSTR, \
// SPI_CR1_BR_1 | SPI_CR1_BR_0);
//16 bit 0:0
// MODIFY_REG(SPI1->CR1, SPI_CR1_BIDIMODE | SPI_CR1_BIDIOE | SPI_CR1_CRCEN | SPI_CR1_CRCNEXT | \
// SPI_CR1_RXONLY | SPI_CR1_LSBFIRST | \
// SPI_CR1_BR_2 | SPI_CR1_CPOL | SPI_CR1_CPHA | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_MSTR, \
// SPI_CR1_BR_1 | SPI_CR1_DFF | SPI_CR1_BR_0);
//16 bit 1:1
MODIFY_REG(SPI1->CR1, SPI_CR1_BIDIMODE | SPI_CR1_BIDIOE | SPI_CR1_CRCEN | SPI_CR1_CRCNEXT | \
SPI_CR1_RXONLY | SPI_CR1_LSBFIRST | SPI_CR1_BR_2 | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_MSTR, \
SPI_CR1_BR_1 | SPI_CR1_DFF | SPI_CR1_BR_0 | SPI_CR1_CPOL | SPI_CR1_CPHA);
По большому счёту, три бита, которые я выделил, перейдут из 3 параметра макроса во 2-й.
В функции GPIO_Init уберём инициализацию ножки PA4, так как она теперь у нас инициализирована в функции SPI1_Init, останется там инициализация только ножки PB12
1 2 3 4 5 |
static void GPIO_Init(void) { SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPBEN); MODIFY_REG(GPIOB->CRH, GPIO_CRH_CNF12_1 | GPIO_CRH_CNF12_0 | GPIO_CRH_MODE12_1, GPIO_CRH_MODE12_0); } |
В функции main() обнулим наши переменные
1 2 |
SPI2_Init(); i=0; r=0; |
Вот это убираем, так как данной ножкой мы теперь не управляем
CS1_RESET();
В бесконечном цикле удалим весь код и добавим вот такой
1 2 3 4 5 6 7 8 9 |
while(1) { while(!(READ_BIT(SPI1->SR, SPI_SR_RXNE) == (SPI_SR_RXNE))) {} r = READ_REG(SPI1->DR); while(!(READ_BIT(SPI1->SR, SPI_SR_TXE) == (SPI_SR_TXE))) {} i = 9999 - r; WRITE_REG(SPI1->DR, i); NumberR_7219(r); NumberL_7219(i); |
Теперь здесь мы дожидаемся значения с ведущего, отнимаем его из числа 9999 и передаём результат назад ведущему, также затем мы и пришедшее и отправленное числа отображаем на индикаторе.
Попробуем собрать наш код и запустить его
Всё отлично передаётся и принимается.
Кто-то задавал вопрос, почему у нас при обмене получается, что на устройстве MASTER значение в правой части индикатора больше, чем на индикаторе устройства SLAVE. Думаю, тут ответ простой. Тот, кто знает, как идёт обмен по шине SPI никогда бы таким вопросом не задался. Обмен идёт побитно по шине и он именно обмен. Регистр в каждом узле шины только один. И когда мы передаём с мастера наше 16-битное значение, то мы, конечно же, принимаем предыдущий байт. Можно эту проблему обойти, но мы здесь не будем этим заниматься, так как нам именно интересен сырой обмен данными в нашем случае. А вообще можно было бы Мастеру сначала передать определённое значение, по которому Slave поймёт, что это значение именно для запроса данных и чтобы он ответил какими-нибудь 0xF0F0, например. Мастер прочитает эти данные, поймёт, что SLAVE ему готов ответить. SLAVE в это время подготавливает уже реальное значение, MASTER передаёт любое значение и в этом же сеансе обмена принимает нужное значение от Slave. Мы примерно так и делаем, когда работаем с реальными устройствами типа каких-то датчиков. Просто, используя библиотеку HAL, мы этого не видим, это всё спрятано в его функционале, а если мы откроем функцию обмена, то увидим, что всё там так и делается. И мы так и делали, когда работали с AVR. Ну да ладно, это всё мелочи. Будут ещё и датчики и много чего.
Давайте посмотрим наш обмен в программе логического анализа
Теперь посмотрим здесь
Отлично! У нас ничего не пропускается и всё доходит.
Таким образом, мы сегодня поработали с шиной SPI в режиме SLAVE или ведомого устройства, применяя при этом уже библиотеку CMSIS, тем самым обеспечив полнодуплексный обмен данными по шине SPI между двумя контроллерами.
Всем спасибо за внимание!
Предыдущий урок Программирование МК STM32 Следующий урок
Исходный код для ведущего устройства (MASTER)
Исходный код для ведомого устройства (SLAVE)
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Индикатор светодиодный семиразрядный с драйвером MAX7219
Логический анализатор 16 каналов можно приобрести здесь
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Добавить комментарий