Making a "SHUT UP Device"
Solving a niche problem with embedded Rust + ArduinoI have a problem. My brother is a night owl who plays League of Legends with his friends, at hours that I need to sleep. In the house, his computer is situated in a very open area of the house where sound easily travels, and there is no place else for him to use for such activities. Even if the internet is shut off, he is smart enough to use a mobile hotspot. And his busy schedule already makes it difficult to schedule time for him to "unwind" with friends.
Being an engineer, I like to try and solve problems, sometimes in stupid ways. As someone with a dedicated shelf with old microcontrollers and CPUs lying around, I became inspired to make a really simple noise meter that can be placed near the source of noise you want to be quiet; If you get too loud, lights and a buzzer trigger. Even my brother was OK with testing out something like this, so I got to work.
This is a multipart post of how the project operates, both on a hardware and software level. Originally, it was going to be one post but after all the roadblocks/research, it felt more efficient to split this into parts that will come out "SOON™". Admittedly, the hardware section may be less accurate given I'm not an electrical engineer and hated physics, so take it with a grain of salt and feel free to contact me if you have corrections.
So, how does one measure how loud a sound is?
Feeling the (Analog) Wave
One of the core components of this project is a microphone. Since the use case for this microphone is purely for measurement, there is no need to have anything fancy or worry about audio fidelity and noise reduction (as much). I had this Microphone breakout board with a MAX-9814 amplifier in my box of parts, so I picked it up and started looking at datasheets and code examples.
](/blog/0007/HIM-eMz2-V-970.webp)
This type of microphone is called an Electret microphone; Reductively speaking, it uses electromagnetism and physics magic to effectively convert the mechanical sound waves into an analog voltage. There is no need to really dig further into how it works, all that matters is that it can allow for some measure of sound. However, understanding how sound is "measured" is a whole can of worms.
I am not an audio engineer nor a physicist. I'm just a software engineer who likes to do dumb things from time to time. So when I google "how to measure decibels from analog microphone", there was a lot of jargon and science to dig through. Worst of all, using some of the math based on measurements did not yield any numbers that I was expecting. This is half of the reason this project took so long; I could not understand sound measurements like how Hank Hill doesn't understand JPGs. Here is a small collage of all the scratch-work and pages I looked at while trying to figure this out:

A piece I was able to take away from the research is that the analog voltage generated by the microphone circuit is not a "stable" voltage value; it acts more like an AC wave. This is due to the fact the actual mechanical pieces inside the microphone using electromagnetism to convert the sound wave into electrical signals. Given this, getting how "loud" a sound is through the microphone must be done by checking either the Root Mean Square (RMS) or peak-to-peak voltage over some sample period. Performing RMS can be mathematically intensive since AVR doesn't have dedicated floating point operations (see above), so I opted for peak-to-peak since the pseudocode is fairly simple:
int min_val, max_val = 0;
//Sample and capture as much of the waveform (number is arbitrary-ish)
for (i = 0; i < 1000; i++){
int measured = read_analog_input();
min_val = min(min_val, measured);
max_val = max(max_val, measured);
}
int p2p = max_val - min_val;It should be noted that RMS is often considered the more reliable metric and is used in other applications like AC power measurements. Also, this measurement of peak-to-peak voltage does not immediately give us the decibel measurement. That math still eludes me to this day, even if I do empirical calibration.
One day, I realized that the device doesn't need to convert to decibel measurements for what it needs to do, it just needs to associate a "reference" value to a known reliable decibel measurement. This is a black-box-like approach to the solution where outputs of a closed system are correlated to known inputs that are controllable, allowing a definition of how the system behaves without actually dissecting it. In this case, the controllable input is a sound with a known loudness measurement and the output is the voltage of the microphone. If I was good at statistics, I could collect a bunch of voltage data based on different levels of loud sounds and then do some kind of (probably logarithmic) regression to get a function based on the correlation, but I opted for a simpler, but more brittle approach.
The device fundamentally only cares about the microphone input in a binary fashion: the input sound is either too loud or it is fine. Given this, all that is really needed is to identify the voltage the microphone circuit would produce given the sound that meets the minimum threshold to trigger the "you are too loud" warning. Using a smartphone decibel measurement app (I used decibelX) and some speakers generating pink noise, I gathered that if I wanted the device to trigger for sounds louder than 70dB, the microphone outputs around 1.8V. Aside from the caveats of distance from the source sound and microphone spec differences, the device can work fairly well under the assumption that any microphone measurement above 1.8V is over 70dB and should trigger the buzzer. This becomes the following simple if-statement:
float voltage; //Generated from Peak-to-Peak measurements
if (voltage >= 1.8){
enable_buzzer();
} else {
disable_buzzer();
}This means the pieces are in place to interface with a microphone. Now for the next core piece: being loud back.
How to be obnoxious (with the power of sound)
Another core component of the project is to make a sound when there is a sound noise detected. For this, a piezoelectric buzzer can be used for that. In a sense, these buzzers work in an opposite way to the microphone; it takes an electrical signal and converts it into sound waves via mechanical vibration. In fact, the signal that is sent to a buzzer is as simple as a square wave with the frequency matching the desired tone frequency.

