From 02982994021518417ab2cdd0aa4dd6c032192758 Mon Sep 17 00:00:00 2001 From: Peter McGoron Date: Fri, 16 Sep 2022 18:01:34 -0400 Subject: [PATCH] add everything im working on --- COPYING | 26 ++ firmware/rtl/control_loop/boothmul.v | 107 +++++ firmware/rtl/control_loop/control_loop.v | 396 ++++++++++++++++++ firmware/rtl/control_loop/intsat.v | 45 ++ firmware/rtl/spi/spi_master.v | 6 +- firmware/rtl/spi/spi_master_no_read.v | 3 + firmware/verilator/boothmul_sim.cpp | 76 ++++ .../control_loop/control_loop_sim.cpp | 39 ++ firmware/verilator/control_loop/top.v | 42 ++ firmware/verilator/intsat_sim.cpp | 55 +++ software/manual_dhcp.c | 42 ++ software/src/buf.c | 97 +++++ software/src/buf.h | 20 + software/src/jsmn.h | 33 +- software/src/main.c | 64 ++- software/src/msg.c | 211 ++++++++++ software/src/msg.h | 54 +++ software/src/sock.c | 93 ++++ software/src/sock.h | 19 + 19 files changed, 1407 insertions(+), 21 deletions(-) create mode 100644 COPYING create mode 100644 firmware/rtl/control_loop/boothmul.v create mode 100644 firmware/rtl/control_loop/control_loop.v create mode 100644 firmware/rtl/control_loop/intsat.v create mode 100644 firmware/rtl/spi/spi_master_no_read.v create mode 100644 firmware/verilator/boothmul_sim.cpp create mode 100644 firmware/verilator/control_loop/control_loop_sim.cpp create mode 100644 firmware/verilator/control_loop/top.v create mode 100644 firmware/verilator/intsat_sim.cpp create mode 100644 software/manual_dhcp.c create mode 100644 software/src/buf.c create mode 100644 software/src/buf.h create mode 100644 software/src/msg.c create mode 100644 software/src/msg.h create mode 100644 software/src/sock.c create mode 100644 software/src/sock.h diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..65db12a --- /dev/null +++ b/COPYING @@ -0,0 +1,26 @@ +Licenses for other components: + +software/src/jsmn.h +=================== + +MIT License + +Copyright (c) 2010 Serge Zaitsev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/firmware/rtl/control_loop/boothmul.v b/firmware/rtl/control_loop/boothmul.v new file mode 100644 index 0000000..79bfde8 --- /dev/null +++ b/firmware/rtl/control_loop/boothmul.v @@ -0,0 +1,107 @@ +/* Booth Multiplication v0.1 + * Written by Peter McGoron, 2022. + */ + +module boothmul +#( + parameter A1_LEN = 32, + parameter A2_LEN = 32, + // AZLEN_SIZ = floor(log2(A2_LEN + 2) + 1). + // It must be able to store A2_LEN + 2. + parameter A2LEN_SIZ = 6 +) +( + input clk, + input arm, + input [A1_LEN-1:0] a1, + input [A2_LEN-1:0] a2, + output [A1_LEN+A2_LEN-1:0] outn, + output reg fin +); + +/*********************** + * Booth Parameters + **********************/ + +localparam OUT_LEN = A1_LEN + A2_LEN; +localparam REG_LEN = OUT_LEN + 2; + +/* The Booth multiplication algorithm is a sequential algorithm for + * twos-compliment integers. + * + * Let REG_LEN be equal to 1 + len(a1) + len(a2) + 1. + * Let P, S, and A be of length REG_LEN. + * Let A = a1 << len(a2) + 1, where a1 sign extends to the upper bit. + * Let S = -a1 << len(a2) + 1, where a1 sign extens to the upper bit. + * Let P = a2 << 1. + * + * Repeat the following len(a2) times: + * case(P[1:0]) + * 2'b00, 2'b11: P <= P >>> 1; + * 2'b01: P <= (P + A) >>> 1; + * 2'b10: P <= (P + S) >>> 1; + * endcase + * The final value is P[REG_LEN-2:1]. + * + * Wires and registers of REG_LEN length are organized like: + * + * /Overflow bit + * [M][ REG_LEN ][0] + * [M][ A1_LEN ][ A2_LEN ][0] + */ + +reg signed [REG_LEN-1:0] a; +reg signed [REG_LEN-1:0] s; +reg signed [REG_LEN-1:0] p = 0; + +assign outn[OUT_LEN-1:0] = p[REG_LEN-2:1]; + +/********************** + * Loop Implementation + *********************/ + +reg[A2LEN_SIZ-1:0] loop_accul = 0; + +always @ (posedge clk) begin + if (!arm) begin + loop_accul <= 0; + fin <= 0; + end else if (loop_accul == 0) begin + p[0] <= 0; + p[A2_LEN:1] <= a2; + p[REG_LEN-1:A2_LEN+1] <= 0; + + a[A2_LEN:0] <= 0; + a[REG_LEN-2:A2_LEN + 1] <= a1; + a[REG_LEN-1] <= a1[A1_LEN-1]; // Sign extension + + s[A2_LEN:0] <= 0; + // Extend before negation to ensure size + s[REG_LEN-1:A2_LEN+1] <= ~{a1[A1_LEN-1],a1} + 1; + + loop_accul <= loop_accul + 1; + end else if (loop_accul < A2_LEN + 1) begin + /* The loop counter starts from 1, so it must go to + * A2_LEN + 1 exclusive. + * (i = 0; i < len; i++) + * becomes (i = 1; i < len + 1; i++) + */ + loop_accul <= loop_accul + 1; + case (p[1:0]) + 2'b00, 2'b11: p <= p >>> 1; + 2'b10: p <= (p + s) >>> 1; + 2'b01: p <= (p + a) >>> 1; + endcase + end else begin + fin <= 1; + end +end + +`ifdef BOOTH_SIM +initial begin + $dumpfile("booth.vcd"); + $dumpvars; +end +`endif + +endmodule diff --git a/firmware/rtl/control_loop/control_loop.v b/firmware/rtl/control_loop/control_loop.v new file mode 100644 index 0000000..cbef1b8 --- /dev/null +++ b/firmware/rtl/control_loop/control_loop.v @@ -0,0 +1,396 @@ +/************ Introduction to PI Controllers + * The continuous form of a PI loop is + * + * A(t) = P e(t) + I∫ e(t')dt' + * where e(t) is the error (setpoint - measured), and + * the integral goes from 0 to the current time 't'. + * + * In digital systems the integral must be approximated. + * The normal way of doing this is a first-order approximation + * of the derivative of A(t). + * + * dA(t)/dt = P de(t)/dt + Ie(t) + * A(t_n) - A(t_{n-1}) ≅ P (e(t_n) - e(t_{n-1})) + Ie(t_n)Δt + * A(t_n) ≅ A(t_{n-1}) + e(t_n)(P + IΔt) - Pe(t_{n-1}) + * + * Using α = P + IΔt, and denoting A(t_{n-1}) as A_p, + * + * A ≅ A_p + αe - Pe_p. + * + * The formula above is what this module implements. This way, + * the controller only has to store two values between each + * run of the loop: the previous error and the previous output. + * This also reduces the amount of (redundant) computations + * the loop must execute each iteration. + * + * Calculating α requires knowing the precise timing of each + * control loop cycle, which in turn requires knowing the + * ADC and DAC timings. This is done outside the Verilog code. + * and can be calculated from simulating one iteration of the + * control loop. + * + *************** Fixed Point Integers ************* + * A regular number is stored in decimal: 123056. + * This is equal to + * 6*10^0 + 5*10^1 + 0*10^2 + 3*10^3 + 2*10^4 + 1*10^5. + * A whole binary number is only ones and zeros: 1101, and is + * equal to + * 1*2^0 + 0*2^1 + 1*2^2 + 1*2^3. + * + * Fixed-point integers shift the exponent of each number by a + * fixed amount. For instance, 123.056 is + * 6*10^-3 + 5*10^-2 + 0*10^-1 + 3*10^0 + 2*10^1 + 1*10^2. + * Similarly, the fixed point binary integer 11.01 is + * 1*2^-2 + 0*2^-1 + 1*2^0 + 1*2^1. + * + * To a computer, a whole binary number and a fixed point binary + * number are stored in exactly the same way: no decimal point + * is stored. It is only the *interpretation* of the data that + * changes. + * + * Fixed point numbers are denoted WHOLE.FRAC or [WHOLE].[FRAC], + * where WHOLE is the amount of whole number bits (including sign) + * and FRAC is the amount of fractional bits (2^-1, 2^-2, etc.). + * + * The rules for how many digits the output has given an input + * is the same for fixed point binary and regular decimals. + * + * Addition: W1.F1 + W2.F2 = [max(W1,W2)+1].[max(F1,F2)] + * Multiplication: W1.F1 * W2.F2 = [W1+W2].[F1+F2] + * + *************** Precision ************** + * The control loop is designed around these values, but generally + * does not hardcode them. + * + * Since α and P are precalculated outside of the loop, their + * conversion to numbers the loop understands is done outside of + * the loop and in the kernel. + * + * The 18-bit ADC is twos-compliment, -10.24V to 10.24V, + * with 78μV per increment. + * The 20-bit DAC is twos-compliment, -10V to 10V. + * + * The `P` constant has a minimum value of 1e-7 with a precision + * of 1e-9, and a maxmimum value of 1. + * + * The `I` constant has a minimum value of 1e-4 with a precision + * of 1e-6 and a maximum value of 100. + * + * Δt is cycles/100MHz. This makes Δt at least 10 ns, with a + * maximum of 1 ms. + * + * Intermediate values are 48-bit fixed-point integers multiplied + * by the step size of the ADC. The first 18 bits are the whole + * number and sign bits. This means intermediate values correspond + * exactly to values as understood by the ADC, with extra precision. + * + * To get the normal fixed-point value of an intermediate value, + * multiply it by 78e-6. To convert a normal fixed-point integer + * to an intermediate value, multiply it by 1/78e-6. In both + * cases, the conversion constant is a normal fixed-point integer. + * + * For instance, to encode the value 78e-6 as an intermediate + * value, multiply it by 1/78e-6 to obtain 1. Thus the value + * should be stored as 1 (whole bit) followed by zeros (fractional + * bits). + */ + +/* + * 0x3214*78e-6 = 0.9996 + lower order (storable as 15 whole bits) + */ + +`define ERR_WID (ADC_WID + 1) + +module control_loop +#( + parameter ADC_WID = 18, + /* Code assumes DAC_WID > ADC_WID. If/when this is not the + * case, truncation code must be changed. + */ + parameter DAC_WID = 24, + /* Analog Devices DACs have a register code in the upper 4 bits. + * The data follows it. + */ + parameter DAC_DATA_WID = 20, + parameter CONSTS_WID = 48, // larger than ADC_WID + parameter CONSTS_FRAC_WID = CONSTS_WID-15, + parameter DELAY_WID = 16, + /* [ERR_WID_SIZ-1:0] must be able to store + * ERR_WID (ADC_WID + 1). + */ + parameter ERR_WID_SIZ = 6, + parameter ADC_POLARITY = 1, + parameter ADC_PHASE = 0, + parameter DAC_POLARITY = 0, + parameter DAC_PHASE = 1 +) ( + input clk, + input arm, + + output adc_sck, + input adc_in, + output adc_conv, + + output dac_sck, + output dac_ss, + output dac_out, + + /* Informational output. + * These registers are also used for storing information while + * the loop is running. + */ + output signed [ERR_WID-1:0] err_cur, + output signed [CONSTS_WID-1:0] adj, + + input signed [ADC_WID-1:0] setpt_in, + input signed [CONSTS_WID-1:0] cl_alpha_in, + input signed [CONSTS_WID-1:0] cl_p_in, + input [DELAY_WID-1:0] delay_in +); + +/* Registers used to lock in values at the start of each iteration */ +reg signed [ADC_WID-1:0] setpt = 0; +reg signed [CONSTS_WID-1:0] cl_alpha_reg = 0; +reg signed [CONSTS_WID-1:0] cl_p_reg = 0; +reg [DELAY_WID-1:0] saved_delay = 0; + +/* Registers for PI calculations */ +reg signed [ERR_WID-1:0] err_prev = 0; + +/****** State machine + * + * -> WAIT_ON_ARM -> WAIT_ON_ADC -> WAIT_ON_MUL -\ + * \------\------------ WAIT_ON_DAC discard fractional bits: CONSTS_WHOLE+ADC_WID+1.(DAC_DATA_WID - ADC_WID) + * --> Saturate-truncate: ADC_WID.(DAC_DATA_WID-ADC_WID) + * --> reinterpret and write into DAC: DAC_DATA_WID.0 + */ + +/******* Get measured value ********/ + +reg signed [ADC_WID-1:0] measured = 0; +reg adc_arm = 0; +wire adc_finished; +spi_master_no_write #( + .WID(ADC_WID), + .POLARITY(ADC_POLARITY), + .PHASE(ADC_PHASE), + .CYCLE_HALF_WAIT(1), + .TIMER_LEN(3) +) adc ( + .clk(clk), + .from_slave(adcbuf), + .miso(adc_in), + .sck_wire(adc_sck), + .arm(adc_arm), + .finished(adc_finished) +); + +/**** Calculate Error ****/ +assign err_cur = measured - setpoint; + +/****** Multiplication ******* + * Truncation of a fixed-point integer to a smaller buffer requires + * 1) truncating higher order bits + * 2) removing lower order bits + * + * The ADC number has no fractional digits, so the fixed point output + * is [CONSTS_WHOLE + ERR_WID].CONSTS_FRAC_WID + * with total width CONSTS_WID + ERR_WID + * + * Both multipliers are armed at the same time. + * Their output wires are ANDed together so the state machine + * progresses when both are finished. + */ + +localparam MUL_WHOLE_WID = CONSTS_WHOLE + ERR_WID; +localparam MUL_FRAC_WID = CONSTS_FRAC; +localparam MUL_WID = MUL_WHOLE_WID + MUL_FRAC_WID; + +reg arm_mul = 0; + +wire alpha_err_fin; +wire signed [MUL_WID-1:0] alpha_err; +wire p_err_prev_fin; +wire signed [MUL_WID-1:0] p_err_prev; + +wire mul_finished = alpha_err_prev_fin & p_err_fin; + +/* αe */ +boothmul #( + .A1_LEN(CONSTS_WID), + .A2_LEN(ERR_WID), + .A2LEN_SIZ(ERR_WID_SIZ) +) boothmul_alpha_err_mul ( + .clk(clk), + .arm(arm_mul), + .a1(cl_alpha_reg), + .a2(err), + .outn(alpha_err), + .fin(alpha_err_fin) +); + +/* Pe_p */ +boothmul #( + .A1_LEN(CONSTS_WID), + .A2_LEN(ERR_WID), + .A2LEN_SIZ(ERR_WID_SIZ) +) booth_mul_P_err_mul ( + .clk(clk), + .arm(arm_mul), + .a1(cl_p_reg), + .a2(err_prev), + .outn(p_err_prev), + .fin(p_err_prev_fin) +); + +/**** Subtraction after multiplication ****/ +localparam SUB_WHOLE_WID = MUL_WHOLE_WID + 1; +localparam SUB_FRAC_WID = MUL_FRAC_WID; +localparam SUB_WID = SUB_WHOLE_WID + SUB_FRAC_WID; + +reg signed [SUB_WID-1:0] adj_old; +wire signed [SUB_WID-1:0] newadj = adj_old + alpha_err - p_err_prev; + +/**** Discard fractional bits **** + * The truncation of the subtraction result first takes off the lower + * order bits: + * [ SUB_WHOLE_WID ].[ SUB_FRAC_WID ] + * [ SUB_WHOLE_WID ].[RTRUNC_FRAC_WID]############ + * (SUB_FRAC_WID - RTRUNC_FRAC_WID)^ + */ + +localparam RTRUNC_FRAC_WID = DAC_DATA_WID - ADC_WID; +localparam RTRUNC_WHOLE_WID = SUB_WHOLE_WID; +localparam RTRUNC_WID = RTRUNC_WHOLE_WID + RTRUNC_FRAC_WID; + +wire signed rtrunc[RTRUNC_WID-1:0] = + newadj[SUB_WID-1:SUB_FRAC_WID-RTRUNC_FRAC_WID]; + +/**** Truncate-Saturate **** + * Truncate the result into a value acceptable to the DAC. + * [ SUB_WHOLE_WID ].[RTRUNC_FRAC_WID]############ + * [ADC_WID].[DAC_DATA_WID - ADC_WID] + * reinterpreted as + * [DAC_DATA_WID].0 + */ + +wire signed dac_adj_val[DAC_DATA_WID-1:0]; + +intsat #( + .IN_LEN(RTRUNC_WID), + .LTRUNC(DAC_DATA_WID) +) ( + .inp(newadj_rtrunc), + .outp(dac_adj_val) +); + +/**** Write to DAC ****/ + +reg dac_arm = 0; +spi_master_no_read #( + .WID(DAC_WID), + .WID_LEN(5), + .CYCLE_HALF_WAIT(3), + .TIMER_LEN(3), + .POLARITY(DAC_POLARITY), + .PHASE(DAC_PHASE) +) dac ( + .clk(clk), + .to_slave({4'b0010,dac_adj_val}), + .mosi(dac_out), + .sck_wire(dac_sck), + .arm(dac_arm) +); + +reg [DELAY_WID-1:0] timer = 0; + +always @ (posedge clk) begin + case (state) + WAIT_ON_ARM: begin + if (!arm) begin + adj_prev <= 0; + err_prev <= 0; + timer <= 0; + end else if (timer == 0) begin + saved_delay <= delay_in; + timer <= 1; + end else if (timer < saved_delay) begin + timer <= timer + 1; + setpt <= setpt_in; + cl_alpha_reg <= cl_alpha_in; + cl_p_reg <= cl_p_in; + end else begin + state <= WAIT_ON_ADC; + timer <= 0; + adc_arm <= 1; + end + end + WAIT_ON_ADC: if (adc_finished) begin + adc_arm <= 0; + arm_mul <= 1; + state <= WAIT_ON_MUL; + end + WAIT_ON_MUL: if (mul_finished) begin + arm_mul <= 0; + dac_arm <= 1; + state <= WAIT_ON_DAC; + end + WAIT_ON_DAC: if (dac_finished) begin + state <= WAIT_ON_ARM; + dac_arm <= 0; + end +end + +endmodule diff --git a/firmware/rtl/control_loop/intsat.v b/firmware/rtl/control_loop/intsat.v new file mode 100644 index 0000000..41f1d12 --- /dev/null +++ b/firmware/rtl/control_loop/intsat.v @@ -0,0 +1,45 @@ +/* Saturate integers. v0.1 + * Written by Peter McGoron, 2022. + */ + +module intsat +#( + parameter IN_LEN = 64, + parameter LTRUNC = 32 +) +( + input signed [IN_LEN-1:0] inp, + output signed [IN_LEN-LTRUNC-1:0] outp +); + +/* Truncating a twos-complement integer can cause overflow. + * To check for overflow, look at all truncated bits and + * the most significant bit that will be kept. + * If they are all 0 or all 1, then there is no truncation. + */ + +localparam INP_TRUNC_START = IN_LEN - LTRUNC; +localparam INP_CHECK_START = INP_TRUNC_START - 1; +localparam OUT_LEN = IN_LEN - LTRUNC; + +/* + * [ IN_LEN ] + * [ LTRUNC | OUT_LEN ] + * * & + * *: INP_TRUNC_START + * &: INP_CHECK_START +*/ + +always @ (*) begin + if (inp[IN_LEN-1:INP_CHECK_START] == {(LTRUNC + 1){inp[IN_LEN-1]}}) begin + outp[OUT_LEN-1:0] = inp[OUT_LEN-1:0]; + end else if (inp[IN_LEN-1]) begin + // most negative number: 1000000.... + outp[OUT_LEN-1:0] = {1'b1,{(OUT_LEN-1){1'b0}}}; + end else begin + // most positive number: 0111111.... + outp[OUT_LEN-1:0] = {1'b0,{(OUT_LEN-1){1'b1}}}; + end +end + +endmodule diff --git a/firmware/rtl/spi/spi_master.v b/firmware/rtl/spi/spi_master.v index cc5f01d..9169111 100644 --- a/firmware/rtl/spi/spi_master.v +++ b/firmware/rtl/spi/spi_master.v @@ -1,7 +1,5 @@ -/* (c) Peter McGoron 2022 - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v.2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. +/* SPI master. + * Written by Peter McGoron, 2022. */ module diff --git a/firmware/rtl/spi/spi_master_no_read.v b/firmware/rtl/spi/spi_master_no_read.v new file mode 100644 index 0000000..0999807 --- /dev/null +++ b/firmware/rtl/spi/spi_master_no_read.v @@ -0,0 +1,3 @@ +`define SPI_MASTER_NO_READ +/* verilator lint_off DECLFILENAME */ +`include "spi_master.v" diff --git a/firmware/verilator/boothmul_sim.cpp b/firmware/verilator/boothmul_sim.cpp new file mode 100644 index 0000000..0b697ee --- /dev/null +++ b/firmware/verilator/boothmul_sim.cpp @@ -0,0 +1,76 @@ +#include +#include +#include +#include +#include +#include "Vboothmul.h" + +using word = int16_t; +using dword = int32_t; + +constexpr word minint = std::numeric_limits::min(); +constexpr word maxint = std::numeric_limits::max(); + +uint32_t main_time = 0; + +double sc_time_stamp() { + return main_time; +} + +Vboothmul *mod; + +static void run_clock() { + mod->clk = !mod->clk; + mod->eval(); + main_time++; + + mod->clk = !mod->clk; + mod->eval(); + main_time++; +} + +static void run(word i, word j) { + // Processor is twos-compliment + mod->a1 = i; + mod->a2 = j; + mod->arm = 1; + + while (!mod->fin) + run_clock(); + + dword expected = (dword) i * (dword) j; + if (mod->outn != expected) { + std::cout << i << "*" << j << "=" << expected + << "(" << mod->outn << ")" << std::endl; + } + + mod->arm = 0; + run_clock(); +} + +int main(int argc, char **argv) { + Verilated::commandArgs(argc, argv); + // Verilated::traceEverOn(true); + mod = new Vboothmul; + + mod->clk = 0; + mod->arm = 0; + run_clock(); + + run(minint, minint); + run(minint, maxint); + run(maxint, minint); + run(maxint, maxint); + + for (word i = -20; i < 20; i++) { + for (word j = - 20; j < 20; j++) { + run(i, j); + } + } + + mod->final(); + + delete mod; + + return 0; +} diff --git a/firmware/verilator/control_loop/control_loop_sim.cpp b/firmware/verilator/control_loop/control_loop_sim.cpp new file mode 100644 index 0000000..a764900 --- /dev/null +++ b/firmware/verilator/control_loop/control_loop_sim.cpp @@ -0,0 +1,39 @@ +#include +#include +#include +#include +#include + +#include +#include "Vcontrol_loop.h" + +Vcontrol_loop *mod; + +/* Very simple simulation of measurement. + * A transfer function defines the mapping from the DAC values + * -2**(20) -> 2**(20)-1 + * to the values -2**(18) -> 2**18 - 1. + * + * The transfer function has Gaussian noise which is added at each + * measurement. + */ + +class Transfer { + std::random_device rd; + std::normal_distribution dist; + double scale; + + double sample() {return scale*dist(rd);} + + public: + Transfer(double scale, double mean, double dev, double m, double b) + : scale{scale}, rd{}, dist{mean,dev} {} + + double val(double x) { + return m*x + b + sample(); + } +}; + +int main(int argc, char **argv) { + +} diff --git a/firmware/verilator/control_loop/top.v b/firmware/verilator/control_loop/top.v new file mode 100644 index 0000000..6c516ff --- /dev/null +++ b/firmware/verilator/control_loop/top.v @@ -0,0 +1,42 @@ +module top +#( + parameter ADC_WID = 18, + parameter DAC_WID = 24, + parameter ADC_POLARITY = 1, + parameter ADC_PHASE = 0, + parameter DAC_DATA_WID = 20, + parameter CONSTS_WID = 48, + parameter DELAY_WID = 16 +( + input clk, + input signed [ADC_WID-1:0] read_data, + output signed [DAC_WID-1:0] write_data, + input signed [ADC_WID-1:0] setpt, + input signed [CONSTS_WID-1:0] alpha, + input signed [CONSTS_WID-1:0] cl_p, + input [DELAY_WID-1:0] dy, + + output signed [ADC_WID:0] err, + output signed [CONSTS_WID-1:0] adj +); + +wire adc_sck; +wire adc_ss; +wire adc_mosi; + +spi_slave_no_write #( + .WID(ADC_WID), + .WID(5), + . + +control_loop #( + .ADC_WID(ADC_WID), + .DAC_WID(DAC_WID), + .DAC_DATA_WID(DAC_DATA_WID), + .CONSTS_WID(CONSTS_WID), + .DELAY_WID(DELAY_WID) +) cloop ( + +); + +endmodule diff --git a/firmware/verilator/intsat_sim.cpp b/firmware/verilator/intsat_sim.cpp new file mode 100644 index 0000000..c391085 --- /dev/null +++ b/firmware/verilator/intsat_sim.cpp @@ -0,0 +1,55 @@ +#include +#include +#include +#include +#include +#include "Vintsat.h" + +using dword = int16_t; +using word = int8_t; + +double sc_time_stamp() { + return 0; +} + +Vintsat *mod; + +static void +run(dword i) +{ + const auto max = std::numeric_limits::max(); + const auto min = std::numeric_limits::min(); + mod->inp = i; + mod->eval(); + + int in = (dword) mod->inp; + int out = (word) mod->outp; + + if (i <= max && i >= min) { + if (in != out) + std::cout << in << "->" << out << std::endl; + } else { + if (i < min && out != min) + std::cout << in << "->" << out << std::endl; + else if (i > max && out != max) + std::cout << in << "->" << out << std::endl; + } +} + +int main(int argc, char **argv) { + Verilated::commandArgs(argc, argv); + mod = new Vintsat; + + dword i; + for (i = std::numeric_limits::min(); + i < std::numeric_limits::max(); + i++) + run(i); + run(i); + + mod->final(); + + delete mod; + + return 0; +} diff --git a/software/manual_dhcp.c b/software/manual_dhcp.c new file mode 100644 index 0000000..8e1dd17 --- /dev/null +++ b/software/manual_dhcp.c @@ -0,0 +1,42 @@ +K_SEM_DEFINE(dhcp_ready, 0, 1); + +static bool +check_dhcp(struct net_if *iface) +{ + // Scan IP addresses allocated for Unicast + for (int i = 0; i < NET_IF_MAX_IPV4_ADDR; i++) { + if (iface->config.ip.ipv4->unicast[i].addr_type != NET_ADDR_DHCP) + continue; + return true; + } + + return false; +} + +static void +dhcp_handler(struct net_mgmt_event_callback *cb, + uint32_t ev, + struct net_if *iface) +{ + if (ev != NET_EVENT_IPV4_ADDR_ADD) + return; + if (check_dhcp(iface)) + k_sem_give(&dhcp_ready); +} + +static void +setup_dhcp(void) +{ + + static struct net_mgmt_event_callback cb; + net_mgmt_init_event_callback(&cb, dhcp_handler, NET_EVENT_IPV4_ADDR_ADD); + net_mgmt_add_event_callback(&cb); + + struct net_if *iface = net_if_get_default(); + + if (!check_dhcp(iface)) { + net_dhcpv4_start(iface); + k_sem_take(&dhcp_ready, K_FOREVER); + } +} + diff --git a/software/src/buf.c b/software/src/buf.c new file mode 100644 index 0000000..4e0e540 --- /dev/null +++ b/software/src/buf.c @@ -0,0 +1,97 @@ +#include +#include +#include +#include "buf.h" + +/* Read from the socket into the buffer. + * This function is meant to be called multiple times on the + * same struct. The controller loads bp->left with the amount + * of bytes it wishes to read, and continues until bp->left == 0. + * + * This function returns false if there was an error reading + * from the socket. + * A read of 0 bytes returns true. + */ +bool +buf_read_sock(int sock, struct bufptr *bp) +{ + ssize_t l = zsock_recv(sock, bp->p, bp->left, 0); + if (l < 0) + return false; + + bp->left -= l; + bp->p += l; + return true; +} + +/* Write from the bufptr into a socket. + * This function is meant to be called once per prepared bufptr. + * + * This function returns false if there was an error on the + * socket. + */ +bool +buf_write_sock(int sock, struct bufptr *bp) +{ + while (bp->left) { + ssize_t l = zsock_send(sock, bp->p, bp->left, 0); + if (l < 0) + return false; + bp->p += l; + bp->left -= l; + } + + return true; +} + +/* Write a formatted string to bp. + * This function uses printf(), which means that it deals with + * writing _C-strings_, not unterminated buffers. + * When using this function, the buffer must be _one more_ than + * the maximum message length. For instance, a 1024-byte message + * should be in a 1025-byte buffer. HOWEVER, bp->left must still + * be set to the total length of the buffer (in the example, 1025). + * + * The final bufptr points to the NUL terminator, so that it + * is overwritten on each call to the function. + * + * This function returns 0 for a successful write, -1 for an + * encoding error (should never happen), and a positive value + * for the amount of bytes that could not fit. + */ +int +buf_writevf(struct bufptr *bp, const char *fmt, va_list va) +{ + int w; + + /* vsnprintk() returns the amount of bytes that would + * be stored in the buffer if the buffer was big enough, + * excluding the NUL terminator. + * The function will _always_ write a NUL terminator + * unless bp->left == 0. + */ + w = vsnprintk(bp->p, bp->left, fmt, va); + + if (w < 0) + return BUF_WRITE_ERR; + + if (w >= bp->left) { + size_t oldleft = bp->left; + buf->p += bp->left - 1; + bp->left = 1; + return w - oldleft + 1; + } else { + bp->p += w; + bp->left -= w; + return BUF_OK; + } +} + +int +buf_writef(struct bufptr *bp, const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + buf_writevf(bp, fmt, va); + va_end(va); +} diff --git a/software/src/buf.h b/software/src/buf.h new file mode 100644 index 0000000..104a7ee --- /dev/null +++ b/software/src/buf.h @@ -0,0 +1,20 @@ +#pragma once + +/* This is a pointer _into_ a buffer. It is increment + * (and the variable left decremented) after each + * operation. + */ +struct bufptr { + char *p; + size_t left; +}; + +enum { + BUF_OK = 0, + BUF_WRITE_ERR = -1 +}; + +bool buf_read_sock(int sock, struct bufptr *bp); +bool buf_write_sock(int sock, struct bufptr *bp); +int buf_writevf(struct bufptr *bp, const char *fmt, va_list va); +int buf_writef(struct bufptr *bp, const char *fmt, ...); diff --git a/software/src/jsmn.h b/software/src/jsmn.h index 07bda31..f31d447 100644 --- a/software/src/jsmn.h +++ b/software/src/jsmn.h @@ -1,4 +1,35 @@ -/* From: https://github.com/zserge/jsmn 25647e692c7906b96ffd2b05ca54c097948e879c +/* + * Jsmn is JSON parser that does not use the C standard library and + * does not allocate memory. Jsmn stores the structure of the JSON + * document in an array that records + * * the type of the token, + * * the start position and extent of the token, + * * and the number of elements inside the token. + * For example, the document + {"key":"val", "obj" : { "x" : 2, "y" : false }, "arr" : [1, "2", 3]} + * would contain parse to: + * OBJECT 6, STRING "key", STRING "val", STRING "obj", OBJECT 4, + * STRING "key", PRIMITIVE 2, STRING y, PRIMITIVE false, + * STRING "arr", ARRAY 3, PRIMITIVE 1, STRING "2", PRIMITIVE 3 + * The number next to OBJECT and ARRAY denote the size of each. + * Strings always start with a quote and end with one: primitives + * (numbers, booleans, null) do not. + * + * TOKENS ARE NOT NUL TERMINATED. However, because of how the JSON + * grammar works, the NUL terminator can be manually added. + * STRING ESCAPE SEQUENCES ARE NOT PARSED. + * Strings start at one-past the quotation mark, and the end index + * points to the end quotation mark. + * + * Why JSON? + * 1) Every language has a JSON library. + * 2) JSMN is perfectly suited to this program. + * 3) Another standard format would most likely require writing + * a parser by hand. + * 4) A minimalistic, custom format would require more debugging. + * + * From: https://github.com/zserge/jsmn 25647e692c7906b96ffd2b05ca54c097948e879c + * * MIT License * * Copyright (c) 2010 Serge Zaitsev diff --git a/software/src/main.c b/software/src/main.c index f68930b..e38d2cf 100644 --- a/software/src/main.c +++ b/software/src/main.c @@ -1,26 +1,58 @@ #include +#include +#include #include #include -#include "pin_io.h" + +#include "sock.h" + +LOG_MODULE_REGISTER(main); + +#define PORT 6626 +enum fds { + CLIENT_FD, + SCANDATA_FD, + MAXFDS +}; + +static void +process_client(int cli) +{ + struct zsock_pollfd fds[MAXFDS] = {0}; + struct clireadbuf readbuf = {0}; + + client_buf_reset(&readbuf); + + fds[CLIENT_FD].fd = cli; + fds[CLIENT_FD].events = ZSOCK_POLLIN; + // Currently not used + fds[SCANDATA_FD].fd = -1; + + while (zsock_poll(fds, MAXFDS, 0) >= 0) { + if (fds[CLIENT_FD].revents | POLLIN) { + if (!client_read_into_buf(cli, &readbuf)) { + INFO_WRN("client_read_into_buf: %d", errno); + goto cleanup; + } + + if (readbuf.st == MSG_READY) { + msg_parse_dispatch(cli, &readbuf); + client_buf_reset(&buf); + } + } + } +cleanup: +} void main(void) { - uint32_t v = 0; - uint32_t r = 0; - LOG_PRINTK("hello, world\n"); + int srv = server_init_sock(PORT); + for (;;) { - k_sleep(K_MSEC(1000)); -#if 0 // ADC code - *adc_conv[0] = v; - v = !v; - r = *adc_sdo[0]; -#endif - *dac_ctrl[0] = v; - v++; - if (v == 8) - v = 0; - r = *dac_miso[0]; - LOG_PRINTK("out: %d; in: %d\n", v, r); + int cli = server_get_client(server_sock); + process_client(cli); + LOG_INF("Closing client socket"); + zsock_close(cli); } } diff --git a/software/src/msg.c b/software/src/msg.c new file mode 100644 index 0000000..cb04afe --- /dev/null +++ b/software/src/msg.c @@ -0,0 +1,211 @@ +#include +#include +#include +#include +#include + +#define JSMN_PARENT_LINKS +#define JSMN_STRICT +#define JSMN_STATIC +#include "jsmn.h" +#include "sock.h" +#include "buf.h" + +LOG_MODULE_REGISTER(msg); + +enum cmdtype { + NONE, + IDENT, + CONSTS, + RAMP, + READ_ADC, + RESET_DAC, + RESET, + CMDTYPE_LEN +}; + +struct cmd { + enum cmdtype typ; + char *version; + char *msgid; + char *emsg; + union { + char *ident; + struct { + int P; + int I; + int Vnm; + } consts; + struct { + int dac; + int offset; + int dely; + } rampcmd; + int read_adc; + int reset_dac; + struct { + int id; + int blklen; + int siz; + } startscan; + }; +}; + +struct parser { + struct readbuf *js; // JSON buffer. + jsmntok_t *ctok; // Parser is at this token. + jsmntok_t *last; // Last token in the sequence. + int cli; // Socket. + struct cmd cmd; // Command parsed into. +}; + +enum { + OK = 0, + CONVERR = BUF_WRITE_ERR, + SOCKERR = BUF_WRITE_ERR - 1 +}; + +// Serialize struct cmd into JSON and send it. +static int +dispatch(int sock, struct cmd *cmd) +{ + // Add byte for NUL terminator for snprintf() + char buf[CLIREADBUF_SIZ + sizeof(uint16_t) + 1] = {0}; + struct bufptr bp = {buf, sizeof(buf)}; + int r; + + /* If no version could be read (i.e. sending a message + * saying that the JSON was invalid) then send a + * version 0 message. + */ + if (!cmd->version) + cmd->version = "0"; + + if ((r = buf_writef(&bp, "{\"version\":\"%s\"", cmd->version)) != BUF_OK) + return r; + + if (cmd->msgid) { + if ((r = buf_writef(&bp, ",\"msgid\":\"%s\"", cmd->msgid)) != BUF_OK) + return r; + } + if (cmd->emsg) { + if ((r = buf_writef(&bp, ",\"error\":\"%s\", cmd->emsg)) != BUF_OK) + return r; + goto send; + } + + if (!cmd->emsg) switch (cmd->typ) { + case IDENT: + if ((r = buf_writef(&bp, ",\"%s\":\"%s\", sl[IDENT].s, "cryosnom") != 0) + return r; + goto send; + case CONSTS: + // STUB + goto send; + case RAMPCMD: + // STUB + goto send; + case READ_ADC: + // STUB + goto send; + case READ_DAC: + // STUB + goto send; + case STARTSCAN: + // STUB + goto send; + } + +send: + if ((r = buf_writef(&bp, "}")) != 0) + return r; + + struct bufptr wptr = {buf, sizeof(buf) - bp.left}; + if (!buf_write_sock(sock, &wptr)) + return SOCKERR; + return OK; +} + +static inline bool +eq(jsmntok_t *tk, char *buf, const char *s, size_t slen) +{ + return (slen == buf->end - buf->start + && strncasecmp(s, &jr->js[buf->start], slen) == 0); +} +#define liteq(buf,tok,st) liteq((buf), (tok), (st), sizeof(st) - 1) + +static inline void +jsmn_term(char *buf, jsmntok_t *tok) +{ + buf[tok->end] = 0; +} + +static jsmntok_t +parse_consts(int sock, jsmntok_t *ct, int len, char *js) +{ + while (len > 0) { + if (liteq(ct, js, "P")) { + ct++; + len--; + if ( +} + +static bool +parse(int sock, jsmntok_t *first, jsmntok_t *last, char *js) +{ + if (first->type != JSMN_OBJECT) { + psr->obj.emsg = "malformed json (not an object)"; + goto fail; + } + + for (jsmntok_t *ct = first; ct < psr->last; ct++) { + if (liteq(ct, js, "version")) { + ct++; + if (!liteq(ct, js, "0")) { + psr->obj.emsg = "invalid version"; + goto fail; + } + jsmn_term(js, ct); + psr->version = js + ct->start; + } else if (liteq(ct, js, "msgid")) { + ct++; + jsmn_term(js, ct); + psr->msgid = js + ct->start; + } else if (liteq(ct, js, "ident")) { + ct++; + psr->typ = IDENT; + } else if (liteq(ct, js, "consts")) { + ct++; + psr->typ = CONSTS; + if (ct->type == JSMN_OBJECT) { + if (!(ct = parse_consts(sock, ct+1, ct->size, js))) + goto fail; + } + } + } + +fail: + return dispatch(&psr->obj); +} + +/* Read a JSON message, parse it, execute it, and respond to it. + */ +bool +msg_parse_dispatch(int cli, struct readbuf *buf) +{ + jsmn_parser psr; + jsmntok_t tokens[JSON_MAX_LEN]; + + struct cmd cmd = { + .typ = NONE + }; + + jsmn_init(&psr); + + if (jsmn_parse(buf->buf, buf->readlen, &tokens, JSON_MAX_LEN) < 0) { + psr.emsg = "malformed json"; + return dispatch(cli, &cmd); + } + + return parse(cli, &tokens[0], &tokens[JSON_MAX_LEN-1], buf); +} diff --git a/software/src/msg.h b/software/src/msg.h new file mode 100644 index 0000000..94a0beb --- /dev/null +++ b/software/src/msg.h @@ -0,0 +1,54 @@ +#pragma once + +/* Controller only supports JSON_MAX_LEN JSON tokens. + * A token is + * * the beginning of an object + * * A key + * * any value + * * the beginning of an array + +Due to a particular quirk of JSMN, the numbers +can also be strings. Numbers MUST be integers. +msgid SHOULD be a small number. + + Messages are formatted like +{ + "version" : num, // required, set to 0 + "msgid" : str, // optional + "error" : str, // only from controller, ignored from computer + + // and zero to one of + "ident" : null, + "consts" : { // all are optional + "P" : num, + "I" : num, + "Vnm" : num + }, // or null + "ramp" : { + "dac" : num, + "off" : num, + "dly" : num + }, + "read_adc" : num, + "reset_dac" : num, + "reset_all" : null, + "startscan" : null, // from computer + "startscan" : { // from controller + "id" : int, + "blksiz" : int, + "blklen" : int + } +} + +Both the controller and the computer read and write these messages. +*/ + +#define JSON_MAX_LEN 32 + +enum msg_ret { + MSG_OK, + MSG_BAD_JSON, + MSG_SOCKERR +}; + +enum msg_ret msg_parse_dispatch(int client, struct readbuf *buf); diff --git a/software/src/sock.c b/software/src/sock.c new file mode 100644 index 0000000..dcd8cf9 --- /dev/null +++ b/software/src/sock.c @@ -0,0 +1,93 @@ +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(sock); + +int +server_init_sock(int port) +{ + int sock; + + sock = zsock_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + + if (sock < 0) { + LOG_ERR("error: socket: %d", sock); + k_fatal_halt(K_ERR_KERNEL_PANIC); + } + + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_addr = {.s_addr = htonl(INADDR_ANY)}, + .sin_port = htons(port) + }; + + if (zsock_bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + LOG_ERR("error: bind: %d", errno); + k_fatal_halt(K_ERR_KERNEL_PANIC); + } + + if (zsock_listen(sock, 2) < 0) { + LOG_ERR("error: listen: %d", errno); + k_fatal_halt(K_ERR_KERNEL_PANIC); + } + + LOG_INF("Upsilon waiting on %d", port); + + return sock; +} + +int +server_accept_client(int server) +{ + int client; + struct sockaddr_in addr; + socklen_t len = sizeof(addr); + + do { + client = zsock_accept(server, (struct sockaddr *)&addr, &len); + if (client < 0) + LOG_WRN("error in accept: %d", errno); + } while (client < 0); + + char ipaddr[32]; + zsock_inet_ntop(addr.sin_family, &addr.sin_addr, ipaddr, sizeof(ipaddr)); + LOG_INF("Connection received from %s", ipaddr); + return client; +} + +bool +client_read_into_buf(int sock, struct clireadbuf *buf) +{ + if (buf->st == MSG_READY) { + LOG_WRN("%s called while MSG_READY: misuse", __func__); + return true; + } + + if (!buf_read_sock(sock, &buf->b)) + return false; + + if (buf->b.left == 0) switch (buf->st) { + case WAIT_ON_HEADER: { + uint16_t len; + memcpy(&len, buf->buf, sizeof(len)); + buf->b.left = ntohs(len); + buf->st = READING_CLIENT; + break; + } case READING_CLIENT: + buf->st = MSG_READY; + break; + } + + return true; +} + +void +client_buf_reset(struct clireadbuf *buf) +{ + buf->st = WAIT_ON_HEADER; + buf->b.p = buf->buf; + buf->b.left = sizeof(uint16_t); +} diff --git a/software/src/sock.h b/software/src/sock.h new file mode 100644 index 0000000..d3147b7 --- /dev/null +++ b/software/src/sock.h @@ -0,0 +1,19 @@ +#pragma once +#include "buf.h" +int server_init_sock(int port); +int server_accept_client(int server); + +#define CLIREADBUF_SIZ 1024 +enum clireadbuf_state { + WAIT_ON_HEADER, + READING_CLIENT, + MSG_READY +}; +struct clireadbuf { + struct bufptr b; + enum clireadbuf_state st; + char buf[CLIREADBUF_SIZ]; +}; + +bool client_read_into_buf(int sock, struct clireadbuf *buf); +void client_buf_reset(struct clireadbuf *buf);