Довольно часто программисту встраиваемых систем приходится работать с протоколом Modbus RTU или Modbus через Ethernet. Это клиент-серверный протокол. На встраиваемом устройстве может работать как клиент (шлет запросы), так и сервер (отвечает на запросы).
Далее приведено описание моей реализации modbus сервера (slave) для микроконтроллеров (в принципе ее можно использовать и на ПК).
Для полного понимания, как работает modbus можно почитать этот документ-Modbus RTU. Кроме того, для подсчета CRC16 для modbus RTU можно использовать этот алгоритм.
Команды, которые будут использоваться:
- 03 — запрос данных группы регистров;
- 06 — запись данных в один регистр;
- 16 — запись данных в группу регистров;
Для программной реализации modbus создадим структуры:
// группа регистров - последовательно расположенные регистры
// каждая группа отделена промежутком modbus адресов и имеет свой уровень доступа
#define NUMBER_GROUP 4 // количество групп регистров
#define NUMBER_GROUP_4 1
typedef struct
{
uint32_t rxcnt; // количество принятых байт в modbus буфере
uint8_t buffer[256]; // modbus буфер для хранения, анализа и модификации modbus данных
uint16_t tr_len; // количество байт для передачи из modbus буфера
uint8_t crc_error; // флаг указывает об ошибке CRC
uint16_t adr_end[NUMBER_GROUP]; // массив содержащий конечный адрес каждой группы регистров
uint16_t adr_begin[NUMBER_GROUP]; // массив содержащий начальный адрес каждой группы регистров
uint8_t not_read_flag[NUMBER_GROUP]; // у каждой группы регистров есть разрешение/запрещение чтения ее
uint8_t not_write_flag[NUMBER_GROUP]; // у каждой группы регистров есть разрешение/запрещение записи в нее
uint8_t adr_dev; // modbus адрес устройства
} modbus_init;
// физические адреса (в памяти микроконтроллера) modbsu групп регистров
typedef struct
{
uint8_t* pB;
} adr_block_s;
// сами modbus переменные разбитые на группы регистров
#pragma pack(push, 1) // для выравнивания по 1 байту (!!обязательно!!)
typedef struct
{
uint16_t x0;
uint32_t y0;
} group_0_s;
#pragma pack(pop)
#pragma pack(push, 1) // для выравнивания по 1 байту (!!обязательно!!)
typedef struct
{
uint16_t x1;
uint32_t y1;
} group_1_s;
#pragma pack(pop)
#pragma pack(push, 1) // для выравнивания по 1 байту (!!обязательно!!)
typedef struct
{
uint16_t x2;
uint32_t y2;
} group_2_s;
#pragma pack(pop)
#pragma pack(push, 1) // для выравнивания по 1 байту (!!обязательно!!)
typedef struct
{
uint16_t x3;
uint32_t y3;
} group_3_s;
#pragma pack(pop)
В самом начале необходимо проинициализировать modbus (заполнить все поля modbus структуры, которые этого требуют):
// начальные адреса modbus групп
#define ADR_GROUP_0 1000
#define ADR_GROUP_1 1010
#define ADR_GROUP_2 1013
#define ADR_GROUP_3 1016
modbus_init modbus;
adr_block_s adr_block[NUMBER_GROUP];
group_0_s group_0;
group_1_s group_1;
group_2_s group_2;
group_3_s group_3;
// флаги запрещения/разрешения записи и чтения каждой группы начиная с 1
// 1 2 3 4
#define NOT_ADR_READ {0,0,0,0} // 1 - запрет чтения
// 1 2 3 4
#define NOT_ADR_WRITE {0,0,0,0} // 1 - запрет записи
#define ADR_DEVICE 1 // modbus адрес устройства
void init_modbus(void)
{
uint8_t i;
uint16_t adr_begin[4] = {ADR_GROUP_0,
ADR_GROUP_1, ADR_GROUP_2, ADR_GROUP_3};
uint8_t not_read_flag[4] = NOT_ADR_READ;
uint8_t not_write_flag[4] = NOT_ADR_WRITE;
modbus.adr_dev = ADR_DEVICE;
for(i = 0; i < NUMBER_GROUP; i++)
{
modbus.adr_begin[i] = adr_begin[i];
modbus.not_read_flag[i] = not_read_flag[i];
modbus.not_write_flag[i] = not_write_flag[i];
}
// рассчитываем конец modbus адресов для каждой группы
// и указываем физические адреса переменных для каждой группы
#ifdef NUMBER_GROUP_1
modbus.adr_end[0] = sizeof(group_0)/2 + modbus.adr_begin[0];
adr_block[0].pB = (uint8_t*)&group_0;
#endif
#ifdef NUMBER_GROUP_2
modbus.adr_end[0] = sizeof(group_0)/2 + modbus.adr_begin[0];
modbus.adr_end[1] = sizeof(group_1)/2 + modbus.adr_begin[1];
adr_block[0].pB = (uint8_t*)&group_0;
adr_block[1].pB = (uint8_t*)&group_1;
#endif
#ifdef NUMBER_GROUP_3
modbus.adr_end[0]= sizeof(group_0)/2 + modbus.adr_begin[0];
modbus.adr_end[1]= sizeof(group_1)/2 + modbus.adr_begin[1];
modbus.adr_end[2]= sizeof(group_2)/2 + modbus.adr_begin[2];
adr_block[0].pB = (uint8_t*)&group_0;
adr_block[1].pB = (uint8_t*)&group_1;
adr_block[2].pB = (uint8_t*)&group_2;
#endif
#ifdef NUMBER_GROUP_4
modbus.adr_end[0] = sizeof(modbus_temp.group_0)/2 + modbus.adr_begin[0];
modbus.adr_end[1] = sizeof(modbus_temp.group_1)/2 + modbus.adr_begin[1];
modbus.adr_end[2] = sizeof(modbus_temp.group_2)/2 + modbus.adr_begin[2];
modbus.adr_end[3] = sizeof(modbus_temp.group_3)/2 + modbus.adr_begin[3];
adr_block[0].pB = (uint8_t*)&(modbus_temp.group_0);
adr_block[1].pB = (uint8_t*)&(modbus_temp.group_1);
adr_block[2].pB = (uint8_t*)&(modbus_temp.group_2);
adr_block[3].pB = (uint8_t*)&(modbus_temp.group_3);
#endif
}
Вам необходимо написать драйвер для вашего физического канала (обычно RS-232, RS-485), который будет принимать modbus сообщения и разделять их (сообщение должно начинаться и заканчиваться интервалом тишины, длительностью не менее 3,5 символов при данной скорости передачи; Во время передачи сообщения не должно быть пауз длительностью более 1,5 символов).
Когда мы приняли по физическому каналу modbus сообщение, вызывается функция modbus_slave, которая переписывает их в modbus буфер, проверяет CRC в зависимости от тех или иных результатов вызывает ту или иную дальнейшую обработку (command_3, command_6, command_16, error).
void modbus_slave(void)
{
uint8_t tmp_2;
uint16_t tmp;
uint8_t number_buf;
// тем или иным способом копируем все modbus сообщение в modbus.buffer
// если у вас сразу копируется в этот буфер пропустите ее
copy_to_modbus_byf();
// проверка что идет запрос к нашему modbus адресу
if( modbus.buffer[0] == modbus.adr_dev )
{
tmp = crc16(modbus.buffer, (modbus.rxcnt - 2)); // расчет CRC16 (см. ст. ...)
// CRC16 - 2 байта, разделяем байты для сравнения
tmp_2 = tmp >> 8; // старший байт
tmp = tmp & 0x00ff; // младший байт
// сравнение расчетного CRC и CRC в сообщение
if( (tmp == modbus.buffer[modbus.rxcnt - 1]) && (tmp_2 == modbus.buffer[modbus.rxcnt - 2]))
{
// 2 байт в сообщении это команада - проверка команды
switch(modbus.buffer[1])
{
case 3: // запрос данных группы регистров
command_3();
break;
case 6: // запись данных в один регистр
command_6();
break;
case 16: // запись данных в группу регистров
command_16();
break;
default:
error(); // неправильная команда
}
}
// CRC не верный
else
modbus.tr_len = 0;
modbus.crc_error = 1;
}
// не верный modbus адрес устройства - ответа нет
else
{
modbus.tr_len = 0;
modbus.crc_error = 1;
}
}
Поскольку обработка команд выполняется практически по идентичному алгоритму, рассмотрим только обработку command_3:
void command_3(void)
{
uint16_t reg_l; // младший байт начального адреса регистра
uint16_t reg_h; // старший байт начального адреса регистра
uint16_t reg; // адрес регистра, вычисляется по reg_h, reg_l
uint16_t N_reg; // количество байт для записи в modbus регистры (из modbus шапки)
uint8_t i;
uint16_t n;
uint16_t m;
uint8_t offset;
uint16_t tmp_val_h; // для копирования из буфера
uint16_t tmp_val_l; // для копирования из буфера
uint16_t crc;
uint16_t crc_h;
uint16_t crc_l;
// начальный адрес регистра с которого читать
reg_h = modbus.buffer[2];
reg_l = modbus.buffer[3];
reg = (reg_h << 8) + reg_l;
// 6й байт в modbus сообщении содержит количество считываемых регистров
N_reg = modbus.buffer[5];
// проверка возможности прочитать регистры
for(i = 0; i <= NUMBER_GROUP; i++)
{
if(reg >= modbus.adr_begin[i] && reg <= modbus.adr_end[i])
break;
}
// если нет такого адреса в группах регистров или запрещено чтение
if(i == NUMBER_GROUP || modbus.not_read_flag[i] == 1 ||
(reg + N_reg) > modbus.adr_end[i])
{
error();
}
// все нормально - читаем регистры
else
{
m = 3; // данные в ответа modbus начинаются с 4 байта поэтому m=3
offset = 0;
reg = reg - modbus.adr_begin[i];
for(n = 0; n < N_reg; n = n + 1)
{
// получаем физический адрес
// 1 регистр 2 байта поэтому reg умножаем на 2 и считываем 2 последовательных байта
tmp_val_h = *(adr_block[i].pB + reg*2 + offset);
tmp_val_l = *(adr_block[i].pB + reg*2 + offset + 1);
offset = offset + 2;
// переписываем обратно в modbus буфер
modbus.buffer[m] = tmp_val_l;
modbus.buffer[m+1] = tmp_val_h;
// переписали 2 байта -> прибавляем 2 для смещения в буфере
m = m + 2;
}
n = n * 2; // количество считанных байт данных
modbus.tr_len = n + 5; // количество считанных байт данных + 5 служебных байт
modbus.buffer[2] = n; // 3й байт при ответе, это количество передаваемых байт
// расчет CRC16 для передаваемых данных
crc = crc16(modbus.buffer, (modbus.tr_len - 2) );
crc_h = crc >> 8;
crc_l = crc & 0x00ff;
// переписываем CRC16 в буфер для передачи
modbus.buffer[modbus.tr_len - 2] = crc_h;
modbus.buffer[modbus.tr_len - 1] = crc_l;
}
}
После того как мы сформировали буфер modbus.buffer для ответа его нужно отправить обратно клиенту (master’у) по физическому каналу.
P.S. ___ Код выше собран из двух моих проектов (в каждом из них много лишнего для объяснения, и я это повыбрасывал для простоты понимания), и его я не тестировал (не запускал именно такой код), поэтому в нем могут быть ошибки. Но для общего понимания, как просто и быстро реализовать modbus RTU в самый раз.