Secrets of Arduino PWM

Pulse-width modulation (PWM) can be implemented on the Arduino in several ways. This article explains simple PWM techniques, as well as how to use the PWM registers directly for more control over the duty cycle and frequency. This article focuses on the Arduino Diecimila and Duemilanove models, which use the ATmega168 or ATmega328.

If you're unfamiliar with Pulse Width Modulation, see the tutorial. Briefly, a PWM signal is a digital square wave, where the frequency is constant, but that fraction of the time the signal is on (the duty cycle) can be varied between 0 and 100%.
PWM examples
PWM has several uses:

  • Dimming an LED
  • Providing an analog output; if the digital output is filtered, it will provide an analog voltage between 0% and 100% .
  • Generating audio signals.
  • Providing variable speed control for motors.
  • Generating a modulated signal, for example to drive an infrared LED for a remote control.

Simple Pulse Width Modulation with analogWrite

The Arduino's programming language makes PWM easy to use; simply call analogWrite(pin, dutyCycle), where dutyCycle is a value from 0 to 255, and pin is one of the PWM pins (3, 5, 6, 9, 10, or 11). The analogWrite function provides a simple interface to the hardware PWM, but doesn't provide any control over frequency. (Note that despite the function name, the output is a digital signal.)

Probably 99% of the readers can stop here, and just use analogWrite, but there are other options that provide more flexibility.

Bit-banging Pulse Width Modulation

You can "manually" implement PWM on any pin by repeatedly turning the pin on and off for the desired times. e.g.
void setup()
{
  pinMode(13, OUTPUT);
}

void loop()
{
  digitalWrite(13, HIGH);
  delayMicroseconds(100); // Approximately 10% duty cycle @ 1KHz
  digitalWrite(13, LOW);
  delayMicroseconds(900);
}
This technique has the advantage that it can use any digital output pin. In addition, you have full control the duty cycle and frequency. One major disadvantage is that any interrupts will affect the timing, which can cause considerable jitter unless you disable interrupts. A second disadvantage is you can't leave the output running while the processor does something else. Finally, it's difficult to determine the appropriate constants for a particular duty cycle and frequency unless you either carefully count cycles, or tweak the values while watching an oscilloscope.

Using the ATmega PWM registers directly

The ATmega168P/328P chip has three PWM timers, controlling 6 PWM outputs. By manipulating the chip's timer registers directly, you can obtain more control than the analogWrite function provides.

The AVR ATmega328P datasheet provides a detailed description of the PWM timers, but the datasheet can be difficult to understand, due to the many different control and output modes of the timers. The following attempts to clarify the use of the timers.

The ATmega328P has three timers known as Timer 0, Timer 1, and Timer 2. Each timer has two output compare registers that control the PWM width for the timer's two outputs: when the timer reaches the compare register value, the corresponding output is toggled. The two outputs for each timer will normally have the same frequency, but can have different duty cycles (depending on the respective output compare register).

Each of the timers has a prescaler that generates the timer clock by dividing the system clock by a prescale factor such as 1, 8, 64, 256, or 1024. The Arduino has a system clock of 16MHz and the timer clock frequency will be the system clock frequency divided by the prescale factor. Note that Timer 2 has a different set of prescale values from the other timers.

The timers are complicated by several different modes. The main PWM modes are "Fast PWM" and "Phase-correct PWM", which will be described below. The timer can either run from 0 to 255, or from 0 to a fixed value. (The 16-bit Timer 1 has additional modes to supports timer values up to 16 bits.) Each output can also be inverted.

The timers can also generate interrupts on overflow and/or match against either output compare register, but that's beyond the scope of this article.

Timer Registers

Several registers are used to control each timer. The Timer/Counter Control Registers TCCRnA and TCCRnB hold the main control bits for the timer. (Note that TCCRnA and TCCRnB do not correspond to the outputs A and B.) These registers hold several groups of bits:
  • Waveform Generation Mode bits (WGM): these control the overall mode of the timer. (These bits are split between TCCRnA and TCCRnB.)
  • Clock Select bits (CS): these control the clock prescaler
  • Compare Match Output A Mode bits (COMnA): these enable/disable/invert output A
  • Compare Match Output B Mode bits (COMnB): these enable/disable/invert output B
The Output Compare Registers OCRnA and OCRnB set the levels at which outputs A and B will be affected. When the timer value matches the register value, the corresponding output will be modified as specified by the mode.

The bits are slightly different for each timer, so consult the datasheet for details. Timer 1 is a 16-bit timer and has additional modes. Timer 2 has different prescaler values.

Fast PWM

In the simplest PWM mode, the timer repeatedly counts from 0 to 255. The output turns on when the timer is at 0, and turns off when the timer matches the output compare register. The higher the value in the output compare register, the higher the duty cycle. This mode is known as Fast PWM Mode.
The following diagram shows the outputs for two particular values of OCRnA and OCRnB. Note that both outputs have the same frequency, matching the frequency of a complete timer cycle.
Fast PWM Mode
The following code fragment sets up fast PWM on pins 3 and 11 (Timer 2). To summarize the register settings, setting the waveform generation mode bits WGM to 011 selects fast PWM. Setting the COM2A bits and COM2B bits to 10 provides non-inverted PWM for outputs A and B. Setting the CS bits to 100 sets the prescaler to divide the clock by 64. (Since the bits are different for the different timers, consult the datasheet for the right values.) The output compare registers are arbitrarily set to 180 and 50 to control the PWM duty cycle of outputs A and B. (Of course, you can modify the registers directly instead of using pinMode, but you do need to set the pins to output.)
  pinMode(3, OUTPUT);
  pinMode(11, OUTPUT);
  TCCR2A = _BV(COM2A1) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
  TCCR2B = _BV(CS22);
  OCR2A = 180;
  OCR2B = 50;
