Maple Mini компактная 40 пиновая отладочная плата с установленным на ней процессором STM32F103CBT6. На плате минимум обвязки, однако есть один светодиод и пользовательская кнопка, которую можно использовать для работы программы. Поскольку у контроллера есть целых два интерфейса I2C, то есть возможность подключить дисплей на основе контроллера SSD1306 с интерфейсом I2C.
Дисплей планируется для вывода отладочной информации, т.е. в первую очередь используется режим вывода текстовой информации. Видео как это работает. Для программирования и отладки использовалась плата STM32F3Discovery Конкретный дисплей имеет интерфейс I2C. Четыре вывода. VCC - 3.3 вольта. GND, SDA, SСL.
У STM32F103CBT6 есть два интерфейса I2C. Нам понадобится один, подключенный на ноги PB7, PB6 (см. документацию на STM32F103CBT6)
D15 |
PB7 |
|
1_SDA |
D16 |
PB6 |
|
2_SCL |
Адрес дисплея 0x78 по умолчанию задан перемычкой. Дисплей 128x64 точки. Внутренняя память небольшая, и вся отражена на экран, т.е. при записи байта в память происходит мгновенная засветка соответствующих бит на дисплее. Каждый переданный байт управляет 8 вертикальными точками колонке. 128 колонок в 8 строках. Для формирования на экране символа 8x8 необходимо установить командой начальную колонку, а затем передать 8 байт.
Для обмена данными с дисплеем используется два режима: передача данных и передача команд, разница в байте управления, для передачи команд байт 0x80, для передачи данных 0x40.
Команды позволяют инвертировать дисплей, осуществлять прокрутку, переворачивать изображение, менять контрастность, менять порядок адресации и т.п. подробно в даташите.
Например, если нужно инвертировать картинку, то нужно передать команду 0xA7, вернуть обратно в нормальный режим 0xA6.
Для начала необходимо подключить интерфейс I2C и инициализации. Код для CooCox CoIDE
void main() { init_I2C1(); Delay(50); // вызов отдельной библиотечки для небольшой паузы }
функция инициализации
void init_I2C1(void) { // Включаем тактирование нужных модулей RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); // настройка I2C i2c.I2C_ClockSpeed = 1000000; i2c.I2C_Mode = I2C_Mode_I2C; i2c.I2C_DutyCycle = I2C_DutyCycle_2; i2c.I2C_OwnAddress1 = 0x0; i2c.I2C_Ack = I2C_Ack_Enable; i2c.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_Init(I2C1, &i2c); // I2C использует две ноги микроконтроллера, их тоже нужно настроить i2c_gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7 ; i2c_gpio.GPIO_Mode = GPIO_Mode_AF_OD; i2c_gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &i2c_gpio); // включаем модуль I2C1 I2C_Cmd(I2C1, ENABLE); }
Процесс передачи данных по I2C стандартный, генерация стартующей последовательности, передача данных, генерация завершения
Здесь важно, что работа с I2C у разных контроллеров отличается. Таким образом, этот код работает на STM32F103CBT6 (который установлен на Maple Mini), но для STM32F0xx или STM32F3xx работать не будет.
/*******************************************************************/ void I2C_StartTransmission(I2C_TypeDef* I2Cx, uint8_t transmissionDirection, uint8_t slaveAddress) { // На всякий случай ждем, пока шина осовободится while(I2C_GetFlagStatus(I2Cx, I2C_FLAG_BUSY)); // Генерируем старт I2C_GenerateSTART(I2Cx, ENABLE); // Ждем, пока взлетит нужный флаг while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_MODE_SELECT)); // Посылаем адрес подчиненному //возможно тут нужен сдвиг влево //http://microtechnics.ru/stm32-ispolzovanie-i2c/#comment-8109 slaveAddress<<1 I2C_Send7bitAddress(I2Cx, slaveAddress, transmissionDirection); // А теперь у нас два варианта развития событий - в зависимости от выбранного направления обмена данными if(transmissionDirection== I2C_Direction_Transmitter) { while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); } if(transmissionDirection== I2C_Direction_Receiver) { while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); } }
Если неправильно задан адрес, либо нет связи с устройством, то программа глухо виснет, что не есть правильно, конечно.
Запись данных
/*******************************************************************/ void I2C_WriteData(I2C_TypeDef* I2Cx, uint8_t data) { // Просто вызываем готовую функцию из SPL и ждем, пока данные улетят I2C_SendData(I2Cx, data); while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); }
Завершение передачи
void I2C_EndTransmission(I2C_TypeDef* I2Cx) { // Просто вызываем готовую функцию из SPL для однообразия работы I2C_GenerateSTOP(I2Cx, ENABLE); }
Для дисплея функция передача команды и передача данных. При передаче данных, дисплей загружает данные в видеопамаять, при передаче команд, дисплей выполняет команды и не ждет данных для видеопамяти.
#define OLED_COMMAND_MODE 0x80 #define OLED_DATA_MODE 0x40 #define OLED_DATA_ADDR 0x78 // передача команды void LCDI2C_WriteCommand(uint8_t _data){ I2C_StartTransmission (I2C1, I2C_Direction_Transmitter, OLED_DATA_ADDR); I2C_WriteData(I2C1, OLED_COMMAND_MODE); I2C_WriteData(I2C1, (int)(_data)); I2C_EndTransmission(I2C1); } // передача данных void LCDI2C_WriteData(uint8_t _data){ I2C_StartTransmission (I2C1, I2C_Direction_Transmitter, OLED_DATA_ADDR); //Wire.beginTransmission(_Addr); I2C_WriteData(I2C1, OLED_DATA_MODE); I2C_WriteData(I2C1, (int)(_data)); I2C_EndTransmission(I2C1); //I2C_GenerateSTOP(I2C1, ENABLE); //Wire.endTransmission(); }
Теперь инициализация дисплея при помощи последовательности команд, согласно даташита
void LCDI2C_init() { /* Init LCD */ LCDI2C_WriteCommand(0xAE); //display off LCDI2C_WriteCommand(0x20); //Set Memory Addressing Mode LCDI2C_WriteCommand(0x10); //00,Horizontal Addressing Mode;01,Vertical Addressing Mode;10,Page Addressing Mode (RESET);11,Invalid LCDI2C_WriteCommand(0xB0); //Set Page Start Address for Page Addressing Mode,0-7 LCDI2C_WriteCommand(0xC8); //Set COM Output Scan Direction LCDI2C_WriteCommand(0x00); //---set low column address LCDI2C_WriteCommand(0x10); //---set high column address LCDI2C_WriteCommand(0x40); //--set start line address LCDI2C_WriteCommand(0x81); //--set contrast control register LCDI2C_WriteCommand(0xFF); LCDI2C_WriteCommand(0xA1); //--set segment re-map 0 to 127 LCDI2C_WriteCommand(0xA6); //--set normal display LCDI2C_WriteCommand(0xA8); //--set multiplex ratio(1 to 64) LCDI2C_WriteCommand(0x3F); // LCDI2C_WriteCommand(0xA4); //0xa4,Output follows RAM content;0xa5,Output ignores RAM content LCDI2C_WriteCommand(0xD3); //-set display offset LCDI2C_WriteCommand(0x00); //-not offset LCDI2C_WriteCommand(0xD5); //--set display clock divide ratio/oscillator frequency LCDI2C_WriteCommand(0xF0); //--set divide ratio LCDI2C_WriteCommand(0xD9); //--set pre-charge period LCDI2C_WriteCommand(0x22); // LCDI2C_WriteCommand(0xDA); //--set com pins hardware configuration LCDI2C_WriteCommand(0x12); LCDI2C_WriteCommand(0xDB); //--set vcomh LCDI2C_WriteCommand(0x20); //0x20,0.77xVcc LCDI2C_WriteCommand(0x8D); //--set DC-DC enable LCDI2C_WriteCommand(0x14); // LCDI2C_WriteCommand(0x2E); // stop scrolling LCDI2C_WriteCommand(0xAF); //--turn on SSD1306 panel вот здесь должен засветиться "мусор" на экране }
после инициализации, при первом включении на экране должен быть "мусор", который удалим позднее.
Для более простого вывода текста необходимо создать шрифт, можно взять уже готовый, можно сделать при помощи утилиты с открытым кодом TheDotFactory
// шрифт экономичный, только латиница uint8_t LCD_Buffer[][22] = { {0x00, 0x00, 0x00, 0x00, 0x00},// (space) {0x00, 0x00, 0x5F, 0x00, 0x00},// ! {0x00, 0x07, 0x00, 0x07, 0x00},// " .... {0x00, 0x41, 0x36, 0x08, 0x00},// } {0x02, 0x01, 0x02, 0x04, 0x02},// ~ {0x08, 0x1C, 0x2A, 0x08, 0x08} // <- } //Печать символа 8x8, реально символ меньше, просто место занимает 8х8 void LCDI2C_draw8x8(uint8_t * buffer, uint8_t x, uint8_t y) { // send a bunch of data in one xmission LCDI2C_WriteCommand(0xB0 + y);//set page address //LCDI2C_WriteCommand(x & 0xf);//set lower column address LCDI2C_WriteCommand(x& 0x8);//set lower column address LCDI2C_WriteCommand(0x10 | (x >> 4));//set higher column address for (x=0; x<8; x++) { LCDI2C_WriteData(buffer[x]); // берем из шрифта 8 байт, которые отвечают за символ и выгоняем в память дисплея } }
Помним, что символов у нас помещается на экране 16x8, при этом небходимо осуществить "прокрукту" строки, если экран закончился.
Для этого создаем буфер экрана, в котором будет храниться вся информация, которую вывели на экран. Мы помним, что к сожалению, получить данные из дисплея не получается, поэтому храним в своем буфере.
uint8_t LCD_X,LCD_Y; // это текущие позиции для печати. Колонок 128, строк 8. uint8_t LCD_char[16][8]; // текстовый буфер экрана void LCDI2C_PrintChar(char c) // напечатать символ с текущей позиции экрана { if (c<32|| c>128){ // если переданный символ не входит в таблицу, то отобразиться пробелом c=32; } // если печатать некуда, сдвигаем экран if (LCD_Y>7){ // нужна новая строка int x1 = 0; // прорисовка из буфера int y1 = 0; char b; for (y1=0;y1<7;y1++){ for (x1 = 0; x1<16;x1++){ b = LCD_char[x1][y1+1]; // сдвиг на одну строку if (b<32|| b>127){ b=32; } LCD_char[x1][y1]=b; LCDI2C_draw8x8((uint8_t*)&LCD_Buffer[b-32],x1*8,y1); } } for (x1=0;x1<16;x1++){ // последняя строка пробелами заполняется LCDI2C_draw8x8((uint8_t*)&LCD_Buffer[0],x1*8,7); LCD_char[x1][7]=32; } LCD_Y = 7; // печатаем на последней, но остальные должны быть сдвинуты } LCD_char[LCD_X/8][LCD_Y]=c; // сохраняем напечатанный символ в буфере для сдвига LCDI2C_draw8x8((uint8_t*)&LCD_Buffer[c-32],LCD_X,LCD_Y); //печать символа из таблицы символов LCD_X += 8; // символ состоит из 8 колонок, следующий печатать через 8 if(LCD_X>=SSD1306_LCDWIDTH) { LCD_X =SSD1306_DEFAULT_SPACE; LCD_Y++; // добавить строку } } //Вывод на экран идет посимвольно. Если нужно произвести вывод, а //последняя строка заполнена, то экран сдвигается на одну строку вверх. // печать с переносом, следующая печать с новой строки void LCDI2C_Printf(char* buf) { while (*buf!=0) { if((LCD_X>SSD1306_LCDWIDTH)||(LCD_X<5)){LCD_X=SSD1306_DEFAULT_SPACE;} LCDI2C_PrintChar(*buf++); } LCD_Y++; LCD_X=0; } // печать без переноса, но если доходим до 16 символа, то перенос идет автоматически void LCDI2C_Print(char* buf) { while (*buf!=0) { if((LCD_X>SSD1306_LCDWIDTH)||(LCD_X<5))LCD_X=SSD1306_DEFAULT_SPACE;} LCDI2C_PrintChar(*buf++); } }
Есть возможность сформировать буфер экрана в программе, а затем вывести его целиком. Это происходит довольно быстро
// обновление экрана из буфера void LCDI2C_refresh(){ int page = 0; int column = 0; LCDI2C_setCursor(0,0); LCD_X=0; LCD_Y=0; for(page=0; page<8; page++) { LCDI2C_setCursor(0, page); for(column=0; column<16; column++){ LCDI2C_PrintChar(LCD_char[column][page]); } } } // очистка экрана, заполняем экран пробелами void LCDI2C_clear(){ // сначала буфер заполним пробелами int x1=0; int y1=0; for (y1=0;y1<8;y1++){ for (x1 = 0; x1<16;x1++){ LCD_char[x1][y1]=32; } } LCDI2C_refresh(); // просто обновим экран LCD_X=0; LCD_Y=0; }
Для заполнения цифрами буфера будет небольшая функция
здесь символ ` апострофа изменен в LCD_Buffer[][22]
//ӎx00, 0x01, 0x02, 0x04, 0x00},// `
ӎxFF, 0xFF, 0xFF, 0xFF, 0xFF},// ` квадратик рисуем вместо апострофа
// печать большими цифрами, фактически идет заполнение буфера определенным символом. буфер затем перегоняется на диспрей при помощи функции refresh
// void LCDI2C_bigprint(int col,int num) { if (num == 0){ LCD_char[col][0] = '`'; LCD_char[col][1] = '`'; LCD_char[col][2] = '`'; LCD_char[col][3] = '`'; LCD_char[col][4] = '`'; LCD_char[col][5] = '`'; LCD_char[col][6] = '`'; LCD_char[col+1][0] = '`'; LCD_char[col+1][1] = ' '; LCD_char[col+1][2] = ' '; LCD_char[col+1][3] = ' '; LCD_char[col+1][4] = ' '; LCD_char[col+1][5] = ' '; LCD_char[col+1][6] = '`'; LCD_char[col+2][0]= '`'; LCD_char[col+2][1]= '`'; LCD_char[col+2][2]= '`'; LCD_char[col+2][3]= '`'; LCD_char[col+2][4]= '`'; LCD_char[col+2][5]= '`'; LCD_char[col+2][6]= '`'; } if (num == 1){ ...... далее все остальные цифры }
Основная программа для управления main.c
Интересна тем, что используется и кнопка и светодиод, а также таймер. Обновление счетчиков и экрана происходит по таймеру TIM4. В основном цикле программы производится отслеживание нажатия кнопки. Нажатие на кнопку производит изменение визуального представления из построчного вывода с прокруткой на вывод счетчика большими цифрами. При этом светодиод отражает состояние, т.е. гаснет или включается в зависимости от типа вывода.
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #include "stm32f10x_usart.h" #include "stm32f10x_i2c.h" #include "stm32f10x_tim.h" #include "delay.h" #include "str.h" #include "I2C.h" // библиотека для работы с I2C #include "LCD_I2C.h" #define TIMER_PRESCALER 1000 // инициализация портов и таймера GPIO_InitTypeDef PORT; TIM_TimeBaseInitTypeDef timer; uint16_t previousState; // по кнопке будет переключаться int counter,a0,a1,a2,a3; // для вывода счетчика void main() { uint8_t data; // светодиод на PB1 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); PORT.GPIO_Pin = GPIO_Pin_1 ; PORT.GPIO_Mode = GPIO_Mode_Out_PP; PORT.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init( GPIOB , &PORT); // кнопка на PB8 PORT.GPIO_Pin = GPIO_Pin_8 ; PORT.GPIO_Mode = GPIO_Mode_IPD; PORT.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOB, &PORT); /* * GPIO_Mode_AIN — аналоговый вход. GPIO_Mode_IN_FLOATING — вход без подтяжки. GPIO_Mode_IPD — вход с подтяжкой к земле. GPIO_Mode_IPU — вход с подтяжкой к питанию. GPIO_Mode_Out_OD — выход с открытым коллектором. GPIO_Mode_Out_PP — выход с подтяжкой. GPIO_Mode_AF_OD — альтернативный выход с открытым коллектором. GPIO_Mode_AF_PP — альтернативный выход с подтяжкой. * */ //Включаем тактирование порта таймера TIM4 //Таймер 4 у нас висит на шине APB1 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); //А тут настройка таймера //Заполняем поля структуры дефолтными значениями TIM_TimeBaseStructInit(&timer); //Выставляем предделитель timer.TIM_Prescaler = TIMER_PRESCALER; //Тут значение, досчитав до которого таймер сгенерирует прерывание //Кстати это значение можно менять в самом прерывании timer.TIM_Period = 1000; //Инициализируем TIM4 нашими значениями TIM_TimeBaseInit(TIM4, &timer); __enable_irq(); // разрешить прерывания init_I2C1(); // инициализация i2с Delay(50); LCDI2C_init(); // инициализация дисплея, адрес 0x78 там зашит LCDI2C_clear(); // очистить от прошлого мусора LCD_X = 0; LCD_Y = 0; counter = 0; //Настраиваем таймер для генерации прерывания по обновлению (переполнению) TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE); //Запускаем таймер TIM_Cmd(TIM4, ENABLE); //Разрешаем соответствующее прерывание NVIC_EnableIRQ(TIM4_IRQn); while(1) { // Вся полезная работа – в прерывании if(GPIO_ReadInputDataBit (GPIOB, GPIO_Pin_8)){ // нажата кнопка NVIC_DisableIRQ(TIM4_IRQn); // выключили прерывание LCDI2C_clear(); GPIO_SetBits(GPIOB, GPIO_Pin_1); if (previousState==0){ previousState = 1; GPIO_SetBits(GPIOB, GPIO_Pin_1); }else { previousState = 0; GPIO_ResetBits(GPIOB, GPIO_Pin_1); } Delay(500); NVIC_EnableIRQ(TIM4_IRQn); // опять включили } __NOP(); } return; } void TIM4_IRQHandler() // прерывание от таймера { // перебрасываем счетчики counter++; if (counter>9999){ counter=0; } char buf[5]; itoa(counter,buf,10); // теперь в строке символы, функция в библиотечке if (previousState !=1 ) // по нажатию кнопки меняется представление на экране { // большие цифры GPIO_SetBits(GPIOB, GPIO_Pin_1); // включили светодиод // печать больших цифр // от 0000 до 9999 a0 = 0; a1 = 0; a2 = 0; a3 = 0; if (strlen(buf)==1){ a0= buf[0]-48; } if (strlen(buf)==2){ a0 = buf[1]-48; a1 = buf[0]-48; } if (strlen(buf)==3){ a0 = buf[2]-48; a1 = buf[1]-48; a2 = buf[0]-48; } if (strlen(buf)==4){ a0 = buf[3]-48; a1 = buf[2]-48; a2 = buf[1]-48; a3 = buf[0]-48; } LCDI2C_bigprint(0,a3); // заполняем большой цифрой буфер LCDI2C_bigprint(4,a2); LCDI2C_bigprint(9,a1); LCDI2C_bigprint(13,a0); LCDI2C_refresh(); timer.TIM_Period = 1000; TIM_TimeBaseInit(TIM4, &timer); } else { //строки с прокруткой экрана //гасим светодиод GPIO_ResetBits(GPIOB, GPIO_Pin_1); LCDI2C_Print("test: "); // печать без переноса строки LCDI2C_Printf(buf); // печать с переносом строки timer.TIM_Period = 20000; // медленный пересчет циферок в строковом режиме TIM_TimeBaseInit(TIM4, &timer); } //Очищаем бит прерывания TIM_ClearITPendingBit(TIM4, TIM_IT_Update); }
Ссылка на проект для CooCox со всеми исходниками здесь>>>