[Home] | [Donate!] [Контакты] |
[<< SPI. Пример. Передача данных по прерываниям] | [Измерение температуры. Цифровой датчик DS18B20 >>] |
Пример очень похож на предыдущий, здесь также управление устройством SPI2 происходит по прерываниям. Но в данном случае, для передачи данных устройством SPI1, используется DMA.
Оглавление
Если сравнивать с предыдущим примером, то функция main здесь практически остаётся без изменений. Лишь дополнительно включается тактирование DMA1, а при настройке SPI разрешаются запросы DMA.
Все изменения сосредоточены внутри функции spi_send_receive, которая полностью переписана для осуществления передачи данных с помощью DMA. Реализуются такая передача весьма просто. Нужно лишь указать DMA, что передавать, куда и в каком количестве. После этого включаем используемые каналы DMA и ждём завершения передачи.
Желательно время, необходимое для передачи данных, потратить на какую-то полезную работу. Используемый здесь вариант
while(DMA1_Channel2->CNDTR!=0) {}
сводит на нет все преимущества DMA. Вместо того чтобы освободиться для выполнения вычислений, микроконтроллер снова ждёт завершения передачи данных в бесполезном цикле. Правильным решением было бы настроить DMA на генерацию прерывания после завершения передачи данных и уже в обработчике этого прерывания выполнить требуемые действия с полученными данными.
Далее приведён полный текст программы.
/* File: main.cpp Простейший пример с использованием SPI: передача данных между SPI1 и SPI2 микроконтроллера. Использование DMA для передачи данных. SPI1 настроим как ведущее, SPI2 - ведомое устройство. MCU: STM32F100RB SPI1: PA4 - NSS; PA5 - SCK; PA6 - MISO; PA7 - MOSI. SPI2: PB12 - NSS; PB13 - SCK; PB14 - MISO; PB15 - MOSI. Для теста одноимённые выводы обоих устройств SPI соединяем между собой. DMA1: SPI1_RX - channel 2 SPI1_TX - channel 3 */ /** * IMPORTANT NOTE! * The symbol VECT_TAB_SRAM needs to be defined when building the project * if code has been located to RAM and interrupts are used... */ #include "stm32f10x.h" // Используемые полярность и фаза тактового сигнала; // должны быть одинаковыми и для ведущего, и для ведомого устройства. const uint32_t CPOL=SPI_CR1_CPOL*0; const uint32_t CPHA=SPI_CR1_CPHA*1; // Тестовые данные для передачи и // буферы для хранения полученных данных. // Сообщение, передаваемое через SPI1. uint8_t a1[]="Hello!"; // Сообщение, получаемое SPI1. uint8_t b1[sizeof a1]; // Сообщение, передаваемое через SPI2 (при необходимости будет // дополнено нулевыми байтами). uint8_t a2[]="hi!"; // Количество уже переданных устройством SPI2 байт. volatile unsigned int a2n=0; // С помощью этой глобальной переменной, сообщаем обработчику // прерывания от SPI2, сколько байт ему требуется передать. unsigned int a2n_max=0; // Буфер для сообщения, получаемого SPI2 (не помещающиеся в буфер // данные будут отброшены). const unsigned int b2_size=8; // Размер буфера. volatile uint8_t b2[b2_size]; // Буфер. volatile unsigned int b2n=0; // Текущее количество данных в буфере. // Передача/получение заданного количества байт через SPI // (используется DMA). // Возвращает false, если предыдущая передача ещё не закончена, // true в случае успеха. bool spi_send_receive( SPI_TypeDef *spi, // Используемое устройство SPI. const void *tx_buf, // Буфер с передаваемыми данными. void *rx_buf, // Буфер для принимаемых данных. uint16_t n // Размер буфера (оба одинаковые). ) { if(n==0) return true; if(DMA1_Channel2->CNDTR!=0) return false; DMA1_Channel2->CCR&=0x80000000; // Сброс всех значимых битов. DMA1_Channel2->CMAR=(uint32_t)rx_buf; DMA1_Channel2->CPAR=(uint32_t)&SPI1->DR; DMA1_Channel2->CNDTR=n; DMA1_Channel2->CCR|= DMA_CCR2_MINC| DMA_CCR2_EN| (1<<8); // PSIZE: 16 bits DMA1_Channel3->CCR&=0x80000000; // Сброс всех значимых битов. DMA1_Channel3->CMAR=(uint32_t)tx_buf; DMA1_Channel3->CPAR=(uint32_t)&SPI1->DR; DMA1_Channel3->CNDTR=n; DMA1_Channel3->CCR|= DMA_CCR3_MINC| DMA_CCR3_DIR| // Mem ---> Periph. DMA_CCR3_EN| (1<<8); // PSIZE: 16 bits return true; } // Обработчик прерываний от SPI2: // выясняем причину прерывания и выполняем требуемые действия. extern "C" void SPI2_IRQHandler() { if(SPI2->SR&SPI_SR_TXE) // Если установлен TXE флаг... { if(a2n<a2n_max) { SPI2->DR=(a2n<sizeof a2)?a2[a2n]:0; a2n++; } else SPI2->CR2&=~SPI_CR2_TXEIE; } if(SPI2->SR&SPI_SR_RXNE) // Если установлен RXNE флаг... { uint8_t d=SPI2->DR; if(b2n<b2_size) b2[b2n++]=d; } } int main(void) { // Будем обрабатывать прерывание от SPI2, конфигурируем NVIC. // Задаём приоритет (используемая идиома задаёт низший приоритет). NVIC_SetPriority(SPI2_IRQn, (1<<__NVIC_PRIO_BITS)-1); // Разрешаем обработку этого прерывания. NVIC_EnableIRQ(SPI2_IRQn); // Включаем тактирование используемых устройств: // DMA; // SPI1 и порт ввода-вывода GPIOA (SPI1 использует выводы PA4..PA7); // SPI2 и порт ввода-вывода GPIOB (SPI2 использует PB12..PB15). RCC->AHBENR|=RCC_AHBENR_DMA1EN; RCC->APB2ENR|= RCC_APB2ENR_SPI1EN| RCC_APB2ENR_IOPAEN| RCC_APB2ENR_IOPBEN; RCC->APB1ENR|=RCC_APB1ENR_SPI2EN; // Конфигурируем выводы SPI1, учитывая, что у нас SPI1 - ведущее. // PA4, SPI1_NSS: alt. out, push-pull, high speed // PA5, SPI1_SCK: alt. out, push-pull, high speed // PA6, SPI1_MISO: input, pull up/down // PA7, SPI1_MOSI: alt. out, push-pull, high speed GPIOA->CRL= GPIOA->CRL&~0xFFFF0000| 0xB8BB0000; // Настраиваем подтяжку входа PA6 (SPI1_MISO) - к высокому уровню // (если вход окажется не подключён, SPI будет получать все // единичные биты; вообще использование подтяжки необязательно). GPIOA->BSRR=GPIO_BSRR_BS6; // Конфигурируем выводы SPI2 (SPI2 у нас ведомое). // PB12, SPI2_NSS: input, pull up/down // PB13, SPI2_SCK: input, pull up/down // PB14, SPI2_MISO: alt. out, push-pull, high speed // PB15, SPI2_MOSI: input, pull up/down GPIOB->CRH= GPIOB->CRH&~0xFFFF0000| 0x8B880000; // Настраиваем подтяжку входов (подтяжка необязательна, но // не помешает в случае отсутствия физического подключения, // когда вход остаётся "висящим"). GPIOB->BSRR= GPIO_BSRR_BS12| GPIO_BSRR_BR13| GPIO_BSRR_BS15; // Конфигурируем SPI1 (обычный ведущий режим в данном случае). // BIDIMODE: 0, включение режима с одной линией данных (отключено, // используется обычный режим с двумя линиями для передачи данных); // BIDIOE: 0, направление передачи (используется при BIDIMODE=1); // CRCEN: 0, включение аппаратного подсчёта CRC (отключено); // CRCNEXT:0, бит связан с вычислением CRC, используется при CRCEN=1; // DFF: 0, формат фрейма данных (здесь - 8-битовый фрейм); // RXONLY: 0, включение режима "только приём" (здесь - полнодуплексная связь); // SSM: 0, включение режима программного управления сигналом NSS; // SSI: 0, при SSM=1 бит замещает значение со входа NSS (здесь - не используется); // LSBFIRST: 0, порядок передачи битов (здесь - первым передаётся старший); // SPE: 0, бит включения SPI (здесь разделяем этапы конфигурирования и включения); // BR[2:0]: управление скоростью передачи (не влияет, если SPI настроен как подчинённое устройство); // здесь задано 0x7, что соотв. макс. делителю /256 (для теста выбираем минимальную скорость); // MSTR: 1, бит переключения в ведущий режим. SPI1->CR1= SPI_CR1_MSTR| // Ведущее устройство. SPI_CR1_BR| // Минимальная скорость для теста. CPOL| // Полярность и CPHA; // фаза тактового сигнала SPI. // С помощью регистра CR2 настраиваем генерацию запросов на // прерывание и DMA (если нужно); с помощью бита SSOE запрещаем или // разрешаем использовать ведущему устройству вывод NSS как выход. SPI1->CR2&=~0xE7; // Сбрасываем все значимые биты регистра. SPI1->CR2|= SPI_CR2_SSOE| // NSS будет выходом. SPI_CR2_RXDMAEN|// Установка флага RXNE формирует DMA запрос. SPI_CR2_TXDMAEN;// Установка флага TXE формирует DMA запрос. // Конфигурируем SPI2 для работы в обычном ведомом режиме. SPI2->CR1= // Сбрасываем все биты регистра и CPHA|CPOL; // задаём полярность и фазу SCK. SPI2->CR2&=~0xE7; // Сбрасываем все значимые биты регистра CR2. SPI2->CR2|= // Разрешаем прерывания при установке флагов SPI_CR2_RXNEIE| // RXNE и SPI_CR2_TXEIE; // TXE. // Включаем SPI1. // Передача не начнётся, пока не запишем что-то в регистр данных, // но установится состояние выходов SPI (выходы переходят из Z- // состояния в состояние формирования выходного сигнала). // После этого можно будет включить ведомое устройство без // опасения, что оно получит некоторое количество мусорных данных // в процессе включения ведущего. SPI1->CR1|=SPI_CR1_SPE; // Не слишком изящный способ сообщить обработчику прерывания SPI2, // сколько следует передать данных (сколько ожидает ведущее // устройство). a2n_max=sizeof a1; // Включаем SPI2. SPI2->CR1|=SPI_CR1_SPE; // Даём время ведомому устройству подготовиться к обмену - записать // первый отправляемый байт в свой регистр данных (это произойдёт // в обработчике прерывания сразу после включения SPI2). while(a2n<1) {} // Отправляем данные через ведущее устройство, побочным продуктом // чего всегда является получение такого же объёма входящих данных. spi_send_receive(SPI1, a1, b1, sizeof a1); // Происходит передача данных; // SPI1 выполняет обмен с помощью DMA; // SPI2 - по прерываниям. // О завершении обмена можно узнать по обнулению регистра // DMA1_Channel2->CNDTR; // либо по установленному флагу DMA_ISR_TCIF2 в регистре DMA1->ISR. // Кстати, можно разрешить генерацию прерывания на установку флага // и тогда о завершении передачи узнаем по вызову обработчика, // а до тех пор заняться другими делами. // Здесь наиболее примитивный вариант с ожиданием цикле. while(DMA1_Channel2->CNDTR!=0) {} // Обмен завершён, буфер b1 теперь содержит полученные // ведущим устройством данные. Можем анализировать их. // Буфер b2 содержит полученные ведомым устройством данные. // Если не планируется дальнейший обмен, устройства SPI // могут быть отключены. // Рекомендуемая Руководством процедура (при работе в обычном // полнодуплексном режиме): // после получения последнего байта ждём установки флага TXE, // затем ждём сброса BSY, после чего отключаем SPI. while(!(SPI2->SR&SPI_SR_TXE)) {} while(SPI2->SR&SPI_SR_BSY) {} SPI2->CR1&=~SPI_CR1_SPE; while(!(SPI1->SR&SPI_SR_TXE)) {} while(SPI1->SR&SPI_SR_BSY) {} SPI1->CR1&=~SPI_CR1_SPE; /* Infinite loop */ while (true) {} }