Пробуем организовать связь с TM1637 без библиотеки.

Krendelyok
Offline
Зарегистрирован: 05.10.2016

Поставил я себе невыполнимую задачу написать первую свою программку для ардуины. Взор упал на датчик DHT11. За три дня удалось наладить общение с датчиком без библиотеки. Обрадовался я и поставил себе еще более сложную задачу для новичка: наладить связь 4-разрядным дисплеем, где в качестве драйвера микросхема TM1637. Покурил DATASHEET, наваял за недельку в свободное время код.

byte command1 = B01000000; //Команда записи данных в регистр дисплея
byte command2 = B11000000; //Команда "начальный адрес дисплея"

byte symbol_1 = 0xF0;//Символ_1
byte symbol_2 = 0x20;//Символ_2
byte symbol_3 = 0xF0;//Символ_3
byte symbol_4 = 0x80;//Символ_4
byte symbol_5 = 0xF0;//Символ_5
byte symbol_6 = 0xD0;//Символ_6


byte command3 = B10001101; //Яркость и ON/OFF дисплея
byte command4 = B01000010;//Команда чтения клавиатуры.
unsigned int bit_n;
unsigned int WAIT_HL = 0;
unsigned int pack = 0x01;
unsigned int send_byte;
//int SCL_LOW = 0; //Переменная, в которой хранится номер цикла при ожидании LOW на SCL.
//int SCL_HIHG = 0; //Переменная, в которой хранится номер цикла во время ожидания HIGH SCL.
unsigned int SCL_TM1637 = B00001000; //Задаем 3 пин, как выход SCL
unsigned int SDA_TM1637 = B00000100;  //Задаем 2 пин, как шину SDA
volatile boolean SCL_STAT = true;//Переменная, в которой хранится текущее состояние выхода SCL
volatile boolean ACK_tm1637 = false; //Переменная в которой возвращается проверка сигнала ACK (подтверждение приема байта)

void setup() {
TIMSK2 &= ~(1<<TOIE2);// Запрещаем  прерывания, сбросив бит TOIE2 в регистре TIMSK2. 
TCCR2A &= ~((1<<COM2A1) | (1<<COM2A0) | (1<<COM2B1) | (1<<COM2B0) | (1<<WGM21) | (1<<WGM20));//Сбрасываем биты 7 - 4 в регистре TCCR2A, так как переключение выходов OC2A и OC2B нас пока не интересует, а также отключаем режим ШИМ (WGM).          
TIMSK2 |= (1<<TOIE2);// Разрешаем  прерывания, установив бит TOIE2 в регистре TIMSK2. Прерывание будет переворачивать сигнал на SCL
DDRD |= B00001100;  //Инициализируем третий и второй пины порта D как выходы.
PORTD |= B00001100; //Выводим HIGH на третий и второй пины порта D.
Serial.begin(9600);
}

void generator_SCL(){
PORTD = PORTD ^ SCL_TM1637; //Инвертируем третий пин порта D. Тем самым формируем сигнал SCL.
//Serial.println(PORTD);
SCL_STAT = !SCL_STAT;        //Записываем состояние SCL в переменную SCL_STAT

}



void start_transfer(){
DDRD |= B00001100;  //Инициализируем третий и второй пины порта D как выходы.
PORTD |= B00001100; //Выводим HIGH на SCL и SDA
SCL_STAT = true;
delay(1);
PORTD &= B11111011; //  Посылаем на SDA LOW, тем самым отправляем приемнику сигнал о старте передачи данных (SCL = HIGH).
TCCR2B = 0x02;       //Подключаем 2 Мгц к Таймеру2. Таймер начинает считать до 255, вызывать подпрограмму прерывания (generator_SCL()) и сбрасываться в 0. 
}

void stop_start(){  //Используется после инициализации и перед третьей командой
WAIT_HL = 0;
while(WAIT_HL < 2000){     //Ждем LOW на SCL (см. DATASHEET TM1637
  if(!(PIND & B00001000)){     //Если пришел LOW,
    //Serial.println("GOOD");
    WAIT_HL = 2000;         //идем дальше
  }
  WAIT_HL += 1;
}
  WAIT_HL = 0;
PORTD |= SDA_TM1637;               //выводим 1 на пин SDA,
while(WAIT_HL < 50){
  WAIT_HL += 1;
  }
WAIT_HL = 0;
PORTD &= B11111011;               //выводим 0 на пин SDA,    
}
 

void end_transfer(){
TCCR2B = 0x00;       //Останавливаем Таймер2.
PORTD |= B00001000;  //Выводим на SCL HIGH
SCL_STAT = true;     // Не забываем присвоить переменной статуса SCL значение true
DDRD |= B00000100;   //Инициализируем второй пин порта D (SDA) как выход.
PORTD &= B11111011;  //Выводим на SDA LOW.
delay(1);
TCNT2 = 0;           //Обнуляем таймер.
PORTD |= B00000100;  //Выводим на SDA HIGH.
}

void wait_ack(){
ACK_tm1637 = 0;
WAIT_HL = 0;
while(WAIT_HL < 2000){     //Ждем LOW на SCL (см. DATASHEET TM1637
  if(!SCL_STAT){     //Если пришел LOW,
    //Serial.println("GOOD");
    WAIT_HL = 2000;         //идем дальше
  }
  WAIT_HL += 1;
}
DDRD &= B11111011;          //Инициализируем пин SDA как вход
PORTD |= B00000100;         //Подключаем подтягивающий резистор к SDA
WAIT_HL = 0;
while(WAIT_HL < 2000){    //Начинаем слушать шину SDA не более 2000 циклов
   //Serial.println(PORTD);
   if(!(PIND & B00000100)){//Если  SDA = LOW,
    Serial.println("OK");
    Serial.println(pack);
       WAIT_HL = 2000;
       ACK_tm1637 = 1;       //присваиваем переменной ACK_tm1637 значение 1, как подтверждение получения ACK
     }
    WAIT_HL += 1;
   }
if(ACK_tm1637 == 0){
   Serial.println(pack);
   Serial.println("NO ACK!");
}
WAIT_HL = 0;
while(WAIT_HL < 2000){     //Ждем положительного фронта на SCL (см. DATASHEET TM1637
  if(PIND & B00001000){     //Если пришел положительный фронт,
    //Serial.println("GOOD");
    WAIT_HL = 2000;         //уходим в loop
  }
  WAIT_HL += 1;
}
WAIT_HL = 0;
DDRD |= B00000100;  //Инициализируем второй пин (SDA) порта D как выход.
}


