Skip to content

Instantly share code, notes, and snippets.

@jadonk
Last active July 19, 2019 18:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jadonk/d435886df74a061052d82c1d7e22313b to your computer and use it in GitHub Desktop.
Save jadonk/d435886df74a061052d82c1d7e22313b to your computer and use it in GitHub Desktop.
pwmsp driver

http://git.infradead.org/users/ezequielg/linux/commitdiff/a71564c13877aac573232d9d203ea7cdaacb072b https://patchwork.kernel.org/patch/6217971/

sudo apt-get install linux-headers-`uname -r`
make -C /lib/modules/`uname -r`/build M=$PWD modules V=1 ARCH=arm
sudo make -C /lib/modules/`uname -r`/build M=$PWD modules_install V=1 ARCH=arm
sudo /opt/scripts/tools/developers/update_initrd.sh

reboot

config-pin p1.33 pwm
sudo modprobe snd_pwmsp
aplay /usr/share/sounds/alsa/Front_Center.wav

Test PWM

config-pin p1.33 pwm
echo 1 > /sys/class/pwm/pwmchip0/export
sleep 1
echo 1000000 > /sys/class/pwm/pwm-0\:1/period
echo 500000 > /sys/class/pwm/pwm-0\:1/duty_cycle
echo 1 > /sys/class/pwm/pwm-0\:1/enable
sleep 3
echo 0 > /sys/class/pwm/pwm-0\:1/enable
echo 1 > /sys/class/pwm/pwmchip0/unexport

Examine PWM driver status

sudo cat /sys/kernel/debug/pwm
config SND_PWMSP
tristate "PWM Speaker driver (PCM)"
depends on HIGH_RES_TIMERS
select SND_PCM
select PWM
help
snd-pwmsp-objs := pwmsp.o pwmsp_lib.o
obj-m += snd-pwmsp.o
/*
* Based on PC-Speaker driver for Linux
*
* Copyright (C) 1997-2001 David Woodhouse
* Copyright (C) 2001-2008 Stas Sergeev
*
* sndpwm {
* compatible = "snd-pwm";
* pwms = <&ehrpwm2 1 0 0>;
* status = "okay";
* };
*
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <sound/core.h>
#include <sound/initval.h>
#include <sound/pcm.h>
#include <linux/delay.h>
#include <asm/bitops.h>
#include <linux/pwm.h>
#include "pwmsp.h"
static int index = SNDRV_DEFAULT_IDX1; /* Index 0-MAX */
static char *id = SNDRV_DEFAULT_STR1; /* ID for this card */
static bool enable = SNDRV_DEFAULT_ENABLE1; /* Enable this card */
module_param(index, int, 0444);
MODULE_PARM_DESC(index, "Index value for pwmsp soundcard.");
module_param(id, charp, 0444);
MODULE_PARM_DESC(id, "ID string for pwmsp soundcard.");
module_param(enable, bool, 0444);
MODULE_PARM_DESC(enable, "Enable PC-Speaker sound.");
struct snd_pwmsp pwmsp_chip;
static int snd_pwmsp_create(struct snd_card *card)
{
static struct snd_device_ops ops = { };
int err;
pr_info("PCSP: Timer resolution is (%linS)\n", hrtimer_resolution);
spin_lock_init(&pwmsp_chip.substream_lock);
atomic_set(&pwmsp_chip.timer_active, 0);
pwmsp_chip.playback_ptr = 0;
pwmsp_chip.period_ptr = 0;
pwmsp_chip.enable = 1;
pwmsp_chip.card = card;
/* Register device */
err = snd_device_new(card, SNDRV_DEV_LOWLEVEL, &pwmsp_chip, &ops);
if (err < 0)
return err;
return 0;
}
static int snd_card_pwmsp_probe(int devnum, struct device *dev)
{
struct snd_card *card;
int err;
if (devnum != 0)
return -EINVAL;
hrtimer_init(&pwmsp_chip.timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
pwmsp_chip.timer.function = pwmsp_do_timer;
err = snd_card_new(dev, index, id, THIS_MODULE, 0, &card);
if (err < 0) {
printk(KERN_ERR "PWM PC-Speaker snd_card_new() call failed.\n");
return err;
}
err = snd_pwmsp_create(card);
if (err < 0) {
printk(KERN_ERR "PWM PC-Speaker snd_pwmsp_create() call failed.\n");
snd_card_free(card);
return err;
}
err = snd_pwmsp_new_pcm(&pwmsp_chip);
if (err < 0) {
printk(KERN_ERR "PWM PC-Speaker snd_pwmsp_new_pcm() call failed.\n");
snd_card_free(card);
return err;
}
strcpy(card->driver, "PC-Speaker");
strcpy(card->shortname, "pwmsp");
sprintf(card->longname, "PWM PC-Speaker");
err = snd_card_register(card);
if (err < 0) {
printk(KERN_ERR "PWM PC-Speaker snd_card_register() call failed.\n");
snd_card_free(card);
return err;
}
return 0;
}
static int alsa_card_pwmsp_init(struct device *dev)
{
int err;
err = snd_card_pwmsp_probe(0, dev);
if (err) {
printk(KERN_ERR "PC-Speaker initialization failed.\n");
return err;
}
#ifdef CONFIG_DEBUG_PAGEALLOC
/* Well, CONFIG_DEBUG_PAGEALLOC makes the sound horrible. Lets alert */
printk(KERN_WARNING "PCSP: CONFIG_DEBUG_PAGEALLOC is enabled, "
"which may make the sound noisy.\n");
#endif
return 0;
}
static void alsa_card_pwmsp_exit(struct snd_pwmsp *chip)
{
snd_card_free(chip->card);
}
static int pwmsp_probe(struct platform_device *dev)
{
int err;
pwmsp_chip.pwm = devm_pwm_get(&dev->dev, NULL);
if (IS_ERR(pwmsp_chip.pwm)) {
printk(KERN_ERR "PWM PC-Speaker devm_pwm_get() call failed.\n");
dev_err(&dev->dev, "unable to request PWM\n");
err = PTR_ERR(pwmsp_chip.pwm);
return err;
}
err = alsa_card_pwmsp_init(&dev->dev);
if (err < 0) {
printk(KERN_ERR "PWM PC-Speaker alsa_card_pwmsp_init() call failed.\n");
return err;
}
platform_set_drvdata(dev, &pwmsp_chip);
return 0;
}
static int pwmsp_remove(struct platform_device *dev)
{
struct snd_pwmsp *chip = platform_get_drvdata(dev);
alsa_card_pwmsp_exit(chip);
return 0;
}
static struct of_device_id pwmsp_of_match[] = {
{ .compatible = "snd-pwmsp" },
{ },
};
static struct platform_driver pwmsp_platform_driver = {
.driver = {
.name = "pwmspkr",
.owner = THIS_MODULE,
.of_match_table = pwmsp_of_match,
},
.probe = pwmsp_probe,
.remove = pwmsp_remove,
};
module_platform_driver(pwmsp_platform_driver);
MODULE_LICENSE("GPL");
/*
* PC-Speaker driver for Linux
*
* Copyright (C) 1993-1997 Michael Beck
* Copyright (C) 1997-2001 David Woodhouse
* Copyright (C) 2001-2008 Stas Sergeev
*/
#ifndef __PCSP_H__
#define __PCSP_H__
#include <linux/hrtimer.h>
#include <linux/timex.h>
#define MAX_DUTY_NS 998
#define MIN_DUTY_NS 10
#define DELTA_DUTY (MAX_DUTY_NS - MIN_DUTY_NS)
#define NSECS_PER_SEC 1000000000UL
#define HZ_TO_NANOSECONDS(x) (NSECS_PER_SEC / (x))
#define PWM_FREQ 1000000UL /* Period is 1us */
#define SAMPLING_FREQ 44100
#define PWM_PERIOD_NS HZ_TO_NANOSECONDS(PWM_FREQ)
#define SAMPLING_PERIOD_NS HZ_TO_NANOSECONDS(SAMPLING_FREQ)
#define PWMSP_MAX_PERIOD_SIZE (64*1024)
#define PWMSP_MAX_PERIODS 512
#define PWMSP_BUFFER_SIZE (128*1024)
struct snd_pwmsp {
struct snd_card *card;
struct snd_pcm *pcm;
struct hrtimer timer;
spinlock_t substream_lock;
struct snd_pcm_substream *playback_substream;
unsigned int fmt_size;
unsigned int is_signed;
size_t playback_ptr;
size_t period_ptr;
atomic_t timer_active;
int enable;
struct pwm_device *pwm;
};
extern struct snd_pwmsp pwmsp_chip;
extern enum hrtimer_restart pwmsp_do_timer(struct hrtimer *handle);
extern void pwmsp_sync_stop(struct snd_pwmsp *chip);
extern int snd_pwmsp_new_pcm(struct snd_pwmsp *chip);
extern int snd_pwmsp_new_mixer(struct snd_pwmsp *chip, int nopcm);
void pwmsp_stop_sound(void);
#endif
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
/*
* Based on PC-Speaker driver for Linux
*
* Copyright (C) 1993-1997 Michael Beck
* Copyright (C) 1997-2001 David Woodhouse
* Copyright (C) 2001-2008 Stas Sergeev
*/
#include <linux/module.h>
#include <linux/gfp.h>
#include <linux/moduleparam.h>
#include <linux/interrupt.h>
#include <linux/pwm.h>
#include <sound/pcm.h>
#include "pwmsp.h"
//#define PWMSP_DEBUG
#define DEBUG_TIME
static __maybe_unused ktime_t calltime, delta, rettime;
static __maybe_unused unsigned long duration;
static __maybe_unused unsigned long pwm_config_calls;
/*
* Call snd_pcm_period_elapsed in a tasklet
* This avoids spinlock messes and long-running irq contexts
*/
static void pwmsp_call_pcm_elapsed(unsigned long priv)
{
if (atomic_read(&pwmsp_chip.timer_active)) {
struct snd_pcm_substream *substream;
substream = pwmsp_chip.playback_substream;
if (substream)
snd_pcm_period_elapsed(substream);
#ifdef DEBUG_TIME
pr_debug("%lu calls, %lu ns, %lu ns per call\n",
pwm_config_calls, duration,
duration / pwm_config_calls);
pwm_config_calls = 0;
duration = 0;
#endif
}
}
static DECLARE_TASKLET(pwmsp_pcm_tasklet, pwmsp_call_pcm_elapsed, 0);
/* write the port and returns the next expire time in ns;
* called at the trigger-start and in hrtimer callback
*/
static u64 pwmsp_timer_update(struct snd_pwmsp *chip)
{
struct snd_pcm_substream *substream;
struct snd_pcm_runtime *runtime;
int duty_ns;
s16 val;
substream = chip->playback_substream;
if (!substream)
return 0;
runtime = substream->runtime;
/* TODO: Revisit here */
val = runtime->dma_area[chip->playback_ptr] |
runtime->dma_area[chip->playback_ptr + 1] << 8;
duty_ns = (val * DELTA_DUTY) / 65535 + DELTA_DUTY / 2 + MIN_DUTY_NS;
#ifdef DEBUG_TIME
calltime = ktime_get();
#endif
if (duty_ns && chip->enable)
pwm_config(chip->pwm, duty_ns, PWM_PERIOD_NS);
#ifdef DEBUG_TIME
rettime = ktime_get();
delta = ktime_sub(rettime, calltime);
duration += (unsigned long long) ktime_to_ns(delta) >> 10;
pwm_config_calls++;
#endif
return SAMPLING_PERIOD_NS;
}
static void pwmsp_pointer_update(struct snd_pwmsp *chip)
{
struct snd_pcm_substream *substream;
size_t period_bytes, buffer_bytes;
int periods_elapsed;
unsigned long flags;
/* update the playback position */
substream = chip->playback_substream;
if (!substream)
return;
period_bytes = snd_pcm_lib_period_bytes(substream);
buffer_bytes = snd_pcm_lib_buffer_bytes(substream);
spin_lock_irqsave(&chip->substream_lock, flags);
chip->playback_ptr += chip->fmt_size;
periods_elapsed = chip->playback_ptr - chip->period_ptr;
if (periods_elapsed < 0) {
#ifdef PWMSP_DEBUG
printk(KERN_INFO "PWMSP: buffer_bytes mod period_bytes != 0 ? "
"(%zi %zi %zi)\n",
chip->playback_ptr, period_bytes, buffer_bytes);
#endif
periods_elapsed += buffer_bytes;
}
periods_elapsed /= period_bytes;
/* wrap the pointer _before_ calling snd_pcm_period_elapsed(),
* or ALSA will BUG on us. */
chip->playback_ptr %= buffer_bytes;
if (periods_elapsed) {
chip->period_ptr += periods_elapsed * period_bytes;
chip->period_ptr %= buffer_bytes;
}
spin_unlock_irqrestore(&chip->substream_lock, flags);
if (periods_elapsed)
tasklet_schedule(&pwmsp_pcm_tasklet);
}
enum hrtimer_restart pwmsp_do_timer(struct hrtimer *handle)
{
struct snd_pwmsp *chip = container_of(handle, struct snd_pwmsp, timer);
u64 ns;
if (!atomic_read(&chip->timer_active) || !chip->playback_substream)
return HRTIMER_NORESTART;
ns = pwmsp_timer_update(chip);
if (!ns) {
printk(KERN_WARNING "PWMSP: unexpected stop\n");
return HRTIMER_NORESTART;
}
hrtimer_forward(handle, hrtimer_get_expires(handle), ns_to_ktime(ns));
pwmsp_pointer_update(chip);
return HRTIMER_RESTART;
}
static int pwmsp_start_playing(struct snd_pwmsp *chip)
{
#ifdef PWMSP_DEBUG
printk(KERN_INFO "PWMSP: start_playing called\n");
#endif
if (atomic_read(&chip->timer_active)) {
printk(KERN_ERR "PWMSP: Timer already active\n");
return -EIO;
}
/* Start the PWM */
pwm_enable(chip->pwm);
pwm_config(chip->pwm, 0, PWM_PERIOD_NS);
atomic_set(&chip->timer_active, 1);
hrtimer_start(&pwmsp_chip.timer, ktime_set(0, 0), HRTIMER_MODE_REL);
return 0;
}
static void pwmsp_stop_playing(struct snd_pwmsp *chip)
{
#ifdef PWMSP_DEBUG
printk(KERN_INFO "PWMSP: stop_playing called\n");
#endif
if (!atomic_read(&chip->timer_active))
return;
atomic_set(&chip->timer_active, 0);
pwm_disable(chip->pwm);
}
/*
* Force to stop and sync the stream
*/
void pwmsp_sync_stop(struct snd_pwmsp *chip)
{
local_irq_disable();
pwmsp_stop_playing(chip);
local_irq_enable();
hrtimer_cancel(&chip->timer);
tasklet_kill(&pwmsp_pcm_tasklet);
}
static int snd_pwmsp_playback_close(struct snd_pcm_substream *substream)
{
struct snd_pwmsp *chip = snd_pcm_substream_chip(substream);
#ifdef PWMSP_DEBUG
printk(KERN_INFO "PWMSP: close called\n");
#endif
pwmsp_sync_stop(chip);
chip->playback_substream = NULL;
return 0;
}
static int snd_pwmsp_playback_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *hw_params)
{
struct snd_pwmsp *chip = snd_pcm_substream_chip(substream);
int err;
pwmsp_sync_stop(chip);
err = snd_pcm_lib_malloc_pages(substream,
params_buffer_bytes(hw_params));
if (err < 0)
return err;
return 0;
}
static int snd_pwmsp_playback_hw_free(struct snd_pcm_substream *substream)
{
struct snd_pwmsp *chip = snd_pcm_substream_chip(substream);
#ifdef PWMSP_DEBUG
printk(KERN_INFO "PWMSP: hw_free called\n");
#endif
pwmsp_sync_stop(chip);
return snd_pcm_lib_free_pages(substream);
}
static int snd_pwmsp_playback_prepare(struct snd_pcm_substream *substream)
{
struct snd_pwmsp *chip = snd_pcm_substream_chip(substream);
pwmsp_sync_stop(chip);
chip->playback_ptr = 0;
chip->period_ptr = 0;
chip->fmt_size =
snd_pcm_format_physical_width(substream->runtime->format) >> 3;
chip->is_signed = snd_pcm_format_signed(substream->runtime->format);
#ifdef PWMSP_DEBUG
printk(KERN_INFO "PWMSP: prepare called, "
"size=%zi psize=%zi f=%zi f1=%i fsize=%i\n",
snd_pcm_lib_buffer_bytes(substream),
snd_pcm_lib_period_bytes(substream),
snd_pcm_lib_buffer_bytes(substream) /
snd_pcm_lib_period_bytes(substream),
substream->runtime->periods,
chip->fmt_size);
#endif
return 0;
}
static int snd_pwmsp_trigger(struct snd_pcm_substream *substream, int cmd)
{
struct snd_pwmsp *chip = snd_pcm_substream_chip(substream);
#ifdef PWMSP_DEBUG
printk(KERN_INFO "PWMSP: trigger called\n");
#endif
switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
case SNDRV_PCM_TRIGGER_RESUME:
return pwmsp_start_playing(chip);
case SNDRV_PCM_TRIGGER_STOP:
case SNDRV_PCM_TRIGGER_SUSPEND:
pwmsp_stop_playing(chip);
break;
default:
return -EINVAL;
}
return 0;
}
static snd_pcm_uframes_t snd_pwmsp_playback_pointer(struct snd_pcm_substream
*substream)
{
struct snd_pwmsp *chip = snd_pcm_substream_chip(substream);
unsigned int pos;
spin_lock(&chip->substream_lock);
pos = chip->playback_ptr;
spin_unlock(&chip->substream_lock);
return bytes_to_frames(substream->runtime, pos);
}
static struct snd_pcm_hardware snd_pwmsp_playback = {
.info = (SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_HALF_DUPLEX |
SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID),
.formats = SNDRV_PCM_FMTBIT_S16_LE,
.rates = SNDRV_PCM_RATE_KNOT,
.rate_min = SAMPLING_FREQ,
.rate_max = SAMPLING_FREQ,
.channels_min = 1,
.channels_max = 1,
.buffer_bytes_max = PWMSP_BUFFER_SIZE,
.period_bytes_min = 64,
.period_bytes_max = PWMSP_MAX_PERIOD_SIZE,
.periods_min = 2,
.periods_max = PWMSP_MAX_PERIODS,
.fifo_size = 0,
};
static int snd_pwmsp_playback_open(struct snd_pcm_substream *substream)
{
struct snd_pwmsp *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
#ifdef PWMSP_DEBUG
printk(KERN_INFO "PWMSP: open called\n");
#endif
if (atomic_read(&chip->timer_active)) {
printk(KERN_ERR "PWMSP: still active!!\n");
return -EBUSY;
}
runtime->hw = snd_pwmsp_playback;
chip->playback_substream = substream;
return 0;
}
static struct snd_pcm_ops snd_pwmsp_playback_ops = {
.open = snd_pwmsp_playback_open,
.close = snd_pwmsp_playback_close,
.ioctl = snd_pcm_lib_ioctl,
.hw_params = snd_pwmsp_playback_hw_params,
.hw_free = snd_pwmsp_playback_hw_free,
.prepare = snd_pwmsp_playback_prepare,
.trigger = snd_pwmsp_trigger,
.pointer = snd_pwmsp_playback_pointer,
};
int snd_pwmsp_new_pcm(struct snd_pwmsp *chip)
{
int err;
err = snd_pcm_new(chip->card, "pwmspeaker", 0, 1, 0, &chip->pcm);
if (err < 0)
return err;
snd_pcm_set_ops(chip->pcm, SNDRV_PCM_STREAM_PLAYBACK,
&snd_pwmsp_playback_ops);
chip->pcm->private_data = chip;
chip->pcm->info_flags = SNDRV_PCM_INFO_HALF_DUPLEX;
strcpy(chip->pcm->name, "pwmsp");
snd_pcm_lib_preallocate_pages_for_all(chip->pcm,
SNDRV_DMA_TYPE_CONTINUOUS,
snd_dma_continuous_data
(GFP_KERNEL), PWMSP_BUFFER_SIZE,
PWMSP_BUFFER_SIZE);
return 0;
}
@jadonk
Copy link
Author

jadonk commented Jul 18, 2019

I was also seeing hangs when I tried to play audio and was confused because it worked for me when I manually built and tested. Just today, I figured out to try to test the PWM first, then load the driver and it started working again. It seems there is a bug in the driver that keeps it from configuring the driver correctly at first.

Try running the "Test PWM" instructions first.

jadonk/bb.org-overlays@b6aecdd

@wolfallein
Copy link

@jadonk
I found the problem. My system was freezing when I was trying to play any sound, but I could see the kernel messages, and I also used #define PWMSP_DEBUG on pwmsp.c to see where it was happening.

I figured out that the driver was having a problem on the line #162 atomic_set(&chip->timer_active, 1); of pwmsp_lib.c I could fix the problem adding the line pm_runtime_irq_safe(&pdev->dev); to the drivers/pwm/pwm-tiehrpwm.c (like here) from the sources of the kernel 4.14.108.

When I recompiled the kernel I got sound :)

The inconvenience is that the sound work only in the second attempt. I have to start playing something, stop, and start again, and them it works.

I'm using the dts that I pasted in the previous message, and then I don't need to use config-pin pxx pwm, and I can also disable cape universal to save some seconds in the boot.

So, maybe this can be added to future builds of the kernel to avoid this problem, but I don't know what else can be impacted by the line...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment