Trying software serial output using interrupts

I would like to use an Arduino as a protocol converter. The PC side is 9600 serial, the other side is 5 bit baudot at low speeds. The output side is between 50 and 300, possibly 600 baud.

The output side not only outputs the data but must also read back data at the same rate. The read back is either a similar time to the output, or could be timed differently.

I cannot use software serial for the input, because I believe it waits in the read method, so I am liable to drop computer characters.

So I am trying to drive the output and input by interrupt.

At the moment the output seems to be working, but I have timing issues with the input. Some characters are OK and some are not. It gets worse at the higher speeds.

Also I must be doing something wrong because I cannot use digitalWrite or digitalRead in the interrupt routine. I don’t know why this does not work?

I am using millis() in one of the routines called by the polling loop, is that allowable with an interrupt routine?

The complete sketch is long, so I am posting the interrupt code, setup() and loop(). There are other routines, but no interrupt ones.

Using 0012 on linux, duemilanove.

My current test set up just has the output pin wired back to the input pin, at the TTL level, so I am trying to receive the sent characters back through the interrupt serial input.

The interrupt design is:
TIMER2 set to trigger interrupts on timer overflow at a multiple of half a bit time.

When an interrupt occurs, the first thing to do is reset the timer so the next interrupt will occur at the next half bit time.

The output side counts interrupts, and at a bit time occurs, shifts and outputs the data. A flag mechanism tells the interrupt routine to start.

The input side counts interrupts, and when a half bit time occurs, samples the input pin, making the value available, again using a flag, to a polling receive routine.

The idea is that the interrupt routines only occur about once per millisecond and do not take a lot of processing. The main loop must run faster than one half a bit time.

/* software output, reading data to be output from serial host in.
 Also software input to read back the data

#include <avr/interrupt.h>
#include <avr/io.h>

#define DEBUG 1

// see scaling spreadsheet, set for  50 baud, 64 prescaler, txmult=24,rxmult=12
// see scaling spreadsheet, set for 150 baud, 64 prescaler, txmult=8, rxmult=4
// see scaling spreadsheet, set for 300 baud, 64 prescaler, txmult=4, rxmult=2
#define SW_TXMULT 24

#define INIT_TIMER_COUNT 48  // see scaling spreadsheet for value

volatile int sw_tx_mult_counter = 0;
volatile int sw_rx_mult_counter = 0;
const int sw_tx_mult_constant = SW_TXMULT;
const int sw_rx_mult_constant = SW_RXMULT;

// used for signalling to interrupt routine for output
volatile int int_out_rdy = 0;
volatile int int_out_data;
volatile int int_out_bits;

// used for signalling from interrupt for input
volatile int int_in_read_pin;
volatile int int_in_rxtimer_gone = 0;

void setup_interrupt()
  //Timer2 Settings: Timer Prescaler /64  CS22 only
  TCCR2B |=  (1<<CS22);
  TCCR2B &= ~((1<<CS21) | (1<<CS20));
  TCCR2B &= ~(1<<WGM22);

  // Use normal mode
  TCCR2A &= ~((1<<WGM21) | (1<<WGM20));
  // Use internal clock - external clock not used in Arduino
  ASSR &= ~(1<<AS2);
  //Timer2 Overflow Interrupt Enable, clear the two output compare interrupt enables
  TIMSK2 |= (1<<TOIE2);
  TIMSK2 &= ~((1<<OCIE2A) | (1<<OCIE2B));

// Arduino runs at 16 Mhz,
// the prescaler is set to 64
// 1/ ((16000000 / 64) / 256) = 1 / 10000
ISR(TIMER2_OVF_vect) {
  if (sw_tx_mult_counter++ >= sw_tx_mult_constant) {
    sw_tx_mult_counter = 0;
    if (int_out_rdy)
      if (int_out_bits--)
        if (int_out_data & 0x40)
          //digitalWrite(doutPin, 1);
          PORTB |= 0x20;// corresponds to Arduino Pun 13
          //digitalWrite(doutPin, 0);
          PORTB &= ~(0x20);
        int_out_data <<= 1;
        int_out_rdy = 0;
  if (sw_rx_mult_counter++ >= sw_rx_mult_constant) {
    sw_rx_mult_counter = 0;
    int_in_read_pin = PINB & 0x01;// why does digitalRead not work in here?
    //int_in_read_pin = digitalRead(rdbackPin);
    int_in_rxtimer_gone = 1;  //to be picked up by loop routine

// more conversion code and buffering omitted

/* Class Definition
 Reads data in for PLBO or reception purposes
class OP_DataIn {
  volatile int dxbit;
  volatile char c_data;
  volatile char o_data;
  volatile int completed;

  void reset_state(void);

  char ca[40];
  volatile int cnt;

  int process(void); 
  char get_read_char(void);

  void put_dbg(int x);
  char *get_dbg();

}; // end class definition

inline void OP_DataIn::put_dbg(int x) { 
    if (cnt >= sizeof(ca)) return;
    ca[cnt++] = (x==1) ? '1' : '0'; 
    ca[cnt] = '0'; 
inline char *OP_DataIn::get_dbg() { 
    return ca; 

OP_DataIn::OP_DataIn() :

inline void OP_DataIn::reset_state(void) {
  dxbit = 0;
  c_data = 0;
  cnt=0; // resets the debug buffer counter 

inline int OP_DataIn::process(void) {
  int j = int_in_read_pin;

  if (int_in_rxtimer_gone) {
    int_in_rxtimer_gone = 0;
    if (dxbit < 0 || dxbit >14) {
      Serial.print("dxbit err "); 
    switch (dxbit) {
    case 0:
      // look for a transition for a start bit
      if (j == 0) {
    case 1:
      // if still zero, a good transition, else reset
      if (j == 0) {
      else {
    case 3:
    case 5:
    case 7:
    case 9:
    case 11:
    case 12:
      dxbit ++;

    case 2: // bit 1
    case 4: // 2
    case 6: // 3
    case 8: // 4
    case 10: //5
      dxbit ++;
      if (j) 
        c_data |= 0x01;
      c_data <<= 1;

    case 13:  // stop bit
      if (j != 0) {
        o_data = c_data >> 1;   
      completed = 1;

      Serial.print("dx err2");
      completed = 1;
    } // end switch
  } // end if timer gone

  return completed;
} // class member

inline char OP_DataIn::get_read_char(void)
  completed = 0;
  Serial.print(get_dbg()); Serial.print(" ");
  return o_data;

/*  Class Definition
 serial output, 1 start, 5 data and 1 stop
 *  Written so that it can be used in a loop
class OP_DataOut {

  OP_DataOut() {
  void start(char _cdata);
  inline int is_ready(void) { // indicates the output ready to do the next char 
    return (int_out_rdy == 0); 
}; // end class definition

inline void OP_DataOut::start(char _cdata) 
  _cdata &= 0x1f;     // make sure its only 5 bits
  int_out_data = 0;   // clear all bits to ensure start bit 
  int_out_data = (_cdata << 1) | 0x01;  // put stop bit on at end
  int_out_bits = 7;   // if you change the figure, change the mask in the interrupt routine too
  int_out_rdy = 1;    // set flag for interrupt routine to start output

// end DataOut functions

OP_DataOut op_datout;
OP_DataIn  op_datain;
RingBuffer ringbuffer;
Do_Control_Lines control_lines;
ConvertAlphabet convertalphabet;
ConvertIA2_IA5 c2_to_5; 

void setup()                    // run once, when the sketch starts
  pinMode(rdbackPin, INPUT);
  pinMode(doutPin, OUTPUT);
  pinMode(dtrPin,  INPUT);
  pinMode(dsrPin,  OUTPUT);
  pinMode(rtsPin,  INPUT);
  pinMode(ctsPin,  OUTPUT);
  digitalWrite(doutPin, HIGH);

void loop()                     // run over and over again
  if (Serial.available()) {
  if (convertalphabet.is_ready_for_put() && ringbuffer.current_contents() > 0) {
  if (op_datout.is_ready() && convertalphabet.is_ready_for_get()) {

  if (op_datain.process()) {
      char c = op_datain.get_read_char();
      if (c2_to_5.convert(c)) 
       char c1 = c2_to_5.get_result();
} // end loop

The point about the baudot code (actually it should be called Murry code after the Englishman who actually invented it not the American who invented something diffrent and then stuck his name to Murry's invention) is that it has one and a half bits stop bit.

A digitalWrite takes too long to perform (about 2mS) so that is why it will not work on the input side. You have to use direct port accessing. You have to synchronise the generation of the sampling interrupts on the input side to the falling edge of the start bit. Then have the ISR kick in every 1 baud period from half a baud period after this.

You can use millis() in an ISR but it will not get updated while it is in the routine. You will have to watch also for buffer overflow as there is nothing restraining the 9600 baud from the PC side. Do you need the input and output of the low speed to occur simultaneously? If so I would use an I2C UART instead of flaky software.

shelleycat, I have some code to do software serial using timer interrupts. A brief glance at your code makes it clear you know what you're doing, but you may want to check it out to mine it for ideas. It is based off of NewSoftSerial.

Some notes:

It uses pin change interrupts to detect the start bits.

It uses Timer1 and Timer2 for read and write. It runs them in CTC mode so that they auto-reset. Pin change interrupts are used to detect the start bits.

Timer interrupt flags get set even when the interrupts are not enabled, so you have to clear them appropriately to avoid getting spurious interrupts.

digitalWrite() and digitalRead() do some messing about with turning off PWM if called on a PWM pin, which seems to screw things up. I defined my own "lite" versions which don't bother with the PWM stuff.

A digitalWrite takes too long to perform (about 2mS)

digitalWrite() is slow (20x slower than direct IO, by my measurements), but it's NOT 2ms slow! (or did you mean microseconds. I got "about 5 microseconds" on the slower pins (those with PWM possible.) About 3.5 microseconds on regular pins... Slower than you'd LIKE for serial interrupts (especially at 9600bps, when you've only got about 100us per bit.)

Ah memories! My first hardware project out of college (I was mostly a SW engineer) was a Baudout converter (well, actually 75bps 6-bit newswire code to 300bps 8bit), but I only had to do the "easy" directions, so I got by with a couple of UARTS, some SSI logic, and a 555... (Microcontrollers not existing at the time, more or less.))

westfw: Thanks for the information, I’ve read the link. Looks like it should be fast enough even with digitalWrite, but I will stick with port io for now. What is newswire feed? I know of something that is a 6 bit signalling, where the first bit is ltrs/figs - is it like that?

jin: Thank you. Checking your code now.

GrumpyMike: Thank you. The devices I am interfacing with are only 1 stop bit. I should really have said IA2 rather than baudot. The input and output can be simultaneous (as in my test) but may be delayed by other hardware. If it was really known to be simultaneous I would just read the input as soon as the output is set - but the extra hardware has a delay.

What is newswire feed?

It is (was) the service that news services (AP, NYT, Dow Jones) used to broadcast late-breaking news to radio stations and such. Some used Baudot (Murray), but the particular one I worked with used a 6-bit code with Baudot-like SHIFT characters so that it could do more punctuation and such.

In the late 70s, early 80s, researchers we getting the idea that the data could be fed into a computer instead of just a special printer that someone read. Once the data was in the computer, you could do fancy searches on it, use the text for natural language research, and stuff like that. Stanford University had a nice setup that read AP and NYT newswires into a searchable database. You could also set it up with boolean queries ("NYT:(ardunino+atmel)") and it would email you matching stories. It was neat stuff.

My first job out of college was to take the Stanford code and port it to a different operating system, different newswire, and different serial codes (supposedly for that "natural language research" by the AI group, but I think mostly because "this newswire stuff is cool and we want want too instead of having to leach off of Stanford.") (all assembly language too. But oh, what a sweet assembly language it was. Sigh.)

"Computer" in this case means a small "mainframe" (DecSystem-10 at Stanford, DecSystem-20 for me.) Microcomputers of the day (pre IBM/PC) typically didn't have hard drives...

all assembly language too. But oh, what a sweet assembly language it was. Sigh.


Was that a VAX architecture? I’m not sure that VAX should really count as assembly language. :slight_smile: VAX-11 had “machine instructions” for string manipulation. My favorite was the single “instruction” to evaluate an nth degree polynomial.


I've put the digitalWrite and digitalRead back in. They work now I have updated the avr-gcc compiler to version 4.3.2.