From 360de6949790543afc961dbed58abedcbb889923 Mon Sep 17 00:00:00 2001 From: eddyem Date: Sun, 12 Apr 2020 18:44:42 +0300 Subject: [PATCH] Add ID filtering for CAN-USB --- F0-nolib/Snippets/Readme.md | 3 + F0-nolib/Snippets/readint.c | 77 ++++++++++++++++ F0-nolib/usbcdc/can.c | 8 +- F0-nolib/usbcdc/can.h | 5 ++ F0-nolib/usbcdc/main.c | 4 + F0-nolib/usbcdc/proto.c | 172 ++++++++++++++++++++++++++++++++++-- F0-nolib/usbcdc/usbcan.bin | Bin 11636 -> 13888 bytes 7 files changed, 258 insertions(+), 11 deletions(-) create mode 100644 F0-nolib/Snippets/readint.c diff --git a/F0-nolib/Snippets/Readme.md b/F0-nolib/Snippets/Readme.md index 6cfa3bd..808ed80 100644 --- a/F0-nolib/Snippets/Readme.md +++ b/F0-nolib/Snippets/Readme.md @@ -2,3 +2,6 @@ Some snippets ============= * Flash_EEPROM - EEPROM emulation in flash +* Jump2Boot.c - software init of bootloader +* printuhex.c - print uint32_t in HEX +* readint.c - read integer number in any (bin/hex/dec) system diff --git a/F0-nolib/Snippets/readint.c b/F0-nolib/Snippets/readint.c new file mode 100644 index 0000000..70691c0 --- /dev/null +++ b/F0-nolib/Snippets/readint.c @@ -0,0 +1,77 @@ +char *omit_spaces(char *buf){ + while(*buf){ + if(*buf > ' ') break; + ++buf; + } + return buf; +} + +// THERE'S NO OVERFLOW PROTECTION IN NUMBER READ PROCEDURES! +// read decimal number +static char *getdec(char *buf, uint32_t *N){ + uint32_t num = 0; + while(*buf){ + char c = *buf; + if(c < '0' || c > '9'){ + break; + } + num *= 10; + num += c - '0'; + ++buf; + } + *N = num; + return buf; +} +// read hexadecimal number (without 0x prefix!) +static char *gethex(char *buf, uint32_t *N){ + uint32_t num = 0; + while(*buf){ + char c = *buf; + uint8_t M = 0; + if(c >= '0' && c <= '9'){ + M = '0'; + }else if(c >= 'A' && c <= 'F'){ + M = 'A' - 10; + }else if(c >= 'a' && c <= 'f'){ + M = 'a' - 10; + } + if(M){ + num <<= 4; + num += c - M; + }else{ + break; + } + ++buf; + } + *N = num; + return buf; +} +// read binary number (without 0b prefix!) +static char *getbin(char *buf, uint32_t *N){ + uint32_t num = 0; + while(*buf){ + char c = *buf; + if(c < '0' || c > '1'){ + break; + } + num <<= 1; + if(c == '1') num |= 1; + ++buf; + } + *N = num; + return buf; +} + +/** + * @brief getnum - read uint32_t from string (dec, hex or bin: 127, 0x7f, 0b1111111) + * @param buf - buffer with number and so on + * @param N - the number read + * @return pointer to first non-number symbol in buf (if it is == buf, there's no number) + */ +char *getnum(char *txt, uint32_t *N){ + if(*txt == '0'){ + if(txt[1] == 'x' || txt[1] == 'X') return gethex(txt+2, N); + if(txt[1] == 'b' || txt[1] == 'B') return getbin(txt+2, N); + } + return getdec(txt, N); +} diff --git a/F0-nolib/usbcdc/can.c b/F0-nolib/usbcdc/can.c index 860658d..2852b4c 100644 --- a/F0-nolib/usbcdc/can.c +++ b/F0-nolib/usbcdc/can.c @@ -168,13 +168,15 @@ void CAN_setup(uint16_t speed){ while((CAN->MSR & CAN_MSR_INAK)==CAN_MSR_INAK) if(--tmout == 0) break; /* (6) */ // accept ALL CAN->FMR = CAN_FMR_FINIT; /* (7) */ - CAN->FA1R = CAN_FA1R_FACT0; /* (8) */ + CAN->FA1R = CAN_FA1R_FACT0 | CAN_FA1R_FACT1; /* (8) */ // set to 1 all needed bits of CAN->FFA1R to switch given filters to FIFO1 #if 0 CAN->FM1R = CAN_FM1R_FBM0; /* (9) */ CAN->sFilterRegister[0].FR1 = CANID << 5 | ((BCAST_ID << 5) << 16); /* (10) */ #else - CAN->sFilterRegister[0].FR1 = 0; + CAN->sFilterRegister[0].FR1 = (1<<21)|(1<<5); // all odd IDs + CAN->FFA1R = 2; // filter 1 for FIFO1, filter 0 - for FIFO0 + CAN->sFilterRegister[1].FR1 = (1<<21); // all even IDs #endif CAN->FMR &=~ CAN_FMR_FINIT; /* (12) */ CAN->IER |= CAN_IER_ERRIE | CAN_IER_FOVIE0 | CAN_IER_FOVIE1; /* (13) */ @@ -352,6 +354,8 @@ static void can_process_fifo(uint8_t fifo_num){ uint8_t len = box->RDTR & 0x7; msg.length = len; msg.ID = box->RIR >> 21; + msg.filterNo = (box->RDTR >> 8) & 0xff; + msg.fifoNum = fifo_num; if(len){ // message can be without data uint32_t hb = box->RDHR, lb = box->RDLR; switch(len){ diff --git a/F0-nolib/usbcdc/can.h b/F0-nolib/usbcdc/can.h index 859d71a..6ff83eb 100644 --- a/F0-nolib/usbcdc/can.h +++ b/F0-nolib/usbcdc/can.h @@ -26,6 +26,9 @@ #include "hardware.h" +// amount of filter banks in STM32F0 +#define STM32F0FBANKNO 28 + // simple 1-byte commands #define CMD_TOGGLE (0xDA) #define CMD_BCAST (0xAD) @@ -45,6 +48,8 @@ typedef struct{ uint8_t data[8]; // up to 8 bytes of data uint8_t length; // data length + uint8_t filterNo; // filter number + uint8_t fifoNum; // message FIFO number uint16_t ID; // ID of receiver } CAN_message; diff --git a/F0-nolib/usbcdc/main.c b/F0-nolib/usbcdc/main.c index 2537e9b..b791b3b 100644 --- a/F0-nolib/usbcdc/main.c +++ b/F0-nolib/usbcdc/main.c @@ -137,6 +137,10 @@ int main(void){ switchbuff(3); SEND("got message, ID="); printuhex(can_mesg->ID); + SEND(", filter #"); + printu(can_mesg->filterNo); + SEND(", FIFO #"); + printu(can_mesg->fifoNum); SEND(", len="); printu(len); SEND(", data: "); diff --git a/F0-nolib/usbcdc/proto.c b/F0-nolib/usbcdc/proto.c index df31e22..6521217 100644 --- a/F0-nolib/usbcdc/proto.c +++ b/F0-nolib/usbcdc/proto.c @@ -264,6 +264,154 @@ TRUE_INLINE void print_ign_buf(){ } } +// print ID/mask of CAN->sFilterRegister[x] half +static void printID(uint16_t FRn){ + if(FRn & 0x1f) return; // trash + printuhex(FRn >> 5); +} +/* +Can filtering: FSCx=0 (CAN->FS1R) -> 16-bit identifiers +CAN->FMR = (sb)<<8 | FINIT - init filter in starting bank sb +CAN->FFA1R FFAx = 1 -> FIFO1, 0 -> FIFO0 +CAN->FA1R FACTx=1 - filter active +MASK: FBMx=0 (CAN->FM1R), two filters (n in FR1 and n+1 in FR2) + ID: CAN->sFilterRegister[x].FRn[0..15] + MASK: CAN->sFilterRegister[x].FRn[16..31] + FR bits: STID[10:0] RTR IDE EXID[17:15] +LIST: FBMx=1, four filters (n&n+1 in FR1, n+2&n+3 in FR2) + IDn: CAN->sFilterRegister[x].FRn[0..15] + IDn+1: CAN->sFilterRegister[x].FRn[16..31] +*/ +TRUE_INLINE void list_filters(){ + uint32_t fa = CAN->FA1R, ctr = 0, mask = 1; + while(fa){ + if(fa & 1){ + SEND("Filter "); printu(ctr); SEND(", FIFO"); + if(CAN->FFA1R & mask) SEND("1"); + else SEND("0"); + SEND(" in "); + if(CAN->FM1R & mask){ // up to 4 filters in LIST mode + SEND("LIST mode, IDs: "); + printID(CAN->sFilterRegister[ctr].FR1 & 0xffff); + SEND(" "); + printID(CAN->sFilterRegister[ctr].FR1 >> 16); + SEND(" "); + printID(CAN->sFilterRegister[ctr].FR2 & 0xffff); + SEND(" "); + printID(CAN->sFilterRegister[ctr].FR2 >> 16); + }else{ // up to 2 filters in MASK mode + SEND("MASK mode: "); + if(!(CAN->sFilterRegister[ctr].FR1&0x1f)){ + SEND("ID="); printID(CAN->sFilterRegister[ctr].FR1 & 0xffff); + SEND(", MASK="); printID(CAN->sFilterRegister[ctr].FR1 >> 16); + SEND(" "); + } + if(!(CAN->sFilterRegister[ctr].FR2&0x1f)){ + SEND("ID="); printID(CAN->sFilterRegister[ctr].FR2 & 0xffff); + SEND(", MASK="); printID(CAN->sFilterRegister[ctr].FR2 >> 16); + } + } + newline(); + } + fa >>= 1; + ++ctr; + mask <<= 1; + } + sendbuf(); +} + +/** + * @brief add_filter - add/modify filter + * @param str - string in format "bank# FIFO# mode num0 .. num3" + * where bank# - 0..27 + * if there's nothing after bank# - delete filter + * FIFO# - 0,1 + * mode - 'I' for ID, 'M' for mask + * num0..num3 - IDs in ID mode, ID/MASK for mask mode + */ +static void add_filter(char *str){ + uint32_t N; + str = omit_spaces(str); + char *n = getnum(str, &N); + if(n == str){ + SEND("No bank# given"); + return; + } + if(N > STM32F0FBANKNO-1){ + SEND("bank# > 27"); + return; + } + uint8_t bankno = (uint8_t)N; + str = omit_spaces(n); + if(!*str){ // deactivate filter + SEND("Deactivate filters in bank "); + printu(bankno); + CAN->FMR = CAN_FMR_FINIT; + CAN->FA1R &= ~(1<FMR &=~ CAN_FMR_FINIT; + return; + } + uint8_t fifono = 0; + if(*str == '1') fifono = 1; + else if(*str != '0'){ + SEND("FIFO# is 0 or 1"); + return; + } + str = omit_spaces(str + 1); + char c = *str; + uint8_t mode = 0; // ID + if(c == 'M' || c == 'm') mode = 1; + else if(c != 'I' && c != 'i'){ + SEND("mode is 'M/m' for MASK and 'I/i' for IDLIST"); + return; + } + str = omit_spaces(str + 1); + uint32_t filters[4]; + uint32_t nfilt; + for(nfilt = 0; nfilt < 4; ++nfilt){ + n = getnum(str, &N); + if(n == str) break; + filters[nfilt] = N; + str = omit_spaces(n); + } + if(nfilt == 0){ + SEND("You should add at least one filter!"); + return; + } + if(mode && (nfilt&1)){ + SEND("In MASK mode you should point pairs of ID/MASK"); + return; + } + CAN->FMR = CAN_FMR_FINIT; + uint32_t mask = 1<FA1R |= mask; // activate given filter + if(fifono) CAN->FFA1R |= mask; // set FIFO number + else CAN->FFA1R &= ~mask; + if(mode) CAN->FM1R &= ~mask; // MASK + else CAN->FM1R |= mask; // LIST + uint32_t F1 = (0x8f<<16); + uint32_t F2 = (0x8f<<16); + // reset filter registers to wrong value + CAN->sFilterRegister[bankno].FR1 = (0x8f<<16) | 0x8f; + CAN->sFilterRegister[bankno].FR2 = (0x8f<<16) | 0x8f; + switch(nfilt){ + case 4: + F2 = filters[3] << 21; + // fallthrough + case 3: + CAN->sFilterRegister[bankno].FR2 = (F2 & 0xffff0000) | (filters[2] << 5); + // fallthrough + case 2: + F1 = filters[1] << 21; + // fallthrough + case 1: + CAN->sFilterRegister[bankno].FR1 = (F1 & 0xffff0000) | (filters[0] << 5); + } + CAN->FMR &=~ CAN_FMR_FINIT; + SEND("Added filter with "); + printu(nfilt); SEND(" parameters"); +} + /** * @brief cmd_parser - command parsing * @param txt - buffer with commands & data @@ -277,24 +425,26 @@ void cmd_parser(char *txt, uint8_t isUSB){ * parse long commands here */ switch(_1st){ - case 's': - case 'S': - sendCANcommand(txt + 1); + case 'a': + addIGN(txt + 1); goto eof; break; case 'b': CANini(txt + 1); goto eof; break; - case 'a': - addIGN(txt + 1); + case 'f': + add_filter(txt + 1); goto eof; + break; + case 's': + case 'S': + sendCANcommand(txt + 1); + goto eof; + break; } if(txt[1] != '\n') *txt = '?'; // help for wrong message length switch(_1st){ - case 'f': - transmit_tbuf(); - break; case 'B': can_send_broadcast(); break; @@ -315,6 +465,9 @@ void cmd_parser(char *txt, uint8_t isUSB){ printuhex(getCANID()); newline(); break; + case 'l': + list_filters(); + break; case 'p': print_ign_buf(); break; @@ -345,9 +498,10 @@ void cmd_parser(char *txt, uint8_t isUSB){ "'B' - send broadcast dummy byte\n" "'C' - send dummy byte over CAN\n" "'d' - delete ignore list\n" - "'f' - flush UART buffer\n" + "'f' - add/delete filter, format: bank# FIFO# mode(M/I) num0 [num1 [num2 [num3]]]\n" "'G' - get CAN address\n" "'I' - reinit CAN (with new address)\n" + "'l' - list all active filters\n" "'p' - print ignore buffer\n" "'R' - software reset\n" "'s/S' - send data over CAN: s ID byte0 .. byteN\n" diff --git a/F0-nolib/usbcdc/usbcan.bin b/F0-nolib/usbcdc/usbcan.bin index 374a6d192f4db97a2ffa3a135933138d0554e348..fbe7f64c48e9e4996c6f2485713f8cdeac9d2957 100755 GIT binary patch delta 6874 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 delta 4661 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