FPC And Breakout Boards For MX340 Control Panel

I’ve been using my salvaged Canon Pixma MX340 multi-function inkjet control panel as test target hardware for learning CircuitPython. After exposing a keyboard event queue, my first attempt at a CircuitPython library is now feature-complete. Well, it is on the software side. The hardware side still need help but I’ve recently learned it is a problem I can solve by spending money.

Up until this point, I had been exploring control panel communications by soldering wires to the back side of its FPC (flexible printed circuit) cable connector. This was essential because I still need the cable installed for main logic board and control panel to talk to each other. Now that I have a code library to let any CircuitPython microcontroller take the place of the main logic board, it means I can actually use my own cable and avoid the need to solder any more wires to these pins.

There are vendors on Amazon selling breakout boards for popular FPC sizes, so I searched for 12-pin 1.0mm pitch FPC connector breakout to see my options. Some of these listings were for a bare circuit board, so I picked this one (*) as it had the connector already soldered and its output pins were spaced out in a prototype board friendly 0.1″/2.54mm pitch format. Those output pins were also already numbered, which I wanted even though it made the next step a bit harder.

I then searched for a short piece of FPC I can use to connect my salvaged control panel with the breakout board. There were again multiple vendors offering 1.0mm pitch 12-position cables, with 150mm being a popular length. I just had to choose if I wanted “A-type” (*) with metal contacts on the same side of the ribbon on both ends, or if I wanted “B-type” (*) with metal contacts on opposite sides of the ribbon. And here I got stuck. I didn’t know which side the breakout board’s connectors made their metal connections. If I chose wrong, I could flip the board over to make the connection, but that would mean the pin numbers would become reversed. That is a recipe for confusion and potentially project-killing mistakes. After waffling back and forth for a while, I decided on the solution of buying both sets. I felt spending a few extra dollars was worth the guarantee of consistent pin numbers.

Once the products arrived, a little probing indicated A-type was the correct answer for me to wire up a Raspberry Pi Pico microcontroller board.


(*) Disclosure: As an Amazon Associate I earn from qualifying purchases.

Sparky Danger Organ Now On The Autopian

Discovering I can just treat Sparky Danger Organ like a piezo buzzer for making music was great, it opened up a world of already-written Arduino sketches playing music with tone(). Now Sparky is getting its 15 minutes of fame with Emily’s This Is How Electronic Ignition Works And Also How To Make A Spark Plug Play Music published on The Autopian.

In Emily’s article, she compared modern electronic ignition with electromechanical ignition from BC (before computers) era of cars. The distributor diagram is a pretty good explanation why I couldn’t do very much with an old ~1950s Chrysler ignition coil my neighbor had sitting on a shelf: I needed something to interrupt power to the ignition coil so its primary winding’s 12V field can collapse and induce high-voltage in its secondary winding. Since I usually play with electronics manipulating 5V or less, at well under 1A, I didn’t have anything on hand suitable to handle such power.

However, a modern electronic ignition coil has that switch built-in. (The “Igniter” box in the LS1 Coil diagram of Emily’s article.) So to repurpose a retired coil+plug only requires a 5V, low-current, digital control signal to be sent on its IGT pin. Now that’s something Arduino tone() can do easily and help Sparky find its singing voice.

So what’s next? There was a bit of confusion between Emily and I about Arduino tone(). She referred to an installed library, but I found it as a built-in function. Investigating this confusion led me to https://github.com/bhagman/Tone where I learned we were both right. There’s a full non-core version that Emily had to install into her (presumably older) Arduino IDE, and Arduino had incorporated a simplified version of this library into core which is what I found. The simplified version can only generate one voice. The full version can generate more voices: one per hardware timer on the AVR chip. ATmega8-based chips have two timers so two voices. ATmega328P (like that on the Arduino Nano I used) can do three voices, and an ATmega1280-based board like the Arduino Mega can control six spark plugs. Useful facts to keep in mind whenever Emily or I get our hands on more retired electronic ignition coils.

MX340 CircuitPython Key Events

My CircuitPython learning project was to communicate with the K13988 chip on board a salvaged Canon MX340 multi-function inkjet control panel. The follow-up project, currently in progress, is to convert my jumble of experimental and exploratory code into something resembling an usable library. It looks pretty good after I rearranged all the setup/cleanup into context manager syntax, but then I realized I was missing a big piece: key press events. I had been printing key codes to serial terminal as they come in, but that was no way to write an API!

For precedence I looked into CircuitPython keypad class. I had expected this to be an opportunity to learn about Python events and event listeners, so I was surprised (and mildly disappointed) to find an event queue that client code is expected to poll on a regular basis. Well, at least I know how to implement such a thing, and Python standard libraries should make it pretty easy to implement.

I could reuse their Event class to report my key press events, but the EventQueue class itself is locked away inside Adafruit implementation and not available to me. I went looking for an existing Python standard library queue and the first search result seemed to be far more complex than I had expected. Turns out that particular queue was designed for coordination across threads, overkill for this purpose. I just need the basic deque (double-ended queue) class to emulate most of Adafruit’s EventQueue.

For my first draft I decided against implementing a separate object to expose keyboard event queue. I’ll expose a get_key_event() method which should serve basic scenarios well enough until I see justification to do more.

MX340 CircuitPython: Internals and Context Manager

I’ve got CircuitPython code to interface with control panel of a Canon MX340 multi-function inkjet, and gradually refining my crude experimental code into an actual reusable CircuitPython library. Using events for synchronization and exposing property setters was just the start.

