move posts to folders by year

This commit is contained in:
Matthew Tran
2024-02-04 03:45:42 -08:00
parent cdfe64e9b8
commit 6f801e56cf
107 changed files with 0 additions and 0 deletions
@@ -0,0 +1,10 @@
---
title: Moving to Jekyll
date: 2023-07-03
categories: other
excerpt: After several years on WordPress, I realized my use case would be better suited for a static generator like Jekyll. To better maintainability and security!
header:
teaser: /assets/img/2023/jekyll-logo.png
---
Nothing much to see here. The process was pretty smooth, only had to do a little custom HTML and CSS to get things more right. I used an exporter to get my Wordpress posts into a close enough format for Jekyll, then manually cleaned up each file.
+32
View File
@@ -0,0 +1,32 @@
---
title: Cheatsheets
date: 2023-07-05
categories: school
excerpt: A compiled list of the cheatsheets I made while at Berkeley.
header:
teaser: /assets/img/2023/berkeley.jpg
---
Heres an archive of the cheatsheets Ive made for some of the classes Ive taken. Excuse the small writing and my use of fountain pens. Ironically enough, the one class I didn't get an A in after making these cheatsheets is embedded systems, the very field I'm working and specialized in.
- [Physics 7B Electricity and Magnetism](/assets/pdf/physics7b.pdf)
- [CS61B Data Structures](/assets/pdf/cs61b.pdf)
- [CS61C Great Ideas of Computer Architecture](/assets/pdf/cs61c.pdf)
- [CS70 Discrete Mathematics and Probability Theory](/assets/pdf/cs70.pdf)
- [CS152 Computer Architecture and Engineering](/assets/pdf/cs152.pdf)
- [CS161 Computer Security](/assets/pdf/cs161.pdf)
- [CS162 Operating Systems and System Programming](/assets/pdf/cs162.pdf)
- [CS164 Programming Languages and Compilers](/assets/pdf/cs164.pdf)
- [CS170 Efficient Algorithms and Intractable Problems](/assets/pdf/cs170.pdf)
- [CS188 Introduction to Artificial Intelligence](/assets/pdf/cs188.pdf)
- [CS189 Introduction to Machine Learning](/assets/pdf/cs189.pdf)
- [EE16B Designing Information Devices and System II](/assets/pdf/ee16b.pdf)
- [EE105 Microelectronic Devices and Circuits](/assets/pdf/ee105.pdf)
- [EE106A Introduction to Robotics](/assets/pdf/ee106a.pdf)
- [EE120 Signals and Systems](/assets/pdf/ee120.pdf)
- [EE123 Digital Signal Processing](/assets/pdf/ee123.pdf)
- [EE127 Optimization Models in Engineering](/assets/pdf/ee127.pdf)
- [EE130 Integrated-Circuit Devices](/assets/pdf/ee130.pdf)
- [EE142 Integrated Circuits for Communications](/assets/pdf/ee142.pdf)
- [EE149 Introduction to Embedded Systems](/assets/pdf/ee149.pdf)
- [EE151 Introduction to Digital Design and Integrated Circuits](/assets/pdf/ee151.pdf)
+16
View File
@@ -0,0 +1,16 @@
---
title: FT4232H JTAG
date: 2023-07-24
categories: projects
excerpt: A breakout for the FT4232H so I can do JTAG/SWD.
header:
teaser: /assets/img/2023/ft4232h-jtag.jpg
---
<https://github.com/dragonlock2/kicadboards/tree/main/breakouts/ft4232h>
I had a FT4232H leftover from my [Spartan 7 board](/2021/08/spartan-7-breakout/) and since it's never bad to have too many alternative programmers, I designed a breakout for it. It's got two debug interfaces configurable for either SWD or JTAG. It's also my first programmer that uses the standard pinout I'm using in all boards going forward (JTAG/SWD and UART in the same connector!). Even more, it has software controllable 3v3 rails which are on by default because the indicator LED doesn't add enough pulldown. The layout supports both the QFN and LQFP packages of the FT4232H for that supply chain flexibility. Overall, it was very much a "just works" kind of board.
{% include figure image_path="/assets/img/2023/ft4232h-jtag.jpg" %}
It looks like Xilinx now supports [converting normal FTDI devices](https://docs.xilinx.com/r/en-US/ug908-vivado-programming-debugging/Programming-FTDI-Devices-for-Vivado-Hardware-Manager-Support) into a programmer recognized by Vivado. No more need for stealing EEPROM data from existing programmers!
+44
View File
@@ -0,0 +1,44 @@
---
title: keyboard
date: 2023-07-24
categories: projects
excerpt: First try at a BLE project with the Pi Pico W since SDK support was recently added.
header:
teaser: /assets/img/2023/keyboard-1.jpg
---
<https://github.com/dragonlock2/kicadboards/tree/main/projects/keyboard>
<https://github.com/dragonlock2/miscboards/tree/main/avr/sleepManager>
<https://github.com/dragonlock2/miscboards/tree/main/pi_pico/keyboard>
Since BLE support in the Pi Pico SDK was recently added and I had a cheap 45-pack of mechanical switches, I decided to make a keyboard with it. With 45 keys, it's more of a keypad or macropad, but I digress.
## hardware
The hardware design was overall pretty simple with a lot of schematic reuse from prior projects. The Pi Pico W is very straightforward to work with since the pin choice is so flexible. One mistake I made was not using consecutive pins for the WS2812B outputs which meant I needed more PIO state machines in the firmware. To have low quiescent draw but also an inactive sleep timeout, I used an ATtiny to act as a soft switch. It controls a high-side NMOS with a charge pump driver. Since there's no battery protection circuitry, I at least added battery voltage monitoring.
The WS2812B-Mini were a huge pain to work with since they kept breaking in the oven or with external heat applied. I eventually had to salvage the larger WS2812B from a leftover strip I had and fit them onto the smaller footprint while also squeezing underneath the mechanical switch.
{% include figure image_path="/assets/img/2023/keyboard-3.jpg" %}
## firmware
Starting with the firmware on the ATtiny, it was relatively straightforward. Probably because the Homebrew version needs updating, I keep running into linker script issues where the regions aren't placed correctly. I originally wanted to add a watchdog for the Pi Pico to pet but ended up just having the Pi Pico signal back to the ATtiny when it wants to sleep.
For the Pi Pico, working with the SDK was pretty nice. The CMake based build system fit in nicely with how I tend to do baremetal and Zephyr development. USB debugging was convenient with `stdout` piped over USB while the USB HID devices remained working. Had I put in the effort, I could have also programmed over USB without needing to hold the BOOTSEL button and power cycle. It was also my first time since Arduino using C++ in a project. I didn't end up using many C++ features, mostly just using classes to build abstractions.
The WS2812B support was done using the PIO and I basically just used a slightly modified version of the standard implementation. Since I put the output pins on non-consecutive pins, I had to use a PIO state machine for each.
Implementing the encoder was far more complicated than I initially thought. Since I used a mechanical encoder there was a significant amount of noise that required debouncing. I eventually learned that using a Gray code and a state machine to decode worked best. However, it also required a high sample rate so I used a PIO state machine to handle it. As my first real PIO program, I learned a lot about the limitations and features of the PIO instruction set. To reduce bandwidth needed, I only pushed samples to be processed if any pins changed. With 2 pins sampled, we can fit 16 samples into each 32-bit word of the FIFO. To be responsive, I also forced the state machine to flush current readings every time I wanted to process the encoder.
BLE support was a huge pain especially since I wasn't sure what was a bug in my code or in the BTstack port. One huge issue was that the asynchronous task handling BTstack things would end up running on the other core and somehow hang the entire program. I had to switch to a single core program because of this but thankfully I had more than enough extra performance to keep things responsive. Eventually after wading through poor documentation and much code, I had something working and was able to send all of the HID over GATT reports I wanted. BLE pairing was unreliable and didn't work at all on macOS, but once I update the SDK it'll probably work.
USB support was far easier and mainly involved getting the HID descriptors set up correctly. TinyUSB is pretty well documented. Unlike BLE, I even got the LED report working. One interesting thing was how macOS, Windows, and Linux handled media controls differently. In order for the encoder ticks to match up with volume I needed to ensure at least one HID report with the key pressed was sent before unpressing it. I also did this for BLE.
The SSD1306 driver was relatively straightforward as I copied my previous implementation. I even used DMA again which was quite awesome. Interestingly, I was able to push it to 3MHz I2C.
The keyboard matrix algorithm was pretty standard with my own twist for the debouncing. In order to register presses immediately, I would register the initial state change and not register future changes for a short debounce period. This would ignore any bounces but also remain responsive.
On top of all of these low-level drivers, I built the macro system and GUI. Those aren't as great so I won't go into much detail. There's definitely a lot more room for customization.
{% include figure image_path="/assets/img/2023/keyboard-2.jpg" %}
@@ -0,0 +1,17 @@
---
title: Revisiting Tag-Connect
date: 2023-07-24
categories: projects
excerpt: Recreating patented technology yet again but this time using OSH Park for the PCBs.
header:
teaser: /assets/img/2023/tagconnect-3.jpg
---
<https://github.com/dragonlock2/kicadboards/tree/main/projects/tagconnect>
Like in my last [post](/2020/10/diy-tag-connect-cable/), the Tag-Connect technology is still patented for about another 10 years. What I'm doing is legally questionable but ethically not.
Since backups are important, I decided to design a version that can be sent to a normal PCB fab like OSH Park. Tolerancing the 1mm holes was the biggest worry but thankfully it turned out ok. Going a little bit tight turned out to be a benefit since it helped keep things super aligned. Since 2x3 1.27mm headers are hard to come by, I also used this as an opportunity to do my first flex PCB design and develop a standardized pinout I can use across all my future boards.
{% include figure image_path="/assets/img/2023/tagconnect-2.jpg" %}
{% include figure image_path="/assets/img/2023/tagconnect-1.jpg" %}
+21
View File
@@ -0,0 +1,21 @@
---
title: solarFRAM
date: 2023-07-24
categories: projects
excerpt: First time trying solar power, PIC, and FRAM.
header:
teaser: /assets/img/2023/solarfram-2.jpg
---
<https://github.com/dragonlock2/kicadboards/tree/main/tests/solarFRAM>
<https://github.com/dragonlock2/miscboards/tree/main/pic/solarFRAM>
Since I bought a solar panel, low-end PIC, and FRAM for fun a long time ago, I decided to make a board to combine them all. Essentially, this board uses solar power to turn on a PIC which tests FRAM endurance until the capacitor depletes. If the capacitor doesn't go out, this will probably outlast me.
To harness solar power, we simply pipe the output through a diode into a supercapacitor. It's far from the most efficient method which would employ something like an MPPT, but one can only do so much with parts I had on hand. Since the solar panel output voltage was in the ~0.5V range, I needed a boost converter for which I had a TPS61201. I did some basic conservative calculations to set the UVLO and supercapacitor size in a way that would deliver enough energy into my circuit for at least a noticeable amount of time. One issue was that the output voltage was fixed at 3v3 but my FRAM needed 5V. Noting that the TPS61200 has an adjustable output, I correctly assumed that the TPS61201 is the same just with an internal divider. By adding an external potentiometer, I was able to force the TPS61201 to output 5V! The rest of the circuit was typical microcontroller stuff, not much to say there.
The software side was interesting as this was my first PIC project. Getting CMake set up was a pain and involved importing another repo since the process isn't simple. Definitely sticking to AVR in the future. I ended up bitbanging I2C (and UART for debugging at some point). I also downclocked everything to keep the power draw low.
{% include figure image_path="/assets/img/2023/solarfram-1.jpg" %}
Overall, I'm happy with the project. My conservative calculations were pretty off and it doesn't turn on as quickly or last as long as I'd hoped and only does so in sunlight or a bright flashlight, but it works! Perhaps I'll try another solar powered project in the future.
+47
View File
@@ -0,0 +1,47 @@
---
title: Lightsaber v5
date: 2023-07-25
categories: projects
excerpt: With much more experience under my belt, I challenged myself both in size and processing power in the next version of my lightsaber.
header:
teaser: /assets/img/2023/lightsaber-v5-1.jpg
gallery:
- image_path: /assets/img/2023/lightsaber-v5-3.jpg
- image_path: /assets/img/2023/lightsaber-v5-4.jpg
- image_path: /assets/img/2023/lightsaber-v5-5.jpg
---
<https://github.com/dragonlock2/kicadboards/tree/main/projects/lightsaber>
<https://github.com/dragonlock2/miscboards/tree/main/avr/lightsaber>
This project has certainly been a long time coming with work actually starting over a year ago. I just finally found time to finish it. This is finally a design that is easily reproducible and also the first time I really understood all aspects of the design down to the fundamentals. As always, my additional challenge was to not buy any more parts than what I already had available.
## hardware
As usual, the hardware design was relatively straightforward with some interesting caveats. To minimize sleep current, all peripherals can be turned off with power controlled by the ATtiny1616. The fun part was using the motor driver STSPIN250 as a speaker driver. Class D amplifiers are usually built this way but with an LC filter tuned for high impedance at the carrier frequency to improve efficiency. Considering the LED strip would draw several amps, I ended up not having an LC filter. Audio quality (after switching away from the awful LCSC speakers to one salvaged from a laptop) was surprisingly good even with a 25kHz carrier.
The first mistake I made with the hardware design was not adding voltage dividers to the QSPI flash input. The ATtiny1616 runs at 4.2V while the QSPI flash runs at 3.3V which definitely exceeds its limit. My jank fix was to add pull-down resistors to the ATtiny1616's outputs so that a high voltage was around 3.6V instead. I'm still within the package current limit so I should be safe. The second mistake I made was not soft starting the WS2812B connection which has a surprisingly high inrush current. Increasing the series resistor to 1MΩ ended up working. I also made a mistake with the STSPIN250 pinout but thankfully that was fixable by cutting some traces and some soldering.
{% include figure image_path="/assets/img/2023/lightsaber-v5-2.jpg" %}
## firmware
I actually worked on the firmware first on a dev board to check that my hardware architecture would be viable. Getting CMake set up for the ATtiny1616 and ATtiny817 was a huge pain due to lack of support in the normal Homebrew installation. I had to copy a bunch of files and even build avr-gcc to get the right files to make everything work. The I2C driver, bit-banged WS2812B driver, animation system, and power management were all relatively straightforward. I even had a decently clever UI using just one button for everything. Surprisingly, I was able to run everything at 50Hz with some extra compute to spare so I was set.
The audio was my biggest worry with the first step being file storage. I initially tried implementing the SD SPI protocol and was successful but it became quite clear that there would be a lot of overhead. Since the SPI protocol to access QSPI flash allows me to stream contiguous blocks of memory with basically no overhead, I went with that instead. I developed a very basic "filesystem" called LightFS to store multiple fonts. The implementation involved reading one byte from SPI and writing it to PWM at 25kHz. I still need to work out the audio popping when transitioning between audio files, but overall it was quite the success.
## mechanics
The first part of the mechanics was the blade and socket design. Taking inspiration from the community, I had one board with many pogo pins and another with pads. By using concentric rings, blade rotation doesn't matter.
The next step was the external design which took inspiration from Exar Kun's double-bladed lightsaber. Since a 6" tube was too short, I had to extend the blade socket area about 20mm past the end of the tube. It'll definitely need to be 3D printed from metal if I ever want to fight. With the blades in the way, I had to tastefully cut slots in the casing to let the sound out.
The final part was the internal chassis that held all of the electronics. I took a cylinder and cut out all of the spaces for the parts and cable routing. Since I need this design to be repairable and the charge port is inside the chassis, I made sure to keep things neat. To avoid further holes in the metal casing, I mounted the control switch to one of the sockets and made it disconnectable.
Overall the project was a success!
{% include gallery %}
{% include video id="NGLt8ukFE4w" provider="youtube" %}
{% include video id="5Cayb8j8YM4" provider="youtube" %}
+33
View File
@@ -0,0 +1,33 @@
---
title: OSRO (Open-Source Reflow Oven)
date: 2023-07-26
categories: projects
excerpt: An easy to deploy hardware and software solution for converting any oven into a Wi-Fi enabled reflow oven. This is just one.
header:
teaser: /assets/img/2023/osro-3.jpg
---
<https://github.com/dragonlock2/OSRO>
Having used my original [reflow oven](/2018/01/pcb-reflow-oven/) for several years now, I decided to build a better one. As is the theme with my recent projects, it's important that what I design from now on be easy enough to reproduce. Since I didn't want to add a screen or buttons, I used an ESP32-C3 (purely for the RISC-V, go bears!) and gave it a web UI. I actually started this project almost two years ago, completing both the hardware and initial firmware. I can't believe it took this long to actually deploy it but here we are.
## hardware
As per usual, the hardware design is rather simple except for the power part. Using an ESP32-C3 module with an external antenna meant not having to worry about the RF layout. It even supports programming and debugging over the USB connection for maximum convenience. To avoid needing an external 5V supply, the board contains a 5V converter. The ZCD (zero-crossing detector) is made from an optocoupler with appropriately sized resistors which basically just signals when the absolute value of the AC voltage falls below some threshold. I tried properly calculating the thermals and triggering of the TRIAC this time around. An optocoupler provides an initial gate current until the TRIAC latches on. Very importantly, the layout was done to maintain enough isolation gap between the control electronics and the high voltage stuff. I also used isolated TRIACs (at the cost of thermal performance) to not worry about a live heatsink.
With initial testing with a small heater, I blew a couple of optocouplers and traces. That's when I realized TRIACs from Amazon were fake or at best out of spec parts. The TRIACs failed to turn on which meant the optocoupler took all of the load. After emergency shipping legitimate TRIACs from Digi-Key, it worked perfectly!
## firmware
My initial firmware from nearly two years ago was pretty basic and inflexible so I refactored it completely. The Wi-Fi module supports connecting to anything ESP-IDF supports, including WPA2-Enterprise networks like eduroam. It adds a console command for changing the Wi-Fi connection but requires a recompile to make anything permanent. The server module creates an HTTP server that serves the web UI and a JSON REST interface to control the oven submodule. The oven module handles PWM of the heater (using the ZCD to switch at zero crossings and reduce noise), reading temperatures, and running a PID loop. The oven plays profiles by hooking in to the profile module and requesting the current target temperature. This was also my first time playing with FreeRTOS and I have to say it's nice and minimal.
The web UI is something I'm quite proud of as my first React project. It was definitely a pain though, I won't be doing this often. The development process involved doing all of the UI and business logic in JS and all of the styling in CSS. I put quite a lot of effort into making the UI look good even on mobile. One interesting caveat with JS is that code can stop running when the tab goes out of focus. This messed with my temperature sampling and I ended up having to timestamp each sample.
{% include figure image_path="/assets/img/2023/osro-1.jpg" %}
## mechanics
Since I didn't want to deep clean a used oven, I ended up buying a brand new one to convert. I settled on the popular Dash oven because it's tiny and cute. It was a bit finnicky to get mounted, but I got it. Liberal amounts of thermal tape were used to move heat from the TRIACs to the casing. After many tries, I got the PID tuned manually. One day I'll learn how to do it automatically. Overall, the project was successful!
{% include figure image_path="/assets/img/2023/osro-2.jpg" %}
{% include figure image_path="/assets/img/2023/osro-3.jpg" %}
@@ -0,0 +1,40 @@
---
title: Basic LPC845 Zephyr Support
date: 2023-12-28
categories: projects
excerpt: While still lacking in many features, I've added just enough support to get the LPC845-BRK and my own LIN breakout working.
header:
teaser: /assets/img/2023/lpc845_lin.jpg
---
<https://github.com/dragonlock2/zephyrboards>
<https://github.com/dragonlock2/kicadboards/tree/main/breakouts/lpc845_lin>
Although I now make more than enough money to buy almost any development board, I still like buying the cheapest ones and seeing what they can do. For around $6, the LPC845-BRK was an easy choice. It's got the standard set of peripherals, with the main standout being the switch matrix (SWM) which allows virtually any pin to map to any peripheral function. The LPC1549 also has this feature, but I'm not aware of any others. It seems like an expensive feature to add in silicon.
After working with several different microcontroller families at `$DAYJOB`, I realized that, outside of a few architecture-specific gotchas, everything is fundamentally the same. Once one understands that, and can write well-abstracted code, moving between microcontrollers isn't so daunting. This is especially true with ARM, where everything from the interrupt model, the programming interface, and the ISA are standardized. Vendor IDEs can provide nice tooling to speed up development, but generally aren't crucial. The shell, a good text editor, and the reference manual are really all one needs.
## Zephyr support
Given that it's another ARM processor, the LPC845 was the perfect candidate for my first attempt at supporting a new microcontroller in Zephyr RTOS. With excellent out-of-tree support, I didn't even have to branch off of mainline to do this. The following is all of the files I had to add.
- `boards/arm/lpc845brk`
- This folder contains the standard set of board support files. Here we define the supported debugger, default set of Kconfig selections, and the devicetree. Importantly, you need to select the microcontroller in Kconfig and import the default `.dtsi` in the devicetree. One gotcha with this microcontroller is the needed checksum to boot, which we can add using CMake to call the correct command. The other gotcha is that flashing with pyOCD sometimes fails unless I first run OpenOCD to "unlock" the chip. Must be something nonstandard.
- `drivers/`
- Here we can add drivers that implement the appropriate API. Zephyr has helpers and subsystems that build upon this low-level API implementation. I've added ones for `gpio` and `serial`. Since NXP, as well as most other vendors, provide a HAL that abstracts away the low-level register manipulation, most drivers, including the ones I wrote, are mostly thin conversions between the Zephyr API and the vendor API. The most complex part is usually managing state and interrupts since we have to be as flexible as the API requires.
- `dts/arm/nxp`
- This folder contains the default set of devicetree configurations that boards import. This is where we define the CPU type, flash size, RAM size, and available peripherals. For each peripheral, we generally need to specify the driver, memory address, clock, and interrupts. Since I didn't use the `pinctrl` system most other microcontrollers use, I also specify the SWM pin functions for each peripheral here.
- `dts/bindings`
- This folder contains a set of YAML files that define what properties are available to set in for peripherals in the devicetree. These are the properties that the drivers need when doing their configuration. The devicetree parser will enforce any requirements and defaults.
- `include/zephyrboards/dt-bindings`
- This contains a set of header files that can be imported into devicetree configs to provide constants. Since full C isn't supported in devicetree, ports usually copy constants here for use. I added peripheral clock mappings in `lpc84x_clock.h`. Since I didn't want to figure out the whole `pinctrl` system, I added constants to `lpc84x-pinctrl.h` for pin selection and let the drivers do more of the heavy lifting for pin configuration.
- `soc/arm/nxp_lpc/lpc84x`
- This is where support for the base microcontroller is added, without any peripherals. Here we use Kconfig to configure the number interrupts, choose the CPU architecture, select any architecture-specific options, and enable drivers. Importantly, I believe the `HAS_MCUX` flag triggers the NXP HAL to be compiled, although I still had to add a few files specific to the LPC845 in `CMakeLists.txt`. The linker script is also here, but we simply import the architecture specific one. Last but not least, clock configuration is generally done in `soc.c` which I kept simple and just used the 30MHz internal oscillator.
Doing all of the above can sound quite daunting. The first step is really just to get the microcontroller booted and running `main()` at all, no `gpio` or `serial` drivers needed. You can use the debugger or even just call into the vendor APIs to check this. To get here, you really only need the boilerplate board files, basic devicetree without peripherals, and the basic set of Kconfig selections in the `soc` folder. Once you have Zephyr booting, then you can proceed to add driver support for each of the peripherals. Remember, you can hardcode as much as you want during development. Start from a known good state, then change things one at a time.
## lpc845_lin
Since I had a couple of LPC845 samples laying around, I decided to build a board. I've always wanted another LIN board to use with JABI, so I designed a USB to 4x LIN converter. Since I didn't have any LIN transceivers, I tried designing my own. The first design is what I used with [attiny10_lin](https://matthewtran.dev/2022/12/attiny10-lin-node/) which uses a single FET level shifter. Since current sink capabilities are limited by the IO pin used, I did a second design which uses two BJTs for TX and one diode for RX. The layout was definitely easier due to the switch matrix, but probably not to the extent that justifies the additional cost.
As expected, it mostly worked! I had to do a complete refactor of my LIN API since it required a ridiculous amount of memory for storing queued frames. The LIN transceivers both break the LIN specification since the slew rate is too steep and voltage levels defining high and low are far stricter than required. I also didn't have a CP2102N and only a CP2102, so couldn't test the LPC845 bootloader. Still, very happy with the outcome.
@@ -0,0 +1,69 @@
---
title: Baremetal C/C++ on CH32V (including FreeRTOS and TinyUSB)
date: 2023-12-29
categories: projects
excerpt: Developing for CH32V using 100% open-source components, true to the essence of RISC-V.
header:
teaser: /assets/img/2023/ch32v203.jpg
---
<https://github.com/dragonlock2/miscboards/tree/main/wch>
I'm a little late to the party, but I recently bought a bunch of CH32V003 and CH32V203. Unlike other Chinese chips, these ones are easily acquireable so I can actually use them in projects. With it's bare minimum set of features (UART, SPI, I2C, ADC) and low cost, the CH32V003 is a worthy alternative to low-end AVR. By adding USB, CAN, and a more powerful core, the CH32V203 replaces a lot of mid-range ARM chips. There are more chips in WCH's lineup with even more interesting features, but these are the ones I'm starting with.
Instead of using MounRiver Studio IDE or even the entirety of the vendor SDK, I decided to build a toolchain myself. First was the compiler. I initially tried compiling LLVM and GCC from source, but ran into issues bootstrapping libc. I eventually gave up on LLVM since it doesn't yet have RV32E support in mainline. I ended up using [`riscv-collab/riscv-gnu-toolchain`](https://github.com/riscv-collab/riscv-gnu-toolchain) which has scripts to streamline building up a toolchain. Next was OpenOCD, which I had to request the source code of from WCH since no one had done so recently and I needed WCH-LinkE suppport. After realizing some of my WCH-LinkE came unflashed and then flashing WCH's provided bootloader and app binaries, I had a set of working debuggers.
From here, I put together a set of CMake scripts to ease the build process and bringup of new projects. I won't go into too much detail on the CMake front since there's better tutorials out there and my source code is available. Instead, I'll go over the more interesting parts. The process for CH32V003 and CH32V203 are more or less exactly the same.
## linker script
The linker scripts provided by the vendor SDK were quite bloated, hard to read, and contained a lot of legacy parts. They appeared to be copied straight out of SiFive's examples. Wanting more control and a learning experience, I wrote my own. After specifying the ROM (flash) and RAM regions, these are the crucial sections to get C working.
- `.text` - This is where all of your functions get placed by default. I also put `.rodata` here which refers to read-only constants, but some people do split it off into a separate section. Notably, I placed `reset_handler` at the very beginning of ROM, which I'll go into more detail later.
- `.data` - Variables that have an initial value live here. Importantly, the initial values live in ROM and must be copied into RAM by the startup code. I also put the value of `__global_pointer` here, offset 2048 bytes from the start of RAM, to allow linker relaxation.
- `.bss` - Variables that are zero-initialized live here. Since the hardware doesn't do it, our startup code will zero out this section on boot.
Importantly, we also define a couple of symbols (`_data`, `_edata`, `_data_rom`, `_bss`, _`ebss`) that our startup code will use to setup `.data` and `.bss`. `_end` and `end` get used by the default implementation of `sbrk` to provide a heap. While I've seen many people allocate a stack section, I simply have a marker for the top of stack `_eram` and acknowledge that any RAM remaining after `.data` and `.bss` are allocated become my heap and stack.
If you're using C functions or C++ constructors that run before `main()`, you'll need to add the following sections.
- `.preinit_array` - This is an array of function pointers that run before things in `init_array` do.
- `.init_array` - This is an array of function pointers that run before `main()`.
- `.fini_array` - This is an array of function pointers that run after `main()` returns. Since `main()` rarely returns, we can consider this optional but I left it in to be complete.
It took several hours of debugging to get C++ exceptions working, especially with such poor documentation from the internet and ChatGPT. Eventually, I realized it boiled down to adding the following sections (to ROM). Funnily enough, this even works with Newlib-nano which is supposed to lack exception support by default.
- `.gcc_except_table`
- `.eh_frame`
## startup code
As is my style, I opted to write the startup code in C instead of pure assembly. Here we define `reset_handler`, which we placed at `0x0` in the linker script. From the reference manual, CH32V starts executing instructions from `0x0` at boot. Since the interrupt vector table also lives at `0x0`, this is typically a jump instruction to the `reset_handler`. However since I intended on relocating the vector table, I just let `reset_handler` occupy `0x0`. Notably, we mark `reset_handler` as `naked` since we don't need to save any registers as no one else calls it and also with `O1` optimization to prevent inlining that was causing things to write to the top of stack.
In the `reset_handler`, we start by setting up `gp` and `sp` as this chip doesn't do that for us. Then we can call `memcpy` and `memset` to setup the `.data` and `.bss` sections. Next, we modify a couple of CSR registers, one of which I haven't a clue what it does since it's undocumented. Notably, this is where we enable interrupts and interrupt nesting. We also setup the vector table here which I placed in RAM for flexibility. After that, we call `libc_init_array()` to run functions in `.preinit_array` and `.init_array`, then `main()`, and finally `libc_fini_array()`. We end everything with a `while (1);` to prevent further execution.
As an aside, my decision to relocate the vector table to RAM does invite discussion. Most applications won't need the flexibility and would be better served having it in ROM. If needed, I'll probably add a compile flag in the future to enable a constant user-provided vector table. The need to relocate at all I would say is useful if a bootloader that requires interrupts ends up being used.
## drivers
Register definitions for each of the peripherals are the one place where I'll almost always use the vendor provided ones. Those things can be quite a pain to write from scratch and easy to mess up on. I've written many drivers from just the register definitions, but in this case WCH provides a decent set of them in their vendor SDK which I reused.
I did end up writing the clock setup functions myself since the vendor SDK ones appeared to assume starting from boot. Since I wanted to eventually work on bootloaders which might use a different clock configuration, I wrote my own clock setup functions which made no assumptions about the prior config. Basically, we enable the internal oscillator and slow down the flash access before switching to it. From there we have a known good config to proceed from.
## FreeRTOS
Since CH32V appears to have a pretty nonstandard RISC-V implementation due to it essentially being an ARM microcontroller with the CPU swapped out, I decided against adding Zephyr support for now. Instead, I decided to get FreeRTOS working. RISC-V support in FreeRTOS is pretty basic, with no support for interrupt nesting. It also uses a dedicated ISR stack (which is just the typical non-RTOS stack) to avoid tasks each needing enough stack space for interrupts. FreeRTOS also hooks into all of the interrupt handlers, at least those that need FreeRTOS, and calls an application-specified handler with the value of `mcause`. Since the FreeRTOS handler does register saving, I didn't need an `__attribute__((interrupt))` for my handlers and only needed to make the application-specified handler call functions from my original vector table.
While the initial port was easy, I kept running into a memory corruption bug with multiple tasks enabled. Even worse, the bug could take up to a minute to manifest. After many hours of debugging, I realized that `ecall` (used by `portYIELD()` similar to how ARM uses a `PendSV`) could preempt even if interrupts were disabled, so I used the software interrupt method WCH used in their SDK. Not liking this hacky way, I eventually realized that I had to modify the INTSYSCR CSR register to disable interrupt nesting. Just like that, I got mainline FreeRTOS working!
## TinyUSB
At a low level, USB is actually not so different from other commander-responder protocols like LIN. USB endpoints are essentially like LIN message IDs and can be written to or read from. The only difference in software is that since it's so much faster and requires lower latency, DMA is more or less required. Packets need to be precomputed rather than done on the fly.
Due to how well-documented it is, adding TinyUSB support was pretty straightforward. I simply followed the [porting guide](https://docs.tinyusb.org/en/latest/contributing/porting.html). I drew a lot of inspiration from the CH32V307 USB HS driver, with a few notable differences. While the USB HS IP can specify a separate TX and RX buffer for DMA, the USB FS IP in the CH32V203 needs a combined 128-byte TX/RX buffer, with data placed in different halves. This necessitated allocating buffers in the driver instead of using application provided ones.
While working on audio streaming, I realized the USB HS driver doesn't have isochronous endpoint support, so I added it to my USB FS driver. The only difference is not requiring an ACK from the host. It was at that point where I realized USB supports up to 1023-byte packets that are only used by isochronous transfers. Bulk transfers split things up into 64-byte packets. The reference manual and vendor SDK didn't detail how the TX/RX buffer should be setup, so by guessing and checking I realized that it still assumed a 64-byte split which means received >64-byte OUT packets would overwrite queued IN data. A notable limitation, but not one that affects most applications.
## conclusion
This project was a complete success! It was an excellent learning experience and I had to really dig into the implementation of exceptions in C++, task switching in FreeRTOS, and low-level USB implementation. Next, I'll look into Zephyr, LLVM, and Rust support.
+34
View File
@@ -0,0 +1,34 @@
---
title: RViCE ADC
date: 2023-12-29
categories: projects
excerpt: First time using a RISC-V microcontroller, iCE40 FPGA, and an LTC2320 ADC. It streams 8 channels of differential 16-bit 48kHz ADC readings over USB as audio.
header:
teaser: /assets/img/2023/rvice_adc.jpg
---
<https://github.com/dragonlock2/kicadboards/tree/main/projects/rvice_adc>
<https://github.com/dragonlock2/miscboards/tree/main/lattice/rvice_adc>
<https://github.com/dragonlock2/miscboards/tree/main/wch/rvice_adc>
Looking through old parts in my stock, I came across the LTC2320 which I had samples of from years ago. Perusing the datasheet, it appeared that its protocol did not lend itself to a simple implementation on a microcontroller. Well, it does recommend using an FPGA. This was the perfect opportunity to build my first board that legitimately needed an FPGA. I also wanted to use a CH32V203 in something, so I had my project idea.
## hardware
As always the hardware design was relatively simple. Just a matter of reading the datasheets and putting my blocks together. Due to the higher complexity, I organized everything into separate sheets. The ADC inputs needed anti-aliasing RC filters, which in hindsight need a cutoff frequency closer to 48kHz. I reused the power sequencing design from my [Spartan 7 Breakout](https://matthewtran.dev/2021/08/spartan-7-breakout/). The CH32V203 design was just like any other microcontroller. I used a bitbanged SPI to write to the flash chip and 2x SPIs for streaming ADC data from the FPGA. Looking back, I could probably come up with some sort of parallel interface using the GPIO to increase bandwidth. The iCE40 design was straightforward, but like the Spartan 7 took great care to ensure I wasn't messing anything up. The LTC2320 ADC design was also straightforward and basically pulled straight from the datasheet.
As my first hardware design since March, I did a pretty good job especially with the layout and assembly. I used mostly 0402 components and a couple of 0603 capacitors that may or may not be smaller than recommended. Everything ended up fitting within two layers, even with the sheer number of nets connecting the chips. For assembly without a stencil, I used tweezers to pick and place tiny blobs of solder. By adding a minimal amount of paste across pads, not even on individual pads, I even got QFP soldered with almost no bridging.
## software
Writing the firmware was really where the real complexity lies. This was also my first time using C++ in a more idiomatic way. I've documented the majority of my trials in [Baremetal C/C++ on CH32V](https://matthewtran.dev/2023/12/baremetal-c-cpp-on-ch32v/), but I'll add more detail here. As always, I started by getting `printf` debugging working. Then I moved on to writing to the SPI flash. It took me way more hours than I'd like to admit to realize that the iCE40 was putting the SPI flash to sleep which explained why I didn't get any responses to my commands. After fixing that, I added a very basic RPC over UART to test writing the flash via a Python script. Once that worked, I wrote some very basic Verilog built with the fully open-source Yosys toolchain to test the FPGA. Since UART is quite slow, I then moved the RPC to USB bulk transfers which brought the flashing time down to \~2s. Considering the synthesis time would be longer, this was fast enough. Interestingly, switching from C-style arrays to `std::array` and templates in the bitbanged SPI actually saved \~0.5KB of flash.
After that, I started working out how to get the ADC data out over USB. After learning about USB audio and doing the calculations, I realized I could get 8 channels of 48kHz, 16-bit data out over the USB FS connection. The ADC can do up to a 1.5MHz sample rate, but that would require USB HS and a lot more care on the FPGA side. TinyUSB provides decent audio examples, but getting the full 8 channels working was quite painful. It took a while to realize that isochronous endpoints do everything in one large packet without an ACK and then adding that support in my USB driver. It took a bit more time to realize that the FIFO buffer needs to ensure alignment of the channels, no partial channel writes, or else the OS could get confused and silently fail. After considerable effort and extensive use of Wireshark USB monitoring, I got it working.
From there, I figured out how to get the ADC data off the FPGA. I designed in 2x SPI interfaces, but based on the bandwidth requirements I only needed one. Since the CH32V DMA doesn't support variable strides, using only one also meant not needing to post-process buffers before sending out over USB or alternatively a much more complex Verilog implementation. One somewhat unnecessary worry was desynchronization between the microcontroller and FPGA SPI. What if it somehow ended up one bit off? To fix this, I had the FPGA assert on the CS line to signal that at least 1ms of samples was in its FIFO. At this point the SPI should be idle, so we can also resynchronize to the correct bit and sample boundary. Then the microcontroller would pull off 1ms worth of samples from the FIFO over SPI. During testing one annoying bug was that after flashing, samples could end up shifted one channel over. The samples were perfectly synced on an oscilloscope so I eventually realized this was caused by a DMA overrun where I started one before the prior one finished, leaving one sample in the SPI register that wasn't easily discarded. Since my bitbanged SPI was in a critical section, this overrun made sense. The fix was simple, only start a DMA if the previous one finished.
Next, I worked on the Verilog for the FPGA. I would've used Chisel, but wanted to keep things simple. I started with PWM RGB and the PLL to get my feet wet. I ran it at 36MHz as it is an integer multiple of 48kHz. I didn't realize the PLL doesn't work below 10MHz and switched to a 72MHz clock from the microcontroller. Then I worked on the SPI which was difficult to sync because it had to work at 9MHz. Since the clock phases might not align, I had to add an input buffer on the clock to make things work. Due to that added delay, I actually shifted out the next data bit on the rising edge of the clock in order to get it physically shifting on the falling edge. After that, I added a very basic FIFO which was easy to implement. The one caveat was that iCE40 block RAM needs an extra clock cycle to read. From there, I worked on the ADC protocol. I used a state machine triggered by the 48kHz timer. The only bug I had to deal with was the samples coming in shifted. I was at home for the holidays without an oscilloscope, but eventually tracked it down to Yosys using 32-bit math on constants causing my CNV timing to be off. After that everything worked!
The LTC2320 is a pretty incredible part. Low noise, high resolution, high bandwidth. No wonder it's nearly \$40.
{% include figure image_path="/assets/img/2023/rvice_adc-2.png" %}