diff --git a/README.md b/README.md index 546ecb4..9a609a0 100644 --- a/README.md +++ b/README.md @@ -34,4 +34,5 @@ These projects use the Xilinx Zynq SoCs which combine FPGA (PL) with ARM cores ( - **simple_vga** - a simple VGA project with a red grid and a yellow moving square. - **video_ip_test** - using Xilinx video IPs to show test patterns via VGA. - \ No newline at end of file + +- **thermal_vga** - displaying thermal sensor data via VGA \ No newline at end of file diff --git a/zynq/thermal_vga/README.md b/zynq/thermal_vga/README.md new file mode 100644 index 0000000..16875c2 --- /dev/null +++ b/zynq/thermal_vga/README.md @@ -0,0 +1,381 @@ +## AMG8833 8x8 Thermal Grid Sensor + VGA on Xilinx Zynq 7000 + +In this project, we read thermal data from an AMG8833 thermal grid sensor, and display it via VGA using an Avnet Minized. + +We communicate with the AMG8833 using I2C, using the IIC IP block on the PL. We use a dual port BRAM block to communicate the thermal data from the code running on the PS to the VGA module on the PL. + +The data is upscaled from 8x8 to 32x32 on the PS, before sending it to the PL. + +To send VGA output to the monitor, we used a Digilent PmodVGA adapter. + +[![video](https://img.youtube.com/vi/CUWag961Cfk/hqdefault.jpg)](https://youtu.be/CUWag961Cfk) + +(Click on image to see video.) + +## Block Diagram + +![bd](bd.png) + +## On the PL + +Here's the VGA module on the PL. It reads from BRAM, and splits up the video frame into a 32x32 grid to display the image. + + +``` +`timescale 1ns / 1ps +`default_nettype none + +module grid64_vga( + input wire reset, + input wire clk, + + // BRAM + output wire [31:0] addr, + input wire [31:0] data_in, + output wire [31:0] data_out, + output wire enable, + output wire [3:0] write_enable, + + // VGA + output reg [3:0] r, + output reg [3:0] g, + output reg [3:0] b, + output reg hsync, + output reg vsync, + + // debug LED + output reg LED + ); + + + // blink LED + reg [23:0] led_counter; + always @ ( posedge clk ) begin + + led_counter <= led_counter + 1; + + if (!led_counter) begin + LED <= ~LED; + end + end + + // + // BRAM + // + + + // set BRAM parameters + assign enable = 1'b1; + assign write_enable = 4'b0000; + + always @ ( posedge clk ) begin + + if (!led_counter) begin + LED <= ~LED; + end + end + + // + // VGA + // + + // create a strobe for 25 MHz VGA clock + // from: http://zipcpu.com/blog/2017/06/02/generating-timing.html + reg ck_stb; + reg [15:0] counter; + always @(posedge clk) begin + { ck_stb, counter } <= counter + 16'h8000; + end + + + // generate hsync and vsync + // Part of this code is adapted from the VGA example from http://8bitworkshop.com/ + reg [10:0] hpos; + reg [10:0] vpos; + + // declarations for TV-simulator sync parameters + // horizontal constants + parameter H_DISPLAY = 640; // horizontal display width + parameter H_BACK = 48; // horizontal left border (back porch) + parameter H_FRONT = 16; // horizontal right border (front porch) + parameter H_SYNC = 96; // horizontal sync width + // vertical constants + parameter V_DISPLAY = 480; // vertical display height + parameter V_TOP = 33; // vertical top border + parameter V_BOTTOM = 10; // vertical bottom border + parameter V_SYNC = 2; // vertical sync # lines + // derived constants + parameter H_SYNC_START = H_DISPLAY + H_FRONT; + parameter H_SYNC_END = H_DISPLAY + H_FRONT + H_SYNC - 1; + parameter H_MAX = H_DISPLAY + H_BACK + H_FRONT + H_SYNC - 1; + parameter V_SYNC_START = V_DISPLAY + V_BOTTOM; + parameter V_SYNC_END = V_DISPLAY + V_BOTTOM + V_SYNC - 1; + parameter V_MAX = V_DISPLAY + V_TOP + V_BOTTOM + V_SYNC - 1; + + wire hmaxxed = (hpos == H_MAX) || !reset; // set when hpos is maximum + wire vmaxxed = (vpos == V_MAX) || !reset; // set when vpos is maximum + + + // horizontal position counter + always @(posedge clk) + begin + + if (!reset) begin + hpos <= 0; + end + + if (ck_stb) begin + hsync <= ~(hpos>=H_SYNC_START && hpos<=H_SYNC_END); + if(hmaxxed) + hpos <= 0; + else + hpos <= hpos + 1; + end + end + + // vertical position counter + always @(posedge clk) + begin + + if (!reset) begin + vpos <= 0; + end + + if (ck_stb) begin + vsync <= ~(vpos>=V_SYNC_START && vpos<=V_SYNC_END); + if(hmaxxed) + if (vmaxxed) + vpos <= 0; + else + vpos <= vpos + 1; + end + end + + // display_on is set when beam is in "safe" visible frame + wire display_on = (hpos 560) + begin + + r <= {4{1'b0}}; + g <= {4{1'b0}}; + b <= {4{1'b0}}; + + end + else + begin + + // i = (hpos - 80)/W + // j = (vpos)/W + // index = 640*j + i + index <= (N*((640-vpos)/W) + (hpos - 80)/W); + + r <= data_in[7:4]; + g <= data_in[15:12]; + b <= data_in[23:20]; + + end + + end + end + +endmodule +``` + +## On the PS + +Here's the code running on the PS: + +``` +/* + * + * main.c + * + * This program uses the IIC driver t communicate with the AM88 8x8 thermal sensor. + * The data is sent to PL via BRAM, where it's sent out via VGA. + * + */ + +#include +#include +#include "platform.h" +#include "xil_printf.h" + +#include "xparameters.h" +#include "xiic.h" + +#include "blerp.h" + +//#include "arm_math.h" +#include "interpolated_colors.h" + +#define GRID_START (uint32_t*)XPAR_AXI_BRAM_CTRL_0_S_AXI_BASEADDR + + +/* + * The following constants map to the XPAR parameters created in the + * xparameters.h file. They are defined here such that a user can easily + * change all the needed parameters in one place. + */ +#define IIC_BASE_ADDRESS XPAR_IIC_0_BASEADDR + + +XIic IicInstance; /* The instance of the IIC device. */ + +int ByteCount; +u8 send_byte; +u8 write_data [256]; +u8 read_data [256]; +u8 i2c_device_addr = 0x69; // AMG88 I2C address + +uint8_t data_raw[128]; +int16_t data[64]; +uint32_t cgrid[64]; + +uint32_t cigrid[1024]; + + +u8 AMG88_ReadReg(u8 Reg, u8 *Bufp, u16 len) +{ + write_data[0] = Reg; + ByteCount = XIic_Send(IIC_BASE_ADDRESS, 0x69, (u8*)&write_data, 1, XIIC_REPEATED_START); + ByteCount = XIic_Recv(IIC_BASE_ADDRESS, 0x69, (u8*)Bufp, len, XIIC_STOP); + return(ByteCount); +} + +void AMG88_test() +{ + u8 count = AMG88_ReadReg(0x80, data_raw, 128); + + //xil_printf("Read %d bytes\n\r", count); + + uint8_t fr; + AMG88_ReadReg(0x02, &fr, 1); + xil_printf("Frame rate: %d \n\r", fr); + + for (int i = 0; i < count; i++) { + xil_printf("data[%d] = %d\n\r", i, data_raw[i]); + } +} + + +/** + * @brief Convert 12 bit Two's complement hex to decimal + */ +static int16_t amg88_pixel_temperature(uint8_t t00l, uint8_t t00h) +{ + int16_t shVal = ((int16_t)(t00h & 0x07) << 8) | t00l; + if (0 != (0x08 & t00h)) + { + shVal -= 2048; + } + shVal *= 64; + return (shVal); +} + +bool AMG88_read_grid() +{ + u8 count = AMG88_ReadReg(0x80, data_raw, 128); + if (count != 128) + return false; + + int j= 0; + for (int i = 0; i < 128; i+=2) { + uint8_t t00l = data_raw[i]; + uint8_t t00h = data_raw[i+1]; + int16_t temp = amg88_pixel_temperature(t00l, t00h); + data[j++] = temp/256; + } + +#if 0 //test + for(int i = 0; i < 64; i++) { + xil_printf("data[%d] = %d\n\r", i, data[i]); + } +#endif + + + return true; +} + +image_t src; +image_t dst; + +void copy_colour_grid(uint32_t* grid) +{ + + // map to colour + for(int i = 0; i < 64; i++) { + + uint16_t val = constrain_q15(100*data[i], MINTEMP, MAXTEMP); + uint32_t color_val = map_q15(val, MINTEMP, MAXTEMP, 0, COLORDEPTH - 1); + + cgrid[i] = hue_line[color_val]; + } + // interpolate + src.pixels = cgrid; + src.w = 8; + src.h = 8; + dst.pixels = grid; + dst.w = 32; + dst.h = 32; + scale(&src, &dst, 4, 4); +} + +int main() +{ + init_platform(); + + uint32_t* grid = GRID_START; + + // draw a diagonal red line on a white background + int N = 32; + for(int i = 0; i < N; i++){ + + for (int j = 0; j < N; j++) { + + int index = N*i + j; + + if (i == j) { + + grid[index] = 0x000000ff; + } + else { + grid[index] = 0x00ffffff; + } + + } + } + + // update BRAM with sensor data + for (;;) { + + AMG88_read_grid(); + copy_colour_grid(grid); + + usleep(1000); + + } + + cleanup_platform(); + return 0; +} + + +``` + +In addition, the header files for bilinear interpolation and the colour map are in the repository. diff --git a/zynq/thermal_vga/bd.png b/zynq/thermal_vga/bd.png new file mode 100644 index 0000000..0fef582 Binary files /dev/null and b/zynq/thermal_vga/bd.png differ diff --git a/zynq/thermal_vga/blerp.h b/zynq/thermal_vga/blerp.h new file mode 100644 index 0000000..b292486 --- /dev/null +++ b/zynq/thermal_vga/blerp.h @@ -0,0 +1,64 @@ +/* + * blerp.h + * + * Bilinear interpolation code based on: + * https://rosettacode.org/wiki/Bilinear_interpolation#C + * + * + * Created on: 19-Jul-2019 + * Author: Mahesh Venkitachalam + * Company: Electronut Labs + */ + +#ifndef SRC_BLERP_H_ +#define SRC_BLERP_H_ + +#include +typedef struct _image_t { + uint32_t *pixels; + unsigned int w; + unsigned int h; +} image_t; +#define getByte(value, n) (value >> (n*8) & 0xFF) + +static inline uint32_t getpixel(image_t *image, unsigned int x, unsigned int y){ + return image->pixels[(y*image->w)+x]; +} +static inline float lerp(float s, float e, float t){return s+(e-s)*t;} + +static inline float blerp(float c00, float c10, float c01, float c11, float tx, float ty){ + return lerp(lerp(c00, c10, tx), lerp(c01, c11, tx), ty); +} + +static void putpixel(image_t *image, unsigned int x, unsigned int y, uint32_t color){ + image->pixels[(y*image->w) + x] = color; +} + +static void scale(image_t *src, image_t *dst, float scalex, float scaley){ + int newWidth = (int)src->w*scalex; + int newHeight= (int)src->h*scaley; + int x, y; + for(x= 0, y=0; y < newHeight; x++){ + if(x > newWidth){ + x = 0; y++; + } + float gx = x / (float)(newWidth) * (src->w-1); + float gy = y / (float)(newHeight) * (src->h-1); + int gxi = (int)gx; + int gyi = (int)gy; + uint32_t result=0; + uint32_t c00 = getpixel(src, gxi, gyi); + uint32_t c10 = getpixel(src, gxi+1, gyi); + uint32_t c01 = getpixel(src, gxi, gyi+1); + uint32_t c11 = getpixel(src, gxi+1, gyi+1); + uint8_t i; + for(i = 0; i < 3; i++){ + //((uint8_t*)&result)[i] = blerp( ((uint8_t*)&c00)[i], ((uint8_t*)&c10)[i], ((uint8_t*)&c01)[i], ((uint8_t*)&c11)[i], gxi - gx, gyi - gy); // this is shady + result |= (uint8_t)blerp(getByte(c00, i), getByte(c10, i), getByte(c01, i), getByte(c11, i), gx - gxi, gy -gyi) << (8*i); + } + putpixel(dst,x, y, result); + } +} + + +#endif /* SRC_BLERP_H_ */ diff --git a/zynq/thermal_vga/interpolated_colors.h b/zynq/thermal_vga/interpolated_colors.h new file mode 100644 index 0000000..bcb3007 --- /dev/null +++ b/zynq/thermal_vga/interpolated_colors.h @@ -0,0 +1,39 @@ +#ifndef __INTERPOLATED_COLORS__ +#define __INTERPOLATED_COLORS__ + +#include + +/* +from colour import Color +COLORDEPTH=1024 +blue = Color("indigo") +colors = list(blue.range_to(Color("red"), COLORDEPTH)) +colors = [(int(c.red * 255), int(c.green * 255), int(c.blue * 255)) for c in colors] +rgb = [c[0] | c[1] << 8 | c[2] << 16 for c in colors] + */ + + +// this was exported from python, see +// https://learn.adafruit.com/adafruit-amg8833-8x8-thermal-camera-sensor/raspberry-pi-thermal-camera + +static const uint32_t hue_line[1024] = +{8519754, 8519754, 8519753, 8519753, 8519752, 8519752, 8519751, 8519751, 8519750, 8585286, 8585285, 8585285, 8585284, 8585284, 8585283, 8585283, 8585282, 8650818, 8650817, 8650817, 8650816, 8650816, 8650815, 8650814, 8650814, 8716349, 8716349, 8716348, 8716348, 8716347, 8716347, 8716346, 8716346, 8781881, 8781880, 8781880, 8781879, 8781879, 8781878, 8781878, 8781877, 8847413, 8847412, 8847412, 8847411, 8847410, 8847410, 8847409, 8847409, 8847408, 8912944, 8912943, 8912942, 8912942, 8912941, 8912941, 8912940, 8912940, 8978475, 8978474, 8978474, 8978473, 8978473, 8978472, 8978472, 8978471, 9044006, 9044006, 9044005, 9044005, 9044004, 9044003, 9044003, 9044002, 9109538, 9109537, 9109536, 9109536, 9109535, 9109535, 9109534, 9109534, 9175069, 9175068, 9175068, 9175067, 9175066, 9175066, 9175065, 9175065, 9175064, 9240599, 9240599, 9240598, 9240598, 9240597, 9240596, 9240596, 9240595, 9306131, 9306130, 9306129, 9306129, 9306128, 9306127, 9306127, 9306126, 9371662, 9371661, 9371660, 9371660, 9371659, 9371658, 9371658, 9371657, 9437192, 9437192, 9437191, 9437191, 9437190, 9437189, 9437189, 9437188, 9502723, 9502723, 9502722, 9502721, 9502721, 9502720, 9502720, 9502720, 9568512, 9568512, 9568768, 9569024, 9569024, 9569280, 9569536, 9569536, 9569792, 9635584, 9635584, 9635840, 9636096, 9636096, 9636352, 9636608, 9636608, 9702400, 9702656, 9702656, 9702912, 9703168, 9703168, 9703424, 9703680, 9769472, 9769472, 9769728, 9769984, 9769984, 9770240, 9770496, 9770496, 9836288, 9836544, 9836544, 9836800, 9837056, 9837056, 9837312, 9837568, 9903360, 9903360, 9903616, 9903872, 9903872, 9904128, 9904384, 9904640, 9904640, 9970432, 9970688, 9970688, 9970944, 9971200, 9971200, 9971456, 9971712, 10037504, 10037504, 10037760, 10038016, 10038272, 10038272, 10038528, 10038784, 10104320, 10104576, 10104832, 10105088, 10105088, 10105344, 10105600, 10105856, 10171392, 10171648, 10171904, 10171904, 10172160, 10172416, 10172672, 10172672, 10238464, 10238720, 10238976, 10238976, 10239232, 10239488, 10239744, 10239744, 10305536, 10305792, 10306048, 10306048, 10306304, 10306560, 10306816, 10306816, 10307072, 10372864, 10373120, 10373376, 10373376, 10373632, 10373888, 10374144, 10374144, 10439936, 10440192, 10440448, 10440448, 10440704, 10440960, 10441216, 10441472, 10507008, 10507264, 10507520, 10507776, 10507776, 10508032, 10508288, 10508544, 10574336, 10574336, 10574592, 10574848, 10575104, 10575360, 10575360, 10575616, 10641408, 10641664, 10641920, 10641920, 10642176, 10642432, 10642688, 10642944, 10642944, 10708736, 10708992, 10709248, 10709504, 10709504, 10709760, 10710016, 10710272, 10776064, 10776064, 10776320, 10776576, 10776832, 10777088, 10777344, 10777344, 10843136, 10843392, 10843648, 10843904, 10844160, 10844160, 10844416, 10844672, 10910464, 10910720, 10910976, 10910976, 10911232, 10911488, 10911744, 10912000, 10977792, 10977792, 10978048, 10978304, 10978560, 10978816, 10979072, 10979072, 11044864, 11045120, 11045376, 11045632, 11045888, 11046144, 11046144, 11046400, 11046656, 11112448, 11112704, 11112960, 11113216, 11113216, 11113472, 11113728, 11113984, 11179776, 11180032, 11180288, 11180544, 11180544, 11180800, 11181056, 11181312, 11247104, 11247360, 11247616, 11247872, 11247872, 11248128, 11248384, 11248640, 11314432, 11314688, 11314944, 11315200, 11315456, 11315456, 11315712, 11315968, 11381760, 11316480, 11316480, 11250944, 11185408, 11185408, 11119872, 11054336, 11054336, 10989056, 10923520, 10923520, 10857984, 10792448, 10792448, 10726912, 10661376, 10661632, 10596096, 10530560, 10530560, 10465024, 10399488, 10399488, 10333952, 10268672, 10268672, 10203136, 10137600, 10137600, 10072064, 10006528, 9940992, 9941248, 9875712, 9810176, 9810176, 9744640, 9679104, 9679104, 9613568, 9548288, 9548288, 9482752, 9417216, 9351680, 9351680, 9286144, 9220608, 9220608, 9155328, 9089792, 9089792, 9024256, 8958720, 8893184, 8893184, 8827648, 8762368, 8762368, 8696832, 8631296, 8565760, 8565760, 8500224, 8434688, 8369408, 8369408, 8303872, 8238336, 8238336, 8172800, 8107264, 8041728, 8041984, 7976448, 7910912, 7845376, 7845376, 7779840, 7714304, 7714304, 7649024, 7583488, 7517952, 7517952, 7452416, 7386880, 7321344, 7321344, 7256064, 7190528, 7124992, 7124992, 7059456, 6993920, 6928384, 6928384, 6862848, 6797568, 6732032, 6666496, 6666496, 6600960, 6535424, 6469888, 6469888, 6404608, 6339072, 6273536, 6273536, 6208000, 6142464, 6076928, 6011392, 6011648, 5946112, 5880576, 5815040, 5815040, 5749504, 5683968, 5618432, 5553152, 5553152, 5487616, 5422080, 5356544, 5356544, 5291008, 5225472, 5160192, 5094656, 5094656, 5029120, 4963584, 4898048, 4832512, 4832512, 4766976, 4701696, 4636160, 4570624, 4570624, 4505088, 4439552, 4374016, 4308480, 4243200, 4243200, 4177664, 4112128, 4046592, 3981056, 3981056, 3915520, 3850240, 3784704, 3719168, 3653632, 3653632, 3588096, 3522560, 3457024, 3391744, 3326208, 3326208, 3260672, 3195136, 3129600, 3064064, 2998528, 2998784, 2933248, 2867712, 2802176, 2736640, 2671104, 2671104, 2605568, 2540288, 2474752, 2409216, 2343680, 2278144, 2278144, 2212608, 2147072, 2081536, 2016256, 1950720, 1885184, 1885184, 1819648, 1754112, 1688576, 1623040, 1557760, 1492224, 1492224, 1426688, 1361152, 1295616, 1230080, 1164544, 1099264, 1033728, 1033728, 968192, 902656, 837120, 771584, 706048, 640768, 575232, 575232, 509696, 444160, 378624, 313088, 247552, 182272, 116736, 51200, 51200, 51200, 51201, 51202, 51203, 51204, 51461, 51462, 51463, 51464, 51465, 51465, 51466, 51467, 51724, 51725, 51726, 51727, 51728, 51729, 51730, 51731, 51988, 51988, 51989, 51990, 51991, 51992, 51993, 51994, 52251, 52252, 52253, 52254, 52255, 52256, 52257, 52257, 52514, 52515, 52516, 52517, 52518, 52519, 52520, 52521, 52778, 52779, 52780, 52781, 52782, 52783, 52784, 52785, 52786, 53042, 53043, 53044, 53045, 53046, 53047, 53048, 53049, 53306, 53307, 53308, 53309, 53310, 53311, 53312, 53313, 53570, 53571, 53572, 53573, 53574, 53575, 53576, 53577, 53834, 53835, 53836, 53837, 53838, 53839, 53840, 53841, 54098, 54099, 54100, 54101, 54102, 54103, 54104, 54105, 54106, 54363, 54364, 54365, 54366, 54367, 54368, 54369, 54370, 54627, 54628, 54629, 54630, 54631, 54632, 54633, 54634, 54891, 54892, 54893, 54894, 54895, 54896, 54897, 54898, 55155, 55156, 55157, 55158, 55159, 55160, 55161, 55162, 55419, 55420, 55421, 55422, 55423, 55424, 55425, 55427, 55428, 55685, 55686, 55687, 55688, 55689, 55690, 55691, 55692, 55949, 55950, 55951, 55952, 55953, 55954, 55955, 55956, 56213, 56215, 56216, 56217, 56218, 56219, 56220, 56221, 56478, 56479, 56480, 56481, 56482, 56483, 56484, 56486, 56743, 56744, 56745, 56746, 56747, 56748, 56749, 56750, 57007, 57008, 57010, 57011, 57012, 57013, 57014, 57015, 57016, 57273, 57274, 57275, 57276, 57278, 57279, 57280, 57281, 57538, 57539, 57540, 57541, 57542, 57544, 57545, 57546, 57803, 57804, 57805, 57806, 57807, 57809, 57810, 57811, 58068, 58069, 58070, 58071, 58072, 58074, 58075, 58076, 58333, 58334, 58335, 58336, 58337, 58339, 58339, 58083, 57827, 57572, 57316, 57060, 56804, 56548, 56548, 56292, 56036, 55781, 55525, 55269, 55013, 54757, 54501, 54245, 53989, 53734, 53734, 53478, 53222, 52966, 52710, 52454, 52198, 51943, 51687, 51431, 51175, 50919, 50663, 50663, 50407, 50152, 49896, 49640, 49384, 49128, 48872, 48616, 48360, 48105, 47849, 47593, 47337, 47081, 46825, 46569, 46569, 46313, 46058, 45802, 45546, 45290, 45034, 44778, 44522, 44266, 44011, 43755, 43499, 43243, 42987, 42731, 42475, 42219, 41964, 41708, 41452, 41196, 40940, 40684, 40428, 40172, 39917, 39661, 39405, 39149, 38893, 38637, 38381, 38381, 38126, 37870, 37614, 37358, 37102, 36846, 36590, 36334, 36078, 35823, 35567, 35311, 35055, 34799, 34543, 34287, 34031, 33776, 33520, 33264, 33008, 32752, 32240, 31984, 31728, 31473, 31217, 30961, 30705, 30449, 30193, 29937, 29681, 29426, 29170, 28914, 28658, 28402, 28146, 27890, 27634, 27379, 27123, 26867, 26611, 26355, 26099, 25843, 25587, 25332, 25076, 24820, 24564, 24308, 24052, 23540, 23284, 23028, 22773, 22517, 22261, 22005, 21749, 21493, 21237, 20981, 20726, 20470, 20214, 19958, 19702, 19446, 18934, 18678, 18423, 18167, 17911, 17655, 17399, 17143, 16887, 16631, 16376, 16120, 15864, 15608, 15096, 14840, 14584, 14328, 14073, 13817, 13561, 13305, 13049, 12793, 12537, 12025, 11769, 11514, 11258, 11002, 10746, 10490, 10234, 9978, 9722, 9211, 8955, 8699, 8443, 8187, 7931, 7675, 7419, 7164, 6652, 6396, 6140, 5884, 5628, 5372, 5116, 4861, 4349, 4093, 3837, 3581, 3325, 3069, 2813, 2558, 2046, 1790, 1534, 1278, 1022, 766, 510, 255}; + + +#define COLORDEPTH 1024 +#define MINTEMP 2600 +#define MAXTEMP 3200 + +#define MIN(a,b) (((a)<(b))?(a):(b)) +#define MAX(a,b) (((a)>(b))?(a):(b)) + +static inline uint32_t map_q15(uint16_t x, uint16_t in_min, uint16_t in_max, uint16_t out_min, uint16_t out_max) { + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; +} + + +static inline uint16_t constrain_q15(uint16_t val, uint16_t min_val, uint16_t max_val) { + return MIN(max_val, MAX(min_val, val)); +} + +#endif //__INTERPOLATED_COLORS__