Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add N3DS EXTHID driver #3

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open

Conversation

EvansJahja
Copy link

@EvansJahja EvansJahja commented Apr 28, 2024

Add N3DS EXTHID (device 17h) driver for getting ZL, ZR, and C-Stick for supported devices

Needed for profi200/open_agb_firm#173

Notes:

A lot of the device information is still unknown. Here's what we know so far for a fact:

  • It's a 44-pin device, probably QFN or QFP package with the marking HF374 7NU9
  • It's responsible for ZL, ZR, and C-stick on New 3DS (and other New variants).
  • It is connected as Device 17 (0x11), having i2c address of 0x2a
  • Communication uses i2c protocol without registers. On ir service (ir:rst), the communication is done by WriteCommand8 and ReadDeviceRaw wiki
  • ir service only reads 5 bytes. Reading past this may reveal additional information but seems like this is not the intended use
  • the device can work without writing anything, but ir service writes a few things before reading from it.
  • it supports automatic recalibration.

Assumptions:

  • It's likely an ADC chip, likely a sigma delta ADC with some GPIO pins for reading buttons.
  • C stick is most likely a form of strain gauge, which is why it needs such complicated IC
  • writing to the chip likely changes the sample rate, division, and other parameters
  • the first byte we get from reading the chip gives 0x80. ir:rst however, expects that the first byte is 0x82. One possibility is because of the writecommand issued by ir:rst prior to reading: 0xc1 0x52 0xf7 0xf5 (followed by read). Writing 0xf5 seems to cause the first byte to be 0x82 according to nocash, but the other bytes usage is still unknown.

Images:

https://gbatemp.net/attachments/rbvaefc22h6aixqgaas6sohprly388_edit-jpg.198252/
https://gbatemp.net/attachments/4_power_ic_id_edit-jpg.198242/

@profi200
Copy link
Owner

Device 17 is a bit of a cryptic name. I remember that nocash called it IO expander somewhere which is a bit better but still not sure.

https://problemkaputt.de/gbatek-3ds-i2c-new3ds-c-stick-and-zl-zr-buttons.htm

@EvansJahja
Copy link
Author

Device 17 is a bit of a cryptic name. I remember that nocash called it IO expander somewhere which is a bit better but still not sure.

https://problemkaputt.de/gbatek-3ds-i2c-new3ds-c-stick-and-zl-zr-buttons.htm

100% agree that it's a cryptic name but I have no idea what to call it. Any suggestion?

nocash mentioned IO expander but not for this one, it was for another device , I think it was the QTM

@profi200
Copy link
Owner

profi200 commented Apr 28, 2024

Hmm, it seems to be this chip on the A/B/X/Y board i guess. Literally no datasheet.

https://de.ifixit.com/Teardown/Nintendo+3DS+XL+2015+Teardown/36346?utm_term=Teardown#s81199

As for ideas got a few from the GodMode9 Discord/IRC. N3DS_EXTHID for example.

@EvansJahja
Copy link
Author

EvansJahja commented Apr 28, 2024

Hmm, it seems to be this chip on the A/B/X/Y board i guess. Literally no datasheet.

https://de.ifixit.com/Teardown/Nintendo+3DS+XL+2015+Teardown/36346?utm_term=Teardown#s81199

As for ideas got a few from the GodMode9 Discord/IRC. N3DS_EXTHID for example.

I got some clues that it might be this chip

https://gbatemp.net/attachments/rbvaefc22h6aixqgaas6sohprly388_edit-jpg.198252/
https://gbatemp.net/attachments/4_power_ic_id_edit-jpg.198242/

I think nocash also mentioned this. HF374 7NU9

But your link mentioning it's a strain gauge sensor is something I totally miss! Let me try some more digging. In the mean time I'll call it N3DS_EXTHID. I think that's what was used on i2c.c but it got me curious.

Also, the real ir:rst service actually has some init code that includes sending some i2c command. Probably some setting for sensitivity / resolution. I'll try to dig more for the datasheet.

@EvansJahja EvansJahja changed the title add device 17h driver add N3DS EXTHID driver Apr 29, 2024
@EvansJahja
Copy link
Author

updated name to N3DS_EXTHID in e80c04b

@profi200
Copy link
Owner

I will do some experiments before i merge this or add changes. Something i definitely want to change is making this IRQ driven because bigger I2C reads every frame are not good. They are simply too slow. That chip is apparently connected to a GPIO and sends signals every time data is ready for read. Also i will merge the I2C start functions to de-duplicate a bit of code.

@EvansJahja
Copy link
Author

I was able to get GPIO interrupt working, but I'm hesitant to put it here, seems there's conflict with codec.c that changes GPIO_3_0 mode and that overwrites the interrupt.

I was quite confused when at first I tried to register the ISR and just expects it to work, but it never fired. Turns out I was missing call to GPIO_config(GPIO_3_0, GPIO_IRQ_FALLING); 😅

On the subject of i2c, could you also add this?

bool I2C_writeCommand(const I2cDevice devId, const u8 command)
{
	const u8 devAddr = g_i2cDevTable[devId].devAddr;
	const I2cState *const state = &g_i2cState[g_i2cDevTable[devId].busId];
	I2cBus *const i2cBus = state->i2cBus;
	const KHandle event = state->event;
	const KHandle mutex = state->mutex;
	lockMutex(mutex);

	if(i2cBus->cnt & I2C_EN) waitForEvent(event);
		clearEvent(event);

	// Select device and start.
	sendByte(i2cBus, devAddr, I2C_START, event);
	if(!checkAck(i2cBus))
	{
		unlockMutex(mutex);
		return false;
	}

	sendByte(i2cBus, command, I2C_STOP, event);
	if(!checkAck(i2cBus))
	{
		unlockMutex(mutex);
		return false;
	}
	unlockMutex(mutex);
	return true;
}

It's not used at the moment, and ZL & ZR works fine without it. I think the C-stick is not too sensitive without some more configuration, even though it works without configuration. This should correspond to WriteCommand8

@profi200
Copy link
Owner

profi200 commented May 2, 2024

This is what i came up with in the meantime after looking how ir module interfaces with this chip. Some code isn't ever taken with the hardcoded values. It was a test anyway. And it needs some I2C and GPIO driver changes i did.
This produces C-Stick values from 0-255 instead of what gbatek says. And it seems the X/Y axis is rotated 45° or so (at least that is what ir module code for converting the raw data suggests).

As for the GPIO conflict we can easily solve this because the depop GPIO is actually only used on old 3DS in codec module.

static u8 g_extHidRev = 0;



static bool EXTHID_command(const u8 cmd)
{
	return I2C_writeReg(I2C_DEV_N3DS_HID, 0xFFFFFFFF, cmd);
}

static bool EXTHID_read5(void *buf)
{
	return I2C_readRegArray(I2C_DEV_N3DS_HID, 0xFFFFFFFF, buf, 5);
}

static bool exthidSleep(void)
{
	bool res;
	if(g_extHidRev == 2)
	{
		// Some kind of bug fix for revision 2?
		res = EXTHID_command(0x30u | 7);
		if(!res) return res;
	}

	// Disable output.
	res = EXTHID_command(0xF7);
	if(!res) return res;

	TIMER_sleepMs(6);           // \ Is this yet another bug fix?
	u8 unused[5];               // |
	res = EXTHID_read5(unused); // /
	if(!res) return res;

	// Changes Which GPIO bits are copied to the regs? Prevents the chip from waking up with ZL/ZR?
	res = EXTHID_command(0x7C);
	if(!res) return res;

	// Sleep command with auto calibration?
	return EXTHID_command(0xD0);
}

