mirror of
https://github.com/cosmic-pi-deprecated/cosmicpi-arduino_V1.6.git
synced 2026-05-12 16:09:25 +00:00
First upload - not functional
Compiles, but doesn't run. Left to do: Fix serial output Verify GPS Verify ADC + HV settings Swap accelerometer library Sort out timers (software for now) Confirm serial output format.
This commit is contained in:
parent
4e1f0ac098
commit
8282d0a0b1
8 changed files with 1076 additions and 0 deletions
55
asyncSerial.cpp
Normal file
55
asyncSerial.cpp
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#include "asyncSerial.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
|
||||
// Avoid being blocked in the serial write routine
|
||||
|
||||
AsyncSerial::AsyncSerial(int baudRate){
|
||||
Serial.begin(baudRate);
|
||||
txtw = 0;
|
||||
txtr = 0;
|
||||
tsze = 0;
|
||||
terr = 0;
|
||||
tmax = 0;
|
||||
}
|
||||
|
||||
// Copy text to the buffer for future printing
|
||||
void AsyncSerial::print(char *txt) {
|
||||
|
||||
int i, l = strlen(txt);
|
||||
|
||||
// If this happens there is a programming bug
|
||||
if (l > TBLEN) { // Can't handle more than TBLEN at a time
|
||||
terr = TXT_TOOBIG; // say error and abort
|
||||
return;
|
||||
}
|
||||
|
||||
// If the buffer is filling up to fast throw it away and return an error
|
||||
if ((l + tsze) >= TBLEN) { // If there is no room in the buffer
|
||||
terr = TXT_OVERFL; // Buffer overflow
|
||||
return; // Simply stop printing when txt comming too fast
|
||||
}
|
||||
|
||||
// Copy the new text onto the ring buffer for later output
|
||||
// from the loop idle function
|
||||
for (i=0; i<l; i++) {
|
||||
txtb[txtw] = txt[i]; // Put char in the buffer and
|
||||
txtw = (txtw + 1) % TBLEN; // get the next write pointer modulo TBLEN
|
||||
}
|
||||
tsze = (tsze + l) % TBLEN; // new buffer size
|
||||
if (tsze > tmax) tmax = tsze; // track the max size
|
||||
}
|
||||
|
||||
// Take the next character from the ring buffer and print it, called from the main loop
|
||||
|
||||
void AsyncSerial::PutChar() {
|
||||
char c[2]; // One character zero terminated string
|
||||
if ((tsze) && (!Serial.available())) { // If the buffer is not empty and not reading
|
||||
//if ((tsze)) { // If the buffer is not empty and not reading
|
||||
c[0] = txtb[txtr]; // Get the next character from the read pointer
|
||||
c[1] = '\0'; // Build a zero terminated string
|
||||
txtr = (txtr + 1) % TBLEN; // Get the next read pointer modulo TBLEN
|
||||
tsze = (tsze - 1) % TBLEN; // Reduce the buffer size
|
||||
Serial.print(c); // and print the character
|
||||
}
|
||||
}
|
||||
36
asyncSerial.h
Normal file
36
asyncSerial.h
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#ifndef __ASYNCSERIAL__
|
||||
#define __ASYNCSERIAL__
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// These two routines are needed because the Serial.print method prints without using interrupts.
|
||||
// Calls to Serial.print block interrupts and use a wait in kernel space causing all ISRs to
|
||||
// be blocked and hence we could miss some timer interrupts.
|
||||
// To avoid this problem call PushTxt to have stuff delivered to the serial line, PushTxt simply
|
||||
// stores your text for future print out by PutChar. The PutChar routine removes one character
|
||||
// from the stored text each time its called. By placing a call to PutChar in the outermost loop
|
||||
// of the Arduino loop function, then for each loop one character is printed, avoiding blocking
|
||||
// of interrupts and vastly improving the loops real time behaviour.
|
||||
|
||||
class AsyncSerial {
|
||||
// This is the text ring buffer for real time output to serial line with interrupt on
|
||||
static const int TBLEN = 1024; // Serial line output ring buffer size, 8K
|
||||
char txtb[TBLEN]; // Text ring buffer
|
||||
uint32_t txtw, txtr, // Write and Read indexes
|
||||
tsze, terr, // Buffer size and error code
|
||||
tmax; // The maximum size the buffer reached
|
||||
|
||||
typedef enum { TXT_NOERR=0, TXT_TOOBIG=1, TXT_OVERFL=2 } TxtErr;
|
||||
|
||||
public:
|
||||
AsyncSerial(int baudRate);
|
||||
// Copy text to the buffer for future printing
|
||||
void print(char *txt);
|
||||
// Take the next character from the ring buffer and print it, called from the main loop
|
||||
void PutChar();
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
#endif
|
||||
496
cosmicpi-arduino_V1.5.ino
Normal file
496
cosmicpi-arduino_V1.5.ino
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
//Cosmic Pi software for Arduino - modified for STM32,
|
||||
//J. Devine
|
||||
//July 2019.
|
||||
//Licensed under GPL V3 or later.
|
||||
//cosmicpi.org
|
||||
|
||||
//pinouts
|
||||
/*
|
||||
* PA0 - Pin 14 - Shaped Signal 1
|
||||
* PA1 - Pin 15 - Shaped Signal 2
|
||||
* PA2 - Pin 16 - TX0
|
||||
* PA3 - Pin 16 - RX0
|
||||
* PA4 - Pin 20 - LED1 - Power/GPS
|
||||
* PA5 - Pin 21 - LED2 - Event
|
||||
* PA6 - Pin 22 - Injection leds
|
||||
* PA7 - Pin 23 - Bias FB1
|
||||
* PA8 - Pin 41 - SCL_Slave
|
||||
* PA9 - Pin 42 - USB_OTG_VBUS
|
||||
* PA10 - Pin 43 - GPSTX
|
||||
* PA11 - Pin 44 - USBOTG DM
|
||||
* PA12 - Pin 45 - USBOTG DP
|
||||
* PA13 - Pin 46 - SWDIO
|
||||
* PA14 - Pin 49 - SWCLK
|
||||
* PA15 - Pin 50 - GPSPPS Input
|
||||
* PB0 - Pin 26 - Bias FB2
|
||||
* PB1 - Pin 27 - Flag to RPi
|
||||
* PB2 Pin 28 - NC
|
||||
* PB 3 - Pin 55 - NC
|
||||
* PB4 - Pin 56 - SDA_Slave
|
||||
* PB5 - Pin 57 - NC
|
||||
* PB6 - Pin 58 - GPSRX
|
||||
* PB7 - Pin 59 - SDA0
|
||||
* PB8 - Pin 61 - SCL0
|
||||
* PB9 - Pin 62 - NC
|
||||
* PB10 - Pin 29 - Trigout (input to STM)
|
||||
* PB12 - Pin 33 - NC
|
||||
* PB13 - Pin 34 0 HVPSU SCLK (Clock to MAX1932)
|
||||
* PB14, PB15 - NC
|
||||
* PC0 - Pin 8 - NC
|
||||
* PC1 - Pin 9 - HVPSU CL1
|
||||
* PC2 - Pin 10 - HVPSU CL2
|
||||
* PC3 - Pin 11 - HV PSU DIN
|
||||
* PC4, PC5, PC6- NC
|
||||
* PC7 - Pin 38 - HVPSU CS2
|
||||
* PC8 - Pin 39 - HVPSU CS1
|
||||
* PC9 - Pin 40 - Mag Interrupt
|
||||
* PC10 - Pin 51 - NC
|
||||
* PC11 - Pin 52 - STRIGOUT B
|
||||
* PC12 - Pin 53 - STRIGOUT A
|
||||
* PC13 - Pin 2 - Baro Int
|
||||
* PC14 - Pin 3 - Accelint 1
|
||||
* PC15 - Pin 4 - Accelint 2.
|
||||
|
||||
*/
|
||||
#include "asyncSerial.h"
|
||||
#include <Wire.h>
|
||||
#include <EEPROM.h>
|
||||
|
||||
static const int SERIAL_BAUD_RATE = 19200; // Serial baud rate for version 1.5 production
|
||||
static const int GPS_BAUD_RATE = 9600; // GPS and Serial1 line
|
||||
|
||||
// simulate events
|
||||
static const bool simulateEvents = false;
|
||||
unsigned long nextSimEvent = 0;
|
||||
|
||||
// enable gps and sensor output
|
||||
static const bool enableSensorOutput = true;
|
||||
static const bool enableGPSPipe = true;
|
||||
|
||||
// start our async serial connection for global use
|
||||
// it would normally work just fine as an instance
|
||||
// but I want to pass around the pointer to different classes
|
||||
// to make sure that only one instance is ever used
|
||||
AsyncSerial *aSer;
|
||||
|
||||
// sensors for global use
|
||||
#include "sensors.h"
|
||||
Sensors sensors(aSer);
|
||||
|
||||
// LED pins
|
||||
#define PPS_PIN PA4 // PPS (Pulse Per Second) and LED
|
||||
#define EVT_PIN PA5 // Cosmic ray event detected
|
||||
|
||||
// Leds flag
|
||||
bool leds_on = true;
|
||||
|
||||
// time to print a sensor update (in ms before a PPL)
|
||||
// the sensor update should always print inbetween the PPS, to avoid problems with the serial pipe from the GPS
|
||||
static unsigned long distanceSensorUpdatePPS = 200;
|
||||
unsigned long nextSensorUpdate = 0;
|
||||
|
||||
// How long the event LED should light up (in ms)
|
||||
static int event_LED_time = 15;
|
||||
|
||||
|
||||
// string used for passing data to our asynchronous serial print class
|
||||
static const int TXTLEN = 512;
|
||||
static char txt[TXTLEN]; // For writing to serial
|
||||
|
||||
static uint32_t displ = 0; // Display values in loop
|
||||
|
||||
// Timer registers REGA....
|
||||
static uint32_t rega1, stsr1 = 0;
|
||||
static uint32_t stsr2 = 0;
|
||||
|
||||
boolean pll_flag = false;
|
||||
boolean pll_pulse = false;
|
||||
|
||||
long eventCount = 0;
|
||||
unsigned long pps_micros = 0;
|
||||
unsigned long target_mills = millis() + 1030;
|
||||
unsigned long last_event_LED = 0;
|
||||
int eventstack = 0;
|
||||
#define maxevent 100 //we don't expect more events than this
|
||||
unsigned long evttime [maxevent];
|
||||
|
||||
|
||||
|
||||
#define FREQ 84000000 // Clock frequency
|
||||
#define MFRQ 80000000 // Sanity check frequency value
|
||||
|
||||
// Timer chip interrupt handlers try to get time stamps to within 4 system clock ticks
|
||||
static uint32_t rega0 = FREQ, // RA reg
|
||||
stsr0 = 0, // Interrupt status register
|
||||
ppcnt = 0, // PPS count
|
||||
delcn = 0; // Synthetic PPS ms
|
||||
|
||||
// GPS and time flags
|
||||
boolean gps_ok = false; // Chip OK flag
|
||||
boolean pps_recieved = false;
|
||||
|
||||
|
||||
|
||||
// ---------------------- Timing
|
||||
|
||||
// Initialize the timer chips to measure time between the PPS pulses and the EVENT pulse
|
||||
// The PPS enters pin D2, the PPS is forwarded accross an isolating diode to pin D5
|
||||
// The event pulse is also connected to pin D5. So D5 sees the LOR of the PPS and the
|
||||
// event, while D2 sees only the PPS. In this way we measure the frequency of the
|
||||
// clock MCLK/2 each second on the first counter, and the time between EVENTs on the second
|
||||
// I use a the unconnected timer block TC1 to make a PLL that is kept in phase by the PPS
|
||||
// arrival in TC0 and which is loaded with the last measured PPS frequency. This PLL will
|
||||
// take over the PPS generation if the real PPS goes missing.
|
||||
// In this implementation the diode is implemented in software, see later
|
||||
|
||||
|
||||
void TimersStart() {
|
||||
|
||||
uint32_t config = 0;
|
||||
|
||||
// Set up the power management controller for TC0 and TC2
|
||||
|
||||
/*
|
||||
* pmc_set_writeprotect(false); // Enable write access to power management chip
|
||||
pmc_enable_periph_clk(ID_TC0); // Turn on power for timer block 0 channel 0
|
||||
pmc_enable_periph_clk(ID_TC3); // Turn on power for timer block 1 channel 0
|
||||
pmc_enable_periph_clk(ID_TC6); // Turn on power for timer block 2 channel 0
|
||||
|
||||
// Timer block 0 channel 0 is connected only to the PPS
|
||||
// We set it up to load regester RA on each PPS and reset
|
||||
// So RA will contain the number of clock ticks between two PPS, this
|
||||
// value is the clock frequency and should be very stable +/- one tick
|
||||
|
||||
config = TC_CMR_TCCLKS_TIMER_CLOCK1 | // Select fast clock MCK/2 = 42 MHz
|
||||
TC_CMR_ETRGEDG_RISING | // External trigger rising edge on TIOA0
|
||||
TC_CMR_ABETRG | // Use the TIOA external input line
|
||||
TC_CMR_LDRA_RISING; // Latch counter value into RA
|
||||
|
||||
TC_Configure(TC0, 0, config); // Configure channel 0 of TC0
|
||||
TC_Start(TC0, 0); // Start timer running
|
||||
|
||||
TC0->TC_CHANNEL[0].TC_IER = TC_IER_LDRAS; // Enable the load AR channel 0 interrupt each PPS
|
||||
TC0->TC_CHANNEL[0].TC_IDR = ~TC_IER_LDRAS; // and disable the rest of the interrupt sources
|
||||
NVIC_EnableIRQ(TC0_IRQn); // Enable interrupt handler for channel 0
|
||||
|
||||
// Timer block 1 channel 0 is the PLL for when the GPS chip isn't providing the PPS
|
||||
// it has the frequency loaded in reg C and is triggered from the TC0 ISR
|
||||
|
||||
config = TC_CMR_TCCLKS_TIMER_CLOCK1 | // Select fast clock MCK/2 = 42 MHz
|
||||
TC_CMR_CPCTRG; // Compare register C with count value
|
||||
|
||||
TC_Configure(TC1, 0, config); // Configure channel 0 of TC1
|
||||
TC_SetRC(TC1, 0, FREQ); // One second approx initial PLL value
|
||||
TC_Start(TC1, 0); // Start timer running
|
||||
|
||||
TC1->TC_CHANNEL[0].TC_IER = TC_IER_CPCS; // Enable the C register compare interrupt
|
||||
TC1->TC_CHANNEL[0].TC_IDR = ~TC_IER_CPCS; // and disable the rest
|
||||
NVIC_EnableIRQ(TC3_IRQn); // Enable interrupt handler for channel 0
|
||||
|
||||
// Timer block 2 channel 0 is connected to the RAY event
|
||||
// It is kept in phase by the PPS comming from TC0 when the PPS arrives
|
||||
// or from TC1 when the PLL is active (This is the so called software diode logic)
|
||||
|
||||
config = TC_CMR_TCCLKS_TIMER_CLOCK1 | // Select fast clock MCK/2 = 42 MHz
|
||||
TC_CMR_ETRGEDG_RISING | // External trigger rising edge on TIOA1
|
||||
TC_CMR_ABETRG | // Use the TIOA external input line
|
||||
TC_CMR_LDRA_RISING; // Latch counter value into RA
|
||||
|
||||
TC_Configure(TC2, 0, config); // Configure channel 0 of TC2
|
||||
TC_Start(TC2, 0); // Start timer running
|
||||
|
||||
TC2->TC_CHANNEL[0].TC_IER = TC_IER_LDRAS; // Enable the load AR channel 0 interrupt each PPS
|
||||
TC2->TC_CHANNEL[0].TC_IDR = ~TC_IER_LDRAS; // and disable the rest of the interrupt sources
|
||||
NVIC_EnableIRQ(TC6_IRQn); // Enable interrupt handler for channel 0
|
||||
|
||||
// Set up the PIO controller to route input pins for TC0 and TC2
|
||||
|
||||
PIO_Configure(PIOC,PIO_INPUT,
|
||||
PIO_PB25B_TIOA0, // D2 Input
|
||||
PIO_DEFAULT);
|
||||
|
||||
PIO_Configure(PIOC,PIO_INPUT,
|
||||
PIO_PC25B_TIOA6, // D5 Input
|
||||
PIO_DEFAULT);
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
// Dead time is the time to wait after seeing an event
|
||||
// before detecting a new event. There is ringing on the
|
||||
// event input on pin 5 that needs suppressing
|
||||
|
||||
uint32_t old_ra = 0; // Old register value from previous event
|
||||
uint32_t new_ra = 0; // New counter value that must be bigger by dead time
|
||||
uint32_t dead_time = 840000; // 10ms
|
||||
uint32_t dead_cntr = 0; // Suppressed interrupts due to dead time
|
||||
uint32_t dead_dely = 0; // Amout of time lost in dead time
|
||||
|
||||
// Handle the PPS interrupt in counter block 0 ISR
|
||||
|
||||
void TC0_Handler() {
|
||||
//aSer->print("TC0debug\n");
|
||||
|
||||
// reset pps_millis
|
||||
//pps_micros = micros();
|
||||
// disable our backup "timer"
|
||||
//target_mills = millis() + 1010;
|
||||
|
||||
// In principal we could connect a diode
|
||||
// to pass on the PPS to counter blocks 1 & 2. However for some unknown
|
||||
// reason this pulls down the PPS voltage level to less than 1V and
|
||||
// the trigger becomes unreliable !!
|
||||
// In any case the PPS is 100ms wide !! Introducing a blind spot when
|
||||
// the diode creates the OR of the event trigger and the PPS.
|
||||
// So this is a software diode
|
||||
/*
|
||||
TC2->TC_CHANNEL[0].TC_CCR = TC_CCR_SWTRG; // Forward PPS to counter block 2
|
||||
TC1->TC_CHANNEL[0].TC_CCR = TC_CCR_SWTRG; // Forward PPS to counter block 1
|
||||
|
||||
rega0 = TC0->TC_CHANNEL[0].TC_RA; // Read the RA reg (PPS period)
|
||||
stsr0 = TC_GetStatus(TC0, 0); // Read status and clear load bits
|
||||
|
||||
if (rega0 < MFRQ) // Sanity check against noise
|
||||
rega0 = FREQ; // Use nominal value
|
||||
|
||||
TC_SetRC(TC1, 0, rega0); // Set the PLL count to what we just counted
|
||||
|
||||
ppcnt++; // PPS count
|
||||
gps_ok = true; // Its OK because we got a PPS
|
||||
pll_flag = true; // Inhibit PLL, dont take over PPS arrived
|
||||
pps_recieved = true;
|
||||
|
||||
old_ra = 0; // Dead time counters
|
||||
new_ra = 0;
|
||||
dead_dely = 0; // Reset dead delay
|
||||
|
||||
if (leds_on) {
|
||||
digitalWrite(PPS_PIN, !digitalRead(PPS_PIN)); // Toggle led
|
||||
}
|
||||
|
||||
displ = 1;
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
// Handle PLL interrupts
|
||||
// When/If the PPS goes missing due to a lost lock we carry on with the last measured
|
||||
// value for the second from TC0
|
||||
|
||||
void TC3_Handler() {
|
||||
/*
|
||||
//aSer->print("TC3debug\n");
|
||||
|
||||
stsr2 = TC_GetStatus(TC1, 0); // Read status and clear interrupt
|
||||
|
||||
if (pll_flag == false) { // Only take over when no PPS
|
||||
//aSer->print("PLL FALSE\n");
|
||||
TC2->TC_CHANNEL[0].TC_CCR = TC_CCR_SWTRG; // Forward PPS to counter block 2
|
||||
ppcnt++; // PPS count
|
||||
displ = 1; // Display stuff in the loop
|
||||
gps_ok = false; // PPS missing
|
||||
pll_pulse = true;
|
||||
}
|
||||
pll_flag = false; // Take over until PPS comes back
|
||||
*/
|
||||
}
|
||||
|
||||
// Handle isolated PPS (via diode) LOR with the Event
|
||||
// The diode is needed to block Event pulses getting back to TC0
|
||||
// LOR means Logical inclusive OR
|
||||
// Now we are using the software diode implementation
|
||||
|
||||
|
||||
void TC6_Handler() {
|
||||
|
||||
/*
|
||||
//this is the event interrupt
|
||||
|
||||
//aSer->print("TC6 event debug\n");
|
||||
|
||||
//unsigned long us = micros() - pps_micros;
|
||||
|
||||
|
||||
// send the event twice to make sure it is actually recieved without problems
|
||||
// the reading software must be tuned to not double count this
|
||||
//sprintf(txt,"Event: sub second micros:%d; Event Count:%d\n", us, eventCount);
|
||||
//aSer->print(txt);
|
||||
|
||||
// turn on LED, it will be turned off in the main loop
|
||||
last_event_LED = millis();
|
||||
if (leds_on) {
|
||||
digitalWrite(EVT_PIN, HIGH);
|
||||
}
|
||||
|
||||
|
||||
// Then unblock
|
||||
rega1 = TC2->TC_CHANNEL[0].TC_RA; // Read the RA on channel 1 (PPS period)
|
||||
if (eventstack > 0){
|
||||
evttime[eventstack] = rega1+evttime[eventstack-1];
|
||||
}
|
||||
else
|
||||
{
|
||||
evttime[eventstack] = rega1;
|
||||
}
|
||||
eventCount++;
|
||||
eventstack++; //increment the event stack for this second
|
||||
|
||||
|
||||
stsr1 = TC_GetStatus(TC2, 0); // Read status clear load bits
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Push the GPS state when we recieve a PPS or PPL
|
||||
void printPPS() {
|
||||
sprintf(txt,"PPS: GPS lock:%d;\n", gps_ok);
|
||||
aSer->print(txt);
|
||||
}
|
||||
|
||||
// Things that need handling on PPS and PLL, but are non time critical
|
||||
void PPL_PPS_combinedHandling(){
|
||||
printPPS();
|
||||
// set the time for the next sensor update
|
||||
nextSensorUpdate = target_mills - distanceSensorUpdatePPS;
|
||||
}
|
||||
|
||||
|
||||
// ------------------------- Arudino Functions
|
||||
|
||||
// Arduino setup function, initialize hardware and software
|
||||
// This is the first function to be called when the sketch is started
|
||||
|
||||
//setup serials
|
||||
HardwareSerial Serial1(PB6,PA10);
|
||||
//HardwareSerial Serial(PA3, PA2);
|
||||
|
||||
void setup() {
|
||||
|
||||
aSer->print("poweron");
|
||||
|
||||
|
||||
if (simulateEvents) {
|
||||
randomSeed(42);
|
||||
}
|
||||
|
||||
// The two leds on the front pannel for PPS and Event
|
||||
if (leds_on) {
|
||||
pinMode(EVT_PIN, OUTPUT); // Pin for the cosmic ray event
|
||||
pinMode(PPS_PIN, OUTPUT); // Pin for the PPS (LED pin)
|
||||
}
|
||||
if (leds_on) {
|
||||
digitalWrite(PPS_PIN, HIGH); // Turn on led
|
||||
}
|
||||
|
||||
//TimersStart(); // Start timers
|
||||
//target_mills = millis() + 1010; // backup PPS
|
||||
|
||||
aSer = new AsyncSerial(SERIAL_BAUD_RATE); // Start the serial line
|
||||
// start the GPS
|
||||
Serial1.begin(GPS_BAUD_RATE);
|
||||
GpsSetup();
|
||||
|
||||
// start the i2c bus
|
||||
Wire.begin();
|
||||
Wire.setSDA(PB7);
|
||||
Wire.setSCL(PB8);
|
||||
|
||||
// start the detector
|
||||
setupDetector();
|
||||
|
||||
// start the sensors
|
||||
sensors = Sensors(aSer);
|
||||
|
||||
|
||||
|
||||
// initilize the sensors
|
||||
if(!sensors.init()){
|
||||
aSer->print("WARNING: Error in sensor initialization - continuing - output may be inclomplete!\n");
|
||||
} else{
|
||||
aSer->print("INFO: Sensor setup complete\n");
|
||||
}
|
||||
|
||||
aSer->print("INFO: Running\n");
|
||||
}
|
||||
|
||||
|
||||
// Arduino main loop does all the user space work
|
||||
|
||||
void loop() {
|
||||
/*
|
||||
// on a PPS from the GPS
|
||||
if (pps_recieved){
|
||||
PPL_PPS_combinedHandling();
|
||||
pps_recieved = false;
|
||||
}
|
||||
*/
|
||||
/*
|
||||
// if no pps recieved
|
||||
if (millis() >= target_mills){
|
||||
target_mills = millis() + 1000;
|
||||
// reset pps_millis
|
||||
pps_micros = micros();
|
||||
// while we have no pps we will keep the LED solid
|
||||
if (leds_on) {
|
||||
digitalWrite(PPS_PIN, HIGH);
|
||||
}
|
||||
gps_ok = false;
|
||||
PPL_PPS_combinedHandling();
|
||||
}
|
||||
|
||||
// simulate an interrupt if we want to simulate events
|
||||
if (simulateEvents) {
|
||||
if (millis() >= nextSimEvent){
|
||||
aSer->print("INFO: Simulating next event\n");
|
||||
TC6_Handler();
|
||||
nextSimEvent = millis() + random(100, 1000);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// reset event LED when enough time has passed
|
||||
if (millis() >= (last_event_LED + event_LED_time)){
|
||||
if (leds_on) {
|
||||
digitalWrite(EVT_PIN, LOW);
|
||||
}
|
||||
}
|
||||
|
||||
// print out sensor updates
|
||||
if (enableSensorOutput){
|
||||
if (millis() >= (nextSensorUpdate)){
|
||||
sensors.printAll();
|
||||
nextSensorUpdate += 1000;
|
||||
}
|
||||
}
|
||||
//print out the events here
|
||||
if (displ>0){
|
||||
for (int i=0; i < eventstack; i++){
|
||||
if (gps_ok) {
|
||||
sprintf(txt,"Event: sub second micros:%d/%d; Event Count:%d\n", evttime[i], rega0, (eventCount-eventstack+i));
|
||||
}
|
||||
else {
|
||||
sprintf(txt,"Event: sub second micros:%d/%d; Event Count:%d\n", evttime[i], FREQ, (eventCount-eventstack+i));
|
||||
}
|
||||
aSer->print(txt);
|
||||
}
|
||||
|
||||
eventstack=0;//reset the event stack for the next second.
|
||||
displ=0;
|
||||
}
|
||||
|
||||
if ((pll_pulse)+(pps_recieved))
|
||||
{
|
||||
printPPS();
|
||||
pps_recieved = false;
|
||||
pll_pulse = false;
|
||||
}
|
||||
|
||||
// pipe GPS if it's available
|
||||
if (enableGPSPipe){
|
||||
pipeGPS();
|
||||
}
|
||||
|
||||
aSer->PutChar(); // Print one character per loop !!!
|
||||
}
|
||||
211
detector_setting.ino
Normal file
211
detector_setting.ino
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
#include <Wire.h>
|
||||
|
||||
// configure these to configure the detector
|
||||
static const int HV_DEFAULT = 0xAC;
|
||||
static const int DEFAULT_DAC_THRESH = 559; //modification for V1.5 production batch, this is a generic setting to be tuned by users
|
||||
|
||||
// other constants
|
||||
static const int HV_MAX = 89;
|
||||
static const int HV_MIN = 255;
|
||||
static const int DEFAULT_THRESH = 559;
|
||||
static const bool USE_DAC = true;
|
||||
|
||||
//set up the pins to remap SPI by hand
|
||||
//static const int num_devices = 2;
|
||||
//static const int SS_pin[num_devices] = {14, 15};
|
||||
//static const int SCK_pin = 17;
|
||||
//static const int MISO_pin = 22;
|
||||
//static const int MOSI_pin = 16;
|
||||
|
||||
// I2C adress pins
|
||||
//#define MAX5387_PA0_pin A9
|
||||
//#define MAX5387_PA1_pin A10
|
||||
//#define MAX5387_PA2_pin A11
|
||||
|
||||
byte thresh1;
|
||||
byte thresh2;
|
||||
int bigpart;
|
||||
int smallpart;
|
||||
|
||||
// initilizes the detector with default values
|
||||
void setupDetector(){
|
||||
// setup pins
|
||||
detecSetPinModes();
|
||||
detcSetConstantPins();
|
||||
// set defaults
|
||||
if (USE_DAC){
|
||||
// analogWrite(DAC0, DEFAULT_DAC_THRESH);
|
||||
// analogWrite(DAC1, DEFAULT_DAC_THRESH);
|
||||
}else{
|
||||
setThreshold(3, DEFAULT_THRESH);
|
||||
}
|
||||
setHV(HV_DEFAULT);
|
||||
sprintf(txt,"Detector threshold: %d\n", DEFAULT_DAC_THRESH);
|
||||
aSer->print(txt);
|
||||
sprintf(txt,"Detector setup finished\n");
|
||||
aSer->print(txt);
|
||||
}
|
||||
|
||||
|
||||
// sets pin modes needed for the detector
|
||||
void detecSetPinModes(){
|
||||
//setup analog writemode
|
||||
// analogWriteResolution(12);
|
||||
// I2C adress pins for the MAX5387
|
||||
// pinMode(MAX5387_PA0_pin, OUTPUT);
|
||||
// pinMode(MAX5387_PA1_pin, OUTPUT);
|
||||
// pinMode(MAX5387_PA2_pin, OUTPUT);
|
||||
// HV pins
|
||||
// digitalWrite(SS, HIGH); // Start with SS high
|
||||
// for (int i=0; i<num_devices; i++){
|
||||
// pinMode(SS_pin[i], OUTPUT);
|
||||
// digitalWrite(SS_pin[i], HIGH);
|
||||
// }
|
||||
// pinMode(SCK_pin, OUTPUT);
|
||||
//pinMode(MISO_pin, INPUT); //this is the avalanche pin, not implemented yet
|
||||
// pinMode(MOSI_pin, OUTPUT);
|
||||
}
|
||||
|
||||
|
||||
// sets pins that don't need changing (ever)
|
||||
void detcSetConstantPins(){
|
||||
// I2C adress pins for the MAX5387
|
||||
// digitalWrite(MAX5387_PA0_pin, LOW);//configure the address of the MAX5387 pot
|
||||
// digitalWrite(MAX5387_PA1_pin, LOW);//configure the address of the MAX5387 pot
|
||||
// digitalWrite(MAX5387_PA2_pin, LOW);//configure the address of the MAX5387 pot
|
||||
}
|
||||
|
||||
|
||||
// this function sets the thresholds for the MAX5387
|
||||
// 1 is the first channel, 2 the second and 3 sets both at the same time
|
||||
void setThreshold(int pot_channel, int value){
|
||||
int address = 0x60; // config I2C address of the DAC
|
||||
|
||||
//bitshifts for the two bytes to upload (10 bit value)
|
||||
smallpart=byte(value);
|
||||
bigpart=byte(value>>8);
|
||||
|
||||
|
||||
switch(pot_channel){
|
||||
case 1:
|
||||
sprintf(txt,"Setting threshold on channel 1 to: %d\n", value);
|
||||
aSer->print(txt);
|
||||
|
||||
Wire.beginTransmission(address);
|
||||
Wire.write(B00001000); // sends five bytes
|
||||
Wire.write(bigpart); // sends one byte
|
||||
Wire.write(smallpart);
|
||||
Wire.endTransmission();
|
||||
break;
|
||||
case 2:
|
||||
sprintf(txt,"Setting threshold on channel 2 to: %d\n", value);
|
||||
aSer->print(txt);
|
||||
|
||||
Wire.beginTransmission(address);
|
||||
Wire.write(B00000000); // sends five bytes
|
||||
Wire.write(bigpart); // sends one byte
|
||||
Wire.write(smallpart);
|
||||
Wire.endTransmission();
|
||||
break;
|
||||
case 3:
|
||||
sprintf(txt,"Setting threshold on both channels to: %d\n", value);
|
||||
aSer->print(txt);
|
||||
|
||||
Wire.beginTransmission(address);
|
||||
Wire.write(B00001000); // sends five bytes
|
||||
Wire.write(bigpart); // sends one byte
|
||||
Wire.write(smallpart);
|
||||
Wire.endTransmission();
|
||||
|
||||
Wire.beginTransmission(address);
|
||||
Wire.write(B00000000); // sends five bytes
|
||||
Wire.write(bigpart); // sends one byte
|
||||
Wire.write(smallpart);
|
||||
Wire.endTransmission();
|
||||
break;
|
||||
}
|
||||
/*
|
||||
// do a value check
|
||||
if (value > 255 || value < 1){
|
||||
return;
|
||||
} else {
|
||||
value = byte(value);
|
||||
}
|
||||
|
||||
Wire.begin();
|
||||
Wire.beginTransmission(byte(0x28)); // transmit to device #112
|
||||
switch(pot_channel){
|
||||
case 1:
|
||||
sprintf(txt,"Setting threshold on channel 1 to: %d\n", value);
|
||||
aSer->print(txt);
|
||||
Wire.write(byte(B00010001)); //sets value to the first channel
|
||||
Wire.write(value);
|
||||
thresh1 = value;
|
||||
break;
|
||||
case 2:
|
||||
sprintf(txt,"Setting threshold on channel 2 to: %d\n", value);
|
||||
aSer->print(txt);
|
||||
Wire.write(byte(B00010010)); //sets value to the second channel
|
||||
Wire.write(value);
|
||||
thresh2 = value;
|
||||
break;
|
||||
case 3:
|
||||
sprintf(txt,"Setting threshold on channel 1&2 to: %d\n", value);
|
||||
aSer->print(txt);
|
||||
Wire.write(byte(B00010011)); //sets value to both channels
|
||||
Wire.write(value);
|
||||
thresh1 = value;
|
||||
thresh2 = value;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Wire.endTransmission();
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
// set the two HV supplies
|
||||
byte setHV(byte _send) // This function is what bitbangs the data
|
||||
{
|
||||
if (_send > 0x5A){ //hardlimit values
|
||||
|
||||
sprintf(txt,"INFO: Setting HV 1&2 to: %d\n", _send);
|
||||
aSer->print(txt);
|
||||
//reception isn't implemented in this version.
|
||||
//byte _receive = 0;
|
||||
|
||||
digitalWrite(PC7, LOW);
|
||||
|
||||
for(int i=0; i<8; i++) // There are 8 bits in a byte
|
||||
{
|
||||
digitalWrite(PC3, bitRead(_send, 7-i)); // Set MOSI
|
||||
//delay(1);
|
||||
digitalWrite(PB13, HIGH); // SCK high
|
||||
//bitWrite(_receive, i, digitalRead(MISO_pin)); // Capture MISO
|
||||
digitalWrite(PB13, LOW); // SCK low
|
||||
//digitalWrite(MOSI_pin, LOW); // Set MOSI
|
||||
|
||||
}
|
||||
|
||||
digitalWrite(PC7, HIGH);
|
||||
|
||||
|
||||
digitalWrite(PC8, LOW);
|
||||
|
||||
for(int i=0; i<8; i++) // There are 8 bits in a byte
|
||||
{
|
||||
digitalWrite(PC3, bitRead(_send, 7-i)); // Set MOSI
|
||||
//delay(1);
|
||||
digitalWrite(PB13, HIGH); // SCK high
|
||||
//bitWrite(_receive, i, digitalRead(MISO_pin)); // Capture MISO
|
||||
digitalWrite(PB13, LOW); // SCK low
|
||||
//digitalWrite(MOSI_pin, LOW); // Set MOSI
|
||||
|
||||
}
|
||||
|
||||
digitalWrite(PC8, HIGH);
|
||||
//return _receive; // Return the received data
|
||||
}
|
||||
}
|
||||
49
gps_reading.ino
Normal file
49
gps_reading.ino
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
|
||||
// WARNING: One up the spout !!
|
||||
// The GPS chip puts the next nmea string in its output buffer
|
||||
// only if its been read, IE its empty.
|
||||
// So if you read infrequently the string in the buffer is old and
|
||||
// has the WRONG time !!! The string lies around like a bullet in
|
||||
// the breach waiting for some mug.
|
||||
|
||||
//I don't know who wrote that comment.. I'm guessing it was Julian!
|
||||
|
||||
boolean pipeGPS() {
|
||||
while (Serial1.available()) {
|
||||
char c[2];
|
||||
c[0] = Serial1.read();
|
||||
c[1]='\0'; //null character for termination required.
|
||||
aSer->print(c);
|
||||
}
|
||||
}
|
||||
|
||||
// GPS setup
|
||||
|
||||
void GpsSetup() {
|
||||
|
||||
// definitions for different outputs
|
||||
// only one of these strings can be used at a time
|
||||
// otherwise they will overwrite each other
|
||||
// for more information take a look at the QUECTEL L70 protocoll specification: http://docs-europe.electrocomponents.com/webdocs/147d/0900766b8147dbdd.pdf
|
||||
#define RMCGGA "$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28" // RCM & GGA
|
||||
#define ZDA "$PMTK314,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0*29" // ZDA
|
||||
#define GGAZDA "$PMTK314,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0*28" // GGA & ZDA
|
||||
#define GGA "$PMTK314,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*29" // GGA
|
||||
|
||||
// gets the firmware version
|
||||
#define FMWVERS "$PMTK605*31" // PMTK_Q_RELEASE
|
||||
// Sets the update intervall
|
||||
#define NORMAL "$PMTK220,1000*1F" // PMTK_SET_NMEA_UPDATE_1HZ
|
||||
// disables updates for the antenna status (only Adafruit ultimate GPS?)
|
||||
#define NOANTENNA "$PGCMD,33,0*6D" // PGCMD_NOAN
|
||||
delay(5000); //added delay to give GPS time to boot.
|
||||
Serial1.println(NOANTENNA);
|
||||
Serial1.println(GGAZDA);
|
||||
Serial1.println(NORMAL);
|
||||
Serial1.println(FMWVERS);
|
||||
delay(10000); //wait a bit longer and repeat just in case it hasn't booted
|
||||
Serial1.println(NOANTENNA);
|
||||
Serial1.println(GGAZDA);
|
||||
Serial1.println(NORMAL);
|
||||
Serial1.println(FMWVERS);
|
||||
}
|
||||
46
readme.md
Normal file
46
readme.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# CosmicPi V1.5 Arduino DUE software
|
||||
|
||||
This is the Arduino DUE software running on the CosmicPi V1.5.
|
||||
This software is designed to work in a plug and play fashion, no interaction is required.
|
||||
|
||||
|
||||
## Features
|
||||
* Set up the detector with default values
|
||||
* Send events from the detector via serial
|
||||
* Send data from all sensors on the board via serial
|
||||
* Configure the on board GPS and pipe it's data to serial
|
||||
|
||||
### Meaning of the LEDs
|
||||
* Lower LED (green): Power and GPS
|
||||
* Solid: Power, but no GPS lock
|
||||
* Blinking: Power and GPS lock
|
||||
* Upper LED (red): Event
|
||||
* Flash: An Event has been registered
|
||||
|
||||
|
||||
## ToDo before release
|
||||
* The timer needs a rework, its current implementation is imprecise
|
||||
* The detector should be able to calibrate it's parameters automatically
|
||||
* High voltage
|
||||
* Thresholds
|
||||
|
||||
|
||||
## Installation
|
||||
For regular users of the CosmicPi V1.5 this should be taken care of automatically by the software on the RaspberryPi.
|
||||
|
||||
For everybody interested in looking into developing this oneself:
|
||||
1. Download the most recent Arduino IDE: https://www.arduino.cc/en/main/software
|
||||
2. Install the SAM core: https://www.arduino.cc/en/Guide/Cores
|
||||
3. Clone this repository: `git clone https://github.com/CosmicPi/cosmicpi-arduino_V1.5.git`
|
||||
(note you can also download the .zip file from the menu above to the right, expand and remember to re-name the directory so cosmpicpi-arduino_v1.5 otherwise Arduino will give you errors)
|
||||
4. Open the file `cosmicpi-arduino_V1.5.ino` with the Arduino IDE
|
||||
5. Connect your CosmicPi to your computer via the USB **Programming** port
|
||||
6. Select the newly appearing port in the Arduino IDE (Tools -> Port)
|
||||
7. Compile and upload the firmware (Sketch -> Upload)
|
||||
|
||||
|
||||
## Usage
|
||||
As soon as the CosmicPi is connected you can open the software of your choice for monitoring serial data. Open the Arduinos serial port with a baudrate of 19200.
|
||||
This firmware is designed to be a plug and play firmware.
|
||||
It will only send data. It will not accept inputs. What data is sent is defined [here](https://github.com/CosmicPi/cosmicpi-rpi_V1.5/blob/master/documentation/CosmicPi_V15_serial_comm.txt).
|
||||
|
||||
133
sensors.cpp
Normal file
133
sensors.cpp
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
#include "sensors.h"
|
||||
#include <Arduino.h>
|
||||
#include "asyncSerial.h"
|
||||
// LPS lib from here: https://github.com/pololu/lps-arduino
|
||||
//LSM303 isn't used anymore - change this out.
|
||||
#include "src/LPS.h"
|
||||
// LSM303 lib from here: https://github.com/pololu/lsm303-arduino
|
||||
#include "src/LSM303.h"
|
||||
// HTU21D lib from here: https://github.com/adafruit/Adafruit_HTU21DF_Library
|
||||
#include "src/SparkFunHTU21D.h"
|
||||
|
||||
Sensors::Sensors(AsyncSerial *aS){
|
||||
aSer = aS;
|
||||
}
|
||||
|
||||
// initilize sensors
|
||||
bool Sensors::init(){
|
||||
bool return_val = true;
|
||||
baroOK = baro.init();
|
||||
if (!baroOK)
|
||||
{
|
||||
sprintf(txt,"WARINING: Couldn't initilize the barometer!\n");
|
||||
aSer->print(txt);
|
||||
return_val = false;
|
||||
} else{
|
||||
baro.enableDefault();
|
||||
}
|
||||
|
||||
accelMagnetoOK = accelMagneto.init();
|
||||
if (!accelMagnetoOK)
|
||||
{
|
||||
sprintf(txt,"WARINING: Couldn't initilize the accelerometer and magnetometer!\n");
|
||||
aSer->print(txt);
|
||||
return_val = false;
|
||||
} else{
|
||||
accelMagneto.enableDefault();
|
||||
}
|
||||
|
||||
humidOK = humidity.begin();
|
||||
if (!humidOK)
|
||||
{
|
||||
sprintf(txt,"WARINING: Couldn't initilize the humidity sensor!\n");
|
||||
aSer->print(txt);
|
||||
return_val = false;
|
||||
}
|
||||
|
||||
return return_val;
|
||||
}
|
||||
|
||||
|
||||
// different ways of printing data
|
||||
void Sensors::printBaro(){
|
||||
if (baroOK) {
|
||||
float pressure = baro.readPressureMillibars();
|
||||
float altitude = baro.pressureToAltitudeMeters(pressure);
|
||||
float temperature = baro.readTemperatureC();
|
||||
|
||||
sprintf(txt,"Pressure: %f;\nAltitude: %f;\nTemperatureCBaro: %f;\n", pressure, altitude, temperature);
|
||||
aSer->print(txt);
|
||||
}
|
||||
}
|
||||
|
||||
void Sensors::printAccel(){
|
||||
if (accelMagnetoOK) {
|
||||
accelMagneto.read();
|
||||
float x = AclToMs2(accelMagneto.a.x);
|
||||
float y = AclToMs2(accelMagneto.a.y);
|
||||
float z = AclToMs2(accelMagneto.a.z);
|
||||
sprintf(txt,"AccelX: %f;\nAccelY: %f;\nAccelZ: %f;\n", x, y, z);
|
||||
aSer->print(txt);
|
||||
}
|
||||
}
|
||||
|
||||
void Sensors::printMagneto(){
|
||||
if (accelMagnetoOK) {
|
||||
accelMagneto.read();
|
||||
float x = MagToGauss(accelMagneto.m.x);
|
||||
float y = MagToGauss(accelMagneto.m.y);
|
||||
float z = MagToGauss(accelMagneto.m.z);
|
||||
sprintf(txt,"MagX: %f;\nMagY: %f;\nMagZ: %f;\n", x, y, z);
|
||||
aSer->print(txt);
|
||||
}
|
||||
}
|
||||
|
||||
void Sensors::printHumid(){
|
||||
if (humidOK) {
|
||||
sprintf(txt,"TemperatureCHumid: %f;\nHumidity: %f;\n", humidity.readTemperature(), humidity.readHumidity());
|
||||
aSer->print(txt);
|
||||
}
|
||||
}
|
||||
|
||||
void Sensors::printTempAvg(){
|
||||
short count = 0;
|
||||
int sum = 0;
|
||||
if (humidOK) {
|
||||
count++;
|
||||
sum += humidity.readTemperature();
|
||||
}
|
||||
if (baroOK) {
|
||||
count++;
|
||||
sum += baro.readTemperatureC();
|
||||
}
|
||||
|
||||
if (count != 0){
|
||||
float out = sum / count;
|
||||
sprintf(txt,"TemperatureC: %f;\n", out);
|
||||
aSer->print(txt);
|
||||
}
|
||||
}
|
||||
|
||||
void Sensors::printAll(){
|
||||
printBaro();
|
||||
printAccel();
|
||||
printMagneto();
|
||||
printHumid();
|
||||
printTempAvg();
|
||||
}
|
||||
|
||||
static const double GEARTH = 9.80665;
|
||||
// normaly there is no - in ACL_FS, but the Accel is on upwards down, so it was necessary to get correct values
|
||||
static const double ACL_FS = -2.0; // +-2g 16 bit 2's compliment
|
||||
|
||||
// Convert to meters per sec per sec
|
||||
|
||||
float Sensors::AclToMs2(int16_t val) {
|
||||
return (ACL_FS * GEARTH) * ((float) val / (float) 0x7FFF);
|
||||
}
|
||||
|
||||
#define MAGNETIC_FS 4.0 // Full scale Gauss 16 bit 2's compliment
|
||||
|
||||
float Sensors::MagToGauss(int16_t val) {
|
||||
return (MAGNETIC_FS * (float) val) / (float) 0x7FFF;
|
||||
}
|
||||
50
sensors.h
Normal file
50
sensors.h
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#ifndef __SENSORS__
|
||||
#define __SENSORS__
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "asyncSerial.h"
|
||||
// LPS lib from here: https://github.com/pololu/lps-arduino
|
||||
#include "src/LPS.h"
|
||||
// LSM303 lib from here: https://github.com/pololu/lsm303-arduino
|
||||
#include "src/LSM303.h"
|
||||
// HTU21D lib from here: https://github.com/sparkfun/SparkFun_HTU21D_Breakout_Arduino_Library
|
||||
#include "src/SparkFunHTU21D.h"
|
||||
|
||||
class Sensors {
|
||||
// object for the pressure sensor
|
||||
LPS baro;
|
||||
// object for the accel and magnetometer
|
||||
LSM303 accelMagneto;
|
||||
// object for humidity sensor
|
||||
HTU21D humidity;
|
||||
|
||||
// vars for checking if a sensor initilized correctly
|
||||
bool baroOK;
|
||||
bool accelMagnetoOK;
|
||||
bool humidOK;
|
||||
|
||||
// cute class for printing and the needed char array
|
||||
AsyncSerial *aSer;
|
||||
static const int TXTLEN = 512;
|
||||
char txt[TXTLEN]; // For writing to serial
|
||||
|
||||
public:
|
||||
Sensors(AsyncSerial *aS);
|
||||
// initilize sensors
|
||||
bool init();
|
||||
// different ways of printing data
|
||||
void printBaro();
|
||||
void printAccel();
|
||||
void printMagneto();
|
||||
void printHumid();
|
||||
void printTempAvg();
|
||||
void printAll();
|
||||
|
||||
private:
|
||||
float AclToMs2(int16_t val);
|
||||
float MagToGauss(int16_t val);
|
||||
|
||||
};
|
||||
|
||||
|
||||
#endif
|
||||
Loading…
Reference in a new issue