void byte_send(){ 
send_byte = pack;
//DDRD |= B00000100;  //Инициализируем второй пин (SDA) порта D как выход.
bit_n = 0;
while(bit_n <= 7){             //отправляем байт на SDA
//Serial.println(pack);
//Serial.println(bit_n);  
    if(!SCL_STAT){                 //Если пин SCL переключился на LOW
      //Serial.println(pack);
      if(send_byte & B00000001){            //Если младший бит равен 1,
      //Serial.println("1");
      PORTD |= SDA_TM1637;               //выводим 1 на пин SDA,
      send_byte = send_byte >> 1;                 //сдвигаем байт на 1 бит вправо и переходим к отправке следующего бита.                          
      bit_n += 1;
      //i += 1;
      while(!SCL_STAT){}              //Ждем HIGH на SCL
         } else{                            //Если младший бит равен 0
       //Serial.println("0");
        PORTD &= ~SDA_TM1637;           //выводим на SDA 0,
        send_byte = send_byte >> 1;            //сдвигаем байт вправо на 1 бит и переходим к отправке следующего бита
        bit_n += 1;
       // z +=1;
      
       while(!SCL_STAT){}              //Ждем HIGH на SCL
      }
     }
  
}
//Serial.println(pack);
//wait_ack();                        //После передачи байта переходим в подпрограмму ожидания сигнала подтверждения приема
}


void loop() {
//delay(1000);
//Serial.println(pack);
start_transfer();
pack = command1;
byte_send();
wait_ack();
stop_start();
pack = command2;
byte_send();
wait_ack();
pack = byte(random());
byte_send();
wait_ack();
pack = byte(random());
byte_send();
wait_ack();
pack = byte(random());
byte_send();
wait_ack();
pack = byte(random());
byte_send();
wait_ack();
//pack = symbol_5;
//byte_send();
//wait_ack();
//pack = symbol_6;
//byte_send();
//wait_ack();
stop_start();
pack = command3;
byte_send();
wait_ack();
pack = command4;
byte_send();
wait_ack();
end_transfer();
delay(5000);
}




ISR(TIMER2_OVF_vect) {
generator_SCL(); 
}

А работать он не хочет. Хотя сигнал ACK от TM1637 приходит. Похоже, я не совсем разобрался в структуре команд данной микросхемы. Замечена следующая странность. Если залить в Ардуино простейший скетч с библиотекой для работы с TM1637, подождать вывода на экран каких-нибудь цифр, а потом залить мой код, то с каждой отправкой пакетов на дисплее начинают менять кракозябры. Видео https://www.youtube.com/watch?v=XTGoT2qXfXs

Причем, такое происходит, даже если отправлять постоянные, а не рандомные значения. Тоесть, в память TM1637 что-то записывается , но что и по какой логике - не понятно. Если сделать рестарт Ардуины, то на дисплее не светится ни один сегмент. 

Не совсем понятен следующий момент из даташита. Что значит инициализация? Правильно ли я понял, что это команда 0100 0000? В блок-схеме получается три команды: инициализация, команда записи в память, начальный адрес. А на графике две команды.

 

 

Krendelyok
Offline
Зарегистрирован: 05.10.2016

Еще заметил особенность. Если использовать в качестве символов использовать константы, то информация на дисплее (в случае если его "завести" предварительно стандартной библиотекой) сдвигается влево при каждом сеансе связи с TM1637. (кракозябры бегут влево). Видео https://www.youtube.com/watch?v=xdhpGZvaWU0  И это происходит, даже если в 126 строке записать while bit_n < 6 Тоесть даже если отправлять не 8 бит, а 5. Получается, микросхема просто сдвигает данные в своей памяти на 1 адрес при каком-то событии на SCL и SDA, а программа моя не работает.

Logik
Offline
Зарегистрирован: 05.08.2014

///завести" предварительно стандартной библиотекой

Она просто правильно его инициализирует. Ищите ошибку в инициализации. Но учитывая что она идет первой, то может ошибатся и в низкоуровневом обмене, проверяйте порядок отправки бит например.

Остальное - потом.

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

У мег есть шикарный аппаратный драйвер шины I2C, пользуйтесь им и не мучайтесь. Работает вплоть до 880кГц, позволяет меге быть как мастером, так и слейвом, и даже отправлять пакеты типа "всем" и принимать таковые .. зачем это?!?

Logik
Offline
Зарегистрирован: 05.08.2014

Завязыватся на драйвер конкретного процессора не имеет смысла, сегодня один процессор завтра другой и снова изучать его аппаратную часть заново прийдется. При том что в некоторых драйвера вообще нет. А научитесь работать с i2c низкоуровнево - это умение на всю жизнь полезное, у любого процессора на любых ногах заработает Ваш код после правки буквально 5 строк.

Krendelyok
Offline
Зарегистрирован: 05.10.2016

Просто спортивный интерес. Хочется разобраться, как работает TM1637. Ну и I²C. Хоть в этом случае он и неполноценный.

Krendelyok
Offline
Зарегистрирован: 05.10.2016

Ой, а программа то работает, оказывается. Если ввести вот такие символы

byte symbol_1 = 0x06;//Символ_1
byte symbol_2 = 0x5B;//Символ_2
byte symbol_3 = 0x66;//Символ_3
byte symbol_4 = 0x6D;//Символ_4
byte symbol_5 = 0x7C;//Символ_5
byte symbol_6 = 0x27;//Символ_6
Получается вот такой результат  https://www.youtube.com/watch?v=vRPjkRTdsXA  Просто я не сразу понял, что режим автоинкремента TM1637 - это, оказывается, режим бегущей строки. 
  Осталось понять, как правильно "запустить" дисплей. В даташите не могу ничего найти. Есть управление яркостью, так оно отправляется в моей программе третьей командой. Что не так делаю? Ну и режим фиксированной адресации надо понять.
 
 
dhog1
Offline
Зарегистрирован: 01.03.2016

Krendelyok

Постараюсь обойтись без оценок, но вы  _все_  делаете "не так". Техдок на TM163x в переводе на английский действительно выглядит невразумительно. Но способы работы с "подтянутой" линией никто не отменял. И чтобы два раза не вставать - приведенный вами код никуда не годится, причем это не критика кода, а критика подхода. Коротко она (критика) выглядит так:

    - если вы беретесь генерировать тактовые сигналы по SCL (в вашем коде через прерывание таймера), то обеспокойтесь правильным (по времени) выставлением уровней по линии SDA (данные)

    - на "подтянутых" линиях (которые соединены с питанием через подтягивающие резисторы) используют две операции: освободить линию (перейти в высокоимпеденсное состояние, т.е. отключиться от линии, и тогда резисторы подтянут ее в состояние логической единицы), и прижать линию к земле, установив на ней логический ноль. Что-либо писать в подтянутую линию (устанавливая бит порта в "1") смысла не имеет

Ваш код не делает этого.

Отдельно вопросы с работой самой TM163x. Видимо мы читали одинаковую документацию, но поняли логику по-разному. Там все работает согласно описанию (чуднОму в переводе, но все-таки).

Режимы "бегущей строки", шаманство с "номером" команды - все из области ударов в бубен, заменяющих работу по техдоку. Если вам еще интересно разобраться, могу предоставить примеры кода.

Отдельно по рекомендациям использовать аппаратные (да и программные) драйверы I2C. Посадить TM163x на общую шину I2C нельзя. Работать с TM163x строго по спецификациям I2C - излишне.

Krendelyok
Offline
Зарегистрирован: 05.10.2016

dhog1 пишет:

Krendelyok

могу предоставить примеры кода.

Спасибо за ответ. Нашел в интернете вот такой код http://xn--90azbaece.xn--p1ai/?p=97 В принципе, всё понятно, вроде бы. Понятно, как просто можно запрограммировать I²C на любом пине. Сразу расхотелось работать с таймером. Бегло пробежался по коду и комментариям. Пока не понял, зачем делается очистка дисплея. Без нее, похоже, не работает ничего. Ткните носом, где в даташите об этом говорится? Во всяком случе выдернул из этого кода TM1637_writeByte, TM1637_stop(), TM1637_start(). Но этих функций недостаточно для того, чтобы дисплей работал. Тоесть работа с TM1637 идет в такой последовательности:

  TM1637_start();
  TM1637_writeByte(ADDR_AUTO);//Отправка команды автоинкремента адреса
  TM1637_stop();
  TM1637_start();
  TM1637_writeByte(Cmd_SetAddr);//Отправка команды начального адреса
  for(i=0;i < 4;i ++)
  {
    TM1637_writeByte(SegData[i]); //Отправка символов
  }
  TM1637_stop();
  TM1637_start();
  TM1637_writeByte(Cmd_DispCtrl);//Отправка команды управления состоянием дисплея
  TM1637_stop();

Но чего-то не хватает для того, чтобы дисплей зажегся.

Krendelyok
Offline
Зарегистрирован: 05.10.2016

dhog1 пишет:
Что-либо писать в подтянутую линию (устанавливая бит порта в "1") смысла не имеет

Уточните, пожалуйста, к какой конкретно строчке кода это относится? А то голова уже не соображает.

dhog1
Offline
Зарегистрирован: 01.03.2016

Krendelyok

Ok. Давайте так - никто вас тыкать ни во что не будет, потому что вы всё делаете в правильном направлении (и очень быстро), но на пути невразумительный даташит и путаница (не ваша) с I2C. Сейчас мы просто разложим по косточкам (на предмет "понять как") работать с китайской микросхемой TM163x. Их целое (и интересное) семейство, управляемое на один лад.

Мне понадобится некоторое время (часов 12), чтобы аккуратно всё сформулировать и привести примеры.

Krendelyok
Offline
Зарегистрирован: 05.10.2016

Arhat109-2 пишет:

У мег есть шикарный аппаратный драйвер шины I2C, пользуйтесь им и не мучайтесь. Работает вплоть до 880кГц, позволяет меге быть как мастером, так и слейвом, и даже отправлять пакеты типа "всем" и принимать таковые .. зачем это?!?

Да, я в курсе, что есть аппаратная реализация. TWI, кажется, называется. Будет интересно про нее почитать. Но у tm1637, кстати, неполноценный I²C. У этой микросхемы нет "личного" адреса. Это может быть проблемой , если использовать аппаратный драйвер?

dhog1
Offline
Зарегистрирован: 01.03.2016

Krendelyok

Будет много букв, по-другому не получилось. Это первая часть.

1. Терминология

