Для дома для семьи задумал я сваять двухканальный дешифратор энкодеров с семисегментной индикацией. Достал контроллер atmega8 и стал кодить его на С... Практически все закодил, только одно "но" повлекло за собой трудности, которые с разбегу решить не удалось. По порядку.
Поскольку портов в данном контроллере мало, а должен он сканировать 2 энкодера, выдавать 3-битный код на 4 аналоговых мультиплексора и показывать текущее значение режима на 2 4х-разрядных семисегментных индикатора, было принято решение значения индикации сохранять в 2 8-разрядных последовательных регистра 74хх164 и стробировать. Получившаяся функция записи данных в регистр выглядит так:
Код
static inline void WriteLedValue(uint8_t value)
{
for (uint8_t i = 0; i < 8; i++) // толкаем 8 раз одну цифру
{
CBI(STR); // строб в 0
if (value & 0x1)
{
SBI(LED); // если последняя 1, устанавливаем бит
}
else
{
CBI(LED); // иначе стираем
}
SBI(STR); // формируем фронт строба для записи в регистр
value >>= 1; // первый разряд протолкнули, сдвигаем
}
}
{
for (uint8_t i = 0; i < 8; i++) // толкаем 8 раз одну цифру
{
CBI(STR); // строб в 0
if (value & 0x1)
{
SBI(LED); // если последняя 1, устанавливаем бит
}
else
{
CBI(LED); // иначе стираем
}
SBI(STR); // формируем фронт строба для записи в регистр
value >>= 1; // первый разряд протолкнули, сдвигаем
}
}
Однако одна итерация составляет 14 тактов, что в сумме дает 14*16 = 224 такта. При условии, что тактовая частота процессора планируется 1МГц, а частота опроса энкодеров и она же частота стробирования - 1кГц, получается, что добрая 1/3 цикла свечения тратится тупо на проталкивание бит...
Я было подумал, что при написании кода на асме получилось бы максимум 3 такта на цикл (сдвиг, проверка регистра переноса, запись бита в порт), но умные люди посоветовали для этой задачи использовать готовое решение - SPI. Посмотрев расположение пинов, я пришел к выводу, что использовать классический SPI мне будет неудобно, а вот если перейти на Atmega88, то можно задействовать USART в режиме SPI. И вот тут начались непонятные казусы...
До этого я весь код писал в AVR Studio 5 + Proteus, проблем не было. При попытке же симулировать данный порт в режиме SPI получается странная штуковина: первый вызов функции в режиме отладки в студии проскакивает влет, без задержки на проверку опустошения буфера, срабатывает только при повторном. А в Протеусе последняя запись выдается всегда ПЕРВОЙ
Код
static inline void WriteLedValue(uint8_t value)
{
while (!(UCSR0A & (1<<UDRE0)));
UDR0 = value;
while (!(UCSR0A & (1<<UDRE0)));
}
{
while (!(UCSR0A & (1<<UDRE0)));
UDR0 = value;
while (!(UCSR0A & (1<<UDRE0)));
}
Вызов выглядит так
Код
SBI(_CS); // гасим индикаторы
DIG(digit); // выставляем адрес разряда индикации
CBI(_RS); // сбрасываем регистры
SBI(_RS); // надеюсь, достаточно 1 такта
WriteLedValue(Symbols[0]); // записываем значения в регистр
WriteLedValue(Symbols[1]);
CBI(_CS); // зажигаем индикаторы
DIG(digit); // выставляем адрес разряда индикации
CBI(_RS); // сбрасываем регистры
SBI(_RS); // надеюсь, достаточно 1 такта
WriteLedValue(Symbols[0]); // записываем значения в регистр
WriteLedValue(Symbols[1]);
CBI(_CS); // зажигаем индикаторы
При таком раскладе у меня сначала проталкивается пустой символ, затем "2". В результате на индикаторах горит 2 в первом разряде одного канала и ничего - во втором. При следующей итерации во втором разряде первого канала горит 0, как и положено, а во втором канале горит "2", которая должна была гореть на предыдущей итерации... Методом научного тыка нашел решение:
Код
WriteLedValue(Symbols[0]); // записываем значения в регистр
WriteLedValue(Symbols[1]);
WriteLedValue(0); // хрень какая-то, но по-другому не симулируется
WriteLedValue(Symbols[1]);
WriteLedValue(0); // хрень какая-то, но по-другому не симулируется
При таком раскладе первым проталкивается 0 (!!!), хотя вызов идет третьим, затем первый канал и второй в нужном порядке. Но так получается лишние 16+ тактов на пропихивание "пустышки", что не есть тру...
Конфигурация порта такая:
Код
// Init ports as outputs
DDRB |= 0x3F;
DDRC |= 0x3F;
DDRD |= 0x3F;
// Init timer
TIMSK0 |= 1<<TOIE0; // timer int enable
TCCR0B |= 1<<CS01 | 0<<CS00; // start timer, divider 64
// int each 1ms
// Init USART
UBRR0 = 0; // максимальная скорость Fclk/2
UCSR0C = 1<<UMSEL01 | 1<<UMSEL00 | 1<<UCPHA0 | 1<<UCPOL0 | 0<<UDORD0; // конфигурируем USART в режим SPI mode 0
UCSR0B = 1<<TXEN0; // включение пина передатчика, приемник при этом используется как порт В/В
UBRR0 = 0;
DDRB |= 0x3F;
DDRC |= 0x3F;
DDRD |= 0x3F;
// Init timer
TIMSK0 |= 1<<TOIE0; // timer int enable
TCCR0B |= 1<<CS01 | 0<<CS00; // start timer, divider 64
// int each 1ms
// Init USART
UBRR0 = 0; // максимальная скорость Fclk/2
UCSR0C = 1<<UMSEL01 | 1<<UMSEL00 | 1<<UCPHA0 | 1<<UCPOL0 | 0<<UDORD0; // конфигурируем USART в режим SPI mode 0
UCSR0B = 1<<TXEN0; // включение пина передатчика, приемник при этом используется как порт В/В
UBRR0 = 0;
Помогите, пожалуйста, я уже весь мозг сломал...
Для большей понятности выкладываю два примера со схемками в Протеусе.
encoders_atm8_v5 - это как должно работать идейно, но с неоптимальным временем записи
encoders_atm88_v5 - это то, что я пытаюсь сделать с помощью SPI. Сейчас там используется 3 вызова функции, в чем можно убедиться, активировав осцилл и прерывание на ноге 1 U3:A