poniedziałek, 20 lipca 2020

[5] STM32F429I - Biblioteki LL - Obsługa wyświetlacza TFT SPI

W tym poście chciałbym opisać obsługę wyświetlacza TFT zamontowanego na płytce rozwojowej STM32F429ZI. Do komunikacji z układem ILI9341 wykorzystałem interfejs SPI.


CubeMx:


W programie należy uruchomić następujące wyprowadzenia jako piny wyjściowe:
  • PD13 - DC
  • PD12 - RESET
  • PC2 - CS
Pin CS jest sterowany z zewnątrz. Linia NSS (sprzętowe sterowanie linią CS) zostaje nie wykorzystywana i wolna. Można ją dowolnie podłączyć i obsłużyć. 
Wykorzystywanie zewnętrznego pinu CS wydaje się najrozsądniejszym rozwiązaniem. Ponieważ pozwala na podłączenie większej ilości układów do interfejsu. Natomiast gdy zostanie wykorzystywana wewnętrzna linia NSS to dany interfejs może obsłużyć tylko jeden układ do niego podłączony.

Następnie należy uruchomić interfejs SPI5:


Wyświetlacz po SPI:


Teraz przejdę do opisu najważniejszych funkcji w programie.

Wygenerowana inicjalizacja interfejsu SPI5 wygląda następująco:

  1. static void MX_SPI5_Init(void)
  2. {
  3.   /* USER CODE BEGIN SPI5_Init 0 */
  4.   /* USER CODE END SPI5_Init 0 */
  5.   LL_SPI_InitTypeDef SPI_InitStruct = {0};
  6.   LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
  7.   /* Peripheral clock enable */
  8.   LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_SPI5);
  9.   LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOF);
  10.   /**SPI5 GPIO Configuration  
  11.   PF7   ------> SPI5_SCK
  12.   PF8   ------> SPI5_MISO
  13.   PF9   ------> SPI5_MOSI
  14.   */
  15.   GPIO_InitStruct.Pin = LL_GPIO_PIN_7|LL_GPIO_PIN_8|LL_GPIO_PIN_9;
  16.   GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
  17.   GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
  18.   GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
  19.   GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
  20.   GPIO_InitStruct.Alternate = LL_GPIO_AF_5;
  21.   LL_GPIO_Init(GPIOF, &GPIO_InitStruct);
  22.   /* USER CODE BEGIN SPI5_Init 1 */
  23.   /* USER CODE END SPI5_Init 1 */
  24.   /* SPI5 parameter configuration*/
  25.   SPI_InitStruct.TransferDirection = LL_SPI_FULL_DUPLEX;
  26.   SPI_InitStruct.Mode = LL_SPI_MODE_MASTER;
  27.   SPI_InitStruct.DataWidth = LL_SPI_DATAWIDTH_8BIT;
  28.   SPI_InitStruct.ClockPolarity = LL_SPI_POLARITY_LOW;
  29.   SPI_InitStruct.ClockPhase = LL_SPI_PHASE_1EDGE;
  30.   SPI_InitStruct.NSS = LL_SPI_NSS_SOFT;
  31.   SPI_InitStruct.BaudRate = LL_SPI_BAUDRATEPRESCALER_DIV4;
  32.   SPI_InitStruct.BitOrder = LL_SPI_MSB_FIRST;
  33.   SPI_InitStruct.CRCCalculation = LL_SPI_CRCCALCULATION_DISABLE;
  34.   SPI_InitStruct.CRCPoly = 10;
  35.   LL_SPI_Init(SPI5, &SPI_InitStruct);
  36.   LL_SPI_SetStandard(SPI5, LL_SPI_PROTOCOL_MOTOROLA);
  37.   /* USER CODE BEGIN SPI5_Init 2 */
  38.   /* USER CODE END SPI5_Init 2 */
  39. }

Po wywołaniu funkcji uruchamiającej SPI należy pamiętać o jego włączeniu:

  1. LL_SPI_Enable(SPI5); //((SPI5->CR1) |= (SPI_CR1_SPE));