On the Arduino Duemilanove, these values yield:
  • Output A frequency: 16 MHz / 64 / 256 = 976.5625Hz
  • Output A duty cycle: (180+1) / 256 = 70.7%
  • Output B frequency: 16 MHz / 64 / 256 = 976.5625Hz
  • Output B duty cycle: (50+1) / 256 = 19.9%

The output frequency is the 16MHz system clock frequency, divided by the prescaler value (64), divided by the 256 cycles it takes for the timer to wrap around. Note that fast PWM holds the output high one cycle longer than the compare register value.

Phase-Correct PWM

The second PWM mode is called phase-correct PWM. In this mode, the timer counts from 0 to 255 and then back down to 0. The output turns off as the timer hits the output compare register value on the way up, and turns back on as the timer hits the output compare register value on the way down. The result is a more symmetrical output. The output frequency will be approximately half of the value for fast PWM mode, because the timer runs both up and down.
Phase-Correct PWM
The following code fragment sets up phase-correct PWM on pins 3 and 11 (Timer 2). The waveform generation mode bits WGM are set to to 001 for phase-correct PWM. The other bits are the same as for fast PWM.
  pinMode(3, OUTPUT);
  pinMode(11, OUTPUT);
  TCCR2A = _BV(COM2A1) | _BV(COM2B1) | _BV(WGM20);
  TCCR2B = _BV(CS22);
  OCR2A = 180;
  OCR2B = 50;
On the Arduino Duemilanove, these values yield:
  • Output A frequency: 16 MHz / 64 / 255 / 2 = 490.196Hz
  • Output A duty cycle: 180 / 255 = 70.6%
  • Output B frequency: 16 MHz / 64 / 255 / 2 = 490.196Hz
  • Output B duty cycle: 50 / 255 = 19.6%

Phase-correct PWM divides the frequency by two compared to fast PWM, because the timer goes both up and down. Somewhat surprisingly, the frequency is divided by 255 instead of 256, and the duty cycle calculations do not add one as for fast PWM. See the explanation below under "Off-by-one".

Varying the timer top limit: fast PWM

Both fast PWM and phase correct PWM have an additional mode that gives control over the output frequency. In this mode, the timer counts from 0 to OCRA (the value of output compare register A), rather than from 0 to 255. This gives much more control over the output frequency than the previous modes. (For even more frequency control, use the 16-bit Timer 1.)

Note that in this mode, only output B can be used for PWM; OCRA cannot be used both as the top value and the PWM compare value. However, there is a special-case mode "Toggle OCnA on Compare Match" that will toggle output A at the end of each cycle, generating a fixed 50% duty cycle and half frequency in this case. The examples will use this mode.

In the following diagram, the timer resets when it matches OCRnA, yielding a faster output frequency for OCnB than in the previous diagrams. Note how OCnA toggles once for each timer reset.
Fast PWM Mode with OCRA top
The following code fragment sets up fast PWM on pins 3 and 11 (Timer 2), using OCR2A as the top value for the timer. The waveform generation mode bits WGM are set to to 111 for fast PWM with OCRA controlling the top limit. The OCR2A top limit is arbitrarily set to 180, and the OCR2B compare register is arbitrarily set to 50. OCR2A's mode is set to "Toggle on Compare Match" by setting the COM2A bits to 01.

  pinMode(3, OUTPUT);
  pinMode(11, OUTPUT);
  TCCR2A = _BV(COM2A0) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
  TCCR2B = _BV(WGM22) | _BV(CS22);
  OCR2A = 180;
  OCR2B = 50;
On the Arduino Duemilanove, these values yield:
  • Output A frequency: 16 MHz / 64 / (180+1) / 2 = 690.6Hz
  • Output A duty cycle: 50%
  • Output B frequency: 16 MHz / 64 / (180+1) = 1381.2Hz
  • Output B duty cycle: (50+1) / (180+1) = 28.2%
Note that in this example, the timer goes from 0 to 180, which takes 181 clock cycles, so the output frequency is divided by 181. Output A has half the frequency of Output B because the Toggle on Compare Match mode toggles Output A once each complete timer cycle.

Varying the timer top limit: phase-correct PWM

Similarly, the timer can be configured in phase-correct PWM mode to reset when it reaches OCRnA.
Phase-Correct PWM with OCRA top
The following code fragment sets up phase-correct PWM on pins 3 and 11 (Timer 2), using OCR2A as the top value for the timer. The waveform generation mode bits WGM are set to to 101 for phase-correct PWM with OCRA controlling the top limit. The OCR2A top limit is arbitrarily set to 180, and the OCR2B compare register is arbitrarily set to 50. OCR2A's mode is set to "Toggle on Compare Match" by setting the COM2A bits to 01.
  pinMode(3, OUTPUT);
  pinMode(11, OUTPUT);
  TCCR2A = _BV(COM2A0) | _BV(COM2B1) | _BV(WGM20);
  TCCR2B = _BV(WGM22) | _BV(CS22);
  OCR2A = 180;
  OCR2B = 50;
