Skip to content

Instantly share code, notes, and snippets.

@boop5
Created July 22, 2023 19:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save boop5/0cad063d281f181d31f84593eaffd12a to your computer and use it in GitHub Desktop.
Save boop5/0cad063d281f181d31f84593eaffd12a to your computer and use it in GitHub Desktop.
Modified SoftwareSerial library to avoid blocking interrupt pins
/*
SoftwareSerial.cpp (formerly NewSoftSerial.cpp) -
Multi-instance software serial library for Arduino/Wiring
-- Interrupt-driven receive and other improvements by ladyada
(http://ladyada.net)
-- Tuning, circular buffer, derivation from class Print/Stream,
multi-instance support, porting to 8MHz processors,
various optimizations, PROGMEM delay tables, inverse logic and
direct port writing by Mikal Hart (http://www.arduiniana.org)
-- Pin change interrupt macros by Paul Stoffregen (http://www.pjrc.com)
-- 20MHz processor support by Garrett Mace (http://www.macetech.com)
-- ATmega1280/2560 support by Brett Hagman (http://www.roguerobotics.com/)
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
The latest version of this library can always be found at
http://arduiniana.org.
*/
// When set, _DEBUG co-opts pins 11 and 13 for debugging with an
// oscilloscope or logic analyzer. Beware: it also slightly modifies
// the bit times, so don't rely on it too much at high baud rates
#define _DEBUG 0
#define _DEBUG_PIN1 11
#define _DEBUG_PIN2 13
//
// Includes
//
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <Arduino.h>
#include <LemonSerial.h>
#include <util/delay_basic.h>
//
// Statics
//
LemonSerial *LemonSerial::active_object = 0;
uint8_t LemonSerial::_receive_buffer[_SS_MAX_RX_BUFF];
volatile uint8_t LemonSerial::_receive_buffer_tail = 0;
volatile uint8_t LemonSerial::_receive_buffer_head = 0;
//
// Debugging
//
// This function generates a brief pulse
// for debugging or measuring on an oscilloscope.
#if _DEBUG
inline void DebugPulse(uint8_t pin, uint8_t count)
{
volatile uint8_t *pport = portOutputRegister(digitalPinToPort(pin));
uint8_t val = *pport;
while (count--)
{
*pport = val | digitalPinToBitMask(pin);
*pport = val;
}
}
#else
inline void DebugPulse(uint8_t, uint8_t) {}
#endif
//
// Private methods
//
/* static */
inline void LemonSerial::tunedDelay(uint16_t delay) {
_delay_loop_2(delay);
}
// This function sets the current object as the "listening"
// one and returns true if it replaces another
bool LemonSerial::listen()
{
if (!_rx_delay_stopbit)
return false;
if (active_object != this)
{
if (active_object)
active_object->stopListening();
_buffer_overflow = false;
_receive_buffer_head = _receive_buffer_tail = 0;
active_object = this;
setRxIntMsk(true);
return true;
}
return false;
}
// Stop listening. Returns true if we were actually listening.
bool LemonSerial::stopListening()
{
if (active_object == this)
{
setRxIntMsk(false);
active_object = NULL;
return true;
}
return false;
}
//
// The receive routine called by the interrupt handler
//
void LemonSerial::recv()
{
#if GCC_VERSION < 40302
// Work-around for avr-gcc 4.3.0 OSX version bug
// Preserve the registers that the compiler misses
// (courtesy of Arduino forum user *etracer*)
asm volatile(
"push r18 \n\t"
"push r19 \n\t"
"push r20 \n\t"
"push r21 \n\t"
"push r22 \n\t"
"push r23 \n\t"
"push r26 \n\t"
"push r27 \n\t"
::);
#endif
uint8_t d = 0;
// If RX line is high, then we don't see any start bit
// so interrupt is probably not for us
if (_inverse_logic ? rx_pin_read() : !rx_pin_read())
{
// Disable further interrupts during reception, this prevents
// triggering another interrupt directly after we return, which can
// cause problems at higher baudrates.
setRxIntMsk(false);
// Wait approximately 1/2 of a bit width to "center" the sample
tunedDelay(_rx_delay_centering);
DebugPulse(_DEBUG_PIN2, 1);
// Read each of the 8 bits
for (uint8_t i=8; i > 0; --i)
{
tunedDelay(_rx_delay_intrabit);
d >>= 1;
DebugPulse(_DEBUG_PIN2, 1);
if (rx_pin_read())
d |= 0x80;
}
if (_inverse_logic)
d = ~d;
// if buffer full, set the overflow flag and return
uint8_t next = (_receive_buffer_tail + 1) % _SS_MAX_RX_BUFF;
if (next != _receive_buffer_head)
{
// save new data in buffer: tail points to where byte goes
_receive_buffer[_receive_buffer_tail] = d; // save new byte
_receive_buffer_tail = next;
}
else
{
DebugPulse(_DEBUG_PIN1, 1);
_buffer_overflow = true;
}
// skip the stop bit
tunedDelay(_rx_delay_stopbit);
DebugPulse(_DEBUG_PIN1, 1);
// Re-enable interrupts when we're sure to be inside the stop bit
setRxIntMsk(true);
}
#if GCC_VERSION < 40302
// Work-around for avr-gcc 4.3.0 OSX version bug
// Restore the registers that the compiler misses
asm volatile(
"pop r27 \n\t"
"pop r26 \n\t"
"pop r23 \n\t"
"pop r22 \n\t"
"pop r21 \n\t"
"pop r20 \n\t"
"pop r19 \n\t"
"pop r18 \n\t"
::);
#endif
}
uint8_t LemonSerial::rx_pin_read()
{
return *_receivePortRegister & _receiveBitMask;
}
//
// Interrupt handling
//
// change here: removed the inline keyword
/* static */
void LemonSerial::handle_interrupt()
{
if (active_object)
{
active_object->recv();
}
}
// change here: commented out the ISR section
// #if defined(PCINT0_vect)
// ISR(PCINT0_vect)
// {
// LemonSerial::handle_interrupt();
// }
// #endif
// #if defined(PCINT1_vect)
// ISR(PCINT1_vect, ISR_ALIASOF(PCINT0_vect));
// #endif
// #if defined(PCINT2_vect)
// ISR(PCINT2_vect, ISR_ALIASOF(PCINT0_vect));
// #endif
// #if defined(PCINT3_vect)
// ISR(PCINT3_vect, ISR_ALIASOF(PCINT0_vect));
// #endif
//
// Constructor
//
LemonSerial::LemonSerial(uint8_t receivePin, uint8_t transmitPin, bool inverse_logic /* = false */) :
_rx_delay_centering(0),
_rx_delay_intrabit(0),
_rx_delay_stopbit(0),
_tx_delay(0),
_buffer_overflow(false),
_inverse_logic(inverse_logic)
{
setTX(transmitPin);
setRX(receivePin);
}
//
// Destructor
//
LemonSerial::~LemonSerial()
{
end();
}
void LemonSerial::setTX(uint8_t tx)
{
// First write, then set output. If we do this the other way around,
// the pin would be output low for a short while before switching to
// output high. Now, it is input with pullup for a short while, which
// is fine. With inverse logic, either order is fine.
digitalWrite(tx, _inverse_logic ? LOW : HIGH);
pinMode(tx, OUTPUT);
_transmitBitMask = digitalPinToBitMask(tx);
uint8_t port = digitalPinToPort(tx);
_transmitPortRegister = portOutputRegister(port);
}
void LemonSerial::setRX(uint8_t rx)
{
pinMode(rx, INPUT);
if (!_inverse_logic)
digitalWrite(rx, HIGH); // pullup for normal logic!
_receivePin = rx;
_receiveBitMask = digitalPinToBitMask(rx);
uint8_t port = digitalPinToPort(rx);
_receivePortRegister = portInputRegister(port);
}
uint16_t LemonSerial::subtract_cap(uint16_t num, uint16_t sub) {
if (num > sub)
return num - sub;
else
return 1;
}
//
// Public methods
//
void LemonSerial::begin(long speed)
{
_rx_delay_centering = _rx_delay_intrabit = _rx_delay_stopbit = _tx_delay = 0;
// Precalculate the various delays, in number of 4-cycle delays
uint16_t bit_delay = (F_CPU / speed) / 4;
// 12 (gcc 4.8.2) or 13 (gcc 4.3.2) cycles from start bit to first bit,
// 15 (gcc 4.8.2) or 16 (gcc 4.3.2) cycles between bits,
// 12 (gcc 4.8.2) or 14 (gcc 4.3.2) cycles from last bit to stop bit
// These are all close enough to just use 15 cycles, since the inter-bit
// timings are the most critical (deviations stack 8 times)
_tx_delay = subtract_cap(bit_delay, 15 / 4);
// Only setup rx when we have a valid PCINT for this pin
if (digitalPinToPCICR((int8_t)_receivePin)) {
#if GCC_VERSION > 40800
// Timings counted from gcc 4.8.2 output. This works up to 115200 on
// 16Mhz and 57600 on 8Mhz.
//
// When the start bit occurs, there are 3 or 4 cycles before the
// interrupt flag is set, 4 cycles before the PC is set to the right
// interrupt vector address and the old PC is pushed on the stack,
// and then 75 cycles of instructions (including the RJMP in the
// ISR vector table) until the first delay. After the delay, there
// are 17 more cycles until the pin value is read (excluding the
// delay in the loop).
// We want to have a total delay of 1.5 bit time. Inside the loop,
// we already wait for 1 bit time - 23 cycles, so here we wait for
// 0.5 bit time - (71 + 18 - 22) cycles.
_rx_delay_centering = subtract_cap(bit_delay / 2, (4 + 4 + 75 + 17 - 23) / 4);
// There are 23 cycles in each loop iteration (excluding the delay)
_rx_delay_intrabit = subtract_cap(bit_delay, 23 / 4);
// There are 37 cycles from the last bit read to the start of
// stopbit delay and 11 cycles from the delay until the interrupt
// mask is enabled again (which _must_ happen during the stopbit).
// This delay aims at 3/4 of a bit time, meaning the end of the
// delay will be at 1/4th of the stopbit. This allows some extra
// time for ISR cleanup, which makes 115200 baud at 16Mhz work more
// reliably
_rx_delay_stopbit = subtract_cap(bit_delay * 3 / 4, (37 + 11) / 4);
#else // Timings counted from gcc 4.3.2 output
// Note that this code is a _lot_ slower, mostly due to bad register
// allocation choices of gcc. This works up to 57600 on 16Mhz and
// 38400 on 8Mhz.
_rx_delay_centering = subtract_cap(bit_delay / 2, (4 + 4 + 97 + 29 - 11) / 4);
_rx_delay_intrabit = subtract_cap(bit_delay, 11 / 4);
_rx_delay_stopbit = subtract_cap(bit_delay * 3 / 4, (44 + 17) / 4);
#endif
// Enable the PCINT for the entire port here, but never disable it
// (others might also need it, so we disable the interrupt by using
// the per-pin PCMSK register).
*digitalPinToPCICR((int8_t)_receivePin) |= _BV(digitalPinToPCICRbit(_receivePin));
// Precalculate the pcint mask register and value, so setRxIntMask
// can be used inside the ISR without costing too much time.
_pcint_maskreg = digitalPinToPCMSK(_receivePin);
_pcint_maskvalue = _BV(digitalPinToPCMSKbit(_receivePin));
tunedDelay(_tx_delay); // if we were low this establishes the end
}
#if _DEBUG
pinMode(_DEBUG_PIN1, OUTPUT);
pinMode(_DEBUG_PIN2, OUTPUT);
#endif
listen();
}
void LemonSerial::setRxIntMsk(bool enable)
{
if (enable)
*_pcint_maskreg |= _pcint_maskvalue;
else
*_pcint_maskreg &= ~_pcint_maskvalue;
}
void LemonSerial::end()
{
stopListening();
}
// Read data from buffer
int LemonSerial::read()
{
if (!isListening())
return -1;
// Empty buffer?
if (_receive_buffer_head == _receive_buffer_tail)
return -1;
// Read from "head"
uint8_t d = _receive_buffer[_receive_buffer_head]; // grab next byte
_receive_buffer_head = (_receive_buffer_head + 1) % _SS_MAX_RX_BUFF;
return d;
}
int LemonSerial::available()
{
if (!isListening())
return 0;
return ((unsigned int)(_receive_buffer_tail + _SS_MAX_RX_BUFF - _receive_buffer_head)) % _SS_MAX_RX_BUFF;
}
size_t LemonSerial::write(uint8_t b)
{
if (_tx_delay == 0) {
setWriteError();
return 0;
}
// By declaring these as local variables, the compiler will put them
// in registers _before_ disabling interrupts and entering the
// critical timing sections below, which makes it a lot easier to
// verify the cycle timings
volatile uint8_t *reg = _transmitPortRegister;
uint8_t reg_mask = _transmitBitMask;
uint8_t inv_mask = ~_transmitBitMask;
uint8_t oldSREG = SREG;
bool inv = _inverse_logic;
uint16_t delay = _tx_delay;
if (inv)
b = ~b;
cli(); // turn off interrupts for a clean txmit
// Write the start bit
if (inv)
*reg |= reg_mask;
else
*reg &= inv_mask;
tunedDelay(delay);
// Write each of the 8 bits
for (uint8_t i = 8; i > 0; --i)
{
if (b & 1) // choose bit
*reg |= reg_mask; // send 1
else
*reg &= inv_mask; // send 0
tunedDelay(delay);
b >>= 1;
}
// restore pin to natural state
if (inv)
*reg &= inv_mask;
else
*reg |= reg_mask;
SREG = oldSREG; // turn interrupts back on
tunedDelay(_tx_delay);
return 1;
}
void LemonSerial::flush()
{
// There is no tx buffering, simply return
}
int LemonSerial::peek()
{
if (!isListening())
return -1;
// Empty buffer?
if (_receive_buffer_head == _receive_buffer_tail)
return -1;
// Read from "head"
return _receive_buffer[_receive_buffer_head];
}
/*
SoftwareSerial.h (formerly NewSoftSerial.h) -
Multi-instance software serial library for Arduino/Wiring
-- Interrupt-driven receive and other improvements by ladyada
(http://ladyada.net)
-- Tuning, circular buffer, derivation from class Print/Stream,
multi-instance support, porting to 8MHz processors,
various optimizations, PROGMEM delay tables, inverse logic and
direct port writing by Mikal Hart (http://www.arduiniana.org)
-- Pin change interrupt macros by Paul Stoffregen (http://www.pjrc.com)
-- 20MHz processor support by Garrett Mace (http://www.macetech.com)
-- ATmega1280/2560 support by Brett Hagman (http://www.roguerobotics.com/)
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
The latest version of this library can always be found at
http://arduiniana.org.
*/
#ifndef LemonSerial_h
#define LemonSerial_h
#include <inttypes.h>
#include <Stream.h>
/******************************************************************************
* Definitions
******************************************************************************/
#ifndef _SS_MAX_RX_BUFF
#define _SS_MAX_RX_BUFF 64 // RX buffer size
#endif
#ifndef GCC_VERSION
#define GCC_VERSION (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__)
#endif
class LemonSerial : public Stream
{
private:
// per object data
uint8_t _receivePin;
uint8_t _receiveBitMask;
volatile uint8_t *_receivePortRegister;
uint8_t _transmitBitMask;
volatile uint8_t *_transmitPortRegister;
volatile uint8_t *_pcint_maskreg;
uint8_t _pcint_maskvalue;
// Expressed as 4-cycle delays (must never be 0!)
uint16_t _rx_delay_centering;
uint16_t _rx_delay_intrabit;
uint16_t _rx_delay_stopbit;
uint16_t _tx_delay;
uint16_t _buffer_overflow:1;
uint16_t _inverse_logic:1;
// static data
static uint8_t _receive_buffer[_SS_MAX_RX_BUFF];
static volatile uint8_t _receive_buffer_tail;
static volatile uint8_t _receive_buffer_head;
static LemonSerial *active_object;
// private methods
inline void recv() __attribute__((__always_inline__));
uint8_t rx_pin_read();
void setTX(uint8_t transmitPin);
void setRX(uint8_t receivePin);
inline void setRxIntMsk(bool enable) __attribute__((__always_inline__));
// Return num - sub, or 1 if the result would be < 1
static uint16_t subtract_cap(uint16_t num, uint16_t sub);
// private static method for timing
static inline void tunedDelay(uint16_t delay);
public:
// public methods
LemonSerial(uint8_t receivePin, uint8_t transmitPin, bool inverse_logic = false);
~LemonSerial();
void begin(long speed);
bool listen();
void end();
bool isListening() { return this == active_object; }
bool stopListening();
bool overflow() { bool ret = _buffer_overflow; if (ret) _buffer_overflow = false; return ret; }
int peek();
virtual size_t write(uint8_t byte);
virtual int read();
virtual int available();
virtual void flush();
operator bool() { return true; }
using Print::write;
// change here: removed the inline keyword and always_inline attribute
// public only for easy access by interrupt handlers
static void handle_interrupt();
};
#endif
@boop5
Copy link
Author