Uruchomienie pinów sterujących ustawionych jako wyjście wygląda następująco:

  1. static void MX_GPIO_Init(void)
  2. {
  3.   LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
  4.   /* GPIO Ports Clock Enable */
  5.   LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOF);
  6.   LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOH);
  7.   LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOC);
  8.   LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);
  9.   LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOD);
  10.   LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOG);
  11.   /**/
  12.   LL_GPIO_ResetOutputPin(GPIOC, LL_GPIO_PIN_2);
  13.   /**/
  14.   LL_GPIO_ResetOutputPin(GPIOD, LL_GPIO_PIN_12|LL_GPIO_PIN_13);
  15.   /**/
  16.   LL_GPIO_ResetOutputPin(GPIOG, LL_GPIO_PIN_13|LL_GPIO_PIN_14);
  17.   /**/
  18.   GPIO_InitStruct.Pin = LL_GPIO_PIN_2;
  19.   GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
  20.   GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
  21.   GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
  22.   GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
  23.   LL_GPIO_Init(GPIOC, &GPIO_InitStruct);
  24.   /**/
  25.   GPIO_InitStruct.Pin = LL_GPIO_PIN_0;
  26.   GPIO_InitStruct.Mode = LL_GPIO_MODE_INPUT;
  27.   GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
  28.   LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
  29.   /**/
  30.   GPIO_InitStruct.Pin = LL_GPIO_PIN_12|LL_GPIO_PIN_13;
  31.   GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
  32.   GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
  33.   GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
  34.   GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
  35.   LL_GPIO_Init(GPIOD, &GPIO_InitStruct);
  36.   /**/
  37.   GPIO_InitStruct.Pin = LL_GPIO_PIN_13|LL_GPIO_PIN_14;
  38.   GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
  39.   GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_LOW;
  40.   GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
  41.   GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
  42.   LL_GPIO_Init(GPIOG, &GPIO_InitStruct);
  43. }

W celu przesłania danych do wyświetlacza:

  1. static uint8_t tftDisplay_ILI9341_SendData(uint8_t data)
  2. {
  3.     CS_ACTIVE();
  4.     DC_SET_SEND_DATA();
  5.     while(!LL_SPI_IsActiveFlag_TXE(SPI_TFT_INTERFACE)) {}
  6.     LL_SPI_TransmitData8(SPI_TFT_INTERFACE, data);
  7.     while(!LL_SPI_IsActiveFlag_RXNE(SPI_TFT_INTERFACE)) {}
  8.     data = LL_SPI_ReceiveData8(SPI_TFT_INTERFACE);
  9.     CS_DESELECT();

  10.     return data;
  11. }

Tą komendę można w bardzo łatwy sposób przerobić na rejestry. Będzie to wyglądało mniej więcej tak:

  1. static uint8_t tftDisplay_ILI9341_SendCommand(uint8_t cmd)
  2. {
  3.   CS_ACTIVE();
  4.   DC_SET_SEND_COMMAND();   //DC LOW
  5.   while(!LL_SPI_IsActiveFlag_TXE(SPI_TFT_INTERFACE))     {}
  6.   LL_SPI_TransmitData8(SPI_TFT_INTERFACE, cmd);
  7.   while(!LL_SPI_IsActiveFlag_RXNE(SPI_TFT_INTERFACE)) {}
  8.   cmd = LL_SPI_ReceiveData8(SPI_TFT_INTERFACE);
  9.   CS_DESELECT();
  10.   return cmd;
  11. }

Odnośnik do SPI5 przekazywany jest przez definicje SPI_TFT_INTERFACE. Pozwoli to na wygodny sposób pozwalający na zmianę wykorzystywanego interfejsu.

