From 17b4d714e0c705a401cc29bcc0b4d0d7241e50ad Mon Sep 17 00:00:00 2001 From: Edward Emelianov Date: Sun, 5 Oct 2025 00:10:02 +0300 Subject: [PATCH] Add BME280 over SPI --- F3:F303/MLX90640-allsky/BMP280.c | 399 ++++++++++++++++++ F3:F303/MLX90640-allsky/BMP280.h | 96 +++++ F3:F303/MLX90640-allsky/Readme.md | 24 +- F3:F303/MLX90640-allsky/allsky.bin | Bin 26544 -> 20624 bytes F3:F303/MLX90640-allsky/hardware.c | 76 +++- F3:F303/MLX90640-allsky/hardware.h | 19 +- .../MLX90640-allsky/ir-allsky.creator.user | 2 +- F3:F303/MLX90640-allsky/ir-allsky.files | 4 + F3:F303/MLX90640-allsky/main.c | 9 +- F3:F303/MLX90640-allsky/proto.c | 65 ++- F3:F303/MLX90640-allsky/proto.h | 2 +- F3:F303/MLX90640-allsky/spi.c | 113 +++++ F3:F303/MLX90640-allsky/spi.h | 35 ++ F3:F303/MLX90640-allsky/usb_dev.c | 11 +- F3:F303/MLX90640-allsky/usb_dev.h | 6 + F3:F303/MLX90640-allsky/version.inc | 4 +- 16 files changed, 826 insertions(+), 39 deletions(-) create mode 100644 F3:F303/MLX90640-allsky/BMP280.c create mode 100644 F3:F303/MLX90640-allsky/BMP280.h create mode 100644 F3:F303/MLX90640-allsky/spi.c create mode 100644 F3:F303/MLX90640-allsky/spi.h diff --git a/F3:F303/MLX90640-allsky/BMP280.c b/F3:F303/MLX90640-allsky/BMP280.c new file mode 100644 index 0000000..374eebf --- /dev/null +++ b/F3:F303/MLX90640-allsky/BMP280.c @@ -0,0 +1,399 @@ +/** + * Ciastkolog.pl (https://github.com/ciastkolog) + * +*/ +/** + * The MIT License (MIT) + * + * Copyright (c) 2016 sheinz (https://github.com/sheinz) + * + * 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. + */ +/* + * Copyright 2023 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 "hardware.h" +#include "spi.h" +#include "BMP280.h" + +#include "usb_dev.h" // DBG +#ifdef EBUG +#include "strfunc.h" +extern volatile uint32_t Tms; +#endif + +#include +#include + +#define BMP280_I2C_ADDRESS_MASK (0x76) +#define BMP280_I2C_ADDRESS_0 (0x76) +#define BMP280_I2C_ADDRESS_1 (0x77) +/** + * BMP280 registers + */ +#define BMP280_REG_HUM_LSB 0xFE +#define BMP280_REG_HUM_MSB 0xFD +#define BMP280_REG_HUM (BMP280_REG_HUM_MSB) +#define BMP280_REG_TEMP_XLSB 0xFC /* bits: 7-4 */ +#define BMP280_REG_TEMP_LSB 0xFB +#define BMP280_REG_TEMP_MSB 0xFA +#define BMP280_REG_TEMP (BMP280_REG_TEMP_MSB) +#define BMP280_REG_PRESS_XLSB 0xF9 /* bits: 7-4 */ +#define BMP280_REG_PRESS_LSB 0xF8 +#define BMP280_REG_PRESS_MSB 0xF7 +#define BMP280_REG_PRESSURE (BMP280_REG_PRESS_MSB) +#define BMP280_REG_ALLDATA (BMP280_REG_PRESS_MSB) // all data: P, T & H +#define BMP280_REG_CONFIG 0xF5 /* bits: 7-5 t_sb; 4-2 filter; 0 spi3w_en */ +#define BMP280_REG_CTRL 0xF4 /* bits: 7-5 osrs_t; 4-2 osrs_p; 1-0 mode */ +#define BMP280_REG_STATUS 0xF3 /* bits: 3 measuring; 0 im_update */ +#define BMP280_REG_CTRL_HUM 0xF2 /* bits: 2-0 osrs_h; */ +#define BMP280_REG_RESET 0xE0 +#define BMP280_RESET_VALUE 0xB6 +#define BMP280_REG_ID 0xD0 + +#define BMP280_REG_CALIBA 0x88 +#define BMP280_CALIBA_SIZE (26) // 26 bytes of calibration registers sequence from 0x88 to 0xa1 +#define BMP280_CALIBB_SIZE (7) // 7 bytes of calibration registers sequence from 0xe1 to 0xe7 +#define BMP280_REG_CALIBB 0xE1 + +#define BMP280_MODE_FORSED (1) // force single measurement +#define BMP280_MODE_NORMAL (3) // run continuosly +#define BMP280_STATUS_IMCOPY (1<<0) // im-copy (sensor busy) +#define BME280_STATUS_MSRGOOD (1<<2) // measurement is good (undocumented bit) +#define BMP280_STATUS_MSRNG (1<<3) // measuring in process + +static struct { + // temperature + uint16_t dig_T1; // 0x88 (LSB), 0x98 (MSB) + int16_t dig_T2; // ... + int16_t dig_T3; + // pressure + uint16_t dig_P1; + int16_t dig_P2; + int16_t dig_P3; + int16_t dig_P4; + int16_t dig_P5; + int16_t dig_P6; + int16_t dig_P7; + int16_t dig_P8; + int16_t dig_P9; // 0x9e, 0x9f + // humidity (partially calculated from EEE struct) + uint8_t unused; // 0xA0 + uint8_t dig_H1; // 0xA1 + int16_t dig_H2; // -------------------- + uint8_t dig_H3; // only from EEE + uint16_t dig_H4; + uint16_t dig_H5; + int8_t dig_H6; + // data is ready + uint8_t rdy; +} __attribute__ ((packed)) CaliData = {0}; + +static struct{ + BMP280_Filter filter; // filtering + BMP280_Oversampling p_os; // oversampling for pressure + BMP280_Oversampling t_os; // -//- temperature + BMP280_Oversampling h_os; // -//- humidity + uint8_t ID; // identificator + uint8_t regctl; // control register base value [(params.t_os << 5) | (params.p_os << 2)] +} params = { + .filter = BMP280_FILTER_OFF, + .p_os = BMP280_OVERS16, + .t_os = BMP280_OVERS16, + .h_os = BMP280_OVERS16, + .ID = 0 +}; + +static BMP280_status bmpstatus = BMP280_NOTINIT; + +BMP280_status BMP280_get_status(){ + return bmpstatus; +} + +#define SPI_BUFSIZE (64) +static uint8_t SPIbuf[SPI_BUFSIZE]; + +// work with BME280 register over SPI +static uint8_t *read_regs(uint8_t reg, uint8_t len){ + if(len > SPI_BUFSIZE-2) return NULL; + SPI_CS_0(); + bzero(SPIbuf, sizeof(SPIbuf)); + SPIbuf[0] = reg | 0x80; + int r = spi_writeread(SPIbuf, len+1); + SPI_CS_1(); + if(!r) return NULL; +#ifdef EBUG + USB_sendstr("Register "); USB_sendstr(uhex2str(reg)); USB_sendstr(": "); + hexdump(USB_sendstr, SPIbuf + 1, len); +#endif + return SPIbuf + 1; +} +static int read_reg(uint8_t reg, uint8_t *val){ + uint8_t *got = read_regs(reg, 1); + if(!got) return 0; + if(val) *val = *got; + return 1; +} +static int write_reg(uint8_t reg, uint8_t val){ + SPI_CS_0(); + SPIbuf[0] = reg & 0x7F; // clear MSbit for writing + SPIbuf[1] = val; + uint8_t r = spi_writeread(SPIbuf, 2); + SPI_CS_1(); + return r; +} + + +void BMP280_setup(){ + bmpstatus = BMP280_NOTINIT; + spi_setup(); +} + +// setters for `params` +void BMP280_setfilter(BMP280_Filter f){ + params.filter = f; +} +BMP280_Filter BMP280_getfilter(){ + return params.filter; +} +void BMP280_setOSt(BMP280_Oversampling os){ + params.t_os = os; +} +void BMP280_setOSp(BMP280_Oversampling os){ + params.p_os = os; +} +void BMP280_setOSh(BMP280_Oversampling os){ + params.h_os = os; +} +// get compensation data, return 1 if OK +static int readcompdata(){ + uint8_t *got = read_regs(BMP280_REG_CALIBA, BMP280_CALIBA_SIZE); + if(!got) return 0; + memcpy(&CaliData, got, BMP280_CALIBA_SIZE); + CaliData.rdy = 1; + if(params.ID == BME280_CHIP_ID){ + got = read_regs(BMP280_REG_CALIBB, BMP280_CALIBB_SIZE); + if(got){ + CaliData.dig_H2 = (got[1] << 8) | got[0]; + CaliData.dig_H3 = got[2]; + CaliData.dig_H4 = (got[3] << 4) | (got[4] & 0x0f); + CaliData.dig_H5 = (got[5] << 4) | (got[4] >> 4); + CaliData.dig_H6 = got[6]; + } + } + return 1; +} + +// read compensation data & write registers +int BMP280_init(){ + IWDG->KR = IWDG_REFRESH; + DBG("INI:"); + if(!read_reg(BMP280_REG_ID, ¶ms.ID)){ + DBG("Can't get ID"); + return 0; + } + if(params.ID != BMP280_CHIP_ID && params.ID != BME280_CHIP_ID){ + DBG("Not BMP/BME"); + return 0; + } + if(!write_reg(BMP280_REG_RESET, BMP280_RESET_VALUE)){ + DBG("Can't reset"); + return 0; + } + uint8_t reg = 1; + int ntries = 100; + while((reg & BMP280_STATUS_IMCOPY) && --ntries){ + IWDG->KR = IWDG_REFRESH; + if(!read_reg(BMP280_REG_STATUS, ®)){ + DBG("can't get status"); + return 0; + } + } + if(ntries < 0){ + DBG("Timeout getting status"); + return 0; + } + if(!readcompdata()){ + DBG("Can't read calibration data"); + return 0; + } + // write filter configuration + reg = params.filter << 2; + if(!write_reg(BMP280_REG_CONFIG, reg)){DBG("Can't save filter settings");} + reg = (params.t_os << 5) | (params.p_os << 2); // oversampling for P/T, sleep mode + if(!write_reg(BMP280_REG_CTRL, reg)){ + DBG("Can't write settings for P/T"); + return 0; + } + params.regctl = reg; + if(params.ID == BME280_CHIP_ID){ // write CTRL_HUM only AFTER CTRL! + reg = params.h_os; + if(!write_reg(BMP280_REG_CTRL_HUM, reg)){ + DBG("Can't write settings for H"); + return 0; + } + } + bmpstatus = BMP280_RELAX; + return 1; +} + +// @return 1 if OK, *devid -> BMP/BME +int BMP280_read_ID(uint8_t *devid){ + if(params.ID != BMP280_CHIP_ID && params.ID != BME280_CHIP_ID) return 0; + *devid = params.ID; + return 1; +} + +// start measurement, @return 1 if all OK +int BMP280_start(){ + if(!CaliData.rdy || bmpstatus == BMP280_BUSY){ +#ifdef EBUG + USB_sendstr("rdy="); USB_sendstr(u2str(CaliData.rdy)); + USB_sendstr("\nbmpstatus="); USB_sendstr(u2str(bmpstatus)); + newline(); +#endif + return 0; + } + uint8_t reg = params.regctl | BMP280_MODE_FORSED; + if(!write_reg(BMP280_REG_CTRL, reg)){ + DBG("Can't write CTRL reg"); + return 0; + } + bmpstatus = BMP280_BUSY; + return 1; +} + +void BMP280_process(){ + if(bmpstatus == BMP280_NOTINIT){ + BMP280_init(); return; + } + if(bmpstatus != BMP280_BUSY) return; + // BUSY state: poll data ready + uint8_t reg; + if(!read_reg(BMP280_REG_STATUS, ®)) return; + if(reg & (BMP280_STATUS_MSRNG | BMP280_STATUS_IMCOPY)) return; // still busy + /*if(params.ID == BME280_CHIP_ID && !(reg & BME280_STATUS_MSRGOOD)){ // check if data is good + DBG("Wrong data!"); + bmpstatus = BMP280_RELAX; + read_regs(BMP280_REG_ALLDATA, 8); + return; + }*/ + bmpstatus = BMP280_RDY; // data ready +} + +// return T*100 degC +static inline int32_t compTemp(int32_t adc_temp, int32_t *t_fine){ + int32_t var1, var2; + var1 = ((((adc_temp >> 3) - ((int32_t) CaliData.dig_T1 << 1))) + * (int32_t) CaliData.dig_T2) >> 11; + var2 = (((((adc_temp >> 4) - (int32_t) CaliData.dig_T1) + * ((adc_temp >> 4) - (int32_t) CaliData.dig_T1)) >> 12) + * (int32_t) CaliData.dig_T3) >> 14; + *t_fine = var1 + var2; + return (*t_fine * 5 + 128) >> 8; +} + +// return p*256 hPa +static inline uint32_t compPres(int32_t adc_press, int32_t fine_temp) { + int64_t var1, var2, p; + var1 = (int64_t) fine_temp - 128000; + var2 = var1 * var1 * (int64_t) CaliData.dig_P6; + var2 = var2 + ((var1 * (int64_t) CaliData.dig_P5) << 17); + var2 = var2 + (((int64_t) CaliData.dig_P4) << 35); + var1 = ((var1 * var1 * (int64_t) CaliData.dig_P3) >> 8) + + ((var1 * (int64_t) CaliData.dig_P2) << 12); + var1 = (((int64_t) 1 << 47) + var1) * ((int64_t) CaliData.dig_P1) >> 33; + if (var1 == 0){ + return 0; // avoid exception caused by division by zero + } + p = 1048576 - adc_press; + p = (((p << 31) - var2) * 3125) / var1; + var1 = ((int64_t) CaliData.dig_P9 * (p >> 13) * (p >> 13)) >> 25; + var2 = ((int64_t) CaliData.dig_P8 * p) >> 19; + p = ((p + var1 + var2) >> 8) + ((int64_t) CaliData.dig_P7 << 4); + return p; +} + +// return H*1024 % +static inline uint32_t compHum(int32_t adc_hum, int32_t fine_temp){ + int32_t v_x1_u32r; + v_x1_u32r = fine_temp - (int32_t) 76800; + v_x1_u32r = ((((adc_hum << 14) - (((int32_t)CaliData.dig_H4) << 20) + - (((int32_t)CaliData.dig_H5) * v_x1_u32r)) + (int32_t)16384) >> 15) + * (((((((v_x1_u32r * ((int32_t)CaliData.dig_H6)) >> 10) + * (((v_x1_u32r * ((int32_t)CaliData.dig_H3)) >> 11) + + (int32_t)32768)) >> 10) + (int32_t)2097152) + * ((int32_t)CaliData.dig_H2) + 8192) >> 14); + v_x1_u32r = v_x1_u32r + - (((((v_x1_u32r >> 15) * (v_x1_u32r >> 15)) >> 7) + * ((int32_t)CaliData.dig_H1)) >> 4); + v_x1_u32r = v_x1_u32r < 0 ? 0 : v_x1_u32r; + v_x1_u32r = v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r; + return v_x1_u32r >> 12; +} + + +// read data & convert it +int BMP280_getdata(float *T, float *P, float *H){ + if(bmpstatus != BMP280_RDY) return 0; + bmpstatus = BMP280_RELAX; + uint8_t datasz = 8; // amount of bytes to read + if(params.ID != BME280_CHIP_ID){ + if(H) *H = 0; + H = NULL; + datasz = 6; + } + uint8_t *data = read_regs(BMP280_REG_ALLDATA, datasz); + if(!data) return 0; + int32_t p = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4); + int32_t t = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4); + int32_t t_fine; + if(T){ + int32_t Temp = compTemp(t, &t_fine); + *T = ((float)Temp)/100.f; + } + if(P) *P = ((float)compPres(p, t_fine)) / 256.f; + if(H){ + int32_t h = (data[6] << 8) | data[7]; + *H = ((float)compHum(h, t_fine))/1024.; + } + return 1; +} + +// dewpoint calculation (T in degrC, H in percents) +float Tdew(float T, float H){ + if(H < 1e-3) return -300.f; + float gamma = 17.27f * T / (237.7f + T) + logf(H/100.f); + if(fabsf(17.27f - gamma) < 1e-3) return -300.f; + return (237.7f * gamma)/(17.27f - gamma); +} diff --git a/F3:F303/MLX90640-allsky/BMP280.h b/F3:F303/MLX90640-allsky/BMP280.h new file mode 100644 index 0000000..b5488e4 --- /dev/null +++ b/F3:F303/MLX90640-allsky/BMP280.h @@ -0,0 +1,96 @@ +/** + * Ciastkolog.pl (https://github.com/ciastkolog) + * +*/ +/** + * The MIT License (MIT) + * + * Copyright (c) 2016 sheinz (https://github.com/sheinz) + * + * 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. + */ +/* + * Copyright 2023 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 +#ifndef BMP280_H__ +#define BMP280_H__ + +#include + +#define BMP280_CHIP_ID 0x58 +#define BME280_CHIP_ID 0x60 + +typedef enum{ // K for filtering: next = [prev*(k-1) + data_ADC]/k + BMP280_FILTER_OFF = 0, // k=1, no filtering + BMP280_FILTER_2 = 1, // k=2, 2 samples to reach >75% of data_ADC + BMP280_FILTER_4 = 2, // k=4, 5 samples + BMP280_FILTER_8 = 3, // k=8, 11 samples + BMP280_FILTER_16 = 4, // k=16, 22 samples + BMP280_FILTERMAX +} BMP280_Filter; + +typedef enum{ // Number of oversampling + BMP280_NOMEASUR = 0, + BMP280_OVERS1 = 1, + BMP280_OVERS2 = 2, + BMP280_OVERS4 = 3, + BMP280_OVERS8 = 4, + BMP280_OVERS16 = 5, + BMP280_OVERSMAX +} BMP280_Oversampling; + +typedef enum{ + BMP280_NOTINIT, // wasn't inited + BMP280_BUSY, // measurement in progress + BMP280_ERR, // error in I2C + BMP280_RELAX, // relaxed state + BMP280_RDY, // data ready - can get it +} BMP280_status; + + +void BMP280_setup(); +int BMP280_init(); +void BMP280_setfilter(BMP280_Filter f); +BMP280_Filter BMP280_getfilter(); +void BMP280_setOSt(BMP280_Oversampling os); +void BMP280_setOSp(BMP280_Oversampling os); +void BMP280_setOSh(BMP280_Oversampling os); +int BMP280_read_ID(uint8_t *devid); +BMP280_status BMP280_get_status(); +int BMP280_start(); +void BMP280_process(); +int BMP280_getdata(float *T, float *P, float *H); +float Tdew(float T, float H); + +#endif // BMP280_H__ diff --git a/F3:F303/MLX90640-allsky/Readme.md b/F3:F303/MLX90640-allsky/Readme.md index f05327d..44789c8 100644 --- a/F3:F303/MLX90640-allsky/Readme.md +++ b/F3:F303/MLX90640-allsky/Readme.md @@ -13,21 +13,29 @@ Protocol: ``` +dn - draw nth image in ASCII +gn - get nth image 'as is' - float array of 768x4 bytes +l - list active sensors IDs +mn - show temperature map of nth image +tn - show nth image aquisition time +B - reinit BME280 +E - get environment parameters (temperature etc) +G - get MLX state +R - reset device +T - print current Tms + Debugging options: aa - change I2C address to a (a should be non-shifted value!!!) c - continue MLX -d - draw image in ASCII i0..4 - setup I2C with speed 10k, 100k, 400k, 1M or 2M (experimental!) p - pause MLX -r0..3 - change resolution (0 - 16bit, 3 - 19-bit) -t - show temperature map -C - "cartoon" mode on/off (show each new image) -D - dump MLX parameters -G - get MLX state -Ia addr - set device address +s - stop MLX (and start from zero @ 'c') +C - "cartoon" mode on/off (show each new image) - USB only!!! +Dn - dump MLX parameters for sensor number n +Ia addr [n] - set device address for interactive work or (with n) change address of n'th sensor Ir reg n - read n words from 16-bit register Iw words - send words (hex/dec/oct/bin) to I2C Is - scan I2C bus -T - print current Tms +Us - send string 's' to other interface ``` diff --git a/F3:F303/MLX90640-allsky/allsky.bin b/F3:F303/MLX90640-allsky/allsky.bin index b8a8e40d1a59214b4d2f2af854e8e67c50ccc365..81bc8a85d28c70ba9f98ce9381ec755be039b4fa 100755 GIT binary patch literal 20624 zcmcJ%3s_TE)-b%!B_RZq0A3(cdmzXyULtrwv^9iq2-i@t)uPiTTr?m80$R)TtHDdF zSZ5SEtx7vuwc2W10d=O<`hnLvt(`Y0&SL>K>F|S{wMGbp!+|m_y4-i>;ER-r`zLB#N@okqxP?zM8Jk-ZO5dHsO(@< zR92M!Hvd6a3a6%m4OdOxfAlPsHsj&paHJp_8%<3<^fgIWxIxHu5=}(H5e?D!@CMOz zO2b$Z*8Yx1Dx^SwdGiS-H%bci#8O$wn+qD#*qe*;T3 zbEi3k7^EW9^WPL9ps7~F(;wbtnaB90Ow-C8nwYjgR(3oqtVkxYkcpISBdy}eX(>Fd z{6N#+;gc!Jc-i=Z={G!`se3rT+E2ZKQ&D+_$~4p_&^917T8h;EKu^akDf|DTM`dD4 zFaJyZDfhXQ$SlR?Ze{YVw~*LrX2pG9vg+>S@acw`rf1VBr+T!$8-*d884)(KWRpw% z^0NcKb5c(9qQBFe<=@wyML5w5qCWMDTR6sdB_jINFI?o&_Dj4r_cF)`u!qw@66OST zl-@MALAD?mrS864ZXhP ze#jFoo6|y}RtGJ<7U?oCtZhs7MQCh^cU)f;#>#(h3-U znPEkVr={tB85pLXWDbYgMPto3ihMGb77^hd_Yx0^v3KZQ{@NBR9ur}Ffd5G%B*8MS zhq0Ej^W!!&Z)X#4Je&QjzK+$h>d7%}>c|7FIyRj3xF>i%vC(8h>qpwSlV3XL2awmP zW-OB!)7VV)!}+TSbseW7Vhy0nQDhe}_jv(Ai3!#6xhU-w*QQRHNcGhA= z;S!c;@VL2QVzCTnJ+FjPOn|*XC;eUW9tqTg@VTG}Qc23_j#Y)T2i*H^`>}7Gu zQlMkM#3%4{Sp-k6pc8g#_zuL3rkQK6>EGvnVb1m@2NTLd3#oj?f|M^e|ZmKl5?n-<46^I%>B@WYKVD5-GkddYiwi+Q_L$s!J)3?OdJ_r%r)+6s$I8Y;dXb zf(?N%OQY$_*KYG~ocVf$FBEgZa*NMWrG*s$Z*;;s*=GW8Z~K(8zw)*Rcx!KMw&v(^ z)6bQ)KAhV?+lWYo+UFZMBH2piapq|&l)J+-?_(1a?etP zQr~@L9Bsi;^tk22?_1yJ@2i-(lu|5=$oK#W^yF=R@^mzHONR|gyH(z|^&NMuGju+n zd!8XQb)D8CvJ-zpHAo0hg2?{SPaOo$p$H6w?+f*SU(1`Z#|< z9yjv5Dzx`$i&)ubktm}=U zi!b2K2fS|8*o;Gb%yfeBxNi&*!0BC8ycNR_@p03qKn_zPlyWwvjfe zLxeC-+S)vx$F}VEkf&kJ;JIHHj3lEo19YLLfQ)dCAY{iw4auft7n+`IXyB(1WZ%wJ zgsnZ$97!-Y3`-5`c@-%aEG3yD2Mf0tk!l19)21^->3JrCNpYdPJj10bY)^0*g0is8 zXVT>+y$h)#ID%9imm4Gn`KHj!v(9xSii`U3DnBEr|70USC3$Q*{1Sxi{FvqeGlwRr zeLr5f0U+o2M9n#VMg)|G`(_fhvw`M_8-{#?+@!%2zsB4U;Tw2W&`m1^Tz6Xq-VzCc zH--c|cbr=5!y+Z(c9=<)>nvuCbg!Kczzk_y?UTQ9=vGxZCP8nX+9Fb2rv&Mj0YH5?Q88ql~bOS&JqcEVubf z?YOR}3u@^BUxX%iTMjr?Vv%hVm0ZkV6pmbGy$ z8QEg6V5nN3*R{}pFtV;D#SezEG9I|R(rN|~G-)UQvph^g?t_nj@KUw#{ybPAv zZY%;kXj52^{6Sx+iIvAE-3-yeDo>jEw1^PPICgCMxa^MyDKd<`DuERU(M}b)&!#4Q zCxZp%K@!*h*fK={xp`zFZe67?^K5+O*QkeOD$_(6D13=#J)agtMBT7TSd8Ia-)B>Z z`Tp|d%d!llfbw|NNfkWVc(R9$kg-7LxL-EelNRyuAPOef>k?R_X~L|SZ!F0Z z&pEHlSk)7HnAY|d*$Hax`evHVYau@MxX%o$S~hBE3$c@qA+D3nveSCbqZ+W{U(AQ< zcyM;vGeMasJeVhxWD7BUKX@iPS_l;#BfX^zfpthFit(}FQWVEqaBHHN!46#ij#ZRN zM})4)UxVMgd?Z4j@VwJ^f8@^{Cv92&^fU21ITuF4=Ugu}4$`E8RFHiF&C_qtJXJ>X z}cyeg~0AXl4Dxd>`fcHrZUNvQFdf;Gj(^;|iY}Pb8d$-^cQs(}HZt1hsj| zR}8eU8ThVDo0>i?b6N(932F0V15I*MASbE7GhYTXKQ3clM=QTLXiMv=ld={fd_BJ; zC{l;Q_1wCmP{Ub%X^_@{J*dhwFP0}Z=7uNSpMBY|wgn($1D_vcZuP`O+(_m;tCC&o zr;pbq8*Z39({C7XjX<66ge|T6Am_DIdQP$m+X%TKS+Egj`Gr9jtr+v2)^PT!HMtul zCvylWnTx}S~tclJ56&QLc+=fA2k=o#1ji%O7fH&w|PtZ^o)4Dn)6hrxEd1R zxi-&mkNvDKq(o=rWQkAjwY<{N z;2Yn!PR#T~{9r`btE*FY6Dha(%J$HV6b=D(8CYX4<*^xKvsYmsodG%plCBtmYL`H@ z38>x#s;@qw`nyzHhk?Cj^~~AQ>WL+9oaJfraVw@yjZiih<{3JzSU%X#!TsY-BxWb( zT;p?py3KdBLrc-pF7Qq(kC)sPr*;BdbHFy57EPXi{dh?v?d(){gPg(oDzk3$&FyLz z$~71W8#x-+1ndHD?R_OdoQ#a?00?QGAc~2*zPCiik%+Sd!hG0QuqyQzToy4whg%5A zfZE{+$0S{Z&_7??Zu|u39}WF`mhSB&818hyh{U+lcS=$?8Fk0VotClOX;>~Q|7^uI z|5NmdP@t}`v{)8yjbp!01;Bn$Qw|SR2QHQamU8LbUAELS`*5eTw!A`%9VZknx{?Crn zytw^?fe#*$5t-~LrzEqKM97Bp?Pr4&V_N9h_`>nC~Q4)BJ!QHIS1MfUdw z8TbVrMrZKEo#z)e=((+EBfq5KIv=T$7-Fxp-Q<%Q;E79Y^F)l-ZRGO{_BlONQ!97_ zFAx#Qh6DWKc$w}zzcgZx)hoT-))KU(RR#LqIE-Q%Exx)~$h(JpbicE*BXjd_RzY5% zA9!9xg50p4V4VTkr3GdM>7lO$W_Amf*d(vSQd*uf05l%!B~k zoLdLI-JBd+5NZl{RVF99nB-7{-nBRRN7D_%%cf+mop{->3%sDps`J+>ARM9`K?KWa;#-k=BRA&t5m>M#9<@muO@tRXvGm@NDIMig^@o!PX2{PVfv(PDjUy+Xy>pLw~s%e3A%O1RA5qS z?-=fw8a2P%ykpueM~a^exXxWGn&hp0+sc?uh8~_kahv&j!V1ctmYoQKKE0C^eDYm> zTM~6rb{d{>qjhv&tkL8C0_I2GOQ@_o=dOy?7uhzvKa4W+ne zc#M^z^46sOU zn%Iw8hu!NuqcYF&mW;q=)oCg0;UFJ}^FGz60c?Y#}0$qKcoXawed!Nc{K|uJ#| z^hdAsN9*Q*oj48_Dd5Lhav!Z<=2hq=Qr_(FxQ(9c*1ctg#mVyeke{wyv2qrRm0ThC*N*hRy+wB1A2e$6^xw8nEcUwcTfDv*F&)JXbl{TU7VM=Q0l&cOt-#Do!?KHM zp)(rz$>Gu#qU8f9ee5TxPK?gp(EJCexsCTh6z4%pTBKP-1o+eh0~{|ICSu&Mj#-lY zS|}J3wB6(O^`M55Y8FW81_qm!Mps= z{SmTle3a@!FEuu$*GI99pQM%n6{sVXIAro|u+nYgqYWabM1;7K_2ON(?W<~Acr|L_ zH3q^@;@DogGKY8%=sqMN-`-0_^Nye5$N?WWKA>slEXjcmFrNAXe z4v6~d)d0a5apGOjW)kG2C<0&^q6Wz4?_lBFj3gUIIp>pf1Hh9;IipG5VXFVqajBka zpm!604C*JIr5mW-$- zoTCUR*5#k=)%cz56$$bT^ugu2wj)l7pVjeEeuHRyKQRsJeg*JS&cW8+b8Lk2!%nqy z;^&SBME*C94W&!KCKFUjw=Xryxl@v&Wmu>o=+c7+1k_3`wK~?9j&iz@OIkq!wHStbcea-b z{>Jfk={T_tNEIxkTBMiQ$_=94(N~U4)o&#%_!$2($50pGXZGH zF)d8TTh`kl-rd??qiW@AWIILY;8YYj$JTwAt?4OOH@3ztsF$_?UoZkmJG1xLcgb(t zcO^lR?*dputEQ(iIas@5fw<`yUukIUU&9>Z*T^mcry#F5$|)fUMWY(`;IU5lNxyMK zmtvZL#pq$56NBe%HO!cvm^M7$-%P*QOU!~gaZmUDf?ZOIxrUbd*Fr06+po3;fh2I< ztX`kVxD9dB8M{yEF<#wujIYWq%N%gnYxUg|pFd|8*NU4O`)g&#_%{)jL1X`KWg_AE zhODvwf2nAw4fBMeL60Q~L^v@(^+FE>x$};1O22fR&v@H$Fo_r#-FsO2Qu+zt{{pNW zV55>6T7z17$Itm29Z_+AcA$(4j)QTWLz{ocLF9{pMtd)jAcCi+r!1w}Q4F)=Vkxwp z|JQ{uQ$bGKlcF4oQcU4X$A$bJ2Q9tecnfM#3JE*O!9dB~3_NC-pXH9XYR@|eDfj}8 zgL1JTBRtpcl{Pz;=O1#2^P3&o+JladKu@*#oFlDtrek?6z)F8|9LnG4@PYXx`?k<= zuym5+tzG3>8+0RaC@ zfL{gpe_H+F{ebZP9lZYy=11g9i1E%5MgwbGHl%rp0K3Z~$t_46Bv@lJqVqr}q)%*!Ry$Du$ z6iIc3X`j&^)JT7fpAZY4lgHgLKuvVr^0<$H2iMcVu&`3M;d-(YH0%KWIN02`_f`5j z!7|`O$<(lWXJn^a0ETW|6;@u^iS@8j;uSVP2dl1%06*!_01*SVff_!C@woq&hdCM9 zeYeK@j&X?y3Z&UMz;t(wKyjPL9cro5Asa9||8Bl*F9$7XuRRXhfMK(4op7`ZrQ8YR zY&E0E+xF#9Y87DW-p|cQVRl4Isf5Qc_mdiNKe}OkeGFGz+hbo`iy@4|M=EC@!}mKW zs;?6%-|ToHJGM&(x{H*#nYkH_zfkD2HuB-M1oB}goXd3jt>>>#2!V4e6rGlxo^x=J z3YW9rNzU0)0?*m%2vt03OD1Dk5l_S-;kl zpsw0uN*nD{YAd0qAKQ@sYv2b=WtThNu6+b^2r!er7^I@*?4U#y52tcI`)p|>+}Fh} zOYMPEr1sWxDN`f57b>-LPO2g(o|sI|{p=)>5CS`Gr92{soO^JPASG;vgs|0#kZ_`x zO8UkCW~}o%ntb0fB|=__v8O1MmD?tMYHj@CEJ%Wg-fnGy@gO2ItdAwX&Ruo%X-VqZ zkYaw{;-kx~KAFB5=lD(~1rl0i^?25;L#S4J#UC+`@H~LA#bY zt=sLW-aHfa>6XQ@u=e8hw|<}V4NBVOaxAaMADU}tK7vcidoIbL@iJe_#`1XH5p((avU_^sKIE~S_-lgItn;k^PxshePW zT&5c-Yac0_^i-K?q^xzMOv)^QdbjP*fNuWfiEd(<=-Pmv-5)#6^l{)5w>lg8lW+RQ z;afZG_^U9E8+vJqU%EZ*GtGkzj6pliq9rCNvoq}r%|AL$KcKf_s1)zUlb4u6(&#iK z34`PhX$+)_pbbgbhsN>?$VN`H4@k~*2->ujL#xt>CK9aJ(&6SbTR9_e`nAT{VJnAr ze7xE}!?0$= z>0Oora!@3~tHp_ab<~Oeh2!u_d!ipjc~?xY=k=}J>WHefsHiT*Lbi)`El2X*+O90$LG zbs`yjh7{u~cU}Qa%vM+c*3)FDI|TzC%iI?++ihHaoH**D(B@mEe9qd`39mJHN*3YiPm00H0i!(6~7T zOu^Ysu(1U9#?!Rw*Y)`w^-`-G7LDwW1)Bk95QT%HiR2H7I&1}IXHsxic9nndmdD*c zM6=)lSQ2t(=z7bsbyUslEw{HWF~zVu(|XIF6YNQExthJoBlEC3bg&(uUVBJPi?)!{ z;yQZEqXgU8TOPw=JNdYl-lZ-+(Pv)QTRsg=Nx`dmjbWQibAYaW!aS`L>V=Nf^U{l9 zk6Q75-S!AEhplv?xxd&UH{Y^9(VQ;26&@tx>Cn+_Q#7Gf<44qp2}% z42zgkaGD1863*f9I<_PepCp4c;Vx8eumCTA1J4O@o6MrMFXk+s@)kcim#`S7ywQ;d zr`EYUa7+7*B$L@=DEySCRYPv^FbxnCvjXlgnYzMApcj1!PVkBFUZ8dqsJ&vL*533X zt`YNkI^^M-0eZlLM`wWrnT)1=raPwR^a0;6CB_o!8$uDWHOXw4vIWi?Q-R`j^Lu;@ zK4*+>LYMfE%GGerxP=#~w(vAF1ZTVN@v-Xn_&C)8r0q@_1HF_2*4`Qg#=YAr;Hm)^ z_-q}*CG9N+=+hK6ZJ`uf@M$I^>w1q@G8n%~A>adjjK5pJM={+n0-#-Q`-J}kI8THA z9J6TFQfpJXNyVr9`lJnOx9}U2sMpWJeQ*HmWy#=A`AzVyd3}Pf_wDVZ4X6pgH9%%9(FTBC^4js zz^AY(LM@yXkBmKDDY4h`hh{wDd(G5ZeAoSf`Qt3j+5^r?5$*IB6S0!E8SH~)hmUFJ zs+6lB_92*SCMuEl-^wSs#}Mx`Fn@*pYcAkPX$!SNs5yHCGF^BoDUW+DjIEmcr+Hb9 ziX97%EauH(7|&RH)EDoi=LooP9lpT5~$PTA_HAk$U;650-aj(6T8 zxN#;_!)_NZo~SUj_Y`l`0=}(W>{Ov8ruJ5x_gb*|I$Jq{H7Lwv;U9>H-6#0HX*Z!R zf-|(0Lt=K9l?_v|)Tcx4ox;4tzxdr&1tZ9NTL_|OgpHQCx zo4if%t@1a%IH;hT+;^sem_yw^)kB{I` zn|?ZyWXJsfOLZzF354`Co!6!z3GUxQ=o10f$L;Z%*xqB>uye#`^#N@Nh4B<->V`tW z%eIv_2 z4Ekd$H$Q=sBk)}U{I^f2_zP6D0+lsSuxY@?KKptBd({){F#`4!0ehtY7580y+1EM7 zSNyP3_sJ8F|5>4SA^#=t@at+Qc>EHt$A1Ytep~r(MrfSP9Rt4n?S5PN%K#Dg1uoo~ z_7UsrjPxBd!@Gy@QcQY^;zzK)+RAeP|2Dt0Zf6=kIkA;Dzk+3T3-(2}a-BdOyyPcv zivUj%;I{Jj?2?gwju!g){1dp(2%Hq)bDqG(0({v0P>{fk5%?}U8i98U@Hhe94X3n$ zd5w0dbPPyi*nLYVi+MuJcchL*fGY&J*trnFeE`UTd4lIRg7>w6H{l7~e+1qPaP2Hr zq<0{}+>soB)C}osNS7epg~UPPA<;UBDTE}06atBXG#SziNNPwr zNV$+~;9+6!3pf*esK<88RzA*7OY!IoyH|Mdc)vfw;lJa(2U5a##EfD0OP;%}U#88N zEOyf3j*jI&54(5t*XKn4tT1f>`?w{;Rt{%SBp%7>Fk*`UzDZa1=FYTDnb^*56zpsm zk?}K%pcQZA#I0Ge#`otHZz|W&inTc_)=)pcp+`}#KQmXjzk!&VS-T)MMPWiy)m&@c z8fc;C?&{3SjH~>@<^j%_&z{SemD9i{ z&ptM=p?8VzddRK4+t#c9y(T>+^IH??>M96M`ObhLsBH*NC?1*FFz;@PKI+%?mL|}G zcVk~U!vhfaQXSnC3lTnIb`Gmzhun)EJv&g}iy#&fDd)SgK6`bb-hG?@x*5`T6otAT zlq*b&!Hc$)d->A?e=r+I^e2%g{qV?Md4!?FEU!IyDfP}xSiw+OdJ?qYfHiKNYxL*O z-1W<#a|npaQqSJRZ&8iAyBKs4+g4S&_xvUjp28Xeu?25H8D2-ehUaBSw;&;ON%H`n z=f3Hi`v{&Az#ju?1|(!hT{IdH{SpFqbx^hzl6RFw(bW4x?mL5KVFkT;Fs|0pYvm6b0X%zS><@NjUT5XxafW~Wxng{Bcq(- zeCM%AEctvUOCQy9jGvy9N@wxpf7}T3=K_##44j*deyeSfN>W+JqpqzQeRo(G;q<8H zyEY#o*aw2oKm)ID0f#6VONLk%0!?)=Mg?7^7=%8;SY94 zg5jqIG2B}he7DKB;Z9cxZBG@r@H+((S1pmyjfbpE^jGYk?}3k!G6znF$%v>N@Km;g zrvkk_JM3}yeT6oUat4!BIKHRwRZbrNIN0F7t(P^0hxMNC3%kD%%C$UY8a0C*i1X*MPUF2LZey?BJ4Y73kI&}Jx?{}; zAkI*_BOlJ{DKg)Hb3vScfS+YgI^BiZyl=Ecx|@iv*GL=suI|i~n^1TJXReY1w%qhS zA-(o)It93L(;o=>+GEUzSGNh68kafVi}@ZU?c6egxt*drf5qICzCKfG+Ng`*#*-Ru z3@H_E3{W`4l#jIXE+Or#9jV(uJYBaY)62VtBY}VU3S3J@$_sH%NY0;>#Q9sw(nUgF zyzjsE#&y+{S)waRf7!rA>bdR7W?rE-WZdR2*P%&1kr0og{?HI+*lf%zF{WSQ7mrVI zwJY+BnjTp-Y9r=8f{4rQYQ3vGQd^Lpu^=-)UC&+9(uT2&%s@uc%)7?z#{5zoZJ1v` zbP;n%OMdD5J_FZYfRp;HhnJH>4J4D=^Ws8v&riu=(BlwxmjUpSNXt1Bj%`R2_V04Q z8glO%f>>`}3z~omuqHg#1JY^MkbPwQ^ST`+&u7igOoq7a9YzA;mKcICbd(SbL~Icq zMyANi@yv{b^xz`cVH$#qfmYGgqP!wm5$?x32EwGuh8UVeK(U`yn;nwx8d<$A11WJP*V3D|mi|bpd)c?;Di5Q*U>Avo@9+C3e1Cs*f~;}KJ!1$? zN)&&AFL9tOp4id6QqLL5lDAh4DBF)_6c z`@W8b-)nW(`J!kn>n1r^`Km_Ex`)pn9dd7f;x36Bp^t`Wd~OZBb1gcG zM)*^6i5nPb<1W8ZL*69dbj&dOTNs%P>(e8#&Nc6--zP)vkNdGi5|5tdCo|CVgx#P+ zc^hDa)Kjbo<_;3((iF`PgH%&&x52^Z3S&#qZ@KB9r9|`eAl%vuogW%Lz?6w@Id|o@TKsj zgR^5dzeQ~Z?fK*S!Z3_m4}RW%#cz3n4>&=uWQ^*1jMG^1qZm1VyB?4G5}=KGRmU`e(I)o+ZBaGD4p$y%yyMxE49)IPf;LqRY zU%N-LJ2bIB!dz76O$^r(a}?V1FwY3^vnE{83@^vn`DeKUo0spBb*qJmSl8hd_*Z>IzcFhk~*rJCFL#460gmVs&1oWe63t(P~+ zn?jHv>0O`;F<<=m(oeQNqcca~dB1m1ts(BgxznTOpYhD~1KWVVanQwso+mwWpZDCk z_sD(5gLQ`hZ&CRLp6`LslcFRd0pm=(N5kw0k>U9~KU@6(DO z?~@987coe=MNQ-&^%~to?%qF0Dsb6ct*DW1Ix`;b6(7-q#N}tDdx4&)$*4{1rk3lw z6_fqo1WcAkQm6QGDGG-=gk;xOAz3!cAMUUTx(UTYtd|eHE2|G>?Hi;N#Grq|V%g2{ zbAo{KcDPlP|9GDc!VxGhbd%KEj!$R^OePRY-r+ zXAa8h?wt25a7Osc*beA9NMhHi{u=nss z{cpx)1*iF?b)QKi2kE}6<9!CH7xxYND3n@we*|xrPxZf%fgxDlh1#FTgPh%>#shx)K1oTqpY70hFjbxLi`$AAqh`#%(mf9(5(srw1tP2ZMpw)9@f81-2aV`WR{*q2PF4z0+LDNz!l3h)3qTkfNCC;_^6u(LOQQv#hL2|vsAlY*> zi0VTr4M9a;fgbdH+X*=BhVf%84UkIk; z9Nb`Tu%q1@;B?}govMHRZmRG#L-<{-ZZoXoL+(8Tc;&e`pau(9GK}-J!$EibK6&K+ zBgDndcTN8cRs+?szT9=;7ChU@2=9D{dCo-M9!e^#WX#TDr(^)>WNdr+Y zlh7ofkc%ndwmt4|PZ8jU6eW4w7fwwM1-k@x%7E<^{NcMvd^RRIfg@;};qDo6i}*2_ zjs0+wk}$?{9HKk$9csYeAfiObNpHcus*sbwt}3%XLb1p0E2RuLR!edkahiUd3s(T- z7m1qEAui!0YO5F4NV8`i?BkwpE4+X7NLzkS+7kcmwq06(9PYny%Vg(c%47RCJ3f&< zrY3*s5GAj%>u zA5s#$<0sI|D$UZF9X=Un`W6LAv}9jI6`6t{W>%II>7XGh?;D7e5JM~|m2t?S$iM4& zJO5{hulpGyjc}CSJ$NdA{1dbSaZ$C=@XR~)8o&-Y&O^ItQvBW9ZTrkxjIkGDl^|9R zFcDxz*CM6e0(KX8nOb@q61KgE92a4}sip5b4obVM!KW&r4$zYRV7+aREZqYMQ!jwX zKC5G6>1@X`0lUHRmh>-HFRcmEJ@yF@xnYGi%+f<3ov*EX>|>#)V@ol$Xh#81+&EJ2 zp!9QCJDI@@#Cah6$oqF09$D{i9{>ELgrw&ur8hjUw}d?(`@6#Dj~tGh4>3v4tCs$U z=g8s1&v&@T&o-}`J|AxTLDr~k51c^aCqnJ(h|CZZ&c6eCC)MuIAg3n*!8dra6YzCZ zCY%cBcfdJ;>@=yR*hUR5!6ya%R&QK8iW}?*krRoPX}>*LbrQw??>6{qkVDDhE=e1T z32Uo>Jj_Ly%^#io+$r&2d7u6(iuJvTl=7R1o5}yk(_O-P+FY5Y))?VSK%|@iB{5Hx zc;O2)g|-x`GR^x$-1z^3@oSmyQ)Q3q;ahFVhcx>R#Q(sq(p;|@&!*jI9a!I^X*ug; zP$a~N?@+(5hBeavU5I2KuT%JcJ;*6v%_}d)u;ArPAjqi=Y5HXy~n7Pop zT^e=IxH|Irx!NW21Fgpfc}_AYOVD;xS~NI%_+YB4f{X3-?jVbo?F1+vG{le`20cTyDpA6M%~xXrCvdjtve!3ils97?zOS zO(jbP{d>1+d~UoR;tza~SGHb|3?hDSs?P`y>J$zqH43He=_J&%OLiqy^SZ@62!Eu-mkedYLMh}p#5!uZ% zbKA^VcJ*ej?CL;v0^dUa(4b_be;~Vg)zH6HH^{^}+VA8bId-k}qLcBb2E?w20sr2x z6lTERO$>;8_1$A~!Uz1F&!)`99P~dsun{;2#T+F24oU{DkF?sR)fcP->3$aTY!Uk)lOa73IsB^3^K~imNbwdVES= zZfdF))6GlG$;;6{homcB0tTTy;4^+Eyl1Y-U6{NOqd>mq*SrA67yvhxRho?6A}Q`z zD&`#Exg~3h%b1E~%c_fOm`Y=nab{|y7j zJqq$mhmUkL$=cMMm(myLbFx!&7D~~=)a=~U1<4CvT<};X3sUp);O$jiZZhUO_wjpn zw!o|I#q4x#`ofoS8Ls@QHg%DZe>FQb8CS$~pF`PMi}PSy1fqC!Of@x?)k)K)Eib7t ztuB~aSg~?iaZ%Ch#Ve;(*Q`v4uQsl%EGw>_R#R16JZ+`18irvSn~=o9*Z9+b)Ew2U z8LE}5%W6uPg4HEuMT&%3jGBp8#m}5FT{T5DQ_8^KN@KZkdGX5P@){UYrn;njd08=& zzJMvHD60?#mU&D`T2#(VVT!7ZuQBB{CZ=R1)MHA@ndH2b^mOTRT(}%G;@7fhV>MG! z9S!hhWfjI6##mKld>ywkdsgDw84MnvYH1l1mz7jQ>Awz8y0%)n64R_URlLU36tAo- zt}@oFt||tOD>0EL?MQ2$pgn;x##O6Js!M80D$1Ffl9k0$4N$5oE-5dmVKmvP@rf#F zDnKy5#pP>CUwIF6~alz{4%R#zKMJ476ri;-C*o7uzIfxMymoXLg#%3iQ3IhFMlsv687RuBrf)iY|1Zj2z2^4Q06e=KvXPtjObf_adpSAnvC zQ3dK%#l|AY0D&T~8o1NbXH6*pH39@s+u|x|`fDSlm=%zZ7d6IIymneqapAOz!kTFX zCD0^{D-5$VU65#@v0NCTg4Na17X=8eT3rLmvz&>BNdwpwHKyW`ZY(o`b|C~3)4!Cu zfX&h`5^N%?e-Ty#!PKL?l;j*(8wB`^dC3bFzWOU_c|{F~qofAgwAYNl{8IwfW+w}s zd{S1ux}XxK_P@U~PazgnfKJw!fLDa3t1=9WlB7xAVNm`lO)s|X!RmQ z5+p?+6Q9UI^GGCfNq?AO+M^s0u z@q@q*^e)_gK_q}g3n>v&BqXU=2FQdOnm_>oJ`_9EHLytq7u$#{c<+M1;)qVKqp`_sjU30;CROufu-x7Non79zfzCso;-c z1RDRNpErQUW=IDh9fx!o(io})Re|egL}l==8m&ODBY#STro!RuR5TsUqOwsIT8!qv z`z$mAei^_jM& z5rCt{y*P_=5Z<&Fzj*&>IM8MyBBNs9)dPP>B!d5x4WZmA@IRB`%u|0BfWf)pzx?Im qJ^Pq{0U>5!g-R5u1mu0c8tX_?uBwnO>9*7(2w_J087&BONq literal 26544 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 G>;D53HN15I diff --git a/F3:F303/MLX90640-allsky/hardware.c b/F3:F303/MLX90640-allsky/hardware.c index c5623e9..bb9d581 100644 --- a/F3:F303/MLX90640-allsky/hardware.c +++ b/F3:F303/MLX90640-allsky/hardware.c @@ -16,19 +16,89 @@ * along with this program. If not, see . */ +#include "BMP280.h" #include "hardware.h" -static inline void gpio_setup(){ +static bme280_t environment; // current measurements + +#ifndef EBUG +TRUE_INLINE void iwdg_setup(){ + uint32_t tmout = 16000000; + /* Enable the peripheral clock RTC */ + /* (1) Enable the LSI (40kHz) */ + /* (2) Wait while it is not ready */ + RCC->CSR |= RCC_CSR_LSION; /* (1) */ + while((RCC->CSR & RCC_CSR_LSIRDY) != RCC_CSR_LSIRDY){if(--tmout == 0) break;} /* (2) */ + /* Configure IWDG */ + /* (1) Activate IWDG (not needed if done in option bytes) */ + /* (2) Enable write access to IWDG registers */ + /* (3) Set prescaler by 64 (1.6ms for each tick) */ + /* (4) Set reload value to have a rollover each 2s */ + /* (5) Check if flags are reset */ + /* (6) Refresh counter */ + IWDG->KR = IWDG_START; /* (1) */ + IWDG->KR = IWDG_WRITE_ACCESS; /* (2) */ + IWDG->PR = IWDG_PR_PR_1; /* (3) */ + IWDG->RLR = 1250; /* (4) */ + tmout = 16000000; + while(IWDG->SR){if(--tmout == 0) break;} /* (5) */ + IWDG->KR = IWDG_REFRESH; /* (6) */ +} +#endif + +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 GPIOA->AFR[1] = AFRf(14, 11) | AFRf(14, 12); GPIOA->MODER = MODER_AF(11) | MODER_AF(12) | MODER_AF(13) | MODER_AF(14) | MODER_O(15); - GPIOB->MODER = MODER_O(0) | MODER_O(1); - GPIOB->ODR = 1; + GPIOB->MODER = MODER_O(0) | MODER_O(1) | MODER_O(2); + pin_set(GPIOB, 1<<1); + SPI_CS_1(); + } void hw_setup(){ gpio_setup(); +#ifndef EBUG + iwdg_setup(); +#endif } +int bme_init(){ + BMP280_setup(); + if(!BMP280_init()) return 0; + if(!BMP280_start()) return 0; + return 1; +} + +// process sensor and make measurements +void bme_process(){ + static uint32_t Tmeas = 0; + BMP280_status s = BMP280_get_status(); + if(s != BMP280_NOTINIT){ + if(s == BMP280_ERR) BMP280_init(); + else{ + BMP280_process(); + s = BMP280_get_status(); // refresh status after processing + float Temperature, Pressure, Humidity; + if(s == BMP280_RDY && BMP280_getdata(&Temperature, &Pressure, &Humidity)){ + environment.Tdew = Tdew(Temperature, Humidity); + environment.T = Temperature; + environment.P = Pressure; + environment.H = Humidity; + environment.Tmeas = Tms; + } + if(Tms - Tmeas > ENV_MEAS_PERIOD-1){ + if(BMP280_start()) Tmeas = Tms; + } + } + } +} + +int get_environment(bme280_t *env){ + if(!env) return 0; + *env = environment; + if(Tms - environment.Tmeas > 2*ENV_MEAS_PERIOD) return 0; // data may be wrong + return 1; // good data +} diff --git a/F3:F303/MLX90640-allsky/hardware.h b/F3:F303/MLX90640-allsky/hardware.h index e3a96f8..d9fa981 100644 --- a/F3:F303/MLX90640-allsky/hardware.h +++ b/F3:F303/MLX90640-allsky/hardware.h @@ -25,7 +25,24 @@ #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) + +// interval of environment measurements, ms +#define ENV_MEAS_PERIOD (10000) + +typedef struct{ + float T; // temperature, degC + float Tdew; // dew point, degC + float P; // pressure, Pa + float H; // humidity, percents + uint32_t Tmeas; // time of measurement +} bme280_t; + extern volatile uint32_t Tms; void hw_setup(); - +int bme_init(); +void bme_process(); +int get_environment(bme280_t *env); diff --git a/F3:F303/MLX90640-allsky/ir-allsky.creator.user b/F3:F303/MLX90640-allsky/ir-allsky.creator.user index bc2d899..d7eea8f 100644 --- a/F3:F303/MLX90640-allsky/ir-allsky.creator.user +++ b/F3:F303/MLX90640-allsky/ir-allsky.creator.user @@ -1,6 +1,6 @@ - + EnvironmentId diff --git a/F3:F303/MLX90640-allsky/ir-allsky.files b/F3:F303/MLX90640-allsky/ir-allsky.files index 522e29b..eab2e8e 100644 --- a/F3:F303/MLX90640-allsky/ir-allsky.files +++ b/F3:F303/MLX90640-allsky/ir-allsky.files @@ -1,3 +1,5 @@ +BMP280.c +BMP280.h hardware.c hardware.h i2c.c @@ -15,6 +17,8 @@ proto.c proto.h ringbuffer.c ringbuffer.h +spi.c +spi.h strfunc.c strfunc.h usart.c diff --git a/F3:F303/MLX90640-allsky/main.c b/F3:F303/MLX90640-allsky/main.c index c2fc250..e7f91b0 100644 --- a/F3:F303/MLX90640-allsky/main.c +++ b/F3:F303/MLX90640-allsky/main.c @@ -32,7 +32,7 @@ void sys_tick_handler(void){ ++Tms; } -const char *scanend = "SCANEND\n", *foundid = "FOUNDID="; +static const char *const scanend = "SCANEND\n", *const foundid = "FOUNDID="; int main(void){ char inbuff[MAXSTRLEN+1]; @@ -45,6 +45,7 @@ int main(void){ USBPU_OFF(); hw_setup(); i2c_setup(I2C_SPEED_400K); + bme_init(); USB_setup(); usart_setup(115200); USBPU_ON(); @@ -82,8 +83,9 @@ int main(void){ fp_t *im = mlx_getimage(i); if(im){ chsendfun(SEND_USB); - U(Sensno); UN(i2str(i)); - U(Timage); UN(u2str(Tnow)); drawIma(im); + //U(Sensno); UN(i2str(i)); + U(Timage); USB_putbyte('0'+i); USB_putbyte('='); UN(u2str(Tnow)); + drawIma(im); Tlastima[i] = Tnow; } } @@ -95,5 +97,6 @@ int main(void){ const char *ans = parse_cmd(got, SEND_USART); if(ans) usart_sendstr(ans); } + bme_process(); } } diff --git a/F3:F303/MLX90640-allsky/proto.c b/F3:F303/MLX90640-allsky/proto.c index 2805c6a..78c8b3c 100644 --- a/F3:F303/MLX90640-allsky/proto.c +++ b/F3:F303/MLX90640-allsky/proto.c @@ -20,6 +20,7 @@ #include #include +#include "hardware.h" #include "i2c.h" #include "mlxproc.h" #include "proto.h" @@ -64,36 +65,41 @@ void chsendfun(int sendto){ #define printfl(x,n) do{sendfun->S(float2str(x, n));}while(0) // common names for frequent keys -const char *Timage = "TIMAGE="; -const char *Sensno = "SENSNO="; +const char* const Timage = "TIMAGE"; +const char* const Image = "IMAGE"; +static const char *const Sensno = "SENSNO="; -static const char *OK = "OK\n", *ERR = "ERR\n"; -const char *helpstring = +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" - "aa - change I2C address to a (a should be non-shifted value!!!)\n" - "c - continue MLX\n" "dn - draw nth image in ASCII\n" "gn - get nth image 'as is' - float array of 768x4 bytes\n" - "i0..4 - setup I2C with speed 10k, 100k, 400k, 1M or 2M (experimental!)\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" - "tn - show nth image aquisition time\n" "C - \"cartoon\" mode on/off (show each new image) - USB only!!!\n" "Dn - dump MLX parameters for sensor number n\n" - "G - get MLX state\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" - "T - print current Tms\n" "Us - send string 's' to other interface\n" ; TRUE_INLINE const char *setupI2C(char *buf){ - static const char *speeds[I2C_SPEED_AMOUNT] = { + static const char * const speeds[I2C_SPEED_AMOUNT] = { [I2C_SPEED_10K] = "10K", [I2C_SPEED_100K] = "100K", [I2C_SPEED_400K] = "400K", @@ -207,7 +213,7 @@ TRUE_INLINE void dumpparams(const char *buf){ if(N < 0){ sendfun->S(ERR); return; } MLX90640_params *params = mlx_getparams(N); if(!params){ sendfun->S(ERR); return; } - sendfun->S(Sensno); sendfun->S(i2str(N)); N(); + 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); @@ -263,15 +269,15 @@ TRUE_INLINE void getst(){ sendfun->S(states[s]); N(); } -// `draw`==1 - draw, ==0 - show T map, 2 - send raw float array with prefix 'SENSNO=x\nTimage=y\n' and postfix "ENDIMAGE\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->S(u2str(T)); N(); + //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); @@ -280,7 +286,7 @@ static const char *drawimg(const char *buf, int draw){ 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) @@ -290,8 +296,8 @@ static const char *drawimg(const char *buf, int draw){ _2send -= portion; d += portion; } - } sendfun->S("ENDIMAGE"); N(); + break; } return NULL; } @@ -317,10 +323,22 @@ TRUE_INLINE void listactive(){ static void getimt(const char *buf){ int sensno = getsensnum(buf); if(sensno > -1){ - sendfun->S(Timage); sendfun->S(u2str(mlx_lastimT(sensno))); N(); + 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("\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(); +} + /** * @brief parse_cmd - user string parser * @param buf - user data @@ -386,12 +404,21 @@ const char *parse_cmd(char *buf, int sendto){ 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 'R': + NVIC_SystemReset(); + break; case 'T': sendfun->S("T="); sendfun->S(u2str(Tms)); N(); break; @@ -420,7 +447,7 @@ void dumpIma(const fp_t im[MLX_PIXNO]){ #define GRAY_LEVELS (16) // 16-level character set ordered by fill percentage (provided by user) -static const char* CHARS_16 = " .':;+*oxX#&%B$@"; +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 diff --git a/F3:F303/MLX90640-allsky/proto.h b/F3:F303/MLX90640-allsky/proto.h index 398ba7a..aab3d44 100644 --- a/F3:F303/MLX90640-allsky/proto.h +++ b/F3:F303/MLX90640-allsky/proto.h @@ -18,7 +18,7 @@ #pragma once -extern const char *Timage, *Sensno; +extern const char *const Timage; #define SEND_USB (1) #define SEND_USART (0) diff --git a/F3:F303/MLX90640-allsky/spi.c b/F3:F303/MLX90640-allsky/spi.c new file mode 100644 index 0000000..646b01e --- /dev/null +++ b/F3:F303/MLX90640-allsky/spi.c @@ -0,0 +1,113 @@ +/* + * 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 "hardware.h" +#include "spi.h" +#include // memcpy + +#include "usb_dev.h" +#ifdef EBUG +#include "strfunc.h" +#endif + +spiStatus spi_status = SPI_NOTREADY; +#define WAITX(x) do{volatile uint32_t wctr = 0; while((x) && (++wctr < 360000)) IWDG->KR = IWDG_REFRESH; if(wctr==360000){ DBG("timeout"); return 0;}}while(0) + +// init SPI @ ~280kHz (36MHz/128) +void spi_setup(){ + SPI1->CR1 = 0; // clear EN + SPI1->CR2 = 0; + // PB3 - SCK, BP4 - MISO, PB5 - MOSI; AF5 @PB3-5 + GPIOB->AFR[0] = (GPIOB->AFR[0] & ~(GPIO_AFRL_AFRL3 | GPIO_AFRL_AFRL4 | GPIO_AFRL_AFRL5)) | + AFRf(5, 3) | AFRf(5, 4) | AFRf(5, 5); + GPIOB->MODER = (GPIOB->MODER & (MODER_CLR(3) & MODER_CLR(4) & MODER_CLR(5))) | + MODER_AF(3) | MODER_AF(4) | MODER_AF(5); + RCC->APB1RSTR = RCC_APB2RSTR_SPI1RST; + RCC->APB1RSTR = 0; // clear reset + RCC->APB2ENR |= RCC_APB2ENR_SPI1EN; + // software slave management (without hardware NSS pin); RX only; Baudrate = 0b110 - fpclk/128 + // CPOL=1, CPHA=1: + SPI1->CR1 = SPI_CR1_CPOL | SPI_CR1_CPHA | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_MSTR | SPI_CR1_BR_2 | SPI_CR1_BR_1; + // hardware NSS management, RXNE after 8bit; 8bit transfer (default) + // DS=8bit; RXNE generates after 8bit of data in FIFO; + SPI1->CR2 = SPI_CR2_SSOE | SPI_CR2_FRXTH | SPI_CR2_DS_2|SPI_CR2_DS_1|SPI_CR2_DS_0; + spi_status = SPI_READY; + DBG("SPI works"); +} + +void spi_onoff(uint8_t on){ + if(on) SPI1->CR1 |= SPI_CR1_SPE; + else SPI1->CR1 &= ~SPI_CR1_SPE; +} + +// turn off given SPI channel and release GPIO +void spi_deinit(){ + SPI1->CR1 = 0; + SPI1->CR2 = 0; + RCC->APB2ENR &= ~RCC_APB2ENR_SPI1EN; + GPIOB->AFR[0] = GPIOB->AFR[0] & ~(GPIO_AFRL_AFRL3 | GPIO_AFRL_AFRL4 | GPIO_AFRL_AFRL5); + GPIOB->MODER = GPIOB->MODER & (MODER_CLR(3) & MODER_CLR(4) & MODER_CLR(5)); + spi_status = SPI_NOTREADY; +} + +uint8_t spi_waitbsy(){ + WAITX(SPI1->SR & SPI_SR_BSY); + return 1; +} + +/** + * @brief spi_writeread - send data over SPI (change data array with received bytes) + * @param data - data to write/read + * @param n - length of data + * @return 0 if failed + */ +uint8_t spi_writeread(uint8_t *data, uint8_t n){ + if(spi_status != SPI_READY || !data || !n){ + DBG("not ready"); + return 0; + } + // clear SPI Rx FIFO + spi_onoff(TRUE); + for(int i = 0; i < 4; ++i) (void) SPI1->DR; + for(int x = 0; x < n; ++x){ + WAITX(!(SPI1->SR & SPI_SR_TXE)); + *((volatile uint8_t*)&SPI1->DR) = data[x]; + WAITX(!(SPI1->SR & SPI_SR_RXNE)); + data[x] = *((volatile uint8_t*)&SPI1->DR); + } + spi_onoff(FALSE); // turn off SPI + return 1; +} + +// read data through SPI +uint8_t spi_read(uint8_t *data, uint8_t n){ + if(spi_status != SPI_READY || !data || !n){ + DBG("not ready"); + return 0; + } + // clear SPI Rx FIFO + for(int i = 0; i < 4; ++i) (void) SPI1->DR; + spi_onoff(TRUE); + for(int x = 0; x < n; ++x){ + WAITX(!(SPI1->SR & SPI_SR_RXNE)); + data[x] = *((volatile uint8_t*)&SPI1->DR); + } + spi_onoff(FALSE); // turn off SPI + return 1; +} + diff --git a/F3:F303/MLX90640-allsky/spi.h b/F3:F303/MLX90640-allsky/spi.h new file mode 100644 index 0000000..e956f08 --- /dev/null +++ b/F3:F303/MLX90640-allsky/spi.h @@ -0,0 +1,35 @@ +/* + * 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 +#include + +typedef enum{ + SPI_NOTREADY, + SPI_READY, + SPI_BUSY +} spiStatus; + +extern spiStatus spi_status; + +void spi_onoff(uint8_t on); +void spi_deinit(); +void spi_setup(); +uint8_t spi_waitbsy(); +uint8_t spi_writeread(uint8_t *data, uint8_t n); +uint8_t spi_read(uint8_t *data, uint8_t n); diff --git a/F3:F303/MLX90640-allsky/usb_dev.c b/F3:F303/MLX90640-allsky/usb_dev.c index 8e519e4..8d61eee 100644 --- a/F3:F303/MLX90640-allsky/usb_dev.c +++ b/F3:F303/MLX90640-allsky/usb_dev.c @@ -165,7 +165,16 @@ int USB_sendall(){ int USB_send(const uint8_t *buf, int len){ if(!buf || !CDCready || !len) return FALSE; while(len){ - int a = RB_write((ringbuffer*)&rbout, buf, len); + IWDG->KR = IWDG_REFRESH; + int l = RB_datalen((ringbuffer*)&rbout); + if(l < 0) continue; + int portion = rbout.length - 1 - l; + if(portion < 1){ + if(lastdsz == 0) send_next(); + continue; + } + if(portion > len) portion = len; + int a = RB_write((ringbuffer*)&rbout, buf, portion); if(a > 0){ len -= a; buf += a; diff --git a/F3:F303/MLX90640-allsky/usb_dev.h b/F3:F303/MLX90640-allsky/usb_dev.h index a85227c..7be0410 100644 --- a/F3:F303/MLX90640-allsky/usb_dev.h +++ b/F3:F303/MLX90640-allsky/usb_dev.h @@ -50,6 +50,12 @@ void linecoding_handler(usb_LineCoding *lc); #define UN(s) do{USB_sendstr(s); USB_putbyte('\n');}while(0) #define U(s) USB_sendstr(s) +#ifdef EBUG +#define DBG(s) do{USB_sendstr(s); USB_putbyte('\n');}while(0) +#else +#define DBG(s) +#endif + int USB_sendall(); int USB_send(const uint8_t *buf, int len); int USB_putbyte(uint8_t byte); diff --git a/F3:F303/MLX90640-allsky/version.inc b/F3:F303/MLX90640-allsky/version.inc index 3b34625..74584f0 100644 --- a/F3:F303/MLX90640-allsky/version.inc +++ b/F3:F303/MLX90640-allsky/version.inc @@ -1,2 +1,2 @@ -#define BUILD_NUMBER "15" -#define BUILD_DATE "2025-09-29" +#define BUILD_NUMBER "36" +#define BUILD_DATE "2025-10-05"