> One is always torn...
The best thing is to write a minimal but complete compilable code exhibiting the problem, and post it as a zip in its entirety. Then post only relevant portions in the text.
For example:
> void serial_transmit_irq_handler(UART_HandleTypeDef *huart, uint8_t port)
Whatizit? How is it called, in what context, what is preceding and succeeding calling this function, etc.etc.
---
> UART_IT_TXE
As I've said, stop using it.
Use symbols from the CMSIS-mandated device header when accessing/talking about registers.
Use any random Cube/HAL gobbledygook when using the Cube/HAL functions, exclusively.
---
> if (serial_transmit_buffer_tx_index[port] == serial_transmit_buffer_put_index[port])
does not guarantee that there is nothing in the shift register, so clearing USART_CR1.TXEIE does not guarantee that either.
As NorthGuy says too, guard the attempt to transmit by USART_CR1.TCIE (and again, don't use UART_IT_TC etc.)
Or, better, don't transmit from main at all, as soon as you set USART_CR1.TXEIE and the transmitter buffer is empty, the interrupt will fire and perform the transmission.
JW