However, writing code to play sounds is tricky given the continuous nature of the wave. For example, the following C code sets up the microcontroller to generate a simple tone:
//This is structured as an Arduino Sketch for clarity
int frequency = 2000; //2000 Hz Tone
int duty_micros = (1000 * 1000) / frequency;
bool current = LOW;
void setup(){
pinMode(9, OUTPUT);
}
void loop(){
digitalWrite(9, current);
delay_micros(duty_micros);
current = !current; //This correlates with HIGH/LOW arduino constants
}The key problem here is that the tone function is blocking. The processor isn't doing anything else except waiting to toggle the buzzer signal. And because no other operations can happen, the device is not as reactive as before given the reactive use case of the buzzer. This is where some low-level programming comes in clutch.
It is important to remember that not all microcontrollers are made equal, and are often "single-core" systems. While multitasking and higher-abstraction parallelism like threads are technically possible (see literally any 1990s computer), it requires a good amount of resources like disk storage and RAM; resources that this project doesn't have, Instead of trying to implement these things, the solution is to use a couple of features built-in to many microcontrollers and processors: interrupts and timers. While each processor may have a different implementation of these peripherals, the general concept driving their functionality is usually consistent. As a reminder, any specific details will be referring to the ATMEGA328p, which is the processor for the Arduino Uno R3.
HEY YOU, STOP WHAT YOU ARE DOING!
Interrupts at a high level are used to redirect program flow temporarily to a predefined routine upon receiving a signal, effectively interrupting the main program to do something. Interrupt routines are very fast and used in larger numbers on modern processors to notify when a "slow" operations such as disk I/O and networking is finished. Interrupts in general can be masked, so only certain signals are allowed to alter program flow when needed, or disabled outright if the program does not need such processing.
Skipping a bunch of basic computer architecture, interrupts work by manipulating the program counter (PC) register based on the triggered interrupt (assuming it is enabled). When the interrupt is triggered, the current value of the PC is stored in a reserved register, and is changed to the address of the respective interrupt handler/interrupt service routine (ISR); On AVR and some other architectures, these "vectors" are predefined and baked into the chip design, with the compiler having some hints on where to place the specific assembly. There is a lot of nuance to writing a good handler, but a rule of thumb especially for architectures like AVR which have rudimentary interrupt handling is to keep it light. For example, here is the actual interrupt handler (in Rust) and the respective AVR assembly:
//Some necessary globals
//SAFETY: Only used for the buzzer, no multi-buzzer setup expected
static mut BUZZER_PIN_PORT: *mut u8 = core::ptr::null_mut(); //Set early in main
const BUZZER_PIN_MASK: u8 = 0b00000010; //Mask for pin 9a
/// SAFETY ASSUMPTION - ISR Mask is disabled outside of interrupt space
#[avr_device::interrupt(atmega328p)]
fn TIMER2_COMPA() {
unsafe {
*BUZZER_PIN_PORT ^= BUZZER_PIN_MASK; //toggle the port
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
}
}](/blog/0007/S7o49ACcS8-568.webp)
This ISR is written as the Timer2 Compare Match A interrupt, which will trigger when the Timer2 internal timer reaches a specific value to compare (more details in the next section). When configured, the timer will interrupt execution of the program at a regular interval, execute the registered interrupt handler, then return to the original execution of the program. The only thing the ISR does is toggle the buzzer pin by doing an XOR mask against the pin register; after that it returns to normal execution of the program. Looking at the disassembly and referring to the datasheet, the interrupt only takes 23 clock cycles (15 instructions + 4 cycles for each interrupt jump), or about 8 μs assuming a 8MHz CPU clock, which is significantly less time dedicated to handling a tone signal compared to the original example. Sure, some measurement data is lost, but this project does not require such accuracy to justify further optimizations.
WHAT'S WITH THE
unsafeandCOMPILER_FENCEstuff?Those who are unfamiliar with Rust may have noticed the use of the
unsafekeyword or thecompiler_fence()call. These are part of "advanced" Rust programming and are going to be discussed in the final Part 3 of this series (which may take a while). To summarize the reason for these declarations:unsafeallows the ability to perform certain memory operations (in this case, modifying mutable data at a static location in memory) and thecompiler_fenceprovides some enforcement about the order of operations when performing certain optimizations around atomics.It should be noted that
compiler_fencein this case is not going to have much effect since there is no actual code with atomic types in the ISR and AVR architecture doesn't have atomic instructions AFAIK, but it helps me sleep at night so I don't feel like removing it :p.
Watch the Time
Interestingly, "timer" is a misnomer for what is effectively a special register that increments when attached to a clock signal. Since any microcontroller relies on a (usually monotonic) clock signal, a timer can be created by using a simple "counter" (which is constructed of smaller circuits but keeping it simple here). Whenever the clock signal has an "edge", the counter is incremented, representing a tick of the clock signal. By knowing the frequency of the clock, the time can be easily calculated as follows:

