Подключение дисплея SSD1306 к STM32F103CBT6  (Maple Mini)

Maple Mini компактная 40 пиновая отладочная плата с установленным на ней процессором STM32F103CBT6.  На плате минимум обвязки, однако есть один светодиод и пользовательская кнопка, которую можно использовать для работы пользовательской программы. Поскольку у контроллера есть целых два интерфейса I2C, то есть возможность подключить дисплей на основе контроллера SSD1306 с интерфейсом  I2C.

Дисплей планируется для вывода отладочной информации, т.е. в первую очередь используется режим вывода текстовой информации. Конкретный дисплей имеет интерфейс I2C. Четыре вывода. VCC  - 3.3 вольта. GND, SDA, SDL.
У 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]
//{0x00, 0x01, 0x02, 0x04, 0x00},// `
    {0xFF, 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);
}



Видео как это работает. Для программирования и отладки использовалась плата STM32F3Discovery