前言
很多人不知道,电脑和显示器之间除了传输图像信号,还有一条"隐藏"的双向通信通道。通过这条通道,电脑可以:
- 读取/调节显示器亮度、对比度
- 切换输入源
- 控制显示器电源开关
- 调节显示器内置扬声器音量
- 获取显示器型号、序列号等信息
这背后的技术就是DDC/CI 和CEC协议。今天把这套技术从协议原理到代码实现整理一遍。
HDMI接口通信架构
┌─────────────────────────────────────────────────────────────────────────────┐
│ HDMI接口通信通道 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ HDMI接口包含多个独立通信通道: │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ TMDS通道 (单向: 电脑→显示器) │ │
│ │ ════════════════════════════════════════════════════════════ │ │
│ │ - TMDS Data 0/1/2: RGB视频数据 │ │
│ │ - TMDS Clock: 像素时钟 │ │
│ │ - 带宽: 最高48Gbps (HDMI 2.1) │ │
│ │ │ │
│ ├─────────────────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ DDC通道 (双向: I2C总线) │ │
│ │ ════════════════════════════════════════════════════════════ │ │
│ │ - SCL (Pin 15): I2C时钟 │ │
│ │ - SDA (Pin 16): I2C数据 │ │
│ │ - 用途: EDID读取、DDC/CI控制 │ │
│ │ - 速率: 100kHz标准模式 │ │
│ │ │ │
│ ├─────────────────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ CEC通道 (双向: 单线总线) │ │
│ │ ════════════════════════════════════════════════════════════ │ │
│ │ - CEC (Pin 13): Consumer Electronics Control │ │
│ │ - 用途: 设备间控制 (电源、音量、输入切换) │ │
│ │ - 速率: 约400bps │ │
│ │ │ │
│ ├─────────────────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ HPD信号 (单向: 显示器→电脑) │ │
│ │ ════════════════════════════════════════════════════════════ │ │
│ │ - Hot Plug Detect (Pin 19) │ │
│ │ - 检测显示器连接/断开 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ HDMI Type-A 引脚定义: │
│ │
│ ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │10 │11 │12 │13 │14 │15 │16 │17 │18 │19 │
│ └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─ HPD
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─ +5V
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─ GND
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─ SDA (DDC)
│ │ │ │ │ │ │ │ │ │ │ │ │ │ └─ SCL (DDC)
│ │ │ │ │ │ │ │ │ │ │ │ │ └─ CEC ← 控制通道
│ │ │ │ │ │ │ │ │ │ │ │ └─ TMDS Clock-
│ │ │ │ │ │ │ │ │ │ │ └─ TMDS Clock Shield
│ │ │ │ │ │ │ │ │ │ └─ TMDS Clock+
│ └───┴───┴───┴───┴───┴───┴───┴───┴───┴─ TMDS Data 0/1/2 (+/-/Shield)
│ │
└─────────────────────────────────────────────────────────────────────────────┘
DDC/CI协议详解
DDC/CI概述
┌─────────────────────────────────────────────────────────────────────────────┐
│ DDC/CI协议架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ DDC = Display Data Channel (显示数据通道) │
│ CI = Command Interface (命令接口) │
│ │
│ DDC协议族: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ DDC1: 最早版本,仅读取EDID │ │
│ │ └→ 显示器在SCL线上持续输出EDID数据 │ │
│ │ │ │
│ │ DDC2B: 使用I2C协议读取EDID │ │
│ │ └→ 从机地址: 0x50 (EDID EEPROM) │ │
│ │ │ │
│ │ DDC2Bi: DDC2B + 可选的双向通信 │ │
│ │ │ │
│ │ DDC/CI: 完整的双向命令接口 ← 我们关注的重点 │ │
│ │ └→ 从机地址: 0x37 (DDC/CI控制) │ │
│ │ └→ 基于MCCS标准 (Monitor Control Command Set) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 通信模型: │
│ │
│ ┌──────────┐ I2C总线 ┌──────────┐ │
│ │ │ ───────────────→ │ │ │
│ │ 电脑 │ SCL + SDA │ 显示器 │ │
│ │ (主机) │ ←─────────────── │ (从机) │ │
│ │ │ │ │ │
│ └──────────┘ └──────────┘ │
│ │
│ I2C地址分配: │
│ - 0x50 (0xA0/0xA1): EDID数据 │
│ - 0x37 (0x6E/0x6F): DDC/CI控制 │
│ - 0x30 (0x60/0x61): 扩展显示信息 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
MCCS VCP代码
c
/**
* MCCS (Monitor Control Command Set) VCP代码定义
*
* VCP = Virtual Control Panel (虚拟控制面板)
* 每个控制项有唯一的VCP代码
*/
// 常用VCP代码
typedef enum {
// 亮度/对比度控制
VCP_BRIGHTNESS = 0x10, // 亮度 (0-100)
VCP_CONTRAST = 0x12, // 对比度 (0-100)
VCP_BACKLIGHT = 0x13, // 背光亮度
// 颜色控制
VCP_RED_GAIN = 0x16, // 红色增益
VCP_GREEN_GAIN = 0x18, // 绿色增益
VCP_BLUE_GAIN = 0x1A, // 蓝色增益
VCP_COLOR_TEMP = 0x14, // 色温预设
VCP_COLOR_TEMP_REQUEST = 0x0B, // 色温请求
// 几何控制
VCP_HORIZONTAL_POSITION = 0x20, // 水平位置
VCP_VERTICAL_POSITION = 0x30, // 垂直位置
VCP_HORIZONTAL_SIZE = 0x22, // 水平尺寸
VCP_VERTICAL_SIZE = 0x32, // 垂直尺寸
// 输入控制
VCP_INPUT_SOURCE = 0x60, // 输入源选择
VCP_AUDIO_SPEAKER_VOLUME = 0x62, // 扬声器音量 ← 音量控制
VCP_AUDIO_MUTE = 0x8D, // 静音
VCP_AUDIO_TREBLE = 0x8F, // 高音
VCP_AUDIO_BASS = 0x91, // 低音
VCP_AUDIO_BALANCE = 0x93, // 声道平衡
// 电源控制
VCP_POWER_MODE = 0xD6, // 电源模式
// OSD控制
VCP_OSD_LANGUAGE = 0xCC, // OSD语言
VCP_OSD = 0xCA, // OSD开关
// 显示器信息
VCP_DISPLAY_CONTROLLER_TYPE = 0xC8, // 控制器类型
VCP_DISPLAY_FIRMWARE_LEVEL = 0xC9, // 固件版本
VCP_VERSION = 0xDF, // MCCS版本
// 工厂设置
VCP_RESTORE_FACTORY_DEFAULT = 0x04, // 恢复出厂设置
VCP_RESTORE_FACTORY_BRIGHTNESS = 0x05, // 恢复亮度默认
VCP_RESTORE_FACTORY_CONTRAST = 0x06, // 恢复对比度默认
VCP_RESTORE_FACTORY_COLOR = 0x08, // 恢复颜色默认
// 其他
VCP_DEGAUSS = 0x01, // 消磁 (CRT)
VCP_NEW_CONTROL_VALUE = 0x02, // 新控制值标志
VCP_SOFT_CONTROLS = 0x03, // 软控制
} VCPCode_t;
// 输入源定义 (VCP 0x60的值)
typedef enum {
INPUT_VGA1 = 0x01,
INPUT_VGA2 = 0x02,
INPUT_DVI1 = 0x03,
INPUT_DVI2 = 0x04,
INPUT_COMPOSITE1 = 0x05,
INPUT_COMPOSITE2 = 0x06,
INPUT_SVIDEO1 = 0x07,
INPUT_SVIDEO2 = 0x08,
INPUT_TUNER1 = 0x09,
INPUT_TUNER2 = 0x0A,
INPUT_TUNER3 = 0x0B,
INPUT_COMPONENT1 = 0x0C,
INPUT_COMPONENT2 = 0x0D,
INPUT_COMPONENT3 = 0x0E,
INPUT_DP1 = 0x0F,
INPUT_DP2 = 0x10,
INPUT_HDMI1 = 0x11,
INPUT_HDMI2 = 0x12,
} InputSource_t;
// 电源模式定义 (VCP 0xD6的值)
typedef enum {
POWER_ON = 0x01,
POWER_STANDBY = 0x02,
POWER_SUSPEND = 0x03,
POWER_OFF = 0x04,
POWER_OFF_BUTTON = 0x05,
} PowerMode_t;
DDC/CI通信协议
c
/**
* DDC/CI数据包格式
*/
/*
DDC/CI消息格式:
发送命令 (主机→显示器):
┌──────┬──────┬────────┬──────────┬──────────┬──────────┐
│ 目标 │ 源 │ 长度 │ 操作码 │ 数据 │ 校验和 │
│ 地址 │ 地址 │ │ │ │ │
├──────┼──────┼────────┼──────────┼──────────┼──────────┤
│ 0x6E │ 0x51 │ 0x80+n │ opcode │ data... │ XOR │
└──────┴──────┴────────┴──────────┴──────────┴──────────┘
接收响应 (显示器→主机):
┌──────┬──────┬────────┬──────────┬──────────┬──────────┐
│ 目标 │ 源 │ 长度 │ 操作码 │ 数据 │ 校验和 │
│ 地址 │ 地址 │ │ │ │ │
├──────┼──────┼────────┼──────────┼──────────┼──────────┤
│ 0x6F │ 0x6E │ 0x80+n │ opcode │ data... │ XOR │
└──────┴──────┴────────┴──────────┴──────────┴──────────┘
操作码:
- 0x01: VCP Request (读取VCP值)
- 0x02: VCP Reply (VCP值响应)
- 0x03: VCP Set (设置VCP值)
- 0x06: Timing Request
- 0x07: Timing Reply
- 0x0C: Save Current Settings
- 0xC0-0xC8: Capabilities相关
校验和: 所有字节(包括I2C地址)异或
*/
#define DDC_CI_ADDR 0x37 // DDC/CI I2C地址 (7位)
#define DDC_CI_ADDR_WRITE 0x6E // 写地址 (8位)
#define DDC_CI_ADDR_READ 0x6F // 读地址 (8位)
#define DDC_CI_SOURCE_ADDR 0x51 // 源地址 (主机)
// DDC/CI操作码
typedef enum {
DDC_CMD_VCP_REQUEST = 0x01, // 读取VCP
DDC_CMD_VCP_REPLY = 0x02, // VCP响应
DDC_CMD_VCP_SET = 0x03, // 设置VCP
DDC_CMD_TIMING_REQUEST = 0x06, // 时序请求
DDC_CMD_TIMING_REPLY = 0x07, // 时序响应
DDC_CMD_VCP_RESET = 0x09, // VCP复位
DDC_CMD_SAVE_SETTINGS = 0x0C, // 保存设置
DDC_CMD_CAPABILITIES_REQUEST = 0xF3, // 能力请求
DDC_CMD_CAPABILITIES_REPLY = 0xE3, // 能力响应
} DDCCommand_t;
// DDC/CI消息结构
typedef struct {
uint8_t dest_addr;
uint8_t source_addr;
uint8_t length; // 0x80 | data_length
uint8_t opcode;
uint8_t data[32];
uint8_t checksum;
} DDCMessage_t;
/**
* 计算DDC/CI校验和
*/
uint8_t ddc_calc_checksum(const uint8_t *data, int len, uint8_t init)
{
uint8_t checksum = init;
for (int i = 0; i < len; i++) {
checksum ^= data[i];
}
return checksum;
}
/**
* 构建VCP读取命令
*/
int ddc_build_vcp_request(uint8_t *buffer, uint8_t vcp_code)
{
// 格式: [dest] [source] [length] [opcode] [vcp_code] [checksum]
buffer[0] = DDC_CI_ADDR_WRITE; // 0x6E
buffer[1] = DDC_CI_SOURCE_ADDR; // 0x51
buffer[2] = 0x82; // 0x80 | 2 (2字节数据)
buffer[3] = DDC_CMD_VCP_REQUEST; // 0x01
buffer[4] = vcp_code;
// 校验和: 所有字节异或 (从dest_addr开始)
buffer[5] = ddc_calc_checksum(buffer, 5, DDC_CI_ADDR_WRITE ^ DDC_CI_SOURCE_ADDR);
return 6;
}
/**
* 构建VCP设置命令
*/
int ddc_build_vcp_set(uint8_t *buffer, uint8_t vcp_code, uint16_t value)
{
// 格式: [dest] [source] [length] [opcode] [vcp_code] [value_h] [value_l] [checksum]
buffer[0] = DDC_CI_ADDR_WRITE;
buffer[1] = DDC_CI_SOURCE_ADDR;
buffer[2] = 0x84; // 0x80 | 4
buffer[3] = DDC_CMD_VCP_SET; // 0x03
buffer[4] = vcp_code;
buffer[5] = (value >> 8) & 0xFF; // 高字节
buffer[6] = value & 0xFF; // 低字节
buffer[7] = ddc_calc_checksum(buffer, 7, DDC_CI_ADDR_WRITE ^ DDC_CI_SOURCE_ADDR);
return 8;
}
/**
* 解析VCP响应
*/
int ddc_parse_vcp_reply(const uint8_t *buffer, int len,
uint8_t *vcp_code, uint16_t *current, uint16_t *maximum)
{
// 响应格式: [source] [length] [opcode] [result] [vcp_code] [type] [max_h] [max_l] [cur_h] [cur_l] [checksum]
if (len < 10) return -1;
if (buffer[0] != 0x6E) return -2; // 源地址错误
if ((buffer[1] & 0x80) == 0) return -3; // 长度格式错误
if (buffer[2] != DDC_CMD_VCP_REPLY) return -4; // 操作码错误
uint8_t result = buffer[3];
if (result != 0x00) return -5; // 操作失败
*vcp_code = buffer[4];
// buffer[5] = type (0x00=set/get, 0x01=momentary)
*maximum = (buffer[6] << 8) | buffer[7];
*current = (buffer[8] << 8) | buffer[9];
return 0;
}
DDC/CI完整实现
c
/**
* DDC/CI驱动实现
*/
#include <stdint.h>
#include <string.h>
// I2C句柄 (平台相关)
typedef void* I2CHandle_t;
// DDC/CI设备结构
typedef struct {
I2CHandle_t i2c;
uint8_t bus_number; // I2C总线号
uint8_t is_initialized;
// 缓存的显示器信息
char manufacturer[16];
char model[32];
uint16_t mccs_version;
} DDCDevice_t;
static DDCDevice_t ddc_device;
/**
* 初始化DDC/CI通信
*/
int ddc_init(int i2c_bus)
{
ddc_device.bus_number = i2c_bus;
// 打开I2C设备
char dev_path[32];
snprintf(dev_path, sizeof(dev_path), "/dev/i2c-%d", i2c_bus);
ddc_device.i2c = i2c_open(dev_path);
if (ddc_device.i2c == NULL) {
return -1;
}
ddc_device.is_initialized = 1;
return 0;
}
/**
* I2C发送数据 (平台相关实现)
*/
int i2c_write(I2CHandle_t handle, uint8_t addr, const uint8_t *data, int len)
{
#ifdef __linux__
// Linux实现
struct i2c_msg msg = {
.addr = addr,
.flags = 0,
.len = len,
.buf = (uint8_t*)data
};
struct i2c_rdwr_ioctl_data ioctl_data = {
.msgs = &msg,
.nmsgs = 1
};
return ioctl((int)(intptr_t)handle, I2C_RDWR, &ioctl_data);
#else
// 其他平台实现
return HAL_I2C_Master_Transmit(handle, addr << 1, data, len, 100);
#endif
}
/**
* I2C接收数据 (平台相关实现)
*/
int i2c_read(I2CHandle_t handle, uint8_t addr, uint8_t *data, int len)
{
#ifdef __linux__
struct i2c_msg msg = {
.addr = addr,
.flags = I2C_M_RD,
.len = len,
.buf = data
};
struct i2c_rdwr_ioctl_data ioctl_data = {
.msgs = &msg,
.nmsgs = 1
};
return ioctl((int)(intptr_t)handle, I2C_RDWR, &ioctl_data);
#else
return HAL_I2C_Master_Receive(handle, addr << 1, data, len, 100);
#endif
}
/**
* 发送DDC/CI命令并接收响应
*/
int ddc_transact(const uint8_t *cmd, int cmd_len, uint8_t *response, int *resp_len)
{
int ret;
// 发送命令 (跳过第一个字节,那是I2C地址)
ret = i2c_write(ddc_device.i2c, DDC_CI_ADDR, cmd + 1, cmd_len - 1);
if (ret < 0) {
return -1;
}
// 延时等待显示器处理 (DDC/CI规范要求至少40ms)
usleep(50000); // 50ms
// 读取响应
uint8_t temp[64];
ret = i2c_read(ddc_device.i2c, DDC_CI_ADDR, temp, 64);
if (ret < 0) {
return -2;
}
// 解析响应长度
int data_len = temp[1] & 0x7F;
*resp_len = data_len + 3; // source + length + data + checksum
memcpy(response, temp, *resp_len);
// 验证校验和
uint8_t calc_checksum = ddc_calc_checksum(response, *resp_len - 1,
DDC_CI_ADDR_READ);
if (calc_checksum != response[*resp_len - 1]) {
return -3; // 校验和错误
}
return 0;
}
/**
* 读取VCP值
*/
int ddc_get_vcp(uint8_t vcp_code, uint16_t *current, uint16_t *maximum)
{
uint8_t cmd[16];
uint8_t response[64];
int cmd_len, resp_len;
// 构建命令
cmd_len = ddc_build_vcp_request(cmd, vcp_code);
// 发送并接收
int ret = ddc_transact(cmd, cmd_len, response, &resp_len);
if (ret < 0) {
return ret;
}
// 解析响应
uint8_t reply_vcp;
ret = ddc_parse_vcp_reply(response, resp_len, &reply_vcp, current, maximum);
if (ret < 0) {
return ret;
}
if (reply_vcp != vcp_code) {
return -10; // VCP代码不匹配
}
return 0;
}
/**
* 设置VCP值
*/
int ddc_set_vcp(uint8_t vcp_code, uint16_t value)
{
uint8_t cmd[16];
int cmd_len;
// 构建命令
cmd_len = ddc_build_vcp_set(cmd, vcp_code, value);
// 发送命令 (设置命令通常没有响应)
int ret = i2c_write(ddc_device.i2c, DDC_CI_ADDR, cmd + 1, cmd_len - 1);
if (ret < 0) {
return ret;
}
// 等待设置生效
usleep(50000);
return 0;
}
/* ========== 高级封装函数 ========== */
/**
* 获取显示器亮度
*/
int ddc_get_brightness(uint8_t *brightness, uint8_t *max_brightness)
{
uint16_t current, maximum;
int ret = ddc_get_vcp(VCP_BRIGHTNESS, ¤t, &maximum);
if (ret < 0) return ret;
*brightness = (uint8_t)current;
if (max_brightness) {
*max_brightness = (uint8_t)maximum;
}
return 0;
}
/**
* 设置显示器亮度
*/
int ddc_set_brightness(uint8_t brightness)
{
return ddc_set_vcp(VCP_BRIGHTNESS, brightness);
}
/**
* 获取显示器对比度
*/
int ddc_get_contrast(uint8_t *contrast, uint8_t *max_contrast)
{
uint16_t current, maximum;
int ret = ddc_get_vcp(VCP_CONTRAST, ¤t, &maximum);
if (ret < 0) return ret;
*contrast = (uint8_t)current;
if (max_contrast) {
*max_contrast = (uint8_t)maximum;
}
return 0;
}
/**
* 设置显示器对比度
*/
int ddc_set_contrast(uint8_t contrast)
{
return ddc_set_vcp(VCP_CONTRAST, contrast);
}
/**
* 获取扬声器音量
*/
int ddc_get_volume(uint8_t *volume, uint8_t *max_volume)
{
uint16_t current, maximum;
int ret = ddc_get_vcp(VCP_AUDIO_SPEAKER_VOLUME, ¤t, &maximum);
if (ret < 0) return ret;
*volume = (uint8_t)current;
if (max_volume) {
*max_volume = (uint8_t)maximum;
}
return 0;
}
/**
* 设置扬声器音量
*/
int ddc_set_volume(uint8_t volume)
{
return ddc_set_vcp(VCP_AUDIO_SPEAKER_VOLUME, volume);
}
/**
* 设置静音
*/
int ddc_set_mute(uint8_t mute)
{
// 0x01 = 静音, 0x02 = 取消静音
return ddc_set_vcp(VCP_AUDIO_MUTE, mute ? 0x01 : 0x02);
}
/**
* 切换输入源
*/
int ddc_set_input_source(InputSource_t source)
{
return ddc_set_vcp(VCP_INPUT_SOURCE, source);
}
/**
* 获取当前输入源
*/
int ddc_get_input_source(InputSource_t *source)
{
uint16_t current, maximum;
int ret = ddc_get_vcp(VCP_INPUT_SOURCE, ¤t, &maximum);
if (ret < 0) return ret;
*source = (InputSource_t)current;
return 0;
}
/**
* 设置电源模式
*/
int ddc_set_power_mode(PowerMode_t mode)
{
return ddc_set_vcp(VCP_POWER_MODE, mode);
}
/**
* 恢复出厂设置
*/
int ddc_restore_factory_defaults(void)
{
return ddc_set_vcp(VCP_RESTORE_FACTORY_DEFAULT, 0x01);
}
读取显示器能力
c
/**
* 读取显示器能力字符串
*
* 能力字符串描述了显示器支持的所有VCP代码及其取值范围
*/
/**
* 读取能力字符串
*/
int ddc_get_capabilities(char *caps_string, int max_len)
{
uint8_t cmd[16];
uint8_t response[64];
int resp_len;
int offset = 0;
int total_len = 0;
// 能力字符串可能很长,需要分块读取
while (1) {
// 构建能力请求命令
cmd[0] = DDC_CI_ADDR_WRITE;
cmd[1] = DDC_CI_SOURCE_ADDR;
cmd[2] = 0x83; // 长度 = 3
cmd[3] = DDC_CMD_CAPABILITIES_REQUEST; // 0xF3
cmd[4] = (offset >> 8) & 0xFF;
cmd[5] = offset & 0xFF;
cmd[6] = ddc_calc_checksum(cmd, 6, DDC_CI_ADDR_WRITE ^ DDC_CI_SOURCE_ADDR);
int ret = ddc_transact(cmd, 7, response, &resp_len);
if (ret < 0) {
return ret;
}
// 检查响应
if (response[2] != DDC_CMD_CAPABILITIES_REPLY) {
break;
}
// 提取数据
int data_len = (response[1] & 0x7F) - 3; // 减去offset和opcode
if (data_len <= 0) {
break;
}
// 复制数据
int copy_len = data_len;
if (total_len + copy_len >= max_len - 1) {
copy_len = max_len - 1 - total_len;
}
memcpy(caps_string + total_len, response + 6, copy_len);
total_len += copy_len;
offset += data_len;
// 检查是否结束
if (data_len < 32) {
break;
}
}
caps_string[total_len] = '\0';
return total_len;
}
/**
* 解析能力字符串
*
* 能力字符串格式示例:
* (prot(monitor)type(lcd)model(Dell U2412M)cmds(01 02 03 07 0C)
* vcp(02 04 05 08 10 12 14(05 08 0B) 16 18 1A 60(0F 10 11 12))
* mswhql(1)asset_eep(32)mccs_ver(2.1))
*/
typedef struct {
uint8_t vcp_code;
uint8_t num_values;
uint8_t values[32]; // 预设值列表
uint16_t min_value;
uint16_t max_value;
} VCPCapability_t;
typedef struct {
char model[64];
char type[16];
uint16_t mccs_version;
int num_vcp;
VCPCapability_t vcp_caps[64];
} MonitorCapabilities_t;
/**
* 简单的能力字符串解析
*/
int parse_capabilities(const char *caps_string, MonitorCapabilities_t *caps)
{
memset(caps, 0, sizeof(MonitorCapabilities_t));
const char *ptr = caps_string;
// 查找model
const char *model_start = strstr(ptr, "model(");
if (model_start) {
model_start += 6;
const char *model_end = strchr(model_start, ')');
if (model_end) {
int len = model_end - model_start;
if (len > 63) len = 63;
strncpy(caps->model, model_start, len);
}
}
// 查找type
const char *type_start = strstr(ptr, "type(");
if (type_start) {
type_start += 5;
const char *type_end = strchr(type_start, ')');
if (type_end) {
int len = type_end - type_start;
if (len > 15) len = 15;
strncpy(caps->type, type_start, len);
}
}
// 查找mccs_ver
const char *ver_start = strstr(ptr, "mccs_ver(");
if (ver_start) {
ver_start += 9;
int major, minor;
if (sscanf(ver_start, "%d.%d", &major, &minor) == 2) {
caps->mccs_version = (major << 8) | minor;
}
}
// 解析VCP代码列表
const char *vcp_start = strstr(ptr, "vcp(");
if (vcp_start) {
vcp_start += 4;
while (*vcp_start && *vcp_start != ')') {
// 跳过空格
while (*vcp_start == ' ') vcp_start++;
// 读取VCP代码
unsigned int vcp_code;
if (sscanf(vcp_start, "%x", &vcp_code) != 1) {
break;
}
VCPCapability_t *vcp = &caps->vcp_caps[caps->num_vcp++];
vcp->vcp_code = vcp_code;
// 移动到下一个
while (*vcp_start && *vcp_start != ' ' && *vcp_start != '(' && *vcp_start != ')') {
vcp_start++;
}
// 检查是否有预设值列表
if (*vcp_start == '(') {
vcp_start++;
while (*vcp_start && *vcp_start != ')') {
while (*vcp_start == ' ') vcp_start++;
unsigned int value;
if (sscanf(vcp_start, "%x", &value) == 1) {
vcp->values[vcp->num_values++] = value;
}
while (*vcp_start && *vcp_start != ' ' && *vcp_start != ')') {
vcp_start++;
}
}
if (*vcp_start == ')') vcp_start++;
}
if (caps->num_vcp >= 64) break;
}
}
return 0;
}
/**
* 打印显示器能力信息
*/
void print_monitor_capabilities(const MonitorCapabilities_t *caps)
{
printf("显示器型号: %s\n", caps->model);
printf("显示器类型: %s\n", caps->type);
printf("MCCS版本: %d.%d\n", caps->mccs_version >> 8, caps->mccs_version & 0xFF);
printf("\n支持的VCP代码:\n");
for (int i = 0; i < caps->num_vcp; i++) {
VCPCapability_t *vcp = &caps->vcp_caps[i];
printf(" 0x%02X", vcp->vcp_code);
// 打印VCP名称
switch (vcp->vcp_code) {
case VCP_BRIGHTNESS: printf(" (亮度)"); break;
case VCP_CONTRAST: printf(" (对比度)"); break;
case VCP_INPUT_SOURCE: printf(" (输入源)"); break;
case VCP_AUDIO_SPEAKER_VOLUME: printf(" (音量)"); break;
case VCP_POWER_MODE: printf(" (电源)"); break;
default: break;
}
// 打印预设值
if (vcp->num_values > 0) {
printf(" 可选值: ");
for (int j = 0; j < vcp->num_values; j++) {
printf("0x%02X ", vcp->values[j]);
}
}
printf("\n");
}
}
CEC协议详解
CEC概述
┌─────────────────────────────────────────────────────────────────────────────┐
│ HDMI-CEC协议 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ CEC = Consumer Electronics Control (消费电子控制) │
│ │
│ 特点: │
│ - 单线双向总线 (Pin 13) │
│ - 低速率: ~400bps │
│ - 最多15个设备 │
│ - 支持设备发现、一键播放、系统待机等 │
│ │
│ 各厂商品牌名: │
│ - 三星: Anynet+ │
│ - LG: SimpLink │
│ - Sony: BRAVIA Sync │
│ - 飞利浦: EasyLink │
│ - 东芝: Regza-Link │
│ - 松下: VIERA Link │
│ │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ CEC网络拓扑: │
│ │
│ ┌─────────┐ │
│ │ TV │ 逻辑地址: 0 │
│ │ (Root) │ │
│ └────┬────┘ │
│ │ CEC总线 │
│ ─────────┼─────────────────────────────────── │
│ │ │
│ ┌────────┼────────┬────────────┬────────────┐ │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │PC │ │BD │ │STB│ │AVR│ │Game│ │
│ │ │ │ │ │ │ │ │ │ │ │
│ └───┘ └───┘ └───┘ └───┘ └───┘ │
│ 地址:4 地址:4 地址:3 地址:5 地址:4 │
│ │
│ 逻辑地址分配: │
│ 0: TV 8: Playback Device 2 │
│ 1: Recording Device 1 9: Recording Device 3 │
│ 2: Recording Device 2 10: Tuner 4 │
│ 3: Tuner 1 11: Playback Device 3 │
│ 4: Playback Device 1 12: Reserved │
│ 5: Audio System 13: Reserved │
│ 6: Tuner 2 14: Free Use │
│ 7: Tuner 3 15: Broadcast (所有设备) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
CEC消息格式
c
/**
* CEC消息格式和操作码
*/
/*
CEC帧格式:
起始位 + 头块 + 数据块... + 结束
头块 (Header Block):
┌────────────────┬────────────────┬─────┐
│ 发起者地址(4位) │ 目标地址(4位) │ EOM │
└────────────────┴────────────────┴─────┘
数据块 (Data Block):
┌────────────────┬─────┐
│ 数据 (8位) │ EOM │
└────────────────┴─────┘
EOM = End of Message
- 0: 后续还有数据块
- 1: 这是最后一个数据块
*/
// CEC逻辑地址
typedef enum {
CEC_ADDR_TV = 0,
CEC_ADDR_RECORDING_1 = 1,
CEC_ADDR_RECORDING_2 = 2,
CEC_ADDR_TUNER_1 = 3,
CEC_ADDR_PLAYBACK_1 = 4,
CEC_ADDR_AUDIO_SYSTEM = 5,
CEC_ADDR_TUNER_2 = 6,
CEC_ADDR_TUNER_3 = 7,
CEC_ADDR_PLAYBACK_2 = 8,
CEC_ADDR_RECORDING_3 = 9,
CEC_ADDR_TUNER_4 = 10,
CEC_ADDR_PLAYBACK_3 = 11,
CEC_ADDR_FREE_USE = 14,
CEC_ADDR_BROADCAST = 15,
CEC_ADDR_UNREGISTERED = 15,
} CECLogicalAddr_t;
// CEC操作码
typedef enum {
// 一键播放
CEC_OPCODE_ACTIVE_SOURCE = 0x82, // 声明为活动源
CEC_OPCODE_IMAGE_VIEW_ON = 0x04, // 唤醒显示器
CEC_OPCODE_TEXT_VIEW_ON = 0x0D, // 唤醒显示器(文本)
// 路由控制
CEC_OPCODE_ROUTING_CHANGE = 0x80,
CEC_OPCODE_ROUTING_INFORMATION = 0x81,
CEC_OPCODE_SET_STREAM_PATH = 0x86,
CEC_OPCODE_INACTIVE_SOURCE = 0x9D,
CEC_OPCODE_REQUEST_ACTIVE_SOURCE = 0x85,
// 待机
CEC_OPCODE_STANDBY = 0x36, // 进入待机
// 系统信息
CEC_OPCODE_CEC_VERSION = 0x9E,
CEC_OPCODE_GET_CEC_VERSION = 0x9F,
CEC_OPCODE_GIVE_PHYSICAL_ADDRESS = 0x83,
CEC_OPCODE_REPORT_PHYSICAL_ADDRESS = 0x84,
CEC_OPCODE_GET_MENU_LANGUAGE = 0x91,
CEC_OPCODE_SET_MENU_LANGUAGE = 0x32,
// 设备OSD名称
CEC_OPCODE_GIVE_OSD_NAME = 0x46,
CEC_OPCODE_SET_OSD_NAME = 0x47,
CEC_OPCODE_SET_OSD_STRING = 0x64,
// 设备菜单控制
CEC_OPCODE_MENU_REQUEST = 0x8D,
CEC_OPCODE_MENU_STATUS = 0x8E,
CEC_OPCODE_USER_CONTROL_PRESSED = 0x44, // 遥控器按键按下
CEC_OPCODE_USER_CONTROL_RELEASED = 0x45, // 遥控器按键释放
// 遥控器直通
CEC_OPCODE_GIVE_DEVICE_POWER_STATUS = 0x8F,
CEC_OPCODE_REPORT_POWER_STATUS = 0x90,
// 系统音频控制
CEC_OPCODE_GIVE_AUDIO_STATUS = 0x71,
CEC_OPCODE_REPORT_AUDIO_STATUS = 0x7A,
CEC_OPCODE_SET_SYSTEM_AUDIO_MODE = 0x72,
CEC_OPCODE_SYSTEM_AUDIO_MODE_REQUEST = 0x70,
CEC_OPCODE_SYSTEM_AUDIO_MODE_STATUS = 0x7E,
CEC_OPCODE_GIVE_SYSTEM_AUDIO_MODE_STATUS = 0x7D,
CEC_OPCODE_USER_CONTROL_PRESSED_AUDIO = 0x44, // 音量控制
// 音频速率控制
CEC_OPCODE_SET_AUDIO_RATE = 0x9A,
// 音频返回通道 (ARC)
CEC_OPCODE_INITIATE_ARC = 0xC0,
CEC_OPCODE_REPORT_ARC_INITIATED = 0xC1,
CEC_OPCODE_REPORT_ARC_TERMINATED = 0xC2,
CEC_OPCODE_REQUEST_ARC_INITIATION = 0xC3,
CEC_OPCODE_REQUEST_ARC_TERMINATION = 0xC4,
CEC_OPCODE_TERMINATE_ARC = 0xC5,
// 其他
CEC_OPCODE_FEATURE_ABORT = 0x00,
CEC_OPCODE_ABORT = 0xFF,
CEC_OPCODE_POLLING_MESSAGE = 0x100, // 特殊,仅发送头块
} CECOpcode_t;
// 用户控制按键代码 (遥控器按键)
typedef enum {
CEC_USER_CONTROL_SELECT = 0x00,
CEC_USER_CONTROL_UP = 0x01,
CEC_USER_CONTROL_DOWN = 0x02,
CEC_USER_CONTROL_LEFT = 0x03,
CEC_USER_CONTROL_RIGHT = 0x04,
CEC_USER_CONTROL_RIGHT_UP = 0x05,
CEC_USER_CONTROL_RIGHT_DOWN = 0x06,
CEC_USER_CONTROL_LEFT_UP = 0x07,
CEC_USER_CONTROL_LEFT_DOWN = 0x08,
CEC_USER_CONTROL_ROOT_MENU = 0x09,
CEC_USER_CONTROL_SETUP_MENU = 0x0A,
CEC_USER_CONTROL_CONTENTS_MENU = 0x0B,
CEC_USER_CONTROL_EXIT = 0x0D,
CEC_USER_CONTROL_NUMBER_0 = 0x20,
CEC_USER_CONTROL_NUMBER_1 = 0x21,
CEC_USER_CONTROL_NUMBER_2 = 0x22,
CEC_USER_CONTROL_NUMBER_3 = 0x23,
CEC_USER_CONTROL_NUMBER_4 = 0x24,
CEC_USER_CONTROL_NUMBER_5 = 0x25,
CEC_USER_CONTROL_NUMBER_6 = 0x26,
CEC_USER_CONTROL_NUMBER_7 = 0x27,
CEC_USER_CONTROL_NUMBER_8 = 0x28,
CEC_USER_CONTROL_NUMBER_9 = 0x29,
CEC_USER_CONTROL_CHANNEL_UP = 0x30,
CEC_USER_CONTROL_CHANNEL_DOWN = 0x31,
CEC_USER_CONTROL_PREVIOUS_CHANNEL = 0x32,
CEC_USER_CONTROL_VOLUME_UP = 0x41, // 音量+
CEC_USER_CONTROL_VOLUME_DOWN = 0x42, // 音量-
CEC_USER_CONTROL_MUTE = 0x43, // 静音
CEC_USER_CONTROL_PLAY = 0x44,
CEC_USER_CONTROL_STOP = 0x45,
CEC_USER_CONTROL_PAUSE = 0x46,
CEC_USER_CONTROL_RECORD = 0x47,
CEC_USER_CONTROL_REWIND = 0x48,
CEC_USER_CONTROL_FAST_FORWARD = 0x49,
CEC_USER_CONTROL_EJECT = 0x4A,
CEC_USER_CONTROL_FORWARD = 0x4B,
CEC_USER_CONTROL_BACKWARD = 0x4C,
CEC_USER_CONTROL_POWER = 0x40,
CEC_USER_CONTROL_POWER_TOGGLE = 0x6B,
CEC_USER_CONTROL_POWER_OFF = 0x6C,
CEC_USER_CONTROL_POWER_ON = 0x6D,
} CECUserControl_t;
// CEC消息结构
typedef struct {
uint8_t initiator; // 发起者地址 (4位)
uint8_t destination; // 目标地址 (4位)
uint8_t opcode;
uint8_t parameters[14]; // 最多14字节参数
uint8_t param_length;
} CECMessage_t;
CEC控制实现
c
/**
* CEC控制实现
*/
#include <stdint.h>
#include <string.h>
// CEC设备状态
typedef struct {
int fd; // CEC设备文件描述符
uint8_t logical_address; // 本机逻辑地址
uint16_t physical_address; // 物理地址 (如 1.0.0.0)
char osd_name[15]; // OSD名称
uint8_t is_active_source; // 是否为活动源
} CECDevice_t;
static CECDevice_t cec_device;
/**
* 打开CEC设备 (Linux)
*/
int cec_init(const char *device_path)
{
#ifdef __linux__
// 使用cec-utils或内核CEC框架
cec_device.fd = open(device_path ? device_path : "/dev/cec0", O_RDWR);
if (cec_device.fd < 0) {
return -1;
}
// 设置物理地址 (需要从EDID获取)
cec_device.physical_address = 0x1000; // 1.0.0.0
// 注册逻辑地址
cec_device.logical_address = CEC_ADDR_PLAYBACK_1;
strncpy(cec_device.osd_name, "PC", sizeof(cec_device.osd_name));
return 0;
#else
return -1;
#endif
}
/**
* 发送CEC消息
*/
int cec_send_message(const CECMessage_t *msg)
{
uint8_t buffer[16];
int len = 0;
// 头块: [initiator:4][destination:4]
buffer[len++] = (msg->initiator << 4) | msg->destination;
// 操作码
if (msg->opcode != CEC_OPCODE_POLLING_MESSAGE) {
buffer[len++] = msg->opcode;
// 参数
for (int i = 0; i < msg->param_length; i++) {
buffer[len++] = msg->parameters[i];
}
}
#ifdef __linux__
// 使用Linux CEC框架发送
struct cec_msg cec_msg;
memset(&cec_msg, 0, sizeof(cec_msg));
cec_msg.len = len;
memcpy(cec_msg.msg, buffer, len);
return ioctl(cec_device.fd, CEC_TRANSMIT, &cec_msg);
#else
return -1;
#endif
}
/**
* 接收CEC消息
*/
int cec_receive_message(CECMessage_t *msg, int timeout_ms)
{
#ifdef __linux__
struct cec_msg cec_msg;
memset(&cec_msg, 0, sizeof(cec_msg));
cec_msg.timeout = timeout_ms;
int ret = ioctl(cec_device.fd, CEC_RECEIVE, &cec_msg);
if (ret < 0) {
return ret;
}
// 解析消息
msg->initiator = (cec_msg.msg[0] >> 4) & 0x0F;
msg->destination = cec_msg.msg[0] & 0x0F;
if (cec_msg.len > 1) {
msg->opcode = cec_msg.msg[1];
msg->param_length = cec_msg.len - 2;
memcpy(msg->parameters, &cec_msg.msg[2], msg->param_length);
} else {
msg->opcode = CEC_OPCODE_POLLING_MESSAGE;
msg->param_length = 0;
}
return 0;
#else
return -1;
#endif
}
/* ========== 高级控制函数 ========== */
/**
* 唤醒电视
*/
int cec_power_on_tv(void)
{
CECMessage_t msg = {
.initiator = cec_device.logical_address,
.destination = CEC_ADDR_TV,
.opcode = CEC_OPCODE_IMAGE_VIEW_ON,
.param_length = 0
};
return cec_send_message(&msg);
}
/**
* 使电视进入待机
*/
int cec_standby_tv(void)
{
CECMessage_t msg = {
.initiator = cec_device.logical_address,
.destination = CEC_ADDR_TV,
.opcode = CEC_OPCODE_STANDBY,
.param_length = 0
};
return cec_send_message(&msg);
}
/**
* 使所有设备进入待机
*/
int cec_standby_all(void)
{
CECMessage_t msg = {
.initiator = cec_device.logical_address,
.destination = CEC_ADDR_BROADCAST,
.opcode = CEC_OPCODE_STANDBY,
.param_length = 0
};
return cec_send_message(&msg);
}
/**
* 声明为活动源 (切换电视输入到本设备)
*/
int cec_set_active_source(void)
{
CECMessage_t msg = {
.initiator = cec_device.logical_address,
.destination = CEC_ADDR_BROADCAST,
.opcode = CEC_OPCODE_ACTIVE_SOURCE,
.param_length = 2
};
// 参数: 物理地址
msg.parameters[0] = (cec_device.physical_address >> 8) & 0xFF;
msg.parameters[1] = cec_device.physical_address & 0xFF;
cec_device.is_active_source = 1;
return cec_send_message(&msg);
}
/**
* 发送遥控器按键
*/
int cec_send_keypress(uint8_t destination, CECUserControl_t key)
{
CECMessage_t msg = {
.initiator = cec_device.logical_address,
.destination = destination,
.opcode = CEC_OPCODE_USER_CONTROL_PRESSED,
.param_length = 1
};
msg.parameters[0] = key;
int ret = cec_send_message(&msg);
if (ret < 0) return ret;
// 延时后发送释放
usleep(100000); // 100ms
msg.opcode = CEC_OPCODE_USER_CONTROL_RELEASED;
msg.param_length = 0;
return cec_send_message(&msg);
}
/**
* 音量控制 (通过音频系统)
*/
int cec_volume_up(void)
{
return cec_send_keypress(CEC_ADDR_AUDIO_SYSTEM, CEC_USER_CONTROL_VOLUME_UP);
}
int cec_volume_down(void)
{
return cec_send_keypress(CEC_ADDR_AUDIO_SYSTEM, CEC_USER_CONTROL_VOLUME_DOWN);
}
int cec_mute(void)
{
return cec_send_keypress(CEC_ADDR_AUDIO_SYSTEM, CEC_USER_CONTROL_MUTE);
}
/**
* 直接控制电视音量 (如果电视不连接音响)
*/
int cec_tv_volume_up(void)
{
return cec_send_keypress(CEC_ADDR_TV, CEC_USER_CONTROL_VOLUME_UP);
}
int cec_tv_volume_down(void)
{
return cec_send_keypress(CEC_ADDR_TV, CEC_USER_CONTROL_VOLUME_DOWN);
}
/**
* 获取设备电源状态
*/
int cec_get_power_status(uint8_t destination, uint8_t *power_status)
{
CECMessage_t msg = {
.initiator = cec_device.logical_address,
.destination = destination,
.opcode = CEC_OPCODE_GIVE_DEVICE_POWER_STATUS,
.param_length = 0
};
int ret = cec_send_message(&msg);
if (ret < 0) return ret;
// 等待响应
CECMessage_t response;
ret = cec_receive_message(&response, 1000);
if (ret < 0) return ret;
if (response.opcode == CEC_OPCODE_REPORT_POWER_STATUS) {
*power_status = response.parameters[0];
// 0x00 = On, 0x01 = Standby, 0x02 = In transition to On, 0x03 = In transition to Standby
return 0;
}
return -1;
}
Windows/Linux实现
Windows DDC/CI实现
c
/**
* Windows平台DDC/CI实现
*
* 使用 PhysicalMonitor API
*/
#ifdef _WIN32
#include <windows.h>
#include <highlevelmonitorconfigurationapi.h>
#include <lowlevelmonitorconfigurationapi.h>
#include <physicalmonitorenumerationapi.h>
#pragma comment(lib, "Dxva2.lib")
typedef struct {
HMONITOR hMonitor;
PHYSICAL_MONITOR physicalMonitor;
DWORD numMonitors;
} WinDDCDevice_t;
static WinDDCDevice_t win_ddc;
/**
* 初始化Windows DDC
*/
int win_ddc_init(void)
{
// 获取主显示器
POINT pt = {0, 0};
win_ddc.hMonitor = MonitorFromPoint(pt, MONITOR_DEFAULTTOPRIMARY);
// 获取物理显示器数量
if (!GetNumberOfPhysicalMonitorsFromHMONITOR(win_ddc.hMonitor, &win_ddc.numMonitors)) {
return -1;
}
// 获取物理显示器句柄
if (!GetPhysicalMonitorsFromHMONITOR(win_ddc.hMonitor, 1, &win_ddc.physicalMonitor)) {
return -2;
}
return 0;
}
/**
* 关闭Windows DDC
*/
void win_ddc_close(void)
{
DestroyPhysicalMonitors(1, &win_ddc.physicalMonitor);
}
/**
* 获取亮度
*/
int win_ddc_get_brightness(DWORD *minimum, DWORD *current, DWORD *maximum)
{
if (!GetMonitorBrightness(win_ddc.physicalMonitor.hPhysicalMonitor,
minimum, current, maximum)) {
return -1;
}
return 0;
}
/**
* 设置亮度
*/
int win_ddc_set_brightness(DWORD brightness)
{
if (!SetMonitorBrightness(win_ddc.physicalMonitor.hPhysicalMonitor, brightness)) {
return -1;
}
return 0;
}
/**
* 获取对比度
*/
int win_ddc_get_contrast(DWORD *minimum, DWORD *current, DWORD *maximum)
{
if (!GetMonitorContrast(win_ddc.physicalMonitor.hPhysicalMonitor,
minimum, current, maximum)) {
return -1;
}
return 0;
}
/**
* 设置对比度
*/
int win_ddc_set_contrast(DWORD contrast)
{
if (!SetMonitorContrast(win_ddc.physicalMonitor.hPhysicalMonitor, contrast)) {
return -1;
}
return 0;
}
/**
* 使用低级API读取/设置VCP
*/
int win_ddc_get_vcp(BYTE vcp_code, DWORD *current, DWORD *maximum)
{
MC_VCP_CODE_TYPE codeType;
if (!GetVCPFeatureAndVCPFeatureReply(win_ddc.physicalMonitor.hPhysicalMonitor,
vcp_code, &codeType, current, maximum)) {
return -1;
}
return 0;
}
int win_ddc_set_vcp(BYTE vcp_code, DWORD value)
{
if (!SetVCPFeature(win_ddc.physicalMonitor.hPhysicalMonitor, vcp_code, value)) {
return -1;
}
return 0;
}
/**
* 获取显示器音量 (通过VCP)
*/
int win_ddc_get_volume(DWORD *volume, DWORD *max_volume)
{
return win_ddc_get_vcp(VCP_AUDIO_SPEAKER_VOLUME, volume, max_volume);
}
/**
* 设置显示器音量
*/
int win_ddc_set_volume(DWORD volume)
{
return win_ddc_set_vcp(VCP_AUDIO_SPEAKER_VOLUME, volume);
}
/**
* 切换输入源
*/
int win_ddc_set_input(DWORD input_source)
{
return win_ddc_set_vcp(VCP_INPUT_SOURCE, input_source);
}
#endif // _WIN32
Linux DDC/CI实现
c
/**
* Linux平台DDC/CI实现
*
* 使用 ddcutil 或直接访问 /dev/i2c-*
*/
#ifdef __linux__
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>
#include <linux/i2c.h>
/**
* 查找DDC/CI I2C总线
*
* DDC通常在I2C总线上,可能需要尝试多个总线
*/
int linux_find_ddc_bus(void)
{
char path[32];
for (int bus = 0; bus < 20; bus++) {
snprintf(path, sizeof(path), "/dev/i2c-%d", bus);
int fd = open(path, O_RDWR);
if (fd < 0) continue;
// 尝试读取EDID
if (ioctl(fd, I2C_SLAVE, 0x50) >= 0) {
uint8_t edid[128];
uint8_t addr = 0;
// 写入地址
if (write(fd, &addr, 1) == 1) {
// 读取EDID
if (read(fd, edid, 128) == 128) {
// 检查EDID头
if (edid[0] == 0x00 && edid[1] == 0xFF && edid[7] == 0x00) {
close(fd);
printf("找到DDC总线: /dev/i2c-%d\n", bus);
return bus;
}
}
}
}
close(fd);
}
return -1;
}
/**
* 使用ddcutil命令行工具 (推荐方式)
*/
int linux_ddcutil_get_brightness(int *brightness)
{
FILE *fp = popen("ddcutil getvcp 10 --brief", "r");
if (!fp) return -1;
char line[256];
while (fgets(line, sizeof(line), fp)) {
// 格式: VCP 10 C 50 100
int vcp, type, current, max;
char type_char;
if (sscanf(line, "VCP %x %c %d %d", &vcp, &type_char, ¤t, &max) == 4) {
if (vcp == 0x10) {
*brightness = current;
pclose(fp);
return 0;
}
}
}
pclose(fp);
return -1;
}
int linux_ddcutil_set_brightness(int brightness)
{
char cmd[64];
snprintf(cmd, sizeof(cmd), "ddcutil setvcp 10 %d", brightness);
return system(cmd);
}
int linux_ddcutil_get_volume(int *volume)
{
FILE *fp = popen("ddcutil getvcp 62 --brief", "r");
if (!fp) return -1;
char line[256];
while (fgets(line, sizeof(line), fp)) {
int vcp, current, max;
char type_char;
if (sscanf(line, "VCP %x %c %d %d", &vcp, &type_char, ¤t, &max) == 4) {
if (vcp == 0x62) {
*volume = current;
pclose(fp);
return 0;
}
}
}
pclose(fp);
return -1;
}
int linux_ddcutil_set_volume(int volume)
{
char cmd[64];
snprintf(cmd, sizeof(cmd), "ddcutil setvcp 62 %d", volume);
return system(cmd);
}
int linux_ddcutil_set_input(int input)
{
char cmd[64];
snprintf(cmd, sizeof(cmd), "ddcutil setvcp 60 0x%02x", input);
return system(cmd);
}
/**
* 使用Linux CEC框架
*/
int linux_cec_init(void)
{
int fd = open("/dev/cec0", O_RDWR);
if (fd < 0) {
perror("打开CEC设备失败");
return -1;
}
// 获取CEC能力
struct cec_caps caps;
if (ioctl(fd, CEC_ADAP_G_CAPS, &caps) < 0) {
close(fd);
return -2;
}
printf("CEC适配器: %s\n", caps.driver);
printf("CEC能力: 0x%08x\n", caps.capabilities);
return fd;
}
#endif // __linux__
应用示例
完整控制程序
c
/**
* 显示器控制程序示例
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>
void print_usage(const char *prog)
{
printf("用法: %s [选项]\n", prog);
printf("\n选项:\n");
printf(" --brightness <值> 设置亮度 (0-100)\n");
printf(" --contrast <值> 设置对比度 (0-100)\n");
printf(" --volume <值> 设置音量 (0-100)\n");
printf(" --mute 静音\n");
printf(" --unmute 取消静音\n");
printf(" --input <源> 切换输入源 (hdmi1/hdmi2/dp1/vga)\n");
printf(" --power-on 开机\n");
printf(" --power-off 关机\n");
printf(" --info 显示显示器信息\n");
printf(" --help 显示帮助\n");
}
int parse_input_source(const char *str)
{
if (strcasecmp(str, "hdmi1") == 0) return INPUT_HDMI1;
if (strcasecmp(str, "hdmi2") == 0) return INPUT_HDMI2;
if (strcasecmp(str, "dp1") == 0) return INPUT_DP1;
if (strcasecmp(str, "dp2") == 0) return INPUT_DP2;
if (strcasecmp(str, "vga") == 0) return INPUT_VGA1;
if (strcasecmp(str, "vga1") == 0) return INPUT_VGA1;
if (strcasecmp(str, "vga2") == 0) return INPUT_VGA2;
return -1;
}
int main(int argc, char *argv[])
{
// 初始化
#ifdef _WIN32
if (win_ddc_init() < 0) {
fprintf(stderr, "初始化DDC失败\n");
return 1;
}
#else
int bus = linux_find_ddc_bus();
if (bus < 0) {
fprintf(stderr, "找不到DDC总线\n");
return 1;
}
if (ddc_init(bus) < 0) {
fprintf(stderr, "初始化DDC失败\n");
return 1;
}
#endif
// 解析命令行参数
static struct option long_options[] = {
{"brightness", required_argument, 0, 'b'},
{"contrast", required_argument, 0, 'c'},
{"volume", required_argument, 0, 'v'},
{"mute", no_argument, 0, 'm'},
{"unmute", no_argument, 0, 'u'},
{"input", required_argument, 0, 'i'},
{"power-on", no_argument, 0, 'P'},
{"power-off", no_argument, 0, 'p'},
{"info", no_argument, 0, 'I'},
{"help", no_argument, 0, 'h'},
{0, 0, 0, 0}
};
int opt;
int option_index = 0;
while ((opt = getopt_long(argc, argv, "b:c:v:mui:PpIh", long_options, &option_index)) != -1) {
switch (opt) {
case 'b': {
// 设置亮度
int brightness = atoi(optarg);
if (brightness < 0 || brightness > 100) {
fprintf(stderr, "亮度范围: 0-100\n");
return 1;
}
#ifdef _WIN32
win_ddc_set_brightness(brightness);
#else
ddc_set_brightness(brightness);
#endif
printf("亮度已设置为: %d\n", brightness);
break;
}
case 'c': {
// 设置对比度
int contrast = atoi(optarg);
#ifdef _WIN32
win_ddc_set_contrast(contrast);
#else
ddc_set_contrast(contrast);
#endif
printf("对比度已设置为: %d\n", contrast);
break;
}
case 'v': {
// 设置音量
int volume = atoi(optarg);
#ifdef _WIN32
win_ddc_set_volume(volume);
#else
ddc_set_volume(volume);
#endif
printf("音量已设置为: %d\n", volume);
break;
}
case 'm':
// 静音
ddc_set_mute(1);
printf("已静音\n");
break;
case 'u':
// 取消静音
ddc_set_mute(0);
printf("已取消静音\n");
break;
case 'i': {
// 切换输入源
int input = parse_input_source(optarg);
if (input < 0) {
fprintf(stderr, "未知输入源: %s\n", optarg);
return 1;
}
ddc_set_input_source(input);
printf("输入源已切换到: %s\n", optarg);
break;
}
case 'P':
// 开机 (通过CEC)
cec_power_on_tv();
printf("已发送开机命令\n");
break;
case 'p':
// 关机 (通过CEC)
cec_standby_tv();
printf("已发送关机命令\n");
break;
case 'I': {
// 显示信息
uint8_t brightness, contrast, volume;
uint8_t max_b, max_c, max_v;
printf("=== 显示器信息 ===\n");
#ifdef _WIN32
DWORD min, cur, max;
if (win_ddc_get_brightness(&min, &cur, &max) == 0) {
printf("亮度: %lu (最大: %lu)\n", cur, max);
}
if (win_ddc_get_contrast(&min, &cur, &max) == 0) {
printf("对比度: %lu (最大: %lu)\n", cur, max);
}
if (win_ddc_get_volume(&cur, &max) == 0) {
printf("音量: %lu (最大: %lu)\n", cur, max);
}
#else
if (ddc_get_brightness(&brightness, &max_b) == 0) {
printf("亮度: %d (最大: %d)\n", brightness, max_b);
}
if (ddc_get_contrast(&contrast, &max_c) == 0) {
printf("对比度: %d (最大: %d)\n", contrast, max_c);
}
if (ddc_get_volume(&volume, &max_v) == 0) {
printf("音量: %d (最大: %d)\n", volume, max_v);
}
#endif
// 读取能力字符串
char caps[1024];
if (ddc_get_capabilities(caps, sizeof(caps)) > 0) {
MonitorCapabilities_t mon_caps;
parse_capabilities(caps, &mon_caps);
print_monitor_capabilities(&mon_caps);
}
break;
}
case 'h':
default:
print_usage(argv[0]);
return 0;
}
}
// 清理
#ifdef _WIN32
win_ddc_close();
#endif
return 0;
}
Python封装
python
#!/usr/bin/env python3
"""
DDC/CI Python封装
依赖: pip install monitorcontrol
"""
from monitorcontrol import get_monitors
def get_all_monitors():
"""获取所有显示器"""
monitors = get_monitors()
return monitors
def set_brightness(monitor_index, brightness):
"""设置指定显示器亮度"""
monitors = get_monitors()
if monitor_index >= len(monitors):
raise ValueError(f"显示器索引 {monitor_index} 不存在")
with monitors[monitor_index]:
monitors[monitor_index].set_luminance(brightness)
print(f"显示器 {monitor_index} 亮度已设置为 {brightness}")
def get_brightness(monitor_index):
"""获取指定显示器亮度"""
monitors = get_monitors()
with monitors[monitor_index]:
return monitors[monitor_index].get_luminance()
def set_contrast(monitor_index, contrast):
"""设置对比度"""
monitors = get_monitors()
with monitors[monitor_index]:
monitors[monitor_index].set_contrast(contrast)
def set_input_source(monitor_index, source):
"""切换输入源
source: 'hdmi1', 'hdmi2', 'dp1', 'dp2', 'vga1'
"""
from monitorcontrol import InputSource
source_map = {
'hdmi1': InputSource.HDMI1,
'hdmi2': InputSource.HDMI2,
'dp1': InputSource.DP1,
'dp2': InputSource.DP2,
'vga1': InputSource.ANALOG1,
}
if source not in source_map:
raise ValueError(f"未知输入源: {source}")
monitors = get_monitors()
with monitors[monitor_index]:
monitors[monitor_index].set_input_source(source_map[source])
def list_monitors():
"""列出所有显示器信息"""
monitors = get_monitors()
for i, monitor in enumerate(monitors):
print(f"=== 显示器 {i} ===")
with monitor:
try:
print(f" 亮度: {monitor.get_luminance()}")
except:
print(" 亮度: 不支持")
try:
print(f" 对比度: {monitor.get_contrast()}")
except:
print(" 对比度: 不支持")
try:
caps = monitor.get_vcp_capabilities()
print(f" 能力: {caps}")
except:
pass
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("用法:")
print(" python ddc_control.py list")
print(" python ddc_control.py brightness <显示器索引> <值>")
print(" python ddc_control.py input <显示器索引> <源>")
sys.exit(0)
cmd = sys.argv[1]
if cmd == "list":
list_monitors()
elif cmd == "brightness":
idx = int(sys.argv[2])
val = int(sys.argv[3])
set_brightness(idx, val)
elif cmd == "input":
idx = int(sys.argv[2])
src = sys.argv[3]
set_input_source(idx, src)
常见问题与解决
┌─────────────────────────────────────────────────────────────────────────────┐
│ 常见问题与解决方案 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Q1: DDC/CI不工作,读取/设置失败 │
│ ───────────────────────────────────────── │
│ 可能原因: │
│ 1. 显示器不支持DDC/CI (检查显示器规格) │
│ 2. 显示器DDC/CI功能被关闭 (进入OSD菜单开启) │
│ 3. 使用了不支持DDC的转接器 (HDMI转VGA等) │
│ 4. 驱动问题 (更新显卡驱动) │
│ 5. I2C总线被占用 (Linux下检查i2c权限) │
│ │
│ 解决方法: │
│ - Windows: 安装 ControlMyMonitor / Twinkle Tray │
│ - Linux: 安装 ddcutil,添加用户到i2c组 │
│ sudo usermod -aG i2c $USER │
│ │
│ Q2: CEC不工作 │
│ ───────────────────────────────────────── │
│ 可能原因: │
│ 1. 显卡不支持CEC (大多数PC显卡不支持) │
│ 2. 需要专门的CEC适配器 (如Pulse-Eight USB-CEC) │
│ 3. 电视CEC功能被关闭 │
│ │
│ 解决方法: │
│ - 购买USB-CEC适配器 │
│ - 使用libcec库 │
│ │
│ Q3: 某些VCP代码不支持 │
│ ───────────────────────────────────────── │
│ 不同显示器支持的VCP代码不同,需要先读取能力字符串确认 │
│ │
│ Q4: 设置后没有效果 │
│ ───────────────────────────────────────── │
│ - 某些显示器需要延时才能看到效果 │
│ - 某些值可能被显示器限制在特定范围 │
│ - 读取最大值确认有效范围 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
总结
HDMI反向控制显示器的核心技术:
| 协议 | 用途 | 通道 | 典型操作 |
|---|---|---|---|
| DDC/CI | 显示器参数控制 | I2C (Pin 15/16) | 亮度、对比度、音量、输入源 |
| CEC | 设备联动控制 | 单线 (Pin 13) | 电源、遥控器透传、一键播放 |
| EDID | 显示器信息 | I2C (Pin 15/16) | 分辨率、型号、能力 |
DDC/CI常用VCP代码:
| VCP代码 | 功能 | 范围 |
|---|---|---|
| 0x10 | 亮度 | 0-100 |
| 0x12 | 对比度 | 0-100 |
| 0x60 | 输入源 | 见InputSource |
| 0x62 | 音量 | 0-100 |
| 0x8D | 静音 | 0x01/0x02 |
| 0xD6 | 电源模式 | 0x01-0x05 |
平台支持:
- Windows: PhysicalMonitor API / ControlMyMonitor
- Linux: ddcutil / 直接I2C访问
- CEC需要专门硬件支持(USB-CEC适配器)
希望这篇文章对做显示器控制的朋友有帮助!有问题欢迎评论区交流~
参考资料:
- VESA MCCS (Monitor Control Command Set) 标准
- HDMI-CEC 规范
- DDC/CI 协议文档
- libcec 项目
- ddcutil 文档