Electronics > Microcontrollers

32F4 - a weird UART issue

(1/2) > >>

peter-h:
With the project nearly finished I have finally got around to writing some comprehensive factory test code.

And I found that serial port loopback does not work at baud rates over about 38k.

The serial ports all work and have been heavily used with various devices (at/below 38k), and outputting data has been tested up to megabit speeds, but it turns out that at say 115k the 1st byte sent, on a loopback test, is never received. Subsequent ones are. If I send just one byte, the RX ISR is never  hit. This behaviour could easily have never been noticed in practical usage...

I've been "doing UARTs" since about 1980... In general, you

- initialise it (the 32F4 UART transmits "an empty frame" there, apparently without a start bit, so I have a baud rate dependent wait to clear this silly feature)
- flush RX buffer, as many times as the FIFO is deep (2x for the 32F4 one)
- set up the RX interrupt

There tend to be weird things like framing errors which need to be cleared on each RX ISR entry (unless you especially want to log them, etc) but these don't normally prevent the RX interrupt.

The simple test I am running is

- output 1 byte (with loopback, TX to RX)
- wait for it to come back (it never does)

There is a 1k circular buffer on RX but as I said above this is never touched because there is no interrupt. If I send two bytes, I get the interrupt and the data read from DR is the 1st byte. If I send 256 bytes then I get back 255.

On a scope, the data looks fine.

This is the RX ISR


--- Code: ---
void Kde_serial_receive_irq_handler(UART_HandleTypeDef *huart)
{
uint8_t temp = huart->Instance->DR; // fetch the byte received
uint8_t port = KDE_SERIAL_MAX_PORT; // max port number
int32_t count = 0;

__HAL_UART_CLEAR_FLAG(huart, UART_FLAG_RXNE);

// Find which port this is in order to know which buffer to use
while (port > 0 && huart->Instance != kde_serial_port_handles[port-1]->Instance)
{
port--;  // port is 1-4
}

// Put the char into the queue only if port # valid and the queue is not full
if (port > 0)
{
port--; // offset from 0

// If 7 bit, mask off any parity in the MS bit
// If 8 bit with parity, the parity would be in bit 8 which we've already lost by reading into
// and 8 bit value
if (kde_serial_port_configs[port].bits_per_word == 7) {
temp &= 0x7f;
}

kde_serial_receive_buffer[port][kde_serial_receive_buffer_rx_index[port]++] = temp;

kde_serial_receive_buffer_rx_index[port] %= KDE_SERIAL_RX_BUFFER_SIZE;

if (kde_serial_receive_buffer_rx_index[port] == kde_serial_receive_buffer_get_index[port])
{
// Buffer is full - discard a byte from it. This is necessary to resolve the ambiguity of
// the two pointers being equal if FULL or EMPTY. We make sure they are never equal after an input.
// Increment the get index to ensure that only the last BUFFER-1 bytes can be accessed
kde_serial_receive_buffer_get_index[port]++;
kde_serial_receive_buffer_get_index[port] %= KDE_SERIAL_RX_BUFFER_SIZE;
}
}
}
--- End code ---

I am doing the RX clearing diligently after UART init, but I wonder if anyone has any ideas.

One thing I wondered was whether the GPIO (RX) is sampled fast enough; there is a range of sampling rates. I see code setting it up for "very high", but also enabling a UART on certain GPIOs may set up those pins; the extent to which this happens varies with the peripheral.

EDIT: reading SR and then DR manually returns 10010000 (TX empty, ok, and Idle Line detected, not ok) and in DR I see the correct byte value.

Basically, if I transmit by writing to the DR directly, it works, but if I transmit using a more complex TX function (which enables the TX empty and RX complete interrupts) it fails on the first byte. If I comment out the TXE and TC interrupt enable code, it works, but obviously I then get a very slow transmit, 1 byte at a time only. So is there something about these TX interrupts which might block the RX interrupt?

newbrain:
So, it seems we don't have the full picture.

The IRQs are largely independent, but it all comes to what is done in the ISRs.

One thing that I see here is a double clearing of RXNE: once by reading DR, then by actively clearing the flag.
This should not be a problem though, unless some other IRQ interrupts the ISR in between for much too long.

This puzzles me:

--- Quote ---the 32F4 UART transmits "an empty frame" there, apparently without a start bit
--- End quote ---

