diff options
| author | tnn <tnn@NetBSD.org> | 2019-03-17 00:57:15 +0000 |
|---|---|---|
| committer | tnn <tnn@NetBSD.org> | 2019-03-17 00:57:15 +0000 |
| commit | e690818e5949bcde959c18d0b1432faa105eb445 (patch) | |
| tree | 1859d8eb593e386176be1022604777b7c8ed045f /sys/dev | |
| parent | a89bfe2d1be6c2b6e144536980691b104526b222 (diff) | |
add ssdfb
The ssdfb driver provides wsdisplay(4) support for OLED/PLED framebuffer
modules based on one of the following controller chips:
- Solomon Systech Ltd SSD1306
- Sino Wealth Electronic Ltd SH1106
It supports xf86-video-wsfb and can optionally attach as the console.
Some products that should work with this driver include:
- Adafruit 0.96" 128x64 OLED graphic display
- Adafruit 1.3" 128x64 OLED graphic display
- Adafruit 128x32 OLED graphic display
- Adafruit PiOLED 128x32 Monochome OLED for Raspberry Pi
- "GM009605" commonly distributed with Arduino starter kits
- display modules made by Chengdu Heltec Automation technology co. LTD
Note: I used the name ssdfb(4) because that's what OpenBSD calls their
driver but the implementation is entirely unique to NetBSD.
Diffstat (limited to 'sys/dev')
| -rw-r--r-- | sys/dev/ic/ssdfb.c | 1001 | ||||
| -rw-r--r-- | sys/dev/ic/ssdfbvar.h | 237 |
2 files changed, 1238 insertions, 0 deletions
diff --git a/sys/dev/ic/ssdfb.c b/sys/dev/ic/ssdfb.c new file mode 100644 index 00000000000..0448c0766c0 --- /dev/null +++ b/sys/dev/ic/ssdfb.c @@ -0,0 +1,1001 @@ +/* $NetBSD: ssdfb.c,v 1.1 2019/03/17 00:57:15 tnn Exp $ */ + +/* + * Copyright (c) 2019 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is derived from software contributed to The NetBSD Foundation + * by Tobias Nygren. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include <sys/cdefs.h> +__KERNEL_RCSID(0, "$NetBSD: ssdfb.c,v 1.1 2019/03/17 00:57:15 tnn Exp $"); + +#include "opt_ddb.h" + +#include <sys/param.h> +#include <sys/kernel.h> +#include <uvm/uvm_page.h> +#include <sys/condvar.h> +#include <sys/kmem.h> +#include <sys/kthread.h> +#include <dev/wscons/wsdisplayvar.h> +#include <dev/rasops/rasops.h> +#include <dev/ic/ssdfbvar.h> + +#if defined(DDB) +#include <machine/db_machdep.h> +#include <ddb/db_extern.h> +#endif + +/* userland interface */ +static int ssdfb_ioctl(void *, void *, u_long, void *, int, struct lwp *); +static paddr_t ssdfb_mmap(void *, void *, off_t, int); + +/* wscons screen management */ +static int ssdfb_alloc_screen(void *, const struct wsscreen_descr *, + void **, int *, int *, long *); +static void ssdfb_free_screen(void *, void *); +static int ssdfb_show_screen(void *, void *, int, + void (*cb) (void *, int, int), void *); + +/* rasops hooks */ +static void ssdfb_putchar(void *, int, int, u_int, long); +static void ssdfb_copycols(void *, int, int, int, int); +static void ssdfb_erasecols(void *, int, int, int, long); +static void ssdfb_copyrows(void *, int, int, int); +static void ssdfb_eraserows(void *, int, int, long); +static void ssdfb_cursor(void *, int, int, int); + +/* hardware interface */ +static int ssdfb_init(struct ssdfb_softc *); +static int ssdfb_set_contrast(struct ssdfb_softc *, uint8_t, bool); +static int ssdfb_set_display_on(struct ssdfb_softc *, bool, bool); +static int ssdfb_set_mode(struct ssdfb_softc *, u_int); + +/* frame buffer damage tracking and synchronization */ +static bool ssdfb_is_modified(struct ssdfb_softc *sc); +static bool ssdfb_clear_modify(struct ssdfb_softc *sc); +static void ssdfb_damage(struct ssdfb_softc *); +static void ssdfb_thread(void *); +static void ssdfb_set_usepoll(struct ssdfb_softc *, bool); +static int ssdfb_sync(struct ssdfb_softc *, bool); +static uint64_t ssdfb_transpose_block_1bpp(uint8_t *, size_t); +static uint64_t ssdfb_transpose_block_8bpp(uint8_t *, size_t); + +/* misc helpers */ +static const struct ssdfb_product * + ssdfb_lookup_product(ssdfb_product_id_t); +static int ssdfb_pick_font(int *, struct wsdisplay_font **); +static void ssdfb_clear_screen(struct ssdfb_softc *); +#if defined(DDB) +static void ssdfb_ddb_trap_callback(int); +#endif + +static const char *ssdfb_controller_names[] = { + [SSDFB_CONTROLLER_UNKNOWN] = "unknown", + [SSDFB_CONTROLLER_SSD1306] = "Solomon Systech SSD1306", + [SSDFB_CONTROLLER_SH1106] = "Sino Wealth SH1106" +}; + +/* + * Display module assemblies supported by this driver. + */ +static const struct ssdfb_product ssdfb_products[] = { + { + .p_product_id = SSDFB_PRODUCT_SSD1306_GENERIC, + .p_controller_id = SSDFB_CONTROLLER_SSD1306, + .p_name = "generic", + .p_width = 128, + .p_height = 64, + .p_panel_shift = 0, + .p_fosc = 0x8, + .p_fosc_div = 0, + .p_precharge = 0x1, + .p_discharge = 0xf, + .p_compin_cfg = SSDFB_COM_PINS_A1_MASK + | SSDFB_COM_PINS_ALTERNATIVE_MASK, + .p_vcomh_deselect_level = SSD1306_VCOMH_DESELECT_LEVEL_0_77_VCC, + .p_default_contrast = 0x7f, + .p_multiplex_ratio = 0x3f, + .p_chargepump_cmd = SSD1306_CMD_SET_CHARGE_PUMP, + .p_chargepump_arg = SSD1306_CHARGE_PUMP_ENABLE + }, + { + .p_product_id = SSDFB_PRODUCT_SH1106_GENERIC, + .p_controller_id = SSDFB_CONTROLLER_SH1106, + .p_name = "generic", + .p_width = 128, + .p_height = 64, + .p_panel_shift = 2, + .p_fosc = 0x5, + .p_fosc_div = 0, + .p_precharge = 0x2, + .p_discharge = 0x2, + .p_compin_cfg = SSDFB_COM_PINS_A1_MASK + | SSDFB_COM_PINS_ALTERNATIVE_MASK, + .p_vcomh_deselect_level = SH1106_VCOMH_DESELECT_LEVEL_DEFAULT, + .p_default_contrast = 0x80, + .p_multiplex_ratio = 0x3f, + .p_chargepump_cmd = SH1106_CMD_SET_CHARGE_PUMP_7V4, + .p_chargepump_arg = SSDFB_CMD_NOP + }, + { + .p_product_id = SSDFB_PRODUCT_ADAFRUIT_938, + .p_controller_id = SSDFB_CONTROLLER_SSD1306, + .p_name = "Adafruit Industries, LLC product 938", + .p_width = 128, + .p_height = 64, + .p_panel_shift = 0, + .p_fosc = 0x8, + .p_fosc_div = 0, + .p_precharge = 0x1, + .p_discharge = 0xf, + .p_compin_cfg = 0x12, + .p_vcomh_deselect_level = 0x40, + .p_default_contrast = 0x8f, + .p_multiplex_ratio = 0x3f, + .p_chargepump_cmd = SSD1306_CMD_SET_CHARGE_PUMP, + .p_chargepump_arg = SSD1306_CHARGE_PUMP_ENABLE + }, + { + .p_product_id = SSDFB_PRODUCT_ADAFRUIT_931, + .p_controller_id = SSDFB_CONTROLLER_SSD1306, + .p_name = "Adafruit Industries, LLC product 931", + .p_width = 128, + .p_height = 32, + .p_panel_shift = 0, + .p_fosc = 0x8, + .p_fosc_div = 0, + .p_precharge = 0x1, + .p_discharge = 0xf, + .p_compin_cfg = 0x2, + .p_vcomh_deselect_level = 0x40, + .p_default_contrast = 0x8f, + .p_multiplex_ratio = 0x1f, + .p_chargepump_cmd = SSD1306_CMD_SET_CHARGE_PUMP, + .p_chargepump_arg = SSD1306_CHARGE_PUMP_ENABLE + } +}; + +static const struct wsdisplay_accessops ssdfb_accessops = { + .ioctl = ssdfb_ioctl, + .mmap = ssdfb_mmap, + .alloc_screen = ssdfb_alloc_screen, + .free_screen = ssdfb_free_screen, + .show_screen = ssdfb_show_screen +}; + +#define SSDFB_CMD1(c) do { cmd[0] = (c); error = sc->sc_cmd(sc->sc_cookie, cmd, 1, usepoll); } while(0) +#define SSDFB_CMD2(c, a) do { cmd[0] = (c); cmd[1] = (a); error = sc->sc_cmd(sc->sc_cookie, cmd, 2, usepoll); } while(0) +#define SSDFB_CMD3(c, a, b) do { cmd[0] = (c); cmd[1] = (a); cmd[2] = (b); error = sc->sc_cmd(sc->sc_cookie, cmd, 3, usepoll); } while(0) + +void +ssdfb_attach(struct ssdfb_softc *sc, int flags) +{ + struct wsemuldisplaydev_attach_args aa; + struct rasops_info *ri = &sc->sc_ri; + int error = 0; + long defattr; + const struct ssdfb_product *p; + + p = ssdfb_lookup_product(flags & SSDFB_ATTACH_FLAG_PRODUCT_MASK); + if (p == NULL) { + aprint_error(": unknown display assembly\n"); + return; + } + sc->sc_p = p; + + aprint_naive("\n"); + aprint_normal(": %s (%s)\n", + ssdfb_controller_names[p->p_controller_id], + p->p_name); + + sc->sc_mode = WSDISPLAYIO_MODE_EMUL; + sc->sc_is_console = flags & SSDFB_ATTACH_FLAG_CONSOLE ? true : false; + sc->sc_inverse = flags & SSDFB_ATTACH_FLAG_INVERSE ? true : false; + sc->sc_upsidedown = flags & SSDFB_ATTACH_FLAG_UPSIDEDOWN ? true : false; + sc->sc_backoff = 1; + sc->sc_contrast = sc->sc_p->p_default_contrast; + sc->sc_gddram_len = sc->sc_p->p_width * sc->sc_p->p_height / 8; + sc->sc_gddram = kmem_alloc(sc->sc_gddram_len, KM_SLEEP); + if (sc->sc_gddram == NULL) + goto out; + + aprint_normal_dev(sc->sc_dev, "%dx%d%s\n", sc->sc_p->p_width, + sc->sc_p->p_height, sc->sc_is_console ? ", console" : ""); + + /* + * Initialize rasops. The native depth is 1-bit monochrome and we + * support this in text emul mode via rasops1. But modern Xorg + * userland has many rendering glitches when running with 1-bit depth + * so to better support this use case we instead declare ourselves as + * an 8-bit display with a two entry constant color map. + */ + error = ssdfb_pick_font(&sc->sc_fontcookie, &sc->sc_font); + if (error) { + aprint_error_dev(sc->sc_dev, "no font\n"); + goto out; + } + ri->ri_depth = 8; + ri->ri_font = sc->sc_font; + ri->ri_width = sc->sc_p->p_width; + ri->ri_height = sc->sc_p->p_height; + ri->ri_stride = ri->ri_width * ri->ri_depth / 8; + ri->ri_hw = sc; + ri->ri_flg = RI_FULLCLEAR; + sc->sc_ri_bits_len = round_page(ri->ri_stride * ri->ri_height); + ri->ri_bits = (u_char *)uvm_km_alloc(kernel_map, sc->sc_ri_bits_len, + 0, UVM_KMF_WIRED); + if (ri->ri_bits == NULL) + goto out; + + error = rasops_init(ri, + sc->sc_p->p_height / sc->sc_font->fontheight, + sc->sc_p->p_width / sc->sc_font->fontwidth); + if (error) + goto out; + + ri->ri_caps &= ~WSSCREEN_WSCOLORS; + + /* + * Save original emul ops & insert our damage notification hooks. + */ + sc->sc_orig_riops = ri->ri_ops; + ri->ri_ops.putchar = ssdfb_putchar; + ri->ri_ops.copycols = ssdfb_copycols; + ri->ri_ops.erasecols = ssdfb_erasecols; + ri->ri_ops.copyrows = ssdfb_copyrows; + ri->ri_ops.eraserows = ssdfb_eraserows; + ri->ri_ops.cursor = ssdfb_cursor; + + /* + * Set up the screen. + */ + sc->sc_screen_descr = (struct wsscreen_descr){ + .name = "default", + .ncols = ri->ri_cols, + .nrows = ri->ri_rows, + .textops = &ri->ri_ops, + .fontwidth = ri->ri_font->fontwidth, + .fontheight = ri->ri_font->fontheight, + .capabilities = ri->ri_caps + }; + sc->sc_screens[0] = &sc->sc_screen_descr; + sc->sc_screenlist = (struct wsscreen_list){ + .nscreens = 1, + .screens = sc->sc_screens + }; + + /* + * Initialize hardware. + */ + error = ssdfb_init(sc); + if (error) + goto out; + + if (sc->sc_is_console) + ssdfb_set_usepoll(sc, true); + + mutex_init(&sc->sc_cond_mtx, MUTEX_DEFAULT, IPL_SCHED); + cv_init(&sc->sc_cond, "ssdfb"); + error = kthread_create(PRI_SOFTCLOCK, KTHREAD_MPSAFE | KTHREAD_MUSTJOIN, + NULL, ssdfb_thread, sc, &sc->sc_thread, "%s", + device_xname(sc->sc_dev)); + if (error) { + cv_destroy(&sc->sc_cond); + mutex_destroy(&sc->sc_cond_mtx); + goto out; + } + + /* + * Attach wsdisplay. + */ + if (sc->sc_is_console) { + (*ri->ri_ops.allocattr)(ri, 0, 0, 0, &defattr); + wsdisplay_cnattach(&sc->sc_screen_descr, ri, 0, 0, defattr); +#if defined(DDB) + db_trap_callback = ssdfb_ddb_trap_callback; +#endif + } + aa = (struct wsemuldisplaydev_attach_args){ + .console = sc->sc_is_console, + .scrdata = &sc->sc_screenlist, + .accessops = &ssdfb_accessops, + .accesscookie = sc + }; + sc->sc_wsdisplay = + config_found(sc->sc_dev, &aa, wsemuldisplaydevprint); + + return; +out: + aprint_error_dev(sc->sc_dev, "attach failed: %d\n", error); + if (sc->sc_gddram != NULL) + kmem_free(sc->sc_gddram, sc->sc_gddram_len); + if (ri->ri_bits != NULL) + uvm_km_free(kernel_map, (vaddr_t)ri->ri_bits, sc->sc_ri_bits_len, + UVM_KMF_WIRED); + if (sc->sc_fontcookie > 0) + (void) wsfont_unlock(sc->sc_fontcookie); +} + +int +ssdfb_detach(struct ssdfb_softc *sc) +{ + mutex_enter(&sc->sc_cond_mtx); + sc->sc_detaching = true; + cv_broadcast(&sc->sc_cond); + mutex_exit(&sc->sc_cond_mtx); + kthread_join(sc->sc_thread); + + config_detach(sc->sc_wsdisplay, DETACH_FORCE); + + cv_destroy(&sc->sc_cond); + mutex_destroy(&sc->sc_cond_mtx); + uvm_km_free(kernel_map, (vaddr_t)sc->sc_ri.ri_bits, sc->sc_ri_bits_len, + UVM_KMF_WIRED); + kmem_free(sc->sc_gddram, sc->sc_gddram_len); + (void) wsfont_unlock(sc->sc_fontcookie); + return 0; +} + +static int +ssdfb_ioctl(void *v, void *vs, u_long cmd, void *data, int flag, struct lwp *l) +{ + struct ssdfb_softc *sc = v; + struct wsdisplay_param *wdp; + struct wsdisplay_cmap *wc; + u_char cmap[] = {0, 0xff}; + int error; + + switch (cmd) { + case WSDISPLAYIO_GTYPE: + *(u_int *)data = WSDISPLAY_TYPE_SSDFB; + return 0; + case WSDISPLAYIO_GINFO: + *(struct wsdisplay_fbinfo *)data = (struct wsdisplay_fbinfo){ + .width = sc->sc_ri.ri_width, + .height = sc->sc_ri.ri_height, + .depth = sc->sc_ri.ri_depth, + .cmsize = 2 + }; + return 0; + case WSDISPLAYIO_GET_FBINFO: + return wsdisplayio_get_fbinfo(&sc->sc_ri, + (struct wsdisplayio_fbinfo *)data); + case WSDISPLAYIO_LINEBYTES: + *(u_int *)data = sc->sc_ri.ri_stride; + return 0; + case WSDISPLAYIO_GETPARAM: + wdp = (struct wsdisplay_param *)data; + if (wdp->param != WSDISPLAYIO_PARAM_CONTRAST) + return EINVAL; + wdp->min = 0; + wdp->max = 0xff; + wdp->curval = sc->sc_contrast; + return 0; + case WSDISPLAYIO_SETPARAM: + wdp = (struct wsdisplay_param *)data; + if (wdp->param != WSDISPLAYIO_PARAM_CONTRAST) + return EINVAL; + if (wdp->curval < 0 || wdp->curval > 0xff) + return EINVAL; + return ssdfb_set_contrast(sc, wdp->curval, sc->sc_usepoll); + case WSDISPLAYIO_GMODE: + *(u_int *)data = sc->sc_mode; + return 0; + case WSDISPLAYIO_SMODE: + return ssdfb_set_mode(sc, *(u_int *)data); + case WSDISPLAYIO_GVIDEO: + *(u_int *)data = sc->sc_display_on + ? WSDISPLAYIO_VIDEO_ON + : WSDISPLAYIO_VIDEO_OFF; + return 0; + case WSDISPLAYIO_SVIDEO: + switch (*(u_int *)data) { + case WSDISPLAYIO_VIDEO_ON: + case WSDISPLAYIO_VIDEO_OFF: + break; + default: + return EINVAL; + } + return ssdfb_set_display_on(sc, + *(u_int *)data == WSDISPLAYIO_VIDEO_ON ? true : false, + sc->sc_usepoll); +#if 0 /* don't let userland mess with polling yet */ + case WSDISPLAYIO_SET_POLLING: + switch (*(u_int *)data) { + case 0: + case 1: + break; + default: + return EINVAL; + } + mutex_enter(&sc->sc_cond_mtx); + ssdfb_set_usepoll(sc, *(u_int *)data ? true : false); + cv_broadcast(&sc->sc_cond); + mutex_exit(&sc->sc_cond_mtx); + return 0; +#endif + case WSDISPLAYIO_GETCMAP: + wc = (struct wsdisplay_cmap *)data; + if (wc->index >= __arraycount(cmap) || + wc->count >= __arraycount(cmap) - wc->index) + return EINVAL; + error = copyout(&cmap[wc->index], wc->red, wc->count); + if (error) + return error; + error = copyout(&cmap[wc->index], wc->green, wc->count); + if (error) + return error; + error = copyout(&cmap[wc->index], wc->blue, wc->count); + return error; + case WSDISPLAYIO_PUTCMAP: + return ENODEV; + } + + return EPASSTHROUGH; +} + +static paddr_t +ssdfb_mmap(void *v, void *vs, off_t off, int prot) +{ + struct ssdfb_softc *sc = (struct ssdfb_softc *)v; + struct rasops_info *ri = &sc->sc_ri; + vaddr_t va_base = (vaddr_t)ri->ri_bits; + paddr_t pa; + + if (off < 0 || off >= sc->sc_ri_bits_len || (off & PAGE_MASK) != 0) + return -1; + + if (!pmap_extract(pmap_kernel(), va_base + off, &pa)) + return -1; + + return atop(pa); +} + +static int +ssdfb_alloc_screen(void *v, const struct wsscreen_descr *descr, void **cookiep, + int *curxp, int *curyp, long *attrp) +{ + struct ssdfb_softc *sc = v; + struct rasops_info *ri = &sc->sc_ri; + + if (sc->sc_nscreens > 0) + return ENOMEM; + + ri->ri_ops.allocattr(ri, 0, 0, 0, attrp); + *cookiep = &sc->sc_ri; + *curxp = 0; + *curyp = 0; + sc->sc_nscreens++; + + return 0; +} + +static void +ssdfb_free_screen(void *v, void *cookie) +{ + struct ssdfb_softc *sc = v; + + if (sc->sc_is_console) + panic("ssdfb_free_screen: is console"); + + sc->sc_nscreens--; +} + +static int +ssdfb_show_screen(void *v, void *cookie, int waitok, + void (*cb) (void *, int, int), void *cb_arg) +{ + return 0; +} + +static void +ssdfb_putchar(void *cookie, int row, int col, u_int c, long attr) +{ + struct rasops_info *ri = (struct rasops_info *)cookie; + struct ssdfb_softc *sc = ri->ri_hw; + + sc->sc_orig_riops.putchar(cookie, row, col, c, attr); + ssdfb_damage(sc); +} + +static void +ssdfb_copycols(void *cookie, int row, int srccol, int dstcol, int ncols) +{ + struct rasops_info *ri = (struct rasops_info *)cookie; + struct ssdfb_softc *sc = ri->ri_hw; + + sc->sc_orig_riops.copycols(cookie, row, srccol, dstcol, ncols); + ssdfb_damage(sc); +} + +static void +ssdfb_erasecols(void *cookie, int row, int startcol, int ncols, long fillattr) +{ + struct rasops_info *ri = (struct rasops_info *)cookie; + struct ssdfb_softc *sc = ri->ri_hw; + + sc->sc_orig_riops.erasecols(cookie, row, startcol, ncols, fillattr); + ssdfb_damage(sc); +} + +static void +ssdfb_copyrows(void *cookie, int srcrow, int dstrow, int nrows) +{ + struct rasops_info *ri = (struct rasops_info *)cookie; + struct ssdfb_softc *sc = ri->ri_hw; + + sc->sc_orig_riops.copyrows(cookie, srcrow, dstrow, nrows); + ssdfb_damage(sc); +} + +static void +ssdfb_eraserows(void *cookie, int row, int nrows, long fillattr) +{ + struct rasops_info *ri = (struct rasops_info *)cookie; + struct ssdfb_softc *sc = ri->ri_hw; + + sc->sc_orig_riops.eraserows(cookie, row, nrows, fillattr); + ssdfb_damage(sc); +} + +static void +ssdfb_cursor(void *cookie, int on, int row, int col) +{ + struct rasops_info *ri = (struct rasops_info *)cookie; + struct ssdfb_softc *sc = ri->ri_hw; + + sc->sc_orig_riops.cursor(cookie, on, row, col); + ssdfb_damage(sc); +} + +static int +ssdfb_init(struct ssdfb_softc *sc) +{ + int error; + uint8_t cmd[3]; + bool usepoll = true; + + /* + * Enter sleep. + */ + SSDFB_CMD1(SSDFB_CMD_SET_DISPLAY_OFF); + if (error) + return error; + SSDFB_CMD1(SSDFB_CMD_DEACTIVATE_SCROLL); + if (error) + return error; + SSDFB_CMD1(SSDFB_CMD_ENTIRE_DISPLAY_OFF); + if (error) + return error; + + /* + * Configure physical display panel layout. + */ + SSDFB_CMD2(SSDFB_CMD_SET_MULTIPLEX_RATIO, sc->sc_p->p_multiplex_ratio); + if (error) + return error; + SSDFB_CMD2(SSDFB_CMD_SET_DISPLAY_OFFSET, 0); + if (error) + return error; + SSDFB_CMD1(SSDFB_CMD_SET_DISPLAY_START_LINE_BASE + 0x00); + if (error) + return error; + SSDFB_CMD2(SSDFB_CMD_SET_COM_PINS_HARDWARE_CFG, sc->sc_p->p_compin_cfg); + if (error) + return error; + if (sc->sc_upsidedown) { + SSDFB_CMD1(SSDFB_CMD_SET_SEGMENT_REMAP_REVERSE); + if (error) + return error; + SSDFB_CMD1(SSDFB_CMD_SET_COM_OUTPUT_DIRECTION_REMAP); + if (error) + return error; + } else { + SSDFB_CMD1(SSDFB_CMD_SET_SEGMENT_REMAP_NORMAL); + if (error) + return error; + SSDFB_CMD1(SSDFB_CMD_SET_COM_OUTPUT_DIRECTION_NORMAL); + if (error) + return error; + } + SSDFB_CMD1(SSDFB_CMD_SET_NORMAL_DISPLAY + (uint8_t)sc->sc_inverse); + if (error) + return error; + + /* + * Configure timing characteristics. + */ + SSDFB_CMD2(SSDFB_CMD_SET_DISPLAY_CLOCK_RATIO, + ((sc->sc_p->p_fosc << SSDFB_DISPLAY_CLOCK_OSCILLATOR_SHIFT) & + SSDFB_DISPLAY_CLOCK_OSCILLATOR_MASK) | + ((sc->sc_p->p_fosc_div << SSDFB_DISPLAY_CLOCK_DIVIDER_SHIFT) & + SSDFB_DISPLAY_CLOCK_DIVIDER_MASK)); + if (error) + return error; + SSDFB_CMD2(SSDFB_CMD_SET_CONTRAST_CONTROL, sc->sc_contrast); + if (error) + return error; + SSDFB_CMD2(SSDFB_CMD_SET_PRECHARGE_PERIOD, + ((sc->sc_p->p_precharge << SSDFB_PRECHARGE_SHIFT) & + SSDFB_PRECHARGE_MASK) | + ((sc->sc_p->p_discharge << SSDFB_DISCHARGE_SHIFT) & + SSDFB_DISCHARGE_MASK)); + if (error) + return error; + SSDFB_CMD2(SSDFB_CMD_SET_VCOMH_DESELECT_LEVEL, + sc->sc_p->p_vcomh_deselect_level); + if (error) + return error; + + /* + * Start charge pump. + */ + SSDFB_CMD2(sc->sc_p->p_chargepump_cmd, sc->sc_p->p_chargepump_arg); + if (error) + return error; + + if (sc->sc_p->p_controller_id == SSDFB_CONTROLLER_SH1106) { + SSDFB_CMD2(SH1106_CMD_SET_DC_DC, SH1106_DC_DC_ON); + if (error) + return error; + } + + ssdfb_clear_screen(sc); + error = ssdfb_sync(sc, usepoll); + if (error) + return error; + error = ssdfb_set_display_on(sc, true, usepoll); + + return error; +} + +static int +ssdfb_set_contrast(struct ssdfb_softc *sc, uint8_t value, bool usepoll) +{ + uint8_t cmd[2]; + int error; + + sc->sc_contrast = value; + SSDFB_CMD2(SSDFB_CMD_SET_CONTRAST_CONTROL, value); + + return error; +} + +static int +ssdfb_set_display_on(struct ssdfb_softc *sc, bool value, bool usepoll) +{ + uint8_t cmd[1]; + int error; + sc->sc_display_on = value; + + SSDFB_CMD1(value ? SSDFB_CMD_SET_DISPLAY_ON : SSDFB_CMD_SET_DISPLAY_OFF); + + return error; +} + +static int +ssdfb_set_mode(struct ssdfb_softc *sc, u_int mode) +{ + switch (mode) { + case WSDISPLAYIO_MODE_EMUL: + case WSDISPLAYIO_MODE_DUMBFB: + break; + default: + return EINVAL; + } + if (mode == sc->sc_mode) + return 0; + mutex_enter(&sc->sc_cond_mtx); + sc->sc_mode = mode; + cv_broadcast(&sc->sc_cond); + mutex_exit(&sc->sc_cond_mtx); + ssdfb_clear_screen(sc); + ssdfb_damage(sc); + + return 0; +} + +static void +ssdfb_damage(struct ssdfb_softc *sc) +{ + int s; + + if (sc->sc_usepoll) { + (void) ssdfb_sync(sc, true); + } else { + /* + * kernel code isn't permitted to call us via kprintf at + * splhigh. In case misbehaving code calls us anyway we can't + * safely take the mutex so we skip the damage notification. + */ + if (sc->sc_is_console) { + s = splhigh(); + splx(s); + if (s == IPL_HIGH) + return; + } + mutex_enter(&sc->sc_cond_mtx); + sc->sc_modified = true; + cv_broadcast(&sc->sc_cond); + mutex_exit(&sc->sc_cond_mtx); + } +} + +static bool +ssdfb_is_modified(struct ssdfb_softc *sc) +{ + vaddr_t va, va_end; + + if (sc->sc_mode == WSDISPLAYIO_MODE_EMUL) + return sc->sc_modified; + + va = (vaddr_t)sc->sc_ri.ri_bits; + va_end = va + sc->sc_ri_bits_len; + while (va < va_end) { + if (pmap_is_modified(uvm_pageratop(va))) + return true; + va += PAGE_SIZE; + } + + return false; +} + +static bool +ssdfb_clear_modify(struct ssdfb_softc *sc) +{ + vaddr_t va, va_end; + bool ret; + + if (sc->sc_mode == WSDISPLAYIO_MODE_EMUL) { + ret = sc->sc_modified; + sc->sc_modified = false; + return ret; + } + + va = (vaddr_t)sc->sc_ri.ri_bits; + va_end = va + sc->sc_ri_bits_len; + ret = false; + while (va < va_end) { + if (pmap_clear_modify(uvm_pageratop(va))) + ret = true; + va += PAGE_SIZE; + } + + return ret; +} + +static void +ssdfb_thread(void *arg) { + struct ssdfb_softc *sc = (struct ssdfb_softc *)arg; + int error; + + mutex_enter(&sc->sc_cond_mtx); + + if (sc->sc_usepoll) + ssdfb_set_usepoll(sc, false); + + while(!sc->sc_detaching) { + if (!ssdfb_is_modified(sc)) { + if (cv_timedwait(&sc->sc_cond, &sc->sc_cond_mtx, + sc->sc_mode == WSDISPLAYIO_MODE_EMUL + ? 0 : sc->sc_backoff) == EWOULDBLOCK + && sc->sc_backoff < mstohz(200)) { + sc->sc_backoff <<= 1; + } + continue; + } + sc->sc_backoff = 1; + (void) ssdfb_clear_modify(sc); + if (sc->sc_usepoll) + continue; + mutex_exit(&sc->sc_cond_mtx); + error = ssdfb_sync(sc, false); + if (error) + device_printf(sc->sc_dev, "ssdfb_sync: error %d\n", + error); + mutex_enter(&sc->sc_cond_mtx); + } + + mutex_exit(&sc->sc_cond_mtx); +} + +static void +ssdfb_set_usepoll(struct ssdfb_softc *sc, bool enable) { + sc->sc_usepoll = enable; +} + +static int +ssdfb_sync(struct ssdfb_softc *sc, bool usepoll) { + struct rasops_info *ri = &sc->sc_ri; + int block_size = 8; + int ri_block_stride = ri->ri_stride * block_size; + int height_in_blocks = sc->sc_p->p_height / block_size; + int width_in_blocks = sc->sc_p->p_width / block_size; + int ri_block_step = block_size * ri->ri_depth / 8; + int x, y; + union ssdfb_block *blockp; + uint64_t raw_block; + uint8_t *src; + int x1, x2, y1, y2; + + /* + * Transfer rasops bitmap into gddram shadow buffer while keeping track + * of the bounding box of the dirty region we scribbled over. + */ + x1 = width_in_blocks; + x2 = -1; + y1 = height_in_blocks; + y2 = -1; + for (y = 0; y < height_in_blocks; y++) { + src = &ri->ri_bits[y*ri_block_stride]; + blockp = &sc->sc_gddram[y * width_in_blocks]; + for (x = 0; x < width_in_blocks; x++) { + raw_block = (ri->ri_depth == 1) + ? ssdfb_transpose_block_1bpp(src, ri->ri_stride) + : ssdfb_transpose_block_8bpp(src, ri->ri_stride); + if (raw_block != blockp->raw) { + blockp->raw = raw_block; + if (x1 > x) + x1 = x; + if (x2 < x) + x2 = x; + if (y1 > y) + y1 = y; + if (y2 < y) + y2 = y; + } + src += ri_block_step; + blockp++; + } + } + if (x2 != -1) + return sc->sc_transfer_rect(sc->sc_cookie, + x1 * block_size + sc->sc_p->p_panel_shift, + (x2 + 1) * block_size - 1 + sc->sc_p->p_panel_shift, + y1, + y2, + &sc->sc_gddram[y1 * width_in_blocks + x1].col[0], + sc->sc_p->p_width, + usepoll); + + return 0; +} + +static uint64_t +ssdfb_transpose_block_1bpp(uint8_t *src, size_t src_stride) +{ + uint64_t x = 0; + uint64_t t; + int i; + + /* + * collect the 8x8 block. + */ + for (i = 0; i < 8; i++) { + x >>= 8; + x |= (uint64_t)src[i * src_stride] << 56; + } + + /* + * Transpose it into gddram layout. + * Post-transpose bswap is the same as pre-transpose bit order reversal. + * We do this to match rasops1 bit order. + */ + t = (x ^ (x >> 28)) & 0x00000000F0F0F0F0ULL; + x = x ^ t ^ (t << 28); + t = (x ^ (x >> 14)) & 0x0000CCCC0000CCCCULL; + x = x ^ t ^ (t << 14); + t = (x ^ (x >> 7)) & 0x00AA00AA00AA00AAULL; + x = x ^ t ^ (t << 7); + x = bswap64(x); + + return htole64(x); +} + +static uint64_t +ssdfb_transpose_block_8bpp(uint8_t *src, size_t src_stride) +{ + uint64_t x = 0; + int m, n; + + for (m = 0; m < 8; m++) { + for (n = 0; n < 8; n++) { + x >>= 1; + x |= src[n * src_stride + m] ? (1ULL << 63) : 0; + } + } + + return htole64(x); +} + +static const struct ssdfb_product * +ssdfb_lookup_product(ssdfb_product_id_t id) { + int i; + + for (i = 0; i < __arraycount(ssdfb_products); i++) { + if (ssdfb_products[i].p_product_id == id) + return &ssdfb_products[i]; + } + + return NULL; +} + +static int +ssdfb_pick_font(int *cookiep, struct wsdisplay_font **fontp) { + int error; + int c; + struct wsdisplay_font *f; + int i; + uint8_t d[4][2] = {{5, 8}, {8, 8}, {8, 10} ,{8, 16}}; + + /* + * Try to find fonts in order of inreasing size. + */ + wsfont_init(); + for(i = 0; i < __arraycount(d); i++) { + c = wsfont_find(NULL, d[i][0], d[i][1], 0, + WSDISPLAY_FONTORDER_L2R, WSDISPLAY_FONTORDER_L2R, + WSFONT_FIND_BITMAP); + if (c > 0) + break; + } + if (c <= 0) + return ENOENT; + error = wsfont_lock(c, &f); + if (error) + return error; + *cookiep = c; + *fontp = f; + + return 0; +} + +static void +ssdfb_clear_screen(struct ssdfb_softc *sc) +{ + struct rasops_info *ri = &sc->sc_ri; + + memset(sc->sc_gddram, 0xff, sc->sc_gddram_len); + memset(ri->ri_bits, 0, sc->sc_ri_bits_len); +} + +#if defined(DDB) +static void +ssdfb_ddb_trap_callback(int enable) +{ + extern struct cfdriver ssdfb_cd; + struct ssdfb_softc *sc; + int i; + + for (i = 0; i < ssdfb_cd.cd_ndevs; i++) { + sc = device_lookup_private(&ssdfb_cd, i); + if (sc != NULL && sc->sc_is_console) { + ssdfb_set_usepoll(sc, (bool)enable); + } + } +} +#endif diff --git a/sys/dev/ic/ssdfbvar.h b/sys/dev/ic/ssdfbvar.h new file mode 100644 index 00000000000..22063a60cbf --- /dev/null +++ b/sys/dev/ic/ssdfbvar.h @@ -0,0 +1,237 @@ +/* $NetBSD: ssdfbvar.h,v 1.1 2019/03/17 00:57:15 tnn Exp $ */ + +/* + * Copyright (c) 2019 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is derived from software contributed to The NetBSD Foundation + * by Tobias Nygren. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * cfdata attachment flags + */ +#define SSDFB_ATTACH_FLAG_PRODUCT_MASK 0x000000ff +#define SSDFB_ATTACH_FLAG_UPSIDEDOWN 0x00000100 +#define SSDFB_ATTACH_FLAG_INVERSE 0x00000200 +#define SSDFB_ATTACH_FLAG_CONSOLE 0x00000400 + +/* + * Fundamental commands + * SSD1306 Rev 1.1 p.28 + * SH1106 Rev 0.1 p.19,20,22 + */ +#define SSDFB_CMD_SET_CONTRAST_CONTROL 0x81 +#define SSDFB_CMD_ENTIRE_DISPLAY_OFF 0xa4 +#define SSDFB_CMD_ENTIRE_DISPLAY_ON 0xa5 +#define SSDFB_CMD_SET_NORMAL_DISPLAY 0xa6 +#define SSDFB_CMD_SET_INVERSE_DISPLAY 0xa7 +#define SSDFB_CMD_SET_DISPLAY_OFF 0xae +#define SSDFB_CMD_SET_DISPLAY_ON 0xaf + +/* + * Scrolling commands; SSD1306 Rev 1.1 p. 28 + */ +#define SSDFB_CMD_VERTICAL_AND_RIGHT_SCROLL 0x29 +#define SSDFB_CMD_VERTICAL_AND_LEFT_SCROLL 0x2a +#define SSDFB_CMD_DEACTIVATE_SCROLL 0x2e +#define SSDFB_CMD_ACTIVATE_SCROLL 0x2f +#define SSDFB_CMD_SET_VERTICAL_SCROLL_AREA 0xa3 + +/* + * Addressing commands + * SSD1306 Rev 1.1 p.30 + * SH1106 Rev 0.1 p.18,22 + */ +#define SSDFB_CMD_SET_LOWER_COLUMN_START_ADDRESS_BASE 0x00 +#define SSDFB_CMD_SET_LOWER_COLUMN_START_ADDRESS_MAX 0x0f +#define SSDFB_CMD_SET_HIGHER_COLUMN_START_ADDRESS_BASE 0x10 +#define SSDFB_CMD_SET_HIGHER_COLUMN_START_ADDRESS_MAX 0x1f +#define SSD1306_CMD_SET_MEMORY_ADDRESSING_MODE 0x20 + #define SSD1306_MEMORY_ADDRESSING_MODE_HORIZONTAL 0x00 + #define SSD1306_MEMORY_ADDRESSING_MODE_VERTICAL 0x01 + #define SSD1306_MEMORY_ADDRESSING_MODE_PAGE 0x02 +#define SSD1306_CMD_SET_COLUMN_ADDRESS 0x21 +#define SSD1306_CMD_SET_PAGE_ADDRESS 0x22 +#define SSDFB_CMD_SET_PAGE_START_ADDRESS_BASE 0xb0 +#define SSDFB_CMD_SET_PAGE_START_ADDRESS_MAX 0xb7 + +/* + * Resolution & hardware layout commands + * SSD1306 Rev 1.1 p.31 + * SH1106 Rev 0.1 p.19,20,21,23 + */ +#define SSDFB_CMD_SET_DISPLAY_START_LINE_BASE 0x40 +#define SSDFB_CMD_SET_DISPLAY_START_LINE_MAX 0x7f +#define SSDFB_CMD_SET_SEGMENT_REMAP_NORMAL 0xa0 +#define SSDFB_CMD_SET_SEGMENT_REMAP_REVERSE 0xa1 +#define SSDFB_CMD_SET_MULTIPLEX_RATIO 0xa8 +#define SSDFB_CMD_SET_COM_OUTPUT_DIRECTION_NORMAL 0xc0 +#define SSDFB_CMD_SET_COM_OUTPUT_DIRECTION_REMAP 0xc8 +#define SSDFB_CMD_SET_DISPLAY_OFFSET 0xd3 +#define SSDFB_CMD_SET_COM_PINS_HARDWARE_CFG 0xda + #define SSDFB_COM_PINS_A1_MASK 0x02 + #define SSDFB_COM_PINS_ALTERNATIVE_MASK 0x10 + #define SSDFB_COM_PINS_REMAP_MASK 0x20 + +/* + * Timing & driving commands + * SSD1306 Rev 1.1 p.32 + * SH1106 Rev 0.1 p.24,25,26 + */ +#define SSDFB_CMD_SET_DISPLAY_CLOCK_RATIO 0xd5 + #define SSDFB_DISPLAY_CLOCK_DIVIDER_MASK 0x0f + #define SSDFB_DISPLAY_CLOCK_DIVIDER_SHIFT 0 + #define SSDFB_DISPLAY_CLOCK_OSCILLATOR_MASK 0xf0 + #define SSDFB_DISPLAY_CLOCK_OSCILLATOR_SHIFT 4 +#define SSDFB_CMD_SET_PRECHARGE_PERIOD 0xd9 + #define SSDFB_PRECHARGE_MASK 0x0f + #define SSDFB_PRECHARGE_SHIFT 0 + #define SSDFB_DISCHARGE_MASK 0xf0 + #define SSDFB_DISCHARGE_SHIFT 4 +#define SSDFB_CMD_SET_VCOMH_DESELECT_LEVEL 0xdb + #define SSD1306_VCOMH_DESELECT_LEVEL_0_65_VCC 0x00 + #define SSD1306_VCOMH_DESELECT_LEVEL_0_77_VCC 0x20 + #define SSD1306_VCOMH_DESELECT_LEVEL_0_83_VCC 0x30 + #define SH1106_VCOMH_DESELECT_LEVEL_DEFAULT 0x35 + +/* + * Misc commands + * SSD1306 Rev 1.1 p.32 + * SH1106 Rev 0.1 p.27,28 + */ +#define SSDFB_CMD_NOP 0xe3 +#define SH1106_CMD_READ_MODIFY_WRITE 0xe0 +#define SH1106_CMD_READ_MODIFY_WRITE_CANCEL 0xee + +/* + * Charge pump commands + * SSD1306 App Note Rev 0.4 p.3 + * SH1106 V0.1 p.18 + */ +#define SSD1306_CMD_SET_CHARGE_PUMP 0x8d + #define SSD1306_CHARGE_PUMP_ENABLE 0x14 + #define SSD1306_CHARGE_PUMP_DISABE 0x10 +#define SH1106_CMD_SET_CHARGE_PUMP_7V4 0x30 +#define SH1106_CMD_SET_CHARGE_PUMP_8V0 0x31 +#define SH1106_CMD_SET_CHARGE_PUMP_8V4 0x32 +#define SH1106_CMD_SET_CHARGE_PUMP_9V0 0x33 + +/* + * DC-DC commands + * SH1106 V0.1 p.18 + */ +#define SH1106_CMD_SET_DC_DC 0xad + #define SH1106_DC_DC_OFF 0x8a + #define SH1106_DC_DC_ON 0x8b + +typedef enum { + SSDFB_CONTROLLER_UNKNOWN=0, + SSDFB_CONTROLLER_SSD1306=1, + SSDFB_CONTROLLER_SH1106=2, +} ssdfb_controller_id_t; + +typedef enum { + SSDFB_PRODUCT_UNKNOWN=0, + SSDFB_PRODUCT_SSD1306_GENERIC=1, + SSDFB_PRODUCT_SH1106_GENERIC=2, + SSDFB_PRODUCT_ADAFRUIT_931=3, + SSDFB_PRODUCT_ADAFRUIT_938=4, +} ssdfb_product_id_t; + +#define SSDFB_I2C_DEFAULT_ADDR 0x3c +#define SSDFB_I2C_ALTERNATIVE_ADDR 0x3d + +/* Co bit has different behaviour in SSD1306 and SH1106 */ +#define SSDFB_I2C_CTRL_BYTE_CONTINUATION_MASK __BIT(7) +#define SSDFB_I2C_CTRL_BYTE_DATA_MASK __BIT(6) + +union ssdfb_block { + uint8_t col[8]; + uint64_t raw; +}; + +struct ssdfb_product { + ssdfb_product_id_t p_product_id; + ssdfb_controller_id_t p_controller_id; + const char *p_name; + int p_width; + int p_height; + int p_panel_shift; + uint8_t p_fosc; + uint8_t p_fosc_div; + uint8_t p_precharge; + uint8_t p_discharge; + uint8_t p_compin_cfg; + uint8_t p_vcomh_deselect_level; + uint8_t p_default_contrast; + uint8_t p_multiplex_ratio; + uint8_t p_chargepump_cmd; + uint8_t p_chargepump_arg; +}; + +struct ssdfb_softc { + device_t sc_dev; + const struct ssdfb_product *sc_p; + + /* wscons & rasops state */ + u_int sc_mode; + int sc_fontcookie; + struct wsdisplay_font *sc_font; + struct wsscreen_descr sc_screen_descr; + const struct wsscreen_descr *sc_screens[1]; + struct wsscreen_list sc_screenlist; + struct rasops_info sc_ri; + size_t sc_ri_bits_len; + struct wsdisplay_emulops sc_orig_riops; + int sc_nscreens; + device_t sc_wsdisplay; + bool sc_is_console; + bool sc_usepoll; + + /* hardware state */ + bool sc_upsidedown; + bool sc_inverse; + uint8_t sc_contrast; + bool sc_display_on; + union ssdfb_block *sc_gddram; + size_t sc_gddram_len; + + /* damage tracking */ + lwp_t *sc_thread; + kcondvar_t sc_cond; + kmutex_t sc_cond_mtx; + bool sc_detaching; + int sc_backoff; + bool sc_modified; + + /* reference to bus-specific code */ + void *sc_cookie; + int (*sc_cmd)(void *, uint8_t *, size_t, bool); + int (*sc_transfer_rect)(void *, uint8_t, uint8_t, uint8_t, uint8_t, + uint8_t *, size_t, bool); +}; + +void ssdfb_attach(struct ssdfb_softc *, int flags); +int ssdfb_detach(struct ssdfb_softc *); |
