Search code examples
uartkeil8051c51

Keil c51 When an array is declared, uart sends the first character twice, strange behavior of uVision 4


I'm currently facing a peculiar issue with UART transmission on an 89C52 microcontroller using Keil C51 and simulating it in Proteus. The code snippet is provided below:

#include <reg52.h>
#include <intrins.h>

// HERE:  When an array is declared then uart will send the first uart character twice,
unsigned long myArrayDeclaration[4] = { 0x00FFF789, };
// If the line above exists , UART will wrongly send "xxyz", instead of "xyz", nonsense. 

sbit UART_RXD = P3^0;  // RXD pin
sbit UART_TXD = P3^1;  // TXD pin

void UART_Init() { 
    TMOD |= 0x20;    // Timer 1, Mode 2 (8-bit auto-reload)  
    // TH1 = 0xFD;      // Set baud rate for 9600 bps at 11.0592 MHz crystal
    TH1 = 0xFA;       // Set baud rate for 9600 bps at 22.1184 MHz crystal
    TL1 = TH1;
 
    TR1 = 1;         // Start Timer 1

    SCON = 0x50;     // Set serial mode 1 (8-bit UART)
    EA = 1;          // Enable global interrupts
    ES = 1;          // Enable serial interrupt
} 

void UART_TxString(const char *string) {
    while (*string != '\0') {
        SBUF = *string;  // Send character
        while (!TI);     // Wait for transmit complete
        TI = 0;          // Clear transmit interrupt flag
        string++;        // Move to the next character
        //_nop_();       // This makes no difference
    }
}

void main() {  
    UART_Init();            // Init uart
    // _nop_();             // This makes no difference
    UART_TxString("xyz");   // Send 3 chars

    while (1) {
    }
}

The issue I'm encountering is that when an array is declared for example unsigned long myArrayDeclaration[4] = { 0x00FFF789, };, the UART transmission sends the first character twice, resulting in "xxyz" instead of the expected "xyz." If I remove the array declaration, the transmission works correctly.

Any kind of array provokes the problem.

I'm using a Keil C51 environment with the uVision 4 IDE and simulating the code in Proteus. The microcontroller is an 89C52 chip.

Here is a repository with the minimal reproduction of the error, and the Proteus simulation: GITHUB REPO

I appreciate any insights or suggestions on how to resolve this anomaly in UART transmission. Thank you in advance for your assistance.

I have tried:

  • To send just a byte, same result.
  • To wait (a lot) after UART_Init(), no difference.
  • To set up the optimization parameters of uVision 4 but I don't really understand what I am doing.

Solution

  • The error is to enable the serial interrupt without providing an interrupt service routine (ISR).

    You have two options:

    • Do not enable the interrupt.
    • Provide an interrupt service routine and change the rest of the code accordingly.

    Analysis

    The source clearly shows the enabling of the serial interrupt:

        EA = 1;          // Enable global interrupts
        ES = 1;          // Enable serial interrupt
    

    But the source does not define an interrupt service routine. It would look like this:

    void serial_isr(void) interrupt 4 {
      /* statements */
    }
    

    The ISR for the serial interrupt starts for the 89C52 at the fixed address of 0x0023, see the data sheet.

    The next step was to look into the map file generated by the linker to find what is here. The relevant parts are this (shortened):

    LINK MAP OF MODULE:  firmware (?C_STARTUP)
                TYPE    BASE      LENGTH    RELOCATION   SEGMENT NAME
                -----------------------------------------------------
                * * * * * * *   C O D E   M E M O R Y   * * * * * * *
                CODE    0000H     0003H     ABSOLUTE     
                CODE    0003H     008CH     UNIT         ?C_C51STARTUP
                CODE    008FH     0032H     UNIT         ?PR?_UART_TXSTRING?MAIN
    

    And this (shortened):

    SYMBOL TABLE OF MODULE:  firmware (?C_STARTUP)
      VALUE           TYPE          NAME
      ----------------------------------
    
      -------         MODULE        ?C_STARTUP
      C:0003H         SEGMENT       ?C_C51STARTUP
      C:0000H         PUBLIC        ?C_STARTUP
      C:0006H         SYMBOL        IDATALOOP
      C:0003H         SYMBOL        STARTUP1
      C:0000H         LINE#         126
      C:0003H         LINE#         133
      C:0005H         LINE#         134
      C:0006H         LINE#         135
      C:0007H         LINE#         136
      C:0009H         LINE#         185
      C:000CH         LINE#         196
      -------         ENDMOD        ?C_STARTUP
    
      -------         MODULE        MAIN
      C:0000H         SYMBOL        _ICE_DUMMY_
      C:00DAH         PUBLIC        UART_Init
      C:008FH         PUBLIC        _UART_TxString
    
      D:0018H         SYMBOL        string
    

    There is a gap between the end of the module ?C_STARTUP at 0x0C and the start of the module MAIN at 0x8F. Since no other module uses this space, it is filled with zeroes.

    A zero byte is the machine code instruction for NOP. The processor does nothing and advances to the next instruction.

    This is what happens:

    1. The serial interrupt is triggered when the transmission of the first character is executed and therefore TI is set.
    2. The function UART_TxString() is interrupted in its while loop without a chance to see TI set.
    3. The processor pushes its program counter (PC) to the stack and jumps to 0x0023.
    4. There are the mentioned zero bytes, and the processor happily runs through them without any other action.
    5. Finally it arrives at the next non-zero byte, which happens to be the first instruction of UART_TxString() at 0x008F (see excerpt of the map file).
    6. Because of the inner working of the generated machine code, the parameter string is at the static fixed location 0x18 (see excerpt of the map file).
    7. string still points to the first character, and so SBUF receives again the first character.
    8. TI is still set, because it does not reset automatically.
    9. So the while loop quits immediately. TI is reset by software.
    10. Luckily, because the first character was completely sent, the repeated character just left SBUF to the shift register of the UART. So the next character does not overwrite it.
    11. The next time TI is set, the interrupt service routine is not triggered, because ISRs do not interrupt themselves. (Note: Yes, you can do that, but it needs respective instructions, which are not here.)
    12. And so the function UART_TxString() does what is shall do, sending all the characters of string.
    13. When all characters are sent, the function returns.
    14. It returns to the while loop where the first interrupt happened. Since TI will never become set again, it loops forever.

    Because your program has this endless while loop in main(), you cannot notice the difference.

    Final line: It was pure coincidence that this happened. Any other result emerges when some detail of the program changes.