boop5 commented Jul 22, 2023

Don't forget to call handle_interrupt from the outside

ISR(PCINT0_vect)
{
  LemonSerial::handle_interrupt();
}

@davoau
Copy link

davoau commented Nov 7, 2023

Hi, I am trying to use an ATTiny85 with serial communication and to an ESP8266. I want to use PinChangeInterrupt to monitor bucket tips on a rain gauge (has a reed switch) attached to pin 2 (PB3) on the ATTiny85, when the bucket tips, the ATTiny wakes up the ESP8255 and sends the data over serial - using pins PB0(tx) and PB1(rx).
Everything works apart from the monitoring of the interupt.

The cut-down code I have tried is:

#define INT_PINRAIN PB3
void setup() {
  pinMode(INT_PINRAIN, INPUT); 
  attachPCINT(digitalPinToPCINT(INT_PINRAIN), RAIN, FALLING);
}

void RAIN()
{
  if ((millis() - ContactBounceTimeRain) > 250 ) { // debounce the switch contact.
    PluvioFlag++;
    RainFlag = 1;
    ContactBounceTimeRain = millis();
    time1 = millis();
  }
}

however I get the following error:

PinChangeInterrupt.cpp.o (symbol from plugin): In function pcint_null_callback :
(.text+0x0): multiple definition of  __vector_2 
C:\Users\Default User.DESKTOP-UR53KBO\AppData\Local\Temp\arduino\sketches\A06F401CFE2202C1EFB8281DA88A3AC5\sketch\ATTiny_serial_send_20231107_2.ino.cpp.o (symbol from plugin):(.text+0x0): first defined here
collect2.exe: error: ld returned 1 exit status

Using library LemonSerial in folder: C:\Users\Default User.DESKTOP-UR53KBO\Documents\Arduino\libraries\LemonSerial (legacy)
Using library PinChangeInterrupt at version 1.2.9 in folder: C:\Users\Default User.DESKTOP-UR53KBO\Documents\Arduino\libraries\PinChangeInterrupt 
exit status 1

Compilation error: exit status 1

Do you know what I am doing wrong or have any example code I could look at.
Thanks
Davo

@boop5
Copy link
Author

boop5 commented Nov 8, 2023

are you using the arduino IDE or vscode with PIO?

look for PCINT2_vect definitions in your libraries. for some reason you have defined it more than once. that was the whole reason I modified the original SoftwareSerial library

I've commented the changes I made in the files above

// change here: commented out the ISR section
// #if defined(PCINT0_vect)
// ISR(PCINT0_vect)
// {
//   LemonSerial::handle_interrupt();
// }
// #endif

// #if defined(PCINT1_vect)
// ISR(PCINT1_vect, ISR_ALIASOF(PCINT0_vect));
// #endif

// #if defined(PCINT2_vect)
// ISR(PCINT2_vect, ISR_ALIASOF(PCINT0_vect));
// #endif

// #if defined(PCINT3_vect)
// ISR(PCINT3_vect, ISR_ALIASOF(PCINT0_vect));
// #endif

