CS452 - Real-Time Programming - Winter 2018

Lecture 15a - Serial I/O, Debugging

Public Service Announcements

  1. Due date for kernel 4: 16 February, 2018.
  2. March break open house: March 10.
  3. Exam scheduled for Monday, April 23.

Serial I/O


Server-Notifier-Kernel Architecture

There are many different ways to spread the state requirements for transmission across the different tasks used when transmitting bytes through the UARTs. The three examples that follow are chosen to show how functionality can be moved. Using these ideas you can realise many different implementations than the sketches shown here.

Everything in the Server

Putting everything in the server is a common way to get started. Here is a sketch of one method for doing so. For simplicity fifos are disabled.

AwaitEvent handling in kernel.

	Unblock notifier.

Transmit Notifier

	AwaitEvent( XIRQ )	// Returns when transmitter is ready.
	Send( server, , )

Transmit Server

	Receive( &requester, msg )
	switch ( msg.type )
		case CLIENT
			buffer-insert( msg.byte )	// buffer-empty = false
			Reply( requester, )
			if ( xrdy )
				xrdy = false
				write( buffer-extract( ) )
				enable( XIRQ )	//may take interrupt immediately
			xrdy = true
			Reply( requester, )
			if ( !buffer-empty( ) )
				xrdy = false
				write( buffer-extract( ) )
				disable( XIRQ )
xrdy keeps track of whether the transmitter is ready or not. buffer-empty( )<\code> keeps track of whether or not there is a byte to transmit.

Enabling and Disabling in the Notifier

AwaitEvent handling in kernel.

	Unblock notifier.

Transmit Notifier

	Send( server, , reply )
	enable( XIRQ )
	AwaitEvent( XIRQ )
	write( reply.byte )
	disable( XIRQ)

Transmit Server

	Receive( client | xnotifier, msg )
	switch ( msg.type )
		case CLIENT
			buffer-insert( msg.byte )
			Reply( client )
			if ( xnot-waiting )
				xnot-waiting = false
				Reply( xnotifier, buffer-extract( ) )
			xnot-waiting = true
			if ( !buffer-empty( ) )
				xnot-waiting = false
				Reply( xnotifier, buffer-extract( ) )

Minor alterations of this code can put the fifo in the notifier, and/or move more than one byte at a time.

Enabling and Disabling in the Kernel

AwaitEvent handling in the kernel

	//handling AwaitEvent( XIRQ, byte )
	enable XIRQ
	save byte
	//when XIRQ occurs
	write byte on UART
	disable XIRQ
	make AwaitEvent caller ready

Transmit Notifier

	Send( server, , byte )
	AwaitEvent( XIRQ, byte )

Transmit server: as above

Primitives for Serial I/O

We are supposed to support

int Get( int server-tid, int port )


int Put( int server-tid, int port, char c )

These are wrappers for sends to one or more serial servers.

How should we handle a terminal?

Issues. One issue stands out as a lesson for similar occurrences. Echo


How ahould we handle the train controller

The only thing that is new is adding CTS to the state machine. But...

CTS timing.

What you expect to occur when sending to the train controller

  1. The train application puts a byte in the hold register. The byte is moved into the shift register and shifted out.
  2. The stop bit is transmitted.
  3. A train xmit interrupt occurs.
  4. The train application checks CTS.
  5. If CTS is set, you put the next byte in the hold register.

Sometimes an implementation based on this expectation surprises you. In reality what happens when you are sending to the train controller the following set of events occur.

  1. The train application puts a byte in the hold register, which is loaded and shifted out.
  2. The stop bit is transmitted.
  3. Two things happen in parallel.
    1. In the train controller,
      • The train controller sees the stop bit.
      • Then, probably on the next clock edge, it latches the character and negates CTS.
      • After one or two clock cycles the byte is clear of the train UART, and CTS is re-asserted.
    2. In the ARM CPU
      • The train xmit interrupt occurs.
      • The train application checks if there another byte to send, and
      • if there is, it checks the CTS bit in the modem status register (CTS-sr). What value does it read?
        • When the stop bit was transmitted CTS-sr remains asserted.
        • Then, the train controller negates the CTS state internally, probably on the next edge of its UART clock; the ARM UART latches the negated signal on the edge of its UART clock, and into its CTS-sr on the next edge of the peripheral bus clock. From this point CTS-sr is negated.
        • Finally, when CTS is re-asserted in the train controller UART and has been latched a second time in the ARM UART CTS-rs is re-asserted.
    The train application is assumed to check CTS after the train controller has negated it. It may even be unaware that CTS remains negated for a short while. Regardless, because the two sets of events happen in parallel the result is non-deterministic.
  4. If CTS is set, the train application puts the next byte in the hold register.
Whether the final action succeeds, drops a character or crashes the train controller is determined by the whims of the gods.

How do you solve this problem is for you to discover. Hint, interrupts occur in a deterministic order.

Bandwidth of communication with the train controller will probably be the limiting factor in your trains project.

Debugging Real-time Programs

The most common set of debugging tools used by experienced programmers is the oldest: printf, grep & stack trace.

Debugging real-time programs, at its base, is just the same as any other debugging, just the same as discovery in any empirical science or any kind of problem solving.

  1. Gather data.
  2. Create a model that explains the data
  3. Test the model
  4. If the model is not correct, go to 1.
  5. Remember that the model is ALWAYS provisional: data collected later may invalidate it, no matter how much data has confirmed it.

But real-time programs are harder to debug. Why? There are new types of bugs.

Critical Races

There is no known method for eliminating critical races.

It is, in principle, impossible to test away critical races. Why?

Tools you might use to assist debugging


The memory contents are not wiped by reset. Some of the most difficult errors can be detected only by using the contents of memory after a reset. Produce useful results by inserting

      str   pc, <magic location>
many places in your code. Redboot can tell you the contents of the magic places after the program crashes. Then, with the assistance of a load map, you can find out where you were in which code when the problem occurred.

You can often find out this information by looking at the garbage output that often appears at the bottom of the screen. One set of eight characters is the hex address of the instruction that could not be executed. With a load map and assembly listings you can find the instruction on which the program died.

In RedBoot you can, in principle, trace any of the kernel data structures by examining the kernel's stack, but it's extremely arduous to do so.


What does it do?

How do you get it started?

Breakpoint is a special case of a particular sort of tool that is very common.

Such tools (e.g. printf, ASSERT) are very simple in single threaded programs and in multi-threaded single-core programs without hardware interrupts, but they are not simple in multi-core or hardware interruptible programs.

Getting information closer to real-time.

Stack Trace

In single-threaded programs this is often the most useful tool.

What is the equivalent of a stack trace in a real-time multi-tasking environment?

Return to: