eddy_em: (Default)
[personal profile] eddy_em
Всегда основная проблема в своих железках — отсутствие документации. Надо бы хоть немного описать на будущее, как это работает.
Саму основу для сниффера я сделал еще давно — когда необходимо было разработать девборду для "тренировок" с USB и CAN (под контроллеры термодатчиков):

Протокол работы железки я уже описал, теперь опишу исходники ее прошивки.

Итак, первое, что нам нужно для работающего CAN-сниффера — это возможность одновременной работы и CAN, и USB. В дешевой нише STM32 это умеют STM32F0x2 (072 и 042; на работе для термодатчиков я закупал 042, для дома же купил на али 2 десятка 072, тогда это было примерно 60 рублей за штучку).
В отличие от 103-х, где служебный буфер в памяти CAN и USB делять не "по-братски", и он может принадлежать лишь одной из периферий, в 0x2 CAN забирает лишь 256 последних байт буфера USB. Поэтому для USB доступно 768 байт буфера (в принципе, этого вполне достаточно, если не делать сложное составное устройство).
Начнем с USB. Сниффер будет "выдавать" себя за PL2303 (мне нравится, что модуль ядра выделяет для него устройство /dev/ttyUSBx, а не позорный /dev/ttyACMx, как под обычный USB-CDC; кроме того, в некоторых некошерных дистрибутивах вроде бубунты могут быть проблемы с USB-CDC: при подключении запускается modemd и захватывает файл устройства в свое личное распоряжение).
В заголовочном файле usb_defs.h определяем USB_BTABLE_SIZE как 768 байт. Там же определяем размеры буферов конечных точек 0 и 1 (конечная точка 1 — interrupt IN — использоваться при работе не будет, и в принципе можно было бы ее не определять). Там же нам понадобится определить структуры служебных регистров:
typedef struct{
    __IO uint32_t EPnR[STM32ENDPOINTS];
    __IO uint32_t RESERVED[STM32ENDPOINTS];
    __IO uint32_t CNTR;
    __IO uint32_t ISTR;
    __IO uint32_t FNR;
    __IO uint32_t DADDR;
    __IO uint32_t BTABLE;
    __IO uint32_t LPMCSR;
    __IO uint32_t BCDR;
} USB_TypeDef;

и регистров описания конечных точек:
typedef struct{
    __IO uint16_t USB_ADDR_TX;
    __IO uint16_t USB_COUNT_TX;
    __IO uint16_t USB_ADDR_RX;
    __IO uint16_t USB_COUNT_RX;
} USB_EPDATA_TypeDef;

В файлах usb_lib.c и usb_lib.h разместим "низкоуровневые" функции USB. В принципе, эту иерархию не я придумал: когда я только начал заниматься USB, вменяемой реализации на просторах интернета не нашел. Один из пользователей easyelectronix выложил простую реализацию USB-HID. Собственно, на основе ее я и сделал свои экземпляры USB-HID, CDC и эмуляцию PL2303.
В заголовочном файле определим различные стандартные типы запросов и прочее, что нам понадобится (строки с 35 по 77). Простые макросы для понимания, является ли пакет входящим или исходящим и есть ли там данные SETUP. Для работы с регистрами конечных точек нам понадобится определить пару макросов:
#define KEEP_DTOG_STAT(EPnR)            (EPnR & ~(USB_EPnR_STAT_RX|USB_EPnR_STAT_TX|USB_EPnR_DTOG_RX|USB_EPnR_DTOG_TX))
#define KEEP_DTOG(EPnR)                 (EPnR & ~(USB_EPnR_DTOG_RX|USB_EPnR_DTOG_TX))

Здесь сразу скажу: надо помнить, что в регистрах EPnR некоторые флаги имеют свойство toggle. Поэтому не повторяйте моих ошибок: держите это всегда в уме, и когда нужно лишь какой-то флаг установить/сбросить, обнуляйте биты всех ненужных toggle-флагов! Эти макросы собственно и занимаются тем, что оставляют нетронутыми флаги DTOG (они нам не нужны, т.к. мы не пользуемся двойной буферизацией) и STAT (а это важно, когда мы получаем данные, но не хотим сразу же отправлять ACK, пока буфер не будет обработан).
Еще для работы конечного автомата состояния USB понадобится определить его состояния:
typedef enum{
    USB_STATE_DEFAULT,
    USB_STATE_ADDRESSED,
    USB_STATE_CONFIGURED,
    USB_STATE_CONNECTED
} USB_state;

Там же определяем макросы для задания строковой информации (_USB_STRING_, _USB_LANG_ID_) и вспомогательные структуры для разбора конфигурационного пакета (config_pack_t), настройки конечной точки (ep_t) самого USB(usb_dev_t — эта структура используется, чтобы поменьше глобальных переменных заводить). Из /usr/include/linux/usb/cdc.h копируем определение структуры usb_LineCoding (в принципе, можно обрабатывать SET_LINE_CODING, меняя скорость, скажем, USART1; но в данном случае это не нужно, а USART1 у меня использовался исключительно для отладочных сообщений).
В файле usb_lib.c определяемдескрипторы устройства (достать их несложно, если "натравить" на "настоящий китайский" PL2303 lsusb -v). Здесь же как WEAK определены заглушки для обработчиков стандартных запросов SET_LINE_CODING (изменение параметров последовательного порта: скорости, четности и т.п.), SET_CONTROL_LINE_STATE (аппаратное управление потоком) и SEND_BREAK (конец связи).
В отличие от "обычного" USB CDC у pl2303 есть еще и vendor-запросы. Нафиг они нужны — непонятно, однако, благодаря тому, что кто-то уже отреверсил подобную железяку и написал для нее модуль ядра, в исходниках
/usr/src/linux/drivers/usb/serial/pl2303.c можно посмотреть, как оно работает. Собственно, оттуда я и утащил функцию-обработчик:
void WEAK vendor_handler(config_pack_t *packet){
    if(packet->bmRequestType & 0x80){ // read
        uint8_t c;
        switch(packet->wValue){
            case 0x8484:
                c = 2;
            break;
            case 0x0080:
                c = 1;
            break;
            case 0x8686:
                c = 0xaa;
            break;
            default:
                c = 0;
        }
        EP_WriteIRQ(0, &c, 1);
    }else{ // write ZLP
        EP_WriteIRQ(0, (uint8_t *)0, 0);
    }
}

Возможно, в оригинале эти запросы таили что-то эдакое, но в ядре они используются лишь для идентификации — что на том конце действительно PL2303. Во всяком случае, с таким минимумом устройство работает и под android (возможно, будет работать и под игровыми приставками, мне это безразлично).
При работе со строковыми дескрипторами иногда может оказаться, что дескриптор не влезает в стандартный объем 64-байтной посылки (особенно большими размерами славятся HID-дескрипторы), поэтому такие вещи надо разбить на несколько посылок. Отправляются такие дескрипторы только в начале коннекта, поэтому я решил не заморачиваться с конечным автоматом, а сделать блокирующую запись:
static void wr0(const uint8_t *buf, uint16_t size){
    if(setup_packet.wLength < size) size = setup_packet.wLength; // shortened request
    if(size < endpoints[0].txbufsz){
        EP_WriteIRQ(0, buf, size);
        return;
    }
    while(size){
        uint16_t l = size;
        if(l > endpoints[0].txbufsz) l = endpoints[0].txbufsz;
        EP_WriteIRQ(0, buf, l);
        buf += l;
        size -= l;
        uint8_t needzlp = (l == endpoints[0].txbufsz) ? 1 : 0;
        if(size || needzlp){ // send last data buffer
            uint16_t status = KEEP_DTOG(USB->EPnR[0]);
            // keep DTOGs, clear CTR_RX,TX, set TX VALID, leave stat_Rx
            USB->EPnR[0] = (status & ~(USB_EPnR_CTR_RX|USB_EPnR_CTR_TX|USB_EPnR_STAT_RX))
                            ^ USB_EPnR_STAT_TX;
            uint32_t ctr = 1000000;
            while(--ctr && (USB->ISTR & USB_ISTR_CTR) == 0){IWDG->KR = IWDG_REFRESH;};
            if((USB->ISTR & USB_ISTR_CTR) == 0){
                return;
            }
            if(needzlp) EP_WriteIRQ(0, (uint8_t*)0, 0);
        }
    }
}

(здесь еще надо было учесть, что если мы отправляем в последней посылке ровно 64 байта, то нужно еще и ZPL — посылку нулевой длины — отправить).
Работать с USB мы будем на прерываниях, поэтому все самое интересное начинается в обработчике прерывания usb_isr():
void usb_isr(){
    if (USB->ISTR & USB_ISTR_RESET){
        // Reinit registers
        USB->CNTR = USB_CNTR_RESETM | USB_CNTR_CTRM | USB_CNTR_SUSPM | USB_CNTR_WKUPM;
        USB->ISTR = 0;
        // Endpoint 0 - CONTROL
        // ON USB LS size of EP0 may be 8 bytes, but on FS it should be 64 bytes!
        lastaddr = LASTADDR_DEFAULT; // roll back to beginning of buffer
        EP_Init(0, EP_TYPE_CONTROL, USB_EP0_BUFSZ, USB_EP0_BUFSZ, EP0_Handler);
        // clear address, leave only enable bit
        USB->DADDR = USB_DADDR_EF;
        // state is default - wait for enumeration
        USB_Dev.USB_Status = USB_STATE_DEFAULT;
    }
    if(USB->ISTR & USB_ISTR_CTR){
        // EP number
        uint8_t n = USB->ISTR & USB_ISTR_EPID;
        // copy status register
        uint16_t epstatus = USB->EPnR[n];
        // copy received bytes amount
        endpoints[n].rx_cnt = USB_BTABLE->EP[n].USB_COUNT_RX & 0x3FF; // low 10 bits is counter
        // check direction
        if(USB->ISTR & USB_ISTR_DIR){ // OUT interrupt - receive data, CTR_RX==1 (if CTR_TX == 1 - two pending transactions: receive following by transmit)
            if(n == 0){ // control endpoint
                if(epstatus & USB_EPnR_SETUP){ // setup packet -> copy data to conf_pack
                    EP_Read(0, (uint8_t*)&setup_packet);
                    ep0dbuflen = 0;
                    // interrupt handler will be called later
                }else if(epstatus & USB_EPnR_CTR_RX){ // data packet -> push received data to ep0databuf
                    ep0dbuflen = endpoints[0].rx_cnt;
                    EP_Read(0, (uint8_t*)&ep0databuf);
                }
            }
        }
        // call EP handler
        if(endpoints[n].func) endpoints[n].func(endpoints[n]);
    }
    if(USB->ISTR & USB_ISTR_SUSP){ // suspend -> still no connection, may sleep
        usbON = 0;
        USB->CNTR |= USB_CNTR_FSUSP | USB_CNTR_LPMODE;
        USB->ISTR = ~USB_ISTR_SUSP;
    }
    if(USB->ISTR & USB_ISTR_WKUP){ // wakeup
        USB->CNTR &= ~(USB_CNTR_FSUSP | USB_CNTR_LPMODE); // clear suspend flags
        USB->ISTR = ~USB_ISTR_WKUP;
    }
}

Здесь мы обрабатываем такие прерывания, как RESET — хост говорит устройству, что нужно заново инициализировать USB, CTR (correct transfer) — прерывание по приему или передаче данных, а также вспомогательные SUSP и WKUP — "засни" и "проснись".
В обработке OUT-запросов (т.е. входящих для устройства) пришлось пойти на хитрость: т.к. некоторые вещи (как тот же LINECODING) передаются в "два захода", необходимо отдельно заполнять данными setup_packet и вспомогательный ep0databuf (где и лежат эти данные по установке LINECODING). Данные для LINECODING приходят так: сначала без флага SETUP приходит нужная информация, а потом уже с этим флагом — команда запроса SET_LINECODING. И, соответственно, вызывается процедура обработчика запроса.
Чтобы USB корректно работало, сначала нам надо настроить все конечные точки: выдать им адреса и размеры буферов, определить направление, задать функцию-обработчик. Это делается в EP_Init:
int EP_Init(uint8_t number, uint8_t type, uint16_t txsz, uint16_t rxsz, void (*func)()){
    if(number >= STM32ENDPOINTS) return 4; // out of configured amount
    if(txsz > USB_BTABLE_SIZE || rxsz > USB_BTABLE_SIZE) return 1; // buffer too large
    if(lastaddr + txsz + rxsz >= USB_BTABLE_SIZE) return 2; // out of btable
    USB->EPnR[number] = (type << 9) | (number & USB_EPnR_EA);
    USB->EPnR[number] ^= USB_EPnR_STAT_RX | USB_EPnR_STAT_TX_1;
    if(rxsz & 1 || rxsz > 512) return 3; // wrong rx buffer size
    uint16_t countrx = 0;
    if(rxsz < 64) countrx = rxsz / 2;
    else{
        if(rxsz & 0x1f) return 3; // should be multiple of 32
        countrx = 31 + rxsz / 32;
    }
    USB_BTABLE->EP[number].USB_ADDR_TX = lastaddr;
    endpoints[number].tx_buf = (uint16_t *)(USB_BTABLE_BASE + lastaddr);
    endpoints[number].txbufsz = txsz;
    lastaddr += txsz;
    USB_BTABLE->EP[number].USB_COUNT_TX = 0;
    USB_BTABLE->EP[number].USB_ADDR_RX = lastaddr;
    endpoints[number].rx_buf = (uint8_t *)(USB_BTABLE_BASE + lastaddr);
    lastaddr += rxsz;
    // buffer size: Table127 of RM
    USB_BTABLE->EP[number].USB_COUNT_RX = countrx << 10;
    endpoints[number].func = func;
    return 0;
}

Кому-то нравится руками задавать адреса буферов данных, я же решил все упростить — в переменной lastaddr хранится последний свободный адрес в буфере (при RESET эта переменная реинициируется на начало буфера). Дальше все по коду понятно.
Для разбора данных, поступающих на управляющую точку EP0, вызывается функция EP0_Handler (не буду весь ее текст приводить здесь, она длинная). Из остальных конечных точек, как я уже говорил, точка EP1 у нас хоть и определена, но не используется. А для приема/передачи заводим односторонние точки EP2 и EP3 (все эти данные хранятся в дескрипторе устройства, поэтому размеры буферов и направление передачи конечных точек нужно согласовывать с данными в дескрипторе). Их обработчики будут уже в другом файле — с более высокоуровневыми вещами.
У STM32F0x2 регистры данных USB_TX пишутся по 16 бит (в STM32F103 эмулируется 32-битное хранилище, т.е. писать надо как 32-битный блок, но активны там только 16 бит), а USB_RX вообще можно читать побайтно (у 103 они тоже эмулируют 32-битные блоки). В общем, здесь все проще:
void EP_WriteIRQ(uint8_t number, const uint8_t *buf, uint16_t size){
    uint8_t i;
    if(size > USB_TXBUFSZ) size = USB_TXBUFSZ;
    uint16_t N2 = (size + 1) >> 1;
    // the buffer is 16-bit, so we should copy data as it would be uint16_t
    uint16_t *buf16 = (uint16_t *)buf;
    for (i = 0; i < N2; i++){
        endpoints[number].tx_buf[i] = buf16[i];
    }
    USB_BTABLE->EP[number].USB_COUNT_TX = size;
}

void EP_Write(uint8_t number, const uint8_t *buf, uint16_t size){
    EP_WriteIRQ(number, buf, size);
    uint16_t status = KEEP_DTOG(USB->EPnR[number]);
    // keep DTOGs, clear CTR_TX & set TX VALID to start transmission
    USB->EPnR[number] = (status & ~(USB_EPnR_CTR_TX)) ^ USB_EPnR_STAT_TX;
}

int EP_Read(uint8_t number, uint8_t *buf){
    int n = endpoints[number].rx_cnt;
    if(n){
        for(int i = 0; i < n; ++i)
            buf[i] = endpoints[number].rx_buf[i];
    }
    return n;
}

EP_WriteIRQ отличается от EP_Write тем, что вызывается внутри обработчиков прерывания, поэтому в ней флаги EPnR не меняются.
В файле usb.c лежат сравнительно высокоуровневые функции (правда, я и USB_setup зачем-то здесь оставил).
Собственно, настройка USB и начинается с USB_setup:
void USB_setup(){
    RCC->APB1ENR |= RCC_APB1ENR_CRSEN | RCC_APB1ENR_USBEN; // enable CRS (hsi48 sync) & USB
    RCC->CFGR3 &= ~RCC_CFGR3_USBSW; // reset USB
    RCC->CR2 |= RCC_CR2_HSI48ON; // turn ON HSI48
    uint32_t tmout = 16000000;
    while(!(RCC->CR2 & RCC_CR2_HSI48RDY)){if(--tmout == 0) break;}
    FLASH->ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY;
    CRS->CFGR &= ~CRS_CFGR_SYNCSRC;
    CRS->CFGR |= CRS_CFGR_SYNCSRC_1; // USB SOF selected as sync source
    CRS->CR |= CRS_CR_AUTOTRIMEN; // enable auto trim
    CRS->CR |= CRS_CR_CEN; // enable freq counter & block CRS->CFGR as read-only
    RCC->CFGR |= RCC_CFGR_SW;
    // allow RESET and CTRM interrupts
    USB->CNTR = USB_CNTR_RESETM | USB_CNTR_WKUPM;
    // clear flags
    USB->ISTR = 0;
    // and activate pullup
    USB->BCDR |= USB_BCDR_DPPU;
    NVIC_EnableIRQ(USB_IRQn);
}

Тактируем USB от HSI48, что позволяет не цеплять внешний кварц. Используем автокоррекцию HSI48 от USB SOF. Для начала разрешаем только прерывания RESET и WKUP (остальное будем разрешать уже после настройки конечных точек).
IN/OUT запросы данных обрабатываем этими фукнциями:
static void transmit_Handler(){ // EP3IN
    tx_succesfull = 1;
    uint16_t epstatus = KEEP_DTOG_STAT(USB->EPnR[3]);
    // clear CTR keep DTOGs & STATs
    USB->EPnR[3] = (epstatus & ~(USB_EPnR_CTR_TX)); // clear TX ctr
}

static void receive_Handler(){ // EP2OUT
    rxNE = 1;
    uint16_t epstatus = KEEP_DTOG_STAT(USB->EPnR[2]);
    USB->EPnR[2] = (epstatus & ~(USB_EPnR_CTR_RX)); // clear RX ctr
}

Обработчик IN (выходящие данные) ощичает флаг CTR_TX и выставляет внутренний флаг tx_succesfull (говорящий о том, что предыдущая посылка отправлена и можно отправлять следующую). А в обработчике OUT (входящие данные) мы выствляем внутренний флаг rxNE (говорящий, что в буфере есть данные и их можно считать) и очищаем флаг CTR_RX. Выставлять ACK мы будем лишь после того, как данные из буфера будут обработаны!
Для того, чтобы не блокировать МК на время отправки мелких объемов данных (а в основном они значительно меньше 64 байт), в функции USB_send есть возможность "отложенной" отправки данных:
void USB_send(const uint8_t *buf, uint16_t len){
    if(!usbON || !len) return;
    if(len > USB_TXBUFSZ-1 - buflen){
        usbwr(usbbuff, buflen);
        buflen = 0;
    }
    if(len > USB_TXBUFSZ-1){
        USB_send_blk(buf, len);
        return;
    }
    while(len--) usbbuff[buflen++] = *buf++;
}

(последняя строчка просто помещает маленькие объемы данных во вспомогательный буфер, который будет отправлен в последующем при помощи функции send_next()).
Из основного цикла main на каждом проходе надо запускать функцию usb_proc:
void usb_proc(){
    switch(USB_Dev.USB_Status){
        case USB_STATE_CONFIGURED:
            // make new BULK endpoint
            // Buffer have 1024 bytes, but last 256 we use for CAN bus (30.2 of RM: USB main features)
            EP_Init(1, EP_TYPE_INTERRUPT, USB_EP1BUFSZ, 0, EP1_Handler); // IN1 - transmit
            EP_Init(2, EP_TYPE_BULK, 0, USB_RXBUFSZ, receive_Handler); // OUT2 - receive data
            EP_Init(3, EP_TYPE_BULK, USB_TXBUFSZ, 0, transmit_Handler); // IN3 - transmit data
            USB_Dev.USB_Status = USB_STATE_CONNECTED;
        break;
        case USB_STATE_DEFAULT:
        case USB_STATE_ADDRESSED:
            if(usbON){
                usbON = 0;
            }
        break;
        default: // USB_STATE_CONNECTED - send next data portion
            if(!usbON) return;
            send_next();
    }
}

Здесь анализируются состояния КА USB. Скажем, в состоянии USB_STATE_CONFIGURED (EP0 настроена, все дескрипторы отправлены) нужно настроить остальные конечные точки. В состояниях USB_STATE_DEFAULT (начальное) и USB_STATE_ADDRESSED (только прошла процедура адресации) USB еще пользоваться нельзя — поэтому снимаем флаг usbON. А в состоянии USB_STATE_CONNECTED вызываем ту самую send_next().
Более высокоуровневую функцию USB_receive вызываем уже откуда-нибудь извне. Она возвращает количество считанных данных и заполняет ими буфер. Ну, а т.к. данные теперь надежно сохранены, можно отправлять хосту ACK.
uint8_t USB_receive(uint8_t *buf){
    if(!usbON || !rxNE) return 0;
    uint8_t sz = EP_Read(2, buf);
    uint16_t epstatus = KEEP_DTOG(USB->EPnR[2]);
    // keep stat_tx & set ACK rx
    USB->EPnR[2] = (epstatus & ~(USB_EPnR_STAT_TX)) ^ USB_EPnR_STAT_RX;
    rxNE = 0;
    return sz;
}

Уже в main.c размещаем функцию буферизованного чтения из USB с выставлением флага готовности по '\n':
static char *get_USB(){
    static char tmpbuf[USBBUF+1], *curptr = tmpbuf;
    static int rest = USBBUF;
    uint8_t x = USB_receive((uint8_t*)curptr);
    if(!x) return NULL;
    curptr[x] = 0;
    if(x == 1 && *curptr == 0x7f){ // backspace
        if(curptr > tmpbuf){
            --curptr;
            USND("\b \b");
        }
        return NULL;
    }
    USB_sendstr(curptr); // echo
    if(curptr[x-1] == '\n'){ // || curptr[x-1] == '\r'){
        curptr = tmpbuf;
        rest = USBBUF;
        // omit empty lines
        if(tmpbuf[0] == '\n') return NULL;
        // and wrong empty lines
        if(tmpbuf[0] == '\r' && tmpbuf[1] == '\n') return NULL;
        return tmpbuf;
    }
    curptr += x; rest -= x;
    if(rest <= 0){ // buffer overflow
        usart_send("\nUSB buffer overflow!\n"); transmit_tbuf();
        curptr = tmpbuf;
        rest = USBBUF;
    }
    return NULL;
}

Здесь происходит эхо введенных символов и обработка backspace (чтобы можно было удалять неправильно набранные символы).

ОК, с USB закончили. Пора переходить к CAN.
В can.h определим структуру
typedef struct{
    uint8_t data[8];    // up to 8 bytes of data
    uint8_t length;     // data length
    uint16_t ID;        // ID of receiver
} CAN_message;

В ней содержатся данные (до 8 байт) для стандартной CAN-посылки, длина данных и идентификатор получателя этих данных. И определим флаги состояния CAN:
typedef enum{
    CAN_STOP,
    CAN_READY,
    CAN_BUSY,
    CAN_OK,
    CAN_FIFO_OVERRUN
} CAN_status;

В can.c определяем основные функции для работы с CAN. Входящие данные буферизуются в массиве messages[CAN_INMESSAGE_SIZE].
Настройку CAN делаем по сниппетам. Разве что аргументом этой функции является скорость в кбод, поэтому проверяем в начале, допустимым ли является значение скорости.
void CAN_setup(uint16_t speed){
    LED_off(LED1);
    if(speed == 0) speed = oldspeed;
    else if(speed < 50) speed = 50;
    else if(speed > 3000) speed = 3000;
    oldspeed = speed;
    uint32_t tmout = 16000000;
    if(CANID == 0xFFFF) readCANID();
    // Configure GPIO: PB8 - CAN_Rx, PB9 - CAN_Tx
    /* (1) Select AF mode (10) on PB8 and PB9 */
    /* (2) AF4 for CAN signals */
    GPIOB->MODER = (GPIOB->MODER & ~(GPIO_MODER_MODER8 | GPIO_MODER_MODER9))
                 | (GPIO_MODER_MODER8_AF | GPIO_MODER_MODER9_AF); /* (1) */
    GPIOB->AFR[1] = (GPIOB->AFR[1] &~ (GPIO_AFRH_AFRH0 | GPIO_AFRH_AFRH1))\
                  | (4 << (0 * 4)) | (4 << (1 * 4)); /* (2) */
    /* Enable the peripheral clock CAN */
    RCC->APB1ENR |= RCC_APB1ENR_CANEN;
      /* Configure CAN */
    /* (1) Enter CAN init mode to write the configuration */
    /* (2) Wait the init mode entering */
    /* (3) Exit sleep mode */
    /* (4) Normal mode, set timing to 100kb/s: TBS1 = 4, TBS2 = 3, prescaler = 60 */
    /* (5) Leave init mode */
    /* (6) Wait the init mode leaving */
    /* (7) Enter filter init mode, (16-bit + mask, bank 0 for FIFO 0) */
    /* (8) Acivate filter 0 for two IDs */
    /* (9) Identifier mask */
    /* (10) Set the Id mask */
    /* (12) Leave filter init */
    /* (13) Set error interrupts enable */
    CAN->MCR |= CAN_MCR_INRQ; /* (1) */
    while((CAN->MSR & CAN_MSR_INAK)!=CAN_MSR_INAK) /* (2) */
    {
        if(--tmout == 0) break;
    }
    CAN->MCR &=~ CAN_MCR_SLEEP; /* (3) */
    CAN->MCR |= CAN_MCR_ABOM; /* allow automatically bus-off */

    CAN->BTR |=  2 << 20 | 3 << 16 | (6000/speed) << 0; /* (4) */
    CAN->MCR &=~ CAN_MCR_INRQ; /* (5) */
    tmout = 16000000;
    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 | CAN_FA1R_FACT1; /* (8) */
    /*  (9, 10)  */
    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
    CAN->FMR &=~ CAN_FMR_FINIT; /* (12) */
    CAN->IER |= CAN_IER_ERRIE | CAN_IER_FOVIE0 | CAN_IER_FOVIE1; /* (13) */
    /* Configure IT */
    /* (14) Set priority for CAN_IRQn */
    /* (15) Enable CAN_IRQn */
    NVIC_SetPriority(CEC_CAN_IRQn, 0); /* (14) */
    NVIC_EnableIRQ(CEC_CAN_IRQn); /* (15) */
    can_status = CAN_READY;
}

Поначалу фильтр позволяет принимать абсолютно все сообщения: нечетные в FIFO0 и четные в FIFO1. Далее фильтры можно будет перенастроить.
В отличие от USB, у CAN в прерывании анализируются лишь ошибки, а вот для работы с данными из main постоянно запускается can_proc() (объемная, листинг здесь размещать не буду). Здесь рассматриваются различные флаги и обрабатываются ошибки линии: если на шине нет никого, либо накапливается много ошибок, CAN переинициализируется.
Функция can_send(uint8_t *msg, uint8_t len, uint16_t target_id) отправляет сообщение msg длиной len с идентификатором target_id. В принципе, можно было бы уменьшить количество аргументов этой функции, если поместить эти данные в обертку — структуру CAN_message. Функция находит первый свободный "почтовый ящик", заполняет в нем регистры данных и инициализирует посылку.
Функция can_process_fifo(uint8_t fifo_num) запускается при наличии данных в соответствующем буфере FIFO. Все данные помещаются в массив-буфер, откуда впоследствии их можно считать. Если буфер полон, то функция "надеется", что к следующем запуску можно будет его опустошить (иначе происходит переполнение FIFO — ничего с этим не поделать).

Как все это работает.
При получении определенной команды по USB, она анализируется (proto.c), и если это команда на передачу данных (s), запускается функция
TRUE_INLINE void sendCANcommand(char *txt){
    CAN_message *msg = parseCANmsg(txt);
    if(!msg) return;
    uint32_t N = 1000000;
    while(CAN_BUSY == can_send(msg->data, msg->length, msg->ID)){
        if(--N == 0) break;
    }
}

В ней происходит парсинг введенных пользователем данных, и если все ОК, то отправляется сообщение.
При поступлении сообщения с CAN, его содержимое выводится на терминал.
Для упрощения создания фильтров я добавил еще и софтовый фильтр — для возможности отклонения сообщений с ID из списка (иначе пришлось бы лепить аппаратный фильтр с нужной маской и ID, а считать-то лень!).
Функция add_filter позволяет добавить или удалить (если фильтр не содержит данных) фильтр с номером от 0 до 27. Т.е. можно удалить фильтры по умолчанию и заменить их своими. В функции идет довольно-таки длинный парсинг текстовых данных: разбирается, в каком режиме фильтр (список или маска), а далее заполняются сами данные фильтра.

При разработке сниффера я тестировал его на платах термодатчиков и разрабатываемой управлялки шаговыми двигателями.

April 2025

S M T W T F S
  1 23 45
67 89101112
13141516171819
20212223242526
27282930   

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated May. 22nd, 2025 06:54 am
Powered by Dreamwidth Studios