Skip to main content

embassy_ssd1306/
lib.rs

1// Copyright (C) 2026 Jorge Andre Castro
2// GPL-2.0-or-later
3
4#![no_std]
5#![forbid(unsafe_code)]
6
7//! # embassy-ssd1306
8//!
9//! Driver asynchrone `no_std` pour l'écran OLED SSD1306 128x64 via I2C.
10//! Permet d'afficher des nombres et du texte ASCII (A–Z, 0–9) sur les pages 0 à 7 ainsi que le point, 
11//! les parenthèses, la virgule, les crochets, le pourcentage, les signes < > = ? ! : . + /  | _
12//! Ce pilote fournit un framebuffer en RAM avec des primitives graphiques
13//! (pixels, lignes, rectangles, bitmaps, texte numérique) et un flush I2C
14//! optimisé page par page.
15//!
16//! # Exemple
17//!
18//! ```rust,no_run
19//! use embassy_ssd1306::Ssd1306;
20//!
21//! let mut oled = Ssd1306::new(i2c, 0x3C);
22//! oled.init().await.unwrap();
23//!
24//! oled.draw_rect(0, 0, 128, 64, true);
25//! oled.draw_i16(0, 0, -1234);
26//! oled.flush().await.unwrap();
27//! ```
28
29use embassy_time::Timer;
30use embedded_hal_async::i2c::I2c;
31
32/// Largeur de l'écran en pixels.
33pub const SCREEN_WIDTH: usize = 128;
34
35/// Hauteur de l'écran en pixels.
36pub const SCREEN_HEIGHT: usize = 64;
37
38/// Nombre de pages (1 page = 8 pixels de hauteur).
39pub const PAGES: usize = SCREEN_HEIGHT / 8;
40
41/// Font 5x7 — chiffres 0-9, signe moins, espace, lettres A-Z, caractères spéciaux et symboles opérateurs.
42/// Support complet : 0-9, A-Z, -, espace, ., (, ), ,, [, ], %, <, >, =, ?, {, }, !, :, +, /, |, _
43const FONT: [[u8; 5]; 55] = [
44    [0x3E, 0x51, 0x49, 0x45, 0x3E], // 0
45    [0x00, 0x42, 0x7F, 0x40, 0x00], // 1
46    [0x42, 0x61, 0x51, 0x49, 0x46], // 2
47    [0x21, 0x41, 0x45, 0x4B, 0x31], // 3
48    [0x18, 0x14, 0x12, 0x7F, 0x10], // 4
49    [0x27, 0x45, 0x45, 0x45, 0x39], // 5
50    [0x3C, 0x4A, 0x49, 0x49, 0x30], // 6
51    [0x01, 0x71, 0x09, 0x05, 0x03], // 7
52    [0x36, 0x49, 0x49, 0x49, 0x36], // 8
53    [0x06, 0x49, 0x49, 0x29, 0x1E], // 9
54    [0x08, 0x08, 0x08, 0x08, 0x08], // 10 = '-'
55    [0x00, 0x00, 0x00, 0x00, 0x00], // 11 = ' '
56    // Lettres A–Z (index 12–37)
57    [0x7E, 0x11, 0x11, 0x11, 0x7E], // 12 = 'A'
58    [0x7F, 0x49, 0x49, 0x49, 0x36], // 13 = 'B'
59    [0x3E, 0x41, 0x41, 0x41, 0x22], // 14 = 'C'
60    [0x7F, 0x41, 0x41, 0x22, 0x1C], // 15 = 'D'
61    [0x7F, 0x49, 0x49, 0x49, 0x41], // 16 = 'E'
62    [0x7F, 0x09, 0x09, 0x09, 0x01], // 17 = 'F'
63    [0x3E, 0x41, 0x49, 0x49, 0x7A], // 18 = 'G'
64    [0x7F, 0x08, 0x08, 0x08, 0x7F], // 19 = 'H'
65    [0x00, 0x41, 0x7F, 0x41, 0x00], // 20 = 'I'
66    [0x20, 0x40, 0x41, 0x3F, 0x01], // 21 = 'J'
67    [0x7F, 0x08, 0x14, 0x22, 0x41], // 22 = 'K'
68    [0x7F, 0x40, 0x40, 0x40, 0x40], // 23 = 'L'
69    [0x7F, 0x02, 0x0C, 0x02, 0x7F], // 24 = 'M'
70    [0x7F, 0x04, 0x08, 0x10, 0x7F], // 25 = 'N'
71    [0x3E, 0x41, 0x41, 0x41, 0x3E], // 26 = 'O'
72    [0x7F, 0x09, 0x09, 0x09, 0x06], // 27 = 'P'
73    [0x3E, 0x41, 0x51, 0x21, 0x5E], // 28 = 'Q'
74    [0x7F, 0x09, 0x19, 0x29, 0x46], // 29 = 'R'
75    [0x46, 0x49, 0x49, 0x49, 0x31], // 30 = 'S'
76    [0x01, 0x01, 0x7F, 0x01, 0x01], // 31 = 'T'
77    [0x3F, 0x40, 0x40, 0x40, 0x3F], // 32 = 'U'
78    [0x1F, 0x20, 0x40, 0x20, 0x1F], // 33 = 'V'
79    [0x3F, 0x40, 0x38, 0x40, 0x3F], // 34 = 'W'
80    [0x63, 0x14, 0x08, 0x14, 0x63], // 35 = 'X'
81    [0x07, 0x08, 0x70, 0x08, 0x07], // 36 = 'Y'
82    [0x61, 0x51, 0x49, 0x45, 0x43], // 37 = 'Z'
83    [0x00, 0x00, 0x60, 0x60, 0x00], // 38 = '.'
84    [0x00, 0x3E, 0x41, 0x41, 0x00], // 39 = '('
85    [0x00, 0x41, 0x41, 0x3E, 0x00], // 40 = ')'
86    [0x00, 0x40, 0x50, 0x30, 0x00], // 41 = ','
87    [0x00, 0x7F, 0x41, 0x41, 0x00], // 42 = '['
88    [0x00, 0x41, 0x41, 0x7F, 0x00], // 43 = ']'
89    [0x23, 0x13, 0x08, 0x64, 0x62], // 44 = '%'
90    [0x08, 0x14, 0x22, 0x41, 0x00], // 45 = '<'
91    [0x00, 0x41, 0x22, 0x14, 0x08], // 46 = '>'
92    [0x00, 0x24, 0x24, 0x24, 0x00], // 47 = '='
93    [0x02, 0x01, 0x51, 0x09, 0x06], // 48 = '?'
94    [0x00, 0x00, 0x5F, 0x00, 0x00], // 49 = '!'
95    [0x00, 0x36, 0x36, 0x00, 0x00], // 50 = ':'
96    [0x08, 0x08, 0x3E, 0x08, 0x08], // 51 = '+'
97    [0x20, 0x10, 0x08, 0x04, 0x02], // 52 = '/'
98    [0x00, 0x00, 0x7F, 0x00, 0x00], // 53 = '|'
99    [0x40, 0x40, 0x40, 0x40, 0x40], // 54 = '_'
100];
101
102/// Instance principale du driver SSD1306.
103///
104/// Fonctionne avec n'importe quel périphérique implémentant
105/// `embedded-hal-async::i2c::I2c`.
106pub struct Ssd1306<I: I2c> {
107    i2c: I,
108    /// Adresse I2C configurée (0x3C ou 0x3D).
109    pub addr: u8,
110    framebuffer: [u8; SCREEN_WIDTH * PAGES],
111}
112
113impl<I: I2c> Ssd1306<I> {
114    /// Initialise une nouvelle instance du driver.
115    ///
116    /// # Arguments
117    /// * `i2c`  Bus I2C (ou I2cDevice partagé).
118    /// * `addr`  Adresse du composant (généralement 0x3C).
119    pub fn new(i2c: I, addr: u8) -> Self {
120        Self {
121            i2c,
122            addr,
123            framebuffer: [0u8; SCREEN_WIDTH * PAGES],
124        }
125    }
126
127    // Commandes bas niveau 
128
129    async fn cmd(&mut self, c: u8) -> Result<(), I::Error> {
130        self.i2c.write(self.addr, &[0x00, c]).await
131    }
132
133    async fn cmd2(&mut self, c: u8, d: u8) -> Result<(), I::Error> {
134        self.i2c.write(self.addr, &[0x00, c, d]).await
135    }
136
137    async fn cmd3(&mut self, c: u8, d1: u8, d2: u8) -> Result<(), I::Error> {
138        self.i2c.write(self.addr, &[0x00, c, d1, d2]).await
139    }
140
141    // Init
142
143    /// Configure le SSD1306 et efface l'écran.
144    ///
145    /// Doit être appelé une fois avant toute opération d'affichage.
146    pub async fn init(&mut self) -> Result<(), I::Error> {
147        Timer::after_millis(200).await;
148
149        self.cmd(0xAE).await?;           // Display OFF
150        self.cmd2(0xD5, 0x80).await?;    // Osc freq
151        self.cmd2(0xA8, 0x3F).await?;    // Multiplex 64
152        self.cmd2(0xD3, 0x00).await?;    // Display offset 0
153        self.cmd(0x40).await?;           // Start line 0
154        self.cmd2(0x8D, 0x14).await?;    // Charge pump ON
155        self.cmd2(0x20, 0x00).await?;    // Horizontal addressing mode
156        self.cmd(0xA1).await?;           // Segment remap
157        self.cmd(0xC8).await?;           // COM scan dec
158        self.cmd2(0xDA, 0x12).await?;    // COM pins
159        self.cmd2(0x81, 0xCF).await?;    // Contrast
160        self.cmd2(0xD9, 0xF1).await?;    // Pre-charge
161        self.cmd2(0xDB, 0x40).await?;    // VCOMH
162        self.cmd(0xA4).await?;           // RAM display
163        self.cmd(0xA6).await?;           // Normal (non-inversé)
164        self.cmd(0xAF).await?;           // Display ON
165
166        self.clear();
167        self.flush().await
168    }
169
170    // Framebuffer 
171
172    /// Efface le framebuffer (tout noir). Ne flush pas vers l'écran.
173    pub fn clear(&mut self) {
174        self.framebuffer.fill(0x00);
175    }
176
177    /// Remplit le framebuffer (tout blanc). Ne flush pas vers l'écran.
178    pub fn fill(&mut self) {
179        self.framebuffer.fill(0xFF);
180    }
181
182    /// Allume ou éteint un pixel dans le framebuffer.
183    ///
184    /// Les coordonnées hors écran sont ignorées silencieusement.
185    pub fn draw_pixel(&mut self, x: u8, y: u8, on: bool) {
186        if x >= SCREEN_WIDTH as u8 || y >= SCREEN_HEIGHT as u8 {
187            return;
188        }
189        let page = (y / 8) as usize;
190        let bit = y % 8;
191        let idx = page * SCREEN_WIDTH + x as usize;
192        if on {
193            self.framebuffer[idx] |= 1 << bit;
194        } else {
195            self.framebuffer[idx] &= !(1 << bit);
196        }
197    }
198
199    //  Primitives graphiques 
200
201    /// Dessine une ligne horizontale.
202    pub fn draw_hline(&mut self, x: u8, y: u8, w: u8, on: bool) {
203        for i in 0..w {
204            self.draw_pixel(x + i, y, on);
205        }
206    }
207
208    /// Dessine une ligne verticale.
209    pub fn draw_vline(&mut self, x: u8, y: u8, h: u8, on: bool) {
210        for i in 0..h {
211            self.draw_pixel(x, y + i, on);
212        }
213    }
214
215    /// Dessine un rectangle vide.
216    pub fn draw_rect(&mut self, x: u8, y: u8, w: u8, h: u8, on: bool) {
217        self.draw_hline(x,         y,         w, on);
218        self.draw_hline(x,         y + h - 1, w, on);
219        self.draw_vline(x,         y,         h, on);
220        self.draw_vline(x + w - 1, y,         h, on);
221    }
222
223    /// Dessine un rectangle plein.
224    pub fn draw_filled_rect(&mut self, x: u8, y: u8, w: u8, h: u8, on: bool) {
225        for row in 0..h {
226            self.draw_hline(x, y + row, w, on);
227        }
228    }
229
230    /// Dessine un bitmap 1bpp (MSB à gauche).
231    ///
232    /// # Arguments
233    /// * `data`  Chaque byte représente 8 pixels horizontaux consécutifs.
234    pub fn draw_bitmap(&mut self, x: u8, y: u8, w: u8, h: u8, data: &[u8]) {
235        let stride = (w as usize + 7) / 8;
236        for row in 0..h as usize {
237            for col in 0..w as usize {
238                let byte_idx = row * stride + col / 8;
239                let bit = 7 - (col % 8);
240                let on = byte_idx < data.len() && (data[byte_idx] >> bit) & 1 == 1;
241                self.draw_pixel(x + col as u8, y + row as u8, on);
242            }
243        }
244    }
245
246    // Texte 
247
248    /// Dessine un glyphe 5x7 dans le framebuffer à la position (x, page).
249    ///`glyph_idx` : index dans la table FONT (0-9 = chiffres, 10 = '-', 11 = ' ', 12-37 = 'A'-'Z', 38 = '.' , 39 = '(', 40 = ')', 41 = ',', 
250    /// 42 = '[', 43 = ']', 44 = '%', 45 = '<', 46 = '>', 47 = '=', 48 = '?', 49 = '!' , 50 = ':')  51= '+', 52 = '/', 53 = '|', 54 = '_'
251    pub fn draw_char(&mut self, x: u8, page: u8, glyph_idx: usize) {
252        for col in 0..5usize {
253            let byte = FONT[glyph_idx][col];
254            let fb_idx = page as usize * SCREEN_WIDTH + x as usize + col;
255            if fb_idx < self.framebuffer.len() {
256                self.framebuffer[fb_idx] = byte;
257            }
258        }
259    }
260
261    /// Affiche un entier signé 16 bits à la position (x, page).
262    /// Retourne la coordonnée X après le dernier caractère écrit,
263    /// ce qui permet de chaîner plusieurs valeurs sur la même ligne.
264    pub fn draw_i16(&mut self, mut x: u8, page: u8, val: i16) -> u8 {
265        if val < 0 {
266            self.draw_char(x, page, 10); // '-'
267            x += 6;
268        }
269
270        let mut n = val.unsigned_abs();
271        let mut digits = [0u8; 5];
272        let mut count = 0;
273
274        loop {
275            digits[count] = (n % 10) as u8;
276            n /= 10;
277            count += 1;
278            if n == 0 { break; }
279        }
280
281        for i in (0..count).rev() {
282            self.draw_char(x, page, digits[i] as usize);
283            x += 6;
284        }
285        x
286    }
287
288    /// Convertit un caractère ASCII en index dans la table FONT.
289   /// Retourne `None` si le caractère n'est pas supporté.
290   fn char_to_glyph(c: u8) -> Option<usize> {
291    match c {
292        b'0'..=b'9' => Some((c - b'0') as usize),
293        b'-'        => Some(10),
294        b' '        => Some(11),
295        b'A'..=b'Z' => Some((c - b'A') as usize + 12),
296        b'a'..=b'z' => Some((c - b'a') as usize + 12), // minuscules → mêmes glyphes
297        b'.'        => Some(38),
298        b'('        => Some(39),
299        b')'        => Some(40),
300        b','        => Some(41),
301        b'['        => Some(42),
302        b']'        => Some(43),
303        b'%'        => Some(44),
304        b'<'        => Some(45),
305        b'>'        => Some(46),
306        b'='        => Some(47),
307        b'?'        => Some(48),
308        b'!'        => Some(49),
309        b':'        => Some(50),
310        b'+'        => Some(51),
311        b'/'        => Some(52),
312        b'|'        => Some(53),
313        b'_'        => Some(54),
314        _           => None,
315      }
316   }
317
318   /// Affiche une chaîne ASCII à la position (x, page).
319   /// Seuls les caractères supportés sont affichés , les autres sont ignorés.
320  /// Retourne la coordonnée X après le dernier caractère écrit.
321   pub fn draw_str(&mut self, mut x: u8, page: u8, text: &[u8]) -> u8 {
322     for &c in text {
323        if let Some(idx) = Self::char_to_glyph(c) {
324            self.draw_char(x, page, idx);
325        }
326        x = x.saturating_add(6);
327      }
328        x
329    }
330
331
332    // Flush
333    /// Envoie le framebuffer complet vers l'écran via I2C.
334    ///
335    /// À appeler après toutes les opérations de dessin pour rendre
336    /// les modifications visibles.
337    pub async fn flush(&mut self) -> Result<(), I::Error> {
338        self.cmd3(0x21, 0, 127).await?; // colonnes 0..127
339        self.cmd3(0x22, 0, 7).await?;   // pages 0..7
340
341        let mut buf = [0u8; 129];
342        buf[0] = 0x40; // Co=0, D/C#=1 → mode DATA
343        for page in 0..PAGES {
344            let start = page * SCREEN_WIDTH;
345            buf[1..129].copy_from_slice(&self.framebuffer[start..start + SCREEN_WIDTH]);
346            self.i2c.write(self.addr, &buf).await?;
347        }
348        Ok(())
349    }
350}