static void exthidWakeChip(void)
{
	// Triggers an IRQ to wake the chip up?
	GPIO_write(GPIO_EXTHID_WAKE, 0);
	TIMER_sleepUs(100);
	GPIO_write(GPIO_EXTHID_WAKE, 1);
	TIMER_sleepMs(10);
}

static bool exthidWakeup(void)
{
	exthidWakeChip();

	// Changes which GPIO bits are copied to the regs again?
	bool res = EXTHID_command(0x7F);
	if(!res) return res;

	// Calibration really takes that long?
	TIMER_sleepMs(136);

	// Enable output.
	res = EXTHID_command(0xF9);
	if(!res) return res;

	if(g_extHidRev == 2)
	{
		// Revert bug fix on wakeup?
		res = EXTHID_command(0x30u | 3);
	}

	return res;
}

static bool EXTHID_init(void)
{
	// External EXTHID IRQ to wake the chip up?
	GPIO_config(GPIO_EXTHID_WAKE, GPIO_OUTPUT);
	GPIO_write(GPIO_EXTHID_WAKE, 1);
	// -----------------------------

	// Set device mode.
	bool res = EXTHID_command(0xC1); // Selects an ADC curve or something?
	if(!res) return res;
	res = EXTHID_command(0x52);      // Changes conversion mode?
	if(!res) return res;

	// Disable output.
	res = EXTHID_command(0xF7); // Disables output GPIOs or something?
	if(!res) return res;

	// Get device ID.
	struct
	{
		u8 status;
		u8 vendor;
		u8 rev;
		u8 reserved;
		u8 stopByte;
	} devId;
	res = EXTHID_command(0xF5);
	if(!res) return res;
	TIMER_sleepMs(2); // wtf, why is this needed?
	res = EXTHID_read5(&devId);
	if(!res) return res;
	g_extHidRev = devId.rev;
	ee_printf("Dev ID: %" PRIX8 ", %" PRIX8 ", %" PRIX8 ", %" PRIX8 ", %" PRIX8 "\n",
	          devId.status, devId.vendor, devId.rev, devId.reserved, devId.stopByte);

	if(devId.status != 0x82 || devId.stopByte != 0xFF)
		return false;

	// TODO: ir module does skip all of the following code except sleep() if vendor is not 1.
	if(devId.rev >= 1)
	{
		// Set sensitivity to 1.0. Only works with the mode set above.
		res = EXTHID_command(0x08); // Command 0x08-0x0F. 1.0-1.875. In 0.125 steps.
		if(!res) return res;

		// Disable idle state.
		res = EXTHID_command(0xF3);
		if(!res) return res;

		// -----------------------------
		const u8 calThreshold = 30;
		if(true)
		{
			if(calThreshold == 0)
			{
				res = EXTHID_command(0xF1); // Automatic calibration?
			}
			else
			{
				res = EXTHID_command(0xF2); // Automatic calibration again but with thresholds?
			}
		}
		else
		{
			res = EXTHID_command(0xF0); // Automatic calibration disable?
		}
		if(!res) return res;

		if(calThreshold > 0)
		{
			// Set auto calibration threshold. TODO: Can be merged with above code.
			// Auto calibration threshold? 0-0xF. Note: There is a lookup table.
			res = EXTHID_command(0x20u | 0x9);
			if(!res) return res;
		}

		const u8 opThreshold = 5; // Valid values: 1-16.
		res = EXTHID_command(0x80u | ((opThreshold - 1) & 0xFu));
		if(!res) return res;

		const u8 outThreshold = 7; // Valid values: 2-16.
		res = EXTHID_command(0x90u | ((outThreshold - 1) & 0xFu));
		if(!res) return res;

		// TODO: Lookup table for op timer values.
		//const u8 opTimer = 21;
		res = EXTHID_command(0xB0u | 0x4);
		if(!res) return res;

		// TODO: Lookup table for out timer values.
		//const u8 outTimer = 9;
		res = EXTHID_command(0x60u | 0x3);
		if(!res) return res;
		// -----------------------------
	}

	if(devId.rev >= 2)
	{
		res = EXTHID_command(0x30u | 0x3); // Command 0x30-0x37.
		if(!res) return res;
	}

	return exthidSleep();
}