A major change is to separate communication infrastructure from code that utilizes such infrastructure. For example, I’ve had code in there blinking the “In Use/Memory” LED to let me know everything is still running. I consider code to transmit the LED control bytes to the NEC K13988 as infrastructure, and the timing loop to toggle LED on and off to be application code. I have a separate asynchronous task that continually updates the LCD content for the same reason, and that should be separated out as well. During the experimental phase I could just mix code in there in a big jumble. A global space where any bit of code can call any other bit of code. But now I have to think about which methods should be part of the API and which are internal implementation.

To reflect this division, a minor change is to rename several methods with a leading underscore. This is a “weak internal” indicator by Python convention. They are understood to be for internal use though there’s no language level mechanism preventing callers from accessing them. I guess this means they can be changed without becoming a public API breaking change? Reading the linked section of PEP 8, I saw there’s also a way to declare public interface with __all__. As per Python tutorial on module imports, this works in conjunction with “from [module] import *“. I don’t understand enough about Python modules yet to know if it’s applicable to my project.

After I performed this separation, I can clearly see a set of startup and cleanup tasks that have to run before and after application code. This sounds like a job for context managers! I can set up all the infrastructure by using an async with statement, and all application code can run within its scope. I first tried to write my context manager with the generator syntax that has a single yield for application code, but that failed: I need to decorate such a generator with @contextmanager to apply supporting infrastructure from contextlib, which has not been ported to CircuitPython. Oops! Fortunately I can still implement a context manager by falling back to using special method names __aenter__() and __aexit__(). Once implemented, I can apply even more lessons from my recent study session, implementing a clean separation between my test code from my NEC K13988 infrastructure.

I was pretty pleased with myself with how well this is going when I realized I completely forgot about key press events.

MX340 CircuitPython Refinements: Async Event But No Property Setter?

I’ve been writing CircuitPython code to interface with the NEC K13988 chip on board a salvaged Canon MX340 multi-function inkjet control panel. At first I only intended to explore if it was even possible to repurpose the panel. Now that I’m confident it is possible, I decided my follow-up would be to factor my code to extract a reusable library that could potentially support multiple projects. As expected, the task of making the code neater taught some valuable lessons.

Asynchronous Event

I had a few asynchronous tasks declared and running, each handling one aspect of K13988 interface, but sometimes they stepped on each other’s work. I learned about Lock() earlier but I had additional synchronization needs that didn’t fit a Lock(). Some pieces of code needed to wait for certain other startup conditions to be met before it ran. For example, I needed to wait for the K13988 to wake up enough to send data bytes before I could successfully transmit commands to it.

In the constructor, I set a boolean:

self.in_startup = True

And in my data transmission code, I check that boolean value in a loop:

while self.in_startup:
    await asyncio.sleep(0)

That would put data transmission on hold until I receive my first byte from K13988, at which point I could flip that boolean value:

# First successful read complete, exit startup mode
self.in_startup = False

This worked, and I guess it isn’t terrible. But there was a better way. When I read through Python documentation during my study session, I discovered this pattern fit Event from the asyncio library.

Using events, I declare one in the constructor:

self.transmit_startup = asyncio.Event()

And in my data transmission code, I will wait for this event before proceeding:

await self.transmit_startup.wait()

Permission to proceed will happen once I receive my first byte from K13988:

# First successful read complete, exit startup mode
self.transmit_startup.set()

This was a simple case, and I’m glad it helped me learn to add to my toolbox so I am ready to tackle more complicated scenarios in the future.

No Async Property Setter?

This control panel has four LEDs. Two are wired directly to main logic board connector pins, two are controlled by K13988. I thought I would add Python property setters for those two LEDs, following precedence of setting a CircuitPython digitalio output pin value. But it seems like I can’t expose a property setter to turn a LED on like this:

k13988.in_use_led = True

The problem is that such LED update would require sending two command bytes to K13988, and that is an asynchronous operation which meant I have to somehow declare async on the property setter and somehow use await in the caller. If there’s a Python syntax to declare such a thing, I couldn’t find it. For now the best I can do is to implement an explicit method to set LED:

await k13988.in_use_led(True)

Not terrible, just not as straightforward. I can live with this while I learn more asynchronous Python and maybe I will learn something that’ll let me come back to fix this later. In the meantime I have more cleanup work to do.

Updated Goals for MX340 CircuitPython Project

I typically learn new things by alternating between hands-on time and reading study time. After reading through (though not necessarily understand all of) several Python Enhancement Proposals ending with PEP 492 Coroutines with async and await syntax I’m ready to switch back to playing in a code editor. My CircuitPython practice project is to interface with a control panel I salvaged from a Canon MX340 multi-function inkjet. I’ve successfully accomplished that with a few rough test programs, but I didn’t understand what I did, thus the study session.

Now that I understand a bit more of Python generally and CircuitPython in particular, I want to aim higher for my project. Instead of a bunch of test code written on the fly whenever the next idea occurs to me, I should go in and organize things sensibly. I want to learn from Adafruit’s collection of CircuitPython libraries to interface with hardware peripherals, following their lead to reorganize my MX340 jumble into a self-contained piece of reusable code. I believe the primary reference would be Adafruit’s CircuitPython design guide for community-contributed libraries. I don’t understand all of it yet, but I expect to learn by doing and learn from my failures to conform to the design guide. I do not realistically expect my MX340 CircuitPython library to be used by many others. The point of the exercise is to learn how to build such a library, so that a future library I write may actually be useful to others.

