Fixed point HSV to RGB

12 Feb 2017

Something different this week, a little chunk of code I wrote so I could make one of those cheap LED colour rings go through a nice colour cycle.

Cycling through the colours is much easier to do in HSV as you can just increment the hue and you get something that looks pretty good straight away.

The only problem is most of the formulas use floating point, which isn’t great to be doing on a microcontroller.

So I came up with this fixed point implementation.

void hsv2rgb_fixed(uint8_t h, uint8_t s, uint8_t v, uint8_t *r, uint8_t *g, uint8_t *b)
{
    // As we're using 0-256 rather than 0-360 this is 256/60
    const uint16_t DIV_INTO_6 = 0x0600;

    // Remove the fractional as this is a float*float in the orignal.
    uint16_t c = (s * v) >> 8;

    uint32_t t = ((h+1) * DIV_INTO_6);
    uint8_t fiddle = (t & 0x00010000)!=0;

    // This line is the equivalent of (1-abs(fmod(hf/60,2) - 1))
    /*
        h/60 : gives 0..6
        1: fmod(^,2) : gives 0..2 of above with wrap around
        2: ^-1 : gives -1..1
        3: abs(^) : removes negative
        4: (1-^)

        1:      2:     3:     4:
        2.0 ->  1.0 -> 1.0 -> 0.0
        1.5 ->  0.5 -> 0.5 -> 0.5
        1.0 ->  0.0 -> 0.0 -> 1.0
        0.5 -> -0.5 -> 0.5 -> 0.5
        0.0 -> -1.0 -> 1.0 -> 0.0

        In other words, map 0.0-1.0 directly and 2- the value for 1.0-2.0 .
    */

    t = (t & 0x00010000)!=0 ? 0x00020000 - (t & 0x0001ffff) : t & 0x0001ffff;

    /* As this was made by multiplying by a 16-bit constant to get the fixed point value
        it needs shifting back by 16 bits right to remove the fractional part.
    */
    uint32_t x = (t * c) >> 16; // Don't need the fractional part, so shift it out

    // Apply a little fiddle for the rounding issues.
    // Scaling 6 by the size of c removes most of the error and doesn't overshoot.
    // If you don't care about being +/-6 on one of the RGB values you can remove this.
    if (!fiddle) {
        if (x>6) {
            x-=((6*c)>>8);
        }
    }
    else {
        if (x<249) {
            x+=((6*c)>>8);
        }
    }
    uint16_t m = v - c;

    // Shift to get the real result of DIV_INTO_6
    uint8_t quad = ((h+1) * DIV_INTO_6) >> 16;

    switch(quad) {
        case 0:
            *r=c+m; *g=x+m; *b=m;
            break;
        case 1:
            *r=x+m; *g=c+m; *b=m;
            break;
        case 2:
            *r=m; *g=c+m; *b=x+m;
            break;
        case 3:
            *r=m; *g=x+m; *b=c+m;
            break;
        case 4:
            *r=x+m; *g=m; *b=c+m;
            break;
        case 5:
            *r=c+m; *g=m; *b=x+m;
    }
}