On the Arduino Duemilanove, these values yield:
  • Output A frequency: 16 MHz / 64 / 180 / 2 / 2 = 347.2Hz
  • Output A duty cycle: 50%
  • Output B frequency: 16 MHz / 64 / 180 / 2 = 694.4Hz
  • Output B duty cycle: 50 / 180 = 27.8%
Note that in this example, the timer goes from 0 to 180 and back to 0, which takes 360 clock cycles. Thus, everything is divided by 180 or 360, unlike the fast PWM case, which divided everything by 181; see below for details.

Off-by-one

You may have noticed that fast PWM and phase-correct PWM seem to be off-by-one with respect to each other, dividing by 256 versus 255 and adding one in various places. The documentation is a bit opaque here, so I'll explain in a bit of detail.

Suppose the timer is set to fast PWM mode and is set to count up to an OCRnA value of 3. The timer will take on the values 012301230123... Note that there are 4 clock cycles in each timer cycle. Thus, the frequency will be divided by 4, not 3. The duty cycle will be a multiple of 25%, since the output can be high for 0, 1, 2, 3, or 4 cycles out of the four. Likewise, if the timer counts up to 255, there will be 256 clock cycles in each timer cycle, and the duty cycle will be a multiple of 1/256. To summarize, fast PWM divides by N+1 where N is the maximum timer value (either OCRnA or 255).

Now consider phase-correct PWM mode with the timer counting up to an OCRnA value of 3. The timer values will be 012321012321... There are 6 clock cycles in each timer cycle (012321). Thus the frequency will be divided by 6. The duty cycle will be a multiple of 33%, since the output can be high for 0, 2, 4, or 6 of the 6 cycles. Likewise, if the timer counts up to 255 and back down, there will be 510 clock cycles in each timer cycle, and the duty cycle will be a multiple of 1/255. To summarize, phase-correct PWM divides by 2N, where N is the maximum timer value.

The second important timing difference is that fast PWM holds the output high for one cycle longer than the output compare register value. The motivation for this is that for fast PWM counting to 255, the duty cycle can be from 0 to 256 cycles, but the output compare register can only hold a value from 0 to 255. What happens to the missing value? The fast PWM mode keeps the output high for N+1 cycles when the output compare register is set to N so an output compare register value of 255 is 100% duty cycle, but an output compare register value of 0 is not 0% duty cycle but 1/256 duty cycle. This is unlike phase-correct PWM, where a register value of 255 is 100% duty cycle and a value of 0 is a 0% duty cycle.

Timers and the Arduino

The Arduino supports PWM on a subset of its output pins. It may not be immediately obvious which timer controls which output, but the following table will clarify the situation. It gives for each timer output the output pin on the Arduino (i.e. the silkscreened label on the board), the pin on the ATmega chip, and the name and bit of the output port. For instance Timer 0 output OC0A is connected to the Arduino output pin 6; it uses chip pin 12 which is also known as PD6.
Timer outputArduino outputChip pinPin name
OC0A612PD6
OC0B511PD5
OC1A915PB1
OC1B1016PB2
OC2A1117PB3
OC2B35PD3

The Arduino performs some initialization of the timers. The Arduino initializes the prescaler on all three timers to divide the clock by 64. Timer 0 is initialized to Fast PWM, while Timer 1 and Timer 2 is initialized to Phase Correct PWM. See the Arduino source file wiring.c for details.

The Arduino uses Timer 0 internally for the millis() and delay() functions, so be warned that changing the frequency of this timer will cause those functions to be erroneous. Using the PWM outputs is safe if you don't change the frequency, though.

The analogWrite(pin, duty_cycle) function sets the appropriate pin to PWM and sets the appropriate output compare register to duty_cycle (with the special case for duty cycle of 0 on Timer 0). The digitalWrite() function turns off PWM output if called on a timer pin. The relevant code is wiring_analog.c and wiring_digital.c.

If you use analogWrite(5, 0) you get a duty cycle of 0%, even though pin 5's timer (Timer 0) is using fast PWM. How can this be, when a fast PWM value of 0 yields a duty cycle of 1/256 as explained above? The answer is that analogWrite "cheats"; it has special-case code to explicitly turn off the pin when called on Timer 0 with a duty cycle of 0. As a consequency, the duty cycle of 1/256 is unavailable when you use analogWrite on Timer0, and there is a jump in the actual duty cycle between values of 0 and 1.

Some other Arduino models use dfferent AVR processors with similar timers. The Arduino Mega uses the ATmega1280 (datasheet), which has four 16-bit timers with 3 outputs each and two 8-bit timers with 2 outputs each. Only 14 of the PWM outputs are supported by the Arduino Wiring library, however. Some older Arduino models use the ATmega8 (datasheet), which has three timers but only 3 PWM outputs: Timer 0 has no PWM, Timer 1 is 16 bits and has two PWM outputs, and Timer 2 is 8 bits and has one PWM output.

Troubleshooting

It can be tricky to get the PWM outputs to work. Some tips:
  • You need to both enable the pin for output and enable the PWM mode on the pin in order to get any output. I.e. you need to do pinMode() and set the COM bits.
  • The different timers use the control bits and prescaler differently; check the documentation for the appropriate timer.
  • Some combinations of bits that you might expect to work are reserved, which means if you try to use them, they won't work. For example, toggle mode doesn't work with fast PWM to 255, or with output B.
  • Make sure the bits are set the way you think. Bit operations can be tricky, so print out the register values and make sure they are what you expect.
  • Make sure you're using the right output pins. See the table above.
  • You'll probably want a decoupling capacitor to avoid spikes on the output.
