1#!/usr/bin/perl -w
2#
3# Copyright (C) 2004-2008  Jean Delvare <jdelvare@suse.de>
4#
5# Parts inspired from decode-edid.
6# Copyright (C) 2003-2004  Jean Delvare
7#
8# Parts inspired from the ddcmon driver and sensors' print_ddcmon function.
9# Copyright (C) 1998-2004  Mark D. Studebaker
10#
11# Parts inspired from the fbmon driver (Linux 2.6.10).
12# Copyright (C) 2002  James Simmons <jsimmons@users.sf.net>
13#
14#    This program is free software; you can redistribute it and/or modify
15#    it under the terms of the GNU General Public License as published by
16#    the Free Software Foundation; either version 2 of the License, or
17#    (at your option) any later version.
18#
19#    This program is distributed in the hope that it will be useful,
20#    but WITHOUT ANY WARRANTY; without even the implied warranty of
21#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22#    GNU General Public License for more details.
23#
24#    You should have received a copy of the GNU General Public License
25#    along with this program; if not, write to the Free Software
26#    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
27#    MA 02110-1301 USA.
28#
29# This script is a replacement for the now deprecated ddcmon kernel driver.
30# Instead of having a dedicated driver, it is better to reuse the standard
31# eeprom driver and implement the EDID-specific code in user-space.
32#
33# EDID (Extended Display Identification Data) is a VESA standard which
34# allows storing (on manufacturer's side) and retrieving (on user's side)
35# of configuration information about displays, such as manufacturer,
36# serial number, physical dimensions and allowed horizontal and vertical
37# refresh rates.
38#
39# Syntax: ddcmon [bus [address]]
40# Address can be given in decimal or hexadecimal (with a 0x prefix).
41# If no address is given, default is 0x50.
42# Bus number must be given in decimal. If no bus number is given,
43# try them all.
44
45use strict;
46use Fcntl qw(:DEFAULT :seek);
47use vars qw(@standard_scales);
48
49@standard_scales = ([1, 1], [3, 4], [4, 5], [16, 9]);
50
51# Make sure the eeprom module is loaded.
52# For non-modular kernels, we can't help.
53if (-r '/proc/modules')
54{
55	my $found = 0;
56	open(MODULES, '/proc/modules');
57	while (!$found && defined ($_ = <MODULES>))
58	{
59		$found++ if m/^eeprom\s+/;
60	}
61	close(MODULES);
62
63	unless ($found)
64	{
65		print STDERR
66			"This script requires the eeprom module to be loaded.\n";
67		exit 1;
68	}
69}
70
71# Only used for sysfs
72sub rawread
73{
74	my $filename = shift;
75	my $length = shift;
76	my $offset = shift || 0;
77	my $bytes = '';
78
79	sysopen(FH, $filename, O_RDONLY)
80		or die "Can't open $filename";
81	if ($offset)
82	{
83		sysseek(FH, $offset, SEEK_SET)
84			or die "Can't seek in $filename";
85	}
86
87	$offset = 0;
88	while ($length)
89	{
90		my $r = sysread(FH, $bytes, $length, $offset);
91		die "Can't read $filename"
92			unless defined($r);
93		die "Unexpected EOF in $filename"
94			unless $r;
95		$offset += $r;
96		$length -= $r;
97	}
98	close(FH);
99
100	return $bytes;
101}
102
103sub get_edid_sysfs
104{
105	my ($bus, $addr) = @_;
106
107	my @bytes = unpack("C*", rawread("/sys/bus/i2c/devices/$bus-00$addr/eeprom", 128));
108
109	return \@bytes;
110}
111
112sub get_edid_procfs
113{
114	my ($bus, $addr) = @_;
115
116	my @bytes;
117
118	for (my $i = 0 ; $i < 0x80; $i += 0x10)
119	{
120		my $filename = sprintf("/proc/sys/dev/sensors/eeprom-i2c-\%s-\%s/\%02x",
121				       $bus, $addr, $i);
122		open(EEDATA, $filename)
123			or die "Can't read $filename";
124		push @bytes, split(/\s+/, <EEDATA>);
125		close(EEDATA);
126	}
127
128	return \@bytes;
129}
130
131sub print_line
132{
133	my $label = shift;
134	my $pattern = shift;
135
136	printf("\%-24s$pattern\n", $label.':', @_);
137}
138
139sub extract_byte
140{
141	my ($bytes, $offset) = @_;
142
143	return $bytes->[$offset];
144}
145
146sub extract_word
147{
148	my ($bytes, $offset) = @_;
149
150	return ($bytes->[$offset]
151	      | ($bytes->[$offset+1] << 8));
152}
153
154sub extract_manufacturer
155{
156	my ($bytes, $offset) = @_;
157	my $i = ($bytes->[$offset+1] | ($bytes->[$offset] << 8));
158
159	return sprintf('%c%c%c',
160		       (($i >> 10) & 0x1f) + ord('A') - 1,
161		       (($i >> 5) & 0x1f) + ord('A') - 1,
162		       ($i & 0x1f) + ord('A') - 1);
163}
164
165sub extract_sesquiword
166{
167	my ($bytes, $offset) = @_;
168
169	return ($bytes->[$offset]
170	      | ($bytes->[$offset+1] << 8)
171	      | ($bytes->[$offset+2] << 16));
172}
173
174sub extract_dword
175{
176	my ($bytes, $offset) = @_;
177
178	return ($bytes->[$offset]
179	      | ($bytes->[$offset+1] << 8)
180	      | ($bytes->[$offset+2] << 16)
181	      | ($bytes->[$offset+3] << 24));
182}
183
184sub extract_display_input
185{
186	my ($bytes, $offset) = @_;
187
188	my @voltage = ('0.700V/0.300V', '0.714V/0.286V',
189		       '1.000V/0.400V', '0.700V/0.000V');
190
191	return 'Digital'
192		if ($bytes->[$offset] & 0x80);
193
194	return 'Analog ('.$voltage[($bytes->[$offset] & 0x60) >> 5].')';
195}
196
197sub extract_dpms
198{
199	my ($bytes, $offset) = @_;
200
201	my @supported;
202
203	push @supported, 'Active Off' if ($bytes->[$offset] & 0x20);
204	push @supported, 'Suspend' if ($bytes->[$offset] & 0x40);
205	push @supported, 'Standby' if ($bytes->[$offset] & 0x80);
206
207	return join(', ', @supported)
208		if (@supported);
209
210	return 'None supported';
211}
212
213sub extract_color_mode
214{
215	my ($bytes, $offset) = @_;
216
217	my @mode = ('Monochrome', 'RGB Multicolor', 'Non-RGB Multicolor');
218
219	return $mode[($bytes->[$offset] >> 3) & 0x03];
220}
221
222sub good_signature
223{
224	my $bytes = shift;
225
226	return $bytes->[0] == 0x00
227	    && $bytes->[1] == 0xff
228	    && $bytes->[2] == 0xff
229	    && $bytes->[3] == 0xff
230	    && $bytes->[4] == 0xff
231	    && $bytes->[5] == 0xff
232	    && $bytes->[6] == 0xff
233	    && $bytes->[7] == 0x00;
234}
235
236sub verify_checksum
237{
238	my $bytes = shift;
239	my $cs;
240
241	for (my $i = 0, $cs = 0; $i < 0x80; $i++)
242	{
243		$cs += $bytes->[$i];
244	}
245
246	return (($cs & 0xff) == 0 ? 'OK' : 'Not OK');
247}
248
249sub add_timing
250{
251	my ($timings, $new) = @_;
252
253	my $mode = sprintf('%ux%u@%u%s', $new->[0], $new->[1],
254			$new->[2], defined ($new->[3]) &&
255			$new->[3] eq 'interlaced' ? 'i' : '');
256
257	$timings->{$mode} = $new;
258}
259
260sub add_standard_timing
261{
262	my ($timings, $byte0, $byte1) = @_;
263
264	# Unused slot
265	return if ($byte0 == $byte1)
266	       && ($byte0 == 0x01 || $byte0 == 0x00 || $byte0 == 0x20);
267
268	my $width = ($byte0 + 31) * 8;
269	my $height = $width * $standard_scales[$byte1 >> 6]->[0]
270			    / $standard_scales[$byte1 >> 6]->[1];
271	my $refresh = 60 + ($byte1 & 0x3f);
272
273	add_timing($timings, [$width, $height, $refresh]);
274}
275
276sub sort_timings
277{
278	# First order by width
279	return -1 if  $a->[0] < $b->[0];
280	return 1 if  $a->[0] > $b->[0];
281
282	# Second by height
283	return -1 if  $a->[1] < $b->[1];
284	return 1 if  $a->[1] > $b->[1];
285
286	# Third by frequency
287	# Interlaced modes count for half their frequency
288	my $freq_a = $a->[2];
289	my $freq_b = $b->[2];
290	$freq_a /= 2 if defined $a->[3] && $a->[3] eq 'interlaced';
291	$freq_b /= 2 if defined $b->[3] && $b->[3] eq 'interlaced';
292	return -1 if $freq_a < $freq_b;
293	return 1 if $freq_a > $freq_b;
294
295	return 0;
296}
297
298sub print_timings
299{
300	my ($bytes, $timings) = @_;
301
302	# Established Timings
303	my @established =
304	(
305		[720, 400, 70],
306		[720, 400, 88],
307		[640, 480, 60],
308		[640, 480, 67],
309		[640, 480, 72],
310		[640, 480, 75],
311		[800, 600, 56],
312		[800, 600, 60],
313		[800, 600, 72],
314		[800, 600, 75],
315		[832, 624, 75],
316		[1024, 768, 87, 'interlaced'],
317		[1024, 768, 60],
318		[1024, 768, 70],
319		[1024, 768, 75],
320		[1280, 1024, 75],
321		undef, undef, undef,
322		[1152, 870, 75],
323	);
324	my $temp = extract_sesquiword($bytes, 0x23);
325	for (my $i = 0; $i < 24; $i++)
326	{
327		next unless defined($established[$i]);
328		add_timing($timings, $established[$i])
329			if ($temp & (1 << $i));
330	}
331
332	# Standard Timings
333	for (my $i = 0x26; $i < 0x36; $i += 2)
334	{
335		add_standard_timing($timings, $bytes->[$i], $bytes->[$i+1]);
336	}
337
338	foreach my $v (sort sort_timings values(%{$timings}))
339	{
340		print_line("Timing", '%ux%u @ %u Hz%s',
341			   $v->[0], $v->[1], $v->[2],
342			   defined($v->[3]) ? ' ('.$v->[3].')' : '');
343	}
344}
345
346sub extract_string
347{
348	my ($bytes, $offset) = @_;
349	my $string = '';
350
351	for (my $i = 5; $i < 18; $i++)
352	{
353		last if $bytes->[$offset+$i] == 0x0a
354		     || $bytes->[$offset+$i] == 0x00;
355		$string .= chr($bytes->[$offset+$i])
356			if ($bytes->[$offset+$i] >= 32
357			&& $bytes->[$offset+$i] < 127);
358	}
359	$string =~ s/\s+$//;
360
361	return $string;
362}
363
364# Some blocks contain different information:
365#   0x00, 0x00, 0x00, 0xfa: Additional standard timings block
366#   0x00, 0x00, 0x00, 0xfc: Monitor block
367#   0x00, 0x00, 0x00, 0xfd: Limits block
368#   0x00, 0x00, 0x00, 0xfe: Ascii block
369#   0x00, 0x00, 0x00, 0xff: Serial block
370# Return a reference to a hash containing all information.
371sub extract_detailed_timings
372{
373	my ($bytes) = @_;
374
375	my %info = ('timings' => {});
376
377	for (my $offset = 0x36; $offset < 0x7e; $offset += 18)
378	{
379		if ($bytes->[$offset] == 0x00
380		 && $bytes->[$offset+1] == 0x00
381		 && $bytes->[$offset+2] == 0x00
382		 && $bytes->[$offset+4] == 0x00)
383		{
384			if ($bytes->[$offset+3] == 0xfa)
385			{
386				for (my $i = $offset + 5; $i < $offset + 17; $i += 2)
387				{
388					add_standard_timing($info{'timings'},
389							    $bytes->[$i],
390							    $bytes->[$i+1]);
391				}
392			}
393
394			elsif ($bytes->[$offset+3] == 0xfc)
395			{
396				$info{'monitor'} .= extract_string($bytes, $offset);
397			}
398
399			elsif ($bytes->[$offset+3] == 0xfd)
400			{
401				$info{'limits'}{'vsync_min'} = $bytes->[$offset+5];
402				$info{'limits'}{'vsync_max'} = $bytes->[$offset+6];
403				$info{'limits'}{'hsync_min'} = $bytes->[$offset+7];
404				$info{'limits'}{'hsync_max'} = $bytes->[$offset+8];
405				$info{'limits'}{'clock_max'} = $bytes->[$offset+9];
406			}
407
408			elsif ($bytes->[$offset+3] == 0xfe)
409			{
410				$info{'ascii'} .= extract_string($bytes, $offset);
411			}
412
413			elsif ($bytes->[$offset+3] == 0xff)
414			{
415				$info{'serial'} .= extract_string($bytes, $offset);
416			}
417
418			next;
419		}
420
421		# Detailed Timing
422		my $width = $bytes->[$offset+2] + (($bytes->[$offset+4] & 0xf0) << 4);
423		my $height = $bytes->[$offset+5] + (($bytes->[$offset+7] & 0xf0) << 4);
424		my $clock = extract_word($bytes, $offset) * 10000;
425		my $hblank = $bytes->[$offset+3] + (($bytes->[$offset+4] & 0x0f) << 8);
426		my $vblank = $bytes->[$offset+6] + (($bytes->[$offset+7] & 0x0f) << 8);
427		my $area = ($width + $hblank) * ($height + $vblank);
428		next unless $area; # Should not happen, but...
429		my $refresh = ($clock + $area / 2) / $area; # Proper rounding
430		add_timing($info{'timings'}, [$width, $height, $refresh]);
431	}
432
433	return \%info;
434}
435
436sub print_edid
437{
438	my ($bus, $address) = @_;
439	my $bytes;
440
441	if (-r "/sys/bus/i2c/devices/$bus-00$address/eeprom")
442	{
443		$bytes = get_edid_sysfs($bus, $address);
444	}
445	elsif (-r "/proc/sys/dev/sensors/eeprom-i2c-$bus-$address/00")
446	{
447		$bytes = get_edid_procfs($bus, $address);
448	}
449
450	return 1 unless defined $bytes;
451	return 2 unless good_signature($bytes);
452
453	print_line('Checksum', '%s', verify_checksum($bytes));
454	my $edid_version = extract_byte($bytes, 0x12);
455	my $edid_revision = extract_byte($bytes, 0x13);
456	print_line('EDID Version', '%u.%u', $edid_version,
457		   $edid_revision);
458	if ($edid_version > 1 || $edid_revision > 2)
459	{
460		$standard_scales[0][0] = 16;
461		$standard_scales[0][1] = 10;
462	}
463	else
464	{
465		$standard_scales[0][0] = 1;
466		$standard_scales[0][1] = 1;
467	}
468
469	my $info = extract_detailed_timings($bytes);
470
471	print_line('Manufacturer ID', '%s', extract_manufacturer($bytes, 0x08));
472	print_line('Model Number', '0x%04X', extract_word($bytes, 0x0A));
473	print_line('Model Name', '%s', $info->{'monitor'})
474		if defined $info->{'monitor'};
475
476	if ($info->{'serial'})
477	{
478		print_line('Serial Number', '%s', $info->{'serial'})
479	}
480	elsif ((my $temp = extract_dword($bytes, 0x0C)))
481	{
482		print_line('Serial Number', '%u', $temp)
483	}
484
485	print_line('Manufacture Time', '%u-W%02u',
486		   1990 + extract_byte($bytes, 0x11),
487		   extract_byte($bytes, 0x10));
488	print_line('Display Input', '%s', extract_display_input($bytes, 0x14));
489	print_line('Monitor Size (cm)', '%ux%u', extract_byte($bytes, 0x15),
490		   extract_byte($bytes, 0x16));
491	print_line('Gamma Factor', '%.2f',
492		   1 + extract_byte($bytes, 0x17) / 100.0);
493	print_line('DPMS Modes', '%s', extract_dpms($bytes, 0x18));
494	print_line('Color Mode', '%s', extract_color_mode($bytes, 0x18))
495		if (($bytes->[0x18] & 0x18) != 0x18);
496	print_line('Additional Info', '%s', $info->{'ascii'})
497		if $info->{'ascii'};
498
499	if (defined($info->{'limits'}))
500	{
501		print_line('Vertical Sync (Hz)', '%u-%u',
502			   $info->{'limits'}{'vsync_min'},
503			   $info->{'limits'}{'vsync_max'});
504		print_line('Horizontal Sync (kHz)', '%u-%u',
505			   $info->{'limits'}{'hsync_min'},
506			   $info->{'limits'}{'hsync_max'});
507		print_line('Max Pixel Clock (MHz)', '%u',
508			   $info->{'limits'}{'clock_max'} * 10)
509			unless $info->{'limits'}{'clock_max'} == 0xff;
510	}
511
512	print_timings($bytes, $info->{'timings'});
513	print("\n");
514	return 0;
515}
516
517# Get the address. Default to 0x50 if not given.
518my $address;
519if (defined($ARGV[1]))
520{
521	$address = $ARGV[1];
522	# Convert to decimal, whatever the value.
523	$address = oct $address if $address =~ m/^0/;
524	# Convert to an hexadecimal string.
525	$address = sprintf '%02x', $address;
526}
527else
528{
529	$address = '50';
530}
531
532if (defined($ARGV[0]))
533{
534	my $error = print_edid($ARGV[0], $address);
535
536	if ($error == 1)
537	{
538		print STDERR
539			"No EEPROM found at 0x$address on bus $ARGV[0].\n";
540		exit 1;
541	}
542	elsif ($error == 2)
543	{
544		print STDERR
545			"EEPROM found at 0x$address on bus $ARGV[0], but is not an EDID EEPROM.\n";
546		exit 1;
547	}
548}
549# If no bus is given, try them all.
550else
551{
552	my $total = 0;
553
554	for (my $i = 0; $i < 16; $i++)
555	{
556		$total++ unless print_edid($i, $address);
557	}
558
559	unless ($total)
560	{
561		print STDERR
562			"No EDID EEPROM found.\n";
563		exit 1;
564	}
565}
566