ISR выполняется дважды, если данные поступают через SoftwareSerial
Я использую Arduino Pro Mini для получения информации GPS с чипа u-blox NEO-M8N и отображения ее на OLED-дисплее Adafruit. NEO-M8N имеет выход PPS (импульс в секунду), который согласован с секундой GPS. Поскольку UART и десериализация медленные, я использую сигнал PPS, чтобы сделать отображение времени более точным. Сигнал PPS (импульс времени) и сообщение UART приходят следующим образом:

На моем Arduino SoftwareSerial RX/TX находится на выводе 7/8 (но используется только вывод 7, поскольку Arduino не имеет сообщения для отправки в NEO-M8N), а прерывание (PPS) находится на выводе 2. Если я просто подключу вывод 7, дисплей будет обновляться раз в секунду, как и предполагалось; если я просто подключу вывод 2, время увеличится на одну секунду в секунду, как и предполагалось. НО если подключить их оба, ((время увеличится более чем на одну секунду в секунду) и ( processGPS() иногда потеряет синхронизацию с предстоящим сообщением UBX(формат сообщения u-blox))).
А вот и весь код:
#include <SoftwareSerial.h>
#include <Adafruit_SSD1306.h>
#define OLED_RESET 9
Adafruit_SSD1306 display(OLED_RESET);
#if (SSD1306_LCDHEIGHT != 64)
#error("Height incorrect, please fix Adafruit_SSD1306.h!");
#endif
// Подключите GPS RX/TX к контактам 7 и 8 Arduino
SoftwareSerial S_Serial = SoftwareSerial(7,8);
int intpin = 2;
volatile int state = LOW;
const unsigned char UBX_HEADER[] = { 0xB5, 0x62 };
struct NAV_PVT {
unsigned char cls;
unsigned char id;
unsigned short len;
unsigned long iTOW;
unsigned short year;
byte month;
byte day;
byte hour;
byte miniute;
byte sec;
unsigned char valid;
unsigned long tAcc;
long nano;
byte fixType;
unsigned char flag;
unsigned char flag2;
byte numSV;
long lon;
long lat;
long height;
long hMSL;
unsigned long hAcc;
unsigned long vAcc;
long velN;
long velE;
long velD;
long gSpeed;
long headMot;
unsigned long sAcc;
unsigned long headAcc;
unsigned short pDop;
byte reserved1[6];
long headVeh;
byte reserved2[4];
};
NAV_PVT pvt;
void calcChecksum(unsigned char* CK) {
memset(CK, 0, 2);
for (int i = 0; i < (int)sizeof(NAV_PVT); i++) {
CK[0] += ((unsigned char*)(&pvt))[i];
CK[1] += CK[0];
}
}
bool processGPS() {
static int fpos = 0;
static unsigned char checksum[2];
const int payloadSize = sizeof(NAV_PVT);
while ( S_Serial.available() ) {
byte c = S_Serial.read();
if ( fpos < 2 ) { //СИНХРОНИЗАЦИЯ
if ( c == UBX_HEADER[fpos] )
fpos++;
else
fpos = 0;
}
else {
if ( (fpos-2) < payloadSize )
((unsigned char*)(&pvt))[fpos-2] = c;
fpos++;
if ( fpos == (payloadSize+2) ) {
calcChecksum(checksum);
}
else if ( fpos == (payloadSize+3) ) {
if ( c != checksum[0] )
fpos = 0;
}
else if ( fpos == (payloadSize+4) ) {
fpos = 0;
if ( c == checksum[1] ) {
return true;
}
}
else if ( fpos > (payloadSize+4) ) {
fpos = 0;
}
}
}
return false;
}
void pps()
{
state=!state;
}
void PrintScreen()
{
display.clearDisplay();
//Размер 2
display.setTextSize(2);
display.setTextColor(WHITE);
display.setCursor(0,0);
float sik = (pvt.gSpeed / 100) / 3.6f;
if (sik<100)
display.print(" ");
if (sik<10)
display.print(" ");
display.print(sik,1);//СкоростьВКф
display.setCursor(84,0);
if (pvt.numSV<10)
display.print(" ");
display.print(pvt.numSV);//SV
//Размер 1
display.setTextSize(1);
display.setCursor(60,8);
display.print("km/h");
display.setCursor(63,0);
display.print("CHL");
display.setCursor(108,8);
display.print("SVs");
display.setCursor(108,0);
switch (pvt.fixType){
case 0:
display.print("N.F");
break;
case 2:
display.print("2D");
break;
case 3:
display.print("3D");
if ((byte)pvt.flag & 2)
display.print("+");
break;
default:
display.print("INV");
break;
}
long temp;
//Строка 1
display.setCursor(0,16);
if (pvt.lat >= 0)
temp = pvt.lat;
else
temp = -pvt.lat;
if (temp >= 1000000000)
display.print("lat:");
else
if (temp >= 100000000)
display.print("lat: ");
else
display.print("lat: ");
display.print(temp / 10000000);display.print(".");
long temp2 = 1000000;
temp = temp % 10000000;
while ((temp / temp2 == 0)&&(temp2>1))
{
display.print("0");
temp2 /= 10;
}
display.print(temp);
if (pvt.lat >= 0)
display.print("N");
else
display.print("S");
temp = 10000;
while (((pvt.pDop / temp) == 0)&&(temp>1))
{
display.print(" ");
temp /= 10;
}
display.println(pvt.pDop);
//Строка2
if (pvt.lon >= 0)
temp = pvt.lon;
else
temp = -pvt.lon;
if (temp >= 1000000000)
display.print("lon:");
else
if (temp >= 100000000)
display.print("lon: ");
else
display.print("lon: ");
display.print(temp / 10000000);display.print(".");
temp2 = 1000000;
temp = temp % 10000000;
while ((temp / temp2 == 0)&&(temp2>1))
{
display.print("0");
temp2 /= 10;
}
display.print(temp);
if (pvt.lon >= 0)
display.print("E");
else
display.print("W");
display.println(" HEAD");
//Строка3
display.print("hMSL:");
if (pvt.hMSL < 0)
pvt.hMSL = -pvt.hMSL;
temp = 10000000;
while (((pvt.hMSL / temp) == 0)&&(temp>1000))
{
display.print(" ");
temp /= 10;
}
display.print((pvt.hMSL % 100000000) / 1000);display.print(".");
if ((pvt.hMSL % 1000) < 100)
display.print("0");
if ((pvt.hMSL % 1000) < 10)
display.print("0");
display.print(pvt.hMSL % 1000);
display.print("|");
temp = pvt.headMot / 100000;
if (temp < 100)
display.print(" ");
if (temp < 10)
display.print(" ");
display.print(temp);display.print(".");
temp2 = (pvt.headMot%100000)/1000;
if (temp2<0)
temp2 = -temp2;
if (temp2<10)
display.print("0");
display.println(temp2);
//Строка4
display.print("h,vAcc:");
if (pvt.hAcc >= 100000)
display.print(" >100");
else
{
display.print(pvt.hAcc / 1000);display.print(".");
if (pvt.hAcc % 1000 < 100)
display.print("0");
if (pvt.hAcc % 1000 < 10)
display.print("0");
display.print(pvt.hAcc % 1000);
}
display.print(",");
if (pvt.vAcc >= 100000)
display.print(" >100");
else
{
display.print(pvt.vAcc / 1000);display.print(".");
if (pvt.vAcc%1000 < 100)
display.print("0");
if (pvt.vAcc%1000 < 10)
display.print("0");
display.print(pvt.vAcc%1000);
}
display.println("m");
//Строка5
display.print("V:");
if (pvt.gSpeed < 1000000)
display.print(" ");
if (pvt.gSpeed < 100000)
display.print(" ");
if (pvt.gSpeed < 10000)
display.print(" ");
display.print((pvt.gSpeed % 10000000)/1000);display.print(".");
if (pvt.gSpeed % 1000 < 100)
display.print("0");
if (pvt.gSpeed % 1000 < 10)
display.print("0");
display.print(pvt.gSpeed % 1000);
display.print("m/s");
if (pvt.sAcc < 1000000)
{
if (pvt.sAcc < 100000)
display.print(" ");
if (pvt.sAcc < 10000)
display.print(" ");
display.print("~");
display.print(pvt.sAcc / 1000);display.print(".");
if (pvt.sAcc % 1000 < 100)
display.print("0");
if (pvt.sAcc % 1000 < 10)
display.print("0");
display.println(pvt.sAcc % 1000);
}
else
display.println(" ~>100");
//Строка6
if ((pvt.year<10000)&&(pvt.year>=1000))
display.print(pvt.year);
else
display.print("????");
display.print("/");
if ((pvt.month<=12)&&(pvt.month>=1))
{
if (pvt.month < 10)
display.print("0");
display.print(pvt.month);
}
else
display.print("??");
display.print("/");
if ((pvt.day<=31)&&(pvt.day>=1))
{
if (pvt.day < 10)
display.print("0");
display.print(pvt.day);
}
else
display.print("??");
if (pvt.valid & 0x04)
display.print("UTC");
else
display.print("INV");
if ((pvt.hour<=23)&&(pvt.hour>=0))
{
if (pvt.hour < 10)
display.print("0");
display.print(pvt.hour);
}
else
display.print("??");
display.print(":");
if ((pvt.miniute<=59)&&(pvt.miniute>=0))
{
if (pvt.miniute < 10)
display.print("0");
display.print(pvt.miniute);
}
else
display.print("??");
display.print(":");
if ((pvt.sec<=60)&&(pvt.sec>=0))
{
if (pvt.sec < 10)
display.print("0");
display.print(pvt.sec);
}
else
display.print("??");
display.display();
}
void PrintSec()
{
pvt.sec = (pvt.sec + 1)%60;
display.fillRect(114,56,12,8,BLACK);
display.setCursor(114,56);
if (pvt.sec<10)
display.print("0");
display.println(pvt.sec);
display.display();
state = LOW;
}
void setup()
{
S_Serial.begin(9600);
pinMode(intpin, INPUT);
attachInterrupt(digitalPinToInterrupt(intpin), pps, RISING);
// по умолчанию мы будем генерировать высокое напряжение из линии 3,3 В внутри! (здорово!)
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
// инициализация выполнена
}
void loop() {
if (state == HIGH)
PrintSec();
if ( processGPS() )
PrintScreen();
}
Я обнаружил похожую проблему здесь (Процедура обслуживания прерываний выполняется дважды ATmega88), но поскольку ISR выполняется только один раз, если я подключаю только контакт 2, я думаю, что причина может быть в чем-то другом.
Это проблема программного обеспечения или оборудования? Как ее исправить?
@HL. Chen, 👍2
Обсуждение2 ответа
Если я правильно вас понял, у вас есть 2 способа увеличить количество секунд,
когда программный последовательный порт получает данные
Когда вывод PPM становится высоким
И когда каждый из них запускается по отдельности, все работает, но когда вы используете их вместе, секунды увеличиваются на 2 каждый раз?
Не означает ли это, что ваша программа должна использовать только один из них для увеличения секунд?
Нет, у меня есть только один способ увеличить количество секунд: когда вывод PPS переходит в состояние высокого уровня. Я ПОЛУЧАЮ секунды из последовательного порта. То есть, когда вывод PPS переходит в состояние высокого уровня, секунды++; когда поступают последовательные данные, секунды = секунды_от_GPS., @HL. Chen
Возможно, показ вашего полного кода прояснит нам, что происходит., @Hans Neve
Хорошо, я только что отредактировал., @HL. Chen
Вы уверены в этой строке: pvt.sec = (pvt.sec + 1)%60;, @Hans Neve
Увеличивает секунду на единицу. Используйте pvt.sec вместо локальной переменной для экономии места., @HL. Chen
1) Во-первых, откладывание PrintSec для цикла через изменчивое состояние, которое устанавливается ISR, на самом деле занимает больше времени для обновления дисплея. Вы могли бы также просто проверить пин в цикле:
void setup()
{
S_Serial.begin(9600);
pinMode(intpin, INPUT);
// по умолчанию мы будем генерировать высокое напряжение из линии 3,3 В внутри! (здорово!)
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
}
void loop() {
bool state = digitalRead(intPin);
static bool oldState = false;
// Следите за изменениями
if (oldState != state) {
oldState = state;
// ПОДЪЕМНЫЙ край?
if (state)
PrintSec();
}
if ( processGPS() )
PrintScreen();
}
2) «Но я могу пропустить PPS!» — воскликните вы. Плохая новость в том, что если вы не можете закончить
PrintScreen вовремя, чтобы вернуться к тестированию PIN-кода, то вы пытаетесь сделать слишком много.
Возможно, вы захотите рассмотреть инкрементные обновления отображения. Если у вас есть предыдущие части (например, дополнительная структура NAV_PVT), вы можете сравнить каждую часть, чтобы увидеть, изменилась ли она. Если изменилась, то можно потратить время на ее написание. Просто добавьте дополнительный тест к каждой части:
NAV_PVT pvt, prevPVT;
static bool firstTime = true;
.
.
.
void PrintScreen()
{
if (firstTime)
display.clearDisplay();
//Размер 2
display.setTextSize(2);
display.setTextColor(WHITE);
// Обновить скорость?
if (firstTime || (pvt.gSpeed != prevPVT.gSpeed)) {
display.setCursor(0,0);
float sik = (pvt.gSpeed / 100) / 3.6f;
if (sik<100)
display.print(" ");
if (sik<10)
display.print(" ");
display.print(sik,1);//СкоростьВКф
}
В конце PrintScreen сделайте следующее:
display.display();
prevPVT = pvt;
firstTime = false;
}
Для большинства обновлений будет записано только поле секунд.
Очистка экрана и отрисовка меток, вероятно, может выполняться только один раз в настройке.
3) Я бы предложил более эффективную и надежную программную последовательную библиотеку.
SoftwareSerial отключает все прерывания на все время 10-битного символа после получения стартового бита. Это делает его уязвимым и навязчивым для других прерываний, таких как PinChange и SPI. Ужас. Если вы можете использовать контакты 8 и 9, AltSoftSerial — лучший выбор.
Если вы не можете использовать эти контакты и не используете порт USB для подключения к хосту, можете ли вы использовать последовательный вход (контакт 0) для GPS? GPS придется отключить для программирования USB, но он может оставаться подключенным, если вы используете ISP.
4) Вам нужен более надежный способ перезапустить кадрирование пакетов UBX, даже если символы потеряны. Я бы предложил что-то вроде этого:
void loop() {
if (state == HIGH) {
PrintSec();
fpos = 0; // скоро появится новый пакет, отбрасываем старый частичный пакет
}
if ( processGPS() )
PrintScreen();
}
По крайней мере, это позволит вам оставаться в синхронизации. Без этого завершение пакета может не произойти до тех пор, пока не произойдет 2-й PPS (и не начнет прибывать 2-й пакет). Может быть, это объясняет симптомы, которые вы видите?
Сначала я попробовал 3), и аппаратный последовательный ввод работает гораздо лучше и не теряет символы. Но при этом второй порт постоянно увеличивался дважды в секунду. Вы правы, PrintSec был отложен в цикл, поэтому использование прерывания бессмысленно. Затем я попробовал 1), но второй порт всё равно иногда увеличивался дважды. Я нашёл [эту статью](http://gammon.com.au/interrupts), в которой говорится, что «Pin Change Interrupt Request 2 (pins D0 to D7)» использует тот же вектор прерывания, поэтому я использую для получения PPS вывод 12. Теперь второй порт будет увеличиваться дважды только каждые 3 минуты., @HL. Chen
- Как остановить SoftwareSerial от получения данных и повторно включить его в какой-то другой момент?
- Почему необходимо использовать ключевое слово volatile для глобальных переменных при обработке прерываний в ардуино?
- Серийное прерывание
- Влияет ли `millis()` на длинные ISR?
- Как прервать функцию цикла и перезапустить ее?
- Отправка команд PUBX на плату GPS (Ublox NEO-6M) через SoftwareSerial
- Мусор последовательного монитора Arduino Pro Mini
- Аппаратное прерывание срабатывает случайным образом
Как вы определяете, что время увеличивается более чем на секунду за секунду? Вы показываете какой-нибудь последовательный вывод, который вы получаете на своём ПК?, @Gerben
Я показываю время на OLED-дисплее, [выглядит так](http://i.imgur.com/9flJIhi.jpg), и секунды иногда увеличивались вдвое за секунду, а остальная информация на дисплее просто замирала. Обычно долгота (широта), широта (долгота) и другие данные о местоположении постоянно дергаются, поэтому сразу видно, когда что-то идёт не так., @HL. Chen
Обработчик прерывания просто не будет вызван дважды из одного прерывания. Однако ваш код достаточно сложен, чтобы наблюдаемое вами стало возможным. Например, если
stateимеет высокий уровень, вы выводите секунды, а затем считываете данные GPS (который также выводит секунды). Это вполне может привести к двойному обновлению секунд., @Nick Gammon