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.
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( ) ) else enable( XIRQ ) //may take interrupt immediately case XNOTIFIER xrdy = true Reply( requester, ) if ( !buffer-empty( ) ) xrdy = false write( buffer-extract( ) ) else 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( ) )
case XNOTIFIER
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 )
and
int Put( int server-tid, int port, char c )
These are wrappers for sends to one or more serial servers.
-
On Get the server blocks the client until data is available
-
Does a non-blocking TryGet give you anything you cannot do
with task structure?
-
Put is non-blocking. The only guarantee is that the character
has been added to a buffer of characters to be transmitted
some time in the future.
How should we handle a terminal?
Issues. One issue stands out as a lesson for similar occurrences. Echo
Either
-
one server per channel,
-
passing characters from one notifier to the other
Or
- courier connecting receive server to transmit server
- What characteristics does the terminal have? termcap follies
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
-
The train application puts a byte in the hold register. The
byte is moved into the shift register and shifted out.
-
The stop bit is transmitted.
-
A train xmit interrupt occurs.
-
The train application checks CTS.
-
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.
-
The train application puts a byte in the hold register, which is
loaded and shifted out.
-
The stop bit is transmitted.
-
Two things happen in parallel.
-
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.
-
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.
-
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.
-
Use it wisely, which means as little as possible.
-
Any time you can substitute computation for communication you should
do so.
Debugging Real-time Programs
The most common set of debugging tools used by experienced
programmers is the oldest: printf, grep & stack trace.
-
The power of these tools is greatly enhanced by strong
conventions in code formatting.
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.
- Gather data.
- Create a model that explains the data
- Test the model
- If the model is not correct, go to 1.
-
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.
-
The right thing may happen, but at the wrong time.
-
Small changes in the order of events may cause big changes
in execution. E.g., critical races.
-
The order of events is partly controlled by the real world.
-
The rate at which events occur, and the rate at which humans
perceive, are both very slow compared to the rate at which
program states change.
Critical Races
There is no known method for eliminating critical races.
-
Synchronizing everything, which seems to be an obvious solution,
kills performance because it removes flexibility from the
execution.
It is, in principle, impossible to test away critical races. Why?
- When three trains run continuously for ten minutes, how many events
occur in the real world?
- How many possible orders are there for these events?
- Re-ordering isn't even necessary for a critical race to occur, just
getting too close in time.
Tools you might use to assist debugging
Redboot
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.
Breakpoint
What does it do?
-
takes a snapshot of the system
-
The body of the program must remain undisturbed.
-
This means that computation, including response to interrupts,
must stop, or it isn't a snapshot.
- It's easy to make a mistake and have some parts of the
program continuing to run while you are interrogating the
system.
-
provides interactive tools for examining kernel data structures,
such as
- task descriptors
- lists and queues
- stacks, including the program counter and local variables, of
individual tasks
- restart system immediately afterwards
- If you want to continue where processing stopped you must make
certain that all state is saved when you enter Beakpoint
and restored when you leave it. What about pending interrupts?
You can't stop the entire universe!
- Otherwise you can re-enter RedBoot.
How do you get it started?
-
function call, which you insert in your code when compiling.
-
The function call must break an abstraction barrier
by turing off interrupts before running Breakpoint
code.
-
But it's the easiest and fastest form to implement.
-
Having the call as part of ASSERT is common.
-
Has to exit to RedBoot. Why? (Jump to x00.)
-
system call instead of function call, which respects the kernel/user
distinction.
-
an exception triggered externally
-
at initialization
-
Set up the system so that an external event will
generate an FIQ exception.
-
E.g. attach a button to PDIO on the third connector,
set up ICU.
-
at run-time
- Trigger the interrupt
- Switch to Breakpoint in the event handler
- Either exit to RedBoot,
- Or clean up pending interrupts and resume execution.
Breakpoint is a special case of a particular sort of tool that
is very common.
- condition occurs => information is made available
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.
- Anything that terminates execution abnormally prints the set of active
stack frames
- Minimal version
- name of calling function
- line number of call
- Extreme version
- values of arguments
- values of local variables
What is the equivalent of a stack trace in a real-time multi-tasking
environment?
- How would you implement it?
Two basic questions to answer.
- When is it produced?
- What should be in it?
- How would you make it readable?
Return to: