6

Am I complicating things?

I'm architecting my code to talk from a 8051 micro to a peripheral device over UART. The peripheral responds to commands from the host and can only respond to one command at a time. It's a simple send and receive protocol. (tx1, rx1, tx2, rx2, tx3, rx3) Each TX message is terminated with a CR, each response is terminated with a >. I can't send a new message until I receive the response to the last one. Responses can also echo print the original TX message in the beginning if I enable that option (but this causes more traffic)

An example message would be:

  • TX: Hello
  • RX: World!>

Or with echo option...

  • TX: Hello
  • RX: Hello\rWorld!>

Option A A function such as getHello would consist of both the send and receive. A parallel ISR routine would gather the incoming bytes and throw a flag when the '>' character is received.

char* getHello(char * buf){
    sendMsg("Hello\r");
    delay(10ms); //wait a little bit

    //wait for receive to come in or timeout to occur
    while(!receiveFlag || !timeoutFlag);  //thrown by ISR
    receiveMsg(buf);
    //parse the message and do some other stuff
    return buf;
}

Pros:

  • Everything is contained within one function.
  • Easier to debug

Cons:

  • This function is blocking and could hang if the peripheral never responds so a timeout must be implemented.
  • Messages can't be received out of order, must be in series (ie, tx1, rx1, tx2, rx2, tx3, rx3)

Option B A parallel approach is taken. Two separate functions would created. one to send the message, and one that would vertexed upon receiving a response from the ISR.

void sendHello(){
    sendMsg("Hello\r");
    //do some other stuff if needed
}

char* receiveMsg(char * buf){
    //figure out from echo print what the tx message was
    //use a switch statement to decide which response parser to call
    switch(txMessage){ //pseudo code
    case "Hello":
        receiveMsg(buf);
        //parse the message and do some other stuff
        break;
    }
    return buf;
}

Pros:

  • Can handle parallel messages coming back out of order because it relies on the echo printing of the tx message to figure out how to parse it. (ie, tx1, tx2, tx3, rx1,rx2,rx3)

Cons:

  • quite difficult to debug
  • spawns multiple threads
  • lots of extra code
  • not worth it since messages will definitely come back in order

Right now, I'm doing Option B, but as I continue on with the project, I begin to feel like this is getting overly complex. I'm curious what you guys think.

Thanks!

dsolimano
  • 8,870
  • 3
  • 48
  • 63
Jonathan
  • 1,498
  • 2
  • 20
  • 41
  • `case "Hello":` you are comparing pointers with this, not strings. – ouah Nov 22 '12 at 21:36
  • Generally speaking I would: Empty the recieve buffer and wait for a certain amount of time to verify nothing is coming in. Then send the prompt and start a timer. Then on each interrupt of a byte comming into a buffer, buffer it up until you get the end > character, or time out occurs. The interrupt can then trigger a flag to indicate back to main that timeout or a valid packet was recieved. – Myforwik Nov 22 '12 at 21:36
  • Ouah- yes I know, I was lazy with my pseudo code. – Jonathan Nov 22 '12 at 21:59
  • Myforwik - it seems like your suggest is very similar to option b. are there any differences? – Jonathan Nov 23 '12 at 03:22

2 Answers2

4

I tend to do this kind of stuff, though, Id tend to have a separate serial port "class" ( struct + functions ) and a protocol class that lives over the top of serial port. I used these all the time in my embedded systems. This gives you the best of both worlds, a blocking synchronous call and an async call so you can pseudo multitask.

typedef struct serial_port_s serial_port;
typedef void (*serial_on_recived_proc)(serial_port* p);
typedef struct serial_port_s{
    bool timeoutFlag;
    bool receiveFlag;
    void* context;
    serial_on_recived_proc response_handler;
};

void send_serial(serial_port* p, char* message)
{
    //SendMsg?
}
void receive_serial(serial_port* p, char* response)
{
    //receiveMsg?
}

bool has_data(serial_port* p)
{
    return p->receiveFlag;
}

bool has_timed_out(serial_port* p)
{
    return p->timeoutFlag;
}
bool is_serial_finished(serial_port* p)
{
    return has_data(p) || has_timed_out(p); 
}

bool serial_check(serial_port* p)
{
    if(is_serial_finished(p) && p->response_handler != NULL)
    {
       p->response_handler(p)
       p-> response_handler = NULL;
       return true;
    }
    return false;
}

void send(serial_port* p, char* message, char* response)
{
    p->response_handler=NULL;
    send_serial(p, message);
    while(!is_serial_finished(p));
    receive_serial(p, response);
}

void sendAsync(serial_port* p, char* message, serial_on_recived_proc handler, void* context)
{
    p->response_handler = handler;
    p->context = context;
    send_serial(p, message);
}

void pow_response(serial_port* p)
{
    // could pass a pointer to a struct, or anything depending on what you want to do
    char* r = (char*)p->context;  
    receive_serial(p, r);
    // do stuff with the pow response
}

typedef struct
{
   char text[100];       
   int x;
   bool has_result;
} bang_t;

void bang_parse(bang_t* bang)
{
   bang->x = atoi(bang->text);
}

void bang_response(serial_port* p)
{
    bang_t* bang = (bang_t*)p->context;  
    receive_serial(p, bang->text);
    bang_parse(bang);
    bang->has_result=true;
}

void myFunc();
{
    char response[100];
    char pow[100];
    bang_t bang1;
    bang_t bang2;
    serial_port p; //
    int state = 1;
    // whatever you need to do to set the serial port

    // sends and blocks till a response/timeout
    send(&p, "Hello", response);
    // do what you like with the response

    // alternately, lets do an async send...
    sendAsync(&p, "Pow", pow_response, pow);       

    while(true)
    {
        // non block check, will process the response when it arrives               
        if(serial_check(p))
            {
              // it has responded to something, we can send something else...

              // using a very simple state machine, work out what to send next.
              // in practice I'd use enum for states, and functions for managing state
              // transitions, but for this example I'm just using an int which
              // I just increment to move to the next state
              switch(state)
              {
              case 1: 
                 // bang1 is the context, and will receive the data
                 sendAsync(&p, "Bang1", bang_response, &bang1);
                 state++; 
                 break;
              case 2:
                 // now bang2 is the context and will get the data...
                 sendAsync(&p, "Bang2", bang_response, &bang2);
                 state++; 
                 break;
              default:
                 //nothing more to send....
                 break;
              }
            }
        // do other stuff you want to do in parallel
    }
};
Keith Nicholas
  • 43,549
  • 15
  • 93
  • 156
  • Keith, fascinating response. I'm going to implement something like this now and see how it works. I really appreciate your time – Jonathan Nov 23 '12 at 03:43
  • no problem, I just updated it so it clear the handler once it processes the response, otherwise, it will keep triggering. There's likely other wee problems in practice. I also tend to make the hardware dependent bit very very tiny, so I can test a lot of the code on a PC with stubs ( that way I can use the PC serial port and get the logic all sorted before trying to embed it, its also easier to unit test it ) – Keith Nicholas Nov 23 '12 at 03:58
  • Can you recommend a specific Dev environment to test the code on a PC? Right now I'm developing in IAR workbench and building directly to the embedded system. – Jonathan Nov 23 '12 at 04:06
  • 1
    I use visual studio, but if on Linux, you could use gcc and your fav tool. The trick is to keep all the hardware specific stuff as isolated as possible then stub out all those functions. Not as hard as it first seems, and once you've done that, development is a lot quicker on PC ( I also wrote a C unit testing framework called SeaTest which I also use http://code.google.com/p/seatest/ for unit testing). By simulating your system and unit testing, it makes it a lot easier when you put it on the device. Usual problem on hardware is quirks of the hardware and timing problems. – Keith Nicholas Nov 23 '12 at 06:24
  • Hey Keith, I really like the function pointer idea with the async call. Can you explain what context does? – Jonathan Nov 27 '12 at 13:35
  • the context is a very common pattern you will see in C code when dealing with callbacks using function pointers. It's meant so you can get access to data. in my example I simply pass a char* in as my context, so when the call back triggers, it can put the result into that string by casting the context back to a char*. More often you end up making structs and passing them as context. I'll update my answer with another example.... – Keith Nicholas Nov 27 '12 at 18:44
0

Take things simple. An ISR routine must be very fast so for me the best approch is to have a global RXBuffer like this:

#include <cstdint>
#include <deque>
#include <algorithm>

class RXBuffer {
public:
    friend class ISR;

    typedef std::deque<uint8_t>::const_iterator const_iterator;

    RXBuffer();
    size_t size() const { m_buffer.size(); }

    // read from the buffer in a container in the range [first, last)
    template <typename Iterator>
    void read(Iterator first, Iterator last, Iterator to)
    {
        // how many bytes do you want to read?
        size_t bytes_to_read = std::distance(first, last);

        if (bytes_to_read >= size())
        {
            // read the whole buffer
            std::copy(begin(), end(), first);

            // empty the buffer
            m_buffer.clear();

            return size();
        }
        else
        {
            // copy the data
            copy(begin(), begin() + bytes_to_read, firt);

            // now enque the element
            m_buffer.erase(begin(), begon() + bytes_to_read);

            return bytes_to_read;
        }
    }

private:
    void put(uint8_t data)
    {
        // check buffer overflow
        m_buffer.push_back(data);
    }

    const_iterator begin() const { return m_buffer.begin(); }
    const_iterator end() const { return m_buffer.end(); }

private:
    std::deque<uint8_t> m_buffer;           // buffer where store data
    size_t m_size;                          // effective size of the container
};

class ISR {
public:
    ISR(RXBuffer& buffer) : m_buffer(buffer) {}

    // ISR Routine
    void operator () (uint8_t data)
    {
        m_buffer.put(data);
    } 

private:
    RXBuffer& m_buffer;
};
RXBuffer g_uart1_rx_buffer;

Now you have the ISR and a RXBuffer where search data, so you need something to wrap the UART functions. You can implement as follow:

class UART {
public:
    UART(unsigned int uart_device, RXBuffer& rx_buffer) :
        m_uart(uart_device), m_buffer(rx_buffer)
    {
    }

    unsigned int uart_device() const { return m_uart; }

    // set the timeout during a read operation
    void timeout(unsigned ms) { m_timer.countdown(ms); }

    template <typename InputIterator>
    void read(InputIterator first, InputIterator last)
    {
        // start the timer
        m_timer.start();

        size_t size = std::distance(first, last);
        size_t read_bytes = 0;

        while (read_bytes != size && !m_timer.is_expired())
        {
            read_bytes += m_buffer.read(first + read_bytes, last);
        }

        if (read_bytes != size) throw std::exception("timeout");
    }

    template <typename OutputIterator>
    void send(OutputIterator first, OutputIterator last)
    {
        size_t size = std::distance(first, last);
        uart_send(m_uart, &(*first), size);
    }

private:
    unsigned int m_uart;
    RXBuffer& m_buffer;
    timer m_timer;
};
Elvis Dukaj
  • 7,142
  • 12
  • 43
  • 85