Analogicznie wygląda funkcja wysyłające dane. Różnica polega na zmianie pinu sterującego:

  1. static uint8_t tftDisplay_ILI9341_SendData(uint8_t data)
  2. {
  3.     //Registers
  4.     CS_ACTIVE();
  5.     DC_SET_SEND_DATA();   //DC LOW
  6.    
  7.     while(!((((SPI_TFT_INTERFACE->SR) & (SPI_SR_TXE)) == (SPI_SR_TXE)))) {}
  8.    
  9.     volatile uint8_t *spidr = ((volatile uint8_t *)&SPI_TFT_INTERFACE->DR);
  10.     *spidr = data;
  11.    
  12.     while(!((((SPI_TFT_INTERFACE->SR) & (SPI_SR_RXNE)) == (SPI_SR_RXNE)))) {}
  13.    
  14.     data = (uint8_t)(SPI_TFT_INTERFACE->DR);
  15.    
  16.     CS_DESELECT();
  17.    
  18.     return data;
  19. }

Poniżej znajduje się lista funkcji wykorzystywanych do sterowania wyświetlaczem które można pobrać z projektu umieszczonym na dysku Google. Nie będę ich tutaj już omawiał ponieważ były one już opisywane na blogu pod tym linkiem.

Wyświetlacz po SPI z DMA (Memory to Peripheral):


W tej części opiszę sposób sterowania wyświetlaczem przez SPI z użyciem DMA.

Ustawienia w programie CubeMx zostają takie same jak dla poprzedniego przykładu. Jedyną różnicą jest uruchomienie DMA dla linii transmisyjnej:


Wygenerowany kod pozwalający na uruchomienie DMA wygląda następująco:

  1. static void MX_SPI5_Init(void)
  2. {
  3.   /* USER CODE BEGIN SPI5_Init 0 */
  4.   /* USER CODE END SPI5_Init 0 */
  5.   LL_SPI_InitTypeDef SPI_InitStruct = {0}
  6.   LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
  7.   /* Peripheral clock enable */
  8.   LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_SPI5);
  9.   LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOF);
  10.   /**SPI5 GPIO Configuration  
  11.   PF7   ------> SPI5_SCK
  12.   PF8   ------> SPI5_MISO
  13.   PF9   ------> SPI5_MOSI
  14.   */
  15.   GPIO_InitStruct.Pin = LL_GPIO_PIN_7|LL_GPIO_PIN_8|LL_GPIO_PIN_9;
  16.   GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
  17.   GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
  18.   GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
  19.   GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
  20.   GPIO_InitStruct.Alternate = LL_GPIO_AF_5;
  21.   LL_GPIO_Init(GPIOF, &GPIO_InitStruct);
  22.   /* SPI5 DMA Init */
  23.   /* SPI5_TX Init */
  24.   LL_DMA_SetChannelSelection(DMA2, LL_DMA_STREAM_4, LL_DMA_CHANNEL_2);
  25.   LL_DMA_SetDataTransferDirection(DMA2, LL_DMA_STREAM_4, LL_DMA_DIRECTION_MEMORY_TO_PERIPH);
  26.   LL_DMA_SetStreamPriorityLevel(DMA2, LL_DMA_STREAM_4, LL_DMA_PRIORITY_LOW);
  27.   LL_DMA_SetMode(DMA2, LL_DMA_STREAM_4, LL_DMA_MODE_CIRCULAR);
  28.   LL_DMA_SetPeriphIncMode(DMA2, LL_DMA_STREAM_4, LL_DMA_PERIPH_NOINCREMENT);
  29.   LL_DMA_SetMemoryIncMode(DMA2, LL_DMA_STREAM_4, LL_DMA_MEMORY_INCREMENT);
  30.   LL_DMA_SetPeriphSize(DMA2, LL_DMA_STREAM_4, LL_DMA_PDATAALIGN_BYTE);
  31.   LL_DMA_SetMemorySize(DMA2, LL_DMA_STREAM_4, LL_DMA_MDATAALIGN_BYTE);
  32.   LL_DMA_DisableFifoMode(DMA2, LL_DMA_STREAM_4);
  33.   /* USER CODE BEGIN SPI5_Init 1 */
  34.   /* USER CODE END SPI5_Init 1 */
  35.   /* SPI5 parameter configuration*/
  36.   SPI_InitStruct.TransferDirection = LL_SPI_FULL_DUPLEX;
  37.   SPI_InitStruct.Mode = LL_SPI_MODE_MASTER;
  38.   SPI_InitStruct.DataWidth = LL_SPI_DATAWIDTH_8BIT;
  39.   SPI_InitStruct.ClockPolarity = LL_SPI_POLARITY_LOW;
  40.   SPI_InitStruct.ClockPhase = LL_SPI_PHASE_1EDGE;
  41.   SPI_InitStruct.NSS = LL_SPI_NSS_SOFT;
  42.   SPI_InitStruct.BaudRate = LL_SPI_BAUDRATEPRESCALER_DIV2;
  43.   SPI_InitStruct.BitOrder = LL_SPI_MSB_FIRST;
  44.   SPI_InitStruct.CRCCalculation = LL_SPI_CRCCALCULATION_DISABLE;
  45.   SPI_InitStruct.CRCPoly = 10;
  46.   LL_SPI_Init(SPI5, &SPI_InitStruct);
  47.   LL_SPI_SetStandard(SPI5, LL_SPI_PROTOCOL_MOTOROLA);
  48.   /* USER CODE BEGIN SPI5_Init 2 */
  49.   /* USER CODE END SPI5_Init 2 */
  50. }

  1. static void MX_DMA_Init(void)
  2. {
  3.   /* Init with LL driver */
  4.   /* DMA controller clock enable */
  5.   LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA2);
  6.   /* DMA interrupt init */
  7.   /* DMA2_Stream4_IRQn interrupt configuration */
  8.  NVIC_SetPriority(DMA2_Stream4_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),0, 0));
  9.   NVIC_EnableIRQ(DMA2_Stream4_IRQn);
  10. }