Here are my updated goals:

  1. A class to handle serial communication with the NEC K13988 chip on board the control panel, with three functional areas:
    • Buttons: Event queue of button activity, following precedence of Adafruit’s keypad class.
    • LEDs: Two boolean properties with get/set to manipulate LEDs under K13988 control.
    • LCD screen: following precedence of Adafruit’s rgbmatrix class, either create a byte array for raw frame buffer or use a caller-allocated byte array. Implement a refresh() method which will initiate sending frame buffer data to K13988 for display.
  2. An optional class to wrap the above LCD raw byte array into a MicroPython-style FrameBuffer class. Implemented as adafruit_framebuf with a custom frame buffer manipulation class. This is optional and not part of #1 because the caller may wish to manipulate the byte buffer directly.
  3. Some example code utilizing the above.
  4. After writing example code, decide if it’s useful to add one more: An optional class as single point of contact for the entire control panel. Exposing a single event queue integrating direct-control buttons (“Power” and “Stop”) with K13988-scanned buttons, and properties for direct-control LEDs (“Power” and “Alarm”) peer of K13988-controlled LEDs (“WiFi” and “In Use/Memory”)

That seems like a set of achievable goals to guide me as I got started with code reorganizing.

Arduino tone() Can Play Sparky Danger Organ

I got a spark plug to sing a song, but it was singing out of tune and I didn’t know why. I barely know enough about music to get this far. My friend Emily had more sheet music reading skill and offered to double-check my music translation. If her translation sounds good, then we’ll know the problem was my sheet music translation. If her translation also sounds odd, we’ll know the problem is with my timing code controlling spark frequency.

I replaced my version of the song with hers, and it sounded great! So that’s settled: Sparky’s first performance sounded bad because I didn’t know how to read sheet music. I also received feedback that the way I represented music in my Arduino code was peculiar. No argument from me, because I wrote it out of ignorance and it was what I could hack together at the time. Emily then asked if there was a reason why I’m not using tone() if I’m using an Arduino anyway. The answer was that I didn’t know it existed.

Prompted by this question, a bit of searching on “Arduino tone library” found a GitHub repository at https://github.com/bhagman/Tone, whose README explained it was a library to use AVR timers to generate 50% duty cycle square waves of a specified frequencies along with a list of #define that mapped musical notes to frequencies. It also said a single-timer version of the library has been incorporated into Arduino core, so for single-voice applications like Sparky we wouldn’t even need to install an additional library.

I thought the 50% duty cycle (half of time signal is on, half off) might be a problem, because I’ve been using 0.5ms signal pulses and leaving the pin off the rest of the time. I looked in the tone library but wasn’t familiar enough with AVR timer configuration registers to understand how I might convert it over. Then I remembered the always-useful mantra: check the easy thing first. Maybe Sparky doesn’t mind if the trigger pulse was longer.

Since the single-timer version of tone was now built into Arduino, I looked for Arduino built-in examples illustrating its use. I found “Examples/02.Digital/toneMelody” which was designed to work with a piezo buzzer. I pointed it to Sparky’s IGT pin and heard it play the little melody. Great! Sparky doesn’t mind longer IGT pulses and I can throw out all of my hacked-up music-hampering playback code and just use tone(). I don’t even need to struggle with sheet music or convert them to MIDI notes anymore, I can play songs other people have already translated for tone() playback. I found several collections and the first collection with its own take on Beethoven’s “Ode to Joy” is https://github.com/robsoncouto/arduino-songs. I gave to Sparky and yeah, way better than my version and a lot easier too. I wish I knew tone() existed a few days ago, but at least I know now. This tone() music earned Sparky 15 minutes of fame on The Autopian.

Sparky Danger Organ

I’m playing around with a retired Denso ignition coil-on-plug pack. After gaining a marginally better understanding of its limitations, I will convert my test circuit to a proof of concept discussed with my friend Emily Velasco (who gave me this thing to play with): make it into a silly music instrument I’ve named Sparky Danger Organ.

This ignition coil came out of a Toyota Sienna’s V6 engine which probably redlines somewhere around 7000 RPM. At 7200 RPM this coil would be firing sixty times a second, but 60Hz is on the low side for music-making. So my workbench experiments were to find out how much faster I can drive this thing and I think it’s somewhere between 1 kHz and 1.5 kHz. (There isn’t an exact number because sparks fade out rather than abruptly cut off.) But judging from Wikipedia’s list of piano notes and their frequencies, even a limit of 1kHz should be enough to represent an interesting range of music notes.

I then started looking for music I can map to frequencies. I found a Blogspot page Image of 88 piano keys on sheet music with note names, octaves and midi numbers, and downloaded the PDF version of the chart. Then I looked for a relatively simple and well-known piece of music and settled on “Ode to Joy” from Beethoven’s Symphony No. 9. I quickly found a simple arrangement by Benedict Westenra intended for piano beginners. Perfect!

Putting all that together gave Sparky Danger Organ its first performance, and it’s definitely along the lines of a bear riding a bicycle: the novelty is not that Sparky Danger Organ is a great music instrument, but that it can make an attempt at all. There’s only a very limited range of frequencies where it sounds like an acceptable tone, and even within that range the music does not sound great. I may have made mistakes translating piano sheet music into Arduino code. Or maybe the frequency I generated is not what I had intended to generate, throwing the instrument out of tune. The top suspect along these lines is the possibility Arduino framework is doing work behind the scenes and adding delays throwing off the output frequency.

Despite those potential problems, it was a successful proof of concept. Emily has some ideas on how to troubleshoot this device. [UPDATE: the solution was to throw away all of my crappy code and use Arduino tone().] And if we ever get our hands on one or more engine’s worth of ignition coils, there may be a Sparky Danger Orchestra.


Public GitHub repository for this project: https://github.com/Roger-random/ignition_coil

CircuitPython asyncio.Lock Prevents Interleaving Data Transmission

I have built up a rough set of CircuitPython code that sends a bitmap to show on LCD screen of a salvaged Canon Pixma MX340 multi-function inkjet. I abandoned my effort to get sprite compositing via displayio, but I still want to do something fun with this screen. As an experiment, I wrote a loop that continuously sent LCD frames to display, exploring its theoretical ~14fps top speed. The result was very jerky, and CircuitPython console output showed a lot of my error message indication transmission failures that had to be retried.

The first data point was adding a small delay between frames to see if that changes behavior. It did not, so I believe these failures were not caused by a relentless nonstop stream of frames. The next data point was to remove my heartbeat LED, and that stopped the stream of errors. Interesting.

The heartbeat LED is a separate task, with asyncio switching back and forth between LCD updates and heartbeat LED updates. Heartbeat is a simple loop that blinks the “In Use/Memory” LED on and off by transmitting corresponding control flags. Each toggle is a two-byte transmission, but the LCD screen update is a much longer sequence of data transmissions.

Task switching could occur at any await statement, which means it is possible for heartbeat LED update command to barge in the middle of a LCD sequence. The most inconvenient time would be right after the 0x04 0xC4 command “Bulk transfer of 196 bytes incoming”. If the next transmission is a two-byte LED toggle instead of the expected 196 byte transfer, everything would understandably go awry.

To prevent interleaving data transmission between tasks, I need to add a locking mechanism so heartbeat LED updates will hold off until LCD screen update is complete. And in case of continuous nonstop LCD animation, it needs to make sure heartbeat LED update gets a chance in between frames. Out of habit I started writing my own locking mechanism but then realized I was being silly. I went to read documentation and confirmed a lock is already available as asyncio.Lock. I only had to create a single instance of the object, then as per documentation add an “async with” statement on that instance to prevent my two data transmission tasks from stepping into each other’s sequences.

It worked! I could put my heartbeat LED back into the system, spin the LCD animation updates to full speed, and they both run. No longer interrupt each other and causing transmission errors. But this experience really gives me a feeling I’m wielding powers beyond my comprehension. I barely understand async/await and now I’m adding “with” context management which I also barely understand. This is probably a good time to pause my uninformed code-slinging and read some tutorials and/or specifications to understand what’s going on.

These Aren’t The Frame Buffers You’re Looking For

After a small (if not necessarily elegant) enhancement to adafruit_framebuf class, I can use it for basic graphical operations on the bit-mapped LCD of a salvaged Canon Pixma MX340 multi-function inkjet control panel. I then set my sights on adapting it to work with Adafruit’s graphics compositing engine displayio, but that turned out to be more difficult than I had thought.

I don’t know the full history but looking in hindsight I think displayio was optimized for sprite rendering, the most performance-critical part of a 2D game engine. Such a system would support products like Adafruit PyGamer. Not so important for this inkjet control panel LCD, whose refresh rate tops out at just under 14 frames per second. But I thought it would be fun anyway.

Adafruit has displayio drivers for their most popular screens and obviously my LCD is none of them. Looking for an appropriate tool, I found their framebufferio class which takes a frame buffer and returns a display object. Its target usage scenario allows using displayio with a RGB LED matrix, which has resolutions with similar order of magnitude as my LCD. This looks promising.

When I fed framebufferio my FrameBuffer class, I crashed my KB2040 which restarted in safe mode. (Indicated by triple blinking yellow LED.) Annoyingly, when a CircuitPython device crashes and reboots like this, Mu closes its serial terminal so I couldn’t read the error message before it immediately disappeared. I had to bring in a different serial monitor tool in order to see the error:

TypeError: 'FrameBuffer' object does not support 'protocol_framebuffer'

Darn. Looks like the CircuitPython frame buffer object I had been working on, isn’t the CircuitPython frame buffer object expected by framebufferio. If I want this to work, I have to implement “protocol_framebuffer“, but what is it? Clicking on the link brought me to something called circuitpython_typing.FrameBuffer, which forwards to rgbmatrix.RGBMatrix. I added methods to my FrameBuffer-derived class in order to match those listed for RGBMatrix, but that was not good enough.

Digging deeper meant going into core CircuitPython written in C. I found framebufferio/FrameBufferDisplay.c which had this line:

self->framebuffer_protocol = mp_proto_get_or_throw(MP_QSTR_protocol_framebuffer, framebuffer);

I think this is where my error message came from, so the next step is to find more information on the expected protocol which was declared in the associated header file FrameBufferDisplay.h.

typedef struct _framebuffer_p_t {
    MP_PROTOCOL_HEAD // MP_QSTR_protocol_framebuffer

    // Mandatory
    framebuffer_get_bufinfo_fun get_bufinfo;
    framebuffer_swapbuffers_fun swapbuffers;
    framebuffer_deinit_fun deinit;
    framebuffer_get_width_fun get_width;
    framebuffer_get_height_fun get_height;

    // Optional getters
    framebuffer_get_bytes_per_cell_fun get_bytes_per_cell; // default: 2
    framebuffer_get_color_depth_fun get_color_depth; // default: 16
    framebuffer_get_first_pixel_offset_fun get_first_pixel_offset; // default: 0
    framebuffer_get_grayscale_fun get_grayscale; // default: grayscale if depth < 8
    framebuffer_get_native_frames_per_second_fun get_native_frames_per_second; // default: 60
    framebuffer_get_pixels_in_byte_share_row_fun get_pixels_in_byte_share_row; // default: false
    framebuffer_get_reverse_pixels_in_byte_fun get_reverse_pixels_in_byte; // default: false
    framebuffer_get_reverse_pixels_in_word_fun get_reverse_pixels_in_word; // default: false
    framebuffer_get_row_stride_fun get_row_stride; // default: 0 (no extra row padding)

    // Optional -- default is no brightness control
    framebuffer_get_brightness_fun get_brightness;
    framebuffer_set_brightness_fun set_brightness;

} framebuffer_p_t;

This is beyond my current understanding of CircuitPython code. Does it mean framebufferio only works with Adafruit’s own frame buffer objects written in C? Or is this compatible with user-created Python code somehow? Again I tried modifying my FrameBuffer-derived class to fit this signature, but either I made a mistake somewhere or it is fundamentally impossible. I wish I could get finer-grained error message telling me where exactly I failed to support protocol_framebuffer, but I just keep getting the same failure leaving me stabbing in the dark.

After a few hours of fiddling without success, I abandoned displayio compatibility and wrote up this page in case my future self wants to pick up where I left off today. Without displayio I’ll learn what I can from using adafruit_framebuf. First lesson: updating LCD at a high frame rate required task synchronization.

Improve Drawing Via MVMSB For adafruit_framebuf

I’m playing with a LCD screen built inside the control panel I salvaged from a Canon Pixma MX340 multi-function inkjet. I tried using CircuitPython’s adafruit_framebuf library for drawing operations. Out of all supported frame buffer formats, the closest is MVLSB but those bits are in reversed order. I added code to reverse bits, which put a picture on screen but the real solution is to add proper buffer format support.

I was encouraged by adafruit_framebuf‘s description in its documentation: “CircuitPython pure-python framebuf module, based on the micropython framebuf module.” If this library was itself implemented in Python, I should be able to figure something out. Clicking “Edit on GitHub” link on the upper right, I was brought to the GitHub repository for adafruit_framebuf and I quickly found source code file for the module.

At the top of adafruit_framebuf.py are a series of classes, each representing one of the supported buffer formats and implements four static methods: set_pixel(), get_pixel(), fill(), and fill_rect(). I found the implementation for MVLSBFormat and it looks like it’s within my skill level to copy it and reverse its bit manipulation operators to create a MVMSBFormat counterpart. The next problem is: how do I use my custom MVMSBFormat class with the FrameBuffer class? FrameBuffer‘s __init__() assigns a format class in response to initialization parameter, and it raises a ValueError() if it doesn’t recognize the parameter.

That’s when I remembered I am still thinking like a C/C++ programmer and Python doesn’t have a concept of private properties. I could create an instance of FrameBuffer for MVLSB format, and then immediately replace the .format property to point to my MVMSBFormat class. As a C/C++ programmer, this feels extremely icky. Is this the correct Python-ic approach? I doubt it, but I don’t know the right way. So right or wrong this is what I have now.

With my FrameBuffer.format pointing to my MVMSBFormat class, adafruit_framebuf drawing operations render correctly on screen without the need for bit reversal utilities. As a bonus, eliminating bit reversal overhead also meant I could draw text again without encountering “RuntimeError: pystack exhausted“. I think that’s good enough to declare victory with basic bitmap operations and move on to a sprite compositing engine.

Reverse Bits To Draw With adafruit_framebuf

Once I found and fixed a missing command within my initialization sequence, I have gained full programmatic control over the LCD screen of a control panel I salvaged from a Canon MX340 multi-function inkjet. The main point of the exercise was to learn, because it’s a relatively simple black-and-white screen 196 pixels wide by 34 pixels tall and not terribly exciting on its own. Still, now that I know how to send it frame buffer data… what can I do with it?

I’m not interested in re-implementing standard graphics routines myself, so I went looking for one already available. While learning about MicroPython I recall coming across mention of a framebuf utility class for basic graphical operations on a frame buffer. As a fork of MicroPython, Adafruit has a corresponding CircuitPython implementation named adafruit_framebuf so I thought I’d take a look.

One of the frame buffer byte formats supported is called MVLSB. The M stands for monochrome, and the V means each byte represents a vertical column of eight pixels. LSB means least significant bit is the highest pixel, and the most significant bit is the lowest. This is very close to what I have on hand except for the bit order. On this screen, the least significant bit is the lowest pixel and they go up from there.

This is the result of drawing a diagonal line from upper left (0,0) to lower right (195,33). Since bit order is reversed within each byte, each horizontal stripe of data gets rendered vertically inverted from what they are supposed to be.

Drawing text results in gibberish when it crosses stripes. But if I position the text so it sits entirely within a single stripe, the result is legible as vertically inverted text. “Hello Framebuffer” in this case.

I could write some code to laboriously step through each bits in a byte and generate a bit-reversed counterpart value, but since there are only 256 possibilities, a lookup table would be much faster at the expense of a bit of memory. I went online looking for such a lookup table and found one in this Stack Overflow thread: Efficient Algorithm for Bit Reversal (from MSB->LSB to LSB->MSB) in C.

I modified that lookup table from C to Python syntax, and used it to reverse a stripe of frame buffer data before transmitting it to the LCD. Now my diagonal line renders as expected. Unfortunately, text rendering now fails with “RuntimeError: pystack exhausted“. This was unexpected. The lookup table itself would have consumed at least 256 bytes, and there’s an additional 196 bytes of working buffer for me to perform bit reversal on a single stripe. I didn’t think I was skating close to RP2040 limits yet here we are. Also, it’s not a general out of memory error, but specifically about call stack size and the bit reversal mechanism didn’t increase the call depth at all. Weird!

Well, reversing bits was just a hack anyway. The right approach is to add support for this byte format directly so convoluted bit reversal workaround becomes unnecessary. And it restored text rendering, too.

Missing Initialization Command For Frame Buffer Offset On MX340 LCD

I had incorrectly configured asynchronous serial communication parameters between my KB2040 microcontroller running CircuitPython and NEC K13988 chip on a control board salvaged from a Canon MX340 multi-function inkjet. But once I sorted that out (two stop bits, not just one) I could successfully send a full set of frame buffer data to the LCD with minimal transmission failures. This allowed me to follow up on an observation I had from my first test: the vertical frame buffer byte offsets are not as expected.

The pattern I saw via logic analyzer indicated the top 8 pixels of the screen were dictated by sending pixel data with the offset 0x4D to start the sequence. But when I tried doing the same thing with my KB2040, the stripe rendered two pixels lower than expected. This pattern continued for the rest of the screen, with successively lower rows corresponding to 0xCD, 0x2D, and 0xAD. When I decoded frame buffer data captured by logic analyzer, offset 0x6D dictated the bottom-most two rows of pixels. (The least significant six bits are not visible.) However, now data sent to 0x6D is not visible at all. Experimenting with other offsets, I stumbled on the fact I could transmit data to 0x8D and that data would help me fill in the top two rows of pixels. (The most significant six bits are not visible.)

Even though it was unexpected, that was good enough for my next experiment: I changed the program so instead of a fixed value, each byte counts up one. This gave me get a precise delineation and count for all horizontal and vertical pixels. Thanks to this test pattern I am now confident this LCD has 196 pixels horizontally and 34 pixels vertically.

But what about that unexpected offset? The most likely suspect is a mistake in my initialization sequence, and a double-check found the culprit. There was a two-byte sequence 0x04 0x42 in my decoded notes, but I forgot to put that in my KB2040 initialization sequence. Once I put that back in, my LCD frame buffer renders at the expected location. On my to-do list is to play with my initialization sequence, selectively skipping them to see what effect they have on behavior. Looks like I inadvertently started that experiment early! This little side trip also suggested an interesting possibility: that the LCD controller has extra frame buffer memory available, possibly allowing vertical scrolling without re-sending the entire frame buffer.

Without the datasheet, though, that’s mostly an empty thought. It’s more productive to try to pair this display with a graphics library.

I Needed Two Stop Bits To Talk To NEC K13988

I’ve got an Adafruit KB2040 microcontroller connected to a control panel salvaged from a Canon MX340 multi-function inkjet. Using CircuitPython busio.uart, I’m trying to emulate MX340 main logic board with my KB2040, communicating with the NEC K13988 chip on board the control panel hoping to access its capabilities. I saw some initial success, but I had to retry many data transmissions which hinted something was amiss. I forged onward to get more data about what might be wrong.

The first experiment was to test my hypothesis that the first LCD update sent by MX340 main board was a “clear screen” sequence. I sent 0x04 0xF5, the command I suspect to be “start displaying data from frame buffer”, but without transmitting the clear screen sequence of zeroes. I was rewarded by an awakened LCD displaying gibberish, proving the command and “clear screen” data hypothesis.

Pulling up my notes on decoding LCD frame buffer data, I followed that pattern to transmit a single stripe of 0x55 as frame data. Hexadecimal 0x55 translates to binary 0b01010101 and I expected this to show up as four horizontal lines across the screen. The first few attempts failed, with no visible change and no 0x20 acknowledgement byte. Then a transmission went through with visible change and 0x20 acknowledgement, but it looks wrong.

Instead of four horizontal lines all the way across, the line is broken as it hopped up and down across the screen. The fact that it came up on screen at all told me I’m very, very close but there’s definitely a problem. At every byte (vertical slice of 8 pixels) there was supposed to be an alternating sequence of bright and dark, but I see several sections where two white pixels separate black pixels, and less frequently two dark pixels side by side without a white pixel separating them.

What might cause these off-by-one-bit errors? My first guess was that the baud rate sent by KB2040 was a tiny bit off from what the control panel expected, so I tried 250001 baud and 249999 baud but that just made things worse. Even parity and 8 data bits had to be correct or I wouldn’t have gotten this far, leaving stop bits as the final parameter. I changed my busio.uart configuration from 1 stop bit to 2 stop bits and that seemed to have been the answer.

Running with 250000 8E2, I could reliably send five full stripes of 0x55 for a screen full of clean horizontal lines. This makes sense in hindsight. When I examined logic analyzer trace one byte at a time, I wouldn’t have seen anything obviously differentiating between 2 stop bits or just 1 stop bit and delay between each byte of transmission. A mistake that went unnoticed until now. However, I still see the occasional transmit failure, so there might be another bug still lurking. I hope to get more data about it as I continue my experiments.

CircuitPython busio.uart Up and Running, Mostly

After a quick warm-up exercise of verifying activity over expected serial transmission lines, I updated my code to use CircuitPython’s busio.uart to interpret that activity as asynchronous serial data. Based on earlier analysis, I expect to receive data at least once every 9.2ms, so I set up an asynchronous task to constantly process incoming data. The good news is that I saw a constant stream of 0x80 0x40, which was as expected. I had established 0x80 was the value reported for “no buttons are pressed” so I had expected that value to change if I pressed a button. It did not! Apparently key matrix scan code reporting doesn’t start by default.

In order to parrot the initialization sequence decoded by my logic analyzer, I need to write another asynchronous task. This one transmits given data and then wait for the receiver task to see an 0x20 acknowledgement byte before returning. Once up and running and initialization sequence sent, the receiver task started reporting key matrix scan code values. Success!

Up until this point I still had the Neopixel sample code running as a system heartbeat indicator. With serial communication up and running, I thought I’d switch over to using one of the LEDs on the control panel. I created another asynchronous task, this one loops to turn the “In Use/Memory” LED on and off at one second intervals while leaving the “WiFi” LED off. I have already decoded the bit flags necessary to toggle LEDs and I got a LED that blinked for a few seconds.

Then it got stuck.

Debugging indicated that my code got stuck because the data transmission to update LED state never received that 0x20 acknowledgement, which meant the transmit task never returned to caller. I quickly whipped up a timeout mechanism by checking timestamp in a loop. It worked, but then I realized there was already a mechanism built into CircuitPython. So I deleted my timestamp checking loop and replaced it with a single call to asyncio.wait_for().

I needed to write this timeout code and retry transmission for robust error recovery no matter the reason, but I want to know the reason why I never got 0x20 acknowledgement. Maybe there are times when the chip is not listening to my commands? Or maybe there’s a problem in my data transmission? I think it’s very likely the chip isn’t always receptive, but that’s not as important as the discovery there was indeed a problem with my data transmission.

CircuitPython countio For Detecting Activity (Limited on RP2040)

I have a rough idea on how I would use CircuitPython’s asyncio capability to structure my learning project, but jumping straight into async/await in a new platform is too bold for me. I would rather ramp up gradually starting with running a few CircuitPython samples. Once I was satisfied my KB2040 board is generally working as expected, I started poking at project-specific needs. First task: see if I’ve soldered serial communication wires correctly by checking for activity on those pins in reaction to enable pin status. (Toggled via CircuitPython’s digitalio module.) I expect activity on the receive pin when the enable pin is high, and no activity when enable is low. The transmit pin should have no activity in both cases.

I had originally intended to check pin status by polling pins in a loop, which is not guaranteed to catch all activity but probably good enough for an “is there activity” check. Polling in a loop would have been trivial in an Arduino test sketch, but CircuitPython made it just as easy to pull in something more sophisticated: countio module counts rising/falling edges driven by hardware interrupts, which would have taken a bit more work to set up on Arduino. Great! Let’s do that.

I copied sample code from documentation and modified it for my purposes of watching pins D0 and D1, which were default UART transmit and receive pins on my KB2040. I saved my file and promptly got an error telling me I am not allowed to do this on D0. Huh? Re-reading countio documentation I saw a paragraph I had missed:

Limitations: On RP2040, Counter uses the PWM peripheral, and is limited to using PWM channel B pins due to hardware restrictions. See the pin assignments for your board to see which pins can be used.

Reviewing KB2040 pinout reference, I see D0 is also PWM0A and D1 is PWM0B. So they are both PWM-capable pins satisfying the first restriction, but only D1 satisfied the second restriction of channel B. Fortunately D1 on PWM0B is the default UART receive pins, so that’s the one I was more interested in checking status for anyway. And the check was good: when enable pin is high, there is a steady stream of activity on D1. When enable pin is low, activity stops. Good enough to take the next step, connect UART peripheral to those pins.

CircuitPython asyncio For Cooperative Multitasking

I want to apply my CircuitPython lessons to a project that will require importing the busio library to utilize asynchronous serial communication hardware on board a RP2040 chip. This is a pretty common microcontroller scenario, but example projects usually only deal with one direction, either just sending or just receiving. Adafruit’s CircuitPython UART serial introduction only received data and did not send. Things will get more complex as I intend to implement bidirectional communication, and even more so because there’s interaction between the two directions: I need to send data and listen for an acknowledgement.

If that’s the only thing being communicated, it would be easy: one line of code to send data, followed by a line of blocking code to wait for response data. But it’s more complicated in this project because the K13988 chip on the control panel sends key scan matrix reports on a regular basis. (Every ~9ms.) If I wrote my code in the naive way, that wait for response may pick up a key scan matrix report byte instead of the acknowledgement byte. What I need is something that is constantly receiving and processing data, mostly key scan report bytes. After the data sender code runs, it will have to wait on the receiver loop to pick out any acknowledgement bytes as they come through in between key scan reports.

This is doable within an Arduino-style loop(), and I’ve done it before, but I end up with code that’s hard to follow as it rapidly switches between multiple contexts. That’s why I was happy to learn CircuitPython implements the async/await pattern with its asyncio library, which should theoretically allow cleaner code structure. My prior experiences with this pattern mostly centered around JavaScript client/server networking code and not microcontrollers, but never fear, as per usual for Adafruit there’s an excellent guide Cooperative Multitasking in CircuitPython with asyncio putting the pattern in context for microcontroller tasks.

It also highlights an important simplifying characteristic of CircuitPython: it has only a single thread of execution running a single event loop, so there is no concern of race conditions between multiple threads/loops and we don’t have to worry about protecting critical sections. Eliminating concurrency eliminates all the huge headaches of writing concurrent code, at the performance cost of running only a single core. A RP2040 has two cores, like an ESP32, but apparently one core sits idle while the other runs CircuitPython. For this project (and most of my potential CircuitPython projects) a single CPU core will suffice. I’m not going to worry about the lack of dual-core operation until I run into an actual problem that needs it. Right now my problems are simple, like checking to see if there’s any activity on a pin.


Installation note: for most CircuitPython libraries in the bundle, we install it by copying its single corresponding *.mpy file into the \lib\ subdirectory on our microcontroller’s CIRCUITPY storage volume. But asyncio is actually a directory containing multiple *.mpy files and we copy the entire directory under \lib\ so all its components are under \lib\asyncio\*.mpy.

CircuitPython busio For Hardware Serial Communication

For my first nontrivial CircuitPython project, I thought I would try getting a microcontroller to communicate with control panel salvaged from a Canon MX340 multi-function inkjet. Like most microcontrollers, the RP2040 chip at the heart of my KB2040 board can handle asynchronous serial communication via its UART hardware peripheral. Under CircuitPython, this peripheral is exposed by an uart class. It is under the umbrella of busio library, which includes hardware-accelerated communication of popular serial buses I2C, SPI, and asynchronous serial. CircuitPython has several libraries with the io suffix, and given their hardware-focused nature they appear to be a big chunk of the work required to port CircuitPython to a new microcontroller.

One aspects of CircuitPython (and MicroPython) I found enticing is the ideal that I could develop and debug code on my desktop computer and then copy it to run on a microcontroller. Unfortunately this goes out the window when I want to use hardware-accelerated features like busio.uart. Its name clearly announces that it is not trying to act like PySerial on the desktop. If I want to write something that works on both desktop Python and microcontroller CircuitPython, I would have to write an abstraction layer restricted to the small subset of common features I need in my project. Learning how to write such adapter code is on my Python learning to-do list, but it is out of scope for my first CircuitPython project. I’ll stick with busio.uart and develop/debug strictly on microcontroller.

One Python skill area I want to improve is structuring my code to better handle non-sequential events. The relevant scenario today is sending serial data out the transmit pin while simultaneously responsive to serial data coming in over the receive pin. Plus handling when the two interact: for example waiting to receive an acknowledgement after sending data. My earlier serial data filtering tool handled reading from two serial ports by rapidly switching between polling the two ports, but in hindsight, that was not elegant code and it didn’t handle interaction between the two streams. In the search for a better way, I recall Python supported the async/await pattern I’ve encountered earlier in JavaScript and C#. Furthermore, CircuitPython supports some form of the pattern as well, so I’m going to give it another go.

CircuitPython Test Run: MX340 Control Panel

After reading through AdaFruit’s “Welcome to CircuitPython” guide, the obvious next step is to tackle a project to apply what I’ve learned. I do want to apply it to a rover project, but I fear that’s too big of a first step. Looking around at what I have, I decided to communicate with the control panel I salvaged out of a Canon Pixma MX340 multi-function inkjet.

During my teardown I mapped out the pinout of its ribbon cable connector to printer logic board. I then connected my logic analyzer (and other tools) to understand its communication to the best of my ability, with findings summarized here. In theory, all I have to do is to use CircuitPython to tell my KB2040 how to replicate what I observed coming from MX340 logic board, and I will have a control panel I can use for a future project. In practice, I’m sure it wouldn’t be quite that easy but hey that’s the fun part!

Examining my deciphered pinout list, I connected the three pins leading to the NEC K13988 chip on board. Asynchronous serial transmit, receive, and the chip enable pin. I also need to supply +3.3V DC logic power and connect ground, but that doesn’t strictly need to be on the small 1.0mm pitch connector. I found a few capacitors that presented a larger surface for soldering my wires. For this first round I won’t bother wiring up the direct connection to two buttons (Power and Stop) and two LEDs (Power and Alarm) as getting those working should be fairly straightforward for a microcontroller project and I don’t foresee any educational challenges there. I can do that after I tackle the more interesting challenge of CircuitPython serial communication.

I Need More Practice with CadQuery Coordinate Spaces

I thought I might design and 3D-print something to repurpose components salvaged from a disassembled Harbor Freight mini LED flashlight. I still might do that, but another thought occurred to me: for the purpose of CadQuery practice, I don’t need to invent something new, I can try to model the salvaged components themselves.

So I pulled out my calipers and started taking down dimensions for the front window, shiny reflector module, and the circuit board hosting the array of nine LEDs. The front window is trivial, a single flat cylinder, but it got more complex from there.

The good news is that, despite some worrisome early signs, CadQuery has proven to give me better feedback on my mistakes than FreeCAD has. Especially from code centric aspect of Python syntax errors because that gives me a line number of the problem. CQ-Editor also gives a very code centric capability to step through my CadQuery design line by line, which makes debugging much easier than trying to understand where I went wrong in a FreeCAD design.

The bad news is that I have yet to build a mental understanding of how CadQuery coordinate spaces work. I’m frequently confused by parts popping up at positions or orientations different than where I had expected. For some of these mistakes I could see my misunderstanding after the fact, for other mistakes I still don’t understand where I went wrong.

    p = cq.Workplane("XZ")
    p = p.polygon(8,led_ring_diameter, forConstruction=True)
    p = p.vertices()
    p = p.eachpoint(
        lambda pos: (
            ring.add(
                make_led(),
                loc = pos,
                color = color_clear_plastic
                )
            )
        )

In this code snippet, I am working on the XZ plane and I drew a construction octagon on this plane. I then placed an LED at each vertex of that octagon, but they were position on the XY plane instead of the XZ plane.

Why did this happen? I have yet to figure out the root cause. So far I only have a workaround: insert extraneous intermediate CadQuery assemblies for the sake of rotating parts to where I had expected them. I know it isn’t the right tool for the job, but I appreciate CadQuery giving me such tools to bludgeon my way to an imperfect but acceptable state. This is much preferred over FreeCAD’s unhelpful “recompute failed!” leaving me staring at the screen with no recourse.