Simulate HDMI CEC by GPIO
Use the gpio tosimulate the function of HDMI CEC.
分为两部分介绍:
HDMI CEC 的协议标准
Rockchip 使用GPIO 模拟CEC的软件
1,HDMI CEC的协议标准
1.1 具体的CEC标准在“HDMI1.4b Spec”的217页.
Hdmi 1.4spec的一个下载地址:
http://download.youkuaiyun.com/download/jasonwan23/4882713
1.2 CEC的硬件要求
接27k ohms 上拉到3.3v。
1.3 CEC的bit 时序
1.3.1 Start Bit Timing
Start bit总长度4.5ms,其中低电平3.7ms,高电平0.8ms。容错时间是前后0.2ms。
1.3.2 Data Bit Timing
Logic“0”:Start bit总长度2.4ms,其中低电平1.5ms,高电平0.9ms。容错时间是前后0.2ms。
Logic“1”:Start bit总长度2.4ms,其中低电平0.6ms,高电平1.8ms。容错时间是前后0.2ms。
注意:
start bit、data bit0、data bit1,在上面的波形图中,高电平之后拉低。
实际当中,此拉低动作由下一码元去拉低,而非本次码元自己拉低(需要释放CEC到高电平)。
Start bit+1+0的持续的波形图如下(最后的拉低为下一码元的动作):
1.4 CEC的数据格式
1.4.1 Cec 数据的构成和长度
cec发送data都是由frame构成的。
每一个frame都是由startbit和一个或多个Block组成。
HeaderBlock和每个Data Block的大小都是 10 bit (8bit data + 1bit EOM + 1bit ACK)。
总长度是受限制的,最多只能传16个Block(包括HeaderBlock)。
1.4.2 Header/Data Block description
Header Block和每个Data Block的大小都是 10 bit (8bit data + 1bit EOM + 1bit ACK)。
高位在前,传输的时候从高位往地位开始传送。
static int GPIO_CecSendByte(char value,char EOM) {
for(i = 7; i>= 0; i--)
cec_send_bit((value >> i) & 0x1);
cec_send_bit(EOM);
returncec_read_ack();
}
1.4.3 EOM (End of Message)
logic“1”表示后面还有Block。
logic“0”表示后面没有Block。
1.4.4 ACK (Acknowledge)
A,定向通信(有具体某个设备的destination):
1,发送端应该set 1(NAK)
2,接收端应该set 0(ACK),表示接受成功
B,广播通信(destination为广播地址)
1,发送端应该set 1(NAK)
2,接收端set 0(ACK) 表示拒绝接受该广播。
3,接收端set 1(NAK) 表示接受该广播。
注意:
即便是发送端,在readACK阶段也需要先发送NACK(logic1)出去给接收端。
NAK logic“1”,在timing上,是0.6ms的低电平。而ACK是logic“0”,是1.5ms的低电平。
发送端在读ACK时候,先发出logic1,会先拉低0.6ms,然后再释放CEC,等待高电平,读出低电平的持续时间。
如果释放CEC的时候,由于上拉特性,应该自动回到高电平。此时读出的低电平时间是0.6ms,判断为logic1,即NAK。
如果释放cec的时候,由于接收端有回应,接收端拉低,使低电平保持到1.5ms,然后才由接收端释放cec,自动回到高电平(发送端已经在0.6ms的时候释放cec)。此时读出的低电平时间是1.5ms,判断为logic0,即ACK。
static int cec_read_ack(void)
{
structtimeval ts_start, ts_end;
unsignedint time;
do_gettimeofday(&ts_start);
gpio_set_value(GPIOCec.gpio,GPIO_LOW);/*EOM发送完是高电平,此时要先拉低进低电平*/
udelay(600);
gpio_direction_input(GPIOCec.gpio);/*发送端持续拉低0.6ms后释放cec,即发送logic 1*/
udelay(100);
while(gpio_get_value(GPIOCec.gpio)== 0);/*等待回到高电平*/
do_gettimeofday(&ts_end);
time= GPIO_CecComputePeriod(&ts_start, &ts_end);/*计算低电平持续时间*/
if(time> 1300 && time < 1700) {
udelay(900);
return0;/*延时掉logic0 后续的高电平时间*/
}
else{
udelay(1800);
return1; /*延时掉logic1 后续的高电平时间*/
}
}
1.4.5 Header Block Details
这里的initator、destination指的是发送端和接收端的逻辑地址(LA)。
不同类型设备的LA在spec里的定义:
CEC_LOGADDR_TV = 0x00,
CEC_LOGADDR_PLAYBACK1 = 0x04, // DVD1 in Spev v1.3
CEC_LOGADDR_UNREGORBC = 0x0F/*广播地址*/
填充Header Block里的LA地址:
#defineMAKE_SRCDEST( src, dest) (( src << 4) | dest )
GPIOCec.address_logic = CEC_LOGADDR_PLAYBACK1;/*需要polling到可用的LA*/
cecFrame.srcDestAddr = MAKE_SRCDEST( GPIOCec.address_logic, CEC_LOGADDR_TV);
1.5 Logicaddress 和Physical address
PA是从电视的edid里拿出来的,LA是设备自己发message polling得到的。
PA:在edid的VSDB的第4和第5字节,PA =((data4 << 8) | data5);
LA :发送端将Header Block里的initator、destination都填上CEC_LOGADDR_PLAYBACK1,发出去。看看有没有收到ACK,没有收到就采用该地址作为自己的LA。如果有收到ACK,表示该LA已经被其他设备使用。改用CEC_LOGADDR_PLAYBACK2再polling。。。
广播地址LA: CEC_LOGADDR_UNREGORBC = 0x0F/*广播地址*/
接收端收到广播,无需回应ACK。如果回ACK,反而表示拒绝该广播。
1,Gpio模拟CEC 软件
Github上,完整的Rockchip使用gpio模拟cec的代码:
下载下来的部分RK代码和在MTK平台上使用的部分代码:
2.1 软件结构
2.2 CEC软件在mtk平台上的修改
1.2.1 使用pinctrl操作gpio。
1.2.2 加入local_irq_disable()防止发送时候被中断打断。
1.2.3 使用udelay替代usleep_range。
1.2.4 修改部分ACK\NAK错误。
1.2.5 由于平台限制,RX的eint无法实现,RX未验证。
1.2.6 mtk平台测试 gpio-cec.c
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/slab.h>
#include <linux/time.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
//#include <mach/gpio.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include "gpio-cec.h"
GPIO_Cec_t GPIOCec;
static void GPIOCecWorkFunc(struct work_struct *work);
static void GPIO_CecStartRead(void);
static char LA_Player[3] = { CEC_LOGADDR_PLAYBACK1, CEC_LOGADDR_PLAYBACK2, CEC_LOGADDR_PLAYBACK3 };
size_t hdmi_gpio_cec_log = hdmi_gpio_cec_log_all;
struct platform_device *hdmitx_pdev = NULL;
struct pinctrl *pinctrl;
struct pinctrl_state *pins_cec_default;
struct pinctrl_state *pinctrl_hdmi_dtsi_soft_cec_pin_in;
struct pinctrl_state *pinctrl_hdmi_dtsi_soft_cec_pin_out_high;
struct pinctrl_state *pinctrl_hdmi_dtsi_soft_cec_pin_out_low;
struct pinctrl_state *pinctrl_hdmi_dtsi_sda_out_high;
struct pinctrl_state *pinctrl_hdmi_dtsi_sda_out_low;
struct pinctrl_state *pinctrl_hdmi_dtsi_sda;
struct pinctrl_state *pinctrl_hdmi_dtsi_sda_in;
struct pinctrl_state *pinctrl_hdmi_dtsi_sck_out_high;
struct pinctrl_state *pinctrl_hdmi_dtsi_sck_out_low;
struct pinctrl_state *pinctrl_hdmi_dtsi_sck;
static inline unsigned int GPIO_CecComputePeriod(struct timeval *pre_time, struct timeval *cur_time)
{
if(pre_time->tv_sec == 0 && pre_time->tv_usec == 0) {
HDMI_GPIO_CEC_LOG_DBG("%s tv_sec == 0 && pre_time->tv_usec == 0\n", __FUNCTION__);
return 0;
} else
return (cur_time->tv_sec - pre_time->tv_sec)*1000000 + (cur_time->tv_usec - pre_time->tv_usec);
}
static void GPIO_CecSubmitWork(int event, int delay, void *data)
{
struct GPIO_Cec_delayed_work *work;
HDMI_GPIO_CEC_LOG_DBG("%s event %04x delay %d\n", __FUNCTION__, event, delay);
work = kmalloc(sizeof(struct GPIO_Cec_delayed_work), GFP_ATOMIC);
if (work) {
INIT_DELAYED_WORK(&work->work, GPIOCecWorkFunc);
work->event = event;
work->data = data;
queue_delayed_work(GPIOCec.workqueue,
&work->work,
msecs_to_jiffies(delay));
} else {
HDMI_GPIO_CEC_LOG_DBG("GPIO CEC: Cannot allocate memory to "
"create work\n");;
}
}
int cpu_irq_disable_times = 0;
int is_cpu_irq_disable = 0;
static void cec_local_irq_enable(unsigned int enable)
{
if (enable == 1 && cpu_irq_disable_times == 0)
return;
if (enable == 0) {
cpu_irq_disable_times = cpu_irq_disable_times + 1;
} else {
cpu_irq_disable_times = cpu_irq_disable_times - 1;
}
if (cpu_irq_disable_times == 1 && enable == 0) {
local_irq_disable();
is_cpu_irq_disable = 1;
} else if (cpu_irq_disable_times == 0 && enable == 1) {
local_irq_enable();
is_cpu_irq_disable = 0;
}
}
#if GPIO_CEC_DISABLE_LOCAL_IRQ
static void cec_udelay(unsigned long u)
{
cec_local_irq_enable(0);
udelay(u);
cec_local_irq_enable(1);
}
static void cec_send_bit(char bit)
{
//struct timeval ts_start, ts_end;
//unsigned int time_low, time_high;
if (is_cpu_irq_disable == 1) {
HDMI_GPIO_CEC_LOG_DBG("[ERROR]cec_send_bit is_cpu_irq_disable=1\n");
}
GPIO_CecPinSet(SDA_PINCTRL_MODE_OUT_LOW);//chunhui
GPIO_CecPinSet(CEC_PINCTRL_MODE_OUT_LOW);//chunhui
//gpio_set_value(GPIOCec.gpio, GPIO_LOW);
//gpio_direction_output(GPIOCec.gpio, GPIO_DIR_OUT);//chunhui
//gpio_set_value(GPIOCec.gpio, GPIO_LOW);//chunhui
if(bit == BIT_0)
cec_udelay(BIT_0_LOWLEVEL_PERIOD_NOR);
else if(bit == BIT_1)
cec_udelay(BIT_1_LOWLEVEL_PERIOD_NOR);
else {
//do_gettimeofday(&ts_start);
cec_udelay(BIT_START_LOWLEVEL_PERIOD_NOR);
}
//gpio_set_value(GPIOCec.gpio, GPIO_HIGH);
GPIO_CecPinSet(CEC_PINCTRL_MODE_OUT_HIGH);//chunhui
//gpio_direction_output(GPIOCec.gpio, GPIO_DIR_OUT);//chunhui
//gpio_set_value(GPIOCec.gpio, GPIO_HIGH);//chunhui
if(bit == BIT_0)
cec_udelay(BIT_0_HIGHLEVEL_PERIOD_NOR - BIT_0_LOWLEVEL_PERIOD_NOR);
else if(bit == BIT_1)
cec_udelay(BIT_1_HIGHLEVEL_PERIOD_NOR - BIT_1_LOWLEVEL_PERIOD_NOR);
else {
//do_gettimeofday(&ts_end);
//time_low = GPIO_CecComputePeriod(&ts_start, &ts_end);
//do_gettimeofday(&ts_start);
cec_udelay(BIT_START_HIGHLEVEL_PERIOD_NOR - BIT_START_LOWLEVEL_PERIOD_NOR);
//do_gettimeofday(&ts_end);
//time_high = GPIO_CecComputePeriod(&ts_start, &ts_end);
//HDMI_GPIO_CEC_LOG_DBG("START,time_low=%d,time_high=%d\n", time_low, time_high);
}
GPIO_CecPinSet(SDA_PINCTRL_MODE_OUT_HIGH);//chunhui
GPIO_CecPinSet(SDA_PINCTRL_MODE);//chunhui
}
#else
static void cec_send_bit(char bit)
{
#if GPIO_CEC_USE_HRTIMER
struct timeval ts_start, ts_end;
unsigned int time_low, time_high;
gpio_set_value(GPIOCec.gpio, GPIO_LOW);
//gpio_direction_output(GPIOCec.gpio, GPIO_DIR_OUT);//chunhui
//gpio_set_value(GPIOCec.gpio, GPIO_LOW);//chunhui
if(bit == BIT_0)
usleep_range(BIT_0_LOWLEVEL_PERIOD_MIN, BIT_0_LOWLEVEL_PERIOD_MAX);
else if(bit == BIT_1)
usleep_range(BIT_1_LOWLEVEL_PERIOD_MIN, BIT_1_LOWLEVEL_PERIOD_MAX);
else {
do_gettimeofday(&ts_start);
usleep_range(BIT_START_LOWLEVEL_PERIOD_MIN, BIT_START_LOWLEVEL_PERIOD_MAX);
}
gpio_set_value(GPIOCec.gpio, GPIO_HIGH);
//gpio_direction_output(GPIOCec.gpio, GPIO_DIR_OUT);//chunhui
//gpio_set_value(GPIOCec.gpio, GPIO_HIGH);//chunhui
if(bit == BIT_0)
usleep_range(BIT_0_HIGHLEVEL_PERIOD_MIN - BIT_0_LOWLEVEL_PERIOD_MIN, BIT_0_HIGHLEVEL_PERIOD_MAX - BIT_0_LOWLEVEL_PERIOD_MAX);
else if(bit == BIT_1)
usleep_range(BIT_1_HIGHLEVEL_PERIOD_MIN - BIT_1_LOWLEVEL_PERIOD_MIN, BIT_1_HIGHLEVEL_PERIOD_MAX - BIT_1_LOWLEVEL_PERIOD_MAX);
else {
do_gettimeofday(&ts_end);
time_low = GPIO_CecComputePeriod(&ts_start, &ts_end);
do_gettimeofday(&ts_start);
usleep_range(BIT_START_HIGHLEVEL_PERIOD_MIN - BIT_START_LOWLEVEL_PERIOD_MIN, BIT_START_HIGHLEVEL_PERIOD_MAX - BIT_START_LOWLEVEL_PERIOD_MAX);
do_gettimeofday(&ts_end);
time_high = GPIO_CecComputePeriod(&ts_start, &ts_end);
HDMI_GPIO_CEC_LOG_DBG("START,time_low=%d,time_high=%d\n", time_low, time_high);
}
#else
struct timeval ts_start, ts_end;
unsigned int time = 0 ,count = 0, last_time;
gpio_set_value(GPIOCec.gpio, GPIO_LOW);
do_gettimeofday(&ts_start);
while(1) {
do_gettimeofday(&ts_end);
last_time = time;
time = GPIO_CecComputePeriod(&ts_start, &ts_end);
count++;
if( (( bit == BIT_1) && (time > BIT_1_LOWLEVEL_PERIOD_NOR)) ||
(( bit == BIT_0) && (time > BIT_0_LOWLEVEL_PERIOD_NOR)) ||
(( bit == BIT_START) && (time > BIT_START_LOWLEVEL_PERIOD_NOR)) )
{
// HDMI_GPIO_CEC_LOG_DBG(" 0 level is %d %d lasttiem %d\n", time, count, last_time);
break;
}
}
gpio_set_value(GPIOCec.gpio, GPIO_HIGH);
count = 0;
while(1) {
do_gettimeofday(&ts_end);
last_time = time;
time = GPIO_CecComputePeriod(&ts_start, &ts_end);
count++;
if( (( bit == BIT_1) && (time > BIT_1_HIGHLEVEL_PERIOD_NOR)) ||