UART and virtual COM port
Most Discovery and Nucleo boards route one USART to the on-board ST-LINK, which exposes it to the host as a virtual COM port (VCP). No extra adapter is needed — the same USB cable used for debugging carries the serial console.
MSP initialisation
Board-specific pin mapping in stm32xxxx_hal_msp.c:
#define VCP_TX_Pin GPIO_PIN_9
#define VCP_TX_GPIO_Port GPIOA
#define VCP_RX_Pin GPIO_PIN_7
#define VCP_RX_GPIO_Port GPIOB
/**
* @brief UART MSP Initialization
* This function configures the hardware resources used in this example
* @param huart: UART handle pointer
* @retval None
*/
void HAL_UART_MspInit(UART_HandleTypeDef *huart) {
GPIO_InitTypeDef GPIO_InitStruct = { 0 };
RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = { 0 };
if (huart->Instance == USART1) {
/* USER CODE BEGIN USART1_MspInit 0 */
/* USER CODE END USART1_MspInit 0 */
/** Initializes the peripherals clock
*/
PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_USART1;
PeriphClkInitStruct.Usart16ClockSelection =
RCC_USART16CLKSOURCE_D2PCLK2;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK) {
Error_Handler();
}
/* Peripheral clock enable */
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/**USART1 GPIO Configuration
PB7 ------> USART1_RX
PA9 ------> USART1_TX
*/
GPIO_InitStruct.Pin = VCP_RX_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(VCP_RX_GPIO_Port, &GPIO_InitStruct);
GPIO_InitStruct.Pin = VCP_TX_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(VCP_TX_GPIO_Port, &GPIO_InitStruct);
/* USER CODE BEGIN USART1_MspInit 1 */
/* USER CODE END USART1_MspInit 1 */
}
}
/**
* @brief UART MSP De-Initialization
* This function freeze the hardware resources used in this example
* @param huart: UART handle pointer
* @retval None
*/
void HAL_UART_MspDeInit(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
/* Peripheral clock disable */
__HAL_RCC_USART1_CLK_DISABLE();
/**USART1 GPIO Configuration
PB7 ------> USART1_RX
PA9 ------> USART1_TX
*/
HAL_GPIO_DeInit(VCP_RX_GPIO_Port, VCP_RX_Pin);
HAL_GPIO_DeInit(VCP_TX_GPIO_Port, VCP_TX_Pin);
}
}
Points that bite
- The pins are board-specific. This mapping — PA9 TX, PB7 RX — is what
several Discovery boards route to the ST-LINK VCP. Others use PA9/PA10, in
which case the
GPIOBclock enable is unnecessary. Copying a generatedmsp.cbetween boards without checking the schematic produces a port that opens successfully and transmits nothing. - Keep the defines and the comments in agreement. These blocks are
regenerated by CubeMX, and a stale comment describing different pins than the
#defines above it is a reliable source of lost time. - Both GPIO port clocks must be enabled when TX and RX are on different ports. Missing one leaves that pin as a plain input and the direction fails silently.
Usart16ClockSelectionselects the kernel clock feeding USART1 and USART6. The baud-rate divisor is computed from it, not from the APB clock, so a mismatch produces a working port at the wrong bit rate — garbage on the terminal at every setting. See Peripheral kernel clocks.GPIO_SPEED_FREQ_LOWis adequate up to a few hundred kbaud. Raise it for faster links, or the edges are too slow and framing errors appear.- TX and RX are named from the MCU's point of view. The board wires the ST-LINK's TX to the MCU's RX. When wiring an external adapter instead, the lines cross; connecting TX to TX gives a silent link in both directions.
Retargeting printf
printf reaches the UART by overriding a newlib hook. There are two override
points, and either works — pick one, not both.
Bring up the USART first. In STM32CubeIDE this is New Project ▸ Board Selector
with Initialize all peripherals with their default mode: Yes, then enable
USART1 in Asynchronous mode. Then verify with a serial terminal on the host.
Override _write
The lower of the two hooks. newlib routes every stream — printf, fputs,
fwrite — through _write, so this covers stderr as well as stdout. Put it
in main.c or a dedicated syscalls.c:
int _write(int file, char *ptr, int len) {
(void)file;
if (HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, HAL_MAX_DELAY) != HAL_OK) {
return -1;
}
return len;
}
Override __io_putchar
The hook CubeIDE's generated syscalls.c already calls from its own _write,
one character at a time. This is the form ST's examples use:
#include <stdio.h>
/* USER CODE BEGIN 4 */
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
/**
* @brief Retargets the C library printf function to the USART.
* @retval int
*/
PUTCHAR_PROTOTYPE
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
/* USER CODE END 4 */
Keep the definition between the USER CODE markers so CubeMX does not overwrite
it on the next regeneration. Do not also define _write — the generated one
already forwards to __io_putchar, and a second _write collides at link time.
Two things to expect:
- Output is buffered until a newline, or not flushed at all, unless buffering is
disabled:
setvbuf(stdout, NULL, _IONBF, 0);early inmain(). HAL_UART_Transmitblocks. Aprintfinside an interrupt handler, or inside a fast control loop, will affect timing. Use a ring buffer drained by DMA when the output rate matters.
Nothing appears on the terminal
Work through it in this order — the causes are roughly in frequency order:
- Wrong COM port selected, or another program still holds it open.
- Baud rate mismatch. If the text is garbage rather than absent, this or the kernel clock is the cause.
- The peripheral or GPIO clock was never enabled.
- The pins were configured with the wrong
Alternatefunction number.AF7is USART1–3 on most families,AF8for UART4–8; the table is in the datasheet, not the reference manual. - The board routes the VCP to a different USART than the code initialises.
- ST-LINK firmware too old to expose the VCP. Update it from STM32CubeIDE or STM32CubeProgrammer.