From 7a2a0c10521077f746b855c83297e401af3ded83 Mon Sep 17 00:00:00 2001 From: Jelle Raaijmakers Date: Tue, 23 Nov 2021 01:13:02 +0100 Subject: [PATCH] Kernel: Implement AC97 audio device driver --- Kernel/Bus/PCI/Definitions.h | 9 ++ Kernel/CMakeLists.txt | 1 + Kernel/Devices/Audio/AC97.cpp | 258 ++++++++++++++++++++++++++++++++++ Kernel/Devices/Audio/AC97.h | 133 ++++++++++++++++++ Kernel/init.cpp | 2 + 5 files changed, 403 insertions(+) create mode 100644 Kernel/Devices/Audio/AC97.cpp create mode 100644 Kernel/Devices/Audio/AC97.h diff --git a/Kernel/Bus/PCI/Definitions.h b/Kernel/Bus/PCI/Definitions.h index f563064257..97c8555e8f 100644 --- a/Kernel/Bus/PCI/Definitions.h +++ b/Kernel/Bus/PCI/Definitions.h @@ -64,6 +64,7 @@ static constexpr size_t memory_range_per_bus = mmio_device_space_size * to_under // Taken from https://pcisig.com/sites/default/files/files/PCI_Code-ID_r_1_11__v24_Jan_2019.pdf enum class ClassID { MassStorage = 0x1, + Multimedia = 0x4, Bridge = 0x6, }; @@ -79,6 +80,14 @@ enum class SATAProgIF { } +namespace Multimedia { + +enum class SubclassID { + AudioController = 0x1, +}; + +} + namespace Bridge { enum class SubclassID { diff --git a/Kernel/CMakeLists.txt b/Kernel/CMakeLists.txt index e4525bc661..33d65f4b43 100644 --- a/Kernel/CMakeLists.txt +++ b/Kernel/CMakeLists.txt @@ -43,6 +43,7 @@ set(KERNEL_SOURCES CommandLine.cpp Coredump.cpp Devices/AsyncDeviceRequest.cpp + Devices/Audio/AC97.cpp Devices/Audio/SB16.cpp Devices/BlockDevice.cpp Devices/CharacterDevice.cpp diff --git a/Kernel/Devices/Audio/AC97.cpp b/Kernel/Devices/Audio/AC97.cpp new file mode 100644 index 0000000000..f6be6329dd --- /dev/null +++ b/Kernel/Devices/Audio/AC97.cpp @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2021, Jelle Raaijmakers + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +namespace Kernel { + +static constexpr int buffer_descriptor_list_max_entries = 32; + +static ErrorOr> allocate_physical_buffer(size_t size, StringView name) +{ + auto vmobject = TRY(Memory::AnonymousVMObject::try_create_physically_contiguous_with_size(Memory::page_round_up(size))); + return TRY(MM.allocate_kernel_region_with_vmobject(move(vmobject), vmobject->size(), name, Memory::Region::Access::Write)); +} + +UNMAP_AFTER_INIT void AC97::detect() +{ + PCI::enumerate([&](PCI::DeviceIdentifier const& device_identifier) { + // Only consider PCI audio controllers + if (device_identifier.class_code().value() != to_underlying(PCI::ClassID::Multimedia) + || device_identifier.subclass_code().value() != to_underlying(PCI::Multimedia::SubclassID::AudioController)) + return; + + dbgln("AC97: found audio controller at {}", device_identifier.address()); + auto device_or_error = DeviceManagement::try_create_device(device_identifier); + if (device_or_error.is_error()) { + dbgln("AC97: failed to initialize device {}", device_identifier.address()); + return; + } + DeviceManagement::the().attach_audio_device(device_or_error.release_value()); + }); +} + +UNMAP_AFTER_INIT AC97::AC97(PCI::DeviceIdentifier pci_device_identifier) + : PCI::Device(pci_device_identifier.address()) + , IRQHandler(pci_device_identifier.interrupt_line().value()) + , CharacterDevice(42, 42) + , m_io_mixer_base(PCI::get_BAR0(pci_address()) & ~1) + , m_io_bus_base(PCI::get_BAR1(pci_address()) & ~1) +{ + initialize(); +} + +UNMAP_AFTER_INIT AC97::~AC97() +{ +} + +bool AC97::handle_irq(RegisterState const&) +{ + auto pcm_out_status_register = m_io_bus_base.offset(NativeAudioBusRegister::PCMOutStatus); + auto pcm_out_status = pcm_out_status_register.in(); + + bool is_dma_halted = (pcm_out_status & AudioStatusRegisterFlag::DMAControllerHalted) > 0; + bool is_completion_interrupt = (pcm_out_status & AudioStatusRegisterFlag::BufferCompletionInterruptStatus) > 0; + bool is_fifo_error = (pcm_out_status & AudioStatusRegisterFlag::FIFOError) > 0; + + VERIFY(is_completion_interrupt); + VERIFY(!is_fifo_error); + + // On interrupt, we need to reset PCM interrupt flags by setting their bits + pcm_out_status = AudioStatusRegisterFlag::LastValidBufferCompletionInterrupt + | AudioStatusRegisterFlag::BufferCompletionInterruptStatus + | AudioStatusRegisterFlag::FIFOError; + pcm_out_status_register.out(pcm_out_status); + + // Stop the DMA engine if we're through with the buffer and no one is waiting + if (is_dma_halted && m_irq_queue.is_empty()) { + reset_pcm_out(); + } else { + m_irq_queue.wake_all(); + } + + return true; +} + +UNMAP_AFTER_INIT void AC97::initialize() +{ + dbgln("AC97 @ {}: mixer base: {:#04x}", pci_address(), m_io_mixer_base.get()); + dbgln("AC97 @ {}: bus base: {:#04x}", pci_address(), m_io_bus_base.get()); + + enable_pin_based_interrupts(); + PCI::enable_bus_mastering(pci_address()); + + // Bus cold reset, enable interrupts + auto control = m_io_bus_base.offset(NativeAudioBusRegister::GlobalControl).in(); + control |= GlobalControlFlag::GPIInterruptEnable; + control |= GlobalControlFlag::AC97ColdReset; + m_io_bus_base.offset(NativeAudioBusRegister::GlobalControl).out(control); + + // Reset mixer + m_io_mixer_base.offset(NativeAudioMixerRegister::Reset).out(1); + + // Verify extended capabilities + auto extended_audio_id = m_io_mixer_base.offset(NativeAudioMixerRegister::ExtendedAudioID).in(); + VERIFY((extended_audio_id & ExtendedAudioMask::VariableRatePCMAudio) == 1); + VERIFY((extended_audio_id & ExtendedAudioMask::Revision) >> 10 == AC97Revision::Revision23); + + // Left and right volume of 0 means attenuation of 0 dB + set_master_output_volume(0, 0, Muted::No); + set_pcm_output_volume(0, 0, Muted::No); + + set_pcm_output_sample_rate(m_sample_rate); + + reset_pcm_out(); + enable_irq(); +} + +ErrorOr AC97::ioctl(OpenFileDescription&, unsigned request, Userspace arg) +{ + switch (request) { + case SOUNDCARD_IOCTL_GET_SAMPLE_RATE: { + auto output = static_ptr_cast(arg); + return copy_to_user(output, &m_sample_rate); + } + case SOUNDCARD_IOCTL_SET_SAMPLE_RATE: { + auto sample_rate_value = static_cast(arg.ptr()); + if (sample_rate_value == 0) + return EINVAL; + if (m_sample_rate != sample_rate_value) + set_pcm_output_sample_rate(sample_rate_value); + return {}; + } + default: + return EINVAL; + } +} + +ErrorOr AC97::read(OpenFileDescription&, u64, UserOrKernelBuffer&, size_t) +{ + return 0; +} + +void AC97::reset_pcm_out() +{ + auto pcm_out_control_register = m_io_bus_base.offset(NativeAudioBusRegister::PCMOutControl); + pcm_out_control_register.out(AudioControlRegisterFlag::ResetRegisters); + + while ((pcm_out_control_register.in() & AudioControlRegisterFlag::ResetRegisters) > 0) + IO::delay(50); + + m_output_buffer_dma_running = false; + m_buffer_descriptor_list_index = 0; + + dbgln("AC97 @ {}: PCM out is reset", pci_address()); +} + +void AC97::set_master_output_volume(u8 left_channel, u8 right_channel, Muted mute) +{ + u16 volume_value = ((right_channel & 63) << 0) + | ((left_channel & 63) << 8) + | ((mute == Muted::Yes ? 1 : 0) << 15); + m_io_mixer_base.offset(NativeAudioMixerRegister::SetMasterOutputVolume).out(volume_value); +} + +void AC97::set_output_buffer_dma_lvi(u8 last_valid_index) +{ + auto buffer_address = static_cast(m_buffer_descriptor_list->physical_page(0)->paddr().get()); + m_io_bus_base.offset(NativeAudioBusRegister::PCMOutBufferDescriptorListBaseAddress).out(buffer_address); + m_io_bus_base.offset(NativeAudioBusRegister::PCMOutLastValidIndex).out(last_valid_index); +} + +void AC97::set_pcm_output_sample_rate(u16 sample_rate) +{ + auto pcm_front_dac_rate_register = m_io_mixer_base.offset(NativeAudioMixerRegister::PCMFrontDACRate); + pcm_front_dac_rate_register.out(sample_rate); + m_sample_rate = pcm_front_dac_rate_register.in(); + dbgln("AC97 @ {}: PCM front DAC rate set to {} Hz", pci_address(), m_sample_rate); +} + +void AC97::set_pcm_output_volume(u8 left_channel, u8 right_channel, Muted mute) +{ + u16 volume_value = ((right_channel & 31) << 0) + | ((left_channel & 31) << 8) + | ((mute == Muted::Yes ? 1 : 0) << 15); + m_io_mixer_base.offset(NativeAudioMixerRegister::SetPCMOutputVolume).out(volume_value); +} + +void AC97::start_output_buffer_dma() +{ + dbgln("AC97 @ {}: starting DMA engine", pci_address()); + + auto pcm_out_control_register = m_io_bus_base.offset(NativeAudioBusRegister::PCMOutControl); + auto pcm_out_control = pcm_out_control_register.in(); + pcm_out_control |= AudioControlRegisterFlag::RunPauseBusMaster; + pcm_out_control |= AudioControlRegisterFlag::FIFOErrorInterruptEnable; + pcm_out_control |= AudioControlRegisterFlag::InterruptOnCompletionEnable; + pcm_out_control_register.out(pcm_out_control); + + m_output_buffer_dma_running = true; +} + +ErrorOr AC97::write(OpenFileDescription&, u64, UserOrKernelBuffer const& data, size_t length) +{ + VERIFY(length <= PAGE_SIZE); + + if (!m_output_buffer) { + m_output_buffer = TRY(allocate_physical_buffer(m_output_buffer_page_count * PAGE_SIZE, "AC97 Output buffer"sv)); + } + if (!m_buffer_descriptor_list) { + constexpr size_t buffer_descriptor_list_size = buffer_descriptor_list_max_entries * sizeof(BufferDescriptorListEntry); + m_buffer_descriptor_list = TRY(allocate_physical_buffer(buffer_descriptor_list_size, "AC97 Buffer Descriptor List"sv)); + } + + cli(); + + // Block until we can write into an unused buffer + do { + auto pcm_out_status = m_io_bus_base.offset(NativeAudioBusRegister::PCMOutStatus).in(); + auto is_dma_controller_halted = (pcm_out_status & AudioStatusRegisterFlag::DMAControllerHalted) > 0; + auto current_index = m_io_bus_base.offset(NativeAudioBusRegister::PCMOutCurrentIndex).in(); + auto last_valid_index = m_io_bus_base.offset(NativeAudioBusRegister::PCMOutLastValidIndex).in(); + + auto head_distance = static_cast(last_valid_index) - current_index; + if (head_distance < 0) + head_distance += buffer_descriptor_list_max_entries; + if (!is_dma_controller_halted) + ++head_distance; + + if (head_distance < m_output_buffer_page_count) + break; + + m_irq_queue.wait_forever("AC97"sv); + } while (m_output_buffer_dma_running); + + sti(); + + // Copy data from userspace into one of our buffers + TRY(data.read(m_output_buffer->vaddr_from_page_index(m_output_buffer_page_index).as_ptr(), length)); + + if (!m_output_buffer_dma_running) { + reset_pcm_out(); + } + + // Write the next entry to the buffer descriptor list + u16 number_of_samples = length / sizeof(u16); + auto list_entries = reinterpret_cast(m_buffer_descriptor_list->vaddr().get()); + auto list_entry = &list_entries[m_buffer_descriptor_list_index]; + list_entry->buffer_pointer = static_cast(m_output_buffer->physical_page(m_output_buffer_page_index)->paddr().get()); + list_entry->control_and_length = number_of_samples | BufferDescriptorListEntryFlags::InterruptOnCompletion; + set_output_buffer_dma_lvi(m_buffer_descriptor_list_index); + + if (!m_output_buffer_dma_running) { + start_output_buffer_dma(); + } + + m_output_buffer_page_index = (m_output_buffer_page_index + 1) % m_output_buffer_page_count; + m_buffer_descriptor_list_index = (m_buffer_descriptor_list_index + 1) % buffer_descriptor_list_max_entries; + + return length; +} + +} diff --git a/Kernel/Devices/Audio/AC97.h b/Kernel/Devices/Audio/AC97.h new file mode 100644 index 0000000000..3df59d08f6 --- /dev/null +++ b/Kernel/Devices/Audio/AC97.h @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2021, Jelle Raaijmakers + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace Kernel { + +// See: https://www-inst.eecs.berkeley.edu/~cs150/Documents/ac97_r23.pdf +// And: https://www.intel.com/content/dam/doc/manual/io-controller-hub-7-hd-audio-ac97-manual.pdf + +class AC97 final : public PCI::Device + , public IRQHandler + , public CharacterDevice { + friend class DeviceManagement; + +public: + static void detect(); + + virtual ~AC97() override; + + // ^IRQHandler + virtual StringView purpose() const override { return class_name(); } + + // ^CharacterDevice + virtual bool can_read(const OpenFileDescription&, size_t) const override { return false; } + virtual bool can_write(const OpenFileDescription&, size_t) const override { return true; } + virtual ErrorOr ioctl(OpenFileDescription&, unsigned, Userspace) override; + virtual ErrorOr read(OpenFileDescription&, u64, UserOrKernelBuffer&, size_t) override; + virtual ErrorOr write(OpenFileDescription&, u64, const UserOrKernelBuffer&, size_t) override; + +private: + enum NativeAudioMixerRegister : u8 { + Reset = 0x00, + SetMasterOutputVolume = 0x02, + SetPCMOutputVolume = 0x18, + ExtendedAudioID = 0x28, + PCMFrontDACRate = 0x2c, + }; + + enum ExtendedAudioMask : u16 { + VariableRatePCMAudio = 1 << 0, + Revision = 3 << 10, + }; + + enum AC97Revision : u8 { + Revision21OrEarlier = 0b00, + Revision22 = 0b01, + Revision23 = 0b10, + }; + + enum NativeAudioBusRegister : u8 { + PCMOutBufferDescriptorListBaseAddress = 0x10, + PCMOutCurrentIndex = 0x14, + PCMOutLastValidIndex = 0x15, + PCMOutStatus = 0x16, + PCMOutPrefetchedIndex = 0x1a, + PCMOutControl = 0x1b, + GlobalControl = 0x2c, + }; + + enum AudioStatusRegisterFlag : u16 { + DMAControllerHalted = 1 << 0, + CurrentEqualsLastValid = 1 << 1, + LastValidBufferCompletionInterrupt = 1 << 2, + BufferCompletionInterruptStatus = 1 << 3, + FIFOError = 1 << 4, + }; + + enum AudioControlRegisterFlag : u8 { + RunPauseBusMaster = 1 << 0, + ResetRegisters = 1 << 1, + FIFOErrorInterruptEnable = 1 << 3, + InterruptOnCompletionEnable = 1 << 4, + }; + + enum GlobalControlFlag : u32 { + GPIInterruptEnable = 1 << 0, + AC97ColdReset = 1 << 1, + }; + + enum Muted { + Yes, + No, + }; + + struct BufferDescriptorListEntry { + u32 buffer_pointer; + u32 control_and_length; + }; + + enum BufferDescriptorListEntryFlags : u32 { + BufferUnderrunPolicy = 1 << 30, + InterruptOnCompletion = 1u << 31, + }; + + AC97(PCI::DeviceIdentifier); + + // ^IRQHandler + virtual bool handle_irq(const RegisterState&) override; + + // ^CharacterDevice + virtual StringView class_name() const override { return "AC97"sv; } + + void initialize(); + void reset_pcm_out(); + void set_master_output_volume(u8, u8, Muted); + void set_pcm_output_sample_rate(u16); + void set_pcm_output_volume(u8, u8, Muted); + void set_output_buffer_dma_lvi(u8); + void start_output_buffer_dma(); + + OwnPtr m_buffer_descriptor_list; + u8 m_buffer_descriptor_list_index = 0; + IOAddress m_io_mixer_base; + IOAddress m_io_bus_base; + WaitQueue m_irq_queue; + OwnPtr m_output_buffer; + bool m_output_buffer_dma_running = false; + u8 m_output_buffer_page_count = 4; + u8 m_output_buffer_page_index = 0; + u16 m_sample_rate = 44100; +}; + +} diff --git a/Kernel/init.cpp b/Kernel/init.cpp index 32a504d2e9..7abc3a2f8f 100644 --- a/Kernel/init.cpp +++ b/Kernel/init.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -319,6 +320,7 @@ void init_stage2(void*) PTYMultiplexer::initialize(); SB16::try_detect_and_create(); + AC97::detect(); StorageManagement::the().initialize(kernel_command_line().root_device(), kernel_command_line().is_force_pio()); if (VirtualFileSystem::the().mount_root(StorageManagement::the().root_filesystem()).is_error()) {