Ten post chciałbym poświęcić na opisanie biblioteki odpowiedzialnej za komunikację z układem BLE RN4870/RN4871.
Układ RN4870 oraz RN4871 są to moduły Bluetooth Low Energy w standardzie 4.2. Komunikacja z układami odbywa się za pomocą komend ASCII przez interfejs UART.
Posiadają one wbudowany stos bluetooth wraz z silnikiem skryptowym. Z tego powodu mogą one pracować jako niezależne urządzenia.
W porównaniu z poprzednią generacją posiadają 2,5 raza szybszą przepustowość danych.
Dzięki temu, że komunikacja jest przez interfejs UART bez problemów można podłączyć moduł do konwertera, a następnie bezpośrednio do komputera. Pozwala to na prostszy sposób ustawiania modułu.
Dokumentację można pobrać pod tym linkiem.
Wykaz komend wraz z dokładnym objaśnieniem można znaleźć pod tym linkiem.
Przykładowe podłączenie można znaleźć w dokumentacji układu.
Podłączenie dla RN4870:
Podłączenie RN4871:
Definicja pinów i prędkości dla UART:
Na samym początku funkcja uruchamiająca UART:
Uruchamiany jest UART6 z przerwaniami od nadawania.
Przesyłanie danych wygląda następująco:
Poniżej przedstawię wykaz kilku komend jakie zostały zawarte w programie. Są one pisane podobnie, więc nie zawieram tutaj wszystkich funkcji. Całość można pobrać z dysku Google. Link na dole strony.
Teraz główna funkcja uruchamiająca układ w trybie komend:
Uruchomienie trybu danych, czyli normalnej pracy:
Reset do ustawień fabrycznych:
Funkcja odczytu danych:
Zwraca następujące informacje:
Reset układu. Ta komenda wykonywana jest w celu uruchomienia skonfigurowanych usług:
Ustawienie nazwy układu:
Jako argumenty podawana jest nazwa oraz jej długość. Nazwa nie może przekraczać 20 znaków.
Ustawienie klucza publicznego:
Wyczyszczenie wszystkich usług. Po jej wysłaniu wymagane jest podanie sygnału resetu.
Ustaw klucz prywatny:
Ustaw adres układu bluetooth:
W przedstawionych powyżej funkcjach czekam na odpowiedź w pętli od komendy z określonym timeoutem. Funkcja wyszukująca ramkę prezentuje się następująco:
Ramkę można dekodować po odczytaniu znaku końca czyli 0x0D.
Pliki do projektu można pobrać z dysku Google pod tym linkiem.
[Źródło: http://www.st.com/en/evaluation-tools/32f746gdiscovery.html]
Opis układu:
Układ RN4870 oraz RN4871 są to moduły Bluetooth Low Energy w standardzie 4.2. Komunikacja z układami odbywa się za pomocą komend ASCII przez interfejs UART.
Posiadają one wbudowany stos bluetooth wraz z silnikiem skryptowym. Z tego powodu mogą one pracować jako niezależne urządzenia.
W porównaniu z poprzednią generacją posiadają 2,5 raza szybszą przepustowość danych.
[Źródło: https://www.allaboutcircuits.com/news/microchip-produces-first-bluetooth-4.2-compliant-devices-rn4870-and-rn4871/]
Dzięki temu, że komunikacja jest przez interfejs UART bez problemów można podłączyć moduł do konwertera, a następnie bezpośrednio do komputera. Pozwala to na prostszy sposób ustawiania modułu.
Dokumentację można pobrać pod tym linkiem.
Wykaz komend wraz z dokładnym objaśnieniem można znaleźć pod tym linkiem.
Podłączenie układu:
Przykładowe podłączenie można znaleźć w dokumentacji układu.
Podłączenie dla RN4870:
Podłączenie RN4871:
Przykładowa aplikacja bez mikrokontrolera:
Jak wspomniałem wcześniej układu można używać bez mikrokontrolera. Jeden z prostych przykładów wygląda następująco:
$$$ - Uruchomienie trybu komend
+ - włączenie opcji ECHO
SS,C0 - Uruchomienie usługi UART Transparent
R,1 - ponowne uruchomienie układu do załadowania danych
Po wprowadzeniu komend można użyć aplikacji Microchip Bluetooth Data do przesyłania danych z telefonu do modułu bluetooth. Po ich przesłaniu dane odczytane zostaną przesłane przez UART.
Biblioteka:
Poniżej niektóre funkcje odpowiedzialne za obsługę układu:
Definicja pinów i prędkości dla UART:
- #define MICROCH_RN48_UART_COM USART6
- #define MICROCH_RN48_UART_BAUD 115200
- #define MICROCH_RN48_UART_TX_PIN USART6_TX1
- #define MICROCH_RN48_UART_TX_PORT USART6_GPIOC
- #define MICROCH_RN48_UART_RX_PIN USART6_RX1
- #define MICROCH_RN48_UART_RX_PORT USART6_GPIOC
Na samym początku funkcja uruchamiająca UART:
- static void usart_rn48_Init(void)
- {
- GPIO_InitTypeDef gpio_init_structure;
- PORT_CLOCK_ENABLE(MICROCH_RN48_UART_TX_PORT);
- PORT_CLOCK_ENABLE(MICROCH_RN48_UART_RX_PORT);
- usart_Initialize_Clock(MICROCH_RN48_UART_COM);
- gpio_init_structure.Pin = MICROCH_RN48_UART_TX_PIN;
- gpio_init_structure.Mode = GPIO_MODE_AF_PP;
- gpio_init_structure.Speed = GPIO_SPEED_FAST;
- gpio_init_structure.Pull = GPIO_PULLUP;
- gpio_init_structure.Alternate = GPIO_AF8_USART6;
- HAL_GPIO_Init(MICROCH_RN48_UART_TX_PORT, &gpio_init_structure);
- /* GPIO RX */
- gpio_init_structure.Pin = MICROCH_RN48_UART_RX_PIN;
- gpio_init_structure.Mode = GPIO_MODE_AF_PP;
- gpio_init_structure.Alternate = GPIO_AF8_USART6;
- HAL_GPIO_Init(MICROCH_RN48_UART_RX_PORT, &gpio_init_structure);
- UART_Handle6.Instance = USART6;
- UART_Handle6.Init.BaudRate = MICROCH_RN48_UART_BAUD;
- UART_Handle6.Init.WordLength = UART_WORDLENGTH_8B;
- UART_Handle6.Init.StopBits = UART_STOPBITS_1;
- UART_Handle6.Init.Parity = UART_PARITY_NONE;
- UART_Handle6.Init.HwFlowCtl = UART_HWCONTROL_NONE;
- UART_Handle6.Init.Mode = UART_MODE_TX_RX;
- UART_Handle6.Init.OverSampling = UART_OVERSAMPLING_8;
- UART_Handle6.Init.OneBitSampling = UART_ONEBIT_SAMPLING_DISABLED;
- UART_Handle6.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
- HAL_UART_Init(&UART_Handle6);
- HAL_NVIC_DisableIRQ(USART6_IRQn);
- HAL_NVIC_SetPriority(USART6_IRQn, 0, 1);
- HAL_NVIC_EnableIRQ(USART6_IRQn);
- HAL_NVIC_ClearPendingIRQ(USART6_IRQn);
- __HAL_UART_ENABLE_IT(&UART_Handle6, UART_IT_RXNE);
- USART6->CR1 |= USART_CR1_RXNEIE;
- }
Uruchamiany jest UART6 z przerwaniami od nadawania.
Przesyłanie danych wygląda następująco:
- static void rn48_SendStringUart(char* sendPointer, MIRCOCH_RN48_END_DATA_TypeDef lastByte)
- {
- while (*sendPointer != 0)
- {
- rn48_SendByteUart(*sendPointer);
- sendPointer++;
- }
- if(lastByte==RN48_LFCR) {
- rn48_SendByteUart(0x0A);
- rn48_SendByteUart(0x0D);
- }
- else if(lastByte==RN48_CRLF) {
- rn48_SendByteUart(0x0D);
- rn48_SendByteUart(0x0A);
- }
- else if(lastByte==RN48_LF) {
- rn48_SendByteUart(0x0A);
- }
- else if(lastByte==RN48_CR) {
- rn48_SendByteUart(0x0D);
- }
- }
- static void rn48_SendByteUart(char valueToSend)
- {
- usart_PutChar(MICROCH_RN48_UART_COM, valueToSend);
- }
Poniżej przedstawię wykaz kilku komend jakie zostały zawarte w programie. Są one pisane podobnie, więc nie zawieram tutaj wszystkich funkcji. Całość można pobrać z dysku Google. Link na dole strony.
Teraz główna funkcja uruchamiająca układ w trybie komend:
- static MICROCH_RN48_ERRORStatus_TypeDef rn48_CmdMode(void)
- {
- uint32_t maxTimeToWait = 0;
- rn48_clearBuffer();
- for(maxTimeToWait=0;maxTimeToWait<MICROCHIP_RN48_DELAY_TIME;maxTimeToWait++) {}
- RN48_SendString("$$$", RN48_CRLF);
- maxTimeToWait=0;
- do
- {
- maxTimeToWait++;
- if(rn48_searchForFrame(MICROCH_RN48_RX_t.rx_sendDataBuffer, "CMD>", 4) != MICROCH_OP_ERROR)
- {
- return MICROCH_OP_OK;
- }
- }while((maxTimeToWait<MICROCHIP_RN48_RX_TIMEOUT));
- return MICROCH_OP_ERROR;
- }
Uruchomienie trybu danych, czyli normalnej pracy:
- static MICROCH_RN48_ERRORStatus_TypeDef rn48_DataMode(void)
- {
- uint32_t maxTimeToWait = 0;
- rn48_clearBuffer();
- for(maxTimeToWait=0;maxTimeToWait<MICROCHIP_RN48_DELAY_TIME;maxTimeToWait++) {}
- RN48_SendString("---", RN48_CRLF);
- maxTimeToWait=0;
- do
- {
- maxTimeToWait++;
- if(rn48_searchForFrame(MICROCH_RN48_RX_t.rx_sendDataBuffer, "END", 3) != MICROCH_OP_ERROR)
- {
- return MICROCH_OP_OK;
- }
- }while((maxTimeToWait<MICROCHIP_RN48_RX_TIMEOUT));
- return MICROCH_OP_ERROR;
- }
Reset do ustawień fabrycznych:
- static MICROCH_RN48_ERRORStatus_TypeDef rn48_FactoryReset(void)
- {
- uint32_t maxTimeToWait = 0;
rn48_clearBuffer();
- for(maxTimeToWait=0;maxTimeToWait<MICROCHIP_RN48_DELAY_TIME;maxTimeToWait++) {}
- RN48_SendString("SF,1",RN48_CRLF);
- maxTimeToWait=0;
- do
- {
- maxTimeToWait++;
- if(rn48_searchForFrame(MICROCH_RN48_RX_t.rx_sendDataBuffer, "ERR", 3) != MICROCH_OP_ERROR)
- {
- return MICROCH_OP_ERROR;
- }
- else if(rn48_searchForFrame(MICROCH_RN48_RX_t.rx_sendDataBuffer,"%REBOOT%",8)==0)
- {
- return MICROCH_OP_OK;
- }
- }while((maxTimeToWait<MICROCHIP_RN48_RX_TIMEOUT));
- return MICROCH_OP_ERROR;
- }
Funkcja odczytu danych:
- static MICROCH_RN48_ERRORStatus_TypeDef rn48_DumpInformation(void)
- {
- uint32_t maxTimeToWait = 0;
rn48_clearBuffer();
- for(maxTimeToWait=0;maxTimeToWait<MICROCHIP_RN48_DELAY_TIME;maxTimeToWait++) {}
- RN48_SendString("D",RN48_CRLF);
- maxTimeToWait=0;
- do
- {
- maxTimeToWait++;
- if(rn48_searchForFrame(MICROCH_RN48_RX_t.rx_sendDataBuffer, "SERVICES=", 9) != MICROCH_OP_ERROR)
- {
- return MICROCH_OP_OK;
- }
- }while((maxTimeToWait<MICROCHIP_RN48_RX_TIMEOUT));
- return MICROCH_OP_ERROR;
- }
Zwraca następujące informacje:
Reset układu. Ta komenda wykonywana jest w celu uruchomienia skonfigurowanych usług:
- static MICROCH_RN48_ERRORStatus_TypeDef rn48_Reboot(void)
- {
- uint32_t maxTimeToWait = 0;
- rn48_clearBuffer();
- for(maxTimeToWait=0;maxTimeToWait<MICROCHIP_RN48_DELAY_TIME;maxTimeToWait++) {}
- RN48_SendString("R,1", RN48_CRLF);
- maxTimeToWait=0;
- do
- {
- maxTimeToWait++;
- if(rn48_searchForFrame(MICROCH_RN48_RX_t.rx_sendDataBuffer, "%REBOOT%",8) != MICROCH_OP_ERROR)
- {
- return MICROCH_OP_OK;
- }
- }while((maxTimeToWait<MICROCHIP_RN48_RX_TIMEOUT));
- return MICROCH_OP_ERROR;
- }
Ustawienie nazwy układu:
- static MICROCH_RN48_ERRORStatus_TypeDef rn48_SetDeviceName(char *name, uint8_t nameSize)
- {
- uint32_t maxTimeToWait = 0;
- if(nameSize > 20)
- {
- return MICROCH_OP_ERROR;
- }
- rn48_clearBuffer();
- for(maxTimeToWait=0;maxTimeToWait<MICROCHIP_RN48_DELAY_TIME;maxTimeToWait++) {}
- memset(MICROCH_RN48_t.sendDataBuffer, 0x00, sizeof(MICROCH_RN48_t.sendDataBuffer));
- strcpy(MICROCH_RN48_t.sendDataBuffer,"SN,");
- strcat(MICROCH_RN48_t.sendDataBuffer, name);
- RN48_SendString(MICROCH_RN48_t.sendDataBuffer, RN48_CRLF);
- do
- {
- maxTimeToWait++;
- if(rn48_searchForFrame(MICROCH_RN48_RX_t.rx_sendDataBuffer, "AOK",3) != MICROCH_OP_ERROR)
- {
- return MICROCH_OP_OK;
- }
- }while((maxTimeToWait<MICROCHIP_RN48_RX_TIMEOUT));
- return MICROCH_OP_ERROR;
- }
Jako argumenty podawana jest nazwa oraz jej długość. Nazwa nie może przekraczać 20 znaków.
Ustawienie klucza publicznego:
- static MICROCH_RN48_ERRORStatus_TypeDef rn48_SetUUID(char *uuidKey, uint8_t uuidKeySize)
- {
- uint32_t maxTimeToWait = 0;
- if(uuidKeySize != 4 && uuidKeySize != 32)
- {
- return MICROCH_OP_ERROR;
- }
- rn48_clearBuffer();
- for(maxTimeToWait=0;maxTimeToWait<MICROCHIP_RN48_DELAY_TIME;maxTimeToWait++) {}
- memset(MICROCH_RN48_t.sendDataBuffer, 0x00, sizeof(MICROCH_RN48_t.sendDataBuffer));
- strcpy(MICROCH_RN48_t.sendDataBuffer,"PS,");
- strcat(MICROCH_RN48_t.sendDataBuffer, uuidKey);
- rn48_SendStringUart(MICROCH_RN48_t.sendDataBuffer, RN48_CRLF);
- maxTimeToWait=0;
- do
- {
- maxTimeToWait++;
- if(rn48_searchForFrame(MICROCH_RN48_RX_t.rx_sendDataBuffer, "AOK",3) != MICROCH_OP_ERROR)
- {
- return MICROCH_OP_OK;
- }
- else if(rn48_searchForFrame(MICROCH_RN48_RX_t.rx_sendDataBuffer, "ERR",3) != MICROCH_OP_ERROR)
- {
- return MICROCH_OP_ERROR;
- }
- }while((maxTimeToWait<MICROCHIP_RN48_RX_TIMEOUT));
- return MICROCH_OP_ERROR;
- }
Wyczyszczenie wszystkich usług. Po jej wysłaniu wymagane jest podanie sygnału resetu.
- static MICROCH_RN48_ERRORStatus_TypeDef rn48_ClearsSettings(void)
- {
- uint32_t maxTimeToWait = 0;
- rn48_clearBuffer();
- for(maxTimeToWait=0;maxTimeToWait<MICROCHIP_RN48_DELAY_TIME;maxTimeToWait++) {}
- RN48_SendString("PZ",RN48_CRLF);
- maxTimeToWait=0;
- do
- {
- maxTimeToWait++;
- if(rn48_searchForFrame(MICROCH_RN48_RX_t.rx_sendDataBuffer, "AOK",3) != MICROCH_OP_ERROR)
- {
- return MICROCH_OP_OK;
- }
- else if(rn48_searchForFrame(MICROCH_RN48_RX_t.rx_sendDataBuffer, "ERR",3) != MICROCH_OP_ERROR)
- {
- return MICROCH_OP_ERROR;
- }
- }while((maxTimeToWait<MICROCHIP_RN48_RX_TIMEOUT));
- return MICROCH_OP_ERROR;
- }
Ustaw klucz prywatny:
- static MICROCH_RN48_ERRORStatus_TypeDef rn48_SetPrivateCharacteristic(char *uuidKey, char uuidKeySize,
- uint8_t propBitm, uint8_t maxDataSize)
- {
- uint32_t maxTimeToWait = 0;
- if(uuidKeySize != 4 && uuidKeySize != 32)
- {
- return MICROCH_OP_ERROR;
- }
- rn48_clearBuffer();
- for(maxTimeToWait=0;maxTimeToWait<MICROCHIP_RN48_DELAY_TIME;maxTimeToWait++) {}
- memset(MICROCH_RN48_t.sendDataBuffer, 0x00, sizeof(MICROCH_RN48_t.sendDataBuffer));
- strcpy(MICROCH_RN48_t.sendDataBuffer,"PC,");
- strcat(MICROCH_RN48_t.sendDataBuffer, uuidKey);
- strcat(MICROCH_RN48_t.sendDataBuffer, ",");
- strcat(MICROCH_RN48_t.sendDataBuffer, propBitm);
- strcat(MICROCH_RN48_t.sendDataBuffer, ",");
- strcat(MICROCH_RN48_t.sendDataBuffer, maxDataSize);
- RN48_SendString(MICROCH_RN48_t.sendDataBuffer, RN48_CRLF);
- maxTimeToWait=0;
- do
- {
- maxTimeToWait++;
- if(rn48_searchForFrame(MICROCH_RN48_RX_t.rx_sendDataBuffer, "AOK",3) != MICROCH_OP_ERROR)
- {
- return MICROCH_OP_OK;
- }
- else if(rn48_searchForFrame(MICROCH_RN48_RX_t.rx_sendDataBuffer, "ERR",3) != MICROCH_OP_ERROR)
- {
- return MICROCH_OP_ERROR;
- }
- }while((maxTimeToWait<MICROCHIP_RN48_RX_TIMEOUT));
- return MICROCH_OP_ERROR;
- }
Ustaw adres układu bluetooth:
- static MICROCH_RN48_ERRORStatus_TypeDef rn48_SetBltAdress(char *dataPointer, uint8_t dataSize)
- {
- uint32_t maxTimeToWait = 0;
- if(dataSize != 12)
- {
- return MICROCH_OP_ERROR;
- }
- rn48_clearBuffer();
- for(maxTimeToWait=0;maxTimeToWait<MICROCHIP_RN48_DELAY_TIME;maxTimeToWait++) {}
- memset(MICROCH_RN48_t.sendDataBuffer, 0x00, sizeof(MICROCH_RN48_t.sendDataBuffer));
- strcpy(MICROCH_RN48_t.sendDataBuffer,"SR,");
- strcat(MICROCH_RN48_t.sendDataBuffer, dataPointer);
- RN48_SendString(MICROCH_RN48_t.sendDataBuffer, RN48_CRLF);
- do
- {
- maxTimeToWait++;
- if(rn48_searchForFrame(MICROCH_RN48_RX_t.rx_sendDataBuffer, "AOK",3) != MICROCH_OP_ERROR)
- {
- return MICROCH_OP_OK;
- }
- }while((maxTimeToWait<MICROCHIP_RN48_RX_TIMEOUT));
- return MICROCH_OP_ERROR;
- }
W przedstawionych powyżej funkcjach czekam na odpowiedź w pętli od komendy z określonym timeoutem. Funkcja wyszukująca ramkę prezentuje się następująco:
- static MICROCH_RN48_ERRORStatus_TypeDef rn48_searchForFrame(char* buffer, char* frame, uint16_t lengthFrame)
- {
- char* p = buffer;
- for (int i=0; i<(MICROCHIP_RN48_RX_BUFFER_SIZE - lengthFrame); i++)
- {
- if(memcmp(p, frame, lengthFrame * sizeof(uint8_t)) == 0 )
- {
- return i;
- }
- p++;
- }
- return MICROCH_OP_ERROR;
- }
Ramkę można dekodować po odczytaniu znaku końca czyli 0x0D.
Pliki do projektu można pobrać z dysku Google pod tym linkiem.