Exploring STC MCU part 5 – Speak up

It is finally the time to get the chip to speak up – start communicating over the serial link. Keeping true to the nature of my previous example code, the serial driver code will adapt to the oscillator frequency and baudrate you wish to use.

Wiring cautions

The experiment circuit for the UART communication experiment. The LED will be removed in the next experiment.
The experiment circuit for the UART communication experiment. The LED will be removed in the next experiment.

Since the microcontroller draws so little current when running and have such a wide operation voltage range, it is very possible to be powered unexpectedly by the signal pins of the USB to UART adapter, preventing it from being properly reset if USB to Serial is connected but the RESET pin not being enabled and brought out. The 8051-style quasi-bidirectional I/O pins also means using a resistor between the push-pull output of the USB to UART adapter and the RX pin on the chip can result in reliability issues. So some circuitry other than high-value resistors have to be put in between the two.

The test circuit of IAP15W4K61S4 at 30MHz, ISP/ICD probe and USB-Serial adapter attached.
The test circuit of IAP15W4K61S4 at 30MHz, ISP/ICD probe and USB-Serial adapter attached.

Which timer to clock it?

The four UARTs in IAP15W4K61S4 have to be clocked from one of the five timers, true to their Intel 8051 ancestors, but unlike most microcontrollers whose UARTs often comes with built-in baud rate generators. The timer 2 can clock all UARTs, while all except UART2 can also be clocked from another timer. Since I am using UART1, and I intend to keep all UARTs independent of each other, I am clocking UART1 off Timer 1.

Just like when implementing the system core timer using Timer 0, Timer 1 is operating in the 16-bit auto-reload mode. The datasheet contained a demo program that had the correct baud rate formula.

To buffer or not to buffer

UART hardware in most microcontrollers don’t come with a lot of FIFO buffer stages, and sending data off the line takes time. So instead of spin waiting or polling on status bits, it is better to buffer input and output data, and use interrupt-driven buffered input and output.

Since this code is running on a microcontroller with 2kB or 4kB SRAM, I can take advantage of those extra RAM and use bigger buffers. This example implements an input buffer and an output buffer each 256 bytes long. This buffer length also allowed me to simply allow the buffer pointers to overflow instead of relying on the much more expensive division instruction.

In fact, if your code runs on less RAM, you can still use the same technique with smaller buffers, after masking off a few bits when accessing or calculating the pointers though.

Putting it all together

Here is the serial driver code. I have coded it to work with the Keil C51 library, so after calling serial_open() you will be able to printf() to the serial port.

#include "serial.h"

#include <STC15F2K60S2.H>
#include <stdio.h>
#include "wdt.h" // for the wait() macro

// Since I am using Keil C library, I/O functions themselves are handled
// through system libraries and call back here.

// This implementation uses asynchronous receive buffer and synchronous
// transmit buffer.

// On the hardware layer, the UART1 interface is used, multiplexed to pins
// P3.6 and P3.7, clocked by Timer 1.

// Ring buffers for RX and TX.
volatile char xdata rx_buffer[256]; // 256 bytes of receive buffer
volatile char xdata tx_buffer[256]; // 256 bytes of transmit buffer
volatile unsigned char rx_head = 0;
volatile unsigned char rx_tail = 0;
volatile unsigned char tx_head = 0;
volatile unsigned char tx_tail = 0;
volatile bit sending = 0;

void serial_open(unsigned long baudrate)
{
	// baudrate = (F_CPU / (65536 - reload)) / 4
	unsigned short reload = 65535 - F_CPU / 4 / baudrate + 1;
	
	// Set up signal routing around UART1
	AUXR1 = (AUXR1 & 0x3f) | 0x40; // Multiplex UART1 to P3.6/P3.7
	P3M0 &= ~0xc0; // Put pins P3.6/P3.7 into quasi bidirectional mode.
	P3M1 &= ~0xc0;
	AUXR &= ~0x01; // Clock UART1 from T1
	PCON &= ~0x40; // Expose SM0 bit
	SM1 = 1; // UART 8-N-1
	SM0 = 0;
	SM2 = 0; // Point-to-point UART
	ES = 1; // Enable serial interrupt
	
	// Set up timer 1 for baudrate (will look a lot like systick.c)
	TMOD = (TMOD & 0x0f) | 0x00; // Run T1 in mode 0 (16-bit reload)
	AUXR |= 0x40; // Run T1 at F_CPU
	TL1 = reload; // Set the reload value for given baudrate.
	TH1 = reload >> 8;
	ET1 = 0; // We don't need interrupts from timer 1.
	TR1 = 1; // Start timer 1
	
	// Start the UART receiver
	REN = 1;
}

void serial_close(void)
{
	REN = 0; // Stop the receiver.
	
	// Stop the transmitter after the buffer is emptied.
	wait(tx_head != tx_tail);
	TR1 = 0;
}

void serial_interrupt(void) interrupt 4
{
	if (RI) // Receiver interrupt
	{
		RI = 0;
		
		if (rx_head != rx_tail - 1)
		{
			rx_buffer[rx_head] = SBUF;
			rx_head++;
		}
	}
	
	if (TI) // Transmitter interrupt
	{
		TI = 0;
		
		if (tx_head != tx_tail)
		{
			sending = 1;
			SBUF = tx_buffer[tx_tail];
			tx_tail++;
		}
		else
		{
			sending = 0;
		}
	}
}

// _getkey() and putchar() are already prototyped in <stdio.h>, so no need to
// put them in the "serial.h" file, and you can printf() to UART1 this way.

char _getkey(void)
{
	if (rx_head != rx_tail)
	{
		char ch = rx_buffer[rx_tail];
		rx_tail++;
		return ch;
	}
	else
	{
		return 0x1b;
	}
}

char putchar(char ch)
{
	if (sending)
	{
		// Buffer is not empty - sending action under way.
		
		// Wait for a byte in the send buffer.
		wait(tx_head + 1 == tx_tail);
		tx_buffer[tx_head] = ch;
		tx_head++;
	}
	else
	{
		// Buffer empty. Send on.
		sending = 1;
		SBUF = ch;
	}
	
	return ch;
}

Coming up

Next time, the experiment will venture into the peripherals specific to STC chips, and start looking into ADC.

Leave a Reply