mirror of
https://github.com/RGBCube/serenity
synced 2025-05-20 13:45:06 +00:00

Instead of enumerating all available controllers and then ask each to find its audio channels, we change the initialization sequence to match what happens in the Networking subsystem and Graphics subsystem - we essentially probe for a matching driver on a PCI device, create a device instance, and immediately initialize it. This in fact allows us to immediately find any hardware initialization issues and report it, and then dropping the created instance, as usually being done in other initialization paths in the Kernel. This also opens the opportunity to propagate errors when failed to initialize an AudioChannel instance, and it will be addressed in a future commit.
341 lines
15 KiB
C++
341 lines
15 KiB
C++
/*
|
|
* Copyright (c) 2023, Jelle Raaijmakers <jelle@gmta.nl>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include "Controller.h"
|
|
#include <AK/Optional.h>
|
|
#include <AK/Vector.h>
|
|
#include <Kernel/Arch/Delay.h>
|
|
#include <Kernel/Bus/PCI/API.h>
|
|
#include <Kernel/Devices/Audio/IntelHDA/Codec.h>
|
|
#include <Kernel/Devices/Audio/IntelHDA/Stream.h>
|
|
#include <Kernel/Devices/Audio/IntelHDA/Timing.h>
|
|
#include <Kernel/Time/TimeManagement.h>
|
|
|
|
namespace Kernel::Audio::IntelHDA {
|
|
|
|
UNMAP_AFTER_INIT ErrorOr<bool> Controller::probe(PCI::DeviceIdentifier const& device_identifier)
|
|
{
|
|
VERIFY(device_identifier.class_code().value() == to_underlying(PCI::ClassID::Multimedia));
|
|
return device_identifier.subclass_code().value() == to_underlying(PCI::Multimedia::SubclassID::HDACompatibleController);
|
|
}
|
|
|
|
UNMAP_AFTER_INIT ErrorOr<NonnullRefPtr<AudioController>> Controller::create(PCI::DeviceIdentifier const& pci_device_identifier)
|
|
{
|
|
auto controller_io_window = TRY(IOWindow::create_for_pci_device_bar(pci_device_identifier, PCI::HeaderType0BaseRegister::BAR0));
|
|
return TRY(adopt_nonnull_ref_or_enomem(new (nothrow) Controller(pci_device_identifier, move(controller_io_window))));
|
|
}
|
|
|
|
UNMAP_AFTER_INIT Controller::Controller(PCI::DeviceIdentifier const& pci_device_identifier, NonnullOwnPtr<IOWindow> controller_io_window)
|
|
: PCI::Device(const_cast<PCI::DeviceIdentifier&>(pci_device_identifier))
|
|
, m_controller_io_window(move(controller_io_window))
|
|
{
|
|
}
|
|
|
|
UNMAP_AFTER_INIT ErrorOr<void> Controller::initialize(Badge<AudioManagement>)
|
|
{
|
|
// Enable DMA
|
|
PCI::enable_bus_mastering(device_identifier());
|
|
|
|
// 3.3.3, 3.3.4: Controller version
|
|
auto version_minor = m_controller_io_window->read8(ControllerRegister::VersionMinor);
|
|
auto version_major = m_controller_io_window->read8(ControllerRegister::VersionMajor);
|
|
dmesgln_pci(*this, "Intel High Definition Audio specification v{}.{}", version_major, version_minor);
|
|
if (version_major != 1 || version_minor != 0)
|
|
return ENOTSUP;
|
|
|
|
// 3.3.2: Read capabilities
|
|
u16 capabilities = m_controller_io_window->read16(ControllerRegister::GlobalCapabilities);
|
|
dbgln_if(INTEL_HDA_DEBUG, "Controller capabilities:");
|
|
m_number_of_output_streams = capabilities >> 12;
|
|
m_number_of_input_streams = (capabilities >> 8) & 0xf;
|
|
m_number_of_bidirectional_streams = (capabilities >> 3) & 0x1f;
|
|
bool is_64_bit_addressing_supported = (capabilities & 0x1) > 0;
|
|
dbgln_if(INTEL_HDA_DEBUG, "├ Number of output streams: {}", m_number_of_output_streams);
|
|
dbgln_if(INTEL_HDA_DEBUG, "├ Number of input streams: {}", m_number_of_input_streams);
|
|
dbgln_if(INTEL_HDA_DEBUG, "├ Number of bidirectional streams: {}", m_number_of_bidirectional_streams);
|
|
dbgln_if(INTEL_HDA_DEBUG, "└ 64-bit addressing supported: {}", is_64_bit_addressing_supported ? "yes" : "no");
|
|
if (m_number_of_output_streams == 0)
|
|
return ENOTSUP;
|
|
if (!is_64_bit_addressing_supported && sizeof(FlatPtr) == 8)
|
|
return ENOTSUP;
|
|
|
|
// Reset the controller
|
|
TRY(reset());
|
|
|
|
// Register CORB and RIRB
|
|
auto command_io_window = TRY(m_controller_io_window->create_from_io_window_with_offset(ControllerRegister::CommandOutboundRingBufferOffset));
|
|
m_command_buffer = TRY(CommandOutboundRingBuffer::create("IntelHDA CORB"sv, move(command_io_window)));
|
|
TRY(m_command_buffer->register_with_controller());
|
|
|
|
auto response_io_window = TRY(m_controller_io_window->create_from_io_window_with_offset(ControllerRegister::ResponseInboundRingBufferOffset));
|
|
m_response_buffer = TRY(ResponseInboundRingBuffer::create("IntelHDA RIRB"sv, move(response_io_window)));
|
|
TRY(m_response_buffer->register_with_controller());
|
|
|
|
dbgln_if(INTEL_HDA_DEBUG, "CORB ({} entries) and RIRB ({} entries) registered", m_command_buffer->capacity(), m_response_buffer->capacity());
|
|
|
|
// Initialize all codecs
|
|
// 3.3.9: State Change Status
|
|
u16 state_change_status = m_controller_io_window->read16(ControllerRegister::StateChangeStatus);
|
|
for (u8 codec_address = 0; codec_address < 14; ++codec_address) {
|
|
if ((state_change_status & (1 << codec_address)) > 0) {
|
|
dmesgln_pci(*this, "Found codec on address #{}", codec_address);
|
|
TRY(initialize_codec(codec_address));
|
|
}
|
|
}
|
|
|
|
auto result = configure_output_route();
|
|
if (result.is_error()) {
|
|
dmesgln_pci(*this, "Failed to set up an output audio channel: {}", result.error());
|
|
return result.release_error();
|
|
}
|
|
|
|
m_audio_channel = AudioChannel::must_create(*this, fixed_audio_channel_index);
|
|
return {};
|
|
}
|
|
|
|
UNMAP_AFTER_INIT ErrorOr<void> Controller::initialize_codec(u8 codec_address)
|
|
{
|
|
auto codec = TRY(Codec::create(*this, codec_address));
|
|
|
|
auto root_node = TRY(Node::create<RootNode>(codec));
|
|
if constexpr (INTEL_HDA_DEBUG)
|
|
root_node->debug_dump();
|
|
codec->set_root_node(root_node);
|
|
|
|
TRY(m_codecs.try_append(codec));
|
|
|
|
return {};
|
|
}
|
|
|
|
ErrorOr<u32> Controller::send_command(u8 codec_address, u8 node_id, CodecControlVerb verb, u16 payload)
|
|
{
|
|
// Construct command
|
|
// 7.3: If the most significant 4 bits of 12-bits verb are 0xf or 0x7, extended mode is selected
|
|
u32 command_value = codec_address << 28 | (node_id << 20);
|
|
if (((verb & 0x700) > 0) || ((verb & 0xf00) > 0))
|
|
command_value |= ((verb & 0xfff) << 8) | (payload & 0xff);
|
|
else
|
|
command_value |= ((verb & 0xf) << 16) | payload;
|
|
|
|
dbgln_if(INTEL_HDA_DEBUG, "Controller::{}: codec {} node {} verb {:#x} payload {:#b}",
|
|
__FUNCTION__, codec_address, node_id, to_underlying(verb), payload);
|
|
TRY(m_command_buffer->write_value(command_value));
|
|
|
|
// Read response
|
|
Optional<u64> full_response;
|
|
TRY(wait_until(frame_delay_in_microseconds(1), controller_timeout_in_microseconds, [&]() -> ErrorOr<bool> {
|
|
full_response = TRY(m_response_buffer->read_value());
|
|
return full_response.has_value();
|
|
}));
|
|
u32 response = full_response.value() & 0xffffffffu;
|
|
dbgln_if(INTEL_HDA_DEBUG, "Controller::{}: response {:#032b}", __FUNCTION__, response);
|
|
return response;
|
|
}
|
|
|
|
UNMAP_AFTER_INIT ErrorOr<void> Controller::configure_output_route()
|
|
{
|
|
Vector<NonnullRefPtr<WidgetNode>> queued_nodes;
|
|
Vector<WidgetNode*> visited_nodes;
|
|
HashMap<WidgetNode*, WidgetNode*> parents;
|
|
|
|
auto create_output_path = [&](RefPtr<WidgetNode> found_node) -> ErrorOr<NonnullOwnPtr<OutputPath>> {
|
|
// Reconstruct path by traversing parent nodes
|
|
Vector<NonnullRefPtr<WidgetNode>> path;
|
|
auto path_node = found_node;
|
|
while (path_node) {
|
|
TRY(path.try_append(*path_node));
|
|
path_node = parents.get(path_node).value_or(nullptr);
|
|
}
|
|
path.reverse();
|
|
|
|
// Create output stream
|
|
constexpr u8 output_stream_index = 0;
|
|
constexpr u8 output_stream_number = 1;
|
|
u64 output_stream_offset = ControllerRegister::StreamsOffset
|
|
+ m_number_of_input_streams * 0x20
|
|
+ output_stream_index * 0x20;
|
|
auto stream_io_window = TRY(m_controller_io_window->create_from_io_window_with_offset(output_stream_offset));
|
|
auto output_stream = TRY(OutputStream::create(move(stream_io_window), output_stream_number));
|
|
|
|
// Create output path
|
|
auto output_path = TRY(OutputPath::create(move(path), move(output_stream)));
|
|
TRY(output_path->activate());
|
|
return output_path;
|
|
};
|
|
|
|
for (auto codec : m_codecs) {
|
|
// Start off by finding all candidate pin complexes
|
|
auto pin_widgets = TRY(codec->nodes_matching<WidgetNode>([](NonnullRefPtr<WidgetNode> node) {
|
|
// Find pin complexes that support output.
|
|
if (node->widget_type() != WidgetNode::WidgetType::PinComplex
|
|
|| !node->pin_complex_output_supported())
|
|
return false;
|
|
|
|
// Only consider pin complexes that have:
|
|
// - a physical connection (jack or fixed function)
|
|
// - and a default device that is line out, speakers or headphones.
|
|
auto configuration_default = node->pin_configuration_default();
|
|
auto port_connectivity = configuration_default.port_connectivity;
|
|
auto default_device = configuration_default.default_device;
|
|
|
|
bool is_physically_connected = port_connectivity == WidgetNode::PinPortConnectivity::Jack
|
|
|| port_connectivity == WidgetNode::PinPortConnectivity::FixedFunction
|
|
|| port_connectivity == WidgetNode::PinPortConnectivity::JackAndFixedFunction;
|
|
bool is_output_device = default_device == WidgetNode::PinDefaultDevice::LineOut
|
|
|| default_device == WidgetNode::PinDefaultDevice::Speaker
|
|
|| default_device == WidgetNode::PinDefaultDevice::HPOut;
|
|
|
|
return is_physically_connected && is_output_device;
|
|
}));
|
|
|
|
// Perform a breadth-first search to find a path to an audio output widget
|
|
for (auto pin_widget : pin_widgets) {
|
|
VERIFY(queued_nodes.is_empty() && visited_nodes.is_empty() && parents.is_empty());
|
|
|
|
TRY(queued_nodes.try_append(pin_widget));
|
|
Optional<NonnullRefPtr<WidgetNode>> found_node = {};
|
|
while (!queued_nodes.is_empty()) {
|
|
auto current_node = queued_nodes.take_first();
|
|
if (current_node->widget_type() == WidgetNode::AudioOutput) {
|
|
found_node = current_node;
|
|
break;
|
|
}
|
|
|
|
TRY(visited_nodes.try_append(current_node.ptr()));
|
|
for (u8 connection_node_id : current_node->connection_list()) {
|
|
auto connection_node = codec->node_by_node_id(connection_node_id);
|
|
if (!connection_node.has_value() || connection_node.value()->node_type() != Node::NodeType::Widget) {
|
|
dmesgln_pci(*this, "Warning: connection node {} does not exist or is the wrong type", connection_node_id);
|
|
continue;
|
|
}
|
|
|
|
auto connection_widget = NonnullRefPtr<WidgetNode> { *reinterpret_cast<WidgetNode*>(connection_node.release_value()) };
|
|
if (visited_nodes.contains_slow(connection_widget))
|
|
continue;
|
|
|
|
TRY(queued_nodes.try_append(connection_widget));
|
|
TRY(parents.try_set(connection_widget, current_node.ptr()));
|
|
}
|
|
}
|
|
|
|
if (found_node.has_value()) {
|
|
m_output_path = TRY(create_output_path(found_node.release_value()));
|
|
break;
|
|
}
|
|
|
|
queued_nodes.clear_with_capacity();
|
|
visited_nodes.clear_with_capacity();
|
|
parents.clear_with_capacity();
|
|
}
|
|
|
|
if (m_output_path)
|
|
break;
|
|
}
|
|
|
|
if (!m_output_path) {
|
|
dmesgln_pci(*this, "Failed to find an audio output path");
|
|
return ENODEV;
|
|
}
|
|
|
|
// We are ready to go!
|
|
dmesgln_pci(*this, "Successfully configured an audio output path");
|
|
dbgln_if(INTEL_HDA_DEBUG, "{}", TRY(m_output_path->to_string()));
|
|
|
|
return {};
|
|
}
|
|
|
|
ErrorOr<void> Controller::reset()
|
|
{
|
|
// 3.3.7: "Controller Reset (CRST): Writing a 0 to this bit causes the High Definition Audio
|
|
// controller to transition to the Reset state."
|
|
u32 global_control = m_controller_io_window->read32(ControllerRegister::GlobalControl);
|
|
global_control &= ~GlobalControlFlag::ControllerReset;
|
|
global_control &= ~GlobalControlFlag::AcceptUnsolicitedResponseEnable;
|
|
m_controller_io_window->write32(ControllerRegister::GlobalControl, global_control);
|
|
|
|
// 3.3.7: "After the hardware has completed sequencing into the reset state, it will report
|
|
// a 0 in this bit. Software must read a 0 from this bit to verify that the
|
|
// controller is in reset."
|
|
TRY(wait_until(frame_delay_in_microseconds(1), controller_timeout_in_microseconds, [&]() {
|
|
global_control = m_controller_io_window->read32(ControllerRegister::GlobalControl);
|
|
return (global_control & GlobalControlFlag::ControllerReset) == 0;
|
|
}));
|
|
|
|
// 3.3.7: "Writing a 1 to this bit causes the controller to exit its Reset state and
|
|
// de-assert the link RESET# signal. Software is responsible for
|
|
// setting/clearing this bit such that the minimum link RESET# signal assertion
|
|
// pulse width specification is met (see Section 5.5)."
|
|
microseconds_delay(100);
|
|
global_control |= GlobalControlFlag::ControllerReset;
|
|
m_controller_io_window->write32(ControllerRegister::GlobalControl, global_control);
|
|
|
|
// 3.3.7: "When the controller hardware is ready to begin operation, it will report a 1 in
|
|
// this bit. Software must read a 1 from this bit before accessing any controller
|
|
// registers."
|
|
TRY(wait_until(frame_delay_in_microseconds(1), controller_timeout_in_microseconds, [&]() {
|
|
global_control = m_controller_io_window->read32(ControllerRegister::GlobalControl);
|
|
return (global_control & GlobalControlFlag::ControllerReset) > 0;
|
|
}));
|
|
|
|
// 4.3 Codec Discovery:
|
|
// "The software must wait at least 521 us (25 frames) after reading CRST as a 1 before
|
|
// assuming that codecs have all made status change requests and have been registered
|
|
// by the controller."
|
|
microseconds_delay(frame_delay_in_microseconds(25));
|
|
|
|
dbgln_if(INTEL_HDA_DEBUG, "Controller reset");
|
|
return {};
|
|
}
|
|
|
|
LockRefPtr<AudioChannel> Controller::audio_channel(u32 index) const
|
|
{
|
|
if (index != fixed_audio_channel_index)
|
|
return {};
|
|
return m_audio_channel;
|
|
}
|
|
|
|
ErrorOr<size_t> Controller::write(size_t channel_index, UserOrKernelBuffer const& data, size_t length)
|
|
{
|
|
if (channel_index != fixed_audio_channel_index || !m_output_path)
|
|
return ENODEV;
|
|
return m_output_path->output_stream().write(data, length);
|
|
}
|
|
|
|
ErrorOr<void> Controller::set_pcm_output_sample_rate(size_t channel_index, u32 samples_per_second_rate)
|
|
{
|
|
if (channel_index != fixed_audio_channel_index || !m_output_path)
|
|
return ENODEV;
|
|
|
|
TRY(m_output_path->set_format({
|
|
.sample_rate = samples_per_second_rate,
|
|
.pcm_bits = OutputPath::fixed_pcm_bits,
|
|
.number_of_channels = OutputPath::fixed_channel_count,
|
|
}));
|
|
dmesgln_pci(*this, "Set output channel #{} PCM rate: {} Hz", channel_index, samples_per_second_rate);
|
|
return {};
|
|
}
|
|
|
|
ErrorOr<u32> Controller::get_pcm_output_sample_rate(size_t channel_index)
|
|
{
|
|
if (channel_index != fixed_audio_channel_index || !m_output_path)
|
|
return ENODEV;
|
|
|
|
return m_output_path->output_stream().sample_rate();
|
|
}
|
|
|
|
ErrorOr<void> wait_until(size_t delay_in_microseconds, size_t timeout_in_microseconds, Function<ErrorOr<bool>()> condition)
|
|
{
|
|
auto const& time_management = TimeManagement::the();
|
|
u64 start_microseconds = time_management.now().to_microseconds();
|
|
while (!TRY(condition())) {
|
|
microseconds_delay(delay_in_microseconds);
|
|
if ((time_management.now().to_microseconds() - start_microseconds) >= timeout_in_microseconds)
|
|
return ETIMEDOUT;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
}
|