btusb: complete Intel BT firmware download runtime (Phase 7.1)
- btintel.rs: run_intel_command() sends a command and waits for the matching command-complete (opcode + status check). intel_read_version() parses the Intel version response. intel_download_firmware() streams the full SFI command sequence (CSS header + PKey + signature + payload fragments), rejecting on controller NACK. intel_setup_firmware() orchestrates bootloader detection -> enter MFG -> download -> exit MFG -> DDC apply -> version re-read. apply_ddc_config() replays the DDC command records. read_firmware_blob() probes scheme and /lib/firmware (flat + intel/ layouts). - main.rs: wire intel_setup_firmware into daemon_main for Intel CNVi adapters (vendor 0x8087) before standard HCI init; no-op on operational controllers, non-fatal on failure. - 4 new tests (download ACK/NACK, opcode match/mismatch); 164 total pass. Ported from Linux drivers/bluetooth/btintel.c.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use log::{debug, info, warn};
|
||||
|
||||
use crate::hci::{HciCommand, HciPacketData};
|
||||
use crate::hci::{HciCommand, HciEvent};
|
||||
use crate::usb_transport::UsbHciTransport;
|
||||
|
||||
const OP_INTEL_MFG: u16 = 0xfc11;
|
||||
const OP_INTEL_READ_VERSION: u16 = 0xfc05;
|
||||
@@ -223,6 +224,152 @@ pub fn extract_boot_param(fw_data: &[u8]) -> Option<u32> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn read_firmware_blob(name: &str) -> Result<Vec<u8>, String> {
|
||||
let candidates = [
|
||||
format!("/scheme/firmware/intel/{name}"),
|
||||
format!("/scheme/firmware/{name}"),
|
||||
format!("/lib/firmware/intel/{name}"),
|
||||
format!("/lib/firmware/{name}"),
|
||||
];
|
||||
for path in &candidates {
|
||||
if let Ok(data) = std::fs::read(path) {
|
||||
if !data.is_empty() {
|
||||
return Ok(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(format!("firmware {name} not found in scheme/fs paths"))
|
||||
}
|
||||
|
||||
fn run_intel_command(
|
||||
transport: &mut dyn UsbHciTransport,
|
||||
cmd: &HciCommand,
|
||||
) -> Result<(u8, Vec<u8>), String> {
|
||||
transport
|
||||
.send_command(cmd)
|
||||
.map_err(|e| format!("send opcode {:#06x}: {e}", cmd.opcode))?;
|
||||
let event = transport
|
||||
.recv_event()
|
||||
.map_err(|e| format!("recv opcode {:#06x}: {e}", cmd.opcode))?
|
||||
.ok_or_else(|| format!("no response to opcode {:#06x}", cmd.opcode))?;
|
||||
let Some((_n, opcode, params)) = event.command_complete_params() else {
|
||||
return Err(format!(
|
||||
"expected command-complete for {:#06x}, got event {:#04x}",
|
||||
cmd.opcode, event.event_code
|
||||
));
|
||||
};
|
||||
if opcode != cmd.opcode {
|
||||
return Err(format!(
|
||||
"command-complete opcode mismatch: sent {:#06x}, got {:#06x}",
|
||||
cmd.opcode, opcode
|
||||
));
|
||||
}
|
||||
if params.is_empty() {
|
||||
return Err(format!("empty return params for {:#06x}", cmd.opcode));
|
||||
}
|
||||
let status = params[0];
|
||||
Ok((status, params[1..].to_vec()))
|
||||
}
|
||||
|
||||
pub fn intel_read_version(transport: &mut dyn UsbHciTransport) -> Result<IntelVersion, String> {
|
||||
let (_status, params) = run_intel_command(transport, &cmd_intel_read_version())?;
|
||||
IntelVersion::from_response(¶ms)
|
||||
.ok_or_else(|| format!("malformed Intel version response ({} bytes)", params.len()))
|
||||
}
|
||||
|
||||
pub fn intel_download_firmware(
|
||||
transport: &mut dyn UsbHciTransport,
|
||||
fw_data: &[u8],
|
||||
) -> Result<(), String> {
|
||||
let commands = firmware_download_commands(fw_data);
|
||||
let total = commands.len();
|
||||
for (i, cmd) in commands.iter().enumerate() {
|
||||
let (status, _params) = run_intel_command(transport, cmd)
|
||||
.map_err(|e| format!("fragment {i}/{total}: {e}"))?;
|
||||
if status != 0x00 {
|
||||
return Err(format!(
|
||||
"fragment {i}/{total} rejected by controller (status {:#04x})",
|
||||
status
|
||||
));
|
||||
}
|
||||
if i % 32 == 0 {
|
||||
debug!("btintel: firmware download progress {}/{}", i, total);
|
||||
}
|
||||
}
|
||||
info!("btintel: firmware download complete ({} fragments)", total);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn intel_setup_firmware(
|
||||
transport: &mut dyn UsbHciTransport,
|
||||
vendor_id: u16,
|
||||
device_id: u16,
|
||||
) -> Result<IntelVersion, String> {
|
||||
let version = intel_read_version(transport)?;
|
||||
info!(
|
||||
"btintel: version hw_variant={:#04x} hw_rev={:#04x} fw_variant={:#04x} fw_rev={:#04x} ({})",
|
||||
version.hw_variant,
|
||||
version.hw_revision,
|
||||
version.fw_variant,
|
||||
version.fw_revision,
|
||||
if version.is_bootloader() { "bootloader" } else { "operational" }
|
||||
);
|
||||
|
||||
if version.is_bootloader() {
|
||||
let sfi_name = firmware_filename_for_device(vendor_id, device_id)
|
||||
.ok_or_else(|| format!("no SFI firmware known for {vendor_id:04x}:{device_id:04x}"))?;
|
||||
let fw_data = read_firmware_blob(sfi_name)?;
|
||||
|
||||
let (status, _p) = run_intel_command(transport, &cmd_intel_enter_mfg())?;
|
||||
if status != 0x00 {
|
||||
return Err(format!("enter MFG rejected (status {status:#04x})"));
|
||||
}
|
||||
intel_download_firmware(transport, &fw_data)?;
|
||||
let (status, _p) = run_intel_command(transport, &cmd_intel_exit_mfg(true, true))?;
|
||||
if status != 0x00 {
|
||||
return Err(format!("exit MFG rejected (status {status:#04x})"));
|
||||
}
|
||||
info!("btintel: firmware patched, controller reset to operational mode");
|
||||
}
|
||||
|
||||
if let Some(ddc_name) = ddc_filename_for_device(vendor_id, device_id) {
|
||||
if let Ok(ddc_data) = read_firmware_blob(ddc_name) {
|
||||
apply_ddc_config(transport, &ddc_data)?;
|
||||
} else {
|
||||
warn!("btintel: DDC config {} not found, using firmware defaults", ddc_name);
|
||||
}
|
||||
}
|
||||
|
||||
intel_read_version(transport)
|
||||
}
|
||||
|
||||
fn apply_ddc_config(
|
||||
transport: &mut dyn UsbHciTransport,
|
||||
ddc_data: &[u8],
|
||||
) -> Result<(), String> {
|
||||
let mut offset = 0;
|
||||
let mut applied = 0usize;
|
||||
while offset + 3 < ddc_data.len() {
|
||||
let opcode = u16::from_le_bytes([ddc_data[offset], ddc_data[offset + 1]]);
|
||||
let plen = ddc_data[offset + 2] as usize;
|
||||
if offset + 3 + plen > ddc_data.len() {
|
||||
break;
|
||||
}
|
||||
let params = ddc_data[offset + 3..offset + 3 + plen].to_vec();
|
||||
let cmd = HciCommand::new(opcode, params);
|
||||
let (status, _p) = run_intel_command(transport, &cmd)
|
||||
.map_err(|e| format!("DDC opcode {opcode:#06x}: {e}"))?;
|
||||
if status != 0x00 {
|
||||
debug!("btintel: DDC opcode {opcode:#06x} status {status:#04x} (continuing)");
|
||||
} else {
|
||||
applied += 1;
|
||||
}
|
||||
offset += 3 + plen;
|
||||
}
|
||||
info!("btintel: DDC config applied ({} commands)", applied);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct IntelFirmwareLoader {
|
||||
pub version: Option<IntelVersion>,
|
||||
}
|
||||
@@ -380,4 +527,90 @@ mod tests {
|
||||
let fw = vec![0u8; 20];
|
||||
assert_eq!(extract_boot_param(&fw), None);
|
||||
}
|
||||
|
||||
struct TestTransport {
|
||||
events: std::collections::VecDeque<HciEvent>,
|
||||
sent: Vec<HciCommand>,
|
||||
}
|
||||
|
||||
impl TestTransport {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
events: std::collections::VecDeque::new(),
|
||||
sent: Vec::new(),
|
||||
}
|
||||
}
|
||||
fn push_cc(&mut self, opcode: u16, status: u8) {
|
||||
let params = vec![0x01, opcode as u8, (opcode >> 8) as u8, status, 0x00];
|
||||
self.events.push_back(HciEvent {
|
||||
event_code: crate::hci::EVT_COMMAND_COMPLETE,
|
||||
parameters: params,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::usb_transport::UsbHciTransport for TestTransport {
|
||||
fn send_command(&mut self, command: &HciCommand) -> std::io::Result<()> {
|
||||
self.sent.push(command.clone());
|
||||
Ok(())
|
||||
}
|
||||
fn recv_event(&mut self) -> std::io::Result<Option<HciEvent>> {
|
||||
Ok(self.events.pop_front())
|
||||
}
|
||||
fn send_acl(&mut self, _acl: &crate::hci::HciAcl) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn recv_acl(&mut self) -> std::io::Result<Option<crate::hci::HciAcl>> {
|
||||
Ok(None)
|
||||
}
|
||||
fn state(&self) -> crate::usb_transport::TransportState {
|
||||
crate::usb_transport::TransportState::Active
|
||||
}
|
||||
fn close(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_intel_command_matches_opcode() {
|
||||
let mut t = TestTransport::new();
|
||||
t.push_cc(OP_INTEL_READ_VERSION, 0x00);
|
||||
let (status, _params) = run_intel_command(&mut t, &cmd_intel_read_version()).unwrap();
|
||||
assert_eq!(status, 0x00);
|
||||
assert_eq!(t.sent.len(), 1);
|
||||
assert_eq!(t.sent[0].opcode, OP_INTEL_READ_VERSION);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_intel_command_opcode_mismatch_errors() {
|
||||
let mut t = TestTransport::new();
|
||||
t.push_cc(OP_INTEL_READ_BOOT_PARAMS, 0x00);
|
||||
let result = run_intel_command(&mut t, &cmd_intel_read_version());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn download_rejects_controller_nack() {
|
||||
let mut fw = vec![0x01];
|
||||
fw.extend_from_slice(&[0u8; 643]);
|
||||
fw.extend_from_slice(&[0xCC; 300]);
|
||||
let mut t = TestTransport::new();
|
||||
t.push_cc(OP_INTEL_DOWNLOAD_FRAGMENT, 0x01);
|
||||
let result = intel_download_firmware(&mut t, &fw);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn download_succeeds_when_all_fragments_ack() {
|
||||
let mut fw = vec![0x01];
|
||||
fw.extend_from_slice(&[0u8; 643]);
|
||||
fw.extend_from_slice(&[0xCC; 300]);
|
||||
let total = firmware_download_commands(&fw).len();
|
||||
let mut t = TestTransport::new();
|
||||
for _ in 0..total {
|
||||
t.push_cc(OP_INTEL_DOWNLOAD_FRAGMENT, 0x00);
|
||||
}
|
||||
intel_download_firmware(&mut t, &fw).unwrap();
|
||||
assert_eq!(t.sent.len(), total);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -794,6 +794,22 @@ fn daemon_main(config: &TransportConfig) -> Result<(), String> {
|
||||
|
||||
let mut t = StubTransport::new(transport_config);
|
||||
|
||||
// Intel CNVi (0x8087): bootloader detect + SFI download + DDC apply
|
||||
// before standard init. No-op on operational controllers; non-fatal.
|
||||
if adapter.vendor_id == 0x8087 {
|
||||
match btintel::intel_setup_firmware(&mut t, adapter.vendor_id, adapter.device_id) {
|
||||
Ok(version) => {
|
||||
log::info!(
|
||||
"btusb: Intel firmware ready (fw_variant={:#04x} fw_rev={:#04x})",
|
||||
version.fw_variant, version.fw_revision
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("btusb: Intel firmware setup failed ({}); continuing with standard init", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match hci_init_sequence(&mut t) {
|
||||
Ok(info) => {
|
||||
controller_info = info;
|
||||
|
||||
Reference in New Issue
Block a user