| [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) {}
}