feels like it doesn't get the time to fully write the contents of the buffer to the screen before refreshing.
i totally appreciate the effort, but i'm not sure if it's worth it. not for my use case, anyway..
is anyone successfully using i2c OLED / LCD?
Try removing the if(!sent)
line from the bottom
hmm. same.
this is the LFO example.
And increase the usleep in the next line to 100000 ?
@Remork I added another commit that should fix that . Try it pls
yes! definitely the best one yet.
don't know what you did exactly, but it seems to work.
i mean, you can still see it struggling.. parameter blocks are jumpy and especially on the sine waveform you can see the screen rolling. However! timing is as accurate as it will be and the buffer buildup is gone. yay!
i wouldn't use it for waveform displays as they are nowhere near smooth enough, but for most other functions this is beautiful. hats off to you sir.
mind explaining your process? am i correct in thinking this is at 1/4 refresh rate now?
Remork mind explaining your process? am i correct in thinking this is at 1/4 refresh rate now?
see the diff here: https://github.com/giuliomoro/O2O/compare/only-latest
Sending data to the screen has been moved out of the receiving thread (parseMessage()
) and moved to the main thread (main()
). parseMessage()
parses all incoming messages and updates the in-memory buffer with the bitmap representation that has to be sent out to the display. When this buffer has been modified, it notifies the main thread (by setting the global gShouldSend
per each display whose buffer has been updated), which proceeds to send the buffer to the display(s), ultimately causing the output to be visualised. Access to the in-memory buffers and the gShouldSend
flags are protected by the mutex mtx
, so that at any given time only one of parseMessage()
or main()
can read from or write to the buffer. The rate limiting is done by the fact that it doesn't matter how many times parseMessage()
has updated the buffer while main()
was sleeping: only the latest version of it will be written to the output. As parseMessage()
will in principle run much faster than main()
's sleep, main()
will be dropping frames, thus avoiding the backlog.
The max frame rate is sort of limited by the time it takes to send data to the screen plus any sleep in main(). There is usleep(50000)
in main()
, but actually this sleep is only observed if no data has just been sent out, so that this should only affect the jitter of isolated messages (delaying their visualisation by up to 50ms), so if you are sending a steady stream of messages, there will be no sleep and it will immediately try to lock the mtx
again.
Now that I think of it again, we are sort of over-relying on the Linux scheduler to ensure we have enough CPU time to process incoming messages and you could find yourselves having both threads either busy or waiting on a lock and so little progress may be made on the parseMessage()
side, still potentially leading to a backlog. This is because the "critical section" (i.e.: the section where the mutex is held) in main()
is rather long (i.e.: it takes a lot of time because it's sending a lot of bits over I2C, whereas best practice suggests to keep critical sections as short as possible and without I/O. It is possible that a better approach would be to make a deep copy of the U8G2 object that we want to send out to, but this requires changing things deep inside it.
I understand you are not seeing such issue right now, but I am thinking it could happen if the CPU gets busy with something else (e.g.: running Bela audio). One option here would be to change the priority of parseMessage()
thread so that it is higher than main()
(they are both at priority 0 right now. Alternatively, more simply, you could just force a sleep even in case something has just been sent.
Back of the envelope (optimistic) frame rate calculation:
payload (bits): 128*128*4 = 65536
transfer rate: 400000 bits/s
min transfer time (optimistic, assumes bus fully occupied and no transaction overheads): 65536/400000 = 0.16s
max frame rate = 1/0.16s = 6.25Hz
So even adding 1ms of sleep after just sending some data will not significantly slow down the frame rate, but may give parseMessage()
more CPU time to process incoming messages. Something like this would be nice:
if(sent)
usleep(1000); //short sleep
else
usleep(50000); //long sleep
Further note:
I scoped the I2C bus on Bela and it looks like instead of running at the nominal 400kHz it is doing 333kHz. This means we are actually below that, but I see no straightforward way of increasing that (the datasheet lists 100kHz or 400kHz as the only values) and it's probably not worth the effort here as the display's sampling rate wouldn't actually improve much here.
Table 13-6 of the SSD1327 datasheet shows that min "Clock Cycle Time" is 2.5us, i.e.: 400kHz seems to be the highest clock you could use here and we are already close to that.
giuliomoro I scoped the I2C bus on Bela and it looks like instead of running at the nominal 400kHz it is doing 333kHz.
correction: with pull-up resistors this actually goes higher. I am at 384kHz with 10k pullups. Probably reaches the nominal 400kHz with more reasonable 2k2 pullups.
giuliomoro Sending data to the screen has been moved out of the receiving thread (parseMessage()) and moved to the main thread (main()). parseMessage() parses all incoming messages and updates the in-memory buffer with the bitmap representation that has to be sent out to the display. When this buffer has been modified, it notifies the main thread (by setting the global gShouldSend per each display whose buffer has been updated), which proceeds to send the buffer to the display(s), ultimately causing the output to be visualised. Access to the in-memory buffers and the gShouldSend flags are protected by the mutex mtx, so that at any given time only one of parseMessage() or main() can read from or write to the buffer. The rate limiting is done by the fact that it doesn't matter how many times parseMessage() has updated the buffer while main() was sleeping: only the latest version of it will be written to the output. As parseMessage() will in principle run much faster than main()'s sleep, main() will be dropping frames, thus avoiding the backlog.
The max frame rate is sort of limited by the time it takes to send data to the screen plus any sleep in main(). There is usleep(50000) in main(), but actually this sleep is only observed if no data has just been sent out, so that this should only affect the jitter of isolated messages (delaying their visualisation by up to 50ms), so if you are sending a steady stream of messages, there will be no sleep and it will immediately try to lock the mtx again.
okay. i understand that part couldn't have come up w/ that myself, as i don't grasp the concepts used enough to know that such a move would result in more stability. it makes sense when you explain it, though.
the part about the linux scheduler i'll have to read twice.
but i understand that if we're at 333kHz, without pullups, we're slower than the max rate of 6.25Hz?
which seems sort of slow to begin with - how does that compare to 'standard' monochrome Oled?
i mean, i can clearly see the difference, maybe that's all that matters.
thinking of which, would these adjustments also improve speed on a regular SSD1306, for example?
- Edited
Remork thinking of which, would these adjustments also improve speed on a regular SSD1306, for example?
I think something similar to these adjustments is needed only when you are sending data faster than can be transferred to the screen, so that the pile up starts. For any display going through a 400kHz I2C bus you can do similar calculations, but the payload will be different. Above
payload (bits): 128*128*4 = 65536
is 128x128 pixels at 4 bit each. An SSD1306 is monochrome, 128x64, so the per-frame payload is 1/8 of that of the SSD1327 (128*64*1 = 8192)
and so the theoretical frame rate is 8x at 48Hz. Now, it's unlikely you'll reach that when the CPU is doing other things as well, but you probably would be happy with half that anyhow and that is totally achievable. For grayscale or color screens you should probably consider using SPI instead, as that can easily reach over 10Mbps.
Remork but i understand that if we're at 333kHz, without pullups, we're slower than the max rate of 6.25Hz?
Yes, possibly. That is what I measured on Bela without any load on the bus, but it may be that your board already has pullups on that bus, or that the screen breakout has them, or that the simple presence of the screen on the bus makes it slightly faster (I2C is a bit of a weird protocol in terms of "speed"...). Or you can add some 2k2 resistors just in case. Point is, you are not going to get much better than that.
Remork thinking of which, would these adjustments also improve speed on a regular SSD1306, for example?
They shouldn't hurt. Ultimately these only kick in if you are sending data to Bela faster than it can be sent to the screen. While this is a useful catch-all workaround, I would recommend instead that you limit the rate at which you send messages to O2O, so that you save the CPU time spent parsing messages and rendering frames that will eventually not be displayed.
Remork I came across this thread because I am also implementing an SDD1306 OLED. I can get everything working, but I have extreme delays too. I am sending via Max/MSP OSC messages with /params and only get good results by adding a 50 ms speedlim. Did you solved the delays problem? A help would be greatly appreciated
If the processor on Bela was only sending I2C data to the OLED, it would take 128*64/8*10/400000 = 25.6ms
to send a frame (10 being the number of clock slots required to transmit 8 bits via I2c), so that's the hard limit for refresh rate. Considering the communication overhead of the I2C bus and that the processor is also doing other things (at the very least processing audio and OSC), I think 50 ms is a good result and I am not sure it can be pushed much faster. If you want to go faster than that, you should be looking at SPI displays that allow much higher transfer rates.
- Edited
thank you @giuliomoro, super clear. I will adapt to this speed for the moment.
But I have a 7-Segment Serial Display which can communicate via SPI. Are there any examples for a 7-Segment Serial Display implementation via SPI? I just found this thread https://forum.bela.io/d/754-7-segment-display.
Or would you have a recommended SPI display tested with Bela? Thank you very much!
albert But I have a 7-Segment Serial Display which can communicate via SPI.
Do you have a datasheet for it ? This could be probably driven from two Bela GPIOs using soft-SPI, given the very small bandwidth it will probably require ...
albert Or would you have a recommended SPI display tested with Bela? Thank you very much!
there were some posted here in the past, including an RGB one ... I should note that on Bela there is no spare SPI bus, so you'd have to sacrifice either audio inputs or analog inputs. On Bela Mini, on the other hand, there is a spare SPI bus available and ready.
giuliomoro this is the Display I have https://www.sparkfun.com/datasheets/Components/LED/7-Segment/SFE-0012-DS-7segmentSerial-v41.pdf
I have a Bela Mini too, I can tri with the SPI bus
thx
- Edited
If you only need to use one display, then maybe it's best to use the UART, as it provides a UART input as well. See instructions here https://learn.bela.io/using-bela/technical-explainers/device-tree-overlays/#image-v034-or-above to enable UART4 on Bela (the Tx pin, once enabled, is P9.13), or use config-pin P2.07 uart
on BelaMini and use P2.07 as the Tx pin.
Then you can use our Serial library to send messages to the display. The current example for the Serial
library (Communication/Serial
) only receives text via UART. I modified it so that it should work with your display, see below. This should send a waiting pattern .___
_.__
__._
___.
to the display. If you were to connect the RX pin as well to something that can send a k
or s
then you'd get printed "hich" or "5nar" respectively. Anyhow, looking at it you should be able to figure out how to setup and write to the display. This was implemented based on the datasheet, but of course I haven't tested it.
/*
____ _____ _ _
| __ )| ____| | / \
| _ \| _| | | / _ \
| |_) | |___| |___ / ___ \
|____/|_____|_____/_/ \_\
http://bela.io
*/
/**
\example Communication/Serial/render.cpp
Serial communication
------------
This example demonstrates how to receive and transmit serial data from Bela.
When a 'k' or 's' are received on the specified serial port, a kick or snare
sound, respectively, are generated.
Serial data is received in a lower-priority thread (an AuxiliaryTask). From
there, relevant data is passed to the audio thread via a Pipe. The
AuxiliaryTask is also writing to the serial port.
If the `useDisplay` variable is set to `true`, the code will assume that the
output of the UART is connected to a Sparkfun Serial 7-Segment Display
(https://www.sparkfun.com/datasheets/Components/LED/7-Segment/SFE-0012-DS-7segmentSerial-v41.pdf)
and write messages that are compatible with it.
The sound generator is very simple and "retro". It uses a decaying noise
burst for the snare sound and a decaying sine sweep for the kick
*/
#include <Bela.h>
#include <libraries/Pipe/Pipe.h>
#include <libraries/Serial/Serial.h>
#include <cmath>
Serial gSerial;
Pipe gPipe;
unsigned int gSnareDuration;
int gSnareTime = 0;
unsigned int gKickDuration;
int gKickTime = 0;
float gKickPhase = 0;
bool useDisplay = true; // whether to use the Sparkfun Serial 7-Segment Display
void serialIo(void* arg) {
while(!Bela_stopRequested())
{
unsigned int maxLen = 128;
char serialBuffer[maxLen];
// read from the serial port with a timeout of 300ms
int ret = gSerial.read(serialBuffer, maxLen, 300);
if (ret > 0) {
printf("Received: %.*s\n", ret, serialBuffer);
for(int n = 0; n < ret; ++n)
{
// when some relevant data is received
// send a notification to the audio thread via
// the pipe
// and send a reply on the serial port
if('k' == serialBuffer[n])
{
gPipe.writeNonRt('k');
if(useDisplay)
{
// the display does not support all
// characters so we have to be a bit
// creative about which ones we use.
// When writing data, we manually
// specify the size because the display
// requires that exactly 4 bytes are
// written at a time.
const char buf[] = "hich";
gSerial.write(buf, 4);
} else
gSerial.write("kick!\n\r");
} else if('s' == serialBuffer[n])
{
gPipe.writeNonRt('s');
if(useDisplay){
const char buf[] = "5NAR";
gSerial.write(buf, 4);
} else {
gSerial.write("snare!\n\r");
}
}
}
} else {
if(useDisplay)
{
// if nothing came in, show a waiting pattern
char buf[] = "----";
static int dot = 0;
buf[dot] = '.';
dot++;
if(dot >= 4)
dot = 0;
gSerial.write(buf, 4);
}
}
}
}
bool setup(BelaContext *context, void *userData) {
gSerial.setup ("/dev/ttyS4", 9600);
AuxiliaryTask serialCommsTask = Bela_createAuxiliaryTask(serialIo, 0, "serial-thread", NULL);
Bela_scheduleAuxiliaryTask(serialCommsTask);
gPipe.setup("serialpipe", 1024);
gKickDuration = 0.2 * context->audioSampleRate;
gSnareDuration = 0.2 * context->audioSampleRate;
return true;
}
void render(BelaContext *context, void *userData) {
char c;
// check if the serial thread has received any message and sent a notification
while(gPipe.readRt(c) > 0)
{
// if there is, trigger the start of the respective sound
if('s' == c)
gSnareTime = gSnareDuration;
if('k' == c)
gKickTime = gKickDuration;
}
for(unsigned int n = 0; n < context->audioFrames; ++n)
{
// synthesize the snare and kick
float snareOut = 0;
float kickOut = 0;
if(gSnareTime)
{
// just a burst of white noise with a decaying envelope
float noise = 2.f * (rand() / (float)RAND_MAX) - 1.f;
float env = gSnareTime / (float)gSnareDuration;
snareOut = 0.4f * noise * env;
--gSnareTime;
}
if(gKickTime)
{
// a descending sinewave
float frequency = map(gKickTime, gKickDuration, 0, 150, 20);
float env = gKickTime / (float)gKickDuration;
gKickPhase += 2.f * (float)M_PI * frequency / context->audioSampleRate;
if(gKickPhase > M_PI)
gKickPhase -= 2.f * (float)M_PI;
kickOut = env * sinf(gKickPhase);
--gKickTime;
}
float out = snareOut + kickOut;
for(unsigned int ch = 0; ch < context->audioOutChannels; ++ch)
{
audioWrite(context, n, ch, out);
}
}
}
void cleanup(BelaContext *context, void *userData) {}