BradCypert.com
Building a Soil Moisture Sensor in TinyGo with Arduino

Building a Soil Moisture Sensor in TinyGo with Arduino

Posted on May 6, 2025  •  4 minutes  • 773 words
This project uses these versions of languages, frameworks, and libraries.
  • go go: tinygo 0.37.0
This tutorial may work with newer versions and possibly older versions, but has only been tested on the versions mentioned above.

Building a Soil Moisture Sensor in TinyGo with Arduino

If you're a Go developer and want to dip your toes into the world of hardware, I’ve got good news: you can absolutely build cool stuff—like a soil moisture sensor—without switching to C or C++. Thanks to TinyGo , you can write Go code for microcontrollers like the Arduino. And yes, it’s as fun and straightforward as it sounds.

In this post, I’ll walk you through building a soil moisture sensor that reads data and displays it on an LCD screen—all written in Go using TinyGo.

This post is available as an in-depth video tutorial too! Check it out here:

Why TinyGo?

Microcontroller development is traditionally done in C or C++, but for many of us who love Go, this can feel like stepping back in time. TinyGo changes the game by compiling Go programs to run on small devices. It supports a growing number of microcontrollers and comes with a solid standard library for embedded development.

If you're already familiar with Go, this gives you a huge head start. You get familiar syntax, strong typing, and tooling you already know how to use.

What You’ll Need

Here’s the hardware setup I used:

Software:

Wiring It Up

/IMG_3290.JPG

To help you visualize the connections, here's an overview:

Arduino <=> LCD
GND <----> GND
+5V <----> VCC
SDA <----> A4
SCL <----> A5

Arduino <=> Soil Moisture Sensor
AOUT <----> A0
VCC <-----> 5V
GND <-----> GND

Project Setup

Create a new directory to hold your code. In that directory, run go mod init and go get tinygo.org/x/drivers/hd44780i2c. I'm going to shar eall the source code here, but I'll highlight some key pieces in just a second. For now, read through it all.

Source Code

package main

import (
	"machine"
	"strconv"
	"time"

	"tinygo.org/x/drivers/hd44780i2c"
)

const led = machine.LED

func main() {
	println("Hello, TinyGo")
	machine.InitADC()

	i2c := machine.I2C0
	err := i2c.Configure(machine.I2CConfig{})
	if err != nil {
		println("could not configure I2C:", err)
		return
	}

	lcd := hd44780i2c.New(machine.I2C0, 0x27)
	lcd.Configure(hd44780i2c.Config{
		Width:  16,
		Height: 2,
	})

	sensor := machine.ADC{machine.ADC0}
	sensor.Configure(machine.ADCConfig{})

	for {
		val := sensor.Get()
		lcd.ClearDisplay()
		if val < 16500 {
			lcd.Print([]byte("Soil is fine\n" + strconv.Itoa(int(val))))
		} else {
			lcd.Print([]byte("Soil needs water\n" + strconv.Itoa(int(val))))
		}
		time.Sleep(time.Millisecond * 1000)
	}
}

Breaking the Code Apart

We're going to use this section to step through the interesting blocks of code piece by piece.

machine.InitADC()

This line initializes the analog to digital converter. If you're using Analog pins, you're going to want to initialize this.

  i2c := machine.I2C0
	err := i2c.Configure(machine.I2CConfig{})
	if err != nil {
		println("could not configure I2C:", err)
		return
	}

	lcd := hd44780i2c.New(machine.I2C0, 0x27)
	lcd.Configure(hd44780i2c.Config{
		Width:  16,
		Height: 2,
	})

This block of code gets a handle on the I2C0 layer, configures that layer using default values and then passes the I2C0 to our hd44780i2c library. I2C is short for "Inter-integrated circuit" and includes the SDA and SCL ports. SDA is short for Serial Data and SCL is short for Serial Clock. You can find more information on I2C, SDA, and SCL here , but for now, you know everything you need to continue this tutorial.

sensor := machine.ADC{machine.ADC0}
sensor.Configure(machine.ADCConfig{})

These lines configure a reference to our sensor, which is plugged into the ADC0 PIN. We'll recieve data over this pin on an interval and act on this data in just a moment.

  for {
		val := sensor.Get()
		lcd.ClearDisplay()
		if val < 16500 {
			lcd.Print([]byte("Soil is fine\n" + strconv.Itoa(int(val))))
		} else {
			lcd.Print([]byte("Soil needs water\n" + strconv.Itoa(int(val))))
		}
		time.Sleep(time.Millisecond * 1000)
	}

And here's that moment! We're setting up a for loop that runs indefinitely (as long as the machine is powered on). In this loop, we're getting a reading from our sensor and then we're clearing the connected LCD display. Again, the hd44780i2c library makes short work of this for us. Then, we check if the value is lower than 16500 (an arbitrary number that I picked based on a sensor reading after dipping my sensor partially in a cup of water) -- if it is lower, than we print to the LCD screen including the current sensor value. The line break ensures that the sensor reading is placed on the second line of our LCD.

The else condition is more-or-less the same, we just change the message that is printed. Finally, we sleep for 1 second and then let the for loop repeat.

Cartoon headshot of Brad Cypert
Follow me

Connect with me to follow along on my journey in my career, open source, and mentorship. Occasionally, I'll share good advice and content (quality not guaranteed).