From 4614183f38f02c3c3dafb2948c944e326c2c5767 Mon Sep 17 00:00:00 2001 From: Edward Emelianov Date: Wed, 6 May 2026 23:30:36 +0300 Subject: [PATCH] change proto to text strings; add simplest NTC measurement --- F3:F303/MLX90640-allsky/Makefile | 2 + F3:F303/MLX90640-allsky/adc.c | 147 +++- F3:F303/MLX90640-allsky/adc.h | 21 +- F3:F303/MLX90640-allsky/allsky.bin | Bin 22476 -> 39000 bytes F3:F303/MLX90640-allsky/commproto.cpp | 627 ++++++++++++++++++ F3:F303/MLX90640-allsky/commproto.h | 61 ++ F3:F303/MLX90640-allsky/hardware.c | 20 +- F3:F303/MLX90640-allsky/hardware.h | 11 +- .../MLX90640-allsky/ir-allsky.creator.user | 11 +- F3:F303/MLX90640-allsky/ir-allsky.files | 4 +- F3:F303/MLX90640-allsky/main.c | 15 +- F3:F303/MLX90640-allsky/mlxproc.c | 6 +- F3:F303/MLX90640-allsky/mlxproc.h | 1 + F3:F303/MLX90640-allsky/proto.c | 539 --------------- F3:F303/MLX90640-allsky/proto.h | 30 - F3:F303/MLX90640-allsky/version.inc | 4 +- 16 files changed, 878 insertions(+), 621 deletions(-) create mode 100644 F3:F303/MLX90640-allsky/commproto.cpp create mode 100644 F3:F303/MLX90640-allsky/commproto.h delete mode 100644 F3:F303/MLX90640-allsky/proto.c delete mode 100644 F3:F303/MLX90640-allsky/proto.h diff --git a/F3:F303/MLX90640-allsky/Makefile b/F3:F303/MLX90640-allsky/Makefile index 094adbe..555c167 100644 --- a/F3:F303/MLX90640-allsky/Makefile +++ b/F3:F303/MLX90640-allsky/Makefile @@ -8,3 +8,5 @@ LDLIBS := -lm include ../makefile.f3 include ../../makefile.stm32 + +$(OBJDIR)/commproto.o: commproto.cpp $(VERSION_FILE) diff --git a/F3:F303/MLX90640-allsky/adc.c b/F3:F303/MLX90640-allsky/adc.c index b0a92cf..1dc8689 100644 --- a/F3:F303/MLX90640-allsky/adc.c +++ b/F3:F303/MLX90640-allsky/adc.c @@ -16,17 +16,26 @@ * along with this program. If not, see . */ +#include + +#ifdef EBUG +#include "strfunc.h" +#endif + #include "adc.h" /** * @brief ADCx_array - arrays for ADC channels with median filtering: * ADC1: - * 0 - Ch0 - ADC1_IN1 - * 1 - Ch1 - ADC1_IN2 - * 2 - internal Tsens - ADC1_IN16 - * 3 - Vref - ADC1_IN18 + * 0 - Ch0 - ADC1_IN1 - NTC1 + * 1 - Ch1 - ADC1_IN2 - NTC2 + * 2 - Ch2 - ADC1_IN3 - NTC3 + * 3 - Ch3 - ADC1_IN4 - NTC4 + * 4 - internal Tsens - ADC1_IN16 + * 5 - Vref - ADC1_IN18 * ADC2: - * 4 - AIN5/DAC_OUT1 - PA4 - DAC1_OUT1 (onboard heater?), PA5 - ADC2_IN2 (DAC output control) + * AIN5/DAC_OUT1 - PA4 - DAC1_OUT1 (onboard heater) + * 6 - PA5 - ADC2_IN2 (DAC output control) */ static uint16_t ADC_array[NUMBER_OF_ADC_CHANNELS*9]; @@ -65,17 +74,19 @@ TRUE_INLINE void enADC(ADC_TypeDef *chnl){ * ADC1 - DMA1_ch1 * ADC2 - DMA2_ch1 */ -// Setup ADC and DAC +// Setup ADC and DAC; ADC/DAC pins should be prepared in gpio_setup void adc_setup(){ RCC->AHBENR |= RCC_AHBENR_ADC12EN; // Enable clocking ADC12_COMMON->CCR = ADC_CCR_TSEN | ADC_CCR_VREFEN | ADC_CCR_CKMODE; // enable Tsens and Vref, HCLK/4 calADC(ADC1); calADC(ADC2); - // ADC1: channels 1,2,16,18; ADC2: channel 2 - ADC1->SMPR1 = ADC_SMPR1_SMP0 | ADC_SMPR1_SMP1; + // ADC1: channels 1,2,3,4,16,18 + ADC1->SMPR1 = ADC_SMPR1_SMP0 | ADC_SMPR1_SMP1 | ADC_SMPR1_SMP2 | ADC_SMPR1_SMP3; ADC1->SMPR2 = ADC_SMPR2_SMP15 | ADC_SMPR2_SMP17; - // 4 conversions in group: 1->2->16->18 - ADC1->SQR1 = (1<<6) | (2<<12) | (16<<18) | (18<<24) | (NUMBER_OF_ADC1_CHANNELS-1); + // 6 conversions in group: 1->2->3->4->16->18 + ADC1->SQR1 = (1<<6) | (2<<12) | (3<<18) | (4<<24) | (NUMBER_OF_ADC1_CHANNELS-1); + ADC1->SQR2 = (16<<0) | (18<<6); + // ADC2: channel 2 ADC2->SMPR1 = ADC_SMPR1_SMP1; ADC2->SQR1 = (2<<6) | (NUMBER_OF_ADC2_CHANNELS-1); // configure DMA for ADC @@ -109,7 +120,8 @@ void adc_setup(){ * @param nch - number of channel * @return */ -uint16_t getADCval(int nch){ +uint16_t getADCval(uint8_t nch){ + if(nch >= NUMBER_OF_ADC_CHANNELS) return 0; register uint16_t temp; #define PIX_SORT(a,b) { if ((a)>(b)) PIX_SWAP((a),(b)); } #define PIX_SWAP(a,b) { temp=(a);(a)=(b);(b)=temp; } @@ -131,7 +143,7 @@ uint16_t getADCval(int nch){ } // get voltage @input nch (V) -float getADCvoltage(int nch){ +float getADCvoltage(uint8_t nch){ float v = getADCval(nch); v *= getVdd(); v /= 4096.f; // 12bit ADC @@ -155,3 +167,114 @@ float getVdd(){ vdd /= getADCval(ADC_VREF); return vdd; } + +// R lookup table for T=-10..59 degreesC +#if 0 +T=[-10:59]+273.15; +R=1000*exp(3950*(1./T-1/298.15)); +for i=1:length(T); printf("\t%.1f,\t// %d \n", R(i), T(i)-273.15); endfor +#endif + +static const float Rlut[] = { + 5824.6, // -10 + 5502.8, // -9 + 5201.1, // -8 + 4917.9, // -7 + 4652.2, // -6 + 4402.6, // -5 + 4168.1, // -4 + 3947.7, // -3 + 3740.5, // -2 + 3545.5, // -1 + 3362.1, // 0 + 3189.3, // 1 + 3026.6, // 2 + 2873.3, // 3 + 2728.8, // 4 + 2592.5, // 5 + 2463.9, // 6 + 2342.5, // 7 + 2227.9, // 8 + 2119.7, // 9 + 2017.5, // 10 + 1920.8, // 11 + 1829.4, // 12 + 1743.0, // 13 + 1661.2, // 14 + 1583.7, // 15 + 1510.4, // 16 + 1440.9, // 17 + 1375.1, // 18 + 1312.7, // 19 + 1253.5, // 20 + 1197.4, // 21 + 1144.1, // 22 + 1093.6, // 23 + 1045.6, // 24 + 1000.0, // 25 + 956.7, // 26 + 915.5, // 27 + 876.4, // 28 + 839.1, // 29 + 803.7, // 30 + 770.0, // 31 + 737.9, // 32 + 707.4, // 33 + 678.3, // 34 + 650.6, // 35 + 624.1, // 36 + 598.9, // 37 + 574.9, // 38 + 552.0, // 39 + 530.1, // 40 + 509.3, // 41 + 489.4, // 42 + 470.3, // 43 + 452.2, // 44 + 434.8, // 45 + 418.2, // 46 + 402.4, // 47 + 387.2, // 48 + 372.7, // 49 + 358.8, // 50 + 345.5, // 51 + 332.8, // 52 + 320.7, // 53 + 309.0, // 54 + 297.8, // 55 + 287.1, // 56 + 276.9, // 57 + 267.1, // 58 + 257.7, // 59 +}; + +#define LUTSZ (sizeof(Rlut) / sizeof(float)) + +/** + * @brief getNTCtemp - stupid LUT-search and linear approximation of T by R + * @param nch - channel of ADC for Tx + * @return temperature in degr.C + */ +float getNTCtemp(uint8_t nch){ + if(nch > ADC_AIN4) return -300.f; // bad number + uint16_t val = getADCval(nch); + if(val < 5) return -400.f; // short cirquit + else if(val > 4090) return -500.f; // no NTC + float R = 1000.f / (4096.f / val - 1.f); // resistance of NTC +#ifdef EBUG + USB_sendstr("R="); USB_sendstr(float2str(R, 1)); newline(); +#endif + int left = 0, right = LUTSZ-1; + if(R > Rlut[0]) right = 1; + else if(R < Rlut[LUTSZ-1]) left = LUTSZ-2; + while(right - left > 1){ + int idx = left + (right - left) / 2; + float Rl = Rlut[idx]; + if(Rl > R) left = idx + 1; + else right = idx - 1; + } + if(left >= (int)LUTSZ) return 60.f; + float Rleft = Rlut[left], Rright = Rlut[left+1]; + float T = (float)left - 9.f - (R - Rright) / (Rleft - Rright); + return T; +} diff --git a/F3:F303/MLX90640-allsky/adc.h b/F3:F303/MLX90640-allsky/adc.h index 77db4da..eb18570 100644 --- a/F3:F303/MLX90640-allsky/adc.h +++ b/F3:F303/MLX90640-allsky/adc.h @@ -19,22 +19,27 @@ #pragma once #include -#define NUMBER_OF_ADC1_CHANNELS (4) +// 4 sensors on 1..4, TS (16) and Vdd (18) +#define NUMBER_OF_ADC1_CHANNELS (6) #define NUMBER_OF_ADC2_CHANNELS (1) // total number of channels - for array #define NUMBER_OF_ADC_CHANNELS ((NUMBER_OF_ADC1_CHANNELS+NUMBER_OF_ADC2_CHANNELS)) // channels of ADC in array -#define ADC_AIN0 (0) -#define ADC_AIN1 (1) -#define ADC_TS (2) -#define ADC_VREF (3) -#define ADC_AIN5 (4) +#define ADC_AIN1 (0) +#define ADC_AIN2 (1) +#define ADC_AIN3 (2) +#define ADC_AIN4 (3) +#define ADC_NTCIN(x) ((x)-1) +#define ADC_TS (4) +#define ADC_VREF (5) +#define ADC_DACIN (6) // starting index of ADC2 #define ADC2START (9*NUMBER_OF_ADC1_CHANNELS) void adc_setup(); float getMCUtemp(); float getVdd(); -uint16_t getADCval(int nch); -float getADCvoltage(int nch); +uint16_t getADCval(uint8_t nch); +float getADCvoltage(uint8_t nch); +float getNTCtemp(uint8_t nch); diff --git a/F3:F303/MLX90640-allsky/allsky.bin b/F3:F303/MLX90640-allsky/allsky.bin index 7bccbf24b366a8e6f5b01daa2d4f1b80104098c7..a2076548d05f8d8bacbab1893c6812fd93015246 100755 GIT binary patch literal 39000 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 literal 22476 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 H- + +extern "C"{ +#include +#include + +#include "adc.h" +#include "commproto.h" +#include "hardware.h" +#include "i2c.h" +#include "mlxproc.h" +#include "strfunc.h" +#include "usart.h" +#include "usb_dev.h" +} + +// image aquisition time +const char* const Timage = "TIMAGE"; + +// Global senders to send info into other interface +static int (*usb_sender)(const char*) = nullptr; +static int (*usart_sender)(const char*) = nullptr; +static int (*usb_putb)(uint8_t) = nullptr; +static int (*usart_putb)(uint8_t) = nullptr; +static int (*usb_sendbin)(const uint8_t*, int) = nullptr; +static int (*usart_sendbin)(const uint8_t*, int) = nullptr; + +// Function helpers +static int (*SEND)(const char*) = nullptr; +static int (*putb)(uint8_t) = nullptr; +static int (*sendbin)(const uint8_t*, int) = nullptr; + +#define N() putb('\n') +#define printu(x) SEND(u2str(x)) +#define printi(x) SEND(i2str(x)) +#define printuhex(x) SEND(uhex2str(x)) +#define printfl(x,n) SEND(float2str(x, n)) + +void set_senders(int (*usbs)(const char *), + int (*usbb)(uint8_t), + int (*usbbin)(const uint8_t *, int), + int (*usarts)(const char *), + int (*usartb)(uint8_t), + int (*usartbin)(const uint8_t *, int)){ + usb_sender = usbs; + usb_putb = usbb; + usb_sendbin = usbbin; + usart_sender = usarts; + usart_putb = usartb; + usart_sendbin = usartbin; +} + +// Local buffer for I2C data +#define LOCBUFFSZ 32 +static uint16_t locBuffer[LOCBUFFSZ]; +// default I2C address of sensor +static uint8_t I2Caddress = 0x33 << 1; +extern volatile uint32_t Tms; +// show `cartoon` - continuously draw ASCII image of current sensor +uint8_t cartoon = 0; + +// Command list +#define COMMAND_TABLE \ + COMMAND(help, "show this help") \ + COMMAND(ascii, "draw nth image in ASCII (n=0..4)") \ + COMMAND(binary, "get nth image as text array of floats") \ + COMMAND(listids, "list active sensors IDs") \ + COMMAND(tempmap, "show temperature map of nth image") \ + COMMAND(acqtime, "show nth image aquisition time") \ + COMMAND(bmereinit, "reinit BME280") \ + COMMAND(environ, "get environment parameters") \ + COMMAND(state, "get MLX state") \ + COMMAND(reset, "reset MCU") \ + COMMAND(time, "print current Tms") \ + COMMAND(iicaddr, "get/set I2C address (non-shifted)") \ + COMMAND(mlxcont, "continue MLX") \ + COMMAND(iicspeed, "get/set I2C speed (0..4)") \ + COMMAND(mlxpause, "pause MLX") \ + COMMAND(mlxstop, "stop MLX") \ + COMMAND(adc, "get n'th ADC values") \ + COMMAND(ntc, "get n'th NTC temperatures") \ + COMMAND(cartoon, "toggle cartoon mode") \ + COMMAND(mlxdump, "dump MLX parameters for sensor n") \ + COMMAND(mlxaddr, "get/set MLX address of sensor n") \ + COMMAND(readreg, "read I2C register: readreg reg [= nwords]") \ + COMMAND(writedata, "write I2C data: writedata = val1 val2 ...") \ + COMMAND(iicscan, "scan I2C bus") \ + COMMAND(mcutemp, "get MCU temperature") \ + COMMAND(mcuvdd, "get MCU Vdd") \ + COMMAND(dac, "get/set DAC value") \ + COMMAND(pwm, "get/set PWM for channel n (0..100%)") \ + COMMAND(sendstr, "send string to other interface: sendstr = text") + + +// Command prototypes +#define COMMAND(name, desc) static errcodes_t cmd_ ## name(const char*, char*); +COMMAND_TABLE +#undef COMMAND + +// descrtiptions for `help` +typedef struct { + const char *name; + const char *desc; +} CmdInfo; + +static const CmdInfo cmdInfo[] = { +#define COMMAND(name, desc) { #name, desc }, + COMMAND_TABLE +#undef COMMAND +}; + +// Text descriptions for error codes +static const char* errtxt[ERR_AMOUNT] = { + [ERR_OK] = "OK\n", + [ERR_BADCMD] = "BADCMD\n", + [ERR_BADPAR] = "BADPAR\n", + [ERR_BADVAL] = "BADVAL\n", + [ERR_WRONGLEN] = "WRONGLEN\n", + [ERR_CANTRUN] = "CANTRUN\n", + [ERR_BUSY] = "BUSY\n", + [ERR_OVERFLOW] = "OVERFLOW\n", +}; + +const char *EQ = " = "; // equal sign for getters + +// send `command = ` +#define CMDEQ() do{SEND(cmd); SEND(EQ);}while(0) +// send `commandXXX = ` +#define CMDEQP(x) do{SEND(cmd); SEND(u2str((uint32_t)x)); SEND(EQ);}while(0) + +/** + * @brief splitargs - get command parameter and setter from `args` + * @param args (i) - rest of string after command (like `1 = PU OD OUT`) + * @param parno (o) - parameter number or -1 if none + * @return setter (part after `=` without leading spaces) or NULL if none + */ +static const char *splitargs(char *args, int32_t *parno){ + if(!args) return NULL; + uint32_t U32; + const char *next = getnum(args, &U32); + int p = -1; + if(next != args && U32 <= MAXPARNO) p = U32; + if(parno) *parno = p; + next = strchr(next, '='); + if(next){ + if(*(++next)) next = omit_spaces(next); + if(*next == 0) next = NULL; + } + return next; +} + +/** + * @brief argsvals - split `args` into `parno` and setter's value + * @param args - rest of string after command + * @param parno (o) - parameter number or -1 if none + * @param parval - integer setter's value + * @return false if no setter or it's not a number, true - got setter's num + */ +static bool argsvals(char *args, int32_t *parno, int32_t *parval){ + const char *setter = splitargs(args, parno); + if(!setter) return false; + int32_t I32; + const char *next = getint(setter, &I32); + if(next != setter && parval){ + *parval = I32; + return true; + } + return false; +} + + +/************* List of proto functions for each command *************/ + +static errcodes_t cmd_help(const char*, char*){ + SEND(REPOURL); + for(size_t i = 0; i < sizeof(cmdInfo)/sizeof(cmdInfo[0]); ++i){ + SEND(cmdInfo[i].name); + SEND(" - "); + SEND(cmdInfo[i].desc); + SEND("\n"); + } + return ERR_AMOUNT; +} + +static errcodes_t cmd_time(const char* cmd, char*){ + CMDEQ(); + printu(Tms); N(); + return ERR_AMOUNT; +} + +static errcodes_t cmd_reset(const char*, char*){ + NVIC_SystemReset(); + return ERR_CANTRUN; // unreacheable +} + +// send acquisition time +static void imaqtime(uint8_t sensno){ + uint32_t T = mlx_lastimT(sensno); + SEND(Timage); printu(sensno); SEND(EQ); + printu(T); N(); +} + +// Common image command for ASCII/binary/tempmap +static errcodes_t image_cmd(char* args, int mode){ + int32_t sensno = -1; + splitargs(args, &sensno); + if(sensno < 0 || sensno >= N_SENSORS) return ERR_BADPAR; + fp_t *img = mlx_getimage(sensno); + if(!img) return ERR_CANTRUN; + + // Frame number + imaqtime(sensno); + + switch(mode){ + case 0: // tempmap + dumpIma(img); + break; + case 1: // ascii + drawIma(img); + break; + case 2: // binary + SEND("BINARY"); putb('0'+sensno); putb('='); + uint8_t *d = (uint8_t*)img; + uint32_t _2send = MLX_PIXNO * sizeof(float); + // send by portions of 256 bytes + while(_2send){ + uint32_t portion = (_2send > 256) ? 256 : _2send; + if(sendbin(d, portion)){ + _2send -= portion; + d += portion; + } + } + SEND("ENDIMAGE"); N(); + break; + } + return ERR_AMOUNT; +} + +static errcodes_t cmd_ascii(const char* , char* args){ + return image_cmd(args, 1); +} +static errcodes_t cmd_binary(const char* , char* args){ + return image_cmd(args, 2); +} +static errcodes_t cmd_tempmap(const char* , char* args){ + return image_cmd(args, 0); +} + +static errcodes_t cmd_acqtime(const char* , char* args){ + int32_t sensno = -1; + splitargs(args, &sensno); + if(sensno < 0 || sensno >= N_SENSORS) return ERR_BADPAR; + imaqtime(sensno); + return ERR_AMOUNT; +} + +static errcodes_t cmd_listids(const char*, char*){ + int N = mlx_nactive(); + if(!N) return ERR_CANTRUN; + uint8_t *ids = mlx_activeids(); + SEND("Found "); printu(N); SEND(" active sensors:\n"); + for(int i = 0; i < N_SENSORS; ++i){ + if(ids[i]){ + SEND("SENSID"); printu(i); SEND(EQ); printuhex(ids[i]>>1); N(); + } + } + return ERR_AMOUNT; +} + +static errcodes_t cmd_bmereinit(const char*, char*){ + if(bme_init()) return ERR_OK; + return ERR_CANTRUN; +} + +static errcodes_t cmd_environ(const char*, char*){ + bme280_t env; + if(!get_environment(&env)) return ERR_CANTRUN; + SEND("TEMPERATURE="); printfl(env.T, 2); N(); + SEND("SKYTEMPERATURE="); printfl(env.Tsky, 2); N(); + SEND("PRESSURE_HPA="); printfl(env.P/100.f, 2); N(); + SEND("PRESSURE_MM="); printfl(env.P * 0.00750062f, 2); N(); + SEND("HUMIDITY="); printfl(env.H, 2); N(); + SEND("TEMP_DEW="); printfl(env.Tdew, 1); N(); + SEND("T_MEASUREMENT="); printu(env.Tmeas); N(); + return ERR_OK; +} + +static errcodes_t cmd_state(const char* cmd, char*){ + static const char *states[] = { + [MLX_NOTINIT] = "not init", + [MLX_WAITPARAMS] = "wait parameters DMA read", + [MLX_WAITSUBPAGE] = "wait subpage", + [MLX_READSUBPAGE] = "wait subpage DMA read", + [MLX_RELAX] = "do nothing" + }; + mlx_state_t s = mlx_state(); + CMDEQ(); SEND(states[s]); N(); + return ERR_AMOUNT; +} + +/********** I2C commands **********/ + +static errcodes_t cmd_iicaddr(const char* cmd, char* args){ + int32_t addr; + if(argsvals(args, NULL, &addr)){ + if(addr < 0 || addr > 0x7f) return ERR_BADVAL; + I2Caddress = (uint8_t)(addr << 1); + mlx_sethwaddr(I2Caddress, addr); + return ERR_AMOUNT; + } + // getter + CMDEQ(); printuhex(I2Caddress >> 1); N(); + return ERR_AMOUNT; +} + +static errcodes_t cmd_mlxcont(const char*, char*){ + mlx_continue(); + return ERR_OK; +} + +static errcodes_t cmd_iicspeed(const char* cmd, char* args){ + static const char *speeds[] = {"10K","100K","400K","1M","2M"}; + int32_t speed; + // TODO: allow string parameter + if(argsvals(args, NULL, &speed)){ + if (speed < 0 || speed >= I2C_SPEED_AMOUNT) return ERR_BADVAL; + i2c_setup((i2c_speed_t)speed); + } + // getter + CMDEQ(); SEND(speeds[i2c_curspeed]); N(); + return ERR_AMOUNT; +} + +static errcodes_t cmd_mlxpause(const char*, char*){ + mlx_pause(); + return ERR_OK; +} + +static errcodes_t cmd_mlxstop(const char*, char*){ + mlx_stop(); + return ERR_OK; +} + +static errcodes_t cmd_adc(const char* cmd, char* args){ + if(!args){ // show all values + for(uint8_t i = 0; i < NUMBER_OF_ADC_CHANNELS; ++i){ + CMDEQP(i); printu(getADCval(i)); N(); + } + return ERR_AMOUNT; + } + int32_t addr; + splitargs(args, &addr); + if(addr < 0 || addr >= NUMBER_OF_ADC_CHANNELS) return ERR_BADPAR; + CMDEQP(addr); printu(getADCval(static_cast(addr))); N(); + return ERR_AMOUNT; +} + +static errcodes_t cmd_ntc(const char* cmd, char* args){ + if(!args){ // show all values + for(uint8_t i = 0; i <= ADC_AIN4; ++i){ + CMDEQP(i); printfl(getNTCtemp(i), 1); N(); + } + return ERR_AMOUNT; + } + int32_t addr; + splitargs(args, &addr); + if(addr < 0 || addr > ADC_AIN4) return ERR_BADPAR; + CMDEQP(addr); printfl(getNTCtemp(static_cast(addr)), 1); N(); + return ERR_AMOUNT; +} + +static errcodes_t cmd_cartoon(const char*, char*){ + // TODO: should be getter/setter! + cartoon = !cartoon; + return ERR_OK; +} + +static errcodes_t cmd_mlxdump(const char*, char* args){ + int32_t sensno = -1; + splitargs(args, &sensno); + if (sensno < 0 || sensno >= N_SENSORS) return ERR_BADPAR; + MLX90640_params *params = mlx_getparams(sensno); + if(!params) return ERR_CANTRUN; + + SEND("SENSNO="); printi(sensno); N(); + SEND("kVdd="); printi(params->kVdd); N(); + SEND("vdd25="); printi(params->vdd25); N(); + SEND("KvPTAT="); printfl(params->KvPTAT, 4); N(); + SEND("KtPTAT="); printfl(params->KtPTAT, 4); N(); + SEND("vPTAT25="); printi(params->vPTAT25); N(); + SEND("alphaPTAT="); printfl(params->alphaPTAT, 2); N(); + SEND("gainEE="); printi(params->gainEE); N(); + SEND("Pixel offset parameters:\n"); + dumpIma(params->offset); + SEND("K_talpha:\n"); + dumpIma(params->kta); + SEND("Kv: "); + for(int i = 0; i < 4; ++i) { printfl(params->kv[i], 2); putb(' '); } + N(); + SEND("cpOffset="); printi(params->cpOffset[0]); SEND(", "); printi(params->cpOffset[1]); N(); + SEND("cpKta="); printfl(params->cpKta, 2); N(); + SEND("cpKv="); printfl(params->cpKv, 2); N(); + SEND("tgc="); printfl(params->tgc, 2); N(); + SEND("cpALpha="); printfl(params->cpAlpha[0], 2); SEND(", "); printfl(params->cpAlpha[1], 2); N(); + SEND("KsTa="); printfl(params->KsTa, 2); N(); + SEND("Alpha:\n"); + dumpIma(params->alpha); + SEND("CT3="); printfl(params->CT[1], 2); N(); + SEND("CT4="); printfl(params->CT[2], 2); N(); + for(int i = 0; i < 4; ++i){ + SEND("KsTo"); putb('0'+i); putb('='); printfl(params->KsTo[i], 2); N(); + SEND("alphacorr"); putb('0'+i); putb('='); printfl(params->alphacorr[i], 2); N(); + } + return ERR_AMOUNT; +} + +static errcodes_t cmd_mlxaddr(const char* cmd, char* args){ + int32_t sensno = -1; + if(!args || !*args) { // without args: show global address + //CMDEQ(); printuhex(I2Caddress>>1); N(); + //return ERR_AMOUNT; + return ERR_BADPAR; + } + const char *setter = splitargs(args, &sensno); + if(sensno < 0 || sensno >= N_SENSORS) return ERR_BADPAR; + + if(setter){ // setter: set current address + uint32_t a; + const char *nxt = getnum(setter, &a); + if(nxt == setter || a > 0x7f) return ERR_BADVAL; + mlx_setaddr(sensno, (uint8_t)a); + return ERR_AMOUNT; + }else{ // getter + uint8_t a = mlx_getaddr(sensno); + CMDEQP(sensno); printuhex(a); N(); + return ERR_AMOUNT; + } +} + +static errcodes_t cmd_readreg(const char* cmd, char* args){ + int32_t reg = -1, nwords = 1; + const char *setter = splitargs(args, ®); + if(reg < 0) return ERR_BADPAR; + if(setter){ // read more than one byte + uint32_t n; + const char *nxt = getnum(setter, &n); + if (nxt == setter || n == 0 || n > I2C_BUFSIZE) return ERR_BADVAL; + nwords = (int32_t)n; + } + uint16_t *data = i2c_read_reg16(I2Caddress, (uint16_t)reg, nwords, 0); + if(!data) return ERR_CANTRUN; + if(nwords == 1){ + CMDEQP(reg); printuhex(*data); N(); + }else{ + CMDEQP(reg); N(); hexdump16(SEND, data, nwords); + } + return ERR_AMOUNT; +} + +static errcodes_t cmd_writedata(const char*, char* args){ + const char *setter = splitargs(args, NULL); + if(!setter) return ERR_BADVAL; + int N = 0; + const char *p = setter; + while (*p){ + if(N >= LOCBUFFSZ) return ERR_AMOUNT; + uint32_t val; + p = getnum(p, &val); + if(p == setter) break; // not a number + locBuffer[N++] = (uint16_t)val; + p = omit_spaces(p); + if (!*p) break; + } + if(N == 0) return ERR_BADVAL; + if(!i2c_write(I2Caddress, locBuffer, N)) return ERR_CANTRUN; + return ERR_OK; +} + +static errcodes_t cmd_iicscan(const char*, char*) { + i2c_init_scan_mode(); + return ERR_OK; +} + +static errcodes_t cmd_mcutemp(const char* cmd, char*){ + CMDEQ(); printfl(getMCUtemp(), 2); N(); + return ERR_AMOUNT; +} + +static errcodes_t cmd_mcuvdd(const char* cmd, char*){ + CMDEQ(); printfl(getVdd(), 2); N(); + return ERR_AMOUNT; +} + +static errcodes_t cmd_dac(const char* cmd, char* args){ + int32_t val; + if(argsvals(args, NULL, &val)){ + if(val < 0 || val > 4095) return ERR_BADVAL; + DAC1->DHR12R1 = static_cast(val); + } + // getter + CMDEQ(); printu(DAC1->DHR12R1); N(); + return ERR_AMOUNT; +} + +static void showpwm(const char* cmd, uint8_t nch){ + uint16_t ccr; + switch(nch){ + case 0: ccr = TIM3->CCR1; break; + case 1: ccr = TIM3->CCR2; break; + case 2: ccr = TIM3->CCR3; break; + case 3: ccr = TIM3->CCR4; break; + default: return; + } + CMDEQP(nch); printu(ccr); N(); +} + +// four PWM channels: 1,2 - heaters, 3,4 - info +static errcodes_t cmd_pwm(const char* cmd, char* args){ + int32_t ch = -1, val; + const char *setter = splitargs(args, &ch); + if(ch < 0 || ch > PWM_CH_MAX){ // all channels + for(uint8_t i = 0; i <= PWM_CH_MAX; ++i) + showpwm(cmd, i); + return ERR_AMOUNT; + } + if(setter){ + if(!getint(setter, &val) || val < 0 || val > 100) return ERR_BADVAL; + if(!setPWM(static_cast(ch), static_cast(val))) + return ERR_CANTRUN; + } + // getter + showpwm(cmd, (uint8_t)ch); + return ERR_AMOUNT; +} + +static errcodes_t cmd_sendstr(const char*, char* args) { + int32_t dummy; + const char *text = splitargs(args, &dummy); + if(!text || !*text) return ERR_BADVAL; + // switch to other interface + int (*other_sender)(const char*) = (SEND == usb_sender) ? usart_sender : usb_sender; + if (!other_sender) return ERR_CANTRUN; + other_sender(text); + other_sender("\n"); + return ERR_OK; +} + +static constexpr uint32_t hash(const char* str, uint32_t h = 0) { + return *str ? hash(str + 1, h + ((h << 7) ^ *str)) : h; +} + + +const char *parse_cmd(int (*sendfun)(const char*), char *buf) { + if(!buf || !*buf || !sendfun) return NULL; + SEND = sendfun; + if(sendfun == usb_sender){ + putb = usb_putb; + sendbin = usb_sendbin; + }else{ + putb = usart_putb; + sendbin = usart_sendbin; + } + char command[CMD_MAXLEN+1]; + int i = 0; + while(*buf > '@' && i < CMD_MAXLEN) command[i++] = *buf++; + command[i] = 0; + while(*buf && *buf <= ' ') ++buf; + char *args = buf; +#ifdef EBUG + USB_sendstr("__args='"); USB_sendstr(args); USB_sendstr("'\n"); +#endif + if(!*args) args = NULL; + + uint32_t h = hash(command); + errcodes_t ecode = ERR_AMOUNT; + switch (h){ +#define COMMAND(name, desc) case hash(#name): ecode = cmd_##name(command, args); break; + COMMAND_TABLE +#undef COMMAND + default: + SEND("Unknown command, try 'help'\n"); + } + if(ecode < ERR_AMOUNT) return errtxt[ecode]; + return NULL; +} + +void dumpIma(const fp_t im[MLX_PIXNO]){ + for(int row = 0; row < MLX_H; ++row){ + for(int col = 0; col < MLX_W; ++col){ + printfl(*im++, 1); + putb(' '); + } + N(); + } +} + +#define GRAY_LEVELS 16 +static const char *const CHARS_16 = " .':;+*oxX#&%B$@"; +void drawIma(const fp_t im[MLX_PIXNO]){ + fp_t min_val = im[0], max_val = im[0]; + const fp_t *iptr = im; + for(int row = 0; row < MLX_H; ++row) + for(int col = 0; col < MLX_W; ++col){ + fp_t cur = *iptr++; + if (cur < min_val) min_val = cur; + else if (cur > max_val) max_val = cur; + } + fp_t range = max_val - min_val; + SEND("RANGE="); printfl(range, 3); N(); + SEND("MIN="); printfl(min_val, 3); N(); + SEND("MAX="); printfl(max_val, 3); N(); + if(fabsf(range) < 0.001) range = 1.0f; + iptr = im; + char string[MLX_W+2]; + string[MLX_W] = '\n'; string[MLX_W+1] = 0; // end of line + for(int row = 0; row < MLX_H; ++row){ + for(int col = 0; col < MLX_W; ++col){ + fp_t normalized = ((*iptr++) - min_val) / range; + int idx = (int)(normalized * GRAY_LEVELS); + if(idx < 0) idx = 0; + else if(idx >= GRAY_LEVELS) idx = GRAY_LEVELS-1; + string[col] = CHARS_16[idx]; + } + SEND(string); + } + N(); +} diff --git a/F3:F303/MLX90640-allsky/commproto.h b/F3:F303/MLX90640-allsky/commproto.h new file mode 100644 index 0000000..fc4d7ee --- /dev/null +++ b/F3:F303/MLX90640-allsky/commproto.h @@ -0,0 +1,61 @@ +/* + * This file is part of the as3935 project. + * Copyright 2026 Edward V. Emelianov . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "mlx90640.h" + +#include "version.inc" + +#ifdef EBUG +#define RLSDBG "debug" +#else +#define RLSDBG "release" +#endif + +#define REPOURL "https://github.com/eddyem/stm32samples/tree/master/F3:F303/MLX90640-allsky " RLSDBG " build #" BUILD_NUMBER "@" BUILD_DATE "\n" + + +// error codes for answer message +typedef enum{ + ERR_OK, // all OK + ERR_BADCMD, // wrong command + ERR_BADPAR, // wrong parameter + ERR_BADVAL, // wrong value (for setter) + ERR_WRONGLEN, // wrong message length + ERR_CANTRUN, // can't run given command due to bad parameters or other + ERR_BUSY, // target interface busy, try later + ERR_OVERFLOW, // string was too long -> overflow + ERR_AMOUNT // amount of error codes or "send nothing" +} errcodes_t; + +// maximal length of command (without trailing zero) +#define CMD_MAXLEN 15 +// maximal available parameter number (for 16-bit registers is 0xffff +#define MAXPARNO 0xffff + +extern const char *EQ; +const char *parse_cmd(int (*sendfun)(const char*), char *buf); +void set_senders(int (*usbs)(const char*), int (*usbb)(uint8_t), int (*usbbin)(const uint8_t*, int), + int (*usarts)(const char*), int (*usartb)(uint8_t), int (*usartbin)(const uint8_t*, int)); + +extern const char *const Timage; + +extern uint8_t cartoon; +void dumpIma(const fp_t im[MLX_PIXNO]); +void drawIma(const fp_t im[MLX_PIXNO]); diff --git a/F3:F303/MLX90640-allsky/hardware.c b/F3:F303/MLX90640-allsky/hardware.c index 4b78c6b..abae11d 100644 --- a/F3:F303/MLX90640-allsky/hardware.c +++ b/F3:F303/MLX90640-allsky/hardware.c @@ -51,16 +51,19 @@ TRUE_INLINE void iwdg_setup(){ TRUE_INLINE void gpio_setup(){ RCC->AHBENR |= RCC_AHBENR_GPIOAEN | RCC_AHBENR_GPIOBEN; // for USART and LEDs for(int i = 0; i < 10000; ++i) nop(); - // USB - alternate function 14 @ pins PA11/PA12; SWD - AF0 @PA13/14; USB pullup - PA15 - // PA6 - PWM for external heater (TIM3_CH1 or TIM16_CH1); PA7 - PWM propto (humidity - 50%) + // USB - alternate function 14 @ pins PA11/PA12; SWD - AF0 @PA13/14 + // PA6,PA7 - PWM for external heaters (TIM3_CH1, TIM3_CH2) + // PA0..PA4 - NTC in, PA5 - DAC_OUT1 (board heater), PA6 - ADC in for DAC out + // USART pins will be setup in usart.c GPIOA->AFR[0] = AFRf(2, 6) | AFRf(2, 7); GPIOA->AFR[1] = AFRf(14, 11) | AFRf(14, 12); - GPIOA->MODER = MODER_AI(0) | MODER_AI(1) | MODER_AI(4) | MODER_AI(5) | MODER_AF(6) | - MODER_AF(7) | MODER_AF(11) | MODER_AF(12) | MODER_AF(13) | MODER_AF(14) | MODER_O(15); - // PB0 - PWM propto Text (<=20 - 0%, >=30 - 100%), PB1 - PWM propto (Text-Tsky) (<=-5 - 0%, >=+35 - 100%) PB2 - SPI_CS + // force USB DP to low level for a while + GPIOA->MODER = MODER_AI(0) | MODER_AI(1) | MODER_AI(2) | MODER_AI(3) | MODER_AI(4) | MODER_AI(5) | MODER_AF(6) | + MODER_AF(7) | MODER_AF(11) | MODER_O(12) | MODER_AF(13) | MODER_AF(14) | MODER_O(15); + // PB0 - PWM propto Text (<=20 - 0%, >=30 - 100%), PB1 - PWM propto (Text-Tsky) (<=-5 - 0%, >=+35 - 100%) PB9 - SPI_CS + // SPI and I2C will be setup in spi.c and i2c.c GPIOB->AFR[0] = AFRf(2, 0) | AFRf(2, 1); - GPIOB->MODER = MODER_AF(0) | MODER_AF(1) | MODER_O(2); - pin_set(GPIOB, 1<<1); + GPIOB->MODER = MODER_AF(0) | MODER_AF(1) | MODER_O(9); SPI_CS_1(); } @@ -85,7 +88,7 @@ TRUE_INLINE void pwm_setup(){ // change PWM value in percents; return 0 if `val` is bad or `ch` not 0..3 int setPWM(uint8_t ch, uint8_t val){ - if(ch > 3 || val > PWM_CCR_MAX) return 0; + if(ch >= PWM_CH_MAX || val > PWM_CCR_MAX) return 0; volatile uint32_t *CCRs = &(TIM3->CCR1); CCRs[ch] = val; return 1; @@ -139,7 +142,6 @@ void bme_process(){ // set PWM duty propto humidity float h = (Humidity - 50.f) * 2.f; if(h < 0.f) h = 0.f; else if(h > 100.f) h = 100.f; - setPWM(PWM_CH_HUMIDITY, (uint8_t)h); environment.Tmeas = Tms; // set PWM duty propto external T float t = (Temperature + 20.f) * 2.f; diff --git a/F3:F303/MLX90640-allsky/hardware.h b/F3:F303/MLX90640-allsky/hardware.h index 71f6f04..3b92191 100644 --- a/F3:F303/MLX90640-allsky/hardware.h +++ b/F3:F303/MLX90640-allsky/hardware.h @@ -25,9 +25,9 @@ #define USBPU_ON() pin_clear(USBPU_port, USBPU_pin) #define USBPU_OFF() pin_set(USBPU_port, USBPU_pin) -// SPI_CS - PB2 -#define SPI_CS_1() pin_set(GPIOB, 1<<2) -#define SPI_CS_0() pin_clear(GPIOB, 1<<2) +// SPI_CS - PB9 +#define SPI_CS_1() pin_set(GPIOB, 1<<9) +#define SPI_CS_0() pin_clear(GPIOB, 1<<9) // interval of environment measurements, ms #define ENV_MEAS_PERIOD (10000) @@ -36,14 +36,11 @@ // Max PWM CCR1 value (->1) #define PWM_CCR_MAX (100) // PWM channels (start from 0 - CH1) -// external heater -#define PWM_CH_HEATER (0) -// propto humidity (the higher - the brighter) -#define PWM_CH_HUMIDITY (1) // propto external T (the higher - the brighter) #define PWM_CH_TEXT (2) // propto Tsky - Text (the higher - the brighter) #define PWM_CH_TSKY (3) +#define PWM_CH_MAX (3) typedef struct{ float T; // temperature, degC diff --git a/F3:F303/MLX90640-allsky/ir-allsky.creator.user b/F3:F303/MLX90640-allsky/ir-allsky.creator.user index 64d8017..c32ec9c 100644 --- a/F3:F303/MLX90640-allsky/ir-allsky.creator.user +++ b/F3:F303/MLX90640-allsky/ir-allsky.creator.user @@ -1,6 +1,6 @@ - + EnvironmentId @@ -86,6 +86,7 @@ true + 0 @@ -153,6 +154,7 @@ true true true + 2 @@ -162,6 +164,7 @@ ProjectExplorer.CustomExecutableRunConfiguration false + true true @@ -185,6 +188,7 @@ true true true + 2 @@ -194,6 +198,7 @@ ProjectExplorer.CustomExecutableRunConfiguration false + true true @@ -204,10 +209,6 @@ ProjectExplorer.Project.TargetCount 1 - - ProjectExplorer.Project.Updater.FileVersion - 22 - Version 22 diff --git a/F3:F303/MLX90640-allsky/ir-allsky.files b/F3:F303/MLX90640-allsky/ir-allsky.files index eabc985..fdf0783 100644 --- a/F3:F303/MLX90640-allsky/ir-allsky.files +++ b/F3:F303/MLX90640-allsky/ir-allsky.files @@ -2,6 +2,8 @@ BMP280.c BMP280.h adc.c adc.h +commproto.cpp +commproto.h hardware.c hardware.h i2c.c @@ -15,8 +17,6 @@ mlx90640.h mlx90640_regs.h mlxproc.c mlxproc.h -proto.c -proto.h ringbuffer.c ringbuffer.h spi.c diff --git a/F3:F303/MLX90640-allsky/main.c b/F3:F303/MLX90640-allsky/main.c index 02c31d9..d83ae9a 100644 --- a/F3:F303/MLX90640-allsky/main.c +++ b/F3:F303/MLX90640-allsky/main.c @@ -20,7 +20,7 @@ #include "hardware.h" #include "i2c.h" #include "mlxproc.h" -#include "proto.h" +#include "commproto.h" #include "strfunc.h" #include "usart.h" #include "usb_dev.h" @@ -43,14 +43,18 @@ int main(void){ StartHSI(); SysTick_Config((uint32_t)48000); // 1ms } - USBPU_OFF(); + USBPU_OFF(); // for development board with managed pullup resistor hw_setup(); adc_setup(); i2c_setup(I2C_SPEED_400K); bme_init(); - USB_setup(); usart_setup(115200); + // setup USB DP as alternate function - for sensors' board with constant pullup resistor + GPIOA->MODER = (GPIOA->MODER & ~GPIO_MODER_MODER12) | MODER_AF(12); USBPU_ON(); + USB_setup(); + // set senders for abiliby of sending messages between interfaces + set_senders(USB_sendstr, USB_putbyte, USB_send, usart_sendstr, usart_putbyte, usart_send); uint32_t ctr = Tms, Tlastima[N_SENSORS] = {0}; mlx_continue(); // init state machine while(1){ @@ -63,7 +67,7 @@ int main(void){ int l = USB_receivestr(inbuff, MAXSTRLEN); if(l < 0) USB_sendstr("USBOVERFLOW\n"); else if(l){ - const char *ans = parse_cmd(inbuff, SEND_USB); + const char *ans = parse_cmd(USB_sendstr, inbuff); if(ans) USB_sendstr(ans); } if(i2c_scanmode){ // send this to both @@ -84,7 +88,6 @@ int main(void){ if(Tnow != Tlastima[i]){ fp_t *im = mlx_getimage(i); if(im){ - chsendfun(SEND_USB); //U(Sensno); UN(i2str(i)); U(Timage); USB_putbyte('0'+i); USB_putbyte('='); UN(u2str(Tnow)); drawIma(im); @@ -96,7 +99,7 @@ int main(void){ if(usart_ovr()) usart_sendstr("USART_OVERFLOW\n"); char *got = usart_getline(NULL); if(got){ - const char *ans = parse_cmd(got, SEND_USART); + const char *ans = parse_cmd(usart_sendstr, got); if(ans) usart_sendstr(ans); } bme_process(); diff --git a/F3:F303/MLX90640-allsky/mlxproc.c b/F3:F303/MLX90640-allsky/mlxproc.c index eea579e..0e62161 100644 --- a/F3:F303/MLX90640-allsky/mlxproc.c +++ b/F3:F303/MLX90640-allsky/mlxproc.c @@ -63,12 +63,16 @@ static int sensno = -1; mlx_state_t mlx_state(){ return MLX_state; } // set address int mlx_setaddr(int n, uint8_t addr){ - if(n < 0 || n > N_SENSORS) return 0; + if(n < 0 || n >= N_SENSORS) return 0; if(addr > 0x7f) return 0; sens_addresses[n] = addr << 1; Tlastimage[n] = Tms; // refresh counter for autoreset I2C in case of error return 1; } +uint8_t mlx_getaddr(int n){ + if(n < 0 || n >= N_SENSORS) return 0; + return sens_addresses[n]; +} // pause state machine and stop void mlx_pause(){ MLX_oldstate = MLX_state; diff --git a/F3:F303/MLX90640-allsky/mlxproc.h b/F3:F303/MLX90640-allsky/mlxproc.h index f211644..25ad544 100644 --- a/F3:F303/MLX90640-allsky/mlxproc.h +++ b/F3:F303/MLX90640-allsky/mlxproc.h @@ -39,6 +39,7 @@ typedef enum{ } mlx_state_t; int mlx_setaddr(int n, uint8_t addr); +uint8_t mlx_getaddr(int n); mlx_state_t mlx_state(); int mlx_nactive(); uint8_t *mlx_activeids(); diff --git a/F3:F303/MLX90640-allsky/proto.c b/F3:F303/MLX90640-allsky/proto.c deleted file mode 100644 index 1acc3c5..0000000 --- a/F3:F303/MLX90640-allsky/proto.c +++ /dev/null @@ -1,539 +0,0 @@ -/* - * This file is part of the ir-allsky project. - * Copyright 2025 Edward V. Emelianov . - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include - -#include "adc.h" -#include "hardware.h" -#include "i2c.h" -#include "mlxproc.h" -#include "proto.h" -#include "strfunc.h" -#include "usart.h" -#include "usb_dev.h" -#include "version.inc" - -#define LOCBUFFSZ (32) -// local buffer for I2C data to send -static uint16_t locBuffer[LOCBUFFSZ]; -static uint8_t I2Caddress = 0x33 << 1; -extern volatile uint32_t Tms; -uint8_t cartoon = 0; // "cartoon" mode: refresh image each time we get new - -// functions to send data over USB or USART: to change them use flag in `parse_cmd` -typedef struct{ - int (*S)(const char*); // send string - int (*P)(uint8_t); // put byte - int (*B)(const uint8_t*, int); // send raw bytes -} sendfun_t; - -static sendfun_t usbsend = { - .S = USB_sendstr, .P = USB_putbyte, .B = USB_send -}; -static sendfun_t usartsend = { - .S = usart_sendstr, .P = usart_putbyte, .B = usart_send -}; - -static sendfun_t *sendfun = &usbsend; - -void chsendfun(int sendto){ - if(sendto == SEND_USB) sendfun = &usbsend; - else sendfun = &usartsend; -} - -// newline -#define N() sendfun->P('\n') -#define printu(x) do{sendfun->S(u2str(x));}while(0) -#define printi(x) do{sendfun->S(i2str(x));}while(0) -#define printuhex(x) do{sendfun->S(uhex2str(x));}while(0) -#define printfl(x,n) do{sendfun->S(float2str(x, n));}while(0) - -// common names for frequent keys -const char* const Timage = "TIMAGE"; -const char* const Image = "IMAGE"; -static const char *const Sensno = "SENSNO="; - -static const char *const OK = "OK\n", *const ERR = "ERR\n"; -const char *const helpstring = - "https://github.com/eddyem/stm32samples/tree/master/F3:F303/MLX90640multi build#" BUILD_NUMBER " @ " BUILD_DATE "\n" - " management of single IR bolometer MLX90640\n" - "dn - draw nth image in ASCII\n" - "gn - get nth image 'as is' - float array of 768x4 bytes\n" - "l - list active sensors IDs\n" - "mn - show temperature map of nth image\n" - "tn - show nth image aquisition time\n" - "B - reinit BME280\n" - "E - get environment parameters (temperature etc)\n" - "G - get MLX state\n" - "R - reset device\n" - "T - print current Tms\n" - " Debugging options:\n" - "aa - change I2C address to a (a should be non-shifted value!!!)\n" - "c - continue MLX\n" - "i0..4 - setup I2C with speed 10k, 100k, 400k, 1M or 2M (experimental!)\n" - "p - pause MLX\n" - "s - stop MLX (and start from zero @ 'c')\n" - "A - get ADC values\n" - "C - \"cartoon\" mode on/off (show each new image) - USB only!!!\n" - "Dn - dump MLX parameters for sensor number n\n" - "Ia addr [n] - set device address for interactive work or (with n) change address of n'th sensor\n" - "Ir reg n - read n words from 16-bit register\n" - "Iw words - send words (hex/dec/oct/bin) to I2C\n" - "Is - scan I2C bus\n" - "M - get MCU temperature and Vdd value\n" - "O - set output of DAC (0..4095)\n" - "Px - set PWM output (0..100%) or get current value\n" - "Us - send string 's' to other interface\n" -; - -TRUE_INLINE const char *setupI2C(char *buf){ - static const char * const speeds[I2C_SPEED_AMOUNT] = { - [I2C_SPEED_10K] = "10K", - [I2C_SPEED_100K] = "100K", - [I2C_SPEED_400K] = "400K", - [I2C_SPEED_1M] = "1M", - [I2C_SPEED_2M] = "2M" - }; - if(buf && *buf){ - buf = omit_spaces(buf); - int speed = *buf - '0'; - if(speed < 0 || speed >= I2C_SPEED_AMOUNT){ - return ERR; - } - i2c_setup((i2c_speed_t)speed); - } - sendfun->S("I2CSPEED="); sendfun->S(speeds[i2c_curspeed]); N(); - return NULL; -} - -TRUE_INLINE const char *chhwaddr(const char *buf){ - uint32_t a; - if(buf && *buf){ - const char *nxt = getnum(buf, &a); - if(nxt && nxt != buf){ - if(!mlx_sethwaddr(I2Caddress, a)) return ERR; - }else{ - sendfun->S("Wrong number"); N(); - return ERR; - } - }else{ - sendfun->S("Need address"); N(); - return ERR; - } - return OK; -} - -// read sensor's number from `buf`; return -1 if error -static int getsensnum(const char *buf){ - if(!buf || !*buf) return -1; - uint32_t num; - const char *nxt = getnum(buf, &num); - if(!nxt || nxt == buf || num >= N_SENSORS) return -1; - return (int) num; -} - -TRUE_INLINE const char *chaddr(const char *buf){ - uint32_t addr; - const char *nxt = getnum(buf, &addr); - if(nxt && nxt != buf){ - if(addr > 0x7f) return ERR; - I2Caddress = (uint8_t) addr << 1; - int n = getsensnum(nxt); - if(n > -1) mlx_setaddr(n, addr); - }else addr = I2Caddress >> 1; - sendfun->S("I2CADDR="); sendfun->S(uhex2str(addr)); N(); - return NULL; -} - -// read I2C register[s] - only blocking read! (DMA allowable just for config/image reading in main process) -static const char *rdI2C(const char *buf){ - uint32_t N = 0; - const char *nxt = getnum(buf, &N); - if(!nxt || buf == nxt || N > 0xffff) return ERR; - buf = nxt; - uint16_t reg = N, *b16 = NULL; - nxt = getnum(buf, &N); - if(!nxt || buf == nxt || N == 0 || N > I2C_BUFSIZE) return ERR; - if(!(b16 = i2c_read_reg16(I2Caddress, reg, N, 0))) return ERR; - if(N == 1){ - char b[5]; - u16s(*b16, b); - b[4] = 0; - sendfun->S(b); N(); - }else hexdump16(sendfun->S, b16, N); - return NULL; -} - -// read N numbers from buf, @return 0 if wrong or none -TRUE_INLINE uint16_t readNnumbers(const char *buf){ - uint32_t D; - const char *nxt; - uint16_t N = 0; - while((nxt = getnum(buf, &D)) && nxt != buf && N < LOCBUFFSZ){ - buf = nxt; - locBuffer[N++] = (uint16_t) D; - } - return N; -} - -static const char *wrI2C(const char *buf){ - uint16_t N = readNnumbers(buf); - if(N == 0) return ERR; - for(int i = 0; i < N; ++i){ - sendfun->S("byte "); sendfun->S(u2str(i)); - sendfun->S(" :"); sendfun->S(uhex2str(locBuffer[i])); N(); - } - if(!i2c_write(I2Caddress, locBuffer, N)) return ERR; - return OK; -} - -static void dumpfarr(float *arr){ - for(int row = 0; row < 24; ++row){ - for(int col = 0; col < 32; ++col){ - printfl(*arr++, 2); sendfun->P(' '); - } - N(); - } -} -// dump MLX parameters -TRUE_INLINE void dumpparams(const char *buf){ - int N = getsensnum(buf); - if(N < 0){ sendfun->S(ERR); return; } - MLX90640_params *params = mlx_getparams(N); - if(!params){ sendfun->S(ERR); return; } - N(); sendfun->S(Sensno); sendfun->S(i2str(N)); - sendfun->S("\nkVdd="); printi(params->kVdd); - sendfun->S("\nvdd25="); printi(params->vdd25); - sendfun->S("\nKvPTAT="); printfl(params->KvPTAT, 4); - sendfun->S("\nKtPTAT="); printfl(params->KtPTAT, 4); - sendfun->S("\nvPTAT25="); printi(params->vPTAT25); - sendfun->S("\nalphaPTAT="); printfl(params->alphaPTAT, 2); - sendfun->S("\ngainEE="); printi(params->gainEE); - sendfun->S("\nPixel offset parameters:\n"); - float *offset = params->offset; - for(int row = 0; row < 24; ++row){ - for(int col = 0; col < 32; ++col){ - printfl(*offset++, 2); sendfun->P(' '); - } - N(); - } - sendfun->S("K_talpha:\n"); - dumpfarr(params->kta); - sendfun->S("Kv: "); - for(int i = 0; i < 4; ++i){ - printfl(params->kv[i], 2); sendfun->P(' '); - } - sendfun->S("\ncpOffset="); - printi(params->cpOffset[0]); sendfun->S(", "); printi(params->cpOffset[1]); - sendfun->S("\ncpKta="); printfl(params->cpKta, 2); - sendfun->S("\ncpKv="); printfl(params->cpKv, 2); - sendfun->S("\ntgc="); printfl(params->tgc, 2); - sendfun->S("\ncpALpha="); printfl(params->cpAlpha[0], 2); - sendfun->S(", "); printfl(params->cpAlpha[1], 2); - sendfun->S("\nKsTa="); printfl(params->KsTa, 2); - sendfun->S("\nAlpha:\n"); - dumpfarr(params->alpha); - sendfun->S("\nCT3="); printfl(params->CT[1], 2); - sendfun->S("\nCT4="); printfl(params->CT[2], 2); - for(int i = 0; i < 4; ++i){ - sendfun->S("\nKsTo"); sendfun->P('0'+i); sendfun->P('='); - printfl(params->KsTo[i], 2); - sendfun->S("\nalphacorr"); sendfun->P('0'+i); sendfun->P('='); - printfl(params->alphacorr[i], 2); - } - N(); -} -// get MLX state -TRUE_INLINE void getst(){ - static const char *states[] = { - [MLX_NOTINIT] = "not init", - [MLX_WAITPARAMS] = "wait parameters DMA read", - [MLX_WAITSUBPAGE] = "wait subpage", - [MLX_READSUBPAGE] = "wait subpage DMA read", - [MLX_RELAX] = "do nothing" - }; - mlx_state_t s = mlx_state(); - sendfun->S("MLXSTATE="); - sendfun->S(states[s]); N(); -} - -// `draw`==1 - draw, ==0 - show T map, 2 - send raw float array with prefix 'TIMAGEX=y\nIMAGEX=' and postfix "ENDIMAGE\n" -static const char *drawimg(const char *buf, int draw){ - int sensno = getsensnum(buf); - if(sensno > -1){ - uint32_t T = mlx_lastimT(sensno); - fp_t *img = mlx_getimage(sensno); - if(img){ - //sendfun->S(Sensno); sendfun->S(u2str(sensno)); N(); - sendfun->S(Timage); sendfun->P('0'+sensno); sendfun->P('='); sendfun->S(u2str(T)); N(); - switch(draw){ - case 0: - dumpIma(img); - break; - case 1: - drawIma(img); - break; - case 2: - sendfun->S(Image); sendfun->P('0'+sensno); sendfun->P('='); - uint8_t *d = (uint8_t*)img; - uint32_t _2send = MLX_PIXNO * sizeof(float); - // send by portions of 256 bytes (as image is larger than ringbuffer) - while(_2send){ - uint32_t portion = (_2send > 256) ? 256 : _2send; - sendfun->B(d, portion); - _2send -= portion; - d += portion; - } - sendfun->S("ENDIMAGE"); N(); - break; - } - return NULL; - } - } - return ERR; -} - -TRUE_INLINE void listactive(){ - int N = mlx_nactive(); - if(!N){ sendfun->S("No active sensors found!\n"); return; } - uint8_t *ids = mlx_activeids(); - sendfun->S("Found "); sendfun->P('0'+N); - sendfun->S(" active sensors:"); N(); - for(int i = 0; i < N_SENSORS; ++i) - if(ids[i]){ - sendfun->S("SENSID"); - sendfun->S(u2str(i)); sendfun->P('='); - sendfun->S(uhex2str(ids[i] >> 1)); - N(); - } -} - -static void getimt(const char *buf){ - int sensno = getsensnum(buf); - if(sensno > -1){ - sendfun->S(Timage); sendfun->P('0'+sensno); sendfun->P('='); sendfun->S(u2str(mlx_lastimT(sensno))); N(); - }else sendfun->S(ERR); -} - -TRUE_INLINE void getenv(){ - bme280_t env; - if(!get_environment(&env)) sendfun->S("BADENVIRONMENT\n"); - sendfun->S("TEMPERATURE="); sendfun->S(float2str(env.T, 2)); - sendfun->S("\nSKYTEMPERATURE="); sendfun->S(float2str(env.Tsky, 2)); - sendfun->S("\nPRESSURE_HPA="); sendfun->S(float2str(env.P/100.f, 2)); - sendfun->S("\nPRESSURE_MM="); sendfun->S(float2str(env.P * 0.00750062f, 2)); - sendfun->S("\nHUMIDITY="); sendfun->S(float2str(env.H, 2)); - sendfun->S("\nTEMP_DEW="); sendfun->S(float2str(env.Tdew, 1)); - sendfun->S("\nT_MEASUREMENT="); sendfun->S(u2str(env.Tmeas)); - N(); -} - -TRUE_INLINE const char *DAC_chval(const char *buf){ - uint32_t D; - const char *nxt = getnum(buf, &D); - if(!nxt || nxt == buf || D > 4095) return ERR; - DAC1->DHR12R1 = D; - return OK; -} - - -TRUE_INLINE void getADC(){ - sendfun->S("AIN0="); sendfun->S(u2str(getADCval(ADC_AIN0))); - sendfun->S("\nAIN1="); sendfun->S(u2str(getADCval(ADC_AIN1))); - sendfun->S("\nAIN5="); sendfun->S(u2str(getADCval(ADC_AIN5))); - N(); -} - -TRUE_INLINE void getMCUvals(){ - sendfun->S("MCUTEMP="); sendfun->S(float2str(getMCUtemp(), 2)); - sendfun->S("\nMCUVDD="); sendfun->S(float2str(getVdd(), 2)); - N(); -} - -TRUE_INLINE const char* setpwm(const char *buf){ - uint32_t D; - if(!buf || !*buf){ - sendfun->S("PWM1="); sendfun->S(u2str(TIM3->CCR1)); - sendfun->S("\nPWM2="); sendfun->S(u2str(TIM3->CCR2)); - sendfun->S("\nPWM3="); sendfun->S(u2str(TIM3->CCR3)); - sendfun->S("\nPWM4="); sendfun->S(u2str(TIM3->CCR4)); - N(); - return NULL; - } - const char *nxt = getnum(buf, &D); - if(!nxt || nxt == buf || !setPWM(PWM_CH_HEATER, D)) return ERR; - return OK; -} - -/** - * @brief parse_cmd - user string parser - * @param buf - user data - * @param isusb - ==1 to send answer over usb, else send over USART1 - * @return answer OK/ERR or NULL - */ -const char *parse_cmd(char *buf, int sendto){ - if(!buf || !*buf) return NULL; - chsendfun(sendto); - if(buf[1]){ - switch(*buf++){ // "long" commands - case 'a': - return chhwaddr(buf); - case 'd': - return drawimg(buf, 1); - case 'g': - return drawimg(buf, 2); - case 'i': - return setupI2C(buf); - case 'm': - return drawimg(buf, 0); - case 't': - getimt(buf); return NULL; - case 'D': - dumpparams(buf); - return NULL; - break; - case 'I': - buf = omit_spaces(buf); - switch(*buf++){ - case 'a': - return chaddr(buf); - case 'r': - return rdI2C(buf); - case 'w': - return wrI2C(buf); - case 's': - i2c_init_scan_mode(); - return OK; - default: - return ERR; - } - break; - case 'O': - return DAC_chval(buf); - case 'P': - return setpwm(buf); - case 'U': - if(sendto == SEND_USB) chsendfun(SEND_USART); - else chsendfun(SEND_USB); - if(sendfun->S(buf) && N()) return OK; - return ERR; - default: - return ERR; - } - } - switch(*buf){ // "short" (one letter) commands - case 'A': - getADC(); - break; - case 'c': - mlx_continue(); return OK; - break; - case 'i': return setupI2C(NULL); // current settings - case 'l': - listactive(); - break; - case 'p': - mlx_pause(); return OK; - break; - case 's': - mlx_stop(); return OK; - case 'B': - if(bme_init()) return OK; - return ERR; - case 'C': - if(sendto != SEND_USB) return ERR; - cartoon = !cartoon; return OK; - case 'E': - getenv(); - break; - case 'G': - getst(); - break; - case 'M': - getMCUvals(); - break; - case 'P': - return setpwm(NULL); - case 'R': - NVIC_SystemReset(); - break; - case 'T': - sendfun->S("T="); sendfun->S(u2str(Tms)); N(); - break; - case '?': // help - case 'h': - case 'H': - sendfun->S(helpstring); - break; - default: - return ERR; - break; - } - return NULL; -} - -// dump image as temperature matrix -void dumpIma(const fp_t im[MLX_PIXNO]){ - for(int row = 0; row < MLX_H; ++row){ - for(int col = 0; col < MLX_W; ++col){ - printfl(*im++, 1); - sendfun->P(' '); - } - N(); - } -} - -#define GRAY_LEVELS (16) -// 16-level character set ordered by fill percentage (provided by user) -static const char *const CHARS_16 = " .':;+*oxX#&%B$@"; -// draw image in ASCII-art -void drawIma(const fp_t im[MLX_PIXNO]){ - // Find min and max values - fp_t min_val = im[0], max_val = im[0]; - const fp_t *iptr = im; - for(int row = 0; row < MLX_H; ++row){ - for(int col = 0; col < MLX_W; ++col){ - fp_t cur = *iptr++; - if(cur < min_val) min_val = cur; - else if(cur > max_val) max_val = cur; - } - } - fp_t range = max_val - min_val; - sendfun->S("RANGE="); sendfun->S(float2str(range, 3)); - sendfun->S("\nMIN="); sendfun->S(float2str(min_val, 3)); - sendfun->S("\nMAX="); sendfun->S(float2str(max_val, 3)); N(); - if(fabsf(range) < 0.001) range = 1.; // solid fill -> blank - // Generate and print ASCII art - iptr = im; - for(int row = 0; row < MLX_H; ++row){ - for(int col = 0; col < MLX_W; ++col){ - fp_t normalized = ((*iptr++) - min_val) / range; - // Map to character index (0 to 15) - int index = (int)(normalized * GRAY_LEVELS); - // Ensure we stay within bounds - if(index < 0) index = 0; - else if(index > (GRAY_LEVELS-1)) index = (GRAY_LEVELS-1); - sendfun->P(CHARS_16[index]); - } - N(); - } - N(); -} - diff --git a/F3:F303/MLX90640-allsky/proto.h b/F3:F303/MLX90640-allsky/proto.h deleted file mode 100644 index aab3d44..0000000 --- a/F3:F303/MLX90640-allsky/proto.h +++ /dev/null @@ -1,30 +0,0 @@ -/* - * This file is part of the ir-allsky project. - * Copyright 2025 Edward V. Emelianov . - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -extern const char *const Timage; - -#define SEND_USB (1) -#define SEND_USART (0) - -extern uint8_t cartoon; -void chsendfun(int sendto); -const char *parse_cmd(char *buf, int sendto); -void dumpIma(const fp_t im[MLX_PIXNO]); -void drawIma(const fp_t im[MLX_PIXNO]); diff --git a/F3:F303/MLX90640-allsky/version.inc b/F3:F303/MLX90640-allsky/version.inc index a8538a6..f7c8d90 100644 --- a/F3:F303/MLX90640-allsky/version.inc +++ b/F3:F303/MLX90640-allsky/version.inc @@ -1,2 +1,2 @@ -#define BUILD_NUMBER "45" -#define BUILD_DATE "2026-05-01" +#define BUILD_NUMBER "65" +#define BUILD_DATE "2026-05-06"