An oscilloscope is very handy for debugging PWM if you have access to one. If you don't have one, I recommend using your sound card and a program such as xoscope.

Conclusion

I hope this article helps explain the PWM modes of the Arduino. I found the documentation of the different modes somewhat opaque, and the off-by-one issues unexplained. Please let me know if you encounter any errors.

Arduino + SheevaPlug = Cool Hardware Platform

I recently started using an Arduino in combination with a SheevaPlug as a convenient platform for hardware hacking. The two work together well, with complementary strengths. This describes my experience.

The Arduino

The Arduino is a popular microcontroller platform consisting of a small, inexpensive board and an easy-to-use C-based development environment. The board has multiple digital I/O pins and analog inputs, and a USB serial port connection for programming and communication. The Arduino can be interfaced with a wide variety of hardware. (The Arduino is kind of like a Microchip PIC development board, but uses the ATmega328 AVR microcontroller.) The Arduino is about the size of a credit card (but thick). It has connectors on top which will receive a daughter board (inexplicably called a "shield"), or jumper wires.
The Arduino board
The Arduino is very easy to program and use. The Arduino is programmed using the Wiring language, which is an extension of C with many libraries to interface with various types of hardware. The Arduino IDE provides an editor and compiler for a "sketch" (which is the name for an Arduino program), and downloads the code to the Arduino board over USB. After seeing the Arduino hype at the Maker Faire, I got a Adafruit Arduino Starter Pack, and I've been happy with it. More information about the Arduino is at www.arduino.cc; the playground is a good place to start.

You can do a lot with an Arduino, but it has some limitations. There's only 30KB of memory for programming (yes, kilobytes). Many things you might want to connect (Ethernet, SD card, USB hard drive or flash) require additional hardware, and even then are pretty limited.

The SheevaPlug

The Marvell SheevaPlug is a very small Linux server with a 1.2GHz 88F6281 ARM processor and 512MB of RAM and 512MB of flash storage. The SheevaPlug has a Gigabit Ethernet connection, a SD card slot, and a serial USB console connection. This all fits into a box the size of three cassette tapes that sells for $99 and uses about 5 watts of power.
The SheevaPlug
The SheevaPlug is designed to be plugged directly into the wall as a wall-wart, but the plug snaps out and can be replaced by a power coard. I'm currently using the "desktop" configuration with a power cord, since it's easier to connect things. Marvell recently gave me a SheevaPlug, and I've been enjoying it a lot.

Because the SheevaPlug is a full Linux system, the standard Linux software packages are available and easily installed with apt-get (full list). You can run X-windows (of course you need to VPN in). You can run Apache and PHP on the SheevaPlug and use it as a web server. The GCC toolchain runs on it. You can program it in C++, Python, Scheme, or whatever.

The main information source on the SheevaPlug is plugcomputer.org. Another useful site is computingplugs.com, a wiki about the SheevaPlug which is actually served by a SheevaPlug.

There are a few limitations of the SheevaPlug. The SheevaPlug is headless, so you need to access it via ssh or VNC. You can't run x86 binaries. The SheevaPlug's performance is roughly at the Pentium III level (benchmark), so it won't replace a state-of the-art server. Also, floating point performance is poor since there's no hardware FPU. The Linux install has some annoying minor issues with apt-get, DNS, NTP, and passwd (fixes). The SheevaPlug isn't really suited for low-level electronics projects because it doesn't have any accessible general-purpose I/O pins. (The CPU has 50 digital GPIO pins, but the SheevaPlug doesn't expose them.)

Arduino + SheevaPlug

The SheevaPlug and Arduino make a great pair together for hardware hacking projects since the SheevaPlug can easily provide the web server, computing, and storage functionality that the Arduino lacks, while the Arduino makes it easy to interface to hardware circuits. Moreover, the two are easily connected with a USB cable, and then they can communicate through simple serial communication (modulo kernel details below).
SheevaPlug with Arduino
The above picture shows the SheevaPlug and Arduino connected together by a USB cable. The SheevaPlug also has an Ethernet connection (green) and power (on the right). The Arduino has some jumpers to a breadboard circuit (described below).

A sample project: illumination recording

For an initial project, I decided to collect data on the illumnination in my room. (Temperature would have been more useful, but I didn't have a temperature sensor handy.) I implemented a simple Arduino sketch (i.e. program) to read illumination from a photocell and write it to the serial port. The SheevaPlug collects the data, generates a graph using gnuplot, and provides the data as a web page on the Internet. (Sorry, no URL to access it since I'm not currently running this.)

The screenshot below shows the web page that the SheevaPlug serves showing illumination across a few days. There are a lot of spikes due to turning lights on and off, but you can see where the sun rises, light increases through the day, peaking shortly before sunset. This web page illustrates the ability of the Arduino to measure analog data combined with the SheevaPlug's ability to collect data, plot it, and serve web pages.
SheevaPlug screenshot of browser

The Arduino code

Hooking up the Arduino to measure illumination was trivial; I used a 10K resistor and a photocell connected to an analog input pin. (I made the following schematic with Eagle.)
schematic of photocell connection

The Arduino sketch below (download) simply reads the analog input once a second and writes the value to the serial port as a decimal number. (One annoyance is the Arduino IDE won't run on the ARM processor, so I have to connect the Arduino to a separate computer to download the sketch.)

int ainPin = 2;     // select the analog input pin

void setup() {
  Serial.begin(9600);
}

void loop() {
  Serial.println(analogRead(ainPin), DEC);    // read the value from the sensor and write to serial
  delay(1000);
}

The Python code

The Python code that runs on the SheevaPlug (download) is straightforward. I used a simple Python web server to provide the graph page and some static files. It uses gnuplot to generate the graph. Some highlights of the code:

The pyserial library (apt-get install python-serial) provides simple access to the serial port. One thread reads lines from the serial port and dumps them to the data file along with a timestamp. Only one sample per minute is kept, and the others discarded. The Arduino may appear as a different device, e.g. /dev/ttyUSB0. If no such device appears when you plug the Arduino into the SheevaPlug, you probably need the kernel update below.

class Arduino(threading.Thread):
  def run(self):
    f = open('/tmp/data', 'a')
    # Port may vary from /dev/ttyUSB1
    self.ser = serial.Serial('/dev/ttyUSB1', 9600, timeout=10)
    self.ser.flushInput()
    old_timestamp = None
    while 1:
      data = self.ser.readline().strip()
      if data:
        timestamp = time.strftime("%m/%d/%Y %H:%M", time.localtime())
        if timestamp != old_timestamp:
          # Only log once per minute
          print >>f, timestamp, data.strip()
          old_timestamp = timestamp
        f.flush()

The other part of the Python code is an HTTP server using SimpleHTTPServer; a /graph URL runs gnuplot on the data file to generate a png image file. The returned web page loads the image, which is served by the Python server as a static file. (Since the SheevaPlug supports Apache, PHP, and mysql, I could have used them, but it seems like overkill for a simple demo.)

 def graph(self):
    g = os.popen('gnuplot', 'w')
    print >>g, GNUPLOT_CMD
    self.send_response(200)
    self.send_header('Content-type', 'text/html')
    self.end_headers()
    self.wfile.write(GRAPH_HTML)

Kernel with FTDI Serial support

The one stumbling block I encountered when using the SheevaPlug and Arduino together is the SheevaPlug kernel doesn't include FTDI support, which is necessary to access the Arduino via USB. An upgraded kernel is available, and much to my surprise it installed with no problems.

Where from here?

So far, I've found the Arduino and SheevaPlug to work together well. I have various plans to use the Arduino + SheevaPlug for home monitoring and control, solar panel monitoring, IR remote control, and other random projects. If there's interest, maybe I'll write about them in the future.

Inside the news.yc ranking formula

The recent arc3 release of Arc includes news.arc, the Arc source code for the Hacker News forum. Examining this code can give some insight into the ranking algorithm that selects the top articles on the Hacker News forum.

The ranking formula

In outline, each item is given a ranking, and the articles are sorted according to the ranking. The simplistic way to think about ranking is the number of votes is divided by time, so more votes results in a higher ranking, but the ranking also drops over time. The votes are raised to a power less than one, while the time is raised to a power greater than one, so time has more effect than votes. Some additional penalties also may be applied to the ranking.

To be specific, the following is the ranking code from news.arc:

(= gravity* 1.8 timebase* 120 front-threshold* 1
   nourl-factor* .4 lightweight-factor* .3 )

(def frontpage-rank (s (o scorefn realscore) (o gravity gravity*))
  (* (/ (let base (- (scorefn s) 1)
          (if (> base 0) (expt base .8) base))
        (expt (/ (+ (item-age s) timebase*) 60) gravity))
     (if (no (in s!type 'story 'poll))  1
         (blank s!url)                  nourl-factor*
         (lightweight s)                (min lightweight-factor*
                                             (contro-factor s))
                                        (contro-factor s))))

This algorithm can be expressed as a (slightly simplified) equation:

rank=\frac{ \left( score-1 \right) ^{.8}}{ \left( age _{hours} + 2 \right) ^ {1.8}} * penalties

This ranking algorithm is used for items on the front page as well as for ordering the comments on an item.

The key factor in the ranking formula is the exponent on time (i.e. the optional parameter gravity) is higher than the exponent on the votes. As a result, even if an item keeps getting a large number of votes, eventually the denominator will overwhelm it and its ranking will drop. For example, if a popular item gets 100 votes an hour, eventually it won't be able to keep pace with a new item getting 10 votes an hour. This ensures that even the most popular items won't stay on the list forever. In other words, gravity controls how fast articles get pulled down in ranking.

By default, the scoring function scorefn is the realscore, which is the net votes for an item ignoring any upvotes from pontential sockpuppet accounts. Sockpuppet accounts are accounts created to manipulate voting, and an account is considered potentially a sockpuppet based on several simple factors.

Because the score is given an exponent of .8 (for positive scores), the benefit of large numbers of votes is reduced. Mathematically, though, this exponent could be eliminated. Since only relative rankings matter, raise everything to the 1/.8 power, and the numerator's exponent disappears, along with the need for the negative check. This would be a win from the "code golf" perspective.

A few other constants are of interest. Since 1 is subtracted from the score, an item starts off with a rank of 0. In the denominator, 120 minutes are added to the time. Thus, an article can be considered two hours old at time of posting, limiting the ranking boost for very recent articles. The front-threshold* specifies a minimum ranking score for articles to appear.

Penalties

A story (i.e. a normal posting) or poll can receive additional ranking penalties. The penalties are not applied to comments or poll options. The penalties are as follows:
  • An item with a blank url is penalized by nourl-factor*. This lowers the ranking of an "Ask HN" item, for instance.
  • A "lightweight" item is penalized.
  • A "controversial" item is penalized.
An item is considered lightweight if the title is a "rallying cry", the post is an image, or the URL's site is in a list of lightweight sites. A lightweight item is penalized by lightweight-factor*. (The code doesn't show how a "rallying cry" is determined.)

The contro-factor penalty is applied to controversial articles. The contro-factor is computed as:

(def contro-factor (s)
  (aif (check (visible-family nil s) [> _ 20])
       (min 1 (expt (/ (realscore s) it) 2))
       1))
An item with too many comments (at least 20 and more comments than upvotes) gets the contro-factor penalty. An item's visible-family is the number of visible comments (plus one for the item itself). If the visible-family is more than 20, the penalty is:

min\left[ 1, \left( \frac{realscore}{visible\-family}\right) ^{2} \right]

A lightweight item gets the worse of the "lightweight" and "contro-factor" penalties. The contro-factor penalty is only applied if less than 1; i.e. an item can't get a boost from it. Because the ratio is squared, the effect of the penalty is more substantial. I would have expected contro-factor to penalize controversial comment subthreads too, but apparently it doesn't.

How the ranking is performed

When the server starts up, it loads the top stories from a topstories file. If the file is not present, it uses the ranking algorithm to select the top 180 stories out of the most recent 1000 stories. Stories normally are re-ranked by adjust-rank only when they receive a vote. Interestingly, only the updated item is re-ranked, rather than doing a global re-rank, probably for efficiency reasons. The adjust-rank function also saves the top 180 items to thetopstories file to disk.

There's also a background reranking thread to rerank a random story from the top 50 every 30 seconds (again using adjust-rank). This ensures that stories that are no longer receiving any votes at all get reranked occasionally.

As far as I can tell, the list of ranked stories never gets shrunk (except on restarts), but the number of displayed stories is capped at 210; you will hit this limit if you keep paging through the top stories.

Comparison with Arc2

The old code in arc2 is much simpler:
(= gravity* 1.4 timebase* 120 front-threshold* 1)

(def frontpage-rank (s (o gravity gravity*))
  (/ (- (realscore s) 1)
     (expt (/ (+ (item-age s) timebase*) 60) gravity)))
The main differences are that arc3 has higher gravity, so articles will drop off faster; arc3 has an exponent on the numerator, and arc3 adds the various penalties. This illustrates that the ranking formula is changing and gaining complexity over time.

Conclusions

There may be differences there are between the live Hacker News server and news.arc, so my analysis may not exactly describe what's really happening. I'd assume the code is constantly being modified; comparison with arc2 shows that the ranking formula has become considerably more complex. In addition, news.arc has some YC-specific stuff removed.. Finally, since this is Arc, the ranking constants and even the algorithms can be changed at the REPL while the server is running. Thus, the live ranking algorithm might never actually exist in a source file.

So, what's the secret to getting a high-ranked article? I don't see any magic bullets in the code, just the obvious: get lots of upvotes in a short period of time, avoid penalties (don't post fluff), and don't use sockpuppets.

What's new in arc3?

The long-awaited new update of Arc (arc3) has been made available. I've examined the release to determine what's new in arc3 beyond the mysterious new logo: arc logo.

arc3 is a major change from arc2, with about 3000 lines and most files changed. The following gratuitous pie chart illustrates that the largest number of changes are in the news server (news.arc), with many changes to the fundamental language (ac.scm and arc.arc) as well as the web and application servers (app.arc and srv.arc).
Summary of changes
The news server has been heavily modified with new ranking, support for polls, and improved spam rejection, among other features. A new "how-to-run-news" file explains how to set up your own Hacker News-like server. I will discuss the news server, web server, app server, and html library in more detail in a future article. I will focus this article on the language and main libraries.

If you're looking to try arc3, it can be downloaded here. Arc3 has significant incompatibilities with previous versions that will cause problems if you're not prepared.

Incompatibilities

  • Arc now requires mzscheme 372. Arc will not work with the version 352 required by arc2, or with the newer versions of mzscheme.
  • flush-output is disabled by default on write, disp, and functions that use these. See declare below.
  • The result of accum is no longer in reverse order.
  • news.arc is no longer loaded as part of arc.
  • a.b.c is now ((a b) c), not (a b c)
  • a!b!c is now ((a 'b) 'c), not (a 'b 'c)
  • The set function has been renamed assign, and the assert function has been renamed set.
  • writefile1 removed.
  • flat no longer supports stringstoo option.
  • to-nearest renamed to nearest.
  • random-elt renamed to rand-elt.
  • firstn-that renamed to retrieve.
  • plural renamed to pluralize.

New functions

  • int function coerces to an integer.
  • force-close discards buffered output and closes the descriptor.
  • mvfile moves a file.
  • memory returns the current memory use.
  • sin, cos, tan, and log are finally available.
  • declare is used to set system variables: explicit-flush to control automatic output flushing, and direct-calls for the function call optimization.
  • timedate converts seconds to date.
  • copylist copies a list.
  • defs applies def to name, args, body triples to define multiple functions at once.
  • get provides simpler table / array lookup. get!foo creates a function that takes a table and looks up 'foo. (Actually, it's a generic mechanism to create a function to apply an argument to something.)
  • writefile writes to a temporary file first.
  • letter tests if a character is a letter.
  • sum sums up function results.
  • med returns median of list.
  • minutes-since returns minutes since a time.
  • defcache wrapper around cache to reduce boilerplate.
  • datestring creates "y - m - d" string.
  • or= modifies a variable if the variable is false.
  • out evaluates and prints an expression.
  • fromdisk, diskvar, disktable, and todisk provide a simplified mechanism for loading and storing data.
  • halve splits a string in two on the first separator.
  • positions returns a list of positions where a test is true.
  • lines splits a string into lines.
  • urlencode urlencodes a string.
  • nonblank returns a string if it's nonblank.
  • plural returns a count and pluralized word.

Bug fixes

  • atomic-invoke fixed.
  • socket-accept uses custodians so web servers can forcibly close connections if the clients aren't reading data.
  • coerce of string into int rounds the value as does number to char.
  • list modified to copy the list.
  • safeset now writes warnings to stderr.
  • each fixed to avoid conflict with afn.
  • cut fixed.
  • trues fixed.
  • whiler fixed.
  • rand-string now more random to help fix huge security hole.
  • split fixed for negative positions..
  • memo now memoizes nil results too.
  • readline will terminate on nil.
  • copy fixed.
  • hours-since fixed.
  • date fixed to avoid system operation.
  • intersperse now works with nil.
  • load fixed.
  • posmatch fixed.
  • num fixed to handle negative numbers.

Optimizations

  • complement in function position
  • The <, >, and + operations when applied to two arguments.
  • (foo bar) is optimized to call foo directly if foo is a global bound to a function. This is controlled by declare.

Other changes

  • Debugging has been improved by giving the Arc names of things to Scheme, and turning on line counting.
  • A default value can be provided when accessing a hash table.
  • Currying was largely implemented but not enabled.
In conclusion, arc3 has some nice improvements, bug fixes, and optimizations, but nothing too dramatic in the language itself. If you're using the Arc news server, you'll definitely want arc3 for the security fixes. On the other hand, the unofficial Anarki release of Arc has a lot of interesting features too.

Disclaimers: I have provided my interpretations of the changes in arc3, and I'm sure there are some errors; please provide feedback if you find mistakes. Note that I am not connected with the arc team in any way, and this is not an "official" list of changes. In addition, arc3 may change without warning, so any of the above can be obsoleted. (There are some good reasons for real version numbers, bug tracking, and source code control, but I won't belabor that point.)

Arc errors (slightly) demystified

If you've programmed in Arc, you've probably noticed that the error messages are often cryptic at best. Error handling is a major weakness of the current Arc implementation; I've wasted a lot of time trying to figure out why my code fails, because the error message has no connection to the actual problem.

This article gives examples of some mysterious error messages, how they arise, and what they mean. Hopefully this can save some debugging time.


Usually Arc doesn't provide any indication of where the error occurred. In the cases where Arc does provide location information, the location is a byte offset, not a line number, which is hard to interpret:
arc> (load "t.arc")
Error: "t.arc::720: read: expected a ')'"
If you're loading a file, how do you find byte offset 720? One way is dd if=t.arc bs=1 skip=720 which will display the file from the 720th character onward.

Similar errors can be generated from REPL input. In this case, the unexpected parenthesis is at character 910 that's been typed to the REPL (although that's unlikely to be useful):

arc> )
Error: "UNKNOWN::910: read: unexpected ')'"

arc> (def foo (x) (prn x))
#<procedure: foo>
arc> (foo 1 2)
Error: "procedure  foo: expects 1 argument, given 2: 1 2"
This error message is straightforward - too many arguments were given to the procedure..
arc> (def (bar (x) (prn x)))
Error: "#<procedure>: expects at least 2 arguments, given 1: (bar (x . nil) (prn x . nil) . nil)"
In this case, the expected arguments error is more mysterious. Misplaced parentheses generate this error from the depths of the interpreter.
arc> (+ 1 ("s"))
Error: "car: expects argument of type <pair>; given ()"
You probably have extra parentheses, attempting to index into a string. For a number, the error message is much clearer:
arc> (+ 1 (42))
Error: "Function call on inappropriate object 42 ()"

arc> (def foo (a (b c)) (prn a))
arc> (foo 1 2)
Error: "car: expects argument of type <pair>; given 2"
Destructuring bind fails because argument format doesn't match function definition.
arc> (def f (o x (table)) (prn x))
arc> (f 42)
Error: "car: expects argument of type <pair>; given ()"
You need two sets of parentheses in the def, one around the arguments, and one around the optional argument:
arc> (def f ((o x (table))) (prn x))

arc> (+ 1 "s")
Error: "+: expects type  as 2nd argument, given: \"s\"; other arguments were: 1"
Straightforward: wrong type to +.
(= t 42)
Error: "Can't rebind t"
The atoms t and nil can't be changed. Don't use t as a variable.
arc> (map (prn) '(a b c))
Error: "Function call on inappropriate object nil (a)"
You need to pass a function to map, such as prn or [prn _] rather than (prn)
arc> (car "abc")
Error: "Can't take car of \"abc\""
Car and cdr don't work on strings. You'll also get this error if the function erroneously uses car on a string internally:
arc> (counts "abc")
Error: "Can't take car of \"abc\""

arc> (with a 4 (+ a 1))
Error: "Can't take cdr of a"
The mysterious "can't take cdr" error shows up if you don't have parentheses around the variable assignments. You may be thinking of let, which doesn't use the parentheses and assigns a single variable.

Let me know if you have any other candidates for mystery errors.

F# for Cheapskates

Since I've been hearing good things about the F# language from various people, I wanted to give it a try, but I wasn't interested in spending $250 on Microsoft Visual Studio. It turns out, however, that you can run F# for free in several ways.

F# with Visual Studio Shell

On Windows, you can run F# with a complete IDE by using Visual Studio Shell (not to be confused with Visual Studio or Visual Studio Express). Visual Studio Shell is an IDE without any included languages, and is available for free.

Installation

(Instructions updated August 2009 for latest versions.)

  • Download Visual Studio 2008 Shell SP1 (integrated mode).
  • Run the download. Note: this download does not install Visual Studio Shell; it unpacks a "redistributable package" for Visual Studio Shell that contains the installer. Don't ask why. Make sure you note where it puts this, probably C:\VS 2008 Shell Redist\Integrated Mode.
  • Run the Visual Studio Shell installer vside.enu.exe that was unpacked above. Important: you must not have F# installed at this point; if you already have it installed, uninstall it first.
  • Make sure Microsoft Visual Studio 2008 shows up in your Start menu.
  • Download and install F# 2009 CTP (msi).

Running F# inside Visual Studio Shell

Start Visual Studio Shell. Start F# should appear under View -> Other Windows -> F# Interactive; Control-Alt-F should also work. Type into the F# Interactive window:
printfn "Hello world!";;
Hello world!
val it : unit = ()
Note that in the interactive window, you must enter ;; to have your input processed.

F# in Visual Studio

To see the F# tutorial, click "Create Project ..." in the "Recent Projects" window, then double-click "F# Tutorial".

To create a new program, click "Create Project ..." in the "Recent Projects" window, then double-click "F# Application". Type code into the "Program.fs" window, for example:

printfn "Hello world!"

open System
Console.ReadKey(true) |> ignore
To run the program, hit F5. The purpose of the last two lines is to keep the console window open until you hit a key; otherwise, the window will only flash up briefly. Note that the double-semicolons aren't required.

F# in Visual Studio

You can also highlight text in the program window and press Alt-Enter to have it executed in the F# Interactive window. Visual Studio includes a debugger with breakpoints and all. Select Debug -> Start Debugging to run under the debugger.

F# from the Windows command line

You can also skip Visual Studio and run F# from the command line. To do that, just download F# as above. Then run the F# interpreter fsi.exe either from a cmd window or from the Start menu:
C:\Program Files\FSharp-1.9.6.2\bin\fsi.exe
> printfn "Hello world!";;
Hello world!
val it : unit = ()

F# with Mono

Another option is running F# with Mono. This provides a way to run F# on Linux or OS X. To do this, download and unzip F# 2008 CTP (zip). Download and install Mono 2.X. The Readme file in the F# distribution provides all the details on running F# with Mono, so look there.

(I wasn't too successful with Mono; it's no longer supported on RedHat, and compiling from sources ran into libgdiplus incompatibility. I could only run fsi.exe with the --no-gui flag.)

F# under mono

F# vs Arc

Since this is nominally an Arc blog, I should compare F# and Arc. The F# language has some nice features such as type inference, currying, and pattern matching. On the other hand, it lacks powerful Lisp-style macros, and is not as terse as Arc. The F# implementation is much, much more polished than Arc. It has extensive documention, an IDE, real error messages, reasonable performance, huge libraries, a compiler, and so forth. Overall, I find programming in F# more enjoyable than in Arc, and find it easier to get something done in F# after using it for a few hours than in Arc after several months.

Next steps

Once you have F# running, you can take a look at Microsoft's F# in 20 Minutes tutorial to learn more. I'm currently going through the book Expert F#.

One of the interesting things about F# is you can easily write graphical Windows applications. A couple quick hints: to use the Windows Forms dlls, go to "Project -> Add Reference" and click "System.Windows.Forms", and then "ok". To suppress the console window, go to "Project -> ConsoleApplication2 Properties..." and select "Output type: Windows Application".

I found (and I realize this is an obvious lesson) that I learned much more from actually writing simple F# programs than from reading blog posts about it. So if you're still reading, stop now, install F#, and write some code!

Why Unicode is important

A couple months ago I went to a French cafe at the Denver airport. Outside the restaurant was a large rotating menu, clearly costly to manufacture. Some of the beverages on the menu puzzled me: "Caf  Latte", "Caf  Americano", and "Caf  Mocha".
faulty menu
When I saw "Proven al Salad", I realized that they had suffered an unfortunate accented character mishap; the menu was supposed to offer "Café Latte", "Café Americano", "Café Mocha", and "Provençal Salad". Unfortunately, the accented characters disappeared somewhere in the sign printing process. The font, likely Marker Felt Thin, has accented characters, so they should have been able to get it right.

I {entity} unicode So... can any deep insight be extracted from this, or is it just a somewhat entertaining picture? My first conclusion is that notwithstanding the Arc Unicode debacle, support for non-ASCII characters is important even if you're a sign shop in Denver. My second conclusion (based on personal experience) is that everything conspires to destroy non-ASCII characters, and it's non-trivial to get them right. My third conclusion - which will make sense if you've seen UTF-8 get misinterpreted as latin-1 - is the restaurant should be grateful that they didn't end up with "Café Latte".