Jump to content

    

Опрос прибора при помощи ATmega128 и MAX485.

Всем добрый вечер!

 

Заранее хочу извиниться за длинный пост, но мне кажется, чем подробнее я всё опишу, тем будет лучше и для меня, и для тех, кто захочет мне помочь.

 

Я делаю небольшую систему на основе микроконтроллера ATmega128, которая должна опросить электронный вольтметр по интерфейсу RS485, получить пакет, разобрать его, и на основе полученного значения выполнить некоторые действия. В качестве интерфейсной микросхемы я выбрал MAX485, но в данный момент времени я провожу свои
эксперименты не на "голой" микросхеме, а на ардуиновском модуле (делаю я это по одной простой причине: на этом модуле уже есть резисторные подтяжки на линиях связи, и это всяко лучше, чем я бы ставил эти резисторы на макетную плату вокруг микросхемы). 

 

Вывод RXD0 микросхемы подключён к входу модуля R0, а вывод TXD0 - к входу DI. Входы DE и RE модуля закоротил паяльником и подключил к выводу PE5. Ниже представлен код, который я пытаюсь заставить работать:

#define F_CPU 8000000

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include <avr/eeprom.h>

void uart_0_init();
void uart_0_send(uint8_t data);
void call_voltage();

uint8_t buffer[9];  //Буфер для хранения ответа
int counter = 0;    //Счётчик принятых байт

void uart_0_init()   //Инициализация UART
{
	UBRR0H = 0;
	UBRR0L = 103; //Скорость 9600 бод, 
	UCSR0B |= (1<<RXCIE0)|(1<<TXCIE0)|(1<<RXEN0)|(1<<TXEN0); //Приём и передача разрешены, прерывания тоже
	UCSR0C |= (1<<UCSZ00)|(1<<UCSZ01);
	UCSR0A |= (1<<U2X0);
}

void call_voltage()
{
	uart_0_send(0x01);  //Отправка сообщения на прибор (опрос регистра 0x0006 и 0x0007)
	uart_0_send(0x04);
	uart_0_send(0x00);
	uart_0_send(0x06);
	uart_0_send(0x00);
	uart_0_send(0x02);
	uart_0_send(0x91);
	uart_0_send(0xCA);
	
	eeprom_write_byte(0x00,buffer[0]);  //Запись в EEPROM ответа от прибора
	eeprom_write_byte(0x01,buffer[1]);
	eeprom_write_byte(0x02,buffer[2]);
	eeprom_write_byte(0x03,buffer[3]);
	eeprom_write_byte(0x04,buffer[4]);
	eeprom_write_byte(0x05,buffer[5]);
	eeprom_write_byte(0x06,buffer[6]);
	eeprom_write_byte(0x07,buffer[7]);
	eeprom_write_byte(0x08,buffer[8]);
}

ISR(USART0_RX_vect)   //Прерывание по приёму байт
{
	uint8_t data = UDR0;   
	buffer[counter++] = data;  //Запись принятого байта в массив
}

ISR(USART0_TX_vect)  //Прерывание по отправке байта из UART
{
	PORTE &= ~(1<<PORTE5); //Установить на PE5 низкий уровень (разрешение приёма)
}

void uart_0_send(uint8_t data)  //Функция отправки байта
{
	PORTE &= ~(0<<PORTE5);      //Установка на E5 выскокого уровня (разрешение отправки)
	
	UDR0 = data;
	while(!(UCSR0A & (1 << TXC0)) );
}

int main(void) 
{
	uart_0_init(); //Инициализация UART
	sei();         //Разрешение глобальных прерываний
	
	DDRE &=~ (0<<PORTE5); //Настраиваем PE5 на выход (управление приёмом и передачей MAX485)
	
	_delay_ms(1000);
	
	call_voltage(); //Отправка сообщения на прибор
		
	while (1)
	{
		
	}
}

Как на мой взгляд всё это должно работать: на UART отправляется байт данных, перед этим на управляющий вывод подаётся высокий уровень. Когда байт покинул 
буфер, срабатывает прерывание по отправке, и на управляющий вывод подаётся низкий уровень, т.е. "0". Так происходит до конца отправки всего сообщения.
Далее прибор должен прислать ответ, прерывание по приёму байт должно занести принятые данные в массив, после чего они все запишутся в EEPROM.

 

Прибор принимает мой пакет, и даже отсылает ответ, это точно (во время процедуры обмена данными на его лицевой панели начинает мигать соответствующий значок), но 
только в том случае, если в функции call_voltage() отсутствуют функции записи данных в eeprom. Стоит их добавить, и посылка не отправляется (индикация на приборе не горит).

 

Сразу хочу сообщить о вещах, которые мне показались очень странными:

 

1) Посылку отправляю именно таким образом, потому что если использовать что-то вроде этого:

while(i < 8)
{
	uart_0_send(databuffer[i]);
	i++;
}

то посылка попросту не отправится, либо отправится только её первый байт. 

 

2) Если после инструкции DDRE &=~ (0<<PORTE5); не поставить задержку на 1000 ms, или поставить на меньшее время, посылка не отправится.

 

3) Отправка посылки не будет работать, если функцию call_voltage() поместить в цикл while(1) с задержкой после каждого вызова (я это проверил при помощи
переходника USB -> RS485).

 

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

Edited by Stolbov
Не понравились отступы между абзацами.

Share this post


Link to post
Share on other sites

Научитесь сначала общаться с вольтметром (Вы не сказали его имя...) с помощью терминальной программы на ПК.

Share this post


Link to post
Share on other sites
8 минут назад, Tanya сказал:

Научитесь сначала общаться с вольтметром (Вы не сказали его имя...) с помощью терминальной программы на ПК.

О, с этим проблем нету. Модель вольтметра: PD194UI-9K4T. Я подключал его через переходник к ПК, и опрашивал его при помощи его фирменной программы. Так же я его опрашивал при помощи терминала: 

Screenshot_4.jpg

Edited by Stolbov

Share this post


Link to post
Share on other sites
1 hour ago, Stolbov said:

PORTE &= ~(0<<PORTE5);  //Установка на E5 выскокого уровня (разрешение отправки)

Меня очень смущает эта строка. Она 100% делает совсем не то, что вы хотите. Ещё точнее, она не изменяет значения PORTE.

1 hour ago, Stolbov said:

DDRE &=~ (0<<PORTE5); //Настраиваем PE5 на выход (управление приёмом и передачей MAX485)

И эта строка - то же самое. Не изменяет значения DDRE.

Edited by Darth Vader

Share this post


Link to post
Share on other sites
1 hour ago, Stolbov said:

Разумеется, этот код ужасен

Разумеется... :-(

PORTE &= ~(1<<PORTE5); //Установить на PE5 низкий уровень (разрешение приёма)

Тут правильно.

PORTE &= ~(0<<PORTE5);      //Установка на E5 выскокого уровня (разрешение отправки)

А тут должно быть PORTE |= (1<<PORTE5);

И, по-хорошему, это должно быть двумя микро-функциями Drv485_TxEn() и Drv485_RxEn()

 

1 hour ago, Stolbov said:

на UART отправляется байт данных, перед этим на управляющий вывод подаётся высокий уровень. Когда байт покинул 
буфер, срабатывает прерывание по отправке, и на управляющий вывод подаётся низкий уровень, т.е. "0"

Мне не нравится эта идея. Хотя бы потому, что в ATmega есть ДВА регистра в UART'е - data register, куда записываются данные из программы, и shift register, из которого собственно идёт отправка. Вы вот можете поручиться, что прерывание по отправке первого байта сработает ДО того, как в буфер попадёт второй байт?

Я, правда, не сильно понял, как вообще работает бит TXC. Если верить документации, он автоматически сбрасывается прерыванием. Т.е., непонятно, можно ли в принципе надёжно опрашивать его в основном цикле при включённом соотв. прерывании.

Миллион лет не видел атмегу и никогда не видел rs485, но предлагаю следующий алгоритм: запрещаем прерываение по передаче; включаем передатчик MAX485, отправляем свои 8 байт, ждём TX Complete, выключаем передатчик. Предварительно читаем в даташите о необходимости заранее сбросить флаг TXC.

 

1 hour ago, Stolbov said:

прерывание по приёму байт должно занести принятые данные в массив, после чего они все запишутся в EEPROM

Вот на фразе "после чего" у меня возникли некоторые сомнения. В коде нет никакого "после чего" - отправили данные, и сразу же записали в еепром всякий мусор.

 

1 hour ago, Stolbov said:

1) Посылку отправляю именно таким образом, потому что если использовать что-то вроде этого:

Что-то удивительное творится. А что такое databuffer и чему равна j в начале цикла?

 

1 hour ago, Stolbov said:

если функцию call_voltage() поместить в цикл while(1)

...то мы сразу же наступаем на грабли с битовыми масками (см. в начале моего сообщения).

 

А вообще - что вам помешало сразу прицепить параллельно ваш вольтметр, атмегу и usb-485 ? Хоть какая-то отладка...

 

 

PS вопрос больше к общественности. В modbus сразу можно регистры устройства читать, или при появлении его в сети его надо как-то сконфигурировать? Может, дополнительно ещё и с этим проблемы?

Edited by esaulenka

Share this post


Link to post
Share on other sites

Приветствую!

 

Из того, что сразу бросилось в глаза (важное и не очень).

1. buffer[], counter без квалификатора volatile. Почему?

2. DDRE |= 1 << 5 и PORTE |= 1 << 5 вместо соответствующих сдвигов нулей.

3. Большинство проблем, впрочем, возможно, уйдет после выполнения требований п. 2.

4. Ваш код линеен - запись данных в EEPROM происходит без ожидания реального получения этих данных. Почему?

5. counter в прерывании инкрементируется без ограничений. Надо бы сбросить в начальное значение при достижении нужного уровня.

 

По-хорошему, нужно организовать отправку строки-запроса, а потом ожидать в течение конечного времени ответа.

Ответ собирать по байтам в кольцевой буфер, а в суперцикле while(1) разгребать эти данные и анализировать их.

 

6 минут назад, esaulenka сказал:

...Я, правда, не сильно понял, как вообще работает бит TXC...

Скорее всего не тот случай - есть еще бит UDRE, вот он взводится по событию проталкивания байта из регистра хранения в сдвиговый.

А TXE - как раз по окончанию передачи из сдвигового при отсутствии готовых к отправке в регистре хранения UDR.

Share this post


Link to post
Share on other sites
11 minutes ago, Arlleex said:

А TXE - как раз по окончанию передачи из сдвигового при отсутствии готовых к отправке в регистре хранения UDR

TXC. TXCIE - это разрешение соотв. прерывания, только что в памяти освежал. Но тут вопрос чуть другой - что сработает первым - опрос в while'е этого флажка, или прерывание с автоматическим его сбросом. Как-то не выглядит надёжно (хотя... никогда не интересовался временем реакции контроллера прерываний атмеги. Может быть, там с запасом... :-) ).

Share this post


Link to post
Share on other sites
8 минут назад, esaulenka сказал:

TXC. TXCIE - это разрешение соотв. прерывания, только что в памяти освежал...

Да, писал по памяти, опечатка:prankster2:

А по поводу одновременного доступа к биту: действительно, выглядит не очень надежным.

Share this post


Link to post
Share on other sites

Доброе утро!

18 часов назад, esaulenka сказал:

Вот на фразе "после чего" у меня возникли некоторые сомнения. В коде нет никакого "после чего" - отправили данные, и сразу же записали в еепром всякий мусор.

Возможно, это очень "профановское" отношение, но мне показалось, что до первой записи в eeprom мк успеет принять байты по прерыванию и записать их в массив. Возможно, мне просто надо было сделать проверку переменной counter. Если она равна 9 (количество байт в ожидаемой посылке), то тогда можно смело писать данные в память.

18 часов назад, esaulenka сказал:

Что-то удивительное творится. А что такое databuffer и чему равна j в начале цикла?

Здесь мне надо было написать более подробно. Извините. Прекрасно понимаю, что телепатии не существует) Вообщем, если передачу пакета оформить вот так:

