LINUX.ORG.RU

STM32 и прерывания

 , , ,


0

3

Ковыряю сейчас STM32 с libopencm3. Попытался расширить пример usart_irq (наверное, нужно было ещё посмотреть на usart_irq_printf, но его я пока не открывал). Теперь у меня есть буфер для отправки и для приёма строк.
Кода не очень много, потому, думаю, можно выложить весь.

#include <string.h>
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/stm32/usart.h>
#include <libopencm3/cm3/nvic.h>

#define BS 16           // buffer size (number of strings)

char request[256];      // string to send to modem
char response[BS][256]; // last BS strings received from modem
int row, ch;            // number of string and last character received from modem
uint8_t last = 'A';     // last character received from modem
int step = 0;

static void clock_setup(void) {
    rcc_clock_setup_in_hse_8mhz_out_72mhz();

    /* Enable clocks for GPIO port A (for GPIO_USART1_TX) and USART1. */
    rcc_periph_clock_enable(RCC_GPIOA);
    rcc_periph_clock_enable(RCC_AFIO);
    rcc_periph_clock_enable(RCC_USART1);
}

static void usart_setup(void) {
    /* Enable the USART1 interrupt. */
    nvic_enable_irq(NVIC_USART1_IRQ);

    /* Setup GPIO pin GPIO_USART1_RE_TX on GPIO port A for transmit. */
    gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ,
              GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO_USART1_TX);

    /* Setup GPIO pin GPIO_USART1_RE_RX on GPIO port A for receive. */
    gpio_set_mode(GPIOA, GPIO_MODE_INPUT,
              GPIO_CNF_INPUT_FLOAT, GPIO_USART1_RX);

    /* Setup UART parameters. */
    usart_set_baudrate(USART1, 115200);
    usart_set_databits(USART1, 8);
    usart_set_stopbits(USART1, USART_STOPBITS_1);
    usart_set_parity(USART1, USART_PARITY_NONE);
    usart_set_flow_control(USART1, USART_FLOWCONTROL_NONE);
    usart_set_mode(USART1, USART_MODE_TX_RX);

    /* Enable USART1 Receive interrupt. */
    USART_CR1(USART1) |= USART_CR1_RXNEIE;

    /* Finally enable the USART. */
    usart_enable(USART1);
}

static void gpio_setup(void) {
    gpio_set(GPIOA, GPIO1);

    /* Setup GPIO1 (in GPIO port A) for LED use. */
    gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ,
              GPIO_CNF_OUTPUT_PUSHPULL, GPIO1);
}

static void wait(void) {
    int i;
	for (i = 0; i < 800000; i++)	/* Wait a bit. */
		__asm__("nop");
}

static int row_num(int n) {
    // use this function to get correct row offset
    // row_num(0) - latest row, row_num(1) - second latest row, etc.
    int r = row - n;
    if (r < 0) {
        return r + BS;
    } else {
        return r;
    }
}

static void action_select(void) {
    if (strcmp(response[row], "OK") == 0) {
        gpio_toggle(GPIOA, GPIO1);
        wait();
        gpio_toggle(GPIOA, GPIO1);
        wait();
        gpio_toggle(GPIOA, GPIO1);
        wait();
        gpio_toggle(GPIOA, GPIO1);
    } else if (strcmp(response[row], "ERROR") == 0) {
        gpio_toggle(GPIOA, GPIO1);
        wait();
        gpio_toggle(GPIOA, GPIO1);
    } else if (strcmp(response[row], "PRINT") == 0) {
        int i;
        strcpy(request, "\r\n");
        for (i = 0; i < BS; i++) {
            char num[5] = { '[', i < 10 ? '0' + i : 'A' + i - 10, ']', ':', ' ' };
            strcat(request, num);
            strcat(request, response[row_num(i)]);
            strcat(request, "\r\n");
        }
    }
}

static void set_send(void) {
    /* Enable transmit interrupt so it sends back the data. */
    USART_CR1(USART1) |= USART_CR1_TXEIE;
}

static void unset_send(void) {
    /* Disable the TXE interrupt as we don't need it anymore. */
    USART_CR1(USART1) &= ~USART_CR1_TXEIE;
}

static void send(void) {
    if (request[0] != 0) {
        int i = 0;
        uint8_t s = '0';
        while (s = request[i++], s != 0) {
            usart_wait_send_ready(USART1);
            usart_send(USART1, s);
        }
        usart_wait_send_ready(USART1);
        usart_send(USART1, '\r');
        // new line here should be commented out in production
        usart_wait_send_ready(USART1);
        usart_send(USART1, '\n');
        request[0] = 0;
    } else {
        // echo last char back (should also be commented out in production)
        usart_wait_send_ready(USART1);
        usart_send(USART1, last);
        if (last == '\r') {
            usart_wait_send_ready(USART1);
            usart_send(USART1, '\n');
        }
    }

    unset_send();
}

static void receive(void) {
    /* Retrieve the data from the peripheral. */
    last = usart_recv(USART1);

    response[row][ch] = last;
    if (response[row][ch] == '\r') {
        response[row][ch] = '\0';
        action_select();
        if (ch > 1) row++;
        ch = 0;
        if (row == BS) row = 0; 
    } else if (response[row][ch] != '\n') {
        ch++;
    }

    set_send();
}

void usart1_isr(void) {

    /* Check if we were called because of RXNE. */
    if (((USART_CR1(USART1) & USART_CR1_RXNEIE) != 0) &&
        ((USART_SR(USART1) & USART_SR_RXNE) != 0)) {
        receive();
    }

    /* Check if we were called because of TXE. */
    if (((USART_CR1(USART1) & USART_CR1_TXEIE) != 0) &&
        ((USART_SR(USART1) & USART_SR_TXE) != 0)) {
        send();
    }
}

static void send_request(char *str) {
    strcpy(request, str);
    send();
}

int main(void) {
    clock_setup();
    gpio_setup();
    usart_setup();

    gpio_set(GPIOA, GPIO1);
    send_request("HELLO\r");

    /* Wait forever and do nothing. */
    while (1)
        __asm__("nop");

    return 0;
}
И всё работает:
HELLO

OK

ERROR



PRINT
[0]: PRINT
[1]: ERROR
[2]: OK
[3]: 
[4]: 
[5]: 
[6]: 
[7]: 
[8]: 
[9]: 
[A]: 
[B]: 
[C]: 
[D]: 
[E]: 
[F]: 


А как теперь сделать простой вопрос-ответ? Например, плата говорит «HELLO», я отвечаю «WORLD», плата отвечает «TRUE» и включает светодиод. Довольно просто.
Я подумал, что достаточно после инициализации в main добавить следующее:
    gpio_set(GPIOA, GPIO1);
    send_request("HELLO\r");

    while (strcmp(response[row_num(0)], "WORLD") != 0)
        __asm__("nop");

    gpio_clear(GPIOA, GPIO1);
    send_request("TRUE\r");
Но нет. Таким образом прерывания срабатывают, эхо на терминале я получаю нормально, но цикл никогда не завершается и функция вывода последних значений попортилась:
HELLO
WORLD
PRINT
[0]: 
     PRINT
[1]: 
     WORLD
[2]: 

[3]: 

[4]: 

Призываю в тред Eddy_Em, ncrmnt, KivApple, RiseOfDeath. И подскажите форум, где такие вопросы не будут оффтопиком.

★★★★★

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

Eddy_Em ☆☆☆☆☆ ()
Ответ на: комментарий от CYB3R

Да у меня посмотри хоть в том же ircontroller'е на гитхабе (только я там не очень удачно обработку буфера сделал при работе в строковом режиме: посимвольно, лучше было бы проверять на '\n' и лишь когда строка будет полной, обрабатывать).

Eddy_Em ☆☆☆☆☆ ()
Ответ на: комментарий от Eddy_Em
void UART_isr(uint32_t UART){
    uint8_t bufidx = 0, data;
    UART_buff *curbuff;
    // Check if we were called because of RXNE
    if(USART_SR(UART) & USART_SR_RXNE){
        // parce incoming byte
        data = usart_recv(UART);
        fill_uart_RXbuff(UART, data);
    }
    // Check if we were called because of TXE -> send next byte in buffer
    if((USART_CR1(UART) & USART_CR1_TXEIE) && (USART_SR(UART) & USART_SR_TXE)){
        switch(UART){
            case USART1:
                bufidx = 0;
            break;
            case USART2:
                bufidx = 1;
            break;
            case USART3:
                bufidx = 2;
            break;
            default: // error - return
                return;
        }
        curbuff = &TX_buffer[bufidx];
        bufidx = curbuff->start; // start of data in buffer
        if(bufidx != curbuff->end){ // there's data in buffer
            // Put data into the transmit register
            usart_send(UART, curbuff->buf[bufidx]);
            if(++(curbuff->start) == UART_TX_DATA_SIZE){ // reload start
                curbuff->start = 0;
            }
        }else{ // Disable the TXE interrupt, it's no longer needed
            USART_CR1(UART) &= ~USART_CR1_TXEIE;
            // empty indexes
            curbuff->start = 0;
            curbuff->end = 0;
        }
    }
}
Eddy_Em ☆☆☆☆☆ ()
Ответ на: комментарий от Eddy_Em

Сейчас добавил usart_wait_recv_ready(USART1) и всё прекрасно заработало.

CYB3R ★★★★★ ()
Ответ на: комментарий от Eddy_Em

Спасибо, обязательно посмотрю твой гитхаб. Кстати, видел у тебя там ещё примеры для STM8. С STM8L можно использовать что-то похожее на libopencm3? Я нашёл только libstm8, но не использовал пока.

CYB3R ★★★★★ ()
Ответ на: комментарий от CYB3R

С STM8L можно использовать что-то похожее на libopencm3?

Зачем код раздувать? Там даташит тонюсенький же, скажем, на STM8S105 (у меня их с десяток) всего-то с полтыщи страниц. USB нет — никакого смысла юзать чужие библиотеки.

Я и opencm3 непонятно зачем использую, если про USB забыть: все равно ведь на каждый чих лезу в даташит, затем грепаю по коду, в какой функции нужные регистры меняются, смотрю содержимое, если функция дюжеть тяжелей прямой записи в регистры, пишу напрямую, если нет — использую функцию.

Для STM8s у меня даже свой самописный заголовочный файл с регистрами (правда, как назвал я его почему-то STM8l, так и не переименую). По мере необходимости дописываю туда новое.

Eddy_Em ☆☆☆☆☆ ()
Ответ на: комментарий от CYB3R

Ты бы сначала содержимое этой говнофункции глянул! Она же блокирует!!!

void usart_wait_recv_ready(uint32_t usart)
{
    /* Wait until the data is ready to be received. */
    while ((USART_SR(usart) & USART_SR_RXNE) == 0);
}
Нафиг такой быдлокод нужен?!

Eddy_Em ☆☆☆☆☆ ()
Ответ на: комментарий от Eddy_Em

Ну, это слишком хардкорно для меня. Я грепаю даташит только за назначением ног контроллера. И грепаю документацию и примеры кода libopencm3, если что-то конкретное нужно.

CYB3R ★★★★★ ()
Ответ на: комментарий от Eddy_Em

Нафиг такой быдлокод нужен?!

Работает — не трогай.

CYB3R ★★★★★ ()
Ответ на: комментарий от CYB3R

Это пока ты ничего сложного не делаешь. Как только тебе нужно будет на бешеных скоростях с DMA работать и т.п., сразу начнешь напрямую в регистры писать!

Eddy_Em ☆☆☆☆☆ ()
Ответ на: комментарий от CYB3R

Лучше сразу писать по-человечески, а не в стиле «авр головного мозга».

Eddy_Em ☆☆☆☆☆ ()
Ответ на: комментарий от Eddy_Em

А что плохого в блокирующей функции? У меня теперь есть такое в коде:

usart_wait_recv_ready(USART1);
s = usart_recv(USART1);
usart_wait_send_ready(USART1);
usart_send(USART1, s);
Нужно от этого избавляться?

CYB3R ★★★★★ ()
Ответ на: комментарий от CYB3R

А что плохого в блокирующей функции?

Ничего в ней хорошего. Даже когда только этим твой МК занимается. А у меня обычно там всякие процессы вроде работы с USB, таймерами и т.д., и т.п. В основном цикле main проверяются флаги, которые выставляются в прерываниях, а также всякие нажатия кнопочек и т.п. А потом уже вызываются процедуры обработки, реализованные в виде КА, чтобы ничего нигде не блокировалось! Что, думаешь, зря я задницу рвал, чтобы полуаппаратный 1-wire реализовать?

Нужно от этого избавляться?

Конечно. В ожиданиях тупо нет смысла. Ты на компе тоже делаешь в одном-единственном потоке read(fd), пока данные не придут? Нет, ты делаешь поллинг, либо запускаешь отдельный поток! Так и здесь: для потоков нужно менеджер писать, что напрягает, поэтому проще поллинг использовать.

Eddy_Em ☆☆☆☆☆ ()
Ответ на: комментарий от Eddy_Em

В основном цикле main проверяются флаги, которые выставляются в прерываниях, а также всякие нажатия кнопочек и т.п. А потом уже вызываются процедуры обработки, реализованные в виде КА, чтобы ничего нигде не блокировалось

Я сначала тоже так делал, но потом просто повесил вызов функции в прерывание.

CYB3R ★★★★★ ()
Ответ на: комментарий от CYB3R

повесил вызов функции в прерывание

Это разумно делать лишь если твоя функция от силы сотню микросекунд выполняется. И то, не всегда. Правильней флаги выставлять.

Ну да ничего, как нарвешься на проблемы из-за аврного подхода, поймешь.

Eddy_Em ☆☆☆☆☆ ()
Ответ на: комментарий от Eddy_Em

Я так и не понял, как сделать красиво ожидание с таймером.
Что-нибудь типа функций delay_ms(), delay_ns(), delay_us().

CYB3R ★★★★★ ()
Ответ на: комментарий от CYB3R

delay_ms(), delay_ns(), delay_us()

Руки отрывать!

Зачем тебе таймеры? DMA + таймер == правильно.

Если же тебе просто примерно нужно ждать (скажем, время для определения двойного нажатия кнопки), это делается в основном цикле — сравнением времени последнего нажатия с временем текущего.

Все делается элементарно, функции вроде for(x=1;x<N;++x)nop() можно лишь в самом начале (до while(1)) запихивать, когда периферию на старте инициализируешь — там пофиг, т.к. при запуске можно маленько и подождать.

Eddy_Em ☆☆☆☆☆ ()

Короче, ты бросай эту аврщину. Переключайся на человеческий стиль мышления. Не надо быть ардуинщиком.

Eddy_Em ☆☆☆☆☆ ()
Ответ на: комментарий от CYB3R

Погляди в сторону FreeRTOS и подобных, если задача легче бъётся на процессы, чем представляется конечными автоматами. Линейный алгоритм проще отлаживается. Там есть и мютексы/семафоры, и вытесняющая и кооперативная многозадачность. С другой стороны - больше расход RAM под стеки.

qbe ()
Ответ на: комментарий от Eddy_Em

Короче, ты бросай эту аврщину. Переключайся на человеческий стиль мышления. Не надо быть ардуинщиком.

Ну вот я и пытаюсь. А можно ещё примеры кода, например для обработки двойного нажатия (чтобы ещё дребезг контактов программно устранять)? Ещё интересно, например, опрашивать датчик раз в 10 секунд. Или если была нажата кнопка в течении 30 секунд, включить зелёный светодиод, а если 30 секунд прошло, а кнопку не нажали — красный. Как можно такое сделать?

CYB3R ★★★★★ ()
Ответ на: комментарий от Eddy_Em

Твой подход как раз не человеческий стиль мышления, а ассемблер головного мозга и преждевременная оптимизация.

qbe ()
Ответ на: комментарий от CYB3R

Двойное касание еще не делал, т.к. надобности не было. Одинарного — дофига в моих репах на гитхабе (естественно, с устранением дребезга).

опрашивать датчик раз в 10 секунд

У меня в ircontroller'е есть режим «логгирование температуры», когда каждые 10 секунд (ясен пень, микроконтроллерных, а не реальных, т.к. мне там RTC нафиг не нужен был) выплевываются по USB значения температур в консольку. Элементарно в main() проверяешь таймер, как 10с прошло — делаешь.

Или если была нажата кнопка в течении 30 секунд, включить зелёный светодиод, а если 30 секунд прошло, а кнопку не нажали — красный.

Аналогично на КА легко делается.

Eddy_Em ☆☆☆☆☆ ()
Ответ на: комментарий от qbe

ассемблер головного мозга

Никогда в жизни не писал на асме.

преждевременная оптимизация

Лучше, чем никакой.

Если задача легко решается с помощью КА, пусть так и решается. Если же легче распараллелизовать, то нужно писать свою мини-ртось. Вот, кстати, было бы время, чтобы детально вникнуть, сделал бы себе мини-ртоську. Но влом — пока надобности не было. Это как и с ЯП. Вот понадобилось мне быдлоформочки в хытымле делать, пришлось жабкоскрипту слегка научиться, а больше мне никаких ЯП (кроме теха/латеха, естественно, но он не совсем ЯП, хоть и Тьюринг-полный) не нужно.

Eddy_Em ☆☆☆☆☆ ()
Ответ на: комментарий от Eddy_Em

Я в своё время писал небольшую rtos, чтобы разобраться с потрохами армовского ядра, ещё на arm7tdmi, но потом, глянув на переключение контекста, понял, что изобрёл FreeRTOS :)

Реализации по быстродействию получились почти равными, но FreeRTOS оказалась богаче на фичи, поэтому свой велосипед я забросил.

О потраченном времени не жалею, т.к. в результате достаточно хорошо изучил внутренности FreeRTOS.

p.s. «ассемблер» выше при том, что ты, вместо использования библиотечных функций, постоянно строишь велосипед, пытаясь быть умнее компилятора (по твоим же словам, грепаешь код в поисках обращения к регистру, и проверяешь, было или не было уже к нему обращение).

Лучше потратить своё время на действительно критичное место в программе, вместо оптимизации того, что будет выполняться всего один раз при запуске программы. Жизнь - она короткая, и надо правильно расставлять приоритеты.

p.p.s. заворачивание в функцию даже, казалось бы, примитивной записи в порт, может впоследствии очень упростить жизнь. К примеру, при переезде на совершенно другой контроллер. Я однажды подобное проходил - перенёс проект приличного размера (бинарник около 100К) с LPC1768 на STM32F407 за пару дней. В проекте были и USB, и несколько DMA, и реалтайм-DSP. (Без SPL, обкладываемой тобой г-ном и фаллоимитаторами, я бы застрял тогда на пару недель).

qbe ()
Последнее исправление: qbe (всего исправлений: 1)
Ответ на: комментарий от qbe

постоянно строишь велосипед, пытаясь быть умнее компилятора

Увы, компилятор очень тупой. Он не хочет разворачивать и инлайнить функции! В итоге получается очень медленно. В принципе, компилятор-таки не гениальный ИИ, откуда он знает, как правильно?

Поэтому для мелкоконтроллеров использование библиотек должно быть минимизировано до предела!

вместо оптимизации того, что будет выполняться всего один раз при запуске программы

Как раз насчет этих вещей я не парюсь: пофиг, сколько при первом запуске ждать: 1мс или 10мс.

заворачивание в функцию даже, казалось бы, примитивной записи в порт, может впоследствии очень упростить жизнь

Ни в коем разе.

Без SPL, обкладываемой тобой г-ном и фаллоимитаторами, я бы застрял тогда на пару недель

Ну и зачем ты шлак deprecated'ный и донельзя забагованный используешь? Неужто ты этот быдлокод не видел? Это ж как ардуинщики, у которых код «помигать светодиодом по нажатию кнопочки» выливается в итоге в пару-тройку килобайт бинаря!

Eddy_Em ☆☆☆☆☆ ()
Ответ на: комментарий от Eddy_Em

На момент использования SPL он ещё не был deprecated. Багов было не так уж много (только I2C кровь мне попортил), критичные места вроде DMA и нагруженных прерываний перевёл на прямое использование регистров.

Увы, компилятор очень тупой. Он не хочет разворачивать и инлайнить функции!

У нас, наверное, разные компиляторы :) Мне gcc -O2 вполне инлайнит и разворачивает вызовы функций http://gcc.gnu.org/onlinedocs/gcc-4.0.1/gcc/Inline.html, и даже иногда без inline (если вызов функции в том же исходнике, что и определение)

На худой конец, можешь сделать макросом: #define BITBANG_PIN_LOW() (GPIOA->BRR = (1<<PIN_NUMBER))

qbe ()
Ответ на: комментарий от Eddy_Em

И зря ты наезжаешь на ардуинщиков. Все когда-то учились программировать. Пусть у них программа на несколько килобайт жирнее твоей, если она свою задачу выполняет, и вмещается в выбранный контроллер.

Когда-нибудь вырастут и слезут с трёхколёсного велосипеда, в крайнем случае - так и останутся в своей нише, и не будут заставлять тебя дебажить их код.

qbe ()

Я вот тоже интересующийся этой темой нуб, и есть небольшой вопросик: почему так сложно сделаны обработчики прерываний?

(https://github.com/doceme/libopencm3/blob/master/lib/stm32/f4/vector.c)

Ведь можно же явно указывать обработчик напимрер:

vector_table[UART1_ISR] = &my_isr;

Тогда и в рантайме было бы легко менять обработчика.

redixin ★★★★ ()
Ответ на: комментарий от qbe

Пусть у них программа на несколько килобайт жирнее твоей, если она свою задачу выполняет, и вмещается в выбранный контроллер.

А если не вмещается, они покупают более жирный. Те, кто придумали ардуйню — гении сродни гилли бейтсу и джтиву собсу: надо рассчитывать на то, что юзер — полный дебил, но не выдавать ему этот секрет, а обнадеживать. И каждый раз, как юзер натыкается на проблему, предлагать решить ее экстенсивным методом: юзверь-то дурак!

Когда-нибудь вырастут и слезут с трёхколёсного велосипеда

Никогда. Знаю я 50-60-летних аврщиков. Никуда они слезать не хотят.

в крайнем случае

А потом создатель помирает, и приходится для восстановления работоспособности железки с нуля переделывать ВСĒ!

Eddy_Em ☆☆☆☆☆ ()
Ответ на: комментарий от Eddy_Em

Те, кто придумали ардуйню — гении сродни гилли бейтсу и джтиву собсу

Ардуйню придумали для того чтобы она день помигала светодиодами, а потом потерялась в куче хлама за ненадобностью. Это просто одноразовая игрушка. Люди и заработали и хомячков развлекли.

Хотя не исключены варианты где оно может и пользу принести. Например представь задачу где не нужно DMА и нет желания морочить голову с libopencm3. Представил?

redixin ★★★★ ()
Ответ на: комментарий от Eddy_Em

как юзер натыкается на проблему, предлагать решить ее экстенсивным методом

Именно из-за такого подхода ты работаешь с многомегагерцовыми 32-х разрядными ARM-ами, а не с каким-нить 8051.

one_more_hokum ★★★ ()
Ответ на: комментарий от redixin

По умолчанию таблица прерываний размещена в flash-памяти. В 99.9% случаев это более удобно, т.к. обычно всё-таки нет необходимости менять обработчик прерывания в рантайме. И более надёжно.

Иначе можно для какого-то одного прерывания сделать вспомогательный обработчик, который будет вызывать основной обработчик по указателю в ram.

Ну и если чешется, можно записать в VTOR адрес таблицы векторов в RAM.

qbe ()
Ответ на: комментарий от Eddy_Em

А я знаю несколько историй успеха проектов, которые были начаты на arduino, впоследствии от большой части библиотек оставившие только рожки да ножки. К примеру, мозги 3д-принтеров.

qbe ()

Если любишь обмазываться библиотеками, юзай hal. Я чтобы с усб работать именно его и юзал и именно он и взлетел корректно. А вот с spi (казалось бы что там вообще может пойти не так) я уже наебался, буду инициализировать и работать с ним как в старые добрые времена без всяких либ. Так как-то проще если честно. Достаточно 1 раз инициализацию написать и ты знаешь что она всегда работает, а байты не теряются.

anonymous ()
Ответ на: комментарий от qbe

Не надо про 3D принтеры! Я несколько открытых проектов 3D-принтеров и фрезеров полистал. Такой быдлокод надо еще поискать!

Eddy_Em ☆☆☆☆☆ ()
Ответ на: комментарий от Eddy_Em

Зато их код гибко настраиваемый и реально работает на разной кинематике. Вопреки тому, что разработчики, по твоим словам, не д'Артаньяны.

qbe ()
Последнее исправление: qbe (всего исправлений: 1)
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.