Dungeon Controller
Automation system for my “dungeon”/workshop/lab for automatic handling of lighting, temperature, and air quality.
Cool Features
- On COLD days, kick on the heater before work.
- Super bright, super diffuse light when I’m building stuff.
- Light that follows a reasonable schedule. Super dim late a night. Auto-fade at 8pm to prepare for sleep.
- Light turns on/off based on PIR motion detectgors.
- Push button overrides on all important settings.
- Auto-detect dust and engage super filter.
- Auto-detect soldering iron usage and engage solder air filter.
Specifics
- STM32 HAL-based FreeRTOS
- (5) Custom PCBs
- (2) Custom laser-cut enclosures
- Ceiling-mounted LED strips are dimmed based on a schedule
- Soldering Iron vibration kicks on air filters
- When I leave the room, the air filters kick on and the lights turn off.
Code
- STM32 HAL-based FreeRTOS
- Non-blocking peripherals used throughout to adhere to FreeRTOS standards.
- Custom drivers written for PMS5003 dust sensor, LM75B temperature sensor, MCP23017 expander, Real-Time Clock, 25LC256 EEPROM,and BME680*. (The BME680 is currently on pause.)
- Defensive coding used throughout.
Problems and Solutions
- RAM and Flash exceeded
- I2C callback issues inherent in silicon of STM32F103C8T6
- Digikey Delays, Shipping Delays, Ordered-wrong-damn-microcontroller delays
Custom PCBs
- Dungeon Controller PCB
- Front Panel PCB
- Relay PCB
- 24V Splitters for LEDs
- Solder Vibration Detector
3D Modeling
- PIR holders
- LED strip power housing
- Large enclosure
- Relay board enclosure
- Soldering Iron Vibration base for FX-888D
- Oddball PCB Standoffs
Code
STM32 HAL-based FreeRTOS
This project didn’t need RTOS, but I did. So I jumped in with both feet. The war was supposed to last 4 days, but it took much longer….and is still kinda going. (Sound familiar?)
I ended up with VSCode and Segger J-link firmware on Nucleo board as my programmer. I used Segger fprint() and VSCode for most of my debugging, although I setup Segger Ozone for quick access if needed. I really didn’t need it that often.
I got my feet wet with FreeRTOS using a Nucleo 767zi, but created a custom PCB using the STM32F103C8T6. I blew out both the RAM and the flash on it and moved up to a STM32G0B0CET6….which didn’t fit on my new PCB so then I ordered the correct STM32G0B0RET6.
Non-Blocking Peripherals
STM32 HAL gives the option to use blocking functions for UART, I2C, SPI, etc. Those are for wimps…or people in a hurry.
This project exclusively used the non-blocking, interrupt-based transmit and receive version.
In the asynchronous UART, writing the driver for this was considerable effort that required catching the random incoming data from the ISR callback and storing in a queue which would later be processed by a “gatekeeper” task. Sending data required a similar process. Because I needed two serial ports for this project ( the PMS5003 runs at 9600bps ) , I did not want to repeat the code twice. After considerable research, I developed a driver that scales with the number of required UARTs with no additional effort beyond initial declarations and callback configurations.
Too bad that confusing driver that can dynamically generate UART driver instances was really only suited for text. I needed to catch 32 bytes at a time from the PMS5003 and ultimately the IDLE version of the HAL_Receive_DMA_Idle() worked with a little coaxing.
I ran into endless errors from overruns on the UART and after I addressed those, the callback quick working. I’m still working that out as of this writing. It seems to work fine in the afternoon, but not in the morning so maybe it’s related to temperature. SMILIE
The SPI and I2C drivers were considerably simpler in comparison.
Defensive Coding
I was introduced to this idea of “Defensive Coding” where I assume every line of code is trying to kill me. The metaphor in The Pragmatic Programmer about that feeling that every other driver on the road was trying to kill me HIT HOME 100%. Assume that car on the perpendicular street headed right for you is not going to stop. They want you dead. Apply that same logic to everything that can kill you in code. It’s a jungle out there.
The beauty of Defensive Coding is you know exactly where your code stands and you know who broke it. (Me!) By placing config_assert{0} for functions that failed, the program stops immediately. There’s no need to limp on a broken leg when that broken leg is somehow causing my hearing loss. Just fix the leg and maybe the hearing damage goes away by default. This kind of discipline is annoying sometimes but useful.
I have a lot to learn about Defensive Coding, but so far I LIKE IT!!
Fun With Unions
Wow! What a powerful concept! You’ve got a chunk of data. Sometimes you just want to loop through it numerically..(useful if you are storing it in an EEPROM). Sometimes you want to access it by name…(most of the time). Unions, more or less, allow you to create an alias so that you can refer to that chunk of data either way.
Back in my PHP days, I remember having my cake and eating it too with arrays. With Python, you lose the “associative array” aspect. They nudge you into the dictionary direction, which is a whole other, slimy animal. By using unions, you get the best of both worlds. I foresee these being used a ton in my future.
Note: I took a class on C code and walked away with zero understanding of what unions actually did. Make sure to try this on in real life to get a feel for it. Imagine how obnoxious it would be to save each value of a structure individually to an EEPROM. Imagine how confusing it would be to reference the 12th value in of an 8-bit array, but you can’t remember if there are 3 or 16-bit values (so they use an extra 8-bit value for the LSB). Then imagine adding 3 more values to your array and having to match that in all your EEPROM coe. Yuck! You get the idea. Unions solve that problem.
Dynamically Generated Function Calls
You create a menu. It needs the text displayed to the user. The maximum allowed value, the minimum allowed value, and the Callback function when that value changes. Wait? Dynamic callback values? Why not! So basically the structure for System.HeaterState contains the function name “Turn_Heater_On()”.
Now when the menu item is updated, I can just loop through the entire structure and when the correct menu item is up to bat, the correct function can be called because the function name was pulled from the structure.
The usual alternative is using a Switch Case where you have to basically list, “If id=1 then do function1(), if id=2 then do function2(), etc.” Bla!
I’m not sure I’m explaining this one well. Look at the example below.
Custom Drivers
Custom drivers written for PMS5003 dust sensor, LM75B temperature sensor, MCP23017 expander, 25LC256 EEPROM,
PMS5003 “Dust” Sensor UART
The challenge on this part was mostly from the “not great” documentation. I’m lucky I put considerable effort into developing a FreeRTOS-ready UART driver that could be duplicated with almost zero work.
I started this one by writing a blocking version of the library. https://github.com/brandondrury/STM32_HAL_Libraries/tree/main/PMS5003_Blocking
The biggest problem with this “Dust” sensor is it doesn’t sense dust….not my definition of dust. It is useful for detecting flame from matches and the glue when squirted out of my hot glue gun. So far, I can’t get anything else to register. It’s nice to detect when something, presumably harmful, is dominating my room in the 1.0um – 10um range. However, I need a much wider range and the ability to sense VOCs. That’ll wait for the future.
LM75B Temperature Sensor I2C
A straightforward jellybean I2C temperature sensor. It’s not all that accurate. I couldn’t see the purpose of mettling with 11-bit temperature reading for a temperature sensor that was accurate up to +/- 2C. I dropped the “unneccesary” bits and kept the 8 MSB. I just need a vague temperature reading to warm up my dungeon winter mornings. It could be off by a few degrees and it wouldn’t matter.
Working with this IC was quite pleasant and the datasheet was excellent.
MCP23017 Expander I2C
I fell in love with this chip when I designed my synthesizer back in 2020. It’s an incredibly versatile chip with great documentation, and for whatever reason, it’s nice to me. Some ICs aren’t. We are friends.
I needed to read about 10 tactile switches without chewing up I/O on the STM32 so I used this. It’s I2C which means I just need power and 2 wires. Perfect! Then it occurred to me that the reaction to button presses needed to be fast. The MCP23017 triggers it’s own interrupt that I read with an external ISR within the STM32. This allowed me to create a “ButtonTask” and immediately block it with a Notify Wait. The callback from the external ISR notifies the task and the ButtonTask resumes in processing.
This ISR is a bit tricky in that it doesn’t clear until you read either either the GPIO or interrupt-determined GPIO registers. In the event something goes wrong, the interrupt pin will stay HIGH for eternity….which means dead buttons. To counteract this, I created a FreeRTOS timer to check every 500ms if the GPIO pin was HIGH. If so, I read the appropriate registers through I2C to clear them back to a functioning state.
Looking back I would have added LEDs to the extra GPIO on this expander for user feedback when using the system. Lesson learned.
25LC256 EEPROM SPI
I’m using 12 bytes, but I may as well use a 32KB EEPROM. I may get around to using the other 31KB+ some day. I already had this on hand and had good luck with it before. When writing a driver for this one, I ran into a rather nasty bug where the 7th bit rejected any value I sent it. Hmm. On second thought, I may still be working on that one.
DS3231 Real Time Clock
A cheap module was added to the board after the fact. It occurred to me that I did not have a battery backup for the board and I wanted user-defined settings to survive a power outage.
Working with this chip was straight forward. It’s like many I2C ICs.
Problems and Solutions
RAM and Flash Exceeded
The grand debate when selecting a microcontroller for this project was using a trusty, ol’ workhorse that was past its prime or the brand new beast that had yet to prove itself in battle. After considerable research, I made the bad call. I chose the STM32F103C8T6 (from “Blue Pill” fame) as I figured I could Google my way out of any mess. That wasn’t remotely true, btw, and this microcontroller is far from “trusty” for FreeRTOS projects.
I think STM32CubeMX may have confused me a bit. It listed 64KB of RAM. The only problem was this was for the STM32F103 family of microcontroller. It says, “up to 64KB of RAM”. The “fine print” shows 20KB, which was totally inadequate for this project. The good news is I’ve learned a ton about memory management. I have a terminal call that will list all the processes using RAM. (I should probably write a Python script to clean it up.) I have a functions that can printf() peak RAM usage. I have extensions. I’ve gotten a little experience reading .LD files. I’ve gotten practice dealing with Stack Overflow.
I didn’t expect to eat through the 64KB of flash. I’ve done some baremetal C and the code size was shockingly small. Chewing through 64KB of flash for this project isn’t “shocking”, but it was unexpected. Granted, optimization is off, but I fully expected to the project to come in under that.
Lesson: For prototypes, just add $4 to the microcontroller price that I’m considering. If I’m set on a $2 microcontroller, buy a $6 microcontroller. Vastly exceed all performance specs without hesitation. I wasted most of a day spinning up a new board and 10 days waiting for it to come in.
I look forward to learning how to truly/fully optimize the memory usage in code in the future.
I2C Callback Issues and Errata
I needed the ability to use interrupt-based I2C. That requires reacting to the ISR callback. I never could get a callback. I tried everything. I wasted way too long troublshooting this one before I realized that it’s listed in the STM32F103C8T6’s errata. The callback is busted on ISR or DMA based I2C. The number of known bugs in the STM32F103 is much larger than I had expected.
Lesson: Before settling on a microcontroller, Google “[MPN] errata “. That would have been a deal killer before I had started.
Custom PCBs
I’ve lost count of all the PCBs I’ve designed. Almost none of them are “impressive”. No DDR5 RAM. No Linux processors. But cranking out meat-n-taters microcontroller + chips is something I’m fluent in.
Dungeon Controller PCB
The main PCB contains the STM32G0B0RET6 microcontroller. 4-layer board. I/O consists of an RJ45, (2) 3.5mm “audio connectors” for the PIR sensors, SWD header, EEPROM, temperature sensor, PMS5003, BME680 (for later use) and an IDC 2×5 connector for the PCB used for the tactile switches.
I’ll bodge in the RTC clock module using test points I provided exactly for times like this.
The pin orientation on the PMS5003 dust sensor is backwards from the Kicad footprint. Oops. I got lucky. I flipped the PTH connector to the bottom side of the board. No problem.
Front Panel PCB
This one is straightforward. It’s a MCP23017 chip (mentioned above) breaking out to 10 tactile switches. I added a header to the front so I can hook a computer to the serial terminal for setting the menu without opening up the enclosure.
Relay PCB
I needed relays for turning on air filters and heaters. I decided to place this board about 10’ from my Dungeon Controller, although now I don’t remember why. (I just switched to Obsidian for note taking. I feel like I’ll never forget anything again.)
I decided on using an RJ45 connector to talk to the relay board. I’m just sending the GPIO signal. We’ll see how the noise immunity of properly using shielded twisted pairs works out. (I used a ground in each of the twisted pairs.) It probably would have allowed for more expansion if I had come up with a digital communication method, but I clamped the features on this one. Don’t tempt me. I’ll put CAN on this! SMILIE
24V LED Splitter PCB
I love little boards like this where the schematic takes 2 seconds, the layout takes 2 minutes, and it solves a huge wiring headache.
Solder Vibration Detector
The circuit for this one was simple enough. Take a piezo ( I bought like 50 a decade ago and almost never use them.) Clamp it to keep the voltage peaks in check. Send it to a comparator. The STM32 gets a nice HIGH signal when it’s time for the soldering iron filter to run.
Lessons
Sensor Selection
Choosing a sensor and writing a library for it is a big commitment. The marketing buzzwords used to describe a sensor may have no connection to my reality. The PMS5003 does not detect wood sanded with 200 grit sandpaper or grinding wood with a Dremel. Those particles are way too big. The BME680 is a month-long project in and of itself. That may have value for extreme situations, but I really just needed a general sensor that detected general filth so I can figure out if I need to run my air filters.
Visual Status Indicators
I spent a ton of time on learning Defensive Coding strategies to catch any and all problems (hopefully) within the code. However, I did not put thought into how I’d know about these problems once I deployed the project into the wild. Next time I’ll make sure to put an LED or 10 in the enclosure to make issues clear. Beep codes are our friends.
Reset Switches On Sensors
The PMS5003 has a reset pin. In hindsight, I’d put a GPIO pin on that reset pin (possibly with MOSFET). It would have been great to reset the sensor when a malfunction occurs. I could count the malfunctions. If 10 occur in a row, I could blink a warning LED.