When embarking on a new project, I tend to learn the most when about 90% of the work is completely new to me, while 10% is familiar. It was like this when I started work on my [Shinewave] controller mod and I ended up learning a lot along the way. In addition, there are a few cool things in the code that I'd like to share:
- Assembly interrupt routine to read controller input
- Abuse of the Analog Comparator to read a 3.3V signal on a 5V powered chip with no external components
- Repurposing the timer to interrupt at the end of a message
Most interesting, I believe, is the 3.3V comparator trick. Every other approach consists of dropping the microcontroller's voltage to 3.3V, or introducing external components like level shifters or even voltage dividers.
UPDATE(Aug 30, 2015): Since I wrote this post, I have redone most of the code involved into a much simpler, single function. All of the information in this post is still correct, it just doesn't exist in the Shinewave project anymore. You can look at my codebase as it was during the writing of this post here.
The main program flow for the Shinewave is actually pretty simple. Most of the time is spent waiting for a controller signal to trigger the Analog Comparator interrupt, at which point the signal will start being read. Once the signal line has been idle for 16 microseconds, the timer interrupts signals that the transmission is complete.
All of the information in this post refers to the 8-bit ATtiny85, whose datasheet can be found here. Most of the features of this chip are a subset of more advanced chips like the ATmega328 that power the Arduino, so any of my examples below can be modified to run on other platforms.
A big part of the program's control flow is spent inside of interrupts. Because these small microcontroller's only have one thread, interrupts are a way to respond to events quickly without needing to implement a scheduler. Whenever the interrupt's trigger occurs, the microcontroller pauses execution wherever it is, runs the interrupt routine, then resumes execution.
You can trigger interrupts on all sorts of events, such as a pin being toggled(a button being pressed), a timer overflowing, or more. In addition, these triggering events can be configured to trigger more selectively or generally, enabled and disabled on the fly, or even be written in Assembly to run as quickly as possible. Although the Arduino environment supports interrupts with its
attachInterrupt() function, you'd be hard pressed to implement any of these more complicated functions without diving into the datasheet.
While developing on the hardware, I ran into a problem with voltage mismatches.
- The controller supplies both 3.3V and 5V power lines
- The controller signal line is 3.3V
- The ATTiny can operate at both 5V and 3.3V, but can only read and transmit at its operating voltage
- The Neopixels require a 5V signal
Ideally, I'd be able to read the 3.3V controller input and output 5V to the LEDs without any external components. One of my initial ideas was to power the chip at 5V and treat the input as a plain digital signal, but it wouldn't read reliably or consistently trigger external or pin change interrupts. I also considered treating the input as an analog signal and using the ADC(Analog to Digital) converter to read the signal. However, this is impossible to assign an interrupt to and would slow down my tight interrupt routine.
The Analog comparator is a hardware feature that can compare two analog voltages and report which one is higher. This doesn't seem too useful, until you also realize that the chip keeps a regulated 1.1V supply for low voltage brown-out detection. This internal voltage can be connected to the comparator to detect whether a voltage is higher or lower than 1.1V! You also get to define interrupts on this function. This works perfectly for reading low voltage digital signals!
There's a bit of configuration involved, especially if you'd like to read from any of the analog pins and not just AIN1(PB1). In addition, because the 1.1V reference is connected to the positive input and your signal is the negative input, any values that you read need to be inverted.
Below is my configuration for the Shinewave project. If you're planning on doing something similar, I'd recommend reading Chapter 16 in the ATtiny85 datasheet.
ACSR |= (1 << ACBG); // Enable 1.1V positive input reference voltage ACSR |= (1 << ACIS0) | (1 << ACIS1); // Enable rising edge interrupts ACSR |= (1 << ACI); // Clear any pending interrupts ACSR |= (1 << ACIE); // Enable Analog Comparator interrupts
If there's enough interest, I could create an Arduino library to manage the Analog Comparator to easily read signals.
The hardware timer modules are useful for keeping track of how much time has passed. They're used by the Arduino
millis() functions, but they have a lot more potential if you're willing to understand how they work. For example, you can use them to generate a one clock cycle wide pulse.
In my application, I have the 8-bit timer0 module to trigger an interrupt when it overflows to 256. Because the timer increments every clock cycle(prescaler = 1), and the chip runs at 16MHz, it will overflow every 16 microseconds after its been started.
In my application, I'm using Timer0 to signal after an amount of time has passed without a message. In order to accomplish this, I just need to enable and reset it to zero every message. When it finally gets all the way to the top before being reset, it overflows and triggers an interrupt. This interrupt then disables the timer so it doesn't interrupt during normal execution.
Overall, there's a lot of functionality hidden in the datasheets for the chips that we use often. If your project needs to run quickly or if you'd like to cut out some external components, I'd really recommend giving it a read.