//...
uint8_t databuffer[8] = {0x01,0x04,0x00,0x06,0x00,0x02,0x91,0xCA};
int i = 0;

while(i < 8)
{
	uart_0_send(databuffer[i]);
	i++;
}
//...

То пакет не будет отправлен, либо будет отправлен только первый байт. В чём причина этого, мне не известно.

18 часов назад, esaulenka сказал:

А вообще - что вам помешало сразу прицепить параллельно ваш вольтметр, атмегу и usb-485 ? Хоть какая-то отладка...

Попробую сделать так, как вы сказали. И да, конкретно в моём вольтметре нужно было настроить параметры передачи данных (количество байт данных, стоповых бит, скорость), что я и сделал.

 

P.S. Подключил, всё получилось. Красной линией отмечена моя посылка (прочитать регистры 6 и 7), а синей - ответ от прибора. Зелёным прямоугольником отметил текущее напряжение в формате float.

Опыт.jpg

Edited by Stolbov

Share this post


Link to post
Share on other sites

Доброе утро!

14 часов назад, Arlleex сказал:

1. buffer[], counter без квалификатора volatile. Почему?

Не думал, что это важно, поэтому квалификатор не использовал. 

14 часов назад, Arlleex сказал:

2. DDRE |= 1 << 5 и PORTE |= 1 << 5 вместо соответствующих сдвигов нулей.

Простите, я немного не понял, что Вы имеете ввиду. Разве PORTE |= 1<<5 не установит сигнал высокого уровня на PORTE5?

14 часов назад, Arlleex сказал:

4. Ваш код линеен - запись данных в EEPROM происходит без ожидания реального получения этих данных. Почему?

Я почему-то считал, что до момента записи первого байта в eeprom микроконтроллер успеет заполнить по прерываниям массив для входных данных, но теперь я считаю, что это ошибка. Сейчас я хочу попробовать начать проверять переменную counter в течении некоторого времени (если она станет равной 9 до истечении времени проверки, значит, посылка пришла, и её можно разбирать).

14 часов назад, Arlleex сказал:

5. counter в прерывании инкрементируется без ограничений. Надо бы сбросить в начальное значение при достижении нужного уровня.

Сделал так:

ISR(USART0_RX_vect)
{
	uint8_t data;
	
	if(counter > 9)
		counter = 0;
	else
	{
		data = UDR0;
		buffer[counter++] = data;	
	}
}

 

Share this post


Link to post
Share on other sites
3 hours ago, Stolbov said:

uint8_t databuffer = {0x01,0x04,0x00,0x06,0x00,0x02,0x91,0xCA};

Здесь надо uint8_t databuffer[8] = {0x01,0x04,0x00,0x06,0x00,0x02,0x91,0xCA};

Share this post


Link to post
Share on other sites
3 hours ago, Stolbov said:

Разве PORTE |= 1<<5 не установит сигнал высокого уровня на PORTE5?

Установит.
А вот Ваш вариант PORTE &= ~(0<<PORTE5) - нет

Share this post


Link to post
Share on other sites

mcheb, опечатался. Спасибо.

Share this post


Link to post
Share on other sites

Думаю, стоит рассказать, что у меня получилось.

1) Вольтметр теперь подключен не только к микроконтроллеру через MAX485, но и к компьютеру через переходник USB -> RS485. Таким образом я отслеживаю, что вольтметр получил, и что отдал.

2) Изменил функцию отправки сообщения на вольтметр, хотя в целом принцип там остался тот же, плюс исправил некоторые недочёты, на которые мне указали ранее.

3) Нашёл интересную статью по реализации кольцевого буфера на AVR. Воспользовавшись наработками пользователя, я получил следующий код:

#define F_CPU 8000000

//Состояния для автомата функции call_voltage().
#define SEND_ADDR_DEV 1
#define SEND_CODE_FNC 2
#define SEND_ADDR_HGT 3
#define SEND_ADDR_LOW 4
#define SEND_NUMB_HGT 5
#define SEND_NUMB_LOW 6
#define SEND_CRC_HIGH 7
#define SEND_CRC_LOW 8
#define EEPROM_WRITE 9

