Arduino WS2812 control

Hello Community,

I am using WS2812 RGB LED strips with atmega32u4 Arduino Leonardo.

I am using pin33 (PE2) of the atmega32u4 as a data pin for the RGB LED strip.

as before, I used Adafruit_neopixel & fastLED libraries for WS2812

but I found that the PE2 pin has no extension to the Arduino header pins

so I can not directly write that pin to the object creation of the LED strip using either neopixel or fastLED.

what could be the solution to this?

HI,
Rewrite you code to use a pin that is available on the Leonardo board.

Tom... :grinning: :+1: :coffee: :australia:

yeah, Tom

this is correct.. :grinning_face_with_smiling_eyes:

but think... If I wanted to do this, why would I have posted this here? :slightly_smiling_face:

Hi,
So what is stopping you from doing this?
What is the big picture of your project?

Tom.... :grinning: :+1: :coffee: :australia:

I used Adafruit_neopixel and was creating the object for each LED bunch as below,

#define NUM_LEDS                    3
#define C1_STRIP              8        
#define C2_STRIP             12   

Adafruit_NeoPixel     C1(NUM_LEDS, C1_STRIP, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel     C2(NUM_LEDS, C2_STRIP, NEO_GRB + NEO_KHZ800);

and later used this C1 and C2 with fill() of the neopixel header
but this only supports Arduino cape digital pins, but I have to connect it on pin33 PE2 of the mega32u4.

I don't think that that's not the bigger picture that @TomGeorge was referring to. Why did you use PE2 in the first place? You're using a Leonardo so why not use an existing board pin and the associated pin number?

I am going to use atmega32u4 itself in one of my projects.

I have one PCB, in which the strip data pin header is connected to PE2. Need to access via PE2, so that I don't have to revise the existing PCB connections

The question should be "How do you normally reference Port E, pin 2" ?
Mind you, looking at adafruit_neopixel.cpp i can not see the specific PORTE code, so you would have to make your own ASM routine for that.

If it is just the 1, i would modify it, taken that that would be the least amount of work.

yes @Deva_Rishi, neopixels does not provide this functionality.

your help will be appreciated.

thanks :slightly_smiling_face:

first of all you will have to set the pinmode in begin()

void Adafruit_NeoPixel::begin(void) {
  if(pin >= 0) {
    pinMode(pin, OUTPUT);
    digitalWrite(pin, LOW);
  }
  begun = true;
}

Hence my question.
Then in show() if it is a 16MHz AVR, it should not be such a big deal

// 16 MHz(ish) AVR --------------------------------------------------------
#elif (F_CPU >= 15400000UL) && (F_CPU <= 19000000L)

#ifdef NEO_KHZ400 // 800 KHz check needed only if 400 KHz support enabled
  if(is800KHz) {
#endif

    // WS2811 and WS2812 have different hi/lo duty cycles; this is
    // similar but NOT an exact copy of the prior 400-on-8 code.

    // 20 inst. clocks per bit: HHHHHxxxxxxxxLLLLLLL
    // ST instructions:         ^   ^        ^       (T=0,5,13)

    volatile uint8_t next, bit;

    hi   = *port |  pinMask;
    lo   = *port & ~pinMask;
    next = lo;
    bit  = 8;

because this is how it is defined. A lot less complex than writing a port specific ASM as for slower clock rates.

And i suppose you can just set those variables here

// Set the output pin number
void Adafruit_NeoPixel::setPin(uint8_t p) {
  if(begun && (pin >= 0)) pinMode(pin, INPUT);
    pin = p;
    if(begun) {
      pinMode(p, OUTPUT);
      digitalWrite(p, LOW);
    }
#ifdef __AVR__
    port    = portOutputRegister(digitalPinToPort(p));
    pinMask = digitalPinToBitMask(p);
#endif
}

All this code comes from a slightly old version of the library, but i guess this would not have changed much.

okay @Deva_Rishi

I updated code as below,

for now, I hardcoded this for pin33 only


void Adafruit_NeoPixel::begin(void) {

  if(pin == 33){
    DDRE |= (1 << PE2);
    PORTE |= (1 << PE2);
    begun = true;
    return;
  }
  
  if(pin >= 0) {
    pinMode(pin, OUTPUT);
    digitalWrite(pin, LOW);
  }
  begun = true;
}

and this is my setPin function

void Adafruit_NeoPixel::setPin(uint16_t p) {
  if(p == 33){

    if(begun && (pin >= 0)){
        DDRE |= ~(1 << PE2);
    }
    pin = p;

    if(begun){
      DDRE |= (1 << PE2);
      PORTE |= ~(1 << PE2);
    }
    return;

  }
  if(begun && (pin >= 0)) pinMode(pin, INPUT);
  pin = p;
  if(begun) {
    pinMode(p, OUTPUT);
    digitalWrite(p, LOW);
  }
#if defined(__AVR__)
  port    = portOutputRegister(digitalPinToPort(p));
  pinMask = digitalPinToBitMask(p);
#endif
#if defined(ARDUINO_ARCH_STM32) || defined(ARDUINO_ARCH_ARDUINO_CORE_STM32)
  gpioPort = digitalPinToPort(p);
  gpioPin = STM_LL_GPIO_PIN(digitalPinToPinName(p));
#endif
}

i think you still need to assign the proper values to these to variables

  port    = portOutputRegister(digitalPinToPort(p));
  pinMask = digitalPinToBitMask(p);

because they are being referenced in show()
would that be

    if(begun){
      DDRE |= (1 << PE2);
      PORTE |= ~(1 << PE2);
    }
    port    = PORTE;
    pinMask = (1 << PE2); // ?? this one in particular i am not sure.
    return;

okay updated this,

void Adafruit_NeoPixel::setPin(uint16_t p) {
  if(p == 33){

    if(begun && (pin >= 0)){
        DDRE |= ~(1 << PE2);
    }
    pin = p;

    if(begun){
      DDRE |= (1 << PE2);
      PORTE |= ~(1 << PE2);
    }
    port    = PORTE;
    pinMask = ~(1 << PE2);
    return;

  }
  if(begun && (pin >= 0)) pinMode(pin, INPUT);
  pin = p;
  if(begun) {
    pinMode(p, OUTPUT);
    digitalWrite(p, LOW);
  }
#if defined(__AVR__)
  port    = portOutputRegister(digitalPinToPort(p));
  pinMask = digitalPinToBitMask(p);
#endif
#if defined(ARDUINO_ARCH_STM32) || defined(ARDUINO_ARCH_ARDUINO_CORE_STM32)
  gpioPort = digitalPinToPort(p);
  gpioPin = STM_LL_GPIO_PIN(digitalPinToPinName(p));
#endif
}

please check this

pinMask = ~(1 << PE2);

is that correct ? i'm not quite sure how the pinMask works.
these 2 lines look obsolete

if(begun && (pin >= 0)){
        DDRE |= ~(1 << PE2);
    }

Oh hold on, you are supposed to set the pinmode to INPUT first, yes that is correct.
For the rest it looks OK, the question is as always 'Does it work ?'
I am a little confused with the purpose of the variables p & pin, pin is set as p once (begun && pin >= 0) , but you've copied it correctly i think.

yes, I looked into this...

pinMask works this way

but I am too confused with pin & p

because we set p to pin in setPin() that will set board pin 33 again not PE2 in rest of the execution

I tested this but it doesn't work

void Adafruit_NeoPixel::begin(void) {

  if(pin == 33){
    DDRE |= (1 << PE2);
    PORTE |= (1 << PE2);  // i think this sets the pin HIGH, it should be set LOW
    begun = true;
    return;
  }
  
  if(pin >= 0) {
    pinMode(pin, OUTPUT);
    digitalWrite(pin, LOW);
  }
  begun = true;
}

That is one thing. Then there is the issue of the pinMask. I would try it like this as well

pinMask = 1 << PE2;  // non-inverted.

OK it's trial and error, but i think it should work at some point. Are you using a 16MHz clock ?

The official way would be, to my knowledge, to add a new boards package with all the specifics of your board. I have no idea what is exactly involved.

My take on your problem is to hack pins_arduino.h; the only problem is that changes are lost when the boards package is updated.

You need to find pins_arduino.h for the Leonardo. On a Windows system with a standard install of the IDE, the file can be found in C:\Program Files (x86)\Arduino\hardware\arduino\avr\variants\leonardo\pins_arduino.h. If you ever updated the package, you can find it in a subdirectory of C:\Users\yourUserName\AppData\Local\Arduino15\packages\arduino; in my case C:\Users\yourUserName\AppData\Local\Arduino15\packages\arduino\hardware\avr\1.8.3
\variants\leonardo\pins_arduino.h.

This is the hacked version; changes marked with my user name

/*
  pins_arduino.h - Pin definition functions for Arduino
  Part of Arduino - http://www.arduino.cc/

  Copyright (c) 2007 David A. Mellis

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General
  Public License along with this library; if not, write to the
  Free Software Foundation, Inc., 59 Temple Place, Suite 330,
  Boston, MA  02111-1307  USA
*/

#ifndef Pins_Arduino_h
#define Pins_Arduino_h

#include <avr/pgmspace.h>

// Workaround for wrong definitions in "iom32u4.h".
// This should be fixed in the AVR toolchain.
#undef UHCON
#undef UHINT
#undef UHIEN
#undef UHADDR
#undef UHFNUM
#undef UHFNUML
#undef UHFNUMH
#undef UHFLEN
#undef UPINRQX
#undef UPINTX
#undef UPNUM
#undef UPRST
#undef UPCONX
#undef UPCFG0X
#undef UPCFG1X
#undef UPSTAX
#undef UPCFG2X
#undef UPIENX
#undef UPDATX
#undef TCCR2A
#undef WGM20
#undef WGM21
#undef COM2B0
#undef COM2B1
#undef COM2A0
#undef COM2A1
#undef TCCR2B
#undef CS20
#undef CS21
#undef CS22
#undef WGM22
#undef FOC2B
#undef FOC2A
#undef TCNT2
#undef TCNT2_0
#undef TCNT2_1
#undef TCNT2_2
#undef TCNT2_3
#undef TCNT2_4
#undef TCNT2_5
#undef TCNT2_6
#undef TCNT2_7
#undef OCR2A
#undef OCR2_0
#undef OCR2_1
#undef OCR2_2
#undef OCR2_3
#undef OCR2_4
#undef OCR2_5
#undef OCR2_6
#undef OCR2_7
#undef OCR2B
#undef OCR2_0
#undef OCR2_1
#undef OCR2_2
#undef OCR2_3
#undef OCR2_4
#undef OCR2_5
#undef OCR2_6
#undef OCR2_7

// sterretje
#define EXTRA_PINS
#ifdef EXTRA_PINS
#define NUM_DIGITAL_PINS  32
#else
#define NUM_DIGITAL_PINS  31
#endif
#define NUM_ANALOG_INPUTS 12

#define TX_RX_LED_INIT	DDRD |= (1<<5), DDRB |= (1<<0)
#define TXLED0			PORTD |= (1<<5)
#define TXLED1			PORTD &= ~(1<<5)
#define RXLED0			PORTB |= (1<<0)
#define RXLED1			PORTB &= ~(1<<0)

#define PIN_WIRE_SDA         (2)
#define PIN_WIRE_SCL         (3)

static const uint8_t SDA = PIN_WIRE_SDA;
static const uint8_t SCL = PIN_WIRE_SCL;

#define LED_BUILTIN 13
#define LED_BUILTIN_RX 17
#define LED_BUILTIN_TX 30

// Map SPI port to 'new' pins D14..D17
#define PIN_SPI_SS    (17)
#define PIN_SPI_MOSI  (16)
#define PIN_SPI_MISO  (14)
#define PIN_SPI_SCK   (15)

static const uint8_t SS   = PIN_SPI_SS;
static const uint8_t MOSI = PIN_SPI_MOSI;
static const uint8_t MISO = PIN_SPI_MISO;
static const uint8_t SCK  = PIN_SPI_SCK;

// Mapping of analog pins as digital I/O
// A6-A11 share with digital pins
#define PIN_A0   (18)
#define PIN_A1   (19)
#define PIN_A2   (20)
#define PIN_A3   (21)
#define PIN_A4   (22)
#define PIN_A5   (23)
#define PIN_A6   (24)
#define PIN_A7   (25)
#define PIN_A8   (26)
#define PIN_A9   (27)
#define PIN_A10  (28)
#define PIN_A11  (29)

static const uint8_t A0 = PIN_A0;
static const uint8_t A1 = PIN_A1;
static const uint8_t A2 = PIN_A2;
static const uint8_t A3 = PIN_A3;
static const uint8_t A4 = PIN_A4;
static const uint8_t A5 = PIN_A5;
static const uint8_t A6 = PIN_A6;	// D4
static const uint8_t A7 = PIN_A7;	// D6
static const uint8_t A8 = PIN_A8;	// D8
static const uint8_t A9 = PIN_A9;	// D9
static const uint8_t A10 = PIN_A10;	// D10
static const uint8_t A11 = PIN_A11;	// D12

#define digitalPinToPCICR(p)    ((((p) >= 8 && (p) <= 11) || ((p) >= 14 && (p) <= 17) || ((p) >= A8 && (p) <= A10)) ? (&PCICR) : ((uint8_t *)0))
#define digitalPinToPCICRbit(p) 0
#define digitalPinToPCMSK(p)    ((((p) >= 8 && (p) <= 11) || ((p) >= 14 && (p) <= 17) || ((p) >= A8 && (p) <= A10)) ? (&PCMSK0) : ((uint8_t *)0))
#define digitalPinToPCMSKbit(p) ( ((p) >= 8 && (p) <= 11) ? (p) - 4 : ((p) == 14 ? 3 : ((p) == 15 ? 1 : ((p) == 16 ? 2 : ((p) == 17 ? 0 : (p - A8 + 4))))))

//	__AVR_ATmega32U4__ has an unusual mapping of pins to channels
extern const uint8_t PROGMEM analog_pin_to_channel_PGM[];
#define analogPinToChannel(P)  ( pgm_read_byte( analog_pin_to_channel_PGM + (P) ) )

#define digitalPinHasPWM(p)         ((p) == 3 || (p) == 5 || (p) == 6 || (p) == 9 || (p) == 10 || (p) == 11 || (p) == 13)

#define digitalPinToInterrupt(p) ((p) == 0 ? 2 : ((p) == 1 ? 3 : ((p) == 2 ? 1 : ((p) == 3 ? 0 : ((p) == 7 ? 4 : NOT_AN_INTERRUPT)))))

#ifdef ARDUINO_MAIN

// On the Arduino board, digital pins are also used
// for the analog output (software PWM).  Analog input
// pins are a separate set.

// ATMEL ATMEGA32U4 / ARDUINO LEONARDO
//
// D0				PD2					RXD1/INT2
// D1				PD3					TXD1/INT3
// D2				PD1		SDA			SDA/INT1
// D3#				PD0		PWM8/SCL	OC0B/SCL/INT0
// D4		A6		PD4					ADC8
// D5#				PC6		???			OC3A/#OC4A
// D6#		A7		PD7		FastPWM		#OC4D/ADC10
// D7				PE6					INT6/AIN0
//
// D8		A8		PB4					ADC11/PCINT4
// D9#		A9		PB5		PWM16		OC1A/#OC4B/ADC12/PCINT5
// D10#		A10		PB6		PWM16		OC1B/0c4B/ADC13/PCINT6
// D11#				PB7		PWM8/16		0C0A/OC1C/#RTS/PCINT7
// D12		A11		PD6					T1/#OC4D/ADC9
// D13#				PC7		PWM10		CLK0/OC4A
//
// A0		D18		PF7					ADC7
// A1		D19		PF6					ADC6
// A2		D20 	PF5					ADC5
// A3		D21 	PF4					ADC4
// A4		D22		PF1					ADC1
// A5		D23 	PF0					ADC0
//
// New pins D14..D17 to map SPI port to digital pins
//
// MISO		D14		PB3					MISO,PCINT3
// SCK		D15		PB1					SCK,PCINT1
// MOSI		D16		PB2					MOSI,PCINT2
// SS		D17		PB0					RXLED,SS/PCINT0
//
// TXLED	D30		PD5					XCK1
// RXLED	D17	    PB0
// HWB				PE2					HWB
// sterretje
// HWB		D31		PE2					HWB

// these arrays map port names (e.g. port B) to the
// appropriate addresses for various functions (e.g. reading
// and writing)
const uint16_t PROGMEM port_to_mode_PGM[] = {
	NOT_A_PORT,
	NOT_A_PORT,
	(uint16_t) &DDRB,
	(uint16_t) &DDRC,
	(uint16_t) &DDRD,
	(uint16_t) &DDRE,
	(uint16_t) &DDRF,
};

const uint16_t PROGMEM port_to_output_PGM[] = {
	NOT_A_PORT,
	NOT_A_PORT,
	(uint16_t) &PORTB,
	(uint16_t) &PORTC,
	(uint16_t) &PORTD,
	(uint16_t) &PORTE,
	(uint16_t) &PORTF,
};

const uint16_t PROGMEM port_to_input_PGM[] = {
	NOT_A_PORT,
	NOT_A_PORT,
	(uint16_t) &PINB,
	(uint16_t) &PINC,
	(uint16_t) &PIND,
	(uint16_t) &PINE,
	(uint16_t) &PINF,
};

const uint8_t PROGMEM digital_pin_to_port_PGM[] = {
	PD, // D0 - PD2
	PD,	// D1 - PD3
	PD, // D2 - PD1
	PD,	// D3 - PD0
	PD,	// D4 - PD4
	PC, // D5 - PC6
	PD, // D6 - PD7
	PE, // D7 - PE6
	
	PB, // D8 - PB4
	PB,	// D9 - PB5
	PB, // D10 - PB6
	PB,	// D11 - PB7
	PD, // D12 - PD6
	PC, // D13 - PC7
	
	PB,	// D14 - MISO - PB3
	PB,	// D15 - SCK - PB1
	PB,	// D16 - MOSI - PB2
	PB,	// D17 - SS - PB0
	
	PF,	// D18 - A0 - PF7
	PF, // D19 - A1 - PF6
	PF, // D20 - A2 - PF5
	PF, // D21 - A3 - PF4
	PF, // D22 - A4 - PF1
	PF, // D23 - A5 - PF0
	
	PD, // D24 / D4 - A6 - PD4
	PD, // D25 / D6 - A7 - PD7
	PB, // D26 / D8 - A8 - PB4
	PB, // D27 / D9 - A9 - PB5
	PB, // D28 / D10 - A10 - PB6
	PD, // D29 / D12 - A11 - PD6
	PD, // D30 / TX Led - PD5
    
    // sterretje, D31
	PE, // D31 / HWB
};

const uint8_t PROGMEM digital_pin_to_bit_mask_PGM[] = {
	_BV(2), // D0 - PD2
	_BV(3),	// D1 - PD3
	_BV(1), // D2 - PD1
	_BV(0),	// D3 - PD0
	_BV(4),	// D4 - PD4
	_BV(6), // D5 - PC6
	_BV(7), // D6 - PD7
	_BV(6), // D7 - PE6
	
	_BV(4), // D8 - PB4
	_BV(5),	// D9 - PB5
	_BV(6), // D10 - PB6
	_BV(7),	// D11 - PB7
	_BV(6), // D12 - PD6
	_BV(7), // D13 - PC7
	
	_BV(3),	// D14 - MISO - PB3
	_BV(1),	// D15 - SCK - PB1
	_BV(2),	// D16 - MOSI - PB2
	_BV(0),	// D17 - SS - PB0
	
	_BV(7),	// D18 - A0 - PF7
	_BV(6), // D19 - A1 - PF6
	_BV(5), // D20 - A2 - PF5
	_BV(4), // D21 - A3 - PF4
	_BV(1), // D22 - A4 - PF1
	_BV(0), // D23 - A5 - PF0
	
	_BV(4), // D24 / D4 - A6 - PD4
	_BV(7), // D25 / D6 - A7 - PD7
	_BV(4), // D26 / D8 - A8 - PB4
	_BV(5), // D27 / D9 - A9 - PB5
	_BV(6), // D28 / D10 - A10 - PB6
	_BV(6), // D29 / D12 - A11 - PD6
	_BV(5), // D30 / TX Led - PD5

    // sterretje, D31
	_BV(2), // D31 / HWB

};

const uint8_t PROGMEM digital_pin_to_timer_PGM[] = {
	NOT_ON_TIMER,	
	NOT_ON_TIMER,
	NOT_ON_TIMER,
	TIMER0B,		/* 3 */
	NOT_ON_TIMER,
	TIMER3A,		/* 5 */
	TIMER4D,		/* 6 */
	NOT_ON_TIMER,	
	
	NOT_ON_TIMER,	
	TIMER1A,		/* 9 */
	TIMER1B,		/* 10 */
	TIMER0A,		/* 11 */
	
	NOT_ON_TIMER,	
	TIMER4A,		/* 13 */
	
	NOT_ON_TIMER,	
	NOT_ON_TIMER,
	NOT_ON_TIMER,
	NOT_ON_TIMER,
	NOT_ON_TIMER,
	NOT_ON_TIMER,

	NOT_ON_TIMER,
	NOT_ON_TIMER,
	NOT_ON_TIMER,
	NOT_ON_TIMER,
	NOT_ON_TIMER,
	NOT_ON_TIMER,
	NOT_ON_TIMER,
	NOT_ON_TIMER,
	NOT_ON_TIMER,
	NOT_ON_TIMER,
	NOT_ON_TIMER,

    // sterretje, D31
	NOT_ON_TIMER,
};

const uint8_t PROGMEM analog_pin_to_channel_PGM[] = {
	7,	// A0				PF7					ADC7
	6,	// A1				PF6					ADC6	
	5,	// A2				PF5					ADC5	
	4,	// A3				PF4					ADC4
	1,	// A4				PF1					ADC1	
	0,	// A5				PF0					ADC0	
	8,	// A6		D4		PD4					ADC8
	10,	// A7		D6		PD7					ADC10
	11,	// A8		D8		PB4					ADC11
	12,	// A9		D9		PB5					ADC12
	13,	// A10		D10		PB6					ADC13
	9	// A11		D12		PD6					ADC9
};

#endif /* ARDUINO_MAIN */

// These serial port names are intended to allow libraries and architecture-neutral
// sketches to automatically default to the correct port name for a particular type
// of use.  For example, a GPS module would normally connect to SERIAL_PORT_HARDWARE_OPEN,
// the first hardware serial port whose RX/TX pins are not dedicated to another use.
//
// SERIAL_PORT_MONITOR        Port which normally prints to the Arduino Serial Monitor
//
// SERIAL_PORT_USBVIRTUAL     Port which is USB virtual serial
//
// SERIAL_PORT_LINUXBRIDGE    Port which connects to a Linux system via Bridge library
//
// SERIAL_PORT_HARDWARE       Hardware serial port, physical RX & TX pins.
//
// SERIAL_PORT_HARDWARE_OPEN  Hardware serial ports which are open for use.  Their RX & TX
//                            pins are NOT connected to anything by default.
#define SERIAL_PORT_MONITOR        Serial
#define SERIAL_PORT_USBVIRTUAL     Serial
#define SERIAL_PORT_HARDWARE       Serial1
#define SERIAL_PORT_HARDWARE_OPEN  Serial1

// Alias SerialUSB to Serial
#define SerialUSB SERIAL_PORT_USBVIRTUAL

#endif /* Pins_Arduino_h */

This will / should give you a pin that you can access by pin number 31; it's mapped to PE2.

Advantage of the approach is that it also will / should work with FastLed or even as a normal GPIO pin.

Added disavantage: you have to use the Leonardo as the selected board, it will not work with other boards; you will e.g. have to hack the pins_arduino.h for the Micro if you wanted to use that board.

Love the approach ! as it is, it wouldn't work for adafruit_neopixel.h because PORTE just isn't supported, don't know if that is the same for FasLED, but they have been accused of plagiarism before, so i would not be surprised if it isn't supported in FasLED either.

I had a quick look at FastLed and I found one place where it uses the standard functions like digitalWrite() does; I did not look further :wink: From FastPin.h

	void _init() {
		mPinMask = digitalPinToBitMask(mPin);
		mPort = (volatile RwReg*)portOutputRegister(digitalPinToPort(mPin));
		mInPort = (volatile RoReg*)portInputRegister(digitalPinToPort(mPin));
	}

@ruchiatavr can always give it a shot :wink: