Получить или отправить микроконтроллером данные через какой-нибудь интерфейс дело серьезное. Возможности МК и моя фантазия предлагают три варианта:
- Линейный путь прямой как стрела
- Более надёжный через прерывания
- Экзотический через DMA
Коротко пробежимся по первым двум. В целом, они достаточно похожи: мы загружаем данные в соответствующий регистр, а далее проверяем регистр, который подтверждает нам, что данные ушли. Только в первом случае бесконечный цикл проверяет регистр, будто вода точит камень. Так вот, как долго ждать пока вода наточит камень? Очень долго, так и МК может зависнуть на этом процессе надолго.
Во втором случае, мы регистрируем прерывание, которое сработает как только данные уйдут.
Это как неожиданный звонок проследи рабочего дня отвлечет МК от текущих операций. Как в случае со звонком вам захочется узнать важное ли дело вас отрывает, так и в случае МК нужно будет запрограммировать соответствующую реакцию. Нужно ли так усложнять или проще выключить телефон, то есть использовать простой бесконечный цикл, решает всегда разработчик.
DMA стоит особняком, если в первых случаях вы контролируете отправку каждого байта самостоятельно, то DMA позволяет оперировать передачей массивов.
Алгоритм следующий:
- Вы загружаете данные в целевой массив или устанавливаете количество данных, которое надо принять
- Разрешаете отправку или получение
- Ожидание выполнения отправки или заполнения целевого буфера
Это можно сравнить с отправкой корабля, в то время как первые способы это курьерская служба.
HOWTO:
Настройка отправки данных по SPI практически полностью повторяет процедуру для приема данных. Поэтому здесь будет приведена только часть TX. Также здесь не будет приведена часть для инициализации самого интерфейса SPI.
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);
Для начала включаем тактирование для DMA.
LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_5, LL_DMA_DIRECTION_MEMORY_TO_PERIPH);
Указываем, что данные будут переходить от памяти МК в периферию вовне.
LL_DMA_SetChannelPriorityLevel(DMA1, LL_DMA_CHANNEL_5, LL_DMA_PRIORITY_LOW);
Устанавливаем приоритет.
LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_5, LL_DMA_MODE_NORMAL);
Режим не циклический.
LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_5, LL_DMA_PERIPH_NOINCREMENT);
LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_5, LL_DMA_MEMORY_INCREMENT);
Указатели могут автоматически инкрементироваться после передачи данных.
LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_5, LL_DMA_PDATAALIGN_BYTE);
LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_5, LL_DMA_MDATAALIGN_BYTE);
Устанавливаем выравнивание для периферии и памяти.
LL_DMA_ConfigAddresses(DMA1, LL_DMA_CHANNEL_5,
Функция конфигурирования адреса
(uint32_t)BufferForDataTransmitting,
LL_SPI_DMA_GetRegAddr(SPI2),
Указываем сам буфер, из которого будут читаться данные. Так как он -- источник данных он указывается раньше. Указывается источник данных -- в нашем случае SPI.
Для приема данных необходимо сначала указать интерфейс SPI как источника данных, а потом буфер, куда данные прибудут.
LL_DMA_GetDataTransferDirection(DMA1, LL_DMA_CHANNEL_5));
Берем значения направления из характеристик канала DMA, аналогично, можно указать LL_DMA_DIRECTION_MEMORY_TO_PERIPH.
LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_5, Length);
Указываем количество данных, которые должны быть пересланы
LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_5);
LL_DMA_EnableIT_TE(DMA1, LL_DMA_CHANNEL_5);
LL_SPI_EnableDMAReq_TX(SPI2);
Не забываем включать прерывания, которые запустят наши DMA1_Channel5_IRQHandler (в зависимости от startup_stm32f103xb, например)
После этого можно свободно пользоваться интерфейсом SPI
LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_5); //transmit
Отключаем канал на всякий случай, чтобы загрузить в буфер BufferForDataTransmitting передачи важные сведения.
LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_5, how_many_data_to_transmit);
Указываем сколько байт передать из буфера.
LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_5); //transmit
Включаем канал и ждем, когда сработает