2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/public/
|
||||||
|
.hugo_build.lock
|
29
.woodpecker.yml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
platform: linux/amd64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
build:
|
||||||
|
image: hugomods/hugo:go-git
|
||||||
|
commands:
|
||||||
|
- hugo
|
||||||
|
- tar czvf site.tar.gz public/
|
||||||
|
|
||||||
|
upload:
|
||||||
|
image: plugins/s3
|
||||||
|
settings:
|
||||||
|
endpoint: https://api.minio.elara.ws
|
||||||
|
path_style: true
|
||||||
|
bucket: site
|
||||||
|
access_key: AkdgUdmzEJBoiYa2
|
||||||
|
secret_key:
|
||||||
|
from_secret: minio_secret_key
|
||||||
|
source: site.tar.gz
|
||||||
|
target: /
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
image: loq9/drone-nomad
|
||||||
|
settings:
|
||||||
|
addr: http://192.168.100.62:4646
|
||||||
|
template: template.nomad
|
||||||
|
environment:
|
||||||
|
- PLUGIN_WATCH_DEPLOYMENT=true
|
||||||
|
- PLUGIN_WATCH_DEPLOYMENT_TIMEOUT=10m
|
5
README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Site
|
||||||
|
|
||||||
|
[![status-badge](https://ci.elara.ws/api/badges/Elara6331/site/status.svg)](https://ci.elara.ws/Elara6331/site)
|
||||||
|
|
||||||
|
This is the git repository for my personal site at https://www.elara.ws/.
|
6
archetypes/default.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
title: "{{ replace .Name "-" " " | title }}"
|
||||||
|
date: {{ .Date }}
|
||||||
|
draft: true
|
||||||
|
---
|
||||||
|
|
1
assets/icons/gitea.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path fill="currentColor" d="M5.583 7.229C3.119 7.224-.172 8.786.01 12.708c.281 6.125 6.557 6.693 9.068 6.745c.271 1.146 3.224 5.109 5.411 5.318h9.573c5.74-.38 10.036-17.365 6.854-17.427c-5.271.25-8.396.375-11.073.396v5.297l-.839-.365l-.005-4.932c-3.073 0-5.781-.141-10.917-.396c-.646-.005-1.542-.115-2.5-.115zm.344 2.167h.297c.349 3.141.917 4.974 2.068 7.781c-2.938-.349-5.432-1.198-5.891-4.38c-.24-1.646.563-3.365 3.526-3.401zm11.412 3.083c.198.005.406.042.594.13l1 .432l-.714 1.302a.989.989 0 0 0-.323.052c-.464.151-.708.604-.542 1.021a.843.843 0 0 0 .151.229l-1.234 2.25a.841.841 0 0 0-.297.052c-.464.146-.708.604-.542 1.016c.172.417.682.63 1.151.479c.464-.146.703-.604.536-1.021a.834.834 0 0 0-.208-.292l1.203-2.188c.13.01.26 0 .391-.042a.805.805 0 0 0 .281-.151c.464.198.844.354 1.12.49c.406.203.552.339.599.49c.042.146-.005.427-.24.922c-.172.37-.458.896-.797 1.51c-.115 0-.229.016-.333.052c-.469.151-.708.604-.542 1.021c.167.411.682.625 1.146.479c.469-.151.708-.604.542-1.021a.862.862 0 0 0-.182-.271c.333-.609.62-1.135.807-1.526c.25-.536.38-.938.266-1.323s-.469-.635-.932-.865c-.307-.151-.693-.313-1.146-.505c.005-.109-.01-.214-.052-.318s-.109-.198-.193-.281l.703-1.281l3.901 1.682c.703.307.995 1.057.651 1.682l-2.682 4.906c-.339.625-1.182.885-1.885.578l-5.516-2.38c-.703-.307-.995-1.057-.656-1.682l2.682-4.906c.234-.432.708-.688 1.208-.708h.083z"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/img/consul_star64.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
assets/img/nebula.jpg
Normal file
After Width: | Height: | Size: 111 KiB |
BIN
assets/img/nomad_star64.png
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
assets/img/woodpecker-agent-star64.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
assets/img/woodpecker-riscv64-pcre.png
Normal file
After Width: | Height: | Size: 142 KiB |
BIN
assets/logo.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
13
config/_default/config.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# -- Site Configuration --
|
||||||
|
# Refer to the theme docs for more details about each of these parameters.
|
||||||
|
# https://jpanther.github.io/congo/docs/getting-started/
|
||||||
|
|
||||||
|
baseURL = "https://elara.ws/"
|
||||||
|
defaultContentLanguage = "en"
|
||||||
|
|
||||||
|
enableRobotsTXT = true
|
||||||
|
paginate = 10
|
||||||
|
summaryLength = 0
|
||||||
|
|
||||||
|
[outputs]
|
||||||
|
home = ["HTML", "RSS", "JSON"]
|
23
config/_default/languages.en.toml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
title = "Elara"
|
||||||
|
#description = "My awesome website"
|
||||||
|
copyright = "Copyright © Elara6331"
|
||||||
|
|
||||||
|
[languages.en.params]
|
||||||
|
languageCode = "en"
|
||||||
|
languageName = "English"
|
||||||
|
displayName = "EN"
|
||||||
|
dateFormat = "January 2, 2006"
|
||||||
|
isoCode = "en"
|
||||||
|
weight = 1
|
||||||
|
rtl = false
|
||||||
|
|
||||||
|
[params.author]
|
||||||
|
name = "Elara6331"
|
||||||
|
image = "img/nebula.jpg"
|
||||||
|
headline = "Software Engineer"
|
||||||
|
bio = "I'm a FOSS developer who loves writing code and solving problems"
|
||||||
|
links = [
|
||||||
|
{ email = "mailto:elara@elara.ws" },
|
||||||
|
{ gitea = "https://gitea.elara.ws/Elara6331" },
|
||||||
|
{ github = "https://github.com/Elara6331" },
|
||||||
|
]
|
13
config/_default/markup.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# -- Markup --
|
||||||
|
# These settings are required for the theme to function.
|
||||||
|
|
||||||
|
[goldmark]
|
||||||
|
[goldmark.renderer]
|
||||||
|
unsafe = true
|
||||||
|
|
||||||
|
[highlight]
|
||||||
|
noClasses = false
|
||||||
|
|
||||||
|
[tableOfContents]
|
||||||
|
startLevel = 2
|
||||||
|
endLevel = 4
|
40
config/_default/menus.en.toml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# -- Main Menu --
|
||||||
|
# The main menu is displayed in the header at the top of the page.
|
||||||
|
# Acceptable parameters are name, pageRef, page, url, title, weight.
|
||||||
|
#
|
||||||
|
# The simplest menu configuration is to provide:
|
||||||
|
# name = The name to be displayed for this menu link
|
||||||
|
# pageRef = The identifier of the page or section to link to
|
||||||
|
#
|
||||||
|
# By default the menu is ordered alphabetically. This can be
|
||||||
|
# overridden by providing a weight value. The menu will then be
|
||||||
|
# ordered by weight from lowest to highest.
|
||||||
|
|
||||||
|
[[main]]
|
||||||
|
name = "Home"
|
||||||
|
url = "/"
|
||||||
|
weight = 10
|
||||||
|
|
||||||
|
[[main]]
|
||||||
|
name = "About"
|
||||||
|
pageRef = "about"
|
||||||
|
weight = 20
|
||||||
|
|
||||||
|
[[main]]
|
||||||
|
name = "Articles"
|
||||||
|
pageRef = "articles"
|
||||||
|
weight = 30
|
||||||
|
|
||||||
|
# -- Footer Menu --
|
||||||
|
# The footer menu is displayed at the bottom of the page, just before
|
||||||
|
# the copyright notice. Configure as per the main menu above.
|
||||||
|
|
||||||
|
[[footer]]
|
||||||
|
name = "Source Code"
|
||||||
|
url = "https://gitea.elara.ws/Elara6331/site"
|
||||||
|
weight = 40
|
||||||
|
|
||||||
|
[[footer]]
|
||||||
|
name = "Donate"
|
||||||
|
url = "https://liberapay.com/Elara6331/"
|
||||||
|
weight = 50
|
2
config/_default/module.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[[imports]]
|
||||||
|
path = "github.com/jpanther/congo/v2"
|
73
config/_default/params.toml
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# -- Theme Options --
|
||||||
|
# These options control how the theme functions and allow you to
|
||||||
|
# customise the display of your website.
|
||||||
|
#
|
||||||
|
# Refer to the theme docs for more details about each of these parameters.
|
||||||
|
# https://jpanther.github.io/congo/docs/configuration/#theme-parameters
|
||||||
|
|
||||||
|
colorScheme = "ocean"
|
||||||
|
defaultAppearance = "dark" # valid options: light or dark
|
||||||
|
autoSwitchAppearance = false
|
||||||
|
|
||||||
|
enableSearch = true
|
||||||
|
enableCodeCopy = true
|
||||||
|
|
||||||
|
# mainSections = ["section1", "section2"]
|
||||||
|
# robots = ""
|
||||||
|
|
||||||
|
|
||||||
|
[header]
|
||||||
|
layout = "hamburger" # valid options: basic, hamburger, custom
|
||||||
|
logo = "/logo.png"
|
||||||
|
|
||||||
|
[footer]
|
||||||
|
showCopyright = true
|
||||||
|
showThemeAttribution = true
|
||||||
|
showAppearanceSwitcher = false
|
||||||
|
showScrollToTop = true
|
||||||
|
|
||||||
|
[homepage]
|
||||||
|
layout = "profile" # valid options: page, profile, custom
|
||||||
|
showRecent = false
|
||||||
|
|
||||||
|
[article]
|
||||||
|
showDate = true
|
||||||
|
showDateUpdated = false
|
||||||
|
showAuthor = false
|
||||||
|
showBreadcrumbs = true
|
||||||
|
showDraftLabel = true
|
||||||
|
showEdit = false
|
||||||
|
# editURL = "https://github.com/username/repo/"
|
||||||
|
editAppendPath = true
|
||||||
|
showHeadingAnchors = true
|
||||||
|
showPagination = true
|
||||||
|
invertPagination = false
|
||||||
|
showReadingTime = true
|
||||||
|
showTableOfContents = true
|
||||||
|
showTaxonomies = true
|
||||||
|
showWordCount = false
|
||||||
|
showComments = false
|
||||||
|
# sharingLinks = ["facebook", "twitter", "pinterest", "reddit", "linkedin", "email"]
|
||||||
|
|
||||||
|
[list]
|
||||||
|
showBreadcrumbs = false
|
||||||
|
showSummary = true
|
||||||
|
showTableOfContents = false
|
||||||
|
groupByYear = false
|
||||||
|
showTaxonomies = true
|
||||||
|
|
||||||
|
[sitemap]
|
||||||
|
excludedKinds = ["taxonomy", "term"]
|
||||||
|
|
||||||
|
[taxonomy]
|
||||||
|
showTermCount = true
|
||||||
|
|
||||||
|
[fathomAnalytics]
|
||||||
|
# site = "ABC12345"
|
||||||
|
# domain = "llama.yoursite.com"
|
||||||
|
|
||||||
|
[verification]
|
||||||
|
# google = ""
|
||||||
|
# bing = ""
|
||||||
|
# pinterest = ""
|
||||||
|
# yandex = ""
|
15
content/_index.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
+++
|
||||||
|
title = "Home"
|
||||||
|
description = "Homepage for Elara's Site"
|
||||||
|
slug = "home"
|
||||||
|
+++
|
||||||
|
|
||||||
|
{{<button href="/about">}}
|
||||||
|
About Me
|
||||||
|
{{</button>}}
|
||||||
|
|
||||||
|
<span style="margin-left: 5px;">
|
||||||
|
{{<button href="/articles">}}
|
||||||
|
Articles
|
||||||
|
{{</button>}}
|
||||||
|
</span>
|
23
content/about.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
+++
|
||||||
|
title = "About"
|
||||||
|
description = "About Me"
|
||||||
|
showDate = false
|
||||||
|
showPagination = false
|
||||||
|
slug = "about"
|
||||||
|
showTableOfContents = false
|
||||||
|
+++
|
||||||
|
|
||||||
|
## Hello, I'm Elara!
|
||||||
|
|
||||||
|
I'm a passionate free and open source software developer who thrives on the challenge of problem solving in its myriad forms. Whether it's bringing a physical device to life using code, crafting elaborate web applications, or maintaining physical and cloud infrastructure, I have experience in the entire software lifecycle.
|
||||||
|
|
||||||
|
My skill set is very diverse, spanning embedded and systems programming, backend and frontend development, system administration, DevOps, and much more.
|
||||||
|
|
||||||
|
Some of my achievements include:
|
||||||
|
|
||||||
|
- Maintaining a complex, automated server cluster that powers everything in my online world and provides free services to anyone who needs them ([link](https://gitea.elara.ws/Elara6331/nomad))
|
||||||
|
- Building a distribution and build system for Linux software ([link](https://lure.sh))
|
||||||
|
- Designing a custom templating language for Go ([link](https://gitea.elara.ws/Elara6331/salix/))
|
||||||
|
- Writing software to connect the open source [PineTime](https://pine64.org/devices/pinetime/) smartwatch to any Linux system ([link](https://gitea.elara.ws/Elara6331/itd))
|
||||||
|
|
||||||
|
I've also made significant contributions to many well-known open source projects, including [GoReleaser](https://github.com/goreleaser), [Fyne](https://github.com/fyne-io), [TinyGo](https://github.com/tinygo-org), [InfiniTime](https://github.com/InfiniTimeOrg/InfiniTime), and [Lemmy](https://github.com/LemmyNet/lemmy).
|
4
content/articles/_index.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
+++
|
||||||
|
title = "Articles"
|
||||||
|
showPosts = false
|
||||||
|
+++
|
108
content/articles/milkv-duo.md
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
+++
|
||||||
|
title = "Experimenting with the Milk-V Duo RISC-V Single Board Computer"
|
||||||
|
date = "2023-12-12"
|
||||||
|
summary = "My experiments with the Milk-V Duo and how I wrote a Linux GPIO library in Zig"
|
||||||
|
tags = ["milk-v", "duo", "risc-v", "zig"]
|
||||||
|
+++
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
I recently got a few [Milk-V Duo](https://milkv.io/duo) boards to play with and thought I'd share my experience with them here.
|
||||||
|
|
||||||
|
The boards have a similar form factor to the Raspberry Pi Pico, but they run a full Linux distribution rather than custom firmware like the pico.
|
||||||
|
|
||||||
|
The first thing I wanted to try was using the GPIO pins just like you would on a pico or any microcontroller. The way Milk-V expects you to do this is using their modified version of the `wiringX` library, which directly writes to memory mapped registers.
|
||||||
|
|
||||||
|
That's not the recommended way to access GPIO on Linux because it's error-prone and insecure. Instead, Linux provides special interfaces to control GPIO.
|
||||||
|
|
||||||
|
There's the deprecated sysfs interface which you use by writing text to files in `/sys`, for example, `echo 440 > /sys/class/gpio/export` would allow you to access pin 440. This interface has several problems. For one, there's no way to know if a program is currently using the pin, so you could end up with two programs trying to control the same pin. Also, when the programs finish, they have to manually unexport the pins, and if they don't, they'll stay exported even after the program exits.
|
||||||
|
|
||||||
|
That problem was solved by the character device interface. Linux provides character device files at `/dev/gpiochipX`, on which you can use special `ioctl` commands, in order to control the GPIO pins. Once a program closes the file or exits, Linux automatically releases all the GPIO lines it was using.
|
||||||
|
|
||||||
|
Later, it was decided that the API was inadequate, so there's a v2 character device API, which is what I wanted to use. The recommended way to do that is using `libgpiod`, which is a C library that invokes the various syscalls for you and provides a clean API.
|
||||||
|
|
||||||
|
However, that didn't sound fun. I'd just be importing a C library, and besides, I was really bored of using C and wanted to learn a new low-level language, like [Zig](https://ziglang.org/). So, I decided that in order to learn Zig, I'd implement a GPIO library in it as my first project. A bit complicated for a first project, but very fun.
|
||||||
|
|
||||||
|
## Writing the GPIO library
|
||||||
|
|
||||||
|
I started by searching for examples of using the character device API. Unfortunately, what little information I could find was for the deprecated v1 API which, to be fair, still works, but I wanted to use the v2 API, so I decided to read the kernel's source code to figure out how these ioctls worked. Specifically, I found the [`gpio.h`](https://github.com/torvalds/linux/blob/v5.10/include/uapi/linux/gpio.h#L500) file in Linux kernel 5.10 (the first version that implements the v2 API).
|
||||||
|
|
||||||
|
That file has very good descriptions of what the various structs and fields are meant to be used for, but it doesn't provide any actual examples of how to use the API. I decided to just try it anyway.
|
||||||
|
|
||||||
|
I started by rewriting each struct as a Zig `extern` struct. In the process, I learned a lot of interesting stuff about Zig, such as `packed` structs, for example, which make bit sets really easy to implement because each `bool` is stored as one bit, so you can just have a struct of `bool` fields that will act as a bit set.
|
||||||
|
|
||||||
|
I also learned a lot about Zig's type system as I had to do things like accept an unknown number of offsets to request from the chip, use enums, unions, etc.
|
||||||
|
|
||||||
|
Eventually, I finished that and started implementing the actual syscalls. The kernel represents ioctls as macros, such as `_IOWR(0xB4, 0x05, struct gpio_v2_line_info)`. This macro contains the ioctl type (`0xB4`), the ioctl number (`0x05`), and the type which gets sent or received (`struct gpio_v2_line_info`). The kernel uses all of these in order to calculate an ioctl ID that you use when running the syscall.
|
||||||
|
|
||||||
|
Zig provides functions to do the same thing. For example, the `_IOWR` macro above becomes `std.os.linux.IOCTL.IOWR(0xB4, 0x05, LineInfo)`. I used that to implement each of the ioctls implemented by the gpio interface.
|
||||||
|
|
||||||
|
Doing this gave me a low-level base upon which I could build my public API.
|
||||||
|
|
||||||
|
However, I later found out that I misinterpreted the API. Requesting GPIO lines requires offsets, while operations on GPIO lines, such as setting the values or config, requires indices. An index corresponds to the index of the offset in the request. For example, if you request the offsets `[22 20 21]`, then `22` will be `0`, `20` will be `1`, and `21` will be `2`.
|
||||||
|
|
||||||
|
Because I didn't realize this, my GPIO library wasn't working, so I decided to compile a Go program using the [gpiod library](https://github.com/warthog618/gpiod) and then used `strace` to analyze the syscalls it made and see how they were different from mine. That's when I noticed that it was using indices rather than offsets. Once I fixed that, my library finally started working, so I finished writing it, set up zig package manager so it could be used by others, and published it.
|
||||||
|
|
||||||
|
Here are the links to my Zig GPIO library:
|
||||||
|
|
||||||
|
- Gitea: https://gitea.elara.ws/Elara6331/zig-gpio
|
||||||
|
- GitHub: https://github.com/Elara6331/zig-gpio
|
||||||
|
|
||||||
|
## Using `zig-gpio` with the Milk-V Duo
|
||||||
|
|
||||||
|
The first thing I wanted to try was to make the Duo's LED blink, which is like the "Hello World" of GPIO projects.
|
||||||
|
|
||||||
|
To do that, I needed to know which chip controlled the LED and what offset it was at. Unfortunately, there's no documentation for that. However, there is an example script that uses the deprecated sysfs interface to blink the LED. That script uses pin `440` as the LED, which I correlated with `gpiochip` values in sysfs to determine that the LED was offset 22 on `/dev/gpiochip2`.
|
||||||
|
|
||||||
|
I tried it, and it worked! The LED was blinking using entirely my code, which made me really excited. (I'm probably the only person in the world who gets excited over a blinking LED)
|
||||||
|
|
||||||
|
So now I needed the rest of the GPIO offsets so I could do some more testing
|
||||||
|
|
||||||
|
### Finding the Milk-V Duo GPIO offsets
|
||||||
|
|
||||||
|
Since Milk-V doesn't provide any documentation about the offsets, I had to look further. I found the [GPIO Operation Guide](https://doc.sophgo.com/cvitek-develop-docs/master/docs_latest_release/CV180x_CV181x/en/01.software/OSDRV/Peripheral_Driver_Operation_Guide/build/html/7_GPIO_Operation_Guide.html) from the manufacturer of the chip that the Duo used, which let me know how to figure out the GPIO offsets and which chip they belong to.
|
||||||
|
|
||||||
|
To do that, I had to look at the Duo's [schematic](https://github.com/milkv-duo/duo-files/blob/main/hardware/duo/duo-schematic-v1.2.pdf), which had the prefixes `GPIO`, `GPIOA`, `GPIOC`, and `PWR_GPIO`.
|
||||||
|
|
||||||
|
I assumed `GPIOA` would correspond to `gpiochip0`, so I tried it with offset 28, which corresponds to `GP0` on the official Duo pinout. Sure enough, my multimeter showed that it was blinking.
|
||||||
|
|
||||||
|
Next, I assumed that `GPIOC` would correspond to `gpiochip2` (since `C` is the third letter), and tried that, and it worked again!
|
||||||
|
|
||||||
|
Since 0 and 2 are known, I assumed `GPIO` with no letter would be `gpiochip1`. However, when I tried it, nothing happened, so I decided to check `gpiochip4`, and that did work. It turns out `PWR_GPIO` is also `gpiochip4`, so I'm not sure why there's a difference in the naming.
|
||||||
|
|
||||||
|
Anyway, I used this to calculate all the GPIO offsets, which I'll provide below for anyone who wants to use them.
|
||||||
|
|
||||||
|
### GPIO offset table
|
||||||
|
|
||||||
|
| Pin Name | Offset | Chip |
|
||||||
|
|------------|--------|-----------|
|
||||||
|
| GP0 | 28 | gpiochip0 |
|
||||||
|
| GP1 | 29 | gpiochip0 |
|
||||||
|
| GP2 | 26 | gpiochip4 |
|
||||||
|
| GP3 | 25 | gpiochip4 |
|
||||||
|
| GP4 | 19 | gpiochip4 |
|
||||||
|
| GP5 | 20 | gpiochip4 |
|
||||||
|
| GP6 | 23 | gpiochip4 |
|
||||||
|
| GP7 | 22 | gpiochip4 |
|
||||||
|
| GP8 | 21 | gpiochip4 |
|
||||||
|
| GP9 | 18 | gpiochip4 |
|
||||||
|
| GP10 | 9 | gpiochip2 |
|
||||||
|
| GP11 | 10 | gpiochip2 |
|
||||||
|
| GP12 | 16 | gpiochip0 |
|
||||||
|
| GP13 | 17 | gpiochip0 |
|
||||||
|
| GP14 | 14 | gpiochip0 |
|
||||||
|
| GP15 | 15 | gpiochip0 |
|
||||||
|
| GP16 | 23 | gpiochip0 |
|
||||||
|
| GP17 | 24 | gpiochip0 |
|
||||||
|
| GP18 | 22 | gpiochip0 |
|
||||||
|
| GP19 | 25 | gpiochip0 |
|
||||||
|
| GP20 | 27 | gpiochip0 |
|
||||||
|
| GP21 | 26 | gpiochip0 |
|
||||||
|
| GP22 | 4 | gpiochip4 |
|
||||||
|
| GP25 (LED) | 22 | gpiochip2 |
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
The Milk-V Duo is a really fun board to play with, and I encourage anyone who's interested in Linux and embedded programming to get one and try it out. You'll probably learn a lot and it'll be a lot of fun!
|
||||||
|
|
||||||
|
They cost only $9, and you can usually find them for as low as $5. All you need is a USB-C cable and a microSD card. No serial adapter or debugger is required (though a serial adapter is nice to have for troubleshooting boot issues).
|
70
content/articles/nordic-dfu-ble.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
+++
|
||||||
|
title = "Nordic DFU over BLE"
|
||||||
|
date = "2023-07-14"
|
||||||
|
summary = "Using Nordic Semiconductor's DFU protocol over Bluetooth Low Energy"
|
||||||
|
tags = ["pine64", "pinetime", "ble", "itd"]
|
||||||
|
+++
|
||||||
|
|
||||||
|
I maintain a project called [ITD](https://gitea.elara.ws/Elara6331/itd), a companion app for Pine64's [PineTime](https://www.pine64.org/pinetime/) smartwatch. The PineTime uses a Nordic nRF52832 SoC and implements the Nordic DFU protocol. I found the documentation for this protocol on Nordic's site to be severely lacking and had to resort to digging through source code to implement it properly. In this article, I'll explain exactly how to upgrade your firmware over BLE using the information that I've learned.
|
||||||
|
|
||||||
|
## Concepts
|
||||||
|
|
||||||
|
### Control Point characteristic
|
||||||
|
|
||||||
|
All DFU commands will be written to the control point characteristic. Responses will be received as notifications. The ID of the control point characteristic is `00001531-1212-efde-1523-785feabcd123`.
|
||||||
|
|
||||||
|
### Packet characteristic
|
||||||
|
|
||||||
|
The packet characteristic is where the data for the firmware upgrade will be sent, such as the initialization packet and the firmware image itself. The ID of the packet characteristic is `00001532-1212-efde-1523-785feabcd123`
|
||||||
|
|
||||||
|
### Segment Size and Receipt Interval
|
||||||
|
|
||||||
|
The segment size is the size of each firmware packet sent to the packet characteristic. The receipt interval is the amount of packets that will be sent before a receipt packet is returned. The maximum segment size is 20 bytes, and the optimal receipt interval has been experimentally determined to be 10.
|
||||||
|
|
||||||
|
### Receipt Packet
|
||||||
|
|
||||||
|
Every time the receipt interval is reached, a receipt packet is sent as a notification on the control point characteristic. This packet always starts with an opcode (`0x11`), followed by a little-endian uint32 encoding the total size in bytes of the firmware image that has been received. You can compare this with the amount of bytes you've sent to make sure that no packets have been lost.
|
||||||
|
|
||||||
|
## Upgrade Process
|
||||||
|
|
||||||
|
All integers used in the process will be encoded little-endian
|
||||||
|
|
||||||
|
### Preparation
|
||||||
|
|
||||||
|
To prepare for a firmware upgrade, start by enabling notifications on the control point characteristic so that you can receive responses from the device you're upgrading.
|
||||||
|
|
||||||
|
### Initialization
|
||||||
|
|
||||||
|
To start the firmware upgrade, write the start command (`[0x01 0x04]`) to the control point.
|
||||||
|
|
||||||
|
Next, you'll need to write the size of the firmware image in bytes to the *packet characteristic*. The size packet includes three sizes: one for the SoftDevice, one for the bootloader, and one for the firmware. Since we're just upgrading the firmware in this case, we only need to worry about the last number and the rest can be set to 0. Each size is a uint32. That means the packet should contain 8 zeros followed by 4 bytes encoding the size of the firmware. For example, if your firmware image is 412488 bytes, your size packet might look like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
[0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0xB8 0x62 0x06 0x00]
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the size packet has been sent, wait for your device to send the successful start response (`[0x10 0x01 0x01]`) as a notification on the control point.
|
||||||
|
|
||||||
|
Once the start response has been received, you'll need to send the initialization packet. This should be included in a DFU zip package as a `.dat` file. Start by writing `[0x02 0x00]` to the control point, which tells your device that you're about to write the init packet. Then, write the contents of the init packet to the *packet characteristic*, and then write `[0x02 0x01]` to the control point, which signals that you've finished writing the packet. Now, you'll need to wait for the device to process the init packet and send back `[0x10 0x02 0x01]`, indicating that the packet was accepted.
|
||||||
|
|
||||||
|
Next, set the receipt interval by writing `[0x08 <interval>]` to the control point. If you use the optimal interval mentioned in this article (10), your command will be `[0x08 0x0A]`.
|
||||||
|
|
||||||
|
Your device should now be ready to receive the new firmware!
|
||||||
|
|
||||||
|
### Flashing the firmware
|
||||||
|
|
||||||
|
Start by writing `[0x03]` to the control point, which tells your device that you're about to send the firmware image.
|
||||||
|
|
||||||
|
Now comes the interesting part: actually sending the firmware image. Split the image into 20-byte chunks and write each chunk one at a time to the *packet characteristic*. Every time you write 10 packets (or whatever you've set your receipt interval to), wait for the device to process them and send back a notification starting with `0x11` on the control point. That notification is a receipt packet. Verify that the size it returns matches the amount of bytes you've sent. If it does, you can continue writing more packets. If not, some packets were lost and you'll need to restart the process.
|
||||||
|
|
||||||
|
Once you've finished sending the firmware image, wait for the watch to send `[0x10 0x03 0x01]` on the control point. That indicates that the firmware has been received successfully.
|
||||||
|
|
||||||
|
### Finishing
|
||||||
|
|
||||||
|
Once the firmware has been successfully received, you'll need to activate it and reset the device.
|
||||||
|
|
||||||
|
First, write `[0x04]` to the control point. This tells your device to validate the firmware and make sure it matches the CRC in the init packet. If this is successful, you should receive `[0x10 0x04 0x01]` from the control point.
|
||||||
|
|
||||||
|
Once the firmware is successfully validated, write `[0x05]` to the control point, which tells your device to activate the new firmware and reset.
|
||||||
|
|
||||||
|
That's it! Your device should reboot running the new firmware. Some firmware (such as InfiniTime for the PineTime) requires you to manually validate the firmware in their settings or they'll revert on the next reboot, so make sure to remember to do that if your device requires it.
|
252
content/articles/riscv-cluster.md
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
+++
|
||||||
|
title = "Running HashiCorp Nomad and Consul on RISC-V"
|
||||||
|
date = "2023-08-04"
|
||||||
|
summary = "Discussing the process of getting Nomad and Consul running on a RISC-V computer and adding it to a cluster"
|
||||||
|
tags = ["pine64", "star64", "risc-v", "cluster"]
|
||||||
|
+++
|
||||||
|
|
||||||
|
I run a cluster of single board computer servers from my house. It mostly consists of `aarch64` machines with a few old `x86_64` machines I had lying around for the occasional service that doesn't run on ARM.
|
||||||
|
|
||||||
|
I've been interested in RISC-V for a while. I love the fact that it's an open ISA unlike x86 and ARM. I believe RISC-V has a lot of potential and I've wanted to develop software for it for a while.
|
||||||
|
|
||||||
|
Unfortunately, up until pretty recently, there hasn't been very much useful RISC-V hardware to use that wasn't prohibitively expensive. However, there has recently been a wave of new RISC-V SBCs, so I decided to try adding one to my cluster.
|
||||||
|
|
||||||
|
I decided to go with the [Star64](https://wiki.pine64.org/wiki/STAR64) from Pine64, mainly because I'm familiar with Pine64 and I'm already part of their developer community with my ITD project. I also wanted to play around with some of the features it has that other JH7110 boards don't, such as the built-in WiFi and Bluetooth and the PCIe slot.
|
||||||
|
|
||||||
|
## Getting things ready
|
||||||
|
|
||||||
|
My cluster runs [Nomad](https://www.nomadproject.io/) and [Consul](https://www.consul.io/) with the Docker driver, which means Nomad, Consul, and Docker must be installed on all nodes. That's usually pretty simple with Hashicorp's and Docker's APT repo, but their repos don't have any builds for `riscv64`. That's a pretty common occurence because of how new Linux on RISC-V is. Unfortunately, that means I'll have to build each of those projects from source or find them elsewhere.
|
||||||
|
|
||||||
|
Before that though, I have to choose a distro to run. Looking at the [Software Releases section](https://wiki.pine64.org/wiki/STAR64#Software_releases) in Pine64's wiki, there aren't very many options. As of the time of writing this article, the only options on the page are Yocto-based images, Armbian, and NixOS. Since all my nodes run Debian, I wanted to go with a Debian-based image, so I looked at Armbian. Unfortunately, it doesn't have a maintainer at the moment and the images are broken. Out of the two remaining options, I chose to go with the Yocto images instead of NixOS because they use `apt` like Debian, so I wouldn't have to use different package management commands for all my servers.
|
||||||
|
|
||||||
|
While I waited for my Star64 to arrive, I got excited and decided to work on getting Nomad and Consul ready for it early.
|
||||||
|
|
||||||
|
### Consul
|
||||||
|
|
||||||
|
I started with Consul because its docs have [instructions](https://developer.hashicorp.com/consul/docs/install#compiling-from-source) for cross-compiling and it seems to only require setting the `GOOS` and `GOARCH` environment variables.
|
||||||
|
|
||||||
|
So, I cloned the consul repo and ran
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make GOOS=linux GOARCH=riscv64 dev
|
||||||
|
```
|
||||||
|
|
||||||
|
It downloaded some dependencies and started building, but eventually it returned some errors:
|
||||||
|
|
||||||
|
```text
|
||||||
|
# github.com/boltdb/bolt
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/db.go:101:13: undefined array length maxMapSize or missing type constraint
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/db.go:317:12: undefined: maxMapSize
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/db.go:335:10: undefined: maxMapSize
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/db.go:336:8: undefined: maxMapSize
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/db.go:795:2: pos declared and not used
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/bolt_unix.go:62:15: undefined array length maxMapSize or missing type constraint
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/bucket.go:135:15: undefined: brokenUnaligned
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/freelist.go:166:2: idx declared and not used
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/freelist.go:169:19: undefined array length maxAllocSize or missing type constraint
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/freelist.go:176:14: undefined array length maxAllocSize or missing type constraint
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/freelist.go:166:2: too many errors
|
||||||
|
make: *** [Makefile:163: dev-build] Error 1
|
||||||
|
```
|
||||||
|
|
||||||
|
The first thing I noticed is that these errors are for the `github.com/boltdb/bolt` package, which is unmaintained and doesn't have support for `riscv64`. However, Consul switched to a maintained fork with RISC-V support a while ago, so it was another dependency causing problems. I used `go mod why github.com/boltdb/bolt` to identify the culprit, which led me to `github.com/hashicorp/raft-boltdb`, so I went to its repo and it also switched to the maintained fork, but it still depended on boltdb in order to migrate old bolt databases to the new format. I submitted a [pull request](https://github.com/hashicorp/raft-boltdb/pull/37) to disable that functionality on unsupported platforms.
|
||||||
|
|
||||||
|
As of the time I'm writing this, the PR isn't merged yet, so I had to force Consul to use my fork. I did that by adding this replace directive to Consul's `go.mod` file, under the `go 1.20` directive:
|
||||||
|
|
||||||
|
```text
|
||||||
|
replace github.com/hashicorp/raft-boltdb/v2 => github.com/Elara6331/raft-boltdb/v2 v2.0.0-20230729002801-1a3bff1d87a7
|
||||||
|
```
|
||||||
|
|
||||||
|
Then I ran the make command again, and this time it compiled successfully, but I got a different error:
|
||||||
|
|
||||||
|
```text
|
||||||
|
cp: cannot stat '/home/elara/.go/bin/consul': No such file or directory
|
||||||
|
```
|
||||||
|
|
||||||
|
I noticed that Consul's makefile ran `go install` and then copied the resulting binary to the destination rather than just writing it directly to the destination. This is the command it ran:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CGO_ENABLED=0 go install -ldflags "-X github.com/hashicorp/consul/version.GitCommit=449e050741+CHANGES -X github.com/hashicorp/consul/version.BuildDate=2023-07-28T16:49:23Z " -tags ""
|
||||||
|
```
|
||||||
|
|
||||||
|
So, I decided to modify that command so that it would build the binary instead of installing it, which resulted in the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -ldflags "-X github.com/hashicorp/consul/version.GitCommit=449e050741+CHANGES -X github.com/hashicorp/consul/version.BuildDate=2023-07-28T16:49:23Z " -tags "" -o ./bin/consul
|
||||||
|
```
|
||||||
|
|
||||||
|
Once that command completed, I looked in the `bin` directory and found a successfully-built RISC-V Consul binary.
|
||||||
|
|
||||||
|
Since I don't have my star64 yet, I had to test the binary using CPU emulation, so I used `qemu-user-static` and ran `./bin/consul version`, which returned:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Consul v1.17.0-dev
|
||||||
|
Revision 449e050741+CHANGES
|
||||||
|
Build Date 2023-07-28T16:49:23Z
|
||||||
|
Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents)
|
||||||
|
```
|
||||||
|
|
||||||
|
That means the binary is working properly!
|
||||||
|
|
||||||
|
### Nomad
|
||||||
|
|
||||||
|
Nomad can be cross-compiled as well, but it doesn't include any insructions in its documentation.
|
||||||
|
|
||||||
|
I cloned the nomad repo and followed the regular build instructions but with the `GOOS` and `GOARCH` variable set to target `riscv64`.
|
||||||
|
|
||||||
|
First, I ran the command that installs all the build depenedencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make GOOS=linux GOARCH=riscv64 bootstrap
|
||||||
|
```
|
||||||
|
|
||||||
|
That worked, so I moved on to the build command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make GOOS=linux GOARCH=riscv64 dev
|
||||||
|
```
|
||||||
|
|
||||||
|
After a while though, I got some errors:
|
||||||
|
|
||||||
|
```text
|
||||||
|
# runtime/cgo
|
||||||
|
gcc_riscv64.S: Assembler messages:
|
||||||
|
gcc_riscv64.S:17: Error: no such instruction: `sd x1,-200(sp)'
|
||||||
|
gcc_riscv64.S:18: Error: no such instruction: `addi sp,sp,-200'
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
These errors are from the assembler, and they indicate that Go tried to use the wrong C compiler for the target platform. This happened because Nomad has some C dependencies for things like process monitoring. It's pretty easily fixed though, I just needed to install a RISC-V cross-compiler and point Go to it. I run Arch Linux, so I just installed the [`riscv64-linux-gnu-gcc`](https://archlinux.org/packages/extra/x86_64/riscv64-linux-gnu-gcc/) package and then changed the command to point Go to it, like so:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make GOOS=linux GOARCH=riscv64 CC=/usr/bin/riscv64-linux-gnu-gcc CXX=/usr/bin/riscv64-linux-gnu-c++ AR=/usr/bin/riscv64-linux-gnu-gcc-ar dev
|
||||||
|
```
|
||||||
|
|
||||||
|
When I ran that though, I got some more errors:
|
||||||
|
|
||||||
|
```text
|
||||||
|
# github.com/boltdb/bolt
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/db.go:101:13: undefined array length maxMapSize or missing type constraint
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/db.go:317:12: undefined: maxMapSize
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/db.go:335:10: undefined: maxMapSize
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/db.go:336:8: undefined: maxMapSize
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/db.go:795:2: pos declared and not used
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/bolt_unix.go:62:15: undefined array length maxMapSize or missing type constraint
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/bucket.go:135:15: undefined: brokenUnaligned
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/freelist.go:166:2: idx declared and not used
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/freelist.go:169:19: undefined array length maxAllocSize or missing type constraint
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/freelist.go:176:14: undefined array length maxAllocSize or missing type constraint
|
||||||
|
../../../.go/pkg/mod/github.com/boltdb/bolt@v1.3.1/freelist.go:166:2: too many errors
|
||||||
|
make[1]: *** [GNUmakefile:93: pkg/linux_riscv64/nomad] Error 1
|
||||||
|
make: *** [GNUmakefile:262: dev] Error 2
|
||||||
|
```
|
||||||
|
|
||||||
|
These are the same errors consul returned for the boltdb package, so I just added the same replace directive and ran it again, and this time it built successfully!
|
||||||
|
|
||||||
|
I tried running it like I did with consul, using `qemu-user-static` for CPU emulation, but I got the following error:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ ./bin/nomad version
|
||||||
|
qemu-riscv64-static: Could not open '/lib/ld-linux-riscv64-lp64d.so.1': No such file or directory
|
||||||
|
```
|
||||||
|
|
||||||
|
This error indicates that the binary tried to load a `riscv64` linker library, but qemu couldn't find it. That's because Arch's `riscv64-linux-gnu-glibc` package installs the linker to `/usr/riscv64-linux-gnu/lib` rather than the usual `/lib`. So, to fix that, I just told qemu where to find the linker, and the binary worked!
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ QEMU_LD_PREFIX=/usr/riscv64-linux-gnu/ ./bin/nomad version
|
||||||
|
Nomad v1.6.2-dev
|
||||||
|
BuildDate 2023-07-28T18:53:32Z
|
||||||
|
Revision 9e98d694a6230b904f931813b7d53622e9f128c9+CHANGES
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
Luckily, someone else has already successfully run Docker on RISC-V and they've documented the process in their [github repo](https://github.com/carlosedp/riscv-bringup). They even provided a `.tar.gz` archive in the releases, which means I won't have to spend time getting Docker to build (thanks [@carlosedp](https://github.com/carlosedp)!).
|
||||||
|
|
||||||
|
## Running the stuff I just set up
|
||||||
|
|
||||||
|
It's been about a week and my Star64 is finally here. Now that everything is ready, it's time for the interesting part: actually running all of this on the node and seeing if it can properly join my cluster.
|
||||||
|
|
||||||
|
### Consul
|
||||||
|
|
||||||
|
Starting with Consul, I used `scp` to copy the binary I built to the `/usr/bin` directory on my Star64. Then, I copied the systemd services and configs in `.release/linux/package` to their corresponding directories and created a system user for consul using `useradd -r consul`.
|
||||||
|
|
||||||
|
I edited the configs to match the rest of my cluster and started consul with `sudo systemctl start consul`. A few seconds later, I saw this in my Consul dashboard:
|
||||||
|
|
||||||
|
![Screenshot of Star64 in Consul dashboard](/img/consul_star64.png)
|
||||||
|
|
||||||
|
which means Consul is working!
|
||||||
|
|
||||||
|
### Nomad
|
||||||
|
|
||||||
|
Just like with Consul, I used `scp` to copy the relevant files to the Star64, edited the configs, and started the Nomad service. Nomad doesn't need its own user, so no `useradd` was required.
|
||||||
|
|
||||||
|
A few seconds later, the Star64 joined the cluster and appeared in my dashboard!
|
||||||
|
|
||||||
|
![Screenshot of Star64 in Nomad dashboard](/img/nomad_star64.png)
|
||||||
|
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
Docker is going to be more complex to install. I started similarly to Consul and Nomad: I downloaded the `.tar.gz`, extracted it, copied all the files inside to their proper locations, and added a group for it using `groupadd -r docker`. However, when I tried to start docker, I got some errors about my kernel's cgroup support, so I decided to check whether my kernel had the proper config options enabled for Docker.
|
||||||
|
|
||||||
|
To do that, I downloaded moby's [check-config.sh](https://github.com/moby/moby/blob/master/contrib/check-config.sh) script, which checks your kernel config to make sure it supports docker. I found that several required options were missing. That meant I had to compile a custom kernel, so I looked into how Yocto worked and built a new kernel. I won't get into that process here because it could be a whole separate article, but anyway, eventually I had some `.deb` packages with the new kernel. I installed those, rebooted, ran the script again, and this time, all the required options were enabled.
|
||||||
|
|
||||||
|
So now, I tried starting docker again and this time there was no error, so I tested it by running the `hello-world` container, and sure enough, it worked!
|
||||||
|
|
||||||
|
```text
|
||||||
|
root@star64:~/docker# docker run hello-world
|
||||||
|
|
||||||
|
Hello from Docker!
|
||||||
|
This message shows that your installation appears to be working correctly.
|
||||||
|
|
||||||
|
To generate this message, Docker took the following steps:
|
||||||
|
1. The Docker client contacted the Docker daemon.
|
||||||
|
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
|
||||||
|
(riscv64)
|
||||||
|
3. The Docker daemon created a new container from that image which runs the
|
||||||
|
executable that produces the output you are currently reading.
|
||||||
|
4. The Docker daemon streamed that output to the Docker client, which sent it
|
||||||
|
to your terminal.
|
||||||
|
|
||||||
|
To try something more ambitious, you can run an Ubuntu container with:
|
||||||
|
$ docker run -it ubuntu bash
|
||||||
|
|
||||||
|
Share images, automate workflows, and more with a free Docker ID:
|
||||||
|
https://hub.docker.com/
|
||||||
|
|
||||||
|
For more examples and ideas, visit:
|
||||||
|
https://docs.docker.com/get-started/
|
||||||
|
```
|
||||||
|
|
||||||
|
I've opened an [issue](https://github.com/Fishwaldo/meta-pine64/issues/13) for this and published the modified kernel packages [here](https://api.minio.elara.ws/risc-v/star64-kernel-debs.tar.gz) if anyone wants to try them.
|
||||||
|
|
||||||
|
## Using the node for something
|
||||||
|
|
||||||
|
Now that I've got the node added to my cluster, it's time to actually make it run a task. I'm going to be running a [Woodpecker CI](https://woodpecker-ci.org) agent on it so that I can test and build my software on RISC-V hardware.
|
||||||
|
|
||||||
|
### Setting up Woodpecker
|
||||||
|
|
||||||
|
Luckily, woodpecker's official [server](https://hub.docker.com/r/woodpeckerci/woodpecker-server) and [agent](https://hub.docker.com/r/woodpeckerci/woodpecker-agent) images both support RISC-V, so I don't need to change anything there. All I should need to do is add `woodpecker_agent = true` to my Nomad config file and restart Nomad, and it should start right up.
|
||||||
|
|
||||||
|
Sure enough, once I added the variable and restarted Nomad, Woodpecker Agent started immediately:
|
||||||
|
|
||||||
|
![Screenshot of Woodpecker Agent running on the Star64](/img/woodpecker-agent-star64.png)
|
||||||
|
|
||||||
|
### Running a CI job
|
||||||
|
|
||||||
|
Now that Woodpecker is running, I'm going to try running a CI job on it.
|
||||||
|
|
||||||
|
The job I'm going to run is the test job for my [pcre](https://gitea.elara.ws/Elara6331/pcre) library because it supports RISC-V and I'd like to try running its unit tests on actual RISC-V hardware.
|
||||||
|
|
||||||
|
My CI job is configured to run tests for `amd64` and `arm64`, so just add `riscv64` and it should work, right? Well, not quite. That job is using the official Go [docker image](https://hub.docker.com/_/golang) which doesn't have support for RISC-V, so I have to make my own image.
|
||||||
|
|
||||||
|
In order to do that, I made a new repo at [Elara6331/riscv-docker](https://gitea.elara.ws/Elara6331/riscv-docker) and added a custom dockerfile for Go that's based on the `alpine:edge` image, which does support RISC-V, and published the resulting image at [gitea.elara.ws/elara6331/golang](https://gitea.elara.ws/Elara6331/-/packages/container/golang/latest).
|
||||||
|
|
||||||
|
Then, I modified my CI config to run `riscv64` tests using the new image and pushed it. Here's the result:
|
||||||
|
|
||||||
|
![Screenshot of Woodpecker running a test job for PCRE on RISC-V](/img/woodpecker-riscv64-pcre.png)
|
||||||
|
|
||||||
|
The job runs successfully! You can see this result on my Woodpecker instance: https://ci.elara.ws/repos/49/pipeline/5.
|
||||||
|
|
||||||
|
That's pretty much it for this article, but I'm going to be doing lots of interesting stuff with this in the future and I'll be publishing more articles whenever I encounter anything interesting.
|
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module go.elara.ws/site
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require github.com/jpanther/congo/v2 v2.8.2 // indirect
|
6
go.sum
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
github.com/jpanther/congo/v2 v2.6.1 h1:iA8uosVsiMl3JbBSBwMvrxEibzBcDp+RIj18f/cmlDs=
|
||||||
|
github.com/jpanther/congo/v2 v2.6.1/go.mod h1:1S7DRoO1ZYS4YUdFd1LjTkdyjQwsjFWd8TqSfz3Jd+M=
|
||||||
|
github.com/jpanther/congo/v2 v2.7.6 h1:gBz+Zx6PIVgMhknn0t9uOUQA0sSBFKhH4Miox/q9xew=
|
||||||
|
github.com/jpanther/congo/v2 v2.7.6/go.mod h1:1S7DRoO1ZYS4YUdFd1LjTkdyjQwsjFWd8TqSfz3Jd+M=
|
||||||
|
github.com/jpanther/congo/v2 v2.8.2 h1:UNg7225ZqLSt9zu0xBOh5iM9TXnuNG1Ta9eSsioHTEE=
|
||||||
|
github.com/jpanther/congo/v2 v2.8.2/go.mod h1:1S7DRoO1ZYS4YUdFd1LjTkdyjQwsjFWd8TqSfz3Jd+M=
|
71
layouts/_default/rss.xml
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
{{- /* Deprecate site.Author.email in favor of site.Params.author.email */}}
|
||||||
|
{{- $authorEmail := "" }}
|
||||||
|
{{- with site.Params.author }}
|
||||||
|
{{- if reflect.IsMap . }}
|
||||||
|
{{- with .email }}
|
||||||
|
{{- $authorEmail = . }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- else }}
|
||||||
|
{{- with site.Author.email }}
|
||||||
|
{{- $authorEmail = . }}
|
||||||
|
{{- warnf "The author key in site configuration is deprecated. Use params.author.email instead." }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- /* Deprecate site.Author.name in favor of site.Params.author.name */}}
|
||||||
|
{{- $authorName := "" }}
|
||||||
|
{{- with site.Params.author }}
|
||||||
|
{{- if reflect.IsMap . }}
|
||||||
|
{{- with .name }}
|
||||||
|
{{- $authorName = . }}
|
||||||
|
{{- end }}
|
||||||
|
{{- else }}
|
||||||
|
{{- $authorName = . }}
|
||||||
|
{{- end }}
|
||||||
|
{{- else }}
|
||||||
|
{{- with site.Author.name }}
|
||||||
|
{{- $authorName = . }}
|
||||||
|
{{- warnf "The author key in site configuration is deprecated. Use params.author.name instead." }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- $pctx := . }}
|
||||||
|
{{- if .IsHome }}{{ $pctx = .Site }}{{ end }}
|
||||||
|
{{- $pages := slice }}
|
||||||
|
{{- if or $.IsHome $.IsSection }}
|
||||||
|
{{- $pages = $pctx.RegularPages }}
|
||||||
|
{{- else }}
|
||||||
|
{{- $pages = $pctx.Pages }}
|
||||||
|
{{- end }}
|
||||||
|
{{- $limit := .Site.Config.Services.RSS.Limit }}
|
||||||
|
{{- if ge $limit 1 }}
|
||||||
|
{{- $pages = $pages | first $limit }}
|
||||||
|
{{- end }}
|
||||||
|
{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
|
||||||
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
<channel>
|
||||||
|
<title>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{ . }} on {{ end }}{{ .Site.Title }}{{ end }}</title>
|
||||||
|
<link>{{ .Permalink }}</link>
|
||||||
|
<description>Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{ . }} {{ end }}{{ end }}on {{ .Site.Title }}</description>
|
||||||
|
<generator>Hugo -- gohugo.io</generator>
|
||||||
|
<language>{{ site.Language.LanguageCode }}</language>{{ with $authorEmail }}
|
||||||
|
<managingEditor>{{.}}{{ with $authorName }} ({{ . }}){{ end }}</managingEditor>{{ end }}{{ with $authorEmail }}
|
||||||
|
<webMaster>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}</webMaster>{{ end }}{{ with .Site.Copyright }}
|
||||||
|
<copyright>{{ . }}</copyright>{{ end }}{{ if not .Date.IsZero }}
|
||||||
|
<lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
|
||||||
|
{{- with .OutputFormats.Get "RSS" }}
|
||||||
|
{{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }}
|
||||||
|
{{- end }}
|
||||||
|
{{- range $pages }}
|
||||||
|
<item>
|
||||||
|
<title>{{ .Title }}</title>
|
||||||
|
<link>{{ .Permalink }}</link>
|
||||||
|
<pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
|
||||||
|
{{- with $authorEmail }}<author>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}</author>{{ end }}
|
||||||
|
<guid>{{ .Permalink }}</guid>
|
||||||
|
<description>{{ .Content | html }}</description>
|
||||||
|
</item>
|
||||||
|
{{- end }}
|
||||||
|
</channel>
|
||||||
|
</rss>
|
7
layouts/partials/favicons.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="/site.webmanifest">
|
||||||
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||||
|
<meta name="msapplication-TileColor" content="#ffed00">
|
||||||
|
<meta name="theme-color" content="#5bbad5">
|
1
layouts/shortcodes/raw.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
{{.Inner}}
|
4
layouts/shortcodes/spoiler.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<details>
|
||||||
|
<summary>{{.Get 0}}</summary>
|
||||||
|
{{.Inner | markdownify}}
|
||||||
|
</details>
|
After Width: | Height: | Size: 51 KiB |
After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 51 KiB |
After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 70 KiB |
After Width: | Height: | Size: 80 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 8.0 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 124 KiB |
After Width: | Height: | Size: 137 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 81 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 70 KiB |
After Width: | Height: | Size: 80 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 8.0 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 124 KiB |
After Width: | Height: | Size: 137 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 81 KiB |
BIN
static/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
static/android-chrome-384x384.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
static/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
9
static/browserconfig.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/mstile-150x150.png"/>
|
||||||
|
<TileColor>#ffed00</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
BIN
static/favicon-16x16.png
Normal file
After Width: | Height: | Size: 921 B |
BIN
static/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
static/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
82
static/lure.sh
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
info() {
|
||||||
|
echo $'\x1b[32m[INFO]\x1b[0m' $@
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo $'\x1b[31m[WARN]\x1b[0m' $@
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo $'\x1b[31;1m[ERR]\x1b[0m' $@
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
installPkg() {
|
||||||
|
rootCmd=""
|
||||||
|
if command -v doas &>/dev/null; then
|
||||||
|
rootCmd="doas"
|
||||||
|
elif command -v sudo &>/dev/null; then
|
||||||
|
rootCmd="sudo"
|
||||||
|
else
|
||||||
|
warn "No privilege elevation command (e.g. sudo, doas) detected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case $1 in
|
||||||
|
pacman) $rootCmd pacman --noconfirm -U ${@:2} ;;
|
||||||
|
apk) $rootCmd apk add --allow-untrusted ${@:2} ;;
|
||||||
|
*) $rootCmd $1 install -y ${@:2} ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! command -v curl &>/dev/null; then
|
||||||
|
error "This script requires the curl command. Please install it and run again."
|
||||||
|
fi
|
||||||
|
|
||||||
|
pkgFormat=""
|
||||||
|
pkgMgr=""
|
||||||
|
if command -v pacman &>/dev/null; then
|
||||||
|
info "Detected pacman"
|
||||||
|
pkgFormat="pkg.tar.zst"
|
||||||
|
pkgMgr="pacman"
|
||||||
|
elif command -v apt &>/dev/null; then
|
||||||
|
info "Detected apt"
|
||||||
|
pkgFormat="deb"
|
||||||
|
pkgMgr="apt"
|
||||||
|
elif command -v dnf &>/dev/null; then
|
||||||
|
info "Detected dnf"
|
||||||
|
pkgFormat="rpm"
|
||||||
|
pkgMgr="dnf"
|
||||||
|
elif command -v yum &>/dev/null; then
|
||||||
|
info "Detected yum"
|
||||||
|
pkgFormat="rpm"
|
||||||
|
pkgMgr="yum"
|
||||||
|
elif command -v zypper &>/dev/null; then
|
||||||
|
info "Detected zypper"
|
||||||
|
pkgFormat="rpm"
|
||||||
|
pkgMgr="zypper"
|
||||||
|
elif command -v apk &>/dev/null; then
|
||||||
|
info "Detected apk"
|
||||||
|
pkgFormat="apk"
|
||||||
|
pkgMgr="apk"
|
||||||
|
else
|
||||||
|
error "No supported package manager detected!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
latestVersion=$(curl -sI 'https://gitea.elara.ws/Elara6331/lure/releases/latest' | grep -io 'location: .*' | rev | cut -d '/' -f1 | rev | tr -d '[:space:]')
|
||||||
|
info "Found latest LURE version:" $latestVersion
|
||||||
|
|
||||||
|
fname="$(mktemp -u -p /tmp "lure.XXXXXXXXXX").${pkgFormat}"
|
||||||
|
url="https://gitea.elara.ws/Elara6331/lure/releases/download/${latestVersion}/linux-user-repository-${latestVersion#v}-linux-$(uname -m).${pkgFormat}"
|
||||||
|
|
||||||
|
info "Downloading LURE package"
|
||||||
|
curl -L $url -o $fname
|
||||||
|
|
||||||
|
info "Installing LURE package"
|
||||||
|
installPkg $pkgMgr $fname
|
||||||
|
|
||||||
|
info "Cleaning up"
|
||||||
|
rm $fname
|
||||||
|
|
||||||
|
info "Done!"
|
BIN
static/mstile-150x150.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
34
static/safari-pinned-tab.svg
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="500.000000pt" height="500.000000pt" viewBox="0 0 500.000000 500.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<metadata>
|
||||||
|
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||||
|
</metadata>
|
||||||
|
<g transform="translate(0.000000,500.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M58 4655 c-48 -27 -59 -63 -53 -168 8 -137 15 -244 20 -317 2 -36 7
|
||||||
|
-105 10 -155 3 -49 7 -121 10 -160 3 -38 7 -110 10 -160 4 -49 8 -117 10 -150
|
||||||
|
5 -77 15 -221 20 -310 10 -172 16 -252 22 -280 4 -16 7 -32 8 -35 12 -59 71
|
||||||
|
-164 121 -214 29 -29 124 -94 199 -136 314 -176 327 -187 327 -282 -1 -72 -22
|
||||||
|
-93 -202 -197 -80 -45 -160 -97 -179 -115 -75 -70 -108 -158 -98 -259 9 -85
|
||||||
|
416 -1299 447 -1332 51 -54 129 -69 188 -37 18 9 343 327 722 706 l690 690 0
|
||||||
|
-273 0 -273 34 -34 c34 -34 34 -34 130 -34 177 0 176 -3 176 347 l0 268 698
|
||||||
|
-697 c389 -389 709 -702 725 -708 59 -21 131 -4 174 42 18 19 54 95 63 133 4
|
||||||
|
15 11 38 21 65 5 14 11 34 14 45 3 11 82 247 176 525 169 498 171 506 173 595
|
||||||
|
1 80 -2 96 -26 140 -39 75 -78 108 -242 202 -81 48 -159 98 -172 112 -47 51
|
||||||
|
-46 138 1 193 11 13 110 75 220 138 243 140 300 189 354 304 46 96 47 103 66
|
||||||
|
401 3 50 8 119 10 155 2 36 7 108 10 160 3 52 7 115 10 140 2 25 7 97 10 160
|
||||||
|
4 63 8 131 10 150 2 19 6 94 10 165 3 72 8 141 10 155 12 81 15 256 5 280 -18
|
||||||
|
43 -60 68 -108 66 -40 -2 -72 -22 -152 -92 -14 -13 -106 -89 -205 -169 -99
|
||||||
|
-81 -198 -162 -220 -180 -159 -131 -963 -791 -1199 -984 -160 -130 -323 -268
|
||||||
|
-363 -307 l-73 -72 0 150 c0 166 -8 197 -59 218 -41 17 -179 17 -220 1 -53
|
||||||
|
-23 -61 -51 -61 -219 l0 -150 -67 67 c-38 36 -137 123 -222 191 -85 69 -159
|
||||||
|
130 -165 135 -6 6 -36 30 -66 55 -30 25 -60 49 -67 55 -6 5 -78 65 -160 131
|
||||||
|
-339 278 -570 468 -593 488 -14 12 -34 28 -46 36 -11 8 -43 34 -70 57 -27 23
|
||||||
|
-69 58 -94 78 -25 20 -65 53 -89 73 -71 60 -78 66 -113 94 -46 37 -284 232
|
||||||
|
-337 276 -24 21 -53 43 -64 51 -29 20 -88 23 -119 6z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
18
static/site.webmanifest
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"short_name": "",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-384x384.png",
|
||||||
|
"sizes": "384x384",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
53
template.nomad
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
job "site" {
|
||||||
|
region = "global"
|
||||||
|
datacenters = ["dc1"]
|
||||||
|
type = "service"
|
||||||
|
|
||||||
|
group "site" {
|
||||||
|
count = 2
|
||||||
|
|
||||||
|
network {
|
||||||
|
port "http" {
|
||||||
|
to = 8000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task "webserver" {
|
||||||
|
driver = "docker"
|
||||||
|
|
||||||
|
env {
|
||||||
|
// Hack to force Nomad to re-deploy the service
|
||||||
|
// instead of ignoring it
|
||||||
|
COMMIT_SHA = "${DRONE_COMMIT_SHA}"
|
||||||
|
}
|
||||||
|
|
||||||
|
config {
|
||||||
|
image = "gitea.elara.ws/elara6331/webserver:latest"
|
||||||
|
ports = ["http"]
|
||||||
|
volumes = ["local/site/public:/html:ro"]
|
||||||
|
}
|
||||||
|
|
||||||
|
artifact {
|
||||||
|
source = "https://api.minio.elara.ws/site/site.tar.gz"
|
||||||
|
destination = "local/site"
|
||||||
|
}
|
||||||
|
|
||||||
|
service {
|
||||||
|
name = "site"
|
||||||
|
port = "http"
|
||||||
|
|
||||||
|
tags = [
|
||||||
|
"traefik.enable=true",
|
||||||
|
|
||||||
|
"traefik.http.middlewares.site-redir.redirectRegex.regex=^https://elara\\.ws",
|
||||||
|
"traefik.http.middlewares.site-redir.redirectRegex.replacement=https://www.elara.ws",
|
||||||
|
"traefik.http.middlewares.site-redir.redirectRegex.permanent=true",
|
||||||
|
|
||||||
|
"traefik.http.routers.site.rule=Host(`elara.ws`) || Host(`www.elara.ws`)",
|
||||||
|
"traefik.http.routers.site.middlewares=site-redir",
|
||||||
|
"traefik.http.routers.site.tls.certResolver=letsencrypt",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|