На линиях "с подтяжкой к питанию" (pullup) используют операции:

    - освободить линию (перейти в высокоимпедансное состояние, т.е. отключиться от линии), что приводит к установлению на линии логической единицы (работают подтягивающие к питанию резисторы). Это означает перевод пина МК в состояние (input, low), т.е. DDR(PIN) -> 0, PORT(PIN) -> 0

    - прижать линию к земле (установить на линии логический ноль). Это означает перевод пина МК в состояние (output, low), т.е DDR(PIN) -> 1, PORT(PIN) -> 0

В своих примерах буду использовать следующие "ясно читаемые" макросы, заимствованные из апноута avr035 "Efficient C Coding for AVR" (просто нравятся):

#define SETBIT(ADDRESS,BIT) (ADDRESS |= (1<<BIT))

    #define CLEARBIT(ADDRESS,BIT) (ADDRESS &= ~(1<<BIT))
    #define CHECKBIT(ADDRESS,BIT) (ADDRESS & (1<<BIT))

    #define VARFROMCOMB(x, y) x
    #define BITFROMCOMB(x, y) y

    #define C_SETBIT(comb)   SETBIT(VARFROMCOMB(comb),   BITFROMCOMB(comb))
    #define C_CLEARBIT(comb) CLEARBIT(VARFROMCOMB(comb), BITFROMCOMB(comb))
    #define C_CHECKBIT(comb) CHECKBIT(VARFROMCOMB(comb), BITFROMCOMB(comb))

Использование этих макросов предполагает определение конкретной ножки МК, например

#define MY_LED_PIN   PORTD, PORTD5

и, собственно, манипулирование

C_SETBIT(MY_LED_PIN);    // установить 5-й пин порта D в логическую "1"
C_CLEARBIT(MY_LED_PIN);  // установить 5-й пин порта D в логический "0"

2. Операции

Для работы с TM163x нужно задать две ноги МК - пин SDA, по которому передаются (принимаются, об этом позже) данные, и пин SCL, который будет тактирующим ("часы") для данных. Это обычная синхронная двухпроводная шина передачи данных. Таким же образом, напрмер, мы передаем данные в сдвиговые регистры. Способ передачи данных - сначала выставляем на ноге SDA данные (логический ноль или логическую единицу), затем даем синхроимпульс по ноге SCL (поднимаем ее в логическую единицу, выдерживаем некоторое время, опускаем в логический ноль; считывание принимающим устройством происходит в момент перевода SCL в логическую единицу).

Для манипулирования входами TM163x определим удобные (удобочитаемые) макросы

а) определим ножки МК, которые будут подключаться к входам DIO и CLK (по техдоку) микросхемы TM163x

например ножка PORTD4 порта D будет источником синхроимпульсов ("часами"), а по ножке PORTD5 порта D будут передаваться данные



#define _SSCK_PORT   PORTD, PORTD4
#define _SSCK_DDR    DDRD,  DDD4
#define _DATA_PORT   PORTD, PORTD5
#define _DATA_DDR    DDRD,  DDD5

б) и вот теперь определим то, чего мы хотим от этих ножек МК
 

                 // хотим переводить ножку синхроимпульса в состояние "1" (HIGH) и "0" (LOW)
#define CLK_HIGH     C_CLEARBIT(_SSCK_DDR)
#define CLK_LOW      { C_SETBIT(_SSCK_DDR); C_CLEARBIT(_SSCK_PORT); }
                 // хотим переводить ножку данных в состояние "1" (HIGH) и "0" (LOW)
#define DIO_HIGH     C_CLEARBIT(_DATA_DDR)
#define DIO_LOW      { C_SETBIT(_DATA_DDR); C_CLEARBIT(_DATA_PORT); }

3. Использование

Зачем все эти навороты из макросов?

Ниже пойдут примеры работы с микросхемой TM163x, которая понимает состояние своих входов. Этими входами нужно уметь управлять. Важно понимать, как формировать логические "0" и "1".

Установить логическую "1" на подтянутой (к питанию, pullup) линии означает ничего не делать, просто отключиться.

Установить логический "0" на подтянутой (к питанию, pullup) линии означает прижать ее к земле.

Это повторение пункта 1. Макросы из п.2 показывают как именно это делается. Можно написать свои макросы, имеющие другой вид. Нужно держать в памяти следующее - тот, с кем вы работаете (у нас это микросхема TM163x), реагирует на состояние линии - HIGH или LOW. Состояние (подтянутой к питанию, pullup) линии МК определяется манипуляцией пином - регистами DDR и PORT этого пина. Если мы хотим получить "0" (LOW) на линии, мы обеспечиваем DDR (пина) -> 1 (output для пина) и PORT (пина) -> 0 (прижимаем линию к земле). Если мы хотим получить "1" (HIGH) на линии, мы обеспечиваем DDR (пина) -> 0 (input для пина).

В примерах кода буду писать CLK_LOW или, например, DIO_HIGH.

dhog1
Offline
Зарегистрирован: 01.03.2016

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

Базовый код может быть написан (он так и выглядит в моей реализации) таким образом.

void tm1637_start(void) {

    DIO_LOW;
    CLK_LOW;
}

void tm1637_stop(void) {

    DIO_LOW;
    delay_us(_CLOCK_DELAY);
    CLK_HIGH;
    delay_us(_CLOCK_DELAY);
    DIO_HIGH;
}

void tm1637_writeByte(unsigned char data) {

unsigned char i;

    for (i=0; i<8; i++) {
        if (data & 0x01) DIO_HIGH;
        else DIO_LOW;
        data >>= 1;
        CLK_HIGH;
        delay_us(_CLOCK_DELAY);
        CLK_LOW;
    };

    CLK_HIGH;
    delay_us(_CLOCK_DELAY);
    CLK_LOW;
}

Макросы DIO_LOW, CLK_HIGH и CLK_LOW рассмотрены выше, они задают состояние соответствующих линий.

Параметр _CLOCK_DELAY (микросекунды) определяет задержку, чтобы TM1637 работал на своей частоте не выше ~450 kHz. Для быстрых МК (16 MHz) _CLOCK_DELAY = 1 us так оно и выходит (2 х 1us ~ 500 kHz)

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

