From ed2a28722d8dfd7d8ec5b4a5afbaf4658491cd7e Mon Sep 17 00:00:00 2001 From: eddyem Date: Sun, 15 Nov 2020 15:54:57 +0300 Subject: [PATCH] add parameters storage in flash to Socket_fans --- F0-nolib/Socket_fans/flash.c | 196 ++++++++++++++++++++++++++++++ F0-nolib/Socket_fans/flash.h | 51 ++++++++ F0-nolib/Socket_fans/main.c | 2 + F0-nolib/Socket_fans/monitor.c | 19 +-- F0-nolib/Socket_fans/monitor.h | 1 - F0-nolib/Socket_fans/proto.c | 111 ++++++++++++++++- F0-nolib/Socket_fans/sockfans.bin | Bin 12268 -> 15044 bytes 7 files changed, 362 insertions(+), 18 deletions(-) create mode 100644 F0-nolib/Socket_fans/flash.c create mode 100644 F0-nolib/Socket_fans/flash.h diff --git a/F0-nolib/Socket_fans/flash.c b/F0-nolib/Socket_fans/flash.c new file mode 100644 index 0000000..554d091 --- /dev/null +++ b/F0-nolib/Socket_fans/flash.c @@ -0,0 +1,196 @@ +/* + * This file is part of the SockFans project. + * Copyright 2020 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 "adc.h" +#include "flash.h" +#include "proto.h" // printout +#include // memcpy + +// max amount of Config records stored (will be recalculate in flashstorage_init() +static uint32_t maxCnum = FLASH_BLOCK_SIZE / sizeof(user_conf); + +#define USERCONF_INITIALIZER { \ + .userconf_sz = sizeof(user_conf) \ + ,.Tturnoff = 20000 \ + ,.Thyst = 30 \ + ,.Tmin = {400, 400, 400} \ + ,.Tmax = {900, 800, 800, 800} \ + } + +static int erase_flash(const void*, const void*); +static int write2flash(const void*, const void*, uint32_t); +// don't write `static` here, or get error: +// 'memcpy' forming offset 8 is out of the bounds [0, 4] of object '__varsstart' with type 'uint32_t' +const user_conf *Flash_Data = (const user_conf *)(&__varsstart); + +user_conf the_conf = USERCONF_INITIALIZER; + +static int currentconfidx = -1; // index of current configuration + +/** + * @brief binarySearch - binary search in flash for last non-empty cell + * any struct searched should have its sizeof() @ the first field!!! + * @param l - left index + * @param r - right index (should be @1 less than last index!) + * @param start - starting address + * @param stor_size - size of structure to search + * @return index of non-empty cell or -1 + */ +static int binarySearch(int r, const uint8_t *start, int stor_size){ + int l = 0; + while(r >= l){ + int mid = l + (r - l) / 2; + const uint8_t *s = start + mid * stor_size; + if(*((const uint16_t*)s) == stor_size){ + if(*((const uint16_t*)(s + stor_size)) == 0xffff){ + return mid; + }else{ // element is to the right + l = mid + 1; + } + }else{ // element is to the left + r = mid - 1; + } + } + return -1; // not found +} + +/** + * @brief flashstorage_init - initialization of user conf storage + * run in once @ start + */ +void flashstorage_init(){ + if(FLASH_SIZE > 0 && FLASH_SIZE < 20000){ + uint32_t flsz = FLASH_SIZE * 1024; // size in bytes + flsz -= (uint32_t)(&__varsstart) - FLASH_BASE; + maxCnum = flsz / sizeof(user_conf); +//SEND("flsz="); printu(flsz); +//SEND("\nmaxCnum="); printu(maxCnum); newline(); sendbuf(); + } + // -1 if there's no data at all & flash is clear; maxnum-1 if flash is full + currentconfidx = binarySearch((int)maxCnum-2, (const uint8_t*)Flash_Data, sizeof(user_conf)); + if(currentconfidx > -1){ + memcpy(&the_conf, &Flash_Data[currentconfidx], sizeof(user_conf)); + } +} + +// store new configuration +// @return 0 if all OK +int store_userconf(){ + // maxnum - 3 means that there always should be at least one empty record after last data + // for binarySearch() checking that there's nothing more after it! + if(currentconfidx > (int)maxCnum - 3){ // there's no more place + currentconfidx = 0; + if(erase_flash(Flash_Data, (&__varsstart))) return 1; + }else ++currentconfidx; // take next data position (0 - within first run after firmware flashing) + return write2flash((const void*)&Flash_Data[currentconfidx], &the_conf, sizeof(the_conf)); +} + +static int write2flash(const void *start, const void *wrdata, uint32_t stor_size){ + int ret = 0; + if (FLASH->CR & FLASH_CR_LOCK){ // unloch flash + FLASH->KEYR = FLASH_KEY1; + FLASH->KEYR = FLASH_KEY2; + } + while (FLASH->SR & FLASH_SR_BSY); + if(FLASH->SR & FLASH_SR_WRPRTERR){ + MSG("Can't remove write protection\n"); + return 1; // write protection + } + FLASH->SR = FLASH_SR_EOP | FLASH_SR_PGERR | FLASH_SR_WRPRTERR; // clear all flags + FLASH->CR |= FLASH_CR_PG; + const uint16_t *data = (const uint16_t*) wrdata; + volatile uint16_t *address = (volatile uint16_t*) start; + uint32_t i, count = (stor_size + 1) / 2; + for (i = 0; i < count; ++i){ + IWDG->KR = IWDG_REFRESH; + *(volatile uint16_t*)(address + i) = data[i]; + while (FLASH->SR & FLASH_SR_BSY); + if(FLASH->SR & FLASH_SR_PGERR){ + ret = 1; // program error - meet not 0xffff + MSG("FLASH_SR_PGERR\n"); + break; + }else while (!(FLASH->SR & FLASH_SR_EOP)); + FLASH->SR = FLASH_SR_EOP | FLASH_SR_PGERR | FLASH_SR_WRPRTERR; + } + FLASH->CR |= FLASH_CR_LOCK; // lock it back + FLASH->CR &= ~(FLASH_CR_PG); + MSG("Flash stored\n"); + return ret; +} + +/** + * @brief erase_flash - erase N pages of flash memory + * @param start - first address + * @param end - last address (or NULL if need to erase all flash remaining) + * @return 0 if succeed + */ +static int erase_flash(const void *start, const void *end){ + int ret = 0; + uint32_t nblocks = 1, flsz = 0; + if(!end){ // erase all remaining + if(FLASH_SIZE > 0 && FLASH_SIZE < 20000){ + flsz = FLASH_SIZE * 1024; // size in bytes + flsz -= (uint32_t)start - FLASH_BASE; + } + }else{ // erase a part + flsz = (uint32_t)end - (uint32_t)start; + } + nblocks = flsz / FLASH_BLOCK_SIZE; + if(nblocks == 0 || nblocks >= FLASH_SIZE) return 1; + for(uint32_t i = 0; i < nblocks; ++i){ +#ifdef EBUG + SEND("Try to erase page #"); printu(i); newline(); sendbuf(); +#endif + IWDG->KR = IWDG_REFRESH; + /* (1) Wait till no operation is on going */ + /* (2) Clear error & EOP bits */ + /* (3) Check that the Flash is unlocked */ + /* (4) Perform unlock sequence */ + while ((FLASH->SR & FLASH_SR_BSY) != 0){} /* (1) */ + FLASH->SR = FLASH_SR_EOP | FLASH_SR_PGERR | FLASH_SR_WRPRTERR; /* (2) */ + /* if (FLASH->SR & FLASH_SR_EOP){ + FLASH->SR |= FLASH_SR_EOP; + }*/ + if ((FLASH->CR & FLASH_CR_LOCK) != 0){ /* (3) */ + FLASH->KEYR = FLASH_KEY1; /* (4) */ + FLASH->KEYR = FLASH_KEY2; + } + /* (1) Set the PER bit in the FLASH_CR register to enable page erasing */ + /* (2) Program the FLASH_AR register to select a page to erase */ + /* (3) Set the STRT bit in the FLASH_CR register to start the erasing */ + /* (4) Wait until the EOP flag in the FLASH_SR register set */ + /* (5) Clear EOP flag by software by writing EOP at 1 */ + /* (6) Reset the PER Bit to disable the page erase */ + FLASH->CR |= FLASH_CR_PER; /* (1) */ + FLASH->AR = (uint32_t)Flash_Data + i*FLASH_BLOCK_SIZE; /* (2) */ + FLASH->CR |= FLASH_CR_STRT; /* (3) */ + while(!(FLASH->SR & FLASH_SR_EOP)); + FLASH->SR |= FLASH_SR_EOP; /* (5)*/ + if(FLASH->SR & FLASH_SR_WRPRTERR){ /* Check Write protection error */ + ret = 1; + MSG("Write protection error!\n"); + FLASH->SR |= FLASH_SR_WRPRTERR; /* Clear the flag by software by writing it at 1*/ + break; + } + FLASH->CR &= ~FLASH_CR_PER; /* (6) */ + } + return ret; +} + + diff --git a/F0-nolib/Socket_fans/flash.h b/F0-nolib/Socket_fans/flash.h new file mode 100644 index 0000000..8e7b9b6 --- /dev/null +++ b/F0-nolib/Socket_fans/flash.h @@ -0,0 +1,51 @@ +/* + * This file is part of the SockFans project. + * Copyright 2020 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 FLASH_H__ +#define FLASH_H__ + +#include "hardware.h" + +#define FLASH_BLOCK_SIZE (1024) +#define FLASH_SIZE_REG ((uint32_t)0x1FFFF7CC) +#define FLASH_SIZE *((uint16_t*)FLASH_SIZE_REG) + +#define TMINNO (3) +#define TMAXNO (4) + +/* + * struct to save user configurations + */ +typedef struct __attribute__((packed, aligned(4))){ + uint16_t userconf_sz; // "magick number" + uint32_t Tturnoff; // wait for Tturnoff ms before turning power off + uint32_t Thyst; // Thysteresis + int16_t Tmin[TMINNO]; // minT + int16_t Tmax[TMAXNO]; // maxT +} user_conf; + +extern user_conf the_conf; // global user config (read from FLASH to RAM) +// data from ld-file: start address of storage +extern const uint32_t __varsstart; + +void flashstorage_init(); +int store_userconf(); +void dump_userconf(); + +#endif // FLASH_H__ diff --git a/F0-nolib/Socket_fans/main.c b/F0-nolib/Socket_fans/main.c index e7db15a..5838a34 100644 --- a/F0-nolib/Socket_fans/main.c +++ b/F0-nolib/Socket_fans/main.c @@ -19,6 +19,7 @@ * MA 02110-1301, USA. */ +#include "flash.h" #include "hardware.h" #include "monitor.h" #include "proto.h" @@ -80,6 +81,7 @@ int main(void){ char *txt; sysreset(); SysTick_Config(6000, 1); + flashstorage_init(); HW_setup(); USB_setup(); RCC->CSR |= RCC_CSR_RMVF; // remove reset flags diff --git a/F0-nolib/Socket_fans/monitor.c b/F0-nolib/Socket_fans/monitor.c index 100a949..c94e28d 100644 --- a/F0-nolib/Socket_fans/monitor.c +++ b/F0-nolib/Socket_fans/monitor.c @@ -17,32 +17,25 @@ */ #include "adc.h" +#include "flash.h" #include "monitor.h" #include "proto.h" // when critical T reached wait for TturnOff ms and after that turn off system -#define TturnOff (20000) +#define TturnOff the_conf.Tturnoff // don't mind when button 2 pressed again after t<5s #define TbtnPressed (5000) // settings // T0 - CPU, T1 - HDD, T2 - inner T, T3 - power source -static const int16_t Thysteresis = 30; // hysteresis by T=3degC -static const int16_t tmin[3] = {400, 350, 350}; // turn off fans when T[x]tmin+Th -static const int16_t tmax[3] = {900, 800, 600}; // critical T, turn off power after TturnOff milliseconds -static const int16_t t3max = 850; +#define Thysteresis (int16_t)the_conf.Thyst +#define tmin the_conf.Tmin +#define tmax the_conf.Tmax +#define t3max the_conf.Tmax[TMAXNO-1] static uint8_t dontprocess = 0; // don't process monitor static uint32_t TOff = 0; // time to turn off power -// show hardcoded settings -void showSettings(){ - SEND("Thysteresis=30\n"); - SEND("Tmin={400, 350, 350}\n"); - SEND("Tmax={900, 800, 600}\n"); - SEND("T3max=850"); -} - static void chkOffRelay(){ static uint32_t scntr = 0; if(!TOff){ // warning cleared diff --git a/F0-nolib/Socket_fans/monitor.h b/F0-nolib/Socket_fans/monitor.h index cefeb0b..95258fd 100644 --- a/F0-nolib/Socket_fans/monitor.h +++ b/F0-nolib/Socket_fans/monitor.h @@ -25,7 +25,6 @@ #define MONITOR_PERIOD (999) void process_monitor(); -void showSettings(); void SetDontProcess(uint8_t newstate); uint8_t GetDontProcess(); #define MONITOR_H__ diff --git a/F0-nolib/Socket_fans/proto.c b/F0-nolib/Socket_fans/proto.c index 7866598..317701e 100644 --- a/F0-nolib/Socket_fans/proto.c +++ b/F0-nolib/Socket_fans/proto.c @@ -21,6 +21,7 @@ * */ #include "adc.h" +#include "flash.h" #include "hardware.h" #include "monitor.h" #include "proto.h" @@ -306,6 +307,104 @@ void showState(){ NL(); } +static uint8_t userconf_changed = 0; // ==1 if user_conf was changed +static const char *nshould = "N should be from 0 to "; +static const char *changed = "Changed OK\n"; +static const char *notchanged = "Not changed: the same value\n"; +static inline void setArrval(int16_t *arr, uint8_t maxelem, char *params){ + uint32_t N, val; + int16_t sign = 1; + char *next = getnum(params, &N); + if(N > maxelem){ + SEND(nshould); + printu(maxelem); newline(); + return; + } + next = omit_spaces(next); + if(*next == '-'){ + next = omit_spaces(next+1); + sign = -1; + } + char *rest = getnum(next, &val); + if(*rest && *rest != '\n'){ + SEND("Arguments are: N T, where N is channel number, T - temperature\n"); + return; + } + sign *= (int16_t)val; + if(arr[N] != sign){ + arr[N] = sign; + userconf_changed = 1; + SEND(changed); + }else SEND(notchanged); +} +static inline void setval(uint32_t *var, char *params){ + uint32_t val; + char *next = getnum(params, &val); + if(*next && *next != '\n'){ + SEND("Argument is 32-bit number\n"); + return; + } + if(*var != val){ + *var = val; + userconf_changed = 1; + SEND(changed); + }else SEND(notchanged); +} +static inline void showSettings(){ + int i; + SEND("Tturnoff="); printu(the_conf.Tturnoff); newline(); + SEND("Thysteresis="); printu(the_conf.Thyst); newline(); + SEND("Tmin={"); + for(i = 0; i < TMINNO; ++i){ + if(i) SEND(", "); + printu(the_conf.Tmin[i]); + } + SEND("}\n"); + SEND("Tmax={"); + for(i = 0; i < TMAXNO; ++i){ + if(i) SEND(", "); + printu(the_conf.Tmax[i]); + } + SEND("}\n"); +} +static inline void setters(char *txt){ // setters + txt = omit_spaces(txt); + if(!*txt){ + SEND("Setters need more arguments"); + return; + } + char *next = omit_spaces(txt+1); + switch(*txt){ + case '<': // Tmin + setArrval(the_conf.Tmin, TMINNO-1, next); + break; + case '>': // Tmax + setArrval(the_conf.Tmax, TMAXNO-1, next); + break; + case 'H': // Thyst + setval(&the_conf.Thyst, next); + break; + case 'O': // Tturnoff + setval(&the_conf.Tturnoff, next); + break; + case 'S': // save settings + if(userconf_changed){ + if(!store_userconf()){ + userconf_changed = 0; + SEND("Stored!"); + }else SEND("Error when storing!"); + } + break; + default: + SEND("Setters:\n" + "< N T - set nth (0..2) Tmin (T/10degrC)\n" + "> N T - set Tmax\n" + "H T - set temperature hysteresis (T/10degrC)\n" + "O t - time to turn off after emergency (t in ms)\n" + "S - save settings\n"); + } +} + /** * @brief cmd_parser - command parsing * @param txt - buffer with commands & data @@ -341,14 +440,17 @@ void cmd_parser(char *txt){ getPWM(txt[1]); goto eof; break; + case 'P': // set PWM + changePWM(txt+1); + goto eof; + break; case 'r': // relay: set/clear/check onoff(txt[1], RELAY_port, RELAY_pin, "RELAY"); goto eof; break; - case 'S': // set PWM - changePWM(txt+1); + case 'S': // setters + setters(&txt[1]); goto eof; - break; case 't': gett(txt[1]); goto eof; @@ -422,9 +524,10 @@ void cmd_parser(char *txt){ "'Gx' - get cooler x (0..3) PWM settings\n" "'M' - get MCU temperature\n" "'m' - toggle monitoring\n" + "'Px y' - set coolerx PWM to y\n" "'R' - software reset\n" "'rx' - relay on/off (x=1/0) or get status\n" - "'Sx y' - set coolerx PWM to y\n" + "'S' - setters\n" "'s' - show settings\n" "'T' - get time from start (ms)\n" "'tx' - get temperature x (0..3)\n" diff --git a/F0-nolib/Socket_fans/sockfans.bin b/F0-nolib/Socket_fans/sockfans.bin index 71869a7db1fa3a1d54079b5770618ead177ceea8..d1b8b86467b525a01d44bd7576dcd320da8c90a8 100755 GIT binary patch delta 7559 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 delta 4816 zcmc&&e^gV~9l!SlBt)E1(8fUZk$_T&8bn$Utq|}T-sP8yN@J^mKo=AxM{I|)ZSY5D z+tt?jI30#|vsPE9Xc5$Qn^vs$)V8)JQ&*+h6Ficvty^6n#`(>K%vd{WQMRBKg#`B~YLgic z;}*v6bY9P&wJsSg2Q+~$W`|!W89P_3$VFNauqZleQU2k>2vuXm?u4AK8IIB%peiNx$Nv}x)zODfqY15%w zuQ-v~z+JKAnNdcYeHEwUqOua2k>*JUo@3Mb9eeavo8@3ay2y~@9h%r_=kc9A6G zR(Die6Y5zVy?d&5>!O02@B;QqEodfkoOiP9CUS3mny$prR{g=EW4+PsWYLjZOF6=| zDYqkqBDx*ny-Mu4HJG5SO+(jDIBQwfXmdA0I@wRib44qjiA6V8!f z1&BV7j6LTDYe<^*x@oUlYTl-7&~MYB8%gd8cN)@ad%BS4TXD&gVabf2?hV-++}MV* z;w@qC(NHZ^t=7%7CpM#W&MvW`^g@R$1Kl{|WJJtC_hK^7oM^V`Dy~>pz|l+hFCj{E zMUujZJkN{NpZk#Knh&|NpfYDgj}ttXe6_0i#9*?SdQEdl&NMyaXbyL0)2YdmFrGO zLs#_f1St7!*Bn^D8gTn#)QO#1l z_JAaj2Is?7p8$S2SQSW<>NyAew2K4`3DGf1E*x_6{dJzXf&)>?T+&Vdli@ zm9XKBBOB&C**@A3)`~lx4QovFed3}M)1<3lmXjzYx^zrghFOb}AeDrp$SgRdmNPw^ z6mTp|m?UJ&)i_CbNIp9{37)0hyX56qekFkCYcrEAX-Ec#iVFyLC#+4-_`_a%_$xPT zcevHN-I2aSalISy(ht7Nx za96>ugLzIe&D%TUkv8wsuA=55IDsMO`Qmo;Vb$+B7FxiPkPNiCF?g2r@JeHkmB4|` z!1l1WFevHtoY88KfhwU-p7JYH$`-b2p0N}t@A}}msu&;?U@fAS0lzk$C@FS6s?&snlUBtVCkCU^=R~7aegT+GM zq(p0Q5VzL?L$EJXW3bQBEVpYl6?OwL@QtZTV~bv;;Co|@HM${KXb3hv{xEHg-6iPHG}CUQltEI3-oajWkUmazN= zn6QzZ6EWco5p>KI@}3IaLlk@wAPU%w2*hL&@rp}Me!TsXp9(*6EfrDt$_gv ziQ;AO&Zd-PJI&A=MubKMLd7&9oDm`2O@|%HRO|&meeUTBd$&U$*y-;i zoqQ~82c;p$f<760EMikHiA=APL;QL`uy3m_|4TLgi57?B?gdzrx($cy|6z{uD=5ttRjm>R%%2A(Fr8pb>qfhiis z%mU131Ml^Rt=_`H3;n+&TliuitYljvz_Uc~6GUMKfeg^@lR9v$zmZtP!@rpQMFdtM zV$Gx;deg@5T9Z2v@_rI(?XMvwUJLv)*oKIlPyo%e`W#R9FJld475^wbh)|8M18g%& z^E;N46+pJ;JF;_uXg1{CA3FPW`HK&eNBCYNy$iVsDH&GdLlNY5Aon>|_7{;N-V9S0 zvP&bP=ftSKf4&t|H6-Pdh;Ww}&h5`8+5AdKu3#5ML|+r5>HVxqLu@sr{c=@0j5+-+ zss&W;H4+zA<&iRIN2w&0FNdR@8s;Ci#UCKb!(mNoFX0QWb z9bhNGM!s#(`lM8bcVTP;L;pa|C=`f>?jXGgGNh6#qJH?EQ906Y**D;)-@@zQZv_*K zstOz@99K=lCc&X2NKGT;U`N8v=#}BMi?z%2f*xL4PYs= zyB|%TijzAakK)7La5((c5HDTGI2Q1fQ#hEh26qV2nk=~HY8JwwZuN)gFAS)z0+6s! z_9`&a`!-_fkCYWy0 z_hyZJ_+Rb=!vs&kzvv_JT|Xd>jE#2x_E5g{-$+ISj^>;9gULt3{FE)H9;CHH%=P@R*X*vxL8!Omox>Nc^a#>R%ajm>Pu%2IYyBfDAX$N^P$FU!rw X3i`hf3#mdtN*LuM^bQ