Created
April 24, 2019 21:39
-
-
Save branw/5dddb8de7f60b2b1ac3455e5986789e3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
""" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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