Running Code in the Pre-Boot Environment

Before your operating system loads, the UEFI (Unified Extensible Firmware Interface) runs. The code in the UEFI is responsible for getting the initial bits of your operating system loaded and then passes control to it for it to load the rest of its components. For the sake of having a better understanding of how things work, this past weekend I decided to write code that would run from the UEFI. Unless you are making system components, this is something that would be impractical to do in the general case. But curious exploration isn’t constrained by practicality. There do exist SDKs for making UEFI programs. Visual Studio also supports the UEFI as a binary target. I’m not using any of the SDKs though. I won’t need them for what I’m doing. To get as far as a “Hello World” I’ll only use Intel Assembler. I wouldn’t suggest that someone that doesn’t already know x86 assembler try this.

Configuring a Visual Studio Project for Assembler

Yes, I’ve not forgotten that I’ve published a video saying that to generally avoid programming in assembler. This isn’t going in any serious code, I don’t have that concern here. From that video, though, I did have instructions on how to create the appropriate type of project in Visual Studio and modify it so that it can compile assembler code.

To use Visual Studio for Assembler development, you will want to create a C++ project. C++ projects support mixing C++ and assembly. We will make a C++ project where 0% of the code is in C++ and 100% is in assembly. In Visual Studio you can create an empty C++ project. Add a new file to the project named main.asm. Right now, VS will not do anything useful with that file. There are two changes that must be made. One is that MASM (Microsoft Assembler) targets must be enabled for the project. The other, MASM must be set as the target for your ASM files. To enable MASM as a target first click on your project in the Solution three, then navigate in Visual Studio to the menu selection “Projects” and then “Build Customizations.” Check the box next to “masm(.targets .props)” and click on “OK.”

To set the target type for main.asm you will need to manually update the project file. right-click on your project in the solution tree and select “Unload project.” Search for “main.asm” in the file. You will find a line that looks like this.

<None Include="main.asm" />

Change the word “None” to “MASM” so that it looks like this.

<MASM Include="main.asm" />

Right-click on the project file and select “Reload project.” Visual Studio able to compile now. But it will try to make a Windows application. That’s not what we want. We want a UEFI application. Visual C++ must be set to target UEFI as the subsystem type. Right-click on the project file and select “Properties.” At the top, change the Configuration setting to “All Configurations.” In the settings tree on the left, navigate to Configuration -> Linker -> System. Change the SubSystem setting to “EFI Application.” Now, select the configuration path Configuration -> Linker -> Advanced. Here, set the “Entry Point” setting to “main”, set “Randomize Base Address” to “No”, and set “Data Execution Prevention” to “No”.

Visual Studio will produce an executable that is the same name as the project but with the EXE extension. That’s not what we want. We want the file name to be BOOTX64.efi. To set that, navigate to the settings path Configuration->Linker->General. Change the Output File setting from $(OutDir)$(TargetName)$(TargetExt) to $(OutDir)BOOTX64.efi. With that, all the configuration steps are complete. Now, we need to write code.

Creating our First Program

This first program will be incredibly simple. It does nothing but loop for some number of cycles and then it terminates. That’s it. Why make a program so simple? Debugging is a little more complex for EFI programs and won’t be covered here. In the absence of a debugger, we will make a program that is as simple as possible while also having effect so that it is known that the program ran. Without relying on any external functionality, the most observable thing that the program can do is take up execution cycles. I do this with the assembler equivalent of

for(auto i=0;i<0x1000000;++i)
{
}

While the program is running, the screen is black. When it finishes running, the UEFI will take over. I can run the program with larger or smaller values in the loop to observe longer or shorter periods of time with a black screen, letting me know that it ran. Here is the source code.

.DATA
; no data
.CODE
main PROC
     MOV RCX, 01000000000H
delay_loop:
	DEC RCX
	JNZ delay_loop
	XOR RAX, RAX
	RET
main ENDP