Для полноты картины следует привести базовую функцию чтения кнопок. Многие чипы из семейста TM163x могут читать кнопки, замыкающиеся на общий провод.

unsigned char void tm1637_scan_key(void) {

unsigned char i, key;

    tm1637_start();
    tm1637_writeByte(TM1637_DSC|_key_scan);
    
    _DIO_HIGH;
    
    for (i = 0; i < 8; i++) {
        key >>= 1;
        delay_us(_KEY_DELAY);
        _CLK_HIGH;
        if (C_CHECKBIT(_DATA_PIN)) key |= 0x80;
        delay_us(_KEY_DELAY);
        _CLK_LOW;
    }
    
    _DIO_LOW;
    _CLK_HIGH;
    delay_us(_CLOCK_DELAY);
    _CLK_LOW;
    
    tm1637_stop();
    return (key);
}

Параметр _KEY_DELAY (микросекунды) определяет задержку сканирования кнопок, обычно ~0.5ms x число_разрядов, у меня работает при 5us для 16MHz МК (это минимум).

Техдок на TM163x можно резюмировать следующим образом. Комментарии на буржуазном языке отражают не мою англоманию, а сложности с кириллицей в моем IDE.

/*
                                // TM1637 instructions
  command byte  B7 B6  B5  B4  B3  B2  B1  B0
                 0  1  x   x                      Data Command Set
                 1  0  x   x                      Display Control Command Set
                 1  1  x   x                      Address command set

                 0  1                   0   0     Write data to the display register
                 0  1                   1   0     Read key scan data
                 0  1               0             Automatic address incrementing
                 0  1               1             Fixed address
                 0  1           0                 Normal mode
                 0  1           1                 Test mode

                                                 -- address (display register, digit nbr, bit nbr) --
                 1  1           0   0   0   0     0x00   the first digit (rightmost) of digital tube (display)
                 1  1           0   0   0   1     0x01
                 1  1           0   0   1   0     0x02
                 1  1           0   0   1   1     0x03
                 1  1           0   1   0   0     0x04
                 1  1           0   1   0   1     0x05   the last digit (leftmost)

                                                 -- display control (brightness and on/off) --
                 1  0               0   0   0    Set the pulse width of  1 /16
                 1  0               0   0   1    Set the pulse width of  2 /16
                 1  0               0   1   0    Set the pulse width of  4 /16
                 1  0               0   1   1    Set the pulse width of  10/16
                 1  0               1   0   0    Set the pulse width of  11/16
                 1  0               1   0   1    Set the pulse width of  12/16
                 1  0               1   1   0    Set the pulse width of  13/16
                 1  0               1   1   1    Set the pulse width of  14/16
                 1  0           0                Display OFF
                 1  0           1                Display ON

*/

Из приведенного выше следует, что есть 3 (функциональных) группы команд.

#define TM1637_DSC      0x40        // DATA SET COMMAND
    #define _display_write    0x00
    #define _key_scan         0x02
    #define _auto_inc         0x00
    #define _fixed_addr       0x04
    #define _normal_mode      0x00
    #define _test_mode        0x80

#define TM1637_DCC      0x80        // DISPLAY CONTROL COMMAND
    #define _tm1637_disp_off  0x00
    #define _tm1637_disp_on   0x08
    #define _tm1637_brtn_mask 0x07

#define TM1637_ASC      0xC0        // ADDRESS SET COMMAND
    #define _tm1637_r0  0x00
    #define _tm1637_r1  0x01
    #define _tm1637_r2  0x02
    #define _tm1637_r3  0x03
    #define _tm1637_r4  0x04
    #define _tm1637_r5  0x05
    #define _tm1637_rmask 0x03

Каждая команда начинается с start(), далее write() (команда и данные), завершается stop().

Инициализация TM1637 (мне непонятно что там со значениями по умолчанию) выглядит так:

void tm1637_init(void) {

unsigned char i;

    tm1637_start();
    tm1637_writeByte(TM1637_DSC|_display_write|_auto_inc|_normal_mode);
    tm1637_stop();

    tm1637_start();            // это очистка 7-сегментного индикатора по желанию
    tm1637_writeByte(TM1637_ASC | _tm1637_r0);  // Set the first address
    for(i = 0; i < _TM1637_DIGS_ATTACHED; i++) tm1637_writeByte(0x00);    // Send data to clear
    tm1637_stop();

    tm1637_start();
    tm1637_writeByte(TM1637_DCC | _tm1637_disp_on | 0x03); // Open display, set brightness = 3
    tm1637_stop();
}

Здесь _TM1637_DIGS_ATTACHED константа, определяющая кол-во разрядов (цифр) 7-сегментного индикатора.

И как пример простой вывод положительного числа на индикатор, где dp - позиция десятичной точки, отсчитывая справа.

void tm1637_write(uint16_t num, uint8_t dp) {

unsigned char digs[_TM1637_DIGS_ATTACHED];
unsigned char i = 0, j;

    do {
        digs[i++] = digitToSegment2[num % 10];
    } while(num /= 10);           // i - now is actual digital number
    if (dp) digs[dp] |= 0x80;     // положение десятичной точки

    tm1637_start();
    tm1637_writeByte(TM1637_ASC);

    for (j = 0; j < i; j++) tm1637_writeByte(digs[j]);

    tm1637_stop();
}

где digitToSegment2[] таблица перекодировки цифры в представление сегментов индикатора вроде

const unsigned char digitToSegment2[] = {
  0b00111111,    // 0
  0b00000110,    // 1
  0b01011011,    // 2
  0b01001111,    // 3
  0b01100110,    // 4
  0b01101101,    // 5
  0b01111101,    // 6
  0b00000111,    // 7
  0b01111111,    // 8
  0b01101111,    // 9
  0b01000000,    // - (minus)
  0b00000000     // blank
};

 

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