Następnie w funkcji main() trzeba dorzucić pozostałe instrukcję do uruchomienia:

  1. LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_4);
  2. LL_DMA_ClearFlag_TC4(DMA2);
  3. LL_DMA_ClearFlag_TE4(DMA2);
  4. LL_SPI_EnableDMAReq_TX(SPI5);
  5. LL_DMA_EnableIT_TC(DMA2, LL_DMA_STREAM_4);
  6. LL_DMA_EnableIT_TE(DMA2, LL_DMA_STREAM_4);
  7. LL_SPI_Enable(SPI5);

Linia TX SPI została podłączona pod DMA. Ponieważ komunikacja z wyświetlaczem polega na przesyłaniu do niego odpowiednich rozkazów.

Przerwanie od DMA:

  1. void DMA2_Stream4_IRQHandler(void)
  2. {
  3.   /* USER CODE BEGIN DMA2_Stream4_IRQn 0 */
  4.   /* USER CODE END DMA2_Stream4_IRQn 0 */
  5.   /* USER CODE BEGIN DMA2_Stream4_IRQn 1 */
  6.   if(LL_DMA_IsActiveFlag_TC4(DMA2) == 1)
  7.   {
  8.     DMA1_Stream4_TransferComplete();
  9.   }
  10.   else if(LL_DMA_IsActiveFlag_TE4(DMA2) == 1)
  11.   {
  12.     LL_DMA_ClearFlag_TE4(DMA2);
  13.   }
  14.   /* USER CODE END DMA2_Stream4_IRQn 1 */
  15. }

  1. void DMA1_Stream4_TransferComplete(void)
  2. {
  3.   LL_DMA_ClearFlag_TC4(DMA2);
  4.   DmaSpiCnt--;
  5.   if(DmaSpiCnt == 0)
  6.   {
  7.     LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_4);
  8.     DmaSpiCnt=1;
  9.   }
  10. }

W powyższej obsłudze przerwania następuje obsługa dwóch przypadków. Pierwsza gdy transfer danych zostanie zakończony. Wtedy następuje wejście do funkcji DMA1_Stream4_TransferComplete(). W niej następuje wyczyszczenie flagi DMA_HIFCR_CTCIF4 oraz zmniejszenie licznika z danymi. Gdy wszystkie dane zostaną przesłane strumień zostaje wyłączony. Drugim przypadkiem jest zgłoszenie przerwania po wywołaniu błędu. W tym przypadku zostaje wyczyszczona flaga błędu. Można także umieścić tutaj wyłączenie przerwań od DMA oraz wysłanie użytkownikowi informacji o błędzie np. przez zapalenie diody, czy przesłanie ramki UART. Według mnie w przypadku wyświetlaczy nie ma to aż tak dużego znaczenia, chyba, że błędy w transmisji będą pojawiały się bardzo często.