static bool exthidSetSamplePeriod(const u32 period)
{
	if(period < 10 || period > 21) return false;

	if(!EXTHID_command(0xA0u | ((period - 10) & 0xFu))) return false;
	TIMER_sleepMs(56); // bruh

	return true;
}

static KHandle g_extHidEvent = 0;
static void exthidIsr(UNUSED u32 intSource)
{
	signalEvent(g_extHidEvent, false);
}

// Note: libctru uses a default period of 10.
static bool EXTHID_startSampling(const u32 period)
{
	GPIO_config(GPIO_EXTHID_IRQ, GPIO_IRQ_FALLING | GPIO_INPUT);
	IRQ_registerIsr(IRQ_GPIO_3_0, 13, 0, exthidIsr);
	g_extHidEvent = createEvent(true);

	if(!exthidWakeup()) return false;
	return exthidSetSamplePeriod(period);
}

static bool EXTHID_stopSampling(void)
{
	if(!exthidSleep()) return false;

	// Note: Nintendo does a weird sequence of GPIO changes here.
	// In order:
	//   Disable GPIO IRQ (not ARM11 side IRQ).
	//   Change to falling edge trigger (keep IRQ disabled). This is a no-op since it's already falling edge.
	//   Set GPIO to output (wtf).
	//   Set GPIO high (wtf).
	//   Set GPIO to back to input.
	//   Then finally the event is unbound from the IRQ (disables ARM11 side IRQ).
	GPIO_config(GPIO_EXTHID_IRQ, GPIO_INPUT); // Input but no IRQ.

	IRQ_unregisterIsr(IRQ_GPIO_3_0);
	deleteEvent(g_extHidEvent);
	g_extHidEvent = 0;

	return true;
}

static bool EXTHID_getRawData(void *data)
{
	// Note: In polling mode we need to request data and then wait 2 ms.
	return EXTHID_read5(data);
}

int main(void)
{
	GFX_init(GFX_BGR8, GFX_R5G6B5, GFX_TOP_2D);
	GFX_setLcdLuminance(20);
	consoleInit(GFX_LCD_BOT, NULL);
	//CODEC_init();

	ee_printf("init res: %u\n", EXTHID_init());
	ee_printf("start sampling: %u\n", EXTHID_startSampling(16));

	while(1)
	{
		waitForEvent(g_extHidEvent);
		struct
		{
			u8 status;
			u8 gpio;
			u8 x;
			u8 y;
			u8 stopByte;
		} raw;
		const bool res = EXTHID_getRawData(&raw);
		ee_printf("\rres %u %" PRIX8 ", %" PRIX8 ", %" PRIu8 ", %" PRIu8 ", %" PRIX8 " ",
		          res, raw.status, raw.gpio, raw.x, raw.y, raw.stopByte);


		//GFX_waitForVBlank0();
		hidScanInput();
		if(hidGetExtraKeys(0) & (KEY_POWER_HELD | KEY_POWER)) break;
	}

	EXTHID_stopSampling();

	fUnmount(FS_DRIVE_SDMC);
	CODEC_deinit();
	GFX_deinit();

	power_off();

	return 0;
}

@EvansJahja
Copy link
Author

EvansJahja commented May 2, 2024

Wow that's miles ahead from what I found! Could you tell me how you figure it out? I was stuck even figuring out what the commands meant. I couldn't make sense how you'd know which parts are for the devid vendor, for example and I'd love to learn more.

@profi200
Copy link
Owner

profi200 commented May 3, 2024

Join the GodMode9 Discord or IRC (they are bridged). Preferably the offtopic channel. I can explain further there.
https://github.com/d0k3/GodMode9?tab=readme-ov-file#contact-info

In the meanwhile looks like i got X/Y data conversion working.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants