Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions firmware/lib/hardware_mapping/ec_pins.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ extern Uart RS485_2; // RS485 on UART 2
#define PIN_SPARK_TRIG PC13

// Zucrow Board
#define ZUCROW_BOARD_SPI_BUS PT_TC_SPI_1
#define PIN_ZUCROW_BOARD_CS PE3 // TODO - replace with PT board definition

#define PIN_ZUCROW_BOARD_DO1 PF4
Expand Down
180 changes: 180 additions & 0 deletions firmware/lib/zucrow_interface/ZucrowInterface.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
#include "ZucrowInterface.h"
#include "CommandRouter.h"
#include "CommsSerial.h"
#include "ec_pins.h"

#define DAC_COUNTS (1 << 12) // 12 bit DAC, 4096 counts
#define DAC_MAX_COUNT (DAC_COUNTS - 1)
#define VOLTS_PER_COUNT (2.048f * 4.0f / DAC_COUNTS)

// MCP4822 write command: 16 bit word:
// bit 15 channel (0 = A, 1 = B), bit 13 gain (1 = 1x), bit 12 output (1 = active), bits 11:0 data
#define MCP4822_CHANNEL_A (0 << 15)
#define MCP4822_CHANNEL_B (1 << 15)
#define MCP4822_GAIN_1X (1 << 13)
#define MCP4822_ACTIVE (1 << 12)

// TODO: verify clock integrity with a scope; The ISO6441 isolator is rated to 150 Mbps,
// and long traces to the zucrow board may cause issues.
SPISettings ZUCROW_DAC_SPI_SETTINGS(4000000, MSBFIRST, SPI_MODE0);

// TODO - confirm DI1/DI2 and DO1/DO2 against the schematic net names.
#define PIN_ZUCROW_FAULT_IN PIN_ZUCROW_BOARD_DI1
#define PIN_ZUCROW_SYNC_IN PIN_ZUCROW_BOARD_DI2
#define PIN_ZUCROW_DI3 PIN_ZUCROW_BOARD_DI3
#define PIN_ZUCROW_DI4 PIN_ZUCROW_BOARD_DI4
#define PIN_EC_FAULT_OUT PIN_ZUCROW_BOARD_DO1
#define PIN_EC_SYNC_OUT PIN_ZUCROW_BOARD_DO2

// Line polarities, mirroring TADPOLE: Zucrow pulls its lines low to assert, we drive ours high.
// TODO - verify on the bench that an unplugged Zucrow connector reads as a fault. A disconnected
// line must safe the system.
#define ZUCROW_FAULT_LEVEL LOW
#define ZUCROW_RUNNING_LEVEL LOW
#define EC_FAULT_LEVEL HIGH
#define EC_OK_LEVEL LOW
#define EC_RUNNING_LEVEL HIGH
#define EC_IDLE_LEVEL LOW

namespace ZucrowInterface {

uint16_t last_count_ox;
uint16_t last_count_fu;

void write_dac(uint16_t channel, uint16_t count) {
// beginTransaction before CS so the bus is in this device's SPI mode before it is selected.
ZUCROW_BOARD_SPI_BUS.beginTransaction(ZUCROW_DAC_SPI_SETTINGS);
digitalWrite(PIN_ZUCROW_BOARD_CS, LOW);
ZUCROW_BOARD_SPI_BUS.transfer16(channel | MCP4822_GAIN_1X | MCP4822_ACTIVE | count);
digitalWrite(PIN_ZUCROW_BOARD_CS, HIGH);
ZUCROW_BOARD_SPI_BUS.endTransaction();
}

uint16_t angle_to_count(float angle_deg) {
// Written as !(x > 0) rather than x <= 0 so that NaN also maps to 0 instead of
// being cast to an integer, which is undefined behavior.
if (!(angle_deg > 0.0f)) {
return 0;
}

// Full scale maps to 4096, one past what a 12 bit DAC can hold.
float count = angle_deg / FULL_SCALE_ANGLE_DEG * DAC_COUNTS;
if (count > DAC_MAX_COUNT) {
return DAC_MAX_COUNT;
}

return (uint16_t)(count + 0.5f);
}

void send_valve_angles(float ox_angle_deg, float fu_angle_deg) {
last_count_ox = angle_to_count(ox_angle_deg);
last_count_fu = angle_to_count(fu_angle_deg);
write_dac(MCP4822_CHANNEL_A, last_count_ox);
write_dac(MCP4822_CHANNEL_B, last_count_fu);
}

fault_state_t check_fault() {
return digitalRead(PIN_ZUCROW_FAULT_IN) == ZUCROW_FAULT_LEVEL ? FAULT_ASSERTED : FAULT_CLEAR;
}

bool check_sync() {
Comment thread
wadhwat marked this conversation as resolved.
return digitalRead(PIN_ZUCROW_SYNC_IN) == ZUCROW_RUNNING_LEVEL;
}

PinStatus check_di3() {
return digitalRead(PIN_ZUCROW_BOARD_DI3);
}

PinStatus check_di4() {
return digitalRead(PIN_ZUCROW_BOARD_DI4);
}

void send_fault() {
digitalWrite(PIN_EC_FAULT_OUT, EC_FAULT_LEVEL);
}

void send_ok() {
digitalWrite(PIN_EC_FAULT_OUT, EC_OK_LEVEL);
}

void send_sync(bool running) {
digitalWrite(PIN_EC_SYNC_OUT, running ? EC_RUNNING_LEVEL : EC_IDLE_LEVEL);
}

void print_status() {
CommsSerial.printf("From Zucrow: fault %s, sync %s\n", check_fault() == FAULT_ASSERTED ? "ASSERTED" : "clear",
check_sync() ? "running" : "idle");
CommsSerial.printf("To Zucrow: fault %s, sync %s\n",
digitalRead(PIN_EC_FAULT_OUT) == EC_FAULT_LEVEL ? "ASSERTED" : "clear",
digitalRead(PIN_EC_SYNC_OUT) == EC_RUNNING_LEVEL ? "running" : "idle");
CommsSerial.printf("Spare lines from Zucrow: DI3: %s, DI4: %s\n", check_di3() == HIGH ? "high" : "low",
check_di4() == HIGH ? "high" : "low");
CommsSerial.printf("OX: %4u counts, %.3f V\n", last_count_ox, last_count_ox * VOLTS_PER_COUNT);
CommsSerial.printf("FU: %4u counts, %.3f V\n", last_count_fu, last_count_fu * VOLTS_PER_COUNT);
}

void send_angles_cmd(const char *args) {
float ox_angle_deg;
float fu_angle_deg;
if (sscanf(args, "%f %f", &ox_angle_deg, &fu_angle_deg) != 2) {
CommsSerial.println("Usage: zi_angles <ox_deg> <fu_deg>");
return;
}
send_valve_angles(ox_angle_deg, fu_angle_deg);
print_status();
}

// Steps both outputs through known counts, holding each long enough to read a multimeter
// at the Zucrow connector. The readings should land on 2 mV per count.
void calibration_sweep() {
const uint16_t counts[] = {0, 1024, 2048, 3072, DAC_MAX_COUNT};
for (uint16_t count : counts) {
last_count_ox = count;
last_count_fu = count;
write_dac(MCP4822_CHANNEL_A, count);
write_dac(MCP4822_CHANNEL_B, count);
CommsSerial.printf("%4u counts, expect %.3f V\n", count, count * VOLTS_PER_COUNT);
delay(5000);
}
send_valve_angles(0.0f, 0.0f);
CommsSerial.println("Sweep done, outputs zeroed.");
}

bool begin() {
// Deselect the DAC first. SPI1 is shared with PT and TC boards, and until this runs the CS pin
// floats, which could let the DAC clock in their traffic.
digitalWrite(PIN_ZUCROW_BOARD_CS, HIGH);
pinMode(PIN_ZUCROW_BOARD_CS, OUTPUT);

pinMode(PIN_ZUCROW_FAULT_IN, INPUT);
pinMode(PIN_ZUCROW_SYNC_IN, INPUT);
pinMode(PIN_ZUCROW_DI3, INPUT);
pinMode(PIN_ZUCROW_DI4, INPUT);

// Boot reporting a fault, and only report ok once we are actually ready. A controller that
// resets or hangs during boot then looks like a fault to Zucrow. Same as TADPOLE.
digitalWrite(PIN_EC_FAULT_OUT, EC_FAULT_LEVEL);
pinMode(PIN_EC_FAULT_OUT, OUTPUT);
digitalWrite(PIN_EC_SYNC_OUT, EC_IDLE_LEVEL);
pinMode(PIN_EC_SYNC_OUT, OUTPUT);

// No DAC write here. The MCP4822 powers up with both outputs shut down, which already reads as
// 0 V at the connector, and the first write to each channel enables it. Skipping the write means
// begin() puts no traffic on SPI1 while other devices' CS pins may still be floating.

/* clang-format off */
CommandRouter::add(print_status, "zi_status", "Print Zucrow line states and analog outputs.");
CommandRouter::add(send_fault, "zi_fault", "Assert the fault line to Zucrow.");
CommandRouter::add(send_ok, "zi_ok", "Clear the fault line to Zucrow.");
CommandRouter::add([]() { send_sync(true); }, "zi_run", "Set the sync line to Zucrow to running.");
CommandRouter::add([]() { send_sync(false); }, "zi_idle", "Set the sync line to Zucrow to idle.");
CommandRouter::add(send_angles_cmd, "zi_angles", "Send valve angles to Zucrow. Args: <ox_deg> <fu_deg>");
CommandRouter::add(calibration_sweep, "zi_sweep", "Step the analog outputs through known counts for calibration.");
/* clang-format on */

// There is no readback path on this board, so nothing here can fail. The calibration sweep
// is what proves the analog chain works.
return true;
}

} // namespace ZucrowInterface
35 changes: 35 additions & 0 deletions firmware/lib/zucrow_interface/ZucrowInterface.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once
#include <Arduino.h>

// Interface to the Zucrow test facility through the Zucrow board.
// Fault and sync lines run in both directions, and two analog outputs report
// valve angles to Zucrow's DAQ.

// Valve angle that maps to full scale on the analog outputs. Zucrow configures their scaling to match.
#define FULL_SCALE_ANGLE_DEG 90.0f

namespace ZucrowInterface {

bool begin();

// State of the fault line from Zucrow. FAULT_ASSERTED comes first so a zero-initialized value reads
// as a fault: an unknown state should safe the system, the same as a disconnected line.
enum fault_state_t { FAULT_ASSERTED, FAULT_CLEAR };

// Inputs from Zucrow. These hide pin polarity from callers.
fault_state_t check_fault();
bool check_sync(); // true when Zucrow reports running
// TODO: Rename when di3 and di4 have defined purposes
PinStatus check_di3();
PinStatus check_di4();
// Outputs to Zucrow.
void send_fault();
void send_ok();
void send_sync(bool running);

// Valve angles in degrees. Values outside [0, FULL_SCALE_ANGLE_DEG] are clamped.
void send_valve_angles(float ox_angle_deg, float fu_angle_deg);

void print_status();

} // namespace ZucrowInterface
6 changes: 6 additions & 0 deletions firmware/src/ec_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "TemperatureSensors.h"
#include "ThrottleValves.h"
#include "ValveController.h"
#include "ZucrowInterface.h"
#include "fdcan_toad.h"

// shared interfaces
Expand Down Expand Up @@ -53,6 +54,8 @@ void setup() {
bool all_modules_ok = true;

all_modules_ok &= CAN::init();
// Zucrow before PT and TC: its begin() deselects the Zucrow DAC before those boards talk on the shared SPI1 bus.
all_modules_ok &= ZucrowInterface::begin();
all_modules_ok &= PressureSensors::begin();
all_modules_ok &= TemperatureSensors::begin();
all_modules_ok &= ThrottleValves::begin();
Expand All @@ -77,6 +80,9 @@ void loop() {
while (CommsSerial.available()) {
CommandRouter::receive_byte(CommsSerial.read());
}

// TODO - send measured valve angles to Zucrow here with ZucrowInterface::send_valve_angles(), so Zucrow
// always has an angle readout. Needs ThrottleValves to expose the encoder angles.
}

// TODO - these?
Expand Down
Loading