When transmission is enabled, an idle frame is sent - of course without a start bit, otherwise it would just be a spurious character! - is this what you are referring to? And why this needs "clearing" (or rather, what "clearing" means here)?


--- Quote ---flush RX buffer, as many times as the FIFO is deep (2x for the 32F4 one)
--- End quote ---
Why 2x? There's no FIFO in STM32F417, AFAICS - clearing the status flags should be enough.


--- Quote ---One thing I wondered was whether the GPIO (RX) is sampled fast enough; there is a range of sampling rates. I see code setting it up for "very high"
--- End quote ---
The data on the I/O pin is sampled every AHB1 clock, if, OTOH, you are referring to UART oversampling, 16 or 8 are the only alternatives.
On the gripping hand, speed settings in GPIOx_OSPEEDR have absolutely nothing to do with input, or sampling.

peter-h:

--- Quote ---One thing that I see here is a double clearing of RXNE: once by reading DR, then by actively clearing the flag.
This should not be a problem though, unless some other IRQ interrupts the ISR in between for much too long.
--- End quote ---

I didn't realise the 32F4 did that. I know some UARTs do.

EDIT: you are right; no need to clear RXNE. Big improvement in the error rate! I then did the same thing to the TX complete ISR, and it still runs:


--- Code: ---
void Kde_serial_transmit_complete_irq_handler(UART_HandleTypeDef *huart)
{

// Transmit complete
uint8_t port = KDE_SERIAL_MAX_PORT; // max port number

// Find which port this is
while (port > 0 && huart->Instance != kde_serial_port_handles[port-1]->Instance)
{
port--;
}

// Transmit complete
kde_serial_transmit_active[port-1] = false;

// Clear and disable the TC interrupt
//huart->Instance->SR &= ~(UART_FLAG_TC); // not needed
__HAL_UART_DISABLE_IT(huart, UART_IT_TC);

}


--- End code ---


--- Quote ---When transmission is enabled, an idle frame is sent - of course without a start bit, otherwise it would just be a spurious character! - is this what you are referring to? And why this needs "clearing" (or rather, what "clearing" means here)?
--- End quote ---

"Clearing" is the wrong word really. My reading of the RM was that when you enable the UART, you can't use it for anything because it transmits that spurious byte (without a start bit; I agree and I checked that) at the current baud rate, which could be quite a long time. So my UART init code runs a timer for that, before it returns.


--- Quote ---Why 2x? There's no FIFO in STM32F417, AFAICS - clearing the status flags should be enough.
--- End quote ---

With some UARTs, you have a FIFO for data and a parallel FIFO for error conditions, so if say you get 1 good byte and 1 byte with duff parity, you read out the 1st byte and then the parity error status bit pops up. Hence the habit of clearing error conditions more than once, but this should be accompanied by a read of the DR.


--- Quote ---The data on the I/O pin is sampled every AHB1 clock, if, OTOH, you are referring to UART oversampling, 16 or 8 are the only alternatives.
On the gripping hand, speed settings in GPIOx_OSPEEDR have absolutely nothing to do with input, or sampling.
--- End quote ---

I think that once a couple of GPIOs are attached to a UART TX and RX, the config for the clock speed on those GPIOs is bypassed. It would be silly otherwise, and high baud rates would be problematic due to the loss of resolution on edge detection.

I am working on this full time and testing on two boards. One of the boards is not exhibiting the total lack of an RX interrupt on the 1st byte, but it isn't running the same RTOS tasks. The other board does produce the RX interrupt if I comment out some code which doesn't even get executed during the loopback! (But does get executed during UART initialisation). Disabling some RTOS tasks changes the error rate, which is a useful hint. AFAIK the ISR code is mainly from Cube MX (in fact most of it obviously is, looking at the machine comments) and ST have had lots of cases where there were critical regions. I wonder what the relative priority of the serial interrupts is. This is the ex MX code for the four UARTs. I struggle to find where the interrupt priority is configured :)

This issue could have gone for years without anybody noticing. It is only a full-on loopback test at 115k that shows it.


