Skip to content

Instantly share code, notes, and snippets.

@branw
Created April 24, 2019 21:39
Show Gist options
  • Save branw/5dddb8de7f60b2b1ac3455e5986789e3 to your computer and use it in GitHub Desktop.
Save branw/5dddb8de7f60b2b1ac3455e5986789e3 to your computer and use it in GitHub Desktop.
import matplotlib.pyplot as plt
from datetime import datetime
import serial
import serial.tools.list_ports
import struct
import time
import math
import os
# COM port of Arduino Due; leave None for cross-platform auto-detection
DDG_PORT = 'COM5'
# Number of runs between plot updates. Plotting almost doubles the
# duration of each run, so keep this large
PLOT_INTERVAL = 1
# Number of milliseconds for each run, used to add artificial delays after runs.
# Set to 0 to achieve the maximum collection rate
RUN_DURATION = 0
class DDG:
"""
Interface to an Arduino Due running DAQ DAQ Goose
Bare minimum example (collect a single run):
ddg = DDG()
left, right = ddg.collect_data()
plt.plot(left)
plt.plot(right)
plt.show()
"""
FIRMWARE_VERSION = (0, 2)
def __init__(self, port=None):
"""
Connect to a device
:param port: optional, COM port of Arduino Due
"""
# Try to deduce the serial port
#if not port:
# port = self.guess_port()
# Connect to the device
self.ser = serial.Serial(port)
# Attempt to reset the device
self.reset()
# Handshake with the device and get its version
version = self.get_version()
if version != self.FIRMWARE_VERSION:
raise Exception('The Due is running an incompatible firware version '
f'(expected {self.FIRMWARE_VERSION}, got {version})')
print(f'Connected to Due on {port}')
@staticmethod
def guess_port():
"""
Discover any locally connected Arduino Dues
:return: COM port name of discovered Due
"""
# Try to detect any Arduino Dues by USB IDs
available_ports = serial.tools.list_ports.comports()
possible_ports = [port.device for port in available_ports \
if (port.vid == 9025 and port.pid == 62)]
# Yell at the user if no Due was found
if not any(possible_ports):
# Warn them if they are using the wrong port
prog_port_connected = any(1 for port in available_ports \
if (port.vid == 9025 and port.pid == 61))
if prog_port_connected:
raise Exception('Connected to the wrong Due port: use the outer (native) port')
raise Exception('Due not found: verify that it is properly connected')
return possible_ports[0]
@staticmethod
def create_packet(op, body=None):
body_len = len(body) if body else 0
packet = bytearray(5 + body_len)
packet[0:5] = struct.pack('<BI', op, body_len)
if body:
packet[5:] = body
return packet
def reset(self):
"""
Trigger a hardware reset using the serial's DTR; highly
dependent on hardware configuration
"""
self.ser.setDTR(False)
time.sleep(1)
self.ser.flushInput()
self.ser.setDTR(True)
def write(self, packet):
self.ser.write(packet)
def read_header(self, expected_opcode=None, expected_len=None):
"""
Read a packet header from serial; throws when expected values are
different than actual
:param expected_opcode: optional, opcode of packet to receive
:para expected_len: optional, length of packet to receive
"""
op, resp_len = struct.unpack('<BI', self.ser.read(size=5))
if (expected_opcode is not None and op != expected_opcode) or \
(expected_len is not None and resp_len != expected_len):
raise Exception('Unexpected packet! Reset the Due')
return resp_len
def read_body(self, resp_len):
"""
Read a fixed number of bytes from serial
"""
return bytearray(self.ser.read(size=resp_len))
def read(self, expected_opcode=None, expected_len=None):
"""
Read a packet header and body; throws when expected values are
different than actual
:param expected_opcode: optional, opcode of packet to receive
:param expected_len, optional, length of packet to receive
"""
return self.read_body(self.read_header(expected_opcode, expected_len))
def get_version(self):
"""
Get the firmware version
:return: firmware version major and minor
"""
self.write(self.create_packet(0))
resp = self.read(expected_opcode=0x80, expected_len=2)
return struct.unpack('<BB', resp)
def collect_data(self, dynamic=False, dynamic_delayed=False):
"""
Perform a full data collection run
:param dynamic: ears should be moved before data collection
:param dynamic_delayed: add a slight delay before dynamic data collection
:return: collected data split into separate channels
"""
opcode = 2
ddg.write(ddg.create_packet(opcode))
ddg.read(expected_opcode=0x80|opcode, expected_len=0)
# Wait for collection to finish and read in data
raw_data = ddg.read(expected_opcode=0x82, expected_len=2*45000)
# The two channels are interleaved and split across two little endian bytes;
# we need to join the bytes and split the two channels
joined_data = [(3.3/4095 * ((y << 8) | x)) for x, y in zip(raw_data[::2], raw_data[1::2])]
return joined_data
if __name__ == '__main__':
# Connect to Arduino Due
ddg = DDG(port=DDG_PORT)
print('-' * 60)
print('Enter name of file: ', end='')
file_name = input()
print('-' * 60)
print(f'Saving to {os.path.abspath(file_name)}')
print('-' * 60)
data = ddg.collect_data()
with open(file_name, 'w') as f:
for data_point in data:
f.write('{}\n'.format(data_point))
print('Done!')
"""
# Loop data collection indefinitely; press Ctrl-C and close the plot
# to stop elegantly
while True:
try:
run_start = time.time_ns()
# Collect data
# Periodically plot an incoming signal
# Clear previous lines (for speed)
ax1.lines = []
# Plot
ax1.plot(left)
# Show the plot without blocking (there's no separate UI
# thread)
plt.show(block=False)
plt.pause(0.001)
except KeyboardInterrupt:
print('Interrupted')
break
"""
#define VERSION_MAJOR 0
#define VERSION_MINOR 2
// 1.6 MHz is the max frequency of the DAC, so it's easiest
// just to scale everything off of it. 1 ADC channel runs at
// 41.25% (~660 kHz); 2 ADC channels run at 25% (400 kHz each)
const int freq = 3000;
// The ADC channels to sample. Note that the port numbers on the
// Due (A0-7) count in reverse order to the SAM3X's channels (CH7-0),
// i.e. A0 is CH7 and A1 is CH6
const int adc_channels = ADC_CHER_CH7;
// Number of channels listed above
const int num_adc_channels = 1;
// Size of each block in the ADC buffer
const int adc_block_size = 200 * num_adc_channels;
// Number of samples to collect in total, for all channels
const int num_adc_samples = 45000;
// Number of blocks in the ADC buffer
const int num_adc_blocks = num_adc_samples / adc_block_size;
// Internal index of the current block within the DAC buffer
volatile uint16_t adc_block_index = 0;
// Buffers to store data that is converted from the ADC. Sampling for
// a longer time requires either flushing these buffers out perioidcally,
// or just increasing their size. Currently this does the latter which is
// not very scalable and should be resolved later on
volatile uint16_t input_waveforms[num_adc_blocks][adc_block_size];
// Flag to indicate that the data collection is finished and the buffers
// can be dumped
volatile bool data_ready = false;
void setup() {
// USB serial is performed at native speed, negotiated by the host.
// The baud rate set here will be ignored
SerialUSB.begin(1337);
// Enable output on B ports
REG_PIOB_OWER = 0xFFFFFFFF;
REG_PIOB_OER = 0xFFFFFFFF;
}
void loop() {
static bool collect_data = false;
// Don't poll while we're collecting data (although this is unlikely to be
// hit considering all of the cycles are consumed by the timer)
if (collect_data && !data_ready) {
return;
}
// Transmit the collected data when it's ready
if (data_ready) {
int body_len = num_adc_blocks*adc_block_size*2;
char header[5] = {
0x82,
(body_len >> 0*8) & 0xff,
(body_len >> 1*8) & 0xff,
(body_len >> 2*8) & 0xff,
(body_len >> 3*8) & 0xff,
};
SerialUSB.write(header, 5);
for (int n = 0; n < num_adc_blocks; n++) {
SerialUSB.write((uint8_t *)input_waveforms[n], adc_block_size);
SerialUSB.write((uint8_t *)input_waveforms[n] + adc_block_size, adc_block_size);
}
data_ready = false;
collect_data = false;
}
// Poll for incoming request packets
// A packet header consists of an opcode (1 byte) and body length (4 bytes)
if (SerialUSB.available() >= 5) {
uint8_t opcode = SerialUSB.read();
// We enforce little-endian for all communications.
// Do not combine these lines: the lack of a sequence point
// would cause undefined behavior as the calls to read() have
// side effects
uint32_t input_len = SerialUSB.read();
input_len = input_len | (SerialUSB.read() << 8);
input_len = input_len | (SerialUSB.read() << 16);
input_len = input_len | (SerialUSB.read() << 24);
// Dispatch the packet to its handler
switch (opcode) {
// Hello
case 0x00: {
char response[7] = {
opcode | 0x80,
2, 0, 0, 0,
VERSION_MAJOR, VERSION_MINOR
};
SerialUSB.write(response, 7);
break;
}
// Collect data (static)
case 0x02: {
char response[5] = { opcode | 0x80, 0, 0, 0, 0 };
SerialUSB.write(response, 5);
collect_data = true;
reset();
break;
}
// Unknown
default: {
char response[255] = {0};
response[0] = 0xff;
int msg_len = snprintf(&response[5], 250, "Unknown opcode %i", opcode);
response[1] = (uint8_t)(msg_len);
response[2] = (uint8_t)(msg_len >> 8);
response[3] = (uint8_t)(msg_len >> 16);
response[4] = (uint8_t)(msg_len >> 24);
SerialUSB.write(response, msg_len + 5);
break;
}
}
}
}
/*****************************************************************************
* HERE BE DRAGONS:
*
* Everything below this is micro-controller specific and probably not
* something worth looking at or modifying
*****************************************************************************/
// Initialize the timer peripheral TC0
void setup_timer() {
// Enable the clock of the peripheral
pmc_enable_periph_clk(TC_INTERFACE_ID);
TC_Configure(TC0, 0,
// Waveform mode
TC_CMR_WAVE |
// Count-up with RC as the threshold
TC_CMR_WAVSEL_UP_RC |
// Clear on RA and set on RC
TC_CMR_ACPA_CLEAR | TC_CMR_ACPC_SET |
TC_CMR_ASWTRG_CLEAR |
// Prescale by 2 (MCK/2=42MHz)
TC_CMR_TCCLKS_TIMER_CLOCK1);
uint32_t rc = SystemCoreClock / (2 * freq);
// Achieve a duty cycle of 50% by clearing after half a period
TC_SetRA(TC0, 0, rc / 2);
// Set the period
TC_SetRC(TC0, 0, rc);
TC_Start(TC0, 0);
// Enable the interrupts with the controller
NVIC_EnableIRQ(TC0_IRQn);
}
// Initialize the ADC peripheral
void setup_adc() {
// Reset the controller
ADC->ADC_CR = ADC_CR_SWRST;
// Reset all of the configuration options
ADC->ADC_MR = 0;
// Stop any transfers
ADC->ADC_PTCR = (ADC_PTCR_RXTDIS | ADC_PTCR_TXTDIS);
// Setup timings
ADC->ADC_MR |= ADC_MR_PRESCAL(1);
ADC->ADC_MR |= ADC_MR_STARTUP_SUT24;
ADC->ADC_MR |= ADC_MR_TRACKTIM(1);
ADC->ADC_MR |= ADC_MR_SETTLING_AST3;
ADC->ADC_MR |= ADC_MR_TRANSFER(1);
// Use a hardware trigger
ADC->ADC_MR |= ADC_MR_TRGEN_EN;
// Trigger on timer 0, channel 0 (TC0)
ADC->ADC_MR |= ADC_MR_TRGSEL_ADC_TRIG1;
// Enable the necessary channels
ADC->ADC_CHER = adc_channels;
// Load the DMA buffer
ADC->ADC_RPR = (uint32_t)input_waveforms[0];
ADC->ADC_RCR = adc_block_size;
if (num_adc_blocks > 1) {
ADC->ADC_RNPR = (uint32_t)input_waveforms[1];
ADC->ADC_RNCR = adc_block_size;
adc_block_index++;
}
// Enable an interrupt when the end of the DMA buffer is reached
ADC->ADC_IDR = ~ADC_IDR_ENDRX;
ADC->ADC_IER = ADC_IER_ENDRX;
NVIC_EnableIRQ(ADC_IRQn);
// Enable receiving data
ADC->ADC_PTCR = ADC_PTCR_RXTEN;
// Wait for a trigger
ADC->ADC_CR |= ADC_CR_START;
}
// A junk variable we point the DMA buffer to before stopping the ADC.
// This is to ensure that we don't overwrite anything, but might not
// be entirely necessary
uint16_t adc_junk_space[1] = {0};
// Interrupt handler for the ADC peripheral
void ADC_Handler() {
if (ADC->ADC_ISR & ADC_ISR_ENDRX) {
if (adc_block_index >= num_adc_blocks) {
adc_stop(ADC);
TC_Stop(TC0, 0);
data_ready = true;
ADC->ADC_RPR = ADC->ADC_RNPR = (uint32_t)adc_junk_space;
ADC->ADC_RCR = ADC->ADC_RNCR = 1;
return;
}
ADC->ADC_RNPR = (uint32_t)input_waveforms[++adc_block_index % num_adc_blocks];
ADC->ADC_RNCR = adc_block_size;
}
}
// Reset all of the peripherals
void reset() {
adc_block_index = 0;
// Temporarily disable write-protection for the power controller
// while we enable peripheral clocks
pmc_set_writeprotect(false);
setup_adc();
setup_timer();
pmc_set_writeprotect(true);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment