ZS-040 HC-05 Bluetooth Master/Slave Transceiver

STM32 Bluetooth DMA ASCII Payload Communication Part 4

Overview

This tutorial covers STM32 Bluetooth DMA ASCII payload communication using DMA-driven UART. Building on the previous Bluetooth DMA tutorials, you will extend the master and slave application to send and interpret ASCII-formatted messages over DMA-based UART transfers. The project uses two STM32 Nucleo boards, two ZS-040 modules configured as master and slave, and two FTDI interfaces for separate USART terminal sessions. ASCII payloads provide easier debugging and readability compared to binary formats, making this approach well suited for development and testing. This is Part 4 of a multi-part series.

What You Will Learn

  • How to exchange messages between master and slave HC-05 Bluetooth modules using STM32 UART + DMA.
  • How the ASCII payload-length prefix is encoded and transmitted before each message.
  • How the project uses board identity, IOC configuration, terminal output, and logic-analyzer captures to validate the communication flow.

Prerequisites

This tutorial assumes you already know how to configure the ZS-040 HC-05 module and are comfortable with basic STM32CubeIDE project setup, UART usage, and DMA concepts. It also assumes you have two separate hardware setups available so the master and slave boards can communicate with each other.

What Changes from Part 3

This part uses the same project as STM32 Bluetooth DMA Master Slave Communication Part 3. The wiring, the CubeMX configuration, the ring buffer and the rest of the source code are exactly the same. The only change is line 58 of main.c: #define NO_ASCII becomes #define ASCII. With that one change, every message starts with its payload length written as 4 ASCII characters instead of a 2-byte uint16_t value. This post explains how the ASCII length prefix is built and read, and shows the terminal output and logic-analyzer captures for ASCII mode.

Materials List

Board Diagram

STM32F439ZI

Project Structure

The original post focuses on hardware configuration, IOC setup, state-machine logic, terminal captures, and logic-analyzer validation. A full project tree block was not present in the original Gutenberg source for this post, so no source tree is inserted here.

Hardware Configuration / Pinouts

The hardware is identical to Part 3: two STM32 Nucleo boards, two ZS-040 HC-05 modules (one master, one slave) and two FTDI adapters. For the FTDI and ZS-040 pinouts and the schematic, see Hardware Configuration / Pinouts in STM32 Bluetooth DMA Master Slave Communication Part 3.

Project Setup

The project setup is also identical to Part 3, including keeping the master and slave breadboards separate and choosing the master or slave role from the MCU unique ID. Follow the Project Setup section of STM32 Bluetooth DMA Master Slave Communication Part 3, then change line 58 of main.c to #define ASCII. The CubeMX IOC configuration is in the PDF below.

Pinouts & Configurations

Code Walkthrough

The sections below show only the functions that handle the ASCII length prefix: the master and slave state machines and the two initial message parsers. The line numbers match the complete main.c.

main.c

This function processMaster() handles messages to and from the slave. It’s a state machine and as we progress through the message exchange, it changes the state we are in until we are done. Here we are processing the completed message.

We will also be using code wrapped in the #ifdef ASCII for this tutorial. The size of the payload will be included in the prefix of each message in ASCII format. The size will be converted from int16_t type value to ASCII, as shown on the highlighted lines below.

C
void processMaster() {
	HAL_StatusTypeDef halStatus = HAL_OK;
	char masterRxBuffer[MAX_BUFSIZE];
	char obuf[128];
	uint16_t len = 0;

	switch (masterPhase) {
		case M_PHASE_0: {
		  if (!messageSent) {
			  sprintf(obuf, "Hello from Master");
			  printf("Master TxData: %s\r\n", obuf);
			  len = strlen(obuf) + 1;

#ifdef BINARY
			  halStatus = HAL_UART_Transmit(&huart2, (uint8_t *)&len, sizeof(uint16_t), 0xffff);

#endif
#ifdef ASCII
			  char bufLen[80];
			  sprintf(bufLen, "%4.4d", len);

			  halStatus = HAL_UART_Transmit(&huart2, (uint8_t *)&bufLen, 4, 0xffff);
#endif

			  halStatus = HAL_UART_Transmit(&huart2, (uint8_t *)&obuf, len, 0xffff);

			  if (halStatus != HAL_OK) {
				  Error_Handler();
			  }

			  messageSent = true;
			  recvdResponse = false;
		  } else {
			  if (messageSent && recvdResponse) {
				  ringBufferCopyAndRemove(masterRingBuf, masterRxBuffer);

				  printf("masterRxBuffer01: %s\r\n", (char *) masterRxBuffer);

				  if (strcmp("OK", masterRxBuffer) == 0) {
					  recvdResponse = false;
					  waitingOnResponse = false;
					  messageSent = false;
					  waitingOnMaster = false;
					  masterPhase = M_SEND_SERIAL_NO_REQUEST;
				  }
			  }
		  }

		  break;
		}

	  case M_SEND_SERIAL_NO_REQUEST: {
		  if (!messageSent) {
			  sprintf(obuf, "Serial#?");
			  printf("Master TxData: %s\r\n", obuf);
			  len = strlen(obuf) + 1;

#ifdef BINARY
			  halStatus = HAL_UART_Transmit(&huart2, (uint8_t *)&len, sizeof(uint16_t), 0xffff);

#endif
#ifdef ASCII
			  char bufLen[80];
			  sprintf(bufLen, "%4.4d", len);

			  halStatus = HAL_UART_Transmit(&huart2, (uint8_t *)&bufLen, 4, 0xffff);
#endif
			  halStatus = HAL_UART_Transmit(&huart2, (uint8_t *)&obuf, len, 0xffff);

			  if (halStatus != HAL_OK) {
				  Error_Handler();
			  }

			  messageSent = true;
			  recvdResponse = false;
		  } else {
			  if (messageSent && recvdResponse) {
				  ringBufferCopyAndRemove(masterRingBuf, masterRxBuffer);
				  printf("masterRxBuffer02: %s\r\n", (char *) masterRxBuffer);

				  if (strncmp("Serial #:", masterRxBuffer, 9) == 0) {
					  sprintf(obuf, "Serial#: %s\r\n", masterRxBuffer + 9);
					  printf("Master RxData: %s", obuf);
					  recvdResponse = false;
					  waitingOnResponse = false;
					  messageSent = false;
					  waitingOnMaster = false;
					  masterPhase = M_PHASE_OK;
				  } else {
					  printf("Serial # string not found, masterRxBuffer[%s]\r\n", masterRxBuffer);

					  while(1) {
						  HAL_Delay(1000);
					  }
				  }
			  }
		  }

		  break;
	  }

	  case M_PHASE_OK: {
		  sprintf(obuf, "OK\r\n");
		  printf("Master RxData: %s", obuf);
		  recvdResponse = false;
		  waitingOnResponse = false;
		  messageSent = false;
		  waitingOnMaster = true;
		  masterPhase = M_DONE;
	  }

	  case M_DONE: {
		  break;
	  }

	  default: {
		  Error_Handler();
	  }
	}

}

This function processSlave() handles messages to and from the master. It’s a state machine and as we progress through the message exchange, it changes the state we are in until we are done. Here we are processing the completed message. Keep in mind that this example we have defined ASCII, or #define ASCII. Therefore any code, surrounded by #ifdef BINARY will not be included.

The size of the payload will be included in the prefix of each message in ASCII format. The size will be converted from int16_t type value to ASCII, as shown on line 311 & 313 and 347 & 349.

C
void processSlave() {
	HAL_StatusTypeDef halStatus = HAL_OK;
	char slaveRxBuffer[MAX_BUFSIZE];
	char obuf[128];
	int len = 0;

	switch (slavePhase) {
	  case S_PHASE_0: {
		  if (waitingOnMaster && recvdResponse) {
			  	  ringBufferCopyAndRemove(slaveRingBuf, slaveRxBuffer);
			  	  printf("slaveRxBuffer: %s\r\n", (char *) slaveRxBuffer);

			  	  if (strcmp("Hello from Master", slaveRxBuffer) == 0) {
			  		  recvdResponse = false;
			  		  waitingOnResponse = false;
			  		  messageSent = false;
			  		  waitingOnMaster = false;
			  		  slavePhase = S_PHASE_OK;
			  	  }

			  	  if (strcmp("Serial#?", slaveRxBuffer) == 0) {
			  		  recvdResponse = false;
			  		  waitingOnResponse = false;
			  		  messageSent = false;
			  		  waitingOnMaster = false;
			  		  slavePhase = S_PHASE_HANDLE_SERIAL_NO_REQUEST;
			  	  }
		  }

		  break;
	  }

	  case S_PHASE_HANDLE_SERIAL_NO_REQUEST: {
		  if (!waitingOnMaster) {
			  sprintf(obuf, "Serial #: %s", serialNo);
			  printf("Slave TxData: %s\r\n", obuf);
			  len = strlen(obuf) + 1;
#ifdef BINARY
			  halStatus = HAL_UART_Transmit(&huart3, (uint8_t *)&len, sizeof(uint16_t), 0xffff);

#endif
#ifdef ASCII
			  char bufLen[80];
			  sprintf(bufLen, "%4.4d", len);

			  halStatus = HAL_UART_Transmit(&huart3, (uint8_t *)&bufLen, 4, 0xffff);
#endif
			  if (halStatus != HAL_OK) {
				  Error_Handler();
			  }

			  halStatus = HAL_UART_Transmit(&huart3, (uint8_t *)&obuf, len, 0xffff);

			  if (halStatus != HAL_OK) {
				  Error_Handler();
			  }

			  recvdResponse = false;
			  waitingOnResponse = false;
			  messageSent = false;
			  waitingOnMaster = false;
			  slavePhase = S_PHASE_OK;
		  }

		  break;
	  }

	  case S_PHASE_OK: {
		  if (!waitingOnMaster) {
			  sprintf(obuf, "OK");
			  printf("Slave TxData: %s\r\n", obuf);
			  len = strlen(obuf) + 1;

#ifdef BINARY
			  halStatus = HAL_UART_Transmit(&huart3, (uint8_t *)&len, sizeof(uint16_t), 0xffff);

#endif
#ifdef ASCII
			  char bufLen[80];
			  sprintf(bufLen, "%4.4d", len);

			  halStatus = HAL_UART_Transmit(&huart3, (uint8_t *)&bufLen, 4, 0xffff);
#endif
			  if (halStatus != HAL_OK) {
				  Error_Handler();
			  }

			  halStatus = HAL_UART_Transmit(&huart3, (uint8_t *)&obuf, len, 0xffff);

			  if (halStatus != HAL_OK) {
				  Error_Handler();
			  }

			  recvdResponse = false;
			  waitingOnResponse = false;
			  messageSent = false;
			  waitingOnMaster = true;
			  slavePhase = S_PHASE_0;
		  }

		  break;
	  }

	  case S_DONE: {
		  break;
	  }

	  default: {
		  Error_Handler();
	  }
	}
}

Keep in mind that this example we have defined ASCII, or #define ASCII. Therefore any code, surrounded by #ifdef BINARY will not be included. These two functions are the initial message parsers. Both of these functions retrieve the payload size as a prefix. In other words in the case of #define ASCII, we are capturing the first 4 bytes of data which contain the size of the payload in ASCII bytes. This is how we know on the next go around that the payload will be X bytes long. Looking at line 391 and 428, we are taking the ASCII first 4 bytes transferred and converting them into uint16_t to use as the length of the payload.

C
void recvMasterDeviceData() {
	HAL_StatusTypeDef halStatus = HAL_OK;

	if (!isMasterSizeRxcvd)
	{
#ifdef ASCII
		masterBytesToRcv = ((masterRxData[0]-48)*1000)+((masterRxData[1]-48)*100)+((masterRxData[2]-48)*10)+((masterRxData[3]-48));
#endif

		HAL_UART_Receive_DMA(&huart2, masterRxData, masterBytesToRcv);

		if (halStatus != HAL_OK) {
		  Error_Handler();
		}

		isMasterSizeRxcvd = true;
	}
	else {
		recvdResponse = true;

		ringBufferAdd(masterRingBuf, masterRxData);

		memset(masterRxData, 0x00, sizeof(masterRxData));

		isMasterSizeRxcvd = false;
#ifdef BINARY
		HAL_UART_Receive_DMA(&huart2, (uint8_t *) &masterBytesToRcv, sizeof(uint16_t));
#endif
#ifdef ASCII
		HAL_UART_Receive_DMA(&huart2, masterRxData, 4);
#endif
	}
}

bool isSlaveSizeRxcvd = false;
uint16_t slaveBytesToRcv;

void recvSlaveDeviceData() {
	HAL_StatusTypeDef halStatus = HAL_OK;

	if (!isSlaveSizeRxcvd)
	{
#ifdef ASCII
		slaveBytesToRcv = ((slaveRxData[0]-48)*1000)+((slaveRxData[1]-48)*100)+((slaveRxData[2]-48)*10)+((slaveRxData[3]-48));
#endif

		HAL_UART_Receive_DMA(&huart3, slaveRxData, slaveBytesToRcv);

		if (halStatus != HAL_OK) {
		  Error_Handler();
		}

		isSlaveSizeRxcvd = true;
	}
	else {
		recvdResponse = true;

		ringBufferAdd(slaveRingBuf, slaveRxData);

		memset(slaveRxData, 0x00, sizeof(slaveRxData));

		isSlaveSizeRxcvd = false;
#ifdef BINARY
		HAL_UART_Receive_DMA(&huart3, (uint8_t *) &slaveBytesToRcv, sizeof(uint16_t));
#endif
#ifdef ASCII
		HAL_UART_Receive_DMA(&huart3, slaveRxData, 4);
#endif

	}
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	HAL_StatusTypeDef halStatus = HAL_OK;

   if (huart->Instance == USART2) {	// Blue Tooth Device
	   recvMasterDeviceData();

		if (halStatus != HAL_OK) {
		  Error_Handler();
		}
   }  else {
	   if (huart->Instance == USART3) {	// Blue Tooth Device
		   recvSlaveDeviceData();

			if (halStatus != HAL_OK) {
			  Error_Handler();
			}

	   }
   }
}

Everything else is unchanged from Part 3: main.h, the printf redirection to USART1, HAL_UART_RxCpltCallback(), ringBuffer.h, ringBuffer.c and the complete main.c listing (apart from line 58). See the Code Walkthrough section of STM32 Bluetooth DMA Master Slave Communication Part 3 for those files.

Output Sections

USART1 Dumb Terminal Output for Master Bluetooth

Dumb Terminal Output for Master Bluetooth

USART1 Dumb Terminal Output for Slave Bluetooth

Dumb Terminal Output for Slave Bluetooth

Logic Analyzer Output

The channels and the descriptions are in the columns on the left side of the screen.

If you have questions or run into trouble getting the boards programmed and talking to each other, post in the Tutorial Support forum and I will work through it with you. If project source is not linked in the tutorial, it may be available on request — use the email contact option in the site footer.