While the precision of the timer is based on the clock signal frequency, that precision is very high when the clock signal is as high as 16MHz. This is when a prescaler can be used to increase the number of clock signal edges to count before actually incrementing the timer. This is a simple counter and configurable comparator chained to the increment signal of the increment signal of the timer counter, like below:

Using the prescaler, there is now greater control for the precision of the timer, allowing for lower frequency "ticks". So if 4 "divisions" are configured in the prescaler and the clock signal is 1MHz, the timer now increments in ticks as if it was attached to a 250KHz clock, which is useful since timers store small values (for AVR, its either 8-bit or 16-bit).:

Given the knowledge of how long each timer count is, a separate comparator linked to the timer's output can be used to set a flag in a peripheral register, indicating the proper amount of time has elapsed. The comparator will also trigger the reset of the timer, so the next desired interval can be tracked seamlessly. After the timer counter is reset, the comparator turns off, allowing the counting to continue:

What about PWM?
Those who know about this stuff may have asked this question when reading the previous sections. Well... this is basically half duty cycle pulse width modulation (PWM) but with extra steps. The
arduino-halcrate defines PWM functionality using a timer abstraction. Even the Arduino PWM uses (effectively) the same timers under the hood when looking at their core library.The main reason to not use PWM is that it works based on a fixed signal frequency and variable duty cycles. Since the tone works with a square wave which has variable frequency and fixed (50%) duty cycle, PWM wouldn't really solve the issue here. Referring back to the Arduino core library, their implementation of the `tone()´ function also does not use PWM.
Next time on "Dragon Ball Z"
This post was extremely jam-packed with information, and nice drawings that I made using my new reMarkable Paper Pro (not sponsored) I bought for my birthday. This project has been in the works for over 3 months, not because I was lazy, but rather the fact that physical electronics projects can get very complex. I didn't think I needed to learn that much about audio engineering just to measure sound, guess I was pretty naïve about that.
The next post will focus primarily on making the display work in the project. Yes, a small display. It is definitely not necessary if it was an MVP for a corporate project, but it's my project. I make the rules. I'm doing it my way.
No confirmed timeline, other than "it's ready when it's ready". I will say that there are going to be some fun diagrams that I have to draw, but hopefully I don't take forever on those.
Until next time.