Продолжаем изучать возможности контроллера ESP8266 и учиться пользоваться данными возможностями в своём коде.
На данном занятии мы познакомимся с интерфейсом SPI, который поддерживается аппаратно в микроконтроллере ESP8266
С данным интерфейсом мы очень неплохо знакомы, так как мы его постоянно используем при написании кода для других контроллеров, поэтому, дабы не тратить драгоценное время, мы не будем останавливаться на изучении передачи данных по шине и разновидностях режимов этой самой передачи.
В ESP8266 есть два модуля SPI.
Первый называется просто General SPI (SPI общего назначения)
Данный интерфейс может работать максимально со скоростью до 80 мегагерц, в режиме ведомого — 20 мегагерц.
Второй модуль называется HSPI
Хотя в скобках и стоит приписка Slave, данный модуль прекрасно справляется и с работой в режиме MASTER (ведомого), в чём мы на данном уроке убедимся. Здесь максимальная скорость работы — 20 мегагерц.
Использовать мы будем именно второй модуль HSPI, так как на первом практически всегда уже распаян FLASH, на котором находится прошивка, хотя мы можем использовать и его, также можем использовать свободное место во FLASH-памяти для своих нужд.
Регистры модулей SPI, а также их биты не совсем подробно описаны в технической документации на контроллер, как мы привыкли видеть у других контроллеров. Поэтому пришлось воспользоваться некоторыми другими ресурсами для изучения работы с ними (сторонние проекты, заголовочные и файлы исходных кодов SDK). Работать с модулем SPI, как, впрочем, и с другой периферией, можно, используя возможности готового функционала SDK, так и прямого доступа к регистрам. Сегодня мы в основном поработаем по 2 варианту, так виднее процесс.
Работать на данном уроке с шиной SPI мы будем в режиме MASTER, так как он гораздо проще в программировании, с режимом SLAVE мы познакомимся в будущих уроках. Также для ещё большей простоты программирования мы будем работать с шиной SPI только в режиме передачи. В качестве ведомого устройства, подключаемого по шине, мы возьмём восьмиразрядный семисегментный индикатор, динамическая индикация которого реализована на микросхеме-драйвере MAX7219. Данная микросхема общается с контроллером именно по шине SPI. Мы неоднократно уже работали с данной микросхемой с использованием других контроллеров, поэтому весь процесс работы с ней нам знаком.
К каким ножкам подключать индикатор, мы видим в таблице выше. Контакт MISO мы не используем, так как читать нам из индикатора нечего.
На нашей плате ножки, которые мы будем использовать, будут следующие:
MOSI — D7
CLK — D5
CS — D8
Также для лучшего мониторинга передачи данных по шине мы подключим к данным ножкам логический анализатор.
И теперь наша схема будет выглядеть следующим образом
Проект мы сделаем из проекта урока 10 с именем I2C_LCD2004 и назовём его LED7219.
Проект урока 10 был взят за основу потому, что в нашем новом проекте тоже потребуется два дополнительных модуля и поэтому нам легче будет подправить файл сборки Makefile.
Произведём в папке с проектом следующие переименования файлов:
i2с_user.h -> spi_user.h
i2с_user.c -> spi_user.c
lcd.h -> max7219.h
lcd.c -> max7219.c
Откроем наш проект в Eclipse и точно такие же переименования внесём и в Makefile в местах, где встретятся аналогичные файлы.
Можно было бы, конечно создать проект с нуля, но, как мне кажется, это было бы ещё дольше, так как пришлось бы ещё настраивать свойства проекта, и заново писать Makefile. Впрочем, решать вам, как вам легче.
В файлах main.c и max7219.c изменим также имя подключаемой библиотеки
#include "max7219.h"
В файлах max7219.c и spi_user.c изменим также имя другой подключаемой библиотеки
#include "spi_user.h"
Теперь у нас проект скорее всего соберётся.
В файлах max7219.c и spi_user.c также удалим весь полностью код кроме подключения библиотек.
А в файлах max7219.h и spi_user.h также удалим вообще весь код и наполним их пока следующим содержимым
1 2 3 4 5 6 |
#ifndef MAX7219_H_ #define MAX7219_H_ //------------------------------------------------ #include "osapi.h" //------------------------------------------------ #endif /* MAX7219_H_ */ |
1 2 3 4 5 6 |
#ifndef SPI_USER_H_ #define SPI_USER_H_ //------------------------------------------------ #include "osapi.h" //------------------------------------------------ #endif /* SPI_USER_H_ */ |
В файле main.c удалим подключение библиотеки
#include «driver/i2c_master.h»
В функции user_init удалим объявление символьного массива.
Также удалим вот этот код
i2c_master_gpio_init();
I2C_MASTER_SDA_LOW_SCL_LOW();
LCD_ini();
ets_delay_us(100000);
LCD_String(«String 1»);
LCD_SetPos(3,1);
LCD_String(«String 2»);
LCD_SetPos(6,2);
LCD_String(«String 3»);
LCD_SetPos(9,3);
LCD_String(«String 4»);
ets_delay_us(1000000);
system_soft_wdt_feed();
ets_delay_us(1000000);
system_soft_wdt_feed();
LCD_SetPos(9,3);
LCD_String(» «);
Из бесконечного цикла также пока удалим весь код, кроме задержки и сброса сторожевого таймера, чтобы у нас контроллер не перезагружался то и дело
1 2 3 4 5 |
while(1) { ets_delay_us(100000); system_soft_wdt_feed(); } |
Подключим заголовочный файл для SPI, а также наш пользовательский заголовочный файл
1 2 3 |
#include "gpio.h" #include "driver/spi.h" #include "spi_user.h" |
В файле spi_user.c добавим функцию инициализации SPI
1 2 3 4 5 6 |
#include "spi_user.h" //------------------------------------------------ void spi_init(uint8 spi_no) { } //------------------------------------------------ |
В одноимённом заголовочном файле создадим для данной функции прототип и вызовем её в функции user_init файла main.c
1 2 |
gpio_init(); spi_init(HSPI); |
Вернёмся в файл spi_user.c и функции инициализации SPI выйдем из неё, если мы случайно выбрали несуществующий модуль
1 2 3 |
void spi_init(uint8 spi_no) { if(spi_no > 1) return; //Only SPI and HSPI are valid spi modules. |
В файле spi_user.h подключим библиотеку для SPI, а также добавим макросы для делителей
1 2 3 4 5 6 7 |
#include "osapi.h" #include "driver/spi.h" //------------------------------------------------ #define SPI_CLK_USE_DIV 0 #define SPI_CLK_PREDIV 10 #define SPI_CLK_CNTDIV 2 //------------------------------------------------ |
Вернёмся в файл spi_user.c и в функции spi_init в случае использования делителя проинициализируем также свою переменную для делителя
1 2 3 4 5 6 |
if(spi_no > 1) return; //Only SPI and HSPI are valid spi modules. uint32 clock_div_flag = 0; if(SPI_CLK_USE_DIV) { clock_div_flag = 0x0001; } |
Произведём также настройку ножек GPIO, используемых в SPI
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
clock_div_flag = 0x0001; } //GPIO Init if(spi_no==SPI){ WRITE_PERI_REG(PERIPHS_IO_MUX, 0x005|(clock_div_flag<<8)); //Set bit 8 if 80MHz sysclock required PIN_FUNC_SELECT(PERIPHS_IO_MUX_SD_CLK_U, 1); PIN_FUNC_SELECT(PERIPHS_IO_MUX_SD_CMD_U, 1); PIN_FUNC_SELECT(PERIPHS_IO_MUX_SD_DATA0_U, 1); PIN_FUNC_SELECT(PERIPHS_IO_MUX_SD_DATA1_U, 1); }else if(spi_no==HSPI){ WRITE_PERI_REG(PERIPHS_IO_MUX, 0x105|(clock_div_flag<<9)); //Set bit 9 if 80MHz sysclock required PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTDI_U, 2); //GPIO12 is HSPI MISO PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTCK_U, 2); //GPIO13 is HSPI MOSI PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTMS_U, 2); //GPIO14 is HSPI CLK PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTDO_U, 2); //GPIO15 is HSPI CS } |
Как мы видим, наша функция универсальная и мы можем её использовать также и для инициализации первого модуля.
Настроим тактирование модуля
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTDO_U, 2); //GPIO15 is HSPI CS } //SPI Clock Init if((SPI_CLK_PREDIV==0)|(SPI_CLK_CNTDIV==0)) { WRITE_PERI_REG(SPI_CLOCK(spi_no), SPI_CLK_EQU_SYSCLK); } else { WRITE_PERI_REG(SPI_CLOCK(spi_no), (((SPI_CLK_PREDIV-1)&SPI_CLKDIV_PRE)<<SPI_CLKDIV_PRE_S)| (((SPI_CLK_CNTDIV-1)&SPI_CLKCNT_N)<<SPI_CLKCNT_N_S)| (((SPI_CLK_CNTDIV>>1)&SPI_CLKCNT_H)<<SPI_CLKCNT_H_S)| ((0&SPI_CLKCNT_L)<<SPI_CLKCNT_L_S)); } |
Вообщем, здесь происходит занесение значений различных делителей в соответствующие биты регистра SPI_CLOCK соответствующего модуля.
Вкратце битовые поля данного регистра описаны в technical reference на контроллер почти в самом конце документации
Настроим также порядок следования байтов от старшего к младшего для буферов чтения и записи
1 2 3 4 5 6 |
((0&SPI_CLKCNT_L)<<SPI_CLKCNT_L_S)); } //SPI TX Byte order High to Low SET_PERI_REG_MASK(SPI_USER(spi_no), SPI_WR_BYTE_ORDER); //SPI RX Byte order High to Low SET_PERI_REG_MASK(SPI_USER(spi_no), SPI_RD_BYTE_ORDER); |
Здесь уже используется регистр SPI_USER, краткое описание которого есть там же
Установим режим использования ножки выбора
1 2 3 |
SET_PERI_REG_MASK(SPI_USER(spi_no), SPI_RD_BYTE_ORDER); //Set CS Mode SET_PERI_REG_MASK(SPI_USER(spi_no), SPI_CS_SETUP|SPI_CS_HOLD); |
Кстати, про данные биты регистра в документации умалчивается. Остаётся только догадываться, что скорее всего SPI_CS_SETUP — это бит установки аппаратного управления ножкой CS, а — SPI_CS_HOLD — это включение режима удержания ножки при приёме, когда шина занята, хотя это тоже не факт, обычно такие задачи возлагаются на ведомое устройство.
Далее отключаем режим FLASH сбросом соответствующего бита в том же регистре, который также не обозначен в документации
1 2 3 |
SET_PERI_REG_MASK(SPI_USER(spi_no), SPI_CS_SETUP|SPI_CS_HOLD); //Disable Flash Mode CLEAR_PERI_REG_MASK(SPI_USER(spi_no), SPI_FLASH_MODE); |
Вот и вся инициализация.
Теперь нам нужно будет написать функцию отправки в шину 16-битного значения, так как именно с таким значением оптимальнее всего работать с драйвером индикатора. Такой функции готовой в библиотеке нет, поэтому будем писать.
Добавим данную функцию, в которой пока также выйдем при получении в параметре неправильного номера модуля
1 2 3 4 5 6 |
//------------------------------------------------ void spi_send16(uint8 spi_no, uint16 data) { if(spi_no > 1) return; } //------------------------------------------------ |
В файле spi_user.h добавим макрос проверки занятости SPI, а заодно и прототип на нашу функцию передачи
1 2 3 4 |
#define SPI_CLK_CNTDIV 2 //------------------------------------------------ #define spi_busy(spi_no) READ_PERI_REG(SPI_CMD(spi_no))&SPI_USR //------------------------------------------------ |
Здесь мы уже обращаемся к регистру SPI_CMD, проверяя в нём бит SPI_USR.
Вот описание данного регистра, которое состоит из описания только одного бита
Как видим, сбрасывается данный бит аппаратно, за этим мы и будем следить, чтобы не обращаться лишний раз к периферии, когда она занята.
Причем в заголовочном фале находится также макрос только для этого бита.
Мне удалось найти некоторую информацию по другим битам данного регистра, а также по битам других регистров и не только периферии SPI в файле esp8266_peri.h из библиотеки для Arduino. Вот макросы некоторых других битов данного регистра
//SPI CMD
#define SPICMDREAD (1 << 31) //SPI_FLASH_READ
#define SPICMDWREN (1 << 30) //SPI_FLASH_WREN
#define SPICMDWRDI (1 << 29) //SPI_FLASH_WRDI
#define SPICMDRDID (1 << 28) //SPI_FLASH_RDID
#define SPICMDRDSR (1 << 27) //SPI_FLASH_RDSR
#define SPICMDWRSR (1 << 26) //SPI_FLASH_WRSR
#define SPICMDPP (1 << 25) //SPI_FLASH_PP
#define SPICMDSE (1 << 24) //SPI_FLASH_SE
#define SPICMDBE (1 << 23) //SPI_FLASH_BE
#define SPICMDCE (1 << 22) //SPI_FLASH_CE
#define SPICMDDP (1 << 21) //SPI_FLASH_DP
#define SPICMDRES (1 << 20) //SPI_FLASH_RES
#define SPICMDHPM (1 << 19) //SPI_FLASH_HPM
#define SPICMDUSR (1 << 18) //SPI_FLASH_USR
#define SPIBUSY (1 << 18) //SPI_USR
Вернёмся в файл spi_user.c подождём, пока шина будет свободна
1 2 3 |
if(spi_no > 1) return; //wait for SPI to be ready while(spi_busy(spi_no)) ; |
Отключим пока не требующиеся режимы и использование ножек сбросом соответствующих битов в регистре SPI_USER
1 2 3 |
while(spi_busy(spi_no)) ; //disable MOSI, MISO, ADDR, COMMAND, DUMMY CLEAR_PERI_REG_MASK(SPI_USER(spi_no), SPI_USR_MOSI|SPI_USR_MISO|SPI_USR_COMMAND|SPI_USR_ADDR|SPI_USR_DUMMY); |
Установим требуемую длину посылки в битах
1 2 3 4 5 6 |
CLEAR_PERI_REG_MASK(SPI_USER(spi_no), SPI_USR_MOSI|SPI_USR_MISO|SPI_USR_COMMAND|SPI_USR_ADDR|SPI_USR_DUMMY); //Set bitlengths WRITE_PERI_REG(SPI_USER1(spi_no), SPI_USR_ADDR_BITLEN<<SPI_USR_ADDR_BITLEN_S | //Number of bits in Address (15&SPI_USR_MOSI_BITLEN)<<SPI_USR_MOSI_BITLEN_S | //Number of bits to Send SPI_USR_MISO_BITLEN<<SPI_USR_MISO_BITLEN_S | //Number of bits to receive SPI_USR_DUMMY_CYCLELEN<<SPI_USR_DUMMY_CYCLELEN_S); //Number of Dummy bits to insert |
Цифра 15 у нас используется, потому что счёт количества битов в посылке идёт от нуля, то есть bit_length — 1.
Включим ножку MOSI
1 2 3 |
SPI_USR_DUMMY_CYCLELEN<<SPI_USR_DUMMY_CYCLELEN_S); //Number of Dummy bits to insert //Enable MOSI function in SPI module SET_PERI_REG_MASK(SPI_USER(spi_no), SPI_USR_MOSI); |
Далее мы нам нужно будет занести в буфер передачи данные для их последующей передачи в шину.
Вообще передавать по шине SPI с помощью нашего контроллера ESP8266 мы можем до 64 байт (512 бит). То есть буфер содержит 64 байта и разделен на 16 регистров по 4 байта. Как правило, первые 8 регистров (SPI_W0-SPI_W7) используются для передачи, а следующие (SPI_W8-SPI_W15) — для приёма данных.
Подготовим наши байты, загрузив их в самый первый регистр буфера (в старшую его часть, так как передача и приём начинается со старшего байта регистров буфера)
1 2 3 |
SET_PERI_REG_MASK(SPI_USER(spi_no), SPI_USR_MOSI); //Copy data to W0 WRITE_PERI_REG(SPI_W0(spi_no), (uint32)data<<16); |
И напоследок дадим команду передачи
1 2 3 |
WRITE_PERI_REG(SPI_W0(spi_no), (uint32)data<<16); //Begin SPI Transaction SET_PERI_REG_MASK(SPI_CMD(spi_no), SPI_USR); |
Вообщем, в принципе, драйвер шины SPI для нашего сегодняшнего урока мы написали.
Теперь осталось написать драйвер микросхемы, но это не проблема, так как мы с ней уже очень много работали.
Добавим в файле max7219 глобальную переменную, которая будет хранить количество используемых разрядов индикатора, проинициализировав её сразу
1 2 3 |
#include "spi_user.h" //------------------------------------------------ char dg=8; |
Далее добавим функцию передачи в микросхему регистра и данных, объединив их в одно полуслово
1 2 3 4 5 6 7 |
char dg=8; //------------------------------------------------ void Send_7219 (uint8_t rg, uint8_t dt) { spi_send16(HSPI, (uint16_t)rg<<8 | dt); } //------------------------------------------------ |
Добавим функцию очистки индикатора
1 2 3 4 5 6 7 8 9 10 |
//------------------------------------------------ void Clear_7219 (void) { uint8_t i=dg; do { Send_7219(i,0xF);//символ пустоты } while (--i); } //------------------------------------------------ |
Далее добавим функции отображения на индикаторе разнообразных цифровых данных
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
//------------------------------------------------ void Number_7219 (volatile long n) { uint8_t ng=0;//переменная для минуса if(n<0) { ng=1; n*=-1; } uint8_t i=0; do { Send_7219(++i,n%10);//символ цифры n/=10; } while(n); if(ng) { Send_7219(i+1,0x0A);//символ - } } //------------------------------------------------ void NumberL_7219 (volatile int n) //пишем в левую тетраду { uint8_t ng=0;//переменная для минуса if(n<0) { ng=1; n*=-1; } if(n<1000) Send_7219(8,0xF);//символ пустоты if(n<100) Send_7219(7,0xF);//символ пустоты if(n<10) Send_7219(6,0xF);//символ пустоты uint8_t i=4; do { Send_7219(++i,n%10);//символ цифры n/=10; } while(n); if(ng) { Send_7219(i+1,0x0A);//символ - } } //------------------------------------------------------- void NumberR_7219 (volatile int n) { uint8_t ng=0;//переменная для минуса if(n<0) { ng=1; n*=-1; } if(n<1000) Send_7219(4,0xF);//символ пустоты if(n<100) Send_7219(3,0xF);//символ пустоты if(n<10) Send_7219(2,0xF);//символ пустоты uint8_t i=0; do { Send_7219(++i,n%10);//символ цифры n/=10; } while(n); if(ng) { Send_7219(i+1,0x0A);//символ - } } //------------------------------------------------------- void NumberF_7219 (float f) //пишем в левую тетраду c десятичной цифрой { int n = (int)(f * 10); uint8_t ng=0;//переменная для минуса if(n<0) { ng=1; n*=-1; } int m = n; uint8_t i=0; do { if(i==1) Send_7219(++i,(n%10) | 0x80);//символ цифры и точка else Send_7219(++i,n%10);//символ цифры n/=10; } while(n); if(ng) { if(m<10) { Send_7219(i+1,0x80);//ноль с точкой Send_7219(i+2,0x0A);//символ - } else Send_7219(i+1,0x0A);//символ - } else { if((m<10)&&(m!=0)) { Send_7219(i+1,0x80);//ноль с точкой } } } //------------------------------------------------------- void NumberLF_7219 (float f) //пишем в левую тетраду c десятичной цифрой { int n = (int)(f * 10); uint8_t ng=0;//переменная для минуса if(n<0) { ng=1; n*=-1; } int m = n; uint8_t i=4; do { if(i==5) Send_7219(++i,(n%10) | 0x80);//символ цифры и точка else Send_7219(++i,n%10);//символ цифры n/=10; } while(n); if(ng) { if(m<10) { Send_7219(i+1,0x80);//ноль с точкой Send_7219(i+2,0x0A);//символ - } else Send_7219(i+1,0x0A);//символ - } else { if((m<10)&&(m!=0)) { Send_7219(i+1,0x80);//ноль с точкой } } } //------------------------------------------------ |
Все эти функции нами давно написаны и в объяснении не нуждаются. Тем не менее они снабжены комментариями, гласящими о том что в данный момент происходит в коде.
Осталось добавить только функцию инициализации нашего индикатора, которая также нами написана давно и также в течение времени совершенствуется
1 2 3 4 5 6 7 8 9 10 11 |
//------------------------------------------------ void Init_7219 (void) { Send_7219(0x0F,0x00);//отключим режим тестирования Send_7219(0x09,0xFF);//включим режим декодирования Send_7219(0x0B,dg-1);//кол-во используемых разрядов Send_7219(0x0A,0x06);//интенсивность свечения Send_7219(0x0C,0x01);//включим индикатор Clear_7219(); } //------------------------------------------------ |
В заголовочном файле max7219.h добавим прототипы на некоторые требуемые извне функции
1 2 3 4 5 6 7 8 9 10 11 |
#include "osapi.h" //------------------------------------------------ void Send_7219 (uint8_t rg, uint8_t dt); void Clear_7219 (void); void Number_7219 (volatile long n); void Init_7219 (void); void NumberL_7219 (volatile int n); void NumberF_7219 (float f); void NumberR_7219 (volatile int n); void NumberLF_7219 (float f); //------------------------------------------------ |
Вернёмся в main.c и в функции user_init вызовем функцию инициализации индикатора и попробуем вывести на него большое число
1 2 3 |
spi_init(HSPI); Init_7219(); Number_7219(87654321); |
Соберём код, прошьём контроллер и, если у нас всё правильно, наше число должно будет отобразиться на индикаторе
Подождём пару секунд и очистим индикатор
1 2 3 4 5 6 |
Number_7219(87654321); ets_delay_us(1000000); system_soft_wdt_feed(); ets_delay_us(1000000); system_soft_wdt_feed(); Clear_7219(); |
Индикатор через 2 секунды теперь очистится, то есть сегменты во всех его разрядах перестанут светиться
В бесконечном цикле добавим тест, в результате которого примерно 10 раз в секунду в правой половине индикатора будут увеличиваться выводимые числа от 0 до 9999, а в правой уменьшаться, задержка у нас уже есть
1 2 3 4 5 6 |
while(1) { i++; if(i>9999) i=0; NumberR_7219(i); NumberL_7219(9999-i); |
Посмотрим, как работает наш индикатор теперь, собрав код и прошив контроллер
Давайте также посмотрим, как передаются данные в программе логического анализа. Каналы анализатора настроим следующим образом
Хотя в коде мы используем для передачи 16-разрядный режим, но здесь настроим 8-разрядный, так лучше видно передачу регистров и данных.
Вот так выглядит процесс передачи данных
Как видим, передача регистра и данных для занесения в него проходит непрерывно.
Итак, на данном уроке мы начали знакомиться с работой модуля SPI в контроллере ESP8266, пока используя его только для передачи данных в режиме MASTER. Также мы не использовали другие режимы полярности и фазы. В дальнейшем мы обязательно познакомимся с работой SPI в ESP8266 и на приём, а также и в режиме ведомого (SLAVE).
Всем спасибо за внимание!
Предыдущий урок Программирование МК ESP8266 Следующий урок
Модуль ESP NodeMCU можно купить здесь: Модуль ESP NodeMCU
Различные модули ЕSP8266 можно приобрести здесь Модули ЕSP8266
Индикатор светодиодный восьмиразрядный с драйвером MAX7219
Логический анализатор 16 каналов можно приобрести здесь
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
[…] Предыдущий урок Программирование МК ESP8266 Следующий урок […]