Use To Advantage

Используй с пользой!

  • Увеличить размер шрифта
  • Размер шрифта по умолчанию
  • Уменьшить размер шрифта
Home

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

E-mail Печать
2.2/5 (935 голоса)

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 &amp; 0xf);//set lower column address
LCDI2C_WriteCommand(x&amp; 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*)&amp;LCD_Buffer[b-32],x1*8,y1);
}
}
 
for (x1=0;x1<16;x1++){ // последняя строка пробелами заполняется
LCDI2C_draw8x8((uint8_t*)&amp;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*)&amp;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 , &amp;PORT);
// кнопка на PB8
PORT.GPIO_Pin = GPIO_Pin_8 ;
PORT.GPIO_Mode = GPIO_Mode_IPD;
PORT.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOB, &amp;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(&amp;timer);
//Выставляем предделитель
timer.TIM_Prescaler = TIMER_PRESCALER;
//Тут значение, досчитав до которого таймер сгенерирует прерывание
//Кстати это значение можно менять в самом прерывании
timer.TIM_Period = 1000;
//Инициализируем TIM4 нашими значениями
TIM_TimeBaseInit(TIM4, &amp;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, &amp;timer);
}
else
{
//строки с прокруткой экрана
//гасим светодиод
GPIO_ResetBits(GPIOB, GPIO_Pin_1);
LCDI2C_Print("test: "); // печать без переноса строки
LCDI2C_Printf(buf); // печать с переносом строки
timer.TIM_Period = 20000; // медленный пересчет циферок в строковом режиме
TIM_TimeBaseInit(TIM4, &amp;timer);
}
 
//Очищаем бит прерывания
TIM_ClearITPendingBit(TIM4, TIM_IT_Update);
}
 
 

 

Ссылка на проект для CooCox со всеми исходниками здесь>>>

Теги: Maple Mini, stm32, SSD1306, подключение дисплея,

Еще по теме

Полезно? Поделитесь ссылкой! E-mail Сохраните на будущее!  

Разделы

Поиск

Авторизация


(c) 2010-2015 Используй с пользой. При цитировании обязательна рабочая ссылка на http://www.useto.ru.