Lecture 16 – A device driver: Difference between revisions

Copyright © 2017–2023 J. M. Spivey
Jump to navigation Jump to search
Line 62: Line 62:
Why use @sendrec()@ here?  There are three reasons: in order of increasing importance, (i) it is shorter than writing two separate calls; (ii) it is more efficient, because after the client process sends a request and suspends, the client does not need to run again before being ready to receive the reply; and (iii) using @sendrec()@ avoids a situation called ''priority inversion'', where a high-priority process with work to do is blocked because another, lower-priority, process needs to run first.
Why use @sendrec()@ here?  There are three reasons: in order of increasing importance, (i) it is shorter than writing two separate calls; (ii) it is more efficient, because after the client process sends a request and suspends, the client does not need to run again before being ready to receive the reply; and (iii) using @sendrec()@ avoids a situation called ''priority inversion'', where a high-priority process with work to do is blocked because another, lower-priority, process needs to run first.


==@sendrec()@ and priority inversion==
With separate @send()@ and @receive()@, the sequence of events will be as follows.
With separate @send()@ and @receive()@, the sequence of events will be as follows.
# The client process calls @send()@ and (we assume) immediately rendezvous with the @receive()@ call at the top of the server loop.
# The client process calls @send()@ and (we assume) immediately rendezvous with the @receive()@ call at the top of the server loop.

Revision as of 12:41, 6 March 2022

The Nordic chip on the microbit has a temperature sensor that measures the temperature of the processor die in units of 1/4° Celsius. The device consists of an analog circuit that exploits some temperature-dependent property of silicon, together with a dedicated A-to-D converter that digitises a reading. The A-to-D converter takes some time to settle, so there is a protocol where the processor initiates a reading, then must wait for an event that can be signalled by interrupt before getting the result. We will aim to write a device driver for the sensor, and provide a function

int temp_reading(void)

that can be called from any process to get an up-to-date temperature reading.

Reasons for having a separate device driver process:

  • We can manage concurrent access from multiple processes and prevent a new reading being initiated before the previous one is finished.
  • We can connect to the interrupt from the device and react when a reading is complete.

The hardware

Two pages from the nRF51822 manual describe the hardware of the temperature sensor. All the device registers are addressible at offsets from the base address TEMP = 0x4000C000.

  • There is an 'task' START = 0x000 that initiates a reading when triggered with TEMP.START = 1.
  • There is an 'event' DATARDY = 0x100 that indicates when the reading is ready (with TEMP.DATARDY == 1).
  • There is an interrupt enable register INTEN = 0x300 in which bit TEMP_INT_DATARDY = 0 enables the interrupt on the DATARDY event.
  • There is a data register TEMP = 0x508 that contains the value of the reading when ready.

The driver process

Because we will deal with each request – starting the reading, waiting for it to be ready, then retrieving the value – before accepting another request, we can write the driver process in a particularly simple way, where the server loop deals with one complete request for each iteration, and a call receive(INTERRUPT, ...) in the body of the loop pauses until the hardware produces a reading.

static void temp_task(int arg)
{
    message m;
    int temp, client;

    TEMP.INTEN = BIT(TEMP_INT_DATARDY);
    connect(TEMP_IRQ);
    enable_irq(TEMP_IRQ);

    while (1) {
        receive(ANY, &m);

        switch (m.type) {
        case REQUEST:
            client = m.sender;

            TEMP.START = 1;
            receive(INTERRUPT, NULL);
            assert(TEMP.DATARDY);
            temp = TEMP.VALUE;
            TEMP.DATARDY = 0;
            clear_pending(TEMP_IRQ);
            enable_irq(TEMP_IRQ);

            m.int1 = temp;
            send(client, REPLY, &m);
            break;

        default:
            badmesg(m.type);
        }
    }
}

The interface between the client and the device driver is a function temp_reading() that sends a request to the driver, awaits a reply, and returns the temperature reading (still in units of 1/4°). For the remote procedure call idiom of sending a request and waiting for the reply, we can use the sendrec system call.

int temp_reading(void)
{
    message m;
    sendrec(TEMP_TASK, REQUEST, &m);
    return m.int1;
}

Why use sendrec() here? There are three reasons: in order of increasing importance, (i) it is shorter than writing two separate calls; (ii) it is more efficient, because after the client process sends a request and suspends, the client does not need to run again before being ready to receive the reply; and (iii) using sendrec() avoids a situation called priority inversion, where a high-priority process with work to do is blocked because another, lower-priority, process needs to run first.

sendrec() and priority inversion

With separate send() and receive(), the sequence of events will be as follows.

  1. The client process calls send() and (we assume) immediately rendezvous with the receive() call at the top of the server loop.
  2. Because the server process has higher priority than the client process, the client process is suspended on the ready queue and the server process starts to run.
  3. The server process perform a temperature reading. Let's assume that during this time, other processes run and the client (despite being active) does not.
  4. The server calls send() to return a reply to the client. At this point, if the client had used sendrec(), the kernel would know that the client was waiting to receive a reply, the message could be transferred immediately, and the server (with its higher priority) could continue to run.
  5. If the client has not uses sendrec(), then the server blocks at its call the send() for the reply, and must wait until the client process is scheduled again; at that point, the message is transferred and both server and client are made active. Because the server process has higher priority, it is selected to run again.

The upshot is that, unless sendrec() is used, there is are least two additional context switches between server and client, and a situation could arise where the server was needlessly blocked for a while, waiting the a lower-priority process to be scheduled. This priority inversion does no harm in this example, because once the temperature reading is delivered, the server process has nothing more to do before waiting for the next request, but in examples where events can happen continuously, it can create a problem.

There is no way of avoiding the problem by writing the client program in a different way, unless the calls to send() and receive() are combined. When the client program is suspended, that happens inside the system call stub for send(); on being resumed, the process will return from the send() stub, prepare the arguments, and immediately call the receive() stub and enter the operating system again. Using sendrec() instead of send() followed by receive() short-circuits this sequence of events; having sent its message, the client immediately enters the RECEIVING state to wait for the reply, without having to run again first.

To implement sendrec(), we need to add another process state, SENDREC, in case the client process must wait for the server process to be ready to receive; the difference between SENDREC and SENDING is that when it has sent its message, a process in state SENDING enters the ACTIVE state, and a process in state SENDREC enters state RECEIVING, always blocking until the server sends a reply. The implementation of receive() must change to reflect this distinction.

One rule restricts the use of sendrec(), but not in a way that affects its usage in practice: the sendrec() system call cannot be used to send a REPLY message. Allowing this would allow a situation to develop where a chain of processes were waiting, each wanting to use SENDREC to send a reply to the process to its right, but blocked from doing so because that process is itself waiting to send. When the process at the far right end of the chain eventually accepts a message, the whole chain would have to unravel at once, and that would complicate the implementation without making micro:bian any more useful.

A client program

Using the timer service, it's quite easy to write a client program that requests readings from the server at (say) 1 second intervals and prints them. Without floating point arithmetic, we have to be a bit sneaky about how we print the readings.

static const char *decimal[] = {
    "00", "25", "50", "75"
};

void main_task(int n)
{
    int t = 0, temp;

    while (1) {
        temp = temp_reading();
        /* Assume temp >= 0 */
        printf("%d: Temp = %d.%sC\n", t++, temp>>2, decimal[temp&0x3]);
        timer_delay(1000);
    }
}

The init() function must start the device driver and the main process, as well as the drivers for the timer and the serial port.

void init(void)
{
    serial_init();
    timer_init();
    TEMP_TASK = start("Temp", temp_task, 0, 256);
    start("Main", main_task, 0, STACK);
}