На прошлом занятии мы познакомились с механизмом обработки прерываний шины SPI контроллера STM32F1, а также потом немного попрактиковались в данном вопросе.
Только всем этим мы занимались с использованием возможностей библиотеки HAL, а в данном занятии мы уже обратимся к помощи библиотеки LL, которая, как известно, такой простотой программирования, как HAL, похвастаться не может, зато она позволяет нам более гибко подходить к вопросам настройки и передачи данных.
Так как с аппаратной составляющей шины SPI мы уже полностью знакомы, так как все этом мы уже изучили в уроке 152, за исключением конечно работы с интерфейсом I2S, то можем смело приступить сразу к практической части.
Схема урока остаётся полностью та же как и в уроке 154
Логический анализатор также подключим
Подключим к ПК пока только ведущее устройство, питание к ведомому пока не подключаем.
Проект для ведущего устройства был сделан из проекта LL_SPI_MASTER урока 154 и назван был, соответственно, LL_SPI_MASTER_INT.
Откроем наш новый проект в Cube MX и включим прерывания на шине SPI1
Давайте также перейдём на стандартный режим обмена SPI0:0, чтобы нам постоянно в программе логического анализа не переключать режимы и мы уже проверили, что режим SPI1:1 также отлично работает
Сгенерируем проект, откроем его в Keil, подключим к дереву проекта файл max7219.c, настроим программатор на автоперезагрузку и отключим оптимизацию.
Откроем файл main.c и посмотрим, что изменилось в инициализации шины SPI1, не считая режима SPI0:0.
Произошла настройка приоритетов и включения глобальных прерываний
1 2 3 |
/* SPI1 interrupt Init */ NVIC_SetPriority(SPI1_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),0, 0)); NVIC_EnableIRQ(SPI1_IRQn); |
Больше, в принципе, никаких изменений не произошло. Локальные прерывания мы включим позже.
Объявим глобальные буферы приёма и передачи, а также переменные для флага и счётчиков
1 2 3 4 5 |
/* USER CODE BEGIN PV */ uint16_t src_buf[1024] = {0}; uint16_t dst_buf[1024] = {0}; uint8_t cnt, fl=0; uint32_t full_cnt=0; |
В функции main изменим имя локальной переменной
uint16_t i, n;
Удалим инициализацию несуществующей переменной
i=0; r=0;
Включение шины SPI1 пока тоже удалим, мы её включим чуть позже
LL_SPI_Enable(SPI1);
Включим три вида прерываний на шине SPI1 с помощью функций LL, которые установят биты RXNEIE, TXEIE и ERRIE в регистре SPI_CR2
1 2 3 4 |
LL_GPIO_SetOutputPin(GPIOB, LL_GPIO_PIN_12); LL_SPI_EnableIT_RXNE(SPI1); LL_SPI_EnableIT_TXE(SPI1); LL_SPI_EnableIT_ERR(SPI1); |
Заполним буфер передачи
1 2 3 4 5 6 |
Init_7219(); //fill src buffer for(i=0;i<1024;i++) { src_buf[i] = i; } |
Опустим ножку выбора, дождёмся освобождения буфера передачи, дадим команду на передачу первого полуслова, так как оно равно 0, то просто передадим ноль не из буфера, и напоследок установим бит SPE, тем самым дадим команду на начало передачи
1 2 3 4 5 |
LL_mDelay(2000); LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_4); while(!LL_SPI_IsActiveFlag_TXE(SPI1)) {} LL_SPI_TransmitData16 (SPI1, 0); LL_SPI_Enable(SPI1); |
Из бесконечного цикла пока удалим весь пользовательский код.
Передавать и принимать мы будем по кругу наши 1024 полуслова порциями по 128 полуслов, поэтому ножку выбора мы поднимем после передачи 128-го полуслова.
Отследим мы это в обработчике прерываний.
Добавим функцию обработки прерываний передачи по шине, в которой для начала проинкрементируем глобальный счётчик
1 2 3 4 5 |
/* USER CODE BEGIN 4 */ void SPI1_TX_Callback(void) { full_cnt++; } |
Вообще, конечно нехорошо работать с глобальными переменными в обработчиках прерываний, так как мы рискуем получить непредсказуемое их значение, но так как мы приблизительно знаем, когда должно произойти прерывание, то в нашем случае ничего страшного не будет. Да и нет у нас очередей в конфигурациях StandAlone.
Также добавим аналогичную функцию для обработки прерываний приёма, пока пустотелую
1 2 3 4 5 6 |
full_cnt++; } //----------------------------------------------------- void SPI1_RX_Callback(void) { } |
Добавим прототипы на наши функции в файле stm32f1xx_it.c
1 2 3 |
/* USER CODE BEGIN PFP */ void SPI1_TX_Callback(void); void SPI1_RX_Callback(void); |
В обработчике прерываний в этом же файле обработаем установки соответствующих флагов, вызвав нужные функции, ошибки обрабатывать не будет, надеюсь их у нас не будет, но тем не менее установку флага отследим
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void SPI1_IRQHandler(void) { /* USER CODE BEGIN SPI1_IRQn 0 */ if(LL_SPI_IsActiveFlag_RXNE(SPI1)) { SPI1_RX_Callback(); } else if(LL_SPI_IsActiveFlag_TXE(SPI1)) { SPI1_TX_Callback(); } else if(LL_SPI_IsActiveFlag_OVR(SPI1)) { __NOP(); } |
Вернёмся в функцию обработки прерывания по передаче в файл main.c и в том случае, если мы передали полуслово, кратное 128, отключим прерывания по передаче
1 2 3 4 5 |
full_cnt++; if(!(full_cnt%128)) { LL_SPI_DisableIT_TXE(SPI1); } |
Немного подождём и поднимем ножку Chip Select, так как иначе ножка поднимется рановато
1 2 3 |
LL_SPI_DisableIT_TXE(SPI1); for(cnt=0;cnt<=3;cnt++) ; LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_4); |
Затем установим наш пользовательский флаг
1 2 |
LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_4); fl=1; |
Во всех других случаях попадания в обработчик (когда передали не последнее полуслово) мы дадим команду на передачу следующего
1 2 3 4 5 6 |
fl=1; } else { LL_SPI_TransmitData16(SPI1, src_buf[full_cnt]); } |
А в обработчике приёма запишем принятое полуслово в соответствующую ячейку приёмного буфера
1 2 3 |
void SPI1_RX_Callback(void) { dst_buf[full_cnt-1] = LL_SPI_ReceiveData16(SPI1); |
Теперь осталось всё это скоординировать и отобразить на индикаторе в бесконечном цикле функции main(), где мы сначала, дождавшись установки нашего пользовательского флага, обнулим его
1 2 3 |
/* USER CODE BEGIN 3 */ while(!fl) {} fl=0; |
Установим значение локального счётчика на начало нашего пакета
1 2 |
fl=0; i = full_cnt - 128; |
Отобразим наши значения из обоих буферов на индикаторе
1 2 3 4 5 6 7 |
i = full_cnt - 128; for(n=0;n<128;n++) { NumberR_7219(src_buf[i+n]); NumberL_7219(dst_buf[i+n]); LL_mDelay(50); } |
Обнулим значение глобального счётчика, если он досчитал до конца
1 2 3 |
LL_mDelay(50); } if(full_cnt>1023) full_cnt = 0; |
Подождём секунду, опустим ножку выбора шины, отправим первое полуслово в регистр и включим прерывание по передаче
1 2 3 4 5 |
if(full_cnt>1023) full_cnt = 0; LL_mDelay(1000); LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_4); LL_SPI_TransmitData16(SPI1, src_buf[full_cnt]); LL_SPI_EnableIT_TXE(SPI1); |
Вот, в принципе, и весь наш код. Соберём его, прошьём контроллер и пока отключим его от ПК.
Подключим к ПК ведомое устройство и займёмся теперь его проектом, который сделаем из проекта LL_SPI_SLAVE урока 154 и присвоим ему имя LL_SPI_SLAVE_INT.
Откроем наш новый проект в Cube MX и включим прерывания на шине SPI1
Также перейдём на стандартный режим обмена SPI0:0
Сгенерируем проект, откроем его в Keil, подключим к дереву проекта файл max7219.c, настроим программатор на автоперезагрузку и отключим оптимизацию.
Объявим глобальные буфер передачи, буфер приёма, в котором нам достаточно будет 128 элементов, переменные для двух счётчиков и флага
1 2 3 4 5 6 |
/* USER CODE BEGIN PV */ uint16_t src_buf[1024] = {0}; uint16_t dst_buf[128] = {0}; uint16_t dst_cnt=0; uint32_t full_cnt=0; uint8_t fl=0; |
В функции main() переименуем локальную переменную
uint16_t i, n;
Удалим инициализацию несуществующей переменной
i=0; r=0;
Включение шины SPI1 пока тоже удалим, мы её включим чуть позже
LL_SPI_Enable(SPI1);
Заполним буфер передачи, передавать мы будем полуслова от 1024 до 1
1 2 3 4 5 6 |
Init_7219(); //fill src buffer for(i=0;i<1024;i++) { src_buf[i] = 1024-i; } |
Задержку удалим, так как все равно передача не начнётся, пока не будет тактированиия, которым занимается ведущий
LL_mDelay(2000);
Включим шину, отдадим команду на запись числа из массива в буфер передачи шины, затем включим три типа прерываний, аналогично ведущему
1 2 3 4 5 6 |
Number_7219(87654321); LL_SPI_Enable(SPI1); LL_SPI_TransmitData16 (SPI1, 1024); LL_SPI_EnableIT_RXNE(SPI1); LL_SPI_EnableIT_TXE(SPI1); LL_SPI_EnableIT_ERR(SPI1); |
Из бесконечного цикла пока удалим весь пользовательский код.
Добавим функцию для обработки передачи данных по шине, в которой проинкрементируем глобальный счётчик и дадим следующую команду на заполнение буфера
1 2 3 4 5 6 |
/* USER CODE BEGIN 4 */ void SPI1_TX_Callback(void) { full_cnt++; LL_SPI_TransmitData16(SPI1, src_buf[full_cnt]); } |
Добавим также обработчик приёма данных по шине, в котором мы запишем принятые данные в массив, проинкрементируем приёмный счётчик, если достигли последней посылки, установим пользовательский флаг и отключим прерывания по приёму и передаче
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
LL_SPI_TransmitData16(SPI1, src_buf[full_cnt]); } //----------------------------------------------------- void SPI1_RX_Callback(void) { dst_buf[dst_cnt] = LL_SPI_ReceiveData16(SPI1); dst_cnt++; if(!(full_cnt%128)) { fl=1; LL_SPI_DisableIT_TXE(SPI1); LL_SPI_DisableIT_RXNE(SPI1); } } |
В файле stm32f1xx_it.c добавим прототипы на наши функции
1 2 3 |
/* USER CODE BEGIN PFP */ void SPI1_TX_Callback(void); void SPI1_RX_Callback(void); |
Обработаем флаги в функции-обработчике прерываний шины SPI1
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/* USER CODE BEGIN SPI1_IRQn 0 */ if(LL_SPI_IsActiveFlag_RXNE(SPI1)) { SPI1_RX_Callback(); } else if(LL_SPI_IsActiveFlag_TXE(SPI1)) { SPI1_TX_Callback(); } else if(LL_SPI_IsActiveFlag_OVR(SPI1)) { __NOP(); } |
Вернёмся в файл main.c и займёмся теперь бесконечным циклом функции main(), в котором, как обычно, сначала дождёмся установки нашего флага и обнулим его
1 2 3 |
/* USER CODE BEGIN 3 */ while(!fl) {} fl=0; |
узнаем номер элемента и присвоим его переменной
1 2 |
fl=0; i = dst_buf[0]; |
Покажем значения элементов буферов передачи и приёма на индикаторе
1 2 3 4 5 6 7 |
i = dst_buf[0]; for(n=0;n<128;n++) { NumberR_7219(dst_buf[n]); NumberL_7219(src_buf[n+i]); LL_mDelay(50); } |
Произведём синхронизацию номеров элементов буферов ведущего и ведомого устройств и сбросим счётчик приёмного буфера
1 2 3 4 |
LL_mDelay(50); } full_cnt = dst_buf[127] + 1; dst_cnt = 0; |
Обнулим значение глобального счётчика, если он досчитал до конца
1 2 |
dst_cnt = 0; if(full_cnt>1023) full_cnt = 0; |
Отправим в буфер передачи шины следующее полуслово и включим наши прерывания
1 2 3 4 |
if(full_cnt>1023) full_cnt = 0; LL_SPI_TransmitData16(SPI1, src_buf[full_cnt]); LL_SPI_EnableIT_RXNE(SPI1); LL_SPI_EnableIT_TXE(SPI1); |
Соберём код, прошьём контроллер, подключим питание ведущего устройства от независимого источника и посмотрим, как происходит обмен
Обмен проходит отлично и всё синхронизируется.
Посмотрим также, как происходит обмен между устройствами, в программе логического анализа
Здесь также всё отлично и правильно.
Таким образом, сегодня мы произвели обмен между двумя устройствами на контроллерах STM32F1 посредством шины SPI, используя возможности библиотеки LL, а также механизм прерываний по событиям передачи и приёма по интерфейсу SPI.
Всем спасибо за внимание!
Предыдущий урок Программирование МК STM32 Следующий урок
Исходный код для ведущего устройства (MASTER)
Исходный код для ведомого устройства (SLAVE)
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Индикатор светодиодный семиразрядный с драйвером MAX7219
Логический анализатор 16 каналов можно приобрести здесь
Смотреть ВИДЕОУРОК (нажмите на картинку)
Здравствуйте, Владимир.
Не нашел в файле В файле stm32f1xx_it.c прототипы функций
/* USER CODE BEGIN PFP */
void SPI1_TX_Callback(void);
void SPI1_RX_Callback(void);
хотя в теле урока так прямо и сказано «В файле stm32f1xx_it.c добавим прототипы на наши функции», а сами тела функций в файле main.c
Здравствйте!
Так мы их сами же написали.
Но Вы же обращаетесь к функции SPI1_RX_Callback() в файле stm32f1xx_it.с, а сама функция «телом» помещена в main.c и более НИГДЕ не объявлена. Как SPI1_IRQHandler(void) при вызове SPI1_RX_Callback() находит ее местоположение если прототипа ее нигде нет?
Или так можно?
Мы же прописали прототип в файле stm32f1xx_it.с, вот и стало из него её видно. По функциям СИ очень скоро будет урок. Только там пока без модулей. Модульное программирование чуть позже. Для того я и начал курс уроков по СИ, чтобы подобные вопросы отпали.
Все делаю по Вашим урокам за исключением того, что соединяю в SPI FULL-DUPLEX STM32F103C6(MASTER) и STM32L053C6(SLAVE) и индикаторов у меня нет так что на мастере организован USB-SPI и вот у меня странный эффект возникает: при пересылке из мастера в слейв 5-ти 16-разрядных слов и ожидания в ответ 5 подготовленных слейвом во входном буфере слейва происходит явное сложение с исходящими данными слейва. Эффект пропадает если в функции SPI1_IRQHandler(void) закоментировать
// else if(LL_SPI_IsActiveFlag_TXE(SPI1))
// {
// SPI1_TX_Callback();
// }
а без закоментирования все выглядит так:
MASTER отправляе {512, 1024, 2048, 4096, 8192} — проверено логическим анализатором
SLAVE отправляет {123, 456, 789, 258, 369} — проверено логическим анализатором
SLAVE получает {968, 1813, 2306, 4465, 10053} — последнее значение мусор, но в остальном со сдвигом на одну позицию явное сложение.
Без наличия железа ответить тяжело