This code loads a register with a large number. It then loops, decrementing the register value and continuing to do so until that register value has reached zero. When it has, the program sets the RAX register to zero and returns control to the UEFI. The UEFI might check the value of RAX since it is set to a non-zero value if a problem occurred. Compile this and copy the output to a USB key in the folder /EFI/BOOT. It is ready to run!

Running the Program

Usually in Visual Studio, you just press [F5] and your program will compile and run. That’s not an option here. The program must be in a pre-boot environment to run. The easiest way to run the code is either from another computer or from a virtual machine. Attempting to run it on your development machine would mean that you would need to reboot. An emulator or a second machine lets you avoid that. I’m using VM Ware Workstation, which is now available for free from VMWare’s site. ( https://www.vmware.com/products/desktop-hypervisor/workstation-and-fusion ). In any case, you’ll want to ensure that “Secure Boot” is turned off. If Secure Boot is turned on, the computer will refuse to run your code because it isn’t signed with keys that the computer trusts. In VMWare Workstation, right-click on the VM that you plan to use for testing and select “Settings.” In the two tabs at the top of the window that appears, select “Options” and then select “Advanced.” Ensure the firmware type is set to UEFI and that “Secure Boot” is not checked. Click on Okay.

Power-On the Virtual Machine. Once it is powered on, In the “VM” menu select “Removable Devices.” You should see your USB drive. Select it and choose the “Connect Option.” The drive will appear to be unplugged from your computer and connected to the host machine.

Now select the option to reboot the VM.

When the machine reboots, you should see the screen remain black for a bit before the UEFI menu shows. During this period when it is black, the program is looping. If you shut down the VM then the USB drive will become visible to your computer again.

Writing Text

The UEFI has APIs available for reading and writing text, graphics, and more. Let’s try to write text to the screen. We will write “Hello World”, delay, and then return control to the system. This first time that we do that we will be applying knowledge that is known about the system that cannot be inferred from looking at the code alone. When our program starts, two registers already have pointers to a couple of items of information that we need. The RCX register (64-bit general purpose register) has a handle to some Image information. The RDX register has a pointer to the System Table. The system table contains pointers to objects and functions that we will want to use and provides other information about the system. At an offset 0x40 (64) bytes into the system table is a pointer to an object known as ConOut that is used for writing text to the console. At an offset 0x8 bytes from that object’s address is a pointer to the function entry point for the function known as OutputString. We want to call that to display text on the screen. When we call this function, we need to set the RDX register to point to the address of the string that we want to print. After we print, we run our delay and then return control to the UEFI.

.DATA
     szHelloWorld DW 'H','e','l','l','o',' ','W','o','r','l','d','\r','\n', 0
.CODE
main PROC
	SUB RSP, 10H*8	
	MOV RCX, [RDX + 40H] ; Get ConOut function address
	LEA RDX, [szHelloWorld]
	CALL QWORD PTR [RCX + 08H] ;Output String	
	MOV RCX, 01000000000H
delay_loop:
	DEC RCX
	JNZ delay_loop
	ADD RSP, 10H*8	
	XOR RAX, RAX
	RET
main ENDP

END

If we run the program now, it shows text

Adding Structures for Reading Objects

Reading data from arbitrary offsets both work and results in horrible readability. The code will be a lot more readable if we read it using structs instead of arbitrary memory offset. There are three structs that we need: EFI_TABLE_HEADER, SYSTEM_TABLE, and TEXT_OUTPUT_INTERFACE.The EFI_TABLE_HEADER here is being used within the SYSTEM_TABLE struct. I could have defined it in line with SYSTEM_TABLE, but it is used by some other UEFI structures. I decided against it. Most of the other entries in the SYSTEM_TABLE are 64-bit pointers (DQ – or double quads, meaning 8 bytes). Though a few members are doubles (DD – or 32-bit numbers).

EFI_TABLE_HEADER STRUCT
  Signature		DQ ?
  Revision		DD ?
  HeaderSize	DD ?
  CRC			DD ?
  Reserved		DD ?
EFI_TABLE_HEADER ENDS

SYSTEM_TABLE STRUCT
	HDR						EFI_TABLE_HEADER <?> ; 00H
	FIRMWARE_VENDOR_PTR		DQ ? ; 18H
	FIRMWARE_REVISION_PTR	DQ ? ; 20H
	CONSOLE_INPUT_HANDLE	DQ ? ; 28H
	ConIn					DQ ? ; 30H
	CONSOLE_OUTPUT_HANDLE	DQ ? ; 28
	ConOut					DQ ? ; 30
	StandardErrorHandle		DQ ? ; 38
	STD_ERR					DQ ? ; 40
	RuntimeServices			DQ ? ; 48
	BootServices			DQ ? ; 50
	NumberOfTableEntries	DD ? ; 58H
	ConfigurationTable		DQ ? ; 60H
SYSTEM_TABLE ENDS

TEXT_OUTPUT_INTERFACE STRUCT
	Reset				DQ	?
	OutputString		DQ	?
	TestString			DQ	?
	QueryMode			DQ	?
	SetMode				DQ	?
	SetAttribute		DQ	?
	ClearScreen			DQ	?
	SetCursorPosition	DQ	?
	EnableCursor		DQ	?
	Mode				DQ	?
TEXT_OUTPUT_INTERFACE ENDS

With these structs defined, there are a few ways we now have access to access structured data. When using a register to access a field of a struct, I can specify the field offset in the MOV operation. The line of code looks like this.

[RDX + SYSTEM_TABLE.ConOut]

Adding that notation to the code, I end up with code that looks like this.

.CODE
main PROC
	SUB RSP, 10H*8	
	MOV RCX, [RDX + SYSTEM_TABLE.ConOut] ; Get ConOut function address
	LEA RDX, [szHelloWorld]
	CALL QWORD PTR [RCX + TEXT_OUTPUT_INTERFACE.OutputString] ;Output String
	
	MOV RCX, 01000000000H
delay_loop:
	DEC RCX
	JNZ delay_loop
	ADD RSP, 10H*8	
	XOR RAX, RAX
	RET
main ENDP

Reading Text

For someone that wants to play with this, it may also be helpful to be able to read text from the keyboard. Just as there is a Console Output object, there is also a Console Input object. I’ll have the code wait in a spin-loop until a key is pressed. Then it will print the key that was pressed, delay a bit, and terminate. The UEFI boot services do offer a function that will wait on system events. A key press counts as a system event. But I will stick with a spin-wait for simplicity.

I’m declaring a new procedure named waitForKey. This procedure uses a system object that implements the TEXT_INPUT_PROTOCOL. The object has the method ReadKeyStroke that communicates either that there is no keystroke available (sets the RAX register to a non-zero value) or that there is a keystroke (sets RAX register to zero) and writes the keyboard scan code and Unicode character to the memory address that it received in the RDX register. My code loops while RAX is set to non-zero.

.DATA
     szKeyRead DW ' '
.CODE
waitForKey PROC
		SUB RSP, 8
	waitForEnter_Retry:
		MOV RCX, [systemTable]
		MOV RCX, [RCX + SYSTEM_TABLE.ConIn]
		MOV RDX, RSP
		CALL QWORD PTR [ECX+TEXT_INPUT_PROTOCOL.ReadKeyStroke]
		CMP EAX, 0
		JNZ waitForEnter_Retry
		MOV AX, WORD PTR [RSP+2]
		MOV WORD PTR [szKeyRead], AX
		ADD RSP, 8
		RET
waitForKey ENDP

I’ll put the code necessary to print a string in a procedure, too. It will be called printString. The address to the zero-terminated string must be passed in the RAX register.

printString PROC
		MOV RCX, [systemTable]
		MOV RCX, [RCX + SYSTEM_TABLE.ConOut]
		MOV RDX, RAX
		CALL QWORD PTR [RCX+TEXT_OUTPUT_INTERFACE.OutputString]
		RET
printString ENDP

The code will now wait on user input before terminating.

Downloading the Code

If you want to try this out, the Visual Studio project and source code are available on GitHub. In that repository there is also a build folder that contains the binary. If you want to try it out, copy it to the path efi/BOOT on a FAT32 formatted USB drive and boot from it.

Other Resources

I used VMWare for a test device. It is available for free download from the VMWare Workstation web site. For development, I used Microsoft Visual Studio 2022. It is available for free from the Microsoft Visual Studio website. Information about the various objects that are available for use in UEFI code can be found on the site UEFI.org,


Posts may contain products with affiliate links. When you make purchases using these links, we receive a small commission at no extra cost to you. Thank you for your support.

Mastodon: @j2inet@masto.ai
Instagram: @j2inet
Facebook: @j2inet
YouTube: @j2inet
Telegram: j2inet
Bluesky: @j2i.net

Junctions, Hard Links, Symbolic Links on the Windows File System

On windows, the command line tool mklink is used to create symbolic links, junctions, and hard links. But what are those? I’ll first mention a couple of scenarios where they may be helpful. Let’s say that you have a content-driven system. You have multiple versions of your content sets on the file system. Each complete set is in its own folder. The software itself uses the content in a folder named current. When your content syncing applicate gets done downloading a new content set, there are several ways to make it available to the software that is using it. The method I want to focus on is having a virtual folder named current that is actually a pointer to the real folder. A variation of this need is having different versions of an SDK installed. To change from one SDK to another, there could be multiple environment variables that must be updated to point from one SDK version to another. This can be simplified by having a folder that is actually a pointer to the directory that must be used.

Switching from abstract to actual for a moment, I’ve got a couple of versions of the Java Card SDK installed. I just installed the latest version, but I want to keep the previous version around for a while. I’ve got a javacard folder as the root folder of all of the other software used for Java Card development. In it, there are junctions named tools and simulator to point to the Java Card folders for the command line tools and the Java Card simulator. If I need to switch between versions, I only need to delete the old junctions and create new ones.

Arguments for mklink

The arguments to the command are as follows.

mklink[[/j] | [/d] | [/h]] <link> <target>

  • /j – Create a directory junction
  • /d – Create a directory symbolic link
  • /h – Create a hard link (for files only)

Understanding of hard links and junctions requires understanding of the underlying file system. hard links refer to files while junctions refer to directories. Beyond that, they do the same thing. For a hard link or junction, two entries on the file allocation table point to the same inode entries. Symlinks are more like pointers that hold information on the original file system entry. Hard links and junctions can only refer to files on the same file system, while can symlinks refer to a file on a different file system. If no arguments are passed to mklink it will assume that you are making a file symlink.

Command Line Examples

What follows scenarios and the associated commands for those scenarios.

Create a new junction named tools that points to c:\bin\tools_v3.5

mklink/j tools c:\bin\tools_v3.5

Delete a junction named tools.

rd tools

Create a hard link named readme.txt to a file named c:\data\readme_23.txt

mklink /h readme.txt c:\data\readme_23.txt

Delete the hard link for readme.txt.

del readme.txt

What if I Want to Do This with an API Function?

The Win32 API also makes this functionality available to you through the function CreateSymbolicLink and DeviceIoControl.

CreateSymbolicLink

The arguments for the function reflect the arguments used by the command line tool.

BOOLEAN CreateSymbolicLinkW(
  [in] LPCWSTR lpSymlinkFileName,
  [in] LPCWSTR lpTargetFileName,
  [in] DWORD   dwFlags
);

The flag here can be one of three values.

ValueMeaning
0The target is a file
SYMBOLIC_LINK_FLAG_DIRECTORY (0x01)The target is a directory
SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE(0x02)Non-Elevated Create

DeviceIoControl

DeviceIoControl is used for a lot of different functionality. The details of using it in this specific use case may be worthy of its own post. For the sake of brevity, I won’t cover it here. But I’ll mention a few things about using it. When using it to make a junction, the following struct would be used. Note that this struct is a union. The union members that you would use for making a junction to a directory are in the MountPointReparseBuffer.

typedef struct _REPARSE_DATA_BUFFER {
  ULONG  ReparseTag;
  USHORT ReparseDataLength;
  USHORT Reserved;
  union {
    struct {
      USHORT SubstituteNameOffset;
      USHORT SubstituteNameLength;
      USHORT PrintNameOffset;
      USHORT PrintNameLength;
      ULONG  Flags;
      WCHAR  PathBuffer[1];
    } SymbolicLinkReparseBuffer;
    struct {
      USHORT SubstituteNameOffset;
      USHORT SubstituteNameLength;
      USHORT PrintNameOffset;
      USHORT PrintNameLength;
      WCHAR  PathBuffer[1];
    } MountPointReparseBuffer;
    struct {
      UCHAR DataBuffer[1];
    } GenericReparseBuffer;
  } DUMMYUNIONNAME;
} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;

Administrative Level Needed for Non-Developers

This functionality usually requires administrative level privileges to execute. However, if a machine has developer mode enabled, the function can be invoked without administrative level privileges. The mklint command line tool appears to follow the same rule. Running this on my own systems (which have developer mode enabled) I can create links without administrative privileges. If you are creating links with a Win32 API call, remember to set the flag SYMBOLIC_LINK_ALLOW_UNPRIVILEGED_CREATE.


Posts may contain products with affiliate links. When you make purchases using these links, we receive a small commission at no extra cost to you. Thank you for your support.

Mastodon: @j2inet@masto.ai
Instagram: @j2inet
Facebook: @j2inet
YouTube: @j2inet
Telegram: j2inet
Bluesky: @j2i.net

Converting between .Net DateTime and JavaScript Date Ticks

Representations of time can differ significantly between programming languages. I recently mixed some .Net code and JavaScript code and had to make some conversions between time representations. This code is generally useful and is being placed here for those who find themselves looking for a quick conversion. Let’s jump into the code.

const long DOTNET_JAVASCRIPT_EPOCH_DIFFERENCE = 621_355_968_000_000_000;
static long DotNetDateToJavaScriptTicks(DateTime d)
{
    return (d.Ticks - DOTNET_JAVASCRIPT_EPOCH_DIFFERENCE) / 10_000;
}

static DateTime JavaScriptTicksToDotNetDate(long ticks)
{
    long dticks = ticks * 10_000 + DOTNET_JAVASCRIPT_EPOCH_DIFFERENCE;
    var retVal = new DateTime(dticks );
    return retVal;
}

To test that it was working, I converted from a .Net time to JavaScript ticks and then back to .Net time If all went well, then I should end up with the same time that I started with.

var originalDotNetTime = DateTime.Now.Date.AddHours(15).AddHours(4).AddMinutes(0);
var javaScriptTicks = DotNetDateToJavaScriptTicks(originalDotNetTime) ;
var convertedDotNetTime = JavaScriptTicksToDotNetDate(javaScriptTicks);

if(originalDotNetTime == convertedDotNetTime)
{
    Console.WriteLine("Time Conversion Successful!");
}
else
{
    Console.WriteLine("The conversion was unsuccessful");
}

I ran the code, and it worked! Honestly, it didn’t work the first time because I left a 0 off of 10,000. Adding the underscores (_) to the numbers makes discovering such mistakes easier. Were you to use this code in AWS, note that some values in AWS, such as a TTL field on a DynamoDB table, expect values to be in seconds, not milliseconds. The JavaScript ticks value would have to be divided by 1000 when converted from a .Net time or multiplied by 1000 when being converted back to a .Net time.


Posts may contain products with affiliate links. When you make purchases using these links, we receive a small commission at no extra cost to you. Thank you for your support.

Mastodon: @j2inet@masto.ai
Instagram: @j2inet
Facebook: @j2inet
YouTube: @j2inet
Telegram: j2inet
Bluesky: @j2i.net

Rediscovering Pi Pico Programming with an IR Detector

I’ve used a Pi Pico before. But it has been a while, and I decided to jump back into it in furtherance of some other project I want to do. I’m specifically using a Pico W on a Freenove breakout board. The nice thing about this board is that all the GPIOs have status LEDs that lets you monitor the state of each GPIO visually. For those that might have immediate concern, the LEDs are connected to the GPIO via hex inverters instead of directly. This minimizes the interaction that they may have with devices that you connect to them.

Blinking the Light

About the first program that one might try with any micro controller is to blink a light. I accomplished that part without issue. But for those that are newer to this, I’ll cover in detail. Though I won’t cover the steps of setting up the SDK.

I’ve made a folder for my project. Since I plan to evolve this project to work with an infrared detector, I called my project folder irdetect. I’ve made two files in this folder.

  • CMakeList.txt – the build configuration file for the project
  • main.cpp – the source code for the project

For the CMakeList.txt file, I’ve specified that I’m using the C++ 23 standard. This configuration also informs the make process that main.cpp is the source file, and that the target executable name will be irdetect.

cmake_minimum_required(VERSION 3.13)

include(pico_sdk_import.cmake)

project(test_project C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 23) #Latest C__ Standard available
pico_sdk_init()

add_executable(irdetect
   main.cpp
)

pico_enable_stdio_usb(irdetect 1)
pico_enable_stdio_uart(irdetect 1)
pico_add_extra_outputs(irdetect)

The initial source code for blinking a LED is alternating the state of a random GPIO pin. Since I’m using a breakout board with LEDs for all the pins, I am not restricted to one pin. For the pin I selected, it is necessary to call gpio_init() for the pin, and then set its direction to output through gpio_set_dir(). If you don’t do this, then attempts to write to the pen will fail (speaking from experience!).

#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "pico/binary_info.h"
#include "pico/cyw43_arch.h"


const uint LED_DELAY_MS = 250; //quarter second
#ifdef PICO_DEFAULT_LED_PIN
const uint LED_PIN = PICO_DEFAULT_LED_PIN;
#else
const uint LED_PIN = 15;
#endif


// Initialize the GPIO for the LED
void pico_led_init(void) {
	gpio_init(LED_PIN);
	gpio_set_dir(LED_PIN, GPIO_OUT);
}

// Turn the LED on or off
void pico_set_led(bool led_on) {
	gpio_put(LED_PIN, led_on);
}

int main()
{
	stdio_init_all();
	pico_led_init();

	while(true)
	{
		pico_set_led(true);
		sleep_ms(LED_DELAY_MS);
		pico_set_led(false);
		sleep_ms(LED_DELAY_MS);
	}
	return 0;
}

To compile this, I made a subfolder named build inside of my project folder. I’m using a Pico W. When I compile the code, I specify the Pico board that I’m using.

cd build
cmake .. -DPICO_BOARD=pico_w
make

Some output flies by on the screen, after which build files have been deposited into the folder. the one of interest is irdetect.u2f. I need to flash the Pico with this. The process is extremely easy. Hold down the reset button on the Pico while connecting it to the Pi. It will show up as a mass storage device. Copying the file to the device will cause it to flash and then reboot. The device is automatically mounted to the file system. In my case, this is to the path /media/j2inet/RPI-RP2

cp irdetect.u2f /media/j2inet/RPI-RP2

I tried this out, and the light blinks. I’m glad output works, but now to try input.

Reading From a Pin

I want the program to now start off blinking a light until it detects an input. When it does, I want it to switch to a different mode where the output reflects the input. In the updated source I initialize an addition pin and use gpio_set_dir to set the pin as an input pin. I set an additional pin to output as a convenience. I need a positive line to drive the input high. I could use the voltage pin with a resistor, but I found it more convenient to set another GPIO to high and use it as my positive source for now.

#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "pico/binary_info.h"
#include "pico/cyw43_arch.h"


const uint LED_DELAY_MS = 50;
#ifdef PICO_DEFAULT_LED_PIN
const uint LED_PIN = PICO_DEFAULT_LED_PIN;
#else
const uint LED_PIN = 15;
#endif
const uint IR_READ_PIN = 14;
const uint IR_DETECTOR_ENABLE_PIN = 13;


// Initialize the GPIO for the LED
void pico_led_init(void) {
        gpio_init(LED_PIN);
        gpio_set_dir(LED_PIN, GPIO_OUT);

        gpio_init(IR_READ_PIN);
        gpio_set_dir(IR_READ_PIN, GPIO_IN);

        gpio_init(IR_DETECTOR_ENABLE_PIN);
        gpio_set_dir(IR_DETECTOR_ENABLE_PIN, GPIO_OUT);
}

// Turn the LED on or off
void pico_set_led(bool led_on) {
        gpio_put(LED_PIN, led_on);
}

int main()
{
        stdio_init_all();
        pico_led_init();
        bool irDetected = false;
        gpio_put(IR_DETECTOR_ENABLE_PIN, true);
        while(!irDetected)
        {
                irDetected = gpio_get(IR_READ_PIN);
                pico_set_led(true);
                sleep_ms(LED_DELAY_MS);
                pico_set_led(false);
                sleep_ms(LED_DELAY_MS);
        }

        while(true)
        {
                bool p = gpio_get(IR_READ_PIN);
                gpio_put(LED_PIN, p);
                sleep_us(10);
        }
        return 0;
}

When I run this program and manually set the pin to high with a resistor tied to an input, it works fine. My results were not the same when I tried using an IR detector.

Adding an IR Detector

I have two IR detectors. One is an infrared photoresistor diode. This component has a high resistance until it is struck with infrared light. When it is, it becomes low resistance. Placing that component in the circuit, I see the output pin go from low to high when I illuminate the diode with an IR flashlight or aim a remote control at it. Cool.

I tried again with a VS138B. This is a three pin IC. Two of the pins supply it with power. The third pin is an output pin. This IC has a IR detector, but instead of detecting the presence of IR light, it detects the presence of a pulsating IR signal provided that the pulsing is within a certain frequency band. The IC is primarily for detecting signals sent on a 38KHz carrier. I connected this to my Pico and tried it out. The result was no response. I can’t find my logic probe, but I have an osciloscope. Attaching it to the output pin, I detected no signal. What gives?

This is where I searched on the Internet to find the likely problem and solutions. I found other people with similar circuits and problems, but no solutions. I then remembered reading something else about the internal pull-up resistors in Arduinos. I grabbed a resistor and connected my input pin to a pin with a high signal and tried again. It worked! The VS138B signals by pulling the output pin to a low voltage. I went to Bluesky and posted about my experience.

https://bsky.app/profile/j2i.net/post/3lgar7brqfs2n

Someone quickly pointed out to me that there are pull-up resistors in the Pi Pico. I just must turn them on with a function call.

those can be activated at runtime:gpio_pull_up (PIN_NUMBER);This works even for the I2C interface.

Abraxolotlpsylaxis (@abraxolotlpsylaxis.bsky.social) 2025-01-21T12:18:18.964Z

I updated my code, and it works! When I attach the detector to the scope, I also see the signal now.

Now that I can read, the next step is to start decoding a remote signal. Note that there are already libraries for doing this. I won’t be using one (yet) since my primary interest here is diving a bit further into the Pico. But I do encourage the use of a third-party library if you are aiming to just get something working with as little effort as possible.

Code Repository

While you could copy the code from above, if you want to grab the code for this, it is on GitHub at the URL https://github.com/j2inet/irdetect/. Note that with time, the code might transition to something that no longer resembles what was mentioned in this post.


Posts may contain products with affiliate links. When you make purchases using these links, we receive a small commission at no extra cost to you. Thank you for your support.

Mastodon: @j2inet@masto.ai
Instagram: @j2inet
Facebook: @j2inet
YouTube: @j2inet
Telegram: j2inet
Bluesky: @j2i.net