#define BUFFER_SIZE 18 //Максимальный размер кольцевого буфера (на две принятые посылки)
#define BUFFER_MASK (BUFFER_SIZE - 1) //Маска обнуления 

//Библиотеки
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include <avr/eeprom.h>

unsigned char buffer_u0[BUFFER_SIZE]; //Массив, выполняющий роль кольцевого буфера
unsigned char IndexStart;             //Старотовый индекс
unsigned char IndexEnd;               //Конечный индекс

void uart_0_init();                   //Инициализация UART0 (MAX485) 
void uart_1_init();                   //Инициализация UART1 для отладки 
void uart_0_send(uint8_t data);       //Отправка байта на вольтметр
void uart_1_send(uint8_t data);       //Отправка байта на ПК
void call_voltage();                  //Отправка посылки на вольтметр

unsigned char write_byte_in_ring(unsigned char byte);                //Функция записи в кольцевой буфер
unsigned char index_number(void);                                    //Функция, возвращающая количество непрочитанных байт из буфера                 
unsigned char get_data(void);                                        //Проверка, есть данные в буфере, или нет
void read_byte_from_ring(unsigned char *str, unsigned char length);  //Чтение ответа из кольцевого буфера в local_buffer

int global_state = 0;  //Переменная для хранения состояний в call_voltage

unsigned char write_byte_in_ring(unsigned char byte)
{
	IndexEnd++;                       //Инкремент конечного индекса.
	if(IndexEnd == IndexStart)        //Если конечный и стартовый индекс станут равны, то
		return 1;                     //прекращаем запись. Нам не нужно, чтобы чтобы новые данные затёрли старые.
	IndexEnd &= BUFFER_MASK;          //Накладываем маску для обнуления, если индекс перевалил за 18.
	buffer_u0[IndexEnd] = byte;       //Записываем байт в массив.
	return 0;                         //Возвращаем 0 в знак того, что запись прошла успешно.
}

unsigned char index_number(void) //Функция возвращает количество непрочитанных данных в буфере
{
	if(IndexEnd >= IndexStart)
	{
		return (IndexEnd - IndexStart);  //Возвращаем количество свободного места в кольцевом буфере
	}
	else
	{
		return ((BUFFER_SIZE - IndexStart) + IndexEnd); 
	}
}

unsigned char get_data(void)
{
	if(IndexEnd != IndexStart)  //Если индексы не равны, то
		return 1;               //Вернуть "1" (данные есть)
	return 0;                   //Иначе ничего не принято
}

void read_byte_from_ring(unsigned char *str, unsigned char length) //Чтение пришедших по UART данных из кольцевого буфера в local_buffer.
{
	char i;
	for(i = 0; i < length; i++)
	{
		IndexStart++;
		IndexStart &= BUFFER_MASK;
		*str = buffer_u0[IndexStart];
		str++;
	}
}

void uart_1_send(uint8_t data)
{
	while(!(UCSR1A & (1 << UDRE1)));
	UDR1 = data;
}

ISR(USART0_RX_vect)
{
	write_byte_in_ring(UDR0); //Записываем принятые данные в кольцевой буфер
}


void uart_0_init()
{
	UBRR0H = 0;
	UBRR0L = 103; //103 - 9600 бод
	UCSR0B |= (1<<RXCIE0)|(1<<TXCIE0)|(1<<RXEN0)|(1<<TXEN0); //Приём и передача разрешены, прерывания тоже
	UCSR0C |= (1<<UCSZ00)|(1<<UCSZ01);
	UCSR0A |= (1<<U2X0);
}

void uart_1_init()
{
	UBRR1H = 0;         
	UBRR1L = 103; 
	UCSR1B |= (1<<RXCIE1)|(1<<TXCIE1)|(1<<RXEN1)|(1<<TXEN1);
	UCSR1C |= (1<<UCSZ10)|(1<<UCSZ11);
	UCSR1A |= (1<<U2X1);
}

ISR(USART0_TX_vect)
{
	PORTE &= ~(1<<PORTE5); //Настраиваем MAX485 на приём данных
}

void uart_0_send(uint8_t data)
{
	PORTE |= (1<<PORTE5);  //Настраиваем MAX485 на отправку данных
	UDR0 = data;
	while(!(UCSR0A & (1 << TXC0)));
}

void call_voltage() //Отправка сообщения на вольтметр
{
	uint8_t flag = 0x01;
	int global_state = SEND_ADDR_DEV;
	
	while(flag != 0x00)
	{
		switch(global_state)
		{
			case SEND_ADDR_DEV:
				uart_0_send(0x01);
				global_state = SEND_CODE_FNC;
			break;
			
			case SEND_CODE_FNC:
				uart_0_send(0x04);
				global_state = SEND_ADDR_HGT;
			break;
			
			case SEND_ADDR_HGT:
				uart_0_send(0x00);
				global_state = SEND_ADDR_LOW;
			break;
			
			case SEND_ADDR_LOW:
				uart_0_send(0x06);
				global_state = SEND_NUMB_HGT;
			break;
			
			case SEND_NUMB_HGT:
				uart_0_send(0x00);
				global_state = SEND_NUMB_LOW;
			break;
			
			case SEND_NUMB_LOW:
				uart_0_send(0x02);
				global_state = SEND_CRC_HIGH;
			break;
			
			case SEND_CRC_HIGH:
				uart_0_send(0x91);
				global_state = SEND_CRC_LOW;
			break;
			
			case SEND_CRC_LOW:
				uart_0_send(0xCA);
				flag = 0x00;
			break;
			
			case EEPROM_WRITE:
			break;
		}
	}
}

int main(void)
{
	uart_0_init();  //Инициализация UART0
	uart_1_init();  //инициализация UART1
	sei();
	
	unsigned char local_buffer[9]; //Локальный буфер для хранения принятого пакета 
	
	DDRE |= (1<<PORTE5); //Управляющий пин настраиваем на выход
	
	_delay_ms(1000);
		
	call_voltage(); //Отсылаем данные на вольтметр
			
	while (1)
	{

		if(get_data() == 0) //Если функция вернула 0, значит, в массиве нет новых данных
		{
			asm("nop");     //И тогда мы ничего не делаем
		}
		else if(get_data() == 1) //Если функция вернула 1, значит, какие-то данные всё же получены
		{
			read_byte_from_ring(local_buffer,index_number()); //Читаем полученные от вольтметра данные в local_buffer
		}
		else
		{
			break;
		}
		uart_1_send(local_buffer[0]);   //Отсылаем данные из local_buffer на ПК
	}
}

Что получилось:

1) После старта МК через UART на ПК лезет мусор, но потом появляется ответ от вольтметра, после чего на ПК отсылается последний принятый байт от вольтметра (это уже недостаток моего кода).

2) Через переходник USB -> RS485 я вижу тот же самый ответ, который я получил от вольтметра при помощи МК.

Что вызвало вопросы:

1) Я полагал, что функция read_byte_from_ring полностью заполнит local_buffer, но не уверен, что это происходит, так как на пк отсылается только первый элемент массива - local_buffer[0], и, не смотря на это, на ПК я вижу полный полученный от вольтметра пакет. Может быть, я просто ещё не до конца понимаю, как это работает.

2) Попытка изменить программу таким образом, чтобы на ПК отсылалась полученная от вольтметра посылка каждые пять секунд, успехом не увенчалась. Когда я помещаю в супер-цикл функцию call_voltage, в своём ипровизированном отладчике я вижу, что микроконтроллер отправил только самый первый байт запроса: "0x01". Больше не происходит ничего.

Буду рад получать идеи и замечания. Я уверен, что цель уже совсем близко. Результаты опыта на скриншоте:

 

Отредактировать.jpg

Share this post


Link to post
Share on other sites

1. Реализация кольцевого буфера корявая. Но гораздо менее корявая, чем остальной код.

2. К чему весь этот ужас с switch() вместо передачи в цикле из массива?

3. Цикл while(1) в main постоянно валит в UART данные, вне зависимости от того, есть что-то в буфере, или нет. Тот самый "мусор".

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now