Да, это не совсем I2C и аппаратный драйвер не подойдет, хотя мне сначала показалось что можно в качестве адреса подавать первый байт команды .. ан нет. режимы R/W в наборе команд не согласуются с младшим битом команды..

Krendelyok
Offline
Зарегистрирован: 05.10.2016

dhog1 пишет:

1. Терминология

На линиях "с подтяжкой к питанию" (pullup) используют операции:

    - освободить линию (перейти в высокоимпедансное состояние, т.е. отключиться от линии), что приводит к установлению на линии логической единицы (работают подтягивающие к питанию резисторы). Это означает перевод пина МК в состояние (input, low), т.е. DDR(PIN) -> 0, PORT(PIN) -> 0

Вот этот момент мне не понятен. По-моему, тут нестыковка с даташитом на атмегу328. На 97 странице есть такая таблица

http://cs636829.vk.me/v636829594/2f006/Ae1kfNW93Ls.jpg Кстати, не совсем понятна третья строчка в этой таблице. Получается, можно сконфигурировать пин как Вход, записать туда единицу, но, записав в PUD = 1, отключить встроенный подтягивающий резистор. Правильно я понимаю, что это состояние не будет отличаться от первой строчки?

Из нее я и сделал вывод. Чтобы начать "слушать" шину SDA, нужно инициализировать пин как вход и Записать в него HIGH. В этом случае мы подключаем встроенный резистор подтяжки. А если запишем LOW, у нас пин перейдет в высокоимпедансное состояние. Тогда нам нужен будет внешний резистор подтяжки. Правильно? Поэтому я и использовал в коде команду записи единицы на вход. Дальше пока не читал, буду разбираться. Наверное, вечером)

 

dhog1 пишет:
 В своих примерах буду использовать следующие "ясно читаемые" макросы, заимствованные из апноута avr035 "Efficient C Coding for AVR" (просто нравятся):

 

Я только вчера узнал, что такое макросы))) Прикольно.

Krendelyok
Offline
Зарегистрирован: 05.10.2016

Как выяснилось, для простого вывода цифр на экран достаточно удалить всё лишнее, и остается вот такой простенький код:


#define SCL_TM1637  3  //Задаем 3 пин, как шину SCL
#define SDA_TM1637  2  //Задаем 2 пин, как шину SDA
int pack;

byte command1 = B01000000; //Команда записи данных в регистр дисплея
byte command2 = B11000000; //Команда "начальный адрес дисплея"

byte symbol_1 = 0x76;//Символ_1
byte symbol_2 = 0x79;//Символ_2
byte symbol_3 = 0x38;//Символ_3
byte symbol_4 = 0x73;//Символ_4

byte command3 = B10001111; //Яркость и ON/OFF дисплея

void setup() {}

void TM1637_start()// просто функция "старт" для протокола I2C
{
   PORTD |= 1<<SCL_TM1637; 
   PORTD |= 1<<SDA_TM1637;
 _delay_us(2);
  PORTD &= ~(1<<SDA_TM1637); 
} 

void TM1637_stop() // просто функция "стоп" для протокола I2C
{
  PORTD &= ~(1<<SCL_TM1637);
 _delay_us(2);
  PORTD &= ~(1<<SDA_TM1637);
_delay_us(2);
  PORTD |= 1<<SCL_TM1637;;
_delay_us(2);
  PORTD |= 1<<SDA_TM1637;
}  


void TM1637_writeByte(int8_t pack){ // служебная функция записи данных по протоколу I2C, с подтверждением (ACK)

  uint8_t i;
    for(i=0;i<8;i++)        
  {
   PORTD &= ~(1<<SCL_TM1637);
    if(pack & 0x01)
  { PORTD |= 1<<SDA_TM1637;}
    else {PORTD &= ~(1<<SDA_TM1637);}
  _delay_us(3);
    pack = pack>>1;      
    PORTD |= 1<<SCL_TM1637;
  _delay_us(3);  
  }  
 
  PORTD &= ~(1<<SCL_TM1637);
  _delay_us(5);
  DDRD &= ~(1<<SDA_TM1637);// если поменяете порт на какой-то другой кроме PORTD, то тут тоже все DDRD на другие DDRx менять надо будет
  while((PIND & (1<<SDA_TM1637))); 
  DDRD |= (1<<SDA_TM1637);
  PORTD |= 1<<SCL_TM1637;
  _delay_us(2);
  PORTD &= ~(1<<SCL_TM1637);  
}


void TM1637_init()// инициализирует переменные, назначает порт и пины для связи с дисплеем, а потом чистит дисплей
{
   DDRD |= (1<<SCL_TM1637) | (1<<SDA_TM1637);
}


void loop() {
TM1637_init();                   //Инициализируем пины SCL и SDA как выходы
TM1637_start(); 
pack = command1;                    //Команда записи в регистр дисплея 
TM1637_writeByte(pack);
TM1637_stop();
TM1637_start();
pack = command2;                   //Команда начального адреса для автоинкремента адреса
TM1637_writeByte(pack);
pack = symbol_1;                   //Символ 1
TM1637_writeByte(pack);
pack = symbol_2;                   //Символ 2
TM1637_writeByte(pack);
pack = symbol_3;                  //Символ 3
TM1637_writeByte(pack);
pack = symbol_4;                   //Символ 4
TM1637_writeByte(pack);
TM1637_stop();
TM1637_start();
pack = command3;                  //Команда, задающая яркость дисплея
TM1637_writeByte(pack);
TM1637_stop();
delay(2000);
}

Очистка дисплея не нужна (для чего она вообще нужна?). Можно еще таблицу-генератор символов оставить из оригинального кода. Вроде как, кто-то жаловался, что стандартная библиотека tm1637.h не везде работает. Интересно, такой код будет работать, скажем, на тиньке 13? ))

Krendelyok
Offline
Зарегистрирован: 05.10.2016

Уф. Осилил много букв. Открыли мне глаза на грамотное программирование)) Это ж этак можно программу своими словами написать, а после расшифровать их в макросах. А главное, читать то как удобно. В принципе, все понял, кроме предыдущего моего вопроса и вот этих строк:

dhog1 пишет:

    #define BITFROMCOMB(x, y) y

    #define C_SETBIT(comb)   SETBIT(VARFROMCOMB(comb),   BITFROMCOMB(comb))
    #define C_CLEARBIT(comb) CLEARBIT(VARFROMCOMB(comb), BITFROMCOMB(comb))
    #define C_CHECKBIT(comb) CHECKBIT(VARFROMCOMB(comb), BITFROMCOMB(comb))

И еще, может, кто попутно даст ссылку, в чем осбенность  записи вида void name(void)? Или unsigned char void tm1637_scan_key(void). А то я немного поизучал Java, а потом подсел на ардуино. А тут всё в куче. Не понятно.

dhog1
Offline
Зарегистрирован: 01.03.2016

unsigned char void tm1637_scan_key(void)  - это ошибка (описка). Разумеется правильно будет unsigned char tm1637_scan_key(void)

dhog1
Offline
Зарегистрирован: 01.03.2016

Krendelyok пишет:

Вот этот момент мне не понятен. По-моему, тут нестыковка с даташитом на атмегу328. На 97 странице есть такая таблица

http://cs636829.vk.me/v636829594/2f006/Ae1kfNW93Ls.jpg Кстати, не совсем понятна третья строчка в этой таблице. Получается, можно сконфигурировать пин как Вход, записать туда единицу, но, записав в PUD = 1, отключить встроенный подтягивающий резистор. Правильно я понимаю, что это состояние не будет отличаться от первой строчки?

Из нее я и сделал вывод. Чтобы начать "слушать" шину SDA, нужно инициализировать пин как вход и Записать в него HIGH. В этом случае мы подключаем встроенный резистор подтяжки. А если запишем LOW, у нас пин перейдет в высокоимпедансное состояние. Тогда нам нужен будет внешний резистор подтяжки. Правильно? Поэтому я и использовал в коде команду записи единицы на вход. Дальше пока не читал, буду разбираться. Наверное, вечером)

Включив внутреннюю подтяжку (DDRx->0, PORTx->1), вы определяете "1" на линии. Если "с другой стороны" прижмут линию к земле, от вас потечет ток, чего "другая сторона" может не ожидать. (Pxn will source current if ext. pulled low. из даташита).

Линии данных и синхроимпульсов для TM163x подтягиваются внешними резисторами, их нормальное состояние "1". Именно они (резисторы) вытягивают линии к "1", когда вы со своей стороны перестаете их прижимать к земле ("0"). Состояние линии данных после сгенерированного восходящего фронта синхроимпульса считывается через соответствующий пин регистра PINx.

Различие между I2C и двухпроводным интерфейсом TM163x не только в отсутствие адресации. TM163x не поддерживает master hold (clock stretching), поэтому зачем китайские товарищи встроили ACK - непонятно. Разве чтобы не превышали скорость обмена. В этом смысле телодвижения по поимке ACK в конце передачи байта выглядят излишними, достаточно дополнительного синхроимпульса. (Однако при считывании байта командой scan_key выдать ACK прийдется).

 

Krendelyok
Offline
Зарегистрирован: 05.10.2016

dhog1 пишет:

Включив внутреннюю подтяжку (DDRx->0, PORTx->1), вы определяете "1" на линии. Если "с другой стороны" прижмут линию к земле, от вас потечет ток, чего "другая сторона" может не ожидать. (Pxn will source current if ext. pulled low. из даташита).

Так ток потечет точно так же и через внешние резисторы, если линия подтянута к ним. Тут суть не меняется. Просто меняется расположение резисторов. Другое дело, если на линии много устройств, и не у всех есть встроенные резисторы. Тогда логично использовать внешние резисторы подтяжки. А в случае, когда один мастер - не вижу особого смысла во внешних резисторах.

У вас в коде, кстати, инициализация выполняет очистку дисплея. Какой-либо дополнительной функции в ней нет. Как видно из вырезанного мной кода, все работает без очистки дисплея. В блоке инициализации ничего не осталось, кроме установки пинов в режим output. Что не работало в моей программе , пока не разбирался)) Вроде как, делал я все правильно. Наверное, какой то рассинхрон  в использовании таймера2)

dhog1 пишет:

Различие между I2C и двухпроводным интерфейсом TM163x не только в отсутствие адресации. TM163x не поддерживает master hold (clock stretching), поэтому зачем китайские товарищи встроили ACK - непонятно. Разве чтобы не превышали скорость обмена. В этом смысле телодвижения по поимке ACK в конце передачи байта выглядят излишними, достаточно дополнительного синхроимпульса. (Однако при считывании байта командой scan_key выдать ACK прийдется).

Я думал, что ACK здесь также, как и в полноценном I²C, выполняет роль контроля ошибок. Не пришел ACK после отправки байта - отправляем программу обрабатывать ошибку. В вырезанном из библиотеки коде, кстати, мы видим фрагмент кода с while, где ожидается сигнал ACK (строка 55). Но в нем нет аварийного выхода из while. Тоесть, если ACK не придет, то наша сборка просто зависнет. Может быть, это и вызывает у многих проблему?

P.S. Да, вот здесь уже писали о доработке фрагмента с while http://arduino.ru/forum/programmirovanie/modul-4-led-na-draiver-chipe-tm1637#comment-222621

dimam
Offline
Зарегистрирован: 08.02.2017

Только вот функция void tm1637_write(uint16_t num, uint8_t dp) вместо того чтоб вывести число 1234, выводит 4321. Ну и точку ставит не там где указано.

dimam
Offline
Зарегистрирован: 08.02.2017

Нужно в функции вывода положительного числа

void tm1637_write(uint16_t num, uint8_t dp)

выводить число из массива digs[_TM1637_DIGS_ATTACHED] начиная с digs[3], тогда на дисплей все правильно выводится и точка ставится там где указано. Кстати, а как вывести отрицательное число на дисплей?

Абыр Валг
Offline
Зарегистрирован: 04.01.2018

Доброго вечера всем.

Предыстория. Отец попросил сделать ему "часы с белыми цифрами и чтоб яркость регулировалась", единственное что-то доступное с белыми цифрами- это вот выписаный с Али китайский модуль на tm1637. И яркость как раз умеет. Кстати, кто не вкурсе- модули бывают двух типов: "для часов" с центральными точками, и "приборные" с децимальными- многие по ошибке заказывают "часовые", а потом жалуются что децимальные точки не светят.  После того как я почитал ДШ- увидел что микросхема еще и клавиши опрашивать умеет!.. а на модуле их нет... а у меня tiny13- ножек мало... а кнопок надо пять штук...  посему- готовый модуль был безжалостно раздерган на детальки. После разводки платы  встал вопрос об управлении всем хозяйством, бо для удобства разводки моя схема весьма сильно отличалась от даташитовской. Соотвественно, стандартные библиотеки использовать невозможно, кроме того- я вообще использую Code Vision (привык).

Хочу выразить огромную благодарность пользователю dhog1 !  За предоставленные примеры функций, бо я уже весь инфернет перерыл и отчаялся, а тут такой подарок! Тем не менее, мне они показались неудобны, поскольку я больше "железячник" чем "программист", и использование дефайнов и макросов до сих пор вызывает жуткую неприязнь- неудобны мне они, путаюсь! Ладно б сам писал, а тут еще и чужие... Соотвественно, для себе подобных выкладываю "переделаные" в максимально железячный вид примеры функций с максимально подробными комментариями (писал для себя- то есть как для идиота).  Функции совершенно точно рабочие- часы уже стоят на столе и показывают что положено. Коды нажатых клавиш полностью соответствуют кодам по ДШ (если кнопки включены по ДШ- между линиями K и SG)- то есть считывание происходит корректно. Господ программистов убедительно прошу не плеваться что не по-феншую. Надеюсь, кому-нибудь это пригодится- при необходимости крайне легко перепилить под свой проект. Для Code Vision AVR

//PORTB.0 - data
//PORTB.1 - clock
//unsigned char bright=0b10001000;     //яркость дисплея. От 0b10001000 до 0b10001111
//unsigned char keydata=0;                 //результат опроса кнопок

void tm1637_start(void)  //старт передачи данных
{
PORTB.0=0;    //нога данных в землю- начало передачи по стандарту i2c (гыгы :)
delay_us(2);     //задержечка чтоб slave-устройство отреагировало, не обязательна
PORTB.1=0;    //клок в землю. Технически с этого начинается уже функция передачи данных, но так экономится память.
}

void tm1637_stop(void)   //завершение передачи данных
{
PORTB.0=0;      //нога данных в землю (клок в земле с предыдущей функции)
delay_us(2);    //задержка, определяющая скорость передачи данных здесь и далее (по ДШ- до 600 КГц, тут 500 КГц)
PORTB.1=1;      //клок подтянут;
delay_us(2);
PORTB.0=1;      //переход ноги данных с нуля в подтяг при высоком клоке- завершение передачи по стандарту i2c (гыгы :)
}

void tm1637_write(unsigned char databyte)  //запись байта в чип
{
unsigned char i;
for (i=0; i<8; i++)            //8 раз
{
if (databyte&0b1) PORTB.0=1;  //если младший разряд- единичка, нога данных подтянута
else PORTB.0=0;              //иначе в землю;
databyte>>=1;                //сдвигаем число на разряд влево
PORTB.1=1;                     //клок подтянут
delay_us(2);                 //формируем такт клока
PORTB.1=0;                     //клок в землю где он был с начала
};
PORTB.0=0;             //нога даты в землю чтоб во время ACK не спалить порт                   
PORTB.1=1;             //клок подтянут
delay_us(2);         //ACK low level в конце передачи байта- такт клока по ДШ 
PORTB.1=0;               //клок в землю;
}

void tm1637_keyscan(void)  //опрос клавиш
{
unsigned char i;
tm1637_start();
tm1637_write(0b01000010); //команда считывания состояния клавиш
DDRB=0b010110;               //ставим PORTB.0 как вход
PORTB.0=1;                       //подтягиваем (pull-up)
for (i=0; i<8; i++)        //8 раз
{                               
keydata<<=1;              //сдвиг числа на разряд
delay_us(50);             //задержка на опрос (по ДШ- до 250 КГц (4 мкс), но если меньше 40- не работает)
if (PINB.0) keydata|=0b1;  //передача данных в процессор: если на ноге единичка- пишем единичку в младший разряд
PORTB.1=1;                  //подтяг клока
delay_us(2);              //формируем такт клока
PORTB.1=0;                  //клок в землю где он был с начала
}                            
PORTB.1=1;           //клок подтянут;
delay_us(2);       //ACK low level в конце передачи байта- такт клока по ДШ (можно проверить ногу даты- должна быть на земле)
PORTB.1=0;           //клок в землю;
DDRB=0b010111;            //ставим PORTB.0 обратно как выход
tm1637_stop();
}

void tm1637_bright(void)  //установка яркости
{
tm1637_start();
tm1637_write(bright);     
tm1637_stop();
}