-
-
Notifications
You must be signed in to change notification settings - Fork 8.4k
zephyr: Introduce Zephyr displays #17939
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
a13ccf0
to
daaeb67
Compare
Code size report:
|
7a89e4d
to
48e8d5b
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #17939 +/- ##
=======================================
Coverage 98.38% 98.38%
=======================================
Files 171 171
Lines 22297 22297
=======================================
Hits 21938 21938
Misses 359 359 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Looking over this briefly, the duplicate methods with delegation to framebuf feel like they would be a nuisance to maintain. Anyone changing framebuf would need to check for compatibility breaks in Zephyr Display and I'm not sure that's feasible. Is it possible to retrieve the buffer from a Zephyr Display and use that to initialise a vanilla instance of framebuf module? If so, some of the complexity of doing so could be swept under the rug with a Python module that sets the correct pixel type, dimensions, etc. In such a case, ZephyrDisplay should just handle providing the buffer and some basic display config information and it would be compatible with not just framebuf, but any means by which a user cares to set pixels (ulab's numpy implementation for example.) |
Zephyr doesnt provide a buffer to write into so it's not possible to use that directly. But it sounds like maybe it should be possible to have the zephyrdisplay allocate the appropriate buffer and provide that instead, with other attributes to provide to framebuf (display format, etc? that's what you mean i think) Maybe though, allocating the framebuf in the zephyrdisplay, and then having a method return that framebuf instead would be a better solution? |
Actually thinking about it there really is no point allocating the buffer for the user anyway if it's not fully automated /subclassing framebuf, instead the API from zephyr should be duplicated more closely, and allow mpy to retrieve the display infos, then send whatever buffer the user wants to send at x,y coordinate and of z size, like the zephyr API. Maybe both should coexist (ZephyrDisplayFramebuf and ZephyrDisplay?) |
Possibly relevant: LVGL Zephyr integration for MicorPython: |
Ah! In that case yes - FrameBuf should allocate the buffer, and ZephyrDisplay should supply the dimensions and format of buffer the display needs. (Looks like it supplies compatible formats and the user picks one, which is nice)
Absolutely this. It should be completely agnostic to whatever provides the framebuffer, so long as it is in the correct (or a supported?) pixel format and dimensions. It could be a It looks like Zephyr's display interface (this? https://docs.zephyrproject.org/latest/doxygen/html/group__display__interface.html) is already well suited to this pattern. For stuff like So yes- agnostic, 1:1 implementation of the Zephyr display interface, and a Nice work though, |
d2f97e1
to
1be5c40
Compare
@Gadgetoid Updated to be like the API |
843e04d
to
5d85ef2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will need to revive my MicroPython memory and give it another look...
I hope this helps in the meantime.
ports/zephyr/modzephyr.c
Outdated
#if defined(CONFIG_DISPLAY) && defined(MICROPY_PY_FRAMEBUF) | ||
{ MP_ROM_QSTR(MP_QSTR_Display), MP_ROM_PTR(&zephyr_display_type) }, | ||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that there is no inheritance from framebuf, the && defined(MICROPY_PY_FRAMEBUF)
can go?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, forgot to remove.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
ports/zephyr/zephyr_display.c
Outdated
mp_arg_check_num(n_args, n_kw, 1, 1, false); | ||
int id = mp_obj_get_int(args[0]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at other drivers, they use strings for creating new instances:
micropython/ports/zephyr/machine_spi.c
Line 85 in b5fcb33
const struct device *dev = zephyr_device_find(args[ARG_id].u_obj); |
micropython/ports/zephyr/machine_uart.c
Line 166 in b5fcb33
const struct device *dev = zephyr_device_find(args[0]); |
micropython/ports/zephyr/modzsensor.c
Line 45 in b5fcb33
o->dev = zephyr_device_find(args[0]); |
It seems like the first PR that uses DT_CHOSEN()
mapped to numbers instead of strings, but it feels the right way to map chosen nodes IMHO.
To help the user figure whether to use a string or integer, some rule of thumb would be something like this?
Display(0)
valid whenever a module supportsDT_CHOSEN()
(like you did now)Display("labelname")
always available to get a Zephyr device by string name
Using strings can be easier to implement as it's just a zephyr_device_find()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well it can do both depending on input type, so ill add the string method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
|
||
if (mp_obj_get_int(arg) > MP_QSTR_ROTATION_270 || mp_obj_get_int(arg) < 0) { | ||
mp_raise_ValueError("Invalid orientation"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it is just me who misunderstood QSTR, but it looks like this is a comparison between a string index and a value as an integer here?
Maybe you meant to have an extra set of define like this:
#define DISPLAY_ROTATION_NONE 0
#define DISPLAY_ROTATION_90 1
#define DISPLAY_ROTATION_180 2
#define DISPLAY_ROTATION_270 3
And then use them in the zephyr_display_locals_dict_table
:
{ MP_ROM_QSTR(MP_QSTR_ROTATION_NONE), MP_ROM_INT(DISPLAY_ROTATION_NONE) },
{ MP_ROM_QSTR(MP_QSTR_ROTATION_90), MP_ROM_INT(DISPLAY_ROTATION_90) },
{ MP_ROM_QSTR(MP_QSTR_ROTATION_180), MP_ROM_INT(DISPLAY_ROTATION_180) },
{ MP_ROM_QSTR(MP_QSTR_ROTATION_270), MP_ROM_INT(DISPLAY_ROTATION_270) },
So that they can be compared here as integers?
if (mp_obj_get_int(arg) > DISPLAY_ROTATION_270 ||
mp_obj_get_int(arg) < DISPLAY_ROTATION_NONE) {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Using MP_QSTR_ROTATION_270
like this might actually work, but isn't very idiomatic of MicroPython bindings.
(Of course now I'm tempted to try.)
Edit: Of course I'm talking nonsense, mp_obj_get_int(arg)
would raise a TypeError
when given a string, which of course makes sense. And there's no guarantee that MP_QSTR_ROTATION_270 == 270
or even that the QSTR integer values are in any particular order.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is supposed to be DISPLAY_ORIENTATION_ROTATED_270 ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be able to expose those constants to MicroPython too, perhaps with the same naming scheme so Zephyr users are at home?
{ MP_ROM_QSTR(MP_QSTR_DISPLAY_ORIENTATION_NORMAL), MP_ROM_INT(DISPLAY_ORIENTATION_NORMAL) },
{ MP_ROM_QSTR(MP_QSTR_DISPLAY_ORIENTATION_ROTATED_90), MP_ROM_INT(DISPLAY_ORIENTATION_ROTATED_90) },
{ MP_ROM_QSTR(MP_QSTR_DISPLAY_ORIENTATION_ROTATED_180), MP_ROM_INT(DISPLAY_ORIENTATION_ROTATED_180) },
{ MP_ROM_QSTR(MP_QSTR_DISPLAY_ORIENTATION_ROTATED_270), MP_ROM_INT(DISPLAY_ORIENTATION_ROTATED_270) },
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
ports/zephyr/zephyr_display.c
Outdated
|
||
typedef struct _zephyr_display_obj_t { | ||
mp_obj_base_t base; | ||
uint8_t id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If wanting to support displays from other sources than chosen { zephyr,display = &xyz; };
for any reason, then this can be defined as const struct device *dev;
like done elsewhere:
micropython/ports/zephyr/machine_spi.c
Line 52 in b5fcb33
const struct device *dev; |
micropython/ports/zephyr/modzsensor.c
Line 39 in b5fcb33
const struct device *dev; |
Then the code can be a bit shorter using self->dev
instead of zephyr_display_devices[self->id]
.
There might be other reasons to use IDs that I missed though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just scrolled up here to make this same point! 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
ports/zephyr/zephyr_display.c
Outdated
} zephyr_display_obj_t; | ||
|
||
static void zephyr_display_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { | ||
zephyr_display_obj_t *self = self_in; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs MP_OBJ_TO_PTR
I think!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
ports/zephyr/zephyr_display.c
Outdated
len = sx * sy * DISPLAY_BITS_PER_PIXEL(capabilities.current_pixel_format) / 8; | ||
|
||
if (len > bufinfo.len) { | ||
mp_raise_ValueError("Buffer is shorter than size requires"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This and other errors should use MP_ERROR_TEXT
so it doesn't explode the build if/when MicroPython's error text compression is enabled, eg:
mp_raise_ValueError(MP_ERROR_TEXT("Buffer is shorter than size requires"));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
ports/zephyr/zephyr_display.c
Outdated
default: | ||
return mp_obj_new_int(0); | ||
} | ||
return mp_obj_new_int(0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unreachable return.
Possibly better to return mp_const_none
in default:
or even raise an exception so the user isn't left with a valid-looking colour in the exceptional case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
} | ||
if (format < 0) { | ||
mp_raise_msg(&mp_type_RuntimeError, "Display is not of a compatible pixel format"); | ||
return mp_const_none; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unreachable return, there's no need to return after an exception.
I'm not totally sure a RuntimeError
is the right pattern here, either. Should probably just return mp_const_none
and let documentation indicate this means "incompatible." Otherwise it bails user code before they have a chance to (maybe?) set a compatible pixel format.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
ports/zephyr/zephyr_display.c
Outdated
|
||
display_get_capabilities(zephyr_display_devices[self->id], &capabilities); | ||
|
||
return mp_obj_new_int(capabilities.supported_pixel_formats); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could maybe unpack this into a tuple so it's more idiomatic of Python? eg:
if display.FORMAT_MONO01 in display.zephyr_formats():
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
1b4f28e
to
111420b
Compare
{ MP_ROM_QSTR(MP_QSTR_FORMAT_RGB_565), MP_ROM_INT(PIXEL_FORMAT_RGB_565) }, | ||
{ MP_ROM_QSTR(MP_QSTR_FORMAT_BGR_565), MP_ROM_INT(PIXEL_FORMAT_BGR_565) }, | ||
{ MP_ROM_QSTR(MP_QSTR_FORMAT_L_8), MP_ROM_INT(PIXEL_FORMAT_L_8) }, | ||
{ MP_ROM_QSTR(MP_QSTR_FORMAT_AL_88), MP_ROM_INT(PIXEL_FORMAT_AL_88) }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PIXEL_FORMAT_AL_88
is undefined since it was only added five days ago:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like this to work around?
{ MP_ROM_QSTR(MP_QSTR_FORMAT_L_8), MP_ROM_INT(PIXEL_FORMAT_L_8) },
#ifdef PIXEL_FORMAT_AL_88 /* will be in Zephyr 4.3 */
{ MP_ROM_QSTR(MP_QSTR_FORMAT_AL_88), MP_ROM_INT(PIXEL_FORMAT_AL_88) },
#endif
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea I work off main, applied josuah's solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forgot to indent the #ifdef
MicroPython-style as you noticed.
ports/zephyr/zephyr_display.c
Outdated
mp_arg_check_num(n_args, n_kw, 1, 1, false); | ||
const struct device *dev; | ||
|
||
if (mp_obj_get_type(args[0]) == &mp_type_str) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mp_obj_is_type()
is more conventional in these cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
111420b
to
d795874
Compare
Allows using zephyr displays. Signed-off-by: Vdragon <mail@massdriver.space>
d795874
to
518cace
Compare
Also fyi you can just give it the framebuf due to buffer protocol it's really nice:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a few extra checks...
|
||
if (id < 0 || id >= DT_ZEPHYR_DISPLAYS_COUNT) { | ||
mp_raise_ValueError(MP_ERROR_TEXT("Invalid display ID")); | ||
return mp_const_none; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following @Gadgetoid's previous comment, I discovered that mp_raise_msg()
returns in a different function using MicroPython magic, via longjmp()
.
mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("Display is not ready")); | ||
return mp_const_none; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIU, return
is not needed after mp_raise_msg()
.
mp_raise_ValueError(MP_ERROR_TEXT("Not a framebuf pixel format")); | ||
return mp_const_none; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIU, return
is not needed after mp_raise_...()
.
int x = mp_obj_get_int(args_in[2]); | ||
int y = mp_obj_get_int(args_in[3]); | ||
int sx; | ||
int sy; | ||
|
||
mp_get_buffer_raise(args_in[1], &bufinfo, MP_BUFFER_READ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At line 134 it is possible to have between 2 and 6 arguments.
Maybe you meant to allow disp.write(fb)
and disp.write(fb, 0)
?
int x = mp_obj_get_int(args_in[2]); | |
int y = mp_obj_get_int(args_in[3]); | |
int sx; | |
int sy; | |
mp_get_buffer_raise(args_in[1], &bufinfo, MP_BUFFER_READ); | |
int x = 0; | |
int y = 0; | |
int sx; | |
int sy; | |
mp_get_buffer_raise(args_in[1], &bufinfo, MP_BUFFER_READ); | |
if (n_args > 2) { | |
x = mp_obj_get_int(args_in[2]); | |
} | |
if (n_args > 3) { | |
y = mp_obj_get_int(args_in[3]); | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that is a mistake, it should be able to take only the buffer argument.
Looking at other display drivers around:
I like how it uses the buffer protocol now, it feels very native to MicroPython. :) |
Summary
Allows using zephyr displays as framebuf
Testing
SSD1327 + RP2040
and SSD1357 + RP2040