CH32V003 - SSD1306 Oled library - Page 1
Forum section: electronics / general

author: boreack
author_rank: Newbie
timestamp: September 15, 2024, 07:25:15 pm
content: While starting with this little chip in RISC-V I found less libraries than in Arduino.I'll found a simple SSD1306 library that I used as a base and implement 2 libraries. One with a 1024 bytes buffer and one without it.The buffered version has more functionality as drawing lines or single pixels, while the unbuffered just use less ram (1024 bytes less).I also implement a "inverted" param to some methods to display inverted text or bitmaps on the oled.Any further petitions will be appreciate to continue developing the library.https://github.com/rotura/CH32V003-SSD1306-OLED-LibraryIn theory the library will works with any CH32 chip by changing the I2C pins definition on the "i2c_tx.c" file.Greetings!

author: pastaclub
author_rank: Regular Contributor
timestamp: September 20, 2024, 09:46:57 pm
content: This sounds like a fun way to get into CH32V003.I'm trying to understand what you are doing, without diving too deep into the code. May I ask what the reason is for using a screen buffer? I see that the unbuffered version lacks the functions to draw single pixels, so do I assume correctly you have to write 8 pixels at a time (one byte) and without the buffer you cannot change a single pixel without affecting the other 7? Is it possible to read back from the OLED via I2C? That would be a bit slower of course.By the way, there is an implementation of an SSD1306 library for the Padauk MCUs. Those come with 64 to 160 bytes (not kilobytes) of RAM and 1 to 4 kilowords of OTP ROM (which cannot even be read directly, only through an instruction that places an immediate operand into the accu) and somehow it's possible to implement a display driver including pixel font on them

author: boreack
author_rank: Newbie
timestamp: September 20, 2024, 11:18:46 pm
content: You are right. The SSD1306 use a "Page" of 8 bits. A 128x64 displays is seen as a 128x8 to write byte a byte to the I2C. The default font is 5x8 pixels with the last bit of all bytes always 0 to have a separation between lines, and an empty final byte to separate it from the next character.The buffer is needed to make byte operations between superposed images (or single pixel, text, lines, etc) to get the complete byte to send to the displays.I think you can read the byte from the display, but never try it. Maybe I try it next time.Another interesting thing I found, with 2-3 displaysI have at home:The "OLED_setpos" method with some displays the "Y" value is in pages, and others in pixels (that internally the screen round to pages, I think).I discover it while developing the second project with this little library (the unbuffered there) when suddenly all were shifted vertically

author: DavidAlfa
author_rank: Super Contributor
timestamp: September 21, 2024, 03:14:35 am
content: You can read data back in i2c mode, so you shouldd be able to draw single pixels too (Though way slower than with buffer).

author: brucehoult
author_rank: Super Contributor
timestamp: September 21, 2024, 03:37:19 am
content: 1k of buffer is a lot on a chip with 2k total RAM. Would be ok in some situations, not in others.You could use a hybrid approach and cache an active subset of the display.I'd suggest making little buffers with 32 pixels (1 word). There are 256 of those in the display, so you need a 1 byte tag, 5 bytes for tag+pixels.Decide how many of those you can afford ... let's say 32 ... and make an array of 32 bytes for tags and another array of 128 bytes for the pixels for the corresponding tag. You could linearly search the tag array to find the pixels to update -- probably a lot faster in the worst case than fetching them using I2C. Any time you access one of these entries move it a few places earlier in the array (and slide the others down), so that the most frequently used ones are near the start of the arrays. If you don't find the tag you're looking for then dump the last one in the array and fetch those pixels from the LCD.Or, I'd probably organise the array as a heap sorted by tag, so you only need maximum log2(N) -- so 5 in this case -- tries to find the tag you want if it's in the cache.

author: pastaclub
author_rank: Regular Contributor
timestamp: September 21, 2024, 08:26:09 am
content: The tagging is a very clever approach, I love that idea! Did you just come up with that or is it common practice? An amazing property of this approach is that you can freely balance RAM usage against speed by picking any number of buffers that suits your needs.Another approach I've seen is slicing the screen into narrow tall columns, having a buffer for only one column, and simply repeating all the drawing as many times as there are columns. You draw into the buffer, then copy the buffer to the display. Example: 8 columns => 16x64 pixels per column => 128 bytes buffer => 8 iterations. Then of course the library cannot offer a function that just draws something over what's already on the screen. Instead it would expect the application to register a callback function which does the drawing for the whole screen (and call it for every column). This method works really well for driverless LCDs which have to be periodically refreshed. But with I2C's overhead, refreshing the entire screen even after just changing a few pixels is going to be slow... so the tagging might be the better option.Ultimately the best implementation depends on what you want to achieve. Are you trying to maximize drawing speed, minimize RAM usage, or keeping the code simple?Although the tagging and slicing are cool, I assume that with this specific combination of MCU and display, the 1KB buffer is a pretty sweet spot that suits most applications.

author: brucehoult
author_rank: Super Contributor
timestamp: September 21, 2024, 09:09:36 am
content: The idea of a key -> value mapping is a completely standard idea in both hardware and software. How you organise that abstract idea is at the heart of all data structures: linear list, balanced and unbalanced binary trees, B-Trees, hash tables, heaps. In the special case of the keys being consecutive small counting numbers (or being able to be mapped into them) you can make the optimisation of using an "array" where the address of an item can be calculated from the key and you jump straight to it without searching.Many modern programming languages offer only associative arrays aka dictionaries, aka hashes (which is only one way of implementing them) and use those even when the keys are integers.Hardware such as CPU caches and TLBs (and therefore Virtual Memory) work on the same principle.All I did was choose particular parameters for how many pixels to store densely in one array (32) vs how many such arrays are needed to cover the whole display (256), so as to nicely fit a byte-addressed 32 bit CPU, and to not have toooooo much space overhead (25%) for the tags vs actual data.

author: pastaclub
author_rank: Regular Contributor
timestamp: September 22, 2024, 02:45:09 pm
content: I'm familiar with common data structures and algorithms, but I've never seen a cache for selective parts of a screen buffer.I thought it could be improved by maintaining a hit score per buffer, which prevents items that were used frequently but not recently from being purged too quickly. This brings up the question how to add new items to the cache (since uncached blocks would not have a score). Maybe it wouldn't help that much. This project makes an interesting case for benchmarking different configurations.

author: DavidAlfa
author_rank: Super Contributor
timestamp: September 22, 2024, 04:25:01 pm
content: Maybe something like:Code:#define bf_sz 32typedef struct{uint8_t data;uint8_t row;} row_data_t;row_data_t oled_cache; // Stored in priority orderAnytime a new row is accessed, fetch the data from i2c and overwrite the lowest priority block. Whenever a cached row is accessed, increase the priority and sort.But I think this would only work in very specific cases, like writing to a very small area.Probably fetching the data from i2c isn't that slow, but it requires some smart prefetching to avoid a lot of overhead.The drawing routines could detect the rows beforehand, and request filling the cache as required.For example, drawing a line from xy(0,0) to xy(128,0) would need accessing row 0, columns 0-127.If the buffer size is 32 rows, break the operation in chunks, much faster than single-byte reads.

author: boreack
author_rank: Newbie
timestamp: September 23, 2024, 12:53:54 pm
content: Reading the datasheet, the reading from I2C is by byte, like the writing. You can read a 1x8px page per call.Buffering only the pages required could be possible, but if you try to draw something that use more space than the available ram at the moment, will crash on execution.The minimun RAM usage will be read a page, operate the 2 bytes (display and new data) and write, but you have one problem.How do you know if your data need to overwrite the data readed on from the display in white pixels? Because with this aproach of reading, you would need to make a full clear to remove white pixels from the byte page. One aproach will be to have a variable "Overwrite" that swaps between send only the new data to the display or combine the 2 bytes with an "or" operator before send.With the "all screen buffered" you write all the data from the buffer, regardless of what is on the display, at the cost of more RAM usage.The small buffer method is interesting if you are working in an area fully buffered. Outside this area or with drawings bigger than the buffer would be similar to the read, operation and write method, i think.Greetings.

author: pastaclub
author_rank: Regular Contributor
timestamp: September 24, 2024, 01:21:03 pm
content: Quote from: boreack on September 23, 2024, 12:53:54 pmNot sure if I understand the question correctly.The algorithm would be simple:- determine which page you're drawing on- look for this page in cache- if page not found in cache, read page from I2C and store it in cache (evict another one if necessary)- draw on the page in cache- For drawing white pixels over existing content, newData = cachedData | drawData- For drawing black pixels over existing content, newData = cachedData & (~drawData)- send cached page over I2C