--- Code: ---
// Common interrupt handler for all USART interrupts
static void usart_common_IRQHandler(UART_HandleTypeDef *huart)
{
// check if we are here because of RXNE interrupt
if (huart->Instance->SR & USART_SR_RXNE) { //if RX is not empty
Kde_serial_receive_irq_handler(huart);
}

// check if we are here because of TXEIE interrupt
if (huart->Instance->SR & USART_SR_TXE) { //if TX is empty
Kde_serial_transmit_irq_handler(huart);
}

// check if we are here because of the TC interrupt
if (huart->Instance->SR & USART_SR_TC) {
Kde_serial_transmit_complete_irq_handler(huart);
}
}

/**
  * @brief This function handles USART1 global interrupt.
  */
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
usart_common_IRQHandler(&huart1);
/* USER CODE END USART1_IRQn 0 */

// Standard HAL handler - not required
//HAL_UART_IRQHandler(&huart1);

/* USER CODE BEGIN USART1_IRQn 1 */

/* USER CODE END USART1_IRQn 1 */
}

/**
  * @brief This function handles USART2 global interrupt.
  */
void USART2_IRQHandler(void)
{
/* USER CODE BEGIN USART2_IRQn 0 */
usart_common_IRQHandler(&huart2);
/* USER CODE END USART2_IRQn 0 */

// Standard HAL handler - not required
//HAL_UART_IRQHandler(&huart2);

/* USER CODE BEGIN USART2_IRQn 1 */

/* USER CODE END USART2_IRQn 1 */
}

/**
  * @brief This function handles USART3 global interrupt.
  */
void USART3_IRQHandler(void)
{
/* USER CODE BEGIN USART3_IRQn 0 */
usart_common_IRQHandler(&huart3);
/* USER CODE END USART3_IRQn 0 */

// Standard HAL handler - not required
//HAL_UART_IRQHandler(&huart3);

/* USER CODE BEGIN USART3_IRQn 1 */

/* USER CODE END USART3_IRQn 1 */
}



/**
  * @brief This function handles USART6 global interrupt.
  */
void USART6_IRQHandler(void)
{
/* USER CODE BEGIN USART6_IRQn 0 */
usart_common_IRQHandler(&huart6);
/* USER CODE END USART6_IRQn 0 */

// Standard HAL handler - not required
//HAL_UART_IRQHandler(&huart6);

/* USER CODE BEGIN USART6_IRQn 1 */

/* USER CODE END USART6_IRQn 1 */
}


--- End code ---

peter-h:
Newbrain fixed it!

It was the surplus (double) clearing of the interrupts.

peter-h:
However, I am surprised that this ISR works given that it no longer clears TC


--- Code: ---
void Kde_serial_transmit_complete_irq_handler(UART_HandleTypeDef *huart)
{

// Transmit complete
uint8_t port = KDE_SERIAL_MAX_PORT; // max port number

// Find which port this is
while (port > 0 && huart->Instance != kde_serial_port_handles[port-1]->Instance)
{
port--;
}
// Transmit complete
kde_serial_transmit_active[port-1] = false;

// Clear and disable the TC interrupt
//huart->Instance->SR &= ~(UART_FLAG_TC); // Not needed
__HAL_UART_DISABLE_IT(huart, UART_IT_TC);

}


--- End code ---

Pag 1008 in the RM states



but the last sentence is ambiguous in which way of clearing it it applies to.

Still, disabling the interrupt (as the ISR does) should be sufficient. It gets enabled when a byte is placed into the UART for transmission, by the foreground code. That code does a read of SR (a read modify write, to clear TC) and a write to DR; exactly what the RM asks for, and I reckon this is accidentally right ;)


--- Code: ---
// Transmitter is not already running so load the first byte and enable the TXE and TC interrupts
// First clear TC
kde_serial_port_handles[port]->Instance->SR &= ~(UART_FLAG_TC);
kde_serial_port_handles[port]->Instance->DR = kde_serial_transmit_buffer[port][kde_serial_transmit_buffer_tx_index[port]++];
kde_serial_transmit_buffer_tx_index[port] %= KDE_SERIAL_TX_BUFFER_SIZE;
kde_serial_transmit_active[port] = true;

// Enable TX empty and TX complete interrupts
__HAL_UART_ENABLE_IT(kde_serial_port_handles[port], UART_IT_TXE);
__HAL_UART_ENABLE_IT(kde_serial_port_handles[port], UART_IT_TC);

--- End code ---

Navigation

[0] Message Index

[#] Next page

There was an error while thanking
Thanking...
Go to full version