The most complex addition of the night was shops. In particular the Organ Clinic which allows you to buy organs and have them installed in your body, remove organs for a fee (except your starting organs which you can sell to the clinic (except for your appendix) - don’t sell your only heart or you will die). It also has an option for installing the organs you are carrying around in organ contains - either that you found in the dungeon or removed from corpses. Keeping track of several layers of nested menus is not something my engine was designed to handle so it got a bit tedious.
I also implemented the status effects of each organ, and the affect that mutation have on each organ. This got a bit tedious as some organs are passive - they just determine a stat, such as the number of hearts you have determining your max health (though cybernetic hearts, regular hearts, and damaged hearts all count for different amounts - also a heart can be both cybernetic and damaged). But other organs grant abilities, like the Cronenberg guns that allow you to fire guns attached to your body using your health as their ammo. These are handled in a totally different part of the code to the passive organs, and these organs also need to be treated differently if they are cybernetic or damaged (or both).
I implemented environmental effects. If you stand in poison your poison goes up. It’s reduced periodically by a rate determined by the quantity and quality of your livers. If your poison meter fills up then it resets and a random organ is damaged unless all your organs are damaged in which case a random organ is destroyed. Hopefully it’s not your only heart or you will die.
If you have line of sight to a radiation emitter and you are within a certain range then you gain radiation. Also sometimes you just randomly gain radiation. If your radiation meter fills then it resets and a random organ gains or loses a random mutation. One such mutation is “radioactive”. If one of your organs is radioactive, you gain radiation over time while it’s in your body.
Finally there is smoke, which works similarly to radiation in that it requires line of sight to a smoke emitter and has limited range. Standing near a smoke emitter causes your oxygen stat to go down, and if it’s empty then your health stat will start going down instead. Having more lungs is a good way to prevent death from breathing in too much smoke.
Finally I added a boss fight at the bottom level of the city. The boss has several abilities taken from other enemies (it spreads poison, it emits smoke and radiation) and it also teleports when its health cross the 2/3 and 1/3 thresholds of its max health to prevent the player from shooting it several times point blank with a shotgun. This forces you to explore the level looking for it several times and hopefully having more interesting encounters with other enemies along the way. You win the game by killing the boss and then making it back to your starting square (the “Evac Zone”).
Electric Organ can be downloaded or played in a web browser from its itch.io page .
]]>It’s possible to lose the game:
I’ve added lots of information screens to help the player get their bearings. Here’s a list of all the current organs of the player character:
And here’s one of 3 help screens that explain the controls and mechanics:
I also added two additional enemy types: the poisoner which spreads a trail of poison, and the divider which splits into two enemies when damaged.
Each item in the game can be applied, including the interaction of filling an organ container with an organ from a corpse.
And equipped weapons can now be fired. If you have two pistols, they both fire. Also there are weapon organs which cost health to fire.
It’s all coming together nicely but there is still a fair amount of work left for tomorrow but fortunately it’s Friday and I can stay up arbitrarily late. Notably I still need to connect the organs to the player’s stats (e.g. so having two hearts give you twice as much health) and implement the organ traits. I need to implement shops and the organ clinic so players can change their organs. I need to add the final boss fight and make the game winnable. Finally I need to balance and playtest a bunch.
]]>The most complicated part is dealing with what happens when a user drops an item while standing in a cell that already contains an item. The game engine does not allow multiple items to exist at the same location, so when a collision would occur the game searches for the nearest cell that doesn’t contain an item (without traversing wall, etc) and puts the item there.
Part of the game will involve harvesting organs from corpses. In preparation for that I made it so that when enemies die they leave a corpse. I changed the zombie enemy so that its corpse resurrects after some number of turns unless its corpse is destroyed.
I also added a new enemy type - the “snatcher” - which picks up items from the ground and drops them all when it dies. I ran into an interesting bug where snatcher corpses were immediately disappearing. The problem was that the game treats corpses as items, and the snatcher’s corpse was picking itself up as soon as it died.
There are two days left in the jam. The main focus for tomorrow will be allowing items to be applied, and implementing all the interactions with the world that affect the player’s stats. I also need to add some shops where you can buy items and have organs installed.
This leaves the final day for implementing the win condition which I think will be getting to the bottom of the dungeon and kill a boss, then escape.
]]>Firing guns is accompanied by a sound effect. Rather than pre-recording sound effects, sound effects are generated live using the same synthesizer that I use for music. Using a synth means that some properties of the sound effects can be randomized, so each instance of a sound effect sounds slightly different.
Finally I made a full screen UI for navigating the message log as there can be some useful information in there and the regular game’s UI only shows 4 lines at a time.
Next step will be adding items, equipment, and organs.
]]>See the animation along with the music on youtube.
I also got pathfinding working. This was interesting as I have several different types of NPC with different movement rules. Currently there are 3:
To help with pathfinding I compute what I call “distance maps” (sometimes called “Dijkstra maps” elsewhere - read more in this post) which NPCs can use to quickly determine the distance from each location to the player. But now that different NPCs have different movement rules, the effective distance to the player may differ between NPCs. For example if the player is on the other side of a door, NPCs that can open doors will find the player much closer than NPCs that can’t. This means I need to maintain multiple distance maps for the different combination of movement rules that NPCs can have.
I also spent some time improving basic quality of life features like adding a message log and descriptions of tiles. This also carves out some space for the game’s UI which I’ll be adding to over the next few days as I implement the player’s stats and the combat system.
]]>I also spent some time on aesthetics, specifically porting the animation and particle system from a previous project to add a smoke effect. This system will eventually be used to implement projectiles for the combat system.
The level generator now generates all the levels connected by stairs. It’s possible to explore the entire “dungeon” though there is nothing to do yet. Currently the entire dungeon is made up of vertically-connected city blocks in a cyberpunk-style layered city. If I get time I’ll make a second terrain generator for an underground section.
]]>While working on this I fixed a very longstanding bug in my lighting system that caused lights to be too bright near their sources and to drop off with harsh steps rather than a smooth gradient.
]]>Datasheet (pdf) for L78L series voltage regulators which includes the 78L05
The 78L05 provides a regulated 5v supply on its output pin if sufficient voltage (at least 7v) is supplied to its input. If a device’s documentation says that the device requires a regulated 5v power supply, this is the kind of thing that it means.
The TO-92 package is commonly used for transistors but other 3-pin components can also be found in this package.
The pinout in the datasheet is from the bottom-up point of view which I found counterintuitive and possibly dangerous for people skimming the manual looking for a pinout without reading the text indicating it’s a bottom-up view. In my experience, when a voltage source is connected to the output pin of 78L05 (ie. when it is connected backwards), the same voltage can be measured at its input pin. It’s not clear how much current it will allow to flow when connected backwards but it’s possible that this could damage whatever component is being powered by the incorrectly connected 78L05.
The component has a round side and a flat side. As shown in the image above, with the flat side facing you, with the pins pointing downwards:
The figure of 7v as the minimum input voltage is based on this image from the manual:
So the actual minimum input voltage depends on how much current is being drawn from the output, but if it’s up to 100mA then 7v will be fine.
Datasheet (pdf) for LM385 Dual Op Amp (also contains information on related components)
Datasheet (pdf) for TL072 Dual Op Amp (also contains information on related components)
Both components include a pair of op amps on a single chip and they have identical pinouts. This is the pinout viewed from top-down:
This table lists the pins corresponding to their arrangement in the diagram above. The semi-circular indentation on the top of the IC is facing upwards.
Left Side | Right Side |
---|---|
1OUT | VCC+ |
1IN- | 2OUT |
1IN+ | 2IN- |
VCC- | 2IN+ |
My mnemonic for this pinout is that the bottom left is usually ground and the top right is usually positive voltage for ICs, and if you don’t have a negative supply voltage you’ll probably connect the VCC- pin to ground. Then, a common op amp feedback configuration is a voltage forwarder (aka. a buffer amplifier) and in this configuration the IN- pin and the OUT pin are connected together. I like to pretend that the designer wanted to make it easy to setup a voltage forwarder by putting the OUT and IN- pins next to each other for both op amps on the chip.
Datasheet (pdf) for TL074 Dual Op Amp (also contains information on related components)
This is 4 op-amps in a single chip. This is the pinout viewed top-down:
This table lists the pins corresponding to their arrangement in the diagram above. The semi-circular indentation on the top of the IC is facing upwards.
Left Side | Right Side |
---|---|
1OUT | 4OUT |
1IN- | 4IN- |
1IN+ | 4IN+ |
VCC+ | VCC- |
2IN+ | 3IN+ |
2IN- | 3IN- |
2OUT | 3OUT |
Datasheet (pdf) for MAX495 Op Amp (also contains information on related components)
This Op Amp is marketed as “Rail to Rail” which means its min and max outputs are close to the + and - supply voltages.
In the pinouts below I’ve added VCC- and VCC+ labels in addition to the VEE and VCC labels from the manual for consistency with other component pinouts.
This table lists the pins corresponding to their arrangement in the diagram above. The semi-circular indentation on the top of the IC is facing upwards.
Left Side | Right Side |
---|---|
NULL | N.C. |
IN1- | VCC (ie. VCC+) |
IN1+ | OUT |
VEE (ie. VCC-) | NULL |
The inputs are numbered even though there is only one op-amp on the chip. The output is not numbered in the manual but it could just as well be called OUT1.
The NULL pins are not the same as the N.C. (Not Connected) pin. The NULL pins can be used to adjust something called “input offset voltage”. There’s more info in the manual.
Datasheet (pdf) for Atmel Atmega328P (the microcontroller on the Arduino Nano)
This is the pinout of the Arduino Nano from the Arduino documentation, except I’ve modified it to clarify that pins A6 and A7 can’t be used as digital I/O port pins.
Here’s the pinout in tabular form. The positions of pins correspond to a top-down view of the Arduino Nano with the usb port facing upwards. Each entry is named after what is literally printed on the PCB next to each pin. In parentheses after each name are the I/O port pin function (PXn) and the ADC channel (ADCn) if any.
Left Side | Right Side |
---|---|
D13 (PB5) | D12 (PB4) |
3V3 | D11 (PB3) |
REF | D10 (PB2) |
A0 (PC0, ADC0) | D9 (PB1) |
A1 (PC1, ADC1) | D8 (PB0) |
A2 (PC2, ADC2) | D7 (PD7) |
A3 (PC3, ADC3) | D6 (PD6) |
A4 (PC4, ADC4) | D5 (PD5) |
A5 (PC5, ADC5) | D4 (PD4) |
A6 (ADC6) | D3 (PD3) |
A7 (ADC7) | D2 (PD2) |
5V | GND |
RST (PC6) | RST (PC6) |
GND | RX0 (PD0) |
VIN | TX1 (PD1) |
You can power the Arduino Nano by supplying between 7 and 12 volts to the VIN pin, or by supplying regulated 5 volts to the 5V pin (such as with a 78L05 voltage regulator). When powering via the VIN pin, the 5V pin can be used as a 5V power supply for powering other components (including other Arduino Nanos, in which case you would connect the 5V pin on one Arduino Nano to the 5V pin on other other). This works because in between the VIN and 5V pins there is a 5v voltage regulator similar to the 78L05 (but not identical - the Arduino Nano uses a LM117IMPX-5.0). It supplies the Arduino with regulated 5V but can supply other things too via the 5V pin, because its output connects directly to this pin.
Some sources will recommend against supplying your own voltage regulator and powering via the 5V pin and using the VIN pin instead. Indeed there is little point using the 5V pin as the onboard voltage regulator should be just as good as whatever you use instead to produce the regulated 5v. One reason you might do this anyway is because the voltage regulator could wear out over time and it’s much cheaper to replace a voltage regulator than an entire Arduino. You could wait until the onboard regulator fails before circumventing it with an external component but this might be hard once the Arduino is soldered in place compared to replacing the external voltage regulator with a new one.
The Arduino Digital Pin documentation is a good starting point.
Some of the pins are multiplexed and have other functions that prevent them from operating as digital IO pins, but by default all digital pins behave as IO pins.
There are 3 registers per IO port with a bit for each pin:
DDx
(Data Direction) determines whether a pin is an input pin or an output
pin. 0 = input, 1 = output. The default is all 0s (all input pins).PINx
can be read to determine the state of input pins.PORTx
can be written to set the state of output pins. Writing a 1 to a bit
in this register corresponding to an input pin enables the pull-up resistor
for that pin.The main consideration when attaching buttons is not letting the input pin “float”; the pin should always be connected to a voltage source or ground. This is the purpose of “pull-up” and “pull-down” resistors. The resistor connects the pin to the voltage source that will represent the “off” state (this could be logic 1 or logic 0), and then when the button is pressed, the pin is connected directly to the voltage source representing “on”.
Here is an example where the input pin sees 0v via the 10K pull-down resistor to ground, but when the button is pressed the pin sees 5v.
Note that input pins have very high impedance, so they draw almost no current. We don’t have to worry about connecting a voltage source directly to the pin.
Here’s another example where the input pin sees 5v via the 10K pull-up resistor, but when the button is pressed the input pin sees 0v.
When the switch is open the pin sees (almost) the full 5 volts because the pin has incredibly high impedance; it basically acts like an open circuit for the purposes of calculating current and voltage drop.
This example shows how one could connect a switch with the Arduino’s internal pull-up resistor enabled (write a 1 to the PORTx bit for the input pin). The following example is functionally equivalent to the previous example, but the pull-up resistor is inside the Atmega328P chip on the Arduino.
When dealing with physical switches, one problem that can happen is “bouncing” which is when the physical connection rapidly closes and opens for a brief period after the switch is pressed. This can be solved in software by introducing a short delay where the switch is ignored for a brief period after its state changes, or it can be solved in hardware by adding a capacitor in parallel with the switch. The charging and discharging of the capacitor smooths out the rapid connecting and disconnecting, and the pin only sees the state change after enough time has passed for the capacitor to charge or discharge (depending on whether a pull-down or pull-up resistor is being used).
This example shows how to connect a capacitor to the internal pull-up resistor example above, though the same technique can be used for all the examples.
This demonstrates a way of mixing the voltages at two output pins by connecting them together through resistors to a common node. The diagram below shows two pins connected in this way but it works for arbitrarily many pins. If the resistors all have the same resistance then the common node will have a voltage that is the mean of voltages at the output pins, but using different resistors results in the weighted mean instead.
This works because unlike input pins which have very high impedance, output pins have very low impedance (both when acting as current sources and current sinks). This means that they can produce relatively large amounts of current (the docs say 40mA) and also they can consume a lot of current. This means that when an output pin is producing logic 0 (0v), it serves a similar role to ground (it has 0v and it’s a current sink). Thus we can analyse the circuit above as we would a simple voltage divider. When one output is low and one output is high, then half the high voltage is seen between the two resistors provided that they have the same resistance.
This idea can be extended to implement a digital to analog converter by combining the voltages of many pins through carefully chosen resistances.
Note that as with naive voltage divider circuits (this is really just a naive voltage divider circuit) the output of this circuit (the node with mixed voltage) has high impedance (due to the resistors used). This means its voltage will rapidly reduce as current is drawn through that node of the circuit. One way to fix this problem is to connect it to a voltage forwarder op-amp configuration:
Note that at this point we have almost implemented a non-inverting voltage adder (a common op-amp configuration). All that’s missing are resistors in the feedback network to increase the gain of the op-amp to be equal to the number of pins being combined. With a voltage forwarder the gain is 1 so its output is the (possibly weighted) mean of voltages rather than the (possibly weighted) sum of voltages.
]]>For this guide I’ll be using one of these:
It’s an Elegoo Nano - a cheaper drop-in replacement for the Arduino Nano. The only noticeable differences are that the header pins don’t come pre-soldered, it doesn’t come with any cables and it has a different USB to serial chip (a CH340 instead of the FT232 found on the Arduino).
The docs on the Elegoo
website suggest that
you’ll need to install special drivers in order for your computer to detect the
Elegoo Nano when you plug it in with a USB cable. The necessary Linux driver is
called ch341
and it’s probably already installed as a kernel module on most
Linux distributions and it’s likely that it just work when you plug in the USB
cable with the Arduino attached.
If not, here are some things to try.
Enable the kernel module explicitly. If this fails it indicates that you don’t have
the ch341
kernel module installed and may have to install it with a package manager
or build it from source.
$ sudo modprobe ch341
$ lsmod | grep ch341
ch341 28672 0
usbserial 73728 2 pl2303,ch341
usbcore 385024 13 pl2303,usbserial,xhci_hcd,snd_usb_audio,usbhid,snd_usbmidi_lib,xpad,usb_storage,uvcvideo,btusb,xhci_pci,uas,ch341
When you connect the Elegoo Nano via a USB cable, you’ll see this in the output
of dmesg
:
...
[210724.817737] usb 1-6: new full-speed USB device number 23 using xhci_hcd
[210724.958846] usb 1-6: New USB device found, idVendor=1a86, idProduct=7523, bcdDevice= 2.64
[210724.958853] usb 1-6: New USB device strings: Mfr=0, Product=2, SerialNumber=0
[210724.958856] usb 1-6: Product: USB Serial
[210724.967993] ch341 1-6:1.0: ch341-uart converter detected
[210724.981915] usb 1-6: ch341-uart converter now attached to ttyUSB0
After plugging in the device you should see a new device file /dev/ttyUSB0
.
This will be used later on both to program the device and also to see the output
when printing messages over the serial port.
If nothing happens when you plug in the device, try a different cable (the first one I used didn’t work for some reason). If your Linux kernel didn’t come with the ch341 module try building and loading the ch341ser module from here.
Note that even though I’m technically not using an Arduino, I’ll be referring to the device as an Arduino for the remainder of this post!
We won’t be using the Arduino IDE but we still need to install some tools. Namely:
avr-gcc
A c compiler targeting AVR processors such as the one found in the Arduino.
make
Minimal build system.
avrdude
Tool for downloading code to AVR processors such as the one found in the Arduino.
picocom
Tool for sending and receiving data over a serial port. This will be used to connect to the Arduino so we can see the messages it prints.
clangd
A Language Server Protocol (LSP) server for c. This will allow for some
ergonomics such as jumping to function definitions in editors that contain an
LSP client (e.g. vscode, neovim with the LanguageClient-neovim plugin). This
might be found in a package named clang-tools
if no clangd
package is
available in your distro.
bear
bear
is a tool for generating compilation databases (compile_commands.json
)
from a Makefile
.
avr-libc
The standard c library for AVR devices. In some Linux distros this will come with
avr-gcc
but on others it will need to be installed separately.
The code for this section is here.
Put this in main.c
. This program implements a very simple serial (USART)
driver for the Arduino and uses it to print Hello, World!
with printf
:
// main.c
#include <stdio.h>
#include <avr/io.h>
// The arduino clock is 16Mhz and the USART0 divides this clock rate by 16
#define USART0_CLOCK_HZ 1000000
#define BAUD_RATE_HZ 9600
#define UBRR_VALUE (USART0_CLOCK_HZ / BAUD_RATE_HZ)
// Send a character over USART0.
int USART0_tx(char data, struct __file* _f) {
while (!(UCSR0A & (1 << UDRE0))); // wait for the data buffer to be empty
UDR0 = data; // write the character to the data buffer
return 0;
}
// Create a stream associated with transmitting data over USART0 (this will be
// used for stdout so we can print to a terminal with printf).
static FILE uartout = FDEV_SETUP_STREAM(USART0_tx, NULL, _FDEV_SETUP_WRITE);
void USART0_init( void ) {
UBRR0H = (UBRR_VALUE >> 8) & 0xF; // set the high byte of the baud rate
UBRR0L = UBRR_VALUE & 0xFF; // set the low byte of the baud rate
UCSR0B = 1 << TXEN0; // enable the USART0 transmitter
UCSR0C = 3 << UCSZ00; // use 8-bit characters
stdout = &uartout;
}
int main(void) {
USART0_init();
printf("Hello, World!\r\n");
return 0;
}
Compile the code to an elf file. atmega328p
is the name of the microcontroller
on the Arduino Nano but other devices may have a different microcontroller.
$ avr-gcc -mmcu=atmega328p main.c -o hello.elf
Flash the Arduino. Plug it in with a USB cable, then run the following command.
Replace /dev/ttyUSB0
with the device associated with the Arduino’s serial port
(if you have multiple USB serial devices plugged in it might get assigned a
different device file). Also as with the avr-gcc
command above, replace
m328p
with the part number of the microcontroller on your Arduino. avrdude
uses a different naming convention to avr-gcc
.
$ sudo avrdude -P /dev/ttyUSB0 -c arduino -p m328p -U flash:w:hello.elf
avrdude: AVR device initialized and ready to accept instructions
avrdude: device signature = 0x1e950f (probably m328p)
avrdude: Note: flash memory has been specified, an erase cycle will be performed.
To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: reading input file hello.elf for flash
with 390 bytes in 1 section within [0, 0x185]
using 4 pages and 122 pad bytes
avrdude: writing 390 bytes flash ...
Writing | ################################################## | 100% 0.10 s
avrdude: 390 bytes of flash written
avrdude: verifying flash memory against hello.elf
Reading | ################################################## | 100% 0.06 s
avrdude: 390 bytes of flash verified
avrdude done. Thank you.
To see the output of this program you’ll need to use a tool that prints data
received over a serial connection. I’ll use picocom
in this guide. Run the
following command, replacing /dev/ttyUSB0
with the device file associated with
the Arduino.
$ sudo picocom -b9600 /dev/ttyUSB0
picocom v3.2a
port is : /dev/ttyUSB0
flowcontrol : none
baudrate is : 9600
parity is : none
databits are : 8
stopbits are : 1
escape is : C-a
local echo is : no
noinit is : no
noreset is : no
hangup is : no
nolock is : no
send_cmd is : /nix/store/sy0ipq6qy2slql25lbax77i4315bynzp-lrzsz-0.12.20/bin/sz -vv
receive_cmd is : /nix/store/sy0ipq6qy2slql25lbax77i4315bynzp-lrzsz-0.12.20/bin/rz -vv -E
imap is :
omap is :
emap is : crcrlf,delbs,
logfile is : none
initstring : none
exit_after is : not set
exit is : no
Type [C-a] [C-h] to see available commands
Terminal ready
Hello, World!
To exit picocom
, press Ctrl-a, then Ctrl-x.
When the Arduino is plugged in via its USB port, connecting to it with picocom
(running the sudo picocom ...
command)
will cause the device to reset, so you won’t miss the “Hello, World!” message if
you don’t connect fast enough. You can also reset the Arduino by pressing the
button near its built-in LEDs.
Also note the -b9600
sets the baud rate which corresponds to the line #define
BAUD_RATE_HZ 9600
in the program.
Another note on picocom
is that you won’t be able to download code to the Arduino (the
avrdude
command) while connected to the device with picocom
. Exit picocom
(Ctrl-a, then Ctrl-x) before running avrdude
.
So that we don’t have to manually run avr-gcc
every time we compile the code,
create a Makefile
with the following contents:
# Makefile
TARGET=hello
SRC=main.c
OBJ=$(SRC:.c=.o)
CC=avr-gcc
MCU=atmega328p
# The --param=min-pagesize=0 argument is to fix the error:
# error: array subscript 0 is outside array bounds of ‘volatile uint8_t[0]’
# {aka ‘volatile unsigned char[]’}
# ...which is incorrectly reported in some versions of gcc
CFLAGS=-mmcu=$(MCU) -std=c99 -Werror -Wall --param=min-pagesize=0
$(TARGET).elf: $(OBJ)
$(CC) $(CFLAGS) $(OBJ) -o $@
%.o : %.c
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm -rf *.o *.elf
Note that this also enables more warnings and works around a problem where
avr-gcc
would incorrectly report an error.
Now to rebuild the hello.elf
binary after changing the code you can simply run:
$ make
avr-gcc -mmcu=atmega328p -std=c99 -Werror -Wall --param=min-pagesize=0 -c -o main.o main.c
avr-gcc -mmcu=atmega328p -std=c99 -Werror -Wall --param=min-pagesize=0 main.o -o hello.elf
I won’t cover setting up an LSP client here as there are too many different editors and plugins to choose from, but for an example neovim LSP setup using the LanguageClient-neovim plugin, see my neovim config.
For the LSP server we’ll use clangd
. As long as clangd
is installed and your editor is correctly configured,
your editor should take care of starting and stopping clangd
in the background. To check
that it’s installed try running the command clangd
.
For clangd
to work correctly it needs to know how you intend on compiling the
code. It finds this out by reading a file compile_commands.json
that lists the
commands used to compile the code. There are various ways of generating this
file and since it will contain absolute paths it’s not advised to check it into
the project. One way of generating compile_commands.json
is with a tool called
bear
.
bear
can generate a compile_commands.json
from an invocation of make
by
watching what commands are run. For example (the working directory is
/home/s/src/hello-avr
):
$ bear -- make --always-make
avr-gcc -mmcu=atmega328p -std=c99 -Werror -Wall --param=min-pagesize=0 -c -o main.o main.c
avr-gcc -mmcu=atmega328p -std=c99 -Werror -Wall --param=min-pagesize=0 main.o -o hello.elf
$ cat compile_commands.json
[
{
"arguments": [
"/usr/bin/avr-gcc",
"-mmcu=atmega328p",
"-std=c99",
"-Werror",
"-Wall",
"--param=min-pagesize=0",
"-c",
"-o",
"main.o",
"main.c"
],
"directory": "/home/s/src/hello-avr",
"file": "/home/s/src/hello-avr/main.c",
"output": "/home/s/src/hello-avr/main.o"
}
]
The --always-make
argument to make
tells it to unconditionally run the
build commands and removes the need to run make clean
first.
On some systems, this is sufficient to allow an LSP client to do code navigation and other ergonomic features.
I’ve tested this on Alpine Linux and NixOS. On the former, LSP features worked as expected
at this point and no further configuration was necessary, but on NixOS I had
some errors from clangd
when opening main.c
in a text editor with LSP
support:
It’s not immediately clear from the error messages but this problem is caused by
the LSP server (clangd
) being unable to locate the header file <avr/io.h>
.
Since this problem is easy to reproduce on NixOS, we’ll start by looking at the
contents of compile_commands.json
on NixOS:
[
{
"arguments": [
"/nix/store/pfxqwrvm0y6lbs53injrl4sqz2njrpyl-avr-stage-final-gcc-wrapper-12.2.0/bin/avr-gcc",
"-mmcu=atmega328p",
"-std=c99",
"-Werror",
"-Wall",
"--param=min-pagesize=0",
"-c",
"-o",
"main.o",
"main.c"
],
"directory": "/home/s/src/hello-avr",
"file": "/home/s/src/hello-avr/main.c",
"output": "/home/s/src/hello-avr/main.o"
}
]
Let’s try manually adding some extra include paths to help clangd
find
<avr/io.h>
. Since the code compiles when we run make
, avr-gcc
must be
finding headers successfully. We can ask avr-gcc
to print out its additional
include paths with this command:
$ avr-gcc -E -Wp,-v - < /dev/null
ignoring duplicate directory "/nix/store/fh0ccmn4vv7hncyfic4ph3hx34vmzsih-avrdude-7.1/include"
ignoring duplicate directory "/nix/store/fh0ccmn4vv7hncyfic4ph3hx34vmzsih-avrdude-7.1/include"
ignoring duplicate directory "/nix/store/fh0ccmn4vv7hncyfic4ph3hx34vmzsih-avrdude-7.1/include"
ignoring nonexistent directory "/nix/store/ss76yfg4wj01ha9rjjgkr4qg0g76ivpa-avr-stage-final-gcc-12.2.0/lib/gcc/avr/12.2.0/../../../../avr/sys-include"
ignoring nonexistent directory "/nix/store/ss76yfg4wj01ha9rjjgkr4qg0g76ivpa-avr-stage-final-gcc-12.2.0/lib/gcc/avr/12.2.0/../../../../avr/include"
ignoring duplicate directory "/nix/store/ss76yfg4wj01ha9rjjgkr4qg0g76ivpa-avr-stage-final-gcc-12.2.0/lib/gcc/avr/12.2.0/include-fixed"
#include "..." search starts here:
#include <...> search starts here:
/nix/store/fh0ccmn4vv7hncyfic4ph3hx34vmzsih-avrdude-7.1/include
/nix/store/ss76yfg4wj01ha9rjjgkr4qg0g76ivpa-avr-stage-final-gcc-12.2.0/lib/gcc/avr/12.2.0/include
/nix/store/ss76yfg4wj01ha9rjjgkr4qg0g76ivpa-avr-stage-final-gcc-12.2.0/lib/gcc/avr/12.2.0/include-fixed
/nix/store/r2jr0x50g79spg2ncm5kjmw74n7gvxzg-avr-libc-avr-2.1.0/avr/include
End of search list.
# 0 "<stdin>"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "<stdin>"
The 4 lines after
#include <...> search starts here:
are the paths we’re interested in.
We can probably skip the first one as it’s related to avrdude
but including it
won’t hurt and just adding all the extra include paths simplifies the process
compared to adding a select few. Manually editing compile_commands.json
to explicitly add these include files results in:
[
{
"arguments": [
"/nix/store/pfxqwrvm0y6lbs53injrl4sqz2njrpyl-avr-stage-final-gcc-wrapper-12.2.0/bin/avr-gcc",
"-mmcu=atmega328p",
"-std=c99",
"-Werror",
"-Wall",
"--param=min-pagesize=0",
"-c",
"-o",
"main.o",
"main.c",
"-I/nix/store/fh0ccmn4vv7hncyfic4ph3hx34vmzsih-avrdude-7.1/include",
"-I/nix/store/ss76yfg4wj01ha9rjjgkr4qg0g76ivpa-avr-stage-final-gcc-12.2.0/lib/gcc/avr/12.2.0/include",
"-I/nix/store/ss76yfg4wj01ha9rjjgkr4qg0g76ivpa-avr-stage-final-gcc-12.2.0/lib/gcc/avr/12.2.0/include-fixed",
"-I/nix/store/r2jr0x50g79spg2ncm5kjmw74n7gvxzg-avr-libc-avr-2.1.0/avr/include"
],
"directory": "/home/s/src/hello-avr",
"file": "/home/s/src/hello-avr/main.c",
"output": "/home/s/src/hello-avr/main.o"
}
]
Note the extra 4 -I...
arguments to avr-gcc
.
Opening main.c
in an LSP-enabled editor again, the errors are gone:
We can use LSP’s “jump to definition” feature to open <avr/io.h>
. In neovim
(with LanguageClient-neovim)
move the cursor over <avr/io.h>
and run :call
LanguageClient#textDocument_definition()
(obviously bind this to a key
combination):
For another example, move the cursor over a symbol defined in a header, such as
the UCSR0A
register on line 11 of main.c
:
Another handy feature of LSP is showing type signatures and documentation of
symbols. For example put the cursor over the call to printf
in main.c
and
run :call LanguageClient#textDocument_hover()
(again, assuming
LanguageClient-neovim):
One remaining problem is that compile_commands.json
contains a bunch of
absolute paths and so isn’t portable; we can’t check it into version control
which means we need to generate it after checking out the project. Rather than
using bear
directly and manually modifying the result, here is a script that
automates the modification we just performed. Note that it uses the jq
command
which may need to be installed.
#!/bin/sh
set -euo pipefail
# Script that prints a compilation database (typically stored in
# compile_commands.json) which contains additional include paths found by
# querying avr-gcc.
include_paths() {
# Print custom include paths used by the avr compiler to stdout, one per line.
CC=avr-gcc
$CC -E -Wp,-v - < /dev/null 2>&1 \
| sed -n '/#include <...> search starts here:/,/End of search list./p' \
| tail -n+2 \
| head -n-1 \
| xargs -n1 echo
}
compile_commands() {
# Print the compilation database that would normally go in
# compile_commands.json.
# This would be simpler if bear supported printing to stdout:
# https://github.com/rizsotto/Bear/issues/525
TMP="$(mktemp -d)"
trap "rm -rf $TMP" EXIT
OUTPUT=$TMP/x.json
bear --output $OUTPUT -- make --always-make > /dev/null
cat $OUTPUT
}
# Comma-separated list of quoted include paths with "-I" prepended to each. E.g.
# "-Ifoo","-Ibar"
COMMA_SEPARATED_QUOTED_INCLUDE_ARGS=$(
include_paths \
| xargs -I{} echo '"-I{}"' \
| tr '\n' , \
| sed 's/,$//'
)
# Add the extra include paths to the compilation database and print the result.
compile_commands | \
jq "map(.arguments += [$COMMA_SEPARATED_QUOTED_INCLUDE_ARGS])"
Put this script in tools/compile_commands_with_extra_include_paths.sh
and run
it with:
$ tools/compile_commands_with_extra_include_paths.sh > compile_commands.json
Now let’s make a simple LED Chaser circuit by attaching some LEDs to some of the digital IO pins on the Arduino.
Step 1 is working out which pins we’ll be using.
Here’s a close up of an Arduino Nano with the names of each pin visible:
For ease of reading here are the pin labels copied from the above image in a table in the same positions as they appear in the image above:
Left Side | Right Side |
---|---|
D13 | D12 |
3V3 | D11 |
REF | D10 |
A0 | D9 |
A1 | D8 |
A2 | D7 |
A3 | D6 |
A4 | D5 |
A5 | D4 |
A6 | D3 |
A7 | D2 |
5V | GND |
RST | RST |
GND | RX0 |
VIN | TX1 |
And this is the pinout from the Arduino official docs. Most pins have multiple functions. The I/O-Port functions are what we’re interested in. They are coloured orange without stripes in the following diagram. The key calls them “Microcontroler’s Port” and ATmega328P manual calls them “I/O-Port”.
Note that this diagram contains an error. The “ADC[6]” and “ADC[7]” pins have labels coloured both yellow and orange with “ADC[x]” on them but the orange labels are for I/O-Ports only so labeling them with “ADC” doesn’t make sense. We won’t be using these pins here, but I did do an experiment where I turned on all the I/O-Port pins and A6 and A7 pins didn’t turn on so they aren’t connected to an I/O-Port.
I only have enough space on my breadboard for 15 LEDs (after accounting for the Arduino’s pins and current-limiting resistors for the LEDs), so I’ve chosen 15 pins on the Arduino Nano that connect to I/O-Port pins on the microcontroller. Using the labels of pins physically printed on the Arduino board, these pins are: D2, D3, D4, D5, D6, D7, D8, D9, D10, A0, A1, A2, A3, A4, A5.
This is the circuit diagram for the LED chaser. The orientation of the Arduino is the same as in the above image, and the pins are all in the same positions and labels.
In the circuit, some pins are connected to a resistor in series with an LED. The cathode of each LED is connected to ground. The resistor is there to limit the current flowing through the pin and LED to prevent damage to each. I used 1K resistors here.
Here’s how the circuit looks on a breadboard:
Here’s the code for the LED chaser. It’s also available on github here.
Most of the I/O-Port pins on the Arduino have multiple possible functions and must be configured by writing to various registers. All pins which can function as I/O-Port pins are initially configured as I/O-Port pins, so no explicit configuration is necessary for this example. Note however that the pins labelled TX1 and RX0 are also I/O-Port pins (Port D, pins 0 and 1). They must be explicitly configured to behave as USART pins (sending and receiving data over a serial port - this is how “Hello, World!” is printed on startup). Even though nothing is plugged into these pins, they are connected internally to the Arduino’s USB chip. The LED chaser intentionally doesn’t use this pins as if it did we wouldn’t be able to print anything over USART.
#include <stdio.h>
#include <avr/io.h>
// The arduino clock is 16Mhz and the USART0 divides this clock rate by 16
#define USART0_CLOCK_HZ 1000000
#define BAUD_RATE_HZ 9600
#define UBRR_VALUE (USART0_CLOCK_HZ / BAUD_RATE_HZ)
// Send a character over USART0.
int USART0_tx(char data, struct __file* _f) {
while (!(UCSR0A & (1 << UDRE0))); // wait for the data buffer to be empty
UDR0 = data; // write the character to the data buffer
return 0;
}
// Create a stream associated with transmitting data over USART0 (this will be
// used for stdout so we can print to a terminal with printf).
static FILE uartout = FDEV_SETUP_STREAM(USART0_tx, NULL, _FDEV_SETUP_WRITE);
void USART0_init( void ) {
UBRR0H = (UBRR_VALUE >> 8) & 0xF; // set the high byte of the baud rate
UBRR0L = UBRR_VALUE & 0xFF; // set the low byte of the baud rate
UCSR0B = 1 << TXEN0; // enable the USART0 transmitter
UCSR0C = 3 << UCSZ00; // use 8-bit characters
stdout = &uartout;
}
// Represents a single I/O-Port pin
typedef struct {
volatile uint8_t* port; // pointer to the port's register
uint8_t bit; // (0 to 7) which bit in the register the port corresponds to
} led_t;
// List out each pin with an attached LED in the order we want them to flash
led_t leds[] = {
{ .port = &PORTD, .bit = 7 },
{ .port = &PORTD, .bit = 6 },
{ .port = &PORTD, .bit = 5 },
{ .port = &PORTD, .bit = 4 },
{ .port = &PORTD, .bit = 3 },
{ .port = &PORTD, .bit = 2 },
{ .port = &PORTB, .bit = 2 },
{ .port = &PORTB, .bit = 1 },
{ .port = &PORTB, .bit = 0 },
{ .port = &PORTC, .bit = 5 },
{ .port = &PORTC, .bit = 4 },
{ .port = &PORTC, .bit = 3 },
{ .port = &PORTC, .bit = 2 },
{ .port = &PORTC, .bit = 1 },
{ .port = &PORTC, .bit = 0 },
};
// The number of LEDs
#define N_LEDS (sizeof(leds) / sizeof(led_t))
// Turn on a single LED without affecting the state of the other LEDs
void led_on(led_t led) {
*led.port |= 1 << led.bit;
}
int main(void) {
USART0_init();
printf("Hello, World!\r\n");
// Set the data direction for each I/O-Port pin to "output".
// Each DDRx register controls whether each pin in I/O-Port x is
// an input pin (0) or an output pin (1).
DDRB = 0xFF;
DDRC = 0xFF;
DDRD = 0xFF;
// The starting points for the indices into the global `leds` array
// that will be on. We'll have 3 lights on at a time in a rotating
// pattern, evenly spaced out.
int indices[] = {0, 5, 10};
while (1) {
// Briefly turn off all the LEDs
PORTB = 0;
PORTC = 0;
PORTD = 0;
// Turn on just the pins at the indices, and increment each index
// wrapping around at N_LEDS
for (int i = 0; i < (sizeof(indices) / sizeof(indices[0])); i++) {
led_on(leds[indices[i]]);
indices[i] = (indices[i] + 1) % N_LEDS;
}
// Wait for a short amount of time before progressing
uint32_t delay = 100000;
while (delay > 0) {
delay -= 1;
}
}
return 0;
}
Use the same Makefile as in the previous example, then build and download the code to an Arduino with the command:
make && sudo avrdude -P /dev/ttyUSB0 -c arduino -p m328p -U flash:w:hello.elf
Remember to substitute /dev/ttyUSB0
with the device corresponding to your
Arduino.
I have a bunch of these USB to UART adapters lying around from another project.
It would be fun to try using one of these to talk to the Arduino directly via
its header pins rather than through its USB port. We won’t be able to program it
through this adapter but we will be able to power it and print over UART and see
the results in picocom
.
What are the 4 wires? There’s no brand name and I forget where I bought it from so I don’t know how to find documentation. However, the black wire is probably ground, and the red wire provides 5v. Of the two remaining wires, one of them is transmit (TX) and the other receive (RX). I don’t know which one is which and there doesn’t seem to be standardised colours as far as I can tell, so I’ll just guess.
Turns out the white wire is TX and the green wire is RX.
Disconnect the USB cable from the Arduino’s USB port as we should only power it from one source at a time, and we’ll be using the 5V pin on the USB to UART adapter to power it now.
I used male to male patch pins to connect the wires on the USB to UART adapter to my breadboard. Connect the wires as per this table:
UART to USB wire function | UART to USB wire colour | Arduino pin name |
---|---|---|
5v | Red | 5V |
Ground | Black | GND (any of them) |
TX (UART end) | White | TX1 |
RX (UART end) | Green | RX0 |
I clarify “UART end” for the TX an RX wires as for example the other end of the TX wire would be labelled “RX”; the Arduino transmits data on this wire and the device at the other end of the wire (the UART to USB adapter in this case) receives that data, and vice versa.
Again, disconnect the cable from the Arduino’s USB port before doing this. Here’s how it looks on my breadboard:
When you plug the USB end of the adapter into your computer it should show up as
a device /dev/ttyUSBx
, just like the Arduino did when we plugged it in. You
can’t program the device with avrdude
, but you can still connect to it with
picocom
to see it print “Hello, World!”.
Unlike before, connecting to the
device with picocom
will not cause the Arduino to reset, so you can miss the
message it prints when it turns on. Just press the reset button on the top of
the Arduino to restart it and you should see “Hello, World!” in the picocom
session. (Resetting the Arduino does not cause picocom
to disconnect as
technically picocom
is connected to the USB to UART adapter - not the Arduino.
It just displays whatever data arrives over the TX wire no matter what it’s
plugged into.
In fact, if you had a magnetized needle and a steady hand…never
mind).
Also note that we aren’t transmitting any data to the Arduino over UART, so you don’t even need to plug the green wire in at all.
One trick this adapter lets us perform is continuously seeing the output of the
Arduino, even while programming it. Previously if we wanted to program the
Arduino and see the messages it prints over UART we would need to first run
avrdude
to program it, then run picocom
to see its output. To program it
again we would have to stop picocom
before running avrdude
. That gets a bit
annoying.
We’re going to use the USB cable for programming and the USB to UART adapter for receiving messages printed by the Arduino.
To set this up, unplug the red 5v wire from the UART adapter, and plug the USB cable back into
the USB port. Keep at least the white and black wires attached from the setup
above - just make sure the red wire is unplugged as we’ll be powering the
Arduino with the USB cable once again. Now with both the USB cable (the one
attached to the Arduino’s USB port) and the USB to UART adapter both plugged
into your computer, you should see devices /dev/ttyUSB0
and /dev/ttyUSB1
corresponding to both of these devices. You can use dmesg
to determine which
one is which by unplugging and re-plugging them and watching the output of
dmesg
, or just see which one can be used to program the device with avrdude
.
Connect picocom
to the USB to UART adapter, and program the device with
avrdude
via the USB cable, and you’ll no longer need to close picocom
to
reprogram the Arduino!
Here’s an example of how this might look.
In one terminal:
$ sudo avrdude -P /dev/ttyUSB0 -c arduino -p m328p -U flash:w:hello.elf
And in the other terminal:
$ sudo picocom /dev/ttyUSB1
Depending on the order you plugged things in, the ttyUSB0
and ttyUSB1
in the
above commands might have to be swapped.
There are a couple of additional ways you can power the arduino besides the two mentioned above. When your Arduino is deployed in whatever wonderful project you’re planning to use it for, probably there won’t be a USB cable or USB to UART adapter attached to it.
Power solutions will depend on what power supply is available in your project. Mine has a 12v DC power supply, so here are my options:
The simplest solution is to attach the 12v DC supply directly to the VIN pin on the Arduino. Between the VIN and 5V pins on the Arduino there is a 5 volt voltage regulator (specifically, a LM117IMPX-5.0 - see the Arduino Nano schematics here).
This means that you can provide a range of voltages to the VIN pin and the voltage regulator will provide a steady 5v to the rest of the board. All the advice I can find recommends between 7 and 12 volts be provided to this pin. Too little and the voltage regulator might not be able to provide 5v at its output. Too much and it could overheat.
You can also power the Arduino directly from its 5V pin, bypassing the built-in voltage regulator. To do this, I’ll attach an external voltage regulator - a 78L05, like this one:
It looks like a transistor but it’s not. The right pin is the input which I’ll attach to the 12v supply. The middle pin is ground. The left pin is the output which I’ll connect to the 5V pin of the Arduino.
This shows how the voltage regulator will be attached between the 12v power supply and the Arduino, via its 5V pin:
Here it is on my breadboard:
And here’s proof that it didn’t immediately catch fire when I turned it on!
]]>Most of that time appears to be spent linking the binary rather than building individual libraries as the way I’ve structured the project means that only 3 or 4 libraries need to be recompiled after changing gameplay logic. The graphical version of Boat Journey has 307 dependencies when built on linux (at the time of writing). The number of dependencies varies across systems due to different libraries being used to get access to low-level system and graphics APIs. More than half of Boat Journey’s dependencies are transitive dependencies of a single crate wgpu which is the graphics library it uses for rendering.
I had an hypothesis that the slow link times were due to the large number of dependencies so I hacked together a SDL version of my chargrid library that sits between the game and IO (including rendering). SDL is a cross-platform media library with APIs for rendering, input handling, and audio (among other things). Unlike wgpu which is written mostly in rust, SDL’s rust library is just bindings to a shared library that must be installed separately, so there are fewer dependencies and less code to compile. When I build Boat Journey on top of the SDL version of chargrid it has only 147 dependencies on linux and incremental debug builds are down to about 2 seconds.
Because SDL depends on shared libraries, SDL builds of Boat Journey are harder to distribute than wgpu builds. Users would either have to install SDL themselves, or I would have to bundle SDL’s libraries into the game’s distribution. Also there are some problems anti-aliasing text in SDL that means the text doesn’t look as good as in wgpu for certain font sizes. For these reasons I won’t be releasing any games made with the SDL version of chargrid, but I’ll probably be using it for most of the testing I do prior to the release of a game thanks to its relatively fast compile times.
This screenshot shows Boat Journey running on SDL when I accidentally swapped some colour channels around during testing, and before I got it to clear the canvas between each frame.
]]>