1、DISCOVERY_COMMAND(0x10)&& DISCOVERY...RESPONSE(0x11)
2、GET_COMMAND(0x20) && GET...RESPONSE(0x21)
3、SET_COMMAND(0x30) && SET...RESPONSE(0x33)
一、RDM硬件配置
RDM协议是基于DMX512协议扩展而来的,不了解DMX512协议也没关系,本文会从零开始介绍RDM协议。
首先RDM底层是串口通信,不同的是RDM协议为保证数据的可靠传输,需要将串口产生的信号转化为RS485差分信号,接收端再将RS485差分信号转化为串口信号。这里就有一个问题,转换后的RS485差分信号是半双工的,而RDM协议是双向通信的,所以需要进行收/发模式切换,一般需要一个额外的控制引脚进行模式切换,以 SIT3088E为例。
实际应用中一般把/RE DE物理上连接到一起组合成控制引脚,通过控制引脚的高低电平来控制信号的接收于发送。
二、RDM软件配置
既然知道了RDM协议底层是串口,那么只需要按照串口的配置方法来配置即可。官方规定需要250000的波特率,1位起始位,8位数据位,2位停止位。这些按照标准要求来配置即可。以下是HAL库配置代码,因为RDM的时序与DMX时序相同,所以函数命名为DMX。
//TXD1 -> PB6
//RXD1 -> PB7
//RDE -> PB5 //低电平接收使能 //高电平输出使能
//DMX+RDM+UID初始化
void MU_DMX_Init(void)
{
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_USART1_CLK_ENABLE();
GPIO_InitTypeDef GPIO_Init;
GPIO_Init.Pin = GPIO_PIN_5;
GPIO_Init.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOB,&GPIO_Init);
GPIO_Init.Pin = GPIO_PIN_6;
GPIO_Init.Mode = GPIO_MODE_AF_PP;
GPIO_Init.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_Init.Alternate = GPIO_AF2_USART1;
HAL_GPIO_Init(GPIOB,&GPIO_Init);
GPIO_Init.Pin = GPIO_PIN_7;
GPIO_Init.Mode = GPIO_MODE_AF_OD;
GPIO_Init.Alternate = GPIO_AF2_USART1;
HAL_GPIO_Init(GPIOB,&GPIO_Init);
//APB2
huart1.Instance = USART1;
huart1.Init.BaudRate = 250000;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_2;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_8;
HAL_UART_Init(&huart1);
HAL_NVIC_SetPriority(USART1_IRQn,0,2);
HAL_NVIC_DisableIRQ(USART1_IRQn);
RDM_Uid_Init();
}
宏定义
#define DMX512_RECV HAL_GPIO_WritePin(GPIOB,GPIO_PIN_5,GPIO_PIN_RESET)
#define DMX512_SEND HAL_GPIO_WritePin(GPIOB,GPIO_PIN_5,GPIO_PIN_SET)
RDM_Uid_Init 可以先不管,下一篇会介绍。初始化一遍是不够的,还需要在运行过程中产生起始信号,复用成串口后无法产生起始信号,所以需要改变GPIO口的模式,这些代码在介绍完RDM位时序后再贴上。
三、RDM位时序
RDM的时序与DMX512相同,由BREAK和MAB组成起始信号
BREAK:88us ~ 1s的低电平
MAB: 8us ~ 1s的高电平
后面接的就是串口输出的数据,规定最多255帧。
显而易见,按照官方要求配置的串口,一个帧的时间是44us,不能产生起始信号。所以需要改变GPIO口的模式,将TX改成推挽输出,产生起始信号后再复用成串口。
模式切换函数如下
enum dmx_mode {DMX_SEND_RESET,DMX_SEND_DATA,DMX_RECV_DATA,DMX_RECV_RESET};
void MU_DMX_Change_Mode(enum dmx_mode mode)
{
GPIO_InitTypeDef GPIO_Init;
//DMX发送
if(mode == DMX_SEND_DATA)
{
DMX512_SEND;
HAL_UART_DeInit(&huart1);
GPIO_Init.Pin = GPIO_PIN_6;
GPIO_Init.Mode = GPIO_MODE_AF_PP;
GPIO_Init.Alternate = GPIO_AF2_USART1;
HAL_GPIO_Init(GPIOB,&GPIO_Init);
HAL_UART_Init(&huart1);
}
//DMX接收
else if(mode == DMX_RECV_DATA)
{
DMX512_RECV;
// HAL_UART_DeInit(&huart1);
GPIO_Init.Pin = GPIO_PIN_7;
GPIO_Init.Mode = GPIO_MODE_AF_OD;
GPIO_Init.Alternate = GPIO_AF2_USART1;
HAL_GPIO_Init(GPIOB,&GPIO_Init);
// HAL_UART_Init(&huart1);
HAL_NVIC_EnableIRQ(USART1_IRQn);
HAL_UART_Receive_IT(&huart1,&rx_buf.byte,1);
}
//DMX输出起始信号
else if(mode == DMX_SEND_RESET)
{
DMX512_SEND;
// HAL_UART_DeInit(&huart1);
GPIO_Init.Pin = GPIO_PIN_6;
GPIO_Init.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOB,&GPIO_Init);
}
//DMX读取起始信号
// else if(mode == DMX_RECV_RESET)
// {
// DMX512_RECV;
// // HAL_UART_DeInit(&huart1);
// GPIO_Init.Pin = GPIO_PIN_7;
// //下降沿中断
// GPIO_Init.Mode = GPIO_MODE_IT_RISING;
// HAL_GPIO_Init(GPIOB,&GPIO_Init);
// HAL_NVIC_EnableIRQ(EXTI9_5_IRQn);
// //中断回调函数在encoder.c
// }
}
实测读取RDM数据包的时候,串口会把起始信号读成一个字节的数据“0x00”,解析数据包时把这个数据丢掉即可,不需要切换GPIO输入模式读取。
中断回调函数如下
//接收数组定义
typedef struct rx_buffer_t
{
uint8_t byte;
uint8_t buf[2048];
uint8_t index;
}rx_buffer_t;
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart1);
}
//串口接收回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
rx_buf.buf[rx_buf.index++] = rx_buf.byte;
if(rx_buf.index == 2048)
rx_buf.index = 0;
HAL_UART_Receive_IT(huart,&rx_buf.byte,1);
}
}
//串口错误回调
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
__HAL_UART_CLEAR_FLAG(&huart1,UART_FLAG_FE);
HAL_UART_Receive_IT(&huart1,&rx_buf.byte,1);
}
}
采用双缓冲区,可以缓冲至多8个RDM数据包,但是会频繁进入中断。数据包解析函数下一篇介绍。
四、RDM设备及UID
首先要明确RDM协议主要是运用在舞台相关设备的管理的,RDM协议中主要有两种设备,一种是控制器,另一种是接收器,就是舞台设备。控制器用于寻找当前环境中所有的舞台设备,并标记每个设备,同时辅助DMX协议进行DMX地址设置。
明显的UID是用于唯一标识每台设备,所有支持标准RDM协议的设备都有唯一的UID。
UID一共占六个字节,前两个字节是厂商ID,后四个字节是设备ID。如果是学习可以自己定义UID
如果是实际生产必须向官方申请厂商ID,然后可自己定义设备ID。
五、RDM包结构
下面是RDM协议标准的包结构
START:起始字节,与起始信号不是同一个概念,固定为0xCC
Sub-START:固定0x01
Message Length:包的长度,从起始字节计数到末尾,注意不算最后两位校验位
Destination UID:目的地UID,包要发给哪个设备就填哪个设备的UID
Source UID:自己的UID,告诉目标设备是哪个控制器发送的。
15~19:可以不管,填0x00 0x01 0x00 0x00 0x00
Command Class:命令类型,下一模块介绍,占一字节
Parameter ID:命令ID,占二字节
Parameter Data Length:数据长度,仅计算下面要发的数据的长度,占一字节
Paramter Data:不定长数据
Checksum High:校验位高位
Checksum Low:校验位低位,校验位由起始字节逐个加到末尾计算而成,不加校验位本身
六、RDM命令
RDM有六类18种数据包,除一个特殊包外都上面的标准包结构。不用怕多,发送的数据包和接收的数据包是对应的。发送的是三类九种。接收的也是三类九种。
1、DISCOVERY_COMMAND(0x10)&& DISCOVERY...RESPONSE(0x11)
搜寻设备包与搜寻设备回应包
(1)DISC_UNIQUE_BRANCH(0x0001) && 特殊回应包
用于搜寻网络中的所有设备,收到这个包的设备会回应一个特殊的回应包
发送数据格式:6位低UID和6位高UID,数据长度0x0C
特别的需要将目的地UID设置成0xFFFFFFFFFFFF,这样所有设备都需要处理这个包,收到这个包的设备需要判断自己的UID是否在低UID和高UID之间。如果在就回应一个特殊的回应包,至于为什么搞这么复杂是因为信道只有一个,所有设备同时回应必然会产生冲突,需要使用二分查找算法来逐个确认设备。下一篇再介绍算法
回应数据格式:没有起始信号,起始字节不是0xCC,格式是连续7字节0xFE和一个间隔字节0xAA,然后是设备的UID,每字节分别 | 0xAA 和 | 0x55 再传输。校验和是将12位处理后的UID依次相加,得到的值再分别 | 0xAA 和 | 0x55,所以是4位校验位。(校验位不要加1~8字节)
(2)DISC_MUTE(0x0002) && 哑音回应包
用于静默设备,收到这个包的设备将进入静默状态,只处理解除哑音包
发送数据格式:无数据,只有命令,数据长度要填0x00
接收数据格式:无数据,只有命令,数据长度要填0x00
(2)UN_UN_MUTE(0x0003) && 解除哑音回应包
与上面哑音包相同,只有命令格式不同
2、GET_COMMAND(0x20) && GET...RESPONSE(0x21)
(1)IDENTIFY_DEVICE(0x1000) && 获取状态回应包
用于获取设备状态
发送数据格式:无数据,只有命令,数据长度要填0x00
接收数据格式:一个字节的设备状态信息
(2)DMX...ADDRESS(0x00F0) && 获取DMX地址回应包
用于获取设备DMX地址
发送数据格式:无数据,只有命令,数据长度要填0x00
接收数据格式:两个字节的设备DMX地址
(3)...VERSION...(0x00C0) && 获取软件版本回应包
用于获取设备软件版本
发送数据格式:无数据,只有命令,数据长度要填0x00
接收数据格式:0到231个字节的字符串数据
(4)DEVICE_INFO(0x0060) && 获取设备信息回应包
用于获取设备信息
发送数据格式:无数据,只有命令,数据长度要填0x00
接收数据格式:0到231个字节的字符串数据
3、SET_COMMAND(0x30) && SET...RESPONSE(0x33)
(1)IDENTIFY_DEVICE(0x1000) && 设置状态回应包
用于设置设备状态,当用户想知道一个设备在舞台中的哪个位置时通过控制台发送这个数据包,收到这个包的设备根据自身硬件资源告知用户在哪个位置,比如是灯光设备就亮灯。
发送数据格式:一个字节的设备状态信息,数据长度0x01
接收数据格式:无数据
(2)DMX...ADDRESS(0x00F0) && 设置DMX地址回应包
用于设置设备DMX地址,RDM协议的主要功能
发送数据格式:两个字节的DMX地址,数据长度0x02
接收数据格式:无数据
七、RDM数据结构定义
1、UID结构定义
uint64_t 有八字节数据,UID占低六字节
//UID链表定义(单向循环链表)
typedef struct uid_t
{
uint64_t uid;
struct uid_t *next;
}uid_t;
UID添加删除操作
/******************UID链表配置函数******************/
//初始化UID链表
static void RDM_Uid_Init(void)
{
uid_list = (uid_t *)malloc(sizeof(uid_t));
//头节点存厂商ID
uid_list->uid = 0x0000010203040506;
uid_list->next = NULL;
}
//头插法加UID
static void RDM_Uid_Add(uint64_t uid)
{
uid_t* new_uid = (uid_t *)malloc(sizeof(uid_t));
new_uid->uid = uid;
new_uid->next = uid_list->next;
uid_list->next = new_uid;
}
//删除指定UID
static void RDM_Uid_Del(uint64_t uid)
{
uid_t *p = uid_list;
uid_t *q = uid_list->next;
while(q != NULL)
{
if(q->uid == uid)
{
p->next = q->next;
free(q);
break;
}
q=q->next;
}
}
//销毁链表
static void RDM_Uid_Distory(void)
{
uid_t *p = uid_list->next;
while(p != NULL)
{
uid_list->next = p->next;
free(p);
p = uid_list->next;
}
free(uid_list);
}
2、包结构定义
typedef struct rdm_package_t
{
uint8_t start; //第0位 起始码
uint8_t sub_start; //第1位 起始码
uint8_t message_len; //第2位 包长(不含校验位)
uint64_t des_uid; //第3到8位 目的地UID
uint64_t source_uid; //第9到14位 源UID
uint64_t other; //第15到19位 无关紧要的数据
uint8_t cmd_type; //第20位 命令类型
uint16_t cmd; //第21到22位 命令
uint8_t data_len; //第23位 数据长度
uint8_t *data; //23位 往后数据
uint32_t check; //两位校验位 (之所以是uint32_t 是为了防止计算溢出)
}rdm_package_t;
对包的初始化,设置目的地UID,设置命令,设置数据,发送包的操作
/******************组包函数******************/
//初始化包
static void RDM_Package_Init(rdm_package_t *package)
{
package->start = 0xCC;
package->sub_start = 0x01;
package->source_uid = uid_list->uid;
package->other = 0x0000000001000000;
package->check = 0;
}
//设置目的地UID
static void RDM_Package_Set_Uid(rdm_package_t *package,uint64_t uid)
{
package->des_uid = uid;
}
//设置命令
static void RDM_Package_Set_Cmd(rdm_package_t *package,uint8_t cmd_type,uint16_t cmd)
{
package->cmd_type = cmd_type;
package->cmd = cmd;
}
//设置完data后要在内存释放前发送数据
static void RDM_Package_Set_Data(rdm_package_t *package,uint8_t data_len,uint8_t data[])
{
package->data_len = data_len;
package->data = data;
}
//计算包长 校验位
//然后发送数据
static void RDM_Package_Send(rdm_package_t *package)
{
//24 + data_len
package->message_len = 24+package->data_len;
uint8_t *buf = (uint8_t *)malloc(package->message_len+2);
if(buf == NULL)
LED1_ON;
buf[0] = package->start;
buf[1] = package->sub_start;
buf[2] = package->message_len;
buf[3] = (package->des_uid >> 40) & 0xFF;
buf[4] = (package->des_uid >> 32) & 0xFF;
buf[5] = (package->des_uid >> 24) & 0xFF;
buf[6] = (package->des_uid >> 16) & 0xFF;
buf[7] = (package->des_uid >> 8) & 0xFF;
buf[8] = package->des_uid & 0xFF;
buf[9] = (package->source_uid >> 40) & 0xFF;
buf[10] = (package->source_uid >> 32) & 0xFF;
buf[11] = (package->source_uid >> 24) & 0xFF;
buf[12] = (package->source_uid >> 16) & 0xFF;
buf[13] = (package->source_uid >> 8) & 0xFF;
buf[14] = package->source_uid & 0xFF;
buf[15] = (package->other >> 32) & 0xFF;
buf[16] = (package->other >> 24) & 0xFF;
buf[17] = (package->other >> 16) & 0xFF;
buf[18] = (package->other >> 8) & 0xFF;
buf[19] = package->other & 0xFF;
buf[20] = package->cmd_type;
buf[21] = (package->cmd >> 8) & 0xFF;
buf[22] = package->cmd & 0xFF;
buf[23] = package->data_len;
uint8_t j = 0;
for(int i = 24; i < package->message_len; i++)
{
buf[i] = package->data[j++];
}
//计算校验和
for(int i = 0; i < package->message_len; i++)
{
package->check += buf[i];
}
buf[package->message_len] = (package->check >> 8) & 0xFF;
buf[package->message_len+1] = package->check & 0xFF;
//起始信号
MU_DMX_Change_Mode(DMX_SEND_RESET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_6,GPIO_PIN_RESET);
HAL_Delay(1);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_6,GPIO_PIN_SET);
HAL_Delay(1);
//发送包
MU_DMX_Change_Mode(DMX_SEND_DATA);
HAL_UART_Transmit(&huart1,buf,package->message_len+2,HAL_MAX_DELAY);
free(buf);
MU_DMX_Change_Mode(DMX_RECV_DATA);
/* 实测可以不用 */
//只有广播包回应特殊
// if(package->cmd_type == 0x10 && package->cmd == 0x0001)
// {
// //转为接收态
// MU_DMX_Change_Mode(DMX_RECV_DATA);
// }
// else
// {
// //接收电平变化
// MU_DMX_Change_Mode(DMX_RECV_RESET);
// }
}
3、示例代码
以发送搜寻设备包为例
uint8_t RDM_Disc_Driver(uint64_t low_uid,uint64_t high_uid)
{
rdm_package_t package = {0};
RDM_Package_Init(&package);
//广播地址
RDM_Package_Set_Uid(&package,0x0000FFFFFFFFFFFF);
//寻找设备
RDM_Package_Set_Cmd(&package,0x10,0x0001);
//设置数据
uint8_t data[12];
data[0] = (low_uid >> 40) & 0xFF;
data[1] = (low_uid >> 32) & 0xFF;
data[2] = (low_uid >> 24) & 0xFF;
data[3] = (low_uid >> 16) & 0xFF;
data[4] = (low_uid >> 8) & 0xFF;
data[5] = low_uid & 0xFF;
data[6] = (high_uid >> 40) & 0xFF;
data[7] = (high_uid >> 32) & 0xFF;
data[8] = (high_uid >> 24) & 0xFF;
data[9] = (high_uid >> 16) & 0xFF;
data[10] = (high_uid >> 8) & 0xFF;
data[11] = high_uid & 0xFF;
RDM_Package_Set_Data(&package,0x0C,data);
RDM_Package_Send(&package);
HAL_Delay(40);
//解包
rdm_package_prase_t package_prase = RDM_Package_Prase();
if(package_prase.rdm_package == ERROR_PACKAGE)
return 0;
else
return 1;
}
总结
恭喜你看完了RDM协议的基础篇,现在你已经能组建RDM协议包并发送了,但是对于设备的搜寻和回应包的解析还没有熟练,下一篇会带你了解这两个步骤,本篇到此结束。