@davoau
Copy link

davoau commented Nov 9, 2023

Hi, Thanks for the quick reply.
I am using arduino IDE 2.2.0.

The only PCINT2_vect definitions I can find in pinchangeinterrupt library are..

PinChangeInterruptPins.h

#if defined(PCINT2_vect)
#define PCINT_HAS_PORT2 true
#else
#define PCINT_HAS_PORT2 false
#endif

PinChangeInterrupt2.cpp

ISR(PCINT2_vect) {
	// get the new and old pin states for port
	uint8_t newPort = PCINT_INPUT_PORT2;

	// compare with the old value to detect a rising or falling
	uint8_t arrayPos = getArrayPosPCINT(2);
	uint8_t change = newPort ^ oldPorts[arrayPos];
	uint8_t rising = change & newPort;
	uint8_t falling = change & oldPorts[arrayPos];

	// check which pins are triggered, compared with the settings
	uint8_t risingTrigger = rising & risingPorts[arrayPos];
	uint8_t fallingTrigger = falling & fallingPorts[arrayPos];
	uint8_t trigger = risingTrigger | fallingTrigger;

	// save the new state for next comparison
	oldPorts[arrayPos] = newPort;

	// Execute all functions that should be triggered
	// This way we can exclude a single function
	// and the calling is also much faster
	// We may also reorder the pins for different priority
#if !defined(PCINT_CALLBACK_PORT2)
	PCINT_CALLBACK(0, 16);
	PCINT_CALLBACK(1, 17);
	PCINT_CALLBACK(2, 18);
	PCINT_CALLBACK(3, 19);
	PCINT_CALLBACK(4, 20);
	PCINT_CALLBACK(5, 21);
	PCINT_CALLBACK(6, 22);
	PCINT_CALLBACK(7, 23);
#else
	PCINT_CALLBACK_PORT2
#endif
}

I dont really understand any of this.. any help would be great.
Thanks
Davo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment