STM32 абстрагируемся от регистров CMSIS при настройке GPIO

    Как известно CMSIS предоставляет доступ к регистрам микроконтроллера. Это конечно хорошо, но не очень удобно. В данной статье речь пойдет о настройке GPIO. Порты ввода-вывода настраиваются довольно просто и если речь идет об одном - двух пинах, можно воспользоваться напрямую регистрами. Но если необходимо сконфигурировать несколько пинов, а тем более динамически менять конфигурацию ( это может потребоваться, например, для проверки подтянутости линий к плюсу при реализации работы с I2C, а потом для переключения на работу с аппаратным I2C ), то гораздо проще обернуть всю работу с регистрами в класс, и пользоваться методами типа setPin/resetPin.

    Так как работает класс с GPIO я решил его так и назвать. В нем имеется конструктор GPIO( GPIO_TypeDef *port ), который принимает ссылку на порт. Так же в классе есть методы :

    • void pinConf ( uint8_t pin_nomber, uint8_t pin_mode ); // режим работы пина

    • void setPin( uint8_t pin_nomber ); // установить 1

    • void resetPin( uint8_t pin_nomber ); // сбросить пин

    • int getPin ( uint8_t pin_nomber ); // считываем состояние пина (reg. IDR)

    Модуль класса состоит из двух файлов - gpio.h и gpio.cpp.

    GPIO.H

    #ifndef USER_LIB_GPIO_GPIO_H_
    #define USER_LIB_GPIO_GPIO_H_
    
    #include "stm32f103xb.h"
    
    //---------------inputs-------------------------------------------------
    #define INPUT_FLOATING 0x4 		// вход без подтяжки
    #define INPUT_PULL_UP 0x7F 		// с подтяжкой к питанию
    #define INPUT_PULL_DOWN 0xFF 	// с подтяжкой к "земле"
    #define INPUT_ANALOG 0x0 			// аналоговый вход
    
    //--------------outputs--------------------------------------------------
    #define OUTPUT_OPEN_DRAIN 0x7 // выход открытый сток
    #define OUTPUT_PUSH_PULL 0x3 	// выход тяни-толкай
    
    //--------------altarnate function---------------------------------------
    #define AF_PUSH_PULL 0xB 			// альтернативная ф-я с выходом тяни-толкай
    #define AF_OPEN_DRAIN 0xF 		// альтернативная функция с открытым стоком

    У выводов контроллера есть несколько режимов работы. Они задаются в регистрах GPIOx_CRL ( для выводов от 0 до 7 ) и GPIOx_CRH ( для выводов от 8 до 15 ). На каждый вывод в этих регистрах отводится по 4 бита, которые и задают режим работы. Чтобы не вспоминать каждый раз какие биты нужно прописать на каком месте для определенного режима, удобно переписать все возможные комбинации в макроопределения, что я и сделал. В последствии эти константы будут передаваться в методы для задания режима.

    class GPIO {
    
    	public:
    
    	GPIO( GPIO_TypeDef *port );
    	
    	void pinConf ( uint8_t pin_nomber, uint8_t pin_mode ); // режим работы пина
    	void setPin( uint8_t pin_nomber ); // установить 1 на пине
    	void resetPin( uint8_t pin_nomber ); // сбросить пин
    	int getPin ( uint8_t pin_nomber ); // считываем состояние пина (reg. IDR)
    
    	private:
    
    	GPIO_TypeDef *GPIOx;
    	int pin_m;
    
    
    };
    
    #endif /* USER_LIB_GPIO_GPIO_H_ */
    

    Далее по коду объявляем класс GPIO. Конструктор класса принимает ссылку на порт (GPIOA, GPIOB и т.д.) и сохраняет ее в приватной переменной GPIOx. Это нужно для того, чтобы при создании объекта и вызове его методов, методы знали с каким портом работать.

    После конструктора следуют публичные методы класса, образующие интерфейс.

    • pinConf ( uint8_t pin_nomber, uint8_t pin_mode ) принимает номер вывода порта и режим работы, который задается макроопределениями в начале файла.

    • setPin( uint8_t pin_nomber ) устанавливает вывод в 1. Принимает номер пина порта.

    • resetPin( uint8_t pin_nomber ) сбрасывает вывод. Принимает номер вывода.

    • getPin ( uint8_t pin_nomber ) возвращает состояние вывода порта (читает регистр IDR)

    GPIO.CPP

    #define INPUT_PULL_UP_DOWN 0x8 // используется для обоих вариантов
    #define GPIO_BITS_MASK 0xF // маска для стирания битов конфигурации
    
    GPIO::GPIO(GPIO_TypeDef *port){
    
    	this->GPIOx = port;
    // тактируем порт от шины APB1
    	if ( port == GPIOA){
    
    			RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    			return;
    	}
    
    	if ( port == GPIOB ){
    
    			RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
    			return;
    	}
    
    	if ( port == GPIOC ){
    			RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
    			return;
    	}
    
    	if ( port == GPIOD ){
    			RCC->APB2ENR |= RCC_APB2ENR_IOPDEN;
    			return;
    	}
    
    	if ( port == GPIOE ){
    			RCC->APB2ENR |= RCC_APB2ENR_IOPEEN;
    			return;
    
    	}
    
    return;
    
    }

    В конструкторе класса сохраняется ссылка на порт и тактируется выбранный порт.

    void GPIO::pinConf ( uint8_t pin_nomber, uint8_t pin_mode ){
    
    	this->pin_m = pin_mode; // для методов set/reset Pin если используется альтернативная ф-я
    	uint8_t offset; // смещение в регистре
    	uint8_t mode; 
    
    	// если вход с подтяжкой меняем pin_mode
    	if ( ( pin_mode == INPUT_PULL_UP ) || ( pin_mode == INPUT_PULL_DOWN ) ){
    
    		mode = INPUT_PULL_UP_DOWN;
    	}//if
    
    	if ( pin_nomber < 8 ){
    
    		offset = pin_nomber * 4;
    		this->GPIOx->CRL &= ~( GPIO_BITS_MASK << offset );
    		this->GPIOx->CRL |= ( mode << offset );
    	} // if
    	else if ( pin_nomber > 7 ){
    
    		offset = ( pin_nomber - 8 ) * 4;
    		this->GPIOx->CRH &= ~( GPIO_BITS_MASK << offset );
    		this->GPIOx->CRH |= ( mode << offset );
    
    	} // else
    
    	// если режим пулл-ап ставим бит пина в регистре ODR  в 1
    	if ( pin_mode == INPUT_PULL_UP ){
    
    		GPIOx->ODR |= ( 1 << pin_nomber );
    
    	}
    
    	/* доп. условие.  если режим задан INPUT_PULL_DOWN то сбрасываем бит пина в 0
    	 * нужно для исключения ситуации, когда сначала назначили режим INPUT_PULL_UP
    	 * а потом где-то в программе  переназначили режим INPUT_PULL_DOWN. В этом случае в
    	 * регистре ODR останется 1 и пин все равно будет работать как INPUT_PULL_UP
    	 */
    	if ( pin_mode == INPUT_PULL_DOWN ){
    
    		GPIOx->ODR &= ~( 1 << pin_nomber );
    
    	}
    return;
    } //pinConf
    

    В методе конфигурации сохраняем заданный режим в переменную pin_m. Это нужно для того, чтобы при использовании методов setPin()/resetPin() и заданном режиме альтернативной функции эти методы не управляли выводом, так как в данном случае управление должно осуществляться из модуля альтернативной функции.

    Далее проверяем задан ли режим входа с подтяжкой. Если задан то меняем переменную pin_mode так как для обоих режимов входа с подтяжкой биты пина конфигурируются одинаково, а выбор подтяжки к питанию или земле осуществляется записью в регистр ODR 1 или 0.

    В условии if ( pin_nomber < 8 ) и аналогичном if ( pin_nomber > 7 ) рассчитываем смещение относительно начала регистра CRL или CRH соответственно, учитывая, что на вывод отводится 4 бита, и кладем в переменную offset. Затем сначала затираем биты конфигурации маской GPIO BIT MASK, а затем записываем новые биты конфигурации вывода.

    if ( pin_mode == INPUT_PULL_UP ) - проверяем, если задан режим подтяжки к питанию, то выставляем в регистре ODR единицу. Аналогично проверяем режим подтяжки к земле и скидываем бит, если условие верно.

    void GPIO::setPin( uint8_t pin_nomber ){
    
    	// если пин сконфигурирован как альтернативная ф-я ничего не делаем
    	// т.к. управление пином должно быть из альтернативной ф-и
    	if ( ( this->pin_m == AF_PUSH_PULL) || ( this->pin_m == AF_OPEN_DRAIN ) ){
    
    		return;
    
    	}// if
    
    	this->GPIOx->BSRR = ( 1 << pin_nomber );
    return;
    }

    В методе setPin() сначала проверяем не задана ли альтернативная функция. Если да, то выходим ничего не делая. Далее в регистре BSSR устанавливаем 1 на соответствующий вывод.

    void GPIO::resetPin( uint8_t pin_nomber ){
    
    	// если пин сконфигурирован как альтернативная ф-я ничего не делаем
    	// т.к. управление пином должно быть из альтернативной ф-и
    	if ( ( this->pin_m == AF_PUSH_PULL) || ( this->pin_m == AF_OPEN_DRAIN ) ){
    
    		return;
    
    	}// if
    
    	this->GPIOx->BRR = ( 1 << pin_nomber );
    return;
    }

    В методе resetPin() все аналогично предыдущему, за исключением того, что пишем в регистр BRR тем самым скидывая вывод.

    int GPIO::getPin ( uint8_t pin_nomber ){
    
    	uint16_t mask;
    	mask = ( 1<< pin_nomber);
    
    	if ( (this->GPIOx->IDR) & mask) return 1;
    
    	else return 0;
    }

    В методе getPin() создаем маску для сравнения, сравниваем регистр IDR с маской. Если тру - возвращаем 1, иначе 0.

    Применяется все это дело так :

    GPIO *port = new GPIO( GPIOC ); // создаем экземпляр класса, передаем порт GPIOC
    port->pinConf( 13, OUTPUT_PUSH_PULL ); //задаем режим выход пуш-пул
    port->setPin (13); // установка вывода в 1
    port->resetPin (13); // сброс вывода
    int value;
    value = port->getPin (13); // считываем состояние вывода

    Вот собственно и все. Думаю в таком виде работать с портами ввода-вывода гораздо удобнее, чем напрямую с регистрами. Следует отметить, что данный класс осуществляет первичную конфигурацию портов ввода-вывода. Поэтому при работе с аппаратными интерфейсами или АЦП требуется конфигурация еще кучи регистров, обслуживающих эту периферию, но это следует делать в модуле конкретной периферии, наследовав данный класс для первичной конфигурации GPIO. Таким образом отделив яйца от плевел.

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 36

      0
      Но если необходимо сконфигурировать несколько пинов, а тем более динамически менять конфигурацию <...> то гораздо проще обернуть всю работу с регистрами в класс, и пользоваться методами типа setPin/resetPin.
      Было бы не плохо продемонстрировать удобство вашего способа. Ну, скажем, вывод на семисегментник, у которого сегменты разбросаны по разным портам в произвольном порядке.
          0
          И однотипная настройка\установка нескольких битов в порте вместо одного прохода потребует несколько: на каждый бит по отдельности. Плюс «switch-case» сейчас не в моде?
            0
            Смотря какова конечная задача. Тем более, что люди продолжают по привычке считать, что код с несколькими слоями трансляции вызовов будет таким же в результирующем бинаре. Т.е., про оптимизацию компилятором успешно забывают, а потом кидаются экскрементами в код, облегчающий конфигурацию. Зато стоически молчат о том, как тяжко копаться в их «быстром» и «однопроходном» коде.
              0
              Так потому и просим продемонстрировать настройку и выхлоп ассемблера на какой-то практической задаче. Насколько подход очередного слоя абстракции лучше или хуже других.
                0
                Ну, примеры по теме статьи может привести только автор. Я же хотел бы обратить внимание, что оптимизирующий компилятор успешно складывает цепочки вызываемых функций и пересчитывает все константные значения. Т.е., условно говоря, port->setPin(13); превращается в GPIOx->RegName = (GPIOx->RegName & 0xFFFFEFFFU) | 0x00001000U; самим компилятором, а потому бит меняется за минимум операций. А единовременное переключение группы пинов вероятно разве что в том случае, когда вообще весь алгоритм управления периферией может уложиться в логику конечного автомата. Только редко когда проектам на слабых контроллерах нужна подобная скорострельность.
                  0
                  Скажем, я пишу библиотеку для дисплея на hd44780, нужно объявить 4 подряд идущие ноги для линии данных, одну под RS и одну под E. Две последние для удобства разводки могут оказаться на разных портах.
                  На макросах я могу сделать так:
                  #define LCD_44780_DATA C, 6 //PC6, PC7, PC8, PC9
                  #define LCD_44780_RS B, 15
                  #define LCD_44780_E A, 8
                  #include "lcd_44780.h"

                  Да, в библиотеке будет немного шаманства чтобы развернуть 4 байта из структуры, предназначенной для хранения одного. Но это делается один раз. А подключается она потом к произвольным выводам, как видите, просто.
                    0
                    Но сразу ломается, когда изменяется логика визуализации. Тут, внезапно, оказывается, что каждый пиксель — сам по себе. Я же не говорю, что плохо оптимизировать код под конкретную задачу, но человек-то размышлял об удобстве кода, а не решения той самой конкретной задачи. Это — из категории размышлений вида: «контроллер — это компьютер?».

                    Я конце 90-ых, имея под рукой IBM AT-286 с EGA-адаптером, для достижения скорострельности полез в системное программирование и стал управлять буфером кадров напрямую. Так там порядок байт бит-масок пикселей в буфере отличался от процессорного. К тому же, один байт — горизонтальная строка, длиной с ширину символьного сегмента (8x16: 80x25 для VGA и 8x14: 80x25 для EGA). У EGA-режима памяти хватало на два независимых кадра, а VGA — на 1,5 — соответственно один постоянно видимый.

                    Для отрисовки примитивов (это ещё никакого антиалиасинга) приходилось пересчитывать сдвиги битовых масок и накладывать их через смещения по коэффициенту. Но и то API выглядел примерно так:

                    graph->drowPix(x, y);
                    graph->drowLine(x1, y1, x2, y2);
                    graph->drowSquare(x1, y1, r);

                    И т.д.

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

                    Но то время ушло. Теперь даже жалкая AtMega тактуется выше, чем тогда процессор (8-12 МГц было). Теперь стало глупо заниматься побайтовой оптимизацией, хотя искренне жаль…
                      0
                      А какое отношение это имеет к назначению портов?
                      То есть внутри того же «lcd_44780.h» имеет смысл реализовать стандартные функции вывода текста (графику-то он не поддерживает). С этим никто не спорит. Но при разводке платы бывает удобно поменять порты, к которым он подключен. Не лезть же каждый раз внутрь библиотеки! И абстракции вокруг портов должны решать именно эту задачу, а вовсе не лезть во внутренности библиотек.
                        0
                        «И однотипная настройка\установка нескольких битов в порте вместо одного прохода потребует несколько: на каждый бит по отдельности. Плюс «switch-case» сейчас не в моде?»
                        Вся ветка обсуждения тянется от этого поста.
                          0
                          И? Вас постоянно куда-то в сторону уводит.
                          Да, слои абстракции могут не добавлять тормозов, но это надо показать.
            +1
            абстракция она на то и абстракция, чтобы уйти от конкретики. В Вашем случае абстракция получилась над одним, уже теряющем популярность (ибо по практически той же цене и в том же форм-факторе доступна stm32f411/f401), stm32f103.

            опять же, как часто Вам приходилось в устройстве с МК менять настройки пинов динамически? Я даже сходу пример-то не могу такой придумать. Максимум что приходилось делать, это переводить в состояние, при котором потребление будет минимальным. Опять же, однократно перед сном. Получается что динамика в данном случае избыточна. Шаблоны тема сложная, но интересная.

            Вот собственно и все. Думаю в таком виде работать с портами ввода-вывода гораздо удобнее, чем напрямую с регистрами.

            Относительно. Когда говорят «пишу на регистрах» не стоит считать что по всему коду разбросы строки вида:
             GPIOC->BSRR = 1 << pin; 

            Это все так же оборачивается в функции. Поэтому и спорное утверждение об удобстве.
            Мой вариант, правда для nrf52840, но суть та же. Настройка пина. При конфигурации происходит сборка по ИЛИ допустимых настроек.
            
              Gpio<port1, 3> enable_;
              enable_.configurate(output | noPull);
              enable_.set(); // разрешаем питание на драйвер светодиода
            


            вот что порадовало, так это использование регистров «BRR» и «BSRR».

              0
              nomber? это от какого слова?
                0
                mode = Sarcasm;
                можно предположить что от транскрипции [ˈnʌmbə®], достаточно «близко» )
                0
                Пожалуйста, почитайте комментарии вот к этой статье
                habr.com/ru/company/ruvds/blog/529832
                  0
                  абстрагируемся от регистров CMSIS при
                  Погодите, я правильно понял? Вы уже имеющийся слой абстракции (CMSIS) обернули еще одним слоем абстракции (вашим классом)?
                    0
                    Абстракция это хорошо, когда в ней есть нужда.
                    Наверное не совру, что каждый, кто пишет прошивки, так же пишет небольшие «абстракции», чтобы сделать гибкую настройку некоторых пинов, их прерываний и т.д., чтобы далее, в процессе добавления в код прошивки нужных фич, не терять время на переконфигурирование пинов/таймеров/ADC/DMA и т.д. Для каждого проекта, эти «абстракции» свои, своего рода маленький «велосипед». Если этот маленький «велосипед» для текущего проекта выполняет свои задачи, то ок.
                    У вас «велосипед» работает — это хорошо, но, в других проектах он будет не нужен (ИМХО).
                    У меня тоже есть свои «велосипеды», можете посмотреть в коде на гите (не по STM).
                    Я к чему, таких статей можно нагенерить множество (по каждому проекту, свои «велосипеды»), и, кстати ИМХО — это будет своего рода база знаний, что можно сделать при написании кода.

                    Я понимаю, что выражение «велосипед» выглядит в данном контексте не очень, хотел писать «фича», но решил оставить так.

                    Автору: плюс в карму, ноль за статью.
                      0
                      Я к чему, таких статей можно нагенерить множество (по каждому проекту, свои «велосипеды»), и, кстати ИМХО — это будет своего рода база знаний, что можно сделать при написании кода.

                      Идея интересная. Задумался над тем, чтобы показать свой «шаблонный» велосипед над freeRTOS и стеком BLE. Но терзают определенные сомнения ибо «вроде работает, если вот и тут не трогать» )

                      Для каждого проекта, эти «абстракции» свои, своего рода маленький «велосипед». Если этот маленький «велосипед» для текущего проекта выполняет свои задачи, то ок.

                      Да, но у меня достаточно часто при переносе велосипеда из проекта в проект он обрастает новыми деталями. Потому что предусмотреть все и сразу не возможно. Обязательно что-нибудь не обычное вылезет.

                      на тему развития велосипедов

                        +1
                        Да, вот та же мысль — бесполезно писать статью вида «а тут я обернул вот так». Потому что обернуть можно тысячей разных способов.

                        Интересные статьи — про нетрадиционные методы «шоколадного обертывания», вроде тех, что пишет lamerok, либо напротив — критику этих методов (которая неоднократно была к комментариях, но так и не оформилась в виде статьи). А как в 100500-й раз обернуть что-то в класс…
                          0
                          lamerok, ага, его статьи зачитаны до дыр. Более того, по сути эти статьи и сподвигли меня на переход в с++ при разработке проектов.

                          Потому что обернуть можно тысячей разных способов.

                          Согласен, но не все одинаково удобны. у меня аллергия на динамическую работу с памятью в МК, поэтому делаю все по максимуму в статике. Счастье было, когда удалось статический полиморфизм (CRTP) реализовать у себя. Ну по крайней мере мне так кажется что удалось)
                            0
                            Ну, аллергии на динамическую память быть не должно вовсе. Аллергия должна быть на ее освобождение, а вот выделение оной — зачастую единственный возможный способ красиво решить задачу.

                            Есесна запрет на освобождение рождает много демонов — например работа со строками становится «как в Си» (что не есть плохо, просто не идиоматично). Но что поделать.
                              0
                              динамическая память в Qt, для GUI приложения. Там во всю пользуюсь, а тут… с учетом того что SEGGER не понятно когда, где и как создает кучу, как-то не особо приятно.
                                0
                                Segger? А почему он кучу создает? Впрочем неважно, куча всегда создается в одном и том же месте, ваша задача — корректно рассчитать ее параметры, и отработать все сценарии выделения памяти. Повторюсь, иногда никак без выделения, вот освобождать — табу. Поэтому динамика в полном смысле слова невозможна/нежелательна. Ну а в Qt вы не то что вовсю пользуетесь, вы там ей просто _не можете_ не пользоваться ))
                                  0
                                  вот и мне было интересно почему он. В настройках проекта указал, что размер heap = 0. Но тем не менее при переопределении виртуальной функции в памяти образовался регион, дословно не помню, но сочетание seggerHeap там было. Искать концы было лень, так как это была «песочница» для обкатки шаблона.
                                    0
                                    Мммм, загадка… Интересно было бы найти концы, потому что в целом это забота лишь компоновщика, и ничья более. Я только не понял, вы исполняли код только в отладчике, или на железе тоже?
                                      0
                                      код запускался во встроенном симуляторе. В настройках проекта:
                                      Heap Size = 0;
                                      Main Stack Size = 2048;
                                      Process Stack Size = 0;

                                      Linker = SEGGER;
                                      Ну вот например, тренировался на этом шаблоне:
                                      class Singleton {
                                      public:
                                        static Singleton* instance() { return inst_; }
                                        void run()  { }
                                      
                                      private:
                                        Singleton() {}
                                        static Singleton* inst_;
                                      };
                                      Singleton* Singleton::inst_ =  new Singleton();
                                      


                                      Расход памяти можно посмотреть тут:


                                      То есть формально куча не создана, но указатель на нее у segger'а есть. Они относительно недавно, в этом году, меняли и линкера, и компилятора своего. Из-за этого перестали собираться проекты, созданные в ранних версиях. После выхода патча стало легче, но старые проекты восстанавливать все равно пришлось
                                        0
                                        Ах ты ж елки, я не понял, что вы делали проект в Segger Embedded Studio. И крайне удивлен, что там есть свой линкер, думал на ld сделано.
                        –2
                        Настройка портов на препроцессоре (http://we.easyelectronics.ru/blog/STM32/3191.html)
                        Настройка одного пина:
                        GPIO_INIT_PIN(GPIOA,3,GPIO_MODE_INPUT_PULL_DOWN);
                        

                        Настройка нескольких пинов:
                        GPIO_MULTI_INIT(GPIOA,BIN(1010101010101010),GPIO_MODE_OUTPUT50_OPEN_DRAIN_DOWN);
                          0
                          можно пояснить, минус за что?!
                            0
                            .
                            +1

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

                              0
                              а чего хочется увидеть, хоть примерно? Без сарказма вопрос.
                                0
                                Чего-то более оригинального, сложного и необычного.
                                Подробного описания периферии, принципов ее работы, эмуляцию и запуск на аппаратном модуле.
                                Красивого законченного устройства.
                                Полноценной библиотеки, которая бы действительно делала взаимодействие с контроллером более удобным.
                                  0

                                  Товарищ COKPOWEHEU прав. Мне ещё было бы интересно посмотреть на красивое решение на тех же плюсах какой-нибудь нудной проблемы(или проекта), которое добавило бы в копилку "за высокие абстракции".

                                0
                                у меня из нудных проблем, это временная синхронизация BLE устройств. При чем особо не решенная. Но вряд-ли кто-нибудь решит поделится своим решением в данной области.

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

                                Могу рассказать про свой опыт работы с nrf52840. Достаточно интересная периферия с возможностью каскадирования внутри чипа через модуль PPI (Programmable peripheral interconnect — программируемое подключение периферии). Вроде занятная штука, но интересно ли будет?
                                  0
                                  В продолжение статьи требуем сравнение полученного ассемблерного кода в авторской интерпретации с аналогом, например, на макросах.

                                  Only users with full accounts can post comments. Log in, please.