Aby zweryfikować czy transfer DMA został zakończony i czy można dołożyć nowe dane do bufora należy zweryfikować dwie flagi. Jedna czy strumień już został wyłączony (DMA_SxCR_EN) oraz wyzerowany bit TCIFx (DMA_HISR_TCIF4).

Schemat blokowy przesyłania danych wygląda następująco:


Poniżej opiszę funkcje przesyłające dane do układu. 

Wysłanie komendy:

  1. static void tftDisplay_ILI9341_SendCommand(uint8_t command)
  2. {
  3.     CS_ACTIVE();
  4.     DC_SET_SEND_COMMAND();
  5.     DmaSpiCnt = 1;
  6.     LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_4);
  7.     LL_DMA_SetDataLength(DMA2, LL_DMA_STREAM_4, 1);
  8.     LL_DMA_ConfigAddresses(DMA2, LL_DMA_STREAM_4, (uint32_t)&command,
  9.             LL_SPI_DMA_GetRegAddr(SPI5), LL_DMA_GetDataTransferDirection(DMA2, LL_DMA_STREAM_4));
  10.     LL_DMA_EnableStream(DMA2, LL_DMA_STREAM_4);

  11.     while(((DMA2_Stream4->CR & DMA_SxCR_EN) != 0) || ((DMA2->HISR & DMA_HISR_TCIF4) != 0)) { }
  12. }

Wysłanie danej do układu pojedynczo:

  1. static void tftDisplay_ILI9341_SendSingleData(uint32_t* BufferPtr)
  2. {
  3.     CS_ACTIVE();
  4.     DC_SET_SEND_DATA();
  5.     DmaSpiCnt = 1;
  6.     LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_4);
  7.     LL_DMA_SetDataLength(DMA2, LL_DMA_STREAM_4, 1);
  8.     LL_DMA_ConfigAddresses(DMA2, LL_DMA_STREAM_4,
  9.             (uint32_t)BufferPtr,
  10.             LL_SPI_DMA_GetRegAddr(SPI5),
  11.             LL_DMA_GetDataTransferDirection(DMA2, LL_DMA_STREAM_4));
  12.     LL_DMA_EnableStream(DMA2, LL_DMA_STREAM_4);

  13.     while(((DMA2_Stream4->CR & DMA_SxCR_EN) != 0) || ((DMA2->HISR & DMA_HISR_TCIF4) != 0)) { }
  14. }

Wysłanie dużej ilości danych do układu:

  1. static void tftDisplay_ILI9341_WriteMultipleData(uint32_t* BufferPtr, uint32_t BufferSize, uint8_t DmaCount)
  2. {
  3.     CS_ACTIVE();
  4.     DC_SET_SEND_DATA();
  5.     DmaSpiCnt = DmaCount;
  6.     LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_4);
  7.     LL_DMA_SetDataLength(DMA2, LL_DMA_STREAM_4, BufferSize);
  8.     LL_DMA_ConfigAddresses(DMA2, LL_DMA_STREAM_4,
  9.             (uint32_t)BufferPtr,
  10.             LL_SPI_DMA_GetRegAddr(SPI5),
  11.             LL_DMA_GetDataTransferDirection(DMA2, LL_DMA_STREAM_4));
  12.     LL_DMA_EnableStream(DMA2, LL_DMA_STREAM_4);

  13.     while(((DMA2_Stream4->CR & DMA_SxCR_EN) != 0) || ((DMA2->HISR & DMA_HISR_TCIF4) != 0)) { }
  14. }

Projekty opisane w tym poście można pobrać z dysku Google pod tym linkiem.

Dokumentacja:


[1] https://www.st.com/resource/en/application_note/dm00046011-using-the-stm32f2-stm32f4-and-stm32f7-series-dma-controller-stmicroelectronics.pdf
[2] https://www.mimuw.edu.pl/~marpe/mikrokontrolery/w8_dma.pdf