#!/usr/bin/perl # # Simple tool for generating a USB desciptor table or ROM/RAM # use integer; use strict; use File::Spec; use Encode; require 'langid.ph'; # Some forward declarations my($mode, $infile, $outfile) = @ARGV; my $module_name; my $err = 0; # Descriptor types. This follows the document in which they were specified; # some USB documents specify in hex and others in decimal... our %DT = ( 'device' => 1, 'configuration' => 2, 'config' => 2, 'conf' => 2, 'string' => 3, 'str' => '3', 'interface' => 4, 'if' => '4', 'endpoint' => 5, 'ep' => 5, 'device_qualifier' => 6, 'dq' => 6, 'other_speed_configuration' => 7, 'otherspeed' => 7, 'osc' => 7, 'interface_power' => 8, 'ifpwr' => 8, 'otg' => 9, 'debug' => 10, 'interface_association' => 11, 'iad' => 11, 'bos' => 15, 'binary_object_storage' => 15, 'device_capability' => 16, 'devcap' => 16, 'cs_interface' => 0x24, 'cs_if' => 0x24, 'cs_endpoint' => 0x25, 'cs_ep' => 0x25, 'superspeed_usb_endpoint_companion' => 48, 'usb_ep_companion' => 48, 'superspeedplus_isochronous_endpoint_companion' => 49, 'iso_ep_compation' => 49, ); # Descriptor subtypes, where applicable our %DS = ( # Under CS_INTERFACE 0x24 => { 'header' => 0x00, 'call' => 0x01, 'call_management' => 0x01, 'abstract_control' => 0x02, 'acm' => 0x02, 'direct_line' => 0x03, 'ringer' => 0x04, 'line_state' => 0x05, 'union' => 0x06, 'country' => 0x07, 'op_mode' => 0x08, 'terminal' => 0x09, 'net_channel' => 0x0a, 'protocol_unit' => 0x0b, 'extension_unit' => 0x0c, 'multi_channel' => 0x0d, 'capi' => 0x0e, 'ethernet' => 0x0f, 'atm' => 0x10, 'whcm' => 0x11, 'wireless_handset' => 0x11, 'mobile_line' => 0x12, 'mobile_direct_line' => 0x12, 'mdlm_detail' => 0x13, 'mdlm' => 0x13, 'device_management' => 0x14, 'device' => 0x14, 'mgmt' => 0x14, 'command_set' => 0x16, 'command_set_detail' => 0x17, 'telephone_control' => 0x18, 'tcm' => 0x18, 'phone' => 0x18, 'obex_service_identifier' => 0x19, 'obex' => 0x19 } ); # # Class, subclass, and protocol codes. Map a string to a number, then # use that number as an index to descend, if that entry exists. 0 is # the default; for some classes the subclass code is unused and so is # set to 0; no string to look up. # # Numbers below the class level are massively incomplete, feel free to add. # # Applies to all levels my %class_all_levels = ( undef => 0x00, 'undef' => 0x00, '-' => 0x00, 'none' => 0x00, 'default' => 0x00, 'vendor_specific' => 0xff, 'vendor' => 0xff, ); my $cdc_pstn_protocols = { 'v250' => 0x01, 'at' => 0x01, 'v25ter' => 0x01, 'pcca101' => 0x02, 'pcca101o' => 0x03, 'pcca' => 0x03, 'gsm707' => 0x04, 'gsm' => 0x04, '3gpp2707' => 0x05, '3gpp' => 0x05, '3g' => 0x05, 'ca00170' => 0x06, 'tia_cdma' => 0x06, 'cdma' => 0x06 }; my %class_codes = ( 'multi' => 0x00, # Real class code in interface descriptors 'audio' => 0x01, 'cdc' => 0x02, 'communications' => 0x02, 0x02 => { 'dlcm' => 0x01, 'direct_line_control' => 0x01, 'acm' => 0x02, 'abstract_control' => 0x02, 0x02 => $cdc_pstn_protocols, 'tcm' => 0x03, 'telephone_control' => 0x03, 0x03 => $cdc_pstn_protocols, 'mccm' => 0x04, 'multi_channel_control' => 0x04, 0x04 => $cdc_pstn_protocols, 'ccm' => 0x05, 'capi' => 0x05, 'capi_control' => 0x05, 0x05 => $cdc_pstn_protocols, 'ecm' => 0x06, 'ethernet_control' => 0x06, 0x06 => $cdc_pstn_protocols, 'atm' => 0x07, 'ancm' => 0x07, 'atm_networking_control' => 0x07, 'whcm' => 0x08, 'wireless_handset_control' => 0x08, 0x08 => $cdc_pstn_protocols, 'device_management' => 0x09, 'mgmt' => 0x09, 'device' => 0x09, 'mdlm' => 0x0a, 'mobile_direct_line' => 0x0a, 'obex' => 0x0b, 'eem' => 0x0c, 'ethernet' => 0x0c, 'ethernet_emulation' => 0x0c, 0x0c => { 'ethernet' => 0x07, 'eem' => 0x07 }, 'ncm' => 0x0d, 'net' => 0x0d, 'network_control' => 0x0d, }, 'hid' => 0x03, 'physical' => 0x05, 'imaging' => 0x06, 'photo' => 0x06, 'idc' => 0x06, 'printer' => 0x07, 'mass_storage' => 0x08, 'storage' => 0x08, 'disk' => 0x08, 'hub' => 0x09, 'cdc_data' => 0x0a, 'data' => 0x0a, 0x0a => { 0 => { 'ntb' => 0x01, 'network_transfer_block' => 0x01, 'isdn_bri' => 0x30, 'isdn' => 0x30, 'hdlc' => 0x31, 'transparent' => 0x32, 'q921_management' => 0x50, 'q921m' => 0x50, 'q921_datalink' => 0x51, 'q921' => 0x51, 'q921_tei_mux' => 0x52, 'q921tm' => 0x52, 'v42bis' => 0x90, 'euro_isdn' => 0x91, 'q931' => 0x91, 'v120' => 0x92, 'isdn_v24' => 0x92, 'capi' => 0x93, 'host' => 0xfd, 'external' => 0xfe, 'vendor' => 0xff }, }, 'smart_card' => 0x0b, 'smartcard' => 0x0b, 'scdc' => 0x0b, 'content_security' => 0x0d, 'drm' => 0x0d, 'csdc' => 0x0d, 'video' => 0x0e, 'vdc' => 0x0e, 'personal_healthcare' => 0x0f, 'healthcare' => 0x0f, 'health' => 0x0f, 'phdc' => 0x0f, 'audio_video' => 0x10, 'av' => 0x10, 'avdc' => 0x10, 'billboard' => 0x11, 'bdc' => 0x11, 'usb_c_bridge' => 0x12, 'usbc' => 0x12, 'usbcbdc' => 0x12, 'diagnostic' => 0xdc, 'wireless_controller' => 0xe0, 'wireless' => 0xe0, 'miscellaneous' => 0xef, 'misc' => 0xef, 'application_specific' => 0xfe, 'app_specific' => 0xfe, 'app' => 0xfe, ); my %packfmt = ( 1 => 'C', 2 => 'v', 4 => 'V', 8 => 'Q<' ); my $utf16le = find_encoding('utf16le'); sub atom($@) { my($bytes,$b,$adj) = @_; my @o = (); $adj = toint($adj); my $t; while (($t = ref $b) eq 'REF') { $b = $$b; } if ($t eq 'SCALAR') { # To be resolved later push(@o, {'bytes' => $bytes, 'num' => $b, 'adj' => $adj }); } elsif ($t eq '') { push(@o, pack($packfmt{$bytes}, $b+$adj)); } else { push(@o, {'bytes' => $bytes, 'data' => $b}); } return [@o]; } sub byte(@) { return atom(1,@_); } sub word(@) { return atom(2,@_); } sub dword(@) { return atom(4,@_); } sub qword(@) { return atom(8,@_); } # Generate endpoint identifiers sub ep_i($) { my($n) = @_; return byte($n,0x80); } sub ep_o($) { my($n) = @_; return byte($n,0x00); } # # General handy stuff... # # Decode an integer in C notation sub toint($) { my($i) = @_; return ($i =~ /^0/) ? oct $i : ($i =~ /^[1-9]/) ? $i+0 : undef; } # For sorting sub numeric { return $a <=> $b; } # Generate class code triplets sub usb_class($;$$) { my @cl = @_; my $lvl = \%class_codes; my $cd = ''; for (my $i = 0; $i < 3; $i++) { my $cs = shift(@cl); my $cc = defined($cs) ? toint($cs) : 0; if (!defined($cc)) { $cs = lc($cs); $cs =~ s/\P{Alnum}+/_/g; $cc = $lvl->{$cs} if (defined($lvl)); $cc = $class_all_levels{$cs} unless (defined($cc)); if (!defined($cc)) { print STDERR "$0: unknown class code ", join('.', @_), "\n"; $err = 1; $cc = 0; } } $cd .= pack('C', $cc); $lvl = $lvl->{$cc}; } return $cd; } sub datalen(@) { my $l = 0; foreach my $e (@_) { my $b = $e; my $t; while (($t = ref $b) eq 'REF') { $b = $$b; } if ($t eq 'HASH') { $l += $b->{'bytes'}; } elsif ($t eq 'ARRAY') { $l += datalen(@$b); } elsif ($t eq 'SCALAR') { $l += length($$b); } elsif ($t eq '') { $l += length($b); } else { die; } } return $l; } sub makedata(@) { my $o = ''; foreach my $e (@_) { my $b = $e; my $t; while (($t = ref $b) eq 'REF') { $b = $$b; } if ($t eq 'HASH') { unless (defined($b->{'raw'})) { if (defined(my $n = $b->{'num'})) { my $tt; my $adj = $b->{'adj'}; while (($tt = ref $n) eq 'REF') { $n = $$n; } if ($tt eq 'SCALAR') { $b->{'raw'} = pack($packfmt{$b->{'bytes'}}, $$n+$adj); } else { $b->{'raw'} = makedata($n); } } elsif (defined($b->{'data'})) { $b->{'raw'} = makedata($b->{'data'}); } else { die; } } $b->{'offs'} = length($o); $b->{'bytes'} = length($b->{'raw'}); $o .= $b->{'raw'}; } elsif ($t eq 'ARRAY') { $o .= makedata(@$b); } elsif ($t eq 'SCALAR') { $o .= makedata($$b); } elsif ($t eq '') { $o .= $b; } else { die; } } return $o; } # USB descriptor set my $u_self = { 'children' => \(my $children = 0), clist => [] }; sub usb_dset(&) { my($contents) = @_; my $parent = $u_self; my $children = 0; my $index = ${$u_self->{'children'}}++; my $ds = { 'type' => 'dset', 'parent' => $parent, 'data' => undef, 'bytes' => undef, 'raw' => undef, 'children' => \$children, 'index' => \$index, 'offs' => undef, 'clist' => [] }; $u_self = $ds; push(@{$parent->{'clist'}}, $ds); my @data = $contents->($ds, $parent); $ds->{'data'} = \@data; $ds->{'bytes'} = datalen(@data); $u_self = $parent; return $ds; } sub usb_totallen(;$) { my($r) = @_; $r = $u_self unless(defined($r)); return \$r->{'bytes'}; } sub usb_index(;$) { my($r) = @_; $r = $u_self unless(defined($r)); return $r->{'index'}; } sub usb_peers(;$) { my($r) = @_; $r = $u_self unless(defined($r)); return $r->{'parent'}{'children'}; } sub usb_children(;$) { my($r) = @_; $r = $u_self unless(defined($r)); return $r->{'children'}; } # USB descriptor sub usb_desc($@) { my($typestr, @data) = @_; my($type,$subtype) = split(/\./, $typestr, 2); my $tn; my $sn; my $hdr; my $dlen = datalen(@data); $tn = toint($type); $tn = $DT{lc($type)} unless (defined($tn)); die "$0: unknown descriptor type: $typestr\n" unless (defined($tn)); if (defined($subtype)) { $sn = toint($subtype); $sn = $DS{$tn}->{$subtype} unless (defined($sn)); die "$0: unknown descriptor type: $typestr\n" unless (defined($sn)); $dlen += 3; $hdr = pack('CCC', $dlen, $tn, $sn); } else { $dlen += 2; $hdr = pack('CC', $dlen, $tn); } return { 'type' => 'descriptor', 'data' => [$hdr, @data], 'bytes' => $dlen }; } # Device top level my $device_dset; sub usb_device(&) { my($contents) = @_; $device_dset = usb_dset(\&$contents); } # Additional USB data my $additional_dset; sub usb_additional_data(&) { my($contents) = @_; $additional_dset = usb_dset(\&$contents); } my @langlist; my %lang; my @lang_mask = (0xffff, 0x03ff, 0); # Masks for language codes my $stringdata; my %stringoffs; # Pointer into stringdata # Reserved string descriptor numbers my $strdesc_lang = 0; # 0 = reserved for language descriptors my $strdesc_serial = 1; # 1 = reserved for serial number (see below) my $strdesc_msft = 0xee; my %special_strings = ($strdesc_lang => undef, $strdesc_serial => undef, $strdesc_msft => undef); # The index of string descriptors are sets of descriptors which # match for ALL languages so they can be given the same index. my %strdesci = (); my @strdescs = (); # Register a string into the string table and return a descriptor index byte. # Input should be a hash. sub usb_string(%) { my(%strh) = @_; my $descval = ''; my %txts; my $found = 0; if (!%strh) { %strh = ( '' => '' ); # Null string descriptor } foreach my $l (keys(%strh)) { my $str = $strh{$l}; my $co = langid($l); next unless (defined($co)); foreach my $m (@lang_mask) { my $coi = $co & $m; next unless (defined($lang{$coi})); next if (defined($txts{$coi})); $txts{$coi} = $str; $found++; } } return pack('C', 0) unless ($found); foreach my $co (@langlist) { my $txt; foreach my $m (@lang_mask) { last if (defined($txt = $txts{$co & $m})); } my $utf16str = $utf16le->encode($txt, Encode::FB_WARN); unless (defined($stringoffs{$utf16str})) { $stringoffs{$utf16str} = length($stringdata); $stringdata .= pack('CC', length($utf16str)+2, $DT{string}); $stringdata .= $utf16str; } $descval .= pack('v', $stringoffs{$utf16str}); } my $descindex = $strdesci{$descval}; unless (defined($descindex)) { $descindex = scalar(@strdescs) - 1; while (exists($special_strings{++$descindex})) { push(@strdescs, undef); # Skip reserved index } $strdesci{$descval} = $descindex; push(@strdescs, $descval); } return pack('C', $descindex); } # # Special string descriptors. Currently supports the serial number, # but could also be used for the M$ special 0xEE string descriptor # with a bit more infrastructure. Noteworthy is that the serial number # descriptor is quite likely to need to be patched in the field or # at runtime. Thus, these special string descriptors come at the # very beginning of the data, in index order. # # Language codes are not supported for special strings. # # Usage: usb_special_string(index,'default',length) # sub usb_special_string($@) { my($ix,$dft,$len) = @_; if (defined($dft) || $len || !defined($special_strings{$ix})) { $dft = $utf16le->encode($dft, Encode::FB_WARN); my $dchar = length($dft) >> 1; if (!$len) { $len = $dchar; } elsif ($len < $dchar) { $dft = substr($dft, 0, $len << 1); } elsif ($len > $dchar) { # Pad with spaces $dft .= $utf16le->encode(' ' x ($len - $dchar)); } $special_strings{$ix} = pack('CC', ($len << 1)+2, $DT{string}).$dft; } return pack('C', $ix); } sub usb_serial($;$) { return usb_special_string($strdesc_serial, @_); } # # Register the possible languages # sub usb_languages(@) { my @langs = @_; # There are two differences between keys(%lang) and @langlist: the # former lists all possible language codes for which a lookup is # possible in no specific order, whereas @langlist is exactly the # items wanted in order in string descriptor zero. # # In other words, %lang is a reverse mapping of langlist, *plus* # permissible aliases. %lang = (); @langlist = (); foreach my $l (@langs) { my $co = langid($l); if (defined($co) && !defined($lang{$co})) { my $nlang = scalar(@langlist); push(@langlist, $co); # Defaults for partial languages? foreach my $m (@lang_mask) { if (!defined($lang{$co & $m})) { $lang{$co & $m} = $nlang; } } } } if (!scalar(@langlist)) { $stringdata = ''; } else { $stringdata = pack("CCv*", scalar(@langlist)*2 + 2, $DT{string}, @langlist); } } my $descriptor_data; my @descriptor_ptrs; my $additional_offs; my $additional_len; sub generate_data() { my $data = ''; my @ptrs = (); # Special string descriptors first foreach my $ssn (sort numeric keys(%special_strings)) { my $ss = $special_strings{$ssn}; next unless defined($ss); push(@ptrs, {'type' => $DT{string}, 'dindex' => $ssn, 'offs' => length($data), 'len' => length($ss)}); $data .= $ss; } # Device and configuration descriptors my $dev_offs = length($data); $data .= makedata($device_dset); foreach my $dc (@{$device_dset->{data}}) { my $offs = $dev_offs + $dc->{offs}; # Length and type of the *first* descriptor in a set my($flen,$ftype) = unpack('CC', substr($data, $offs, 2)); my $dindex = $dc->{index}; $dindex = $$dindex while (ref $dindex eq 'SCALAR'); push(@ptrs, {'type' => $ftype, 'dindex' => $dindex, 'offs' => $offs, 'len' => $dc->{bytes}}); } my $string_offs = length($data); # 'Regular' string descriptors push(@ptrs, {'type' => $DT{string}, 'dindex' => 0, 'offs' => $string_offs, 'len' => unpack('C', substr($stringdata, 0, 1))}); for (my $i = 1; $i < scalar(@strdescs); $i++) { my @sofs = unpack('v*', $strdescs[$i]); for (my $j = 0; $j < scalar(@sofs); $j++) { my $co = $langlist[$j]; my $of = $sofs[$j]; my $m = $lang_mask[0]; # Widen the mask as much as possible foreach my $mx (@lang_mask) { last unless ($lang{$co & $mx} == $lang{$co & $m}); $m = $mx; } push(@ptrs, {'type' => $DT{string}, 'dindex' => $i, 'windex' => $co & $m, 'wimask' => $m, 'offs' => $string_offs + $of, 'len' => unpack('C', substr($stringdata,$of,1))}); } } $data .= $stringdata; $additional_offs = length($data); if (defined($additional_dset)) { $data .= makedata($additional_dset); } $additional_len = length($data) - $additional_offs; $descriptor_data = $data; @descriptor_ptrs = @ptrs; return $descriptor_data; } # Output a number and a mask as a Verilog case with ? as needed sub v_case($$$) { my($b,$v,$m) = @_; my $o = sprintf("%d'b", $b); for (my $i = $b-1; $i >= 0; $i--) { my $ix = 1 << $i; $o .= ($m & $ix) ? (($v & $ix) ? '1' : '0') : '?'; $o .= '_' if ($i > 0 && ($i & 7) == 0); } return $o; } # Output a Verilog module sub output_verilog($) { my($out) = @_; my $bytes = length($descriptor_data); my $abits = 1; while ((1 << $abits) < $bytes) { $abits++; } my $amax = $abits-1; my $bmax = (1 << $abits)-1; my $afmt = sprintf("%d'h%%0%dx", $abits, ($abits+3) >> 2); my $abad = sprintf("%d'h", $abits) . ('x' x (($abits+3) >> 2)); my $bfmt = "8'h%02x"; my $bbad = "8'hxx"; print $out <<"EOF"; /* * Call it a ROM even through it can be optionally written to. * Trust the tools to figure out if we don't need part of the whole thing. */ module ${module_name}_rom ( input clk, input [$amax:0] usb_addr, output [7:0] usb_rdata, input cpu_clk, input [$amax:0] cpu_addr, output [7:0] cpu_rdata, input [7:0] cpu_wdata, input cpu_wren ); reg [7:0] rom [0:$bmax]; initial begin EOF my $addr = 0; foreach my $b (unpack("C*", $descriptor_data)) { printf $out "\t\trom[$afmt] = $bfmt;\n", $addr++, $b; } while ($addr < $bytes) { printf $out "\t\trom[$afmt] = $bbad;\n", $addr++; } print $out <<"EOF"; end always \@(posedge clk) begin usb_rdata <= rom[usb_addr]; end always \@(posedge cpu_clk) begin cpu_rdata <= rom[cpu_addr]; if (cpu_wren) rom[cpu_addr] <= cpu_wdata; end endmodule module ${module_name}_index ( input [7:0] dtype, input [7:0] dindex, input [15:0] windex, input additional, output reg [$amax:0] addr, output reg [$amax:0] len ); always \@(\*) if (additional) EOF printf $out "\t\t{addr,len} = {$afmt,$afmt};\n", $additional_offs, $additional_len; print $out <<"EOF"; else priority casez ({windex,dindex,dtype}) EOF my @cases; foreach my $d ({ 'len' => 0 }, @descriptor_ptrs) { my $id = 0; my $mask = 0; if (defined($d->{type})) { $id |= $d->{type}; $mask |= 0xff; } if (defined($d->{dindex})) { $id |= $d->{dindex} << 8; $mask |= 0xff << 8; } if (defined($d->{windex})) { $id |= $d->{windex} << 16; $mask |= $d->{wimask} << 16; } my $cs = "\t\t".v_case(32,$id,$mask).': {addr,len} = '; if ($d->{len}) { $cs .= sprintf("{$afmt,$afmt};\n", $d->{offs},$d->{len}); } else { $cs .= sprintf("{$abad,$afmt};\n", $d->{len}); } push(@cases, $cs); } # This relies on ? sorting after digits print $out sort @cases; print $out <<'EOF'; endcase endmodule EOF } if (scalar(@ARGV) < 3) { die "Usage: $0 {bin|v} input output\n"; } my @ofs = File::Spec->splitpath($outfile); $module_name = $ofs[2]; $module_name =~ s/\..*$//; $module_name =~ s/\P{Alnum}+/_/g; unless (defined(do File::Spec->rel2abs($infile))) { die "$0: $infile: ".(($@ ne '') ? $@ : $!)."\n"; } generate_data(); exit ($err) if ($err); open(my $out, '>', $outfile) or die "$0: $outfile: $!\n"; if ($mode eq 'bin') { print $out $descriptor_data; } elsif ($mode eq 'v') { output_verilog($out); } else { die "$0: $mode: unknown operations mode\n"; } close($out); if ($err) { remove($outfile); } exit($err);